mirror of
https://github.com/OrcaSlicer/OrcaSlicer.git
synced 2026-09-16 05:27:50 +00:00
Make Tree-Support Deterministic (#15565)
* Make tree support deterministic without giving up its parallelism * Break equal-distance ties in the tree support MST by coordinates * test: cover the determinism this PR fixes The MST unit tests here cover the tie-break, but the drop_nodes rework has no test. Adds two cases to the tree support suite. The thread-scheduling one slices five configs twice each and compares the support point sequence, which is what the node ordering moves. The MST tie one pins the branch diameter and line width that carry Prim's equal-distance ties into the toolpaths. slice_with_tree_support takes an optional config list so the second case can add the tree parameters it needs, and the double-slice comparison is shared rather than written twice. Both fail on main without this PR. The first passes from60d1ceb580, the second frome148865dd6. --------- Co-authored-by: raistlin7447 <kris.austin@gmail.com>
This commit is contained in:
@@ -1,5 +1,7 @@
|
||||
#include <catch2/catch_all.hpp>
|
||||
|
||||
#include <algorithm>
|
||||
|
||||
#include "libslic3r/Layer.hpp"
|
||||
#include "libslic3r/TriangleMesh.hpp"
|
||||
|
||||
@@ -33,10 +35,13 @@ TriangleMesh scaled(TestMesh id, float scale)
|
||||
return mesh;
|
||||
}
|
||||
|
||||
// `extra` is applied last, so a caller can add or override any key.
|
||||
void slice_with_tree_support(const TriangleMesh &mesh, Slic3r::Print &print, const char *style,
|
||||
int threshold_angle = 30, int build_plate_only = 0, int raft_layers = 0)
|
||||
int threshold_angle = 30, int build_plate_only = 0, int raft_layers = 0,
|
||||
std::initializer_list<Slic3r::ConfigBase::SetDeserializeItem> extra = {})
|
||||
{
|
||||
Slic3r::Test::init_and_process_print({ mesh }, print, {
|
||||
DynamicPrintConfig config = DynamicPrintConfig::full_print_config();
|
||||
config.set_deserialize_strict({
|
||||
{ "enable_support", 1 },
|
||||
{ "support_type", "tree(auto)" },
|
||||
{ "support_style", style },
|
||||
@@ -45,6 +50,8 @@ void slice_with_tree_support(const TriangleMesh &mesh, Slic3r::Print &print, con
|
||||
{ "raft_layers", raft_layers },
|
||||
{ "layer_height", 0.2 },
|
||||
});
|
||||
config.set_deserialize_strict(extra);
|
||||
Slic3r::Test::init_and_process_print({ mesh }, print, config);
|
||||
}
|
||||
|
||||
Points support_points(const Slic3r::Print &print)
|
||||
@@ -63,6 +70,32 @@ size_t support_point_count(const TriangleMesh &mesh, const char *style, int thre
|
||||
return support_points(print).size();
|
||||
}
|
||||
|
||||
// Index of the first differing point, or the common length when they match. An index keeps a
|
||||
// failure readable; comparing the vectors themselves dumps thousands of points.
|
||||
size_t first_difference(const Points &a, const Points &b)
|
||||
{
|
||||
const size_t common = std::min(a.size(), b.size());
|
||||
for (size_t i = 0; i < common; ++i)
|
||||
if (a[i] != b[i])
|
||||
return i;
|
||||
return common;
|
||||
}
|
||||
|
||||
// Slice `mesh` twice and require an identical support point sequence. Point counts and total
|
||||
// length are order insensitive, so the sequence is what a reordering shows up in.
|
||||
void sliced_twice_matches(const TriangleMesh &mesh, int build_plate_only, const char *style = "tree_slim",
|
||||
std::initializer_list<Slic3r::ConfigBase::SetDeserializeItem> extra = {})
|
||||
{
|
||||
Slic3r::Print first_print, second_print;
|
||||
slice_with_tree_support(mesh, first_print, style, 30, build_plate_only, 0, extra);
|
||||
slice_with_tree_support(mesh, second_print, style, 30, build_plate_only, 0, extra);
|
||||
const Points first = support_points(first_print);
|
||||
const Points second = support_points(second_print);
|
||||
REQUIRE(first.size() > 1000); // without support the comparison below passes vacuously
|
||||
REQUIRE(second.size() == first.size());
|
||||
REQUIRE(first_difference(first, second) == first.size());
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
TEST_CASE("Tree support is generated for an overhang and not for a plain cube", "[TreeSupport]")
|
||||
@@ -123,3 +156,35 @@ TEST_CASE("A raft is still generated under tree support", "[TreeSupport]")
|
||||
// The raft goes under the object.
|
||||
REQUIRE(rafted_object->layers().front()->print_z > unrafted_object->layers().front()->print_z);
|
||||
}
|
||||
|
||||
// drop_nodes() decides the node merges and spawns the next layer's nodes in parallel. Every one of
|
||||
// those decisions has to be applied in a fixed order, or the same model gives different branches on
|
||||
// each slice.
|
||||
TEST_CASE("Tree support toolpaths do not depend on thread scheduling", "[TreeSupport][Regression]")
|
||||
{
|
||||
// Scaled up so that a layer holds enough nodes for the parallel range to be split. At stock
|
||||
// size it stays in one chunk and the order never varies.
|
||||
SECTION("overhang") { sliced_twice_matches(scaled(TestMesh::overhang, 2.f), 0); }
|
||||
SECTION("bridge with hole") { sliced_twice_matches(scaled(TestMesh::bridge_with_hole, 3.f), 0); }
|
||||
// Dropping every branch that cannot reach the bed leaves the survivors dense enough that the
|
||||
// neighbour merge fires in bulk.
|
||||
SECTION("on the build plate") { sliced_twice_matches(scaled(TestMesh::overhang, 4.f), 1); }
|
||||
// Branches resting on the model are what put nodes in a part group other than 0, which is the
|
||||
// only way to reach the prune in the second pass. tree_hybrid additionally builds polygon
|
||||
// nodes, so it is the only style that exercises the overhang merge.
|
||||
SECTION("resting on the model") { sliced_twice_matches(two_tier_mesh(), 0); }
|
||||
SECTION("hybrid on the model") { sliced_twice_matches(two_tier_mesh(), 0, "tree_hybrid"); }
|
||||
}
|
||||
|
||||
// Prim breaks equal-distance ties by heap address. A 1 mm branch diameter puts neighbours close
|
||||
// enough to tie, and an explicit line width pins max_move_dist, so the moved tie winner reaches
|
||||
// the support toolpaths.
|
||||
TEST_CASE("Tree support toolpaths do not depend on the MST tie order", "[TreeSupport][Regression]")
|
||||
{
|
||||
sliced_twice_matches(two_tier_mesh(), 0, "tree_hybrid", {
|
||||
{ "tree_support_branch_diameter", 1.0 },
|
||||
{ "tree_support_branch_distance", 5.0 },
|
||||
{ "tree_support_branch_angle", 40 },
|
||||
{ "support_line_width", 0.4 },
|
||||
});
|
||||
}
|
||||
|
||||
@@ -29,6 +29,7 @@ add_executable(${_TEST_NAME}_tests
|
||||
test_polygon.cpp
|
||||
test_mutable_polygon.cpp
|
||||
test_mutable_priority_queue.cpp
|
||||
test_minimum_spanning_tree.cpp
|
||||
test_nozzle_volume_type.cpp
|
||||
test_step.cpp
|
||||
test_stl.cpp
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
#include <catch2/catch_all.hpp>
|
||||
|
||||
#include <algorithm>
|
||||
|
||||
#include "libslic3r/MinimumSpanningTree.hpp"
|
||||
#include "libslic3r/Point.hpp"
|
||||
|
||||
using namespace Slic3r;
|
||||
|
||||
// A 5x5 lattice: at every step of Prim's algorithm several candidates sit at the same
|
||||
// distance from the tree, so the tie-break decides the tree's shape.
|
||||
static std::vector<Point> lattice()
|
||||
{
|
||||
std::vector<Point> vertices;
|
||||
for (int y = 0; y < 5; ++y)
|
||||
for (int x = 0; x < 5; ++x)
|
||||
vertices.emplace_back(Point::new_scale(x, y));
|
||||
return vertices;
|
||||
}
|
||||
|
||||
static std::vector<Point> sorted_neighbours(const MinimumSpanningTree &mst, const Point &vertex)
|
||||
{
|
||||
std::vector<Point> neighbours = mst.adjacent_nodes(vertex);
|
||||
std::sort(neighbours.begin(), neighbours.end());
|
||||
return neighbours;
|
||||
}
|
||||
|
||||
TEST_CASE("Minimum spanning tree connects every vertex", "[MinimumSpanningTree]")
|
||||
{
|
||||
const std::vector<Point> vertices = lattice();
|
||||
const MinimumSpanningTree mst(vertices);
|
||||
|
||||
REQUIRE(mst.vertices().size() == vertices.size());
|
||||
size_t adjacency_entries = 0;
|
||||
for (const Point &vertex : vertices) {
|
||||
const std::vector<Point> neighbours = mst.adjacent_nodes(vertex);
|
||||
REQUIRE(! neighbours.empty());
|
||||
adjacency_entries += neighbours.size();
|
||||
}
|
||||
// A tree on n vertices has n - 1 edges, each listed from both ends.
|
||||
REQUIRE(adjacency_entries == 2 * (vertices.size() - 1));
|
||||
}
|
||||
|
||||
TEST_CASE("Minimum spanning tree does not depend on the order of the non-root vertices", "[MinimumSpanningTree][Regression]")
|
||||
{
|
||||
const std::vector<Point> vertices = lattice();
|
||||
const MinimumSpanningTree reference(vertices);
|
||||
|
||||
// The root stays first: Prim's tree legitimately depends on where it starts.
|
||||
// Every other order of the remaining vertices must give the same tree.
|
||||
std::vector<std::vector<Point>> orders;
|
||||
orders.emplace_back(vertices);
|
||||
std::reverse(orders.back().begin() + 1, orders.back().end());
|
||||
for (size_t shift = 1; shift + 1 < vertices.size(); ++shift) {
|
||||
orders.emplace_back(vertices);
|
||||
std::rotate(orders.back().begin() + 1, orders.back().begin() + 1 + shift, orders.back().end());
|
||||
}
|
||||
|
||||
for (const std::vector<Point> &order : orders) {
|
||||
const MinimumSpanningTree mst(order);
|
||||
for (const Point &vertex : vertices) {
|
||||
INFO("vertex " << vertex.x() << "," << vertex.y());
|
||||
REQUIRE(sorted_neighbours(mst, vertex) == sorted_neighbours(reference, vertex));
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user