From c9d06a1372adacc06aa848ebfdc082a037a897e7 Mon Sep 17 00:00:00 2001 From: Hanif Koh Date: Tue, 13 Oct 2026 19:39:26 +0800 Subject: [PATCH] Size the Prime Tower Estimate from a Mixed Slot's Physical Components Print::validate sizes the pre-slice prime tower from Print::extruders(), which lists every mixed filament slot under its own id, so the estimator planned one purge block per slot. The tool ordering resolves a mixed slot to one physical component on every layer, with or without colour sublayers, and the generated tower only purges those. The GUI preview and its placement clamp already expand the slots, so the tower was drawn inside the bed and then refused as partially outside it: with 8 mixed slots at 0.08 mm layers the estimate was 114 mm for a tower that prints at 64 mm. The estimator now expands mixed slots to their components itself, so validation, the GUI and the CLI placement size the same tower. --- src/libslic3r/GCode/WipeTowerEstimate.cpp | 12 ++++++++++- src/libslic3r/GCode/WipeTowerEstimate.hpp | 2 ++ tests/libslic3r/test_wipe_tower_estimate.cpp | 22 ++++++++++++++++++++ 3 files changed, 35 insertions(+), 1 deletion(-) diff --git a/src/libslic3r/GCode/WipeTowerEstimate.cpp b/src/libslic3r/GCode/WipeTowerEstimate.cpp index 9b6c7882dd..e974f9351f 100644 --- a/src/libslic3r/GCode/WipeTowerEstimate.cpp +++ b/src/libslic3r/GCode/WipeTowerEstimate.cpp @@ -4,6 +4,7 @@ #include "WipeTower.hpp" #include "WipeTower2.hpp" #include "../Config.hpp" +#include "../FilamentMixer.hpp" #include "../PrintConfig.hpp" #include "../libslic3r.h" @@ -74,10 +75,19 @@ Polygon estimate_wipe_tower_first_layer_outline(const ConfigBase &config, WipeTo return WipeTower2::cone_base_polygon(width, depth, height, cone ? cone_angle->getFloat() : 0.); } -WipeTowerFootprint estimate_wipe_tower_footprint(const ConfigBase &config, WipeTowerType tower_type, const std::vector &filament_ids, double layer_height, double max_object_height) +WipeTowerFootprint estimate_wipe_tower_footprint(const ConfigBase &config, WipeTowerType tower_type, const std::vector &filament_slots, double layer_height, double max_object_height) { WipeTowerFootprint footprint; footprint.height = max_object_height; + // A mixed slot is virtual: the tool ordering resolves it to one of its physical components + // on every layer, with or without colour sublayers, so the tower only ever purges those. + // Validation passes the slots as Print::extruders() lists them; the GUI and CLI placement + // expand them beforehand, which repeating here leaves unchanged. + std::vector filament_ids = filament_slots; + const auto *is_mixed_opt = dynamic_cast(option_of(config, "filament_is_mixed")); + const auto *components_opt = dynamic_cast(option_of(config, "filament_mixed_components")); + if (is_mixed_opt != nullptr && components_opt != nullptr && has_any_mixed_filament(is_mixed_opt->values)) + filament_ids = expand_mixed_filaments(filament_slots, is_mixed_opt->values, components_opt->values); const size_t filaments_cnt = filament_ids.size(); if (filaments_cnt == 0 || layer_height < EPSILON) return footprint; diff --git a/src/libslic3r/GCode/WipeTowerEstimate.hpp b/src/libslic3r/GCode/WipeTowerEstimate.hpp index 1f0b674a11..9dad82f51f 100644 --- a/src/libslic3r/GCode/WipeTowerEstimate.hpp +++ b/src/libslic3r/GCode/WipeTowerEstimate.hpp @@ -34,6 +34,8 @@ Polygon estimate_wipe_tower_first_layer_outline(const ConfigBase &config, WipeTo // filament_ids: 0-based filaments purged on the plate. The config cannot see custom G-code tool // changes, so ids derived from the model must include them // (Print::extruders(true)) or a real tower is sized as if it were never built. +// A mixed slot may be passed as itself or already expanded: it is sized as its +// physical components either way, since those are all the tower ever purges. // layer_height: thinnest layer the objects are sliced at. The first layer is folded in here. // // A raft is deliberately not a reason: normalize_fdm_2 clears enable_prime_tower for a plate diff --git a/tests/libslic3r/test_wipe_tower_estimate.cpp b/tests/libslic3r/test_wipe_tower_estimate.cpp index 6f520606a8..22117e686d 100644 --- a/tests/libslic3r/test_wipe_tower_estimate.cpp +++ b/tests/libslic3r/test_wipe_tower_estimate.cpp @@ -133,6 +133,28 @@ TEST_CASE("Type1 sizes the tower from each filament's own prime volume", "[WipeT CHECK_THAT(rib.depth, WithinAbs(WipeTower::estimate_rib_tower_bbox_side({{30.f, 0}, {45.f, 0}}, 35.f, 0.21f, 0.4f, 1.5f, 8.f, 0.f, 5.f), 1e-4)); } +TEST_CASE("A mixed slot is purged as its physical components", "[WipeTowerEstimate]") { + // Slots 4 and 5 blend 1+2 and 1+3: every layer prints one component per mixed slot, so a + // plate using all five slots purges the three physical filaments and nothing more. + const WipeTowerType type = GENERATE(WipeTowerType::Type1, WipeTowerType::Type2); + DynamicPrintConfig config = make_config(); + config.set_key_value("filament_prime_volume", new ConfigOptionFloats({100., 100., 100., 100., 100.})); + config.set_key_value("filament_adhesiveness_category", new ConfigOptionInts({0, 0, 0, 0, 0})); + config.set_key_value("filament_is_mixed", new ConfigOptionBools(std::vector{0, 0, 0, 1, 1})); + config.set_key_value("filament_mixed_components", new ConfigOptionStrings({"", "", "", "1,2", "1,3"})); + const double physical = estimate(config, 3, 0.2, 5., type).depth; + REQUIRE(physical > estimate(config, 2, 0.2, 5., type).depth); + CHECK_THAT(estimate(config, 5, 0.2, 5., type).depth, WithinAbs(physical, 1e-9)); + // The same slots with the mixed flags cleared are five filaments, which is what sized the + // tower before and refused positions the printed tower fits. + DynamicPrintConfig unmixed = config; + unmixed.set_key_value("filament_is_mixed", new ConfigOptionBools(std::vector{0, 0, 0, 0, 0})); + CHECK(estimate(unmixed, 5, 0.2, 5., type).depth > physical); + // Mixed slots alone still purge their components, and a single one already changes tools. + CHECK_THAT(estimate_wipe_tower_footprint(config, type, {3, 4}, 0.2, 5.).depth, WithinAbs(physical, 1e-9)); + CHECK_THAT(estimate_wipe_tower_footprint(config, type, {3}, 0.2, 5.).depth, WithinAbs(estimate(config, 2, 0.2, 5., type).depth, 1e-9)); +} + TEST_CASE("A second nozzle adds the ramming of one nozzle change per layer", "[WipeTowerEstimate]") { // Two filaments on two nozzles: the tool order crosses once per layer, and Type1 rams 10 mm // of filament as three 1.0 mm nozzle-change lines (see the WipeTower case).