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:
Martin Šach 2024-12-10 15:50:28 +01:00 committed by Lukas Matena
parent da10a4fc8a
commit 0dcc654d39
5 changed files with 173 additions and 64 deletions

View File

@ -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())};

View File

@ -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;

View File

@ -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());
}

View File

@ -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

View File

@ -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__