From 0bbd50eaa01c83a2f5dbb8a5b9f9cc53a31769c0 Mon Sep 17 00:00:00 2001 From: tamasmeszaros Date: Tue, 15 Nov 2022 17:00:16 +0100 Subject: [PATCH] Bugfixes and new tests for pillar search --- src/libslic3r/BranchingTree/BranchingTree.hpp | 2 +- src/libslic3r/SLA/SupportTreeUtils.hpp | 331 +----------------- src/libslic3r/SLA/SupportTreeUtilsLegacy.hpp | 80 +++++ tests/sla_print/sla_print_tests.cpp | 8 - tests/sla_print/sla_supptreeutils_tests.cpp | 307 ++++++++++------ tests/sla_print/sla_test_utils.hpp | 54 --- 6 files changed, 277 insertions(+), 505 deletions(-) diff --git a/src/libslic3r/BranchingTree/BranchingTree.hpp b/src/libslic3r/BranchingTree/BranchingTree.hpp index d30499c11..3f7fd7b80 100644 --- a/src/libslic3r/BranchingTree/BranchingTree.hpp +++ b/src/libslic3r/BranchingTree/BranchingTree.hpp @@ -21,7 +21,7 @@ class Properties public: - constexpr bool group_pillars() const noexcept { return true; } + constexpr bool group_pillars() const noexcept { return false; } // Maximum slope for bridges of the tree Properties &max_slope(double val) noexcept diff --git a/src/libslic3r/SLA/SupportTreeUtils.hpp b/src/libslic3r/SLA/SupportTreeUtils.hpp index 08e410431..1a771e028 100644 --- a/src/libslic3r/SLA/SupportTreeUtils.hpp +++ b/src/libslic3r/SLA/SupportTreeUtils.hpp @@ -26,30 +26,6 @@ using Slic3r::opt::AlgNLoptGenetic; using Slic3r::Geometry::dir_to_spheric; using Slic3r::Geometry::spheric_to_dir; -// Helper function for pillar interconnection where pairs of already connected -// pillars should be checked for not to be processed again. This can be done -// in constant time with a set of hash values uniquely representing a pair of -// integers. The order of numbers within the pair should not matter, it has -// the same unique hash. The hash value has to have twice as many bits as the -// arguments need. If the same integral type is used for args and return val, -// make sure the arguments use only the half of the type's bit depth. -template> -IntegerOnly pairhash(I a, I b) -{ - using std::ceil; using std::log2; using std::max; using std::min; - static const auto constexpr Ibits = int(sizeof(I) * CHAR_BIT); - static const auto constexpr DoubleIbits = int(sizeof(DoubleI) * CHAR_BIT); - static const auto constexpr shift = DoubleIbits / 2 < Ibits ? Ibits / 2 : Ibits; - - I g = min(a, b), l = max(a, b); - - // Assume the hash will fit into the output variable - assert((g ? (ceil(log2(g))) : 0) <= shift); - assert((l ? (ceil(log2(l))) : 0) <= shift); - - return (DoubleI(g) << shift) + l; -} - // Give points on a 3D ring with given center, radius and orientation // method based on: // https://math.stackexchange.com/questions/73237/parametric-equation-of-a-circle-in-3d-space @@ -294,62 +270,6 @@ Hit pinhead_mesh_hit(Ex ex, head.r_back_mm, head.width_mm, safety_d); } -template -std::optional search_widening_path(Ex policy, - const SupportableMesh &sm, - const Vec3d &jp, - const Vec3d &dir, - double radius, - double new_radius) -{ - double w = radius + 2 * sm.cfg.head_back_radius_mm; - double stopval = w + jp.z() - ground_level(sm); - Optimizer solver(get_criteria(sm.cfg).stop_score(stopval)); - - auto [polar, azimuth] = dir_to_spheric(dir); - - double fallback_ratio = radius / sm.cfg.head_back_radius_mm; - - auto oresult = solver.to_max().optimize( - [&policy, &sm, jp, radius, new_radius](const opt::Input<3> &input) { - auto &[plr, azm, t] = input; - - auto d = spheric_to_dir(plr, azm).normalized(); - - auto sd = sm.cfg.safety_distance(new_radius); - - double ret = pinhead_mesh_hit(policy, sm.emesh, jp, d, radius, - new_radius, t, sd) - .distance(); - - Beam beam{jp + t * d, d, new_radius}; - double down = beam_mesh_hit(policy, sm.emesh, beam, sd).distance(); - - if (ret > t && std::isinf(down)) - ret += jp.z() - ground_level(sm); - - return ret; - }, - initvals({polar, azimuth, w}), // start with what we have - bounds({ - {PI - sm.cfg.bridge_slope, PI}, // Must not exceed the slope limit - {-PI, PI}, // azimuth can be a full search - {radius + sm.cfg.head_back_radius_mm, - fallback_ratio * sm.cfg.max_bridge_length_mm} - })); - - if (oresult.score >= stopval) { - polar = std::get<0>(oresult.optimum); - azimuth = std::get<1>(oresult.optimum); - double t = std::get<2>(oresult.optimum); - Vec3d endp = jp + t * spheric_to_dir(polar, azimuth); - - return DiffBridge(jp, endp, radius, sm.cfg.head_back_radius_mm); - } - - return {}; -} - inline double distance(const SupportPoint &a, const SupportPoint &b) { return (a.pos - b.pos).norm(); @@ -518,167 +438,6 @@ struct GroundConnection { operator bool() const { return pillar_base.has_value() && !path.empty(); } }; -template -GroundConnection find_pillar_route(Ex policy, - const SupportableMesh &sm, - const Junction &source, - const Vec3d &sourcedir, - double end_radius) -{ - GroundConnection ret; - ret.path.emplace_back(source); - - double sd = sm.cfg.safety_distance(source.r); - auto gp = Vec3d{source.pos.x(), source.pos.y(), ground_level(sm)}; - - auto hit = beam_mesh_hit(policy, - sm.emesh, - Beam{{source.pos, source.r}, {gp, end_radius}}, - sd); - - if (std::isinf(hit.distance())) { - double base_radius = std::max(sm.cfg.base_radius_mm, end_radius); - - ret.pillar_base = - Pedestal{gp, sm.cfg.base_height_mm, base_radius, end_radius}; - } - - return ret; -} - -//template -//GroundConnection find_pillar_route(Ex policy, -// const SupportableMesh &sm, -// const Junction &source, -// const Vec3d &sourcedir, -// double end_radius) -//{ -// GroundConnection ret; - -// ret.path.emplace_back(source); - -// Vec3d jp = source.pos, endp = jp, dir = sourcedir; -// bool can_add_base = false/*, non_head = false*/; - -// double gndlvl = 0.; // The Z level where pedestals should be -// double jp_gnd = 0.; // The lowest Z where a junction center can be -// double gap_dist = 0.; // The gap distance between the model and the pad -// double radius = source.r; -// double sd = sm.cfg.safety_distance(radius); - -// double r2 = radius + (end_radius - radius) / (jp.z() - ground_level(sm)); - -// auto to_floor = [&gndlvl](const Vec3d &p) { return Vec3d{p.x(), p.y(), gndlvl}; }; - -// auto eval_limits = [&sm, &radius, &can_add_base, &gndlvl, &gap_dist, &jp_gnd, end_radius] -// (bool base_en = true) -// { -// can_add_base = base_en && radius >= sm.cfg.head_back_radius_mm; -// double base_r = can_add_base ? std::max(sm.cfg.base_radius_mm, end_radius) : end_radius; -// gndlvl = ground_level(sm); -// if (!can_add_base) gndlvl -= sm.pad_cfg.wall_thickness_mm; -// jp_gnd = gndlvl + (can_add_base ? 0. : sm.cfg.head_back_radius_mm); -// gap_dist = sm.cfg.pillar_base_safety_distance_mm + base_r + EPSILON; -// }; - -// eval_limits(); - -// // We are dealing with a mini pillar that's potentially too long -// if (radius < sm.cfg.head_back_radius_mm && jp.z() - gndlvl > 20 * radius) -// { -// std::optional diffbr = -// search_widening_path(policy, sm, jp, dir, radius, -// sm.cfg.head_back_radius_mm); - -// if (diffbr && diffbr->endp.z() > jp_gnd) { -// endp = diffbr->endp; -// radius = diffbr->end_r; -// ret.path.emplace_back(endp, radius); -// dir = diffbr->get_dir(); -// eval_limits(); -// } else return ret; -// } - -// if (sm.cfg.object_elevation_mm < EPSILON) -// { -// // get a suitable direction for the corrector bridge. It is the -// // original sourcedir's azimuth but the polar angle is saturated to the -// // configured bridge slope. -// auto [polar, azimuth] = dir_to_spheric(dir); -// polar = PI - sm.cfg.bridge_slope; -// Vec3d d = spheric_to_dir(polar, azimuth).normalized(); -// double t = beam_mesh_hit(policy, sm.emesh, Beam{endp, d, radius, r2}, sd).distance(); -// double tmax = std::min(sm.cfg.max_bridge_length_mm, t); -// t = 0.; - -// double zd = endp.z() - jp_gnd; -// double tmax2 = zd / std::sqrt(1 - sm.cfg.bridge_slope * sm.cfg.bridge_slope); -// tmax = std::min(tmax, tmax2); - -// Vec3d nexp = endp; -// double dlast = 0.; -// double rnext = radius; -// while (((dlast = std::sqrt(sm.emesh.squared_distance(to_floor(nexp)))) < gap_dist || -// !std::isinf(beam_mesh_hit(policy, sm.emesh, Beam{nexp, DOWN, rnext, end_radius}, sd).distance())) && -// t < tmax) -// { -// t += radius; -// nexp = endp + t * d; -// double bridge_ratio = dlast / (dlast + (nexp.z() - ground_level(sm))); -// rnext = rnext + bridge_ratio * (end_radius - rnext); -// } - -// // If could not find avoidance bridge for the pad gap, try again -// // without the pillar base -// if (dlast < gap_dist && can_add_base) { -// nexp = endp; -// t = 0.; -// rnext = radius; -// can_add_base = false; -// eval_limits(can_add_base); - -// zd = endp.z() - jp_gnd; -// tmax2 = zd / std::sqrt(1 - sm.cfg.bridge_slope * sm.cfg.bridge_slope); -// tmax = std::min(tmax, tmax2); - -// while (((dlast = std::sqrt(sm.emesh.squared_distance(to_floor(nexp)))) < gap_dist || -// !std::isinf(beam_mesh_hit(policy, sm.emesh, Beam{nexp, DOWN, rnext, end_radius}, sd).distance())) && t < tmax) { -// t += radius; -// nexp = endp + t * d; - -// double bridge_ratio = dlast / (dlast + (nexp.z() - ground_level(sm))); -// rnext = rnext + bridge_ratio * (end_radius - rnext); -// } -// } - -// // Could not find a path to avoid the pad gap -// if (dlast < gap_dist) { -// ret.path.clear(); -// return ret; -// } - -// if (t > 0.) { // Need to make additional bridge -// ret.path.emplace_back(nexp, rnext); -// endp = nexp; -// } -// } - -// Vec3d gp = to_floor(endp); -// auto hit = beam_mesh_hit(policy, sm.emesh, -// Beam{{endp, radius}, {gp, end_radius}}, sd); - -// if (std::isinf(hit.distance())) { -// double base_radius = can_add_base ? -// std::max(sm.cfg.base_radius_mm, end_radius) : end_radius; - -// Vec3d gp = to_floor(endp); -// ret.pillar_base = -// Pedestal{gp, sm.cfg.base_height_mm, base_radius, end_radius}; -// } - -// return ret; -//} - inline long build_ground_connection(SupportTreeBuilder &builder, const SupportableMesh &sm, const GroundConnection &conn) @@ -714,89 +473,6 @@ inline long build_ground_connection(SupportTreeBuilder &builder, return ret; } -template -GroundConnection find_ground_connection( - Ex policy, - const SupportableMesh &sm, - const Junction &j, - const Vec3d &dir, - double end_r) -{ - auto hjp = j.pos; - double r = j.r; - auto sd = sm.cfg.safety_distance(r); - double r2 = j.r + (end_r - j.r) / (j.pos.z() - ground_level(sm)); - - double t = beam_mesh_hit(policy, sm.emesh, Beam{hjp, dir, r, r2}, sd).distance(); - t = std::min(t, sm.cfg.max_bridge_length_mm); - double d = 0.; - - GroundConnection gnd_route; - - while (!gnd_route && d < t) { - Vec3d endp = hjp + d * dir; - double bridge_ratio = d / (d + (endp.z() - ground_level(sm))); - double pill_r = r + bridge_ratio * (end_r - r); - gnd_route = find_pillar_route(policy, sm, {endp, pill_r}, dir, end_r); - - d += r; - } - - GroundConnection ret; - - if (d > 0.) - ret.path.emplace_back(j); - - for (auto &p : gnd_route.path) - ret.path.emplace_back(p); - - // This will ultimately determine if the route is valid or not - // but the path junctions will be provided anyways, so invalid paths - // can be inspected - ret.pillar_base = gnd_route.pillar_base; - - return ret; -} - -template -GroundConnection optimize_ground_connection( - Ex policy, - const SupportableMesh &sm, - const Junction &j, - double end_radius, - const Vec3d &init_dir = DOWN) -{ - double downdst = j.pos.z() - ground_level(sm); - - auto res = find_ground_connection(policy, sm, j, init_dir, end_radius); - if (res) - return res; - - // Optimize bridge direction: - // Straight path failed so we will try to search for a suitable - // direction out of the cavity. - auto [polar, azimuth] = dir_to_spheric(init_dir); - - Optimizer solver(get_criteria(sm.cfg).stop_score(1e6)); - solver.seed(0); // we want deterministic behavior - - auto sd = sm.cfg.safety_distance(j.r); - auto oresult = solver.to_max().optimize( - [&j, sd, &policy, &sm, &downdst, &end_radius](const opt::Input<2> &input) { - auto &[plr, azm] = input; - Vec3d n = spheric_to_dir(plr, azm).normalized(); - Beam beam{Ball{j.pos, j.r}, Ball{j.pos + downdst * n, end_radius}}; - return beam_mesh_hit(policy, sm.emesh, beam, sd).distance(); - }, - initvals({polar, azimuth}), // let's start with what we have - bounds({ {PI - sm.cfg.bridge_slope, PI}, {-PI, PI} }) - ); - - Vec3d bridgedir = spheric_to_dir(oresult.optimum).normalized(); - - return find_ground_connection(policy, sm, j, bridgedir, end_radius); -} - template GroundConnection deepsearch_ground_connection( Ex policy, @@ -861,10 +537,10 @@ GroundConnection deepsearch_ground_connection( if (gap < zelev_gap) ret = full_len - zelev_gap + gap; else // success - ret = StopScore; + ret = StopScore + EPSILON; } else { // No zero elevation, return success - ret = StopScore; + ret = StopScore + EPSILON; } } else { // Ground route is not free @@ -902,7 +578,8 @@ GroundConnection deepsearch_ground_connection( Vec3d gp{bridge_end.x(), bridge_end.y(), gndlvl}; conn.path.emplace_back(j); - conn.path.emplace_back(Junction{bridge_end, bridge_r}); + if (bridge_len > EPSILON) + conn.path.emplace_back(Junction{bridge_end, bridge_r}); conn.pillar_base = Pedestal{gp, sm.cfg.base_height_mm, base_r, end_radius}; diff --git a/src/libslic3r/SLA/SupportTreeUtilsLegacy.hpp b/src/libslic3r/SLA/SupportTreeUtilsLegacy.hpp index 43a8ddb59..b504d82fb 100644 --- a/src/libslic3r/SLA/SupportTreeUtilsLegacy.hpp +++ b/src/libslic3r/SLA/SupportTreeUtilsLegacy.hpp @@ -8,6 +8,86 @@ namespace Slic3r { namespace sla { +// Helper function for pillar interconnection where pairs of already connected +// pillars should be checked for not to be processed again. This can be done +// in constant time with a set of hash values uniquely representing a pair of +// integers. The order of numbers within the pair should not matter, it has +// the same unique hash. The hash value has to have twice as many bits as the +// arguments need. If the same integral type is used for args and return val, +// make sure the arguments use only the half of the type's bit depth. +template> +IntegerOnly pairhash(I a, I b) +{ + using std::ceil; using std::log2; using std::max; using std::min; + static const auto constexpr Ibits = int(sizeof(I) * CHAR_BIT); + static const auto constexpr DoubleIbits = int(sizeof(DoubleI) * CHAR_BIT); + static const auto constexpr shift = DoubleIbits / 2 < Ibits ? Ibits / 2 : Ibits; + + I g = min(a, b), l = max(a, b); + + // Assume the hash will fit into the output variable + assert((g ? (ceil(log2(g))) : 0) <= shift); + assert((l ? (ceil(log2(l))) : 0) <= shift); + + return (DoubleI(g) << shift) + l; +} + +template +std::optional search_widening_path(Ex policy, + const SupportableMesh &sm, + const Vec3d &jp, + const Vec3d &dir, + double radius, + double new_radius) +{ + double w = radius + 2 * sm.cfg.head_back_radius_mm; + double stopval = w + jp.z() - ground_level(sm); + Optimizer solver(get_criteria(sm.cfg).stop_score(stopval)); + + auto [polar, azimuth] = dir_to_spheric(dir); + + double fallback_ratio = radius / sm.cfg.head_back_radius_mm; + + auto oresult = solver.to_max().optimize( + [&policy, &sm, jp, radius, new_radius](const opt::Input<3> &input) { + auto &[plr, azm, t] = input; + + auto d = spheric_to_dir(plr, azm).normalized(); + + auto sd = sm.cfg.safety_distance(new_radius); + + double ret = pinhead_mesh_hit(policy, sm.emesh, jp, d, radius, + new_radius, t, sd) + .distance(); + + Beam beam{jp + t * d, d, new_radius}; + double down = beam_mesh_hit(policy, sm.emesh, beam, sd).distance(); + + if (ret > t && std::isinf(down)) + ret += jp.z() - ground_level(sm); + + return ret; + }, + initvals({polar, azimuth, w}), // start with what we have + bounds({ + {PI - sm.cfg.bridge_slope, PI}, // Must not exceed the slope limit + {-PI, PI}, // azimuth can be a full search + {radius + sm.cfg.head_back_radius_mm, + fallback_ratio * sm.cfg.max_bridge_length_mm} + })); + + if (oresult.score >= stopval) { + polar = std::get<0>(oresult.optimum); + azimuth = std::get<1>(oresult.optimum); + double t = std::get<2>(oresult.optimum); + Vec3d endp = jp + t * spheric_to_dir(polar, azimuth); + + return DiffBridge(jp, endp, radius, sm.cfg.head_back_radius_mm); + } + + return {}; +} + // This is a proxy function for pillar creation which will mind the gap // between the pad and the model bottom in zero elevation mode. // 'pinhead_junctionpt' is the starting junction point which needs to be diff --git a/tests/sla_print/sla_print_tests.cpp b/tests/sla_print/sla_print_tests.cpp index 8ea91d57a..d581f8340 100644 --- a/tests/sla_print/sla_print_tests.cpp +++ b/tests/sla_print/sla_print_tests.cpp @@ -1,4 +1,3 @@ -#include #include #include #include @@ -32,13 +31,6 @@ const char *const SUPPORT_TEST_MODELS[] = { } // namespace -TEST_CASE("Pillar pairhash should be unique", "[SLASupportGeneration]") { - test_pairhash(); - test_pairhash(); - test_pairhash(); - test_pairhash(); -} - TEST_CASE("Support point generator should be deterministic if seeded", "[SLASupportGeneration], [SLAPointGen]") { TriangleMesh mesh = load_model("A_upsidedown.obj"); diff --git a/tests/sla_print/sla_supptreeutils_tests.cpp b/tests/sla_print/sla_supptreeutils_tests.cpp index 95ac76633..69117358d 100644 --- a/tests/sla_print/sla_supptreeutils_tests.cpp +++ b/tests/sla_print/sla_supptreeutils_tests.cpp @@ -1,8 +1,158 @@ #include #include +#include + #include "libslic3r/Execution/ExecutionSeq.hpp" #include "libslic3r/SLA/SupportTreeUtils.hpp" +#include "libslic3r/SLA/SupportTreeUtilsLegacy.hpp" + +// Test pair hash for 'nums' random number pairs. +template void test_pairhash() +{ + const constexpr size_t nums = 1000; + I A[nums] = {0}, B[nums] = {0}; + std::unordered_set CH; + std::unordered_map> ints; + + std::random_device rd; + std::mt19937 gen(rd()); + + const I Ibits = int(sizeof(I) * CHAR_BIT); + const II IIbits = int(sizeof(II) * CHAR_BIT); + + int bits = IIbits / 2 < Ibits ? Ibits / 2 : Ibits; + if (std::is_signed::value) bits -= 1; + const I Imin = 0; + const I Imax = I(std::pow(2., bits) - 1); + + std::uniform_int_distribution dis(Imin, Imax); + + for (size_t i = 0; i < nums;) { + I a = dis(gen); + if (CH.find(a) == CH.end()) { CH.insert(a); A[i] = a; ++i; } + } + + for (size_t i = 0; i < nums;) { + I b = dis(gen); + if (CH.find(b) == CH.end()) { CH.insert(b); B[i] = b; ++i; } + } + + for (size_t i = 0; i < nums; ++i) { + I a = A[i], b = B[i]; + + REQUIRE(a != b); + + II hash_ab = Slic3r::sla::pairhash(a, b); + II hash_ba = Slic3r::sla::pairhash(b, a); + REQUIRE(hash_ab == hash_ba); + + auto it = ints.find(hash_ab); + + if (it != ints.end()) { + REQUIRE(( + (it->second.first == a && it->second.second == b) || + (it->second.first == b && it->second.second == a) + )); + } else + ints[hash_ab] = std::make_pair(a, b); + } +} + +TEST_CASE("Pillar pairhash should be unique", "[suptreeutils]") { + test_pairhash(); + test_pairhash(); + test_pairhash(); + test_pairhash(); +} + +static void eval_ground_conn(const Slic3r::sla::GroundConnection &conn, + const Slic3r::sla::SupportableMesh &sm, + const Slic3r::sla::Junction &j, + double end_r, + const std::string &stl_fname = "output.stl") +{ + using namespace Slic3r; + +#ifndef NDEBUG + + sla::SupportTreeBuilder builder; + + if (!conn) + builder.add_junction(j); + + sla::build_ground_connection(builder, sm, conn); + + indexed_triangle_set mesh = *sm.emesh.get_triangle_mesh(); + its_merge(mesh, builder.merged_mesh()); + + its_write_stl_ascii(stl_fname.c_str(), "stl_fname", mesh); +#endif + + REQUIRE(bool(conn)); + + // The route should include the source and one avoidance junction. + REQUIRE(conn.path.size() == 2); + + // Check if the radius increases with each node + REQUIRE(conn.path.front().r < conn.path.back().r); + REQUIRE(conn.path.back().r < conn.pillar_base->r_top); + + // The end radius and the pillar base's upper radius should match + REQUIRE(conn.pillar_base->r_top == Approx(end_r)); +} + +TEST_CASE("Pillar search dumb case", "[suptreeutils]") { + using namespace Slic3r; + + constexpr double FromR = 0.5; + auto j = sla::Junction{Vec3d::Zero(), FromR}; + + SECTION("with empty mesh") { + sla::SupportableMesh sm{indexed_triangle_set{}, + sla::SupportPoints{}, + sla::SupportTreeConfig{}}; + + constexpr double EndR = 1.; + sla::GroundConnection conn = + sla::deepsearch_ground_connection(ex_seq, sm, j, EndR, sla::DOWN); + + REQUIRE(conn); + REQUIRE(conn.path.size() == 1); + REQUIRE(conn.pillar_base->pos.z() == Approx(ground_level(sm))); + } + + SECTION("with zero R source and destination") { + sla::SupportableMesh sm{indexed_triangle_set{}, + sla::SupportPoints{}, + sla::SupportTreeConfig{}}; + + j.r = 0.; + constexpr double EndR = 0.; + sla::GroundConnection conn = + sla::deepsearch_ground_connection(ex_seq, sm, j, EndR, sla::DOWN); + + REQUIRE(conn); + REQUIRE(conn.path.size() == 1); + REQUIRE(conn.pillar_base->pos.z() == Approx(ground_level(sm))); + REQUIRE(conn.pillar_base->r_top == Approx(0.)); + } + + SECTION("with zero init direction") { + sla::SupportableMesh sm{indexed_triangle_set{}, + sla::SupportPoints{}, + sla::SupportTreeConfig{}}; + + constexpr double EndR = 1.; + Vec3d init_dir = Vec3d::Zero(); + sla::GroundConnection conn = + sla::deepsearch_ground_connection(ex_seq, sm, j, EndR, init_dir); + + REQUIRE(conn); + REQUIRE(conn.path.size() == 1); + REQUIRE(conn.pillar_base->pos.z() == Approx(ground_level(sm))); + } +} TEST_CASE("Avoid disk below junction", "[suptreeutils]") { @@ -27,100 +177,34 @@ TEST_CASE("Avoid disk below junction", "[suptreeutils]") sla::SupportableMesh sm{disk, sla::SupportPoints{}, cfg}; - sla::GroundConnection conn = - sla::deepsearch_ground_connection(ex_seq, sm, j, EndRadius, sla::DOWN); + SECTION("without elevation") { -#ifndef NDEBUG + sla::GroundConnection conn = + sla::deepsearch_ground_connection(ex_seq, sm, j, EndRadius, sla::DOWN); - sla::SupportTreeBuilder builder; + eval_ground_conn(conn, sm, j, EndRadius, "disk.stl"); - if (!conn) - builder.add_junction(j); + // Check if the avoidance junction is indeed outside of the disk barrier's + // edge. + auto p = conn.path.back().pos; + double pR = std::sqrt(p.x() * p.x()) + std::sqrt(p.y() * p.y()); + REQUIRE(pR + FromRadius > CylRadius); + } - sla::build_ground_connection(builder, sm, conn); + SECTION("with elevation") { + sm.cfg.object_elevation_mm = 0.; - its_merge(disk, builder.merged_mesh()); + sla::GroundConnection conn = + sla::deepsearch_ground_connection(ex_seq, sm, j, EndRadius, sla::DOWN); - its_write_stl_ascii("output_disk.stl", "disk", disk); -#endif + eval_ground_conn(conn, sm, j, EndRadius, "disk_ze.stl"); - REQUIRE(bool(conn)); - - // The route should include the source and one avoidance junction. - REQUIRE(conn.path.size() == 2); - - // Check if the radius increases with each node - REQUIRE(conn.path.front().r < conn.path.back().r); - REQUIRE(conn.path.back().r < conn.pillar_base->r_top); - - // The end radius and the pillar base's upper radius should match - REQUIRE(conn.pillar_base->r_top == Approx(EndRadius)); - - // Check if the avoidance junction is indeed outside of the disk barrier's - // edge. - auto p = conn.path.back().pos; - double pR = std::sqrt(p.x() * p.x()) + std::sqrt(p.y() * p.y()); - REQUIRE(pR + FromRadius > CylRadius); -} - -TEST_CASE("Avoid disk below junction - Zero elevation", "[suptreeutils]") -{ - // In this test there will be a disk mesh with some radius, centered at - // (0, 0, 0) and above the disk, a junction from which the support pillar - // should be routed. The algorithm needs to find an avoidance route. - - using namespace Slic3r; - - constexpr double FromRadius = .5; - constexpr double EndRadius = 1.; - constexpr double CylRadius = 4.; - constexpr double CylHeight = 1.; - - sla::SupportTreeConfig cfg; - cfg.object_elevation_mm = 0.; - - indexed_triangle_set disk = its_make_cylinder(CylRadius, CylHeight); - - // 2.5 * CyRadius height should be enough to be able to insert a bridge - // with 45 degree tilt above the disk. - sla::Junction j{Vec3d{0., 0., 2.5 * CylRadius}, FromRadius}; - - sla::SupportableMesh sm{disk, sla::SupportPoints{}, cfg}; - - sla::GroundConnection conn = - sla::deepsearch_ground_connection(ex_seq, sm, j, EndRadius, sla::DOWN); - -#ifndef NDEBUG - - sla::SupportTreeBuilder builder; - - if (!conn) - builder.add_junction(j); - - sla::build_ground_connection(builder, sm, conn); - - its_merge(disk, builder.merged_mesh()); - - its_write_stl_ascii("output_disk_ze.stl", "disk", disk); -#endif - - REQUIRE(bool(conn)); - - // The route should include the source and one avoidance junction. - REQUIRE(conn.path.size() == 2); - - // Check if the radius increases with each node - REQUIRE(conn.path.front().r < conn.path.back().r); - REQUIRE(conn.path.back().r < conn.pillar_base->r_top); - - // The end radius and the pillar base's upper radius should match - REQUIRE(conn.pillar_base->r_top == Approx(EndRadius)); - - // Check if the avoidance junction is indeed outside of the disk barrier's - // edge. - auto p = conn.path.back().pos; - double pR = std::sqrt(p.x() * p.x()) + std::sqrt(p.y() * p.y()); - REQUIRE(pR + FromRadius > CylRadius); + // Check if the avoidance junction is indeed outside of the disk barrier's + // edge. + auto p = conn.path.back().pos; + double pR = std::sqrt(p.x() * p.x()) + std::sqrt(p.y() * p.y()); + REQUIRE(pR + FromRadius > CylRadius); + } } TEST_CASE("Avoid disk below junction with barrier on the side", "[suptreeutils]") @@ -150,38 +234,31 @@ TEST_CASE("Avoid disk below junction with barrier on the side", "[suptreeutils]" sla::SupportableMesh sm{disk, sla::SupportPoints{}, cfg}; - sla::GroundConnection conn = - sla::deepsearch_ground_connection(ex_seq, sm, j, EndRadius, sla::DOWN); + SECTION("without elevation") { + sla::GroundConnection conn = + sla::deepsearch_ground_connection(ex_seq, sm, j, EndRadius, sla::DOWN); -#ifndef NDEBUG + eval_ground_conn(conn, sm, j, EndRadius, "disk_with_barrier.stl"); - sla::SupportTreeBuilder builder; + // Check if the avoidance junction is indeed outside of the disk barrier's + // edge. + auto p = conn.path.back().pos; + double pR = std::sqrt(p.x() * p.x()) + std::sqrt(p.y() * p.y()); + REQUIRE(pR + FromRadius > CylRadius); + } - if (!conn) - builder.add_junction(j); + SECTION("without elevation") { + sm.cfg.object_elevation_mm = 0.; - sla::build_ground_connection(builder, sm, conn); + sla::GroundConnection conn = + sla::deepsearch_ground_connection(ex_seq, sm, j, EndRadius, sla::DOWN); - its_merge(disk, builder.merged_mesh()); + eval_ground_conn(conn, sm, j, EndRadius, "disk_with_barrier_ze.stl"); - its_write_stl_ascii("output_disk_wall.stl", "disk_wall", disk); -#endif - - REQUIRE(bool(conn)); - - // The route should include the source and one avoidance junction. - REQUIRE(conn.path.size() == 2); - - // Check if the radius increases with each node - REQUIRE(conn.path.front().r < conn.path.back().r); - REQUIRE(conn.path.back().r < conn.pillar_base->r_top); - - // The end radius end the pillar base's upper radius should match - REQUIRE(conn.pillar_base->r_top == Approx(EndRadius)); - - // Check if the avoidance junction is indeed outside of the disk barrier's - // edge. - auto p = conn.path.back().pos; - double pR = std::sqrt(p.x() * p.x()) + std::sqrt(p.y() * p.y()); - REQUIRE(pR + FromRadius > CylRadius); + // Check if the avoidance junction is indeed outside of the disk barrier's + // edge. + auto p = conn.path.back().pos; + double pR = std::sqrt(p.x() * p.x()) + std::sqrt(p.y() * p.y()); + REQUIRE(pR + FromRadius > CylRadius); + } } diff --git a/tests/sla_print/sla_test_utils.hpp b/tests/sla_print/sla_test_utils.hpp index 187a72d54..103b2f66a 100644 --- a/tests/sla_print/sla_test_utils.hpp +++ b/tests/sla_print/sla_test_utils.hpp @@ -14,14 +14,12 @@ #include "libslic3r/TriangleMesh.hpp" #include "libslic3r/SLA/Pad.hpp" #include "libslic3r/SLA/SupportTreeBuilder.hpp" -#include "libslic3r/SLA/SupportTreeUtils.hpp" #include "libslic3r/SLA/SupportPointGenerator.hpp" #include "libslic3r/SLA/AGGRaster.hpp" #include "libslic3r/SLA/ConcaveHull.hpp" #include "libslic3r/MTUtils.hpp" #include "libslic3r/SVG.hpp" -#include "libslic3r/Format/OBJ.hpp" using namespace Slic3r; @@ -111,58 +109,6 @@ inline void test_support_model_collision( test_support_model_collision(obj_filename, input_supportcfg, hcfg, {}); } -// Test pair hash for 'nums' random number pairs. -template void test_pairhash() -{ - const constexpr size_t nums = 1000; - I A[nums] = {0}, B[nums] = {0}; - std::unordered_set CH; - std::unordered_map> ints; - - std::random_device rd; - std::mt19937 gen(rd()); - - const I Ibits = int(sizeof(I) * CHAR_BIT); - const II IIbits = int(sizeof(II) * CHAR_BIT); - - int bits = IIbits / 2 < Ibits ? Ibits / 2 : Ibits; - if (std::is_signed::value) bits -= 1; - const I Imin = 0; - const I Imax = I(std::pow(2., bits) - 1); - - std::uniform_int_distribution dis(Imin, Imax); - - for (size_t i = 0; i < nums;) { - I a = dis(gen); - if (CH.find(a) == CH.end()) { CH.insert(a); A[i] = a; ++i; } - } - - for (size_t i = 0; i < nums;) { - I b = dis(gen); - if (CH.find(b) == CH.end()) { CH.insert(b); B[i] = b; ++i; } - } - - for (size_t i = 0; i < nums; ++i) { - I a = A[i], b = B[i]; - - REQUIRE(a != b); - - II hash_ab = sla::pairhash(a, b); - II hash_ba = sla::pairhash(b, a); - REQUIRE(hash_ab == hash_ba); - - auto it = ints.find(hash_ab); - - if (it != ints.end()) { - REQUIRE(( - (it->second.first == a && it->second.second == b) || - (it->second.first == b && it->second.second == a) - )); - } else - ints[hash_ab] = std::make_pair(a, b); - } -} - // SLA Raster test utils: using TPixel = uint8_t;