diff --git a/src/libslic3r/SLA/SupportPointGenerator.cpp b/src/libslic3r/SLA/SupportPointGenerator.cpp index 66b6ea3f41..55124d4fc6 100644 --- a/src/libslic3r/SLA/SupportPointGenerator.cpp +++ b/src/libslic3r/SLA/SupportPointGenerator.cpp @@ -49,18 +49,64 @@ float SupportPointGenerator::distance_limit(float angle) const return 1./(2.4*get_required_density(angle)); }*/ -SupportPointGenerator::SupportPointGenerator(const sla::EigenMesh3D & emesh, - const std::vector &slices, - const std::vector & heights, - const Config & config, - std::function throw_on_cancel, - std::function statusfn) +class SupportPointGenerator::RandomGen { + std::mt19937 m_; +public: + + using result_type = long; + + RandomGen() + { + std::random_device rd; + m_.seed(rd()); + } + + explicit RandomGen(long seedval) { seed(seedval); } + + void seed(long s) { m_.seed(std::mt19937::result_type(s)); } + long operator() () { return long(m_()); } + long min() const { return m_.min(); } + long max() const { return m_.max(); } +}; + +SupportPointGenerator::SupportPointGenerator( + const sla::EigenMesh3D &emesh, + const std::vector &slices, + const std::vector & heights, + const Config & config, + std::function throw_on_cancel, + std::function statusfn) + : SupportPointGenerator(emesh, config, throw_on_cancel, statusfn) +{ + execute(slices, heights); +} + +SupportPointGenerator::SupportPointGenerator( + const EigenMesh3D &emesh, + const SupportPointGenerator::Config &config, + std::function throw_on_cancel, + std::function statusfn) : m_config(config) , m_emesh(emesh) , m_throw_on_cancel(throw_on_cancel) , m_statusfn(statusfn) { - process(slices, heights); +} + +void SupportPointGenerator::execute(const std::vector &slices, + const std::vector & heights) +{ + RandomGen rng; + process(slices, heights, rng); + project_onto_mesh(m_output); +} + +void SupportPointGenerator::execute(const std::vector &slices, + const std::vector & heights, + long seed) +{ + RandomGen rng(seed); + process(slices, heights, rng); project_onto_mesh(m_output); } @@ -184,7 +230,7 @@ static std::vector make_layers( return layers; } -void SupportPointGenerator::process(const std::vector& slices, const std::vector& heights) +void SupportPointGenerator::process(const std::vector& slices, const std::vector& heights, RandomGen &rng) { #ifdef SLA_SUPPORTPOINTGEN_DEBUG std::vector> islands; @@ -239,15 +285,15 @@ void SupportPointGenerator::process(const std::vector& slices, const //float force_deficit = s.support_force_deficit(m_config.tear_pressure()); if (s.islands_below.empty()) { // completely new island - needs support no doubt - uniformly_cover({ *s.polygon }, s, point_grid, true); + uniformly_cover({ *s.polygon }, s, point_grid, rng, true); } else if (! s.dangling_areas.empty()) { // Let's see if there's anything that overlaps enough to need supports: // What we now have in polygons needs support, regardless of what the forces are, so we can add them. //FIXME is it an island point or not? Vojtech thinks it is. - uniformly_cover(s.dangling_areas, s, point_grid); + uniformly_cover(s.dangling_areas, s, point_grid, rng); } else if (! s.overhangs_slopes.empty()) { //FIXME add the support force deficit as a parameter, only cover until the defficiency is covered. - uniformly_cover(s.overhangs_slopes, s, point_grid); + uniformly_cover(s.overhangs_slopes, s, point_grid, rng); } } @@ -266,7 +312,7 @@ void SupportPointGenerator::process(const std::vector& slices, const } } -std::vector sample_expolygon(const ExPolygon &expoly, float samples_per_mm2, std::mt19937 &rng) +std::vector sample_expolygon(const ExPolygon &expoly, float samples_per_mm2, SupportPointGenerator::RandomGen &rng) { // Triangulate the polygon with holes into triplets of 3D points. std::vector triangles = Slic3r::triangulate_expolygon_2f(expoly); @@ -306,7 +352,7 @@ std::vector sample_expolygon(const ExPolygon &expoly, float samples_per_m return out; } -std::vector sample_expolygon_with_boundary(const ExPolygon &expoly, float samples_per_mm2, float samples_per_mm_boundary, std::mt19937 &rng) +std::vector sample_expolygon_with_boundary(const ExPolygon &expoly, float samples_per_mm2, float samples_per_mm_boundary, SupportPointGenerator::RandomGen &rng) { std::vector out = sample_expolygon(expoly, samples_per_mm2, rng); double point_stepping_scaled = scale_(1.f) / samples_per_mm_boundary; @@ -319,7 +365,7 @@ std::vector sample_expolygon_with_boundary(const ExPolygon &expoly, float return out; } -std::vector sample_expolygon_with_boundary(const ExPolygons &expolys, float samples_per_mm2, float samples_per_mm_boundary, std::mt19937 &rng) +std::vector sample_expolygon_with_boundary(const ExPolygons &expolys, float samples_per_mm2, float samples_per_mm_boundary, SupportPointGenerator::RandomGen &rng) { std::vector out; for (const ExPolygon &expoly : expolys) @@ -442,7 +488,7 @@ static inline std::vector poisson_disk_from_samples(const std::vector raw_samples = sample_expolygon_with_boundary(islands, samples_per_mm2, 5.f / poisson_radius, rng); std::vector poisson_samples; for (size_t iter = 0; iter < 4; ++ iter) { diff --git a/src/libslic3r/SLA/SupportPointGenerator.hpp b/src/libslic3r/SLA/SupportPointGenerator.hpp index 3bbe3d7695..b8f5b607ea 100644 --- a/src/libslic3r/SLA/SupportPointGenerator.hpp +++ b/src/libslic3r/SLA/SupportPointGenerator.hpp @@ -29,7 +29,10 @@ public: SupportPointGenerator(const EigenMesh3D& emesh, const std::vector& slices, const std::vector& heights, const Config& config, std::function throw_on_cancel, std::function statusfn); - const std::vector& output() { return m_output; } + SupportPointGenerator(const EigenMesh3D& emesh, const Config& config, std::function throw_on_cancel, std::function statusfn); + + const std::vector& output() const { return m_output; } + std::vector& output() { return m_output; } struct MyLayer; @@ -76,7 +79,7 @@ public: ExPolygons overhangs; // Overhangs, where the surface must slope. ExPolygons overhangs_slopes; - float overhangs_area; + float overhangs_area = 0.f; bool overlaps(const Structure &rhs) const { return this->bbox.overlap(rhs.bbox) && (this->polygon->overlaps(*rhs.polygon) || rhs.polygon->overlaps(*this->polygon)); @@ -184,13 +187,21 @@ public: } }; + void execute(const std::vector &slices, + const std::vector & heights); + + void execute(const std::vector &slices, + const std::vector & heights, long seed); + + class RandomGen; + private: std::vector m_output; SupportPointGenerator::Config m_config; - void process(const std::vector& slices, const std::vector& heights); - void uniformly_cover(const ExPolygons& islands, Structure& structure, PointGrid3D &grid3d, bool is_new_island = false, bool just_one = false); + void process(const std::vector& slices, const std::vector& heights, RandomGen&); + void uniformly_cover(const ExPolygons& islands, Structure& structure, PointGrid3D &grid3d, RandomGen&, bool is_new_island = false, bool just_one = false); void project_onto_mesh(std::vector& points) const; #ifdef SLA_SUPPORTPOINTGEN_DEBUG diff --git a/src/libslic3r/SLA/SupportTreeBuildsteps.cpp b/src/libslic3r/SLA/SupportTreeBuildsteps.cpp index 68afb73919..45fef58cd0 100644 --- a/src/libslic3r/SLA/SupportTreeBuildsteps.cpp +++ b/src/libslic3r/SLA/SupportTreeBuildsteps.cpp @@ -7,6 +7,14 @@ namespace Slic3r { namespace sla { +static const Vec3d DOWN = {0.0, 0.0, -1.0}; + +using libnest2d::opt::initvals; +using libnest2d::opt::bound; +using libnest2d::opt::StopCriteria; +using libnest2d::opt::GeneticOptimizer; +using libnest2d::opt::SubplexOptimizer; + SupportTreeBuildsteps::SupportTreeBuildsteps(SupportTreeBuilder & builder, const SupportableMesh &sm) : m_cfg(sm.cfg) @@ -560,9 +568,7 @@ void SupportTreeBuildsteps::create_ground_pillar(const Vec3d &jp, double radius, long head_id) { - // People were killed for this number (seriously) - static const double SQR2 = std::sqrt(2.0); - static const Vec3d DOWN = {0.0, 0.0, -1.0}; + const double SLOPE = 1. / std::cos(m_cfg.bridge_slope); double gndlvl = m_builder.ground_level; Vec3d endp = {jp(X), jp(Y), gndlvl}; @@ -573,38 +579,47 @@ void SupportTreeBuildsteps::create_ground_pillar(const Vec3d &jp, bool can_add_base = true; bool normal_mode = true; + // If in zero elevation mode and the pillar is too close to the model body, + // the support pillar can not be placed in the gap between the model and + // the pad, and the pillar bases must not touch the model body either. + // To solve this, a corrector bridge is inserted between the starting point + // (jp) and the new pillar. if (m_cfg.object_elevation_mm < EPSILON && (dist = std::sqrt(m_mesh.squared_distance(endp))) < min_dist) { // Get the distance from the mesh. This can be later optimized // to get the distance in 2D plane because we are dealing with // the ground level only. + + normal_mode = false; + + // The min distance needed to move away from the model in XY plane. + double current_d = min_dist - dist; + double current_bride_d = SLOPE * current_d; + + // get a suitable direction for the corrector bridge. It is the + // original sourcedir's azimuth but the polar angle is saturated to the + // configured bridge slope. + auto [polar, azimuth] = dir_to_spheric(sourcedir); + polar = PI - m_cfg.bridge_slope; + auto dir = spheric_to_dir(polar, azimuth).normalized(); - normal_mode = false; - double mind = min_dist - dist; - double azimuth = std::atan2(sourcedir(Y), sourcedir(X)); - double sinpolar = std::sin(PI - m_cfg.bridge_slope); - double cospolar = std::cos(PI - m_cfg.bridge_slope); - double cosazm = std::cos(azimuth); - double sinazm = std::sin(azimuth); - - auto dir = Vec3d(cosazm * sinpolar, sinazm * sinpolar, cospolar) - .normalized(); - - using namespace libnest2d::opt; StopCriteria scr; scr.stop_score = min_dist; SubplexOptimizer solver(scr); + // Search for a distance along the corrector bridge to move the endpoint + // sufficiently away form the model body. The first few optimization + // cycles should succeed here. auto result = solver.optimize_max( [this, dir, jp, gndlvl](double mv) { - Vec3d endpt = jp + SQR2 * mv * dir; + Vec3d endpt = jp + mv * dir; endpt(Z) = gndlvl; return std::sqrt(m_mesh.squared_distance(endpt)); }, - initvals(mind), bound(0.0, 2 * min_dist)); + initvals(current_bride_d), + bound(0.0, m_cfg.max_bridge_length_mm - current_bride_d)); - mind = std::get<0>(result.optimum); - endp = jp + SQR2 * mind * dir; + endp = jp + std::get<0>(result.optimum) * dir; Vec3d pgnd = {endp(X), endp(Y), gndlvl}; can_add_base = result.score > min_dist; @@ -623,7 +638,7 @@ void SupportTreeBuildsteps::create_ground_pillar(const Vec3d &jp, else { // If the new endpoint is below ground, do not make a pillar if (endp(Z) < gndlvl) - endp = endp - SQR2 * (gndlvl - endp(Z)) * dir; // back off + endp = endp - SLOPE * (gndlvl - endp(Z)) * dir; // back off else { auto hit = bridge_mesh_intersect(endp, DOWN, radius); @@ -685,11 +700,6 @@ void SupportTreeBuildsteps::filter() // not be enough space for the pinhead. Filtering is applied for // these reasons. - using libnest2d::opt::bound; - using libnest2d::opt::initvals; - using libnest2d::opt::GeneticOptimizer; - using libnest2d::opt::StopCriteria; - ccr::SpinningMutex mutex; auto addfn = [&mutex](PtIndices &container, unsigned val) { std::lock_guard lk(mutex); @@ -708,10 +718,7 @@ void SupportTreeBuildsteps::filter() // (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)); + auto [polar, azimuth] = dir_to_spheric(n); // skip if the tilt is not sane if(polar >= PI - m_cfg.normal_cutoff_angle) { @@ -729,9 +736,7 @@ void SupportTreeBuildsteps::filter() double pin_r = double(m_support_pts[fidx].head_front_radius); // Reassemble the now corrected normal - auto nn = Vec3d(std::cos(azimuth) * std::sin(polar), - std::sin(azimuth) * std::sin(polar), - std::cos(polar)).normalized(); + auto nn = spheric_to_dir(polar, azimuth).normalized(); // check available distance EigenMesh3D::hit_result t @@ -757,9 +762,7 @@ void SupportTreeBuildsteps::filter() auto oresult = solver.optimize_max( [this, pin_r, w, hp](double plr, double azm) { - auto dir = Vec3d(std::cos(azm) * std::sin(plr), - std::sin(azm) * std::sin(plr), - std::cos(plr)).normalized(); + auto dir = spheric_to_dir(plr, azm).normalized(); double score = pinhead_mesh_intersect( hp, dir, pin_r, m_cfg.head_back_radius_mm, w); @@ -767,17 +770,14 @@ void SupportTreeBuildsteps::filter() return score; }, initvals(polar, azimuth), // start with what we have - bound(3 * PI / 4, - PI), // Must not exceed the tilt limit + bound(3 * PI / 4, PI), // Must not exceed the tilt limit bound(-PI, PI) // azimuth can be a full search ); if(oresult.score > w) { polar = std::get<0>(oresult.optimum); azimuth = std::get<1>(oresult.optimum); - nn = Vec3d(std::cos(azimuth) * std::sin(polar), - std::sin(azimuth) * std::sin(polar), - std::cos(polar)).normalized(); + nn = spheric_to_dir(polar, azimuth).normalized(); t = EigenMesh3D::hit_result(oresult.score); } } @@ -837,16 +837,17 @@ void SupportTreeBuildsteps::classify() m_thr(); auto& head = m_builder.head(i); - Vec3d n(0, 0, -1); double r = head.r_back_mm; Vec3d headjp = head.junction_point(); // collision check - auto hit = bridge_mesh_intersect(headjp, n, r); + auto hit = bridge_mesh_intersect(headjp, DOWN, r); if(std::isinf(hit.distance())) ground_head_indices.emplace_back(i); else if(m_cfg.ground_facing_only) head.invalidate(); - else m_iheads_onmodel.emplace_back(std::make_pair(i, hit)); + else m_iheads_onmodel.emplace_back(i); + + m_head_to_ground_scans[i] = hit; } // We want to search for clusters of points that are far enough @@ -893,13 +894,14 @@ void SupportTreeBuildsteps::routing_to_ground() // get the current cluster centroid auto & thr = m_thr; const auto &points = m_points; - long lcid = cluster_centroid( + + long lcid = cluster_centroid( cl, [&points](size_t idx) { return points.row(long(idx)); }, [thr](const Vec3d &p1, const Vec3d &p2) { thr(); return distance(Vec2d(p1(X), p1(Y)), Vec2d(p2(X), p2(Y))); }); - + assert(lcid >= 0); unsigned hid = cl[size_t(lcid)]; // Head ID @@ -944,192 +946,138 @@ void SupportTreeBuildsteps::routing_to_ground() } } +bool SupportTreeBuildsteps::connect_to_ground(Head &head, const Vec3d &dir) +{ + auto hjp = head.junction_point(); + double r = head.r_back_mm; + double t = bridge_mesh_intersect(hjp, dir, head.r_back_mm); + double d = 0, tdown = 0; + t = std::min(t, m_cfg.max_bridge_length_mm); + + while (d < t && !std::isinf(tdown = bridge_mesh_intersect(hjp + d * dir, DOWN, r))) + d += r; + + if(!std::isinf(tdown)) return false; + + Vec3d endp = hjp + d * dir; + m_builder.add_bridge(head.id, endp); + m_builder.add_junction(endp, head.r_back_mm); + + this->create_ground_pillar(endp, dir, head.r_back_mm); + + return true; +} + +bool SupportTreeBuildsteps::connect_to_ground(Head &head) +{ + if (connect_to_ground(head, head.dir)) return true; + + // Optimize bridge direction: + // Straight path failed so we will try to search for a suitable + // direction out of the cavity. + auto [polar, azimuth] = dir_to_spheric(head.dir); + + StopCriteria stc; + stc.max_iterations = m_cfg.optimizer_max_iterations; + stc.relative_score_difference = m_cfg.optimizer_rel_score_diff; + stc.stop_score = 1e6; + GeneticOptimizer solver(stc); + solver.seed(0); // we want deterministic behavior + + double r_back = head.r_back_mm; + Vec3d hjp = head.junction_point(); + auto oresult = solver.optimize_max( + [this, hjp, r_back](double plr, double azm) { + Vec3d n = spheric_to_dir(plr, azm).normalized(); + return bridge_mesh_intersect(hjp, n, r_back); + }, + initvals(polar, azimuth), // let's start with what we have + bound(3*PI/4, PI), // Must not exceed the slope limit + bound(-PI, PI) // azimuth can be a full range search + ); + + Vec3d bridgedir = spheric_to_dir(oresult.optimum).normalized(); + return connect_to_ground(head, bridgedir); +} + +bool SupportTreeBuildsteps::connect_to_model_body(Head &head) +{ + if (head.id <= ID_UNSET) return false; + + auto it = m_head_to_ground_scans.find(unsigned(head.id)); + if (it == m_head_to_ground_scans.end()) return false; + + auto &hit = it->second; + Vec3d hjp = head.junction_point(); + double zangle = std::asin(hit.direction()(Z)); + zangle = std::max(zangle, PI/4); + double h = std::sin(zangle) * head.fullwidth(); + + // The width of the tail head that we would like to have... + h = std::min(hit.distance() - head.r_back_mm, h); + + if(h <= 0.) return false; + + Vec3d endp{hjp(X), hjp(Y), hjp(Z) - hit.distance() + h}; + auto center_hit = m_mesh.query_ray_hit(hjp, DOWN); + + double hitdiff = center_hit.distance() - hit.distance(); + Vec3d hitp = std::abs(hitdiff) < 2*head.r_back_mm? + center_hit.position() : hit.position(); + + head.transform(); + + long pillar_id = m_builder.add_pillar(head.id, endp, head.r_back_mm); + Pillar &pill = m_builder.pillar(pillar_id); + + Vec3d taildir = endp - hitp; + double dist = distance(endp, hitp) + m_cfg.head_penetration_mm; + double w = dist - 2 * head.r_pin_mm - head.r_back_mm; + + if (w < 0.) { + BOOST_LOG_TRIVIAL(error) << "Pinhead width is negative!"; + w = 0.; + } + + Head tailhead(head.r_back_mm, head.r_pin_mm, w, + m_cfg.head_penetration_mm, taildir, hitp); + + tailhead.transform(); + pill.base = tailhead.mesh; + + m_pillar_index.guarded_insert(pill.endpoint(), pill.id); + + return true; +} + void SupportTreeBuildsteps::routing_to_model() { // We need to check if there is an easy way out to the bed surface. // If it can be routed there with a bridge shorter than // min_bridge_distance. - - // First we want to index the available pillars. The best is to connect - // these points to the available pillars - - auto routedown = [this](Head& head, const Vec3d& dir, double dist) - { - head.transform(); - Vec3d endp = head.junction_point() + dist * dir; - m_builder.add_bridge(head.id, endp); - m_builder.add_junction(endp, head.r_back_mm); - - this->create_ground_pillar(endp, dir, head.r_back_mm); - }; - - std::vector modelpillars; - ccr::SpinningMutex mutex; - auto onmodelfn = - [this, routedown, &modelpillars, &mutex] - (const std::pair &el, size_t) - { + ccr::enumerate(m_iheads_onmodel.begin(), m_iheads_onmodel.end(), + [this] (const unsigned idx, size_t) { m_thr(); - unsigned idx = el.first; - EigenMesh3D::hit_result hit = el.second; auto& head = m_builder.head(idx); - Vec3d hjp = head.junction_point(); - // ///////////////////////////////////////////////////////////////// // Search nearby pillar - // ///////////////////////////////////////////////////////////////// - if(search_pillar_and_connect(head)) { head.transform(); return; } - // ///////////////////////////////////////////////////////////////// - // Try straight path - // ///////////////////////////////////////////////////////////////// - // Cannot connect to nearby pillar. We will try to search for // a route to the ground. + if(connect_to_ground(head)) { head.transform(); return; } - double t = bridge_mesh_intersect(hjp, head.dir, head.r_back_mm); - double d = 0, tdown = 0; - Vec3d dirdown(0.0, 0.0, -1.0); - - t = std::min(t, m_cfg.max_bridge_length_mm); - - while(d < t && !std::isinf(tdown = bridge_mesh_intersect( - hjp + d*head.dir, - dirdown, head.r_back_mm))) { - d += head.r_back_mm; - } - - if(std::isinf(tdown)) { // we heave found a route to the ground - routedown(head, head.dir, d); return; - } - - // ///////////////////////////////////////////////////////////////// - // Optimize bridge direction - // ///////////////////////////////////////////////////////////////// - - // Straight path failed so we will try to search for a suitable - // direction out of the cavity. - - // Get the spherical representation of the normal. its easier to - // work with. - double z = head.dir(Z); - double r = 1.0; // for normalized vector - double polar = std::acos(z / r); - double azimuth = std::atan2(head.dir(Y), head.dir(X)); - - using libnest2d::opt::bound; - using libnest2d::opt::initvals; - using libnest2d::opt::GeneticOptimizer; - using libnest2d::opt::StopCriteria; - - StopCriteria stc; - stc.max_iterations = m_cfg.optimizer_max_iterations; - stc.relative_score_difference = m_cfg.optimizer_rel_score_diff; - stc.stop_score = 1e6; - GeneticOptimizer solver(stc); - solver.seed(0); // we want deterministic behavior - - double r_back = head.r_back_mm; - - auto oresult = solver.optimize_max( - [this, hjp, r_back](double plr, double azm) - { - Vec3d n = Vec3d(std::cos(azm) * std::sin(plr), - std::sin(azm) * std::sin(plr), - std::cos(plr)).normalized(); - return bridge_mesh_intersect(hjp, n, r_back); - }, - initvals(polar, azimuth), // let's start with what we have - bound(3*PI/4, PI), // Must not exceed the slope limit - bound(-PI, PI) // azimuth can be a full range search - ); - - d = 0; t = oresult.score; - - polar = std::get<0>(oresult.optimum); - azimuth = std::get<1>(oresult.optimum); - Vec3d bridgedir = Vec3d(std::cos(azimuth) * std::sin(polar), - std::sin(azimuth) * std::sin(polar), - std::cos(polar)).normalized(); - - t = std::min(t, m_cfg.max_bridge_length_mm); - - while(d < t && !std::isinf(tdown = bridge_mesh_intersect( - hjp + d*bridgedir, - dirdown, - head.r_back_mm))) { - d += head.r_back_mm; - } - - if(std::isinf(tdown)) { // we heave found a route to the ground - routedown(head, bridgedir, d); return; - } - - // ///////////////////////////////////////////////////////////////// - // Route to model body - // ///////////////////////////////////////////////////////////////// - - double zangle = std::asin(hit.direction()(Z)); - zangle = std::max(zangle, PI/4); - double h = std::sin(zangle) * head.fullwidth(); - - // The width of the tail head that we would like to have... - h = std::min(hit.distance() - head.r_back_mm, h); - - if(h > 0) { - Vec3d endp{hjp(X), hjp(Y), hjp(Z) - hit.distance() + h}; - auto center_hit = m_mesh.query_ray_hit(hjp, dirdown); - - double hitdiff = center_hit.distance() - hit.distance(); - Vec3d hitp = std::abs(hitdiff) < 2*head.r_back_mm? - center_hit.position() : hit.position(); - - head.transform(); - - long pillar_id = m_builder.add_pillar(head.id, endp, head.r_back_mm); - Pillar &pill = m_builder.pillar(pillar_id); - - Vec3d taildir = endp - hitp; - double dist = distance(endp, hitp) + m_cfg.head_penetration_mm; - double w = dist - 2 * head.r_pin_mm - head.r_back_mm; - - if (w < 0.) { - BOOST_LOG_TRIVIAL(error) << "Pinhead width is negative!"; - w = 0.; - } - - Head tailhead(head.r_back_mm, - head.r_pin_mm, - w, - m_cfg.head_penetration_mm, - taildir, - hitp); - - tailhead.transform(); - pill.base = tailhead.mesh; - - // Experimental: add the pillar to the index for cascading - std::lock_guard lk(mutex); - modelpillars.emplace_back(unsigned(pill.id)); - 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(); - }; - - ccr::enumerate(m_iheads_onmodel.begin(), m_iheads_onmodel.end(), onmodelfn); - - for(auto pillid : modelpillars) { - auto& pillar = m_builder.pillar(pillid); - m_pillar_index.insert(pillar.endpoint(), pillid); - } + }); } void SupportTreeBuildsteps::interconnect_pillars() @@ -1280,7 +1228,8 @@ void SupportTreeBuildsteps::interconnect_pillars() spts[n] = s; // Check the path vertically down - auto hr = bridge_mesh_intersect(s, {0, 0, -1}, pillar().r); + Vec3d check_from = s + Vec3d{0., 0., pillar().r}; + auto hr = bridge_mesh_intersect(check_from, DOWN, pillar().r); Vec3d gndsp{s(X), s(Y), gnd}; // If the path is clear, check for pillar base collisions @@ -1360,12 +1309,11 @@ void SupportTreeBuildsteps::routing_headless() Vec3d n = m_support_nmls.row(i); // mesh outward normal Vec3d sp = sph - n * HWIDTH_MM; // stick head start point - Vec3d dir = {0, 0, -1}; Vec3d sj = sp + R * n; // stick start point // This is only for checking - double idist = bridge_mesh_intersect(sph, dir, R, true); - double realdist = ray_mesh_intersect(sj, dir); + double idist = bridge_mesh_intersect(sph, DOWN, R, true); + double realdist = ray_mesh_intersect(sj, DOWN); double dist = realdist; if (std::isinf(dist)) dist = sph(Z) - m_builder.ground_level; @@ -1378,7 +1326,7 @@ void SupportTreeBuildsteps::routing_headless() } bool use_endball = !std::isinf(realdist); - Vec3d ej = sj + (dist + HWIDTH_MM) * dir; + Vec3d ej = sj + (dist + HWIDTH_MM) * DOWN ; m_builder.add_compact_bridge(sp, ej, n, R, use_endball); } } diff --git a/src/libslic3r/SLA/SupportTreeBuildsteps.hpp b/src/libslic3r/SLA/SupportTreeBuildsteps.hpp index 3998f5a350..9533049b60 100644 --- a/src/libslic3r/SLA/SupportTreeBuildsteps.hpp +++ b/src/libslic3r/SLA/SupportTreeBuildsteps.hpp @@ -20,6 +20,32 @@ inline Vec2d to_vec2(const Vec3d& v3) { return {v3(X), v3(Y)}; } +inline std::pair dir_to_spheric(const Vec3d &n, double norm = 1.) +{ + double z = n.z(); + double r = norm; + double polar = std::acos(z / r); + double azimuth = std::atan2(n(1), n(0)); + return {polar, azimuth}; +} + +inline Vec3d spheric_to_dir(double polar, double azimuth) +{ + return {std::cos(azimuth) * std::sin(polar), + std::sin(azimuth) * std::sin(polar), std::cos(polar)}; +} + +inline Vec3d spheric_to_dir(const std::tuple &v) +{ + auto [plr, azm] = v; + return spheric_to_dir(plr, azm); +} + +inline Vec3d spheric_to_dir(const std::pair &v) +{ + return spheric_to_dir(v.first, v.second); +} + // This function returns the position of the centroid in the input 'clust' // vector of point indices. template @@ -151,10 +177,10 @@ class SupportTreeBuildsteps { using PtIndices = std::vector; PtIndices m_iheads; // support points with pinhead + PtIndices m_iheads_onmodel; PtIndices m_iheadless; // headless support points - - // supp. pts. connecting to model: point index and the ray hit data - std::vector> m_iheads_onmodel; + + std::map m_head_to_ground_scans; // normals for support points from model faces. PointSet m_support_nmls; @@ -223,15 +249,29 @@ class SupportTreeBuildsteps { // For connecting a head to a nearby pillar. bool connect_to_nearpillar(const Head& head, long nearpillar_id); - + + // Find route for a head to the ground. Inserts additional bridge from the + // head to the pillar if cannot create pillar directly. + // The optional dir parameter is the direction of the bridge which is the + // direction of the pinhead if omitted. + bool connect_to_ground(Head& head, const Vec3d &dir); + inline bool connect_to_ground(Head& head); + + bool connect_to_model_body(Head &head); + bool search_pillar_and_connect(const Head& head); - + // This is a proxy function for pillar creation which will mind the gap // between the pad and the model bottom in zero elevation mode. + // jp is the starting junction point which needs to be routed down. + // sourcedir is the allowed direction of an optional bridge between the + // jp junction and the final pillar. void create_ground_pillar(const Vec3d &jp, const Vec3d &sourcedir, double radius, long head_id = ID_UNSET); + + public: SupportTreeBuildsteps(SupportTreeBuilder & builder, const SupportableMesh &sm); diff --git a/tests/sla_print/sla_print_tests.cpp b/tests/sla_print/sla_print_tests.cpp index 4fefeb6bbd..8f69b84342 100644 --- a/tests/sla_print/sla_print_tests.cpp +++ b/tests/sla_print/sla_print_tests.cpp @@ -85,6 +85,53 @@ TEST_CASE("Pillar pairhash should be unique", "[SLASupportGeneration]") { test_pairhash(); } +TEST_CASE("Support point generator should be deterministic if seeded", + "[SLASupportGeneration], [SLAPointGen]") { + TriangleMesh mesh = load_model("A_upsidedown.obj"); + + sla::EigenMesh3D emesh{mesh}; + + sla::SupportConfig supportcfg; + sla::SupportPointGenerator::Config autogencfg; + autogencfg.head_diameter = float(2 * supportcfg.head_front_radius_mm); + sla::SupportPointGenerator point_gen{emesh, autogencfg, [] {}, [](int) {}}; + + TriangleMeshSlicer slicer{&mesh}; + + auto bb = mesh.bounding_box(); + double zmin = bb.min.z(); + double zmax = bb.max.z(); + double gnd = zmin - supportcfg.object_elevation_mm; + auto layer_h = 0.05f; + + auto slicegrid = grid(float(gnd), float(zmax), layer_h); + std::vector slices; + slicer.slice(slicegrid, CLOSING_RADIUS, &slices, []{}); + + point_gen.execute(slices, slicegrid, 0); + + auto get_chksum = [](const std::vector &pts){ + long long chksum = 0; + for (auto &pt : pts) { + auto p = scaled(pt.pos); + chksum += p.x() + p.y() + p.z(); + } + + return chksum; + }; + + long long checksum = get_chksum(point_gen.output()); + size_t ptnum = point_gen.output().size(); + REQUIRE(point_gen.output().size() > 0); + + for (int i = 0; i < 20; ++i) { + point_gen.output().clear(); + point_gen.execute(slices, slicegrid, 0); + REQUIRE(point_gen.output().size() == ptnum); + REQUIRE(checksum == get_chksum(point_gen.output())); + } +} + TEST_CASE("Flat pad geometry is valid", "[SLASupportGeneration]") { sla::PadConfig padcfg; diff --git a/tests/sla_print/sla_test_utils.cpp b/tests/sla_print/sla_test_utils.cpp index 3da28a50e6..ab1bbac0eb 100644 --- a/tests/sla_print/sla_test_utils.cpp +++ b/tests/sla_print/sla_test_utils.cpp @@ -13,11 +13,6 @@ void test_support_model_collision(const std::string &obj_filename, // the supports will not touch the model body. supportcfg.head_penetration_mm = -0.15; - // TODO: currently, the tailheads penetrating into the model body do not - // respect the penetration parameter properly. No issues were reported so - // far but we should definitely fix this. - supportcfg.ground_facing_only = true; - test_supports(obj_filename, supportcfg, hollowingcfg, drainholes, byproducts); // Slice the support mesh given the slice grid of the model. @@ -115,8 +110,10 @@ void test_supports(const std::string &obj_filename, // Create the support point generator sla::SupportPointGenerator::Config autogencfg; autogencfg.head_diameter = float(2 * supportcfg.head_front_radius_mm); - sla::SupportPointGenerator point_gen{emesh, out.model_slices, out.slicegrid, - autogencfg, [] {}, [](int) {}}; + sla::SupportPointGenerator point_gen{emesh, autogencfg, [] {}, [](int) {}}; + + long seed = 0; // Make the test repeatable + point_gen.execute(out.model_slices, out.slicegrid, seed); // Get the calculated support points. std::vector support_points = point_gen.output();