From 360a68e07876b5bff9cd27d6df6c3cdcd93ce930 Mon Sep 17 00:00:00 2001 From: harrierpigeon Date: Fri, 2 Oct 2026 01:11:59 -0500 Subject: [PATCH] Belt: warn when the purge tower is enabled but the project has no tower object The purge tower is a model object the GUI creates and sizes, and libslic3r only purges into one that exists. A multi-filament belt project sliced from the CLI without it changed filament with nowhere to purge, silently. Raised in Hanif Koh's review of #14394. --- src/libslic3r/Print.cpp | 15 +++++++++++++++ tests/fff_print/test_print.cpp | 30 ++++++++++++++++++++++++++++++ 2 files changed, 45 insertions(+) diff --git a/src/libslic3r/Print.cpp b/src/libslic3r/Print.cpp index 5bfdfe8eac..444bf7cde6 100644 --- a/src/libslic3r/Print.cpp +++ b/src/libslic3r/Print.cpp @@ -1553,6 +1553,21 @@ StringObjectException Print::validate(std::vector *warnin add_warning(warningtemp); } + // The purge tower is a model object the GUI creates and sizes; libslic3r only purges + // into one that exists. A project sliced without it (the CLI on a project saved before + // the tower was generated) changes filament with nowhere to purge. + if (m_config.belt_printer.value && m_config.enable_belt_purge_tower.value + && m_config.print_sequence != PrintSequence::ByObject + && ! m_config.spiral_mode.value && extruders.size() > 1 && ! this->has_belt_purge_tower()) { + StringObjectException warningtemp; + warningtemp.string = L("The belt purge tower is enabled but the project has no purge tower object; " + "filament changes will not be purged. Open the project in the application " + "to generate the tower."); + warningtemp.opt_key = "enable_belt_purge_tower"; + warningtemp.is_warning = true; + add_warning(warningtemp); + } + if (m_config.belt_printer.value && m_config.enable_belt_purge_tower.value) { const size_t prism_count = std::count_if(m_objects.begin(), m_objects.end(), [](const PrintObject *object) { return object->config().belt_purge_tower_object.value; diff --git a/tests/fff_print/test_print.cpp b/tests/fff_print/test_print.cpp index 6d8d3d2ec9..f9c9528461 100644 --- a/tests/fff_print/test_print.cpp +++ b/tests/fff_print/test_print.cpp @@ -336,6 +336,36 @@ TEST_CASE("Belt purge planning requires its managed purge object", "[Print][Purg CHECK_FALSE(print.has_wipe_tower()); } +// The GUI creates the purge tower object; a project sliced without one (the CLI) must say +// that its filament changes go unpurged. +TEST_CASE("Belt purge tower enabled without a tower object warns", "[Print][PurgeTower][belt]") +{ + DynamicPrintConfig config = multifilament_config(2, { + { "belt_printer", 1 }, + { "enable_belt_purge_tower", 1 }, + { "layer_change_gcode", "G92 E0\n" } + }); + auto purge_warnings = [](Print &print) { + std::vector warnings; + print.validate(&warnings); + return std::count_if(warnings.begin(), warnings.end(), [](const StringObjectException &w) { + return w.opt_key == "enable_belt_purge_tower"; + }); + }; + + Model model; + Print print; + build_cubes(model, print, config, /*n=*/2, /*overlap=*/false); + model.objects[1]->config.set_key_value("extruder", new ConfigOptionInt(2)); + print.apply(model, config); + REQUIRE(print.extruders().size() > 1); + CHECK(purge_warnings(print) == 1); + + model.objects.front()->config.set_key_value("belt_purge_tower_object", new ConfigOptionBool(true)); + print.apply(model, config); + CHECK(purge_warnings(print) == 0); +} + TEST_CASE("Belt purge rejects multiple managed purge objects", "[Print][PurgeTower][Regression]") { DynamicPrintConfig config = multifilament_config(2, {