12 changed files with 660 additions and 72 deletions
Before Width: | Height: | Size: 3.2 KiB After Width: | Height: | Size: 3.9 KiB |
Binary file not shown.
@ -0,0 +1,38 @@ |
|||||
|
#version 150 |
||||
|
precision highp float; |
||||
|
|
||||
|
uniform sampler2DRect tex0; // Your FBO texture (RGB FBO) |
||||
|
uniform float texW; // FBO texture width |
||||
|
uniform float texH; // FBO texture height |
||||
|
|
||||
|
out vec4 fragColor; // Output color |
||||
|
in vec2 varyingtexcoord; |
||||
|
|
||||
|
void main() { |
||||
|
float threshold = 0.7; |
||||
|
float depth = texture(tex0, varyingtexcoord).r; |
||||
|
|
||||
|
// Determine which quadrant we're in |
||||
|
int quadrantX = int(varyingtexcoord.x / (256.0 / 2.0)); |
||||
|
int quadrantY = int(varyingtexcoord.y / (336.0 / 2.0)); |
||||
|
int quadrant = quadrantY * 2 + quadrantX; |
||||
|
|
||||
|
vec3 baseColor; |
||||
|
vec3 mixColor = vec3(0.0); // White for mixing in all quadrants |
||||
|
|
||||
|
// Choose base color based on quadrant |
||||
|
if (quadrant == 0) { |
||||
|
baseColor = vec3(1.0, 0.0, 0.0); // Red for bottom-left |
||||
|
} else if (quadrant == 1) { |
||||
|
baseColor = vec3(0.0, 1.0, 0.0); // Green for bottom-right |
||||
|
} else if (quadrant == 2) { |
||||
|
baseColor = vec3(0.0, 0.0, 1.0); // Blue for top-left |
||||
|
} else { |
||||
|
baseColor = vec3(1.0, 1.0, 1.0); // White for top-right |
||||
|
} |
||||
|
|
||||
|
// Mix the base color with white based on the depth |
||||
|
vec3 color = mix(mixColor, baseColor, depth); |
||||
|
|
||||
|
fragColor = vec4(color, 1.0); |
||||
|
} |
@ -0,0 +1,13 @@ |
|||||
|
#version 150 |
||||
|
|
||||
|
uniform mat4 modelViewProjectionMatrix; |
||||
|
|
||||
|
in vec4 position; |
||||
|
in vec2 texcoord; |
||||
|
|
||||
|
out vec2 varyingtexcoord; |
||||
|
|
||||
|
void main(){ |
||||
|
varyingtexcoord = texcoord; |
||||
|
gl_Position = modelViewProjectionMatrix * position; |
||||
|
} |
Binary file not shown.
@ -0,0 +1,292 @@ |
|||||
|
/// @file KDTree.cpp
|
||||
|
/// @author J. Frederico Carvalho
|
||||
|
///
|
||||
|
/// This is an adaptation of the KD-tree implementation in rosetta code
|
||||
|
/// https://rosettacode.org/wiki/K-d_tree
|
||||
|
///
|
||||
|
/// It is a reimplementation of the C code using C++. It also includes a few
|
||||
|
/// more queries than the original, namely finding all points at a distance
|
||||
|
/// smaller than some given distance to a point.
|
||||
|
|
||||
|
#include <algorithm> |
||||
|
#include <cassert> |
||||
|
#include <cmath> |
||||
|
#include <functional> |
||||
|
#include <iterator> |
||||
|
#include <limits> |
||||
|
#include <memory> |
||||
|
#include <vector> |
||||
|
|
||||
|
#include "KDTree.hpp" |
||||
|
|
||||
|
KDNode::KDNode() = default; |
||||
|
|
||||
|
KDNode::KDNode(point_t const& pt, size_t const& idx_, KDNodePtr const& left_, |
||||
|
KDNodePtr const& right_) { |
||||
|
x = pt; |
||||
|
index = idx_; |
||||
|
left = left_; |
||||
|
right = right_; |
||||
|
} |
||||
|
|
||||
|
KDNode::KDNode(pointIndex const& pi, KDNodePtr const& left_, |
||||
|
KDNodePtr const& right_) { |
||||
|
x = pi.first; |
||||
|
index = pi.second; |
||||
|
left = left_; |
||||
|
right = right_; |
||||
|
} |
||||
|
|
||||
|
KDNode::~KDNode() = default; |
||||
|
|
||||
|
double KDNode::coord(size_t const& idx) { return x.at(idx); } |
||||
|
KDNode::operator bool() { return (!x.empty()); } |
||||
|
KDNode::operator point_t() { return x; } |
||||
|
KDNode::operator size_t() { return index; } |
||||
|
KDNode::operator pointIndex() { return std::make_pair(x, index); } |
||||
|
|
||||
|
KDNodePtr NewKDNodePtr() { |
||||
|
KDNodePtr mynode = std::make_shared<KDNode>(); |
||||
|
return mynode; |
||||
|
} |
||||
|
|
||||
|
inline double dist2(point_t const& a, point_t const& b) { |
||||
|
assert(a.size() == b.size()); |
||||
|
double distc = 0; |
||||
|
for (size_t i = 0; i < a.size(); i++) { |
||||
|
double di = a.at(i) - b.at(i); |
||||
|
distc += di * di; |
||||
|
} |
||||
|
return distc; |
||||
|
} |
||||
|
|
||||
|
inline double dist2(KDNodePtr const& a, KDNodePtr const& b) { |
||||
|
return dist2(a->x, b->x); |
||||
|
} |
||||
|
|
||||
|
comparer::comparer(size_t idx_) : idx{idx_} {} |
||||
|
|
||||
|
inline bool comparer::compare_idx(pointIndex const& a, pointIndex const& b) { |
||||
|
return (a.first.at(idx) < b.first.at(idx)); |
||||
|
} |
||||
|
|
||||
|
inline void sort_on_idx(pointIndexArr::iterator const& begin, |
||||
|
pointIndexArr::iterator const& end, size_t idx) { |
||||
|
comparer comp(idx); |
||||
|
comp.idx = idx; |
||||
|
|
||||
|
using std::placeholders::_1; |
||||
|
using std::placeholders::_2; |
||||
|
|
||||
|
std::nth_element(begin, begin + std::distance(begin, end) / 2, end, |
||||
|
std::bind(&comparer::compare_idx, comp, _1, _2)); |
||||
|
} |
||||
|
|
||||
|
namespace detail { |
||||
|
inline bool compare_node_distance(std::pair<KDNodePtr, double> a, |
||||
|
std::pair<KDNodePtr, double> b) { |
||||
|
return a.second < b.second; |
||||
|
} |
||||
|
} // namespace detail
|
||||
|
|
||||
|
using pointVec = std::vector<point_t>; |
||||
|
|
||||
|
KDNodePtr KDTree::make_tree(pointIndexArr::iterator const& begin, |
||||
|
pointIndexArr::iterator const& end, |
||||
|
size_t const& level) { |
||||
|
if (begin == end) { |
||||
|
return leaf_; // empty tree
|
||||
|
} |
||||
|
|
||||
|
assert(std::distance(begin, end) > 0); |
||||
|
|
||||
|
size_t const dim = begin->first.size(); |
||||
|
sort_on_idx(begin, end, level); |
||||
|
|
||||
|
auto const num_points = std::distance(begin, end); |
||||
|
auto const middle{std::next(begin, num_points / 2)}; |
||||
|
|
||||
|
size_t const next_level{(level + 1) % dim}; |
||||
|
KDNodePtr const left{make_tree(begin, middle, next_level)}; |
||||
|
KDNodePtr const right{make_tree(std::next(middle), end, next_level)}; |
||||
|
return std::make_shared<KDNode>(*middle, left, right); |
||||
|
} |
||||
|
|
||||
|
KDTree::KDTree(pointVec point_array) : leaf_{std::make_shared<KDNode>()} { |
||||
|
pointIndexArr arr; |
||||
|
for (size_t i = 0; i < point_array.size(); i++) { |
||||
|
arr.emplace_back(point_array.at(i), i); |
||||
|
} |
||||
|
root_ = KDTree::make_tree(arr.begin(), arr.end(), 0 /* level */); |
||||
|
} |
||||
|
|
||||
|
void KDTree::node_query_( |
||||
|
KDNodePtr const& branch, point_t const& pt, size_t const& level, |
||||
|
size_t const& num_nearest, |
||||
|
std::list<std::pair<KDNodePtr, double>>& k_nearest_buffer) { |
||||
|
if (!static_cast<bool>(*branch)) { |
||||
|
return; |
||||
|
} |
||||
|
knearest_(branch, pt, level, num_nearest, k_nearest_buffer); |
||||
|
double const dl = dist2(branch->x, pt); |
||||
|
// assert(*branch);
|
||||
|
auto const node_distance = std::make_pair(branch, dl); |
||||
|
auto const insert_it = |
||||
|
std::upper_bound(k_nearest_buffer.begin(), k_nearest_buffer.end(), |
||||
|
node_distance, detail::compare_node_distance); |
||||
|
if (insert_it != k_nearest_buffer.end() || |
||||
|
k_nearest_buffer.size() < num_nearest) { |
||||
|
k_nearest_buffer.insert(insert_it, node_distance); |
||||
|
} |
||||
|
while (k_nearest_buffer.size() > num_nearest) { |
||||
|
k_nearest_buffer.pop_back(); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
void KDTree::knearest_( |
||||
|
KDNodePtr const& branch, point_t const& pt, size_t const& level, |
||||
|
size_t const& num_nearest, |
||||
|
std::list<std::pair<KDNodePtr, double>>& k_nearest_buffer) { |
||||
|
if (branch == nullptr || !static_cast<bool>(*branch)) { |
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
point_t branch_pt{*branch}; |
||||
|
size_t dim = branch_pt.size(); |
||||
|
assert(dim != 0); |
||||
|
assert(dim == pt.size()); |
||||
|
|
||||
|
double const dx = branch_pt.at(level) - pt.at(level); |
||||
|
double const dx2 = dx * dx; |
||||
|
|
||||
|
// select which branch makes sense to check
|
||||
|
KDNodePtr const close_branch = (dx > 0) ? branch->left : branch->right; |
||||
|
KDNodePtr const far_branch = (dx > 0) ? branch->right : branch->left; |
||||
|
|
||||
|
size_t const next_level = (level + 1) % dim; |
||||
|
node_query_(close_branch, pt, next_level, num_nearest, k_nearest_buffer); |
||||
|
|
||||
|
// only check the other branch if it makes sense to do so
|
||||
|
if (dx2 < k_nearest_buffer.back().second || |
||||
|
k_nearest_buffer.size() < num_nearest) { |
||||
|
node_query_(far_branch, pt, next_level, num_nearest, k_nearest_buffer); |
||||
|
} |
||||
|
}; |
||||
|
|
||||
|
// default caller
|
||||
|
KDNodePtr KDTree::nearest_(point_t const& pt) { |
||||
|
size_t level = 0; |
||||
|
std::list<std::pair<KDNodePtr, double>> k_buffer{}; |
||||
|
k_buffer.emplace_back(root_, dist2(static_cast<point_t>(*root_), pt)); |
||||
|
knearest_(root_, // beginning of tree
|
||||
|
pt, // point we are querying
|
||||
|
level, // start from level 0
|
||||
|
1, // number of nearest neighbours to return in k_buffer
|
||||
|
k_buffer // list of k nearest neigbours (to be filled)
|
||||
|
); |
||||
|
if (k_buffer.size() > 0) { |
||||
|
return k_buffer.front().first; |
||||
|
} |
||||
|
return nullptr; |
||||
|
}; |
||||
|
|
||||
|
point_t KDTree::nearest_point(point_t const& pt) { |
||||
|
return static_cast<point_t>(*nearest_(pt)); |
||||
|
} |
||||
|
|
||||
|
size_t KDTree::nearest_index(point_t const& pt) { |
||||
|
return static_cast<size_t>(*nearest_(pt)); |
||||
|
} |
||||
|
|
||||
|
pointIndex KDTree::nearest_pointIndex(point_t const& pt) { |
||||
|
KDNodePtr Nearest = nearest_(pt); |
||||
|
return static_cast<pointIndex>(*Nearest); |
||||
|
} |
||||
|
|
||||
|
pointIndexArr KDTree::nearest_pointIndices(point_t const& pt, |
||||
|
size_t const& num_nearest) { |
||||
|
size_t level = 0; |
||||
|
std::list<std::pair<KDNodePtr, double>> k_buffer{}; |
||||
|
k_buffer.emplace_back(root_, dist2(static_cast<point_t>(*root_), pt)); |
||||
|
knearest_(root_, // beginning of tree
|
||||
|
pt, // point we are querying
|
||||
|
level, // start from level 0
|
||||
|
num_nearest, // number of nearest neighbours to return in k_buffer
|
||||
|
k_buffer); // list of k nearest neigbours (to be filled)
|
||||
|
pointIndexArr output{num_nearest}; |
||||
|
std::transform(k_buffer.begin(), k_buffer.end(), output.begin(), |
||||
|
[](auto const& nodeptr_dist) { |
||||
|
return static_cast<pointIndex>(*(nodeptr_dist.first)); |
||||
|
}); |
||||
|
return output; |
||||
|
} |
||||
|
|
||||
|
pointVec KDTree::nearest_points(point_t const& pt, size_t const& num_nearest) { |
||||
|
auto const k_nearest{nearest_pointIndices(pt, num_nearest)}; |
||||
|
pointVec k_nearest_points(k_nearest.size()); |
||||
|
std::transform(k_nearest.begin(), k_nearest.end(), k_nearest_points.begin(), |
||||
|
[](pointIndex const& x) { return x.first; }); |
||||
|
return k_nearest_points; |
||||
|
} |
||||
|
|
||||
|
indexArr KDTree::nearest_indices(point_t const& pt, size_t const& num_nearest) { |
||||
|
auto const k_nearest{nearest_pointIndices(pt, num_nearest)}; |
||||
|
indexArr k_nearest_indices(k_nearest.size()); |
||||
|
std::transform(k_nearest.begin(), k_nearest.end(), |
||||
|
k_nearest_indices.begin(), |
||||
|
[](pointIndex const& x) { return x.second; }); |
||||
|
return k_nearest_indices; |
||||
|
} |
||||
|
|
||||
|
void KDTree::neighborhood_(KDNodePtr const& branch, point_t const& pt, |
||||
|
double const& rad2, size_t const& level, |
||||
|
pointIndexArr& nbh) { |
||||
|
if (!bool(*branch)) { |
||||
|
// branch has no point, means it is a leaf,
|
||||
|
// no points to add
|
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
size_t const dim = pt.size(); |
||||
|
|
||||
|
double const d = dist2(static_cast<point_t>(*branch), pt); |
||||
|
double const dx = static_cast<point_t>(*branch).at(level) - pt.at(level); |
||||
|
double const dx2 = dx * dx; |
||||
|
|
||||
|
if (d <= rad2) { |
||||
|
nbh.push_back(static_cast<pointIndex>(*branch)); |
||||
|
} |
||||
|
|
||||
|
KDNodePtr const close_branch = (dx > 0) ? branch->left : branch->right; |
||||
|
KDNodePtr const far_branch = (dx > 0) ? branch->right : branch->left; |
||||
|
|
||||
|
size_t const next_level{(level + 1) % dim}; |
||||
|
neighborhood_(close_branch, pt, rad2, next_level, nbh); |
||||
|
if (dx2 < rad2) { |
||||
|
neighborhood_(far_branch, pt, rad2, next_level, nbh); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
pointIndexArr KDTree::neighborhood(point_t const& pt, double const& rad) { |
||||
|
pointIndexArr nbh; |
||||
|
neighborhood_(root_, pt, rad * rad, /*level*/ 0, nbh); |
||||
|
return nbh; |
||||
|
} |
||||
|
|
||||
|
pointVec KDTree::neighborhood_points(point_t const& pt, double const& rad) { |
||||
|
auto nbh = std::make_shared<pointIndexArr>(); |
||||
|
neighborhood_(root_, pt, rad * rad, /*level*/ 0, *nbh); |
||||
|
pointVec nbhp(nbh->size()); |
||||
|
auto const first = [](pointIndex const& x) { return x.first; }; |
||||
|
std::transform(nbh->begin(), nbh->end(), nbhp.begin(), first); |
||||
|
return nbhp; |
||||
|
} |
||||
|
|
||||
|
indexArr KDTree::neighborhood_indices(point_t const& pt, double const& rad) { |
||||
|
auto nbh = std::make_shared<pointIndexArr>(); |
||||
|
neighborhood_(root_, pt, rad * rad, /*level*/ 0, *nbh); |
||||
|
indexArr nbhi(nbh->size()); |
||||
|
auto const second = [](pointIndex const& x) { return x.second; }; |
||||
|
std::transform(nbh->begin(), nbh->end(), nbhi.begin(), second); |
||||
|
return nbhi; |
||||
|
} |
@ -0,0 +1,179 @@ |
|||||
|
#pragma once |
||||
|
|
||||
|
/// @file KDTree.hpp
|
||||
|
/// @author J. Frederico Carvalho
|
||||
|
///
|
||||
|
/// This is an adaptation of the KD-tree implementation in rosetta code
|
||||
|
/// https://rosettacode.org/wiki/K-d_tree
|
||||
|
/// It is a reimplementation of the C code using C++.
|
||||
|
/// It also includes a few more queries than the original
|
||||
|
|
||||
|
#include <algorithm> |
||||
|
#include <functional> |
||||
|
#include <list> |
||||
|
#include <memory> |
||||
|
#include <vector> |
||||
|
|
||||
|
/// The point type (vector of double precision floats)
|
||||
|
using point_t = std::vector<double>; |
||||
|
|
||||
|
/// Array of indices
|
||||
|
using indexArr = std::vector<size_t>; |
||||
|
|
||||
|
/// Pair of point and Index
|
||||
|
using pointIndex = typename std::pair<std::vector<double>, size_t>; |
||||
|
|
||||
|
class KDNode { |
||||
|
public: |
||||
|
using KDNodePtr = std::shared_ptr<KDNode>; |
||||
|
size_t index; |
||||
|
point_t x; |
||||
|
KDNodePtr left; |
||||
|
KDNodePtr right; |
||||
|
|
||||
|
// initializer
|
||||
|
KDNode(); |
||||
|
KDNode(point_t const&, size_t const&, KDNodePtr const&, KDNodePtr const&); |
||||
|
KDNode(pointIndex const&, KDNodePtr const&, KDNodePtr const&); |
||||
|
~KDNode(); |
||||
|
|
||||
|
// getter
|
||||
|
double coord(size_t const&); |
||||
|
|
||||
|
// conversions
|
||||
|
explicit operator bool(); |
||||
|
explicit operator point_t(); |
||||
|
explicit operator size_t(); |
||||
|
explicit operator pointIndex(); |
||||
|
}; |
||||
|
|
||||
|
using KDNodePtr = std::shared_ptr<KDNode>; |
||||
|
|
||||
|
KDNodePtr NewKDNodePtr(); |
||||
|
|
||||
|
// square euclidean distance
|
||||
|
inline double dist2(point_t const&, point_t const&); |
||||
|
inline double dist2(KDNodePtr const&, KDNodePtr const&); |
||||
|
|
||||
|
// Need for sorting
|
||||
|
class comparer { |
||||
|
public: |
||||
|
size_t idx; |
||||
|
explicit comparer(size_t idx_); |
||||
|
inline bool compare_idx(std::pair<std::vector<double>, size_t> const&, //
|
||||
|
std::pair<std::vector<double>, size_t> const& //
|
||||
|
); |
||||
|
}; |
||||
|
|
||||
|
using pointIndexArr = typename std::vector<pointIndex>; |
||||
|
|
||||
|
inline void sort_on_idx(pointIndexArr::iterator const&, //
|
||||
|
pointIndexArr::iterator const&, //
|
||||
|
size_t idx); |
||||
|
|
||||
|
using pointVec = std::vector<point_t>; |
||||
|
|
||||
|
class KDTree { |
||||
|
|
||||
|
public: |
||||
|
KDTree() = default; |
||||
|
|
||||
|
/// Build a KDtree
|
||||
|
explicit KDTree(pointVec point_array); |
||||
|
|
||||
|
/// Get the point which lies closest to the input point.
|
||||
|
/// @param pt input point.
|
||||
|
point_t nearest_point(point_t const& pt); |
||||
|
|
||||
|
/// Get the index of the point which lies closest to the input point.
|
||||
|
///
|
||||
|
/// @param pt input point.
|
||||
|
size_t nearest_index(point_t const& pt); |
||||
|
|
||||
|
/// Get the point and its index which lies closest to the input point.
|
||||
|
///
|
||||
|
/// @param pt input point.
|
||||
|
pointIndex nearest_pointIndex(point_t const& pt); |
||||
|
|
||||
|
/// Get both the point and the index of the points closest to the input
|
||||
|
/// point.
|
||||
|
///
|
||||
|
/// @param pt input point.
|
||||
|
/// @param num_nearest Number of nearest points to return.
|
||||
|
///
|
||||
|
/// @returns a vector containing the points and their respective indices
|
||||
|
/// which are at a distance smaller than rad to the input point.
|
||||
|
pointIndexArr nearest_pointIndices(point_t const& pt, |
||||
|
size_t const& num_nearest); |
||||
|
|
||||
|
/// Get the nearest set of points to the given input point.
|
||||
|
///
|
||||
|
/// @param pt input point.
|
||||
|
/// @param num_nearest Number of nearest points to return.
|
||||
|
///
|
||||
|
/// @returns a vector containing the points which are at a distance smaller
|
||||
|
/// than rad to the input point.
|
||||
|
pointVec nearest_points(point_t const& pt, size_t const& num_nearest); |
||||
|
|
||||
|
/// Get the indices of points closest to the input point.
|
||||
|
///
|
||||
|
/// @param pt input point.
|
||||
|
/// @param num_nearest Number of nearest points to return.
|
||||
|
///
|
||||
|
/// @returns a vector containing the indices of the points which are at a
|
||||
|
/// distance smaller than rad to the input point.
|
||||
|
indexArr nearest_indices(point_t const& pt, size_t const& num_nearest); |
||||
|
|
||||
|
/// Get both the point and the index of the points which are at a distance
|
||||
|
/// smaller than the input radius to the input point.
|
||||
|
///
|
||||
|
/// @param pt input point.
|
||||
|
/// @param rad input radius.
|
||||
|
///
|
||||
|
/// @returns a vector containing the points and their respective indices
|
||||
|
/// which are at a distance smaller than rad to the input point.
|
||||
|
pointIndexArr neighborhood(point_t const& pt, double const& rad); |
||||
|
|
||||
|
/// Get the points that are at a distance to the input point which is
|
||||
|
/// smaller than the input radius.
|
||||
|
///
|
||||
|
/// @param pt input point.
|
||||
|
/// @param rad input radius.
|
||||
|
///
|
||||
|
/// @returns a vector containing the points which are at a distance smaller
|
||||
|
/// than rad to the input point.
|
||||
|
pointVec neighborhood_points(point_t const& pt, double const& rad); |
||||
|
|
||||
|
/// Get the indices of points that are at a distance to the input point
|
||||
|
/// which is smaller than the input radius.
|
||||
|
///
|
||||
|
/// @param pt input point.
|
||||
|
/// @param rad input radius.
|
||||
|
///
|
||||
|
/// @returns a vector containing the indices of the points which are at a
|
||||
|
/// distance smaller than rad to the input point.
|
||||
|
indexArr neighborhood_indices(point_t const& pt, double const& rad); |
||||
|
|
||||
|
private: |
||||
|
KDNodePtr make_tree(pointIndexArr::iterator const& begin, |
||||
|
pointIndexArr::iterator const& end, |
||||
|
size_t const& level); |
||||
|
|
||||
|
void knearest_(KDNodePtr const& branch, point_t const& pt, |
||||
|
size_t const& level, size_t const& num_nearest, |
||||
|
std::list<std::pair<KDNodePtr, double>>& k_nearest_buffer); |
||||
|
|
||||
|
void node_query_(KDNodePtr const& branch, point_t const& pt, |
||||
|
size_t const& level, size_t const& num_nearest, |
||||
|
std::list<std::pair<KDNodePtr, double>>& k_nearest_buffer); |
||||
|
|
||||
|
// default caller
|
||||
|
KDNodePtr nearest_(point_t const& pt); |
||||
|
|
||||
|
void neighborhood_(KDNodePtr const& branch, point_t const& pt, |
||||
|
double const& rad2, size_t const& level, |
||||
|
pointIndexArr& nbh); |
||||
|
|
||||
|
KDNodePtr root_; |
||||
|
KDNodePtr leaf_; |
||||
|
}; |
Loading…
Reference in new issue