Merge branch 'main' into feature/texture_displacement

This commit is contained in:
Ian Bassi
2026-07-17 09:07:31 -03:00
committed by GitHub
874 changed files with 110241 additions and 7192 deletions

View File

@@ -13,13 +13,17 @@ endif()
set(TEST_DATA_DIR ${CMAKE_CURRENT_SOURCE_DIR}/data)
file(TO_NATIVE_PATH "${TEST_DATA_DIR}" TEST_DATA_DIR)
# Shipped vendor profiles, so tests can exercise the real machine/filament gcode.
set(PROFILES_DIR ${CMAKE_SOURCE_DIR}/resources/profiles)
file(TO_NATIVE_PATH "${PROFILES_DIR}" PROFILES_DIR)
include(CTest)
include(Catch)
set(CATCH_EXTRA_ARGS "" CACHE STRING "Extra arguments for catch2 test suites.") # Unknown if this still works and/or should be replaced with something else.
add_library(test_common INTERFACE)
target_compile_definitions(test_common INTERFACE TEST_DATA_DIR=R"\(${TEST_DATA_DIR}\)" CATCH_CONFIG_FAST_COMPILE)
target_compile_definitions(test_common INTERFACE TEST_DATA_DIR=R"\(${TEST_DATA_DIR}\)" PROFILES_DIR=R"\(${PROFILES_DIR}\)" CATCH_CONFIG_FAST_COMPILE)
target_include_directories(test_common INTERFACE ${CMAKE_CURRENT_SOURCE_DIR})
if (APPLE)
@@ -57,5 +61,6 @@ add_subdirectory(libslic3r)
add_subdirectory(slic3rutils)
add_subdirectory(fff_print)
add_subdirectory(sla_print)
add_subdirectory(filament_group)

View File

@@ -2,12 +2,14 @@
#include "libslic3r/libslic3r.h"
#include "libslic3r/GCode/GCodeProcessor.hpp"
#include "libslic3r/MultiNozzleUtils.hpp"
#include "libslic3r/PrintConfig.hpp"
#include "test_utils.hpp"
#include <fstream>
#include <map>
#include <memory>
using namespace Slic3r;
using Catch::Matchers::WithinAbs;
@@ -323,3 +325,96 @@ TEST_CASE("Carried-forward tool-change delay reaches the total without polluting
REQUIRE_THAT(rd.at(role), WithinAbs(zero_time, 1e-2));
}
}
TEST_CASE("Per-slot machine limits follow the active nozzle", "[GCodeTiming][MultiNozzle]")
{
// Single physical extruder carrying two nozzle variants: machine slot 0 (Standard) caps X/Y
// speed at 200 mm/s, slot 1 (High Flow) at 50 mm/s. The estimator must clamp each move by the
// slot of the nozzle the active filament occupies -- resolved from the grouping context handed
// over before the replay plus the occupancy recorder, i.e. the exact in-slicer streaming path.
FullPrintConfig config = make_config(0.0, 0.0, 0.0);
config.extruder_type.values = {static_cast<int>(etDirectDrive)};
config.printer_extruder_id.values = {1, 1};
config.printer_extruder_variant.values = {"Direct Drive Standard", "Direct Drive High Flow"};
// Slot-major layout: [slot0-Normal, slot0-Stealth, slot1-Normal, slot1-Stealth].
config.machine_max_speed_x.values = {200., 200., 50., 50.};
config.machine_max_speed_y.values = {200., 200., 50., 50.};
config.machine_max_speed_z.values = {200., 200., 50., 50.};
config.machine_max_speed_e.values = {200., 200., 50., 50.};
// Keep acceleration and jerk far from limiting so move times are speed-dominated.
for (auto *accel : {&config.machine_max_acceleration_x, &config.machine_max_acceleration_y,
&config.machine_max_acceleration_z, &config.machine_max_acceleration_e})
accel->values = {100000., 100000., 100000., 100000.};
config.machine_max_acceleration_travel.values = {100000., 100000.};
config.machine_max_acceleration_extruding.values = {100000., 100000.};
config.machine_max_jerk_x.values = {10000., 10000.};
config.machine_max_jerk_y.values = {10000., 10000.};
config.machine_max_jerk_z.values = {10000., 10000.};
config.machine_max_jerk_e.values = {10000., 10000.};
// Grouping stub: filament 0 lives on the Standard nozzle (slot 0), filament 1 on the
// High Flow nozzle (slot 1), both mounted on extruder 0.
std::vector<MultiNozzleUtils::NozzleInfo> nozzles;
{
MultiNozzleUtils::NozzleInfo n;
n.diameter = "0.4";
n.volume_type = nvtStandard; n.extruder_id = 0; n.group_id = 0; nozzles.push_back(n);
n.volume_type = nvtHighFlow; n.extruder_id = 0; n.group_id = 1; nozzles.push_back(n);
}
std::vector<int> filament_nozzle_map = {0, 1};
std::vector<unsigned int> used_filaments = {0, 1};
auto group = MultiNozzleUtils::LayeredNozzleGroupResult::create(filament_nozzle_map, nozzles, used_filaments);
REQUIRE(group.has_value());
auto context = std::make_shared<MultiNozzleUtils::LayeredNozzleGroupResult>(*group);
// Two identical 100 mm X travels, one per filament; T..H.. carries the target nozzle id.
// The trailing 1 mm move keeps two blocks queued at finalize, so the measured move's time is
// flushed (a lone final block is never attributed); it adds 1 mm to the second bucket.
const char* gcode =
"M83\n"
"T0 H0\n"
"G1 X100 F30000\n"
"T1 H1\n"
"G1 X0 F30000\n"
"G1 X1 F30000\n";
// Travel time accumulated after each tool-change move (bucket 0 = before any T).
auto travel_times_by_tool = [](const GCodeProcessorResult& r) {
std::vector<double> out(1, 0.0);
for (const auto& mv : r.moves) {
if (mv.type == EMoveType::Tool_change)
out.push_back(0.0);
else if (mv.type == EMoveType::Travel)
out.back() += mv.time[NORMAL];
}
return out;
};
SECTION("the move on the High Flow nozzle is clamped by its own slot") {
GCodeProcessor proc;
proc.initialize_from_context(context);
run_processor(proc, config, gcode);
auto times = travel_times_by_tool(proc.get_result());
REQUIRE(times.size() == 3);
REQUIRE_THAT(times[1], Catch::Matchers::WithinRel(100.0 / 200.0, 0.10));
REQUIRE_THAT(times[2], Catch::Matchers::WithinRel(101.0 / 50.0, 0.10));
}
SECTION("an emitted envelope line reaches every slot") {
const std::string enveloped = std::string("M201 X20000\nM203 X80\n") + gcode;
GCodeProcessor proc;
proc.initialize_from_context(context);
run_processor(proc, config, enveloped.c_str());
auto times = travel_times_by_tool(proc.get_result());
REQUIRE(times.size() == 3);
REQUIRE_THAT(times[1], Catch::Matchers::WithinRel(100.0 / 80.0, 0.10));
REQUIRE_THAT(times[2], Catch::Matchers::WithinRel(101.0 / 80.0, 0.10));
}
SECTION("no grouping context degrades to slot 0") {
GCodeProcessor proc;
run_processor(proc, config, gcode);
auto times = travel_times_by_tool(proc.get_result());
REQUIRE(times.size() == 3);
REQUIRE_THAT(times[1], Catch::Matchers::WithinRel(100.0 / 200.0, 0.10));
REQUIRE_THAT(times[2], Catch::Matchers::WithinRel(101.0 / 200.0, 0.10));
}
}

View File

@@ -1,6 +1,10 @@
#include <catch2/catch_all.hpp>
#include <cstdlib>
#include <memory>
#include <sstream>
#include <string>
#include <vector>
#include "libslic3r/GCodeWriter.hpp"
#include "libslic3r/GCode.hpp"
@@ -8,11 +12,23 @@
#include "libslic3r/Print.hpp"
#include "libslic3r/ModelArrange.hpp"
#include <boost/filesystem.hpp>
#include "test_helpers.hpp"
using namespace Slic3r;
using namespace Slic3r::Test;
// Arrange on a finite bed, not an unbounded InfiniteBed: the latter places items
// near INT64_MIN/4 (~2.3e18), which reaches ClipperLib's coordinate limit and throws
// "Coordinate outside allowed range" on Windows/arm64. A 500x500 bed keeps coordinates
// small while still covering large printers.
static void arrange_objects_on_test_bed(Model &model, const DynamicPrintConfig &config)
{
const BoundingBox bed{Point::new_scale(0., 0.), Point::new_scale(500., 500.)};
arrange_objects(model, bed, ArrangeParams{scaled(min_object_distance(config))});
}
SCENARIO("set_speed emits values with fixed-point output.", "[GCodeWriter]") {
GIVEN("GCodeWriter instance") {
@@ -241,8 +257,7 @@ TEST_CASE("Machine envelope emits max limit among used extruders", "[GCodeWriter
obj2->config.set_key_value("extruder", new ConfigOptionInt(2)); // 0-based index 1
Print print;
arrange_objects(model, InfiniteBed{},
ArrangeParams{scaled(min_object_distance(config))});
arrange_objects_on_test_bed(model, config);
for (auto* mo : model.objects) {
mo->ensure_on_bed();
print.auto_assign_extruders(mo);
@@ -354,7 +369,7 @@ TEST_CASE("EXTRUDER_LIMIT per-extruder clamping and max fallback", "[GCodeWriter
obj2->config.set_key_value("extruder", new ConfigOptionInt(2)); // 0-based index 1
Print print;
arrange_objects(model, InfiniteBed{}, ArrangeParams{scaled(min_object_distance(config))});
arrange_objects_on_test_bed(model, config);
for (auto* mo : model.objects) {
mo->ensure_on_bed();
print.auto_assign_extruders(mo);
@@ -404,3 +419,404 @@ TEST_CASE("EXTRUDER_LIMIT per-extruder clamping and max fallback", "[GCodeWriter
}
}
}
SCENARIO("Extruder reads the injected config column", "[GCodeWriter][H2C]") {
GIVEN("A writer whose per-variant arrays hold three columns for two filaments") {
GCodeWriter writer;
// Column layout after a migrating regroup: filament 0 -> column 0, filament 1 ->
// columns 1 (its first variant) and 2 (its second variant).
writer.config.retraction_length.values = {0.8, 0.5, 1.2};
writer.config.z_hop.values = {0.4, 0.6, 0.9};
writer.config.retraction_speed.values = {30., 40., 50.};
writer.config.filament_flow_ratio.values = {0.98, 1.0, 1.02};
// Filament-indexed arrays keep one entry per filament.
writer.config.filament_diameter.values = {1.75, 1.75};
writer.set_extruders({0, 1});
writer.toolchange(1, 1);
Extruder *fil = writer.filament();
REQUIRE(fil != nullptr);
REQUIRE(fil->id() == 1);
const double crossection = 1.75 * 1.75 * 0.25 * PI;
WHEN("no column has been injected") {
THEN("the getters read the filament id's column") {
REQUIRE(fil->config_index() == 1);
REQUIRE_THAT(fil->retraction_length(), Catch::Matchers::WithinAbs(0.5, 1e-9));
REQUIRE_THAT(fil->retract_lift(), Catch::Matchers::WithinAbs(0.6, 1e-9));
REQUIRE(fil->retract_speed() == 40);
REQUIRE_THAT(fil->e_per_mm3(), Catch::Matchers::WithinRel(1.0 / crossection, 1e-9));
}
}
WHEN("the second variant column is injected") {
fil->set_config_index(2);
THEN("the getters follow the column and the flow cache is rescaled") {
REQUIRE(fil->config_index() == 2);
REQUIRE_THAT(fil->retraction_length(), Catch::Matchers::WithinAbs(1.2, 1e-9));
REQUIRE_THAT(fil->retract_lift(), Catch::Matchers::WithinAbs(0.9, 1e-9));
REQUIRE(fil->retract_speed() == 50);
REQUIRE_THAT(fil->e_per_mm3(), Catch::Matchers::WithinRel(1.02 / crossection, 1e-9));
}
THEN("filament-indexed reads keep using the filament id") {
REQUIRE_THAT(fil->filament_diameter(), Catch::Matchers::WithinAbs(1.75, 1e-9));
}
}
WHEN("a negative index is injected") {
fil->set_config_index(2);
fil->set_config_index(-1);
THEN("resolution resets to the filament id") {
REQUIRE(fil->config_index() == 1);
REQUIRE_THAT(fil->retraction_length(), Catch::Matchers::WithinAbs(0.5, 1e-9));
REQUIRE_THAT(fil->e_per_mm3(), Catch::Matchers::WithinRel(1.0 / crossection, 1e-9));
}
}
}
}
// Numeric argument of every line starting with `prefix`, in file order.
static std::vector<int> collect_line_args(const std::string &gcode, const std::string &prefix)
{
std::vector<int> values;
std::istringstream stream(gcode);
std::string line;
while (std::getline(stream, line))
if (line.compare(0, prefix.size(), prefix) == 0)
values.push_back(std::atoi(line.c_str() + int(prefix.size())));
return values;
}
static int count_lines_with_prefix(const std::string &gcode, const std::string &prefix)
{
return (int) collect_line_args(gcode, prefix).size();
}
// A toolchange ordinal sequence is healthy when it advances by exactly one per
// change block; a change-less prime-tower visit must not consume an ordinal.
static bool ordinals_consecutive(const std::vector<int> &values)
{
for (size_t i = 1; i < values.size(); ++i)
if (values[i] != values[i - 1] + 1)
return false;
return true;
}
SCENARIO("Toolchange emission and prefix per printer kind", "[GCodeWriter][H2C]") {
GIVEN("A dual-extruder writer with two filaments") {
GCodeWriter writer;
writer.config.filament_diameter.values = {1.75, 1.75};
writer.set_extruders({0, 1});
WHEN("the printer is a BBL machine") {
writer.set_is_bbl_machine(true);
THEN("the toolchange prefix is the plain T command") {
REQUIRE_THAT(writer.toolchange_prefix(), Catch::Matchers::Equals("T"));
}
THEN("toolchange emits a single M1020 with the nozzle id") {
const std::string gcode = writer.toolchange(1, 0);
REQUIRE_THAT(gcode, Catch::Matchers::ContainsSubstring("M1020 S1 H0"));
REQUIRE_THAT(gcode, !Catch::Matchers::StartsWith("T1"));
}
THEN("the other filament and nozzle emit their own ids") {
REQUIRE_THAT(writer.toolchange(0, 1), Catch::Matchers::ContainsSubstring("M1020 S0 H1"));
}
THEN("an unresolved nozzle keeps the literal -1 convention") {
REQUIRE_THAT(writer.toolchange(1, -1), Catch::Matchers::ContainsSubstring("M1020 S1 H-1"));
}
}
WHEN("the printer is a BBL machine with manual filament change") {
writer.set_is_bbl_machine(true);
writer.config.manual_filament_change.value = true;
THEN("the manual tag wins over the M1020 form") {
REQUIRE_THAT(writer.toolchange_prefix(), Catch::Matchers::StartsWith(";"));
const std::string gcode = writer.toolchange(1, 0);
REQUIRE_THAT(gcode, Catch::Matchers::ContainsSubstring(writer.toolchange_prefix() + "1"));
REQUIRE_THAT(gcode, !Catch::Matchers::ContainsSubstring("M1020"));
}
}
WHEN("the printer is not a BBL machine") {
THEN("toolchange keeps the plain T command") {
REQUIRE_THAT(writer.toolchange_prefix(), Catch::Matchers::Equals("T"));
const std::string gcode = writer.toolchange(1, 0);
REQUIRE_THAT(gcode, Catch::Matchers::StartsWith("T1"));
REQUIRE_THAT(gcode, !Catch::Matchers::ContainsSubstring("M1020"));
}
}
}
}
// Shared dual-extruder printer config for the toolchange-count scenarios below.
static DynamicPrintConfig dual_extruder_toolchange_config()
{
DynamicPrintConfig config = DynamicPrintConfig::full_print_config();
config.set_key_value("gcode_flavor", new ConfigOptionEnum<GCodeFlavor>(gcfMarlinFirmware));
config.set_key_value("emit_machine_limits_to_gcode", new ConfigOptionBool(false));
config.set_key_value("machine_start_gcode", new ConfigOptionString(""));
config.set_key_value("layer_height", new ConfigOptionFloat(0.2));
config.set_key_value("initial_layer_print_height", new ConfigOptionFloat(0.2));
config.set_key_value("initial_layer_line_width", new ConfigOptionFloatOrPercent(0, false));
config.set_key_value("z_hop", new ConfigOptionFloats({0., 0.}));
// The change block carries both a real toolchange command and the ordinal
// placeholder the stock profiles feed to the firmware.
config.set_key_value("change_filament_gcode",
new ConfigOptionString("T[next_filament_id]\nM620 O{toolchange_count + 1}\n"));
// 2 extruders, one filament each (manual map so nothing regroups them).
config.set_key_value("nozzle_diameter", new ConfigOptionFloats({0.4, 0.4}));
config.set_key_value("printer_extruder_id", new ConfigOptionInts({1, 2}));
config.set_key_value("printer_extruder_variant", new ConfigOptionStrings({"Direct Drive Standard", "Direct Drive Standard"}));
config.set_key_value("filament_diameter", new ConfigOptionFloats({1.75, 1.75}));
config.set_key_value("filament_colour", new ConfigOptionStrings({"#FF0000", "#00FF00"}));
config.set_key_value("default_filament_colour", new ConfigOptionStrings({"#FF0000", "#00FF00"}));
config.set_key_value("filament_type", new ConfigOptionStrings({"PLA", "PLA"}));
config.option<ConfigOptionEnum<FilamentMapMode>>("filament_map_mode", true)->value = fmmManual;
config.set_key_value("filament_map", new ConfigOptionInts({1, 2}));
config.set_key_value("nozzle_temperature", new ConfigOptionInts({210, 210}));
config.set_key_value("nozzle_temperature_range_low", new ConfigOptionInts({190, 190}));
config.set_key_value("nozzle_temperature_range_high", new ConfigOptionInts({240, 240}));
config.set_key_value("flush_multiplier", new ConfigOptionFloats({1}));
config.set_key_value("flush_volumes_matrix", new ConfigOptionFloats({0, 140, 140, 0}));
return config;
}
SCENARIO("Change blocks carry consecutive toolchange ordinals without a duplicate command", "[GCodeWriter][H2C]") {
GIVEN("Two sequentially printed objects on different extruders of a BBL machine") {
DynamicPrintConfig config = dual_extruder_toolchange_config();
config.set_key_value("print_sequence", new ConfigOptionEnum<PrintSequence>(PrintSequence::ByObject));
Model model;
auto *obj1 = model.add_object();
obj1->add_volume(cube(20));
obj1->add_instance();
auto *obj2 = model.add_object();
obj2->add_volume(cube(20));
obj2->add_instance();
obj2->config.set_key_value("extruder", new ConfigOptionInt(2));
auto slice_to_gcode = [&]() {
Print print;
print.is_BBL_printer() = true;
arrange_objects_on_test_bed(model, config);
for (auto *mo : model.objects) {
mo->ensure_on_bed();
print.auto_assign_extruders(mo);
}
print.apply(model, config);
print.validate();
print.set_status_silent();
print.process();
return Slic3r::Test::gcode(print);
};
WHEN("the change block already changes the tool") {
const std::string gcode = slice_to_gcode();
const std::vector<int> ordinals = collect_line_args(gcode, "M620 O");
THEN("each change block advances the ordinal by exactly one, without inflation") {
REQUIRE(!ordinals.empty());
REQUIRE(ordinals_consecutive(ordinals));
REQUIRE(ordinals.front() <= 3);
}
THEN("the writer's own command is suppressed as a duplicate") {
REQUIRE(count_lines_with_prefix(gcode, "M1020") == 0);
REQUIRE(count_lines_with_prefix(gcode, "T1") >= 1);
}
}
WHEN("the change block does not change the tool itself") {
config.set_key_value("change_filament_gcode",
new ConfigOptionString("M620 O{toolchange_count + 1}\n"));
const std::string gcode = slice_to_gcode();
const std::vector<int> ordinals = collect_line_args(gcode, "M620 O");
THEN("the writer's toolchange survives and carries a nozzle id") {
REQUIRE(count_lines_with_prefix(gcode, "M1020 S1 H") >= 1);
}
THEN("the ordinal sequence stays consecutive") {
REQUIRE(!ordinals.empty());
REQUIRE(ordinals_consecutive(ordinals));
REQUIRE(ordinals.front() <= 3);
}
}
}
}
SCENARIO("Prime-tower visits without a filament change do not advance the toolchange ordinal", "[GCodeWriter][H2C]") {
GIVEN("A print whose only filament change happens far above the bed") {
DynamicPrintConfig config = dual_extruder_toolchange_config();
config.set_key_value("enable_prime_tower", new ConfigOptionBool(true));
// Filament 2 is used only above z=6, so every tower layer below it is a
// change-less visit — the exact geometry that used to inflate the ordinal.
Model model;
auto *obj = model.add_object();
obj->add_volume(cube(10));
obj->add_instance();
DynamicPrintConfig range_config;
range_config.set_key_value("extruder", new ConfigOptionInt(2));
// Every layer range must carry a layer_height (see layer_height_profile_from_ranges).
range_config.set_key_value("layer_height", new ConfigOptionFloat(0.2));
obj->layer_config_ranges[{6.0, 10.0}].assign_config(std::move(range_config));
Print print;
print.is_BBL_printer() = true;
arrange_objects_on_test_bed(model, config);
for (auto *mo : model.objects) {
mo->ensure_on_bed();
print.auto_assign_extruders(mo);
}
print.apply(model, config);
print.validate();
print.set_status_silent();
print.process();
const std::string gcode = Slic3r::Test::gcode(print);
WHEN("the print is exported") {
const std::vector<int> ordinals = collect_line_args(gcode, "M620 O");
THEN("the prime-tower toolchange path was exercised") {
REQUIRE_THAT(gcode, Catch::Matchers::ContainsSubstring("CP TOOLCHANGE START"));
}
THEN("dozens of change-less tower layers consume no ordinal") {
REQUIRE(!ordinals.empty());
REQUIRE(ordinals_consecutive(ordinals));
REQUIRE(ordinals.front() <= 3);
}
THEN("no duplicate toolchange command follows the change block") {
REQUIRE(count_lines_with_prefix(gcode, "M1020") == 0);
}
}
}
}
// ---------------------------------------------------------------------------
// Real-profile toolchange coverage, targeted. The all-vendors sweep in test_profile_slicing.cpp now
// slices a two-colour cube per printer, so it already expands every shipped change_filament_gcode with
// each printer's DEFAULT extruder variants (both the single-nozzle append_tcr and dual-nozzle set_extruder
// paths). What that sweep can't reach is a variant-conditional branch the defaults never select — H2D's
// change gcode has an `== "Direct Drive TPU High Flow"` block. This scenario forces that branch by handing
// the extruders distinct kits, so an unregistered placeholder inside it still throws "Variable does not
// exist" here instead of only in the field.
// ---------------------------------------------------------------------------
// Two 20mm cubes on separate extruders of a BBL machine, printed by object so exactly
// one real toolchange fires and drives the change_filament_gcode. Returns the g-code.
static std::string slice_two_object_bbl(DynamicPrintConfig &config)
{
config.set_key_value("print_sequence", new ConfigOptionEnum<PrintSequence>(PrintSequence::ByObject));
Model model;
auto *obj1 = model.add_object();
obj1->add_volume(cube(20));
obj1->add_instance();
auto *obj2 = model.add_object();
obj2->add_volume(cube(20));
obj2->add_instance();
obj2->config.set_key_value("extruder", new ConfigOptionInt(2));
Print print;
print.is_BBL_printer() = true;
arrange_objects_on_test_bed(model, config);
for (auto *mo : model.objects) {
mo->ensure_on_bed();
print.auto_assign_extruders(mo);
}
print.apply(model, config);
print.validate();
print.set_status_silent();
print.process();
return Slic3r::Test::gcode(print);
}
// The real change_filament_gcode of a shipped "<printer> 0.4 nozzle" machine profile.
static std::string shipped_change_filament_gcode(const std::string &printer)
{
const std::string path = std::string(PROFILES_DIR) + "/BBL/machine/Bambu Lab " + printer + " 0.4 nozzle.json";
// PROFILES_DIR is an absolute path baked in at build time; a sparse test checkout
// without resources/ leaves it missing. Skip rather than dereference a config that
// never loaded - this is the only fff_print test that reads a shipped profile.
if (!boost::filesystem::exists(path))
SKIP("shipped profile not present in this checkout: " << path);
DynamicPrintConfig config;
std::map<std::string, std::string> key_values;
std::string reason;
config.load_from_json(path, ForwardCompatibilitySubstitutionRule::Enable, key_values, reason);
// Fail loudly on a malformed/renamed profile instead of null-dereferencing in opt_string.
INFO("profile: " << path << (reason.empty() ? "" : (" load reason: " + reason)));
REQUIRE(config.has("change_filament_gcode"));
return config.opt_string("change_filament_gcode");
}
SCENARIO("Toolchange gcode resolves old/new_extruder_variant from printer_extruder_variant", "[GCodeWriter][H2C]")
{
GIVEN("a BBL dual-extruder print whose change gcode reads the extruder-variant placeholders") {
DynamicPrintConfig config = dual_extruder_toolchange_config();
// A distinctive variant that can only reach the g-code through printer_extruder_variant.
// Both entries carry it so the assertion is independent of which physical extruder the
// emitted change routes through.
config.set_key_value("printer_extruder_variant",
new ConfigOptionStrings({"Direct Drive TPU High Flow", "Direct Drive TPU High Flow"}));
config.set_key_value("change_filament_gcode", new ConfigOptionString(
"; VARIANT old={old_extruder_variant} new={new_extruder_variant}\nT[next_filament_id]\n"));
WHEN("the print is sliced") {
const std::string gcode = slice_two_object_bbl(config);
THEN("both placeholders resolve to the printer_extruder_variant value") {
// The resolved line is the proof: an unresolved token or a parser throw would
// prevent this exact line from being emitted. (A negative "{token}" check is
// unreliable — the g-code's trailing config dump echoes the raw template.)
REQUIRE_THAT(gcode, Catch::Matchers::ContainsSubstring(
"; VARIANT old=Direct Drive TPU High Flow new=Direct Drive TPU High Flow"));
}
}
}
}
SCENARIO("Global current-tool placeholders resolve in a context with no local injection", "[GCodeWriter][H2C]")
{
GIVEN("a BBL dual-extruder print whose before_layer_change_gcode reads the current-tool placeholders") {
DynamicPrintConfig config = dual_extruder_toolchange_config();
// before_layer_change is one of the contexts that inject NO current_* into their local config
// (unlike change_filament / machine_end / layer_change), so these placeholders can only resolve
// through the GLOBAL parser vars published at each toolchange (and at the initial set_extruder).
// Pre-fix current_filament_id / current_extruder_id / current_nozzle_id were undefined here and the
// whole slice threw a PlaceholderParserError — the same failure mode X2D's layer_change hit.
config.set_key_value("before_layer_change_gcode", new ConfigOptionString(
"; GVAR fid={current_filament_id} eid={current_extruder_id} nid={current_nozzle_id}\n"));
WHEN("the print is sliced (obj1 on filament 0, obj2 on filament 1)") {
std::string gcode;
REQUIRE_NOTHROW(gcode = slice_two_object_bbl(config));
THEN("all three globals resolve to the CORRECT active-tool values on both sides of the change") {
// Assert the FULL resolved marker, not just no-throw: obj1 prints on filament 0 (extruder 0,
// nozzle 0) and obj2 on filament 1 (extruder 1, nozzle 1). Locking every field means a
// stale or wrong global (e.g. obj2 still reading fid=0, or a mismatched extruder/nozzle id)
// fails here — this is the value guard that replaces the old "throws on undefined" canary.
// Only the emitted before_layer_change lines carry resolved values; the trailing config dump
// keeps the raw "{current_filament_id}" template, so these are unambiguous.
REQUIRE_THAT(gcode, Catch::Matchers::ContainsSubstring("; GVAR fid=0 eid=0 nid=0"));
REQUIRE_THAT(gcode, Catch::Matchers::ContainsSubstring("; GVAR fid=1 eid=1 nid=1"));
}
}
}
}
SCENARIO("Shipped dual-nozzle change_filament_gcode resolves during a real slice", "[GCodeWriter][H2C][Profiles]")
{
const std::string printer = GENERATE(std::string("H2C"), std::string("H2D"), std::string("H2D Pro"), std::string("X2D"));
GIVEN("the real " + printer + " change_filament_gcode driving a BBL dual-extruder slice") {
DynamicPrintConfig config = dual_extruder_toolchange_config();
config.set_key_value("change_filament_gcode", new ConfigOptionString(shipped_change_filament_gcode(printer)));
// H2D's gcode branches on the extruder variant; give the extruders distinct kits so the
// "Direct Drive TPU High Flow" branch is reachable.
config.set_key_value("printer_extruder_variant",
new ConfigOptionStrings({"Direct Drive Standard", "Direct Drive TPU High Flow"}));
// Extruder-indexed machine rates the stock gcode divides by (default size 1); size to 2 extruders.
config.set_key_value("hotend_cooling_rate", new ConfigOptionFloatsNullable({2.0, 2.0}));
config.set_key_value("hotend_heating_rate", new ConfigOptionFloatsNullable({2.0, 2.0}));
THEN("every placeholder resolves (no undefined-variable throw) and the change block runs") {
std::string gcode;
REQUIRE_NOTHROW(gcode = slice_two_object_bbl(config));
// A resolved marker only the emitted change block produces (the trailing config
// dump keeps the raw "{filament_type[...]}" template), so this confirms the real
// change_filament_gcode was expanded, not merely echoed.
REQUIRE_THAT(gcode, Catch::Matchers::ContainsSubstring("set_filament_type:PLA"));
}
}
}

View File

@@ -85,3 +85,22 @@ TEST_CASE("Per-object wall filament override is honored", "[MultiFilament]")
CHECK(tools_for_role(gcode, "perimeter") == std::set<int>{ 0, 1 });
CHECK(tools_for_role(gcode, "infill") == std::set<int>{ 0 }); // infill not overridden: stays on F1
}
// max_layer_height can be shorter than the extruder count (normalization sizes it to the
// filament count under single_extruder_multi_material). calc_max_layer_height() in ToolOrdering
// indexed it per-nozzle and read past the end. Shortened directly here to isolate that read;
// the other per-extruder keys stay extruder-length so slicing reaches the code under test.
TEST_CASE("Multi-extruder slice stays in bounds with a short max_layer_height", "[MultiFilament]")
{
DynamicPrintConfig config = multifilament_config(2);
config.set_deserialize_strict({
{ "nozzle_diameter", "0.4,0.4" },
{ "printer_extruder_id", "1,2" },
{ "printer_extruder_variant", "Direct Drive Standard,Direct Drive Standard" },
{ "extruder_printable_height", "0,0" },
{ "max_layer_height", "0.3" }, // deliberately one entry short
});
Print print;
init_and_process_print({ cube(20) }, print, config);
REQUIRE_FALSE(print.objects().front()->layers().empty());
}

View File

@@ -418,3 +418,25 @@ TEST_CASE("Sequential printing follows model order", "[Print]")
REQUIRE_THAT(first_object_peak_z, Catch::Matchers::WithinAbs(20.0, 0.3));
}
// A sequential (by-object) print must publish the print-level nozzle group result just
// like a by-layer print, so custom g-code can index the per-nozzle placeholder tables
// (e.g. nozzle_diameter_at_nozzle_id[]) instead of failing on an empty vector.
TEST_CASE("Sequential printing publishes the nozzle group result", "[Print][MultiNozzle]")
{
SECTION("process() publishes the result") {
Print print;
Model model;
place_two_cubes_apart(60.0, { { "print_sequence", "by object" } }, print, model);
print.process();
REQUIRE(print.get_layered_nozzle_group_result() != nullptr);
}
SECTION("start g-code can index the per-nozzle diameter table") {
const std::string gcode = slice_two_cubes_arranged({
{ "print_sequence", "by object" },
{ "machine_start_gcode", "{if nozzle_diameter_at_nozzle_id[0] > 0}; SEQ-ND-OK\n{endif}" },
});
CHECK(gcode.find("; SEQ-ND-OK") != std::string::npos);
}
}

View File

@@ -93,3 +93,14 @@ SCENARIO("Support layer Z honors contact distance", "[SupportMaterial]")
}
}
}
// extrude_support once held a `static` lambda capturing `this`, so a second export in the
// same process dereferenced a returned stack frame (ASan: stack-use-after-return).
TEST_CASE("Support G-code emission survives a second slice in the same process", "[SupportMaterial][Regression]")
{
const std::string first = slice({ TestMesh::overhang }, { { "enable_support", 1 } });
REQUIRE(! layers_with_role(first, "support").empty());
const std::string second = slice({ TestMesh::overhang }, { { "enable_support", 1 } });
REQUIRE(! layers_with_role(second, "support").empty());
}

View File

@@ -0,0 +1,23 @@
get_filename_component(_TEST_NAME ${CMAKE_CURRENT_LIST_DIR} NAME)
set(FG_GOLDEN_DIR ${CMAKE_CURRENT_SOURCE_DIR}/golden)
file(TO_NATIVE_PATH "${FG_GOLDEN_DIR}" FG_GOLDEN_DIR)
add_executable(${_TEST_NAME}_tests
filament_group_regression_main.cpp
)
target_link_libraries(${_TEST_NAME}_tests test_common libslic3r nlohmann_json Catch2::Catch2WithMain)
# ${CMAKE_SOURCE_DIR}/deps_src puts <nlohmann/json.hpp> on the include path the same way the
# libslic3r translation units resolve it (via the admesh/.. system include); nlohmann_json's own
# interface dir only exposes <json.hpp>, so the source-tree include is required for the <nlohmann/…>
# spelling the serializers use.
target_include_directories(${_TEST_NAME}_tests PRIVATE ${CMAKE_SOURCE_DIR}/src ${CMAKE_SOURCE_DIR}/deps_src)
target_compile_definitions(${_TEST_NAME}_tests PRIVATE
FG_TEST_GOLDEN_DIR=R"\(${FG_GOLDEN_DIR}\)"
)
set_property(TARGET ${_TEST_NAME}_tests PROPERTY FOLDER "tests")
orcaslicer_copy_test_dlls()
orcaslicer_discover_tests(${_TEST_NAME}_tests)

View File

@@ -0,0 +1,237 @@
#ifndef FG_TEST_EVALUATOR_HPP
#define FG_TEST_EVALUATOR_HPP
#include "fg_test_serialization.hpp"
#include <libslic3r/FilamentGroup.hpp>
#include <libslic3r/GCode/ToolOrderUtils.hpp>
#include <libslic3r/MultiNozzleUtils.hpp>
#include <chrono>
#include <sstream>
#include <unordered_set>
namespace Slic3r {
namespace FGTest {
inline bool check_constraints(const FilamentGroupContext& ctx,
const std::vector<int>& filament_map,
std::vector<std::string>& violations) {
violations.clear();
auto used_filaments = collect_sorted_used_filaments(ctx.model_info.layer_filaments);
// 1. unprintable_filaments check
for (size_t ext = 0; ext < ctx.model_info.unprintable_filaments.size(); ++ext) {
for (int fil : ctx.model_info.unprintable_filaments[ext]) {
if (fil < 0 || fil >= (int)filament_map.size())
continue;
int assigned_nozzle = filament_map[fil];
if (assigned_nozzle < 0 || assigned_nozzle >= (int)ctx.nozzle_info.nozzle_list.size())
continue;
if (ctx.nozzle_info.nozzle_list[assigned_nozzle].extruder_id == (int)ext) {
std::ostringstream ss;
ss << "filament " << fil << " assigned to nozzle " << assigned_nozzle
<< " (extruder " << ext << ") but is unprintable there";
violations.push_back(ss.str());
}
}
}
// 2. unprintable_volumes check
for (auto& [fil, volume_types] : ctx.model_info.unprintable_volumes) {
if (fil < 0 || fil >= (int)filament_map.size())
continue;
int assigned_nozzle = filament_map[fil];
if (assigned_nozzle < 0 || assigned_nozzle >= (int)ctx.nozzle_info.nozzle_list.size())
continue;
if (volume_types.count(ctx.nozzle_info.nozzle_list[assigned_nozzle].volume_type)) {
std::ostringstream ss;
ss << "filament " << fil << " assigned to nozzle " << assigned_nozzle
<< " with volume_type " << (int)ctx.nozzle_info.nozzle_list[assigned_nozzle].volume_type
<< " but that type is unprintable for this filament";
violations.push_back(ss.str());
}
}
// 3. max_group_size per extruder. This cap is an invariant of the flush-partition
// modes only: those solvers partition the filaments across extruders subject to
// each extruder's capacity. MatchMode instead maps every filament to the extruder
// holding the nearest-color loaded AMS filament and does not partition by capacity
// (its solver capacity is the filament count, not max_group_size), so a legitimate
// match may place more than max_group_size filaments on one extruder. Enforce the
// cap only for the partition modes, and only when the instance is feasible.
int total_capacity = 0;
for (auto sz : ctx.machine_info.max_group_size)
total_capacity += sz;
if (ctx.group_info.mode != FGMode::MatchMode &&
total_capacity >= (int)used_filaments.size()) {
std::map<int, int> extruder_count;
for (auto fil : used_filaments) {
if (fil >= filament_map.size()) continue;
int nozzle_id = filament_map[fil];
if (nozzle_id < 0 || nozzle_id >= (int)ctx.nozzle_info.nozzle_list.size())
continue;
extruder_count[ctx.nozzle_info.nozzle_list[nozzle_id].extruder_id]++;
}
for (auto& [ext, count] : extruder_count) {
if (ext >= 0 && ext < (int)ctx.machine_info.max_group_size.size()) {
if (count > ctx.machine_info.max_group_size[ext]) {
std::ostringstream ss;
ss << "extruder " << ext << " has " << count << " filaments but max is "
<< ctx.machine_info.max_group_size[ext];
violations.push_back(ss.str());
}
}
}
}
return violations.empty();
}
inline int compute_flush_cost(const FilamentGroupContext& ctx,
const std::vector<int>& filament_map) {
auto used_filaments = collect_sorted_used_filaments(ctx.model_info.layer_filaments);
if (used_filaments.empty())
return 0;
auto nozzle_group_result = MultiNozzleUtils::LayeredNozzleGroupResult::create(
filament_map, ctx.nozzle_info.nozzle_list, used_filaments);
if (!nozzle_group_result)
return -1;
std::vector<std::vector<unsigned int>> filament_sequences;
auto get_custom_seq_null = [](int, std::vector<int>&) -> bool { return false; };
int cost = reorder_filaments_for_multi_nozzle_extruder(
used_filaments,
*nozzle_group_result,
ctx.model_info.layer_filaments,
ctx.model_info.flush_matrix,
get_custom_seq_null,
&filament_sequences,
MultiNozzleUtils::NozzleStatusRecorder{});
return cost;
}
struct FullEvalResult {
int flush_cost = 0;
double change_time = 0.0;
double full_score = 0.0;
bool constraints_ok = true;
std::vector<std::string> violations;
};
inline double evaluate_score(double flush, double time) {
double approx_density = 1.26;
double approx_flush_speed = 180;
double correction_factor = 2;
double flush_score = flush * approx_density * approx_flush_speed * correction_factor / 1000;
return flush_score + time;
}
inline double calc_change_time_for_group_eval(
const std::vector<int>& filament_change_seq,
const std::vector<int>& nozzle_change_seq,
const std::vector<int>& logical_filaments,
const std::vector<MultiNozzleUtils::NozzleInfo>& nozzle_list,
const MultiNozzleUtils::FilamentChangeTimeParams& time_params,
const std::vector<bool>& ams_preload_enabled,
const std::vector<int>& group_of_filament)
{
auto r = MultiNozzleUtils::simulate_filament_change_time(
logical_filaments, nozzle_list, filament_change_seq,
nozzle_change_seq, group_of_filament, time_params,
ams_preload_enabled);
return r.actual_time;
}
inline FullEvalResult full_evaluate_map(const FilamentGroupContext& ctx,
const std::vector<int>& filament_map) {
FullEvalResult result;
auto used_filaments = collect_sorted_used_filaments(ctx.model_info.layer_filaments);
if (used_filaments.empty()) return result;
auto nozzle_group_result = MultiNozzleUtils::LayeredNozzleGroupResult::create(
filament_map, ctx.nozzle_info.nozzle_list, used_filaments);
if (!nozzle_group_result) return result;
MultiNozzleUtils::NozzleStatusRecorder initial_status;
for (auto& [nozzle_id, filament_id] : ctx.nozzle_info.nozzle_status) {
if (filament_id >= 0) {
int extruder_id = 0;
for (const auto& nozzle : ctx.nozzle_info.nozzle_list) {
if (nozzle.group_id == nozzle_id) { extruder_id = nozzle.extruder_id; break; }
}
initial_status.set_nozzle_status(nozzle_id, filament_id, extruder_id);
}
}
std::vector<std::vector<unsigned int>> filament_sequences;
auto get_custom_seq_null = [](int, std::vector<int>&) -> bool { return false; };
result.flush_cost = reorder_filaments_for_multi_nozzle_extruder(
used_filaments, *nozzle_group_result, ctx.model_info.layer_filaments,
ctx.model_info.flush_matrix, get_custom_seq_null, &filament_sequences, initial_status);
if (!filament_sequences.empty()) {
std::vector<int> filament_change_seq;
std::vector<int> nozzle_change_seq;
int prev_fil = -1, prev_nozzle = -1;
for (const auto& layer_seq : filament_sequences) {
for (unsigned int fil : layer_seq) {
auto nozzle_info = nozzle_group_result->get_first_nozzle_for_filament(fil);
if (!nozzle_info) continue;
int nid = nozzle_info->group_id;
if ((int)fil == prev_fil && nid == prev_nozzle) continue;
filament_change_seq.push_back((int)fil);
nozzle_change_seq.push_back(nid);
prev_fil = (int)fil;
prev_nozzle = nid;
}
}
std::vector<int> logical_filaments(used_filaments.begin(), used_filaments.end());
std::vector<int> group_of_filament(used_filaments.size(), 0);
for (size_t fi = 0; fi < used_filaments.size(); ++fi) {
int nid = filament_map[used_filaments[fi]];
if (nid >= 0 && nid < (int)ctx.nozzle_info.nozzle_list.size())
group_of_filament[fi] = ctx.nozzle_info.nozzle_list[nid].extruder_id;
}
result.change_time = calc_change_time_for_group_eval(
filament_change_seq, nozzle_change_seq, logical_filaments,
ctx.nozzle_info.nozzle_list, ctx.speed_info.change_time_params,
ctx.speed_info.ams_preload_enabled, group_of_filament);
}
result.full_score = evaluate_score(result.flush_cost, result.change_time);
result.constraints_ok = check_constraints(ctx, filament_map, result.violations);
return result;
}
inline TestResult run_and_evaluate(const FilamentGroupContext& ctx,
const ClusteringBudget& budget = {}) {
TestResult result;
auto start = std::chrono::high_resolution_clock::now();
int algo_cost = 0;
FilamentGroup fg(ctx);
fg.set_clustering_budget(budget);
result.filament_map = fg.calc_filament_group(&algo_cost);
auto end = std::chrono::high_resolution_clock::now();
result.elapsed_ms = std::chrono::duration<double, std::milli>(end - start).count();
result.flush_cost = compute_flush_cost(ctx, result.filament_map);
result.constraints_ok = check_constraints(ctx, result.filament_map, result.violations);
return result;
}
} // namespace FGTest
} // namespace Slic3r
#endif // FG_TEST_EVALUATOR_HPP

View File

@@ -0,0 +1,447 @@
#ifndef FG_TEST_SERIALIZATION_HPP
#define FG_TEST_SERIALIZATION_HPP
#include <nlohmann/json.hpp>
#include <libslic3r/FilamentGroup.hpp>
#include <libslic3r/FilamentGroupUtils.hpp>
#include <libslic3r/MultiNozzleUtils.hpp>
#include <libslic3r/PrintConfig.hpp>
#include <fstream>
#include <optional>
#include <string>
#include <vector>
#include <set>
#include <map>
#include <unordered_map>
using json = nlohmann::json;
// Put serializers in correct ADL namespaces for each type
namespace Slic3r {
namespace FilamentGroupUtils {
inline void to_json(json& j, const Color& c) {
char buf[10];
snprintf(buf, sizeof(buf), "#%02X%02X%02X%02X", c.r, c.g, c.b, c.a);
j = std::string(buf);
}
inline void from_json(const json& j, Color& c) {
std::string s = j.get<std::string>();
if (s.size() >= 7 && s[0] == '#') {
c.r = (unsigned char)std::stoi(s.substr(1, 2), nullptr, 16);
c.g = (unsigned char)std::stoi(s.substr(3, 2), nullptr, 16);
c.b = (unsigned char)std::stoi(s.substr(5, 2), nullptr, 16);
c.a = (s.size() >= 9) ? (unsigned char)std::stoi(s.substr(7, 2), nullptr, 16) : 255;
}
}
inline void to_json(json& j, const FilamentInfo& fi) {
j = json{
{"color", fi.color},
{"type", fi.type},
{"is_support", fi.is_support},
{"usage_type", (int)fi.usage_type}
};
}
inline void from_json(const json& j, FilamentInfo& fi) {
fi.color = j.at("color").get<Color>();
j.at("type").get_to(fi.type);
j.at("is_support").get_to(fi.is_support);
fi.usage_type = (FilamentUsageType)j.at("usage_type").get<int>();
}
inline void to_json(json& j, const MachineFilamentInfo& mfi) {
j = json{
{"color", mfi.color},
{"type", mfi.type},
{"is_support", mfi.is_support},
{"usage_type", (int)mfi.usage_type},
{"extruder_id", mfi.extruder_id},
{"is_extended", mfi.is_extended}
};
}
inline void from_json(const json& j, MachineFilamentInfo& mfi) {
mfi.color = j.at("color").get<Color>();
j.at("type").get_to(mfi.type);
j.at("is_support").get_to(mfi.is_support);
mfi.usage_type = (FilamentUsageType)j.at("usage_type").get<int>();
j.at("extruder_id").get_to(mfi.extruder_id);
j.at("is_extended").get_to(mfi.is_extended);
}
} // namespace FilamentGroupUtils
namespace MultiNozzleUtils {
inline void to_json(json& j, const NozzleInfo& ni) {
j = json{
{"diameter", ni.diameter},
{"volume_type", (int)ni.volume_type},
{"extruder_id", ni.extruder_id},
{"group_id", ni.group_id}
};
}
inline void from_json(const json& j, NozzleInfo& ni) {
j.at("diameter").get_to(ni.diameter);
ni.volume_type = (NozzleVolumeType)j.at("volume_type").get<int>();
j.at("extruder_id").get_to(ni.extruder_id);
j.at("group_id").get_to(ni.group_id);
}
inline void to_json(json& j, const FilamentChangeTimeParams& p) {
j = json{
{"selector_load_time", p.selector_load_time},
{"selector_unload_time", p.selector_unload_time},
{"standard_load_time", p.standard_load_time},
{"standard_unload_time", p.standard_unload_time}
};
}
inline void from_json(const json& j, FilamentChangeTimeParams& p) {
j.at("selector_load_time").get_to(p.selector_load_time);
j.at("selector_unload_time").get_to(p.selector_unload_time);
j.at("standard_load_time").get_to(p.standard_load_time);
j.at("standard_unload_time").get_to(p.standard_unload_time);
}
} // namespace MultiNozzleUtils
// ============ Helper: set<int> as JSON array ============
namespace FGTestDetail {
inline json set_to_json(const std::set<int>& s) {
return json(std::vector<int>(s.begin(), s.end()));
}
inline std::set<int> json_to_set(const json& j) {
auto v = j.get<std::vector<int>>();
return std::set<int>(v.begin(), v.end());
}
inline json nvt_set_to_json(const std::set<NozzleVolumeType>& s) {
std::vector<int> v;
for (auto t : s) v.push_back((int)t);
return json(v);
}
inline std::set<NozzleVolumeType> json_to_nvt_set(const json& j) {
std::set<NozzleVolumeType> s;
for (auto& item : j) s.insert((NozzleVolumeType)item.get<int>());
return s;
}
} // namespace FGTestDetail
// ============ FilamentGroupContext::ModelInfo ============
inline void to_json(json& j, const FilamentGroupContext::ModelInfo& mi) {
using namespace FGTestDetail;
j["flush_matrix"] = mi.flush_matrix;
j["layer_filaments"] = mi.layer_filaments;
j["filament_info"] = json::array();
for (auto& fi : mi.filament_info)
j["filament_info"].push_back(fi);
j["filament_ids"] = mi.filament_ids;
j["unprintable_filaments"] = json::array();
for (auto& s : mi.unprintable_filaments)
j["unprintable_filaments"].push_back(set_to_json(s));
json uv = json::object();
for (auto& [fil, types] : mi.unprintable_volumes)
uv[std::to_string(fil)] = nvt_set_to_json(types);
j["unprintable_volumes"] = uv;
}
inline void from_json(const json& j, FilamentGroupContext::ModelInfo& mi) {
using namespace FGTestDetail;
j.at("flush_matrix").get_to(mi.flush_matrix);
j.at("layer_filaments").get_to(mi.layer_filaments);
mi.filament_info.clear();
for (auto& item : j.at("filament_info"))
mi.filament_info.push_back(item.get<FilamentGroupUtils::FilamentInfo>());
j.at("filament_ids").get_to(mi.filament_ids);
mi.unprintable_filaments.clear();
for (auto& item : j.at("unprintable_filaments"))
mi.unprintable_filaments.push_back(json_to_set(item));
mi.unprintable_volumes.clear();
if (j.contains("unprintable_volumes")) {
for (auto& [k, v] : j.at("unprintable_volumes").items())
mi.unprintable_volumes[std::stoi(k)] = json_to_nvt_set(v);
}
}
// ============ FilamentGroupContext::GroupInfo ============
inline void to_json(json& j, const FilamentGroupContext::GroupInfo& gi) {
j = json{
{"total_filament_num", gi.total_filament_num},
{"max_gap_threshold", gi.max_gap_threshold},
{"mode", (int)gi.mode},
{"strategy", (int)gi.strategy},
{"ignore_ext_filament", gi.ignore_ext_filament},
{"has_filament_switcher", gi.has_filament_switcher},
{"filament_volume_map", gi.filament_volume_map}
};
}
inline void from_json(const json& j, FilamentGroupContext::GroupInfo& gi) {
j.at("total_filament_num").get_to(gi.total_filament_num);
j.at("max_gap_threshold").get_to(gi.max_gap_threshold);
gi.mode = (FGMode)j.at("mode").get<int>();
gi.strategy = (FGStrategy)j.at("strategy").get<int>();
j.at("ignore_ext_filament").get_to(gi.ignore_ext_filament);
j.at("has_filament_switcher").get_to(gi.has_filament_switcher);
j.at("filament_volume_map").get_to(gi.filament_volume_map);
}
// ============ FilamentGroupContext::MachineInfo ============
inline void to_json(json& j, const FilamentGroupContext::MachineInfo& mi) {
j["max_group_size"] = mi.max_group_size;
j["machine_filament_info"] = json::array();
for (auto& vec : mi.machine_filament_info) {
json arr = json::array();
for (auto& mfi : vec) arr.push_back(mfi);
j["machine_filament_info"].push_back(arr);
}
j["prefer_non_model_filament"] = mi.prefer_non_model_filament;
j["master_extruder_id"] = mi.master_extruder_id;
}
inline void from_json(const json& j, FilamentGroupContext::MachineInfo& mi) {
j.at("max_group_size").get_to(mi.max_group_size);
mi.machine_filament_info.clear();
for (auto& arr : j.at("machine_filament_info")) {
std::vector<FilamentGroupUtils::MachineFilamentInfo> vec;
for (auto& item : arr)
vec.push_back(item.get<FilamentGroupUtils::MachineFilamentInfo>());
mi.machine_filament_info.push_back(std::move(vec));
}
j.at("prefer_non_model_filament").get_to(mi.prefer_non_model_filament);
j.at("master_extruder_id").get_to(mi.master_extruder_id);
}
// ============ FilamentGroupContext::SpeedInfo ============
inline void to_json(json& j, const FilamentGroupContext::SpeedInfo& si) {
json fpt = json::object();
for (auto& [fil, inner] : si.filament_print_time) {
json inner_j = json::object();
for (auto& [layer, time] : inner)
inner_j[std::to_string(layer)] = time;
fpt[std::to_string(fil)] = inner_j;
}
j["filament_print_time"] = fpt;
j["extruder_change_time"] = si.extruder_change_time;
j["filament_change_time"] = si.filament_change_time;
j["group_with_time"] = si.group_with_time;
j["change_time_params"] = si.change_time_params;
j["ams_preload_enabled"] = si.ams_preload_enabled;
}
inline void from_json(const json& j, FilamentGroupContext::SpeedInfo& si) {
si.filament_print_time.clear();
if (j.contains("filament_print_time")) {
for (auto& [k, v] : j.at("filament_print_time").items()) {
int fil = std::stoi(k);
for (auto& [k2, v2] : v.items())
si.filament_print_time[fil][std::stoi(k2)] = v2.get<double>();
}
}
j.at("extruder_change_time").get_to(si.extruder_change_time);
j.at("filament_change_time").get_to(si.filament_change_time);
j.at("group_with_time").get_to(si.group_with_time);
si.change_time_params = j.at("change_time_params").get<MultiNozzleUtils::FilamentChangeTimeParams>();
j.at("ams_preload_enabled").get_to(si.ams_preload_enabled);
}
// ============ FilamentGroupContext::NozzleInfo ============
inline void to_json(json& j, const FilamentGroupContext::NozzleInfo& ni) {
json enl = json::object();
for (auto& [ext, nozzles] : ni.extruder_nozzle_list)
enl[std::to_string(ext)] = nozzles;
j["extruder_nozzle_list"] = enl;
j["nozzle_list"] = json::array();
for (auto& n : ni.nozzle_list)
j["nozzle_list"].push_back(n);
json ns = json::object();
for (auto& [noz, fil] : ni.nozzle_status)
ns[std::to_string(noz)] = fil;
j["nozzle_status"] = ns;
}
inline void from_json(const json& j, FilamentGroupContext::NozzleInfo& ni) {
ni.extruder_nozzle_list.clear();
for (auto& [k, v] : j.at("extruder_nozzle_list").items())
ni.extruder_nozzle_list[std::stoi(k)] = v.get<std::vector<int>>();
ni.nozzle_list.clear();
for (auto& item : j.at("nozzle_list"))
ni.nozzle_list.push_back(item.get<MultiNozzleUtils::NozzleInfo>());
ni.nozzle_status.clear();
if (j.contains("nozzle_status")) {
for (auto& [k, v] : j.at("nozzle_status").items())
ni.nozzle_status[std::stoi(k)] = v.get<int>();
}
}
// ============ Full FilamentGroupContext ============
inline void to_json(json& j, const FilamentGroupContext& ctx) {
json mi, gi, mai, si, ni;
to_json(mi, ctx.model_info);
to_json(gi, ctx.group_info);
to_json(mai, ctx.machine_info);
to_json(si, ctx.speed_info);
to_json(ni, ctx.nozzle_info);
j["model_info"] = mi;
j["group_info"] = gi;
j["machine_info"] = mai;
j["speed_info"] = si;
j["nozzle_info"] = ni;
}
inline void from_json(const json& j, FilamentGroupContext& ctx) {
from_json(j.at("model_info"), ctx.model_info);
from_json(j.at("group_info"), ctx.group_info);
from_json(j.at("machine_info"), ctx.machine_info);
from_json(j.at("speed_info"), ctx.speed_info);
from_json(j.at("nozzle_info"), ctx.nozzle_info);
}
} // namespace Slic3r
// ============ Test-specific types in FGTest namespace ============
namespace Slic3r {
namespace FGTest {
struct TestMetadata {
std::string id;
std::string config_type;
int seed = 0;
};
inline void to_json(json& j, const TestMetadata& m) {
j = json{{"id", m.id}, {"config_type", m.config_type}, {"seed", m.seed}};
}
inline void from_json(const json& j, TestMetadata& m) {
j.at("id").get_to(m.id);
j.at("config_type").get_to(m.config_type);
j.at("seed").get_to(m.seed);
}
struct TestResult {
std::vector<int> filament_map;
int flush_cost = 0;
double elapsed_ms = 0;
bool constraints_ok = true;
std::vector<std::string> violations;
};
inline void to_json(json& j, const TestResult& r) {
j = json{
{"filament_map", r.filament_map},
{"flush_cost", r.flush_cost},
{"elapsed_ms", r.elapsed_ms},
{"constraints_ok", r.constraints_ok},
{"violations", r.violations}
};
}
inline void from_json(const json& j, TestResult& r) {
j.at("filament_map").get_to(r.filament_map);
j.at("flush_cost").get_to(r.flush_cost);
j.at("elapsed_ms").get_to(r.elapsed_ms);
j.at("constraints_ok").get_to(r.constraints_ok);
if (j.contains("violations"))
j.at("violations").get_to(r.violations);
}
// ============ Base Result (golden baseline stored in input file) ============
struct BaseResult {
double full_score = 0;
int flush_cost = 0;
bool constraints_ok = true;
};
inline void to_json(json& j, const BaseResult& g) {
j = json{
{"full_score", g.full_score},
{"flush_cost", g.flush_cost},
{"constraints_ok", g.constraints_ok}
};
}
inline void from_json(const json& j, BaseResult& g) {
j.at("full_score").get_to(g.full_score);
j.at("flush_cost").get_to(g.flush_cost);
j.at("constraints_ok").get_to(g.constraints_ok);
}
// ============ File I/O ============
struct TestCase {
TestMetadata metadata;
FilamentGroupContext context;
std::optional<BaseResult> base_result;
};
inline TestCase load_test_case(const std::string& path) {
std::ifstream f(path);
json j = json::parse(f);
TestCase tc;
tc.metadata = j.at("metadata").get<TestMetadata>();
Slic3r::from_json(j.at("context"), tc.context);
if (j.contains("base_result"))
tc.base_result = j.at("base_result").get<BaseResult>();
return tc;
}
inline void save_test_case(const std::string& path, const TestCase& tc) {
json j;
j["metadata"] = tc.metadata;
json ctx_j;
Slic3r::to_json(ctx_j, tc.context);
j["context"] = ctx_j;
if (tc.base_result)
j["base_result"] = *tc.base_result;
std::ofstream f(path);
f << j.dump(-1);
}
inline void save_result(const std::string& case_path, const TestResult& result) {
std::string result_path = case_path;
auto pos = result_path.rfind(".json");
if (pos != std::string::npos)
result_path = result_path.substr(0, pos) + ".result.json";
else
result_path += ".result.json";
json j = result;
std::ofstream f(result_path);
f << j.dump(2);
}
inline TestResult load_result(const std::string& result_path) {
std::ifstream f(result_path);
json j = json::parse(f);
return j.get<TestResult>();
}
} // namespace FGTest
} // namespace Slic3r
#endif // FG_TEST_SERIALIZATION_HPP

View File

@@ -0,0 +1,406 @@
#ifndef FG_TEST_UTILS_HPP
#define FG_TEST_UTILS_HPP
#include "fg_test_serialization.hpp"
#include <random>
#include <algorithm>
#include <cassert>
namespace Slic3r {
namespace FGTest {
class TestRng {
public:
explicit TestRng(int seed) : m_gen(seed) {}
int rand_int(int lo, int hi) {
std::uniform_int_distribution<int> dist(lo, hi);
return dist(m_gen);
}
float rand_float(float lo, float hi) {
std::uniform_real_distribution<float> dist(lo, hi);
return dist(m_gen);
}
double rand_double(double lo, double hi) {
std::uniform_real_distribution<double> dist(lo, hi);
return dist(m_gen);
}
bool rand_bool(double prob = 0.5) {
return rand_double(0, 1) < prob;
}
template<typename T>
void shuffle(std::vector<T>& v) {
std::shuffle(v.begin(), v.end(), m_gen);
}
private:
std::mt19937 m_gen;
};
// Generate a flush matrix for one extruder: [filament_count x filament_count]
inline std::vector<std::vector<float>> generate_flush_matrix(int filament_count, TestRng& rng) {
std::vector<std::vector<float>> matrix(filament_count, std::vector<float>(filament_count, 0.0f));
for (int i = 0; i < filament_count; ++i) {
for (int j = 0; j < filament_count; ++j) {
if (i == j)
matrix[i][j] = 0.0f;
else
matrix[i][j] = rng.rand_float(10.0f, 600.0f);
}
}
return matrix;
}
// Generate layer_filaments with interval characteristics
inline std::vector<std::vector<unsigned int>> generate_layer_filaments_interval(
int num_layers, int total_filaments, const std::vector<unsigned int>& used_filaments, TestRng& rng)
{
std::vector<std::vector<unsigned int>> layers;
layers.reserve(num_layers);
int n_used = (int)used_filaments.size();
int fils_per_layer_min = std::min(2, n_used);
int fils_per_layer_max = std::min(n_used, std::max(2, n_used / 2 + 1));
// First layer: random subset
int first_count = rng.rand_int(fils_per_layer_min, fils_per_layer_max);
std::vector<unsigned int> pool = used_filaments;
rng.shuffle(pool);
std::vector<unsigned int> current(pool.begin(), pool.begin() + first_count);
std::sort(current.begin(), current.end());
layers.push_back(current);
for (int layer = 1; layer < num_layers; ++layer) {
// 10% chance: completely random new set (object boundary)
if (rng.rand_bool(0.10)) {
int count = rng.rand_int(fils_per_layer_min, fils_per_layer_max);
pool = used_filaments;
rng.shuffle(pool);
current.assign(pool.begin(), pool.begin() + count);
} else {
// Markov: keep each filament with 70% prob, maybe add new ones
std::vector<unsigned int> next;
for (auto f : current) {
if (rng.rand_bool(0.70))
next.push_back(f);
}
// Maybe add a filament not in current
if (rng.rand_bool(0.30) || next.empty()) {
std::vector<unsigned int> candidates;
std::set<unsigned int> cur_set(next.begin(), next.end());
for (auto f : used_filaments) {
if (!cur_set.count(f))
candidates.push_back(f);
}
if (!candidates.empty()) {
next.push_back(candidates[rng.rand_int(0, (int)candidates.size() - 1)]);
}
}
if (next.empty())
next.push_back(used_filaments[rng.rand_int(0, n_used - 1)]);
current = next;
}
std::sort(current.begin(), current.end());
current.erase(std::unique(current.begin(), current.end()), current.end());
layers.push_back(current);
}
return layers;
}
// Generate layer_filaments where every layer is different (stress/edge)
inline std::vector<std::vector<unsigned int>> generate_layer_filaments_chaotic(
int num_layers, int total_filaments, const std::vector<unsigned int>& used_filaments, TestRng& rng)
{
std::vector<std::vector<unsigned int>> layers;
int n_used = (int)used_filaments.size();
int fils_per_layer_min = std::min(2, n_used);
int fils_per_layer_max = n_used;
for (int layer = 0; layer < num_layers; ++layer) {
int count = rng.rand_int(fils_per_layer_min, fils_per_layer_max);
std::vector<unsigned int> pool = used_filaments;
rng.shuffle(pool);
std::vector<unsigned int> current(pool.begin(), pool.begin() + count);
std::sort(current.begin(), current.end());
layers.push_back(current);
}
return layers;
}
// Generate layer_filaments where all layers are the same (edge)
inline std::vector<std::vector<unsigned int>> generate_layer_filaments_uniform(
int num_layers, const std::vector<unsigned int>& used_filaments)
{
return std::vector<std::vector<unsigned int>>(num_layers, used_filaments);
}
// Generate filament info
inline std::vector<FilamentGroupUtils::FilamentInfo> generate_filament_info(int count, TestRng& rng) {
static const char* types[] = {"PLA", "ABS", "PETG", "TPU", "PA", "PLA-S"};
std::vector<FilamentGroupUtils::FilamentInfo> infos;
for (int i = 0; i < count; ++i) {
FilamentGroupUtils::FilamentInfo fi;
fi.color = FilamentGroupUtils::Color(
(unsigned char)rng.rand_int(0, 255),
(unsigned char)rng.rand_int(0, 255),
(unsigned char)rng.rand_int(0, 255));
fi.type = types[rng.rand_int(0, 5)];
fi.is_support = (fi.type == "PLA-S");
fi.usage_type = fi.is_support ? FilamentUsageType::SupportOnly : FilamentUsageType::ModelOnly;
infos.push_back(fi);
}
return infos;
}
// Generate machine filament info (per extruder)
inline std::vector<std::vector<FilamentGroupUtils::MachineFilamentInfo>> generate_machine_filament_info(
int num_extruders, int filaments_per_extruder, TestRng& rng)
{
std::vector<std::vector<FilamentGroupUtils::MachineFilamentInfo>> result;
for (int ext = 0; ext < num_extruders; ++ext) {
std::vector<FilamentGroupUtils::MachineFilamentInfo> vec;
for (int i = 0; i < filaments_per_extruder; ++i) {
FilamentGroupUtils::MachineFilamentInfo mfi;
mfi.color = FilamentGroupUtils::Color(
(unsigned char)rng.rand_int(0, 255),
(unsigned char)rng.rand_int(0, 255),
(unsigned char)rng.rand_int(0, 255));
mfi.type = "PLA";
mfi.is_support = false;
mfi.usage_type = FilamentUsageType::ModelOnly;
mfi.extruder_id = ext;
mfi.is_extended = (i >= 4);
vec.push_back(mfi);
}
result.push_back(vec);
}
return result;
}
// ============ Machine Config Builders ============
// Config A: 2 extruders, 1 nozzle each
inline void build_config_a(FilamentGroupContext& ctx, int num_filaments, TestRng& rng) {
auto& ni = ctx.nozzle_info;
ni.nozzle_list.clear();
ni.nozzle_list.push_back({"0.4", NozzleVolumeType::nvtStandard, 0, 0});
ni.nozzle_list.push_back({"0.4", NozzleVolumeType::nvtStandard, 1, 1});
ni.extruder_nozzle_list = {{0, {0}}, {1, {1}}};
ctx.machine_info.max_group_size = {num_filaments / 2 + 1, num_filaments / 2 + 1};
ctx.machine_info.prefer_non_model_filament = {false, true};
ctx.machine_info.master_extruder_id = 0;
ctx.machine_info.machine_filament_info = generate_machine_filament_info(2, 4, rng);
ctx.group_info.filament_volume_map.assign(num_filaments, (int)NozzleVolumeType::nvtHybrid);
ctx.model_info.unprintable_filaments.resize(2);
ctx.model_info.flush_matrix.resize(2);
for (int ext = 0; ext < 2; ++ext)
ctx.model_info.flush_matrix[ext] = generate_flush_matrix(num_filaments, rng);
}
// Config B: 2 extruders, ext0 has 1 nozzle, ext1 has K nozzles (K in [2,6])
inline void build_config_b(FilamentGroupContext& ctx, int num_filaments, int k_nozzles, TestRng& rng) {
auto& ni = ctx.nozzle_info;
ni.nozzle_list.clear();
ni.nozzle_list.push_back({"0.4", NozzleVolumeType::nvtStandard, 0, 0});
static const NozzleVolumeType vol_types[] = {
NozzleVolumeType::nvtStandard, NozzleVolumeType::nvtHighFlow, NozzleVolumeType::nvtTPUHighFlow};
std::vector<int> ext1_nozzles;
for (int i = 0; i < k_nozzles; ++i) {
int group_id = i + 1;
NozzleVolumeType vt = vol_types[rng.rand_int(0, 2)];
ni.nozzle_list.push_back({"0.4", vt, 1, group_id});
ext1_nozzles.push_back(group_id);
}
ni.extruder_nozzle_list = {{0, {0}}, {1, ext1_nozzles}};
int ext0_max = std::max(4, num_filaments / 2 + 1);
int ext1_max = std::max(k_nozzles * 2, num_filaments - ext0_max + 1);
ctx.machine_info.max_group_size = {ext0_max, ext1_max};
ctx.machine_info.prefer_non_model_filament = {false, false};
ctx.machine_info.master_extruder_id = 0;
ctx.machine_info.machine_filament_info = generate_machine_filament_info(2, 4, rng);
ctx.group_info.filament_volume_map.assign(num_filaments, (int)NozzleVolumeType::nvtHybrid);
ctx.model_info.unprintable_filaments.resize(2);
ctx.model_info.flush_matrix.resize(2);
for (int ext = 0; ext < 2; ++ext)
ctx.model_info.flush_matrix[ext] = generate_flush_matrix(num_filaments, rng);
}
// Config C: 1 extruder, K nozzles (K in [3,9])
inline void build_config_c(FilamentGroupContext& ctx, int num_filaments, int k_nozzles, TestRng& rng) {
auto& ni = ctx.nozzle_info;
ni.nozzle_list.clear();
static const NozzleVolumeType vol_types[] = {
NozzleVolumeType::nvtStandard, NozzleVolumeType::nvtHighFlow,
NozzleVolumeType::nvtHybrid, NozzleVolumeType::nvtTPUHighFlow};
std::vector<int> nozzle_ids;
for (int i = 0; i < k_nozzles; ++i) {
NozzleVolumeType vt = vol_types[i % 4];
ni.nozzle_list.push_back({"0.4", vt, 0, i});
nozzle_ids.push_back(i);
}
ni.extruder_nozzle_list = {{0, nozzle_ids}};
ctx.machine_info.max_group_size = {num_filaments};
ctx.machine_info.prefer_non_model_filament = {false};
ctx.machine_info.master_extruder_id = 0;
ctx.machine_info.machine_filament_info = generate_machine_filament_info(1, 4, rng);
ctx.group_info.filament_volume_map.assign(num_filaments, (int)NozzleVolumeType::nvtHybrid);
ctx.model_info.unprintable_filaments.resize(1);
ctx.model_info.flush_matrix.resize(1);
ctx.model_info.flush_matrix[0] = generate_flush_matrix(num_filaments, rng);
}
// ============ Constraint Injection ============
// Add unprintable_filaments constraints (some filaments forbidden on some extruders)
inline void inject_unprintable_constraints(FilamentGroupContext& ctx,
const std::vector<unsigned int>& used_filaments,
TestRng& rng, int num_constraints) {
int num_ext = (int)ctx.model_info.unprintable_filaments.size();
for (int i = 0; i < num_constraints && !used_filaments.empty(); ++i) {
int fil = used_filaments[rng.rand_int(0, (int)used_filaments.size() - 1)];
int ext = rng.rand_int(0, num_ext - 1);
ctx.model_info.unprintable_filaments[ext].insert(fil);
}
// Ensure no filament is banned from ALL extruders
for (auto fil : used_filaments) {
bool can_print_somewhere = false;
for (int ext = 0; ext < num_ext; ++ext) {
if (!ctx.model_info.unprintable_filaments[ext].count(fil)) {
can_print_somewhere = true;
break;
}
}
if (!can_print_somewhere) {
int ext_to_allow = rng.rand_int(0, num_ext - 1);
ctx.model_info.unprintable_filaments[ext_to_allow].erase(fil);
}
}
}
// Add unprintable_volumes constraints
inline void inject_volume_constraints(FilamentGroupContext& ctx,
const std::vector<unsigned int>& used_filaments,
TestRng& rng, int num_constraints) {
static const NozzleVolumeType vols[] = {
NozzleVolumeType::nvtStandard, NozzleVolumeType::nvtHighFlow,
NozzleVolumeType::nvtTPUHighFlow};
for (int i = 0; i < num_constraints && !used_filaments.empty(); ++i) {
int fil = used_filaments[rng.rand_int(0, (int)used_filaments.size() - 1)];
NozzleVolumeType vt = vols[rng.rand_int(0, 2)];
ctx.model_info.unprintable_volumes[fil].insert(vt);
}
// Ensure no filament is banned from ALL nozzle volume types present
for (auto fil : used_filaments) {
if (!ctx.model_info.unprintable_volumes.count(fil))
continue;
auto& banned = ctx.model_info.unprintable_volumes[fil];
bool can_go_somewhere = false;
for (auto& noz : ctx.nozzle_info.nozzle_list) {
if (!banned.count(noz.volume_type)) {
can_go_somewhere = true;
break;
}
}
if (!can_go_somewhere && !banned.empty()) {
// Remove one random ban
auto it = banned.begin();
std::advance(it, rng.rand_int(0, (int)banned.size() - 1));
banned.erase(it);
}
}
}
// ============ Full Case Builder ============
inline TestCase build_test_case(const std::string& id, const std::string& config_type,
int seed, int num_filaments, int num_layers,
bool chaotic_layers, bool with_constraints,
FGMode mode, FGStrategy strategy, bool group_with_time) {
TestRng rng(seed);
TestCase tc;
tc.metadata.id = id;
tc.metadata.config_type = config_type;
tc.metadata.seed = seed;
auto& ctx = tc.context;
// Used filaments: 0-based indices
std::vector<unsigned int> used_filaments;
for (int i = 0; i < num_filaments; ++i)
used_filaments.push_back((unsigned int)i);
// Build machine config
if (config_type == "A") {
build_config_a(ctx, num_filaments, rng);
} else if (config_type == "B") {
int k = rng.rand_int(2, 6);
build_config_b(ctx, num_filaments, k, rng);
} else {
int k = rng.rand_int(3, 9);
build_config_c(ctx, num_filaments, k, rng);
}
// Layer filaments
if (chaotic_layers)
ctx.model_info.layer_filaments = generate_layer_filaments_chaotic(num_layers, num_filaments, used_filaments, rng);
else
ctx.model_info.layer_filaments = generate_layer_filaments_interval(num_layers, num_filaments, used_filaments, rng);
// Filament info
ctx.model_info.filament_info = generate_filament_info(num_filaments, rng);
ctx.model_info.filament_ids.resize(num_filaments);
for (int i = 0; i < num_filaments; ++i)
ctx.model_info.filament_ids[i] = "GFL_" + std::to_string(i);
// Group info
ctx.group_info.total_filament_num = num_filaments;
ctx.group_info.max_gap_threshold = 0.01;
ctx.group_info.mode = mode;
ctx.group_info.strategy = strategy;
ctx.group_info.ignore_ext_filament = false;
ctx.group_info.has_filament_switcher = false;
// Speed info
ctx.speed_info.extruder_change_time = 5.0;
ctx.speed_info.filament_change_time = 2.0;
ctx.speed_info.group_with_time = group_with_time;
ctx.speed_info.change_time_params = {1.0f, 1.0f, 3.0f, 2.0f};
int num_ext = (config_type == "C") ? 1 : 2;
ctx.speed_info.ams_preload_enabled.assign(num_ext, true);
// Constraints
if (with_constraints) {
inject_unprintable_constraints(ctx, used_filaments, rng, rng.rand_int(1, num_filaments / 2));
if (config_type != "A")
inject_volume_constraints(ctx, used_filaments, rng, rng.rand_int(1, 3));
}
// Nozzle status (initially empty)
ctx.nozzle_info.nozzle_status.clear();
return tc;
}
} // namespace FGTest
} // namespace Slic3r
#endif // FG_TEST_UTILS_HPP

View File

@@ -0,0 +1,330 @@
// H2C/A2L FilamentGroup golden regression harness.
//
// Notes:
// * Orca: links Catch2::Catch2WithMain and uses the v3 convenience include <catch2/catch_all.hpp>.
// * All three golden families (config_a one-nozzle-per-extruder, config_b/config_c nozzle-centric)
// are evaluated against the goldens. The nozzle-centric FilamentGroup engine and solver layer run
// the same algorithm the goldens were generated with, scored via the nozzle-aware reorder
// (fg_test_evaluator.hpp) at a 3% one-directional tolerance.
// * The hidden [update-golden] utility is intentionally omitted: the goldens are the reference
// and must not be rewritten from Orca output.
#include <catch2/catch_all.hpp>
#include "fg_test_serialization.hpp"
#include "fg_test_evaluator.hpp"
#include "fg_test_utils.hpp"
#include <filesystem>
#include <iostream>
#include <fstream>
#include <string>
#include <vector>
#include <algorithm>
#include <numeric>
namespace fs = std::filesystem;
using namespace Slic3r;
using namespace Slic3r::FGTest;
// ============ Helpers ============
static std::vector<std::string> collect_test_files(const std::string& dir) {
std::vector<std::string> files;
if (!fs::exists(dir)) return files;
for (auto& entry : fs::recursive_directory_iterator(dir)) {
if (entry.path().extension() == ".json" &&
entry.path().string().find(".result.") == std::string::npos) {
files.push_back(entry.path().string());
}
}
std::sort(files.begin(), files.end());
return files;
}
static std::vector<std::string> get_golden_files() {
static std::vector<std::string> files = collect_test_files(FG_TEST_GOLDEN_DIR);
return files;
}
static bool is_constraint_feasible(const FilamentGroupContext& ctx,
const std::vector<unsigned int>& used_filaments) {
int total_capacity = 0;
for (auto sz : ctx.machine_info.max_group_size)
total_capacity += sz;
if (total_capacity < (int)used_filaments.size())
return false;
// Check that every filament has at least one valid nozzle
for (auto fil : used_filaments) {
bool has_valid_nozzle = false;
for (size_t nid = 0; nid < ctx.nozzle_info.nozzle_list.size(); ++nid) {
auto& nozzle = ctx.nozzle_info.nozzle_list[nid];
// Check unprintable_filaments
if (nozzle.extruder_id >= 0 && nozzle.extruder_id < (int)ctx.model_info.unprintable_filaments.size()) {
if (ctx.model_info.unprintable_filaments[nozzle.extruder_id].count(fil))
continue;
}
// Check unprintable_volumes
if (ctx.model_info.unprintable_volumes.count(fil)) {
if (ctx.model_info.unprintable_volumes.at(fil).count(nozzle.volume_type))
continue;
}
has_valid_nozzle = true;
break;
}
if (!has_valid_nozzle)
return false;
}
return true;
}
// ============ Property Check Specs ============
struct PropertySpec {
std::string id;
std::string config;
int seed;
int num_filaments;
int num_layers;
bool chaotic;
bool with_constraints;
FGMode mode;
FGStrategy strategy;
bool group_with_time;
};
static std::vector<PropertySpec> build_property_specs() {
std::vector<PropertySpec> specs;
// Config A: 20 cases
for (int i = 0; i < 6; ++i) {
int seed = 90000 + i;
TestRng rng(seed);
specs.push_back({"prop_a_basic_" + std::to_string(i), "A", seed,
rng.rand_int(2, 6), rng.rand_int(100, 400),
false, false, FGMode::FlushMode, FGStrategy::BestCost, false});
}
for (int i = 0; i < 4; ++i) {
int seed = 90100 + i;
TestRng rng(seed);
specs.push_back({"prop_a_stress_" + std::to_string(i), "A", seed,
rng.rand_int(7, 10), rng.rand_int(500, 1000),
false, false, FGMode::FlushMode, FGStrategy::BestCost, false});
}
for (int i = 0; i < 4; ++i) {
int seed = 90200 + i;
TestRng rng(seed);
specs.push_back({"prop_a_constraint_" + std::to_string(i), "A", seed,
rng.rand_int(3, 8), rng.rand_int(100, 400),
false, true, FGMode::FlushMode, FGStrategy::BestCost, false});
}
for (int i = 0; i < 3; ++i) {
int seed = 90300 + i;
TestRng rng(seed);
specs.push_back({"prop_a_edge_" + std::to_string(i), "A", seed,
rng.rand_int(2, 3), rng.rand_int(10, 50),
true, false, FGMode::FlushMode, FGStrategy::BestCost, false});
}
specs.push_back({"prop_a_mode_match", "A", 90400,
5, 200, false, false, FGMode::MatchMode, FGStrategy::BestCost, false});
specs.push_back({"prop_a_mode_bestfit", "A", 90401,
5, 200, false, false, FGMode::FlushMode, FGStrategy::BestFit, false});
specs.push_back({"prop_a_mode_time", "A", 90402,
5, 200, false, false, FGMode::FlushMode, FGStrategy::BestCost, true});
// Config B: 25 cases
for (int i = 0; i < 6; ++i) {
int seed = 91000 + i;
TestRng rng(seed);
specs.push_back({"prop_b_basic_" + std::to_string(i), "B", seed,
rng.rand_int(3, 8), rng.rand_int(100, 400),
false, false, FGMode::FlushMode, FGStrategy::BestCost, false});
}
for (int i = 0; i < 6; ++i) {
int seed = 91100 + i;
TestRng rng(seed);
specs.push_back({"prop_b_stress_" + std::to_string(i), "B", seed,
rng.rand_int(9, 12), rng.rand_int(500, 1000),
false, false, FGMode::FlushMode, FGStrategy::BestCost, false});
}
for (int i = 0; i < 7; ++i) {
int seed = 91200 + i;
TestRng rng(seed);
specs.push_back({"prop_b_constraint_" + std::to_string(i), "B", seed,
rng.rand_int(4, 10), rng.rand_int(100, 400),
false, true, FGMode::FlushMode, FGStrategy::BestCost, false});
}
for (int i = 0; i < 3; ++i) {
int seed = 91300 + i;
TestRng rng(seed);
specs.push_back({"prop_b_edge_" + std::to_string(i), "B", seed,
rng.rand_int(2, 4), rng.rand_int(10, 50),
true, false, FGMode::FlushMode, FGStrategy::BestCost, false});
}
specs.push_back({"prop_b_mode_match", "B", 91400,
6, 200, false, false, FGMode::MatchMode, FGStrategy::BestCost, false});
specs.push_back({"prop_b_mode_bestfit", "B", 91401,
6, 200, false, false, FGMode::FlushMode, FGStrategy::BestFit, false});
specs.push_back({"prop_b_mode_time", "B", 91402,
6, 200, false, false, FGMode::FlushMode, FGStrategy::BestCost, true});
// Config C: 15 cases
for (int i = 0; i < 5; ++i) {
int seed = 92000 + i;
TestRng rng(seed);
specs.push_back({"prop_c_basic_" + std::to_string(i), "C", seed,
rng.rand_int(3, 9), rng.rand_int(100, 400),
false, false, FGMode::FlushMode, FGStrategy::BestCost, false});
}
for (int i = 0; i < 3; ++i) {
int seed = 92100 + i;
TestRng rng(seed);
specs.push_back({"prop_c_stress_" + std::to_string(i), "C", seed,
rng.rand_int(10, 15), rng.rand_int(500, 1000),
false, false, FGMode::FlushMode, FGStrategy::BestCost, false});
}
for (int i = 0; i < 3; ++i) {
int seed = 92200 + i;
TestRng rng(seed);
specs.push_back({"prop_c_constraint_" + std::to_string(i), "C", seed,
rng.rand_int(4, 9), rng.rand_int(100, 400),
false, true, FGMode::FlushMode, FGStrategy::BestCost, false});
}
for (int i = 0; i < 2; ++i) {
int seed = 92300 + i;
TestRng rng(seed);
specs.push_back({"prop_c_edge_" + std::to_string(i), "C", seed,
rng.rand_int(2, 4), rng.rand_int(10, 50),
true, false, FGMode::FlushMode, FGStrategy::BestCost, false});
}
specs.push_back({"prop_c_mode_match", "C", 92400,
6, 200, false, false, FGMode::MatchMode, FGStrategy::BestCost, false});
specs.push_back({"prop_c_mode_bestfit", "C", 92401,
6, 200, false, false, FGMode::FlushMode, FGStrategy::BestFit, false});
return specs;
}
static std::vector<PropertySpec>& get_property_specs() {
static std::vector<PropertySpec> specs = build_property_specs();
return specs;
}
// Under the default wall clock the result depends on how fast the machine is (see ClusteringBudget),
// so the goldens are graded under a fixed budget instead. Two restarts is the fewest that reaches
// parity with the reference on every golden, stress_79 being the last to get there. Four leaves
// margin, since the search follows a different path on each standard library (see below).
static constexpr ClusteringBudget FIXED_SEARCH_BUDGET{
/*timeout_ms*/ 0, // no wall clock
/*max_restarts*/ 4};
// ============ Layer 1: Golden Regression (all configs) ============
// Graded against the BambuStudio golden the harness was ported from, one-directional at 3%.
//
// The tolerance is a parity allowance, and it also covers a small spread across standard libraries.
// The k-medoids search seeds each restart with std::shuffle, whose algorithm the C++ standard leaves
// unspecified, so libstdc++, libc++ and the MSVC STL permute the same seed differently, start from
// different medoids, and settle on slightly different groupings, about 3e-4 apart on either side of
// the reference, and only on the goldens heavy enough to reach the k-medoids search.
TEST_CASE("FilamentGroup golden regression", "[filament_group][golden]") {
auto files = get_golden_files();
if (files.empty()) {
WARN("No golden files found in " FG_TEST_GOLDEN_DIR);
REQUIRE(!files.empty());
return;
}
auto file_path = GENERATE_REF(from_range(files));
DYNAMIC_SECTION("Golden: " << fs::path(file_path).stem().string()) {
auto tc = load_test_case(file_path);
REQUIRE(tc.base_result.has_value());
auto result = run_and_evaluate(tc.context, FIXED_SEARCH_BUDGET);
auto eval = full_evaluate_map(tc.context, result.filament_map);
auto& base = *tc.base_result;
INFO("Case: " << tc.metadata.id);
INFO("Golden score: " << base.full_score);
INFO("Actual score: " << eval.full_score);
INFO("Flush cost: " << eval.flush_cost << " (golden " << base.flush_cost << ")");
INFO("Elapsed: " << result.elapsed_ms << " ms");
int tolerance = std::max(50, (int)(base.full_score * 0.03));
REQUIRE(result.constraints_ok);
REQUIRE(eval.full_score <= base.full_score + tolerance);
// A slower search still scores the same above, since it searches just as far, but in slicing
// it would mean fewer restarts fit in the wall clock and so worse groupings. Loose on
// purpose, so it never becomes a proxy for how loaded the runner is.
const double throughput_ceiling_ms = 10.0 * ClusteringBudget{}.timeout_ms;
REQUIRE(result.elapsed_ms < throughput_ceiling_ms);
}
}
// Covers the path real slicing takes, under the default wall clock. The score there depends on the
// runner rather than on the code (see FIXED_SEARCH_BUDGET), so the only things worth asserting are
// that the grouping comes back valid and that the search terminates.
TEST_CASE("FilamentGroup returns a valid grouping under the default budget", "[filament_group][budget]") {
auto files = get_golden_files();
REQUIRE(!files.empty());
auto file_path = GENERATE_REF(from_range(files));
DYNAMIC_SECTION("Golden: " << fs::path(file_path).stem().string()) {
auto tc = load_test_case(file_path);
auto result = run_and_evaluate(tc.context); // the default budget, as real slicing runs it
INFO("Case: " << tc.metadata.id);
INFO("Elapsed: " << result.elapsed_ms << " ms");
REQUIRE(result.constraints_ok);
// A hang guard. The clock is only checked between swaps, so a sweep can overshoot.
REQUIRE(result.elapsed_ms < 40000.0);
}
}
// ============ Layer 2: Property Checks (all configs) ============
TEST_CASE("FilamentGroup property checks", "[filament_group][property]") {
auto& specs = get_property_specs();
auto spec = GENERATE_REF(from_range(specs));
DYNAMIC_SECTION("Property: " << spec.id) {
auto tc = build_test_case(spec.id, spec.config, spec.seed,
spec.num_filaments, spec.num_layers,
spec.chaotic, spec.with_constraints,
spec.mode, spec.strategy, spec.group_with_time);
auto result = run_and_evaluate(tc.context);
INFO("Case: " << spec.id);
INFO("Config: " << spec.config);
INFO("Flush cost: " << result.flush_cost);
INFO("Elapsed: " << result.elapsed_ms << " ms");
// RelWithDebInfo runaway guard; the Release-calibrated 10 s limit is raised for the slower
// build (config_b/config_c cases evaluate the full per-layer nozzle-aware reorder for every
// candidate grouping; this is a guard against hangs, not a micro-perf gate).
REQUIRE(result.elapsed_ms < 40000.0);
REQUIRE(result.flush_cost >= 0);
auto used_filaments = collect_sorted_used_filaments(tc.context.model_info.layer_filaments);
if (is_constraint_feasible(tc.context, used_filaments)) {
if (!result.constraints_ok) {
for (auto& v : result.violations)
WARN("Violation: " << v);
}
REQUIRE(result.constraints_ok);
} else {
if (!result.constraints_ok) {
WARN("Constraint violation (infeasible case, soft): " << spec.id);
}
}
}
}

View File

@@ -0,0 +1 @@
{"base_result":{"constraints_ok":true,"flush_cost":0,"full_score":4.0},"context":{"group_info":{"filament_volume_map":[2,2],"has_filament_switcher":false,"ignore_ext_filament":false,"max_gap_threshold":0.01,"mode":0,"strategy":0,"total_filament_num":2},"machine_info":{"machine_filament_info":[[{"color":"#850C02FF","extruder_id":0,"is_extended":false,"is_support":false,"type":"PLA","usage_type":1},{"color":"#F10331FF","extruder_id":0,"is_extended":false,"is_support":false,"type":"PLA","usage_type":1},{"color":"#429A7CFF","extruder_id":0,"is_extended":false,"is_support":false,"type":"PLA","usage_type":1},{"color":"#8D67FEFF","extruder_id":0,"is_extended":false,"is_support":false,"type":"PLA","usage_type":1}],[{"color":"#2677E2FF","extruder_id":1,"is_extended":false,"is_support":false,"type":"PLA","usage_type":1},{"color":"#3A3922FF","extruder_id":1,"is_extended":false,"is_support":false,"type":"PLA","usage_type":1},{"color":"#DB8EB9FF","extruder_id":1,"is_extended":false,"is_support":false,"type":"PLA","usage_type":1},{"color":"#53A5A6FF","extruder_id":1,"is_extended":false,"is_support":false,"type":"PLA","usage_type":1}]],"master_extruder_id":0,"max_group_size":[2,2],"prefer_non_model_filament":[false,true]},"model_info":{"filament_ids":["GFL_0","GFL_1"],"filament_info":[{"color":"#E8650CFF","is_support":true,"type":"PLA-S","usage_type":0},{"color":"#0A9E99FF","is_support":false,"type":"PETG","usage_type":1}],"flush_matrix":[[[0.0,18.864795684814453],[512.7918701171875,0.0]],[[0.0,332.1666259765625],[58.37631607055664,0.0]]],"layer_filaments":[[0,1],[0,1],[0,1],[1],[0,1],[0,1],[0,1],[0],[0],[0],[0,1],[0,1],[0,1],[0,1],[1],[1],[0],[0],[0],[0],[0],[0],[0],[0],[1],[1],[1],[0,1],[0,1],[0,1],[0],[0,1],[0,1],[0,1],[0,1],[0,1],[0,1],[0,1],[0,1],[0,1],[0,1],[0,1],[0,1],[1],[1],[0],[0,1],[0],[0],[0],[0,1],[0,1],[0,1],[0],[0],[1],[0],[0,1],[0,1],[0,1],[0,1],[0,1],[0,1],[0,1],[0,1],[0],[0,1],[0,1],[0,1],[0,1],[0,1],[0],[0],[0],[0],[0,1],[0,1],[1],[1],[1],[1],[1],[0],[0],[0,1],[1],[0],[0,1],[1],[0],[0],[0,1],[0],[0],[0],[0,1],[0,1],[1],[0,1],[0],[0],[0],[0,1],[1],[1],[1],[1],[0,1],[0,1],[1],[0,1],[0,1],[0],[0],[0],[0],[0],[0,1],[1]],"unprintable_filaments":[[],[]],"unprintable_volumes":{}},"nozzle_info":{"extruder_nozzle_list":{"0":[0],"1":[1]},"nozzle_list":[{"diameter":"0.4","extruder_id":0,"group_id":0,"volume_type":0},{"diameter":"0.4","extruder_id":1,"group_id":1,"volume_type":0}],"nozzle_status":{}},"speed_info":{"ams_preload_enabled":[true,true],"change_time_params":{"selector_load_time":1.0,"selector_unload_time":1.0,"standard_load_time":3.0,"standard_unload_time":2.0},"extruder_change_time":5.0,"filament_change_time":2.0,"filament_print_time":{},"group_with_time":false}},"metadata":{"config_type":"A","id":"A_basic_17","seed":10017}}

View File

@@ -0,0 +1 @@
{"base_result":{"constraints_ok":true,"flush_cost":0,"full_score":4.0},"context":{"group_info":{"filament_volume_map":[2,2],"has_filament_switcher":false,"ignore_ext_filament":false,"max_gap_threshold":0.01,"mode":0,"strategy":0,"total_filament_num":2},"machine_info":{"machine_filament_info":[[{"color":"#3C6B30FF","extruder_id":0,"is_extended":false,"is_support":false,"type":"PLA","usage_type":1},{"color":"#FE9954FF","extruder_id":0,"is_extended":false,"is_support":false,"type":"PLA","usage_type":1},{"color":"#FBBAB3FF","extruder_id":0,"is_extended":false,"is_support":false,"type":"PLA","usage_type":1},{"color":"#A1A591FF","extruder_id":0,"is_extended":false,"is_support":false,"type":"PLA","usage_type":1}],[{"color":"#55EE36FF","extruder_id":1,"is_extended":false,"is_support":false,"type":"PLA","usage_type":1},{"color":"#A8F42BFF","extruder_id":1,"is_extended":false,"is_support":false,"type":"PLA","usage_type":1},{"color":"#930D0FFF","extruder_id":1,"is_extended":false,"is_support":false,"type":"PLA","usage_type":1},{"color":"#55C501FF","extruder_id":1,"is_extended":false,"is_support":false,"type":"PLA","usage_type":1}]],"master_extruder_id":0,"max_group_size":[2,2],"prefer_non_model_filament":[false,true]},"model_info":{"filament_ids":["GFL_0","GFL_1"],"filament_info":[{"color":"#C613F1FF","is_support":false,"type":"TPU","usage_type":1},{"color":"#0C0CBAFF","is_support":false,"type":"PLA","usage_type":1}],"flush_matrix":[[[0.0,463.53955078125],[50.04071044921875,0.0]],[[0.0,54.75393295288086],[566.574951171875,0.0]]],"layer_filaments":[[0,1],[0],[0],[0,1],[0,1],[0,1],[0],[0],[0,1],[1],[1],[1],[1],[0,1],[0,1],[0],[0],[0],[0],[0],[1],[1],[0,1],[0,1],[1],[0,1],[0],[0],[0],[0],[0,1],[1],[0],[0],[0],[0,1],[0,1],[0,1],[1],[1],[1],[1],[1],[0,1],[0],[0,1],[0,1],[0,1],[1],[1],[1],[0,1],[0,1],[1],[0],[0,1],[1],[0],[0],[1],[0,1],[1],[1],[1],[0,1],[0,1],[0],[0],[0,1],[0,1],[0,1],[0],[0,1],[0,1],[0,1],[1],[1],[1],[0,1],[0,1],[0,1],[0,1],[0,1],[1],[0,1],[0,1],[0,1],[0,1],[0,1],[0,1],[0,1],[0,1],[0],[0],[0,1],[1],[0,1],[1],[0],[0],[1],[0,1],[0,1],[1],[0,1],[0,1],[1],[1],[1],[0,1],[0,1],[0,1],[0,1],[0],[0,1],[0,1],[0,1],[1],[1],[0,1],[0,1],[0,1],[0],[0],[0,1],[1],[1],[1],[0,1],[1],[0,1],[0,1],[1],[1],[1],[0],[0],[0],[1],[1],[0,1],[1],[1],[0,1],[0],[0],[1],[0,1],[1],[0,1],[1],[1],[0],[0,1],[0,1],[0,1],[1],[1],[0,1],[0,1],[0,1],[1],[0,1],[0],[0,1],[0,1],[0],[0],[1],[1],[0,1],[0,1],[0,1],[0,1],[1],[1],[1],[0,1],[0,1],[0],[0,1],[0,1],[0,1],[1],[0,1],[0,1],[0,1],[0,1],[0,1],[0,1],[0,1],[0,1],[0],[0],[0,1],[1],[0],[0],[1],[1],[0],[0],[0,1],[0],[0],[0],[0,1],[0],[0,1],[0,1],[0],[0,1],[0,1],[0,1],[1],[0],[0,1],[0],[0,1],[0,1],[0,1],[0,1],[1],[0,1],[0,1],[0,1],[0,1],[1],[1],[1],[0,1],[1],[0,1],[0,1],[0,1],[0,1],[0],[0],[1],[0,1],[0,1],[1],[1],[1],[1],[0],[0],[0],[0],[0],[0],[0],[0],[0],[0,1],[1],[1],[1],[1],[0],[0],[0],[0,1],[0,1],[0],[0,1],[0,1],[0,1]],"unprintable_filaments":[[],[]],"unprintable_volumes":{}},"nozzle_info":{"extruder_nozzle_list":{"0":[0],"1":[1]},"nozzle_list":[{"diameter":"0.4","extruder_id":0,"group_id":0,"volume_type":0},{"diameter":"0.4","extruder_id":1,"group_id":1,"volume_type":0}],"nozzle_status":{}},"speed_info":{"ams_preload_enabled":[true,true],"change_time_params":{"selector_load_time":1.0,"selector_unload_time":1.0,"standard_load_time":3.0,"standard_unload_time":2.0},"extruder_change_time":5.0,"filament_change_time":2.0,"filament_print_time":{},"group_with_time":false}},"metadata":{"config_type":"A","id":"A_basic_39","seed":10039}}

View File

@@ -0,0 +1 @@
{"base_result":{"constraints_ok":true,"flush_cost":0,"full_score":4.0},"context":{"group_info":{"filament_volume_map":[2,2],"has_filament_switcher":false,"ignore_ext_filament":false,"max_gap_threshold":0.01,"mode":0,"strategy":0,"total_filament_num":2},"machine_info":{"machine_filament_info":[[{"color":"#835F29FF","extruder_id":0,"is_extended":false,"is_support":false,"type":"PLA","usage_type":1},{"color":"#ACB239FF","extruder_id":0,"is_extended":false,"is_support":false,"type":"PLA","usage_type":1},{"color":"#5EF84BFF","extruder_id":0,"is_extended":false,"is_support":false,"type":"PLA","usage_type":1},{"color":"#FE36A8FF","extruder_id":0,"is_extended":false,"is_support":false,"type":"PLA","usage_type":1}],[{"color":"#7B6ABDFF","extruder_id":1,"is_extended":false,"is_support":false,"type":"PLA","usage_type":1},{"color":"#4FFBDDFF","extruder_id":1,"is_extended":false,"is_support":false,"type":"PLA","usage_type":1},{"color":"#E06DDEFF","extruder_id":1,"is_extended":false,"is_support":false,"type":"PLA","usage_type":1},{"color":"#751104FF","extruder_id":1,"is_extended":false,"is_support":false,"type":"PLA","usage_type":1}]],"master_extruder_id":0,"max_group_size":[2,2],"prefer_non_model_filament":[false,true]},"model_info":{"filament_ids":["GFL_0","GFL_1"],"filament_info":[{"color":"#48BC3DFF","is_support":false,"type":"PETG","usage_type":1},{"color":"#1DEED1FF","is_support":false,"type":"PETG","usage_type":1}],"flush_matrix":[[[0.0,523.2988891601563],[480.04058837890625,0.0]],[[0.0,18.886493682861328],[47.65232467651367,0.0]]],"layer_filaments":[[0,1],[0],[0,1],[1],[0],[0],[1],[1],[0,1],[0,1],[0,1],[0,1],[0,1],[0,1],[0,1],[1],[1],[0,1],[0],[0],[0],[0],[0],[0,1],[0,1],[0,1],[0,1],[0,1],[1],[1],[1],[0],[0],[1],[1],[1],[0],[1],[1],[0,1],[0,1],[0,1],[0,1],[0,1],[0,1],[1],[0,1],[0,1],[0,1],[0],[1],[0],[0],[0],[0],[0,1],[0,1],[0,1],[0,1],[0,1],[0,1],[0,1],[0,1],[0,1],[1],[1],[0],[0],[0],[0],[0],[0,1],[0,1],[0,1],[0,1],[0,1],[1],[0],[0],[0],[0],[0],[0],[0,1],[0,1],[0],[0],[1],[0,1],[0],[0],[0],[0],[0],[0,1],[0,1],[0,1],[0,1],[0],[0],[1],[1],[0,1],[0,1],[0,1],[0],[0,1],[0,1],[0,1],[1],[1],[0],[0],[0],[0],[0],[0,1],[0],[0,1],[0,1],[0],[0],[0],[0],[0],[1],[1],[0,1],[0,1],[0,1],[1],[1],[1],[1],[1],[1],[0,1],[0,1],[0,1],[1],[1],[0,1],[0,1],[1],[1],[1],[0,1],[0,1],[0,1],[0,1],[1],[1],[0,1],[0,1],[0,1],[0,1],[0,1],[0,1],[0,1],[0,1],[0],[0],[0,1],[0,1],[0],[1],[1],[1],[1],[1],[1],[0,1],[0,1],[0,1],[1],[1],[0,1],[1],[0,1],[0,1],[0],[0,1],[0,1],[0,1],[0],[1],[0,1],[0,1],[1],[1],[1],[1],[0,1],[0,1],[0,1],[0,1],[0,1],[0,1],[0,1],[0,1],[0],[0],[0],[0],[0,1],[0,1],[0,1],[0,1],[0,1],[1],[0,1],[1],[1],[1],[0,1],[0,1],[0,1],[0,1],[0,1],[0,1],[0,1],[0,1],[0,1],[0],[0,1],[0],[0,1],[1],[0,1],[0,1],[1],[1],[0,1],[0,1],[0,1],[0,1],[0],[0,1],[0],[0,1],[0,1],[0,1],[0,1],[0,1],[0],[0],[0,1],[0,1],[0,1]],"unprintable_filaments":[[],[]],"unprintable_volumes":{}},"nozzle_info":{"extruder_nozzle_list":{"0":[0],"1":[1]},"nozzle_list":[{"diameter":"0.4","extruder_id":0,"group_id":0,"volume_type":0},{"diameter":"0.4","extruder_id":1,"group_id":1,"volume_type":0}],"nozzle_status":{}},"speed_info":{"ams_preload_enabled":[true,true],"change_time_params":{"selector_load_time":1.0,"selector_unload_time":1.0,"standard_load_time":3.0,"standard_unload_time":2.0},"extruder_change_time":5.0,"filament_change_time":2.0,"filament_print_time":{},"group_with_time":false}},"metadata":{"config_type":"A","id":"A_basic_9","seed":10009}}

View File

@@ -0,0 +1 @@
{"base_result":{"constraints_ok":true,"flush_cost":9996,"full_score":4878.185600000001},"context":{"group_info":{"filament_volume_map":[2,2,2],"has_filament_switcher":false,"ignore_ext_filament":false,"max_gap_threshold":0.01,"mode":0,"strategy":0,"total_filament_num":3},"machine_info":{"machine_filament_info":[[{"color":"#862B28FF","extruder_id":0,"is_extended":false,"is_support":false,"type":"PLA","usage_type":1},{"color":"#5DE136FF","extruder_id":0,"is_extended":false,"is_support":false,"type":"PLA","usage_type":1},{"color":"#B737ACFF","extruder_id":0,"is_extended":false,"is_support":false,"type":"PLA","usage_type":1},{"color":"#AB25E0FF","extruder_id":0,"is_extended":false,"is_support":false,"type":"PLA","usage_type":1}],[{"color":"#03D322FF","extruder_id":1,"is_extended":false,"is_support":false,"type":"PLA","usage_type":1},{"color":"#FEF6E8FF","extruder_id":1,"is_extended":false,"is_support":false,"type":"PLA","usage_type":1},{"color":"#6ACCEDFF","extruder_id":1,"is_extended":false,"is_support":false,"type":"PLA","usage_type":1},{"color":"#5F679DFF","extruder_id":1,"is_extended":false,"is_support":false,"type":"PLA","usage_type":1}]],"master_extruder_id":0,"max_group_size":[2,2],"prefer_non_model_filament":[false,true]},"model_info":{"filament_ids":["GFL_0","GFL_1","GFL_2"],"filament_info":[{"color":"#D4E014FF","is_support":false,"type":"PA","usage_type":1},{"color":"#D01CEEFF","is_support":false,"type":"PA","usage_type":1},{"color":"#E433C7FF","is_support":false,"type":"TPU","usage_type":1}],"flush_matrix":[[[0.0,345.6607360839844,334.3067626953125],[167.5570526123047,0.0,469.119140625],[205.99964904785156,536.2994384765625,0.0]],[[0.0,16.684810638427734,322.7650146484375],[260.761962890625,0.0,230.73336791992188],[267.0419616699219,64.71019744873047,0.0]]],"layer_filaments":[[0,2],[0,1],[0],[1],[1],[1],[1],[1],[1],[1],[1,2],[0,1,2],[0],[2],[1],[1],[1],[1],[1,2],[1,2],[1],[0,1],[0,1],[1,2],[1,2],[1,2],[1,2],[1,2],[2],[0],[1,2],[1,2],[1],[0,2],[0,1,2],[0,1,2],[1],[1,2],[1,2],[1,2],[1,2],[0,1,2],[2],[1],[0,1],[0,1,2],[0,1,2],[0,1,2],[0,2],[0,2],[0,2],[0,2],[0,1,2],[0,1,2],[0,1,2],[2],[2],[2],[1],[0,1],[0,1],[1],[1,2],[1,2],[0,1],[0,1,2],[0,1],[1],[2],[1,2],[2],[2],[2],[2],[2],[1,2],[2],[2],[0,2],[0,1],[0,1],[0,1],[0],[0,1],[0,1],[1],[1],[1],[0],[0,2],[2],[2],[2],[2],[0,2],[1],[1,2],[1],[1],[1],[1],[1],[0,1],[0,1],[0,1],[1],[2],[2],[0],[0],[0],[0],[0,2],[1,2],[0,2],[0,2],[0,2],[0],[1],[1,2],[0,2],[2],[1,2],[1,2],[1,2],[1,2],[1,2],[1,2],[2],[1,2],[2],[0,2],[0,2],[0,2],[0],[0],[0],[0,2],[2],[2],[0,2],[2],[0,1],[0],[0],[0],[0],[0],[1,2],[0,1,2],[0,1,2],[0,1],[1],[1],[0],[0],[0],[0],[1],[1],[1],[0,2],[0,2],[0],[0],[1,2],[1,2]],"unprintable_filaments":[[2],[]],"unprintable_volumes":{}},"nozzle_info":{"extruder_nozzle_list":{"0":[0],"1":[1]},"nozzle_list":[{"diameter":"0.4","extruder_id":0,"group_id":0,"volume_type":0},{"diameter":"0.4","extruder_id":1,"group_id":1,"volume_type":0}],"nozzle_status":{}},"speed_info":{"ams_preload_enabled":[true,true],"change_time_params":{"selector_load_time":1.0,"selector_unload_time":1.0,"standard_load_time":3.0,"standard_unload_time":2.0},"extruder_change_time":5.0,"filament_change_time":2.0,"filament_print_time":{},"group_with_time":false}},"metadata":{"config_type":"A","id":"A_constraint_117","seed":10117}}

View File

@@ -0,0 +1 @@
{"base_result":{"constraints_ok":true,"flush_cost":15640,"full_score":7498.304000000001},"context":{"group_info":{"filament_volume_map":[2,2,2,2,2],"has_filament_switcher":false,"ignore_ext_filament":false,"max_gap_threshold":0.01,"mode":0,"strategy":0,"total_filament_num":5},"machine_info":{"machine_filament_info":[[{"color":"#690A58FF","extruder_id":0,"is_extended":false,"is_support":false,"type":"PLA","usage_type":1},{"color":"#13D9ACFF","extruder_id":0,"is_extended":false,"is_support":false,"type":"PLA","usage_type":1},{"color":"#255B88FF","extruder_id":0,"is_extended":false,"is_support":false,"type":"PLA","usage_type":1},{"color":"#14AD07FF","extruder_id":0,"is_extended":false,"is_support":false,"type":"PLA","usage_type":1}],[{"color":"#E1E180FF","extruder_id":1,"is_extended":false,"is_support":false,"type":"PLA","usage_type":1},{"color":"#A69E85FF","extruder_id":1,"is_extended":false,"is_support":false,"type":"PLA","usage_type":1},{"color":"#9F27DAFF","extruder_id":1,"is_extended":false,"is_support":false,"type":"PLA","usage_type":1},{"color":"#96A697FF","extruder_id":1,"is_extended":false,"is_support":false,"type":"PLA","usage_type":1}]],"master_extruder_id":0,"max_group_size":[3,3],"prefer_non_model_filament":[false,true]},"model_info":{"filament_ids":["GFL_0","GFL_1","GFL_2","GFL_3","GFL_4"],"filament_info":[{"color":"#84C1C1FF","is_support":false,"type":"PLA","usage_type":1},{"color":"#1EB5C4FF","is_support":false,"type":"PLA","usage_type":1},{"color":"#8D50E5FF","is_support":false,"type":"PETG","usage_type":1},{"color":"#F22BC4FF","is_support":false,"type":"PA","usage_type":1},{"color":"#B7A86EFF","is_support":false,"type":"PA","usage_type":1}],"flush_matrix":[[[0.0,494.378173828125,256.524658203125,43.245582580566406,405.5243835449219],[16.6104679107666,0.0,150.26849365234375,400.1143493652344,30.314559936523438],[24.301469802856445,130.10775756835938,0.0,536.2122192382813,383.1336669921875],[437.3901062011719,272.96868896484375,82.35665893554688,0.0,17.11191177368164],[468.4289245605469,355.7682800292969,452.1650085449219,45.59552764892578,0.0]],[[0.0,491.8825378417969,205.77655029296875,349.1083984375,506.652099609375],[101.32499694824219,0.0,175.80380249023438,120.50504302978516,438.80804443359375],[152.76100158691406,355.5727233886719,0.0,581.8612060546875,585.173583984375],[546.7792358398438,22.963092803955078,407.84173583984375,0.0,105.83439636230469],[356.05926513671875,145.5247344970703,230.94595336914063,345.7974548339844,0.0]]],"layer_filaments":[[0,3,4],[0],[0],[0],[0],[0],[4],[4],[1],[1],[1],[3],[1],[1],[3],[0,1,4],[0,1],[0,1,3],[0,1],[1],[0],[0,3],[0,3],[2],[3,4],[0,2],[2,4],[4],[3],[3],[3],[1],[2],[4],[4],[4],[4],[0,3,4],[0,3],[0,3,4],[0,4],[0,3,4],[0,2,3],[0,2],[0,2],[0,2],[0,2],[0],[0],[0],[0,3,4],[3,4],[1,3,4],[3,4],[3,4],[3],[2],[4],[4],[4],[4],[4],[1],[1],[1],[1],[1],[1,4],[1],[1,3],[2,3],[2,3,4],[2,3,4],[3,4],[3,4],[0,1,3],[0,1],[0,1],[0,1,2],[0,1,2],[1,2],[1,2],[1,2],[0,2,3],[0,2,3],[0,2,3],[3],[3],[0,3,4],[0,2,3,4],[0,2,3,4],[0,2,3],[2],[2],[2],[0],[1],[1],[1],[4],[1,4],[0,4],[0,3,4],[0,3,4],[0,3,4],[0,3,4],[0,3,4],[0,3,4],[3],[3],[3],[0,3],[0,3],[0],[4],[3,4]],"unprintable_filaments":[[],[0,1]],"unprintable_volumes":{}},"nozzle_info":{"extruder_nozzle_list":{"0":[0],"1":[1]},"nozzle_list":[{"diameter":"0.4","extruder_id":0,"group_id":0,"volume_type":0},{"diameter":"0.4","extruder_id":1,"group_id":1,"volume_type":0}],"nozzle_status":{}},"speed_info":{"ams_preload_enabled":[true,true],"change_time_params":{"selector_load_time":1.0,"selector_unload_time":1.0,"standard_load_time":3.0,"standard_unload_time":2.0},"extruder_change_time":5.0,"filament_change_time":2.0,"filament_print_time":{},"group_with_time":false}},"metadata":{"config_type":"A","id":"A_constraint_87","seed":10087}}

View File

@@ -0,0 +1 @@
{"base_result":{"constraints_ok":true,"flush_cost":22348,"full_score":10641.052800000001},"context":{"group_info":{"filament_volume_map":[2,2,2,2,2,2,2],"has_filament_switcher":false,"ignore_ext_filament":false,"max_gap_threshold":0.01,"mode":0,"strategy":1,"total_filament_num":7},"machine_info":{"machine_filament_info":[[{"color":"#CA0BBFFF","extruder_id":0,"is_extended":false,"is_support":false,"type":"PLA","usage_type":1},{"color":"#04BBE7FF","extruder_id":0,"is_extended":false,"is_support":false,"type":"PLA","usage_type":1},{"color":"#0C9192FF","extruder_id":0,"is_extended":false,"is_support":false,"type":"PLA","usage_type":1},{"color":"#D03AE7FF","extruder_id":0,"is_extended":false,"is_support":false,"type":"PLA","usage_type":1}],[{"color":"#58ED4CFF","extruder_id":1,"is_extended":false,"is_support":false,"type":"PLA","usage_type":1},{"color":"#F0F7E7FF","extruder_id":1,"is_extended":false,"is_support":false,"type":"PLA","usage_type":1},{"color":"#C01A00FF","extruder_id":1,"is_extended":false,"is_support":false,"type":"PLA","usage_type":1},{"color":"#41BF7EFF","extruder_id":1,"is_extended":false,"is_support":false,"type":"PLA","usage_type":1}]],"master_extruder_id":0,"max_group_size":[4,4],"prefer_non_model_filament":[false,true]},"model_info":{"filament_ids":["GFL_0","GFL_1","GFL_2","GFL_3","GFL_4","GFL_5","GFL_6"],"filament_info":[{"color":"#15D1F3FF","is_support":false,"type":"PETG","usage_type":1},{"color":"#2E765BFF","is_support":false,"type":"TPU","usage_type":1},{"color":"#F7F661FF","is_support":false,"type":"TPU","usage_type":1},{"color":"#1F8FA4FF","is_support":false,"type":"TPU","usage_type":1},{"color":"#548538FF","is_support":false,"type":"TPU","usage_type":1},{"color":"#49CDDAFF","is_support":true,"type":"PLA-S","usage_type":0},{"color":"#77400CFF","is_support":false,"type":"TPU","usage_type":1}],"flush_matrix":[[[0.0,522.1661987304688,100.72758483886719,133.01226806640625,491.89971923828125,10.22260570526123,398.2569580078125],[199.46890258789063,0.0,193.49847412109375,173.7811737060547,254.3165283203125,524.8251342773438,11.158357620239258],[25.300485610961914,185.6349334716797,0.0,14.763650894165039,527.9827270507813,503.7408142089844,196.4144287109375],[14.506301879882813,388.3427429199219,592.0447387695313,0.0,43.87137222290039,568.1431274414063,473.7308654785156],[320.220703125,309.9248352050781,39.37203598022461,47.134002685546875,0.0,97.27034759521484,458.011962890625],[572.5841674804688,336.4319152832031,47.922828674316406,278.3333740234375,182.32958984375,0.0,402.6477966308594],[447.379150390625,588.4238891601563,13.493745803833008,433.3789978027344,390.13360595703125,461.5366516113281,0.0]],[[0.0,298.9002380371094,586.3795776367188,271.62158203125,272.1965637207031,152.54571533203125,239.88775634765625],[99.5313949584961,0.0,165.59657287597656,83.9988021850586,219.39901733398438,418.60491943359375,575.6079711914063],[35.19260787963867,44.58149719238281,0.0,212.56053161621094,139.3209686279297,596.1610107421875,564.784423828125],[488.8506774902344,419.78521728515625,321.46514892578125,0.0,46.86149597167969,310.4212341308594,303.2931213378906],[531.5128173828125,116.25019073486328,207.16612243652344,50.03121566772461,0.0,385.4001770019531,108.3058090209961],[497.9175720214844,444.3203125,331.2178649902344,499.4423828125,546.33642578125,0.0,352.6327209472656],[423.0767822265625,189.04090881347656,132.35194396972656,453.5607604980469,29.324968338012695,336.557861328125,0.0]]],"layer_filaments":[[0,3,4,5],[0,4,5],[0,1,4,5],[1,4,5],[1,5],[4],[4],[5],[5],[5],[5],[6],[1],[1],[1,3],[1,3],[0,1,3],[0,3,4,5],[1,4,5],[1,4],[3,4],[0,1],[0,1],[0,1],[0,1],[1,5],[1,5],[1],[1],[1],[4],[4],[3,4],[0,1,6],[0,1,6],[1,3,6],[1,3,6],[6],[6],[3,6],[3,6],[3,6],[0,3,4,5],[0,3,4,5],[0,2,3,4,5],[0,2,3],[0,3],[0,3],[0,3],[0],[0,4],[4],[2,3,4,6],[2,4],[2,4],[5],[5],[5],[5],[2,5],[2,3,5],[2,3,5],[2,3,5],[2],[2,3,4],[2,3,4],[0,3,4],[0,3,4],[3,4,6],[0,3,4,6],[0,3,6],[0,3,6],[1,3,4,5],[0,3],[1,3,5,6],[5,6],[4,5,6],[3,4],[3],[1,3],[3],[2,3,5,6],[3,5,6],[5],[2],[6],[6],[4,6],[6],[6],[4,6],[1,4,6],[4],[0],[0],[0,5],[5],[6],[0,1],[0,1],[0,1,3],[0],[0],[0],[0,3],[0,3],[0,3],[0,3,5],[3,5],[3],[3],[3],[3],[3,5],[3,5],[0,3,5],[0]],"unprintable_filaments":[[],[]],"unprintable_volumes":{}},"nozzle_info":{"extruder_nozzle_list":{"0":[0],"1":[1]},"nozzle_list":[{"diameter":"0.4","extruder_id":0,"group_id":0,"volume_type":0},{"diameter":"0.4","extruder_id":1,"group_id":1,"volume_type":0}],"nozzle_status":{}},"speed_info":{"ams_preload_enabled":[true,true],"change_time_params":{"selector_load_time":1.0,"selector_unload_time":1.0,"standard_load_time":3.0,"standard_unload_time":2.0},"extruder_change_time":5.0,"filament_change_time":2.0,"filament_print_time":{},"group_with_time":false}},"metadata":{"config_type":"A","id":"A_mode_bestfit_161","seed":10161}}

View File

@@ -0,0 +1 @@
{"base_result":{"constraints_ok":true,"flush_cost":28788,"full_score":13492.236799999999},"context":{"group_info":{"filament_volume_map":[2,2,2,2],"has_filament_switcher":false,"ignore_ext_filament":false,"max_gap_threshold":0.01,"mode":1,"strategy":0,"total_filament_num":4},"machine_info":{"machine_filament_info":[[{"color":"#2C1334FF","extruder_id":0,"is_extended":false,"is_support":false,"type":"PLA","usage_type":1},{"color":"#672D31FF","extruder_id":0,"is_extended":false,"is_support":false,"type":"PLA","usage_type":1},{"color":"#40C111FF","extruder_id":0,"is_extended":false,"is_support":false,"type":"PLA","usage_type":1},{"color":"#0D6A9EFF","extruder_id":0,"is_extended":false,"is_support":false,"type":"PLA","usage_type":1}],[{"color":"#1C9D92FF","extruder_id":1,"is_extended":false,"is_support":false,"type":"PLA","usage_type":1},{"color":"#DFD03AFF","extruder_id":1,"is_extended":false,"is_support":false,"type":"PLA","usage_type":1},{"color":"#9F1CE2FF","extruder_id":1,"is_extended":false,"is_support":false,"type":"PLA","usage_type":1},{"color":"#3F8A30FF","extruder_id":1,"is_extended":false,"is_support":false,"type":"PLA","usage_type":1}]],"master_extruder_id":0,"max_group_size":[3,3],"prefer_non_model_filament":[false,true]},"model_info":{"filament_ids":["GFL_0","GFL_1","GFL_2","GFL_3"],"filament_info":[{"color":"#3F3A9BFF","is_support":false,"type":"TPU","usage_type":1},{"color":"#2A420FFF","is_support":false,"type":"TPU","usage_type":1},{"color":"#37A850FF","is_support":false,"type":"PETG","usage_type":1},{"color":"#EB9B49FF","is_support":false,"type":"PA","usage_type":1}],"flush_matrix":[[[0.0,357.30389404296875,131.00808715820313,575.68603515625],[289.8275451660156,0.0,56.926780700683594,548.9874877929688],[323.252685546875,156.2064666748047,0.0,210.6259002685547],[469.44366455078125,165.70809936523438,174.34884643554688,0.0]],[[0.0,141.2342529296875,146.28591918945313,193.52146911621094],[548.3370971679688,0.0,404.035400390625,559.38232421875],[110.14042663574219,127.97674560546875,0.0,582.9161376953125],[221.37879943847656,321.6277160644531,375.734619140625,0.0]]],"layer_filaments":[[0,1,2],[0],[0],[1],[1],[1],[1],[1,3],[2],[2],[2],[0],[0,1,2],[1,2],[2],[2],[2],[2],[2],[3],[0,3],[0,3],[0,2],[2],[1,2],[1,2],[1],[0,1,2],[0,1],[0,1],[1,3],[0,1,3],[0,1,3],[0],[0,2],[0],[0],[0],[0],[0],[0,1,2],[0,1,3],[1,3],[1,3],[1,3],[1,3],[1,2,3],[0,1,2,3],[1,2,3],[0,1,2],[0,1,2],[0,1],[0,1],[0,1],[0,1,2],[0,1,2],[0,1,2,3],[0,1,2,3],[0,1],[1,2,3],[1,2],[0],[1,3],[0],[0],[2],[2,3],[1,2],[2],[1,2],[0,1],[1],[1,2],[1],[0,3],[0,3],[1,3],[1,3],[1,3],[1,3],[1],[1],[1],[1],[1],[1],[1],[1],[0,2],[0,2],[0,2,3],[1,2],[1,2],[1,2],[2],[2],[2],[2],[1,3],[1],[1],[1],[2,3],[2],[2],[2],[2],[2],[2],[2],[0,1],[0,2,3],[0,2,3],[0,2],[0,2],[0,2],[0],[1,3],[1,3],[3],[1,3],[0,3],[0,2,3],[0,2,3],[0,2,3],[3],[3],[3],[2],[1,2]],"unprintable_filaments":[[],[]],"unprintable_volumes":{}},"nozzle_info":{"extruder_nozzle_list":{"0":[0],"1":[1]},"nozzle_list":[{"diameter":"0.4","extruder_id":0,"group_id":0,"volume_type":0},{"diameter":"0.4","extruder_id":1,"group_id":1,"volume_type":0}],"nozzle_status":{}},"speed_info":{"ams_preload_enabled":[true,true],"change_time_params":{"selector_load_time":1.0,"selector_unload_time":1.0,"standard_load_time":3.0,"standard_unload_time":2.0},"extruder_change_time":5.0,"filament_change_time":2.0,"filament_print_time":{},"group_with_time":false}},"metadata":{"config_type":"A","id":"A_mode_match_150","seed":10150}}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1 @@
{"base_result":{"constraints_ok":true,"flush_cost":0,"full_score":184.0},"context":{"group_info":{"filament_volume_map":[2,2,2],"has_filament_switcher":false,"ignore_ext_filament":false,"max_gap_threshold":0.01,"mode":0,"strategy":0,"total_filament_num":3},"machine_info":{"machine_filament_info":[[{"color":"#58191BFF","extruder_id":0,"is_extended":false,"is_support":false,"type":"PLA","usage_type":1},{"color":"#65BA4BFF","extruder_id":0,"is_extended":false,"is_support":false,"type":"PLA","usage_type":1},{"color":"#8198CBFF","extruder_id":0,"is_extended":false,"is_support":false,"type":"PLA","usage_type":1},{"color":"#ACC0ABFF","extruder_id":0,"is_extended":false,"is_support":false,"type":"PLA","usage_type":1}],[{"color":"#22ED52FF","extruder_id":1,"is_extended":false,"is_support":false,"type":"PLA","usage_type":1},{"color":"#C09D59FF","extruder_id":1,"is_extended":false,"is_support":false,"type":"PLA","usage_type":1},{"color":"#2DA502FF","extruder_id":1,"is_extended":false,"is_support":false,"type":"PLA","usage_type":1},{"color":"#F7A2FAFF","extruder_id":1,"is_extended":false,"is_support":false,"type":"PLA","usage_type":1}]],"master_extruder_id":0,"max_group_size":[4,4],"prefer_non_model_filament":[false,false]},"model_info":{"filament_ids":["GFL_0","GFL_1","GFL_2"],"filament_info":[{"color":"#D0206DFF","is_support":true,"type":"PLA-S","usage_type":0},{"color":"#8F4679FF","is_support":false,"type":"PA","usage_type":1},{"color":"#29B62DFF","is_support":false,"type":"ABS","usage_type":1}],"flush_matrix":[[[0.0,22.997100830078125,311.9136962890625],[467.8121032714844,0.0,309.3307189941406],[409.413330078125,543.3587646484375,0.0]],[[0.0,424.8780517578125,513.323974609375],[432.9106750488281,0.0,186.18800354003906],[419.3179016113281,409.17095947265625,0.0]]],"layer_filaments":[[0,1],[0,2],[0],[0,1],[0,1,2],[1],[1],[1],[1],[2],[0],[0,2],[0],[0],[0,2],[0,1],[0],[2],[2],[2],[2],[2],[2],[1,2],[1,2],[1],[2],[2],[0,1],[0,2],[1,2],[1],[1],[0,1],[0,1,2],[2],[2],[2],[1],[1],[2],[2],[0],[1],[0],[0,1],[0,1],[0,1],[1],[1],[1],[0,1],[1,2],[0,1,2],[1,2],[1,2],[1,2],[1,2],[0,2],[0,1],[0,1],[1,2],[1],[1],[2],[2],[2],[2],[1,2],[1],[1],[1],[1],[0],[0],[2],[0,2],[0,1,2],[2],[2],[2],[0,2],[0,1,2],[0,2],[2],[1],[1],[1],[2],[2],[2],[1,2],[1,2],[2],[2],[2],[0,2],[0,2],[2],[0,2],[0],[0],[0],[0,1],[0,1],[0,1],[0,1],[0,2],[0,2],[0],[0,2],[2],[1,2],[2],[1,2],[0,2],[0],[0,2],[1],[1],[1],[2],[0,1]],"unprintable_filaments":[[],[]],"unprintable_volumes":{}},"nozzle_info":{"extruder_nozzle_list":{"0":[0],"1":[1,2]},"nozzle_list":[{"diameter":"0.4","extruder_id":0,"group_id":0,"volume_type":0},{"diameter":"0.4","extruder_id":1,"group_id":1,"volume_type":0},{"diameter":"0.4","extruder_id":1,"group_id":2,"volume_type":0}],"nozzle_status":{}},"speed_info":{"ams_preload_enabled":[true,true],"change_time_params":{"selector_load_time":1.0,"selector_unload_time":1.0,"standard_load_time":3.0,"standard_unload_time":2.0},"extruder_change_time":5.0,"filament_change_time":2.0,"filament_print_time":{},"group_with_time":false}},"metadata":{"config_type":"B","id":"B_basic_24","seed":20024}}

View File

@@ -0,0 +1 @@
{"base_result":{"constraints_ok":true,"flush_cost":0,"full_score":439.0},"context":{"group_info":{"filament_volume_map":[2,2,2,2,2],"has_filament_switcher":false,"ignore_ext_filament":false,"max_gap_threshold":0.01,"mode":0,"strategy":0,"total_filament_num":5},"machine_info":{"machine_filament_info":[[{"color":"#CE9589FF","extruder_id":0,"is_extended":false,"is_support":false,"type":"PLA","usage_type":1},{"color":"#C1CA1FFF","extruder_id":0,"is_extended":false,"is_support":false,"type":"PLA","usage_type":1},{"color":"#63C10EFF","extruder_id":0,"is_extended":false,"is_support":false,"type":"PLA","usage_type":1},{"color":"#1FB0EDFF","extruder_id":0,"is_extended":false,"is_support":false,"type":"PLA","usage_type":1}],[{"color":"#D1526EFF","extruder_id":1,"is_extended":false,"is_support":false,"type":"PLA","usage_type":1},{"color":"#FFFDCCFF","extruder_id":1,"is_extended":false,"is_support":false,"type":"PLA","usage_type":1},{"color":"#FE458BFF","extruder_id":1,"is_extended":false,"is_support":false,"type":"PLA","usage_type":1},{"color":"#FE7A24FF","extruder_id":1,"is_extended":false,"is_support":false,"type":"PLA","usage_type":1}]],"master_extruder_id":0,"max_group_size":[4,8],"prefer_non_model_filament":[false,false]},"model_info":{"filament_ids":["GFL_0","GFL_1","GFL_2","GFL_3","GFL_4"],"filament_info":[{"color":"#E892ACFF","is_support":false,"type":"PETG","usage_type":1},{"color":"#9BF6B9FF","is_support":false,"type":"ABS","usage_type":1},{"color":"#B37CAAFF","is_support":false,"type":"PETG","usage_type":1},{"color":"#E57574FF","is_support":true,"type":"PLA-S","usage_type":0},{"color":"#4821E8FF","is_support":false,"type":"PETG","usage_type":1}],"flush_matrix":[[[0.0,375.2281494140625,463.1433410644531,91.72261810302734,543.7777709960938],[40.411563873291016,0.0,46.204158782958984,133.43045043945313,140.9217987060547],[385.8175354003906,90.685302734375,0.0,140.52508544921875,75.50867462158203],[316.38568115234375,89.43927764892578,424.980224609375,0.0,27.291486740112305],[50.60981369018555,200.03822326660156,547.7901000976563,466.6471862792969,0.0]],[[0.0,240.17831420898438,577.3026123046875,222.15658569335938,590.8341064453125],[424.3199157714844,0.0,434.2823486328125,209.71908569335938,437.5057373046875],[349.07037353515625,464.85870361328125,0.0,32.95913314819336,527.3640747070313],[256.4498596191406,298.9818115234375,598.7887573242188,0.0,31.422487258911133],[529.8190307617188,558.2802734375,43.493682861328125,432.5175476074219,0.0]]],"layer_filaments":[[1,2,4],[1,2,4],[1,4],[1,4],[4],[4],[2],[2,4],[2,4],[2,4],[2,3],[1,2,3],[2,3],[2,3],[3],[3],[0,4],[0,4],[0,4],[0],[0],[0,3],[0,2,3],[2],[2],[1,2],[1,2],[0,2],[0,2],[0,2],[0],[0,2,4],[0,2,4],[0,1,3],[0,1,3,4],[0,1,2,3],[0,1,2,3],[2,3],[1,2,3],[1,2],[2],[3],[0,2,4],[0,4],[0,4],[1,4],[0,1,4],[0,1,4],[0,4],[4],[4],[4],[3],[3],[3],[0,1,4],[2],[0,2],[0,2],[0,2,4],[0,1,2],[0,1],[0,1],[1],[1],[1],[1],[3],[3],[3],[4],[3,4],[3,4],[4],[4],[4],[4],[4],[2],[2],[2],[2,4],[2,3,4],[3],[0,2,4],[0],[0],[1],[1],[2],[2],[2],[1],[1],[0,1],[0,1,2],[0,2],[1,3],[1,3],[1,3],[0,1],[0,1,3],[0,1,3,4],[0,1,3],[0,3],[3],[2,3],[2],[2],[2],[1],[3],[2,3],[1,3],[3],[3],[2],[2,4],[0,3,4],[3,4],[3],[0,3,4],[0,4],[3,4],[2,3],[1,2],[1,2],[1,2],[1,2],[1,2],[2],[2],[1,2],[1,2]],"unprintable_filaments":[[],[]],"unprintable_volumes":{}},"nozzle_info":{"extruder_nozzle_list":{"0":[0],"1":[1,2,3,4]},"nozzle_list":[{"diameter":"0.4","extruder_id":0,"group_id":0,"volume_type":0},{"diameter":"0.4","extruder_id":1,"group_id":1,"volume_type":0},{"diameter":"0.4","extruder_id":1,"group_id":2,"volume_type":1},{"diameter":"0.4","extruder_id":1,"group_id":3,"volume_type":0},{"diameter":"0.4","extruder_id":1,"group_id":4,"volume_type":3}],"nozzle_status":{}},"speed_info":{"ams_preload_enabled":[true,true],"change_time_params":{"selector_load_time":1.0,"selector_unload_time":1.0,"standard_load_time":3.0,"standard_unload_time":2.0},"extruder_change_time":5.0,"filament_change_time":2.0,"filament_print_time":{},"group_with_time":false}},"metadata":{"config_type":"B","id":"B_basic_27","seed":20027}}

View File

@@ -0,0 +1 @@
{"base_result":{"constraints_ok":true,"flush_cost":0,"full_score":279.0},"context":{"group_info":{"filament_volume_map":[2,2,2],"has_filament_switcher":false,"ignore_ext_filament":false,"max_gap_threshold":0.01,"mode":0,"strategy":0,"total_filament_num":3},"machine_info":{"machine_filament_info":[[{"color":"#21E724FF","extruder_id":0,"is_extended":false,"is_support":false,"type":"PLA","usage_type":1},{"color":"#B2066AFF","extruder_id":0,"is_extended":false,"is_support":false,"type":"PLA","usage_type":1},{"color":"#727804FF","extruder_id":0,"is_extended":false,"is_support":false,"type":"PLA","usage_type":1},{"color":"#CBC06DFF","extruder_id":0,"is_extended":false,"is_support":false,"type":"PLA","usage_type":1}],[{"color":"#0EF122FF","extruder_id":1,"is_extended":false,"is_support":false,"type":"PLA","usage_type":1},{"color":"#7299D2FF","extruder_id":1,"is_extended":false,"is_support":false,"type":"PLA","usage_type":1},{"color":"#EA9A57FF","extruder_id":1,"is_extended":false,"is_support":false,"type":"PLA","usage_type":1},{"color":"#3A2E9EFF","extruder_id":1,"is_extended":false,"is_support":false,"type":"PLA","usage_type":1}]],"master_extruder_id":0,"max_group_size":[4,4],"prefer_non_model_filament":[false,false]},"model_info":{"filament_ids":["GFL_0","GFL_1","GFL_2"],"filament_info":[{"color":"#88704DFF","is_support":false,"type":"PETG","usage_type":1},{"color":"#D3D1CCFF","is_support":false,"type":"PLA","usage_type":1},{"color":"#F8C24BFF","is_support":false,"type":"PLA","usage_type":1}],"flush_matrix":[[[0.0,123.0057144165039,149.25132751464844],[140.3647918701172,0.0,395.5540771484375],[354.4726257324219,160.0218048095703,0.0]],[[0.0,249.49559020996094,285.7671203613281],[366.1941223144531,0.0,411.5499267578125],[69.0042724609375,557.7216186523438,0.0]]],"layer_filaments":[[0,2],[0,1],[0,1],[0,2],[0,2],[0,1,2],[0,1,2],[0],[0],[0],[0],[1],[0,1],[0],[0],[0,1],[1],[0,1],[0,1],[0,1],[0,1],[1,2],[2],[1],[1],[1],[0],[0],[0],[0],[0],[1],[1],[1],[2],[1,2],[1,2],[1],[1,2],[2],[1,2],[1],[1,2],[1,2],[1,2],[2],[2],[1,2],[1],[0],[0],[0],[0,1],[0,1],[1],[0,1],[1],[1,2],[0,1],[0,1],[0,1],[0,1],[0,1,2],[1,2],[1,2],[1,2],[1,2],[2],[2],[2],[1],[1],[1],[1],[1],[1],[0,1],[0],[0],[0],[0],[0],[0],[0],[0],[0,1],[1],[1,2],[1],[0,1],[0],[0],[0],[0],[0],[0],[0,1],[0,2],[2],[0],[1,2],[0,1,2],[0,1,2],[0,1,2],[0,1,2],[0,1,2],[0,2],[1],[1],[1],[0,1],[0,1,2],[0,2],[0],[1],[0],[0],[0],[0],[0],[0,2],[0],[0],[0],[0],[0],[0],[0,1],[0],[0],[0,1],[1],[1],[1],[2],[0,2],[2],[0,2],[0],[2],[1,2],[0,1],[0,1],[0,1,2],[0,1],[0,1],[2],[2],[0,2],[1,2],[1],[1,2],[1,2],[1,2],[1,2],[1],[1],[1],[1],[1],[1],[1],[0,1],[0,1],[0,1],[0],[0],[0],[0],[0],[0],[2],[1],[1],[0],[0],[0,2],[1],[1],[1,2],[1,2],[0,1,2],[0,1,2],[0,1,2],[0],[0],[0],[0,2],[2],[0,1],[0],[1,2],[2],[1],[1,2],[1,2],[1,2],[0,1,2],[1],[1],[1],[1],[1],[0],[0,1],[1],[2],[1],[1,2],[0,1],[0,2],[0,1,2]],"unprintable_filaments":[[],[]],"unprintable_volumes":{}},"nozzle_info":{"extruder_nozzle_list":{"0":[0],"1":[1,2]},"nozzle_list":[{"diameter":"0.4","extruder_id":0,"group_id":0,"volume_type":0},{"diameter":"0.4","extruder_id":1,"group_id":1,"volume_type":0},{"diameter":"0.4","extruder_id":1,"group_id":2,"volume_type":3}],"nozzle_status":{}},"speed_info":{"ams_preload_enabled":[true,true],"change_time_params":{"selector_load_time":1.0,"selector_unload_time":1.0,"standard_load_time":3.0,"standard_unload_time":2.0},"extruder_change_time":5.0,"filament_change_time":2.0,"filament_print_time":{},"group_with_time":false}},"metadata":{"config_type":"B","id":"B_basic_47","seed":20047}}

View File

@@ -0,0 +1 @@
{"base_result":{"constraints_ok":true,"flush_cost":12987,"full_score":6744.9032},"context":{"group_info":{"filament_volume_map":[2,2,2,2],"has_filament_switcher":false,"ignore_ext_filament":false,"max_gap_threshold":0.01,"mode":0,"strategy":0,"total_filament_num":4},"machine_info":{"machine_filament_info":[[{"color":"#1BC545FF","extruder_id":0,"is_extended":false,"is_support":false,"type":"PLA","usage_type":1},{"color":"#D494DDFF","extruder_id":0,"is_extended":false,"is_support":false,"type":"PLA","usage_type":1},{"color":"#99E903FF","extruder_id":0,"is_extended":false,"is_support":false,"type":"PLA","usage_type":1},{"color":"#6C8B32FF","extruder_id":0,"is_extended":false,"is_support":false,"type":"PLA","usage_type":1}],[{"color":"#961DFEFF","extruder_id":1,"is_extended":false,"is_support":false,"type":"PLA","usage_type":1},{"color":"#FF2B42FF","extruder_id":1,"is_extended":false,"is_support":false,"type":"PLA","usage_type":1},{"color":"#BFEBE0FF","extruder_id":1,"is_extended":false,"is_support":false,"type":"PLA","usage_type":1},{"color":"#A6F508FF","extruder_id":1,"is_extended":false,"is_support":false,"type":"PLA","usage_type":1}]],"master_extruder_id":0,"max_group_size":[4,4],"prefer_non_model_filament":[false,false]},"model_info":{"filament_ids":["GFL_0","GFL_1","GFL_2","GFL_3"],"filament_info":[{"color":"#2093D1FF","is_support":false,"type":"TPU","usage_type":1},{"color":"#4B9E95FF","is_support":false,"type":"PLA","usage_type":1},{"color":"#79642EFF","is_support":true,"type":"PLA-S","usage_type":0},{"color":"#DC89BAFF","is_support":false,"type":"PETG","usage_type":1}],"flush_matrix":[[[0.0,145.34756469726563,196.37205505371094,358.7149353027344],[25.324295043945313,0.0,117.23784637451172,303.9642333984375],[553.1095581054688,434.2644348144531,0.0,28.46591567993164],[561.9080810546875,554.3380737304688,435.35601806640625,0.0]],[[0.0,412.2175598144531,165.17893981933594,252.97967529296875],[259.94940185546875,0.0,117.8436279296875,560.5171508789063],[168.64022827148438,568.7840576171875,0.0,28.568143844604492],[517.0733642578125,385.4801330566406,224.5548095703125,0.0]]],"layer_filaments":[[0,3],[1,3],[0,3],[0,1,3],[0,1,3],[1,3],[0,1,3],[1,2,3],[0,2],[2],[2],[2],[3],[1,2,3],[2,3],[1,2,3],[3],[3],[3],[3],[0,3],[3],[3],[2],[3],[2],[2],[0,2,3],[0,2,3],[0,1,2],[0,2],[2],[2],[2],[2],[2],[2],[2],[1,2],[1,2],[1,2],[1,2],[1,2],[2],[2],[2],[1],[1],[3],[3],[1],[0,1],[0,1],[0,1],[0,2,3],[0,2,3],[0],[0],[0,1],[0,1],[0,1,3],[0,1,3],[0,1,3],[3],[2],[2],[2],[2,3],[3],[0],[1,2,3],[3],[3],[1],[1],[1],[1],[0],[0,3],[0,3],[3],[3],[0,3],[3],[3],[3],[3],[2,3],[2,3],[2,3],[2,3],[0,2,3],[0,2,3],[0,3],[1,2,3],[3],[1],[3],[0],[0],[2],[2],[0,1,3],[0,3],[0],[0],[0],[0,2],[0,2],[2],[2],[2],[0,2],[2,3],[2,3],[2,3],[0,2,3],[0,2,3],[0,1,2,3],[0,2,3],[0,2,3],[2,3],[2],[2],[1,2],[2],[2],[0,3],[0,1],[0,1],[3],[3],[0,1,3],[0,1,3],[0,2,3],[0,3],[0],[0,2],[2],[0],[0,1],[0,1],[0,3],[0,3],[0,3],[0,3],[0,1,2],[0,2],[2,3],[1,2,3],[0,1,2],[3],[3],[3],[3],[1],[0,1],[0,1,3],[0,1,3],[0,1,2,3],[1,2,3],[1,2,3],[0],[0],[0],[0],[1],[3],[3],[0,1,2],[1,2],[2],[0,2],[0],[0,2],[0,2],[0,2],[1,2],[1,2],[0,1,2],[2],[1,2],[1,2],[1,2],[1,3],[1],[1],[1,3],[3],[0,1,3],[0,1],[0,1],[0,1],[0],[0],[0],[0],[2],[1],[1],[1],[3],[3],[3],[0,3],[0,3],[0,3],[0,2,3],[0,2,3],[0,1,2],[0,1,2],[1,2],[0,1],[0,1,3],[0,1,2],[0,1,2,3],[0,1,2,3],[1,3],[1,2],[0,1,2],[0,1,2],[0,1,2],[0,1,2],[1],[1],[3],[3],[0,3],[0,3],[0,1,3],[0,1],[1],[0],[0,2],[0,2,3],[2,3],[1,2,3],[0,1,2,3],[0,1,2],[0,1,2],[0,1,2,3],[0,1,2,3],[0,1],[1]],"unprintable_filaments":[[],[3]],"unprintable_volumes":{"2":[3],"3":[1]}},"nozzle_info":{"extruder_nozzle_list":{"0":[0],"1":[1,2]},"nozzle_list":[{"diameter":"0.4","extruder_id":0,"group_id":0,"volume_type":0},{"diameter":"0.4","extruder_id":1,"group_id":1,"volume_type":1},{"diameter":"0.4","extruder_id":1,"group_id":2,"volume_type":3}],"nozzle_status":{}},"speed_info":{"ams_preload_enabled":[true,true],"change_time_params":{"selector_load_time":1.0,"selector_unload_time":1.0,"standard_load_time":3.0,"standard_unload_time":2.0},"extruder_change_time":5.0,"filament_change_time":2.0,"filament_print_time":{},"group_with_time":false}},"metadata":{"config_type":"B","id":"B_constraint_103","seed":20103}}

View File

@@ -0,0 +1 @@
{"base_result":{"constraints_ok":true,"flush_cost":9750,"full_score":4926.6},"context":{"group_info":{"filament_volume_map":[2,2,2,2],"has_filament_switcher":false,"ignore_ext_filament":false,"max_gap_threshold":0.01,"mode":0,"strategy":0,"total_filament_num":4},"machine_info":{"machine_filament_info":[[{"color":"#037005FF","extruder_id":0,"is_extended":false,"is_support":false,"type":"PLA","usage_type":1},{"color":"#D87849FF","extruder_id":0,"is_extended":false,"is_support":false,"type":"PLA","usage_type":1},{"color":"#F3CE18FF","extruder_id":0,"is_extended":false,"is_support":false,"type":"PLA","usage_type":1},{"color":"#BCFEA7FF","extruder_id":0,"is_extended":false,"is_support":false,"type":"PLA","usage_type":1}],[{"color":"#924B26FF","extruder_id":1,"is_extended":false,"is_support":false,"type":"PLA","usage_type":1},{"color":"#3A216BFF","extruder_id":1,"is_extended":false,"is_support":false,"type":"PLA","usage_type":1},{"color":"#D0924FFF","extruder_id":1,"is_extended":false,"is_support":false,"type":"PLA","usage_type":1},{"color":"#A345ADFF","extruder_id":1,"is_extended":false,"is_support":false,"type":"PLA","usage_type":1}]],"master_extruder_id":0,"max_group_size":[4,4],"prefer_non_model_filament":[false,false]},"model_info":{"filament_ids":["GFL_0","GFL_1","GFL_2","GFL_3"],"filament_info":[{"color":"#7FA1F1FF","is_support":false,"type":"PLA","usage_type":1},{"color":"#5864ACFF","is_support":false,"type":"TPU","usage_type":1},{"color":"#F36945FF","is_support":false,"type":"PA","usage_type":1},{"color":"#F02F44FF","is_support":false,"type":"PLA","usage_type":1}],"flush_matrix":[[[0.0,500.582275390625,562.5870971679688,81.93852233886719],[51.72758865356445,0.0,90.24585723876953,490.09320068359375],[430.67327880859375,232.1397247314453,0.0,146.47518920898438],[161.84280395507813,211.6325225830078,363.18682861328125,0.0]],[[0.0,554.25244140625,69.40193176269531,157.41722106933594],[122.196533203125,0.0,229.16275024414063,209.3227996826172],[192.6871337890625,78.21271514892578,0.0,35.126708984375],[227.9981689453125,364.66082763671875,66.126220703125,0.0]]],"layer_filaments":[[0,1,2],[0,1,2],[0,1,2,3],[0,1,2,3],[0,3],[0,3],[0,2],[0,1,3],[3],[1],[0],[2,3],[2],[2],[0,1,3],[0,1,3],[1,2,3],[0,1,2,3],[0,1,2,3],[0,2,3],[0,1,2],[0,1,2],[0,2],[0,2],[2],[2],[2],[0,2],[0,1,2],[0,1,2],[0,1,2],[0,1,2],[0],[2],[0,2],[0,2],[2],[2,3],[1,2],[1],[0],[0],[0,1,3],[1],[1],[0,2,3],[1,2],[1],[1],[2],[2],[1,2],[1,2],[1],[1,2],[1,2],[1,3],[3],[3],[1],[1],[0,1],[0,1],[0],[0,1],[0,1],[0],[1,2,3],[0,1,3],[0,1,2],[0,1],[1,2,3],[2],[2],[2],[2],[2],[1],[2,3],[1,2],[0],[0],[0],[0,2],[0],[0],[0],[0,2],[0,1,3],[0,1,3],[0,1,3],[1,3],[1],[0],[0],[0],[0,3],[0,3],[0],[0],[0],[1],[1,2],[1,2],[2],[0,2],[0,2],[0,2],[0,2],[1],[1],[1],[1],[1,2],[1],[1],[3],[0,3],[0,3],[0,2,3],[2,3],[2,3],[2],[2,3],[2,3],[3],[3],[1],[1,2],[0,1,2],[2],[2],[3],[3],[0,1,2],[0,1,2],[0,2],[0,2],[0],[0],[0],[1,2,3],[1,2,3],[1,2,3],[0,1,2],[0,1],[0,1],[1,2],[1,2],[0,1,2],[0,1,2],[1,2,3],[1,2,3],[1,2,3]],"unprintable_filaments":[[0],[2]],"unprintable_volumes":{"1":[1],"2":[1],"3":[1]}},"nozzle_info":{"extruder_nozzle_list":{"0":[0],"1":[1,2]},"nozzle_list":[{"diameter":"0.4","extruder_id":0,"group_id":0,"volume_type":0},{"diameter":"0.4","extruder_id":1,"group_id":1,"volume_type":0},{"diameter":"0.4","extruder_id":1,"group_id":2,"volume_type":1}],"nozzle_status":{}},"speed_info":{"ams_preload_enabled":[true,true],"change_time_params":{"selector_load_time":1.0,"selector_unload_time":1.0,"standard_load_time":3.0,"standard_unload_time":2.0},"extruder_change_time":5.0,"filament_change_time":2.0,"filament_print_time":{},"group_with_time":false}},"metadata":{"config_type":"B","id":"B_constraint_88","seed":20088}}

View File

@@ -0,0 +1 @@
{"base_result":{"constraints_ok":true,"flush_cost":3367,"full_score":2146.2712},"context":{"group_info":{"filament_volume_map":[2,2,2,2,2],"has_filament_switcher":false,"ignore_ext_filament":false,"max_gap_threshold":0.01,"mode":0,"strategy":0,"total_filament_num":5},"machine_info":{"machine_filament_info":[[{"color":"#2A5557FF","extruder_id":0,"is_extended":false,"is_support":false,"type":"PLA","usage_type":1},{"color":"#995701FF","extruder_id":0,"is_extended":false,"is_support":false,"type":"PLA","usage_type":1},{"color":"#CBB5F1FF","extruder_id":0,"is_extended":false,"is_support":false,"type":"PLA","usage_type":1},{"color":"#E09394FF","extruder_id":0,"is_extended":false,"is_support":false,"type":"PLA","usage_type":1}],[{"color":"#38D65DFF","extruder_id":1,"is_extended":false,"is_support":false,"type":"PLA","usage_type":1},{"color":"#A65DE8FF","extruder_id":1,"is_extended":false,"is_support":false,"type":"PLA","usage_type":1},{"color":"#D6E92AFF","extruder_id":1,"is_extended":false,"is_support":false,"type":"PLA","usage_type":1},{"color":"#21A474FF","extruder_id":1,"is_extended":false,"is_support":false,"type":"PLA","usage_type":1}]],"master_extruder_id":0,"max_group_size":[4,6],"prefer_non_model_filament":[false,false]},"model_info":{"filament_ids":["GFL_0","GFL_1","GFL_2","GFL_3","GFL_4"],"filament_info":[{"color":"#341F11FF","is_support":false,"type":"ABS","usage_type":1},{"color":"#58D9A4FF","is_support":false,"type":"PETG","usage_type":1},{"color":"#11D7FFFF","is_support":true,"type":"PLA-S","usage_type":0},{"color":"#756DEFFF","is_support":true,"type":"PLA-S","usage_type":0},{"color":"#D28F74FF","is_support":false,"type":"TPU","usage_type":1}],"flush_matrix":[[[0.0,40.422149658203125,281.1308898925781,420.726318359375,245.7325439453125],[564.1063842773438,0.0,80.42797088623047,132.09776306152344,33.372379302978516],[29.10088539123535,327.1266784667969,0.0,318.0596923828125,352.4109191894531],[79.35222625732422,349.1927795410156,450.20599365234375,0.0,473.4333190917969],[84.93012237548828,343.9147033691406,480.2047119140625,440.6584167480469,0.0]],[[0.0,203.69725036621094,148.45620727539063,208.9656524658203,217.2571258544922],[170.0343780517578,0.0,516.4771728515625,587.4338989257813,442.1988830566406],[37.863525390625,265.5264892578125,0.0,318.85748291015625,227.68455505371094],[257.746826171875,217.14395141601563,120.68830108642578,0.0,310.3852233886719],[327.35687255859375,328.03076171875,271.71405029296875,420.61297607421875,0.0]]],"layer_filaments":[[1,2],[1],[1],[1],[1],[0,1],[1],[2],[1],[1,3],[4],[1],[4],[4],[4],[4],[2],[2,4],[4],[4],[2],[1,2],[1,2],[1],[0],[0],[0],[0,3],[2],[2],[2,3],[2],[2],[2],[2],[2],[2],[2],[0],[0],[2],[3],[4],[4],[2],[2],[2],[2],[2],[2],[2,3],[2,3],[2,3],[0,2,3],[1,3],[1,3,4],[1,3,4],[1,3,4],[3],[0],[1],[1],[1],[1,4],[1,4],[1,4],[0,1,4],[0,1,3,4],[0,1,3],[0,3],[0,3],[2,3,4],[2,4],[2],[2],[3],[1,3],[1,3,4],[1,2,3,4],[0,1,2,3,4],[0,1,2,4],[2,3,4],[2,3,4],[1],[4],[1,4],[1,4],[3],[3],[3],[3],[3],[1,3],[1,3,4],[1,3,4],[1,3],[0,1,3],[0,1,2,3],[0,2,3],[3],[1,3],[0,3,4],[2,3,4],[2,4],[2,3,4],[2,3,4],[0,2,3,4],[0,2,3,4],[0,1,2,3,4],[0,3,4],[0,3,4],[0],[0],[0,4],[4],[0,2],[0,2],[2],[2],[2],[0,2,3],[0,2,3],[0,1,2,3],[1,2,3],[2,3],[2,3],[0,2],[0,2,3],[2,3,4],[0,2,3],[0,1,2],[0,1,2],[0,1,2],[0,1,2],[1,2],[2],[2],[0,2],[0,2],[0,2,3],[0,2,3],[0,3],[0]],"unprintable_filaments":[[3],[]],"unprintable_volumes":{"3":[3],"4":[0]}},"nozzle_info":{"extruder_nozzle_list":{"0":[0],"1":[1,2,3]},"nozzle_list":[{"diameter":"0.4","extruder_id":0,"group_id":0,"volume_type":0},{"diameter":"0.4","extruder_id":1,"group_id":1,"volume_type":0},{"diameter":"0.4","extruder_id":1,"group_id":2,"volume_type":3},{"diameter":"0.4","extruder_id":1,"group_id":3,"volume_type":1}],"nozzle_status":{}},"speed_info":{"ams_preload_enabled":[true,true],"change_time_params":{"selector_load_time":1.0,"selector_unload_time":1.0,"standard_load_time":3.0,"standard_unload_time":2.0},"extruder_change_time":5.0,"filament_change_time":2.0,"filament_print_time":{},"group_with_time":false}},"metadata":{"config_type":"B","id":"B_constraint_97","seed":20097}}

View File

@@ -0,0 +1 @@
{"base_result":{"constraints_ok":true,"flush_cost":3336,"full_score":2002.2096},"context":{"group_info":{"filament_volume_map":[2,2,2,2],"has_filament_switcher":false,"ignore_ext_filament":false,"max_gap_threshold":0.01,"mode":0,"strategy":1,"total_filament_num":4},"machine_info":{"machine_filament_info":[[{"color":"#28AC58FF","extruder_id":0,"is_extended":false,"is_support":false,"type":"PLA","usage_type":1},{"color":"#412846FF","extruder_id":0,"is_extended":false,"is_support":false,"type":"PLA","usage_type":1},{"color":"#733049FF","extruder_id":0,"is_extended":false,"is_support":false,"type":"PLA","usage_type":1},{"color":"#0AC1B4FF","extruder_id":0,"is_extended":false,"is_support":false,"type":"PLA","usage_type":1}],[{"color":"#A402D9FF","extruder_id":1,"is_extended":false,"is_support":false,"type":"PLA","usage_type":1},{"color":"#0E94B8FF","extruder_id":1,"is_extended":false,"is_support":false,"type":"PLA","usage_type":1},{"color":"#3F2B52FF","extruder_id":1,"is_extended":false,"is_support":false,"type":"PLA","usage_type":1},{"color":"#CC874FFF","extruder_id":1,"is_extended":false,"is_support":false,"type":"PLA","usage_type":1}]],"master_extruder_id":0,"max_group_size":[4,4],"prefer_non_model_filament":[false,false]},"model_info":{"filament_ids":["GFL_0","GFL_1","GFL_2","GFL_3"],"filament_info":[{"color":"#2739B9FF","is_support":false,"type":"PETG","usage_type":1},{"color":"#103A81FF","is_support":false,"type":"PA","usage_type":1},{"color":"#4B971AFF","is_support":false,"type":"PETG","usage_type":1},{"color":"#82A327FF","is_support":false,"type":"PLA","usage_type":1}],"flush_matrix":[[[0.0,160.58099365234375,186.70541381835938,293.4424133300781],[452.10089111328125,0.0,427.1346435546875,482.00482177734375],[26.20694351196289,235.17530822753906,0.0,410.3768615722656],[269.29888916015625,244.5442657470703,496.617919921875,0.0]],[[0.0,409.6128845214844,39.27631378173828,447.314697265625],[520.747314453125,0.0,143.31735229492188,21.33603286743164],[532.3141479492188,215.94216918945313,0.0,394.37481689453125],[321.899169921875,174.60623168945313,109.6452407836914,0.0]]],"layer_filaments":[[1,2,3],[1],[0,1],[0,1],[0,1],[0,1,2],[2],[0],[0],[2],[2],[0,3],[0,3],[0,2,3],[0,2,3],[0,2,3],[0,1,2,3],[0,1,2],[0,2],[0,2],[0,2],[1,2],[2],[2],[2],[2],[1,2],[1,2,3],[1,2,3],[0,2,3],[2,3],[3],[3],[3],[2,3],[2,3],[1,2],[0,2],[0,2],[2],[2],[2],[0,2],[2],[2],[0,1],[0,1],[1],[1],[1],[1],[1,2],[1],[0,2],[0,2,3],[2,3],[3],[0],[0,3],[3],[3],[2,3],[2,3],[2,3],[0,1],[0,1],[1],[1,3],[0,1,3],[0,1],[0,1,2],[0,1,2],[0,3],[0],[2],[2,3],[0,2,3],[0,1],[1],[2],[0],[0,2],[0,2],[2],[0,3],[0,1],[0,1],[0],[0,3],[0,2],[0],[0,1],[1],[1],[1],[0,1],[0],[0],[0,2,3],[3],[3],[3],[0,2],[0,2],[0,1],[0],[0],[0,1],[0,1,2],[0,2],[0,2],[0,1,2],[2,3],[2,3],[2,3],[3],[1,3],[1,3],[3],[1],[0,1,3],[0,3],[0,3],[0,3],[3],[3],[1],[0,1],[0,1],[0,1,2],[0,1],[0,1],[0,1,2],[0,1,2,3],[0,2,3],[3],[3],[3],[3],[1,2,3],[1,3],[1,2,3],[1,2,3],[1,2],[1,2],[1,2],[1],[1,2],[0,1,2],[2,3],[2,3],[3],[3],[0],[0,1],[0,1],[0,1],[0,2],[0,3],[2,3],[2,3],[2,3],[2],[2],[2],[0],[0,3],[0],[0],[0]],"unprintable_filaments":[[],[]],"unprintable_volumes":{}},"nozzle_info":{"extruder_nozzle_list":{"0":[0],"1":[1,2]},"nozzle_list":[{"diameter":"0.4","extruder_id":0,"group_id":0,"volume_type":0},{"diameter":"0.4","extruder_id":1,"group_id":1,"volume_type":0},{"diameter":"0.4","extruder_id":1,"group_id":2,"volume_type":0}],"nozzle_status":{}},"speed_info":{"ams_preload_enabled":[true,true],"change_time_params":{"selector_load_time":1.0,"selector_unload_time":1.0,"standard_load_time":3.0,"standard_unload_time":2.0},"extruder_change_time":5.0,"filament_change_time":2.0,"filament_print_time":{},"group_with_time":false}},"metadata":{"config_type":"B","id":"B_mode_bestfit_162","seed":20162}}

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1 @@
{"base_result":{"constraints_ok":true,"flush_cost":5010,"full_score":2616.536},"context":{"group_info":{"filament_volume_map":[2,2,2,2,2],"has_filament_switcher":false,"ignore_ext_filament":false,"max_gap_threshold":0.01,"mode":0,"strategy":0,"total_filament_num":5},"machine_info":{"machine_filament_info":[[{"color":"#A7BDFDFF","extruder_id":0,"is_extended":false,"is_support":false,"type":"PLA","usage_type":1},{"color":"#E4B564FF","extruder_id":0,"is_extended":false,"is_support":false,"type":"PLA","usage_type":1},{"color":"#64E4F7FF","extruder_id":0,"is_extended":false,"is_support":false,"type":"PLA","usage_type":1},{"color":"#79AE21FF","extruder_id":0,"is_extended":false,"is_support":false,"type":"PLA","usage_type":1}],[{"color":"#BB3A10FF","extruder_id":1,"is_extended":false,"is_support":false,"type":"PLA","usage_type":1},{"color":"#342AAEFF","extruder_id":1,"is_extended":false,"is_support":false,"type":"PLA","usage_type":1},{"color":"#1B853FFF","extruder_id":1,"is_extended":false,"is_support":false,"type":"PLA","usage_type":1},{"color":"#C98C73FF","extruder_id":1,"is_extended":false,"is_support":false,"type":"PLA","usage_type":1}]],"master_extruder_id":0,"max_group_size":[4,4],"prefer_non_model_filament":[false,false]},"model_info":{"filament_ids":["GFL_0","GFL_1","GFL_2","GFL_3","GFL_4"],"filament_info":[{"color":"#B0F63DFF","is_support":false,"type":"TPU","usage_type":1},{"color":"#053F79FF","is_support":false,"type":"ABS","usage_type":1},{"color":"#2236EAFF","is_support":true,"type":"PLA-S","usage_type":0},{"color":"#3E1A46FF","is_support":true,"type":"PLA-S","usage_type":0},{"color":"#E193E6FF","is_support":false,"type":"PETG","usage_type":1}],"flush_matrix":[[[0.0,357.560302734375,158.03790283203125,78.87968444824219,56.35820388793945],[312.62969970703125,0.0,578.679443359375,354.6175537109375,333.78289794921875],[385.2695007324219,297.87310791015625,0.0,18.705421447753906,101.41615295410156],[106.06452941894531,286.4847412109375,112.56126403808594,0.0,244.12152099609375],[234.8699951171875,51.119590759277344,95.25473022460938,578.6362915039063,0.0]],[[0.0,354.75567626953125,51.230716705322266,514.29296875,451.8372802734375],[30.5628719329834,0.0,426.8621520996094,304.156005859375,114.46688079833984],[305.9984436035156,567.757568359375,0.0,254.84019470214844,480.94757080078125],[302.3028259277344,503.4031982421875,186.15228271484375,0.0,498.27532958984375],[198.755859375,412.3115234375,207.9266815185547,364.662841796875,0.0]]],"layer_filaments":[[1,2,4],[1,2],[1,2],[1,2],[1],[4],[4],[4],[0,4],[0,4],[1,2],[2],[2],[0,1,4],[0,1,2],[0,1,2],[0,2],[0],[0,4],[0,3,4],[0,2,3,4],[2,4],[0,2,4],[0,1,4],[0,1,2],[0,2],[0,2,3],[0,3],[0],[0],[2],[2],[2],[0,2],[0,1],[1],[1,2],[1],[1,3],[1,3],[3],[4],[0,3,4],[0,3,4],[4],[4],[4],[4],[4],[4],[2,4],[3],[3],[0],[4],[2,4],[2],[1],[4],[4],[4],[2,3,4],[1,2,4],[2,4],[3,4],[1],[1],[2],[2,3],[2,4],[2,4],[2,4],[2,4],[2],[2],[2],[2],[2],[2],[0],[0],[0,4],[0],[0],[1],[2],[1,2,4],[1,2],[1,2],[1],[1],[1],[1],[0],[0,4],[4],[4],[4],[1],[1],[1],[0,1],[0,1],[0,1],[0,3],[3],[0,1,4],[0],[4],[2],[2],[2],[0],[2],[2],[2],[2],[2,3],[0,2,4],[4],[4],[1,4],[4]],"unprintable_filaments":[[],[]],"unprintable_volumes":{}},"nozzle_info":{"extruder_nozzle_list":{"0":[0],"1":[1,2]},"nozzle_list":[{"diameter":"0.4","extruder_id":0,"group_id":0,"volume_type":0},{"diameter":"0.4","extruder_id":1,"group_id":1,"volume_type":0},{"diameter":"0.4","extruder_id":1,"group_id":2,"volume_type":0}],"nozzle_status":{}},"speed_info":{"ams_preload_enabled":[true,true],"change_time_params":{"selector_load_time":1.0,"selector_unload_time":1.0,"standard_load_time":3.0,"standard_unload_time":2.0},"extruder_change_time":5.0,"filament_change_time":2.0,"filament_print_time":{},"group_with_time":true}},"metadata":{"config_type":"B","id":"B_mode_time_168","seed":20168}}

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1 @@
{"base_result":{"constraints_ok":true,"flush_cost":0,"full_score":533.0},"context":{"group_info":{"filament_volume_map":[2,2,2],"has_filament_switcher":false,"ignore_ext_filament":false,"max_gap_threshold":0.01,"mode":0,"strategy":0,"total_filament_num":3},"machine_info":{"machine_filament_info":[[{"color":"#FD981AFF","extruder_id":0,"is_extended":false,"is_support":false,"type":"PLA","usage_type":1},{"color":"#6618F5FF","extruder_id":0,"is_extended":false,"is_support":false,"type":"PLA","usage_type":1},{"color":"#BBA143FF","extruder_id":0,"is_extended":false,"is_support":false,"type":"PLA","usage_type":1},{"color":"#26550AFF","extruder_id":0,"is_extended":false,"is_support":false,"type":"PLA","usage_type":1}]],"master_extruder_id":0,"max_group_size":[3],"prefer_non_model_filament":[false]},"model_info":{"filament_ids":["GFL_0","GFL_1","GFL_2"],"filament_info":[{"color":"#911BD7FF","is_support":false,"type":"TPU","usage_type":1},{"color":"#89C1C0FF","is_support":false,"type":"PA","usage_type":1},{"color":"#C64EE6FF","is_support":false,"type":"ABS","usage_type":1}],"flush_matrix":[[[0.0,64.12446594238281,186.0951385498047],[482.5750427246094,0.0,78.35060119628906],[161.81735229492188,233.08798217773438,0.0]]],"layer_filaments":[[0,1],[0,1],[0],[0],[0],[0],[0,2],[1,2],[1,2],[0],[0,2],[0,1],[0],[0,2],[0,2],[2],[2],[1],[0,2],[0,2],[0],[0,2],[2],[2],[2],[0],[0],[0,1],[0,1],[0,2],[1,2],[1,2],[0,1],[0],[0],[0],[0,2],[0,1,2],[1,2],[0],[0],[0],[0],[2],[0,2],[0,2],[1,2],[1,2],[0],[0],[2],[2],[0],[0],[2],[2],[0,2],[0,2],[2],[1,2],[1,2],[1,2],[2],[2],[1],[1],[1],[1],[1],[0,1],[0],[0],[0,2],[0,2],[0,2],[0],[0],[0,2],[1,2],[1,2],[1,2],[0,2],[1,2],[1,2],[2],[2],[2],[0,1],[0,1],[1],[1],[0],[0,2],[0,2],[0,2],[0,2],[0,2],[0,1],[0],[0],[0,1],[0,1],[1],[1,2],[1],[1],[0],[0,2],[0,2],[0,2],[0,2],[0],[1],[1],[0,1],[0,1],[1,2],[0,1],[0,1],[1],[0],[0,2],[0,2],[0],[0],[0],[0],[0,2],[2],[0],[1],[0,2],[0,2],[0,1,2],[0,2],[0,2],[0,2],[1,2],[2],[2],[1]],"unprintable_filaments":[[]],"unprintable_volumes":{}},"nozzle_info":{"extruder_nozzle_list":{"0":[0,1,2]},"nozzle_list":[{"diameter":"0.4","extruder_id":0,"group_id":0,"volume_type":0},{"diameter":"0.4","extruder_id":0,"group_id":1,"volume_type":1},{"diameter":"0.4","extruder_id":0,"group_id":2,"volume_type":2}],"nozzle_status":{}},"speed_info":{"ams_preload_enabled":[true],"change_time_params":{"selector_load_time":1.0,"selector_unload_time":1.0,"standard_load_time":3.0,"standard_unload_time":2.0},"extruder_change_time":5.0,"filament_change_time":2.0,"filament_print_time":{},"group_with_time":false}},"metadata":{"config_type":"C","id":"C_basic_3","seed":30003}}

View File

@@ -0,0 +1 @@
{"base_result":{"constraints_ok":true,"flush_cost":0,"full_score":608.0},"context":{"group_info":{"filament_volume_map":[2,2,2,2,2],"has_filament_switcher":false,"ignore_ext_filament":false,"max_gap_threshold":0.01,"mode":0,"strategy":0,"total_filament_num":5},"machine_info":{"machine_filament_info":[[{"color":"#93FE0AFF","extruder_id":0,"is_extended":false,"is_support":false,"type":"PLA","usage_type":1},{"color":"#FE6A12FF","extruder_id":0,"is_extended":false,"is_support":false,"type":"PLA","usage_type":1},{"color":"#E78D3BFF","extruder_id":0,"is_extended":false,"is_support":false,"type":"PLA","usage_type":1},{"color":"#A20CB0FF","extruder_id":0,"is_extended":false,"is_support":false,"type":"PLA","usage_type":1}]],"master_extruder_id":0,"max_group_size":[5],"prefer_non_model_filament":[false]},"model_info":{"filament_ids":["GFL_0","GFL_1","GFL_2","GFL_3","GFL_4"],"filament_info":[{"color":"#C51507FF","is_support":false,"type":"ABS","usage_type":1},{"color":"#D366B2FF","is_support":false,"type":"ABS","usage_type":1},{"color":"#D07D0EFF","is_support":false,"type":"PLA","usage_type":1},{"color":"#E4C00AFF","is_support":true,"type":"PLA-S","usage_type":0},{"color":"#12B359FF","is_support":false,"type":"PETG","usage_type":1}],"flush_matrix":[[[0.0,274.9917297363281,455.0113830566406,551.6867065429688,562.2197265625],[345.1457214355469,0.0,337.8075866699219,484.70074462890625,465.59490966796875],[332.17742919921875,481.7458801269531,0.0,339.1631164550781,394.23785400390625],[317.8833923339844,417.9119873046875,523.1845092773438,0.0,164.09495544433594],[455.5447692871094,228.3113555908203,309.9906311035156,487.07562255859375,0.0]]],"layer_filaments":[[1,3,4],[1,2],[1,2,4],[2,4],[2,4],[2,4],[3,4],[3,4],[1,3,4],[0,3,4],[0,1,3,4],[0,1,3,4],[0,1,2,3,4],[1,2],[1],[0],[4],[2],[2],[2,4],[0,2],[0,2,3],[1,2],[1],[1],[1,4],[4],[0,4],[1],[1],[1],[1,2],[0,3,4],[1,2,3],[1,2],[0,1,2],[0,1,2],[0,1,2],[2,4],[2,4],[2],[2],[1],[1],[0],[1,3,4],[0,1,3,4],[0,1,2,3],[0,1,3,4],[0,1,2],[0,2],[0,2],[2],[2],[2,3],[2,3],[0,1,4],[0,4],[0,4],[0,4],[0,2,4],[0,4],[0],[0,2],[0,2],[0,2],[0,2],[0],[0],[2],[2],[2],[0,2],[0,2],[0,2],[0],[2],[2],[2],[2],[1],[0],[0],[0],[0],[2],[2,3],[3,4],[3],[3],[0,2],[4],[0],[0],[2,3],[2,3],[2,3],[2],[1,2,4],[1,2],[1,2],[1,2,3],[2,3],[0,3],[0],[0,3],[0,2,3],[0,2],[2],[2],[2],[2],[1],[1],[1]],"unprintable_filaments":[[]],"unprintable_volumes":{}},"nozzle_info":{"extruder_nozzle_list":{"0":[0,1,2,3,4]},"nozzle_list":[{"diameter":"0.4","extruder_id":0,"group_id":0,"volume_type":0},{"diameter":"0.4","extruder_id":0,"group_id":1,"volume_type":1},{"diameter":"0.4","extruder_id":0,"group_id":2,"volume_type":2},{"diameter":"0.4","extruder_id":0,"group_id":3,"volume_type":3},{"diameter":"0.4","extruder_id":0,"group_id":4,"volume_type":0}],"nozzle_status":{}},"speed_info":{"ams_preload_enabled":[true],"change_time_params":{"selector_load_time":1.0,"selector_unload_time":1.0,"standard_load_time":3.0,"standard_unload_time":2.0},"extruder_change_time":5.0,"filament_change_time":2.0,"filament_print_time":{},"group_with_time":false}},"metadata":{"config_type":"C","id":"C_basic_33","seed":30033}}

View File

@@ -0,0 +1 @@
{"base_result":{"constraints_ok":true,"flush_cost":8700,"full_score":4679.32},"context":{"group_info":{"filament_volume_map":[2,2,2,2],"has_filament_switcher":false,"ignore_ext_filament":false,"max_gap_threshold":0.01,"mode":0,"strategy":0,"total_filament_num":4},"machine_info":{"machine_filament_info":[[{"color":"#D04D20FF","extruder_id":0,"is_extended":false,"is_support":false,"type":"PLA","usage_type":1},{"color":"#408006FF","extruder_id":0,"is_extended":false,"is_support":false,"type":"PLA","usage_type":1},{"color":"#A31F43FF","extruder_id":0,"is_extended":false,"is_support":false,"type":"PLA","usage_type":1},{"color":"#846951FF","extruder_id":0,"is_extended":false,"is_support":false,"type":"PLA","usage_type":1}]],"master_extruder_id":0,"max_group_size":[4],"prefer_non_model_filament":[false]},"model_info":{"filament_ids":["GFL_0","GFL_1","GFL_2","GFL_3"],"filament_info":[{"color":"#92C1F9FF","is_support":false,"type":"PLA","usage_type":1},{"color":"#B0D9AAFF","is_support":false,"type":"PA","usage_type":1},{"color":"#5855D4FF","is_support":false,"type":"TPU","usage_type":1},{"color":"#2CEAEAFF","is_support":false,"type":"PLA","usage_type":1}],"flush_matrix":[[[0.0,263.98394775390625,335.4018249511719,562.2523193359375],[514.525390625,0.0,491.3005676269531,28.14336585998535],[562.559814453125,351.77215576171875,0.0,321.54388427734375],[304.2557067871094,320.45123291015625,470.2841796875,0.0]]],"layer_filaments":[[2,3],[3],[3],[3],[1,3],[1,2,3],[0,1,2],[2],[2],[2],[2],[2],[2],[1],[1,2],[0,2],[0,2],[0,2],[1],[1],[1],[1],[1],[1],[1,3],[1,3],[1,3],[0],[0],[0],[0],[0],[3],[3],[3],[1,3],[1,3],[2],[2],[0,2],[2,3],[2,3],[3],[3],[1,2],[1,3],[1,3],[1,3],[0,1,3],[1,3],[3],[3],[3],[0,3],[0,1,2],[2],[2],[3],[3],[2,3],[1,2,3],[1,2,3],[1],[1],[1,2],[1,2],[1,2],[1,2],[2],[2,3],[2,3],[3],[0,2,3],[0,2,3],[0,1,2,3],[0,1,2,3],[1,2,3],[1,2,3],[1,2,3],[1,2,3],[0,1,3],[0,1,3],[0,1,3],[0,1,2,3],[2,3],[2,3],[2],[2],[0,2],[0,2],[0,2],[0,1,2],[1,2],[2,3],[2,3],[2],[2],[0,1],[0],[1,3],[2,3],[2],[2,3],[3],[3],[3],[0,2],[0,2],[0,2],[0,2],[0,1,2],[0,1,2],[0,1,2],[0,2,3],[1,3],[1,3],[1,3],[0,1],[0,1],[1,3],[3],[2,3],[3],[3],[0,3],[0],[0],[0,3],[0,1],[0,1],[0,1],[1,3],[1,3],[3],[3],[3],[2,3],[2],[2],[1],[1],[0,1,2],[0,1,3],[2,3],[2,3],[3],[1,3],[1,3],[0,1,3],[0,2],[1,3]],"unprintable_filaments":[[]],"unprintable_volumes":{"2":[1],"3":[1]}},"nozzle_info":{"extruder_nozzle_list":{"0":[0,1,2]},"nozzle_list":[{"diameter":"0.4","extruder_id":0,"group_id":0,"volume_type":0},{"diameter":"0.4","extruder_id":0,"group_id":1,"volume_type":1},{"diameter":"0.4","extruder_id":0,"group_id":2,"volume_type":2}],"nozzle_status":{}},"speed_info":{"ams_preload_enabled":[true],"change_time_params":{"selector_load_time":1.0,"selector_unload_time":1.0,"standard_load_time":3.0,"standard_unload_time":2.0},"extruder_change_time":5.0,"filament_change_time":2.0,"filament_print_time":{},"group_with_time":false}},"metadata":{"config_type":"C","id":"C_constraint_85","seed":30085}}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -56,8 +56,10 @@ struct NfpPlacerFixture {
} // namespace
TEST_CASE_METHOD(NfpPlacerFixture, "NfpPlacer places a single item inside the bin", "[Nesting][Placer]") {
NfpPlacer placer = placer_with();
// The placer only keeps references to the items it packs and re-reads them
// from finalAlign() in its destructor, so the item must outlive the placer.
RectangleItem item{100000000, 100000000};
NfpPlacer placer = placer_with();
REQUIRE(place(placer, item));
REQUIRE(placer.getItems().size() == 1u);
@@ -103,12 +105,15 @@ TEST_CASE_METHOD(NfpPlacerFixture, "NfpPlacer packs many items without overlap",
}
TEST_CASE_METHOD(NfpPlacerFixture, "NfpPlacer evaluates the rotation candidates", "[Nesting][Placer]") {
// The placer re-reads its packed items from finalAlign() in its destructor,
// so the items must outlive the placer — declare them first.
std::vector<RectangleItem> rects = {
{180000000, 40000000}, {180000000, 40000000}, {180000000, 40000000}};
Cfg cfg;
cfg.rotations = {0.0, Pi / 2.0}; // exercise the rotation search loop
NfpPlacer placer = placer_with(cfg);
std::vector<RectangleItem> rects = {
{180000000, 40000000}, {180000000, 40000000}, {180000000, 40000000}};
place_all(placer, rects);
require_disjoint_in_bin(rects);
}

View File

@@ -12,6 +12,8 @@ add_executable(${_TEST_NAME}_tests
test_clipper_offset.cpp
test_clipper_utils.cpp
test_config.cpp
test_config_variant_expansion.cpp
test_toolordering_nozzle_group.cpp
test_preset_bundle_loading.cpp
test_preset_setting_id.cpp
test_elephant_foot_compensation.cpp
@@ -20,6 +22,7 @@ add_executable(${_TEST_NAME}_tests
test_polygon.cpp
test_mutable_polygon.cpp
test_mutable_priority_queue.cpp
test_nozzle_volume_type.cpp
test_stl.cpp
test_meshboolean.cpp
test_marchingsquares.cpp

View File

@@ -1,7 +1,13 @@
#include "libslic3r/Model.hpp"
#include "libslic3r/Format/3mf.hpp"
#include "libslic3r/Format/bbs_3mf.hpp"
#include "libslic3r/Format/STL.hpp"
#include "libslic3r/PrintConfig.hpp"
#include "libslic3r/Semver.hpp"
#include "libslic3r/Preset.hpp"
#include "libslic3r/MultiNozzleUtils.hpp"
#include "libslic3r/ProjectTask.hpp"
#include <boost/filesystem/operations.hpp>
@@ -133,6 +139,376 @@ SCENARIO("Export+Import geometry to/from 3mf file cycle", "[3mf]") {
}
}
// .3mf multi-nozzle round-trip.
// Locks the load/save handling for the H2C multi-nozzle plate metadata:
// * filament_volume_maps -> plate config "filament_volume_map" (with the >1 -> 0 clamp)
// * nozzle_volume_type -> PlateData::nozzle_volume_types (previously write-only)
// and pins the deliberately-lossy keys (enable_filament_dynamic_map) so a future change has to
// consciously unpin them. Uses a store_bbs_3mf -> load_bbs_3mf cycle (no external fixture needed).
SCENARIO("H2C multi-nozzle .3mf round-trip", "[3mf][MultiNozzle]") {
GIVEN("a plate carrying multi-nozzle filament assignment metadata") {
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();
// 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);
// Global (printer) config: give nozzle_volume_type a non-default value so the slice_info
// read-back is a meaningful assertion (High Flow == 1).
DynamicPrintConfig config = DynamicPrintConfig::full_print_config();
config.set_key_value("nozzle_volume_type",
new ConfigOptionEnumsGeneric({ (int) NozzleVolumeType::nvtHighFlow }));
PlateData* plate = new PlateData();
plate->plate_index = 0;
plate->is_sliced_valid = true; // gate for the slice_info.config writer (nozzle_volume_type)
plate->filament_maps = { 1, 2, 1 }; // slice_info uses this; keep it == model_settings' value
plate->config.set_key_value("filament_map_mode", new ConfigOptionEnum<FilamentMapMode>(fmmManual));
plate->config.set_key_value("filament_map", new ConfigOptionInts({ 1, 2, 1 }));
// Deliberately include out-of-range volume-type ids (2 == Hybrid, 3 == TPU High Flow):
// the loader must clamp them back to Standard (0).
plate->config.set_key_value("filament_volume_map", new ConfigOptionInts({ 0, 2, 1, 3 }));
// Known-lossy: a true value must NOT survive the round-trip (slice_info hardcodes false,
// model_settings never writes it).
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";
StoreParams store_params;
store_params.path = test_file.c_str();
store_params.model = &model;
store_params.config = &config;
store_params.plate_data_list.push_back(plate);
store_params.strategy = SaveStrategy::Zip64 | SaveStrategy::Silence;
REQUIRE(store_bbs_3mf(store_params));
Model dst_model;
DynamicPrintConfig dst_config;
ConfigSubstitutionContext ctxt{ ForwardCompatibilitySubstitutionRule::Enable };
PlateDataPtrs dst_plates;
std::vector<Preset*> project_presets;
bool is_bbl_3mf = false, is_orca_3mf = false;
Semver file_version;
// LoadConfig is required for slice_info.config (nozzle_volume_type) to be parsed —
// matches how the app loads projects.
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);
PlateData* rt = dst_plates.front();
// filament_map (model_settings + slice_info; already round-tripped)
auto* fmap = rt->config.option<ConfigOptionInts>("filament_map");
REQUIRE(fmap != nullptr);
REQUIRE(fmap->values == std::vector<int>({ 1, 2, 1 }));
// filament_volume_map (model_settings) with the >1 -> 0 clamp
auto* fvmap = rt->config.option<ConfigOptionInts>("filament_volume_map");
REQUIRE(fvmap != nullptr);
REQUIRE(fvmap->values == std::vector<int>({ 0, 0, 1, 0 }));
// nozzle_volume_type read-back into PlateData::nozzle_volume_types
REQUIRE(rt->nozzle_volume_types == "1");
// enable_filament_dynamic_map pinned lossy: model_settings never serializes it and
// slice_info hardcodes false, so the `true` we set is dropped. Pinned here
// (absent or false, never true) so a future change that persists it must update this.
auto* dyn = rt->config.option<ConfigOptionBool>("enable_filament_dynamic_map");
const bool persisted_true = (dyn != nullptr && dyn->value);
REQUIRE_FALSE(persisted_true);
}
release_PlateData_list(dst_plates);
}
delete plate; // store_bbs_3mf does not take ownership of the source plate
boost::filesystem::remove_all(backup_dir);
}
}
// Saved nozzle diameter for a single-nozzle-per-extruder printer with a non-standard nozzle.
// The grouping result rounds every nozzle diameter to the nearest of {0.2,0.4,0.6,0.8} for its
// internal matching key. That rounded value must NOT reach the saved <filament>/<nozzle> metadata on
// a printer whose extruders each carry one nozzle: the exact per-extruder config diameter is written
// instead, so a 0.5 mm nozzle is preserved rather than saved as 0.4. (Only an extruder that carries a
// nozzle cluster, which the per-extruder config cannot express, keeps the grouping result's diameter.)
SCENARIO("Non-standard nozzle diameter survives .3mf save on a single-nozzle printer", "[3mf][MultiNozzle]") {
GIVEN("a single-extruder plate whose nozzle is 0.5 mm and whose stamped diameter was rounded to 0.4") {
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();
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);
// 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.
DynamicPrintConfig config = DynamicPrintConfig::full_print_config();
config.set_key_value("nozzle_diameter", new ConfigOptionFloats({ 0.5 }));
PlateData* plate = new PlateData();
plate->plate_index = 0;
plate->is_sliced_valid = true; // gate for the slice_info.config writer
plate->filament_maps = { 1 };
// Seed the stamped diameter with the grouping result's rounded value (0.5 -> 0.4) so the
// assertion proves the writer ignores it and emits the exact config diameter instead.
FilamentInfo fi;
fi.id = 0;
fi.type = "PLA";
fi.color = "#FFFFFFFF";
fi.group_id = { 0 };
fi.nozzle_diameter = 0.4; // rounded; must NOT be the value written
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";
StoreParams store_params;
store_params.path = test_file.c_str();
store_params.model = &model;
store_params.config = &config;
store_params.plate_data_list.push_back(plate);
store_params.strategy = SaveStrategy::Zip64 | SaveStrategy::Silence;
REQUIRE(store_bbs_3mf(store_params));
Model dst_model;
DynamicPrintConfig dst_config;
ConfigSubstitutionContext ctxt{ ForwardCompatibilitySubstitutionRule::Enable };
PlateDataPtrs dst_plates;
std::vector<Preset*> project_presets;
bool is_bbl_3mf = false, is_orca_3mf = false;
Semver file_version;
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);
PlateData* rt = dst_plates.front();
// <nozzle> tag: device-facing per-nozzle diameter string, written verbatim.
REQUIRE(rt->nozzles_info.size() >= 1);
REQUIRE(rt->nozzles_info.front().diameter == "0.5");
// <filament> tag: per-filament nozzle_diameter parsed back as 0.5, not 0.4.
REQUIRE(rt->slice_filaments_info.size() >= 1);
REQUIRE_THAT(rt->slice_filaments_info.front().nozzle_diameter, Catch::Matchers::WithinAbs(0.5, 1e-6));
}
release_PlateData_list(dst_plates);
}
delete plate; // store_bbs_3mf does not take ownership of the source plate
boost::filesystem::remove_all(backup_dir);
}
}
// A legacy / foreign project (no multi-nozzle metadata) must load crash-safe through the BBS
// importer and must not fabricate a filament_volume_map.
SCENARIO("Legacy project loads crash-safe via load_bbs_3mf", "[3mf][MultiNozzle]") {
GIVEN("a project without any multi-nozzle metadata") {
std::string path = std::string(TEST_DATA_DIR) + "/test_3mf/Geräte/Büchse.3mf";
Model model;
DynamicPrintConfig config;
ConfigSubstitutionContext ctxt{ ForwardCompatibilitySubstitutionRule::Enable };
PlateDataPtrs plates;
std::vector<Preset*> project_presets;
bool is_bbl_3mf = false, is_orca_3mf = false;
Semver file_version;
WHEN("loaded through the BBS importer") {
bool loaded = false;
REQUIRE_NOTHROW(loaded = load_bbs_3mf(path.c_str(), &config, &ctxt, &model, &plates,
&project_presets, &is_bbl_3mf, &is_orca_3mf,
&file_version, nullptr,
LoadStrategy::LoadModel | LoadStrategy::LoadConfig));
THEN("it does not crash and invents no per-filament volume map") {
for (PlateData* p : plates) {
REQUIRE(p->config.option<ConfigOptionInts>("filament_volume_map") == nullptr);
}
}
release_PlateData_list(plates);
}
}
}
// Device-side nozzle-grouping serialization surface.
// Direct unit coverage for the pure serialize/deserialize + StaticNozzleGroupResult helpers that the
// gcode.3mf writer/reader lean on.
SCENARIO("MultiNozzle serialization helpers", "[3mf][MultiNozzle]") {
using namespace Slic3r::MultiNozzleUtils;
GIVEN("NozzleInfo / NozzleGroupInfo") {
NozzleInfo n0; n0.group_id = 0; n0.extruder_id = 0; n0.diameter = "0.4"; n0.volume_type = nvtStandard;
NozzleInfo n1; n1.group_id = 1; n1.extruder_id = 1; n1.diameter = "0.4"; n1.volume_type = nvtHighFlow;
THEN("NozzleInfo::serialize matches the <nozzle> tag attributes (extruder_id 1-based)") {
REQUIRE(n0.serialize() == "id=\"0\" extruder_id=\"1\" nozzle_diameter=\"0.4\" volume_type=\"Standard\"");
REQUIRE(n1.serialize() == "id=\"1\" extruder_id=\"2\" nozzle_diameter=\"0.4\" volume_type=\"High Flow\"");
}
THEN("NozzleGroupInfo serialize/deserialize round-trips and rejects malformed input") {
NozzleGroupInfo g("0.4", nvtHighFlow, 1, 3);
REQUIRE(g.serialize() == "1-0.4-High Flow-3");
auto rt = NozzleGroupInfo::deserialize(g.serialize());
REQUIRE(rt.has_value());
REQUIRE(*rt == g);
REQUIRE_FALSE(NozzleGroupInfo::deserialize("1-0.4-Standard").has_value()); // too few tokens
REQUIRE_FALSE(NozzleGroupInfo::deserialize("x-0.4-Standard-3").has_value()); // non-numeric extruder
}
}
GIVEN("a StaticNozzleGroupResult built from filament + nozzle infos") {
std::vector<NozzleInfo> nozzles;
{ NozzleInfo n; n.group_id = 0; n.extruder_id = 0; n.diameter = "0.4"; n.volume_type = nvtStandard; nozzles.push_back(n); }
{ NozzleInfo n; n.group_id = 1; n.extruder_id = 1; n.diameter = "0.4"; n.volume_type = nvtHighFlow; nozzles.push_back(n); }
std::vector<FilamentInfo> filaments(3);
filaments[0].id = 0; filaments[0].group_id = { 0 };
filaments[1].id = 1; filaments[1].group_id = { 1 };
filaments[2].id = 2; filaments[2].group_id = { 0, 1 };
auto result = StaticNozzleGroupResult::create(filaments, nozzles, { 0, 1, 2 }, { 0, 1, 0 }, false);
REQUIRE(result.has_value());
THEN("filament->nozzle queries resolve to the stored mapping") {
REQUIRE(result->get_extruder_count() == 2);
REQUIRE(result->get_used_extruders() == std::vector<int>({ 0, 1 }));
REQUIRE(result->get_used_filaments() == std::vector<unsigned int>({ 0, 1, 2 }));
REQUIRE(result->get_nozzles_for_filament(0).size() == 1);
REQUIRE(result->get_nozzles_for_filament(2).size() == 2);
// first-use resolves through the (filament,nozzle) change sequences.
auto first = result->get_first_nozzle_for_filament(1);
REQUIRE(first.has_value());
REQUIRE(first->group_id == 1);
}
THEN("empty inputs yield nullopt") {
REQUIRE_FALSE(StaticNozzleGroupResult::create({}, nozzles, {}, {}, false).has_value());
REQUIRE_FALSE(StaticNozzleGroupResult::create(filaments, {}, {}, {}, false).has_value());
}
}
GIVEN("load_nozzle_infos_with_compatibility fallbacks") {
std::vector<NozzleInfo> new_format;
{ NozzleInfo n; n.group_id = 1; n.extruder_id = 1; n.diameter = "0.4"; n.volume_type = nvtHighFlow; new_format.push_back(n); }
{ NozzleInfo n; n.group_id = 0; n.extruder_id = 0; n.diameter = "0.4"; n.volume_type = nvtStandard; new_format.push_back(n); }
THEN("new-format <nozzle> tags are returned sorted by logical id") {
auto out = load_nozzle_infos_with_compatibility(new_format, {}, {}, {}, {});
REQUIRE(out.size() == 2);
REQUIRE(out[0].group_id == 0);
REQUIRE(out[1].group_id == 1);
}
THEN("oldest single-nozzle 3mf (no tags, no filament group_id) rebuilds from diameters/volume types") {
std::vector<NozzleVolumeType> vt = { nvtStandard, nvtHighFlow };
std::vector<double> dia = { 0.4, 0.4 };
auto out = load_nozzle_infos_with_compatibility({}, {}, {}, vt, dia);
REQUIRE(out.size() == 2);
REQUIRE(out[0].extruder_id == 0);
REQUIRE(out[0].volume_type == nvtStandard);
REQUIRE(out[1].volume_type == nvtHighFlow);
}
}
}
// The layer-aware grouping result must survive the gcode.3mf write/read as
// <nozzle> tags and the enable_filament_dynamic_map flag. Proves the parse_filament_info stamping,
// the NOZZLE_TAG writer, the _handle_config_nozzle reader, and the nozzles_info plate copy.
SCENARIO("Nozzle-group metadata .3mf round-trip", "[3mf][MultiNozzle]") {
GIVEN("a plate carrying a two-nozzle LayeredNozzleGroupResult") {
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();
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);
DynamicPrintConfig config = DynamicPrintConfig::full_print_config();
std::vector<MultiNozzleUtils::NozzleInfo> nozzles;
{ MultiNozzleUtils::NozzleInfo n; n.group_id = 0; n.extruder_id = 0; n.diameter = "0.4"; n.volume_type = NozzleVolumeType::nvtStandard; nozzles.push_back(n); }
{ MultiNozzleUtils::NozzleInfo n; n.group_id = 1; n.extruder_id = 1; n.diameter = "0.4"; n.volume_type = NozzleVolumeType::nvtHighFlow; nozzles.push_back(n); }
auto group = MultiNozzleUtils::LayeredNozzleGroupResult::create(
std::vector<int>{ 0, 1, 0 }, nozzles, std::vector<unsigned int>{ 0, 1, 2 });
REQUIRE(group.has_value());
PlateData* plate = new PlateData();
plate->plate_index = 0;
plate->is_sliced_valid = true;
plate->filament_maps = { 1, 2, 1 };
plate->nozzle_group_result = group;
plate->config.set_key_value("filament_map_mode", new ConfigOptionEnum<FilamentMapMode>(fmmManual));
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";
StoreParams store_params;
store_params.path = test_file.c_str();
store_params.model = &model;
store_params.config = &config;
store_params.plate_data_list.push_back(plate);
store_params.strategy = SaveStrategy::Zip64 | SaveStrategy::Silence;
REQUIRE(store_bbs_3mf(store_params));
Model dst_model;
DynamicPrintConfig dst_config;
ConfigSubstitutionContext ctxt{ ForwardCompatibilitySubstitutionRule::Enable };
PlateDataPtrs dst_plates;
std::vector<Preset*> project_presets;
bool is_bbl_3mf = false, is_orca_3mf = false;
Semver file_version;
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);
PlateData* rt = dst_plates.front();
REQUIRE(rt->nozzles_info.size() == 2);
// reader stores extruder_id 0-based (tag is 1-based), diameter/volume_type preserved.
std::sort(rt->nozzles_info.begin(), rt->nozzles_info.end());
REQUIRE(rt->nozzles_info[0].group_id == 0);
REQUIRE(rt->nozzles_info[0].extruder_id == 0);
REQUIRE(rt->nozzles_info[0].diameter == "0.4");
REQUIRE(rt->nozzles_info[0].volume_type == NozzleVolumeType::nvtStandard);
REQUIRE(rt->nozzles_info[1].group_id == 1);
REQUIRE(rt->nozzles_info[1].extruder_id == 1);
REQUIRE(rt->nozzles_info[1].volume_type == NozzleVolumeType::nvtHighFlow);
// A static (non-selector) result must persist enable_filament_dynamic_map = false.
auto* dyn = rt->config.option<ConfigOptionBool>("enable_filament_dynamic_map");
const bool persisted_true = (dyn != nullptr && dyn->value);
REQUIRE_FALSE(persisted_true);
}
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

View File

@@ -18,6 +18,7 @@
#include <catch2/catch_all.hpp>
#include "libslic3r/Arachne/WallToolPaths.hpp"
#include "libslic3r/Arachne/SkeletalTrapezoidation.hpp"
#include "libslic3r/Arachne/utils/ExtrusionLine.hpp"
#include "libslic3r/Arachne/BeadingStrategy/BeadingStrategyFactory.hpp"
#include "libslic3r/Arachne/BeadingStrategy/BeadingStrategy.hpp"
@@ -265,3 +266,46 @@ TEST_CASE("Arachne widening keeps two beads in transition band (#14376)", "[Arac
for (const coord_t w : beading.bead_widths)
CHECK(w <= inner_width);
}
namespace {
// Exposes the protected static interpolate() for a focused unit test.
struct InterpolateProbe : SkeletalTrapezoidation {
using SkeletalTrapezoidation::interpolate;
};
} // anonymous namespace
// interpolate() indexes the merged beading with an index derived from `left`. The merged beading
// follows the thicker of left/right, so when the thicker side has fewer insets the index runs past
// its end.
TEST_CASE("Beading interpolation tolerates a thicker side with fewer insets", "[Arachne][Regression]") {
using Beading = BeadingStrategy::Beading;
// Thicker side (right) has fewer insets, so the merged beading holds only 2 toolpath locations.
const coord_t w = scaled<coord_t>(0.42);
Beading left;
left.total_thickness = scaled<coord_t>(1.0);
left.bead_widths = { w, w, w, w };
left.toolpath_locations = { scaled<coord_t>(0.1), scaled<coord_t>(0.3), scaled<coord_t>(0.5), scaled<coord_t>(0.7) };
left.left_over = 0;
Beading right;
right.total_thickness = scaled<coord_t>(2.0);
right.bead_widths = { w, w };
right.toolpath_locations = { scaled<coord_t>(0.1), scaled<coord_t>(0.3) };
right.left_over = 0;
// Just past left's location [2] (0.5), so the derived index is 2, past the end of the 2-inset merged beading.
const coord_t switching_radius = scaled<coord_t>(0.6);
Beading result;
REQUIRE_NOTHROW(result = InterpolateProbe::interpolate(left, 0.5, right, switching_radius));
// With the guard the adjustment is skipped, so the result is the plain interpolation.
const Beading expected = InterpolateProbe::interpolate(left, 0.5, right);
REQUIRE(result.toolpath_locations.size() == expected.toolpath_locations.size());
REQUIRE(result.bead_widths.size() == expected.bead_widths.size());
for (size_t i = 0; i < expected.toolpath_locations.size(); ++i) {
CHECK(result.toolpath_locations[i] == expected.toolpath_locations[i]);
CHECK(result.bead_widths[i] == expected.bead_widths[i]);
}
}

View File

@@ -401,3 +401,247 @@ SCENARIO("update_diff_values_to_child_config tolerates legacy machine-limit vect
// }
// }
// }
TEST_CASE("H2C/A2L-era multi-nozzle and pre-heat config keys exist", "[config]") {
// Foundation keys backing H2C 6-nozzle cluster grouping, the pre-heat/pre-cool time
// model, and wipe-tower nozzle-change handling. Defaults must keep existing
// single-nozzle printers behaving identically.
Slic3r::DynamicPrintConfig config = Slic3r::DynamicPrintConfig::full_print_config();
// Printer / per-extruder options
REQUIRE(config.option<ConfigOptionIntsNullable>("extruder_max_nozzle_count") != nullptr);
REQUIRE(config.option<ConfigOptionIntsNullable>("extruder_max_nozzle_count")->values == std::vector<int>{1});
REQUIRE(config.option<ConfigOptionBool>("enable_pre_heating") != nullptr);
REQUIRE(config.option<ConfigOptionBool>("enable_pre_heating")->value == false);
REQUIRE(config.option<ConfigOptionFloatsNullable>("hotend_cooling_rate") != nullptr);
REQUIRE(config.option<ConfigOptionFloatsNullable>("hotend_heating_rate") != nullptr);
REQUIRE(config.option<ConfigOptionFloat>("machine_hotend_change_time") != nullptr);
REQUIRE(config.option<ConfigOptionFloat>("machine_prepare_compensation_time") != nullptr);
// Filament pre-cooling / ramming / nozzle-change (nc) options
REQUIRE(config.option<ConfigOptionIntsNullable>("filament_pre_cooling_temperature") != nullptr);
REQUIRE(config.option<ConfigOptionIntsNullable>("filament_pre_cooling_temperature_nc") != nullptr);
REQUIRE(config.option<ConfigOptionFloatsNullable>("filament_preheat_temperature_delta") != nullptr);
REQUIRE(config.option<ConfigOptionFloatsNullable>("filament_retract_length_nc") != nullptr);
REQUIRE(config.option<ConfigOptionFloats>("filament_change_length_nc") != nullptr);
REQUIRE(config.option<ConfigOptionFloats>("filament_prime_volume_nc") != nullptr);
REQUIRE(config.option<ConfigOptionFloatsNullable>("filament_ramming_travel_time") != nullptr);
REQUIRE(config.option<ConfigOptionFloatsNullable>("filament_ramming_travel_time_nc") != nullptr);
REQUIRE(config.option<ConfigOptionFloatsNullable>("filament_ramming_volumetric_speed") != nullptr);
REQUIRE(config.option<ConfigOptionFloatsNullable>("filament_ramming_volumetric_speed_nc") != nullptr);
// Spot-check defaults that must not alter existing behavior.
REQUIRE(config.option<ConfigOptionFloatsNullable>("filament_retract_length_nc")->values == std::vector<double>{10.});
REQUIRE(config.option<ConfigOptionFloats>("filament_prime_volume_nc")->values == std::vector<double>{60.});
REQUIRE(config.option<ConfigOptionIntsNullable>("filament_pre_cooling_temperature_nc")->values == std::vector<int>{0});
REQUIRE(config.option<ConfigOptionFloatsNullable>("filament_ramming_volumetric_speed")->values == std::vector<double>{-1});
}
SCENARIO("ConfigOptionVector::set_to_index with stride=1 copies values correctly", "[Config][set_to_index]") {
GIVEN("A destination vector and a source vector with 3 values") {
Slic3r::ConfigOptionFloats dest({0.0});
Slic3r::ConfigOptionFloats src({10.0, 20.0, 30.0});
std::vector<int> variant_index = {0, 1, 2};
int stride = 1;
WHEN("set_to_index is called with stride=1") {
dest.set_to_index(&src, variant_index, stride);
THEN("The destination contains the source values") {
REQUIRE(dest.values.size() == 3);
REQUIRE(dest.values[0] == 10.0);
REQUIRE(dest.values[1] == 20.0);
REQUIRE(dest.values[2] == 30.0);
}
}
}
GIVEN("A destination vector and a source vector with subset mapping") {
Slic3r::ConfigOptionFloats dest({0.0});
Slic3r::ConfigOptionFloats src({100.0, 200.0, 300.0});
std::vector<int> variant_index = {1, 2};
int stride = 1;
WHEN("set_to_index maps only indices 1 and 2") {
dest.set_to_index(&src, variant_index, stride);
THEN("Only the mapped values are copied, default fills the others") {
REQUIRE(dest.values.size() == 2);
REQUIRE(dest.values[0] == 200.0);
REQUIRE(dest.values[1] == 300.0);
}
}
}
}
SCENARIO("ConfigOptionVector::set_to_index with stride=2 copies grouped values correctly", "[Config][set_to_index]") {
GIVEN("A destination vector and a source vector with stride=2 (e.g., nozzle groups)") {
// Source has 4 groups of 2 values each: (10,11), (20,21), (30,31), (40,41)
Slic3r::ConfigOptionFloats dest({0.0});
Slic3r::ConfigOptionFloats src({10.0, 11.0, 20.0, 21.0, 30.0, 31.0, 40.0, 41.0});
int stride = 2;
WHEN("set_to_index maps groups 0, 1, 3") {
std::vector<int> variant_index = {0, 1, 3};
dest.set_to_index(&src, variant_index, stride);
THEN("The destination has 3 groups (6 values) mapped correctly") {
REQUIRE(dest.values.size() == 6);
// Group 0: (10, 11)
REQUIRE(dest.values[0] == 10.0);
REQUIRE(dest.values[1] == 11.0);
// Group 1: (20, 21)
REQUIRE(dest.values[2] == 20.0);
REQUIRE(dest.values[3] == 21.0);
// Group 3: (40, 41)
REQUIRE(dest.values[4] == 40.0);
REQUIRE(dest.values[5] == 41.0);
}
}
}
GIVEN("A destination and a single-group source") {
Slic3r::ConfigOptionFloats dest({0.0});
// Source has 1 group of 2 values
Slic3r::ConfigOptionFloats src({50.0, 60.0});
int stride = 2;
WHEN("set_to_index maps group 0 from a single-group source") {
std::vector<int> variant_index = {0};
dest.set_to_index(&src, variant_index, stride);
THEN("The destination contains the single group correctly") {
REQUIRE(dest.values.size() == 2);
REQUIRE(dest.values[0] == 50.0);
REQUIRE(dest.values[1] == 60.0);
}
}
}
}
SCENARIO("ConfigOptionVector::set_to_index handles empty dest_index", "[Config][set_to_index]") {
GIVEN("A destination and source with stride=2") {
Slic3r::ConfigOptionFloats dest({0.0});
Slic3r::ConfigOptionFloats src({10.0, 11.0, 20.0, 21.0});
std::vector<int> variant_index = {};
int stride = 2;
WHEN("set_to_index is called with an empty index vector") {
dest.set_to_index(&src, variant_index, stride);
THEN("The destination is resized to 0") {
REQUIRE(dest.values.size() == 0);
}
}
}
}
SCENARIO("ConfigOptionVector::set_to_index handles nil values in source", "[Config][set_to_index]") {
GIVEN("A source with a nil group (stride=2)") {
Slic3r::ConfigOptionFloatsNullable dest({0.0});
Slic3r::ConfigOptionFloatsNullable src({10.0, 11.0,
Slic3r::ConfigOptionFloatsNullable::nil_value(), Slic3r::ConfigOptionFloatsNullable::nil_value(),
30.0, 31.0});
int stride = 2;
WHEN("set_to_index maps all groups including the nil one") {
std::vector<int> variant_index = {0, 1, 2};
dest.set_to_index(&src, variant_index, stride);
THEN("Non-nil groups are copied and the nil group keeps the default") {
REQUIRE(dest.values.size() == 6);
// Group 0: (10, 11) — copied
REQUIRE(dest.values[0] == 10.0);
REQUIRE(dest.values[1] == 11.0);
// Group 1: nil — keeps default (the front value = 10.0)
REQUIRE(dest.values[2] == 10.0);
REQUIRE(dest.values[3] == 10.0);
// Group 2: (30, 31) — copied
REQUIRE(dest.values[4] == 30.0);
REQUIRE(dest.values[5] == 31.0);
}
}
}
}
SCENARIO("ConfigOptionVector::set_to_index handles out-of-bounds dest_index", "[Config][set_to_index]") {
GIVEN("A source with only 2 groups (4 values) but dest_index references group 3") {
Slic3r::ConfigOptionFloats dest({0.0});
Slic3r::ConfigOptionFloats src({10.0, 11.0, 20.0, 21.0}); // 2 groups of stride 2
int stride = 2;
WHEN("set_to_index maps group 3 which is out of bounds") {
std::vector<int> variant_index = {0, 3}; // group 3 is out of range
dest.set_to_index(&src, variant_index, stride);
THEN("Group 0 is copied, group 3 falls back to default without crashing") {
REQUIRE(dest.values.size() == 4);
// Group 0: (10, 11) — copied
REQUIRE(dest.values[0] == 10.0);
REQUIRE(dest.values[1] == 11.0);
// Group 3: out of bounds — keeps default (10.0 = src.values.front())
REQUIRE(dest.values[2] == 10.0);
REQUIRE(dest.values[3] == 10.0);
}
}
}
}
SCENARIO("ConfigOptionVector::set_to_index handles negative dest_index values", "[Config][set_to_index]") {
GIVEN("A destination and source with a negative entry in dest_index") {
// The dest is initially empty, so resize fills all slots with src.values.front().
Slic3r::ConfigOptionFloats dest;
Slic3r::ConfigOptionFloats src({100.0, 101.0, 200.0, 201.0});
int stride = 2;
WHEN("set_to_index maps group 0 and a negative index") {
std::vector<int> variant_index = {-1, 0};
dest.set_to_index(&src, variant_index, stride);
THEN("The negative index is skipped, the valid group is copied") {
REQUIRE(dest.values.size() == 4);
// Position 0 (variant_index[0] = -1): skipped, keeps default fill
// from resize (src.values.front() = 100.0, applied to all new elements)
REQUIRE(dest.values[0] == 100.0);
REQUIRE(dest.values[1] == 100.0);
// Position 1 (variant_index[1] = 0): copied from group 0 of src
REQUIRE(dest.values[2] == 100.0);
REQUIRE(dest.values[3] == 101.0);
}
}
}
}
SCENARIO("ConfigOptionVector::set_to_index handles single-element groups with stride=1", "[Config][set_to_index]") {
GIVEN("A destination re-mapping one variant index with a stride=1 source") {
// Simulates the PrintObject.cpp code path: stride=1, variant_index={1}
Slic3r::ConfigOptionFloats dest({99.0, 99.0, 99.0, 99.0}); // pre-sized for 4 extruders
Slic3r::ConfigOptionFloats src({0.5, 0.6, 0.7, 0.8}); // 4 extruder values
std::vector<int> variant_index = {1}; // only extruder 1 is active
int stride = 1;
WHEN("set_to_index is called") {
dest.set_to_index(&src, variant_index, stride);
THEN("Only the mapped value is copied, rest are defaulted") {
REQUIRE(dest.values.size() == 1);
REQUIRE(dest.values[0] == 0.6);
}
}
}
}
SCENARIO("ConfigOptionVector::set_to_index throws on incompatible type", "[Config][set_to_index]") {
GIVEN("A Floats destination and an Ints source") {
Slic3r::ConfigOptionFloats dest({0.0});
Slic3r::ConfigOptionInts src({1, 2, 3});
std::vector<int> variant_index = {0};
int stride = 1;
WHEN("set_to_index is called with mismatched types") {
THEN("A ConfigurationError is thrown") {
REQUIRE_THROWS_AS(dest.set_to_index(&src, variant_index, stride), Slic3r::ConfigurationError);
}
}
}
}

View File

@@ -0,0 +1,336 @@
#include <catch2/catch_all.hpp>
#include "libslic3r/PrintConfig.hpp"
using namespace Slic3r;
namespace {
// A 2-extruder printer whose second extruder holds both a Standard and a High Flow nozzle
// (nozzle_volume_type Hybrid), described by extruder_nozzle_stats. The variant lists carry one
// column per (extruder x volume type) as composed from the presets.
DynamicPrintConfig make_hybrid_printer_config()
{
DynamicPrintConfig config;
config.option<ConfigOptionStrings>("extruder_nozzle_stats", true)->values = {"Standard#1", "Standard#3|High Flow#2"};
config.option<ConfigOptionEnumsGeneric>("extruder_type", true)->values = {etDirectDrive, etDirectDrive};
config.option<ConfigOptionEnumsGeneric>("nozzle_volume_type", true)->values = {nvtStandard, nvtHybrid};
config.option<ConfigOptionStrings>("extruder_variant_list", true)->values = {"Direct Drive Standard,Direct Drive High Flow",
"Direct Drive Standard,Direct Drive High Flow"};
return config;
}
void add_print_variant_columns(DynamicPrintConfig &config)
{
config.option<ConfigOptionInts>("print_extruder_id", true)->values = {1, 1, 2, 2};
config.option<ConfigOptionStrings>("print_extruder_variant", true)->values = {"Direct Drive Standard", "Direct Drive High Flow",
"Direct Drive Standard", "Direct Drive High Flow"};
config.option<ConfigOptionFloats>("outer_wall_speed", true)->values = {30., 200., 50., 500.};
}
} // namespace
TEST_CASE("apply_override fills nil entries from the 0-based default index", "[Config]")
{
ConfigOptionFloats machine({10., 20., 30.});
ConfigOptionFloatsNullable filament;
filament.values = {ConfigOptionFloatsNullable::nil_value(), 42.};
SECTION("a nil entry picks the slot addressed by its 0-based index") {
std::vector<int> slot_index{2, 0};
ConfigOptionFloats resolved(machine);
REQUIRE(resolved.apply_override(&filament, slot_index));
REQUIRE(resolved.values == std::vector<double>({30., 42.}));
}
SECTION("an index past the machine slots falls back to the first slot") {
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};
ConfigOptionFloats resolved(machine);
REQUIRE(resolved.apply_override(&filament, slot_index));
REQUIRE(resolved.values == std::vector<double>({10., 42.}));
}
}
TEST_CASE("get_config_index_base resolves (volume type, extruder type, id) to a slot", "[Config]")
{
const std::vector<std::string> variant_list = {"Direct Drive Standard", "Direct Drive High Flow",
"Direct Drive Standard", "Direct Drive High Flow"};
const std::vector<int> variant_ids = {1, 1, 2, 2};
SECTION("a matching (variant, id) pair yields its slot") {
REQUIRE(get_config_index_base(nvtStandard, etDirectDrive, 1, variant_list, variant_ids) == 0);
REQUIRE(get_config_index_base(nvtHighFlow, etDirectDrive, 1, variant_list, variant_ids) == 1);
REQUIRE(get_config_index_base(nvtStandard, etDirectDrive, 2, variant_list, variant_ids) == 2);
REQUIRE(get_config_index_base(nvtHighFlow, etDirectDrive, 2, variant_list, variant_ids) == 3);
}
SECTION("no matching column falls back to slot 0") {
REQUIRE(get_config_index_base(nvtStandard, etDirectDrive, 3, variant_list, variant_ids) == 0);
REQUIRE(get_config_index_base(nvtStandard, etBowden, 1, variant_list, variant_ids) == 0);
}
SECTION("Hybrid is not a preset variant string and falls back to slot 0") {
REQUIRE(get_config_index_base(nvtHybrid, etDirectDrive, 2, variant_list, variant_ids) == 0);
}
}
TEST_CASE("get_extruder_nozzle_volume_count reads the per-extruder volume-type layout", "[Config]")
{
std::vector<std::vector<NozzleVolumeType>> nozzle_volume_types;
SECTION("absent stats fall back to one slot per extruder") {
DynamicPrintConfig config;
REQUIRE(config.get_extruder_nozzle_volume_count(2, nozzle_volume_types) == 2);
REQUIRE(nozzle_volume_types.size() == 2);
REQUIRE(nozzle_volume_types[0].empty());
REQUIRE(nozzle_volume_types[1].empty());
}
SECTION("stats sized differently from the extruder count are ignored") {
DynamicPrintConfig config;
config.option<ConfigOptionStrings>("extruder_nozzle_stats", true)->values = {"Standard#1"};
REQUIRE(config.get_extruder_nozzle_volume_count(2, nozzle_volume_types) == 2);
REQUIRE(nozzle_volume_types[0].empty());
REQUIRE(nozzle_volume_types[1].empty());
}
SECTION("single volume type per extruder counts one slot each") {
DynamicPrintConfig config;
config.option<ConfigOptionStrings>("extruder_nozzle_stats", true)->values = {"Standard#1", "High Flow#1"};
REQUIRE(config.get_extruder_nozzle_volume_count(2, nozzle_volume_types) == 2);
REQUIRE(nozzle_volume_types[0] == std::vector<NozzleVolumeType>{nvtStandard});
REQUIRE(nozzle_volume_types[1] == std::vector<NozzleVolumeType>{nvtHighFlow});
}
SECTION("a mixed-nozzle extruder contributes one slot per volume type, ascending enum order") {
DynamicPrintConfig config;
// list High Flow first in the token string: parsing must still order Standard before High Flow
config.option<ConfigOptionStrings>("extruder_nozzle_stats", true)->values = {"Standard#3", "High Flow#3|Standard#3"};
REQUIRE(config.get_extruder_nozzle_volume_count(2, nozzle_volume_types) == 3);
REQUIRE(nozzle_volume_types[0] == std::vector<NozzleVolumeType>{nvtStandard});
REQUIRE(nozzle_volume_types[1] == std::vector<NozzleVolumeType>({nvtStandard, nvtHighFlow}));
}
}
TEST_CASE("update_values_to_printer_extruders expands one slot per (extruder x volume type)", "[Config]")
{
SECTION("Hybrid extruder yields three slots, extruder-ascending then volume-ascending") {
DynamicPrintConfig config = make_hybrid_printer_config();
add_print_variant_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");
REQUIRE(variant_index == std::vector<int>({0, 2, 3}));
REQUIRE(config.option<ConfigOptionFloats>("outer_wall_speed")->values == std::vector<double>({30., 50., 500.}));
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"}));
}
SECTION("stride-2 options keep (normal, silent) pairs together per slot") {
DynamicPrintConfig config = make_hybrid_printer_config();
config.option<ConfigOptionInts>("printer_extruder_id", true)->values = {1, 1, 2, 2};
config.option<ConfigOptionStrings>("printer_extruder_variant", true)->values = {"Direct Drive Standard", "Direct Drive High Flow",
"Direct Drive Standard", "Direct Drive High Flow"};
config.option<ConfigOptionFloats>("machine_max_speed_x", true)->values = {100., 50., 110., 55., 120., 60., 130., 65.};
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,
printer_options_with_variant_2, "printer_extruder_id", "printer_extruder_variant", 2);
REQUIRE(variant_index == std::vector<int>({0, 2, 3}));
REQUIRE(config.option<ConfigOptionFloats>("machine_max_speed_x")->values ==
std::vector<double>({100., 50., 120., 60., 130., 65.}));
}
SECTION("single-slot expansion on a Hybrid extruder resolves via the filament volume type") {
DynamicPrintConfig printer_config = make_hybrid_printer_config();
DynamicPrintConfig filament_config;
filament_config.option<ConfigOptionStrings>("filament_extruder_variant", true)->values = {"Direct Drive Standard", "Direct Drive High Flow"};
filament_config.option<ConfigOptionFloats>("filament_max_volumetric_speed", true)->values = {12., 20.};
std::vector<std::vector<NozzleVolumeType>> nozzle_volume_types;
int extruder_count = 2;
int count = printer_config.get_extruder_nozzle_volume_count(extruder_count, nozzle_volume_types);
SECTION("default filament volume type selects the Standard column") {
std::vector<int> variant_index = filament_config.update_values_to_printer_extruders(printer_config, extruder_count, count,
nozzle_volume_types, filament_options_with_variant, "", "filament_extruder_variant", 1, 2);
REQUIRE(variant_index == std::vector<int>({0}));
REQUIRE(filament_config.option<ConfigOptionFloats>("filament_max_volumetric_speed")->values == std::vector<double>({12.}));
}
SECTION("a High Flow filament volume type selects the High Flow column") {
std::vector<int> variant_index = filament_config.update_values_to_printer_extruders(printer_config, extruder_count, count,
nozzle_volume_types, filament_options_with_variant, "", "filament_extruder_variant", 1, 2, nvtHighFlow);
REQUIRE(variant_index == std::vector<int>({1}));
REQUIRE(filament_config.option<ConfigOptionFloats>("filament_max_volumetric_speed")->values == std::vector<double>({20.}));
}
}
SECTION("an extruder without per-type stats does not overrun the slot table when another is Hybrid") {
DynamicPrintConfig config;
// e0 carries no per-type stats (empty entry), so the summed volume-type count (2) does
// not exceed the extruder count even though the Hybrid e1 emits one slot per volume type.
config.option<ConfigOptionStrings>("extruder_nozzle_stats", true)->values = {"", "Standard#3|High Flow#3"};
config.option<ConfigOptionEnumsGeneric>("extruder_type", true)->values = {etDirectDrive, etDirectDrive};
config.option<ConfigOptionEnumsGeneric>("nozzle_volume_type", true)->values = {nvtStandard, nvtHybrid};
config.option<ConfigOptionStrings>("extruder_variant_list", true)->values = {"Direct Drive Standard,Direct Drive High Flow",
"Direct Drive Standard,Direct Drive High Flow"};
add_print_variant_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 == 2);
REQUIRE(nozzle_volume_types[0].empty());
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");
// e0 resolves by its configured type; the Hybrid e1 emits one slot per stats volume type
REQUIRE(variant_index == std::vector<int>({0, 2, 3}));
REQUIRE(config.option<ConfigOptionFloats>("outer_wall_speed")->values == std::vector<double>({30., 50., 500.}));
}
SECTION("without Hybrid or extra slots the expansion matches the per-extruder resolution") {
DynamicPrintConfig config;
config.option<ConfigOptionEnumsGeneric>("extruder_type", true)->values = {etDirectDrive, etDirectDrive};
config.option<ConfigOptionEnumsGeneric>("nozzle_volume_type", true)->values = {nvtStandard, nvtHighFlow};
config.option<ConfigOptionStrings>("extruder_variant_list", true)->values = {"Direct Drive Standard,Direct Drive High Flow",
"Direct Drive Standard,Direct Drive High Flow"};
add_print_variant_columns(config);
// compute what the per-extruder loop resolves directly, before the arrays are rewritten
std::vector<int> expected_index;
for (int e_index = 0; e_index < 2; e_index++)
expected_index.push_back(config.get_index_for_extruder(e_index + 1, "print_extruder_id", etDirectDrive,
e_index == 0 ? nvtStandard : nvtHighFlow, "print_extruder_variant"));
REQUIRE(expected_index == std::vector<int>({0, 3}));
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 == 2);
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 == expected_index);
REQUIRE(config.option<ConfigOptionFloats>("outer_wall_speed")->values == std::vector<double>({30., 500.}));
}
}
TEST_CASE("update_values_to_printer_extruders_for_multiple_filaments resolves per-filament slots", "[Config]")
{
auto make_filament_arrays = [](DynamicPrintConfig &config) {
config.option<ConfigOptionInts>("filament_self_index", true)->values = {1, 1, 2, 2};
config.option<ConfigOptionStrings>("filament_extruder_variant", true)->values = {"Direct Drive Standard", "Direct Drive High Flow",
"Direct Drive Standard", "Direct Drive High Flow"};
config.option<ConfigOptionFloats>("filament_max_volumetric_speed", true)->values = {12., 20., 13., 21.};
};
std::set<std::string> filament_keys = filament_options_with_variant;
filament_keys.insert("filament_self_index");
SECTION("filament_volume_map picks the concrete volume type on a Hybrid extruder") {
DynamicPrintConfig config = make_hybrid_printer_config();
make_filament_arrays(config);
config.option<ConfigOptionInts>("filament_map", true)->values = {2, 2};
config.option<ConfigOptionInts>("filament_volume_map", true)->values = {nvtStandard, nvtHighFlow};
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_for_multiple_filaments(config, extruder_count, count, filament_keys,
"filament_self_index", "filament_extruder_variant");
REQUIRE(config.option<ConfigOptionFloats>("filament_max_volumetric_speed")->values == std::vector<double>({12., 21.}));
REQUIRE(config.option<ConfigOptionStrings>("filament_extruder_variant")->values ==
std::vector<std::string>({"Direct Drive Standard", "Direct Drive High Flow"}));
REQUIRE(config.option<ConfigOptionInts>("filament_self_index")->values == std::vector<int>({1, 2}));
}
SECTION("a volume map not sized to the filament count is ignored") {
DynamicPrintConfig config = make_hybrid_printer_config();
make_filament_arrays(config);
config.option<ConfigOptionInts>("filament_map", true)->values = {2, 2};
// the registered default is a single-element vector; it must not override slot resolution
config.option<ConfigOptionInts>("filament_volume_map", true)->values = {nvtStandard};
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_for_multiple_filaments(config, extruder_count, count, filament_keys,
"filament_self_index", "filament_extruder_variant");
// Hybrid resolves as Standard when no usable per-filament map exists
REQUIRE(config.option<ConfigOptionFloats>("filament_max_volumetric_speed")->values == std::vector<double>({12., 13.}));
REQUIRE(config.option<ConfigOptionInts>("filament_self_index")->values == std::vector<int>({1, 2}));
}
SECTION("a single-filament explicit assignment on a Hybrid extruder is honored") {
DynamicPrintConfig config = make_hybrid_printer_config();
config.option<ConfigOptionInts>("filament_self_index", true)->values = {1, 1};
config.option<ConfigOptionStrings>("filament_extruder_variant", true)->values = {"Direct Drive Standard", "Direct Drive High Flow"};
config.option<ConfigOptionFloats>("filament_max_volumetric_speed", true)->values = {12., 20.};
config.option<ConfigOptionInts>("filament_map", true)->values = {2};
// sized to the (single) filament count: the producers guarantee sizing, so a
// single-filament map is as trustworthy as any other and the explicit High Flow
// request must win over the Hybrid->Standard fallback
config.option<ConfigOptionInts>("filament_volume_map", true)->values = {nvtHighFlow};
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_for_multiple_filaments(config, extruder_count, count, filament_keys,
"filament_self_index", "filament_extruder_variant");
REQUIRE(config.option<ConfigOptionFloats>("filament_max_volumetric_speed")->values == std::vector<double>({20.}));
REQUIRE(config.option<ConfigOptionInts>("filament_self_index")->values == std::vector<int>({1}));
}
SECTION("without Hybrid or extra slots the volume map is not consulted") {
DynamicPrintConfig config;
config.option<ConfigOptionEnumsGeneric>("extruder_type", true)->values = {etDirectDrive, etDirectDrive};
config.option<ConfigOptionEnumsGeneric>("nozzle_volume_type", true)->values = {nvtStandard, nvtHighFlow};
config.option<ConfigOptionStrings>("extruder_variant_list", true)->values = {"Direct Drive Standard,Direct Drive High Flow",
"Direct Drive Standard,Direct Drive High Flow"};
make_filament_arrays(config);
config.option<ConfigOptionInts>("filament_map", true)->values = {1, 2};
// sized to the filament count, but inert because no extruder exposes multiple volume types
config.option<ConfigOptionInts>("filament_volume_map", true)->values = {nvtHighFlow, nvtStandard};
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 == 2);
config.update_values_to_printer_extruders_for_multiple_filaments(config, extruder_count, count, filament_keys,
"filament_self_index", "filament_extruder_variant");
// filament 1 keeps its extruder's Standard column, filament 2 its extruder's High Flow column
REQUIRE(config.option<ConfigOptionFloats>("filament_max_volumetric_speed")->values == std::vector<double>({12., 21.}));
REQUIRE(config.option<ConfigOptionInts>("filament_self_index")->values == std::vector<int>({1, 2}));
}
}

View File

@@ -0,0 +1,31 @@
#include <catch2/catch_all.hpp>
#include "libslic3r/PrintConfig.hpp"
using namespace Slic3r;
TEST_CASE("convert_to_nvt_type maps extruder variant strings to nozzle volume types", "[Config]")
{
SECTION("Direct Drive variants") {
REQUIRE(convert_to_nvt_type("Direct Drive Standard") == nvtStandard);
REQUIRE(convert_to_nvt_type("Direct Drive High Flow") == nvtHighFlow);
REQUIRE(convert_to_nvt_type("Direct Drive TPU High Flow") == nvtTPUHighFlow);
}
SECTION("Bowden variants") {
REQUIRE(convert_to_nvt_type("Bowden Standard") == nvtStandard);
REQUIRE(convert_to_nvt_type("Bowden High Flow") == nvtHighFlow);
}
SECTION("Unparsable strings fall back to hybrid") {
REQUIRE(convert_to_nvt_type("Unknown Extruder") == nvtHybrid);
REQUIRE(convert_to_nvt_type("") == nvtHybrid);
REQUIRE(convert_to_nvt_type("High Flow") == nvtHybrid);
REQUIRE(convert_to_nvt_type("Direct Drive") == nvtHybrid);
}
SECTION("Whitespace around the volume-type remainder is trimmed") {
REQUIRE(convert_to_nvt_type("Direct Drive High Flow ") == nvtHighFlow);
REQUIRE(convert_to_nvt_type(" Bowden Standard") == nvtStandard);
}
}

View File

@@ -52,6 +52,11 @@ SCENARIO("Placeholder parser scripting", "[PlaceholderParser]") {
SECTION("math: round(-13.4)") { REQUIRE(parser.process("{round(-13.4)}") == "-13"); }
SECTION("math: round(13.6)") { REQUIRE(parser.process("{round(13.6)}") == "14"); }
SECTION("math: round(-13.6)") { REQUIRE(parser.process("{round(-13.6)}") == "-14"); }
SECTION("math: round(13.5)") { REQUIRE(parser.process("{round(13.5)}") == "14"); }
SECTION("math: floor(13.9)") { REQUIRE(parser.process("{floor(13.9)}") == "13"); }
SECTION("math: floor(-13.1)") { REQUIRE(parser.process("{floor(-13.1)}") == "-14"); }
SECTION("math: ceil(13.1)") { REQUIRE(parser.process("{ceil(13.1)}") == "14"); }
SECTION("math: ceil(-13.9)") { REQUIRE(parser.process("{ceil(-13.9)}") == "-13"); }
SECTION("math: digits(5, 15)") { REQUIRE(parser.process("{digits(5, 15)}") == " 5"); }
SECTION("math: digits(5., 15)") { REQUIRE(parser.process("{digits(5., 15)}") == " 5"); }
SECTION("math: zdigits(5, 15)") { REQUIRE(parser.process("{zdigits(5, 15)}") == "000000000000005"); }
@@ -65,6 +70,8 @@ SCENARIO("Placeholder parser scripting", "[PlaceholderParser]") {
SECTION("math: interpolate_table(13.84375892476, (0, 0), (20, 20))") { REQUIRE(std::stod(parser.process("{interpolate_table(13.84375892476, (0, 0), (20, 20))}")) == Catch::Approx(13.84375892476)); }
SECTION("math: interpolate_table(13, (0, 0), (20, 20), (30, 20))") { REQUIRE(std::stod(parser.process("{interpolate_table(13, (0, 0), (20, 20), (30, 20))}")) == Catch::Approx(13.)); }
SECTION("math: interpolate_table(25, (0, 0), (20, 20), (30, 20))") { REQUIRE(std::stod(parser.process("{interpolate_table(25, (0, 0), (20, 20), (30, 20))}")) == Catch::Approx(20.)); }
// Only the grammar's built-in functions are callable; any other name is an undefined variable and throws.
SECTION("math: a non-built-in function name throws") { REQUIRE_THROWS(parser.process("{sqrt(16)}")); }
// regex_replace(subject, /pattern/, replacement): the string-transform primitive.
SECTION("regex_replace: strips a file extension") { REQUIRE(parser.process("{regex_replace(\"part.stl\", /\\.[^.]*$/, \"\")}") == "part"); }

View File

@@ -0,0 +1,936 @@
#include <catch2/catch_all.hpp>
#include "libslic3r/FilamentGroupUtils.hpp"
#include "libslic3r/MultiNozzleUtils.hpp"
#include "libslic3r/PrintConfig.hpp"
#include "libslic3r/GCode/ToolOrdering.hpp"
#include "libslic3r/Model.hpp"
#include "libslic3r/Print.hpp"
#include "libslic3r/TriangleMesh.hpp"
#include <algorithm>
#include <map>
#include <set>
#include <unordered_map>
#include <vector>
#include <boost/filesystem.hpp>
// H2C/A2L multi-nozzle filament grouping core.
//
// These tests pin the behaviour of the grouping result type
// (Slic3r::MultiNozzleUtils::LayeredNozzleGroupResult) that GCode consumes via
// group_result->get_nozzle_id(filament, layer) and
// group_result->get_first_nozzle_for_filament(filament)->group_id.
//
// The central requirement is ZERO behaviour change for existing (single-nozzle)
// printers: with extruder_max_nozzle_count == 1 per extruder the result collapses
// to the classic filament->extruder grouping (nozzle id == extruder id).
using namespace Slic3r;
using namespace Slic3r::MultiNozzleUtils;
namespace {
// Build a trivial "one logical nozzle per extruder" list, the single-nozzle case
// that every current printer profile produces.
std::vector<NozzleInfo> single_nozzle_per_extruder(int extruder_count)
{
std::vector<NozzleInfo> nozzle_list;
for (int e = 0; e < extruder_count; ++e) {
NozzleInfo n;
n.diameter = "0.4";
n.volume_type = nvtStandard;
n.extruder_id = e;
n.group_id = e; // one nozzle per extruder => nozzle id == extruder id
nozzle_list.push_back(n);
}
return nozzle_list;
}
} // namespace
TEST_CASE("Multi-nozzle gate predicate mirrors BambuStudio", "[ToolOrdering][H2C]")
{
// The multi-nozzle gate: std::any_of(extruder_max_nozzle_count > 1).
DynamicPrintConfig config = DynamicPrintConfig::full_print_config();
auto *opt = config.option<ConfigOptionIntsNullable>("extruder_max_nozzle_count");
REQUIRE(opt != nullptr); // extruder_max_nozzle_count must be a real config option
// extruder_nozzle_stats must be a real config option so printer profiles and
// 3mf projects round-trip the per-extruder nozzle inventory (GUI producers wire it later).
REQUIRE(config.option<ConfigOptionStrings>("extruder_nozzle_stats") != nullptr);
auto has_multiple_nozzle = [](const std::vector<int> &values) {
return std::any_of(values.begin(), values.end(), [](int v) { return v > 1; });
};
// Default for every existing printer: 1 nozzle per extruder => gate is closed.
REQUIRE_FALSE(has_multiple_nozzle(opt->values));
// Synthetic H2C-like machine: extruder 1 is a 6-nozzle cluster => gate opens.
REQUIRE(has_multiple_nozzle(std::vector<int>{1, 6}));
}
TEST_CASE("Single-nozzle grouping: every filament maps to its extruder nozzle", "[ToolOrdering][H2C]")
{
SECTION("single extruder => all filaments map to nozzle 0")
{
auto nozzle_list = single_nozzle_per_extruder(1);
// 3 filaments, all assigned to the single extruder 0.
std::vector<int> filament_nozzle_map = {0, 0, 0};
std::vector<unsigned int> used_filaments = {0, 1, 2};
auto group_opt = LayeredNozzleGroupResult::create(filament_nozzle_map, nozzle_list, used_filaments);
REQUIRE(group_opt.has_value());
auto &group = *group_opt;
for (int f = 0; f < 3; ++f) {
REQUIRE(group.get_nozzle_id(f) == 0);
REQUIRE(group.get_extruder_id(f) == 0);
auto first = group.get_first_nozzle_for_filament(f);
REQUIRE(first.has_value());
REQUIRE(first->group_id == 0);
}
REQUIRE_FALSE(group.is_support_dynamic_nozzle_map());
}
SECTION("dual extruder => nozzle id equals the classic extruder grouping")
{
auto nozzle_list = single_nozzle_per_extruder(2);
// filament -> extruder map (the map Orca's reorder already computes).
std::vector<int> filament_map = {0, 1, 0, 1};
std::vector<unsigned int> used_filaments = {0, 1, 2, 3};
auto group_opt = LayeredNozzleGroupResult::create(filament_map, nozzle_list, used_filaments);
REQUIRE(group_opt.has_value());
auto &group = *group_opt;
REQUIRE(group.get_nozzle_id(0) == 0);
REQUIRE(group.get_nozzle_id(1) == 1);
REQUIRE(group.get_nozzle_id(2) == 0);
REQUIRE(group.get_nozzle_id(3) == 1);
// With one nozzle per extruder, nozzle id and extruder id agree.
for (int f = 0; f < 4; ++f)
REQUIRE(group.get_nozzle_id(f) == group.get_extruder_id(f));
}
}
TEST_CASE("H2C multi-nozzle: filaments get distinct nozzles on the 6-nozzle extruder", "[ToolOrdering][H2C]")
{
// Synthetic H2C-like config: 2 extruders, extruder_max_nozzle_count = {1, 6},
// 4 filaments all assigned to extruder 1 (0-based). Each filament requests a
// distinct logical nozzle cluster (as the grouping algorithm would emit), so the
// create() overload must resolve them to 4 distinct physical nozzles.
std::vector<unsigned int> used_filaments = {0, 1, 2, 3};
std::vector<int> filament_map = {1, 1, 1, 1}; // extruder 1
std::vector<int> filament_volume_map = {0, 0, 0, 0}; // nvtStandard
std::vector<int> filament_nozzle_map = {0, 1, 2, 3}; // distinct clusters
std::vector<std::map<NozzleVolumeType, int>> nozzle_count(2);
nozzle_count[0] = {}; // extruder 0: 1-nozzle (unused here)
nozzle_count[1] = {{nvtStandard, 6}}; // extruder 1: 6-nozzle cluster
auto group_opt = LayeredNozzleGroupResult::create(
used_filaments, filament_map, filament_volume_map, filament_nozzle_map, nozzle_count, 0.4f);
REQUIRE(group_opt.has_value());
auto &group = *group_opt;
// All four filaments live on extruder 1, on four distinct physical nozzles.
std::set<int> distinct_nozzles;
for (int f = 0; f < 4; ++f) {
REQUIRE(group.get_extruder_id(f) == 1);
int nid = group.get_nozzle_id(f);
REQUIRE(nid >= 0);
distinct_nozzles.insert(nid);
}
REQUIRE(distinct_nozzles.size() == 4);
// get_nozzle_id must be stable across layers (no per-layer / selector map here).
for (int f = 0; f < 4; ++f) {
int base = group.get_nozzle_id(f, -1);
REQUIRE(group.get_nozzle_id(f, 0) == base);
REQUIRE(group.get_nozzle_id(f, 5) == base);
}
// first-nozzle lookup agrees with the per-layer lookup for a static map.
for (int f = 0; f < 4; ++f) {
auto first = group.get_first_nozzle_for_filament(f);
REQUIRE(first.has_value());
REQUIRE(first->extruder_id == 1);
REQUIRE(first->group_id == group.get_nozzle_id(f));
}
}
TEST_CASE("H2C dynamic selector: per-layer nozzle ids reach the g-code surface", "[ToolOrdering][H2C][Dynamic]")
{
// The per-layer regroup engine
// (plan_filament_mapping_and_order_by_combo_ranges -> 4-arg LayeredNozzleGroupResult::create)
// produces a *selector* result whose filament->nozzle map varies across layers. This is exactly
// what GCode reads for H2C dynamic mode: hotend_id_for_gcode_placeholder /
// nozzle_id_for_gcode_placeholder call group->is_support_dynamic_nozzle_map() and, when true,
// group->get_nozzle_id(filament, layer) / get_first_nozzle_for_filament(filament). Here we build
// the selector result directly (the engine's output shape) and assert those accessors return
// per-layer values -- the surface that "goes live" only in dynamic mode. The static path (every
// other test above) keeps is_support_dynamic_nozzle_map() == false and a stable nozzle id, so its
// g-code is unchanged.
// H2C-like fleet: extruder 0 = 1 nozzle (group 0), extruder 1 = a 3-nozzle rack (groups 1..3).
std::vector<NozzleInfo> nozzle_list;
for (int g = 0; g < 4; ++g) {
NozzleInfo n;
n.diameter = "0.4";
n.volume_type = nvtStandard;
n.extruder_id = (g == 0) ? 0 : 1;
n.group_id = g;
nozzle_list.push_back(n);
}
// Three filaments; filament 2 is reassigned from physical nozzle 2 (layers 0-1) to nozzle 3
// (layers 2-3) by the per-layer selector -- the case that sets support_dynamic_nozzle_map.
std::vector<std::vector<int>> layer_filament_nozzle_maps = {
{0, 1, 2}, // layer 0
{0, 1, 2}, // layer 1
{0, 1, 3}, // layer 2: filament 2 moved to nozzle 3
{0, 1, 3}, // layer 3
};
std::vector<std::vector<unsigned int>> layer_filament_sequences = {
{0, 1, 2}, {0, 1, 2}, {0, 1, 2}, {0, 1, 2},
};
std::vector<unsigned int> used_filaments = {0, 1, 2};
auto group_opt = LayeredNozzleGroupResult::create(layer_filament_nozzle_maps, nozzle_list, used_filaments, layer_filament_sequences);
REQUIRE(group_opt.has_value());
auto &group = *group_opt;
// The selector is active: a filament maps to more than one physical nozzle across layers.
REQUIRE(group.is_support_dynamic_nozzle_map());
// Per-layer hotend/nozzle ids -- the values the dynamic g-code placeholders emit.
REQUIRE(group.get_nozzle_id(2, 0) == 2);
REQUIRE(group.get_nozzle_id(2, 1) == 2);
REQUIRE(group.get_nozzle_id(2, 2) == 3); // reassigned on layer 2
REQUIRE(group.get_nozzle_id(2, 3) == 3);
REQUIRE(group.get_extruder_id(2, 0) == 1);
REQUIRE(group.get_extruder_id(2, 2) == 1);
// Unmoved filaments keep a stable id across layers.
REQUIRE(group.get_nozzle_id(0, 0) == 0);
REQUIRE(group.get_nozzle_id(0, 3) == 0);
REQUIRE(group.get_nozzle_id(1, 0) == 1);
REQUIRE(group.get_nozzle_id(1, 3) == 1);
// first-nozzle lookup (used by the *_first_* placeholders / start g-code) is the first layer's id.
auto first2 = group.get_first_nozzle_for_filament(2);
REQUIRE(first2.has_value());
REQUIRE(first2->group_id == 2);
// every physical nozzle a filament visits is reported (3mf metadata / nozzle_diameters_by_nozzle_id).
std::set<int> fil2_nozzles;
for (const auto &n : group.get_nozzles_for_filament(2))
fil2_nozzles.insert(n.group_id);
REQUIRE(fil2_nozzles == std::set<int>({2, 3}));
}
TEST_CASE("Multi-nozzle reorder tolerates a filament with no nozzle (RL-48)", "[ToolOrdering][H2C][Dynamic]")
{
// The per-layer engine can hand reorder_filaments_for_multi_nozzle_extruder a group result that
// resolves no nozzle for a layer's filament (a degenerate/malformed input where a layer references
// a filament index outside the grouping map). Unguarded, that dereferences std::max_element() on an
// empty extruder set (SIGSEGV). The guard must instead emit each layer's filaments in order and
// return, so a bad input degrades gracefully rather than crashing.
auto nozzle_list = single_nozzle_per_extruder(2);
std::vector<int> filament_nozzle_map = {0}; // map only covers filament 0
auto group_opt = LayeredNozzleGroupResult::create(filament_nozzle_map, nozzle_list, std::vector<unsigned int>{0});
REQUIRE(group_opt.has_value());
std::vector<unsigned int> filament_lists = {3}; // filament 3 resolves to no nozzle
std::vector<std::vector<unsigned int>> layer_filaments = {{3}, {3}};
std::vector<std::vector<std::vector<float>>> flush_matrix(2, {{0.f}}); // unused on the guard path
std::vector<std::vector<unsigned int>> sequences;
REQUIRE_NOTHROW(reorder_filaments_for_multi_nozzle_extruder(filament_lists, *group_opt, layer_filaments, flush_matrix, nullptr, &sequences));
// Each layer still gets a valid sequence (its own filaments) — no reorder, no crash.
REQUIRE(sequences.size() == layer_filaments.size());
REQUIRE(sequences[0] == std::vector<unsigned int>{3});
REQUIRE(sequences[1] == std::vector<unsigned int>{3});
}
// The round-robin build_multi_nozzle_group_result adapter was superseded by the
// nozzle-centric FilamentGroup engine (get_recommended_filament_maps now decides nozzle co-location
// by flush cost, not round-robin). The two former pipeline tests are dropped:
// * H2C multi-nozzle physical-nozzle resolution (6-arg create) is covered above by the
// "H2C multi-nozzle: filaments get distinct nozzles" case;
// * the single-nozzle "nozzle id == extruder id" degradation is covered above by the
// "Single-nozzle grouping" case (build_default_nozzle_list + 3-arg create is the exact path the
// gate-closed branch and by-object fallback use);
// * end-to-end H2C/H2D grouping co-location is now pinned by the filament_group golden suite
// (tests/filament_group, config_b/config_c).
TEST_CASE("extruder_nozzle_stats round-trips through save/parse", "[ToolOrdering][H2C]")
{
// The per-extruder nozzle inventory must survive save_extruder_nozzle_stats_to_string ->
// get_extruder_nozzle_stats unchanged, so printer presets and 3mf projects persist it.
std::vector<std::map<NozzleVolumeType, int>> stats = {
{{nvtStandard, 1}}, // extruder 0: single standard nozzle
{{nvtStandard, 5}, {nvtHighFlow, 1}}, // extruder 1: 6-nozzle mixed cluster
};
REQUIRE(get_extruder_nozzle_stats(save_extruder_nozzle_stats_to_string(stats)) == stats);
}
// The filament-change-time model (MultiNozzleUtils::simulate_filament_change_time) is self-contained
// analytic code with no slicing-pipeline caller yet; these fixtures pin its numeric output so future
// changes and its first consumer (the filament_group golden harness) build on a locked model. Expected
// values are hand-traced through the AMS -> selector -> extruder transport model.
TEST_CASE("Filament-change-time model matches the BBS analytic simulation", "[MultiNozzle][H2C][ChangeTime]")
{
using Catch::Matchers::WithinAbs;
// Load/unload constants mirror the golden config_c change_time_params
// (selector 1/1, standard 3/2): a selector move costs 1, a full AMS load 3 / unload 2.
FilamentChangeTimeParams params;
params.selector_load_time = 1.0f;
params.selector_unload_time = 1.0f;
params.standard_load_time = 3.0f;
params.standard_unload_time = 2.0f;
// One extruder carrying one physical nozzle (nozzle id == extruder id == 0).
std::vector<NozzleInfo> nozzle_list(1);
nozzle_list[0].diameter = "0.4";
nozzle_list[0].volume_type = nvtStandard;
nozzle_list[0].extruder_id = 0;
nozzle_list[0].group_id = 0;
// Two filaments in distinct AMS groups, printed in the order A, B, A on nozzle 0.
std::vector<int> logical_filaments = {0, 1};
std::vector<int> group_of_filament = {0, 1};
std::vector<int> filament_change_seq = {0, 1, 0};
std::vector<int> nozzle_change_seq = {0, 0, 0};
SECTION("no AMS pre-load: each change is a full AMS<->extruder transport")
{
auto r = simulate_filament_change_time(
logical_filaments, nozzle_list, filament_change_seq, nozzle_change_seq,
group_of_filament, params, /*ams_preload_enabled=*/{}, /*calc_sliced_time=*/true);
// load0(3) + [unload0(2)+load1(3)] + [unload1(2)+load0(3)] = 13
REQUIRE_THAT(r.actual_time, WithinAbs(13.0, 1e-6));
// Single nozzle, no selector overlap => slicer estimate equals the actual time.
REQUIRE_THAT(r.sliced_time, WithinAbs(13.0, 1e-6));
}
SECTION("AMS pre-load overlaps transport, shrinking the actual time")
{
std::vector<bool> preload = {true, true};
auto r = simulate_filament_change_time(
logical_filaments, nozzle_list, filament_change_seq, nozzle_change_seq,
group_of_filament, params, preload, /*calc_sliced_time=*/false);
// Pre-loading the next filament into the selector runs in parallel with the current
// extruder move, so the selector<->extruder legs dominate: 3 + (1+1) + (1+1) = 7.
REQUIRE_THAT(r.actual_time, WithinAbs(7.0, 1e-6));
}
SECTION("degenerate inputs return zero")
{
auto r = simulate_filament_change_time({}, nozzle_list, filament_change_seq,
nozzle_change_seq, {}, params);
REQUIRE_THAT(r.actual_time, WithinAbs(0.0, 1e-6));
REQUIRE_THAT(r.sliced_time, WithinAbs(0.0, 1e-6));
}
}
TEST_CASE("NozzleStatusRecorder tracks nozzle/extruder occupancy", "[MultiNozzle][H2C][ChangeTime]")
{
NozzleStatusRecorder rec;
REQUIRE(rec.is_nozzle_empty(0));
REQUIRE(rec.get_filament_in_nozzle(0) == -1);
REQUIRE(rec.get_nozzle_in_extruder(0) == -1);
rec.set_nozzle_status(2, 5, 1); // nozzle 2 holds filament 5, mounted on extruder 1
REQUIRE_FALSE(rec.is_nozzle_empty(2));
REQUIRE(rec.get_filament_in_nozzle(2) == 5);
REQUIRE(rec.get_nozzle_in_extruder(1) == 2);
rec.clear_nozzle_status(2);
REQUIRE(rec.is_nozzle_empty(2));
REQUIRE(rec.get_filament_in_nozzle(2) == -1);
// Clearing a nozzle leaves the extruder->nozzle association intact.
REQUIRE(rec.get_nozzle_in_extruder(1) == 2);
}
TEST_CASE("Hybrid nozzle stats resolve to concrete volume types", "[ToolOrdering][H2C]")
{
// Extruder 0 is Standard-only; extruder 1 carries a mixed Standard + High Flow inventory
// (the "Hybrid" flow selection). The write-back pipeline persists get_volume_map(), so the
// result must always carry concrete per-filament volume types, never the Hybrid seed.
auto stats = get_extruder_nozzle_stats({"Standard#1", "Standard#1|High Flow#1"});
REQUIRE(stats.size() == 2);
REQUIRE(stats[1].size() == 2);
std::vector<unsigned int> used_filaments = {0, 1, 2};
std::vector<int> filament_map = {0, 1, 1}; // 0-based extruder ids
std::vector<int> volume_requests = {(int) nvtStandard, (int) nvtHighFlow, (int) nvtStandard};
std::vector<int> nozzle_requests = {0, 1, 2}; // distinct logical nozzles
auto group = LayeredNozzleGroupResult::create(used_filaments, filament_map, volume_requests, nozzle_requests, stats, 0.4f);
REQUIRE(group.has_value());
auto volume_map = group->get_volume_map();
REQUIRE(volume_map == volume_requests);
for (auto fid : used_filaments)
REQUIRE(volume_map[fid] != (int) nvtHybrid);
// The Hybrid seed itself matches no physical nozzle: such a request is unsatisfiable.
std::vector<int> hybrid_requests = {(int) nvtStandard, (int) nvtHybrid, (int) nvtStandard};
REQUIRE_FALSE(LayeredNozzleGroupResult::create(used_filaments, filament_map, hybrid_requests, nozzle_requests, stats, 0.4f).has_value());
}
TEST_CASE("update_used_filament_values merges only used filaments", "[ToolOrdering][H2C]")
{
// The config write-back merges the engine's per-filament values over the config baseline:
// used filaments adopt the engine value, unused filaments keep their config assignment.
std::vector<int> old_values = {1, 1, 2, 1};
std::vector<int> new_values = {2, 2, 1, 2};
std::vector<unsigned int> used = {0, 2};
auto merged = FilamentGroupUtils::update_used_filament_values(old_values, new_values, used);
REQUIRE(merged == std::vector<int>{2, 1, 1, 1});
// No used filaments => the config baseline is returned untouched.
REQUIRE(FilamentGroupUtils::update_used_filament_values(old_values, new_values, {}) == old_values);
}
TEST_CASE("Print config-index resolvers pick per-filament Hybrid slots", "[Print][H2C]")
{
// A 2-extruder printer whose second extruder is Hybrid (Standard + High Flow nozzles).
// The preset-style variant columns carry one column per (extruder x volume type); apply()
// expands them to the 3-slot layout [e1-Std, e2-Std, e2-HF].
DynamicPrintConfig config = DynamicPrintConfig::full_print_config();
config.option<ConfigOptionFloats>("nozzle_diameter", true)->values = {0.4, 0.4};
config.option<ConfigOptionStrings>("extruder_nozzle_stats", true)->values = {"Standard#1", "Standard#1|High Flow#2"};
config.option<ConfigOptionEnumsGeneric>("extruder_type", true)->values = {etDirectDrive, etDirectDrive};
config.option<ConfigOptionEnumsGeneric>("nozzle_volume_type", true)->values = {nvtStandard, nvtHybrid};
config.option<ConfigOptionStrings>("extruder_variant_list", true)->values = {"Direct Drive Standard,Direct Drive High Flow",
"Direct Drive Standard,Direct Drive High Flow"};
config.option<ConfigOptionInts>("print_extruder_id", true)->values = {1, 1, 2, 2};
config.option<ConfigOptionStrings>("print_extruder_variant", true)->values = {"Direct Drive Standard", "Direct Drive High Flow",
"Direct Drive Standard", "Direct Drive High Flow"};
config.option<ConfigOptionFloats>("outer_wall_speed", true)->values = {30., 200., 50., 500.};
// Three filaments: 0 -> extruder 1 (Std), 1 -> extruder 2 (Std), 2 -> extruder 2 (High Flow).
config.option<ConfigOptionFloats>("filament_diameter", true)->values = {1.75, 1.75, 1.75};
config.option<ConfigOptionStrings>("filament_colour", true)->values = {"#FF0000", "#00FF00", "#0000FF"};
config.option<ConfigOptionInts>("filament_map", true)->values = {1, 2, 2};
config.option<ConfigOptionInts>("filament_volume_map", true)->values = {(int) nvtStandard, (int) nvtStandard, (int) nvtHighFlow};
Model model;
model.add_object("cube", "", make_cube(20, 20, 20))->add_instance();
Print print;
print.apply(model, config);
// Stub grouping result mirroring the maps above: one nozzle per (extruder, volume type).
std::vector<NozzleInfo> nozzle_list;
{
NozzleInfo n;
n.diameter = "0.4";
n.volume_type = nvtStandard; n.extruder_id = 0; n.group_id = 0; nozzle_list.push_back(n);
n.volume_type = nvtStandard; n.extruder_id = 1; n.group_id = 1; nozzle_list.push_back(n);
n.volume_type = nvtHighFlow; n.extruder_id = 1; n.group_id = 2; nozzle_list.push_back(n);
}
std::vector<unsigned int> used_filaments = {0, 1, 2};
auto group = LayeredNozzleGroupResult::create(std::vector<int>{0, 1, 2}, nozzle_list, used_filaments);
REQUIRE(group.has_value());
print.set_nozzle_group_result(std::make_shared<LayeredNozzleGroupResult>(*group));
// The write-back re-expands the config and refreshes the resolver caches.
print.update_filament_maps_to_config({1, 2, 2}, {(int) nvtStandard, (int) nvtStandard, (int) nvtHighFlow}, {0, 1, 2});
// The expansion must have produced the 3-slot layout the resolvers index into.
const auto &region_config = print.default_region_config();
REQUIRE(region_config.print_extruder_variant.values ==
std::vector<std::string>({"Direct Drive Standard", "Direct Drive Standard", "Direct Drive High Flow"}));
REQUIRE(region_config.print_extruder_id.values == std::vector<int>({1, 2, 2}));
SECTION("each filament resolves to its own (extruder x volume type) slot") {
REQUIRE(print.get_nozzle_config_index(0, 0) == 0); // extruder 1, Standard
REQUIRE(print.get_nozzle_config_index(1, 0) == 1); // extruder 2, Standard
REQUIRE(print.get_nozzle_config_index(2, 0) == 2); // extruder 2, High Flow
}
SECTION("without a group result the resolver falls back to the filament's extruder slot") {
print.set_nozzle_group_result(nullptr);
REQUIRE(print.get_nozzle_config_index(0, 0) == 0);
REQUIRE(print.get_nozzle_config_index(1, 0) == 1);
REQUIRE(print.get_nozzle_config_index(2, 0) == 1); // extruder slot, not the High Flow slot
}
}
TEST_CASE("Re-applying an unchanged config after slicing keeps the result valid", "[Print][H2C]")
{
// apply() rebuilds m_config.filament_map_2 to the real per-filament slot map, while the
// incoming full config only ever carries the ConfigDef default for it. The engine-derived
// key must therefore be kept out of the apply diff: the GUI re-applies right after slicing
// completes, and a phantom filament_map_2 diff would invalidate every freshly sliced result
// on any multi-extruder printer.
DynamicPrintConfig config = DynamicPrintConfig::full_print_config();
config.option<ConfigOptionFloats>("nozzle_diameter", true)->values = {0.4, 0.4};
config.option<ConfigOptionStrings>("extruder_nozzle_stats", true)->values = {"Standard#1", "Standard#1|High Flow#2"};
config.option<ConfigOptionEnumsGeneric>("extruder_type", true)->values = {etDirectDrive, etDirectDrive};
config.option<ConfigOptionEnumsGeneric>("nozzle_volume_type", true)->values = {nvtStandard, nvtHybrid};
config.option<ConfigOptionStrings>("extruder_variant_list", true)->values = {"Direct Drive Standard,Direct Drive High Flow",
"Direct Drive Standard,Direct Drive High Flow"};
config.option<ConfigOptionInts>("print_extruder_id", true)->values = {1, 1, 2, 2};
config.option<ConfigOptionStrings>("print_extruder_variant", true)->values = {"Direct Drive Standard", "Direct Drive High Flow",
"Direct Drive Standard", "Direct Drive High Flow"};
config.option<ConfigOptionFloats>("filament_diameter", true)->values = {1.75, 1.75, 1.75};
config.option<ConfigOptionStrings>("filament_colour", true)->values = {"#FF0000", "#00FF00", "#0000FF"};
config.option<ConfigOptionInts>("filament_map", true)->values = {1, 2, 2};
config.option<ConfigOptionInts>("filament_volume_map", true)->values = {(int) nvtStandard, (int) nvtStandard, (int) nvtHighFlow};
Model model;
ModelObject *object = model.add_object("cube", "", make_cube(20, 20, 20));
object->add_instance()->set_offset(Vec3d(100., 100., 0.));
Print print;
print.apply(model, config);
print.process();
REQUIRE(print.is_step_done(psSlicingFinished));
auto status = print.apply(model, config);
REQUIRE(status != PrintBase::APPLY_STATUS_INVALIDATED);
REQUIRE(print.is_step_done(psSlicingFinished));
}
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") {
// Filament 1 extrudes on layers 0 (nozzle 1) and 3 (nozzle 2); the planner leaves stale
// entries on the layers in between.
std::vector<std::vector<int>> maps = {
{0, 1},
{0, -1}, // filament 1 idle
{0, -1}, // filament 1 idle
{0, 2},
};
std::vector<std::vector<unsigned int>> filaments = {{0, 1}, {0}, {0}, {0, 1}};
normalize_nozzle_map_per_layer(maps, filaments);
REQUIRE(maps[0] == std::vector<int>({0, 1}));
REQUIRE(maps[1] == std::vector<int>({0, 1})); // carried forward
REQUIRE(maps[2] == std::vector<int>({0, 1})); // carried forward
REQUIRE(maps[3] == std::vector<int>({0, 2})); // used layer untouched
}
SECTION("layers before a filament's first use inherit its first nozzle") {
std::vector<std::vector<int>> maps = {
{0, -1},
{0, -1},
{0, 3}, // filament 1 first extrudes here
};
std::vector<std::vector<unsigned int>> filaments = {{0}, {0}, {0, 1}};
normalize_nozzle_map_per_layer(maps, filaments);
REQUIRE(maps[0] == std::vector<int>({0, 3})); // back-filled
REQUIRE(maps[1] == std::vector<int>({0, 3})); // back-filled
REQUIRE(maps[2] == std::vector<int>({0, 3}));
}
SECTION("empty and ragged inputs are safe no-ops") {
std::vector<std::vector<int>> empty_maps;
std::vector<std::vector<unsigned int>> no_filaments;
REQUIRE_NOTHROW(normalize_nozzle_map_per_layer(empty_maps, no_filaments));
REQUIRE(empty_maps.empty());
// Rows of different widths and a filament list shorter than the map list.
std::vector<std::vector<int>> ragged = {{0}, {0, 1, 2}};
std::vector<std::vector<unsigned int>> short_filaments = {{0}};
REQUIRE_NOTHROW(normalize_nozzle_map_per_layer(ragged, short_filaments));
REQUIRE(ragged[0] == std::vector<int>({0}));
}
SECTION("a single layer is left unchanged") {
std::vector<std::vector<int>> maps = {{2, 1, 0}};
std::vector<std::vector<unsigned int>> filaments = {{0, 1, 2}};
normalize_nozzle_map_per_layer(maps, filaments);
REQUIRE(maps[0] == std::vector<int>({2, 1, 0}));
}
}
TEST_CASE("Stitched sequential blocks resolve per-layer after normalization", "[MultiNozzle][H2C][Dynamic]")
{
// Shape of the sequential (by-object) stitch: two per-object plan blocks concatenated on one
// global layer axis, where the second object's plan moves filament 1 to another physical
// nozzle. After normalization the 4-arg create() must detect the migration (selector result)
// and resolve stable ids inside each object's layer range.
std::vector<NozzleInfo> nozzle_list;
for (int g = 0; g < 3; ++g) {
NozzleInfo n;
n.diameter = "0.4";
n.volume_type = nvtStandard;
n.extruder_id = (g == 0) ? 0 : 1;
n.group_id = g;
nozzle_list.push_back(n);
}
// Object A (layers 0-1): filament 1 on nozzle 1, filament 0 idle until layer 1.
// Object B (layers 2-3): filament 1 moved to nozzle 2.
std::vector<std::vector<int>> stitched_maps = {
{-1, 1},
{0, 1},
{0, 2},
{0, 2},
};
std::vector<std::vector<unsigned int>> stitched_filaments = {{1}, {0, 1}, {0, 1}, {0, 1}};
std::vector<unsigned int> used_filaments = {0, 1};
normalize_nozzle_map_per_layer(stitched_maps, stitched_filaments);
REQUIRE(stitched_maps[0] == std::vector<int>({0, 1})); // filament 0 back-filled to its first nozzle
auto group_opt = LayeredNozzleGroupResult::create(stitched_maps, nozzle_list, used_filaments, stitched_filaments);
REQUIRE(group_opt.has_value());
auto &group = *group_opt;
// A filament on two physical nozzles across the objects => selector result.
REQUIRE(group.is_support_dynamic_nozzle_map());
REQUIRE(group.get_nozzle_id(1, 0) == 1);
REQUIRE(group.get_nozzle_id(1, 1) == 1);
REQUIRE(group.get_nozzle_id(1, 2) == 2); // second object's range
REQUIRE(group.get_nozzle_id(1, 3) == 2);
// The default (out-of-range) map is the first layer's normalized row.
REQUIRE(group.get_nozzle_id(0, 999) == 0);
REQUIRE(group.get_nozzle_id(1, 999) == 1);
}
TEST_CASE("Sequential selector prints publish a stitched result and cache the plans", "[Print][H2C][Dynamic]")
{
// By-object + smart filament assign: the by-object branch of Print::process must plan each
// object with nozzle-status threading, cache the plans for the g-code export, stitch them
// into the published print-wide result, and write the grouping result back to the config
// once (per-object orderings must not churn the config).
DynamicPrintConfig config = DynamicPrintConfig::full_print_config();
config.option<ConfigOptionFloats>("nozzle_diameter", true)->values = {0.4, 0.4};
config.option<ConfigOptionStrings>("extruder_nozzle_stats", true)->values = {"Standard#1", "Standard#1|High Flow#2"};
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 High Flow",
"Direct Drive Standard,Direct Drive High Flow"};
config.option<ConfigOptionInts>("print_extruder_id", true)->values = {1, 1, 2, 2};
config.option<ConfigOptionStrings>("print_extruder_variant", true)->values = {"Direct Drive Standard", "Direct Drive High Flow",
"Direct Drive Standard", "Direct Drive High Flow"};
config.option<ConfigOptionFloats>("filament_diameter", true)->values = {1.75, 1.75};
config.option<ConfigOptionStrings>("filament_colour", true)->values = {"#FF0000", "#00FF00"};
config.option<ConfigOptionInts>("filament_map", true)->values = {1, 2};
config.option<ConfigOptionInts>("filament_volume_map", true)->values = {(int) nvtStandard, (int) nvtStandard};
config.set_key_value("enable_filament_dynamic_map", new ConfigOptionBool(true));
config.option<ConfigOptionEnum<FilamentMapMode>>("filament_map_mode", true)->value = FilamentMapMode::fmmAutoForFlush;
config.option<ConfigOptionEnum<PrintSequence>>("print_sequence", true)->value = PrintSequence::ByObject;
// Export validates flush_volumes_matrix as filaments^2 values per head.
config.option<ConfigOptionFloats>("flush_volumes_matrix", true)->values = std::vector<double>(8, 140.);
config.option<ConfigOptionFloats>("flush_multiplier", true)->values = {1., 1.};
Model model;
ModelObject *object_a = model.add_object("cube_a", "", make_cube(20, 20, 20));
ModelInstance *instance_a = object_a->add_instance();
instance_a->set_offset(Vec3d(70., 100., 0.));
ModelObject *object_b = model.add_object("cube_b", "", make_cube(20, 20, 20));
object_b->config.set_key_value("extruder", new ConfigOptionInt(2));
ModelInstance *instance_b = object_b->add_instance();
instance_b->set_offset(Vec3d(150., 100., 0.));
// The sequential instance ordering keys on arrange_order, which validate() assigns before
// process() in the real pipeline (instances tying at 0 get dropped from the ordering);
// initialize it here since the test drives process() directly.
instance_a->arrange_order = 1;
instance_b->arrange_order = 2;
Print print;
print.apply(model, config);
REQUIRE(print.objects().size() == 2);
print.process();
REQUIRE(print.is_step_done(psSlicingFinished));
auto result = print.get_layered_nozzle_group_result();
REQUIRE(result != nullptr);
// One cached plan per unique object, and a stitched layer axis spanning both objects.
REQUIRE(print.sequential_dynamic_orderings().size() == 2);
REQUIRE(result->get_layer_count() > 0);
// The write-back mirrors the stitched result's extruder map.
REQUIRE(print.config().filament_map.values == result->get_extruder_map(false));
// The per-slot filament arrays stay label-consistent whether or not the stitched plan
// actually migrated a filament (one slot per filament, plus one per extra variant).
REQUIRE(print.config().filament_extruder_variant.values.size() == print.config().filament_self_index.values.size());
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);
}
TEST_CASE("Per-variant expansion gives migrating filaments one slot per variant", "[PrintConfig][H2C][Dynamic]")
{
// The selector write-back rebuilds the filament arrays from the grouping result: a filament
// that prints through several (extruder x volume type) variants keeps one slot per variant,
// and every key grows in lockstep with the self-index / variant labels.
DynamicPrintConfig config = DynamicPrintConfig::full_print_config();
// Extruder 1 Standard, extruder 2 Hybrid (Standard + High Flow): 3 nozzle slots, 2 extruders.
config.option<ConfigOptionEnumsGeneric>("extruder_type", true)->values = {etDirectDrive, etDirectDrive};
config.option<ConfigOptionEnumsGeneric>("nozzle_volume_type", true)->values = {nvtStandard, nvtHybrid};
config.option<ConfigOptionStrings>("extruder_variant_list", true)->values = {"Direct Drive Standard,Direct Drive High Flow",
"Direct Drive Standard,Direct Drive High Flow"};
config.option<ConfigOptionInts>("print_extruder_id", true)->values = {1, 1, 2, 2};
config.option<ConfigOptionStrings>("print_extruder_variant", true)->values = {"Direct Drive Standard", "Direct Drive High Flow",
"Direct Drive Standard", "Direct Drive High Flow"};
// Two filaments with superset arrays: one column per (filament x variant).
config.option<ConfigOptionInts>("filament_map", true)->values = {1, 2};
config.option<ConfigOptionInts>("filament_volume_map", true)->values = {(int) nvtStandard, (int) nvtHighFlow};
config.option<ConfigOptionInts>("filament_self_index", true)->values = {1, 1, 2, 2};
config.option<ConfigOptionStrings>("filament_extruder_variant", true)->values = {"Direct Drive Standard", "Direct Drive High Flow",
"Direct Drive Standard", "Direct Drive High Flow"};
config.option<ConfigOptionInts>("nozzle_temperature", true)->values = {220, 230, 240, 250};
std::set<std::string> key_set = {"filament_self_index", "filament_extruder_variant", "nozzle_temperature"};
auto make_use = [](ExtruderType et, NozzleVolumeType nvt, int extruder_id) {
FilamentVariantUse use;
use.extruder_type = et;
use.nozzle_volume_type = nvt;
use.extruder_id = extruder_id;
return use;
};
SECTION("a migrating filament expands, machine slots track each output slot") {
std::unordered_map<int, std::vector<FilamentVariantUse>> uses;
uses[0] = {make_use(etDirectDrive, nvtStandard, 0), make_use(etDirectDrive, nvtHighFlow, 1)};
uses[1] = {make_use(etDirectDrive, nvtHighFlow, 1)};
std::vector<int> slot_machine_indices;
config.update_filament_config_values_for_multiple_extruders(config, uses, 2, 3, key_set,
"filament_self_index", "filament_extruder_variant",
&slot_machine_indices);
REQUIRE(config.option<ConfigOptionInts>("filament_self_index")->values == std::vector<int>{1, 1, 2});
REQUIRE(config.option<ConfigOptionStrings>("filament_extruder_variant")->values ==
std::vector<std::string>({"Direct Drive Standard", "Direct Drive High Flow", "Direct Drive High Flow"}));
REQUIRE(config.option<ConfigOptionInts>("nozzle_temperature")->values == std::vector<int>{220, 230, 250});
// Slot 0 backs onto extruder 1 Standard; slots 1-2 onto extruder 2 High Flow.
REQUIRE(slot_machine_indices == std::vector<int>{0, 3, 3});
}
SECTION("filaments absent from the uses fall back to their static assignment") {
std::unordered_map<int, std::vector<FilamentVariantUse>> uses;
uses[0] = {make_use(etDirectDrive, nvtStandard, 0)};
// Filament 1 unrouted: filament_map -> extruder 2 (Hybrid) -> volume map -> High Flow.
config.update_filament_config_values_for_multiple_extruders(config, uses, 2, 3, key_set,
"filament_self_index", "filament_extruder_variant");
REQUIRE(config.option<ConfigOptionInts>("filament_self_index")->values == std::vector<int>{1, 2});
REQUIRE(config.option<ConfigOptionInts>("nozzle_temperature")->values == std::vector<int>{220, 250});
}
SECTION("a mis-sized filament_volume_map is ignored") {
config.option<ConfigOptionInts>("filament_volume_map", true)->values = {(int) nvtHighFlow};
std::unordered_map<int, std::vector<FilamentVariantUse>> uses;
uses[0] = {make_use(etDirectDrive, nvtStandard, 0)};
// Unrouted filament 1 keeps the extruder's own typing (Hybrid folds to Standard).
config.update_filament_config_values_for_multiple_extruders(config, uses, 2, 3, key_set,
"filament_self_index", "filament_extruder_variant");
REQUIRE(config.option<ConfigOptionInts>("nozzle_temperature")->values == std::vector<int>{220, 240});
}
}
TEST_CASE("Selector write-back expands migrating filaments and survives re-apply", "[Print][H2C][Dynamic]")
{
// A filament the per-layer plan moves between nozzle variants must end up with one config
// slot per variant (so per-layer temperatures/retractions resolve correctly), the extruder
// retract overrides must key each slot to its own variant's machine value, and an unchanged
// re-apply must reproduce the expansion instead of trimming it back to one slot per
// filament — a trim-back would diff the freshly written values and invalidate the result.
DynamicPrintConfig config = DynamicPrintConfig::full_print_config();
config.option<ConfigOptionFloats>("nozzle_diameter", true)->values = {0.4, 0.4};
config.option<ConfigOptionStrings>("extruder_nozzle_stats", true)->values = {"Standard#1", "Standard#1|High Flow#2"};
config.option<ConfigOptionEnumsGeneric>("extruder_type", true)->values = {etDirectDrive, etDirectDrive};
config.option<ConfigOptionEnumsGeneric>("nozzle_volume_type", true)->values = {nvtStandard, nvtHybrid};
config.option<ConfigOptionStrings>("extruder_variant_list", true)->values = {"Direct Drive Standard,Direct Drive High Flow",
"Direct Drive Standard,Direct Drive High Flow"};
config.option<ConfigOptionInts>("print_extruder_id", true)->values = {1, 1, 2, 2};
config.option<ConfigOptionStrings>("print_extruder_variant", true)->values = {"Direct Drive Standard", "Direct Drive High Flow",
"Direct Drive Standard", "Direct Drive High Flow"};
// Three filaments: 0 -> extruder 1 (Std), 1 -> extruder 2 (Std), 2 -> extruder 2, migrating
// Standard -> High Flow between layers. Superset arrays: one column per (filament x variant).
// filament_type must be sized to the filament count: the variant-use collection (like the
// full-config producers) keys the per-filament loop on it.
config.option<ConfigOptionStrings>("filament_type", true)->values = {"PLA", "PLA", "PLA"};
config.option<ConfigOptionFloats>("filament_diameter", true)->values = {1.75, 1.75, 1.75};
config.option<ConfigOptionStrings>("filament_colour", true)->values = {"#FF0000", "#00FF00", "#0000FF"};
config.option<ConfigOptionInts>("filament_map", true)->values = {1, 2, 2};
config.option<ConfigOptionInts>("filament_volume_map", true)->values = {(int) nvtStandard, (int) nvtStandard, (int) nvtStandard};
config.option<ConfigOptionInts>("filament_self_index", true)->values = {1, 1, 2, 2, 3, 3};
config.option<ConfigOptionStrings>("filament_extruder_variant", true)->values = {"Direct Drive Standard", "Direct Drive High Flow",
"Direct Drive Standard", "Direct Drive High Flow",
"Direct Drive Standard", "Direct Drive High Flow"};
config.option<ConfigOptionInts>("nozzle_temperature", true)->values = {200, 210, 220, 230, 240, 250};
// The migrating filament's Standard column is nil, so the override merge must fall back to
// the machine value of the Standard slot (not the High Flow one).
config.option<ConfigOptionFloatsNullable>("filament_retraction_length", true)->values =
{0.5, 0.5, 0.6, 0.6, ConfigOptionFloatsNullable::nil_value(), 1.2};
config.option<ConfigOptionFloats>("retraction_length", true)->values = {0.8, 0.9, 1.0, 1.1};
Model model;
model.add_object("cube", "", make_cube(20, 20, 20))->add_instance();
Print print;
print.apply(model, config);
// Stub grouping result: nozzles as in the resolver test; filament 2 prints on the Standard
// nozzle at layer 0 and on the High Flow nozzle at layer 1.
std::vector<NozzleInfo> nozzle_list;
{
NozzleInfo n;
n.diameter = "0.4";
n.volume_type = nvtStandard; n.extruder_id = 0; n.group_id = 0; nozzle_list.push_back(n);
n.volume_type = nvtStandard; n.extruder_id = 1; n.group_id = 1; nozzle_list.push_back(n);
n.volume_type = nvtHighFlow; n.extruder_id = 1; n.group_id = 2; nozzle_list.push_back(n);
}
std::vector<std::vector<int>> layer_maps = {{0, 1, 1}, {0, 1, 2}};
std::vector<std::vector<unsigned int>> layer_seqs = {{0, 1, 2}, {0, 1, 2}};
auto group = LayeredNozzleGroupResult::create(layer_maps, nozzle_list, {0, 1, 2}, layer_seqs);
REQUIRE(group.has_value());
REQUIRE(group->is_support_dynamic_nozzle_map());
print.set_nozzle_group_result(std::make_shared<LayeredNozzleGroupResult>(*group));
print.update_to_config_by_nozzle_group_result(*group);
// Filament 2 holds two slots (Standard + High Flow), everything in lockstep.
REQUIRE(print.config().filament_map.values == group->get_extruder_map(false));
REQUIRE(print.config().filament_self_index.values == std::vector<int>{1, 2, 3, 3});
REQUIRE(print.config().nozzle_temperature.values == std::vector<int>{200, 220, 240, 250});
// The layer-aware resolver picks the slot matching each layer's variant.
REQUIRE(print.get_filament_config_indx(2, 0) == 2);
REQUIRE(print.get_filament_config_indx(2, 1) == 3);
// Retract overrides: non-nil slots take the filament value; the nil Standard slot of the
// migrating filament falls back to its own variant's machine value.
const auto &machine_retract = print.full_print_config().option<ConfigOptionFloats>("retraction_length")->values;
int f2_std_machine_slot = print.full_print_config().get_index_for_extruder(2, "print_extruder_id", etDirectDrive, nvtStandard,
"print_extruder_variant");
REQUIRE(f2_std_machine_slot >= 0);
const std::vector<double> merged_retract = print.config().retraction_length.values;
REQUIRE(merged_retract.size() == 4);
REQUIRE_THAT(merged_retract[0], Catch::Matchers::WithinAbs(0.5, 1e-9));
REQUIRE_THAT(merged_retract[1], Catch::Matchers::WithinAbs(0.6, 1e-9));
REQUIRE_THAT(merged_retract[2], Catch::Matchers::WithinAbs(machine_retract[f2_std_machine_slot], 1e-9));
REQUIRE_THAT(merged_retract[3], Catch::Matchers::WithinAbs(1.2, 1e-9));
// Re-apply the unchanged config: the persisted result must reproduce the exact expansion.
auto status = print.apply(model, config);
REQUIRE(status != PrintBase::APPLY_STATUS_INVALIDATED);
REQUIRE(print.config().filament_self_index.values == std::vector<int>{1, 2, 3, 3});
REQUIRE(print.config().nozzle_temperature.values == std::vector<int>{200, 220, 240, 250});
REQUIRE(print.config().retraction_length.values == merged_retract);
}
TEST_CASE("Filaments ordered after a migrator shift columns and the resolver tracks them", "[Print][H2C][Dynamic]")
{
// When a mid-list filament expands to two columns, every later filament's values move one
// column to the right — a raw get_at(filament_id) lands in the migrator's second column.
// The layer-aware resolver must return the shifted column for both the expanded filament
// arrays and the merged machine overrides.
DynamicPrintConfig config = DynamicPrintConfig::full_print_config();
config.option<ConfigOptionFloats>("nozzle_diameter", true)->values = {0.4, 0.4};
config.option<ConfigOptionStrings>("extruder_nozzle_stats", true)->values = {"Standard#1", "Standard#1|High Flow#2"};
config.option<ConfigOptionEnumsGeneric>("extruder_type", true)->values = {etDirectDrive, etDirectDrive};
config.option<ConfigOptionEnumsGeneric>("nozzle_volume_type", true)->values = {nvtStandard, nvtHybrid};
config.option<ConfigOptionStrings>("extruder_variant_list", true)->values = {"Direct Drive Standard,Direct Drive High Flow",
"Direct Drive Standard,Direct Drive High Flow"};
config.option<ConfigOptionInts>("print_extruder_id", true)->values = {1, 1, 2, 2};
config.option<ConfigOptionStrings>("print_extruder_variant", true)->values = {"Direct Drive Standard", "Direct Drive High Flow",
"Direct Drive Standard", "Direct Drive High Flow"};
// Three filaments: 0 -> extruder 1 (Std), 1 -> extruder 2, migrating Standard -> High Flow
// between layers, 2 -> extruder 2 (Std) — ordered AFTER the migrator.
config.option<ConfigOptionStrings>("filament_type", true)->values = {"PLA", "PLA", "PLA"};
config.option<ConfigOptionFloats>("filament_diameter", true)->values = {1.75, 1.75, 1.75};
config.option<ConfigOptionStrings>("filament_colour", true)->values = {"#FF0000", "#00FF00", "#0000FF"};
config.option<ConfigOptionInts>("filament_map", true)->values = {1, 2, 2};
config.option<ConfigOptionInts>("filament_volume_map", true)->values = {(int) nvtStandard, (int) nvtStandard, (int) nvtStandard};
config.option<ConfigOptionInts>("filament_self_index", true)->values = {1, 1, 2, 2, 3, 3};
config.option<ConfigOptionStrings>("filament_extruder_variant", true)->values = {"Direct Drive Standard", "Direct Drive High Flow",
"Direct Drive Standard", "Direct Drive High Flow",
"Direct Drive Standard", "Direct Drive High Flow"};
config.option<ConfigOptionInts>("nozzle_temperature", true)->values = {200, 210, 220, 230, 240, 250};
config.option<ConfigOptionFloatsNullable>("filament_retraction_length", true)->values = {0.5, 0.5, 0.7, 0.9, 1.4, 1.4};
config.option<ConfigOptionFloats>("retraction_length", true)->values = {0.8, 0.9, 1.0, 1.1};
Model model;
model.add_object("cube", "", make_cube(20, 20, 20))->add_instance();
Print print;
print.apply(model, config);
std::vector<NozzleInfo> nozzle_list;
{
NozzleInfo n;
n.diameter = "0.4";
n.volume_type = nvtStandard; n.extruder_id = 0; n.group_id = 0; nozzle_list.push_back(n);
n.volume_type = nvtStandard; n.extruder_id = 1; n.group_id = 1; nozzle_list.push_back(n);
n.volume_type = nvtHighFlow; n.extruder_id = 1; n.group_id = 2; nozzle_list.push_back(n);
}
// Filament 1: Standard nozzle on layer 0, High Flow nozzle on layer 1; filament 2 stays Standard.
std::vector<std::vector<int>> layer_maps = {{0, 1, 1}, {0, 2, 1}};
std::vector<std::vector<unsigned int>> layer_seqs = {{0, 1, 2}, {0, 1, 2}};
auto group = LayeredNozzleGroupResult::create(layer_maps, nozzle_list, {0, 1, 2}, layer_seqs);
REQUIRE(group.has_value());
REQUIRE(group->is_support_dynamic_nozzle_map());
print.set_nozzle_group_result(std::make_shared<LayeredNozzleGroupResult>(*group));
print.update_to_config_by_nozzle_group_result(*group);
// Filament 1 holds columns 1-2; filament 2's values shift to column 3.
REQUIRE(print.config().filament_self_index.values == std::vector<int>{1, 2, 2, 3});
REQUIRE(print.config().nozzle_temperature.values == std::vector<int>{200, 220, 230, 240});
// The migrator resolves per layer to its two columns.
REQUIRE(print.get_filament_config_indx(1, 0) == 1);
REQUIRE(print.get_filament_config_indx(1, 1) == 2);
// The filament after it no longer lives at its raw index on any layer.
REQUIRE(print.get_filament_config_indx(2, 0) == 3);
REQUIRE(print.get_filament_config_indx(2, 1) == 3);
// Merged machine override: filament 2's value sits in the shifted column, while a raw
// get_at(2) would read the migrator's High Flow column.
const std::vector<double> merged = print.config().retraction_length.values;
REQUIRE(merged.size() == 4);
REQUIRE_THAT(merged[3], Catch::Matchers::WithinAbs(1.4, 1e-9));
REQUIRE_THAT(merged[2], Catch::Matchers::WithinAbs(0.9, 1e-9));
}
TEST_CASE("Selector slicing keeps the result valid across re-apply", "[Print][H2C][Dynamic]")
{
// The dynamic counterpart of the static re-apply test above: a full process() run through
// the selector branch (whatever grouping it settles on) must leave the config in a state
// the next apply reproduces without invalidating the freshly sliced result.
DynamicPrintConfig config = DynamicPrintConfig::full_print_config();
config.option<ConfigOptionFloats>("nozzle_diameter", true)->values = {0.4, 0.4};
config.option<ConfigOptionStrings>("extruder_nozzle_stats", true)->values = {"Standard#1", "Standard#1|High Flow#2"};
config.option<ConfigOptionEnumsGeneric>("extruder_type", true)->values = {etDirectDrive, etDirectDrive};
config.option<ConfigOptionEnumsGeneric>("nozzle_volume_type", true)->values = {nvtStandard, nvtHybrid};
config.option<ConfigOptionStrings>("extruder_variant_list", true)->values = {"Direct Drive Standard,Direct Drive High Flow",
"Direct Drive Standard,Direct Drive High Flow"};
config.option<ConfigOptionInts>("print_extruder_id", true)->values = {1, 1, 2, 2};
config.option<ConfigOptionStrings>("print_extruder_variant", true)->values = {"Direct Drive Standard", "Direct Drive High Flow",
"Direct Drive Standard", "Direct Drive High Flow"};
config.option<ConfigOptionFloats>("filament_diameter", true)->values = {1.75, 1.75, 1.75};
config.option<ConfigOptionStrings>("filament_colour", true)->values = {"#FF0000", "#00FF00", "#0000FF"};
config.option<ConfigOptionInts>("filament_map", true)->values = {1, 2, 2};
config.option<ConfigOptionInts>("filament_volume_map", true)->values = {(int) nvtStandard, (int) nvtStandard, (int) nvtHighFlow};
config.set_key_value("enable_filament_dynamic_map", new ConfigOptionBool(true));
config.option<ConfigOptionEnum<FilamentMapMode>>("filament_map_mode", true)->value = FilamentMapMode::fmmAutoForFlush;
Model model;
ModelObject *object = model.add_object("cube", "", make_cube(20, 20, 20));
object->add_instance()->set_offset(Vec3d(100., 100., 0.));
Print print;
print.apply(model, config);
print.process();
REQUIRE(print.is_step_done(psSlicingFinished));
auto status = print.apply(model, config);
REQUIRE(status != PrintBase::APPLY_STATUS_INVALIDATED);
REQUIRE(print.is_step_done(psSlicingFinished));
}