diff --git a/.gitignore b/.gitignore index 8007f0a05ca..e69944015ca 100644 --- a/.gitignore +++ b/.gitignore @@ -45,4 +45,6 @@ test.js .clangd internal_docs/ *.flatpak -/flatpak-repo/ \ No newline at end of file +/flatpak-repo/ +*.3mf +*.gcode diff --git a/src/libslic3r/CMakeLists.txt b/src/libslic3r/CMakeLists.txt index 990370a22df..be216c2229b 100644 --- a/src/libslic3r/CMakeLists.txt +++ b/src/libslic3r/CMakeLists.txt @@ -304,6 +304,8 @@ set(lisbslic3r_sources ModelArrange.hpp Model.cpp Model.hpp + ModelTextureDataRemap.cpp + ModelTextureDataRemap.hpp MTUtils.hpp MultiMaterialSegmentation.cpp MultiMaterialSegmentation.hpp diff --git a/src/libslic3r/Model.cpp b/src/libslic3r/Model.cpp index 16bf05a4b4d..db41bd81d31 100644 --- a/src/libslic3r/Model.cpp +++ b/src/libslic3r/Model.cpp @@ -4580,11 +4580,23 @@ bool ColorFacetsAnnotation::set_from_triangle_sampler(const ModelVolume float split_color_threshold, const TextureMappingColorSubdivisionDepths &subdivision_depths, const std::vector *resample_triangles, - const TextureMappingColorLeafResamplePredicate &resample_leaf) + const TextureMappingColorLeafResamplePredicate &resample_leaf, + const TextureMappingColorProgressFn &progress_fn) +{ + return this->set_from_triangle_sampler(mv.mesh().its, sampler, max_depth, split_color_threshold, subdivision_depths, resample_triangles, resample_leaf, progress_fn); +} + +bool ColorFacetsAnnotation::set_from_triangle_sampler(const indexed_triangle_set &its, + const TextureMappingColorSampler &sampler, + int max_depth, + float split_color_threshold, + const TextureMappingColorSubdivisionDepths &subdivision_depths, + const std::vector *resample_triangles, + const TextureMappingColorLeafResamplePredicate &resample_leaf, + const TextureMappingColorProgressFn &progress_fn) { TriangleColorSplittingData new_data; new_data.metadata_json = m_data.metadata_json; - const indexed_triangle_set &its = mv.mesh().its; new_data.triangles_to_split.reserve(its.indices.size()); if (resample_triangles != nullptr) { new_data.bitstream.reserve(m_data.bitstream.size()); @@ -4600,6 +4612,21 @@ bool ColorFacetsAnnotation::set_from_triangle_sampler(const ModelVolume max_depth = std::clamp(max_depth, 0, 7); split_color_threshold = std::max(split_color_threshold, 0.f); + const size_t total_triangles = its.indices.size(); + int last_progress = -1; + auto report_progress = [&progress_fn, total_triangles, &last_progress](size_t completed_triangles) { + if (!progress_fn) + return; + const int progress = total_triangles == 0 ? + 100 : + int((uint64_t(completed_triangles) * 100u) / uint64_t(total_triangles)); + if (progress != last_progress) { + last_progress = progress; + progress_fn(completed_triangles, total_triangles); + } + }; + report_progress(0); + size_t preserved_mapping_idx = 0; auto existing_triangle_range = [this, &preserved_mapping_idx](size_t tri_idx, int &bitstream_start, @@ -4655,13 +4682,20 @@ bool ColorFacetsAnnotation::set_from_triangle_sampler(const ModelVolume }; for (size_t tri_idx = 0; tri_idx < its.indices.size(); ++tri_idx) { + auto report_triangle_done = [&report_progress, tri_idx]() { + report_progress(tri_idx + 1); + }; const auto &tri = its.indices[tri_idx]; - if (tri[0] < 0 || tri[1] < 0 || tri[2] < 0) + if (tri[0] < 0 || tri[1] < 0 || tri[2] < 0) { + report_triangle_done(); continue; + } if (size_t(tri[0]) >= its.vertices.size() || size_t(tri[1]) >= its.vertices.size() || - size_t(tri[2]) >= its.vertices.size()) + size_t(tri[2]) >= its.vertices.size()) { + report_triangle_done(); continue; + } const std::array vertices = { its.vertices[size_t(tri[0])].cast(), @@ -4676,8 +4710,10 @@ bool ColorFacetsAnnotation::set_from_triangle_sampler(const ModelVolume if (resample_triangles != nullptr && tri_idx < resample_triangles->size() && !(*resample_triangles)[tri_idx] && - append_preserved_triangle(tri_idx)) + append_preserved_triangle(tri_idx)) { + report_triangle_done(); continue; + } int triangle_min_depth = 0; int triangle_max_depth = max_depth; @@ -4714,8 +4750,10 @@ bool ColorFacetsAnnotation::set_from_triangle_sampler(const ModelVolume 0, triangle_min_depth, triangle_max_depth, - split_color_threshold)) + split_color_threshold)) { + report_triangle_done(); continue; + } new_data.bitstream.resize(new_bitstream_start); new_data.colors_rgba.resize(new_color_start); } @@ -4729,7 +4767,9 @@ bool ColorFacetsAnnotation::set_from_triangle_sampler(const ModelVolume triangle_min_depth, triangle_max_depth, split_color_threshold); + report_triangle_done(); } + report_progress(total_triangles); new_data.triangles_to_split.shrink_to_fit(); new_data.bitstream.shrink_to_fit(); diff --git a/src/libslic3r/Model.hpp b/src/libslic3r/Model.hpp index 0e2aed13769..81763a25319 100644 --- a/src/libslic3r/Model.hpp +++ b/src/libslic3r/Model.hpp @@ -849,6 +849,7 @@ using TextureMappingColorSampler = std::function(size_t, const std::array &)>; using TextureMappingColorLeafResamplePredicate = std::function &, const std::array &, uint32_t)>; +using TextureMappingColorProgressFn = std::function; class ColorFacetsAnnotation final : public ObjectWithTimestamp { public: @@ -891,7 +892,16 @@ public: float split_color_threshold = 0.045f, const TextureMappingColorSubdivisionDepths &subdivision_depths = {}, const std::vector *resample_triangles = nullptr, - const TextureMappingColorLeafResamplePredicate &resample_leaf = {}); + const TextureMappingColorLeafResamplePredicate &resample_leaf = {}, + const TextureMappingColorProgressFn &progress_fn = {}); + bool set_from_triangle_sampler(const indexed_triangle_set &its, + const TextureMappingColorSampler &sampler, + int max_depth = 2, + float split_color_threshold = 0.045f, + const TextureMappingColorSubdivisionDepths &subdivision_depths = {}, + const std::vector *resample_triangles = nullptr, + const TextureMappingColorLeafResamplePredicate &resample_leaf = {}, + const TextureMappingColorProgressFn &progress_fn = {}); void get_facet_triangles(const ModelVolume &mv, std::vector &facets) const; void get_facet_triangles(const indexed_triangle_set &its, std::vector &facets) const; diff --git a/src/libslic3r/ModelTextureDataRemap.cpp b/src/libslic3r/ModelTextureDataRemap.cpp new file mode 100644 index 00000000000..1d141c46909 --- /dev/null +++ b/src/libslic3r/ModelTextureDataRemap.cpp @@ -0,0 +1,1339 @@ +// original author: sentientstardust + +#include "ModelTextureDataRemap.hpp" + +#include "AABBMesh.hpp" +#include "ImageMapRawFilamentOffsetAtlas.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include + +namespace Slic3r { +namespace { + +constexpr uint32_t GeneratedImageTextureSize = 2048; +constexpr uint32_t MaxSourceImageTextureSize = 2048; +constexpr int GeneratedImageTextureUvMapVersion = 1; +constexpr float RemapEpsilon = 1e-6f; + +struct GeneratedImageTextureIsland +{ + size_t tri_idx = 0; + std::array local_uvs; + float width = 0.f; + float height = 0.f; + int rect_width = 0; + int rect_height = 0; + int x = 0; + int y = 0; +}; + +struct GeneratedImageTextureAtlas +{ + int padding_px = 0; + float scale = 0.f; + std::vector islands; + std::vector island_by_triangle; +}; + +struct RgbaFacetLookup +{ + std::unordered_map> by_triangle; +}; + +struct FastTriangleProjection +{ + bool enabled = false; + size_t source_triangle = 0; + std::array source_barycentrics; +}; + +static void check_cancel(const SimplifyTextureCancelFn &throw_on_cancel) +{ + if (throw_on_cancel) + throw_on_cancel(); +} + +static void set_progress(const SimplifyTextureProgressFn &status_fn, int percent) +{ + if (status_fn) + status_fn(std::clamp(percent, 0, 100)); +} + +static uint32_t pack_rgba(const ColorRGBA &color) +{ + auto to_u8 = [](float value) -> uint32_t { + return uint32_t(std::clamp(value, 0.f, 1.f) * 255.f + 0.5f); + }; + const uint32_t r = to_u8(color.r()); + const uint32_t g = to_u8(color.g()); + const uint32_t b = to_u8(color.b()); + const uint32_t a = to_u8(color.a()); + return (r << 24) | (g << 16) | (b << 8) | a; +} + +static ColorRGBA unpack_rgba(uint32_t packed) +{ + return ColorRGBA(float((packed >> 24) & 0xFFu) / 255.f, + float((packed >> 16) & 0xFFu) / 255.f, + float((packed >> 8) & 0xFFu) / 255.f, + float(packed & 0xFFu) / 255.f); +} + +static bool valid_triangle_vertices(const indexed_triangle_set &its, size_t tri_idx, std::array &vertices) +{ + if (tri_idx >= its.indices.size()) + return false; + const stl_triangle_vertex_indices &tri = its.indices[tri_idx]; + if (tri[0] < 0 || tri[1] < 0 || tri[2] < 0) + return false; + if (size_t(tri[0]) >= its.vertices.size() || size_t(tri[1]) >= its.vertices.size() || size_t(tri[2]) >= its.vertices.size()) + return false; + vertices = { its.vertices[size_t(tri[0])].cast(), + its.vertices[size_t(tri[1])].cast(), + its.vertices[size_t(tri[2])].cast() }; + return true; +} + +static Vec3f normalized_nonnegative_barycentric(Vec3f weights) +{ + weights.x() = std::max(weights.x(), 0.f); + weights.y() = std::max(weights.y(), 0.f); + weights.z() = std::max(weights.z(), 0.f); + const float sum = weights.x() + weights.y() + weights.z(); + if (sum <= RemapEpsilon) + return Vec3f(1.f / 3.f, 1.f / 3.f, 1.f / 3.f); + weights /= sum; + return weights; +} + +static Vec3f barycentric_weights_3d(const Vec3f &point, const std::array &vertices) +{ + const Vec3f v0 = vertices[1] - vertices[0]; + const Vec3f v1 = vertices[2] - vertices[0]; + const Vec3f v2 = point - vertices[0]; + const float d00 = v0.dot(v0); + const float d01 = v0.dot(v1); + const float d11 = v1.dot(v1); + const float d20 = v2.dot(v0); + const float d21 = v2.dot(v1); + const float denom = d00 * d11 - d01 * d01; + if (!std::isfinite(denom) || std::abs(denom) <= RemapEpsilon) + return Vec3f(1.f / 3.f, 1.f / 3.f, 1.f / 3.f); + const float v = (d11 * d20 - d01 * d21) / denom; + const float w = (d00 * d21 - d01 * d20) / denom; + return Vec3f(1.f - v - w, v, w); +} + +static Vec3f barycentric_weights_2d(const Vec2f &point, const std::array &vertices) +{ + const Vec2f v0 = vertices[1] - vertices[0]; + const Vec2f v1 = vertices[2] - vertices[0]; + const Vec2f v2 = point - vertices[0]; + const float d00 = v0.dot(v0); + const float d01 = v0.dot(v1); + const float d11 = v1.dot(v1); + const float d20 = v2.dot(v0); + const float d21 = v2.dot(v1); + const float denom = d00 * d11 - d01 * d01; + if (!std::isfinite(denom) || std::abs(denom) <= RemapEpsilon) + return Vec3f(1.f / 3.f, 1.f / 3.f, 1.f / 3.f); + const float v = (d11 * d20 - d01 * d21) / denom; + const float w = (d00 * d21 - d01 * d20) / denom; + return Vec3f(1.f - v - w, v, w); +} + +static float wrap_texture_uv(float uv) +{ + if (!std::isfinite(uv)) + return 0.f; + float wrapped = uv - std::floor(uv); + if (wrapped < 0.f) + wrapped += 1.f; + return wrapped; +} + +static ColorRGBA sample_rgba_bilinear_wrapped(const std::vector &rgba, uint32_t width, uint32_t height, const Vec2f &uv) +{ + if (width == 0 || height == 0 || rgba.size() < size_t(width) * size_t(height) * 4) + return ColorRGBA(1.f, 1.f, 1.f, 1.f); + + const float u = wrap_texture_uv(uv.x()); + const float v = wrap_texture_uv(uv.y()); + const float x = u * float(width > 1 ? width - 1 : 0); + const float y = v * float(height > 1 ? height - 1 : 0); + const size_t x0 = std::min(size_t(std::floor(x)), size_t(width - 1)); + const size_t y0 = std::min(size_t(std::floor(y)), size_t(height - 1)); + const size_t x1 = std::min(x0 + 1, size_t(width - 1)); + const size_t y1 = std::min(y0 + 1, size_t(height - 1)); + const float tx = x - float(x0); + const float ty = y - float(y0); + + auto sample_channel = [&rgba, width](size_t sx, size_t sy, size_t channel) { + const size_t idx = (sy * size_t(width) + sx) * 4 + channel; + return float(rgba[idx]) / 255.f; + }; + auto blend_channel = [&](size_t channel) { + const float c00 = sample_channel(x0, y0, channel); + const float c10 = sample_channel(x1, y0, channel); + const float c01 = sample_channel(x0, y1, channel); + const float c11 = sample_channel(x1, y1, channel); + 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); + }; + + return ColorRGBA(blend_channel(0), blend_channel(1), blend_channel(2), blend_channel(3)); +} + +static std::vector sample_raw_offsets_bilinear_wrapped(const std::vector &offsets, + uint32_t width, + uint32_t height, + uint32_t channels, + const Vec2f &uv) +{ + std::vector out(size_t(channels), 0); + if (width == 0 || height == 0 || channels == 0 || offsets.size() < size_t(width) * size_t(height) * size_t(channels)) + return out; + + const float u = wrap_texture_uv(uv.x()); + const float v = wrap_texture_uv(uv.y()); + const float x = u * float(width > 1 ? width - 1 : 0); + const float y = v * float(height > 1 ? height - 1 : 0); + const size_t x0 = std::min(size_t(std::floor(x)), size_t(width - 1)); + const size_t y0 = std::min(size_t(std::floor(y)), size_t(height - 1)); + const size_t x1 = std::min(x0 + 1, size_t(width - 1)); + const size_t y1 = std::min(y0 + 1, size_t(height - 1)); + const float tx = x - float(x0); + const float ty = y - float(y0); + + auto sample_channel = [&offsets, width, channels](size_t sx, size_t sy, size_t channel) { + const size_t idx = (sy * size_t(width) + sx) * size_t(channels) + channel; + return float(offsets[idx]); + }; + for (size_t channel = 0; channel < size_t(channels); ++channel) { + const float c00 = sample_channel(x0, y0, channel); + const float c10 = sample_channel(x1, y0, channel); + const float c01 = sample_channel(x0, y1, channel); + const float c11 = sample_channel(x1, y1, channel); + const float cx0 = c00 + (c10 - c00) * tx; + const float cx1 = c01 + (c11 - c01) * tx; + out[channel] = uint8_t(std::clamp(cx0 + (cx1 - cx0) * ty, 0.f, 255.f) + 0.5f); + } + return out; +} + +static std::vector resized_rgba_bilinear(const std::vector &rgba, + uint32_t width, + uint32_t height, + uint32_t resized_width, + uint32_t resized_height) +{ + std::vector resized(size_t(resized_width) * size_t(resized_height) * 4, 255); + if (width == 0 || height == 0 || resized_width == 0 || resized_height == 0 || rgba.size() < size_t(width) * size_t(height) * 4) + return resized; + + for (uint32_t y = 0; y < resized_height; ++y) { + for (uint32_t x = 0; x < resized_width; ++x) { + const Vec2f uv((float(x) + 0.5f) / float(resized_width), (float(y) + 0.5f) / float(resized_height)); + const ColorRGBA color = sample_rgba_bilinear_wrapped(rgba, width, height, uv); + const size_t idx = (size_t(y) * size_t(resized_width) + size_t(x)) * 4; + resized[idx + 0] = uint8_t(std::clamp(color.r(), 0.f, 1.f) * 255.f + 0.5f); + resized[idx + 1] = uint8_t(std::clamp(color.g(), 0.f, 1.f) * 255.f + 0.5f); + resized[idx + 2] = uint8_t(std::clamp(color.b(), 0.f, 1.f) * 255.f + 0.5f); + resized[idx + 3] = uint8_t(std::clamp(color.a(), 0.f, 1.f) * 255.f + 0.5f); + } + } + return resized; +} + +static std::vector resized_raw_offsets_bilinear(const std::vector &offsets, + uint32_t width, + uint32_t height, + uint32_t channels, + uint32_t resized_width, + uint32_t resized_height) +{ + std::vector resized(size_t(resized_width) * size_t(resized_height) * size_t(channels), 0); + if (width == 0 || height == 0 || channels == 0 || resized_width == 0 || resized_height == 0 || + offsets.size() < size_t(width) * size_t(height) * size_t(channels)) + return resized; + + for (uint32_t y = 0; y < resized_height; ++y) { + for (uint32_t x = 0; x < resized_width; ++x) { + const Vec2f uv((float(x) + 0.5f) / float(resized_width), (float(y) + 0.5f) / float(resized_height)); + const float old_x = wrap_texture_uv(uv.x()) * float(width > 1 ? width - 1 : 0); + const float old_y = wrap_texture_uv(uv.y()) * float(height > 1 ? height - 1 : 0); + const size_t x0 = std::min(size_t(std::floor(old_x)), size_t(width - 1)); + const size_t y0 = std::min(size_t(std::floor(old_y)), size_t(height - 1)); + const size_t x1 = std::min(x0 + 1, size_t(width - 1)); + const size_t y1 = std::min(y0 + 1, size_t(height - 1)); + const float tx = old_x - float(x0); + const float ty = old_y - float(y0); + const size_t idx = (size_t(y) * size_t(resized_width) + size_t(x)) * size_t(channels); + for (size_t channel = 0; channel < size_t(channels); ++channel) { + auto sample_channel = [&offsets, width, channels, channel](size_t sx, size_t sy) { + return float(offsets[(sy * size_t(width) + sx) * size_t(channels) + channel]); + }; + const float c00 = sample_channel(x0, y0); + const float c10 = sample_channel(x1, y0); + const float c01 = sample_channel(x0, y1); + const float c11 = sample_channel(x1, y1); + const float cx0 = c00 + (c10 - c00) * tx; + const float cx1 = c01 + (c11 - c01) * tx; + resized[idx + channel] = uint8_t(std::clamp(cx0 + (cx1 - cx0) * ty, 0.f, 255.f) + 0.5f); + } + } + } + return resized; +} + +static Vec3f texture_barycentric_for_bleed_safe_sampling(const Vec3f &barycentric, + const Vec2f &uv0, + const Vec2f &uv1, + const Vec2f &uv2, + uint32_t width, + uint32_t height) +{ + Vec3f safe = normalized_nonnegative_barycentric(barycentric); + if (width == 0 || height == 0) + return safe; + + auto pixel_edge_length = [width, height](const Vec2f &a, const Vec2f &b) { + const Vec2f delta = b - a; + return std::sqrt(Slic3r::sqr(delta.x() * float(width)) + Slic3r::sqr(delta.y() * float(height))); + }; + const float max_edge = std::max({ pixel_edge_length(uv0, uv1), pixel_edge_length(uv1, uv2), pixel_edge_length(uv2, uv0) }); + if (!std::isfinite(max_edge) || max_edge <= RemapEpsilon) + return safe; + + const float min_barycentric = std::min(0.08f, 0.75f / max_edge); + if (min_barycentric <= 0.f) + return safe; + + safe.x() = std::max(safe.x(), min_barycentric); + safe.y() = std::max(safe.y(), min_barycentric); + safe.z() = std::max(safe.z(), min_barycentric); + return normalized_nonnegative_barycentric(safe); +} + +static void unwrap_uvs(std::array &uvs) +{ + for (size_t idx = 1; idx < uvs.size(); ++idx) { + while (uvs[idx].x() - uvs[0].x() > 0.5f) + uvs[idx].x() -= 1.f; + while (uvs[idx].x() - uvs[0].x() < -0.5f) + uvs[idx].x() += 1.f; + while (uvs[idx].y() - uvs[0].y() > 0.5f) + uvs[idx].y() -= 1.f; + while (uvs[idx].y() - uvs[0].y() < -0.5f) + uvs[idx].y() += 1.f; + } +} + +static bool snapshot_has_valid_raw_atlas(const SimplifyTextureDataSnapshot &snapshot) +{ + return snapshot.texture_width > 0 && + snapshot.texture_height > 0 && + snapshot.texture_raw_channels > 0 && + snapshot.texture_raw_filament_offsets.size() >= + size_t(snapshot.texture_width) * size_t(snapshot.texture_height) * size_t(snapshot.texture_raw_channels); +} + +static bool snapshot_has_valid_rgba_texture(const SimplifyTextureDataSnapshot &snapshot) +{ + return snapshot.texture_width > 0 && + snapshot.texture_height > 0 && + snapshot.texture_rgba.size() >= size_t(snapshot.texture_width) * size_t(snapshot.texture_height) * 4; +} + +static void limit_snapshot_texture_resolution(SimplifyTextureDataSnapshot &snapshot) +{ + if (snapshot.texture_width <= MaxSourceImageTextureSize && snapshot.texture_height <= MaxSourceImageTextureSize) + return; + if (snapshot.texture_width == 0 || snapshot.texture_height == 0) + return; + + const uint32_t old_width = snapshot.texture_width; + const uint32_t old_height = snapshot.texture_height; + const float scale = std::min(float(MaxSourceImageTextureSize) / float(old_width), + float(MaxSourceImageTextureSize) / float(old_height)); + const uint32_t resized_width = std::max(1, uint32_t(std::round(float(old_width) * scale))); + const uint32_t resized_height = std::max(1, uint32_t(std::round(float(old_height) * scale))); + + if (snapshot_has_valid_rgba_texture(snapshot)) + snapshot.texture_rgba = resized_rgba_bilinear(snapshot.texture_rgba, old_width, old_height, resized_width, resized_height); + if (snapshot_has_valid_raw_atlas(snapshot)) + snapshot.texture_raw_filament_offsets = + resized_raw_offsets_bilinear(snapshot.texture_raw_filament_offsets, + old_width, + old_height, + snapshot.texture_raw_channels, + resized_width, + resized_height); + + snapshot.texture_width = resized_width; + snapshot.texture_height = resized_height; +} + +static bool volume_has_valid_raw_atlas(const ModelVolume &volume) +{ + return volume.imported_texture_width > 0 && + volume.imported_texture_height > 0 && + volume.imported_texture_raw_channels > 0 && + volume.imported_texture_raw_filament_offsets.size() >= + size_t(volume.imported_texture_width) * size_t(volume.imported_texture_height) * size_t(volume.imported_texture_raw_channels); +} + +static bool volume_has_valid_image_texture(const ModelVolume &volume) +{ + const indexed_triangle_set &its = volume.mesh().its; + if (its.vertices.empty() || its.indices.empty()) + return false; + if (volume.imported_texture_uv_valid.size() != its.indices.size() || volume.imported_texture_uvs_per_face.size() < its.indices.size() * 6) + return false; + if (!std::any_of(volume.imported_texture_uv_valid.begin(), volume.imported_texture_uv_valid.end(), [](uint8_t valid) { return valid != 0; })) + return false; + const bool valid_rgba = volume.imported_texture_width > 0 && + volume.imported_texture_height > 0 && + volume.imported_texture_rgba.size() >= + size_t(volume.imported_texture_width) * size_t(volume.imported_texture_height) * 4; + return valid_rgba || volume_has_valid_raw_atlas(volume); +} + +static std::array source_triangle_uvs(const SimplifyTextureDataSnapshot &snapshot, size_t tri_idx, bool *valid = nullptr) +{ + std::array uvs = { Vec2f::Zero(), Vec2f::Zero(), Vec2f::Zero() }; + bool ok = tri_idx < snapshot.texture_uv_valid.size() && + snapshot.texture_uv_valid[tri_idx] != 0 && + tri_idx * 6 + 5 < snapshot.texture_uvs_per_face.size(); + if (ok) { + const size_t uv_offset = tri_idx * 6; + uvs = { Vec2f(snapshot.texture_uvs_per_face[uv_offset + 0], snapshot.texture_uvs_per_face[uv_offset + 1]), + Vec2f(snapshot.texture_uvs_per_face[uv_offset + 2], snapshot.texture_uvs_per_face[uv_offset + 3]), + Vec2f(snapshot.texture_uvs_per_face[uv_offset + 4], snapshot.texture_uvs_per_face[uv_offset + 5]) }; + unwrap_uvs(uvs); + } + if (valid != nullptr) + *valid = ok; + return uvs; +} + +static ColorRGBA sample_image_rgba_at_source(const SimplifyTextureDataSnapshot &snapshot, size_t tri_idx, const Vec3f &barycentric) +{ + bool uv_valid = false; + const std::array uvs = source_triangle_uvs(snapshot, tri_idx, &uv_valid); + if (!uv_valid) + return ColorRGBA(1.f, 1.f, 1.f, 1.f); + const Vec3f safe = texture_barycentric_for_bleed_safe_sampling( + barycentric, uvs[0], uvs[1], uvs[2], snapshot.texture_width, snapshot.texture_height); + const Vec2f uv = uvs[0] * safe.x() + uvs[1] * safe.y() + uvs[2] * safe.z(); + return sample_rgba_bilinear_wrapped(snapshot.texture_rgba, snapshot.texture_width, snapshot.texture_height, uv); +} + +static std::vector sample_image_raw_at_source(const SimplifyTextureDataSnapshot &snapshot, size_t tri_idx, const Vec3f &barycentric) +{ + bool uv_valid = false; + const std::array uvs = source_triangle_uvs(snapshot, tri_idx, &uv_valid); + if (!uv_valid) + return std::vector(size_t(snapshot.texture_raw_channels), 0); + const Vec3f safe = texture_barycentric_for_bleed_safe_sampling( + barycentric, uvs[0], uvs[1], uvs[2], snapshot.texture_width, snapshot.texture_height); + const Vec2f uv = uvs[0] * safe.x() + uvs[1] * safe.y() + uvs[2] * safe.z(); + return sample_raw_offsets_bilinear_wrapped(snapshot.texture_raw_filament_offsets, + snapshot.texture_width, + snapshot.texture_height, + snapshot.texture_raw_channels, + uv); +} + +static bool project_to_source(const SimplifyTextureDataSnapshot &snapshot, + const AABBMesh &aabb, + const Vec3f &point, + size_t &source_triangle, + Vec3f &source_barycentric) +{ + int tri_idx = -1; + Vec3d closest = Vec3d::Zero(); + const double squared_distance = aabb.squared_distance(point.cast(), tri_idx, closest); + if (tri_idx < 0 || !std::isfinite(squared_distance) || size_t(tri_idx) >= snapshot.source_mesh.indices.size()) + return false; + + std::array vertices; + if (!valid_triangle_vertices(snapshot.source_mesh, size_t(tri_idx), vertices)) + return false; + + source_triangle = size_t(tri_idx); + source_barycentric = normalized_nonnegative_barycentric(barycentric_weights_3d(closest.cast(), vertices)); + return true; +} + +static FastTriangleProjection make_fast_triangle_projection(const SimplifyTextureDataSnapshot &snapshot, + const AABBMesh &aabb, + const std::array &target_vertices) +{ + FastTriangleProjection projection; + size_t first_source_triangle = 0; + for (size_t corner = 0; corner < 3; ++corner) { + size_t source_triangle = 0; + Vec3f source_barycentric = Vec3f(1.f / 3.f, 1.f / 3.f, 1.f / 3.f); + if (!project_to_source(snapshot, aabb, target_vertices[corner], source_triangle, source_barycentric)) + return projection; + if (corner == 0) + first_source_triangle = source_triangle; + else if (source_triangle != first_source_triangle) + return projection; + projection.source_barycentrics[corner] = source_barycentric; + } + + projection.enabled = true; + projection.source_triangle = first_source_triangle; + return projection; +} + +static bool projected_source_from_fast_triangle(const FastTriangleProjection &projection, + const Vec3f &target_barycentric, + size_t &source_triangle, + Vec3f &source_barycentric) +{ + if (!projection.enabled) + return false; + source_triangle = projection.source_triangle; + source_barycentric = normalized_nonnegative_barycentric(projection.source_barycentrics[0] * target_barycentric.x() + + projection.source_barycentrics[1] * target_barycentric.y() + + projection.source_barycentrics[2] * target_barycentric.z()); + return true; +} + +static bool make_generated_image_texture_island(const indexed_triangle_set &its, + size_t tri_idx, + GeneratedImageTextureIsland &island) +{ + std::array vertices; + if (!valid_triangle_vertices(its, tri_idx, vertices)) + return false; + + const float edge_01 = (vertices[1] - vertices[0]).norm(); + const float edge_12 = (vertices[2] - vertices[1]).norm(); + const float edge_20 = (vertices[0] - vertices[2]).norm(); + + int a = 0; + int b = 1; + int c = 2; + float base_length = edge_01; + if (edge_12 > base_length) { + a = 1; + b = 2; + c = 0; + base_length = edge_12; + } + if (edge_20 > base_length) { + a = 2; + b = 0; + c = 1; + base_length = edge_20; + } + if (!std::isfinite(base_length) || base_length <= RemapEpsilon) + return false; + + const Vec3f base = vertices[b] - vertices[a]; + const Vec3f side = vertices[c] - vertices[a]; + const float projected = side.dot(base) / base_length; + const float height_sq = std::max(0.f, side.squaredNorm() - projected * projected); + const float height = std::sqrt(height_sq); + + std::array local_uvs; + local_uvs[size_t(a)] = Vec2f(0.f, 0.f); + local_uvs[size_t(b)] = Vec2f(base_length, 0.f); + local_uvs[size_t(c)] = Vec2f(projected, height); + + const float min_x = std::min({ local_uvs[0].x(), local_uvs[1].x(), local_uvs[2].x() }); + const float min_y = std::min({ local_uvs[0].y(), local_uvs[1].y(), local_uvs[2].y() }); + const float max_x = std::max({ local_uvs[0].x(), local_uvs[1].x(), local_uvs[2].x() }); + const float max_y = std::max({ local_uvs[0].y(), local_uvs[1].y(), local_uvs[2].y() }); + for (Vec2f &uv : local_uvs) + uv -= Vec2f(min_x, min_y); + + island.tri_idx = tri_idx; + island.local_uvs = local_uvs; + island.width = std::max(max_x - min_x, 1e-4f); + island.height = std::max(max_y - min_y, 1e-4f); + return std::isfinite(island.width) && std::isfinite(island.height); +} + +static bool pack_generated_image_texture_islands(std::vector &islands, + uint32_t texture_size, + int padding_px, + float scale) +{ + if (islands.empty() || texture_size == 0 || !std::isfinite(scale) || scale <= 0.f) + return false; + + const int atlas_size = int(texture_size); + for (GeneratedImageTextureIsland &island : islands) { + const int content_width = std::max(1, int(std::ceil(island.width * scale))) + 1; + const int content_height = std::max(1, int(std::ceil(island.height * scale))) + 1; + island.rect_width = content_width + padding_px * 2; + island.rect_height = content_height + padding_px * 2; + if (island.rect_width > atlas_size || island.rect_height > atlas_size) + return false; + } + + std::vector order(islands.size()); + for (size_t idx = 0; idx < order.size(); ++idx) + order[idx] = idx; + std::sort(order.begin(), order.end(), [&islands](size_t lhs, size_t rhs) { + const GeneratedImageTextureIsland &a = islands[lhs]; + const GeneratedImageTextureIsland &b = islands[rhs]; + if (a.rect_height != b.rect_height) + return a.rect_height > b.rect_height; + if (a.rect_width != b.rect_width) + return a.rect_width > b.rect_width; + return a.tri_idx < b.tri_idx; + }); + + int x = 0; + int y = 0; + int row_height = 0; + for (const size_t island_idx : order) { + GeneratedImageTextureIsland &island = islands[island_idx]; + if (x + island.rect_width > atlas_size) { + y += row_height; + x = 0; + row_height = 0; + } + if (y + island.rect_height > atlas_size) + return false; + island.x = x; + island.y = y; + x += island.rect_width; + row_height = std::max(row_height, island.rect_height); + } + + return true; +} + +static bool pack_generated_image_texture_atlas(GeneratedImageTextureAtlas &atlas, uint32_t texture_size) +{ + if (atlas.islands.empty()) + return false; + + float max_dimension = 0.f; + for (const GeneratedImageTextureIsland &island : atlas.islands) + max_dimension = std::max({ max_dimension, island.width, island.height }); + if (!std::isfinite(max_dimension) || max_dimension <= RemapEpsilon) + return false; + + const std::array padding_options = { 2, 1, 0 }; + for (const int padding_px : padding_options) { + const float available = float(int(texture_size) - padding_px * 2 - 1); + if (available <= 0.f) + continue; + + float high = available / max_dimension; + if (!std::isfinite(high) || high <= 0.f) + continue; + + bool found = false; + std::vector best; + float best_scale = 0.f; + float upper = high; + for (int attempt = 0; attempt < 12; ++attempt) { + std::vector candidate = atlas.islands; + if (pack_generated_image_texture_islands(candidate, texture_size, padding_px, high)) { + found = true; + best = std::move(candidate); + best_scale = high; + break; + } + upper = high; + high *= 0.5f; + } + if (!found) + continue; + + float low = best_scale; + high = upper; + for (int iter = 0; iter < 24; ++iter) { + const float mid = (low + high) * 0.5f; + std::vector candidate = atlas.islands; + if (pack_generated_image_texture_islands(candidate, texture_size, padding_px, mid)) { + low = mid; + best = std::move(candidate); + best_scale = mid; + } else { + high = mid; + } + } + + atlas.padding_px = padding_px; + atlas.scale = best_scale; + atlas.islands = std::move(best); + atlas.island_by_triangle.assign(atlas.island_by_triangle.size(), -1); + for (size_t island_idx = 0; island_idx < atlas.islands.size(); ++island_idx) + if (atlas.islands[island_idx].tri_idx < atlas.island_by_triangle.size()) + atlas.island_by_triangle[atlas.islands[island_idx].tri_idx] = int(island_idx); + return true; + } + + return false; +} + +static bool initialize_generated_image_texture(const indexed_triangle_set &its, + SimplifyTextureDataResult &result, + GeneratedImageTextureAtlas &atlas, + const ColorRGBA &background) +{ + if (its.vertices.empty() || its.indices.empty()) + return false; + + atlas.island_by_triangle.assign(its.indices.size(), -1); + atlas.islands.reserve(its.indices.size()); + for (size_t tri_idx = 0; tri_idx < its.indices.size(); ++tri_idx) { + GeneratedImageTextureIsland island; + if (make_generated_image_texture_island(its, tri_idx, island)) + atlas.islands.emplace_back(island); + } + if (!pack_generated_image_texture_atlas(atlas, GeneratedImageTextureSize)) + return false; + + const uint8_t r = uint8_t(std::clamp(background.r(), 0.f, 1.f) * 255.f + 0.5f); + const uint8_t g = uint8_t(std::clamp(background.g(), 0.f, 1.f) * 255.f + 0.5f); + const uint8_t b = uint8_t(std::clamp(background.b(), 0.f, 1.f) * 255.f + 0.5f); + const uint8_t a = uint8_t(std::clamp(background.a(), 0.f, 1.f) * 255.f + 0.5f); + const size_t pixel_count = size_t(GeneratedImageTextureSize) * size_t(GeneratedImageTextureSize); + + result.texture_width = GeneratedImageTextureSize; + result.texture_height = GeneratedImageTextureSize; + result.uv_map_generator_version = GeneratedImageTextureUvMapVersion; + result.texture_rgba.assign(pixel_count * 4, 0); + for (size_t idx = 0; idx < pixel_count; ++idx) { + result.texture_rgba[idx * 4 + 0] = r; + result.texture_rgba[idx * 4 + 1] = g; + result.texture_rgba[idx * 4 + 2] = b; + result.texture_rgba[idx * 4 + 3] = a; + } + result.texture_uv_valid.assign(its.indices.size(), 0); + result.texture_uvs_per_face.assign(its.indices.size() * 6, 0.f); + + const float texture_size = float(GeneratedImageTextureSize); + for (const GeneratedImageTextureIsland &island : atlas.islands) { + if (island.tri_idx >= its.indices.size()) + continue; + result.texture_uv_valid[island.tri_idx] = 1; + const size_t uv_offset = island.tri_idx * 6; + for (size_t corner = 0; corner < 3; ++corner) { + const Vec2f pixel(float(island.x + atlas.padding_px) + 0.5f + island.local_uvs[corner].x() * atlas.scale, + float(island.y + atlas.padding_px) + 0.5f + island.local_uvs[corner].y() * atlas.scale); + result.texture_uvs_per_face[uv_offset + corner * 2 + 0] = std::clamp(pixel.x() / texture_size, 0.f, 1.f); + result.texture_uvs_per_face[uv_offset + corner * 2 + 1] = std::clamp(pixel.y() / texture_size, 0.f, 1.f); + } + } + + return true; +} + +static bool write_rgba_pixel(std::vector &rgba, uint32_t width, uint32_t x, uint32_t y, const ColorRGBA &color) +{ + if (width == 0) + return false; + const size_t idx = (size_t(y) * size_t(width) + size_t(x)) * 4; + if (idx + 3 >= rgba.size()) + return false; + rgba[idx + 0] = uint8_t(std::clamp(color.r(), 0.f, 1.f) * 255.f + 0.5f); + rgba[idx + 1] = uint8_t(std::clamp(color.g(), 0.f, 1.f) * 255.f + 0.5f); + rgba[idx + 2] = uint8_t(std::clamp(color.b(), 0.f, 1.f) * 255.f + 0.5f); + rgba[idx + 3] = uint8_t(std::clamp(color.a(), 0.f, 1.f) * 255.f + 0.5f); + return true; +} + +static bool write_raw_offset_pixel(std::vector &offsets, + uint32_t width, + uint32_t channels, + uint32_t x, + uint32_t y, + const std::vector &values) +{ + if (width == 0 || channels == 0 || values.empty()) + return false; + const size_t idx = (size_t(y) * size_t(width) + size_t(x)) * size_t(channels); + if (idx + size_t(channels) > offsets.size()) + return false; + for (size_t channel = 0; channel < size_t(channels); ++channel) + offsets[idx + channel] = channel < values.size() ? values[channel] : 0; + return true; +} + +static RgbaFacetLookup make_rgba_facet_lookup(const SimplifyTextureDataSnapshot &snapshot) +{ + RgbaFacetLookup lookup; + lookup.by_triangle.reserve(snapshot.rgba_facets.size()); + for (size_t idx = 0; idx < snapshot.rgba_facets.size(); ++idx) + lookup.by_triangle[snapshot.rgba_facets[idx].source_triangle].emplace_back(idx); + return lookup; +} + +static ColorRGBA rgb_metadata_background_color(const std::string &metadata) +{ + const std::string key = "\"background_color\":\"#"; + const size_t start = metadata.find(key); + if (start == std::string::npos || start + key.size() + 8 > metadata.size()) + return ColorRGBA(1.f, 1.f, 1.f, 1.f); + + uint32_t packed = 0; + for (size_t idx = 0; idx < 8; ++idx) { + const char ch = metadata[start + key.size() + idx]; + const int value = ch >= '0' && ch <= '9' ? ch - '0' : + ch >= 'a' && ch <= 'f' ? ch - 'a' + 10 : + ch >= 'A' && ch <= 'F' ? ch - 'A' + 10 : -1; + if (value < 0) + return ColorRGBA(1.f, 1.f, 1.f, 1.f); + packed = (packed << 4) | uint32_t(value); + } + return unpack_rgba(packed); +} + +static std::string rgb_metadata_json(const ColorRGBA &background) +{ + const uint32_t packed = pack_rgba(background); + char buffer[48]; + std::snprintf(buffer, + sizeof(buffer), + "{\"background_color\":\"#%02X%02X%02X%02X\"}", + unsigned((packed >> 24) & 0xFFu), + unsigned((packed >> 16) & 0xFFu), + unsigned((packed >> 8) & 0xFFu), + unsigned(packed & 0xFFu)); + return buffer; +} + +static ColorRGBA sample_rgba_facets_at_source(const SimplifyTextureDataSnapshot &snapshot, + const RgbaFacetLookup &lookup, + size_t source_triangle, + const Vec3f &point) +{ + const auto range_it = lookup.by_triangle.find(int(source_triangle)); + const ColorRGBA background = rgb_metadata_background_color(snapshot.rgba_metadata_json); + if (range_it == lookup.by_triangle.end()) + return background; + + float best_distance = std::numeric_limits::max(); + uint32_t best_rgba = pack_rgba(background); + for (const size_t facet_idx : range_it->second) { + const ColorFacetTriangle &facet = snapshot.rgba_facets[facet_idx]; + const Vec3f bary = barycentric_weights_3d(point, facet.vertices); + const float min_weight = std::min({ bary.x(), bary.y(), bary.z() }); + if (min_weight >= -1e-4f) + return unpack_rgba(facet.rgba); + + const Vec3f safe = normalized_nonnegative_barycentric(bary); + const Vec3f closest = facet.vertices[0] * safe.x() + facet.vertices[1] * safe.y() + facet.vertices[2] * safe.z(); + const float distance = (closest - point).squaredNorm(); + if (distance < best_distance) { + best_distance = distance; + best_rgba = facet.rgba; + } + } + return unpack_rgba(best_rgba); +} + +static ColorRGBA sample_vertex_color_at_source(const SimplifyTextureDataSnapshot &snapshot, size_t source_triangle, const Vec3f &barycentric) +{ + if (source_triangle >= snapshot.source_mesh.indices.size()) + return ColorRGBA(1.f, 1.f, 1.f, 1.f); + const stl_triangle_vertex_indices &tri = snapshot.source_mesh.indices[source_triangle]; + if (tri[0] < 0 || tri[1] < 0 || tri[2] < 0) + return ColorRGBA(1.f, 1.f, 1.f, 1.f); + if (size_t(tri[0]) >= snapshot.vertex_colors_rgba.size() || + size_t(tri[1]) >= snapshot.vertex_colors_rgba.size() || + size_t(tri[2]) >= snapshot.vertex_colors_rgba.size()) + return ColorRGBA(1.f, 1.f, 1.f, 1.f); + + const ColorRGBA c0 = unpack_rgba(snapshot.vertex_colors_rgba[size_t(tri[0])]); + const ColorRGBA c1 = unpack_rgba(snapshot.vertex_colors_rgba[size_t(tri[1])]); + const ColorRGBA c2 = unpack_rgba(snapshot.vertex_colors_rgba[size_t(tri[2])]); + const Vec3f safe = normalized_nonnegative_barycentric(barycentric); + return ColorRGBA(c0.r() * safe.x() + c1.r() * safe.y() + c2.r() * safe.z(), + c0.g() * safe.x() + c1.g() * safe.y() + c2.g() * safe.z(), + c0.b() * safe.x() + c1.b() * safe.y() + c2.b() * safe.z(), + c0.a() * safe.x() + c1.a() * safe.y() + c2.a() * safe.z()); +} + +static ColorRGBA sample_snapshot_color_at_source(const SimplifyTextureDataSnapshot &snapshot, + const RgbaFacetLookup *rgba_lookup, + size_t source_triangle, + const Vec3f &source_point, + const Vec3f &source_barycentric) +{ + switch (snapshot.source) { + case SimplifyColorSource::RgbaData: + return rgba_lookup != nullptr ? sample_rgba_facets_at_source(snapshot, *rgba_lookup, source_triangle, source_point) : + ColorRGBA(1.f, 1.f, 1.f, 1.f); + case SimplifyColorSource::ImageTexture: + return sample_image_rgba_at_source(snapshot, source_triangle, source_barycentric); + case SimplifyColorSource::VertexColors: + return sample_vertex_color_at_source(snapshot, source_triangle, source_barycentric); + case SimplifyColorSource::None: + break; + } + return ColorRGBA(1.f, 1.f, 1.f, 1.f); +} + +static float triangle_max_edge_length(const std::array &vertices) +{ + return std::max({ (vertices[1] - vertices[0]).norm(), (vertices[2] - vertices[1]).norm(), (vertices[0] - vertices[2]).norm() }); +} + +static float mesh_max_axis_span(const indexed_triangle_set &its) +{ + if (its.vertices.empty()) + return 1.f; + + Vec3f min_point = its.vertices.front().cast(); + Vec3f max_point = min_point; + for (const stl_vertex &vertex : its.vertices) { + const Vec3f point = vertex.cast(); + min_point = min_point.cwiseMin(point); + max_point = max_point.cwiseMax(point); + } + + const Vec3f span = max_point - min_point; + return std::max({ span.x(), span.y(), span.z(), 1.f }); +} + +static int texture_mapping_depth_from_span(float span, float target_span, int max_depth) +{ + if (!std::isfinite(span) || !std::isfinite(target_span) || span <= target_span || target_span <= RemapEpsilon) + return 0; + return std::clamp(int(std::ceil(std::log2(span / target_span))), 0, max_depth); +} + +static SimplifyTextureDataResult remap_rgba_from_snapshot(const SimplifyTextureDataSnapshot &snapshot, + const indexed_triangle_set &simplified_mesh, + const SimplifyTextureCancelFn &throw_on_cancel, + const SimplifyTextureProgressFn &status_fn) +{ + SimplifyTextureDataResult result; + result.source = SimplifyColorSource::RgbaData; + if (simplified_mesh.indices.empty() || snapshot.source_mesh.indices.empty()) + return result; + + set_progress(status_fn, 0); + AABBMesh source_aabb(snapshot.source_mesh); + std::vector projection_cache(simplified_mesh.indices.size()); + std::vector projection_cache_valid(simplified_mesh.indices.size(), 0); + RgbaFacetLookup rgba_lookup; + if (snapshot.source == SimplifyColorSource::RgbaData) + rgba_lookup = make_rgba_facet_lookup(snapshot); + + auto annotation = ColorFacetsAnnotation::make_temporary(); + annotation->set_metadata_json(snapshot.source == SimplifyColorSource::RgbaData ? + snapshot.rgba_metadata_json : + rgb_metadata_json(ColorRGBA(1.f, 1.f, 1.f, 1.f))); + + const int max_depth = snapshot.source == SimplifyColorSource::ImageTexture ? 5 : 4; + const float target_span = std::max(mesh_max_axis_span(simplified_mesh) / 120.f, 0.3f); + TextureMappingColorSubdivisionDepths subdivision_depths = + [target_span, max_depth](size_t, const std::array &vertices) { + const int depth = texture_mapping_depth_from_span(triangle_max_edge_length(vertices), target_span, max_depth); + return std::make_pair(depth, max_depth); + }; + + size_t sample_counter = 0; + TextureMappingColorSampler sampler = [&](size_t target_triangle, const Vec3f &point, const Vec3f &target_barycentric) { + if ((++sample_counter & 1023u) == 0) + check_cancel(throw_on_cancel); + + size_t source_triangle = 0; + Vec3f source_barycentric = Vec3f(1.f / 3.f, 1.f / 3.f, 1.f / 3.f); + bool source_projected = false; + if (target_triangle < projection_cache.size()) { + if (projection_cache_valid[target_triangle] == 0) { + std::array target_vertices; + if (valid_triangle_vertices(simplified_mesh, target_triangle, target_vertices)) + projection_cache[target_triangle] = make_fast_triangle_projection(snapshot, source_aabb, target_vertices); + projection_cache_valid[target_triangle] = 1; + } + source_projected = projected_source_from_fast_triangle(projection_cache[target_triangle], + target_barycentric, + source_triangle, + source_barycentric); + } + if (!source_projected && !project_to_source(snapshot, source_aabb, point, source_triangle, source_barycentric)) + return pack_rgba(ColorRGBA(1.f, 1.f, 1.f, 1.f)); + + std::array source_vertices; + if (!valid_triangle_vertices(snapshot.source_mesh, source_triangle, source_vertices)) + return pack_rgba(ColorRGBA(1.f, 1.f, 1.f, 1.f)); + const Vec3f source_point = source_vertices[0] * source_barycentric.x() + + source_vertices[1] * source_barycentric.y() + + source_vertices[2] * source_barycentric.z(); + return pack_rgba(sample_snapshot_color_at_source(snapshot, + snapshot.source == SimplifyColorSource::RgbaData ? &rgba_lookup : nullptr, + source_triangle, + source_point, + source_barycentric)); + }; + + const float split_threshold = snapshot.source == SimplifyColorSource::ImageTexture ? 0.025f : 0.04f; + TextureMappingColorProgressFn progress_fn = [&status_fn](size_t completed, size_t total) { + const int percent = total == 0 ? 100 : int((uint64_t(completed) * 100u) / uint64_t(total)); + set_progress(status_fn, percent); + }; + annotation->set_from_triangle_sampler(simplified_mesh, sampler, max_depth, split_threshold, subdivision_depths, nullptr, {}, progress_fn); + check_cancel(throw_on_cancel); + set_progress(status_fn, 100); + + if (!annotation->empty()) { + result.rgba_data = std::move(annotation); + } + return result; +} + +static bool refresh_result_preview_from_raw(SimplifyTextureDataResult &result) +{ + ImageMapRawFilamentOffsetAtlas atlas; + atlas.width = result.texture_width; + atlas.height = result.texture_height; + atlas.channels = result.texture_raw_channels; + atlas.offsets.swap(result.texture_raw_filament_offsets); + atlas.metadata_json = result.texture_raw_metadata_json; + atlas.filaments = image_map_raw_filaments_from_metadata_json(atlas.metadata_json, atlas.channels); + result.texture_rgba = image_map_raw_filament_offset_preview_rgba(atlas); + result.texture_raw_filament_offsets.swap(atlas.offsets); + return result.texture_rgba.size() >= size_t(result.texture_width) * size_t(result.texture_height) * 4; +} + +static SimplifyTextureDataResult remap_image_texture_from_snapshot(const SimplifyTextureDataSnapshot &snapshot, + const indexed_triangle_set &simplified_mesh, + const SimplifyTextureCancelFn &throw_on_cancel, + const SimplifyTextureProgressFn &status_fn) +{ + SimplifyTextureDataResult result; + std::unique_ptr limited_snapshot; + const SimplifyTextureDataSnapshot *sampling_snapshot = &snapshot; + if (snapshot.texture_width > MaxSourceImageTextureSize || snapshot.texture_height > MaxSourceImageTextureSize) { + limited_snapshot = std::make_unique(snapshot); + limit_snapshot_texture_resolution(*limited_snapshot); + sampling_snapshot = limited_snapshot.get(); + } + + if ((!snapshot_has_valid_rgba_texture(*sampling_snapshot) && !snapshot_has_valid_raw_atlas(*sampling_snapshot)) || + simplified_mesh.indices.empty() || + sampling_snapshot->source_mesh.indices.empty()) + return result; + + set_progress(status_fn, 0); + GeneratedImageTextureAtlas atlas; + if (!initialize_generated_image_texture(simplified_mesh, result, atlas, ColorRGBA(1.f, 1.f, 1.f, 1.f))) + return result; + set_progress(status_fn, 8); + check_cancel(throw_on_cancel); + + const bool remap_raw = snapshot_has_valid_raw_atlas(*sampling_snapshot); + if (remap_raw) { + result.texture_raw_channels = sampling_snapshot->texture_raw_channels; + result.texture_raw_metadata_json = sampling_snapshot->texture_raw_metadata_json; + result.texture_raw_filament_offsets.assign(size_t(result.texture_width) * size_t(result.texture_height) * size_t(result.texture_raw_channels), 0); + } + + AABBMesh source_aabb(sampling_snapshot->source_mesh); + const size_t island_count = atlas.islands.size(); + auto island_pixel_count = [&result](const GeneratedImageTextureIsland &island) -> uint64_t { + const int x_begin = std::max(0, island.x); + const int y_begin = std::max(0, island.y); + const int x_end = std::min(island.x + island.rect_width, int(result.texture_width)); + const int y_end = std::min(island.y + island.rect_height, int(result.texture_height)); + if (x_end <= x_begin || y_end <= y_begin) + return 0; + return uint64_t(x_end - x_begin) * uint64_t(y_end - y_begin); + }; + uint64_t total_pixels = 0; + for (const GeneratedImageTextureIsland &island : atlas.islands) + total_pixels += island_pixel_count(island); + uint64_t processed_pixels = 0; + int last_raster_progress = -1; + auto report_raster_progress = [&status_fn, total_pixels, &processed_pixels, &last_raster_progress]() { + const int progress = total_pixels == 0 ? + 96 : + 8 + int((processed_pixels * 88u) / total_pixels); + if (progress != last_raster_progress) { + last_raster_progress = progress; + set_progress(status_fn, progress); + } + }; + report_raster_progress(); + + size_t pixel_counter = 0; + for (size_t island_idx = 0; island_idx < island_count; ++island_idx) { + if ((island_idx & 31u) == 0) + check_cancel(throw_on_cancel); + + const GeneratedImageTextureIsland &island = atlas.islands[island_idx]; + const uint64_t island_pixels = island_pixel_count(island); + std::array target_vertices; + if (!valid_triangle_vertices(simplified_mesh, island.tri_idx, target_vertices)) { + processed_pixels += island_pixels; + report_raster_progress(); + continue; + } + const FastTriangleProjection fast_projection = make_fast_triangle_projection(*sampling_snapshot, source_aabb, target_vertices); + + const size_t uv_offset = island.tri_idx * 6; + if (uv_offset + 5 >= result.texture_uvs_per_face.size()) { + processed_pixels += island_pixels; + report_raster_progress(); + continue; + } + const float texture_size = float(result.texture_width); + const std::array target_uv_pixels = { + Vec2f(result.texture_uvs_per_face[uv_offset + 0] * texture_size, result.texture_uvs_per_face[uv_offset + 1] * texture_size), + Vec2f(result.texture_uvs_per_face[uv_offset + 2] * texture_size, result.texture_uvs_per_face[uv_offset + 3] * texture_size), + Vec2f(result.texture_uvs_per_face[uv_offset + 4] * texture_size, result.texture_uvs_per_face[uv_offset + 5] * texture_size) + }; + + const int x_end = std::min(island.x + island.rect_width, int(result.texture_width)); + const int y_end = std::min(island.y + island.rect_height, int(result.texture_height)); + for (int y = std::max(0, island.y); y < y_end; ++y) { + for (int x = std::max(0, island.x); x < x_end; ++x) { + ++processed_pixels; + report_raster_progress(); + if ((++pixel_counter & 65535u) == 0) + check_cancel(throw_on_cancel); + const Vec2f pixel(float(x) + 0.5f, float(y) + 0.5f); + const Vec3f raw_target_bary = barycentric_weights_2d(pixel, target_uv_pixels); + const Vec3f target_bary = normalized_nonnegative_barycentric(raw_target_bary); + if (std::min({ raw_target_bary.x(), raw_target_bary.y(), raw_target_bary.z() }) < -1e-4f) { + const Vec2f closest = target_uv_pixels[0] * target_bary.x() + + target_uv_pixels[1] * target_bary.y() + + target_uv_pixels[2] * target_bary.z(); + const float bleed_px = float(atlas.padding_px) + 1.5f; + if ((closest - pixel).squaredNorm() > bleed_px * bleed_px) + continue; + } + const Vec3f target_point = target_vertices[0] * target_bary.x() + + target_vertices[1] * target_bary.y() + + target_vertices[2] * target_bary.z(); + + size_t source_triangle = 0; + Vec3f source_barycentric = Vec3f(1.f / 3.f, 1.f / 3.f, 1.f / 3.f); + if (!projected_source_from_fast_triangle(fast_projection, target_bary, source_triangle, source_barycentric) && + !project_to_source(*sampling_snapshot, source_aabb, target_point, source_triangle, source_barycentric)) + continue; + + if (remap_raw) { + write_raw_offset_pixel(result.texture_raw_filament_offsets, + result.texture_width, + result.texture_raw_channels, + uint32_t(x), + uint32_t(y), + sample_image_raw_at_source(*sampling_snapshot, source_triangle, source_barycentric)); + } else { + write_rgba_pixel(result.texture_rgba, + result.texture_width, + uint32_t(x), + uint32_t(y), + sample_image_rgba_at_source(*sampling_snapshot, source_triangle, source_barycentric)); + } + } + } + } + + if (remap_raw && !refresh_result_preview_from_raw(result)) + return SimplifyTextureDataResult(); + + set_progress(status_fn, 100); + result.source = SimplifyColorSource::ImageTexture; + return result; +} + +static SimplifyTextureDataResult remap_vertex_colors_from_snapshot(const SimplifyTextureDataSnapshot &snapshot, + const indexed_triangle_set &simplified_mesh, + const SimplifyTextureCancelFn &throw_on_cancel, + const SimplifyTextureProgressFn &status_fn) +{ + SimplifyTextureDataResult result; + result.source = SimplifyColorSource::VertexColors; + if (snapshot.vertex_colors_rgba.size() != snapshot.source_mesh.vertices.size() || simplified_mesh.vertices.empty()) + return result; + + set_progress(status_fn, 0); + AABBMesh source_aabb(snapshot.source_mesh); + result.vertex_colors_rgba.assign(simplified_mesh.vertices.size(), 0xFFFFFFFFu); + for (size_t vertex_idx = 0; vertex_idx < simplified_mesh.vertices.size(); ++vertex_idx) { + if ((vertex_idx & 1023u) == 0) { + check_cancel(throw_on_cancel); + set_progress(status_fn, int((uint64_t(vertex_idx) * 100u) / std::max(simplified_mesh.vertices.size(), 1))); + } + + size_t source_triangle = 0; + Vec3f source_barycentric = Vec3f(1.f / 3.f, 1.f / 3.f, 1.f / 3.f); + if (project_to_source(snapshot, source_aabb, simplified_mesh.vertices[vertex_idx].cast(), source_triangle, source_barycentric)) + result.vertex_colors_rgba[vertex_idx] = pack_rgba(sample_vertex_color_at_source(snapshot, source_triangle, source_barycentric)); + } + set_progress(status_fn, 100); + return result; +} + +static void clear_image_texture(ModelVolume &volume) +{ + volume.imported_texture_uvs_per_face.clear(); + volume.imported_texture_uv_valid.clear(); + volume.imported_texture_rgba.clear(); + volume.imported_texture_raw_filament_offsets.clear(); + volume.imported_texture_width = 0; + volume.imported_texture_height = 0; + volume.imported_texture_raw_channels = 0; + volume.imported_texture_raw_metadata_json.clear(); + volume.uv_map_generator_version = 0; +} + +static void touch_imported_texture(ModelVolume &volume) +{ + volume.imported_texture_uvs_per_face.set_new_unique_id(); + volume.imported_texture_uv_valid.set_new_unique_id(); + volume.imported_texture_rgba.set_new_unique_id(); + volume.imported_texture_raw_filament_offsets.set_new_unique_id(); +} + +} // namespace + +SimplifyTextureDataSnapshot snapshot_simplify_texture_data(const ModelVolume &volume) +{ + SimplifyTextureDataSnapshot snapshot; + const indexed_triangle_set &its = volume.mesh().its; + if (its.vertices.empty() || its.indices.empty()) + return snapshot; + + if (!volume.texture_mapping_color_facets.empty()) { + snapshot.source = SimplifyColorSource::RgbaData; + snapshot.source_mesh = its; + snapshot.rgba_metadata_json = volume.texture_mapping_color_facets.metadata_json(); + volume.texture_mapping_color_facets.get_facet_triangles(volume, snapshot.rgba_facets); + if (!snapshot.rgba_facets.empty()) + return snapshot; + snapshot = SimplifyTextureDataSnapshot(); + } + + if (volume_has_valid_image_texture(volume)) { + snapshot.source = SimplifyColorSource::ImageTexture; + snapshot.source_mesh = its; + snapshot.texture_rgba.assign(volume.imported_texture_rgba.begin(), volume.imported_texture_rgba.end()); + snapshot.texture_uvs_per_face.assign(volume.imported_texture_uvs_per_face.begin(), volume.imported_texture_uvs_per_face.end()); + snapshot.texture_uv_valid.assign(volume.imported_texture_uv_valid.begin(), volume.imported_texture_uv_valid.end()); + snapshot.texture_raw_filament_offsets.assign(volume.imported_texture_raw_filament_offsets.begin(), + volume.imported_texture_raw_filament_offsets.end()); + snapshot.texture_width = volume.imported_texture_width; + snapshot.texture_height = volume.imported_texture_height; + snapshot.texture_raw_channels = volume.imported_texture_raw_channels; + snapshot.texture_raw_metadata_json = volume.imported_texture_raw_metadata_json; + snapshot.uv_map_generator_version = volume.uv_map_generator_version; + if (snapshot_has_valid_rgba_texture(snapshot) || snapshot_has_valid_raw_atlas(snapshot)) + return snapshot; + snapshot = SimplifyTextureDataSnapshot(); + } + + if (!volume.imported_vertex_colors_rgba.empty() && volume.imported_vertex_colors_rgba.size() == its.vertices.size()) { + snapshot.source = SimplifyColorSource::VertexColors; + snapshot.source_mesh = its; + snapshot.vertex_colors_rgba.assign(volume.imported_vertex_colors_rgba.begin(), volume.imported_vertex_colors_rgba.end()); + } + return snapshot; +} + +SimplifyTextureDataResult remap_simplify_texture_data(const SimplifyTextureDataSnapshot &snapshot, + const indexed_triangle_set &simplified_mesh, + const SimplifyTextureCancelFn &throw_on_cancel, + const SimplifyTextureProgressFn &status_fn) +{ + if (snapshot.source == SimplifyColorSource::None || simplified_mesh.indices.empty()) { + set_progress(status_fn, 100); + SimplifyTextureDataResult result; + result.source = snapshot.source; + return result; + } + + switch (snapshot.source) { + case SimplifyColorSource::RgbaData: + return remap_rgba_from_snapshot(snapshot, simplified_mesh, throw_on_cancel, status_fn); + case SimplifyColorSource::ImageTexture: { + SimplifyTextureDataResult result = remap_image_texture_from_snapshot(snapshot, simplified_mesh, throw_on_cancel, status_fn); + if (result.source == SimplifyColorSource::ImageTexture) + return result; + SimplifyTextureDataSnapshot fallback_snapshot = snapshot; + limit_snapshot_texture_resolution(fallback_snapshot); + if (!snapshot_has_valid_rgba_texture(fallback_snapshot) && snapshot_has_valid_raw_atlas(fallback_snapshot)) { + ImageMapRawFilamentOffsetAtlas atlas; + atlas.width = fallback_snapshot.texture_width; + atlas.height = fallback_snapshot.texture_height; + atlas.channels = fallback_snapshot.texture_raw_channels; + atlas.offsets = fallback_snapshot.texture_raw_filament_offsets; + atlas.metadata_json = fallback_snapshot.texture_raw_metadata_json; + atlas.filaments = image_map_raw_filaments_from_metadata_json(atlas.metadata_json, atlas.channels); + fallback_snapshot.texture_rgba = image_map_raw_filament_offset_preview_rgba(atlas); + } + result = remap_rgba_from_snapshot(fallback_snapshot, simplified_mesh, throw_on_cancel, status_fn); + result.remap_failed = true; + result.used_fallback_rgba = result.source == SimplifyColorSource::RgbaData && result.rgba_data != nullptr; + return result; + } + case SimplifyColorSource::VertexColors: + return remap_vertex_colors_from_snapshot(snapshot, simplified_mesh, throw_on_cancel, status_fn); + case SimplifyColorSource::None: + break; + } + return SimplifyTextureDataResult(); +} + +void apply_simplify_texture_data_result(ModelVolume &volume, SimplifyTextureDataResult &&result) +{ + switch (result.source) { + case SimplifyColorSource::RgbaData: + if (result.rgba_data) + volume.texture_mapping_color_facets.assign(std::move(*result.rgba_data)); + else + volume.texture_mapping_color_facets.reset(); + clear_image_texture(volume); + volume.imported_vertex_colors_rgba.clear(); + volume.imported_vertex_colors_rgba.set_new_unique_id(); + touch_imported_texture(volume); + break; + case SimplifyColorSource::ImageTexture: + volume.texture_mapping_color_facets.reset(); + volume.imported_vertex_colors_rgba.clear(); + volume.imported_vertex_colors_rgba.set_new_unique_id(); + volume.imported_texture_uvs_per_face = std::move(result.texture_uvs_per_face); + volume.imported_texture_uv_valid = std::move(result.texture_uv_valid); + volume.imported_texture_rgba = std::move(result.texture_rgba); + volume.imported_texture_raw_filament_offsets = std::move(result.texture_raw_filament_offsets); + volume.imported_texture_width = result.texture_width; + volume.imported_texture_height = result.texture_height; + volume.imported_texture_raw_channels = result.texture_raw_channels; + volume.imported_texture_raw_metadata_json = std::move(result.texture_raw_metadata_json); + volume.uv_map_generator_version = result.uv_map_generator_version; + touch_imported_texture(volume); + break; + case SimplifyColorSource::VertexColors: + volume.texture_mapping_color_facets.reset(); + clear_image_texture(volume); + volume.imported_vertex_colors_rgba = std::move(result.vertex_colors_rgba); + volume.imported_vertex_colors_rgba.set_new_unique_id(); + touch_imported_texture(volume); + break; + case SimplifyColorSource::None: + volume.texture_mapping_color_facets.reset(); + clear_image_texture(volume); + volume.imported_vertex_colors_rgba.clear(); + volume.imported_vertex_colors_rgba.set_new_unique_id(); + touch_imported_texture(volume); + break; + } +} + +} // namespace Slic3r diff --git a/src/libslic3r/ModelTextureDataRemap.hpp b/src/libslic3r/ModelTextureDataRemap.hpp new file mode 100644 index 00000000000..e1a9e55bce1 --- /dev/null +++ b/src/libslic3r/ModelTextureDataRemap.hpp @@ -0,0 +1,75 @@ +// original author: sentientstardust + +#ifndef slic3r_ModelTextureDataRemap_hpp_ +#define slic3r_ModelTextureDataRemap_hpp_ + +#include "Model.hpp" +#include "TriangleMesh.hpp" + +#include +#include +#include +#include +#include + +namespace Slic3r { + +enum class SimplifyColorSource +{ + None, + RgbaData, + ImageTexture, + VertexColors +}; + +struct SimplifyTextureDataSnapshot +{ + SimplifyColorSource source { SimplifyColorSource::None }; + indexed_triangle_set source_mesh; + std::vector rgba_facets; + std::string rgba_metadata_json; + std::vector texture_rgba; + std::vector texture_uvs_per_face; + std::vector texture_uv_valid; + std::vector texture_raw_filament_offsets; + uint32_t texture_width { 0 }; + uint32_t texture_height { 0 }; + uint32_t texture_raw_channels { 0 }; + std::string texture_raw_metadata_json; + int uv_map_generator_version { 0 }; + std::vector vertex_colors_rgba; +}; + +struct SimplifyTextureDataResult +{ + SimplifyColorSource source { SimplifyColorSource::None }; + bool remap_failed { false }; + bool used_fallback_rgba { false }; + std::unique_ptr rgba_data; + std::vector texture_rgba; + std::vector texture_uvs_per_face; + std::vector texture_uv_valid; + std::vector texture_raw_filament_offsets; + uint32_t texture_width { 0 }; + uint32_t texture_height { 0 }; + uint32_t texture_raw_channels { 0 }; + std::string texture_raw_metadata_json; + int uv_map_generator_version { 0 }; + std::vector vertex_colors_rgba; +}; + +using SimplifyTextureCancelFn = std::function; +using SimplifyTextureProgressFn = std::function; + +SimplifyTextureDataSnapshot snapshot_simplify_texture_data(const ModelVolume &volume); + +SimplifyTextureDataResult remap_simplify_texture_data(const SimplifyTextureDataSnapshot &snapshot, + const indexed_triangle_set &simplified_mesh, + const SimplifyTextureCancelFn &throw_on_cancel = {}, + const SimplifyTextureProgressFn &status_fn = {}); + +void apply_simplify_texture_data_result(ModelVolume &volume, SimplifyTextureDataResult &&result); + +} // namespace Slic3r + +#endif diff --git a/src/libslic3r/TextureMapping.cpp b/src/libslic3r/TextureMapping.cpp index f8770f65e5c..f58fb133c29 100644 --- a/src/libslic3r/TextureMapping.cpp +++ b/src/libslic3r/TextureMapping.cpp @@ -1,3 +1,5 @@ +// original author: sentientstardust + #include "TextureMapping.hpp" #include "ColorSolver.hpp" diff --git a/src/libslic3r/TextureMapping.hpp b/src/libslic3r/TextureMapping.hpp index 0bd00f32320..a8d3121af53 100644 --- a/src/libslic3r/TextureMapping.hpp +++ b/src/libslic3r/TextureMapping.hpp @@ -1,3 +1,5 @@ +// original author: sentientstardust + #ifndef slic3r_TextureMapping_hpp_ #define slic3r_TextureMapping_hpp_ diff --git a/src/slic3r/GUI/Gizmos/GLGizmoSimplify.cpp b/src/slic3r/GUI/Gizmos/GLGizmoSimplify.cpp index 1f8af3bf096..fe8d1b09895 100644 --- a/src/slic3r/GUI/Gizmos/GLGizmoSimplify.cpp +++ b/src/slic3r/GUI/Gizmos/GLGizmoSimplify.cpp @@ -13,6 +13,9 @@ #include +#include +#include +#include #include namespace Slic3r::GUI { @@ -163,12 +166,16 @@ void GLGizmoSimplify::on_render_input_window(float x, float y, float bottom_limi bool is_cancelling = false; bool is_worker_running = false; bool is_result_ready = false; - int progress = 0; + bool skip_color_conversion_requested = false; + bool color_conversion_in_progress = false; + float progress = 0.f; { std::lock_guard lk(m_state_mutex); is_cancelling = m_state.status == State::cancelling; is_worker_running = m_state.status == State::running; is_result_ready = bool(m_state.result); + skip_color_conversion_requested = m_state.skip_color_conversion_requested; + color_conversion_in_progress = m_state.color_conversion_in_progress; progress = m_state.progress; } @@ -336,6 +343,21 @@ void GLGizmoSimplify::on_render_input_window(float x, float y, float bottom_limi m_imgui->disabled_end(); // use_count m_imgui->bbl_checkbox(_L("Show wireframe").c_str(), m_show_wireframe); + const wxString skip_color_conversion_label = color_conversion_in_progress ? + _L("Skip color conversion (in progress)") : + _L("Skip color conversion"); + m_imgui->disabled_begin(!is_worker_running || is_cancelling || skip_color_conversion_requested); + m_imgui->push_cancel_button_style(); + if (m_imgui->bbl_button(skip_color_conversion_label)) { + skip_color_conversion_request(); + } else if (ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled)) { + if (skip_color_conversion_requested) + ImGui::SetTooltip("%s", _u8L("Color conversion will be skipped.").c_str()); + else if (!is_worker_running) + ImGui::SetTooltip("%s", _u8L("Color conversion can only be skipped while processing.").c_str()); + } + m_imgui->pop_cancel_button_style(); + m_imgui->disabled_end(); // draw progress bar ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding,12); @@ -343,9 +365,15 @@ void GLGizmoSimplify::on_render_input_window(float x, float y, float bottom_limi ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing,ImVec2(10,20)); if (is_worker_running) { // apply or preview // draw progress bar - std::string progress_text = GUI::format("%1%", std::to_string(progress)) + "%%"; + char progress_buffer[32]; + const float displayed_progress = std::clamp(progress, 0.f, 100.f); + if (std::abs(displayed_progress - std::round(displayed_progress)) < 0.05f) + std::snprintf(progress_buffer, sizeof(progress_buffer), "%.0f%%", displayed_progress); + else + std::snprintf(progress_buffer, sizeof(progress_buffer), "%.1f%%", displayed_progress); + std::string progress_text = progress_buffer; ImVec2 progress_size(bottom_left_width - space_size, 0.0f); - ImGui::BBLProgressBar2(progress / 100., progress_size); + ImGui::BBLProgressBar2(displayed_progress / 100.f, progress_size); ImGui::SameLine(); ImGui::AlignTextToFramePadding(); ImGui::TextColored(ImVec4(0.42f, 0.42f, 0.42f, 1.00f), progress_text.c_str()); @@ -407,6 +435,16 @@ void GLGizmoSimplify::stop_worker_thread_request() m_state.status = State::Status::cancelling; } +void GLGizmoSimplify::skip_color_conversion_request() +{ + { + std::lock_guard lk(m_state_mutex); + if (m_state.status == State::running) + m_state.skip_color_conversion_requested = true; + } + request_rerender(true); +} + // Following is called from a UI thread when the worker terminates // worker calls it through a CallAfter. @@ -424,8 +462,8 @@ void GLGizmoSimplify::worker_finished() m_worker.join(); if (GLGizmoBase::m_state == Off) return; - if (m_state.result) - init_model(*m_state.result); + if (m_state.result && m_state.result->mesh) + init_model(*m_state.result->mesh); if (m_state.config != m_configuration || m_state.mv != m_volume) { // Settings were changed, restart the worker immediately. process(); @@ -471,13 +509,16 @@ void GLGizmoSimplify::process() m_state.config = m_configuration; m_state.mv = m_volume; m_state.status = State::running; + m_state.skip_color_conversion_requested = false; + m_state.color_conversion_in_progress = false; // Create a copy of current mesh to pass to the worker thread. // Using unique_ptr instead of pass-by-value to avoid an extra // copy (which would happen when passing to std::thread). + SimplifyTextureDataSnapshot texture_snapshot = snapshot_simplify_texture_data(*m_volume); auto its = std::make_unique(m_volume->mesh().its); - m_worker = std::thread([this](std::unique_ptr its) { + m_worker = std::thread([this](std::unique_ptr its, SimplifyTextureDataSnapshot texture_snapshot) { // Checks that the UI thread did not request cancellation, throws if so. std::function throw_on_cancel = [this]() { @@ -488,11 +529,24 @@ void GLGizmoSimplify::process() // Called by worker thread, updates progress bar. // Using CallAfter so the rerequest function is run in UI thread. - std::function statusfn = [this](int percent) { + std::function statusfn = [this](float percent) { std::lock_guard lk(m_state_mutex); - m_state.progress = percent; + m_state.progress = std::clamp(percent, 0.f, 100.f); call_after_if_active([this]() { request_rerender(); }); }; + auto set_color_conversion_in_progress = [this](bool in_progress) { + { + std::lock_guard lk(m_state_mutex); + m_state.color_conversion_in_progress = in_progress; + } + call_after_if_active([this]() { request_rerender(); }); + }; + auto throw_on_color_conversion_stop = [this, &throw_on_cancel]() { + throw_on_cancel(); + std::lock_guard lk(m_state_mutex); + if (m_state.skip_color_conversion_requested) + throw SimplifyColorConversionSkippedException(); + }; // Initialize. uint32_t triangle_count = 0; @@ -503,33 +557,64 @@ void GLGizmoSimplify::process() triangle_count = m_state.config.wanted_count; if (! m_state.config.use_count) max_error = m_state.config.max_error; - m_state.progress = 0; + m_state.progress = 0.f; m_state.result.reset(); + m_state.color_conversion_in_progress = false; m_state.status = State::Status::running; } // Start the actual calculation. try { - its_quadric_edge_collapse(*its, triangle_count, &max_error, throw_on_cancel, statusfn); + auto decimate_statusfn = [&statusfn](int percent) { + statusfn(float(std::clamp(percent, 0, 100)) * 0.9f); + }; + its_quadric_edge_collapse(*its, triangle_count, &max_error, throw_on_cancel, decimate_statusfn); + statusfn(90); + SimplifyTextureDataResult texture_result; + set_color_conversion_in_progress(true); + try { + throw_on_color_conversion_stop(); + texture_result = + remap_simplify_texture_data(texture_snapshot, *its, throw_on_color_conversion_stop, [&statusfn](int percent) { + statusfn(90.f + float(std::clamp(percent, 0, 100)) * 0.1f); + }); + } catch (SimplifyColorConversionSkippedException &) { + texture_result = SimplifyTextureDataResult(); + } catch (...) { + set_color_conversion_in_progress(false); + throw; + } + set_color_conversion_in_progress(false); + statusfn(100); + auto result = std::make_unique(); + result->mesh = std::move(its); + result->texture_data = std::move(texture_result); + + std::lock_guard lk(m_state_mutex); + if (m_state.status == State::Status::running) { + m_state.status = State::Status::idle; + m_state.color_conversion_in_progress = false; + m_state.result = std::move(result); + } else if (m_state.status == State::Status::cancelling) { + m_state.color_conversion_in_progress = false; + m_state.status = State::Status::idle; + } } catch (SimplifyCanceledException &) { std::lock_guard lk(m_state_mutex); + m_state.color_conversion_in_progress = false; m_state.status = State::idle; } - std::lock_guard lk(m_state_mutex); - if (m_state.status == State::Status::running) { - // We were not cancelled, the result is valid. - m_state.status = State::Status::idle; - m_state.result = std::move(its); - } - // Update UI. Use CallAfter so the function is run on UI thread. call_after_if_active([this]() { worker_finished(); }); - }, std::move(its)); + }, std::move(its), std::move(texture_snapshot)); } void GLGizmoSimplify::apply_simplify() { + if (!m_state.result || !m_state.result->mesh) + return; + const Selection& selection = m_parent.get_selection(); int object_idx = selection.get_object_idx(); @@ -540,7 +625,10 @@ void GLGizmoSimplify::apply_simplify() { ModelVolume* mv = get_model_volume(selection, wxGetApp().model()); assert(mv == m_volume); - mv->set_mesh(std::move(*m_state.result)); + const bool remap_failed = m_state.result->texture_data.remap_failed; + const bool used_fallback_rgba = m_state.result->texture_data.used_fallback_rgba; + mv->set_mesh(std::move(*m_state.result->mesh)); + apply_simplify_texture_data_result(*mv, std::move(m_state.result->texture_data)); m_state.result.reset(); mv->calculate_convex_hull(); mv->invalidate_convex_hull_2d(); @@ -550,6 +638,17 @@ void GLGizmoSimplify::apply_simplify() { // fix hollowing, sla support points, modifiers, ... plater->changed_mesh(object_idx); + if (used_fallback_rgba) { + plater->get_notification_manager()->push_notification( + NotificationType::CustomNotification, + NotificationManager::NotificationLevel::PrintInfoNotificationLevel, + _u8L("Image texture UVs could not be regenerated after simplification; texture colors were preserved as RGBA data.")); + } else if (remap_failed) { + plater->get_notification_manager()->push_notification( + NotificationType::CustomNotification, + NotificationManager::NotificationLevel::PrintInfoNotificationLevel, + _u8L("Image texture UVs could not be regenerated after simplification; stale texture data was cleared.")); + } // Fix warning icon in object list wxGetApp().obj_list()->update_item_error_icon(object_idx, -1); close(); diff --git a/src/slic3r/GUI/Gizmos/GLGizmoSimplify.hpp b/src/slic3r/GUI/Gizmos/GLGizmoSimplify.hpp index b4be94d2c94..2cdc84dc66d 100644 --- a/src/slic3r/GUI/Gizmos/GLGizmoSimplify.hpp +++ b/src/slic3r/GUI/Gizmos/GLGizmoSimplify.hpp @@ -5,6 +5,7 @@ // which overrides our localization "L" macro. #include "GLGizmoBase.hpp" #include "slic3r/GUI/3DScene.hpp" +#include "libslic3r/ModelTextureDataRemap.hpp" #include "admesh/stl.h" // indexed_triangle_set #include #include @@ -46,6 +47,7 @@ private: void process(); void stop_worker_thread_request(); + void skip_color_conversion_request(); void worker_finished(); void create_gui_cfg(); @@ -86,6 +88,12 @@ private: // Following struct is accessed by both UI and worker thread. // Accesses protected by a mutex. + struct SimplifyResult + { + std::unique_ptr mesh; + SimplifyTextureDataResult texture_data; + }; + struct State { enum Status { idle, @@ -94,10 +102,12 @@ private: }; Status status = idle; - int progress = 0; // percent of done work + float progress = 0.f; // percent of done work Configuration config; // Configuration we started with. const ModelVolume* mv = nullptr; - std::unique_ptr result; + bool skip_color_conversion_requested = false; + bool color_conversion_in_progress = false; + std::unique_ptr result; }; std::thread m_worker; @@ -141,6 +151,15 @@ private: return L("Model simplification has been canceled"); } }; + + class SimplifyColorConversionSkippedException: public std::exception + { + public: + const char *what() const throw() + { + return L("Color conversion has been skipped"); + } + }; }; } // namespace GUI