improve smoothing
This commit is contained in:
parent
be6a1d4f8a
commit
686e8cf397
7509
data/bunnyLowPoly-noisy.obj
Normal file
7509
data/bunnyLowPoly-noisy.obj
Normal file
File diff suppressed because it is too large
Load Diff
@ -1,4 +1,5 @@
|
|||||||
#include "hole_filling.h"
|
#include "hole_filling.h"
|
||||||
|
#include "IO.h"
|
||||||
#include "util.h"
|
#include "util.h"
|
||||||
#include <MeshReconstruction.h>
|
#include <MeshReconstruction.h>
|
||||||
|
|
||||||
@ -306,10 +307,8 @@ MyMesh fillHoleImplicit(MyMesh &mesh, Hole_Filling &hf,
|
|||||||
std::vector<VertexHandle> verts;
|
std::vector<VertexHandle> verts;
|
||||||
for (HalfedgeHandle hh : hole) {
|
for (HalfedgeHandle hh : hole) {
|
||||||
verts.push_back(mesh.to_vertex_handle(hh));
|
verts.push_back(mesh.to_vertex_handle(hh));
|
||||||
verts.push_back(mesh.to_vertex_handle(
|
|
||||||
mesh.next_halfedge_handle(
|
|
||||||
mesh.opposite_halfedge_handle(hh))));
|
|
||||||
}
|
}
|
||||||
|
verts = hf.next_neighbors(verts);
|
||||||
auto [system, pts_list] = hf.compute_approx_mat(verts);
|
auto [system, pts_list] = hf.compute_approx_mat(verts);
|
||||||
auto [alpha, beta] = hf.solve_approx(system, pts_list.size(), 10);
|
auto [alpha, beta] = hf.solve_approx(system, pts_list.size(), 10);
|
||||||
Implicit_RBF rbf(alpha, beta, pts_list);
|
Implicit_RBF rbf(alpha, beta, pts_list);
|
||||||
@ -339,4 +338,20 @@ std::vector<MyMesh> fillHolesImplicit(MyMesh &mesh,
|
|||||||
fillings.push_back(fillHoleImplicit(mesh, hf, hole));
|
fillings.push_back(fillHoleImplicit(mesh, hf, hole));
|
||||||
}
|
}
|
||||||
return fillings;
|
return fillings;
|
||||||
|
|
||||||
|
// auto sdf = [&](Vec3 const& v) { return v.Norm() - 10.; };
|
||||||
|
// Rect3 domain {{-10, -10, -10}, {20, 20, 20}};
|
||||||
|
// auto filling = MarchCube(sdf, domain);
|
||||||
|
// WriteObjFile(filling, "out.obj");
|
||||||
|
// MyMesh ret;
|
||||||
|
// for (const Vec3 &v : filling.vertices) {
|
||||||
|
// VertexHandle vh = ret.new_vertex({v.x, v.y, v.z});
|
||||||
|
// ret.set_color(vh, ret.default_color);
|
||||||
|
// }
|
||||||
|
// for (const Triangle &t : filling.triangles) {
|
||||||
|
// ret.add_face(ret.vertex_handle(t[0]),
|
||||||
|
// ret.vertex_handle(t[1]),
|
||||||
|
// ret.vertex_handle(t[2]));
|
||||||
|
// }
|
||||||
|
// return {ret};
|
||||||
}
|
}
|
||||||
|
@ -15,8 +15,10 @@ static MeshProcessor *create_mesh_processor(const QString &path,
|
|||||||
mesh_processor, &MeshProcessor::fillHolesDumb);
|
mesh_processor, &MeshProcessor::fillHolesDumb);
|
||||||
QObject::connect(&main_window, &MainWindow::fillHolesImplicitClicked,
|
QObject::connect(&main_window, &MainWindow::fillHolesImplicitClicked,
|
||||||
mesh_processor, &MeshProcessor::fillHolesImplicit);
|
mesh_processor, &MeshProcessor::fillHolesImplicit);
|
||||||
QObject::connect(&main_window, &MainWindow::smoothClicked,
|
QObject::connect(&main_window, &MainWindow::smoothUniformClicked,
|
||||||
mesh_processor, &MeshProcessor::smooth);
|
mesh_processor, &MeshProcessor::smoothUniform);
|
||||||
|
QObject::connect(&main_window, &MainWindow::smoothCotangentClicked,
|
||||||
|
mesh_processor, &MeshProcessor::smoothCotangent);
|
||||||
QObject::connect(&main_window, &MainWindow::patchViewToggled,
|
QObject::connect(&main_window, &MainWindow::patchViewToggled,
|
||||||
mesh_processor, &MeshProcessor::setPatchView);
|
mesh_processor, &MeshProcessor::setPatchView);
|
||||||
QObject::connect(&main_window, &MainWindow::fillHolesImplicitScaleChanged,
|
QObject::connect(&main_window, &MainWindow::fillHolesImplicitScaleChanged,
|
||||||
|
@ -16,22 +16,6 @@ MainWindow::MainWindow(QWidget *parent)
|
|||||||
toolbar(this),
|
toolbar(this),
|
||||||
mesh_viewer() {
|
mesh_viewer() {
|
||||||
setCentralWidget(&mesh_viewer);
|
setCentralWidget(&mesh_viewer);
|
||||||
// addToolBar(Qt::RightToolBarArea, &toolbar);
|
|
||||||
|
|
||||||
// open_action = toolbar.addAction("Ouvrir…", [&](){
|
|
||||||
// emit open(QFileDialog::getOpenFileName(this, "Ouvrir un maillage"));
|
|
||||||
// });
|
|
||||||
// toolbar_actions.append(toolbar.addAction("Fractionner", [&](){
|
|
||||||
// QVector<QPair<MyMesh::Point, MyMesh>> fragments = shatter(mesh);
|
|
||||||
// mesh_viewer.removeOpenGLMesh(glm);
|
|
||||||
// for (auto &[pos, fragment] : fragments) {
|
|
||||||
// fragment.triangulate();
|
|
||||||
// QMatrix4x4 mat;
|
|
||||||
// float scale = 1.2;
|
|
||||||
// mat.translate(pos[0] * scale, pos[1] * scale, pos[2] * scale);
|
|
||||||
// mesh_viewer.addOpenGLMeshFromOpenMesh(&fragment, mat);
|
|
||||||
// }
|
|
||||||
// }));
|
|
||||||
|
|
||||||
QMenuBar *menu_bar = new QMenuBar();
|
QMenuBar *menu_bar = new QMenuBar();
|
||||||
setMenuBar(menu_bar);
|
setMenuBar(menu_bar);
|
||||||
@ -94,12 +78,24 @@ MainWindow::MainWindow(QWidget *parent)
|
|||||||
|
|
||||||
// Smoothing tools
|
// Smoothing tools
|
||||||
QGroupBox *smooth_box = new QGroupBox("Adoucissement");
|
QGroupBox *smooth_box = new QGroupBox("Adoucissement");
|
||||||
QLayout *smooth_layout = new QVBoxLayout();
|
QGridLayout *smooth_layout = new QGridLayout();
|
||||||
smooth_box->setLayout(smooth_layout);
|
smooth_box->setLayout(smooth_layout);
|
||||||
QPushButton *smooth = new QPushButton("Adoucir");
|
QPushButton *smooth = new QPushButton("Adoucir (uniforme)");
|
||||||
connect(smooth, &QPushButton::clicked,
|
connect(smooth, &QPushButton::clicked,
|
||||||
this, &MainWindow::smoothClicked);
|
this, &MainWindow::smoothUniformClicked);
|
||||||
smooth_layout->addWidget(smooth);
|
smooth_layout->addWidget(smooth, 1, 0);
|
||||||
|
QPushButton *smooth_cotan = new QPushButton("Adoucir (cotangent)");
|
||||||
|
smooth_cotangent_factor_input =
|
||||||
|
new DoubleInput(this, .00001, .001, .0001);
|
||||||
|
connect(smooth_cotangent_factor_input, &DoubleInput::valueChanged,
|
||||||
|
[&](double value) { smooth_cotangent_factor = value; });
|
||||||
|
connect(smooth_cotan, &QPushButton::clicked,
|
||||||
|
[&]() { emit smoothCotangentClicked(smooth_cotangent_factor); });
|
||||||
|
smooth_layout->addWidget(smooth_cotan, 2, 0);
|
||||||
|
smooth_layout->addWidget(smooth_cotangent_factor_input->slider(), 3, 0);
|
||||||
|
QDoubleSpinBox *sb = (QDoubleSpinBox *)(smooth_cotangent_factor_input->spinBox());
|
||||||
|
sb->setDecimals(5);
|
||||||
|
smooth_layout->addWidget(smooth_cotangent_factor_input->spinBox(), 3, 1);
|
||||||
toolbar.addWidget(smooth_box);
|
toolbar.addWidget(smooth_box);
|
||||||
|
|
||||||
|
|
||||||
|
@ -19,6 +19,8 @@ class MainWindow : public QMainWindow {
|
|||||||
QAction *save_action;
|
QAction *save_action;
|
||||||
DoubleInput *fill_holes_implicit_scale;
|
DoubleInput *fill_holes_implicit_scale;
|
||||||
DoubleInput *fill_holes_implicit_discr;
|
DoubleInput *fill_holes_implicit_discr;
|
||||||
|
DoubleInput *smooth_cotangent_factor_input;
|
||||||
|
double smooth_cotangent_factor;
|
||||||
|
|
||||||
signals:
|
signals:
|
||||||
void open(const QString &path);
|
void open(const QString &path);
|
||||||
@ -27,7 +29,8 @@ signals:
|
|||||||
void fillHolesImplicitClicked();
|
void fillHolesImplicitClicked();
|
||||||
void fillHolesImplicitScaleChanged(float value);
|
void fillHolesImplicitScaleChanged(float value);
|
||||||
void fillHolesImplicitDiscrChanged(float value);
|
void fillHolesImplicitDiscrChanged(float value);
|
||||||
void smoothClicked();
|
void smoothUniformClicked();
|
||||||
|
void smoothCotangentClicked(double factor);
|
||||||
void patchViewToggled(bool checked);
|
void patchViewToggled(bool checked);
|
||||||
|
|
||||||
public:
|
public:
|
||||||
|
@ -95,8 +95,14 @@ void MeshProcessor::setImplicitHoleFillingDiscr(float discr) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
void MeshProcessor::smooth() {
|
void MeshProcessor::smoothCotangent(double factor) {
|
||||||
::smooth(mesh);
|
::smooth(mesh, SmoothingMethod::COTANGENT, factor);
|
||||||
|
updateView();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
void MeshProcessor::smoothUniform() {
|
||||||
|
::smooth(mesh, SmoothingMethod::UNIFORM);
|
||||||
updateView();
|
updateView();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -34,7 +34,8 @@ public slots:
|
|||||||
void fillHolesImplicit();
|
void fillHolesImplicit();
|
||||||
void setImplicitHoleFillingScale(float scale);
|
void setImplicitHoleFillingScale(float scale);
|
||||||
void setImplicitHoleFillingDiscr(float discr);
|
void setImplicitHoleFillingDiscr(float discr);
|
||||||
void smooth();
|
void smoothCotangent(double factor);
|
||||||
|
void smoothUniform();
|
||||||
void setPatchView(bool on);
|
void setPatchView(bool on);
|
||||||
void click(QVector3D position);
|
void click(QVector3D position);
|
||||||
};
|
};
|
||||||
|
@ -11,12 +11,12 @@
|
|||||||
|
|
||||||
/*
|
/*
|
||||||
vα/--\
|
vα/--\
|
||||||
/ ------\ v
|
/ ------\ vi
|
||||||
/ ---X
|
/ ---X
|
||||||
/ /---- / \
|
/ /---- / \
|
||||||
/ /--- / -\
|
/ /--- / -\
|
||||||
/ /---- / \
|
/ /---- / \
|
||||||
vi --- | \
|
vj --- | \
|
||||||
\-- / -\
|
\-- / -\
|
||||||
\- / /--\
|
\- / /--\
|
||||||
\-- / /-------
|
\-- / /-------
|
||||||
@ -24,98 +24,119 @@
|
|||||||
vβ
|
vβ
|
||||||
*/
|
*/
|
||||||
|
|
||||||
Point laplace_beltrami(const MyMesh &mesh, VertexHandle vert) {
|
|
||||||
Point sum {0, 0, 0};
|
double uniform_weight(MyMesh &mesh, HalfedgeHandle vi_vj) {
|
||||||
qreal area = 0;
|
(void) mesh;
|
||||||
Point p = mesh.point(vert);
|
(void) vi_vj;
|
||||||
qreal count = 0;
|
return 1;
|
||||||
for (HalfedgeHandle v_vi : mesh.voh_range(vert)) {
|
}
|
||||||
Point pi = mesh.point(mesh.to_vertex_handle(v_vi));
|
|
||||||
HalfedgeHandle vi_v = mesh.opposite_halfedge_handle(v_vi);
|
double uniform_mass(MyMesh &mesh, VertexHandle vi) {
|
||||||
HalfedgeHandle v_va = mesh.next_halfedge_handle(vi_v);
|
double count = 0;
|
||||||
Point pa = mesh.point(mesh.to_vertex_handle(v_va));
|
for (VertexHandle v : mesh.vv_range(vi)) {
|
||||||
HalfedgeHandle vi_vb = mesh.next_halfedge_handle(v_vi);
|
(void) v;
|
||||||
Point pb = mesh.point(mesh.to_vertex_handle(vi_vb));
|
|
||||||
qreal a = angle_between(pi, pa, p);
|
|
||||||
qreal b = angle_between(pi, pb, p);
|
|
||||||
sum += (cotan(a) + cotan(b)) * (p - pi);
|
|
||||||
area += triangle_area(p, pi, pb);
|
|
||||||
count++;
|
count++;
|
||||||
}
|
}
|
||||||
// area /= 3.;
|
return 1. / count;
|
||||||
// return sum / (2.*area);
|
|
||||||
return sum / count;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
Eigen::SparseMatrix<qreal> laplacian_matrix(const MyMesh &mesh) {
|
/* Calcule le poids cotangent pour l'arête reliant vi à vj. */
|
||||||
|
double cotangent_weight(MyMesh &mesh, HalfedgeHandle vi_vj) {
|
||||||
|
Point pj = mesh.point(mesh.to_vertex_handle(vi_vj));
|
||||||
|
HalfedgeHandle vj_vb = mesh.next_halfedge_handle(vi_vj);
|
||||||
|
Point pb = mesh.point(mesh.to_vertex_handle(vj_vb));
|
||||||
|
HalfedgeHandle vj_vi = mesh.opposite_halfedge_handle(vi_vj);
|
||||||
|
Point pi = mesh.point(mesh.to_vertex_handle(vj_vi));
|
||||||
|
HalfedgeHandle vi_va = mesh.next_halfedge_handle(vj_vi);
|
||||||
|
Point pa = mesh.point(mesh.to_vertex_handle(vi_va));
|
||||||
|
double a = angle_between(pi, pa, pj);
|
||||||
|
double b = angle_between(pj, pb, pi);
|
||||||
|
return cotan(a) + cotan(b);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* Calcule l'aire de chaque face et la stoque dans une propriété
|
||||||
|
* "face_area" du maillage. */
|
||||||
|
void compute_face_areas(MyMesh &mesh) {
|
||||||
|
auto area = OpenMesh::getOrMakeProperty<FaceHandle, double>
|
||||||
|
(mesh, "face_area");
|
||||||
|
for (FaceHandle fh : mesh.faces()) {
|
||||||
|
MyMesh::FaceVertexIter fv_it = mesh.fv_iter(fh);
|
||||||
|
Point pi = mesh.point(*fv_it);
|
||||||
|
Point pj = mesh.point(*++fv_it);
|
||||||
|
Point pk = mesh.point(*++fv_it);
|
||||||
|
area[fh] = triangle_area(pi, pj, pk);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* Calcule l'aire barycentrique incidente à vert. */
|
||||||
|
double barycentric_vertex_area(MyMesh &mesh, VertexHandle vert) {
|
||||||
|
auto area = OpenMesh::getProperty<FaceHandle, double>(mesh, "face_area");
|
||||||
|
double sum = 0;
|
||||||
|
for (FaceHandle fh : mesh.vf_range(vert)) {
|
||||||
|
sum += area[fh];
|
||||||
|
}
|
||||||
|
return sum / 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
double cotangent_mass(MyMesh &mesh, VertexHandle vi) {
|
||||||
|
double area = barycentric_vertex_area(mesh, vi);
|
||||||
|
return 1. / (2 * area);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
Eigen::SparseMatrix<qreal> laplacian_matrix(MyMesh &mesh, double (*edge_weight)(MyMesh &, HalfedgeHandle), double (*vertex_mass)(MyMesh &, VertexHandle)) {
|
||||||
|
compute_face_areas(mesh);
|
||||||
size_t n_verts = mesh.n_vertices();
|
size_t n_verts = mesh.n_vertices();
|
||||||
Eigen::SparseMatrix<qreal> mass(n_verts, n_verts);
|
Eigen::SparseMatrix<double> weight(n_verts, n_verts);
|
||||||
Eigen::SparseMatrix<qreal> cotangent(n_verts, n_verts);
|
Eigen::SparseMatrix<double> mass(n_verts, n_verts);
|
||||||
|
for (VertexHandle vi : mesh.vertices()) {
|
||||||
|
if (mesh.is_boundary(vi)) continue;
|
||||||
|
double sum = 0;
|
||||||
|
for (HalfedgeHandle vi_vj : mesh.voh_range(vi)) {
|
||||||
|
VertexHandle vj = mesh.to_vertex_handle(vi_vj);
|
||||||
|
double halfedge_weight = edge_weight(mesh, vi_vj);
|
||||||
|
weight.insert(vi.idx(), vj.idx()) = halfedge_weight;
|
||||||
|
sum -= halfedge_weight;
|
||||||
|
}
|
||||||
|
weight.insert(vi.idx(), vi.idx()) = sum;
|
||||||
|
mass.insert(vi.idx(), vi.idx()) = vertex_mass(mesh, vi);
|
||||||
|
}
|
||||||
|
return mass * weight;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
void smooth(MyMesh &mesh, SmoothingMethod method, double cotan_factor) {
|
||||||
|
double factor;
|
||||||
|
Eigen::SparseMatrix<qreal> laplacian;
|
||||||
|
if (method == SmoothingMethod::COTANGENT) {
|
||||||
|
factor = cotan_factor;
|
||||||
|
laplacian = laplacian_matrix(mesh, cotangent_weight, cotangent_mass);
|
||||||
|
} else {
|
||||||
|
factor = 1;
|
||||||
|
laplacian = laplacian_matrix(mesh, uniform_weight, uniform_mass);
|
||||||
|
}
|
||||||
|
// laplacian = laplacian * laplacian;
|
||||||
|
size_t n_verts = mesh.n_vertices();
|
||||||
|
Eigen::VectorX<qreal> X(n_verts), Y(n_verts), Z(n_verts);
|
||||||
for (VertexHandle vert : mesh.vertices()) {
|
for (VertexHandle vert : mesh.vertices()) {
|
||||||
if (mesh.is_boundary(vert)) continue;
|
if (mesh.is_boundary(vert)) continue;
|
||||||
qreal sum = 0;
|
size_t id = vert.idx();
|
||||||
qreal area = 0;
|
|
||||||
qreal count = 0;
|
|
||||||
Point p = mesh.point(vert);
|
Point p = mesh.point(vert);
|
||||||
for (HalfedgeHandle v_vi : mesh.voh_range(vert)) {
|
X(id) = p[0];
|
||||||
VertexHandle vi = mesh.to_vertex_handle(v_vi);
|
Y(id) = p[1];
|
||||||
Point pi = mesh.point(vi);
|
Z(id) = p[2];
|
||||||
HalfedgeHandle vi_v = mesh.opposite_halfedge_handle(v_vi);
|
}
|
||||||
HalfedgeHandle v_va = mesh.next_halfedge_handle(vi_v);
|
X = laplacian * X;
|
||||||
Point pa = mesh.point(mesh.to_vertex_handle(v_va));
|
Y = laplacian * Y;
|
||||||
HalfedgeHandle vi_vb = mesh.next_halfedge_handle(v_vi);
|
Z = laplacian * Z;
|
||||||
Point pb = mesh.point(mesh.to_vertex_handle(vi_vb));
|
for (VertexHandle vert : mesh.vertices()) {
|
||||||
qreal a = angle_between(pi, pa, p);
|
if (mesh.is_boundary(vert)) continue;
|
||||||
qreal b = angle_between(pi, pb, p);
|
size_t id = vert.idx();
|
||||||
qreal w = -(cotan(a) + cotan(b)) / 2.;
|
Point offset {X(id), Y(id), Z(id)};
|
||||||
sum += w;
|
mesh.set_point(vert, mesh.point(vert) + offset * factor);
|
||||||
cotangent.insert(vert.idx(), vi.idx()) = w;
|
|
||||||
area += triangle_area(p, pi, pb);
|
|
||||||
count++;
|
|
||||||
}
|
|
||||||
// area /= 3.;
|
|
||||||
cotangent.insert(vert.idx(), vert.idx()) = -sum;
|
|
||||||
mass.insert(vert.idx(), vert.idx()) = 1. / (4. * area);
|
|
||||||
// mass.insert(vert.idx(), vert.idx()) = 1. / count;
|
|
||||||
}
|
}
|
||||||
return mass * cotangent;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
void smooth(MyMesh &mesh) {
|
|
||||||
auto new_pos = OpenMesh::makeTemporaryProperty<VertexHandle, Point>(mesh);
|
|
||||||
for (VertexHandle v : mesh.vertices()) {
|
|
||||||
if (!mesh.is_boundary(v)) {
|
|
||||||
new_pos[v] = mesh.point(v) - laplace_beltrami(mesh, v);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for (VertexHandle v : mesh.vertices()) {
|
|
||||||
if (!mesh.is_boundary(v)) {
|
|
||||||
mesh.set_point(v, new_pos[v]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// // Approche matricielle
|
|
||||||
// Eigen::SparseMatrix<qreal> laplacian = laplacian_matrix(mesh);
|
|
||||||
// // laplacian = laplacian * laplacian;
|
|
||||||
// size_t n_verts = mesh.n_vertices();
|
|
||||||
// Eigen::VectorX<qreal> X(n_verts), Y(n_verts), Z(n_verts);
|
|
||||||
// for (VertexHandle vert : mesh.vertices()) {
|
|
||||||
// if (mesh.is_boundary(vert)) continue;
|
|
||||||
// size_t id = vert.idx();
|
|
||||||
// Point p = mesh.point(vert);
|
|
||||||
// X(id) = p[0];
|
|
||||||
// Y(id) = p[1];
|
|
||||||
// Z(id) = p[2];
|
|
||||||
// }
|
|
||||||
// X = laplacian * X;
|
|
||||||
// Y = laplacian * Y;
|
|
||||||
// Z = laplacian * Z;
|
|
||||||
// for (VertexHandle vert : mesh.vertices()) {
|
|
||||||
// if (mesh.is_boundary(vert)) continue;
|
|
||||||
// size_t id = vert.idx();
|
|
||||||
// Point offset {X(id), Y(id), Z(id)};
|
|
||||||
// mesh.set_point(vert, mesh.point(vert) - offset);
|
|
||||||
// }
|
|
||||||
}
|
}
|
||||||
|
@ -4,8 +4,12 @@
|
|||||||
#include "my_mesh.h"
|
#include "my_mesh.h"
|
||||||
|
|
||||||
|
|
||||||
Point laplace_beltrami(const MyMesh &mesh, VertexHandle vert);
|
enum class SmoothingMethod {
|
||||||
void smooth(MyMesh &mesh);
|
UNIFORM,
|
||||||
|
COTANGENT,
|
||||||
|
};
|
||||||
|
|
||||||
|
void smooth(MyMesh &mesh, SmoothingMethod method, double cotan_factor=.0001);
|
||||||
|
|
||||||
|
|
||||||
#endif
|
#endif
|
Loading…
Reference in New Issue
Block a user