From 17317577498d4ee3578040113786d67c102bd5d8 Mon Sep 17 00:00:00 2001 From: ExPikaPaka Date: Mon, 5 Oct 2026 08:38:45 +0200 Subject: [PATCH] Make the connected-net layout affordable on a dense patch Laying a patch out as a connected net cost ~175 ms on a 42k-triangle patch, against ~21 ms for the unwrap it works from, and the gizmo asks for it on every preview, overlay and bake. Measured on a real project the grid behind it ran ~19 million triangle-pair tests per net, nearly all of them misses: a cell holds every triangle whose box touches it, and a candidate really meets a couple of them. Keep a bounding box with each stored triangle and answer those misses with four comparisons instead of a full intersection. The net drops to ~53 ms with identical output - the seam metrics on the test project did not move by one. Two further attempts were measured and dropped, and are recorded in the comment so they are not tried again: a free-space pre-check per chart came out slower, because a folded chart lands against the net by construction and the cells under it are occupied anyway, and splitting the boxes into their own array for locality lost more to growing two vectors per bucket than it gained. Also pick a pair's fold line from the longest boundary they share rather than whichever edge came first, and grow the net strongest-adjacency-first rather than breadth-first by area. Only the fold a chart is reached by comes out matching, so a chart claimed across a short boundary leaves the long one it shared with its true neighbour torn. texture_unwrap_dump reports an unwrap from a saved project - charts, their topology, folded triangles, where the texture is discontinuous and how long those seams are. All of the above was found with it, and it is what keeps a claim about this code honest; reading the 3D view and guessing had produced three wrong diagnoses in a row. --- src/dev-utils/CMakeLists.txt | 6 + src/dev-utils/texture_unwrap_dump.cpp | 292 ++++++++++++++++++++++++++ src/libslic3r/TextureDisplacement.cpp | 165 +++++++++++---- 3 files changed, 425 insertions(+), 38 deletions(-) create mode 100644 src/dev-utils/texture_unwrap_dump.cpp diff --git a/src/dev-utils/CMakeLists.txt b/src/dev-utils/CMakeLists.txt index e718da9e83..19fa1c3ce3 100644 --- a/src/dev-utils/CMakeLists.txt +++ b/src/dev-utils/CMakeLists.txt @@ -28,6 +28,12 @@ if (ORCA_TOOLS) target_link_libraries(generate_system_cache libslic3r boost_headeronly) target_compile_definitions(generate_system_cache PRIVATE ${_DEV_DEFS}) + # texture_unwrap_dump: reports the LSCM unwrap of a saved project's texture displacement layers, + # chart by chart, so a defect can be reproduced from the project file instead of from a screenshot. + add_executable(texture_unwrap_dump texture_unwrap_dump.cpp) + target_link_libraries(texture_unwrap_dump libslic3r boost_headeronly nanosvg) + target_compile_definitions(texture_unwrap_dump PRIVATE ${_DEV_DEFS}) + # profile_include_dump: prints what included templates contribute to a vendor's presets, # to diff against the same tool built in BambuStudio. Built only on request. add_executable(profile_include_dump EXCLUDE_FROM_ALL profile_include_dump.cpp) diff --git a/src/dev-utils/texture_unwrap_dump.cpp b/src/dev-utils/texture_unwrap_dump.cpp new file mode 100644 index 0000000000..5c94f4b12b --- /dev/null +++ b/src/dev-utils/texture_unwrap_dump.cpp @@ -0,0 +1,292 @@ +// Diagnostic for the LSCM unwrap of a texture displacement layer. +// +// It exists because the defect it hunts only shows up on a real painted patch: the paint mask is built +// by TriangleSelector splitting base triangles, so the patch topology cannot be written down by hand, +// and reasoning about it from a screenshot of the 3D view had already produced three wrong diagnoses. +// This loads a saved project, rebuilds exactly the patch the bake would act on, runs the same unwrap, +// and reports what came out - per chart, so a bad one can be pointed at rather than guessed at. +// +// texture_unwrap_dump + +// nanosvg is header-only and libslic3r's 3mf import references it without carrying the implementation, +// so every executable that links libslic3r has to supply it. Must precede any include that pulls the +// header in, or its include guard suppresses the implementation. Same pattern as the other dev tools. +#define NANOSVG_IMPLEMENTATION +#include "nanosvg/nanosvg.h" +#define NANOSVGRAST_IMPLEMENTATION +#include "nanosvg/nanosvgrast.h" + +#include +#include +#include +#include +#include +#include + +#include "libslic3r/Model.hpp" +#include "libslic3r/TextureDisplacement.hpp" +#include "libslic3r/Format/bbs_3mf.hpp" +#include "libslic3r/Utils.hpp" + +#include + +using namespace Slic3r; + +namespace { + +uint64_t edge_key(int a, int b) +{ + if (a > b) + std::swap(a, b); + return (uint64_t(uint32_t(a)) << 32) | uint32_t(b); +} + +// Boundary loops and the Euler characteristic of a face set, which together say whether a chart is the +// topological disk LSCM needs (one loop, V - E + F == 1). +void chart_topology(const indexed_triangle_set &mesh, const std::vector &faces, int &loops, int &euler) +{ + std::unordered_map edge_use; + std::unordered_map local; + for (const int f : faces) { + const stl_triangle_vertex_indices &t = mesh.indices[size_t(f)]; + for (int i = 0; i < 3; ++i) { + ++edge_use[edge_key(t[i], t[(i + 1) % 3])]; + local.emplace(t[i], int(local.size())); + } + } + euler = int(local.size()) - int(edge_use.size()) + int(faces.size()); + + std::unordered_map parent; + const std::function find = [&](int x) { + while (parent[x] != x) + x = parent[x] = parent[parent[x]]; + return x; + }; + for (const auto &[key, uses] : edge_use) + if (uses == 1) + for (const int v : { int(key >> 32), int(uint32_t(key)) }) + parent.emplace(v, v); + for (const auto &[key, uses] : edge_use) + if (uses == 1) { + const int a = find(int(key >> 32)), b = find(int(uint32_t(key))); + if (a != b) + parent[b] = a; + } + std::unordered_map roots; + for (const auto &[v, p] : parent) + roots[find(v)] = 1; + loops = int(roots.size()); +} + +float signed_area_2d(const Vec2f &a, const Vec2f &b, const Vec2f &c) +{ + return 0.5f * ((b.x() - a.x()) * (c.y() - a.y()) - (c.x() - a.x()) * (b.y() - a.y())); +} + +} // namespace + +int main(int argc, char **argv) +{ + if (argc < 2) { + std::printf("usage: texture_unwrap_dump \n"); + return 2; + } + + Model model; + DynamicPrintConfig config; + ConfigSubstitutionContext ctx(ForwardCompatibilitySubstitutionRule::Enable); + PlateDataPtrs plate_data; + std::vector project_presets; + bool is_bbl_3mf = false, is_orca_3mf = false; + Semver file_version; + // The importer writes a backup copy under the data dir and silently loses objects without one. + const boost::filesystem::path tmp = boost::filesystem::temp_directory_path() / "texture_unwrap_dump"; + boost::filesystem::create_directories(tmp); + set_data_dir(tmp.string()); + + // LoadModel so the meshes come through; AddDefaultInstances because an object with no instance is + // dropped by the plate mapping, which is what "skip this object" in the log means. + if (!load_bbs_3mf(argv[1], &config, &ctx, &model, &plate_data, &project_presets, &is_bbl_3mf, &is_orca_3mf, + &file_version, nullptr, + LoadStrategy::LoadModel | LoadStrategy::LoadConfig | LoadStrategy::AddDefaultInstances | + LoadStrategy::Silence)) { + std::printf("failed to load %s\n", argv[1]); + return 1; + } + + std::printf("loaded: %zu object(s)\n", model.objects.size()); + + for (const ModelObject *object : model.objects) + for (const ModelVolume *volume : object->volumes) { + if (volume->texture_displacement_layers.empty()) { + std::printf("volume \"%s\": no texture displacement layers; paint masks per slot:", + volume->name.c_str()); + for (int i = 0; i < int(TEXTURE_DISPLACEMENT_MAX_LAYERS); ++i) + std::printf(" %zu", volume->texture_displacement_facet(i).get_data().triangles_to_split.size()); + std::printf("\n"); + continue; + } + std::printf("volume \"%s\": %zu base triangles, %zu layer(s)\n", volume->name.c_str(), + volume->mesh().its.indices.size(), volume->texture_displacement_layers.size()); + + for (const TextureDisplacementLayer &layer : volume->texture_displacement_layers) { + std::printf("\n layer %d \"%s\" mapping=%d seam_angle=%.1f connect=%d islands_stored=%zu\n", + layer.slot, layer.name.c_str(), int(layer.projection_method), + layer.lscm_seam_angle_deg, int(layer.auto_connect_islands), layer.islands.size()); + if (layer.projection_method != TextureProjectionMethod::LSCM) + continue; + + const indexed_triangle_set patch = + extract_painted_patch(volume->mesh().its, volume->texture_displacement_facet(layer.slot).get_data()); + std::printf(" patch: %zu vertices, %zu triangles\n", patch.vertices.size(), patch.indices.size()); + if (patch.indices.empty()) + continue; + + const auto t0 = std::chrono::steady_clock::now(); + const PatchUnwrap unwrap = compute_patch_unwrap(patch, layer.lscm_seam_angle_deg, 0.f, + layer.lscm_seam_edges); + const auto t1 = std::chrono::steady_clock::now(); + std::printf(" TIMING compute_patch_unwrap: %.0f ms\n", + std::chrono::duration(t1 - t0).count()); + std::printf(" unwrap: %d charts, %zu unwrapped triangles\n", unwrap.chart_count, + unwrap.indices.size()); + + // Group the patch's faces by chart so each can be examined on its own. + std::vector> chart_faces(size_t(std::max(unwrap.chart_count, 0))); + for (size_t i = 0; i < unwrap.indices.size(); ++i) { + const int chart = unwrap.vertex_chart[size_t(unwrap.indices[i][0])]; + if (chart >= 0 && size_t(chart) < chart_faces.size()) + chart_faces[size_t(chart)].push_back(unwrap.source_face[i]); + } + + int bad_charts = 0; + for (size_t c = 0; c < chart_faces.size(); ++c) { + int loops = 0, euler = 0; + chart_topology(patch, chart_faces[c], loops, euler); + + // Flipped triangles: the unwrap folded over itself, which is what a planar fallback + // does to a chart that is not flat. Measured on the unwrap's own triangles. + int pos = 0, neg = 0; + for (size_t i = 0; i < unwrap.indices.size(); ++i) { + const stl_triangle_vertex_indices &t = unwrap.indices[i]; + if (unwrap.vertex_chart[size_t(t[0])] != int(c)) + continue; + const float a = signed_area_2d(unwrap.uvs[size_t(t[0])], unwrap.uvs[size_t(t[1])], + unwrap.uvs[size_t(t[2])]); + if (a > 0.f) ++pos; else if (a < 0.f) ++neg; + } + const int flipped = std::min(pos, neg); + const bool disk = loops == 1 && euler == 1; + if (!disk || flipped > 0) { + ++bad_charts; + std::printf(" chart %2zu: %4zu faces loops=%d euler=%d%s flipped=%d/%d%s\n", c, + chart_faces[c].size(), loops, euler, disk ? "" : " NOT A DISK", flipped, + pos + neg, flipped ? " FOLDED" : ""); + } + } + std::printf(" charts with a defect: %d / %d\n", bad_charts, unwrap.chart_count); + + // What the eye actually sees. Every patch edge shared by two charts should carry the same + // UV on both sides once the islands are laid out as a connected net; where it does not, + // the texture jumps across that seam. Measured through compute_lscm_uvs(), i.e. the exact + // coordinates the bake and the checker overlay sample. + { + const auto n0 = std::chrono::steady_clock::now(); + const std::vector net = compute_connected_net(unwrap); + const auto n1 = std::chrono::steady_clock::now(); + std::printf(" TIMING compute_connected_net: %.0f ms (%zu islands)\n", + std::chrono::duration(n1 - n0).count(), net.size()); + } + const auto t2 = std::chrono::steady_clock::now(); + const std::vector uv = compute_lscm_uvs(patch, layer); + const auto t3 = std::chrono::steady_clock::now(); + std::printf(" TIMING compute_lscm_uvs: %.0f ms (called on every preview, overlay and bake)\n", + std::chrono::duration(t3 - t2).count()); + if (uv.size() != patch.vertices.size()) { + std::printf(" compute_lscm_uvs returned %zu uvs for %zu vertices\n", uv.size(), + patch.vertices.size()); + continue; + } + // Per-corner UVs carry each chart's own placement, so an edge shared by two charts shows + // the jump directly: the same mesh vertex lands at two different UVs. That is exactly what + // the eye reads as the texture breaking. + const auto t4 = std::chrono::steady_clock::now(); + const std::vector corner = compute_lscm_corner_uvs(patch, layer); + const auto t5 = std::chrono::steady_clock::now(); + std::printf(" TIMING compute_lscm_corner_uvs: %.0f ms\n", + std::chrono::duration(t5 - t4).count()); + // Keyed by edge, holding the UV each incident face gives to the edge's *lower-numbered* + // endpoint. Comparing that same vertex on both sides is the point: indexing by corner + // position instead compares opposite ends of the edge, because the two faces wind it in + // opposite directions. + std::unordered_map> edge_seen; + if (corner.size() == patch.indices.size() * 3) + for (size_t f = 0; f < patch.indices.size(); ++f) { + const stl_triangle_vertex_indices &t = patch.indices[f]; + for (int k = 0; k < 3; ++k) { + const int a = t[k], b = t[(k + 1) % 3]; + const int probe = std::min(a, b); + const int local = (a == probe) ? k : (k + 1) % 3; + edge_seen[edge_key(a, b)].push_back(corner[f * 3 + size_t(local)]); + } + } + // Which chart each patch face belongs to, so a broken edge can be attributed to a pair. + std::vector chart_of_face(patch.indices.size(), -1); + for (size_t i = 0; i < unwrap.indices.size(); ++i) + chart_of_face[size_t(unwrap.source_face[i])] = unwrap.vertex_chart[size_t(unwrap.indices[i][0])]; + + std::unordered_map> edge_faces; + for (size_t f = 0; f < patch.indices.size(); ++f) { + const stl_triangle_vertex_indices &t = patch.indices[f]; + for (int k = 0; k < 3; ++k) + edge_faces[edge_key(t[k], t[(k + 1) % 3])].push_back(int(f)); + } + + int adjacent = 0, broken = 0, broken_same_chart = 0; + float worst = 0.f; + std::map, std::pair> by_pair; + for (const auto &[key, seen] : edge_seen) { + if (seen.size() != 2) + continue; + ++adjacent; + const float d = (seen[0] - seen[1]).norm(); + if (d <= 1e-4f) + continue; + ++broken; + worst = std::max(worst, d); + const auto &faces_here = edge_faces[key]; + int c1 = -1, c2 = -1; + if (faces_here.size() == 2) { + c1 = chart_of_face[size_t(faces_here[0])]; + c2 = chart_of_face[size_t(faces_here[1])]; + } + if (c1 == c2) + ++broken_same_chart; + auto &slot = by_pair[{ std::min(c1, c2), std::max(c1, c2) }]; + ++slot.first; + slot.second = std::max(slot.second, d); + } + std::printf(" broken edges inside a single chart: %d\n", broken_same_chart); + std::printf(" broken by chart pair:"); + for (const auto &[pk, v] : by_pair) + std::printf(" (%d,%d)x%d/%.1f", pk.first, pk.second, v.first, v.second); + std::printf("\n"); + // Total length of the seams left broken, in mm: how much visibly torn edge the layout has, + // which is what the eye adds up. A count alone hides whether the breaks are hairlines or + // whole sides of an island. + float seam_mm = 0.f; + for (const auto &[key, seen] : edge_seen) { + if (seen.size() != 2 || (seen[0] - seen[1]).norm() <= 1e-4f) + continue; + seam_mm += (patch.vertices[size_t(key >> 32)] - patch.vertices[size_t(uint32_t(key))]).norm(); + } + std::printf(" interior edges: %d, discontinuous: %d, total torn seam: %.2f mm (worst jump %.3f)\n", + adjacent, broken, seam_mm, worst); + std::printf(" stored islands %zu vs charts %d -> %s\n", layer.islands.size(), + unwrap.chart_count, + layer.islands.size() == size_t(unwrap.chart_count) ? "stored placements used" + : "net rebuilt"); + } + } + return 0; +} diff --git a/src/libslic3r/TextureDisplacement.cpp b/src/libslic3r/TextureDisplacement.cpp index 862c6c9044..a22cc31abc 100644 --- a/src/libslic3r/TextureDisplacement.cpp +++ b/src/libslic3r/TextureDisplacement.cpp @@ -1236,52 +1236,81 @@ bool triangles_overlap(const Tri2 &a, const Tri2 &b, float eps) struct NetGrid { static constexpr int BIG_SPAN = 16; - float cell; - float eps; - std::unordered_map> cells; - std::vector big; + // Each stored triangle keeps its own bounding box. Overlap testing is dominated by rejects - a cell + // holds every triangle whose box touches it, and a candidate meets only a couple of them for real - + // so paying six floats per entry to answer most of those rejects with four comparisons, instead of a + // full triangle intersection, is what makes the net affordable. Measured on a 42k-triangle patch the + // grid ran ~19 million candidate pairs per net, nearly all of them misses, and rejecting them this + // way took the net from ~175 ms to ~53 ms. + // + // The box rides inside the entry rather than in a parallel array: splitting them to scan boxes back + // to back was tried and came out slower, because each bucket then grows two vectors instead of one. + struct Entry + { + Tri2 tri; + Vec2f lo, hi; + }; + float cell; + float eps; + std::unordered_map> cells; + std::vector big; static uint64_t key(int x, int y) { return (uint64_t(uint32_t(x)) << 32) | uint32_t(y); } - bool range(const Tri2 &t, int &x0, int &y0, int &x1, int &y1) const + static Entry entry(const Tri2 &t) + { + return Entry{ t, t[0].cwiseMin(t[1]).cwiseMin(t[2]), t[0].cwiseMax(t[1]).cwiseMax(t[2]) }; + } + bool range(const Vec2f &lo, const Vec2f &hi, int &x0, int &y0, int &x1, int &y1) const { - const Vec2f lo = t[0].cwiseMin(t[1]).cwiseMin(t[2]), hi = t[0].cwiseMax(t[1]).cwiseMax(t[2]); x0 = int(std::floor(lo.x() / cell)); y0 = int(std::floor(lo.y() / cell)); x1 = int(std::floor(hi.x() / cell)); y1 = int(std::floor(hi.y() / cell)); return x1 - x0 <= BIG_SPAN && y1 - y0 <= BIG_SPAN; } + // Boxes grown by eps on both sides, to match the tolerance triangles_overlap() itself works to: a + // reject here must never discard a pair that test would have called touching. + bool boxes_apart(const Entry &a, const Entry &b) const + { + return a.hi.x() + eps < b.lo.x() || b.hi.x() + eps < a.lo.x() || a.hi.y() + eps < b.lo.y() || + b.hi.y() + eps < a.lo.y(); + } + bool hits(const Entry &q, const std::vector &bucket) const + { + for (const Entry &b : bucket) + if (!boxes_apart(q, b) && triangles_overlap(q.tri, b.tri, eps)) + return true; + return false; + } bool overlaps(const Tri2 &t) const { - for (const Tri2 &b : big) - if (triangles_overlap(t, b, eps)) - return true; + const Entry q = entry(t); + if (hits(q, big)) + return true; int x0, y0, x1, y1; - if (!range(t, x0, y0, x1, y1)) { - for (const auto &[k, tris] : cells) - for (const Tri2 &b : tris) - if (triangles_overlap(t, b, eps)) - return true; + if (!range(q.lo, q.hi, x0, y0, x1, y1)) { + for (const auto &[k, bucket] : cells) + if (hits(q, bucket)) + return true; return false; } for (int x = x0; x <= x1; ++x) for (int y = y0; y <= y1; ++y) - if (const auto it = cells.find(key(x, y)); it != cells.end()) - for (const Tri2 &b : it->second) - if (triangles_overlap(t, b, eps)) - return true; + if (const auto it = cells.find(key(x, y)); it != cells.end() && hits(q, it->second)) + return true; return false; } void insert(const Tri2 &t) { - int x0, y0, x1, y1; - if (!range(t, x0, y0, x1, y1)) { - big.push_back(t); + const Entry e = entry(t); + int x0, y0, x1, y1; + if (!range(e.lo, e.hi, x0, y0, x1, y1)) { + big.push_back(e); return; } for (int x = x0; x <= x1; ++x) for (int y = y0; y <= y1; ++y) - cells[key(x, y)].push_back(t); + cells[key(x, y)].push_back(e); } }; } // namespace @@ -1293,11 +1322,20 @@ std::vector compute_connected_net(const PatchUnwrap &unwrap) if (n <= 1) return islands; - // Chart adjacency, with one representative shared edge per adjacent pair. + // Chart adjacency, with one representative shared edge per adjacent pair: the fold line the pair is + // unfolded about. + // + // Which edge that is matters, because two charts can touch along more than one run. A chart cut open + // to flatten it - a ring opened by segment_into_charts(), say - touches its other half along *both* + // sides of the cut. Folding is rigid, so only the run the fold line belongs to comes out matching; + // every other run is left mismatched, and a mismatched run is exactly where the texture visibly + // jumps. Taking whichever edge the map happened to yield first therefore left the long side broken + // about as often as the short one. The fold line is picked from the longest run instead, so what is + // left discontinuous is the shortest boundary the pair has. const auto edges = build_shared_edges(unwrap); struct PairEdge { ChartEdge a, b; }; - std::map, PairEdge> pair_edge; - std::vector> adj(static_cast(n)); + struct SharedEdge { PairEdge fold; int base_lo = -1, base_hi = -1; float length = 0.f; }; + std::map, std::vector> pair_shared; for (const auto &[base_edge, list] : edges) { for (size_t i = 0; i < list.size(); ++i) for (size_t j = i + 1; j < list.size(); ++j) { @@ -1305,14 +1343,50 @@ std::vector compute_connected_net(const PatchUnwrap &unwrap) if (c1 == c2 || c1 < 0 || c2 < 0 || c1 >= n || c2 >= n) continue; const std::pair pk{ std::min(c1, c2), std::max(c1, c2) }; - if (pair_edge.count(pk)) - continue; // keep the first shared edge as the fold line for this pair - pair_edge[pk] = (c1 < c2) ? PairEdge{ list[i], list[j] } : PairEdge{ list[j], list[i] }; - adj[size_t(pk.first)].push_back(pk.second); - adj[size_t(pk.second)].push_back(pk.first); + SharedEdge se; + se.fold = (c1 < c2) ? PairEdge{ list[i], list[j] } : PairEdge{ list[j], list[i] }; + se.base_lo = base_edge.first; + se.base_hi = base_edge.second; + // The unwrap is scaled to true surface area, so a uv distance is a length in mm. + se.length = (unwrap.uvs[size_t(se.fold.a.uv_lo)] - unwrap.uvs[size_t(se.fold.a.uv_hi)]).norm(); + pair_shared[pk].push_back(se); } } + std::map, PairEdge> pair_edge; + std::map, float> pair_weight; // length of the run each pair folds across + std::vector> adj(static_cast(n)); + for (const auto &[pk, shared] : pair_shared) { + // Group the pair's shared edges into runs - edges joined end to end through a base vertex - and + // total each run's length. + std::unordered_map local; + for (const SharedEdge &se : shared) + for (const int v : { se.base_lo, se.base_hi }) + local.emplace(v, int(local.size())); + UnionFind runs(local.size()); + for (const SharedEdge &se : shared) + runs.unite(local[se.base_lo], local[se.base_hi]); + + std::unordered_map run_length; + std::unordered_map run_first; + for (size_t i = 0; i < shared.size(); ++i) { + const int root = runs.find(local[shared[i].base_lo]); + run_length[root] += shared[i].length; + run_first.emplace(root, i); + } + int best_root = -1; + float best_len = -1.f; + for (const auto &[root, len] : run_length) + if (len > best_len) { best_len = len; best_root = root; } + if (best_root < 0) + continue; + + pair_edge[pk] = shared[run_first[best_root]].fold; + pair_weight[pk] = best_len; + adj[size_t(pk.first)].push_back(pk.second); + adj[size_t(pk.second)].push_back(pk.first); + } + // Per chart: its vertices, its triangles and its flattened area. std::vector> chart_verts(static_cast(n)), chart_tris(static_cast(n)); std::vector chart_area(static_cast(n), 0.f); @@ -1366,15 +1440,29 @@ std::vector compute_connected_net(const PatchUnwrap &unwrap) for (const int t : chart_tris[size_t(root)]) grid.insert(placed(m, t)); } - std::queue q; - q.push(root); + // Grown strongest-adjacency-first (Prim, not breadth-first): a chart is folded onto whichever + // neighbour it shares the longest boundary with, among everything reachable so far. Order matters + // because only the fold a chart is actually reached by comes out matching - every other boundary + // it has is left to chance. Taking neighbours in breadth-first order, biggest-area first, let a + // far-off branch claim a chart across a short boundary before its true neighbour was reached, and + // the long boundary they shared then stayed broken. That is the visible seam next to a hole: a + // ring is cut into two halves that share a long boundary, and whichever half was reached first + // took the other one along some unrelated edge. + using Candidate = std::pair>; // weight, (from, to) + std::priority_queue q; + const auto push_neighbours = [&](int p) { + for (const int c : adj[size_t(p)]) + if (net_of[size_t(c)] < 0 && !chart_tris[size_t(c)].empty()) { + const auto w = pair_weight.find({ std::min(p, c), std::max(p, c) }); + q.push({ w == pair_weight.end() ? 0.f : w->second, { p, c } }); + } + }; + push_neighbours(root); while (!q.empty()) { - const int p = q.front(); + const auto [weight, link] = q.top(); q.pop(); - std::vector neighbours = adj[size_t(p)]; - std::stable_sort(neighbours.begin(), neighbours.end(), - [&chart_area](int a, int b) { return chart_area[size_t(a)] > chart_area[size_t(b)]; }); - for (const int c : neighbours) { + const int p = link.first, c = link.second; + { if (net_of[size_t(c)] >= 0 || chart_tris[size_t(c)].empty()) continue; const auto it = pair_edge.find({ std::min(p, c), std::max(p, c) }); @@ -1405,7 +1493,7 @@ std::vector compute_connected_net(const PatchUnwrap &unwrap) for (const Tri2 &t : tris) grid.insert(t); net_of[size_t(c)] = net; - q.push(c); + push_neighbours(c); } } } @@ -5041,4 +5129,5 @@ indexed_triangle_set cut_mesh_at_steps(const indexed_triangle_set &mesh, const s return out; } + } // namespace Slic3r