Merge pull request #61 from HarrierPigeon/belt/purgeTower

Add Purge Tower and Finalize Multicolor Support
This commit is contained in:
Joseph Robertson
2026-08-08 18:59:00 -05:00
committed by GitHub
20 changed files with 1001 additions and 22 deletions
+348
View File
@@ -0,0 +1,348 @@
// ORCA-Belt: backend of the belt purge tower (the belt replacement for the
// classic wipe/prime tower).
//
// Kept in its own translation unit so the belt-purge logic stays out of the way
// of unrelated upstream changes to Print.cpp / PrintObjectSlice.cpp and carries
// no regression risk for normal printers: none of these methods do anything
// unless the print is a belt printer with the belt purge tower enabled.
//
// Print::has_belt_purge_tower() - is the belt purge tower active?
// Print::_align_belt_purge_layers() - snap the prism's layer grid onto the
// printed objects' grid
// Print::_plan_belt_purge() - route filament-change purging into the
// prism (flush-into-objects), no wipe tower
// PrintObject::belt_shift_layer_grid() - shift a sliced layer grid
// PrintObject::belt_truncate_layers_above() - cancel the prism past the last swap
//
// (Declarations live in Print.hpp alongside the rest of the Print interface.)
#include "Print.hpp"
#include "PrintConfig.hpp"
#include "Exception.hpp"
#include "GCode/ToolOrdering.hpp"
#include "Layer.hpp"
#include "ExtrusionEntity.hpp"
#include "ExtrusionEntityCollection.hpp"
#include "I18N.hpp"
#include "format.hpp"
#include "LocalesUtils.hpp"
#include "libslic3r.h"
#include <algorithm>
#include <cmath>
#include <limits>
#include <boost/log/trivial.hpp>
namespace Slic3r {
// Belt purge prism: purging after filament changes is routed into a sliced
// prism object via the flush-into-objects machinery instead of a wipe tower.
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).
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
// prism) onto one common layer grid, so the prism can absorb every toolchange.
//
// After belt slicing each object's layer print_z carries a per-object global z
// offset (mesh-vertex-scan belt_z_shift + instance-Y-dependent terms), so
// objects at different belt-Y positions get layer grids with DIFFERENT residues
// (mod layer height). Purge marking looks absorbers up with
// get_layer_at_printz(lt.print_z, EPSILON), so a toolchange on object B only
// absorbs into the prism if the prism has a layer at B's print_z. Snapping only
// the prism to one object therefore worked for a single (assembled) multi-color
// object but failed with multiple separate objects — the prism could follow only
// one grid, and toolchanges on the others went unabsorbed ("multiple layer
// grids" warning).
//
// Fix: pick one reference grid (the tallest object) and shift every object onto
// it. Each shift is at most half a layer height — a sub-100µm move along the
// belt, the very same mechanism the per-object global_z_offset already uses, and
// it keeps each object internally consistent (belt_shift_layer_grid moves the
// object's layers, its support layers, and its belt floor together). Equal layer
// height across objects is enforced by Print::validate(), so once residues match
// every object steps on the same lattice {ref_offset + k*h} and every toolchange
// layer coincides with a prism layer.
void Print::_align_belt_purge_layers()
{
PrintObject *prism = nullptr;
for (PrintObject *po : m_objects)
if (po->config().belt_purge_tower_object.value) {
prism = po;
break;
}
if (prism == nullptr || prism->layers().empty())
return;
const double h = prism->config().layer_height.value;
if (h <= EPSILON)
return;
// Grid residue of an object's layer grid: identical for all of an object's
// layers above the first since they step by h.
auto grid_offset = [h](const PrintObject *po) -> double {
if (po->layers().empty())
return 0.;
const double z = po->layers().front()->print_z;
return z - std::floor(z / h) * h; // in [0, h)
};
// Reference grid: the tallest non-prism object (proxy for the object with
// the most toolchange layers — minimizes how far the rest must move).
const PrintObject *ref = nullptr;
double ref_top = -std::numeric_limits<double>::max();
for (const PrintObject *po : m_objects) {
if (po->config().belt_purge_tower_object.value || po->layers().empty())
continue;
const double top = po->layers().back()->print_z;
if (top > ref_top) {
ref_top = top;
ref = po;
}
}
if (ref == nullptr)
return;
const double ref_offset = grid_offset(ref);
// Snap every object (printed objects AND the prism) onto the reference grid.
for (PrintObject *po : m_objects) {
if (po->layers().empty())
continue;
double delta = ref_offset - grid_offset(po);
if (delta > 0.5 * h)
delta -= h;
else if (delta <= -0.5 * h)
delta += h;
po->belt_shift_layer_grid(delta); // no-op for the reference object (delta ~ 0)
}
BOOST_LOG_TRIVIAL(debug) << "[BELT-DEBUG] purge grid align: snapped " << m_objects.size()
<< " objects onto ref grid offset=" << ref_offset
<< " (ref=" << ref->model_object()->name << ")";
}
// Belt mode replacement for _make_wipe_tower(): plan filament-change purging
// into the belt purge prism (and any other flush_into_* object) using the
// flush-into-objects machinery, without generating classic wipe tower G-code.
// The toolchange itself is emitted by GCode::set_extruder() via the
// change_filament_gcode macro; the overrides marked here make the new
// filament's first extrusions land in the purge prism.
void Print::_plan_belt_purge()
{
m_wipe_tower_data.clear();
// Must run before ToolOrdering is built: LayerTools merge per-object layer
// print_z values, and the prism only absorbs purge where its (snapped)
// layers coincide with the toolchange layers.
this->_align_belt_purge_layers();
const unsigned int number_of_extruders = (unsigned int) m_config.filament_colour.values.size();
// No initial priming extrusions: there is no tower to prime on.
m_wipe_tower_data.tool_ordering = ToolOrdering(*this, (unsigned int) -1, false);
m_wipe_tower_data.tool_ordering.sort_and_build_data(*this, (unsigned int) -1, false);
if (m_wipe_tower_data.tool_ordering.empty() || m_wipe_tower_data.tool_ordering.last_extruder() == unsigned(-1))
throw Slic3r::SlicingError("The print is empty. The model is not printable with current print settings.");
if (!m_wipe_tower_data.tool_ordering.has_wipe_tower())
// No toolchanges anywhere, nothing to purge.
return;
this->throw_if_canceled();
// Flush volumes per filament pair, mirroring the generic wipe tower path:
// full flush matrix for single extruder multi material with purging enabled,
// plain prime volume otherwise.
std::vector<float> flush_matrix(cast<float>(
get_flush_volumes_matrix(m_config.flush_volumes_matrix.values, 0, m_config.nozzle_diameter.values.size())));
std::vector<std::vector<float>> wipe_volumes;
for (unsigned int i = 0; i < number_of_extruders; ++i)
wipe_volumes.push_back(std::vector<float>(flush_matrix.begin() + i * number_of_extruders,
flush_matrix.begin() + (i + 1) * number_of_extruders));
const bool use_flush_matrix = m_config.purge_in_prime_tower && m_config.single_extruder_multi_material;
const float flush_multiplier = (float) m_config.flush_multiplier.get_at(0);
// Cancel the purge prism early: pre-scan the tool ordering for the highest
// print_z that actually has a toolchange, then drop the prism's layers above
// it so the tower stops at the last color swap (saves filament/time). This
// MUST happen before the marking loop below: ensure_perimeters_infills_order
// force-overrides the prism's extrusions on every layer (it is a dedicated
// flush object), so truncating afterwards would leave dangling overrides
// pointing into deleted layers.
{
double last_tc_z = -1.;
unsigned int cur_ext = m_wipe_tower_data.tool_ordering.first_extruder();
for (const auto &lt : m_wipe_tower_data.tool_ordering.layer_tools())
for (const unsigned int e : lt.extruders)
if (e != cur_ext) { last_tc_z = lt.print_z; cur_ext = e; }
if (last_tc_z >= 0.)
for (PrintObject *po : m_objects)
if (po->config().belt_purge_tower_object.value) {
po->belt_truncate_layers_above(last_tc_z);
break;
}
}
// Diagnostic: the prism only absorbs purge at toolchange layers whose
// print_z coincides with one of its own layers. Compare the prism's layer
// print_z range to the toolchange print_z range and count how many
// toolchange layers actually land on a prism layer. This distinguishes a
// range/grid-alignment failure (no coverage) from a capacity shortfall
// (covered but not enough cross-section).
PrintObject *prism_po = nullptr;
for (PrintObject *po : m_objects)
if (po->config().belt_purge_tower_object.value && !po->layers().empty()) { prism_po = po; break; }
const PrintObject *diag_prism = prism_po;
if (diag_prism != nullptr)
BOOST_LOG_TRIVIAL(warning) << "[BELT-DEBUG] purge prism layer range print_z=["
<< diag_prism->layers().front()->print_z << ", " << diag_prism->layers().back()->print_z
<< "] nlayers=" << diag_prism->layers().size();
int tc_layers = 0, tc_layers_covered = 0;
float total_leftover = 0.f;
float worst_layer_leftover = 0.f;
double worst_layer_z = 0.;
unsigned int current_extruder_id = m_wipe_tower_data.tool_ordering.first_extruder();
for (auto &layer_tools : m_wipe_tower_data.tool_ordering.layer_tools()) {
float layer_leftover = 0.f;
bool layer_has_tc = false;
for (const unsigned int extruder_id : layer_tools.extruders) {
if (extruder_id == current_extruder_id)
continue;
if (!layer_has_tc) {
layer_has_tc = true;
++tc_layers;
if (diag_prism != nullptr && diag_prism->get_layer_at_printz(layer_tools.print_z, EPSILON) != nullptr)
++tc_layers_covered;
}
float volume_to_wipe = use_flush_matrix ?
wipe_volumes[current_extruder_id][extruder_id] * flush_multiplier :
(float) m_config.prime_volume;
float leftover = layer_tools.wiping_extrusions().mark_wiping_extrusions(*this, current_extruder_id, extruder_id,
volume_to_wipe);
BOOST_LOG_TRIVIAL(trace) << "[BELT-DEBUG] purge toolchange print_z=" << layer_tools.print_z
<< " filament " << current_extruder_id << "->" << extruder_id
<< " requested=" << volume_to_wipe
<< " absorbed=" << volume_to_wipe - leftover
<< " leftover=" << leftover;
layer_leftover += leftover;
current_extruder_id = extruder_id;
}
// Plastic saving: drop the prism's infill that no toolchange on this
// layer claimed. At this point (after the real-purge marking, before the
// force-override pass below) the prism's OVERRIDDEN fills are exactly the
// purge; the rest is infill that would otherwise print as the prism's own
// filament for nothing — which is most of the prism on layers with little
// or no purge. We keep perimeters so the bar's wall shell stays
// continuous along the belt. (Must run before ensure_perimeters_infills_
// order, which force-overrides every remaining fill and would make them
// all look "claimed".)
if (prism_po != nullptr) {
if (Layer *pl = prism_po->get_layer_at_printz(layer_tools.print_z, EPSILON)) {
const auto &we = layer_tools.wiping_extrusions();
for (LayerRegion *lr : pl->regions()) {
auto &ents = lr->fills.entities;
size_t keep = 0;
for (size_t r = 0; r < ents.size(); ++r) {
if (we.is_entity_overridden(ents[r], prism_po, 0))
ents[keep++] = ents[r];
else
delete ents[r];
}
ents.resize(keep);
}
}
}
layer_tools.wiping_extrusions().ensure_perimeters_infills_order(*this);
if (layer_leftover > 0.f) {
total_leftover += layer_leftover;
if (layer_leftover > worst_layer_leftover) {
worst_layer_leftover = layer_leftover;
worst_layer_z = layer_tools.print_z;
}
}
this->throw_if_canceled();
}
BOOST_LOG_TRIVIAL(warning) << "[BELT-DEBUG] purge coverage: " << tc_layers_covered << "/" << tc_layers
<< " toolchange layers land on a prism layer"
<< (tc_layers > 0 && tc_layers_covered == 0 ? " (RANGE/GRID MISALIGNMENT — prism absorbs nothing)" :
tc_layers_covered < tc_layers ? " (partial coverage)" : " (full coverage)");
if (total_leftover > 1.f) {
this->active_step_add_warning(
PrintStateBase::WarningLevel::CRITICAL,
Slic3r::format(_u8L("The belt purge tower cannot absorb the full purge volume: %1% mm³ in total could not "
"be purged (worst layer: %2% mm³ at height %3%). The print may show color bleeding. "
"Increase the belt purge tower width, or reduce flushing volumes."),
int(std::ceil(total_leftover)), int(std::ceil(worst_layer_leftover)),
Slic3r::float_to_string_decimal_point(worst_layer_z, 2)));
BOOST_LOG_TRIVIAL(warning) << "[BELT-DEBUG] purge planning leftover total=" << total_leftover
<< " worst_layer=" << worst_layer_leftover << " at print_z=" << worst_layer_z;
}
}
// Belt mode: shift the sliced layer grid by delta. Mirrors the global_z_offset
// application in slice() — layer print_z and belt_floor_z_shift move together
// so belt floor clipping stays consistent with the shifted grid. Used by
// Print::_align_belt_purge_layers() to snap the purge prism onto the printed
// objects' layer grid; |delta| <= half a layer height, i.e. a sub-layer shift
// of the prism along the belt.
void PrintObject::belt_shift_layer_grid(double delta)
{
if (std::abs(delta) < EPSILON)
return;
for (Layer *layer : m_layers)
layer->print_z += delta;
for (SupportLayer *layer : m_support_layers)
layer->print_z += delta;
m_slicing_params.belt_floor_z_shift += delta;
BOOST_LOG_TRIVIAL(trace) << "[BELT-DEBUG] belt_shift_layer_grid"
<< " obj=" << this->model_object()->name
<< " delta=" << delta
<< " first_layer.print_z=" << (m_layers.empty() ? 0. : m_layers.front()->print_z);
}
// Belt mode: drop layers strictly above z (used to cancel the purge prism early
// once there are no more toolchanges above z, so the tower stops at the last
// color swap instead of wasting filament up the rest of the belt). Each layer's
// cross-section is already sliced, so removing upper layers does not affect the
// last toolchange's coverage. Deletes the Layer objects and clears the new top
// layer's upper-layer link. Returns the number of layers removed.
size_t PrintObject::belt_truncate_layers_above(coordf_t z)
{
size_t keep = m_layers.size();
while (keep > 0 && m_layers[keep - 1]->print_z > z + EPSILON)
--keep;
if (keep >= m_layers.size())
return 0;
const size_t removed = m_layers.size() - keep;
for (size_t i = keep; i < m_layers.size(); ++i)
delete m_layers[i];
m_layers.resize(keep);
if (!m_layers.empty())
m_layers.back()->upper_layer = nullptr;
BOOST_LOG_TRIVIAL(debug) << "[BELT-DEBUG] truncate purge prism above print_z=" << z
<< " kept=" << keep << " removed=" << removed
<< " new_top=" << (m_layers.empty() ? 0. : m_layers.back()->print_z);
return removed;
}
} // namespace Slic3r
+1
View File
@@ -86,6 +86,7 @@ set(lisbslic3r_sources
BeltGCode.hpp
BeltGCodeWriter.cpp
BeltGCodeWriter.hpp
BeltPurge.cpp
BeltSliceStrategy.cpp
BeltSliceStrategy.hpp
BeltTransform.cpp
+4
View File
@@ -381,6 +381,10 @@ bool ToolOrdering::insert_wipe_tower_extruder()
{
if (!m_print_config_ptr || !m_print_config_ptr->enable_prime_tower)
return false;
// Belt mode has no classic wipe tower; the dedicated wipe tower filament
// must not inject extra toolchanges into the purge prism planning.
if (m_print_config_ptr->belt_printer)
return false;
if (m_print_config_ptr->wipe_tower_filament == 0)
return false;
+8 -6
View File
@@ -45,6 +45,14 @@ public:
void ensure_perimeters_infills_order(const Print& print);
// Returns true if entity is not printed with its usual extruder for a given copy
// (i.e. it was claimed as a wiping/purge extrusion). Used by the belt purge
// 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() && 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;
bool is_overriddable_and_mark(const ExtrusionEntityCollection& ee, const PrintConfig& print_config, const PrintObject& object, const PrintRegion& region) {
bool out = this->is_overriddable(ee, print_config, object, region);
@@ -80,12 +88,6 @@ private:
void set_support_extruder_override(const PrintObject* object, size_t copy_id, int extruder, size_t num_of_copies);
void set_support_interface_extruder_override(const PrintObject* object, size_t copy_id, int extruder, size_t num_of_copies);
// Returns true in case that entity is not printed with its usual extruder for a given copy:
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;
}
std::map<std::tuple<const ExtrusionEntity*, const PrintObject *>, ExtruderPerCopy> entity_map; // to keep track of who prints what
// BBS
std::map<const PrintObject*, int> support_map;
+3
View File
@@ -1160,6 +1160,8 @@ static std::vector<std::string> s_Preset_print_options{
"prime_volume",
"prime_tower_infill_gap",
"prime_tower_flat_ironing",
"belt_purge_tower_width",
"belt_purge_tower_object",
"enable_tower_interface_features",
"enable_tower_interface_cooldown_during_tower",
"wipe_tower_no_sparse_layers",
@@ -1410,6 +1412,7 @@ static std::vector<std::string> s_Preset_printer_options {
"belt_preslice_global",
"first_layer_plane", "first_layer_plane_offset", "first_layer_plane_thickness",
"belt_support_floor_offset", "belt_support_floor_mode", "belt_support_z_offset_mode",
"enable_belt_purge_tower",
"gcode_flavor",
"fan_kickstart", "part_cooling_fan_min_pwm", "fan_speedup_time", "fan_speedup_overhangs",
"single_extruder_multi_material", "manual_filament_change", "file_start_gcode", "machine_start_gcode", "machine_end_gcode", "before_layer_change_gcode", "printing_by_object_gcode", "layer_change_gcode", "time_lapse_gcode", "wrapping_detection_gcode", "change_filament_gcode", "change_extrusion_role_gcode",
+38 -7
View File
@@ -22,6 +22,7 @@
#include "MaterialType.hpp"
#include "Model.hpp"
#include "format.hpp"
#include "LocalesUtils.hpp"
#include <float.h>
#include <algorithm>
@@ -358,6 +359,7 @@ bool Print::invalidate_state_by_config_options(const ConfigOptionResolver & /* n
|| opt_key == "hot_plate_temp"
|| opt_key == "textured_plate_temp"
|| opt_key == "enable_prime_tower"
|| opt_key == "enable_belt_purge_tower"
|| opt_key == "enable_wrapping_detection"
|| opt_key == "prime_tower_enable_framework"
|| opt_key == "prime_tower_width"
@@ -389,6 +391,7 @@ bool Print::invalidate_state_by_config_options(const ConfigOptionResolver & /* n
|| opt_key == "prime_volume"
|| opt_key == "flush_into_infill"
|| opt_key == "flush_into_support"
|| opt_key == "belt_purge_tower_width"
|| opt_key == "initial_layer_infill_speed"
|| opt_key == "travel_speed"
|| opt_key == "travel_speed_z"
@@ -1472,6 +1475,17 @@ StringObjectException Print::validate(std::vector<StringObjectException> *warnin
add_warning(layer_warning);
}
if (m_config.belt_printer.value && m_config.enable_belt_purge_tower.value
&& m_config.print_sequence == PrintSequence::ByObject
&& 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;
add_warning(warningtemp);
}
if (m_config.enable_prime_tower) {
for (const PrintObject* object : m_objects) {
if (object->config().precise_z_height.value) {
@@ -1596,7 +1610,7 @@ StringObjectException Print::validate(std::vector<StringObjectException> *warnin
return {_u8L("Variable layer height is not supported with Organic supports.") };
}
if (this->has_wipe_tower() && ! m_objects.empty()) {
if ((this->has_wipe_tower() || this->has_belt_purge_tower()) && ! m_objects.empty()) {
// Make sure all extruders use same diameter filament and have the same nozzle diameter
// EPSILON comparison is used for nozzles and 10 % tolerance is used for filaments
double first_nozzle_diam = m_config.nozzle_diameter.get_at(extruders.front());
@@ -1612,12 +1626,17 @@ StringObjectException Print::validate(std::vector<StringObjectException> *warnin
}
}
if (! m_config.use_relative_e_distances)
return { L("The Wipe Tower is currently only supported with the relative extruder addressing (use_relative_e_distances=1).") };
// The following two constraints come from the classic wipe tower G-code
// generator; purging into the belt purge prism uses normal object
// extrusions and does not need them.
if (this->has_wipe_tower()) {
if (! m_config.use_relative_e_distances)
return { L("The Wipe Tower is currently only supported with the relative extruder addressing (use_relative_e_distances=1).") };
if (m_config.ooze_prevention && m_config.single_extruder_multi_material)
return {L("Ooze prevention is only supported with the wipe tower when 'single_extruder_multi_material' is off.")};
}
if (m_config.ooze_prevention && m_config.single_extruder_multi_material)
return {L("Ooze prevention is only supported with the wipe tower when 'single_extruder_multi_material' is off.")};
#if 0
if (m_config.gcode_flavor != gcfRepRapSprinter && m_config.gcode_flavor != gcfRepRapFirmware &&
m_config.gcode_flavor != gcfRepetier && m_config.gcode_flavor != gcfMarlinLegacy && m_config.gcode_flavor != gcfMarlinFirmware)
@@ -2708,7 +2727,10 @@ void Print::process(long long *time_cost_with_cache, bool use_cache)
m_wipe_tower_data.clear();
m_tool_ordering.clear();
if (this->has_wipe_tower()) {
if (this->has_belt_purge_tower() && this->config().print_sequence != PrintSequence::ByObject) {
this->_plan_belt_purge();
}
else if (this->has_wipe_tower()) {
this->_make_wipe_tower();
}
else if (this->config().print_sequence != PrintSequence::ByObject) {
@@ -4085,6 +4107,13 @@ int Print::get_config_index(int filament_id, int layer_id, const std::vector<std
// Wipe tower support.
bool Print::has_wipe_tower() const
{
// Belt printers never get the classic wipe tower: its G-code is generated
// directly in machine XY coordinates and bypasses the belt rotation
// transform. Purging is routed into the belt purge prism instead
// (see has_belt_purge_tower() / _plan_belt_purge()).
if (m_config.belt_printer.value)
return false;
if (m_config.enable_prime_tower.value == true) {
if (m_config.enable_wrapping_detection.value && m_config.wrapping_exclude_area.values.size() > 2)
return true;
@@ -4097,6 +4126,7 @@ bool Print::has_wipe_tower() const
return false;
}
const WipeTowerData &Print::wipe_tower_data(size_t filaments_cnt) const
{
// If the wipe tower wasn't created yet, make sure the depth and brim_width members are set to default.
@@ -4183,6 +4213,7 @@ bool Print::enable_timelapse_print() const
return m_config.timelapse_type.value == TimelapseType::tlSmooth;
}
void Print::_make_wipe_tower()
{
m_wipe_tower_data.clear();
+15
View File
@@ -558,6 +558,13 @@ private:
std::vector<std::set<int>> detect_extruder_geometric_unprintables() const;
void slice_volumes();
// Belt mode: shift the sliced layer grid (layer print_z and the belt floor
// clipping shift) by delta, used by Print::_align_belt_purge_layers() to
// snap the purge prism's layers onto the printed objects' layer grid.
void belt_shift_layer_grid(double delta);
// Belt mode: remove layers above z (cancel the purge prism past the last
// toolchange). Returns how many layers were dropped.
size_t belt_truncate_layers_above(coordf_t z);
//BBS
ExPolygons _shrink_contour_holes(double contour_delta, double hole_delta, const ExPolygons& polys) const;
// BBS
@@ -1085,6 +1092,8 @@ public:
// Wipe tower support.
bool has_wipe_tower() const;
// Belt purge prism (belt-mode replacement for the wipe tower).
bool has_belt_purge_tower() const;
const WipeTowerData& wipe_tower_data(size_t filaments_cnt = 0) const;
const ToolOrdering& tool_ordering() const { return m_tool_ordering; }
@@ -1330,6 +1339,12 @@ private:
void _make_skirt();
void _make_wipe_tower();
// Belt mode: route filament-change purging into the belt purge prism
// (flush-into-objects) without generating a classic wipe tower.
void _plan_belt_purge();
// Belt mode: shift the purge prism's layer grid to match the printed
// objects' grid so toolchange layers find prism layers to purge into.
void _align_belt_purge_layers();
void finalize_first_layer_convex_hull();
void update_filament_self_index_cache();
// Deduplicates, per filament, the (extruder type x volume type) variants the grouping
+35
View File
@@ -7321,6 +7321,31 @@ void PrintConfigDef::init_fff_params()
def->set_default_value(new ConfigOptionEnum<BeltSupportZOffsetMode>(BeltSupportZOffsetMode::Unconditional));
}
def = this->add("enable_belt_purge_tower", coBool);
def->label = L("Enable belt purge tower");
def->category = L("Multimaterial");
def->tooltip = L("Belt-printer replacement for the wipe/prime tower. When enabled on a belt "
"printer, a purge prism is automatically generated next to the printed parts "
"and filament-change purging is routed into it (the classic wipe tower cannot "
"be used on belt printers because its G-code bypasses the belt transform). "
"Only available on belt printers.");
def->mode = comAdvanced;
def->set_default_value(new ConfigOptionBool(false));
def = this->add("belt_purge_tower_width", coFloat);
def->label = L("Belt purge tower width");
def->category = L("Printable space");
def->tooltip = L("Width (machine X, across the belt) of the purge prism that is automatically "
"generated on belt printers when the prime tower is enabled and multiple "
"filaments are used. Filament-change purging is routed into this prism's "
"extrusions instead of a classic wipe tower. Its height is computed "
"automatically from the worst-case purge volume per layer: a wider prism "
"results in a shorter one.");
def->sidetext = L("mm");
def->min = 1.;
def->mode = comAdvanced;
def->set_default_value(new ConfigOptionFloat(35.));
def = this->add("tree_support_branch_angle", coFloat);
def->label = L("Tree support branch angle");
def->category = L("Support");
@@ -7966,6 +7991,16 @@ void PrintConfigDef::init_fff_params()
"It will not take effect unless the prime tower is enabled.");
def->set_default_value(new ConfigOptionBool(false));
// Internal marker (not shown in any settings tab): identifies the auto-generated
// belt purge prism so it can be updated/removed by the auto-manager and aligned
// to the object layer grid by the backend. Persisted to 3mf like any per-object key.
def = this->add("belt_purge_tower_object", coBool);
def->category = L("Flush options");
def->label = L("Belt purge tower object");
def->tooltip = L("Marks the auto-generated belt purge prism. Managed automatically; do not set manually.");
def->mode = comDevelop;
def->set_default_value(new ConfigOptionBool(false));
def = this->add("wipe_tower_bridging", coFloat);
def->label = L("Maximal bridging distance");
def->tooltip = L("Maximal distance between supports on sparse infill sections.");
+8
View File
@@ -1228,6 +1228,9 @@ PRINT_CONFIG_CLASS_DEFINE(
// BBS
((ConfigOptionBool, flush_into_infill))
((ConfigOptionBool, flush_into_support))
// Marker for the auto-generated belt purge prism; identifies the object to
// the auto-manager (GUI) and the layer-grid alignment step (backend).
((ConfigOptionBool, belt_purge_tower_object))
// BBS
((ConfigOptionFloat, tree_support_branch_distance))
((ConfigOptionFloat, tree_support_tip_diameter))
@@ -1802,6 +1805,11 @@ PRINT_CONFIG_CLASS_DERIVED_DEFINE(
((ConfigOptionFloat, belt_support_floor_offset))
((ConfigOptionEnum<BeltSupportFloorMode>, belt_support_floor_mode))
((ConfigOptionEnum<BeltSupportZOffsetMode>, belt_support_z_offset_mode))
// Width (machine X, across the belt) of the auto-generated belt purge prism.
((ConfigOptionFloat, belt_purge_tower_width))
// Belt-printer-only "type" of purge tower: enables the auto-generated belt
// purge prism (the belt replacement for the classic wipe/prime tower).
((ConfigOptionBool, enable_belt_purge_tower))
//BBS
((ConfigOptionInts, additional_cooling_fan_speed))
((ConfigOptionInts, close_additional_fan_first_x_layers))
+2 -1
View File
@@ -1663,7 +1663,8 @@ bool PrintObject::invalidate_state_by_config_options(
} else if (
opt_key == "flush_into_infill"
|| opt_key == "flush_into_objects"
|| opt_key == "flush_into_support") {
|| opt_key == "flush_into_support"
|| opt_key == "belt_purge_tower_object") {
invalidated |= m_print->invalidate_step(psWipeTower);
invalidated |= m_print->invalidate_step(psGCodeExport);
} else {
+1
View File
@@ -1392,6 +1392,7 @@ void apply_fuzzy_skin_segmentation(PrintObject &print_object, ThrowOnCancel thro
}); // end of parallel_for
}
// 1) Decides Z positions of the layers,
// 2) Initializes layers and their regions
// 3) Slices the object meshes
+2
View File
@@ -397,6 +397,8 @@ set(SLIC3R_GUI_SOURCES
GUI/ParamsPanel.hpp
GUI/PartPlate.cpp
GUI/PartPlate.hpp
GUI/BeltPurgeTower.cpp
GUI/BeltPurgeTower.hpp
GUI/FilamentGroupPopup.hpp
GUI/FilamentGroupPopup.cpp
GUI/PhysicalPrinterDialog.cpp
+373
View File
@@ -0,0 +1,373 @@
#include "BeltPurgeTower.hpp"
#include "GUI_App.hpp"
#include "GUI_ObjectList.hpp"
#include "PartPlate.hpp"
#include "I18N.hpp"
#include "libslic3r/Model.hpp"
#include "libslic3r/Preset.hpp"
#include "libslic3r/PresetBundle.hpp"
#include "libslic3r/PrintConfig.hpp"
#include "libslic3r/TriangleMesh.hpp"
#include "libslic3r/Geometry.hpp"
#include "libslic3r/BoundingBox.hpp"
#include <algorithm>
#include <cmath>
#include <limits>
#include <set>
#include <vector>
#include <boost/log/trivial.hpp>
namespace Slic3r {
namespace GUI {
// The classic wipe tower is disabled in belt mode (its G-code bypasses the belt
// transform), so filament-change purging is routed into this prism via
// flush_into_objects (see Print::_plan_belt_purge()). The prism is a real model
// object so it is sliced through the normal pipeline and picks up the belt
// rotation. Width across the belt is user-set (belt_purge_tower_width); height
// is sized so each tilted slicing plane's cross-section through the prism can
// absorb the worst-case purge volume of one layer; length follows the printed
// objects along the belt (plus ramp/height compensation at both tilted ends).
bool ensure_belt_purge_tower(Model &model, PartPlateList &partplate_list, ObjectList *obj_list, BeltPurgeSignature &sig)
{
auto is_prism = [](const ModelObject *mo) {
const ConfigOption *opt = mo->config.option("belt_purge_tower_object");
return opt != nullptr && opt->getBool();
};
std::vector<int> prism_idxs;
for (int i = 0; i < (int) model.objects.size(); ++i)
if (is_prism(model.objects[i]))
prism_idxs.push_back(i);
// Deletes prism objects, keeping the sidebar and part plates in sync
// (same primitives as Plater::priv::remove(), minus scene update — the
// caller refreshes the scene).
auto remove_prisms = [&](const std::vector<int> &idxs) {
for (auto it = idxs.rbegin(); it != idxs.rend(); ++it) {
model.delete_object(size_t(*it));
partplate_list.notify_instance_removed(*it, -1);
obj_list->delete_object_from_list(size_t(*it));
}
};
// Cheap early-out for non-belt printers: only belt printers ever get a belt
// purge tower, and this runs on every background-process tick — so avoid the
// full_config() merge below unless this is actually a belt printer. (Also
// tidies up a stale prism if the user switched away from a belt printer.)
{
const auto *belt_pre = wxGetApp().preset_bundle->printers.get_edited_preset().config.option<ConfigOptionBool>("belt_printer");
if (belt_pre == nullptr || !belt_pre->value) {
sig = BeltPurgeSignature{};
if (prism_idxs.empty())
return false;
remove_prisms(prism_idxs);
return true;
}
}
// Read every sizing input from the MERGED full config — the exact same
// config the backend slices with (BackgroundSlicingProcess::apply uses
// preset_bundle->full_config()). Reading from the individual presets caused
// GUI/backend mismatches: e.g. purge_in_prime_tower or a populated
// flush_volumes_matrix present in the merged config but false/empty in the
// preset the GUI happened to read, so the tower was sized for the small
// prime_volume instead of the real color-change flush and could not absorb
// it. The three names below alias the one merged config so the rest of the
// function is unchanged.
const DynamicPrintConfig full_cfg = wxGetApp().preset_bundle->full_config();
const DynamicPrintConfig &printer_config = full_cfg;
const DynamicPrintConfig &print_config = full_cfg;
const DynamicPrintConfig &project_config = full_cfg;
const auto *belt_opt = printer_config.option<ConfigOptionBool>("belt_printer");
const bool belt = belt_opt != nullptr && belt_opt->value;
// Belt purge tower is its own type, gated by the belt-only printer option
// enable_belt_purge_tower (not the classic process enable_prime_tower).
const bool prime_tower_enabled = printer_config.has("enable_belt_purge_tower") && printer_config.opt_bool("enable_belt_purge_tower");
const auto *seq_opt = print_config.option<ConfigOptionEnum<PrintSequence>>("print_sequence");
const bool by_object = seq_opt != nullptr && seq_opt->value == PrintSequence::ByObject;
// Filaments used and bounding extent of the non-prism objects on the
// current plate (1-based filament ids; volume extruder 0 = object default).
PartPlate *plate = partplate_list.get_curr_plate();
std::set<int> filaments;
double x_min = std::numeric_limits<double>::max();
double x_max = -std::numeric_limits<double>::max();
double y_min = std::numeric_limits<double>::max();
double y_max = -std::numeric_limits<double>::max();
double z_max = 0.;
bool have_objects = false;
if (belt && plate != nullptr) {
for (int obj_idx = 0; obj_idx < (int) model.objects.size(); ++obj_idx) {
const ModelObject *mo = model.objects[obj_idx];
if (is_prism(mo))
continue;
int obj_extruder = 1;
if (const ConfigOption *opt = mo->config.option("extruder"); opt != nullptr && opt->getInt() > 0)
obj_extruder = opt->getInt();
bool any_instance_on_plate = false;
for (int inst_idx = 0; inst_idx < (int) mo->instances.size(); ++inst_idx) {
if (!plate->contain_instance_totally(obj_idx, inst_idx))
continue;
any_instance_on_plate = true;
const BoundingBoxf3 bb = mo->instance_bounding_box(inst_idx);
x_min = std::min(x_min, bb.min.x());
x_max = std::max(x_max, bb.max.x());
y_min = std::min(y_min, bb.min.y());
y_max = std::max(y_max, bb.max.y());
z_max = std::max(z_max, bb.max.z());
}
if (!any_instance_on_plate)
continue;
have_objects = true;
for (const ModelVolume *mv : mo->volumes)
for (int e : mv->get_extruders())
filaments.insert(e > 0 ? e : obj_extruder);
}
}
const bool wanted = belt && prime_tower_enabled && !by_object && have_objects && filaments.size() > 1;
if (!wanted) {
sig = BeltPurgeSignature{};
if (prism_idxs.empty())
return false;
remove_prisms(prism_idxs);
BOOST_LOG_TRIVIAL(warning) << "[BELT-DEBUG] belt purge tower removed (conditions not met)";
return true;
}
// --- Sizing -----------------------------------------------------------
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
// travels along the *other* horizontal axis (X-rotation -> belt along Y,
// the CR-30 default). The purge prism is a long bar laid along the belt
// travel direction, beside the parts. For no/Z rotation we fall back to
// vertical slicing geometry (theta = 90 deg).
const auto *axis_opt = printer_config.option<ConfigOptionEnum<BeltRotationAxis>>("belt_slice_rotation");
const auto *angle_opt = printer_config.option<ConfigOptionFloat>("belt_slice_rotation_angle");
const BeltRotationAxis rot = axis_opt != nullptr ? axis_opt->value : BeltRotationAxis::X;
const bool belt_is_y = (rot != BeltRotationAxis::Y); // X / None / Z -> belt along Y
double theta = M_PI / 2.;
if ((rot == BeltRotationAxis::X || rot == BeltRotationAxis::Y) && angle_opt != nullptr && std::abs(angle_opt->value) > EPSILON)
theta = std::clamp(Geometry::deg2rad(std::abs(angle_opt->value)), Geometry::deg2rad(5.), M_PI / 2.);
const double sin_t = std::sin(theta);
const double cot_t = std::cos(theta) / sin_t;
// Parts' extent along the belt-travel axis and the lateral (across-belt) axis.
const double belt_min = belt_is_y ? y_min : x_min;
const double belt_max = belt_is_y ? y_max : x_max;
const double lat_min = belt_is_y ? x_min : y_min;
const double lat_max = belt_is_y ? x_max : y_max;
// Worst-case purge volume of one layer: up to (filament count - 1)
// toolchanges, each needing the worst flush matrix entry (mirrors the
// volume selection in Print::_plan_belt_purge()).
// NOTE: both purge_in_prime_tower and single_extruder_multi_material are
// PRINTER options (Preset.cpp s_Preset_printer_options) — read them from the
// printer preset. Reading purge_in_prime_tower from the print preset returns
// has()==false, collapsing use_matrix to false and sizing the tower for the
// small prime_volume instead of the real color-change flush. This must match
// the backend Print::_plan_belt_purge() which reads both from the merged config.
const bool use_matrix = (printer_config.has("purge_in_prime_tower") && printer_config.opt_bool("purge_in_prime_tower"))
&& (printer_config.has("single_extruder_multi_material") && printer_config.opt_bool("single_extruder_multi_material"));
double max_flush = print_config.has("prime_volume") ? print_config.opt_float("prime_volume") : 45.;
if (use_matrix) {
const size_t extruder_nums = wxGetApp().preset_bundle->get_printer_extruder_count();
const std::vector<double> matrix = get_flush_volumes_matrix(
project_config.option<ConfigOptionFloats>("flush_volumes_matrix")->values, 0, extruder_nums);
const auto * multi_opt = project_config.option<ConfigOptionFloats>("flush_multiplier");
const double multiplier = multi_opt != nullptr && !multi_opt->values.empty() ? multi_opt->get_at(0) : 1.;
const int n_total = (int) (std::sqrt(double(matrix.size())) + 0.5);
double m = 0.;
for (int i : filaments)
for (int j : filaments)
if (i != j && i <= n_total && j <= n_total)
m = std::max(m, matrix[size_t(i - 1) * n_total + size_t(j - 1)]);
if (m > 0.)
max_flush = m * multiplier;
}
const double v_layer = double(filaments.size() - 1) * max_flush;
// Height from the per-layer purge demand. A tilted slicing plane cuts a
// width x (height/sin) rectangle out of the bar, so one layer slab absorbs
// width * (height/sin) * layer_height of purge. Solve for the height that
// holds the worst-case per-layer purge, with eta (infill/perimeter packing)
// and a safety margin for the tilt ramps / grid-alignment slop, plus a
// minimum so the tower is a real printable body rather than a sliver.
const double eta = 0.85;
const double safety = 1.6;
const double printable_height = printer_config.has("printable_height") ? printer_config.opt_float("printable_height") : 250.;
double height = safety * v_layer * sin_t / (width * layer_h * eta);
height = std::clamp(height, 8.0, std::max(8.0, printable_height));
// --- Idempotence (input-keyed) ----------------------------------------
auto q = [](double v) { return std::lround(v * 10.0); }; // 0.1 mm quantization
BeltPurgeSignature new_sig;
new_sig.valid = true;
new_sig.filament_count = (int) filaments.size();
new_sig.key[0] = q(width);
new_sig.key[1] = q(layer_h);
new_sig.key[2] = q(height);
new_sig.key[3] = q(belt_min);
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
// --- Position ----------------------------------------------------------
// Belt-travel axis. With the mesh rotated by theta before slicing, a machine
// point (y,z) maps to slicing-Z = y*sin(theta) + z*cos(theta). The parts
// occupy slicing-Z in [y_min*sin, y_max*sin + z_max*cos], and the bar's
// FULL-cross-section region (the part not in a triangular end ramp) spans
// slicing-Z [belt_start*sin + H*cos, belt_end*sin]. Covering the parts'
// whole band needs belt_start <= y_min - H*cot (bar's own leading ramp) and
// belt_end >= y_max + z_max*cot (parts' top features print further up the
// belt). The trailing z_max*cot term dominates the bar's own ramp.
const double margin = 5.;
const double ramp_compensation = height / sin_t;
const double belt_start = std::max(0.0, belt_min - ramp_compensation); // leading ramp, toward Y=0
const double belt_end = belt_max + margin + ramp_compensation + z_max * cot_t; // + parts' top-feature belt reach
const double length = std::max(belt_end - belt_start, 10.);
const double belt_center = 0.5 * (belt_start + belt_end);
// Across-belt: flush against the bed's maximum edge, inset by half the bar
// width so the bar's far edge sits on the boundary and the whole bar stays
// on the bed. The bed (printable_area) is plate-local but model instances
// live in the plate's world frame, so add the plate origin's lateral
// component. lat_min/lat_max come from instance_bounding_box (world frame).
const Vec3d plate_origin = plate->get_origin();
const double lat_origin = plate_origin[belt_is_y ? 0 : 1];
const double inset = 1.;
double lat_center = lat_max + 5. + 0.5 * width; // fallback: just past the parts
BoundingBoxf bed_ext_dbg;
if (const auto *bed_opt = printer_config.option<ConfigOptionPoints>("printable_area");
bed_opt != nullptr && !bed_opt->values.empty()) {
const BoundingBoxf bed_ext = get_extents(bed_opt->values);
bed_ext_dbg = bed_ext;
const double bed_lat_max = belt_is_y ? bed_ext.max.x() : bed_ext.max.y();
lat_center = lat_origin + bed_lat_max - inset - 0.5 * width;
}
BOOST_LOG_TRIVIAL(warning) << "[BELT-DEBUG] purge place"
<< " plate_origin=(" << plate_origin.x() << "," << plate_origin.y() << ")"
<< " parts_x=[" << x_min << "," << x_max << "] parts_y=[" << y_min << "," << y_max << "] z_max=" << z_max
<< " bed_ext=[" << bed_ext_dbg.min.x() << "," << bed_ext_dbg.min.y()
<< " -> " << bed_ext_dbg.max.x() << "," << bed_ext_dbg.max.y() << "]"
<< " lat_center=" << lat_center << " belt=[" << belt_start << "," << belt_end << "]";
const Vec3d desired_center(belt_is_y ? lat_center : belt_center,
belt_is_y ? belt_center : lat_center,
0.5 * height);
// Build the prism as N DISCONNECTED sub-bars side by side across the belt,
// N = (filaments - 1) = the worst-case number of toolchanges on one layer.
// Why: mark_wiping_extrusions overrides whole extrusion-entity COLLECTIONS,
// and each disconnected island slices into its own infill collection. With a
// single solid box there is one collection per layer, so the FIRST toolchange
// on a layer grabs the entire collection (consuming all of it for one swap)
// and any further swaps on that layer find nothing left and go unabsorbed
// (the classic wipe tower hides this by spilling leftover into the real
// tower; the belt prism has no fallback). One island per simultaneous swap
// 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.
// 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 w_sub = (width - (n_islands - 1) * gap) / n_islands;
// --- (Re)create ---------------------------------------------------------
if (!prism_idxs.empty())
remove_prisms(prism_idxs);
TriangleMesh prism_mesh;
for (int i = 0; i < n_islands; ++i) {
const double lat_off = i * (w_sub + gap);
// Box dims: lateral = w_sub, along-belt = length, vertical = height.
TriangleMesh box = belt_is_y ? make_cube(w_sub, length, height) // X = lateral, Y = belt
: make_cube(length, w_sub, height); // X = belt, Y = lateral
box.translate(belt_is_y ? Vec3f((float) lat_off, 0.f, 0.f) : Vec3f(0.f, (float) lat_off, 0.f));
prism_mesh.merge(box);
}
ModelObject *new_object = model.add_object();
new_object->name = _u8L("Belt Purge Tower");
new_object->add_instance();
ModelVolume *new_volume = new_object->add_volume(std::move(prism_mesh));
new_volume->name = new_object->name;
auto &cfg = new_object->config;
cfg.set_key_value("belt_purge_tower_object", new ConfigOptionBool(true));
cfg.set_key_value("flush_into_objects", new ConfigOptionBool(true));
cfg.set_key_value("extruder", new ConfigOptionInt(1));
// Sacrificial solid prism: one wall, no shells, dense rectilinear infill —
// every extrusion is overriddable, so the absorbed volume matches the
// cross-section x layer-height estimate used for the height above.
cfg.set_key_value("wall_loops", new ConfigOptionInt(1));
cfg.set_key_value("top_shell_layers", new ConfigOptionInt(0));
cfg.set_key_value("bottom_shell_layers", new ConfigOptionInt(0));
cfg.set_key_value("sparse_infill_density", new ConfigOptionPercent(100));
cfg.set_key_value("sparse_infill_pattern", new ConfigOptionEnum<InfillPattern>(ipRectilinear));
cfg.set_key_value("enable_support", new ConfigOptionBool(false));
cfg.set_key_value("brim_type", new ConfigOptionEnum<BrimType>(btNoBrim));
cfg.set_key_value("seam_slope_type", new ConfigOptionEnum<SeamScarfType>(SeamScarfType::None));
cfg.set_key_value("precise_z_height", new ConfigOptionBool(false));
// Position by the belt-calibration pattern: drop to the bed, then translate
// the instance by the delta between the object's ACTUAL bbox center and the
// target. Setting the instance offset directly is unreliable here — the
// freshly added cube's local frame is not centered, so set_offset() lands
// the min corner (not the center) on the target, leaving the bar centered
// on the bed edge with half of it hanging off.
new_object->invalidate_bounding_box();
new_object->ensure_on_bed();
const BoundingBoxf3 cur = new_object->bounding_box_exact();
new_object->translate_instances(Vec3d(desired_center.x() - cur.center().x(),
desired_center.y() - cur.center().y(),
0.0));
new_object->instances.front()->set_assemble_transformation(new_object->instances.front()->get_transformation());
const size_t obj_idx = model.objects.size() - 1;
// Registers the object in the sidebar and notifies the part plates;
// selection is left untouched (auto-managed object).
obj_list->add_object_to_list(obj_idx, /*call_selection_changed=*/false);
// Record the inputs that produced this prism so subsequent ticks are no-ops
// until the parts/config actually change.
sig = new_sig;
BOOST_LOG_TRIVIAL(warning) << "[BELT-DEBUG] belt purge tower generated"
<< " belt_is_y=" << belt_is_y
<< " W=" << width << " L=" << length << " H=" << height
<< " v_layer=" << v_layer << " max_flush=" << max_flush
<< " filaments=" << filaments.size()
<< " theta_deg=" << Geometry::rad2deg(theta)
<< " desired_center=(" << desired_center.x() << "," << desired_center.y() << "," << desired_center.z() << ")"
<< " achieved_center=(" << new_object->bounding_box_exact().center().x() << ","
<< new_object->bounding_box_exact().center().y() << ")";
return true;
}
} // namespace GUI
} // namespace Slic3r
+45
View File
@@ -0,0 +1,45 @@
#pragma once
// ORCA-Belt: auto-managed purge prism for belt printers.
//
// Kept in its own translation unit (not buried in Plater.cpp) so it stays out
// of the way of unrelated upstream changes and carries no regression risk for
// normal printers: nothing here runs unless the printer is a belt printer with
// the belt purge tower enabled. See Slic3r::GUI::ensure_belt_purge_tower().
namespace Slic3r {
class Model;
class PartPlateList;
namespace GUI {
class ObjectList;
// Inputs of the last belt purge prism generation. Idempotence is keyed on these
// (not the prism's resulting bbox): the prism gets nudged by plate assignment
// after creation, so comparing its bbox would falsely detect a change and
// regenerate it on every background-process tick — which would re-invalidate a
// freshly sliced result and make the G-code preview unreachable.
struct BeltPurgeSignature
{
bool valid = false;
int filament_count = 0;
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 < 10; ++i)
if (key[i] != o.key[i])
return false;
return true;
}
};
// Keep the auto-generated belt purge prism in sync with the current config and
// plate contents. Creates / updates / removes the marked prism ModelObject.
// Returns true when the model was mutated (caller should refresh the scene).
// Runs on every background-process update, so it is idempotent: it only mutates
// the model when the desired prism differs from the cached signature in `sig`.
bool ensure_belt_purge_tower(Model &model, PartPlateList &partplate_list, ObjectList *obj_list, BeltPurgeSignature &sig);
} // namespace GUI
} // namespace Slic3r
+14 -6
View File
@@ -971,11 +971,19 @@ void ConfigManipulation::toggle_print_fff_options(DynamicPrintConfig *config, in
toggle_line("preheat_steps", have_ooze_prevention && (preheat_steps > 0));
bool have_prime_tower = config->opt_bool("enable_prime_tower");
// ORCA-Belt: belt printers replace the classic wipe tower with the
// auto-generated belt purge prism, enabled by the belt-only printer option
// enable_belt_purge_tower. Its width (a process option) is only relevant
// then. (is_belt_printer is computed at the top of this function.)
const bool have_belt_purge_tower = is_belt_printer
&& preset_bundle->printers.get_edited_preset().config.has("enable_belt_purge_tower")
&& preset_bundle->printers.get_edited_preset().config.opt_bool("enable_belt_purge_tower");
toggle_line("belt_purge_tower_width", have_belt_purge_tower);
for (auto el : {"prime_tower_width", "prime_tower_brim_width", "prime_tower_skip_points", "wipe_tower_wall_type", "prime_tower_infill_gap","prime_tower_enable_framework", "enable_tower_interface_features"})
toggle_line(el, have_prime_tower);
toggle_line(el, have_prime_tower && !is_belt_printer);
toggle_line("enable_tower_interface_cooldown_during_tower",
have_prime_tower && config->opt_bool("enable_tower_interface_features"));
have_prime_tower && !is_belt_printer && config->opt_bool("enable_tower_interface_features"));
bool purge_in_primetower = preset_bundle->printers.get_edited_preset().config.opt_bool("purge_in_prime_tower");
@@ -983,17 +991,17 @@ void ConfigManipulation::toggle_print_fff_options(DynamicPrintConfig *config, in
"wipe_tower_extra_spacing", "wipe_tower_max_purge_speed",
"wipe_tower_bridging", "wipe_tower_extra_flow",
"wipe_tower_no_sparse_layers"})
toggle_line(el, have_prime_tower && supports_wipe_tower_2);
toggle_line(el, have_prime_tower && supports_wipe_tower_2 && !is_belt_printer);
WipeTowerWallType wipe_tower_wall_type = config->opt_enum<WipeTowerWallType>("wipe_tower_wall_type");
bool have_rib_wall = (wipe_tower_wall_type == WipeTowerWallType::wtwRib)&&have_prime_tower;
toggle_line("wipe_tower_cone_angle", have_prime_tower && supports_wipe_tower_2 && wipe_tower_wall_type == WipeTowerWallType::wtwCone);
bool have_rib_wall = (wipe_tower_wall_type == WipeTowerWallType::wtwRib)&&have_prime_tower&&!is_belt_printer;
toggle_line("wipe_tower_cone_angle", have_prime_tower && supports_wipe_tower_2 && !is_belt_printer && wipe_tower_wall_type == WipeTowerWallType::wtwCone);
toggle_line("wipe_tower_extra_rib_length", have_rib_wall);
toggle_line("wipe_tower_rib_width", have_rib_wall);
toggle_line("wipe_tower_fillet_wall", have_rib_wall);
toggle_field("prime_tower_width", have_prime_tower && !have_rib_wall);
toggle_line("single_extruder_multi_material_priming", !bSEMM && have_prime_tower && supports_wipe_tower_2);
toggle_line("single_extruder_multi_material_priming", !bSEMM && have_prime_tower && supports_wipe_tower_2 && !is_belt_printer);
toggle_line("prime_volume",have_prime_tower && (!purge_in_primetower || !bSEMM));
+29 -1
View File
@@ -2866,7 +2866,13 @@ void GLCanvas3D::reload_scene(bool refresh_immediately, bool force_full_scene_re
need_wipe_tower |= dynamic_cast<const ConfigOptionBool*>(dconfig.option("enable_wrapping_detection"))->value;
}
if (wt && (need_wipe_tower || filaments_count > 1) && !wxGetApp().plater()->only_gcode_mode() && !wxGetApp().plater()->is_gcode_3mf()) {
// Belt printers replace the classic wipe tower with the auto-generated
// belt purge prism (a real model object), so never draw the tower widget.
bool is_belt_printer = false;
if (const auto *belt_opt = wxGetApp().preset_bundle->printers.get_edited_preset().config.option<ConfigOptionBool>("belt_printer"))
is_belt_printer = belt_opt->value;
if (wt && !is_belt_printer && (need_wipe_tower || filaments_count > 1) && !wxGetApp().plater()->only_gcode_mode() && !wxGetApp().plater()->is_gcode_3mf()) {
for (int plate_id = 0; plate_id < n_plates; plate_id++) {
// If print ByObject and there is only one object in the plate, the wipe tower is allowed to be generated.
PartPlate* part_plate = ppl.get_plate(plate_id);
@@ -2942,6 +2948,28 @@ void GLCanvas3D::reload_scene(bool refresh_immediately, bool force_full_scene_re
}
update_volumes_colors_by_extruder();
// ORCA-Belt: render the auto-generated belt purge prism like a wipe tower —
// semi-transparent, in its (filament) color. It is a real sliced object, so
// the G-code preview already shows the actual per-layer purge colors; here in
// the editor we just make the block translucent so it reads as a purge tower
// rather than a solid part. update_colors_by_extruder() preserves alpha when
// not updating alpha, so lowering it once sticks across recolors.
if (m_model != nullptr) {
for (GLVolume *volume : m_volumes.volumes) {
if (volume == nullptr || volume->volume_idx() < 0)
continue;
const int obj_idx = volume->object_idx();
if (obj_idx < 0 || obj_idx >= (int) m_model->objects.size())
continue;
const ConfigOption *opt = m_model->objects[obj_idx]->config.option("belt_purge_tower_object");
if (opt != nullptr && opt->getBool()) {
volume->color.a(0.66f);
volume->force_transparent = true;
}
}
}
// Update selection indices based on the old/new GLVolumeCollection.
if (m_selection.get_mode() == Selection::Instance)
m_selection.instances_changed(instance_ids_selected);
+6
View File
@@ -291,6 +291,12 @@ void ArrangeJob::prepare_wipe_tower()
bool enable_prime_tower = op && op->getBool();
if (!enable_prime_tower || params.is_seq_print) return;
// Belt printers have no classic wipe tower; purging goes into the belt
// purge prism, which is a real model object and arranges like any other.
if (const auto *belt_opt = wxGetApp().preset_bundle->printers.get_edited_preset().config.option<ConfigOptionBool>("belt_printer");
belt_opt && belt_opt->value)
return;
bool smooth_timelapse = false;
auto sop = current_config.option("timelapse_type");
if (sop) { smooth_timelapse = sop->getInt() == TimelapseType::tlSmooth; }
+20
View File
@@ -105,6 +105,7 @@
#include "GUI_Preview.hpp"
#include "3DBed.hpp"
#include "PartPlate.hpp"
#include "BeltPurgeTower.hpp"
#include "Camera.hpp"
#include "Mouse3DController.hpp"
#include "Tab.hpp"
@@ -5475,6 +5476,11 @@ struct Plater::priv
void exit_gizmo();
void remove(size_t obj_idx);
bool delete_object_from_model(size_t obj_idx, bool refresh_immediately = true); //BBS
// ORCA-Belt: keep the auto-generated belt purge prism in sync with the
// config and plate contents (thin wrapper over GUI::ensure_belt_purge_tower
// in BeltPurgeTower.cpp). Returns true when the model was mutated.
bool ensure_belt_purge_tower();
BeltPurgeSignature m_belt_purge_sig;
void delete_all_objects_from_model();
void reset(bool apply_presets_change = false);
void center_selection();
@@ -8879,6 +8885,16 @@ void Plater::priv::process_validation_warnings(const std::vector<StringObjectExc
}
// ORCA-Belt: the actual implementation lives in BeltPurgeTower.cpp (kept out of
// this large, frequently-touched file so it stays clear of unrelated upstream
// changes and carries no regression risk for normal printers). This is a thin
// wrapper that hands it the model, plates, object list, and cached signature.
bool Plater::priv::ensure_belt_purge_tower()
{
return GUI::ensure_belt_purge_tower(model, partplate_list, sidebar->obj_list(), m_belt_purge_sig);
}
// Update background processing thread from the current config and Model.
// Returns a bitmask of UpdateBackgroundProcessReturnState.
unsigned int Plater::priv::update_background_process(bool force_validation, bool postpone_error_messages, bool switch_print)
@@ -8889,6 +8905,10 @@ unsigned int Plater::priv::update_background_process(bool force_validation, bool
// If the update_background_process() was not called by the timer, kill the timer,
// so the update_restart_background_process() will not be called again in vain.
background_process_timer.Stop();
// ORCA-Belt: sync the auto-managed belt purge prism before the model is
// applied to the Print below, so the prism change rides this same apply.
if (printer_technology == ptFFF && this->ensure_belt_purge_tower())
return_state |= UPDATE_BACKGROUND_PROCESS_REFRESH_SCENE;
// Update the "out of print bed" state of ModelInstances.
update_print_volume_state();
// Apply new config to the possibly running background task.
+14 -1
View File
@@ -3009,6 +3009,7 @@ void TabPrint::build()
optgroup->append_single_option_line("enable_tower_interface_cooldown_during_tower", "multimaterial_settings_prime_tower");
optgroup->append_single_option_line("prime_tower_enable_framework", "multimaterial_settings_prime_tower");
optgroup->append_single_option_line("prime_tower_width", "multimaterial_settings_prime_tower#width");
optgroup->append_single_option_line("belt_purge_tower_width", "multimaterial_settings_prime_tower");
optgroup->append_single_option_line("prime_volume", "multimaterial_settings_prime_tower");
optgroup->append_single_option_line("prime_tower_brim_width", "multimaterial_settings_prime_tower#brim-width");
optgroup->append_single_option_line("prime_tower_infill_gap", "multimaterial_settings_prime_tower");
@@ -5698,6 +5699,12 @@ if (is_marlin_flavor)
optgroup->append_single_option_line("enable_filament_ramming", "printer_multimaterial_wipe_tower#enable-filament-ramming");
optgroup->append_single_option_line("tool_change_on_wipe_tower", "printer_multimaterial_wipe_tower#tool-change-on-wipe-tower");
// Orca-Belt: belt printers replace the classic wipe tower with an
// auto-generated purge prism; this is its enable (gated to belt printers
// in toggle_options()).
optgroup = page->new_optgroup(L("Belt purge tower"), "param_tower");
optgroup->append_single_option_line("enable_belt_purge_tower", "printer_multimaterial_wipe_tower");
optgroup = page->new_optgroup(L("Single extruder multi-material parameters"), "param_settings");
optgroup->append_single_option_line("cooling_tube_retraction", "printer_multimaterial_semm_parameters#cooling-tube-position");
@@ -6232,8 +6239,14 @@ void TabPrinter::toggle_options()
}
if (m_active_page->title() == L("Multimaterial")) {
// Orca-Belt: belt printers use the belt purge tower instead of the classic
// wipe tower — show its enable only on belt printers, and hide the classic
// wipe-tower fields there (the classic tower's G-code bypasses the belt transform).
const bool is_belt_printer = m_config->opt_bool("belt_printer");
toggle_line("enable_belt_purge_tower", is_belt_printer);
const bool supports_wipe_tower_2 = !is_BBL_printer && m_config->opt_enum<WipeTowerType>("wipe_tower_type") == WipeTowerType::Type2;
toggle_line("wipe_tower_type", !is_BBL_printer);
toggle_line("wipe_tower_type", !is_BBL_printer && !is_belt_printer);
// SoftFever: hide specific settings for BBL printer
for (auto el : {
"enable_filament_ramming",
+35
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) }, {