From b1e3cdc666b19948d64dc66e9ff9aa834a10ce9f Mon Sep 17 00:00:00 2001 From: SoftFever Date: Sat, 22 Aug 2026 13:15:09 +0800 Subject: [PATCH] Port colored OBJ import pipeline from BambuStudio --- src/libslic3r/Format/objparser.cpp | 112 ++- src/libslic3r/Model.cpp | 68 +- src/libslic3r/TexturePainting.cpp | 63 ++ src/libslic3r/TexturePainting.hpp | 22 + .../TextureToColor/TextureToColor.cpp | 757 ++++++++++++------ .../TextureToColor/TextureToColor.hpp | 38 + src/slic3r/GUI/Plater.cpp | 3 + src/slic3r/GUI/TextureImportDialog.cpp | 119 ++- src/slic3r/GUI/TextureImportDialog.hpp | 8 + 9 files changed, 893 insertions(+), 297 deletions(-) diff --git a/src/libslic3r/Format/objparser.cpp b/src/libslic3r/Format/objparser.cpp index 6ee117adc9..886fa423bd 100644 --- a/src/libslic3r/Format/objparser.cpp +++ b/src/libslic3r/Format/objparser.cpp @@ -262,12 +262,9 @@ static bool obj_parseline(const char *line, ObjData &data) } face_index_count++; } - if (face_index_count == 3) {//tri - data.usemtls.back().face_end++; - } else if (face_index_count == 4) {//quad - data.usemtls.back().face_end++; - data.usemtls.back().face_end++; - } + if (face_index_count >= 3) { + data.usemtls.back().face_end += face_index_count - 2; + } } vertex.coordIdx = -1; vertex.normalIdx = -1; @@ -374,6 +371,107 @@ static bool obj_parseline(const char *line, ObjData &data) return true; } static std::string cur_mtl_name = ""; +static bool mtl_is_space(char c) +{ + return c == ' ' || c == '\t' || c == '\r'; +} + +static const char* mtl_skip_ws(const char *line) +{ + while (mtl_is_space(*line)) + ++line; + return line; +} + +static const char* mtl_skip_token(const char *line) +{ + while (*line != 0 && !mtl_is_space(*line)) + ++line; + return line; +} + +static bool mtl_token_equals(const char *begin, const char *end, const char *token) +{ + const size_t len = static_cast(end - begin); + return strlen(token) == len && strncmp(begin, token, len) == 0; +} + +static std::string mtl_trim_value(const char *line) +{ + const char *begin = mtl_skip_ws(line); + const char *end = begin + strlen(begin); + while (end > begin && mtl_is_space(*(end - 1))) + --end; + return std::string(begin, end); +} + +static bool mtl_skip_numeric_token(const char *&line) +{ + const char *begin = mtl_skip_ws(line); + if (*begin == 0) + return false; + char *endptr = 0; + strtod(begin, &endptr); + if (endptr == begin || (!mtl_is_space(*endptr) && *endptr != 0)) + return false; + line = mtl_skip_ws(endptr); + return true; +} + +static bool mtl_skip_required_tokens(const char *&line, int count) +{ + for (int i = 0; i < count; ++i) { + line = mtl_skip_ws(line); + if (*line == 0) + return false; + line = mtl_skip_token(line); + } + line = mtl_skip_ws(line); + return true; +} + +static std::string mtl_parse_texture_name(const char *line) +{ + const char *original = mtl_skip_ws(line); + const char *current = original; + + while (*current == '-') { + const char *option_begin = current; + const char *option_end = mtl_skip_token(current); + current = option_end; + + if (mtl_token_equals(option_begin, option_end, "-o") || + mtl_token_equals(option_begin, option_end, "-s") || + mtl_token_equals(option_begin, option_end, "-t")) { + int skipped = 0; + while (skipped < 3 && mtl_skip_numeric_token(current)) + ++skipped; + if (skipped == 0) + return mtl_trim_value(original); + continue; + } + + int option_args = -1; + if (mtl_token_equals(option_begin, option_end, "-mm")) + option_args = 2; + else if (mtl_token_equals(option_begin, option_end, "-bm") || + mtl_token_equals(option_begin, option_end, "-boost") || + mtl_token_equals(option_begin, option_end, "-texres") || + mtl_token_equals(option_begin, option_end, "-clamp") || + mtl_token_equals(option_begin, option_end, "-blendu") || + mtl_token_equals(option_begin, option_end, "-blendv") || + mtl_token_equals(option_begin, option_end, "-cc") || + mtl_token_equals(option_begin, option_end, "-imfchan") || + mtl_token_equals(option_begin, option_end, "-type")) + option_args = 1; + + if (option_args < 0 || !mtl_skip_required_tokens(current, option_args)) + return mtl_trim_value(original); + } + + return mtl_trim_value(current); +} + static bool mtl_parseline(const char *line, MtlData &data) { if (*line == 0) return true; @@ -401,7 +499,7 @@ static bool mtl_parseline(const char *line, MtlData &data) if (*(line++) != 'a' || *(line++) != 'p' || *(line++) != '_' || *(line++) != 'K' || *(line++) != 'd') return false; EATWS(); if (data.new_mtl_unmap.find(cur_mtl_name) != data.new_mtl_unmap.end()) { - data.new_mtl_unmap[cur_mtl_name]->map_Kd = line; + data.new_mtl_unmap[cur_mtl_name]->map_Kd = mtl_parse_texture_name(line); } break; } diff --git a/src/libslic3r/Model.cpp b/src/libslic3r/Model.cpp index c9322eff3c..81e5990d36 100644 --- a/src/libslic3r/Model.cpp +++ b/src/libslic3r/Model.cpp @@ -320,31 +320,55 @@ Model Model::read_from_file(const std::string& model.texture_mesh = tex_mesh; } } - else if (result){ - ObjDialogInOut in_out; - in_out.model = &model; - in_out.lost_material_name = obj_info.lost_material_name; + else if (result && !model.objects.empty() && !model.objects.back()->volumes.empty()) { + // Vertex-colour and MTL face-colour OBJs also go through the texture-to-color + // importer (as precomputed per-face colors) instead of the legacy flat + // per-face colour dialog, matching the uv_png branch above. + auto build_tex_mesh_geometry = [&]() { + auto tex_mesh = std::make_shared(); + const auto& its = model.objects.back()->volumes[0]->mesh().its; + tex_mesh->vertices.resize(its.vertices.size()); + for (size_t i = 0; i < its.vertices.size(); ++i) + tex_mesh->vertices[i] = {its.vertices[i].x(), its.vertices[i].y(), its.vertices[i].z()}; + tex_mesh->indices.resize(its.indices.size()); + for (size_t i = 0; i < its.indices.size(); ++i) + tex_mesh->indices[i] = {its.indices[i][0], its.indices[i][1], its.indices[i][2]}; + return tex_mesh; + }; if (obj_info.vertex_colors.size() > 0) { - if (objFn) { // 1.result is ok and pop up a dialog - in_out.input_colors = std::move(obj_info.vertex_colors); - in_out.is_single_color = false; - in_out.deal_vertex_color = true; - objFn(in_out); + auto tex_mesh = build_tex_mesh_geometry(); + const auto& its = model.objects.back()->volumes[0]->mesh().its; + tex_mesh->precomputed_face_colors.resize(its.indices.size()); + for (size_t i = 0; i < its.indices.size(); ++i) { + const auto& f = its.indices[i]; + auto avg = [&](int ch) -> std::size_t { + float v = (obj_info.vertex_colors[f[0]][ch] + + obj_info.vertex_colors[f[1]][ch] + + obj_info.vertex_colors[f[2]][ch]) / 3.0f * 255.0f; + return (std::size_t) std::clamp(v, 0.0f, 255.0f); + }; + tex_mesh->precomputed_face_colors[i] = {avg(0), avg(1), avg(2)}; } - } else if (obj_info.face_colors.size() > 0 && obj_info.has_uv_png == false) { // mtl file - if (objFn) { // 1.result is ok and pop up a dialog - in_out.input_colors = std::move(obj_info.face_colors); - in_out.is_single_color = obj_info.is_single_mtl; - in_out.deal_vertex_color = false; - objFn(in_out); + tex_mesh->precomputed_vertex_colors = obj_info.vertex_colors; + model.texture_mesh = tex_mesh; + } else if (obj_info.face_colors.size() > 0 && obj_info.has_uv_png == false) { + auto tex_mesh = build_tex_mesh_geometry(); + const size_t nf = tex_mesh->indices.size(); + tex_mesh->precomputed_face_colors.resize(nf); + for (size_t i = 0; i < nf; ++i) { + if (i < obj_info.face_colors.size()) { + const auto& c = obj_info.face_colors[i]; + tex_mesh->precomputed_face_colors[i] = { + (std::size_t) std::clamp(c[0] * 255.0f, 0.0f, 255.0f), + (std::size_t) std::clamp(c[1] * 255.0f, 0.0f, 255.0f), + (std::size_t) std::clamp(c[2] * 255.0f, 0.0f, 255.0f) + }; + } else { + tex_mesh->precomputed_face_colors[i] = {128, 128, 128}; + } } - } /*else if (obj_info.has_uv_png && obj_info.uvs.size() > 0) { - boost::filesystem::path full_path(input_file); - std::string obj_directory = full_path.parent_path().string(); - obj_info.obj_dircetory = obj_directory; - result = false; - message = _L("Importing obj with png function is developing."); - }*/ + model.texture_mesh = tex_mesh; + } } } else if (boost::algorithm::iends_with(input_file, ".glb") || diff --git a/src/libslic3r/TexturePainting.cpp b/src/libslic3r/TexturePainting.cpp index 187f218863..9f83f282cd 100644 --- a/src/libslic3r/TexturePainting.cpp +++ b/src/libslic3r/TexturePainting.cpp @@ -353,6 +353,69 @@ bool texture_to_painting( return true; } +bool face_colors_to_painting( + const TexturedMesh& mesh, + PaintedMesh& painted, + const TexturePaintingSettings& settings, + PaintProgressCallback progress, + PaintCancelCallback cancel) +{ + if (mesh.vertices.empty() || mesh.indices.empty() || mesh.precomputed_face_colors.empty()) + return false; + + // Build tex2color::TriMesh from input geometry + tex2color::TriMesh input_mesh; + input_mesh.vertices.resize(mesh.vertices.size()); + for (size_t i = 0; i < mesh.vertices.size(); ++i) + input_mesh.vertices[i] = Vec3f(mesh.vertices[i][0], mesh.vertices[i][1], mesh.vertices[i][2]); + input_mesh.indices.resize(mesh.indices.size()); + for (size_t i = 0; i < mesh.indices.size(); ++i) + input_mesh.indices[i] = Vec3i32(mesh.indices[i][0], mesh.indices[i][1], mesh.indices[i][2]); + + // Forward settings to tex2color + tex2color::TextureToColorSettings algo_settings; + algo_settings.target_colors_num = settings.target_colors_num; + algo_settings.smooth_weight = settings.smooth_weight; + switch (settings.mesh_repair_decision) { + case TexturePaintingSettings::MeshRepairDecision::Ask: + algo_settings.mesh_repair_decision = tex2color::MeshRepairDecision::Ask; + break; + case TexturePaintingSettings::MeshRepairDecision::RepairAndImport: + algo_settings.mesh_repair_decision = tex2color::MeshRepairDecision::RepairAndImport; + break; + case TexturePaintingSettings::MeshRepairDecision::ImportWithoutRepair: + default: + algo_settings.mesh_repair_decision = tex2color::MeshRepairDecision::ImportWithoutRepair; + break; + } + algo_settings.mesh_repair_decision_required = settings.mesh_repair_decision_required; + algo_settings.mesh_repair_callback = settings.mesh_repair_callback; + + tex2color::AlgoProgressCallback algo_progress = nullptr; + if (progress) { + algo_progress = [&progress](tex2color::AlgoProgress p) { + progress(p.percent, p.message); + }; + } + tex2color::AlgoCancelCallback algo_cancel = nullptr; + if (cancel) { + algo_cancel = [&cancel]() -> bool { return cancel(); }; + } + + tex2color::TriMesh out_mesh; + std::vector> out_face_colors; + bool ok = tex2color::ClusterAndSmooth( + input_mesh, mesh.precomputed_face_colors, out_mesh, out_face_colors, + algo_settings, algo_progress, algo_cancel, + mesh.precomputed_vertex_colors); + + if (!ok) + return false; + + extract_painted_mesh(out_mesh, out_face_colors, painted); + return true; +} + double compute_delta_e( const std::array& rgb1, const std::array& rgba2) diff --git a/src/libslic3r/TexturePainting.hpp b/src/libslic3r/TexturePainting.hpp index ac98e968c7..fc4688620b 100644 --- a/src/libslic3r/TexturePainting.hpp +++ b/src/libslic3r/TexturePainting.hpp @@ -38,6 +38,18 @@ struct TexturedMesh { std::vector> uv_indices; // per-face UV indices into uv_coords bool has_face_uvs() const { return !uv_indices.empty() && !uv_coords.empty(); } + + // Pre-computed per-face colors (e.g. from OBJ vertex colors or MTL Kd). + // When non-empty, the pipeline skips texture decode/sample/oversample and + // consumes these instead of sampling a texture. + // Each entry is {R, G, B} in [0..255]. + std::vector> precomputed_face_colors; + + // Per-vertex colors from OBJ (RGBA, [0..1]), indexed by vertex index. + // On a low-poly mesh these are quantized into a small palette and the mesh is + // split along the resulting cluster boundaries, so color borders stay sharp + // instead of being averaged away into a single color per face. + std::vector> precomputed_vertex_colors; }; struct PaintedMesh { @@ -83,6 +95,16 @@ bool texture_to_painting( const TexturePaintingSettings& settings = {}, PaintProgressCallback progress = nullptr, PaintCancelCallback cancel = nullptr); +// Turn pre-computed per-face colors into a painted mesh, skipping texture decode +// and UV sampling. A low-poly mesh that also carries precomputed_vertex_colors is +// split along quantized color boundaries, which replaces its geometry. +bool face_colors_to_painting( + const TexturedMesh& mesh, + PaintedMesh& painted, + const TexturePaintingSettings& settings = {}, + PaintProgressCallback progress = nullptr, + PaintCancelCallback cancel = nullptr); + std::vector match_clusters_to_filaments( const std::vector>& cluster_colors, diff --git a/src/libslic3r/TextureToColor/TextureToColor.cpp b/src/libslic3r/TextureToColor/TextureToColor.cpp index e3afc63cd9..bcdc985fc9 100644 --- a/src/libslic3r/TextureToColor/TextureToColor.cpp +++ b/src/libslic3r/TextureToColor/TextureToColor.cpp @@ -7,7 +7,6 @@ #include #include #include -#include #include #include "CgalUtils.hpp" @@ -379,6 +378,413 @@ static bool linear_subdivision(TriMesh& mesh, std::vector& uv_coord return true; } +using VertexColor = std::array; + +// Quantize continuous per-vertex colors into a small palette of cluster centers. +// The legacy OBJ vertex-color import consumed discrete filament ids, so split +// decisions could be made by comparing integers. Quantizing up front restores +// that property for the adaptive splitter below. +static bool quantize_vertex_colors( + const std::vector& vertex_colors, + const TextureToColorSettings& settings, + AlgoCancelCallback cancel_callback, + std::vector& out_centers, + std::vector& out_vertex_cluster_ids) +{ + out_centers.clear(); + out_vertex_cluster_ids.clear(); + if (vertex_colors.empty()) + return false; + + std::vector vertex_rgb(vertex_colors.size()); + for (std::size_t i = 0; i < vertex_colors.size(); ++i) { + for (int c = 0; c < 3; ++c) { + float v = std::clamp(vertex_colors[i][c] * 255.0f, 0.0f, 255.0f); + vertex_rgb[i][c] = static_cast(v); + } + } + + ClusterParameters para; + para.cancel_callback = cancel_callback ? [&]() { return cancel_callback(); } : std::function{}; + if (settings.target_colors_num == 0) { + para.max_color_distance = settings.max_color_distance; + para.max_cluster_k = settings.max_cluster_k; + out_centers = cluster_adaptive(vertex_rgb, para); + } else { + para.cluster_k = settings.target_colors_num; + out_centers = cluster_k_means(vertex_rgb, para); + } + if (out_centers.empty()) { + BOOST_LOG_TRIVIAL(debug) << "quantize_vertex_colors: no cluster center generated."; + return false; + } + + out_vertex_cluster_ids.resize(vertex_rgb.size()); + for (std::size_t i = 0; i < vertex_rgb.size(); ++i) { + std::size_t nearest_id = 0; + if (!calc_nearest_color_id(out_centers, vertex_rgb[i], nearest_id)) + nearest_id = 0; + out_vertex_cluster_ids[i] = nearest_id; + } + BOOST_LOG_TRIVIAL(debug) << "quantize_vertex_colors: quantized " << vertex_rgb.size() + << " vertex colors into " << out_centers.size() << " clusters."; + return true; +} + +// Single-level adaptive subdivision driven by per-vertex cluster ids. +// +// Reproduces the split topology that the legacy OBJ vertex-color import encoded +// into mmu_segmentation_facets (TriangleSelector::perform_split cases 1/2/3), but +// materializes it as real geometry. An edge is split at its midpoint if and only +// if its two endpoints belong to different clusters. Because that predicate reads +// only the shared endpoints, adjacent faces always reach the same conclusion and +// no T-junctions can appear. +static bool adaptive_split_by_vertex_clusters( + TriMesh& mesh, + const std::vector& vertex_cluster_ids, + const std::vector& cluster_centers, + std::vector& out_face_colors) +{ + const TriVertices original_vertices = mesh.vertices; + const TriFaces original_faces = mesh.indices; + if (original_vertices.empty() || original_faces.empty() || cluster_centers.empty()) + return false; + if (vertex_cluster_ids.size() != original_vertices.size()) { + BOOST_LOG_TRIVIAL(warning) << "adaptive_split_by_vertex_clusters: cluster id count (" + << vertex_cluster_ids.size() << ") != vertex count (" + << original_vertices.size() << ")."; + return false; + } + if (original_vertices.size() >= (1ULL << 32)) [[unlikely]] { + BOOST_LOG_TRIVIAL(warning) << "adaptive_split_by_vertex_clusters: vertex_count=" + << original_vertices.size() << " exceeds 32-bit edge_key range."; + return false; + } + + TriVertices out_vertices = original_vertices; + TriFaces out_faces; + out_faces.reserve(original_faces.size() * 5); + out_face_colors.clear(); + out_face_colors.reserve(original_faces.size() * 5); + + auto edge_key = [](std::size_t a, std::size_t b) -> uint64_t { + return a < b ? ((static_cast(a) << 32) | b) + : ((static_cast(b) << 32) | a); + }; + std::unordered_map edge_to_mid; + edge_to_mid.reserve(original_faces.size() * 3 / 2); + + // Midpoints on shared edges must be deduplicated so that neighbouring faces + // reference the same vertex instead of coincident duplicates. + auto midpoint_of_edge = [&](std::size_t a, std::size_t b) -> std::size_t { + const uint64_t key = edge_key(a, b); + auto it = edge_to_mid.find(key); + if (it != edge_to_mid.end()) + return it->second; + const std::size_t idx = out_vertices.size(); + out_vertices.push_back((original_vertices[a] + original_vertices[b]) * 0.5f); + edge_to_mid.emplace(key, idx); + return idx; + }; + // Points strictly inside an original face are never shared, so they skip the map. + // The midpoint is computed before push_back so a reallocation cannot dangle it. + auto append_interior_midpoint = [&](std::size_t a, std::size_t b) -> std::size_t { + const TriVertex mid = (out_vertices[a] + out_vertices[b]) * 0.5f; + const std::size_t idx = out_vertices.size(); + out_vertices.push_back(mid); + return idx; + }; + auto emit = [&](std::size_t a, std::size_t b, std::size_t c, std::size_t cluster_id) { + out_faces.push_back(Vec3i32(static_cast(a), static_cast(b), static_cast(c))); + out_face_colors.push_back(cluster_centers[cluster_id]); + }; + + for (const auto& f : original_faces) { + const std::size_t v[3] = {static_cast(f[0]), static_cast(f[1]), static_cast(f[2])}; + const std::size_t c[3] = {vertex_cluster_ids[v[0]], vertex_cluster_ids[v[1]], vertex_cluster_ids[v[2]]}; + + // Case A: uniform cluster, keep the face untouched. + if (c[0] == c[1] && c[1] == c[2]) { + emit(v[0], v[1], v[2], c[0]); + continue; + } + + // Case B: two vertices share a cluster and the third is isolated. Split the + // two edges incident to the isolated vertex, which are exactly the + // cross-cluster ones; the opposite edge stays intact. + int iso = -1; + if (c[1] == c[2]) iso = 0; + else if (c[2] == c[0]) iso = 1; + else if (c[0] == c[1]) iso = 2; + if (iso >= 0) { + const int i = iso, j = (iso + 1) % 3, k = (iso + 2) % 3; + const std::size_t m_ij = midpoint_of_edge(v[i], v[j]); + const std::size_t m_ki = midpoint_of_edge(v[k], v[i]); + emit(v[i], m_ij, m_ki, c[i]); + emit(m_ij, v[j], m_ki, c[j]); + emit(v[j], v[k], m_ki, c[j]); + continue; + } + + // Case C: all three clusters differ. Split every edge, then cut the centre + // triangle once more. The centre is equidistant from all three clusters, so + // the legacy heuristic selects the cut by widest interior angle, which is + // the vertex opposite the longest edge. + const std::size_t m01 = midpoint_of_edge(v[0], v[1]); + const std::size_t m12 = midpoint_of_edge(v[1], v[2]); + const std::size_t m20 = midpoint_of_edge(v[2], v[0]); + emit(v[0], m01, m20, c[0]); + emit(m01, v[1], m12, c[1]); + emit(m12, v[2], m20, c[2]); + + const TriVertex& p0 = original_vertices[v[0]]; + const TriVertex& p1 = original_vertices[v[1]]; + const TriVertex& p2 = original_vertices[v[2]]; + const float sq_opposite_v0 = (p2 - p1).squaredNorm(); + const float sq_opposite_v1 = (p0 - p2).squaredNorm(); + const float sq_opposite_v2 = (p1 - p0).squaredNorm(); + int widest = 0; + float widest_len = sq_opposite_v0; + if (sq_opposite_v1 > widest_len) { widest = 1; widest_len = sq_opposite_v1; } + if (sq_opposite_v2 > widest_len) { widest = 2; } + + if (widest == 0) { + const std::size_t mc = append_interior_midpoint(m20, m01); + emit(m12, m20, mc, c[1]); + emit(mc, m01, m12, c[2]); + } else if (widest == 1) { + const std::size_t mc = append_interior_midpoint(m01, m12); + emit(m20, m01, mc, c[0]); + emit(mc, m12, m20, c[2]); + } else { + const std::size_t mc = append_interior_midpoint(m12, m20); + emit(m01, m12, mc, c[1]); + emit(mc, m20, m01, c[0]); + } + } + + BOOST_LOG_TRIVIAL(info) << "adaptive_split_by_vertex_clusters: faces " << original_faces.size() + << " -> " << out_faces.size() << ", vertices " << original_vertices.size() + << " -> " << out_vertices.size(); + mesh = TriMesh(out_faces, out_vertices); + return true; +} + +// Shared pipeline: mesh repair -> color clustering -> label assignment -> smoothing. +// Called by both TextureToColor (after UV sampling) and ClusterAndSmooth (after vertex-color oversample). +// progress_callback reports 0~100 within this function; the caller maps it to its own global range. +static bool repair_cluster_smooth( + TriMesh& mesh, + std::vector& face_colors, + std::vector& out_clustered_face_colors, + const TextureToColorSettings& settings, + AlgoProgressCallback progress_callback, + AlgoCancelCallback cancel_callback, + const char* log_prefix) +{ + auto report = [&](int pct, const char* msg) { + if (progress_callback) + progress_callback({pct, msg}); + }; + auto cancelled = [&]() -> bool { + if (cancel_callback && cancel_callback()) { + BOOST_LOG_TRIVIAL(debug) << log_prefix << " cancelled"; + return true; + } + return false; + }; + + report(0, "Repairing mesh"); + if (cancelled()) return false; + + // Resample face colors onto a repaired mesh via centroid nearest-neighbor. + auto resample_face_colors = [&](TriMesh&& repaired_mesh) -> bool { + TriVertices old_vertices = std::move(mesh.vertices); + TriFaces old_indices = std::move(mesh.indices); + auto aabb_tree = AABBTreeIndirect::build_aabb_tree_over_indexed_triangle_set(old_vertices, old_indices); + mesh = std::move(repaired_mesh); + + if (is_closed(mesh)) { + BOOST_LOG_TRIVIAL(debug) << log_prefix << ": repaired mesh is closed."; + } else { + BOOST_LOG_TRIVIAL(debug) << log_prefix << ": repaired mesh is open."; + } + + std::vector new_face_colors(mesh.facets_count()); + tbb::parallel_for(tbb::blocked_range(0, mesh.facets_count()), [&](const tbb::blocked_range& range) { + for (std::size_t fid = range.begin(); fid < range.end(); ++fid) { + const auto& face = mesh.indices[fid]; + Vec3f center = (mesh.vertices[face[0]] + mesh.vertices[face[1]] + mesh.vertices[face[2]]) / 3.0f; + size_t hit_idx = 0; + Vec3f closest; + AABBTreeIndirect::squared_distance_to_indexed_triangle_set( + old_vertices, old_indices, aabb_tree, center, hit_idx, closest); + new_face_colors[fid] = face_colors[hit_idx]; + } + }); + face_colors = std::move(new_face_colors); + return true; + }; + + auto repair_and_resample = [&]() -> bool { + std::shared_ptr repaired_mesh; + if (!RepairMesh(mesh, repaired_mesh)) { + BOOST_LOG_TRIVIAL(debug) << log_prefix << ": RepairMesh failed."; + return false; + } + if (cancelled()) return false; + return resample_face_colors(std::move(*repaired_mesh)); + }; + + { + TriangleMesh stats_mesh(static_cast(mesh)); + const auto& stats = stats_mesh.stats(); + // Orca's TriangleMeshStats defines manifold() as open_edges == 0 and does not track + // non-manifold edges/vertices separately, so BBS's "!manifold() || has_open_edges()" + // collapses to this single test and the extra counters drop out of the log. + if (!stats.manifold()) { + BOOST_LOG_TRIVIAL(info) << log_prefix << ": mesh has non-manifold geometry or open boundaries, open_edges=" + << stats.open_edges; + if (settings.mesh_repair_decision == MeshRepairDecision::Ask) { + if (settings.mesh_repair_decision_required) + *settings.mesh_repair_decision_required = true; + return false; + } + if (settings.mesh_repair_decision == MeshRepairDecision::RepairAndImport) { + indexed_triangle_set repaired_its; + std::string repair_error; + bool repaired = settings.mesh_repair_callback && settings.mesh_repair_callback( + static_cast(mesh), repaired_its, + [&](const char* message, unsigned /*percent*/) { + report(5, message ? message : "Repairing mesh"); + }, + [&]() { return cancelled(); }, &repair_error); + if (repaired) { + if (cancelled()) return false; + BOOST_LOG_TRIVIAL(info) << log_prefix << ": Windows 3D mesh repair finished."; + if (!resample_face_colors(TriMesh(std::move(repaired_its)))) + return false; + } else { + BOOST_LOG_TRIVIAL(warning) << log_prefix << ": Windows 3D mesh repair failed: " << repair_error; + } + } else { + BOOST_LOG_TRIVIAL(info) << log_prefix << ": importing mesh without Windows 3D repair."; + } + } + } + + if (!cgalutils::is_mesh_halfedge_compatible(mesh)) { + BOOST_LOG_TRIVIAL(info) << log_prefix << ": mesh not halfedge-compatible, attempting RepairMesh."; + if (!repair_and_resample()) + return false; + } + +#ifdef OUTPUT_TEST_RESULT + SaveToOFF(std::string(log_prefix) + "_1_repair.off", mesh, face_colors); +#endif + + report(20, "Color clustering"); + if (cancelled()) return false; + + // Clustering + std::vector cluster_centers; + out_clustered_face_colors = face_colors; + std::vector clustered_face_labels(face_colors.size()); + const bool adaptive_cluster = settings.target_colors_num == 0; + + if (adaptive_cluster) { + BOOST_LOG_TRIVIAL(debug) << log_prefix << ": use cluster adaptive method."; + ClusterParameters para; + para.max_color_distance = settings.max_color_distance; + para.max_cluster_k = settings.max_cluster_k; + para.cancel_callback = cancel_callback ? [&]() { return cancel_callback(); } : std::function{}; + cluster_centers = cluster_adaptive(face_colors, para); + if (cancelled()) return false; + } else { + BOOST_LOG_TRIVIAL(debug) << log_prefix << ": use cluster k-means method."; + ClusterParameters para; + para.cluster_k = settings.target_colors_num; + para.cancel_callback = cancel_callback ? [&]() { return cancel_callback(); } : std::function{}; + cluster_centers = cluster_k_means(face_colors, para); + if (cancelled()) return false; + } + + BOOST_LOG_TRIVIAL(debug) << log_prefix << ": k = " << cluster_centers.size() << "."; + if (cluster_centers.empty()) { + BOOST_LOG_TRIVIAL(debug) << log_prefix << ": no cluster center generated."; + return false; + } + + report(40, "Assigning cluster labels"); + if (cancelled()) return false; + + // Assign each face to nearest cluster center + { + std::atomic done{0}; + std::atomic cancel_requested{false}; + const size_t total = mesh.indices.size(); + const size_t interval = std::max(total / 20, 1); + tbb::parallel_for(tbb::blocked_range(0, total), [&](const tbb::blocked_range& range) { + for (std::size_t fid = range.begin(); fid < range.end(); ++fid) { + if (cancel_requested.load(std::memory_order_relaxed)) return; + std::size_t nearest_id = 0; + calc_nearest_color_id(cluster_centers, face_colors[fid], nearest_id); + clustered_face_labels[fid] = nearest_id; + out_clustered_face_colors[fid] = cluster_centers[nearest_id]; + size_t cnt = done.fetch_add(1, std::memory_order_relaxed) + 1; + if (cnt % interval == 0) { + if (cancelled()) { cancel_requested.store(true, std::memory_order_relaxed); return; } + } + } + }); + if (cancel_requested.load() || cancelled()) return false; + } + if (adaptive_cluster) { + if (!discard_unused_cluster_centers(cluster_centers, clustered_face_labels, "cluster assignment")) + return false; + } else { + ensure_all_cluster_centers_used(face_colors, cluster_centers, clustered_face_labels, "cluster assignment"); + } + +#ifdef OUTPUT_TEST_RESULT + { + std::vector tmp = out_clustered_face_colors; + for (std::size_t i = 0; i < tmp.size(); ++i) + tmp[i] = cluster_centers[clustered_face_labels[i]]; + SaveToOFF(std::string(log_prefix) + "_3_cluster.off", mesh, tmp); + } +#endif + + report(65, "Smoothing colors"); + if (cancelled()) return false; + + SmoothParameters smooth_parameters; + smooth_parameters.smooth_weight = settings.smooth_weight; + if (!smooth_region(mesh, clustered_face_labels, smooth_parameters)) { + BOOST_LOG_TRIVIAL(debug) << log_prefix << ": smooth region failed."; + return false; + } + if (adaptive_cluster) { + if (!discard_unused_cluster_centers(cluster_centers, clustered_face_labels, "color smoothing")) + return false; + } else { + ensure_all_cluster_centers_used(face_colors, cluster_centers, clustered_face_labels, "color smoothing"); + } + + report(90, "Updating face colors"); + if (cancelled()) return false; + + for (std::size_t i = 0; i < out_clustered_face_colors.size(); ++i) + out_clustered_face_colors[i] = cluster_centers[clustered_face_labels[i]]; + +#ifdef OUTPUT_TEST_RESULT + SaveToOFF(std::string(log_prefix) + "_4_smooth.off", mesh, out_clustered_face_colors); +#endif + + report(100, "Completed"); + return true; +} + bool TextureToColor(const TriMesh& texture_mesh, const std::vector>& texture_mesh_uv_coords, const cv::Mat& texture, TriMesh& color_mesh, std::vector>& face_colors, const TextureToColorSettings& settings, AlgoProgressCallback progress_callback, AlgoCancelCallback cancel_callback) { @@ -525,259 +931,22 @@ bool TextureToColor(const TriMesh& texture_mesh, const std::vector clustered_face_colors; + if (!repair_cluster_smooth(color_mesh, face_colors, clustered_face_colors, + settings, rcs_progress, cancel_callback, "TextureToColor")) return false; - } - - // Sub-stage timing helper for the "Repairing mesh" outer lap. Logs each - // sub-phase under a [timing][Repairing mesh] prefix so that regressions in - // mesh inspection, RepairMesh, AABB resampling, etc. can be attributed - // to a specific sub-stage without changing the outer lap structure. - auto sub_lap = [&](const char* sub_name, Clock::time_point t0) { - double ms = std::chrono::duration(Clock::now() - t0).count(); - BOOST_LOG_TRIVIAL(debug) << "[timing][Repairing mesh] " << sub_name << ": " << ms << "ms"; - }; - - // Step 3: Repair mesh - // Many textured models have non-manifold, non-closed, or other issues that need to be fixed beforehand - auto resample_repaired_mesh = [&](TriMesh&& repaired_mesh) -> bool { - // AABBTreeIndirect references vertices/faces externally, so snapshot the - // pre-repair geometry by moving them out of color_mesh before it gets - // overwritten with the repaired mesh below. std::move on std::vector is - // O(1) (pointer adoption), no element copy. - const auto t_aabb = Clock::now(); - TriVertices old_vertices = std::move(color_mesh.vertices); - TriFaces old_indices = std::move(color_mesh.indices); - auto before_repair_tree = AABBTreeIndirect::build_aabb_tree_over_indexed_triangle_set(old_vertices, old_indices); - sub_lap("resample.aabb_build", t_aabb); - - color_mesh = std::move(repaired_mesh); - - const auto t_is_closed = Clock::now(); - if (is_closed(color_mesh)) { - BOOST_LOG_TRIVIAL(debug) << "TextureToColor: repaired mesh is closed."; - } else { - BOOST_LOG_TRIVIAL(debug) << "TextureToColor: repaired mesh is open."; - } - sub_lap("resample.is_closed", t_is_closed); - - // New faces after repair inherit old face colors via centroid nearest-neighbor lookup. - // Since the mesh barely changes after repair, resampling via centroid nearest-neighbor is sufficient. - const auto t_resample = Clock::now(); - std::vector new_face_colors(color_mesh.facets_count()); - tbb::parallel_for(tbb::blocked_range(0, color_mesh.facets_count()), [&](const tbb::blocked_range& range) { - for (std::size_t fid = range.begin(); fid < range.end(); ++fid) { - const auto& face = color_mesh.indices[fid]; - Vec3f center = (color_mesh.vertices[face[0]] + color_mesh.vertices[face[1]] + color_mesh.vertices[face[2]]) / 3.0f; - size_t hit_idx = 0; - Vec3f closest; - AABBTreeIndirect::squared_distance_to_indexed_triangle_set( - old_vertices, old_indices, before_repair_tree, center, hit_idx, closest); - new_face_colors[fid] = face_colors[hit_idx]; - } - }); - face_colors = std::move(new_face_colors); - sub_lap("resample.parallel_nearest", t_resample); - return true; - }; - - auto repair_and_resample_mesh = [&]() -> bool { - std::shared_ptr repaired_mesh; - const auto t_repair = Clock::now(); - bool success = RepairMesh(color_mesh, repaired_mesh); - sub_lap("RepairMesh", t_repair); - if (success == false) { - BOOST_LOG_TRIVIAL(debug) << "TextureToColor: repair mesh failed."; - return false; - } - if (cancelled()) return false; - return resample_repaired_mesh(std::move(*repaired_mesh)); - }; - - { - const auto t_stats = Clock::now(); - TriangleMesh stats_mesh(static_cast(color_mesh)); - const auto& stats = stats_mesh.stats(); - sub_lap("stats_check", t_stats); - // Orca's TriangleMeshStats defines manifold() as open_edges == 0 and does not track - // non-manifold edges/vertices separately, so BBS's "!manifold() || has_open_edges()" - // collapses to this single test and the extra counters drop out of the log. - if (!stats.manifold()) { - BOOST_LOG_TRIVIAL(info) << "TextureToColor: mesh has non-manifold geometry or open boundaries, open_edges=" - << stats.open_edges; - if (settings.mesh_repair_decision == MeshRepairDecision::Ask) { - if (settings.mesh_repair_decision_required) - *settings.mesh_repair_decision_required = true; - return false; - } - if (settings.mesh_repair_decision == MeshRepairDecision::RepairAndImport) { - indexed_triangle_set repaired_its; - std::string repair_error; - const auto t_win3d = Clock::now(); - bool repaired = settings.mesh_repair_callback && settings.mesh_repair_callback(static_cast(color_mesh), repaired_its, - [&](const char* message, unsigned percent) { - sub_report(static_cast(percent), 40, 60, message ? message : "Repairing mesh"); - }, - [&]() { return cancelled(); }, &repair_error); - sub_lap("windows_3d_repair", t_win3d); - if (repaired) { - if (cancelled()) return false; - BOOST_LOG_TRIVIAL(info) << "TextureToColor: Windows 3D mesh repair finished."; - if (!resample_repaired_mesh(TriMesh(std::move(repaired_its)))) - return false; - } else { - BOOST_LOG_TRIVIAL(warning) << "TextureToColor: Windows 3D mesh repair failed: " << repair_error; - } - } else { - BOOST_LOG_TRIVIAL(info) << "TextureToColor: importing mesh without Windows 3D repair."; - } - } - } - - const auto t_halfedge = Clock::now(); - const bool halfedge_ok = cgalutils::is_mesh_halfedge_compatible(color_mesh); - sub_lap("is_mesh_halfedge_compatible", t_halfedge); - if (!halfedge_ok && repair_and_resample_mesh() == false) { - BOOST_LOG_TRIVIAL(debug) << "TextureToColor: repair and resample mesh failed."; - return false; - } - lap("Repairing mesh"); -#ifdef OUTPUT_TEST_RESULT - SaveToOFF("texture_to_color_1_repair.off", color_mesh, face_colors); -#endif - - report(65, "Color clustering"); - if (cancelled()) { - return false; - } - - // Step 5: Color clustering - std::vector cluster_centers; - std::vector clustered_face_colors = face_colors; - std::vector clustered_face_labels(face_colors.size()); - const bool adaptive_cluster = settings.target_colors_num == 0; - - // Compute cluster centers - if (adaptive_cluster) { - BOOST_LOG_TRIVIAL(debug) << "TextureToColor: use cluster adaptive method."; - ClusterParameters para; - para.max_color_distance = settings.max_color_distance; - para.max_cluster_k = settings.max_cluster_k; - para.cancel_callback = cancel_callback ? [&]() { return cancel_callback(); } : std::function{}; - cluster_centers = cluster_adaptive(face_colors, para); - if (cancelled()) return false; - } else { - BOOST_LOG_TRIVIAL(debug) << "TextureToColor: use cluster k-means method."; - ClusterParameters para; - para.cluster_k = settings.target_colors_num; - para.cancel_callback = cancel_callback ? [&]() { return cancel_callback(); } : std::function{}; - cluster_centers = cluster_k_means(face_colors, para); - if (cancelled()) return false; - } - BOOST_LOG_TRIVIAL(debug) << "TextureToColor: the k is " << cluster_centers.size() << "."; - if (cluster_centers.empty()) { - BOOST_LOG_TRIVIAL(debug) << "TextureToColor: no cluster center generated."; - return false; - } - const std::set unique_cluster_centers(cluster_centers.begin(), cluster_centers.end()); - if (unique_cluster_centers.size() != cluster_centers.size()) { - BOOST_LOG_TRIVIAL(warning) << "TextureToColor: cluster centers contain duplicated RGB values, unique exported colors may be fewer than centers."; - } - - report(70, "Assigning cluster labels"); - if (cancelled()) { - return false; - } - - // Assign each face's color to the nearest cluster center - constexpr bool use_simple_cluster = true; // Complex algorithm is still being optimized; use simple assignment for now - if (use_simple_cluster) { - std::atomic done_cluster{0}; - std::atomic cancel_requested{false}; - const size_t total_cluster = color_mesh.indices.size(); - const size_t cluster_interval = std::max(total_cluster / 20, 1); - tbb::parallel_for(tbb::blocked_range(0, total_cluster), [&](const tbb::blocked_range& range) { - for (std::size_t fid = range.begin(); fid < range.end(); ++fid) { - if (cancel_requested.load(std::memory_order_relaxed)) return; - auto& face_color = face_colors[fid]; - auto nearest_color_id = std::numeric_limits::max(); - bool success = calc_nearest_color_id(cluster_centers, face_color, nearest_color_id); - if (success == false) { - BOOST_LOG_TRIVIAL(debug) << "TextureToColor: calc nearest color id failed."; - continue; - } - clustered_face_labels[fid] = nearest_color_id; - clustered_face_colors[fid] = cluster_centers[nearest_color_id]; - size_t cnt = done_cluster.fetch_add(1, std::memory_order_relaxed) + 1; - if (cnt % cluster_interval == 0) { - if (cancelled()) { cancel_requested.store(true, std::memory_order_relaxed); return; } - sub_report(static_cast(cnt * 100 / total_cluster), 70, 85, "Assigning cluster labels"); - } - } - }); - if (cancel_requested.load() || cancelled()) return false; - } else { - bool success = mesh_cluster(color_mesh, cluster_centers, clustered_face_colors, clustered_face_labels); - if (success == false) { - BOOST_LOG_TRIVIAL(debug) << "TextureToColor: mesh cluster failed."; - return false; - } - } - if (adaptive_cluster) { - if (!discard_unused_cluster_centers(cluster_centers, clustered_face_labels, "cluster assignment")) { - return false; - } - } else { - ensure_all_cluster_centers_used(face_colors, cluster_centers, clustered_face_labels, "cluster assignment"); - } - lap("Color clustering & labeling"); -#ifdef OUTPUT_TEST_RESULT - for (std::size_t i = 0; i < clustered_face_colors.size(); ++i) { - clustered_face_colors[i] = cluster_centers[clustered_face_labels[i]]; - } - SaveToOFF("texture_to_color_3_cluster.off", color_mesh, clustered_face_colors); -#endif - - report(85, "Smoothing colors"); - if (cancelled()) { - return false; - } - - // Step 6: Post-process colors - SmoothParameters smooth_parameters; - smooth_parameters.smooth_weight = settings.smooth_weight; - if (!smooth_region(color_mesh, clustered_face_labels, smooth_parameters)) { - BOOST_LOG_TRIVIAL(debug) << "TextureToColor: smooth region failed."; - return false; - } - BOOST_LOG_TRIVIAL(debug) << "TextureToColor: smooth region success."; - if (adaptive_cluster) { - if (!discard_unused_cluster_centers(cluster_centers, clustered_face_labels, "color smoothing")) { - return false; - } - } else { - ensure_all_cluster_centers_used(face_colors, cluster_centers, clustered_face_labels, "color smoothing"); - } - report(95, "Updating face colors"); - if (cancelled()) { - return false; - } - for (std::size_t i = 0; i < clustered_face_colors.size(); ++i) { - clustered_face_colors[i] = cluster_centers[clustered_face_labels[i]]; - } - const std::set unique_exported_colors(clustered_face_colors.begin(), clustered_face_colors.end()); - if (unique_exported_colors.size() < cluster_centers.size()) { - BOOST_LOG_TRIVIAL(warning) << "TextureToColor: final exported unique colors (" << unique_exported_colors.size() - << ") are fewer than cluster centers (" << cluster_centers.size() - << "), likely due to duplicate centers or unsatisfied seed assignment."; - } -#ifdef OUTPUT_TEST_RESULT - SaveToOFF("texture_to_color_4_smooth.off", color_mesh, clustered_face_colors); -#endif face_colors = std::move(clustered_face_colors); - lap("Smoothing colors"); + lap("Repair + Clustering + Smoothing"); double total_ms = std::chrono::duration(Clock::now() - t_total_start).count(); BOOST_LOG_TRIVIAL(debug) << "[timing] TextureToColor total: " << total_ms << "ms" << " faces=" << color_mesh.facets_count(); @@ -785,5 +954,91 @@ bool TextureToColor(const TriMesh& texture_mesh, const std::vector>& input_face_colors, + TriMesh& out_mesh, + std::vector>& out_face_colors, + const TextureToColorSettings& settings, + AlgoProgressCallback progress_callback, + AlgoCancelCallback cancel_callback, + const std::vector>& vertex_colors) +{ + auto report = [&](int pct, const char* msg) { + if (progress_callback) + progress_callback({pct, msg}); + }; + auto cancelled = [&]() -> bool { + if (cancel_callback && cancel_callback()) { + BOOST_LOG_TRIVIAL(debug) << __FUNCTION__ << " cancelled"; + return true; + } + return false; + }; + + out_mesh = mesh; + out_face_colors.clear(); + + if (mesh.indices.empty() || input_face_colors.empty()) { + BOOST_LOG_TRIVIAL(debug) << "ClusterAndSmooth: empty mesh or face colors."; + return false; + } + if (input_face_colors.size() != mesh.indices.size()) { + BOOST_LOG_TRIVIAL(warning) << "ClusterAndSmooth: face_colors size (" + << input_face_colors.size() << ") != indices size (" + << mesh.indices.size() << "), clamping."; + } + + report(0, "Initializing"); + if (cancelled()) return false; + + // Prepare face colors aligned to mesh size + std::vector face_colors(out_mesh.indices.size()); + for (size_t i = 0; i < out_mesh.indices.size(); ++i) { + if (i < input_face_colors.size()) + face_colors[i] = input_face_colors[i]; + else + face_colors[i] = {128, 128, 128}; + } + + // Low-poly vertex-color meshes take the legacy OBJ import route: quantize the + // vertex colors, then split only across cluster boundaries. Colors are exact + // cluster centers afterwards, so repair / re-clustering / smoothing are skipped + // to match the legacy behaviour, which never touched the mesh either. + // A vertex color count that disagrees with the mesh falls through to the generic + // pipeline below rather than failing the import outright. + if (!vertex_colors.empty() && + vertex_colors.size() == out_mesh.vertices.size() && + out_mesh.facets_count() < settings.oversampling_min_face_count) { + report(10, "Quantizing vertex colors"); + std::vector cluster_centers; + std::vector vertex_cluster_ids; + if (!quantize_vertex_colors(vertex_colors, settings, cancel_callback, cluster_centers, vertex_cluster_ids)) { + BOOST_LOG_TRIVIAL(debug) << "ClusterAndSmooth: vertex color quantization failed."; + return false; + } + if (cancelled()) return false; + + report(50, "Splitting color boundaries"); + if (!adaptive_split_by_vertex_clusters(out_mesh, vertex_cluster_ids, cluster_centers, face_colors)) { + BOOST_LOG_TRIVIAL(debug) << "ClusterAndSmooth: adaptive vertex-color split failed."; + return false; + } + if (cancelled()) return false; + + out_face_colors = std::move(face_colors); + report(100, "Completed"); + return true; + } + + std::vector clustered_face_colors; + if (!repair_cluster_smooth(out_mesh, face_colors, clustered_face_colors, + settings, progress_callback, cancel_callback, + "ClusterAndSmooth")) + return false; + + out_face_colors = std::move(clustered_face_colors); + return true; +} + } // namespace tex2color } // namespace Slic3r diff --git a/src/libslic3r/TextureToColor/TextureToColor.hpp b/src/libslic3r/TextureToColor/TextureToColor.hpp index f18cce5759..019a113fc4 100644 --- a/src/libslic3r/TextureToColor/TextureToColor.hpp +++ b/src/libslic3r/TextureToColor/TextureToColor.hpp @@ -61,5 +61,43 @@ bool TextureToColor(const TriMesh& texture_mesh, const std::vector>& face_colors, const TextureToColorSettings& settings = TextureToColorSettings(), AlgoProgressCallback progress_callback = nullptr, AlgoCancelCallback cancel_callback = nullptr); +/** + * @brief Turn pre-computed per-face colors into a clustered color mesh (no texture/UV). + * + * Used for OBJ vertex colors and MTL face colors, which bypass texture sampling. + * Two routes are possible: + * - Low-poly meshes carrying per-vertex colors: the vertex colors are quantized + * into a small palette and the mesh is geometrically split along cluster + * boundaries, reproducing the split topology of the legacy OBJ vertex-color + * import. Output colors are then exact cluster centers, so mesh repair, + * re-clustering and smoothing are skipped. + * - Everything else: mesh repair, color clustering (K-Means or adaptive) and + * region smoothing, sharing the same pipeline as TextureToColor. + * + * @param[in] mesh Input triangle mesh + * @param[in] input_face_colors Pre-computed per-face RGB colors [0..255] + * @param[out] out_mesh Output mesh. Geometry is subdivided on the + * vertex-color route, and may still be replaced + * by mesh repair on the generic route. + * @param[out] out_face_colors Output per-face colors, one entry per out_mesh face + * @param[in] settings Algorithm parameters (target_colors_num, smooth_weight; + * oversampling_min_face_count doubles as the low-poly + * threshold for the vertex-color route) + * @param[in] progress_callback Progress callback + * @param[in] cancel_callback Cancel callback + * @param[in] vertex_colors Optional per-vertex RGBA [0..1]. Must match + * mesh.vertices in size to enable the vertex-color + * route; otherwise it is ignored. + * @return true on success, false on failure or cancellation + */ +bool ClusterAndSmooth(const TriMesh& mesh, + const std::vector>& input_face_colors, + TriMesh& out_mesh, + std::vector>& out_face_colors, + const TextureToColorSettings& settings = TextureToColorSettings(), + AlgoProgressCallback progress_callback = nullptr, + AlgoCancelCallback cancel_callback = nullptr, + const std::vector>& vertex_colors = {}); + } // namespace tex2color } // namespace Slic3r diff --git a/src/slic3r/GUI/Plater.cpp b/src/slic3r/GUI/Plater.cpp index 9a6b52016f..29e6dab244 100644 --- a/src/slic3r/GUI/Plater.cpp +++ b/src/slic3r/GUI/Plater.cpp @@ -215,6 +215,9 @@ static bool has_importable_texture(const Slic3r::TexturedMesh& textured_mesh) if (textured_mesh.vertices.empty() || textured_mesh.indices.empty()) return false; + if (!textured_mesh.precomputed_face_colors.empty()) + return true; + return std::any_of(textured_mesh.textures.begin(), textured_mesh.textures.end(), [](const Slic3r::TextureImage& texture) { return !texture.data.empty(); }); } diff --git a/src/slic3r/GUI/TextureImportDialog.cpp b/src/slic3r/GUI/TextureImportDialog.cpp index c9e5fb2147..e2886687f3 100644 --- a/src/slic3r/GUI/TextureImportDialog.cpp +++ b/src/slic3r/GUI/TextureImportDialog.cpp @@ -491,7 +491,8 @@ public: std::function on_add_filament, std::function on_decompose_color, std::function can_add_filament, - std::function on_close) + std::function on_close, + std::vector display_numbers) : PopupWindow(parent, wxBORDER_NONE | wxPU_CONTAINS_CONTROLS) , m_entries(entries) , m_colors_rgba(colors_rgba) @@ -503,6 +504,7 @@ public: , m_on_decompose_color(std::move(on_decompose_color)) , m_can_add_filament(std::move(can_add_filament)) , m_on_close(std::move(on_close)) + , m_display_numbers(std::move(display_numbers)) { wxColour pop_bg = dark_or(*wxWHITE, wxColour(0x2D, 0x2D, 0x31)); SetBackgroundColour(pop_bg); @@ -550,9 +552,12 @@ public: } }; + // Section order matches compute_display_numbers() so the visible IDs + // ascend monotonically (ExistingPhysical -> NewPhysical -> ExistingMixed + // -> NewMixed) instead of jumping (e.g. 1,2 -> 7 -> 3,4,5,6 -> 8,9,10). add_section(_L("Project Physical Filaments"), TextureFilamentKind::ExistingPhysical); - add_section(_L("Project Mixed Filaments"), TextureFilamentKind::ExistingMixed); add_section(_L("New Physical Filaments"), TextureFilamentKind::NewPhysical); + add_section(_L("Project Mixed Filaments"), TextureFilamentKind::ExistingMixed); add_section(_L("New Mixed Filaments"), TextureFilamentKind::NewMixed); auto* decompose_label = new wxStaticText(this, wxID_ANY, _L("Decompose Color")); @@ -682,7 +687,7 @@ private: : wxColour(128, 128, 128); wxString name_str = (idx < m_names.size()) ? filament_name_to_wx_string(m_names[idx]) - : wxString::Format("Filament %d", (int)(idx + 1)); + : wxString::Format("Filament %d", display_number((int)idx)); row->SetToolTip(name_str); row->Bind(wxEVT_PAINT, [this, idx, sq, sq_r, sq_x, gap1, fil_clr, name_str, row_bg, hover_bg, name_fg](wxPaintEvent& e) { @@ -711,7 +716,7 @@ private: nf.SetPointSize(9); dc.SetFont(nf); dc.SetTextForeground(paint_clr.GetLuminance() < 0.6 ? *wxWHITE : texture_import_gray9000()); - wxString ns = wxString::Format("%d", (int)(idx + 1)); + wxString ns = wxString::Format("%d", display_number((int)idx)); wxSize tsz = dc.GetTextExtent(ns); dc.DrawText(ns, sq_x + (sq - tsz.x) / 2, sq_y + (sq - tsz.y) / 2); } @@ -767,7 +772,7 @@ private: row->SetBackgroundColour(row_bg); row->SetBackgroundStyle(wxBG_STYLE_PAINT); row->SetCursor(wxCursor(wxCURSOR_HAND)); - row->SetToolTip(entry.name.empty() ? wxString::Format("Filament %d", idx + 1) : filament_name_to_wx_string(entry.name)); + row->SetToolTip(entry.name.empty() ? wxString::Format("Filament %d", display_number(idx)) : filament_name_to_wx_string(entry.name)); row->Bind(wxEVT_PAINT, [this, entry, idx, row_bg, hover_bg, name_fg, plus_fg](wxPaintEvent& e) { auto* p = static_cast(e.GetEventObject()); @@ -810,7 +815,7 @@ private: dc.DrawRoundedRectangle(x, y, sw, sw, sw_r); draw_filament_swatch_border(dc, comp_clr, x, y, sw, sw, sw_r); - wxString num = wxString::Format("%u", comp_id); + wxString num = wxString::Format("%d", display_number(comp_dialog_idx)); wxSize nsz = dc.GetTextExtent(num); dc.SetTextForeground(comp_clr.GetLuminance() < 0.6 ? *wxWHITE : texture_import_gray9000()); dc.DrawText(num, x + (sw - nsz.x) / 2, y + (sw - nsz.y) / 2); @@ -858,9 +863,19 @@ private: std::function m_on_decompose_color; std::function m_can_add_filament; std::function m_on_close; + // 1-based display number per dialog_index, mirroring the post-apply + // sidebar ordering (ExistingPhysical, NewPhysical, ExistingMixed, NewMixed). + std::vector m_display_numbers; int m_hover_idx = -1; bool m_closing_from_action = false; bool m_destroy_scheduled = false; + + // Returns the display number for a dialog_index, falling back to idx + 1 + // when no mapping is available (e.g. index out of range). + int display_number(int idx) const { + return (idx >= 0 && idx < (int)m_display_numbers.size() && m_display_numbers[idx] > 0) + ? m_display_numbers[idx] : idx + 1; + } }; // ============================================================ @@ -1735,8 +1750,11 @@ TextureImportDialog::TextureImportDialog( m_preview_canvas->set_mesh_data(m_textured_mesh.vertices, m_textured_mesh.indices); - // Prepare texture rendering data for the Original tab - if (!m_textured_mesh.textures.empty()) { + // Pre-computed face colors (OBJ vertex colors / MTL face colors): + // use them directly as the Original preview, skip texture decode. + if (!m_textured_mesh.precomputed_face_colors.empty()) { + m_preview_canvas->set_original_face_colors(m_textured_mesh.precomputed_face_colors); + } else if (!m_textured_mesh.textures.empty()) { std::vector> tex_pixels_rgb; std::vector tex_widths, tex_heights; tex_pixels_rgb.reserve(m_textured_mesh.textures.size()); @@ -2444,7 +2462,13 @@ void TextureImportDialog::start_computation(bool auto_color, bool initial) auto worker_settings = settings; bool mesh_repair_decision_required = false; worker_settings.mesh_repair_decision_required = &mesh_repair_decision_required; - bool ok = Slic3r::texture_to_painting(mesh_copy, result, worker_settings, progress_cb, cancel_cb); + bool ok; + if (!mesh_copy.precomputed_face_colors.empty()) { + ok = Slic3r::face_colors_to_painting( + mesh_copy, result, worker_settings, progress_cb, cancel_cb); + } else { + ok = Slic3r::texture_to_painting(mesh_copy, result, worker_settings, progress_cb, cancel_cb); + } if (m_cancel_flag.load()) { wxQueueEvent(handler, new wxCommandEvent(EVT_TEXTURE_COMPUTE_ERROR)); @@ -3075,6 +3099,54 @@ void TextureImportDialog::compact_used_virtual_filaments() } } +std::vector TextureImportDialog::compute_display_numbers() const +{ + // Assigns each entry a 1-based display number in the order the sidebar will + // show after apply: ExistingPhysical, NewPhysical, ExistingMixed, NewMixed. + // This keeps the dialog's visible IDs in sync with the post-apply sidebar, + // instead of the raw dialog_index (which interleaves physicals and mixeds + // by processing order and causes e.g. CMYW to show 4,5,6,8 instead of 3,4,5,6). + // MUST mirror ordering in apply_textured_mesh_import_result (Plater.cpp:9896): + // - ExistingPhysical keeps its project_config_index + // - NewPhysical is inserted at existing_physical_count + new_order + // - ExistingMixed shifts to project_config_index + new_physical_count + // - NewMixed is appended after all existing mixeds + std::vector result(m_filament_entries.size(), 0); + int next = 1; + + auto assign_group = [&](TextureFilamentKind kind, bool by_project_config_index) { + if (by_project_config_index) { + std::vector group; + for (const auto& e : m_filament_entries) + if (e.kind == kind) + group.push_back(&e); + std::sort(group.begin(), group.end(), + [](const TextureFilamentEntry* a, const TextureFilamentEntry* b) { + return a->project_config_index < b->project_config_index; + }); + for (const auto* e : group) { + if (e->dialog_index >= 0 && e->dialog_index < (int)result.size()) + result[e->dialog_index] = next; + ++next; + } + } else { + for (const auto& e : m_filament_entries) { + if (e.kind != kind) + continue; + if (e.dialog_index >= 0 && e.dialog_index < (int)result.size()) + result[e.dialog_index] = next; + ++next; + } + } + }; + + assign_group(TextureFilamentKind::ExistingPhysical, true); + assign_group(TextureFilamentKind::NewPhysical, false); + assign_group(TextureFilamentKind::ExistingMixed, true); + assign_group(TextureFilamentKind::NewMixed, false); + return result; +} + void TextureImportDialog::dismiss_filament_popup() { if (!m_filament_popup) { @@ -3429,7 +3501,13 @@ void TextureImportDialog::show_filament_popup(size_t row_index) dismiss_filament_popup(); } - auto on_select = [this, row_index](int idx) { + const auto display_numbers = compute_display_numbers(); + auto display_number = [display_numbers](int idx) -> int { + return (idx >= 0 && idx < (int)display_numbers.size() && display_numbers[idx] > 0) + ? display_numbers[idx] : idx + 1; + }; + + auto on_select = [this, row_index, display_number](int idx) { if (row_index >= m_mapping_rows.size()) return; m_mapping_rows[row_index].target_filament_idx = idx; if (row_index < m_current_matches.size()) @@ -3437,7 +3515,7 @@ void TextureImportDialog::show_filament_popup(size_t row_index) if (m_mapping_rows[row_index].target_panel) { wxString label = (idx >= 0 && idx < (int)m_filament_names.size()) ? filament_name_to_wx_string(m_filament_names[idx]) - : wxString::Format("Filament %d", idx + 1); + : wxString::Format("Filament %d", display_number(idx)); m_mapping_rows[row_index].target_panel->SetToolTip(label); m_mapping_rows[row_index].target_panel->Refresh(); } @@ -3490,7 +3568,8 @@ void TextureImportDialog::show_filament_popup(size_t row_index) m_existing_filament_count, tp->GetSize().x, tp, on_select, on_add_filament, on_decompose_color, [this]() { return can_add_virtual_filament(); }, - on_close); + on_close, + display_numbers); wxPoint pos = tp->ClientToScreen(wxPoint(0, tp->GetSize().y)); wxRect display_rect; @@ -3673,10 +3752,16 @@ void TextureImportDialog::rebuild_mapping_rows() return wxColour(128, 128, 128); }; - auto get_filament_label = [this](int idx) -> wxString { + const auto display_numbers = compute_display_numbers(); + auto display_number = [display_numbers](int idx) -> int { + return (idx >= 0 && idx < (int)display_numbers.size() && display_numbers[idx] > 0) + ? display_numbers[idx] : idx + 1; + }; + + auto get_filament_label = [this, display_number](int idx) -> wxString { if (idx >= 0 && idx < (int)m_filament_names.size()) return filament_name_to_wx_string(m_filament_names[idx]); - return wxString::Format("Filament %d", idx + 1); + return wxString::Format("Filament %d", display_number(idx)); }; const wxColour dash_clr = dark_or(wxColour(179, 179, 179), wxColour(100, 100, 106)); @@ -3804,7 +3889,7 @@ void TextureImportDialog::rebuild_mapping_rows() row.target_panel->SetCursor(wxCursor(wxCURSOR_HAND)); row.target_panel->Bind(wxEVT_PAINT, [this, ci, get_target_wxcolor, get_filament_label, - card_bg, card_bd, name_fg, chev_clr](wxPaintEvent& e) { + display_number, card_bg, card_bd, name_fg, chev_clr](wxPaintEvent& e) { auto* p = static_cast(e.GetEventObject()); wxAutoBufferedPaintDC dc(p); wxSize sz = p->GetClientSize(); @@ -3856,7 +3941,7 @@ void TextureImportDialog::rebuild_mapping_rows() dc.DrawRoundedRectangle(x, sw_y, sw, sw, sw_r); draw_filament_swatch_border(dc, comp_clr, x, sw_y, sw, sw, sw_r); - wxString num_str = wxString::Format("%u", comp_id); + wxString num_str = wxString::Format("%d", display_number(comp_idx)); wxSize nsz = dc.GetTextExtent(num_str); dc.SetTextForeground(comp_clr.GetLuminance() < 0.6 ? *wxWHITE : texture_import_gray9000()); dc.DrawText(num_str, x + (sw - nsz.x) / 2, sw_y + (sw - nsz.y) / 2); @@ -3897,7 +3982,7 @@ void TextureImportDialog::rebuild_mapping_rows() num_font.SetPointSize(10); dc.SetFont(num_font); dc.SetTextForeground(fil_clr.GetLuminance() < 0.6 ? *wxWHITE : texture_import_gray9000()); - wxString num_str = wxString::Format("%d", fil_idx + 1); + wxString num_str = wxString::Format("%d", display_number(fil_idx)); wxSize nsz = dc.GetTextExtent(num_str); dc.DrawText(num_str, sq_x + (sq - nsz.x) / 2, sq_y + (sq - nsz.y) / 2); } diff --git a/src/slic3r/GUI/TextureImportDialog.hpp b/src/slic3r/GUI/TextureImportDialog.hpp index 63e30dfc5b..3d7ba43c31 100644 --- a/src/slic3r/GUI/TextureImportDialog.hpp +++ b/src/slic3r/GUI/TextureImportDialog.hpp @@ -275,6 +275,14 @@ private: void update_drop_warning_visibility(); void compact_used_virtual_filaments(); int find_closest_filament_index(const std::array& color) const; + // Returns a vector indexed by dialog_index whose value is the 1-based + // display number that mirrors the final sidebar ordering produced by + // apply_textured_mesh_import_result (Plater.cpp): ExistingPhysical, + // NewPhysical, ExistingMixed, NewMixed. Used so the dialog shows the + // same IDs the sidebar will show after OK, instead of the raw + // dialog_index + 1 (which interleaves physicals and mixeds). + // MUST mirror ordering in apply_textured_mesh_import_result (Plater.cpp:9896). + std::vector compute_display_numbers() const; void on_color_preset_clicked(wxCommandEvent& evt); void on_color_slider_changed(wxCommandEvent& evt);