Create smaller supports in problematic areas with established strategies
Completely remove the concept of CompactBridge. Replace it with Heads having the same back radius as front radius. Try to apply the same rules for mini supports as in the route_to_model step. Increased accuracy of bridge_mesh_intersect shot from support points Refining mini support integration
This commit is contained in:
parent
38239f09e3
commit
0622322146
8 changed files with 571 additions and 357 deletions
|
@ -8,6 +8,7 @@
|
|||
#include <libslic3r/SLA/Common.hpp>
|
||||
#include <libslic3r/SLA/SpatIndex.hpp>
|
||||
#include <libslic3r/SLA/SupportTreeBuilder.hpp>
|
||||
#include <libslic3r/SLA/SupportTreeBuildsteps.hpp>
|
||||
|
||||
#include <libslic3r/MTUtils.hpp>
|
||||
#include <libslic3r/ClipperUtils.hpp>
|
||||
|
@ -103,9 +104,11 @@ SupportTree::UPtr SupportTree::create(const SupportableMesh &sm,
|
|||
builder->m_ctl = ctl;
|
||||
|
||||
if (sm.cfg.enabled) {
|
||||
builder->build(sm);
|
||||
// Execute takes care about the ground_level
|
||||
SupportTreeBuildsteps::execute(*builder, sm);
|
||||
builder->merge_and_cleanup(); // clean metadata, leave only the meshes.
|
||||
} else {
|
||||
// If a pad gets added later, it will be in the right Z level
|
||||
builder->ground_level = sm.emesh.ground_level();
|
||||
}
|
||||
|
||||
|
|
|
@ -155,6 +155,65 @@ Contour3D cylinder(double r, double h, size_t ssteps, const Vec3d &sp)
|
|||
return ret;
|
||||
}
|
||||
|
||||
Contour3D pinhead(double r_pin, double r_back, double length, size_t steps)
|
||||
{
|
||||
assert(length > 0.);
|
||||
assert(r_back > 0.);
|
||||
assert(r_pin > 0.);
|
||||
|
||||
Contour3D mesh;
|
||||
|
||||
// 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_back + r_pin + length;
|
||||
double phi = PI / 2. - std::acos((r_back - r_pin) / 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_back, make_portion(0, PI/2 + phi), detail);
|
||||
auto&& s2 = sphere(r_pin, make_portion(PI/2 + phi, PI), detail);
|
||||
|
||||
for(auto& p : s2.points) p.z() += 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.faces3.emplace_back(i1s1, i2s1, i2s2);
|
||||
mesh.faces3.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.faces3.emplace_back(i2s2, i2s1, i1s1);
|
||||
mesh.faces3.emplace_back(i1s2, i2s2, i1s1);
|
||||
|
||||
return mesh;
|
||||
}
|
||||
|
||||
Head::Head(double r_big_mm,
|
||||
double r_small_mm,
|
||||
double length_mm,
|
||||
|
@ -164,67 +223,17 @@ Head::Head(double r_big_mm,
|
|||
const size_t circlesteps)
|
||||
: steps(circlesteps)
|
||||
, dir(direction)
|
||||
, tr(offset)
|
||||
, pos(offset)
|
||||
, r_back_mm(r_big_mm)
|
||||
, r_pin_mm(r_small_mm)
|
||||
, width_mm(length_mm)
|
||||
, penetration_mm(penetration)
|
||||
{
|
||||
assert(width_mm > 0.);
|
||||
assert(r_back_mm > 0.);
|
||||
assert(r_pin_mm > 0.);
|
||||
|
||||
// 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) p.z() += 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.faces3.emplace_back(i1s1, i2s1, i2s2);
|
||||
mesh.faces3.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.faces3.emplace_back(i2s2, i2s1, i1s1);
|
||||
mesh.faces3.emplace_back(i1s2, i2s2, i1s1);
|
||||
mesh = pinhead(r_pin_mm, r_back_mm, width_mm, steps);
|
||||
|
||||
// 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) p.z() -= (h + r_small_mm - penetration_mm);
|
||||
for(auto& p : mesh.points) p.z() -= (fullwidth() - r_back_mm);
|
||||
}
|
||||
|
||||
Pillar::Pillar(const Vec3d &jp, const Vec3d &endp, double radius, size_t st):
|
||||
|
@ -305,34 +314,6 @@ Bridge::Bridge(const Vec3d &j1, const Vec3d &j2, double r_mm, size_t steps):
|
|||
for(auto& p : mesh.points) p = quater * p + j1;
|
||||
}
|
||||
|
||||
CompactBridge::CompactBridge(const Vec3d &sp,
|
||||
const Vec3d &ep,
|
||||
const Vec3d &n,
|
||||
double r,
|
||||
bool endball,
|
||||
size_t steps)
|
||||
{
|
||||
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;
|
||||
|
||||
if(endball) {
|
||||
auto lowerball = sphere(r, Portion{0, PI/2 + 2*fa}, fa);
|
||||
for(auto& p : lowerball.points) p += endp;
|
||||
mesh.merge(lowerball);
|
||||
}
|
||||
|
||||
mesh.merge(upperball);
|
||||
}
|
||||
|
||||
Pad::Pad(const TriangleMesh &support_mesh,
|
||||
const ExPolygons & model_contours,
|
||||
double ground_level,
|
||||
|
@ -368,7 +349,6 @@ SupportTreeBuilder::SupportTreeBuilder(SupportTreeBuilder &&o)
|
|||
, m_pillars{std::move(o.m_pillars)}
|
||||
, m_bridges{std::move(o.m_bridges)}
|
||||
, m_crossbridges{std::move(o.m_crossbridges)}
|
||||
, m_compact_bridges{std::move(o.m_compact_bridges)}
|
||||
, m_pad{std::move(o.m_pad)}
|
||||
, m_meshcache{std::move(o.m_meshcache)}
|
||||
, m_meshcache_valid{o.m_meshcache_valid}
|
||||
|
@ -382,7 +362,6 @@ SupportTreeBuilder::SupportTreeBuilder(const SupportTreeBuilder &o)
|
|||
, m_pillars{o.m_pillars}
|
||||
, m_bridges{o.m_bridges}
|
||||
, m_crossbridges{o.m_crossbridges}
|
||||
, m_compact_bridges{o.m_compact_bridges}
|
||||
, m_pad{o.m_pad}
|
||||
, m_meshcache{o.m_meshcache}
|
||||
, m_meshcache_valid{o.m_meshcache_valid}
|
||||
|
@ -397,7 +376,6 @@ SupportTreeBuilder &SupportTreeBuilder::operator=(SupportTreeBuilder &&o)
|
|||
m_pillars = std::move(o.m_pillars);
|
||||
m_bridges = std::move(o.m_bridges);
|
||||
m_crossbridges = std::move(o.m_crossbridges);
|
||||
m_compact_bridges = std::move(o.m_compact_bridges);
|
||||
m_pad = std::move(o.m_pad);
|
||||
m_meshcache = std::move(o.m_meshcache);
|
||||
m_meshcache_valid = o.m_meshcache_valid;
|
||||
|
@ -413,7 +391,6 @@ SupportTreeBuilder &SupportTreeBuilder::operator=(const SupportTreeBuilder &o)
|
|||
m_pillars = o.m_pillars;
|
||||
m_bridges = o.m_bridges;
|
||||
m_crossbridges = o.m_crossbridges;
|
||||
m_compact_bridges = o.m_compact_bridges;
|
||||
m_pad = o.m_pad;
|
||||
m_meshcache = o.m_meshcache;
|
||||
m_meshcache_valid = o.m_meshcache_valid;
|
||||
|
@ -443,12 +420,7 @@ const TriangleMesh &SupportTreeBuilder::merged_mesh() const
|
|||
if (ctl().stopcondition()) break;
|
||||
merged.merge(j.mesh);
|
||||
}
|
||||
|
||||
for (auto &cb : m_compact_bridges) {
|
||||
if (ctl().stopcondition()) break;
|
||||
merged.merge(cb.mesh);
|
||||
}
|
||||
|
||||
|
||||
for (auto &bs : m_bridges) {
|
||||
if (ctl().stopcondition()) break;
|
||||
merged.merge(bs.mesh);
|
||||
|
@ -499,7 +471,6 @@ const TriangleMesh &SupportTreeBuilder::merge_and_cleanup()
|
|||
m_pillars = {};
|
||||
m_junctions = {};
|
||||
m_bridges = {};
|
||||
m_compact_bridges = {};
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
@ -514,11 +485,130 @@ const TriangleMesh &SupportTreeBuilder::retrieve_mesh(MeshType meshtype) const
|
|||
return m_meshcache;
|
||||
}
|
||||
|
||||
bool SupportTreeBuilder::build(const SupportableMesh &sm)
|
||||
template<class C, class Hit = EigenMesh3D::hit_result>
|
||||
static Hit min_hit(const C &hits)
|
||||
{
|
||||
ground_level = sm.emesh.ground_level() - sm.cfg.object_elevation_mm;
|
||||
return SupportTreeBuildsteps::execute(*this, sm);
|
||||
auto mit = std::min_element(hits.begin(), hits.end(),
|
||||
[](const Hit &h1, const Hit &h2) {
|
||||
return h1.distance() < h2.distance();
|
||||
});
|
||||
|
||||
return *mit;
|
||||
}
|
||||
|
||||
EigenMesh3D::hit_result query_hit(const SupportableMesh &msh, const Head &h)
|
||||
{
|
||||
static const size_t SAMPLES = 8;
|
||||
|
||||
// Move away slightly from the touching point to avoid raycasting on the
|
||||
// inner surface of the mesh.
|
||||
|
||||
const double& sd = msh.cfg.safety_distance_mm;
|
||||
|
||||
auto& m = msh.emesh;
|
||||
using HitResult = EigenMesh3D::hit_result;
|
||||
|
||||
// Hit results
|
||||
std::array<HitResult, SAMPLES> hits;
|
||||
|
||||
Vec3d s1 = h.pos, s2 = h.junction_point();
|
||||
|
||||
struct Rings {
|
||||
double rpin;
|
||||
double rback;
|
||||
Vec3d spin;
|
||||
Vec3d sback;
|
||||
PointRing<SAMPLES> ring;
|
||||
|
||||
Vec3d backring(size_t idx) { return ring.get(idx, sback, rback); }
|
||||
Vec3d pinring(size_t idx) { return ring.get(idx, spin, rpin); }
|
||||
} rings {h.r_pin_mm + sd, h.r_back_mm + sd, s1, s2, h.dir};
|
||||
|
||||
// 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.
|
||||
|
||||
auto hitfn = [&m, &rings, sd](HitResult &hit, size_t i) {
|
||||
// Point on the circle on the pin sphere
|
||||
Vec3d ps = rings.pinring(i);
|
||||
// This is the point on the circle on the back sphere
|
||||
Vec3d p = rings.backring(i);
|
||||
|
||||
// Point ps is not on mesh but can be inside or
|
||||
// outside as well. This would cause many problems
|
||||
// with ray-casting. To detect the position we will
|
||||
// use the ray-casting result (which has an is_inside
|
||||
// predicate).
|
||||
|
||||
Vec3d n = (p - ps).normalized();
|
||||
auto q = m.query_ray_hit(ps + sd * n, n);
|
||||
|
||||
if (q.is_inside()) { // the hit is inside the model
|
||||
if (q.distance() > rings.rpin) {
|
||||
// If we are inside the model and the hit
|
||||
// distance is bigger than our pin circle
|
||||
// diameter, it probably indicates that the
|
||||
// support point was already inside the
|
||||
// model, or there is really no space
|
||||
// around the point. We will assign a zero
|
||||
// hit distance to these cases which will
|
||||
// enforce the function return value to be
|
||||
// an invalid ray with zero hit distance.
|
||||
// (see min_element at the end)
|
||||
hit = HitResult(0.0);
|
||||
} else {
|
||||
// re-cast the ray from the outside of the
|
||||
// object. The starting point has an offset
|
||||
// of 2*safety_distance because the
|
||||
// original ray has also had an offset
|
||||
auto q2 = m.query_ray_hit(ps + (q.distance() + 2 * sd) * n, n);
|
||||
hit = q2;
|
||||
}
|
||||
} else
|
||||
hit = q;
|
||||
};
|
||||
|
||||
ccr::enumerate(hits.begin(), hits.end(), hitfn);
|
||||
|
||||
return min_hit(hits);
|
||||
}
|
||||
|
||||
EigenMesh3D::hit_result query_hit(const SupportableMesh &msh, const Bridge &br, double safety_d)
|
||||
{
|
||||
static const size_t SAMPLES = 8;
|
||||
|
||||
Vec3d dir = (br.endp - br.startp).normalized();
|
||||
PointRing<SAMPLES> ring{dir};
|
||||
|
||||
using Hit = EigenMesh3D::hit_result;
|
||||
|
||||
// Hit results
|
||||
std::array<Hit, SAMPLES> hits;
|
||||
|
||||
const double sd = std::isnan(safety_d) ? msh.cfg.safety_distance_mm : safety_d;
|
||||
bool ins_check = sd < msh.cfg.safety_distance_mm;
|
||||
|
||||
auto hitfn = [&br, &ring, &msh, dir, sd, ins_check](Hit & hit, size_t i) {
|
||||
// Point on the circle on the pin sphere
|
||||
Vec3d p = ring.get(i, br.startp, br.r + sd);
|
||||
|
||||
auto hr = msh.emesh.query_ray_hit(p + sd * dir, dir);
|
||||
|
||||
if (ins_check && hr.is_inside()) {
|
||||
if (hr.distance() > 2 * br.r + sd)
|
||||
hit = Hit(0.0);
|
||||
else {
|
||||
// re-cast the ray from the outside of the object
|
||||
hit = msh.emesh.query_ray_hit(p + (hr.distance() + 2 * sd) * dir,
|
||||
dir);
|
||||
}
|
||||
} else
|
||||
hit = hr;
|
||||
};
|
||||
|
||||
ccr::enumerate(hits.begin(), hits.end(), hitfn);
|
||||
|
||||
return min_hit(hits);
|
||||
}
|
||||
|
||||
}} // namespace Slic3r::sla
|
||||
|
|
|
@ -76,6 +76,8 @@ Contour3D sphere(double rho, Portion portion = make_portion(0.0, 2.0*PI),
|
|||
// sp: starting point
|
||||
Contour3D cylinder(double r, double h, size_t ssteps = 45, const Vec3d &sp = {0,0,0});
|
||||
|
||||
Contour3D pinhead(double r_pin, double r_back, double length, size_t steps = 45);
|
||||
|
||||
const constexpr long ID_UNSET = -1;
|
||||
|
||||
struct Head {
|
||||
|
@ -83,7 +85,7 @@ struct Head {
|
|||
|
||||
size_t steps = 45;
|
||||
Vec3d dir = {0, 0, -1};
|
||||
Vec3d tr = {0, 0, 0};
|
||||
Vec3d pos = {0, 0, 0};
|
||||
|
||||
double r_back_mm = 1;
|
||||
double r_pin_mm = 0.5;
|
||||
|
@ -120,17 +122,22 @@ struct Head {
|
|||
// the -1 z coordinate
|
||||
auto quatern = Quaternion::FromTwoVectors(Vec3d{0, 0, -1}, dir);
|
||||
|
||||
for(auto& p : mesh.points) p = quatern * p + tr;
|
||||
for(auto& p : mesh.points) p = quatern * p + pos;
|
||||
}
|
||||
|
||||
inline double real_width() const
|
||||
{
|
||||
return 2 * r_pin_mm + width_mm + 2 * r_back_mm ;
|
||||
}
|
||||
|
||||
inline double fullwidth() const
|
||||
{
|
||||
return 2 * r_pin_mm + width_mm + 2*r_back_mm - penetration_mm;
|
||||
return real_width() - penetration_mm;
|
||||
}
|
||||
|
||||
inline Vec3d junction_point() const
|
||||
{
|
||||
return tr + ( 2 * r_pin_mm + width_mm + r_back_mm - penetration_mm)*dir;
|
||||
return pos + (fullwidth() - r_back_mm) * dir;
|
||||
}
|
||||
|
||||
inline double request_pillar_radius(double radius) const
|
||||
|
@ -211,20 +218,6 @@ struct Bridge {
|
|||
size_t steps = 45);
|
||||
};
|
||||
|
||||
// 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 = ID_UNSET;
|
||||
|
||||
CompactBridge(const Vec3d& sp,
|
||||
const Vec3d& ep,
|
||||
const Vec3d& n,
|
||||
double r,
|
||||
bool endball = true,
|
||||
size_t steps = 45);
|
||||
};
|
||||
|
||||
// A wrapper struct around the pad
|
||||
struct Pad {
|
||||
TriangleMesh tmesh;
|
||||
|
@ -242,6 +235,67 @@ struct Pad {
|
|||
bool empty() const { return tmesh.facets_count() == 0; }
|
||||
};
|
||||
|
||||
// Give points on a 3D ring with given center, radius and orientation
|
||||
// method based on:
|
||||
// https://math.stackexchange.com/questions/73237/parametric-equation-of-a-circle-in-3d-space
|
||||
template<size_t N>
|
||||
class PointRing {
|
||||
std::array<double, N> m_phis;
|
||||
|
||||
// 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.
|
||||
// a and b vectors are perpendicular to the ring direction and to each other.
|
||||
// Together they define the plane where we have to iterate with the
|
||||
// given angles in the 'm_phis' vector
|
||||
Vec3d a = {0, 1, 0}, b;
|
||||
double m_radius = 0.;
|
||||
|
||||
static inline bool constexpr is_one(double val)
|
||||
{
|
||||
return std::abs(std::abs(val) - 1) < 1e-20;
|
||||
}
|
||||
|
||||
public:
|
||||
|
||||
PointRing(const Vec3d &n)
|
||||
{
|
||||
m_phis = linspace_array<N>(0., 2 * PI);
|
||||
|
||||
// We have to address the case when the direction vector v (same as
|
||||
// dir) is coincident with one of the world axes. In this case two of
|
||||
// its components will be completely zero and one is 1.0. Our method
|
||||
// becomes dangerous here due to division with zero. Instead, vector
|
||||
// 'a' can be an element-wise rotated version of 'v'
|
||||
if(is_one(n(X)) || is_one(n(Y)) || is_one(n(Z))) {
|
||||
a = {n(Z), n(X), n(Y)};
|
||||
b = {n(Y), n(Z), n(X)};
|
||||
}
|
||||
else {
|
||||
a(Z) = -(n(Y)*a(Y)) / n(Z); a.normalize();
|
||||
b = a.cross(n);
|
||||
}
|
||||
}
|
||||
|
||||
Vec3d get(size_t idx, const Vec3d src, double r) const
|
||||
{
|
||||
double phi = m_phis[idx];
|
||||
double sinphi = std::sin(phi);
|
||||
double cosphi = std::cos(phi);
|
||||
|
||||
double rpscos = r * cosphi;
|
||||
double rpssin = r * sinphi;
|
||||
|
||||
// Point on the sphere
|
||||
return {src(X) + rpscos * a(X) + rpssin * b(X),
|
||||
src(Y) + rpscos * a(Y) + rpssin * b(Y),
|
||||
src(Z) + rpscos * a(Z) + rpssin * b(Z)};
|
||||
}
|
||||
};
|
||||
|
||||
EigenMesh3D::hit_result query_hit(const SupportableMesh &msh, const Bridge &br, double safety_d = std::nan(""));
|
||||
EigenMesh3D::hit_result query_hit(const SupportableMesh &msh, const Head &br, double safety_d = std::nan(""));
|
||||
|
||||
// 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
|
||||
|
@ -264,7 +318,6 @@ class SupportTreeBuilder: public SupportTree {
|
|||
std::vector<Junction> m_junctions;
|
||||
std::vector<Bridge> m_bridges;
|
||||
std::vector<Bridge> m_crossbridges;
|
||||
std::vector<CompactBridge> m_compact_bridges;
|
||||
Pad m_pad;
|
||||
|
||||
using Mutex = ccr::SpinningMutex;
|
||||
|
@ -415,15 +468,6 @@ public:
|
|||
return _add_bridge(m_crossbridges, std::forward<Args>(args)...);
|
||||
}
|
||||
|
||||
template<class...Args> const CompactBridge& add_compact_bridge(Args&&...args)
|
||||
{
|
||||
std::lock_guard<Mutex> lk(m_mutex);
|
||||
m_compact_bridges.emplace_back(std::forward<Args>(args)...);
|
||||
m_compact_bridges.back().id = long(m_compact_bridges.size() - 1);
|
||||
m_meshcache_valid = false;
|
||||
return m_compact_bridges.back();
|
||||
}
|
||||
|
||||
Head &head(unsigned id)
|
||||
{
|
||||
std::lock_guard<Mutex> lk(m_mutex);
|
||||
|
@ -488,8 +532,6 @@ public:
|
|||
|
||||
virtual const TriangleMesh &retrieve_mesh(
|
||||
MeshType meshtype = MeshType::Support) const override;
|
||||
|
||||
bool build(const SupportableMesh &supportable_mesh);
|
||||
};
|
||||
|
||||
}} // namespace Slic3r::sla
|
||||
|
|
|
@ -42,6 +42,8 @@ bool SupportTreeBuildsteps::execute(SupportTreeBuilder & builder,
|
|||
{
|
||||
if(sm.pts.empty()) return false;
|
||||
|
||||
builder.ground_level = sm.emesh.ground_level() - sm.cfg.object_elevation_mm;
|
||||
|
||||
SupportTreeBuildsteps alg(builder, sm);
|
||||
|
||||
// Let's define the individual steps of the processing. We can experiment
|
||||
|
@ -166,64 +168,6 @@ bool SupportTreeBuildsteps::execute(SupportTreeBuilder & builder,
|
|||
return pc == ABORT;
|
||||
}
|
||||
|
||||
// Give points on a 3D ring with given center, radius and orientation
|
||||
// method based on:
|
||||
// https://math.stackexchange.com/questions/73237/parametric-equation-of-a-circle-in-3d-space
|
||||
template<size_t N>
|
||||
class PointRing {
|
||||
std::array<double, N> m_phis;
|
||||
|
||||
// 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.
|
||||
// a and b vectors are perpendicular to the ring direction and to each other.
|
||||
// Together they define the plane where we have to iterate with the
|
||||
// given angles in the 'm_phis' vector
|
||||
Vec3d a = {0, 1, 0}, b;
|
||||
double m_radius = 0.;
|
||||
|
||||
static inline bool constexpr is_one(double val)
|
||||
{
|
||||
return std::abs(std::abs(val) - 1) < 1e-20;
|
||||
}
|
||||
|
||||
public:
|
||||
|
||||
PointRing(const Vec3d &n)
|
||||
{
|
||||
m_phis = linspace_array<N>(0., 2 * PI);
|
||||
|
||||
// We have to address the case when the direction vector v (same as
|
||||
// dir) is coincident with one of the world axes. In this case two of
|
||||
// its components will be completely zero and one is 1.0. Our method
|
||||
// becomes dangerous here due to division with zero. Instead, vector
|
||||
// 'a' can be an element-wise rotated version of 'v'
|
||||
if(is_one(n(X)) || is_one(n(Y)) || is_one(n(Z))) {
|
||||
a = {n(Z), n(X), n(Y)};
|
||||
b = {n(Y), n(Z), n(X)};
|
||||
}
|
||||
else {
|
||||
a(Z) = -(n(Y)*a(Y)) / n(Z); a.normalize();
|
||||
b = a.cross(n);
|
||||
}
|
||||
}
|
||||
|
||||
Vec3d get(size_t idx, const Vec3d src, double r) const
|
||||
{
|
||||
double phi = m_phis[idx];
|
||||
double sinphi = std::sin(phi);
|
||||
double cosphi = std::cos(phi);
|
||||
|
||||
double rpscos = r * cosphi;
|
||||
double rpssin = r * sinphi;
|
||||
|
||||
// Point on the sphere
|
||||
return {src(X) + rpscos * a(X) + rpssin * b(X),
|
||||
src(Y) + rpscos * a(Y) + rpssin * b(Y),
|
||||
src(Z) + rpscos * a(Z) + rpssin * b(Z)};
|
||||
}
|
||||
};
|
||||
|
||||
template<class C, class Hit = EigenMesh3D::hit_result>
|
||||
static Hit min_hit(const C &hits)
|
||||
{
|
||||
|
@ -312,7 +256,7 @@ EigenMesh3D::hit_result SupportTreeBuildsteps::pinhead_mesh_intersect(
|
|||
}
|
||||
|
||||
EigenMesh3D::hit_result SupportTreeBuildsteps::bridge_mesh_intersect(
|
||||
const Vec3d &src, const Vec3d &dir, double r, bool ins_check)
|
||||
const Vec3d &src, const Vec3d &dir, double r, double safety_d)
|
||||
{
|
||||
static const size_t SAMPLES = 8;
|
||||
PointRing<SAMPLES> ring{dir};
|
||||
|
@ -321,16 +265,19 @@ EigenMesh3D::hit_result SupportTreeBuildsteps::bridge_mesh_intersect(
|
|||
|
||||
// Hit results
|
||||
std::array<Hit, SAMPLES> hits;
|
||||
|
||||
double sd = std::isnan(safety_d) ? m_cfg.safety_distance_mm : safety_d;
|
||||
sd = sd * r / m_cfg.head_back_radius_mm;
|
||||
|
||||
bool ins_check = sd < m_cfg.safety_distance_mm;
|
||||
|
||||
ccr::enumerate(hits.begin(), hits.end(),
|
||||
[this, r, src, ins_check, &ring, dir] (Hit &hit, size_t i) {
|
||||
|
||||
const double sd = m_cfg.safety_distance_mm;
|
||||
|
||||
[this, r, src, ins_check, &ring, dir, sd] (Hit &hit, size_t i) {
|
||||
|
||||
// Point on the circle on the pin sphere
|
||||
Vec3d p = ring.get(i, src, r + sd);
|
||||
|
||||
auto hr = m_mesh.query_ray_hit(p + sd * dir, dir);
|
||||
auto hr = m_mesh.query_ray_hit(p + r * dir, dir);
|
||||
|
||||
if(ins_check && hr.is_inside()) {
|
||||
if(hr.distance() > 2 * r + sd) hit = Hit(0.0);
|
||||
|
@ -460,7 +407,7 @@ bool SupportTreeBuildsteps::connect_to_nearpillar(const Head &head,
|
|||
|
||||
Vec3d bridgestart = headjp;
|
||||
Vec3d bridgeend = nearjp_u;
|
||||
double max_len = m_cfg.max_bridge_length_mm;
|
||||
double max_len = r * m_cfg.max_bridge_length_mm / m_cfg.head_back_radius_mm;
|
||||
double max_slope = m_cfg.bridge_slope;
|
||||
double zdiff = 0.0;
|
||||
|
||||
|
@ -494,7 +441,7 @@ bool SupportTreeBuildsteps::connect_to_nearpillar(const Head &head,
|
|||
|
||||
// There will be a minimum distance from the ground where the
|
||||
// bridge is allowed to connect. This is an empiric value.
|
||||
double minz = m_builder.ground_level + 2 * m_cfg.head_width_mm;
|
||||
double minz = m_builder.ground_level + 4 * head.r_back_mm;
|
||||
if(bridgeend(Z) < minz) return false;
|
||||
|
||||
double t = bridge_mesh_distance(bridgestart, dirv(bridgestart, bridgeend), r);
|
||||
|
@ -509,7 +456,7 @@ bool SupportTreeBuildsteps::connect_to_nearpillar(const Head &head,
|
|||
if(zdiff > 0) {
|
||||
m_builder.add_pillar(head.id, bridgestart, r);
|
||||
m_builder.add_junction(bridgestart, r);
|
||||
m_builder.add_bridge(bridgestart, bridgeend, head.r_back_mm);
|
||||
m_builder.add_bridge(bridgestart, bridgeend, r);
|
||||
} else {
|
||||
m_builder.add_bridge(head.id, bridgeend);
|
||||
}
|
||||
|
@ -520,40 +467,6 @@ bool SupportTreeBuildsteps::connect_to_nearpillar(const Head &head,
|
|||
return true;
|
||||
}
|
||||
|
||||
bool SupportTreeBuildsteps::search_pillar_and_connect(const Head &head)
|
||||
{
|
||||
PointIndex spindex = m_pillar_index.guarded_clone();
|
||||
|
||||
long nearest_id = ID_UNSET;
|
||||
|
||||
Vec3d querypoint = head.junction_point();
|
||||
|
||||
while(nearest_id < 0 && !spindex.empty()) { m_thr();
|
||||
// loop until a suitable head is not found
|
||||
// if there is a pillar closer than the cluster center
|
||||
// (this may happen as the clustering is not perfect)
|
||||
// than we will bridge to this closer pillar
|
||||
|
||||
Vec3d qp(querypoint(X), querypoint(Y), m_builder.ground_level);
|
||||
auto qres = spindex.nearest(qp, 1);
|
||||
if(qres.empty()) break;
|
||||
|
||||
auto ne = qres.front();
|
||||
nearest_id = ne.second;
|
||||
|
||||
if(nearest_id >= 0) {
|
||||
if(size_t(nearest_id) < m_builder.pillarcount()) {
|
||||
if(!connect_to_nearpillar(head, nearest_id)) {
|
||||
nearest_id = ID_UNSET; // continue searching
|
||||
spindex.remove(ne); // without the current pillar
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nearest_id >= 0;
|
||||
}
|
||||
|
||||
void SupportTreeBuildsteps::create_ground_pillar(const Vec3d &jp,
|
||||
const Vec3d &sourcedir,
|
||||
double radius,
|
||||
|
@ -565,9 +478,10 @@ void SupportTreeBuildsteps::create_ground_pillar(const Vec3d &jp,
|
|||
Vec3d endp = {jp(X), jp(Y), gndlvl};
|
||||
double sd = m_cfg.pillar_base_safety_distance_mm;
|
||||
long pillar_id = ID_UNSET;
|
||||
double min_dist = sd + m_cfg.base_radius_mm + EPSILON;
|
||||
bool can_add_base = radius >= m_cfg.head_back_radius_mm;
|
||||
double base_r = can_add_base ? m_cfg.base_radius_mm : 0.;
|
||||
double min_dist = sd + base_r + EPSILON;
|
||||
double dist = 0;
|
||||
bool can_add_base = true;
|
||||
bool normal_mode = true;
|
||||
|
||||
// If in zero elevation mode and the pillar is too close to the model body,
|
||||
|
@ -612,7 +526,7 @@ void SupportTreeBuildsteps::create_ground_pillar(const Vec3d &jp,
|
|||
|
||||
endp = jp + std::get<0>(result.optimum) * dir;
|
||||
Vec3d pgnd = {endp(X), endp(Y), gndlvl};
|
||||
can_add_base = result.score > min_dist;
|
||||
can_add_base = can_add_base && result.score > min_dist;
|
||||
|
||||
double gnd_offs = m_mesh.ground_level_offset();
|
||||
auto abort_in_shame =
|
||||
|
@ -712,84 +626,85 @@ void SupportTreeBuildsteps::filter()
|
|||
auto [polar, azimuth] = dir_to_spheric(n);
|
||||
|
||||
// skip if the tilt is not sane
|
||||
if(polar >= PI - m_cfg.normal_cutoff_angle) {
|
||||
if(polar < PI - m_cfg.normal_cutoff_angle) return;
|
||||
|
||||
// We saturate the polar angle to 3pi/4
|
||||
polar = std::max(polar, 3*PI / 4);
|
||||
|
||||
// save the head (pinpoint) position
|
||||
Vec3d hp = m_points.row(fidx);
|
||||
|
||||
double w = m_cfg.head_width_mm +
|
||||
m_cfg.head_back_radius_mm +
|
||||
2*m_cfg.head_front_radius_mm;
|
||||
|
||||
double pin_r = double(m_support_pts[fidx].head_front_radius);
|
||||
|
||||
// Reassemble the now corrected normal
|
||||
auto nn = spheric_to_dir(polar, azimuth).normalized();
|
||||
|
||||
// check available distance
|
||||
EigenMesh3D::hit_result t
|
||||
= pinhead_mesh_intersect(hp, // touching point
|
||||
nn, // normal
|
||||
pin_r,
|
||||
m_cfg.head_back_radius_mm,
|
||||
w);
|
||||
|
||||
if(t.distance() <= w) {
|
||||
|
||||
// Let's try to optimize this angle, there might be a
|
||||
// viable normal that doesn't collide with the model
|
||||
// geometry and its very close to the default.
|
||||
|
||||
StopCriteria stc;
|
||||
stc.max_iterations = m_cfg.optimizer_max_iterations;
|
||||
stc.relative_score_difference = m_cfg.optimizer_rel_score_diff;
|
||||
stc.stop_score = w; // space greater than w is enough
|
||||
GeneticOptimizer solver(stc);
|
||||
solver.seed(0); // we want deterministic behavior
|
||||
|
||||
auto oresult = solver.optimize_max(
|
||||
[this, pin_r, w, hp](double plr, double azm)
|
||||
{
|
||||
auto dir = spheric_to_dir(plr, azm).normalized();
|
||||
|
||||
double score = pinhead_mesh_distance(
|
||||
hp, dir, pin_r, m_cfg.head_back_radius_mm, w);
|
||||
|
||||
return score;
|
||||
},
|
||||
initvals(polar, azimuth), // start with what we have
|
||||
bound(3 * PI / 4, PI), // Must not exceed the tilt limit
|
||||
bound(-PI, PI) // azimuth can be a full search
|
||||
);
|
||||
|
||||
if(oresult.score > w) {
|
||||
polar = std::get<0>(oresult.optimum);
|
||||
azimuth = std::get<1>(oresult.optimum);
|
||||
nn = spheric_to_dir(polar, azimuth).normalized();
|
||||
t = EigenMesh3D::hit_result(oresult.score);
|
||||
}
|
||||
}
|
||||
|
||||
// save the verified and corrected normal
|
||||
m_support_nmls.row(fidx) = nn;
|
||||
|
||||
if (t.distance() > w) {
|
||||
// Check distance from ground, we might have zero elevation.
|
||||
if (hp(Z) + w * nn(Z) < m_builder.ground_level) {
|
||||
addfn(m_iheadless, fidx);
|
||||
} else {
|
||||
// mark the point for needing a head.
|
||||
addfn(m_iheads, fidx);
|
||||
}
|
||||
} else if (polar >= 3 * PI / 4) {
|
||||
// Headless supports do not tilt like the headed ones
|
||||
// so the normal should point almost to the ground.
|
||||
addfn(m_iheadless, fidx);
|
||||
// We saturate the polar angle to 3pi/4
|
||||
polar = std::max(polar, 3*PI / 4);
|
||||
|
||||
// save the head (pinpoint) position
|
||||
Vec3d hp = m_points.row(fidx);
|
||||
|
||||
// The distance needed for a pinhead to not collide with model.
|
||||
double w = m_cfg.head_width_mm +
|
||||
m_cfg.head_back_radius_mm +
|
||||
2*m_cfg.head_front_radius_mm;
|
||||
|
||||
double pin_r = double(m_support_pts[fidx].head_front_radius);
|
||||
|
||||
// Reassemble the now corrected normal
|
||||
auto nn = spheric_to_dir(polar, azimuth).normalized();
|
||||
|
||||
// check available distance
|
||||
EigenMesh3D::hit_result t
|
||||
= pinhead_mesh_intersect(hp, // touching point
|
||||
nn, // normal
|
||||
pin_r,
|
||||
m_cfg.head_back_radius_mm,
|
||||
w);
|
||||
|
||||
if(t.distance() <= w) {
|
||||
|
||||
// Let's try to optimize this angle, there might be a
|
||||
// viable normal that doesn't collide with the model
|
||||
// geometry and its very close to the default.
|
||||
|
||||
StopCriteria stc;
|
||||
stc.max_iterations = m_cfg.optimizer_max_iterations;
|
||||
stc.relative_score_difference = m_cfg.optimizer_rel_score_diff;
|
||||
stc.stop_score = w; // space greater than w is enough
|
||||
GeneticOptimizer solver(stc);
|
||||
solver.seed(0); // we want deterministic behavior
|
||||
|
||||
auto oresult = solver.optimize_max(
|
||||
[this, pin_r, w, hp](double plr, double azm)
|
||||
{
|
||||
auto dir = spheric_to_dir(plr, azm).normalized();
|
||||
|
||||
double score = pinhead_mesh_intersect(
|
||||
hp, dir, pin_r, m_cfg.head_back_radius_mm, w).distance();
|
||||
|
||||
return score;
|
||||
},
|
||||
initvals(polar, azimuth), // start with what we have
|
||||
bound(3 * PI / 4, PI), // Must not exceed the tilt limit
|
||||
bound(-PI, PI) // azimuth can be a full search
|
||||
);
|
||||
|
||||
if(oresult.score > w) {
|
||||
polar = std::get<0>(oresult.optimum);
|
||||
azimuth = std::get<1>(oresult.optimum);
|
||||
nn = spheric_to_dir(polar, azimuth).normalized();
|
||||
t = EigenMesh3D::hit_result(oresult.score);
|
||||
}
|
||||
}
|
||||
|
||||
// save the verified and corrected normal
|
||||
m_support_nmls.row(fidx) = nn;
|
||||
|
||||
if (t.distance() > w) {
|
||||
// Check distance from ground, we might have zero elevation.
|
||||
if (hp(Z) + w * nn(Z) < m_builder.ground_level) {
|
||||
addfn(m_iheadless, fidx);
|
||||
} else {
|
||||
// mark the point for needing a head.
|
||||
addfn(m_iheads, fidx);
|
||||
}
|
||||
} else if (polar >= 3 * PI / 4) {
|
||||
// Headless supports do not tilt like the headed ones
|
||||
// so the normal should point almost to the ground.
|
||||
addfn(m_iheadless, fidx);
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
ccr::enumerate(filtered_indices.begin(), filtered_indices.end(), filterfn);
|
||||
|
@ -811,6 +726,27 @@ void SupportTreeBuildsteps::add_pinheads()
|
|||
m_support_pts[i].pos.cast<double>() // displacement
|
||||
);
|
||||
}
|
||||
|
||||
for (unsigned i : m_iheadless) {
|
||||
const auto R = double(m_support_pts[i].head_front_radius);
|
||||
|
||||
// The support point position on the mesh
|
||||
Vec3d sph = m_support_pts[i].pos.cast<double>();
|
||||
|
||||
// Get an initial normal from the filtering step
|
||||
Vec3d n = m_support_nmls.row(i);
|
||||
|
||||
// First we need to determine the available space for a mini pinhead.
|
||||
// The goal is the move away from the model a little bit to make the
|
||||
// contact point small as possible and avoid pearcing the model body.
|
||||
double pin_space = std::min(2 * R, bridge_mesh_distance(sph, n, R, 0.));
|
||||
|
||||
if (pin_space <= 0) continue;
|
||||
|
||||
m_iheads.emplace_back(i);
|
||||
m_builder.add_head(i, R, R, pin_space,
|
||||
m_cfg.head_penetration_mm, n, sph);
|
||||
}
|
||||
}
|
||||
|
||||
void SupportTreeBuildsteps::classify()
|
||||
|
@ -864,8 +800,6 @@ void SupportTreeBuildsteps::classify()
|
|||
|
||||
void SupportTreeBuildsteps::routing_to_ground()
|
||||
{
|
||||
const double pradius = m_cfg.head_back_radius_mm;
|
||||
|
||||
ClusterEl cl_centroids;
|
||||
cl_centroids.reserve(m_pillar_clusters.size());
|
||||
|
||||
|
@ -931,7 +865,7 @@ void SupportTreeBuildsteps::routing_to_ground()
|
|||
Vec3d pstart = sidehead.junction_point();
|
||||
// Vec3d pend = Vec3d{pstart(X), pstart(Y), gndlvl};
|
||||
// Could not find a pillar, create one
|
||||
create_ground_pillar(pstart, sidehead.dir, pradius, sidehead.id);
|
||||
create_ground_pillar(pstart, sidehead.dir, sidehead.r_back_mm, sidehead.id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -943,7 +877,7 @@ bool SupportTreeBuildsteps::connect_to_ground(Head &head, const Vec3d &dir)
|
|||
double r = head.r_back_mm;
|
||||
double t = bridge_mesh_distance(hjp, dir, head.r_back_mm);
|
||||
double d = 0, tdown = 0;
|
||||
t = std::min(t, m_cfg.max_bridge_length_mm);
|
||||
t = std::min(t, m_cfg.max_bridge_length_mm * r / m_cfg.head_back_radius_mm);
|
||||
|
||||
while (d < t && !std::isinf(tdown = bridge_mesh_distance(hjp + d * dir, DOWN, r)))
|
||||
d += r;
|
||||
|
@ -1041,6 +975,42 @@ bool SupportTreeBuildsteps::connect_to_model_body(Head &head)
|
|||
return true;
|
||||
}
|
||||
|
||||
bool SupportTreeBuildsteps::search_pillar_and_connect(const Head &source)
|
||||
{
|
||||
// Hope that a local copy takes less time than the whole search loop.
|
||||
// We also need to remove elements progressively from the copied index.
|
||||
PointIndex spindex = m_pillar_index.guarded_clone();
|
||||
|
||||
long nearest_id = ID_UNSET;
|
||||
|
||||
Vec3d querypt = source.junction_point();
|
||||
|
||||
while(nearest_id < 0 && !spindex.empty()) { m_thr();
|
||||
// loop until a suitable head is not found
|
||||
// if there is a pillar closer than the cluster center
|
||||
// (this may happen as the clustering is not perfect)
|
||||
// than we will bridge to this closer pillar
|
||||
|
||||
Vec3d qp(querypt(X), querypt(Y), m_builder.ground_level);
|
||||
auto qres = spindex.nearest(qp, 1);
|
||||
if(qres.empty()) break;
|
||||
|
||||
auto ne = qres.front();
|
||||
nearest_id = ne.second;
|
||||
|
||||
if(nearest_id >= 0) {
|
||||
if(size_t(nearest_id) < m_builder.pillarcount()) {
|
||||
if(!connect_to_nearpillar(source, nearest_id)) {
|
||||
nearest_id = ID_UNSET; // continue searching
|
||||
spindex.remove(ne); // without the current pillar
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nearest_id >= 0;
|
||||
}
|
||||
|
||||
void SupportTreeBuildsteps::routing_to_model()
|
||||
{
|
||||
// We need to check if there is an easy way out to the bed surface.
|
||||
|
@ -1054,18 +1024,18 @@ void SupportTreeBuildsteps::routing_to_model()
|
|||
auto& head = m_builder.head(idx);
|
||||
|
||||
// Search nearby pillar
|
||||
if(search_pillar_and_connect(head)) { head.transform(); return; }
|
||||
if (search_pillar_and_connect(head)) { head.transform(); return; }
|
||||
|
||||
// Cannot connect to nearby pillar. We will try to search for
|
||||
// a route to the ground.
|
||||
if(connect_to_ground(head)) { head.transform(); return; }
|
||||
if (connect_to_ground(head)) { head.transform(); return; }
|
||||
|
||||
// No route to the ground, so connect to the model body as a last resort
|
||||
if (connect_to_model_body(head)) { return; }
|
||||
|
||||
// We have failed to route this head.
|
||||
BOOST_LOG_TRIVIAL(warning)
|
||||
<< "Failed to route model facing support point. ID: " << idx;
|
||||
<< "Failed to route model facing support point. ID: " << idx;
|
||||
|
||||
head.invalidate();
|
||||
});
|
||||
|
@ -1107,9 +1077,10 @@ void SupportTreeBuildsteps::interconnect_pillars()
|
|||
// connections are already enough for the pillar
|
||||
if(pillar.links >= neighbors) return;
|
||||
|
||||
double max_d = d * pillar.r / m_cfg.head_back_radius_mm;
|
||||
// Query all remaining points within reach
|
||||
auto qres = m_pillar_index.query([qp, d](const PointIndexEl& e){
|
||||
return distance(e.first, qp) < d;
|
||||
auto qres = m_pillar_index.query([qp, max_d](const PointIndexEl& e){
|
||||
return distance(e.first, qp) < max_d;
|
||||
});
|
||||
|
||||
// sort the result by distance (have to check if this is needed)
|
||||
|
@ -1288,37 +1259,54 @@ void SupportTreeBuildsteps::routing_headless()
|
|||
|
||||
// We will sink the pins into the model surface for a distance of 1/3 of
|
||||
// the pin radius
|
||||
for(unsigned i : m_iheadless) {
|
||||
m_thr();
|
||||
|
||||
const auto R = double(m_support_pts[i].head_front_radius);
|
||||
const double HWIDTH_MM = std::min(R, m_cfg.head_penetration_mm);
|
||||
|
||||
// Exact support position
|
||||
Vec3d sph = m_support_pts[i].pos.cast<double>();
|
||||
Vec3d n = m_support_nmls.row(i); // mesh outward normal
|
||||
Vec3d sp = sph - n * HWIDTH_MM; // stick head start point
|
||||
|
||||
Vec3d sj = sp + R * n; // stick start point
|
||||
|
||||
// This is only for checking
|
||||
double idist = bridge_mesh_distance(sph, DOWN, R, true);
|
||||
double realdist = ray_mesh_intersect(sj, DOWN).distance();
|
||||
double dist = realdist;
|
||||
|
||||
if (std::isinf(dist)) dist = sph(Z) - m_builder.ground_level;
|
||||
|
||||
if(std::isnan(idist) || idist < 2*R || std::isnan(dist) || dist < 2*R) {
|
||||
BOOST_LOG_TRIVIAL(warning) << "Can not find route for headless"
|
||||
<< " support stick at: "
|
||||
<< sj.transpose();
|
||||
continue;
|
||||
}
|
||||
|
||||
bool use_endball = !std::isinf(realdist);
|
||||
Vec3d ej = sj + (dist + HWIDTH_MM) * DOWN ;
|
||||
m_builder.add_compact_bridge(sp, ej, n, R, use_endball);
|
||||
}
|
||||
// for(unsigned i : m_iheadless) {
|
||||
// m_thr();
|
||||
|
||||
// const auto R = double(m_support_pts[i].head_front_radius);
|
||||
|
||||
// // The support point position on the mesh
|
||||
// Vec3d sph = m_support_pts[i].pos.cast<double>();
|
||||
|
||||
// // Get an initial normal from the filtering step
|
||||
// Vec3d n = m_support_nmls.row(i);
|
||||
|
||||
// // First we need to determine the available space for a mini pinhead.
|
||||
// // The goal is the move away from the model a little bit to make the
|
||||
// // contact point small as possible and avoid pearcing the model body.
|
||||
// double pin_space = std::min(2 * R, bridge_mesh_distance(sph, n, R, 0.));
|
||||
|
||||
// if (pin_space <= 0) continue;
|
||||
|
||||
// auto &head = m_builder.add_head(i, R, R, pin_space,
|
||||
// m_cfg.head_penetration_mm, n, sph);
|
||||
|
||||
// // collision check
|
||||
|
||||
// m_head_to_ground_scans[i] =
|
||||
// bridge_mesh_intersect(head.junction_point(), DOWN, R);
|
||||
|
||||
// // Here the steps will be similar as in route_to_model step:
|
||||
// // 1. Search for a nearby pillar, include other mini pillars
|
||||
|
||||
// // Search nearby pillar
|
||||
// if (search_pillar_and_connect(head)) { head.transform(); continue; }
|
||||
|
||||
// if (std::isinf(m_head_to_ground_scans[i].distance())) {
|
||||
// create_ground_pillar(head.junction_point(), head.dir, m_cfg.head_back_radius_mm, head.id);
|
||||
// }
|
||||
|
||||
// // Cannot connect to nearby pillar. We will try to search for
|
||||
// // a route to the ground.
|
||||
// if (connect_to_ground(head)) { head.transform(); continue; }
|
||||
|
||||
// // No route to the ground, so connect to the model body as a last resort
|
||||
// if (connect_to_model_body(head)) { continue; }
|
||||
|
||||
// BOOST_LOG_TRIVIAL(warning) << "Can not find route for headless"
|
||||
// << " support stick at: "
|
||||
// << sph.transpose();
|
||||
// head.invalidate();
|
||||
// }
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -229,11 +229,6 @@ class SupportTreeBuildsteps {
|
|||
double r_pin,
|
||||
double r_back,
|
||||
double width);
|
||||
|
||||
template<class...Args>
|
||||
inline double pinhead_mesh_distance(Args&&...args) {
|
||||
return pinhead_mesh_intersect(std::forward<Args>(args)...).distance();
|
||||
}
|
||||
|
||||
// Checking bridge (pillar and stick as well) intersection with the model.
|
||||
// If the function is used for headless sticks, the ins_check parameter
|
||||
|
@ -247,7 +242,7 @@ class SupportTreeBuildsteps {
|
|||
const Vec3d& s,
|
||||
const Vec3d& dir,
|
||||
double r,
|
||||
bool ins_check = false);
|
||||
double safety_d = std::nan(""));
|
||||
|
||||
template<class...Args>
|
||||
inline double bridge_mesh_distance(Args&&...args) {
|
||||
|
@ -268,8 +263,8 @@ class SupportTreeBuildsteps {
|
|||
inline bool connect_to_ground(Head& head);
|
||||
|
||||
bool connect_to_model_body(Head &head);
|
||||
|
||||
bool search_pillar_and_connect(const Head& head);
|
||||
|
||||
bool search_pillar_and_connect(const Head& source);
|
||||
|
||||
// This is a proxy function for pillar creation which will mind the gap
|
||||
// between the pad and the model bottom in zero elevation mode.
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
get_filename_component(_TEST_NAME ${CMAKE_CURRENT_LIST_DIR} NAME)
|
||||
add_executable(${_TEST_NAME}_tests ${_TEST_NAME}_tests_main.cpp
|
||||
sla_print_tests.cpp
|
||||
sla_test_utils.hpp sla_test_utils.cpp
|
||||
sla_test_utils.hpp sla_test_utils.cpp sla_treebuilder_tests.cpp
|
||||
sla_raycast_tests.cpp)
|
||||
target_link_libraries(${_TEST_NAME}_tests test_common libslic3r)
|
||||
set_property(TARGET ${_TEST_NAME}_tests PROPERTY FOLDER "tests")
|
||||
|
|
|
@ -129,8 +129,7 @@ void test_supports(const std::string &obj_filename,
|
|||
// If there is no elevation, support points shall be removed from the
|
||||
// bottom of the object.
|
||||
if (std::abs(supportcfg.object_elevation_mm) < EPSILON) {
|
||||
sla::remove_bottom_points(support_points, zmin,
|
||||
supportcfg.base_height_mm);
|
||||
sla::remove_bottom_points(support_points, zmin + supportcfg.base_height_mm);
|
||||
} else {
|
||||
// Should be support points at least on the bottom of the model
|
||||
REQUIRE_FALSE(support_points.empty());
|
||||
|
@ -141,7 +140,8 @@ void test_supports(const std::string &obj_filename,
|
|||
|
||||
// Generate the actual support tree
|
||||
sla::SupportTreeBuilder treebuilder;
|
||||
treebuilder.build(sla::SupportableMesh{emesh, support_points, supportcfg});
|
||||
sla::SupportableMesh sm{emesh, support_points, supportcfg};
|
||||
sla::SupportTreeBuildsteps::execute(treebuilder, sm);
|
||||
|
||||
check_support_tree_integrity(treebuilder, supportcfg);
|
||||
|
||||
|
|
96
tests/sla_print/sla_treebuilder_tests.cpp
Normal file
96
tests/sla_print/sla_treebuilder_tests.cpp
Normal file
|
@ -0,0 +1,96 @@
|
|||
#include <catch2/catch.hpp>
|
||||
#include <test_utils.hpp>
|
||||
|
||||
#include "libslic3r/TriangleMesh.hpp"
|
||||
#include "libslic3r/SLA/SupportTreeBuilder.hpp"
|
||||
|
||||
TEST_CASE("Test bridge_mesh_intersect on a cube's wall", "[SLABridgeMeshInters]") {
|
||||
using namespace Slic3r;
|
||||
|
||||
TriangleMesh cube = make_cube(10., 10., 10.);
|
||||
|
||||
sla::SupportConfig cfg = {}; // use default config
|
||||
sla::SupportPoints pts = {{10.f, 5.f, 5.f, float(cfg.head_front_radius_mm), false}};
|
||||
sla::SupportableMesh sm{cube, pts, cfg};
|
||||
|
||||
SECTION("Bridge is straight horizontal and pointing away from the cube") {
|
||||
|
||||
sla::Bridge bridge(pts[0].pos.cast<double>(), Vec3d{15., 5., 5.},
|
||||
pts[0].head_front_radius);
|
||||
|
||||
auto hit = sla::query_hit(sm, bridge);
|
||||
|
||||
REQUIRE(std::isinf(hit.distance()));
|
||||
|
||||
cube.merge(sla::to_triangle_mesh(bridge.mesh));
|
||||
cube.require_shared_vertices();
|
||||
cube.WriteOBJFile("cube1.obj");
|
||||
}
|
||||
|
||||
SECTION("Bridge is tilted down in 45 degrees, pointing away from the cube") {
|
||||
sla::Bridge bridge(pts[0].pos.cast<double>(), Vec3d{15., 5., 0.},
|
||||
pts[0].head_front_radius);
|
||||
|
||||
auto hit = sla::query_hit(sm, bridge);
|
||||
|
||||
REQUIRE(std::isinf(hit.distance()));
|
||||
|
||||
cube.merge(sla::to_triangle_mesh(bridge.mesh));
|
||||
cube.require_shared_vertices();
|
||||
cube.WriteOBJFile("cube2.obj");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
TEST_CASE("Test bridge_mesh_intersect on a sphere", "[SLABridgeMeshInters]") {
|
||||
using namespace Slic3r;
|
||||
|
||||
TriangleMesh sphere = make_sphere(1.);
|
||||
|
||||
sla::SupportConfig cfg = {}; // use default config
|
||||
cfg.head_back_radius_mm = cfg.head_front_radius_mm;
|
||||
sla::SupportPoints pts = {{1.f, 0.f, 0.f, float(cfg.head_front_radius_mm), false}};
|
||||
sla::SupportableMesh sm{sphere, pts, cfg};
|
||||
|
||||
SECTION("Bridge is straight horizontal and pointing away from the sphere") {
|
||||
|
||||
sla::Bridge bridge(pts[0].pos.cast<double>(), Vec3d{2., 0., 0.},
|
||||
pts[0].head_front_radius);
|
||||
|
||||
auto hit = sla::query_hit(sm, bridge);
|
||||
|
||||
sphere.merge(sla::to_triangle_mesh(bridge.mesh));
|
||||
sphere.require_shared_vertices();
|
||||
sphere.WriteOBJFile("sphere1.obj");
|
||||
|
||||
REQUIRE(std::isinf(hit.distance()));
|
||||
}
|
||||
|
||||
SECTION("Bridge is tilted down 45 deg and pointing away from the sphere") {
|
||||
|
||||
sla::Bridge bridge(pts[0].pos.cast<double>(), Vec3d{2., 0., -2.},
|
||||
pts[0].head_front_radius);
|
||||
|
||||
auto hit = sla::query_hit(sm, bridge);
|
||||
|
||||
sphere.merge(sla::to_triangle_mesh(bridge.mesh));
|
||||
sphere.require_shared_vertices();
|
||||
sphere.WriteOBJFile("sphere2.obj");
|
||||
|
||||
REQUIRE(std::isinf(hit.distance()));
|
||||
}
|
||||
|
||||
SECTION("Bridge is tilted down 90 deg and pointing away from the sphere") {
|
||||
|
||||
sla::Bridge bridge(pts[0].pos.cast<double>(), Vec3d{1., 0., -2.},
|
||||
pts[0].head_front_radius);
|
||||
|
||||
auto hit = sla::query_hit(sm, bridge);
|
||||
|
||||
sphere.merge(sla::to_triangle_mesh(bridge.mesh));
|
||||
sphere.require_shared_vertices();
|
||||
sphere.WriteOBJFile("sphere3.obj");
|
||||
|
||||
REQUIRE(std::isinf(hit.distance()));
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue