mirror of
https://github.com/OrcaSlicer/OrcaSlicer.git
synced 2026-05-14 09:02:06 +00:00
Integrations up to commit b3c41fda4180a2946812726218d0a849be0aedb6.
- libslic3r: vendor FilamentMixer; MixedFilamentManager (auto-gen, resolve,
serialize; manual-pattern / gradient / pointillism); 19 new PrintConfig
keys; PresetBundle owns the canonical manager with 3MF + AppConfig
roundtrip and AMS-safe strip+restore; Print owns the slicing-time copy
with PrintApply auto-regen on color change; TriangleSelector::
shift_states_above + filament-id remap; inset_idx propagation through
ExtrusionPath/Loop/MultiPath copy/assign.
- Slicing: virtual filament IDs in painted regions (same-physical channels
collapse when mixed_filament_region_collapse is on); ByObject
collect_filament_data expands mixed slots; pair-cadence + whole-object +
3+component Local-Z plan generators; LocalZOrderOptimizer utility.
- GCode + ToolOrdering: LayerTools resolves virtual IDs through wall /
infill / sparse / solid queries; SameLayerPointillisme in process_layer
(uniform-segment + grouped per-perimeter-index splitters); WipeTower2
Local-Z reservation + sub-layer G-code emission; per-layer infill
filament override.
- PartPlate: get_extruders* expand virtual slots into physical components;
CLI path rebuilds a local manager from full_config.
- GUI: five widget files extracted from FS Plater.cpp (~5000 LOC) —
MixedMixPreview, MixedGradientSelector + WeightsDialog,
MixedFilamentColorMapPanel, MixedFilamentColorMatchDialog (ΔE₀₀ recipe
search), MixedFilamentConfigPanel; Sidebar Mixed Filaments panel
(drag-reorder, enable/delete, Add Gradient/Pattern/Color); Tab exposure
of mixed-filament / dithering / per-layer infill-override settings +
ConfigManipulation visibility and slot-validation rules;
BBLMixedFilamentBroken / BBLSingleExtruderMixedFilamentRisk
notifications + slice gate; WipeTowerDialog edits physical P×P
sub-matrix; bounds-safe extruder_id guards in 3DScene / GLCanvas3D /
GLGizmoMmuSegmentation; change_filament merge guard and
on_filaments_delete is_mixed_before_delete propagation.
- Tests: 4 Catch2 tests for 3MF roundtrip (auto/custom persistence,
PresetBundle string path, total_filaments stability); full-pipeline
slice E2E deferred — TODO in file.
Co-authored-by: Rad <radugheorghiu96@gmail.com>
Co-authored-by: Justin Hayes <justinh@rahb.ca>
Co-authored-by: Calogero Guagenti <calogeroguagenti@gmail.com>
Co-authored-by: xSil3nt <ahmedshazin21@gmail.com>
Co-authored-by: ratdoux <62392831+ratdoux@users.noreply.github.com>
Co-authored-by: Rad <radugheorghiu96@gmail.com>
Co-authored-by: Justin Hayes <justinh@rahb.ca>
Co-authored-by: Calogero Guagenti <calogeroguagenti@gmail.com>
Co-authored-by: xSil3nt <ahmedshazin21@gmail.com>
Co-authored-by: ratdoux <62392831+ratdoux@users.noreply.github.com>
219 lines
9.0 KiB
C++
219 lines
9.0 KiB
C++
// Tests for mixed-filament project-config round-trip persistence.
|
|
//
|
|
// The full-3MF (store_bbs_3mf / load_bbs_3mf) round-trip requires a heavy
|
|
// PlateDataPtrs + Model + PresetBundle setup that is not yet scaffolded in
|
|
// fff_print tests. We therefore exercise the *persistence layer* directly:
|
|
//
|
|
// MixedFilamentManager::serialize_custom_entries()
|
|
// → stored in project_config["mixed_filament_definitions"]
|
|
// → PresetBundle::sync_mixed_filaments_to_config() (mirrors store path)
|
|
// → PresetBundle::sync_mixed_filaments_from_config() (mirrors load path)
|
|
//
|
|
// This is the code path that bbs_3mf.cpp invokes when writing/reading the
|
|
// project config block, so a regression here would break 3MF persistence.
|
|
//
|
|
// TODO: full-pipeline E2E slice test (T0/T1 in G-code, dithering_local_z_mode
|
|
// sublayer assertions) requires the full Print::process() scaffolding;
|
|
// defer to a follow-up once a minimal PrintObject fixture exists.
|
|
|
|
#include <catch2/catch_all.hpp>
|
|
|
|
#include "libslic3r/MixedFilament.hpp"
|
|
#include "libslic3r/PresetBundle.hpp"
|
|
|
|
#include <algorithm>
|
|
#include <cstdint>
|
|
#include <string>
|
|
#include <vector>
|
|
|
|
using namespace Slic3r;
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Helper: build a two-filament PresetBundle with colours set.
|
|
// ---------------------------------------------------------------------------
|
|
namespace {
|
|
|
|
static PresetBundle make_bundle_2(const std::string &col_a = "#FF0000",
|
|
const std::string &col_b = "#0000FF")
|
|
{
|
|
PresetBundle bundle;
|
|
bundle.filament_presets = {"Default Filament", "Default Filament"};
|
|
bundle.project_config.option<ConfigOptionStrings>("filament_colour")->values = {col_a, col_b};
|
|
return bundle;
|
|
}
|
|
|
|
} // anonymous namespace
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Test 1 — plain auto-generated round-trip
|
|
// Build bundle → sync to config string → reload in fresh bundle → compare
|
|
// ---------------------------------------------------------------------------
|
|
TEST_CASE("Mixed filament 3MF round-trip: auto-generated entries survive serialize/load cycle",
|
|
"[MixedFilamentRoundTrip]")
|
|
{
|
|
PresetBundle origin = make_bundle_2();
|
|
origin.sync_mixed_filaments_from_config();
|
|
|
|
const auto &orig_mixed = origin.mixed_filaments.mixed_filaments();
|
|
REQUIRE(!orig_mixed.empty());
|
|
|
|
// Capture the stable_id and component IDs of the first enabled entry.
|
|
const MixedFilament *first = nullptr;
|
|
for (const auto &mf : orig_mixed) {
|
|
if (mf.enabled && !mf.deleted) { first = &mf; break; }
|
|
}
|
|
REQUIRE(first != nullptr);
|
|
const uint64_t orig_stable_id = first->stable_id;
|
|
const unsigned int orig_component_a = first->component_a;
|
|
const unsigned int orig_component_b = first->component_b;
|
|
|
|
// Sync to config (mirrors what store_bbs_3mf does).
|
|
origin.sync_mixed_filaments_to_config();
|
|
const std::string serialized = origin.project_config.opt_string("mixed_filament_definitions");
|
|
// Auto-generated entries are NOT stored in the custom definitions string —
|
|
// they are rebuilt by auto_generate() on load. The string will be empty
|
|
// unless a custom entry was added. We just verify the round-trip doesn't
|
|
// drop auto-generated rows on re-sync.
|
|
|
|
// Reload into a fresh bundle with the same colours.
|
|
PresetBundle loaded = make_bundle_2();
|
|
loaded.project_config.option<ConfigOptionString>("mixed_filament_definitions")->value = serialized;
|
|
loaded.sync_mixed_filaments_from_config();
|
|
|
|
const auto &load_mixed = loaded.mixed_filaments.mixed_filaments();
|
|
REQUIRE(load_mixed.size() == orig_mixed.size());
|
|
|
|
// Find the matching entry by component pair.
|
|
const MixedFilament *reloaded = nullptr;
|
|
for (const auto &mf : load_mixed) {
|
|
if (mf.component_a == orig_component_a && mf.component_b == orig_component_b) {
|
|
reloaded = &mf;
|
|
break;
|
|
}
|
|
}
|
|
REQUIRE(reloaded != nullptr);
|
|
CHECK(reloaded->stable_id == orig_stable_id);
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Test 2 — custom entry round-trip
|
|
// 2 physical + 1 custom mixed entry: ratio_a, ratio_b, mix_b_percent, stable_id
|
|
// ---------------------------------------------------------------------------
|
|
TEST_CASE("Mixed filament 3MF round-trip: custom entry count, components, ratio, stable_id",
|
|
"[MixedFilamentRoundTrip]")
|
|
{
|
|
const std::vector<std::string> colors = {"#FF0000", "#00FF00"};
|
|
|
|
MixedFilamentManager mgr;
|
|
// Add two custom entries to exercise the multi-entry path.
|
|
mgr.add_custom_filament(1, 2, 25, colors);
|
|
mgr.add_custom_filament(1, 2, 75, colors);
|
|
|
|
const auto &entries = mgr.mixed_filaments();
|
|
REQUIRE(entries.size() == 2);
|
|
|
|
const uint64_t stable_id_0 = entries[0].stable_id;
|
|
const uint64_t stable_id_1 = entries[1].stable_id;
|
|
CHECK(stable_id_0 != stable_id_1);
|
|
|
|
// Serialize and reload.
|
|
const std::string serialized = mgr.serialize_custom_entries();
|
|
REQUIRE(!serialized.empty());
|
|
|
|
MixedFilamentManager loaded;
|
|
loaded.load_custom_entries(serialized, colors);
|
|
|
|
const auto &reloaded = loaded.mixed_filaments();
|
|
REQUIRE(reloaded.size() == 2);
|
|
|
|
// Order must be preserved.
|
|
CHECK(reloaded[0].component_a == 1);
|
|
CHECK(reloaded[0].component_b == 2);
|
|
CHECK(reloaded[0].mix_b_percent == 25);
|
|
CHECK(reloaded[0].stable_id == stable_id_0);
|
|
|
|
CHECK(reloaded[1].component_a == 1);
|
|
CHECK(reloaded[1].component_b == 2);
|
|
CHECK(reloaded[1].mix_b_percent == 75);
|
|
CHECK(reloaded[1].stable_id == stable_id_1);
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Test 3 — PresetBundle project_config string path (mirrors 3MF store+load)
|
|
// Verify that sync_mixed_filaments_to_config + sync_mixed_filaments_from_config
|
|
// preserves a custom entry end-to-end through the project_config string.
|
|
// ---------------------------------------------------------------------------
|
|
TEST_CASE("Mixed filament 3MF round-trip: PresetBundle project_config string path",
|
|
"[MixedFilamentRoundTrip]")
|
|
{
|
|
PresetBundle origin = make_bundle_2("#FFFF00", "#FF00FF");
|
|
origin.sync_mixed_filaments_from_config();
|
|
|
|
// Add a custom entry on top of auto-generated ones.
|
|
const auto &colors = origin.project_config.option<ConfigOptionStrings>("filament_colour")->values;
|
|
origin.mixed_filaments.add_custom_filament(1, 2, 40, colors);
|
|
|
|
const size_t num_entries_before = origin.mixed_filaments.mixed_filaments().size();
|
|
|
|
// Count custom entries in origin.
|
|
size_t custom_count_before = 0;
|
|
uint64_t custom_stable_id = 0;
|
|
for (const auto &mf : origin.mixed_filaments.mixed_filaments()) {
|
|
if (mf.custom && mf.enabled && !mf.deleted) {
|
|
++custom_count_before;
|
|
custom_stable_id = mf.stable_id;
|
|
}
|
|
}
|
|
REQUIRE(custom_count_before == 1);
|
|
|
|
// Sync to config string — mirrors bbs_3mf store path.
|
|
origin.sync_mixed_filaments_to_config();
|
|
const std::string defs = origin.project_config.opt_string("mixed_filament_definitions");
|
|
REQUIRE(!defs.empty());
|
|
|
|
// Reload — mirrors bbs_3mf load path.
|
|
PresetBundle loaded = make_bundle_2("#FFFF00", "#FF00FF");
|
|
loaded.project_config.option<ConfigOptionString>("mixed_filament_definitions")->value = defs;
|
|
loaded.sync_mixed_filaments_from_config();
|
|
|
|
const auto &reloaded = loaded.mixed_filaments.mixed_filaments();
|
|
REQUIRE(reloaded.size() == num_entries_before);
|
|
|
|
// Custom entry must survive with its stable_id intact.
|
|
size_t custom_count_after = 0;
|
|
uint64_t reloaded_stable_id = 0;
|
|
for (const auto &mf : reloaded) {
|
|
if (mf.custom && mf.enabled && !mf.deleted) {
|
|
++custom_count_after;
|
|
reloaded_stable_id = mf.stable_id;
|
|
}
|
|
}
|
|
CHECK(custom_count_after == custom_count_before);
|
|
CHECK(reloaded_stable_id == custom_stable_id);
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Test 4 — total_filaments counts physical + mixed correctly after round-trip
|
|
// ---------------------------------------------------------------------------
|
|
TEST_CASE("Mixed filament 3MF round-trip: total_filaments count is stable after reload",
|
|
"[MixedFilamentRoundTrip]")
|
|
{
|
|
PresetBundle origin = make_bundle_2();
|
|
origin.sync_mixed_filaments_from_config();
|
|
|
|
const size_t num_physical = origin.filament_presets.size();
|
|
const size_t total_before = origin.mixed_filaments.total_filaments(num_physical);
|
|
// For 2 physical filaments C(2,2)=1 virtual → total must be 3.
|
|
REQUIRE(total_before == 3u);
|
|
|
|
origin.sync_mixed_filaments_to_config();
|
|
const std::string defs = origin.project_config.opt_string("mixed_filament_definitions");
|
|
|
|
PresetBundle loaded = make_bundle_2();
|
|
loaded.project_config.option<ConfigOptionString>("mixed_filament_definitions")->value = defs;
|
|
loaded.sync_mixed_filaments_from_config();
|
|
|
|
const size_t total_after = loaded.mixed_filaments.total_filaments(loaded.filament_presets.size());
|
|
CHECK(total_after == total_before);
|
|
}
|