mirror of
https://github.com/OrcaSlicer/OrcaSlicer.git
synced 2026-05-14 09:02:06 +00:00
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)
190 lines
8.1 KiB
C++
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);
|
|
}
|