mirror of
https://git.mirrors.martin98.com/https://github.com/prusa3d/PrusaSlicer.git
synced 2025-08-12 02:39:06 +08:00
Pm jps path finding (#8)
New step - estimation of curling on both the model and the support extrusions. Improvements in curled filament estimation algortihm Implementation of Jump Point Search algorithm Use of JPS algorithm to avoid curled extrusions during travel moves in Gcode export
This commit is contained in:
parent
06fbab12fe
commit
633ce8aa21
@ -160,6 +160,8 @@ set(SLIC3R_SOURCES
|
|||||||
Geometry/VoronoiOffset.hpp
|
Geometry/VoronoiOffset.hpp
|
||||||
Geometry/VoronoiVisualUtils.hpp
|
Geometry/VoronoiVisualUtils.hpp
|
||||||
Int128.hpp
|
Int128.hpp
|
||||||
|
JumpPointSearch.cpp
|
||||||
|
JumpPointSearch.hpp
|
||||||
KDTreeIndirect.hpp
|
KDTreeIndirect.hpp
|
||||||
Layer.cpp
|
Layer.cpp
|
||||||
Layer.hpp
|
Layer.hpp
|
||||||
|
@ -7,6 +7,9 @@
|
|||||||
#include "GCode/PrintExtents.hpp"
|
#include "GCode/PrintExtents.hpp"
|
||||||
#include "GCode/Thumbnails.hpp"
|
#include "GCode/Thumbnails.hpp"
|
||||||
#include "GCode/WipeTower.hpp"
|
#include "GCode/WipeTower.hpp"
|
||||||
|
#include "Point.hpp"
|
||||||
|
#include "Polygon.hpp"
|
||||||
|
#include "PrintConfig.hpp"
|
||||||
#include "ShortestPath.hpp"
|
#include "ShortestPath.hpp"
|
||||||
#include "Print.hpp"
|
#include "Print.hpp"
|
||||||
#include "Thread.hpp"
|
#include "Thread.hpp"
|
||||||
@ -2347,6 +2350,14 @@ LayerResult GCode::process_layer(
|
|||||||
}
|
}
|
||||||
} // for objects
|
} // for objects
|
||||||
|
|
||||||
|
if (this->config().avoid_curled_filament_during_travels) {
|
||||||
|
m_avoid_curled_filaments.clear();
|
||||||
|
for (const LayerToPrint &layer_to_print : layers) {
|
||||||
|
m_avoid_curled_filaments.add_obstacles(layer_to_print.object_layer, Point(scaled(this->origin())));
|
||||||
|
m_avoid_curled_filaments.add_obstacles(layer_to_print.support_layer, Point(scaled(this->origin())));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Extrude the skirt, brim, support, perimeters, infill ordered by the extruders.
|
// Extrude the skirt, brim, support, perimeters, infill ordered by the extruders.
|
||||||
for (unsigned int extruder_id : layer_tools.extruders)
|
for (unsigned int extruder_id : layer_tools.extruders)
|
||||||
{
|
{
|
||||||
@ -3071,6 +3082,12 @@ std::string GCode::travel_to(const Point &point, ExtrusionRole role, std::string
|
|||||||
this->origin in order to get G-code coordinates. */
|
this->origin in order to get G-code coordinates. */
|
||||||
Polyline travel { this->last_pos(), point };
|
Polyline travel { this->last_pos(), point };
|
||||||
|
|
||||||
|
if (this->config().avoid_curled_filament_during_travels) {
|
||||||
|
Point scaled_origin = Point(scaled(this->origin()));
|
||||||
|
travel = m_avoid_curled_filaments.find_path(this->last_pos() + scaled_origin, point + scaled_origin);
|
||||||
|
travel.translate(-scaled_origin);
|
||||||
|
}
|
||||||
|
|
||||||
// check whether a straight travel move would need retraction
|
// check whether a straight travel move would need retraction
|
||||||
bool needs_retraction = this->needs_retraction(travel, role);
|
bool needs_retraction = this->needs_retraction(travel, role);
|
||||||
// check whether wipe could be disabled without causing visible stringing
|
// check whether wipe could be disabled without causing visible stringing
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
#ifndef slic3r_GCode_hpp_
|
#ifndef slic3r_GCode_hpp_
|
||||||
#define slic3r_GCode_hpp_
|
#define slic3r_GCode_hpp_
|
||||||
|
|
||||||
|
#include "JumpPointSearch.hpp"
|
||||||
#include "libslic3r.h"
|
#include "libslic3r.h"
|
||||||
#include "ExPolygon.hpp"
|
#include "ExPolygon.hpp"
|
||||||
#include "GCodeWriter.hpp"
|
#include "GCodeWriter.hpp"
|
||||||
@ -374,6 +375,7 @@ private:
|
|||||||
OozePrevention m_ooze_prevention;
|
OozePrevention m_ooze_prevention;
|
||||||
Wipe m_wipe;
|
Wipe m_wipe;
|
||||||
AvoidCrossingPerimeters m_avoid_crossing_perimeters;
|
AvoidCrossingPerimeters m_avoid_crossing_perimeters;
|
||||||
|
JPSPathFinder m_avoid_curled_filaments;
|
||||||
bool m_enable_loop_clipping;
|
bool m_enable_loop_clipping;
|
||||||
// If enabled, the G-code generator will put following comments at the ends
|
// If enabled, the G-code generator will put following comments at the ends
|
||||||
// of the G-code lines: _EXTRUDE_SET_SPEED, _WIPE, _BRIDGE_FAN_START, _BRIDGE_FAN_END
|
// of the G-code lines: _EXTRUDE_SET_SPEED, _WIPE, _BRIDGE_FAN_START, _BRIDGE_FAN_END
|
||||||
|
364
src/libslic3r/JumpPointSearch.cpp
Normal file
364
src/libslic3r/JumpPointSearch.cpp
Normal file
@ -0,0 +1,364 @@
|
|||||||
|
#include "JumpPointSearch.hpp"
|
||||||
|
#include "BoundingBox.hpp"
|
||||||
|
#include "Point.hpp"
|
||||||
|
#include "libslic3r/AStar.hpp"
|
||||||
|
#include "libslic3r/KDTreeIndirect.hpp"
|
||||||
|
#include "libslic3r/Polygon.hpp"
|
||||||
|
#include "libslic3r/Polyline.hpp"
|
||||||
|
#include "libslic3r/libslic3r.h"
|
||||||
|
#include <algorithm>
|
||||||
|
#include <cmath>
|
||||||
|
#include <cstddef>
|
||||||
|
#include <cstdint>
|
||||||
|
#include <cstdlib>
|
||||||
|
#include <iterator>
|
||||||
|
#include <limits>
|
||||||
|
#include <optional>
|
||||||
|
#include <string>
|
||||||
|
#include <unordered_map>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
//#define DEBUG_FILES
|
||||||
|
#ifdef DEBUG_FILES
|
||||||
|
#include "libslic3r/SVG.hpp"
|
||||||
|
#endif
|
||||||
|
|
||||||
|
namespace Slic3r {
|
||||||
|
|
||||||
|
template<typename PointFn> void dda(coord_t x0, coord_t y0, coord_t x1, coord_t y1, const PointFn &fn)
|
||||||
|
{
|
||||||
|
coord_t dx = abs(x1 - x0);
|
||||||
|
coord_t dy = abs(y1 - y0);
|
||||||
|
coord_t x = x0;
|
||||||
|
coord_t y = y0;
|
||||||
|
coord_t n = 1 + dx + dy;
|
||||||
|
coord_t x_inc = (x1 > x0) ? 1 : -1;
|
||||||
|
coord_t y_inc = (y1 > y0) ? 1 : -1;
|
||||||
|
coord_t error = dx - dy;
|
||||||
|
dx *= 2;
|
||||||
|
dy *= 2;
|
||||||
|
|
||||||
|
for (; n > 0; --n) {
|
||||||
|
fn(x, y);
|
||||||
|
|
||||||
|
if (error > 0) {
|
||||||
|
x += x_inc;
|
||||||
|
error -= dy;
|
||||||
|
} else {
|
||||||
|
y += y_inc;
|
||||||
|
error += dx;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// will draw the line twice, second time with and offset of 1 in the direction of normal
|
||||||
|
// may call the fn on the same coordiantes multiple times!
|
||||||
|
template<typename PointFn> void double_dda_with_offset(coord_t x0, coord_t y0, coord_t x1, coord_t y1, const PointFn &fn)
|
||||||
|
{
|
||||||
|
Vec2d normal = Point{y1 - y0, x1 - x0}.cast<double>().normalized();
|
||||||
|
normal.x() = ceil(normal.x());
|
||||||
|
normal.y() = ceil(normal.y());
|
||||||
|
Point start_offset = Point(x0,y0) + (normal).cast<coord_t>();
|
||||||
|
Point end_offset = Point(x1,y1) + (normal).cast<coord_t>();
|
||||||
|
|
||||||
|
dda(x0, y0, x1, y1, fn);
|
||||||
|
dda(start_offset.x(), start_offset.y(), end_offset.x(), end_offset.y(), fn);
|
||||||
|
}
|
||||||
|
|
||||||
|
template<typename CellPositionType, typename CellQueryFn> class JPSTracer
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
// Use incoming_dir [0,0] for starting points, so that all directions are checked from that point
|
||||||
|
struct Node
|
||||||
|
{
|
||||||
|
CellPositionType position;
|
||||||
|
CellPositionType incoming_dir;
|
||||||
|
};
|
||||||
|
|
||||||
|
JPSTracer(CellPositionType target, CellQueryFn is_passable) : target(target), is_passable(is_passable) {}
|
||||||
|
|
||||||
|
private:
|
||||||
|
CellPositionType target;
|
||||||
|
CellQueryFn is_passable; // should return boolean whether the cell is passable or not
|
||||||
|
|
||||||
|
CellPositionType find_jump_point(CellPositionType start, CellPositionType forward_dir) const
|
||||||
|
{
|
||||||
|
CellPositionType next = start + forward_dir;
|
||||||
|
while (next != target && is_passable(next) && !(is_jump_point(next, forward_dir))) { next = next + forward_dir; }
|
||||||
|
|
||||||
|
if (is_passable(next)) {
|
||||||
|
return next;
|
||||||
|
} else {
|
||||||
|
return start;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool is_jump_point(CellPositionType pos, CellPositionType forward_dir) const
|
||||||
|
{
|
||||||
|
if (abs(forward_dir.x()) + abs(forward_dir.y()) == 2) {
|
||||||
|
// diagonal
|
||||||
|
CellPositionType horizontal_check_dir = CellPositionType{forward_dir.x(), 0};
|
||||||
|
CellPositionType vertical_check_dir = CellPositionType{0, forward_dir.y()};
|
||||||
|
|
||||||
|
if (!is_passable(pos - horizontal_check_dir) && is_passable(pos + forward_dir - 2 * horizontal_check_dir)) { return true; }
|
||||||
|
|
||||||
|
if (!is_passable(pos - vertical_check_dir) && is_passable(pos + forward_dir - 2 * vertical_check_dir)) { return true; }
|
||||||
|
|
||||||
|
if (find_jump_point(pos, horizontal_check_dir) != pos) { return true; }
|
||||||
|
|
||||||
|
if (find_jump_point(pos, vertical_check_dir) != pos) { return true; }
|
||||||
|
|
||||||
|
return false;
|
||||||
|
} else { // horizontal or vertical
|
||||||
|
CellPositionType side_dir = CellPositionType(forward_dir.y(), forward_dir.x());
|
||||||
|
|
||||||
|
if (!is_passable(pos + side_dir) && is_passable(pos + forward_dir + side_dir)) { return true; }
|
||||||
|
|
||||||
|
if (!is_passable(pos - side_dir) && is_passable(pos + forward_dir - side_dir)) { return true; }
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public:
|
||||||
|
template<class Fn> void foreach_reachable(const Node &from, Fn &&fn) const
|
||||||
|
{
|
||||||
|
const CellPositionType &pos = from.position;
|
||||||
|
const CellPositionType &forward_dir = from.incoming_dir;
|
||||||
|
std::vector<CellPositionType> dirs_to_check{};
|
||||||
|
|
||||||
|
if (abs(forward_dir.x()) + abs(forward_dir.y()) == 0) { // special case for starting point
|
||||||
|
dirs_to_check = all_directions;
|
||||||
|
} else if (abs(forward_dir.x()) + abs(forward_dir.y()) == 2) {
|
||||||
|
// diagonal
|
||||||
|
CellPositionType horizontal_check_dir = CellPositionType{forward_dir.x(), 0};
|
||||||
|
CellPositionType vertical_check_dir = CellPositionType{0, forward_dir.y()};
|
||||||
|
|
||||||
|
if (!is_passable(pos - horizontal_check_dir) && is_passable(pos + forward_dir - 2 * horizontal_check_dir)) {
|
||||||
|
dirs_to_check.push_back(forward_dir - 2 * horizontal_check_dir);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!is_passable(pos - vertical_check_dir) && is_passable(pos + forward_dir - 2 * vertical_check_dir)) {
|
||||||
|
dirs_to_check.push_back(forward_dir - 2 * vertical_check_dir);
|
||||||
|
}
|
||||||
|
|
||||||
|
dirs_to_check.push_back(horizontal_check_dir);
|
||||||
|
dirs_to_check.push_back(vertical_check_dir);
|
||||||
|
dirs_to_check.push_back(forward_dir);
|
||||||
|
|
||||||
|
} else { // horizontal or vertical
|
||||||
|
CellPositionType side_dir = CellPositionType(forward_dir.y(), forward_dir.x());
|
||||||
|
|
||||||
|
if (!is_passable(pos + side_dir) && is_passable(pos + forward_dir + side_dir)) {
|
||||||
|
dirs_to_check.push_back(forward_dir + side_dir);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!is_passable(pos - side_dir) && is_passable(pos + forward_dir - side_dir)) {
|
||||||
|
dirs_to_check.push_back(forward_dir - side_dir);
|
||||||
|
}
|
||||||
|
dirs_to_check.push_back(forward_dir);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const CellPositionType &dir : dirs_to_check) {
|
||||||
|
CellPositionType jp = find_jump_point(pos, dir);
|
||||||
|
if (jp != pos) fn(Node{jp, dir});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
float distance(Node a, Node b) const { return (a.position - b.position).template cast<double>().norm(); }
|
||||||
|
|
||||||
|
float goal_heuristic(Node n) const { return n.position == target ? -1.f : (target - n.position).template cast<double>().norm(); }
|
||||||
|
|
||||||
|
size_t unique_id(Node n) const { return (static_cast<size_t>(uint16_t(n.position.x())) << 16) + static_cast<size_t>(uint16_t(n.position.y())); }
|
||||||
|
|
||||||
|
const std::vector<CellPositionType> all_directions{{1, 0}, {1, 1}, {0, 1}, {-1, 1}, {-1, 0}, {-1, -1}, {0, -1}, {1, -1}};
|
||||||
|
};
|
||||||
|
|
||||||
|
void JPSPathFinder::clear()
|
||||||
|
{
|
||||||
|
inpassable.clear();
|
||||||
|
obstacle_max = Pixel(std::numeric_limits<coord_t>::min(), std::numeric_limits<coord_t>::min());
|
||||||
|
obstacle_min = Pixel(std::numeric_limits<coord_t>::max(), std::numeric_limits<coord_t>::max());
|
||||||
|
}
|
||||||
|
|
||||||
|
void JPSPathFinder::add_obstacles(const Lines &obstacles)
|
||||||
|
{
|
||||||
|
auto store_obstacle = [&](coord_t x, coord_t y) {
|
||||||
|
obstacle_max.x() = std::max(obstacle_max.x(), x);
|
||||||
|
obstacle_max.y() = std::max(obstacle_max.y(), y);
|
||||||
|
obstacle_min.x() = std::min(obstacle_min.x(), x);
|
||||||
|
obstacle_min.y() = std::min(obstacle_min.y(), y);
|
||||||
|
inpassable.insert(Pixel{x, y});
|
||||||
|
};
|
||||||
|
|
||||||
|
for (const Line &l : obstacles) {
|
||||||
|
Pixel start = pixelize(l.a);
|
||||||
|
Pixel end = pixelize(l.b);
|
||||||
|
double_dda_with_offset(start.x(), start.y(), end.x(), end.y(), store_obstacle);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void JPSPathFinder::add_obstacles(const Layer *layer, const Point &global_origin)
|
||||||
|
{
|
||||||
|
if (layer != nullptr) { this->print_z = layer->print_z; }
|
||||||
|
|
||||||
|
auto store_obstacle = [&](coord_t x, coord_t y) {
|
||||||
|
obstacle_max.x() = std::max(obstacle_max.x(), x);
|
||||||
|
obstacle_max.y() = std::max(obstacle_max.y(), y);
|
||||||
|
obstacle_min.x() = std::min(obstacle_min.x(), x);
|
||||||
|
obstacle_min.y() = std::min(obstacle_min.y(), y);
|
||||||
|
inpassable.insert(Pixel{x, y});
|
||||||
|
};
|
||||||
|
Lines obstacles;
|
||||||
|
for (size_t step = 0; step < 3; step++) {
|
||||||
|
if (layer != nullptr) {
|
||||||
|
obstacles.insert(obstacles.end(), layer->malformed_lines.begin(), layer->malformed_lines.end());
|
||||||
|
layer = layer->lower_layer;
|
||||||
|
} else {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const Line &l : obstacles) {
|
||||||
|
Pixel start = pixelize(l.a + global_origin);
|
||||||
|
Pixel end = pixelize(l.b + global_origin);
|
||||||
|
double_dda_with_offset(start.x(), start.y(), end.x(), end.y(), store_obstacle);
|
||||||
|
}
|
||||||
|
#ifdef DEBUG_FILES
|
||||||
|
::Slic3r::SVG svg(debug_out_path(("obstacles_jps" + std::to_string(print_z) + "_" + std::to_string(rand() % 1000)).c_str()).c_str(),
|
||||||
|
get_extents(obstacles));
|
||||||
|
svg.draw(obstacles);
|
||||||
|
svg.Close();
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
Polyline JPSPathFinder::find_path(const Point &p0, const Point &p1)
|
||||||
|
{
|
||||||
|
Pixel start = pixelize(p0);
|
||||||
|
Pixel end = pixelize(p1);
|
||||||
|
if (inpassable.empty() || (start - end).cast<float>().norm() < 3.0) { return Polyline{p0, p1}; }
|
||||||
|
|
||||||
|
BoundingBox search_box({start,end,obstacle_max,obstacle_min});
|
||||||
|
search_box.max += Pixel(1,1);
|
||||||
|
search_box.min -= Pixel(1,1);
|
||||||
|
|
||||||
|
|
||||||
|
BoundingBox bounding_square(Points{start,end});
|
||||||
|
bounding_square.max += Pixel(5,5);
|
||||||
|
bounding_square.min -= Pixel(5,5);
|
||||||
|
coord_t bounding_square_size = 2*std::max(bounding_square.size().x(),bounding_square.size().y());
|
||||||
|
bounding_square.max.x() += (bounding_square_size - bounding_square.size().x()) / 2;
|
||||||
|
bounding_square.min.x() -= (bounding_square_size - bounding_square.size().x()) / 2;
|
||||||
|
bounding_square.max.y() += (bounding_square_size - bounding_square.size().y()) / 2;
|
||||||
|
bounding_square.min.y() -= (bounding_square_size - bounding_square.size().y()) / 2;
|
||||||
|
|
||||||
|
// Intersection - limit the search box to a square area around the start and end, to fasten the path searching
|
||||||
|
search_box.max = search_box.max.cwiseMin(bounding_square.max);
|
||||||
|
search_box.min = search_box.min.cwiseMax(bounding_square.min);
|
||||||
|
|
||||||
|
auto cell_query = [&](Pixel pixel) {
|
||||||
|
return search_box.contains(pixel) && (pixel == start || pixel == end || inpassable.find(pixel) == inpassable.end());
|
||||||
|
};
|
||||||
|
|
||||||
|
JPSTracer<Pixel, decltype(cell_query)> tracer(end, cell_query);
|
||||||
|
using QNode = astar::QNode<JPSTracer<Pixel, decltype(cell_query)>>;
|
||||||
|
|
||||||
|
std::unordered_map<size_t, QNode> astar_cache{};
|
||||||
|
std::vector<Pixel> out_path;
|
||||||
|
std::vector<decltype(tracer)::Node> out_nodes;
|
||||||
|
|
||||||
|
if (!astar::search_route(tracer, {start, {0, 0}}, std::back_inserter(out_nodes), astar_cache)) {
|
||||||
|
// path not found - just reconstruct the best path from astar cache.
|
||||||
|
// Note that astar_cache is NOT empty - at least the starting point should always be there
|
||||||
|
auto coordiante_func = [&astar_cache](size_t idx, size_t dim) { return float(astar_cache[idx].node.position[dim]); };
|
||||||
|
std::vector<size_t> keys;
|
||||||
|
keys.reserve(astar_cache.size());
|
||||||
|
for (const auto &pair : astar_cache) { keys.push_back(pair.first); }
|
||||||
|
KDTreeIndirect<2, float, decltype(coordiante_func)> kd_tree(coordiante_func, keys);
|
||||||
|
size_t closest_qnode = find_closest_point(kd_tree, end.cast<float>());
|
||||||
|
|
||||||
|
out_path.push_back(end);
|
||||||
|
while (closest_qnode != astar::Unassigned) {
|
||||||
|
out_path.push_back(astar_cache[closest_qnode].node.position);
|
||||||
|
closest_qnode = astar_cache[closest_qnode].parent;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
for (const auto& node : out_nodes) {
|
||||||
|
out_path.push_back(node.position);
|
||||||
|
}
|
||||||
|
out_path.push_back(start);
|
||||||
|
}
|
||||||
|
|
||||||
|
#ifdef DEBUG_FILES
|
||||||
|
auto scaled_points = [](const Points &ps) {
|
||||||
|
Points r;
|
||||||
|
for (const Point &p : ps) { r.push_back(Point::new_scale(p.x(), p.y())); }
|
||||||
|
return r;
|
||||||
|
};
|
||||||
|
auto scaled_point = [](const Point &p) { return Point::new_scale(p.x(), p.y()); };
|
||||||
|
::Slic3r::SVG svg(debug_out_path(("path_jps" + std::to_string(print_z) + "_" + std::to_string(rand() % 1000)).c_str()).c_str(),
|
||||||
|
BoundingBox(scaled_point(search_box.min), scaled_point(search_box.max)));
|
||||||
|
for (const auto &p : inpassable) { svg.draw(scaled_point(p), "black", scale_(0.4)); }
|
||||||
|
for (const auto &qn : astar_cache) { svg.draw(scaled_point(qn.second.node.position), "blue", scale_(0.3)); }
|
||||||
|
svg.draw(Polyline(scaled_points(out_path)), "yellow", scale_(0.25));
|
||||||
|
svg.draw(scaled_point(end), "purple", scale_(0.4));
|
||||||
|
svg.draw(scaled_point(start), "green", scale_(0.4));
|
||||||
|
#endif
|
||||||
|
|
||||||
|
std::vector<Pixel> tmp_path;
|
||||||
|
tmp_path.reserve(out_path.size());
|
||||||
|
// Some path found, reverse and remove points that do not change direction
|
||||||
|
std::reverse(out_path.begin(), out_path.end());
|
||||||
|
{
|
||||||
|
tmp_path.push_back(out_path.front()); // first point
|
||||||
|
for (size_t i = 1; i < out_path.size() - 1; i++) {
|
||||||
|
if ((out_path[i] - out_path[i - 1]).cast<float>().normalized() != (out_path[i + 1] - out_path[i]).cast<float>().normalized()) {
|
||||||
|
tmp_path.push_back(out_path[i]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
tmp_path.push_back(out_path.back()); // last_point
|
||||||
|
out_path = tmp_path;
|
||||||
|
}
|
||||||
|
|
||||||
|
#ifdef DEBUG_FILES
|
||||||
|
svg.draw(Polyline(scaled_points(out_path)), "orange", scale_(0.20));
|
||||||
|
#endif
|
||||||
|
|
||||||
|
tmp_path.clear();
|
||||||
|
// remove redundant jump points - there are points that change direction but are not needed - this inefficiency arises from the
|
||||||
|
// usage of grid search The removal alg tries to find the longest Px Px+k path without obstacles. If Px Px+k+1 is blocked, it will
|
||||||
|
// insert the Px+k point to result and continue search from Px+k
|
||||||
|
{
|
||||||
|
tmp_path.push_back(out_path.front()); // first point
|
||||||
|
size_t index_of_last_stored_point = 0;
|
||||||
|
for (size_t i = 1; i < out_path.size(); i++) {
|
||||||
|
if (i - index_of_last_stored_point < 2) continue;
|
||||||
|
bool passable = true;
|
||||||
|
auto store_obstacle = [&](coord_t x, coord_t y) {
|
||||||
|
if (Pixel(x, y) != start && Pixel(x, y) != end && inpassable.find(Pixel(x, y)) != inpassable.end()) { passable = false; };
|
||||||
|
};
|
||||||
|
dda(tmp_path.back().x(), tmp_path.back().y(), out_path[i].x(), out_path[i].y(), store_obstacle);
|
||||||
|
if (!passable) {
|
||||||
|
tmp_path.push_back(out_path[i - 1]);
|
||||||
|
index_of_last_stored_point = i - 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
tmp_path.push_back(out_path.back()); // last_point
|
||||||
|
out_path = tmp_path;
|
||||||
|
}
|
||||||
|
|
||||||
|
#ifdef DEBUG_FILES
|
||||||
|
svg.draw(Polyline(scaled_points(out_path)), "red", scale_(0.15));
|
||||||
|
svg.Close();
|
||||||
|
#endif
|
||||||
|
|
||||||
|
// before returing the path, transform it from pixels back to points.
|
||||||
|
// Also replace the first and last pixel by input points so that result path patches input params exactly.
|
||||||
|
for (Pixel &p : out_path) { p = unpixelize(p); }
|
||||||
|
out_path.front() = p0;
|
||||||
|
out_path.back() = p1;
|
||||||
|
|
||||||
|
return Polyline(out_path);
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace Slic3r
|
36
src/libslic3r/JumpPointSearch.hpp
Normal file
36
src/libslic3r/JumpPointSearch.hpp
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
#ifndef SRC_LIBSLIC3R_JUMPPOINTSEARCH_HPP_
|
||||||
|
#define SRC_LIBSLIC3R_JUMPPOINTSEARCH_HPP_
|
||||||
|
|
||||||
|
#include "BoundingBox.hpp"
|
||||||
|
#include "libslic3r/Layer.hpp"
|
||||||
|
#include "libslic3r/Point.hpp"
|
||||||
|
#include "libslic3r/Polyline.hpp"
|
||||||
|
#include "libslic3r/libslic3r.h"
|
||||||
|
#include <unordered_map>
|
||||||
|
#include <unordered_set>
|
||||||
|
|
||||||
|
namespace Slic3r {
|
||||||
|
|
||||||
|
class JPSPathFinder
|
||||||
|
{
|
||||||
|
using Pixel = Point;
|
||||||
|
std::unordered_set<Pixel, PointHash> inpassable;
|
||||||
|
coordf_t print_z;
|
||||||
|
Pixel obstacle_min;
|
||||||
|
Pixel obstacle_max;
|
||||||
|
|
||||||
|
const coord_t resolution = scaled(1.5);
|
||||||
|
Pixel pixelize(const Point &p) { return p / resolution; }
|
||||||
|
Point unpixelize(const Pixel &p) { return p * resolution; }
|
||||||
|
|
||||||
|
public:
|
||||||
|
JPSPathFinder() { clear(); };
|
||||||
|
void clear();
|
||||||
|
void add_obstacles(const Lines &obstacles);
|
||||||
|
void add_obstacles(const Layer* layer, const Point& global_origin);
|
||||||
|
Polyline find_path(const Point &start, const Point &end);
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace Slic3r
|
||||||
|
|
||||||
|
#endif /* SRC_LIBSLIC3R_JUMPPOINTSEARCH_HPP_ */
|
@ -123,6 +123,9 @@ public:
|
|||||||
coordf_t height; // layer height in unscaled coordinates
|
coordf_t height; // layer height in unscaled coordinates
|
||||||
coordf_t bottom_z() const { return this->print_z - this->height; }
|
coordf_t bottom_z() const { return this->print_z - this->height; }
|
||||||
|
|
||||||
|
//Lines estimated to be seriously malformed, info from the IssueSearch algorithm. These lines should probably be avoided during fast travels.
|
||||||
|
Lines malformed_lines;
|
||||||
|
|
||||||
// Collection of expolygons generated by slicing the possibly multiple meshes of the source geometry
|
// Collection of expolygons generated by slicing the possibly multiple meshes of the source geometry
|
||||||
// (with possibly differing extruder ID and slicing parameters) and merged.
|
// (with possibly differing extruder ID and slicing parameters) and merged.
|
||||||
// For the first layer, if the Elephant foot compensation is applied, this lslice is uncompensated, therefore
|
// For the first layer, if the Elephant foot compensation is applied, this lslice is uncompensated, therefore
|
||||||
|
@ -4,6 +4,7 @@
|
|||||||
|
|
||||||
#include "libslic3r/Geometry/Circle.hpp"
|
#include "libslic3r/Geometry/Circle.hpp"
|
||||||
#include "libslic3r/SurfaceMesh.hpp"
|
#include "libslic3r/SurfaceMesh.hpp"
|
||||||
|
#include <numeric>
|
||||||
|
|
||||||
namespace Slic3r {
|
namespace Slic3r {
|
||||||
namespace Measure {
|
namespace Measure {
|
||||||
|
@ -420,7 +420,7 @@ void Preset::set_visible_from_appconfig(const AppConfig &app_config)
|
|||||||
static std::vector<std::string> s_Preset_print_options {
|
static std::vector<std::string> s_Preset_print_options {
|
||||||
"layer_height", "first_layer_height", "perimeters", "spiral_vase", "slice_closing_radius", "slicing_mode",
|
"layer_height", "first_layer_height", "perimeters", "spiral_vase", "slice_closing_radius", "slicing_mode",
|
||||||
"top_solid_layers", "top_solid_min_thickness", "bottom_solid_layers", "bottom_solid_min_thickness",
|
"top_solid_layers", "top_solid_min_thickness", "bottom_solid_layers", "bottom_solid_min_thickness",
|
||||||
"extra_perimeters","extra_perimeters_on_overhangs", "ensure_vertical_shell_thickness", "avoid_crossing_perimeters", "thin_walls", "overhangs",
|
"extra_perimeters", "extra_perimeters_on_overhangs", "ensure_vertical_shell_thickness", "avoid_curled_filament_during_travels", "avoid_crossing_perimeters", "thin_walls", "overhangs",
|
||||||
"seam_position","staggered_inner_seams", "external_perimeters_first", "fill_density", "fill_pattern", "top_fill_pattern", "bottom_fill_pattern",
|
"seam_position","staggered_inner_seams", "external_perimeters_first", "fill_density", "fill_pattern", "top_fill_pattern", "bottom_fill_pattern",
|
||||||
"infill_every_layers", "infill_only_where_needed", "solid_infill_every_layers", "fill_angle", "bridge_angle",
|
"infill_every_layers", "infill_only_where_needed", "solid_infill_every_layers", "fill_angle", "bridge_angle",
|
||||||
"solid_infill_below_area", "only_retract_when_crossing_perimeters", "infill_first",
|
"solid_infill_below_area", "only_retract_when_crossing_perimeters", "infill_first",
|
||||||
|
@ -58,6 +58,7 @@ bool Print::invalidate_state_by_config_options(const ConfigOptionResolver & /* n
|
|||||||
// Cache the plenty of parameters, which influence the G-code generator only,
|
// Cache the plenty of parameters, which influence the G-code generator only,
|
||||||
// or they are only notes not influencing the generated G-code.
|
// or they are only notes not influencing the generated G-code.
|
||||||
static std::unordered_set<std::string> steps_gcode = {
|
static std::unordered_set<std::string> steps_gcode = {
|
||||||
|
"avoid_curled_filament_during_travels",
|
||||||
"avoid_crossing_perimeters",
|
"avoid_crossing_perimeters",
|
||||||
"avoid_crossing_perimeters_max_detour",
|
"avoid_crossing_perimeters_max_detour",
|
||||||
"bed_shape",
|
"bed_shape",
|
||||||
@ -829,6 +830,8 @@ void Print::process()
|
|||||||
obj->generate_support_spots();
|
obj->generate_support_spots();
|
||||||
for (PrintObject *obj : m_objects)
|
for (PrintObject *obj : m_objects)
|
||||||
obj->generate_support_material();
|
obj->generate_support_material();
|
||||||
|
for (PrintObject *obj : m_objects)
|
||||||
|
obj->estimate_curled_extrusions();
|
||||||
if (this->set_started(psWipeTower)) {
|
if (this->set_started(psWipeTower)) {
|
||||||
m_wipe_tower_data.clear();
|
m_wipe_tower_data.clear();
|
||||||
m_tool_ordering.clear();
|
m_tool_ordering.clear();
|
||||||
|
@ -62,7 +62,7 @@ enum PrintStep : unsigned int {
|
|||||||
|
|
||||||
enum PrintObjectStep : unsigned int {
|
enum PrintObjectStep : unsigned int {
|
||||||
posSlice, posPerimeters, posPrepareInfill,
|
posSlice, posPerimeters, posPrepareInfill,
|
||||||
posInfill, posIroning, posSupportSpotsSearch, posSupportMaterial, posCount,
|
posInfill, posIroning, posSupportSpotsSearch, posSupportMaterial, posEstimateCurledExtrusions, posCount,
|
||||||
};
|
};
|
||||||
|
|
||||||
// A PrintRegion object represents a group of volumes to print
|
// A PrintRegion object represents a group of volumes to print
|
||||||
@ -358,6 +358,7 @@ private:
|
|||||||
void ironing();
|
void ironing();
|
||||||
void generate_support_spots();
|
void generate_support_spots();
|
||||||
void generate_support_material();
|
void generate_support_material();
|
||||||
|
void estimate_curled_extrusions();
|
||||||
|
|
||||||
void slice_volumes();
|
void slice_volumes();
|
||||||
// Has any support (not counting the raft).
|
// Has any support (not counting the raft).
|
||||||
|
@ -399,6 +399,13 @@ void PrintConfigDef::init_fff_params()
|
|||||||
|
|
||||||
// Maximum extruder temperature, bumped to 1500 to support printing of glass.
|
// Maximum extruder temperature, bumped to 1500 to support printing of glass.
|
||||||
const int max_temp = 1500;
|
const int max_temp = 1500;
|
||||||
|
def = this->add("avoid_curled_filament_during_travels", coBool);
|
||||||
|
def->label = L("Avoid curled filament during travels");
|
||||||
|
def->tooltip = L("Plan travel moves such that the extruder avoids areas where filament may be curled up. "
|
||||||
|
"This is mostly happening on steeper rounded overhangs and may cause crash or borken print. "
|
||||||
|
"This feature slows down both the print and the G-code generation.");
|
||||||
|
def->mode = comExpert;
|
||||||
|
def->set_default_value(new ConfigOptionBool(false));
|
||||||
|
|
||||||
def = this->add("avoid_crossing_perimeters", coBool);
|
def = this->add("avoid_crossing_perimeters", coBool);
|
||||||
def->label = L("Avoid crossing perimeters");
|
def->label = L("Avoid crossing perimeters");
|
||||||
|
@ -729,6 +729,7 @@ PRINT_CONFIG_CLASS_DERIVED_DEFINE(
|
|||||||
PrintConfig,
|
PrintConfig,
|
||||||
(MachineEnvelopeConfig, GCodeConfig),
|
(MachineEnvelopeConfig, GCodeConfig),
|
||||||
|
|
||||||
|
((ConfigOptionBool, avoid_curled_filament_during_travels))
|
||||||
((ConfigOptionBool, avoid_crossing_perimeters))
|
((ConfigOptionBool, avoid_crossing_perimeters))
|
||||||
((ConfigOptionFloatOrPercent, avoid_crossing_perimeters_max_detour))
|
((ConfigOptionFloatOrPercent, avoid_crossing_perimeters_max_detour))
|
||||||
((ConfigOptionPoints, bed_shape))
|
((ConfigOptionPoints, bed_shape))
|
||||||
|
@ -423,7 +423,8 @@ void PrintObject::generate_support_spots()
|
|||||||
[](const ModelVolume* mv){return mv->supported_facets.empty();})
|
[](const ModelVolume* mv){return mv->supported_facets.empty();})
|
||||||
) {
|
) {
|
||||||
SupportSpotsGenerator::Params params{this->print()->m_config.filament_type.values};
|
SupportSpotsGenerator::Params params{this->print()->m_config.filament_type.values};
|
||||||
SupportSpotsGenerator::Issues issues = SupportSpotsGenerator::full_search(this, params);
|
auto [issues, malformations] = SupportSpotsGenerator::full_search(this, params);
|
||||||
|
|
||||||
auto obj_transform = this->trafo_centered();
|
auto obj_transform = this->trafo_centered();
|
||||||
for (ModelVolume *model_volume : this->model_object()->volumes) {
|
for (ModelVolume *model_volume : this->model_object()->volumes) {
|
||||||
if (model_volume->is_model_part()) {
|
if (model_volume->is_model_part()) {
|
||||||
@ -477,6 +478,26 @@ void PrintObject::generate_support_material()
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void PrintObject::estimate_curled_extrusions()
|
||||||
|
{
|
||||||
|
if (this->set_started(posEstimateCurledExtrusions)) {
|
||||||
|
if (this->print()->config().avoid_curled_filament_during_travels) {
|
||||||
|
BOOST_LOG_TRIVIAL(debug) << "Estimating areas with curled extrusions - start";
|
||||||
|
m_print->set_status(88, L("Estimating curled extrusions"));
|
||||||
|
|
||||||
|
// Estimate curling of support material and add it to the malformaition lines of each layer
|
||||||
|
float support_flow_width = support_material_flow(this, this->config().layer_height).width();
|
||||||
|
SupportSpotsGenerator::Params params{this->print()->m_config.filament_type.values};
|
||||||
|
SupportSpotsGenerator::estimate_supports_malformations(this->support_layers(), support_flow_width, params);
|
||||||
|
SupportSpotsGenerator::estimate_malformations(this->layers(), params);
|
||||||
|
|
||||||
|
m_print->throw_if_canceled();
|
||||||
|
BOOST_LOG_TRIVIAL(debug) << "Estimating areas with curled extrusions - end";
|
||||||
|
}
|
||||||
|
this->set_done(posEstimateCurledExtrusions);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
std::pair<FillAdaptive::OctreePtr, FillAdaptive::OctreePtr> PrintObject::prepare_adaptive_infill_data()
|
std::pair<FillAdaptive::OctreePtr, FillAdaptive::OctreePtr> PrintObject::prepare_adaptive_infill_data()
|
||||||
{
|
{
|
||||||
using namespace FillAdaptive;
|
using namespace FillAdaptive;
|
||||||
@ -785,7 +806,7 @@ bool PrintObject::invalidate_step(PrintObjectStep step)
|
|||||||
|
|
||||||
// propagate to dependent steps
|
// propagate to dependent steps
|
||||||
if (step == posPerimeters) {
|
if (step == posPerimeters) {
|
||||||
invalidated |= this->invalidate_steps({ posPrepareInfill, posInfill, posIroning });
|
invalidated |= this->invalidate_steps({ posPrepareInfill, posInfill, posIroning, posEstimateCurledExtrusions });
|
||||||
invalidated |= m_print->invalidate_steps({ psSkirtBrim });
|
invalidated |= m_print->invalidate_steps({ psSkirtBrim });
|
||||||
} else if (step == posPrepareInfill) {
|
} else if (step == posPrepareInfill) {
|
||||||
invalidated |= this->invalidate_steps({ posInfill, posIroning });
|
invalidated |= this->invalidate_steps({ posInfill, posIroning });
|
||||||
@ -793,11 +814,12 @@ bool PrintObject::invalidate_step(PrintObjectStep step)
|
|||||||
invalidated |= this->invalidate_steps({ posIroning });
|
invalidated |= this->invalidate_steps({ posIroning });
|
||||||
invalidated |= m_print->invalidate_steps({ psSkirtBrim });
|
invalidated |= m_print->invalidate_steps({ psSkirtBrim });
|
||||||
} else if (step == posSlice) {
|
} else if (step == posSlice) {
|
||||||
invalidated |= this->invalidate_steps({ posPerimeters, posPrepareInfill, posInfill, posIroning, posSupportMaterial });
|
invalidated |= this->invalidate_steps({ posPerimeters, posPrepareInfill, posInfill, posIroning, posSupportMaterial, posEstimateCurledExtrusions });
|
||||||
invalidated |= m_print->invalidate_steps({ psSkirtBrim });
|
invalidated |= m_print->invalidate_steps({ psSkirtBrim });
|
||||||
m_slicing_params.valid = false;
|
m_slicing_params.valid = false;
|
||||||
} else if (step == posSupportMaterial) {
|
} else if (step == posSupportMaterial) {
|
||||||
invalidated |= m_print->invalidate_steps({ psSkirtBrim });
|
invalidated |= m_print->invalidate_steps({ psSkirtBrim, });
|
||||||
|
invalidated |= this->invalidate_steps({ posEstimateCurledExtrusions });
|
||||||
m_slicing_params.valid = false;
|
m_slicing_params.valid = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -2,14 +2,20 @@
|
|||||||
|
|
||||||
#include "ExPolygon.hpp"
|
#include "ExPolygon.hpp"
|
||||||
#include "ExtrusionEntity.hpp"
|
#include "ExtrusionEntity.hpp"
|
||||||
|
#include "ExtrusionEntityCollection.hpp"
|
||||||
#include "Line.hpp"
|
#include "Line.hpp"
|
||||||
|
#include "Point.hpp"
|
||||||
#include "Polygon.hpp"
|
#include "Polygon.hpp"
|
||||||
|
#include "libslic3r.h"
|
||||||
#include "tbb/parallel_for.h"
|
#include "tbb/parallel_for.h"
|
||||||
#include "tbb/blocked_range.h"
|
#include "tbb/blocked_range.h"
|
||||||
#include "tbb/blocked_range2d.h"
|
#include "tbb/blocked_range2d.h"
|
||||||
#include "tbb/parallel_reduce.h"
|
#include "tbb/parallel_reduce.h"
|
||||||
#include <boost/log/trivial.hpp>
|
#include <boost/log/trivial.hpp>
|
||||||
#include <cmath>
|
#include <cmath>
|
||||||
|
#include <cstdio>
|
||||||
|
#include <functional>
|
||||||
|
#include <unordered_map>
|
||||||
#include <unordered_set>
|
#include <unordered_set>
|
||||||
#include <stack>
|
#include <stack>
|
||||||
|
|
||||||
@ -333,7 +339,7 @@ std::vector<ExtrusionLine> to_short_lines(const ExtrusionEntity *e, float length
|
|||||||
std::vector<ExtrusionLine> lines;
|
std::vector<ExtrusionLine> lines;
|
||||||
lines.reserve(pl.points.size() * 1.5f);
|
lines.reserve(pl.points.size() * 1.5f);
|
||||||
lines.emplace_back(unscaled(pl.points[0]).cast<float>(), unscaled(pl.points[0]).cast<float>(), e);
|
lines.emplace_back(unscaled(pl.points[0]).cast<float>(), unscaled(pl.points[0]).cast<float>(), e);
|
||||||
for (int point_idx = 0; point_idx < int(pl.points.size() - 1); ++point_idx) {
|
for (int point_idx = 0; point_idx < int(pl.points.size()) - 1; ++point_idx) {
|
||||||
Vec2f start = unscaled(pl.points[point_idx]).cast<float>();
|
Vec2f start = unscaled(pl.points[point_idx]).cast<float>();
|
||||||
Vec2f next = unscaled(pl.points[point_idx + 1]).cast<float>();
|
Vec2f next = unscaled(pl.points[point_idx + 1]).cast<float>();
|
||||||
Vec2f v = next - start; // vector from next to current
|
Vec2f v = next - start; // vector from next to current
|
||||||
@ -367,12 +373,6 @@ void check_extrusion_entity_stability(const ExtrusionEntity *entity,
|
|||||||
const auto to_vec3f = [layer_z](const Vec2f &point) {
|
const auto to_vec3f = [layer_z](const Vec2f &point) {
|
||||||
return Vec3f(point.x(), point.y(), layer_z);
|
return Vec3f(point.x(), point.y(), layer_z);
|
||||||
};
|
};
|
||||||
float overhang_dist = tan(params.overhang_angle_deg * PI / 180.0f) * layer_region->layer()->height;
|
|
||||||
float min_malformation_dist = tan(params.malformation_angle_span_deg.first * PI / 180.0f)
|
|
||||||
* layer_region->layer()->height;
|
|
||||||
float max_malformation_dist = tan(params.malformation_angle_span_deg.second * PI / 180.0f)
|
|
||||||
* layer_region->layer()->height;
|
|
||||||
|
|
||||||
std::vector<ExtrusionLine> lines = to_short_lines(entity, params.bridge_distance);
|
std::vector<ExtrusionLine> lines = to_short_lines(entity, params.bridge_distance);
|
||||||
if (lines.empty()) return;
|
if (lines.empty()) return;
|
||||||
|
|
||||||
@ -380,6 +380,9 @@ void check_extrusion_entity_stability(const ExtrusionEntity *entity,
|
|||||||
ExtrusionPropertiesAccumulator malformation_acc { };
|
ExtrusionPropertiesAccumulator malformation_acc { };
|
||||||
bridging_acc.add_distance(params.bridge_distance + 1.0f);
|
bridging_acc.add_distance(params.bridge_distance + 1.0f);
|
||||||
const float flow_width = get_flow_width(layer_region, entity->role());
|
const float flow_width = get_flow_width(layer_region, entity->role());
|
||||||
|
float min_malformation_dist = flow_width - params.malformation_overlap_factor.first * flow_width;
|
||||||
|
float max_malformation_dist = flow_width - params.malformation_overlap_factor.second * flow_width;
|
||||||
|
|
||||||
|
|
||||||
for (size_t line_idx = 0; line_idx < lines.size(); ++line_idx) {
|
for (size_t line_idx = 0; line_idx < lines.size(); ++line_idx) {
|
||||||
ExtrusionLine ¤t_line = lines[line_idx];
|
ExtrusionLine ¤t_line = lines[line_idx];
|
||||||
@ -395,9 +398,12 @@ void check_extrusion_entity_stability(const ExtrusionEntity *entity,
|
|||||||
bridging_acc.add_angle(curr_angle);
|
bridging_acc.add_angle(curr_angle);
|
||||||
// malformation in concave angles does not happen
|
// malformation in concave angles does not happen
|
||||||
malformation_acc.add_angle(std::max(0.0f, curr_angle));
|
malformation_acc.add_angle(std::max(0.0f, curr_angle));
|
||||||
|
if (curr_angle < -20.0 * PI / 180.0) {
|
||||||
|
malformation_acc.reset();
|
||||||
|
}
|
||||||
|
|
||||||
auto [dist_from_prev_layer, nearest_line_idx, nearest_point] = prev_layer_lines.signed_distance_from_lines_extra(current_line.b);
|
auto [dist_from_prev_layer, nearest_line_idx, nearest_point] = prev_layer_lines.signed_distance_from_lines_extra(current_line.b);
|
||||||
if (fabs(dist_from_prev_layer) < overhang_dist) {
|
if (fabs(dist_from_prev_layer) < flow_width) {
|
||||||
bridging_acc.reset();
|
bridging_acc.reset();
|
||||||
} else {
|
} else {
|
||||||
bridging_acc.add_distance(current_line.len);
|
bridging_acc.add_distance(current_line.len);
|
||||||
@ -416,15 +422,16 @@ void check_extrusion_entity_stability(const ExtrusionEntity *entity,
|
|||||||
}
|
}
|
||||||
|
|
||||||
//malformation
|
//malformation
|
||||||
if (fabs(dist_from_prev_layer) < 3.0f * flow_width) {
|
if (fabs(dist_from_prev_layer) < 2.0f * flow_width) {
|
||||||
const ExtrusionLine &nearest_line = prev_layer_lines.get_line(nearest_line_idx);
|
const ExtrusionLine &nearest_line = prev_layer_lines.get_line(nearest_line_idx);
|
||||||
current_line.malformation += 0.9 * nearest_line.malformation;
|
current_line.malformation += 0.85 * nearest_line.malformation;
|
||||||
}
|
}
|
||||||
if (dist_from_prev_layer > min_malformation_dist && dist_from_prev_layer < max_malformation_dist) {
|
if (dist_from_prev_layer > min_malformation_dist && dist_from_prev_layer < max_malformation_dist) {
|
||||||
|
float factor = std::abs(dist_from_prev_layer - (max_malformation_dist + min_malformation_dist) * 0.5) /
|
||||||
|
(max_malformation_dist - min_malformation_dist);
|
||||||
malformation_acc.add_distance(current_line.len);
|
malformation_acc.add_distance(current_line.len);
|
||||||
current_line.malformation += layer_region->layer()->height *
|
current_line.malformation += layer_region->layer()->height * factor * (2.0f + 3.0f * (malformation_acc.max_curvature / PI));
|
||||||
(0.5f + 1.5f * (malformation_acc.max_curvature / PI) *
|
current_line.malformation = std::min(current_line.malformation, float(layer_region->layer()->height * params.max_malformation_factor));
|
||||||
gauss(malformation_acc.distance, 5.0f, 1.0f, 0.2f));
|
|
||||||
} else {
|
} else {
|
||||||
malformation_acc.reset();
|
malformation_acc.reset();
|
||||||
}
|
}
|
||||||
@ -1023,7 +1030,7 @@ Issues check_global_stability(SupportGridFilter supports_presence_grid,
|
|||||||
return issues;
|
return issues;
|
||||||
}
|
}
|
||||||
|
|
||||||
std::tuple<Issues, std::vector<LayerIslands>> check_extrusions_and_build_graph(const PrintObject *po,
|
std::tuple<Issues, Malformations, std::vector<LayerIslands>> check_extrusions_and_build_graph(const PrintObject *po,
|
||||||
const Params ¶ms) {
|
const Params ¶ms) {
|
||||||
#ifdef DEBUG_FILES
|
#ifdef DEBUG_FILES
|
||||||
FILE *segmentation_f = boost::nowide::fopen(debug_out_path("segmentation.obj").c_str(), "w");
|
FILE *segmentation_f = boost::nowide::fopen(debug_out_path("segmentation.obj").c_str(), "w");
|
||||||
@ -1031,6 +1038,7 @@ std::tuple<Issues, std::vector<LayerIslands>> check_extrusions_and_build_graph(c
|
|||||||
#endif
|
#endif
|
||||||
|
|
||||||
Issues issues { };
|
Issues issues { };
|
||||||
|
Malformations malformations{};
|
||||||
std::vector<LayerIslands> islands_graph;
|
std::vector<LayerIslands> islands_graph;
|
||||||
std::vector<ExtrusionLine> layer_lines;
|
std::vector<ExtrusionLine> layer_lines;
|
||||||
float flow_width = get_flow_width(po->layers()[po->layer_count() - 1]->regions()[0], erExternalPerimeter);
|
float flow_width = get_flow_width(po->layers()[po->layer_count() - 1]->regions()[0], erExternalPerimeter);
|
||||||
@ -1038,6 +1046,7 @@ std::tuple<Issues, std::vector<LayerIslands>> check_extrusions_and_build_graph(c
|
|||||||
|
|
||||||
// PREPARE BASE LAYER
|
// PREPARE BASE LAYER
|
||||||
const Layer *layer = po->layers()[0];
|
const Layer *layer = po->layers()[0];
|
||||||
|
malformations.layers.push_back({}); // no malformations to be expected at first layer
|
||||||
for (const LayerRegion *layer_region : layer->regions()) {
|
for (const LayerRegion *layer_region : layer->regions()) {
|
||||||
for (const ExtrusionEntity *ex_entity : layer_region->perimeters.entities) {
|
for (const ExtrusionEntity *ex_entity : layer_region->perimeters.entities) {
|
||||||
for (const ExtrusionEntity *perimeter : static_cast<const ExtrusionEntityCollection*>(ex_entity)->entities) {
|
for (const ExtrusionEntity *perimeter : static_cast<const ExtrusionEntityCollection*>(ex_entity)->entities) {
|
||||||
@ -1106,6 +1115,12 @@ std::tuple<Issues, std::vector<LayerIslands>> check_extrusions_and_build_graph(c
|
|||||||
layer_lines, params);
|
layer_lines, params);
|
||||||
islands_graph.push_back(std::move(layer_islands));
|
islands_graph.push_back(std::move(layer_islands));
|
||||||
|
|
||||||
|
Lines malformed_lines{};
|
||||||
|
for (const auto &line : layer_lines) {
|
||||||
|
if (line.malformation > 0.3f) { malformed_lines.push_back(Line{Point::new_scale(line.a), Point::new_scale(line.b)}); }
|
||||||
|
}
|
||||||
|
malformations.layers.push_back(malformed_lines);
|
||||||
|
|
||||||
#ifdef DEBUG_FILES
|
#ifdef DEBUG_FILES
|
||||||
for (size_t x = 0; x < size_t(layer_grid.get_pixel_count().x()); ++x) {
|
for (size_t x = 0; x < size_t(layer_grid.get_pixel_count().x()); ++x) {
|
||||||
for (size_t y = 0; y < size_t(layer_grid.get_pixel_count().y()); ++y) {
|
for (size_t y = 0; y < size_t(layer_grid.get_pixel_count().y()); ++y) {
|
||||||
@ -1122,7 +1137,7 @@ std::tuple<Issues, std::vector<LayerIslands>> check_extrusions_and_build_graph(c
|
|||||||
}
|
}
|
||||||
for (const auto &line : layer_lines) {
|
for (const auto &line : layer_lines) {
|
||||||
if (line.malformation > 0.0f) {
|
if (line.malformation > 0.0f) {
|
||||||
Vec3f color = value_to_rgbf(0, 1.0f, line.malformation);
|
Vec3f color = value_to_rgbf(-EPSILON, layer->height*params.max_malformation_factor, line.malformation);
|
||||||
fprintf(malform_f, "v %f %f %f %f %f %f\n", line.b[0],
|
fprintf(malform_f, "v %f %f %f %f %f %f\n", line.b[0],
|
||||||
line.b[1], layer->slice_z, color[0], color[1], color[2]);
|
line.b[1], layer->slice_z, color[0], color[1], color[2]);
|
||||||
}
|
}
|
||||||
@ -1138,7 +1153,7 @@ std::tuple<Issues, std::vector<LayerIslands>> check_extrusions_and_build_graph(c
|
|||||||
fclose(malform_f);
|
fclose(malform_f);
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
return {issues, islands_graph};
|
return {issues, malformations, islands_graph};
|
||||||
}
|
}
|
||||||
|
|
||||||
#ifdef DEBUG_FILES
|
#ifdef DEBUG_FILES
|
||||||
@ -1167,8 +1182,8 @@ void debug_export(Issues issues, std::string file_name) {
|
|||||||
// return {};
|
// return {};
|
||||||
// }
|
// }
|
||||||
|
|
||||||
Issues full_search(const PrintObject *po, const Params ¶ms) {
|
std::tuple<Issues, Malformations> full_search(const PrintObject *po, const Params ¶ms) {
|
||||||
auto [local_issues, graph] = check_extrusions_and_build_graph(po, params);
|
auto [local_issues, malformations, graph] = check_extrusions_and_build_graph(po, params);
|
||||||
Issues global_issues = check_global_stability( { po, params.min_distance_between_support_points }, graph, params);
|
Issues global_issues = check_global_stability( { po, params.min_distance_between_support_points }, graph, params);
|
||||||
#ifdef DEBUG_FILES
|
#ifdef DEBUG_FILES
|
||||||
debug_export(local_issues, "local_issues");
|
debug_export(local_issues, "local_issues");
|
||||||
@ -1178,7 +1193,146 @@ Issues full_search(const PrintObject *po, const Params ¶ms) {
|
|||||||
global_issues.support_points.insert(global_issues.support_points.end(),
|
global_issues.support_points.insert(global_issues.support_points.end(),
|
||||||
local_issues.support_points.begin(), local_issues.support_points.end());
|
local_issues.support_points.begin(), local_issues.support_points.end());
|
||||||
|
|
||||||
return global_issues;
|
return {global_issues, malformations};
|
||||||
|
}
|
||||||
|
|
||||||
|
struct LayerCurlingEstimator
|
||||||
|
{
|
||||||
|
LD prev_layer_lines = LD({});
|
||||||
|
Params params;
|
||||||
|
std::function<float(const ExtrusionLine &)> flow_width_getter;
|
||||||
|
|
||||||
|
LayerCurlingEstimator(std::function<float(const ExtrusionLine &)> flow_width_getter, const Params ¶ms)
|
||||||
|
: flow_width_getter(flow_width_getter), params(params)
|
||||||
|
{}
|
||||||
|
|
||||||
|
void estimate_curling(std::vector<ExtrusionLine> &extrusion_lines, Layer *l)
|
||||||
|
{
|
||||||
|
ExtrusionPropertiesAccumulator malformation_acc{};
|
||||||
|
for (size_t line_idx = 0; line_idx < extrusion_lines.size(); ++line_idx) {
|
||||||
|
ExtrusionLine ¤t_line = extrusion_lines[line_idx];
|
||||||
|
|
||||||
|
float flow_width = flow_width_getter(current_line);
|
||||||
|
|
||||||
|
float min_malformation_dist = flow_width - params.malformation_overlap_factor.first * flow_width;
|
||||||
|
float max_malformation_dist = flow_width - params.malformation_overlap_factor.second * flow_width;
|
||||||
|
|
||||||
|
float curr_angle = 0;
|
||||||
|
if (line_idx + 1 < extrusion_lines.size()) {
|
||||||
|
const Vec2f v1 = current_line.b - current_line.a;
|
||||||
|
const Vec2f v2 = extrusion_lines[line_idx + 1].b - extrusion_lines[line_idx + 1].a;
|
||||||
|
curr_angle = angle(v1, v2);
|
||||||
|
}
|
||||||
|
// malformation in concave angles does not happen
|
||||||
|
malformation_acc.add_angle(std::max(0.0f, curr_angle));
|
||||||
|
if (curr_angle < -20.0 * PI / 180.0) { malformation_acc.reset(); }
|
||||||
|
|
||||||
|
auto [dist_from_prev_layer, nearest_line_idx, nearest_point] = prev_layer_lines.signed_distance_from_lines_extra(current_line.b);
|
||||||
|
|
||||||
|
if (fabs(dist_from_prev_layer) < 2.0f * flow_width) {
|
||||||
|
const ExtrusionLine &nearest_line = prev_layer_lines.get_line(nearest_line_idx);
|
||||||
|
current_line.malformation += 0.85 * nearest_line.malformation;
|
||||||
|
}
|
||||||
|
if (dist_from_prev_layer > min_malformation_dist && dist_from_prev_layer < max_malformation_dist) {
|
||||||
|
float factor = std::abs(dist_from_prev_layer - (max_malformation_dist + min_malformation_dist) * 0.5) /
|
||||||
|
(max_malformation_dist - min_malformation_dist);
|
||||||
|
malformation_acc.add_distance(current_line.len);
|
||||||
|
current_line.malformation += l->height * factor * (2.0f + 3.0f * (malformation_acc.max_curvature / PI));
|
||||||
|
current_line.malformation = std::min(current_line.malformation, float(l->height * params.max_malformation_factor));
|
||||||
|
} else {
|
||||||
|
malformation_acc.reset();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const ExtrusionLine &line : extrusion_lines) {
|
||||||
|
if (line.malformation > 0.3f) { l->malformed_lines.push_back(Line{Point::new_scale(line.a), Point::new_scale(line.b)}); }
|
||||||
|
}
|
||||||
|
prev_layer_lines = LD(extrusion_lines);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
void estimate_supports_malformations(SupportLayerPtrs &layers, float supports_flow_width, const Params ¶ms)
|
||||||
|
{
|
||||||
|
#ifdef DEBUG_FILES
|
||||||
|
FILE *debug_file = boost::nowide::fopen(debug_out_path("supports_malformations.obj").c_str(), "w");
|
||||||
|
#endif
|
||||||
|
auto flow_width_getter = [=](const ExtrusionLine& l) {
|
||||||
|
return supports_flow_width;
|
||||||
|
};
|
||||||
|
|
||||||
|
LayerCurlingEstimator lce{flow_width_getter, params};
|
||||||
|
|
||||||
|
for (SupportLayer *l : layers) {
|
||||||
|
std::vector<ExtrusionLine> extrusion_lines;
|
||||||
|
for (const ExtrusionEntity *extrusion : l->support_fills.flatten().entities) {
|
||||||
|
Polyline pl = extrusion->as_polyline();
|
||||||
|
Polygon pol(pl.points);
|
||||||
|
pol.make_counter_clockwise();
|
||||||
|
pl = pol.split_at_first_point();
|
||||||
|
for (int point_idx = 0; point_idx < int(pl.points.size() - 1); ++point_idx) {
|
||||||
|
Vec2f start = unscaled(pl.points[point_idx]).cast<float>();
|
||||||
|
Vec2f next = unscaled(pl.points[point_idx + 1]).cast<float>();
|
||||||
|
ExtrusionLine line{start, next, extrusion};
|
||||||
|
extrusion_lines.push_back(line);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
lce.estimate_curling(extrusion_lines, l);
|
||||||
|
|
||||||
|
#ifdef DEBUG_FILES
|
||||||
|
for (const ExtrusionLine &line : extrusion_lines) {
|
||||||
|
if (line.malformation > 0.3f) {
|
||||||
|
Vec3f color = value_to_rgbf(-EPSILON, l->height * params.max_malformation_factor, line.malformation);
|
||||||
|
fprintf(debug_file, "v %f %f %f %f %f %f\n", line.b[0], line.b[1], l->print_z, color[0], color[1], color[2]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
#ifdef DEBUG_FILES
|
||||||
|
fclose(debug_file);
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
void estimate_malformations(LayerPtrs &layers, const Params ¶ms)
|
||||||
|
{
|
||||||
|
#ifdef DEBUG_FILES
|
||||||
|
FILE *debug_file = boost::nowide::fopen(debug_out_path("object_malformations.obj").c_str(), "w");
|
||||||
|
#endif
|
||||||
|
auto flow_width_getter = [](const ExtrusionLine &l) { return 0.0; };
|
||||||
|
LayerCurlingEstimator lce{flow_width_getter, params};
|
||||||
|
|
||||||
|
for (Layer *l : layers) {
|
||||||
|
if (l->regions().empty()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
std::unordered_map<const ExtrusionEntity*, float> extrusions_widths;
|
||||||
|
std::vector<ExtrusionLine> extrusion_lines;
|
||||||
|
for (const LayerRegion *region : l->regions()) {
|
||||||
|
for (const ExtrusionEntity *extrusion : region->perimeters.flatten().entities) {
|
||||||
|
auto lines = to_short_lines(extrusion, params.bridge_distance);
|
||||||
|
extrusion_lines.insert(extrusion_lines.end(), lines.begin(), lines.end());
|
||||||
|
extrusions_widths.emplace(extrusion, get_flow_width(region, extrusion->role()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
lce.flow_width_getter = [&](const ExtrusionLine &l) { return extrusions_widths[l.origin_entity]; };
|
||||||
|
|
||||||
|
lce.estimate_curling(extrusion_lines, l);
|
||||||
|
|
||||||
|
#ifdef DEBUG_FILES
|
||||||
|
for (const ExtrusionLine &line : extrusion_lines) {
|
||||||
|
if (line.malformation > 0.3f) {
|
||||||
|
Vec3f color = value_to_rgbf(-EPSILON, l->height * params.max_malformation_factor, line.malformation);
|
||||||
|
fprintf(debug_file, "v %f %f %f %f %f %f\n", line.b[0], line.b[1], l->print_z, color[0], color[1], color[2]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
#ifdef DEBUG_FILES
|
||||||
|
fclose(debug_file);
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
} //SupportableIssues End
|
} //SupportableIssues End
|
||||||
|
@ -26,8 +26,8 @@ struct Params {
|
|||||||
// the algorithm should use the following units for all computations: distance [mm], mass [g], time [s], force [g*mm/s^2]
|
// the algorithm should use the following units for all computations: distance [mm], mass [g], time [s], force [g*mm/s^2]
|
||||||
const float bridge_distance = 12.0f; //mm
|
const float bridge_distance = 12.0f; //mm
|
||||||
const float bridge_distance_decrease_by_curvature_factor = 5.0f; // allowed bridge distance = bridge_distance / (1 + this factor * (curvature / PI) )
|
const float bridge_distance_decrease_by_curvature_factor = 5.0f; // allowed bridge distance = bridge_distance / (1 + this factor * (curvature / PI) )
|
||||||
const float overhang_angle_deg = 80.0f;
|
const std::pair<float,float> malformation_overlap_factor = std::pair<float, float> { 0.45, -0.1 };
|
||||||
const std::pair<float,float> malformation_angle_span_deg = std::pair<float, float> { 45.0f, 80.0f };
|
const float max_malformation_factor = 10.0f;
|
||||||
|
|
||||||
const float min_distance_between_support_points = 3.0f; //mm
|
const float min_distance_between_support_points = 3.0f; //mm
|
||||||
const float support_points_interface_radius = 1.5f; // mm
|
const float support_points_interface_radius = 1.5f; // mm
|
||||||
@ -72,11 +72,17 @@ struct Issues {
|
|||||||
std::vector<SupportPoint> support_points;
|
std::vector<SupportPoint> support_points;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
struct Malformations {
|
||||||
|
std::vector<Lines> layers; //for each layer
|
||||||
|
};
|
||||||
|
|
||||||
// std::vector<size_t> quick_search(const PrintObject *po, const Params ¶ms);
|
// std::vector<size_t> quick_search(const PrintObject *po, const Params ¶ms);
|
||||||
Issues full_search(const PrintObject *po, const Params ¶ms);
|
std::tuple<Issues, Malformations> full_search(const PrintObject *po, const Params ¶ms);
|
||||||
|
|
||||||
}
|
void estimate_supports_malformations(SupportLayerPtrs &layers, float supports_flow_width, const Params ¶ms);
|
||||||
|
void estimate_malformations(LayerPtrs &layers, const Params ¶ms);
|
||||||
|
|
||||||
|
} // namespace SupportSpotsGenerator
|
||||||
}
|
}
|
||||||
|
|
||||||
#endif /* SRC_LIBSLIC3R_SUPPORTABLEISSUESSEARCH_HPP_ */
|
#endif /* SRC_LIBSLIC3R_SUPPORTABLEISSUESSEARCH_HPP_ */
|
||||||
|
@ -1438,6 +1438,7 @@ void TabPrint::build()
|
|||||||
optgroup->append_single_option_line("extra_perimeters", category_path + "extra-perimeters-if-needed");
|
optgroup->append_single_option_line("extra_perimeters", category_path + "extra-perimeters-if-needed");
|
||||||
optgroup->append_single_option_line("extra_perimeters_on_overhangs", category_path + "extra-perimeters-on-overhangs");
|
optgroup->append_single_option_line("extra_perimeters_on_overhangs", category_path + "extra-perimeters-on-overhangs");
|
||||||
optgroup->append_single_option_line("ensure_vertical_shell_thickness", category_path + "ensure-vertical-shell-thickness");
|
optgroup->append_single_option_line("ensure_vertical_shell_thickness", category_path + "ensure-vertical-shell-thickness");
|
||||||
|
optgroup->append_single_option_line("avoid_curled_filament_during_travels", category_path + "avoid-curled-filament-during-travels");
|
||||||
optgroup->append_single_option_line("avoid_crossing_perimeters", category_path + "avoid-crossing-perimeters");
|
optgroup->append_single_option_line("avoid_crossing_perimeters", category_path + "avoid-crossing-perimeters");
|
||||||
optgroup->append_single_option_line("avoid_crossing_perimeters_max_detour", category_path + "avoid_crossing_perimeters_max_detour");
|
optgroup->append_single_option_line("avoid_crossing_perimeters_max_detour", category_path + "avoid_crossing_perimeters_max_detour");
|
||||||
optgroup->append_single_option_line("thin_walls", category_path + "detect-thin-walls");
|
optgroup->append_single_option_line("thin_walls", category_path + "detect-thin-walls");
|
||||||
|
@ -30,6 +30,7 @@ add_executable(${_TEST_NAME}_tests
|
|||||||
test_timeutils.cpp
|
test_timeutils.cpp
|
||||||
test_indexed_triangle_set.cpp
|
test_indexed_triangle_set.cpp
|
||||||
test_astar.cpp
|
test_astar.cpp
|
||||||
|
test_jump_point_search.cpp
|
||||||
../libnest2d/printer_parts.cpp
|
../libnest2d/printer_parts.cpp
|
||||||
)
|
)
|
||||||
|
|
||||||
|
35
tests/libslic3r/test_jump_point_search.cpp
Normal file
35
tests/libslic3r/test_jump_point_search.cpp
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
#include <catch2/catch.hpp>
|
||||||
|
#include "libslic3r/BoundingBox.hpp"
|
||||||
|
#include "libslic3r/JumpPointSearch.hpp"
|
||||||
|
|
||||||
|
using namespace Slic3r;
|
||||||
|
|
||||||
|
TEST_CASE("Test jump point search path finding", "[JumpPointSearch]")
|
||||||
|
{
|
||||||
|
Lines obstacles{};
|
||||||
|
obstacles.push_back(Line(Point::new_scale(0, 0), Point::new_scale(50, 50)));
|
||||||
|
obstacles.push_back(Line(Point::new_scale(0, 100), Point::new_scale(50, 50)));
|
||||||
|
obstacles.push_back(Line(Point::new_scale(0, 0), Point::new_scale(100, 0)));
|
||||||
|
obstacles.push_back(Line(Point::new_scale(0, 100), Point::new_scale(100, 100)));
|
||||||
|
obstacles.push_back(Line(Point::new_scale(25, -25), Point::new_scale(25, 125)));
|
||||||
|
|
||||||
|
JPSPathFinder jps;
|
||||||
|
jps.add_obstacles(obstacles);
|
||||||
|
|
||||||
|
Polyline path = jps.find_path(Point::new_scale(5, 50), Point::new_scale(100, 50));
|
||||||
|
path = jps.find_path(Point::new_scale(5, 50), Point::new_scale(150, 50));
|
||||||
|
path = jps.find_path(Point::new_scale(5, 50), Point::new_scale(25, 15));
|
||||||
|
path = jps.find_path(Point::new_scale(25, 25), Point::new_scale(125, 125));
|
||||||
|
|
||||||
|
// SECTION("Output is empty when source is also the destination") {
|
||||||
|
// bool found = astar::search_route(DummyTracer{}, 0, std::back_inserter(out));
|
||||||
|
// REQUIRE(out.empty());
|
||||||
|
// REQUIRE(found);
|
||||||
|
// }
|
||||||
|
|
||||||
|
// SECTION("Return false when there is no route to destination") {
|
||||||
|
// bool found = astar::search_route(DummyTracer{}, 1, std::back_inserter(out));
|
||||||
|
// REQUIRE(!found);
|
||||||
|
// REQUIRE(out.empty());
|
||||||
|
// }
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user