Import project

This commit is contained in:
Ian Bassi
2026-08-14 11:52:47 -03:00
committed by SoftFever
parent 42f708bbb6
commit 3c37bf9ca4
6 changed files with 293 additions and 44 deletions

View File

@@ -1,5 +1,6 @@
#include "libslic3r/Model.hpp"
#include "libslic3r/TriangleSelector.hpp"
#include "libslic3r/Format/3mf.hpp"
#include "libslic3r/Format/bbs_3mf.hpp"
#include "libslic3r/Format/STL.hpp"
@@ -497,3 +498,97 @@ SCENARIO("Nozzle-group metadata .3mf round-trip", "[3mf][MultiNozzle]") {
delete plate;
}
}
// A mixed-color filament occupies an ordinary filament slot, and painting with it stores an
// ordinary extruder state — a project saved by BambuStudio encodes filament 5 of a 5-slot setup
// as paint state 5, with the mix described by the parallel filament_mixed_* project arrays. This
// pins both halves of that contract at the .3mf layer: the project keys and the painted states
// must come back exactly as written.
SCENARIO("Mixed-color filament setup and painting round-trip through a .3mf", "[3mf][MixedFilament]") {
GIVEN("a painted model whose project config describes a mixed filament in the last slot") {
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();
// Both the exporter and the importer stage Metadata/project_settings.config through the
// model's backup path; point them at writable temp dirs.
ScopedTemporaryDir backup_dir("orca_mixed_src");
model.set_backup_path(backup_dir.string());
ModelVolume* mv = model.objects.front()->volumes.front();
{
TriangleSelector selector(mv->mesh());
selector.set_facet(0, EnforcerBlockerType::Extruder5); // the mixed slot
selector.set_facet(1, EnforcerBlockerType::Extruder2);
REQUIRE(mv->mmu_segmentation_facets.set(selector));
}
DynamicPrintConfig config = DynamicPrintConfig::full_print_config();
config.set_key_value("filament_colour", new ConfigOptionStrings(
{ "#00AE42", "#FFFF00", "#FF0000", "#0000FF", "#FF6A26" }));
config.set_key_value("filament_is_mixed", new ConfigOptionBools(
{ false, false, false, false, true }));
config.set_key_value("filament_mixed_components", new ConfigOptionStrings(
{ "", "", "", "", "3,2" }));
config.set_key_value("filament_mixed_sublayer_ratios", new ConfigOptionStrings(
{ "", "", "", "", "0.4200,0.5800" }));
WHEN("stored to and reloaded from a .3mf") {
ScopedTemporaryFile temp(".3mf");
const std::string test_file = temp.string();
PlateData* plate = new PlateData();
plate->plate_index = 0;
StoreParams store_params;
store_params.path = test_file.c_str();
store_params.model = &model;
store_params.config = &config;
store_params.strategy = SaveStrategy::Zip64 | SaveStrategy::Silence;
store_params.plate_data_list.push_back(plate);
REQUIRE(store_bbs_3mf(store_params));
Model dst_model;
ScopedTemporaryDir dst_backup_dir("orca_mixed_dst");
dst_model.set_backup_path(dst_backup_dir.string());
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;
REQUIRE(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));
THEN("the mixed-filament project keys survive") {
auto* is_mixed = dst_config.option<ConfigOptionBools>("filament_is_mixed");
REQUIRE(is_mixed != nullptr);
REQUIRE(is_mixed->values == std::vector<unsigned char>({ 0, 0, 0, 0, 1 }));
auto* components = dst_config.option<ConfigOptionStrings>("filament_mixed_components");
REQUIRE(components != nullptr);
REQUIRE(components->values.size() == 5);
REQUIRE(components->values[4] == "3,2");
auto* ratios = dst_config.option<ConfigOptionStrings>("filament_mixed_sublayer_ratios");
REQUIRE(ratios != nullptr);
REQUIRE(ratios->values.size() == 5);
REQUIRE(ratios->values[4] == "0.4200,0.5800");
}
THEN("the painted facets survive, including the one painted with the mixed slot") {
REQUIRE(dst_model.objects.size() == 1);
ModelVolume* dst_mv = dst_model.objects.front()->volumes.front();
REQUIRE_FALSE(dst_mv->mmu_segmentation_facets.empty());
REQUIRE(dst_mv->mmu_segmentation_facets.has_facets(*dst_mv, EnforcerBlockerType::Extruder2));
REQUIRE(dst_mv->mmu_segmentation_facets.has_facets(*dst_mv, EnforcerBlockerType::Extruder5));
}
release_PlateData_list(dst_plates);
delete plate; // store_bbs_3mf does not take ownership of the source plate
}
}
}

View File

@@ -567,21 +567,25 @@ TEST_CASE("A printer specific filament supersedes the generic library filament w
}
namespace {
const char *kMixedKeys[] = {
"filament_is_mixed",
"filament_mixed_components",
"filament_mixed_sublayer_ratios",
"filament_mixed_gradient",
"filament_mixed_gradient_range",
"filament_mixed_gradient_curve",
"filament_mixed_gradient_per_part",
};
} // namespace
// Mixed-color filament metadata lives in project_config as parallel per-filament arrays.
// set_num_filaments() is the single place that grows them alongside filament_colour; if it
// misses them, creating a mixed slot writes past the end of the short arrays.
TEST_CASE("set_num_filaments keeps mixed-color arrays in step with the filament count", "[Preset][Bundle][FilamentMixer]")
{
static const char *kMixedKeys[] = {
"filament_is_mixed",
"filament_mixed_components",
"filament_mixed_sublayer_ratios",
"filament_mixed_gradient",
"filament_mixed_gradient_range",
"filament_mixed_gradient_curve",
"filament_mixed_gradient_per_part",
};
auto mixed_array_size = [](const DynamicPrintConfig &cfg, const std::string &key) -> size_t {
if (const auto *b = cfg.option<ConfigOptionBools>(key))
return b->values.size();
@@ -609,3 +613,96 @@ TEST_CASE("set_num_filaments keeps mixed-color arrays in step with the filament
CHECK(mixed_array_size(bundle.project_config, key) == 1);
}
}
// A mix is described by 1-based indices into the project's filament list, so it is only meaningful
// alongside that list. As in BambuStudio the app-config snapshot is global — one "last session"
// copy under the shared "presets" section, restored at startup only. A PER-PRINTER copy would be
// re-applied on every printer change and would replace a loaded project's mixes with whatever
// snapshot that printer last held, which also shrinks the filament count and makes reload_scene
// strip painted facets above it.
TEST_CASE("Mixed-color filament metadata is snapshotted globally, never per printer", "[Preset][Bundle][FilamentMixer]")
{
PresetBundle bundle;
// export_selections skips the built-in "Default Printer" placeholder entirely.
add_inmemory_preset(bundle.printers, "Test Printer");
bundle.printers.select_preset_by_name("Test Printer", true);
bundle.set_num_filaments(2u, std::string("#FF0000"));
bundle.project_config.option<ConfigOptionBools>("filament_is_mixed")->values = { false, true };
bundle.project_config.option<ConfigOptionStrings>("filament_mixed_components")->values = { "", "1,2" };
bundle.project_config.option<ConfigOptionStrings>("filament_mixed_sublayer_ratios")->values = { "", "0.5,0.5" };
AppConfig app_config;
bundle.export_selections(app_config);
const std::string printer_name = bundle.printers.get_selected_preset_name();
for (const char *key : kMixedKeys) {
DYNAMIC_SECTION("global, not per printer: " << key) {
CHECK(app_config.has("presets", key));
CHECK_FALSE(app_config.has_printer_setting(printer_name, key));
}
}
SECTION("with the encoding load_selections reads back") {
CHECK(app_config.get("presets", "filament_is_mixed") == "0,1");
CHECK(app_config.get("presets", "filament_mixed_components") == "|1,2");
CHECK(app_config.get("presets", "filament_mixed_sublayer_ratios") == "|0.5,0.5");
}
}
// The gradient curve is the one mixed array whose values contain '|' themselves — it separates the
// control points — so it cannot be '|'-joined into the app config like its siblings without a
// multi-point curve being split across filament slots on the way back in.
TEST_CASE("A multi-point gradient curve survives the app-config snapshot", "[Preset][Bundle][FilamentMixer]")
{
const std::vector<std::string> curves = { "", "", "0,0|0.5,0.3|1,1" };
PresetBundle bundle;
add_inmemory_preset(bundle.printers, "Test Printer");
bundle.printers.select_preset_by_name("Test Printer", true);
bundle.set_num_filaments(3u, std::string("#FF0000"));
bundle.project_config.option<ConfigOptionStrings>("filament_mixed_gradient_curve")->values = curves;
AppConfig app_config;
bundle.export_selections(app_config);
// Decoding the stored form returns the three slots intact, curve delimiters and all. A plain
// '|' join would decode as five slots here instead of three.
std::vector<std::string> decoded;
REQUIRE(unescape_strings_cstyle(app_config.get("presets", "filament_mixed_gradient_curve"), decoded));
CHECK(decoded == curves);
}
// A multi-tool printer sizes the filament list from its nozzle count. Mixed-color slots are extra
// virtual filaments at the tail of that list with no nozzle of their own, so the sync has to add
// them on top. Sizing to the nozzle count alone truncates them — and because that sync runs right
// after a project is loaded, it silently drops the project's mixes and then lets the filament-count
// change strip every painted facet above the new count.
TEST_CASE("Sizing the filament list to a multi-tool nozzle count keeps mixed slots", "[Preset][Bundle][FilamentMixer]")
{
// The 5-slot layout of a 4-tool project carrying one mix of filaments 2 and 3.
const size_t nozzle_count = 4;
PresetBundle bundle;
bundle.set_num_filaments(5u, std::string("#FF0000"));
bundle.project_config.option<ConfigOptionBools>("filament_is_mixed")->values =
{ false, false, false, false, true };
bundle.project_config.option<ConfigOptionStrings>("filament_mixed_components")->values =
{ "", "", "", "", "2,3" };
REQUIRE(bundle.num_mixed_filaments() == 1);
SECTION("nozzle count plus the mixed slots preserves the mix") {
bundle.set_num_filaments(nozzle_count + bundle.num_mixed_filaments(), std::string("#00FF00"));
CHECK(bundle.filament_presets.size() == 5);
CHECK(bundle.num_mixed_filaments() == 1);
CHECK(bundle.is_mixed_filament(4));
CHECK(bundle.project_config.option<ConfigOptionStrings>("filament_mixed_components")->values[4] == "2,3");
}
SECTION("the nozzle count alone is what truncated it away") {
bundle.set_num_filaments(nozzle_count, std::string("#00FF00"));
CHECK(bundle.filament_presets.size() == nozzle_count);
CHECK(bundle.num_mixed_filaments() == 0);
}
}