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