Upgrade support tree route search functions, add tests

This commit is contained in:
tamasmeszaros 2022-11-11 15:12:53 +01:00
parent 4dc0741766
commit 1e9bd28714
14 changed files with 385 additions and 76 deletions

View File

@ -20,6 +20,9 @@ class Properties
ExPolygons m_bed_shape;
public:
constexpr bool group_pillars() const noexcept { return false; }
// Maximum slope for bridges of the tree
Properties &max_slope(double val) noexcept
{

View File

@ -356,6 +356,8 @@ set(SLIC3R_SOURCES
BranchingTree/BranchingTree.hpp
BranchingTree/PointCloud.cpp
BranchingTree/PointCloud.hpp
OrganicTree/OrganicTree.hpp
OrganicTree/OrganicTreeImpl.hpp
Arachne/BeadingStrategy/BeadingStrategy.hpp
Arachne/BeadingStrategy/BeadingStrategy.cpp

View File

@ -13,7 +13,9 @@ template<size_t N>
long num_iter(const std::array<size_t, N> &idx, size_t gridsz)
{
long ret = 0;
for (size_t i = 0; i < N; ++i) ret += idx[i] * std::pow(gridsz, i);
for (size_t i = 0; i < N; ++i)
ret += idx[i] * std::pow(gridsz, i);
return ret;
}

View File

@ -158,7 +158,7 @@ public:
return optimize(nl, std::forward<Func>(func), initvals);
}
explicit NLoptOpt(StopCriteria stopcr = {}) : m_stopcr(stopcr) {}
explicit NLoptOpt(const StopCriteria &stopcr = {}) : m_stopcr(stopcr) {}
void set_criteria(const StopCriteria &cr) { m_stopcr = cr; }
const StopCriteria &get_criteria() const noexcept { return m_stopcr; }
@ -226,7 +226,7 @@ using AlgNLoptGenetic = detail::NLoptAlgComb<NLOPT_GN_ESCH>;
using AlgNLoptSubplex = detail::NLoptAlg<NLOPT_LN_SBPLX>;
using AlgNLoptSimplex = detail::NLoptAlg<NLOPT_LN_NELDERMEAD>;
using AlgNLoptDIRECT = detail::NLoptAlg<NLOPT_GN_DIRECT>;
using AlgNLoptMLSL = detail::NLoptAlg<NLOPT_GN_MLSL>;
using AlgNLoptMLSL = detail::NLoptAlgComb<NLOPT_GN_MLSL, NLOPT_LN_SBPLX>;
}} // namespace Slic3r::opt

View File

@ -0,0 +1,95 @@
#ifndef ORGANICTREE_HPP
#define ORGANICTREE_HPP
#include <type_traits>
#include <utility>
#include <optional>
namespace Slic3r { namespace organictree {
enum class NodeType { Bed, Mesh, Junction };
template <class T> struct DomainTraits_ {
using Node = typename T::Node;
static void push(const T &dom, const Node &n)
{
dom.push_junction(n);
}
static Node pop(T &dom) { return dom.pop(); }
static bool empty(const T &dom) { return dom.empty(); }
static std::optional<std::pair<Node, NodeType>>
closest(const T &dom, const Node &n)
{
return dom.closest(n);
}
static Node merge_node(const T &dom, const Node &a, const Node &b)
{
return dom.merge_node(a, b);
}
static void bridge(T &dom, const Node &from, const Node &to)
{
dom.bridge(from, to);
}
static void anchor(T &dom, const Node &from, const Node &to)
{
dom.anchor(from, to);
}
static void pillar(T &dom, const Node &from, const Node &to)
{
dom.pillar(from, to);
}
static void merge (T &dom, const Node &n1, const Node &n2, const Node &mrg)
{
dom.merge(n1, n2, mrg);
}
static void report_fail(T &dom, const Node &n) { dom.report_fail(n); }
};
template<class Domain>
void build_tree(Domain &&D)
{
using Dom = DomainTraits_<std::remove_cv_t<std::remove_reference_t<Domain>>>;
using Node = typename Dom::Node;
while (! Dom::empty(D)) {
Node n = Dom::pop(D);
std::optional<std::pair<Node, NodeType>> C = Dom::closest(D, n);
if (!C) {
Dom::report_fail(D, n);
} else switch (C->second) {
case NodeType::Bed:
Dom::pillar(D, n, C->first);
break;
case NodeType::Mesh:
Dom::anchor(D, n, C->first);
break;
case NodeType::Junction: {
Node M = Dom::merge_node(D, n, C->first);
if (M == C->first) {
Dom::bridge(D, n, C->first);
} else {
Dom::push(D, M);
Dom::merge(D, n, M, C->first);
}
break;
}
}
}
}
}} // namespace Slic3r::organictree
#endif // ORGANICTREE_HPP

View File

@ -0,0 +1,11 @@
#ifndef ORGANICTREEIMPL_HPP
#define ORGANICTREEIMPL_HPP
namespace Slic3r { namespace organictree {
}}
#endif // ORGANICTREEIMPL_HPP

View File

@ -253,13 +253,11 @@ bool BranchingTreeBuilder::add_ground_bridge(const branchingtree::Node &from,
// if (!result) {
auto conn = optimize_ground_connection(
ex_tbb,
m_builder,
m_sm,
j,
get_radius(to));
if (conn) {
// build_ground_connection(m_builder, m_sm, conn);
// Junction connlast = conn.path.back();
// branchingtree::Node n{connlast.pos.cast<float>(), float(connlast.r)};
// n.left = from.id;
@ -321,6 +319,17 @@ bool BranchingTreeBuilder::add_mesh_bridge(const branchingtree::Node &from,
return bool(anchor);
}
inline void build_pillars(SupportTreeBuilder &builder,
BranchingTreeBuilder &vbuilder,
const SupportableMesh &sm)
{
for (size_t pill_id = 0; pill_id < vbuilder.pillars().size(); ++pill_id) {
auto * conn = vbuilder.ground_conn(pill_id);
if (conn)
build_ground_connection(builder, sm, *conn);
}
}
void create_branching_tree(SupportTreeBuilder &builder, const SupportableMesh &sm)
{
auto coordfn = [&sm](size_t id, size_t dim) { return sm.pts[id].pos(dim); };
@ -373,6 +382,8 @@ void create_branching_tree(SupportTreeBuilder &builder, const SupportableMesh &s
BranchingTreeBuilder vbuilder{builder, sm, nodes};
branchingtree::build_tree(nodes, vbuilder);
if constexpr (props.group_pillars()) {
std::vector<branchingtree::Node> bedleafs;
for (auto n : vbuilder.pillars()) {
n.left = branchingtree::Node::ID_NONE;
@ -380,27 +391,13 @@ void create_branching_tree(SupportTreeBuilder &builder, const SupportableMesh &s
bedleafs.emplace_back(n);
}
props.max_branch_length(50.f);
auto gndsm = sm;
branchingtree::PointCloud gndnodes{{}, nodes.get_bedpoints(), bedleafs, props};
BranchingTreeBuilder gndbuilder{builder, sm, gndnodes};
branchingtree::build_tree(gndnodes, gndbuilder);
// All leafs of gndbuilder are nodes that already proved to be routable
// to the ground. gndbuilder should not encounter any unroutable nodes
// assert(gndbuilder.unroutable_pinheads().empty());
// for (size_t pill_id = 0; pill_id < vbuilder.pillars().size(); ++pill_id) {
// auto * conn = vbuilder.ground_conn(pill_id);
// if (conn)
// build_ground_connection(builder, sm, *conn);
// }
for (size_t pill_id = 0; pill_id < gndbuilder.pillars().size(); ++pill_id) {
auto * conn = gndbuilder.ground_conn(pill_id);
if (conn)
build_ground_connection(builder, sm, *conn);
build_pillars(builder, gndbuilder, sm);
} else {
build_pillars(builder, vbuilder, sm);
}
for (size_t id : vbuilder.unroutable_pinheads())

View File

@ -96,8 +96,8 @@ struct SupportTreeConfig
static const double constexpr max_solo_pillar_height_mm = 15.0;
static const double constexpr max_dual_pillar_height_mm = 35.0;
static const double constexpr optimizer_rel_score_diff = 1e-16;
static const unsigned constexpr optimizer_max_iterations = 20000;
static const double constexpr optimizer_rel_score_diff = 1e-10;
static const unsigned constexpr optimizer_max_iterations = 2000;
static const unsigned constexpr pillar_cascade_neighbors = 3;
};

View File

@ -165,7 +165,8 @@ indexed_triangle_set halfcone(double baseheight,
{
assert(steps > 0);
if (baseheight <= 0 || steps <= 0) return {};
if (baseheight <= 0 || steps <= 0 || (r_bottom <= 0. && r_top <= 0.))
return {};
indexed_triangle_set base;

View File

@ -6,6 +6,7 @@
#include <libslic3r/Execution/Execution.hpp>
#include <libslic3r/Optimize/NLoptOptimizer.hpp>
#include <libslic3r/Optimize/BruteforceOptimizer.hpp>
#include <libslic3r/MeshNormals.hpp>
#include <libslic3r/Geometry.hpp>
#include <libslic3r/SLA/SupportTreeBuilder.hpp>
@ -644,10 +645,9 @@ struct GroundConnection {
static constexpr size_t MaxExpectedJunctions = 3;
boost::container::small_vector<Junction, MaxExpectedJunctions> path;
double end_radius;
std::optional<Pedestal> pillar_base;
operator bool() const { return !path.empty(); }
operator bool() const { return pillar_base.has_value() && !path.empty(); }
};
template<class Ex>
@ -659,6 +659,8 @@ GroundConnection find_pillar_route(Ex policy,
{
GroundConnection ret;
ret.path.emplace_back(source);
Vec3d jp = source.pos, endp = jp, dir = sourcedir;
bool can_add_base = false/*, non_head = false*/;
@ -666,6 +668,7 @@ GroundConnection find_pillar_route(Ex policy,
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_mm;
double r2 = radius + (end_radius - radius) / (jp.z() - ground_level(sm));
@ -692,7 +695,6 @@ GroundConnection find_pillar_route(Ex policy,
sm.cfg.head_back_radius_mm);
if (diffbr && diffbr->endp.z() > jp_gnd) {
ret.path.emplace_back(source);
endp = diffbr->endp;
radius = diffbr->end_r;
ret.path.emplace_back(endp, radius);
@ -709,7 +711,6 @@ GroundConnection find_pillar_route(Ex policy,
auto [polar, azimuth] = dir_to_spheric(dir);
polar = PI - sm.cfg.bridge_slope;
Vec3d d = spheric_to_dir(polar, azimuth).normalized();
auto sd = sm.cfg.safety_distance_mm; //radius * sm.cfg.safety_distance_mm / sm.cfg.head_back_radius_mm;
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.;
@ -759,15 +760,19 @@ GroundConnection find_pillar_route(Ex policy,
}
Vec3d gp = to_floor(endp);
auto hit = beam_mesh_hit(policy, sm.emesh,
Beam{{endp, radius}, {gp, end_radius}}, sd);
ret.end_radius = end_radius;
if (std::isinf(hit.distance())) {
double base_radius = can_add_base ?
std::max(sm.cfg.base_radius_mm, end_radius) : end_radius;
if (can_add_base) {
Vec3d gp = to_floor(endp);
ret.pillar_base =
Pedestal{gp, sm.cfg.base_height_mm, sm.cfg.base_radius_mm, end_radius};
Pedestal{gp, sm.cfg.base_height_mm, base_radius, end_radius};
}
return ret; //{true, pillar_id};
return ret;
}
inline long build_ground_connection(SupportTreeBuilder &builder,
@ -798,9 +803,8 @@ inline long build_ground_connection(SupportTreeBuilder &builder,
// long head_id = std::abs(conn.path.back().id);
// ret = builder.add_pillar(head_id, h);
// } else
ret = builder.add_pillar(gp, h, conn.path.back().r, conn.end_radius);
ret = builder.add_pillar(gp, h, conn.path.back().r, conn.pillar_base->r_top);
if (conn.pillar_base)
builder.add_pillar_base(ret, conn.pillar_base->height, conn.pillar_base->r_bottom);
return ret;
@ -809,7 +813,6 @@ inline long build_ground_connection(SupportTreeBuilder &builder,
template<class Ex>
GroundConnection find_ground_connection(
Ex policy,
SupportTreeBuilder &builder,
const SupportableMesh &sm,
const Junction &j,
const Vec3d &dir,
@ -817,39 +820,36 @@ GroundConnection find_ground_connection(
{
auto hjp = j.pos;
double r = j.r;
auto sd = sm.cfg.safety_distance_mm; //r * sm.cfg.safety_distance_mm / sm.cfg.head_back_radius_mm;
auto sd = sm.cfg.safety_distance_mm;
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();
double d = 0, tdown = 0;
t = std::min(t,
sm.cfg.max_bridge_length_mm * r / sm.cfg.head_back_radius_mm);
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() - sm.emesh.ground_level()));
double pill_r = r + bridge_ratio * (end_r - r);
gnd_route = find_pillar_route(policy, sm, {endp, pill_r}, dir, end_r);
while (
d < t &&
!std::isinf(tdown = beam_mesh_hit(policy, sm.emesh,
Beam{hjp + d * dir, DOWN, r, r2}, sd)
.distance())) {
d += r;
}
GroundConnection ret;
ret.end_radius = end_r;
if (std::isinf(tdown)) {
ret.path.emplace_back(j);
Vec3d endp = hjp + d * dir;
double bridge_ratio = d / (d + (endp.z() - sm.emesh.ground_level()));
double pill_r = r + bridge_ratio * (end_r - r);
auto route = find_pillar_route(policy, sm, {endp, pill_r}, dir, end_r);
for (auto &j : route.path)
if (d > 0.)
ret.path.emplace_back(j);
ret.pillar_base = route.pillar_base;
ret.end_radius = end_r;
}
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 debugged
ret.pillar_base = gnd_route.pillar_base;
return ret;
}
@ -857,7 +857,6 @@ GroundConnection find_ground_connection(
template<class Ex>
GroundConnection optimize_ground_connection(
Ex policy,
SupportTreeBuilder &builder,
const SupportableMesh &sm,
const Junction &j,
double end_radius,
@ -865,8 +864,8 @@ GroundConnection optimize_ground_connection(
{
double downdst = j.pos.z() - ground_level(sm);
auto res = find_ground_connection(policy, builder, sm, j, init_dir, end_radius);
if (!res)
auto res = find_ground_connection(policy, sm, j, init_dir, end_radius);
if (res)
return res;
// Optimize bridge direction:
@ -874,7 +873,7 @@ GroundConnection optimize_ground_connection(
// direction out of the cavity.
auto [polar, azimuth] = dir_to_spheric(init_dir);
Optimizer<AlgNLoptGenetic> solver(get_criteria(sm.cfg).stop_score(1e6));
Optimizer<opt::AlgNLoptMLSL> solver(get_criteria(sm.cfg).stop_score(1e6));
solver.seed(0); // we want deterministic behavior
auto sd = /*j.r **/ sm.cfg.safety_distance_mm /*/ sm.cfg.head_back_radius_mm*/;
@ -891,7 +890,7 @@ GroundConnection optimize_ground_connection(
Vec3d bridgedir = spheric_to_dir(oresult.optimum).normalized();
return find_ground_connection(policy, builder, sm, j, bridgedir, end_radius);
return find_ground_connection(policy, sm, j, bridgedir, end_radius);
}
template<class Ex>
@ -904,7 +903,7 @@ std::pair<bool, long> connect_to_ground(Ex policy,
{
std::pair<bool, long> ret = {false, SupportTreeNode::ID_UNSET};
auto conn = find_ground_connection(policy, builder, sm, j, dir, end_r);
auto conn = find_ground_connection(policy, sm, j, dir, end_r);
ret.first = bool(conn);
ret.second = build_ground_connection(builder, sm, conn);
@ -921,8 +920,7 @@ std::pair<bool, long> search_ground_route(Ex policy,
{
std::pair<bool, long> ret = {false, SupportTreeNode::ID_UNSET};
auto conn = optimize_ground_connection(policy, builder, sm, j,
end_r, init_dir);
auto conn = optimize_ground_connection(policy, sm, j, end_r, init_dir);
ret.first = bool(conn);
ret.second = build_ground_connection(builder, sm, conn);

View File

@ -12,6 +12,8 @@
#include <wx/settings.h>
#include <wx/stattext.h>
#include <boost/log/trivial.hpp>
#include "slic3r/GUI/GUI_App.hpp"
#include "slic3r/GUI/GUI.hpp"
#include "slic3r/GUI/GUI_ObjectSettings.hpp"
@ -1019,6 +1021,8 @@ void GLGizmoSlaSupports::select_point(int i)
m_new_point_head_diameter = m_editing_cache[0].support_point.head_front_radius * 2.f;
}
else {
if (!m_editing_cache[i].selected)
BOOST_LOG_TRIVIAL(debug) << "Support point selected [" << i << "]: " << m_editing_cache[i].support_point.pos.transpose() << " \tnormal: " << m_editing_cache[i].normal.transpose();
m_editing_cache[i].selected = true;
m_selection_empty = false;
m_new_point_head_diameter = m_editing_cache[i].support_point.head_front_radius * 2.f;

76
tests/data/U_overhang.obj Normal file
View File

@ -0,0 +1,76 @@
####
#
# OBJ File Generated by Meshlab
#
####
# Object U_overhang.obj
#
# Vertices: 16
# Faces: 28
#
####
vn 1.570797 1.570796 1.570796
v 10.000000 10.000000 11.000000
vn 4.712389 1.570796 -1.570796
v 10.000000 1.000000 10.000000
vn 1.570796 1.570796 -1.570796
v 10.000000 10.000000 10.000000
vn 1.570796 -1.570796 1.570796
v 10.000000 0.000000 11.000000
vn 4.712389 1.570796 1.570796
v 10.000000 1.000000 1.000000
vn 1.570797 1.570796 -1.570796
v 10.000000 10.000000 0.000000
vn 1.570796 1.570796 1.570796
v 10.000000 10.000000 1.000000
vn 1.570796 -1.570796 -1.570796
v 10.000000 0.000000 0.000000
vn -1.570796 1.570796 1.570796
v 0.000000 10.000000 1.000000
vn -4.712389 1.570796 1.570796
v 0.000000 1.000000 1.000000
vn -1.570796 -1.570796 -1.570796
v 0.000000 0.000000 0.000000
vn -1.570797 1.570796 -1.570796
v 0.000000 10.000000 0.000000
vn -4.712389 1.570796 -1.570796
v 0.000000 1.000000 10.000000
vn -1.570797 1.570796 1.570796
v 0.000000 10.000000 11.000000
vn -1.570796 1.570796 -1.570796
v 0.000000 10.000000 10.000000
vn -1.570796 -1.570796 1.570796
v 0.000000 0.000000 11.000000
# 16 vertices, 0 vertices normals
f 1//1 2//2 3//3
f 2//2 4//4 5//5
f 4//4 2//2 1//1
f 5//5 6//6 7//7
f 5//5 8//8 6//6
f 8//8 5//5 4//4
f 9//9 5//5 7//7
f 5//5 9//9 10//10
f 11//11 6//6 8//8
f 6//6 11//11 12//12
f 12//12 10//10 9//9
f 10//10 11//11 13//13
f 11//11 10//10 12//12
f 13//13 14//14 15//15
f 13//13 16//16 14//14
f 16//16 13//13 11//11
f 6//6 9//9 7//7
f 9//9 6//6 12//12
f 11//11 4//4 16//16
f 4//4 11//11 8//8
f 13//13 3//3 2//2
f 3//3 13//13 15//15
f 5//5 13//13 2//2
f 13//13 5//5 10//10
f 14//14 4//4 1//1
f 4//4 14//14 16//16
f 3//3 14//14 1//1
f 14//14 3//3 15//15
# 28 faces, 0 coords texture
# End of File

View File

@ -4,6 +4,7 @@ add_executable(${_TEST_NAME}_tests ${_TEST_NAME}_tests_main.cpp
sla_test_utils.hpp sla_test_utils.cpp
sla_supptgen_tests.cpp
sla_raycast_tests.cpp
sla_supptreeutils_tests.cpp
sla_archive_readwrite_tests.cpp)
# mold linker for successful linking needs also to link TBB library and link it before libslic3r.

View File

@ -0,0 +1,119 @@
#include <catch2/catch.hpp>
#include <test_utils.hpp>
#include "libslic3r/Execution/ExecutionSeq.hpp"
#include "libslic3r/SLA/SupportTreeUtils.hpp"
TEST_CASE("Avoid disk below junction", "[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;
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::optimize_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.stl", "disk", disk);
#endif
REQUIRE(bool(conn));
// The route should include the source and one avoidance junction.
REQUIRE(conn.path.size() == 2);
// 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);
}
TEST_CASE("Avoid disk below junction with barrier on the side", "[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.;
constexpr double JElevX = 2.5;
sla::SupportTreeConfig cfg;
indexed_triangle_set disk = its_make_cylinder(CylRadius, CylHeight);
indexed_triangle_set wall = its_make_cube(1., 2 * CylRadius, JElevX * CylRadius);
its_translate(wall, Vec3f{float(FromRadius), -float(CylRadius), 0.f});
its_merge(disk, wall);
// 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., JElevX * CylRadius}, FromRadius};
sla::SupportableMesh sm{disk, sla::SupportPoints{}, cfg};
sla::GroundConnection conn =
sla::optimize_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_wall.stl", "disk_wall", disk);
#endif
REQUIRE(bool(conn));
// The route should include the source and one avoidance junction.
REQUIRE(conn.path.size() == 2);
// 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);
}