From 99ee8893cd4dea6c313be5c6f78f940717163072 Mon Sep 17 00:00:00 2001 From: harrierpigeon Date: Sat, 8 Aug 2026 17:14:26 -0500 Subject: [PATCH] Fix belt purge tower activation and placement safety --- result.json | 10 -------- src/libslic3r/BeltPurge.cpp | 13 +++++++---- src/libslic3r/GCode/ToolOrdering.hpp | 2 +- src/libslic3r/Print.cpp | 4 ++-- src/slic3r/GUI/BeltPurgeTower.cpp | 16 +++++++++---- src/slic3r/GUI/BeltPurgeTower.hpp | 9 +++---- tests/fff_print/test_print.cpp | 35 ++++++++++++++++++++++++++++ 7 files changed, 62 insertions(+), 27 deletions(-) delete mode 100644 result.json diff --git a/result.json b/result.json deleted file mode 100644 index 517f6bffa5..0000000000 --- a/result.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "error_string": "Success.", - "export_time": 0, - "layer_height": 0.0, - "plate_index": 0, - "prepare_time": 0, - "return_code": 0, - "sparse_infill_density": 0.0, - "wall_loops": 0 -} diff --git a/src/libslic3r/BeltPurge.cpp b/src/libslic3r/BeltPurge.cpp index c51c5511c6..3e261a1882 100644 --- a/src/libslic3r/BeltPurge.cpp +++ b/src/libslic3r/BeltPurge.cpp @@ -42,10 +42,15 @@ bool Print::has_belt_purge_tower() const { // Its own purge-tower "type", gated by the belt-only enable_belt_purge_tower // option (not the classic enable_prime_tower). - return m_config.belt_printer.value - && m_config.enable_belt_purge_tower.value - && !m_config.spiral_mode.value - && m_config.filament_diameter.values.size() > 1; + if (!(m_config.belt_printer.value + && m_config.enable_belt_purge_tower.value + && !m_config.spiral_mode.value + && m_config.filament_diameter.values.size() > 1)) + return false; + + return std::any_of(m_objects.begin(), m_objects.end(), [](const PrintObject *object) { + return object->config().belt_purge_tower_object.value; + }); } // Belt mode: align ALL objects on the plate (the printed objects AND the purge diff --git a/src/libslic3r/GCode/ToolOrdering.hpp b/src/libslic3r/GCode/ToolOrdering.hpp index f2071e75b7..52778690bb 100644 --- a/src/libslic3r/GCode/ToolOrdering.hpp +++ b/src/libslic3r/GCode/ToolOrdering.hpp @@ -50,7 +50,7 @@ public: // tower to tell which prism fills carry purge vs. which are unclaimed waste. bool is_entity_overridden(const ExtrusionEntity* entity, const PrintObject *object, size_t copy_id) const { auto it = entity_map.find(std::make_tuple(entity, object)); - return it == entity_map.end() ? false : it->second[copy_id] != -1; + return it != entity_map.end() && copy_id < it->second.size() && it->second[copy_id] != -1; } bool is_overriddable(const ExtrusionEntityCollection& ee, const PrintConfig& print_config, const PrintObject& object, const PrintRegion& region) const; diff --git a/src/libslic3r/Print.cpp b/src/libslic3r/Print.cpp index 922168ccb8..7cec3de5eb 100644 --- a/src/libslic3r/Print.cpp +++ b/src/libslic3r/Print.cpp @@ -1477,13 +1477,13 @@ StringObjectException Print::validate(std::vector *warnin if (m_config.belt_printer.value && m_config.enable_belt_purge_tower.value && m_config.print_sequence == PrintSequence::ByObject - && extruders.size() > 1 && warning != nullptr) { + && extruders.size() > 1) { StringObjectException warningtemp; warningtemp.string = L("The belt purge tower is not generated in \"By object\" print sequence; " "filament changes will not be purged."); warningtemp.opt_key = "enable_belt_purge_tower"; warningtemp.is_warning = true; - *warning = warningtemp; + add_warning(warningtemp); } if (m_config.enable_prime_tower) { diff --git a/src/slic3r/GUI/BeltPurgeTower.cpp b/src/slic3r/GUI/BeltPurgeTower.cpp index f04f311d6b..cf1489d6a0 100644 --- a/src/slic3r/GUI/BeltPurgeTower.cpp +++ b/src/slic3r/GUI/BeltPurgeTower.cpp @@ -142,7 +142,14 @@ bool ensure_belt_purge_tower(Model &model, PartPlateList &partplate_list, Object } // --- Sizing ----------------------------------------------------------- - const double width = print_config.has("belt_purge_tower_width") ? std::max(1., print_config.opt_float("belt_purge_tower_width")) : 35.; + const int n_islands = std::max(1, (int) filaments.size() - 1); + const double gap = 1.0; + // Every disconnected island needs at least 1 mm of printable width. Honor + // the configured total width whenever possible, but never let the island + // layout silently grow past the footprint used for placement. + const double min_width = n_islands + (n_islands - 1) * gap; + const double width = std::max(min_width, + print_config.has("belt_purge_tower_width") ? print_config.opt_float("belt_purge_tower_width") : 35.); const double layer_h = print_config.has("layer_height") ? print_config.opt_float("layer_height") : 0.2; // Belt geometry. The rotation axis is the gantry tilt axis; the belt @@ -219,6 +226,9 @@ bool ensure_belt_purge_tower(Model &model, PartPlateList &partplate_list, Object new_sig.key[4] = q(belt_max); new_sig.key[5] = q(lat_min); new_sig.key[6] = q(z_max); + new_sig.key[7] = static_cast(rot); + new_sig.key[8] = std::lround(theta * 10000.0); + new_sig.key[9] = q(lat_max); if (prism_idxs.size() == 1 && new_sig == sig) return false; // already up to date — do not touch the model @@ -279,15 +289,13 @@ bool ensure_belt_purge_tower(Model &model, PartPlateList &partplate_list, Object // lets each swap claim its own island. Total lateral footprint stays `width` // (each island width/N wide, separated by a small gap), so per-layer capacity // per island ~= max_flush, matching the height sizing. - const int n_islands = std::max(1, (int) filaments.size() - 1); // Minimal gap between sub-bars: they must stay just-separated so the slicer // keeps them as distinct islands (hence distinct infill collections, one per // simultaneous swap). Zero gap would union them into one collection and // reintroduce the multi-swap-per-layer absorption bug; a hair over ~2 line // widths also keeps gap-fill from bridging them. 1 mm is about as close as // they can butt up while staying individually purgeable. - const double gap = 1.0; - const double w_sub = std::max(1.0, (width - (n_islands - 1) * gap) / n_islands); + const double w_sub = (width - (n_islands - 1) * gap) / n_islands; // --- (Re)create --------------------------------------------------------- if (!prism_idxs.empty()) diff --git a/src/slic3r/GUI/BeltPurgeTower.hpp b/src/slic3r/GUI/BeltPurgeTower.hpp index b1735303c5..5e5b614834 100644 --- a/src/slic3r/GUI/BeltPurgeTower.hpp +++ b/src/slic3r/GUI/BeltPurgeTower.hpp @@ -1,5 +1,4 @@ -#ifndef slic3r_GUI_BeltPurgeTower_hpp_ -#define slic3r_GUI_BeltPurgeTower_hpp_ +#pragma once // ORCA-Belt: auto-managed purge prism for belt printers. // @@ -23,12 +22,12 @@ struct BeltPurgeSignature { bool valid = false; int filament_count = 0; - long key[7] = {0}; // rounded geometry inputs (0.1 mm units) + long key[10] = {0}; // rounded geometry inputs (0.1 mm units) bool operator==(const BeltPurgeSignature &o) const { if (valid != o.valid || filament_count != o.filament_count) return false; - for (int i = 0; i < 7; ++i) + for (int i = 0; i < 10; ++i) if (key[i] != o.key[i]) return false; return true; @@ -44,5 +43,3 @@ bool ensure_belt_purge_tower(Model &model, PartPlateList &partplate_list, Object } // namespace GUI } // namespace Slic3r - -#endif // slic3r_GUI_BeltPurgeTower_hpp_ diff --git a/tests/fff_print/test_print.cpp b/tests/fff_print/test_print.cpp index 9cb085f78b..ba3432b4ad 100644 --- a/tests/fff_print/test_print.cpp +++ b/tests/fff_print/test_print.cpp @@ -300,6 +300,41 @@ TEST_CASE("Print::validate tolerates a null warnings pointer", "[Print][validate CHECK(err.string.empty()); } +TEST_CASE("Purge tower selection keeps ordinary printers on the classic path", "[Print][PurgeTower][Regression]") +{ + DynamicPrintConfig config = multifilament_config(2, { + { "belt_printer", 0 }, + { "enable_prime_tower", 1 }, + { "enable_belt_purge_tower", 1 } + }); + config.set_key_value("timelapse_type", new ConfigOptionEnum(TimelapseType::tlSmooth)); + + Model model; + Print print; + build_cubes(model, print, config, /*n=*/1, /*overlap=*/false); + + CHECK(print.has_wipe_tower()); + CHECK_FALSE(print.has_belt_purge_tower()); +} + +TEST_CASE("Belt purge planning requires its managed purge object", "[Print][PurgeTower][Regression]") +{ + DynamicPrintConfig config = multifilament_config(2, { + { "belt_printer", 1 }, + { "enable_belt_purge_tower", 1 } + }); + + Model model; + Print print; + build_cubes(model, print, config, /*n=*/1, /*overlap=*/false); + CHECK_FALSE(print.has_belt_purge_tower()); + + model.objects.front()->config.set_key_value("belt_purge_tower_object", new ConfigOptionBool(true)); + print.apply(model, config); + CHECK(print.has_belt_purge_tower()); + CHECK_FALSE(print.has_wipe_tower()); +} + TEST_CASE("A default slice emits perimeter, infill, and skirt", "[Print]") { const std::string gcode = slice({ cube(20) }, {