Port mixed filament engine fixes from BambuStudio

This commit is contained in:
SoftFever
2026-08-23 22:11:49 +08:00
parent 9d733e50f9
commit fcdfcae427
14 changed files with 552 additions and 31 deletions
+275
View File
@@ -6,6 +6,7 @@
#include <cmath>
#include <cstdio>
#include <limits>
#include <set>
#include <sstream>
#include <numeric>
@@ -551,4 +552,278 @@ void expand_mixed_slots_in_unprintables(
}
}
void sanitize_mixed_gradient_curve_array(std::vector<std::string>& vals)
{
for (size_t i = 0; i < vals.size(); ++i) {
if (vals[i].empty())
continue;
// parse_gradient_curve returns empty for both "empty input" and "<2 valid points";
// we already skipped empty, so an empty result means a corrupted single-point slot.
if (parse_gradient_curve(vals[i]).empty()) {
BOOST_LOG_TRIVIAL(warning) << "sanitize_mixed_gradient_curve_array: slot "
<< i << " curve \"" << vals[i]
<< "\" has fewer than 2 valid points; clearing to linear";
vals[i].clear();
}
}
}
bool try_parse_mixed_components_strict(const std::string &str,
std::vector<unsigned int> &components,
std::string &err)
{
components.clear();
if (str.empty()) {
err = "empty component list";
return false;
}
std::istringstream ss(str);
std::string token;
while (std::getline(ss, token, ',')) {
if (token.empty()) {
err = "empty component index";
return false;
}
try {
const long val = std::stol(token);
if (val < 1) {
err = "component index must be >= 1 (got " + token + ")";
return false;
}
components.push_back(static_cast<unsigned int>(val));
} catch (...) {
err = "invalid component index \"" + token + "\"";
return false;
}
}
if (components.size() < 2) {
err = "at least 2 components required (got " + std::to_string(components.size()) + ")";
return false;
}
std::set<unsigned int> seen;
for (unsigned int c : components) {
if (!seen.insert(c).second) {
err = "duplicate component index " + std::to_string(c);
return false;
}
}
return true;
}
bool try_parse_mixed_ratios_strict(const std::string &str,
size_t n_components,
std::string &err)
{
if (str.empty())
return true;
CNumericLocalesSetter c_locale_setter;
std::vector<double> ratios;
std::istringstream ss(str);
std::string token;
while (std::getline(ss, token, ',')) {
if (token.empty()) {
err = "empty ratio value";
return false;
}
try {
const double val = std::stod(token);
if (!(val > 0.0)) {
err = "ratio must be positive (got " + token + ")";
return false;
}
ratios.push_back(val);
} catch (...) {
err = "invalid ratio \"" + token + "\"";
return false;
}
}
if (ratios.size() != n_components) {
err = "expected " + std::to_string(n_components) + " ratio(s), got "
+ std::to_string(ratios.size());
return false;
}
return true;
}
bool validate_gradient_range_strict(const std::string &str, std::string &err)
{
if (str.empty())
return true;
CNumericLocalesSetter c_locale_setter;
float v0 = 0.f, v1 = 0.f;
if (std::sscanf(str.c_str(), "%f,%f", &v0, &v1) != 2) {
err = "expected two comma-separated floats, e.g. \"0.10,0.90\"";
return false;
}
if (!(v0 > 0.f && v0 < 1.f && v1 > 0.f && v1 < 1.f)) {
err = "start and end ratios must be in (0, 1)";
return false;
}
return true;
}
static void append_error(std::map<std::string, std::string> &errors,
const std::string &key,
const std::string &msg)
{
auto it = errors.find(key);
if (it == errors.end())
errors.emplace(key, msg);
else
it->second += "; " + msg;
}
static bool has_mixed_sub_params_specified(
const std::vector<std::string> &comp_strs,
const std::vector<std::string> &ratio_strs,
const std::vector<unsigned char> &gradient_flags)
{
for (const std::string &s : comp_strs)
if (!s.empty()) return true;
for (const std::string &s : ratio_strs)
if (!s.empty()) return true;
for (unsigned char g : gradient_flags)
if (g) return true;
return false;
}
static bool mixed_string_array_was_specified(const std::vector<std::string> &vals)
{
for (const std::string &s : vals)
if (!s.empty())
return true;
return false;
}
static bool mixed_bool_array_was_specified(const std::vector<unsigned char> &vals)
{
for (unsigned char v : vals)
if (v)
return true;
return false;
}
static void check_mixed_array_size_required(std::map<std::string, std::string> &errors,
const std::string &opt_key,
size_t actual_size,
size_t expected_size)
{
if (actual_size != expected_size) {
append_error(errors, opt_key,
"array size " + std::to_string(actual_size)
+ " does not match filament slot count " + std::to_string(expected_size));
}
}
std::map<std::string, std::string> validate_mixed_filament_params(
const std::vector<unsigned char> &is_mixed,
const std::vector<std::string> &comp_strs,
const std::vector<std::string> &ratio_strs,
const std::vector<unsigned char> &gradient_flags,
const std::vector<std::string> &gradient_range_strs,
const std::vector<std::string> &gradient_curve_strs)
{
std::map<std::string, std::string> errors;
if (has_mixed_sub_params_specified(comp_strs, ratio_strs, gradient_flags)
&& !has_any_mixed_filament(is_mixed)) {
append_error(errors, "filament_is_mixed",
"must be set when mixed filament parameters are specified");
return errors;
}
if (!has_any_mixed_filament(is_mixed))
return errors;
const size_t slot_count = is_mixed.size();
// Rule 1: mixed filament model → components & ratios arrays must cover every slot.
check_mixed_array_size_required(errors, "filament_mixed_components", comp_strs.size(), slot_count);
check_mixed_array_size_required(errors, "filament_mixed_sublayer_ratios", ratio_strs.size(), slot_count);
// Rule 2: gradient passed (any slot true) → gradient & range arrays must cover every slot.
const bool gradient_specified = mixed_bool_array_was_specified(gradient_flags);
if (gradient_specified) {
check_mixed_array_size_required(errors, "filament_mixed_gradient", gradient_flags.size(), slot_count);
check_mixed_array_size_required(errors, "filament_mixed_gradient_range", gradient_range_strs.size(), slot_count);
}
// Rule 3: curve passed (any non-empty entry) → curve array must cover every slot.
const bool curve_specified = mixed_string_array_was_specified(gradient_curve_strs);
if (curve_specified)
check_mixed_array_size_required(errors, "filament_mixed_gradient_curve", gradient_curve_strs.size(), slot_count);
size_t num_physical = 0;
for (unsigned char v : is_mixed)
if (!v) ++num_physical;
for (size_t i = 0; i < is_mixed.size(); ++i) {
if (!is_mixed[i])
continue;
const std::string slot = "slot " + std::to_string(i + 1);
const std::string comp_str = i < comp_strs.size() ? comp_strs[i] : "";
std::vector<unsigned int> components;
std::string comp_err;
if (!try_parse_mixed_components_strict(comp_str, components, comp_err)) {
append_error(errors, "filament_mixed_components", slot + ": " + comp_err);
continue;
}
for (unsigned int c : components) {
if (c > num_physical) {
append_error(errors, "filament_mixed_components",
slot + ": component " + std::to_string(c)
+ " out of range (max physical filament index is "
+ std::to_string(num_physical) + ")");
break;
}
if (c == i + 1) {
append_error(errors, "filament_mixed_components",
slot + ": cannot reference itself as a component");
break;
}
const size_t idx0 = static_cast<size_t>(c - 1);
if (idx0 < is_mixed.size() && is_mixed[idx0]) {
append_error(errors, "filament_mixed_components",
slot + ": component " + std::to_string(c)
+ " references a mixed filament slot");
break;
}
}
std::string ratio_err;
const std::string ratio_str = i < ratio_strs.size() ? ratio_strs[i] : "";
if (!try_parse_mixed_ratios_strict(ratio_str, components.size(), ratio_err))
append_error(errors, "filament_mixed_sublayer_ratios", slot + ": " + ratio_err);
const bool gradient_on = i < gradient_flags.size() && gradient_flags[i];
if (gradient_on) {
if (components.size() != 2) {
append_error(errors, "filament_mixed_gradient",
slot + ": gradient requires exactly 2 components");
}
if (gradient_specified) {
std::string range_err;
const std::string range_str = i < gradient_range_strs.size() ? gradient_range_strs[i] : "";
if (!validate_gradient_range_strict(range_str, range_err))
append_error(errors, "filament_mixed_gradient_range", slot + ": " + range_err);
}
if (curve_specified) {
const std::string curve_str = i < gradient_curve_strs.size() ? gradient_curve_strs[i] : "";
if (!curve_str.empty() && parse_gradient_curve(curve_str).empty())
append_error(errors, "filament_mixed_gradient_curve",
slot + ": invalid curve (need at least 2 valid control points)");
}
}
}
return errors;
}
} // namespace Slic3r
+20
View File
@@ -2,6 +2,7 @@
#define SLIC3R_FILAMENT_MIXER_HPP
#include <limits>
#include <map>
#include <set>
#include <string>
#include <utility>
@@ -139,6 +140,25 @@ void expand_mixed_slots_in_unprintables(
const std::vector<unsigned char> &is_mixed,
const std::vector<std::string> &comp_strs);
// Clear any non-empty gradient-curve slot that parses to fewer than 2 control points.
// Heals per-slot arrays corrupted by the legacy "|" separator collision between
// PresetBundle::export_selections / load_selections (which used "|" as the inter-slot
// delimiter) and serialize_gradient_curve / parse_gradient_curve (which use "|" as the
// intra-slot control-point delimiter). Such a round-trip splits a multi-point curve
// across adjacent slots, leaving single-point entries that fail MakerWorld's strict
// "curve needs >= 2 points" check. Clearing them falls back to the linear range.
void sanitize_mixed_gradient_curve_array(std::vector<std::string>& vals);
// Validate mixed-color (混色) parameters. Returns error messages keyed by option name.
// Slot details are included in the message text (1-based slot index).
std::map<std::string, std::string> validate_mixed_filament_params(
const std::vector<unsigned char> &is_mixed,
const std::vector<std::string> &comp_strs,
const std::vector<std::string> &ratio_strs,
const std::vector<unsigned char> &gradient_flags,
const std::vector<std::string> &gradient_range_strs,
const std::vector<std::string> &gradient_curve_strs);
} // namespace Slic3r
#endif // SLIC3R_FILAMENT_MIXER_HPP
+42
View File
@@ -4,6 +4,7 @@
#include "../Preset.hpp"
#include "../Utils.hpp"
#include "../LocalesUtils.hpp"
#include "../FilamentMixer.hpp"
#include "../GCode.hpp"
#include "../Geometry.hpp"
#include "../GCode/ThumbnailData.hpp"
@@ -246,6 +247,8 @@ static constexpr const char* BUILD_TAG = "build";
static constexpr const char* ITEM_TAG = "item";
static constexpr const char* METADATA_TAG = "metadata";
static constexpr const char* FILAMENT_TAG = "filament";
static constexpr const char* MIXED_FILAMENT_TAG = "mixed_filament";
static constexpr const char* MIXED_FILAMENT_COMPONENTS_TAG = "components";
static constexpr const char* SLICE_WARNING_TAG = "warning";
static constexpr const char* WARNING_MSG_TAG = "msg";
static constexpr const char *FILAMENT_ID_TAG = "id";
@@ -1315,6 +1318,7 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result)
bool _handle_end_config_metadata();
bool _handle_start_config_filament(const char** attributes, unsigned int num_attributes);
bool _handle_start_config_mixed_filament(const char** attributes, unsigned int num_attributes);
bool _handle_end_config_filament();
bool _handle_start_config_warning(const char** attributes, unsigned int num_attributes);
@@ -2694,6 +2698,14 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result)
return;
}
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(", load project config file successfully from %1%\n") %dest_file;
// Heal any gradient-curve slots corrupted by the legacy "|" separator collision
// (see FilamentMixer::sanitize_mixed_gradient_curve_array). The 3MF JSON itself
// is safe (";" + C-style escape), but older projects saved through the buggy
// export_selections/load_selections path may already carry single-point entries
// that fail MakerWorld's "curve needs >= 2 points" check.
if (auto* curve_opt = config.option<ConfigOptionStrings>("filament_mixed_gradient_curve"))
Slic3r::sanitize_mixed_gradient_curve_array(curve_opt->values);
}
}
@@ -3511,6 +3523,8 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result)
res = _handle_start_config_plater_instance(attributes, num_attributes);
else if (::strcmp(FILAMENT_TAG, name) == 0)
res = _handle_start_config_filament(attributes, num_attributes);
else if (::strcmp(MIXED_FILAMENT_TAG, name) == 0)
res = _handle_start_config_mixed_filament(attributes, num_attributes);
else if (::strcmp(SLICE_WARNING_TAG, name) == 0)
res = _handle_start_config_warning(attributes, num_attributes);
else if (::strcmp(NOZZLE_TAG, name) == 0)
@@ -4684,6 +4698,23 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result)
return true;
}
bool _BBS_3MF_Importer::_handle_start_config_mixed_filament(const char** attributes, unsigned int num_attributes)
{
if (m_curr_plater) {
std::string id = bbs_get_attribute_value_string(attributes, num_attributes, FILAMENT_ID_TAG);
std::string type = bbs_get_attribute_value_string(attributes, num_attributes, FILAMENT_TYPE_TAG);
std::string color = bbs_get_attribute_value_string(attributes, num_attributes, FILAMENT_COLOR_TAG);
std::string components = bbs_get_attribute_value_string(attributes, num_attributes, MIXED_FILAMENT_COMPONENTS_TAG);
PlateMixedFilamentInfo mixed_info;
mixed_info.id = atoi(id.c_str());
mixed_info.type = type;
mixed_info.color = color;
mixed_info.components = components;
m_curr_plater->mixed_filaments_info.push_back(mixed_info);
}
return true;
}
bool _BBS_3MF_Importer::_handle_end_config_filament()
{
// do nothing
@@ -8488,6 +8519,17 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result)
<< FILAMENT_USED_FOR_SUPPORT << "=\"" << std::boolalpha << it->used_for_support << "\"/>\n";
}
// Mixed (virtual) filaments used by this plate. These are resolved to physical
// components before g-code statistics, so they are not present in the <filament>
// list above and are recorded separately here.
for (auto it = plate_data->mixed_filaments_info.begin(); it != plate_data->mixed_filaments_info.end(); it++)
{
stream << " <" << MIXED_FILAMENT_TAG << " " << FILAMENT_ID_TAG << "=\"" << std::to_string(it->id) << "\" "
<< FILAMENT_TYPE_TAG << "=\"" << it->type << "\" "
<< FILAMENT_COLOR_TAG << "=\"" << it->color << "\" "
<< MIXED_FILAMENT_COMPONENTS_TAG << "=\"" << it->components << "\"/>\n";
}
for (auto it = plate_data->warnings.begin(); it != plate_data->warnings.end(); it++) {
stream << " <" << SLICE_WARNING_TAG << " msg=\"" << it->msg << "\" level=\"" << std::to_string(it->level) << "\" error_code =\"" << it->error_code << "\" />\n";
}
+14
View File
@@ -48,6 +48,18 @@ public:
};
// Mixed (virtual) filament used by a plate. Mixed filaments are virtual slots that get
// resolved to their physical components before g-code statistics, so they never appear in
// slice_filaments_info. They are recorded here separately so a plate's mixed-color usage
// can be recovered from slice_info.
struct PlateMixedFilamentInfo
{
int id{0}; // 1-based virtual filament slot id
std::string type;
std::string color; // blended display color, "#RRGGBB"
std::string components; // 1-based physical component ids, comma separated, e.g. "1,3"
};
//BBS: define plate data list related structures
struct PlateData
{
@@ -89,6 +101,8 @@ struct PlateData
std::string first_layer_time;
std::string plate_name;
std::vector<FilamentInfo> slice_filaments_info;
// Mixed (virtual) filaments used by this plate; empty when no mixed filament is used.
std::vector<PlateMixedFilamentInfo> mixed_filaments_info;
std::vector<size_t> skipped_objects;
DynamicPrintConfig config;
bool is_support_used {false};
+4 -2
View File
@@ -4195,6 +4195,8 @@ void GCode::export_layer_filaments(GCodeProcessorResult* result)
}
}
result->used_mixed_filaments = m_print->get_slice_used_mixed_filaments();
result->optimal_assignment.clear();
result->optimal_assignment.reserve(filament_map.size());
for (int nozzle_id : filament_map)
@@ -6859,7 +6861,7 @@ LayerResult GCode::process_layer(
if (instance_to_print.object_by_extruder.support && !instance_to_print.object_by_extruder.support->empty()) {
if (use_per_volume) {
m_nominal_z = obj_sub_z;
gcode += m_writer.travel_to_z(obj_sub_z, "restore Z for support");
m_need_change_layer_lift_z = true;
}
ExtrusionRole support_role = instance_to_print.object_by_extruder.support_extrusion_role;
gcode += this->extrude_support(*instance_to_print.object_by_extruder.support, support_role);
@@ -6897,7 +6899,7 @@ LayerResult GCode::process_layer(
if (!layer_tools.mixed_sub_layer_groups.empty()) {
m_writer.add_object_end_labels(gcode);
m_nominal_z = print_z;
gcode += m_writer.travel_to_z(print_z, "restore Z after sublayers");
m_need_change_layer_lift_z = true;
}
}
+1
View File
@@ -2543,6 +2543,7 @@ void GCodeProcessorResult::reset() {
spiral_vase_mode = false;
layer_filaments.clear();
filament_change_sequence.clear();
used_mixed_filaments.clear();
nozzle_change_sequence.clear();
optimal_assignment.clear();
filament_change_count_map.clear();
+4
View File
@@ -306,6 +306,9 @@ class Print;
std::unordered_map<std::vector<unsigned int>, std::vector<std::pair<int, int>>,FilamentSequenceHash> layer_filaments;
std::vector<unsigned int> nozzle_change_sequence;
std::vector<unsigned int> filament_change_sequence;
// 0-based mixed (virtual) filament slots actually used on this plate.
// Recorded before resolve_mixed_filaments expands them to physical components.
std::vector<unsigned int> used_mixed_filaments;
std::vector<int> optimal_assignment;
// first key stores `from` filament, second keys stores the `to` filament
std::map<std::pair<int,int>, int > filament_change_count_map;
@@ -357,6 +360,7 @@ class Print;
printer_extruder_id = other.printer_extruder_id;
layer_filaments = other.layer_filaments;
filament_change_sequence = other.filament_change_sequence;
used_mixed_filaments = other.used_mixed_filaments;
nozzle_change_sequence = other.nozzle_change_sequence;
optimal_assignment = other.optimal_assignment;
filament_change_count_map = other.filament_change_count_map;
+91 -1
View File
@@ -1015,7 +1015,7 @@ void ToolOrdering::fill_wipe_tower_partitions(const PrintConfig &config, coordf_
//FIXME this is a hack to get the ball rolling.
for (LayerTools &lt : m_layer_tools)
lt.has_wipe_tower |= (lt.has_object && (config.timelapse_type == TimelapseType::tlSmooth || lt.wipe_tower_partitions > 0))
lt.has_wipe_tower |= ((lt.has_object || lt.has_support) && (config.timelapse_type == TimelapseType::tlSmooth || lt.wipe_tower_partitions > 0))
|| lt.print_z < object_bottom_z + EPSILON;
// Test for a raft, insert additional wipe tower layer to fill in the raft separation gap.
@@ -1056,6 +1056,84 @@ void ToolOrdering::fill_wipe_tower_partitions(const PrintConfig &config, coordf_
}
}
// Ensure wipe tower vertical continuity:
//
// (1) Any existing LayerTools sandwiched between two has_wipe_tower layers must itself be a
// wipe-tower layer. The LayerTools entry already exists, but it has neither object nor
// support geometry (has_object == false && has_support == false), so the marking pass
// above leaves has_wipe_tower == false. Happens e.g. when one object is fully floating
// above another and the support_top_z_distance / support_bottom_z_distance gap leaves an
// interior layer with no object and no support (e.g. B top z=20.4, A first layer z=20.8,
// the z=20.6 LayerTools entry exists but stays unmarked).
//
// (2) When two adjacent has_wipe_tower layers are farther apart than max_layer_height and no
// LayerTools entry exists between them, insert virtual wipe-tower-only layers to bridge
// the gap. Happens with raft: BambuStudio's raft contact layer can be thicker than
// max_layer_height (e.g. raft base top z=0.2, raft contact top z=0.5 — gap 0.3 > 0.28),
// and there is no LayerTools entry between those two z values.
//
// wipe_tower_partitions has already been max-propagated downward above, so partition counts
// on the filled-in / inserted layers stay consistent.
{
int first_wt_idx = -1;
int last_wt_idx = -1;
for (int i = 0; i < (int)m_layer_tools.size(); ++i)
if (m_layer_tools[i].has_wipe_tower) {
if (first_wt_idx < 0) first_wt_idx = i;
last_wt_idx = i;
}
for (int i = first_wt_idx + 1; i < last_wt_idx; ++i) {
LayerTools &lt = m_layer_tools[i];
lt.has_wipe_tower = true;
// GCode::process_layer emits wipe-tower G-code inside `for (extruder_id : layer_tools.extruders)`.
// An empty extruders vector here would silently skip wipe tower output, leaving the tower
// physically floating. Seed from the nearest non-empty neighbor so the loop actually runs.
if (lt.extruders.empty()) {
unsigned int seed_extruder = 0;
bool found_seed = false;
for (int j = i - 1; j >= 0; --j)
if (!m_layer_tools[j].extruders.empty()) {
seed_extruder = m_layer_tools[j].extruders.back();
found_seed = true;
break;
}
if (!found_seed)
for (int j = i + 1; j < (int)m_layer_tools.size(); ++j)
if (!m_layer_tools[j].extruders.empty()) {
seed_extruder = m_layer_tools[j].extruders.front();
found_seed = true;
break;
}
if (found_seed)
lt.extruders.push_back(seed_extruder);
}
}
// Walk adjacent has_wipe_tower pairs and split oversized gaps. Re-evaluate the same i
// after each insertion so very large gaps get split into multiple layers.
for (int i = 0; i + 1 < (int)m_layer_tools.size(); ) {
LayerTools &lt = m_layer_tools[i];
LayerTools &lt_next = m_layer_tools[i + 1];
if (!lt.has_wipe_tower || !lt_next.has_wipe_tower) {
++i;
continue;
}
coordf_t gap = lt_next.print_z - lt.print_z;
if (gap <= max_layer_height + EPSILON) {
++i;
continue;
}
LayerTools lt_new(0.5 * (lt.print_z + lt_next.print_z));
lt_new.has_wipe_tower = true;
if (!lt_next.extruders.empty())
lt_new.extruders.push_back(lt_next.extruders.front());
else if (!lt.extruders.empty())
lt_new.extruders.push_back(lt.extruders.back());
lt_new.wipe_tower_partitions = lt_next.wipe_tower_partitions;
m_layer_tools.insert(m_layer_tools.begin() + i + 1, lt_new);
}
}
// If the model contains empty layers (such as https://github.com/prusa3d/Slic3r/issues/1266), there might be layers
// that were not marked as has_wipe_tower, even when they should have been. This produces a crash with soluble supports
// and maybe other problems. We will therefore go through layer_tools and detect and fix this.
@@ -2081,6 +2159,18 @@ void ToolOrdering::resolve_mixed_filaments(const PrintConfig &config)
const auto &comp_strs = config.filament_mixed_components.values;
const auto &ratio_strs = config.filament_mixed_sublayer_ratios.values;
// Capture mixed slots that actually appear on layers before they are expanded to
// physical components. Assigned-but-unused mixed slots never enter layer_tools.
m_used_mixed_filaments.clear();
if (has_any_mixed_filament(is_mixed)) {
std::set<unsigned int> used;
for (const LayerTools &lt : m_layer_tools)
for (unsigned int ext : lt.extruders)
if (ext < is_mixed.size() && is_mixed[ext])
used.insert(ext);
m_used_mixed_filaments.assign(used.begin(), used.end());
}
if (!has_any_mixed_filament(is_mixed))
return;
+4
View File
@@ -290,6 +290,9 @@ public:
// For a multi-material print, the printing extruders are ordered in the order they shall be primed.
const std::vector<unsigned int>& all_extruders() const { return m_all_printing_extruders; }
// 0-based mixed (virtual) slots that appeared on layers before resolve_mixed_filaments
// expanded them to physical components.
const std::vector<unsigned int>& used_mixed_filaments() const { return m_used_mixed_filaments; }
// Find LayerTools with the closest print_z.
const LayerTools& tools_for_layer(coordf_t print_z) const;
@@ -376,6 +379,7 @@ private:
unsigned int m_last_printing_extruder = (unsigned int)-1;
// All extruders, which extrude some material over m_layer_tools.
std::vector<unsigned int> m_all_printing_extruders;
std::vector<unsigned int> m_used_mixed_filaments;
const DynamicPrintConfig* m_print_full_config = nullptr;
const PrintConfig* m_print_config_ptr = nullptr;
+3
View File
@@ -2760,6 +2760,9 @@ static void load_mixed_filament_settings(DynamicPrintConfig &project_config, con
vals = std::move(curves);
}
vals.resize(n_filaments, std::string{});
// Heal legacy corruption: clear any non-empty slot that ended up with < 2 points
// (e.g. a curve split across slots by the old "|" delimiter). Falls back to linear.
Slic3r::sanitize_mixed_gradient_curve_array(vals);
}
}
+39 -28
View File
@@ -1388,6 +1388,13 @@ StringObjectException Print::validate(std::vector<StringObjectException> *warnin
// #4043
if (total_copies_count > 1 && m_config.print_sequence != PrintSequence::ByObject)
return {L("Please select \"By object\" print sequence to print multiple objects in spiral vase mode."), nullptr, "spiral_mode"};
// A mixed (virtual) filament always resolves to multiple physical components, which
// spiral vase cannot print.
const auto &is_mixed = m_config.filament_is_mixed.values;
for (const PrintObject *object : m_objects)
for (unsigned int ext : object->object_extruders())
if (ext < is_mixed.size() && is_mixed[ext])
return {L("Spiral (vase) mode does not work when an object contains more than one material."), nullptr, "spiral_mode"};
assert(m_objects.size() == 1);
const auto all_regions = m_objects.front()->all_regions();
if (all_regions.size() > 1) {
@@ -2595,6 +2602,7 @@ void Print::process(long long *time_cost_with_cache, bool use_cache)
// Order object instances for sequential print.
print_object_instances_ordering = sort_object_instances_by_model_order(*this);
std::vector<unsigned int> first_layer_used_filaments;
std::vector<unsigned int> used_mixed_filaments;
std::vector<std::vector<unsigned int>> all_filaments;
for (print_object_instance_sequential_active = print_object_instances_ordering.begin(); print_object_instance_sequential_active != print_object_instances_ordering.end(); ++print_object_instance_sequential_active) {
tool_ordering = ToolOrdering(*(*print_object_instance_sequential_active)->print_object, initial_extruder_id);
@@ -2604,10 +2612,14 @@ void Print::process(long long *time_cost_with_cache, bool use_cache)
if (idx == 0)
first_layer_used_filaments.insert(first_layer_used_filaments.end(), layer_filament.begin(), layer_filament.end());
}
used_mixed_filaments.insert(used_mixed_filaments.end(),
tool_ordering.used_mixed_filaments().begin(), tool_ordering.used_mixed_filaments().end());
}
sort_remove_duplicates(first_layer_used_filaments);
sort_remove_duplicates(used_mixed_filaments);
auto used_filaments = collect_sorted_used_filaments(all_filaments);
this->set_slice_used_filaments(first_layer_used_filaments,used_filaments);
this->set_slice_used_mixed_filaments(used_mixed_filaments);
auto physical_unprintables = this->get_physical_unprintable_filaments(used_filaments);
auto geometric_unprintables = this->get_geometric_unprintable_filaments();
@@ -2717,6 +2729,7 @@ void Print::process(long long *time_cost_with_cache, bool use_cache)
first_layer_used_filaments = tool_ordering.layer_tools().front().extruders;
this->set_slice_used_filaments(first_layer_used_filaments, tool_ordering.all_extruders());
this->set_slice_used_mixed_filaments(tool_ordering.used_mixed_filaments());
has_wipe_tower = this->has_wipe_tower() && tool_ordering.has_wipe_tower();
initial_extruder_id = tool_ordering.first_extruder();
print_object_instances_ordering = chain_print_object_instances(*this);
@@ -4034,38 +4047,36 @@ void Print::_make_wipe_tower()
return;
// Check whether there are any layers in m_tool_ordering, which are marked with has_wipe_tower,
// they print neither object, nor support. These layers are above the raft and below the object, and they
// shall be added to the support layers to be printed.
// see https://github.com/prusa3d/PrusaSlicer/issues/607
// they print neither object, nor support. Each such layer needs a virtual support layer
// counterpart in m_objects.front() so that GCode::collect_layers_to_print picks it up and the
// wipe tower G-code is actually emitted for that z. Such layers appear in two scenarios:
// - above the raft, between raft top and the first real object layer
// (see https://github.com/prusa3d/PrusaSlicer/issues/607);
// - between two real wipe-tower layers, when one object is fully floating above another and
// the support_top_z_distance / support_bottom_z_distance gap leaves interior z values with
// neither object nor support (continuity fill in ToolOrdering::fill_wipe_tower_partitions).
// The previous implementation only handled the first contiguous run starting at the first
// virtual layer, which made the second scenario silently produce empty wipe-tower layers.
{
size_t idx_begin = size_t(-1);
size_t idx_end = m_wipe_tower_data.tool_ordering.layer_tools().size();
// Find the first wipe tower layer, which does not have a counterpart in an object or a support layer.
auto &support_layers = m_objects.front()->support_layers();
auto it_layer = support_layers.begin();
const size_t idx_end = m_wipe_tower_data.tool_ordering.layer_tools().size();
for (size_t i = 0; i < idx_end; ++ i) {
const LayerTools &lt = m_wipe_tower_data.tool_ordering.layer_tools()[i];
if (lt.has_wipe_tower && ! lt.has_object && ! lt.has_support) {
idx_begin = i;
break;
}
}
if (idx_begin != size_t(-1)) {
// Find the position in m_objects.first()->support_layers to insert these new support layers.
double wipe_tower_new_layer_print_z_first = m_wipe_tower_data.tool_ordering.layer_tools()[idx_begin].print_z;
auto it_layer = m_objects.front()->support_layers().begin();
auto it_end = m_objects.front()->support_layers().end();
for (; it_layer != it_end && (*it_layer)->print_z - EPSILON < wipe_tower_new_layer_print_z_first; ++ it_layer);
// Find the stopper of the sequence of wipe tower layers, which do not have a counterpart in an object or a support layer.
for (size_t i = idx_begin; i < idx_end; ++ i) {
LayerTools &lt = const_cast<LayerTools&>(m_wipe_tower_data.tool_ordering.layer_tools()[i]);
if (! (lt.has_wipe_tower && ! lt.has_object && ! lt.has_support))
break;
lt.has_support = true;
// Insert the new support layer.
double height = lt.print_z - (i == 0 ? 0. : m_wipe_tower_data.tool_ordering.layer_tools()[i-1].print_z);
//FIXME the support layer ID is set to -1, as Vojtech hopes it is not being used anyway.
it_layer = m_objects.front()->insert_support_layer(it_layer, -1, 0, height, lt.print_z, lt.print_z - 0.5 * height);
LayerTools &lt = const_cast<LayerTools&>(m_wipe_tower_data.tool_ordering.layer_tools()[i]);
if (! (lt.has_wipe_tower && ! lt.has_object && ! lt.has_support))
continue;
while (it_layer != support_layers.end() && (*it_layer)->print_z + EPSILON < lt.print_z)
++ it_layer;
if (it_layer != support_layers.end() && std::abs((*it_layer)->print_z - lt.print_z) < EPSILON) {
lt.has_support = true;
++ it_layer;
continue;
}
lt.has_support = true;
double height = lt.print_z - (i == 0 ? 0. : m_wipe_tower_data.tool_ordering.layer_tools()[i-1].print_z);
//FIXME the support layer ID is set to -1, as Vojtech hopes it is not being used anyway.
it_layer = m_objects.front()->insert_support_layer(it_layer, -1, 0, height, lt.print_z, lt.print_z - 0.5 * height);
++ it_layer;
}
}
this->throw_if_canceled();
+6
View File
@@ -1088,6 +1088,10 @@ public:
m_slice_used_filaments = used_filaments;
}
std::vector<unsigned int> get_slice_used_filaments(bool first_layer) const { return first_layer ? m_slice_used_filaments_first_layer : m_slice_used_filaments;}
void set_slice_used_mixed_filaments(const std::vector<unsigned int> &used_mixed_filaments) {
m_slice_used_mixed_filaments = used_mixed_filaments;
}
const std::vector<unsigned int>& get_slice_used_mixed_filaments() const { return m_slice_used_mixed_filaments; }
/**
* @brief Determines the unprintable filaments for each extruder based on its physical attributes
@@ -1355,6 +1359,8 @@ private:
std::vector<unsigned int> m_slice_used_filaments;
std::vector<unsigned int> m_slice_used_filaments_first_layer;
// 0-based mixed (virtual) filament slots actually used on this plate.
std::vector<unsigned int> m_slice_used_mixed_filaments;
//BBS: plate's origin
Vec3d m_origin {0, 0, 0};
+17
View File
@@ -11826,6 +11826,23 @@ std::map<std::string, std::string> validate(const FullPrintConfig &cfg, bool und
}
}
// Mixed-color (混色) parameter validation.
{
const auto &is_mixed = cfg.filament_is_mixed.values;
const auto &comp_strs = cfg.filament_mixed_components.values;
const auto &ratio_strs = cfg.filament_mixed_sublayer_ratios.values;
const auto &gradient_flags = cfg.filament_mixed_gradient.values;
const auto &range_strs = cfg.filament_mixed_gradient_range.values;
const auto &curve_strs = cfg.filament_mixed_gradient_curve.values;
std::map<std::string, std::string> mixed_errors = validate_mixed_filament_params(
is_mixed, comp_strs, ratio_strs, gradient_flags,
range_strs, curve_strs);
for (const auto &kv : mixed_errors)
if (error_message.find(kv.first) == error_message.end())
error_message.emplace(kv.first, kv.second);
}
// The configuration is valid.
return error_message;
}