mirror of
https://github.com/OrcaSlicer/OrcaSlicer.git
synced 2026-09-11 19:17:33 +00:00
Merge branch 'main' into feature/texture_displacement
This commit is contained in:
@@ -18,7 +18,10 @@ add_executable(${_TEST_NAME}_tests
|
||||
test_preset_setting_id.cpp
|
||||
test_preset_diff.cpp
|
||||
test_elephant_foot_compensation.cpp
|
||||
test_fill_corner_smoothing.cpp
|
||||
test_fill_plane_path.cpp
|
||||
test_geometry.cpp
|
||||
test_multimaterial_segmentation.cpp
|
||||
test_placeholder_parser.cpp
|
||||
test_polygon.cpp
|
||||
test_mutable_polygon.cpp
|
||||
@@ -27,10 +30,12 @@ add_executable(${_TEST_NAME}_tests
|
||||
test_stl.cpp
|
||||
test_meshboolean.cpp
|
||||
test_marchingsquares.cpp
|
||||
test_model.cpp
|
||||
test_utils.cpp
|
||||
test_timeutils.cpp
|
||||
test_voronoi.cpp
|
||||
test_optimizers.cpp
|
||||
test_ordering_strategies.cpp
|
||||
# test_png_io.cpp
|
||||
test_indexed_triangle_set.cpp
|
||||
test_texture_displacement.cpp
|
||||
|
||||
@@ -9,6 +9,8 @@
|
||||
#include "libslic3r/MultiNozzleUtils.hpp"
|
||||
#include "libslic3r/ProjectTask.hpp"
|
||||
|
||||
#include "test_utils.hpp"
|
||||
|
||||
#include <boost/filesystem/operations.hpp>
|
||||
|
||||
#include <catch2/catch_tostring.hpp>
|
||||
@@ -109,8 +111,8 @@ SCENARIO("Export+Import geometry to/from 3mf file cycle", "[3mf]") {
|
||||
src_object->instances.front()->set_transformation(src_instance_transform);
|
||||
|
||||
WHEN("model is saved+loaded to/from 3mf file") {
|
||||
// save the model to 3mf file
|
||||
std::string test_file = std::string(TEST_DATA_DIR) + "/test_3mf/prusa.3mf";
|
||||
ScopedTemporaryFile temp(".3mf");
|
||||
const std::string test_file = temp.string();
|
||||
store_3mf(test_file.c_str(), &src_model, nullptr, false);
|
||||
|
||||
// load back the model from the 3mf file
|
||||
@@ -120,7 +122,6 @@ SCENARIO("Export+Import geometry to/from 3mf file cycle", "[3mf]") {
|
||||
ConfigSubstitutionContext ctxt{ ForwardCompatibilitySubstitutionRule::Disable };
|
||||
load_3mf(test_file.c_str(), dst_config, ctxt, &dst_model, false);
|
||||
}
|
||||
boost::filesystem::remove(test_file);
|
||||
|
||||
// compare meshes
|
||||
TriangleMesh src_mesh = src_model.mesh();
|
||||
@@ -154,10 +155,8 @@ SCENARIO("H2C multi-nozzle .3mf round-trip", "[3mf][MultiNozzle]") {
|
||||
|
||||
// store_bbs_3mf stages Metadata/project_settings.config through the model's backup path;
|
||||
// point it at a writable temp dir (the default lives under a read-only root in CI).
|
||||
std::string backup_dir =
|
||||
(boost::filesystem::temp_directory_path() / boost::filesystem::unique_path("orca_mn_%%%%%%%%")).string();
|
||||
boost::filesystem::create_directories(backup_dir);
|
||||
model.set_backup_path(backup_dir);
|
||||
ScopedTemporaryDir backup_dir("orca_mn");
|
||||
model.set_backup_path(backup_dir.string());
|
||||
|
||||
// Global (printer) config: give nozzle_volume_type a non-default value so the slice_info
|
||||
// read-back is a meaningful assertion (High Flow == 1).
|
||||
@@ -179,7 +178,8 @@ SCENARIO("H2C multi-nozzle .3mf round-trip", "[3mf][MultiNozzle]") {
|
||||
plate->config.set_key_value("enable_filament_dynamic_map", new ConfigOptionBool(true));
|
||||
|
||||
WHEN("stored to and reloaded from a .3mf") {
|
||||
std::string test_file = std::string(TEST_DATA_DIR) + "/test_3mf/mn_roundtrip.3mf";
|
||||
ScopedTemporaryFile temp(".3mf");
|
||||
const std::string test_file = temp.string();
|
||||
|
||||
StoreParams store_params;
|
||||
store_params.path = test_file.c_str();
|
||||
@@ -201,8 +201,6 @@ SCENARIO("H2C multi-nozzle .3mf round-trip", "[3mf][MultiNozzle]") {
|
||||
bool loaded = load_bbs_3mf(test_file.c_str(), &dst_config, &ctxt, &dst_model, &dst_plates,
|
||||
&project_presets, &is_bbl_3mf, &is_orca_3mf, &file_version, nullptr,
|
||||
LoadStrategy::LoadModel | LoadStrategy::LoadConfig);
|
||||
boost::filesystem::remove(test_file);
|
||||
|
||||
THEN("every multi-nozzle key round-trips as expected") {
|
||||
REQUIRE(loaded);
|
||||
REQUIRE(dst_plates.size() >= 1);
|
||||
@@ -232,7 +230,6 @@ SCENARIO("H2C multi-nozzle .3mf round-trip", "[3mf][MultiNozzle]") {
|
||||
release_PlateData_list(dst_plates);
|
||||
}
|
||||
delete plate; // store_bbs_3mf does not take ownership of the source plate
|
||||
boost::filesystem::remove_all(backup_dir);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -249,10 +246,8 @@ SCENARIO("Non-standard nozzle diameter survives .3mf save on a single-nozzle pri
|
||||
REQUIRE(load_stl(src_file.c_str(), &model));
|
||||
model.add_default_instances();
|
||||
|
||||
std::string backup_dir =
|
||||
(boost::filesystem::temp_directory_path() / boost::filesystem::unique_path("orca_nd_%%%%%%%%")).string();
|
||||
boost::filesystem::create_directories(backup_dir);
|
||||
model.set_backup_path(backup_dir);
|
||||
ScopedTemporaryDir backup_dir("orca_nd");
|
||||
model.set_backup_path(backup_dir.string());
|
||||
|
||||
// Single extruder with a non-standard 0.5 mm nozzle; extruder_max_nozzle_count stays at its
|
||||
// default (no nozzle cluster), so the writer must emit the exact config diameter.
|
||||
@@ -275,7 +270,8 @@ SCENARIO("Non-standard nozzle diameter survives .3mf save on a single-nozzle pri
|
||||
plate->slice_filaments_info.push_back(fi);
|
||||
|
||||
WHEN("stored to and reloaded from a .3mf") {
|
||||
std::string test_file = std::string(TEST_DATA_DIR) + "/test_3mf/nd_roundtrip.3mf";
|
||||
ScopedTemporaryFile temp(".3mf");
|
||||
const std::string test_file = temp.string();
|
||||
|
||||
StoreParams store_params;
|
||||
store_params.path = test_file.c_str();
|
||||
@@ -295,8 +291,6 @@ SCENARIO("Non-standard nozzle diameter survives .3mf save on a single-nozzle pri
|
||||
bool loaded = load_bbs_3mf(test_file.c_str(), &dst_config, &ctxt, &dst_model, &dst_plates,
|
||||
&project_presets, &is_bbl_3mf, &is_orca_3mf, &file_version, nullptr,
|
||||
LoadStrategy::LoadModel | LoadStrategy::LoadConfig);
|
||||
boost::filesystem::remove(test_file);
|
||||
|
||||
THEN("the saved nozzle diameter is the exact 0.5, not the rounded 0.4") {
|
||||
REQUIRE(loaded);
|
||||
REQUIRE(dst_plates.size() >= 1);
|
||||
@@ -314,7 +308,6 @@ SCENARIO("Non-standard nozzle diameter survives .3mf save on a single-nozzle pri
|
||||
release_PlateData_list(dst_plates);
|
||||
}
|
||||
delete plate; // store_bbs_3mf does not take ownership of the source plate
|
||||
boost::filesystem::remove_all(backup_dir);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -435,10 +428,8 @@ SCENARIO("Nozzle-group metadata .3mf round-trip", "[3mf][MultiNozzle]") {
|
||||
REQUIRE(load_stl(src_file.c_str(), &model));
|
||||
model.add_default_instances();
|
||||
|
||||
std::string backup_dir =
|
||||
(boost::filesystem::temp_directory_path() / boost::filesystem::unique_path("orca_ng_%%%%%%%%")).string();
|
||||
boost::filesystem::create_directories(backup_dir);
|
||||
model.set_backup_path(backup_dir);
|
||||
ScopedTemporaryDir backup_dir("orca_ng");
|
||||
model.set_backup_path(backup_dir.string());
|
||||
|
||||
DynamicPrintConfig config = DynamicPrintConfig::full_print_config();
|
||||
|
||||
@@ -458,7 +449,8 @@ SCENARIO("Nozzle-group metadata .3mf round-trip", "[3mf][MultiNozzle]") {
|
||||
plate->config.set_key_value("filament_map", new ConfigOptionInts({ 1, 2, 1 }));
|
||||
|
||||
WHEN("stored to and reloaded from a .3mf") {
|
||||
std::string test_file = std::string(TEST_DATA_DIR) + "/test_3mf/ng_roundtrip.3mf";
|
||||
ScopedTemporaryFile temp(".3mf");
|
||||
const std::string test_file = temp.string();
|
||||
|
||||
StoreParams store_params;
|
||||
store_params.path = test_file.c_str();
|
||||
@@ -478,8 +470,6 @@ SCENARIO("Nozzle-group metadata .3mf round-trip", "[3mf][MultiNozzle]") {
|
||||
bool loaded = load_bbs_3mf(test_file.c_str(), &dst_config, &ctxt, &dst_model, &dst_plates,
|
||||
&project_presets, &is_bbl_3mf, &is_orca_3mf, &file_version, nullptr,
|
||||
LoadStrategy::LoadModel | LoadStrategy::LoadConfig);
|
||||
boost::filesystem::remove(test_file);
|
||||
|
||||
THEN("the <nozzle> tags round-trip into the loaded plate's nozzles_info") {
|
||||
REQUIRE(loaded);
|
||||
REQUIRE(dst_plates.size() >= 1);
|
||||
@@ -505,67 +495,5 @@ SCENARIO("Nozzle-group metadata .3mf round-trip", "[3mf][MultiNozzle]") {
|
||||
release_PlateData_list(dst_plates);
|
||||
}
|
||||
delete plate;
|
||||
boost::filesystem::remove_all(backup_dir);
|
||||
}
|
||||
}
|
||||
|
||||
SCENARIO("2D convex hull of sinking object", "[3mf][.]") {
|
||||
GIVEN("model") {
|
||||
// load a model
|
||||
Model model;
|
||||
std::string src_file = std::string(TEST_DATA_DIR) + "/test_3mf/Prusa.stl";
|
||||
REQUIRE(load_stl(src_file.c_str(), &model));
|
||||
model.add_default_instances();
|
||||
|
||||
WHEN("model is rotated, scaled and set as sinking") {
|
||||
ModelObject* object = model.objects[0];
|
||||
object->center_around_origin(false);
|
||||
|
||||
// This outputs the same exact data as the Prusaslicer test
|
||||
object->volumes[0]->mesh().write_ascii("/tmp/orca.ascii");
|
||||
|
||||
// set instance's attitude so that it is rotated, scaled (and sinking? how is it sinking? the rotation? does it matter if it's sinking?)
|
||||
ModelInstance* instance = object->instances[0];
|
||||
instance->set_rotation(X, -M_PI / 4.0);
|
||||
instance->set_offset(Vec3d::Zero());
|
||||
instance->set_scaling_factor({ 2.0, 2.0, 2.0 });
|
||||
|
||||
// calculate 2D convex hull
|
||||
auto trafo = instance->get_transformation().get_matrix();
|
||||
|
||||
// This matrix is the same exact matrix as the Prusaslicer test
|
||||
CAPTURE(trafo);
|
||||
Polygon hull_2d = object->convex_hull_2d(trafo);
|
||||
|
||||
// But we get different hull_2d.points here (and somehow decimal numbers despite being int64_t values, but that's probabaly printing configuration somewhere -- Prusaslicer's prints out with newlines between the X&Y and not one between coordinates, which is about the worse possible output).
|
||||
// I think it's something to do with PrusaSlicer ignoring everything under the Z plane, which makes sense from the results.
|
||||
// See the comments added to ModelObject::convex_hull_2d for more information.
|
||||
|
||||
// verify result
|
||||
Points result = {
|
||||
{ -91501496, -15914144 },
|
||||
{ 91501496, -15914144 },
|
||||
{ 91501496, 4243 },
|
||||
{ 78229680, 4246883 },
|
||||
{ 56898100, 4246883 },
|
||||
{ -85501496, 4242641 },
|
||||
{ -91501496, 4243 }
|
||||
};
|
||||
|
||||
THEN("2D convex hull should match with reference") {
|
||||
// Allow 1um error due to floating point rounding.
|
||||
bool res = hull_2d.points.size() == result.size();
|
||||
if (res) {
|
||||
for (size_t i = 0; i < result.size(); ++ i) {
|
||||
const Point &p1 = result[i];
|
||||
const Point &p2 = hull_2d.points[i];
|
||||
CHECK((std::abs(p1.x() - p2.x()) > 1 || std::abs(p1.y() - p2.y()) > 1));
|
||||
}
|
||||
}
|
||||
|
||||
CAPTURE(hull_2d.points);
|
||||
REQUIRE(res);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
#include <catch2/catch_all.hpp>
|
||||
|
||||
#include <algorithm>
|
||||
#include <sstream>
|
||||
#include <string>
|
||||
|
||||
#include "libslic3r/calib.hpp"
|
||||
#include "libslic3r/Model.hpp"
|
||||
#include "libslic3r/TriangleMesh.hpp"
|
||||
@@ -38,3 +42,69 @@ TEST_CASE("Zero calibration line width resolves to a positive default", "[Calib]
|
||||
REQUIRE(pattern.line_width() > 0.);
|
||||
REQUIRE(pattern.line_width_first_layer() > 0.);
|
||||
}
|
||||
|
||||
namespace {
|
||||
|
||||
struct EndState { double final_e; double max_e; };
|
||||
|
||||
EndState simulate_absolute_e(const std::string &gcode)
|
||||
{
|
||||
double final_e = 0.;
|
||||
double max_e = 0.;
|
||||
|
||||
std::istringstream lines(gcode);
|
||||
std::string line;
|
||||
while (std::getline(lines, line)) {
|
||||
std::istringstream words(line);
|
||||
std::string op;
|
||||
if (!(words >> op))
|
||||
continue;
|
||||
if (op != "G1" && op != "G0" && op != "G92")
|
||||
continue;
|
||||
|
||||
std::string word;
|
||||
while (words >> word) {
|
||||
if (word.size() >= 2 && word[0] == 'E') {
|
||||
final_e = std::stod(word.substr(1));
|
||||
max_e = std::max(max_e, final_e);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {final_e, max_e};
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
TEST_CASE("PA pattern resets the extruder after the final layer in absolute E mode", "[Calib][Regression]")
|
||||
{
|
||||
DynamicPrintConfig config = DynamicPrintConfig::full_print_config();
|
||||
config.set_deserialize_strict({
|
||||
{"use_relative_e_distances", "0"},
|
||||
{"line_width", "0.45"},
|
||||
{"initial_layer_line_width", "0.45"},
|
||||
});
|
||||
|
||||
Model model;
|
||||
model.add_object("cube", "", make_cube(20, 20, 20))->add_instance();
|
||||
|
||||
Calib_Params params;
|
||||
params.mode = CalibMode::Calib_PA_Pattern;
|
||||
params.start = 0.;
|
||||
params.end = 0.08;
|
||||
params.step = 0.002;
|
||||
|
||||
CalibPressureAdvancePattern pattern(params, config, /* is_bbl_machine */ false, *model.objects.front(), Vec3d(0, 0, 0));
|
||||
const CustomGCode::Info info = pattern.generate_custom_gcodes(config, /* is_bbl_machine */ false, *model.objects.front(),
|
||||
Vec3d(0, 0, 0));
|
||||
|
||||
std::string gcode;
|
||||
for (const CustomGCode::Item &item : info.gcodes)
|
||||
gcode += item.extra;
|
||||
|
||||
const EndState state = simulate_absolute_e(gcode);
|
||||
|
||||
REQUIRE(state.max_e > 1.);
|
||||
REQUIRE_THAT(state.final_e, Catch::Matchers::WithinAbs(0., 1e-9));
|
||||
}
|
||||
|
||||
@@ -4,6 +4,8 @@
|
||||
#include "libslic3r/PrintConfigConstants.hpp"
|
||||
#include "libslic3r/LocalesUtils.hpp"
|
||||
|
||||
#include "test_utils.hpp"
|
||||
|
||||
#include <cereal/types/polymorphic.hpp>
|
||||
#include <cereal/types/string.hpp>
|
||||
#include <cereal/types/vector.hpp>
|
||||
@@ -233,6 +235,56 @@ SCENARIO("Config ini load/save interface", "[Config]") {
|
||||
}
|
||||
}
|
||||
|
||||
TEST_CASE("Flush-volume warning predicate respects used filament transitions", "[Config][Regression]")
|
||||
{
|
||||
const std::vector<double> multipliers = {1.0};
|
||||
|
||||
SECTION("Single used filament does not trigger warning with zero transition entries")
|
||||
{
|
||||
const std::vector<double> matrix = {
|
||||
0.0, 0.0,
|
||||
0.0, 0.0
|
||||
};
|
||||
const std::vector<int> used_filaments = {1};
|
||||
|
||||
REQUIRE_FALSE(has_zero_flush_volume_for_used_filaments(matrix, multipliers, used_filaments));
|
||||
}
|
||||
|
||||
SECTION("Two used filaments trigger warning when transition flush entry is zero")
|
||||
{
|
||||
const std::vector<double> matrix = {
|
||||
0.0, 0.0,
|
||||
0.0, 0.0
|
||||
};
|
||||
const std::vector<int> used_filaments = {1, 2};
|
||||
|
||||
REQUIRE(has_zero_flush_volume_for_used_filaments(matrix, multipliers, used_filaments));
|
||||
}
|
||||
|
||||
SECTION("Two used filaments do not trigger warning when transitions are non-zero")
|
||||
{
|
||||
const std::vector<double> matrix = {
|
||||
0.0, 280.0,
|
||||
280.0, 0.0
|
||||
};
|
||||
const std::vector<int> used_filaments = {1, 2};
|
||||
|
||||
REQUIRE_FALSE(has_zero_flush_volume_for_used_filaments(matrix, multipliers, used_filaments));
|
||||
}
|
||||
|
||||
SECTION("Zero multiplier still triggers warning when multiple filaments are used")
|
||||
{
|
||||
const std::vector<double> matrix = {
|
||||
0.0, 280.0,
|
||||
280.0, 0.0
|
||||
};
|
||||
const std::vector<double> zero_multiplier = {0.0};
|
||||
const std::vector<int> used_filaments = {1, 2};
|
||||
|
||||
REQUIRE(has_zero_flush_volume_for_used_filaments(matrix, zero_multiplier, used_filaments));
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: https://github.com/SoftFever/OrcaSlicer/issues/11269 - Is this test still relevant? Delete if not.
|
||||
// It was failing so at least "nozzle_type" and "extruder_printable_area" could not be serialized
|
||||
// and an exception was thrown, but "nozzle_type" has been around for at least 3 months now.
|
||||
@@ -407,8 +459,7 @@ SCENARIO("update_diff_values_to_child_config tolerates legacy machine-limit vect
|
||||
// }
|
||||
|
||||
TEST_CASE("save_to_json round-trips plugin capability references as strings", "[Config][plugins]") {
|
||||
namespace fs = boost::filesystem;
|
||||
const fs::path tmp = fs::temp_directory_path() / fs::unique_path("orca_plugins_%%%%-%%%%.json");
|
||||
ScopedTemporaryFile tmp(".json");
|
||||
const std::vector<std::string> refs = {
|
||||
"local_plugin;;inset",
|
||||
"cloud_plugin;550e8400-e29b-41d4-a716-446655440000;inset"
|
||||
@@ -435,8 +486,6 @@ TEST_CASE("save_to_json round-trips plugin capability references as strings", "[
|
||||
REQUIRE(reloaded.load_from_json(tmp.string(), substitutions, true, key_values, reason) == 0);
|
||||
CHECK(reason.empty());
|
||||
CHECK(reloaded.option<ConfigOptionStrings>("slicing_pipeline_plugin")->values == refs);
|
||||
|
||||
fs::remove(tmp);
|
||||
}
|
||||
|
||||
TEST_CASE("plugin capability references survive string-map serialization", "[Config][plugins]") {
|
||||
|
||||
@@ -43,18 +43,67 @@ TEST_CASE("apply_override fills nil entries from the 0-based default index", "[C
|
||||
REQUIRE(resolved.values == std::vector<double>({30., 42.}));
|
||||
}
|
||||
|
||||
SECTION("an index past the machine slots falls back to the first slot") {
|
||||
SECTION("an index past the machine slots keeps the slot's own value") {
|
||||
std::vector<int> slot_index{5, 0};
|
||||
ConfigOptionFloats resolved(machine);
|
||||
REQUIRE(resolved.apply_override(&filament, slot_index));
|
||||
REQUIRE(resolved.values == std::vector<double>({10., 42.}));
|
||||
}
|
||||
|
||||
SECTION("a negative index (unresolved slot) falls back to the first slot") {
|
||||
std::vector<int> slot_index{-1, 0};
|
||||
SECTION("a negative index (unresolved slot) keeps the slot's own value") {
|
||||
ConfigOptionFloatsNullable all_nil;
|
||||
all_nil.values = {ConfigOptionFloatsNullable::nil_value(), ConfigOptionFloatsNullable::nil_value(),
|
||||
ConfigOptionFloatsNullable::nil_value()};
|
||||
std::vector<int> slot_index{2, -1, 0};
|
||||
ConfigOptionFloats resolved(machine);
|
||||
REQUIRE(resolved.apply_override(&filament, slot_index));
|
||||
REQUIRE(resolved.values == std::vector<double>({10., 42.}));
|
||||
REQUIRE(!resolved.apply_override(&all_nil, slot_index));
|
||||
REQUIRE(resolved.values == std::vector<double>({30., 20., 10.}));
|
||||
}
|
||||
|
||||
SECTION("all-nil overrides keyed by unresolved slots leave the machine values intact") {
|
||||
// The failed-lookup map a degenerate print_extruder_id used to produce; the negative
|
||||
// slots must not collapse the machine array to its first value.
|
||||
ConfigOptionFloats per_extruder({100., 70., 70., 70., 100.});
|
||||
ConfigOptionFloatsNullable all_nil;
|
||||
all_nil.values.assign(5, ConfigOptionFloatsNullable::nil_value());
|
||||
std::vector<int> slot_index{0, -1, -1, -1, 0};
|
||||
ConfigOptionFloats resolved(per_extruder);
|
||||
REQUIRE(!resolved.apply_override(&all_nil, slot_index));
|
||||
REQUIRE(resolved.values == std::vector<double>({100., 70., 70., 70., 100.}));
|
||||
}
|
||||
}
|
||||
|
||||
TEST_CASE("support_different_extruders is true only when the printer defines more than one variant column", "[Config]")
|
||||
{
|
||||
int extruder_count = 0;
|
||||
|
||||
SECTION("a non-Bambu dual-nozzle printer with one variant column reports false") {
|
||||
DynamicPrintConfig config;
|
||||
config.option<ConfigOptionFloats>("nozzle_diameter", true)->values = {0.4, 0.4};
|
||||
// Both extruders resolve to the same default variant, so there is only one column.
|
||||
config.option<ConfigOptionStrings>("extruder_variant_list", true)->values = {"Direct Drive Standard",
|
||||
"Direct Drive Standard"};
|
||||
REQUIRE(config.support_different_extruders(extruder_count) == false);
|
||||
REQUIRE(extruder_count == 2);
|
||||
}
|
||||
|
||||
SECTION("a Bambu H2D-style printer with distinct variants reports true") {
|
||||
DynamicPrintConfig config;
|
||||
config.option<ConfigOptionFloats>("nozzle_diameter", true)->values = {0.4, 0.4};
|
||||
config.option<ConfigOptionStrings>("extruder_variant_list", true)->values = {
|
||||
"Direct Drive Standard,Direct Drive High Flow",
|
||||
"Direct Drive Standard,Direct Drive High Flow,Direct Drive TPU High Flow"};
|
||||
REQUIRE(config.support_different_extruders(extruder_count) == true);
|
||||
REQUIRE(extruder_count == 2);
|
||||
}
|
||||
|
||||
SECTION("a many-toolhead printer that never opts into variants reports false") {
|
||||
// A Snapmaker U1 has four identical toolheads and never defines extruder_variant_list,
|
||||
// so the config falls back to a single default variant token.
|
||||
DynamicPrintConfig config;
|
||||
config.option<ConfigOptionFloats>("nozzle_diameter", true)->values = {0.4, 0.4, 0.4, 0.4};
|
||||
REQUIRE(config.support_different_extruders(extruder_count) == false);
|
||||
REQUIRE(extruder_count == 4);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -238,6 +287,102 @@ TEST_CASE("update_values_to_printer_extruders expands one slot per (extruder x v
|
||||
}
|
||||
}
|
||||
|
||||
TEST_CASE("update_values_to_printer_extruders synthesizes degenerate process variant columns", "[Config]")
|
||||
{
|
||||
// Non-BBL process presets and 3mf project configs keep the length-1 defaults for
|
||||
// print_extruder_id/print_extruder_variant; only BBL system presets ship full-width columns.
|
||||
auto add_degenerate_print_columns = [](DynamicPrintConfig &config) {
|
||||
config.option<ConfigOptionInts>("print_extruder_id", true)->values = {1};
|
||||
config.option<ConfigOptionStrings>("print_extruder_variant", true)->values = {"Direct Drive Standard"};
|
||||
config.option<ConfigOptionFloats>("outer_wall_speed", true)->values = {30.};
|
||||
};
|
||||
|
||||
SECTION("a single-column pair on a multi-extruder machine expands to one column per extruder") {
|
||||
DynamicPrintConfig config;
|
||||
config.option<ConfigOptionEnumsGeneric>("extruder_type", true)->values = {etDirectDrive, etDirectDrive};
|
||||
config.option<ConfigOptionEnumsGeneric>("nozzle_volume_type", true)->values = {nvtStandard, nvtStandard};
|
||||
config.option<ConfigOptionStrings>("extruder_variant_list", true)->values = {"Direct Drive Standard", "Direct Drive Standard"};
|
||||
add_degenerate_print_columns(config);
|
||||
|
||||
std::vector<std::vector<NozzleVolumeType>> nozzle_volume_types;
|
||||
int extruder_count = 2;
|
||||
int count = config.get_extruder_nozzle_volume_count(extruder_count, nozzle_volume_types);
|
||||
|
||||
std::vector<int> variant_index = config.update_values_to_printer_extruders(config, extruder_count, count, nozzle_volume_types,
|
||||
print_options_with_variant, "print_extruder_id", "print_extruder_variant");
|
||||
|
||||
REQUIRE(variant_index == std::vector<int>({0, 1}));
|
||||
REQUIRE(config.option<ConfigOptionInts>("print_extruder_id")->values == std::vector<int>({1, 2}));
|
||||
REQUIRE(config.option<ConfigOptionStrings>("print_extruder_variant")->values ==
|
||||
std::vector<std::string>({"Direct Drive Standard", "Direct Drive Standard"}));
|
||||
// width-1 data arrays replicate their only column into every slot
|
||||
REQUIRE(config.option<ConfigOptionFloats>("outer_wall_speed")->values == std::vector<double>({30., 30.}));
|
||||
}
|
||||
|
||||
SECTION("a multi-variant list synthesizes one column per (extruder x variant)") {
|
||||
DynamicPrintConfig config = make_hybrid_printer_config();
|
||||
add_degenerate_print_columns(config);
|
||||
|
||||
std::vector<std::vector<NozzleVolumeType>> nozzle_volume_types;
|
||||
int extruder_count = 2;
|
||||
int count = config.get_extruder_nozzle_volume_count(extruder_count, nozzle_volume_types);
|
||||
REQUIRE(count == 3);
|
||||
|
||||
std::vector<int> variant_index = config.update_values_to_printer_extruders(config, extruder_count, count, nozzle_volume_types,
|
||||
print_options_with_variant, "print_extruder_id", "print_extruder_variant");
|
||||
|
||||
// same slot resolution as the explicit BBL-style 4-column layout
|
||||
REQUIRE(variant_index == std::vector<int>({0, 2, 3}));
|
||||
REQUIRE(config.option<ConfigOptionInts>("print_extruder_id")->values == std::vector<int>({1, 2, 2}));
|
||||
REQUIRE(config.option<ConfigOptionStrings>("print_extruder_variant")->values ==
|
||||
std::vector<std::string>({"Direct Drive Standard", "Direct Drive Standard", "Direct Drive High Flow"}));
|
||||
REQUIRE(config.option<ConfigOptionFloats>("outer_wall_speed")->values == std::vector<double>({30., 30., 30.}));
|
||||
}
|
||||
|
||||
SECTION("a single-extruder single-column layout is not treated as degenerate") {
|
||||
DynamicPrintConfig config;
|
||||
config.option<ConfigOptionEnumsGeneric>("extruder_type", true)->values = {etDirectDrive};
|
||||
config.option<ConfigOptionEnumsGeneric>("nozzle_volume_type", true)->values = {nvtStandard};
|
||||
config.option<ConfigOptionStrings>("extruder_variant_list", true)->values = {"Direct Drive Standard"};
|
||||
add_degenerate_print_columns(config);
|
||||
|
||||
std::vector<std::vector<NozzleVolumeType>> nozzle_volume_types;
|
||||
int extruder_count = 1;
|
||||
int count = config.get_extruder_nozzle_volume_count(extruder_count, nozzle_volume_types);
|
||||
|
||||
config.update_values_to_printer_extruders(config, extruder_count, count, nozzle_volume_types,
|
||||
print_options_with_variant, "print_extruder_id", "print_extruder_variant");
|
||||
|
||||
REQUIRE(config.option<ConfigOptionInts>("print_extruder_id")->values == std::vector<int>({1}));
|
||||
REQUIRE(config.option<ConfigOptionFloats>("outer_wall_speed")->values == std::vector<double>({30.}));
|
||||
}
|
||||
|
||||
SECTION("a second expansion leaves the synthesized layout unchanged") {
|
||||
DynamicPrintConfig config;
|
||||
config.option<ConfigOptionEnumsGeneric>("extruder_type", true)->values = {etDirectDrive, etDirectDrive};
|
||||
config.option<ConfigOptionEnumsGeneric>("nozzle_volume_type", true)->values = {nvtStandard, nvtStandard};
|
||||
config.option<ConfigOptionStrings>("extruder_variant_list", true)->values = {"Direct Drive Standard", "Direct Drive Standard"};
|
||||
add_degenerate_print_columns(config);
|
||||
|
||||
std::vector<std::vector<NozzleVolumeType>> nozzle_volume_types;
|
||||
int extruder_count = 2;
|
||||
int count = config.get_extruder_nozzle_volume_count(extruder_count, nozzle_volume_types);
|
||||
|
||||
config.update_values_to_printer_extruders(config, extruder_count, count, nozzle_volume_types,
|
||||
print_options_with_variant, "print_extruder_id", "print_extruder_variant");
|
||||
DynamicPrintConfig once = config;
|
||||
config.update_values_to_printer_extruders(config, extruder_count, count, nozzle_volume_types,
|
||||
print_options_with_variant, "print_extruder_id", "print_extruder_variant");
|
||||
|
||||
REQUIRE(config.option<ConfigOptionInts>("print_extruder_id")->values ==
|
||||
once.option<ConfigOptionInts>("print_extruder_id")->values);
|
||||
REQUIRE(config.option<ConfigOptionStrings>("print_extruder_variant")->values ==
|
||||
once.option<ConfigOptionStrings>("print_extruder_variant")->values);
|
||||
REQUIRE(config.option<ConfigOptionFloats>("outer_wall_speed")->values ==
|
||||
once.option<ConfigOptionFloats>("outer_wall_speed")->values);
|
||||
}
|
||||
}
|
||||
|
||||
TEST_CASE("update_values_to_printer_extruders_for_multiple_filaments resolves per-filament slots", "[Config]")
|
||||
{
|
||||
auto make_filament_arrays = [](DynamicPrintConfig &config) {
|
||||
|
||||
173
tests/libslic3r/test_fill_corner_smoothing.cpp
Normal file
173
tests/libslic3r/test_fill_corner_smoothing.cpp
Normal file
@@ -0,0 +1,173 @@
|
||||
#include <catch2/catch_all.hpp>
|
||||
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <limits>
|
||||
|
||||
#include "libslic3r/Fill/FillCornerSmoothing.hpp"
|
||||
#include "libslic3r/Polyline.hpp"
|
||||
#include "libslic3r/libslic3r.h"
|
||||
|
||||
using namespace Slic3r;
|
||||
|
||||
namespace {
|
||||
|
||||
// A right angle turn, with the outgoing leg ten times longer than the incoming one.
|
||||
Polyline asymmetric_corner()
|
||||
{
|
||||
return Polyline{ Point::new_scale(0., 0.), Point::new_scale(10., 0.), Point::new_scale(10., 100.) };
|
||||
}
|
||||
|
||||
double max_turn_cosine(const Polyline &polyline)
|
||||
{
|
||||
double sharpest = 1.;
|
||||
for (size_t i = 1; i + 1 < polyline.size(); ++i) {
|
||||
const Vec2d incoming = (polyline[i] - polyline[i - 1]).cast<double>().normalized();
|
||||
const Vec2d outgoing = (polyline[i + 1] - polyline[i]).cast<double>().normalized();
|
||||
sharpest = std::min(sharpest, incoming.dot(outgoing));
|
||||
}
|
||||
return sharpest;
|
||||
}
|
||||
|
||||
bool contains(const Polyline &polyline, const Point &point)
|
||||
{
|
||||
return std::find(polyline.points.begin(), polyline.points.end(), point) != polyline.points.end();
|
||||
}
|
||||
|
||||
const double tolerance = scaled<double>(0.0125);
|
||||
|
||||
} // namespace
|
||||
|
||||
TEST_CASE("Corner smoothing replaces a sharp vertex by a curve", "[FillCornerSmoothing]")
|
||||
{
|
||||
const Polyline sharp = asymmetric_corner();
|
||||
Polyline smooth = sharp;
|
||||
smooth_polyline_corners(smooth, 1., tolerance);
|
||||
|
||||
REQUIRE(smooth.size() > sharp.size());
|
||||
REQUIRE(smooth.front() == sharp.front());
|
||||
REQUIRE(smooth.back() == sharp.back());
|
||||
// The right angle is gone, every remaining turn is a gentle one.
|
||||
REQUIRE(max_turn_cosine(sharp) < 0.1);
|
||||
REQUIRE(max_turn_cosine(smooth) > 0.9);
|
||||
REQUIRE(smooth.length() < sharp.length());
|
||||
}
|
||||
|
||||
TEST_CASE("Corner smoothing keeps the path untouched at a zero factor", "[FillCornerSmoothing]")
|
||||
{
|
||||
const Polyline sharp = asymmetric_corner();
|
||||
|
||||
Polyline none = sharp;
|
||||
smooth_polyline_corners(none, 0., tolerance);
|
||||
REQUIRE(none.points == sharp.points);
|
||||
|
||||
Polyline invalid = sharp;
|
||||
smooth_polyline_corners(invalid, std::numeric_limits<double>::quiet_NaN(), tolerance);
|
||||
REQUIRE(invalid.points == sharp.points);
|
||||
}
|
||||
|
||||
TEST_CASE("Corner smoothing consumes at most half of the shorter leg", "[FillCornerSmoothing]")
|
||||
{
|
||||
// The curve must not reach beyond the middle of either adjoining segment, otherwise the curves of
|
||||
// two adjacent corners would overlap. The shorter leg is 10mm long, so the corner at (10, 0) is
|
||||
// left 5mm before it and rejoined 5mm past it, even though the other leg is 100mm long.
|
||||
Polyline smooth = asymmetric_corner();
|
||||
smooth_polyline_corners(smooth, 1., tolerance);
|
||||
|
||||
REQUIRE(contains(smooth, Point::new_scale(5., 0.)));
|
||||
REQUIRE(contains(smooth, Point::new_scale(10., 5.)));
|
||||
// A Bezier curve stays within the convex hull of its control points, so the rounded path stays
|
||||
// inside the box spanned by the two legs.
|
||||
for (const Point &point : smooth.points) {
|
||||
REQUIRE(point.x() >= 0);
|
||||
REQUIRE(point.y() >= 0);
|
||||
REQUIRE(point.x() <= Point::new_scale(10., 0.).x());
|
||||
REQUIRE(point.y() <= Point::new_scale(0., 100.).y());
|
||||
}
|
||||
}
|
||||
|
||||
TEST_CASE("Corner smoothing scales the curve with the factor", "[FillCornerSmoothing]")
|
||||
{
|
||||
Polyline half = asymmetric_corner();
|
||||
smooth_polyline_corners(half, 0.5, tolerance);
|
||||
Polyline full = asymmetric_corner();
|
||||
smooth_polyline_corners(full, 1., tolerance);
|
||||
|
||||
// Half of the factor leaves the 10mm leg half as far from the corner.
|
||||
REQUIRE(contains(half, Point::new_scale(7.5, 0.)));
|
||||
REQUIRE(contains(full, Point::new_scale(5., 0.)));
|
||||
// A larger factor rounds a wider portion of the legs, cutting more of the corner off.
|
||||
REQUIRE(full.length() < half.length());
|
||||
}
|
||||
|
||||
TEST_CASE("Corner smoothing leaves hairpins sharp", "[FillCornerSmoothing]")
|
||||
{
|
||||
// Both ends of a curve replacing a nearly reversing turn coincide, which would round the hairpin
|
||||
// into a degenerate loop instead of a tip.
|
||||
Polyline hairpin{ Point::new_scale(0., 0.), Point::new_scale(10., 0.), Point::new_scale(0., 0.5) };
|
||||
const Polyline sharp = hairpin;
|
||||
smooth_polyline_corners(hairpin, 1., tolerance);
|
||||
REQUIRE(hairpin == sharp);
|
||||
}
|
||||
|
||||
TEST_CASE("Corner smoothing follows the flattening tolerance", "[FillCornerSmoothing]")
|
||||
{
|
||||
Polyline coarse = asymmetric_corner();
|
||||
smooth_polyline_corners(coarse, 1., scaled<double>(0.2));
|
||||
Polyline fine = asymmetric_corner();
|
||||
smooth_polyline_corners(fine, 1., scaled<double>(0.001));
|
||||
|
||||
REQUIRE(fine.size() > coarse.size());
|
||||
REQUIRE(fine.front() == coarse.front());
|
||||
REQUIRE(fine.back() == coarse.back());
|
||||
}
|
||||
|
||||
TEST_CASE("Corner smoothing emits no zero length segments", "[FillCornerSmoothing]")
|
||||
{
|
||||
// Fully smoothed adjacent corners meet at the midpoint of the segment they share.
|
||||
Polyline zigzag;
|
||||
for (int i = 0; i < 8; ++i)
|
||||
zigzag.points.emplace_back(Point::new_scale(i, i % 2 ? 1. : 0.));
|
||||
smooth_polyline_corners(zigzag, 1., tolerance);
|
||||
|
||||
for (size_t i = 1; i < zigzag.size(); ++i)
|
||||
REQUIRE((zigzag[i] - zigzag[i - 1]).cast<double>().squaredNorm() > 0.);
|
||||
}
|
||||
|
||||
TEST_CASE("Corner smoothing rounds every vertex of a polygon", "[FillCornerSmoothing]")
|
||||
{
|
||||
// A polygon closes implicitly, so none of its corners may stay sharp, not even the first one.
|
||||
const Polygon square{ Point::new_scale(0., 0.), Point::new_scale(10., 0.), Point::new_scale(10., 10.),
|
||||
Point::new_scale(0., 10.) };
|
||||
Polygons smooth{ square };
|
||||
smooth_polygons_corners(smooth, 1., tolerance);
|
||||
const Polyline rounded = smooth.front().split_at_first_point();
|
||||
|
||||
REQUIRE(smooth.front().size() > square.size());
|
||||
REQUIRE(max_turn_cosine(rounded) > 0.9);
|
||||
// The turn from the closing segment back into the first one must be gentle as well.
|
||||
const Vec2d incoming = (rounded[rounded.size() - 1] - rounded[rounded.size() - 2]).cast<double>().normalized();
|
||||
const Vec2d outgoing = (rounded[1] - rounded[0]).cast<double>().normalized();
|
||||
REQUIRE(incoming.dot(outgoing) > 0.9);
|
||||
// None of the corners is cut by more than half of a 10mm side.
|
||||
for (const Point &point : smooth.front().points) {
|
||||
REQUIRE(point.x() >= 0);
|
||||
REQUIRE(point.y() >= 0);
|
||||
REQUIRE(point.x() <= Point::new_scale(10., 0.).x());
|
||||
REQUIRE(point.y() <= Point::new_scale(0., 10.).y());
|
||||
}
|
||||
}
|
||||
|
||||
TEST_CASE("Corner smoothing keeps the ends of a path that returns to its start", "[FillCornerSmoothing][Regression]")
|
||||
{
|
||||
// A branch of a lightning tree walks out and retraces its way back, ending where it started. Its
|
||||
// ends are two free ends that happen to coincide, and joining them would close it into a loop.
|
||||
Polyline retrace{ Point::new_scale(0., 0.), Point::new_scale(10., 0.), Point::new_scale(10., 10.),
|
||||
Point::new_scale(5., 10.), Point::new_scale(0., 0.) };
|
||||
const Polyline sharp = retrace;
|
||||
smooth_polyline_corners(retrace, 1., tolerance);
|
||||
|
||||
REQUIRE(retrace.size() > sharp.size());
|
||||
REQUIRE(retrace.front() == sharp.front());
|
||||
REQUIRE(retrace.back() == sharp.back());
|
||||
}
|
||||
218
tests/libslic3r/test_fill_plane_path.cpp
Normal file
218
tests/libslic3r/test_fill_plane_path.cpp
Normal file
@@ -0,0 +1,218 @@
|
||||
#include <catch2/catch_all.hpp>
|
||||
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <limits>
|
||||
#include <utility>
|
||||
|
||||
#include "libslic3r/Fill/FillPlanePath.hpp"
|
||||
#include "libslic3r/PrintConfig.hpp"
|
||||
|
||||
using namespace Slic3r;
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr double output_scale = 1'000'000.;
|
||||
|
||||
class TestableHilbertCurve : public FillHilbertCurve
|
||||
{
|
||||
public:
|
||||
Points generate_points(double resolution, double smooth_factor = 0., coord_t max_coordinate = 7)
|
||||
{
|
||||
InfillPolylineOutput output(output_scale);
|
||||
FillParams params;
|
||||
params.smooth_factor = smooth_factor;
|
||||
FillHilbertCurve::generate(0, 0, max_coordinate, max_coordinate, resolution, params, output);
|
||||
return std::move(output.result());
|
||||
}
|
||||
};
|
||||
|
||||
class TestableOctagramSpiral : public FillOctagramSpiral
|
||||
{
|
||||
public:
|
||||
Points generate_points(double resolution, double smooth_factor = 0., coord_t max_coordinate = 7)
|
||||
{
|
||||
InfillPolylineOutput output(output_scale);
|
||||
FillParams params;
|
||||
params.smooth_factor = smooth_factor;
|
||||
FillOctagramSpiral::generate(-max_coordinate, -max_coordinate, max_coordinate, max_coordinate, resolution, params, output);
|
||||
return std::move(output.result());
|
||||
}
|
||||
};
|
||||
|
||||
// Cosine of the sharpest turn of a path, 1 meaning it has no turn at all.
|
||||
double sharpest_turn_cosine(const Points &points)
|
||||
{
|
||||
double sharpest = 1.;
|
||||
for (size_t i = 1; i + 1 < points.size(); ++i) {
|
||||
const Vec2d incoming = (points[i] - points[i - 1]).cast<double>().normalized();
|
||||
const Vec2d outgoing = (points[i + 1] - points[i]).cast<double>().normalized();
|
||||
sharpest = std::min(sharpest, incoming.dot(outgoing));
|
||||
}
|
||||
return sharpest;
|
||||
}
|
||||
|
||||
double path_length(const Points &points)
|
||||
{
|
||||
double length = 0.;
|
||||
for (size_t i = 1; i < points.size(); ++i)
|
||||
length += (points[i] - points[i - 1]).cast<double>().norm();
|
||||
return length;
|
||||
}
|
||||
|
||||
double discrete_curvature_at(const Points &points, const Point &point)
|
||||
{
|
||||
const auto point_it = std::find(points.begin(), points.end(), point);
|
||||
REQUIRE(point_it != points.end());
|
||||
const size_t point_idx = size_t(std::distance(points.begin(), point_it));
|
||||
REQUIRE(point_idx > 0);
|
||||
REQUIRE(point_idx + 1 < points.size());
|
||||
|
||||
const Vec2d incoming = (points[point_idx] - points[point_idx - 1]).cast<double>() / output_scale;
|
||||
const Vec2d outgoing = (points[point_idx + 1] - points[point_idx]).cast<double>() / output_scale;
|
||||
const Vec2d chord = incoming + outgoing;
|
||||
const double cross = std::abs(incoming.x() * outgoing.y() - incoming.y() * outgoing.x());
|
||||
return 2. * cross / (incoming.norm() * outgoing.norm() * chord.norm());
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
TEST_CASE("Hilbert curve exposes a smoothing factor", "[FillPlanePath]")
|
||||
{
|
||||
const ConfigOptionDef *factor_def = print_config_def.get("sparse_infill_smooth_factor");
|
||||
REQUIRE(factor_def != nullptr);
|
||||
REQUIRE(factor_def->type == coPercent);
|
||||
REQUIRE_THAT(factor_def->min, Catch::Matchers::WithinAbs(0., 1e-12));
|
||||
REQUIRE_THAT(factor_def->max, Catch::Matchers::WithinAbs(100., 1e-12));
|
||||
REQUIRE_THAT(factor_def->get_default_value<ConfigOptionPercent>()->value,
|
||||
Catch::Matchers::WithinAbs(0., 1e-12));
|
||||
}
|
||||
|
||||
TEST_CASE("Hilbert curve smoothing rounds right angle turns", "[FillPlanePath]")
|
||||
{
|
||||
const Points sharp = TestableHilbertCurve().generate_points(0.005);
|
||||
const Points smooth = TestableHilbertCurve().generate_points(0.005, 1.);
|
||||
|
||||
REQUIRE(smooth.front() == sharp.front());
|
||||
REQUIRE(smooth.back() == sharp.back());
|
||||
REQUIRE(smooth.size() > sharp.size());
|
||||
|
||||
bool has_turn = false;
|
||||
for (size_t i = 1; i < smooth.size(); ++i) {
|
||||
const Vec2d segment = (smooth[i] - smooth[i - 1]).cast<double>();
|
||||
REQUIRE(segment.squaredNorm() > 0.);
|
||||
}
|
||||
for (size_t i = 1; i + 1 < smooth.size(); ++i) {
|
||||
const Vec2d incoming = (smooth[i] - smooth[i - 1]).cast<double>();
|
||||
const Vec2d outgoing = (smooth[i + 1] - smooth[i]).cast<double>();
|
||||
const double cross = incoming.x() * outgoing.y() - incoming.y() * outgoing.x();
|
||||
const double cosine = incoming.dot(outgoing) / (incoming.norm() * outgoing.norm());
|
||||
has_turn |= std::abs(cross) > 0.;
|
||||
REQUIRE(cosine > 0.);
|
||||
}
|
||||
REQUIRE(has_turn);
|
||||
|
||||
const coord_t upper_bound = coord_t(7 * output_scale);
|
||||
for (const Point &point : smooth) {
|
||||
REQUIRE(point.x() >= 0);
|
||||
REQUIRE(point.y() >= 0);
|
||||
REQUIRE(point.x() <= upper_bound);
|
||||
REQUIRE(point.y() <= upper_bound);
|
||||
}
|
||||
}
|
||||
|
||||
TEST_CASE("Smoothed Hilbert curve honors path resolution", "[FillPlanePath]")
|
||||
{
|
||||
const Points coarse = TestableHilbertCurve().generate_points(0.1, 1.);
|
||||
const Points fine = TestableHilbertCurve().generate_points(0.001, 1.);
|
||||
|
||||
REQUIRE(fine.size() > coarse.size());
|
||||
REQUIRE(fine.front() == coarse.front());
|
||||
REQUIRE(fine.back() == coarse.back());
|
||||
}
|
||||
|
||||
TEST_CASE("Smoothed Hilbert corners use a uniform subdivision depth", "[FillPlanePath]")
|
||||
{
|
||||
const Points smooth = TestableHilbertCurve().generate_points(0.0035, 1., 1);
|
||||
const Point curve_entry(0, coord_t(0.5 * output_scale));
|
||||
const Point curve_exit(coord_t(0.5 * output_scale), coord_t(output_scale));
|
||||
|
||||
const auto entry_it = std::find(smooth.begin(), smooth.end(), curve_entry);
|
||||
REQUIRE(entry_it != smooth.end());
|
||||
const auto exit_it = std::find(entry_it, smooth.end(), curve_exit);
|
||||
REQUIRE(exit_it != smooth.end());
|
||||
|
||||
const size_t segment_count = size_t(std::distance(entry_it, exit_it));
|
||||
REQUIRE(segment_count > 1);
|
||||
REQUIRE((segment_count & (segment_count - 1)) == 0);
|
||||
|
||||
double previous_length = (entry_it[1] - entry_it[0]).cast<double>().norm();
|
||||
REQUIRE(previous_length > 0.);
|
||||
double max_length_ratio = 1.;
|
||||
for (size_t segment = 1; segment < segment_count; ++segment) {
|
||||
const double current_length = (entry_it[segment + 1] - entry_it[segment]).cast<double>().norm();
|
||||
REQUIRE(current_length > 0.);
|
||||
max_length_ratio = std::max(max_length_ratio,
|
||||
std::max(current_length / previous_length, previous_length / current_length));
|
||||
previous_length = current_length;
|
||||
}
|
||||
REQUIRE(max_length_ratio < 1.5);
|
||||
}
|
||||
|
||||
TEST_CASE("Hilbert smoothing joins straight segments with continuous curvature", "[FillPlanePath]")
|
||||
{
|
||||
const Points coarse = TestableHilbertCurve().generate_points(0.005, 0.5, 1);
|
||||
const Points fine = TestableHilbertCurve().generate_points(0.0001, 0.5, 1);
|
||||
const Point first_curve_entry(0, coord_t(0.75 * output_scale));
|
||||
|
||||
const double coarse_entry_curvature = discrete_curvature_at(coarse, first_curve_entry);
|
||||
const double fine_entry_curvature = discrete_curvature_at(fine, first_curve_entry);
|
||||
REQUIRE(coarse_entry_curvature > 0.);
|
||||
REQUIRE(fine_entry_curvature < 0.25 * coarse_entry_curvature);
|
||||
}
|
||||
|
||||
TEST_CASE("Octagram spiral smoothing rounds the turns of the spiral", "[FillPlanePath]")
|
||||
{
|
||||
const Points sharp = TestableOctagramSpiral().generate_points(0.005);
|
||||
const Points smooth = TestableOctagramSpiral().generate_points(0.005, 1.);
|
||||
|
||||
REQUIRE(smooth.size() > sharp.size());
|
||||
REQUIRE(smooth.front() == sharp.front());
|
||||
REQUIRE(smooth.back() == sharp.back());
|
||||
// The spiral alternates between 90 and 135 degree turns; both are rounded into gentle ones.
|
||||
REQUIRE(sharpest_turn_cosine(sharp) < -0.7);
|
||||
REQUIRE(sharpest_turn_cosine(smooth) > 0.9);
|
||||
|
||||
for (size_t i = 1; i < smooth.size(); ++i)
|
||||
REQUIRE((smooth[i] - smooth[i - 1]).cast<double>().squaredNorm() > 0.);
|
||||
}
|
||||
|
||||
TEST_CASE("Octagram spiral smooth factor controls corner curvature", "[FillPlanePath]")
|
||||
{
|
||||
const Points sharp = TestableOctagramSpiral().generate_points(0.005);
|
||||
const Points half_smooth = TestableOctagramSpiral().generate_points(0.005, 0.5);
|
||||
const Points full_smooth = TestableOctagramSpiral().generate_points(0.005, 1.);
|
||||
const Points invalid_factor = TestableOctagramSpiral().generate_points(
|
||||
0.005, std::numeric_limits<double>::quiet_NaN());
|
||||
|
||||
REQUIRE(path_length(full_smooth) < path_length(half_smooth));
|
||||
REQUIRE(path_length(half_smooth) < path_length(sharp));
|
||||
REQUIRE(invalid_factor == sharp);
|
||||
}
|
||||
|
||||
TEST_CASE("Hilbert curve smooth factor controls corner curvature", "[FillPlanePath]")
|
||||
{
|
||||
const Points sharp = TestableHilbertCurve().generate_points(0.005);
|
||||
const Points half_smooth = TestableHilbertCurve().generate_points(0.005, 0.5);
|
||||
const Points full_smooth = TestableHilbertCurve().generate_points(0.005, 1.);
|
||||
const Points invalid_factor = TestableHilbertCurve().generate_points(
|
||||
0.005, std::numeric_limits<double>::quiet_NaN());
|
||||
|
||||
REQUIRE(full_smooth.front() == half_smooth.front());
|
||||
REQUIRE(full_smooth.back() == half_smooth.back());
|
||||
REQUIRE(path_length(full_smooth) < path_length(half_smooth));
|
||||
REQUIRE(invalid_factor == sharp);
|
||||
|
||||
for (size_t i = 1; i < full_smooth.size(); ++i)
|
||||
REQUIRE((full_smooth[i] - full_smooth[i - 1]).squaredNorm() > 0);
|
||||
}
|
||||
@@ -4,6 +4,8 @@
|
||||
|
||||
#include "libslic3r/SLA/Hollowing.hpp"
|
||||
|
||||
#include "test_utils.hpp"
|
||||
|
||||
TEST_CASE("Hollow two overlapping spheres") {
|
||||
using namespace Slic3r;
|
||||
|
||||
@@ -16,6 +18,6 @@ TEST_CASE("Hollow two overlapping spheres") {
|
||||
|
||||
sla::hollow_mesh(sphere1, sla::HollowingConfig{}, sla::HollowingFlags::hfRemoveInsideTriangles);
|
||||
|
||||
sphere1.WriteOBJFile("twospheres.obj");
|
||||
write_debug_obj("hollowing/twospheres.obj", sphere1);
|
||||
}
|
||||
|
||||
|
||||
@@ -5,6 +5,8 @@
|
||||
|
||||
#include "libslic3r/TriangleMesh.hpp"
|
||||
|
||||
#include "test_utils.hpp"
|
||||
|
||||
using namespace Slic3r;
|
||||
|
||||
TEST_CASE("Split empty mesh", "[its_split][its]") {
|
||||
@@ -29,13 +31,15 @@ TEST_CASE("Split simple mesh consisting of one part", "[its_split][its]") {
|
||||
REQUIRE(res.front().vertices.size() == cube.vertices.size());
|
||||
}
|
||||
|
||||
// Dump each split part as its own OBJ for eyeballing; no-op in release.
|
||||
void debug_write_obj(const std::vector<indexed_triangle_set> &res, const std::string &name)
|
||||
{
|
||||
#ifndef NDEBUG
|
||||
size_t part_idx = 0;
|
||||
for (auto &part_its : res) {
|
||||
its_write_obj(part_its, (name + std::to_string(part_idx++) + ".obj").c_str());
|
||||
}
|
||||
for (const auto &part_its : res)
|
||||
write_debug_obj("indexed_triangle_set/" + name + std::to_string(part_idx++) + ".obj", part_its);
|
||||
#else
|
||||
(void) res; (void) name;
|
||||
#endif
|
||||
}
|
||||
|
||||
@@ -260,7 +264,6 @@ TEST_CASE("Reduce one edge by Quadric Edge Collapse", "[its]")
|
||||
CHECK(is_similar(its_, its, cfg));
|
||||
}
|
||||
|
||||
#include "test_utils.hpp"
|
||||
TEST_CASE("Simplify mesh by Quadric edge collapse to 5%", "[its]")
|
||||
{
|
||||
TriangleMesh mesh = load_model("frog_legs.obj");
|
||||
|
||||
@@ -191,22 +191,21 @@ static void test_expolys(Rst&& rst, const ExPolygons& ref, Vec2i32 window, const
|
||||
for (const ExPolygon& expoly : ref)
|
||||
rst.draw(expoly);
|
||||
|
||||
std::fstream out(name + ".png", std::ios::out);
|
||||
out << rst.encode(sla::PNGRasterEncoder{});
|
||||
out.close();
|
||||
write_debug_stream("marchingsquares/" + name + ".png",
|
||||
[&] { return rst.encode(sla::PNGRasterEncoder{}); });
|
||||
|
||||
const ExPolygons bmp = rstGetPolys(rst);
|
||||
const ExPolygons ext = sla::raster_to_polygons(rst, window);
|
||||
|
||||
SVG svg(name + ".svg", raster_bb);
|
||||
svg.draw(bmp, "green");
|
||||
if (pixel_size.x() >= scale_(0.5))
|
||||
svg.draw_grid(raster_bb, "grey", scale_(0.05), pixel_size.x());
|
||||
if (window_size.x() >= scale_(1.0))
|
||||
svg.draw_grid(raster_bb, "grey", scale_(0.10), window_size.x());
|
||||
svg.draw_outline(ref, "red", "red", scale_(0.3));
|
||||
svg.draw_outline(ext, "blue", "blue");
|
||||
svg.Close();
|
||||
write_debug_svg("marchingsquares/" + name + ".svg", raster_bb, [&](SVG &svg) {
|
||||
svg.draw(bmp, "green");
|
||||
if (pixel_size.x() >= scale_(0.5))
|
||||
svg.draw_grid(raster_bb, "grey", scale_(0.05), pixel_size.x());
|
||||
if (window_size.x() >= scale_(1.0))
|
||||
svg.draw_grid(raster_bb, "grey", scale_(0.10), window_size.x());
|
||||
svg.draw_outline(ref, "red", "red", scale_(0.3));
|
||||
svg.draw_outline(ext, "blue", "blue");
|
||||
});
|
||||
|
||||
// Note all these areas are unscaled back to mm^2.
|
||||
double raster_area = unscaled(unscaled(area(bmp)));
|
||||
@@ -432,9 +431,7 @@ static void recreate_object_from_rasters(const std::string& objname, float lh)
|
||||
double disp_w = 120.96;
|
||||
double disp_h = 68.04;
|
||||
|
||||
#ifndef NDEBUG
|
||||
size_t cntr = 0;
|
||||
#endif
|
||||
for (ExPolygons& layer : layers) {
|
||||
auto rst = create_raster(res, disp_w, disp_h);
|
||||
|
||||
@@ -442,11 +439,8 @@ static void recreate_object_from_rasters(const std::string& objname, float lh)
|
||||
rst.draw(island);
|
||||
}
|
||||
|
||||
#ifndef NDEBUG
|
||||
std::fstream out(objname + std::to_string(cntr) + ".png", std::ios::out);
|
||||
out << rst.encode(sla::PNGRasterEncoder{});
|
||||
out.close();
|
||||
#endif
|
||||
write_debug_stream("marchingsquares/" + objname + std::to_string(cntr) + ".png",
|
||||
[&] { return rst.encode(sla::PNGRasterEncoder{}); });
|
||||
|
||||
ExPolygons layer_ = sla::raster_to_polygons(rst);
|
||||
// float delta = scaled(std::min(rst.pixel_dimensions().h_mm,
|
||||
@@ -454,21 +448,19 @@ static void recreate_object_from_rasters(const std::string& objname, float lh)
|
||||
|
||||
// layer_ = expolygons_simplify(layer_, delta);
|
||||
|
||||
#ifndef NDEBUG
|
||||
SVG svg(objname + std::to_string(cntr) + ".svg", rstBBox(rst));
|
||||
svg.draw(layer_);
|
||||
svg.draw(layer, "green");
|
||||
svg.Close();
|
||||
#endif
|
||||
write_debug_svg("marchingsquares/" + objname + std::to_string(cntr) + ".svg", rstBBox(rst),
|
||||
[&](SVG &svg) {
|
||||
svg.draw(layer_);
|
||||
svg.draw(layer, "green");
|
||||
});
|
||||
|
||||
double layera = 0., layera_ = 0.;
|
||||
for (auto& p : layer)
|
||||
layera += p.area();
|
||||
for (auto& p : layer_)
|
||||
layera_ += p.area();
|
||||
#ifndef NDEBUG
|
||||
std::cout << cntr++ << std::endl;
|
||||
#endif
|
||||
++cntr;
|
||||
|
||||
double diff = std::abs(layera_ - layera);
|
||||
REQUIRE((diff <= 0.1 * layera || diff < scaled<double>(1.) * scaled<double>(1.)));
|
||||
|
||||
@@ -477,7 +469,7 @@ static void recreate_object_from_rasters(const std::string& objname, float lh)
|
||||
|
||||
indexed_triangle_set out = slices_to_mesh(layers, bb.min.z(), double(lh), double(lh));
|
||||
|
||||
its_write_obj(out, "out_from_rasters.obj");
|
||||
write_debug_obj("marchingsquares/out_from_rasters.obj", out);
|
||||
}
|
||||
|
||||
TEST_CASE("Recreate object from rasters", "[SL1Import]") { recreate_object_from_rasters("frog_legs.obj", 0.05f); }
|
||||
|
||||
40
tests/libslic3r/test_model.cpp
Normal file
40
tests/libslic3r/test_model.cpp
Normal file
@@ -0,0 +1,40 @@
|
||||
#include <catch2/catch_all.hpp>
|
||||
|
||||
#include "libslic3r/Model.hpp"
|
||||
|
||||
using namespace Slic3r;
|
||||
|
||||
// convex_hull_2d does not clip geometry below the bed, so these cases avoid
|
||||
// sinking transforms.
|
||||
TEST_CASE("A part's 2D convex hull is its footprint projected onto the bed", "[Model]")
|
||||
{
|
||||
Model model;
|
||||
ModelObject* object = model.add_object();
|
||||
// Keep the cube's raw coordinates ([0,20] on every axis): the default
|
||||
// add_volume re-centers the geometry, which would move the footprint.
|
||||
object->add_volume(make_cube(20, 20, 20), ModelVolumeType::MODEL_PART, false);
|
||||
|
||||
SECTION("identity transform yields the 20 mm square") {
|
||||
const Polygon hull = object->convex_hull_2d(Geometry::Transformation{}.get_matrix());
|
||||
const BoundingBox bb = hull.bounding_box();
|
||||
CHECK(hull.size() == 4);
|
||||
CHECK(bb.min.x() == scaled(0.));
|
||||
CHECK(bb.min.y() == scaled(0.));
|
||||
CHECK(bb.max.x() == scaled(20.));
|
||||
CHECK(bb.max.y() == scaled(20.));
|
||||
}
|
||||
|
||||
SECTION("scaling and offset move and grow the footprint") {
|
||||
Geometry::Transformation t;
|
||||
t.set_scaling_factor({2, 2, 2}); // cube now spans [0,40]
|
||||
t.set_offset({10, 5, 0}); // then shift +10 in X, +5 in Y
|
||||
|
||||
const Polygon hull = object->convex_hull_2d(t.get_matrix());
|
||||
const BoundingBox bb = hull.bounding_box();
|
||||
CHECK(hull.size() == 4);
|
||||
CHECK(bb.min.x() == scaled(10.));
|
||||
CHECK(bb.min.y() == scaled(5.));
|
||||
CHECK(bb.max.x() == scaled(50.));
|
||||
CHECK(bb.max.y() == scaled(45.));
|
||||
}
|
||||
}
|
||||
57
tests/libslic3r/test_multimaterial_segmentation.cpp
Normal file
57
tests/libslic3r/test_multimaterial_segmentation.cpp
Normal file
@@ -0,0 +1,57 @@
|
||||
#include <catch2/catch_all.hpp>
|
||||
|
||||
// MultiMaterialSegmentation.hpp declares boost::polygon traits for ColoredLine, so its
|
||||
// geometry/boost dependencies must be included first.
|
||||
#include <boost/polygon/polygon.hpp>
|
||||
#include "libslic3r/Line.hpp"
|
||||
#include "libslic3r/Flow.hpp"
|
||||
#include "libslic3r/MultiMaterialSegmentation.hpp"
|
||||
#include "libslic3r/PrintConfig.hpp"
|
||||
|
||||
using namespace Slic3r;
|
||||
|
||||
TEST_CASE("Multi-material segmentation resolves the outer-wall line width", "[MultiMaterialSegmentation][Regression]")
|
||||
{
|
||||
struct Case
|
||||
{
|
||||
std::string description;
|
||||
double outer_value;
|
||||
bool outer_percent;
|
||||
double line_value;
|
||||
bool line_percent;
|
||||
std::vector<double> nozzle_diameters;
|
||||
int outer_wall_filament_id;
|
||||
double expected;
|
||||
};
|
||||
|
||||
auto c = GENERATE(values<Case>({
|
||||
{"absolute outer-wall width is used as-is", 0.6, false, 0.42, false, {0.4}, 1, 0.6},
|
||||
{"percent outer-wall width uses the nozzle", 120, true, 0.42, false, {0.5}, 1, 0.6},
|
||||
{"zero outer-wall width uses the line width", 0, false, 0.5, false, {0.4}, 1, 0.5},
|
||||
{"zero outer-wall width uses a percent line", 0, false, 100, true, {0.5}, 1, 0.5},
|
||||
{"zero width falls back to auto", 0, false, 0, false, {0.4}, 1, Flow::auto_extrusion_width(frExternalPerimeter, 0.4)},
|
||||
{"the auto fallback scales with the nozzle", 0, false, 0, false, {0.6}, 1, Flow::auto_extrusion_width(frExternalPerimeter, 0.6)},
|
||||
{"a percent width uses the outer wall's nozzle", 120, true, 0.42, false, {0.4, 0.8}, 2, 0.96},
|
||||
{"the auto width uses the outer wall's nozzle", 0, false, 0, false, {0.4, 0.8}, 2, Flow::auto_extrusion_width(frExternalPerimeter, 0.8)},
|
||||
{"an absolute width ignores the nozzle", 0.6, false, 0.42, false, {0.4, 0.8}, 2, 0.6},
|
||||
{"a zero percent width uses the line width", 0, true, 0.5, false, {0.4}, 1, 0.5},
|
||||
{"an unset filament id uses the first nozzle", 0, false, 0, false, {0.4, 0.8}, 0, Flow::auto_extrusion_width(frExternalPerimeter, 0.4)},
|
||||
{"an out-of-range filament id uses nozzle 1", 0, false, 0, false, {0.4, 0.8}, 5, Flow::auto_extrusion_width(frExternalPerimeter, 0.4)},
|
||||
}));
|
||||
|
||||
DYNAMIC_SECTION(c.description)
|
||||
{
|
||||
PrintConfig print_config;
|
||||
print_config.nozzle_diameter.values = c.nozzle_diameters;
|
||||
|
||||
PrintObjectConfig object_config;
|
||||
object_config.line_width = ConfigOptionFloatOrPercent(c.line_value, c.line_percent);
|
||||
|
||||
PrintRegionConfig region_config;
|
||||
region_config.outer_wall_line_width = ConfigOptionFloatOrPercent(c.outer_value, c.outer_percent);
|
||||
region_config.outer_wall_filament_id.value = c.outer_wall_filament_id;
|
||||
|
||||
REQUIRE_THAT(resolve_outer_wall_line_width(region_config, object_config, print_config),
|
||||
Catch::Matchers::WithinAbs(c.expected, 1e-9));
|
||||
}
|
||||
}
|
||||
297
tests/libslic3r/test_ordering_strategies.cpp
Normal file
297
tests/libslic3r/test_ordering_strategies.cpp
Normal file
@@ -0,0 +1,297 @@
|
||||
#include <catch2/catch_all.hpp>
|
||||
|
||||
#define SLIC3R_TEST_HARNESS
|
||||
|
||||
#include "libslic3r/Point.hpp"
|
||||
#include "libslic3r/GCode/OrderingStrategies.hpp"
|
||||
#include "libslic3r/Geometry.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
#include <unordered_set>
|
||||
|
||||
using namespace Slic3r;
|
||||
|
||||
// --- Helpers ---
|
||||
|
||||
static double euclidean_path_length(const std::vector<size_t>& path, const Points& centers)
|
||||
{
|
||||
return tsp_cycle_path_length(path, centers);
|
||||
}
|
||||
|
||||
static bool has_crossings(const std::vector<size_t>& path, const Points& centers)
|
||||
{
|
||||
size_t pn = path.size();
|
||||
if (pn < 4) return false;
|
||||
for (size_t i = 0; i < pn; ++i) {
|
||||
size_t i_next = (i + 1) % pn;
|
||||
for (size_t j = i + 2; j < pn; ++j) {
|
||||
if (j == i_next) continue;
|
||||
if (j == (pn - 1) && i == 0) continue;
|
||||
size_t j_next = (j + 1) % pn;
|
||||
if (Geometry::segments_intersect(
|
||||
centers[path[i]], centers[path[i_next]],
|
||||
centers[path[j]], centers[path[j_next]])) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
static bool is_permutation(const std::vector<size_t>& path, size_t n)
|
||||
{
|
||||
if (path.size() != n) return false;
|
||||
std::unordered_set<size_t> seen(path.begin(), path.end());
|
||||
for (size_t i = 0; i < n; ++i) {
|
||||
if (seen.count(i) != 1) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// --- Test fixtures ---
|
||||
|
||||
static Points make_grid_4x4()
|
||||
{
|
||||
Points pts;
|
||||
for (int row = 0; row < 4; ++row)
|
||||
for (int col = 0; col < 4; ++col)
|
||||
pts.emplace_back(100000 * col, 100000 * row);
|
||||
return pts;
|
||||
}
|
||||
|
||||
static Points make_linear_5()
|
||||
{
|
||||
Points pts;
|
||||
for (int i = 0; i < 5; ++i)
|
||||
pts.emplace_back(100000 * i, 0);
|
||||
return pts;
|
||||
}
|
||||
|
||||
static Points make_ring_8()
|
||||
{
|
||||
Points pts;
|
||||
constexpr double R = 100000.0;
|
||||
for (int i = 0; i < 8; ++i) {
|
||||
double angle = 2.0 * M_PI * i / 8.0;
|
||||
pts.emplace_back(static_cast<coord_t>(R * std::cos(angle)),
|
||||
static_cast<coord_t>(R * std::sin(angle)));
|
||||
}
|
||||
return pts;
|
||||
}
|
||||
|
||||
static Points make_random_16()
|
||||
{
|
||||
// Deterministic "random" points via simple hash.
|
||||
Points pts;
|
||||
for (int i = 0; i < 16; ++i) {
|
||||
uint32_t h = static_cast<uint32_t>(i * 2654435761u);
|
||||
coord_t x = static_cast<coord_t>((h >> 16) & 0xFFFF) * 10;
|
||||
coord_t y = static_cast<coord_t>(h & 0xFFFF) * 10;
|
||||
pts.emplace_back(x, y);
|
||||
}
|
||||
return pts;
|
||||
}
|
||||
|
||||
// --- TSP Post-Processing Tests ---
|
||||
|
||||
TEST_CASE("tsp_2opt_improve reduces path length", "[TSPPostProcessing]") {
|
||||
Points centers = make_random_16();
|
||||
std::vector<size_t> path(centers.size());
|
||||
// Reverse half the path to create a deliberately bad ordering.
|
||||
for (size_t i = 0; i < path.size(); ++i) path[i] = i;
|
||||
std::reverse(path.begin(), path.end() - path.size() / 2);
|
||||
|
||||
double before = euclidean_path_length(path, centers);
|
||||
tsp_2opt_improve(path, centers);
|
||||
double after = euclidean_path_length(path, centers);
|
||||
|
||||
REQUIRE(is_permutation(path, centers.size()));
|
||||
CHECK(after <= before);
|
||||
}
|
||||
|
||||
TEST_CASE("tsp_remove_crossings eliminates crossings", "[TSPPostProcessing]") {
|
||||
Points centers = make_random_16();
|
||||
std::vector<size_t> path(centers.size());
|
||||
for (size_t i = 0; i < path.size(); ++i) path[i] = i;
|
||||
// Create a crossing by reversing a middle segment.
|
||||
if (path.size() >= 4) {
|
||||
std::reverse(path.begin() + 1, path.end() - 1);
|
||||
}
|
||||
|
||||
tsp_remove_crossings(path, centers);
|
||||
CHECK(!has_crossings(path, centers));
|
||||
REQUIRE(is_permutation(path, centers.size()));
|
||||
}
|
||||
|
||||
|
||||
|
||||
TEST_CASE("tsp_rotate_minimize_closing shortens closing edge", "[TSPPostProcessing]") {
|
||||
Points centers = make_random_16();
|
||||
std::vector<size_t> path(centers.size());
|
||||
for (size_t i = 0; i < path.size(); ++i) path[i] = i;
|
||||
|
||||
// Compute all possible closing edge lengths.
|
||||
size_t pn = path.size();
|
||||
double min_closing2 = std::numeric_limits<double>::max();
|
||||
for (size_t start = 0; start < pn; ++start) {
|
||||
size_t last = (start + pn - 1) % pn;
|
||||
double d2 = (centers[path[start]].cast<double>() - centers[path[last]].cast<double>()).squaredNorm();
|
||||
if (d2 < min_closing2) min_closing2 = d2;
|
||||
}
|
||||
|
||||
tsp_rotate_minimize_closing(path, centers);
|
||||
|
||||
// Closing edge should be the minimum possible.
|
||||
double actual_closing2 = (centers[path.front()].cast<double>() - centers[path.back()].cast<double>()).squaredNorm();
|
||||
CHECK(actual_closing2 == min_closing2);
|
||||
REQUIRE(is_permutation(path, centers.size()));
|
||||
}
|
||||
|
||||
TEST_CASE("tsp_cycle_path_length is correct for triangle", "[TSPPostProcessing]") {
|
||||
Points pts;
|
||||
pts.emplace_back(0, 0);
|
||||
pts.emplace_back(100000, 0);
|
||||
pts.emplace_back(50000, 86602); // equilateral ~100mm sides
|
||||
|
||||
std::vector<size_t> path = {0, 1, 2};
|
||||
double len = tsp_cycle_path_length(path, pts);
|
||||
// Perimeter of equilateral triangle with side ~100000.
|
||||
REQUIRE(len > 290000);
|
||||
REQUIRE(len < 310000);
|
||||
}
|
||||
|
||||
TEST_CASE("tsp_max_edge_length finds longest edge", "[TSPPostProcessing]") {
|
||||
Points pts;
|
||||
pts.emplace_back(0, 0);
|
||||
pts.emplace_back(100000, 0);
|
||||
pts.emplace_back(50000, 0);
|
||||
|
||||
std::vector<size_t> path = {0, 1, 2};
|
||||
double mx = tsp_max_edge_length(path, pts);
|
||||
// Longest edge is 0->1 = 100000.
|
||||
CHECK(mx == Catch::Approx(100000).margin(1));
|
||||
}
|
||||
|
||||
// --- Core Strategy Tests: Empty / Small Inputs ---
|
||||
|
||||
TEST_CASE("snake_core handles empty input", "[Snake]") {
|
||||
Points centers;
|
||||
auto path = snake_core(centers);
|
||||
REQUIRE(path.empty());
|
||||
}
|
||||
|
||||
TEST_CASE("snake_core handles single point", "[Snake]") {
|
||||
Points pts{{100, 200}};
|
||||
CHECK(snake_core(pts) == std::vector<size_t>{0});
|
||||
}
|
||||
|
||||
TEST_CASE("snake_core handles two points", "[Snake]") {
|
||||
Points pts{{100, 200}, {300, 400}};
|
||||
auto p2 = snake_core(pts);
|
||||
|
||||
REQUIRE(is_permutation(p2, 2));
|
||||
}
|
||||
|
||||
// --- Core Strategy Tests: Grid Layout ---
|
||||
|
||||
TEST_CASE("snake produces good path on grid", "[Snake]") {
|
||||
Points centers = make_grid_4x4();
|
||||
auto path = snake_core(centers);
|
||||
|
||||
REQUIRE(is_permutation(path, centers.size()));
|
||||
CHECK(!has_crossings(path, centers));
|
||||
}
|
||||
|
||||
// --- Core Strategy Tests: Variable Row Spacing ---
|
||||
|
||||
TEST_CASE("snake handles variable Y spacing", "[Snake]") {
|
||||
// Rows at Y = 0, 50, 100, 1000 (large gap between last two rows).
|
||||
// The adaptive row detection should identify the tight cluster (0, 50, 100)
|
||||
// and the isolated row (1000) without splitting them incorrectly.
|
||||
Points pts;
|
||||
pts.emplace_back(0, 0); pts.emplace_back(100000, 0);
|
||||
pts.emplace_back(0, 50000); pts.emplace_back(100000, 50000);
|
||||
pts.emplace_back(0, 100000); pts.emplace_back(100000, 100000);
|
||||
pts.emplace_back(0, 1000000); pts.emplace_back(100000, 1000000);
|
||||
|
||||
auto path = snake_core(pts);
|
||||
REQUIRE(is_permutation(path, pts.size()));
|
||||
CHECK(!has_crossings(path, pts));
|
||||
}
|
||||
|
||||
// --- Core Strategy Tests: All Points Same Y ---
|
||||
|
||||
TEST_CASE("snake handles all points on same Y", "[Snake]") {
|
||||
// All points share the same Y coordinate. This exercises the
|
||||
// division-by-zero guard (ys.size() == 1).
|
||||
Points pts;
|
||||
for (int i = 0; i < 6; ++i)
|
||||
pts.emplace_back(100000 * i, 50000);
|
||||
|
||||
auto path = snake_core(pts);
|
||||
REQUIRE(is_permutation(path, pts.size()));
|
||||
}
|
||||
|
||||
// --- Core Strategy Tests: Collinear Points ---
|
||||
|
||||
TEST_CASE("snake_core handles collinear points", "[Snake]") {
|
||||
Points centers = make_linear_5();
|
||||
|
||||
auto p2 = snake_core(centers);
|
||||
|
||||
REQUIRE(is_permutation(p2, centers.size()));
|
||||
}
|
||||
|
||||
// --- Core Strategy Tests: Ring Layout ---
|
||||
|
||||
TEST_CASE("snake_core produces valid paths on ring", "[Snake]") {
|
||||
Points centers = make_ring_8();
|
||||
|
||||
auto p2 = snake_core(centers);
|
||||
|
||||
REQUIRE(is_permutation(p2, centers.size()));
|
||||
}
|
||||
|
||||
// --- Core Strategy Tests: Random Layout ---
|
||||
|
||||
TEST_CASE("snake_core produces valid paths on random input", "[Snake]") {
|
||||
Points centers = make_random_16();
|
||||
|
||||
auto p2 = snake_core(centers);
|
||||
|
||||
REQUIRE(is_permutation(p2, centers.size()));
|
||||
}
|
||||
|
||||
// --- Quality Comparison Tests ---
|
||||
|
||||
TEST_CASE("snake has no crossings on random input", "[Snake]") {
|
||||
Points centers = make_random_16();
|
||||
auto path = snake_core(centers);
|
||||
|
||||
REQUIRE(is_permutation(path, centers.size()));
|
||||
CHECK(!has_crossings(path, centers));
|
||||
}
|
||||
|
||||
// --- Edge Cases ---
|
||||
|
||||
TEST_CASE("snake_core handles duplicate points", "[Snake]") {
|
||||
Points pts;
|
||||
pts.emplace_back(100, 200);
|
||||
pts.emplace_back(100, 200); // duplicate
|
||||
pts.emplace_back(300, 400);
|
||||
|
||||
auto p2 = snake_core(pts);
|
||||
|
||||
REQUIRE(p2.size() == pts.size());
|
||||
}
|
||||
|
||||
TEST_CASE("snake_core handles three points", "[Snake]") {
|
||||
Points pts;
|
||||
pts.emplace_back(0, 0);
|
||||
pts.emplace_back(100000, 0);
|
||||
pts.emplace_back(50000, 86602);
|
||||
|
||||
auto p2 = snake_core(pts);
|
||||
|
||||
REQUIRE(is_permutation(p2, 3));
|
||||
}
|
||||
@@ -5,28 +5,14 @@
|
||||
#include "libslic3r/PresetBundle.hpp"
|
||||
#include "libslic3r/AppConfig.hpp"
|
||||
|
||||
#include "test_utils.hpp"
|
||||
|
||||
using namespace Slic3r;
|
||||
|
||||
namespace {
|
||||
|
||||
namespace fs = boost::filesystem;
|
||||
|
||||
struct TempPresetDir {
|
||||
fs::path path;
|
||||
|
||||
TempPresetDir()
|
||||
{
|
||||
path = fs::temp_directory_path() / fs::unique_path("orcaslicer-preset-%%%%-%%%%-%%%%");
|
||||
fs::create_directories(path);
|
||||
}
|
||||
|
||||
~TempPresetDir()
|
||||
{
|
||||
boost::system::error_code ec;
|
||||
fs::remove_all(path, ec);
|
||||
}
|
||||
};
|
||||
|
||||
void write_print_preset(const DynamicPrintConfig &default_config, const fs::path &file, const std::string &name, const std::string &inherits = {})
|
||||
{
|
||||
DynamicPrintConfig config(default_config);
|
||||
@@ -82,17 +68,17 @@ struct RenameTestCollection : public PresetCollection
|
||||
|
||||
TEST_CASE("Preset identity is canonicalized from load path", "[Preset][Identity]")
|
||||
{
|
||||
TempPresetDir temp_dir;
|
||||
ScopedTemporaryDir temp_dir;
|
||||
PresetBundle bundle;
|
||||
PresetsConfigSubstitutions substitutions;
|
||||
|
||||
write_print_preset(bundle.prints.default_preset().config, temp_dir.path / PRESET_PRINT_NAME / "User.json", "User");
|
||||
write_print_preset(bundle.prints.default_preset().config, temp_dir.path / PRESET_LOCAL_DIR / "bundle-1" / PRESET_PRINT_NAME / "LocalBundle.json", "LocalBundle");
|
||||
write_print_preset(bundle.prints.default_preset().config, temp_dir.path / PRESET_SUBSCRIBED_DIR / "remote-1" / PRESET_PRINT_NAME / "Subscribed.json", "Subscribed");
|
||||
write_print_preset(bundle.prints.default_preset().config, temp_dir.path() / PRESET_PRINT_NAME / "User.json", "User");
|
||||
write_print_preset(bundle.prints.default_preset().config, temp_dir.path() / PRESET_LOCAL_DIR / "bundle-1" / PRESET_PRINT_NAME / "LocalBundle.json", "LocalBundle");
|
||||
write_print_preset(bundle.prints.default_preset().config, temp_dir.path() / PRESET_SUBSCRIBED_DIR / "remote-1" / PRESET_PRINT_NAME / "Subscribed.json", "Subscribed");
|
||||
|
||||
bundle.prints.load_presets(temp_dir.path.string(), PRESET_PRINT_NAME, substitutions, ForwardCompatibilitySubstitutionRule::Disable);
|
||||
bundle.prints.load_presets((temp_dir.path / PRESET_LOCAL_DIR / "bundle-1").string(), PRESET_PRINT_NAME, substitutions, ForwardCompatibilitySubstitutionRule::Disable);
|
||||
bundle.prints.load_presets((temp_dir.path / PRESET_SUBSCRIBED_DIR / "remote-1").string(), PRESET_PRINT_NAME, substitutions, ForwardCompatibilitySubstitutionRule::Disable);
|
||||
bundle.prints.load_presets(temp_dir.path().string(), PRESET_PRINT_NAME, substitutions, ForwardCompatibilitySubstitutionRule::Disable);
|
||||
bundle.prints.load_presets((temp_dir.path() / PRESET_LOCAL_DIR / "bundle-1").string(), PRESET_PRINT_NAME, substitutions, ForwardCompatibilitySubstitutionRule::Disable);
|
||||
bundle.prints.load_presets((temp_dir.path() / PRESET_SUBSCRIBED_DIR / "remote-1").string(), PRESET_PRINT_NAME, substitutions, ForwardCompatibilitySubstitutionRule::Disable);
|
||||
|
||||
const Preset *root_user = bundle.prints.find_preset("User");
|
||||
REQUIRE(root_user != nullptr);
|
||||
@@ -112,14 +98,14 @@ TEST_CASE("Preset identity is canonicalized from load path", "[Preset][Identity]
|
||||
|
||||
TEST_CASE("Legacy bundle import without bundle metadata stays in the user preset directory", "[Preset][Identity]")
|
||||
{
|
||||
TempPresetDir temp_dir;
|
||||
ScopedTemporaryDir temp_dir;
|
||||
PresetBundle bundle;
|
||||
|
||||
PresetsConfigSubstitutions substitutions;
|
||||
std::vector<std::string> result;
|
||||
int overwrite = 0;
|
||||
std::string file = (temp_dir.path / "legacy-bundle" / "Imported.json").string();
|
||||
const fs::path user_root = temp_dir.path / "user";
|
||||
std::string file = (temp_dir.path() / "legacy-bundle" / "Imported.json").string();
|
||||
const fs::path user_root = temp_dir.path() / "user";
|
||||
|
||||
write_print_preset(bundle.prints.default_preset().config, file, "Imported");
|
||||
fs::create_directories(user_root);
|
||||
@@ -252,7 +238,7 @@ TEST_CASE("find_preset2 auto-matches removed Generic vendor profiles to the libr
|
||||
|
||||
TEST_CASE("Renamed parent is normalized into a loaded preset's inherits", "[Preset][Rename]")
|
||||
{
|
||||
TempPresetDir temp_dir;
|
||||
ScopedTemporaryDir temp_dir;
|
||||
RenameTestCollection coll;
|
||||
|
||||
// Current parent, renamed from "Old Process".
|
||||
@@ -262,10 +248,10 @@ TEST_CASE("Renamed parent is normalized into a loaded preset's inherits", "[Pres
|
||||
|
||||
// A user preset on disk that still inherits the OLD name.
|
||||
write_preset_with_inherits(coll.default_preset().config,
|
||||
temp_dir.path / PRESET_PRINT_NAME / "Child.json", "Child", "Old Process");
|
||||
temp_dir.path() / PRESET_PRINT_NAME / "Child.json", "Child", "Old Process");
|
||||
|
||||
PresetsConfigSubstitutions substitutions;
|
||||
coll.load_presets(temp_dir.path.string(), PRESET_PRINT_NAME, substitutions,
|
||||
coll.load_presets(temp_dir.path().string(), PRESET_PRINT_NAME, substitutions,
|
||||
ForwardCompatibilitySubstitutionRule::Disable);
|
||||
|
||||
const Preset *child = coll.find_preset("Child");
|
||||
@@ -279,17 +265,17 @@ TEST_CASE("Renamed parent is normalized into a loaded preset's inherits", "[Pres
|
||||
|
||||
TEST_CASE("Removed Generic parent is normalized into a loaded filament's inherits", "[Preset][Rename]")
|
||||
{
|
||||
TempPresetDir temp_dir;
|
||||
ScopedTemporaryDir temp_dir;
|
||||
PresetBundle bundle;
|
||||
|
||||
add_inmemory_preset(bundle.filaments, "Generic PLA @System");
|
||||
|
||||
// A user filament that still inherits a removed "<vendor> Generic PLA" profile.
|
||||
write_preset_with_inherits(bundle.filaments.default_preset().config,
|
||||
temp_dir.path / PRESET_FILAMENT_NAME / "MyPLA.json", "MyPLA", "Voron Generic PLA");
|
||||
temp_dir.path() / PRESET_FILAMENT_NAME / "MyPLA.json", "MyPLA", "Voron Generic PLA");
|
||||
|
||||
PresetsConfigSubstitutions substitutions;
|
||||
bundle.filaments.load_presets(temp_dir.path.string(), PRESET_FILAMENT_NAME, substitutions,
|
||||
bundle.filaments.load_presets(temp_dir.path().string(), PRESET_FILAMENT_NAME, substitutions,
|
||||
ForwardCompatibilitySubstitutionRule::Disable);
|
||||
|
||||
const Preset *child = bundle.filaments.find_preset("MyPLA");
|
||||
@@ -464,3 +450,93 @@ TEST_CASE("Profile validator flags dangling and renamed preset references", "[Pr
|
||||
}
|
||||
}
|
||||
|
||||
// Under a shared override key, the last preset merged into the full config overwrote the others', so an
|
||||
// edited slicing-pipeline override never reached Print::apply's diff and re-configuring a plugin never
|
||||
// re-sliced. Per-type keys make that collision impossible; guard the scoping here.
|
||||
TEST_CASE("Plugin capability override keys are scoped per preset type", "[Preset][Plugin]")
|
||||
{
|
||||
// Pin the key names: presets and 3mf files store them verbatim, so a rename is a format change.
|
||||
CHECK(Preset::plugin_overrides_key(Preset::TYPE_PRINT) == std::string("print_plugin_config_overrides"));
|
||||
CHECK(Preset::plugin_overrides_key(Preset::TYPE_PRINTER) == std::string("printer_plugin_config_overrides"));
|
||||
CHECK(Preset::plugin_overrides_key(Preset::TYPE_FILAMENT) == std::string("filament_plugin_config_overrides"));
|
||||
|
||||
// ...and each key lives on exactly its own preset type's option list, so no two ever share a slot.
|
||||
const std::pair<Preset::Type, const std::vector<std::string>*> scopes[] = {
|
||||
{Preset::TYPE_PRINT, &Preset::print_options()},
|
||||
{Preset::TYPE_PRINTER, &Preset::printer_options()},
|
||||
{Preset::TYPE_FILAMENT, &Preset::filament_options()},
|
||||
};
|
||||
for (const auto &owner : scopes)
|
||||
for (const auto &scoped : scopes) {
|
||||
const std::string key = Preset::plugin_overrides_key(scoped.first);
|
||||
CAPTURE(owner.first, key);
|
||||
CHECK(contains(*owner.second, key) == (owner.first == scoped.first));
|
||||
}
|
||||
}
|
||||
|
||||
namespace {
|
||||
|
||||
// A standalone filament collection that exposes the protected library masking builder, so the Orca
|
||||
// Filament Library scenario can be set up without the full system-profile load pipeline.
|
||||
struct LibraryFilamentTestCollection : public PresetCollection
|
||||
{
|
||||
LibraryFilamentTestCollection()
|
||||
: PresetCollection(Preset::TYPE_FILAMENT, Preset::filament_options(),
|
||||
static_cast<const PrintRegionConfig &>(FullPrintConfig::defaults()))
|
||||
{}
|
||||
using PresetCollection::update_library_profile_excluded_from;
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
// Orca: a filament in the Orca Filament Library that names its compatible printers has to hide the generic
|
||||
// library filament sharing its alias, the same way a vendor owned filament does. Otherwise both are compatible
|
||||
// with that printer and the plater combo box lists the shared alias twice.
|
||||
TEST_CASE("A printer specific filament supersedes the generic library filament with the same alias", "[Preset][Bundle]")
|
||||
{
|
||||
LibraryFilamentTestCollection filaments;
|
||||
PresetCollection printers(Preset::TYPE_PRINTER, Preset::printer_options(),
|
||||
static_cast<const PrintRegionConfig &>(FullPrintConfig::defaults()));
|
||||
// The masking keys off the vendor name, which VendorProfile's constructor does not derive from the id.
|
||||
VendorProfile library(PresetBundle::ORCA_FILAMENT_LIBRARY);
|
||||
VendorProfile vendor("Vendor");
|
||||
library.name = PresetBundle::ORCA_FILAMENT_LIBRARY;
|
||||
vendor.name = "Vendor";
|
||||
|
||||
auto add_filament = [&filaments](const VendorProfile &owner, const std::string &name, std::vector<std::string> compatible_printers) {
|
||||
Preset &preset = add_inmemory_preset(filaments, name);
|
||||
preset.alias = "Generic ABS";
|
||||
preset.vendor = &owner;
|
||||
preset.config.option<ConfigOptionStrings>("compatible_printers", true)->values = std::move(compatible_printers);
|
||||
};
|
||||
|
||||
add_filament(library, "Generic ABS @System", {});
|
||||
add_filament(library, "Generic ABS @Printer A", { "Printer A" });
|
||||
add_filament(vendor, "Generic ABS @Printer B", { "Printer B" });
|
||||
|
||||
filaments.update_library_profile_excluded_from();
|
||||
|
||||
const Preset *generic = filaments.find_preset("Generic ABS @System");
|
||||
REQUIRE(generic != nullptr);
|
||||
CHECK(generic->m_excluded_from.count("Printer A") == 1);
|
||||
CHECK(generic->m_excluded_from.count("Printer B") == 1);
|
||||
CHECK(generic->m_excluded_from.size() == 2);
|
||||
|
||||
// A printer specific profile names printers, so it is never the one being hidden - not even by itself.
|
||||
const Preset *specific = filaments.find_preset("Generic ABS @Printer A");
|
||||
REQUIRE(specific != nullptr);
|
||||
CHECK(specific->m_excluded_from.empty());
|
||||
|
||||
// ...and the generic profile really drops out of the compatible set on the printer it is hidden from.
|
||||
add_inmemory_preset(printers, "Printer A");
|
||||
add_inmemory_preset(printers, "Printer C");
|
||||
const Preset *printer_a = printers.find_preset("Printer A");
|
||||
const Preset *printer_c = printers.find_preset("Printer C");
|
||||
REQUIRE(printer_a != nullptr);
|
||||
REQUIRE(printer_c != nullptr);
|
||||
|
||||
const PresetWithVendorProfile generic_lib(*generic, &library);
|
||||
CHECK_FALSE(is_compatible_with_printer(generic_lib, PresetWithVendorProfile(*printer_a, nullptr)));
|
||||
CHECK(is_compatible_with_printer(generic_lib, PresetWithVendorProfile(*printer_c, nullptr)));
|
||||
}
|
||||
|
||||
|
||||
@@ -8,6 +8,8 @@
|
||||
#include "libslic3r/Print.hpp"
|
||||
#include "libslic3r/TriangleMesh.hpp"
|
||||
|
||||
#include "test_utils.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
#include <map>
|
||||
#include <set>
|
||||
@@ -500,6 +502,52 @@ TEST_CASE("Re-applying an unchanged config after slicing keeps the result valid"
|
||||
REQUIRE(print.is_step_done(psSlicingFinished));
|
||||
}
|
||||
|
||||
TEST_CASE("A degenerate process variant map on a custom multi-extruder printer slices to a stable result", "[Print][Regression]")
|
||||
{
|
||||
// Non-BBL multi-extruder printers get machine-scope variant columns synthesized on preset
|
||||
// load (extend_extruder_variant), but nothing ships process-scope print_extruder_id /
|
||||
// print_extruder_variant: presets and 3mf project configs carry the length-1 defaults. The
|
||||
// apply-time expansion must synthesize the process columns from extruder_variant_list;
|
||||
// otherwise the failed per-extruder lookups collapse the per-extruder retract overrides
|
||||
// during slicing and the post-slice re-apply invalidates every fresh result, forever.
|
||||
DynamicPrintConfig config = DynamicPrintConfig::full_print_config();
|
||||
config.set_num_extruders(5);
|
||||
config.option<ConfigOptionFloats>("nozzle_diameter", true)->values = {0.4, 0.4, 0.4, 0.4, 0.4};
|
||||
// per-extruder machine values that a first-slot collapse would destroy
|
||||
config.option<ConfigOptionPercents>("retract_before_wipe", true)->values = {100., 70., 70., 70., 100.};
|
||||
config.option<ConfigOptionEnumsGeneric>("z_hop_types", true)->values = {zhtSlope, zhtNormal, zhtNormal, zhtNormal, zhtSlope};
|
||||
// filament presets carry the nullable override twins (all-nil = "no override"); they are what
|
||||
// routes the machine values through apply_override in the in-slice override recompute
|
||||
config.option<ConfigOptionPercentsNullable>("filament_retract_before_wipe", true)->values =
|
||||
std::vector<double>(5, ConfigOptionPercentsNullable::nil_value());
|
||||
config.option<ConfigOptionEnumsGenericNullable>("filament_z_hop_types", true)->values =
|
||||
std::vector<int>(5, ConfigOptionEnumsGenericNullable::nil_value());
|
||||
config.option<ConfigOptionFloats>("filament_diameter", true)->values = std::vector<double>(5, 1.75);
|
||||
config.option<ConfigOptionStrings>("filament_colour", true)->values = {"#FF0000", "#00FF00", "#0000FF", "#FFFF00", "#00FFFF"};
|
||||
config.option<ConfigOptionInts>("filament_map", true)->values = {1, 2, 3, 4, 1};
|
||||
|
||||
Model model;
|
||||
model.add_object("cube", "", make_cube(20, 20, 20))->add_instance()->set_offset(Vec3d(100., 100., 0.));
|
||||
|
||||
Print print;
|
||||
print.apply(model, config);
|
||||
print.process();
|
||||
REQUIRE(print.is_step_done(psSlicingFinished));
|
||||
|
||||
// BackgroundSlicingProcess reads the engine-computed maps back into the plate config after
|
||||
// slicing; the next apply overlays that written-back state.
|
||||
config.option<ConfigOptionInts>("filament_map", true)->values = print.get_filament_maps();
|
||||
config.option<ConfigOptionInts>("filament_volume_map", true)->values = print.get_filament_volume_maps();
|
||||
config.option<ConfigOptionInts>("filament_nozzle_map", true)->values = print.get_filament_nozzle_maps();
|
||||
|
||||
auto status = print.apply(model, config);
|
||||
REQUIRE(status == PrintBase::APPLY_STATUS_UNCHANGED);
|
||||
REQUIRE(print.is_step_done(psSlicingFinished));
|
||||
// the per-extruder machine values must survive the in-slice override recompute
|
||||
REQUIRE(print.config().retract_before_wipe.values == std::vector<double>({100., 70., 70., 70., 100.}));
|
||||
REQUIRE(print.config().z_hop_types.values == std::vector<int>({zhtSlope, zhtNormal, zhtNormal, zhtNormal, zhtSlope}));
|
||||
}
|
||||
|
||||
TEST_CASE("normalize_nozzle_map_per_layer makes per-filament assignments gap-free", "[MultiNozzle][H2C][Dynamic]")
|
||||
{
|
||||
SECTION("gaps inherit the last used nozzle, entries on used layers stay untouched") {
|
||||
@@ -662,10 +710,9 @@ TEST_CASE("Sequential selector prints publish a stitched result and cache the pl
|
||||
REQUIRE(print.config().filament_self_index.values.size() >= print.config().filament_map.values.size());
|
||||
|
||||
// Export must consume the cached plans and produce g-code without throwing.
|
||||
boost::filesystem::path gcode_path = boost::filesystem::temp_directory_path() / "orca_seq_dynamic_publish_test.gcode";
|
||||
REQUIRE_NOTHROW(print.export_gcode(gcode_path.string(), nullptr, nullptr));
|
||||
REQUIRE(boost::filesystem::exists(gcode_path));
|
||||
boost::filesystem::remove(gcode_path);
|
||||
ScopedTemporaryFile gcode(".gcode");
|
||||
REQUIRE_NOTHROW(print.export_gcode(gcode.string(), nullptr, nullptr));
|
||||
REQUIRE(boost::filesystem::exists(gcode.path()));
|
||||
}
|
||||
|
||||
TEST_CASE("Per-variant expansion gives migrating filaments one slot per variant", "[PrintConfig][H2C][Dynamic]")
|
||||
|
||||
Reference in New Issue
Block a user