mirror of
https://github.com/OrcaSlicer/OrcaSlicer.git
synced 2026-08-02 15:52:14 +00:00
feat(plugin)!: faithful mutable geometry bindings; edit slices via the class API
Replaces the plugin-only set_slices/set_fill_surfaces/set_lslices mutators with a faithful, mutable binding of the core geometry types, so a plugin edits the slicing graph through the same object model the C++ code uses. - Point, Polygon, ExPolygon, Surface and SurfaceCollection gain constructors, writable accessors (contour/holes, set/append/clear, filter_by_type), transforms (rotate/scale/translate), boolean ops and offset. Polygon exposes a zero-copy writable numpy view via a make_writable_rows helper. - LayerRegion.slices/fill_surfaces stay read-only refs but are now live, in-place-editable SurfaceCollections; Layer.make_slices() re-derives the islands and refreshes lslice bounding boxes. - Rewrites the Inset and Twistify samples on the new API (in-place ExPolygon transforms, ExPolygon.offset, SurfaceCollection.set), dropping their numpy dependency; each touched layer calls make_slices() so downstream steps see the edited footprint. Adds tests covering in-place edits through a live collection. BREAKING CHANGE: set_slices/set_fill_surfaces/set_lslices and the internal parse_expolygon(_list)/surfaces_from_py helpers are removed. Plugins mutate through the class API (SurfaceCollection.set/append/clear, Polygon.set_points/append, ExPolygon.set_holes) instead.
This commit is contained in:
@@ -1,81 +1,35 @@
|
||||
# /// 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 = "0.01"
|
||||
# version = "0.02"
|
||||
# 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
|
||||
LayerRegion.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.
|
||||
sliced surface by INSET_MM using a real polygon offset (ExPolygon.offset) and
|
||||
writes the result back with SurfaceCollection.set(). After the per-region edits,
|
||||
layer.make_slices() re-derives the layer's merged islands (lslices) so
|
||||
overhang/bridge detection, skirt/brim and support stay coherent with the inset
|
||||
geometry. At Step.Slice the split slice loop runs make_perimeters() right after
|
||||
the hook, so the change cascades into perimeters, infill and the final G-code
|
||||
-- the toolpath preview 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) 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.
|
||||
Unlike the old axis-aligned demo, ExPolygon.offset() is a correct inward offset
|
||||
for any contour (it is Clipper under the hood), and it naturally handles holes.
|
||||
A surface may split into several islands or vanish when shrunk; both are handled.
|
||||
|
||||
numpy is declared as a dependency: the geometry accessors hand back zero-copy
|
||||
int64 ndarrays, and set_slices() requires genuine ndarrays back (not plain lists),
|
||||
so building the modified contour needs numpy.
|
||||
No numpy required: the whole edit is expressed with the host geometry classes.
|
||||
"""
|
||||
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"
|
||||
@@ -84,32 +38,41 @@ class InsetEverySlice(orca.slicing.SlicingPipelineCapabilityBase):
|
||||
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.
|
||||
# Millimeters -> scaled integer units via the *live* scale (never hardcode 1e6).
|
||||
inset_scaled = int(round(INSET_MM / orca.slicing.unscale(1)))
|
||||
|
||||
regions_touched = 0
|
||||
for layer in ctx.object.layers():
|
||||
if ctx.cancelled():
|
||||
break
|
||||
layer_touched = False
|
||||
for region in layer.regions():
|
||||
surfaces = region.slices.surfaces
|
||||
if not surfaces:
|
||||
continue # an empty region has nothing to inset
|
||||
continue
|
||||
|
||||
new_surfaces = []
|
||||
# Group the inward-offset geometry by surface type so each type is
|
||||
# preserved when written back (set() tags all its expolygons one type).
|
||||
by_type = {}
|
||||
for surface in surfaces:
|
||||
expoly = surface.expolygon
|
||||
contour = expoly.contour.points()
|
||||
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, [h.points() for h in expoly.holes]])
|
||||
shrunk = surface.expolygon.offset(-inset_scaled) # [ExPolygon], may be empty
|
||||
if shrunk:
|
||||
by_type.setdefault(surface.surface_type, []).extend(shrunk)
|
||||
|
||||
region.set_slices(new_surfaces)
|
||||
if not by_type:
|
||||
continue # every surface collapsed: leave the region untouched this demo
|
||||
|
||||
# Rebuild the collection type-by-type: first set(), then append() the rest.
|
||||
items = list(by_type.items())
|
||||
first_type, first_expolys = items[0]
|
||||
region.slices.set(first_expolys, first_type)
|
||||
for st, expolys in items[1:]:
|
||||
region.slices.append(expolys, st)
|
||||
regions_touched += 1
|
||||
layer_touched = True
|
||||
if layer_touched:
|
||||
# Re-derive the merged islands from the inset region slices.
|
||||
layer.make_slices()
|
||||
|
||||
return orca.ExecutionResult.success(f"inset applied to {regions_touched} region(s)")
|
||||
|
||||
|
||||
Reference in New Issue
Block a user