Using aabb tree for lines to detect closest expolygons
This commit is contained in:
parent
8f4b799ddb
commit
a0eecb91c8
@ -87,26 +87,29 @@ inline AABBTreeIndirect::Tree<2, typename LineType::Scalar> build_aabb_tree_over
|
||||
}
|
||||
|
||||
// Finding a closest line, its closest point and squared distance to the closest point
|
||||
// Returns squared distance to the closest point or -1 if the input is empty.
|
||||
template<typename LineType, typename TreeType, typename VectorType>
|
||||
// Returns squared distance to the closest point or -1 if the input is empty
|
||||
// or no closer point than max_sq_dist
|
||||
template<typename LineType, typename TreeType, typename VectorType,
|
||||
typename Scalar = typename VectorType::Scalar>
|
||||
inline typename VectorType::Scalar squared_distance_to_indexed_lines(
|
||||
const std::vector<LineType> &lines,
|
||||
const TreeType &tree,
|
||||
const VectorType &point,
|
||||
size_t &hit_idx_out,
|
||||
Eigen::PlainObjectBase<VectorType> &hit_point_out)
|
||||
{
|
||||
using Scalar = typename VectorType::Scalar;
|
||||
const std::vector<LineType> &lines,
|
||||
const TreeType &tree,
|
||||
const VectorType &point,
|
||||
size_t &hit_idx_out,
|
||||
Eigen::PlainObjectBase<VectorType> &hit_point_out,
|
||||
Scalar max_sqr_dist = std::numeric_limits<Scalar>::infinity())
|
||||
{
|
||||
if (tree.empty()) return Scalar(-1);
|
||||
auto distancer = detail::IndexedLinesDistancer<LineType, TreeType, VectorType>
|
||||
{ lines, tree, point };
|
||||
return tree.empty() ?
|
||||
Scalar(-1) :
|
||||
AABBTreeIndirect::detail::squared_distance_to_indexed_primitives_recursive(distancer, size_t(0), Scalar(0),
|
||||
std::numeric_limits<Scalar>::infinity(), hit_idx_out, hit_point_out);
|
||||
hit_idx_out = std::numeric_limits<size_t>::max();
|
||||
Scalar distance = AABBTreeIndirect::detail::squared_distance_to_indexed_primitives_recursive(
|
||||
distancer, size_t(0), Scalar(0), max_sqr_dist, hit_idx_out, hit_point_out);
|
||||
if (hit_idx_out == std::numeric_limits<size_t>::max()) return Scalar(-1);
|
||||
return distance;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
#endif /* SRC_LIBSLIC3R_AABBTREELINES_HPP_ */
|
||||
|
@ -34,8 +34,6 @@ set(SLIC3R_SOURCES
|
||||
clipper.hpp
|
||||
ClipperUtils.cpp
|
||||
ClipperUtils.hpp
|
||||
ClosestPoint.cpp
|
||||
ClosestPoint.hpp
|
||||
Color.cpp
|
||||
Color.hpp
|
||||
Config.cpp
|
||||
|
@ -1,6 +0,0 @@
|
||||
#include "ClosestPoint.hpp"
|
||||
#include "Point.hpp"
|
||||
|
||||
// Compile template for specific data type
|
||||
template<>
|
||||
size_t Slic3r::find_closest_in_sorted<Slic3r::Point>(const Slic3r::Point &p, const Slic3r::Points &pts);
|
@ -1,252 +0,0 @@
|
||||
#ifndef slic3r_ClosestPoint_hpp_
|
||||
#define slic3r_ClosestPoint_hpp_
|
||||
|
||||
#include <vector>
|
||||
#include <type_traits>
|
||||
|
||||
namespace Slic3r {
|
||||
|
||||
/// <summary>
|
||||
/// Sort points and use find_closest_in_sorted
|
||||
/// </summary>
|
||||
/// <param name="p">Seach for closest index to this point</param>
|
||||
/// <param name="pts">Search inside of thoose points</param>
|
||||
/// <returns>Index of closest point from sorted_pts</returns>
|
||||
template<class P>
|
||||
// /*SFINAE*/ typename std::enable_if<std::is_member_function_pointer_v<decltype(&P::x)>>::type * = nullptr >
|
||||
size_t find_closest(const P &p, const std::vector<P> &pts){
|
||||
// check input
|
||||
if (pts.empty()) return std::numeric_limits<size_t>::max();
|
||||
if (pts.size() == 1) return 0;
|
||||
|
||||
using V = decltype(p.x());
|
||||
// extend P with order
|
||||
struct PP
|
||||
{
|
||||
size_t ord;
|
||||
const P* p;
|
||||
V x() const { return p->x(); }
|
||||
V y() const { return p->y(); }
|
||||
};
|
||||
std::vector<PP> pts_ord;
|
||||
pts_ord.reserve(pts.size());
|
||||
size_t ord = 0;
|
||||
for (const P &pp : pts) pts_ord.push_back({ord++, &pp});
|
||||
std::stable_sort(pts_ord.begin(), pts_ord.end(), closestPoint::sort_fnc<PP>);
|
||||
PP pp{0, &p};
|
||||
size_t closest_index = find_closest_in_sorted(pp, pts_ord);
|
||||
return pts_ord[closest_index].ord;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Use a plane sweep algorithm to find closest point in sorted points
|
||||
/// https://www.cs.mcgill.ca/~cs251/ClosestPair/ClosestPairPS.html
|
||||
/// </summary>
|
||||
/// <param name="p">Seach for closest index to this point</param>
|
||||
/// <param name="sorted_pts">Sorted points by X coordinate</param>
|
||||
/// <returns>Index of closest point from sorted_pts</returns>
|
||||
template<class P>
|
||||
size_t find_closest_in_sorted(const P &p, const std::vector<P> &sorted_pts);
|
||||
|
||||
/// <summary>
|
||||
/// Use a plane sweep algorithm to find closest point from pts in sorted_pts
|
||||
/// https://www.cs.mcgill.ca/~cs251/ClosestPair/ClosestPairPS.html
|
||||
/// </summary>
|
||||
/// <param name="pts">Seach for closest index from thoose points</param>
|
||||
/// <param name="sorted_pts">Seach for closest index to thoose points
|
||||
/// Sorted points by X coordinate(by function closestPoint::sort_fnc)</param>
|
||||
/// <returns>Indices to pts(first) and sorted_pts(second)</returns>
|
||||
template<class P>
|
||||
std::pair<size_t, size_t> find_closest_in_sorted(
|
||||
const std::vector<P> &pts, const std::vector<P> &sorted_pts);
|
||||
|
||||
namespace closestPoint {
|
||||
/// <summary>
|
||||
/// Function used to sort points before searching for closest
|
||||
/// </summary>
|
||||
/// <param name="p1">First point</param>
|
||||
/// <param name="p2">Second Point</param>
|
||||
/// <returns>True when, p1.x < p2.x </returns>
|
||||
template<class P> bool sort_fnc(const P &p1, const P &p2){ return p1.x() < p2.x(); }
|
||||
|
||||
/// <summary> Function used to find upper bound in sorted points. </summary>
|
||||
template<class P, typename V> bool upper_fnc(V value, const P &p){ return value < p.x(); }
|
||||
/// <summary> Function used to find lower bound in sorted points. </summary>
|
||||
template<class P, typename V> bool lower_fnc(const P &p, V value){ return value > p.x(); }
|
||||
/// <summary> Calc manhatn size of point. Mainly to explain meaning</summary>
|
||||
template<class P> uint32_t manhattan_size(const P &p1, const P &p2)
|
||||
{ return std::abs(p1.x()-p2.x()) + abs(p1.y()-p2.y()); }
|
||||
|
||||
template<class P>
|
||||
typename std::vector<P>::const_iterator get_lower_bound(uint32_t& dist, const P &p,
|
||||
typename std::vector<P>::const_iterator it_x, const std::vector<P>& pts)
|
||||
{
|
||||
if(it_x == pts.begin()) return pts.begin();
|
||||
assert(it_x > pts.begin() && it_x < pts.end());
|
||||
|
||||
using V = decltype(p.x());
|
||||
using It = std::vector<P>::const_iterator;
|
||||
It res = std::lower_bound(pts.begin(), it_x, p.x() - dist, lower_fnc<P,V>);
|
||||
for (auto it = it_x - 1; it > res; --it) {
|
||||
uint32_t diff_y = std::abs(it->y() - p.y());
|
||||
if (diff_y > dist) continue;
|
||||
uint32_t diff_x = std::abs(it->x() - p.x());
|
||||
uint32_t act_dist = diff_y + diff_x;
|
||||
if (dist > act_dist) {
|
||||
dist = act_dist;
|
||||
res = std::lower_bound(res, it_x, p.x() - dist, lower_fnc<P,V>);
|
||||
}
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
template<class P>
|
||||
typename std::vector<P>::const_iterator get_uppper_bound(uint32_t& dist, const P &p,
|
||||
typename std::vector<P>::const_iterator it_x, const std::vector<P>& pts)
|
||||
{
|
||||
assert(it_x >= pts.begin() && it_x < pts.end());
|
||||
|
||||
using V = decltype(p.x());
|
||||
using It = std::vector<P>::const_iterator;
|
||||
It res = std::upper_bound(it_x, pts.end(), p.x() + dist, upper_fnc<P,V>);
|
||||
for (auto it = it_x + 1; it < res; ++it) {
|
||||
uint32_t diff_y = std::abs(it->y() - p.y());
|
||||
if (diff_y > dist) continue;
|
||||
uint32_t diff_x = std::abs(it->x() - p.x());
|
||||
uint32_t act_dist = diff_y + diff_x;
|
||||
if (dist > act_dist) {
|
||||
// IMPROVE: calc euclid distance when e.g. (diff_Biggery < 2*diff_smaller)
|
||||
dist = act_dist;
|
||||
res = std::upper_bound(it_x, res, p.x() + dist, upper_fnc<P,V>);
|
||||
}
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
} // namespace closestPoint
|
||||
} // namespace Slic3r
|
||||
|
||||
template<class P>
|
||||
size_t Slic3r::find_closest_in_sorted(const P &p, const std::vector<P> &pts)
|
||||
{
|
||||
using namespace closestPoint;
|
||||
// check that input is really sorted
|
||||
assert(std::is_sorted(pts.begin(), pts.end(), sort_fnc<P>));
|
||||
|
||||
// check input
|
||||
if (pts.empty()) return std::numeric_limits<size_t>::max();
|
||||
if (pts.size() == 1) return 0;
|
||||
|
||||
using V = decltype(p.x());
|
||||
using It = std::vector<P>::const_iterator;
|
||||
// closest point node in X
|
||||
It it_x = std::upper_bound(pts.begin(), pts.end(), p.x(), upper_fnc<P,V>);
|
||||
bool is_it_x_end = it_x == pts.end();
|
||||
// it_x can't pointing to end so change to last point
|
||||
if (is_it_x_end) --it_x;
|
||||
// manhatn distance to closest point
|
||||
uint32_t manhattan_dist = manhattan_size(*it_x, p);
|
||||
It it_l = get_lower_bound(manhattan_dist,p, it_x, pts);
|
||||
|
||||
// node for upper bound
|
||||
It it_u = (is_it_x_end) ? pts.end() :
|
||||
get_uppper_bound(manhattan_dist, p, it_x, pts);
|
||||
|
||||
// find closest by squer distance
|
||||
float dist_sq = std::numeric_limits<float>::max();
|
||||
size_t result = it_x - pts.begin();
|
||||
for (It it = it_l; it < it_u; ++it) {
|
||||
uint32_t diff_y = std::abs(it->y() - p.y());
|
||||
if (diff_y > manhattan_dist) continue;
|
||||
float diff_x = it->x() - p.x();
|
||||
// calculate square distance
|
||||
float d = (float) diff_y * diff_y + diff_x * diff_x;
|
||||
if (dist_sq > d) {
|
||||
dist_sq = d;
|
||||
result = it - pts.begin();
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
template<class P>
|
||||
std::pair<size_t, size_t> Slic3r::find_closest_in_sorted(
|
||||
const std::vector<P> &pts, const std::vector<P> &sorted_pts)
|
||||
{
|
||||
using namespace closestPoint;
|
||||
// check that input is really sorted
|
||||
assert(std::is_sorted(sorted_pts.begin(), sorted_pts.end(), sort_fnc<P>));
|
||||
|
||||
std::pair<size_t, size_t> res = {std::numeric_limits<size_t>::max(),
|
||||
std::numeric_limits<size_t>::max()};
|
||||
// check inputs
|
||||
if (pts.empty() || sorted_pts.empty()) return res;
|
||||
// Speed up for one element in vector
|
||||
if (pts.size() == 1)
|
||||
return {0, find_closest_in_sorted(pts.front(), sorted_pts)};
|
||||
if (sorted_pts.size() == 1)
|
||||
return {find_closest(sorted_pts.front(), pts), 0};
|
||||
|
||||
using V = decltype(p.x());
|
||||
using It = std::vector<P>::const_iterator;
|
||||
|
||||
uint32_t res_dist = std::numeric_limits<uint32_t>::max();
|
||||
float res_dist_sq = std::numeric_limits<float>::max();
|
||||
auto set_res_dist_sq = [&res_dist_sq, &res_dist](float dist_sq) {
|
||||
res_dist_sq = dist_sq;
|
||||
float dist = sqrt(dist_sq);
|
||||
// crop dist to unsigned int
|
||||
uint32_t dist_ui = static_cast<uint32_t>(std::ceil(dist));
|
||||
if (res_dist < dist_ui) res_dist = dist_ui;
|
||||
};
|
||||
// find limit for manhatn distance
|
||||
bool is_first = true;
|
||||
for (const P &p : pts) {
|
||||
if (is_first) {
|
||||
is_first = false;
|
||||
size_t index = find_closest_in_sorted(p, sorted_pts);
|
||||
res = {0, index};
|
||||
const P &p2 = sorted_pts[index];
|
||||
V d_x = p.x()-p2.x();
|
||||
V d_y = p.y()-p2.y();
|
||||
set_res_dist_sq(d_x * d_x + d_y * d_y);
|
||||
continue;
|
||||
}
|
||||
// closest point node in X
|
||||
It it_x = std::upper_bound(pts.begin(), pts.end(), p.x(), upper_fnc<P, V>);
|
||||
bool is_it_x_end = it_x == pts.end();
|
||||
// it_x can't pointing to end so change to last point
|
||||
if (is_it_x_end) --it_x;
|
||||
// manhatn distance to closest point
|
||||
uint32_t p_dist = manhattan_size(*it_x, p);
|
||||
|
||||
It it_l = get_lower_bound(res_dist, p, it_x, pts);
|
||||
|
||||
// node for upper bound
|
||||
It it_u = pts.end();
|
||||
if (!is_it_x_end) it_u = get_uppper_bound(res_dist, p, it_x, pts);
|
||||
|
||||
// find closest by squer distance
|
||||
float dist_sq = res_dist_sq;
|
||||
for (It it = it_l; it < it_u; ++it) {
|
||||
uint32_t diff_y = std::abs(it->y() - p.y());
|
||||
if (diff_y > res_dist) continue;
|
||||
float diff_x = it->x() - p.x();
|
||||
// calculate square distance
|
||||
float d = (float) diff_y * diff_y + diff_x * diff_x;
|
||||
if (dist_sq > d) {
|
||||
dist_sq = d;
|
||||
size_t i1 = &p - &pts.begin();
|
||||
size_t i2 = it - pts.begin();
|
||||
res = {i1, i2};
|
||||
}
|
||||
}
|
||||
|
||||
// found closer?
|
||||
if (res_dist_sq > dist_sq)
|
||||
set_res_dist_sq(dist_sq);
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
#endif // slic3r_ClosestPoint_hpp_
|
@ -411,11 +411,13 @@ VDistances calc_distances(const SurfacePatches &patches,
|
||||
/// </summary>
|
||||
/// <param name="distances">All distances - Vector distances for each shape point</param>
|
||||
/// <param name="shapes">Vector of letters</param>
|
||||
/// <param name="center">Center of projection</param>
|
||||
/// <param name="shape_point_2_index">Convert index to addresss inside of shape</param>
|
||||
/// <returns>Best projection distances</returns>
|
||||
ProjectionDistances choose_best_distance(
|
||||
const VDistances &distances,
|
||||
const ExPolygons &shapes,
|
||||
const Point ¢er,
|
||||
const ShapePoint2index &shape_point_2_index);
|
||||
|
||||
/// <summary>
|
||||
@ -454,6 +456,7 @@ void store(const CutAOIs &aois, const CutMesh &mesh, const std::string &dir);
|
||||
void store(const SurfacePatches &patches, const std::string &dir);
|
||||
void store(const Vec3f &vertex, const Vec3f &normal, const std::string &file, float size = 2.f);
|
||||
void store(const ProjectionDistances &pds, const VCutAOIs &aois, const CutMeshes &meshes, const std::string &file, float width = 0.2f/* [in mm] */);
|
||||
void store(const ExPolygons& shapes, const std::vector<bool>& mask_distances, const std::vector<std::pair<size_t, size_t>>& connections, const std::string& file_svg);
|
||||
void store(const SurfaceCut &cut, const std::string &file, const std::string &contour_dir);
|
||||
void store(const std::vector<indexed_triangle_set> &models, const std::string &obj_filename);
|
||||
void store(const std::vector<CutMesh>&models, const std::string &dir);
|
||||
@ -541,7 +544,7 @@ SurfaceCut Slic3r::cut_surface(const ExPolygons &shapes,
|
||||
priv::VDistances distances = priv::calc_distances(patches, cgal_models, cgal_shape, shapes_points, projection_ratio);
|
||||
|
||||
// for each point select best projection
|
||||
priv::ProjectionDistances best_projection = priv::choose_best_distance(distances, shapes, s2i);
|
||||
priv::ProjectionDistances best_projection = priv::choose_best_distance(distances, shapes, shapes_bb.center(), s2i);
|
||||
std::vector<bool> use_patch = priv::select_patches(best_projection, patches, model_cuts);
|
||||
SurfaceCut result = merge_patches(patches, use_patch);
|
||||
#ifdef DEBUG_OUTPUT_DIR
|
||||
@ -1685,6 +1688,9 @@ priv::VDistances priv::calc_distances(const SurfacePatches &patches,
|
||||
return result;
|
||||
}
|
||||
|
||||
|
||||
#include "libslic3r/AABBTreeLines.hpp"
|
||||
#include "libslic3r/Line.hpp"
|
||||
// functions for choose_best_distance
|
||||
namespace priv {
|
||||
|
||||
@ -1700,20 +1706,22 @@ struct ClosePoint
|
||||
float dist_sq = std::numeric_limits<float>::max();
|
||||
};
|
||||
|
||||
// search in all shapes points to found closest point to given point
|
||||
uint32_t get_closest_point_index(const Point &p, const ExPolygons &shapes, const VDistances &distances);
|
||||
struct SearchData{
|
||||
// IMPROVE: float lines are enough
|
||||
std::vector<Linef> lines;
|
||||
// convert line index into Shape point index
|
||||
std::vector<size_t> cvt;
|
||||
// contain lines from prev point to Point index
|
||||
AABBTreeIndirect::Tree<2, double> tree;
|
||||
};
|
||||
|
||||
using PNode = std::pair<Point, uint32_t>;
|
||||
using PNodes = std::vector<PNode>;
|
||||
PNodes create_nodes(const ExPolygons &shapes, const VDistances &distances, size_t shapes_point_count);
|
||||
SearchData create_search_data(const ExPolygons &shapes, const std::vector<bool>& mask);
|
||||
uint32_t get_closest_point_index(const SearchData &sd, size_t line_idx, const Vec2d &hit_point, const ExPolygons &shapes, const ShapePoint2index &s2i);
|
||||
|
||||
/// <summary>
|
||||
/// Search in all shapes points(nodes) to found closest point to given point
|
||||
/// </summary>
|
||||
/// <param name="p">Point to search closest</param>
|
||||
/// <param name="nodes">Points sorted by X</param>
|
||||
/// <returns>Index into shapes of point closest to given point</returns>
|
||||
uint32_t get_closest_point_index2(const Point &p, const PNodes& nodes);
|
||||
// use AABB Tree Lines to find closest point
|
||||
uint32_t find_closest_point_index(const Point &p, const ExPolygons &shapes, const ShapePoint2index &s2i, const std::vector<bool> &mask);
|
||||
|
||||
std::pair<uint32_t, uint32_t> find_closest_point_pair(const ExPolygons &shapes, const std::vector<bool> &done_shapes, const ShapePoint2index &s2i, const std::vector<bool> &mask);
|
||||
|
||||
// Search for closest projection to wanted distance
|
||||
const ProjectionDistance *get_closest_projection(const ProjectionDistances &distance, float wanted_distance);
|
||||
@ -1729,136 +1737,179 @@ ClosePoint find_close_point(const Point &p, ProjectionDistances &result, std::ve
|
||||
|
||||
}
|
||||
|
||||
float priv::calc_size_sq(const Point &p){
|
||||
float priv::calc_size_sq(const Point &p){
|
||||
// NOTE: p.squaredNorm() can't be use due to overflow max int value
|
||||
return (float) p.x() * p.x() + (float) p.y() * p.y();
|
||||
}
|
||||
|
||||
uint32_t priv::get_closest_point_index(const Point &p,
|
||||
const ExPolygons &shapes,
|
||||
const VDistances &distances)
|
||||
priv::SearchData priv::create_search_data(const ExPolygons &shapes,
|
||||
const std::vector<bool> &mask)
|
||||
{
|
||||
ClosePoint cp;
|
||||
uint32_t id{0};
|
||||
auto get_closest = [&distances, &p, &id, &cp]
|
||||
(const Points &pts) {
|
||||
for (const Point &p_ : pts) {
|
||||
if (distances[id].empty()) {
|
||||
++id;
|
||||
continue;
|
||||
}
|
||||
float d = calc_size_sq(p - p_);
|
||||
if (cp.dist_sq > d) {
|
||||
cp.dist_sq = d;
|
||||
cp.index = id;
|
||||
}
|
||||
++id;
|
||||
}
|
||||
};
|
||||
for (const ExPolygon &shape : shapes) {
|
||||
get_closest(shape.contour.points);
|
||||
for (const Polygon &hole : shape.holes)
|
||||
get_closest(hole.points);
|
||||
}
|
||||
return cp.index;
|
||||
}
|
||||
|
||||
priv::PNodes priv::create_nodes(const ExPolygons &shapes, const VDistances &distances, size_t shapes_point_count) {
|
||||
PNodes pts;
|
||||
pts.reserve(shapes_point_count);
|
||||
uint32_t index = 0;
|
||||
auto add_polygon = [&pts, &index, &distances](const Polygon &poly) {
|
||||
// IMPROVE: Use float precission (it is enough)
|
||||
SearchData sd;
|
||||
sd.lines.reserve(mask.size());
|
||||
sd.cvt.reserve(mask.size());
|
||||
size_t index = 0;
|
||||
auto add_lines = [&sd, &index, &mask]
|
||||
(const Polygon &poly) {
|
||||
Vec2d prev = poly.back().cast<double>();
|
||||
bool use_point = mask[index + poly.points.size() - 1];
|
||||
for (const Point &p : poly.points) {
|
||||
// skip empty distances
|
||||
if (distances[index].empty()) {
|
||||
++index;
|
||||
continue;
|
||||
if (!use_point) {
|
||||
use_point = mask[index];
|
||||
if (use_point) prev = p.cast<double>();
|
||||
} else if (!mask[index]) {
|
||||
use_point = false;
|
||||
} else {
|
||||
Vec2d p_d = p.cast<double>();
|
||||
sd.lines.emplace_back(prev, p_d);
|
||||
sd.cvt.push_back(index);
|
||||
prev = p_d;
|
||||
}
|
||||
pts.emplace_back(p, index++);
|
||||
++index;
|
||||
}
|
||||
};
|
||||
for (const ExPolygon s : shapes) {
|
||||
add_polygon(s.contour);
|
||||
for (const Polygon &h : s.holes) add_polygon(h);
|
||||
|
||||
for (const ExPolygon &shape : shapes) {
|
||||
add_lines(shape.contour);
|
||||
for (const Polygon &hole : shape.holes) add_lines(hole);
|
||||
}
|
||||
|
||||
std::sort(pts.begin(), pts.end(), [](const PNode &n1, const PNode &n2) {
|
||||
return n1.first.x() < n2.first.x();
|
||||
});
|
||||
return pts;
|
||||
sd.tree = AABBTreeLines::build_aabb_tree_over_indexed_lines(sd.lines);
|
||||
return sd;
|
||||
}
|
||||
|
||||
uint32_t priv::get_closest_point_index2(const Point &p, const PNodes &nodes)
|
||||
uint32_t priv::get_closest_point_index(const SearchData &sd,
|
||||
size_t line_idx,
|
||||
const Vec2d &hit_point,
|
||||
const ExPolygons &shapes,
|
||||
const ShapePoint2index &s2i)
|
||||
{
|
||||
assert(!nodes.empty());
|
||||
const Linef &line = sd.lines[line_idx];
|
||||
Vec2d dir = line.a - line.b;
|
||||
Vec2d dir_abs = dir.cwiseAbs();
|
||||
// use x coordinate
|
||||
int i = (dir_abs.x() > dir_abs.y())? 0 :1;
|
||||
|
||||
bool use_index = abs(line.a[i] - hit_point[i]) >
|
||||
abs(line.b[i] - hit_point[i]);
|
||||
size_t point_index = sd.cvt[line_idx];
|
||||
|
||||
// Lambda used only for check result
|
||||
auto is_same = [&s2i, &shapes](const Vec2d &p, size_t i) -> bool {
|
||||
auto id = s2i.calc_id(i);
|
||||
const ExPolygon &shape = shapes[id.expolygons_index];
|
||||
const Polygon &poly = (id.polygon_index == 0) ?
|
||||
shape.contour :
|
||||
shape.holes[id.polygon_index - 1];
|
||||
Vec2i p_ = p.cast<int>();
|
||||
return p_ == poly[id.point_index];
|
||||
};
|
||||
|
||||
if (use_index) {
|
||||
assert(is_same(line.b, point_index));
|
||||
return point_index;
|
||||
}
|
||||
auto id = s2i.calc_id(point_index);
|
||||
if (id.point_index != 0) {
|
||||
assert(is_same(line.a, point_index - 1));
|
||||
return point_index - 1;
|
||||
}
|
||||
const ExPolygon &shape = shapes[id.expolygons_index];
|
||||
size_t count_polygon_points = (id.polygon_index == 0) ?
|
||||
shape.contour.size() :
|
||||
shape.holes[id.polygon_index - 1].size();
|
||||
size_t prev_point_index = point_index + (count_polygon_points - 1);
|
||||
assert(is_same(line.a, prev_point_index));
|
||||
// return previous point index
|
||||
return prev_point_index;
|
||||
}
|
||||
|
||||
// use AABB Tree Lines
|
||||
uint32_t priv::find_closest_point_index(const Point &p,
|
||||
const ExPolygons &shapes,
|
||||
const ShapePoint2index &s2i,
|
||||
const std::vector<bool> &mask)
|
||||
{
|
||||
SearchData sd = create_search_data(shapes, mask);
|
||||
size_t line_idx = std::numeric_limits<size_t>::max();
|
||||
Vec2d hit_point;
|
||||
Vec2d p_d = p.cast<double>();
|
||||
double distance_sq = AABBTreeLines::squared_distance_to_indexed_lines(
|
||||
sd.lines, sd.tree, p_d, line_idx, hit_point);
|
||||
assert(distance_sq > 0);
|
||||
|
||||
// function to find upper point node
|
||||
auto f_u = [](coord_t value, const PNode& n) {
|
||||
return value < n.first.x();
|
||||
};
|
||||
// IMPROVE: one could use line ratio to find closest point
|
||||
return get_closest_point_index(sd, line_idx, hit_point, shapes, s2i);
|
||||
}
|
||||
|
||||
// function to find lower point node
|
||||
auto f_l = [](const PNode &n, coord_t value) {
|
||||
return value > n.first.x();
|
||||
};
|
||||
std::pair<uint32_t, uint32_t> priv::find_closest_point_pair(
|
||||
const ExPolygons &shapes,
|
||||
const std::vector<bool> &done_shapes,
|
||||
const ShapePoint2index &s2i,
|
||||
const std::vector<bool> &mask)
|
||||
{
|
||||
assert(mask.size() == s2i.get_count());
|
||||
assert(done_shapes.size() == shapes.size());
|
||||
std::vector<bool> unfinished_mask = mask; // copy
|
||||
|
||||
// closest point node in X
|
||||
auto it_x = std::upper_bound(nodes.begin(), nodes.end(), p.x(), f_u);
|
||||
// manhatn distance to closest point
|
||||
auto manhattan_size = [](const Point &p) -> uint32_t {
|
||||
return std::abs(p.x()) + abs(p.y());
|
||||
};
|
||||
uint32_t manhattan_dist = (it_x != nodes.end())?
|
||||
manhattan_size(it_x->first - p) :
|
||||
manhattan_size(nodes.back().first - p);
|
||||
// node for lower bound
|
||||
auto it_l = std::lower_bound(nodes.begin(), it_x, p.x() - manhattan_dist, f_l);
|
||||
auto it = it_x;
|
||||
while (it > it_l) {
|
||||
uint32_t diff_y = std::abs(it->first.y() - p.y());
|
||||
if (diff_y > manhattan_dist) {
|
||||
--it;
|
||||
size_t index = 0;
|
||||
for (size_t shape_index = 0; shape_index < shapes.size(); shape_index++) {
|
||||
size_t count = count_points(shapes[shape_index]);
|
||||
if (done_shapes[shape_index]) {
|
||||
for (size_t i = 0; i < count; ++i, ++index)
|
||||
unfinished_mask[index] = false;
|
||||
} else {
|
||||
index += count;
|
||||
}
|
||||
}
|
||||
assert(index == s2i.get_count());
|
||||
SearchData sd = create_search_data(shapes, unfinished_mask);
|
||||
|
||||
struct ClosestPair
|
||||
{
|
||||
size_t finish_idx = std::numeric_limits<size_t>::max();
|
||||
size_t unfinished_line_idx = std::numeric_limits<size_t>::max();
|
||||
Vec2d hit_point = Vec2d();
|
||||
double distance_sq = std::numeric_limits<double>::max();
|
||||
} cp;
|
||||
|
||||
index = 0;
|
||||
for (size_t shape_index = 0; shape_index < shapes.size(); shape_index++) {
|
||||
const ExPolygon shape = shapes[shape_index];
|
||||
if (!done_shapes[shape_index]) {
|
||||
index += count_points(shape);
|
||||
continue;
|
||||
}
|
||||
uint32_t diff_x = std::abs(it->first.x() - p.x());
|
||||
uint32_t act_dist = diff_y + diff_x;
|
||||
if (manhattan_dist > act_dist) {
|
||||
manhattan_dist = act_dist;
|
||||
it_l = std::lower_bound(it_l, it_x, p.x() - manhattan_dist, f_l);
|
||||
}
|
||||
--it;
|
||||
}
|
||||
|
||||
auto search_in_polygon = [&index, &cp, &sd, &mask](const Polygon& polygon) {
|
||||
for (size_t i = 0; i < polygon.size(); ++i, ++index) {
|
||||
if (mask[index] == false) continue;
|
||||
Vec2d p_d = polygon[i].cast<double>();
|
||||
size_t line_idx = std::numeric_limits<size_t>::max();
|
||||
Vec2d hit_point;
|
||||
double distance_sq = AABBTreeLines::squared_distance_to_indexed_lines(
|
||||
sd.lines, sd.tree, p_d, line_idx, hit_point, cp.distance_sq);
|
||||
if (distance_sq < 0) continue;
|
||||
assert(distance_sq <= cp.distance_sq);
|
||||
cp.distance_sq = distance_sq;
|
||||
cp.unfinished_line_idx = line_idx;
|
||||
cp.hit_point = hit_point;
|
||||
cp.finish_idx = index;
|
||||
}
|
||||
};
|
||||
search_in_polygon(shape.contour);
|
||||
for (const Polygon& hole: shape.holes)
|
||||
search_in_polygon(hole);
|
||||
}
|
||||
assert(index == s2i.get_count());
|
||||
// check that exists result
|
||||
if (cp.finish_idx == std::numeric_limits<size_t>::max()) {
|
||||
return std::make_pair(std::numeric_limits<size_t>::max(),
|
||||
std::numeric_limits<size_t>::max());
|
||||
}
|
||||
|
||||
// node for upper bound
|
||||
auto it_u = std::upper_bound(it_x, nodes.end(), p.x() + manhattan_dist, f_u);
|
||||
it = it_x;
|
||||
while (it < it_u) {
|
||||
++it;
|
||||
uint32_t diff_y = std::abs(it->first.y() - p.y());
|
||||
if (diff_y > manhattan_dist) continue;
|
||||
uint32_t diff_x = std::abs(it->first.x() - p.x());
|
||||
uint32_t act_dist = diff_y + diff_x;
|
||||
if (manhattan_dist > act_dist) {
|
||||
// IMPROVE: calc euclid distance when e.g. (diff_Biggery < 2*diff_smaller)
|
||||
manhattan_dist = act_dist;
|
||||
it_u = std::upper_bound(it_x, it_u, p.x() + manhattan_dist, f_u);
|
||||
}
|
||||
}
|
||||
|
||||
// find closest by squer distance
|
||||
it = it_l;
|
||||
ClosePoint cp;
|
||||
for (it = it_l; it < it_u; ++it) {
|
||||
uint32_t diff_y = std::abs(it->first.y() - p.y());
|
||||
if (diff_y > manhattan_dist) continue;
|
||||
float diff_x = it->first.x() - p.x();
|
||||
// calculate square distance
|
||||
float d = (float) diff_y * diff_y + diff_x * diff_x;
|
||||
if (cp.dist_sq > d) {
|
||||
cp.dist_sq = d;
|
||||
cp.index = it->second;
|
||||
}
|
||||
}
|
||||
return cp.index;
|
||||
size_t unfinished_idx = get_closest_point_index(sd, cp.unfinished_line_idx, cp.hit_point, shapes, s2i);
|
||||
return std::make_pair(cp.finish_idx, unfinished_idx);
|
||||
}
|
||||
|
||||
const priv::ProjectionDistance *priv::get_closest_projection(
|
||||
@ -2065,6 +2116,7 @@ priv::ClosePoint priv::find_close_point(const Point &p,
|
||||
priv::ProjectionDistances priv::choose_best_distance(
|
||||
const VDistances &distances,
|
||||
const ExPolygons &shapes,
|
||||
const Point ¢er,
|
||||
const ShapePoint2index &s2i)
|
||||
{
|
||||
// collect one closest projection for each outline point
|
||||
@ -2077,14 +2129,19 @@ priv::ProjectionDistances priv::choose_best_distance(
|
||||
// Distances are relative to projection distance
|
||||
// so first wanted distance is the closest one (ZERO)
|
||||
float wanted_distance = 0.f;
|
||||
|
||||
std::vector<bool> mask_distances(s2i.get_count(), {true});
|
||||
for (const auto &d : distances)
|
||||
if (d.empty()) mask_distances[&d - &distances.front()] = false;
|
||||
|
||||
// NOTE: Shapes are centered to respect allign of text
|
||||
Point center(0, 0);
|
||||
// Select point from shapes(text contour) which is closest to center (all in 2d)
|
||||
uint32_t unfinished_index = get_closest_point_index(center, shapes, distances);
|
||||
|
||||
//PNodes pts = create_nodes(shapes, distances, s2i.get_count());
|
||||
//uint32_t unfinished_index2 = get_closest_point_index2(center, pts);
|
||||
uint32_t unfinished_index = find_closest_point_index(center, shapes, s2i, mask_distances);
|
||||
|
||||
#ifdef DEBUG_OUTPUT_DIR
|
||||
std::vector<std::pair<size_t, size_t>> connections;
|
||||
connections.reserve(shapes.size());
|
||||
connections.emplace_back(unfinished_index, unfinished_index);
|
||||
#endif // DEBUG_OUTPUT_DIR
|
||||
|
||||
do {
|
||||
const ProjectionDistance* pd = get_closest_projection(distances[unfinished_index], wanted_distance);
|
||||
@ -2094,45 +2151,26 @@ priv::ProjectionDistances priv::choose_best_distance(
|
||||
fill_shape_distances(unfinished_index, *pd, result, finished_shapes, s2i, shapes, distances);
|
||||
|
||||
// The most close points between finished and unfinished shapes
|
||||
unfinished_index = std::numeric_limits<uint32_t>::max();
|
||||
ClosePoint best_cp; // must be finished
|
||||
|
||||
// for each unfinished points
|
||||
for (uint32_t shape_index = 0; shape_index < shapes.size(); ++shape_index) {
|
||||
if (finished_shapes[shape_index]) continue;
|
||||
const ExPolygon &shape = shapes[shape_index];
|
||||
uint32_t index = s2i.calc_index({shape_index, 0, 0});
|
||||
auto find_close_point_in_points =
|
||||
[&unfinished_index, &best_cp,
|
||||
&index, &result, &finished_shapes, &distances, &s2i, &shapes]
|
||||
(const Points &pts) {
|
||||
for (const Point &p : pts) {
|
||||
if (distances[index].empty()){
|
||||
++index;
|
||||
continue;
|
||||
}
|
||||
ClosePoint cp = find_close_point(p, result, finished_shapes, s2i, shapes);
|
||||
if (cp.index != std::numeric_limits<uint32_t>::max() &&
|
||||
best_cp.dist_sq > cp.dist_sq) {
|
||||
best_cp = cp; // copy
|
||||
unfinished_index = index;
|
||||
}
|
||||
++index;
|
||||
}
|
||||
};
|
||||
find_close_point_in_points(shape.contour.points);
|
||||
// shape could be inside of another shape's hole
|
||||
for (const Polygon &h : shape.holes)
|
||||
find_close_point_in_points(h.points);
|
||||
}
|
||||
// detect finish (best doesn't have value)
|
||||
if (best_cp.index == std::numeric_limits<uint32_t>::max()) break;
|
||||
auto [finished, unfinished] = find_closest_point_pair(
|
||||
shapes, finished_shapes, s2i, mask_distances);
|
||||
|
||||
const ProjectionDistance &closest_pd = result[best_cp.index];
|
||||
// detection of end (best doesn't have value)
|
||||
if (finished == std::numeric_limits<uint32_t>::max()) break;
|
||||
|
||||
assert(unfinished != std::numeric_limits<uint32_t>::max());
|
||||
const ProjectionDistance &closest_pd = result[finished];
|
||||
// check that best_cp is finished and has result
|
||||
assert(closest_pd.aoi_index != std::numeric_limits<uint32_t>::max());
|
||||
wanted_distance = closest_pd.distance;
|
||||
} while (unfinished_index != std::numeric_limits<uint32_t>::max());
|
||||
unfinished_index = unfinished;
|
||||
|
||||
#ifdef DEBUG_OUTPUT_DIR
|
||||
connections.emplace_back(finished, unfinished);
|
||||
#endif // DEBUG_OUTPUT_DIR
|
||||
} while (true); //(unfinished_index != std::numeric_limits<uint32_t>::max());
|
||||
#ifdef DEBUG_OUTPUT_DIR
|
||||
store(shapes, mask_distances, connections, DEBUG_OUTPUT_DIR + "closest_points.svg");
|
||||
#endif // DEBUG_OUTPUT_DIR
|
||||
return result;
|
||||
}
|
||||
|
||||
@ -3411,6 +3449,50 @@ void priv::store(const ProjectionDistances &pds,
|
||||
its_write_obj(its, file.c_str());
|
||||
}
|
||||
|
||||
#include "SVG.hpp"
|
||||
void priv::store(const ExPolygons &shapes,
|
||||
const std::vector<bool> &mask,
|
||||
const std::vector<std::pair<size_t, size_t>> &connections,
|
||||
const std::string &file_svg)
|
||||
{
|
||||
auto bb = get_extents(shapes);
|
||||
int width = get_extents(shapes.front()).size().x() / 70;
|
||||
|
||||
SVG svg(file_svg, bb);
|
||||
svg.draw(shapes);
|
||||
|
||||
ShapePoint2index s2i(shapes);
|
||||
auto get_point = [&shapes, &s2i](size_t i)->Point {
|
||||
auto id = s2i.calc_id(i);
|
||||
const ExPolygon &s = shapes[id.expolygons_index];
|
||||
const Polygon &p = (id.polygon_index == 0) ?
|
||||
s.contour :
|
||||
s.holes[id.polygon_index - 1];
|
||||
return p[id.point_index];
|
||||
};
|
||||
|
||||
bool is_first = true;
|
||||
for (auto c : connections) {
|
||||
if (is_first) {
|
||||
is_first = false;
|
||||
Point p = get_point(c.first);
|
||||
svg.draw(p, "purple", 4 * width);
|
||||
continue;
|
||||
}
|
||||
Point p1 = get_point(c.first);
|
||||
Point p2 = get_point(c.second);
|
||||
svg.draw(Line(p1, p2), "red", width);
|
||||
}
|
||||
|
||||
for (size_t i = 0; i < s2i.get_count(); i++) {
|
||||
Point p = get_point(i);
|
||||
svg.draw(p, "black", 2*width);
|
||||
if (!mask[i])
|
||||
svg.draw(p, "white", width);
|
||||
}
|
||||
svg.Close();
|
||||
}
|
||||
|
||||
namespace priv {
|
||||
/// <summary>
|
||||
/// Create model consist of rectangles for each contour edge
|
||||
|
@ -7,7 +7,6 @@ add_executable(${_TEST_NAME}_tests
|
||||
test_kdtreeindirect.cpp
|
||||
test_clipper_offset.cpp
|
||||
test_clipper_utils.cpp
|
||||
test_closest_point.cpp
|
||||
test_color.cpp
|
||||
test_config.cpp
|
||||
test_curve_fitting.cpp
|
||||
|
@ -87,6 +87,49 @@ TEST_CASE("Creating a several 2d lines, testing closest point query", "[AABBIndi
|
||||
REQUIRE(hit_point_out.y() == Approx(0.5));
|
||||
}
|
||||
|
||||
TEST_CASE("Find the closest point from ExPolys", "[ClosestPoint]") {
|
||||
//////////////////////////////
|
||||
// 0 - 3
|
||||
// |Ex0| 0 - 3
|
||||
// | |p |Ex1|
|
||||
// 1 - 2 | |
|
||||
// 1 - 2
|
||||
//[0,0]
|
||||
///////////////////
|
||||
ExPolygons ex_polys{
|
||||
/*Ex0*/ {{0, 4}, {0, 1}, {2, 1}, {2, 4}},
|
||||
/*Ex1*/ {{4, 3}, {4, 0}, {6, 0}, {6, 3}}
|
||||
};
|
||||
Vec2d p{2.5, 3.5};
|
||||
|
||||
std::vector<Linef> lines;
|
||||
auto add_lines = [&lines](const Polygon& poly) {
|
||||
for (const auto &line : poly.lines())
|
||||
lines.emplace_back(
|
||||
line.a.cast<double>(),
|
||||
line.b.cast<double>());
|
||||
};
|
||||
for (const ExPolygon &ex_poly : ex_polys) {
|
||||
add_lines(ex_poly.contour);
|
||||
for (const Polygon &hole : ex_poly.holes)
|
||||
add_lines(hole);
|
||||
}
|
||||
AABBTreeIndirect::Tree<2, double> tree =
|
||||
AABBTreeLines::build_aabb_tree_over_indexed_lines(lines);
|
||||
|
||||
size_t hit_idx_out = std::numeric_limits<size_t>::max();
|
||||
Vec2d hit_point_out;
|
||||
double distance_sq = AABBTreeLines::squared_distance_to_indexed_lines(
|
||||
lines, tree, p, hit_idx_out, hit_point_out, 0.24/* < (0.5*0.5) */);
|
||||
CHECK(hit_idx_out == std::numeric_limits<size_t>::max());
|
||||
distance_sq = AABBTreeLines::squared_distance_to_indexed_lines(
|
||||
lines, tree, p, hit_idx_out, hit_point_out, 0.26);
|
||||
CHECK(hit_idx_out != std::numeric_limits<size_t>::max());
|
||||
|
||||
//double distance = sqrt(distance_sq);
|
||||
//const Linef &line = lines[hit_idx_out];
|
||||
}
|
||||
|
||||
#if 0
|
||||
#include "libslic3r/EdgeGrid.hpp"
|
||||
#include <iostream>
|
||||
|
@ -1,81 +0,0 @@
|
||||
#include <catch2/catch.hpp>
|
||||
|
||||
#include <libslic3r/ClosestPoint.hpp>
|
||||
#include <libslic3r/Point.hpp>
|
||||
|
||||
using namespace Slic3r;
|
||||
TEST_CASE("Find the closest point from 2", "[ClosestPoint]")
|
||||
{
|
||||
Points pts = {{0, 1}, {0, 2}};
|
||||
CHECK(std::is_sorted(pts.begin(), pts.end(),
|
||||
closestPoint::sort_fnc<Point>));
|
||||
CHECK(0 == find_closest_in_sorted(Point{0, 0}, pts));
|
||||
CHECK(0 == find_closest_in_sorted(Point{1, 1}, pts));
|
||||
CHECK(1 == find_closest_in_sorted(Point{1, 2}, pts));
|
||||
}
|
||||
|
||||
TEST_CASE("Find the closest point from 9", "[ClosestPoint]")
|
||||
{
|
||||
// 0 - 3 - 6
|
||||
// | | |
|
||||
// 1 - 4 - 7
|
||||
// | | |
|
||||
// 2 - 5 - 8
|
||||
Points pts = {{-3, 3}, {-3, 0}, {-3, -3}, {0, 3}, {0, 0},
|
||||
{0, -3}, {3, 3}, {3, 0}, {3, -3}};
|
||||
CHECK(std::is_sorted(pts.begin(), pts.end(),
|
||||
closestPoint::sort_fnc<Point>));
|
||||
|
||||
CHECK(0 == find_closest_in_sorted(Point{-4, 4}, pts));
|
||||
CHECK(0 == find_closest_in_sorted(Point{-2, 2}, pts));
|
||||
// check center
|
||||
CHECK(4 == find_closest_in_sorted(Point{-1, 1}, pts));
|
||||
CHECK(4 == find_closest_in_sorted(Point{ 0, 1}, pts));
|
||||
CHECK(4 == find_closest_in_sorted(Point{ 1, 1}, pts));
|
||||
CHECK(4 == find_closest_in_sorted(Point{-1, 0}, pts));
|
||||
CHECK(4 == find_closest_in_sorted(Point{ 0, 0}, pts));
|
||||
CHECK(4 == find_closest_in_sorted(Point{ 1, 0}, pts));
|
||||
CHECK(4 == find_closest_in_sorted(Point{-1,-1}, pts));
|
||||
CHECK(4 == find_closest_in_sorted(Point{ 0,-1}, pts));
|
||||
CHECK(4 == find_closest_in_sorted(Point{ 1,-1}, pts));
|
||||
CHECK(4 == find_closest_in_sorted(Point{ 0, 0}, pts));
|
||||
|
||||
CHECK(7 == find_closest_in_sorted(Point{2, 1}, pts));
|
||||
CHECK(8 == find_closest_in_sorted(Point{2,-2}, pts));
|
||||
}
|
||||
|
||||
TEST_CASE("Find the closest point from 9 unsorted", "[ClosestPoint]")
|
||||
{
|
||||
// 4 - 3 - 0
|
||||
// | | |
|
||||
// 1 - 6 - 5
|
||||
// | | |
|
||||
// 2 - 7 - 8
|
||||
Points pts = {
|
||||
/*0*/ {3, 3},
|
||||
/*1*/ {-3, 0},
|
||||
/*2*/ {-3, -3},
|
||||
/*3*/ {0, 3},
|
||||
/*4*/ {-3, 3},
|
||||
/*5*/ {3, 0},
|
||||
/*6*/ {0, 0},
|
||||
/*7*/ {0, -3},
|
||||
/*8*/ {3, -3}
|
||||
};
|
||||
CHECK(4 == find_closest(Point{-4, 4}, pts));
|
||||
CHECK(4 == find_closest(Point{-2, 2}, pts));
|
||||
// check center
|
||||
CHECK(6 == find_closest(Point{-1, 1}, pts));
|
||||
CHECK(6 == find_closest(Point{ 0, 1}, pts));
|
||||
CHECK(6 == find_closest(Point{ 1, 1}, pts));
|
||||
CHECK(6 == find_closest(Point{-1, 0}, pts));
|
||||
CHECK(6 == find_closest(Point{ 0, 0}, pts));
|
||||
CHECK(6 == find_closest(Point{ 1, 0}, pts));
|
||||
CHECK(6 == find_closest(Point{-1,-1}, pts));
|
||||
CHECK(6 == find_closest(Point{ 0,-1}, pts));
|
||||
CHECK(6 == find_closest(Point{ 1,-1}, pts));
|
||||
CHECK(6 == find_closest(Point{ 0, 0}, pts));
|
||||
|
||||
CHECK(5 == find_closest(Point{2, 1}, pts));
|
||||
CHECK(8 == find_closest(Point{2,-2}, pts));
|
||||
}
|
Loading…
Reference in New Issue
Block a user