Merge upstream main: belt printing, texture displacement color mixing, 3MF component cycle checks, undo blocked during background jobs

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Clifford Garwood
2026-10-09 07:44:12 -04:00
co-authored by Claude Opus 5.5
227 changed files with 14506 additions and 1079 deletions
+314 -50
View File
@@ -71,6 +71,8 @@
#include "Thread.hpp"
#include "Time.hpp"
#include "GCode.hpp"
#include "BeltGCode.hpp"
#include "BeltTransform.hpp"
#include "GCode/WipeTower.hpp"
#include "GCode/WipeTower2.hpp"
#include "GCode/WipeTowerEstimate.hpp"
@@ -79,6 +81,7 @@
#include "MaterialType.hpp"
#include "Model.hpp"
#include "format.hpp"
#include "LocalesUtils.hpp"
#include <float.h>
#include <algorithm>
@@ -105,6 +108,7 @@
#include "Format/STEP.hpp"
#include "PlaceholderParser.hpp"
#include "SurfaceCollection.hpp"
#include "BeltBrim.hpp"
namespace fs = boost::filesystem;
@@ -168,6 +172,14 @@ bool Print::invalidate_state_by_config_options(const ConfigOptionResolver & /* n
// Cache the plenty of parameters, which influence the G-code generator only,
// or they are only notes not influencing the generated G-code.
static std::unordered_set<std::string> steps_gcode = {
// Belt printer G-code axis remap (only affects G-code output, not slicing).
"gcode_remap_x",
"gcode_remap_y",
"gcode_remap_z",
// Machine-frame transform (derived from belt tilt; only affects G-code output).
"belt_frame_tilt_decouple", "belt_frame_tilt_angle",
// Only inflates the GUI bed volume, like printable_area.
"belt_printer_infinite_y",
//BBS
"additional_cooling_fan_speed",
"reduce_crossing_wall",
@@ -369,8 +381,18 @@ bool Print::invalidate_state_by_config_options(const ConfigOptionResolver & /* n
// Spiral Vase forces different kind of slicing than the normal model:
// In Spiral Vase mode, holes are closed and only the largest area contour is kept at each layer.
// Therefore toggling the Spiral Vase on / off requires complete reslicing.
|| opt_key == "spiral_mode") {
|| opt_key == "spiral_mode"
// Build plate tilt changes slicing plane orientation.
|| opt_key == "build_plate_tilt_x"
|| opt_key == "build_plate_tilt_y"
// Belt printer transform options change the mesh geometry before slicing.
|| opt_key == "belt_printer"
|| opt_key == "belt_slice_rotation"
|| opt_key == "belt_slice_rotation_angle") {
osteps.emplace_back(posSlice);
} else if (
opt_key == "belt_support_floor_offset") {
osteps.emplace_back(posSupportMaterial);
} else if (
opt_key == "print_sequence"
|| opt_key == "filament_type"
@@ -405,6 +427,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"
@@ -439,6 +462,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"
@@ -676,6 +700,9 @@ std::vector<ObjectID> Print::print_object_ids() const
bool Print::has_infinite_skirt() const
{
// Belt printer: no skirt support.
if (m_config.belt_printer.value)
return false;
// Orca: unclear why (m_config.ooze_prevention && this->extruders().size() > 1) logic is here, removed.
// return (m_config.draft_shield == dsEnabled && m_config.skirt_loops > 0) || (m_config.ooze_prevention && this->extruders().size() > 1);
@@ -684,6 +711,9 @@ bool Print::has_infinite_skirt() const
bool Print::has_skirt() const
{
// Belt printer: no skirt support.
if (m_config.belt_printer.value)
return false;
return (m_config.skirt_height > 0);
}
@@ -692,6 +722,24 @@ bool Print::has_brim() const
return std::any_of(m_objects.begin(), m_objects.end(), [](PrintObject *object) { return object->has_brim(); });
}
bool Print::has_tilted_belt() const
{
if (! m_config.belt_printer.value)
return false;
// A Z rotation leaves the belt floor flat (BeltTransform forces shear = 0) and no
// rotation at all means the machine is geometrically a flat bed.
const BeltRotationAxis axis = m_config.belt_slice_rotation.value;
if (axis != BeltRotationAxis::X && axis != BeltRotationAxis::Y)
return false;
const double tilt = std::abs(m_config.belt_slice_rotation_angle.value);
return tilt >= BELT_BRIM_MIN_TILT_DEG && tilt <= BELT_BRIM_MAX_TILT_DEG;
}
bool Print::has_belt_brim() const
{
return std::any_of(m_objects.begin(), m_objects.end(), [](PrintObject *object) { return object->has_belt_brim(); });
}
//BBS
std::vector<size_t> Print::layers_sorted_for_object(float start, float end, std::vector<LayerPtrs> &layers_of_objects, std::vector<BoundingBox> &boundingBox_for_objects, VecOfPoints &objects_instances_shift)
{
@@ -1937,6 +1985,75 @@ StringObjectException Print::validate(std::vector<StringObjectException> *warnin
}
}
// Belt printer validation: incompatible features.
if (m_config.belt_printer.value) {
for (const PrintObject *object : m_objects) {
if (object->config().raft_layers > 0)
return { L("Raft is not compatible with belt printer mode.") };
}
if (m_config.draft_shield != dsDisabled)
return { L("Draft shield is not compatible with belt printer mode.") };
// Belt brim spans many layers and owns the layers below the object, which
// spiral vase cannot share. The prime tower setting is no obstacle: belt
// printers never print the classic tower, and the belt purge prism is an
// ordinary object that never takes a brim.
if (this->has_belt_brim()) {
if (m_config.spiral_mode.value)
return { L("Brim is not compatible with spiral vase mode on a belt printer. "
"Disable one of them.") };
}
for (const PrintObject *object : m_objects) {
const PrintObjectConfig &ocfg = object->config();
// Mirror PrintObject::has_belt_brim(): an inner-only brim needs a positive
// brim_width (leading/extra widen only the outer ring), so keep this
// predicate in step or the belt-brim warnings below would fire for a brim
// that has_belt_brim() rejects.
const bool wants_brim = ocfg.brim_type != btNoBrim
&& (ocfg.brim_type == btInnerOnly
? ocfg.brim_width.value > 0.
: (ocfg.brim_width.value > 0. || ocfg.leading_brim_length.value > 0.
|| ocfg.extra_brim_width.value > 0.));
if (! wants_brim)
continue;
if (! this->has_tilted_belt()) {
if (std::abs(m_config.belt_slice_rotation_angle.value) > BELT_BRIM_MAX_TILT_DEG)
warn(L("The belt is too steep for a brim, so no brim will be generated."),
"brim_width", object->model_object());
else
warn(L("A brim is only generated when the belt is tilted. Set a belt tilt angle, "
"or remove the brim setting."),
"brim_type", object->model_object());
}
if (ocfg.brim_type == btAutoBrim || ocfg.brim_type == btEar || ocfg.brim_type == btPainted)
warn(L("Belt printers support outer and inner brim only. Auto, Mouse ear and Painted "
"brim are printed as Outer brim only, using Brim width."),
"brim_type", object->model_object());
if (ocfg.leading_brim_length.value > 0. && ocfg.brim_object_gap.value > 0.)
warn(L("Brim-object gap separates the leading brim from the object's leading edge, "
"which is the edge it is meant to anchor. Set the gap to 0 when using leading "
"brim length."),
"brim_object_gap", object->model_object());
}
if (this->has_belt_brim() && m_objects.size() > 1)
warn(L("Leading brim length extends ahead of each object along the belt, and Arrange does "
"not reserve that space. Leave room between objects."),
"leading_brim_length");
} else {
// "Leading edge only" describes where a part meets a moving belt, so it has no
// meaning on a fixed bed. Brim.cpp prints it as an ordinary outer brim rather
// than silently producing nothing; say so.
for (const PrintObject *object : m_objects)
if (object->config().brim_type == btLeadingEdgeOnly)
warn(L("\"Leading edge only\" brim applies to belt printers. On this printer it is "
"printed as an ordinary outer brim."),
"brim_type", object->model_object());
}
// Orca: a gradient mixed filament only renders its gradient with "Mixed color sublayer" on;
// without it ToolOrdering::resolve_mixed_filaments prints one whole component per layer and
// the gradient is dropped silently. extruders() already covers painting, height ranges,
@@ -2002,6 +2119,40 @@ StringObjectException Print::validate(std::vector<StringObjectException> *warnin
}
}
if (m_config.belt_printer.value && m_config.enable_belt_purge_tower.value
&& m_config.print_sequence == PrintSequence::ByObject
&& extruders.size() > 1) {
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);
}
// The purge tower is a model object the GUI creates and sizes; libslic3r only purges
// into one that exists. A project sliced without it (the CLI on a project saved before
// the tower was generated) changes filament with nowhere to purge.
if (m_config.belt_printer.value && m_config.enable_belt_purge_tower.value
&& m_config.print_sequence != PrintSequence::ByObject
&& ! m_config.spiral_mode.value && this->object_extruders().size() > 1 && ! this->has_belt_purge_tower()) {
StringObjectException warningtemp;
warningtemp.string = L("The belt purge tower is enabled but the project has no purge tower object; "
"filament changes will not be purged. Open the project in the application "
"to generate the tower.");
warningtemp.opt_key = "enable_belt_purge_tower";
warningtemp.is_warning = true;
add_warning(warningtemp);
}
if (m_config.belt_printer.value && m_config.enable_belt_purge_tower.value) {
const size_t prism_count = std::count_if(m_objects.begin(), m_objects.end(), [](const PrintObject *object) {
return object->config().belt_purge_tower_object.value;
});
if (prism_count > 1)
return {L("The project contains multiple managed belt purge towers. Reload the plate or toggle the belt purge tower off and on to regenerate it.")};
}
if (m_config.enable_prime_tower) {
for (const PrintObject* object : m_objects) {
if (object->config().precise_z_height.value) {
@@ -2055,35 +2206,57 @@ StringObjectException Print::validate(std::vector<StringObjectException> *warnin
return profile;
};
// Checks that the print does not exceed the max print height
// Checks that the print does not exceed the max print height.
// For belt printers the slicing-frame Z spans the sheared X-length and
// is not comparable to printable_height (which is gantry clearance in the
// build-volume frame). Compare against the model's pre-shear Z instead,
// mirroring the bbox computed in PrintObject::update_slicing_parameters.
// The machine-frame transform only changes how that height is written to
// G-code, not how much room there is under the gantry.
const bool belt_printer = this->config().belt_printer.value;
const double shrinkage_compensation_z = this->shrinkage_compensation().z();
for (size_t print_object_idx = 0; print_object_idx < m_objects.size(); ++ print_object_idx) {
const PrintObject &print_object = *m_objects[print_object_idx];
//FIXME It is quite expensive to generate object layers just to get the print height!
if (auto layers = generate_object_layers(print_object.slicing_parameters(), layer_height_profile(print_object_idx), print_object.config().precise_z_height.value);
!layers.empty()) {
Vec3d test =this->shrinkage_compensation();
const double shrinkage_compensation_z = this->shrinkage_compensation().z();
if (shrinkage_compensation_z != 1. && layers.back() > (this->config().printable_height / shrinkage_compensation_z + EPSILON)) {
// The object exceeds the maximum build volume height because of shrinkage compensation.
return StringObjectException{
Slic3r::format(_u8L("While the object %1% itself fits the build volume, it exceeds the maximum build volume height because of material shrinkage compensation."), print_object.model_object()->name),
print_object.model_object(),
""
};
} else if (layers.back() > this->config().printable_height + EPSILON) {
// Test whether the last slicing plane is below or above the print volume.
return StringObjectException{
0.5 * (layers[layers.size() - 2] + layers.back()) > this->config().printable_height + EPSILON ?
Slic3r::format(_u8L("The object %1% exceeds the maximum build volume height."), print_object.model_object()->name) :
Slic3r::format(_u8L("While the object %1% itself fits the build volume, its last layer exceeds the maximum build volume height."), print_object.model_object()->name) +
" " + _u8L("You might want to reduce the size of your model or change current print settings and retry."),
print_object.model_object(),
""
};
double effective_max_z = 0;
bool last_layer_below_max = false;
bool have_height = false;
if (belt_printer) {
const double raw_z = print_object.model_object()->max_z();
effective_max_z = raw_z;
have_height = raw_z > 0;
} else {
//FIXME It is quite expensive to generate object layers just to get the print height!
auto layers = generate_object_layers(print_object.slicing_parameters(), layer_height_profile(print_object_idx), print_object.config().precise_z_height.value);
if (!layers.empty()) {
effective_max_z = layers.back();
last_layer_below_max = layers.size() >= 2 &&
0.5 * (layers[layers.size() - 2] + layers.back()) <= this->config().printable_height + EPSILON;
have_height = true;
}
}
if (!have_height)
continue;
if (shrinkage_compensation_z != 1. && effective_max_z > (this->config().printable_height / shrinkage_compensation_z + EPSILON)) {
// The object exceeds the maximum build volume height because of shrinkage compensation.
return StringObjectException{
Slic3r::format(_u8L("While the object %1% itself fits the build volume, it exceeds the maximum build volume height because of material shrinkage compensation."), print_object.model_object()->name),
print_object.model_object(),
""
};
} else if (effective_max_z > this->config().printable_height + EPSILON) {
return StringObjectException{
last_layer_below_max ?
Slic3r::format(_u8L("While the object %1% itself fits the build volume, its last layer exceeds the maximum build volume height."), print_object.model_object()->name) +
" " + _u8L("You might want to reduce the size of your model or change current print settings and retry.") :
Slic3r::format(_u8L("The object %1% exceeds the maximum build volume height."), print_object.model_object()->name),
print_object.model_object(),
""
};
}
}
// Some of the objects has variable layer height applied by painting or by a table.
@@ -2103,12 +2276,12 @@ 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()) {
// Orca: wipe_tower_filament (issue #10971) is inserted into the tool order after
// resolve_mixed_filaments has expanded every mixed (virtual) slot, so a mixed slot here
// would reach the G-code as a tool change to a slot no nozzle carries. The GUI hides
// mixed slots from the option; this guards loaded projects and the CLI.
if (m_config.wipe_tower_filament > 0) {
if (this->has_wipe_tower() && m_config.wipe_tower_filament > 0) {
const auto &is_mixed = m_config.filament_is_mixed.values;
const size_t wipe_idx = size_t(m_config.wipe_tower_filament - 1);
if (wipe_idx < is_mixed.size() && is_mixed[wipe_idx])
@@ -2130,12 +2303,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)
@@ -2862,8 +3040,28 @@ BoundingBox PrintObject::get_first_layer_bbox(float& a, float& layer_height, std
a += area(slice);
}
}
if (has_brim())
// Guard on `defined`: make_brim() can return before assigning this (it does on
// belt printers, where has_brim() is still true but the plate brim is skipped),
// and overwriting a valid bbox with an undefined one corrupted the first-layer
// centre and the GUI's first-layer area readout.
if (has_brim() && firstLayerObjectBrimBoundingBox.defined)
bbox = firstLayerObjectBrimBoundingBox;
// Belt brim: the apron reaches ahead of the object along the belt.
if (has_belt_brim()) {
const Point shift = instances().empty() ? Point(0, 0) : instances()[0].shift_without_plate_offset();
for (const ExPolygons &areas : m_belt_brim_areas_by_layer)
for (const ExPolygon &ex : areas) {
BoundingBox bb = get_extents(ex.contour);
bb.translate(shift.x(), shift.y());
bbox.merge(bb);
}
for (const BeltBrimBand &band : m_belt_brim_prologue)
for (const ExPolygon &ex : band.areas) {
BoundingBox bb = get_extents(ex.contour);
bb.translate(shift.x(), shift.y());
bbox.merge(bb);
}
}
return bbox;
}
@@ -2914,6 +3112,19 @@ void Print::process(long long *time_cost_with_cache, bool use_cache)
if (m_objects.empty())
return;
// Belt purge prism: _plan_belt_purge() (psWipeTower) truncates the prism's
// layers and drops its unclaimed fills, stashing both so a replan can undo
// them. The object steps below regenerate per-layer content over m_layers
// ONLY, so if any of them is about to rerun the stashes must go back first;
// otherwise truncated layers keep stale perimeters/fills and dropped fills
// are re-inserted next to freshly generated ones. Every object-step
// invalidation also invalidates psWipeTower, so "psWipeTower not done" is
// exactly "some object step may rerun" -- and when it IS done nothing below
// regenerates, and the plan's edits have to stay.
if (!this->is_step_done(psWipeTower))
for (PrintObject *obj : m_objects)
obj->belt_undo_purge_plan();
{
LifecycleEventContext ctx;
ctx.id = std::to_string(m_model.id().id);
@@ -2975,15 +3186,20 @@ void Print::process(long long *time_cost_with_cache, bool use_cache)
int object_count = m_objects.size();
std::set<PrintObject*> need_slicing_objects;
std::set<PrintObject*> re_slicing_objects;
// Belt global modes couple each object's bed position into its layer Z values,
// so sharing layers between "identical" objects is wrong.
bool belt_no_share = m_config.belt_printer.value;
if (!use_cache) {
for (int index = 0; index < object_count; index++)
{
PrintObject *obj = m_objects[index];
for (PrintObject *slicing_obj : need_slicing_objects)
{
if (is_print_object_the_same(obj, slicing_obj)) {
obj->set_shared_object(slicing_obj);
break;
if (!belt_no_share) {
for (PrintObject *slicing_obj : need_slicing_objects)
{
if (is_print_object_the_same(obj, slicing_obj)) {
obj->set_shared_object(slicing_obj);
break;
}
}
}
if (!obj->get_shared_object())
@@ -3002,12 +3218,14 @@ void Print::process(long long *time_cost_with_cache, bool use_cache)
PrintObject *obj = m_objects[index];
bool found_shared = false;
if (need_slicing_objects.find(obj) == need_slicing_objects.end()) {
for (PrintObject *slicing_obj : need_slicing_objects)
{
if (is_print_object_the_same(obj, slicing_obj)) {
obj->set_shared_object(slicing_obj);
found_shared = true;
break;
if (!belt_no_share) {
for (PrintObject *slicing_obj : need_slicing_objects)
{
if (is_print_object_the_same(obj, slicing_obj)) {
obj->set_shared_object(slicing_obj);
found_shared = true;
break;
}
}
}
if (!found_shared) {
@@ -3130,7 +3348,8 @@ void Print::process(long long *time_cost_with_cache, bool use_cache)
for (int i = range.begin(); i < range.end(); i++) {
PrintObject* obj = m_objects[i];
if (need_slicing_objects.count(obj) != 0) {
obj->generate_support_material();
// The belt brim follows sequentially below.
obj->generate_support_material(false);
}
else {
if (obj->set_started(posSupportMaterial))
@@ -3139,6 +3358,10 @@ void Print::process(long long *time_cost_with_cache, bool use_cache)
}
}
);
// The belt brim keeps clear of every object's layers and support layers,
// so it runs once no support step is rebuilding them any more.
for (PrintObject *obj : m_objects)
obj->generate_belt_brim();
if (m_pipeline_plugin_active)
for (size_t i = 0; i < m_objects.size(); ++i)
@@ -3215,7 +3438,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) {
@@ -3469,6 +3695,26 @@ void Print::process(long long *time_cost_with_cache, bool use_cache)
}
// Belt brim: bound the first-layer convex hull by the lowest apron band, so
// bed levelling and the initial purge line account for brim that reaches
// ahead of every object.
if (this->has_belt_brim()) {
for (PrintObject *object : m_objects) {
if (! object->has_belt_brim() || object->belt_brim_prologue().empty())
continue;
const BeltBrimBand &lowest = object->belt_brim_prologue().front();
for (const PrintInstance &instance : object->instances())
for (const ExPolygon &ex : lowest.areas) {
Polygon poly = ex.contour;
poly.translate(instance.shift);
append(m_first_layer_convex_hull.points, std::move(poly.points));
}
}
}
// Unchanged for belt printers: _make_skirt() already returns early for them, and
// the belt brim does not populate m_brimMapByInstance, which is what the
// skirt/brim grouping reads.
if (has_skirt() || has_infinite_skirt() || has_brim()) {
// Generate skirt/brim groups after brim so per-object and draft-shield footprints
// include brims when grouping and offsetting skirt loops.
@@ -3572,7 +3818,12 @@ std::string Print::export_gcode(const std::string& path_template, GCodeProcessor
this->set_status(80, message);
// The following line may die for multiple reasons.
GCode gcode;
// Factory: use BeltGCode for belt printers, plain GCode otherwise.
std::unique_ptr<GCode> gcode;
if (m_config.belt_printer.value)
gcode = std::make_unique<BeltGCode>();
else
gcode = std::make_unique<GCode>();
//BBS: compute plate offset for gcode-generator
const Vec3d origin = this->get_plate_origin();
// IMEX firmware-managed zones: writer offset gets the IMEX shift so emitted gcode is
@@ -3584,9 +3835,9 @@ std::string Print::export_gcode(const std::string& path_template, GCodeProcessor
// still emits in the frame the current config asks for.
this->update_imex_slice_offset();
const Vec2d imex_off = this->get_imex_slice_offset();
gcode.set_gcode_offset_with_imex_shift(origin(0), origin(1), imex_off.x(), imex_off.y());
gcode.do_export(this, path.c_str(), result, thumbnail_cb);
gcode.export_layer_filaments(result);
gcode->set_gcode_offset_with_imex_shift(origin(0), origin(1), imex_off.x(), imex_off.y());
gcode->do_export(this, path.c_str(), result, thumbnail_cb);
gcode->export_layer_filaments(result);
//BBS
if (result != nullptr) {
result->conflict_result = m_conflict_result;
@@ -3601,6 +3852,10 @@ std::string Print::export_gcode(const std::string& path_template, GCodeProcessor
void Print::_make_skirt()
{
// Belt printer: skirt is not compatible.
if (m_config.belt_printer.value)
return;
const bool generate_skirt = this->has_skirt() || this->has_infinite_skirt();
// First off we need to decide how tall the skirt must be.
@@ -4695,6 +4950,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;
@@ -4707,6 +4969,7 @@ bool Print::has_wipe_tower() const
return false;
}
const WipeTowerData &Print::wipe_tower_data(size_t filaments_cnt) const
{
// Until the tower is generated, size it with the estimate the GUI/CLI placement uses, so
@@ -4736,6 +4999,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();