Files
OrcaSlicer/tests/libslic3r/test_review_fixes.cpp
SoftFever 103cf247e5 Update from FS at b3c41fda4.
Slicing
  - align merge_segmented_layers shape with FS apply_mm_segmentation
    (size = num_facets_states, loop from 0, no -1 shift); painted mixed
    regions were previously attributed to filament_id-1 of intent.
    apply_fuzzy_skin_segmentation reads channel 1;
    apply_mixed_surface_indentation uses segmentation_channel_filament_id
  - port apply_mixed_surface_indentation, apply_mixed_component_surface_offsets,
    apply_mixed_region_surface_offsets, apply_surface_emboss_mixed_region_override,
    plus surface_emboss_mixed_* debug subsystem
  - refactor apply_mm_segmentation (by-value MM, bias_mode, surface-type-
    preserving intersection, region normalization, post-MM dump); hoist MM
    segmentation into slice_volumes so mixed apply_* flow can mutate it
  - restore clear_local_z_plan() invalidation hooks
    (PrintObject.cpp:805/1264/1286)

  GCode
  - add LayerTools::preserve_extruder_order, honored by collect_extruders,
    both reorder_extruders overloads, and
    reorder_filaments_for_minimum_flush_volume; helpers
    append_unique_preserve_order / remove_duplicates_preserve_order
  - wire MixedFilamentManager::ordered_perimeter_extruders for grouped
    manual-pattern walls; set preserve_extruder_order when >= 2
  - mixed-aware support: layer_height set for support-only layers,
    ExtrusionRole-based has_support/has_interface with erMixed short-
    circuit, support_filament / support_interface_filament routed through
    resolve_mixed

  Print
  - materialize mixed_filament_pointillism_{pixel_size,line_gap} in
    PrintApply's option-tracking block so in-session edits diff correctly

  GUI
  - Tab::on_value_change: dithering_local_z_mode cascading clears, 17-key
    project_config sync, update_mixed_filament_panel(false) on
    mixed_filament_component_bias_enabled change
  - GUI_Factories: physical_filaments_count, ui_ordered_filament_ids,
    filament_menu_item_name; filaments_count includes enabled mixed
    virtuals; right-click 'Change filament' submenus iterate UI-ordered IDs

  Tests
  - sentinel asserting MultiMaterialSegmentation uses FS-aligned shape
    (39 -> 40)
2026-05-02 12:34:18 +08:00

190 lines
8.1 KiB
C++

#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 <boost/filesystem.hpp>
#include <fstream>
#include <sstream>
#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);
}
}
TEST_CASE("[review-fixes] clear_local_z_plan called from clear_layers", "[review-fixes][mixed-filament]")
{
// Sentinel: ensure the source contains the 3 invalidation hooks the
// FullSpectrum verification report flagged as missing. This is a
// smoke-test against the source so we can detect future regressions
// without depending on a full slicing-pipeline harness.
namespace fs = boost::filesystem;
const fs::path repo = fs::path(__FILE__).parent_path().parent_path().parent_path();
const fs::path src = repo / "src" / "libslic3r" / "PrintObject.cpp";
REQUIRE(fs::exists(src));
std::ifstream in(src.string());
std::stringstream buf; buf << in.rdbuf();
const std::string body = buf.str();
const auto count_substr = [&](const std::string &needle) {
size_t n = 0, pos = 0;
while ((pos = body.find(needle, pos)) != std::string::npos) { ++n; ++pos; }
return n;
};
// Three FS-mandated call sites: clear_layers, invalidate_step(posSlice),
// invalidate_all_steps. Plus the one already-present site inside
// build_local_z_plan(). PrintObject.cpp itself has 3 (4 once the
// backport is complete).
REQUIRE(count_substr("this->clear_local_z_plan()") >= 3);
}
TEST_CASE("[review-fixes] merge_segmented_layers preserves channel 0", "[review-fixes][mixed-filament]")
{
// Sentinel: ensure merge_segmented_layers uses the FS shape
// (output sized num_facets_states, channel 0 = default), not the
// pre-FS-a11b70e3a shape (output sized num_facets_states - 1,
// channel 0 dropped). The FS-verbatim apply_mm_segmentation in
// PrintObjectSlice.cpp expects channel 0 to be present; mismatching
// shapes silently shifts every painted region's filament_id by -1
// (e.g. paint with mixed slot 4 -> applied as physical filament 3).
namespace fs = boost::filesystem;
const fs::path repo = fs::path(__FILE__).parent_path().parent_path().parent_path();
const fs::path src = repo / "src" / "libslic3r" / "MultiMaterialSegmentation.cpp";
REQUIRE(fs::exists(src));
std::ifstream in(src.string());
std::stringstream buf; buf << in.rdbuf();
const std::string body = buf.str();
// Producer must NOT subtract one from num_facets_states when sizing the output.
REQUIRE(body.find("num_facets_states - 1") == std::string::npos);
// Producer must NOT shift indexing back by one when writing into the output.
REQUIRE(body.find("[extruder_id - 1]") == std::string::npos);
// Loop must include channel 0 (extruder_id starts at 0, not 1).
REQUIRE(body.find("for (size_t extruder_id = 0; extruder_id < num_facets_states") != std::string::npos);
}