diff --git a/src/libslic3r/TextureMapping.cpp b/src/libslic3r/TextureMapping.cpp index ae4c596509c..27bd873d2ae 100644 --- a/src/libslic3r/TextureMapping.cpp +++ b/src/libslic3r/TextureMapping.cpp @@ -534,6 +534,45 @@ static TextureMappingZone::LinearGradientAnchor linear_gradient_anchor_from_json return out; } +static nlohmann::json linear_gradient_stop_to_json(const TextureMappingZone::LinearGradientStop &stop) +{ + nlohmann::json out; + out["position"] = std::clamp(std::isfinite(stop.position) ? stop.position : 0.f, 0.f, 1.f); + out["filament_id"] = stop.filament_id; + return out; +} + +static TextureMappingZone::LinearGradientStop linear_gradient_stop_from_json(const nlohmann::json &value) +{ + TextureMappingZone::LinearGradientStop out; + if (!value.is_object()) + return out; + const float position = value.value("position", 0.f); + out.position = std::clamp(std::isfinite(position) ? position : 0.f, 0.f, 1.f); + out.filament_id = value.value("filament_id", 1u); + return out; +} + +static nlohmann::json linear_gradient_stops_to_json(const std::vector &stops) +{ + nlohmann::json out = nlohmann::json::array(); + for (const TextureMappingZone::LinearGradientStop &stop : stops) + out.push_back(linear_gradient_stop_to_json(stop)); + return out; +} + +static std::vector linear_gradient_stops_from_json(const nlohmann::json &value) +{ + std::vector out; + if (!value.is_array()) + return out; + out.reserve(value.size()); + for (const nlohmann::json &item : value) + if (item.is_object()) + out.emplace_back(linear_gradient_stop_from_json(item)); + return out; +} + static std::string surface_pattern_name(int surface_pattern) { if (surface_pattern == int(TextureMappingZone::Gradient2D)) @@ -930,6 +969,17 @@ bool TextureMappingZone::operator==(const TextureMappingZone &rhs) const } return true; }; + auto stops_equal = [eps](const std::vector &lhs, + const std::vector &rhs_values) { + if (lhs.size() != rhs_values.size()) + return false; + for (size_t i = 0; i < lhs.size(); ++i) { + if (std::abs(lhs[i].position - rhs_values[i].position) > eps || + lhs[i].filament_id != rhs_values[i].filament_id) + return false; + } + return true; + }; return stable_id == rhs.stable_id && zone_id == rhs.zone_id && @@ -990,7 +1040,8 @@ bool TextureMappingZone::operator==(const TextureMappingZone &rhs) const std::abs(linear_gradient_radius_mm - rhs.linear_gradient_radius_mm) <= eps && linear_gradient_radius_percent == rhs.linear_gradient_radius_percent && std::abs(linear_gradient_radius_pct - rhs.linear_gradient_radius_pct) <= eps && - show_linear_gradient_direction_arrow == rhs.show_linear_gradient_direction_arrow; + show_linear_gradient_direction_arrow == rhs.show_linear_gradient_direction_arrow && + stops_equal(linear_gradient_stops, rhs.linear_gradient_stops); } uint64_t TextureMappingManager::allocate_stable_id() @@ -1021,6 +1072,15 @@ void TextureMappingManager::refresh(const std::vector &filament_col zone.stable_id = normalize_stable_id(zone.stable_id); if (zone.display_color.empty() || zone.display_color[0] != '#') zone.display_color = random_display_color(zone.stable_id); + if (zone.is_linear_gradient()) { + zone.linear_gradient_stops = normalized_linear_gradient_stops(zone, m_filament_colours.size()); + const std::vector ids = linear_gradient_component_ids_from_stops(zone, m_filament_colours.size()); + zone.component_ids = encode_component_ids(ids); + if (!zone.linear_gradient_stops.empty()) { + zone.component_a = zone.linear_gradient_stops.front().filament_id; + zone.component_b = zone.linear_gradient_stops.back().filament_id; + } + } } } @@ -1044,6 +1104,8 @@ void TextureMappingManager::remove_physical_filament(unsigned int deleted_filame for (TextureMappingZone &zone : m_zones) { zone.component_a = remap_id(zone.component_a); zone.component_b = remap_id(zone.component_b); + for (TextureMappingZone::LinearGradientStop &stop : zone.linear_gradient_stops) + stop.filament_id = remap_id(stop.filament_id); std::vector ids = decode_component_ids(zone.component_ids, 9); for (unsigned int &id : ids) @@ -1063,13 +1125,18 @@ void TextureMappingManager::remove_physical_filament(unsigned int deleted_filame zone.component_a = ids[0]; zone.component_b = ids[1]; } - if (zone.is_linear_gradient() && ids.size() > 2) - ids.resize(2); - if (zone.is_linear_gradient() && ids.size() >= 2) { + if (zone.is_linear_gradient()) { + zone.linear_gradient_stops = normalized_linear_gradient_stops(zone, new_physical_count); + ids = linear_gradient_component_ids_from_stops(zone, new_physical_count); + if (!zone.linear_gradient_stops.empty()) { + zone.component_a = zone.linear_gradient_stops.front().filament_id; + zone.component_b = zone.linear_gradient_stops.back().filament_id; + } + } else if (ids.size() >= 2) { zone.component_a = ids[0]; zone.component_b = ids[1]; } - if (new_physical_count < 2) + if (new_physical_count == 0 || (!zone.is_linear_gradient() && new_physical_count < 2)) zone.enabled = false; zone.component_ids = encode_component_ids(ids); @@ -1088,7 +1155,8 @@ TextureMappingZone *TextureMappingManager::add_zone(size_t num_physical, const std::vector &filament_colours, int surface_pattern) { - if (num_physical < 2) + const bool linear_gradient = surface_pattern == int(TextureMappingZone::LinearGradient); + if (num_physical == 0 || (!linear_gradient && num_physical < 2)) return nullptr; TextureMappingZone zone; @@ -1107,6 +1175,13 @@ TextureMappingZone *TextureMappingManager::add_zone(size_t num_physical, zone.component_ids = encode_component_ids(ids); zone.component_a = ids[0]; zone.component_b = ids.size() > 1 ? ids[1] : ids[0]; + if (zone.is_linear_gradient()) { + zone.linear_gradient_stops = { + {0.f, zone.component_a}, + {1.f, zone.component_b} + }; + zone.component_ids = encode_component_ids(linear_gradient_component_ids_from_stops(zone, num_physical)); + } zone.display_color = random_display_color(zone.stable_id); if (zone.is_image_texture()) auto_adjust_texture_component_ids(zone, num_physical, filament_colours); @@ -1120,10 +1195,12 @@ bool TextureMappingManager::duplicate_zone(size_t zone_index, size_t num_physical, const std::vector &filament_colours) { - if (zone_index >= m_zones.size() || num_physical < 2) + if (zone_index >= m_zones.size() || num_physical == 0) return false; TextureMappingZone copy = m_zones[zone_index]; + if (!copy.is_linear_gradient() && num_physical < 2) + return false; copy.stable_id = allocate_stable_id(); copy.zone_id = allocate_zone_id(num_physical); if (copy.zone_id == 0) @@ -1192,20 +1269,23 @@ std::string TextureMappingManager::serialize_entries() zone.display_color = random_display_color(zone.stable_id); std::vector component_ids = decode_component_ids(zone.component_ids, 9); - if (component_ids.size() < 2) { + if (zone.is_linear_gradient()) { + zone.linear_gradient_stops = normalized_linear_gradient_stops(zone, m_filament_colours.size()); + component_ids = linear_gradient_component_ids_from_stops(zone, m_filament_colours.size()); + if (!zone.linear_gradient_stops.empty()) { + zone.component_a = zone.linear_gradient_stops.front().filament_id; + zone.component_b = zone.linear_gradient_stops.back().filament_id; + } + zone.component_ids = encode_component_ids(component_ids); + } + if (!zone.is_linear_gradient() && component_ids.size() < 2) { component_ids = {zone.component_a, zone.component_b}; component_ids.erase(std::remove_if(component_ids.begin(), component_ids.end(), [](unsigned int id) { return id == 0 || id > 9; }), component_ids.end()); } - if (zone.is_linear_gradient() && component_ids.size() > 2) - component_ids.resize(2); unsigned int anchor_a = zone.component_a; unsigned int anchor_b = zone.component_b; - if (zone.is_linear_gradient() && component_ids.size() >= 2) { - anchor_a = component_ids[0]; - anchor_b = component_ids[1]; - } const std::string normalized_weights = normalize_weights(zone.component_weights, component_ids.size()); const std::string normalized_distances = @@ -1222,14 +1302,17 @@ std::string TextureMappingManager::serialize_entries() entry["zone_id"] = zone.zone_id; entry["enabled"] = zone.enabled; entry["surface_pattern"] = surface_pattern_name(zone.surface_pattern); - entry["anchor_filaments"] = {anchor_a, anchor_b}; - entry["component_filaments"] = ids_to_json(component_ids); - entry["component_weights_pct"] = weights_to_json(normalized_weights, component_ids.size()); + if (!zone.is_linear_gradient()) { + entry["anchor_filaments"] = {anchor_a, anchor_b}; + entry["component_filaments"] = ids_to_json(component_ids); + entry["component_weights_pct"] = weights_to_json(normalized_weights, component_ids.size()); + } entry["display_color"] = zone.display_color; if (zone.is_linear_gradient()) { nlohmann::json linear_gradient; linear_gradient["start"] = linear_gradient_anchor_to_json(zone.linear_gradient_start); linear_gradient["end"] = linear_gradient_anchor_to_json(zone.linear_gradient_end); + linear_gradient["stops"] = linear_gradient_stops_to_json(zone.linear_gradient_stops); linear_gradient["mode"] = linear_gradient_mode_name(zone.linear_gradient_mode); linear_gradient["radius_mm"] = std::isfinite(zone.linear_gradient_radius_mm) ? std::max(0.f, zone.linear_gradient_radius_mm) : @@ -1322,7 +1405,7 @@ void TextureMappingManager::load_entries(const std::string &serialized, refresh(filament_colours); const size_t n = filament_colours.size(); - if (serialized.empty() || n < 2) + if (serialized.empty() || n == 0) return; nlohmann::json root; @@ -1354,22 +1437,32 @@ void TextureMappingManager::load_entries(const std::string &serialized, continue; } + const int surface_pattern = surface_pattern_from_name(entry.value("surface_pattern", std::string("image_texture"))); + const bool linear_gradient = surface_pattern == int(TextureMappingZone::LinearGradient); std::vector component_ids = ids_from_json(entry.value("component_filaments", nlohmann::json::array()), n); - if (component_ids.size() < 2) { + if (!linear_gradient && component_ids.size() < 2) { component_ids.clear(); for (size_t i = 1; i <= std::min(n, 9); ++i) component_ids.emplace_back(unsigned(i)); } - if (component_ids.size() < 2) { + if (!linear_gradient && component_ids.size() < 2) { ++skipped_rows; continue; } - std::vector anchors = ids_from_json(entry.value("anchor_filaments", nlohmann::json::array()), n); - if (anchors.size() < 2) + if (anchors.size() < 2 && component_ids.size() >= 2) anchors = {component_ids[0], component_ids[1]}; - if (anchors[0] == anchors[1]) + else if (anchors.empty()) { + const unsigned int anchor_a = component_ids.empty() ? 1u : component_ids.front(); + const unsigned int anchor_b = component_ids.size() >= 2 ? component_ids[1] : (n >= 2 && anchor_a == 1 ? 2u : anchor_a); + anchors = {anchor_a, anchor_b}; + } + while (anchors.size() < 2) + anchors.emplace_back(anchors.front()); + if (!linear_gradient && anchors[0] == anchors[1] && n >= 2) anchors[1] = anchors[0] == 1 ? 2 : 1; + if (linear_gradient && component_ids.empty()) + component_ids = {anchors[0], anchors[1]}; TextureMappingZone zone; zone.component_a = anchors[0]; @@ -1378,13 +1471,7 @@ void TextureMappingManager::load_entries(const std::string &serialized, zone.zone_id = entry.value("zone_id", entry.value("filament_id", 0u)); zone.enabled = entry.value("enabled", true); zone.deleted = false; - zone.surface_pattern = surface_pattern_from_name(entry.value("surface_pattern", std::string("image_texture"))); - if (zone.is_linear_gradient() && component_ids.size() > 2) - component_ids.resize(2); - if (zone.is_linear_gradient() && component_ids.size() >= 2) { - zone.component_a = component_ids[0]; - zone.component_b = component_ids[1]; - } + zone.surface_pattern = surface_pattern; zone.component_ids = encode_component_ids(component_ids); zone.component_weights = weights_from_json(entry.value("component_weights_pct", nlohmann::json::array()), component_ids.size()); @@ -1395,6 +1482,14 @@ void TextureMappingManager::load_entries(const std::string &serialized, const nlohmann::json linear_gradient = entry.value("linear_gradient", nlohmann::json::object()); zone.linear_gradient_start = linear_gradient_anchor_from_json(linear_gradient.value("start", nlohmann::json::object())); zone.linear_gradient_end = linear_gradient_anchor_from_json(linear_gradient.value("end", nlohmann::json::object())); + zone.linear_gradient_stops = linear_gradient_stops_from_json(linear_gradient.value("stops", nlohmann::json::array())); + zone.linear_gradient_stops = normalized_linear_gradient_stops(zone, n); + component_ids = linear_gradient_component_ids_from_stops(zone, n); + if (!zone.linear_gradient_stops.empty()) { + zone.component_a = zone.linear_gradient_stops.front().filament_id; + zone.component_b = zone.linear_gradient_stops.back().filament_id; + } + zone.component_ids = encode_component_ids(component_ids); zone.linear_gradient_mode = linear_gradient_mode_from_name(linear_gradient.value("mode", std::string("linear"))); const float radius_mm = linear_gradient.value("radius_mm", TextureMappingZone::DefaultLinearGradientRadiusMm); zone.linear_gradient_radius_mm = std::isfinite(radius_mm) ? std::max(0.f, radius_mm) : TextureMappingZone::DefaultLinearGradientRadiusMm; @@ -1680,12 +1775,127 @@ bool TextureMappingManager::component_count_mismatch(const TextureMappingZone &z return expected != 0 && selected_component_ids(zone, num_physical).size() != expected; } +std::vector TextureMappingManager::normalized_linear_gradient_stops(const TextureMappingZone &zone, + size_t num_physical) +{ + const unsigned int max_id = unsigned(std::min(num_physical, 9)); + std::vector stops; + if (max_id == 0) + return stops; + + stops.reserve(zone.linear_gradient_stops.size()); + for (TextureMappingZone::LinearGradientStop stop : zone.linear_gradient_stops) { + if (stop.filament_id < 1 || stop.filament_id > max_id) + continue; + stop.position = std::clamp(std::isfinite(stop.position) ? stop.position : 0.f, 0.f, 1.f); + stops.emplace_back(stop); + } + + if (stops.size() < 2) { + std::vector ids = decode_component_ids(zone.component_ids, num_physical); + if (ids.size() < 2) { + ids.clear(); + if (zone.component_a >= 1 && zone.component_a <= max_id) + ids.emplace_back(zone.component_a); + if (zone.component_b >= 1 && zone.component_b <= max_id) + ids.emplace_back(zone.component_b); + } + if (ids.empty()) + ids.emplace_back(1); + while (ids.size() < 2) + ids.emplace_back(ids.front()); + stops = { + {0.f, std::clamp(ids[0], 1u, max_id)}, + {1.f, std::clamp(ids[1], 1u, max_id)} + }; + } + + std::stable_sort(stops.begin(), stops.end(), [](const auto &lhs, const auto &rhs) { + return lhs.position < rhs.position; + }); + return stops; +} + +std::vector TextureMappingManager::linear_gradient_component_ids_from_stops(const TextureMappingZone &zone, + size_t num_physical) +{ + std::vector ids; + bool seen[10] = { false }; + for (const TextureMappingZone::LinearGradientStop &stop : normalized_linear_gradient_stops(zone, num_physical)) { + const unsigned int id = stop.filament_id; + if (id == 0 || id > num_physical || id > 9 || seen[id]) + continue; + seen[id] = true; + ids.emplace_back(id); + } + return ids; +} + +std::vector TextureMappingManager::linear_gradient_compact_weights(float t, + const std::vector &stops, + const std::vector &component_ids) +{ + std::vector weights(component_ids.size(), 0.f); + if (stops.empty() || component_ids.empty()) + return weights; + + auto add_weight = [&weights, &component_ids](unsigned int filament_id, float weight) { + if (weight <= 0.f) + return; + for (size_t idx = 0; idx < component_ids.size(); ++idx) { + if (component_ids[idx] == filament_id) { + weights[idx] = std::max(weights[idx], std::clamp(weight, 0.f, 1.f)); + return; + } + } + }; + + const float clamped_t = std::clamp(std::isfinite(t) ? t : 0.f, 0.f, 1.f); + if (clamped_t <= stops.front().position) { + add_weight(stops.front().filament_id, 1.f); + return weights; + } + if (clamped_t >= stops.back().position) { + add_weight(stops.back().filament_id, 1.f); + return weights; + } + + size_t right = 1; + while (right < stops.size() && clamped_t > stops[right].position) + ++right; + if (right >= stops.size()) { + add_weight(stops.back().filament_id, 1.f); + return weights; + } + + const TextureMappingZone::LinearGradientStop &lhs = stops[right - 1]; + const TextureMappingZone::LinearGradientStop &rhs = stops[right]; + const float denom = rhs.position - lhs.position; + if (lhs.filament_id == rhs.filament_id || denom <= 1e-6f) { + add_weight(rhs.filament_id, 1.f); + return weights; + } + + const float local_t = std::clamp((clamped_t - lhs.position) / denom, 0.f, 1.f); + float lhs_weight = 1.f - local_t; + float rhs_weight = local_t; + const float max_weight = std::max(lhs_weight, rhs_weight); + if (max_weight > 1e-6f) { + lhs_weight /= max_weight; + rhs_weight /= max_weight; + } + add_weight(lhs.filament_id, lhs_weight); + add_weight(rhs.filament_id, rhs_weight); + return weights; +} + std::vector TextureMappingManager::selected_component_ids(const TextureMappingZone &zone, size_t num_physical) { + if (zone.is_linear_gradient()) + return linear_gradient_component_ids_from_stops(zone, num_physical); + std::vector ids = decode_component_ids(zone.component_ids, num_physical); if (!ids.empty()) { - if (zone.is_linear_gradient() && ids.size() > 2) - ids.resize(2); return ids; } @@ -1693,8 +1903,6 @@ std::vector TextureMappingManager::selected_component_ids(const Te ids.emplace_back(zone.component_a); if (zone.component_b >= 1 && zone.component_b <= num_physical && zone.component_b != zone.component_a) ids.emplace_back(zone.component_b); - if (zone.is_linear_gradient() && ids.size() > 2) - ids.resize(2); return ids; } diff --git a/src/libslic3r/TextureMapping.hpp b/src/libslic3r/TextureMapping.hpp index 26c97cd8de6..842748d0852 100644 --- a/src/libslic3r/TextureMapping.hpp +++ b/src/libslic3r/TextureMapping.hpp @@ -167,6 +167,11 @@ struct TextureMappingZone std::array global_point { { 0.f, 0.f, 0.f } }; }; + struct LinearGradientStop { + float position = 0.f; + unsigned int filament_id = 1; + }; + uint64_t stable_id = 0; unsigned int zone_id = 0; bool enabled = true; @@ -229,6 +234,7 @@ struct TextureMappingZone bool linear_gradient_radius_percent = DefaultLinearGradientRadiusPercent; float linear_gradient_radius_pct = DefaultLinearGradientRadiusPct; bool show_linear_gradient_direction_arrow = true; + std::vector linear_gradient_stops; bool is_image_texture() const { return surface_pattern == int(ImageTexture); } bool is_2d_gradient() const { return surface_pattern == int(Gradient2D); } @@ -287,6 +293,7 @@ struct TextureMappingZone linear_gradient_radius_mm = DefaultLinearGradientRadiusMm; linear_gradient_radius_percent = DefaultLinearGradientRadiusPercent; linear_gradient_radius_pct = DefaultLinearGradientRadiusPct; + linear_gradient_stops.clear(); filament_strengths_pct.clear(); filament_minimum_offsets_pct.clear(); filament_transmission_distances_mm.clear(); @@ -432,6 +439,13 @@ public: static bool auto_adjust_texture_component_ids(TextureMappingZone &zone, size_t num_physical, const std::vector &filament_colours); + static std::vector normalized_linear_gradient_stops(const TextureMappingZone &zone, + size_t num_physical); + static std::vector linear_gradient_component_ids_from_stops(const TextureMappingZone &zone, + size_t num_physical); + static std::vector linear_gradient_compact_weights(float t, + const std::vector &stops, + const std::vector &component_ids); static std::vector texture_component_color_matches(const TextureMappingZone &zone, size_t num_physical, const std::vector &filament_colours); diff --git a/src/libslic3r/TextureMappingOffset.cpp b/src/libslic3r/TextureMappingOffset.cpp index 9c6418d3479..0947ce6e427 100644 --- a/src/libslic3r/TextureMappingOffset.cpp +++ b/src/libslic3r/TextureMappingOffset.cpp @@ -2675,6 +2675,9 @@ std::optional> sample_weight_field_rgb(const TextureMapping std::vector decode_texture_mapping_offset_component_ids(const TextureMappingZone &zone, size_t num_physical) { + if (zone.is_linear_gradient()) + return TextureMappingManager::linear_gradient_component_ids_from_stops(zone, num_physical); + std::vector out; for (const char c : zone.component_ids) { if (c < '1' || c > '9') @@ -2771,13 +2774,6 @@ std::optional build_texture_mapping_offset_context_ if (!effective_component_ids.empty()) component_ids = effective_component_ids; } - if (linear_gradient_mode && component_ids.size() > 2) - component_ids.resize(2); - if (linear_gradient_mode && component_ids.size() < 2) { - component_ids.clear(); - for (size_t i = 1; i <= std::min(num_physical, 2); ++i) - component_ids.emplace_back(unsigned(i)); - } if (component_ids.empty()) return std::nullopt; @@ -3046,6 +3042,8 @@ std::optional build_texture_mapping_offset_context_ context.active_component_id = active_component_id; context.active_component_idx = active_component_idx; context.component_ids = std::move(component_ids); + if (linear_gradient_mode) + context.linear_gradient_stops = TextureMappingManager::normalized_linear_gradient_stops(zone, num_physical); context.component_distances_mm = std::move(distances_mm); context.rotated_angles = std::move(rotated_angles); context.weight_field = std::move(weight_field); @@ -3129,12 +3127,11 @@ float texture_mapping_offset_surface_inset_mm(const TextureMappingOffsetContext t = clamp01f((sample - context.linear_gradient_start_mm).dot(gradient_vec) / denom); } if (context.linear_gradient_radial_mode || (context.linear_gradient_end_mm - context.linear_gradient_start_mm).squaredNorm() > 1e-8f) { - float desired_strength = context.active_component_idx == 0 ? 1.f - t : - context.active_component_idx == 1 ? t : - 0.f; - const float max_strength = std::max(1.f - t, t); - if (max_strength > EPSILON) - desired_strength = clamp01f(desired_strength / max_strength); + const std::vector weights = + TextureMappingManager::linear_gradient_compact_weights(t, context.linear_gradient_stops, context.component_ids); + const float desired_strength = context.active_component_idx < weights.size() ? + clamp01f(weights[context.active_component_idx]) : + 0.f; inset_strength = std::clamp(1.f - desired_strength, 0.f, 1.f); } else { inset_strength = 0.f; diff --git a/src/libslic3r/TextureMappingOffset.hpp b/src/libslic3r/TextureMappingOffset.hpp index 4cd77513f35..8ec4324266e 100644 --- a/src/libslic3r/TextureMappingOffset.hpp +++ b/src/libslic3r/TextureMappingOffset.hpp @@ -66,6 +66,7 @@ struct TextureMappingOffsetContext { Vec3f linear_gradient_end_mm { Vec3f::Zero() }; bool linear_gradient_radial_mode { false }; float linear_gradient_radius_mm { 0.f }; + std::vector linear_gradient_stops; unsigned int active_component_id { 0 }; size_t active_component_idx { size_t(-1) }; std::vector component_ids; diff --git a/src/slic3r/GUI/3DScene.cpp b/src/slic3r/GUI/3DScene.cpp index 2bac87eb4e6..3c7b6dbb93f 100644 --- a/src/slic3r/GUI/3DScene.cpp +++ b/src/slic3r/GUI/3DScene.cpp @@ -364,10 +364,8 @@ std::vector linear_gradient_component_ids_for_arrow(const TextureM return id == 0 || id > num_physical; }), ids.end()); ids.erase(std::unique(ids.begin(), ids.end()), ids.end()); - if (ids.size() > 2) - ids.resize(2); - if (ids.size() < 2 && num_physical >= 2) - ids = {1, 2}; + if (ids.empty() && num_physical >= 1) + ids = {1}; return ids; } @@ -381,16 +379,24 @@ std::array linear_gradient_filament_color(unsigned int filament_id, co return { 0.5f, 0.5f, 0.5f }; } -ColorRGBA linear_gradient_arrow_color(const std::array &start_color, - const std::array &end_color, +ColorRGBA linear_gradient_arrow_color(const std::vector> &component_colors, + const std::vector &stops, + const std::vector &component_ids, float t) { - std::vector> colors { start_color, end_color }; - std::vector weights { std::clamp(1.f - t, 0.f, 1.f), std::clamp(t, 0.f, 1.f) }; - const std::array mixed = mix_color_solver_components(colors, weights, ColorSolverMixModel::PigmentPainter); + const std::vector weights = TextureMappingManager::linear_gradient_compact_weights(t, stops, component_ids); + const std::array mixed = mix_color_solver_components(component_colors, weights, ColorSolverMixModel::PigmentPainter); return { std::clamp(mixed[0], 0.f, 1.f), std::clamp(mixed[1], 0.f, 1.f), std::clamp(mixed[2], 0.f, 1.f), 1.f }; } +float linear_gradient_arrow_t(const Vec3f &point, + const Vec3f &arrow_start, + const Vec3f &axis, + float arrow_length) +{ + return std::clamp((point - arrow_start).dot(axis) / std::max(0.0001f, arrow_length), 0.f, 1.f); +} + struct LinearGradientArrowUsage { bool any { false }; Vec3f default_start { Vec3f::Zero() }; @@ -493,39 +499,43 @@ void append_linear_gradient_cylinder(GUI::GLModel::Geometry &geometry, const Vec3f &u, const Vec3f &v, float radius, - const std::array &start_color, - const std::array &end_color, + const std::vector> &component_colors, + const std::vector &stops, + const std::vector &component_ids, bool black) { constexpr int sectors = 32; + constexpr int axial_steps = 72; const ColorRGBA black_color = ColorRGBA::BLACK(); - const float safe_arrow_length = std::max(0.0001f, arrow_length); - const float center_t = std::clamp((start - arrow_start).dot(axis) / safe_arrow_length, 0.f, 1.f); + std::vector> ring_ids(size_t(axial_steps + 1), std::vector(size_t(sectors + 1), 0)); + for (int step = 0; step <= axial_steps; ++step) { + const float step_t = float(step) / float(axial_steps); + const Vec3f center = start + (end - start) * step_t; + const float arrow_t = linear_gradient_arrow_t(center, arrow_start, axis, arrow_length); + const ColorRGBA color = black ? black_color : linear_gradient_arrow_color(component_colors, stops, component_ids, arrow_t); + for (int sector = 0; sector <= sectors; ++sector) { + const float a = 2.f * float(PI) * float(sector) / float(sectors); + const Vec3f radial = std::cos(a) * u + std::sin(a) * v; + ring_ids[size_t(step)][size_t(sector)] = + linear_gradient_add_vertex(geometry, center + radius * radial, radial, color); + } + } + const unsigned int start_center = linear_gradient_add_vertex(geometry, start, -axis, - black ? black_color : linear_gradient_arrow_color(start_color, end_color, center_t)); + black ? black_color : linear_gradient_arrow_color(component_colors, stops, component_ids, linear_gradient_arrow_t(start, arrow_start, axis, arrow_length))); for (int i = 0; i < sectors; ++i) { - const float a0 = 2.f * float(PI) * float(i) / float(sectors); - const float a1 = 2.f * float(PI) * float(i + 1) / float(sectors); - const Vec3f r0 = std::cos(a0) * u + std::sin(a0) * v; - const Vec3f r1 = std::cos(a1) * u + std::sin(a1) * v; - const Vec3f p0 = start + radius * r0; - const Vec3f p1 = start + radius * r1; - const Vec3f p2 = end + radius * r0; - const Vec3f p3 = end + radius * r1; - const float t0 = std::clamp((p0 - arrow_start).dot(axis) / safe_arrow_length, 0.f, 1.f); - const float t1 = std::clamp((p2 - arrow_start).dot(axis) / safe_arrow_length, 0.f, 1.f); - const ColorRGBA c0 = black ? black_color : linear_gradient_arrow_color(start_color, end_color, t0); - const ColorRGBA c1 = black ? black_color : linear_gradient_arrow_color(start_color, end_color, t1); - const unsigned int i0 = linear_gradient_add_vertex(geometry, p0, r0, c0); - const unsigned int i1 = linear_gradient_add_vertex(geometry, p1, r1, c0); - const unsigned int i2 = linear_gradient_add_vertex(geometry, p2, r0, c1); - const unsigned int i3 = linear_gradient_add_vertex(geometry, p3, r1, c1); - geometry.add_triangle(i0, i2, i1); - geometry.add_triangle(i1, i2, i3); - geometry.add_triangle(start_center, i1, i0); + geometry.add_triangle(start_center, ring_ids[0][size_t(i + 1)], ring_ids[0][size_t(i)]); + for (int step = 0; step < axial_steps; ++step) { + const unsigned int i0 = ring_ids[size_t(step)][size_t(i)]; + const unsigned int i1 = ring_ids[size_t(step)][size_t(i + 1)]; + const unsigned int i2 = ring_ids[size_t(step + 1)][size_t(i)]; + const unsigned int i3 = ring_ids[size_t(step + 1)][size_t(i + 1)]; + geometry.add_triangle(i0, i2, i1); + geometry.add_triangle(i1, i2, i3); + } } } @@ -538,30 +548,44 @@ void append_linear_gradient_cone(GUI::GLModel::Geometry &geometry, const Vec3f &u, const Vec3f &v, float radius, - const std::array &start_color, - const std::array &end_color, + const std::vector> &component_colors, + const std::vector &stops, + const std::vector &component_ids, bool black) { constexpr int sectors = 32; + constexpr int axial_steps = 24; const ColorRGBA black_color = ColorRGBA::BLACK(); - const float safe_arrow_length = std::max(0.0001f, arrow_length); - const float tip_t = std::clamp((tip - arrow_start).dot(axis) / safe_arrow_length, 0.f, 1.f); - const ColorRGBA tip_color = black ? black_color : linear_gradient_arrow_color(start_color, end_color, tip_t); + const ColorRGBA tip_color = black ? black_color : linear_gradient_arrow_color(component_colors, stops, component_ids, linear_gradient_arrow_t(tip, arrow_start, axis, arrow_length)); + std::vector> ring_ids(size_t(axial_steps), std::vector(size_t(sectors + 1), 0)); + const Vec3f cone_axis = tip - base; + for (int step = 0; step < axial_steps; ++step) { + const float step_t = float(step) / float(axial_steps); + const Vec3f center = base + cone_axis * step_t; + const float ring_radius = radius * (1.f - step_t); + const ColorRGBA color = black ? black_color : linear_gradient_arrow_color(component_colors, stops, component_ids, linear_gradient_arrow_t(center, arrow_start, axis, arrow_length)); + for (int sector = 0; sector <= sectors; ++sector) { + const float a = 2.f * float(PI) * float(sector) / float(sectors); + const Vec3f radial = std::cos(a) * u + std::sin(a) * v; + const Vec3f normal = (radial + axis * 0.35f).normalized(); + ring_ids[size_t(step)][size_t(sector)] = + linear_gradient_add_vertex(geometry, center + ring_radius * radial, normal, color); + } + } + for (int i = 0; i < sectors; ++i) { - const float a0 = 2.f * float(PI) * float(i) / float(sectors); - const float a1 = 2.f * float(PI) * float(i + 1) / float(sectors); - const Vec3f r0 = std::cos(a0) * u + std::sin(a0) * v; - const Vec3f r1 = std::cos(a1) * u + std::sin(a1) * v; - const Vec3f p0 = base + radius * r0; - const Vec3f p1 = base + radius * r1; - const float base_t = std::clamp((base - arrow_start).dot(axis) / safe_arrow_length, 0.f, 1.f); - const ColorRGBA base_color = black ? black_color : linear_gradient_arrow_color(start_color, end_color, base_t); - const Vec3f n0 = (r0 + axis * 0.35f).normalized(); - const Vec3f n1 = (r1 + axis * 0.35f).normalized(); - const unsigned int i0 = linear_gradient_add_vertex(geometry, p0, n0, base_color); - const unsigned int i1 = linear_gradient_add_vertex(geometry, p1, n1, base_color); - const unsigned int i2 = linear_gradient_add_vertex(geometry, tip, axis, tip_color); - geometry.add_triangle(i0, i2, i1); + for (int step = 0; step + 1 < axial_steps; ++step) { + const unsigned int i0 = ring_ids[size_t(step)][size_t(i)]; + const unsigned int i1 = ring_ids[size_t(step)][size_t(i + 1)]; + const unsigned int i2 = ring_ids[size_t(step + 1)][size_t(i)]; + const unsigned int i3 = ring_ids[size_t(step + 1)][size_t(i + 1)]; + geometry.add_triangle(i0, i2, i1); + geometry.add_triangle(i1, i2, i3); + } + const unsigned int i0 = ring_ids[size_t(axial_steps - 1)][size_t(i)]; + const unsigned int i1 = ring_ids[size_t(axial_steps - 1)][size_t(i + 1)]; + const unsigned int tip_idx = linear_gradient_add_vertex(geometry, tip, axis, tip_color); + geometry.add_triangle(i0, tip_idx, i1); } } @@ -661,8 +685,9 @@ void append_linear_gradient_arrow(GUI::GLModel::Geometry &geometry, float radius, float head_radius, float head_length, - const std::array &start_color, - const std::array &end_color, + const std::vector> &component_colors, + const std::vector &stops, + const std::vector &component_ids, bool black) { const Vec3f delta = end - start; @@ -676,8 +701,8 @@ void append_linear_gradient_arrow(GUI::GLModel::Geometry &geometry, float clamped_head_length = std::min(head_length, length * 0.75f); clamped_head_length = std::max(clamped_head_length, std::min(radius * 2.f, length * 0.5f)); const Vec3f shaft_end = start + axis * std::max(radius, length - clamped_head_length); - append_linear_gradient_cylinder(geometry, start, shaft_end, start, length, axis, u, v, radius, start_color, end_color, black); - append_linear_gradient_cone(geometry, shaft_end, end, start, length, axis, u, v, head_radius, start_color, end_color, black); + append_linear_gradient_cylinder(geometry, start, shaft_end, start, length, axis, u, v, radius, component_colors, stops, component_ids, black); + append_linear_gradient_cone(geometry, shaft_end, end, start, length, axis, u, v, head_radius, component_colors, stops, component_ids, black); } } // namespace @@ -3173,8 +3198,14 @@ void GLVolumeCollection::render_linear_gradient_direction_arrows(const Transform continue; const std::vector component_ids = linear_gradient_component_ids_for_arrow(zone, num_physical); - if (component_ids.size() < 2) + if (component_ids.empty()) continue; + const std::vector gradient_stops = + TextureMappingManager::normalized_linear_gradient_stops(zone, num_physical); + std::vector> component_colors; + component_colors.reserve(component_ids.size()); + for (const unsigned int component_id : component_ids) + component_colors.emplace_back(linear_gradient_filament_color(component_id, filament_colors)); const LinearGradientArrowUsage usage = linear_gradient_arrow_usage(volumes, zone.zone_id); const std::optional start_anchor = linear_gradient_anchor_global_point(volumes, zone.linear_gradient_start); @@ -3219,8 +3250,6 @@ void GLVolumeCollection::render_linear_gradient_direction_arrows(const Transform delta = end - start; } - const std::array start_color = linear_gradient_filament_color(component_ids[0], filament_colors); - const std::array end_color = linear_gradient_filament_color(component_ids[1], filament_colors); const float shaft_radius = std::clamp(length * 0.018f, 0.35f, 2.2f); const float head_length = std::min(length * 0.24f, std::max(shaft_radius * 4.f, length * 0.12f)); const float head_radius = shaft_radius * 3.1f; @@ -3230,8 +3259,9 @@ void GLVolumeCollection::render_linear_gradient_direction_arrows(const Transform shaft_radius * 1.55f, head_radius * 1.28f, head_length, - start_color, - end_color, + component_colors, + gradient_stops, + component_ids, true); append_linear_gradient_arrow(color_geometry, start, @@ -3239,8 +3269,9 @@ void GLVolumeCollection::render_linear_gradient_direction_arrows(const Transform shaft_radius, head_radius, head_length, - start_color, - end_color, + component_colors, + gradient_stops, + component_ids, false); const float sphere_radius = shaft_radius * 2.45f; diff --git a/src/slic3r/GUI/MMUPaintedTexturePreview.cpp b/src/slic3r/GUI/MMUPaintedTexturePreview.cpp index 74471b99cca..1eca518b00e 100644 --- a/src/slic3r/GUI/MMUPaintedTexturePreview.cpp +++ b/src/slic3r/GUI/MMUPaintedTexturePreview.cpp @@ -124,6 +124,7 @@ struct SurfaceGradientPreviewSettings Vec3f linear_start = Vec3f::Zero(); Vec3f linear_end = Vec3f::UnitZ(); float linear_radius_mm = 1.f; + std::vector linear_gradient_stops; std::vector> linear_gradient_lut_colors; }; @@ -397,27 +398,6 @@ std::array decode_color(const std::string &color) }; } -std::vector compact_linear_gradient_weights(float t, size_t component_count) -{ - std::vector weights(component_count, 0.f); - if (component_count == 0) - return weights; - if (component_count == 1) { - weights[0] = 1.f; - return weights; - } - - weights[0] = 1.f - std::clamp(t, 0.f, 1.f); - weights[1] = std::clamp(t, 0.f, 1.f); - float max_weight = 0.f; - for (const float weight : weights) - max_weight = std::max(max_weight, std::clamp(weight, 0.f, 1.f)); - if (max_weight > k_epsilon) - for (float &weight : weights) - weight = std::clamp(weight / max_weight, 0.f, 1.f); - return weights; -} - ColorRGBA blend_component_colors(const std::vector> &colors, const std::vector &weights) { if (colors.empty() || weights.empty()) @@ -1065,6 +1045,8 @@ bool texture_preview_settings_invalid_for_filament(unsigned int filament_id, siz return false; if (is_image_zone(*zone)) return TextureMappingManager::component_count_mismatch(*zone, num_physical); + if (is_gradient_zone(*zone) && zone->is_linear_gradient()) + return TextureMappingManager::selected_component_ids(*zone, num_physical).empty(); if (is_gradient_zone(*zone)) return TextureMappingManager::selected_component_ids(*zone, num_physical).size() < 2; return false; @@ -4112,6 +4094,9 @@ bool build_simulated_image_texture_halftone_preview_model_for_state( std::vector decode_surface_gradient_component_ids(const TextureMappingZone &zone, size_t num_physical) { + if (zone.is_linear_gradient()) + return TextureMappingManager::linear_gradient_component_ids_from_stops(zone, num_physical); + std::vector ids; bool seen[10] = { false }; for (const char c : zone.component_ids) { @@ -4357,10 +4342,9 @@ std::optional surface_gradient_preview_settings_ const std::vector colors = physical_filament_colors_for_texture_preview(num_physical); SurfaceGradientPreviewSettings settings; settings.component_ids = decode_surface_gradient_component_ids(zone, num_physical); - if (zone.is_linear_gradient() && settings.component_ids.size() > 2) - settings.component_ids.resize(2); if (settings.component_ids.size() < 2) - return std::nullopt; + if (!zone.is_linear_gradient()) + return std::nullopt; settings.component_colors.reserve(settings.component_ids.size()); settings.strength_factors.reserve(settings.component_ids.size()); @@ -4380,12 +4364,14 @@ std::optional surface_gradient_preview_settings_ const ColorRGBA base_color = blend_component_colors(settings.component_colors, base_weights); settings.base_color = { base_color.r(), base_color.g(), base_color.b() }; if (zone.is_linear_gradient()) { + settings.linear_gradient_stops = TextureMappingManager::normalized_linear_gradient_stops(zone, num_physical); settings.linear_gradient_lut_colors.reserve(k_surface_gradient_preview_lut_size); for (size_t idx = 0; idx < k_surface_gradient_preview_lut_size; ++idx) { const float t = k_surface_gradient_preview_lut_size > 1 ? float(idx) / float(k_surface_gradient_preview_lut_size - 1) : 0.f; - const std::vector weights = compact_linear_gradient_weights(t, settings.component_colors.size()); + const std::vector weights = + TextureMappingManager::linear_gradient_compact_weights(t, settings.linear_gradient_stops, settings.component_ids); const std::array rgb = mix_color_solver_components(settings.component_colors, weights, ColorSolverMixModel::PigmentPainter); settings.linear_gradient_lut_colors.push_back({ std::clamp(rgb[0], 0.f, 1.f), @@ -5228,6 +5214,10 @@ static void texture_preview_mix_zone_baked_model_settings(size_t &signature, }; mix_anchor(zone.linear_gradient_start); mix_anchor(zone.linear_gradient_end); + for (const TextureMappingZone::LinearGradientStop &stop : TextureMappingManager::normalized_linear_gradient_stops(zone, num_physical)) { + signature_mix_float(stop.position, 10000.f); + signature_mix(std::hash{}(stop.filament_id)); + } } const std::vector physical_colors = physical_filament_colors_for_texture_preview(num_physical); diff --git a/src/slic3r/GUI/Plater.cpp b/src/slic3r/GUI/Plater.cpp index 2b73f603fc1..3a1a470d491 100644 --- a/src/slic3r/GUI/Plater.cpp +++ b/src/slic3r/GUI/Plater.cpp @@ -68,6 +68,7 @@ #include "libslic3r/Format/STL.hpp" #include "libslic3r/Format/DRC.hpp" #include "libslic3r/Format/STEP.hpp" +#include "libslic3r/ColorSolver.hpp" #include "libslic3r/Format/AMF.hpp" //#include "libslic3r/Format/3mf.hpp" #include "libslic3r/Format/bbs_3mf.hpp" @@ -879,6 +880,8 @@ static std::vector texture_mapping_selected_ids(const TextureMappi return id == 0 || id > num_physical || id > 9; }), ids.end()); ids.erase(std::unique(ids.begin(), ids.end()), ids.end()); + if (zone.is_linear_gradient()) + return ids; if (ids.size() < 2) { ids.clear(); for (size_t i = 1; i <= std::min(num_physical, 9); ++i) @@ -886,8 +889,6 @@ static std::vector texture_mapping_selected_ids(const TextureMappi } if (ids.size() < 2 && num_physical >= 2) ids = {1, 2}; - if (zone.is_linear_gradient() && ids.size() > 2) - ids.resize(2); return ids; } @@ -1054,12 +1055,14 @@ public: void set_data(const std::vector &palette, const std::vector &component_ids, const wxColour &fallback, + const std::vector &linear_gradient_stops = {}, bool linear_gradient = false, bool radial_gradient = false) { m_palette = palette; m_component_ids = component_ids; m_fallback = fallback.IsOk() ? fallback : wxColour("#26A69A"); + m_linear_gradient_stops = linear_gradient_stops; m_linear_gradient = linear_gradient; m_radial_gradient = radial_gradient; Refresh(); @@ -1073,6 +1076,24 @@ private: return m_fallback; } + wxColour gradient_color_for(double t) const + { + if (m_component_ids.empty()) + return m_fallback; + std::vector> colors; + colors.reserve(m_component_ids.size()); + for (const unsigned int id : m_component_ids) { + const wxColour color = color_for(id); + colors.push_back({float(color.Red()) / 255.f, float(color.Green()) / 255.f, float(color.Blue()) / 255.f}); + } + const std::vector weights = + TextureMappingManager::linear_gradient_compact_weights(float(t), m_linear_gradient_stops, m_component_ids); + const std::array mixed = mix_color_solver_components(colors, weights, ColorSolverMixModel::PigmentPainter); + return wxColour(std::clamp(int(std::lround(mixed[0] * 255.f)), 0, 255), + std::clamp(int(std::lround(mixed[1] * 255.f)), 0, 255), + std::clamp(int(std::lround(mixed[2] * 255.f)), 0, 255)); + } + void on_paint(wxPaintEvent &) { wxAutoBufferedPaintDC dc(this); @@ -1084,12 +1105,7 @@ private: dc.SetPen(*wxTRANSPARENT_PEN); dc.SetBrush(wxBrush(m_fallback)); dc.DrawRectangle(rect); - if (m_linear_gradient && m_component_ids.size() >= 2) { - const wxColour start = color_for(m_component_ids[0]); - const wxColour end = color_for(m_component_ids[1]); - auto mix_channel = [](double t, unsigned char a, unsigned char b) { - return static_cast(std::lround(double(a) * (1.0 - t) + double(b) * t)); - }; + if (m_linear_gradient && !m_component_ids.empty()) { if (m_radial_gradient) { const double cx = double(rect.GetLeft()) + 0.5 * double(rect.GetWidth() - 1); const double cy = double(rect.GetTop()) + 0.5 * double(rect.GetHeight() - 1); @@ -1097,9 +1113,7 @@ private: for (int y = rect.GetTop(); y <= rect.GetBottom(); ++y) { for (int x = rect.GetLeft(); x <= rect.GetRight(); ++x) { const double t = std::clamp(std::hypot(double(x) - cx, double(y) - cy) / radius, 0.0, 1.0); - dc.SetPen(wxPen(wxColour(mix_channel(t, start.Red(), end.Red()), - mix_channel(t, start.Green(), end.Green()), - mix_channel(t, start.Blue(), end.Blue())))); + dc.SetPen(wxPen(gradient_color_for(t))); dc.DrawPoint(x, y); } } @@ -1108,9 +1122,7 @@ private: const int h = std::max(1, rect.GetHeight()); for (int y = 0; y < h; ++y) { const double t = h <= 1 ? 0.0 : double(h - 1 - y) / double(h - 1); - dc.SetBrush(wxBrush(wxColour(mix_channel(t, start.Red(), end.Red()), - mix_channel(t, start.Green(), end.Green()), - mix_channel(t, start.Blue(), end.Blue())))); + dc.SetBrush(wxBrush(gradient_color_for(t))); dc.DrawRectangle(rect.GetLeft(), rect.GetTop() + y, rect.GetWidth(), 1); } } @@ -1128,11 +1140,331 @@ private: std::vector m_palette; std::vector m_component_ids; + std::vector m_linear_gradient_stops; wxColour m_fallback {wxColour("#26A69A")}; bool m_linear_gradient { false }; bool m_radial_gradient { false }; }; +class LinearGradientStopsBar : public wxPanel +{ +public: + explicit LinearGradientStopsBar(wxWindow *parent) + : wxPanel(parent, wxID_ANY, wxDefaultPosition, from_dip_for_parent(parent, wxSize(320, 44)), wxBORDER_NONE) + { + SetBackgroundStyle(wxBG_STYLE_PAINT); + SetMinSize(wxSize(FromDIP(220), FromDIP(44))); + Bind(wxEVT_PAINT, &LinearGradientStopsBar::on_paint, this); + Bind(wxEVT_LEFT_DOWN, &LinearGradientStopsBar::on_left_down, this); + Bind(wxEVT_LEFT_DCLICK, &LinearGradientStopsBar::on_left_dclick, this); + Bind(wxEVT_LEFT_UP, &LinearGradientStopsBar::on_left_up, this); + Bind(wxEVT_MOTION, &LinearGradientStopsBar::on_motion, this); + } + + void set_data(const std::vector &palette, + size_t num_physical, + const std::vector &stops, + int selected_index) + { + m_palette = palette; + m_num_physical = num_physical; + m_stops = stops; + sort_stops(); + m_selected_index = selected_index >= 0 && selected_index < int(m_stops.size()) ? selected_index : -1; + Refresh(); + } + + const std::vector &stops() const { return m_stops; } + int selected_index() const { return m_selected_index; } + + void set_changed_callback(std::function &, int)> callback) + { + m_changed_callback = std::move(callback); + } + + void set_selection_callback(std::function callback) + { + m_selection_callback = std::move(callback); + } + + void add_point_at_largest_gap() + { + if (m_stops.empty()) + return; + sort_stops(); + float best_gap = std::clamp(m_stops.front().position, 0.f, 1.f); + float best_position = 0.5f * best_gap; + for (size_t i = 0; i + 1 < m_stops.size(); ++i) { + const float gap = m_stops[i + 1].position - m_stops[i].position; + if (gap > best_gap) { + best_gap = gap; + best_position = 0.5f * (m_stops[i].position + m_stops[i + 1].position); + } + } + const float tail_gap = std::clamp(1.f - m_stops.back().position, 0.f, 1.f); + if (tail_gap > best_gap) + best_position = 0.5f * (m_stops.back().position + 1.f); + add_stop(best_position); + } + + void remove_selected() + { + if (m_selected_index < 0 || m_selected_index >= int(m_stops.size()) || m_stops.size() <= 2) + return; + m_stops.erase(m_stops.begin() + m_selected_index); + m_selected_index = -1; + notify_changed(); + notify_selection(); + Refresh(); + } + + void set_selected_filament(unsigned int filament_id) + { + if (m_selected_index < 0 || m_selected_index >= int(m_stops.size()) || filament_id == 0) + return; + m_stops[size_t(m_selected_index)].filament_id = normalize_filament_id(filament_id); + notify_changed(); + Refresh(); + } + + void set_edge_filament(bool start_edge, unsigned int filament_id) + { + if (m_stops.empty() || filament_id == 0) + return; + sort_stops(); + m_stops[start_edge ? 0 : m_stops.size() - 1].filament_id = normalize_filament_id(filament_id); + notify_changed(); + Refresh(); + } + +private: + unsigned int max_filament_id() const + { + return unsigned(std::min(m_num_physical, 9)); + } + + unsigned int normalize_filament_id(unsigned int filament_id) const + { + const unsigned int max_id = max_filament_id(); + return max_id == 0 ? 0u : std::clamp(filament_id, 1u, max_id); + } + + wxColour color_for(unsigned int id) const + { + if (id >= 1 && id <= m_palette.size()) + return m_palette[id - 1]; + return wxColour("#808080"); + } + + wxColour gradient_color_for(float t) const + { + std::vector component_ids; + bool seen[10] = { false }; + for (const TextureMappingZone::LinearGradientStop &stop : m_stops) { + const unsigned int id = stop.filament_id; + if (id == 0 || id > m_num_physical || id > 9 || seen[id]) + continue; + seen[id] = true; + component_ids.emplace_back(id); + } + if (component_ids.empty()) + return wxColour("#808080"); + std::vector> colors; + colors.reserve(component_ids.size()); + for (const unsigned int id : component_ids) { + const wxColour color = color_for(id); + colors.push_back({float(color.Red()) / 255.f, float(color.Green()) / 255.f, float(color.Blue()) / 255.f}); + } + const std::vector weights = TextureMappingManager::linear_gradient_compact_weights(t, m_stops, component_ids); + const std::array mixed = mix_color_solver_components(colors, weights, ColorSolverMixModel::PigmentPainter); + return wxColour(std::clamp(int(std::lround(mixed[0] * 255.f)), 0, 255), + std::clamp(int(std::lround(mixed[1] * 255.f)), 0, 255), + std::clamp(int(std::lround(mixed[2] * 255.f)), 0, 255)); + } + + wxRect bar_rect() const + { + const wxSize size = GetClientSize(); + const int handle_radius = FromDIP(7); + const int left = handle_radius + FromDIP(2); + const int right = std::max(left + 1, size.x - handle_radius - FromDIP(2)); + const int height = FromDIP(14); + const int top = std::max(FromDIP(4), (size.y - height) / 2); + return wxRect(left, top, right - left, height); + } + + int x_for_position(float position) const + { + const wxRect rect = bar_rect(); + return rect.GetLeft() + int(std::lround(std::clamp(position, 0.f, 1.f) * float(rect.GetWidth()))); + } + + float position_for_x(int x) const + { + const wxRect rect = bar_rect(); + if (rect.GetWidth() <= 0) + return 0.f; + return std::clamp(float(x - rect.GetLeft()) / float(rect.GetWidth()), 0.f, 1.f); + } + + int hit_stop(const wxPoint &point) const + { + const wxRect rect = bar_rect(); + const int cy = rect.GetTop() + rect.GetHeight() / 2; + const int radius = FromDIP(9); + int best_idx = -1; + int best_dist_sq = radius * radius + 1; + for (size_t i = 0; i < m_stops.size(); ++i) { + const int dx = point.x - x_for_position(m_stops[i].position); + const int dy = point.y - cy; + const int dist_sq = dx * dx + dy * dy; + if (dist_sq <= radius * radius && dist_sq < best_dist_sq) { + best_idx = int(i); + best_dist_sq = dist_sq; + } + } + return best_idx; + } + + void sort_stops() + { + std::stable_sort(m_stops.begin(), m_stops.end(), [](const auto &lhs, const auto &rhs) { + return lhs.position < rhs.position; + }); + } + + unsigned int filament_for_new_stop(float position) const + { + if (m_stops.empty()) + return normalize_filament_id(1); + size_t best_idx = 0; + float best_distance = std::numeric_limits::max(); + for (size_t i = 0; i < m_stops.size(); ++i) { + const float distance = std::abs(m_stops[i].position - position); + if (distance < best_distance) { + best_idx = i; + best_distance = distance; + } + } + return normalize_filament_id(m_stops[best_idx].filament_id); + } + + void add_stop(float position) + { + TextureMappingZone::LinearGradientStop stop; + stop.position = std::clamp(position, 0.f, 1.f); + stop.filament_id = filament_for_new_stop(stop.position); + sort_stops(); + auto insert_it = std::upper_bound(m_stops.begin(), m_stops.end(), stop.position, [](float value, const auto &rhs) { + return value < rhs.position; + }); + m_selected_index = int(insert_it - m_stops.begin()); + m_stops.insert(insert_it, stop); + notify_changed(); + notify_selection(); + Refresh(); + } + + void set_stop_position(int index, float position) + { + if (index < 0 || index >= int(m_stops.size())) + return; + TextureMappingZone::LinearGradientStop stop = m_stops[size_t(index)]; + stop.position = std::clamp(position, 0.f, 1.f); + m_stops.erase(m_stops.begin() + index); + auto insert_it = std::upper_bound(m_stops.begin(), m_stops.end(), stop.position, [](float value, const auto &rhs) { + return value < rhs.position; + }); + m_selected_index = int(insert_it - m_stops.begin()); + m_stops.insert(insert_it, stop); + notify_changed(); + notify_selection(); + Refresh(); + } + + void notify_changed() + { + if (m_changed_callback) + m_changed_callback(m_stops, m_selected_index); + } + + void notify_selection() + { + if (m_selection_callback) + m_selection_callback(m_selected_index); + } + + void on_paint(wxPaintEvent &) + { + wxAutoBufferedPaintDC dc(this); + dc.SetBackground(wxBrush(GetBackgroundColour())); + dc.Clear(); + const wxRect rect = bar_rect(); + dc.SetPen(*wxTRANSPARENT_PEN); + for (int x = rect.GetLeft(); x <= rect.GetRight(); ++x) { + const float t = position_for_x(x); + dc.SetBrush(wxBrush(gradient_color_for(t))); + dc.DrawRectangle(x, rect.GetTop(), 1, rect.GetHeight()); + } + dc.SetPen(wxPen(wxColour(90, 90, 90), FromDIP(1))); + dc.SetBrush(*wxTRANSPARENT_BRUSH); + dc.DrawRoundedRectangle(rect, FromDIP(3)); + + const int cy = rect.GetTop() + rect.GetHeight() / 2; + const int radius = FromDIP(7); + for (size_t i = 0; i < m_stops.size(); ++i) { + const int x = x_for_position(m_stops[i].position); + dc.SetBrush(wxBrush(color_for(m_stops[i].filament_id))); + dc.SetPen(wxPen(i == size_t(m_selected_index) ? wxColour(255, 255, 255) : wxColour(30, 30, 30), + i == size_t(m_selected_index) ? FromDIP(3) : FromDIP(2))); + dc.DrawCircle(wxPoint(x, cy), radius); + } + } + + void on_left_down(wxMouseEvent &evt) + { + const int hit = hit_stop(evt.GetPosition()); + if (hit >= 0) { + m_selected_index = hit; + m_dragging = true; + if (!HasCapture()) + CaptureMouse(); + } else { + m_selected_index = -1; + } + notify_selection(); + Refresh(); + } + + void on_left_dclick(wxMouseEvent &evt) + { + if (hit_stop(evt.GetPosition()) < 0) + add_stop(position_for_x(evt.GetX())); + } + + void on_left_up(wxMouseEvent &) + { + m_dragging = false; + if (HasCapture()) + ReleaseMouse(); + } + + void on_motion(wxMouseEvent &evt) + { + if (!m_dragging || m_selected_index < 0 || !evt.Dragging() || !evt.LeftIsDown()) + return; + set_stop_position(m_selected_index, position_for_x(evt.GetX())); + } + + std::vector m_palette; + size_t m_num_physical { 0 }; + std::vector m_stops; + int m_selected_index { -1 }; + bool m_dragging { false }; + std::function &, int)> m_changed_callback; + std::function m_selection_callback; +}; + class TextureMappingNumberSwatch : public wxPanel { public: @@ -4549,13 +4881,15 @@ Sidebar::Sidebar(Plater *parent) return; ConfigOptionStrings *colors_opt = bundle->project_config.option("filament_colour"); std::vector colors = colors_opt ? colors_opt->values : std::vector(); - if (colors.size() < 2) + if (colors.empty()) return; const std::string serialized = bundle->project_config.has("texture_mapping_definitions") ? bundle->project_config.opt_string("texture_mapping_definitions") : std::string(); bundle->texture_mapping_zones.load_entries(serialized, colors); - bundle->texture_mapping_zones.add_zone(colors.size(), colors); + bundle->texture_mapping_zones.add_zone(colors.size(), + colors, + colors.size() < 2 ? int(TextureMappingZone::LinearGradient) : int(TextureMappingZone::ImageTexture)); persist_texture_mapping(); }; @@ -6295,9 +6629,9 @@ void Sidebar::update_texture_mapping_panel(bool sync_manager) }; if (p->m_btn_add_texture_map != nullptr) - p->m_btn_add_texture_map->Enable(num_physical >= 2); + p->m_btn_add_texture_map->Enable(num_physical >= 1); - if (num_physical < 2) { + if (num_physical == 0) { p->m_panel_texture_mapping_title->Hide(); p->m_panel_texture_mapping_content->Hide(); if (p->m_panel_texture_mapping_resize_handle != nullptr) @@ -6357,49 +6691,6 @@ void Sidebar::update_texture_mapping_panel(bool sync_manager) combo->SetSelection(std::clamp(int(id) - 1, 0, int(combo->GetCount() - 1))); }; - auto linear_gradient_filament_ids = [num_physical, filament_combo_id, set_filament_combo_id](::ComboBox *start_combo, - ::ComboBox *end_combo, - const TextureMappingZone &zone, - int changed_side) { - const unsigned int count = unsigned(std::min(num_physical, 9)); - std::vector ids; - if (count == 0) - return ids; - auto normalize_id = [count](unsigned int id) { - return id >= 1 && id <= count ? id : 1u; - }; - unsigned int start_id = normalize_id(filament_combo_id(start_combo, zone.component_a)); - unsigned int end_id = normalize_id(filament_combo_id(end_combo, zone.component_b)); - auto different_id = [count](unsigned int id) { - for (unsigned int candidate = 1; candidate <= count; ++candidate) - if (candidate != id) - return candidate; - return id; - }; - if (count == 1) { - start_id = 1; - end_id = 1; - } else if (start_id == end_id) { - if (changed_side == 1) { - end_id = normalize_id(zone.component_a); - if (end_id == start_id) - end_id = different_id(start_id); - } else if (changed_side == 2) { - start_id = normalize_id(zone.component_b); - if (start_id == end_id) - start_id = different_id(end_id); - } else { - end_id = normalize_id(zone.component_b); - if (end_id == start_id) - end_id = different_id(start_id); - } - } - set_filament_combo_id(start_combo, start_id); - set_filament_combo_id(end_combo, end_id); - ids = {start_id, end_id}; - return ids; - }; - auto repaint_sidebar_checkbox = [](::CheckBox *checkbox) { if (checkbox == nullptr) return; @@ -6569,6 +6860,7 @@ void Sidebar::update_texture_mapping_panel(bool sync_manager) TextureMappingManager::effective_texture_component_ids(entry, num_physical, physical_colors) : texture_mapping_selected_ids(entry, num_physical), parse_texture_mapping_color(entry.display_color), + entry.is_linear_gradient() ? TextureMappingManager::normalized_linear_gradient_stops(entry, num_physical) : std::vector(), entry.is_linear_gradient(), entry.is_radial_linear_gradient()); header_sizer->Add(preview, 0, wxALIGN_CENTER_VERTICAL | wxRIGHT, gap); @@ -6592,6 +6884,7 @@ void Sidebar::update_texture_mapping_panel(bool sync_manager) TextureMappingManager::effective_texture_component_ids(zone, num_physical, physical_colors) : texture_mapping_selected_ids(zone, num_physical), parse_texture_mapping_color(zone.display_color), + zone.is_linear_gradient() ? TextureMappingManager::normalized_linear_gradient_stops(zone, num_physical) : std::vector(), zone.is_linear_gradient(), zone.is_radial_linear_gradient()); } @@ -6665,15 +6958,29 @@ void Sidebar::update_texture_mapping_panel(bool sync_manager) linear_gradient_mode_row->Add(linear_gradient_mode_choice, 0, wxALIGN_CENTER_VERTICAL | wxRIGHT, gap); editor_sizer->Add(linear_gradient_mode_row, 0, wxEXPAND | wxLEFT | wxRIGHT | wxTOP, gap); + std::vector initial_linear_gradient_stops = + TextureMappingManager::normalized_linear_gradient_stops(entry, num_physical); + const unsigned int initial_start_filament = initial_linear_gradient_stops.empty() ? 1 : initial_linear_gradient_stops.front().filament_id; + const unsigned int initial_end_filament = initial_linear_gradient_stops.empty() ? initial_start_filament : initial_linear_gradient_stops.back().filament_id; + auto *linear_gradient_stops_bar = new LinearGradientStopsBar(editor); + linear_gradient_stops_bar->SetBackgroundColour(row_bg); + linear_gradient_stops_bar->set_data(palette, num_physical, initial_linear_gradient_stops, -1); + editor_sizer->Add(linear_gradient_stops_bar, 0, wxEXPAND | wxLEFT | wxRIGHT | wxTOP, gap); + auto *linear_gradient_row = new wxBoxSizer(wxHORIZONTAL); - linear_gradient_row->Add(new wxStaticText(editor, wxID_ANY, _L("Gradient")), 0, wxALIGN_CENTER_VERTICAL | wxRIGHT, gap); - auto *start_filament_combo = make_filament_swatch_combo(editor, selected_ids.empty() ? 1 : selected_ids.front()); - auto *end_filament_combo = make_filament_swatch_combo(editor, selected_ids.size() > 1 ? selected_ids[1] : (selected_ids.empty() ? 1 : selected_ids.front())); + auto *start_filament_combo = make_filament_swatch_combo(editor, initial_start_filament); + auto *end_filament_combo = make_filament_swatch_combo(editor, initial_end_filament); + auto *point_filament_combo = make_filament_swatch_combo(editor, initial_start_filament); const bool can_choose_linear_gradient_filaments = std::min(num_physical, 9) > 1; if (start_filament_combo != nullptr) start_filament_combo->Enable(can_choose_linear_gradient_filaments); if (end_filament_combo != nullptr) end_filament_combo->Enable(can_choose_linear_gradient_filaments); + if (point_filament_combo != nullptr) + point_filament_combo->Enable(can_choose_linear_gradient_filaments); + auto *add_point_btn = new wxButton(editor, wxID_ANY, _L("Add Point")); + auto *add_selected_point_btn = new wxButton(editor, wxID_ANY, _L("Add Point")); + auto *remove_point_btn = new wxButton(editor, wxID_ANY, _L("Remove Point")); const bool entry_radial_gradient = linear_gradient_mode_selection(entry) == 1; auto *set_start_btn = new wxButton(editor, wxID_ANY, @@ -6688,8 +6995,16 @@ void Sidebar::update_texture_mapping_panel(bool sync_manager) linear_gradient_row->Add(start_filament_combo, 0, wxALIGN_CENTER_VERTICAL | wxRIGHT, gap); linear_gradient_row->Add(new wxStaticText(editor, wxID_ANY, _L("End")), 0, wxALIGN_CENTER_VERTICAL | wxRIGHT, gap / 2); linear_gradient_row->Add(end_filament_combo, 0, wxALIGN_CENTER_VERTICAL | wxRIGHT, gap); + linear_gradient_row->Add(add_point_btn, 0, wxALIGN_CENTER_VERTICAL | wxRIGHT, gap); editor_sizer->Add(linear_gradient_row, 0, wxEXPAND | wxLEFT | wxRIGHT | wxTOP, gap); + auto *linear_gradient_point_row = new wxBoxSizer(wxHORIZONTAL); + linear_gradient_point_row->Add(new wxStaticText(editor, wxID_ANY, _L("Point")), 0, wxALIGN_CENTER_VERTICAL | wxRIGHT, gap / 2); + linear_gradient_point_row->Add(point_filament_combo, 0, wxALIGN_CENTER_VERTICAL | wxRIGHT, gap); + linear_gradient_point_row->Add(remove_point_btn, 0, wxALIGN_CENTER_VERTICAL | wxRIGHT, gap); + linear_gradient_point_row->Add(add_selected_point_btn, 0, wxALIGN_CENTER_VERTICAL | wxRIGHT, gap); + editor_sizer->Add(linear_gradient_point_row, 0, wxEXPAND | wxLEFT | wxRIGHT | wxTOP, gap); + auto *linear_gradient_buttons_row = new wxWrapSizer(wxHORIZONTAL, wxWRAPSIZER_DEFAULT_FLAGS); linear_gradient_buttons_row->Add(set_start_btn, 0, wxALIGN_CENTER_VERTICAL | wxRIGHT, gap); linear_gradient_buttons_row->Add(set_end_btn, 0, wxALIGN_CENTER_VERTICAL | wxRIGHT, gap); @@ -6774,6 +7089,41 @@ void Sidebar::update_texture_mapping_panel(bool sync_manager) Layout(); }; + auto sync_linear_gradient_stop_controls = [this, + editor_sizer, + linear_gradient_stops_bar, + linear_gradient_row, + linear_gradient_point_row, + start_filament_combo, + end_filament_combo, + point_filament_combo, + remove_point_btn, + surface_choice, + set_filament_combo_id, + editor, + row, + update_texture_mapping_area_height]() { + const bool linear_gradient = surface_choice != nullptr && surface_choice->GetSelection() == 1; + const int selected_index = linear_gradient_stops_bar != nullptr ? linear_gradient_stops_bar->selected_index() : -1; + const std::vector stops = + linear_gradient_stops_bar != nullptr ? linear_gradient_stops_bar->stops() : std::vector(); + if (selected_index >= 0 && selected_index < int(stops.size())) { + set_filament_combo_id(point_filament_combo, stops[size_t(selected_index)].filament_id); + } else if (!stops.empty()) { + set_filament_combo_id(start_filament_combo, stops.front().filament_id); + set_filament_combo_id(end_filament_combo, stops.back().filament_id); + } + if (remove_point_btn != nullptr) + remove_point_btn->Enable(stops.size() > 2); + editor_sizer->Show(linear_gradient_row, linear_gradient && selected_index < 0, true); + editor_sizer->Show(linear_gradient_point_row, linear_gradient && selected_index >= 0, true); + editor->Layout(); + row->Layout(); + update_texture_mapping_area_height(); + m_scrolled_sizer->Layout(); + Layout(); + }; + auto *mode_row = new wxWrapSizer(wxHORIZONTAL, wxWRAPSIZER_DEFAULT_FLAGS); mode_row->Add(new wxStaticText(editor, wxID_ANY, _L("Filament colors")), 0, wxALIGN_CENTER_VERTICAL | wxRIGHT, gap); auto *mode_choice = make_sidebar_combo(editor, @@ -6812,10 +7162,10 @@ void Sidebar::update_texture_mapping_panel(bool sync_manager) editor_sizer->Add(button_row, 0, wxEXPAND | wxALL, gap); auto apply_controls = [zone_index, mgr_ptr, num_physical, filament_checks, surface_choice, - start_filament_combo, end_filament_combo, linear_gradient_mode_choice, linear_gradient_radius_spin, + linear_gradient_stops_bar, linear_gradient_mode_choice, linear_gradient_radius_spin, linear_gradient_radius_percent_chk, - linear_gradient_filament_ids, surface_pattern_from_selection, linear_gradient_mode_from_selection, mode_choice, preview_colors_chk, - show_direction_arrow_chk, contrast_spin, apply_zone](int changed_side = 0) { + surface_pattern_from_selection, linear_gradient_mode_from_selection, mode_choice, preview_colors_chk, + show_direction_arrow_chk, contrast_spin, apply_zone]() { if (mgr_ptr == nullptr) return; auto &rows = mgr_ptr->zones(); @@ -6825,7 +7175,11 @@ void Sidebar::update_texture_mapping_panel(bool sync_manager) const int surface_pattern = surface_pattern_from_selection(surface_choice != nullptr ? surface_choice->GetSelection() : 0); std::vector ids; if (surface_pattern == int(TextureMappingZone::LinearGradient)) { - ids = linear_gradient_filament_ids(start_filament_combo, end_filament_combo, updated, changed_side); + updated.linear_gradient_stops = linear_gradient_stops_bar != nullptr ? + linear_gradient_stops_bar->stops() : + TextureMappingManager::normalized_linear_gradient_stops(updated, num_physical); + updated.linear_gradient_stops = TextureMappingManager::normalized_linear_gradient_stops(updated, num_physical); + ids = TextureMappingManager::linear_gradient_component_ids_from_stops(updated, num_physical); } else { for (size_t idx = 0; idx < filament_checks.size(); ++idx) if (filament_checks[idx] != nullptr && filament_checks[idx]->GetValue()) @@ -6836,8 +7190,13 @@ void Sidebar::update_texture_mapping_panel(bool sync_manager) updated.enabled = true; updated.surface_pattern = surface_pattern; updated.component_ids = encode_texture_mapping_component_ids(ids); - updated.component_a = ids.empty() ? 1 : ids.front(); - updated.component_b = ids.size() > 1 ? ids[1] : updated.component_a; + if (updated.is_linear_gradient() && !updated.linear_gradient_stops.empty()) { + updated.component_a = updated.linear_gradient_stops.front().filament_id; + updated.component_b = updated.linear_gradient_stops.back().filament_id; + } else { + updated.component_a = ids.empty() ? 1 : ids.front(); + updated.component_b = ids.size() > 1 ? ids[1] : updated.component_a; + } updated.linear_gradient_mode = linear_gradient_mode_from_selection(linear_gradient_mode_choice != nullptr ? linear_gradient_mode_choice->GetSelection() : 0); @@ -6862,6 +7221,16 @@ void Sidebar::update_texture_mapping_panel(bool sync_manager) apply_zone(std::move(updated)); }; + if (linear_gradient_stops_bar != nullptr) { + linear_gradient_stops_bar->set_changed_callback([apply_controls, sync_linear_gradient_stop_controls](const std::vector &, int) { + sync_linear_gradient_stop_controls(); + apply_controls(); + }); + linear_gradient_stops_bar->set_selection_callback([sync_linear_gradient_stop_controls](int) { + sync_linear_gradient_stop_controls(); + }); + } + contrast_spin->Bind(wxEVT_CHAR_HOOK, [contrast_spin, apply_controls](wxKeyEvent &evt) { const int key = evt.GetKeyCode(); if (key == WXK_UP || key == WXK_NUMPAD_UP || key == WXK_DOWN || key == WXK_NUMPAD_DOWN) { @@ -6872,14 +7241,17 @@ void Sidebar::update_texture_mapping_panel(bool sync_manager) } evt.Skip(); }); - auto update_pattern_visibility = [this, editor_sizer, filaments_row, linear_gradient_mode_row, linear_gradient_mode_choice, linear_gradient_row, linear_gradient_buttons_row, linear_gradient_radius_row, show_direction_arrow_row, mode_row, contrast_row, preview_colors_row, offset_btn, advanced_btn, surface_choice, row, editor, update_texture_mapping_area_height, set_linear_gradient_picker_button_labels]() { + auto update_pattern_visibility = [this, editor_sizer, filaments_row, linear_gradient_mode_row, linear_gradient_mode_choice, linear_gradient_stops_bar, linear_gradient_row, linear_gradient_point_row, linear_gradient_buttons_row, linear_gradient_radius_row, show_direction_arrow_row, mode_row, contrast_row, preview_colors_row, offset_btn, advanced_btn, surface_choice, row, editor, update_texture_mapping_area_height, set_linear_gradient_picker_button_labels, sync_linear_gradient_stop_controls]() { const int selection = surface_choice == nullptr ? 0 : surface_choice->GetSelection(); const bool image_texture = selection == 0; const bool linear_gradient = selection == 1; const bool radial_gradient = linear_gradient && linear_gradient_mode_choice != nullptr && linear_gradient_mode_choice->GetSelection() == 1; + const int selected_stop = linear_gradient_stops_bar != nullptr ? linear_gradient_stops_bar->selected_index() : -1; editor_sizer->Show(filaments_row, !linear_gradient, true); editor_sizer->Show(linear_gradient_mode_row, linear_gradient, true); - editor_sizer->Show(linear_gradient_row, linear_gradient, true); + editor_sizer->Show(linear_gradient_stops_bar, linear_gradient, true); + editor_sizer->Show(linear_gradient_row, linear_gradient && selected_stop < 0, true); + editor_sizer->Show(linear_gradient_point_row, linear_gradient && selected_stop >= 0, true); editor_sizer->Show(linear_gradient_buttons_row, linear_gradient, true); editor_sizer->Show(linear_gradient_radius_row, radial_gradient, true); editor_sizer->Show(show_direction_arrow_row, linear_gradient, true); @@ -6891,6 +7263,7 @@ void Sidebar::update_texture_mapping_panel(bool sync_manager) if (advanced_btn != nullptr) advanced_btn->Show(); set_linear_gradient_picker_button_labels(GLGizmoTextureGradientPointPicker::Target::None); + sync_linear_gradient_stop_controls(); editor->Layout(); row->Layout(); update_texture_mapping_area_height(); @@ -6915,9 +7288,33 @@ void Sidebar::update_texture_mapping_panel(bool sync_manager) evt.Skip(); }); if (start_filament_combo != nullptr) - start_filament_combo->Bind(wxEVT_COMBOBOX, [apply_controls](wxCommandEvent &) { apply_controls(1); }); + start_filament_combo->Bind(wxEVT_COMBOBOX, [linear_gradient_stops_bar, start_filament_combo, filament_combo_id](wxCommandEvent &) { + if (linear_gradient_stops_bar != nullptr) + linear_gradient_stops_bar->set_edge_filament(true, filament_combo_id(start_filament_combo, 1)); + }); if (end_filament_combo != nullptr) - end_filament_combo->Bind(wxEVT_COMBOBOX, [apply_controls](wxCommandEvent &) { apply_controls(2); }); + end_filament_combo->Bind(wxEVT_COMBOBOX, [linear_gradient_stops_bar, end_filament_combo, filament_combo_id](wxCommandEvent &) { + if (linear_gradient_stops_bar != nullptr) + linear_gradient_stops_bar->set_edge_filament(false, filament_combo_id(end_filament_combo, 1)); + }); + if (point_filament_combo != nullptr) + point_filament_combo->Bind(wxEVT_COMBOBOX, [linear_gradient_stops_bar, point_filament_combo, filament_combo_id](wxCommandEvent &) { + if (linear_gradient_stops_bar != nullptr) + linear_gradient_stops_bar->set_selected_filament(filament_combo_id(point_filament_combo, 1)); + }); + auto add_linear_gradient_point = [linear_gradient_stops_bar](wxCommandEvent &) { + if (linear_gradient_stops_bar != nullptr) + linear_gradient_stops_bar->add_point_at_largest_gap(); + }; + if (add_point_btn != nullptr) + add_point_btn->Bind(wxEVT_BUTTON, add_linear_gradient_point); + if (add_selected_point_btn != nullptr) + add_selected_point_btn->Bind(wxEVT_BUTTON, add_linear_gradient_point); + if (remove_point_btn != nullptr) + remove_point_btn->Bind(wxEVT_BUTTON, [linear_gradient_stops_bar](wxCommandEvent &) { + if (linear_gradient_stops_bar != nullptr) + linear_gradient_stops_bar->remove_selected(); + }); int last_mode_selection = mode_choice->GetSelection(); auto on_mode_choice = [this, zone_index, @@ -6925,12 +7322,10 @@ void Sidebar::update_texture_mapping_panel(bool sync_manager) num_physical, physical_colors, filament_checks, - start_filament_combo, - end_filament_combo, + linear_gradient_stops_bar, linear_gradient_mode_choice, linear_gradient_radius_spin, linear_gradient_radius_percent_chk, - linear_gradient_filament_ids, linear_gradient_mode_from_selection, mode_choice, surface_choice, @@ -6955,7 +7350,11 @@ void Sidebar::update_texture_mapping_panel(bool sync_manager) const int surface_pattern = surface_pattern_from_selection(surface_choice != nullptr ? surface_choice->GetSelection() : 0); std::vector ids; if (surface_pattern == int(TextureMappingZone::LinearGradient)) { - ids = linear_gradient_filament_ids(start_filament_combo, end_filament_combo, updated, 0); + updated.linear_gradient_stops = linear_gradient_stops_bar != nullptr ? + linear_gradient_stops_bar->stops() : + TextureMappingManager::normalized_linear_gradient_stops(updated, num_physical); + updated.linear_gradient_stops = TextureMappingManager::normalized_linear_gradient_stops(updated, num_physical); + ids = TextureMappingManager::linear_gradient_component_ids_from_stops(updated, num_physical); } else { for (size_t idx = 0; idx < filament_checks.size(); ++idx) if (filament_checks[idx] != nullptr && filament_checks[idx]->GetValue()) @@ -6967,8 +7366,13 @@ void Sidebar::update_texture_mapping_panel(bool sync_manager) updated.enabled = true; updated.surface_pattern = surface_pattern; updated.component_ids = encode_texture_mapping_component_ids(ids); - updated.component_a = ids.empty() ? 1 : ids.front(); - updated.component_b = ids.size() > 1 ? ids[1] : updated.component_a; + if (updated.is_linear_gradient() && !updated.linear_gradient_stops.empty()) { + updated.component_a = updated.linear_gradient_stops.front().filament_id; + updated.component_b = updated.linear_gradient_stops.back().filament_id; + } else { + updated.component_a = ids.empty() ? 1 : ids.front(); + updated.component_b = ids.size() > 1 ? ids[1] : updated.component_a; + } updated.linear_gradient_mode = linear_gradient_mode_from_selection(linear_gradient_mode_choice != nullptr ? linear_gradient_mode_choice->GetSelection() : 0);