diff --git a/src/slic3r/GUI/DeviceCore/DevFilaSystem.cpp b/src/slic3r/GUI/DeviceCore/DevFilaSystem.cpp index 806af9d5f0..ee522a8241 100644 --- a/src/slic3r/GUI/DeviceCore/DevFilaSystem.cpp +++ b/src/slic3r/GUI/DeviceCore/DevFilaSystem.cpp @@ -752,9 +752,9 @@ void DevFilaSystemParser::ParseV1_0(const json& jj, MachineObject* obj, DevFilaS { curr_tray->remain = -1; } - if (tray_it->contains("tray_slot_placeholder")) { - curr_tray->is_slot_placeholder = true; - } + // The tray objects are reused across status updates. Reset this + // state when a previously empty slot receives a filament again. + curr_tray->is_slot_placeholder = tray_it->contains("tray_slot_placeholder"); int ams_id_int = 0; int tray_id_int = 0; try @@ -989,4 +989,4 @@ void DevFilaSystemParser::ParseAgentFilament(const json& data, MachineObject* ob } } -} \ No newline at end of file +} diff --git a/src/slic3r/GUI/Plater.cpp b/src/slic3r/GUI/Plater.cpp index 5b06db9d3e..583f7d1528 100644 --- a/src/slic3r/GUI/Plater.cpp +++ b/src/slic3r/GUI/Plater.cpp @@ -4336,11 +4336,30 @@ void Sidebar::load_ams_list(MachineObject* obj) filament_ams_list = build_filament_ams_list(obj); } - bool device_change = false; const std::string& device = obj ? obj->get_dev_id() : ""; - if (p->ams_list_device != device) { + const bool same_device = p->ams_list_device == device; + + // Keep sync metadata out of the device payload, but preserve it across a + // subscription refresh when the physical filament in a slot is unchanged. + // Otherwise the refreshed configs differ only by the missing + // filament_changed key, causing combo boxes to rebuild and lose their + // transient post-sync badges. + auto &previous_filament_ams_list = wxGetApp().preset_bundle->filament_ams_list; + for (auto &entry : filament_ams_list) { + auto previous = previous_filament_ams_list.find(entry.first); + const auto *previous_changed = previous == previous_filament_ams_list.end() ? nullptr : + dynamic_cast(previous->second.option("filament_changed")); + if (!same_device || previous_changed == nullptr || + previous->second.opt_string("filament_id", 0u) != entry.second.opt_string("filament_id", 0u)) { + continue; + } + entry.second.set_key_value("filament_changed", + new ConfigOptionBool{previous_changed->value}); + } + + bool device_change = !same_device; + if (device_change) { p->ams_list_device = device; - device_change = true; } BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": %1% items") % filament_ams_list.size(); if (wxGetApp().preset_bundle->filament_ams_list == filament_ams_list && !device_change) @@ -4350,9 +4369,27 @@ void Sidebar::load_ams_list(MachineObject* obj) wxGetApp().preset_bundle->filament_ams_list = filament_ams_list; for (auto c : p->combos_filament){ + c->set_sync_badge(false); c->update(); - if (device_change) { - c->ShowBadge(false);//change printer,then clear badge + } + + if (!device_change) { + size_t combo_index = 0; + for (const auto &entry : filament_ams_list) { + const auto &tray = entry.second; + const bool has_filament = !tray.opt_string("filament_id", 0u).empty(); + const bool is_placeholder = tray.has("filament_slot_placeholder") && + tray.opt_bool("filament_slot_placeholder", 0u); + if (!has_filament && !is_placeholder) { + continue; + } + if (combo_index >= p->combos_filament.size()) { + break; + } + const auto *filament_changed = dynamic_cast(tray.option("filament_changed")); + p->combos_filament[combo_index]->set_sync_badge( + has_filament && !is_placeholder && filament_changed != nullptr && filament_changed->value); + ++combo_index; } } @@ -4526,18 +4563,32 @@ void Sidebar::sync_ams_list(bool is_from_big_sync_btn) auto tip = sync_color_only ? _L("Only filament color information has been synchronized from printer.") : _L("Filament type and color information have been synchronized, but slot information is not included."); c->SetToolTip(tip); - c->ShowBadge(true); + c->set_sync_badge(true); }; { // badge ams filament clear_combos_filament_badge(); if (sync_result.direct_sync) { - // Orca: PresetBundle::sync_ams_list rebuilds combos_filament - // 1:1 from the AMS trays that produce a combo (loaded trays + placeholders; non-placeholder - // empty trays are skipped), so every resulting combo is AMS-sourced and gets a badge. The - // previous per-tray index walked the full filament_ams_list (including the skipped empties), - // so an empty slot before a loaded one dropped the badge for the trailing filaments. - for (auto &c : p->combos_filament) { - badge_combox_filament(c); + // A placeholder contributes a preserved project filament to the + // overwrite result, but it is not AMS-sourced and must not get a + // sync badge. Non-placeholder empty trays are omitted entirely. + size_t combo_index = 0; + for (const auto &entry : wxGetApp().preset_bundle->filament_ams_list) { + const auto &tray = entry.second; + const bool has_filament = !tray.opt_string("filament_id", 0u).empty(); + const bool is_placeholder = tray.has("filament_slot_placeholder") && + tray.opt_bool("filament_slot_placeholder", 0u); + if (!has_filament && !is_placeholder) { + continue; + } + if (combo_index >= p->combos_filament.size()) { + break; + } + if (is_placeholder) { + p->combos_filament[combo_index]->set_sync_badge(false); + } else { + badge_combox_filament(p->combos_filament[combo_index]); + } + ++combo_index; } } } @@ -4935,17 +4986,14 @@ void Sidebar::clear_combos_filament_badge() { auto &combos_filament = p->combos_filament; for (auto &c : combos_filament) { // clear flag - c->ShowBadge(false); + c->set_sync_badge(false); } } void Sidebar::udpate_combos_filament_badge() { auto &combos_filament = p->combos_filament; for (auto &c : combos_filament) { - auto selection = c->GetSelection(); - auto select_flag = c->GetFlag(selection); - auto ok = select_flag == (int) PresetComboBox::FilamentAMSType::FROM_AMS; - c->ShowBadge(ok); + c->update_badge_according_flag(); } } @@ -10485,7 +10533,7 @@ void Plater::priv::on_select_preset(wxCommandEvent &evt) sidebar->auto_calc_flushing_volumes(idx); } auto select_flag = combo->GetFlag(selection); - combo->ShowBadge(select_flag == (int)PresetComboBox::FilamentAMSType::FROM_AMS); + combo->set_sync_badge(select_flag == (int)PresetComboBox::FilamentAMSType::FROM_AMS); q->on_filament_change(idx); } bool select_preset = !combo->selection_is_changed_according_to_physical_printers(); diff --git a/src/slic3r/GUI/PresetComboBoxes.cpp b/src/slic3r/GUI/PresetComboBoxes.cpp index c979fc3212..27653790dd 100644 --- a/src/slic3r/GUI/PresetComboBoxes.cpp +++ b/src/slic3r/GUI/PresetComboBoxes.cpp @@ -994,7 +994,13 @@ void PlaterPresetComboBox::update_badge_according_flag() { auto selection = GetSelection(); auto select_flag = GetFlag(selection); auto ok = select_flag == (int) PresetComboBox::FilamentAMSType::FROM_AMS; - ShowBadge(ok); + ShowBadge(m_sync_badge || ok); +} + +void PlaterPresetComboBox::set_sync_badge(bool show) +{ + m_sync_badge = show; + ShowBadge(show); } bool PlaterPresetComboBox::switch_to_tab() diff --git a/src/slic3r/GUI/PresetComboBoxes.hpp b/src/slic3r/GUI/PresetComboBoxes.hpp index 53644cecf5..dcd5457dfd 100644 --- a/src/slic3r/GUI/PresetComboBoxes.hpp +++ b/src/slic3r/GUI/PresetComboBoxes.hpp @@ -205,6 +205,7 @@ public: void msw_rescale() override; void OnSelect(wxCommandEvent& evt) override; void update_badge_according_flag(); + void set_sync_badge(bool show); FilamentColor get_cur_color_info(); void show_default_color_picker(); @@ -214,6 +215,7 @@ public: private: // BBS wxColor m_color; + bool m_sync_badge{false}; }; diff --git a/tests/slic3rutils/test_dev_mapping.cpp b/tests/slic3rutils/test_dev_mapping.cpp index 1c1528f818..658117d2e4 100644 --- a/tests/slic3rutils/test_dev_mapping.cpp +++ b/tests/slic3rutils/test_dev_mapping.cpp @@ -26,6 +26,37 @@ using json = nlohmann::json; using namespace Slic3r; +TEST_CASE("AMS tray placeholder state follows the latest status", "[DevFilaSystem]") +{ + MachineObject obj(nullptr, nullptr, "test", "test_dev", "127.0.0.1"); + + const json empty_slot = json::parse(R"({ + "ams": { + "tray_exist_bits": "0", + "ams": [ { "id": "0", "info": "00000001", "tray": [ + { "id": "0", "tray_slot_placeholder": "1", "tray_color": "00000000" } + ] } ] + } + })"); + DevFilaSystemParser::ParseV1_0(empty_slot, &obj, obj.GetFilaSystem().get(), false); + + DevAmsTray* tray = obj.GetFilaSystem()->GetAmsTray("0", "0"); + REQUIRE(tray != nullptr); + REQUIRE(tray->is_slot_placeholder); + + const json loaded_slot = json::parse(R"({ + "ams": { + "tray_exist_bits": "1", + "ams": [ { "id": "0", "info": "00000001", "tray": [ + { "id": "0", "tray_color": "FF0000FF" } + ] } ] + } + })"); + DevFilaSystemParser::ParseV1_0(loaded_slot, &obj, obj.GetFilaSystem().get(), false); + + CHECK_FALSE(tray->is_slot_placeholder); +} + TEST_CASE("Switch-bound AMS trays map to the left extruder", "[DevMapping]") { MachineObject obj(nullptr, nullptr, "test", "test_dev", "127.0.0.1"); diff --git a/tests/slic3rutils/test_printer_agent.cpp b/tests/slic3rutils/test_printer_agent.cpp index e77ff14a29..1d9948164b 100644 --- a/tests/slic3rutils/test_printer_agent.cpp +++ b/tests/slic3rutils/test_printer_agent.cpp @@ -8,6 +8,9 @@ #include #include +#include +#include +#include #include #include @@ -69,6 +72,45 @@ TEST_CASE("unit: Moonraker reports untranslated commands as not supported", "[un CHECK(agent.send_message("dev", "{not json", 0, 0) == BAMBU_NETWORK_ERR_INVALID_RESULT); } +TEST_CASE("unit: MoonrakerPrinterAgent::fetch_filament_info is fire-and-forget and dispatches to the derived override", + "[unit][moonraker]") +{ + class RecordingAgent : public Slic3r::MoonrakerPrinterAgent + { + public: + explicit RecordingAgent(std::string log_dir) : MoonrakerPrinterAgent(std::move(log_dir)) {} + + std::atomic invoked{false}; + std::promise release_gate; + std::promise done_promise; + + bool do_fetch_filament_info(std::string /*dev_id*/) override + { + invoked.store(true); + // Block here until the test explicitly releases us, proving the caller + // (fetch_filament_info) does not wait for this to run. + release_gate.get_future().wait(); + done_promise.set_value(); + return true; + } + }; + + auto agent = std::make_shared(std::string{}); + auto done_future = agent->done_promise.get_future(); + + bool immediate_result = agent->fetch_filament_info("test-dev"); + + // fetch_filament_info must return before do_fetch_filament_info completes — prove + // it by confirming the background call is still blocked on the gate right now. + REQUIRE(immediate_result == true); + REQUIRE(done_future.wait_for(std::chrono::milliseconds(100)) == std::future_status::timeout); + + // Now let the background call finish and confirm it actually ran (polymorphic dispatch). + agent->release_gate.set_value(); + REQUIRE(done_future.wait_for(std::chrono::seconds(2)) == std::future_status::ready); + REQUIRE(agent->invoked.load() == true); +} + // =========================================================================== // UNIT - printer-agent registry duplicate handling. // Confirms a duplicate agent id is rejected so a plugin cannot shadow a built-in