Port colored OBJ import pipeline from BambuStudio

This commit is contained in:
SoftFever
2026-08-22 13:15:09 +08:00
parent 94a1cd6c93
commit b1e3cdc666
9 changed files with 893 additions and 297 deletions

View File

@@ -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<size_t>(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;
}

View File

@@ -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<TexturedMesh>();
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") ||

View File

@@ -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<std::array<std::size_t,3>> 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<std::size_t,3>& rgb1,
const std::array<float,4>& rgba2)

View File

@@ -38,6 +38,18 @@ struct TexturedMesh {
std::vector<std::array<int,3>> 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<std::array<std::size_t,3>> 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<std::array<float,4>> 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<FilamentMatch> match_clusters_to_filaments(
const std::vector<std::array<std::size_t,3>>& cluster_colors,

View File

@@ -7,7 +7,6 @@
#include <cmath>
#include <cstdint>
#include <functional>
#include <set>
#include <boost/next_prior.hpp>
#include "CgalUtils.hpp"
@@ -379,6 +378,413 @@ static bool linear_subdivision(TriMesh& mesh, std::vector<FaceUVArray>& uv_coord
return true;
}
using VertexColor = std::array<float, 4>;
// 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<VertexColor>& vertex_colors,
const TextureToColorSettings& settings,
AlgoCancelCallback cancel_callback,
std::vector<RGB>& out_centers,
std::vector<std::size_t>& out_vertex_cluster_ids)
{
out_centers.clear();
out_vertex_cluster_ids.clear();
if (vertex_colors.empty())
return false;
std::vector<RGB> 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<std::size_t>(v);
}
}
ClusterParameters para;
para.cancel_callback = cancel_callback ? [&]() { return cancel_callback(); } : std::function<bool()>{};
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<std::size_t>& vertex_cluster_ids,
const std::vector<RGB>& cluster_centers,
std::vector<RGB>& 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<uint64_t>(a) << 32) | b)
: ((static_cast<uint64_t>(b) << 32) | a);
};
std::unordered_map<uint64_t, std::size_t> 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<int>(a), static_cast<int>(b), static_cast<int>(c)));
out_face_colors.push_back(cluster_centers[cluster_id]);
};
for (const auto& f : original_faces) {
const std::size_t v[3] = {static_cast<std::size_t>(f[0]), static_cast<std::size_t>(f[1]), static_cast<std::size_t>(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<RGB>& face_colors,
std::vector<RGB>& 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<RGB> new_face_colors(mesh.facets_count());
tbb::parallel_for(tbb::blocked_range<std::size_t>(0, mesh.facets_count()), [&](const tbb::blocked_range<size_t>& 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<TriMesh> 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<const indexed_triangle_set&>(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<const indexed_triangle_set&>(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<RGB> cluster_centers;
out_clustered_face_colors = face_colors;
std::vector<std::size_t> 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<bool()>{};
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<bool()>{};
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<size_t> done{0};
std::atomic<bool> cancel_requested{false};
const size_t total = mesh.indices.size();
const size_t interval = std::max<size_t>(total / 20, 1);
tbb::parallel_for(tbb::blocked_range<std::size_t>(0, total), [&](const tbb::blocked_range<size_t>& 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<RGB> 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<std::vector<Vec2f>>& texture_mesh_uv_coords, const cv::Mat& texture, TriMesh& color_mesh,
std::vector<std::array<std::size_t, 3>>& face_colors, const TextureToColorSettings& settings, AlgoProgressCallback progress_callback,
AlgoCancelCallback cancel_callback) {
@@ -525,259 +931,22 @@ bool TextureToColor(const TriMesh& texture_mesh, const std::vector<std::vector<V
SaveToOFF("texture_to_color_0_initialize.off", color_mesh, face_colors);
#endif
report(40, "Repairing mesh");
if (cancelled()) {
// Map progress from repair_cluster_smooth's [0,100] to TextureToColor's [40,100]
AlgoProgressCallback rcs_progress = nullptr;
if (progress_callback) {
rcs_progress = [&](AlgoProgress p) {
int mapped_pct = 40 + p.percent * 60 / 100;
progress_callback({mapped_pct, p.message});
};
}
std::vector<RGB> 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<double, std::milli>(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<RGB> new_face_colors(color_mesh.facets_count());
tbb::parallel_for(tbb::blocked_range<std::size_t>(0, color_mesh.facets_count()), [&](const tbb::blocked_range<size_t>& 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<TriMesh> 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<const indexed_triangle_set&>(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<const indexed_triangle_set&>(color_mesh), repaired_its,
[&](const char* message, unsigned percent) {
sub_report(static_cast<int>(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<RGB> cluster_centers;
std::vector<RGB> clustered_face_colors = face_colors;
std::vector<std::size_t> 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<bool()>{};
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<bool()>{};
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<RGB> 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<size_t> done_cluster{0};
std::atomic<bool> cancel_requested{false};
const size_t total_cluster = color_mesh.indices.size();
const size_t cluster_interval = std::max<size_t>(total_cluster / 20, 1);
tbb::parallel_for(tbb::blocked_range<std::size_t>(0, total_cluster), [&](const tbb::blocked_range<size_t>& 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<std::size_t>::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<int>(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<RGB> 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<double, std::milli>(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<std::vector<V
return true;
}
bool ClusterAndSmooth(const TriMesh& mesh,
const std::vector<std::array<std::size_t, 3>>& input_face_colors,
TriMesh& out_mesh,
std::vector<std::array<std::size_t, 3>>& out_face_colors,
const TextureToColorSettings& settings,
AlgoProgressCallback progress_callback,
AlgoCancelCallback cancel_callback,
const std::vector<std::array<float, 4>>& 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<RGB> 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<RGB> cluster_centers;
std::vector<std::size_t> 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<RGB> 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

View File

@@ -61,5 +61,43 @@ bool TextureToColor(const TriMesh& texture_mesh, const std::vector<std::vector<V
std::vector<std::array<std::size_t, 3>>& 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<std::array<std::size_t, 3>>& input_face_colors,
TriMesh& out_mesh,
std::vector<std::array<std::size_t, 3>>& out_face_colors,
const TextureToColorSettings& settings = TextureToColorSettings(),
AlgoProgressCallback progress_callback = nullptr,
AlgoCancelCallback cancel_callback = nullptr,
const std::vector<std::array<float, 4>>& vertex_colors = {});
} // namespace tex2color
} // namespace Slic3r

View File

@@ -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(); });
}

View File

@@ -491,7 +491,8 @@ public:
std::function<void(wxColour)> on_add_filament,
std::function<void()> on_decompose_color,
std::function<bool()> can_add_filament,
std::function<void(bool)> on_close)
std::function<void(bool)> on_close,
std::vector<int> 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<wxPanel*>(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<void()> m_on_decompose_color;
std::function<bool()> m_can_add_filament;
std::function<void(bool)> m_on_close;
// 1-based display number per dialog_index, mirroring the post-apply
// sidebar ordering (ExistingPhysical, NewPhysical, ExistingMixed, NewMixed).
std::vector<int> 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<std::vector<unsigned char>> tex_pixels_rgb;
std::vector<int> 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<int> 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<int> 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<const TextureFilamentEntry*> 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<wxPanel*>(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);
}

View File

@@ -275,6 +275,14 @@ private:
void update_drop_warning_visibility();
void compact_used_virtual_filaments();
int find_closest_filament_index(const std::array<std::size_t, 3>& 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<int> compute_display_numbers() const;
void on_color_preset_clicked(wxCommandEvent& evt);
void on_color_slider_changed(wxCommandEvent& evt);