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:
SoftFever
2026-07-08 15:04:40 +08:00
parent fd2a489980
commit 11dd078f64
7 changed files with 558 additions and 484 deletions

View File

@@ -53,6 +53,23 @@ pybind11::array make_readonly_rows(pybind11::handle base, const T* data, pybind1
return std::move(arr);
}
// Zero-copy, WRITABLE (rows, N) numpy view over `data`, lifetime tied to `base`.
// Twin of make_readonly_rows: a base-carrying pybind array is writable by default,
// so we simply do not clear the write flag. Writing through the view mutates the
// underlying C++ buffer in place. rows == 0 / null data yields a fresh empty (0, N)
// array (writable, no base).
template<typename T, int N>
pybind11::array make_writable_rows(pybind11::handle base, T* data, pybind11::ssize_t rows)
{
namespace py = pybind11;
if (rows == 0 || data == nullptr)
return py::array_t<T>(std::vector<py::ssize_t>{ 0, (py::ssize_t) N });
return py::array_t<T>(
{ rows, (py::ssize_t) N },
{ (py::ssize_t)(N * sizeof(T)), (py::ssize_t) sizeof(T) },
data, base);
}
// Serialize one config key to a Python string, or None if the key is absent.
// Works on any ConfigBase (resolved DynamicPrintConfig snapshots,
// PrintObjectConfig, PrintRegionConfig, preset configs).

View File

@@ -3,6 +3,7 @@
#include "libslic3r/libslic3r.h" // unscale<>, scale_
#include "libslic3r/BoundingBox.hpp"
#include "libslic3r/ClipperUtils.hpp" // offset/offset_ex/union_ex/diff_ex/intersection_ex
#include "libslic3r/ExPolygon.hpp"
#include "libslic3r/Surface.hpp"
#include "libslic3r/SurfaceCollection.hpp"
@@ -13,7 +14,6 @@
#include <pybind11/stl.h>
#include <memory>
#include <optional>
#include <vector>
namespace py = pybind11;
@@ -51,87 +51,14 @@ static Polygon parse_polygon(py::handle h, const char* who)
return poly;
}
// One Python entry -> ExPolygon. Accepts a bare (N,2) ndarray (contour only), a
// [contour, [hole, ...]] sequence, or (G9) a [contour, [hole, ...], SurfaceType] triple whose
// third element overrides the surface type for set_slices/set_fill_surfaces. When `out_type` is
// null (geometry-only consumers such as set_lslices) any third element is ignored. Orientation
// is normalized (contour CCW, holes CW) so downstream area/offset math is correct regardless of
// the caller's winding.
static ExPolygon parse_expolygon(py::handle entry, const char* who,
std::optional<SurfaceType>* out_type = nullptr)
// Accept a bound orca.host.Polygon (copied) or an (N,2) int64 ndarray. Used by the ExPolygon
// binding, whose constructor/contour-setter/set_holes must accept the Polygon it itself hands
// out (e.g. `ExPolygon(some_polygon_ref)`) in addition to the ndarray-only parse_polygon() path.
static Polygon as_polygon(py::handle h, const char* who)
{
ExPolygon ex;
if (py::isinstance<py::array>(entry)) {
ex.contour = parse_polygon(entry, who);
} else if (py::isinstance<py::sequence>(entry) && !py::isinstance<py::str>(entry)) {
py::sequence seq = py::reinterpret_borrow<py::sequence>(entry);
if (py::len(seq) < 1)
throw py::value_error(std::string(who) + ": a [contour, holes] entry needs a contour");
ex.contour = parse_polygon(seq[0], who);
if (py::len(seq) >= 2) {
// Type-check the holes element up front: a non-sequence (e.g. an int) would otherwise
// reach reinterpret_borrow<py::sequence> and raise a bare Python TypeError on iteration,
// whereas the API contract is ValueError for malformed input (str is excluded because it
// is iterable but never a valid holes container).
py::object holes_obj = seq[1];
if (!py::isinstance<py::sequence>(holes_obj) || py::isinstance<py::str>(holes_obj))
throw py::value_error(std::string(who) + ": the holes element must be a list of (N,2) int64 ndarrays");
for (py::handle hh : py::reinterpret_borrow<py::sequence>(holes_obj)) {
Polygon hole = parse_polygon(hh, who);
hole.make_clockwise();
ex.holes.emplace_back(std::move(hole));
}
}
// G9: optional third element -> per-surface SurfaceType override (None keeps the
// carried-forward type). A wrong type raises ValueError, matching the API contract.
if (out_type != nullptr && py::len(seq) >= 3) {
py::object t = seq[2];
if (!t.is_none()) {
try { *out_type = t.cast<SurfaceType>(); }
catch (const py::cast_error&) {
throw py::value_error(std::string(who) + ": the third entry element must be an orca.host.SurfaceType");
}
}
}
} else {
throw py::value_error(std::string(who) + ": each entry must be an (N,2) ndarray or a [contour, holes] pair");
}
ex.contour.make_counter_clockwise();
return ex;
}
// A Python list of entries -> ExPolygons (each entry parsed + oriented). G7: an empty list is
// legal and means "no geometry" (clears the target collection). Per-entry types are ignored
// here (geometry-only consumers such as set_lslices).
static ExPolygons parse_expolygon_list(py::handle list_h, const char* who)
{
if (!py::isinstance<py::sequence>(list_h) || py::isinstance<py::str>(list_h))
throw py::value_error(std::string(who) + ": expected a list of polygons");
ExPolygons out;
for (py::handle entry : py::reinterpret_borrow<py::sequence>(list_h))
out.emplace_back(parse_expolygon(entry, who));
return out;
}
// Build Surfaces from a Python list, carrying surface_type (and the other per-surface
// attributes) forward from the collection being replaced, or defaulting to stInternal when the
// region had none. G9: a per-entry SurfaceType (optional third element) overrides that default.
// G7: an empty list is legal and yields an empty Surfaces (clears the collection).
static Surfaces surfaces_from_py(py::handle list_h, const SurfaceCollection& replaced, const char* who)
{
if (!py::isinstance<py::sequence>(list_h) || py::isinstance<py::str>(list_h))
throw py::value_error(std::string(who) + ": expected a list of polygons");
const Surface tmpl = replaced.surfaces.empty() ? Surface(stInternal) : replaced.surfaces.front();
Surfaces out;
for (py::handle entry : py::reinterpret_borrow<py::sequence>(list_h)) {
std::optional<SurfaceType> type;
ExPolygon e = parse_expolygon(entry, who, &type);
Surface s(tmpl, std::move(e));
if (type)
s.surface_type = *type;
out.emplace_back(std::move(s));
}
return out;
if (py::isinstance<Polygon>(h))
return h.cast<Polygon>();
return parse_polygon(h, who);
}
// Flatten an extrusion graph into a list of leaf ExtrusionPath* while walking the
@@ -164,6 +91,17 @@ static void collect_extrusion_paths(const ExtrusionEntity* ee, std::vector<const
out.push_back(path);
}
}
// Rebuild a layer's per-island bbox cache from lslices — the same inline pattern
// every C++ call site uses (PrintObjectSlice.cpp, Print.cpp, TreeSupport.cpp); no
// libslic3r helper exists to reuse.
static void refresh_lslices_bboxes(Layer& l)
{
l.lslices_bboxes.clear();
l.lslices_bboxes.reserve(l.lslices.size());
for (const ExPolygon& island : l.lslices)
l.lslices_bboxes.emplace_back(get_extents(island));
}
} // namespace
void PluginHostSlicing::RegisterBindings(py::module_& host)
@@ -177,10 +115,10 @@ void PluginHostSlicing::RegisterBindings(py::module_& host)
// out below is a non-owning reference into the live slicing graph owned by
// the Print. References — and every numpy view they hand out — are valid
// only while the plugin hook (execute(ctx)) runs, and a container-replacing
// mutator (LayerRegion.set_slices / set_fill_surfaces, Layer.set_lslices)
// invalidates previously obtained references into that container, exactly
// as std::vector operations invalidate C++ iterators. Do not stash
// references or arrays across execute() calls; copy what you need.
// mutator (SurfaceCollection.set / append / clear, Polygon.set_points / append,
// ExPolygon.set_holes) invalidates previously obtained references into that
// container, exactly as std::vector operations invalidate C++ iterators. Do
// not stash references or arrays across execute() calls; copy what you need.
// ------------------------------------------------------------------
py::enum_<SurfaceType>(host, "SurfaceType")
@@ -197,52 +135,203 @@ void PluginHostSlicing::RegisterBindings(py::module_& host)
.value("stCount", stCount)
.export_values();
py::class_<Polygon, std::unique_ptr<Polygon, py::nodelete>>(host, "Polygon")
// Point: a constructible value type (default holder, so Python-owned instances
// are freed). Returned-by-reference from Polygon.points, it aliases the buffer;
// x()/y() are Eigen lvalues, so the properties are read/write. p+q / p-q go
// through Eigen expression templates, wrapped back into a Point.
py::class_<Point>(host, "Point")
.def(py::init([](coord_t x, coord_t y) { return Point(x, y); }), py::arg("x"), py::arg("y"))
.def_property("x", [](const Point& p) { return p.x(); },
[](Point& p, coord_t v) { p.x() = v; })
.def_property("y", [](const Point& p) { return p.y(); },
[](Point& p, coord_t v) { p.y() = v; })
.def("__add__", [](const Point& a, const Point& b) { return Point(a + b); }, py::is_operator())
.def("__sub__", [](const Point& a, const Point& b) { return Point(a - b); }, py::is_operator())
.def("__mul__", [](const Point& a, double s) { return Point(a.x() * s, a.y() * s); }, py::is_operator())
.def("__repr__", [](const Point& p) {
return "orca.host.Point(" + std::to_string(p.x()) + ", " + std::to_string(p.y()) + ")";
});
py::class_<Polygon>(host, "Polygon")
.def(py::init<>())
.def("size", [](const Polygon& p) { return p.points.size(); })
.def("is_valid", [](const Polygon& p) { return p.is_valid(); })
.def("is_counter_clockwise", [](const Polygon& p) { return p.is_counter_clockwise(); })
.def("points", [](py::object self) {
const Polygon& p = self.cast<const Polygon&>();
.def("is_clockwise", [](const Polygon& p) { return p.is_clockwise(); })
.def("make_counter_clockwise", [](Polygon& p) { return p.make_counter_clockwise(); },
"Reorient to CCW in place. Returns True if it reversed the winding.")
.def("make_clockwise", [](Polygon& p) { return p.make_clockwise(); })
.def("area", [](const Polygon& p) { return p.area(); })
.def("centroid", [](const Polygon& p) { return p.centroid(); })
.def("contains", [](const Polygon& p, const Point& pt) { return p.contains(pt); }, py::arg("point"))
.def("translate", [](Polygon& p, double x, double y) { p.translate(x, y); }, py::arg("x"), py::arg("y"))
.def("rotate", [](Polygon& p, double angle) { p.rotate(angle); }, py::arg("angle"))
.def("rotate", [](Polygon& p, double angle, const Point& c) { p.rotate(angle, c); },
py::arg("angle"), py::arg("center"))
.def("douglas_peucker", [](Polygon& p, double tol) { p.douglas_peucker(tol); }, py::arg("tolerance"))
.def("simplify", [](const Polygon& p, double tol) { return p.simplify(tol); }, py::arg("tolerance"),
"Return simplified geometry as a list of Polygon (may split into several).")
.def("offset", [](const Polygon& p, coord_t delta) { return offset(p, (float) delta); }, py::arg("delta"),
"Clipper offset by `delta` scaled units (negative shrinks). Returns [Polygon].")
// --- Point-object idiom: references into the buffer (in-place element edit). ---
.def_property_readonly("points", [](py::object self) {
Polygon& p = self.cast<Polygon&>();
py::list out;
for (Point& pt : p.points)
out.append(py::cast(&pt, py::return_value_policy::reference_internal, self));
return out;
}, "Vertices as [Point] references into this polygon. Editing a Point mutates the "
"buffer in place. Structural changes (count) go through set_points/append, which "
"invalidate previously returned Point refs and array views (C++ vector semantics).")
.def("append", [](Polygon& p, const Point& pt) { p.points.push_back(pt); }, py::arg("point"),
"Append a vertex. Structural change (count): invalidates previously returned "
"Point refs and array views into this polygon (C++ vector semantics).")
// --- numpy idiom: writable zero-copy (N,2) view (bulk affine edits). ---
.def("as_array", [](py::object self) {
Polygon& p = self.cast<Polygon&>();
return with_numpy([&] {
return py::object(make_readonly_rows<coord_t, 2>(
return py::object(make_writable_rows<coord_t, 2>(
self, p.points.empty() ? nullptr : p.points.front().data(),
(py::ssize_t) p.points.size()));
});
}, "Vertices as a read-only int64 (N,2) numpy view in scaled coords. "
"Valid only during the execute(ctx) call. Requires numpy.");
}, "Vertices as a WRITABLE int64 (N,2) numpy view in scaled coords, aliasing the "
"buffer. Count-preserving in-place edits only; valid during execute(ctx). Requires numpy.")
.def("set_points", [](Polygon& p, py::handle src) { p = parse_polygon(src, "Polygon.set_points"); },
py::arg("points"),
"Replace all vertices from an (N,2) int64 ndarray (scaled coords). Count-changing; "
"invalidates prior Point refs and array views. Raises ValueError on malformed input.");
py::class_<ExPolygon, std::unique_ptr<ExPolygon, py::nodelete>>(host, "ExPolygon")
.def_property_readonly("contour", [](ExPolygon& e) -> Polygon& { return e.contour; },
// ExPolygon: default holder (Python-owned instances are freed) so plugins can construct
// their own geometry, not just navigate the live slicing graph. contour/holes accessors
// still use reference_internal, so refs into a graph-owned ExPolygon stay non-owning views
// tied to that owner's lifetime, same as Polygon/Surface above.
py::class_<ExPolygon>(host, "ExPolygon")
.def(py::init([](py::handle contour, py::handle holes) {
// Accept bound Polygons or (N,2) ndarrays for both contour and each hole.
ExPolygon ex;
ex.contour = as_polygon(contour, "ExPolygon.contour");
if (!holes.is_none()) {
if (!py::isinstance<py::sequence>(holes) || py::isinstance<py::str>(holes))
throw py::value_error("ExPolygon: holes must be a list of Polygon or (N,2) ndarrays");
for (py::handle h : py::reinterpret_borrow<py::sequence>(holes)) {
Polygon hole = as_polygon(h, "ExPolygon.hole");
hole.make_clockwise();
ex.holes.emplace_back(std::move(hole));
}
}
ex.contour.make_counter_clockwise();
return ex;
}), py::arg("contour"), py::arg("holes") = py::none(),
"Construct from a Polygon/ndarray contour and optional list of hole Polygons/ndarrays. "
"Orientation is normalized (contour CCW, holes CW).")
.def_property("contour",
[](ExPolygon& e) -> Polygon& { return e.contour; },
[](ExPolygon& e, py::handle v) { e.contour = as_polygon(v, "ExPolygon.contour"); },
py::return_value_policy::reference_internal,
"Outer contour (CCW) as a Polygon.")
"Outer contour (CCW). Read returns a live Polygon ref; assign a Polygon/ndarray to replace it.")
.def_property_readonly("holes", [](py::object self) {
ExPolygon& e = self.cast<ExPolygon&>();
py::list out;
for (Polygon& h : e.holes)
out.append(py::cast(&h, py::return_value_policy::reference_internal, self));
return out;
}, "Hole contours (CW) as [Polygon].");
}, "Hole contours (CW) as [Polygon] references (in-place editable). set_holes replaces them.")
.def("set_holes", [](ExPolygon& e, py::handle holes) {
ExPolygon tmp;
if (!py::isinstance<py::sequence>(holes) || py::isinstance<py::str>(holes))
throw py::value_error("set_holes: expected a list of Polygon or (N,2) ndarrays");
for (py::handle h : py::reinterpret_borrow<py::sequence>(holes)) {
Polygon hole = as_polygon(h, "ExPolygon.set_holes");
hole.make_clockwise();
tmp.holes.emplace_back(std::move(hole));
}
e.holes = std::move(tmp.holes);
}, py::arg("holes"), "Replace all holes. Invalidates prior hole refs (C++ vector semantics).")
.def("translate", [](ExPolygon& e, double x, double y) { e.translate(x, y); }, py::arg("x"), py::arg("y"))
.def("rotate", [](ExPolygon& e, double a) { e.rotate(a); }, py::arg("angle"))
.def("rotate", [](ExPolygon& e, double a, const Point& c) { e.rotate(a, c); },
py::arg("angle"), py::arg("center"))
.def("scale", [](ExPolygon& e, double f) { e.scale(f); }, py::arg("factor"))
.def("douglas_peucker", [](ExPolygon& e, double t) { e.douglas_peucker(t); }, py::arg("tolerance"))
.def("area", [](const ExPolygon& e) { return e.area(); })
.def("is_valid", [](const ExPolygon& e) { return e.is_valid(); })
.def("contains", [](const ExPolygon& e, const Point& p) { return e.contains(p); }, py::arg("point"))
.def("num_contours", [](const ExPolygon& e) { return e.num_contours(); })
.def("simplify", [](const ExPolygon& e, double t) { return e.simplify(t); }, py::arg("tolerance"),
"Return simplified geometry as [ExPolygon].")
.def("offset", [](const ExPolygon& e, coord_t delta) { return offset_ex(e, (float) delta); },
py::arg("delta"), "Clipper offset by `delta` scaled units (negative shrinks). Returns [ExPolygon].")
.def("union_ex", [](const ExPolygon& a, const ExPolygon& b) {
return union_ex(ExPolygons{ a, b });
}, py::arg("other"), "Union with another ExPolygon. Returns [ExPolygon].")
.def("diff_ex", [](const ExPolygon& a, const ExPolygon& b) {
return diff_ex(ExPolygons{ a }, ExPolygons{ b });
}, py::arg("other"), "This minus `other`. Returns [ExPolygon].")
.def("intersection_ex", [](const ExPolygon& a, const ExPolygon& b) {
return intersection_ex(ExPolygons{ a }, ExPolygons{ b });
}, py::arg("other"), "Intersection with `other`. Returns [ExPolygon].");
py::class_<Surface, std::unique_ptr<Surface, py::nodelete>>(host, "Surface")
// Surface: default holder (Python-owned instances are freed), so plugins can construct
// their own Surface(surface_type, expolygon) — not just navigate the live slicing graph.
// expolygon is a reference_internal property, same idiom as Polygon/ExPolygon above.
py::class_<Surface>(host, "Surface")
.def(py::init([](SurfaceType t, const ExPolygon& e) { return Surface(t, e); }),
py::arg("surface_type"), py::arg("expolygon"))
.def(py::init([](SurfaceType t) { return Surface(t); }), py::arg("surface_type"))
.def_readwrite("surface_type", &Surface::surface_type,
"This surface's SurfaceType. Writable: assigning reclassifies the "
"surface in place on the live slicing graph (geometry unchanged).")
.def_readonly("thickness", &Surface::thickness)
.def_readonly("bridge_angle", &Surface::bridge_angle)
.def_readonly("extra_perimeters", &Surface::extra_perimeters)
.def_property_readonly("expolygon", [](Surface& s) -> ExPolygon& { return s.expolygon; },
"This surface's SurfaceType. Assigning reclassifies it in place (geometry unchanged).")
.def_readwrite("thickness", &Surface::thickness)
.def_readwrite("bridge_angle", &Surface::bridge_angle)
.def_readwrite("extra_perimeters", &Surface::extra_perimeters)
.def_property("expolygon",
[](Surface& s) -> ExPolygon& { return s.expolygon; },
[](Surface& s, const ExPolygon& e) { s.expolygon = e; },
py::return_value_policy::reference_internal,
"This surface's geometry.");
"This surface's geometry. Read returns a live ExPolygon ref; assign to replace it.")
.def("area", [](const Surface& s) { return s.area(); })
.def("is_top", [](const Surface& s) { return s.is_top(); })
.def("is_bottom", [](const Surface& s) { return s.is_bottom(); })
.def("is_bridge", [](const Surface& s) { return s.is_bridge(); })
.def("is_internal", [](const Surface& s) { return s.is_internal(); })
.def("is_external", [](const Surface& s) { return s.is_external(); })
.def("is_solid", [](const Surface& s) { return s.is_solid(); });
// SurfaceCollection: kept on py::nodelete — it is only ever a reference into the live
// slicing graph (LayerRegion::slices/fill_surfaces), never constructed by a plugin.
py::class_<SurfaceCollection, std::unique_ptr<SurfaceCollection, py::nodelete>>(host, "SurfaceCollection")
.def("size", [](const SurfaceCollection& c) { return c.surfaces.size(); })
.def("empty", [](const SurfaceCollection& c) { return c.empty(); })
.def("clear", [](SurfaceCollection& c) { c.clear(); })
.def("has", [](const SurfaceCollection& c, SurfaceType t) { return c.has(t); }, py::arg("surface_type"))
.def("set_type", [](SurfaceCollection& c, SurfaceType t) { c.set_type(t); }, py::arg("surface_type"))
.def("set", [](SurfaceCollection& c, const std::vector<ExPolygon>& src, SurfaceType t) { c.set(src, t); },
py::arg("expolygons"), py::arg("surface_type"),
"Replace all surfaces from a list of ExPolygon, all tagged `surface_type`. "
"This is the faithful replacement for the retired set_slices().")
.def("set", [](SurfaceCollection& c, const std::vector<Surface>& src) { c.set(src); },
py::arg("surfaces"), "Replace all surfaces from a list of Surface (types preserved per surface).")
.def("append", [](SurfaceCollection& c, const std::vector<ExPolygon>& src, SurfaceType t) { c.append(src, t); },
py::arg("expolygons"), py::arg("surface_type"))
.def("filter_by_type", [](py::object self, SurfaceType t) {
SurfaceCollection& c = self.cast<SurfaceCollection&>();
py::list out;
// SurfacesPtr (SurfaceCollection::filter_by_type's return type) is
// std::vector<const Surface*> (see Surface.hpp); the brief's note describing it
// as std::vector<Surface*> does not match the header, so this iterates by const
// pointer (py::cast accepts `const itype*` directly, see cast.h cast(const itype*)).
for (const Surface* s : c.filter_by_type(t))
out.append(py::cast(s, py::return_value_policy::reference_internal, self));
return out;
}, py::arg("surface_type"), "Surfaces of a given type as [Surface] refs. Invalidated by "
"set()/append()/clear() on this collection (C++ vector semantics), same as .surfaces.")
.def_property_readonly("surfaces", [](py::object self) {
SurfaceCollection& c = self.cast<SurfaceCollection&>();
py::list out;
for (Surface& s : c.surfaces)
out.append(py::cast(&s, py::return_value_policy::reference_internal, self));
return out;
}, "Surfaces as [Surface] references into the live collection. Invalidated "
"by set_slices/set_fill_surfaces on the owning region (C++ vector semantics).");
}, "Surfaces as [Surface] references into the live collection. Invalidated by "
"set()/append()/clear() on this collection (C++ vector semantics).");
// --- Extrusion tree (read-only in v1). Registered polymorphically: when a returned
// ExtrusionEntity*'s dynamic type IS one of the classes registered below, pybind
@@ -324,14 +413,15 @@ void PluginHostSlicing::RegisterBindings(py::module_& host)
auto layer_region = py::class_<LayerRegion, std::unique_ptr<LayerRegion, py::nodelete>>(host, "LayerRegion");
layer_region
.def_readonly("slices", &LayerRegion::slices,
"Sliced, typed surfaces (SurfaceCollection). At Step.Slice this is the "
"primary mutation target via set_slices().")
"Sliced, typed surfaces (SurfaceCollection). Edit in place, or replace with "
"slices.set(expolygons, surface_type). At Step.Slice this is the primary mutation "
"target; the split slice loop runs make_perimeters() afterward so edits cascade downstream.")
.def_readonly("fill_surfaces", &LayerRegion::fill_surfaces,
"Surfaces prepared for infill (SurfaceCollection).")
"Surfaces prepared for infill (SurfaceCollection). Edit in place or via fill_surfaces.set(...).")
.def_readonly("perimeters", &LayerRegion::perimeters,
"Perimeter toolpaths (ExtrusionEntityCollection).")
"Perimeter toolpaths (ExtrusionEntityCollection, read-only in v1).")
.def_readonly("fills", &LayerRegion::fills,
"Infill toolpaths (ExtrusionEntityCollection).")
"Infill toolpaths (ExtrusionEntityCollection, read-only in v1).")
.def("layer", [](LayerRegion& r) -> py::object {
Layer* l = r.layer();
if (l == nullptr)
@@ -344,58 +434,7 @@ void PluginHostSlicing::RegisterBindings(py::module_& host)
.def("config_value", [](const LayerRegion& r, const std::string& key) {
return config_value_or_none(r.region().config(), key);
}, py::arg("key"),
"Serialized value of this region's resolved config option, or None if absent.")
// MUTATOR (G1/G3/G9). Replace this region's sliced surfaces. `polygons` is a list of
// (N,2) int64 ndarrays (scaled coords), [contour, [holes...]] pairs, or (G9)
// [contour, [holes...], SurfaceType] triples; orientation is normalized (contour CCW,
// holes CW) and surface_type is carried forward from the replaced surfaces (else
// stInternal) unless a per-entry type is given.
.def("set_slices", [](LayerRegion& region, py::object polygons, bool refresh_lslices) {
region.slices.set(surfaces_from_py(polygons, region.slices, "set_slices"));
// G1: rebuild the owning layer's merged islands (lslices) + bbox cache from the
// mutated region slices so downstream consumers (detect_surfaces_type neighbor
// diffs, overhang/bridge detection, brim/skirt/support) see coherent islands.
// Skipped when the region has no owning layer (unit-test regions).
if (refresh_lslices) {
if (Layer* layer = region.layer()) {
layer->make_slices();
layer->lslices_bboxes.clear();
layer->lslices_bboxes.reserve(layer->lslices.size());
for (const ExPolygon& island : layer->lslices)
layer->lslices_bboxes.emplace_back(get_extents(island));
}
}
}, py::arg("polygons"), py::arg("refresh_lslices") = true,
"Replace this region's sliced surfaces from a list of (N,2) int64 ndarrays (scaled "
"coords), [contour, [holes...]] pairs, or [contour, [holes...], SurfaceType] triples "
"(orientation normalized: contour CCW / holes CW; surface_type carried forward from the "
"replaced surfaces, else stInternal, unless a per-entry SurfaceType is supplied). An "
"empty list clears this region's slices.\n"
"MUTATION-CASCADE: at the Slice boundary this is the primary, fully-supported entry "
"point -- the split slice loop runs make_perimeters() afterward, so the change cascades "
"into perimeters and everything downstream (final G-code).\n"
"LSLICES (G1): refresh_lslices=True (default) re-derives the owning layer's merged "
"islands and bbox cache from the new slices so overhang/bridge/skirt/support stay "
"coherent; pass False only if you manage lslices yourself via Layer.set_lslices.\n"
"PERSISTENCE (G3): the Slice hook re-snapshots raw_slices after it returns, so the "
"mutation survives a later perimeter-only re-run (restore_untyped_slices) instead of "
"silently reverting; it still does not persist across a full re-slice unless the hook "
"re-fires (re-select the plugin, or any posSlice-invalidating change).\n"
"DUPLICATES: identical objects share Layer*, so the mutation on the object that slices "
"is automatically seen by its duplicates; objects that must mutate independently must "
"not be identical.\n"
"Raises ValueError on malformed input. Valid only during the execute(ctx) call.")
// MUTATOR. Replace this region's fill (infill-prep) surfaces; identical input format and
// validation to set_slices.
.def("set_fill_surfaces", [](LayerRegion& region, py::object polygons) {
region.fill_surfaces.set(surfaces_from_py(polygons, region.fill_surfaces, "set_fill_surfaces"));
}, py::arg("polygons"),
"Replace this region's fill (infill-prep) surfaces; same input format/validation as "
"set_slices (per-entry SurfaceType supported; an empty list clears them).\n"
"MUTATION-CASCADE: at the PrepareInfill boundary (G4) make_fills runs afterward, so this "
"cascades into the generated infill. At the Infill boundary it changes the stored "
"surfaces but does NOT regenerate the already-built `fills` toolpaths (v1).\n"
"Raises ValueError on malformed input. Valid only during the execute(ctx) call.");
"Serialized value of this region's resolved config option, or None if absent.");
auto layer = py::class_<Layer, std::unique_ptr<Layer, py::nodelete>>(host, "Layer");
layer
@@ -417,38 +456,19 @@ void PluginHostSlicing::RegisterBindings(py::module_& host)
out.append(py::cast(r, py::return_value_policy::reference_internal, self));
return out;
}, "Per-region data as [LayerRegion].")
.def("make_slices", [](Layer& l) {
l.make_slices();
refresh_lslices_bboxes(l);
}, "Re-derive lslices (merged islands) from the region slices and refresh the bbox "
"cache — the C++ invariant-maintenance call after in-place slice edits.")
.def("lslices", [](py::object self) {
Layer& l = self.cast<Layer&>();
py::list out;
for (ExPolygon& e : l.lslices)
out.append(py::cast(&e, py::return_value_policy::reference_internal, self));
return out;
}, "Merged per-layer islands as [ExPolygon] references. Invalidated by "
"set_lslices/make_slices (C++ vector semantics).")
.def("make_slices", [](Layer& l) {
l.make_slices();
l.lslices_bboxes.clear();
l.lslices_bboxes.reserve(l.lslices.size());
for (const ExPolygon& island : l.lslices)
l.lslices_bboxes.emplace_back(get_extents(island));
}, "Re-derive lslices (merged islands) from the region slices and refresh the "
"bbox cache — the C++ invariant-maintenance call after in-place geometry edits. "
"set_slices(refresh_lslices=True) runs this for you.")
// MUTATOR. Replace this layer's merged islands (lslices) and refresh the cache-invariant
// `lslices_bboxes` (one BoundingBox per island via get_extents). Same input format and
// validation as LayerRegion.set_slices.
.def("set_lslices", [](Layer& l, py::object islands) {
l.lslices = parse_expolygon_list(islands, "set_lslices");
l.lslices_bboxes.clear();
l.lslices_bboxes.reserve(l.lslices.size());
for (const ExPolygon& island : l.lslices)
l.lslices_bboxes.emplace_back(get_extents(island));
}, py::arg("islands"),
"Replace this layer's merged islands (lslices) from a list of (N,2) int64 ndarrays "
"(scaled coords) or [contour, [holes...]] pairs, and refresh lslices_bboxes (one "
"bounding box per island via get_extents) so the bbox cache stays consistent. Same "
"input format/validation as LayerRegion.set_slices. Raises ValueError on malformed "
"input. Valid only during the execute(ctx) call.");
}, "Merged per-layer islands as [ExPolygon] refs (in-place editable). Derived from the "
"region slices; call make_slices() to re-derive after edits. Invalidated by make_slices().");
py::class_<PrintObject, std::unique_ptr<PrintObject, py::nodelete>>(host, "PrintObject")
.def("id", [](const PrintObject& o) { return o.id().id; },

View File

@@ -17,8 +17,8 @@ void SlicingPipelinePluginCapability::RegisterBindings(py::module_& module, py::
.value("Slice", SlicingPipelineStep::Slice)
.value("Perimeters", SlicingPipelineStep::Perimeters)
.value("EstimateCurledExtrusions", SlicingPipelineStep::EstimateCurledExtrusions)
.value("PrepareInfill", SlicingPipelineStep::PrepareInfill) // after prepare_infill, before make_fills: set_fill_surfaces here CASCADES
.value("Infill", SlicingPipelineStep::Infill) // after make_fills: set_fill_surfaces here does NOT regenerate fills (v1)
.value("PrepareInfill", SlicingPipelineStep::PrepareInfill) // after prepare_infill, before make_fills: editing fill_surfaces here CASCADES
.value("Infill", SlicingPipelineStep::Infill) // after make_fills: editing fill_surfaces here does NOT regenerate fills (v1)
.value("Ironing", SlicingPipelineStep::Ironing)
.value("Contouring", SlicingPipelineStep::Contouring)
.value("SupportMaterial", SlicingPipelineStep::SupportMaterial)