From 6222bd0f232748b1fae8601a24685de2dd243cf8 Mon Sep 17 00:00:00 2001 From: sentientstardust Date: Sat, 2 May 2026 13:41:24 +0100 Subject: [PATCH] c764fd19c New color data format for 3d models, full color painting, image projection to model surface --- resources/images/image_projection.svg | 61 + resources/images/image_projection_dark.svg | 61 + resources/images/true_color_painting.svg | 89 + resources/images/true_color_painting_dark.svg | 82 + src/libslic3r/Format/3mf.cpp | 19 + src/libslic3r/Format/bbs_3mf.cpp | 34 +- src/libslic3r/GCode.cpp | 34 + src/libslic3r/Model.cpp | 446 +- src/libslic3r/Model.hpp | 151 +- src/libslic3r/PrintApply.cpp | 3 + src/libslic3r/PrintObjectSlice.cpp | 7 +- src/slic3r/GUI/3DScene.cpp | 5 +- src/slic3r/GUI/CreatePresetsDialog.cpp | 33 +- src/slic3r/GUI/GLCanvas3D.cpp | 28 +- src/slic3r/GUI/GLCanvas3D.hpp | 2 +- src/slic3r/GUI/GUI_ObjectList.cpp | 4 +- .../GUI/Gizmos/GLGizmoMmuSegmentation.cpp | 4131 +++++++++++++++-- .../GUI/Gizmos/GLGizmoMmuSegmentation.hpp | 205 +- src/slic3r/GUI/Gizmos/GLGizmoPainterBase.cpp | 87 +- src/slic3r/GUI/Gizmos/GLGizmoPainterBase.hpp | 8 + src/slic3r/GUI/Gizmos/GLGizmosManager.cpp | 29 +- src/slic3r/GUI/Gizmos/GLGizmosManager.hpp | 2 + src/slic3r/GUI/KBShortcutsDialog.cpp | 4 +- src/slic3r/GUI/MMUPaintedTexturePreview.cpp | 229 +- src/slic3r/GUI/MMUPaintedTexturePreview.hpp | 7 +- src/slic3r/GUI/NotificationManager.cpp | 6 +- src/slic3r/GUI/ObjectDataViewModel.cpp | 3 +- src/slic3r/GUI/Plater.cpp | 2 +- 28 files changed, 5260 insertions(+), 512 deletions(-) create mode 100644 resources/images/image_projection.svg create mode 100644 resources/images/image_projection_dark.svg create mode 100644 resources/images/true_color_painting.svg create mode 100644 resources/images/true_color_painting_dark.svg diff --git a/resources/images/image_projection.svg b/resources/images/image_projection.svg new file mode 100644 index 0000000000..cfafff833b --- /dev/null +++ b/resources/images/image_projection.svg @@ -0,0 +1,61 @@ + + + + + + + + + + diff --git a/resources/images/image_projection_dark.svg b/resources/images/image_projection_dark.svg new file mode 100644 index 0000000000..f27861f634 --- /dev/null +++ b/resources/images/image_projection_dark.svg @@ -0,0 +1,61 @@ + + + + + + + + + + diff --git a/resources/images/true_color_painting.svg b/resources/images/true_color_painting.svg new file mode 100644 index 0000000000..6625390ae3 --- /dev/null +++ b/resources/images/true_color_painting.svg @@ -0,0 +1,89 @@ + + + + + + + + + + + + + + + + diff --git a/resources/images/true_color_painting_dark.svg b/resources/images/true_color_painting_dark.svg new file mode 100644 index 0000000000..ae36df454e --- /dev/null +++ b/resources/images/true_color_painting_dark.svg @@ -0,0 +1,82 @@ + + + + + + + + + + + + + + diff --git a/src/libslic3r/Format/3mf.cpp b/src/libslic3r/Format/3mf.cpp index 812e9f0154..5ff82ce17f 100644 --- a/src/libslic3r/Format/3mf.cpp +++ b/src/libslic3r/Format/3mf.cpp @@ -112,6 +112,7 @@ static constexpr const char* INSTANCESCOUNT_ATTR = "instances_count"; static constexpr const char* CUSTOM_SUPPORTS_ATTR = "slic3rpe:custom_supports"; static constexpr const char* CUSTOM_SEAM_ATTR = "slic3rpe:custom_seam"; static constexpr const char* MMU_SEGMENTATION_ATTR = "slic3rpe:mmu_segmentation"; +static constexpr const char* TEXTURE_MAPPING_COLOR_ATTR = "slic3rpe:texture_mapping_color"; static constexpr const char* FUZZY_SKIN_ATTR = "slic3rpe:fuzzy_skin"; static constexpr const char* KEY_ATTR = "key"; @@ -418,6 +419,7 @@ ModelVolumeType type_from_string(const std::string &s) std::vector custom_supports; std::vector custom_seam; std::vector mmu_segmentation; + std::vector texture_mapping_color; std::vector fuzzy_skin; bool empty() { return vertices.empty() || triangles.empty(); } @@ -428,6 +430,7 @@ ModelVolumeType type_from_string(const std::string &s) custom_supports.clear(); custom_seam.clear(); mmu_segmentation.clear(); + texture_mapping_color.clear(); fuzzy_skin.clear(); } }; @@ -1744,6 +1747,8 @@ ModelVolumeType type_from_string(const std::string &s) m_curr_object.geometry.custom_seam.push_back(get_attribute_value_string(attributes, num_attributes, CUSTOM_SEAM_ATTR)); m_curr_object.geometry.fuzzy_skin.push_back(get_attribute_value_string(attributes, num_attributes, FUZZY_SKIN_ATTR)); m_curr_object.geometry.mmu_segmentation.push_back(get_attribute_value_string(attributes, num_attributes, MMU_SEGMENTATION_ATTR)); + m_curr_object.geometry.texture_mapping_color.push_back( + get_attribute_value_string(attributes, num_attributes, TEXTURE_MAPPING_COLOR_ATTR)); return true; } @@ -2160,24 +2165,29 @@ ModelVolumeType type_from_string(const std::string &s) volume->supported_facets.reserve(triangles_count); volume->seam_facets.reserve(triangles_count); volume->mmu_segmentation_facets.reserve(triangles_count); + volume->texture_mapping_color_facets.reserve(triangles_count); volume->fuzzy_skin_facets.reserve(triangles_count); for (size_t i=0; isupported_facets.set_triangle_from_string(i, geometry.custom_supports[index]); if (! geometry.custom_seam[index].empty()) volume->seam_facets.set_triangle_from_string(i, geometry.custom_seam[index]); if (! geometry.mmu_segmentation[index].empty()) volume->mmu_segmentation_facets.set_triangle_from_string(i, geometry.mmu_segmentation[index]); + if (! geometry.texture_mapping_color[index].empty()) + volume->texture_mapping_color_facets.set_triangle_from_string(i, geometry.texture_mapping_color[index]); if (! geometry.fuzzy_skin[index].empty()) volume->fuzzy_skin_facets.set_triangle_from_string(i, geometry.fuzzy_skin[index]); } volume->supported_facets.shrink_to_fit(); volume->seam_facets.shrink_to_fit(); volume->mmu_segmentation_facets.shrink_to_fit(); + volume->texture_mapping_color_facets.shrink_to_fit(); volume->fuzzy_skin_facets.shrink_to_fit(); // apply the remaining volume's metadata @@ -2832,6 +2842,15 @@ ModelVolumeType type_from_string(const std::string &s) output_buffer += "\""; } + std::string texture_mapping_color_data_string = volume->texture_mapping_color_facets.get_triangle_as_string(i); + if (! texture_mapping_color_data_string.empty()) { + output_buffer += " "; + output_buffer += TEXTURE_MAPPING_COLOR_ATTR; + output_buffer += "=\""; + output_buffer += texture_mapping_color_data_string; + output_buffer += "\""; + } + std::string fuzzy_skin_data_string = volume->fuzzy_skin_facets.get_triangle_as_string(i); if (!fuzzy_skin_data_string.empty()) { output_buffer += " "; diff --git a/src/libslic3r/Format/bbs_3mf.cpp b/src/libslic3r/Format/bbs_3mf.cpp index e80463e745..0fa39faa37 100644 --- a/src/libslic3r/Format/bbs_3mf.cpp +++ b/src/libslic3r/Format/bbs_3mf.cpp @@ -357,6 +357,7 @@ static constexpr const char* CUSTOM_SUPPORTS_ATTR = "paint_supports"; static constexpr const char* CUSTOM_FUZZY_SKIN_ATTR = "paint_fuzzy_skin"; static constexpr const char* CUSTOM_SEAM_ATTR = "paint_seam"; static constexpr const char* MMU_SEGMENTATION_ATTR = "paint_color"; +static constexpr const char* TEXTURE_MAPPING_COLOR_ATTR = "texture_mapping_color"; // BBS static constexpr const char* FACE_PROPERTY_ATTR = "face_property"; @@ -940,8 +941,9 @@ static bool decode_jpeg_rgba_from_memory(const std::vector &encoded, while (cinfo.output_scanline < cinfo.output_height) { jpeg_read_scanlines(&cinfo, scanline, 1); const unsigned char *src = scanline[0]; + const uint32_t dst_y = height - 1 - y; for (uint32_t x = 0; x < width; ++x) { - const size_t dst = (size_t(y) * size_t(width) + size_t(x)) * 4; + const size_t dst = (size_t(dst_y) * size_t(width) + size_t(x)) * 4; if (components >= 3) { const size_t s = size_t(x) * size_t(components); out_rgba[dst + 0] = src[s + 0]; @@ -1274,6 +1276,7 @@ static void append_triangle_material_data(std::vector &uv_valid, std::vector custom_supports; std::vector custom_seam; std::vector mmu_segmentation; + std::vector texture_mapping_color; std::vector fuzzy_skin; // BBS std::vector face_properties; @@ -1293,6 +1296,9 @@ static void append_triangle_material_data(std::vector &uv_valid, std::swap(triangles, o.triangles); std::swap(custom_supports, o.custom_supports); std::swap(custom_seam, o.custom_seam); + std::swap(mmu_segmentation, o.mmu_segmentation); + std::swap(texture_mapping_color, o.texture_mapping_color); + std::swap(fuzzy_skin, o.fuzzy_skin); std::swap(face_properties, o.face_properties); std::swap(texture_uvs_per_face, o.texture_uvs_per_face); std::swap(texture_uv_valid, o.texture_uv_valid); @@ -1309,6 +1315,7 @@ static void append_triangle_material_data(std::vector &uv_valid, custom_supports.clear(); custom_seam.clear(); mmu_segmentation.clear(); + texture_mapping_color.clear(); fuzzy_skin.clear(); face_properties.clear(); texture_uvs_per_face.clear(); @@ -4472,6 +4479,8 @@ static void append_triangle_material_data(std::vector &uv_valid, m_curr_object->geometry.custom_supports.push_back(bbs_get_attribute_value_string(attributes, num_attributes, CUSTOM_SUPPORTS_ATTR)); m_curr_object->geometry.custom_seam.push_back(bbs_get_attribute_value_string(attributes, num_attributes, CUSTOM_SEAM_ATTR)); m_curr_object->geometry.mmu_segmentation.push_back(bbs_get_attribute_value_string(attributes, num_attributes, MMU_SEGMENTATION_ATTR)); + m_curr_object->geometry.texture_mapping_color.push_back( + bbs_get_attribute_value_string(attributes, num_attributes, TEXTURE_MAPPING_COLOR_ATTR)); m_curr_object->geometry.fuzzy_skin.push_back(bbs_get_attribute_value_string(attributes, num_attributes, CUSTOM_FUZZY_SKIN_ATTR)); // BBS m_curr_object->geometry.face_properties.push_back(bbs_get_attribute_value_string(attributes, num_attributes, FACE_PROPERTY_ATTR)); @@ -5717,11 +5726,13 @@ static void append_triangle_material_data(std::vector &uv_valid, volume->supported_facets.reserve(triangles_count); volume->seam_facets.reserve(triangles_count); volume->mmu_segmentation_facets.reserve(triangles_count); + volume->texture_mapping_color_facets.reserve(triangles_count); volume->fuzzy_skin_facets.reserve(triangles_count); for (size_t i=0; igeometry.custom_supports.size()); assert(i < sub_object->geometry.custom_seam.size()); assert(i < sub_object->geometry.mmu_segmentation.size()); + assert(i < sub_object->geometry.texture_mapping_color.size()); assert(i < sub_object->geometry.fuzzy_skin.size()); if (! sub_object->geometry.custom_supports[i].empty()) volume->supported_facets.set_triangle_from_string(i, sub_object->geometry.custom_supports[i]); @@ -5729,6 +5740,8 @@ static void append_triangle_material_data(std::vector &uv_valid, volume->seam_facets.set_triangle_from_string(i, sub_object->geometry.custom_seam[i]); if (! sub_object->geometry.mmu_segmentation[i].empty()) volume->mmu_segmentation_facets.set_triangle_from_string(i, sub_object->geometry.mmu_segmentation[i]); + if (!sub_object->geometry.texture_mapping_color[i].empty()) + volume->texture_mapping_color_facets.set_triangle_from_string(i, sub_object->geometry.texture_mapping_color[i]); if (!sub_object->geometry.fuzzy_skin[i].empty()) volume->fuzzy_skin_facets.set_triangle_from_string(i, sub_object->geometry.fuzzy_skin[i]); } @@ -5736,6 +5749,8 @@ static void append_triangle_material_data(std::vector &uv_valid, volume->seam_facets.shrink_to_fit(); volume->mmu_segmentation_facets.shrink_to_fit(); volume->mmu_segmentation_facets.touch(); + volume->texture_mapping_color_facets.shrink_to_fit(); + volume->texture_mapping_color_facets.touch(); volume->fuzzy_skin_facets.shrink_to_fit(); volume->fuzzy_skin_facets.touch(); } @@ -5927,21 +5942,26 @@ static void append_triangle_material_data(std::vector &uv_valid, volume->supported_facets.reserve(triangles_count); volume->seam_facets.reserve(triangles_count); volume->mmu_segmentation_facets.reserve(triangles_count); + volume->texture_mapping_color_facets.reserve(triangles_count); for (size_t i=0; isupported_facets.set_triangle_from_string(i, geometry.custom_supports[index]); if (! geometry.custom_seam[index].empty()) volume->seam_facets.set_triangle_from_string(i, geometry.custom_seam[index]); if (! geometry.mmu_segmentation[index].empty()) volume->mmu_segmentation_facets.set_triangle_from_string(i, geometry.mmu_segmentation[index]); + if (! geometry.texture_mapping_color[index].empty()) + volume->texture_mapping_color_facets.set_triangle_from_string(i, geometry.texture_mapping_color[index]); } volume->supported_facets.shrink_to_fit(); volume->seam_facets.shrink_to_fit(); volume->mmu_segmentation_facets.shrink_to_fit(); + volume->texture_mapping_color_facets.shrink_to_fit(); volume->set_type(volume_data.part_type); @@ -6339,6 +6359,8 @@ static void append_triangle_material_data(std::vector &uv_valid, current_object->geometry.custom_supports.push_back(bbs_get_attribute_value_string(attributes, num_attributes, CUSTOM_SUPPORTS_ATTR)); current_object->geometry.custom_seam.push_back(bbs_get_attribute_value_string(attributes, num_attributes, CUSTOM_SEAM_ATTR)); current_object->geometry.mmu_segmentation.push_back(bbs_get_attribute_value_string(attributes, num_attributes, MMU_SEGMENTATION_ATTR)); + current_object->geometry.texture_mapping_color.push_back( + bbs_get_attribute_value_string(attributes, num_attributes, TEXTURE_MAPPING_COLOR_ATTR)); current_object->geometry.fuzzy_skin.push_back(bbs_get_attribute_value_string(attributes, num_attributes, CUSTOM_FUZZY_SKIN_ATTR)); // BBS current_object->geometry.face_properties.push_back(bbs_get_attribute_value_string(attributes, num_attributes, FACE_PROPERTY_ATTR)); @@ -7814,6 +7836,7 @@ static void append_triangle_material_data(std::vector &uv_valid, if ((shared_volume->supported_facets.equals(volume->supported_facets)) && (shared_volume->seam_facets.equals(volume->seam_facets)) && (shared_volume->mmu_segmentation_facets.equals(volume->mmu_segmentation_facets)) + && (shared_volume->texture_mapping_color_facets.equals(volume->texture_mapping_color_facets)) && (shared_volume->fuzzy_skin_facets.equals(volume->fuzzy_skin_facets)) && (shared_volume->imported_vertex_colors_rgba == volume->imported_vertex_colors_rgba) && (shared_volume->imported_texture_uvs_per_face == volume->imported_texture_uvs_per_face) @@ -8457,6 +8480,15 @@ static void append_triangle_material_data(std::vector &uv_valid, output_buffer += "\""; } + std::string texture_mapping_color_data_string = volume->texture_mapping_color_facets.get_triangle_as_string(i); + if (! texture_mapping_color_data_string.empty()) { + output_buffer += " "; + output_buffer += TEXTURE_MAPPING_COLOR_ATTR; + output_buffer += "=\""; + output_buffer += texture_mapping_color_data_string; + output_buffer += "\""; + } + std::string fuzzy_skin_painting_data_string = volume->fuzzy_skin_facets.get_triangle_as_string(i); if (!fuzzy_skin_painting_data_string.empty()) { output_buffer += " "; diff --git a/src/libslic3r/GCode.cpp b/src/libslic3r/GCode.cpp index 3ae97a8fcd..885f76b89e 100644 --- a/src/libslic3r/GCode.cpp +++ b/src/libslic3r/GCode.cpp @@ -6559,6 +6559,40 @@ static VertexColorOverhangWeightField build_vertex_color_weight_field_for_gcode( const indexed_triangle_set &its = mesh_ptr->its; const Transform3d volume_trafo = object_trafo * volume->get_matrix(); + if (!volume->texture_mapping_color_facets.empty()) { + std::vector color_facets; + volume->texture_mapping_color_facets.get_facet_triangles(*volume, color_facets); + for (const ColorFacetTriangle &facet : color_facets) { + const Vec3d p0 = volume_trafo * facet.vertices[0].cast(); + const Vec3d p1 = volume_trafo * facet.vertices[1].cast(); + const Vec3d p2 = volume_trafo * facet.vertices[2].cast(); + if (!p0.allFinite() || !p1.allFinite() || !p2.allFinite()) + continue; + + const Vec3d world_pos = (p0 + p1 + p2) / 3.0; + std::array rgba = unpack_rgba_u32(facet.rgba); + rgba[3] = 1.f; + + const double tri_area_mm2 = 0.5 * ((p1 - p0).cross(p2 - p0)).norm(); + if (!std::isfinite(tri_area_mm2)) + continue; + float sample_weight = std::max(0.05f, float(tri_area_mm2)); + if (use_layer_weighting) { + const float dz = std::abs(float(world_pos.z()) - layer_z_mm); + const float z_norm = dz / safe_layer_z_falloff_mm; + const float z_weight = std::exp(-0.5f * z_norm * z_norm); + if (!std::isfinite(z_weight)) + continue; + sample_weight *= z_weight; + } + if (sample_weight <= EPSILON) + continue; + + accumulate_sample(float(world_pos.x()), float(world_pos.y()), rgba, sample_weight); + } + continue; + } + bool sampled_from_uv_texture = false; const bool has_uv_texture = !volume->imported_texture_rgba.empty() && diff --git a/src/libslic3r/Model.cpp b/src/libslic3r/Model.cpp index 5a43ded8b5..09c8133929 100644 --- a/src/libslic3r/Model.cpp +++ b/src/libslic3r/Model.cpp @@ -284,8 +284,9 @@ static bool decode_jpeg_texture_rgba(const std::string &texture_path, while (cinfo.output_scanline < cinfo.output_height) { jpeg_read_scanlines(&cinfo, scanline, 1); const unsigned char *src = scanline[0]; + const uint32_t dst_y = height - 1 - y; for (uint32_t x = 0; x < width; ++x) { - const size_t dst = (size_t(y) * size_t(width) + size_t(x)) * 4; + const size_t dst = (size_t(dst_y) * size_t(width) + size_t(x)) * 4; if (components >= 3) { const size_t s = size_t(x) * size_t(components); out_rgba[dst + 0] = src[s + 0]; @@ -2425,6 +2426,7 @@ void ModelObject::convert_units(ModelObjectPtrs& new_objects, ConversionType con vol->supported_facets.assign(volume->supported_facets); vol->seam_facets.assign(volume->seam_facets); vol->mmu_segmentation_facets.assign(volume->mmu_segmentation_facets); + vol->texture_mapping_color_facets.assign(volume->texture_mapping_color_facets); vol->fuzzy_skin_facets.assign(volume->fuzzy_skin_facets); // Perform conversion only if the target "imperial" state is different from the current one. @@ -2537,6 +2539,7 @@ void ModelVolume::reset_extra_facets() this->supported_facets.reset(); this->seam_facets.reset(); this->mmu_segmentation_facets.reset(); + this->texture_mapping_color_facets.reset(); this->fuzzy_skin_facets.reset(); } @@ -2642,6 +2645,9 @@ void ModelObject::split(ModelObjectPtrs* new_objects) if (new_vol->mmu_segmentation_facets.timestamp() == volume->mmu_segmentation_facets.timestamp()) new_vol->mmu_segmentation_facets.reset(); // BBS: let next assign take effect new_vol->mmu_segmentation_facets.assign(volume->mmu_segmentation_facets); + if (new_vol->texture_mapping_color_facets.timestamp() == volume->texture_mapping_color_facets.timestamp()) + new_vol->texture_mapping_color_facets.reset(); + new_vol->texture_mapping_color_facets.assign(volume->texture_mapping_color_facets); } // BBS: clear volume's config, as we already set them into object @@ -3275,6 +3281,7 @@ size_t ModelVolume::split(unsigned int max_extruders) // BBS: reset facet annotations this->mmu_segmentation_facets.reset(); + this->texture_mapping_color_facets.reset(); this->exterior_facets.reset(); this->supported_facets.reset(); this->seam_facets.reset(); @@ -3339,6 +3346,7 @@ void ModelVolume::assign_new_unique_ids_recursive() supported_facets.set_new_unique_id(); seam_facets.set_new_unique_id(); mmu_segmentation_facets.set_new_unique_id(); + texture_mapping_color_facets.set_new_unique_id(); fuzzy_skin_facets.set_new_unique_id(); } @@ -4109,6 +4117,429 @@ bool FacetsAnnotation::equals(const FacetsAnnotation &other) const return (m_data == data); } +static char color_facets_hex_digit(unsigned int value) +{ + value &= 0x0Fu; + return value < 10u ? char('0' + value) : char('A' + value - 10u); +} + +static int color_facets_hex_value(char ch) +{ + if (ch >= '0' && ch <= '9') + return int(ch - '0'); + if (ch >= 'A' && ch <= 'F') + return 10 + int(ch - 'A'); + if (ch >= 'a' && ch <= 'f') + return 10 + int(ch - 'a'); + return -1; +} + +static void color_facets_append_nibble(std::vector &bits, unsigned int code) +{ + for (int bit = 0; bit < 4; ++bit) + bits.emplace_back((code & (1u << bit)) != 0u); +} + +static std::string color_facets_bits_to_hex(const std::vector &bits, int offset, int end) +{ + std::string out; + while (offset < end) { + int next_code = 0; + for (int bit = 3; bit >= 0; --bit) { + next_code <<= 1; + next_code |= int(bits[size_t(offset + bit)]); + } + offset += 4; + out.insert(out.begin(), color_facets_hex_digit(unsigned(next_code))); + } + return out; +} + +static void color_facets_append_rgba_hex(std::string &out, uint32_t rgba) +{ + for (int shift = 28; shift >= 0; shift -= 4) + out.push_back(color_facets_hex_digit((rgba >> shift) & 0x0Fu)); +} + +static bool color_facets_parse_rgba_hex(const std::string &str, size_t offset, uint32_t &rgba) +{ + if (offset + 8 > str.size()) + return false; + uint32_t value = 0; + for (size_t idx = 0; idx < 8; ++idx) { + const int hex = color_facets_hex_value(str[offset + idx]); + if (hex < 0) + return false; + value = (value << 4) | uint32_t(hex); + } + rgba = value; + return true; +} + +static void color_facets_append_byte_hex(std::string &out, unsigned char value) +{ + out.push_back(color_facets_hex_digit((value >> 4) & 0x0Fu)); + out.push_back(color_facets_hex_digit(value & 0x0Fu)); +} + +static std::string color_facets_string_to_hex(const std::string &str) +{ + std::string out; + out.reserve(str.size() * 2); + for (unsigned char ch : str) + color_facets_append_byte_hex(out, ch); + return out; +} + +static std::string color_facets_hex_to_string(const std::string &str) +{ + std::string out; + if (str.size() % 2 != 0) + return out; + + out.reserve(str.size() / 2); + for (size_t idx = 0; idx + 1 < str.size(); idx += 2) { + const int high = color_facets_hex_value(str[idx]); + const int low = color_facets_hex_value(str[idx + 1]); + if (high < 0 || low < 0) + return {}; + out.push_back(char((high << 4) | low)); + } + return out; +} + +static ColorRGBA color_facets_unpack_rgba(uint32_t rgba) +{ + return ColorRGBA(float((rgba >> 24) & 0xFFu) / 255.f, + float((rgba >> 16) & 0xFFu) / 255.f, + float((rgba >> 8) & 0xFFu) / 255.f, + float(rgba & 0xFFu) / 255.f); +} + +static float color_facets_delta(uint32_t a, uint32_t b) +{ + const ColorRGBA ca = color_facets_unpack_rgba(a); + const ColorRGBA cb = color_facets_unpack_rgba(b); + return std::max({ std::abs(ca.r() - cb.r()), + std::abs(ca.g() - cb.g()), + std::abs(ca.b() - cb.b()), + std::abs(ca.a() - cb.a()) }); +} + +static std::array, 4> color_facets_split_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; +} + +static void color_facets_append_sampled_triangle(TriangleColorSplittingData &data, + const TextureMappingColorSampler &sampler, + size_t source_triangle, + const std::array &vertices, + const std::array &barycentrics, + int depth, + int min_depth, + int max_depth, + float split_color_threshold) +{ + const Vec3f centroid = (vertices[0] + vertices[1] + vertices[2]) / 3.f; + const Vec3f centroid_bary = (barycentrics[0] + barycentrics[1] + barycentrics[2]) / 3.f; + const uint32_t c0 = sampler(source_triangle, vertices[0], barycentrics[0]); + const uint32_t c1 = sampler(source_triangle, vertices[1], barycentrics[1]); + const uint32_t c2 = sampler(source_triangle, vertices[2], barycentrics[2]); + const uint32_t cc = sampler(source_triangle, centroid, centroid_bary); + const float max_delta = std::max({ color_facets_delta(c0, c1), + color_facets_delta(c1, c2), + color_facets_delta(c2, c0), + color_facets_delta(c0, cc), + color_facets_delta(c1, cc), + color_facets_delta(c2, cc) }); + + if (depth < max_depth && (depth < min_depth || max_delta > split_color_threshold)) { + color_facets_append_nibble(data.bitstream, 3u); + const std::array, 4> child_vertices = color_facets_split_triangle(vertices, 3, 0); + const std::array, 4> child_barycentrics = color_facets_split_triangle(barycentrics, 3, 0); + for (int child_idx = 3; child_idx >= 0; --child_idx) { + color_facets_append_sampled_triangle(data, + sampler, + source_triangle, + child_vertices[size_t(child_idx)], + child_barycentrics[size_t(child_idx)], + depth + 1, + min_depth, + max_depth, + split_color_threshold); + } + return; + } + + color_facets_append_nibble(data.bitstream, 0u); + data.colors_rgba.emplace_back(cc); +} + +static void color_facets_extract_triangle(const TriangleColorSplittingData &data, + int bitstream_end, + size_t color_end, + int &bit_idx, + size_t &color_idx, + int source_triangle, + const std::array &vertices, + std::vector &facets) +{ + if (bit_idx + 3 >= bitstream_end) + return; + + int code = 0; + for (int bit = 0; bit < 4; ++bit) + code |= int(data.bitstream[size_t(bit_idx++)]) << bit; + + const int split_sides = code & 0b11; + if (split_sides == 0) { + if (color_idx >= color_end || color_idx >= data.colors_rgba.size()) + return; + ColorFacetTriangle facet; + facet.vertices = vertices; + facet.source_triangle = source_triangle; + facet.rgba = data.colors_rgba[color_idx++]; + facets.emplace_back(facet); + return; + } + + const int special_side = (code >> 2) & 0b11; + const std::array, 4> children = color_facets_split_triangle(vertices, split_sides, special_side); + for (int child_idx = split_sides; child_idx >= 0; --child_idx) + color_facets_extract_triangle(data, + bitstream_end, + color_end, + bit_idx, + color_idx, + source_triangle, + children[size_t(child_idx)], + facets); +} + +void ColorFacetsAnnotation::reset() +{ + m_data.triangles_to_split.clear(); + m_data.bitstream.clear(); + m_data.colors_rgba.clear(); + m_data.metadata_json.clear(); + this->touch(); +} + +void ColorFacetsAnnotation::set_metadata_json(std::string metadata_json) +{ + if (m_data.metadata_json != metadata_json) { + m_data.metadata_json = std::move(metadata_json); + this->touch(); + } +} + +bool ColorFacetsAnnotation::equals(const ColorFacetsAnnotation &other) const +{ + return m_data == other.get_data(); +} + +std::string ColorFacetsAnnotation::get_triangle_as_string(int triangle_idx) const +{ + std::string out; + auto triangle_it = std::lower_bound(m_data.triangles_to_split.begin(), + m_data.triangles_to_split.end(), + triangle_idx, + [](const ColorTriangleBitStreamMapping &lhs, int rhs) { + return lhs.triangle_idx < rhs; + }); + if (triangle_it == m_data.triangles_to_split.end() || triangle_it->triangle_idx != triangle_idx) + return out; + + const auto next_it = std::next(triangle_it); + const int bitstream_end = next_it == m_data.triangles_to_split.end() ? int(m_data.bitstream.size()) : next_it->bitstream_start_idx; + const size_t color_end = next_it == m_data.triangles_to_split.end() ? m_data.colors_rgba.size() : size_t(next_it->color_start_idx); + if (triangle_it->bitstream_start_idx < 0 || + triangle_it->bitstream_start_idx >= bitstream_end || + triangle_it->color_start_idx < 0 || + size_t(triangle_it->color_start_idx) >= color_end) + return out; + + if (triangle_it == m_data.triangles_to_split.begin() && !m_data.metadata_json.empty()) { + out = "~"; + out += color_facets_string_to_hex(m_data.metadata_json); + out += "~"; + } + out += color_facets_bits_to_hex(m_data.bitstream, triangle_it->bitstream_start_idx, bitstream_end); + out += "|"; + for (size_t idx = size_t(triangle_it->color_start_idx); idx < color_end; ++idx) + color_facets_append_rgba_hex(out, m_data.colors_rgba[idx]); + return out; +} + +void ColorFacetsAnnotation::set_triangle_from_string(int triangle_id, const std::string &str) +{ + assert(!str.empty()); + assert(m_data.triangles_to_split.empty() || m_data.triangles_to_split.back().triangle_idx < triangle_id); + + size_t data_start = 0; + if (!str.empty() && str.front() == '~') { + const size_t metadata_end = str.find('~', 1); + if (metadata_end == std::string::npos) + return; + if (m_data.triangles_to_split.empty()) + m_data.metadata_json = color_facets_hex_to_string(str.substr(1, metadata_end - 1)); + data_start = metadata_end + 1; + } + + const size_t separator = str.find('|', data_start); + if (separator == std::string::npos || separator == data_start || separator + 8 > str.size()) + return; + + m_data.triangles_to_split.emplace_back(triangle_id, int(m_data.bitstream.size()), int(m_data.colors_rgba.size())); + + const std::string tree = str.substr(data_start, separator - data_start); + for (auto it = tree.crbegin(); it != tree.crend(); ++it) { + const int dec = color_facets_hex_value(*it); + if (dec < 0) + continue; + color_facets_append_nibble(m_data.bitstream, unsigned(dec)); + } + + size_t color_offset = separator + 1; + while (color_offset + 8 <= str.size()) { + uint32_t rgba = 0xFFFFFFFFu; + if (color_facets_parse_rgba_hex(str, color_offset, rgba)) + m_data.colors_rgba.emplace_back(rgba); + color_offset += 8; + } +} + +bool ColorFacetsAnnotation::set_from_triangle_sampler(const ModelVolume &mv, + const TextureMappingColorSampler &sampler, + int max_depth, + float split_color_threshold, + const TextureMappingColorSubdivisionDepths &subdivision_depths) +{ + 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()); + + const std::array root_barycentrics = { + Vec3f(1.f, 0.f, 0.f), + Vec3f(0.f, 1.f, 0.f), + Vec3f(0.f, 0.f, 1.f) + }; + + max_depth = std::clamp(max_depth, 0, 7); + split_color_threshold = std::max(split_color_threshold, 0.f); + + for (size_t tri_idx = 0; tri_idx < its.indices.size(); ++tri_idx) { + const auto &tri = its.indices[tri_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 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 triangle_min_depth = 0; + int triangle_max_depth = max_depth; + if (subdivision_depths) { + const std::pair depths = subdivision_depths(tri_idx, vertices); + triangle_min_depth = std::clamp(depths.first, 0, 7); + triangle_max_depth = std::clamp(depths.second, 0, 7); + if (triangle_max_depth < triangle_min_depth) + triangle_max_depth = triangle_min_depth; + } + + new_data.triangles_to_split.emplace_back(int(tri_idx), int(new_data.bitstream.size()), int(new_data.colors_rgba.size())); + color_facets_append_sampled_triangle(new_data, + sampler, + tri_idx, + vertices, + root_barycentrics, + 0, + triangle_min_depth, + triangle_max_depth, + split_color_threshold); + } + + new_data.triangles_to_split.shrink_to_fit(); + new_data.bitstream.shrink_to_fit(); + new_data.colors_rgba.shrink_to_fit(); + + if (new_data != m_data) { + m_data = std::move(new_data); + this->touch(); + return true; + } + return false; +} + +void ColorFacetsAnnotation::get_facet_triangles(const ModelVolume &mv, std::vector &facets) const +{ + facets.clear(); + const indexed_triangle_set &its = mv.mesh().its; + if (its.vertices.empty() || its.indices.empty()) + return; + + facets.reserve(m_data.colors_rgba.size()); + for (auto mapping_it = m_data.triangles_to_split.begin(); mapping_it != m_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 std::array vertices = { + its.vertices[size_t(tri[0])].cast(), + its.vertices[size_t(tri[1])].cast(), + its.vertices[size_t(tri[2])].cast() + }; + const auto next_it = std::next(mapping_it); + const int bitstream_end = next_it == m_data.triangles_to_split.end() ? int(m_data.bitstream.size()) : next_it->bitstream_start_idx; + const size_t color_end = next_it == m_data.triangles_to_split.end() ? m_data.colors_rgba.size() : size_t(next_it->color_start_idx); + int bit_idx = mapping_it->bitstream_start_idx; + size_t color_idx = size_t(mapping_it->color_start_idx); + color_facets_extract_triangle(m_data, bitstream_end, color_end, bit_idx, color_idx, mapping_it->triangle_idx, vertices, facets); + } +} + // Test whether the two models contain the same number of ModelObjects with the same set of IDs // ordered in the same order. In that case it is not necessary to kill the background processing. bool model_object_list_equal(const Model &model_old, const Model &model_new) @@ -4226,7 +4657,18 @@ bool model_mmu_segmentation_data_changed(const ModelObject& mo, const ModelObjec { return model_property_changed(mo, mo_new, [](const ModelVolumeType t) { return t == ModelVolumeType::MODEL_PART; }, - [](const ModelVolume &mv_old, const ModelVolume &mv_new){ return mv_old.mmu_segmentation_facets.timestamp_matches(mv_new.mmu_segmentation_facets); }); + [](const ModelVolume &mv_old, const ModelVolume &mv_new) { + return mv_old.mmu_segmentation_facets.timestamp_matches(mv_new.mmu_segmentation_facets); + }); +} + +bool model_texture_mapping_color_data_changed(const ModelObject& mo, const ModelObject& mo_new) +{ + return model_property_changed(mo, mo_new, + [](const ModelVolumeType t) { return t == ModelVolumeType::MODEL_PART; }, + [](const ModelVolume &mv_old, const ModelVolume &mv_new) { + return mv_old.texture_mapping_color_facets.timestamp_matches(mv_new.texture_mapping_color_facets); + }); } bool model_fuzzy_skin_data_changed(const ModelObject &mo, const ModelObject &mo_new) diff --git a/src/libslic3r/Model.hpp b/src/libslic3r/Model.hpp index cac890738a..2ffaf299ac 100644 --- a/src/libslic3r/Model.hpp +++ b/src/libslic3r/Model.hpp @@ -34,6 +34,7 @@ #include #include #include +#include #include #include #include @@ -791,6 +792,119 @@ private: friend class ModelVolume; }; +struct ColorTriangleBitStreamMapping +{ + int triangle_idx = -1; + int bitstream_start_idx = -1; + int color_start_idx = -1; + + ColorTriangleBitStreamMapping() = default; + ColorTriangleBitStreamMapping(int triangle_idx_, int bitstream_start_idx_, int color_start_idx_) + : triangle_idx(triangle_idx_), bitstream_start_idx(bitstream_start_idx_), color_start_idx(color_start_idx_) {} + + friend bool operator==(const ColorTriangleBitStreamMapping &lhs, const ColorTriangleBitStreamMapping &rhs) + { + return lhs.triangle_idx == rhs.triangle_idx && + lhs.bitstream_start_idx == rhs.bitstream_start_idx && + lhs.color_start_idx == rhs.color_start_idx; + } + friend bool operator!=(const ColorTriangleBitStreamMapping &lhs, const ColorTriangleBitStreamMapping &rhs) { return !(lhs == rhs); } + +private: + friend class cereal::access; + template void serialize(Archive &ar) { ar(triangle_idx, bitstream_start_idx, color_start_idx); } +}; + +struct TriangleColorSplittingData +{ + std::vector triangles_to_split; + std::vector bitstream; + std::vector colors_rgba; + std::string metadata_json; + + friend bool operator==(const TriangleColorSplittingData &lhs, const TriangleColorSplittingData &rhs) + { + return lhs.triangles_to_split == rhs.triangles_to_split && + lhs.bitstream == rhs.bitstream && + lhs.colors_rgba == rhs.colors_rgba && + lhs.metadata_json == rhs.metadata_json; + } + friend bool operator!=(const TriangleColorSplittingData &lhs, const TriangleColorSplittingData &rhs) { return !(lhs == rhs); } + +private: + friend class cereal::access; + template void serialize(Archive &ar) { ar(triangles_to_split, bitstream, colors_rgba, metadata_json); } +}; + +struct ColorFacetTriangle +{ + std::array vertices; + int source_triangle = -1; + uint32_t rgba = 0xFFFFFFFFu; +}; + +using TextureMappingColorSampler = std::function; +using TextureMappingColorSubdivisionDepths = std::function(size_t, const std::array &)>; + +class ColorFacetsAnnotation final : public ObjectWithTimestamp { +public: + void assign(const ColorFacetsAnnotation &rhs) + { + if (!this->timestamp_matches(rhs)) { + m_data = rhs.m_data; + this->copy_timestamp(rhs); + } + } + void assign(ColorFacetsAnnotation &&rhs) + { + if (!this->timestamp_matches(rhs)) { + m_data = std::move(rhs.m_data); + this->copy_timestamp(rhs); + } + } + const TriangleColorSplittingData &get_data() const noexcept { return m_data; } + const std::string &metadata_json() const noexcept { return m_data.metadata_json; } + void set_metadata_json(std::string metadata_json); + bool empty() const { return m_data.triangles_to_split.empty(); } + void reset(); + void reserve(int n_triangles) { m_data.triangles_to_split.reserve(n_triangles); } + void shrink_to_fit() + { + m_data.triangles_to_split.shrink_to_fit(); + m_data.bitstream.shrink_to_fit(); + m_data.colors_rgba.shrink_to_fit(); + } + bool equals(const ColorFacetsAnnotation &other) const; + std::string get_triangle_as_string(int triangle_idx) const; + void set_triangle_from_string(int triangle_id, const std::string &str); + bool set_from_triangle_sampler(const ModelVolume &mv, + const TextureMappingColorSampler &sampler, + int max_depth = 2, + float split_color_threshold = 0.045f, + const TextureMappingColorSubdivisionDepths &subdivision_depths = {}); + void get_facet_triangles(const ModelVolume &mv, std::vector &facets) const; + +private: + explicit ColorFacetsAnnotation() = default; + explicit ColorFacetsAnnotation(int) : ObjectWithTimestamp(-1) {} + explicit ColorFacetsAnnotation(const ColorFacetsAnnotation &rhs) = default; + explicit ColorFacetsAnnotation(ColorFacetsAnnotation &&rhs) = default; + + ColorFacetsAnnotation& operator=(const ColorFacetsAnnotation &rhs) = default; + ColorFacetsAnnotation& operator=(ColorFacetsAnnotation &&rhs) = default; + + friend class cereal::access; + friend class UndoRedo::StackImpl; + friend class ModelVolume; + + template void serialize(Archive &ar) + { + ar(cereal::base_class(this), m_data); + } + + TriangleColorSplittingData m_data; +}; + // An object STL, or a modifier volume, over which a different set of parameters shall be applied. // ModelVolume instances are owned by a ModelObject. class ModelVolume final : public ObjectBase @@ -877,6 +991,8 @@ public: // List of mesh facets painted for MMU segmentation. FacetsAnnotation mmu_segmentation_facets; + ColorFacetsAnnotation texture_mapping_color_facets; + std::vector imported_vertex_colors_rgba; std::vector imported_texture_uvs_per_face; @@ -1011,12 +1127,14 @@ public: this->supported_facets.set_new_unique_id(); this->seam_facets.set_new_unique_id(); this->mmu_segmentation_facets.set_new_unique_id(); + this->texture_mapping_color_facets.set_new_unique_id(); this->fuzzy_skin_facets.set_new_unique_id(); } bool is_fdm_support_painted() const { return !this->supported_facets.empty(); } bool is_seam_painted() const { return !this->seam_facets.empty(); } bool is_mm_painted() const { return !this->mmu_segmentation_facets.empty(); } + bool has_texture_mapping_color_data() const { return !this->texture_mapping_color_facets.empty(); } bool is_fuzzy_skin_painted() const { return !this->fuzzy_skin_facets.empty(); } // Orca: Implement prusa's filament shrink compensation approach @@ -1069,11 +1187,13 @@ private: assert(this->supported_facets.id().valid()); assert(this->seam_facets.id().valid()); assert(this->mmu_segmentation_facets.id().valid()); + assert(this->texture_mapping_color_facets.id().valid()); assert(this->fuzzy_skin_facets.id().valid()); assert(this->id() != this->config.id()); assert(this->id() != this->supported_facets.id()); assert(this->id() != this->seam_facets.id()); assert(this->id() != this->mmu_segmentation_facets.id()); + assert(this->id() != this->texture_mapping_color_facets.id()); assert(this->id() != this->fuzzy_skin_facets.id()); if (mesh.facets_count() > 1) calculate_convex_hull(); @@ -1085,11 +1205,13 @@ private: assert(this->supported_facets.id().valid()); assert(this->seam_facets.id().valid()); assert(this->mmu_segmentation_facets.id().valid()); + assert(this->texture_mapping_color_facets.id().valid()); assert(this->fuzzy_skin_facets.id().valid()); assert(this->id() != this->config.id()); assert(this->id() != this->supported_facets.id()); assert(this->id() != this->seam_facets.id()); assert(this->id() != this->mmu_segmentation_facets.id()); + assert(this->id() != this->texture_mapping_color_facets.id()); assert(this->id() != this->fuzzy_skin_facets.id()); } ModelVolume(ModelObject *object, TriangleMesh &&mesh, TriangleMesh &&convex_hull, ModelVolumeType type = ModelVolumeType::MODEL_PART) : @@ -1099,11 +1221,13 @@ private: assert(this->supported_facets.id().valid()); assert(this->seam_facets.id().valid()); assert(this->mmu_segmentation_facets.id().valid()); + assert(this->texture_mapping_color_facets.id().valid()); assert(this->fuzzy_skin_facets.id().valid()); assert(this->id() != this->config.id()); assert(this->id() != this->supported_facets.id()); assert(this->id() != this->seam_facets.id()); assert(this->id() != this->mmu_segmentation_facets.id()); + assert(this->id() != this->texture_mapping_color_facets.id()); assert(this->id() != this->fuzzy_skin_facets.id()); } @@ -1113,6 +1237,7 @@ private: name(other.name), source(other.source), m_mesh(other.m_mesh), m_convex_hull(other.m_convex_hull), config(other.config), m_type(other.m_type), object(object), m_transformation(other.m_transformation), supported_facets(other.supported_facets), seam_facets(other.seam_facets), mmu_segmentation_facets(other.mmu_segmentation_facets), + texture_mapping_color_facets(other.texture_mapping_color_facets), imported_vertex_colors_rgba(other.imported_vertex_colors_rgba), imported_texture_uvs_per_face(other.imported_texture_uvs_per_face), imported_texture_uv_valid(other.imported_texture_uv_valid), @@ -1126,16 +1251,19 @@ private: assert(this->supported_facets.id().valid()); assert(this->seam_facets.id().valid()); assert(this->mmu_segmentation_facets.id().valid()); + assert(this->texture_mapping_color_facets.id().valid()); assert(this->fuzzy_skin_facets.id().valid()); assert(this->id() != this->config.id()); assert(this->id() != this->supported_facets.id()); assert(this->id() != this->seam_facets.id()); assert(this->id() != this->mmu_segmentation_facets.id()); + assert(this->id() != this->texture_mapping_color_facets.id()); assert(this->id() == other.id()); assert(this->config.id() == other.config.id()); assert(this->supported_facets.id() == other.supported_facets.id()); assert(this->seam_facets.id() == other.seam_facets.id()); assert(this->mmu_segmentation_facets.id() == other.mmu_segmentation_facets.id()); + assert(this->texture_mapping_color_facets.id() == other.texture_mapping_color_facets.id()); assert(this->fuzzy_skin_facets.id() == other.fuzzy_skin_facets.id()); this->set_material_id(other.material_id()); } @@ -1149,11 +1277,13 @@ private: assert(this->supported_facets.id().valid()); assert(this->seam_facets.id().valid()); assert(this->mmu_segmentation_facets.id().valid()); + assert(this->texture_mapping_color_facets.id().valid()); assert(this->fuzzy_skin_facets.id().valid()); assert(this->id() != this->config.id()); assert(this->id() != this->supported_facets.id()); assert(this->id() != this->seam_facets.id()); assert(this->id() != this->mmu_segmentation_facets.id()); + assert(this->id() != this->texture_mapping_color_facets.id()); assert(this->id() != this->fuzzy_skin_facets.id()); assert(this->id() != other.id()); assert(this->config.id() == other.config.id()); @@ -1166,11 +1296,13 @@ private: assert(this->supported_facets.id() != other.supported_facets.id()); assert(this->seam_facets.id() != other.seam_facets.id()); assert(this->mmu_segmentation_facets.id() != other.mmu_segmentation_facets.id()); + assert(this->texture_mapping_color_facets.id() != other.texture_mapping_color_facets.id()); assert(this->fuzzy_skin_facets.id() != other.fuzzy_skin_facets.id()); assert(this->id() != this->config.id()); assert(this->supported_facets.empty()); assert(this->seam_facets.empty()); assert(this->mmu_segmentation_facets.empty()); + assert(this->texture_mapping_color_facets.empty()); assert(this->fuzzy_skin_facets.empty()); } @@ -1179,12 +1311,13 @@ private: friend class cereal::access; friend class UndoRedo::StackImpl; // Used for deserialization, therefore no IDs are allocated. - ModelVolume() : ObjectBase(-1), config(-1), supported_facets(-1), seam_facets(-1), mmu_segmentation_facets(-1), fuzzy_skin_facets(-1), object(nullptr) { + ModelVolume() : ObjectBase(-1), config(-1), supported_facets(-1), seam_facets(-1), mmu_segmentation_facets(-1), texture_mapping_color_facets(-1), fuzzy_skin_facets(-1), object(nullptr) { assert(this->id().invalid()); assert(this->config.id().invalid()); assert(this->supported_facets.id().invalid()); assert(this->seam_facets.id().invalid()); assert(this->mmu_segmentation_facets.id().invalid()); + assert(this->texture_mapping_color_facets.id().invalid()); assert(this->fuzzy_skin_facets.id().invalid()); } template void load(Archive &ar) { @@ -1203,6 +1336,9 @@ private: t = mmu_segmentation_facets.timestamp(); cereal::load_by_value(ar, mmu_segmentation_facets); mesh_changed |= t != mmu_segmentation_facets.timestamp(); + t = texture_mapping_color_facets.timestamp(); + cereal::load_by_value(ar, texture_mapping_color_facets); + mesh_changed |= t != texture_mapping_color_facets.timestamp(); ar(imported_vertex_colors_rgba); ar(imported_texture_uvs_per_face, imported_texture_uv_valid, imported_texture_rgba, imported_texture_width, imported_texture_height); cereal::load_by_value(ar, fuzzy_skin_facets); @@ -1210,6 +1346,16 @@ private: cereal::load_by_value(ar, config); cereal::load(ar, text_configuration); cereal::load(ar, emboss_shape); + if (!m_mesh) { + m_mesh = std::make_shared(); + imported_vertex_colors_rgba.clear(); + imported_texture_uvs_per_face.clear(); + imported_texture_uv_valid.clear(); + imported_texture_rgba.clear(); + imported_texture_width = 0; + imported_texture_height = 0; + texture_mapping_color_facets.reset(); + } assert(m_mesh); if (has_convex_hull) { cereal::load_optional(ar, m_convex_hull); @@ -1227,6 +1373,7 @@ private: cereal::save_by_value(ar, supported_facets); cereal::save_by_value(ar, seam_facets); cereal::save_by_value(ar, mmu_segmentation_facets); + cereal::save_by_value(ar, texture_mapping_color_facets); ar(imported_vertex_colors_rgba); ar(imported_texture_uvs_per_face, imported_texture_uv_valid, imported_texture_rgba, imported_texture_width, imported_texture_height); cereal::save_by_value(ar, fuzzy_skin_facets); @@ -1778,6 +1925,8 @@ bool model_custom_seam_data_changed(const ModelObject& mo, const ModelObject& mo // The function assumes that volumes list is synchronized. extern bool model_mmu_segmentation_data_changed(const ModelObject& mo, const ModelObject& mo_new); +extern bool model_texture_mapping_color_data_changed(const ModelObject& mo, const ModelObject& mo_new); + // Test whether the now ModelObject has newer fuzzy skin data than the old one. // The function assumes that volumes list is synchronized. extern bool model_fuzzy_skin_data_changed(const ModelObject &mo, const ModelObject &mo_new); diff --git a/src/libslic3r/PrintApply.cpp b/src/libslic3r/PrintApply.cpp index 7466964cc4..00b790d497 100644 --- a/src/libslic3r/PrintApply.cpp +++ b/src/libslic3r/PrintApply.cpp @@ -78,6 +78,8 @@ static inline void model_volume_list_copy_configs(ModelObject &model_object_dst, mv_dst.seam_facets.assign(mv_src.seam_facets); assert(mv_dst.mmu_segmentation_facets.id() == mv_src.mmu_segmentation_facets.id()); mv_dst.mmu_segmentation_facets.assign(mv_src.mmu_segmentation_facets); + assert(mv_dst.texture_mapping_color_facets.id() == mv_src.texture_mapping_color_facets.id()); + mv_dst.texture_mapping_color_facets.assign(mv_src.texture_mapping_color_facets); assert(mv_dst.fuzzy_skin_facets.id() == mv_src.fuzzy_skin_facets.id()); mv_dst.fuzzy_skin_facets.assign(mv_src.fuzzy_skin_facets); //FIXME what to do with the materials? @@ -1393,6 +1395,7 @@ Print::ApplyStatus Print::apply(const Model &model, DynamicPrintConfig new_full_ // Only volume IDs, volume types, transformation matrices and their order are checked, configuration and other parameters are NOT checked. bool solid_or_modifier_differ = model_volume_list_changed(model_object, model_object_new, solid_or_modifier_types) || model_mmu_segmentation_data_changed(model_object, model_object_new) || + model_texture_mapping_color_data_changed(model_object, model_object_new) || (model_object_new.is_mm_painted() && num_extruders_changed) || model_fuzzy_skin_data_changed(model_object, model_object_new); bool supports_differ = model_volume_list_changed(model_object, model_object_new, ModelVolumeType::SUPPORT_BLOCKER) || diff --git a/src/libslic3r/PrintObjectSlice.cpp b/src/libslic3r/PrintObjectSlice.cpp index 12236b3079..5d428e5835 100644 --- a/src/libslic3r/PrintObjectSlice.cpp +++ b/src/libslic3r/PrintObjectSlice.cpp @@ -216,8 +216,11 @@ static std::vector collect_texture_mapping_vertex_color_match_warni bool has_imported_vertex_color_data = false; bool has_imported_texture_data = false; + bool has_texture_mapping_color_data = false; bool has_uv_texture_reference_but_no_image = false; for (const ModelVolume *volume : model_object->volumes) { + if (volume != nullptr && !volume->texture_mapping_color_facets.empty()) + has_texture_mapping_color_data = true; if (volume != nullptr && !volume->imported_vertex_colors_rgba.empty()) has_imported_vertex_color_data = true; if (volume != nullptr && @@ -236,11 +239,11 @@ static std::vector collect_texture_mapping_vertex_color_match_warni !volume->imported_texture_uvs_per_face.empty()) { has_imported_texture_data = true; } - if (has_imported_vertex_color_data || has_imported_texture_data) + if (has_texture_mapping_color_data || has_imported_vertex_color_data || has_imported_texture_data) break; } - if (has_imported_vertex_color_data || has_imported_texture_data) + if (has_texture_mapping_color_data || has_imported_vertex_color_data || has_imported_texture_data) return {}; if (has_uv_texture_reference_but_no_image) diff --git a/src/slic3r/GUI/3DScene.cpp b/src/slic3r/GUI/3DScene.cpp index f0ef99c381..04a5332bb1 100644 --- a/src/slic3r/GUI/3DScene.cpp +++ b/src/slic3r/GUI/3DScene.cpp @@ -116,7 +116,8 @@ std::vector build_full_mesh_texture_previe bool model_volume_has_any_texture_preview_data(const ModelVolume &model_volume) { - return !model_volume.imported_vertex_colors_rgba.empty() || + return !model_volume.texture_mapping_color_facets.empty() || + !model_volume.imported_vertex_colors_rgba.empty() || (!model_volume.imported_texture_rgba.empty() && model_volume.imported_texture_width > 0 && model_volume.imported_texture_height > 0); @@ -647,6 +648,8 @@ void GLVolume::simple_render(GLShaderProgram* shader, ModelObjectPtrs& model_obj (preview_visual_signature << 6) + (preview_visual_signature >> 2); preview_visual_signature ^= reinterpret_cast(model_volume->imported_vertex_colors_rgba.data()) + 0x9e3779b97f4a7c15ull + (preview_visual_signature << 6) + (preview_visual_signature >> 2); + preview_visual_signature ^= model_volume_texture_mapping_color_preview_signature(*model_volume) + 0x9e3779b97f4a7c15ull + + (preview_visual_signature << 6) + (preview_visual_signature >> 2); if (model_volume->mmu_segmentation_facets.timestamp() != mmuseg_ts || preview_visual_signature != mmuseg_texture_preview_visual_signature) { mmuseg_models.clear(); diff --git a/src/slic3r/GUI/CreatePresetsDialog.cpp b/src/slic3r/GUI/CreatePresetsDialog.cpp index 12718f3609..6ff773b952 100644 --- a/src/slic3r/GUI/CreatePresetsDialog.cpp +++ b/src/slic3r/GUI/CreatePresetsDialog.cpp @@ -245,7 +245,10 @@ static bool delete_filament_preset_by_name(std::string delete_preset_name, std:: try { // BBS delete preset Preset *need_delete_preset = m_presets.find_preset(delete_preset_name); - if (!need_delete_preset) BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(" can't find delete preset and name: %1%") % delete_preset_name; + if (!need_delete_preset) { + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << " can't find delete preset and name: " << delete_preset_name; + return false; + } if (!need_delete_preset->setting_id.empty()) { BOOST_LOG_TRIVIAL(info) << "delete preset = " << need_delete_preset->name << ", setting_id = " << need_delete_preset->setting_id; m_presets.set_sync_info_and_save(need_delete_preset->name, need_delete_preset->setting_id, "delete", 0); @@ -4754,27 +4757,29 @@ wxWindow *EditFilamentPresetDialog::create_dialog_buttons() _L("Delete filament"), wxYES | wxCANCEL | wxCANCEL_DEFAULT | wxCENTRE); int res = dlg.ShowModal(); if (wxID_YES == res) { - PresetBundle *preset_bundle = wxGetApp().preset_bundle; - std::set> inherit_preset_names; - std::set> root_preset_names; - for (std::pair>> printer_and_preset : m_printer_compatible_presets) { - for (std::shared_ptr preset : printer_and_preset.second) { + std::set inherit_preset_names; + std::set root_preset_names; + for (const std::pair>> &printer_and_preset : m_printer_compatible_presets) { + for (const std::shared_ptr &preset : printer_and_preset.second) { + if (!preset) continue; if (preset->inherits().empty()) { - root_preset_names.insert(preset); + root_preset_names.insert(preset->name); } else { - inherit_preset_names.insert(preset); + inherit_preset_names.insert(preset->name); } } } // delete inherit preset first std::string next_selected_preset_name = wxGetApp().preset_bundle->filaments.get_selected_preset().name; - for (std::shared_ptr preset : inherit_preset_names) { - bool delete_result = delete_filament_preset_by_name(preset->name, next_selected_preset_name); - BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << " inherit filament name: " << preset->name << (delete_result ? " delete successful" : " delete failed"); + for (const std::string &preset_name : inherit_preset_names) { + bool delete_result = delete_filament_preset_by_name(preset_name, next_selected_preset_name); + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << " inherit filament name: " << preset_name + << (delete_result ? " delete successful" : " delete failed"); } - for (std::shared_ptr preset : root_preset_names) { - bool delete_result = delete_filament_preset_by_name(preset->name, next_selected_preset_name); - BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << " root filament name: " << preset->name << (delete_result ? " delete successful" : " delete failed"); + for (const std::string &preset_name : root_preset_names) { + bool delete_result = delete_filament_preset_by_name(preset_name, next_selected_preset_name); + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << " root filament name: " << preset_name + << (delete_result ? " delete successful" : " delete failed"); } m_printer_compatible_presets.clear(); wxGetApp().preset_bundle->filaments.select_preset_by_name(next_selected_preset_name,true); diff --git a/src/slic3r/GUI/GLCanvas3D.cpp b/src/slic3r/GUI/GLCanvas3D.cpp index 0060ff3512..eeba37733c 100644 --- a/src/slic3r/GUI/GLCanvas3D.cpp +++ b/src/slic3r/GUI/GLCanvas3D.cpp @@ -2001,7 +2001,12 @@ void GLCanvas3D::render(bool only_init) //only_body = true; only_current = true; } - else if ((gizmo_type == GLGizmosManager::FdmSupports) || (gizmo_type == GLGizmosManager::Seam) || (gizmo_type == GLGizmosManager::MmSegmentation) || (gizmo_type == GLGizmosManager::FuzzySkin)) + else if ((gizmo_type == GLGizmosManager::FdmSupports) || + (gizmo_type == GLGizmosManager::Seam) || + (gizmo_type == GLGizmosManager::MmSegmentation) || + (gizmo_type == GLGizmosManager::TrueColorPainting) || + (gizmo_type == GLGizmosManager::ImageProjection) || + (gizmo_type == GLGizmosManager::FuzzySkin)) no_partplate = true; else if (gizmo_type == GLGizmosManager::BrimEars && !camera.is_looking_downward()) show_grid = false; @@ -3416,7 +3421,9 @@ void GLCanvas3D::on_char(wxKeyEvent& evt) if (keyCode < '7') keyCode += 10; m_timer_set_color.Stop(); } - if (m_gizmos.get_current_type() != GLGizmosManager::MmSegmentation) + if (m_gizmos.get_current_type() != GLGizmosManager::MmSegmentation && + m_gizmos.get_current_type() != GLGizmosManager::TrueColorPainting && + m_gizmos.get_current_type() != GLGizmosManager::ImageProjection) obj_list->set_extruder_for_selected_items(keyCode - '0'); break; } @@ -3958,7 +3965,9 @@ void GLCanvas3D::on_render_timer(wxTimerEvent& evt) void GLCanvas3D::on_set_color_timer(wxTimerEvent& evt) { auto obj_list = wxGetApp().obj_list(); - if (m_gizmos.get_current_type() != GLGizmosManager::MmSegmentation) + if (m_gizmos.get_current_type() != GLGizmosManager::MmSegmentation && + m_gizmos.get_current_type() != GLGizmosManager::TrueColorPainting && + m_gizmos.get_current_type() != GLGizmosManager::ImageProjection) obj_list->set_extruder_for_selected_items(1); m_timer_set_color.Stop(); } @@ -4113,7 +4122,10 @@ void GLCanvas3D::on_mouse(wxMouseEvent& evt) m_dirty = true; // do not return if dragging or tooltip not empty to allow for tooltip update // also, do not return if the mouse is moving and also is inside MM gizmo to allow update seed fill selection - if (!m_mouse.dragging && m_tooltip.is_empty() && (m_gizmos.get_current_type() != GLGizmosManager::MmSegmentation || !evt.Moving())) + if (!m_mouse.dragging && m_tooltip.is_empty() && + ((m_gizmos.get_current_type() != GLGizmosManager::MmSegmentation && + m_gizmos.get_current_type() != GLGizmosManager::TrueColorPainting && + m_gizmos.get_current_type() != GLGizmosManager::ImageProjection) || !evt.Moving())) return; } @@ -4314,6 +4326,8 @@ void GLCanvas3D::on_mouse(wxMouseEvent& evt) && m_gizmos.get_current_type() != GLGizmosManager::Seam && m_gizmos.get_current_type() != GLGizmosManager::Cut && m_gizmos.get_current_type() != GLGizmosManager::MmSegmentation + && m_gizmos.get_current_type() != GLGizmosManager::TrueColorPainting + && m_gizmos.get_current_type() != GLGizmosManager::ImageProjection && m_gizmos.get_current_type() != GLGizmosManager::FuzzySkin) { m_rectangle_selection.start_dragging(m_mouse.position, evt.ShiftDown() ? GLSelectionRectangle::Select : GLSelectionRectangle::Deselect); m_dirty = true; @@ -4466,6 +4480,8 @@ void GLCanvas3D::on_mouse(wxMouseEvent& evt) const Vec3d rot = (Vec3d(pos.x(), pos.y(), 0.) - m_mouse.drag.start_position_3D) * (PI * TRACKBALLSIZE / 180.) * mult; if (this->m_canvas_type == ECanvasType::CanvasAssembleView || m_gizmos.get_current_type() == GLGizmosManager::FdmSupports || m_gizmos.get_current_type() == GLGizmosManager::Seam || m_gizmos.get_current_type() == GLGizmosManager::MmSegmentation || + m_gizmos.get_current_type() == GLGizmosManager::TrueColorPainting || + m_gizmos.get_current_type() == GLGizmosManager::ImageProjection || m_gizmos.get_current_type() == GLGizmosManager::FuzzySkin) { Vec3d rotate_target = Vec3d::Zero(); if (!m_selection.is_empty()) @@ -8942,7 +8958,9 @@ void GLCanvas3D::_render_assemble_control() GLVolume::explosion_ratio = m_explosion_ratio = 1.0; return; } - if (m_gizmos.get_current_type() == GLGizmosManager::EType::MmSegmentation) { + if (m_gizmos.get_current_type() == GLGizmosManager::EType::MmSegmentation || + m_gizmos.get_current_type() == GLGizmosManager::EType::TrueColorPainting || + m_gizmos.get_current_type() == GLGizmosManager::EType::ImageProjection) { m_gizmos.m_assemble_view_data->model_objects_clipper()->set_position(0.0, true); return; } diff --git a/src/slic3r/GUI/GLCanvas3D.hpp b/src/slic3r/GUI/GLCanvas3D.hpp index 2895b110d3..8591a89d97 100644 --- a/src/slic3r/GUI/GLCanvas3D.hpp +++ b/src/slic3r/GUI/GLCanvas3D.hpp @@ -731,7 +731,7 @@ public: void set_context(wxGLContext* context) { m_context = context; } void set_type(ECanvasType type) { m_canvas_type = type; } - ECanvasType get_canvas_type() { return m_canvas_type; } + ECanvasType get_canvas_type() const { return m_canvas_type; } wxGLCanvas* get_wxglcanvas() { return m_canvas; } const wxGLCanvas* get_wxglcanvas() const { return m_canvas; } diff --git a/src/slic3r/GUI/GUI_ObjectList.cpp b/src/slic3r/GUI/GUI_ObjectList.cpp index 6335a0d8a4..3daa1c6fe7 100644 --- a/src/slic3r/GUI/GUI_ObjectList.cpp +++ b/src/slic3r/GUI/GUI_ObjectList.cpp @@ -655,7 +655,7 @@ void ObjectList::set_tooltip_for_item(const wxPoint& pt) } else if (col->GetModelColumn() == (unsigned int)colColorPaint) { if (node->HasColorPainting()) - tooltip = _(L("Click the icon to edit color painting of the object")); + tooltip = _(L("Click the icon to edit color region painting of the object")); } else if (col->GetModelColumn() == (unsigned int)colSinking) { if (node->HasSinking()) @@ -2662,7 +2662,7 @@ void ObjectList::del_info_item(const int obj_idx, InfoItemType type) case InfoItemType::MmSegmentation: cnv->get_gizmos_manager().reset_all_states(); - Plater::TakeSnapshot(plater, "Remove color painting"); + Plater::TakeSnapshot(plater, "Remove color region painting"); for (ModelVolume* mv : (*m_objects)[obj_idx]->volumes) mv->mmu_segmentation_facets.reset(); break; diff --git a/src/slic3r/GUI/Gizmos/GLGizmoMmuSegmentation.cpp b/src/slic3r/GUI/Gizmos/GLGizmoMmuSegmentation.cpp index 3b5c1465c9..381ca85a29 100644 --- a/src/slic3r/GUI/Gizmos/GLGizmoMmuSegmentation.cpp +++ b/src/slic3r/GUI/Gizmos/GLGizmoMmuSegmentation.cpp @@ -10,8 +10,8 @@ #include "slic3r/GUI/GUI_ObjectList.hpp" #include "slic3r/GUI/NotificationManager.hpp" #include "slic3r/GUI/GUI.hpp" -#include "slic3r/GUI/MainFrame.hpp" #include "slic3r/GUI/ObjColorDialog.hpp" +#include "slic3r/GUI/MainFrame.hpp" #include "slic3r/GUI/Tab.hpp" #include "libslic3r/PresetBundle.hpp" #include "libslic3r/Model.hpp" @@ -21,11 +21,17 @@ #include - #include #include #include +#include #include +#include +#include +#include +#include +#include +#include namespace Slic3r::GUI { @@ -39,6 +45,95 @@ static inline void show_notification_extruders_limit_exceeded() "first %1% filaments will be available in painting tool."), GLGizmoMmuSegmentation::EXTRUDERS_LIMIT)); } +void GLGizmoMmuSegmentation::on_opening() +{ + if (get_extruders_colors().size() > GLGizmoMmuSegmentation::EXTRUDERS_LIMIT) + show_notification_extruders_limit_exceeded(); +} + +void GLGizmoMmuSegmentation::on_shutdown() +{ + m_parent.use_slope(false); + m_parent.toggle_model_objects_visibility(true); +} + +std::string GLGizmoMmuSegmentation::on_get_name() const +{ + return _u8L("Color Region Painting"); +} + +bool GLGizmoMmuSegmentation::on_is_selectable() const +{ + return (wxGetApp().preset_bundle->printers.get_edited_preset().printer_technology() == ptFFF + && /*wxGetApp().get_mode() != comSimple && */wxGetApp().filaments_cnt() > 1); +} + +bool GLGizmoMmuSegmentation::on_is_activable() const +{ + const Selection& selection = m_parent.get_selection(); + return !selection.is_empty() && (selection.is_single_full_instance() || selection.is_any_volume()) && wxGetApp().filaments_cnt() > 1; +} + +//BBS: use the global one in 3DScene.cpp +/*static std::vector get_extruders_colors() +{ + unsigned char rgb_color[3] = {}; + std::vector colors = Slic3r::GUI::wxGetApp().plater()->get_extruder_colors_from_plater_config(); + std::vector colors_out(colors.size()); + for (const std::string &color : colors) { + Slic3r::GUI::BitmapCache::parse_color(color, rgb_color); + size_t color_idx = &color - &colors.front(); + colors_out[color_idx] = {float(rgb_color[0]) / 255.f, float(rgb_color[1]) / 255.f, float(rgb_color[2]) / 255.f, 1.f}; + } + + return colors_out; +}*/ + +static std::vector get_extruder_id_for_volumes(const ModelObject &model_object) +{ + std::vector extruders_idx; + extruders_idx.reserve(model_object.volumes.size()); + for (const ModelVolume *model_volume : model_object.volumes) { + if (!model_volume->is_model_part()) + continue; + + extruders_idx.emplace_back(model_volume->extruder_id()); + } + + return extruders_idx; +} + +static std::vector get_display_filament_ids(size_t total_filaments) +{ + std::vector ordered_filament_ids; + if (wxGetApp().plater() != nullptr) + ordered_filament_ids = wxGetApp().plater()->sidebar().get_ui_ordered_filament_ids(); + + std::vector sanitized_filament_ids; + sanitized_filament_ids.reserve(total_filaments); + std::vector used_filament_ids(total_filaments + 1, false); + const size_t physical_count = size_t(std::max(wxGetApp().filaments_cnt(), 0)); + auto real_filament_id = [physical_count](unsigned int filament_id) { + if (filament_id >= 1 && filament_id <= physical_count) + return true; + return wxGetApp().preset_bundle != nullptr && + wxGetApp().preset_bundle->texture_mapping_zones.is_texture_mapping_zone_id(filament_id); + }; + for (const unsigned int filament_id : ordered_filament_ids) { + if (filament_id == 0 || filament_id > total_filaments || used_filament_ids[filament_id] || !real_filament_id(filament_id)) + continue; + used_filament_ids[filament_id] = true; + sanitized_filament_ids.emplace_back(filament_id); + } + + for (unsigned int filament_id = 1; filament_id <= total_filaments; ++filament_id) { + if (!used_filament_ids[filament_id] && real_filament_id(filament_id)) + sanitized_filament_ids.emplace_back(filament_id); + } + + return sanitized_filament_ids; +} + static unsigned int ensure_texture_mapping_zone() { if (wxGetApp().preset_bundle == nullptr || wxGetApp().plater() == nullptr) @@ -100,7 +195,8 @@ static bool model_volume_has_bakeable_image_texture_data(const ModelVolume *volu !its.indices.empty() && volume->imported_texture_uv_valid.size() == its.indices.size() && volume->imported_texture_uvs_per_face.size() >= its.indices.size() * 6 && - volume->imported_texture_rgba.size() >= size_t(volume->imported_texture_width) * size_t(volume->imported_texture_height) * 4 && + volume->imported_texture_rgba.size() >= + size_t(volume->imported_texture_width) * size_t(volume->imported_texture_height) * 4 && std::any_of(volume->imported_texture_uv_valid.begin(), volume->imported_texture_uv_valid.end(), [](uint8_t valid) { return valid != 0; }); @@ -165,6 +261,183 @@ static uint32_t pack_vertex_color_rgba(const ColorRGBA &color) return (r << 24) | (g << 16) | (b << 8) | a; } +static ColorRGBA unpack_vertex_color_rgba_for_conversion(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 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 <= EPSILON) + return 0; + + return std::clamp(int(std::ceil(std::log2(span / target_span))), 0, max_depth); +} + +static int texture_mapping_depth_for_budget(size_t triangle_count, int requested_max_depth, size_t max_leaf_triangles) +{ + int depth = std::clamp(requested_max_depth, 0, 7); + while (depth > 0) { + double leaf_count = double(std::max(triangle_count, 1)); + for (int idx = 0; idx < depth; ++idx) + leaf_count *= 4.0; + if (leaf_count <= double(max_leaf_triangles)) + break; + --depth; + } + return depth; +} + +static void normalize_color_mix_weights(std::vector &weights) +{ + float sum = 0.f; + for (float &weight : weights) { + if (!std::isfinite(weight) || weight < 0.f) + weight = 0.f; + sum += weight; + } + + if (sum <= EPSILON) { + const float uniform = weights.empty() ? 0.f : 1.f / float(weights.size()); + for (float &weight : weights) + weight = uniform; + return; + } + + const float inv_sum = 1.f / sum; + for (float &weight : weights) + weight *= inv_sum; +} + +static ColorRGBA color_mix_from_weights(const std::vector &colors, + const std::vector &weights, + const ColorRGBA &fallback) +{ + if (colors.empty() || weights.empty()) + return fallback; + + float sum = 0.f; + float r = 0.f; + float g = 0.f; + float b = 0.f; + for (size_t idx = 0; idx < colors.size() && idx < weights.size(); ++idx) { + const float weight = std::max(weights[idx], 0.f); + sum += weight; + r += colors[idx].r() * weight; + g += colors[idx].g() * weight; + b += colors[idx].b() * weight; + } + + if (sum <= EPSILON) + return fallback; + + const float inv_sum = 1.f / sum; + return ColorRGBA(r * inv_sum, g * inv_sum, b * inv_sum, fallback.a()); +} + +static float color_mix_error_squared(const std::vector &colors, const std::vector &weights, const ColorRGBA &target) +{ + const ColorRGBA mix = color_mix_from_weights(colors, weights, target); + return Slic3r::sqr(mix.r() - target.r()) + Slic3r::sqr(mix.g() - target.g()) + Slic3r::sqr(mix.b() - target.b()); +} + +static std::vector closest_color_mix_weights(const std::vector &colors, const ColorRGBA &target) +{ + std::vector weights(colors.size(), 0.f); + if (colors.empty()) + return weights; + + auto improve = [&colors, &target](std::vector candidate) { + normalize_color_mix_weights(candidate); + float step = 0.28f; + for (int iter = 0; iter < 140; ++iter) { + const ColorRGBA mix = color_mix_from_weights(colors, candidate, target); + const float err_r = mix.r() - target.r(); + const float err_g = mix.g() - target.g(); + const float err_b = mix.b() - target.b(); + std::vector next = candidate; + for (size_t idx = 0; idx < colors.size(); ++idx) { + const float grad = 2.f * (err_r * colors[idx].r() + err_g * colors[idx].g() + err_b * colors[idx].b()); + next[idx] -= step * grad; + } + normalize_color_mix_weights(next); + candidate = std::move(next); + step *= 0.985f; + } + return candidate; + }; + + std::vector uniform(colors.size(), 1.f / float(colors.size())); + weights = improve(uniform); + float best_error = color_mix_error_squared(colors, weights, target); + + for (size_t idx = 0; idx < colors.size(); ++idx) { + std::vector single(colors.size(), 0.f); + single[idx] = 1.f; + single = improve(std::move(single)); + const float error = color_mix_error_squared(colors, single, target); + if (error < best_error) { + best_error = error; + weights = std::move(single); + } + } + + return weights; +} + +static float texture_triangle_uv_pixel_span(const ModelVolume *volume, size_t tri_idx) +{ + if (volume == nullptr || + tri_idx >= volume->imported_texture_uv_valid.size() || + volume->imported_texture_uv_valid[tri_idx] == 0) + return 0.f; + + const size_t uv_offset = tri_idx * 6; + if (uv_offset + 5 >= volume->imported_texture_uvs_per_face.size()) + return 0.f; + + const Vec2f uv0(volume->imported_texture_uvs_per_face[uv_offset + 0], volume->imported_texture_uvs_per_face[uv_offset + 1]); + const Vec2f uv1(volume->imported_texture_uvs_per_face[uv_offset + 2], volume->imported_texture_uvs_per_face[uv_offset + 3]); + const Vec2f uv2(volume->imported_texture_uvs_per_face[uv_offset + 4], volume->imported_texture_uvs_per_face[uv_offset + 5]); + const float width = float(std::max(volume->imported_texture_width, 1)); + const float height = float(std::max(volume->imported_texture_height, 1)); + auto pixel_edge_length = [width, height](const Vec2f &a, const Vec2f &b) { + const Vec2f delta = b - a; + return std::sqrt(Slic3r::sqr(delta.x() * width) + Slic3r::sqr(delta.y() * height)); + }; + + return std::max({ pixel_edge_length(uv0, uv1), + pixel_edge_length(uv1, uv2), + pixel_edge_length(uv2, uv0) }); +} + static bool barycentric_weights_for_region_vertex_colors(const Vec3f &point, const Vec3f &p0, const Vec3f &p1, @@ -189,58 +462,989 @@ static bool barycentric_weights_for_region_vertex_colors(const Vec3f &point, return std::isfinite(weights.x()) && std::isfinite(weights.y()) && std::isfinite(weights.z()); } -void GLGizmoMmuSegmentation::on_opening() +static std::string rgb_metadata_json(const ColorRGBA &background) { - if (wxGetApp().filaments_cnt() > int(GLGizmoMmuSegmentation::EXTRUDERS_LIMIT)) - show_notification_extruders_limit_exceeded(); + const uint32_t packed = pack_vertex_color_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; } -void GLGizmoMmuSegmentation::on_shutdown() +static ColorRGBA rgb_metadata_background_color(const ColorFacetsAnnotation &annotation) { - m_parent.use_slope(false); - m_parent.toggle_model_objects_visibility(true); + const std::string &metadata = annotation.metadata_json(); + 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_vertex_color_rgba_for_conversion(packed); } -std::string GLGizmoMmuSegmentation::on_get_name() const +static void refresh_imported_texture_storage(ModelVolume &volume) { - return _u8L("Color Painting"); + std::vector refreshed(volume.imported_texture_rgba.begin(), volume.imported_texture_rgba.end()); + volume.imported_texture_rgba.swap(refreshed); } -bool GLGizmoMmuSegmentation::on_is_selectable() const +static std::optional sample_rgb_color_facets(const std::vector &facets, + const std::unordered_map> &facets_by_source_triangle, + int source_triangle, + const Vec3f &point) { - return (wxGetApp().preset_bundle->printers.get_edited_preset().printer_technology() == ptFFF - && /*wxGetApp().get_mode() != comSimple && */wxGetApp().filaments_cnt() > 1); -} + auto found = facets_by_source_triangle.find(source_triangle); + if (found == facets_by_source_triangle.end()) + return std::nullopt; -bool GLGizmoMmuSegmentation::on_is_activable() const -{ - const Selection& selection = m_parent.get_selection(); - return !selection.is_empty() && (selection.is_single_full_instance() || selection.is_any_volume()) && wxGetApp().filaments_cnt() > 1; -} - -static std::vector get_extruder_id_for_volumes(const ModelObject &model_object) -{ - std::vector extruders_idx; - extruders_idx.reserve(model_object.volumes.size()); - for (const ModelVolume *model_volume : model_object.volumes) { - if (!model_volume->is_model_part()) + const float tolerance = -1e-4f; + for (const size_t facet_idx : found->second) { + if (facet_idx >= facets.size()) continue; - extruders_idx.emplace_back(model_volume->extruder_id()); + const ColorFacetTriangle &facet = facets[facet_idx]; + Vec3f weights = Vec3f::Zero(); + if (!barycentric_weights_for_region_vertex_colors(point, facet.vertices[0], facet.vertices[1], facet.vertices[2], weights)) + continue; + if (weights.x() >= tolerance && weights.y() >= tolerance && weights.z() >= tolerance) + return unpack_vertex_color_rgba_for_conversion(facet.rgba); } - return extruders_idx; + if (found->second.empty() || found->second.front() >= facets.size()) + return std::nullopt; + return unpack_vertex_color_rgba_for_conversion(facets[found->second.front()].rgba); +} + +struct RGBStrokeVertexKey +{ + long long x = 0; + long long y = 0; + long long z = 0; +}; + +struct RGBStrokeEdgeKey +{ + RGBStrokeVertexKey a; + RGBStrokeVertexKey b; +}; + +struct RGBStrokeBoundaryEdge +{ + int source_triangle = -1; + Vec3f a = Vec3f::Zero(); + Vec3f b = Vec3f::Zero(); +}; + +struct RGBStrokeEdgeData +{ + int count = 0; + RGBStrokeBoundaryEdge edge; +}; + +struct RGBStrokeEdgeKeyHash +{ + size_t operator()(const RGBStrokeEdgeKey &key) const + { + size_t hash = 1469598103934665603ull; + auto mix = [&hash](long long value) { + hash ^= std::hash{}(value) + 0x9e3779b97f4a7c15ull + (hash << 6) + (hash >> 2); + }; + mix(key.a.x); + mix(key.a.y); + mix(key.a.z); + mix(key.b.x); + mix(key.b.y); + mix(key.b.z); + return hash; + } +}; + +static bool operator==(const RGBStrokeVertexKey &lhs, const RGBStrokeVertexKey &rhs) +{ + return lhs.x == rhs.x && lhs.y == rhs.y && lhs.z == rhs.z; +} + +static bool operator==(const RGBStrokeEdgeKey &lhs, const RGBStrokeEdgeKey &rhs) +{ + return lhs.a == rhs.a && lhs.b == rhs.b; +} + +static bool rgb_stroke_vertex_key_less(const RGBStrokeVertexKey &lhs, const RGBStrokeVertexKey &rhs) +{ + if (lhs.x != rhs.x) + return lhs.x < rhs.x; + if (lhs.y != rhs.y) + return lhs.y < rhs.y; + return lhs.z < rhs.z; +} + +static RGBStrokeVertexKey rgb_stroke_vertex_key(const Vec3f &point) +{ + auto key = [](float value) { + return std::isfinite(value) ? static_cast(std::llround(double(value) * 100000.0)) : 0ll; + }; + return { key(point.x()), key(point.y()), key(point.z()) }; +} + +struct RGBStrokeBoundaryEdges +{ + std::unordered_map> by_source_triangle; + std::vector all; +}; + +static RGBStrokeBoundaryEdges build_rgb_stroke_boundary_edges( + const std::vector &stroke_facets) +{ + std::unordered_map edges; + edges.reserve(stroke_facets.size() * 3); + + auto add_edge = [&edges](const TriangleSelector::FacetStateTriangle &facet, const Vec3f &a, const Vec3f &b) { + RGBStrokeEdgeKey key { rgb_stroke_vertex_key(a), rgb_stroke_vertex_key(b) }; + if (rgb_stroke_vertex_key_less(key.b, key.a)) + std::swap(key.a, key.b); + + RGBStrokeEdgeData &data = edges[key]; + ++data.count; + if (data.count == 1) + data.edge = { facet.source_triangle, a, b }; + }; + + for (const TriangleSelector::FacetStateTriangle &facet : stroke_facets) { + add_edge(facet, facet.vertices[0], facet.vertices[1]); + add_edge(facet, facet.vertices[1], facet.vertices[2]); + add_edge(facet, facet.vertices[2], facet.vertices[0]); + } + + RGBStrokeBoundaryEdges boundary_edges; + boundary_edges.all.reserve(edges.size()); + for (const auto &edge : edges) { + if (edge.second.count != 1) + continue; + boundary_edges.by_source_triangle[edge.second.edge.source_triangle].emplace_back(edge.second.edge); + boundary_edges.all.emplace_back(edge.second.edge); + } + return boundary_edges; +} + +static float distance_to_segment(const Vec3f &point, const Vec3f &a, const Vec3f &b) +{ + const Vec3f ab = b - a; + const float len2 = ab.squaredNorm(); + if (len2 <= EPSILON) + return (point - a).norm(); + const float t = std::clamp((point - a).dot(ab) / len2, 0.f, 1.f); + return (point - (a + ab * t)).norm(); +} + +static float sample_rgb_stroke_alpha(const std::vector &stroke_facets, + const std::unordered_map> &stroke_by_source_triangle, + const RGBStrokeBoundaryEdges &stroke_boundary_edges, + int source_triangle, + const Vec3f &point, + float hardness, + float opacity, + float brush_radius) +{ + if (opacity <= 0.f) + return 0.f; + + auto found = stroke_by_source_triangle.find(source_triangle); + if (found == stroke_by_source_triangle.end()) + return 0.f; + + const float tolerance = -1e-4f; + bool inside_stroke = false; + for (const size_t facet_idx : found->second) { + if (facet_idx >= stroke_facets.size()) + continue; + + const TriangleSelector::FacetStateTriangle &facet = stroke_facets[facet_idx]; + Vec3f weights = Vec3f::Zero(); + if (!barycentric_weights_for_region_vertex_colors(point, facet.vertices[0], facet.vertices[1], facet.vertices[2], weights)) + continue; + if (weights.x() < tolerance || weights.y() < tolerance || weights.z() < tolerance) + continue; + inside_stroke = true; + break; + } + + if (!inside_stroke) + return 0.f; + + hardness = std::clamp(hardness, 0.f, 1.f); + opacity = std::clamp(opacity, 0.f, 1.f); + const float fade_width = brush_radius * (1.f - hardness); + if (fade_width <= EPSILON) + return opacity; + + if (stroke_boundary_edges.all.empty()) + return opacity; + + float boundary_distance = std::numeric_limits::max(); + auto boundary_found = stroke_boundary_edges.by_source_triangle.find(source_triangle); + if (boundary_found != stroke_boundary_edges.by_source_triangle.end()) + for (const RGBStrokeBoundaryEdge &edge : boundary_found->second) + boundary_distance = std::min(boundary_distance, distance_to_segment(point, edge.a, edge.b)); + + if (!std::isfinite(boundary_distance) || boundary_distance > fade_width) { + for (const RGBStrokeBoundaryEdge &edge : stroke_boundary_edges.all) + boundary_distance = std::min(boundary_distance, distance_to_segment(point, edge.a, edge.b)); + } + + if (!std::isfinite(boundary_distance)) + return opacity; + + const float t = std::clamp(boundary_distance / fade_width, 0.f, 1.f); + const float soft_alpha = t * t * (3.f - 2.f * t); + return opacity * soft_alpha; +} + +static bool apply_rgb_stroke_to_volume(ModelVolume &volume, + const std::vector &stroke_facets, + const ColorRGBA &brush_color, + float hardness, + float opacity, + float brush_radius) +{ + if (stroke_facets.empty()) + return false; + + std::vector existing_facets; + volume.texture_mapping_color_facets.get_facet_triangles(volume, existing_facets); + std::unordered_map> existing_by_source_triangle; + existing_by_source_triangle.reserve(existing_facets.size()); + for (size_t idx = 0; idx < existing_facets.size(); ++idx) + existing_by_source_triangle[existing_facets[idx].source_triangle].emplace_back(idx); + + std::unordered_map> stroke_by_source_triangle; + stroke_by_source_triangle.reserve(stroke_facets.size()); + for (size_t idx = 0; idx < stroke_facets.size(); ++idx) + stroke_by_source_triangle[stroke_facets[idx].source_triangle].emplace_back(idx); + RGBStrokeBoundaryEdges stroke_boundary_edges = build_rgb_stroke_boundary_edges(stroke_facets); + + const ColorRGBA background = rgb_metadata_background_color(volume.texture_mapping_color_facets); + const uint32_t brush_packed = pack_vertex_color_rgba(brush_color); + TextureMappingColorSampler sampler = [&existing_facets, + &existing_by_source_triangle, + &stroke_facets, + &stroke_by_source_triangle, + &stroke_boundary_edges, + background, + brush_color, + brush_packed, + hardness, + opacity, + brush_radius](size_t tri_idx, const Vec3f &point, const Vec3f &) { + ColorRGBA source_color = background; + if (std::optional sampled = + sample_rgb_color_facets(existing_facets, existing_by_source_triangle, int(tri_idx), point)) { + source_color = *sampled; + } + + const float alpha = sample_rgb_stroke_alpha(stroke_facets, + stroke_by_source_triangle, + stroke_boundary_edges, + int(tri_idx), + point, + hardness, + opacity, + brush_radius); + if (alpha <= 0.f) + return pack_vertex_color_rgba(source_color); + if (alpha >= 1.f) + return brush_packed; + + return pack_vertex_color_rgba(ColorRGBA(source_color.r() * (1.f - alpha) + brush_color.r() * alpha, + source_color.g() * (1.f - alpha) + brush_color.g() * alpha, + source_color.b() * (1.f - alpha) + brush_color.b() * alpha, + source_color.a() * (1.f - alpha) + brush_color.a() * alpha)); + }; + + const float mesh_span = mesh_max_axis_span(volume.mesh().its); + const int safe_max_depth = texture_mapping_depth_for_budget(volume.mesh().its.indices.size(), 7, 1800000); + TextureMappingColorSubdivisionDepths subdivision_depths = + [mesh_span, safe_max_depth, &stroke_by_source_triangle](size_t tri_idx, const std::array &vertices) { + const int base_depth = texture_mapping_depth_from_span(triangle_max_edge_length(vertices), + std::max(mesh_span / 220.f, 0.18f), + std::min(6, safe_max_depth)); + if (stroke_by_source_triangle.find(int(tri_idx)) != stroke_by_source_triangle.end()) + return std::make_pair(std::min(std::max(base_depth, 4), safe_max_depth), safe_max_depth); + return std::make_pair(base_depth, safe_max_depth); + }; + + return volume.texture_mapping_color_facets.set_from_triangle_sampler(volume, sampler, safe_max_depth, 0.012f, subdivision_depths); +} + +static bool initialize_volume_rgb_data(ModelVolume &volume, const ColorRGBA &background) +{ + if (volume.mesh().its.indices.empty() || volume.mesh().its.vertices.empty()) + return false; + + const uint32_t packed = pack_vertex_color_rgba(background); + TextureMappingColorSampler sampler = [packed](size_t, const Vec3f &, const Vec3f &) { return packed; }; + const bool changed = volume.texture_mapping_color_facets.set_from_triangle_sampler(volume, sampler, 0, 0.f); + volume.texture_mapping_color_facets.set_metadata_json(rgb_metadata_json(background)); + return changed; +} + +struct ProjectionContext +{ + Matrix4d view_projection = Matrix4d::Identity(); + int canvas_width = 1; + int canvas_height = 1; + float overlay_left = 0.f; + float overlay_top = 0.f; + float overlay_width = 0.f; + float overlay_height = 0.f; + const std::vector *image_rgba = nullptr; + uint32_t image_width = 0; + uint32_t image_height = 0; + float image_opacity = 1.f; + bool apply_transparency_as_background = false; +}; + +struct VolumeColorSource +{ + std::vector rgb_facets; + std::unordered_map> rgb_by_source_triangle; +}; + +static ColorRGBA sample_rgba_bilinear_clamped(const std::vector &rgba, uint32_t width, uint32_t height, float u, float v) +{ + if (width == 0 || height == 0 || rgba.size() < size_t(width) * size_t(height) * 4) + return ColorRGBA(1.f, 1.f, 1.f, 1.f); + + u = std::clamp(u, 0.f, 1.f); + v = std::clamp(v, 0.f, 1.f); + 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 channel = [&rgba, width](size_t sx, size_t sy, size_t ch) { + return float(rgba[(sy * size_t(width) + sx) * 4 + ch]) / 255.f; + }; + 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); + return std::clamp((c00 + (c10 - c00) * tx) + ((c01 + (c11 - c01) * tx) - (c00 + (c10 - c00) * tx)) * 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); + return std::clamp((c00 + (c10 - c00) * tx) + ((c01 + (c11 - c01) * tx) - (c00 + (c10 - c00) * tx)) * ty, 0.f, 1.f); + }; + + const float a = blend_channel(3); + if (a <= 0.f) + return ColorRGBA(blend_channel(0), blend_channel(1), blend_channel(2), 0.f); + return ColorRGBA(std::clamp(blend_premultiplied_channel(0) / a, 0.f, 1.f), + std::clamp(blend_premultiplied_channel(1) / a, 0.f, 1.f), + std::clamp(blend_premultiplied_channel(2) / a, 0.f, 1.f), + a); +} + +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); + if (alpha <= 0.f) + return base; + const float out_alpha = std::clamp(alpha + base.a() * (1.f - alpha), 0.f, 1.f); + if (out_alpha <= EPSILON) + return ColorRGBA(overlay.r(), overlay.g(), overlay.b(), 0.f); + return ColorRGBA(base.r() * (1.f - alpha) + overlay.r() * alpha, + base.g() * (1.f - alpha) + overlay.g() * alpha, + base.b() * (1.f - alpha) + overlay.b() * alpha, + out_alpha); +} + +static float projection_overlay_alpha(const ColorRGBA &overlay, const ProjectionContext &context) +{ + return std::clamp(overlay.a(), 0.f, 1.f) * std::clamp(context.image_opacity, 0.f, 1.f); +} + +static bool projection_overlay_has_paintable_alpha(const ColorRGBA &overlay, const ProjectionContext &context) +{ + return projection_overlay_alpha(overlay, context) > 0.5f / 255.f; +} + +static ColorRGBA apply_projection_color(const ColorRGBA &base, const ColorRGBA &overlay, const ProjectionContext &context, bool image_texture_target) +{ + const float opacity = std::clamp(context.image_opacity, 0.f, 1.f); + if (!context.apply_transparency_as_background) + return blend_projection_color(base, overlay, opacity); + + const float alpha = std::clamp(overlay.a(), 0.f, 1.f) * opacity; + if (image_texture_target) + return ColorRGBA(overlay.r() * alpha, overlay.g() * alpha, overlay.b() * alpha, 1.f); + return ColorRGBA(overlay.r(), overlay.g(), overlay.b(), alpha); +} + +static bool wx_image_to_rgba(const wxImage &image, std::vector &rgba, uint32_t &width, uint32_t &height) +{ + if (!image.IsOk() || image.GetWidth() <= 0 || image.GetHeight() <= 0) + return false; + + width = uint32_t(image.GetWidth()); + height = uint32_t(image.GetHeight()); + rgba.assign(size_t(width) * size_t(height) * 4, 255); + + const unsigned char *rgb = image.GetData(); + const unsigned char *alpha = image.HasAlpha() ? image.GetAlpha() : nullptr; + const bool has_mask = image.HasMask(); + const int mask_r = has_mask ? image.GetMaskRed() : -1; + const int mask_g = has_mask ? image.GetMaskGreen() : -1; + const int mask_b = has_mask ? image.GetMaskBlue() : -1; + if (rgb == nullptr) + return false; + + for (size_t idx = 0; idx < size_t(width) * size_t(height); ++idx) { + rgba[idx * 4 + 0] = rgb[idx * 3 + 0]; + rgba[idx * 4 + 1] = rgb[idx * 3 + 1]; + rgba[idx * 4 + 2] = rgb[idx * 3 + 2]; + rgba[idx * 4 + 3] = + has_mask && + int(rgb[idx * 3 + 0]) == mask_r && + int(rgb[idx * 3 + 1]) == mask_g && + int(rgb[idx * 3 + 2]) == mask_b ? + 0 : + (alpha == nullptr ? 255 : alpha[idx]); + } + return true; +} + +static bool project_point_to_screen(const ProjectionContext &context, const Vec3d &world_point, Vec2f &screen, float *ndc_z = nullptr) +{ + const Vec4d clip = context.view_projection * Vec4d(world_point.x(), world_point.y(), world_point.z(), 1.0); + if (clip.w() <= 0.0) + return false; + + const Vec3d ndc = clip.head<3>() / clip.w(); + if (ndc.x() < -1.0 || ndc.x() > 1.0 || ndc.y() < -1.0 || ndc.y() > 1.0 || ndc.z() < -1.0 || ndc.z() > 1.0) + return false; + + screen.x() = float((ndc.x() * 0.5 + 0.5) * double(context.canvas_width)); + screen.y() = float((1.0 - (ndc.y() * 0.5 + 0.5)) * double(context.canvas_height)); + if (ndc_z != nullptr) + *ndc_z = float(ndc.z()); + return true; +} + +static std::optional projected_image_color_at_point(const ProjectionContext &context, + const Transform3d &world_matrix, + const Vec3f &point) +{ + if (context.image_rgba == nullptr || context.overlay_width <= 0.f || context.overlay_height <= 0.f) + return std::nullopt; + + Vec2f screen = Vec2f::Zero(); + if (!project_point_to_screen(context, world_matrix * point.cast(), screen)) + return std::nullopt; + if (screen.x() < context.overlay_left || + screen.y() < context.overlay_top || + screen.x() > context.overlay_left + context.overlay_width || + screen.y() > context.overlay_top + context.overlay_height) + return std::nullopt; + + const float u = (screen.x() - context.overlay_left) / context.overlay_width; + const float v = (screen.y() - context.overlay_top) / context.overlay_height; + return sample_rgba_bilinear_clamped(*context.image_rgba, context.image_width, context.image_height, u, v); +} + +static bool projection_triangle_intersects_overlay(const ProjectionContext &context, + const Transform3d &world_matrix, + const std::array &vertices) +{ + float min_x = std::numeric_limits::max(); + float min_y = std::numeric_limits::max(); + float max_x = std::numeric_limits::lowest(); + float max_y = std::numeric_limits::lowest(); + bool any_projected = false; + + for (const Vec3f &vertex : vertices) { + Vec2f screen = Vec2f::Zero(); + if (!project_point_to_screen(context, world_matrix * vertex.cast(), screen)) + continue; + min_x = std::min(min_x, screen.x()); + min_y = std::min(min_y, screen.y()); + max_x = std::max(max_x, screen.x()); + max_y = std::max(max_y, screen.y()); + any_projected = true; + } + + if (!any_projected) + return false; + return max_x >= context.overlay_left && + min_x <= context.overlay_left + context.overlay_width && + max_y >= context.overlay_top && + min_y <= context.overlay_top + context.overlay_height; +} + +static bool barycentric_weights_2d(const Vec2f &point, const Vec2f &a, const Vec2f &b, const Vec2f &c, Vec3f &weights) +{ + const Vec2f v0 = b - a; + const Vec2f v1 = c - a; + const Vec2f v2 = point - a; + 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::abs(denom) <= EPSILON) + return false; + + weights.y() = (d11 * d20 - d01 * d21) / denom; + weights.z() = (d00 * d21 - d01 * d20) / denom; + weights.x() = 1.f - weights.y() - weights.z(); + return std::isfinite(weights.x()) && std::isfinite(weights.y()) && std::isfinite(weights.z()); +} + +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 <= EPSILON) + return Vec3f(1.f / 3.f, 1.f / 3.f, 1.f / 3.f); + weights /= sum; + return weights; +} + +static float distance_to_segment_2d(const Vec2f &point, const Vec2f &a, const Vec2f &b) +{ + const Vec2f ab = b - a; + const float len2 = ab.squaredNorm(); + if (len2 <= EPSILON) + return (point - a).norm(); + const float t = std::clamp((point - a).dot(ab) / len2, 0.f, 1.f); + return (point - (a + ab * t)).norm(); +} + +static bool conservative_barycentric_weights_2d(const Vec2f &point, + const Vec2f &a, + const Vec2f &b, + const Vec2f &c, + float tolerance, + Vec3f &weights) +{ + if (!barycentric_weights_2d(point, a, b, c, weights)) + return false; + if (weights.x() >= -1e-4f && weights.y() >= -1e-4f && weights.z() >= -1e-4f) + return true; + + const float distance = std::min({ distance_to_segment_2d(point, a, b), + distance_to_segment_2d(point, b, c), + distance_to_segment_2d(point, c, a) }); + if (distance > tolerance) + return false; + + weights = normalized_nonnegative_barycentric(weights); + return true; +} + +static std::array unwrap_projection_uvs(std::array uvs) +{ + auto unwrap_axis = [&uvs](bool use_u_axis) mutable { + std::array values = { + use_u_axis ? uvs[0].x() : uvs[0].y(), + use_u_axis ? uvs[1].x() : uvs[1].y(), + use_u_axis ? uvs[2].x() : uvs[2].y() + }; + + const float min_value = std::min({ values[0], values[1], values[2] }); + const float max_value = std::max({ values[0], values[1], values[2] }); + if (max_value - min_value <= 0.5f) + return; + + for (float &value : values) + if (value < 0.5f) + value += 1.f; + + if (use_u_axis) { + uvs[0].x() = values[0]; + uvs[1].x() = values[1]; + uvs[2].x() = values[2]; + } else { + uvs[0].y() = values[0]; + uvs[1].y() = values[1]; + uvs[2].y() = values[2]; + } + }; + + unwrap_axis(true); + unwrap_axis(false); + + const float min_u = std::min({ uvs[0].x(), uvs[1].x(), uvs[2].x() }); + const float min_v = std::min({ uvs[0].y(), uvs[1].y(), uvs[2].y() }); + const Vec2f offset(std::floor(min_u), std::floor(min_v)); + for (Vec2f &uv : uvs) + uv -= offset; + + return uvs; +} + +static uint32_t wrapped_texture_pixel(int value, uint32_t size) +{ + if (size == 0) + return 0; + int wrapped = value % int(size); + if (wrapped < 0) + wrapped += int(size); + return uint32_t(wrapped); +} + +static VolumeColorSource build_volume_color_source(const ModelVolume &volume) +{ + VolumeColorSource source; + if (!volume.texture_mapping_color_facets.empty()) { + volume.texture_mapping_color_facets.get_facet_triangles(volume, source.rgb_facets); + source.rgb_by_source_triangle.reserve(source.rgb_facets.size()); + for (size_t idx = 0; idx < source.rgb_facets.size(); ++idx) + source.rgb_by_source_triangle[source.rgb_facets[idx].source_triangle].emplace_back(idx); + } + return source; +} + +static ColorRGBA sample_volume_color_source(const ModelVolume &volume, + const VolumeColorSource &source, + size_t tri_idx, + const Vec3f &point, + const Vec3f &barycentric, + bool use_image_texture = true, + const ColorRGBA *fallback_color = nullptr) +{ + if (!volume.texture_mapping_color_facets.empty()) { + if (std::optional color = sample_rgb_color_facets(source.rgb_facets, + source.rgb_by_source_triangle, + int(tri_idx), + point)) + return *color; + return rgb_metadata_background_color(volume.texture_mapping_color_facets); + } + + const indexed_triangle_set &its = volume.mesh().its; + if (use_image_texture && model_volume_has_bakeable_image_texture_data(&volume) && tri_idx < volume.imported_texture_uv_valid.size()) { + const size_t uv_offset = tri_idx * 6; + if (volume.imported_texture_uv_valid[tri_idx] != 0 && uv_offset + 5 < volume.imported_texture_uvs_per_face.size()) { + const Vec2f uv0(volume.imported_texture_uvs_per_face[uv_offset + 0], volume.imported_texture_uvs_per_face[uv_offset + 1]); + const Vec2f uv1(volume.imported_texture_uvs_per_face[uv_offset + 2], volume.imported_texture_uvs_per_face[uv_offset + 3]); + const Vec2f uv2(volume.imported_texture_uvs_per_face[uv_offset + 4], volume.imported_texture_uvs_per_face[uv_offset + 5]); + const Vec2f uv = uv0 * barycentric.x() + uv1 * barycentric.y() + uv2 * barycentric.z(); + return sample_texture_rgba_for_vertex_bake(volume.imported_texture_rgba, + volume.imported_texture_width, + volume.imported_texture_height, + uv); + } + } + + if (volume.imported_vertex_colors_rgba.size() == its.vertices.size() && tri_idx < its.indices.size()) { + const stl_triangle_vertex_indices &tri = its.indices[tri_idx]; + if (tri[0] >= 0 && tri[1] >= 0 && tri[2] >= 0 && + size_t(tri[0]) < volume.imported_vertex_colors_rgba.size() && + size_t(tri[1]) < volume.imported_vertex_colors_rgba.size() && + size_t(tri[2]) < volume.imported_vertex_colors_rgba.size()) { + const ColorRGBA c0 = unpack_vertex_color_rgba_for_conversion(volume.imported_vertex_colors_rgba[size_t(tri[0])]); + const ColorRGBA c1 = unpack_vertex_color_rgba_for_conversion(volume.imported_vertex_colors_rgba[size_t(tri[1])]); + const ColorRGBA c2 = unpack_vertex_color_rgba_for_conversion(volume.imported_vertex_colors_rgba[size_t(tri[2])]); + return ColorRGBA(c0.r() * barycentric.x() + c1.r() * barycentric.y() + c2.r() * barycentric.z(), + c0.g() * barycentric.x() + c1.g() * barycentric.y() + c2.g() * barycentric.z(), + c0.b() * barycentric.x() + c1.b() * barycentric.y() + c2.b() * barycentric.z(), + c0.a() * barycentric.x() + c1.a() * barycentric.y() + c2.a() * barycentric.z()); + } + } + + return fallback_color != nullptr ? *fallback_color : ColorRGBA(1.f, 1.f, 1.f, 1.f); +} + +static bool initialize_volume_rgb_data_from_current_surface_color(ModelVolume &volume, const ColorRGBA &fallback_color) +{ + const indexed_triangle_set &its = volume.mesh().its; + if (its.indices.empty() || its.vertices.empty()) + return false; + + const bool has_image_texture = model_volume_has_bakeable_image_texture_data(&volume); + const bool has_vertex_colors = volume.imported_vertex_colors_rgba.size() == its.vertices.size(); + if (!has_image_texture && !has_vertex_colors) + return initialize_volume_rgb_data(volume, fallback_color); + + const VolumeColorSource source = build_volume_color_source(volume); + TextureMappingColorSampler sampler = [&volume, source, fallback_color](size_t tri_idx, const Vec3f &point, const Vec3f &barycentric) { + return pack_vertex_color_rgba(sample_volume_color_source(volume, source, tri_idx, point, barycentric, true, &fallback_color)); + }; + + bool changed = false; + if (has_image_texture) { + const int safe_max_depth = texture_mapping_depth_for_budget(its.indices.size(), 7, 3200000); + TextureMappingColorSubdivisionDepths subdivision_depths = [&volume, safe_max_depth](size_t tri_idx, const std::array &) { + const int depth = texture_mapping_depth_from_span(texture_triangle_uv_pixel_span(&volume, tri_idx), 8.f, safe_max_depth); + return std::make_pair(depth, depth); + }; + changed = volume.texture_mapping_color_facets.set_from_triangle_sampler(volume, + sampler, + safe_max_depth, + 0.015f, + subdivision_depths); + } else { + const float target_edge = std::max(mesh_max_axis_span(its) / 160.f, 0.25f); + TextureMappingColorSubdivisionDepths subdivision_depths = [target_edge](size_t, const std::array &vertices) { + const int depth = texture_mapping_depth_from_span(triangle_max_edge_length(vertices), target_edge, 5); + return std::make_pair(depth, depth); + }; + changed = volume.texture_mapping_color_facets.set_from_triangle_sampler(volume, sampler, 5, 0.025f, subdivision_depths); + } + + if (changed && volume.texture_mapping_color_facets.metadata_json().empty()) + volume.texture_mapping_color_facets.set_metadata_json(rgb_metadata_json(fallback_color)); + return changed; +} + +static ColorRGBA projection_base_color_for_volume(const ModelVolume &volume) +{ + std::vector colors = get_extruders_colors(); + if (!colors.empty()) { + int extruder_idx = volume.extruder_id() > 0 ? volume.extruder_id() - 1 : 0; + extruder_idx = std::clamp(extruder_idx, 0, int(colors.size() - 1)); + ColorRGBA color = colors[size_t(extruder_idx)]; + color.a(1.f); + return color; + } + return ColorRGBA(0.15f, 0.65f, 0.6f, 1.f); +} + +static uint32_t projection_texture_size_for_triangles(size_t triangle_count) +{ + const uint32_t grid = uint32_t(std::max(1, size_t(std::ceil(std::sqrt(double(std::max(triangle_count, 1))))))); + uint32_t size = 256; + while (size < grid * 8 && size < 4096) + size *= 2; + return std::clamp(size, 256, 4096); +} + +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; + const uint8_t r = uint8_t(std::clamp(color.r(), 0.f, 1.f) * 255.f + 0.5f); + const uint8_t g = uint8_t(std::clamp(color.g(), 0.f, 1.f) * 255.f + 0.5f); + const uint8_t b = uint8_t(std::clamp(color.b(), 0.f, 1.f) * 255.f + 0.5f); + const uint8_t a = uint8_t(std::clamp(color.a(), 0.f, 1.f) * 255.f + 0.5f); + if (rgba[idx + 0] == r && rgba[idx + 1] == g && rgba[idx + 2] == b && rgba[idx + 3] == a) + return false; + rgba[idx + 0] = r; + rgba[idx + 1] = g; + rgba[idx + 2] = b; + rgba[idx + 3] = a; + return true; +} + +static ColorRGBA read_rgba_pixel(const std::vector &rgba, uint32_t width, uint32_t x, uint32_t y) +{ + if (width == 0) + return ColorRGBA(1.f, 1.f, 1.f, 1.f); + const size_t idx = (size_t(y) * size_t(width) + size_t(x)) * 4; + if (idx + 3 >= rgba.size()) + return ColorRGBA(1.f, 1.f, 1.f, 1.f); + return ColorRGBA(float(rgba[idx + 0]) / 255.f, + float(rgba[idx + 1]) / 255.f, + float(rgba[idx + 2]) / 255.f, + float(rgba[idx + 3]) / 255.f); +} + +static Transform3d projection_world_matrix_for_volume(const GLCanvas3D &parent, + const ModelObject *object, + const ModelVolume *volume, + int instance_idx) +{ + if (object == nullptr || volume == nullptr || object->instances.empty()) + return Transform3d::Identity(); + + instance_idx = std::clamp(instance_idx, 0, int(object->instances.size() - 1)); + const ModelInstance *instance = object->instances[size_t(instance_idx)]; + if (parent.get_canvas_type() == GLCanvas3D::CanvasAssembleView) + return instance->get_assemble_transformation().get_matrix() * volume->get_matrix(); + return instance->get_transformation().get_matrix() * volume->get_matrix(); +} + +struct ProjectionVisibility +{ + int width = 0; + int height = 0; + float left = 0.f; + float top = 0.f; + float scale = 1.f; + std::vector depth; +}; + +static bool projection_visibility_valid(const ProjectionVisibility &visibility) +{ + return visibility.width > 0 && + visibility.height > 0 && + visibility.depth.size() == size_t(visibility.width) * size_t(visibility.height); +} + +static ProjectionVisibility build_projection_visibility(const ProjectionContext &context, + const GLCanvas3D &parent, + const ModelObject *object, + int instance_idx) +{ + ProjectionVisibility visibility; + if (object == nullptr || context.overlay_width <= 0.f || context.overlay_height <= 0.f) + return visibility; + + const float max_dim = std::max(context.overlay_width, context.overlay_height); + visibility.scale = max_dim > 2048.f ? 2048.f / max_dim : 1.f; + visibility.width = std::max(1, int(std::ceil(context.overlay_width * visibility.scale))); + visibility.height = std::max(1, int(std::ceil(context.overlay_height * visibility.scale))); + visibility.left = context.overlay_left; + visibility.top = context.overlay_top; + visibility.depth.assign(size_t(visibility.width) * size_t(visibility.height), std::numeric_limits::max()); + + auto rasterize_triangle = [&visibility](const std::array &screen, const std::array &depths) { + const float min_screen_x = std::min({ screen[0].x(), screen[1].x(), screen[2].x() }); + const float max_screen_x = std::max({ screen[0].x(), screen[1].x(), screen[2].x() }); + const float min_screen_y = std::min({ screen[0].y(), screen[1].y(), screen[2].y() }); + const float max_screen_y = std::max({ screen[0].y(), screen[1].y(), screen[2].y() }); + const int min_x = std::clamp(int(std::floor((min_screen_x - visibility.left) * visibility.scale)) - 1, 0, visibility.width - 1); + const int max_x = std::clamp(int(std::ceil((max_screen_x - visibility.left) * visibility.scale)) + 1, 0, visibility.width - 1); + const int min_y = std::clamp(int(std::floor((min_screen_y - visibility.top) * visibility.scale)) - 1, 0, visibility.height - 1); + const int max_y = std::clamp(int(std::ceil((max_screen_y - visibility.top) * visibility.scale)) + 1, 0, visibility.height - 1); + + for (int y = min_y; y <= max_y; ++y) { + for (int x = min_x; x <= max_x; ++x) { + const Vec2f pixel(visibility.left + (float(x) + 0.5f) / visibility.scale, + visibility.top + (float(y) + 0.5f) / visibility.scale); + Vec3f weights = Vec3f::Zero(); + if (!barycentric_weights_2d(pixel, screen[0], screen[1], screen[2], weights)) + continue; + if (weights.x() < -1e-4f || weights.y() < -1e-4f || weights.z() < -1e-4f) + continue; + + const float depth = depths[0] * weights.x() + depths[1] * weights.y() + depths[2] * weights.z(); + const size_t idx = size_t(y) * size_t(visibility.width) + size_t(x); + visibility.depth[idx] = std::min(visibility.depth[idx], depth); + } + } + }; + + for (const ModelVolume *volume : object->volumes) { + if (volume == nullptr || !volume->is_model_part()) + continue; + + const indexed_triangle_set &its = volume->mesh().its; + if (its.vertices.empty() || its.indices.empty()) + continue; + + const Transform3d world_matrix = projection_world_matrix_for_volume(parent, object, volume, instance_idx); + for (const stl_triangle_vertex_indices &tri : its.indices) { + 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 std::array vertices = { + its.vertices[size_t(tri[0])].cast(), + its.vertices[size_t(tri[1])].cast(), + its.vertices[size_t(tri[2])].cast() + }; + if (!projection_triangle_intersects_overlay(context, world_matrix, vertices)) + continue; + + std::array screen; + std::array depths; + bool projected = true; + for (size_t idx = 0; idx < vertices.size(); ++idx) { + if (!project_point_to_screen(context, world_matrix * vertices[idx].cast(), screen[idx], &depths[idx])) { + projected = false; + break; + } + } + if (projected) + rasterize_triangle(screen, depths); + } + } + + return visibility; +} + +static bool projection_point_is_visible(const ProjectionVisibility &visibility, + const ProjectionContext &context, + const Transform3d &world_matrix, + const Vec3f &point) +{ + if (!projection_visibility_valid(visibility)) + return true; + + Vec2f screen = Vec2f::Zero(); + float depth = 0.f; + if (!project_point_to_screen(context, world_matrix * point.cast(), screen, &depth)) + return false; + + const int x = int(std::floor((screen.x() - visibility.left) * visibility.scale)); + const int y = int(std::floor((screen.y() - visibility.top) * visibility.scale)); + if (x < 0 || y < 0 || x >= visibility.width || y >= visibility.height) + return false; + + const float nearest = visibility.depth[size_t(y) * size_t(visibility.width) + size_t(x)]; + if (!std::isfinite(nearest)) + return false; + return depth <= nearest + 2e-3f; +} + +void GLGizmoMmuSegmentation::init_extruders_data(const std::vector &extruder_colors) +{ + const unsigned int old_selected_filament_id = + m_selected_extruder_idx < m_display_filament_ids.size() ? m_display_filament_ids[m_selected_extruder_idx] : + (m_selected_extruder_idx < m_extruders_colors.size() ? unsigned(m_selected_extruder_idx + 1) : 0); + + m_extruders_colors = extruder_colors; + m_display_filament_ids = get_display_filament_ids(m_extruders_colors.size()); + + m_selected_extruder_idx = 0; + if (!m_display_filament_ids.empty()) { + auto selected_it = std::find(m_display_filament_ids.begin(), m_display_filament_ids.end(), old_selected_filament_id); + if (selected_it != m_display_filament_ids.end()) + m_selected_extruder_idx = size_t(std::distance(m_display_filament_ids.begin(), selected_it)); + } + + // keep remap table consistent with current extruder count + m_extruder_remap.resize(m_display_filament_ids.size()); + for (size_t i = 0; i < m_extruder_remap.size(); ++i) + m_extruder_remap[i] = i; } void GLGizmoMmuSegmentation::init_extruders_data() { - m_extruders_colors = wxGetApp().plater()->get_extruders_colors(); - m_selected_extruder_idx = 0; - - // keep remap table consistent with current extruder count - m_extruder_remap.resize(m_extruders_colors.size()); - for (size_t i = 0; i < m_extruder_remap.size(); ++i) - m_extruder_remap[i] = i; + init_extruders_data(get_extruders_colors()); } bool GLGizmoMmuSegmentation::on_init() @@ -248,61 +1452,55 @@ bool GLGizmoMmuSegmentation::on_init() // BBS m_shortcut_key = WXK_CONTROL_N; - const wxString ctrl = GUI::shortkey_ctrl_prefix(); - const wxString alt = GUI::shortkey_alt_prefix(); - const wxString shift = GUI::shortkey_shift_prefix(); + // FIXME: maybe should be using GUI::shortkey_ctrl_prefix() or equivalent? + const wxString ctrl = _L("Ctrl+"); + // FIXME: maybe should be using GUI::shortkey_alt_prefix() or equivalent? + const wxString alt = _L("Alt+"); + const wxString shift = _L("Shift+"); - m_desc["clipping_of_view"] = _L("Section view"); - m_desc["reset_direction"] = _L("Reset direction"); - m_desc["cursor_size"] = _L("Brush size"); - m_desc["cursor_type"] = _L("Brush shape"); - m_desc["paint"] = _L("Paint"); - m_desc["erase"] = _L("Erase"); - m_desc["shortcut_key"] = _L("Choose filament"); - m_desc["edge_detection"] = _L("Edge detection"); - m_desc["gap_area"] = _L("Gap area"); - m_desc["perform"] = _L("Perform"); - m_desc["remove_all"] = _L("Erase all painting"); - m_desc["circle"] = _L("Circle"); - m_desc["sphere"] = _L("Sphere"); - m_desc["pointer"] = _L("Triangles"); - m_desc["filaments"] = _L("Filaments"); - m_desc["tool_type"] = _L("Tool type"); - m_desc["tool_brush"] = _L("Brush"); - m_desc["tool_smart_fill"] = _L("Smart fill"); - m_desc["tool_bucket_fill"] = _L("Bucket fill"); - m_desc["smart_fill_angle"] = _L("Smart fill angle"); - m_desc["height_range"] = _L("Height range"); - m_desc["toggle_wireframe"] = _L("Toggle Wireframe"); - m_desc["perform_remap"] = _L("Remap filaments"); - m_desc["remap"] = _L("Remap"); - m_desc["cancel_remap"] = _L("Cancel"); + m_desc["clipping_of_view_caption"] = alt + _L("Mouse wheel"); + m_desc["clipping_of_view"] = _L("Section view"); + m_desc["reset_direction"] = _L("Reset direction"); + m_desc["cursor_size_caption"] = ctrl + _L("Mouse wheel"); + m_desc["cursor_size"] = _L("Pen size"); + m_desc["cursor_type"] = _L("Pen shape"); - std::pair paint_shortcut = {_L("Left mouse button"), m_desc["paint"]}; - std::pair erase_shortcut = {shift + _L("Left mouse button"), m_desc["erase"]}; - std::pair clipping_shortcut = {alt + _L("Mouse wheel"), m_desc["clipping_of_view"]}; - std::pair toggle_wireframe_shortcut = {alt + shift + _L("Enter"), m_desc["toggle_wireframe"]}; + m_desc["paint_caption"] = _L("Left mouse button"); + m_desc["paint"] = _L("Paint"); + m_desc["erase_caption"] = shift + _L("Left mouse button"); + m_desc["erase"] = _L("Erase"); + m_desc["shortcut_key_caption"] = _L("Key 1~9"); + m_desc["shortcut_key"] = _L("Choose filament"); + m_desc["edge_detection"] = _L("Edge detection"); + m_desc["gap_area_caption"] = ctrl + _L("Mouse wheel"); + m_desc["gap_area"] = _L("Gap area"); + m_desc["perform"] = _L("Perform"); - m_shortcuts_brush = { - paint_shortcut, - erase_shortcut, - {ctrl + _L("Mouse wheel"), m_desc["cursor_size"]}, - clipping_shortcut, - toggle_wireframe_shortcut - }; + m_desc["remove_all"] = _L("Erase all painting"); + m_desc["circle"] = _L("Circle"); + m_desc["sphere"] = _L("Sphere"); + m_desc["pointer"] = _L("Triangles"); - m_shortcuts_bucket_fill = { - paint_shortcut, - erase_shortcut, - {ctrl + _L("Mouse wheel"), m_desc["smart_fill_angle"]}, - clipping_shortcut, - toggle_wireframe_shortcut - }; + m_desc["filaments"] = _L("Filaments"); + m_desc["tool_type"] = _L("Tool type"); + m_desc["tool_brush"] = _L("Brush"); + m_desc["tool_smart_fill"] = _L("Smart fill"); + m_desc["tool_bucket_fill"] = _L("Bucket fill"); - m_shortcuts_gap_fill = { - {ctrl + _L("Mouse wheel"), m_desc["gap_area"]}, - toggle_wireframe_shortcut - }; + m_desc["smart_fill_angle_caption"] = ctrl + _L("Mouse wheel"); + m_desc["smart_fill_angle"] = _L("Smart fill angle"); + + m_desc["height_range_caption"] = ctrl + _L("Mouse wheel"); + m_desc["height_range"] = _L("Height range"); + + //add toggle wire frame hint + m_desc["toggle_wireframe_caption"] = alt + shift + _L("Enter"); + m_desc["toggle_wireframe"] = _L("Toggle Wireframe"); + + // Filament remapping descriptions + m_desc["perform_remap"] = _L("Remap filaments"); + m_desc["remap"] = _L("Remap"); + m_desc["cancel_remap"] = _L("Cancel"); init_extruders_data(); @@ -337,19 +1535,26 @@ void GLGizmoMmuSegmentation::data_changed(bool is_serializing) return; ModelObject* model_object = m_c->selection_info()->model_object(); - int prev_extruders_count = int(m_extruders_colors.size()); - if (prev_extruders_count != wxGetApp().filaments_cnt()) { - if (wxGetApp().filaments_cnt() > int(GLGizmoMmuSegmentation::EXTRUDERS_LIMIT)) + const std::vector current_extruder_colors = get_extruders_colors(); + const int prev_extruders_count = int(m_extruders_colors.size()); + const int current_extruders_count = int(current_extruder_colors.size()); + const std::vector current_display_filament_ids = get_display_filament_ids(current_extruder_colors.size()); + if (prev_extruders_count != current_extruders_count) { + if (current_extruder_colors.size() > GLGizmoMmuSegmentation::EXTRUDERS_LIMIT) show_notification_extruders_limit_exceeded(); - this->init_extruders_data(); + this->init_extruders_data(current_extruder_colors); // Reinitialize triangle selectors because of change of extruder count need also change the size of GLIndexedVertexArray - if (prev_extruders_count != wxGetApp().filaments_cnt()) + if (prev_extruders_count != current_extruders_count) this->init_model_triangle_selectors(); - } else if (wxGetApp().plater()->get_extruders_colors() != m_extruders_colors) { - this->init_extruders_data(); + } + else if (current_extruder_colors != m_extruders_colors) { + this->init_extruders_data(current_extruder_colors); this->update_triangle_selectors_colors(); } + else if (current_display_filament_ids != m_display_filament_ids) { + this->init_extruders_data(current_extruder_colors); + } else if (model_object != nullptr && get_extruder_id_for_volumes(*model_object) != m_volumes_extruder_idxs) { this->init_model_triangle_selectors(); } @@ -359,7 +1564,7 @@ void GLGizmoMmuSegmentation::data_changed(bool is_serializing) bool GLGizmoMmuSegmentation::on_number_key_down(int number) { int extruder_idx = number - 1; - if (extruder_idx < m_extruders_colors.size() && extruder_idx >= 0) + if (extruder_idx >= 0 && size_t(extruder_idx) < m_display_filament_ids.size()) m_selected_extruder_idx = extruder_idx; return true; @@ -399,14 +1604,14 @@ static void render_extruders_combo(const std::string& label, size_t& selection_idx) { assert(!extruders_colors.empty()); - assert(extruders_colors.size() == extruders_colors.size()); + assert(extruders.size() == extruders_colors.size()); size_t selection_out = selection_idx; // It is necessary to use BeginGroup(). Otherwise, when using SameLine() is called, then other items will be drawn inside the combobox. ImGui::BeginGroup(); ImVec2 combo_pos = ImGui::GetCursorScreenPos(); if (ImGui::BeginCombo(label.c_str(), "")) { - for (size_t extruder_idx = 0; extruder_idx < std::min(extruders.size(), GLGizmoMmuSegmentation::EXTRUDERS_LIMIT); ++extruder_idx) { + for (size_t extruder_idx = 0; extruder_idx < extruders.size(); ++extruder_idx) { ImGui::PushID(int(extruder_idx)); ImVec2 start_position = ImGui::GetCursorScreenPos(); @@ -445,22 +1650,48 @@ static void render_extruders_combo(const std::string& label, selection_idx = selection_out; } -void GLGizmoMmuSegmentation::render_tooltip_button(float x, float y) +void GLGizmoMmuSegmentation::show_tooltip_information(float caption_max, float x, float y) { - auto get_shortcuts = [this]() -> std::vector> { + ImTextureID normal_id = m_parent.get_gizmos_manager().get_icon_texture_id(GLGizmosManager::MENU_ICON_NAME::IC_TOOLBAR_TOOLTIP); + ImTextureID hover_id = m_parent.get_gizmos_manager().get_icon_texture_id(GLGizmosManager::MENU_ICON_NAME::IC_TOOLBAR_TOOLTIP_HOVER); + + caption_max += m_imgui->calc_text_size(std::string_view{": "}).x + 15.f; + + float scale = m_parent.get_scale(); + ImVec2 button_size = ImVec2(25 * scale, 25 * scale); // ORCA: Use exact resolution will prevent blur on icon + ImGui::PushStyleVar(ImGuiStyleVar_FrameBorderSize, 0.0f); + ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, {0, 0}); // ORCA: Dont add padding + ImGui::ImageButton3(normal_id, hover_id, button_size); + + if (ImGui::IsItemHovered()) { + ImGui::BeginTooltip2(ImVec2(x, y)); + auto draw_text_with_caption = [this, &caption_max](const wxString &caption, const wxString &text) { + m_imgui->text_colored(ImGuiWrapper::COL_ACTIVE, caption); + ImGui::SameLine(caption_max); + m_imgui->text_colored(ImGuiWrapper::COL_WINDOW_BG, text); + }; + + std::vector tip_items; switch (m_tool_type) { - case ToolType::BRUSH: return m_shortcuts_brush; - - case ToolType::BUCKET_FILL: - case ToolType::SMART_FILL: return m_shortcuts_bucket_fill; - - case ToolType::GAP_FILL: return m_shortcuts_gap_fill; - - default: return {}; + case ToolType::BRUSH: + tip_items = {"paint", "erase", "cursor_size", "clipping_of_view", "toggle_wireframe"}; + break; + case ToolType::BUCKET_FILL: + tip_items = {"paint", "erase", "smart_fill_angle", "clipping_of_view", "toggle_wireframe"}; + break; + case ToolType::SMART_FILL: + // TODO: + break; + case ToolType::GAP_FILL: + tip_items = {"gap_area", "toggle_wireframe"}; + break; + default: + break; } - }; - - GLGizmoUtils::render_tooltip_button(m_imgui, m_parent, get_shortcuts(), x, y); + for (const auto &t : tip_items) draw_text_with_caption(m_desc.at(t + "_caption") + ": ", m_desc.at(t)); + ImGui::EndTooltip(); + } + ImGui::PopStyleVar(2); } void GLGizmoMmuSegmentation::on_render_input_window(float x, float y, float bottom_limit) @@ -493,6 +1724,9 @@ void GLGizmoMmuSegmentation::on_render_input_window(float x, float y, float bott const float buttons_width = remove_btn_width + filter_btn_width + remap_btn_width + m_imgui->scaled(2.f); const float minimal_slider_width = m_imgui->scaled(4.f); const float color_button_width = m_imgui->calc_text_size(std::string_view{""}).x + m_imgui->scaled(1.75f); + const size_t total_filament_count = m_extruders_colors.size(); + const std::string max_filament_label = std::to_string(std::max(total_filament_count, 1)); + const ImVec2 max_filament_label_size = ImGui::CalcTextSize(max_filament_label.c_str(), NULL, true); float caption_max = 0.f; float total_text_max = 0.f; @@ -512,7 +1746,7 @@ void GLGizmoMmuSegmentation::on_render_input_window(float x, float y, float bott float window_width = minimal_slider_width + sliders_left_width + slider_icon_width; const int max_filament_items_per_line = 8; const float empty_button_width = m_imgui->calc_button_size("").x; - const float filament_item_width = empty_button_width + m_imgui->scaled(1.5f); + const float filament_item_width = std::max(empty_button_width, max_filament_label_size.x + m_imgui->scaled(1.4f)) + m_imgui->scaled(1.5f); window_width = std::max(window_width, total_text_max); window_width = std::max(window_width, buttons_width); @@ -532,17 +1766,18 @@ void GLGizmoMmuSegmentation::on_render_input_window(float x, float y, float bott m_imgui->text(m_desc.at("filaments")); float start_pos_x = ImGui::GetCursorPos().x; - const ImVec2 max_label_size = ImGui::CalcTextSize("99", NULL, true); - const float item_spacing = m_imgui->scaled(0.8f); - size_t n_extruder_colors = std::min((size_t)EnforcerBlockerType::ExtruderMax, m_extruders_colors.size()); - for (int extruder_idx = 0; extruder_idx < n_extruder_colors; extruder_idx++) { - const ColorRGBA &extruder_color = m_extruders_colors[extruder_idx]; + size_t n_extruder_colors = std::min(GLGizmoMmuSegmentation::EXTRUDERS_LIMIT, m_display_filament_ids.size()); + for (size_t extruder_idx = 0; extruder_idx < n_extruder_colors; ++extruder_idx) { + const unsigned int actual_filament_id = m_display_filament_ids[extruder_idx]; + if (actual_filament_id == 0 || actual_filament_id > m_extruders_colors.size()) + continue; + const ColorRGBA &extruder_color = m_extruders_colors[actual_filament_id - 1]; ImVec4 color_vec = ImGuiWrapper::to_ImVec4(extruder_color); std::string color_label = std::string("##extruder color ") + std::to_string(extruder_idx); std::string item_text = std::to_string(extruder_idx + 1); const ImVec2 label_size = ImGui::CalcTextSize(item_text.c_str(), NULL, true); - const ImVec2 button_size(max_label_size.x + m_imgui->scaled(0.5f),0.f); + const ImVec2 button_size(max_filament_label_size.x + m_imgui->scaled(0.5f), 0.f); float button_offset = start_pos_x; if (extruder_idx % max_filament_items_per_line != 0) { @@ -571,7 +1806,12 @@ void GLGizmoMmuSegmentation::on_render_input_window(float x, float y, float bott color_button_high = ImGui::GetCursorPos().y - color_button - 2.0; if (color_picked) { m_selected_extruder_idx = extruder_idx; } - if (extruder_idx < 16 && ImGui::IsItemHovered()) m_imgui->tooltip(_L("Shortcut Key ") + std::to_string(extruder_idx + 1), max_tooltip_width); + if (ImGui::IsItemHovered()) { + if (extruder_idx < 9) + m_imgui->tooltip(_L("Shortcut Key ") + std::to_string(extruder_idx + 1), max_tooltip_width); + else + m_imgui->tooltip(wxString::Format(_L("Filament %d"), int(extruder_idx + 1)), max_tooltip_width); + } // draw filament id float gray = 0.299 * extruder_color.r() + 0.587 * extruder_color.g() + 0.114 * extruder_color.b(); @@ -587,6 +1827,21 @@ void GLGizmoMmuSegmentation::on_render_input_window(float x, float y, float bott //ImGui::NewLine(); ImGui::Dummy(ImVec2(0.0f, ImGui::GetFontSize() * 0.1)); + if (n_extruder_colors > 0) { + int selected_filament = int(m_selected_extruder_idx) + 1; + ImGui::AlignTextToFramePadding(); + m_imgui->text(_L("Selected filament")); + ImGui::SameLine(); + ImGui::PushItemWidth(m_imgui->scaled(4.5f)); + if (ImGui::InputInt("##selected_filament", &selected_filament, 1, 10, ImGuiInputTextFlags_CharsDecimal)) { + selected_filament = std::clamp(selected_filament, 1, int(n_extruder_colors)); + m_selected_extruder_idx = size_t(selected_filament - 1); + } + ImGui::SameLine(); + m_imgui->text(wxString::Format(_L("/ %d"), int(n_extruder_colors))); + ImGui::Dummy(ImVec2(0.0f, ImGui::GetFontSize() * 0.1)); + } + m_imgui->text(m_desc.at("tool_type")); std::array tool_ids; @@ -649,44 +1904,66 @@ void GLGizmoMmuSegmentation::on_render_input_window(float x, float y, float bott ImGui::AlignTextToFramePadding(); m_imgui->text(m_desc.at("cursor_size")); - ImGui::SameLine(sliders_left_width); + ImGui::SameLine(circle_max_width); ImGui::PushItemWidth(sliders_width); m_imgui->bbl_slider_float_style("##cursor_radius", &m_cursor_radius, CursorRadiusMin, CursorRadiusMax, "%.2f", 1.0f, true); - ImGui::SameLine(drag_left_width + sliders_left_width); + ImGui::SameLine(drag_left_width + circle_max_width); ImGui::PushItemWidth(1.5 * slider_icon_width); ImGui::BBLDragFloat("##cursor_radius_input", &m_cursor_radius, 0.05f, 0.0f, 0.0f, "%.2f"); - if (m_imgui->bbl_checkbox(_L("Vertical"), m_vertical_only)) { - if (m_vertical_only) { - m_horizontal_only = false; + ImGui::Separator(); + if (m_c->object_clipper()->get_position() == 0.f) { + ImGui::AlignTextToFramePadding(); + m_imgui->text(m_desc.at("clipping_of_view")); + } + else { + if (m_imgui->button(m_desc.at("reset_direction"))) { + wxGetApp().CallAfter([this]() { + m_c->object_clipper()->set_position_by_ratio(-1., false); + }); } } - if (m_imgui->bbl_checkbox(_L("Horizontal"), m_horizontal_only)) { - if (m_horizontal_only) { - m_vertical_only = false; - } - } - } - else if (m_current_tool == ImGui::TriangleButtonIcon) { + + auto clp_dist = float(m_c->object_clipper()->get_position()); + ImGui::SameLine(circle_max_width); + ImGui::PushItemWidth(sliders_width); + bool slider_clp_dist = m_imgui->bbl_slider_float_style("##clp_dist", &clp_dist, 0.f, 1.f, "%.2f", 1.0f, true); + ImGui::SameLine(drag_left_width + circle_max_width); + ImGui::PushItemWidth(1.5 * slider_icon_width); + bool b_clp_dist_input = ImGui::BBLDragFloat("##clp_dist_input", &clp_dist, 0.05f, 0.0f, 0.0f, "%.2f"); + + if (slider_clp_dist || b_clp_dist_input) { m_c->object_clipper()->set_position_by_ratio(clp_dist, true); } + + } else if (m_current_tool == ImGui::TriangleButtonIcon) { m_cursor_type = TriangleSelector::CursorType::POINTER; m_tool_type = ToolType::BRUSH; - if (m_imgui->bbl_checkbox(_L("Vertical"), m_vertical_only)) { - if (m_vertical_only) { - m_horizontal_only = false; + if (m_c->object_clipper()->get_position() == 0.f) { + ImGui::AlignTextToFramePadding(); + m_imgui->text(m_desc.at("clipping_of_view")); + } + else { + if (m_imgui->button(m_desc.at("reset_direction"))) { + wxGetApp().CallAfter([this]() { + m_c->object_clipper()->set_position_by_ratio(-1., false); + }); } } - if (m_imgui->bbl_checkbox(_L("Horizontal"), m_horizontal_only)) { - if (m_horizontal_only) { - m_vertical_only = false; - } - } - } - else if (m_current_tool == ImGui::FillButtonIcon) { - m_cursor_type = TriangleSelector::CursorType::POINTER; - m_tool_type = ToolType::BUCKET_FILL; + auto clp_dist = float(m_c->object_clipper()->get_position()); + ImGui::SameLine(clipping_slider_left); + ImGui::PushItemWidth(sliders_width); + bool slider_clp_dist = m_imgui->bbl_slider_float_style("##clp_dist", &clp_dist, 0.f, 1.f, "%.2f", 1.0f, true); + ImGui::SameLine(drag_left_width + clipping_slider_left); + ImGui::PushItemWidth(1.5 * slider_icon_width); + bool b_clp_dist_input = ImGui::BBLDragFloat("##clp_dist_input", &clp_dist, 0.05f, 0.0f, 0.0f, "%.2f"); + + if (slider_clp_dist || b_clp_dist_input) { m_c->object_clipper()->set_position_by_ratio(clp_dist, true); } + + } else if (m_current_tool == ImGui::FillButtonIcon) { + m_cursor_type = TriangleSelector::CursorType::POINTER; m_imgui->bbl_checkbox(m_desc["edge_detection"], m_detect_geometry_edge); + m_tool_type = ToolType::BUCKET_FILL; if (m_detect_geometry_edge) { ImGui::AlignTextToFramePadding(); @@ -707,54 +1984,89 @@ void GLGizmoMmuSegmentation::on_render_input_window(float x, float y, float bott // set to negative value to disable edge detection m_smart_fill_angle = -1.f; } - } - else if (m_current_tool == ImGui::HeightRangeIcon) { + ImGui::Separator(); + if (m_c->object_clipper()->get_position() == 0.f) { + ImGui::AlignTextToFramePadding(); + m_imgui->text(m_desc.at("clipping_of_view")); + } + else { + if (m_imgui->button(m_desc.at("reset_direction"))) { + wxGetApp().CallAfter([this]() { + m_c->object_clipper()->set_position_by_ratio(-1., false); + }); + } + } + + auto clp_dist = float(m_c->object_clipper()->get_position()); + ImGui::SameLine(sliders_left_width); + ImGui::PushItemWidth(sliders_width); + bool slider_clp_dist = m_imgui->bbl_slider_float_style("##clp_dist", &clp_dist, 0.f, 1.f, "%.2f", 1.0f, true); + ImGui::SameLine(drag_left_width + sliders_left_width); + ImGui::PushItemWidth(1.5 * slider_icon_width); + bool b_clp_dist_input = ImGui::BBLDragFloat("##clp_dist_input", &clp_dist, 0.05f, 0.0f, 0.0f, "%.2f"); + + if (slider_clp_dist || b_clp_dist_input) { m_c->object_clipper()->set_position_by_ratio(clp_dist, true);} + + } else if (m_current_tool == ImGui::HeightRangeIcon) { m_tool_type = ToolType::BRUSH; m_cursor_type = TriangleSelector::CursorType::HEIGHT_RANGE; ImGui::AlignTextToFramePadding(); m_imgui->text(m_desc["height_range"] + ":"); - ImGui::SameLine(sliders_left_width); + ImGui::SameLine(height_max_width); ImGui::PushItemWidth(sliders_width); - std::string format_str = std::string("%.2f") + I18N::translate_utf8("mm", "Height range," "Facet in [cursor z, cursor z + height] will be selected."); + std::string format_str = std::string("%.2f") + I18N::translate_utf8("mm", "Heigh range," "Facet in [cursor z, cursor z + height] will be selected."); m_imgui->bbl_slider_float_style("##cursor_height", &m_cursor_height, CursorHeightMin, CursorHeightMax, format_str.data(), 1.0f, true); - ImGui::SameLine(drag_left_width + sliders_left_width); + ImGui::SameLine(drag_left_width + height_max_width); ImGui::PushItemWidth(1.5 * slider_icon_width); ImGui::BBLDragFloat("##cursor_height_input", &m_cursor_height, 0.05f, 0.0f, 0.0f, "%.2f"); + + ImGui::Separator(); + if (m_c->object_clipper()->get_position() == 0.f) { + ImGui::AlignTextToFramePadding(); + m_imgui->text(m_desc.at("clipping_of_view")); + } + else { + if (m_imgui->button(m_desc.at("reset_direction"))) { + wxGetApp().CallAfter([this]() { + m_c->object_clipper()->set_position_by_ratio(-1., false); + }); + } + } + + auto clp_dist = float(m_c->object_clipper()->get_position()); + ImGui::SameLine(height_max_width); + ImGui::PushItemWidth(sliders_width); + bool slider_clp_dist = m_imgui->bbl_slider_float_style("##clp_dist", &clp_dist, 0.f, 1.f, "%.2f", 1.0f, true); + ImGui::SameLine(drag_left_width + height_max_width); + ImGui::PushItemWidth(1.5 * slider_icon_width); + bool b_clp_dist_input = ImGui::BBLDragFloat("##clp_dist_input", &clp_dist, 0.05f, 0.0f, 0.0f, "%.2f"); + + if (slider_clp_dist || b_clp_dist_input) { m_c->object_clipper()->set_position_by_ratio(clp_dist, true); } } else if (m_current_tool == ImGui::GapFillIcon) { m_tool_type = ToolType::GAP_FILL; m_cursor_type = TriangleSelector::CursorType::POINTER; ImGui::AlignTextToFramePadding(); m_imgui->text(m_desc["gap_area"] + ":"); - ImGui::SameLine(sliders_left_width); + ImGui::SameLine(gap_area_slider_left); ImGui::PushItemWidth(sliders_width); std::string format_str = std::string("%.2f") + I18N::translate_utf8("", "Triangle patch area threshold,""triangle patch will be merged to neighbor if its area is less than threshold"); m_imgui->bbl_slider_float_style("##gap_area", &TriangleSelectorPatch::gap_area, TriangleSelectorPatch::GapAreaMin, TriangleSelectorPatch::GapAreaMax, format_str.data(), 1.0f, true); - ImGui::SameLine(drag_left_width + sliders_left_width); + ImGui::SameLine(drag_left_width + gap_area_slider_left); ImGui::PushItemWidth(1.5 * slider_icon_width); ImGui::BBLDragFloat("##gap_area_input", &TriangleSelectorPatch::gap_area, 0.05f, 0.0f, 0.0f, "%.2f"); } ImGui::Separator(); - if (m_c->object_clipper()->get_position() == 0.f) { - ImGui::AlignTextToFramePadding(); - m_imgui->text(m_desc.at("clipping_of_view")); - } else { - if (m_imgui->button(m_desc.at("reset_direction"))) { - wxGetApp().CallAfter([this]() { m_c->object_clipper()->set_position_by_ratio(-1., false); }); + if(m_imgui->bbl_checkbox(_L("Vertical"), m_vertical_only)){ + if(m_vertical_only){ + m_horizontal_only = false; } } - - auto clp_dist = float(m_c->object_clipper()->get_position()); - ImGui::SameLine(sliders_left_width); - ImGui::PushItemWidth(sliders_width); - bool slider_clp_dist = m_imgui->bbl_slider_float_style("##clp_dist", &clp_dist, 0.f, 1.f, "%.2f", 1.0f, true); - ImGui::SameLine(drag_left_width + sliders_left_width); - ImGui::PushItemWidth(1.5 * slider_icon_width); - bool b_clp_dist_input = ImGui::BBLDragFloat("##clp_dist_input", &clp_dist, 0.05f, 0.0f, 0.0f, "%.2f"); - - if (slider_clp_dist || b_clp_dist_input) { - m_c->object_clipper()->set_position_by_ratio(clp_dist, true); + if(m_imgui->bbl_checkbox(_L("Horizontal"), m_horizontal_only)){ + if(m_horizontal_only){ + m_vertical_only = false; + } } ImGui::Separator(); @@ -777,7 +2089,32 @@ void GLGizmoMmuSegmentation::on_render_input_window(float x, float y, float bott bake_selected_object_image_texture_to_vertex_colors(); if (ImGui::IsItemHovered()) { if (can_bake_image_texture_data) - m_imgui->tooltip(_L("Sample imported image texture UVs into stored vertex colors, then discard the baked image texture data."), max_tooltip_width); + m_imgui->tooltip(_L("Sample imported image texture UVs into stored vertex colors, then discard the baked image texture data."), + max_tooltip_width); + else + m_imgui->tooltip(_L("This object does not have imported image texture data with UVs."), max_tooltip_width); + } + m_imgui->disabled_end(); + + const bool can_convert_vertex_colors_to_texture_mapping_colors = selected_object_has_imported_vertex_colors(); + m_imgui->disabled_begin(!can_convert_vertex_colors_to_texture_mapping_colors); + if (m_imgui->button(_L("Convert vertex colors to RGB data"))) + convert_selected_object_vertex_colors_to_texture_mapping_colors(); + if (ImGui::IsItemHovered()) { + if (can_convert_vertex_colors_to_texture_mapping_colors) + m_imgui->tooltip(_L("Convert imported vertex colors into RGB data for texture mapping zones."), max_tooltip_width); + else + m_imgui->tooltip(_L("This object does not have stored imported vertex colors."), max_tooltip_width); + } + m_imgui->disabled_end(); + + const bool can_convert_image_texture_to_texture_mapping_colors = selected_object_has_bakeable_image_texture_data(); + m_imgui->disabled_begin(!can_convert_image_texture_to_texture_mapping_colors); + if (m_imgui->button(_L("Convert image texture to RGB data"))) + convert_selected_object_image_texture_to_texture_mapping_colors(); + if (ImGui::IsItemHovered()) { + if (can_convert_image_texture_to_texture_mapping_colors) + m_imgui->tooltip(_L("Sample imported image texture UVs into RGB data for texture mapping zones."), max_tooltip_width); else m_imgui->tooltip(_L("This object does not have imported image texture data with UVs."), max_tooltip_width); } @@ -795,6 +2132,18 @@ void GLGizmoMmuSegmentation::on_render_input_window(float x, float y, float bott } m_imgui->disabled_end(); + const bool can_clear_texture_mapping_color_data = selected_object_has_texture_mapping_color_data(); + m_imgui->disabled_begin(!can_clear_texture_mapping_color_data); + if (m_imgui->button(_L("Clear RGB Data"))) + clear_selected_object_texture_mapping_color_data(); + if (ImGui::IsItemHovered()) { + if (can_clear_texture_mapping_color_data) + m_imgui->tooltip(_L("Discard RGB data from the selected object."), max_tooltip_width); + else + m_imgui->tooltip(_L("This object does not have RGB data."), max_tooltip_width); + } + m_imgui->disabled_end(); + ImGui::Separator(); const bool can_apply_stored_vertex_colors = selected_object_has_imported_vertex_colors(); @@ -811,63 +2160,27 @@ void GLGizmoMmuSegmentation::on_render_input_window(float x, float y, float bott ImGui::Separator(); - // ORCA: Remap filaments section (Border only, Title in border). - // Styled as a panel for visual grouping. + if (m_imgui->button(m_desc.at("perform_remap"))) { - m_show_remap_panel = !m_show_remap_panel; + m_show_filament_remap_ui = !m_show_filament_remap_ui; + if (m_show_filament_remap_ui) { + // reset remap to identity on opening + m_extruder_remap.resize(m_extruders_colors.size()); + for (size_t i = 0; i < m_extruder_remap.size(); ++i) + m_extruder_remap[i] = i; + } } - if (m_show_remap_panel) - { - ImGui::Spacing(); - - ImDrawList* draw_list = ImGui::GetWindowDrawList(); - std::string title = into_u8(m_desc.at("perform_remap")); - float available_width = ImGui::GetContentRegionAvail().x; - - // ORCA: Draw Background filled (consistent with Filaments section) - // Use static to remember height from previous frame so we can draw it behind. - static float remap_panel_high = 40.0f; - ImVec2 p_bg_min = ImGui::GetCursorScreenPos(); - // Adjust background position: slight negative offset to align with padding, width fills available - // height from static variable. - draw_list->AddRectFilled({p_bg_min.x - 10.0f, p_bg_min.y - 7.0f}, {p_bg_min.x + available_width + ImGui::GetFrameHeight(), p_bg_min.y + remap_panel_high}, ImGui::GetColorU32(ImGuiCol_FrameBgActive, 1.0f), 5.0f); - - float start_y = ImGui::GetCursorPos().y; - - // ORCA: Title as simple text - Removed as per request (redundant with button) - // m_imgui->text(title); - - ImGui::BeginGroup(); - // ORCA: Reduce vertical spacing within this group - ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(m_imgui->scaled(0.4f), m_imgui->scaled(0.2f))); - + // Render filament swap UI if enabled + if (m_show_filament_remap_ui) { + ImGui::Separator(); render_filament_remap_ui(window_width, max_tooltip_width); - - ImGui::PopStyleVar(); - ImGui::EndGroup(); - - // ORCA: Update height for next frame fill - remap_panel_high = ImGui::GetCursorPos().y - start_y; - - // ORCA: Add Remap and Cancel buttons (outside the panel) - ImGui::Spacing(); - if (m_imgui->button(m_desc.at("remap"))) { - this->remap_filament_assignments(); - // Reset mapping to identity after apply - for (size_t i = 0; i < m_extruder_remap.size(); ++i) m_extruder_remap[i] = i; - } - ImGui::SameLine(); - if (m_imgui->button(m_desc.at("cancel_remap"))) { - // Reset mapping to identity - for (size_t i = 0; i < m_extruder_remap.size(); ++i) m_extruder_remap[i] = i; - } } - ImGui::Separator(); ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(6.0f, 10.0f)); - render_tooltip_button(x, y); + float get_cur_y = ImGui::GetContentRegionMax().y + ImGui::GetFrameHeight() + y; + show_tooltip_information(caption_max, x, get_cur_y); float f_scale =m_parent.get_gizmos_manager().get_layout_scale(); ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2(6.0f, 4.0f * f_scale)); @@ -925,14 +2238,34 @@ void GLGizmoMmuSegmentation::update_model_object() } if (updated) { + const size_t num_physical = static_cast(std::max(wxGetApp().filaments_cnt(), 0)); + size_t num_total = num_physical; + if (wxGetApp().preset_bundle != nullptr) + num_total = wxGetApp().preset_bundle->texture_mapping_zones.total_filaments(num_physical); + + size_t max_used_state = 0; + for (const ModelVolume *mv : mo->volumes) { + if (!mv->is_model_part()) + continue; + const auto &used_states = mv->mmu_segmentation_facets.get_data().used_states; + for (size_t state_idx = static_cast(EnforcerBlockerType::Extruder1); state_idx < used_states.size(); ++state_idx) { + if (used_states[state_idx]) + max_used_state = std::max(max_used_state, state_idx); + } + } + + if (max_used_state > num_physical) { + BOOST_LOG_TRIVIAL(warning) << "GLGizmoMmuSegmentation::update_model_object painted virtual extruder state detected" + << " max_used_state=" << max_used_state + << " physical_filaments=" << num_physical + << " total_filaments=" << num_total; + } + const ModelObjectPtrs &mos = wxGetApp().model().objects; size_t obj_idx = std::find(mos.begin(), mos.end(), mo) - mos.begin(); wxGetApp().obj_list()->update_info_items(obj_idx); wxGetApp().plater()->get_partplate_list().notify_instance_update(obj_idx, 0); m_parent.post_event(SimpleEvent(EVT_GLCANVAS_SCHEDULE_BACKGROUND_PROCESS)); - - // ORCA: Refresh cache - this->update_used_filaments(); } } @@ -990,14 +2323,12 @@ void GLGizmoMmuSegmentation::update_from_model_object(bool first_update) // Extruder colors need to be reloaded before calling init_model_triangle_selectors to render painted triangles // using colors from loaded 3MF and not from printer profile in Slicer. + const std::vector current_extruder_colors = get_extruders_colors(); if (int prev_extruders_count = int(m_extruders_colors.size()); - prev_extruders_count != wxGetApp().filaments_cnt() || wxGetApp().plater()->get_extruders_colors() != m_extruders_colors) - this->init_extruders_data(); + prev_extruders_count != int(current_extruder_colors.size()) || current_extruder_colors != m_extruders_colors) + this->init_extruders_data(current_extruder_colors); this->init_model_triangle_selectors(); - - // ORCA: Refresh cache when model changes - this->update_used_filaments(); } void GLGizmoMmuSegmentation::tool_changed(wchar_t old_tool, wchar_t new_tool) @@ -1057,6 +2388,21 @@ bool GLGizmoMmuSegmentation::selected_object_has_bakeable_image_texture_data() c return false; } +bool GLGizmoMmuSegmentation::selected_object_has_texture_mapping_color_data() const +{ + const ModelObject *object = m_c->selection_info()->model_object(); + if (object == nullptr) + return false; + + for (const ModelVolume *volume : object->volumes) { + if (volume == nullptr || !volume->is_model_part()) + continue; + if (!volume->texture_mapping_color_facets.empty()) + return true; + } + return false; +} + bool GLGizmoMmuSegmentation::selected_object_has_painted_regions() const { for (const auto &selector : m_triangle_selectors) { @@ -1100,42 +2446,38 @@ void GLGizmoMmuSegmentation::open_obj_vertex_color_mapping_dialog() if (target_volume->mesh().its.vertices.size() != target_volume->imported_vertex_colors_rgba.size()) return; - ObjDialogInOut in_out; - in_out.input_colors.reserve(target_volume->imported_vertex_colors_rgba.size()); + std::vector input_colors; + input_colors.reserve(target_volume->imported_vertex_colors_rgba.size()); for (const uint32_t packed : target_volume->imported_vertex_colors_rgba) { const float r = float((packed >> 24) & 0xFF) / 255.f; const float g = float((packed >> 16) & 0xFF) / 255.f; const float b = float((packed >> 8) & 0xFF) / 255.f; const float a = float(packed & 0xFF) / 255.f; - in_out.input_colors.emplace_back(RGBA{r, g, b, a}); + input_colors.emplace_back(RGBA{r, g, b, a}); } - if (in_out.input_colors.empty()) + if (input_colors.empty()) return; - in_out.is_single_color = true; - const RGBA first_color = in_out.input_colors.front(); - for (const RGBA &color : in_out.input_colors) { + bool is_single_color = true; + const RGBA first_color = input_colors.front(); + for (const RGBA &color : input_colors) { if (color != first_color) { - in_out.is_single_color = false; + is_single_color = false; break; } } - in_out.first_extruder_id = 1; - in_out.deal_vertex_color = true; - Model preview_model; - preview_model.add_object(*object); - in_out.model = &preview_model; - const std::vector extruder_colours = wxGetApp().plater()->get_extruder_colors_from_plater_config(nullptr, false); - ObjColorDialog color_dlg(nullptr, in_out, extruder_colours); + std::vector filament_ids; + unsigned char first_extruder_id = 1; + const std::vector extruder_colours = wxGetApp().plater()->get_extruder_colors_from_plater_config(); + ObjColorDialog color_dlg(nullptr, input_colors, is_single_color, extruder_colours, filament_ids, first_extruder_id); if (color_dlg.ShowModal() != wxID_OK) return; - if (in_out.filament_ids.empty()) + if (filament_ids.empty()) return; - Plater::TakeSnapshot snapshot(wxGetApp().plater(), "Convert vertex colors to regions", UndoRedo::SnapshotType::GizmoAction); - if (!Model::obj_import_vertex_color_deal_for_object(in_out.filament_ids, in_out.first_extruder_id, object)) + if (!Model::obj_import_vertex_color_deal_for_object(filament_ids, first_extruder_id, object)) return; update_from_model_object(); @@ -1199,7 +2541,7 @@ void GLGizmoMmuSegmentation::bake_selected_object_image_texture_to_vertex_colors const Vec3f p2 = its.vertices[size_t(tri[2])].cast(); const float area = 0.5f * (p1 - p0).cross(p2 - p0).norm(); const double weight = std::isfinite(area) && area > EPSILON ? double(area) : 1.0; - const std::array vertex_indices = {tri[0], tri[1], tri[2]}; + const std::array vertex_indices = { tri[0], tri[1], tri[2] }; for (size_t corner = 0; corner < 3; ++corner) { const ColorRGBA color = sample_texture_rgba_for_vertex_bake(volume->imported_texture_rgba, @@ -1250,6 +2592,8 @@ void GLGizmoMmuSegmentation::bake_selected_object_image_texture_to_vertex_colors if (selector != nullptr) selector->request_update_render_data(true); + update_from_model_object(); + m_parent.update_volumes_colors_by_extruder(); m_parent.set_as_dirty(); const ModelObjectPtrs &objects = wxGetApp().model().objects; const size_t object_idx = size_t(std::find(objects.begin(), objects.end(), object) - objects.begin()); @@ -1260,6 +2604,160 @@ void GLGizmoMmuSegmentation::bake_selected_object_image_texture_to_vertex_colors m_parent.post_event(SimpleEvent(EVT_GLCANVAS_SCHEDULE_BACKGROUND_PROCESS)); } +void GLGizmoMmuSegmentation::convert_selected_object_vertex_colors_to_texture_mapping_colors() +{ + ModelObject *object = m_c->selection_info()->model_object(); + if (object == nullptr) + return; + + bool converted = false; + Plater::TakeSnapshot snapshot(wxGetApp().plater(), "Convert vertex colors to RGB data", UndoRedo::SnapshotType::GizmoAction); + + for (ModelVolume *volume : object->volumes) { + if (volume == nullptr || !volume->is_model_part()) + continue; + + const indexed_triangle_set &its = volume->mesh().its; + if (its.vertices.empty() || + its.indices.empty() || + volume->imported_vertex_colors_rgba.size() != its.vertices.size()) + continue; + + TextureMappingColorSampler sampler = [volume, &its](size_t tri_idx, const Vec3f &, const Vec3f &barycentric) { + if (tri_idx >= its.indices.size()) + return 0xFFFFFFFFu; + + const auto &tri = its.indices[tri_idx]; + if (tri[0] < 0 || tri[1] < 0 || tri[2] < 0) + return 0xFFFFFFFFu; + if (size_t(tri[0]) >= volume->imported_vertex_colors_rgba.size() || + size_t(tri[1]) >= volume->imported_vertex_colors_rgba.size() || + size_t(tri[2]) >= volume->imported_vertex_colors_rgba.size()) + return 0xFFFFFFFFu; + + const ColorRGBA c0 = unpack_vertex_color_rgba_for_conversion(volume->imported_vertex_colors_rgba[size_t(tri[0])]); + const ColorRGBA c1 = unpack_vertex_color_rgba_for_conversion(volume->imported_vertex_colors_rgba[size_t(tri[1])]); + const ColorRGBA c2 = unpack_vertex_color_rgba_for_conversion(volume->imported_vertex_colors_rgba[size_t(tri[2])]); + return pack_vertex_color_rgba(ColorRGBA(c0.r() * barycentric.x() + c1.r() * barycentric.y() + c2.r() * barycentric.z(), + c0.g() * barycentric.x() + c1.g() * barycentric.y() + c2.g() * barycentric.z(), + c0.b() * barycentric.x() + c1.b() * barycentric.y() + c2.b() * barycentric.z(), + c0.a() * barycentric.x() + c1.a() * barycentric.y() + c2.a() * barycentric.z())); + }; + + const float target_edge = std::max(mesh_max_axis_span(its) / 160.f, 0.25f); + TextureMappingColorSubdivisionDepths subdivision_depths = [target_edge](size_t, const std::array &vertices) { + const int depth = texture_mapping_depth_from_span(triangle_max_edge_length(vertices), target_edge, 5); + return std::make_pair(depth, depth); + }; + + volume->texture_mapping_color_facets.set_from_triangle_sampler(*volume, sampler, 5, 0.025f, subdivision_depths); + if (volume->texture_mapping_color_facets.metadata_json().empty()) + volume->texture_mapping_color_facets.set_metadata_json(rgb_metadata_json(ColorRGBA(1.f, 1.f, 1.f, 1.f))); + converted = true; + } + + if (!converted) + return; + + const unsigned int texture_mapping_filament_id = ensure_texture_mapping_zone(); + if (texture_mapping_filament_id != 0) { + object->config.set("extruder", int(texture_mapping_filament_id)); + for (ModelVolume *volume : object->volumes) + if (volume != nullptr && volume->is_model_part()) + volume->config.set("extruder", int(texture_mapping_filament_id)); + } + + for (auto &selector : m_triangle_selectors) + if (selector != nullptr) + selector->request_update_render_data(true); + + update_from_model_object(); + m_parent.update_volumes_colors_by_extruder(); + m_parent.set_as_dirty(); + + const ModelObjectPtrs &objects = wxGetApp().model().objects; + const size_t object_idx = size_t(std::find(objects.begin(), objects.end(), object) - objects.begin()); + if (object_idx < objects.size()) { + wxGetApp().obj_list()->update_info_items(object_idx); + wxGetApp().plater()->get_partplate_list().notify_instance_update(object_idx, 0); + } + m_parent.post_event(SimpleEvent(EVT_GLCANVAS_SCHEDULE_BACKGROUND_PROCESS)); +} + +void GLGizmoMmuSegmentation::convert_selected_object_image_texture_to_texture_mapping_colors() +{ + ModelObject *object = m_c->selection_info()->model_object(); + if (object == nullptr) + return; + + bool converted = false; + Plater::TakeSnapshot snapshot(wxGetApp().plater(), "Convert image texture to RGB data", UndoRedo::SnapshotType::GizmoAction); + + for (ModelVolume *volume : object->volumes) { + if (volume == nullptr || !volume->is_model_part() || !model_volume_has_bakeable_image_texture_data(volume)) + continue; + + const indexed_triangle_set &its = volume->mesh().its; + TextureMappingColorSampler sampler = [volume, &its](size_t tri_idx, const Vec3f &, const Vec3f &barycentric) { + if (tri_idx >= its.indices.size() || + tri_idx >= volume->imported_texture_uv_valid.size() || + volume->imported_texture_uv_valid[tri_idx] == 0) + return 0xFFFFFFFFu; + + const size_t uv_offset = tri_idx * 6; + if (uv_offset + 5 >= volume->imported_texture_uvs_per_face.size()) + return 0xFFFFFFFFu; + + const Vec2f uv0(volume->imported_texture_uvs_per_face[uv_offset + 0], volume->imported_texture_uvs_per_face[uv_offset + 1]); + const Vec2f uv1(volume->imported_texture_uvs_per_face[uv_offset + 2], volume->imported_texture_uvs_per_face[uv_offset + 3]); + const Vec2f uv2(volume->imported_texture_uvs_per_face[uv_offset + 4], volume->imported_texture_uvs_per_face[uv_offset + 5]); + const Vec2f uv = uv0 * barycentric.x() + uv1 * barycentric.y() + uv2 * barycentric.z(); + return pack_vertex_color_rgba(sample_texture_rgba_for_vertex_bake(volume->imported_texture_rgba, + volume->imported_texture_width, + volume->imported_texture_height, + uv)); + }; + + const int safe_max_depth = texture_mapping_depth_for_budget(its.indices.size(), 7, 3200000); + TextureMappingColorSubdivisionDepths subdivision_depths = [volume, safe_max_depth](size_t tri_idx, const std::array &) { + const int depth = texture_mapping_depth_from_span(texture_triangle_uv_pixel_span(volume, tri_idx), 8.f, safe_max_depth); + return std::make_pair(depth, depth); + }; + + volume->texture_mapping_color_facets.set_from_triangle_sampler(*volume, sampler, safe_max_depth, 0.015f, subdivision_depths); + if (volume->texture_mapping_color_facets.metadata_json().empty()) + volume->texture_mapping_color_facets.set_metadata_json(rgb_metadata_json(ColorRGBA(1.f, 1.f, 1.f, 1.f))); + converted = true; + } + + if (!converted) + return; + + const unsigned int texture_mapping_filament_id = ensure_texture_mapping_zone(); + if (texture_mapping_filament_id != 0) { + object->config.set("extruder", int(texture_mapping_filament_id)); + for (ModelVolume *volume : object->volumes) + if (volume != nullptr && volume->is_model_part()) + volume->config.set("extruder", int(texture_mapping_filament_id)); + } + + for (auto &selector : m_triangle_selectors) + if (selector != nullptr) + selector->request_update_render_data(true); + + update_from_model_object(); + m_parent.update_volumes_colors_by_extruder(); + m_parent.set_as_dirty(); + + const ModelObjectPtrs &objects = wxGetApp().model().objects; + const size_t object_idx = size_t(std::find(objects.begin(), objects.end(), object) - objects.begin()); + if (object_idx < objects.size()) { + wxGetApp().obj_list()->update_info_items(object_idx); + wxGetApp().plater()->get_partplate_list().notify_instance_update(object_idx, 0); + } + m_parent.post_event(SimpleEvent(EVT_GLCANVAS_SCHEDULE_BACKGROUND_PROCESS)); +} + void GLGizmoMmuSegmentation::convert_selected_regions_to_vertex_colors() { ModelObject *object = m_c->selection_info()->model_object(); @@ -1273,7 +2771,7 @@ void GLGizmoMmuSegmentation::convert_selected_regions_to_vertex_colors() std::vector filament_colors; filament_colors.reserve(color_strings.size()); for (const std::string &color_string : color_strings) { - unsigned char rgba[4] = {38, 166, 154, 255}; + unsigned char rgba[4] = { 38, 166, 154, 255 }; BitmapCache::parse_color4(color_string, rgba); filament_colors.emplace_back(float(rgba[0]) / 255.f, float(rgba[1]) / 255.f, @@ -1361,7 +2859,7 @@ void GLGizmoMmuSegmentation::convert_selected_regions_to_vertex_colors() const float area = 0.5f * (triangle.vertices[1] - triangle.vertices[0]).cross(triangle.vertices[2] - triangle.vertices[0]).norm(); const double area_weight = std::max(double(area), 1e-6); - const std::array bary = {weights.x(), weights.y(), weights.z()}; + const std::array bary = { weights.x(), weights.y(), weights.z() }; for (size_t corner = 0; corner < 3; ++corner) { VertexColorAccumulator &acc = accumulators[size_t(source_indices[corner])]; @@ -1451,6 +2949,43 @@ void GLGizmoMmuSegmentation::clear_selected_object_image_texture_data() if (selector != nullptr) selector->request_update_render_data(true); + update_from_model_object(); + m_parent.update_volumes_colors_by_extruder(); + m_parent.set_as_dirty(); + const ModelObjectPtrs &objects = wxGetApp().model().objects; + const size_t object_idx = size_t(std::find(objects.begin(), objects.end(), object) - objects.begin()); + if (object_idx < objects.size()) { + wxGetApp().obj_list()->update_info_items(object_idx); + wxGetApp().plater()->get_partplate_list().notify_instance_update(object_idx, 0); + } + m_parent.post_event(SimpleEvent(EVT_GLCANVAS_SCHEDULE_BACKGROUND_PROCESS)); +} + +void GLGizmoMmuSegmentation::clear_selected_object_texture_mapping_color_data() +{ + ModelObject *object = m_c->selection_info()->model_object(); + if (object == nullptr) + return; + + bool cleared = false; + Plater::TakeSnapshot snapshot(wxGetApp().plater(), "Clear RGB data", UndoRedo::SnapshotType::GizmoAction); + for (ModelVolume *volume : object->volumes) { + if (volume == nullptr || !volume->is_model_part() || volume->texture_mapping_color_facets.empty()) + continue; + + volume->texture_mapping_color_facets.reset(); + cleared = true; + } + + if (!cleared) + return; + + for (auto &selector : m_triangle_selectors) + if (selector != nullptr) + selector->request_update_render_data(true); + + update_from_model_object(); + m_parent.update_volumes_colors_by_extruder(); m_parent.set_as_dirty(); const ModelObjectPtrs &objects = wxGetApp().model().objects; const size_t object_idx = size_t(std::find(objects.begin(), objects.end(), object) - objects.begin()); @@ -1469,10 +3004,12 @@ PainterGizmoType GLGizmoMmuSegmentation::get_painter_type() const // BBS ColorRGBA GLGizmoMmuSegmentation::get_cursor_hover_color() const { - if (m_selected_extruder_idx < m_extruders_colors.size()) - return m_extruders_colors[m_selected_extruder_idx]; - else - return m_extruders_colors[0]; + if (m_selected_extruder_idx < m_display_filament_ids.size()) { + const unsigned int actual_filament_id = m_display_filament_ids[m_selected_extruder_idx]; + if (actual_filament_id >= 1 && actual_filament_id <= m_extruders_colors.size()) + return m_extruders_colors[actual_filament_id - 1]; + } + return m_extruders_colors.empty() ? ColorRGBA() : m_extruders_colors[0]; } void GLGizmoMmuSegmentation::on_set_state() @@ -1483,9 +3020,6 @@ void GLGizmoMmuSegmentation::on_set_state() ModelObject* mo = m_c->selection_info()->model_object(); if (mo) Slic3r::save_object_mesh(*mo); m_parent.post_event(SimpleEvent(EVT_GLCANVAS_FORCE_UPDATE)); - if (m_current_tool == ImGui::GapFillIcon) {//exit gap fill - m_current_tool = ImGui::CircleButtonIcon; - } } } @@ -1495,11 +3029,1858 @@ wxString GLGizmoMmuSegmentation::handle_snapshot_action_name(bool shift_down, GL if (shift_down) action_name = _L("Remove painted color"); else { - action_name = GUI::format(_L("Painted using: Filament %1%"), m_selected_extruder_idx); + action_name = GUI::format(_L("Painted using: Filament %1%"), m_selected_extruder_idx + 1); } return action_name; } +GLGizmoTrueColorPainting::GLGizmoTrueColorPainting(GLCanvas3D& parent, const std::string& icon_filename, unsigned int sprite_id) + : GLGizmoPainterBase(parent, icon_filename, sprite_id) +{ +} + +bool GLGizmoTrueColorPainting::on_init() +{ + m_cursor_type = TriangleSelector::CursorType::SPHERE; + m_tool_type = ToolType::BRUSH; + m_triangle_splitting_enabled = true; + m_cursor_radius = 1.f; + sync_active_color_mode_from_rgb(true); + return true; +} + +void GLGizmoTrueColorPainting::on_opening() +{ + update_selected_object_color_state(); + if (m_color_input_mode == ColorInputMode::FilamentColors) + sync_active_color_mode_from_rgb(true); +} + +void GLGizmoTrueColorPainting::on_shutdown() +{ + m_color_picker_active = false; + m_color_picker_source_cache.clear(); + m_parent.use_slope(false); + m_parent.toggle_model_objects_visibility(true); +} + +PainterGizmoType GLGizmoTrueColorPainting::get_painter_type() const +{ + return PainterGizmoType::TRUE_COLOR; +} + +std::string GLGizmoTrueColorPainting::on_get_name() const +{ + return _u8L("True Color Painting"); +} + +bool GLGizmoTrueColorPainting::on_is_selectable() const +{ + return wxGetApp().preset_bundle->printers.get_edited_preset().printer_technology() == ptFFF; +} + +bool GLGizmoTrueColorPainting::on_is_activable() const +{ + const Selection& selection = m_parent.get_selection(); + return wxGetApp().preset_bundle->printers.get_edited_preset().printer_technology() == ptFFF && + !selection.is_empty() && + (selection.is_single_full_instance() || selection.is_any_volume()); +} + +ColorRGBA GLGizmoTrueColorPainting::get_cursor_hover_color() const +{ + if (m_color_picker_active) { + ColorRGBA color; + if (sample_color_from_model(m_parent.get_local_mouse_position(), color)) + return ColorRGBA(std::clamp(color.r(), 0.f, 1.f), + std::clamp(color.g(), 0.f, 1.f), + std::clamp(color.b(), 0.f, 1.f), + 1.f); + } + return ColorRGBA(m_rgb_color[0], m_rgb_color[1], m_rgb_color[2], 1.f); +} + +ColorRGBA GLGizmoTrueColorPainting::get_cursor_sphere_left_button_color() const +{ + return ColorRGBA(m_rgb_color[0], + m_rgb_color[1], + m_rgb_color[2], + 0.15f + 0.35f * std::clamp(m_opacity, 0.f, 1.f)); +} + +void GLGizmoTrueColorPainting::render_painter_gizmo() +{ + const ModelObject *object = selected_model_object(); + if (object == nullptr) + return; + if (object->id() != m_selected_color_state_object_id) + update_selected_object_color_state(); + + const Selection& selection = m_parent.get_selection(); + glsafe(::glEnable(GL_BLEND)); + glsafe(::glEnable(GL_DEPTH_TEST)); + + render_triangles(selection); + m_c->object_clipper()->render_cut(); + m_c->instances_hider()->render_cut(); + render_cursor(); + + glsafe(::glDisable(GL_BLEND)); +} + +bool GLGizmoTrueColorPainting::gizmo_event(SLAGizmoEventType action, + const Vec2d& mouse_position, + bool shift_down, + bool alt_down, + bool control_down) +{ + const bool painting_event = + action == SLAGizmoEventType::LeftDown || + action == SLAGizmoEventType::RightDown || + action == SLAGizmoEventType::Dragging || + action == SLAGizmoEventType::LeftUp || + action == SLAGizmoEventType::RightUp || + action == SLAGizmoEventType::Moving; + const ModelObject *object = selected_model_object(); + if (object == nullptr || object->id() != m_selected_color_state_object_id) + update_selected_object_color_state(); + + if (m_color_picker_active) { + if (action == SLAGizmoEventType::LeftDown) { + if (pick_color_from_model(mouse_position)) + m_color_picker_active = false; + m_parent.set_as_dirty(); + return true; + } + if (action == SLAGizmoEventType::RightDown) { + m_color_picker_active = false; + m_parent.set_as_dirty(); + return true; + } + if (painting_event) + return true; + } + + return GLGizmoPainterBase::gizmo_event(action, mouse_position, shift_down, alt_down, control_down); +} + +void GLGizmoTrueColorPainting::init_model_triangle_selectors() +{ + const ModelObject *object = selected_model_object(); + m_triangle_selectors.clear(); + if (object == nullptr) + return; + + const std::vector colors = { + ColorRGBA(1.f, 1.f, 1.f, 0.f), + ColorRGBA(m_rgb_color[0], m_rgb_color[1], m_rgb_color[2], 1.f) + }; + for (const ModelVolume *volume : object->volumes) { + if (volume == nullptr || !volume->is_model_part()) + continue; + + m_triangle_selectors.emplace_back(std::make_unique(volume->mesh(), volume, colors, 0.2f)); + if (TriangleSelectorPatch *patch = dynamic_cast(m_triangle_selectors.back().get())) { + patch->set_none_state_rendered(false); + patch->set_texture_preview_needed(true); + patch->set_texture_preview_opaque(true); + } + m_triangle_selectors.back()->set_wireframe_needed(true); + m_triangle_selectors.back()->request_update_render_data(true); + } +} + +void GLGizmoTrueColorPainting::update_triangle_selectors_color() +{ + const std::vector colors = { + ColorRGBA(1.f, 1.f, 1.f, 0.f), + ColorRGBA(m_rgb_color[0], m_rgb_color[1], m_rgb_color[2], 1.f) + }; + for (std::unique_ptr &selector : m_triangle_selectors) { + TriangleSelectorPatch *patch = dynamic_cast(selector.get()); + if (patch == nullptr) + continue; + patch->set_ebt_colors(colors); + } + m_parent.set_as_dirty(); +} + +void GLGizmoTrueColorPainting::update_from_model_object(bool first_update) +{ + (void)first_update; + wxBusyCursor wait; + update_selected_object_color_state(); + init_model_triangle_selectors(); +} + +void GLGizmoTrueColorPainting::update_model_object() +{ + ModelObject *object = selected_model_object(); + if (object == nullptr) + return; + + bool updated = false; + int selector_idx = -1; + for (ModelVolume *volume : object->volumes) { + if (volume == nullptr || !volume->is_model_part()) + continue; + ++selector_idx; + if (selector_idx < 0 || + size_t(selector_idx) >= m_triangle_selectors.size() || + m_triangle_selectors[size_t(selector_idx)] == nullptr) + continue; + + std::vector> triangles_per_type; + m_triangle_selectors[size_t(selector_idx)]->get_facet_triangles(triangles_per_type); + const size_t paint_state = size_t(EnforcerBlockerType::ENFORCER); + if (triangles_per_type.size() <= paint_state || triangles_per_type[paint_state].empty()) + continue; + + if (volume->texture_mapping_color_facets.empty()) + initialize_volume_rgb_data_from_current_surface_color(*volume, ColorRGBA(1.f, 1.f, 1.f, 1.f)); + + const ColorRGBA brush_color(m_rgb_color[0], m_rgb_color[1], m_rgb_color[2], 1.f); + updated |= apply_rgb_stroke_to_volume(*volume, + triangles_per_type[paint_state], + brush_color, + m_brush_hardness, + m_opacity, + m_cursor_radius); + m_triangle_selectors[size_t(selector_idx)]->reset(); + m_triangle_selectors[size_t(selector_idx)]->request_update_render_data(true); + } + + if (!updated) + return; + + const unsigned int texture_mapping_filament_id = ensure_texture_mapping_zone(); + if (texture_mapping_filament_id != 0) { + object->config.set("extruder", int(texture_mapping_filament_id)); + for (ModelVolume *volume : object->volumes) + if (volume != nullptr && volume->is_model_part()) + volume->config.set("extruder", int(texture_mapping_filament_id)); + } + + refresh_selected_object_after_rgb_change(object); +} + +ModelObject *GLGizmoTrueColorPainting::selected_model_object() const +{ + if (m_c == nullptr) + return nullptr; + const auto *selection_info = m_c->selection_info(); + return selection_info != nullptr ? selection_info->model_object() : nullptr; +} + +void GLGizmoTrueColorPainting::update_selected_object_color_state() +{ + m_selected_has_rgb_data = false; + m_selected_has_imported_color_data = false; + m_selected_can_convert_vertex = false; + m_selected_can_convert_image = false; + + const ModelObject *object = selected_model_object(); + const ObjectID previous_object_id = m_selected_color_state_object_id; + m_selected_color_state_object_id = object != nullptr ? object->id() : ObjectID(); + if (m_selected_color_state_object_id != previous_object_id) + m_color_picker_source_cache.clear(); + if (object == nullptr) + return; + + for (const ModelVolume *volume : object->volumes) { + if (volume == nullptr || !volume->is_model_part()) + continue; + m_selected_has_rgb_data |= !volume->texture_mapping_color_facets.empty(); + m_selected_can_convert_vertex |= !volume->imported_vertex_colors_rgba.empty(); + m_selected_can_convert_image |= model_volume_has_bakeable_image_texture_data(volume); + } + m_selected_has_imported_color_data = m_selected_can_convert_vertex || m_selected_can_convert_image; +} + +bool GLGizmoTrueColorPainting::selected_object_has_rgb_data() const +{ + return m_selected_has_rgb_data; +} + +bool GLGizmoTrueColorPainting::selected_object_has_imported_color_data() const +{ + return m_selected_has_imported_color_data; +} + +void GLGizmoTrueColorPainting::initialize_selected_object_rgb_data() +{ + ModelObject *object = selected_model_object(); + if (object == nullptr) + return; + + bool initialized = false; + Plater::TakeSnapshot snapshot(wxGetApp().plater(), "Create blank RGB data", UndoRedo::SnapshotType::GizmoAction); + for (ModelVolume *volume : object->volumes) { + if (volume == nullptr || !volume->is_model_part()) + continue; + initialized |= initialize_volume_rgb_data(*volume, ColorRGBA(1.f, 1.f, 1.f, 1.f)); + } + + if (!initialized) + return; + + const unsigned int texture_mapping_filament_id = ensure_texture_mapping_zone(); + if (texture_mapping_filament_id != 0) { + object->config.set("extruder", int(texture_mapping_filament_id)); + for (ModelVolume *volume : object->volumes) + if (volume != nullptr && volume->is_model_part()) + volume->config.set("extruder", int(texture_mapping_filament_id)); + } + + refresh_selected_object_after_rgb_change(object); +} + +void GLGizmoTrueColorPainting::convert_selected_object_vertex_colors_to_rgb_data() +{ + ModelObject *object = selected_model_object(); + if (object == nullptr) + return; + + bool converted = false; + Plater::TakeSnapshot snapshot(wxGetApp().plater(), "Convert vertex colors to RGB data", UndoRedo::SnapshotType::GizmoAction); + for (ModelVolume *volume : object->volumes) { + if (volume == nullptr || !volume->is_model_part()) + continue; + + const indexed_triangle_set &its = volume->mesh().its; + if (its.vertices.empty() || + its.indices.empty() || + volume->imported_vertex_colors_rgba.size() != its.vertices.size()) + continue; + + TextureMappingColorSampler sampler = [volume, &its](size_t tri_idx, const Vec3f &, const Vec3f &barycentric) { + if (tri_idx >= its.indices.size()) + return 0xFFFFFFFFu; + + const stl_triangle_vertex_indices &tri = its.indices[tri_idx]; + if (tri[0] < 0 || tri[1] < 0 || tri[2] < 0) + return 0xFFFFFFFFu; + if (size_t(tri[0]) >= volume->imported_vertex_colors_rgba.size() || + size_t(tri[1]) >= volume->imported_vertex_colors_rgba.size() || + size_t(tri[2]) >= volume->imported_vertex_colors_rgba.size()) + return 0xFFFFFFFFu; + + const ColorRGBA c0 = unpack_vertex_color_rgba_for_conversion(volume->imported_vertex_colors_rgba[size_t(tri[0])]); + const ColorRGBA c1 = unpack_vertex_color_rgba_for_conversion(volume->imported_vertex_colors_rgba[size_t(tri[1])]); + const ColorRGBA c2 = unpack_vertex_color_rgba_for_conversion(volume->imported_vertex_colors_rgba[size_t(tri[2])]); + return pack_vertex_color_rgba(ColorRGBA(c0.r() * barycentric.x() + c1.r() * barycentric.y() + c2.r() * barycentric.z(), + c0.g() * barycentric.x() + c1.g() * barycentric.y() + c2.g() * barycentric.z(), + c0.b() * barycentric.x() + c1.b() * barycentric.y() + c2.b() * barycentric.z(), + c0.a() * barycentric.x() + c1.a() * barycentric.y() + c2.a() * barycentric.z())); + }; + + const float target_edge = std::max(mesh_max_axis_span(its) / 160.f, 0.25f); + TextureMappingColorSubdivisionDepths subdivision_depths = [target_edge](size_t, const std::array &vertices) { + const int depth = texture_mapping_depth_from_span(triangle_max_edge_length(vertices), target_edge, 5); + return std::make_pair(depth, depth); + }; + + volume->texture_mapping_color_facets.set_from_triangle_sampler(*volume, sampler, 5, 0.025f, subdivision_depths); + if (volume->texture_mapping_color_facets.metadata_json().empty()) + volume->texture_mapping_color_facets.set_metadata_json(rgb_metadata_json(ColorRGBA(1.f, 1.f, 1.f, 1.f))); + converted = true; + } + + if (!converted) + return; + + const unsigned int texture_mapping_filament_id = ensure_texture_mapping_zone(); + if (texture_mapping_filament_id != 0) { + object->config.set("extruder", int(texture_mapping_filament_id)); + for (ModelVolume *volume : object->volumes) + if (volume != nullptr && volume->is_model_part()) + volume->config.set("extruder", int(texture_mapping_filament_id)); + } + + refresh_selected_object_after_rgb_change(object); +} + +void GLGizmoTrueColorPainting::convert_selected_object_image_texture_to_rgb_data() +{ + ModelObject *object = selected_model_object(); + if (object == nullptr) + return; + + bool converted = false; + Plater::TakeSnapshot snapshot(wxGetApp().plater(), "Convert image texture to RGB data", UndoRedo::SnapshotType::GizmoAction); + for (ModelVolume *volume : object->volumes) { + if (volume == nullptr || !volume->is_model_part() || !model_volume_has_bakeable_image_texture_data(volume)) + continue; + + const indexed_triangle_set &its = volume->mesh().its; + TextureMappingColorSampler sampler = [volume, &its](size_t tri_idx, const Vec3f &, const Vec3f &barycentric) { + if (tri_idx >= its.indices.size() || + tri_idx >= volume->imported_texture_uv_valid.size() || + volume->imported_texture_uv_valid[tri_idx] == 0) + return 0xFFFFFFFFu; + + const size_t uv_offset = tri_idx * 6; + if (uv_offset + 5 >= volume->imported_texture_uvs_per_face.size()) + return 0xFFFFFFFFu; + + const Vec2f uv0(volume->imported_texture_uvs_per_face[uv_offset + 0], volume->imported_texture_uvs_per_face[uv_offset + 1]); + const Vec2f uv1(volume->imported_texture_uvs_per_face[uv_offset + 2], volume->imported_texture_uvs_per_face[uv_offset + 3]); + const Vec2f uv2(volume->imported_texture_uvs_per_face[uv_offset + 4], volume->imported_texture_uvs_per_face[uv_offset + 5]); + const Vec2f uv = uv0 * barycentric.x() + uv1 * barycentric.y() + uv2 * barycentric.z(); + return pack_vertex_color_rgba(sample_texture_rgba_for_vertex_bake(volume->imported_texture_rgba, + volume->imported_texture_width, + volume->imported_texture_height, + uv)); + }; + + const int safe_max_depth = texture_mapping_depth_for_budget(its.indices.size(), 7, 3200000); + TextureMappingColorSubdivisionDepths subdivision_depths = [volume, safe_max_depth](size_t tri_idx, const std::array &) { + const int depth = texture_mapping_depth_from_span(texture_triangle_uv_pixel_span(volume, tri_idx), 8.f, safe_max_depth); + return std::make_pair(depth, depth); + }; + + volume->texture_mapping_color_facets.set_from_triangle_sampler(*volume, sampler, safe_max_depth, 0.015f, subdivision_depths); + if (volume->texture_mapping_color_facets.metadata_json().empty()) + volume->texture_mapping_color_facets.set_metadata_json(rgb_metadata_json(ColorRGBA(1.f, 1.f, 1.f, 1.f))); + converted = true; + } + + if (!converted) + return; + + const unsigned int texture_mapping_filament_id = ensure_texture_mapping_zone(); + if (texture_mapping_filament_id != 0) { + object->config.set("extruder", int(texture_mapping_filament_id)); + for (ModelVolume *volume : object->volumes) + if (volume != nullptr && volume->is_model_part()) + volume->config.set("extruder", int(texture_mapping_filament_id)); + } + + refresh_selected_object_after_rgb_change(object); +} + +void GLGizmoTrueColorPainting::refresh_selected_object_after_rgb_change(ModelObject *object) +{ + update_selected_object_color_state(); + init_model_triangle_selectors(); + m_parent.update_volumes_colors_by_extruder(); + m_parent.set_as_dirty(); + + const ModelObjectPtrs &objects = wxGetApp().model().objects; + const size_t object_idx = size_t(std::find(objects.begin(), objects.end(), object) - objects.begin()); + if (object_idx < objects.size()) { + wxGetApp().obj_list()->update_info_items(object_idx); + wxGetApp().plater()->get_partplate_list().notify_instance_update(object_idx, 0); + } + m_parent.post_event(SimpleEvent(EVT_GLCANVAS_SCHEDULE_BACKGROUND_PROCESS)); +} + +bool GLGizmoTrueColorPainting::pick_color_from_model(const Vec2d &mouse_position) +{ + ColorRGBA color; + if (!sample_color_from_model(mouse_position, color)) + return false; + + set_active_color_from_sample(color); + return true; +} + +bool GLGizmoTrueColorPainting::sample_color_from_model(const Vec2d &mouse_position, ColorRGBA &color) const +{ + ModelObject *object = selected_model_object(); + if (object == nullptr) + return false; + + int mesh_idx = -1; + Vec3f hit = Vec3f::Zero(); + size_t tri_idx = 0; + if (!raycast_to_selected_mesh(mouse_position, mesh_idx, hit, tri_idx) || mesh_idx < 0) + return false; + + ModelVolume *volume = nullptr; + int part_idx = -1; + for (ModelVolume *candidate : object->volumes) { + if (candidate == nullptr || !candidate->is_model_part()) + continue; + ++part_idx; + if (part_idx == mesh_idx) { + volume = candidate; + break; + } + } + if (volume == nullptr) + return false; + + const indexed_triangle_set &its = volume->mesh().its; + 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; + + Vec3f barycentric = Vec3f::Zero(); + if (!barycentric_weights_for_region_vertex_colors(hit, + its.vertices[size_t(tri[0])].cast(), + its.vertices[size_t(tri[1])].cast(), + its.vertices[size_t(tri[2])].cast(), + barycentric)) + return false; + + if (!volume->texture_mapping_color_facets.empty()) { + const ColorPickerVolumeSourceCache &source = cached_volume_color_source(*volume); + if (std::optional sampled = sample_rgb_color_facets(source.rgb_facets, + source.rgb_by_source_triangle, + int(tri_idx), + hit)) { + color = *sampled; + } else { + color = rgb_metadata_background_color(volume->texture_mapping_color_facets); + } + return true; + } + + const VolumeColorSource source; + color = sample_volume_color_source(*volume, source, tri_idx, hit, barycentric); + return true; +} + +const GLGizmoTrueColorPainting::ColorPickerVolumeSourceCache & +GLGizmoTrueColorPainting::cached_volume_color_source(const ModelVolume &volume) const +{ + const ObjectID volume_id = volume.id(); + const ObjectBase::Timestamp timestamp = volume.texture_mapping_color_facets.timestamp(); + auto cache_it = std::find_if(m_color_picker_source_cache.begin(), + m_color_picker_source_cache.end(), + [volume_id](const ColorPickerVolumeSourceCache &cache) { + return cache.volume_id == volume_id; + }); + if (cache_it == m_color_picker_source_cache.end()) { + m_color_picker_source_cache.emplace_back(); + cache_it = m_color_picker_source_cache.end() - 1; + cache_it->volume_id = volume_id; + } + if (cache_it->timestamp != timestamp) { + cache_it->timestamp = timestamp; + cache_it->rgb_facets.clear(); + cache_it->rgb_by_source_triangle.clear(); + volume.texture_mapping_color_facets.get_facet_triangles(volume, cache_it->rgb_facets); + cache_it->rgb_by_source_triangle.reserve(cache_it->rgb_facets.size()); + for (size_t idx = 0; idx < cache_it->rgb_facets.size(); ++idx) + cache_it->rgb_by_source_triangle[cache_it->rgb_facets[idx].source_triangle].emplace_back(idx); + } + return *cache_it; +} + +void GLGizmoTrueColorPainting::set_active_color_from_sample(const ColorRGBA &color) +{ + m_rgb_color[0] = std::clamp(color.r(), 0.f, 1.f); + m_rgb_color[1] = std::clamp(color.g(), 0.f, 1.f); + m_rgb_color[2] = std::clamp(color.b(), 0.f, 1.f); + m_rgb_color[3] = 1.f; + sync_active_color_mode_from_rgb(true); + update_triangle_selectors_color(); +} + +void GLGizmoTrueColorPainting::sync_cmy_from_rgb() +{ + m_cmy_color[0] = 1.f - std::clamp(m_rgb_color[0], 0.f, 1.f); + m_cmy_color[1] = 1.f - std::clamp(m_rgb_color[1], 0.f, 1.f); + m_cmy_color[2] = 1.f - std::clamp(m_rgb_color[2], 0.f, 1.f); +} + +void GLGizmoTrueColorPainting::sync_rgb_from_cmy() +{ + m_rgb_color[0] = 1.f - std::clamp(m_cmy_color[0], 0.f, 1.f); + m_rgb_color[1] = 1.f - std::clamp(m_cmy_color[1], 0.f, 1.f); + m_rgb_color[2] = 1.f - std::clamp(m_cmy_color[2], 0.f, 1.f); +} + +void GLGizmoTrueColorPainting::sync_cmyk_from_rgb() +{ + const float r = std::clamp(m_rgb_color[0], 0.f, 1.f); + const float g = std::clamp(m_rgb_color[1], 0.f, 1.f); + const float b = std::clamp(m_rgb_color[2], 0.f, 1.f); + const float k = 1.f - std::max({ r, g, b }); + m_cmyk_color[3] = std::clamp(k, 0.f, 1.f); + if (k >= 1.f - EPSILON) { + m_cmyk_color[0] = 0.f; + m_cmyk_color[1] = 0.f; + m_cmyk_color[2] = 0.f; + return; + } + + const float denom = 1.f - k; + m_cmyk_color[0] = std::clamp((1.f - r - k) / denom, 0.f, 1.f); + m_cmyk_color[1] = std::clamp((1.f - g - k) / denom, 0.f, 1.f); + m_cmyk_color[2] = std::clamp((1.f - b - k) / denom, 0.f, 1.f); +} + +void GLGizmoTrueColorPainting::sync_rgb_from_cmyk() +{ + const float c = std::clamp(m_cmyk_color[0], 0.f, 1.f); + const float m = std::clamp(m_cmyk_color[1], 0.f, 1.f); + const float y = std::clamp(m_cmyk_color[2], 0.f, 1.f); + const float k = std::clamp(m_cmyk_color[3], 0.f, 1.f); + m_rgb_color[0] = std::clamp((1.f - c) * (1.f - k), 0.f, 1.f); + m_rgb_color[1] = std::clamp((1.f - m) * (1.f - k), 0.f, 1.f); + m_rgb_color[2] = std::clamp((1.f - y) * (1.f - k), 0.f, 1.f); +} + +void GLGizmoTrueColorPainting::sync_cmyw_from_rgb() +{ + const std::vector colors = { + ColorRGBA(0.f, 1.f, 1.f, 1.f), + ColorRGBA(1.f, 0.f, 1.f, 1.f), + ColorRGBA(1.f, 1.f, 0.f, 1.f), + ColorRGBA(1.f, 1.f, 1.f, 1.f) + }; + const std::vector weights = closest_color_mix_weights(colors, ColorRGBA(m_rgb_color[0], m_rgb_color[1], m_rgb_color[2], 1.f)); + for (size_t idx = 0; idx < m_cmyw_color.size() && idx < weights.size(); ++idx) + m_cmyw_color[idx] = weights[idx]; +} + +void GLGizmoTrueColorPainting::sync_rgb_from_cmyw() +{ + const std::vector colors = { + ColorRGBA(0.f, 1.f, 1.f, 1.f), + ColorRGBA(1.f, 0.f, 1.f, 1.f), + ColorRGBA(1.f, 1.f, 0.f, 1.f), + ColorRGBA(1.f, 1.f, 1.f, 1.f) + }; + const std::vector weights(m_cmyw_color.begin(), m_cmyw_color.end()); + const ColorRGBA mixed = color_mix_from_weights(colors, weights, ColorRGBA(1.f, 1.f, 1.f, 1.f)); + m_rgb_color[0] = mixed.r(); + m_rgb_color[1] = mixed.g(); + m_rgb_color[2] = mixed.b(); +} + +void GLGizmoTrueColorPainting::sync_rgbk_from_rgb() +{ + const std::vector colors = { + ColorRGBA(1.f, 0.f, 0.f, 1.f), + ColorRGBA(0.f, 1.f, 0.f, 1.f), + ColorRGBA(0.f, 0.f, 1.f, 1.f), + ColorRGBA(0.f, 0.f, 0.f, 1.f) + }; + const std::vector weights = closest_color_mix_weights(colors, ColorRGBA(m_rgb_color[0], m_rgb_color[1], m_rgb_color[2], 1.f)); + for (size_t idx = 0; idx < m_rgbk_color.size() && idx < weights.size(); ++idx) + m_rgbk_color[idx] = weights[idx]; +} + +void GLGizmoTrueColorPainting::sync_rgb_from_rgbk() +{ + const std::vector colors = { + ColorRGBA(1.f, 0.f, 0.f, 1.f), + ColorRGBA(0.f, 1.f, 0.f, 1.f), + ColorRGBA(0.f, 0.f, 1.f, 1.f), + ColorRGBA(0.f, 0.f, 0.f, 1.f) + }; + const std::vector weights(m_rgbk_color.begin(), m_rgbk_color.end()); + const ColorRGBA mixed = color_mix_from_weights(colors, weights, ColorRGBA(0.f, 0.f, 0.f, 1.f)); + m_rgb_color[0] = mixed.r(); + m_rgb_color[1] = mixed.g(); + m_rgb_color[2] = mixed.b(); +} + +void GLGizmoTrueColorPainting::sync_rgbw_from_rgb() +{ + const std::vector colors = { + ColorRGBA(1.f, 0.f, 0.f, 1.f), + ColorRGBA(0.f, 1.f, 0.f, 1.f), + ColorRGBA(0.f, 0.f, 1.f, 1.f), + ColorRGBA(1.f, 1.f, 1.f, 1.f) + }; + const std::vector weights = closest_color_mix_weights(colors, ColorRGBA(m_rgb_color[0], m_rgb_color[1], m_rgb_color[2], 1.f)); + for (size_t idx = 0; idx < m_rgbw_color.size() && idx < weights.size(); ++idx) + m_rgbw_color[idx] = weights[idx]; +} + +void GLGizmoTrueColorPainting::sync_rgb_from_rgbw() +{ + const std::vector colors = { + ColorRGBA(1.f, 0.f, 0.f, 1.f), + ColorRGBA(0.f, 1.f, 0.f, 1.f), + ColorRGBA(0.f, 0.f, 1.f, 1.f), + ColorRGBA(1.f, 1.f, 1.f, 1.f) + }; + const std::vector weights(m_rgbw_color.begin(), m_rgbw_color.end()); + const ColorRGBA mixed = color_mix_from_weights(colors, weights, ColorRGBA(1.f, 1.f, 1.f, 1.f)); + m_rgb_color[0] = mixed.r(); + m_rgb_color[1] = mixed.g(); + m_rgb_color[2] = mixed.b(); +} + +void GLGizmoTrueColorPainting::sync_bw_from_rgb() +{ + const float r = std::clamp(m_rgb_color[0], 0.f, 1.f); + const float g = std::clamp(m_rgb_color[1], 0.f, 1.f); + const float b = std::clamp(m_rgb_color[2], 0.f, 1.f); + const float luminance = std::clamp(0.2126f * r + 0.7152f * g + 0.0722f * b, 0.f, 1.f); + m_bw_color[0] = 1.f - luminance; + m_bw_color[1] = luminance; +} + +void GLGizmoTrueColorPainting::sync_rgb_from_bw() +{ + const float black = std::clamp(m_bw_color[0], 0.f, 1.f); + const float white = std::clamp(m_bw_color[1], 0.f, 1.f); + const float sum = black + white; + const float value = sum <= EPSILON ? 1.f : white / sum; + m_rgb_color[0] = value; + m_rgb_color[1] = value; + m_rgb_color[2] = value; +} + +void GLGizmoTrueColorPainting::ensure_filament_mix_colors() +{ + std::vector colors = get_extruders_colors(); + const size_t physical_count = size_t(std::max(wxGetApp().filaments_cnt(), 0)); + if (physical_count > 0 && colors.size() > physical_count) + colors.resize(physical_count); + + bool changed = colors.size() != m_filament_mix_colors.size(); + if (!changed) { + for (size_t idx = 0; idx < colors.size(); ++idx) { + if (colors[idx] != m_filament_mix_colors[idx]) { + changed = true; + break; + } + } + } + + if (changed) { + m_filament_mix_colors = std::move(colors); + m_filament_mix.clear(); + } + + if (m_filament_mix.size() != m_filament_mix_colors.size()) + sync_filament_mix_from_rgb(); +} + +void GLGizmoTrueColorPainting::sync_filament_mix_from_rgb() +{ + m_filament_mix = closest_color_mix_weights(m_filament_mix_colors, ColorRGBA(m_rgb_color[0], m_rgb_color[1], m_rgb_color[2], 1.f)); +} + +void GLGizmoTrueColorPainting::sync_rgb_from_filament_mix() +{ + const ColorRGBA mixed = color_mix_from_weights(m_filament_mix_colors, + m_filament_mix, + ColorRGBA(m_rgb_color[0], m_rgb_color[1], m_rgb_color[2], 1.f)); + m_rgb_color[0] = mixed.r(); + m_rgb_color[1] = mixed.g(); + m_rgb_color[2] = mixed.b(); +} + +void GLGizmoTrueColorPainting::sync_active_color_mode_from_rgb(bool update_filament_mix) +{ + switch (m_color_input_mode) { + case ColorInputMode::FilamentColors: + ensure_filament_mix_colors(); + if (update_filament_mix) + sync_filament_mix_from_rgb(); + break; + case ColorInputMode::RGB: + break; + case ColorInputMode::CMY: + sync_cmy_from_rgb(); + break; + case ColorInputMode::CMYK: + sync_cmyk_from_rgb(); + break; + case ColorInputMode::CMYW: + sync_cmyw_from_rgb(); + break; + case ColorInputMode::RGBK: + sync_rgbk_from_rgb(); + break; + case ColorInputMode::RGBW: + sync_rgbw_from_rgb(); + break; + case ColorInputMode::BW: + sync_bw_from_rgb(); + break; + } +} + +bool GLGizmoTrueColorPainting::render_rgb_picker(float item_width) +{ + bool changed = false; + ImGui::PushItemWidth(item_width); + ImGuiColorEditFlags flags = ImGuiColorEditFlags_DisplayRGB | + ImGuiColorEditFlags_InputRGB | + ImGuiColorEditFlags_NoInputs; + changed |= ImGui::ColorEdit3("##true_color_rgb_visual", m_rgb_color.data(), flags); + changed |= ImGui::SliderFloat("Red", &m_rgb_color[0], 0.f, 1.f, "%.2f"); + changed |= ImGui::SliderFloat("Green", &m_rgb_color[1], 0.f, 1.f, "%.2f"); + changed |= ImGui::SliderFloat("Blue", &m_rgb_color[2], 0.f, 1.f, "%.2f"); + ImGui::PopItemWidth(); + return changed; +} + +bool GLGizmoTrueColorPainting::render_cmy_picker(float item_width) +{ + bool changed = false; + ImGui::PushItemWidth(item_width); + ImGuiColorEditFlags flags = ImGuiColorEditFlags_DisplayRGB | + ImGuiColorEditFlags_InputRGB | + ImGuiColorEditFlags_NoInputs; + if (ImGui::ColorEdit3("##true_color_cmy_visual", m_rgb_color.data(), flags)) { + sync_cmy_from_rgb(); + changed = true; + } + + bool cmy_changed = false; + cmy_changed |= ImGui::SliderFloat("Cyan", &m_cmy_color[0], 0.f, 1.f, "%.2f"); + cmy_changed |= ImGui::SliderFloat("Magenta", &m_cmy_color[1], 0.f, 1.f, "%.2f"); + cmy_changed |= ImGui::SliderFloat("Yellow", &m_cmy_color[2], 0.f, 1.f, "%.2f"); + ImGui::PopItemWidth(); + + if (cmy_changed) { + sync_rgb_from_cmy(); + changed = true; + } + return changed; +} + +bool GLGizmoTrueColorPainting::render_cmyk_picker(float item_width) +{ + bool changed = false; + ImGui::PushItemWidth(item_width); + ImGuiColorEditFlags flags = ImGuiColorEditFlags_DisplayRGB | + ImGuiColorEditFlags_InputRGB | + ImGuiColorEditFlags_NoInputs; + if (ImGui::ColorEdit3("##true_color_cmyk_visual", m_rgb_color.data(), flags)) { + sync_cmyk_from_rgb(); + changed = true; + } + + bool cmyk_changed = false; + cmyk_changed |= ImGui::SliderFloat("Cyan", &m_cmyk_color[0], 0.f, 1.f, "%.2f"); + cmyk_changed |= ImGui::SliderFloat("Magenta", &m_cmyk_color[1], 0.f, 1.f, "%.2f"); + cmyk_changed |= ImGui::SliderFloat("Yellow", &m_cmyk_color[2], 0.f, 1.f, "%.2f"); + cmyk_changed |= ImGui::SliderFloat("Key", &m_cmyk_color[3], 0.f, 1.f, "%.2f"); + ImGui::PopItemWidth(); + + if (cmyk_changed) { + sync_rgb_from_cmyk(); + changed = true; + } + return changed; +} + +bool GLGizmoTrueColorPainting::render_cmyw_picker(float item_width) +{ + bool changed = false; + ImGui::PushItemWidth(item_width); + ImGuiColorEditFlags flags = ImGuiColorEditFlags_DisplayRGB | + ImGuiColorEditFlags_InputRGB | + ImGuiColorEditFlags_NoInputs; + if (ImGui::ColorEdit3("##true_color_cmyw_visual", m_rgb_color.data(), flags)) { + sync_cmyw_from_rgb(); + changed = true; + } + + bool cmyw_changed = false; + cmyw_changed |= ImGui::SliderFloat("Cyan", &m_cmyw_color[0], 0.f, 1.f, "%.2f"); + cmyw_changed |= ImGui::SliderFloat("Magenta", &m_cmyw_color[1], 0.f, 1.f, "%.2f"); + cmyw_changed |= ImGui::SliderFloat("Yellow", &m_cmyw_color[2], 0.f, 1.f, "%.2f"); + cmyw_changed |= ImGui::SliderFloat("White", &m_cmyw_color[3], 0.f, 1.f, "%.2f"); + ImGui::PopItemWidth(); + + if (cmyw_changed) { + sync_rgb_from_cmyw(); + changed = true; + } + return changed; +} + +bool GLGizmoTrueColorPainting::render_rgbk_picker(float item_width) +{ + bool changed = false; + ImGui::PushItemWidth(item_width); + ImGuiColorEditFlags flags = ImGuiColorEditFlags_DisplayRGB | + ImGuiColorEditFlags_InputRGB | + ImGuiColorEditFlags_NoInputs; + if (ImGui::ColorEdit3("##true_color_rgbk_visual", m_rgb_color.data(), flags)) { + sync_rgbk_from_rgb(); + changed = true; + } + + bool rgbk_changed = false; + rgbk_changed |= ImGui::SliderFloat("Red", &m_rgbk_color[0], 0.f, 1.f, "%.2f"); + rgbk_changed |= ImGui::SliderFloat("Green", &m_rgbk_color[1], 0.f, 1.f, "%.2f"); + rgbk_changed |= ImGui::SliderFloat("Blue", &m_rgbk_color[2], 0.f, 1.f, "%.2f"); + rgbk_changed |= ImGui::SliderFloat("Black", &m_rgbk_color[3], 0.f, 1.f, "%.2f"); + ImGui::PopItemWidth(); + + if (rgbk_changed) { + sync_rgb_from_rgbk(); + changed = true; + } + return changed; +} + +bool GLGizmoTrueColorPainting::render_rgbw_picker(float item_width) +{ + bool changed = false; + ImGui::PushItemWidth(item_width); + ImGuiColorEditFlags flags = ImGuiColorEditFlags_DisplayRGB | + ImGuiColorEditFlags_InputRGB | + ImGuiColorEditFlags_NoInputs; + if (ImGui::ColorEdit3("##true_color_rgbw_visual", m_rgb_color.data(), flags)) { + sync_rgbw_from_rgb(); + changed = true; + } + + bool rgbw_changed = false; + rgbw_changed |= ImGui::SliderFloat("Red", &m_rgbw_color[0], 0.f, 1.f, "%.2f"); + rgbw_changed |= ImGui::SliderFloat("Green", &m_rgbw_color[1], 0.f, 1.f, "%.2f"); + rgbw_changed |= ImGui::SliderFloat("Blue", &m_rgbw_color[2], 0.f, 1.f, "%.2f"); + rgbw_changed |= ImGui::SliderFloat("White", &m_rgbw_color[3], 0.f, 1.f, "%.2f"); + ImGui::PopItemWidth(); + + if (rgbw_changed) { + sync_rgb_from_rgbw(); + changed = true; + } + return changed; +} + +bool GLGizmoTrueColorPainting::render_bw_picker(float item_width) +{ + bool changed = false; + ImGui::PushItemWidth(item_width); + ImGuiColorEditFlags flags = ImGuiColorEditFlags_DisplayRGB | + ImGuiColorEditFlags_InputRGB | + ImGuiColorEditFlags_NoInputs; + if (ImGui::ColorEdit3("##true_color_bw_visual", m_rgb_color.data(), flags)) { + sync_bw_from_rgb(); + changed = true; + } + + float value = std::clamp(m_bw_color[1], 0.f, 1.f); + const bool slider_changed = ImGui::SliderFloat("Black / White", &value, 0.f, 1.f, "%.2f"); + ImGui::PopItemWidth(); + + if (slider_changed) { + m_bw_color[0] = 1.f - value; + m_bw_color[1] = value; + sync_rgb_from_bw(); + changed = true; + } + return changed; +} + +bool GLGizmoTrueColorPainting::render_filament_colors_picker(float item_width) +{ + ensure_filament_mix_colors(); + if (m_filament_mix_colors.empty()) { + m_imgui->text(_L("No real filaments are available.")); + return false; + } + + bool changed = false; + ImGui::PushItemWidth(item_width); + ImGuiColorEditFlags flags = ImGuiColorEditFlags_DisplayRGB | + ImGuiColorEditFlags_InputRGB | + ImGuiColorEditFlags_NoInputs; + if (ImGui::ColorEdit3("##true_color_filament_visual", m_rgb_color.data(), flags)) + changed = true; + if (ImGui::IsItemDeactivatedAfterEdit()) + sync_filament_mix_from_rgb(); + + bool mix_changed = false; + for (size_t idx = 0; idx < m_filament_mix.size(); ++idx) { + const std::string label = GUI::format(_u8L("Filament %1%"), idx + 1); + mix_changed |= ImGui::SliderFloat(label.c_str(), &m_filament_mix[idx], 0.f, 1.f, "%.2f"); + } + ImGui::PopItemWidth(); + + if (mix_changed) { + sync_rgb_from_filament_mix(); + changed = true; + } + return changed; +} + +void GLGizmoTrueColorPainting::on_render_input_window(float x, float y, float bottom_limit) +{ + ModelObject *object = selected_model_object(); + if (object == nullptr) + return; + if (object->id() != m_selected_color_state_object_id) + update_selected_object_color_state(); + + const float approx_height = m_imgui->scaled(22.0f); + y = std::min(y, bottom_limit - approx_height); + GizmoImguiSetNextWIndowPos(x, y, ImGuiCond_Always); + + ImGuiWrapper::push_toolbar_style(m_parent.get_scale()); + GizmoImguiBegin(get_name(), ImGuiWindowFlags_NoMove | ImGuiWindowFlags_AlwaysAutoResize | + ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoTitleBar); + + const float slider_width = m_imgui->scaled(8.f); + const float max_tooltip_width = ImGui::GetFontSize() * 20.f; + const char *mode_labels[] = { + "Filament colors", + "RGB", + "CMY", + "CMYK", + "CMYW", + "RGBK", + "RGBW", + "BW" + }; + const int mode_count = int(sizeof(mode_labels) / sizeof(mode_labels[0])); + int mode = std::clamp(int(m_color_input_mode), 0, mode_count - 1); + if (ImGui::BeginCombo("##true_color_mode", mode_labels[mode])) { + for (int idx = 0; idx < mode_count; ++idx) { + const bool selected = idx == mode; + if (ImGui::Selectable(mode_labels[idx], selected)) { + mode = idx; + m_color_input_mode = ColorInputMode(mode); + sync_active_color_mode_from_rgb(true); + update_triangle_selectors_color(); + } + if (selected) + ImGui::SetItemDefaultFocus(); + } + ImGui::EndCombo(); + } + + const wxString picker_label = m_color_picker_active ? _L("Cancel color picker") : _L("Pick color from model"); + if (m_imgui->button(picker_label)) { + m_color_picker_active = !m_color_picker_active; + m_parent.set_as_dirty(); + } + + bool color_changed = false; + switch (m_color_input_mode) { + case ColorInputMode::FilamentColors: + color_changed = render_filament_colors_picker(slider_width); + break; + case ColorInputMode::RGB: + color_changed = render_rgb_picker(slider_width); + break; + case ColorInputMode::CMY: + color_changed = render_cmy_picker(slider_width); + break; + case ColorInputMode::CMYK: + color_changed = render_cmyk_picker(slider_width); + break; + case ColorInputMode::CMYW: + color_changed = render_cmyw_picker(slider_width); + break; + case ColorInputMode::RGBK: + color_changed = render_rgbk_picker(slider_width); + break; + case ColorInputMode::RGBW: + color_changed = render_rgbw_picker(slider_width); + break; + case ColorInputMode::BW: + color_changed = render_bw_picker(slider_width); + break; + } + if (color_changed) + update_triangle_selectors_color(); + + ImGui::Separator(); + m_imgui->text(_L("Pen size")); + ImGui::PushItemWidth(slider_width); + m_imgui->bbl_slider_float_style("##true_color_cursor_radius", &m_cursor_radius, CursorRadiusMin, CursorRadiusMax, "%.2f", 1.f, true); + ImGui::PopItemWidth(); + + float softness_pct = (1.f - m_brush_hardness) * 100.f; + m_imgui->text(_L("Brush softness")); + ImGui::PushItemWidth(slider_width); + if (m_imgui->bbl_slider_float_style("##true_color_softness", &softness_pct, 0.f, 100.f, "%.0f%%", 1.f, true)) + m_brush_hardness = 1.f - std::clamp(softness_pct / 100.f, 0.f, 1.f); + ImGui::PopItemWidth(); + + float opacity_pct = m_opacity * 100.f; + m_imgui->text(_L("Opacity")); + ImGui::PushItemWidth(slider_width); + if (m_imgui->bbl_slider_float_style("##true_color_opacity", &opacity_pct, 0.f, 100.f, "%.0f%%", 1.f, true)) { + m_opacity = std::clamp(opacity_pct / 100.f, 0.f, 1.f); + m_parent.set_as_dirty(); + } + ImGui::PopItemWidth(); + + ImGui::Separator(); + if (m_c->object_clipper()->get_position() == 0.f) { + m_imgui->text(_L("Section view")); + } else if (m_imgui->button(_L("Reset direction"))) { + wxGetApp().CallAfter([this]() { + m_c->object_clipper()->set_position_by_ratio(-1., false); + }); + } + + float clp_dist = float(m_c->object_clipper()->get_position()); + ImGui::PushItemWidth(slider_width); + if (m_imgui->bbl_slider_float_style("##true_color_clp_dist", &clp_dist, 0.f, 1.f, "%.2f", 1.f, true)) + m_c->object_clipper()->set_position_by_ratio(clp_dist, true); + ImGui::PopItemWidth(); + + if (ImGui::IsItemHovered()) + m_imgui->tooltip(_L("Section view"), max_tooltip_width); + + GizmoImguiEnd(); + ImGuiWrapper::pop_toolbar_style(); +} + +wxString GLGizmoTrueColorPainting::handle_snapshot_action_name(bool shift_down, GLGizmoPainterBase::Button button_down) const +{ + (void)shift_down; + (void)button_down; + return _L("Paint RGB color"); +} + +GLGizmoImageProjection::GLGizmoImageProjection(GLCanvas3D& parent, const std::string& icon_filename, unsigned int sprite_id) + : GLGizmoBase(parent, icon_filename, sprite_id) +{ +} + +bool GLGizmoImageProjection::on_init() +{ + return true; +} + +void GLGizmoImageProjection::on_render() +{ +} + +std::string GLGizmoImageProjection::on_get_name() const +{ + return _u8L("Project image to model surface"); +} + +void GLGizmoImageProjection::on_set_state() +{ + if (get_state() == On) { + m_parent.enable_picking(false); + m_projection_mode_initialized = false; + } else if (get_state() == Off) { + m_parent.enable_picking(true); + m_parent.toggle_model_objects_visibility(true); + } +} + +bool GLGizmoImageProjection::on_is_selectable() const +{ + return wxGetApp().preset_bundle->printers.get_edited_preset().printer_technology() == ptFFF; +} + +bool GLGizmoImageProjection::on_is_activable() const +{ + const Selection& selection = m_parent.get_selection(); + return wxGetApp().preset_bundle->printers.get_edited_preset().printer_technology() == ptFFF && + !selection.is_empty() && + (selection.is_single_full_instance() || selection.is_any_volume()); +} + +CommonGizmosDataID GLGizmoImageProjection::on_get_requirements() const +{ + return CommonGizmosDataID(int(CommonGizmosDataID::SelectionInfo) | int(CommonGizmosDataID::InstancesHider)); +} + +bool GLGizmoImageProjection::load_projection_image() +{ + m_image_error.clear(); + wxFileDialog dialog(wxGetApp().mainframe, + _L("Load projection image"), + "", + "", + _L("Image files (*.png;*.jpg;*.jpeg;*.bmp)|*.png;*.jpg;*.jpeg;*.bmp|All files (*.*)|*.*"), + wxFD_OPEN | wxFD_FILE_MUST_EXIST); + if (dialog.ShowModal() != wxID_OK) + return false; + + wxImage image(dialog.GetPath(), wxBITMAP_TYPE_ANY); + std::vector rgba; + uint32_t width = 0; + uint32_t height = 0; + if (!wx_image_to_rgba(image, rgba, width, height)) { + m_image_error = _u8L("Unable to load the selected image."); + return false; + } + + m_image_path = into_u8(dialog.GetPath()); + m_image_rgba = std::move(rgba); + m_image_width = width; + m_image_height = height; + m_overlay_texture_dirty = true; + m_parent.set_as_dirty(); + return true; +} + +void GLGizmoImageProjection::clear_projection_image() +{ + m_image_path.clear(); + m_image_error.clear(); + m_image_rgba.clear(); + m_image_width = 0; + m_image_height = 0; + m_overlay_texture.reset(); + m_overlay_texture_dirty = false; + m_parent.set_as_dirty(); +} + +bool GLGizmoImageProjection::ensure_overlay_texture() +{ + if (m_overlay_texture.get_id() != 0 && !m_overlay_texture_dirty) + return true; + if (m_image_rgba.empty() || m_image_width == 0 || m_image_height == 0) + return false; + + std::vector raw(m_image_rgba.begin(), m_image_rgba.end()); + if (!m_overlay_texture.load_from_raw_data(std::move(raw), m_image_width, m_image_height)) { + m_image_error = _u8L("Unable to display the selected image."); + return false; + } + m_overlay_texture_dirty = false; + return true; +} + +GLGizmoImageProjection::OverlayRect GLGizmoImageProjection::overlay_rect() const +{ + OverlayRect rect; + if (m_image_width == 0 || m_image_height == 0) + return rect; + + const Size canvas_size = m_parent.get_canvas_size(); + const float canvas_w = float(std::max(1, canvas_size.get_width())); + const float canvas_h = float(std::max(1, canvas_size.get_height())); + const float max_w = canvas_w * 0.48f; + const float max_h = canvas_h * 0.48f; + float scale = std::min(max_w / float(m_image_width), max_h / float(m_image_height)); + if (!std::isfinite(scale) || scale <= 0.f) + scale = 1.f; + + rect.width = float(m_image_width) * scale; + rect.height = float(m_image_height) * scale; + rect.left = (canvas_w - rect.width) * 0.5f; + rect.top = (canvas_h - rect.height) * 0.5f; + return rect; +} + +ModelObject *GLGizmoImageProjection::selected_model_object() const +{ + if (m_c == nullptr) + return nullptr; + const auto *selection_info = m_c->selection_info(); + return selection_info != nullptr ? selection_info->model_object() : nullptr; +} + +void GLGizmoImageProjection::update_default_projection_mode() +{ + const ModelObject *object = selected_model_object(); + if (object == nullptr) + return; + + if (m_projection_mode_initialized && + object->id() == m_projection_mode_object_id && + projection_mode_allowed(m_projection_mode)) + return; + + m_projection_mode = default_projection_mode(); + m_projection_mode_initialized = true; + m_projection_mode_object_id = object->id(); +} + +GLGizmoImageProjection::ProjectionMode GLGizmoImageProjection::default_projection_mode() const +{ + if (selected_object_has_rgb_data()) + return ProjectionMode::RGBData; + if (selected_object_has_image_texture_data()) + return ProjectionMode::ImageTexture; + return ProjectionMode::RGBData; +} + +bool GLGizmoImageProjection::projection_mode_allowed(ProjectionMode mode) const +{ + if (selected_object_has_rgb_data()) + return mode == ProjectionMode::RGBData; + if (selected_object_has_image_texture_data()) + return mode == ProjectionMode::ImageTexture || mode == ProjectionMode::RGBData; + return true; +} + +bool GLGizmoImageProjection::selected_object_has_image_texture_data() const +{ + const ModelObject *object = selected_model_object(); + if (object == nullptr) + return false; + for (const ModelVolume *volume : object->volumes) + if (volume != nullptr && volume->is_model_part() && model_volume_has_bakeable_image_texture_data(volume)) + return true; + return false; +} + +bool GLGizmoImageProjection::selected_object_has_vertex_color_data() const +{ + const ModelObject *object = selected_model_object(); + if (object == nullptr) + return false; + for (const ModelVolume *volume : object->volumes) + if (volume != nullptr && volume->is_model_part() && !volume->imported_vertex_colors_rgba.empty()) + return true; + return false; +} + +bool GLGizmoImageProjection::selected_object_has_rgb_data() const +{ + const ModelObject *object = selected_model_object(); + if (object == nullptr) + return false; + for (const ModelVolume *volume : object->volumes) + if (volume != nullptr && volume->is_model_part() && !volume->texture_mapping_color_facets.empty()) + return true; + return false; +} + +void GLGizmoImageProjection::on_render_input_window(float x, float y, float bottom_limit) +{ + update_default_projection_mode(); + + if (ensure_overlay_texture()) { + const OverlayRect rect = overlay_rect(); + if (rect.width > 0.f && rect.height > 0.f) { + ImGui::SetNextWindowPos(ImVec2(rect.left, rect.top), ImGuiCond_Always); + ImGui::SetNextWindowSize(ImVec2(rect.width, rect.height), ImGuiCond_Always); + ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(0.f, 0.f)); + ImGui::PushStyleVar(ImGuiStyleVar_WindowBorderSize, 0.f); + ImGui::Begin("##image_projection_overlay", + nullptr, + ImGuiWindowFlags_NoDecoration | + ImGuiWindowFlags_NoInputs | + ImGuiWindowFlags_NoBackground | + ImGuiWindowFlags_NoSavedSettings); + ImGui::Image((void *)(intptr_t)m_overlay_texture.get_id(), + ImVec2(rect.width, rect.height), + ImVec2(0.f, 0.f), + ImVec2(1.f, 1.f), + ImVec4(1.f, 1.f, 1.f, 0.72f * std::clamp(m_projection_opacity, 0.f, 1.f))); + ImGui::End(); + ImGui::PopStyleVar(2); + } + } + + const float approx_height = m_imgui->scaled(12.0f); + y = std::min(y, bottom_limit - approx_height); + GizmoImguiSetNextWIndowPos(x, y, ImGuiCond_Always); + + ImGuiWrapper::push_toolbar_style(m_parent.get_scale()); + GizmoImguiBegin(get_name(), ImGuiWindowFlags_NoMove | ImGuiWindowFlags_AlwaysAutoResize | + ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoTitleBar); + + if (m_imgui->button(_L("Load image"))) + load_projection_image(); + + ImGui::SameLine(); + m_imgui->disabled_begin(m_image_rgba.empty()); + if (m_imgui->button(_L("Clear image"))) + clear_projection_image(); + m_imgui->disabled_end(); + + if (!m_image_path.empty()) { + const size_t slash = m_image_path.find_last_of("/\\"); + ImGui::SameLine(); + m_imgui->text(from_u8(slash == std::string::npos ? m_image_path : m_image_path.substr(slash + 1))); + } + + m_imgui->text(_L("Apply to:")); + ImGui::SameLine(); + const char *mode_labels[] = { "Vertex colors", "Image Texture", "RGB data" }; + int mode = std::clamp(int(m_projection_mode), 0, 2); + if (ImGui::BeginCombo("##projection_mode", mode_labels[mode])) { + for (int idx = 0; idx < 3; ++idx) { + const ProjectionMode candidate = ProjectionMode(idx); + if (!projection_mode_allowed(candidate)) + continue; + const bool selected = m_projection_mode == candidate; + if (ImGui::Selectable(mode_labels[idx], selected)) { + mode = idx; + m_projection_mode = candidate; + } + if (selected) + ImGui::SetItemDefaultFocus(); + } + ImGui::EndCombo(); + } + + float opacity_pct = m_projection_opacity * 100.f; + m_imgui->text(_L("Opacity")); + ImGui::PushItemWidth(m_imgui->scaled(8.f)); + if (m_imgui->bbl_slider_float_style("##image_projection_opacity", &opacity_pct, 0.f, 100.f, "%.0f%%", 1.f, true)) { + m_projection_opacity = std::clamp(opacity_pct / 100.f, 0.f, 1.f); + m_parent.set_as_dirty(); + } + ImGui::PopItemWidth(); + + ImGui::Checkbox("Apply transparent regions as background color", &m_apply_transparency_as_background); + ImGui::Checkbox("Pass through model", &m_pass_through_model); + + m_imgui->disabled_begin(m_image_rgba.empty()); + if (m_imgui->button(_L("Project image onto model"))) + project_image_to_selected_object(); + m_imgui->disabled_end(); + + if (!m_image_error.empty()) + m_imgui->warning_text(from_u8(m_image_error)); + + GizmoImguiEnd(); + ImGuiWrapper::pop_toolbar_style(); +} + +bool GLGizmoImageProjection::project_image_to_selected_object() +{ + ModelObject *object = selected_model_object(); + if (object == nullptr || m_image_rgba.empty()) + return false; + + update_default_projection_mode(); + if (!projection_mode_allowed(m_projection_mode)) + return false; + + bool changed = false; + Plater::TakeSnapshot snapshot(wxGetApp().plater(), "Project image onto model", UndoRedo::SnapshotType::GizmoAction); + switch (m_projection_mode) { + case ProjectionMode::VertexColors: + changed = project_to_vertex_colors(object); + break; + case ProjectionMode::ImageTexture: + changed = project_to_image_texture(object); + break; + case ProjectionMode::RGBData: + changed = project_to_rgb_data(object); + break; + } + + if (!changed) + return false; + + const unsigned int texture_mapping_filament_id = ensure_texture_mapping_zone(); + if (texture_mapping_filament_id != 0) { + object->config.set("extruder", int(texture_mapping_filament_id)); + for (ModelVolume *volume : object->volumes) + if (volume != nullptr && volume->is_model_part()) + volume->config.set("extruder", int(texture_mapping_filament_id)); + } + + refresh_projected_object(object); + m_projection_mode_initialized = true; + m_projection_mode_object_id = object->id(); + return true; +} + +bool GLGizmoImageProjection::project_to_vertex_colors(ModelObject *object) +{ + const Selection &selection = m_parent.get_selection(); + const int instance_idx = selection.get_instance_idx(); + const Camera &camera = wxGetApp().plater()->get_camera(); + const std::array &viewport = camera.get_viewport(); + const OverlayRect rect = overlay_rect(); + + ProjectionContext context; + context.view_projection = (camera.get_projection_matrix() * camera.get_view_matrix()).matrix(); + context.canvas_width = std::max(1, viewport[2]); + context.canvas_height = std::max(1, viewport[3]); + context.overlay_left = rect.left; + context.overlay_top = rect.top; + context.overlay_width = rect.width; + context.overlay_height = rect.height; + context.image_rgba = &m_image_rgba; + context.image_width = m_image_width; + context.image_height = m_image_height; + context.image_opacity = m_projection_opacity; + context.apply_transparency_as_background = m_apply_transparency_as_background; + + const ProjectionVisibility visibility = m_pass_through_model ? + ProjectionVisibility() : + build_projection_visibility(context, m_parent, object, instance_idx); + + bool changed = false; + for (ModelVolume *volume : object->volumes) { + if (volume == nullptr || !volume->is_model_part()) + continue; + + const indexed_triangle_set &its = volume->mesh().its; + if (its.vertices.empty() || its.indices.empty()) + continue; + + const VolumeColorSource source = build_volume_color_source(*volume); + const ColorRGBA fallback_color = projection_base_color_for_volume(*volume); + std::vector> base_accum(its.vertices.size(), { 0.f, 0.f, 0.f, 0.f }); + std::vector base_counts(its.vertices.size(), 0); + + for (size_t tri_idx = 0; tri_idx < its.indices.size(); ++tri_idx) { + const stl_triangle_vertex_indices &tri = its.indices[tri_idx]; + for (int corner = 0; corner < 3; ++corner) { + if (tri[corner] < 0 || size_t(tri[corner]) >= its.vertices.size()) + continue; + Vec3f barycentric = Vec3f::Zero(); + barycentric[corner] = 1.f; + const ColorRGBA color = sample_volume_color_source(*volume, + source, + tri_idx, + its.vertices[size_t(tri[corner])].cast(), + barycentric, + true, + &fallback_color); + std::array &accum = base_accum[size_t(tri[corner])]; + accum[0] += color.r(); + accum[1] += color.g(); + accum[2] += color.b(); + accum[3] += color.a(); + ++base_counts[size_t(tri[corner])]; + } + } + + std::vector base_colors(its.vertices.size(), ColorRGBA(1.f, 1.f, 1.f, 1.f)); + for (size_t idx = 0; idx < base_colors.size(); ++idx) { + if (base_counts[idx] == 0) + continue; + const float inv = 1.f / float(base_counts[idx]); + base_colors[idx] = ColorRGBA(base_accum[idx][0] * inv, + base_accum[idx][1] * inv, + base_accum[idx][2] * inv, + base_accum[idx][3] * inv); + } + + std::vector> projected_accum(its.vertices.size(), { 0.f, 0.f, 0.f, 0.f }); + std::vector projected_counts(its.vertices.size(), 0); + const Transform3d world_matrix = projection_world_matrix_for_volume(m_parent, object, volume, instance_idx); + + for (size_t tri_idx = 0; tri_idx < its.indices.size(); ++tri_idx) { + const stl_triangle_vertex_indices &tri = its.indices[tri_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 std::array vertices = { + its.vertices[size_t(tri[0])].cast(), + its.vertices[size_t(tri[1])].cast(), + its.vertices[size_t(tri[2])].cast() + }; + for (int corner = 0; corner < 3; ++corner) { + const size_t vertex_idx = size_t(tri[corner]); + if (!m_pass_through_model && !projection_point_is_visible(visibility, context, world_matrix, vertices[size_t(corner)])) + continue; + if (std::optional projected = projected_image_color_at_point(context, world_matrix, vertices[size_t(corner)])) { + if (!context.apply_transparency_as_background && !projection_overlay_has_paintable_alpha(*projected, context)) + continue; + const ColorRGBA color = apply_projection_color(base_colors[vertex_idx], *projected, context, false); + std::array &accum = projected_accum[vertex_idx]; + accum[0] += color.r(); + accum[1] += color.g(); + accum[2] += color.b(); + accum[3] += color.a(); + ++projected_counts[vertex_idx]; + } + } + } + + volume->imported_vertex_colors_rgba.assign(its.vertices.size(), 0xFFFFFFFFu); + for (size_t idx = 0; idx < its.vertices.size(); ++idx) { + ColorRGBA color = base_colors[idx]; + if (projected_counts[idx] > 0) { + const float inv = 1.f / float(projected_counts[idx]); + color = ColorRGBA(projected_accum[idx][0] * inv, + projected_accum[idx][1] * inv, + projected_accum[idx][2] * inv, + projected_accum[idx][3] * inv); + } + volume->imported_vertex_colors_rgba[idx] = pack_vertex_color_rgba(color); + } + changed = true; + } + return changed; +} + +bool GLGizmoImageProjection::project_to_image_texture(ModelObject *object) +{ + const Selection &selection = m_parent.get_selection(); + const int instance_idx = selection.get_instance_idx(); + const Camera &camera = wxGetApp().plater()->get_camera(); + const std::array &viewport = camera.get_viewport(); + const OverlayRect rect = overlay_rect(); + + ProjectionContext context; + context.view_projection = (camera.get_projection_matrix() * camera.get_view_matrix()).matrix(); + context.canvas_width = std::max(1, viewport[2]); + context.canvas_height = std::max(1, viewport[3]); + context.overlay_left = rect.left; + context.overlay_top = rect.top; + context.overlay_width = rect.width; + context.overlay_height = rect.height; + context.image_rgba = &m_image_rgba; + context.image_width = m_image_width; + context.image_height = m_image_height; + context.image_opacity = m_projection_opacity; + context.apply_transparency_as_background = m_apply_transparency_as_background; + + const ProjectionVisibility visibility = m_pass_through_model ? + ProjectionVisibility() : + build_projection_visibility(context, m_parent, object, instance_idx); + + bool changed = false; + for (ModelVolume *volume : object->volumes) { + if (volume == nullptr || !volume->is_model_part()) + continue; + + const indexed_triangle_set &its = volume->mesh().its; + if (its.vertices.empty() || its.indices.empty()) + continue; + + const bool generated_texture = !model_volume_has_bakeable_image_texture_data(volume); + if (generated_texture) { + const uint32_t texture_size = projection_texture_size_for_triangles(its.indices.size()); + const uint32_t grid = uint32_t(std::ceil(std::sqrt(double(std::max(its.indices.size(), 1))))); + const float tile = float(texture_size) / float(std::max(grid, 1)); + volume->imported_texture_width = texture_size; + volume->imported_texture_height = texture_size; + volume->imported_texture_rgba.assign(size_t(texture_size) * size_t(texture_size) * 4, 255); + volume->imported_texture_uv_valid.assign(its.indices.size(), 1); + volume->imported_texture_uvs_per_face.assign(its.indices.size() * 6, 0.f); + + for (size_t tri_idx = 0; tri_idx < its.indices.size(); ++tri_idx) { + const uint32_t cell_x = uint32_t(tri_idx % grid); + const uint32_t cell_y = uint32_t(tri_idx / grid); + const float left = float(cell_x) * tile + 0.5f; + const float top = float(cell_y) * tile + 0.5f; + const float right = std::min(float(texture_size) - 0.5f, float(cell_x + 1) * tile - 0.5f); + const float bottom = std::min(float(texture_size) - 0.5f, float(cell_y + 1) * tile - 0.5f); + const size_t uv = tri_idx * 6; + volume->imported_texture_uvs_per_face[uv + 0] = left / float(texture_size); + volume->imported_texture_uvs_per_face[uv + 1] = top / float(texture_size); + volume->imported_texture_uvs_per_face[uv + 2] = right / float(texture_size); + volume->imported_texture_uvs_per_face[uv + 3] = top / float(texture_size); + volume->imported_texture_uvs_per_face[uv + 4] = left / float(texture_size); + volume->imported_texture_uvs_per_face[uv + 5] = bottom / float(texture_size); + } + } + + const VolumeColorSource source = build_volume_color_source(*volume); + const ColorRGBA fallback_color = projection_base_color_for_volume(*volume); + const Transform3d world_matrix = projection_world_matrix_for_volume(m_parent, object, volume, instance_idx); + const bool rewrite_texture_base = generated_texture || !volume->texture_mapping_color_facets.empty(); + const std::vector source_texture_rgba(volume->imported_texture_rgba.begin(), volume->imported_texture_rgba.end()); + bool volume_changed = generated_texture; + + for (size_t tri_idx = 0; tri_idx < its.indices.size(); ++tri_idx) { + const stl_triangle_vertex_indices &tri = its.indices[tri_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; + if (tri_idx >= volume->imported_texture_uv_valid.size() || + volume->imported_texture_uv_valid[tri_idx] == 0) + continue; + + const size_t uv_offset = tri_idx * 6; + if (uv_offset + 5 >= volume->imported_texture_uvs_per_face.size()) + continue; + + const std::array uvs = unwrap_projection_uvs(std::array{ + Vec2f(volume->imported_texture_uvs_per_face[uv_offset + 0], volume->imported_texture_uvs_per_face[uv_offset + 1]), + Vec2f(volume->imported_texture_uvs_per_face[uv_offset + 2], volume->imported_texture_uvs_per_face[uv_offset + 3]), + Vec2f(volume->imported_texture_uvs_per_face[uv_offset + 4], volume->imported_texture_uvs_per_face[uv_offset + 5]) + }); + 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() + }; + const float texture_width = float(volume->imported_texture_width); + const float texture_height = float(volume->imported_texture_height); + if (texture_width <= 0.f || texture_height <= 0.f) + continue; + const std::array pixel_uvs = { + Vec2f(uvs[0].x() * texture_width, uvs[0].y() * texture_height), + Vec2f(uvs[1].x() * texture_width, uvs[1].y() * texture_height), + Vec2f(uvs[2].x() * texture_width, uvs[2].y() * texture_height) + }; + const float min_u = std::min({ uvs[0].x(), uvs[1].x(), uvs[2].x() }); + const float max_u = std::max({ uvs[0].x(), uvs[1].x(), uvs[2].x() }); + const float min_v = std::min({ uvs[0].y(), uvs[1].y(), uvs[2].y() }); + const float max_v = std::max({ uvs[0].y(), uvs[1].y(), uvs[2].y() }); + int min_x = int(std::floor(min_u * texture_width)) - 1; + int max_x = int(std::ceil(max_u * texture_width)) + 1; + int min_y = int(std::floor(min_v * texture_height)) - 1; + int max_y = int(std::ceil(max_v * texture_height)) + 1; + const bool uv_raster_too_large = + max_x - min_x > int(volume->imported_texture_width) * 2 || + max_y - min_y > int(volume->imported_texture_height) * 2; + + if (!uv_raster_too_large) { + for (int y_px = min_y; y_px <= max_y; ++y_px) { + for (int x_px = min_x; x_px <= max_x; ++x_px) { + const Vec2f pixel(float(x_px) + 0.5f, float(y_px) + 0.5f); + Vec3f barycentric = Vec3f::Zero(); + if (!conservative_barycentric_weights_2d(pixel, pixel_uvs[0], pixel_uvs[1], pixel_uvs[2], 0.7072f, barycentric)) + continue; + + const Vec3f point = vertices[0] * barycentric.x() + + vertices[1] * barycentric.y() + + vertices[2] * barycentric.z(); + ColorRGBA color = rewrite_texture_base ? + sample_volume_color_source(*volume, source, tri_idx, point, barycentric, false, &fallback_color) : + read_rgba_pixel(source_texture_rgba, + volume->imported_texture_width, + wrapped_texture_pixel(x_px, volume->imported_texture_width), + wrapped_texture_pixel(y_px, volume->imported_texture_height)); + if (m_pass_through_model || projection_point_is_visible(visibility, context, world_matrix, point)) { + if (std::optional projected = projected_image_color_at_point(context, world_matrix, point)) { + const bool transparent_sample = + !context.apply_transparency_as_background && + !projection_overlay_has_paintable_alpha(*projected, context); + if (transparent_sample) { + if (!rewrite_texture_base) { + continue; + } + } else { + color = apply_projection_color(color, *projected, context, true); + } + } else if (!rewrite_texture_base) { + continue; + } + } + volume_changed |= write_rgba_pixel(volume->imported_texture_rgba, + volume->imported_texture_width, + wrapped_texture_pixel(x_px, volume->imported_texture_width), + wrapped_texture_pixel(y_px, volume->imported_texture_height), + color); + } + } + } + } + if (volume_changed) { + refresh_imported_texture_storage(*volume); + changed = true; + } + } + return changed; +} + +bool GLGizmoImageProjection::project_to_rgb_data(ModelObject *object) +{ + const Selection &selection = m_parent.get_selection(); + const int instance_idx = selection.get_instance_idx(); + const Camera &camera = wxGetApp().plater()->get_camera(); + const std::array &viewport = camera.get_viewport(); + const OverlayRect rect = overlay_rect(); + + ProjectionContext context; + context.view_projection = (camera.get_projection_matrix() * camera.get_view_matrix()).matrix(); + context.canvas_width = std::max(1, viewport[2]); + context.canvas_height = std::max(1, viewport[3]); + context.overlay_left = rect.left; + context.overlay_top = rect.top; + context.overlay_width = rect.width; + context.overlay_height = rect.height; + context.image_rgba = &m_image_rgba; + context.image_width = m_image_width; + context.image_height = m_image_height; + context.image_opacity = m_projection_opacity; + context.apply_transparency_as_background = m_apply_transparency_as_background; + + const ProjectionVisibility visibility = m_pass_through_model ? + ProjectionVisibility() : + build_projection_visibility(context, m_parent, object, instance_idx); + + bool changed = false; + for (ModelVolume *volume : object->volumes) { + if (volume == nullptr || !volume->is_model_part()) + continue; + + const indexed_triangle_set &its = volume->mesh().its; + if (its.vertices.empty() || its.indices.empty()) + continue; + + const VolumeColorSource source = build_volume_color_source(*volume); + const ColorRGBA fallback_color = projection_base_color_for_volume(*volume); + const Transform3d world_matrix = projection_world_matrix_for_volume(m_parent, object, volume, instance_idx); + std::vector projected_triangles(its.indices.size(), false); + + for (size_t tri_idx = 0; tri_idx < its.indices.size(); ++tri_idx) { + const stl_triangle_vertex_indices &tri = its.indices[tri_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 std::array vertices = { + its.vertices[size_t(tri[0])].cast(), + its.vertices[size_t(tri[1])].cast(), + its.vertices[size_t(tri[2])].cast() + }; + projected_triangles[tri_idx] = projection_triangle_intersects_overlay(context, world_matrix, vertices); + } + + TextureMappingColorSampler sampler = [this, volume, source, context, world_matrix, fallback_color, &projected_triangles, &visibility](size_t tri_idx, + const Vec3f &point, + const Vec3f &barycentric) { + ColorRGBA color = sample_volume_color_source(*volume, source, tri_idx, point, barycentric, true, &fallback_color); + if (tri_idx < projected_triangles.size() && projected_triangles[tri_idx]) { + if (m_pass_through_model || projection_point_is_visible(visibility, context, world_matrix, point)) { + if (std::optional projected = projected_image_color_at_point(context, world_matrix, point)) { + if (!context.apply_transparency_as_background && !projection_overlay_has_paintable_alpha(*projected, context)) + return pack_vertex_color_rgba(color); + color = apply_projection_color(color, *projected, context, false); + } + } + } + return pack_vertex_color_rgba(color); + }; + + const float mesh_span = mesh_max_axis_span(its); + const int safe_max_depth = texture_mapping_depth_for_budget(its.indices.size(), 7, 2200000); + const float split_threshold = safe_max_depth < 5 ? 0.018f : 0.012f; + TextureMappingColorSubdivisionDepths subdivision_depths = + [volume, mesh_span, safe_max_depth, &projected_triangles](size_t tri_idx, const std::array &vertices) { + int base_depth = model_volume_has_bakeable_image_texture_data(volume) ? + texture_mapping_depth_from_span(texture_triangle_uv_pixel_span(volume, tri_idx), 8.f, safe_max_depth) : + texture_mapping_depth_from_span(triangle_max_edge_length(vertices), std::max(mesh_span / 180.f, 0.18f), std::min(6, safe_max_depth)); + if (tri_idx < projected_triangles.size() && projected_triangles[tri_idx]) + base_depth = std::min(std::max(base_depth, 4), safe_max_depth); + return std::make_pair(base_depth, safe_max_depth); + }; + + volume->texture_mapping_color_facets.set_from_triangle_sampler(*volume, sampler, safe_max_depth, split_threshold, subdivision_depths); + if (volume->texture_mapping_color_facets.metadata_json().empty()) + volume->texture_mapping_color_facets.set_metadata_json(rgb_metadata_json(ColorRGBA(1.f, 1.f, 1.f, 1.f))); + changed = true; + } + return changed; +} + +void GLGizmoImageProjection::refresh_projected_object(ModelObject *object) +{ + m_parent.update_volumes_colors_by_extruder(); + m_parent.set_as_dirty(); + + const ModelObjectPtrs &objects = wxGetApp().model().objects; + const size_t object_idx = size_t(std::find(objects.begin(), objects.end(), object) - objects.begin()); + if (object_idx < objects.size()) { + wxGetApp().obj_list()->update_info_items(object_idx); + wxGetApp().plater()->get_partplate_list().notify_instance_update(object_idx, 0); + } + m_parent.post_event(SimpleEvent(EVT_GLCANVAS_SCHEDULE_BACKGROUND_PROCESS)); +} + void GLMmSegmentationGizmo3DScene::release_geometry() { if (this->vertices_VBO_id) { glsafe(::glDeleteBuffers(1, &this->vertices_VBO_id)); @@ -1509,16 +4890,6 @@ void GLMmSegmentationGizmo3DScene::release_geometry() { glsafe(::glDeleteBuffers(1, &triangle_indices_VBO_id)); triangle_indices_VBO_id = 0; } -#if !SLIC3R_OPENGL_ES - if (OpenGLManager::get_gl_info().is_core_profile()) { -#endif // !SLIC3R_OPENGL_ES - if (this->vertices_VAO_id > 0) { - glsafe(::glDeleteVertexArrays(1, &this->vertices_VAO_id)); - this->vertices_VAO_id = 0; - } -#if !SLIC3R_OPENGL_ES - } -#endif // !SLIC3R_OPENGL_ES this->clear(); } @@ -1527,13 +4898,6 @@ void GLMmSegmentationGizmo3DScene::render(size_t triangle_indices_idx) const { assert(triangle_indices_idx < this->triangle_indices_VBO_ids.size()); assert(this->triangle_patches.size() == this->triangle_indices_VBO_ids.size()); -#if !SLIC3R_OPENGL_ES - if (OpenGLManager::get_gl_info().is_core_profile()) { -#endif // !SLIC3R_OPENGL_ES - assert(this->vertices_VAO_id != 0); -#if !SLIC3R_OPENGL_ES - } -#endif // !SLIC3R_OPENGL_ES assert(this->vertices_VBO_id != 0); assert(this->triangle_indices_VBO_ids[triangle_indices_idx] != 0); @@ -1541,13 +4905,6 @@ void GLMmSegmentationGizmo3DScene::render(size_t triangle_indices_idx) const if (shader == nullptr) return; -#if !SLIC3R_OPENGL_ES - if (OpenGLManager::get_gl_info().is_core_profile()) { -#endif // !SLIC3R_OPENGL_ES - glsafe(::glBindVertexArray(this->vertices_VAO_id)); -#if !SLIC3R_OPENGL_ES - } -#endif // !SLIC3R_OPENGL_ES // the following binding is needed to set the vertex attributes glsafe(::glBindBuffer(GL_ARRAY_BUFFER, this->vertices_VBO_id)); const GLint position_id = shader->get_attrib_location("v_position"); @@ -1568,48 +4925,17 @@ void GLMmSegmentationGizmo3DScene::render(size_t triangle_indices_idx) const glsafe(::glDisableVertexAttribArray(position_id)); glsafe(::glBindBuffer(GL_ARRAY_BUFFER, 0)); -#if !SLIC3R_OPENGL_ES - if (OpenGLManager::get_gl_info().is_core_profile()) { -#endif // !SLIC3R_OPENGL_ES - glsafe(::glBindVertexArray(0)); -#if !SLIC3R_OPENGL_ES - } -#endif // !SLIC3R_OPENGL_ES } void GLMmSegmentationGizmo3DScene::finalize_vertices() { -#if !SLIC3R_OPENGL_ES - if (OpenGLManager::get_gl_info().is_core_profile()) { -#endif // !SLIC3R_OPENGL_ES - assert(this->vertices_VAO_id == 0); -#if !SLIC3R_OPENGL_ES - } -#endif // !SLIC3R_OPENGL_ES assert(this->vertices_VBO_id == 0); if (!this->vertices.empty()) { -#if !SLIC3R_OPENGL_ES - if (OpenGLManager::get_gl_info().is_core_profile()) { -#endif // !SLIC3R_OPENGL_ES - glsafe(::glGenVertexArrays(1, &this->vertices_VAO_id)); - glsafe(::glBindVertexArray(this->vertices_VAO_id)); -#if !SLIC3R_OPENGL_ES - } -#endif // !SLIC3R_OPENGL_ES - glsafe(::glGenBuffers(1, &this->vertices_VBO_id)); glsafe(::glBindBuffer(GL_ARRAY_BUFFER, this->vertices_VBO_id)); glsafe(::glBufferData(GL_ARRAY_BUFFER, this->vertices.size() * sizeof(float), this->vertices.data(), GL_STATIC_DRAW)); glsafe(::glBindBuffer(GL_ARRAY_BUFFER, 0)); this->vertices.clear(); - -#if !SLIC3R_OPENGL_ES - if (OpenGLManager::get_gl_info().is_core_profile()) { -#endif // !SLIC3R_OPENGL_ES - glsafe(::glBindVertexArray(0)); -#if !SLIC3R_OPENGL_ES - } -#endif // !SLIC3R_OPENGL_ES } } @@ -1632,70 +4958,33 @@ void GLMmSegmentationGizmo3DScene::finalize_triangle_indices() } } -// ORCA: Update the cache of used filaments (both base volume extruders and painted triangles) -void GLGizmoMmuSegmentation::update_used_filaments() -{ - m_used_filaments.clear(); - - // Add base extruder IDs from volumes (unpainted areas) - for (int ext_id : m_volumes_extruder_idxs) { - // ext_id is 1-based (1 = Extruder 1), 0 = Default (usually maps to first available or object default) - // Here we assume 0 maps to index 0 (Extruder 1) for simplicity in display, - // or we should check logic in init_model_triangle_selectors where it does: - // int extruder_idx = (mv->extruder_id() > 0) ? mv->extruder_id() - 1 : 0; - int idx = (ext_id > 0) ? ext_id - 1 : 0; - if (idx >= 0 && idx < m_extruders_colors.size()) - m_used_filaments.insert((size_t)idx); - } - - // Add painted states - for (const auto& selector : m_triangle_selectors) { - if (!selector) continue; - TriangleSelector::TriangleSplittingData data = selector->serialize(); - std::vector states = TriangleSelector::extract_used_facet_states(data); - for (EnforcerBlockerType s : states) { - int idx = (int)s - (int)EnforcerBlockerType::Extruder1; - if (idx >= 0 && idx < m_extruders_colors.size()) - m_used_filaments.insert((size_t)idx); - } - } -} - void GLGizmoMmuSegmentation::render_filament_remap_ui(float window_width, float max_tooltip_width) { - size_t n_extr = std::min((size_t)EnforcerBlockerType::ExtruderMax, m_extruders_colors.size()); + size_t n_extr = std::min((size_t)EnforcerBlockerType::ExtruderMax, m_display_filament_ids.size()); - const ImVec2 max_label_size = ImGui::CalcTextSize("99", NULL, true); + const std::string max_label = std::to_string(std::max(n_extr, 1)); + const ImVec2 max_label_size = ImGui::CalcTextSize(max_label.c_str(), NULL, true); const ImVec2 button_size(max_label_size.x + m_imgui->scaled(0.5f), 0.f); + const int max_items_per_line = 8; + const float item_width = button_size.x + m_imgui->scaled(1.5f); + const float start_pos_x = ImGui::GetCursorPosX(); - int displayed_count = 0; - const int max_per_line = 8; + for (int src = 0; src < (int)n_extr; ++src) { + const unsigned int dst_filament_id = m_extruder_remap[src] < m_display_filament_ids.size() ? m_display_filament_ids[m_extruder_remap[src]] : 0; + if (dst_filament_id == 0 || dst_filament_id > m_extruders_colors.size()) + continue; + const ColorRGBA &dst_col = m_extruders_colors[dst_filament_id - 1]; + ImVec4 col_vec = ImGuiWrapper::to_ImVec4(dst_col); - // ORCA: Use m_used_filaments to show only relevant source filaments - for (size_t src : m_used_filaments) { - if (src >= n_extr) continue; - - const ColorRGBA &src_col = m_extruders_colors[src]; // keep for text contrast - const ColorRGBA &dst_col = m_extruders_colors[m_extruder_remap[src]]; - - // ORCA: Button now shows the SOURCE color (per maintainer request) - // This keeps the UI stable until "Remap" is clicked. - ImVec4 col_vec = ImGuiWrapper::to_ImVec4(src_col); - - if (displayed_count > 0 && (displayed_count % max_per_line != 0)) - ImGui::SameLine(); - + if (src % max_items_per_line != 0) { + ImGui::SameLine(start_pos_x + item_width * (src % max_items_per_line)); + } std::string btn_id = "##remap_src_" + std::to_string(src); - std::string pop_id = "popup_" + std::to_string(src); ImGuiColorEditFlags flags = ImGuiColorEditFlags_NoAlpha | ImGuiColorEditFlags_NoInputs | ImGuiColorEditFlags_NoLabel | ImGuiColorEditFlags_NoPicker | ImGuiColorEditFlags_NoTooltip; - - // ORCA: Show border ONLY if the popup is open (visual feedback for active selection) - // Decoupled from m_selected_extruder_idx to prevent unwanted selection highlights. - if (!ImGui::IsPopupOpen(pop_id.c_str())) - flags |= ImGuiColorEditFlags_NoBorder; + if (m_selected_extruder_idx != src) flags |= ImGuiColorEditFlags_NoBorder; #ifdef __APPLE__ ImGui::PushStyleColor(ImGuiCol_FrameBg, ImGuiWrapper::COL_ORCA); @@ -1714,9 +5003,8 @@ void GLGizmoMmuSegmentation::render_filament_remap_ui(float window_width, float #endif // overlay destination number with proper contrast calculation - // ORCA: Text still shows DESTINATION index, but contrast is against SOURCE color now. std::string dst_txt = std::to_string(m_extruder_remap[src] + 1); - float gray = 0.299f * src_col.r() + 0.587f * src_col.g() + 0.114f * src_col.b(); + float gray = 0.299f * dst_col.r() + 0.587f * dst_col.g() + 0.114f * dst_col.b(); ImVec2 txt_sz = ImGui::CalcTextSize(dst_txt.c_str()); ImVec2 pos = ImGui::GetItemRectMin(); ImVec2 size = ImGui::GetItemRectSize(); @@ -1730,35 +5018,8 @@ void GLGizmoMmuSegmentation::render_filament_remap_ui(float window_width, float ImVec2(pos.x + (size.x - txt_sz.x) * 0.5f, pos.y + (size.y - txt_sz.y) * 0.5f), IM_COL32(0,0,0,255), dst_txt.c_str()); - // ORCA: Show NEW color as a small triangle in the corner if remapped - if (src != m_extruder_remap[src]) { - float s = m_imgui->scaled(0.55f); - float offset = m_imgui->scaled(0.15f); // Inset to avoid rounded corner clipping - ImVec2 p = ImVec2(pos.x + offset, pos.y + offset); - - // Contrast outline: White for dark backgrounds, Black for light backgrounds - // Use dst_col (new color) for outline contrast check? Or src_col? - // Usually outline is around the triangle (dst_col). - float dst_gray = 0.299f * dst_col.r() + 0.587f * dst_col.g() + 0.114f * dst_col.b(); - ImU32 outline_col = (dst_gray * 255.f < 80.f) ? IM_COL32(255, 255, 255, 180) : IM_COL32(0, 0, 0, 180); - - ImDrawList* draw_list = ImGui::GetWindowDrawList(); - draw_list->AddTriangleFilled( - p, - ImVec2(p.x + s, p.y), - ImVec2(p.x, p.y + s), - ImGuiWrapper::to_ImU32(dst_col)); - - // ORCA: Add a thin outline for better contrast when colors are similar - draw_list->AddTriangle( - p, - ImVec2(p.x + s, p.y), - ImVec2(p.x, p.y + s), - outline_col, - 0.5f); - } - // popup with possible destinations + std::string pop_id = "popup_" + std::to_string(src); if (clicked) { // Calculate popup position centered below the current button ImVec2 button_pos = ImGui::GetItemRectMin(); @@ -1774,19 +5035,20 @@ void GLGizmoMmuSegmentation::render_filament_remap_ui(float window_width, float // Apply popup styling before BeginPopup using standard Orca colors ImGui::PushStyleVar(ImGuiStyleVar_PopupRounding, 4.0f); ImGui::PushStyleVar(ImGuiStyleVar_PopupBorderSize, 1.0f); - // ORCA: Use FrameBgActive for consistency and to ensure visibility of white filaments - ImGui::PushStyleColor(ImGuiCol_PopupBg, ImGui::GetStyleColorVec4(ImGuiCol_FrameBgActive)); + ImGui::PushStyleColor(ImGuiCol_PopupBg, m_is_dark_mode ? ImGuiWrapper::COL_WINDOW_BG_DARK : ImGuiWrapper::COL_WINDOW_BG); ImGui::PushStyleColor(ImGuiCol_Border, m_is_dark_mode ? ImVec4(0.5f, 0.5f, 0.5f, 1.0f) : ImVec4(0.6f, 0.6f, 0.6f, 1.0f)); if (ImGui::BeginPopup(pop_id.c_str())) { + const float popup_start_pos_x = ImGui::GetCursorPosX(); - m_imgui->text(_L("To:")); - for (int dst = 0; dst < (int)n_extr; ++dst) { - const ColorRGBA &dst_col_popup = m_extruders_colors[dst]; + const unsigned int popup_filament_id = m_display_filament_ids[dst]; + if (popup_filament_id == 0 || popup_filament_id > m_extruders_colors.size()) + continue; + const ColorRGBA &dst_col_popup = m_extruders_colors[popup_filament_id - 1]; ImVec4 dst_vec = ImGuiWrapper::to_ImVec4(dst_col_popup); - if (dst > 0 && (dst % max_per_line != 0)) - ImGui::SameLine(); + if (dst % max_items_per_line != 0) + ImGui::SameLine(popup_start_pos_x + item_width * (dst % max_items_per_line)); std::string dst_btn = "##dst_" + std::to_string(src) + "_" + std::to_string(dst); // Apply same styling to destination buttons @@ -1841,9 +5103,18 @@ void GLGizmoMmuSegmentation::render_filament_remap_ui(float window_width, float // Clean up popup styling (always pop, whether popup was open or not) ImGui::PopStyleColor(2); // PopupBg and Border ImGui::PopStyleVar(2); // PopupRounding and PopupBorderSize - - displayed_count++; } + + ImGui::Dummy(ImVec2(0.0f, ImGui::GetFontSize() * 0.3f)); + + if (m_imgui->button(m_desc.at("remap"))) { + remap_filament_assignments(); + m_show_filament_remap_ui = false; + } + + ImGui::SameLine(); + if (m_imgui->button(m_desc.at("cancel_remap"))) + m_show_filament_remap_ui = false; } void GLGizmoMmuSegmentation::remap_filament_assignments() @@ -1858,15 +5129,23 @@ void GLGizmoMmuSegmentation::remap_filament_assignments() for (size_t i = 0; i <= MAX_EBT; ++i) state_map[i] = static_cast(i); - size_t n_extr = std::min(m_extruder_remap.size(), MAX_EBT); - const int start_extruder = (int) EnforcerBlockerType::Extruder1; + size_t n_extr = std::min({m_extruder_remap.size(), m_display_filament_ids.size(), MAX_EBT}); bool any_change = false; for (size_t src = 0; src < n_extr; ++src) { - size_t dst = m_extruder_remap[src]; - if (dst != src) { - state_map[src+start_extruder] = static_cast(dst+start_extruder); - any_change = true; - } + const size_t dst = m_extruder_remap[src]; + if (dst >= m_display_filament_ids.size()) + continue; + + const unsigned int src_state = m_display_filament_ids[src]; + const unsigned int dst_state = m_display_filament_ids[dst]; + if (src_state == 0 || dst_state == 0 || src_state == dst_state) + continue; + + state_map[src_state] = static_cast(dst_state); + if (src_state == 1) + state_map[0] = static_cast(dst_state); + + any_change = true; } if (!any_change) return; @@ -1880,57 +5159,21 @@ void GLGizmoMmuSegmentation::remap_filament_assignments() ModelObject* mo = m_c->selection_info()->model_object(); if (!mo) return; - bool volume_extruder_changed = false; - for (ModelVolume* mv : mo->volumes) { if (!mv->is_model_part()) continue; ++idx; TriangleSelectorGUI* ts = m_triangle_selectors[idx].get(); if (!ts) continue; - - // Remap painted triangles ts->remap_triangle_state(state_map); ts->request_update_render_data(true); - - // ORCA: Remap base volume extruder as well if selected - int current_ext_id = mv->extruder_id(); - int current_idx = (current_ext_id > 0) ? current_ext_id - 1 : 0; - - if (current_idx >= 0 && current_idx < m_extruder_remap.size()) { - size_t dest_idx = m_extruder_remap[current_idx]; - if (dest_idx != current_idx) { - // Check if volume has its own extruder config or uses object's fallback - const ConfigOption *vol_opt = mv->config.option("extruder"); - if (vol_opt != nullptr && vol_opt->getInt() != 0) { - // Volume has its own extruder setting, update it - mv->config.set("extruder", (int)dest_idx + 1); - } else { - // Volume uses object's extruder setting, update the object - mo->config.set("extruder", (int)dest_idx + 1); - } - if (idx < m_volumes_extruder_idxs.size()) - m_volumes_extruder_idxs[idx] = (int)dest_idx + 1; - volume_extruder_changed = true; - } - } - updated = true; } if (updated) { - // ORCA: Update renderer colors if base volume extruder changed - if (volume_extruder_changed) { - this->update_triangle_selectors_colors(); - // ORCA: Update GUI_ObjectList extruder column to reflect the new extruder value - wxGetApp().obj_list()->update_objects_list_filament_column(wxGetApp().filaments_cnt()); - } - - // ORCA: Removed "Filament remapping finished" notification to reduce UI noise. + wxGetApp().plater()->get_notification_manager()->push_notification( + _L("Filament remapping finished.").ToStdString()); update_model_object(); m_parent.set_as_dirty(); - - // ORCA: Refresh used filaments cache - this->update_used_filaments(); } } diff --git a/src/slic3r/GUI/Gizmos/GLGizmoMmuSegmentation.hpp b/src/slic3r/GUI/Gizmos/GLGizmoMmuSegmentation.hpp index 209340a9cd..4c61b968fb 100644 --- a/src/slic3r/GUI/Gizmos/GLGizmoMmuSegmentation.hpp +++ b/src/slic3r/GUI/Gizmos/GLGizmoMmuSegmentation.hpp @@ -3,6 +3,12 @@ #include "GLGizmoPainterBase.hpp" +#include +#include +#include +#include +#include + namespace Slic3r::GUI { class GLMmSegmentationGizmo3DScene @@ -100,13 +106,14 @@ protected: wxString handle_snapshot_action_name(bool shift_down, Button button_down) const override; - std::string get_gizmo_entering_text() const override { return "Entering color painting"; } - std::string get_gizmo_leaving_text() const override { return "Leaving color painting"; } - std::string get_action_snapshot_name() const override { return "Color painting editing"; } + std::string get_gizmo_entering_text() const override { return "Entering color region painting"; } + std::string get_gizmo_leaving_text() const override { return "Leaving color region painting"; } + std::string get_action_snapshot_name() const override { return "Color region painting editing"; } // BBS size_t m_selected_extruder_idx = 0; std::vector m_extruders_colors; + std::vector m_display_filament_ids; std::vector m_volumes_extruder_idxs; // BBS @@ -114,10 +121,8 @@ protected: bool m_detect_geometry_edge = true; // Filament remap feature - bool m_show_remap_panel = false; std::vector m_extruder_remap; // index → target extruder index - // ORCA: Cache used filaments to filter UI - std::set m_used_filaments; // Set of used filament indices (cached) + bool m_show_filament_remap_ui = false; static const constexpr float CursorRadiusMin = 0.1f; // cannot be zero @@ -139,6 +144,7 @@ private: // BBS void update_triangle_selectors_colors(); void init_extruders_data(); + void init_extruders_data(const std::vector &extruder_colors); // Filament remapping methods void remap_filament_assignments(); @@ -146,13 +152,15 @@ private: bool selected_object_has_imported_vertex_colors() const; bool selected_object_has_imported_texture_data() const; bool selected_object_has_bakeable_image_texture_data() const; + bool selected_object_has_texture_mapping_color_data() const; bool selected_object_has_painted_regions() const; void open_obj_vertex_color_mapping_dialog(); void bake_selected_object_image_texture_to_vertex_colors(); + void convert_selected_object_vertex_colors_to_texture_mapping_colors(); + void convert_selected_object_image_texture_to_texture_mapping_colors(); void convert_selected_regions_to_vertex_colors(); void clear_selected_object_image_texture_data(); - // ORCA: Helper to update the cache of used filaments - void update_used_filaments(); + void clear_selected_object_texture_mapping_color_data(); // This map holds all translated description texts, so they can be easily referenced during layout calculations // etc. When language changes, GUI is recreated and this class constructed again, so the change takes effect. @@ -166,6 +174,187 @@ private: std::vector> m_shortcuts_gap_fill; }; +class GLGizmoTrueColorPainting : public GLGizmoPainterBase +{ +public: + GLGizmoTrueColorPainting(GLCanvas3D& parent, const std::string& icon_filename, unsigned int sprite_id); + ~GLGizmoTrueColorPainting() override = default; + + void render_painter_gizmo() override; + bool gizmo_event(SLAGizmoEventType action, const Vec2d& mouse_position, bool shift_down, bool alt_down, bool control_down) override; + + const float get_cursor_radius_min() const override { return CursorRadiusMin; } + +protected: + ColorRGBA get_cursor_hover_color() const override; + ColorRGBA get_cursor_sphere_left_button_color() const override; + EnforcerBlockerType get_left_button_state_type() const override { return EnforcerBlockerType::ENFORCER; } + EnforcerBlockerType get_right_button_state_type() const override { return EnforcerBlockerType(-1); } + + void on_render_input_window(float x, float y, float bottom_limit) override; + std::string on_get_name() const override; + bool on_is_selectable() const override; + bool on_is_activable() const override; + + wxString handle_snapshot_action_name(bool shift_down, Button button_down) const override; + + std::string get_gizmo_entering_text() const override { return "Entering true color painting"; } + std::string get_gizmo_leaving_text() const override { return "Leaving true color painting"; } + std::string get_action_snapshot_name() const override { return "True color painting editing"; } + +private: + enum class ColorInputMode : int + { + FilamentColors, + RGB, + CMY, + CMYK, + CMYW, + RGBK, + RGBW, + BW + }; + + bool on_init() override; + void update_model_object() override; + void update_from_model_object(bool first_update = false) override; + void on_opening() override; + void on_shutdown() override; + PainterGizmoType get_painter_type() const override; + + void init_model_triangle_selectors(); + ModelObject *selected_model_object() const; + void update_selected_object_color_state(); + bool selected_object_has_rgb_data() const; + bool selected_object_has_imported_color_data() const; + void initialize_selected_object_rgb_data(); + void convert_selected_object_vertex_colors_to_rgb_data(); + void convert_selected_object_image_texture_to_rgb_data(); + void refresh_selected_object_after_rgb_change(ModelObject *object); + void update_triangle_selectors_color(); + bool pick_color_from_model(const Vec2d &mouse_position); + bool sample_color_from_model(const Vec2d &mouse_position, ColorRGBA &color) const; + void set_active_color_from_sample(const ColorRGBA &color); + void sync_cmy_from_rgb(); + void sync_rgb_from_cmy(); + void sync_cmyk_from_rgb(); + void sync_rgb_from_cmyk(); + void sync_cmyw_from_rgb(); + void sync_rgb_from_cmyw(); + void sync_rgbk_from_rgb(); + void sync_rgb_from_rgbk(); + void sync_rgbw_from_rgb(); + void sync_rgb_from_rgbw(); + void sync_bw_from_rgb(); + void sync_rgb_from_bw(); + void ensure_filament_mix_colors(); + void sync_filament_mix_from_rgb(); + void sync_rgb_from_filament_mix(); + void sync_active_color_mode_from_rgb(bool update_filament_mix); + bool render_rgb_picker(float item_width); + bool render_cmy_picker(float item_width); + bool render_cmyk_picker(float item_width); + bool render_cmyw_picker(float item_width); + bool render_rgbk_picker(float item_width); + bool render_rgbw_picker(float item_width); + bool render_bw_picker(float item_width); + bool render_filament_colors_picker(float item_width); + + std::array m_rgb_color { 0.f, 0.f, 0.f, 1.f }; + std::array m_cmy_color { 1.f, 1.f, 1.f }; + std::array m_cmyk_color { 0.f, 0.f, 0.f, 1.f }; + std::array m_cmyw_color { 0.f, 0.f, 0.f, 1.f }; + std::array m_rgbk_color { 0.f, 0.f, 0.f, 1.f }; + std::array m_rgbw_color { 0.f, 0.f, 0.f, 1.f }; + std::array m_bw_color { 1.f, 0.f }; + std::vector m_filament_mix_colors; + std::vector m_filament_mix; + float m_brush_hardness = 1.f; + float m_opacity = 1.f; + ColorInputMode m_color_input_mode = ColorInputMode::RGB; + bool m_selected_has_rgb_data = false; + bool m_selected_has_imported_color_data = false; + bool m_selected_can_convert_vertex = false; + bool m_selected_can_convert_image = false; + bool m_color_picker_active = false; + ObjectID m_selected_color_state_object_id; + struct ColorPickerVolumeSourceCache + { + ObjectID volume_id; + ObjectBase::Timestamp timestamp = 0; + std::vector rgb_facets; + std::unordered_map> rgb_by_source_triangle; + }; + const ColorPickerVolumeSourceCache &cached_volume_color_source(const ModelVolume &volume) const; + mutable std::vector m_color_picker_source_cache; + + static const constexpr float CursorRadiusMin = 0.1f; +}; + +class GLGizmoImageProjection : public GLGizmoBase +{ +public: + GLGizmoImageProjection(GLCanvas3D& parent, const std::string& icon_filename, unsigned int sprite_id); + ~GLGizmoImageProjection() override = default; + +protected: + bool on_init() override; + void on_render() override; + void on_render_input_window(float x, float y, float bottom_limit) override; + std::string on_get_name() const override; + void on_set_state() override; + bool on_is_selectable() const override; + bool on_is_activable() const override; + CommonGizmosDataID on_get_requirements() const override; + +private: + enum class ProjectionMode : int + { + VertexColors, + ImageTexture, + RGBData + }; + + struct OverlayRect + { + float left = 0.f; + float top = 0.f; + float width = 0.f; + float height = 0.f; + }; + + bool load_projection_image(); + void clear_projection_image(); + bool ensure_overlay_texture(); + OverlayRect overlay_rect() const; + ModelObject *selected_model_object() const; + void update_default_projection_mode(); + ProjectionMode default_projection_mode() const; + bool projection_mode_allowed(ProjectionMode mode) const; + bool selected_object_has_image_texture_data() const; + bool selected_object_has_vertex_color_data() const; + bool selected_object_has_rgb_data() const; + bool project_image_to_selected_object(); + bool project_to_vertex_colors(ModelObject *object); + bool project_to_image_texture(ModelObject *object); + bool project_to_rgb_data(ModelObject *object); + void refresh_projected_object(ModelObject *object); + + ProjectionMode m_projection_mode = ProjectionMode::RGBData; + bool m_projection_mode_initialized = false; + ObjectID m_projection_mode_object_id; + std::string m_image_path; + std::string m_image_error; + std::vector m_image_rgba; + uint32_t m_image_width = 0; + uint32_t m_image_height = 0; + GLTexture m_overlay_texture; + bool m_overlay_texture_dirty = false; + float m_projection_opacity = 1.f; + bool m_apply_transparency_as_background = false; + bool m_pass_through_model = false; +}; + } // namespace Slic3r diff --git a/src/slic3r/GUI/Gizmos/GLGizmoPainterBase.cpp b/src/slic3r/GUI/Gizmos/GLGizmoPainterBase.cpp index 51be765d95..08539c8b95 100644 --- a/src/slic3r/GUI/Gizmos/GLGizmoPainterBase.cpp +++ b/src/slic3r/GUI/Gizmos/GLGizmoPainterBase.cpp @@ -23,6 +23,9 @@ namespace { bool model_volume_has_texture_preview_data_for_painting(const ModelVolume &model_volume) { + if (!model_volume.texture_mapping_color_facets.empty()) + return false; + return !model_volume.imported_texture_rgba.empty() && model_volume.imported_texture_width > 0 && model_volume.imported_texture_height > 0 && @@ -101,6 +104,14 @@ void GLGizmoPainterBase::render_triangles(const Selection& selection) const shader->set_uniform("clipping_plane", clp_data.clp_dataf); shader->set_uniform("z_range", clp_data.z_range); + const GLboolean cull_face_enabled = glIsEnabled(GL_CULL_FACE); + ScopeGuard cull_face_guard([cull_face_enabled]() { + if (cull_face_enabled) + glsafe(::glEnable(GL_CULL_FACE)); + else + glsafe(::glDisable(GL_CULL_FACE)); + }); + // BBS: to improve the random white pixel issue glsafe(::glDisable(GL_CULL_FACE)); @@ -1069,6 +1080,57 @@ bool GLGizmoPainterBase::on_mouse(const wxMouseEvent &mouse_event) return false; } +bool GLGizmoPainterBase::raycast_to_selected_mesh(const Vec2d &mouse_position, int &mesh_id, Vec3f &hit, size_t &facet) const +{ + mesh_id = -1; + hit = Vec3f::Zero(); + facet = 0; + + if (m_c == nullptr || m_c->selection_info() == nullptr) + return false; + + const ModelObject *mo = m_c->selection_info()->model_object(); + if (mo == nullptr) + return false; + + const Selection &selection = m_parent.get_selection(); + const int instance_idx = selection.get_instance_idx(); + if (instance_idx < 0 || size_t(instance_idx) >= mo->instances.size() || mo->instances[size_t(instance_idx)] == nullptr) + return false; + + const ModelInstance *mi = mo->instances[size_t(instance_idx)]; + const Transform3d instance_trafo = m_parent.get_canvas_type() == GLCanvas3D::CanvasAssembleView ? + mi->get_assemble_transformation().get_matrix() : + mi->get_transformation().get_matrix(); + + std::vector trafo_matrices; + for (const ModelVolume *mv : mo->volumes) { + if (mv == nullptr || !mv->is_model_part()) + continue; + + if (m_parent.get_canvas_type() == GLCanvas3D::CanvasAssembleView) { + Transform3d temp = instance_trafo * mv->get_matrix(); + temp.translate(mv->get_transformation().get_offset() * (GLVolume::explosion_ratio - 1.0) + mi->get_offset_to_assembly() * (GLVolume::explosion_ratio - 1.0)); + trafo_matrices.emplace_back(temp); + } else { + trafo_matrices.emplace_back(instance_trafo * mv->get_matrix()); + } + } + + if (trafo_matrices.empty()) + return false; + + m_rr.mouse_position = Vec2d(std::numeric_limits::lowest(), std::numeric_limits::lowest()); + update_raycast_cache(mouse_position, wxGetApp().plater()->get_camera(), trafo_matrices); + if (m_rr.mesh_id == -1) + return false; + + mesh_id = m_rr.mesh_id; + hit = m_rr.hit; + facet = m_rr.facet; + return true; +} + void GLGizmoPainterBase::update_raycast_cache(const Vec2d& mouse_position, const Camera& camera, const std::vector& trafo_matrices) const @@ -1409,6 +1471,8 @@ void TriangleSelectorPatch::update_triangles_per_type() continue; int state = (int)triangle.get_state(); + if (!m_render_none_state && state == int(EnforcerBlockerType::NONE)) + continue; auto& patch = m_triangle_patches[state]; //patch.triangle_indices.insert(patch.triangle_indices.end(), triangle.verts_idxs.begin(), triangle.verts_idxs.end()); for (int i = 0; i < 3; ++i) { @@ -1614,7 +1678,7 @@ void TriangleSelectorPatch::update_render_data() m_vertex_color_preview_models.clear(); m_vertex_color_preview_colors.clear(); m_vertex_color_preview_filament_ids.clear(); - if (m_model_volume != nullptr) { + if (m_texture_preview_needed && m_model_volume != nullptr) { std::vector> triangles_per_type; get_facet_triangles(triangles_per_type); const size_t num_physical = std::max(0, wxGetApp().filaments_cnt()); @@ -1759,7 +1823,7 @@ void TriangleSelectorPatch::render_texture_preview(const Transform3d& m const std::array& z_range, const std::array& clipping_plane) const { - if (m_model_volume == nullptr) + if (!m_texture_preview_needed || m_model_volume == nullptr) return; const size_t num_physical = std::max(0, wxGetApp().filaments_cnt()); @@ -1775,10 +1839,13 @@ void TriangleSelectorPatch::render_texture_preview(const Transform3d& m if (m_texture_preview_models.empty() && m_vertex_color_preview_models.empty()) return; - auto adjusted_preview_colors = [](const std::vector &colors) { + auto adjusted_preview_colors = [this](const std::vector &colors) { std::vector preview_colors = colors; - for (ColorRGBA &preview_color : preview_colors) + for (ColorRGBA &preview_color : preview_colors) { preview_color = adjust_color_for_rendering(preview_color); + if (m_texture_preview_opaque) + preview_color.a(1.f); + } return preview_colors; }; @@ -1795,7 +1862,11 @@ void TriangleSelectorPatch::render_texture_preview(const Transform3d& m view_matrix, projection_matrix, z_range, - clipping_plane); + clipping_plane, + -1, + std::array{ 0.f, 0.f, 0.f, 0.f }, + std::array{ 0.f, 0.f }, + m_texture_preview_opaque); } if (!m_vertex_color_preview_models.empty()) { @@ -1808,7 +1879,11 @@ void TriangleSelectorPatch::render_texture_preview(const Transform3d& m view_matrix, projection_matrix, z_range, - clipping_plane); + clipping_plane, + -1, + std::array{ 0.f, 0.f, 0.f, 0.f }, + std::array{ 0.f, 0.f }, + m_texture_preview_opaque); } } diff --git a/src/slic3r/GUI/Gizmos/GLGizmoPainterBase.hpp b/src/slic3r/GUI/Gizmos/GLGizmoPainterBase.hpp index 9e2945afae..fb98c85d18 100644 --- a/src/slic3r/GUI/Gizmos/GLGizmoPainterBase.hpp +++ b/src/slic3r/GUI/Gizmos/GLGizmoPainterBase.hpp @@ -29,6 +29,7 @@ enum class PainterGizmoType { FDM_SUPPORTS, SEAM, MM_SEGMENTATION, + TRUE_COLOR, FUZZY_SKIN }; @@ -128,6 +129,9 @@ public: void set_ebt_colors(const std::vector ebt_colors) { m_ebt_colors = ebt_colors; } void set_filter_state(bool is_filter_state); + void set_none_state_rendered(bool rendered) { m_render_none_state = rendered; } + void set_texture_preview_needed(bool needed) { m_texture_preview_needed = needed; } + void set_texture_preview_opaque(bool opaque) { m_texture_preview_opaque = opaque; } constexpr static float GapAreaMin = 0.f; constexpr static float GapAreaMax = 5.f; @@ -194,6 +198,9 @@ protected: std::vector m_vertex_color_preview_filament_ids; bool m_filter_state = false; + bool m_render_none_state = true; + bool m_texture_preview_needed = true; + bool m_texture_preview_opaque = false; private: void update_render_data(); @@ -335,6 +342,7 @@ protected: ClippingPlaneDataWrapper get_clipping_plane_data() const; TriangleSelector::ClippingPlane get_clipping_plane_in_volume_coordinates(const Transform3d &trafo) const; + bool raycast_to_selected_mesh(const Vec2d &mouse_position, int &mesh_id, Vec3f &hit, size_t &facet) const; private: std::vector> get_projected_mouse_positions(const Vec2d &mouse_position, double resolution, const std::vector &trafo_matrices) const; diff --git a/src/slic3r/GUI/Gizmos/GLGizmosManager.cpp b/src/slic3r/GUI/Gizmos/GLGizmosManager.cpp index 0ca62d849a..3a9426f345 100644 --- a/src/slic3r/GUI/Gizmos/GLGizmosManager.cpp +++ b/src/slic3r/GUI/Gizmos/GLGizmosManager.cpp @@ -67,7 +67,9 @@ std::vector GLGizmosManager::get_selectable_idxs() const m_gizmos[i]->get_sprite_id() == (unsigned int) Rotate || m_gizmos[i]->get_sprite_id() == (unsigned int) Measure || m_gizmos[i]->get_sprite_id() == (unsigned int) Assembly || - m_gizmos[i]->get_sprite_id() == (unsigned int) MmSegmentation) + m_gizmos[i]->get_sprite_id() == (unsigned int) MmSegmentation || + m_gizmos[i]->get_sprite_id() == (unsigned int) TrueColorPainting || + m_gizmos[i]->get_sprite_id() == (unsigned int) ImageProjection) out.push_back(i); } else { @@ -161,6 +163,12 @@ void GLGizmosManager::switch_gizmos_icon_filename() case(EType::MmSegmentation): gizmo->set_icon_filename(m_is_dark ? "mmu_segmentation_dark.svg" : "mmu_segmentation.svg"); break; + case(EType::TrueColorPainting): + gizmo->set_icon_filename(m_is_dark ? "true_color_painting_dark.svg" : "true_color_painting.svg"); + break; + case(EType::ImageProjection): + gizmo->set_icon_filename(m_is_dark ? "image_projection_dark.svg" : "image_projection.svg"); + break; case(EType::FuzzySkin): gizmo->set_icon_filename(m_is_dark ? "toolbar_fuzzy_skin_paint_dark.svg" : "toolbar_fuzzy_skin_paint.svg"); break; @@ -213,6 +221,12 @@ bool GLGizmosManager::init() m_gizmos.emplace_back(new GLGizmoSeam(m_parent, m_is_dark ? "toolbar_seam_dark.svg" : "toolbar_seam.svg", EType::Seam)); m_gizmos.emplace_back(new GLGizmoFuzzySkin(m_parent, m_is_dark ? "toolbar_fuzzy_skin_paint_dark.svg" : "toolbar_fuzzy_skin_paint.svg", EType::FuzzySkin)); m_gizmos.emplace_back(new GLGizmoMmuSegmentation(m_parent, m_is_dark ? "mmu_segmentation_dark.svg" : "mmu_segmentation.svg", EType::MmSegmentation)); + m_gizmos.emplace_back(new GLGizmoTrueColorPainting(m_parent, + m_is_dark ? "true_color_painting_dark.svg" : "true_color_painting.svg", + EType::TrueColorPainting)); + m_gizmos.emplace_back(new GLGizmoImageProjection(m_parent, + m_is_dark ? "image_projection_dark.svg" : "image_projection.svg", + EType::ImageProjection)); m_gizmos.emplace_back(new GLGizmoEmboss(m_parent, m_is_dark ? "toolbar_text_dark.svg" : "toolbar_text.svg", EType::Emboss)); m_gizmos.emplace_back(new GLGizmoSVG(m_parent)); m_gizmos.emplace_back(new GLGizmoMeasure(m_parent, m_is_dark ? "toolbar_measure_dark.svg" : "toolbar_measure.svg", EType::Measure)); @@ -516,6 +530,9 @@ bool GLGizmosManager::gizmo_event(SLAGizmoEventType action, const Vec2d& mouse_p return dynamic_cast(m_gizmos[Seam].get())->gizmo_event(action, mouse_position, shift_down, alt_down, control_down); else if (m_current == MmSegmentation) return dynamic_cast(m_gizmos[MmSegmentation].get())->gizmo_event(action, mouse_position, shift_down, alt_down, control_down); + else if (m_current == TrueColorPainting) + return dynamic_cast(m_gizmos[TrueColorPainting].get()) + ->gizmo_event(action, mouse_position, shift_down, alt_down, control_down); else if (m_current == Measure) return dynamic_cast(m_gizmos[Measure].get())->gizmo_event(action, mouse_position, shift_down, alt_down, control_down); else if (m_current == Assembly) @@ -536,6 +553,7 @@ bool GLGizmosManager::is_paint_gizmo() { return m_current == EType::FdmSupports || m_current == EType::MmSegmentation || + m_current == EType::TrueColorPainting || m_current == EType::FuzzySkin || m_current == EType::Seam; } @@ -635,7 +653,8 @@ bool GLGizmosManager::on_mouse_wheel(const wxMouseEvent &evt) { bool processed = false; - if (/*m_current == SlaSupports || m_current == Hollow ||*/ m_current == FdmSupports || m_current == Seam || m_current == MmSegmentation || m_current == FuzzySkin || m_current == BrimEars) { + if (/*m_current == SlaSupports || m_current == Hollow ||*/ m_current == FdmSupports || m_current == Seam || + m_current == MmSegmentation || m_current == TrueColorPainting || m_current == FuzzySkin || m_current == BrimEars) { float rot = (float)evt.GetWheelRotation() / (float)evt.GetWheelDelta(); if (gizmo_event((rot > 0.f ? SLAGizmoEventType::MouseWheelUp : SLAGizmoEventType::MouseWheelDown), Vec2d::Zero(), evt.ShiftDown(), evt.AltDown() // BBS @@ -1468,7 +1487,11 @@ std::string get_name_from_gizmo_etype(GLGizmosManager::EType type) case GLGizmosManager::EType::Emboss: return "Text"; case GLGizmosManager::EType::MmSegmentation: - return "Color Painting"; + return "Color Region Painting"; + case GLGizmosManager::EType::TrueColorPainting: + return "True Color Painting"; + case GLGizmosManager::EType::ImageProjection: + return "Project image to model surface"; case GLGizmosManager::EType::FuzzySkin: return "Fuzzy Skin Painting"; default: diff --git a/src/slic3r/GUI/Gizmos/GLGizmosManager.hpp b/src/slic3r/GUI/Gizmos/GLGizmosManager.hpp index 5897c2a512..bc6393e477 100644 --- a/src/slic3r/GUI/Gizmos/GLGizmosManager.hpp +++ b/src/slic3r/GUI/Gizmos/GLGizmosManager.hpp @@ -84,6 +84,8 @@ public: Seam, FuzzySkin, MmSegmentation, + TrueColorPainting, + ImageProjection, Emboss, Svg, Measure, diff --git a/src/slic3r/GUI/KBShortcutsDialog.cpp b/src/slic3r/GUI/KBShortcutsDialog.cpp index 7d346571bb..ae95dc09df 100644 --- a/src/slic3r/GUI/KBShortcutsDialog.cpp +++ b/src/slic3r/GUI/KBShortcutsDialog.cpp @@ -267,8 +267,8 @@ void KBShortcutsDialog::fill_shortcuts() Shortcuts gizmos_shortcuts = { {L("Esc"), L("Deselect all")}, {shift, L("Move: press to snap by 1mm")}, - {ctrl + L("Mouse wheel"), L("Support/Color Painting: adjust pen radius")}, - {alt + L("Mouse wheel"), L("Support/Color Painting: adjust section position")}, + {ctrl + L("Mouse wheel"), L("Support/Color Region Painting: adjust pen radius")}, + {alt + L("Mouse wheel"), L("Support/Color Region Painting: adjust section position")}, }; m_full_shortcuts.push_back({{_L("Gizmo"), ""}, gizmos_shortcuts}); diff --git a/src/slic3r/GUI/MMUPaintedTexturePreview.cpp b/src/slic3r/GUI/MMUPaintedTexturePreview.cpp index 75ff72a6d1..ddacaf3506 100644 --- a/src/slic3r/GUI/MMUPaintedTexturePreview.cpp +++ b/src/slic3r/GUI/MMUPaintedTexturePreview.cpp @@ -115,6 +115,11 @@ bool model_volume_has_vertex_color_preview_data(const ModelVolume &model_volume) model_volume.imported_vertex_colors_rgba.size() == model_volume.mesh().its.vertices.size(); } +bool model_volume_has_texture_mapping_color_preview_data(const ModelVolume &model_volume) +{ + return !model_volume.texture_mapping_color_facets.empty(); +} + std::array unwrap_triangle_uvs(const Vec2f &uv0, const Vec2f &uv1, const Vec2f &uv2) { std::array out{ uv0, uv1, uv2 }; @@ -1352,6 +1357,151 @@ bool build_vertex_color_preview_model_for_state(const ModelVolume return true; } +std::optional sample_texture_mapping_color_preview( + const std::vector &color_facets, + const std::unordered_map> &facets_by_source_triangle, + int source_triangle, + const Vec3f &point) +{ + auto found = facets_by_source_triangle.find(source_triangle); + if (found == facets_by_source_triangle.end() || found->second.empty()) + return std::nullopt; + + const float tolerance = -1e-4f; + for (const size_t facet_idx : found->second) { + if (facet_idx >= color_facets.size()) + continue; + + const ColorFacetTriangle &facet = color_facets[facet_idx]; + Vec3f weights = Vec3f::Zero(); + if (!barycentric_weights(point, facet.vertices[0], facet.vertices[1], facet.vertices[2], weights)) + continue; + if (weights.x() >= tolerance && weights.y() >= tolerance && weights.z() >= tolerance) + return unpack_vertex_color(facet.rgba); + } + + return unpack_vertex_color(color_facets[found->second.front()].rgba); +} + +bool build_texture_mapping_color_preview_model_for_state( + const ModelVolume &model_volume, + const std::vector &state_triangles, + const TexturePreviewSimulationSettings *simulation_settings, + GUI::GLModel &out_model) +{ + if (!model_volume_has_texture_mapping_color_preview_data(model_volume) || state_triangles.empty()) + return false; + + std::vector color_facets; + model_volume.texture_mapping_color_facets.get_facet_triangles(model_volume, color_facets); + if (color_facets.empty()) + return false; + + std::unordered_map> facets_by_source_triangle; + facets_by_source_triangle.reserve(color_facets.size()); + for (size_t idx = 0; idx < color_facets.size(); ++idx) + facets_by_source_triangle[color_facets[idx].source_triangle].emplace_back(idx); + + GUI::GLModel::Geometry geometry; + geometry.format = { GUI::GLModel::Geometry::EPrimitiveType::Triangles, GUI::GLModel::Geometry::EVertexLayout::P3N3C4 }; + geometry.reserve_vertices(color_facets.size() * 3); + geometry.reserve_indices(color_facets.size() * 3); + + std::unordered_map simulated_color_cache; + if (simulation_settings != nullptr) + simulated_color_cache.reserve(std::min(color_facets.size(), size_t(65536))); + auto preview_color = [simulation_settings, &simulated_color_cache](const ColorRGBA &source_color) { + if (simulation_settings == nullptr) + return source_color; + + const uint32_t key = (uint32_t(std::clamp(source_color.r(), 0.f, 1.f) * 255.f + 0.5f) << 24) | + (uint32_t(std::clamp(source_color.g(), 0.f, 1.f) * 255.f + 0.5f) << 16) | + (uint32_t(std::clamp(source_color.b(), 0.f, 1.f) * 255.f + 0.5f) << 8) | + uint32_t(std::clamp(source_color.a(), 0.f, 1.f) * 255.f + 0.5f); + auto cached = simulated_color_cache.find(key); + if (cached != simulated_color_cache.end()) + return cached->second; + + const ColorRGBA simulated_color = simulated_texture_preview_color_for_vertex_color(&source_color, simulation_settings); + simulated_color_cache.emplace(key, simulated_color); + return simulated_color; + }; + + unsigned int vertex_index = 0; + for (const TriangleSelector::FacetStateTriangle &triangle : state_triangles) { + if (triangle.source_triangle < 0) + continue; + + const Vec3f edge_0 = triangle.vertices[1] - triangle.vertices[0]; + const Vec3f edge_1 = triangle.vertices[2] - triangle.vertices[0]; + Vec3f normal = edge_0.cross(edge_1); + const float normal_len = normal.norm(); + if (normal_len <= k_epsilon) + continue; + normal /= normal_len; + const Vec3f offset = normal * k_preview_offset; + + bool emitted_color_facets = false; + auto color_facets_for_triangle = facets_by_source_triangle.find(triangle.source_triangle); + if (color_facets_for_triangle != facets_by_source_triangle.end()) { + const float tolerance = -1e-4f; + for (const size_t facet_idx : color_facets_for_triangle->second) { + if (facet_idx >= color_facets.size()) + continue; + + const ColorFacetTriangle &facet = color_facets[facet_idx]; + const Vec3f centroid = (facet.vertices[0] + facet.vertices[1] + facet.vertices[2]) / 3.f; + Vec3f weights = Vec3f::Zero(); + if (!barycentric_weights(centroid, triangle.vertices[0], triangle.vertices[1], triangle.vertices[2], weights)) + continue; + if (weights.x() < tolerance || weights.y() < tolerance || weights.z() < tolerance) + continue; + + const ColorRGBA color = preview_color(unpack_vertex_color(facet.rgba)); + geometry.add_vertex(facet.vertices[0] + offset, normal, color); + geometry.add_vertex(facet.vertices[1] + offset, normal, color); + geometry.add_vertex(facet.vertices[2] + offset, normal, color); + geometry.add_triangle(vertex_index, vertex_index + 1, vertex_index + 2); + vertex_index += 3; + emitted_color_facets = true; + } + } + + if (emitted_color_facets) + continue; + + std::array leaf_colors; + bool valid_leaf = true; + for (size_t vertex_idx = 0; vertex_idx < triangle.vertices.size(); ++vertex_idx) { + std::optional sampled = + sample_texture_mapping_color_preview(color_facets, + facets_by_source_triangle, + triangle.source_triangle, + triangle.vertices[vertex_idx]); + if (!sampled) { + valid_leaf = false; + break; + } + leaf_colors[vertex_idx] = preview_color(*sampled); + } + + if (!valid_leaf) + continue; + + geometry.add_vertex(triangle.vertices[0] + offset, normal, leaf_colors[0]); + geometry.add_vertex(triangle.vertices[1] + offset, normal, leaf_colors[1]); + geometry.add_vertex(triangle.vertices[2] + offset, normal, leaf_colors[2]); + geometry.add_triangle(vertex_index, vertex_index + 1, vertex_index + 2); + vertex_index += 3; + } + + if (geometry.is_empty()) + return false; + + out_model.init_from(std::move(geometry)); + return true; +} + float normalize_angle(float angle) { if (!std::isfinite(angle)) @@ -1771,26 +1921,35 @@ bool build_surface_gradient_vertex_color_preview_model_for_state(const std::vect struct TexturePreviewRenderState { GLboolean blend_enabled { GL_FALSE }; + GLboolean cull_face_enabled { GL_FALSE }; GLboolean polygon_offset_fill_enabled { GL_FALSE }; GLboolean depth_mask { GL_TRUE }; + GLint cull_face_mode { GL_BACK }; GLfloat polygon_offset_factor { 0.f }; GLfloat polygon_offset_units { 0.f }; GLint depth_func { GL_LESS }; }; -TexturePreviewRenderState begin_render_state() +TexturePreviewRenderState begin_render_state(bool opaque) { TexturePreviewRenderState state; state.blend_enabled = glIsEnabled(GL_BLEND); + state.cull_face_enabled = glIsEnabled(GL_CULL_FACE); state.polygon_offset_fill_enabled = glIsEnabled(GL_POLYGON_OFFSET_FILL); glsafe(::glGetBooleanv(GL_DEPTH_WRITEMASK, &state.depth_mask)); + glsafe(::glGetIntegerv(GL_CULL_FACE_MODE, &state.cull_face_mode)); glsafe(::glGetFloatv(GL_POLYGON_OFFSET_FACTOR, &state.polygon_offset_factor)); glsafe(::glGetFloatv(GL_POLYGON_OFFSET_UNITS, &state.polygon_offset_units)); glsafe(::glGetIntegerv(GL_DEPTH_FUNC, &state.depth_func)); - glsafe(::glEnable(GL_BLEND)); - glsafe(::glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA)); - glsafe(::glDepthMask(GL_FALSE)); + if (opaque) { + glsafe(::glDisable(GL_BLEND)); + } else { + glsafe(::glEnable(GL_BLEND)); + glsafe(::glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA)); + } + glsafe(::glDisable(GL_CULL_FACE)); + glsafe(::glDepthMask(opaque ? GL_TRUE : GL_FALSE)); glsafe(::glDepthFunc(GL_LEQUAL)); glsafe(::glEnable(GL_POLYGON_OFFSET_FILL)); glsafe(::glPolygonOffset(k_polygon_offset_factor, k_polygon_offset_units)); @@ -1804,7 +1963,14 @@ void restore_render_state(const TexturePreviewRenderState &state) glsafe(::glDisable(GL_POLYGON_OFFSET_FILL)); glsafe(::glDepthFunc(state.depth_func)); glsafe(::glDepthMask(state.depth_mask)); - if (!state.blend_enabled) + glsafe(::glCullFace(state.cull_face_mode)); + if (state.cull_face_enabled) + glsafe(::glEnable(GL_CULL_FACE)); + else + glsafe(::glDisable(GL_CULL_FACE)); + if (state.blend_enabled) + glsafe(::glEnable(GL_BLEND)); + else glsafe(::glDisable(GL_BLEND)); } @@ -1913,11 +2079,19 @@ bool build_mmu_vertex_color_preview_models( texture_preview_simulation_settings_for_filament(filament_id, num_physical, texture_mgr, physical_colors); if (simulation_settings) prepare_texture_preview_simulation_settings(*simulation_settings); - if (!build_vertex_color_preview_model_for_state(model_volume, - triangles_per_type[state_id], - simulation_settings ? &*simulation_settings : nullptr, - model)) - continue; + if (model_volume_has_texture_mapping_color_preview_data(model_volume)) { + if (!build_texture_mapping_color_preview_model_for_state(model_volume, + triangles_per_type[state_id], + simulation_settings ? &*simulation_settings : nullptr, + model)) + continue; + } else { + if (!build_vertex_color_preview_model_for_state(model_volume, + triangles_per_type[state_id], + simulation_settings ? &*simulation_settings : nullptr, + model)) + continue; + } } out_models.emplace_back(std::move(model)); @@ -1969,6 +2143,31 @@ size_t model_volume_texture_preview_signature(const ModelVolume &model_volume) return signature; } +size_t model_volume_texture_mapping_color_preview_signature(const ModelVolume &model_volume) +{ + size_t signature = 1469598103934665603ull; + auto mix = [&signature](size_t value) { + signature ^= value + 0x9e3779b97f4a7c15ull + (signature << 6) + (signature >> 2); + }; + + const TriangleColorSplittingData &data = model_volume.texture_mapping_color_facets.get_data(); + mix(data.triangles_to_split.size()); + mix(data.bitstream.size()); + mix(data.colors_rgba.size()); + for (const ColorTriangleBitStreamMapping &mapping : data.triangles_to_split) { + mix(size_t(mapping.triangle_idx)); + mix(size_t(mapping.bitstream_start_idx)); + mix(size_t(mapping.color_start_idx)); + } + for (const bool bit : data.bitstream) + mix(bit ? 1u : 0u); + for (const uint32_t color : data.colors_rgba) + mix(size_t(color)); + for (const char ch : data.metadata_json) + mix(size_t(static_cast(ch))); + return signature; +} + bool ensure_model_volume_texture_preview(const ModelVolume &model_volume, GUI::GLTexture &texture, size_t &texture_signature) @@ -2067,7 +2266,8 @@ void render_model_texture_preview_models( const std::array &clipping_plane, int print_volume_type, const std::array &print_volume_xy, - const std::array &print_volume_z) + const std::array &print_volume_z, + bool opaque) { if (models.empty() || colors.size() != models.size() || filament_ids.size() != models.size() || texture.get_id() == 0) return; @@ -2076,7 +2276,7 @@ void render_model_texture_preview_models( if (shader == nullptr) return; - const TexturePreviewRenderState render_state = begin_render_state(); + const TexturePreviewRenderState render_state = begin_render_state(opaque); shader->start_using(); set_common_uniforms(*shader, model_matrix, view_matrix, projection_matrix, z_range, clipping_plane, print_volume_type, print_volume_xy, print_volume_z); glsafe(::glActiveTexture(GL_TEXTURE0)); @@ -2128,7 +2328,8 @@ void render_model_vertex_color_preview_models( const std::array &clipping_plane, int print_volume_type, const std::array &print_volume_xy, - const std::array &print_volume_z) + const std::array &print_volume_z, + bool opaque) { if (models.empty() || colors.size() != models.size() || filament_ids.size() != models.size()) return; @@ -2137,7 +2338,7 @@ void render_model_vertex_color_preview_models( if (shader == nullptr) return; - const TexturePreviewRenderState render_state = begin_render_state(); + const TexturePreviewRenderState render_state = begin_render_state(opaque); shader->start_using(); set_common_uniforms(*shader, model_matrix, view_matrix, projection_matrix, z_range, clipping_plane, print_volume_type, print_volume_xy, print_volume_z); diff --git a/src/slic3r/GUI/MMUPaintedTexturePreview.hpp b/src/slic3r/GUI/MMUPaintedTexturePreview.hpp index 0da4c43741..f7e749cd30 100644 --- a/src/slic3r/GUI/MMUPaintedTexturePreview.hpp +++ b/src/slic3r/GUI/MMUPaintedTexturePreview.hpp @@ -50,6 +50,7 @@ bool build_mmu_vertex_color_preview_models( std::vector &out_filament_ids); size_t model_volume_texture_preview_signature(const ModelVolume &model_volume); +size_t model_volume_texture_mapping_color_preview_signature(const ModelVolume &model_volume); bool ensure_model_volume_texture_preview(const ModelVolume &model_volume, GUI::GLTexture &texture, @@ -75,7 +76,8 @@ void render_model_texture_preview_models( const std::array &clipping_plane, int print_volume_type = -1, const std::array &print_volume_xy = std::array{ 0.f, 0.f, 0.f, 0.f }, - const std::array &print_volume_z = std::array{ 0.f, 0.f }); + const std::array &print_volume_z = std::array{ 0.f, 0.f }, + bool opaque = false); void render_model_vertex_color_preview_models( std::vector &models, @@ -90,7 +92,8 @@ void render_model_vertex_color_preview_models( const std::array &clipping_plane, int print_volume_type = -1, const std::array &print_volume_xy = std::array{ 0.f, 0.f, 0.f, 0.f }, - const std::array &print_volume_z = std::array{ 0.f, 0.f }); + const std::array &print_volume_z = std::array{ 0.f, 0.f }, + bool opaque = false); } // namespace Slic3r diff --git a/src/slic3r/GUI/NotificationManager.cpp b/src/slic3r/GUI/NotificationManager.cpp index 620e5fa8ef..e108841399 100644 --- a/src/slic3r/GUI/NotificationManager.cpp +++ b/src/slic3r/GUI/NotificationManager.cpp @@ -1302,7 +1302,11 @@ void NotificationManager::UpdatedItemsInfoNotification::add_type(InfoItemType ty case InfoItemType::CustomSupports: text += format(_L_PLURAL("%1$d Object has custom supports.", "%1$d Objects have custom supports.", (*it).second), (*it).second) + "\n"; break; // BBS //case InfoItemType::CustomSeam: text += format(("%1$d Object has custom seam.", "%1$d Objects have custom seam.", (*it).second), (*it).second) + "\n"; break; - case InfoItemType::MmSegmentation: text += format(_L_PLURAL("%1$d Object has color painting.", "%1$d Objects have color painting.",(*it).second), (*it).second) + "\n"; break; + case InfoItemType::MmSegmentation: + text += format(_L_PLURAL("%1$d Object has color region painting.", + "%1$d Objects have color region painting.", + (*it).second), (*it).second) + "\n"; + break; // BBS //case InfoItemType::Sinking: text += format(("%1$d Object has partial sinking.", "%1$d Objects have partial sinking.", (*it).second), (*it).second) + "\n"; break; case InfoItemType::CutConnectors: text += format(_L_PLURAL("%1$d object was loaded as a part of cut object.", "%1$d objects were loaded as parts of cut object.", (*it).second), (*it).second) + "\n"; break; diff --git a/src/slic3r/GUI/ObjectDataViewModel.cpp b/src/slic3r/GUI/ObjectDataViewModel.cpp index ed4ad60f55..e48752f945 100644 --- a/src/slic3r/GUI/ObjectDataViewModel.cpp +++ b/src/slic3r/GUI/ObjectDataViewModel.cpp @@ -69,7 +69,7 @@ const std::map INFO_ITEMS{ // info_item Type info_item Name info_item BitmapName { InfoItemType::CustomSupports, {L("Support painting"), "toolbar_support" }, }, //{ InfoItemType::CustomSeam, {L("Paint-on seam"), "seam_" }, }, - { InfoItemType::MmSegmentation, {L("Color painting"), "mmu_segmentation"}, }, + { InfoItemType::MmSegmentation, {L("Color region painting"), "mmu_segmentation"}, }, //{ InfoItemType::Sinking, {L("Sinking"), "objlist_sinking"}, }, { InfoItemType::CutConnectors, {L("Cut connectors"), "cut_connectors" }, }, { InfoItemType::FuzzySkin, {L("Paint-on fuzzy skin"), "objlist_fuzzy_skin_paint" }, }, @@ -2495,4 +2495,3 @@ void ObjectDataViewModel::UpdateCutObjectIcon(const wxDataViewItem &item, bool h } // namespace GUI } // namespace Slic3r - diff --git a/src/slic3r/GUI/Plater.cpp b/src/slic3r/GUI/Plater.cpp index 66d3fcd491..ad299a4dfa 100644 --- a/src/slic3r/GUI/Plater.cpp +++ b/src/slic3r/GUI/Plater.cpp @@ -18460,7 +18460,7 @@ void Plater::clear_before_change_mesh(int obj_idx) get_notification_manager()->push_notification( NotificationType::CustomSupportsAndSeamRemovedAfterRepair, NotificationManager::NotificationLevel::PrintInfoNotificationLevel, - _u8L("Custom supports and color painting were removed before repairing.")); + _u8L("Custom supports and color region painting were removed before repairing.")); } }