2044 lines
73 KiB
C++
2044 lines
73 KiB
C++
/**
|
|
* In this file we will implement the automatic SLA support tree generation.
|
|
*
|
|
*/
|
|
|
|
#include <numeric>
|
|
#include "SLASupportTree.hpp"
|
|
#include "SLABoilerPlate.hpp"
|
|
#include "SLASpatIndex.hpp"
|
|
#include "SLABasePool.hpp"
|
|
|
|
#include <libslic3r/ClipperUtils.hpp>
|
|
#include <libslic3r/Model.hpp>
|
|
|
|
#include <boost/log/trivial.hpp>
|
|
#include <tbb/parallel_for.h>
|
|
|
|
/**
|
|
* Terminology:
|
|
*
|
|
* Support point:
|
|
* The point on the model surface that needs support.
|
|
*
|
|
* Pillar:
|
|
* A thick column that spans from a support point to the ground and has
|
|
* a thick cone shaped base where it touches the ground.
|
|
*
|
|
* Ground facing support point:
|
|
* A support point that can be directly connected with the ground with a pillar
|
|
* that does not collide or cut through the model.
|
|
*
|
|
* Non ground facing support point:
|
|
* A support point that cannot be directly connected with the ground (only with
|
|
* the model surface).
|
|
*
|
|
* Head:
|
|
* The pinhead that connects to the model surface with the sharp end end
|
|
* to a pillar or bridge stick with the dull end.
|
|
*
|
|
* Headless support point:
|
|
* A support point on the model surface for which there is not enough place for
|
|
* the head. It is either in a hole or there is some barrier that would collide
|
|
* with the head geometry. The headless support point can be ground facing and
|
|
* non ground facing as well.
|
|
*
|
|
* Bridge:
|
|
* A stick that connects two pillars or a head with a pillar.
|
|
*
|
|
* Junction:
|
|
* A small ball in the intersection of two or more sticks (pillar, bridge, ...)
|
|
*
|
|
* CompactBridge:
|
|
* A bridge that connects a headless support point with the model surface or a
|
|
* nearby pillar.
|
|
*/
|
|
|
|
namespace Slic3r {
|
|
namespace sla {
|
|
|
|
using Coordf = double;
|
|
using Portion = std::tuple<double, double>;
|
|
|
|
inline Portion make_portion(double a, double b) {
|
|
return std::make_tuple(a, b);
|
|
}
|
|
|
|
template<class Vec> double distance(const Vec& p) {
|
|
return std::sqrt(p.transpose() * p);
|
|
}
|
|
|
|
template<class Vec> double distance(const Vec& pp1, const Vec& pp2) {
|
|
auto p = pp2 - pp1;
|
|
return distance(p);
|
|
}
|
|
|
|
Contour3D sphere(double rho, Portion portion = make_portion(0.0, 2.0*PI),
|
|
double fa=(2*PI/360)) {
|
|
|
|
Contour3D ret;
|
|
|
|
// prohibit close to zero radius
|
|
if(rho <= 1e-6 && rho >= -1e-6) return ret;
|
|
|
|
auto& vertices = ret.points;
|
|
auto& facets = ret.indices;
|
|
|
|
// Algorithm:
|
|
// Add points one-by-one to the sphere grid and form facets using relative
|
|
// coordinates. Sphere is composed effectively of a mesh of stacked circles.
|
|
|
|
// adjust via rounding to get an even multiple for any provided angle.
|
|
double angle = (2*PI / floor(2*PI / fa));
|
|
|
|
// Ring to be scaled to generate the steps of the sphere
|
|
std::vector<double> ring;
|
|
|
|
for (double i = 0; i < 2*PI; i+=angle) ring.emplace_back(i);
|
|
|
|
const auto sbegin = size_t(2*std::get<0>(portion)/angle);
|
|
const auto send = size_t(2*std::get<1>(portion)/angle);
|
|
|
|
const size_t steps = ring.size();
|
|
const double increment = 1.0 / double(steps);
|
|
|
|
// special case: first ring connects to 0,0,0
|
|
// insert and form facets.
|
|
if(sbegin == 0)
|
|
vertices.emplace_back(Vec3d(0.0, 0.0, -rho + increment*sbegin*2.0*rho));
|
|
|
|
auto id = coord_t(vertices.size());
|
|
for (size_t i = 0; i < ring.size(); i++) {
|
|
// Fixed scaling
|
|
const double z = -rho + increment*rho*2.0 * (sbegin + 1.0);
|
|
// radius of the circle for this step.
|
|
const double r = std::sqrt(std::abs(rho*rho - z*z));
|
|
Vec2d b = Eigen::Rotation2Dd(ring[i]) * Eigen::Vector2d(0, r);
|
|
vertices.emplace_back(Vec3d(b(0), b(1), z));
|
|
|
|
if(sbegin == 0)
|
|
facets.emplace_back((i == 0) ? Vec3crd(coord_t(ring.size()), 0, 1) :
|
|
Vec3crd(id - 1, 0, id));
|
|
++ id;
|
|
}
|
|
|
|
// General case: insert and form facets for each step,
|
|
// joining it to the ring below it.
|
|
for (size_t s = sbegin + 2; s < send - 1; s++) {
|
|
const double z = -rho + increment*double(s*2.0*rho);
|
|
const double r = std::sqrt(std::abs(rho*rho - z*z));
|
|
|
|
for (size_t i = 0; i < ring.size(); i++) {
|
|
Vec2d b = Eigen::Rotation2Dd(ring[i]) * Eigen::Vector2d(0, r);
|
|
vertices.emplace_back(Vec3d(b(0), b(1), z));
|
|
auto id_ringsize = coord_t(id - int(ring.size()));
|
|
if (i == 0) {
|
|
// wrap around
|
|
facets.emplace_back(Vec3crd(id - 1, id,
|
|
id + coord_t(ring.size() - 1)));
|
|
facets.emplace_back(Vec3crd(id - 1, id_ringsize, id));
|
|
} else {
|
|
facets.emplace_back(Vec3crd(id_ringsize - 1, id_ringsize, id));
|
|
facets.emplace_back(Vec3crd(id - 1, id_ringsize - 1, id));
|
|
}
|
|
id++;
|
|
}
|
|
}
|
|
|
|
// special case: last ring connects to 0,0,rho*2.0
|
|
// only form facets.
|
|
if(send >= size_t(2*PI / angle)) {
|
|
vertices.emplace_back(Vec3d(0.0, 0.0, -rho + increment*send*2.0*rho));
|
|
for (size_t i = 0; i < ring.size(); i++) {
|
|
auto id_ringsize = coord_t(id - int(ring.size()));
|
|
if (i == 0) {
|
|
// third vertex is on the other side of the ring.
|
|
facets.emplace_back(Vec3crd(id - 1, id_ringsize, id));
|
|
} else {
|
|
auto ci = coord_t(id_ringsize + coord_t(i));
|
|
facets.emplace_back(Vec3crd(ci - 1, ci, id));
|
|
}
|
|
}
|
|
}
|
|
id++;
|
|
|
|
return ret;
|
|
}
|
|
|
|
// Down facing cylinder in Z direction with arguments:
|
|
// r: radius
|
|
// h: Height
|
|
// ssteps: how many edges will create the base circle
|
|
// sp: starting point
|
|
Contour3D cylinder(double r, double h, size_t ssteps, const Vec3d sp = {0,0,0})
|
|
{
|
|
Contour3D ret;
|
|
|
|
auto steps = int(ssteps);
|
|
auto& points = ret.points;
|
|
auto& indices = ret.indices;
|
|
points.reserve(2*ssteps);
|
|
double a = 2*PI/steps;
|
|
|
|
Vec3d jp = sp;
|
|
Vec3d endp = {sp(X), sp(Y), sp(Z) + h};
|
|
|
|
// Upper circle points
|
|
for(int i = 0; i < steps; ++i) {
|
|
double phi = i*a;
|
|
double ex = endp(X) + r*std::cos(phi);
|
|
double ey = endp(Y) + r*std::sin(phi);
|
|
points.emplace_back(ex, ey, endp(Z));
|
|
}
|
|
|
|
// Lower circle points
|
|
for(int i = 0; i < steps; ++i) {
|
|
double phi = i*a;
|
|
double x = jp(X) + r*std::cos(phi);
|
|
double y = jp(Y) + r*std::sin(phi);
|
|
points.emplace_back(x, y, jp(Z));
|
|
}
|
|
|
|
// Now create long triangles connecting upper and lower circles
|
|
indices.reserve(2*ssteps);
|
|
auto offs = steps;
|
|
for(int i = 0; i < steps - 1; ++i) {
|
|
indices.emplace_back(i, i + offs, offs + i + 1);
|
|
indices.emplace_back(i, offs + i + 1, i + 1);
|
|
}
|
|
|
|
// Last triangle connecting the first and last vertices
|
|
auto last = steps - 1;
|
|
indices.emplace_back(0, last, offs);
|
|
indices.emplace_back(last, offs + last, offs);
|
|
|
|
// According to the slicing algorithms, we need to aid them with generating
|
|
// a watertight body. So we create a triangle fan for the upper and lower
|
|
// ending of the cylinder to close the geometry.
|
|
points.emplace_back(jp); size_t ci = points.size() - 1;
|
|
for(int i = 0; i < steps - 1; ++i)
|
|
indices.emplace_back(i + offs + 1, i + offs, ci);
|
|
|
|
indices.emplace_back(offs, steps + offs - 1, ci);
|
|
|
|
points.emplace_back(endp); ci = points.size() - 1;
|
|
for(int i = 0; i < steps - 1; ++i)
|
|
indices.emplace_back(ci, i, i + 1);
|
|
|
|
indices.emplace_back(steps - 1, 0, ci);
|
|
|
|
return ret;
|
|
}
|
|
|
|
struct Head {
|
|
Contour3D mesh;
|
|
|
|
size_t steps = 45;
|
|
Vec3d dir = {0, 0, -1};
|
|
Vec3d tr = {0, 0, 0};
|
|
|
|
double r_back_mm = 1;
|
|
double r_pin_mm = 0.5;
|
|
double width_mm = 2;
|
|
double penetration_mm = 0.5;
|
|
|
|
// For identification purposes. This will be used as the index into the
|
|
// container holding the head structures. See SLASupportTree::Impl
|
|
long id = -1;
|
|
|
|
// If there is a pillar connecting to this head, then the id will be set.
|
|
long pillar_id = -1;
|
|
|
|
inline void invalidate() { id = -1; }
|
|
inline bool is_valid() const { return id >= 0; }
|
|
|
|
Head(double r_big_mm,
|
|
double r_small_mm,
|
|
double length_mm,
|
|
double penetration,
|
|
Vec3d direction = {0, 0, -1}, // direction (normal to the dull end )
|
|
Vec3d offset = {0, 0, 0}, // displacement
|
|
const size_t circlesteps = 45):
|
|
steps(circlesteps), dir(direction), tr(offset),
|
|
r_back_mm(r_big_mm), r_pin_mm(r_small_mm), width_mm(length_mm),
|
|
penetration_mm(penetration)
|
|
{
|
|
|
|
// We create two spheres which will be connected with a robe that fits
|
|
// both circles perfectly.
|
|
|
|
// Set up the model detail level
|
|
const double detail = 2*PI/steps;
|
|
|
|
// We don't generate whole circles. Instead, we generate only the
|
|
// portions which are visible (not covered by the robe) To know the
|
|
// exact portion of the bottom and top circles we need to use some
|
|
// rules of tangent circles from which we can derive (using simple
|
|
// triangles the following relations:
|
|
|
|
// The height of the whole mesh
|
|
const double h = r_big_mm + r_small_mm + width_mm;
|
|
double phi = PI/2 - std::acos( (r_big_mm - r_small_mm) / h );
|
|
|
|
// To generate a whole circle we would pass a portion of (0, Pi)
|
|
// To generate only a half horizontal circle we can pass (0, Pi/2)
|
|
// The calculated phi is an offset to the half circles needed to smooth
|
|
// the transition from the circle to the robe geometry
|
|
|
|
auto&& s1 = sphere(r_big_mm, make_portion(0, PI/2 + phi), detail);
|
|
auto&& s2 = sphere(r_small_mm, make_portion(PI/2 + phi, PI), detail);
|
|
|
|
for(auto& p : s2.points) z(p) += h;
|
|
|
|
mesh.merge(s1);
|
|
mesh.merge(s2);
|
|
|
|
for(size_t idx1 = s1.points.size() - steps, idx2 = s1.points.size();
|
|
idx1 < s1.points.size() - 1;
|
|
idx1++, idx2++)
|
|
{
|
|
coord_t i1s1 = coord_t(idx1), i1s2 = coord_t(idx2);
|
|
coord_t i2s1 = i1s1 + 1, i2s2 = i1s2 + 1;
|
|
|
|
mesh.indices.emplace_back(i1s1, i2s1, i2s2);
|
|
mesh.indices.emplace_back(i1s1, i2s2, i1s2);
|
|
}
|
|
|
|
auto i1s1 = coord_t(s1.points.size()) - coord_t(steps);
|
|
auto i2s1 = coord_t(s1.points.size()) - 1;
|
|
auto i1s2 = coord_t(s1.points.size());
|
|
auto i2s2 = coord_t(s1.points.size()) + coord_t(steps) - 1;
|
|
|
|
mesh.indices.emplace_back(i2s2, i2s1, i1s1);
|
|
mesh.indices.emplace_back(i1s2, i2s2, i1s1);
|
|
|
|
// To simplify further processing, we translate the mesh so that the
|
|
// last vertex of the pointing sphere (the pinpoint) will be at (0,0,0)
|
|
for(auto& p : mesh.points) z(p) -= (h + r_small_mm - penetration_mm);
|
|
}
|
|
|
|
void transform()
|
|
{
|
|
using Quaternion = Eigen::Quaternion<double>;
|
|
|
|
// We rotate the head to the specified direction The head's pointing
|
|
// side is facing upwards so this means that it would hold a support
|
|
// point with a normal pointing straight down. This is the reason of
|
|
// the -1 z coordinate
|
|
auto quatern = Quaternion::FromTwoVectors(Vec3d{0, 0, -1}, dir);
|
|
|
|
for(auto& p : mesh.points) p = quatern * p + tr;
|
|
}
|
|
|
|
double fullwidth() const {
|
|
return 2 * r_pin_mm + width_mm + 2*r_back_mm - penetration_mm;
|
|
}
|
|
|
|
Vec3d junction_point() const {
|
|
return tr + ( 2 * r_pin_mm + width_mm + r_back_mm - penetration_mm)*dir;
|
|
}
|
|
|
|
double request_pillar_radius(double radius) const {
|
|
const double rmax = r_back_mm;
|
|
return radius > 0 && radius < rmax ? radius : rmax;
|
|
}
|
|
};
|
|
|
|
struct Junction {
|
|
Contour3D mesh;
|
|
double r = 1;
|
|
size_t steps = 45;
|
|
Vec3d pos;
|
|
|
|
long id = -1;
|
|
|
|
Junction(const Vec3d& tr, double r_mm, size_t stepnum = 45):
|
|
r(r_mm), steps(stepnum), pos(tr)
|
|
{
|
|
mesh = sphere(r_mm, make_portion(0, PI), 2*PI/steps);
|
|
for(auto& p : mesh.points) p += tr;
|
|
}
|
|
};
|
|
|
|
struct Pillar {
|
|
Contour3D mesh;
|
|
Contour3D base;
|
|
double r = 1;
|
|
size_t steps = 0;
|
|
Vec3d endpoint;
|
|
|
|
long id = -1;
|
|
|
|
// If the pillar connects to a head, this is the id of that head
|
|
bool starts_from_head = true; // Could start from a junction as well
|
|
long start_junction_id = -1;
|
|
|
|
Pillar(const Vec3d& jp, const Vec3d& endp,
|
|
double radius = 1, size_t st = 45):
|
|
r(radius), steps(st), endpoint(endp), starts_from_head(false)
|
|
{
|
|
assert(steps > 0);
|
|
|
|
double h = jp(Z) - endp(Z);
|
|
assert(h > 0); // Endpoint is below the starting point
|
|
|
|
// We just create a bridge geometry with the pillar parameters and
|
|
// move the data.
|
|
Contour3D body = cylinder(radius, h, st, endp);
|
|
mesh.points.swap(body.points);
|
|
mesh.indices.swap(body.indices);
|
|
}
|
|
|
|
Pillar(const Junction& junc, const Vec3d& endp):
|
|
Pillar(junc.pos, endp, junc.r, junc.steps){}
|
|
|
|
Pillar(const Head& head, const Vec3d& endp, double radius = 1):
|
|
Pillar(head.junction_point(), endp, head.request_pillar_radius(radius),
|
|
head.steps)
|
|
{
|
|
}
|
|
|
|
void add_base(double height = 3, double radius = 2) {
|
|
if(height <= 0) return;
|
|
|
|
assert(steps >= 0);
|
|
auto last = int(steps - 1);
|
|
|
|
if(radius < r ) radius = r;
|
|
|
|
double a = 2*PI/steps;
|
|
double z = endpoint(2) + height;
|
|
|
|
for(size_t i = 0; i < steps; ++i) {
|
|
double phi = i*a;
|
|
double x = endpoint(0) + r*std::cos(phi);
|
|
double y = endpoint(1) + r*std::sin(phi);
|
|
base.points.emplace_back(x, y, z);
|
|
}
|
|
|
|
for(size_t i = 0; i < steps; ++i) {
|
|
double phi = i*a;
|
|
double x = endpoint(0) + radius*std::cos(phi);
|
|
double y = endpoint(1) + radius*std::sin(phi);
|
|
base.points.emplace_back(x, y, z - height);
|
|
}
|
|
|
|
auto ep = endpoint; ep(2) += height;
|
|
base.points.emplace_back(endpoint);
|
|
base.points.emplace_back(ep);
|
|
|
|
auto& indices = base.indices;
|
|
auto hcenter = int(base.points.size() - 1);
|
|
auto lcenter = int(base.points.size() - 2);
|
|
auto offs = int(steps);
|
|
for(int i = 0; i < last; ++i) {
|
|
indices.emplace_back(i, i + offs, offs + i + 1);
|
|
indices.emplace_back(i, offs + i + 1, i + 1);
|
|
indices.emplace_back(i, i + 1, hcenter);
|
|
indices.emplace_back(lcenter, offs + i + 1, offs + i);
|
|
}
|
|
|
|
indices.emplace_back(0, last, offs);
|
|
indices.emplace_back(last, offs + last, offs);
|
|
indices.emplace_back(hcenter, last, 0);
|
|
indices.emplace_back(offs, offs + last, lcenter);
|
|
|
|
}
|
|
|
|
bool has_base() const { return !base.points.empty(); }
|
|
};
|
|
|
|
// A Bridge between two pillars (with junction endpoints)
|
|
struct Bridge {
|
|
Contour3D mesh;
|
|
double r = 0.8;
|
|
|
|
long id = -1;
|
|
long start_jid = -1;
|
|
long end_jid = -1;
|
|
|
|
// We should reduce the radius a tiny bit to help the convex hull algorithm
|
|
Bridge(const Vec3d& j1, const Vec3d& j2,
|
|
double r_mm = 0.8, size_t steps = 45):
|
|
r(r_mm)
|
|
{
|
|
using Quaternion = Eigen::Quaternion<double>;
|
|
Vec3d dir = (j2 - j1).normalized();
|
|
double d = distance(j2, j1);
|
|
|
|
mesh = cylinder(r, d, steps);
|
|
|
|
auto quater = Quaternion::FromTwoVectors(Vec3d{0,0,1}, dir);
|
|
for(auto& p : mesh.points) p = quater * p + j1;
|
|
}
|
|
|
|
Bridge(const Junction& j1, const Junction& j2, double r_mm = 0.8):
|
|
Bridge(j1.pos, j2.pos, r_mm, j1.steps) {}
|
|
|
|
};
|
|
|
|
// A bridge that spans from model surface to model surface with small connecting
|
|
// edges on the endpoints. Used for headless support points.
|
|
struct CompactBridge {
|
|
Contour3D mesh;
|
|
long id = -1;
|
|
|
|
CompactBridge(const Vec3d& sp,
|
|
const Vec3d& ep,
|
|
const Vec3d& n,
|
|
double r,
|
|
size_t steps = 45)
|
|
{
|
|
Vec3d startp = sp + r * n;
|
|
Vec3d dir = (ep - startp).normalized();
|
|
Vec3d endp = ep - r * dir;
|
|
|
|
Bridge br(startp, endp, r, steps);
|
|
mesh.merge(br.mesh);
|
|
|
|
// now add the pins
|
|
double fa = 2*PI/steps;
|
|
auto upperball = sphere(r, Portion{PI / 2 - fa, PI}, fa);
|
|
for(auto& p : upperball.points) p += startp;
|
|
|
|
auto lowerball = sphere(r, Portion{0, PI/2 + 2*fa}, fa);
|
|
for(auto& p : lowerball.points) p += endp;
|
|
|
|
mesh.merge(upperball);
|
|
mesh.merge(lowerball);
|
|
}
|
|
};
|
|
|
|
// A wrapper struct around the base pool (pad)
|
|
struct Pad {
|
|
TriangleMesh tmesh;
|
|
PoolConfig cfg;
|
|
double zlevel = 0;
|
|
|
|
Pad() {}
|
|
|
|
Pad(const TriangleMesh& object_support_mesh,
|
|
const ExPolygons& baseplate,
|
|
double ground_level,
|
|
const PoolConfig& pcfg) :
|
|
cfg(pcfg),
|
|
zlevel(ground_level +
|
|
(sla::get_pad_fullheight(pcfg) - sla::get_pad_elevation(pcfg)) )
|
|
{
|
|
ExPolygons basep;
|
|
cfg.throw_on_cancel();
|
|
|
|
// The 0.1f is the layer height with which the mesh is sampled and then
|
|
// the layers are unified into one vector of polygons.
|
|
base_plate(object_support_mesh, basep,
|
|
float(cfg.min_wall_height_mm + cfg.min_wall_thickness_mm),
|
|
0.1f, pcfg.throw_on_cancel);
|
|
|
|
for(auto& bp : baseplate) basep.emplace_back(bp);
|
|
|
|
create_base_pool(basep, tmesh, cfg);
|
|
tmesh.translate(0, 0, float(zlevel));
|
|
}
|
|
|
|
bool empty() const { return tmesh.facets_count() == 0; }
|
|
};
|
|
|
|
// The minimum distance for two support points to remain valid.
|
|
static const double /*constexpr*/ D_SP = 0.1;
|
|
|
|
enum { // For indexing Eigen vectors as v(X), v(Y), v(Z) instead of numbers
|
|
X, Y, Z
|
|
};
|
|
|
|
PointSet to_point_set(const std::vector<Vec3d> &v)
|
|
{
|
|
PointSet ret(v.size(), 3);
|
|
{ long i = 0; for(const Vec3d& p : v) ret.row(i++) = p; }
|
|
return ret;
|
|
}
|
|
|
|
Vec3d model_coord(const ModelInstance& object, const Vec3f& mesh_coord) {
|
|
return object.transform_vector(mesh_coord.cast<double>());
|
|
}
|
|
|
|
inline double ray_mesh_intersect(const Vec3d& s,
|
|
const Vec3d& dir,
|
|
const EigenMesh3D& m)
|
|
{
|
|
return m.query_ray_hit(s, dir).distance();
|
|
}
|
|
|
|
// This function will test if a future pinhead would not collide with the model
|
|
// geometry. It does not take a 'Head' object because those are created after
|
|
// this test.
|
|
// Parameters:
|
|
// s: The touching point on the model surface.
|
|
// dir: This is the direction of the head from the pin to the back
|
|
// r_pin, r_back: the radiuses of the pin and the back sphere
|
|
// width: This is the full width from the pin center to the back center
|
|
// m: The object mesh
|
|
//
|
|
// Optional:
|
|
// samples: how many rays will be shot
|
|
// safety distance: This will be added to the radiuses to have a safety distance
|
|
// from the mesh.
|
|
double pinhead_mesh_intersect(const Vec3d& s,
|
|
const Vec3d& dir,
|
|
double r_pin,
|
|
double r_back,
|
|
double width,
|
|
const EigenMesh3D& m,
|
|
unsigned samples = 8,
|
|
double safety_distance = 0.001)
|
|
{
|
|
// 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 = safety_distance;
|
|
|
|
// 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::vector<double> phis(samples);
|
|
for(size_t i = 0; i < phis.size(); ++i) phis[i] = i*2*PI/phis.size();
|
|
|
|
a(Z) = -(v(X)*a(X) + v(Y)*a(Y)) / v(Z);
|
|
|
|
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
|
|
|
|
tbb::parallel_for(size_t(0), phis.size(),
|
|
[&phis, &m, sd, r_pin, r_back, s, a, b, c](size_t i)
|
|
{
|
|
double& phi = phis[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. So we query the closest
|
|
// point on the mesh to this.
|
|
// auto psq = m.signed_distance(ps);
|
|
|
|
// 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 - psq.point_on_mesh()).normalized();
|
|
// phi = m.query_ray_hit(psq.point_on_mesh() + sd*n, n);
|
|
|
|
Vec3d n = (p - ps).normalized();
|
|
auto hr = m.query_ray_hit(ps + sd*n, n);
|
|
|
|
if(hr.is_inside()) { // the hit is inside the model
|
|
if(hr.distance() > 2*r_pin) phi = 0;
|
|
else {
|
|
// re-cast the ray from the outside of the object
|
|
auto hr2 = m.query_ray_hit(ps + (hr.distance() + 2*sd)*n, n);
|
|
phi = hr2.distance();
|
|
}
|
|
} else phi = hr.distance();
|
|
});
|
|
|
|
auto mit = std::min_element(phis.begin(), phis.end());
|
|
|
|
return *mit;
|
|
}
|
|
|
|
// Checking bridge (pillar and stick as well) intersection with the model. If
|
|
// the function is used for headless sticks, the ins_check parameter have to be
|
|
// true as the beginning of the stick might be inside the model geometry.
|
|
double bridge_mesh_intersect(const Vec3d& s,
|
|
const Vec3d& dir,
|
|
double r,
|
|
const EigenMesh3D& m,
|
|
bool ins_check = false,
|
|
unsigned samples = 4,
|
|
double safety_distance = 0.001)
|
|
{
|
|
// helper vector calculations
|
|
Vec3d a(0, 1, 0), b;
|
|
const double& sd = safety_distance;
|
|
|
|
a(Z) = -(dir(X)*a(X) + dir(Y)*a(Y)) / dir(Z);
|
|
b = a.cross(dir);
|
|
|
|
// circle portions
|
|
std::vector<double> phis(samples);
|
|
for(size_t i = 0; i < phis.size(); ++i) phis[i] = i*2*PI/phis.size();
|
|
|
|
tbb::parallel_for(size_t(0), phis.size(),
|
|
[&phis, &m, a, b, sd, dir, r, s, ins_check](size_t i)
|
|
{
|
|
double& phi = phis[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) phi = 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);
|
|
phi = hr2.distance();
|
|
}
|
|
} else phi = hr.distance();
|
|
});
|
|
|
|
auto mit = std::min_element(phis.begin(), phis.end());
|
|
|
|
return *mit;
|
|
}
|
|
|
|
PointSet normals(const PointSet& points, const EigenMesh3D& mesh,
|
|
double eps = 0.05, // min distance from edges
|
|
std::function<void()> throw_on_cancel = [](){});
|
|
|
|
inline Vec2d to_vec2(const Vec3d& v3) {
|
|
return {v3(X), v3(Y)};
|
|
}
|
|
|
|
bool operator==(const SpatElement& e1, const SpatElement& e2) {
|
|
return e1.second == e2.second;
|
|
}
|
|
|
|
// Clustering a set of points by the given criteria
|
|
ClusteredPoints cluster(
|
|
const PointSet& points,
|
|
std::function<bool(const SpatElement&, const SpatElement&)> pred,
|
|
unsigned max_points = 0);
|
|
|
|
// This class will hold the support tree meshes with some additional bookkeeping
|
|
// as well. Various parts of the support geometry are stored separately and are
|
|
// merged when the caller queries the merged mesh. The merged result is cached
|
|
// for fast subsequent delivery of the merged mesh which can be quite complex.
|
|
// An object of this class will be used as the result type during the support
|
|
// generation algorithm. Parts will be added with the appropriate methods such
|
|
// as add_head or add_pillar which forwards the constructor arguments and fills
|
|
// the IDs of these substructures. The IDs are basically indices into the arrays
|
|
// of the appropriate type (heads, pillars, etc...). One can later query e.g. a
|
|
// pillar for a specific head...
|
|
//
|
|
// The support pad is considered an auxiliary geometry and is not part of the
|
|
// merged mesh. It can be retrieved using a dedicated method (pad())
|
|
class SLASupportTree::Impl {
|
|
std::vector<Head> m_heads;
|
|
std::vector<Pillar> m_pillars;
|
|
std::vector<Junction> m_junctions;
|
|
std::vector<Bridge> m_bridges;
|
|
std::vector<CompactBridge> m_compact_bridges;
|
|
Controller m_ctl;
|
|
|
|
Pad m_pad;
|
|
mutable TriangleMesh meshcache; mutable bool meshcache_valid = false;
|
|
mutable double model_height = 0; // the full height of the model
|
|
public:
|
|
double ground_level = 0;
|
|
|
|
Impl() = default;
|
|
inline Impl(const Controller& ctl): m_ctl(ctl) {}
|
|
|
|
const Controller& ctl() const { return m_ctl; }
|
|
|
|
template<class...Args> Head& add_head(Args&&... args) {
|
|
m_heads.emplace_back(std::forward<Args>(args)...);
|
|
m_heads.back().id = long(m_heads.size() - 1);
|
|
meshcache_valid = false;
|
|
return m_heads.back();
|
|
}
|
|
|
|
template<class...Args> Pillar& add_pillar(long headid, Args&&... args) {
|
|
assert(headid >= 0 && headid < m_heads.size());
|
|
Head& head = m_heads[size_t(headid)];
|
|
m_pillars.emplace_back(head, std::forward<Args>(args)...);
|
|
Pillar& pillar = m_pillars.back();
|
|
pillar.id = long(m_pillars.size() - 1);
|
|
head.pillar_id = pillar.id;
|
|
pillar.start_junction_id = head.id;
|
|
pillar.starts_from_head = true;
|
|
meshcache_valid = false;
|
|
return m_pillars.back();
|
|
}
|
|
|
|
const Head& pillar_head(long pillar_id) const {
|
|
assert(pillar_id >= 0 && pillar_id < m_pillars.size());
|
|
const Pillar& p = m_pillars[size_t(pillar_id)];
|
|
assert(p.starts_from_head && p.start_junction_id >= 0 &&
|
|
p.start_junction_id < m_heads.size() );
|
|
return m_heads[size_t(p.start_junction_id)];
|
|
}
|
|
|
|
const Pillar& head_pillar(long headid) const {
|
|
assert(headid >= 0 && headid < m_heads.size());
|
|
const Head& h = m_heads[size_t(headid)];
|
|
assert(h.pillar_id >= 0 && h.pillar_id < m_pillars.size());
|
|
return m_pillars[size_t(h.pillar_id)];
|
|
}
|
|
|
|
template<class...Args> const Junction& add_junction(Args&&... args) {
|
|
m_junctions.emplace_back(std::forward<Args>(args)...);
|
|
m_junctions.back().id = long(m_junctions.size() - 1);
|
|
meshcache_valid = false;
|
|
return m_junctions.back();
|
|
}
|
|
|
|
template<class...Args> const Bridge& add_bridge(Args&&... args) {
|
|
m_bridges.emplace_back(std::forward<Args>(args)...);
|
|
m_bridges.back().id = long(m_bridges.size() - 1);
|
|
meshcache_valid = false;
|
|
return m_bridges.back();
|
|
}
|
|
|
|
template<class...Args>
|
|
const CompactBridge& add_compact_bridge(Args&&...args) {
|
|
m_compact_bridges.emplace_back(std::forward<Args>(args)...);
|
|
m_compact_bridges.back().id = long(m_compact_bridges.size() - 1);
|
|
meshcache_valid = false;
|
|
return m_compact_bridges.back();
|
|
}
|
|
|
|
const std::vector<Head>& heads() const { return m_heads; }
|
|
Head& head(size_t idx) { meshcache_valid = false; return m_heads[idx]; }
|
|
const std::vector<Pillar>& pillars() const { return m_pillars; }
|
|
const std::vector<Bridge>& bridges() const { return m_bridges; }
|
|
const std::vector<Junction>& junctions() const { return m_junctions; }
|
|
const std::vector<CompactBridge>& compact_bridges() const {
|
|
return m_compact_bridges;
|
|
}
|
|
|
|
const Pad& create_pad(const TriangleMesh& object_supports,
|
|
const ExPolygons& baseplate,
|
|
const PoolConfig& cfg) {
|
|
m_pad = Pad(object_supports, baseplate, ground_level, cfg);
|
|
return m_pad;
|
|
}
|
|
|
|
void remove_pad() {
|
|
m_pad = Pad();
|
|
}
|
|
|
|
const Pad& pad() const { return m_pad; }
|
|
|
|
// WITHOUT THE PAD!!!
|
|
const TriangleMesh& merged_mesh() const {
|
|
if(meshcache_valid) return meshcache;
|
|
|
|
Contour3D merged;
|
|
|
|
for(auto& head : heads()) {
|
|
if(m_ctl.stopcondition()) break;
|
|
if(head.is_valid())
|
|
merged.merge(head.mesh);
|
|
}
|
|
|
|
for(auto& stick : pillars()) {
|
|
if(m_ctl.stopcondition()) break;
|
|
merged.merge(stick.mesh);
|
|
merged.merge(stick.base);
|
|
}
|
|
|
|
for(auto& j : junctions()) {
|
|
if(m_ctl.stopcondition()) break;
|
|
merged.merge(j.mesh);
|
|
}
|
|
|
|
for(auto& cb : compact_bridges()) {
|
|
if(m_ctl.stopcondition()) break;
|
|
merged.merge(cb.mesh);
|
|
}
|
|
|
|
for(auto& bs : bridges()) {
|
|
if(m_ctl.stopcondition()) break;
|
|
merged.merge(bs.mesh);
|
|
}
|
|
|
|
|
|
if(m_ctl.stopcondition()) {
|
|
// In case of failure we have to return an empty mesh
|
|
meshcache = TriangleMesh();
|
|
return meshcache;
|
|
}
|
|
|
|
meshcache = mesh(merged);
|
|
|
|
// TODO: Is this necessary?
|
|
//meshcache.repair();
|
|
|
|
BoundingBoxf3&& bb = meshcache.bounding_box();
|
|
model_height = bb.max(Z) - bb.min(Z);
|
|
|
|
meshcache_valid = true;
|
|
return meshcache;
|
|
}
|
|
|
|
// WITH THE PAD
|
|
double full_height() const {
|
|
if(merged_mesh().empty() && !pad().empty())
|
|
return get_pad_fullheight(pad().cfg);
|
|
|
|
double h = mesh_height();
|
|
if(!pad().empty()) h += sla::get_pad_elevation(pad().cfg);
|
|
return h;
|
|
}
|
|
|
|
// WITHOUT THE PAD!!!
|
|
double mesh_height() const {
|
|
if(!meshcache_valid) merged_mesh();
|
|
return model_height;
|
|
}
|
|
|
|
};
|
|
|
|
template<class DistFn>
|
|
long cluster_centroid(const ClusterEl& clust,
|
|
std::function<Vec3d(size_t)> pointfn,
|
|
DistFn df)
|
|
{
|
|
switch(clust.size()) {
|
|
case 0: /* empty cluster */ return -1;
|
|
case 1: /* only one element */ return 0;
|
|
case 2: /* if two elements, there is no center */ return 0;
|
|
default: ;
|
|
}
|
|
|
|
// The function works by calculating for each point the average distance
|
|
// from all the other points in the cluster. We create a selector bitmask of
|
|
// the same size as the cluster. The bitmask will have two true bits and
|
|
// false bits for the rest of items and we will loop through all the
|
|
// permutations of the bitmask (combinations of two points). Get the
|
|
// distance for the two points and add the distance to the averages.
|
|
// The point with the smallest average than wins.
|
|
|
|
std::vector<bool> sel(clust.size(), false); // create full zero bitmask
|
|
std::fill(sel.end() - 2, sel.end(), true); // insert the two ones
|
|
std::vector<double> avgs(clust.size(), 0.0); // store the average distances
|
|
|
|
do {
|
|
std::array<size_t, 2> idx;
|
|
for(size_t i = 0, j = 0; i < clust.size(); i++) if(sel[i]) idx[j++] = i;
|
|
|
|
double d = df(pointfn(clust[idx[0]]),
|
|
pointfn(clust[idx[1]]));
|
|
|
|
// add the distance to the sums for both associated points
|
|
for(auto i : idx) avgs[i] += d;
|
|
|
|
// now continue with the next permutation of the bitmask with two 1s
|
|
} while(std::next_permutation(sel.begin(), sel.end()));
|
|
|
|
// Divide by point size in the cluster to get the average (may be redundant)
|
|
for(auto& a : avgs) a /= clust.size();
|
|
|
|
// get the lowest average distance and return the index
|
|
auto minit = std::min_element(avgs.begin(), avgs.end());
|
|
return long(minit - avgs.begin());
|
|
}
|
|
|
|
/**
|
|
* This function will calculate the convex hull of the input point set and
|
|
* return the indices of those points belonging to the chull in the right
|
|
* (counter clockwise) order. The input is also the set of indices and a
|
|
* functor to get the actual point form the index.
|
|
*
|
|
* I've adapted this algorithm from here:
|
|
* https://www.geeksforgeeks.org/convex-hull-set-1-jarviss-algorithm-or-wrapping/
|
|
* and modified it so that it starts with the leftmost lower vertex. Also added
|
|
* support for floating point coordinates.
|
|
*
|
|
* This function is a modded version of the standard convex hull. If the points
|
|
* are all collinear with each other, it will return their indices in spatially
|
|
* subsequent order (the order they appear on the screen).
|
|
*/
|
|
ClusterEl pts_convex_hull(const ClusterEl& inpts,
|
|
std::function<Vec2d(unsigned)> pfn)
|
|
{
|
|
using Point = Vec2d;
|
|
using std::vector;
|
|
|
|
static const double ERR = 1e-3;
|
|
|
|
auto orientation = [](const Point& p, const Point& q, const Point& r)
|
|
{
|
|
double val = (q(Y) - p(Y)) * (r(X) - q(X)) -
|
|
(q(X) - p(X)) * (r(Y) - q(Y));
|
|
|
|
if (std::abs(val) < ERR) return 0; // collinear
|
|
return (val > ERR)? 1: 2; // clock or counterclockwise
|
|
};
|
|
|
|
size_t n = inpts.size();
|
|
|
|
if (n < 3) return inpts;
|
|
|
|
// Initialize Result
|
|
ClusterEl hull;
|
|
vector<Point> points; points.reserve(n);
|
|
for(auto i : inpts) {
|
|
points.emplace_back(pfn(i));
|
|
}
|
|
|
|
// Check if the triplet of points is collinear. The standard convex hull
|
|
// algorithms are not capable of handling such input properly.
|
|
bool collinear = true;
|
|
for(auto one = points.begin(), two = std::next(one), three = std::next(two);
|
|
three != points.end() && collinear;
|
|
++one, ++two, ++three)
|
|
{
|
|
// check if the points are collinear
|
|
if(orientation(*one, *two, *three) != 0) collinear = false;
|
|
}
|
|
|
|
// Find the leftmost (bottom) point
|
|
size_t l = 0;
|
|
for (size_t i = 1; i < n; i++) {
|
|
if(std::abs(points[i](X) - points[l](X)) < ERR) {
|
|
if(points[i](Y) < points[l](Y)) l = i;
|
|
}
|
|
else if (points[i](X) < points[l](X)) l = i;
|
|
}
|
|
|
|
if(collinear) {
|
|
// fill the output with the spatially ordered set of points.
|
|
|
|
// find the direction
|
|
hull = inpts;
|
|
auto& lp = points[l];
|
|
std::sort(hull.begin(), hull.end(),
|
|
[&lp, points](unsigned i1, unsigned i2) {
|
|
// compare the distance from the leftmost point
|
|
return distance(lp, points[i1]) < distance(lp, points[i2]);
|
|
});
|
|
|
|
return hull;
|
|
}
|
|
|
|
// TODO: this algorithm is O(m*n) and O(n^2) in the worst case so it needs
|
|
// to be replaced with a graham scan or something O(nlogn)
|
|
|
|
// Start from leftmost point, keep moving counterclockwise
|
|
// until reach the start point again. This loop runs O(h)
|
|
// times where h is number of points in result or output.
|
|
size_t p = l;
|
|
do
|
|
{
|
|
// Add current point to result
|
|
hull.push_back(inpts[p]);
|
|
|
|
// Search for a point 'q' such that orientation(p, x,
|
|
// q) is counterclockwise for all points 'x'. The idea
|
|
// is to keep track of last visited most counterclock-
|
|
// wise point in q. If any point 'i' is more counterclock-
|
|
// wise than q, then update q.
|
|
size_t q = (p + 1) % n;
|
|
for (size_t i = 0; i < n; i++)
|
|
{
|
|
// If i is more counterclockwise than current q, then
|
|
// update q
|
|
if (orientation(points[p], points[i], points[q]) == 2) q = i;
|
|
}
|
|
|
|
// Now q is the most counterclockwise with respect to p
|
|
// Set p as q for next iteration, so that q is added to
|
|
// result 'hull'
|
|
p = q;
|
|
|
|
} while (p != l); // While we don't come to first point
|
|
|
|
auto first = hull.front();
|
|
hull.emplace_back(first);
|
|
|
|
return hull;
|
|
}
|
|
|
|
Vec3d dirv(const Vec3d& startp, const Vec3d& endp) {
|
|
return (endp - startp).normalized();
|
|
}
|
|
|
|
/// Generation of the supports, entry point function. This is called from the
|
|
/// SLASupportTree constructor and throws an SLASupportsStoppedException if it
|
|
/// gets canceled by the ctl object's stopcondition functor.
|
|
bool SLASupportTree::generate(const PointSet &points,
|
|
const EigenMesh3D& mesh,
|
|
const SupportConfig &cfg,
|
|
const Controller &ctl)
|
|
{
|
|
// If there are no input points there is no point in doing anything
|
|
if(points.rows() == 0) return false;
|
|
|
|
PointSet filtered_points; // all valid support points
|
|
PointSet head_positions; // support points with pinhead
|
|
PointSet head_normals; // head normals
|
|
PointSet headless_positions; // headless support points
|
|
PointSet headless_normals; // headless support point normals
|
|
|
|
using IndexSet = std::vector<unsigned>;
|
|
|
|
// Distances from head positions to ground or mesh touch points
|
|
std::vector<double> head_heights;
|
|
|
|
// Indices of those who touch the ground
|
|
IndexSet ground_heads;
|
|
|
|
// Indices of those who don't touch the ground
|
|
IndexSet noground_heads;
|
|
|
|
// Groups of the 'ground_head' indices that belong into one cluster. These
|
|
// are candidates to be connected to one pillar.
|
|
ClusteredPoints ground_connectors;
|
|
|
|
// A help function to translate ground head index to the actual coordinates.
|
|
auto gnd_head_pt = [&ground_heads, &head_positions] (size_t idx) {
|
|
return Vec3d(head_positions.row(ground_heads[idx]));
|
|
};
|
|
|
|
// This algorithm uses the Impl class as its output stream. It will be
|
|
// filled gradually with support elements (heads, pillars, bridges, ...)
|
|
using Result = SLASupportTree::Impl;
|
|
Result& result = *m_impl;
|
|
|
|
// 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,
|
|
HEADLESS,
|
|
DONE,
|
|
HALT,
|
|
ABORT,
|
|
NUM_STEPS
|
|
//...
|
|
};
|
|
|
|
// t-hrow i-f c-ance-l-ed: It will be called many times so a shorthand will
|
|
// come in handy.
|
|
auto& tifcl = ctl.cancelfn;
|
|
|
|
// Filtering step: here we will discard inappropriate support points and
|
|
// decide the future of the appropriate ones. We will check if a pinhead
|
|
// is applicable and adjust its angle at each support point.
|
|
// We will also merge the support points that are just too close and can be
|
|
// considered as one.
|
|
auto filterfn = [tifcl] (
|
|
const SupportConfig& cfg,
|
|
const PointSet& points,
|
|
const EigenMesh3D& mesh,
|
|
PointSet& filt_pts,
|
|
PointSet& head_norm,
|
|
PointSet& head_pos,
|
|
PointSet& headless_pos,
|
|
PointSet& headless_norm)
|
|
{
|
|
// Get the points that are too close to each other and keep only the
|
|
// first one
|
|
auto aliases =
|
|
cluster(points,
|
|
[tifcl](const SpatElement& p, const SpatElement& se)
|
|
{
|
|
tifcl();
|
|
return distance(p.first, se.first) < D_SP;
|
|
}, 2);
|
|
|
|
filt_pts.resize(Eigen::Index(aliases.size()), 3);
|
|
int count = 0;
|
|
for(auto& a : aliases) {
|
|
// Here we keep only the front point of the cluster.
|
|
filt_pts.row(count++) = points.row(a.front());
|
|
}
|
|
|
|
tifcl();
|
|
|
|
// calculate the normals to the triangles belonging to filtered points
|
|
auto nmls = sla::normals(filt_pts, mesh, cfg.head_front_radius_mm, tifcl);
|
|
|
|
head_norm.resize(count, 3);
|
|
head_pos.resize(count, 3);
|
|
headless_pos.resize(count, 3);
|
|
headless_norm.resize(count, 3);
|
|
|
|
// 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.
|
|
|
|
int pcount = 0, hlcount = 0;
|
|
for(int i = 0; i < count; i++) {
|
|
tifcl();
|
|
auto n = nmls.row(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 - cfg.normal_cutoff_angle) {
|
|
|
|
// We saturate the polar angle to 3pi/4
|
|
polar = std::max(polar, 3*PI / 4);
|
|
|
|
// Reassemble the now corrected normal
|
|
Vec3d nn(std::cos(azimuth) * std::sin(polar),
|
|
std::sin(azimuth) * std::sin(polar),
|
|
std::cos(polar));
|
|
|
|
nn.normalize();
|
|
|
|
// save the head (pinpoint) position
|
|
Vec3d hp = filt_pts.row(i);
|
|
|
|
// the full width of the head
|
|
double w = cfg.head_width_mm +
|
|
cfg.head_back_radius_mm +
|
|
2*cfg.head_front_radius_mm;
|
|
|
|
// We should shoot a ray in the direction of the pinhead and
|
|
// see if there is enough space for it
|
|
double t = pinhead_mesh_intersect(
|
|
hp, // touching point
|
|
nn,
|
|
cfg.head_front_radius_mm, // approx the radius
|
|
cfg.head_back_radius_mm,
|
|
w,
|
|
mesh);
|
|
|
|
if(t > w || std::isinf(t)) {
|
|
head_pos.row(pcount) = hp;
|
|
|
|
// save the verified and corrected normal
|
|
head_norm.row(pcount) = nn;
|
|
|
|
++pcount;
|
|
} else if( polar >= 3*PI/4 ) {
|
|
// Headless supports do not tilt like the headed ones so
|
|
// the normal should point almost to the ground.
|
|
headless_norm.row(hlcount) = nn;
|
|
headless_pos.row(hlcount++) = hp;
|
|
}
|
|
}
|
|
}
|
|
|
|
head_pos.conservativeResize(pcount, Eigen::NoChange);
|
|
head_norm.conservativeResize(pcount, Eigen::NoChange);
|
|
headless_pos.conservativeResize(hlcount, Eigen::NoChange);
|
|
headless_norm.conservativeResize(hlcount, Eigen::NoChange);
|
|
};
|
|
|
|
// Pinhead creation: based on the filtering results, the Head objects will
|
|
// be constructed (together with their triangle meshes).
|
|
auto pinheadfn = [tifcl] (
|
|
const SupportConfig& cfg,
|
|
PointSet& head_pos,
|
|
PointSet& nmls,
|
|
Result& result
|
|
)
|
|
{
|
|
|
|
/* ******************************************************** */
|
|
/* Generating Pinheads */
|
|
/* ******************************************************** */
|
|
|
|
for (int i = 0; i < head_pos.rows(); ++i) {
|
|
tifcl();
|
|
result.add_head(
|
|
cfg.head_back_radius_mm,
|
|
cfg.head_front_radius_mm,
|
|
cfg.head_width_mm,
|
|
cfg.head_penetration_mm,
|
|
nmls.row(i), // dir
|
|
head_pos.row(i) // displacement
|
|
);
|
|
}
|
|
};
|
|
|
|
// Further classification of the support points with pinheads. If the
|
|
// ground is directly reachable through a vertical line parallel to the Z
|
|
// axis we consider a support point as pillar candidate. If touches the
|
|
// model geometry, it will be marked as non-ground facing and further steps
|
|
// will process it. Also, the pillars will be grouped into clusters that can
|
|
// be interconnected with bridges. Elements of these groups may or may not
|
|
// be interconnected. Here we only run the clustering algorithm.
|
|
auto classifyfn = [tifcl] (
|
|
const SupportConfig& cfg,
|
|
const EigenMesh3D& mesh,
|
|
PointSet& head_pos,
|
|
IndexSet& gndidx,
|
|
IndexSet& nogndidx,
|
|
std::vector<double>& gndheight,
|
|
ClusteredPoints& ground_clusters,
|
|
Result& result
|
|
) {
|
|
|
|
/* ******************************************************** */
|
|
/* Classification */
|
|
/* ******************************************************** */
|
|
|
|
// We should first get the heads that reach the ground directly
|
|
gndheight.reserve(size_t(head_pos.rows()));
|
|
gndidx.reserve(size_t(head_pos.rows()));
|
|
nogndidx.reserve(size_t(head_pos.rows()));
|
|
|
|
// First we search 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 = 0; i < head_pos.rows(); i++) {
|
|
tifcl();
|
|
auto& head = result.head(i);
|
|
|
|
Vec3d dir(0, 0, -1);
|
|
bool accept = false;
|
|
int ri = 1;
|
|
double t = std::numeric_limits<double>::infinity();
|
|
double hw = head.width_mm;
|
|
|
|
// We will try to assign a pillar to all the pinheads. If a pillar
|
|
// would pierce the model surface, we will try to adjust slightly
|
|
// the head with so that the pillar can be deployed.
|
|
while(!accept && head.width_mm > 0) {
|
|
|
|
Vec3d startpoint = head.junction_point();
|
|
|
|
// Collision detection
|
|
t = bridge_mesh_intersect(startpoint, dir, head.r_back_mm, mesh);
|
|
|
|
// Precise distance measurement
|
|
double tprec = ray_mesh_intersect(startpoint, dir, mesh);
|
|
|
|
if(std::isinf(tprec) && !std::isinf(t)) {
|
|
// This is a damned case where the pillar melds into the
|
|
// model but its center ray can reach the ground. We can
|
|
// not route this to the ground nor to the model surface.
|
|
head.width_mm = hw + (ri % 2? -1 : 1) * ri * head.r_back_mm;
|
|
} else {
|
|
accept = true; t = tprec;
|
|
|
|
auto id = head.id;
|
|
// We need to regenerate the head geometry
|
|
head = Head(head.r_back_mm,
|
|
head.r_pin_mm,
|
|
head.width_mm,
|
|
head.penetration_mm,
|
|
head.dir,
|
|
head.tr);
|
|
head.id = id;
|
|
}
|
|
|
|
ri++;
|
|
}
|
|
|
|
// Save the distance from a surface in the Z axis downwards. It may
|
|
// be infinity but that is telling us that it touches the ground.
|
|
gndheight.emplace_back(t);
|
|
|
|
if(accept) {
|
|
if(std::isinf(t)) gndidx.emplace_back(i);
|
|
else nogndidx.emplace_back(i);
|
|
} else {
|
|
// This is a serious issue. There was no way to deploy a pillar
|
|
// for the given pinhead. The whole thing has to be discarded
|
|
// leaving the model potentially unprintable.
|
|
//
|
|
// TODO: In the future this has to be solved by searching for
|
|
// a path in 3D space from this support point to a suitable
|
|
// pillar position or an existing pillar.
|
|
// As a workaround we could mark this head as "sidehead only"
|
|
// let it go trough the nearby pillar search in the next step.
|
|
BOOST_LOG_TRIVIAL(warning) << "A support point at "
|
|
<< head.tr.transpose()
|
|
<< " had to be discarded as there is"
|
|
<< " nowhere to route it.";
|
|
head.invalidate();
|
|
}
|
|
}
|
|
|
|
// Transform the ground facing point indices top actual coordinates.
|
|
PointSet gnd(gndidx.size(), 3);
|
|
for(size_t i = 0; i < gndidx.size(); i++)
|
|
gnd.row(long(i)) = head_pos.row(gndidx[i]);
|
|
|
|
// 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 d_base = 2*cfg.base_radius_mm;
|
|
ground_clusters =
|
|
cluster(
|
|
gnd,
|
|
[d_base, tifcl](const SpatElement& p, const SpatElement& s)
|
|
{
|
|
tifcl();
|
|
return distance(Vec2d(p.first(X), p.first(Y)),
|
|
Vec2d(s.first(X), s.first(Y))) < d_base;
|
|
}, 3); // max 3 heads to connect to one centroid
|
|
};
|
|
|
|
// Helper function for interconnecting two pillars with zig-zag bridges.
|
|
// This is not an individual step.
|
|
auto interconnect = [&cfg](
|
|
const Pillar& pillar,
|
|
const Pillar& nextpillar,
|
|
const EigenMesh3D& emesh,
|
|
Result& result)
|
|
{
|
|
const Head& phead = result.pillar_head(pillar.id);
|
|
const Head& nextphead = result.pillar_head(nextpillar.id);
|
|
|
|
Vec3d sj = phead.junction_point();
|
|
sj(Z) = std::min(sj(Z), nextphead.junction_point()(Z));
|
|
Vec3d ej = nextpillar.endpoint;
|
|
double pillar_dist = distance(Vec2d{sj(X), sj(Y)},
|
|
Vec2d{ej(X), ej(Y)});
|
|
double zstep = pillar_dist * std::tan(-cfg.tilt);
|
|
ej(Z) = sj(Z) + zstep;
|
|
|
|
double chkd = bridge_mesh_intersect(sj, dirv(sj, ej), pillar.r, emesh);
|
|
double bridge_distance = pillar_dist / std::cos(-cfg.tilt);
|
|
|
|
// If the pillars are so close that they touch each other,
|
|
// there is no need to bridge them together.
|
|
if(pillar_dist > 2*cfg.head_back_radius_mm &&
|
|
bridge_distance < cfg.max_bridge_length_mm)
|
|
while(sj(Z) > pillar.endpoint(Z) + cfg.base_radius_mm &&
|
|
ej(Z) > nextpillar.endpoint(Z) + cfg.base_radius_mm)
|
|
{
|
|
if(chkd >= bridge_distance) {
|
|
result.add_bridge(sj, ej, pillar.r);
|
|
|
|
auto pcm = cfg.pillar_connection_mode;
|
|
|
|
// double bridging: (crosses)
|
|
if( pcm == PillarConnectionMode::cross ||
|
|
(pcm == PillarConnectionMode::dynamic &&
|
|
pillar_dist > 2*cfg.base_radius_mm))
|
|
{
|
|
// If the columns are close together, no need to
|
|
// double bridge them
|
|
Vec3d bsj(ej(X), ej(Y), sj(Z));
|
|
Vec3d bej(sj(X), sj(Y), ej(Z));
|
|
|
|
// need to check collision for the cross stick
|
|
double backchkd = bridge_mesh_intersect(bsj,
|
|
dirv(bsj, bej),
|
|
pillar.r,
|
|
emesh);
|
|
|
|
|
|
if(backchkd >= bridge_distance) {
|
|
result.add_bridge(bsj, bej, pillar.r);
|
|
}
|
|
}
|
|
}
|
|
sj.swap(ej);
|
|
ej(Z) = sj(Z) + zstep;
|
|
chkd = bridge_mesh_intersect(sj, dirv(sj, ej), pillar.r, emesh);
|
|
}
|
|
};
|
|
|
|
// Step: Routing the ground connected pinheads, and interconnecting them
|
|
// with additional (angled) bridges. Not all of these pinheads will be
|
|
// a full pillar (ground connected). Some will connect to a nearby pillar
|
|
// using a bridge. The max number of such side-heads for a central pillar
|
|
// is limited to avoid bad weight distribution.
|
|
auto routing_ground_fn = [gnd_head_pt, interconnect, tifcl](
|
|
const SupportConfig& cfg,
|
|
const ClusteredPoints& gnd_clusters,
|
|
const IndexSet& gndidx,
|
|
const EigenMesh3D& emesh,
|
|
Result& result)
|
|
{
|
|
const double hbr = cfg.head_back_radius_mm;
|
|
const double pradius = cfg.head_back_radius_mm;
|
|
const double maxbridgelen = cfg.max_bridge_length_mm;
|
|
const double gndlvl = result.ground_level;
|
|
|
|
ClusterEl cl_centroids;
|
|
cl_centroids.reserve(gnd_clusters.size());
|
|
|
|
SpatIndex pheadindex; // spatial index for the junctions
|
|
for(auto& cl : gnd_clusters) { tifcl();
|
|
// 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
|
|
long lcid = cluster_centroid(cl, gnd_head_pt,
|
|
[tifcl](const Vec3d& p1, const Vec3d& p2)
|
|
{
|
|
tifcl();
|
|
return distance(Vec2d(p1(X), p1(Y)), Vec2d(p2(X), p2(Y)));
|
|
});
|
|
|
|
assert(lcid >= 0);
|
|
auto cid = unsigned(lcid);
|
|
|
|
cl_centroids.push_back(unsigned(cid));
|
|
|
|
unsigned hid = gndidx[cl[cid]]; // Head index
|
|
Head& h = result.head(hid);
|
|
h.transform();
|
|
Vec3d p = h.junction_point(); p(Z) = gndlvl;
|
|
|
|
pheadindex.insert(p, hid);
|
|
}
|
|
|
|
// 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 : gnd_clusters) { tifcl();
|
|
|
|
auto cidx = cl_centroids[ci];
|
|
cl_centroids[ci++] = cl[cidx];
|
|
|
|
size_t index_to_heads = gndidx[cl[cidx]];
|
|
auto& head = result.head(index_to_heads);
|
|
|
|
Vec3d startpoint = head.junction_point();
|
|
auto endpoint = startpoint; endpoint(Z) = gndlvl;
|
|
|
|
// Create the central pillar of the cluster with its base on the
|
|
// ground
|
|
result.add_pillar(long(index_to_heads), endpoint, pradius)
|
|
.add_base(cfg.base_height_mm, cfg.base_radius_mm);
|
|
|
|
// Process side point in current cluster
|
|
cl.erase(cl.begin() + cidx); // delete the centroid before looping
|
|
|
|
// TODO: dont 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 search_nearest =
|
|
[&tifcl, &cfg, &result, &emesh, maxbridgelen, gndlvl, pradius]
|
|
(SpatIndex& spindex, const Vec3d& jsh)
|
|
{
|
|
long nearest_id = -1;
|
|
const double max_len = maxbridgelen / 2;
|
|
while(nearest_id < 0 && !spindex.empty()) { tifcl();
|
|
// 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(jsh(X), jsh(Y), gndlvl);
|
|
auto ne = spindex.nearest(qp, 1).front();
|
|
const Head& nearhead = result.heads()[ne.second];
|
|
|
|
Vec3d jh = nearhead.junction_point();
|
|
Vec3d jp = jsh;
|
|
double dist2d = distance(qp, ne.first);
|
|
|
|
// Bridge endpoint on the main pillar
|
|
Vec3d jn(jh(X), jh(Y), jp(Z) + dist2d*std::tan(-cfg.tilt));
|
|
|
|
if(jn(Z) > jh(Z)) {
|
|
// If the sidepoint cannot connect to the pillar from
|
|
// its head junction, then just skip this pillar.
|
|
spindex.remove(ne);
|
|
continue;
|
|
}
|
|
|
|
double d = distance(jp, jn);
|
|
|
|
if(jn(Z) <= gndlvl + 2*cfg.head_width_mm || d > max_len)
|
|
break;
|
|
|
|
double chkd = bridge_mesh_intersect(jp, dirv(jp, jn),
|
|
pradius,
|
|
emesh);
|
|
if(chkd >= d) nearest_id = ne.second;
|
|
|
|
spindex.remove(ne);
|
|
}
|
|
return nearest_id;
|
|
};
|
|
|
|
for(auto c : cl) { tifcl();
|
|
auto& sidehead = result.head(gndidx[c]);
|
|
sidehead.transform();
|
|
|
|
Vec3d jsh = sidehead.junction_point();
|
|
SpatIndex spindex = pheadindex;
|
|
long nearest_id = search_nearest(spindex, jsh);
|
|
|
|
// at this point we either have our pillar index or we have
|
|
// to connect the sidehead to the ground
|
|
if(nearest_id < 0) {
|
|
// Could not find a pillar, create one
|
|
Vec3d jp = jsh; jp(Z) = gndlvl;
|
|
result.add_pillar(sidehead.id, jp, pradius).
|
|
add_base(cfg.base_height_mm, cfg.base_radius_mm);
|
|
|
|
// connects to ground, eligible for bridging
|
|
cl_centroids.emplace_back(c);
|
|
} else {
|
|
// Creating the bridge to the nearest pillar
|
|
|
|
const Head& nearhead = result.heads()[size_t(nearest_id)];
|
|
Vec3d jp = jsh;
|
|
Vec3d jh = nearhead.junction_point();
|
|
|
|
double d = distance(Vec2d{jp(X), jp(Y)},
|
|
Vec2d{jh(X), jh(Y)});
|
|
Vec3d jn(jh(X), jh(Y), jp(Z) + d*std::tan(-cfg.tilt));
|
|
|
|
if(jn(Z) > jh(Z)) {
|
|
double hdiff = jn(Z) - jh(Z);
|
|
jp(Z) -= hdiff;
|
|
jn(Z) -= hdiff;
|
|
|
|
// pillar without base, this does not connect to ground.
|
|
result.add_pillar(sidehead.id, jp, pradius);
|
|
}
|
|
|
|
if(jp(Z) < jsh(Z)) result.add_junction(jp, hbr);
|
|
if(jn(Z) >= jh(Z)) result.add_junction(jn, hbr);
|
|
double r_pillar = sidehead.request_pillar_radius(pradius);
|
|
result.add_bridge(jp, jn, r_pillar);
|
|
}
|
|
}
|
|
}
|
|
|
|
// We will break down the pillar positions in 2D into concentric rings.
|
|
// Connecting the pillars belonging to the same ring will prevent
|
|
// bridges from crossing each other. After bridging the rings we can
|
|
// create bridges between the rings without the possibility of crossing
|
|
// bridges. Two pillars will be bridged with X shaped stick pairs.
|
|
// If they are really close to each other, than only one stick will be
|
|
// used in zig-zag mode.
|
|
|
|
// Breaking down the points into rings will be done with a modified
|
|
// convex hull algorithm (see pts_convex_hull()), that works for
|
|
// collinear points as well. If the points are on the same surface,
|
|
// they can be part of an imaginary line segment for which the convex
|
|
// hull is not defined. I this case it is enough to sort the points
|
|
// spatially and create the bridge stick from the one endpoint to
|
|
// another.
|
|
|
|
ClusterEl rem = cl_centroids;
|
|
ClusterEl ring;
|
|
|
|
while(!rem.empty()) { // loop until all the points belong to some ring
|
|
tifcl();
|
|
std::sort(rem.begin(), rem.end());
|
|
|
|
auto newring = pts_convex_hull(rem,
|
|
[gnd_head_pt](unsigned i) {
|
|
auto&& p = gnd_head_pt(i);
|
|
return Vec2d(p(X), p(Y)); // project to 2D in along Z axis
|
|
});
|
|
|
|
if(!ring.empty()) {
|
|
// inner ring is now in 'newring' and outer ring is in 'ring'
|
|
SpatIndex innerring;
|
|
for(unsigned i : newring) { tifcl();
|
|
const Pillar& pill = result.head_pillar(gndidx[i]);
|
|
assert(pill.id >= 0);
|
|
innerring.insert(pill.endpoint, unsigned(pill.id));
|
|
}
|
|
|
|
// For all pillars in the outer ring find the closest in the
|
|
// inner ring and connect them. This will create the spider web
|
|
// fashioned connections between pillars
|
|
for(unsigned i : ring) { tifcl();
|
|
const Pillar& outerpill = result.head_pillar(gndidx[i]);
|
|
auto res = innerring.nearest(outerpill.endpoint, 1);
|
|
if(res.empty()) continue;
|
|
|
|
auto ne = res.front();
|
|
const Pillar& innerpill = result.pillars()[ne.second];
|
|
interconnect(outerpill, innerpill, emesh, result);
|
|
}
|
|
}
|
|
|
|
// no need for newring anymore in the current iteration
|
|
ring.swap(newring);
|
|
|
|
/*std::cout << "ring: \n";
|
|
for(auto ri : ring) {
|
|
std::cout << ri << " " << " X = " << gnd_head_pt(ri)(X)
|
|
<< " Y = " << gnd_head_pt(ri)(Y) << std::endl;
|
|
}
|
|
std::cout << std::endl;*/
|
|
|
|
// now the ring has to be connected with bridge sticks
|
|
for(auto it = ring.begin(), next = std::next(it);
|
|
next != ring.end();
|
|
++it, ++next)
|
|
{
|
|
tifcl();
|
|
const Pillar& pillar = result.head_pillar(gndidx[*it]);
|
|
const Pillar& nextpillar = result.head_pillar(gndidx[*next]);
|
|
interconnect(pillar, nextpillar, emesh, result);
|
|
}
|
|
|
|
auto sring = ring; ClusterEl tmp;
|
|
std::sort(sring.begin(), sring.end());
|
|
std::set_difference(rem.begin(), rem.end(),
|
|
sring.begin(), sring.end(),
|
|
std::back_inserter(tmp));
|
|
rem.swap(tmp);
|
|
}
|
|
};
|
|
|
|
// Step: routing the pinheads that are would connect to the model surface
|
|
// along the Z axis downwards. For now these will actually be connected with
|
|
// the model surface with a flipped pinhead. In the future here we could use
|
|
// some smart algorithms to search for a safe path to the ground or to a
|
|
// nearby pillar that can hold the supported weight.
|
|
auto routing_nongnd_fn = [tifcl](
|
|
const SupportConfig& cfg,
|
|
const std::vector<double>& gndheight,
|
|
const IndexSet& nogndidx,
|
|
Result& result)
|
|
{
|
|
// TODO: connect these to the ground pillars if possible
|
|
for(auto idx : nogndidx) { tifcl();
|
|
double gh = gndheight[idx];
|
|
double base_width = cfg.head_width_mm;
|
|
|
|
auto& head = result.head(idx);
|
|
|
|
// In this case there is no room for the base pinhead.
|
|
if(gh < head.fullwidth()) {
|
|
double min_l =
|
|
2 * cfg.head_front_radius_mm +
|
|
2 * cfg.head_back_radius_mm - cfg.head_penetration_mm;
|
|
|
|
base_width = gh - min_l;
|
|
}
|
|
|
|
if(base_width < 0) {
|
|
// There is really no space for even a reduced size head. We
|
|
// have to replace that with a small half sphere that touches
|
|
// the model surface. (TODO)
|
|
head.invalidate();
|
|
continue;
|
|
}
|
|
|
|
head.transform();
|
|
|
|
Vec3d headend = head.junction_point();
|
|
|
|
Head base_head(cfg.head_back_radius_mm,
|
|
cfg.head_front_radius_mm,
|
|
base_width,
|
|
cfg.head_penetration_mm,
|
|
{0.0, 0.0, 1.0},
|
|
{headend(X), headend(Y), headend(Z) - gh});
|
|
|
|
base_head.transform();
|
|
|
|
// Robustness check:
|
|
if(headend(Z) < base_head.junction_point()(Z)) {
|
|
// This should not happen it is against all assumptions
|
|
BOOST_LOG_TRIVIAL(warning)
|
|
<< "Ignoring invalid supports connecting to model body";
|
|
head.invalidate();
|
|
continue;
|
|
}
|
|
|
|
double hl = base_head.fullwidth() - head.r_back_mm;
|
|
|
|
result.add_pillar(idx,
|
|
Vec3d{headend(X), headend(Y), headend(Z) - gh + hl},
|
|
cfg.head_back_radius_mm
|
|
).base = base_head.mesh;
|
|
}
|
|
};
|
|
|
|
// Step: process the support points where there is not enough space for a
|
|
// full pinhead. In this case we will use a rounded sphere as a touching
|
|
// point and use a thinner bridge (let's call it a stick).
|
|
auto process_headless = [tifcl](
|
|
const SupportConfig& cfg,
|
|
const PointSet& headless_pts,
|
|
const PointSet& headless_norm,
|
|
const EigenMesh3D& emesh,
|
|
Result& result)
|
|
{
|
|
// For now we will just generate smaller headless sticks with a sharp
|
|
// ending point that connects to the mesh surface.
|
|
|
|
const double R = cfg.headless_pillar_radius_mm;
|
|
const double HWIDTH_MM = R/3;
|
|
|
|
// We will sink the pins into the model surface for a distance of 1/3 of
|
|
// the pin radius
|
|
for(int i = 0; i < headless_pts.rows(); i++) { tifcl();
|
|
Vec3d sph = headless_pts.row(i); // Exact support position
|
|
Vec3d n = headless_norm.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, emesh, true);
|
|
double dist = ray_mesh_intersect(sj, dir, emesh);
|
|
|
|
if(std::isinf(idist) || std::isnan(idist) || idist < 2*R ||
|
|
std::isinf(dist) || 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;
|
|
result.add_compact_bridge(sp, ej, n, R);
|
|
}
|
|
};
|
|
|
|
// Now that the individual blocks are defined, lets connect the wires. We
|
|
// will create an array of functions which represents a program. Place the
|
|
// step methods in the array and bind the right arguments to the methods
|
|
// This way the data dependencies will be easily traceable between
|
|
// individual steps.
|
|
// There will be empty steps as well like the begin step or the done or
|
|
// abort steps. These are slots for future initialization or cleanup.
|
|
|
|
using std::cref; // Bind inputs with cref (read-only)
|
|
using std::ref; // Bind outputs with ref (writable)
|
|
using std::bind;
|
|
|
|
// Here we can easily track what goes in and what comes out of each step:
|
|
// (see the cref-s as inputs and ref-s as outputs)
|
|
std::array<std::function<void()>, NUM_STEPS> program = {
|
|
[] () {
|
|
// Begin...
|
|
// Potentially clear up the shared data (not needed for now)
|
|
},
|
|
|
|
// Filtering unnecessary support points
|
|
bind(filterfn, cref(cfg), cref(points), cref(mesh),
|
|
ref(filtered_points), ref(head_normals),
|
|
ref(head_positions), ref(headless_positions), ref(headless_normals)),
|
|
|
|
// Pinhead generation
|
|
bind(pinheadfn, cref(cfg),
|
|
ref(head_positions), ref(head_normals), ref(result)),
|
|
|
|
// Classification of support points
|
|
bind(classifyfn, cref(cfg), cref(mesh),
|
|
ref(head_positions), ref(ground_heads), ref(noground_heads),
|
|
ref(head_heights), ref(ground_connectors), ref(result)),
|
|
|
|
// Routing ground connecting clusters
|
|
bind(routing_ground_fn,
|
|
cref(cfg), cref(ground_connectors), cref(ground_heads), cref(mesh),
|
|
ref(result)),
|
|
|
|
// routing non ground connecting support points
|
|
bind(routing_nongnd_fn, cref(cfg), cref(head_heights), cref(noground_heads),
|
|
ref(result)),
|
|
|
|
bind(process_headless,
|
|
cref(cfg), cref(headless_positions),
|
|
cref(headless_normals), cref(mesh),
|
|
ref(result)),
|
|
[] () {
|
|
// Done
|
|
},
|
|
[] () {
|
|
// Halt
|
|
},
|
|
[] () {
|
|
// Abort
|
|
}
|
|
};
|
|
|
|
Steps pc = BEGIN, pc_prev = BEGIN;
|
|
|
|
// Let's define a simple automaton that will run our program.
|
|
auto progress = [&ctl, &pc, &pc_prev] () {
|
|
static const std::array<std::string, NUM_STEPS> stepstr {
|
|
"Starting",
|
|
"Filtering",
|
|
"Generate pinheads",
|
|
"Classification",
|
|
"Routing to ground",
|
|
"Routing supports to model surface",
|
|
"Processing small holes",
|
|
"Done",
|
|
"Halt",
|
|
"Abort"
|
|
};
|
|
|
|
static const std::array<unsigned, NUM_STEPS> stepstate {
|
|
0,
|
|
10,
|
|
30,
|
|
50,
|
|
60,
|
|
70,
|
|
80,
|
|
100,
|
|
0,
|
|
0
|
|
};
|
|
|
|
if(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 = HEADLESS; break;
|
|
case HEADLESS: pc = DONE; break;
|
|
case HALT: pc = pc_prev; break;
|
|
case DONE:
|
|
case ABORT: break;
|
|
default: ;
|
|
}
|
|
ctl.statuscb(stepstate[pc], stepstr[pc]);
|
|
};
|
|
|
|
// Just here we run the computation...
|
|
while(pc < DONE || pc == HALT) {
|
|
progress();
|
|
program[pc]();
|
|
}
|
|
|
|
if(pc == ABORT) throw SLASupportsStoppedException();
|
|
|
|
return pc == ABORT;
|
|
}
|
|
|
|
SLASupportTree::SLASupportTree(): m_impl(new Impl()) {}
|
|
|
|
const TriangleMesh &SLASupportTree::merged_mesh() const
|
|
{
|
|
return get().merged_mesh();
|
|
}
|
|
|
|
void SLASupportTree::merged_mesh_with_pad(TriangleMesh &outmesh) const {
|
|
outmesh.merge(merged_mesh());
|
|
outmesh.merge(get_pad());
|
|
}
|
|
|
|
SlicedSupports SLASupportTree::slice(float layerh, float init_layerh) const
|
|
{
|
|
if(init_layerh < 0) init_layerh = layerh;
|
|
auto& stree = get();
|
|
|
|
const auto modelh = float(stree.full_height());
|
|
auto gndlvl = float(this->m_impl->ground_level);
|
|
const Pad& pad = m_impl->pad();
|
|
if(!pad.empty()) gndlvl -= float(get_pad_elevation(pad.cfg));
|
|
|
|
std::vector<float> heights;
|
|
heights.reserve(size_t(modelh/layerh) + 1);
|
|
|
|
for(float h = gndlvl + init_layerh; h < gndlvl + modelh; h += layerh) {
|
|
heights.emplace_back(h);
|
|
}
|
|
|
|
TriangleMesh fullmesh = m_impl->merged_mesh();
|
|
fullmesh.merge(get_pad());
|
|
TriangleMeshSlicer slicer(&fullmesh);
|
|
SlicedSupports ret;
|
|
slicer.slice(heights, &ret, get().ctl().cancelfn);
|
|
|
|
return ret;
|
|
}
|
|
|
|
const TriangleMesh &SLASupportTree::add_pad(const SliceLayer& baseplate,
|
|
const PoolConfig& pcfg) const
|
|
{
|
|
// PoolConfig pcfg;
|
|
// pcfg.min_wall_thickness_mm = min_wall_thickness_mm;
|
|
// pcfg.min_wall_height_mm = min_wall_height_mm;
|
|
// pcfg.max_merge_distance_mm = max_merge_distance_mm;
|
|
// pcfg.edge_radius_mm = edge_radius_mm;
|
|
return m_impl->create_pad(merged_mesh(), baseplate, pcfg).tmesh;
|
|
}
|
|
|
|
const TriangleMesh &SLASupportTree::get_pad() const
|
|
{
|
|
return m_impl->pad().tmesh;
|
|
}
|
|
|
|
void SLASupportTree::remove_pad()
|
|
{
|
|
m_impl->remove_pad();
|
|
}
|
|
|
|
SLASupportTree::SLASupportTree(const PointSet &points,
|
|
const EigenMesh3D& emesh,
|
|
const SupportConfig &cfg,
|
|
const Controller &ctl):
|
|
m_impl(new Impl(ctl))
|
|
{
|
|
m_impl->ground_level = emesh.ground_level() - cfg.object_elevation_mm;
|
|
generate(points, emesh, cfg, ctl);
|
|
}
|
|
|
|
SLASupportTree::SLASupportTree(const SLASupportTree &c):
|
|
m_impl(new Impl(*c.m_impl)) {}
|
|
|
|
SLASupportTree &SLASupportTree::operator=(const SLASupportTree &c)
|
|
{
|
|
m_impl = make_unique<Impl>(*c.m_impl);
|
|
return *this;
|
|
}
|
|
|
|
SLASupportTree::~SLASupportTree() {}
|
|
|
|
SLASupportsStoppedException::SLASupportsStoppedException():
|
|
std::runtime_error("") {}
|
|
|
|
}
|
|
}
|