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");