mirror of
https://github.com/OrcaSlicer/OrcaSlicer.git
synced 2026-10-12 10:21:17 +00:00
Merge pull request #61 from HarrierPigeon/belt/purgeTower
Add Purge Tower and Finalize Multicolor Support
This commit is contained in:
@@ -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 < : 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
|
||||
@@ -86,6 +86,7 @@ set(lisbslic3r_sources
|
||||
BeltGCode.hpp
|
||||
BeltGCodeWriter.cpp
|
||||
BeltGCodeWriter.hpp
|
||||
BeltPurge.cpp
|
||||
BeltSliceStrategy.cpp
|
||||
BeltSliceStrategy.hpp
|
||||
BeltTransform.cpp
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
@@ -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();
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.");
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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,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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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));
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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; }
|
||||
|
||||
@@ -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
@@ -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",
|
||||
|
||||
@@ -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) }, {
|
||||
|
||||
Reference in New Issue
Block a user