Purge tower part 1

This commit is contained in:
harrierpigeon
2026-08-08 16:35:54 -05:00
parent 39b087d6ea
commit 88726d76e8
14 changed files with 548 additions and 15 deletions
+10
View File
@@ -0,0 +1,10 @@
{
"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
}
+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) if (!m_print_config_ptr || !m_print_config_ptr->enable_prime_tower)
return false; 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) if (m_print_config_ptr->wipe_tower_filament == 0)
return false; return false;
+2
View File
@@ -1160,6 +1160,8 @@ static std::vector<std::string> s_Preset_print_options{
"prime_volume", "prime_volume",
"prime_tower_infill_gap", "prime_tower_infill_gap",
"prime_tower_flat_ironing", "prime_tower_flat_ironing",
"belt_purge_tower_width",
"belt_purge_tower_object",
"enable_tower_interface_features", "enable_tower_interface_features",
"enable_tower_interface_cooldown_during_tower", "enable_tower_interface_cooldown_during_tower",
"wipe_tower_no_sparse_layers", "wipe_tower_no_sparse_layers",
+216 -7
View File
@@ -22,6 +22,7 @@
#include "MaterialType.hpp" #include "MaterialType.hpp"
#include "Model.hpp" #include "Model.hpp"
#include "format.hpp" #include "format.hpp"
#include "LocalesUtils.hpp"
#include <float.h> #include <float.h>
#include <algorithm> #include <algorithm>
@@ -389,6 +390,7 @@ bool Print::invalidate_state_by_config_options(const ConfigOptionResolver & /* n
|| opt_key == "prime_volume" || opt_key == "prime_volume"
|| opt_key == "flush_into_infill" || opt_key == "flush_into_infill"
|| opt_key == "flush_into_support" || opt_key == "flush_into_support"
|| opt_key == "belt_purge_tower_width"
|| opt_key == "initial_layer_infill_speed" || opt_key == "initial_layer_infill_speed"
|| opt_key == "travel_speed" || opt_key == "travel_speed"
|| opt_key == "travel_speed_z" || opt_key == "travel_speed_z"
@@ -1473,6 +1475,15 @@ StringObjectException Print::validate(std::vector<StringObjectException> *warnin
} }
if (m_config.enable_prime_tower) { if (m_config.enable_prime_tower) {
if (m_config.belt_printer.value && m_config.print_sequence == PrintSequence::ByObject
&& extruders.size() > 1 && warning != nullptr) {
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_prime_tower";
warningtemp.is_warning = true;
*warning = warningtemp;
}
for (const PrintObject* object : m_objects) { for (const PrintObject* object : m_objects) {
if (object->config().precise_z_height.value) { if (object->config().precise_z_height.value) {
warn(L("Enabling both precise Z height and the prime tower may cause slicing errors."), "precise_z_height"); warn(L("Enabling both precise Z height and the prime tower may cause slicing errors."), "precise_z_height");
@@ -1596,7 +1607,7 @@ StringObjectException Print::validate(std::vector<StringObjectException> *warnin
return {_u8L("Variable layer height is not supported with Organic supports.") }; 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 // 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 // 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()); double first_nozzle_diam = m_config.nozzle_diameter.get_at(extruders.front());
@@ -1612,12 +1623,17 @@ StringObjectException Print::validate(std::vector<StringObjectException> *warnin
} }
} }
if (! m_config.use_relative_e_distances) // The following two constraints come from the classic wipe tower G-code
return { L("The Wipe Tower is currently only supported with the relative extruder addressing (use_relative_e_distances=1).") }; // 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 0
if (m_config.gcode_flavor != gcfRepRapSprinter && m_config.gcode_flavor != gcfRepRapFirmware && 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) m_config.gcode_flavor != gcfRepetier && m_config.gcode_flavor != gcfMarlinLegacy && m_config.gcode_flavor != gcfMarlinFirmware)
@@ -2708,7 +2724,10 @@ void Print::process(long long *time_cost_with_cache, bool use_cache)
m_wipe_tower_data.clear(); m_wipe_tower_data.clear();
m_tool_ordering.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(); this->_make_wipe_tower();
} }
else if (this->config().print_sequence != PrintSequence::ByObject) { else if (this->config().print_sequence != PrintSequence::ByObject) {
@@ -4085,6 +4104,13 @@ int Print::get_config_index(int filament_id, int layer_id, const std::vector<std
// Wipe tower support. // Wipe tower support.
bool Print::has_wipe_tower() const 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_prime_tower.value == true) {
if (m_config.enable_wrapping_detection.value && m_config.wrapping_exclude_area.values.size() > 2) if (m_config.enable_wrapping_detection.value && m_config.wrapping_exclude_area.values.size() > 2)
return true; return true;
@@ -4097,6 +4123,16 @@ bool Print::has_wipe_tower() const
return false; return false;
} }
// 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
{
return m_config.belt_printer.value
&& m_config.enable_prime_tower.value
&& !m_config.spiral_mode.value
&& m_config.filament_diameter.values.size() > 1;
}
const WipeTowerData &Print::wipe_tower_data(size_t filaments_cnt) const 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. // If the wipe tower wasn't created yet, make sure the depth and brim_width members are set to default.
@@ -4183,6 +4219,179 @@ bool Print::enable_timelapse_print() const
return m_config.timelapse_type.value == TimelapseType::tlSmooth; return m_config.timelapse_type.value == TimelapseType::tlSmooth;
} }
// Belt mode: snap the purge prism's layer grid onto the printed objects' grid.
// After belt slicing every 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 do not share a layer grid. Purge
// marking looks absorbers up with get_layer_at_printz(lt.print_z, EPSILON),
// so the prism only absorbs purge at toolchange print_z values that coincide
// with one of its own layers. Shifting the prism by at most half a layer
// (a sub-layer-height displacement along the belt) makes its grid residue
// match the reference object's; both grids step by the same layer height
// (enforced by validate()), so matching residues means exact layer matches.
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) {
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).
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);
bool grids_mismatch = false;
for (const PrintObject *po : m_objects) {
if (po == ref || po->config().belt_purge_tower_object.value || po->layers().empty())
continue;
double d = std::abs(grid_offset(po) - ref_offset);
d = std::min(d, h - d);
if (d > 5. * EPSILON) {
grids_mismatch = true;
break;
}
}
// Shift normalized to (-h/2, h/2].
double delta = ref_offset - grid_offset(prism);
if (delta > 0.5 * h)
delta -= h;
else if (delta <= -0.5 * h)
delta += h;
BOOST_LOG_TRIVIAL(debug) << "[BELT-DEBUG] purge prism grid snap"
<< " ref=" << ref->model_object()->name
<< " ref_offset=" << ref_offset
<< " prism_offset=" << grid_offset(prism)
<< " delta=" << delta
<< " grids_mismatch=" << grids_mismatch;
prism->belt_shift_layer_grid(delta);
if (grids_mismatch)
this->active_step_add_warning(
PrintStateBase::WarningLevel::NON_CRITICAL,
_u8L("Objects on the plate are sliced on different layer grids; the belt purge tower can only follow "
"one of them. Filament changes on layers of the other objects may not be fully purged."));
}
// 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);
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;
for (const unsigned int extruder_id : layer_tools.extruders) {
if (extruder_id == current_extruder_id)
continue;
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;
}
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();
}
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;
}
}
void Print::_make_wipe_tower() void Print::_make_wipe_tower()
{ {
m_wipe_tower_data.clear(); m_wipe_tower_data.clear();
+12
View File
@@ -558,6 +558,10 @@ private:
std::vector<std::set<int>> detect_extruder_geometric_unprintables() const; std::vector<std::set<int>> detect_extruder_geometric_unprintables() const;
void slice_volumes(); 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);
//BBS //BBS
ExPolygons _shrink_contour_holes(double contour_delta, double hole_delta, const ExPolygons& polys) const; ExPolygons _shrink_contour_holes(double contour_delta, double hole_delta, const ExPolygons& polys) const;
// BBS // BBS
@@ -1085,6 +1089,8 @@ public:
// Wipe tower support. // Wipe tower support.
bool has_wipe_tower() const; 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 WipeTowerData& wipe_tower_data(size_t filaments_cnt = 0) const;
const ToolOrdering& tool_ordering() const { return m_tool_ordering; } const ToolOrdering& tool_ordering() const { return m_tool_ordering; }
@@ -1330,6 +1336,12 @@ private:
void _make_skirt(); void _make_skirt();
void _make_wipe_tower(); 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 finalize_first_layer_convex_hull();
void update_filament_self_index_cache(); void update_filament_self_index_cache();
// Deduplicates, per filament, the (extruder type x volume type) variants the grouping // Deduplicates, per filament, the (extruder type x volume type) variants the grouping
+24
View File
@@ -7321,6 +7321,20 @@ void PrintConfigDef::init_fff_params()
def->set_default_value(new ConfigOptionEnum<BeltSupportZOffsetMode>(BeltSupportZOffsetMode::Unconditional)); def->set_default_value(new ConfigOptionEnum<BeltSupportZOffsetMode>(BeltSupportZOffsetMode::Unconditional));
} }
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 = this->add("tree_support_branch_angle", coFloat);
def->label = L("Tree support branch angle"); def->label = L("Tree support branch angle");
def->category = L("Support"); def->category = L("Support");
@@ -7966,6 +7980,16 @@ void PrintConfigDef::init_fff_params()
"It will not take effect unless the prime tower is enabled."); "It will not take effect unless the prime tower is enabled.");
def->set_default_value(new ConfigOptionBool(false)); 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 = this->add("wipe_tower_bridging", coFloat);
def->label = L("Maximal bridging distance"); def->label = L("Maximal bridging distance");
def->tooltip = L("Maximal distance between supports on sparse infill sections."); def->tooltip = L("Maximal distance between supports on sparse infill sections.");
+5
View File
@@ -1228,6 +1228,9 @@ PRINT_CONFIG_CLASS_DEFINE(
// BBS // BBS
((ConfigOptionBool, flush_into_infill)) ((ConfigOptionBool, flush_into_infill))
((ConfigOptionBool, flush_into_support)) ((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 // BBS
((ConfigOptionFloat, tree_support_branch_distance)) ((ConfigOptionFloat, tree_support_branch_distance))
((ConfigOptionFloat, tree_support_tip_diameter)) ((ConfigOptionFloat, tree_support_tip_diameter))
@@ -1802,6 +1805,8 @@ PRINT_CONFIG_CLASS_DERIVED_DEFINE(
((ConfigOptionFloat, belt_support_floor_offset)) ((ConfigOptionFloat, belt_support_floor_offset))
((ConfigOptionEnum<BeltSupportFloorMode>, belt_support_floor_mode)) ((ConfigOptionEnum<BeltSupportFloorMode>, belt_support_floor_mode))
((ConfigOptionEnum<BeltSupportZOffsetMode>, belt_support_z_offset_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))
//BBS //BBS
((ConfigOptionInts, additional_cooling_fan_speed)) ((ConfigOptionInts, additional_cooling_fan_speed))
((ConfigOptionInts, close_additional_fan_first_x_layers)) ((ConfigOptionInts, close_additional_fan_first_x_layers))
+2 -1
View File
@@ -1663,7 +1663,8 @@ bool PrintObject::invalidate_state_by_config_options(
} else if ( } else if (
opt_key == "flush_into_infill" opt_key == "flush_into_infill"
|| opt_key == "flush_into_objects" || 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(psWipeTower);
invalidated |= m_print->invalidate_step(psGCodeExport); invalidated |= m_print->invalidate_step(psGCodeExport);
} else { } else {
+21
View File
@@ -1392,6 +1392,27 @@ void apply_fuzzy_skin_segmentation(PrintObject &print_object, ThrowOnCancel thro
}); // end of parallel_for }); // end of parallel_for
} }
// 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);
}
// 1) Decides Z positions of the layers, // 1) Decides Z positions of the layers,
// 2) Initializes layers and their regions // 2) Initializes layers and their regions
// 3) Slices the object meshes // 3) Slices the object meshes
+11 -6
View File
@@ -971,11 +971,16 @@ void ConfigManipulation::toggle_print_fff_options(DynamicPrintConfig *config, in
toggle_line("preheat_steps", have_ooze_prevention && (preheat_steps > 0)); toggle_line("preheat_steps", have_ooze_prevention && (preheat_steps > 0));
bool have_prime_tower = config->opt_bool("enable_prime_tower"); 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 — only its width is tunable; the
// classic tower geometry options do not apply. (is_belt_printer is
// computed at the top of this function.)
toggle_line("belt_purge_tower_width", have_prime_tower && is_belt_printer);
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"}) 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", 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"); bool purge_in_primetower = preset_bundle->printers.get_edited_preset().config.opt_bool("purge_in_prime_tower");
@@ -983,17 +988,17 @@ void ConfigManipulation::toggle_print_fff_options(DynamicPrintConfig *config, in
"wipe_tower_extra_spacing", "wipe_tower_max_purge_speed", "wipe_tower_extra_spacing", "wipe_tower_max_purge_speed",
"wipe_tower_bridging", "wipe_tower_extra_flow", "wipe_tower_bridging", "wipe_tower_extra_flow",
"wipe_tower_no_sparse_layers"}) "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"); 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; 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 && wipe_tower_wall_type == WipeTowerWallType::wtwCone); 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_extra_rib_length", have_rib_wall);
toggle_line("wipe_tower_rib_width", have_rib_wall); toggle_line("wipe_tower_rib_width", have_rib_wall);
toggle_line("wipe_tower_fillet_wall", have_rib_wall); toggle_line("wipe_tower_fillet_wall", have_rib_wall);
toggle_field("prime_tower_width", have_prime_tower && !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)); toggle_line("prime_volume",have_prime_tower && (!purge_in_primetower || !bSEMM));
+7 -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; 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++) { 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. // 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); PartPlate* part_plate = ppl.get_plate(plate_id);
+6
View File
@@ -291,6 +291,12 @@ void ArrangeJob::prepare_wipe_tower()
bool enable_prime_tower = op && op->getBool(); bool enable_prime_tower = op && op->getBool();
if (!enable_prime_tower || params.is_seq_print) return; 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; bool smooth_timelapse = false;
auto sop = current_config.option("timelapse_type"); auto sop = current_config.option("timelapse_type");
if (sop) { smooth_timelapse = sop->getInt() == TimelapseType::tlSmooth; } if (sop) { smooth_timelapse = sop->getInt() == TimelapseType::tlSmooth; }
+227
View File
@@ -5475,6 +5475,10 @@ struct Plater::priv
void exit_gizmo(); void exit_gizmo();
void remove(size_t obj_idx); void remove(size_t obj_idx);
bool delete_object_from_model(size_t obj_idx, bool refresh_immediately = true); //BBS 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. Returns true when the model was mutated
// (caller should refresh the scene).
bool ensure_belt_purge_tower();
void delete_all_objects_from_model(); void delete_all_objects_from_model();
void reset(bool apply_presets_change = false); void reset(bool apply_presets_change = false);
void center_selection(); void center_selection();
@@ -8879,6 +8883,225 @@ void Plater::priv::process_validation_warnings(const std::vector<StringObjectExc
} }
// ORCA-Belt: auto-managed purge prism for belt printers. 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 tilt lead-in/lead-out). Runs on every
// background-process update, so it must be idempotent: it only mutates the
// model when the desired prism differs from the existing one beyond coarse
// tolerances.
bool Plater::priv::ensure_belt_purge_tower()
{
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 = [this](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);
sidebar->obj_list()->delete_object_from_list(size_t(*it));
}
};
const auto &printer_config = wxGetApp().preset_bundle->printers.get_edited_preset().config;
const auto &print_config = wxGetApp().preset_bundle->prints.get_edited_preset().config;
auto &project_config = wxGetApp().preset_bundle->project_config;
const auto *belt_opt = printer_config.option<ConfigOptionBool>("belt_printer");
const bool belt = belt_opt != nullptr && belt_opt->value;
const bool prime_tower_enabled = print_config.has("enable_prime_tower") && print_config.opt_bool("enable_prime_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 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);
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) {
if (prism_idxs.empty())
return false;
remove_prisms(prism_idxs);
BOOST_LOG_TRIVIAL(info) << "[BELT-DEBUG] belt purge tower removed (conditions not met)";
return true;
}
// --- Sizing -----------------------------------------------------------
const double width = print_config.has("belt_purge_tower_width") ? std::max(1., 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 tilt: standard belt printers rotate about X. For other axes (or no
// rotation) fall back to vertical slicing geometry (theta = 90 deg); the
// backend leftover warning catches any resulting under-absorption.
double theta = M_PI / 2.;
const auto *axis_opt = printer_config.option<ConfigOptionEnum<BeltRotationAxis>>("belt_slice_rotation");
const auto *angle_opt = printer_config.option<ConfigOptionFloat>("belt_slice_rotation_angle");
if (axis_opt != nullptr && axis_opt->value == BeltRotationAxis::X && 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;
// 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()).
const bool use_matrix = (print_config.has("purge_in_prime_tower") && print_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;
// One tilted slicing plane cuts a width x (height/sin) rectangle out of
// the prism interior; one layer slab absorbs that area x layer height.
const double eta = 0.85; // perimeters/infill packing safety factor
double height = v_layer * sin_t / (width * layer_h * eta);
const double printable_height = printer_config.has("printable_height") ? printer_config.opt_float("printable_height") : 250.;
height = std::clamp(height, 2. * layer_h, std::max(2. * layer_h, printable_height));
// Length along the belt: cover the objects' Y extent; a slicing plane
// spans height*cot(theta) of belt travel, so pad both ends with the full
// lead-in (tilt sign is padded symmetrically) plus the objects' top
// overhang along the belt.
const double margin = 10.;
const double lead = height * cot_t;
double y_start = y_min - lead - margin;
double y_end = y_max + (z_max + height) * cot_t + margin;
const Vec3d plate_origin = plate->get_origin();
const auto *bed_opt = printer_config.option<ConfigOptionPoints>("printable_area");
const bool infinite_y = printer_config.has("belt_printer_infinite_y") && printer_config.opt_bool("belt_printer_infinite_y");
double lane_x = plate_origin.x() + 5.; // fallback left lane
if (bed_opt != nullptr && !bed_opt->values.empty()) {
const BoundingBoxf bed_ext = get_extents(bed_opt->values);
lane_x = plate_origin.x() + bed_ext.min.x() + 5.;
if (!infinite_y) {
y_start = std::max(y_start, plate_origin.y() + bed_ext.min.y() + 1.);
y_end = std::min(y_end, plate_origin.y() + bed_ext.max.y() - 1.);
}
}
const double length = std::max(y_end - y_start, 10.);
const Vec3d desired_size(width, length, height);
const Vec3d desired_center(lane_x + 0.5 * width, y_start + 0.5 * length, 0.5 * height);
// --- Idempotence check -------------------------------------------------
// Coarse tolerances so object nudges and sizing jitter do not regenerate
// the prism on every background-process tick.
if (prism_idxs.size() == 1) {
const ModelObject *prism = model.objects[prism_idxs.front()];
const BoundingBoxf3 bb = prism->bounding_box_exact();
if ((bb.size() - desired_size).cwiseAbs().maxCoeff() < 5. && (bb.center() - desired_center).cwiseAbs().maxCoeff() < 5.)
return false;
}
// --- (Re)create ---------------------------------------------------------
if (!prism_idxs.empty())
remove_prisms(prism_idxs);
ModelObject *new_object = model.add_object();
new_object->name = _u8L("Belt Purge Tower");
new_object->add_instance();
ModelVolume *new_volume = new_object->add_volume(make_cube(width, length, height));
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));
new_object->invalidate_bounding_box();
const BoundingBoxf3 mesh_bb = new_volume->mesh().bounding_box();
new_object->translate(-mesh_bb.center());
new_object->instances.front()->set_offset(desired_center);
new_object->ensure_on_bed();
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).
sidebar->obj_list()->add_object_to_list(obj_idx, /*call_selection_changed=*/false);
BOOST_LOG_TRIVIAL(info) << "[BELT-DEBUG] belt purge tower generated"
<< " W=" << width << " L=" << length << " H=" << height
<< " v_layer=" << v_layer << " max_flush=" << max_flush
<< " filaments=" << filaments.size()
<< " theta_deg=" << Geometry::rad2deg(theta)
<< " center=(" << desired_center.x() << "," << desired_center.y() << ")";
return true;
}
// Update background processing thread from the current config and Model. // Update background processing thread from the current config and Model.
// Returns a bitmask of UpdateBackgroundProcessReturnState. // Returns a bitmask of UpdateBackgroundProcessReturnState.
unsigned int Plater::priv::update_background_process(bool force_validation, bool postpone_error_messages, bool switch_print) unsigned int Plater::priv::update_background_process(bool force_validation, bool postpone_error_messages, bool switch_print)
@@ -8889,6 +9112,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, // 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. // so the update_restart_background_process() will not be called again in vain.
background_process_timer.Stop(); 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 the "out of print bed" state of ModelInstances.
update_print_volume_state(); update_print_volume_state();
// Apply new config to the possibly running background task. // Apply new config to the possibly running background task.
+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("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_enable_framework", "multimaterial_settings_prime_tower");
optgroup->append_single_option_line("prime_tower_width", "multimaterial_settings_prime_tower#width"); 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_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_brim_width", "multimaterial_settings_prime_tower#brim-width");
optgroup->append_single_option_line("prime_tower_infill_gap", "multimaterial_settings_prime_tower"); optgroup->append_single_option_line("prime_tower_infill_gap", "multimaterial_settings_prime_tower");