mirror of
https://git.mirrors.martin98.com/https://github.com/prusa3d/PrusaSlicer.git
synced 2025-08-13 19:25:59 +08:00
Add multiple beds error states handling
Print can be unslicable for various reasons: - object partially on bed - invalid data (e.g. when sequential printing) - bed is empty Keep this information for each bed a behave accordingly.
This commit is contained in:
parent
da10a4fc8a
commit
0dcc654d39
@ -13,6 +13,19 @@ MultipleBeds s_multiple_beds;
|
||||
bool s_reload_preview_after_switching_beds = false;
|
||||
bool s_beds_just_switched = false;
|
||||
|
||||
bool is_sliceable(const PrintStatus status) {
|
||||
if (status == PrintStatus::empty) {
|
||||
return false;
|
||||
}
|
||||
if (status == PrintStatus::invalid) {
|
||||
return false;
|
||||
}
|
||||
if (status == PrintStatus::outside) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
namespace BedsGrid {
|
||||
Index grid_coords_abs2index(GridCoords coords) {
|
||||
coords = {std::abs(coords.x()), std::abs(coords.y())};
|
||||
|
@ -29,6 +29,19 @@ inline std::vector<unsigned> s_bed_selector_thumbnail_texture_ids;
|
||||
inline std::array<bool, MAX_NUMBER_OF_BEDS> s_bed_selector_thumbnail_changed;
|
||||
inline bool bed_selector_updated{false};
|
||||
|
||||
enum class PrintStatus {
|
||||
idle,
|
||||
running,
|
||||
finished,
|
||||
outside,
|
||||
invalid,
|
||||
empty
|
||||
};
|
||||
|
||||
bool is_sliceable(const PrintStatus status);
|
||||
|
||||
inline std::array<PrintStatus, MAX_NUMBER_OF_BEDS> s_print_statuses;
|
||||
|
||||
class MultipleBeds {
|
||||
public:
|
||||
MultipleBeds() = default;
|
||||
|
@ -125,6 +125,8 @@ void GLCanvas3D::select_bed(int i, bool triggered_by_user)
|
||||
m_sequential_print_clearance.m_evaluating = true;
|
||||
reset_sequential_print_clearance();
|
||||
|
||||
post_event(Event<bool>(EVT_GLCANVAS_ENABLE_ACTION_BUTTONS, is_sliceable(s_print_statuses[i])));
|
||||
|
||||
// The stop call above schedules some events that would be processed after the switch.
|
||||
// Among else, on_process_completed would be called, which would stop slicing of
|
||||
// the new bed. We need to stop the process, pump all the events out of the queue
|
||||
@ -1560,8 +1562,9 @@ bool GLCanvas3D::check_volumes_outside_state(GLVolumeCollection& volumes, ModelI
|
||||
if (volume->printable) {
|
||||
if (overall_state == ModelInstancePVS_Inside && volume->is_outside)
|
||||
overall_state = ModelInstancePVS_Fully_Outside;
|
||||
if (overall_state == ModelInstancePVS_Fully_Outside && volume->is_outside && state == BuildVolume::ObjectState::Colliding)
|
||||
if (overall_state == ModelInstancePVS_Fully_Outside && volume->is_outside && state == BuildVolume::ObjectState::Colliding) {
|
||||
overall_state = ModelInstancePVS_Partly_Outside;
|
||||
}
|
||||
contained_min_one |= !volume->is_outside;
|
||||
|
||||
if (bed_idx != -1 && bed_idx == s_multiple_beds.get_number_of_beds())
|
||||
@ -2211,22 +2214,20 @@ void GLCanvas3D::render()
|
||||
if (m_picking_enabled && m_rectangle_selection.is_dragging())
|
||||
m_rectangle_selection.render(*this);
|
||||
} else {
|
||||
const auto &prints{
|
||||
tcb::span{wxGetApp().plater()->get_fff_prints()}
|
||||
.subspan(0, s_multiple_beds.get_number_of_beds())
|
||||
};
|
||||
const auto &prints{wxGetApp().plater()->get_fff_prints()};
|
||||
|
||||
const bool all_finished{std::all_of(
|
||||
prints.begin(),
|
||||
prints.end(),
|
||||
[](const std::unique_ptr<Print> &print){
|
||||
return print->finished() || print->empty();
|
||||
bool all_finished{true};
|
||||
for (std::size_t bed_index{}; bed_index < s_multiple_beds.get_number_of_beds(); ++bed_index) {
|
||||
const std::unique_ptr<Print> &print{prints[bed_index]};
|
||||
if (!print->finished() && is_sliceable(s_print_statuses[bed_index])) {
|
||||
all_finished = false;
|
||||
break;
|
||||
}
|
||||
)};
|
||||
}
|
||||
|
||||
if (!all_finished) {
|
||||
render_autoslicing_wait();
|
||||
if (fff_print()->finished() || fff_print()->empty()) {
|
||||
if (fff_print()->finished() || !is_sliceable(s_print_statuses[s_multiple_beds.get_active_bed()])) {
|
||||
s_multiple_beds.autoslice_next_bed();
|
||||
wxYield();
|
||||
} else {
|
||||
@ -2847,7 +2848,7 @@ void GLCanvas3D::reload_scene(bool refresh_immediately, bool force_full_scene_re
|
||||
// checks for geometry outside the print volume to render it accordingly
|
||||
if (!m_volumes.empty()) {
|
||||
ModelInstanceEPrintVolumeState state;
|
||||
const bool contained_min_one = check_volumes_outside_state(m_volumes, &state, !force_full_scene_refresh);
|
||||
check_volumes_outside_state(m_volumes, &state, !force_full_scene_refresh);
|
||||
const bool partlyOut = (state == ModelInstanceEPrintVolumeState::ModelInstancePVS_Partly_Outside);
|
||||
const bool fullyOut = (state == ModelInstanceEPrintVolumeState::ModelInstancePVS_Fully_Outside);
|
||||
|
||||
@ -2870,15 +2871,11 @@ void GLCanvas3D::reload_scene(bool refresh_immediately, bool force_full_scene_re
|
||||
_set_warning_notification(EWarning::SlaSupportsOutside, false);
|
||||
}
|
||||
}
|
||||
|
||||
post_event(Event<bool>(EVT_GLCANVAS_ENABLE_ACTION_BUTTONS,
|
||||
contained_min_one && !m_model->objects.empty() && !partlyOut));
|
||||
}
|
||||
else {
|
||||
_set_warning_notification(EWarning::ObjectOutside, false);
|
||||
_set_warning_notification(EWarning::ObjectClashed, false);
|
||||
_set_warning_notification(EWarning::SlaSupportsOutside, false);
|
||||
post_event(Event<bool>(EVT_GLCANVAS_ENABLE_ACTION_BUTTONS, false));
|
||||
}
|
||||
|
||||
refresh_camera_scene_box();
|
||||
@ -6581,17 +6578,14 @@ void GLCanvas3D::_render_overlays()
|
||||
|
||||
#define use_scrolling 1
|
||||
|
||||
enum class PrintStatus {
|
||||
idle,
|
||||
running,
|
||||
finished
|
||||
};
|
||||
|
||||
std::string get_status_text(PrintStatus status) {
|
||||
switch(status) {
|
||||
case PrintStatus::idle: return _u8L("Unsliced");
|
||||
case PrintStatus::running: return _u8L("Slicing...");
|
||||
case PrintStatus::finished: return _u8L("Sliced");
|
||||
case PrintStatus::outside: return _u8L("Outside");
|
||||
case PrintStatus::invalid: return _u8L("Invalid");
|
||||
case PrintStatus::empty: return _u8L("Empty");
|
||||
}
|
||||
return {};
|
||||
}
|
||||
@ -6601,6 +6595,9 @@ wchar_t get_raw_status_icon(const PrintStatus status) {
|
||||
case PrintStatus::finished: return ImGui::PrintFinished;
|
||||
case PrintStatus::running: return ImGui::PrintRunning;
|
||||
case PrintStatus::idle: return ImGui::PrintIdle;
|
||||
case PrintStatus::outside: return ImGui::PrintIdle;
|
||||
case PrintStatus::invalid: return ImGui::PrintIdle;
|
||||
case PrintStatus::empty: return ImGui::PrintIdle;
|
||||
}
|
||||
return ImGui::PrintIdle;
|
||||
}
|
||||
@ -6685,39 +6682,42 @@ void Slic3r::GUI::GLCanvas3D::_render_bed_selector()
|
||||
|
||||
auto render_bed_button = [btn_side, btn_border, btn_size, btn_padding, this, &extra_frame, scale](int i)
|
||||
{
|
||||
bool empty = ! s_multiple_beds.is_bed_occupied(i);
|
||||
bool inactive = i != s_multiple_beds.get_active_bed() || s_multiple_beds.is_autoslicing();
|
||||
|
||||
ImGui::PushStyleColor(ImGuiCol_Button, ImGuiPureWrap::COL_GREY_DARK);
|
||||
ImGui::PushStyleColor(ImGuiCol_Border, inactive ? ImGuiPureWrap::COL_GREY_DARK : ImGuiPureWrap::COL_BUTTON_ACTIVE);
|
||||
|
||||
if (empty)
|
||||
ImGui::PushItemFlag(ImGuiItemFlags_Disabled, true);
|
||||
const PrintStatus print_status{s_print_statuses[i]};
|
||||
|
||||
bool clicked = false;
|
||||
|
||||
std::optional<PrintStatus> print_status;
|
||||
if (current_printer_technology() == ptFFF) {
|
||||
print_status = PrintStatus::idle;
|
||||
if (wxGetApp().plater()->get_fff_prints()[i]->finished()) {
|
||||
print_status = PrintStatus::finished;
|
||||
} else if (m_process->fff_print() == wxGetApp().plater()->get_fff_prints()[i].get() && m_process->running()) {
|
||||
print_status = PrintStatus::running;
|
||||
if ( !previous_print_status[i]
|
||||
|| print_status != previous_print_status[i]
|
||||
) {
|
||||
extra_frame = true;
|
||||
}
|
||||
previous_print_status[i] = print_status;
|
||||
}
|
||||
|
||||
if (!previous_print_status[i] || print_status != previous_print_status[i]) {
|
||||
extra_frame = true;
|
||||
}
|
||||
previous_print_status[i] = print_status;
|
||||
|
||||
if (s_bed_selector_thumbnail_changed[i]) {
|
||||
extra_frame = true;
|
||||
s_bed_selector_thumbnail_changed[i] = false;
|
||||
}
|
||||
|
||||
if (i >= int(s_bed_selector_thumbnail_texture_ids.size()) || empty) {
|
||||
clicked = ImGui::Button(empty ? "empty" : std::to_string(i + 1).c_str(), btn_size + btn_padding);
|
||||
if (
|
||||
!is_sliceable(print_status)
|
||||
) {
|
||||
ImGui::PushItemFlag(ImGuiItemFlags_Disabled, true);
|
||||
}
|
||||
|
||||
bool clicked = false;
|
||||
if (!is_sliceable(print_status)) {
|
||||
ImGui::Button(get_status_text(print_status).c_str(), btn_size + btn_padding);
|
||||
} else if (
|
||||
i >= int(s_bed_selector_thumbnail_texture_ids.size())
|
||||
) {
|
||||
clicked = ImGui::Button(
|
||||
std::to_string(i + 1).c_str(), btn_size + btn_padding
|
||||
);
|
||||
} else {
|
||||
clicked = bed_selector_thumbnail(
|
||||
btn_size,
|
||||
@ -6726,19 +6726,22 @@ void Slic3r::GUI::GLCanvas3D::_render_bed_selector()
|
||||
btn_border,
|
||||
scale,
|
||||
s_bed_selector_thumbnail_texture_ids[i],
|
||||
print_status
|
||||
current_printer_technology() == ptFFF ? std::optional{print_status} : std::nullopt
|
||||
);
|
||||
}
|
||||
|
||||
if (clicked && ! empty)
|
||||
if (clicked && is_sliceable(print_status))
|
||||
select_bed(i, true);
|
||||
|
||||
ImGui::PopStyleColor(2);
|
||||
if (empty)
|
||||
if (
|
||||
!is_sliceable(print_status)
|
||||
) {
|
||||
ImGui::PopItemFlag();
|
||||
}
|
||||
|
||||
if (print_status) {
|
||||
const std::string status_text{get_status_text(*print_status)};
|
||||
if (current_printer_technology() == ptFFF) {
|
||||
const std::string status_text{get_status_text(print_status)};
|
||||
if (ImGui::IsItemHovered()) {
|
||||
ImGui::SetTooltip("%s", status_text.c_str());
|
||||
}
|
||||
|
@ -581,8 +581,7 @@ struct Plater::priv
|
||||
std::string last_output_path;
|
||||
std::string last_output_dir_path;
|
||||
bool inside_snapshot_capture() { return m_prevent_snapshots != 0; }
|
||||
bool process_completed_with_error { false };
|
||||
|
||||
|
||||
private:
|
||||
bool layers_height_allowed() const;
|
||||
|
||||
@ -1888,13 +1887,61 @@ void Plater::priv::selection_changed()
|
||||
view3D->render();
|
||||
}
|
||||
|
||||
std::size_t count_instances(SpanOfConstPtrs<PrintObject> objects) {
|
||||
return std::accumulate(
|
||||
objects.begin(),
|
||||
objects.end(),
|
||||
std::size_t{},
|
||||
[](const std::size_t result, const PrintObject *object){
|
||||
return result + object->instances().size();
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
std::size_t count_instances(const std::map<ObjectID, int> &bed_instances, const int bed_index) {
|
||||
return std::accumulate(
|
||||
bed_instances.begin(),
|
||||
bed_instances.end(),
|
||||
std::size_t{},
|
||||
[&](const std::size_t result, const auto &key_value){
|
||||
const auto &[object_id, _bed_index]{key_value};
|
||||
if (_bed_index != bed_index) {
|
||||
return result;
|
||||
}
|
||||
return result + 1;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
void Plater::priv::object_list_changed()
|
||||
{
|
||||
const bool export_in_progress = this->background_process.is_export_scheduled(); // || ! send_gcode_file.empty());
|
||||
// XXX: is this right?
|
||||
const bool model_fits = view3D->get_canvas3d()->check_volumes_outside_state() == ModelInstancePVS_Inside;
|
||||
//
|
||||
if (printer_technology == ptFFF) {
|
||||
for (std::size_t bed_index{}; bed_index < s_multiple_beds.get_number_of_beds(); ++bed_index) {
|
||||
const std::size_t print_instances_count{
|
||||
count_instances(wxGetApp().plater()->get_fff_prints()[bed_index]->objects())
|
||||
};
|
||||
const std::size_t bed_instances_count{
|
||||
count_instances(s_multiple_beds.get_inst_map(), bed_index)
|
||||
};
|
||||
if (print_instances_count != bed_instances_count) {
|
||||
s_print_statuses[bed_index] = PrintStatus::outside;
|
||||
} else if (print_instances_count == 0) {
|
||||
s_print_statuses[bed_index] = PrintStatus::empty;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (model.objects.empty()) {
|
||||
s_print_statuses[s_multiple_beds.get_active_bed()] = PrintStatus::empty;
|
||||
}
|
||||
}
|
||||
|
||||
sidebar->enable_buttons(s_multiple_beds.is_bed_occupied(s_multiple_beds.get_active_bed()) && !model.objects.empty() && !export_in_progress && model_fits);
|
||||
sidebar->enable_buttons(
|
||||
s_multiple_beds.is_bed_occupied(s_multiple_beds.get_active_bed())
|
||||
&& !export_in_progress
|
||||
&& is_sliceable(s_print_statuses[s_multiple_beds.get_active_bed()])
|
||||
);
|
||||
}
|
||||
|
||||
void Plater::priv::select_all()
|
||||
@ -2302,6 +2349,34 @@ unsigned int Plater::priv::update_background_process(bool force_validation, bool
|
||||
throw std::runtime_error{"Ivalid printer technology!"};
|
||||
}
|
||||
|
||||
for (std::size_t bed_index{}; bed_index < s_multiple_beds.get_number_of_beds(); ++bed_index) {
|
||||
if (printer_technology == ptFFF) {
|
||||
if (apply_statuses[bed_index] != Print::ApplyStatus::APPLY_STATUS_UNCHANGED) {
|
||||
s_print_statuses[bed_index] = PrintStatus::idle;
|
||||
}
|
||||
} else if (printer_technology == ptSLA) {
|
||||
if (apply_statuses[0] != Print::ApplyStatus::APPLY_STATUS_UNCHANGED) {
|
||||
s_print_statuses[bed_index] = PrintStatus::idle;
|
||||
}
|
||||
} else {
|
||||
throw std::runtime_error{"Ivalid printer technology!"};
|
||||
}
|
||||
}
|
||||
|
||||
if (printer_technology == ptFFF) {
|
||||
for (std::size_t bed_index{0}; bed_index < q->p->fff_prints.size(); ++bed_index) {
|
||||
const std::unique_ptr<Print> &print{q->p->fff_prints[bed_index]};
|
||||
using MultipleBedsUtils::with_single_bed_model_fff;
|
||||
with_single_bed_model_fff(model, bed_index, [&](){
|
||||
std::vector<std::string> warnings;
|
||||
std::string err{print->validate(&warnings)};
|
||||
if (!err.empty()) {
|
||||
s_print_statuses[bed_index] = PrintStatus::invalid;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const bool any_status_changed{std::any_of(
|
||||
apply_statuses.begin(),
|
||||
apply_statuses.end(),
|
||||
@ -2346,7 +2421,6 @@ unsigned int Plater::priv::update_background_process(bool force_validation, bool
|
||||
notification_manager->set_slicing_progress_hidden();
|
||||
}
|
||||
|
||||
|
||||
if ((invalidated != Print::APPLY_STATUS_UNCHANGED || force_validation) && ! background_process.empty()) {
|
||||
// The delayed error message is no more valid.
|
||||
delayed_error_message.clear();
|
||||
@ -2410,7 +2484,7 @@ unsigned int Plater::priv::update_background_process(bool force_validation, bool
|
||||
return return_state;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if (! this->delayed_error_message.empty())
|
||||
// Reusing the old state.
|
||||
return_state |= UPDATE_BACKGROUND_PROCESS_INVALID;
|
||||
@ -2423,7 +2497,6 @@ unsigned int Plater::priv::update_background_process(bool force_validation, bool
|
||||
actualize_slicing_warnings(*this->background_process.current_print());
|
||||
actualize_object_warnings(*this->background_process.current_print());
|
||||
show_warning_dialog = false;
|
||||
process_completed_with_error = false;
|
||||
}
|
||||
|
||||
if (invalidated != Print::APPLY_STATUS_UNCHANGED && was_running && ! this->background_process.running() &&
|
||||
@ -2439,7 +2512,7 @@ unsigned int Plater::priv::update_background_process(bool force_validation, bool
|
||||
const wxString invalid_str = _L("Invalid data");
|
||||
for (auto btn : {ActionButtonType::Reslice, ActionButtonType::SendGCode, ActionButtonType::Export})
|
||||
sidebar->set_btn_label(btn, invalid_str);
|
||||
process_completed_with_error = true;
|
||||
s_print_statuses[s_multiple_beds.get_active_bed()] = PrintStatus::invalid;
|
||||
}
|
||||
else
|
||||
{
|
||||
@ -2455,9 +2528,7 @@ unsigned int Plater::priv::update_background_process(bool force_validation, bool
|
||||
const wxString slice_string = background_process.running() && wxGetApp().get_mode() == comSimple ?
|
||||
_L("Slicing") + dots : _L("Slice now");
|
||||
sidebar->set_btn_label(ActionButtonType::Reslice, slice_string);
|
||||
if (background_process.empty()) {
|
||||
sidebar->enable_buttons(false);
|
||||
} else if (background_process.finished())
|
||||
if (background_process.finished())
|
||||
show_action_buttons(false);
|
||||
else if (!background_process.empty() &&
|
||||
!background_process.running()) /* Do not update buttons if background process is running
|
||||
@ -2467,6 +2538,7 @@ unsigned int Plater::priv::update_background_process(bool force_validation, bool
|
||||
show_action_buttons(true);
|
||||
}
|
||||
|
||||
this->q->object_list_changed();
|
||||
return return_state;
|
||||
}
|
||||
|
||||
@ -3076,10 +3148,12 @@ void Plater::priv::set_current_panel(wxPanel* panel)
|
||||
|
||||
if (wxGetApp().is_editor()) {
|
||||
// see: Plater::priv::object_list_changed()
|
||||
// FIXME: it may be better to have a single function making this check and let it be called wherever needed
|
||||
bool export_in_progress = this->background_process.is_export_scheduled();
|
||||
bool model_fits = view3D->get_canvas3d()->check_volumes_outside_state() != ModelInstancePVS_Partly_Outside;
|
||||
if (s_multiple_beds.is_bed_occupied(s_multiple_beds.get_active_bed()) && !model.objects.empty() && !export_in_progress && model_fits) {
|
||||
if (
|
||||
s_multiple_beds.is_bed_occupied(s_multiple_beds.get_active_bed())
|
||||
&& !export_in_progress
|
||||
&& is_sliceable(s_print_statuses[s_multiple_beds.get_active_bed()])
|
||||
) {
|
||||
preview->get_canvas3d()->init_gcode_viewer();
|
||||
preview->get_canvas3d()->load_gcode_shells();
|
||||
q->reslice();
|
||||
@ -3258,6 +3332,7 @@ void Plater::priv::on_slicing_began()
|
||||
notification_manager->close_notification_of_type(NotificationType::SignDetected);
|
||||
notification_manager->close_notification_of_type(NotificationType::ExportFinished);
|
||||
notification_manager->set_slicing_progress_began();
|
||||
s_print_statuses[s_multiple_beds.get_active_bed()] = PrintStatus::running;
|
||||
}
|
||||
void Plater::priv::add_warning(const Slic3r::PrintStateBase::Warning& warning, size_t oid)
|
||||
{
|
||||
@ -3349,15 +3424,19 @@ void Plater::priv::on_process_completed(SlicingProcessCompletedEvent &evt)
|
||||
const wxString invalid_str = _L("Invalid data");
|
||||
for (auto btn : { ActionButtonType::Reslice, ActionButtonType::SendGCode, ActionButtonType::Export })
|
||||
sidebar->set_btn_label(btn, invalid_str);
|
||||
process_completed_with_error = true;
|
||||
}
|
||||
has_error = true;
|
||||
s_print_statuses[s_multiple_beds.get_active_bed()] = PrintStatus::invalid;
|
||||
}
|
||||
if (evt.cancelled()) {
|
||||
this->notification_manager->set_slicing_progress_canceled(_u8L("Slicing Cancelled."));
|
||||
s_print_statuses[s_multiple_beds.get_active_bed()] = PrintStatus::idle;
|
||||
}
|
||||
|
||||
this->sidebar->show_sliced_info_sizer(evt.success());
|
||||
if (evt.success()) {
|
||||
s_print_statuses[s_multiple_beds.get_active_bed()] = PrintStatus::finished;
|
||||
}
|
||||
|
||||
// This updates the "Slice now", "Export G-code", "Arrange" buttons status.
|
||||
// Namely, it refreshes the "Out of print bed" property of all the ModelObjects, and it enables
|
||||
@ -5904,7 +5983,7 @@ void Plater::export_gcode(bool prefer_removable)
|
||||
return;
|
||||
|
||||
|
||||
if (p->process_completed_with_error)
|
||||
if (!is_sliceable(s_print_statuses[s_multiple_beds.get_active_bed()]))
|
||||
return;
|
||||
|
||||
// If possible, remove accents from accented latin characters.
|
||||
@ -6395,7 +6474,7 @@ void Plater::export_toolpaths_to_obj() const
|
||||
void Plater::reslice()
|
||||
{
|
||||
// There is "invalid data" button instead "slice now"
|
||||
if (p->process_completed_with_error)
|
||||
if (!is_sliceable(s_print_statuses[s_multiple_beds.get_active_bed()]))
|
||||
return;
|
||||
|
||||
// In case SLA gizmo is in editing mode, refuse to continue
|
||||
|
@ -763,6 +763,7 @@ void Sidebar::on_select_preset(wxCommandEvent& evt)
|
||||
this->switch_from_autoslicing_mode();
|
||||
this->m_plater->regenerate_thumbnails();
|
||||
this->m_plater->update();
|
||||
s_print_statuses.fill(PrintStatus::idle);
|
||||
}
|
||||
|
||||
#ifdef __WXMSW__
|
||||
|
Loading…
x
Reference in New Issue
Block a user