mirror of
https://github.com/FULU-Foundation/OrcaSlicer-bambulab.git
synced 2026-07-11 08:04:25 +00:00
8ca7e56d0f
Try to link tests on Mac. Fix inaccurate pad brim size fix build on mac (attempt 2) Fixes for support tree faults and race conditions in release mode. Fix crashing test executable on gcc 4.9 fix warning on msvc
1394 lines
51 KiB
C++
1394 lines
51 KiB
C++
#include "SLASupportTreeBuildsteps.hpp"
|
|
|
|
#include <libnest2d/optimizers/nlopt/genetic.hpp>
|
|
#include <libnest2d/optimizers/nlopt/subplex.hpp>
|
|
#include <boost/log/trivial.hpp>
|
|
|
|
namespace Slic3r {
|
|
namespace sla {
|
|
|
|
SupportTreeBuildsteps::SupportTreeBuildsteps(SupportTreeBuilder & builder,
|
|
const SupportableMesh &sm)
|
|
: m_cfg(sm.cfg)
|
|
, m_mesh(sm.emesh)
|
|
, m_support_pts(sm.pts)
|
|
, m_support_nmls(sm.pts.size(), 3)
|
|
, m_builder(builder)
|
|
, m_points(sm.pts.size(), 3)
|
|
, m_thr(builder.ctl().cancelfn)
|
|
{
|
|
// Prepare the support points in Eigen/IGL format as well, we will use
|
|
// it mostly in this form.
|
|
|
|
long i = 0;
|
|
for (const SupportPoint &sp : m_support_pts) {
|
|
m_points.row(i)(X) = double(sp.pos(X));
|
|
m_points.row(i)(Y) = double(sp.pos(Y));
|
|
m_points.row(i)(Z) = double(sp.pos(Z));
|
|
++i;
|
|
}
|
|
}
|
|
|
|
bool SupportTreeBuildsteps::execute(SupportTreeBuilder & builder,
|
|
const SupportableMesh &sm)
|
|
{
|
|
if(sm.pts.empty()) return false;
|
|
|
|
SupportTreeBuildsteps alg(builder, sm);
|
|
|
|
// Let's define the individual steps of the processing. We can experiment
|
|
// later with the ordering and the dependencies between them.
|
|
enum Steps {
|
|
BEGIN,
|
|
FILTER,
|
|
PINHEADS,
|
|
CLASSIFY,
|
|
ROUTING_GROUND,
|
|
ROUTING_NONGROUND,
|
|
CASCADE_PILLARS,
|
|
HEADLESS,
|
|
MERGE_RESULT,
|
|
DONE,
|
|
ABORT,
|
|
NUM_STEPS
|
|
//...
|
|
};
|
|
|
|
// Collect the algorithm steps into a nice sequence
|
|
std::array<std::function<void()>, NUM_STEPS> program = {
|
|
[] () {
|
|
// Begin...
|
|
// Potentially clear up the shared data (not needed for now)
|
|
},
|
|
|
|
std::bind(&SupportTreeBuildsteps::filter, &alg),
|
|
|
|
std::bind(&SupportTreeBuildsteps::add_pinheads, &alg),
|
|
|
|
std::bind(&SupportTreeBuildsteps::classify, &alg),
|
|
|
|
std::bind(&SupportTreeBuildsteps::routing_to_ground, &alg),
|
|
|
|
std::bind(&SupportTreeBuildsteps::routing_to_model, &alg),
|
|
|
|
std::bind(&SupportTreeBuildsteps::interconnect_pillars, &alg),
|
|
|
|
std::bind(&SupportTreeBuildsteps::routing_headless, &alg),
|
|
|
|
std::bind(&SupportTreeBuildsteps::merge_result, &alg),
|
|
|
|
[] () {
|
|
// Done
|
|
},
|
|
|
|
[] () {
|
|
// Abort
|
|
}
|
|
};
|
|
|
|
Steps pc = BEGIN;
|
|
|
|
if(sm.cfg.ground_facing_only) {
|
|
program[ROUTING_NONGROUND] = []() {
|
|
BOOST_LOG_TRIVIAL(info)
|
|
<< "Skipping model-facing supports as requested.";
|
|
};
|
|
program[HEADLESS] = []() {
|
|
BOOST_LOG_TRIVIAL(info) << "Skipping headless stick generation as"
|
|
" requested.";
|
|
};
|
|
}
|
|
|
|
// Let's define a simple automaton that will run our program.
|
|
auto progress = [&builder, &pc] () {
|
|
static const std::array<std::string, NUM_STEPS> stepstr {
|
|
"Starting",
|
|
"Filtering",
|
|
"Generate pinheads",
|
|
"Classification",
|
|
"Routing to ground",
|
|
"Routing supports to model surface",
|
|
"Interconnecting pillars",
|
|
"Processing small holes",
|
|
"Merging support mesh",
|
|
"Done",
|
|
"Abort"
|
|
};
|
|
|
|
static const std::array<unsigned, NUM_STEPS> stepstate {
|
|
0,
|
|
10,
|
|
30,
|
|
50,
|
|
60,
|
|
70,
|
|
80,
|
|
85,
|
|
99,
|
|
100,
|
|
0
|
|
};
|
|
|
|
if(builder.ctl().stopcondition()) pc = ABORT;
|
|
|
|
switch(pc) {
|
|
case BEGIN: pc = FILTER; break;
|
|
case FILTER: pc = PINHEADS; break;
|
|
case PINHEADS: pc = CLASSIFY; break;
|
|
case CLASSIFY: pc = ROUTING_GROUND; break;
|
|
case ROUTING_GROUND: pc = ROUTING_NONGROUND; break;
|
|
case ROUTING_NONGROUND: pc = CASCADE_PILLARS; break;
|
|
case CASCADE_PILLARS: pc = HEADLESS; break;
|
|
case HEADLESS: pc = MERGE_RESULT; break;
|
|
case MERGE_RESULT: pc = DONE; break;
|
|
case DONE:
|
|
case ABORT: break;
|
|
default: ;
|
|
}
|
|
|
|
builder.ctl().statuscb(stepstate[pc], stepstr[pc]);
|
|
};
|
|
|
|
// Just here we run the computation...
|
|
while(pc < DONE) {
|
|
progress();
|
|
program[pc]();
|
|
}
|
|
|
|
return pc == ABORT;
|
|
}
|
|
|
|
EigenMesh3D::hit_result SupportTreeBuildsteps::pinhead_mesh_intersect(
|
|
const Vec3d &s, const Vec3d &dir, double r_pin, double r_back, double width)
|
|
{
|
|
static const size_t SAMPLES = 8;
|
|
|
|
// method based on:
|
|
// https://math.stackexchange.com/questions/73237/parametric-equation-of-a-circle-in-3d-space
|
|
|
|
// We will shoot multiple rays from the head pinpoint in the direction
|
|
// of the pinhead robe (side) surface. The result will be the smallest
|
|
// hit distance.
|
|
|
|
// Move away slightly from the touching point to avoid raycasting on the
|
|
// inner surface of the mesh.
|
|
Vec3d v = dir; // Our direction (axis)
|
|
Vec3d c = s + width * dir;
|
|
const double& sd = m_cfg.safety_distance_mm;
|
|
|
|
// Two vectors that will be perpendicular to each other and to the
|
|
// axis. Values for a(X) and a(Y) are now arbitrary, a(Z) is just a
|
|
// placeholder.
|
|
Vec3d a(0, 1, 0), b;
|
|
|
|
// The portions of the circle (the head-back circle) for which we will
|
|
// shoot rays.
|
|
std::array<double, SAMPLES> phis;
|
|
for(size_t i = 0; i < phis.size(); ++i) phis[i] = i*2*PI/phis.size();
|
|
|
|
auto& m = m_mesh;
|
|
using HitResult = EigenMesh3D::hit_result;
|
|
|
|
// Hit results
|
|
std::array<HitResult, SAMPLES> hits;
|
|
|
|
// We have to address the case when the direction vector v (same as
|
|
// dir) is coincident with one of the world axes. In this case two of
|
|
// its components will be completely zero and one is 1.0. Our method
|
|
// becomes dangerous here due to division with zero. Instead, vector
|
|
// 'a' can be an element-wise rotated version of 'v'
|
|
auto chk1 = [] (double val) {
|
|
return std::abs(std::abs(val) - 1) < 1e-20;
|
|
};
|
|
|
|
if(chk1(v(X)) || chk1(v(Y)) || chk1(v(Z))) {
|
|
a = {v(Z), v(X), v(Y)};
|
|
b = {v(Y), v(Z), v(X)};
|
|
}
|
|
else {
|
|
a(Z) = -(v(Y)*a(Y)) / v(Z); a.normalize();
|
|
b = a.cross(v);
|
|
}
|
|
|
|
// Now a and b vectors are perpendicular to v and to each other.
|
|
// Together they define the plane where we have to iterate with the
|
|
// given angles in the 'phis' vector
|
|
ccr_par::enumerate(
|
|
phis.begin(), phis.end(),
|
|
[&hits, &m, sd, r_pin, r_back, s, a, b, c](double phi, size_t i) {
|
|
double sinphi = std::sin(phi);
|
|
double cosphi = std::cos(phi);
|
|
|
|
// Let's have a safety coefficient for the radiuses.
|
|
double rpscos = (sd + r_pin) * cosphi;
|
|
double rpssin = (sd + r_pin) * sinphi;
|
|
double rpbcos = (sd + r_back) * cosphi;
|
|
double rpbsin = (sd + r_back) * sinphi;
|
|
|
|
// Point on the circle on the pin sphere
|
|
Vec3d ps(s(X) + rpscos * a(X) + rpssin * b(X),
|
|
s(Y) + rpscos * a(Y) + rpssin * b(Y),
|
|
s(Z) + rpscos * a(Z) + rpssin * b(Z));
|
|
|
|
// Point ps is not on mesh but can be inside or
|
|
// outside as well. This would cause many problems
|
|
// with ray-casting. To detect the position we will
|
|
// use the ray-casting result (which has an is_inside
|
|
// predicate).
|
|
|
|
// This is the point on the circle on the back sphere
|
|
Vec3d p(c(X) + rpbcos * a(X) + rpbsin * b(X),
|
|
c(Y) + rpbcos * a(Y) + rpbsin * b(Y),
|
|
c(Z) + rpbcos * a(Z) + rpbsin * b(Z));
|
|
|
|
Vec3d n = (p - ps).normalized();
|
|
auto q = m.query_ray_hit(ps + sd * n, n);
|
|
|
|
if (q.is_inside()) { // the hit is inside the model
|
|
if (q.distance() > r_pin + sd) {
|
|
// If we are inside the model and the hit
|
|
// distance is bigger than our pin circle
|
|
// diameter, it probably indicates that the
|
|
// support point was already inside the
|
|
// model, or there is really no space
|
|
// around the point. We will assign a zero
|
|
// hit distance to these cases which will
|
|
// enforce the function return value to be
|
|
// an invalid ray with zero hit distance.
|
|
// (see min_element at the end)
|
|
hits[i] = HitResult(0.0);
|
|
} else {
|
|
// re-cast the ray from the outside of the
|
|
// object. The starting point has an offset
|
|
// of 2*safety_distance because the
|
|
// original ray has also had an offset
|
|
auto q2 = m.query_ray_hit(
|
|
ps + (q.distance() + 2 * sd) * n, n);
|
|
hits[i] = q2;
|
|
}
|
|
} else
|
|
hits[i] = q;
|
|
});
|
|
|
|
auto mit = std::min_element(hits.begin(), hits.end());
|
|
|
|
return *mit;
|
|
}
|
|
|
|
EigenMesh3D::hit_result SupportTreeBuildsteps::bridge_mesh_intersect(
|
|
const Vec3d &s, const Vec3d &dir, double r, bool ins_check)
|
|
{
|
|
static const size_t SAMPLES = 8;
|
|
|
|
// helper vector calculations
|
|
Vec3d a(0, 1, 0), b;
|
|
const double& sd = m_cfg.safety_distance_mm;
|
|
|
|
// INFO: for explanation of the method used here, see the previous
|
|
// method's comments.
|
|
|
|
auto chk1 = [] (double val) {
|
|
return std::abs(std::abs(val) - 1) < 1e-20;
|
|
};
|
|
|
|
if(chk1(dir(X)) || chk1(dir(Y)) || chk1(dir(Z))) {
|
|
a = {dir(Z), dir(X), dir(Y)};
|
|
b = {dir(Y), dir(Z), dir(X)};
|
|
}
|
|
else {
|
|
a(Z) = -(dir(Y)*a(Y)) / dir(Z); a.normalize();
|
|
b = a.cross(dir);
|
|
}
|
|
|
|
// circle portions
|
|
std::array<double, SAMPLES> phis;
|
|
for(size_t i = 0; i < phis.size(); ++i) phis[i] = i*2*PI/phis.size();
|
|
|
|
auto& m = m_mesh;
|
|
using HitResult = EigenMesh3D::hit_result;
|
|
|
|
// Hit results
|
|
std::array<HitResult, SAMPLES> hits;
|
|
|
|
ccr_par::enumerate(
|
|
phis.begin(), phis.end(),
|
|
[&m, a, b, sd, dir, r, s, ins_check, &hits] (double phi, size_t i) {
|
|
double sinphi = std::sin(phi);
|
|
double cosphi = std::cos(phi);
|
|
|
|
// Let's have a safety coefficient for the radiuses.
|
|
double rcos = (sd + r) * cosphi;
|
|
double rsin = (sd + r) * sinphi;
|
|
|
|
// Point on the circle on the pin sphere
|
|
Vec3d p (s(X) + rcos * a(X) + rsin * b(X),
|
|
s(Y) + rcos * a(Y) + rsin * b(Y),
|
|
s(Z) + rcos * a(Z) + rsin * b(Z));
|
|
|
|
auto hr = m.query_ray_hit(p + sd*dir, dir);
|
|
|
|
if(ins_check && hr.is_inside()) {
|
|
if(hr.distance() > 2 * r + sd) hits[i] = HitResult(0.0);
|
|
else {
|
|
// re-cast the ray from the outside of the object
|
|
auto hr2 =
|
|
m.query_ray_hit(p + (hr.distance() + 2*sd)*dir, dir);
|
|
|
|
hits[i] = hr2;
|
|
}
|
|
} else hits[i] = hr;
|
|
});
|
|
|
|
auto mit = std::min_element(hits.begin(), hits.end());
|
|
|
|
return *mit;
|
|
}
|
|
|
|
bool SupportTreeBuildsteps::interconnect(const Pillar &pillar,
|
|
const Pillar &nextpillar)
|
|
{
|
|
// We need to get the starting point of the zig-zag pattern. We have to
|
|
// be aware that the two head junctions are at different heights. We
|
|
// may start from the lowest junction and call it a day but this
|
|
// strategy would leave unconnected a lot of pillar duos where the
|
|
// shorter pillar is too short to start a new bridge but the taller
|
|
// pillar could still be bridged with the shorter one.
|
|
bool was_connected = false;
|
|
|
|
Vec3d supper = pillar.startpoint();
|
|
Vec3d slower = nextpillar.startpoint();
|
|
Vec3d eupper = pillar.endpoint();
|
|
Vec3d elower = nextpillar.endpoint();
|
|
|
|
double zmin = m_builder.ground_level + m_cfg.base_height_mm;
|
|
eupper(Z) = std::max(eupper(Z), zmin);
|
|
elower(Z) = std::max(elower(Z), zmin);
|
|
|
|
// The usable length of both pillars should be positive
|
|
if(slower(Z) - elower(Z) < 0) return false;
|
|
if(supper(Z) - eupper(Z) < 0) return false;
|
|
|
|
double pillar_dist = distance(Vec2d{slower(X), slower(Y)},
|
|
Vec2d{supper(X), supper(Y)});
|
|
double bridge_distance = pillar_dist / std::cos(-m_cfg.bridge_slope);
|
|
double zstep = pillar_dist * std::tan(-m_cfg.bridge_slope);
|
|
|
|
if(pillar_dist < 2 * m_cfg.head_back_radius_mm ||
|
|
pillar_dist > m_cfg.max_pillar_link_distance_mm) return false;
|
|
|
|
if(supper(Z) < slower(Z)) supper.swap(slower);
|
|
if(eupper(Z) < elower(Z)) eupper.swap(elower);
|
|
|
|
double startz = 0, endz = 0;
|
|
|
|
startz = slower(Z) - zstep < supper(Z) ? slower(Z) - zstep : slower(Z);
|
|
endz = eupper(Z) + zstep > elower(Z) ? eupper(Z) + zstep : eupper(Z);
|
|
|
|
if(slower(Z) - eupper(Z) < std::abs(zstep)) {
|
|
// no space for even one cross
|
|
|
|
// Get max available space
|
|
startz = std::min(supper(Z), slower(Z) - zstep);
|
|
endz = std::max(eupper(Z) + zstep, elower(Z));
|
|
|
|
// Align to center
|
|
double available_dist = (startz - endz);
|
|
double rounds = std::floor(available_dist / std::abs(zstep));
|
|
startz -= 0.5 * (available_dist - rounds * std::abs(zstep));
|
|
}
|
|
|
|
auto pcm = m_cfg.pillar_connection_mode;
|
|
bool docrosses =
|
|
pcm == PillarConnectionMode::cross ||
|
|
(pcm == PillarConnectionMode::dynamic &&
|
|
pillar_dist > 2*m_cfg.base_radius_mm);
|
|
|
|
// 'sj' means starting junction, 'ej' is the end junction of a bridge.
|
|
// They will be swapped in every iteration thus the zig-zag pattern.
|
|
// According to a config parameter, a second bridge may be added which
|
|
// results in a cross connection between the pillars.
|
|
Vec3d sj = supper, ej = slower; sj(Z) = startz; ej(Z) = sj(Z) + zstep;
|
|
|
|
// TODO: This is a workaround to not have a faulty last bridge
|
|
while(ej(Z) >= eupper(Z) /*endz*/) {
|
|
if(bridge_mesh_intersect(sj, dirv(sj, ej), pillar.r) >= bridge_distance)
|
|
{
|
|
m_builder.add_crossbridge(sj, ej, pillar.r);
|
|
was_connected = true;
|
|
}
|
|
|
|
// double bridging: (crosses)
|
|
if(docrosses) {
|
|
Vec3d sjback(ej(X), ej(Y), sj(Z));
|
|
Vec3d ejback(sj(X), sj(Y), ej(Z));
|
|
if (sjback(Z) <= slower(Z) && ejback(Z) >= eupper(Z) &&
|
|
bridge_mesh_intersect(sjback, dirv(sjback, ejback),
|
|
pillar.r) >= bridge_distance) {
|
|
// need to check collision for the cross stick
|
|
m_builder.add_crossbridge(sjback, ejback, pillar.r);
|
|
was_connected = true;
|
|
}
|
|
}
|
|
|
|
sj.swap(ej);
|
|
ej(Z) = sj(Z) + zstep;
|
|
}
|
|
|
|
return was_connected;
|
|
}
|
|
|
|
bool SupportTreeBuildsteps::connect_to_nearpillar(const Head &head,
|
|
long nearpillar_id)
|
|
{
|
|
auto nearpillar = [this, nearpillar_id]() {
|
|
return m_builder.pillar(nearpillar_id);
|
|
};
|
|
|
|
if (m_builder.bridgecount(nearpillar()) > m_cfg.max_bridges_on_pillar)
|
|
return false;
|
|
|
|
Vec3d headjp = head.junction_point();
|
|
Vec3d nearjp_u = nearpillar().startpoint();
|
|
Vec3d nearjp_l = nearpillar().endpoint();
|
|
|
|
double r = head.r_back_mm;
|
|
double d2d = distance(to_2d(headjp), to_2d(nearjp_u));
|
|
double d3d = distance(headjp, nearjp_u);
|
|
|
|
double hdiff = nearjp_u(Z) - headjp(Z);
|
|
double slope = std::atan2(hdiff, d2d);
|
|
|
|
Vec3d bridgestart = headjp;
|
|
Vec3d bridgeend = nearjp_u;
|
|
double max_len = m_cfg.max_bridge_length_mm;
|
|
double max_slope = m_cfg.bridge_slope;
|
|
double zdiff = 0.0;
|
|
|
|
// check the default situation if feasible for a bridge
|
|
if(d3d > max_len || slope > -max_slope) {
|
|
// not feasible to connect the two head junctions. We have to search
|
|
// for a suitable touch point.
|
|
|
|
double Zdown = headjp(Z) + d2d * std::tan(-max_slope);
|
|
Vec3d touchjp = bridgeend; touchjp(Z) = Zdown;
|
|
double D = distance(headjp, touchjp);
|
|
zdiff = Zdown - nearjp_u(Z);
|
|
|
|
if(zdiff > 0) {
|
|
Zdown -= zdiff;
|
|
bridgestart(Z) -= zdiff;
|
|
touchjp(Z) = Zdown;
|
|
|
|
double t = bridge_mesh_intersect(headjp, {0,0,-1}, r);
|
|
|
|
// We can't insert a pillar under the source head to connect
|
|
// with the nearby pillar's starting junction
|
|
if(t < zdiff) return false;
|
|
}
|
|
|
|
if(Zdown <= nearjp_u(Z) && Zdown >= nearjp_l(Z) && D < max_len)
|
|
bridgeend(Z) = Zdown;
|
|
else
|
|
return false;
|
|
}
|
|
|
|
// There will be a minimum distance from the ground where the
|
|
// bridge is allowed to connect. This is an empiric value.
|
|
double minz = m_builder.ground_level + 2 * m_cfg.head_width_mm;
|
|
if(bridgeend(Z) < minz) return false;
|
|
|
|
double t = bridge_mesh_intersect(bridgestart,
|
|
dirv(bridgestart, bridgeend), r);
|
|
|
|
// Cannot insert the bridge. (further search might not worth the hassle)
|
|
if(t < distance(bridgestart, bridgeend)) return false;
|
|
|
|
std::lock_guard<ccr::BlockingMutex> lk(m_bridge_mutex);
|
|
|
|
if (m_builder.bridgecount(nearpillar()) < m_cfg.max_bridges_on_pillar) {
|
|
// A partial pillar is needed under the starting head.
|
|
if(zdiff > 0) {
|
|
m_builder.add_pillar(unsigned(head.id), bridgestart, r);
|
|
m_builder.add_junction(bridgestart, r);
|
|
}
|
|
|
|
auto &br = m_builder.add_bridge(bridgestart, bridgeend, r);
|
|
m_builder.increment_bridges(nearpillar());
|
|
if (head.pillar_id == ID_UNSET)
|
|
m_builder.head(unsigned(head.id)).bridge_id = br.id;
|
|
|
|
} else return false;
|
|
|
|
return true;
|
|
}
|
|
|
|
bool SupportTreeBuildsteps::search_pillar_and_connect(const Head &head)
|
|
{
|
|
PointIndex spindex = m_pillar_index.guarded_clone();
|
|
|
|
long nearest_id = ID_UNSET;
|
|
|
|
Vec3d querypoint = head.junction_point();
|
|
|
|
while(nearest_id < 0 && !spindex.empty()) { m_thr();
|
|
// loop until a suitable head is not found
|
|
// if there is a pillar closer than the cluster center
|
|
// (this may happen as the clustering is not perfect)
|
|
// than we will bridge to this closer pillar
|
|
|
|
Vec3d qp(querypoint(X), querypoint(Y), m_builder.ground_level);
|
|
auto qres = spindex.nearest(qp, 1);
|
|
if(qres.empty()) break;
|
|
|
|
auto ne = qres.front();
|
|
nearest_id = ne.second;
|
|
|
|
if(nearest_id >= 0) {
|
|
auto nearpillarID = unsigned(nearest_id);
|
|
if(nearpillarID < m_builder.pillarcount()) {
|
|
if(!connect_to_nearpillar(head, nearpillarID)) {
|
|
nearest_id = ID_UNSET; // continue searching
|
|
spindex.remove(ne); // without the current pillar
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return nearest_id >= 0;
|
|
}
|
|
|
|
void SupportTreeBuildsteps::create_ground_pillar(const Vec3d &jp,
|
|
const Vec3d &sourcedir,
|
|
double radius,
|
|
long head_id)
|
|
{
|
|
// People were killed for this number (seriously)
|
|
static const double SQR2 = std::sqrt(2.0);
|
|
static const Vec3d DOWN = {0.0, 0.0, -1.0};
|
|
|
|
double gndlvl = m_builder.ground_level;
|
|
Vec3d endp = {jp(X), jp(Y), gndlvl};
|
|
double sd = m_cfg.pillar_base_safety_distance_mm;
|
|
long pillar_id = ID_UNSET;
|
|
double min_dist = sd + m_cfg.base_radius_mm + EPSILON;
|
|
double dist = 0;
|
|
bool can_add_base = true;
|
|
bool normal_mode = true;
|
|
|
|
if (m_cfg.object_elevation_mm < EPSILON
|
|
&& (dist = std::sqrt(m_mesh.squared_distance(endp))) < min_dist) {
|
|
// Get the distance from the mesh. This can be later optimized
|
|
// to get the distance in 2D plane because we are dealing with
|
|
// the ground level only.
|
|
|
|
normal_mode = false;
|
|
double mind = min_dist - dist;
|
|
double azimuth = std::atan2(sourcedir(Y), sourcedir(X));
|
|
double sinpolar = std::sin(PI - m_cfg.bridge_slope);
|
|
double cospolar = std::cos(PI - m_cfg.bridge_slope);
|
|
double cosazm = std::cos(azimuth);
|
|
double sinazm = std::sin(azimuth);
|
|
|
|
auto dir = Vec3d(cosazm * sinpolar, sinazm * sinpolar, cospolar)
|
|
.normalized();
|
|
|
|
using namespace libnest2d::opt;
|
|
StopCriteria scr;
|
|
scr.stop_score = min_dist;
|
|
SubplexOptimizer solver(scr);
|
|
|
|
auto result = solver.optimize_max(
|
|
[this, dir, jp, gndlvl](double mv) {
|
|
Vec3d endpt = jp + SQR2 * mv * dir;
|
|
endpt(Z) = gndlvl;
|
|
return std::sqrt(m_mesh.squared_distance(endpt));
|
|
},
|
|
initvals(mind), bound(0.0, 2 * min_dist));
|
|
|
|
mind = std::get<0>(result.optimum);
|
|
endp = jp + SQR2 * mind * dir;
|
|
Vec3d pgnd = {endp(X), endp(Y), gndlvl};
|
|
can_add_base = result.score > min_dist;
|
|
|
|
double gnd_offs = m_mesh.ground_level_offset();
|
|
auto abort_in_shame =
|
|
[gnd_offs, &normal_mode, &can_add_base, &endp, jp, gndlvl]()
|
|
{
|
|
normal_mode = true;
|
|
can_add_base = false; // Nothing left to do, hope for the best
|
|
endp = {jp(X), jp(Y), gndlvl - gnd_offs };
|
|
};
|
|
|
|
// We have to check if the bridge is feasible.
|
|
if (bridge_mesh_intersect(jp, dir, radius) < (endp - jp).norm())
|
|
abort_in_shame();
|
|
else {
|
|
// If the new endpoint is below ground, do not make a pillar
|
|
if (endp(Z) < gndlvl)
|
|
endp = endp - SQR2 * (gndlvl - endp(Z)) * dir; // back off
|
|
else {
|
|
|
|
auto hit = bridge_mesh_intersect(endp, DOWN, radius);
|
|
if (!std::isinf(hit.distance())) abort_in_shame();
|
|
|
|
Pillar &plr = m_builder.add_pillar(endp, pgnd, radius);
|
|
|
|
if (can_add_base)
|
|
plr.add_base(m_cfg.base_height_mm,
|
|
m_cfg.base_radius_mm);
|
|
|
|
pillar_id = plr.id;
|
|
}
|
|
|
|
m_builder.add_bridge(jp, endp, radius);
|
|
m_builder.add_junction(endp, radius);
|
|
|
|
// Add a degenerated pillar and the bridge.
|
|
// The degenerate pillar will have zero length and it will
|
|
// prevent from queries of head_pillar() to have non-existing
|
|
// pillar when the head should have one.
|
|
if (head_id >= 0)
|
|
m_builder.add_pillar(unsigned(head_id), jp, radius);
|
|
}
|
|
}
|
|
|
|
if (normal_mode) {
|
|
Pillar &plr = head_id >= 0
|
|
? m_builder.add_pillar(unsigned(head_id), endp, radius)
|
|
: m_builder.add_pillar(jp, endp, radius);
|
|
|
|
if (can_add_base)
|
|
plr.add_base(m_cfg.base_height_mm, m_cfg.base_radius_mm);
|
|
|
|
pillar_id = plr.id;
|
|
}
|
|
|
|
if(pillar_id >= 0) // Save the pillar endpoint in the spatial index
|
|
m_pillar_index.guarded_insert(endp, unsigned(pillar_id));
|
|
}
|
|
|
|
void SupportTreeBuildsteps::filter()
|
|
{
|
|
// Get the points that are too close to each other and keep only the
|
|
// first one
|
|
auto aliases = cluster(m_points, D_SP, 2);
|
|
|
|
PtIndices filtered_indices;
|
|
filtered_indices.reserve(aliases.size());
|
|
m_iheads.reserve(aliases.size());
|
|
m_iheadless.reserve(aliases.size());
|
|
for(auto& a : aliases) {
|
|
// Here we keep only the front point of the cluster.
|
|
filtered_indices.emplace_back(a.front());
|
|
}
|
|
|
|
// calculate the normals to the triangles for filtered points
|
|
auto nmls = sla::normals(m_points, m_mesh, m_cfg.head_front_radius_mm,
|
|
m_thr, filtered_indices);
|
|
|
|
// Not all of the support points have to be a valid position for
|
|
// support creation. The angle may be inappropriate or there may
|
|
// not be enough space for the pinhead. Filtering is applied for
|
|
// these reasons.
|
|
|
|
using libnest2d::opt::bound;
|
|
using libnest2d::opt::initvals;
|
|
using libnest2d::opt::GeneticOptimizer;
|
|
using libnest2d::opt::StopCriteria;
|
|
|
|
ccr::SpinningMutex mutex;
|
|
auto addfn = [&mutex](PtIndices &container, unsigned val) {
|
|
std::lock_guard<ccr::SpinningMutex> lk(mutex);
|
|
container.emplace_back(val);
|
|
};
|
|
|
|
auto filterfn = [this, &nmls, addfn](unsigned fidx, size_t i) {
|
|
m_thr();
|
|
|
|
auto n = nmls.row(Eigen::Index(i));
|
|
|
|
// for all normals we generate the spherical coordinates and
|
|
// saturate the polar angle to 45 degrees from the bottom then
|
|
// convert back to standard coordinates to get the new normal.
|
|
// Then we just create a quaternion from the two normals
|
|
// (Quaternion::FromTwoVectors) and apply the rotation to the
|
|
// arrow head.
|
|
|
|
double z = n(2);
|
|
double r = 1.0; // for normalized vector
|
|
double polar = std::acos(z / r);
|
|
double azimuth = std::atan2(n(1), n(0));
|
|
|
|
// skip if the tilt is not sane
|
|
if(polar >= PI - m_cfg.normal_cutoff_angle) {
|
|
|
|
// We saturate the polar angle to 3pi/4
|
|
polar = std::max(polar, 3*PI / 4);
|
|
|
|
// save the head (pinpoint) position
|
|
Vec3d hp = m_points.row(fidx);
|
|
|
|
double w = m_cfg.head_width_mm +
|
|
m_cfg.head_back_radius_mm +
|
|
2*m_cfg.head_front_radius_mm;
|
|
|
|
double pin_r = double(m_support_pts[fidx].head_front_radius);
|
|
|
|
// Reassemble the now corrected normal
|
|
auto nn = Vec3d(std::cos(azimuth) * std::sin(polar),
|
|
std::sin(azimuth) * std::sin(polar),
|
|
std::cos(polar)).normalized();
|
|
|
|
// check available distance
|
|
EigenMesh3D::hit_result t
|
|
= pinhead_mesh_intersect(hp, // touching point
|
|
nn, // normal
|
|
pin_r,
|
|
m_cfg.head_back_radius_mm,
|
|
w);
|
|
|
|
if(t.distance() <= w) {
|
|
|
|
// Let's try to optimize this angle, there might be a
|
|
// viable normal that doesn't collide with the model
|
|
// geometry and its very close to the default.
|
|
|
|
StopCriteria stc;
|
|
stc.max_iterations = m_cfg.optimizer_max_iterations;
|
|
stc.relative_score_difference = m_cfg.optimizer_rel_score_diff;
|
|
stc.stop_score = w; // space greater than w is enough
|
|
GeneticOptimizer solver(stc);
|
|
solver.seed(0); // we want deterministic behavior
|
|
|
|
auto oresult = solver.optimize_max(
|
|
[this, pin_r, w, hp](double plr, double azm)
|
|
{
|
|
auto dir = Vec3d(std::cos(azm) * std::sin(plr),
|
|
std::sin(azm) * std::sin(plr),
|
|
std::cos(plr)).normalized();
|
|
|
|
double score = pinhead_mesh_intersect(
|
|
hp, dir, pin_r, m_cfg.head_back_radius_mm, w);
|
|
|
|
return score;
|
|
},
|
|
initvals(polar, azimuth), // start with what we have
|
|
bound(3 * PI / 4,
|
|
PI), // Must not exceed the tilt limit
|
|
bound(-PI, PI) // azimuth can be a full search
|
|
);
|
|
|
|
if(oresult.score > w) {
|
|
polar = std::get<0>(oresult.optimum);
|
|
azimuth = std::get<1>(oresult.optimum);
|
|
nn = Vec3d(std::cos(azimuth) * std::sin(polar),
|
|
std::sin(azimuth) * std::sin(polar),
|
|
std::cos(polar)).normalized();
|
|
t = oresult.score;
|
|
}
|
|
}
|
|
|
|
// save the verified and corrected normal
|
|
m_support_nmls.row(fidx) = nn;
|
|
|
|
if (t.distance() > w) {
|
|
// Check distance from ground, we might have zero elevation.
|
|
if (hp(Z) + w * nn(Z) < m_builder.ground_level) {
|
|
addfn(m_iheadless, fidx);
|
|
} else {
|
|
// mark the point for needing a head.
|
|
addfn(m_iheads, fidx);
|
|
}
|
|
} else if (polar >= 3 * PI / 4) {
|
|
// Headless supports do not tilt like the headed ones
|
|
// so the normal should point almost to the ground.
|
|
addfn(m_iheadless, fidx);
|
|
}
|
|
}
|
|
};
|
|
|
|
ccr::enumerate(filtered_indices.begin(), filtered_indices.end(), filterfn);
|
|
|
|
m_thr();
|
|
}
|
|
|
|
void SupportTreeBuildsteps::add_pinheads()
|
|
{
|
|
for (unsigned i : m_iheads) {
|
|
m_thr();
|
|
m_builder.add_head(
|
|
i,
|
|
m_cfg.head_back_radius_mm,
|
|
m_support_pts[i].head_front_radius,
|
|
m_cfg.head_width_mm,
|
|
m_cfg.head_penetration_mm,
|
|
m_support_nmls.row(i), // dir
|
|
m_support_pts[i].pos.cast<double>() // displacement
|
|
);
|
|
}
|
|
}
|
|
|
|
void SupportTreeBuildsteps::classify()
|
|
{
|
|
// We should first get the heads that reach the ground directly
|
|
PtIndices ground_head_indices;
|
|
ground_head_indices.reserve(m_iheads.size());
|
|
m_iheads_onmodel.reserve(m_iheads.size());
|
|
|
|
// First we decide which heads reach the ground and can be full
|
|
// pillars and which shall be connected to the model surface (or
|
|
// search a suitable path around the surface that leads to the
|
|
// ground -- TODO)
|
|
for(unsigned i : m_iheads) {
|
|
m_thr();
|
|
|
|
auto& head = m_builder.head(i);
|
|
Vec3d n(0, 0, -1);
|
|
double r = head.r_back_mm;
|
|
Vec3d headjp = head.junction_point();
|
|
|
|
// collision check
|
|
auto hit = bridge_mesh_intersect(headjp, n, r);
|
|
|
|
if(std::isinf(hit.distance())) ground_head_indices.emplace_back(i);
|
|
else if(m_cfg.ground_facing_only) head.invalidate();
|
|
else m_iheads_onmodel.emplace_back(std::make_pair(i, hit));
|
|
}
|
|
|
|
// We want to search for clusters of points that are far enough
|
|
// from each other in the XY plane to not cross their pillar bases
|
|
// These clusters of support points will join in one pillar,
|
|
// possibly in their centroid support point.
|
|
|
|
auto pointfn = [this](unsigned i) {
|
|
return m_builder.head(i).junction_point();
|
|
};
|
|
|
|
auto predicate = [this](const PointIndexEl &e1,
|
|
const PointIndexEl &e2) {
|
|
double d2d = distance(to_2d(e1.first), to_2d(e2.first));
|
|
double d3d = distance(e1.first, e2.first);
|
|
return d2d < 2 * m_cfg.base_radius_mm
|
|
&& d3d < m_cfg.max_bridge_length_mm;
|
|
};
|
|
|
|
m_pillar_clusters = cluster(ground_head_indices,
|
|
pointfn,
|
|
predicate,
|
|
m_cfg.max_bridges_on_pillar);
|
|
}
|
|
|
|
void SupportTreeBuildsteps::routing_to_ground()
|
|
{
|
|
const double pradius = m_cfg.head_back_radius_mm;
|
|
|
|
ClusterEl cl_centroids;
|
|
cl_centroids.reserve(m_pillar_clusters.size());
|
|
|
|
for (auto &cl : m_pillar_clusters) {
|
|
m_thr();
|
|
|
|
// place all the centroid head positions into the index. We
|
|
// will query for alternative pillar positions. If a sidehead
|
|
// cannot connect to the cluster centroid, we have to search
|
|
// for another head with a full pillar. Also when there are two
|
|
// elements in the cluster, the centroid is arbitrary and the
|
|
// sidehead is allowed to connect to a nearby pillar to
|
|
// increase structural stability.
|
|
|
|
if (cl.empty()) continue;
|
|
|
|
// get the current cluster centroid
|
|
auto & thr = m_thr;
|
|
const auto &points = m_points;
|
|
long lcid = cluster_centroid(
|
|
cl, [&points](size_t idx) { return points.row(long(idx)); },
|
|
[thr](const Vec3d &p1, const Vec3d &p2) {
|
|
thr();
|
|
return distance(Vec2d(p1(X), p1(Y)), Vec2d(p2(X), p2(Y)));
|
|
});
|
|
|
|
assert(lcid >= 0);
|
|
unsigned hid = cl[size_t(lcid)]; // Head ID
|
|
|
|
cl_centroids.emplace_back(hid);
|
|
|
|
Head &h = m_builder.head(hid);
|
|
h.transform();
|
|
|
|
create_ground_pillar(h.junction_point(), h.dir, h.r_back_mm, h.id);
|
|
}
|
|
|
|
// now we will go through the clusters ones again and connect the
|
|
// sidepoints with the cluster centroid (which is a ground pillar)
|
|
// or a nearby pillar if the centroid is unreachable.
|
|
size_t ci = 0;
|
|
for (auto cl : m_pillar_clusters) {
|
|
m_thr();
|
|
|
|
auto cidx = cl_centroids[ci++];
|
|
|
|
// TODO: don't consider the cluster centroid but calculate a
|
|
// central position where the pillar can be placed. this way
|
|
// the weight is distributed more effectively on the pillar.
|
|
|
|
auto centerpillarID = m_builder.head_pillar(cidx).id;
|
|
|
|
for (auto c : cl) {
|
|
m_thr();
|
|
if (c == cidx) continue;
|
|
|
|
auto &sidehead = m_builder.head(c);
|
|
sidehead.transform();
|
|
|
|
if (!connect_to_nearpillar(sidehead, centerpillarID) &&
|
|
!search_pillar_and_connect(sidehead)) {
|
|
Vec3d pstart = sidehead.junction_point();
|
|
// Vec3d pend = Vec3d{pstart(X), pstart(Y), gndlvl};
|
|
// Could not find a pillar, create one
|
|
create_ground_pillar(pstart, sidehead.dir, pradius, sidehead.id);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
void SupportTreeBuildsteps::routing_to_model()
|
|
{
|
|
// We need to check if there is an easy way out to the bed surface.
|
|
// If it can be routed there with a bridge shorter than
|
|
// min_bridge_distance.
|
|
|
|
// First we want to index the available pillars. The best is to connect
|
|
// these points to the available pillars
|
|
|
|
auto routedown = [this](Head& head, const Vec3d& dir, double dist)
|
|
{
|
|
head.transform();
|
|
Vec3d hjp = head.junction_point();
|
|
Vec3d endp = hjp + dist * dir;
|
|
m_builder.add_bridge(hjp, endp, head.r_back_mm);
|
|
m_builder.add_junction(endp, head.r_back_mm);
|
|
|
|
this->create_ground_pillar(endp, dir, head.r_back_mm, head.id);
|
|
};
|
|
|
|
std::vector<unsigned> modelpillars;
|
|
ccr::SpinningMutex mutex;
|
|
|
|
auto onmodelfn =
|
|
[this, routedown, &modelpillars, &mutex]
|
|
(const std::pair<unsigned, EigenMesh3D::hit_result> &el, size_t)
|
|
{
|
|
m_thr();
|
|
unsigned idx = el.first;
|
|
EigenMesh3D::hit_result hit = el.second;
|
|
|
|
auto& head = m_builder.head(idx);
|
|
Vec3d hjp = head.junction_point();
|
|
|
|
// /////////////////////////////////////////////////////////////////
|
|
// Search nearby pillar
|
|
// /////////////////////////////////////////////////////////////////
|
|
|
|
if(search_pillar_and_connect(head)) { head.transform(); return; }
|
|
|
|
// /////////////////////////////////////////////////////////////////
|
|
// Try straight path
|
|
// /////////////////////////////////////////////////////////////////
|
|
|
|
// Cannot connect to nearby pillar. We will try to search for
|
|
// a route to the ground.
|
|
|
|
double t = bridge_mesh_intersect(hjp, head.dir, head.r_back_mm);
|
|
double d = 0, tdown = 0;
|
|
Vec3d dirdown(0.0, 0.0, -1.0);
|
|
|
|
t = std::min(t, m_cfg.max_bridge_length_mm);
|
|
|
|
while(d < t && !std::isinf(tdown = bridge_mesh_intersect(
|
|
hjp + d*head.dir,
|
|
dirdown, head.r_back_mm))) {
|
|
d += head.r_back_mm;
|
|
}
|
|
|
|
if(std::isinf(tdown)) { // we heave found a route to the ground
|
|
routedown(head, head.dir, d); return;
|
|
}
|
|
|
|
// /////////////////////////////////////////////////////////////////
|
|
// Optimize bridge direction
|
|
// /////////////////////////////////////////////////////////////////
|
|
|
|
// Straight path failed so we will try to search for a suitable
|
|
// direction out of the cavity.
|
|
|
|
// Get the spherical representation of the normal. its easier to
|
|
// work with.
|
|
double z = head.dir(Z);
|
|
double r = 1.0; // for normalized vector
|
|
double polar = std::acos(z / r);
|
|
double azimuth = std::atan2(head.dir(Y), head.dir(X));
|
|
|
|
using libnest2d::opt::bound;
|
|
using libnest2d::opt::initvals;
|
|
using libnest2d::opt::GeneticOptimizer;
|
|
using libnest2d::opt::StopCriteria;
|
|
|
|
StopCriteria stc;
|
|
stc.max_iterations = m_cfg.optimizer_max_iterations;
|
|
stc.relative_score_difference = m_cfg.optimizer_rel_score_diff;
|
|
stc.stop_score = 1e6;
|
|
GeneticOptimizer solver(stc);
|
|
solver.seed(0); // we want deterministic behavior
|
|
|
|
double r_back = head.r_back_mm;
|
|
|
|
auto oresult = solver.optimize_max(
|
|
[this, hjp, r_back](double plr, double azm)
|
|
{
|
|
Vec3d n = Vec3d(std::cos(azm) * std::sin(plr),
|
|
std::sin(azm) * std::sin(plr),
|
|
std::cos(plr)).normalized();
|
|
return bridge_mesh_intersect(hjp, n, r_back);
|
|
},
|
|
initvals(polar, azimuth), // let's start with what we have
|
|
bound(3*PI/4, PI), // Must not exceed the slope limit
|
|
bound(-PI, PI) // azimuth can be a full range search
|
|
);
|
|
|
|
d = 0; t = oresult.score;
|
|
|
|
polar = std::get<0>(oresult.optimum);
|
|
azimuth = std::get<1>(oresult.optimum);
|
|
Vec3d bridgedir = Vec3d(std::cos(azimuth) * std::sin(polar),
|
|
std::sin(azimuth) * std::sin(polar),
|
|
std::cos(polar)).normalized();
|
|
|
|
t = std::min(t, m_cfg.max_bridge_length_mm);
|
|
|
|
while(d < t && !std::isinf(tdown = bridge_mesh_intersect(
|
|
hjp + d*bridgedir,
|
|
dirdown,
|
|
head.r_back_mm))) {
|
|
d += head.r_back_mm;
|
|
}
|
|
|
|
if(std::isinf(tdown)) { // we heave found a route to the ground
|
|
routedown(head, bridgedir, d); return;
|
|
}
|
|
|
|
// /////////////////////////////////////////////////////////////////
|
|
// Route to model body
|
|
// /////////////////////////////////////////////////////////////////
|
|
|
|
double zangle = std::asin(hit.direction()(Z));
|
|
zangle = std::max(zangle, PI/4);
|
|
double h = std::sin(zangle) * head.fullwidth();
|
|
|
|
// The width of the tail head that we would like to have...
|
|
h = std::min(hit.distance() - head.r_back_mm, h);
|
|
|
|
if(h > 0) {
|
|
Vec3d endp{hjp(X), hjp(Y), hjp(Z) - hit.distance() + h};
|
|
auto center_hit = m_mesh.query_ray_hit(hjp, dirdown);
|
|
|
|
double hitdiff = center_hit.distance() - hit.distance();
|
|
Vec3d hitp = std::abs(hitdiff) < 2*head.r_back_mm?
|
|
center_hit.position() : hit.position();
|
|
|
|
head.transform();
|
|
|
|
Pillar& pill = m_builder.add_pillar(unsigned(head.id),
|
|
endp,
|
|
head.r_back_mm);
|
|
|
|
Vec3d taildir = endp - hitp;
|
|
double dist = distance(endp, hitp) + m_cfg.head_penetration_mm;
|
|
double w = dist - 2 * head.r_pin_mm - head.r_back_mm;
|
|
|
|
Head tailhead(head.r_back_mm,
|
|
head.r_pin_mm,
|
|
w,
|
|
m_cfg.head_penetration_mm,
|
|
taildir,
|
|
hitp);
|
|
|
|
tailhead.transform();
|
|
pill.base = tailhead.mesh;
|
|
|
|
// Experimental: add the pillar to the index for cascading
|
|
std::lock_guard<ccr::SpinningMutex> lk(mutex);
|
|
modelpillars.emplace_back(unsigned(pill.id));
|
|
return;
|
|
}
|
|
|
|
// We have failed to route this head.
|
|
BOOST_LOG_TRIVIAL(warning)
|
|
<< "Failed to route model facing support point."
|
|
<< " ID: " << idx;
|
|
head.invalidate();
|
|
};
|
|
|
|
ccr::enumerate(m_iheads_onmodel.begin(), m_iheads_onmodel.end(), onmodelfn);
|
|
|
|
for(auto pillid : modelpillars) {
|
|
auto& pillar = m_builder.pillar(pillid);
|
|
m_pillar_index.insert(pillar.endpoint(), pillid);
|
|
}
|
|
}
|
|
|
|
void SupportTreeBuildsteps::interconnect_pillars()
|
|
{
|
|
// Now comes the algorithm that connects pillars with each other.
|
|
// Ideally every pillar should be connected with at least one of its
|
|
// neighbors if that neighbor is within max_pillar_link_distance
|
|
|
|
// Pillars with height exceeding H1 will require at least one neighbor
|
|
// to connect with. Height exceeding H2 require two neighbors.
|
|
double H1 = m_cfg.max_solo_pillar_height_mm;
|
|
double H2 = m_cfg.max_dual_pillar_height_mm;
|
|
double d = m_cfg.max_pillar_link_distance_mm;
|
|
|
|
//A connection between two pillars only counts if the height ratio is
|
|
// bigger than 50%
|
|
double min_height_ratio = 0.5;
|
|
|
|
std::set<unsigned long> pairs;
|
|
|
|
// A function to connect one pillar with its neighbors. THe number of
|
|
// neighbors is given in the configuration. This function if called
|
|
// for every pillar in the pillar index. A pair of pillar will not
|
|
// be connected multiple times this is ensured by the 'pairs' set which
|
|
// remembers the processed pillar pairs
|
|
auto cascadefn =
|
|
[this, d, &pairs, min_height_ratio, H1] (const PointIndexEl& el)
|
|
{
|
|
Vec3d qp = el.first; // endpoint of the pillar
|
|
|
|
const Pillar& pillar = m_builder.pillar(el.second); // actual pillar
|
|
|
|
// Get the max number of neighbors a pillar should connect to
|
|
unsigned neighbors = m_cfg.pillar_cascade_neighbors;
|
|
|
|
// connections are already enough for the pillar
|
|
if(pillar.links >= neighbors) return;
|
|
|
|
// Query all remaining points within reach
|
|
auto qres = m_pillar_index.query([qp, d](const PointIndexEl& e){
|
|
return distance(e.first, qp) < d;
|
|
});
|
|
|
|
// sort the result by distance (have to check if this is needed)
|
|
std::sort(qres.begin(), qres.end(),
|
|
[qp](const PointIndexEl& e1, const PointIndexEl& e2){
|
|
return distance(e1.first, qp) < distance(e2.first, qp);
|
|
});
|
|
|
|
for(auto& re : qres) { // process the queried neighbors
|
|
|
|
if(re.second == el.second) continue; // Skip self
|
|
|
|
auto a = el.second, b = re.second;
|
|
|
|
// Get unique hash for the given pair (order doesn't matter)
|
|
auto hashval = pairhash(a, b);
|
|
|
|
// Search for the pair amongst the remembered pairs
|
|
if(pairs.find(hashval) != pairs.end()) continue;
|
|
|
|
const Pillar& neighborpillar = m_builder.pillar(re.second);
|
|
|
|
// this neighbor is occupied, skip
|
|
if(neighborpillar.links >= neighbors) continue;
|
|
|
|
if(interconnect(pillar, neighborpillar)) {
|
|
pairs.insert(hashval);
|
|
|
|
// If the interconnection length between the two pillars is
|
|
// less than 50% of the longer pillar's height, don't count
|
|
if(pillar.height < H1 ||
|
|
neighborpillar.height / pillar.height > min_height_ratio)
|
|
m_builder.increment_links(pillar);
|
|
|
|
if(neighborpillar.height < H1 ||
|
|
pillar.height / neighborpillar.height > min_height_ratio)
|
|
m_builder.increment_links(neighborpillar);
|
|
|
|
}
|
|
|
|
// connections are enough for one pillar
|
|
if(pillar.links >= neighbors) break;
|
|
}
|
|
};
|
|
|
|
// Run the cascade for the pillars in the index
|
|
m_pillar_index.foreach(cascadefn);
|
|
|
|
// We would be done here if we could allow some pillars to not be
|
|
// connected with any neighbors. But this might leave the support tree
|
|
// unprintable.
|
|
//
|
|
// The current solution is to insert additional pillars next to these
|
|
// lonely pillars. One or even two additional pillar might get inserted
|
|
// depending on the length of the lonely pillar.
|
|
|
|
size_t pillarcount = m_builder.pillarcount();
|
|
|
|
// Again, go through all pillars, this time in the whole support tree
|
|
// not just the index.
|
|
for(size_t pid = 0; pid < pillarcount; pid++) {
|
|
auto pillar = [this, pid]() { return m_builder.pillar(pid); };
|
|
|
|
// Decide how many additional pillars will be needed:
|
|
|
|
unsigned needpillars = 0;
|
|
if (pillar().bridges > m_cfg.max_bridges_on_pillar)
|
|
needpillars = 3;
|
|
else if (pillar().links < 2 && pillar().height > H2) {
|
|
// Not enough neighbors to support this pillar
|
|
needpillars = 2;
|
|
} else if (pillar().links < 1 && pillar().height > H1) {
|
|
// No neighbors could be found and the pillar is too long.
|
|
needpillars = 1;
|
|
}
|
|
|
|
needpillars = std::max(pillar().links, needpillars) - pillar().links;
|
|
if (needpillars == 0) continue;
|
|
|
|
// Search for new pillar locations:
|
|
|
|
bool found = false;
|
|
double alpha = 0; // goes to 2Pi
|
|
double r = 2 * m_cfg.base_radius_mm;
|
|
Vec3d pillarsp = pillar().startpoint();
|
|
|
|
// temp value for starting point detection
|
|
Vec3d sp(pillarsp(X), pillarsp(Y), pillarsp(Z) - r);
|
|
|
|
// A vector of bool for placement feasbility
|
|
std::vector<bool> canplace(needpillars, false);
|
|
std::vector<Vec3d> spts(needpillars); // vector of starting points
|
|
|
|
double gnd = m_builder.ground_level;
|
|
double min_dist = m_cfg.pillar_base_safety_distance_mm +
|
|
m_cfg.base_radius_mm + EPSILON;
|
|
|
|
while(!found && alpha < 2*PI) {
|
|
for (unsigned n = 0;
|
|
n < needpillars && (!n || canplace[n - 1]);
|
|
n++)
|
|
{
|
|
double a = alpha + n * PI / 3;
|
|
Vec3d s = sp;
|
|
s(X) += std::cos(a) * r;
|
|
s(Y) += std::sin(a) * r;
|
|
spts[n] = s;
|
|
|
|
// Check the path vertically down
|
|
auto hr = bridge_mesh_intersect(s, {0, 0, -1}, pillar().r);
|
|
Vec3d gndsp{s(X), s(Y), gnd};
|
|
|
|
// If the path is clear, check for pillar base collisions
|
|
canplace[n] = std::isinf(hr.distance()) &&
|
|
std::sqrt(m_mesh.squared_distance(gndsp)) >
|
|
min_dist;
|
|
}
|
|
|
|
found = std::all_of(canplace.begin(), canplace.end(),
|
|
[](bool v) { return v; });
|
|
|
|
// 20 angles will be tried...
|
|
alpha += 0.1 * PI;
|
|
}
|
|
|
|
std::vector<long> newpills;
|
|
newpills.reserve(needpillars);
|
|
|
|
if (found)
|
|
for (unsigned n = 0; n < needpillars; n++) {
|
|
Vec3d s = spts[n];
|
|
Pillar p(s, Vec3d(s(X), s(Y), gnd), pillar().r);
|
|
p.add_base(m_cfg.base_height_mm, m_cfg.base_radius_mm);
|
|
|
|
if (interconnect(pillar(), p)) {
|
|
Pillar &pp = m_builder.add_pillar(p);
|
|
m_pillar_index.insert(pp.endpoint(), unsigned(pp.id));
|
|
|
|
m_builder.add_junction(s, pillar().r);
|
|
double t = bridge_mesh_intersect(pillarsp,
|
|
dirv(pillarsp, s),
|
|
pillar().r);
|
|
if (distance(pillarsp, s) < t)
|
|
m_builder.add_bridge(pillarsp, s, pillar().r);
|
|
|
|
if (pillar().endpoint()(Z) > m_builder.ground_level)
|
|
m_builder.add_junction(pillar().endpoint(),
|
|
pillar().r);
|
|
|
|
newpills.emplace_back(pp.id);
|
|
m_builder.increment_links(pillar());
|
|
m_builder.increment_links(pp);
|
|
}
|
|
}
|
|
|
|
if(!newpills.empty()) {
|
|
for(auto it = newpills.begin(), nx = std::next(it);
|
|
nx != newpills.end(); ++it, ++nx) {
|
|
const Pillar& itpll = m_builder.pillar(*it);
|
|
const Pillar& nxpll = m_builder.pillar(*nx);
|
|
if(interconnect(itpll, nxpll)) {
|
|
m_builder.increment_links(itpll);
|
|
m_builder.increment_links(nxpll);
|
|
}
|
|
}
|
|
|
|
m_pillar_index.foreach(cascadefn);
|
|
}
|
|
}
|
|
}
|
|
|
|
void SupportTreeBuildsteps::routing_headless()
|
|
{
|
|
// For now we will just generate smaller headless sticks with a sharp
|
|
// ending point that connects to the mesh surface.
|
|
|
|
// We will sink the pins into the model surface for a distance of 1/3 of
|
|
// the pin radius
|
|
for(unsigned i : m_iheadless) {
|
|
m_thr();
|
|
|
|
const auto R = double(m_support_pts[i].head_front_radius);
|
|
const double HWIDTH_MM = R/3;
|
|
|
|
// Exact support position
|
|
Vec3d sph = m_support_pts[i].pos.cast<double>();
|
|
Vec3d n = m_support_nmls.row(i); // mesh outward normal
|
|
Vec3d sp = sph - n * HWIDTH_MM; // stick head start point
|
|
|
|
Vec3d dir = {0, 0, -1};
|
|
Vec3d sj = sp + R * n; // stick start point
|
|
|
|
// This is only for checking
|
|
double idist = bridge_mesh_intersect(sph, dir, R, true);
|
|
double dist = ray_mesh_intersect(sj, dir);
|
|
if (std::isinf(dist))
|
|
dist = sph(Z) - m_mesh.ground_level()
|
|
+ m_mesh.ground_level_offset();
|
|
|
|
if(std::isnan(idist) || idist < 2*R ||
|
|
std::isnan(dist) || dist < 2*R)
|
|
{
|
|
BOOST_LOG_TRIVIAL(warning) << "Can not find route for headless"
|
|
<< " support stick at: "
|
|
<< sj.transpose();
|
|
continue;
|
|
}
|
|
|
|
Vec3d ej = sj + (dist + HWIDTH_MM)* dir;
|
|
m_builder.add_compact_bridge(sp, ej, n, R, !std::isinf(dist));
|
|
}
|
|
}
|
|
|
|
}
|
|
}
|