From c414e377a0e82748b32a73c514fbb2868ca56a88 Mon Sep 17 00:00:00 2001 From: Rad Date: Thu, 12 Feb 2026 02:29:34 +0100 Subject: [PATCH] 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. --- src/libslic3r/GCode.cpp | 493 ++++++++++- src/libslic3r/GCode/ToolOrdering.cpp | 9 +- src/libslic3r/MixedFilament.cpp | 389 ++++++++- src/libslic3r/MixedFilament.hpp | 47 +- src/libslic3r/PrintApply.cpp | 129 ++- src/libslic3r/PrintConfig.cpp | 20 + src/libslic3r/PrintConfig.hpp | 2 + src/libslic3r/PrintObjectSlice.cpp | 655 +++++++++++++- src/slic3r/GUI/Plater.cpp | 1213 +++++++++++++++++++++++++- src/slic3r/GUI/Tab.cpp | 4 + 10 files changed, 2889 insertions(+), 72 deletions(-) diff --git a/src/libslic3r/GCode.cpp b/src/libslic3r/GCode.cpp index 73029b7785..4b62879613 100644 --- a/src/libslic3r/GCode.cpp +++ b/src/libslic3r/GCode.cpp @@ -26,6 +26,7 @@ #include #include #include +#include #include #include #include @@ -3440,6 +3441,367 @@ static std::unique_ptr clip_extrusion_collection_for_ return out; } +static std::vector decode_manual_pattern_sequence_for_gcode(const MixedFilament& mf, size_t num_physical) +{ + std::vector 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 decode_gradient_component_ids_for_gcode(const MixedFilament& mf, size_t num_physical) +{ + std::vector 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 decode_gradient_component_weights_for_gcode(const MixedFilament& mf, size_t expected_components) +{ + std::vector 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 build_weighted_gradient_sequence_for_gcode(const std::vector& ids, + const std::vector& weights) +{ + if (ids.empty()) + return {}; + + std::vector filtered_ids; + std::vector 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 sequence; + sequence.reserve(size_t(cycle)); + std::vector 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& sequence, size_t num_physical) +{ + if (sequence.empty() || num_physical == 0) + return 0; + std::vector 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 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 gradient_ids = decode_gradient_component_ids_for_gcode(mf, num_physical); + if (gradient_ids.size() >= 2) { + const std::vector gradient_weights = decode_gradient_component_weights_for_gcode(mf, gradient_ids.size()); + const std::vector weighted = + build_weighted_gradient_sequence_for_gcode(gradient_ids, + gradient_weights.empty() ? std::vector(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 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& sequence, + size_t num_physical, + const double split_length_scaled, + const double split_gap_scaled, + size_t sequence_phase, + std::vector>& 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& dst = out_by_extruder[extruder_id - 1]; + if (!dst) { + dst = std::make_unique(); + 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(entity)) { + split_one_path(*path); + } else if (const auto* multipath = dynamic_cast(entity)) { + for (const ExtrusionPath& path : multipath->paths) + split_one_path(path); + } else if (const auto* loop = dynamic_cast(entity)) { + for (const ExtrusionPath& path : loop->paths) + split_one_path(path); + } + } + + for (const std::unique_ptr& bucket : out_by_extruder) { + if (bucket && !bucket->entities.empty()) + ++out_stats.bucket_count; + } + return out_stats.segment_count > 0; +} + inline std::vector& object_islands_by_extruder( std::map>& 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> by_extruder; bool is_anything_overridden = const_cast(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(scale_(0.10), scale_(pointillism_segment_len_mm)); + const double pointillism_line_gap_scaled = std::max(0.0, scale_(pointillism_line_gap_mm)); + std::map> 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* { + 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 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* pointillism_sequence = + is_anything_overridden ? nullptr : pointillism_sequence_for_filament(configured_filament_id); + if (pointillism_sequence != nullptr) { + std::vector> 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& 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& 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 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(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) diff --git a/src/libslic3r/GCode/ToolOrdering.cpp b/src/libslic3r/GCode/ToolOrdering.cpp index 5dd4f1c578..c4b52f9dd1 100644 --- a/src/libslic3r/GCode/ToolOrdering.cpp +++ b/src/libslic3r/GCode/ToolOrdering.cpp @@ -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(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(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; } } } diff --git a/src/libslic3r/MixedFilament.cpp b/src/libslic3r/MixedFilament.cpp index 75489da3f4..0f0d038c26 100644 --- a/src/libslic3r/MixedFilament.cpp +++ b/src/libslic3r/MixedFilament.cpp @@ -5,6 +5,7 @@ #include #include #include +#include #include #include #include @@ -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(c)) || c == '/' || c == '-' || c == '_' || c == '|' || c == ':' || c == ';'; + return std::isspace(static_cast(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(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 decode_gradient_component_ids(const std::string &components, size_t num_physical) +{ + std::vector 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 parse_gradient_weight_tokens(const std::string &weights) +{ + std::vector 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 normalize_weight_vector_to_percent(const std::vector &weights) +{ + std::vector 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 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 parsed = parse_gradient_weight_tokens(weights); + if (parsed.size() != expected_components) + return std::string(); + std::vector 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 decode_gradient_component_weights(const std::string &weights, size_t expected_components) +{ + if (expected_components == 0) + return {}; + std::vector parsed = parse_gradient_weight_tokens(weights); + if (parsed.size() != expected_components) + return {}; + std::vector normalized = normalize_weight_vector_to_percent(parsed); + int sum = 0; + for (const int v : normalized) + sum += v; + return (sum > 0) ? normalized : std::vector(); +} + +static std::vector build_weighted_gradient_sequence(const std::vector &ids, + const std::vector &weights) +{ + if (ids.empty()) + return {}; + + std::vector filtered_ids; + std::vector 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 sequence; + sequence.reserve(size_t(cycle)); + std::vector 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 gradient_ids = decode_gradient_component_ids(mf.gradient_component_ids, num_physical); + if (!use_simple_mode && gradient_ids.size() >= 3) { + const std::vector gradient_weights = + decode_gradient_component_weights(mf.gradient_component_weights, gradient_ids.size()); + const std::vector gradient_sequence = build_weighted_gradient_sequence( + gradient_ids, gradient_weights.empty() ? std::vector(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 &filament_colours) { for (MixedFilament &mf : m_mixed) { + const std::vector 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 gradient_weights = + decode_gradient_component_weights(mf.gradient_component_weights, gradient_ids.size()); + const std::vector gradient_sequence = + build_weighted_gradient_sequence(gradient_ids, + gradient_weights.empty() ? std::vector(gradient_ids.size(), 1) : gradient_weights); + if (gradient_sequence.empty()) { + mf.display_color = "#26A69A"; + continue; + } + + std::vector 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"; diff --git a/src/libslic3r/MixedFilament.hpp b/src/libslic3r/MixedFilament.hpp index 287ff9249c..bb4f90a176 100644 --- a/src/libslic3r/MixedFilament.hpp +++ b/src/libslic3r/MixedFilament.hpp @@ -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 &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, diff --git a/src/libslic3r/PrintApply.cpp b/src/libslic3r/PrintApply.cpp index c75b822ead..a5da429e3b 100644 --- a/src/libslic3r/PrintApply.cpp +++ b/src/libslic3r/PrintApply.cpp @@ -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 &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 &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("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 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(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(state_idx)); - else + append_same_layer_component_extruders(m_mixed_filament_mgr, + static_cast(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(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(); diff --git a/src/libslic3r/PrintConfig.cpp b/src/libslic3r/PrintConfig.cpp index 85a99d193e..face5fe77a 100644 --- a/src/libslic3r/PrintConfig.cpp +++ b/src/libslic3r/PrintConfig.cpp @@ -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" diff --git a/src/libslic3r/PrintConfig.hpp b/src/libslic3r/PrintConfig.hpp index ca395d3fb6..703879ab83 100644 --- a/src/libslic3r/PrintConfig.hpp +++ b/src/libslic3r/PrintConfig.hpp @@ -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)) diff --git a/src/libslic3r/PrintObjectSlice.cpp b/src/libslic3r/PrintObjectSlice.cpp index d746bcadc4..074c397948 100644 --- a/src/libslic3r/PrintObjectSlice.cpp +++ b/src/libslic3r/PrintObjectSlice.cpp @@ -3,6 +3,7 @@ #include #include #include +#include #include #include #include @@ -1048,6 +1049,554 @@ static std::vector build_local_z_pass_heights(double base_height, return build_uniform_local_z_pass_heights(base_height, lo, hi); } +static std::vector decode_manual_pattern_sequence(const MixedFilament &mf, size_t num_physical) +{ + std::vector 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 decode_gradient_component_ids(const MixedFilament &mf, size_t num_physical) +{ + std::vector 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 decode_gradient_component_weights(const MixedFilament &mf, size_t expected_components) +{ + std::vector 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 build_weighted_gradient_sequence(const std::vector &ids, + const std::vector &weights) +{ + if (ids.empty()) + return {}; + + std::vector filtered_ids; + std::vector 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 sequence; + sequence.reserve(size_t(cycle)); + std::vector 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 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 selected_gradient_ids = decode_gradient_component_ids(mf, num_physical); + if (selected_gradient_ids.size() >= 2) { + const std::vector selected_gradient_weights = decode_gradient_component_weights(mf, selected_gradient_ids.size()); + const std::vector weighted_sequence = + build_weighted_gradient_sequence(selected_gradient_ids, + selected_gradient_weights.empty() ? std::vector(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 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 &sequence, size_t num_physical) +{ + if (sequence.empty() || num_physical == 0) + return 0; + + std::vector 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 &sequence, + size_t num_physical, + size_t layer_id, + coord_t stripe_pitch, + bool flip_orientation, + std::vector &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 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(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(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 &masks_by_extruder) +{ + size_t count = 0; + for (const ExPolygons &masks : masks_by_extruder) + if (!masks.empty()) + ++count; + return count; +} + +template +static bool apply_pointillism_mixed_segmentation(PrintObject &print_object, std::vector> &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(scale_(0.25), scale_(stripe_pitch_mm)); + + std::vector> same_layer_sequences(mixed_rows.size()); + std::vector same_layer_row_active(mixed_rows.size(), false); + std::vector 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 * { + 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 &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 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 *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 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 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("")) + << " 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("")) + << " 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::vectormixed_filament_manager(); + const auto &mixed_rows = mixed_mgr.mixed_filaments(); + size_t pointillism_rows = 0; + for (const MixedFilament &mf : mixed_rows) { + const std::vector 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")) + << " 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("")) + << " 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> 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(); }); } diff --git a/src/slic3r/GUI/Plater.cpp b/src/slic3r/GUI/Plater.cpp index 89fd785b8b..c976d9e032 100644 --- a/src/slic3r/GUI/Plater.cpp +++ b/src/slic3r/GUI/Plater.cpp @@ -4,8 +4,12 @@ #include "common_func/common_func.hpp" #include +#include #include +#include #include +#include +#include #include #include #include @@ -2226,11 +2230,23 @@ public: } int value() const { return m_value; } + bool is_multi_mode() const { return m_multi_mode; } void set_colors(const wxColour &left, const wxColour &right) { m_left = left; m_right = right; + m_multi_mode = false; + m_multi_colors.clear(); + m_multi_weights.clear(); + Refresh(); + } + + void set_multi_preview(const std::vector &corner_colors, const std::vector &weights) + { + m_multi_mode = corner_colors.size() >= 3; + m_multi_colors = corner_colors; + m_multi_weights = weights; Refresh(); } @@ -2273,11 +2289,62 @@ private: dc.Clear(); const wxRect rect = gradient_rect(); - dc.GradientFillLinear(rect, m_left, m_right, wxEAST); + if (m_multi_mode && m_multi_colors.size() >= 3) { + const wxPoint tl(rect.GetLeft(), rect.GetTop()); + const wxPoint tr(rect.GetRight(), rect.GetTop()); + const wxPoint br(rect.GetRight(), rect.GetBottom()); + const wxPoint bl(rect.GetLeft(), rect.GetBottom()); + const wxPoint cc(rect.GetLeft() + rect.GetWidth() / 2, rect.GetTop() + rect.GetHeight() / 2); + auto draw_tri = [&dc](const wxColour &color, const wxPoint &a, const wxPoint &b, const wxPoint &c) { + wxPoint pts[3] = { a, b, c }; + dc.SetPen(*wxTRANSPARENT_PEN); + dc.SetBrush(wxBrush(color)); + dc.DrawPolygon(3, pts); + }; + + if (m_multi_colors.size() >= 4) { + draw_tri(m_multi_colors[0], tl, tr, cc); + draw_tri(m_multi_colors[1], tr, br, cc); + draw_tri(m_multi_colors[2], br, bl, cc); + draw_tri(m_multi_colors[3], bl, tl, cc); + } else { + // 3-color layout: first color occupies one full side, two others on the opposite corners. + draw_tri(m_multi_colors[0], tl, bl, cc); + draw_tri(m_multi_colors[1], tl, tr, cc); + draw_tri(m_multi_colors[2], bl, br, cc); + } + + if (m_multi_weights.size() == m_multi_colors.size()) { + dc.SetTextForeground(wxColour(20, 20, 20)); + dc.SetFont(Label::Body_10); + const int pad = FromDIP(2); + if (m_multi_colors.size() >= 4) { + dc.DrawText(wxString::Format("%d%%", m_multi_weights[0]), rect.GetLeft() + pad, rect.GetTop() + pad); + dc.DrawText(wxString::Format("%d%%", m_multi_weights[1]), rect.GetRight() - FromDIP(28), rect.GetTop() + pad); + dc.DrawText(wxString::Format("%d%%", m_multi_weights[2]), rect.GetRight() - FromDIP(28), rect.GetBottom() - FromDIP(14)); + dc.DrawText(wxString::Format("%d%%", m_multi_weights[3]), rect.GetLeft() + pad, rect.GetBottom() - FromDIP(14)); + } else { + dc.DrawText(wxString::Format("%d%%", m_multi_weights[0]), rect.GetLeft() + pad, rect.GetTop() + rect.GetHeight() / 2 - FromDIP(6)); + dc.DrawText(wxString::Format("%d%%", m_multi_weights[1]), rect.GetRight() - FromDIP(28), rect.GetTop() + pad); + dc.DrawText(wxString::Format("%d%%", m_multi_weights[2]), rect.GetRight() - FromDIP(28), rect.GetBottom() - FromDIP(14)); + } + } + } else { + dc.GradientFillLinear(rect, m_left, m_right, wxEAST); + } dc.SetPen(wxPen(wxColour(170, 170, 170), 1)); dc.SetBrush(*wxTRANSPARENT_BRUSH); dc.DrawRectangle(rect); + if (m_multi_mode) { + dc.SetTextForeground(wxColour(30, 30, 30)); + dc.SetFont(Label::Body_10); + const wxString hint = _L("Click to edit"); + wxSize text_sz = dc.GetTextExtent(hint); + dc.DrawText(hint, rect.GetRight() - text_sz.GetWidth() - FromDIP(4), rect.GetTop() + FromDIP(2)); + return; + } + int marker_x = rect.GetLeft() + (rect.GetWidth() * m_value + 50) / 100; marker_x = std::clamp(marker_x, rect.GetLeft(), rect.GetRight()); dc.SetPen(wxPen(wxColour(255, 255, 255), 3)); @@ -2288,6 +2355,8 @@ private: void on_left_down(wxMouseEvent &evt) { + if (m_multi_mode) + return; if (!HasCapture()) CaptureMouse(); m_dragging = true; @@ -2296,6 +2365,12 @@ private: void on_left_up(wxMouseEvent &evt) { + if (m_multi_mode) { + wxCommandEvent click_evt(wxEVT_BUTTON, GetId()); + click_evt.SetEventObject(this); + ProcessWindowEvent(click_evt); + return; + } if (m_dragging) update_from_x(evt.GetX(), true); m_dragging = false; @@ -2317,9 +2392,479 @@ private: private: wxColour m_left; wxColour m_right; + bool m_multi_mode { false }; + std::vector m_multi_colors; + std::vector m_multi_weights; int m_value {50}; bool m_dragging {false}; }; + +class MixedGradientWeightsDialog : public wxDialog +{ +public: + MixedGradientWeightsDialog(wxWindow *parent, + const std::vector &filament_ids, + const std::vector &palette, + const std::vector &initial_weights) + : wxDialog(parent, wxID_ANY, _L("Gradient Mix Weights"), wxDefaultPosition, wxDefaultSize, + wxDEFAULT_DIALOG_STYLE | wxRESIZE_BORDER) + , m_filament_ids(filament_ids) + { + m_weights = normalize_weights(initial_weights, filament_ids.size()); + m_colors.reserve(filament_ids.size()); + for (size_t i = 0; i < filament_ids.size(); ++i) { + const unsigned int id = filament_ids[i]; + if (id >= 1 && id <= palette.size()) + m_colors.emplace_back(palette[id - 1]); + else + m_colors.emplace_back(wxColour("#26A69A")); + } + if (m_colors.empty()) + m_colors.emplace_back(wxColour("#26A69A")); + + auto *root = new wxBoxSizer(wxVERTICAL); + auto *hint = new wxStaticText(this, wxID_ANY, _L("Pick a point in the gradient map to control multi-filament mix.")); + root->Add(hint, 0, wxEXPAND | wxALL, FromDIP(10)); + + m_canvas = new wxPanel(this, wxID_ANY, wxDefaultPosition, wxSize(FromDIP(240), FromDIP(240)), wxBORDER_SIMPLE); + m_canvas->SetBackgroundStyle(wxBG_STYLE_PAINT); + m_canvas->SetMinSize(wxSize(FromDIP(220), FromDIP(220))); + root->Add(m_canvas, 1, wxEXPAND | wxLEFT | wxRIGHT | wxBOTTOM, FromDIP(10)); + + m_canvas->Bind(wxEVT_PAINT, &MixedGradientWeightsDialog::on_canvas_paint, this); + m_canvas->Bind(wxEVT_LEFT_DOWN, &MixedGradientWeightsDialog::on_canvas_left_down, this); + m_canvas->Bind(wxEVT_LEFT_UP, &MixedGradientWeightsDialog::on_canvas_left_up, this); + m_canvas->Bind(wxEVT_MOTION, &MixedGradientWeightsDialog::on_canvas_motion, this); + m_canvas->Bind(wxEVT_MOUSE_CAPTURE_LOST, &MixedGradientWeightsDialog::on_canvas_capture_lost, this); + + for (size_t i = 0; i < filament_ids.size(); ++i) { + auto *row = new wxBoxSizer(wxHORIZONTAL); + wxPanel *chip = new wxPanel(this, wxID_ANY, wxDefaultPosition, wxSize(FromDIP(18), FromDIP(18)), wxBORDER_SIMPLE); + chip->SetBackgroundColour(m_colors[i]); + row->Add(chip, 0, wxALIGN_CENTER_VERTICAL | wxRIGHT, FromDIP(6)); + row->Add(new wxStaticText(this, wxID_ANY, wxString::Format("F%d", int(filament_ids[i]))), + 0, wxALIGN_CENTER_VERTICAL | wxRIGHT, FromDIP(8)); + auto *label = new wxStaticText(this, wxID_ANY, wxString::Format("%d%%", m_weights[i])); + label->SetFont(Label::Body_12); + row->Add(label, 0, wxALIGN_CENTER_VERTICAL); + root->Add(row, 0, wxEXPAND | wxLEFT | wxRIGHT | wxBOTTOM, FromDIP(8)); + m_weight_labels.emplace_back(label); + } + + root->Add(CreateSeparatedButtonSizer(wxOK | wxCANCEL), 0, wxEXPAND | wxALL, FromDIP(8)); + SetSizerAndFit(root); + SetMinSize(wxSize(FromDIP(380), std::max(GetSize().GetHeight(), FromDIP(460)))); + + initialize_cursor_from_weights(); + update_weight_labels(); + } + + std::vector normalized_weights() const + { + return m_weights; + } + +private: + struct AnchorPoint { + double x { 0.5 }; + double y { 0.5 }; + }; + + static std::vector normalize_weights(const std::vector &weights, size_t n) + { + std::vector out = weights; + if (out.size() != n) + out.assign(n, (n > 0) ? int(100 / n) : 0); + int sum = 0; + for (int &v : out) { + v = std::max(0, v); + sum += v; + } + if (sum <= 0 && n > 0) { + out.assign(n, 0); + out[0] = 100; + return out; + } + std::vector rem(n, 0.); + int assigned = 0; + for (size_t i = 0; i < n; ++i) { + const double exact = 100.0 * double(out[i]) / double(sum); + out[i] = int(std::floor(exact)); + rem[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 < rem.size(); ++i) { + if (rem[i] > best_rem) { + best_rem = rem[i]; + best_idx = i; + } + } + ++out[best_idx]; + rem[best_idx] = 0.0; + --missing; + } + return out; + } + + std::vector anchor_points() const + { + std::vector anchors; + const size_t n = m_colors.size(); + anchors.reserve(n); + if (n == 0) + return anchors; + if (n == 1) { + anchors.emplace_back(AnchorPoint{0.5, 0.5}); + return anchors; + } + if (n == 2) { + anchors.emplace_back(AnchorPoint{0.0, 0.5}); + anchors.emplace_back(AnchorPoint{1.0, 0.5}); + return anchors; + } + if (n == 3) { + anchors.emplace_back(AnchorPoint{0.0, 0.5}); + anchors.emplace_back(AnchorPoint{1.0, 0.0}); + anchors.emplace_back(AnchorPoint{1.0, 1.0}); + return anchors; + } + if (n == 4) { + anchors.emplace_back(AnchorPoint{0.0, 0.0}); + anchors.emplace_back(AnchorPoint{1.0, 0.0}); + anchors.emplace_back(AnchorPoint{1.0, 1.0}); + anchors.emplace_back(AnchorPoint{0.0, 1.0}); + return anchors; + } + + constexpr double k_pi = 3.14159265358979323846; + const double cx = 0.5; + const double cy = 0.5; + const double r = 0.45; + for (size_t i = 0; i < n; ++i) { + const double ang = (2.0 * k_pi * double(i)) / double(n); + anchors.emplace_back(AnchorPoint{cx + r * std::cos(ang), cy + r * std::sin(ang)}); + } + return anchors; + } + + std::vector raw_weights_from_pos(double nx, double ny) const + { + const std::vector anchors = anchor_points(); + std::vector out(anchors.size(), 0.0); + if (anchors.empty()) + return out; + + constexpr double eps = 1e-8; + size_t exact_idx = size_t(-1); + for (size_t i = 0; i < anchors.size(); ++i) { + const double dx = nx - anchors[i].x; + const double dy = ny - anchors[i].y; + const double d2 = dx * dx + dy * dy; + if (d2 <= eps) { + exact_idx = i; + break; + } + out[i] = 1.0 / std::max(1e-6, d2); + } + if (exact_idx != size_t(-1)) { + std::fill(out.begin(), out.end(), 0.0); + out[exact_idx] = 1.0; + return out; + } + + double sum = 0.0; + for (double v : out) + sum += v; + if (sum <= 0.0) { + out.assign(out.size(), 0.0); + out[0] = 1.0; + return out; + } + for (double &v : out) + v /= sum; + return out; + } + + std::vector normalized_weights_from_pos(double nx, double ny) const + { + std::vector raw; + const std::vector w = raw_weights_from_pos(nx, ny); + raw.reserve(w.size()); + for (double v : w) + raw.emplace_back(std::max(0, int(std::lround(v * 100.0)))); + return normalize_weights(raw, w.size()); + } + + wxColour blended_color(const std::vector &weights) const + { + if (weights.empty() || m_colors.empty()) + return wxColour("#26A69A"); + double r = 0.0; + double g = 0.0; + double b = 0.0; + for (size_t i = 0; i < weights.size() && i < m_colors.size(); ++i) { + r += weights[i] * double(m_colors[i].Red()); + g += weights[i] * double(m_colors[i].Green()); + b += weights[i] * double(m_colors[i].Blue()); + } + return wxColour(std::clamp(int(std::lround(r)), 0, 255), + std::clamp(int(std::lround(g)), 0, 255), + std::clamp(int(std::lround(b)), 0, 255)); + } + + wxRect canvas_rect() const + { + if (!m_canvas) + return wxRect(0, 0, 1, 1); + const wxSize sz = m_canvas->GetClientSize(); + return wxRect(0, 0, std::max(1, sz.GetWidth()), std::max(1, sz.GetHeight())); + } + + void set_cursor_from_mouse(const wxMouseEvent &evt) + { + const wxRect rect = canvas_rect(); + const int w = std::max(1, rect.GetWidth() - 1); + const int h = std::max(1, rect.GetHeight() - 1); + m_cursor_x = std::clamp(double(evt.GetX() - rect.GetLeft()) / double(w), 0.0, 1.0); + m_cursor_y = std::clamp(double(evt.GetY() - rect.GetTop()) / double(h), 0.0, 1.0); + m_weights = normalized_weights_from_pos(m_cursor_x, m_cursor_y); + update_weight_labels(); + if (m_canvas) + m_canvas->Refresh(); + } + + void update_weight_labels() + { + for (size_t i = 0; i < m_weight_labels.size() && i < m_weights.size(); ++i) { + if (m_weight_labels[i]) + m_weight_labels[i]->SetLabel(wxString::Format("%d%%", m_weights[i])); + } + Layout(); + } + + void initialize_cursor_from_weights() + { + if (m_weights.empty()) { + m_cursor_x = 0.5; + m_cursor_y = 0.5; + return; + } + double best_x = 0.5; + double best_y = 0.5; + double best_err = std::numeric_limits::max(); + constexpr int grid = 80; + for (int yi = 0; yi <= grid; ++yi) { + const double ny = double(yi) / double(grid); + for (int xi = 0; xi <= grid; ++xi) { + const double nx = double(xi) / double(grid); + const std::vector probe = normalized_weights_from_pos(nx, ny); + if (probe.size() != m_weights.size()) + continue; + double err = 0.0; + for (size_t i = 0; i < probe.size(); ++i) { + const double d = double(probe[i] - m_weights[i]); + err += d * d; + } + if (err < best_err) { + best_err = err; + best_x = nx; + best_y = ny; + } + } + } + m_cursor_x = best_x; + m_cursor_y = best_y; + m_weights = normalized_weights_from_pos(m_cursor_x, m_cursor_y); + } + + void on_canvas_paint(wxPaintEvent &) + { + if (!m_canvas) + return; + wxAutoBufferedPaintDC dc(m_canvas); + dc.SetBackground(wxBrush(m_canvas->GetBackgroundColour())); + dc.Clear(); + + const wxRect rect = canvas_rect(); + const int w = rect.GetWidth(); + const int h = rect.GetHeight(); + if (w <= 0 || h <= 0) + return; + + wxImage img(w, h); + unsigned char *data = img.GetData(); + if (data) { + for (int y = 0; y < h; ++y) { + const double ny = (h > 1) ? double(y) / double(h - 1) : 0.5; + for (int x = 0; x < w; ++x) { + const double nx = (w > 1) ? double(x) / double(w - 1) : 0.5; + const std::vector raw = raw_weights_from_pos(nx, ny); + const wxColour c = blended_color(raw); + const int idx = (y * w + x) * 3; + data[idx + 0] = c.Red(); + data[idx + 1] = c.Green(); + data[idx + 2] = c.Blue(); + } + } + } + dc.DrawBitmap(wxBitmap(img), rect.GetLeft(), rect.GetTop(), false); + + dc.SetPen(wxPen(wxColour(160, 160, 160), 1)); + dc.SetBrush(*wxTRANSPARENT_BRUSH); + dc.DrawRectangle(rect); + + const auto anchors = anchor_points(); + for (size_t i = 0; i < anchors.size() && i < m_colors.size(); ++i) { + const int ax = rect.GetLeft() + int(std::lround(anchors[i].x * double(std::max(1, w - 1)))); + const int ay = rect.GetTop() + int(std::lround(anchors[i].y * double(std::max(1, h - 1)))); + dc.SetPen(wxPen(wxColour(30, 30, 30), 1)); + dc.SetBrush(wxBrush(m_colors[i])); + dc.DrawCircle(wxPoint(ax, ay), FromDIP(4)); + } + + const int cx = rect.GetLeft() + int(std::lround(m_cursor_x * double(std::max(1, w - 1)))); + const int cy = rect.GetTop() + int(std::lround(m_cursor_y * double(std::max(1, h - 1)))); + dc.SetPen(wxPen(wxColour(255, 255, 255), 3)); + dc.SetBrush(*wxTRANSPARENT_BRUSH); + dc.DrawCircle(wxPoint(cx, cy), FromDIP(7)); + dc.SetPen(wxPen(wxColour(30, 30, 30), 1)); + dc.DrawCircle(wxPoint(cx, cy), FromDIP(7)); + } + + void on_canvas_left_down(wxMouseEvent &evt) + { + if (!m_canvas) + return; + if (!m_canvas->HasCapture()) + m_canvas->CaptureMouse(); + m_dragging = true; + set_cursor_from_mouse(evt); + } + + void on_canvas_left_up(wxMouseEvent &evt) + { + if (!m_canvas) + return; + if (m_dragging) + set_cursor_from_mouse(evt); + m_dragging = false; + if (m_canvas->HasCapture()) + m_canvas->ReleaseMouse(); + } + + void on_canvas_motion(wxMouseEvent &evt) + { + if (m_dragging && evt.LeftIsDown()) + set_cursor_from_mouse(evt); + } + + void on_canvas_capture_lost(wxMouseCaptureLostEvent &) + { + m_dragging = false; + } + +private: + std::vector m_filament_ids; + wxPanel *m_canvas { nullptr }; + std::vector m_colors; + std::vector m_weights; + std::vector m_weight_labels; + double m_cursor_x { 0.5 }; + double m_cursor_y { 0.5 }; + bool m_dragging { false }; +}; + +class MixedMixPreview : public wxPanel +{ +public: + explicit MixedMixPreview(wxWindow *parent) + : wxPanel(parent, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxBORDER_NONE) + { + SetBackgroundStyle(wxBG_STYLE_PAINT); + SetMinSize(wxSize(FromDIP(132), FromDIP(24))); + Bind(wxEVT_PAINT, &MixedMixPreview::on_paint, this); + } + + void set_data(const std::vector &palette, + const std::vector &sequence, + bool same_layer_mode, + const wxColour &fallback) + { + m_palette = palette; + m_sequence = sequence; + m_same_layer = same_layer_mode; + m_fallback = fallback; + Refresh(); + } + +private: + wxRect preview_rect() const + { + const int margin_x = FromDIP(1); + const int margin_y = FromDIP(1); + const wxSize sz = GetClientSize(); + return wxRect(margin_x, margin_y, std::max(1, sz.GetWidth() - margin_x * 2), std::max(1, sz.GetHeight() - margin_y * 2)); + } + + wxColour color_for_extruder(unsigned int extruder_id) const + { + if (extruder_id >= 1 && extruder_id <= m_palette.size()) + return m_palette[extruder_id - 1]; + return m_fallback; + } + + void on_paint(wxPaintEvent &) + { + wxAutoBufferedPaintDC dc(this); + dc.SetBackground(wxBrush(GetBackgroundColour())); + dc.Clear(); + + const wxRect rect = preview_rect(); + dc.SetPen(*wxTRANSPARENT_PEN); + dc.SetBrush(wxBrush(m_fallback)); + dc.DrawRectangle(rect); + + if (!m_sequence.empty()) { + if (m_same_layer) { + // Same-layer preview: full-height stripe lines. + const int stripes = 24; + const int stripe_w = std::max(1, rect.GetWidth() / stripes); + const size_t seq_len = m_sequence.size(); + for (int s = 0; s < stripes; ++s) { + const size_t idx = size_t(s % int(seq_len)); + dc.SetBrush(wxBrush(color_for_extruder(m_sequence[idx]))); + const int x = rect.GetLeft() + s * stripe_w; + const int w = (s == stripes - 1) ? (rect.GetRight() - x + 1) : stripe_w; + dc.DrawRectangle(x, rect.GetTop(), std::max(1, w), rect.GetHeight()); + } + } else { + const int bars = std::min(24, std::max(1, int(m_sequence.size()))); + const int bar_w = std::max(1, rect.GetWidth() / bars); + for (int i = 0; i < bars; ++i) { + const unsigned int extruder_id = m_sequence[size_t(i) % m_sequence.size()]; + dc.SetBrush(wxBrush(color_for_extruder(extruder_id))); + const int x = rect.GetLeft() + i * bar_w; + const int w = (i == bars - 1) ? (rect.GetRight() - x + 1) : bar_w; + dc.DrawRectangle(x, rect.GetTop(), std::max(1, w), rect.GetHeight()); + } + } + } + + dc.SetPen(wxPen(wxColour(170, 170, 170), 1)); + dc.SetBrush(*wxTRANSPARENT_BRUSH); + dc.DrawRectangle(rect); + } + +private: + std::vector m_palette; + std::vector m_sequence; + bool m_same_layer { false }; + wxColour m_fallback { wxColour(38, 166, 154) }; +}; } // namespace void Sidebar::update_mixed_filament_panel() @@ -2459,12 +3004,256 @@ void Sidebar::update_mixed_filament_panel() if (wxGetApp().mainframe) wxGetApp().mainframe->on_config_changed(print_cfg); }; + auto decode_gradient_ids = [num_physical](const std::string &encoded) { + std::vector ids; + if (encoded.empty() || num_physical == 0) + return ids; + bool seen[10] = { false }; + for (const char c : encoded) { + 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; + }; + auto encode_gradient_ids = [num_physical](const std::vector &ids) { + std::string encoded; + bool seen[10] = { false }; + for (const unsigned int id : ids) { + if (id == 0 || id > num_physical || id > 9 || seen[id]) + continue; + seen[id] = true; + encoded.push_back(char('0' + id)); + } + return encoded; + }; + auto decode_gradient_weights = [](const std::string &encoded, size_t expected_count) { + std::vector out; + if (encoded.empty() || expected_count == 0) + return out; + std::string token; + for (const char c : encoded) { + 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_count) + out.clear(); + return out; + }; + auto normalize_gradient_weights = [](const std::vector &weights, size_t n) { + std::vector out = weights; + if (out.size() != n) + out.assign(n, (n > 0) ? int(100 / n) : 0); + int sum = 0; + for (int &v : out) { + v = std::max(0, v); + sum += v; + } + if (sum <= 0 && n > 0) { + out.assign(n, 0); + out[0] = 100; + return out; + } + std::vector rem(n, 0.); + int assigned = 0; + for (size_t i = 0; i < n; ++i) { + const double exact = 100.0 * double(out[i]) / double(sum); + out[i] = int(std::floor(exact)); + rem[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 < rem.size(); ++i) { + if (rem[i] > best_rem) { + best_rem = rem[i]; + best_idx = i; + } + } + ++out[best_idx]; + rem[best_idx] = 0.0; + --missing; + } + return out; + }; + auto encode_gradient_weights = [](const std::vector &weights) { + std::ostringstream ss; + for (size_t i = 0; i < weights.size(); ++i) { + if (i > 0) + ss << '/'; + ss << std::max(0, weights[i]); + } + return ss.str(); + }; + auto build_weighted_multi_sequence = [normalize_gradient_weights](const std::vector &ids, const std::vector &weights) { + if (ids.empty()) + return std::vector(); + std::vector normalized = normalize_gradient_weights(weights, ids.size()); + std::vector sequence; + int total = 0; + for (int w : normalized) + total += std::max(0, w); + if (total <= 0) + return std::vector(ids.begin(), ids.end()); + constexpr int k_cycle = 48; + std::vector counts; + counts.reserve(normalized.size()); + for (int w : normalized) + counts.emplace_back(std::max(1, int(std::round((double(w) / 100.0) * k_cycle)))); + int cycle = std::accumulate(counts.begin(), counts.end(), 0); + while (cycle > k_cycle) { + auto it = std::max_element(counts.begin(), counts.end()); + if (it == counts.end() || *it <= 1) + break; + --(*it); + --cycle; + } + sequence.reserve(size_t(cycle)); + std::vector 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(std::max(1, cycle)); + const double score = target - double(emitted[i]); + if (score > best_score) { + best_score = score; + best_idx = i; + } + } + ++emitted[best_idx]; + sequence.emplace_back(ids[best_idx]); + } + if (sequence.empty()) + sequence = ids; + return sequence; + }; + auto decode_manual_pattern_ids = [num_physical](const std::string &pattern, unsigned int component_a, unsigned int component_b) { + std::vector sequence; + if (num_physical == 0) + return sequence; + const std::string normalized = MixedFilamentManager::normalize_manual_pattern(pattern); + sequence.reserve(normalized.size()); + for (const char token : normalized) { + unsigned int extruder_id = 0; + if (token == '1') + extruder_id = component_a; + else if (token == '2') + extruder_id = 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; + }; + auto build_weighted_pair_sequence = [](unsigned int component_a, unsigned int component_b, int mix_b_percent) { + std::vector sequence; + const int b_percent = std::clamp(mix_b_percent, 0, 100); + int ratio_a = std::max(1, 100 - b_percent); + int ratio_b = std::max(1, b_percent); + 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); + 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) ? component_b : component_a); + } + return sequence; + }; + auto summarize_sequence = [num_physical](const std::vector &sequence) { + if (sequence.empty() || num_physical == 0) + return std::string(); + std::vector counts(num_physical + 1, size_t(0)); + size_t total = 0; + for (const unsigned int id : sequence) { + if (id == 0 || id > num_physical) + continue; + ++counts[id]; + ++total; + } + if (total == 0) + return std::string(); + std::ostringstream ss; + bool first = true; + for (size_t id = 1; id <= num_physical; ++id) { + if (counts[id] == 0) + continue; + const int pct = int(std::lround(100.0 * double(counts[id]) / double(total))); + if (!first) + ss << " "; + first = false; + ss << "F" << id << ":" << pct << "%"; + } + return ss.str(); + }; + auto blend_from_sequence = [num_physical](const std::vector &colors, const std::vector &sequence, const std::string &fallback) { + if (colors.empty() || sequence.empty() || num_physical == 0) + return fallback; + std::vector counts(num_physical + 1, size_t(0)); + size_t total = 0; + for (const unsigned int id : sequence) { + if (id == 0 || id > num_physical) + continue; + ++counts[id]; + ++total; + } + if (total == 0) + return fallback; + + unsigned int first_id = 0; + for (size_t id = 1; id <= num_physical; ++id) { + if (counts[id] > 0) { + first_id = unsigned(id); + break; + } + } + if (first_id == 0 || first_id > colors.size()) + return fallback; + + std::string blended = colors[first_id - 1]; + int acc = int(counts[first_id]); + for (size_t id = size_t(first_id + 1); id <= num_physical; ++id) { + if (counts[id] == 0 || id > colors.size()) + continue; + blended = MixedFilamentManager::blend_color(blended, colors[id - 1], acc, int(counts[id])); + acc += int(counts[id]); + } + return blended; + }; const bool height_weighted_mode = get_mixed_mode(false); int gradient_mode = height_weighted_mode ? 1 : 0; float lower_bound = std::max(0.01f, get_mixed_float("mixed_filament_height_lower_bound", 0.04f)); float upper_bound = std::max(lower_bound, get_mixed_float("mixed_filament_height_upper_bound", 0.16f)); int cycle_layers = std::max(2, get_mixed_int("mixed_filament_cycle_layers", 4)); + float pointillism_pixel_size = std::max(0.f, get_mixed_float("mixed_filament_pointillism_pixel_size", 0.f)); + float pointillism_line_gap = std::max(0.f, get_mixed_float("mixed_filament_pointillism_line_gap", 0.f)); bool advanced_dithering = get_mixed_bool("mixed_filament_advanced_dithering", false); const std::string mixed_definitions = get_mixed_string("mixed_filament_definitions"); @@ -2482,6 +3271,8 @@ void Sidebar::update_mixed_filament_panel() set_mixed_float("mixed_filament_height_lower_bound", lower_bound); set_mixed_float("mixed_filament_height_upper_bound", upper_bound); set_mixed_int("mixed_filament_cycle_layers", cycle_layers); + set_mixed_float("mixed_filament_pointillism_pixel_size", pointillism_pixel_size); + set_mixed_float("mixed_filament_pointillism_line_gap", pointillism_line_gap); set_mixed_string("mixed_filament_definitions", mixed_mgr.serialize_custom_entries()); } @@ -2504,11 +3295,16 @@ void Sidebar::update_mixed_filament_panel() mgr.add_custom_filament(1, 2, 50, physical_colors); if (!mgr.mixed_filaments().empty()) { MixedFilament &row = mgr.mixed_filaments().back(); + row.distribution_mode = int(MixedFilament::Simple); if (pattern_row) { row.manual_pattern = "12"; row.mix_b_percent = 50; + row.pointillism_all_filaments = false; + row.gradient_component_ids.clear(); } else { row.manual_pattern.clear(); + row.pointillism_all_filaments = false; + row.gradient_component_ids.clear(); } } @@ -2579,6 +3375,18 @@ void Sidebar::update_mixed_filament_panel() auto *cycle_spin = new wxSpinCtrl(settings_row, wxID_ANY, wxEmptyString, wxDefaultPosition, wxSize(FromDIP(56), -1), wxSP_ARROW_KEYS, 2, 32, cycle_layers); settings_sizer->Add(cycle_spin, 0, wxALIGN_CENTER_VERTICAL | wxLEFT | wxRIGHT, FromDIP(3)); + + settings_sizer->Add(new wxStaticText(settings_row, wxID_ANY, _L("Pixel")), 0, wxALIGN_CENTER_VERTICAL | wxLEFT, FromDIP(6)); + auto *pixel_spin = new wxSpinCtrlDouble(settings_row, wxID_ANY, wxEmptyString, wxDefaultPosition, + wxSize(FromDIP(70), -1), wxSP_ARROW_KEYS, 0.0, 10.0, pointillism_pixel_size, 0.01); + pixel_spin->SetToolTip(_L("Pointillisme segment length in mm. 0 means automatic nozzle-based sizing.")); + settings_sizer->Add(pixel_spin, 0, wxALIGN_CENTER_VERTICAL | wxLEFT, FromDIP(3)); + + settings_sizer->Add(new wxStaticText(settings_row, wxID_ANY, _L("Gap")), 0, wxALIGN_CENTER_VERTICAL | wxLEFT, FromDIP(6)); + auto *gap_spin = new wxSpinCtrlDouble(settings_row, wxID_ANY, wxEmptyString, wxDefaultPosition, + wxSize(FromDIP(70), -1), wxSP_ARROW_KEYS, 0.0, 10.0, pointillism_line_gap, 0.01); + gap_spin->SetToolTip(_L("Optional spacing between pointillisme line segments (mm).")); + settings_sizer->Add(gap_spin, 0, wxALIGN_CENTER_VERTICAL | wxLEFT | wxRIGHT, FromDIP(3)); settings_row->SetSizer(settings_sizer); p->m_sizer_mixed_filaments->Add(settings_row, 0, wxEXPAND | wxLEFT | wxRIGHT, FromDIP(4)); p->m_sizer_mixed_filaments->AddSpacer(FromDIP(2)); @@ -2590,18 +3398,27 @@ void Sidebar::update_mixed_filament_panel() auto *rows_sizer = new wxBoxSizer(wxVERTICAL); rows_scroller->SetSizer(rows_sizer); - auto apply_settings = [this, preset_bundle, get_mixed_mode, get_mixed_bool, lower_spin, upper_spin, cycle_spin, set_mixed_int, set_mixed_float, set_mixed_string, notify_mixed_change]() { + auto apply_settings = [this, preset_bundle, get_mixed_mode, get_mixed_bool, lower_spin, upper_spin, cycle_spin, pixel_spin, gap_spin, + set_mixed_int, set_mixed_float, set_mixed_string, notify_mixed_change]() { const int mode = get_mixed_mode(false) ? 1 : 0; const bool advanced = get_mixed_bool("mixed_filament_advanced_dithering", false); float lo = std::max(0.01f, float(lower_spin->GetValue())); float hi = std::max(lo, float(upper_spin->GetValue())); int cycle = std::max(2, cycle_spin->GetValue()); + float pixel = std::max(0.f, float(pixel_spin->GetValue())); + float gap = std::max(0.f, float(gap_spin->GetValue())); + if (pixel > 1e-6f) + gap = std::min(gap, pixel * 0.90f); if (std::abs(float(upper_spin->GetValue()) - hi) > 1e-6f) upper_spin->SetValue(hi); + if (std::abs(float(gap_spin->GetValue()) - gap) > 1e-6f) + gap_spin->SetValue(gap); set_mixed_float("mixed_filament_height_lower_bound", lo); set_mixed_float("mixed_filament_height_upper_bound", hi); set_mixed_int("mixed_filament_cycle_layers", cycle); + set_mixed_float("mixed_filament_pointillism_pixel_size", pixel); + set_mixed_float("mixed_filament_pointillism_line_gap", gap); auto &mgr = preset_bundle->mixed_filaments; mgr.apply_gradient_settings(mode, lo, hi, cycle, advanced); @@ -2616,15 +3433,23 @@ void Sidebar::update_mixed_filament_panel() lower_spin->Bind(wxEVT_SPINCTRLDOUBLE, [apply_settings](wxSpinDoubleEvent &) { apply_settings(); }); upper_spin->Bind(wxEVT_SPINCTRLDOUBLE, [apply_settings](wxSpinDoubleEvent &) { apply_settings(); }); cycle_spin->Bind(wxEVT_SPINCTRL, [apply_settings](wxSpinEvent &) { apply_settings(); }); + pixel_spin->Bind(wxEVT_SPINCTRLDOUBLE, [apply_settings](wxSpinDoubleEvent &) { apply_settings(); }); + gap_spin->Bind(wxEVT_SPINCTRLDOUBLE, [apply_settings](wxSpinDoubleEvent &) { apply_settings(); }); lower_spin->Bind(wxEVT_TEXT, [apply_settings](wxCommandEvent &) { apply_settings(); }); upper_spin->Bind(wxEVT_TEXT, [apply_settings](wxCommandEvent &) { apply_settings(); }); cycle_spin->Bind(wxEVT_TEXT, [apply_settings](wxCommandEvent &) { apply_settings(); }); + pixel_spin->Bind(wxEVT_TEXT, [apply_settings](wxCommandEvent &) { apply_settings(); }); + gap_spin->Bind(wxEVT_TEXT, [apply_settings](wxCommandEvent &) { apply_settings(); }); lower_spin->Bind(wxEVT_TEXT_ENTER, [apply_settings](wxCommandEvent &) { apply_settings(); }); upper_spin->Bind(wxEVT_TEXT_ENTER, [apply_settings](wxCommandEvent &) { apply_settings(); }); cycle_spin->Bind(wxEVT_TEXT_ENTER, [apply_settings](wxCommandEvent &) { apply_settings(); }); + pixel_spin->Bind(wxEVT_TEXT_ENTER, [apply_settings](wxCommandEvent &) { apply_settings(); }); + gap_spin->Bind(wxEVT_TEXT_ENTER, [apply_settings](wxCommandEvent &) { apply_settings(); }); lower_spin->Bind(wxEVT_KILL_FOCUS, [apply_settings](wxFocusEvent &evt) { apply_settings(); evt.Skip(); }); upper_spin->Bind(wxEVT_KILL_FOCUS, [apply_settings](wxFocusEvent &evt) { apply_settings(); evt.Skip(); }); cycle_spin->Bind(wxEVT_KILL_FOCUS, [apply_settings](wxFocusEvent &evt) { apply_settings(); evt.Skip(); }); + pixel_spin->Bind(wxEVT_KILL_FOCUS, [apply_settings](wxFocusEvent &evt) { apply_settings(); evt.Skip(); }); + gap_spin->Bind(wxEVT_KILL_FOCUS, [apply_settings](wxFocusEvent &evt) { apply_settings(); evt.Skip(); }); for (size_t mixed_id = 0; mixed_id < mixed.size(); ++mixed_id) { MixedFilament &mf = mixed[mixed_id]; @@ -2676,16 +3501,94 @@ void Sidebar::update_mixed_filament_panel() MixedGradientSelector *blend_selector = nullptr; wxStaticText *blend_label = nullptr; wxTextCtrl *pattern_ctrl = nullptr; + wxChoice *choice_c = nullptr; + wxChoice *choice_d = nullptr; + wxButton *add_extra_color_btn = nullptr; + wxChoice *distribution_choice = nullptr; + MixedMixPreview *mix_preview = nullptr; + wxStaticText *mix_summary_label = nullptr; + wxChoice *pattern_insert_choice = nullptr; + wxButton *pattern_insert_btn = nullptr; + std::vector pattern_quick_filament_buttons; + auto selected_weight_state = std::make_shared>(); + + const int row_distribution_mode = std::clamp(mf.distribution_mode, + int(MixedFilament::LayerCycle), + int(MixedFilament::Simple)); + wxArrayString distribution_choices; + distribution_choices.Add(_L("Layer cycling")); + distribution_choices.Add(_L("Same-layer pointillisme")); + distribution_choices.Add(_L("Simple")); + auto *distribution_row = new wxBoxSizer(wxHORIZONTAL); + distribution_row->Add(new wxStaticText(row, wxID_ANY, _L("Mode")), 0, wxALIGN_CENTER_VERTICAL | wxRIGHT, FromDIP(6)); + distribution_choice = new wxChoice(row, wxID_ANY, wxDefaultPosition, wxDefaultSize, distribution_choices); + distribution_choice->SetSelection(row_distribution_mode); + distribution_choice->SetToolTip(_L("Choose whether this mixed row alternates by layer or interleaves colors on the same layer.")); + distribution_row->Add(distribution_choice, 1, wxALIGN_CENTER_VERTICAL); + content_sizer->Add(distribution_row, 0, wxEXPAND | wxLEFT | wxTOP, FromDIP(8)); if (pattern_row_mode) { auto *pattern_row = new wxBoxSizer(wxHORIZONTAL); pattern_row->Add(new wxStaticText(row, wxID_ANY, _L("Pattern")), 0, wxALIGN_CENTER_VERTICAL | wxRIGHT, FromDIP(6)); pattern_ctrl = new wxTextCtrl(row, wxID_ANY, from_u8(normalized_pattern), wxDefaultPosition, wxSize(FromDIP(170), -1), wxTE_PROCESS_ENTER); - pattern_ctrl->SetToolTip(_L("Manual repeating pattern. Use 1/2 or A/B, example: 1/1/1/1/2/2/2/2.")); + pattern_ctrl->SetToolTip(_L("Manual repeating pattern. Use 1/2 or A/B for component A/B, " + "and 3..9 for direct physical filament IDs. " + "Example: 1/1/1/1/2/2/2/2 or 1/2/3/4.")); pattern_row->Add(pattern_ctrl, 1, wxALIGN_CENTER_VERTICAL); content_sizer->Add(pattern_row, 0, wxEXPAND | wxLEFT | wxTOP, FromDIP(8)); + + auto *insert_row = new wxBoxSizer(wxHORIZONTAL); + insert_row->Add(new wxStaticText(row, wxID_ANY, _L("Insert")), 0, wxALIGN_CENTER_VERTICAL | wxRIGHT, FromDIP(6)); + pattern_insert_choice = new wxChoice(row, wxID_ANY, wxDefaultPosition, wxDefaultSize, filament_choices); + pattern_insert_choice->SetSelection(component_a - 1); + pattern_insert_choice->SetToolTip(_L("Select a physical filament to append into the pattern.")); + insert_row->Add(pattern_insert_choice, 1, wxALIGN_CENTER_VERTICAL); + pattern_insert_btn = new wxButton(row, wxID_ANY, "+", wxDefaultPosition, wxSize(FromDIP(28), FromDIP(24)), wxBU_EXACTFIT); + pattern_insert_btn->SetToolTip(_L("Append selected filament ID to pattern")); + insert_row->Add(pattern_insert_btn, 0, wxALIGN_CENTER_VERTICAL | wxLEFT, FromDIP(6)); + content_sizer->Add(insert_row, 0, wxEXPAND | wxLEFT | wxTOP, FromDIP(8)); + + auto *quick_row = new wxBoxSizer(wxHORIZONTAL); + quick_row->Add(new wxStaticText(row, wxID_ANY, _L("Filaments")), 0, wxALIGN_CENTER_VERTICAL | wxRIGHT, FromDIP(6)); + auto *quick_buttons = new wxBoxSizer(wxHORIZONTAL); + for (size_t fid = 0; fid < num_physical; ++fid) { + wxButton *btn = new wxButton(row, wxID_ANY, wxString::Format("%d", int(fid + 1)), + wxDefaultPosition, wxSize(FromDIP(24), FromDIP(22)), wxBU_EXACTFIT); + const wxColour chip_color = parse_mixed_color(physical_colors[fid]); + btn->SetBackgroundColour(chip_color); + btn->SetToolTip(wxString::Format(_L("Append filament %d to pattern"), int(fid + 1))); + quick_buttons->Add(btn, 0, wxRIGHT, FromDIP(4)); + pattern_quick_filament_buttons.emplace_back(btn); + } + quick_row->Add(quick_buttons, 1, wxALIGN_CENTER_VERTICAL); + content_sizer->Add(quick_row, 0, wxEXPAND | wxLEFT | wxTOP, FromDIP(8)); } else { + wxArrayString optional_filament_choices; + optional_filament_choices.Add(_L("None")); + for (size_t i = 0; i < num_physical; ++i) + optional_filament_choices.Add(wxString::Format("Filament %d", int(i + 1))); + + const bool simple_mode = row_distribution_mode == int(MixedFilament::Simple); + std::vector selected_gradient_ids = simple_mode ? std::vector() : decode_gradient_ids(mf.gradient_component_ids); + if (selected_gradient_ids.size() < 3) + selected_gradient_ids.clear(); + if (selected_gradient_ids.empty()) { + selected_gradient_ids.emplace_back(unsigned(component_a)); + if (component_b != component_a) + selected_gradient_ids.emplace_back(unsigned(component_b)); + } + const bool multi_gradient_mode = selected_gradient_ids.size() >= 3; + int selection_c = 0; + int selection_d = 0; + if (selected_gradient_ids.size() >= 3) + selection_c = int(selected_gradient_ids[2]); + if (selected_gradient_ids.size() >= 4) + selection_d = int(selected_gradient_ids[3]); + *selected_weight_state = normalize_gradient_weights( + decode_gradient_weights(mf.gradient_component_weights, selected_gradient_ids.size()), + selected_gradient_ids.size()); + wxColour color_a = parse_mixed_color(physical_colors[size_t(component_a - 1)]); wxColour color_b = parse_mixed_color(physical_colors[size_t(component_b - 1)]); blend_selector = new MixedGradientSelector(row, color_a, color_b, std::clamp(mf.mix_b_percent, 0, 100)); @@ -2693,16 +3596,97 @@ void Sidebar::update_mixed_filament_panel() blend_row->Add(blend_selector, 1, wxEXPAND); content_sizer->Add(blend_row, 0, wxEXPAND | wxLEFT | wxTOP, FromDIP(8)); - blend_label = new wxStaticText(row, wxID_ANY, wxString::Format("%d%%/%d%%", - 100 - std::clamp(mf.mix_b_percent, 0, 100), - std::clamp(mf.mix_b_percent, 0, 100))); + const bool same_layer_mode = row_distribution_mode == int(MixedFilament::SameLayerPointillisme); + blend_label = new wxStaticText(row, wxID_ANY, multi_gradient_mode ? + wxString::Format(same_layer_mode ? _L("%d-color pointillisme") : _L("%d-color layer cycle"), + int(selected_gradient_ids.size())) : + wxString::Format(simple_mode ? _L("Simple %d%%/%d%%") : + (same_layer_mode ? _L("Pointillisme %d%%/%d%%") : _L("%d%%/%d%%")), + 100 - std::clamp(mf.mix_b_percent, 0, 100), + std::clamp(mf.mix_b_percent, 0, 100))); auto *ratio_row = new wxBoxSizer(wxHORIZONTAL); ratio_row->AddStretchSpacer(1); ratio_row->Add(blend_label, 0, wxALIGN_CENTER_VERTICAL); content_sizer->Add(ratio_row, 0, wxEXPAND | wxLEFT | wxTOP, FromDIP(8)); + + if (num_physical >= 3 && !simple_mode) { + add_extra_color_btn = new wxButton(row, wxID_ANY, "+", wxDefaultPosition, wxSize(FromDIP(24), FromDIP(22)), wxBU_EXACTFIT); + add_extra_color_btn->SetToolTip(_L("Add an extra filament color to this gradient")); + picker_row->Add(add_extra_color_btn, 0, wxALIGN_CENTER_VERTICAL | wxLEFT, FromDIP(6)); + auto *extra_row = new wxBoxSizer(wxHORIZONTAL); + extra_row->Add(new wxStaticText(row, wxID_ANY, _L("Extra colors")), 0, wxALIGN_CENTER_VERTICAL | wxRIGHT, FromDIP(6)); + choice_c = new wxChoice(row, wxID_ANY, wxDefaultPosition, wxDefaultSize, optional_filament_choices); + choice_d = new wxChoice(row, wxID_ANY, wxDefaultPosition, wxDefaultSize, optional_filament_choices); + choice_c->SetSelection(std::clamp(selection_c, 0, int(num_physical))); + choice_d->SetSelection(std::clamp(selection_d, 0, int(num_physical))); + choice_c->SetToolTip(_L("Select a third filament for multi-color gradient mixing.")); + choice_d->SetToolTip(_L("Select a fourth filament for multi-color gradient mixing.")); + extra_row->Add(choice_c, 1, wxALIGN_CENTER_VERTICAL); + extra_row->Add(new wxStaticText(row, wxID_ANY, "+"), 0, wxALIGN_CENTER_VERTICAL | wxLEFT | wxRIGHT, FromDIP(6)); + extra_row->Add(choice_d, 1, wxALIGN_CENTER_VERTICAL); + content_sizer->Add(extra_row, 0, wxEXPAND | wxLEFT | wxTOP, FromDIP(8)); + } + + if (blend_selector) { + std::vector corner_colors; + corner_colors.reserve(selected_gradient_ids.size()); + for (const unsigned int id : selected_gradient_ids) { + if (id >= 1 && id <= physical_colors.size()) + corner_colors.emplace_back(parse_mixed_color(physical_colors[id - 1])); + } + if (!simple_mode && corner_colors.size() >= 3) + blend_selector->set_multi_preview(corner_colors, *selected_weight_state); + } } - auto apply_custom_row = [this, preset_bundle, mixed_id, choice_a, choice_b, blend_selector, blend_label, pattern_ctrl, swatch, num_physical, pattern_row_mode, get_mixed_mode, get_mixed_int, get_mixed_float, get_mixed_bool, set_mixed_string, notify_mixed_change](bool refresh_panel) { + auto *preview_row = new wxBoxSizer(wxHORIZONTAL); + preview_row->Add(new wxStaticText(row, wxID_ANY, _L("Preview")), 0, wxALIGN_CENTER_VERTICAL | wxRIGHT, FromDIP(6)); + mix_preview = new MixedMixPreview(row); + preview_row->Add(mix_preview, 1, wxALIGN_CENTER_VERTICAL); + content_sizer->Add(preview_row, 0, wxEXPAND | wxLEFT | wxTOP, FromDIP(8)); + + mix_summary_label = new wxStaticText(row, wxID_ANY, wxEmptyString); + mix_summary_label->SetForegroundColour(wxColour(90, 90, 90)); + content_sizer->Add(mix_summary_label, 0, wxEXPAND | wxLEFT | wxTOP, FromDIP(8)); + + { + std::vector initial_sequence; + const bool initial_simple_mode = row_distribution_mode == int(MixedFilament::Simple); + if (pattern_row_mode) { + initial_sequence = decode_manual_pattern_ids(normalized_pattern, unsigned(component_a), unsigned(component_b)); + } else { + std::vector initial_gradient_ids = initial_simple_mode ? std::vector() : decode_gradient_ids(mf.gradient_component_ids); + if (initial_gradient_ids.size() >= 3) + initial_sequence = build_weighted_multi_sequence(initial_gradient_ids, *selected_weight_state); + else + initial_sequence = build_weighted_pair_sequence(unsigned(component_a), unsigned(component_b), std::clamp(mf.mix_b_percent, 0, 100)); + } + + if (mix_preview) { + std::vector palette; + palette.reserve(physical_colors.size()); + for (const std::string &hex : physical_colors) + palette.emplace_back(parse_mixed_color(hex)); + mix_preview->set_data(palette, + initial_sequence, + row_distribution_mode == int(MixedFilament::SameLayerPointillisme), + wxColour(mf.display_color)); + } + if (mix_summary_label) { + const std::string summary = summarize_sequence(initial_sequence); + mix_summary_label->SetLabel(summary.empty() ? _L("No mix sequence") : from_u8(summary)); + } + } + + auto apply_custom_row = [this, preset_bundle, mixed_id, choice_a, choice_b, choice_c, choice_d, + add_extra_color_btn, distribution_choice, blend_selector, blend_label, pattern_ctrl, swatch, + mix_preview, mix_summary_label, num_physical, pattern_row_mode, + decode_gradient_ids, encode_gradient_ids, decode_manual_pattern_ids, + decode_gradient_weights, normalize_gradient_weights, encode_gradient_weights, + build_weighted_pair_sequence, build_weighted_multi_sequence, + summarize_sequence, blend_from_sequence, + get_mixed_mode, get_mixed_int, get_mixed_float, get_mixed_bool, + set_mixed_string, notify_mixed_change, selected_weight_state, physical_colors, row](bool refresh_panel) { if (num_physical < 1) return; @@ -2721,6 +3705,15 @@ void Sidebar::update_mixed_filament_panel() MixedFilament &cur = mfs[mixed_id]; cur.component_a = unsigned(a); cur.component_b = unsigned(b); + cur.distribution_mode = distribution_choice ? + std::clamp(distribution_choice->GetSelection(), + int(MixedFilament::LayerCycle), + int(MixedFilament::Simple)) : + int(MixedFilament::Simple); + const bool simple_mode = cur.distribution_mode == int(MixedFilament::Simple); + const bool same_layer_mode = cur.distribution_mode == int(MixedFilament::SameLayerPointillisme); + std::vector preview_sequence; + if (pattern_row_mode) { std::string normalized = MixedFilamentManager::normalize_manual_pattern(into_u8(pattern_ctrl->GetValue())); if (normalized.empty()) @@ -2730,9 +3723,52 @@ void Sidebar::update_mixed_filament_panel() cur.manual_pattern = normalized; const int count_b = int(std::count(normalized.begin(), normalized.end(), '2')); cur.mix_b_percent = std::clamp((100 * count_b + int(normalized.size()) / 2) / std::max(1, int(normalized.size())), 0, 100); + cur.pointillism_all_filaments = false; + cur.gradient_component_ids.clear(); + cur.gradient_component_weights.clear(); + preview_sequence = decode_manual_pattern_ids(cur.manual_pattern, cur.component_a, cur.component_b); } else { + std::vector selected_ids; + selected_ids.reserve(4); + auto add_unique = [&selected_ids](unsigned int id) { + if (id == 0) + return; + if (std::find(selected_ids.begin(), selected_ids.end(), id) == selected_ids.end()) + selected_ids.emplace_back(id); + }; + add_unique(unsigned(a)); + add_unique(unsigned(b)); + if (!simple_mode) { + if (choice_c && choice_c->GetSelection() > 0) + add_unique(unsigned(choice_c->GetSelection())); + if (choice_d && choice_d->GetSelection() > 0) + add_unique(unsigned(choice_d->GetSelection())); + } else { + if (choice_c) + choice_c->SetSelection(0); + if (choice_d) + choice_d->SetSelection(0); + } + const bool multi_gradient_mode = selected_ids.size() >= 3; cur.mix_b_percent = std::clamp(blend_selector ? blend_selector->value() : 50, 0, 100); cur.manual_pattern.clear(); + cur.pointillism_all_filaments = false; + if (multi_gradient_mode) { + const std::vector decoded_weights = + decode_gradient_weights(cur.gradient_component_weights, selected_ids.size()); + if (selected_weight_state->size() != selected_ids.size()) + *selected_weight_state = decoded_weights; + *selected_weight_state = normalize_gradient_weights(*selected_weight_state, selected_ids.size()); + cur.gradient_component_ids = encode_gradient_ids(selected_ids); + cur.gradient_component_weights = encode_gradient_weights(*selected_weight_state); + preview_sequence = build_weighted_multi_sequence(selected_ids, *selected_weight_state); + } else { + cur.gradient_component_ids.clear(); + cur.gradient_component_weights.clear(); + } + preview_sequence = multi_gradient_mode ? + preview_sequence : + build_weighted_pair_sequence(cur.component_a, cur.component_b, cur.mix_b_percent); } cur.custom = true; @@ -2745,9 +3781,50 @@ void Sidebar::update_mixed_filament_panel() if (blend_selector) blend_selector->set_colors(color_a_wx, color_b_wx); - cur.display_color = MixedFilamentManager::blend_color(colors[size_t(a - 1)], colors[size_t(b - 1)], 100 - cur.mix_b_percent, cur.mix_b_percent); - if (blend_label) - blend_label->SetLabel(wxString::Format("%d%%/%d%%", 100 - cur.mix_b_percent, cur.mix_b_percent)); + const std::vector selected_gradient_ids = decode_gradient_ids(cur.gradient_component_ids); + if (preview_sequence.empty()) + preview_sequence = build_weighted_pair_sequence(cur.component_a, cur.component_b, cur.mix_b_percent); + if (blend_selector) { + std::vector corner_colors; + corner_colors.reserve(selected_gradient_ids.size()); + for (const unsigned int id : selected_gradient_ids) { + if (id >= 1 && id <= colors.size()) + corner_colors.emplace_back(parse_mixed_color(colors[id - 1])); + } + if (!simple_mode && corner_colors.size() >= 3) + blend_selector->set_multi_preview(corner_colors, *selected_weight_state); + } + if (selected_gradient_ids.size() >= 3 || !preview_sequence.empty()) { + cur.display_color = blend_from_sequence(colors, preview_sequence, "#26A69A"); + if (blend_label) { + if (selected_gradient_ids.size() >= 3) { + blend_label->SetLabel(wxString::Format(same_layer_mode ? _L("%d-color pointillisme") : _L("%d-color layer cycle"), + int(selected_gradient_ids.size()))); + } else { + blend_label->SetLabel(wxString::Format(simple_mode ? _L("Simple %d%%/%d%%") : + (same_layer_mode ? _L("Pointillisme %d%%/%d%%") : _L("%d%%/%d%%")), + 100 - cur.mix_b_percent, cur.mix_b_percent)); + } + } + } else { + cur.display_color = MixedFilamentManager::blend_color(colors[size_t(a - 1)], colors[size_t(b - 1)], 100 - cur.mix_b_percent, cur.mix_b_percent); + if (blend_label) + blend_label->SetLabel(wxString::Format(simple_mode ? _L("Simple %d%%/%d%%") : + (same_layer_mode ? _L("Pointillisme %d%%/%d%%") : _L("%d%%/%d%%")), + 100 - cur.mix_b_percent, cur.mix_b_percent)); + } + + if (mix_preview) { + std::vector palette; + palette.reserve(colors.size()); + for (const std::string &hex : colors) + palette.emplace_back(parse_mixed_color(hex)); + mix_preview->set_data(palette, preview_sequence, same_layer_mode, wxColour(cur.display_color)); + } + if (mix_summary_label) { + const std::string summary = summarize_sequence(preview_sequence); + mix_summary_label->SetLabel(summary.empty() ? _L("No mix sequence") : from_u8(summary)); + } swatch->SetBackgroundColour(wxColour(cur.display_color)); swatch->Refresh(); @@ -2777,11 +3854,127 @@ void Sidebar::update_mixed_filament_panel() choice_a->Bind(wxEVT_CHOICE, [apply_custom_row](wxCommandEvent &) { apply_custom_row(true); }); choice_b->Bind(wxEVT_CHOICE, [apply_custom_row](wxCommandEvent &) { apply_custom_row(true); }); + if (distribution_choice) + distribution_choice->Bind(wxEVT_CHOICE, [apply_custom_row](wxCommandEvent &) { apply_custom_row(true); }); + if (choice_c) + choice_c->Bind(wxEVT_CHOICE, [apply_custom_row](wxCommandEvent &) { apply_custom_row(true); }); + if (choice_d) + choice_d->Bind(wxEVT_CHOICE, [apply_custom_row](wxCommandEvent &) { apply_custom_row(true); }); if (blend_selector) blend_selector->Bind(wxEVT_SLIDER, [apply_custom_row](wxCommandEvent &) { apply_custom_row(false); }); + if (add_extra_color_btn && choice_c && choice_d) { + add_extra_color_btn->Bind(wxEVT_BUTTON, [choice_a, choice_b, choice_c, choice_d, num_physical, apply_custom_row](wxCommandEvent &) { + std::vector used; + used.reserve(4); + auto append_used = [&used](int id) { + if (id <= 0) + return; + if (std::find(used.begin(), used.end(), id) == used.end()) + used.emplace_back(id); + }; + append_used(choice_a ? (choice_a->GetSelection() + 1) : 0); + append_used(choice_b ? (choice_b->GetSelection() + 1) : 0); + append_used(choice_c ? choice_c->GetSelection() : 0); + append_used(choice_d ? choice_d->GetSelection() : 0); + auto find_first_free = [&used, num_physical]() -> int { + for (int id = 1; id <= int(num_physical); ++id) { + if (std::find(used.begin(), used.end(), id) == used.end()) + return id; + } + return 0; + }; + + if (choice_c->GetSelection() <= 0) { + const int free_id = find_first_free(); + if (free_id > 0) { + choice_c->SetSelection(free_id); + apply_custom_row(true); + } + return; + } + if (choice_d->GetSelection() <= 0) { + const int free_id = find_first_free(); + if (free_id > 0) { + choice_d->SetSelection(free_id); + apply_custom_row(true); + } + } + }); + } + if (blend_selector) { + blend_selector->Bind(wxEVT_BUTTON, [this, row, blend_selector, choice_a, choice_b, choice_c, choice_d, + num_physical, selected_weight_state, normalize_gradient_weights, + physical_colors, apply_custom_row](wxCommandEvent &) { + if (!blend_selector->is_multi_mode()) + return; + + std::vector selected_ids; + selected_ids.reserve(4); + auto add_unique = [&selected_ids](unsigned int id) { + if (id == 0) + return; + if (std::find(selected_ids.begin(), selected_ids.end(), id) == selected_ids.end()) + selected_ids.emplace_back(id); + }; + add_unique(unsigned(std::clamp(choice_a ? (choice_a->GetSelection() + 1) : 0, 1, int(num_physical)))); + add_unique(unsigned(std::clamp(choice_b ? (choice_b->GetSelection() + 1) : 0, 1, int(num_physical)))); + if (choice_c && choice_c->GetSelection() > 0) + add_unique(unsigned(choice_c->GetSelection())); + if (choice_d && choice_d->GetSelection() > 0) + add_unique(unsigned(choice_d->GetSelection())); + if (selected_ids.size() < 3) + return; + + std::vector palette; + palette.reserve(physical_colors.size()); + for (const std::string &hex : physical_colors) + palette.emplace_back(parse_mixed_color(hex)); + + const std::vector initial_weights = normalize_gradient_weights(*selected_weight_state, selected_ids.size()); + MixedGradientWeightsDialog dlg(row, selected_ids, palette, initial_weights); + if (dlg.ShowModal() != wxID_OK) + return; + + *selected_weight_state = dlg.normalized_weights(); + apply_custom_row(false); + }); + } if (pattern_ctrl) { + auto append_pattern_token = [pattern_ctrl](int filament_id) { + if (!pattern_ctrl || filament_id <= 0) + return; + std::string pattern = into_u8(pattern_ctrl->GetValue()); + if (!pattern.empty()) { + const char last = pattern.back(); + const bool has_sep = last == '/' || last == '-' || last == '_' || last == '|' || last == ':' || last == ';' || last == ',' || last == ' '; + if (!has_sep) + pattern.push_back('/'); + } + pattern += std::to_string(filament_id); + pattern_ctrl->ChangeValue(from_u8(pattern)); + }; + pattern_ctrl->Bind(wxEVT_TEXT_ENTER, [apply_custom_row](wxCommandEvent &) { apply_custom_row(true); }); pattern_ctrl->Bind(wxEVT_KILL_FOCUS, [apply_custom_row](wxFocusEvent &evt) { apply_custom_row(true); evt.Skip(); }); + if (pattern_insert_btn && pattern_insert_choice) { + pattern_insert_btn->Bind(wxEVT_BUTTON, [apply_custom_row, append_pattern_token, pattern_insert_choice](wxCommandEvent &) { + const int sel = pattern_insert_choice->GetSelection(); + if (sel >= 0) { + append_pattern_token(sel + 1); + apply_custom_row(true); + } + }); + } + for (size_t fid = 0; fid < pattern_quick_filament_buttons.size(); ++fid) { + wxButton *btn = pattern_quick_filament_buttons[fid]; + if (!btn) + continue; + const int filament_id = int(fid + 1); + btn->Bind(wxEVT_BUTTON, [apply_custom_row, append_pattern_token, filament_id](wxCommandEvent &) { + append_pattern_token(filament_id); + apply_custom_row(true); + }); + } } } diff --git a/src/slic3r/GUI/Tab.cpp b/src/slic3r/GUI/Tab.cpp index dd9306614a..34c128c532 100644 --- a/src/slic3r/GUI/Tab.cpp +++ b/src/slic3r/GUI/Tab.cpp @@ -1789,6 +1789,8 @@ void Tab::on_value_change(const std::string& opt_key, const boost::any& value) opt_key == "mixed_color_layer_height_b" || opt_key == "mixed_filament_cycle_layers" || opt_key == "mixed_filament_advanced_dithering" || + opt_key == "mixed_filament_pointillism_pixel_size" || + opt_key == "mixed_filament_pointillism_line_gap" || opt_key == "dithering_z_step_size" || opt_key == "dithering_local_z_mode" || opt_key == "dithering_step_painted_zones_only" || @@ -2518,6 +2520,8 @@ optgroup->append_single_option_line("skirt_loops", "others_settings_skirt#loops" optgroup->append_single_option_line("mixed_filament_height_upper_bound"); optgroup->append_single_option_line("mixed_filament_cycle_layers"); optgroup->append_single_option_line("mixed_filament_advanced_dithering"); + optgroup->append_single_option_line("mixed_filament_pointillism_pixel_size"); + optgroup->append_single_option_line("mixed_filament_pointillism_line_gap"); optgroup->append_single_option_line("dithering_z_step_size"); optgroup->append_single_option_line("dithering_local_z_mode"); optgroup->append_single_option_line("dithering_step_painted_zones_only");