mirror of
https://github.com/OrcaSlicer/OrcaSlicer.git
synced 2026-10-06 15:21:09 +00:00
* Remove Unused Project Includes and Forward-Declare Where a Type Is Only Referenced Generated with include-what-you-use and applied conservatively. Only OrcaSlicer's own headers, the ones under src/ and tests/, are removed or forward-declared; standard-library and third-party includes are left alone. An include is removed only when both the Release and the Debug configuration leave it unused, never from inside a conditional block, and never from a file with platform-specific blocks, which only gain includes. Files whose only use of a header sits behind a feature or debug macro (libvgcode's OpenGL ES and marker code, the ARACHNE/TESTS_EXPORT_SVGS debug output) keep their includes. clonable_ptr.hpp gains #pragma once; it had no include guard and was only safe while Config.hpp was its sole includer. * Remove Unused Project Includes From Files With Platform-Specific Code A Linux include-what-you-use run cannot see the code inside _WIN32, __APPLE__ or __linux__ blocks, so its verdict is only taken where nothing the removed header declares, directly or through what it includes, is named inside those blocks. Removals also have to hold in both the Release and Debug configuration and never touch a line inside a conditional block. * Restore the libslic3r Precompiled Header and Direct Includes Lost in the Platform Pass The platform-file pass treated pchheader.hpp as an ordinary header and emptied it, and left GUI_Preview.hpp and 14 other files relying on headers they no longer reached directly. * Restore MainFrame.hpp in ParamsDialog.cpp for the Windows-Only Reparent Call * Include Headers That Files Reached Through Ones the Cleanup Removed * Drop Includes Duplicated by the Cleanup or by Main's Own Additions * Leave PreciseSeam.cpp as Main Has It After the Precise Seam Rework
587 lines
34 KiB
C++
587 lines
34 KiB
C++
#include <catch2/catch_test_macros.hpp>
|
|
#include "libslic3r/Config.hpp"
|
|
#include "libslic3r/Model.hpp"
|
|
#include <cstddef>
|
|
#include <string>
|
|
#include "libslic3r/Surface.hpp"
|
|
#include <utility>
|
|
#include "libslic3r/libslic3r.h"
|
|
#include "libslic3r/Point.hpp"
|
|
#include <catch2/matchers/catch_matchers.hpp>
|
|
#include "libslic3r/ExtrusionEntity.hpp"
|
|
#include "libslic3r/PrintConfig.hpp"
|
|
#include "test_helpers.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<ConfigOptionStrings>("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::SlicingPipelineStepPlugin){ ++calls; });
|
|
Slic3r::Print::set_slicing_pipeline_hook_fn(nullptr); // reset — must be legal
|
|
CHECK(calls == 0);
|
|
}
|
|
|
|
#include <vector>
|
|
#include <algorithm>
|
|
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::SlicingPipelineStepPlugin step; };
|
|
std::vector<Call> calls;
|
|
Slic3r::Print::set_slicing_pipeline_hook_fn(
|
|
[&](Slic3r::Print&, const Slic3r::PrintObject* o, Slic3r::SlicingPipelineStepPlugin 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({cube(20)}, print, model, config);
|
|
print.process();
|
|
Slic3r::Print::set_slicing_pipeline_hook_fn(nullptr);
|
|
|
|
using S = Slic3r::SlicingPipelineStepPlugin;
|
|
auto count = [&](S s){ return std::count_if(calls.begin(), calls.end(), [&](const Call& c){ return c.step == s; }); };
|
|
CHECK(count(S::posSlice) == 1);
|
|
CHECK(count(S::posPerimeters) == 1);
|
|
CHECK(count(S::posPrepareInfill) == 1); // the prepare-infill seam fires once per object
|
|
CHECK(count(S::posInfill) == 1);
|
|
CHECK(count(S::psWipeTower) == 1);
|
|
CHECK(count(S::psSkirtBrim) == 1);
|
|
// psGCodePostProcess fires from the GUI export path, never from process():
|
|
CHECK(count(S::psGCodePostProcess) == 0);
|
|
// print-wide steps carry a null object:
|
|
for (const auto& c : calls)
|
|
if (c.step == S::psWipeTower || c.step == S::psSkirtBrim) CHECK(c.obj == nullptr);
|
|
// 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::posSlice) < idx(S::posPerimeters));
|
|
CHECK(idx(S::posPerimeters) < idx(S::posPrepareInfill)); // prepare-infill fires after perimeters...
|
|
CHECK(idx(S::posPrepareInfill) < idx(S::posInfill)); // ...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:
|
|
// (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::SlicingPipelineStepPlugin){});
|
|
else
|
|
Slic3r::Print::set_slicing_pipeline_hook_fn(nullptr);
|
|
init_print({cube(20)}, 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
|
|
}
|
|
|
|
// 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::SlicingPipelineStepPlugin){ ++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({cube(20)}, print, model, config);
|
|
print.process();
|
|
Slic3r::Print::set_slicing_pipeline_hook_fn(nullptr);
|
|
CHECK(calls == 0);
|
|
}
|
|
|
|
// 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::SlicingPipelineStepPlugin s){
|
|
if (s == Slic3r::SlicingPipelineStepPlugin::posSlice) ++slice_calls;
|
|
if (s == Slic3r::SlicingPipelineStepPlugin::posPerimeters) ++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({cube(20)}, 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::SlicingPipelineStepPlugin s){
|
|
if (s != Slic3r::SlicingPipelineStepPlugin::posSlice || !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) 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({cube(20)}, 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({cube(20)}, 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
|
|
}
|
|
|
|
// Editing a slicing plugin's config (print_plugin_config_overrides) must re-run posSlice, where the
|
|
// plugin transforms each layer's geometry; otherwise the cached slice keeps the old config's result.
|
|
TEST_CASE("Changing print_plugin_config_overrides invalidates posSlice", "[slicing_pipeline]") {
|
|
Slic3r::Print print; Slic3r::Model model;
|
|
auto config = Slic3r::DynamicPrintConfig::full_print_config();
|
|
init_print({cube(20)}, print, model, config);
|
|
print.process();
|
|
REQUIRE(print.objects().front()->is_step_done(posSlice));
|
|
config.set_key_value("print_plugin_config_overrides",
|
|
new Slic3r::ConfigOptionString("[{\"type\":\"slicing-pipeline\",\"name\":\"Twistify\",\"config\":{\"twist_deg_per_mm\":2.0}}]"));
|
|
print.apply(model, config);
|
|
CHECK_FALSE(print.objects().front()->is_step_done(posSlice)); // re-slice required
|
|
}
|
|
|
|
#include <catch2/matchers/catch_matchers_floating_point.hpp>
|
|
|
|
// A similarity transform (rotate + uniform scale) applied to slices at Step.posSlice, matching
|
|
// what the Twistify plugin (sandboxes/orca_twistify_plugin_any.py) does. This C++ analogue
|
|
// rotates every region's slices a fixed 45 deg about the object's base-footprint center -- the same
|
|
// seam and cascade the sample drives through the slices.set() + Layer::make_slices() path. 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::SlicingPipelineStepPlugin s){
|
|
if (s != Slic3r::SlicingPipelineStepPlugin::posSlice || !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({cube(20)}, 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);
|
|
}
|
|
|
|
// The Twistify sample skips exact-identity layers entirely, but every transformed layer invokes
|
|
// the slices.set() 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 slices.set() 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::SlicingPipelineStepPlugin s){
|
|
if (!roundtrip || s != Slic3r::SlicingPipelineStepPlugin::posSlice || !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({cube(20)}, 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::SlicingPipelineStepPlugin s){
|
|
if (s != Slic3r::SlicingPipelineStepPlugin::posSlice || !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({cube(20)}, 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.
|
|
// 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::SlicingPipelineStepPlugin 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::SlicingPipelineStepPlugin 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({cube(20)}, 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::SlicingPipelineStepPlugin;
|
|
const size_t base = fill_paths(false, S::posPrepareInfill); // active hook, no mutation
|
|
CHECK(base > 0);
|
|
CHECK(fill_paths(true, S::posPrepareInfill) < base); // mutation before make_fills cascades
|
|
CHECK(fill_paths(true, S::posInfill) == base); // mutation after make_fills is a no-op
|
|
}
|
|
|
|
// lslices (the layer's merged islands) are built once in slice() and never rebuilt by
|
|
// make_perimeters, so mutating region slices leaves them stale. The slices.set() + Layer::make_slices()
|
|
// path re-derives them; 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::SlicingPipelineStepPlugin s){
|
|
if (s != Slic3r::SlicingPipelineStepPlugin::posSlice || !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 the slices.set() + Layer::make_slices() path
|
|
l->make_slices();
|
|
}
|
|
});
|
|
init_print({cube(20)}, 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
|
|
}
|
|
|
|
#include <random> // deterministic RNG for the fuzzy-skin analogue below
|
|
#include "libslic3r/ExPolygon.hpp"
|
|
#include "libslic3r/Polygon.hpp"
|
|
#include "libslic3r/SurfaceCollection.hpp"
|
|
|
|
// Fuzzy skin applied to the slice contours at the Slice boundary, matching what the Fuzzy
|
|
// Slices sample (sandboxes/orca_fuzzy_slices_plugin_any.py) does: resample every ring at
|
|
// 3/4..5/4 * point_distance and displace each new vertex +/-thickness along the segment
|
|
// normal (libslic3r's fuzzy_polyline with uniform noise). Unlike the count-preserving rotate
|
|
// test above, this is a count-CHANGING rebuild -- each ring is replaced by one with a
|
|
// different vertex count. Three end-to-end invariants after process() confirm the cascade:
|
|
// (1) the jitter is zero-mean, so total fill area is preserved within a few %,
|
|
// (2) the fuzz genuinely cascaded into make_perimeters' fill_surfaces -- their contours
|
|
// carry far more vertices than the crisp baseline square's,
|
|
// (3) displacement is bounded: the sliced footprint grows by at most ~2*thickness.
|
|
TEST_CASE("Fuzzing slice contours at the Slice boundary cascades with bounded displacement", "[slicing_pipeline]") {
|
|
using Catch::Matchers::WithinRel;
|
|
static constexpr double kThickness = 0.3, kPointDist = 0.8; // mm; the built-in fuzzy-skin defaults
|
|
struct Measure { double area; size_t verts; double width; };
|
|
auto measure = [](bool fuzz) -> 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"})); // active in both runs
|
|
if (fuzz) Slic3r::Print::set_slicing_pipeline_hook_fn(
|
|
[](Slic3r::Print&, const Slic3r::PrintObject* o, Slic3r::SlicingPipelineStepPlugin s){
|
|
if (s != Slic3r::SlicingPipelineStepPlugin::posSlice || !o) return;
|
|
const double thickness = scale_(kThickness);
|
|
const double min_dist = scale_(kPointDist) * 0.75;
|
|
const double rand_range = scale_(kPointDist) * 0.5;
|
|
std::mt19937 rng(0x5EED); // fixed seed: the run is deterministic
|
|
std::uniform_real_distribution<double> uni(0.0, 1.0);
|
|
auto fuzz_ring = [&](Slic3r::Points& pts) {
|
|
if (pts.size() < 3) return;
|
|
Slic3r::Points out;
|
|
double dist_left_over = uni(rng) * (min_dist / 2.0);
|
|
const Slic3r::Point* p0 = &pts.back();
|
|
for (const Slic3r::Point& p1 : pts) {
|
|
const Slic3r::Vec2d v = (p1 - *p0).cast<double>();
|
|
const double seg = v.norm();
|
|
if (seg > 0.0) {
|
|
double d = dist_left_over;
|
|
for (; d < seg; d += min_dist + uni(rng) * rand_range) {
|
|
const double r = (uni(rng) * 2.0 - 1.0) * thickness;
|
|
const Slic3r::Vec2d pa = p0->cast<double>() + v * (d / seg);
|
|
const Slic3r::Vec2d n = Slic3r::Vec2d(-v.y(), v.x()) / seg;
|
|
out.emplace_back((coord_t) std::llround(pa.x() + n.x() * r),
|
|
(coord_t) std::llround(pa.y() + n.y() * r));
|
|
}
|
|
dist_left_over = d - seg;
|
|
}
|
|
p0 = &p1;
|
|
}
|
|
if (out.size() >= 3) pts = std::move(out); // else: ring too short, keep it crisp
|
|
};
|
|
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) {
|
|
fuzz_ring(sf.expolygon.contour.points);
|
|
for (auto& h : sf.expolygon.holes) fuzz_ring(h.points);
|
|
}
|
|
r->slices.set(std::move(in));
|
|
}
|
|
});
|
|
else Slic3r::Print::set_slicing_pipeline_hook_fn(nullptr);
|
|
init_print({cube(20)}, print, model, config);
|
|
print.process();
|
|
Measure m { 0.0, 0, outer_slices_width(print) };
|
|
for (auto* l : print.objects().front()->layers())
|
|
for (auto* r : l->regions())
|
|
for (auto& sf : r->fill_surfaces.surfaces) {
|
|
m.area += sf.expolygon.area();
|
|
m.verts += sf.expolygon.contour.points.size();
|
|
}
|
|
Slic3r::Print::set_slicing_pipeline_hook_fn(nullptr);
|
|
return m;
|
|
};
|
|
const Measure base = measure(false);
|
|
const Measure fz = measure(true);
|
|
// (1) Zero-mean jitter: the fills add up to (nearly) the same area.
|
|
CHECK_THAT(fz.area, WithinRel(base.area, 0.05));
|
|
// (2) The resample cascaded downstream: fill boundaries derived from the fuzzed slices
|
|
// carry far more vertices than the baseline square's.
|
|
CHECK(fz.verts > 4 * base.verts);
|
|
// (3) Displacement is bounded by the +/-thickness jitter: the footprint widened, but by
|
|
// no more than ~2*thickness (one thickness per side, plus rounding slack).
|
|
CHECK(fz.width > base.width);
|
|
CHECK(fz.width < base.width + 2.5 * scale_(kThickness));
|
|
}
|