Enhance mixed filament functionality: Introduce support for gradient component IDs and weights in mixed filaments, allowing for more complex color mixing configurations. Update parsing logic to accommodate new gradient definitions and ensure backward compatibility. Implement pointillism distribution mode for same-layer mixing, enhancing user control over filament blending. Improve GUI elements to facilitate gradient weight adjustments and multi-color previews, enriching the user experience in mixed filament management.

This commit is contained in:
Rad
2026-02-12 02:29:34 +01:00
parent 0178ad32ad
commit c414e377a0
10 changed files with 2889 additions and 72 deletions

View File

@@ -26,6 +26,7 @@
#include <cstdlib>
#include <chrono>
#include <iostream>
#include <numeric>
#include <math.h>
#include <stdlib.h>
#include <string>
@@ -3440,6 +3441,367 @@ static std::unique_ptr<ExtrusionEntityCollection> clip_extrusion_collection_for_
return out;
}
static std::vector<unsigned int> decode_manual_pattern_sequence_for_gcode(const MixedFilament& mf, size_t num_physical)
{
std::vector<unsigned int> sequence;
if (mf.manual_pattern.empty())
return sequence;
sequence.reserve(mf.manual_pattern.size());
for (const char token : mf.manual_pattern) {
unsigned int extruder_id = 0;
if (token == '1')
extruder_id = mf.component_a;
else if (token == '2')
extruder_id = mf.component_b;
else if (token >= '3' && token <= '9')
extruder_id = unsigned(token - '0');
if (extruder_id >= 1 && extruder_id <= num_physical)
sequence.emplace_back(extruder_id);
}
return sequence;
}
static std::vector<unsigned int> decode_gradient_component_ids_for_gcode(const MixedFilament& mf, size_t num_physical)
{
std::vector<unsigned int> ids;
if (mf.gradient_component_ids.empty() || num_physical == 0)
return ids;
bool seen[10] = { false };
ids.reserve(mf.gradient_component_ids.size());
for (const char c : mf.gradient_component_ids) {
if (c < '1' || c > '9')
continue;
const unsigned int id = unsigned(c - '0');
if (id == 0 || id > num_physical || seen[id])
continue;
seen[id] = true;
ids.emplace_back(id);
}
return ids;
}
static std::vector<int> decode_gradient_component_weights_for_gcode(const MixedFilament& mf, size_t expected_components)
{
std::vector<int> out;
if (mf.gradient_component_weights.empty() || expected_components == 0)
return out;
std::string token;
for (const char c : mf.gradient_component_weights) {
if (c >= '0' && c <= '9') {
token.push_back(c);
continue;
}
if (!token.empty()) {
out.emplace_back(std::max(0, std::atoi(token.c_str())));
token.clear();
}
}
if (!token.empty())
out.emplace_back(std::max(0, std::atoi(token.c_str())));
if (out.size() != expected_components)
return {};
return out;
}
static std::vector<unsigned int> build_weighted_gradient_sequence_for_gcode(const std::vector<unsigned int>& ids,
const std::vector<int>& weights)
{
if (ids.empty())
return {};
std::vector<unsigned int> filtered_ids;
std::vector<int> counts;
filtered_ids.reserve(ids.size());
counts.reserve(ids.size());
for (size_t i = 0; i < ids.size(); ++i) {
const int w = (i < weights.size()) ? std::max(0, weights[i]) : 0;
if (w <= 0)
continue;
filtered_ids.emplace_back(ids[i]);
counts.emplace_back(w);
}
if (filtered_ids.empty()) {
filtered_ids = ids;
counts.assign(ids.size(), 1);
}
int g = 0;
for (const int c : counts)
g = std::gcd(g, std::max(1, c));
if (g > 1) {
for (int &c : counts)
c = std::max(1, c / g);
}
int cycle = std::accumulate(counts.begin(), counts.end(), 0);
constexpr int k_max_cycle = 48;
if (cycle > k_max_cycle) {
const double scale = double(k_max_cycle) / double(cycle);
for (int &c : counts)
c = std::max(1, int(std::round(double(c) * scale)));
cycle = std::accumulate(counts.begin(), counts.end(), 0);
while (cycle > k_max_cycle) {
auto it = std::max_element(counts.begin(), counts.end());
if (it == counts.end() || *it <= 1)
break;
--(*it);
--cycle;
}
}
if (cycle <= 0)
return {};
std::vector<unsigned int> sequence;
sequence.reserve(size_t(cycle));
std::vector<int> emitted(counts.size(), 0);
for (int pos = 0; pos < cycle; ++pos) {
size_t best_idx = 0;
double best_score = -1e9;
for (size_t i = 0; i < counts.size(); ++i) {
const double target = double((pos + 1) * counts[i]) / double(cycle);
const double score = target - double(emitted[i]);
if (score > best_score) {
best_score = score;
best_idx = i;
}
}
++emitted[best_idx];
sequence.emplace_back(filtered_ids[best_idx]);
}
return sequence;
}
static size_t unique_extruder_count_for_gcode(const std::vector<unsigned int>& sequence, size_t num_physical)
{
if (sequence.empty() || num_physical == 0)
return 0;
std::vector<bool> seen(num_physical + 1, false);
size_t unique = 0;
for (const unsigned int id : sequence) {
if (id == 0 || id > num_physical)
continue;
if (!seen[id]) {
seen[id] = true;
++unique;
}
}
return unique;
}
static std::vector<unsigned int> pointillism_sequence_for_row_for_gcode(const MixedFilament& mf, size_t num_physical)
{
if (!mf.enabled || num_physical == 0 || mf.distribution_mode != int(MixedFilament::SameLayerPointillisme))
return {};
if (!mf.manual_pattern.empty())
return decode_manual_pattern_sequence_for_gcode(mf, num_physical);
const std::vector<unsigned int> gradient_ids = decode_gradient_component_ids_for_gcode(mf, num_physical);
if (gradient_ids.size() >= 2) {
const std::vector<int> gradient_weights = decode_gradient_component_weights_for_gcode(mf, gradient_ids.size());
const std::vector<unsigned int> weighted =
build_weighted_gradient_sequence_for_gcode(gradient_ids,
gradient_weights.empty() ? std::vector<int>(gradient_ids.size(), 1) : gradient_weights);
if (!weighted.empty())
return weighted;
}
if (mf.component_a < 1 || mf.component_a > num_physical ||
mf.component_b < 1 || mf.component_b > num_physical ||
mf.component_a == mf.component_b)
return {};
int ratio_a = std::max(0, mf.ratio_a);
int ratio_b = std::max(0, mf.ratio_b);
if (ratio_a == 0 && ratio_b == 0)
ratio_a = 1;
if (ratio_a > 0 && ratio_b > 0) {
const int g = std::gcd(ratio_a, ratio_b);
if (g > 1) {
ratio_a /= g;
ratio_b /= g;
}
}
constexpr int k_max_cycle = 24;
if (ratio_a + ratio_b > k_max_cycle) {
const double scale = double(k_max_cycle) / double(ratio_a + ratio_b);
ratio_a = std::max(1, int(std::round(double(ratio_a) * scale)));
ratio_b = std::max(1, int(std::round(double(ratio_b) * scale)));
}
const int cycle = std::max(1, ratio_a + ratio_b);
std::vector<unsigned int> sequence;
sequence.reserve(size_t(cycle));
for (int pos = 0; pos < cycle; ++pos) {
const int b_before = (pos * ratio_b) / cycle;
const int b_after = ((pos + 1) * ratio_b) / cycle;
sequence.emplace_back((b_after > b_before) ? mf.component_b : mf.component_a);
}
bool seen_a = false;
bool seen_b = false;
for (const unsigned int extruder_id : sequence) {
seen_a = seen_a || extruder_id == mf.component_a;
seen_b = seen_b || extruder_id == mf.component_b;
if (seen_a && seen_b)
break;
}
if (!seen_a || !seen_b)
return {};
return sequence;
}
static void split_polyline_by_length_for_pointillism(const Polyline& src,
const double split_length,
Polylines& out)
{
out.clear();
if (!src.is_valid())
return;
if (split_length <= EPSILON) {
out.emplace_back(src);
return;
}
Polyline remainder = src;
size_t guard = 0;
while (remainder.is_valid() && remainder.points.size() >= 2 && ++guard < 200000) {
if (remainder.length() <= split_length + EPSILON) {
out.emplace_back(std::move(remainder));
break;
}
Polyline head;
Polyline tail;
if (!remainder.split_at_length(split_length, &head, &tail) || !head.is_valid()) {
out.emplace_back(std::move(remainder));
break;
}
out.emplace_back(std::move(head));
if (!tail.is_valid() || tail.points.size() < 2)
break;
remainder = std::move(tail);
}
if (out.empty())
out.emplace_back(src);
}
static bool trim_polyline_for_pointillism_gap(Polyline& src, const double trim_each_end)
{
if (!src.is_valid())
return false;
if (trim_each_end <= EPSILON)
return true;
const double original_len = src.length();
if (original_len <= 2.0 * trim_each_end + EPSILON)
return false;
Polyline head;
Polyline tail;
if (!src.split_at_length(trim_each_end, &head, &tail) || !tail.is_valid() || tail.points.size() < 2)
return false;
src = std::move(tail);
const double keep_len = src.length() - trim_each_end;
if (keep_len <= EPSILON)
return false;
if (!src.split_at_length(keep_len, &head, &tail) || !head.is_valid() || head.points.size() < 2)
return false;
src = std::move(head);
return src.is_valid() && src.points.size() >= 2;
}
struct PointillismPathSplitStats
{
size_t segment_count { 0 };
size_t bucket_count { 0 };
};
// Sentinel used only in G-code generation to recognize pointillism path-domain
// split segments. This lets us apply per-segment runtime guards without
// affecting regular perimeter/infill paths.
static constexpr int k_pointillism_path_inset_marker = -7777;
static bool split_extrusion_collection_for_pointillism_paths(
const ExtrusionEntityCollection& source,
const std::vector<unsigned int>& sequence,
size_t num_physical,
const double split_length_scaled,
const double split_gap_scaled,
size_t sequence_phase,
std::vector<std::unique_ptr<ExtrusionEntityCollection>>& out_by_extruder,
PointillismPathSplitStats& out_stats)
{
out_by_extruder.clear();
out_by_extruder.resize(num_physical);
out_stats = {};
if (source.entities.empty() || sequence.empty() || num_physical == 0 || split_length_scaled <= EPSILON)
return false;
unsigned int fallback_extruder = 0;
for (const unsigned int id : sequence) {
if (id >= 1 && id <= num_physical) {
fallback_extruder = id;
break;
}
}
if (fallback_extruder == 0)
return false;
size_t sequence_idx = sequence_phase % sequence.size();
auto append_piece = [&](unsigned int extruder_id, const ExtrusionPath& src_path, Polyline& piece) {
if (!piece.is_valid())
return;
if (extruder_id == 0 || extruder_id > num_physical)
extruder_id = fallback_extruder;
std::unique_ptr<ExtrusionEntityCollection>& dst = out_by_extruder[extruder_id - 1];
if (!dst) {
dst = std::make_unique<ExtrusionEntityCollection>();
dst->no_sort = source.no_sort;
}
ExtrusionPath out_path(piece, src_path);
out_path.inset_idx = k_pointillism_path_inset_marker;
dst->append(std::move(out_path));
++out_stats.segment_count;
};
ExtrusionEntityCollection flattened = source.flatten(false);
for (const ExtrusionEntity* entity : flattened.entities) {
auto split_one_path = [&](const ExtrusionPath& path) {
Polylines pieces;
split_polyline_by_length_for_pointillism(path.polyline, split_length_scaled, pieces);
const double trim_each_end = std::max(0.0, split_gap_scaled * 0.5);
for (Polyline& piece : pieces) {
if (trim_each_end > EPSILON && !trim_polyline_for_pointillism_gap(piece, trim_each_end)) {
++sequence_idx;
continue;
}
unsigned int extruder_id = sequence[sequence_idx % sequence.size()];
append_piece(extruder_id, path, piece);
++sequence_idx;
}
};
if (const auto* path = dynamic_cast<const ExtrusionPath*>(entity)) {
split_one_path(*path);
} else if (const auto* multipath = dynamic_cast<const ExtrusionMultiPath*>(entity)) {
for (const ExtrusionPath& path : multipath->paths)
split_one_path(path);
} else if (const auto* loop = dynamic_cast<const ExtrusionLoop*>(entity)) {
for (const ExtrusionPath& path : loop->paths)
split_one_path(path);
}
}
for (const std::unique_ptr<ExtrusionEntityCollection>& bucket : out_by_extruder) {
if (bucket && !bucket->entities.empty())
++out_stats.bucket_count;
}
return out_stats.segment_count > 0;
}
inline std::vector<GCode::ObjectByExtruder::Island>& object_islands_by_extruder(
std::map<unsigned int, std::vector<GCode::ObjectByExtruder>>& by_extruder,
unsigned int extruder_id,
@@ -4138,6 +4500,50 @@ LayerResult GCode::process_layer(const Print& print,
// Group extrusions by an extruder, then by an object, an island and a region.
std::map<unsigned int, std::vector<ObjectByExtruder>> by_extruder;
bool is_anything_overridden = const_cast<LayerTools&>(layer_tools).wiping_extrusions().is_anything_overridden();
const double nozzle_0_mm = m_config.nozzle_diameter.values.empty() ? 0.4 : m_config.nozzle_diameter.get_at(0);
const double pointillism_pixel_size_cfg = std::max(0.0, double(m_config.mixed_filament_pointillism_pixel_size.value));
const double pointillism_segment_len_mm = pointillism_pixel_size_cfg > EPSILON ?
std::max(0.10, pointillism_pixel_size_cfg) :
std::max(0.60, 1.60 * nozzle_0_mm);
const double pointillism_line_gap_cfg_mm = std::max(0.0, double(m_config.mixed_filament_pointillism_line_gap.value));
const double pointillism_line_gap_mm = std::min(pointillism_line_gap_cfg_mm, pointillism_segment_len_mm * 0.90);
const double pointillism_segment_len_scaled = std::max<double>(scale_(0.10), scale_(pointillism_segment_len_mm));
const double pointillism_line_gap_scaled = std::max<double>(0.0, scale_(pointillism_line_gap_mm));
std::map<unsigned int, std::vector<unsigned int>> pointillism_sequence_cache;
size_t pointillism_path_split_entities = 0;
size_t pointillism_path_split_segments = 0;
size_t pointillism_path_split_fallbacks = 0;
auto configured_filament_id_1based = [&layer_tools](const ExtrusionEntityCollection& entities, const PrintRegion& region) -> unsigned int {
if (layer_tools.extruder_override != 0)
return layer_tools.extruder_override;
if (entities.has_infill()) {
if (entities.has_solid_infill())
return region.config().solid_infill_filament.value;
return region.config().sparse_infill_filament.value;
}
return region.config().wall_filament.value;
};
auto pointillism_sequence_for_filament = [&](unsigned int filament_id_1based) -> const std::vector<unsigned int>* {
if (filament_id_1based == 0 || layer_tools.mixed_mgr == nullptr || layer_tools.num_physical == 0)
return nullptr;
auto cache_it = pointillism_sequence_cache.find(filament_id_1based);
if (cache_it != pointillism_sequence_cache.end())
return cache_it->second.empty() ? nullptr : &cache_it->second;
std::vector<unsigned int> sequence;
if (layer_tools.mixed_mgr->is_mixed(filament_id_1based, layer_tools.num_physical)) {
const MixedFilament* mixed_row = layer_tools.mixed_mgr->mixed_filament_from_id(filament_id_1based, layer_tools.num_physical);
if (mixed_row != nullptr)
sequence = pointillism_sequence_for_row_for_gcode(*mixed_row, layer_tools.num_physical);
if (unique_extruder_count_for_gcode(sequence, layer_tools.num_physical) < 2)
sequence.clear();
}
auto inserted = pointillism_sequence_cache.emplace(filament_id_1based, std::move(sequence));
return inserted.first->second.empty() ? nullptr : &inserted.first->second;
};
// Compensate perimeter clipping at mixed-mask boundaries to avoid cracks from exact centerline clipping.
constexpr double LOCAL_Z_PERIMETER_MASK_EXPAND_MM = 0.10;
// Keep base exclusion smaller than mixed-pass inclusion to guarantee a slight overlap
@@ -4557,6 +4963,49 @@ LayerResult GCode::process_layer(const Print& print,
local_z_clipped_collections.emplace_back(std::move(clipped_base));
}
const unsigned int configured_filament_id = configured_filament_id_1based(*filtered_extrusions, region);
const std::vector<unsigned int>* pointillism_sequence =
is_anything_overridden ? nullptr : pointillism_sequence_for_filament(configured_filament_id);
if (pointillism_sequence != nullptr) {
std::vector<std::unique_ptr<ExtrusionEntityCollection>> split_by_extruder;
PointillismPathSplitStats split_stats;
const size_t sequence_phase = pointillism_sequence->empty() ?
0 : size_t(std::max(0, layer_tools.layer_index)) % pointillism_sequence->size();
if (split_extrusion_collection_for_pointillism_paths(*filtered_extrusions,
*pointillism_sequence,
layer_tools.num_physical,
pointillism_segment_len_scaled,
pointillism_line_gap_scaled,
sequence_phase,
split_by_extruder,
split_stats) &&
split_stats.bucket_count >= 2) {
++pointillism_path_split_entities;
pointillism_path_split_segments += split_stats.segment_count;
for (size_t extruder_idx = 0; extruder_idx < split_by_extruder.size(); ++extruder_idx) {
std::unique_ptr<ExtrusionEntityCollection>& split_collection = split_by_extruder[extruder_idx];
if (!split_collection || split_collection->entities.empty())
continue;
const ExtrusionEntityCollection* split_ptr = split_collection.get();
local_z_clipped_collections.emplace_back(std::move(split_collection));
std::vector<ObjectByExtruder::Island>& islands =
object_islands_by_extruder(by_extruder, unsigned(extruder_idx), layer_to_print_idx, layers.size(), n_slices + 1);
for (size_t i = 0; i <= n_slices; ++i) {
const bool last = i == n_slices;
const size_t island_idx = last ? n_slices : slices_test_order[i];
if (last || point_inside_surface(island_idx, split_ptr->first_point())) {
if (islands[island_idx].by_region.empty())
islands[island_idx].by_region.assign(print.num_print_regions(), ObjectByExtruder::Island::Region());
islands[island_idx].by_region[region.print_region_id()].append(entity_type, split_ptr, nullptr);
break;
}
}
}
continue;
}
++pointillism_path_split_fallbacks;
}
// This extrusion is part of certain Region, which tells us which extruder should be used for it:
int correct_extruder_id = layer_tools.extruder(*filtered_extrusions, region);
@@ -4754,15 +5203,20 @@ LayerResult GCode::process_layer(const Print& print,
gcode += "; local-z phase-b perimeter passes end\n";
}
std::vector<unsigned int> layer_extruders = layer_tools.extruders;
for (const auto& by_extruder_entry : by_extruder) {
if (std::find(layer_extruders.begin(), layer_extruders.end(), by_extruder_entry.first) == layer_extruders.end())
layer_extruders.emplace_back(by_extruder_entry.first);
}
// Extrude the skirt, brim, support, perimeters, infill ordered by the extruders.
for (unsigned int extruder_id : layer_tools.extruders) {
for (unsigned int extruder_id : layer_extruders) {
if (print.config().skirt_type == stCombined && !print.skirt().empty())
gcode += generate_skirt(print, print.skirt(), Point(0, 0), layer.object()->config().skirt_start_angle, layer_tools, layer,
extruder_id);
std::string gcode_toolchange;
if (has_wipe_tower) {
if (!m_wipe_tower->is_empty_wipe_tower_gcode(*this, extruder_id, extruder_id == layer_tools.extruders.back())) {
if (!m_wipe_tower->is_empty_wipe_tower_gcode(*this, extruder_id, extruder_id == layer_extruders.back())) {
if (need_insert_timelapse_gcode_for_traditional && !has_insert_timelapse_gcode) {
gcode += this->retract(false, false, LiftType::NormalLift);
m_writer.add_object_change_labels(gcode);
@@ -4781,7 +5235,7 @@ LayerResult GCode::process_layer(const Print& print,
}
has_insert_timelapse_gcode = true;
}
gcode_toolchange = m_wipe_tower->tool_change(*this, extruder_id, extruder_id == layer_tools.extruders.back());
gcode_toolchange = m_wipe_tower->tool_change(*this, extruder_id, extruder_id == layer_extruders.back());
}
} else {
gcode_toolchange = this->set_extruder(extruder_id, print_z);
@@ -5113,6 +5567,17 @@ LayerResult GCode::process_layer(const Print& print,
}
}
if (pointillism_path_split_entities > 0) {
BOOST_LOG_TRIVIAL(warning) << "Same-layer pointillisme path-domain split"
<< " layer_id=" << layer.id()
<< " print_z=" << print_z
<< " entities=" << pointillism_path_split_entities
<< " segments=" << pointillism_path_split_segments
<< " segment_len_mm=" << pointillism_segment_len_mm
<< " line_gap_mm=" << pointillism_line_gap_mm
<< " split_fallbacks=" << pointillism_path_split_fallbacks;
}
result.gcode = std::move(gcode);
result.cooling_buffer_flush = object_layer || raft_layer || last_layer;
return result;
@@ -5848,6 +6313,16 @@ std::string GCode::_extrude(const ExtrusionPath& path, std::string description,
gcode += this->unretract();
m_config.apply(m_calib_config);
const bool pointillism_path = path.inset_idx == k_pointillism_path_inset_marker;
const double path_length_mm = unscale<double>(path.length());
const double pointillism_pixel_size_mm = std::max(0.0, double(m_config.mixed_filament_pointillism_pixel_size.value));
const double pointillism_nominal_segment_mm = pointillism_pixel_size_mm > EPSILON
? std::max(0.10, pointillism_pixel_size_mm)
: std::max(0.20, double(m_config.nozzle_diameter.values.empty() ? 0.4 : m_config.nozzle_diameter.values.front()) * 2.0);
const double pointillism_min_accel_switch_len_mm = std::max(0.30, pointillism_nominal_segment_mm * 1.5);
const bool skip_accel_jerk_switch_for_short_pointillism =
pointillism_path && path_length_mm <= pointillism_min_accel_switch_len_mm + EPSILON;
// Orca: optimize for Klipper, set acceleration and jerk in one command
unsigned int acceleration_i = 0;
double jerk = 0;
@@ -5895,12 +6370,14 @@ std::string GCode::_extrude(const ExtrusionPath& path, std::string description,
}
}
if (m_writer.get_gcode_flavor() == gcfKlipper) {
gcode += m_writer.set_accel_and_jerk(acceleration_i, jerk);
if (!skip_accel_jerk_switch_for_short_pointillism) {
if (m_writer.get_gcode_flavor() == gcfKlipper) {
gcode += m_writer.set_accel_and_jerk(acceleration_i, jerk);
} else {
gcode += m_writer.set_print_acceleration(acceleration_i);
gcode += m_writer.set_jerk_xy(jerk);
} else {
gcode += m_writer.set_print_acceleration(acceleration_i);
gcode += m_writer.set_jerk_xy(jerk);
}
}
// calculate effective extrusion length per distance unit (e_per_mm)

View File

@@ -40,9 +40,8 @@ unsigned int resolve_mixed_with_layer_heights(const MixedFilamentManager *mixed_
if (!(mixed_mgr && mixed_mgr->is_mixed(filament_id_1based, num_physical)))
return filament_id_1based;
const size_t idx = static_cast<size_t>(filament_id_1based - num_physical - 1);
const auto &mixed = mixed_mgr->mixed_filaments();
const bool is_custom_mixed = idx < mixed.size() && mixed[idx].custom;
const MixedFilament *mixed_row = mixed_mgr->mixed_filament_from_id(filament_id_1based, num_physical);
const bool is_custom_mixed = mixed_row != nullptr && mixed_row->custom;
if (!is_custom_mixed && (layer_height_a > 0.f || layer_height_b > 0.f)) {
const float safe_base = std::max<float>(0.01f, base_layer_height);
@@ -51,9 +50,9 @@ unsigned int resolve_mixed_with_layer_heights(const MixedFilamentManager *mixed_
const int cycle = ratio_a + ratio_b;
if (cycle > 0) {
if (idx < mixed.size()) {
if (mixed_row != nullptr) {
const int pos = ((layer_index % cycle) + cycle) % cycle;
return pos < ratio_a ? mixed[idx].component_a : mixed[idx].component_b;
return pos < ratio_a ? mixed_row->component_a : mixed_row->component_b;
}
}
}

View File

@@ -5,6 +5,7 @@
#include <cctype>
#include <cmath>
#include <cstdio>
#include <cstdlib>
#include <sstream>
#include <iomanip>
#include <numeric>
@@ -263,7 +264,11 @@ static bool parse_row_definition(const std::string &row,
bool &enabled,
bool &custom,
int &mix_b_percent,
std::string &manual_pattern)
bool &pointillism_all_filaments,
std::string &gradient_component_ids,
std::string &gradient_component_weights,
std::string &manual_pattern,
int &distribution_mode)
{
auto trim_copy = [](const std::string &s) {
size_t lo = 0;
@@ -297,7 +302,7 @@ static bool parse_row_definition(const std::string &row,
while (std::getline(ss, token, ','))
tokens.emplace_back(trim_copy(token));
if (tokens.size() < 4 || tokens.size() > 6)
if (tokens.size() < 4 || tokens.size() > 12)
return false;
int values[5] = { 0, 0, 1, 1, 50 };
@@ -309,7 +314,7 @@ static bool parse_row_definition(const std::string &row,
!parse_int_token(tokens[3], values[4]))
return false;
} else {
// Current: a,b,enabled,custom,mix[,pattern]
// Current: a,b,enabled,custom,mix[,pointillism_all[,pattern]]
for (size_t i = 0; i < 5; ++i)
if (!parse_int_token(tokens[i], values[i]))
return false;
@@ -323,23 +328,72 @@ static bool parse_row_definition(const std::string &row,
enabled = (values[2] != 0);
custom = (tokens.size() == 4) ? true : (values[3] != 0);
mix_b_percent = clamp_int(values[4], 0, 100);
manual_pattern = (tokens.size() == 6) ? tokens[5] : std::string();
pointillism_all_filaments = false;
gradient_component_ids.clear();
gradient_component_weights.clear();
manual_pattern.clear();
distribution_mode = int(MixedFilament::Simple);
size_t token_idx = 5;
if (tokens.size() >= 6) {
// Backward compatibility:
// - old: token[5] is pointillism flag ("0"/"1")
// - old: token[5] is pattern ("12", "1212", ...)
// - new: token[5] may be metadata token ("g..." / "m...")
const std::string &legacy = tokens[5];
if (legacy == "0" || legacy == "1") {
pointillism_all_filaments = (legacy == "1");
token_idx = 6;
} else if (legacy.empty() || legacy[0] == 'g' || legacy[0] == 'G' || legacy[0] == 'm' || legacy[0] == 'M') {
token_idx = 5;
} else {
manual_pattern = legacy;
token_idx = 6;
}
}
for (size_t i = token_idx; i < tokens.size(); ++i) {
const std::string &tok = tokens[i];
if (tok.empty())
continue;
if (tok[0] == 'g' || tok[0] == 'G') {
gradient_component_ids = tok.substr(1);
continue;
}
if (tok[0] == 'w' || tok[0] == 'W') {
gradient_component_weights = tok.substr(1);
continue;
}
if (tok[0] == 'm' || tok[0] == 'M') {
int parsed_mode = distribution_mode;
if (parse_int_token(tok.substr(1), parsed_mode))
distribution_mode = clamp_int(parsed_mode, int(MixedFilament::LayerCycle), int(MixedFilament::Simple));
continue;
}
manual_pattern = tok;
}
// Compatibility for early same-layer prototype rows.
if (distribution_mode == int(MixedFilament::LayerCycle) && pointillism_all_filaments)
distribution_mode = int(MixedFilament::SameLayerPointillisme);
return true;
}
static bool is_pattern_separator(char c)
{
return std::isspace(static_cast<unsigned char>(c)) || c == '/' || c == '-' || c == '_' || c == '|' || c == ':' || c == ';';
return std::isspace(static_cast<unsigned char>(c)) || c == '/' || c == '-' || c == '_' || c == '|' || c == ':' || c == ';' || c == ',';
}
static bool decode_pattern_step(char c, char &out)
{
if (c >= '1' && c <= '9') {
out = c;
return true;
}
switch (std::tolower(static_cast<unsigned char>(c))) {
case '1':
case 'a':
out = '1';
return true;
case '2':
case 'b':
out = '2';
return true;
@@ -352,10 +406,211 @@ static int mix_percent_from_normalized_pattern(const std::string &pattern)
{
if (pattern.empty())
return 50;
// Legacy blend ratio for UI preview: count component-B aliases only.
// Tokens '3'..'9' are direct physical filament IDs and are ignored here.
const int count_b = int(std::count(pattern.begin(), pattern.end(), '2'));
return clamp_int(int(std::lround(100.0 * double(count_b) / double(pattern.size()))), 0, 100);
}
static std::string normalize_gradient_component_ids(const std::string &components)
{
std::string normalized;
normalized.reserve(components.size());
bool seen[10] = { false };
for (const char c : components) {
if (c < '1' || c > '9')
continue;
const int idx = c - '0';
if (seen[idx])
continue;
seen[idx] = true;
normalized.push_back(c);
}
return normalized;
}
static std::vector<unsigned int> decode_gradient_component_ids(const std::string &components, size_t num_physical)
{
std::vector<unsigned int> ids;
if (components.empty() || num_physical == 0)
return ids;
bool seen[10] = { false };
ids.reserve(components.size());
for (const char c : components) {
if (c < '1' || c > '9')
continue;
const unsigned int id = unsigned(c - '0');
if (id == 0 || id > num_physical || seen[id])
continue;
seen[id] = true;
ids.emplace_back(id);
}
return ids;
}
static std::vector<int> parse_gradient_weight_tokens(const std::string &weights)
{
std::vector<int> out;
std::string token;
for (const char c : weights) {
if (c >= '0' && c <= '9') {
token.push_back(c);
continue;
}
if (!token.empty()) {
out.emplace_back(std::max(0, std::atoi(token.c_str())));
token.clear();
}
}
if (!token.empty())
out.emplace_back(std::max(0, std::atoi(token.c_str())));
return out;
}
static std::vector<int> normalize_weight_vector_to_percent(const std::vector<int> &weights)
{
std::vector<int> out(weights.size(), 0);
if (weights.empty())
return out;
int sum = 0;
for (const int w : weights)
sum += std::max(0, w);
if (sum <= 0)
return out;
std::vector<double> remainders(weights.size(), 0.);
int assigned = 0;
for (size_t i = 0; i < weights.size(); ++i) {
const double exact = 100.0 * double(std::max(0, weights[i])) / double(sum);
out[i] = int(std::floor(exact));
remainders[i] = exact - double(out[i]);
assigned += out[i];
}
int missing = std::max(0, 100 - assigned);
while (missing > 0) {
size_t best_idx = 0;
double best_rem = -1.0;
for (size_t i = 0; i < remainders.size(); ++i) {
if (weights[i] <= 0)
continue;
if (remainders[i] > best_rem) {
best_rem = remainders[i];
best_idx = i;
}
}
++out[best_idx];
remainders[best_idx] = 0.0;
--missing;
}
return out;
}
static std::string normalize_gradient_component_weights(const std::string &weights, size_t expected_components)
{
if (expected_components == 0)
return std::string();
std::vector<int> parsed = parse_gradient_weight_tokens(weights);
if (parsed.size() != expected_components)
return std::string();
std::vector<int> normalized = normalize_weight_vector_to_percent(parsed);
int sum = 0;
for (const int v : normalized)
sum += v;
if (sum <= 0)
return std::string();
std::ostringstream ss;
for (size_t i = 0; i < normalized.size(); ++i) {
if (i > 0)
ss << '/';
ss << normalized[i];
}
return ss.str();
}
static std::vector<int> decode_gradient_component_weights(const std::string &weights, size_t expected_components)
{
if (expected_components == 0)
return {};
std::vector<int> parsed = parse_gradient_weight_tokens(weights);
if (parsed.size() != expected_components)
return {};
std::vector<int> normalized = normalize_weight_vector_to_percent(parsed);
int sum = 0;
for (const int v : normalized)
sum += v;
return (sum > 0) ? normalized : std::vector<int>();
}
static std::vector<unsigned int> build_weighted_gradient_sequence(const std::vector<unsigned int> &ids,
const std::vector<int> &weights)
{
if (ids.empty())
return {};
std::vector<unsigned int> filtered_ids;
std::vector<int> counts;
filtered_ids.reserve(ids.size());
counts.reserve(ids.size());
for (size_t i = 0; i < ids.size(); ++i) {
const int w = (i < weights.size()) ? std::max(0, weights[i]) : 0;
if (w <= 0)
continue;
filtered_ids.emplace_back(ids[i]);
counts.emplace_back(w);
}
if (filtered_ids.empty()) {
filtered_ids = ids;
counts.assign(ids.size(), 1);
}
int g = 0;
for (const int c : counts)
g = std::gcd(g, std::max(1, c));
if (g > 1) {
for (int &c : counts)
c = std::max(1, c / g);
}
int cycle = std::accumulate(counts.begin(), counts.end(), 0);
constexpr int k_max_cycle = 48;
if (cycle > k_max_cycle) {
const double scale = double(k_max_cycle) / double(cycle);
for (int &c : counts)
c = std::max(1, int(std::round(double(c) * scale)));
cycle = std::accumulate(counts.begin(), counts.end(), 0);
while (cycle > k_max_cycle) {
auto it = std::max_element(counts.begin(), counts.end());
if (it == counts.end() || *it <= 1)
break;
--(*it);
--cycle;
}
}
if (cycle <= 0)
return {};
std::vector<unsigned int> sequence;
sequence.reserve(size_t(cycle));
std::vector<int> emitted(counts.size(), 0);
for (int pos = 0; pos < cycle; ++pos) {
size_t best_idx = 0;
double best_score = -1e9;
for (size_t i = 0; i < counts.size(); ++i) {
const double target = double((pos + 1) * counts[i]) / double(cycle);
const double score = target - double(emitted[i]);
if (score > best_score) {
best_score = score;
best_idx = i;
}
}
++emitted[best_idx];
sequence.emplace_back(filtered_ids[best_idx]);
}
return sequence;
}
// ---------------------------------------------------------------------------
// MixedFilamentManager
// ---------------------------------------------------------------------------
@@ -455,6 +710,10 @@ void MixedFilamentManager::add_custom_filament(unsigned int component_a,
mf.ratio_a = 1;
mf.ratio_b = 1;
mf.manual_pattern.clear();
mf.gradient_component_ids.clear();
mf.gradient_component_weights.clear();
mf.pointillism_all_filaments = false;
mf.distribution_mode = int(MixedFilament::Simple);
mf.enabled = true;
mf.custom = true;
m_mixed.push_back(std::move(mf));
@@ -514,11 +773,17 @@ std::string MixedFilamentManager::serialize_custom_entries() const
if (!first)
ss << ';';
first = false;
const std::string normalized_ids = normalize_gradient_component_ids(mf.gradient_component_ids);
const std::string normalized_weights = normalize_gradient_component_weights(mf.gradient_component_weights, normalized_ids.size());
ss << mf.component_a << ','
<< mf.component_b << ','
<< (mf.enabled ? 1 : 0) << ','
<< (mf.custom ? 1 : 0) << ','
<< clamp_int(mf.mix_b_percent, 0, 100);
<< clamp_int(mf.mix_b_percent, 0, 100) << ','
<< (mf.pointillism_all_filaments ? 1 : 0) << ','
<< 'g' << normalized_ids << ','
<< 'w' << normalized_weights << ','
<< 'm' << clamp_int(mf.distribution_mode, int(MixedFilament::LayerCycle), int(MixedFilament::Simple));
const std::string normalized_pattern = normalize_manual_pattern(mf.manual_pattern);
if (!normalized_pattern.empty())
ss << ',' << normalized_pattern;
@@ -552,8 +817,13 @@ void MixedFilamentManager::load_custom_entries(const std::string &serialized, co
bool enabled = true;
bool custom = true;
int mix = 50;
bool pointillism_all_filaments = false;
std::string gradient_component_ids;
std::string gradient_component_weights;
std::string manual_pattern;
if (!parse_row_definition(row, a, b, enabled, custom, mix, manual_pattern)) {
int distribution_mode = int(MixedFilament::Simple);
if (!parse_row_definition(row, a, b, enabled, custom, mix, pointillism_all_filaments,
gradient_component_ids, gradient_component_weights, manual_pattern, distribution_mode)) {
++skipped_rows;
BOOST_LOG_TRIVIAL(warning) << "MixedFilamentManager::load_custom_entries invalid row format: " << row;
continue;
@@ -574,7 +844,12 @@ void MixedFilamentManager::load_custom_entries(const std::string &serialized, co
});
if (it_auto != m_mixed.end()) {
it_auto->enabled = enabled;
it_auto->pointillism_all_filaments = pointillism_all_filaments;
it_auto->gradient_component_ids = normalize_gradient_component_ids(gradient_component_ids);
it_auto->gradient_component_weights =
normalize_gradient_component_weights(gradient_component_weights, it_auto->gradient_component_ids.size());
it_auto->manual_pattern = normalize_manual_pattern(manual_pattern);
it_auto->distribution_mode = clamp_int(distribution_mode, int(MixedFilament::LayerCycle), int(MixedFilament::Simple));
it_auto->mix_b_percent = it_auto->manual_pattern.empty() ? mix : mix_percent_from_normalized_pattern(it_auto->manual_pattern);
++updated_auto;
continue;
@@ -587,7 +862,12 @@ void MixedFilamentManager::load_custom_entries(const std::string &serialized, co
mf.mix_b_percent = mix;
mf.ratio_a = 1;
mf.ratio_b = 1;
mf.pointillism_all_filaments = pointillism_all_filaments;
mf.gradient_component_ids = normalize_gradient_component_ids(gradient_component_ids);
mf.gradient_component_weights =
normalize_gradient_component_weights(gradient_component_weights, mf.gradient_component_ids.size());
mf.manual_pattern = normalize_manual_pattern(manual_pattern);
mf.distribution_mode = clamp_int(distribution_mode, int(MixedFilament::LayerCycle), int(MixedFilament::Simple));
if (!mf.manual_pattern.empty())
mf.mix_b_percent = mix_percent_from_normalized_pattern(mf.manual_pattern);
mf.enabled = enabled;
@@ -612,20 +892,41 @@ unsigned int MixedFilamentManager::resolve(unsigned int filament_id,
float layer_height,
bool force_height_weighted) const
{
if (!is_mixed(filament_id, num_physical))
const int mixed_idx = mixed_index_from_filament_id(filament_id, num_physical);
if (mixed_idx < 0)
return filament_id;
const size_t idx = index_of(filament_id, num_physical);
if (idx >= m_mixed.size())
return 1; // fallback to first extruder
const MixedFilament &mf = m_mixed[idx];
const MixedFilament &mf = m_mixed[size_t(mixed_idx)];
// Manual pattern takes precedence when provided. Pattern uses repeating
// steps: '1' => component_a, '2' => component_b.
// steps: '1' => component_a, '2' => component_b, '3'..'9' => direct
// physical filament IDs.
if (!mf.manual_pattern.empty()) {
const int pos = safe_mod(layer_index, int(mf.manual_pattern.size()));
return mf.manual_pattern[size_t(pos)] == '2' ? mf.component_b : mf.component_a;
const char token = mf.manual_pattern[size_t(pos)];
if (token == '2')
return mf.component_b;
if (token == '1')
return mf.component_a;
if (token >= '3' && token <= '9') {
const unsigned int direct = unsigned(token - '0');
if (direct >= 1 && direct <= num_physical)
return direct;
}
return mf.component_a;
}
const bool use_simple_mode = mf.distribution_mode == int(MixedFilament::Simple);
const std::vector<unsigned int> gradient_ids = decode_gradient_component_ids(mf.gradient_component_ids, num_physical);
if (!use_simple_mode && gradient_ids.size() >= 3) {
const std::vector<int> gradient_weights =
decode_gradient_component_weights(mf.gradient_component_weights, gradient_ids.size());
const std::vector<unsigned int> gradient_sequence = build_weighted_gradient_sequence(
gradient_ids, gradient_weights.empty() ? std::vector<int>(gradient_ids.size(), 1) : gradient_weights);
if (!gradient_sequence.empty()) {
const size_t pos = size_t(safe_mod(layer_index, int(gradient_sequence.size())));
return gradient_sequence[pos];
}
}
// Height-weighted cadence can be forced by the local-Z planner. The
@@ -656,6 +957,29 @@ unsigned int MixedFilamentManager::resolve(unsigned int filament_id,
return (pos < mf.ratio_a) ? mf.component_a : mf.component_b;
}
int MixedFilamentManager::mixed_index_from_filament_id(unsigned int filament_id, size_t num_physical) const
{
if (filament_id <= num_physical)
return -1;
const size_t enabled_virtual_idx = size_t(filament_id - num_physical - 1);
size_t enabled_seen = 0;
for (size_t i = 0; i < m_mixed.size(); ++i) {
if (!m_mixed[i].enabled)
continue;
if (enabled_seen == enabled_virtual_idx)
return int(i);
++enabled_seen;
}
return -1;
}
const MixedFilament *MixedFilamentManager::mixed_filament_from_id(unsigned int filament_id, size_t num_physical) const
{
const int idx = mixed_index_from_filament_id(filament_id, num_physical);
return idx >= 0 ? &m_mixed[size_t(idx)] : nullptr;
}
std::string MixedFilamentManager::blend_color(const std::string &color_a,
const std::string &color_b,
int ratio_a, int ratio_b)
@@ -693,6 +1017,37 @@ std::string MixedFilamentManager::blend_color(const std::string &color_a,
void MixedFilamentManager::refresh_display_colors(const std::vector<std::string> &filament_colours)
{
for (MixedFilament &mf : m_mixed) {
const std::vector<unsigned int> gradient_ids = decode_gradient_component_ids(mf.gradient_component_ids, filament_colours.size());
if (mf.distribution_mode != int(MixedFilament::Simple) && gradient_ids.size() >= 3) {
const std::vector<int> gradient_weights =
decode_gradient_component_weights(mf.gradient_component_weights, gradient_ids.size());
const std::vector<unsigned int> gradient_sequence =
build_weighted_gradient_sequence(gradient_ids,
gradient_weights.empty() ? std::vector<int>(gradient_ids.size(), 1) : gradient_weights);
if (gradient_sequence.empty()) {
mf.display_color = "#26A69A";
continue;
}
std::vector<int> counts(gradient_ids.size(), 0);
for (const unsigned int id : gradient_sequence) {
auto it = std::find(gradient_ids.begin(), gradient_ids.end(), id);
if (it != gradient_ids.end())
++counts[size_t(it - gradient_ids.begin())];
}
std::string blended = filament_colours[gradient_ids.front() - 1];
int accum = std::max(1, counts.front());
for (size_t i = 1; i < gradient_ids.size(); ++i) {
const int wi = std::max(0, counts[i]);
if (wi == 0)
continue;
blended = blend_color(blended, filament_colours[gradient_ids[i] - 1], accum, wi);
accum += wi;
}
mf.display_color = blended;
continue;
}
if (mf.component_a == 0 || mf.component_b == 0 ||
mf.component_a > filament_colours.size() || mf.component_b > filament_colours.size()) {
mf.display_color = "#26A69A";

View File

@@ -8,12 +8,19 @@
namespace Slic3r {
// Represents a virtual "mixed" filament created by alternating layers of two
// physical filaments. The display colour uses an RYB pigment-style blend so
// Represents a virtual "mixed" filament created from physical filaments
// (layer cadence and/or same-layer interleaved stripe distribution). The display
// colour uses an RYB pigment-style blend so
// pair previews better match expected print mixing (for example Blue+Yellow
// -> Green, Red+Yellow -> Orange, Red+Blue -> Purple).
struct MixedFilament
{
enum DistributionMode : uint8_t {
LayerCycle = 0,
SameLayerPointillisme = 1,
Simple = 2
};
// 1-based physical filament IDs that are combined.
unsigned int component_a = 1;
unsigned int component_b = 2;
@@ -26,11 +33,27 @@ struct MixedFilament
// Blend percentage of component B in [0..100].
int mix_b_percent = 50;
// Optional manual layer pattern for this mixed filament, encoded as a
// string of '1' and '2'. '1' means component_a, '2' means component_b.
// Example: "11112222" => AAAABBBB repeating.
// Optional manual pattern for this mixed filament. Tokens:
// '1' => component_a, '2' => component_b, '3'..'9' => direct physical
// filament IDs (1-based). Example: "11112222" => AAAABBBB repeating.
std::string manual_pattern;
// Optional explicit gradient multi-color component list, encoded as
// compact physical filament IDs (for example "123" -> filaments 1,2,3).
// Interleaved stripe mode is active for gradient rows only when this list has 3+ IDs.
std::string gradient_component_ids;
// Optional explicit multi-color weights aligned with gradient_component_ids.
// Compact integer list joined by '/': for example "50/25/25".
std::string gradient_component_weights;
// Legacy compatibility flag from earlier prototype serialization.
bool pointillism_all_filaments = false;
// How this mixed row is distributed:
// - LayerCycle: one filament per layer based on cadence.
// - SameLayerPointillisme: split painted masks in XY on each layer.
int distribution_mode = int(Simple);
// Whether this mixed filament is enabled (available for assignment).
bool enabled = true;
@@ -48,6 +71,10 @@ struct MixedFilament
ratio_b == rhs.ratio_b &&
mix_b_percent == rhs.mix_b_percent &&
manual_pattern == rhs.manual_pattern &&
gradient_component_ids == rhs.gradient_component_ids &&
gradient_component_weights == rhs.gradient_component_weights &&
pointillism_all_filaments == rhs.pointillism_all_filaments &&
distribution_mode == rhs.distribution_mode &&
enabled == rhs.enabled &&
custom == rhs.custom;
}
@@ -99,7 +126,7 @@ public:
std::string serialize_custom_entries() const;
void load_custom_entries(const std::string &serialized, const std::vector<std::string> &filament_colours);
// Normalize a manual mixed-pattern string into compact '1'/'2' form.
// Normalize a manual mixed-pattern string into compact token form.
// Accepts separators and A/B aliases. Returns empty string if invalid.
static std::string normalize_manual_pattern(const std::string &pattern);
@@ -108,7 +135,7 @@ public:
// True when `filament_id` (1-based) refers to a mixed filament.
bool is_mixed(unsigned int filament_id, size_t num_physical) const
{
return filament_id > num_physical && index_of(filament_id, num_physical) < m_mixed.size();
return mixed_index_from_filament_id(filament_id, num_physical) >= 0;
}
// Resolve a mixed filament ID to a physical extruder (1-based) for the
@@ -121,6 +148,12 @@ public:
float layer_height = 0.f,
bool force_height_weighted = false) const;
// Map virtual filament ID (1-based, after physical IDs) to index into
// m_mixed. Virtual IDs enumerate enabled mixed rows only.
int mixed_index_from_filament_id(unsigned int filament_id, size_t num_physical) const;
const MixedFilament *mixed_filament_from_id(unsigned int filament_id, size_t num_physical) const;
// Compute a display colour by blending in RYB pigment space.
static std::string blend_color(const std::string &color_a,
const std::string &color_b,

View File

@@ -1048,7 +1048,20 @@ static PrintObjectRegions* generate_print_object_regions(
cfg.wall_filament.value = painted_extruder_id;
cfg.solid_infill_filament.value = painted_extruder_id;
cfg.sparse_infill_filament.value = painted_extruder_id;
layer_range.painted_regions.push_back({ painted_extruder_id, parent_region_id, get_create_region(std::move(cfg))});
PrintRegion *painted_region = get_create_region(std::move(cfg));
if (painted_region->config().wall_filament.value != painted_extruder_id ||
painted_region->config().solid_infill_filament.value != painted_extruder_id ||
painted_region->config().sparse_infill_filament.value != painted_extruder_id) {
BOOST_LOG_TRIVIAL(warning) << "Painted region filament mismatch"
<< " requested_extruder_id=" << painted_extruder_id
<< " wall_filament=" << painted_region->config().wall_filament.value
<< " solid_infill_filament=" << painted_region->config().solid_infill_filament.value
<< " sparse_infill_filament=" << painted_region->config().sparse_infill_filament.value
<< " parent_region_id=" << parent_region_id
<< " parent_print_region_id=" << parent_region.region->print_object_region_id()
<< " painted_print_region_id=" << painted_region->print_object_region_id();
}
layer_range.painted_regions.push_back({ painted_extruder_id, parent_region_id, painted_region });
}
// Sort the regions by parent region::print_object_region_id() and extruder_id to help the slicing algorithm when applying MM segmentation.
std::sort(layer_range.painted_regions.begin(), layer_range.painted_regions.end(), [&layer_range](auto &l, auto &r) {
@@ -1089,6 +1102,58 @@ static PrintObjectRegions* generate_print_object_regions(
return out.release();
}
static inline void append_unique_painted_extruder(std::vector<unsigned int> &painting_extruders,
unsigned int extruder_id,
size_t num_physical_extruders)
{
if (extruder_id < 1 || extruder_id > num_physical_extruders)
return;
if (std::find(painting_extruders.begin(), painting_extruders.end(), extruder_id) == painting_extruders.end())
painting_extruders.emplace_back(extruder_id);
}
static void append_same_layer_component_extruders(const MixedFilamentManager &mixed_mgr,
unsigned int state_id,
size_t num_physical_extruders,
std::vector<unsigned int> &painting_extruders)
{
if (state_id <= num_physical_extruders)
return;
const MixedFilament *mixed_row = mixed_mgr.mixed_filament_from_id(state_id, num_physical_extruders);
if (mixed_row == nullptr || !mixed_row->enabled || mixed_row->distribution_mode != int(MixedFilament::SameLayerPointillisme))
return;
append_unique_painted_extruder(painting_extruders, mixed_row->component_a, num_physical_extruders);
append_unique_painted_extruder(painting_extruders, mixed_row->component_b, num_physical_extruders);
for (char token : mixed_row->gradient_component_ids) {
if (token < '1' || token > '9')
continue;
append_unique_painted_extruder(painting_extruders, unsigned(token - '0'), num_physical_extruders);
}
for (char token : mixed_row->manual_pattern) {
unsigned int extruder_id = 0;
if (token == '1')
extruder_id = mixed_row->component_a;
else if (token == '2')
extruder_id = mixed_row->component_b;
else if (token >= '3' && token <= '9')
extruder_id = unsigned(token - '0');
append_unique_painted_extruder(painting_extruders, extruder_id, num_physical_extruders);
}
}
static bool same_layer_pointillism_enabled(const MixedFilamentManager &mixed_mgr)
{
for (const MixedFilament &mf : mixed_mgr.mixed_filaments())
if (mf.enabled && mf.distribution_mode == int(MixedFilament::SameLayerPointillisme))
return true;
return false;
}
Print::ApplyStatus Print::apply(const Model &model, DynamicPrintConfig new_full_config)
{
#ifdef _DEBUG
@@ -1110,6 +1175,8 @@ Print::ApplyStatus Print::apply(const Model &model, DynamicPrintConfig new_full_
new_full_config.option("mixed_filament_height_upper_bound", true);
new_full_config.option("mixed_filament_cycle_layers", true);
new_full_config.option("mixed_filament_advanced_dithering", true);
new_full_config.option("mixed_filament_pointillism_pixel_size", true);
new_full_config.option("mixed_filament_pointillism_line_gap", true);
new_full_config.option("mixed_filament_definitions", true);
m_config.option("dithering_z_step_size", true);
m_config.option("dithering_local_z_mode", true);
@@ -1119,6 +1186,8 @@ Print::ApplyStatus Print::apply(const Model &model, DynamicPrintConfig new_full_
m_config.option("mixed_filament_height_upper_bound", true);
m_config.option("mixed_filament_cycle_layers", true);
m_config.option("mixed_filament_advanced_dithering", true);
m_config.option("mixed_filament_pointillism_pixel_size", true);
m_config.option("mixed_filament_pointillism_line_gap", true);
m_config.option("mixed_filament_definitions", true);
m_default_object_config.option("dithering_z_step_size", true);
m_default_object_config.option("dithering_local_z_mode", true);
@@ -1128,6 +1197,8 @@ Print::ApplyStatus Print::apply(const Model &model, DynamicPrintConfig new_full_
m_default_object_config.option("mixed_filament_height_upper_bound", true);
m_default_object_config.option("mixed_filament_cycle_layers", true);
m_default_object_config.option("mixed_filament_advanced_dithering", true);
m_default_object_config.option("mixed_filament_pointillism_pixel_size", true);
m_default_object_config.option("mixed_filament_pointillism_line_gap", true);
m_default_object_config.option("mixed_filament_definitions", true);
// BBS
int used_filaments = this->extruders(true).size();
@@ -1231,6 +1302,8 @@ Print::ApplyStatus Print::apply(const Model &model, DynamicPrintConfig new_full_
float mixed_height_upper = 0.16f;
int mixed_cycle_layers = 4;
bool mixed_advanced_dither = false;
float mixed_pointillism_pixel_size = 0.f;
float mixed_pointillism_line_gap = 0.f;
std::string mixed_custom_definitions;
if (new_full_config.has("mixed_filament_gradient_mode")) {
if (const ConfigOptionBool *opt = new_full_config.option<ConfigOptionBool>("mixed_filament_gradient_mode"))
@@ -1250,6 +1323,10 @@ Print::ApplyStatus Print::apply(const Model &model, DynamicPrintConfig new_full_
else
mixed_advanced_dither = (new_full_config.opt_int("mixed_filament_advanced_dithering") != 0);
}
if (new_full_config.has("mixed_filament_pointillism_pixel_size"))
mixed_pointillism_pixel_size = float(new_full_config.opt_float("mixed_filament_pointillism_pixel_size"));
if (new_full_config.has("mixed_filament_pointillism_line_gap"))
mixed_pointillism_line_gap = float(new_full_config.opt_float("mixed_filament_pointillism_line_gap"));
if (new_full_config.has("mixed_filament_definitions"))
mixed_custom_definitions = new_full_config.opt_string("mixed_filament_definitions");
@@ -1257,6 +1334,8 @@ Print::ApplyStatus Print::apply(const Model &model, DynamicPrintConfig new_full_
mixed_height_lower = std::max(0.01f, mixed_height_lower);
mixed_height_upper = std::max(mixed_height_lower, mixed_height_upper);
mixed_cycle_layers = std::max(2, mixed_cycle_layers);
mixed_pointillism_pixel_size = std::max(0.f, mixed_pointillism_pixel_size);
mixed_pointillism_line_gap = std::max(0.f, mixed_pointillism_line_gap);
BOOST_LOG_TRIVIAL(info) << "Print::apply mixed settings"
<< ", gradient_mode=" << mixed_gradient_mode
@@ -1264,6 +1343,8 @@ Print::ApplyStatus Print::apply(const Model &model, DynamicPrintConfig new_full_
<< ", upper=" << mixed_height_upper
<< ", cycle_layers=" << mixed_cycle_layers
<< ", advanced_dither=" << (mixed_advanced_dither ? 1 : 0)
<< ", pointillism_pixel_size=" << mixed_pointillism_pixel_size
<< ", pointillism_line_gap=" << mixed_pointillism_line_gap
<< ", custom_definitions_len=" << mixed_custom_definitions.size()
<< ", physical_extruders=" << num_extruders;
@@ -1672,6 +1753,7 @@ Print::ApplyStatus Print::apply(const Model &model, DynamicPrintConfig new_full_
print_object_regions->ref_cnt_inc();
}
std::vector<unsigned int> painting_extruders;
const bool same_layer_mode_active = same_layer_pointillism_enabled(m_mixed_filament_mgr);
if (const auto &volumes = print_object.model_object()->volumes;
num_extruders > 1 &&
std::find_if(volumes.begin(), volumes.end(), [](const ModelVolume *v) { return ! v->mmu_segmentation_facets.empty(); }) != volumes.end()) {
@@ -1689,11 +1771,27 @@ Print::ApplyStatus Print::apply(const Model &model, DynamicPrintConfig new_full_
for (size_t state_idx = static_cast<size_t>(EnforcerBlockerType::Extruder1); state_idx < used_facet_states.size(); ++state_idx) {
if (!used_facet_states[state_idx])
continue;
if (state_idx <= num_total_filaments)
if (state_idx <= num_total_filaments) {
painting_extruders.emplace_back(static_cast<unsigned int>(state_idx));
else
append_same_layer_component_extruders(m_mixed_filament_mgr,
static_cast<unsigned int>(state_idx),
num_extruders,
painting_extruders);
} else
++dropped_painted_states;
}
std::sort(painting_extruders.begin(), painting_extruders.end());
painting_extruders.erase(std::unique(painting_extruders.begin(), painting_extruders.end()), painting_extruders.end());
bool expanded_all_channels_for_same_layer = false;
if (same_layer_mode_active && !painting_extruders.empty()) {
const unsigned int max_channel = unsigned(std::min<size_t>(num_total_filaments, size_t(EnforcerBlockerType::ExtruderMax)));
for (unsigned int channel_id = 1; channel_id <= max_channel; ++channel_id)
painting_extruders.emplace_back(channel_id);
std::sort(painting_extruders.begin(), painting_extruders.end());
painting_extruders.erase(std::unique(painting_extruders.begin(), painting_extruders.end()), painting_extruders.end());
expanded_all_channels_for_same_layer = true;
}
if (dropped_painted_states > 0) {
BOOST_LOG_TRIVIAL(warning) << "Print::apply dropping painted extruder IDs above available filament range"
@@ -1715,12 +1813,22 @@ Print::ApplyStatus Print::apply(const Model &model, DynamicPrintConfig new_full_
BOOST_LOG_TRIVIAL(warning) << "Print::apply detected painted extruder IDs above available filament range"
<< " painted_extruders=[" << painting_ids << "]"
<< " physical_filaments=" << num_extruders
<< " total_filaments=" << num_total_filaments;
<< " total_filaments=" << num_total_filaments
<< " same_layer_expand_all_channels=" << (expanded_all_channels_for_same_layer ? 1 : 0);
} else {
BOOST_LOG_TRIVIAL(debug) << "Print::apply collected painted extruders"
<< " painted_extruders=[" << painting_ids << "]"
<< " physical_filaments=" << num_extruders
<< " total_filaments=" << num_total_filaments;
if (same_layer_mode_active) {
BOOST_LOG_TRIVIAL(warning) << "Print::apply collected painted extruders"
<< " painted_extruders=[" << painting_ids << "]"
<< " physical_filaments=" << num_extruders
<< " total_filaments=" << num_total_filaments
<< " same_layer_expand_all_channels=" << (expanded_all_channels_for_same_layer ? 1 : 0);
} else {
BOOST_LOG_TRIVIAL(debug) << "Print::apply collected painted extruders"
<< " painted_extruders=[" << painting_ids << "]"
<< " physical_filaments=" << num_extruders
<< " total_filaments=" << num_total_filaments
<< " same_layer_expand_all_channels=" << (expanded_all_channels_for_same_layer ? 1 : 0);
}
}
}
}
@@ -1731,6 +1839,11 @@ Print::ApplyStatus Print::apply(const Model &model, DynamicPrintConfig new_full_
if ((*it)->m_shared_regions != nullptr)
update_apply_status((*it)->invalidate_all_steps());
};
if (same_layer_mode_active && !painting_extruders.empty()) {
invalidate();
model_object_status.print_object_regions_status = ModelObjectStatus::PrintObjectRegionsStatus::PartiallyValid;
print_regions_reshuffled = true;
} else
if (print_object_regions && ! trafos_differ_in_rotation_by_z_and_mirroring_by_xy_only(print_object_regions->trafo_bboxes, model_object_status.print_instances.front().trafo)) {
invalidate();
print_object_regions->clear();

View File

@@ -4194,6 +4194,26 @@ void PrintConfigDef::init_fff_params()
def->mode = comAdvanced;
def->set_default_value(new ConfigOptionBool(false));
def = this->add("mixed_filament_pointillism_pixel_size", coFloat);
def->label = L("Pointillisme pixel size");
def->category = L("Others");
def->tooltip = L("Length of one pointillisme segment along an extrusion path for same-layer pointillisme mode. "
"Set to 0 to use automatic nozzle-based sizing.");
def->sidetext = "mm";
def->min = 0.;
def->mode = comAdvanced;
def->set_default_value(new ConfigOptionFloat(0.0));
def = this->add("mixed_filament_pointillism_line_gap", coFloat);
def->label = L("Pointillisme line gap");
def->category = L("Others");
def->tooltip = L("Optional non-extruded spacing between adjacent pointillisme segments. "
"Increase carefully to improve separation and print quality.");
def->sidetext = "mm";
def->min = 0.;
def->mode = comAdvanced;
def->set_default_value(new ConfigOptionFloat(0.0));
def = this->add("mixed_filament_definitions", coString);
def->label = L("Mixed filament custom definitions");
def->tooltip = L("Serialized custom mixed filament rows.\n\n"

View File

@@ -1358,6 +1358,8 @@ PRINT_CONFIG_CLASS_DERIVED_DEFINE(
((ConfigOptionFloat, mixed_filament_height_upper_bound))
((ConfigOptionInt, mixed_filament_cycle_layers))
((ConfigOptionBool, mixed_filament_advanced_dithering))
((ConfigOptionFloat, mixed_filament_pointillism_pixel_size))
((ConfigOptionFloat, mixed_filament_pointillism_line_gap))
((ConfigOptionString, mixed_filament_definitions))
((ConfigOptionFloat, dithering_z_step_size))
((ConfigOptionBool, dithering_local_z_mode))

View File

@@ -3,6 +3,7 @@
#include <algorithm>
#include <array>
#include <cmath>
#include <cstdlib>
#include <fstream>
#include <iomanip>
#include <numeric>
@@ -1048,6 +1049,554 @@ static std::vector<double> build_local_z_pass_heights(double base_height,
return build_uniform_local_z_pass_heights(base_height, lo, hi);
}
static std::vector<unsigned int> decode_manual_pattern_sequence(const MixedFilament &mf, size_t num_physical)
{
std::vector<unsigned int> sequence;
if (mf.manual_pattern.empty())
return sequence;
sequence.reserve(mf.manual_pattern.size());
for (const char token : mf.manual_pattern) {
unsigned int extruder_id = 0;
if (token == '1')
extruder_id = mf.component_a;
else if (token == '2')
extruder_id = mf.component_b;
else if (token >= '3' && token <= '9')
extruder_id = unsigned(token - '0');
if (extruder_id >= 1 && extruder_id <= num_physical)
sequence.emplace_back(extruder_id);
}
return sequence;
}
static std::vector<unsigned int> decode_gradient_component_ids(const MixedFilament &mf, size_t num_physical)
{
std::vector<unsigned int> ids;
if (mf.gradient_component_ids.empty() || num_physical == 0)
return ids;
bool seen[10] = { false };
ids.reserve(mf.gradient_component_ids.size());
for (const char c : mf.gradient_component_ids) {
if (c < '1' || c > '9')
continue;
const unsigned int id = unsigned(c - '0');
if (id == 0 || id > num_physical || seen[id])
continue;
seen[id] = true;
ids.emplace_back(id);
}
return ids;
}
static std::vector<int> decode_gradient_component_weights(const MixedFilament &mf, size_t expected_components)
{
std::vector<int> out;
if (mf.gradient_component_weights.empty() || expected_components == 0)
return out;
std::string token;
for (const char c : mf.gradient_component_weights) {
if (c >= '0' && c <= '9') {
token.push_back(c);
continue;
}
if (!token.empty()) {
out.emplace_back(std::max(0, std::atoi(token.c_str())));
token.clear();
}
}
if (!token.empty())
out.emplace_back(std::max(0, std::atoi(token.c_str())));
if (out.size() != expected_components)
return {};
int sum = 0;
for (const int v : out)
sum += std::max(0, v);
if (sum <= 0)
return {};
return out;
}
static std::vector<unsigned int> build_weighted_gradient_sequence(const std::vector<unsigned int> &ids,
const std::vector<int> &weights)
{
if (ids.empty())
return {};
std::vector<unsigned int> filtered_ids;
std::vector<int> counts;
filtered_ids.reserve(ids.size());
counts.reserve(ids.size());
for (size_t i = 0; i < ids.size(); ++i) {
const int w = (i < weights.size()) ? std::max(0, weights[i]) : 0;
if (w <= 0)
continue;
filtered_ids.emplace_back(ids[i]);
counts.emplace_back(w);
}
if (filtered_ids.empty()) {
filtered_ids = ids;
counts.assign(ids.size(), 1);
}
int g = 0;
for (const int c : counts)
g = std::gcd(g, std::max(1, c));
if (g > 1) {
for (int &c : counts)
c = std::max(1, c / g);
}
int cycle = std::accumulate(counts.begin(), counts.end(), 0);
constexpr int k_max_cycle = 48;
if (cycle > k_max_cycle) {
const double scale = double(k_max_cycle) / double(cycle);
for (int &c : counts)
c = std::max(1, int(std::round(double(c) * scale)));
cycle = std::accumulate(counts.begin(), counts.end(), 0);
while (cycle > k_max_cycle) {
auto it = std::max_element(counts.begin(), counts.end());
if (it == counts.end() || *it <= 1)
break;
--(*it);
--cycle;
}
}
if (cycle <= 0)
return {};
std::vector<unsigned int> sequence;
sequence.reserve(size_t(cycle));
std::vector<int> emitted(counts.size(), 0);
for (int pos = 0; pos < cycle; ++pos) {
size_t best_idx = 0;
double best_score = -1e9;
for (size_t i = 0; i < counts.size(); ++i) {
const double target = double((pos + 1) * counts[i]) / double(cycle);
const double score = target - double(emitted[i]);
if (score > best_score) {
best_score = score;
best_idx = i;
}
}
++emitted[best_idx];
sequence.emplace_back(filtered_ids[best_idx]);
}
return sequence;
}
static std::vector<unsigned int> pointillism_sequence_for_row(const MixedFilament &mf, size_t num_physical)
{
if (!mf.enabled || num_physical == 0)
return {};
if (mf.distribution_mode != int(MixedFilament::SameLayerPointillisme))
return {};
if (!mf.manual_pattern.empty())
return decode_manual_pattern_sequence(mf, num_physical);
const std::vector<unsigned int> selected_gradient_ids = decode_gradient_component_ids(mf, num_physical);
if (selected_gradient_ids.size() >= 2) {
const std::vector<int> selected_gradient_weights = decode_gradient_component_weights(mf, selected_gradient_ids.size());
const std::vector<unsigned int> weighted_sequence =
build_weighted_gradient_sequence(selected_gradient_ids,
selected_gradient_weights.empty() ? std::vector<int>(selected_gradient_ids.size(), 1) : selected_gradient_weights);
if (!weighted_sequence.empty())
return weighted_sequence;
}
if (mf.component_a < 1 || mf.component_a > num_physical ||
mf.component_b < 1 || mf.component_b > num_physical ||
mf.component_a == mf.component_b)
return {};
int ratio_a = std::max(0, mf.ratio_a);
int ratio_b = std::max(0, mf.ratio_b);
if (ratio_a == 0 && ratio_b == 0)
ratio_a = 1;
if (ratio_a > 0 && ratio_b > 0) {
const int g = std::gcd(ratio_a, ratio_b);
if (g > 1) {
ratio_a /= g;
ratio_b /= g;
}
}
constexpr int k_max_cycle = 24;
if (ratio_a + ratio_b > k_max_cycle) {
const double scale = double(k_max_cycle) / double(ratio_a + ratio_b);
ratio_a = std::max(1, int(std::round(double(ratio_a) * scale)));
ratio_b = std::max(1, int(std::round(double(ratio_b) * scale)));
}
const int cycle = std::max(1, ratio_a + ratio_b);
std::vector<unsigned int> sequence;
sequence.reserve(size_t(cycle));
for (int pos = 0; pos < cycle; ++pos) {
const int b_before = (pos * ratio_b) / cycle;
const int b_after = ((pos + 1) * ratio_b) / cycle;
sequence.emplace_back((b_after > b_before) ? mf.component_b : mf.component_a);
}
bool seen_a = false;
bool seen_b = false;
for (const unsigned int extruder_id : sequence) {
seen_a = seen_a || (extruder_id == mf.component_a);
seen_b = seen_b || (extruder_id == mf.component_b);
if (seen_a && seen_b)
break;
}
if (!seen_a || !seen_b)
return {};
return sequence;
}
static size_t unique_extruder_count(const std::vector<unsigned int> &sequence, size_t num_physical)
{
if (sequence.empty() || num_physical == 0)
return 0;
std::vector<bool> seen(num_physical + 1, false);
size_t unique_count = 0;
for (const unsigned int extruder_id : sequence) {
if (extruder_id == 0 || extruder_id > num_physical)
continue;
if (!seen[extruder_id]) {
seen[extruder_id] = true;
++unique_count;
}
}
return unique_count;
}
static bool split_masks_pointillism_stripes(const ExPolygons &source_masks,
const std::vector<unsigned int> &sequence,
size_t num_physical,
size_t layer_id,
coord_t stripe_pitch,
bool flip_orientation,
std::vector<ExPolygons> &out_by_extruder)
{
if (source_masks.empty() || sequence.empty() || num_physical == 0 || stripe_pitch <= 0)
return false;
const BoundingBox bbox = get_extents(source_masks);
if (!bbox.defined || bbox.min.x() >= bbox.max.x() || bbox.min.y() >= bbox.max.y())
return false;
out_by_extruder.assign(num_physical, ExPolygons());
const size_t slot_count = sequence.size();
const size_t phase = slot_count > 0 ? (layer_id % slot_count) : 0;
auto align_down_to_grid = [stripe_pitch](coord_t value) {
coord_t rem = value % stripe_pitch;
if (rem < 0)
rem += stripe_pitch;
return value - rem;
};
std::vector<Polygons> stripe_polygons_by_slot(slot_count);
const bool vertical_base = (bbox.max.x() - bbox.min.x()) >= (bbox.max.y() - bbox.min.y());
// Alternate stripe orientation every layer so different faces of the model
// receive mixed-color variation instead of long single-direction bands.
const bool layer_alternates = (layer_id & 1) != 0;
bool vertical = layer_alternates ? !vertical_base : vertical_base;
if (flip_orientation)
vertical = !vertical;
if (vertical) {
const coord_t y0 = bbox.min.y();
const coord_t y1 = bbox.max.y();
const coord_t x_start_aligned = align_down_to_grid(bbox.min.x());
size_t stripe_idx = 0;
for (coord_t x = x_start_aligned; x < bbox.max.x(); x += stripe_pitch, ++stripe_idx) {
const coord_t x0 = std::max(x, bbox.min.x());
const coord_t x1 = std::min<coord_t>(x + stripe_pitch, bbox.max.x());
if (x1 <= x0)
continue;
const size_t slot = (stripe_idx + phase) % slot_count;
stripe_polygons_by_slot[slot].emplace_back(BoundingBox(Point(x0, y0), Point(x1, y1)).polygon());
}
} else {
const coord_t x0 = bbox.min.x();
const coord_t x1 = bbox.max.x();
const coord_t y_start_aligned = align_down_to_grid(bbox.min.y());
size_t stripe_idx = 0;
for (coord_t y = y_start_aligned; y < bbox.max.y(); y += stripe_pitch, ++stripe_idx) {
const coord_t y0 = std::max(y, bbox.min.y());
const coord_t y1 = std::min<coord_t>(y + stripe_pitch, bbox.max.y());
if (y1 <= y0)
continue;
const size_t slot = (stripe_idx + phase) % slot_count;
stripe_polygons_by_slot[slot].emplace_back(BoundingBox(Point(x0, y0), Point(x1, y1)).polygon());
}
}
unsigned int fallback_extruder = 0;
for (const unsigned int extruder_id : sequence) {
if (extruder_id >= 1 && extruder_id <= num_physical) {
fallback_extruder = extruder_id;
break;
}
}
if (fallback_extruder == 0)
return false;
for (size_t slot = 0; slot < slot_count; ++slot) {
const unsigned int extruder_id = sequence[slot];
if (extruder_id == 0 || extruder_id > num_physical || stripe_polygons_by_slot[slot].empty())
continue;
ExPolygons clipped = intersection_ex(source_masks, stripe_polygons_by_slot[slot], ApplySafetyOffset::Yes);
if (!clipped.empty())
append(out_by_extruder[extruder_id - 1], std::move(clipped));
}
ExPolygons assigned_union;
for (ExPolygons &masks : out_by_extruder) {
if (masks.size() > 1)
masks = union_ex(masks);
append(assigned_union, masks);
}
if (assigned_union.empty()) {
append(out_by_extruder[fallback_extruder - 1], source_masks);
return true;
}
if (assigned_union.size() > 1)
assigned_union = union_ex(assigned_union);
ExPolygons remainder = diff_ex(source_masks, assigned_union, ApplySafetyOffset::Yes);
if (!remainder.empty()) {
append(out_by_extruder[fallback_extruder - 1], std::move(remainder));
ExPolygons &fallback_masks = out_by_extruder[fallback_extruder - 1];
if (fallback_masks.size() > 1)
fallback_masks = union_ex(fallback_masks);
}
return true;
}
static size_t non_empty_mask_count(const std::vector<ExPolygons> &masks_by_extruder)
{
size_t count = 0;
for (const ExPolygons &masks : masks_by_extruder)
if (!masks.empty())
++count;
return count;
}
template<typename ThrowOnCancel>
static bool apply_pointillism_mixed_segmentation(PrintObject &print_object, std::vector<std::vector<ExPolygons>> &segmentation, ThrowOnCancel throw_on_cancel)
{
const Print *print = print_object.print();
if (print == nullptr || segmentation.empty())
return false;
const PrintConfig &print_cfg = print->config();
const size_t num_physical = print_cfg.filament_colour.size();
if (num_physical < 2)
return false;
const MixedFilamentManager &mixed_mgr = print->mixed_filament_manager();
const auto &mixed_rows = mixed_mgr.mixed_filaments();
if (mixed_rows.empty())
return false;
const size_t num_channels = segmentation.front().size();
if (num_channels <= num_physical)
return false;
const double nozzle = print_cfg.nozzle_diameter.values.empty() ? 0.4 : print_cfg.nozzle_diameter.get_at(0);
// Keep stripe width at or above roughly one printable line to avoid
// non-printable slivers that can get dropped later and create holes.
const double stripe_pitch_mm = std::max(0.25, 1.10 * nozzle);
const coord_t stripe_pitch = std::max<coord_t>(scale_(0.25), scale_(stripe_pitch_mm));
std::vector<std::vector<unsigned int>> same_layer_sequences(mixed_rows.size());
std::vector<bool> same_layer_row_active(mixed_rows.size(), false);
std::vector<size_t> same_layer_row_indices;
for (size_t mixed_idx = 0; mixed_idx < mixed_rows.size(); ++mixed_idx) {
const MixedFilament &mf = mixed_rows[mixed_idx];
if (!mf.enabled || mf.distribution_mode != int(MixedFilament::SameLayerPointillisme))
continue;
same_layer_sequences[mixed_idx] = pointillism_sequence_for_row(mf, num_physical);
if (unique_extruder_count(same_layer_sequences[mixed_idx], num_physical) >= 2) {
same_layer_row_active[mixed_idx] = true;
same_layer_row_indices.emplace_back(mixed_idx);
}
}
auto find_sequence_override = [&](size_t mixed_idx) -> const std::vector<unsigned int> * {
if (mixed_idx >= mixed_rows.size())
return nullptr;
if (same_layer_row_active[mixed_idx])
return &same_layer_sequences[mixed_idx];
const MixedFilament &src = mixed_rows[mixed_idx];
for (size_t idx : same_layer_row_indices) {
if (idx >= mixed_rows.size())
continue;
const MixedFilament &candidate = mixed_rows[idx];
if ((candidate.component_a == src.component_a && candidate.component_b == src.component_b) ||
(candidate.component_a == src.component_b && candidate.component_b == src.component_a))
return &same_layer_sequences[idx];
}
if (same_layer_row_indices.size() == 1)
return &same_layer_sequences[same_layer_row_indices.front()];
return nullptr;
};
size_t same_layer_rows = 0;
for (size_t mixed_idx = 0; mixed_idx < mixed_rows.size(); ++mixed_idx) {
const MixedFilament &mf = mixed_rows[mixed_idx];
if (!same_layer_row_active[mixed_idx])
continue;
const std::vector<unsigned int> &seq = same_layer_sequences[mixed_idx];
const size_t unique = unique_extruder_count(seq, num_physical);
BOOST_LOG_TRIVIAL(debug) << "Same-layer pointillisme row"
<< " mixed_idx=" << mixed_idx
<< " component_a=" << mf.component_a
<< " component_b=" << mf.component_b
<< " mix_b_percent=" << mf.mix_b_percent
<< " manual_pattern_len=" << mf.manual_pattern.size()
<< " gradient_components=" << mf.gradient_component_ids
<< " sequence_len=" << seq.size()
<< " unique_extruders=" << unique;
if (unique >= 2)
++same_layer_rows;
}
size_t transformed_layers = 0;
size_t transformed_states = 0;
size_t transformed_masks = 0;
size_t skipped_states = 0;
size_t retried_states = 0;
size_t weak_split_states = 0;
size_t pair_override_states = 0;
size_t global_override_states = 0;
for (size_t layer_id = 0; layer_id < segmentation.size(); ++layer_id) {
throw_on_cancel();
if (segmentation[layer_id].size() != num_channels) {
++skipped_states;
continue;
}
bool layer_transformed = false;
std::vector<bool> touched_physical(num_physical, false);
for (size_t channel_idx = num_physical; channel_idx < num_channels; ++channel_idx) {
ExPolygons &state_masks = segmentation[layer_id][channel_idx];
if (state_masks.empty())
continue;
const unsigned int state_id = unsigned(channel_idx + 1);
const int mixed_idx = mixed_mgr.mixed_index_from_filament_id(state_id, num_physical);
if (mixed_idx < 0 || size_t(mixed_idx) >= mixed_rows.size()) {
++skipped_states;
continue;
}
const MixedFilament &mf = mixed_rows[size_t(mixed_idx)];
const std::vector<unsigned int> *sequence_ptr = find_sequence_override(size_t(mixed_idx));
if (sequence_ptr == nullptr || sequence_ptr->empty() || unique_extruder_count(*sequence_ptr, num_physical) < 2) {
++skipped_states;
continue;
}
if (!same_layer_row_active[size_t(mixed_idx)]) {
bool pair_match = false;
for (size_t idx : same_layer_row_indices) {
const MixedFilament &candidate = mixed_rows[idx];
if ((candidate.component_a == mf.component_a && candidate.component_b == mf.component_b) ||
(candidate.component_a == mf.component_b && candidate.component_b == mf.component_a)) {
pair_match = true;
break;
}
}
if (pair_match)
++pair_override_states;
else if (same_layer_row_indices.size() == 1)
++global_override_states;
}
std::vector<ExPolygons> split_by_extruder;
if (!split_masks_pointillism_stripes(state_masks, *sequence_ptr, num_physical, layer_id, stripe_pitch, false, split_by_extruder)) {
++skipped_states;
continue;
}
size_t split_unique = non_empty_mask_count(split_by_extruder);
if (split_unique < 2) {
std::vector<ExPolygons> retry_split;
if (split_masks_pointillism_stripes(state_masks, *sequence_ptr, num_physical, layer_id, stripe_pitch, true, retry_split)) {
const size_t retry_unique = non_empty_mask_count(retry_split);
if (retry_unique > split_unique) {
split_by_extruder = std::move(retry_split);
split_unique = retry_unique;
}
++retried_states;
}
}
if (split_unique < 2)
++weak_split_states;
for (size_t extruder_idx = 0; extruder_idx < num_physical; ++extruder_idx) {
if (split_by_extruder[extruder_idx].empty())
continue;
append(segmentation[layer_id][extruder_idx], std::move(split_by_extruder[extruder_idx]));
touched_physical[extruder_idx] = true;
}
transformed_masks += state_masks.size();
state_masks.clear();
layer_transformed = true;
++transformed_states;
}
if (layer_transformed) {
++transformed_layers;
for (size_t extruder_idx = 0; extruder_idx < num_physical; ++extruder_idx) {
if (!touched_physical[extruder_idx] || segmentation[layer_id][extruder_idx].size() <= 1)
continue;
segmentation[layer_id][extruder_idx] = union_ex(segmentation[layer_id][extruder_idx]);
}
}
}
if (transformed_states > 0) {
BOOST_LOG_TRIVIAL(warning) << "Mixed interleaved-stripe segmentation applied"
<< " object=" << (print_object.model_object() ? print_object.model_object()->name : std::string("<unknown>"))
<< " same_layer_rows=" << same_layer_rows
<< " transformed_layers=" << transformed_layers
<< " transformed_states=" << transformed_states
<< " transformed_masks=" << transformed_masks
<< " retried_states=" << retried_states
<< " weak_split_states=" << weak_split_states
<< " pair_override_states=" << pair_override_states
<< " global_override_states=" << global_override_states
<< " stripe_pitch_mm=" << stripe_pitch_mm
<< " skipped_states=" << skipped_states;
return true;
}
if (same_layer_rows > 0) {
BOOST_LOG_TRIVIAL(warning) << "Same-layer pointillisme requested but produced no transformed states"
<< " object=" << (print_object.model_object() ? print_object.model_object()->name : std::string("<unknown>"))
<< " same_layer_rows=" << same_layer_rows
<< " stripe_pitch_mm=" << stripe_pitch_mm
<< " skipped_states=" << skipped_states;
}
return false;
}
static ExPolygons collect_layer_region_slices(const Layer &layer)
{
ExPolygons out;
@@ -1190,6 +1739,22 @@ static void build_local_z_plan(PrintObject &print_object, const std::vector<std:
return;
}
const MixedFilamentManager &mixed_mgr = print->mixed_filament_manager();
const auto &mixed_rows = mixed_mgr.mixed_filaments();
size_t pointillism_rows = 0;
for (const MixedFilament &mf : mixed_rows) {
const std::vector<unsigned int> sequence = pointillism_sequence_for_row(mf, num_physical);
if (unique_extruder_count(sequence, num_physical) >= 2)
++pointillism_rows;
}
if (pointillism_rows > 0) {
BOOST_LOG_TRIVIAL(warning) << "Local-Z plan skipped: interleaved stripe mixed pattern active"
<< " object=" << object_name
<< " interleaved_rows=" << pointillism_rows;
return;
}
BOOST_LOG_TRIVIAL(debug) << "Local-Z plan start"
<< " object=" << object_name
<< " layers=" << print_object.layer_count()
@@ -1199,7 +1764,6 @@ static void build_local_z_plan(PrintObject &print_object, const std::vector<std:
<< " preferred_b=" << preferred_b
<< " physical_filaments=" << num_physical;
const MixedFilamentManager &mixed_mgr = print->mixed_filament_manager();
std::vector<LocalZInterval> intervals;
std::vector<SubLayerPlan> plans;
intervals.reserve(print_object.layer_count());
@@ -1223,7 +1787,6 @@ static void build_local_z_plan(PrintObject &print_object, const std::vector<std:
double locked_gradient_h_a = 0.0;
double locked_gradient_h_b = 0.0;
bool locked_gradient_valid = false;
const auto &mixed_rows = mixed_mgr.mixed_filaments();
int cadence_index = 0;
for (size_t layer_id = 0; layer_id < print_object.layer_count(); ++layer_id) {
@@ -1257,7 +1820,8 @@ static void build_local_z_plan(PrintObject &print_object, const std::vector<std:
const double mixed_area = std::abs(area(state_masks));
if (mixed_area > dominant_mixed_area) {
dominant_mixed_area = mixed_area;
dominant_mixed_idx = state_id > num_physical ? size_t(state_id - num_physical - 1) : size_t(-1);
const int resolved_mixed_idx = mixed_mgr.mixed_index_from_filament_id(state_id, num_physical);
dominant_mixed_idx = resolved_mixed_idx >= 0 ? size_t(resolved_mixed_idx) : size_t(-1);
}
}
}
@@ -1360,12 +1924,12 @@ static void build_local_z_plan(PrintObject &print_object, const std::vector<std:
if (!mixed_mgr.is_mixed(state_id, num_physical))
continue;
++forced_height_resolve_calls;
const size_t mixed_idx = state_id > num_physical ? size_t(state_id - num_physical - 1) : size_t(-1);
if (mixed_idx >= mixed_rows.size() || !mixed_rows[mixed_idx].custom)
const int mixed_idx = mixed_mgr.mixed_index_from_filament_id(state_id, num_physical);
if (mixed_idx < 0 || size_t(mixed_idx) >= mixed_rows.size() || !mixed_rows[size_t(mixed_idx)].custom)
++forced_height_resolve_non_custom_calls;
unsigned int target_extruder = 0;
if (mixed_idx < mixed_rows.size()) {
const MixedFilament &mf = mixed_rows[mixed_idx];
if (mixed_idx >= 0 && size_t(mixed_idx) < mixed_rows.size()) {
const MixedFilament &mf = mixed_rows[size_t(mixed_idx)];
if (mf.component_a > 0 && mf.component_a <= num_physical &&
mf.component_b > 0 && mf.component_b <= num_physical) {
// Enforce strict per-pass alternation inside split local-Z intervals.
@@ -1426,8 +1990,8 @@ static void build_local_z_plan(PrintObject &print_object, const std::vector<std:
if (!mixed_mgr.is_mixed(state_id, num_physical))
continue;
++forced_height_resolve_calls;
const size_t mixed_idx = state_id > num_physical ? size_t(state_id - num_physical - 1) : size_t(-1);
if (mixed_idx >= mixed_rows.size() || !mixed_rows[mixed_idx].custom)
const int mixed_idx = mixed_mgr.mixed_index_from_filament_id(state_id, num_physical);
if (mixed_idx < 0 || size_t(mixed_idx) >= mixed_rows.size() || !mixed_rows[size_t(mixed_idx)].custom)
++forced_height_resolve_non_custom_calls;
const unsigned int target_extruder =
mixed_mgr.resolve(state_id, num_physical, cadence_index, float(plan.print_z), float(plan.flow_height), true);
@@ -1540,6 +2104,8 @@ static inline void apply_mm_segmentation(PrintObject &print_object, std::vector<
by_extruder.assign(num_extruders, ByExtruder());
by_region.assign(layer.region_count(), ByRegion());
bool layer_split = false;
size_t missing_target_regions = 0;
std::vector<int> missing_target_extruders;
for (size_t extruder_id = 0; extruder_id < num_extruders; ++ extruder_id) {
ByExtruder &region = by_extruder[extruder_id];
append(region.expolygons, std::move(segmentation[layer_id][extruder_id]));
@@ -1581,28 +2147,42 @@ static inline void apply_mm_segmentation(PrintObject &print_object, std::vector<
const BoundingBox parent_layer_region_bbox = get_extents(parent_layer_region.slices.surfaces);
bool self_trimmed = false;
int self_extruder_id = -1; // 1-based extruder ID
if (const int cfg_wall = parent_print_region.config().wall_filament.value;
cfg_wall >= 1 && cfg_wall <= int(by_extruder.size()))
self_extruder_id = cfg_wall;
std::vector<bool> assigned_extruder(by_extruder.size(), false);
std::vector<int> alias_to_self_extruders;
for (int extruder_id = 1; extruder_id <= int(by_extruder.size()); ++extruder_id) {
const ByExtruder &segmented = by_extruder[extruder_id - 1];
if (!segmented.bbox.defined || !parent_layer_region_bbox.overlap(segmented.bbox))
continue;
// Find the first target region iterator.
auto it_target_region = std::find_if(it_painted_region_begin, layer_range.painted_regions.cend(), [extruder_id](const auto &painted_region) {
return int(painted_region.extruder_id) >= extruder_id;
// Find the matching target region for this parent and extruder ID.
auto it_target_region = std::find_if(it_painted_region_begin, layer_range.painted_regions.cend(), [&layer_range, &parent_print_region, extruder_id](const auto &painted_region) {
return layer_range.volume_regions[painted_region.parent].region == &parent_print_region &&
int(painted_region.extruder_id) == extruder_id;
});
assert(it_target_region != layer_range.painted_regions.end());
assert(layer_range.volume_regions[it_target_region->parent].region == &parent_print_region && int(it_target_region->extruder_id) == extruder_id);
if (it_target_region == layer_range.painted_regions.cend()) {
++missing_target_regions;
missing_target_extruders.emplace_back(extruder_id);
continue;
}
// Update the beginning PaintedRegion iterator for the next iteration.
it_painted_region_begin = it_target_region;
// FIXME: Don't trim by self, it is not reliable.
if (it_target_region->region == &parent_print_region) {
self_extruder_id = extruder_id;
if (self_extruder_id < 0)
self_extruder_id = extruder_id;
if (extruder_id != self_extruder_id)
alias_to_self_extruders.emplace_back(extruder_id);
continue;
}
assigned_extruder[size_t(extruder_id - 1)] = true;
// Steal from this region.
int target_region_id = it_target_region->region->print_object_region_id();
ExPolygons stolen = intersection_ex(parent_layer_region.slices.surfaces, segmented.expolygons);
@@ -1620,8 +2200,11 @@ static inline void apply_mm_segmentation(PrintObject &print_object, std::vector<
if (!self_trimmed) {
// Trim slices of this LayerRegion with all the MM regions.
Polygons mine = to_polygons(parent_layer_region.slices.surfaces);
for (auto &segmented : by_extruder) {
if (&segmented - by_extruder.data() + 1 != self_extruder_id && segmented.bbox.defined && parent_layer_region_bbox.overlap(segmented.bbox)) {
for (size_t extruder_idx = 0; extruder_idx < by_extruder.size(); ++extruder_idx) {
const ByExtruder &segmented = by_extruder[extruder_idx];
if (!assigned_extruder[extruder_idx])
continue;
if (int(extruder_idx + 1) != self_extruder_id && segmented.bbox.defined && parent_layer_region_bbox.overlap(segmented.bbox)) {
mine = diff(mine, segmented.expolygons);
if (mine.empty())
break;
@@ -1647,6 +2230,41 @@ static inline void apply_mm_segmentation(PrintObject &print_object, std::vector<
}
}
}
if (!alias_to_self_extruders.empty()) {
std::sort(alias_to_self_extruders.begin(), alias_to_self_extruders.end());
alias_to_self_extruders.erase(std::unique(alias_to_self_extruders.begin(), alias_to_self_extruders.end()), alias_to_self_extruders.end());
std::string alias_ids;
for (size_t i = 0; i < alias_to_self_extruders.size(); ++i) {
if (i > 0)
alias_ids += ",";
alias_ids += std::to_string(alias_to_self_extruders[i]);
}
BOOST_LOG_TRIVIAL(warning) << "MM segmentation alias-to-parent channels ignored"
<< " object=" << (print_object.model_object() ? print_object.model_object()->name : std::string("<unknown>"))
<< " layer_id=" << layer_id
<< " parent_region_id=" << parent_print_region.print_object_region_id()
<< " self_extruder_id=" << self_extruder_id
<< " alias_extruders=[" << alias_ids << "]";
}
}
if (missing_target_regions > 0) {
std::sort(missing_target_extruders.begin(), missing_target_extruders.end());
missing_target_extruders.erase(std::unique(missing_target_extruders.begin(), missing_target_extruders.end()), missing_target_extruders.end());
std::string missing_ids;
for (size_t i = 0; i < missing_target_extruders.size(); ++i) {
if (i > 0)
missing_ids += ",";
missing_ids += std::to_string(missing_target_extruders[i]);
}
BOOST_LOG_TRIVIAL(warning) << "MM segmentation missing painted target regions"
<< " object=" << (print_object.model_object() ? print_object.model_object()->name : std::string("<unknown>"))
<< " layer_id=" << layer_id
<< " missing_targets=" << missing_target_regions
<< " missing_extruders=[" << missing_ids << "]"
<< " segmentation_channels=" << num_extruders
<< " painted_regions=" << layer_range.painted_regions.size();
}
// Re-create Surfaces of LayerRegions.
@@ -1856,6 +2474,9 @@ void PrintObject::slice_volumes()
BOOST_LOG_TRIVIAL(debug) << "Slicing volumes - MMU segmentation";
std::vector<std::vector<ExPolygons>> mm_segmentation = multi_material_segmentation_by_painting(*this, [print]() { print->throw_if_canceled(); });
// Same-layer pointillisme is applied in G-code path domain (segment-level assignment),
// not by XY state mask splitting, to avoid boolean-induced voids.
BOOST_LOG_TRIVIAL(info) << "Same-layer pointillisme uses path-domain G-code segmentation";
build_local_z_plan(*this, mm_segmentation, [print]() { print->throw_if_canceled(); });
apply_mm_segmentation(*this, std::move(mm_segmentation), [print]() { print->throw_if_canceled(); });
}