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:
tamasmeszaros 2020-06-02 17:31:52 +02:00
parent 38239f09e3
commit 0622322146
8 changed files with 571 additions and 357 deletions

View file

@ -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();
}

View file

@ -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

View file

@ -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

View file

@ -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();
// }
}
}

View file

@ -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.

View file

@ -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")

View file

@ -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);

View 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()));
}
}