From 008d13a59e214580d0c7b8462f738f636410d791 Mon Sep 17 00:00:00 2001 From: Clifford Garwood Date: Wed, 26 Aug 2026 08:25:25 -0400 Subject: [PATCH] fix(imex): keep the placement error across every validation path Plater::validate_current_plate() runs the same background_process.validate() as update_background_process(), and on success clears update_apply_result_invalid(false) and closes the ValidateError notification -- but it never consulted imex_placement_violation(). Any event reaching it wiped a live IMEX placement error and re-enabled the Slice button on a plate the slicer still refused; pressing Slice then hit the check in reslice() and returned early, so the job simply never started. Reproduce by clicking the bed with the prime tower overlapping a reserved area. Plater::select_plate_by_hover_id() -> select_plate() calls validate_current_plate() unconditionally, even when the clicked plate is already current, and deselects as a side effect -- which makes it look as though deselecting the tower cleared the error. Escape and clicks off the bed go through selection_changed(), which only renders and clears nothing. Extract the violation-to-message mapping into one helper and call it from both paths. model_fits is set alongside err.string in validate_current_plate, mirroring the missing-plugin block below it: the slice is already gated by m_apply_invalid, but leaving m_ready_for_slice true would trap a future consumer that reads it alone. These are the only two sites that matter. NotificationType::ValidateError has exactly three references in the tree and update_apply_result_invalid exactly four; the other slice-ready writers can only touch m_ready_for_slice, never m_apply_invalid, so they cannot re-enable Slice on their own. Three adjacent gaps are left alone, all pre-existing: "Slice all" is hard-coded always-enabled regardless of plate state; a slice-all batch halts silently at a violating plate because reslice() returns above the line that queues the advance; and object_list_changed() computes its own can_slice from geometry, harmless only because the result is ANDed with PartPlate::can_slice(). This is a hole in the shipped tower-zone check rather than a regression from rotating the tower hull -- it was simply invisible until a tower could be placed in violation. No automated gate: Plater is GUI-only, the helper is file-local and unlinkable, and imex_placement_violation() needs a live wxApp and preset bundle. "Both call sites consult it" is a call-graph property no unit test can express. The grep for a single imex_placement_violation reference is a future regression tripwire, not evidence this refactor happened -- it already returned 1 beforehand. Co-Authored-By: Claude Opus 5 (1M context) --- src/slic3r/GUI/Plater.cpp | 70 ++++++++++++++++++++++++++------------- 1 file changed, 47 insertions(+), 23 deletions(-) diff --git a/src/slic3r/GUI/Plater.cpp b/src/slic3r/GUI/Plater.cpp index 9aeef3ae76..38f2a9fd35 100644 --- a/src/slic3r/GUI/Plater.cpp +++ b/src/slic3r/GUI/Plater.cpp @@ -8897,6 +8897,40 @@ void Plater::priv::process_validation_warnings(const std::vectorimex_placement_violation()) { + case PartPlate::ImexPlacementViolation::Object: + return _u8L("Cannot slice: an object overlaps an area reserved for IMEX parallel printing " + "(a secondary zone, or a carriage clearance strip inside the primary zone)."); + case PartPlate::ImexPlacementViolation::PrimeTower: + return _u8L("Cannot slice: the prime tower overlaps an area reserved for IMEX parallel printing " + "(a secondary zone, or a carriage clearance strip inside the primary zone)."); + case PartPlate::ImexPlacementViolation::None: + break; + } + return {}; +} + +} // namespace + unsigned int Plater::priv::update_background_process(bool force_validation, bool postpone_error_messages, bool switch_print) { // bitmap of enum UpdateBackgroundProcessReturnState @@ -8994,29 +9028,8 @@ unsigned int Plater::priv::update_background_process(bool force_validation, bool q->post_process_string_object_exception(err); BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": validate err=%1%, warnings=%2%")%err.string%warnings.size(); - // IDEX/IQEX placement check: if anything overlaps a reserved area, treat it as a - // validation error so the standard pathway handles button state, notifications, - // and auto-slice blocking consistently. The message names both kinds of reserved - // area because the check covers secondary zones AND the carriage clearance strips, - // and the strips sit INSIDE the primary zone — saying "secondary zone" alone sends - // the user looking in the wrong half of the bed. - if (err.string.empty()) { - PartPlate* imex_plate = partplate_list.get_curr_plate(); - if (imex_plate) { - switch (imex_plate->imex_placement_violation()) { - case PartPlate::ImexPlacementViolation::Object: - err.string = _u8L("Cannot slice: an object overlaps an area reserved for IMEX parallel printing " - "(a secondary zone, or a carriage clearance strip inside the primary zone)."); - break; - case PartPlate::ImexPlacementViolation::PrimeTower: - err.string = _u8L("Cannot slice: the prime tower overlaps an area reserved for IMEX parallel printing " - "(a secondary zone, or a carriage clearance strip inside the primary zone)."); - break; - case PartPlate::ImexPlacementViolation::None: - break; - } - } - } + if (err.string.empty()) + err.string = imex_placement_error(partplate_list.get_curr_plate()); if (err.string.empty()) { this->partplate_list.get_curr_plate()->update_apply_result_invalid(false); @@ -19074,6 +19087,17 @@ void Plater::validate_current_plate(bool& model_fits, bool& validate_error) post_process_string_object_exception(err); BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": validate err=%1%, warnings=%2%, model_fits %3%")%err.string%warnings.size() %model_fits; + // The same check update_background_process() applies. Without it this path clears + // the invalid flag and closes the notification on a plate that is still unslicable. + // model_fits is set too, mirroring the missing-plugin block further down: the slice + // is already gated by m_apply_invalid, but leaving m_ready_for_slice true would be a + // trap for any future consumer that reads it on its own. + if (err.string.empty()) { + err.string = imex_placement_error(p->partplate_list.get_curr_plate()); + if (!err.string.empty()) + model_fits = false; + } + if (err.string.empty()) { p->partplate_list.get_curr_plate()->update_apply_result_invalid(false); p->notification_manager->set_all_slicing_errors_gray(true);