mirror of
https://github.com/OrcaSlicer/OrcaSlicer.git
synced 2026-10-05 14:51:06 +00:00
Compare commits
5
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1731757749 | ||
|
|
9d71125495 | ||
|
|
88f05d3060 | ||
|
|
4743d00793 | ||
|
|
2f4758446a |
@@ -45,7 +45,7 @@ on:
|
||||
|
||||
|
||||
schedule:
|
||||
- cron: '0 17 * * *' # run once a day at 1 AM Singapore time (UTC+8)
|
||||
- cron: '15 2 * * *' # 10:15 AM Singapore time (UTC+8), when macOS runners are least busy
|
||||
|
||||
workflow_dispatch: # allows for manual dispatch
|
||||
inputs:
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
# Reports the newest push build of main that was not cancelled, for the README badge.
|
||||
name: Main build status
|
||||
|
||||
on:
|
||||
workflow_run:
|
||||
workflows: ["Build all"]
|
||||
types: [completed]
|
||||
branches: [main]
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
actions: read
|
||||
|
||||
jobs:
|
||||
status:
|
||||
if: github.repository == 'OrcaSlicer/OrcaSlicer'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
run: |
|
||||
for attempt in 1 2 3; do
|
||||
conclusion=$(gh api "repos/${{ github.repository }}/actions/workflows/build_all.yml/runs?branch=main&event=push&status=completed&per_page=100" \
|
||||
--jq '[.workflow_runs[] | select(.conclusion != "cancelled")][0].conclusion') && break
|
||||
sleep 10
|
||||
done
|
||||
echo "Latest finished push build of main: $conclusion"
|
||||
[ "$conclusion" = success ]
|
||||
@@ -6,7 +6,7 @@
|
||||
|
||||
<a href="https://trendshift.io/repositories/15552" target="_blank"><img src="https://trendshift.io/api/badge/repositories/15552" alt="OrcaSlicer%2FOrcaSlicer | Trendshift" style="width: 250px; height: 55px;" width="250" height="55"/></a>
|
||||
|
||||
[](https://github.com/OrcaSlicer/OrcaSlicer/stargazers) [](https://github.com/OrcaSlicer/OrcaSlicer/actions/workflows/build_all.yml)
|
||||
[](https://github.com/OrcaSlicer/OrcaSlicer/stargazers) [](https://github.com/OrcaSlicer/OrcaSlicer/actions/workflows/build_all.yml)
|
||||
|
||||
OrcaSlicer: an open source Next-Gen Slicing Software for Precision 3D Prints.
|
||||
Optimize your prints with ultra-fast slicing, intelligent support generation, and seamless printer compatibility—engineered for perfection.
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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 <project.3mf>
|
||||
|
||||
// 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 <chrono>
|
||||
#include <cstdio>
|
||||
#include <string>
|
||||
#include <functional>
|
||||
#include <unordered_map>
|
||||
#include <vector>
|
||||
|
||||
#include "libslic3r/Model.hpp"
|
||||
#include "libslic3r/TextureDisplacement.hpp"
|
||||
#include "libslic3r/Format/bbs_3mf.hpp"
|
||||
#include "libslic3r/Utils.hpp"
|
||||
|
||||
#include <boost/filesystem.hpp>
|
||||
|
||||
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<int> &faces, int &loops, int &euler)
|
||||
{
|
||||
std::unordered_map<uint64_t, int> edge_use;
|
||||
std::unordered_map<int, int> 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<int, int> parent;
|
||||
const std::function<int(int)> 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<int, int> 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 <project.3mf>\n");
|
||||
return 2;
|
||||
}
|
||||
|
||||
Model model;
|
||||
DynamicPrintConfig config;
|
||||
ConfigSubstitutionContext ctx(ForwardCompatibilitySubstitutionRule::Enable);
|
||||
PlateDataPtrs plate_data;
|
||||
std::vector<Preset *> 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<double, std::milli>(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<std::vector<int>> 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<TextureIsland> 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<double, std::milli>(n1 - n0).count(), net.size());
|
||||
}
|
||||
const auto t2 = std::chrono::steady_clock::now();
|
||||
const std::vector<Vec2f> 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<double, std::milli>(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<Vec2f> 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<double, std::milli>(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<uint64_t, std::vector<Vec2f>> 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<int> 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<uint64_t, std::vector<int>> 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<int, int>, std::pair<int, float>> 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;
|
||||
}
|
||||
@@ -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<uint64_t, std::vector<Tri2>> cells;
|
||||
std::vector<Tri2> 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<uint64_t, std::vector<Entry>> cells;
|
||||
std::vector<Entry> 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<Entry> &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<TextureIsland> 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<std::pair<int, int>, PairEdge> pair_edge;
|
||||
std::vector<std::vector<int>> adj(static_cast<size_t>(n));
|
||||
struct SharedEdge { PairEdge fold; int base_lo = -1, base_hi = -1; float length = 0.f; };
|
||||
std::map<std::pair<int, int>, std::vector<SharedEdge>> 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<TextureIsland> compute_connected_net(const PatchUnwrap &unwrap)
|
||||
if (c1 == c2 || c1 < 0 || c2 < 0 || c1 >= n || c2 >= n)
|
||||
continue;
|
||||
const std::pair<int, int> 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<std::pair<int, int>, PairEdge> pair_edge;
|
||||
std::map<std::pair<int, int>, float> pair_weight; // length of the run each pair folds across
|
||||
std::vector<std::vector<int>> adj(static_cast<size_t>(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<int, int> 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<int, float> run_length;
|
||||
std::unordered_map<int, size_t> 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<std::vector<int>> chart_verts(static_cast<size_t>(n)), chart_tris(static_cast<size_t>(n));
|
||||
std::vector<float> chart_area(static_cast<size_t>(n), 0.f);
|
||||
@@ -1366,15 +1440,29 @@ std::vector<TextureIsland> compute_connected_net(const PatchUnwrap &unwrap)
|
||||
for (const int t : chart_tris[size_t(root)])
|
||||
grid.insert(placed(m, t));
|
||||
}
|
||||
std::queue<int> 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<float, std::pair<int, int>>; // weight, (from, to)
|
||||
std::priority_queue<Candidate> 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<int> 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<TextureIsland> 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
|
||||
|
||||
Reference in New Issue
Block a user