mirror of
https://github.com/OrcaSlicer/OrcaSlicer.git
synced 2026-09-27 19:01:02 +00:00
Purge tower part 1
This commit is contained in:
@@ -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;
|
||||
|
||||
|
||||
@@ -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",
|
||||
|
||||
+216
-7
@@ -22,6 +22,7 @@
|
||||
#include "MaterialType.hpp"
|
||||
#include "Model.hpp"
|
||||
#include "format.hpp"
|
||||
#include "LocalesUtils.hpp"
|
||||
#include <float.h>
|
||||
|
||||
#include <algorithm>
|
||||
@@ -389,6 +390,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"
|
||||
@@ -1473,6 +1475,15 @@ StringObjectException Print::validate(std::vector<StringObjectException> *warnin
|
||||
}
|
||||
|
||||
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) {
|
||||
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");
|
||||
@@ -1596,7 +1607,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 +1623,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 +2724,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 +4104,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 +4123,16 @@ bool Print::has_wipe_tower() const
|
||||
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
|
||||
{
|
||||
// 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;
|
||||
}
|
||||
|
||||
// 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()
|
||||
{
|
||||
m_wipe_tower_data.clear();
|
||||
|
||||
@@ -558,6 +558,10 @@ 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);
|
||||
//BBS
|
||||
ExPolygons _shrink_contour_holes(double contour_delta, double hole_delta, const ExPolygons& polys) const;
|
||||
// BBS
|
||||
@@ -1085,6 +1089,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 +1336,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
|
||||
|
||||
@@ -7321,6 +7321,20 @@ void PrintConfigDef::init_fff_params()
|
||||
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->label = L("Tree support branch angle");
|
||||
def->category = L("Support");
|
||||
@@ -7966,6 +7980,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.");
|
||||
|
||||
@@ -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,8 @@ 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))
|
||||
//BBS
|
||||
((ConfigOptionInts, additional_cooling_fan_speed))
|
||||
((ConfigOptionInts, close_additional_fan_first_x_layers))
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -1392,6 +1392,27 @@ void apply_fuzzy_skin_segmentation(PrintObject &print_object, ThrowOnCancel thro
|
||||
}); // 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,
|
||||
// 2) Initializes layers and their regions
|
||||
// 3) Slices the object meshes
|
||||
|
||||
@@ -971,11 +971,16 @@ 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 — 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"})
|
||||
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 +988,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));
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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; }
|
||||
|
||||
@@ -5475,6 +5475,10 @@ 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. Returns true when the model was mutated
|
||||
// (caller should refresh the scene).
|
||||
bool ensure_belt_purge_tower();
|
||||
void delete_all_objects_from_model();
|
||||
void reset(bool apply_presets_change = false);
|
||||
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.
|
||||
// Returns a bitmask of UpdateBackgroundProcessReturnState.
|
||||
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,
|
||||
// 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.
|
||||
|
||||
@@ -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");
|
||||
|
||||
Reference in New Issue
Block a user