mirror of
https://github.com/OrcaSlicer/OrcaSlicer.git
synced 2026-09-28 03:11:47 +00:00
Initial Draco write support.
Currently always exports with 16-bit precision and speed 0 (best compression). The back-end function does have arguments to specify them, it's just not hooked into the GUI.
This commit is contained in:
@@ -4,6 +4,7 @@
|
||||
|
||||
#include <boost/iostreams/device/mapped_file.hpp>
|
||||
|
||||
#include <draco/compression/encode.h>
|
||||
#include <draco/compression/decode.h>
|
||||
#include <draco/io/mesh_io.h>
|
||||
#include <draco/mesh/mesh.h>
|
||||
@@ -96,22 +97,69 @@ bool load_drc(const char *path, Model *model, const char *object_name_in)
|
||||
return ret;
|
||||
}
|
||||
|
||||
bool store_drc(const char *path, TriangleMesh *mesh, bool binary)
|
||||
bool store_drc(const char *path, TriangleMesh *mesh, int bits, int speed)
|
||||
{
|
||||
// TODO
|
||||
return false;
|
||||
try {
|
||||
const std::vector<stl_triangle_vertex_indices>* indices = &(mesh->its.indices);
|
||||
const std::vector<stl_vertex>* vertices = &(mesh->its.vertices);
|
||||
|
||||
Mesh dracoMesh;
|
||||
|
||||
dracoMesh.set_num_points(vertices->size());
|
||||
|
||||
GeometryAttribute gaPos;
|
||||
gaPos.Init(GeometryAttribute::POSITION, nullptr, 3, DT_FLOAT32, false, sizeof(float)*3, 0);
|
||||
int32_t idPos = dracoMesh.AddAttribute(gaPos, true, indices->size() * 3);
|
||||
|
||||
dracoMesh.attribute(idPos)->Resize(vertices->size());
|
||||
|
||||
for (size_t i = 0; i < vertices->size(); ++ i) {
|
||||
float vertex[3];
|
||||
vertex[0] = vertices->at(i)(0);
|
||||
vertex[1] = vertices->at(i)(1);
|
||||
vertex[2] = vertices->at(i)(2);
|
||||
dracoMesh.attribute(idPos)->SetAttributeValue(AttributeValueIndex(i), vertex);
|
||||
}
|
||||
|
||||
dracoMesh.SetNumFaces(indices->size());
|
||||
for (size_t i = 0; i < indices->size(); ++ i) {
|
||||
Mesh::Face face;
|
||||
face[0] = PointIndex(indices->at(i)[0]);
|
||||
face[1] = PointIndex(indices->at(i)[1]);
|
||||
face[2] = PointIndex(indices->at(i)[2]);
|
||||
dracoMesh.SetFace(FaceIndex(i), face);
|
||||
}
|
||||
|
||||
Encoder encoder;
|
||||
encoder.SetSpeedOptions(speed, speed);
|
||||
encoder.SetAttributeQuantization(GeometryAttribute::POSITION, bits);
|
||||
|
||||
EncoderBuffer buffer;
|
||||
encoder.EncodeMeshToBuffer(dracoMesh, &buffer);
|
||||
|
||||
FILE* fp = boost::nowide::fopen(path, "wb");
|
||||
if (!fp) return false;
|
||||
size_t written = fwrite(buffer.data(), 1, buffer.size(), fp);
|
||||
fclose(fp);
|
||||
|
||||
if (written != buffer.size()) return false;
|
||||
} catch (const std::exception& e) {
|
||||
BOOST_LOG_TRIVIAL(error) << "store_drc: " << e.what();
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool store_drc(const char *path, ModelObject *model_object, bool binary)
|
||||
bool store_drc(const char *path, ModelObject *model_object, int bits, int speed)
|
||||
{
|
||||
TriangleMesh mesh = model_object->mesh();
|
||||
return store_drc(path, &mesh, binary);
|
||||
return store_drc(path, &mesh, bits, speed);
|
||||
}
|
||||
|
||||
bool store_drc(const char *path, Model *model, bool binary)
|
||||
bool store_drc(const char *path, Model *model, int bits, int speed)
|
||||
{
|
||||
TriangleMesh mesh = model->mesh();
|
||||
return store_drc(path, &mesh, binary);
|
||||
return store_drc(path, &mesh, bits, speed);
|
||||
}
|
||||
|
||||
}; // namespace Slic3r
|
||||
|
||||
@@ -11,9 +11,9 @@ class Model;
|
||||
extern bool load_drc(const char *path, TriangleMesh *meshptr);
|
||||
extern bool load_drc(const char *path, Model *model, const char *object_name = nullptr);
|
||||
|
||||
extern bool store_drc(const char *path, TriangleMesh *mesh, bool binary);
|
||||
extern bool store_drc(const char *path, ModelObject *model_object, bool binary);
|
||||
extern bool store_drc(const char *path, Model *model, bool binary);
|
||||
extern bool store_drc(const char *path, TriangleMesh *mesh, int bits, int speed);
|
||||
extern bool store_drc(const char *path, ModelObject *model_object, int bits, int speed);
|
||||
extern bool store_drc(const char *path, Model *model, int bits, int speed);
|
||||
|
||||
}; // namespace Slic3r
|
||||
|
||||
|
||||
@@ -529,6 +529,7 @@ static const FileWildcards file_wildcards_by_type[FT_SIZE] = {
|
||||
/* FT_SVG */ { "SVG files"sv, { ".svg"sv } },
|
||||
/* FT_TEX */ { "Texture"sv, { ".png"sv, ".svg"sv } },
|
||||
/* FT_SL1 */ { "Masked SLA files"sv, { ".sl1"sv, ".sl1s"sv } },
|
||||
/* FT_DRC */ { "Draco files"sv, { ".drc"sv } },
|
||||
};
|
||||
|
||||
// This function produces a Win32 file dialog file template mask to be consumed by wxWidgets on all platforms.
|
||||
|
||||
@@ -105,6 +105,8 @@ enum FileType
|
||||
|
||||
FT_SL1,
|
||||
|
||||
FT_DRC,
|
||||
|
||||
FT_SIZE,
|
||||
};
|
||||
|
||||
|
||||
@@ -855,6 +855,27 @@ void MenuFactory::append_menu_item_export_stl(wxMenu* menu, bool is_mulity_menu)
|
||||
}, m_parent);
|
||||
}
|
||||
|
||||
void MenuFactory::append_menu_item_export_drc(wxMenu* menu, bool is_mulity_menu)
|
||||
{
|
||||
append_menu_item(menu, wxID_ANY, _L("Export as one DRC") + dots, "",
|
||||
[](wxCommandEvent&) { plater()->export_drc(false, true); }, "", nullptr,
|
||||
[is_mulity_menu]() {
|
||||
const Selection& selection = plater()->canvas3D()->get_selection();
|
||||
if (is_mulity_menu)
|
||||
return selection.is_multiple_full_instance() || selection.is_multiple_full_object();
|
||||
else
|
||||
return selection.is_single_full_instance() || selection.is_single_full_object();
|
||||
}, m_parent);
|
||||
if (!is_mulity_menu)
|
||||
return;
|
||||
append_menu_item(menu, wxID_ANY, _L("Export as DRCs") + dots, "",
|
||||
[](wxCommandEvent&) { plater()->export_drc(false, true, true); }, "", nullptr,
|
||||
[]() {
|
||||
const Selection& selection = plater()->canvas3D()->get_selection();
|
||||
return selection.is_multiple_full_instance() || selection.is_multiple_full_object();
|
||||
}, m_parent);
|
||||
}
|
||||
|
||||
void MenuFactory::append_menu_item_reload_from_disk(wxMenu* menu)
|
||||
{
|
||||
append_menu_item(menu, wxID_ANY, _L("Reload from disk"), _L("Reload the selected parts from disk"),
|
||||
@@ -1269,6 +1290,7 @@ void MenuFactory::create_common_object_menu(wxMenu* menu)
|
||||
// BBS
|
||||
append_menu_item_reload_from_disk(menu);
|
||||
append_menu_item_export_stl(menu);
|
||||
append_menu_item_export_drc(menu);
|
||||
// "Scale to print volume" makes a sense just for whole object
|
||||
append_menu_item_scale_selection_to_fit_print_volume(menu);
|
||||
|
||||
@@ -1348,6 +1370,7 @@ void MenuFactory::create_extra_object_menu()
|
||||
append_menu_item_reload_from_disk(&m_object_menu);
|
||||
append_menu_item_replace_with_stl(&m_object_menu);
|
||||
append_menu_item_export_stl(&m_object_menu);
|
||||
append_menu_item_export_drc(&m_object_menu);
|
||||
}
|
||||
|
||||
void MenuFactory::create_bbl_assemble_object_menu()
|
||||
@@ -1382,6 +1405,7 @@ void MenuFactory::create_part_menu()
|
||||
append_menu_item_delete(menu);
|
||||
append_menu_item_reload_from_disk(menu);
|
||||
append_menu_item_export_stl(menu);
|
||||
append_menu_item_export_drc(menu);
|
||||
append_menu_item_fix_through_netfabb(menu);
|
||||
append_menu_items_mirror(menu);
|
||||
append_menu_item_merge_parts_to_single_part(menu);
|
||||
@@ -1697,6 +1721,7 @@ wxMenu* MenuFactory::multi_selection_menu()
|
||||
append_menu_item_change_filament(menu);
|
||||
menu->AppendSeparator();
|
||||
append_menu_item_export_stl(menu, true);
|
||||
append_menu_item_export_drc(menu, true);
|
||||
}
|
||||
else {
|
||||
append_menu_item_center(menu);
|
||||
|
||||
@@ -135,6 +135,7 @@ private:
|
||||
wxMenuItem* append_menu_item_fix_through_netfabb(wxMenu* menu);
|
||||
//wxMenuItem* append_menu_item_simplify(wxMenu* menu);
|
||||
void append_menu_item_export_stl(wxMenu* menu, bool is_mulity_menu = false);
|
||||
void append_menu_item_export_drc(wxMenu* menu, bool is_mulity_menu = false);
|
||||
void append_menu_item_reload_from_disk(wxMenu* menu);
|
||||
void append_menu_item_replace_with_stl(wxMenu* menu);
|
||||
void append_menu_item_change_extruder(wxMenu* menu);
|
||||
|
||||
@@ -2403,6 +2403,12 @@ void MainFrame::init_menubar_as_editor()
|
||||
append_menu_item(export_menu, wxID_ANY, _L("Export all objects as STLs") + dots, _L("Export all objects as STLs"),
|
||||
[this](wxCommandEvent&) { if (m_plater) m_plater->export_stl(false, false, true); }, "menu_export_stl", nullptr,
|
||||
[this](){return can_export_model(); }, this);
|
||||
append_menu_item(export_menu, wxID_ANY, _L("Export all objects as one DRC") + dots, _L("Export all objects as one DRC"),
|
||||
[this](wxCommandEvent&) { if (m_plater) m_plater->export_drc(); }, "menu_export_stl", nullptr,
|
||||
[this](){return can_export_model(); }, this);
|
||||
append_menu_item(export_menu, wxID_ANY, _L("Export all objects as DRCs") + dots, _L("Export all objects as DRCs"),
|
||||
[this](wxCommandEvent&) { if (m_plater) m_plater->export_drc(false, false, true); }, "menu_export_stl", nullptr,
|
||||
[this](){return can_export_model(); }, this);
|
||||
append_menu_item(export_menu, wxID_ANY, _L("Export Generic 3MF") + dots/* + "\tCtrl+G"*/, _L("Export 3mf file without using some 3mf-extensions"),
|
||||
[this](wxCommandEvent&) { if (m_plater) m_plater->export_core_3mf(); }, "menu_export_sliced_file", nullptr,
|
||||
[this](){return can_export_model(); }, this);
|
||||
|
||||
@@ -47,6 +47,7 @@
|
||||
|
||||
#include "libslic3r/libslic3r.h"
|
||||
#include "libslic3r/Format/STL.hpp"
|
||||
#include "libslic3r/Format/DRC.hpp"
|
||||
#include "libslic3r/Format/STEP.hpp"
|
||||
#include "libslic3r/Format/AMF.hpp"
|
||||
//#include "libslic3r/Format/3mf.hpp"
|
||||
@@ -4766,6 +4767,7 @@ wxString Plater::priv::get_export_file(GUI::FileType file_type)
|
||||
wxString wildcard;
|
||||
switch (file_type) {
|
||||
case FT_STL:
|
||||
case FT_DRC:
|
||||
case FT_AMF:
|
||||
case FT_3MF:
|
||||
case FT_GCODE:
|
||||
@@ -4787,6 +4789,12 @@ wxString Plater::priv::get_export_file(GUI::FileType file_type)
|
||||
dlg_title = _L("Export STL file:");
|
||||
break;
|
||||
}
|
||||
case FT_DRC:
|
||||
{
|
||||
output_file.replace_extension("drc");
|
||||
dlg_title = _L("Export Draco file:");
|
||||
break;
|
||||
}
|
||||
case FT_AMF:
|
||||
{
|
||||
// XXX: Problem on OS X with double extension?
|
||||
@@ -12135,6 +12143,225 @@ void Plater::export_stl(bool extended, bool selection_only, bool multi_stls)
|
||||
}
|
||||
}*/
|
||||
|
||||
void Plater::export_drc(bool extended, bool selection_only, bool multi_drcs)
|
||||
{
|
||||
if (p->model.objects.empty()) { return; }
|
||||
|
||||
wxString path;
|
||||
int bits = 16;
|
||||
int speed = 0;
|
||||
if (multi_drcs) {
|
||||
wxDirDialog dlg(this, _L("Choose a directory"), from_u8(wxGetApp().app_config->get_last_dir()),
|
||||
wxDD_DEFAULT_STYLE | wxDD_DIR_MUST_EXIST);
|
||||
if (dlg.ShowModal() == wxID_OK) {
|
||||
path = dlg.GetPath() + "/";
|
||||
}
|
||||
} else {
|
||||
path = p->get_export_file(FT_DRC);
|
||||
}
|
||||
if (path.empty()) { return; }
|
||||
const std::string path_u8 = into_u8(path);
|
||||
|
||||
wxBusyCursor wait;
|
||||
const auto& selection = p->get_selection();
|
||||
const auto obj_idx = selection.get_object_idx();
|
||||
|
||||
#if EXPORT_WITH_BOOLEAN
|
||||
if (selection_only && (obj_idx == -1 || selection.is_wipe_tower()))
|
||||
return;
|
||||
#else
|
||||
// BBS support selecting multiple objects
|
||||
if (selection_only && selection.is_wipe_tower()) return;
|
||||
|
||||
// BBS
|
||||
if (selection_only) {
|
||||
// only support selection single full object and mulitiple full object
|
||||
if (!selection.is_single_full_object() && !selection.is_multiple_full_object()) return;
|
||||
}
|
||||
|
||||
// Following lambda generates a combined mesh for export with normals pointing outwards.
|
||||
auto mesh_to_export_fff_no_boolean = [this](const ModelObject &mo, int instance_id) {
|
||||
TriangleMesh mesh;
|
||||
|
||||
//Prusa export negative parts
|
||||
std::vector<csg::CSGPart> csgmesh;
|
||||
csgmesh.reserve(2 * mo.volumes.size());
|
||||
csg::model_to_csgmesh(mo, Transform3d::Identity(), std::back_inserter(csgmesh),
|
||||
csg::mpartsPositive | csg::mpartsNegative | csg::mpartsDoSplits);
|
||||
|
||||
auto csgrange = range(csgmesh);
|
||||
if (csg::is_all_positive(csgrange)) {
|
||||
mesh = TriangleMesh{csg::csgmesh_merge_positive_parts(csgrange)};
|
||||
} else if (std::get<2>(csg::check_csgmesh_booleans(csgrange)) == csgrange.end()) {
|
||||
try {
|
||||
auto cgalm = csg::perform_csgmesh_booleans(csgrange);
|
||||
mesh = MeshBoolean::cgal::cgal_to_triangle_mesh(*cgalm);
|
||||
} catch (...) {}
|
||||
}
|
||||
|
||||
if (mesh.empty()) {
|
||||
get_notification_manager()->push_plater_error_notification(
|
||||
_u8L("Unable to perform boolean operation on model meshes. "
|
||||
"Only positive parts will be exported."));
|
||||
|
||||
for (const ModelVolume* v : mo.volumes)
|
||||
if (v->is_model_part()) {
|
||||
TriangleMesh vol_mesh(v->mesh());
|
||||
vol_mesh.transform(v->get_matrix(), true);
|
||||
mesh.merge(vol_mesh);
|
||||
}
|
||||
}
|
||||
if (instance_id == -1) {
|
||||
TriangleMesh vols_mesh(mesh);
|
||||
mesh = TriangleMesh();
|
||||
for (const ModelInstance *i : mo.instances) {
|
||||
TriangleMesh m = vols_mesh;
|
||||
m.transform(i->get_matrix(), true);
|
||||
mesh.merge(m);
|
||||
}
|
||||
} else if (0 <= instance_id && instance_id < int(mo.instances.size()))
|
||||
mesh.transform(mo.instances[instance_id]->get_matrix(), true);
|
||||
return mesh;
|
||||
};
|
||||
#endif
|
||||
auto mesh_to_export_sla = [&, this](const ModelObject& mo, int instance_id) {
|
||||
TriangleMesh mesh;
|
||||
|
||||
const SLAPrintObject *object = this->p->sla_print.get_print_object_by_model_object_id(mo.id());
|
||||
|
||||
if (auto m = object->get_mesh_to_print(); m.empty())
|
||||
mesh = combine_mesh_fff(mo, instance_id, [this](const std::string& msg) {return get_notification_manager()->push_plater_error_notification(msg); });
|
||||
else {
|
||||
const Transform3d mesh_trafo_inv = object->trafo().inverse();
|
||||
const bool is_left_handed = object->is_left_handed();
|
||||
|
||||
auto pad_mesh = extended? object->pad_mesh() : TriangleMesh{};
|
||||
pad_mesh.transform(mesh_trafo_inv);
|
||||
|
||||
auto supports_mesh = extended ? object->support_mesh() : TriangleMesh{};
|
||||
supports_mesh.transform(mesh_trafo_inv);
|
||||
|
||||
const std::vector<SLAPrintObject::Instance>& obj_instances = object->instances();
|
||||
for (const SLAPrintObject::Instance& obj_instance : obj_instances) {
|
||||
auto it = std::find_if(object->model_object()->instances.begin(), object->model_object()->instances.end(),
|
||||
[&obj_instance](const ModelInstance *mi) { return mi->id() == obj_instance.instance_id; });
|
||||
assert(it != object->model_object()->instances.end());
|
||||
|
||||
if (it != object->model_object()->instances.end()) {
|
||||
const bool one_inst_only = selection_only && ! selection.is_single_full_object();
|
||||
|
||||
const int instance_idx = it - object->model_object()->instances.begin();
|
||||
const Transform3d& inst_transform = one_inst_only
|
||||
? Transform3d::Identity()
|
||||
: object->model_object()->instances[instance_idx]->get_transformation().get_matrix();
|
||||
|
||||
TriangleMesh inst_mesh;
|
||||
|
||||
if (!pad_mesh.empty()) {
|
||||
TriangleMesh inst_pad_mesh = pad_mesh;
|
||||
inst_pad_mesh.transform(inst_transform, is_left_handed);
|
||||
inst_mesh.merge(inst_pad_mesh);
|
||||
}
|
||||
|
||||
if (!supports_mesh.empty()) {
|
||||
TriangleMesh inst_supports_mesh = supports_mesh;
|
||||
inst_supports_mesh.transform(inst_transform, is_left_handed);
|
||||
inst_mesh.merge(inst_supports_mesh);
|
||||
}
|
||||
|
||||
TriangleMesh inst_object_mesh = object->get_mesh_to_print();
|
||||
|
||||
inst_object_mesh.transform(mesh_trafo_inv);
|
||||
inst_object_mesh.transform(inst_transform, is_left_handed);
|
||||
|
||||
inst_mesh.merge(inst_object_mesh);
|
||||
|
||||
// ensure that the instance lays on the bed
|
||||
inst_mesh.translate(0.0f, 0.0f, -inst_mesh.bounding_box().min.z());
|
||||
|
||||
// merge instance with global mesh
|
||||
mesh.merge(inst_mesh);
|
||||
|
||||
if (one_inst_only)
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return mesh;
|
||||
};
|
||||
|
||||
std::function<TriangleMesh(const ModelObject& mo, int instance_id)>
|
||||
mesh_to_export;
|
||||
|
||||
if (p->printer_technology == ptFFF)
|
||||
#if EXPORT_WITH_BOOLEAN
|
||||
mesh_to_export = [this](const ModelObject& mo, int instance_id) {return Plater::combine_mesh_fff(mo, instance_id,
|
||||
[this](const std::string& msg) {return get_notification_manager()->push_plater_error_notification(msg); }); };
|
||||
#else
|
||||
mesh_to_export = mesh_to_export_fff_no_boolean;
|
||||
#endif
|
||||
else
|
||||
mesh_to_export = mesh_to_export_sla;
|
||||
|
||||
auto get_save_file = [](std::string const & dir, std::string const & name) {
|
||||
auto path = dir + name + ".drc";
|
||||
int n = 1;
|
||||
while (boost::filesystem::exists(path))
|
||||
path = dir + name + "(" + std::to_string(n++) + ").drc";
|
||||
return path;
|
||||
};
|
||||
|
||||
TriangleMesh mesh;
|
||||
if (selection_only) {
|
||||
if (selection.is_single_full_object()) {
|
||||
const auto obj_idx = selection.get_object_idx();
|
||||
const ModelObject* model_object = p->model.objects[obj_idx];
|
||||
if (selection.get_mode() == Selection::Instance)
|
||||
mesh = mesh_to_export(*model_object, (model_object->instances.size() > 1) ? -1 : selection.get_instance_idx());
|
||||
else {
|
||||
const GLVolume* volume = selection.get_first_volume();
|
||||
mesh = model_object->volumes[volume->volume_idx()]->mesh();
|
||||
mesh.transform(volume->get_volume_transformation().get_matrix(), true);
|
||||
}
|
||||
|
||||
if (model_object->instances.size() == 1) mesh.translate(-model_object->origin_translation.cast<float>());
|
||||
}
|
||||
else if (selection.is_multiple_full_object() && !multi_drcs) {
|
||||
const std::set<std::pair<int, int>>& instances_idxs = p->get_selection().get_selected_object_instances();
|
||||
for (const std::pair<int, int>& i : instances_idxs) {
|
||||
ModelObject* object = p->model.objects[i.first];
|
||||
mesh.merge(mesh_to_export(*object, i.second));
|
||||
}
|
||||
}
|
||||
else if (selection.is_multiple_full_object() && multi_drcs) {
|
||||
const std::set<std::pair<int, int>> &instances_idxs = p->get_selection().get_selected_object_instances();
|
||||
for (const std::pair<int, int> &i : instances_idxs) {
|
||||
ModelObject *object = p->model.objects[i.first];
|
||||
auto mesh = mesh_to_export(*object, i.second);
|
||||
mesh.translate(-object->origin_translation.cast<float>());
|
||||
|
||||
Slic3r::store_drc(get_save_file(path_u8, object->name).c_str(), &mesh, bits, speed);
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
else if (!multi_drcs) {
|
||||
for (const ModelObject* o : p->model.objects) {
|
||||
mesh.merge(mesh_to_export(*o, -1));
|
||||
}
|
||||
} else {
|
||||
for (const ModelObject* o : p->model.objects) {
|
||||
auto mesh = mesh_to_export(*o, -1);
|
||||
mesh.translate(-o->origin_translation.cast<float>());
|
||||
Slic3r::store_drc(get_save_file(path_u8, o->name).c_str(), &mesh, bits, speed);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
Slic3r::store_drc(path_u8.c_str(), &mesh, bits, speed);
|
||||
}
|
||||
|
||||
namespace {
|
||||
std::string get_file_name(const std::string &file_path)
|
||||
{
|
||||
|
||||
@@ -414,6 +414,7 @@ public:
|
||||
void export_core_3mf();
|
||||
static TriangleMesh combine_mesh_fff(const ModelObject& mo, int instance_id, std::function<void(const std::string&)> notify_func = {});
|
||||
void export_stl(bool extended = false, bool selection_only = false, bool multi_stls = false);
|
||||
void export_drc(bool extended = false, bool selection_only = false, bool multi_drcs = false);
|
||||
//BBS: remove amf
|
||||
//void export_amf();
|
||||
//BBS add extra param for exporting 3mf silence
|
||||
|
||||
Reference in New Issue
Block a user