#include #include "libslic3r/PrintConfig.hpp" using namespace Slic3r; TEST_CASE("slicing_pipeline_plugin option exists and defaults empty", "[slicing_pipeline]") { DynamicPrintConfig cfg = DynamicPrintConfig::full_print_config(); const ConfigOptionStrings* opt = cfg.option("slicing_pipeline_plugin"); REQUIRE(opt != nullptr); CHECK(opt->values.empty()); const ConfigOptionDef* def = cfg.def()->get("slicing_pipeline_plugin"); REQUIRE(def != nullptr); CHECK(def->plugin_type == "slicing-pipeline"); CHECK(def->is_plugin_backed()); CHECK(def->gui_type == ConfigOptionDef::GUIType::plugin_picker); } #include "libslic3r/Print.hpp" TEST_CASE("slicing pipeline hook setter is a no-op-safe injection", "[slicing_pipeline]") { int calls = 0; Slic3r::Print::set_slicing_pipeline_hook_fn( [&](Slic3r::Print&, const Slic3r::PrintObject*, Slic3r::SlicingPipelineStep){ ++calls; }); Slic3r::Print::set_slicing_pipeline_hook_fn(nullptr); // reset — must be legal CHECK(calls == 0); } #include "test_data.hpp" #include #include using namespace Slic3r::Test; TEST_CASE("SlicingPipeline hook fires once per step per object in order", "[slicing_pipeline]") { struct Call { const Slic3r::PrintObject* obj; Slic3r::SlicingPipelineStep step; }; std::vector calls; Slic3r::Print::set_slicing_pipeline_hook_fn( [&](Slic3r::Print&, const Slic3r::PrintObject* o, Slic3r::SlicingPipelineStep s){ calls.push_back({o, s}); }); Slic3r::Print print; Slic3r::Model model; Slic3r::DynamicPrintConfig config = Slic3r::DynamicPrintConfig::full_print_config(); config.set_key_value("slicing_pipeline_plugin", new Slic3r::ConfigOptionStrings({"probe"})); // activate init_print({TestMesh::cube_20x20x20}, print, model, config); print.process(); Slic3r::Print::set_slicing_pipeline_hook_fn(nullptr); using S = Slic3r::SlicingPipelineStep; 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); // print-wide steps carry a null object: for (const auto& c : calls) if (c.step == S::WipeTower || c.step == S::SkirtBrim) CHECK(c.obj == nullptr); // Slice must fire before Perimeters for the same object: auto idx = [&](S s){ for (size_t i=0;i #include // 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 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: // (activate=false, hook=none) baseline -- feature entirely absent. // (activate=false, hook=noop) hook registered but option empty -> gated off, never fires. // (activate=true, hook=noop) hook ACTIVE and firing at every pipeline seam, mutating // nothing. This is the real backward-compat claim: an active // but non-mutating hook must not perturb the output. auto run = [](bool activate, bool set_noop_hook) { Slic3r::Print print; Slic3r::Model model; auto config = Slic3r::DynamicPrintConfig::full_print_config(); // Activating requires BOTH a non-empty option and a registered hook (see Print::apply). if (activate) config.set_key_value("slicing_pipeline_plugin", new Slic3r::ConfigOptionStrings({"probe"})); if (set_noop_hook) Slic3r::Print::set_slicing_pipeline_hook_fn([](Slic3r::Print&, const Slic3r::PrintObject*, Slic3r::SlicingPipelineStep){}); else Slic3r::Print::set_slicing_pipeline_hook_fn(nullptr); 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; }; // 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 // registered hook must NOT fire even once across a full slice (m_pipeline_plugin_active // stays false in Print::apply). Distinct from the byte-identical test above: this asserts // the gate directly by counting invocations rather than comparing output. TEST_CASE("Empty option: registered hook is gated off and never fires", "[slicing_pipeline]") { int calls = 0; Slic3r::Print::set_slicing_pipeline_hook_fn( [&](Slic3r::Print&, const Slic3r::PrintObject*, Slic3r::SlicingPipelineStep){ ++calls; }); Slic3r::Print print; Slic3r::Model model; auto config = Slic3r::DynamicPrintConfig::full_print_config(); // option left EMPTY -> inactive regardless of the registered hook. init_print({TestMesh::cube_20x20x20}, print, model, config); print.process(); Slic3r::Print::set_slicing_pipeline_hook_fn(nullptr); CHECK(calls == 0); } // Fix 4(b): duplicate-skip gating. Two ModelObjects that share one mesh_ptr are detected as // identical by Print::process()'s is_print_object_the_same(); the second becomes a shared // (duplicate) object and is NOT re-sliced, so the Slice hook must fire exactly once even // though there are two print objects. The clone shares mesh_ptr and copies the volume // transformation/config (ModelVolume copy ctor), which the equality check requires. TEST_CASE("Duplicate objects share a slice: Slice hook fires exactly once", "[slicing_pipeline]") { int slice_calls = 0, perim_calls = 0; Slic3r::Print::set_slicing_pipeline_hook_fn( [&](Slic3r::Print&, const Slic3r::PrintObject*, Slic3r::SlicingPipelineStep s){ if (s == Slic3r::SlicingPipelineStep::Slice) ++slice_calls; if (s == Slic3r::SlicingPipelineStep::Perimeters) ++perim_calls; }); Slic3r::Print print; Slic3r::Model model; auto config = Slic3r::DynamicPrintConfig::full_print_config(); config.set_key_value("slicing_pipeline_plugin", new Slic3r::ConfigOptionStrings({"probe"})); // activate // init_print builds one arranged, on-bed cube object (o1). init_print({TestMesh::cube_20x20x20}, print, model, config); Slic3r::ModelObject* o1 = model.objects.front(); // Model::add_object(const ModelObject&) force-sets object extruder=1 on the clone; give o1 // the same so the two objects' configs match (is_print_object_the_same compares config). if (!o1->config.has("extruder")) o1->config.set_key_value("extruder", new Slic3r::ConfigOptionInt(1)); // Clone o1: shares mesh_ptr and copies the volume transformation + config (genuine duplicate). Slic3r::ModelObject* o2 = model.add_object(*o1); // Shift the clone in X so validate() sees no collision (20mm cubes -> 40mm centres = 20mm gap). for (Slic3r::ModelInstance* inst : o2->instances) inst->set_offset(inst->get_offset() + Slic3r::Vec3d(40.0, 0.0, 0.0)); print.apply(model, config); print.validate(); print.set_status_silent(); print.process(); Slic3r::Print::set_slicing_pipeline_hook_fn(nullptr); REQUIRE(print.objects().size() == 2); // two print objects present... CHECK(slice_calls == 1); // ...but the duplicate is skipped -> one slice CHECK(perim_calls == 1); // and one perimeters pass (the sliced object) } #include "libslic3r/Layer.hpp" // Layer, LayerRegion (full defs for the cascade hook) #include "libslic3r/ClipperUtils.hpp" // offset_ex // 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 // fill_surfaces area must shrink relative to a baseline (un-inset) run. This proves // the mutation cascade end-to-end using the same C++ APIs the Python mutators wrap. TEST_CASE("Mutating slices at the Slice boundary cascades downstream", "[slicing_pipeline]") { auto fill_area = [](bool inset) { 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 (inset) 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(o)->layers()) for (Slic3r::LayerRegion* r : l->regions()) { Slic3r::Surfaces in = r->slices.surfaces; for (auto& sf : in) sf.expolygon = offset_ex(sf.expolygon, -scale_(1.0)).front(); 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 a = 0; for (auto* l : print.objects().front()->layers()) for (auto* r : l->regions()) for (auto& s : r->fill_surfaces.surfaces) a += s.expolygon.area(); Slic3r::Print::set_slicing_pipeline_hook_fn(nullptr); return a; }; CHECK(fill_area(true) < fill_area(false)); } TEST_CASE("Changing slicing_pipeline_plugin invalidates posSlice", "[slicing_pipeline]") { Slic3r::Print print; Slic3r::Model model; auto config = Slic3r::DynamicPrintConfig::full_print_config(); init_print({TestMesh::cube_20x20x20}, print, model, config); print.process(); REQUIRE(print.objects().front()->is_step_done(posSlice)); config.set_key_value("slicing_pipeline_plugin", new Slic3r::ConfigOptionStrings({"probe"})); print.apply(model, config); CHECK_FALSE(print.objects().front()->is_step_done(posSlice)); // re-slice required } #include // §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(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(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(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(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(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(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 }