Fix belt purge tower activation and placement safety

This commit is contained in:
harrierpigeon
2026-08-08 17:14:26 -05:00
parent ee3e014f02
commit 99ee8893cd
7 changed files with 62 additions and 27 deletions

View File

@@ -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
}

View File

@@ -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

View File

@@ -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;

View File

@@ -1477,13 +1477,13 @@ StringObjectException Print::validate(std::vector<StringObjectException> *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) {

View File

@@ -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<long>(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())

View File

@@ -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_

View File

@@ -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>(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) }, {