init work to integrate OrcaSlicer-FullSpectrum fork

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>
This commit is contained in:
SoftFever
2026-04-29 00:42:48 +08:00
parent e66ec7fd70
commit 9c8caf121e
68 changed files with 18988 additions and 267 deletions

View File

@@ -1,5 +1,5 @@
get_filename_component(_TEST_NAME ${CMAKE_CURRENT_LIST_DIR} NAME)
add_executable(${_TEST_NAME}_tests
add_executable(${_TEST_NAME}_tests
${_TEST_NAME}_tests.cpp
test_data.cpp
test_data.hpp
@@ -8,6 +8,7 @@ add_executable(${_TEST_NAME}_tests
test_flow.cpp
test_gcode.cpp
test_gcodewriter.cpp
test_mixed_filament_e2e.cpp
test_model.cpp
test_print.cpp
test_printgcode.cpp

View File

@@ -0,0 +1,218 @@
// 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);
}

View File

@@ -16,6 +16,8 @@ add_executable(${_TEST_NAME}_tests
test_polygon.cpp
test_mutable_polygon.cpp
test_mutable_priority_queue.cpp
test_mixed_filament.cpp
test_review_fixes.cpp
test_stl.cpp
test_meshboolean.cpp
test_marchingsquares.cpp

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,132 @@
#include <catch2/catch_all.hpp>
#include "libslic3r/MixedFilament.hpp"
#include "libslic3r/Print.hpp"
#include "libslic3r/PrintConfig.hpp"
#include "libslic3r/Model.hpp"
#include "libslic3r/Slicing.hpp"
#include <string>
#include <vector>
using namespace Slic3r;
// Finding 1 — the helper computing max supported filament ID after a 3MF
// project-config load must include enabled mixed rows.
TEST_CASE("[review-fixes] mixed-aware max filament id", "[review-fixes]")
{
MixedFilamentManager mgr;
const size_t physical_count = 3;
const std::vector<std::string> colors = {"#FF0000", "#00FF00", "#0000FF"};
// No mixed rows: max == physical.
REQUIRE(mgr.total_filaments(physical_count) == physical_count);
// Add an enabled mixed row spanning physical 1 + 2 (custom + enabled by default).
mgr.add_custom_filament(1u, 2u, 50, colors);
REQUIRE(mgr.mixed_filaments().size() == 1);
REQUIRE(mgr.total_filaments(physical_count) == physical_count + 1);
// A disabled row does not contribute.
mgr.add_custom_filament(1u, 2u, 25, colors);
REQUIRE(mgr.mixed_filaments().size() == 2);
mgr.mixed_filaments().back().enabled = false;
REQUIRE(mgr.total_filaments(physical_count) == physical_count + 1);
// A deleted row also does not contribute (enabled_count() filters deleted).
mgr.add_custom_filament(2u, 3u, 50, colors);
REQUIRE(mgr.mixed_filaments().size() == 3);
mgr.mixed_filaments().back().deleted = true;
mgr.mixed_filaments().back().enabled = false;
REQUIRE(mgr.total_filaments(physical_count) == physical_count + 1);
}
// Finding 2 — passing nullptr for the print object must keep the legacy
// behaviour: no mixed gradient or dithering ranges are applied, and the
// profile is still seeded from the slicing parameters.
TEST_CASE("[review-fixes] update_layer_height_profile passthrough without print object",
"[review-fixes]")
{
Model model;
ModelObject *mo = model.add_object();
REQUIRE(mo != nullptr);
SlicingParameters sp;
sp.layer_height = 0.2;
sp.first_object_layer_height = 0.2;
sp.object_print_z_min = 0.0;
sp.object_print_z_uncompensated_max = 10.0;
std::vector<coordf_t> profile;
const bool updated = PrintObject::update_layer_height_profile(*mo, sp, profile, nullptr);
REQUIRE(updated);
REQUIRE(!profile.empty());
// Strong check: the nullptr branch must be a passthrough to
// layer_height_profile_from_ranges with the model_object's own
// layer_config_ranges — i.e. no mixed-gradient or dithering overrides
// were applied. Computing the expected profile via the same call the
// function performs internally lets a regression that silently sneaks
// those overrides into the nullptr path fail this test.
const std::vector<coordf_t> expected =
Slic3r::layer_height_profile_from_ranges(sp, mo->layer_config_ranges);
REQUIRE(profile == expected);
}
// Finding 4 — ToolOrdering used to push raw 1-based virtual mixed-filament IDs
// directly onto layer_tools.extruders, which then got decremented as if they
// were physical extruder IDs. The fix routes those IDs through the
// MixedFilamentManager so that virtual IDs collapse to a real physical extruder
// while physical IDs pass through unchanged.
TEST_CASE("[review-fixes] resolve_mixed_1based passthrough on physical id",
"[review-fixes]")
{
MixedFilamentManager mgr;
const std::vector<std::string> colors = {"#FF0000", "#00FF00"};
// Add a custom mixed row spanning physical 1 + 2.
mgr.add_custom_filament(1u, 2u, 50, colors);
REQUIRE(mgr.mixed_filaments().size() == 1);
const size_t num_physical = colors.size();
// Physical id (1) must passthrough.
REQUIRE(mgr.resolve(1u, num_physical, /*layer_index=*/0, /*print_z=*/0.f, /*layer_height=*/0.2f) == 1u);
REQUIRE(mgr.resolve(2u, num_physical, /*layer_index=*/0, /*print_z=*/0.f, /*layer_height=*/0.2f) == 2u);
// Virtual id (3 = 2 physical + 1 mixed) must resolve to 1 or 2 depending on cadence.
const unsigned int resolved = mgr.resolve(3u, num_physical, 0, 0.f, 0.2f);
REQUIRE((resolved == 1u || resolved == 2u));
}
TEST_CASE("[review-fixes] mixed/dithering option keys exist in PrintConfig",
"[review-fixes]")
{
// Sanity guard for finding 6: any key the porter wires into invalidation
// must resolve in the config. Build a default-populated DynamicPrintConfig
// that contains every print_config_def key, then probe the option lookup.
static const std::vector<std::string> keys = {
"mixed_filament_definitions",
"mixed_filament_gradient_mode",
"mixed_filament_height_lower_bound",
"mixed_filament_height_upper_bound",
"mixed_filament_advanced_dithering",
"mixed_filament_component_bias_enabled",
"mixed_filament_surface_indentation",
"mixed_filament_region_collapse",
"dithering_z_step_size",
"dithering_local_z_mode",
"dithering_local_z_whole_objects",
"dithering_local_z_direct_multicolor",
"dithering_step_painted_zones_only",
};
std::unique_ptr<DynamicPrintConfig> cfg(DynamicPrintConfig::new_from_defaults_keys(keys));
REQUIRE(cfg != nullptr);
for (const std::string &k : keys) {
INFO("missing config key: " << k);
REQUIRE(cfg->option(k) != nullptr);
// And the canonical print_config_def must know about it.
REQUIRE(print_config_def.get(k) != nullptr);
}
}