Compare commits

...
Author SHA1 Message Date
ExPikaPaka 1731757749 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.
2026-10-05 08:38:45 +02:00
Kris Austin 9d71125495 ci: allow running the Main build status workflow manually (#16057) 2026-10-01 18:25:12 -03:00
Kris Austin 88f05d3060 ci: move the nightly Build all to the quietest hour for macOS runners (#16053) 2026-10-01 17:58:00 -03:00
Kris Austin 4743d00793 ci: README build badge no longer shows failing when a queued main build is cancelled (#16054) 2026-10-01 17:56:57 -03:00
Ian Chua 2f4758446a feat: add foundation for configurable printer agent connection (#16027)
* feat: add foundation for configurable printer agent connection

* fix: regression after merge

* chore: remove duplcate http include

* fix: remove unrelated moonraker.cpp changes

* chore: port add_platform_root_certificates to 15711

* fix: clean diff
2026-10-01 22:50:56 +08:00
10 changed files with 492 additions and 62 deletions
+1 -1
View File
@@ -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:
+28
View File
@@ -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 ]
+1 -1
View File
@@ -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>
[![GitHub Repo stars](https://img.shields.io/github/stars/OrcaSlicer/OrcaSlicer)](https://github.com/OrcaSlicer/OrcaSlicer/stargazers) [![Build all](https://github.com/OrcaSlicer/OrcaSlicer/actions/workflows/build_all.yml/badge.svg?branch=main)](https://github.com/OrcaSlicer/OrcaSlicer/actions/workflows/build_all.yml)
[![GitHub Repo stars](https://img.shields.io/github/stars/OrcaSlicer/OrcaSlicer)](https://github.com/OrcaSlicer/OrcaSlicer/stargazers) [![Build all](https://img.shields.io/github/actions/workflow/status/OrcaSlicer/OrcaSlicer/main_build_status.yml?label=Build%20all)](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.
+6
View File
@@ -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)
+292
View File
@@ -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;
}
+127 -38
View File
@@ -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
+4 -1
View File
@@ -276,7 +276,10 @@ void MonitorPanel::select_machine(std::string machine_sn)
void MonitorPanel::on_timer(wxTimerEvent& event)
{
if (update_flag) {
// MediaPlayCtrl may yield the event loop while it joins its camera worker
// during window teardown. Do not let a queued monitor refresh touch panels
// that are already being destroyed.
if (!wxGetApp().is_closing() && update_flag) {
update_all();
//Layout();
}
+26 -15
View File
@@ -23,14 +23,15 @@
#include <wx/filedlg.h>
#include <miniz.h>
#include <algorithm>
#include <cctype>
#include "Plater.hpp"
#include "BitmapCache.hpp"
#include "slic3r/GUI/GUI_App.hpp"
#include "DeviceCore/DevManager.h"
#include "DeviceCore/DevStorage.h"
#include "md4c/src/md4c-html.h"
#include "../Utils/Http.hpp"
#include "md4c/src/md4c-html.h"
namespace Slic3r { namespace GUI {
@@ -1777,7 +1778,7 @@ void InputIpAddressDialog::set_machine_obj(MachineObject* obj)
auto str_ip = m_input_ip->GetTextCtrl()->GetValue();
auto str_access_code = m_input_access_code->GetTextCtrl()->GetValue();
// ORCA enabling / disabling buttons with conditions enough to change its style
m_button_ok->Enable(isIp(str_ip.ToStdString()) &&
m_button_ok->Enable(isValidEndpoint(str_ip.ToStdString()) &&
(str_access_code.IsEmpty() || str_access_code.Length() >= 8));
Layout();
@@ -1815,19 +1816,29 @@ void InputIpAddressDialog::update_test_msg(wxString msg,bool connected)
Fit();
}
bool InputIpAddressDialog::isIp(std::string ipstr)
bool InputIpAddressDialog::isValidEndpoint(std::string endpoint)
{
istringstream ipstream(ipstr);
int num[4];
char point[3];
string end;
ipstream >> num[0] >> point[0] >> num[1] >> point[1] >> num[2] >> point[2] >> num[3] >> end;
for (int i = 0; i < 3; ++i) {
if (num[i] < 0 || num[i]>255) return false;
if (point[i] != '.') return false;
}
if (num[3] < 0 || num[3]>255) return false;
if (!end.empty()) return false;
if (endpoint.empty() || std::any_of(endpoint.begin(), endpoint.end(), [](unsigned char c) {
return std::isspace(c) != 0;
}))
return false;
const bool has_http_scheme = endpoint.rfind("http://", 0) == 0;
const bool has_https_scheme = endpoint.rfind("https://", 0) == 0;
const auto scheme_pos = endpoint.find("://");
if (scheme_pos != std::string::npos && !has_http_scheme && !has_https_scheme)
return false;
std::string port;
const std::string host = Http::get_host_from_url(endpoint, &port);
if (host.empty())
return false;
// get_host_from_url returns its input when libcurl cannot parse it. For a
// URL with a scheme, that means a failed parse still needs to be rejected.
if (scheme_pos != std::string::npos && host == endpoint)
return false;
return true;
}
@@ -2130,7 +2141,7 @@ void InputIpAddressDialog::on_text(wxCommandEvent &evt)
// ORCA enabling / disabling buttons with conditions enough to change its style
bool valid_access_code_length = str_access_code.IsEmpty() || str_access_code.Length() >= 8;
bool enable_btns = isIp(str_ip.ToStdString()) && valid_access_code_length && invalid_access_code;
bool enable_btns = isValidEndpoint(str_ip.ToStdString()) && valid_access_code_length && invalid_access_code;
m_button_manual_setup->Enable(enable_btns);
m_button_ok->Enable(enable_btns);
+1 -1
View File
@@ -348,7 +348,7 @@ public:
void update_title(wxString title);
void set_machine_obj(MachineObject* obj);
void update_test_msg(wxString msg, bool connected);
bool isIp(std::string ipstr);
bool isValidEndpoint(std::string endpoint);
void check_ip_address_failed(int result);
void on_check_ip_address_failed(wxCommandEvent& evt);
void on_ok(wxMouseEvent& evt);
+6 -5
View File
@@ -1,5 +1,6 @@
#include "Http.hpp"
#include <atomic>
#include <cstdlib>
#include <functional>
#include <thread>
@@ -122,7 +123,7 @@ struct Http::priv
std::string error_buffer; // Used for CURLOPT_ERRORBUFFER
std::string headers;
size_t limit;
bool cancel;
std::atomic_bool cancel;
std::unique_ptr<form_file> putFile;
std::thread io_thread;
@@ -260,9 +261,9 @@ int Http::priv::xfercb(void *userp, curl_off_t dltotal, curl_off_t dlnow, curl_o
self->progressfn(progress, cb_cancel);
}
if (cb_cancel) { self->cancel = true; }
if (cb_cancel) { self->cancel.store(true); }
return self->cancel;
return self->cancel.load();
}
int Http::priv::xfercb_legacy(void *userp, double dltotal, double dlnow, double ultotal, double ulnow)
@@ -473,7 +474,7 @@ void Http::priv::http_perform()
if (res != CURLE_OK) {
if (res == CURLE_ABORTED_BY_CALLBACK) {
if (cancel) {
if (cancel.load()) {
// The abort comes from the request being cancelled programatically
Progress dummyprogress(0, 0, 0, 0, std::string());
bool cancel = true;
@@ -784,7 +785,7 @@ void Http::perform_sync()
void Http::cancel()
{
if (p) { p->cancel = true; }
if (p) { p->cancel.store(true); }
}
void Http::print() const