mirror of
https://git.mirrors.martin98.com/https://github.com/prusa3d/PrusaSlicer.git
synced 2025-05-07 06:49:05 +08:00

Conditional compilation of an igl winding number tree for SLA support generator, as it is not used as of now and initialization of the tree is expensive. Fixed issue with passing the new SLA point definition to the back end and back to the UI.
2063 lines
73 KiB
C++
2063 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<SupportPoint> &v)
|
|
{
|
|
PointSet ret(v.size(), 3);
|
|
long i = 0;
|
|
for(const SupportPoint& support_point : v) {
|
|
ret.row(i)(0) = support_point.pos(0);
|
|
ret.row(i)(1) = support_point.pos(1);
|
|
ret.row(i)(2) = support_point.pos(2);
|
|
++i;
|
|
}
|
|
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
|
|
}
|
|
};
|
|
|
|
if(cfg.ground_facing_only) { // Delete the non-gnd steps if necessary
|
|
program[ROUTING_NONGROUND] = []() {
|
|
BOOST_LOG_TRIVIAL(info) << "Skipping non-ground facing supports as "
|
|
"requested.";
|
|
};
|
|
program[HEADLESS] = [](){
|
|
BOOST_LOG_TRIVIAL(info) << "Skipping headless stick generation as "
|
|
"requested";
|
|
};
|
|
}
|
|
|
|
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("") {}
|
|
|
|
}
|
|
}
|