Files
OrcaSlicer/tests/fff_print/test_tree_support.cpp
T
HanifKohandraistlin7447 4deadc9dce 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 from 60d1ceb580, the
second from e148865dd6.

---------

Co-authored-by: raistlin7447 <kris.austin@gmail.com>
2026-09-09 12:33:42 +08:00

191 lines
8.2 KiB
C++

#include <catch2/catch_all.hpp>
#include <algorithm>
#include "libslic3r/Layer.hpp"
#include "libslic3r/TriangleMesh.hpp"
#include "test_helpers.hpp"
using namespace Slic3r::Test;
using namespace Slic3r;
namespace {
// The upper plate overhangs both the lower plate and open air, so branches land on the model and on
// the bed in the same slice.
TriangleMesh two_tier_mesh()
{
TriangleMesh lower = make_cube(30, 30, 3);
TriangleMesh column = make_cube(8, 8, 15);
TriangleMesh upper = make_cube(50, 50, 3);
// Each part overlaps the one below rather than resting on it; a coplanar join slices ambiguously.
column.translate(11.f, 11.f, 2.f);
upper.translate(-10.f, -10.f, 16.f);
TriangleMesh mesh = lower;
mesh.merge(column);
mesh.merge(upper);
return mesh;
}
TriangleMesh scaled(TestMesh id, float scale)
{
TriangleMesh mesh = Slic3r::Test::mesh(id);
mesh.scale(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,
std::initializer_list<Slic3r::ConfigBase::SetDeserializeItem> extra = {})
{
DynamicPrintConfig config = DynamicPrintConfig::full_print_config();
config.set_deserialize_strict({
{ "enable_support", 1 },
{ "support_type", "tree(auto)" },
{ "support_style", style },
{ "support_on_build_plate_only", build_plate_only },
{ "support_threshold_angle", threshold_angle },
{ "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)
{
Points points;
for (const SupportLayer *layer : print.objects().front()->support_layers())
layer->support_fills.collect_points(points);
return points;
}
size_t support_point_count(const TriangleMesh &mesh, const char *style, int threshold_angle = 30,
int build_plate_only = 0)
{
Slic3r::Print print;
slice_with_tree_support(mesh, print, style, threshold_angle, build_plate_only);
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]")
{
REQUIRE(support_point_count(scaled(TestMesh::overhang, 2.f), "tree_slim") > 1000);
REQUIRE(support_point_count(Slic3r::Test::cube(20), "tree_slim") == 0);
}
TEST_CASE("Restricting tree support to the build plate changes what is generated", "[TreeSupport]")
{
const TriangleMesh mesh = two_tier_mesh();
const size_t anywhere = support_point_count(mesh, "tree_slim", 30, 0);
const size_t plate_only = support_point_count(mesh, "tree_slim", 30, 1);
REQUIRE(anywhere > 1000);
REQUIRE(plate_only > 1000);
// The upper plate overhangs the lower one, so some branches would land on the model.
REQUIRE(plate_only != anywhere);
}
TEST_CASE("Tree support layers rise monotonically within the layer height limits", "[TreeSupport]")
{
Slic3r::Print print;
slice_with_tree_support(scaled(TestMesh::overhang, 2.f), print, "tree_slim");
const double nozzle = print.config().nozzle_diameter.values.front();
size_t checked = 0;
double previous = 0;
bool previous_was_adjacent = false;
for (const SupportLayer *layer : print.objects().front()->support_layers()) {
if (layer->print_z <= 0 || layer->height <= 0) {
// Layers with no nodes are left at zero. Skipping one leaves a hole, so the next pair
// spans more than one layer and its gap says nothing about the layer height limit.
previous_was_adjacent = false;
continue;
}
if (previous > 0) {
CAPTURE(previous, layer->print_z);
REQUIRE(layer->print_z > previous);
if (previous_was_adjacent)
REQUIRE(layer->print_z - previous <= nozzle + EPSILON);
}
previous = layer->print_z;
previous_was_adjacent = true;
++checked;
}
REQUIRE(checked > 10);
}
TEST_CASE("A raft is still generated under tree support", "[TreeSupport]")
{
// The mesh supports itself, so a layer count alone passes with no raft at all.
Slic3r::Print rafted, unrafted;
slice_with_tree_support(scaled(TestMesh::overhang, 2.f), rafted, "tree_slim", 30, 0, 3);
slice_with_tree_support(scaled(TestMesh::overhang, 2.f), unrafted, "tree_slim", 30, 0, 0);
const PrintObject *rafted_object = rafted.objects().front();
const PrintObject *unrafted_object = unrafted.objects().front();
REQUIRE(rafted_object->support_layers().size() > unrafted_object->support_layers().size());
// 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 },
});
}