mirror of
https://github.com/OrcaSlicer/OrcaSlicer.git
synced 2026-09-27 19:01:02 +00:00
Merge branch 'main' into weilun/speed_dial
# Conflicts: # src/slic3r/GUI/Tab.cpp
This commit is contained in:
@@ -19,6 +19,7 @@ add_executable(${_TEST_NAME}_tests
|
||||
test_preset_setting_id.cpp
|
||||
test_preset_diff.cpp
|
||||
test_vendor_cache.cpp
|
||||
test_preset_options.cpp
|
||||
test_elephant_foot_compensation.cpp
|
||||
test_fill_corner_smoothing.cpp
|
||||
test_filament_mixer.cpp
|
||||
|
||||
@@ -4,6 +4,8 @@
|
||||
#include "libslic3r/BoundingBox.hpp"
|
||||
#include "libslic3r/ClipperUtils.hpp"
|
||||
#include "libslic3r/ExPolygon.hpp"
|
||||
#include "libslic3r/Print.hpp"
|
||||
#include "libslic3r/PrintConfig.hpp"
|
||||
|
||||
using namespace Slic3r;
|
||||
using namespace Slic3r::arrangement;
|
||||
@@ -24,11 +26,13 @@ ArrangePolygon make_square(coord_t side)
|
||||
return ap;
|
||||
}
|
||||
|
||||
ArrangePolygons squares(int n, double side_mm)
|
||||
ArrangePolygons squares(int n, double side_mm, double height_mm = 0.)
|
||||
{
|
||||
ArrangePolygons items;
|
||||
for (int i = 0; i < n; ++i)
|
||||
for (int i = 0; i < n; ++i) {
|
||||
items.emplace_back(make_square(scaled(side_mm)));
|
||||
items.back().height = height_mm;
|
||||
}
|
||||
return items;
|
||||
}
|
||||
|
||||
@@ -82,6 +86,38 @@ void require_no_overlap(const ArrangePolygons &items)
|
||||
REQUIRE(disjoint(placed_shapes(items)));
|
||||
}
|
||||
|
||||
// The sequential-print floor is chosen by comparing object height against the nozzle,
|
||||
// so the two are defined together and every expectation is derived from them.
|
||||
constexpr double NOZZLE_HEIGHT_MM = 2.5;
|
||||
constexpr double CLEARANCE_MM = 30.;
|
||||
constexpr double NOZZLE_FLOOR_MM = MAX_OUTER_NOZZLE_DIAMETER / 2.;
|
||||
|
||||
ArrangeParams seq_print_params(coord_t min_dist)
|
||||
{
|
||||
ArrangeParams p = quiet_params(min_dist);
|
||||
p.is_seq_print = true;
|
||||
p.clearance_radius = float(CLEARANCE_MM);
|
||||
p.nozzle_height = float(NOZZLE_HEIGHT_MM);
|
||||
p.object_skirt_offset = 0.f;
|
||||
return p;
|
||||
}
|
||||
|
||||
// update_selected_items_inflation reads the bed out of the config to cap inflation.
|
||||
DynamicPrintConfig bed_config()
|
||||
{
|
||||
DynamicPrintConfig c;
|
||||
c.set_key_value("printable_area", new ConfigOptionPoints{{0, 0}, {200, 0}, {200, 200}, {0, 200}});
|
||||
return c;
|
||||
}
|
||||
|
||||
ArrangePolygons squares_of_heights(const std::vector<double> &heights_mm)
|
||||
{
|
||||
ArrangePolygons items;
|
||||
for (double height_mm : heights_mm)
|
||||
items.push_back(squares(1, 20., height_mm).front());
|
||||
return items;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
// Prove the overlap check the other tests rely on actually detects overlap.
|
||||
@@ -222,3 +258,61 @@ TEST_CASE("Arrange aligns the pile to a custom center", "[Arrange]")
|
||||
REQUIRE(ap.bed_idx == 0);
|
||||
require_no_overlap(items);
|
||||
}
|
||||
|
||||
TEST_CASE("Sequential print floors the object distance by object height", "[Arrange]")
|
||||
{
|
||||
// The only place sequential-print clearance is enforced. The arrange menu offers
|
||||
// no floor of its own, so a stored 0 has to be raised here or not at all.
|
||||
struct Case
|
||||
{
|
||||
std::string description;
|
||||
std::vector<double> heights;
|
||||
double skirt_offset_mm;
|
||||
double expected_floor_mm;
|
||||
};
|
||||
|
||||
auto c = GENERATE(values<Case>({
|
||||
{"objects taller than the nozzle need the full clearance", {NOZZLE_HEIGHT_MM * 2, NOZZLE_HEIGHT_MM * 2}, 0., CLEARANCE_MM},
|
||||
{"an object exactly at the nozzle height counts as tall", {NOZZLE_HEIGHT_MM, NOZZLE_HEIGHT_MM}, 0., CLEARANCE_MM},
|
||||
{"one tall object among short ones is enough", {NOZZLE_HEIGHT_MM / 2, NOZZLE_HEIGHT_MM * 2}, 0., CLEARANCE_MM},
|
||||
{"objects the nozzle clears keep only the nozzle-width floor", {NOZZLE_HEIGHT_MM / 2, NOZZLE_HEIGHT_MM / 2}, 0., NOZZLE_FLOOR_MM},
|
||||
{"a wide skirt raises the floor for short objects", {NOZZLE_HEIGHT_MM / 2, NOZZLE_HEIGHT_MM / 2}, 3., 6.},
|
||||
}));
|
||||
|
||||
DYNAMIC_SECTION(c.description)
|
||||
{
|
||||
ArrangePolygons items = squares_of_heights(c.heights);
|
||||
DynamicPrintConfig cfg = bed_config();
|
||||
ArrangeParams p = seq_print_params(0);
|
||||
p.object_skirt_offset = float(c.skirt_offset_mm);
|
||||
|
||||
update_selected_items_inflation(items, &cfg, p);
|
||||
|
||||
CHECK(p.min_obj_distance >= scaled(c.expected_floor_mm));
|
||||
CHECK(p.min_obj_distance <= scaled(c.expected_floor_mm + 0.01));
|
||||
// Half each, so a pair ends up a full min_obj_distance apart.
|
||||
CHECK(items.front().inflation == p.min_obj_distance / 2);
|
||||
}
|
||||
}
|
||||
|
||||
TEST_CASE("Sequential print keeps an object distance already above the floor", "[Arrange]")
|
||||
{
|
||||
const coord_t stored = scaled(CLEARANCE_MM * 2);
|
||||
ArrangePolygons items = squares_of_heights({NOZZLE_HEIGHT_MM * 2, NOZZLE_HEIGHT_MM * 2});
|
||||
DynamicPrintConfig cfg = bed_config();
|
||||
ArrangeParams p = seq_print_params(stored);
|
||||
|
||||
update_selected_items_inflation(items, &cfg, p);
|
||||
CHECK(p.min_obj_distance == stored);
|
||||
}
|
||||
|
||||
TEST_CASE("Layered printing does not floor the object distance", "[Arrange]")
|
||||
{
|
||||
ArrangePolygons items = squares_of_heights({NOZZLE_HEIGHT_MM * 2, NOZZLE_HEIGHT_MM * 2});
|
||||
DynamicPrintConfig cfg = bed_config();
|
||||
ArrangeParams p = seq_print_params(0);
|
||||
p.is_seq_print = false;
|
||||
|
||||
update_selected_items_inflation(items, &cfg, p);
|
||||
CHECK(p.min_obj_distance == 0);
|
||||
}
|
||||
|
||||
@@ -1091,3 +1091,128 @@ TEST_CASE("get_filament_type treats empty vector options as absent", "[Config][F
|
||||
REQUIRE(displayed == "Sup.PLA");
|
||||
}
|
||||
}
|
||||
|
||||
namespace {
|
||||
|
||||
// min_object_distance reads exactly these three options.
|
||||
DynamicPrintConfig spacing_config(PrinterTechnology tech, PrintSequence seq, double clearance_radius)
|
||||
{
|
||||
DynamicPrintConfig c;
|
||||
c.set_key_value("printer_technology", new ConfigOptionEnum<PrinterTechnology>(tech));
|
||||
c.set_key_value("print_sequence", new ConfigOptionEnum<PrintSequence>(seq));
|
||||
c.set_key_value("extruder_clearance_radius", new ConfigOptionFloat(clearance_radius));
|
||||
return c;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
TEST_CASE("min_object_distance floors object spacing per print sequence", "[Config]")
|
||||
{
|
||||
struct Case
|
||||
{
|
||||
std::string description;
|
||||
PrinterTechnology tech;
|
||||
PrintSequence sequence;
|
||||
double clearance_radius;
|
||||
double expected;
|
||||
};
|
||||
|
||||
auto c = GENERATE(values<Case>({
|
||||
{"sequential FFF takes a clearance radius above the floor", ptFFF, PrintSequence::ByObject, 12., 12.},
|
||||
{"sequential FFF holds the floor at the radius", ptFFF, PrintSequence::ByObject, 6., 6.},
|
||||
{"sequential FFF holds the floor below the radius", ptFFF, PrintSequence::ByObject, 4., 6.},
|
||||
{"layered FFF ignores the clearance radius", ptFFF, PrintSequence::ByLayer, 12., 6.},
|
||||
{"SLA is a flat 6mm", ptSLA, PrintSequence::ByObject, 12., 6.},
|
||||
{"SLA ignores the print sequence too", ptSLA, PrintSequence::ByLayer, 12., 6.},
|
||||
}));
|
||||
|
||||
DYNAMIC_SECTION(c.description)
|
||||
{
|
||||
CHECK_THAT(min_object_distance(spacing_config(c.tech, c.sequence, c.clearance_radius)),
|
||||
Catch::Matchers::WithinAbs(c.expected, 1e-9));
|
||||
}
|
||||
}
|
||||
|
||||
TEST_CASE("min_object_distance yields no floor when an FFF config lacks the options", "[Config]")
|
||||
{
|
||||
// Missing options yield 0 rather than an error, so a caller gets no floor at all.
|
||||
SECTION("no clearance radius") {
|
||||
DynamicPrintConfig c;
|
||||
c.set_key_value("printer_technology", new ConfigOptionEnum<PrinterTechnology>(ptFFF));
|
||||
c.set_key_value("print_sequence", new ConfigOptionEnum<PrintSequence>(PrintSequence::ByObject));
|
||||
CHECK_THAT(min_object_distance(c), Catch::Matchers::WithinAbs(0., 1e-9));
|
||||
}
|
||||
|
||||
SECTION("no print sequence") {
|
||||
DynamicPrintConfig c;
|
||||
c.set_key_value("printer_technology", new ConfigOptionEnum<PrinterTechnology>(ptFFF));
|
||||
c.set_key_value("extruder_clearance_radius", new ConfigOptionFloat(12.));
|
||||
CHECK_THAT(min_object_distance(c), Catch::Matchers::WithinAbs(0., 1e-9));
|
||||
}
|
||||
|
||||
SECTION("nothing at all") {
|
||||
CHECK_THAT(min_object_distance(DynamicPrintConfig{}), Catch::Matchers::WithinAbs(0., 1e-9));
|
||||
}
|
||||
|
||||
SECTION("an unset printer technology is treated as FFF") {
|
||||
DynamicPrintConfig c;
|
||||
c.set_key_value("print_sequence", new ConfigOptionEnum<PrintSequence>(PrintSequence::ByObject));
|
||||
c.set_key_value("extruder_clearance_radius", new ConfigOptionFloat(12.));
|
||||
CHECK_THAT(min_object_distance(c), Catch::Matchers::WithinAbs(12., 1e-9));
|
||||
}
|
||||
}
|
||||
|
||||
TEST_CASE("Static print configs compare, order and hash by their option values", "[Config]")
|
||||
{
|
||||
// PrintObjectConfig comes from PRINT_CONFIG_CLASS_DEFINE; PrintConfig combines MachineEnvelopeConfig
|
||||
// and GCodeConfig through PRINT_CONFIG_CLASS_DERIVED_DEFINE. Both generate hash(), operator==,
|
||||
// operator< and the option registration from the same option list. The hash inequalities use fixed
|
||||
// inputs, so they are deterministic; they check that hash() covers the changed option.
|
||||
SECTION("default-constructed configs are equal and find their options by key")
|
||||
{
|
||||
PrintObjectConfig a, b;
|
||||
REQUIRE(a == b);
|
||||
REQUIRE(a.hash() == b.hash());
|
||||
REQUIRE_FALSE(a < b);
|
||||
REQUIRE_FALSE(b < a);
|
||||
REQUIRE(a.optptr("layer_height") == &a.layer_height);
|
||||
REQUIRE(a.optptr("brim_object_gap") == &a.brim_object_gap);
|
||||
}
|
||||
|
||||
SECTION("one differing option makes the configs unequal and orders them")
|
||||
{
|
||||
PrintObjectConfig a, b;
|
||||
b.layer_height.value = a.layer_height.value + 0.05;
|
||||
REQUIRE(a != b);
|
||||
REQUIRE(a.hash() != b.hash());
|
||||
REQUIRE(a < b);
|
||||
REQUIRE_FALSE(b < a);
|
||||
}
|
||||
|
||||
SECTION("ordering is decided by the first option in declaration order that differs")
|
||||
{
|
||||
PrintObjectConfig a, b;
|
||||
a.brim_object_gap.value = b.brim_object_gap.value + 1.0; // declared first
|
||||
a.layer_height.value = b.layer_height.value - 0.05; // declared later, points the other way
|
||||
REQUIRE(b < a);
|
||||
REQUIRE_FALSE(a < b);
|
||||
}
|
||||
|
||||
SECTION("a derived config sees differences in its parents and in its own options")
|
||||
{
|
||||
PrintConfig a, b;
|
||||
REQUIRE(a == b);
|
||||
REQUIRE(a.hash() == b.hash());
|
||||
|
||||
b.gcode_flavor.value = b.gcode_flavor.value == gcfMarlinLegacy ? gcfKlipper : gcfMarlinLegacy; // GCodeConfig parent
|
||||
REQUIRE(a != b);
|
||||
REQUIRE(a.hash() != b.hash());
|
||||
|
||||
PrintConfig c, d;
|
||||
d.skirt_distance.value = c.skirt_distance.value + 1.0; // PrintConfig's own list
|
||||
REQUIRE(c != d);
|
||||
REQUIRE(c.hash() != d.hash());
|
||||
REQUIRE(c.optptr("skirt_distance") == &c.skirt_distance);
|
||||
REQUIRE(c.optptr("gcode_flavor") == &c.gcode_flavor);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
// Regression test for the "option in def + UI but missing from preset key list"
|
||||
// crash class.
|
||||
//
|
||||
// The print preset's DynamicPrintConfig is seeded with only the keys returned by
|
||||
// Preset::print_options() (PresetBundle.cpp). A field added to PrintRegionConfig
|
||||
// or PrintObjectConfig and registered via print_config_def plus a TabPrint
|
||||
// optgroup, but left out of print_options(), still gets its control built; on tab
|
||||
// activation reload_config -> get_config_value dispatches to opt_bool/opt_int on a
|
||||
// DynamicPrintConfig with no entry for the key, and the accessor null-derefs the
|
||||
// result of option<T>(key).
|
||||
//
|
||||
// The invariant asserted here is the inverse: every key declared on
|
||||
// PrintRegionConfig and PrintObjectConfig appears in Preset::print_options() or
|
||||
// Preset::filament_options(), the two preset key lists that seed a print preset's
|
||||
// DynamicConfig.
|
||||
|
||||
#include <catch2/catch_all.hpp>
|
||||
|
||||
#include "libslic3r/Preset.hpp"
|
||||
#include "libslic3r/PrintConfig.hpp"
|
||||
|
||||
#include <set>
|
||||
|
||||
using namespace Slic3r;
|
||||
|
||||
namespace {
|
||||
|
||||
// Deprecated keys renamed in handle_legacy() (ironing_direction ->
|
||||
// ironing_angle, wall_infill_order -> wall_sequence); neither is in a
|
||||
// preset list. Register new options in a preset list, not here.
|
||||
const std::set<std::string> kDeprecatedRegionFields = {
|
||||
"ironing_direction",
|
||||
"wall_infill_order",
|
||||
};
|
||||
|
||||
void check_keys_are_in_a_preset(const t_config_option_keys& keys, const std::string& class_name)
|
||||
{
|
||||
REQUIRE_FALSE(keys.empty());
|
||||
const auto& print_options = Preset::print_options();
|
||||
const auto& filament_options = Preset::filament_options();
|
||||
const std::set<std::string> in_print(print_options.begin(), print_options.end());
|
||||
const std::set<std::string> in_filament(filament_options.begin(), filament_options.end());
|
||||
for (const std::string& key : keys) {
|
||||
DYNAMIC_SECTION(class_name << "::" << key)
|
||||
{
|
||||
INFO("'" << key << "' on " << class_name
|
||||
<< " is missing from "
|
||||
"Preset::print_options()/filament_options(); add it to "
|
||||
"s_Preset_print_options (or s_Preset_filament_options) in Preset.cpp.");
|
||||
const bool registered = in_print.count(key) || in_filament.count(key) || kDeprecatedRegionFields.count(key);
|
||||
REQUIRE(registered);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
// Bodies are laid out like the rest of the test suite rather than collapsed
|
||||
// onto the brace line.
|
||||
// clang-format off
|
||||
TEST_CASE("Every PrintRegionConfig field is registered in a preset key list", "[Preset][Config]")
|
||||
{
|
||||
check_keys_are_in_a_preset(PrintRegionConfig::defaults().keys(), "PrintRegionConfig");
|
||||
}
|
||||
|
||||
TEST_CASE("Every PrintObjectConfig field is registered in a preset key list", "[Preset][Config]")
|
||||
{
|
||||
check_keys_are_in_a_preset(PrintObjectConfig::defaults().keys(), "PrintObjectConfig");
|
||||
}
|
||||
// clang-format on
|
||||
Reference in New Issue
Block a user