mirror of
https://github.com/OrcaSlicer/OrcaSlicer.git
synced 2026-07-21 01:42:09 +00:00
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:
@@ -9,7 +9,8 @@ TEST_CASE("slicing_pipeline_plugin option exists and defaults empty", "[slicing_
|
||||
CHECK(opt->values.empty());
|
||||
const ConfigOptionDef* def = cfg.def()->get("slicing_pipeline_plugin");
|
||||
REQUIRE(def != nullptr);
|
||||
CHECK(def->support_plugin == true);
|
||||
CHECK(def->plugin_type == "slicing-pipeline");
|
||||
CHECK(def->is_plugin_backed());
|
||||
CHECK(def->gui_type == ConfigOptionDef::GUIType::plugin_picker);
|
||||
}
|
||||
|
||||
@@ -45,6 +46,7 @@ TEST_CASE("SlicingPipeline hook fires once per step per object in order", "[slic
|
||||
auto count = [&](S s){ return std::count_if(calls.begin(), calls.end(), [&](const Call& c){ return c.step == s; }); };
|
||||
CHECK(count(S::Slice) == 1);
|
||||
CHECK(count(S::Perimeters) == 1);
|
||||
CHECK(count(S::PrepareInfill) == 1); // the prepare-infill seam fires once per object
|
||||
CHECK(count(S::Infill) == 1);
|
||||
CHECK(count(S::WipeTower) == 1);
|
||||
CHECK(count(S::SkirtBrim) == 1);
|
||||
@@ -54,9 +56,32 @@ TEST_CASE("SlicingPipeline hook fires once per step per object in order", "[slic
|
||||
// Slice must fire before Perimeters for the same object:
|
||||
auto idx = [&](S s){ for (size_t i=0;i<calls.size();++i) if (calls[i].step==s) return (int)i; return -1; };
|
||||
CHECK(idx(S::Slice) < idx(S::Perimeters));
|
||||
CHECK(idx(S::Perimeters) < idx(S::PrepareInfill)); // prepare-infill fires after perimeters...
|
||||
CHECK(idx(S::PrepareInfill) < idx(S::Infill)); // ...and before the fills are built
|
||||
}
|
||||
|
||||
#include <sstream>
|
||||
#include <cmath>
|
||||
|
||||
// Exported G-code carries a few nondeterministic comment lines unrelated to toolpaths: a
|
||||
// wall-clock timestamp ("; generated by ..."), ObjectID-derived ids (from a process-global
|
||||
// counter never reset between runs), and a config-dump line naming the selected plugin (an
|
||||
// active run records it, the absent baseline does not). Strip exactly those lines so a raw
|
||||
// byte-compare isolates the real motion/extrusion output; every other byte is still compared.
|
||||
static std::string strip_nondeterministic_gcode_lines(const std::string& gcode) {
|
||||
std::string out; out.reserve(gcode.size());
|
||||
std::istringstream in(gcode);
|
||||
std::string line;
|
||||
while (std::getline(in, line)) {
|
||||
if (line.compare(0, 15, "; generated by ") == 0) continue; // wall-clock timestamp
|
||||
if (line.compare(0, 18, "; model label id: ") == 0) continue; // ObjectID-derived
|
||||
// "; [stop] printing object <name> id:N copy M" / "... unique label id: N" (ObjectID-derived):
|
||||
if (line.find("printing object") != std::string::npos && line.find(" id:") != std::string::npos) continue;
|
||||
if (line.find("slicing_pipeline_plugin") != std::string::npos) continue; // config-dump plugin name
|
||||
out += line; out += '\n';
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
TEST_CASE("Inactive hook: process output is byte-identical (no-op hook == unset)", "[slicing_pipeline]") {
|
||||
// Three configurations must all normalize to the same G-code:
|
||||
@@ -80,35 +105,12 @@ TEST_CASE("Inactive hook: process output is byte-identical (no-op hook == unset)
|
||||
Slic3r::Print::set_slicing_pipeline_hook_fn(nullptr);
|
||||
return g;
|
||||
};
|
||||
// Pre-existing nondeterminism unrelated to the hook makes a raw string compare
|
||||
// impossible: exported gcode embeds a wall-clock timestamp and ids derived from
|
||||
// the process-global ObjectID counter (never reset between runs) in a handful of
|
||||
// comment lines. Strip exactly those comment lines; every other byte -- all
|
||||
// motion/extrusion/temperature commands and all remaining comments -- is still
|
||||
// compared, so the assertion still proves the inactive hook leaves all
|
||||
// machine-meaningful output byte-identical.
|
||||
auto normalize = [](const std::string& gcode) {
|
||||
std::string out; out.reserve(gcode.size());
|
||||
std::istringstream in(gcode);
|
||||
std::string line;
|
||||
while (std::getline(in, line)) {
|
||||
if (line.compare(0, 15, "; generated by ") == 0) continue; // wall-clock timestamp
|
||||
if (line.compare(0, 18, "; model label id: ") == 0) continue; // ObjectID-derived
|
||||
// "; [stop] printing object <name> id:N copy M" and
|
||||
// "; start/stop printing object, unique label id: N" (ObjectID-derived):
|
||||
if (line.find("printing object") != std::string::npos && line.find(" id:") != std::string::npos) continue;
|
||||
// Config-dump comment: the active run legitimately records the selected plugin
|
||||
// ("; slicing_pipeline_plugin = probe") while the baseline leaves it empty. This
|
||||
// is a machine-irrelevant comment, not motion -- strip it so the comparison isolates
|
||||
// whether the active-but-non-mutating hook perturbs the real toolpath.
|
||||
if (line.find("slicing_pipeline_plugin") != std::string::npos) continue;
|
||||
out += line; out += '\n';
|
||||
}
|
||||
return out;
|
||||
};
|
||||
const std::string baseline = normalize(run(false, false)); // feature absent
|
||||
CHECK(normalize(run(false, true)) == baseline); // gated off: hook never fires
|
||||
CHECK(normalize(run(true, true)) == baseline); // active no-op hook fires everywhere, mutates nothing
|
||||
// Compare only machine-meaningful output (see strip_nondeterministic_gcode_lines): every
|
||||
// motion/extrusion byte is still compared, so this proves the inactive hook -- and the
|
||||
// active-but-non-mutating hook -- leave the real toolpath byte-identical.
|
||||
const std::string baseline = strip_nondeterministic_gcode_lines(run(false, false)); // feature absent
|
||||
CHECK(strip_nondeterministic_gcode_lines(run(false, true)) == baseline); // gated off: hook never fires
|
||||
CHECK(strip_nondeterministic_gcode_lines(run(true, true)) == baseline); // active no-op hook fires everywhere, mutates nothing
|
||||
}
|
||||
|
||||
// Fix 4(a): gating negative path. With the option EMPTY the plugin is inactive, so a
|
||||
@@ -172,7 +174,7 @@ TEST_CASE("Duplicate objects share a slice: Slice hook fires exactly once", "[sl
|
||||
#include "libslic3r/Layer.hpp" // Layer, LayerRegion (full defs for the cascade hook)
|
||||
#include "libslic3r/ClipperUtils.hpp" // offset_ex
|
||||
|
||||
// Task 11: the correctness heart of the mutation feature. A C++ hook insets every
|
||||
// The correctness heart of the mutation feature. A C++ hook insets every
|
||||
// region's `slices` at the Slice boundary (via SurfaceCollection::set with offset
|
||||
// polygons); because make_perimeters() derives fill_surfaces from slices AFTER the
|
||||
// Slice hook fires (see Print::process's split slice loop), the downstream
|
||||
@@ -213,3 +215,256 @@ TEST_CASE("Changing slicing_pipeline_plugin invalidates posSlice", "[slicing_pip
|
||||
print.apply(model, config);
|
||||
CHECK_FALSE(print.objects().front()->is_step_done(posSlice)); // re-slice required
|
||||
}
|
||||
|
||||
#include <catch2/matchers/catch_matchers_floating_point.hpp>
|
||||
|
||||
// §3.6 (Twistify design): Twistify's effect is a similarity transform (rotate + uniform
|
||||
// scale) applied to slices at Step.Slice. This C++ analogue rotates every region's slices a
|
||||
// fixed 45 deg about the object's base-footprint center -- the same seam and cascade that
|
||||
// Twistify.py drives through the pybind set_slices binding. Two end-to-end invariants after
|
||||
// process() confirm the approach:
|
||||
// (1) a pure rotation is a similarity with scale 1, so total fill area is preserved, and
|
||||
// (2) the mutation genuinely cascaded into make_perimeters' fill_surfaces -- a 20mm square
|
||||
// rotated 45 deg becomes a diamond whose bbox is ~sqrt(2)x wider (it did not stay
|
||||
// axis-aligned), proving downstream geometry was rebuilt from the twisted slices.
|
||||
TEST_CASE("Rotating slices at the Slice boundary cascades (area preserved, bbox rotated)", "[slicing_pipeline]") {
|
||||
using Catch::Matchers::WithinRel;
|
||||
struct Measure { double area; double width; double height; };
|
||||
auto measure = [](bool rotate) -> Measure {
|
||||
Slic3r::Print print; Slic3r::Model model;
|
||||
auto config = Slic3r::DynamicPrintConfig::full_print_config();
|
||||
config.set_key_value("slicing_pipeline_plugin", new Slic3r::ConfigOptionStrings({"probe"}));
|
||||
if (rotate) Slic3r::Print::set_slicing_pipeline_hook_fn(
|
||||
[](Slic3r::Print&, const Slic3r::PrintObject* o, Slic3r::SlicingPipelineStep s){
|
||||
if (s != Slic3r::SlicingPipelineStep::Slice || !o) return;
|
||||
auto* obj = const_cast<Slic3r::PrintObject*>(o);
|
||||
// Twist axis = center of the first sliced layer's footprint (Twistify's anchor).
|
||||
coord_t nx=0, xx=0, ny=0, xy=0; bool seeded=false;
|
||||
for (Slic3r::Layer* l : obj->layers()) {
|
||||
for (Slic3r::LayerRegion* r : l->regions())
|
||||
for (const Slic3r::Surface& sf : r->slices.surfaces)
|
||||
for (const Slic3r::Point& p : sf.expolygon.contour.points) {
|
||||
if (!seeded) { nx=xx=p.x(); ny=xy=p.y(); seeded=true; }
|
||||
else { nx=std::min(nx,p.x()); xx=std::max(xx,p.x());
|
||||
ny=std::min(ny,p.y()); xy=std::max(xy,p.y()); }
|
||||
}
|
||||
if (seeded) break;
|
||||
}
|
||||
const double cx = 0.5*((double)nx+(double)xx), cy = 0.5*((double)ny+(double)xy);
|
||||
const double ct = 0.7071067811865476, st = 0.7071067811865476; // cos/sin 45 deg
|
||||
auto rot = [&](const Slic3r::Point& p) {
|
||||
const double dx = (double)p.x()-cx, dy = (double)p.y()-cy;
|
||||
return Slic3r::Point((coord_t)std::llround(dx*ct - dy*st + cx),
|
||||
(coord_t)std::llround(dx*st + dy*ct + cy));
|
||||
};
|
||||
for (Slic3r::Layer* l : obj->layers())
|
||||
for (Slic3r::LayerRegion* r : l->regions()) {
|
||||
Slic3r::Surfaces in = r->slices.surfaces;
|
||||
for (auto& sf : in) {
|
||||
for (auto& pt : sf.expolygon.contour.points) pt = rot(pt);
|
||||
for (auto& h : sf.expolygon.holes)
|
||||
for (auto& pt : h.points) pt = rot(pt);
|
||||
}
|
||||
r->slices.set(std::move(in));
|
||||
}
|
||||
});
|
||||
else Slic3r::Print::set_slicing_pipeline_hook_fn(nullptr);
|
||||
init_print({TestMesh::cube_20x20x20}, print, model, config);
|
||||
print.process();
|
||||
double area = 0;
|
||||
coord_t nx=0, xx=0, ny=0, xy=0; bool seeded=false;
|
||||
for (auto* l : print.objects().front()->layers())
|
||||
for (auto* r : l->regions())
|
||||
for (auto& sf : r->fill_surfaces.surfaces) {
|
||||
area += sf.expolygon.area();
|
||||
for (const Slic3r::Point& p : sf.expolygon.contour.points) {
|
||||
if (!seeded) { nx=xx=p.x(); ny=xy=p.y(); seeded=true; }
|
||||
else { nx=std::min(nx,p.x()); xx=std::max(xx,p.x());
|
||||
ny=std::min(ny,p.y()); xy=std::max(xy,p.y()); }
|
||||
}
|
||||
}
|
||||
Slic3r::Print::set_slicing_pipeline_hook_fn(nullptr);
|
||||
return { area, (double)(xx-nx), (double)(xy-ny) };
|
||||
};
|
||||
const Measure base = measure(false);
|
||||
const Measure rot = measure(true);
|
||||
// (1) A pure rotation preserves area (similarity, scale 1): fills add up to the same area.
|
||||
CHECK_THAT(rot.area, WithinRel(base.area, 0.05));
|
||||
// (2) The rotation cascaded downstream: the square's fill bbox grew toward the sqrt(2)
|
||||
// diagonal (diamond) instead of staying axis-aligned.
|
||||
CHECK(rot.width > 1.3 * base.width);
|
||||
CHECK(rot.width < 1.5 * base.width);
|
||||
CHECK(rot.height > 1.3 * base.height);
|
||||
CHECK(rot.height < 1.5 * base.height);
|
||||
}
|
||||
|
||||
// §3.6 (Twistify design): Twistify skips exact-identity layers entirely, but every transformed
|
||||
// layer invokes the set_slices write-back + make_perimeters re-run. This proves that write path
|
||||
// is lossless for already-normalized (CCW contour / CW hole) input -- an active hook that
|
||||
// re-sets every region's slices to their CURRENT geometry (the identity similarity transform)
|
||||
// produces output byte-identical to an active hook that mutates nothing. Both runs are active
|
||||
// (same config dump); the only difference is whether the write path ran, so equality isolates it.
|
||||
TEST_CASE("Identity round-trip through set_slices is byte-identical", "[slicing_pipeline]") {
|
||||
auto run = [](bool roundtrip) {
|
||||
Slic3r::Print print; Slic3r::Model model;
|
||||
auto config = Slic3r::DynamicPrintConfig::full_print_config();
|
||||
config.set_key_value("slicing_pipeline_plugin", new Slic3r::ConfigOptionStrings({"probe"})); // active in both runs
|
||||
Slic3r::Print::set_slicing_pipeline_hook_fn(
|
||||
[roundtrip](Slic3r::Print&, const Slic3r::PrintObject* o, Slic3r::SlicingPipelineStep s){
|
||||
if (!roundtrip || s != Slic3r::SlicingPipelineStep::Slice || !o) return;
|
||||
for (Slic3r::Layer* l : const_cast<Slic3r::PrintObject*>(o)->layers())
|
||||
for (Slic3r::LayerRegion* r : l->regions()) {
|
||||
Slic3r::Surfaces in = r->slices.surfaces; // copy current (already-normalized) geometry
|
||||
r->slices.set(std::move(in)); // write back unchanged: identity transform
|
||||
}
|
||||
});
|
||||
init_print({TestMesh::cube_20x20x20}, print, model, config);
|
||||
std::string g = Slic3r::Test::gcode(print);
|
||||
Slic3r::Print::set_slicing_pipeline_hook_fn(nullptr);
|
||||
return g;
|
||||
};
|
||||
CHECK(strip_nondeterministic_gcode_lines(run(true)) == strip_nondeterministic_gcode_lines(run(false)));
|
||||
}
|
||||
|
||||
#include "libslic3r/ExtrusionEntityCollection.hpp" // count fill paths in the fill-surface cascade test
|
||||
|
||||
// Total leaf ExtrusionPath count under an extrusion (sub)tree (collections recursed into).
|
||||
static size_t count_leaf_paths(const Slic3r::ExtrusionEntity* ee) {
|
||||
if (ee == nullptr) return 0;
|
||||
if (const auto* coll = dynamic_cast<const Slic3r::ExtrusionEntityCollection*>(ee)) {
|
||||
size_t n = 0;
|
||||
for (const Slic3r::ExtrusionEntity* e : coll->entities) n += count_leaf_paths(e);
|
||||
return n;
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
|
||||
// Width (scaled) of the object-wide bounding box over every region's sliced contour.
|
||||
static double outer_slices_width(const Slic3r::Print& print) {
|
||||
coord_t min_x = 0, max_x = 0; bool seeded = false;
|
||||
for (auto* l : print.objects().front()->layers())
|
||||
for (auto* r : l->regions())
|
||||
for (const Slic3r::Surface& sf : r->slices.surfaces)
|
||||
for (const Slic3r::Point& p : sf.expolygon.contour.points) {
|
||||
if (!seeded) { min_x = max_x = p.x(); seeded = true; }
|
||||
else { min_x = std::min(min_x, p.x()); max_x = std::max(max_x, p.x()); }
|
||||
}
|
||||
return (double)(max_x - min_x);
|
||||
}
|
||||
|
||||
// After the Slice hook mutates slices, raw_slices must be re-snapshotted so the mutation
|
||||
// becomes the untyped baseline. make_perimeters() restores untyped slices from raw_slices on
|
||||
// any perimeter re-run; invoking that restore directly must reproduce the mutation, not revert
|
||||
// to the pre-hook geometry (which is what happened before this fix).
|
||||
TEST_CASE("raw_slices captures post-hook geometry so a perimeter re-run keeps the mutation", "[slicing_pipeline]") {
|
||||
using Catch::Matchers::WithinRel;
|
||||
Slic3r::Print::set_slicing_pipeline_hook_fn(
|
||||
[](Slic3r::Print&, const Slic3r::PrintObject* o, Slic3r::SlicingPipelineStep s){
|
||||
if (s != Slic3r::SlicingPipelineStep::Slice || !o) return;
|
||||
for (Slic3r::Layer* l : const_cast<Slic3r::PrintObject*>(o)->layers())
|
||||
for (Slic3r::LayerRegion* r : l->regions()) {
|
||||
Slic3r::Surfaces in = r->slices.surfaces;
|
||||
for (auto& sf : in) {
|
||||
Slic3r::ExPolygons e = offset_ex(sf.expolygon, -scale_(1.0));
|
||||
if (!e.empty()) sf.expolygon = e.front();
|
||||
}
|
||||
r->slices.set(std::move(in));
|
||||
}
|
||||
});
|
||||
Slic3r::Print print; Slic3r::Model model;
|
||||
auto config = Slic3r::DynamicPrintConfig::full_print_config();
|
||||
config.set_key_value("slicing_pipeline_plugin", new Slic3r::ConfigOptionStrings({"probe"}));
|
||||
init_print({TestMesh::cube_20x20x20}, print, model, config);
|
||||
print.process();
|
||||
const double w_mutated = outer_slices_width(print); // inset applied at the Slice hook
|
||||
|
||||
// The same restore make_perimeters() runs on a perimeter-only re-slice. With the post-hook
|
||||
// backup this reproduces the inset; without it this reverts to the wider original outline.
|
||||
for (Slic3r::Layer* l : print.objects().front()->layers())
|
||||
l->restore_untyped_slices();
|
||||
const double w_restored = outer_slices_width(print);
|
||||
Slic3r::Print::set_slicing_pipeline_hook_fn(nullptr);
|
||||
CHECK_THAT(w_restored, WithinRel(w_mutated, 0.02)); // mutation survived the restore
|
||||
}
|
||||
|
||||
// A plugin can mutate fill_surfaces at the new PrepareInfill seam and have make_fills consume
|
||||
// them, whereas the pre-existing Infill seam fires after the fills are already built (v1 limit).
|
||||
// All three runs register a hook (active path) so the comparison isolates only the mutation.
|
||||
TEST_CASE("fill_surfaces mutation cascades at PrepareInfill but not at Infill", "[slicing_pipeline]") {
|
||||
auto fill_paths = [](bool shrink, Slic3r::SlicingPipelineStep at) {
|
||||
Slic3r::Print print; Slic3r::Model model;
|
||||
auto config = Slic3r::DynamicPrintConfig::full_print_config();
|
||||
config.set_key_value("slicing_pipeline_plugin", new Slic3r::ConfigOptionStrings({"probe"}));
|
||||
Slic3r::Print::set_slicing_pipeline_hook_fn(
|
||||
[shrink, at](Slic3r::Print&, const Slic3r::PrintObject* o, Slic3r::SlicingPipelineStep s){
|
||||
if (!shrink || s != at || !o) return;
|
||||
for (Slic3r::Layer* l : const_cast<Slic3r::PrintObject*>(o)->layers())
|
||||
for (Slic3r::LayerRegion* r : l->regions()) {
|
||||
Slic3r::Surfaces in = r->fill_surfaces.surfaces, out;
|
||||
for (const Slic3r::Surface& sf : in)
|
||||
for (const Slic3r::ExPolygon& e : offset_ex(sf.expolygon, -scale_(3.0))) {
|
||||
Slic3r::Surface s2 = sf; s2.expolygon = e; out.push_back(std::move(s2));
|
||||
}
|
||||
r->fill_surfaces.set(std::move(out));
|
||||
}
|
||||
});
|
||||
init_print({TestMesh::cube_20x20x20}, print, model, config);
|
||||
print.process();
|
||||
size_t n = 0;
|
||||
for (auto* l : print.objects().front()->layers())
|
||||
for (auto* r : l->regions())
|
||||
n += count_leaf_paths(&r->fills);
|
||||
Slic3r::Print::set_slicing_pipeline_hook_fn(nullptr);
|
||||
return n;
|
||||
};
|
||||
using S = Slic3r::SlicingPipelineStep;
|
||||
const size_t base = fill_paths(false, S::PrepareInfill); // active hook, no mutation
|
||||
CHECK(base > 0);
|
||||
CHECK(fill_paths(true, S::PrepareInfill) < base); // mutation before make_fills cascades
|
||||
CHECK(fill_paths(true, S::Infill) == base); // mutation after make_fills is a no-op (v1)
|
||||
}
|
||||
|
||||
// lslices (the layer's merged islands) are built once in slice() and never rebuilt by
|
||||
// make_perimeters, so mutating region slices leaves them stale. set_slices(refresh_lslices=True)
|
||||
// re-derives them via Layer::make_slices(); this C++ analogue proves the mechanism -- without the
|
||||
// refresh the islands keep the original 20mm footprint, with it they track the 18mm inset.
|
||||
TEST_CASE("refreshing lslices after a slice mutation makes islands track the geometry", "[slicing_pipeline]") {
|
||||
auto lslices_width = [](bool refresh) {
|
||||
Slic3r::Print print; Slic3r::Model model;
|
||||
auto config = Slic3r::DynamicPrintConfig::full_print_config();
|
||||
config.set_key_value("slicing_pipeline_plugin", new Slic3r::ConfigOptionStrings({"probe"}));
|
||||
Slic3r::Print::set_slicing_pipeline_hook_fn(
|
||||
[refresh](Slic3r::Print&, const Slic3r::PrintObject* o, Slic3r::SlicingPipelineStep s){
|
||||
if (s != Slic3r::SlicingPipelineStep::Slice || !o) return;
|
||||
for (Slic3r::Layer* l : const_cast<Slic3r::PrintObject*>(o)->layers()) {
|
||||
for (Slic3r::LayerRegion* r : l->regions()) {
|
||||
Slic3r::Surfaces in = r->slices.surfaces;
|
||||
for (auto& sf : in) {
|
||||
Slic3r::ExPolygons e = offset_ex(sf.expolygon, -scale_(1.0));
|
||||
if (!e.empty()) sf.expolygon = e.front();
|
||||
}
|
||||
r->slices.set(std::move(in));
|
||||
}
|
||||
if (refresh) // the load-bearing half of set_slices(refresh_lslices=True)
|
||||
l->make_slices();
|
||||
}
|
||||
});
|
||||
init_print({TestMesh::cube_20x20x20}, print, model, config);
|
||||
print.process();
|
||||
coord_t min_x = 0, max_x = 0; bool seeded = false;
|
||||
for (auto* l : print.objects().front()->layers())
|
||||
for (const Slic3r::ExPolygon& island : l->lslices)
|
||||
for (const Slic3r::Point& p : island.contour.points) {
|
||||
if (!seeded) { min_x = max_x = p.x(); seeded = true; }
|
||||
else { min_x = std::min(min_x, p.x()); max_x = std::max(max_x, p.x()); }
|
||||
}
|
||||
Slic3r::Print::set_slicing_pipeline_hook_fn(nullptr);
|
||||
return (double)(max_x - min_x);
|
||||
};
|
||||
using Catch::Matchers::WithinRel;
|
||||
const double stale = lslices_width(false); // islands keep the original ~20 mm footprint
|
||||
const double fresh = lslices_width(true); // islands track the ~18 mm inset region slices
|
||||
CHECK(fresh < stale);
|
||||
CHECK_THAT(stale, WithinRel((double) scale_(20.0), 0.05)); // stale islands = original outline
|
||||
CHECK_THAT(fresh, WithinRel((double) scale_(18.0), 0.05)); // refreshed islands = inset outline
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user