Merge upstream/main into belt-printer

Brings belt-printer up to main 4b4a261787. Resolutions:

- G-code header (#15897, #15915): main moved the header, config and
  thumbnail block later in _do_export; write_belt_header() moves with it,
  still after the thumbnails and outside the BTT_TFT gate.
- _extrude: first-layer acceleration keeps the per-path first-layer plane
  test with main's cached nozzle index (#16028); main's set_speed out-param
  form (#16108) everywhere else.
- GCodeWriter (#16108): the arc-to-polyline fallback for machine mappings
  that cannot express G2/G3 now runs in the out-param extrude_arc_to_xy,
  which is the overload GCode calls, and appends to the caller's string.
- GCodeProcessorResult: the belt fields join main's forwarding assign.
- Clipper2 (#15969): belt arrange helpers take Slic3r::Point; the tree
  support join types lose their ClipperLib qualifier.
- CLI arrange (#15837): belt printers still reserve no wipe tower.
- Wipe tower options (#15841): the two new sparse-layer toggles are hidden
  for belt printers like the rest of the tower options.
- Keyboard shortcuts (#15706): main's registry replaces the old key switch;
  the belt view toggle is re-registered in the next commit.
- Print::process: the belt purge-plan undo runs before main's SliceStarted
  event.
- scripts/filament_id_snapshot.json: deleted on main (a77209af8f).
- Includes and appended tests: union of both sides.
This commit is contained in:
harrierpigeon
2026-10-04 19:20:33 -05:00
11471 changed files with 2992690 additions and 2720441 deletions
+106 -23
View File
@@ -1,4 +1,9 @@
#include "libslic3r/Exception.hpp"
#include "libslic3r/CustomGCode.hpp"
#include "libslic3r/Config.hpp"
#include "ExtrusionEntity.hpp"
#include "libslic3r/ObjectID.hpp"
#include "libslic3r/FilamentGroup.hpp"
#include "Print.hpp"
#include "ToolOrdering.hpp"
#include "Layer.hpp"
@@ -9,10 +14,23 @@
#include "MultiNozzleUtils.hpp"
#include "FilamentMixer.hpp"
#include "LocalesUtils.hpp"
#include "libslic3r/PrintConfig.hpp"
#include "Utils.hpp"
#include "format.hpp"
#include "I18N.hpp"
#include <boost/log/trivial.hpp>
#include <vector>
#include <string>
#include <utility>
#include <cmath>
#include <cstdlib>
#include <optional>
#include <functional>
#include <exception>
#include <memory>
#include <tuple>
#include <iostream>
// #define SLIC3R_DEBUG
@@ -82,8 +100,9 @@ bool check_filament_printable_after_group(const std::vector<unsigned int> &used_
int printable_status = print_config->filament_printable.get_at(filament_id);
int extruder_idx = filament_maps[filament_id];
if (!(printable_status >> extruder_idx & 1)) {
std::string extruder_name = extruder_idx == 0 ? _L("left") : _L("right");
std::string error_msg = _L("Grouping error: ") + filament_type + _L(" can not be placed in the ") + extruder_name + _L(" nozzle");
std::string error_msg = extruder_idx == 0 ?
Slic3r::format(_L("Grouping error: %1% cannot be placed in the left nozzle"), filament_type) :
Slic3r::format(_L("Grouping error: %1% cannot be placed in the right nozzle"), filament_type);
throw Slic3r::RuntimeError(error_msg);
}
}
@@ -1295,15 +1314,21 @@ void ToolOrdering::cal_most_used_extruder(const PrintConfig &config)
float ToolOrdering::cal_max_additional_fan(const PrintConfig &config)
{
// record
std::set<unsigned int> used_filaments;
for (const LayerTools &layer_tools : m_layer_tools)
used_filaments.insert(layer_tools.extruders.begin(), layer_tools.extruders.end());
if (used_filaments.empty())
return 0;
// Orca: additional_cooling_fan_speed can hold one value per extruder variant a filament prints with;
// filament_self_index maps such a column to its filament.
const std::vector<int> &self_index = config.filament_self_index.values;
const size_t columns = std::max(config.additional_cooling_fan_speed.size(), size_t(*used_filaments.rbegin()) + 1);
float max_fan = 0;
for (LayerTools &layer_tools : m_layer_tools) {
std::vector<unsigned int> filaments = layer_tools.extruders;
std::set<int> layer_extruder_count;
// count once only
for (unsigned int &filament : filaments)
if (max_fan < config.additional_cooling_fan_speed.get_at(filament))
max_fan = config.additional_cooling_fan_speed.get_at(filament);
for (size_t column = 0; column < columns; ++column) {
const unsigned int filament_id = self_index.size() == columns ? self_index[column] - 1 : column;
if (used_filaments.count(filament_id) && max_fan < config.additional_cooling_fan_speed.get_at(column))
max_fan = config.additional_cooling_fan_speed.get_at(column);
}
return max_fan;
}
@@ -1555,10 +1580,10 @@ static FilamentGroupContext build_filament_group_context(
auto machine_filament_info = build_machine_filaments(print->get_extruder_filament_info(), extruder_ams_counts, ignore_ext_filament);
std::vector<std::string> filament_types = print_config.filament_type.values;
std::vector<std::string> filament_colours = print_config.filament_colour.values;
std::vector<unsigned char> filament_is_support = print_config.filament_is_support.values;
std::vector<std::string> filament_ids = print_config.filament_ids.values;
// The grouping code walks filament_ids and indexes filament_info by the same position.
std::vector<std::string> filament_ids = print_config.filament_ids.values;
if (filament_ids.size() > filament_nums)
filament_ids.resize(filament_nums);
FGMode fg_mode = mode == FilamentMapMode::fmmAutoForMatch ? FGMode::MatchMode : FGMode::FlushMode;
context.model_info.flush_matrix = std::move(nozzle_flush_mtx);
@@ -1567,11 +1592,14 @@ static FilamentGroupContext build_filament_group_context(
context.model_info.filament_ids = filament_ids;
context.model_info.unprintable_volumes = unprintable_volumes;
for (size_t idx = 0; idx < filament_types.size(); ++idx) {
// Consumers index filament_info by filament id, so it must span the filament count: a partial
// or legacy config can leave any of these arrays short, and get_at clamps.
context.model_info.filament_info.reserve(filament_nums);
for (size_t idx = 0; idx < filament_nums; ++idx) {
FilamentGroupUtils::FilamentInfo info;
info.color = filament_colours[idx];
info.type = filament_types[idx];
info.is_support = filament_is_support[idx];
info.color = print_config.filament_colour.get_at(idx);
info.type = print_config.filament_type.get_at(idx);
info.is_support = print_config.filament_is_support.get_at(idx);
context.model_info.filament_info.emplace_back(std::move(info));
}
@@ -2799,6 +2827,28 @@ void ToolOrdering::enforce_mixed_component_order()
}
}
// Declared in ToolOrdering.hpp (exposed for unit testing).
std::vector<unsigned int> parse_cyclic_order(const std::string& str, unsigned int number_of_extruders)
{
std::vector<unsigned int> order;
for (const std::string& token : split_string(str, ',')) {
try {
size_t pos = 0;
int filament = std::stoi(token, &pos); // stoi skips leading whitespace by itself
// stoi stops at the first non-digit, so "2x" would parse as 2. Require the whole token to be
// consumed (bar trailing whitespace) to drop it like any other garbage.
if (token.find_first_not_of(" \t\r\n", pos) != std::string::npos)
continue;
if (filament >= 1 && (unsigned int)filament <= number_of_extruders
&& std::find(order.begin(), order.end(), (unsigned int)(filament - 1)) == order.end())
order.emplace_back((unsigned int)(filament - 1));
} catch (const std::exception&) {
// Not a number, ignore it.
}
}
return order;
}
void ToolOrdering::reorder_extruders_for_minimum_flush_volume(bool reorder_first_layer)
{
const PrintConfig* print_config = m_print_config_ptr;
@@ -2896,11 +2946,41 @@ void ToolOrdering::reorder_extruders_for_minimum_flush_volume(bool reorder_first
const bool use_cyclic_ordering =
(print_config->toolchange_ordering == ToolChangeOrderingType::Cyclic);
// By default the first layer keeps its adhesion-optimized order (and any custom first layer
// sequence); the cyclic sequence is only forced onto it when the user opts in.
const bool cyclic_first_layer = use_cyclic_ordering && print_config->toolchange_cyclic_first_layer.value;
// Optional user defined cyclic sequence, given as 1-based filament numbers ("3,2,1,4"). Filaments
// missing from it keep their ascending order after the listed ones, so a partial or bogus entry
// still yields the default cyclic order.
const std::vector<unsigned int> cyclic_order =
use_cyclic_ordering ? parse_cyclic_order(print_config->toolchange_cyclic_order.value, number_of_extruders)
: std::vector<unsigned int>();
// Reorder a layer's filaments (0-based) for cyclic ordering: ascending by default, or following the
// user defined sequence when one was given. Filaments absent from the sequence keep ascending order
// after the listed ones.
auto apply_cyclic_order = [&cyclic_order](std::vector<unsigned int>& filaments) {
std::sort(filaments.begin(), filaments.end());
if (!cyclic_order.empty())
std::stable_sort(filaments.begin(), filaments.end(), [&cyclic_order](unsigned int lhs, unsigned int rhs) {
auto rank = [&cyclic_order](unsigned int filament) {
return size_t(std::find(cyclic_order.begin(), cyclic_order.end(), filament) - cyclic_order.begin());
};
return rank(lhs) < rank(rhs);
});
};
// other_layers_seq: the layer_idx and extruder_idx are base on 1
auto get_custom_seq = [&other_layers_seqs, &reorder_first_layer, &first_layer_filaments, &layer_filaments, use_cyclic_ordering](int layer_idx, std::vector<int>& out_seq) -> bool {
auto get_custom_seq = [&other_layers_seqs, &reorder_first_layer, &first_layer_filaments, &layer_filaments, use_cyclic_ordering, cyclic_first_layer, &apply_cyclic_order](int layer_idx, std::vector<int>& out_seq) -> bool {
if (!reorder_first_layer && layer_idx == 0) {
out_seq.resize(first_layer_filaments.size());
std::transform(first_layer_filaments.begin(), first_layer_filaments.end(), out_seq.begin(), [](auto item) {return item + 1; });
// The first layer tool order is already decided (adhesion-optimized, plus any custom first
// layer sequence). Only override it with the cyclic sequence when the user opted in.
std::vector<unsigned int> ordered = first_layer_filaments;
if (cyclic_first_layer)
apply_cyclic_order(ordered);
out_seq.resize(ordered.size());
std::transform(ordered.begin(), ordered.end(), out_seq.begin(), [](auto item) {return int(item) + 1; });
return true;
}
for (size_t idx = other_layers_seqs.size() - 1; idx != size_t(-1); --idx) {
@@ -2911,9 +2991,12 @@ void ToolOrdering::reorder_extruders_for_minimum_flush_volume(bool reorder_first
}
}
if (use_cyclic_ordering && layer_idx >= 0 && size_t(layer_idx) < layer_filaments.size()) {
// Skip the first layer here (layer_idx == 0 only reaches this point on the reorder_first_layer
// path) unless the user asked for cyclic order on it, so it keeps the default flush ordering.
if (use_cyclic_ordering && layer_idx >= 0 && (layer_idx != 0 || cyclic_first_layer)
&& size_t(layer_idx) < layer_filaments.size()) {
std::vector<unsigned int> ordered = layer_filaments[size_t(layer_idx)];
std::sort(ordered.begin(), ordered.end());
apply_cyclic_order(ordered);
out_seq.resize(ordered.size());
std::transform(ordered.begin(), ordered.end(), out_seq.begin(), [](auto item) { return int(item) + 1; });
return true;