From f27801b4ef9b76aa438cc012673e95acfff78179 Mon Sep 17 00:00:00 2001 From: sentientstardust Date: Mon, 11 May 2026 01:27:44 +0100 Subject: [PATCH] Add controls to allow resizing image textures, and simplifying RGBA data --- .../GUI/Gizmos/GLGizmoMmuSegmentation.cpp | 799 +++++++++++++++++- 1 file changed, 778 insertions(+), 21 deletions(-) diff --git a/src/slic3r/GUI/Gizmos/GLGizmoMmuSegmentation.cpp b/src/slic3r/GUI/Gizmos/GLGizmoMmuSegmentation.cpp index 85a9f11df06..4cee2ab38e6 100644 --- a/src/slic3r/GUI/Gizmos/GLGizmoMmuSegmentation.cpp +++ b/src/slic3r/GUI/Gizmos/GLGizmoMmuSegmentation.cpp @@ -41,6 +41,7 @@ #include #include #include +#include #include #include @@ -2319,6 +2320,202 @@ static std::vector sample_raw_offsets_bilinear_wrapped(const std::vecto return values; } +static bool checked_texture_buffer_size(uint32_t width, uint32_t height, uint32_t channels, size_t& size) +{ + size = 0; + if (width == 0 || height == 0 || channels == 0) + return false; + if (size_t(width) > std::numeric_limits::max() / size_t(height)) + return false; + const size_t pixels = size_t(width) * size_t(height); + if (pixels > std::numeric_limits::max() / size_t(channels)) + return false; + size = pixels * size_t(channels); + return true; +} + +static std::vector resize_rgba_texture_bilinear( + const std::vector& rgba, uint32_t width, uint32_t height, uint32_t resized_width, uint32_t resized_height) +{ + size_t resized_size = 0; + if (!checked_texture_buffer_size(resized_width, resized_height, 4, resized_size)) + return {}; + + std::vector resized(resized_size, 255); + size_t source_size = 0; + if (!checked_texture_buffer_size(width, height, 4, source_size) || rgba.size() < source_size) + return resized; + + auto channel = [&rgba, width](size_t sx, size_t sy, size_t ch) { return float(rgba[(sy * size_t(width) + sx) * 4 + ch]) / 255.f; }; + for (uint32_t y = 0; y < resized_height; ++y) { + const float source_y = std::clamp((float(y) + 0.5f) * float(height) / float(resized_height) - 0.5f, 0.f, float(height - 1)); + const size_t y0 = std::min(size_t(std::floor(source_y)), size_t(height - 1)); + const size_t y1 = std::min(y0 + 1, size_t(height - 1)); + const float ty = source_y - float(y0); + for (uint32_t x = 0; x < resized_width; ++x) { + const float source_x = std::clamp((float(x) + 0.5f) * float(width) / float(resized_width) - 0.5f, 0.f, float(width - 1)); + const size_t x0 = std::min(size_t(std::floor(source_x)), size_t(width - 1)); + const size_t x1 = std::min(x0 + 1, size_t(width - 1)); + const float tx = source_x - float(x0); + auto blend_channel = [&](size_t ch) { + const float c00 = channel(x0, y0, ch); + const float c10 = channel(x1, y0, ch); + const float c01 = channel(x0, y1, ch); + const float c11 = channel(x1, y1, ch); + const float cx0 = c00 + (c10 - c00) * tx; + const float cx1 = c01 + (c11 - c01) * tx; + return std::clamp(cx0 + (cx1 - cx0) * ty, 0.f, 1.f); + }; + auto blend_premultiplied_channel = [&](size_t ch) { + const float c00 = channel(x0, y0, ch) * channel(x0, y0, 3); + const float c10 = channel(x1, y0, ch) * channel(x1, y0, 3); + const float c01 = channel(x0, y1, ch) * channel(x0, y1, 3); + const float c11 = channel(x1, y1, ch) * channel(x1, y1, 3); + const float cx0 = c00 + (c10 - c00) * tx; + const float cx1 = c01 + (c11 - c01) * tx; + return std::clamp(cx0 + (cx1 - cx0) * ty, 0.f, 1.f); + }; + const float a = blend_channel(3); + const size_t idx = (size_t(y) * size_t(resized_width) + size_t(x)) * 4; + resized[idx + 0] = + uint8_t(std::clamp(a > 0.f ? blend_premultiplied_channel(0) / a : blend_channel(0), 0.f, 1.f) * 255.f + 0.5f); + resized[idx + 1] = + uint8_t(std::clamp(a > 0.f ? blend_premultiplied_channel(1) / a : blend_channel(1), 0.f, 1.f) * 255.f + 0.5f); + resized[idx + 2] = + uint8_t(std::clamp(a > 0.f ? blend_premultiplied_channel(2) / a : blend_channel(2), 0.f, 1.f) * 255.f + 0.5f); + resized[idx + 3] = uint8_t(a * 255.f + 0.5f); + } + } + return resized; +} + +static std::vector resize_raw_offsets_bilinear(const std::vector& offsets, + uint32_t width, + uint32_t height, + uint32_t channels, + uint32_t resized_width, + uint32_t resized_height) +{ + size_t resized_size = 0; + if (!checked_texture_buffer_size(resized_width, resized_height, channels, resized_size)) + return {}; + + std::vector resized(resized_size, 0); + size_t source_size = 0; + if (!checked_texture_buffer_size(width, height, channels, source_size) || offsets.size() < source_size) + return resized; + + auto channel = [&offsets, width, channels](size_t sx, size_t sy, size_t ch) { + return float(offsets[(sy * size_t(width) + sx) * size_t(channels) + ch]); + }; + for (uint32_t y = 0; y < resized_height; ++y) { + const float source_y = std::clamp((float(y) + 0.5f) * float(height) / float(resized_height) - 0.5f, 0.f, float(height - 1)); + const size_t y0 = std::min(size_t(std::floor(source_y)), size_t(height - 1)); + const size_t y1 = std::min(y0 + 1, size_t(height - 1)); + const float ty = source_y - float(y0); + for (uint32_t x = 0; x < resized_width; ++x) { + const float source_x = std::clamp((float(x) + 0.5f) * float(width) / float(resized_width) - 0.5f, 0.f, float(width - 1)); + const size_t x0 = std::min(size_t(std::floor(source_x)), size_t(width - 1)); + const size_t x1 = std::min(x0 + 1, size_t(width - 1)); + const float tx = source_x - float(x0); + const size_t idx = (size_t(y) * size_t(resized_width) + size_t(x)) * size_t(channels); + for (size_t ch = 0; ch < size_t(channels); ++ch) { + const float c00 = channel(x0, y0, ch); + const float c10 = channel(x1, y0, ch); + const float c01 = channel(x0, y1, ch); + const float c11 = channel(x1, y1, ch); + const float cx0 = c00 + (c10 - c00) * tx; + const float cx1 = c01 + (c11 - c01) * tx; + resized[idx + ch] = uint8_t(std::clamp(cx0 + (cx1 - cx0) * ty, 0.f, 255.f) + 0.5f); + } + } + } + return resized; +} + +static std::string resized_raw_texture_metadata_json(const std::string& metadata_json, uint32_t width, uint32_t height, uint32_t channels) +{ + nlohmann::json root; + try { + root = nlohmann::json::parse(metadata_json); + if (!root.is_object()) + root = nlohmann::json::object(); + } catch (...) { + root = nlohmann::json::object(); + } + root["format"] = "raw_filament_offset_atlas"; + root["image"] = {{"width", width}, {"height", height}, {"channels", channels}}; + root.erase("regions"); + return root.dump(); +} + +static bool resize_volume_image_texture(ModelVolume& volume, uint32_t resized_width, uint32_t resized_height) +{ + if (!model_volume_has_imported_image_texture_data(&volume) || resized_width == 0 || resized_height == 0) + return false; + + const uint32_t old_width = volume.imported_texture_width; + const uint32_t old_height = volume.imported_texture_height; + if (old_width == resized_width && old_height == resized_height) + return false; + + const bool has_raw_atlas = model_volume_has_raw_atlas_texture_data(&volume); + if (has_raw_atlas) { + std::vector resized_offsets = resize_raw_offsets_bilinear(volume.imported_texture_raw_filament_offsets, old_width, + old_height, volume.imported_texture_raw_channels, resized_width, + resized_height); + if (resized_offsets.empty()) + return false; + volume.imported_texture_raw_filament_offsets = std::move(resized_offsets); + volume.imported_texture_width = resized_width; + volume.imported_texture_height = resized_height; + volume.imported_texture_raw_metadata_json = resized_raw_texture_metadata_json(volume.imported_texture_raw_metadata_json, + resized_width, resized_height, + volume.imported_texture_raw_channels); + refresh_imported_texture_preview_from_raw_offsets(volume); + volume.imported_texture_raw_filament_offsets.set_new_unique_id(); + volume.imported_texture_rgba.set_new_unique_id(); + return true; + } + + size_t source_rgba_size = 0; + if (!checked_texture_buffer_size(old_width, old_height, 4, source_rgba_size) || volume.imported_texture_rgba.size() < source_rgba_size) + return false; + + std::vector resized_rgba = resize_rgba_texture_bilinear(volume.imported_texture_rgba, old_width, old_height, resized_width, + resized_height); + if (resized_rgba.empty()) + return false; + + volume.imported_texture_rgba = std::move(resized_rgba); + const bool cleared_raw_data = !volume.imported_texture_raw_filament_offsets.empty() || volume.imported_texture_raw_channels != 0 || + !volume.imported_texture_raw_metadata_json.empty(); + if (cleared_raw_data) + clear_imported_texture_raw_atlas(volume); + volume.imported_texture_width = resized_width; + volume.imported_texture_height = resized_height; + volume.imported_texture_rgba.set_new_unique_id(); + if (cleared_raw_data) + volume.imported_texture_raw_filament_offsets.set_new_unique_id(); + return true; +} + +static bool resize_object_image_textures(ModelObject& object, double scale) +{ + if (!std::isfinite(scale) || scale <= 0.0) + return false; + + bool changed = false; + for (ModelVolume* volume : object.volumes) { + if (volume == nullptr || !volume->is_model_part() || !model_volume_has_imported_image_texture_data(volume)) + continue; + const uint32_t resized_width = std::max(1, uint32_t(std::llround(double(volume->imported_texture_width) * scale))); + const uint32_t resized_height = std::max(1, uint32_t(std::llround(double(volume->imported_texture_height) * scale))); + changed |= resize_volume_image_texture(*volume, resized_width, resized_height); + } + return changed; +} + static ColorRGBA blend_projection_color(const ColorRGBA &base, const ColorRGBA &overlay, float opacity) { const float alpha = std::clamp(overlay.a(), 0.f, 1.f) * std::clamp(opacity, 0.f, 1.f); @@ -4410,7 +4607,7 @@ struct ManagedColorDataSummary size_t raw_offset_image_texture_count = 0; uint32_t max_texture_width = 0; uint32_t max_texture_height = 0; - size_t rgba_data_bytes = 0; + size_t rgba_data_triangle_count = 0; std::vector raw_offset_color_modes; }; @@ -4508,15 +4705,6 @@ static wxString managed_color_data_type_label(ManagedColorDataType type) return wxString(); } -static size_t estimated_rgba_data_bytes(const ColorFacetsAnnotation &annotation) -{ - const TriangleColorSplittingData &data = annotation.get_data(); - return data.triangles_to_split.size() * sizeof(ColorTriangleBitStreamMapping) + - (data.bitstream.size() + 7) / 8 + - data.colors_rgba.size() * sizeof(uint32_t) + - data.metadata_json.size(); -} - static ManagedColorDataSummary summarize_managed_color_data(const ModelObject *object) { ManagedColorDataSummary summary; @@ -4555,7 +4743,7 @@ static ManagedColorDataSummary summarize_managed_color_data(const ModelObject *o if (!volume->texture_mapping_color_facets.empty()) { summary.has_rgba_data = true; - summary.rgba_data_bytes += estimated_rgba_data_bytes(volume->texture_mapping_color_facets); + summary.rgba_data_triangle_count += volume->texture_mapping_color_facets.get_data().colors_rgba.size(); } } return summary; @@ -4580,9 +4768,7 @@ static wxString managed_color_data_size_text(const ManagedColorDataSummary &summ static_cast(summary.max_texture_width), static_cast(summary.max_texture_height)); case ManagedColorDataType::RgbaData: - if (summary.rgba_data_bytes > 0 && summary.rgba_data_bytes < 1024 * 1024 / 100) - return _L("<0.01 MB"); - return wxString::Format(_L("%.2f MB"), double(summary.rgba_data_bytes) / (1024.0 * 1024.0)); + return wxString::Format(_L("%llu triangles"), static_cast(summary.rgba_data_triangle_count)); } return wxString(); } @@ -5113,6 +5299,364 @@ static void managed_color_data_append_rgba_hex(std::string &out, uint32_t rgba) out.push_back(managed_color_data_hex_digit((rgba >> shift) & 0x0Fu)); } +struct ManagedColorDataRgbaAccumulator +{ + double r = 0.0; + double g = 0.0; + double b = 0.0; + double a = 0.0; + double area_weight = 0.0; + double count_r = 0.0; + double count_g = 0.0; + double count_b = 0.0; + double count_a = 0.0; + size_t count = 0; +}; + +static double managed_color_data_triangle_area(const std::array &vertices) +{ + const Vec3f edge0 = vertices[1] - vertices[0]; + const Vec3f edge1 = vertices[2] - vertices[0]; + const double area = 0.5 * double(edge0.cross(edge1).norm()); + return std::isfinite(area) && area > double(EPSILON) ? area : 0.0; +} + +static double managed_color_data_triangle_area(const ColorFacetTriangle &facet) +{ + return managed_color_data_triangle_area(facet.vertices); +} + +static void managed_color_data_accumulate_rgba(ManagedColorDataRgbaAccumulator &accumulator, uint32_t rgba, double area) +{ + const ColorRGBA color = unpack_vertex_color_rgba_for_conversion(rgba); + if (area > 0.0) { + accumulator.r += double(color.r()) * area; + accumulator.g += double(color.g()) * area; + accumulator.b += double(color.b()) * area; + accumulator.a += double(color.a()) * area; + accumulator.area_weight += area; + } + accumulator.count_r += double(color.r()); + accumulator.count_g += double(color.g()); + accumulator.count_b += double(color.b()); + accumulator.count_a += double(color.a()); + ++accumulator.count; +} + +static uint32_t managed_color_data_averaged_rgba(const ManagedColorDataRgbaAccumulator &accumulator) +{ + if (accumulator.area_weight > 0.0) + return pack_vertex_color_rgba(ColorRGBA(float(accumulator.r / accumulator.area_weight), + float(accumulator.g / accumulator.area_weight), + float(accumulator.b / accumulator.area_weight), + float(accumulator.a / accumulator.area_weight))); + + const double count = std::max(1.0, double(accumulator.count)); + return pack_vertex_color_rgba(ColorRGBA(float(accumulator.count_r / count), + float(accumulator.count_g / count), + float(accumulator.count_b / count), + float(accumulator.count_a / count))); +} + +static void managed_color_data_merge_rgba_accumulator(ManagedColorDataRgbaAccumulator &target, + const ManagedColorDataRgbaAccumulator &source) +{ + target.r += source.r; + target.g += source.g; + target.b += source.b; + target.a += source.a; + target.area_weight += source.area_weight; + target.count_r += source.count_r; + target.count_g += source.count_g; + target.count_b += source.count_b; + target.count_a += source.count_a; + target.count += source.count; +} + +static int managed_color_data_read_rgba_nibble(const TriangleColorSplittingData &data, int bitstream_end, int &bit_idx) +{ + if (bit_idx < 0 || bit_idx + 4 > bitstream_end || size_t(bit_idx + 3) >= data.bitstream.size()) + return -1; + + int code = 0; + for (int bit = 0; bit < 4; ++bit) + code |= int(data.bitstream[size_t(bit_idx++)]) << bit; + return code; +} + +static std::array, 4> managed_color_data_split_rgba_triangle(const std::array &vertices, + int split_sides, + int special_side) +{ + std::array, 4> children{}; + special_side = std::clamp(special_side, 0, 2); + const int j = (special_side + 1) % 3; + const int k = (special_side + 2) % 3; + const Vec3f a = vertices[size_t(special_side)]; + const Vec3f b = vertices[size_t(j)]; + const Vec3f c = vertices[size_t(k)]; + const Vec3f ab = 0.5f * (a + b); + const Vec3f bc = 0.5f * (b + c); + const Vec3f ca = 0.5f * (c + a); + + if (split_sides == 1) { + children[0] = { a, b, bc }; + children[1] = { bc, c, a }; + } else if (split_sides == 2) { + children[0] = { a, ab, ca }; + children[1] = { ab, b, ca }; + children[2] = { b, c, ca }; + } else { + children[0] = { a, ab, ca }; + children[1] = { ab, b, bc }; + children[2] = { bc, c, ca }; + children[3] = { ab, bc, ca }; + } + + return children; +} + +struct ManagedColorDataUnsplitResult +{ + bool valid = false; + bool input_leaf = false; + bool changed = false; + ManagedColorDataRgbaAccumulator accumulator; + std::vector bits; + std::vector colors; +}; + +static ManagedColorDataUnsplitResult managed_color_data_unsplit_rgba_node_once(const TriangleColorSplittingData &data, + int bitstream_end, + size_t color_end, + int &bit_idx, + size_t &color_idx, + const std::array &vertices) +{ + ManagedColorDataUnsplitResult result; + const int code = managed_color_data_read_rgba_nibble(data, bitstream_end, bit_idx); + if (code < 0) + return result; + + const int split_sides = code & 0b11; + if (split_sides == 0) { + if (color_idx >= color_end || color_idx >= data.colors_rgba.size()) + return result; + + const uint32_t rgba = data.colors_rgba[color_idx++]; + managed_color_data_append_nibble(result.bits, 0u); + result.colors.emplace_back(rgba); + managed_color_data_accumulate_rgba(result.accumulator, rgba, managed_color_data_triangle_area(vertices)); + result.valid = true; + result.input_leaf = true; + return result; + } + + const int special_side = (code >> 2) & 0b11; + const std::array, 4> child_vertices = + managed_color_data_split_rgba_triangle(vertices, split_sides, special_side); + std::array child_results; + bool all_children_are_leaves = true; + bool any_child_changed = false; + for (int child_idx = split_sides; child_idx >= 0; --child_idx) { + child_results[size_t(child_idx)] = managed_color_data_unsplit_rgba_node_once(data, + bitstream_end, + color_end, + bit_idx, + color_idx, + child_vertices[size_t(child_idx)]); + const ManagedColorDataUnsplitResult &child = child_results[size_t(child_idx)]; + if (!child.valid) + return ManagedColorDataUnsplitResult(); + all_children_are_leaves &= child.input_leaf; + any_child_changed |= child.changed; + managed_color_data_merge_rgba_accumulator(result.accumulator, child.accumulator); + } + + result.valid = true; + if (all_children_are_leaves) { + managed_color_data_append_nibble(result.bits, 0u); + result.colors.emplace_back(managed_color_data_averaged_rgba(result.accumulator)); + result.changed = true; + return result; + } + + managed_color_data_append_nibble(result.bits, unsigned(code)); + for (int child_idx = split_sides; child_idx >= 0; --child_idx) { + const ManagedColorDataUnsplitResult &child = child_results[size_t(child_idx)]; + result.bits.insert(result.bits.end(), child.bits.begin(), child.bits.end()); + result.colors.insert(result.colors.end(), child.colors.begin(), child.colors.end()); + } + result.changed = any_child_changed; + return result; +} + +static bool rgba_data_has_split_triangles(const ColorFacetsAnnotation &annotation) +{ + const std::vector &bitstream = annotation.get_data().bitstream; + for (size_t bit_idx = 0; bit_idx + 3 < bitstream.size(); bit_idx += 4) { + int code = 0; + for (int bit = 0; bit < 4; ++bit) + code |= int(bitstream[bit_idx + size_t(bit)]) << bit; + if ((code & 0b11) != 0) + return true; + } + return false; +} + +static bool object_has_splittable_rgba_data(const ModelObject &object) +{ + for (const ModelVolume *volume : object.volumes) + if (volume != nullptr && volume->is_model_part() && rgba_data_has_split_triangles(volume->texture_mapping_color_facets)) + return true; + return false; +} + +static bool unsplit_volume_rgba_data_once(ModelVolume &volume) +{ + const TriangleColorSplittingData &data = volume.texture_mapping_color_facets.get_data(); + if (data.triangles_to_split.empty() || !rgba_data_has_split_triangles(volume.texture_mapping_color_facets)) + return false; + + const indexed_triangle_set &its = volume.mesh().its; + if (its.indices.empty()) + return false; + + std::unique_ptr unsplit = ColorFacetsAnnotation::make_temporary(); + if (!unsplit) + return false; + + unsplit->set_metadata_json(volume.texture_mapping_color_facets.metadata_json()); + unsplit->reserve(int(data.triangles_to_split.size())); + bool any = false; + for (auto mapping_it = data.triangles_to_split.begin(); mapping_it != data.triangles_to_split.end(); ++mapping_it) { + if (mapping_it->triangle_idx < 0 || size_t(mapping_it->triangle_idx) >= its.indices.size()) + continue; + + const auto &tri = its.indices[size_t(mapping_it->triangle_idx)]; + if (tri[0] < 0 || tri[1] < 0 || tri[2] < 0) + continue; + if (size_t(tri[0]) >= its.vertices.size() || + size_t(tri[1]) >= its.vertices.size() || + size_t(tri[2]) >= its.vertices.size()) + continue; + + const auto next_it = std::next(mapping_it); + const int bitstream_end = next_it == data.triangles_to_split.end() ? + int(data.bitstream.size()) : + next_it->bitstream_start_idx; + const size_t color_end = next_it == data.triangles_to_split.end() ? + data.colors_rgba.size() : + size_t(next_it->color_start_idx); + if (mapping_it->bitstream_start_idx < 0 || + mapping_it->bitstream_start_idx >= bitstream_end || + size_t(bitstream_end) > data.bitstream.size() || + mapping_it->color_start_idx < 0 || + size_t(mapping_it->color_start_idx) >= color_end || + color_end > data.colors_rgba.size()) + continue; + + const std::array vertices = { + its.vertices[size_t(tri[0])].cast(), + its.vertices[size_t(tri[1])].cast(), + its.vertices[size_t(tri[2])].cast() + }; + int bit_idx = mapping_it->bitstream_start_idx; + size_t color_idx = size_t(mapping_it->color_start_idx); + ManagedColorDataUnsplitResult result = + managed_color_data_unsplit_rgba_node_once(data, bitstream_end, color_end, bit_idx, color_idx, vertices); + if (!result.valid || result.bits.empty() || result.colors.empty()) + continue; + + std::string encoded; + managed_color_data_bits_to_hex(result.bits, encoded); + encoded.push_back('|'); + for (uint32_t rgba : result.colors) + managed_color_data_append_rgba_hex(encoded, rgba); + unsplit->set_triangle_from_string(mapping_it->triangle_idx, encoded); + any = true; + } + if (!any) + return false; + + unsplit->shrink_to_fit(); + if (volume.texture_mapping_color_facets.equals(*unsplit)) + return false; + + volume.texture_mapping_color_facets.assign(*unsplit); + return true; +} + +static bool unsplit_object_rgba_data_once(ModelObject &object) +{ + bool changed = false; + for (ModelVolume *volume : object.volumes) { + if (volume == nullptr || !volume->is_model_part()) + continue; + changed |= unsplit_volume_rgba_data_once(*volume); + } + return changed; +} + +static bool simplify_volume_rgba_data_to_one_color_per_triangle(ModelVolume &volume) +{ + if (volume.texture_mapping_color_facets.empty()) + return false; + + const indexed_triangle_set &its = volume.mesh().its; + if (its.indices.empty()) + return false; + + std::vector facets; + volume.texture_mapping_color_facets.get_facet_triangles(volume, facets); + if (facets.empty()) + return false; + + std::vector accumulators(its.indices.size()); + for (const ColorFacetTriangle &facet : facets) { + if (facet.source_triangle < 0 || size_t(facet.source_triangle) >= accumulators.size()) + continue; + managed_color_data_accumulate_rgba(accumulators[size_t(facet.source_triangle)], facet.rgba, managed_color_data_triangle_area(facet)); + } + + std::unique_ptr simplified = ColorFacetsAnnotation::make_temporary(); + if (!simplified) + return false; + + simplified->set_metadata_json(volume.texture_mapping_color_facets.metadata_json()); + simplified->reserve(int(its.indices.size())); + bool any = false; + for (size_t triangle_idx = 0; triangle_idx < accumulators.size(); ++triangle_idx) { + if (accumulators[triangle_idx].count == 0) + continue; + + std::string encoded = "0|"; + managed_color_data_append_rgba_hex(encoded, managed_color_data_averaged_rgba(accumulators[triangle_idx])); + simplified->set_triangle_from_string(int(triangle_idx), encoded); + any = true; + } + if (!any) + return false; + + simplified->shrink_to_fit(); + if (volume.texture_mapping_color_facets.equals(*simplified)) + return false; + + volume.texture_mapping_color_facets.assign(*simplified); + return true; +} + +static bool simplify_object_rgba_data_to_one_color_per_triangle(ModelObject &object) +{ + bool changed = false; + for (ModelVolume *volume : object.volumes) { + if (volume == nullptr || !volume->is_model_part()) + continue; + changed |= simplify_volume_rgba_data_to_one_color_per_triangle(*volume); + } + return changed; +} + static ColorRGBA managed_color_data_state_color(const ManagedRegionColorSource &source, unsigned int state) { if (state < source.state_colors.size()) @@ -6052,6 +6596,139 @@ static void refresh_managed_color_data_object(GLCanvas3D &parent, ModelObject *o parent.post_event(SimpleEvent(EVT_GLCANVAS_SCHEDULE_BACKGROUND_PROCESS)); } +class ResizeImageTextureDialog : public wxDialog +{ +public: + ResizeImageTextureDialog(wxWindow* parent, uint32_t width, uint32_t height) + : wxDialog(parent, wxID_ANY, _L("Resize Texture"), wxDefaultPosition, wxDefaultSize, wxDEFAULT_DIALOG_STYLE) + , m_original_width(width) + , m_original_height(height) + { + wxBoxSizer* main_sizer = new wxBoxSizer(wxVERTICAL); + main_sizer->Add(new wxStaticText(this, wxID_ANY, + wxString::Format(_L("Original size: %u x %u"), static_cast(m_original_width), + static_cast(m_original_height))), + 0, wxEXPAND | wxLEFT | wxRIGHT | wxTOP, FromDIP(16)); + + wxFlexGridSizer* grid = new wxFlexGridSizer(2, FromDIP(8), FromDIP(8)); + grid->AddGrowableCol(1, 1); + const uint32_t max_axis = std::min(std::max({uint32_t(8192), m_original_width, m_original_height}), + uint32_t(std::numeric_limits::max())); + const uint32_t original_max_axis = std::max(m_original_width, m_original_height); + const double max_scale = original_max_axis > 0 ? double(max_axis) / double(original_max_axis) : 1.0; + auto scaled_max = [max_scale](uint32_t value) { + return std::min(uint32_t(std::numeric_limits::max()), + std::max(std::max(1, value), + uint32_t(std::floor(double(value) * max_scale)))); + }; + m_max_width = scaled_max(m_original_width); + m_max_height = scaled_max(m_original_height); + m_width_spin = new wxSpinCtrl(this, wxID_ANY, wxEmptyString, wxDefaultPosition, wxSize(FromDIP(96), -1), wxSP_ARROW_KEYS, 1, + int(m_max_width), int(std::min(m_original_width, m_max_width))); + m_height_spin = new wxSpinCtrl(this, wxID_ANY, wxEmptyString, wxDefaultPosition, wxSize(FromDIP(96), -1), wxSP_ARROW_KEYS, 1, + int(m_max_height), int(std::min(m_original_height, m_max_height))); + grid->Add(new wxStaticText(this, wxID_ANY, _L("Width")), 0, wxALIGN_CENTER_VERTICAL); + grid->Add(m_width_spin, 0, wxALIGN_CENTER_VERTICAL); + grid->Add(new wxStaticText(this, wxID_ANY, _L("Height")), 0, wxALIGN_CENTER_VERTICAL); + grid->Add(m_height_spin, 0, wxALIGN_CENTER_VERTICAL); + main_sizer->Add(grid, 0, wxEXPAND | wxALL, FromDIP(16)); + + m_scale_text = new wxStaticText(this, wxID_ANY, wxString()); + main_sizer->Add(m_scale_text, 0, wxEXPAND | wxLEFT | wxRIGHT, FromDIP(16)); + main_sizer->Add(new wxStaticLine(this), 0, wxEXPAND | wxLEFT | wxRIGHT | wxTOP, FromDIP(16)); + + wxStdDialogButtonSizer* buttons = new wxStdDialogButtonSizer(); + m_ok_button = new wxButton(this, wxID_OK, _L("OK")); + wxButton* cancel_button = new wxButton(this, wxID_CANCEL, _L("Cancel")); + buttons->AddButton(m_ok_button); + buttons->AddButton(cancel_button); + buttons->Realize(); + main_sizer->Add(buttons, 0, wxEXPAND | wxALL, FromDIP(16)); + + SetSizer(main_sizer); + update_summary(); + Fit(); + SetMinSize(GetSize()); + CenterOnParent(); + + m_width_spin->Bind(wxEVT_SPINCTRL, [this](wxSpinEvent&) { update_from_width(); }); + m_width_spin->Bind(wxEVT_TEXT, [this](wxCommandEvent&) { update_from_width(); }); + m_height_spin->Bind(wxEVT_SPINCTRL, [this](wxSpinEvent&) { update_from_height(); }); + m_height_spin->Bind(wxEVT_TEXT, [this](wxCommandEvent&) { update_from_height(); }); + } + + double scale() const { return m_scale; } + +private: + static uint32_t rounded_dimension(double value) { return std::max(1, uint32_t(std::llround(value))); } + + void set_spin_value(wxSpinCtrl* spin, uint32_t value, uint32_t max_value) + { + value = std::clamp(value, 1, max_value); + if (spin != nullptr && spin->GetValue() != int(value)) + spin->SetValue(int(value)); + } + + void update_from_width() + { + if (m_updating || m_width_spin == nullptr || m_height_spin == nullptr || m_original_width == 0) + return; + + m_updating = true; + const uint32_t width = std::clamp(uint32_t(std::max(1, m_width_spin->GetValue())), 1, m_max_width); + set_spin_value(m_width_spin, width, m_max_width); + m_scale = double(width) / double(m_original_width); + set_spin_value(m_height_spin, rounded_dimension(double(m_original_height) * m_scale), m_max_height); + m_updating = false; + update_summary(); + } + + void update_from_height() + { + if (m_updating || m_width_spin == nullptr || m_height_spin == nullptr || m_original_height == 0) + return; + + m_updating = true; + const uint32_t height = std::clamp(uint32_t(std::max(1, m_height_spin->GetValue())), 1, m_max_height); + set_spin_value(m_height_spin, height, m_max_height); + m_scale = double(height) / double(m_original_height); + set_spin_value(m_width_spin, rounded_dimension(double(m_original_width) * m_scale), m_max_width); + m_updating = false; + update_summary(); + } + + void update_summary() + { + if (m_width_spin == nullptr || m_height_spin == nullptr) + return; + + const uint32_t width = std::clamp(uint32_t(std::max(1, m_width_spin->GetValue())), 1, m_max_width); + const uint32_t height = std::clamp(uint32_t(std::max(1, m_height_spin->GetValue())), 1, m_max_height); + const double dimension_pct = m_scale * 100.0; + const double original_pixels = double(m_original_width) * double(m_original_height); + const double resized_pixels = double(width) * double(height); + const double pixel_pct = original_pixels > 0.0 ? resized_pixels * 100.0 / original_pixels : 100.0; + if (m_scale_text != nullptr) + m_scale_text->SetLabel(wxString::Format(_L("New size: %u x %u (%.1f%% dimensions, %.1f%% pixels)"), + static_cast(width), static_cast(height), dimension_pct, pixel_pct)); + if (m_ok_button != nullptr) + m_ok_button->Enable(width != m_original_width || height != m_original_height); + Layout(); + Fit(); + } + + uint32_t m_original_width = 0; + uint32_t m_original_height = 0; + uint32_t m_max_width = 1; + uint32_t m_max_height = 1; + wxSpinCtrl* m_width_spin = nullptr; + wxSpinCtrl* m_height_spin = nullptr; + wxStaticText* m_scale_text = nullptr; + wxButton* m_ok_button = nullptr; + double m_scale = 1.0; + bool m_updating = false; +}; + class ColorDataManagementDialog : public wxDialog { public: @@ -6157,9 +6834,9 @@ private: wxButton *actions = nullptr; wxBoxSizer *size_sizer = new wxBoxSizer(wxHORIZONTAL); size_sizer->Add(size, 1, wxALIGN_CENTER_VERTICAL); - if (type == ManagedColorDataType::ImageTexture) { + if (type == ManagedColorDataType::ImageTexture || type == ManagedColorDataType::RgbaData) { actions = new wxButton(this, wxID_ANY, wxString::FromUTF8("\xE2\x96\xBE"), wxDefaultPosition, wxDefaultSize, wxBU_EXACTFIT); - actions->SetToolTip(_L("Image texture actions")); + actions->SetToolTip(type == ManagedColorDataType::ImageTexture ? _L("Image texture actions") : _L("RGBA data actions")); size_sizer->Add(actions, 0, wxALIGN_CENTER_VERTICAL | wxLEFT, FromDIP(6)); } @@ -6172,8 +6849,14 @@ private: clear->Bind(wxEVT_BUTTON, [this, type](wxCommandEvent &) { clear_data(type); }); create->Bind(wxEVT_BUTTON, [this, type, create](wxCommandEvent &) { show_create_menu(type, create); }); - if (actions != nullptr) - actions->Bind(wxEVT_BUTTON, [this, actions](wxCommandEvent &) { show_image_texture_menu(actions); }); + if (actions != nullptr) { + actions->Bind(wxEVT_BUTTON, [this, type, actions](wxCommandEvent &) { + if (type == ManagedColorDataType::ImageTexture) + show_image_texture_menu(actions); + else if (type == ManagedColorDataType::RgbaData) + show_rgba_data_menu(actions); + }); + } m_rows.push_back({ type, status, preview, size, clear, create, actions }); } @@ -6285,16 +6968,64 @@ private: info->Enable(false); menu.AppendSeparator(); - const int generate_id = wxWindow::NewControlId(); - wxMenuItem *generate = menu.Append(generate_id, _L("Generate new UV Map")); + const int resize_id = wxWindow::NewControlId(); + wxMenuItem* resize = menu.Append(resize_id, _L("Resize Texture")); + resize->Enable(m_object != nullptr && object_has_image_texture_data(*m_object)); + + const int generate_id = wxWindow::NewControlId(); + wxMenuItem* generate = menu.Append(generate_id, _L("Generate new UV Map")); generate->Enable(m_object != nullptr && object_has_regenerable_image_texture_uvs(*m_object)); - menu.Bind(wxEVT_COMMAND_MENU_SELECTED, [this, generate_id](wxCommandEvent &event) { + menu.Bind(wxEVT_COMMAND_MENU_SELECTED, [this, resize_id, generate_id](wxCommandEvent& event) { + if (event.GetId() == resize_id) + resize_image_texture(); if (event.GetId() == generate_id) generate_new_uv_map(); }); button->PopupMenu(&menu, wxPoint(0, button->GetSize().GetHeight())); } + void show_rgba_data_menu(wxButton *button) + { + if (button == nullptr) + return; + + wxMenu menu; + const int unsplit_id = wxWindow::NewControlId(); + const int simplify_id = wxWindow::NewControlId(); + wxMenuItem *unsplit = menu.Append(unsplit_id, _L("Unsplit once")); + wxMenuItem *simplify = menu.Append(simplify_id, _L("Simplify to 1 color per triangle")); + unsplit->Enable(m_object != nullptr && object_has_splittable_rgba_data(*m_object)); + simplify->Enable(m_object != nullptr && object_has_rgba_data(*m_object)); + menu.Bind(wxEVT_COMMAND_MENU_SELECTED, [this, unsplit_id, simplify_id](wxCommandEvent &event) { + if (event.GetId() == unsplit_id) + unsplit_rgba_data_once(); + else if (event.GetId() == simplify_id) + simplify_rgba_data_to_one_color_per_triangle(); + }); + button->PopupMenu(&menu, wxPoint(0, button->GetSize().GetHeight())); + } + + void resize_image_texture() + { + if (m_object == nullptr) + return; + + const ManagedColorDataSummary summary = summarize_managed_color_data(m_object); + if (!summary.has_image_texture || summary.max_texture_width == 0 || summary.max_texture_height == 0) + return; + + ResizeImageTextureDialog dialog(this, summary.max_texture_width, summary.max_texture_height); + if (dialog.ShowModal() != wxID_OK) + return; + + Plater::TakeSnapshot snapshot(wxGetApp().plater(), "Resize image texture", UndoRedo::SnapshotType::GizmoAction); + if (!resize_object_image_textures(*m_object, dialog.scale())) + return; + + refresh_object_after_change(); + refresh_rows(); + } + void generate_new_uv_map() { if (m_object == nullptr || !object_has_regenerable_image_texture_uvs(*m_object)) @@ -6308,6 +7039,32 @@ private: refresh_rows(); } + void unsplit_rgba_data_once() + { + if (m_object == nullptr || !object_has_splittable_rgba_data(*m_object)) + return; + + Plater::TakeSnapshot snapshot(wxGetApp().plater(), "Unsplit RGBA data", UndoRedo::SnapshotType::GizmoAction); + if (!unsplit_object_rgba_data_once(*m_object)) + return; + + refresh_object_after_change(); + refresh_rows(); + } + + void simplify_rgba_data_to_one_color_per_triangle() + { + if (m_object == nullptr || !object_has_rgba_data(*m_object)) + return; + + Plater::TakeSnapshot snapshot(wxGetApp().plater(), "Simplify RGBA data", UndoRedo::SnapshotType::GizmoAction); + if (!simplify_object_rgba_data_to_one_color_per_triangle(*m_object)) + return; + + refresh_object_after_change(); + refresh_rows(); + } + void create_data(ManagedColorDataType type, const ManagedColorDataCreateSource &source) { if (m_object == nullptr || object_has_managed_color_data(*m_object, type) || (source.type && *source.type == type))