#include "WebGuideDialog.hpp" #include "ConfigWizard.hpp" #include #include #include #include #include #include #include "I18N.hpp" #include "libslic3r/AppConfig.hpp" #include "libslic3r/Config.hpp" #include "libslic3r/Preset.hpp" #include "libslic3r/PresetBundle.hpp" #include "slic3r/GUI/wxExtensions.hpp" #include "slic3r/GUI/GUI_App.hpp" #include "libslic3r_version.h" #include #include #include #include #include #include #include #include #include #include #include #include #include "MainFrame.hpp" #include #include #include #include #include #include "CreatePresetsDialog.hpp" using namespace nlohmann; namespace Slic3r { namespace GUI { static std::string guide_json_cache_version_key(const boost::filesystem::path& rsrc_dir, const boost::filesystem::path& user_dir) { std::vector parts; auto collect = [&](const boost::filesystem::path& dir) { boost::system::error_code ec; if (!boost::filesystem::exists(dir, ec)) return; for (const auto& e : boost::filesystem::directory_iterator(dir, ec)) { if (e.path().extension().string() != ".json") continue; const std::string k = get_vendor_cache_key(e.path().string()); if (!k.empty()) parts.push_back(e.path().filename().string() + "=" + k); } }; collect(rsrc_dir); if (user_dir != rsrc_dir) collect(user_dir); std::sort(parts.begin(), parts.end()); std::string result; for (const auto& p : parts) { result += p; result += ';'; } return result; } static boost::filesystem::path guide_json_cache_path() { return boost::filesystem::path(data_dir()) / "guide_profile_cache.json"; } static wxString update_custom_filaments() { json m_Res = json::object(); m_Res["command"] = "update_custom_filaments"; m_Res["sequence_id"] = "2000"; json m_CustomFilaments = json::array(); PresetBundle * preset_bundle = wxGetApp().preset_bundle; std::map> temp_filament_id_to_presets = preset_bundle->filaments.get_filament_presets(); std::vector> need_sort; bool need_delete_some_filament = false; for (std::pair> filament_id_to_presets : temp_filament_id_to_presets) { std::string filament_id = filament_id_to_presets.first; if (filament_id.empty()) continue; if (filament_id == "null") { need_delete_some_filament = true; } bool filament_with_base_id = false; bool not_need_show = false; std::string filament_name; for (const Preset *preset : filament_id_to_presets.second) { if (preset->is_system || preset->is_project_embedded) { not_need_show = true; break; } if (preset->inherits() != "") continue; if (!preset->base_id.empty()) filament_with_base_id = true; if (!not_need_show) { auto filament_vendor = dynamic_cast(const_cast(preset)->config.option("filament_vendor", false)); if (filament_vendor && filament_vendor->values.size() && filament_vendor->values[0] == "Generic") not_need_show = true; } if (filament_name.empty()) { std::string preset_name = preset->name; size_t index_at = preset_name.find(" @"); if (std::string::npos != index_at) { preset_name = preset_name.substr(0, index_at); } filament_name = preset_name; } } if (not_need_show) continue; if (!filament_name.empty()) { if (filament_with_base_id) { need_sort.push_back(std::make_pair(into_u8(_L("[Action Required] ")) + filament_name, filament_id)); } else { need_sort.push_back(std::make_pair(filament_name, filament_id)); } } } std::sort(need_sort.begin(), need_sort.end(), [](const std::pair &a, const std::pair &b) { return a.first < b.first; }); if (need_delete_some_filament) { need_sort.push_back(std::make_pair(into_u8(_L("[Action Required]")), "null")); } json temp_j; for (std::pair &filament_name_to_id : need_sort) { temp_j["name"] = filament_name_to_id.first; temp_j["id"] = filament_name_to_id.second; m_CustomFilaments.push_back(temp_j); } m_Res["data"] = m_CustomFilaments; wxString strJS = wxString::Format("HandleStudio(%s)", wxString::FromUTF8(m_Res.dump(-1, ' ', false, json::error_handler_t::ignore))); return strJS; } GuideFrame::GuideFrame(GUI_App *pGUI, long style) : DPIDialog((wxWindow *) (pGUI->mainframe), wxID_ANY, "OrcaSlicer", wxDefaultPosition, wxDefaultSize, style), m_appconfig_new() { SetBackgroundColour(*wxWHITE); // INI m_SectionName = "firstguide"; PrivacyUse = false; StealthMode = false; InstallNetplugin = false; m_MainPtr = pGUI; // set the frame icon wxBoxSizer *topsizer = new wxBoxSizer(wxVERTICAL); wxString TargetUrl = SetStartPage(BBL_WELCOME, false); BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(", set start page to welcome "); // Create the webview m_browser = WebView::CreateWebView(this, TargetUrl); if (m_browser == nullptr) { wxLogError("Could not init m_browser"); return; } m_browser->Hide(); m_browser->SetSize(0, 0); SetSizer(topsizer); topsizer->Add(m_browser, wxSizerFlags().Expand().Proportion(1)); // Log backend information // wxLogMessage(wxWebView::GetBackendVersionInfo().ToString()); // wxLogMessage("Backend: %s Version: %s", // m_browser->GetClassInfo()->GetClassName(),wxWebView::GetBackendVersionInfo().ToString()); // wxLogMessage("User Agent: %s", m_browser->GetUserAgent()); // Set a more sensible size for web browsing wxSize pSize = FromDIP(wxSize(820, 660)); SetSize(pSize); int screenheight = wxSystemSettings::GetMetric(wxSYS_SCREEN_Y, NULL); int screenwidth = wxSystemSettings::GetMetric(wxSYS_SCREEN_X, NULL); int MaxY = (screenheight - pSize.y) > 0 ? (screenheight - pSize.y) / 2 : 0; wxPoint tmpPT((screenwidth - pSize.x) / 2, MaxY); Move(tmpPT); #ifdef __WXMSW__ this->Bind(wxEVT_CHAR_HOOK, [this](wxKeyEvent& e) { if ((m_page == BBL_FILAMENT_ONLY || m_page == BBL_MODELS_ONLY) && e.GetKeyCode() == WXK_ESCAPE) { if (this->IsModal()) this->EndModal(wxID_CANCEL); else this->Close(); } else e.Skip(); }); #endif // Connect the webview events Bind(wxEVT_WEBVIEW_NAVIGATING, &GuideFrame::OnNavigationRequest, this, m_browser->GetId()); Bind(wxEVT_WEBVIEW_NAVIGATED, &GuideFrame::OnNavigationComplete, this, m_browser->GetId()); Bind(wxEVT_WEBVIEW_LOADED, &GuideFrame::OnDocumentLoaded, this, m_browser->GetId()); Bind(wxEVT_WEBVIEW_ERROR, &GuideFrame::OnError, this, m_browser->GetId()); Bind(wxEVT_WEBVIEW_NEWWINDOW, &GuideFrame::OnNewWindow, this, m_browser->GetId()); Bind(wxEVT_WEBVIEW_TITLE_CHANGED, &GuideFrame::OnTitleChanged, this, m_browser->GetId()); Bind(wxEVT_WEBVIEW_FULLSCREEN_CHANGED, &GuideFrame::OnFullScreenChanged, this, m_browser->GetId()); Bind(wxEVT_WEBVIEW_SCRIPT_MESSAGE_RECEIVED, &GuideFrame::OnScriptMessage, this, m_browser->GetId()); // Connect the idle events // Bind(wxEVT_IDLE, &GuideFrame::OnIdle, this); // Bind(wxEVT_CLOSE_WINDOW, &GuideFrame::OnClose, this); // UI SetStartPage(BBL_REGION); BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(", finished"); wxGetApp().UpdateDlgDarkUI(this); } GuideFrame::~GuideFrame() { m_destroy = true; *m_cancel_token = true; // signal any queued CallAfter lambdas before join if (m_load_task && m_load_task->joinable()) m_load_task->join(); m_load_task.reset(); if (m_browser) { delete m_browser; m_browser = nullptr; } } void GuideFrame::load_url(wxString &url) { BOOST_LOG_TRIVIAL(trace) << __FUNCTION__<< " enter, url=" << url.ToStdString(); WebView::LoadUrl(m_browser, url); m_browser->SetFocus(); UpdateState(); BOOST_LOG_TRIVIAL(info) << __FUNCTION__<< " exit"; } wxString GuideFrame::SetStartPage(GuidePage startpage, bool load) { m_page = startpage; BOOST_LOG_TRIVIAL(info) << __FUNCTION__<< boost::format(" enter, load=%1%, start_page=%2%")%load%int(startpage); //wxLogMessage("GUIDE: webpage_1 %s", (boost::filesystem::path(resources_dir()) / "web\\guide\\1\\index.html").make_preferred().string().c_str() ); wxString TargetUrl = from_u8( (boost::filesystem::path(resources_dir()) / "web/guide/0/index.html?target=1").make_preferred().string() ); //wxLogMessage("GUIDE: webpage_2 %s", TargetUrl.mb_str()); if (startpage == BBL_WELCOME){ SetTitle(_L("Setup Wizard")); TargetUrl = from_u8((boost::filesystem::path(resources_dir()) / "web/guide/0/index.html?target=1").make_preferred().string()); } else if (startpage == BBL_REGION) { SetTitle(_L("Setup Wizard")); TargetUrl = from_u8((boost::filesystem::path(resources_dir()) / "web/guide/0/index.html?target=11").make_preferred().string()); } else if (startpage == BBL_MODELS) { SetTitle(_L("Setup Wizard")); TargetUrl = from_u8((boost::filesystem::path(resources_dir()) / "web/guide/0/index.html?target=21").make_preferred().string()); } else if (startpage == BBL_FILAMENTS) { SetTitle(_L("Setup Wizard")); int nSize = m_ProfileJson["model"].size(); if (nSize>0) TargetUrl = from_u8((boost::filesystem::path(resources_dir()) / "web/guide/0/index.html?target=22").make_preferred().string()); else TargetUrl = from_u8((boost::filesystem::path(resources_dir()) / "web/guide/0/index.html?target=21").make_preferred().string()); } else if (startpage == BBL_FILAMENT_ONLY) { SetTitle(""); TargetUrl = from_u8((boost::filesystem::path(resources_dir()) / "web/guide/0/index.html?target=23").make_preferred().string()); } else if (startpage == BBL_MODELS_ONLY) { SetTitle(""); TargetUrl = from_u8((boost::filesystem::path(resources_dir()) / "web/guide/0/index.html?target=24").make_preferred().string()); } else { SetTitle(_L("Setup Wizard")); TargetUrl = from_u8((boost::filesystem::path(resources_dir()) / "web/guide/0/index.html?target=21").make_preferred().string()); } wxString strlang = wxGetApp().current_language_code_safe(); BOOST_LOG_TRIVIAL(info) << __FUNCTION__<< boost::format(", strlang=%1%") % into_u8(strlang); if (strlang != "") TargetUrl = wxString::Format("%s&lang=%s", w2s(TargetUrl), strlang); TargetUrl = "file://" + TargetUrl; if (load) load_url(TargetUrl); BOOST_LOG_TRIVIAL(info) << __FUNCTION__<< " exit"; return TargetUrl; } /** * Method that retrieves the current state from the web control and updates * the GUI the reflect this current state. */ void GuideFrame::UpdateState() { // SetTitle(m_browser->GetCurrentTitle()); } void GuideFrame::OnIdle(wxIdleEvent &WXUNUSED(evt)) { if (m_browser->IsBusy()) { wxSetCursor(wxCURSOR_ARROWWAIT); } else { wxSetCursor(wxNullCursor); } } // void GuideFrame::OnClose(wxCloseEvent& evt) //{ // this->Hide(); //} /** * Callback invoked when there is a request to load a new page (for instance * when the user clicks a link) */ void GuideFrame::OnNavigationRequest(wxWebViewEvent &evt) { // wxLogMessage("%s", "Navigation request to '" + evt.GetURL() + "' // (target='" + evt.GetTarget() + "')"); UpdateState(); } /** * Callback invoked when a navigation request was accepted */ void GuideFrame::init_guide_paths() { m_ProfileJson = json::parse("{}"); m_ProfileJson["model"] = json::array(); m_ProfileJson["machine"] = json::object(); m_ProfileJson["filament"] = json::object(); m_ProfileJson["process"] = json::array(); vendor_dir = (boost::filesystem::path(Slic3r::data_dir()) / PRESET_SYSTEM_DIR).make_preferred(); rsrc_vendor_dir = (boost::filesystem::path(resources_dir()) / "profiles").make_preferred(); orca_bundle_rsrc = true; if (boost::filesystem::exists(vendor_dir)) { for (const auto& entry : boost::filesystem::directory_iterator(vendor_dir)) { if (!boost::filesystem::is_directory(entry) && boost::iequals(entry.path().extension().string(), ".json") && !boost::iequals(entry.path().stem().string(), PresetBundle::ORCA_FILAMENT_LIBRARY)) { orca_bundle_rsrc = false; break; } } } auto lib_json = boost::filesystem::path(PresetBundle::ORCA_FILAMENT_LIBRARY).replace_extension(".json"); m_OrcaFilaLibPath = boost::filesystem::exists(vendor_dir / lib_json) ? (vendor_dir / PresetBundle::ORCA_FILAMENT_LIBRARY).string() : (rsrc_vendor_dir / PresetBundle::ORCA_FILAMENT_LIBRARY).string(); } void GuideFrame::on_profile_loaded() { // Must be called on the main thread. SaveProfileData(); const std::string strAll = m_ProfileJson.dump(-1, ' ', false, json::error_handler_t::ignore); BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << ", finished, json contents:\n" << strAll; json res; res["command"] = "userguide_profile_load_finish"; res["sequence_id"] = "10001"; RunScript(wxString::Format("HandleStudio(%s)", res.dump(-1, ' ', true))); } void GuideFrame::OnNavigationComplete(wxWebViewEvent &evt) { //wxLogMessage("%s", "Navigation complete; url='" + evt.GetURL() + "'"); if (!bFirstComplete) { bFirstComplete = true; try { init_guide_paths(); if (BuildProfileDataFromPresetBundle()) { // Persist so future opens that start before preset_bundle is ready // can skip the slower loading paths. Capture by value so the // thread is safe even if the dialog closes before it finishes. boost::thread([data = m_ProfileJson, rsrc = rsrc_vendor_dir, user = vendor_dir] { try { json cache; cache["version"] = guide_json_cache_version_key(rsrc, user); if (cache["version"].get().empty()) return; json base = data; for (auto& entry : base["model"]) entry["nozzle_selected"] = ""; cache["data"] = std::move(base); boost::nowide::ofstream ofs(guide_json_cache_path().string()); ofs << cache.dump(-1, ' ', false, json::error_handler_t::ignore); BOOST_LOG_TRIVIAL(info) << "GuideFrame: guide JSON cache saved"; } catch (const std::exception& e) { BOOST_LOG_TRIVIAL(warning) << "GuideFrame: failed to save guide JSON cache: " << e.what(); } }).detach(); if (!m_destroy) on_profile_loaded(); } else { // Presets not yet in memory — delegate to background thread. m_load_task = std::make_unique(boost::bind(&GuideFrame::LoadProfileData, this)); } } catch (const std::exception& e) { BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << ", init error: " << e.what(); m_load_task = std::make_unique(boost::bind(&GuideFrame::LoadProfileData, this)); } } m_browser->Show(); Layout(); wxString NewUrl = evt.GetURL(); UpdateState(); } /** * Callback invoked when a page is finished loading */ void GuideFrame::OnDocumentLoaded(wxWebViewEvent &evt) { // Only notify if the document is the main frame, not a subframe wxString tmpUrl = evt.GetURL(); wxString NowUrl = m_browser->GetCurrentURL(); if (evt.GetURL() == m_browser->GetCurrentURL()) { // wxLogMessage("%s", "Document loaded; url='" + evt.GetURL() + "'"); } UpdateState(); // wxCommandEvent *event = new // wxCommandEvent(EVT_WEB_RESPONSE_MESSAGE,this->GetId()); wxQueueEvent(this, // event); } /** * On new window, we veto to stop extra windows appearing */ void GuideFrame::OnNewWindow(wxWebViewEvent &evt) { wxString flag = " (other)"; wxString NewUrl= evt.GetURL(); wxLaunchDefaultBrowser(NewUrl); //if (evt.GetNavigationAction() == wxWEBVIEW_NAV_ACTION_USER) { flag = " (user)"; } // wxLogMessage("%s", "New window; url='" + evt.GetURL() + "'" + flag); // If we handle new window events then just load them in this window as we // are a single window browser // if (m_tools_handle_new_window->IsChecked()) // m_browser->LoadURL(evt.GetURL()); UpdateState(); } void GuideFrame::OnTitleChanged(wxWebViewEvent &evt) { // SetTitle(evt.GetString()); // wxLogMessage("%s", "Title changed; title='" + evt.GetString() + "'"); } void GuideFrame::OnFullScreenChanged(wxWebViewEvent &evt) { // wxLogMessage("Full screen changed; status = %d", evt.GetInt()); ShowFullScreen(evt.GetInt() != 0); } void GuideFrame::OnScriptMessage(wxWebViewEvent &evt) { try { wxString strInput = evt.GetString(); BOOST_LOG_TRIVIAL(trace) << "GuideFrame::OnScriptMessage;OnRecv:" << strInput.c_str(); json j = json::parse(strInput.utf8_string()); wxString strCmd = j["command"]; BOOST_LOG_TRIVIAL(trace) << "GuideFrame::OnScriptMessage;Command:" << strCmd; if (strCmd == "close_page") { this->EndModal(wxID_CANCEL); } if (strCmd == "user_clause") { wxString strAction = j["data"]["action"]; if (strAction == "refuse") { // CloseTheApp this->EndModal(wxID_OK); m_MainPtr->mainframe->Close(); // Refuse Clause, App quit immediately } } else if (strCmd == "user_private_choice") { wxString strAction = j["data"]["action"]; if (strAction == "agree") { PrivacyUse = true; } else { PrivacyUse = false; } } else if (strCmd == "request_userguide_profile") { json m_Res = json::object(); m_Res["command"] = "response_userguide_profile"; m_Res["sequence_id"] = "10001"; m_Res["response"] = m_ProfileJson; //wxString strJS = wxString::Format("HandleStudio(%s)", m_Res.dump(-1, ' ', false, json::error_handler_t::ignore)); wxString strJS = wxString::Format("HandleStudio(%s)", m_Res.dump(-1, ' ', true)); BOOST_LOG_TRIVIAL(trace) << "GuideFrame::OnScriptMessage;request_userguide_profile:" << strJS.c_str(); wxGetApp().CallAfter([this,strJS] { RunScript(strJS); }); } else if (strCmd == "request_custom_filaments") { wxString strJS = update_custom_filaments(); wxGetApp().CallAfter([this, strJS] { RunScript(strJS); }); } else if (strCmd == "create_custom_filament") { this->EndModal(wxID_OK); wxQueueEvent(wxGetApp().plater(), new SimpleEvent(EVT_CREATE_FILAMENT)); } else if (strCmd == "modify_custom_filament") { m_editing_filament_id = j["id"]; this->EndModal(wxID_EDIT); } else if (strCmd == "save_userguide_models") { json MSelected = j["data"]; int nModel = m_ProfileJson["model"].size(); for (int m = 0; m < nModel; m++) { json TmpModel = m_ProfileJson["model"][m]; m_ProfileJson["model"][m]["nozzle_selected"] = ""; for (auto it = MSelected.begin(); it != MSelected.end(); ++it) { json OneSelect = it.value(); wxString s1 = TmpModel["model"]; wxString s2 = OneSelect["model"]; if (s1.compare(s2) == 0) { m_ProfileJson["model"][m]["nozzle_selected"] = m_ProfileJson["model"][m]["nozzle_diameter"]; // Automatically select default materials for this printer model // This mirrors the behavior of the old ConfigWizard::select_default_materials_for_printer_model() if (TmpModel.contains("materials") && !TmpModel["materials"].is_null()) { std::string materials_str; // Handle both string and JSON array formats for materials if (TmpModel["materials"].is_string()) { materials_str = TmpModel["materials"].get(); } else if (TmpModel["materials"].is_array()) { // Convert JSON array to semicolon-separated string for unescape_strings_cstyle for (const auto& material : TmpModel["materials"]) { if (!materials_str.empty()) materials_str += ";"; materials_str += material.get(); } } else { materials_str = ""; } boost::trim(materials_str); BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << " Processing default_materials for printer: " << s1.ToStdString() << " - materials: " << materials_str; // Use the same parsing logic as ConfigWizard::select_default_materials_for_printer_model() // This calls unescape_strings_cstyle() just like Preset.cpp:298 does std::vector materials; if (Slic3r::unescape_strings_cstyle(materials_str, materials)) { for (const std::string& material : materials) { if (!material.empty()) { // Mark this filament as selected if it exists in our filament list // This mirrors appconfig_new.set(section, material, "true") from ConfigWizard.cpp:2150 if (m_ProfileJson["filament"].contains(material)) { m_ProfileJson["filament"][material]["selected"] = 1; BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << " Automatically selected default filament: " << material; } else { BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << " Default filament '" << material << "' not found in available filaments for printer: " << s1.ToStdString(); } } } } else { BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << " Malformed default_materials field: " << materials_str << " for printer: " << s1.ToStdString(); } } else { BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << " No default_materials defined for printer: " << s1.ToStdString(); } break; } } } } else if (strCmd == "save_userguide_filaments") { //reset for (auto it = m_ProfileJson["filament"].begin(); it != m_ProfileJson["filament"].end(); ++it) { m_ProfileJson["filament"][it.key()]["selected"] = 0; } json fSelected = j["data"]["filament"]; int nF = fSelected.size(); for (int m = 0; m < nF; m++) { std::string fName = fSelected[m]; m_ProfileJson["filament"][fName]["selected"] = 1; } } else if (strCmd == "user_guide_finish") { SaveProfile(); std::string oldregion = m_ProfileJson["region"]; if (m_Region != oldregion) { AppConfig* config = GUI::wxGetApp().app_config; std::string country_code = config->get_country_code(); NetworkAgent* agent = wxGetApp().getAgent(); if (agent) { agent->set_country_code(country_code); if (wxGetApp().is_user_login()) { BOOST_LOG_TRIVIAL(info) << "logout: user_logout on user_guide_finish"; // agent->user_logout(); wxGetApp().request_user_logout(); } } } this->EndModal(wxID_OK); if (InstallNetplugin) GUI::wxGetApp().CallAfter([] { GUI::wxGetApp().ShowDownNetPluginDlg(); }); } else if (strCmd == "user_guide_create_printer") { this->EndModal(wxID_CANCEL); this->Close(); GUI::wxGetApp().CallAfter([this] {GUI::wxGetApp().sidebar().create_printer_preset();}); } else if (strCmd == "user_guide_cancel") { this->EndModal(wxID_CANCEL); this->Close(); } else if (strCmd == "save_region") { m_Region = j["region"]; } else if (strCmd == "network_plugin_install") { std::string sAction = j["data"]["action"]; if (sAction == "yes") { if (!network_plugin_ready) InstallNetplugin = true; else //already ready InstallNetplugin = false; } else InstallNetplugin = false; } else if (strCmd == "save_stealth_mode") { wxString strAction = j["data"]["action"]; if (strAction == "yes") { StealthMode = true; } else { StealthMode = false; } } } catch (std::exception &e) { // wxMessageBox(e.what(), "json Exception", MB_OK); BOOST_LOG_TRIVIAL(trace) << "GuideFrame::OnScriptMessage;Error:" << e.what(); } wxString strAll = m_ProfileJson.dump(-1,' ',false, json::error_handler_t::ignore); } void GuideFrame::RunScript(const wxString &javascript) { // Remember the script we run in any case, so the next time the user opens // the "Run Script" dialog box, it is shown there for convenient updating. //m_javascript = javascript; // wxLogMessage("Running JavaScript:\n%s\n", javascript); if (!m_browser) return; WebView::RunScript(m_browser, javascript); } #if wxUSE_WEBVIEW_IE void GuideFrame::OnRunScriptObjectWithEmulationLevel(wxCommandEvent &WXUNUSED(evt)) { wxWebViewIE::MSWSetModernEmulationLevel(); RunScript("function f(){var person = new Object();person.name = 'Foo'; \ person.lastName = 'Bar';return person;}f();"); wxWebViewIE::MSWSetModernEmulationLevel(false); } void GuideFrame::OnRunScriptDateWithEmulationLevel(wxCommandEvent &WXUNUSED(evt)) { wxWebViewIE::MSWSetModernEmulationLevel(); RunScript("function f(){var d = new Date('10/08/2017 21:30:40'); \ var tzoffset = d.getTimezoneOffset() * 60000; return \ new Date(d.getTime() - tzoffset);}f();"); wxWebViewIE::MSWSetModernEmulationLevel(false); } void GuideFrame::OnRunScriptArrayWithEmulationLevel(wxCommandEvent &WXUNUSED(evt)) { wxWebViewIE::MSWSetModernEmulationLevel(); RunScript("function f(){ return [\"foo\", \"bar\"]; }f();"); wxWebViewIE::MSWSetModernEmulationLevel(false); } #endif /** * Callback invoked when a loading error occurs */ void GuideFrame::OnError(wxWebViewEvent &evt) { #define WX_ERROR_CASE(type) \ case type: category = #type; break; wxString category; switch (evt.GetInt()) { WX_ERROR_CASE(wxWEBVIEW_NAV_ERR_CONNECTION); WX_ERROR_CASE(wxWEBVIEW_NAV_ERR_CERTIFICATE); WX_ERROR_CASE(wxWEBVIEW_NAV_ERR_AUTH); WX_ERROR_CASE(wxWEBVIEW_NAV_ERR_SECURITY); WX_ERROR_CASE(wxWEBVIEW_NAV_ERR_NOT_FOUND); WX_ERROR_CASE(wxWEBVIEW_NAV_ERR_REQUEST); WX_ERROR_CASE(wxWEBVIEW_NAV_ERR_USER_CANCELLED); WX_ERROR_CASE(wxWEBVIEW_NAV_ERR_OTHER); } // wxLogMessage("%s", "Error; url='" + evt.GetURL() + "', error='" + // category + " (" + evt.GetString() + ")'"); // Show the info bar with an error // m_info->ShowMessage(_L("An error occurred loading ") + evt.GetURL() + // "\n" + "'" + category + "'", wxICON_ERROR); BOOST_LOG_TRIVIAL(trace) << "GuideFrame::OnError: An error occurred loading " << evt.GetURL() << category; UpdateState(); } void GuideFrame::OnScriptResponseMessage(wxCommandEvent &WXUNUSED(evt)) { } bool GuideFrame::IsFirstUse() { wxString strUse; std::string strVal = wxGetApp().app_config->get(std::string(m_SectionName.mb_str()), "finish"); if (strVal == "1") return false; if (orca_bundle_rsrc == true) return true; return true; } int GuideFrame::SaveProfile() { // SoftFever: don't collect info //privacy // if (PrivacyUse == true) { // m_MainPtr->app_config->set(std::string(m_SectionName.mb_str()), "privacyuse", "1"); // } else // m_MainPtr->app_config->set(std::string(m_SectionName.mb_str()), "privacyuse", "0"); m_MainPtr->app_config->set("region", m_Region); m_MainPtr->app_config->set_bool("stealth_mode", StealthMode); //finish m_MainPtr->app_config->set(std::string(m_SectionName.mb_str()), "finish", "1"); m_MainPtr->app_config->save(); std::string strAll = m_ProfileJson.dump(-1, ' ', false, json::error_handler_t::ignore); BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << "before save to app_config: "<< std::endl< section_new; m_appconfig_new.clear_section(section_name); for (auto it = m_ProfileJson["filament"].begin(); it != m_ProfileJson["filament"].end(); ++it) { if (it.value()["selected"] == 1){ section_new[it.key()] = "true"; } } m_appconfig_new.set_section(section_name, section_new); //set vendors to app_config Slic3r::AppConfig::VendorMap empty_vendor_map; m_appconfig_new.set_vendors(empty_vendor_map); for (auto it = m_ProfileJson["model"].begin(); it != m_ProfileJson["model"].end(); ++it) { if (it.value().is_object()) { json temp_model = it.value(); std::string model_name = temp_model["model"]; std::string vendor_name = temp_model["vendor"]; std::string selected = temp_model["nozzle_selected"]; boost::trim(selected); std::string nozzle; while (selected.size() > 0) { auto pos = selected.find(';'); if (pos != std::string::npos) { nozzle = selected.substr(0, pos); m_appconfig_new.set_variant(vendor_name, model_name, nozzle, "true"); BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format("vendor_name %1%, model_name %2%, nozzle %3% selected")%vendor_name %model_name %nozzle; selected = selected.substr(pos + 1); boost::trim(selected); } else { m_appconfig_new.set_variant(vendor_name, model_name, selected, "true"); BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format("vendor_name %1%, model_name %2%, nozzle %3% selected")%vendor_name %model_name %selected; break; } } } } //m_appconfig_new return 0; } static std::set get_new_added_presets(const std::map& old_data, const std::map& new_data) { auto get_aliases = [](const std::map& data) { std::set old_aliases; for (auto item : data) { const std::string& name = item.first; size_t pos = name.find("@"); old_aliases.emplace(pos == std::string::npos ? name : name.substr(0, pos-1)); } return old_aliases; }; std::set old_aliases = get_aliases(old_data); std::set new_aliases = get_aliases(new_data); std::set diff; std::set_difference(new_aliases.begin(), new_aliases.end(), old_aliases.begin(), old_aliases.end(), std::inserter(diff, diff.begin())); return diff; } static std::string get_first_added_preset(const std::map& old_data, const std::map& new_data) { std::set diff = get_new_added_presets(old_data, new_data); if (diff.empty()) return std::string(); return *diff.begin(); } bool GuideFrame::apply_config(AppConfig *app_config, PresetBundle *preset_bundle, const PresetUpdater *updater, bool& apply_keeped_changes) { const auto enabled_vendors = m_appconfig_new.vendors(); const auto old_enabled_vendors = app_config->vendors(); const auto enabled_filaments = m_appconfig_new.has_section(AppConfig::SECTION_FILAMENTS) ? m_appconfig_new.get_section(AppConfig::SECTION_FILAMENTS) : std::map(); const auto old_enabled_filaments = app_config->has_section(AppConfig::SECTION_FILAMENTS) ? app_config->get_section(AppConfig::SECTION_FILAMENTS) : std::map(); bool check_unsaved_preset_changes = false; std::vector install_bundles; std::vector remove_bundles; const auto vendor_dir = (boost::filesystem::path(Slic3r::data_dir()) / PRESET_SYSTEM_DIR).make_preferred(); for (const auto &it : enabled_vendors) { if (it.second.size() > 0) { auto vendor_file = vendor_dir/(it.first + ".json"); if (!fs::exists(vendor_file)) { install_bundles.emplace_back(it.first); } } } //add the removed vendor bundles for (const auto &it : old_enabled_vendors) { if (it.second.size() > 0) { if (enabled_vendors.find(it.first) != enabled_vendors.end()) continue; auto vendor_file = vendor_dir/(it.first + ".json"); if (fs::exists(vendor_file)) { remove_bundles.emplace_back(it.first); } } } check_unsaved_preset_changes = (enabled_vendors != old_enabled_vendors) || (enabled_filaments != old_enabled_filaments); wxString header = _L("The configuration package is changed in previous Config Guide"); wxString caption = _L("Configuration package changed"); int act_btns = ActionButtons::KEEP|ActionButtons::SAVE; if (check_unsaved_preset_changes && !wxGetApp().check_and_keep_current_preset_changes(caption, header, act_btns, &apply_keeped_changes)) return false; // If there are bundles to install, they will be installed by apply_vendor_config if (!install_bundles.empty()) { BOOST_LOG_TRIVIAL(info) << "Will install " << install_bundles.size() << " vendor bundles from resources"; } else { BOOST_LOG_TRIVIAL(info) << "No bundles need to be installed from resource directory"; } // Not remove, because these bundles may be updated //if (remove_bundles.size() > 0) { // //remove unused bundles // for (const auto &it : remove_bundles) { // auto vendor_file = vendor_dir/(it + ".json"); // auto sub_dir = vendor_dir/(it); // if (fs::exists(vendor_file)) // fs::remove(vendor_file); // if (fs::exists(sub_dir)) // fs::remove_all(sub_dir); // } //} else { // BOOST_LOG_TRIVIAL(info) << "No bundles need to be removed"; //} std::string preferred_model; std::string preferred_variant; PrinterTechnology preferred_pt = ptFFF; auto get_preferred_printer_model = [preset_bundle, enabled_vendors, old_enabled_vendors, preferred_pt](const std::string& bundle_name, std::string& variant) { const auto config = enabled_vendors.find(bundle_name); if (config == enabled_vendors.end()) return std::string(); const VendorProfile & printer_profile = preset_bundle->vendors[bundle_name]; const std::map>& model_maps = config->second; //for (const auto& vendor_profile : preset_bundle->vendors) { for (const auto& model_it: model_maps) { if (model_it.second.size() > 0) { variant = *model_it.second.begin(); if (model_it.second.size() > 1) { if (printer_profile.models.size() > 0) { const VendorProfile::PrinterModel& printer_model = *std::find_if(printer_profile.models.begin(), printer_profile.models.end(), [id = model_it.first](auto& m) { return m.id == id; }); for (auto& vt : printer_model.variants) { if (std::find(model_it.second.begin(), model_it.second.end(), vt.name) != model_it.second.end()) { variant = vt.name; break; } } } else if (variant != PresetBundle::ORCA_DEFAULT_PRINTER_VARIANT){ if (std::find(model_it.second.begin(), model_it.second.end(), PresetBundle::ORCA_DEFAULT_PRINTER_VARIANT) != model_it.second.end()) variant = PresetBundle::ORCA_DEFAULT_PRINTER_VARIANT; } } const auto config_old = old_enabled_vendors.find(bundle_name); if (config_old == old_enabled_vendors.end()) return model_it.first; const auto model_it_old = config_old->second.find(model_it.first); if (model_it_old == config_old->second.end()) return model_it.first; else if (model_it_old->second != model_it.second) { for (const auto& var : model_it.second) if (model_it_old->second.find(var) == model_it_old->second.end()) { variant = var; return model_it.first; } } } } //} if (!variant.empty()) variant.clear(); return std::string(); }; // Orca "custom" printers are considered first, then 3rd party. if (preferred_model = get_preferred_printer_model(PresetBundle::ORCA_DEFAULT_BUNDLE, preferred_variant); preferred_model.empty()) { for (const auto& bundle : enabled_vendors) { if (bundle.first == PresetBundle::ORCA_DEFAULT_BUNDLE) { continue; } if (preferred_model = get_preferred_printer_model(bundle.first, preferred_variant); !preferred_model.empty()) break; } } std::string first_added_filament; auto get_first_added_material_preset = [this, app_config](const std::string& section_name, std::string& first_added_preset) { if (m_appconfig_new.has_section(section_name)) { // get first of new added preset names const std::map& old_presets = app_config->has_section(section_name) ? app_config->get_section(section_name) : std::map(); first_added_preset = get_first_added_preset(old_presets, m_appconfig_new.get_section(section_name)); } }; // Not switch filament //get_first_added_material_preset(AppConfig::SECTION_FILAMENTS, first_added_filament); // ORCA: functionality moved to PresetBundle::apply_vendor_config; keeping for future reference // // For each @System filament, check if a vendor-specific override exists // // in the loaded profiles. If so, replace the @System variant with the // // override (e.g. replace "Generic ABS @System" with BBL "Generic ABS"). // // When printers from the default bundle are also selected, keep @System // // too since those printers need it. // static const std::string system_suffix = " @System"; // auto it_default = enabled_vendors.find(PresetBundle::ORCA_DEFAULT_BUNDLE); // bool has_default_bundle_printer = it_default != enabled_vendors.end() && !it_default->second.empty(); // bool has_filament_profiles = m_ProfileJson.contains("filament"); // // Check if any non-default vendor has selected printers // bool has_vendor_printer = false; // for (const auto& [vendor, models] : enabled_vendors) { // if (vendor != PresetBundle::ORCA_DEFAULT_BUNDLE && !models.empty()) { // has_vendor_printer = true; // break; // } // } // std::map supplemented_filaments; // for (const auto& [name, value] : enabled_filaments) { // if (name.size() > system_suffix.size() && // name.compare(name.size() - system_suffix.size(), system_suffix.size(), system_suffix) == 0) { // std::string short_name = name.substr(0, name.size() - system_suffix.size()); // if (has_vendor_printer && has_filament_profiles && m_ProfileJson["filament"].contains(short_name)) { // supplemented_filaments[short_name] = value; // BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << " Replacing @System filament: '" << name << "' -> '" << short_name << "'"; // if (has_default_bundle_printer) { // supplemented_filaments[name] = value; // BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << " Also keeping '" << name << "' for default bundle printers"; // } // continue; // } // } // supplemented_filaments[name] = value; // } // //update the app_config // app_config->set_section(AppConfig::SECTION_FILAMENTS, supplemented_filaments); // app_config->set_vendors(m_appconfig_new); // if (check_unsaved_preset_changes) // preset_bundle->load_presets(*app_config, ForwardCompatibilitySubstitutionRule::Enable, // {preferred_model, preferred_variant, first_added_filament, std::string()}); // // If the active filament is not in the wizard-selected filaments, switch to the first // // compatible wizard-selected filament. This handles the first-run case where load_presets // // falls back to "Generic PLA" even though the user selected a different filament. // bool active_filament_selected = supplemented_filaments.empty() // || supplemented_filaments.count(preset_bundle->filament_presets.front()) > 0; // if (!active_filament_selected) { // for (const auto& [filament_name, _] : supplemented_filaments) { // const Preset* preset = preset_bundle->filaments.find_preset(filament_name); // if (preset && preset->is_visible && preset->is_compatible) { // preset_bundle->filaments.select_preset_by_name(filament_name, true); // preset_bundle->filament_presets.front() = preset_bundle->filaments.get_selected_preset_name(); // break; // } // } // } // // Update the selections from the compatibilty. // preset_bundle->export_selections(*app_config); BOOST_LOG_TRIVIAL(info) << "calling apply_vendor_config from WebGuideDialog"; // Call the Core library function to apply vendor configuration // This handles bundle installation, filament @System substitution, AppConfig updates, and preset loading if (!preset_bundle->apply_vendor_config( enabled_vendors, enabled_filaments, app_config, true, preferred_model, preferred_variant)) return false; return true; } bool GuideFrame::run() { //BOOST_LOG_TRIVIAL(info) << boost::format("Running ConfigWizard, reason: %1%, start_page: %2%") % reason % start_page; GUI_App &app = wxGetApp(); //p->set_run_reason(reason); //p->set_start_page(start_page); app.preset_bundle->export_selections(*app.app_config); BOOST_LOG_TRIVIAL(info) << "GuideFrame before ShowModal"; // display position int main_frame_display_index = wxDisplay::GetFromWindow(wxGetApp().mainframe); int guide_display_index = wxDisplay::GetFromWindow(this); if (main_frame_display_index != guide_display_index) { wxDisplay display = wxDisplay(main_frame_display_index); wxRect screenRect = display.GetGeometry(); int guide_x = screenRect.x + (screenRect.width - this->GetSize().GetWidth()) / 2; int guide_y = screenRect.y + (screenRect.height - this->GetSize().GetHeight()) / 2; this->SetPosition(wxPoint(guide_x, guide_y)); } int result = this->ShowModal(); if (result == wxID_OK) { bool apply_keeped_changes = false; BOOST_LOG_TRIVIAL(info) << "GuideFrame returned ok"; if (! this->apply_config(app.app_config, app.preset_bundle, app.preset_updater, apply_keeped_changes)) return false; if (apply_keeped_changes) app.apply_keeped_preset_modifications(); app.app_config->set_legacy_datadir(false); app.update_mode(); // BBS //app.obj_manipul()->update_ui_from_settings(); BOOST_LOG_TRIVIAL(info) << "GuideFrame applied"; this->Close(); return true; } else if (result == wxID_CANCEL) { BOOST_LOG_TRIVIAL(info) << "GuideFrame cancelled"; if (app.preset_bundle->printers.only_default_printers()) { //we install the default here bool apply_keeped_changes = false; //clear filament section and use default materials app.app_config->set_variant(PresetBundle::ORCA_DEFAULT_BUNDLE, PresetBundle::ORCA_DEFAULT_PRINTER_MODEL, PresetBundle::ORCA_DEFAULT_PRINTER_VARIANT, "true"); app.app_config->clear_section(AppConfig::SECTION_FILAMENTS); app.preset_bundle->load_selections(*app.app_config, {PresetBundle::ORCA_DEFAULT_PRINTER_MODEL, PresetBundle::ORCA_DEFAULT_PRINTER_VARIANT, PresetBundle::ORCA_DEFAULT_FILAMENT, std::string()}); app.app_config->set_legacy_datadir(false); app.update_mode(); return true; } else return false; } else if (result == wxID_EDIT) { this->Close(); Filamentinformation *filament_info = new Filamentinformation(); filament_info->filament_id = m_editing_filament_id; wxQueueEvent(wxGetApp().plater(), new SimpleEvent(EVT_MODIFY_FILAMENT, filament_info)); return false; } else return false; } int GuideFrame::GetFilamentInfo( std::string VendorDirectory, json & pFilaList, std::string filepath, std::string &sVendor, std::string &sType) { //GetStardardFilePath(filepath); BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << " GetFilamentInfo:VendorDirectory - " << VendorDirectory << ", Filepath - "<second.vendor; type = cache_it->second.type; status = cache_it->second.status; } else { try { std::string contents; LoadFile(filepath, contents); BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << ": Json Contents: " << contents; json jLocal = json::parse(contents); if (jLocal.contains("filament_vendor")) vendor = jLocal["filament_vendor"][0]; else BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << filepath << " - Not Contains filament_vendor"; if (jLocal.contains("filament_type")) type = jLocal["filament_type"][0]; else BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << filepath << " - Not Contains filament_type"; if (vendor == "" || type == "") { if (jLocal.contains("inherits")) { std::string FName = jLocal["inherits"]; if (!pFilaList.contains(FName)) { BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << "pFilaList - Not Contains inherits filaments: " << FName; status = -1; } else { std::string FPath = pFilaList[FName]["sub_path"]; BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << " Before Format Inherits Path: VendorDirectory - " << VendorDirectory << ", sub_path - " << FPath; wxString strNewFile = wxString::Format("%s%c%s", wxString(VendorDirectory.c_str(), wxConvUTF8), boost::filesystem::path::preferred_separator, FPath); boost::filesystem::path inherits_path(w2s(strNewFile)); if (!boost::filesystem::exists(inherits_path)) inherits_path = (boost::filesystem::path(m_OrcaFilaLibPath) / boost::filesystem::path(FPath)).make_preferred(); if (boost::filesystem::exists(inherits_path)) { // Recurse with this file's own (vendor, type) as the chain accumulator. status = GetFilamentInfo(VendorDirectory, pFilaList, inherits_path.string(), vendor, type); } else { BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << " inherits File Not Exist: " << inherits_path; status = -1; } } } else { BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << filepath << " - Not Contains inherits"; if (type == "") { BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << "sType is Empty"; status = -1; } else { if (vendor == "") vendor = "Generic"; status = 0; } } } else { status = 0; } } catch (nlohmann::detail::parse_error &err) { BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << ": parse " << filepath << " got a nlohmann::detail::parse_error, reason = " << err.what(); status = -1; } catch (std::exception &e) { BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << ": parse " << filepath << " got exception: " << e.what(); status = -1; } filament_info_cache[filepath] = CachedFilamentInfo{status, vendor, type}; } // Merge this file's resolved values into the caller's out-params (fill empties only). if (sVendor.empty()) sVendor = vendor; if (sType.empty()) sType = type; return status; } bool GuideFrame::TryLoadGuideJsonCache() { const auto path = guide_json_cache_path(); boost::system::error_code ec; if (!boost::filesystem::exists(path, ec)) return false; try { boost::nowide::ifstream ifs(path.string()); json cache; ifs >> cache; if (!cache.contains("version") || !cache.contains("data")) return false; const std::string expected = guide_json_cache_version_key(rsrc_vendor_dir, vendor_dir); if (expected.empty() || cache["version"].get() != expected) return false; m_ProfileJson = cache["data"]; if (m_ProfileJson["machine"].empty()) return false; BOOST_LOG_TRIVIAL(info) << "GuideFrame: loaded profile data from guide JSON cache (" << m_ProfileJson["model"].size() << " models, " << m_ProfileJson["machine"].size() << " machines, " << m_ProfileJson["filament"].size() << " filaments)"; return true; } catch (const std::exception& e) { BOOST_LOG_TRIVIAL(warning) << "GuideFrame: guide JSON cache load failed: " << e.what(); return false; } } void GuideFrame::SaveGuideJsonCache() { try { json cache; cache["version"] = guide_json_cache_version_key(rsrc_vendor_dir, vendor_dir); if (cache["version"].get().empty()) return; json base = m_ProfileJson; // Strip user-specific state — SaveProfileData() re-applies it from AppConfig. for (auto& entry : base["model"]) entry["nozzle_selected"] = ""; cache["data"] = std::move(base); boost::nowide::ofstream ofs(guide_json_cache_path().string()); ofs << cache.dump(-1, ' ', false, json::error_handler_t::ignore); BOOST_LOG_TRIVIAL(info) << "GuideFrame: guide JSON cache saved"; } catch (const std::exception& e) { BOOST_LOG_TRIVIAL(warning) << "GuideFrame: failed to save guide JSON cache: " << e.what(); } } bool GuideFrame::BuildProfileDataFromPresetBundle() { PresetBundle* pb = wxGetApp().preset_bundle; if (!pb || pb->vendors.empty()) return false; try { // Models from vendor profiles for (const auto& [vendor_id, vp] : pb->vendors) { for (const auto& model : vp.models) { std::string nozzle_str; for (const auto& v : model.variants) { if (!nozzle_str.empty()) nozzle_str += ";"; nozzle_str += v.name; } std::string materials_str; for (const auto& m : model.default_materials) { if (!materials_str.empty()) materials_str += ";"; materials_str += m; } boost::filesystem::path cover_path = (boost::filesystem::path(resources_dir()) / "profiles" / vendor_id / (model.id + "_cover.png")) .make_preferred(); if (!boost::filesystem::exists(cover_path)) cover_path = (boost::filesystem::path(resources_dir()) / "web/image/printer" / (model.id + "_cover.png")) .make_preferred(); json entry; entry["model"] = model.id; entry["name"] = model.name; entry["vendor"] = vendor_id; entry["nozzle_diameter"] = nozzle_str; entry["materials"] = materials_str; entry["cover"] = cover_path.string(); entry["nozzle_selected"] = ""; entry["sub_path"] = ""; m_ProfileJson["model"].push_back(entry); } } // Machine map: preset name -> {model, nozzle variant} for (const Preset& p : pb->printers()) { if (!p.is_system) continue; const auto* printer_model = p.config.option("printer_model"); const auto* printer_variant = p.config.option("printer_variant"); if (!printer_model || printer_model->value.empty() || !printer_variant) continue; json mach; mach["model"] = printer_model->value; mach["nozzle"] = printer_variant->value; m_ProfileJson["machine"][p.name] = mach; } // Filament map from system filament presets (vendor/type already resolved in config) for (const Preset& p : pb->filaments()) { if (!p.is_system) continue; const auto* fila_vendor = p.config.option("filament_vendor"); const auto* fila_type = p.config.option("filament_type"); const auto* compat_printers = p.config.option("compatible_printers"); std::string vendor = (fila_vendor && !fila_vendor->values.empty()) ? fila_vendor->values[0] : ""; std::string type = (fila_type && !fila_type->values.empty()) ? fila_type->values[0] : ""; std::string model_list; if (compat_printers) { for (const std::string& pname : compat_printers->values) { if (m_ProfileJson["machine"].contains(pname)) { std::string m = m_ProfileJson["machine"][pname]["model"]; std::string n = m_ProfileJson["machine"][pname]["nozzle"]; model_list += "[" + m + "++" + n + "]"; } } } json ff; ff["name"] = p.name; ff["sub_path"] = p.file; ff["vendor"] = vendor; ff["type"] = type; ff["models"] = model_list; ff["selected"] = 0; m_ProfileJson["filament"][p.name] = ff; } // Process list from visible system print presets for (const Preset& p : pb->prints()) { if (!p.is_system || !p.is_visible) continue; json entry; entry["name"] = p.name; entry["sub_path"] = p.file; m_ProfileJson["process"].push_back(entry); } // If rsrc_vendor_dir has vendor JSONs not covered by the current bundle, the // bundle is incomplete (e.g. dev env where data_dir/system only has // OrcaFilamentLibrary+Custom). Fall back so LoadProfileFamily reads both dirs. try { for (const auto& e : boost::filesystem::directory_iterator(rsrc_vendor_dir)) { if (e.path().extension().string() != ".json") continue; const std::string stem = e.path().stem().string(); if (pb->vendors.find(stem) == pb->vendors.end()) { BOOST_LOG_TRIVIAL(info) << "GuideFrame: vendor '" << stem << "' in resources but not in preset_bundle — falling back to JSON loading"; m_ProfileJson["model"] = json::array(); m_ProfileJson["machine"] = json::object(); m_ProfileJson["filament"] = json::object(); m_ProfileJson["process"] = json::array(); return false; } } } catch (const std::exception&) {} BOOST_LOG_TRIVIAL(info) << "GuideFrame: built profile data from preset_bundle (" << m_ProfileJson["model"].size() << " models, " << m_ProfileJson["machine"].size() << " machines, " << m_ProfileJson["filament"].size() << " filaments)"; return !m_ProfileJson["machine"].empty(); } catch (const std::exception& e) { BOOST_LOG_TRIVIAL(warning) << "GuideFrame::BuildProfileDataFromPresetBundle failed: " << e.what() << " — falling back to JSON loading"; m_ProfileJson["model"] = json::array(); m_ProfileJson["machine"] = json::object(); m_ProfileJson["filament"] = json::object(); m_ProfileJson["process"] = json::array(); return false; } } // Builds guide profile JSON from the per-vendor bundled caches // (resources/profiles/.cache, generated by CI). // This avoids the 90-second LoadProfileFamily fallback on first launch. bool GuideFrame::BuildProfileDataFromBundledCache() { // Enumerate per-vendor .cache files in resources/profiles/. std::vector cache_files; try { for (const auto& e : boost::filesystem::directory_iterator(rsrc_vendor_dir)) { if (e.path().extension() == ".cache") cache_files.push_back(e.path()); } } catch (const std::exception& ex) { BOOST_LOG_TRIVIAL(warning) << "GuideFrame::BuildProfileDataFromBundledCache: cannot scan " << rsrc_vendor_dir << ": " << ex.what(); return false; } if (cache_files.empty()) return false; try { for (const auto& cache_path : cache_files) { const std::string vendor_id = cache_path.stem().string(); // Validate against the version in the corresponding vendor JSON. const boost::filesystem::path json_path = rsrc_vendor_dir / (vendor_id + ".json"); const std::string ver_str = get_vendor_cache_key(json_path.string()); VendorProfile vc_profile; std::vector vc_printer_presets; std::vector vc_filament_presets; std::vector vc_print_presets; if (!PresetBundle::load_vendor_cache_for_guide(cache_path.string(), ver_str, vc_profile, vc_printer_presets, vc_filament_presets, vc_print_presets)) continue; // Models from this vendor's cached profile for (const auto& cm : vc_profile.models) { std::string nozzle_str; for (const auto& v : cm.variants) { if (!nozzle_str.empty()) nozzle_str += ";"; nozzle_str += v.name; } std::string materials_str; for (const auto& m : cm.default_materials) { if (!materials_str.empty()) materials_str += ";"; materials_str += m; } boost::filesystem::path cover_path = (boost::filesystem::path(resources_dir()) / "profiles" / vc_profile.id / (cm.id + "_cover.png")) .make_preferred(); if (!boost::filesystem::exists(cover_path)) cover_path = (boost::filesystem::path(resources_dir()) / "web/image/printer" / (cm.id + "_cover.png")) .make_preferred(); json entry; entry["model"] = cm.id; entry["name"] = cm.name; entry["vendor"] = vc_profile.id; entry["nozzle_diameter"] = nozzle_str; entry["materials"] = materials_str; entry["cover"] = cover_path.string(); entry["nozzle_selected"] = ""; entry["sub_path"] = ""; m_ProfileJson["model"].push_back(entry); } // Machines from cached printer presets for (const auto& cp : vc_printer_presets) { const auto* pm = cp.config.option("printer_model"); const auto* pv = cp.config.option("printer_variant"); if (!pm || pm->value.empty() || !pv) continue; json mach; mach["model"] = pm->value; mach["nozzle"] = pv->value; m_ProfileJson["machine"][cp.name] = mach; } // Filaments from cached filament presets for (const auto& cp : vc_filament_presets) { const auto* fv = cp.config.option("filament_vendor"); const auto* ft = cp.config.option("filament_type"); const auto* compat = cp.config.option("compatible_printers"); std::string vendor = (fv && !fv->values.empty()) ? fv->values[0] : ""; std::string type = (ft && !ft->values.empty()) ? ft->values[0] : ""; std::string model_list; if (compat) { for (const std::string& pname : compat->values) { if (m_ProfileJson["machine"].contains(pname)) { std::string m = m_ProfileJson["machine"][pname]["model"]; std::string n = m_ProfileJson["machine"][pname]["nozzle"]; model_list += "[" + m + "++" + n + "]"; } } } json ff; ff["name"] = cp.name; ff["sub_path"] = cp.file; ff["vendor"] = vendor; ff["type"] = type; ff["models"] = model_list; ff["selected"] = 0; m_ProfileJson["filament"][cp.name] = ff; } // Process from cached print presets for (const auto& cp : vc_print_presets) { if (!cp.is_visible) continue; json entry; entry["name"] = cp.name; entry["sub_path"] = cp.file; m_ProfileJson["process"].push_back(entry); } } BOOST_LOG_TRIVIAL(info) << "GuideFrame: built profile data from bundled per-vendor caches (" << m_ProfileJson["model"].size() << " models, " << m_ProfileJson["machine"].size() << " machines, " << m_ProfileJson["filament"].size() << " filaments)"; return !m_ProfileJson["machine"].empty(); } catch (const std::exception& ex) { BOOST_LOG_TRIVIAL(warning) << "GuideFrame::BuildProfileDataFromBundledCache failed: " << ex.what(); m_ProfileJson["model"] = json::array(); m_ProfileJson["machine"] = json::object(); m_ProfileJson["filament"] = json::object(); m_ProfileJson["process"] = json::array(); return false; } } int GuideFrame::LoadProfileData() { // Background thread: the fast path in OnNavigationComplete failed (presets not yet loaded). // Loading order (fastest to slowest): // 1. Guide JSON cache (data_dir/guide_profile_cache.json, sub-second) // 2. Bundled per-vendor binary caches (CI-generated, ~1-2s) // 3. Read all vendor JSONs (~90s) // After paths 2 or 3 the guide JSON cache is written so next open uses path 1. try { if (!TryLoadGuideJsonCache()) { if (!BuildProfileDataFromBundledCache()) { // Last resort — read all vendor JSONs (~90s) std::set loaded_vendors; auto filament_library_name = boost::filesystem::path(PresetBundle::ORCA_FILAMENT_LIBRARY).replace_extension(".json"); if (boost::filesystem::exists(vendor_dir / filament_library_name)) LoadProfileFamily(PresetBundle::ORCA_FILAMENT_LIBRARY, (vendor_dir / filament_library_name).string()); else LoadProfileFamily(PresetBundle::ORCA_FILAMENT_LIBRARY, (rsrc_vendor_dir / filament_library_name).string()); loaded_vendors.insert(PresetBundle::ORCA_FILAMENT_LIBRARY); boost::filesystem::directory_iterator endIter; for (boost::filesystem::directory_iterator iter(vendor_dir); iter != endIter; iter++) { if (!boost::filesystem::is_directory(*iter)) { wxString strVendor = from_u8(iter->path().string()).BeforeLast('.'); strVendor = strVendor.AfterLast('\\'); strVendor = strVendor.AfterLast('/'); wxString strExtension = from_u8(iter->path().string()).AfterLast('.').Lower(); if (strExtension.CmpNoCase("json") != 0 || loaded_vendors.find(w2s(strVendor)) != loaded_vendors.end()) continue; LoadProfileFamily(w2s(strVendor), iter->path().string()); loaded_vendors.insert(w2s(strVendor)); } if (m_destroy) return 0; } boost::filesystem::directory_iterator others_endIter; for (boost::filesystem::directory_iterator iter(rsrc_vendor_dir); iter != others_endIter; iter++) { if (!boost::filesystem::is_directory(*iter)) { wxString strVendor = from_u8(iter->path().string()).BeforeLast('.'); strVendor = strVendor.AfterLast('\\'); strVendor = strVendor.AfterLast('/'); wxString strExtension = from_u8(iter->path().string()).AfterLast('.').Lower(); if (strExtension.CmpNoCase("json") != 0 || loaded_vendors.find(w2s(strVendor)) != loaded_vendors.end()) continue; LoadProfileFamily(w2s(strVendor), iter->path().string()); loaded_vendors.insert(w2s(strVendor)); } if (m_destroy) return 0; } } // Persist the result so subsequent opens skip both the bundled cache and // the slow JSON loading path entirely. SaveGuideJsonCache(); } // Capture the cancel token by value (shared_ptr) so the lambda doesn't // touch `this` if GuideFrame is destroyed before the event fires. auto tok = m_cancel_token; wxGetApp().CallAfter([this, tok] { if (!*tok) on_profile_loaded(); }); } catch (const std::exception& e) { BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << ", error: " << e.what(); } filament_info_cache.clear(); return 0; } int GuideFrame::SaveProfileData() { try { const auto enabled_filaments = wxGetApp().app_config->has_section(AppConfig::SECTION_FILAMENTS) ? wxGetApp().app_config->get_section(AppConfig::SECTION_FILAMENTS) : std::map(); m_appconfig_new.set_vendors(*wxGetApp().app_config); m_appconfig_new.set_section(AppConfig::SECTION_FILAMENTS, enabled_filaments); for (auto it = m_ProfileJson["model"].begin(); it != m_ProfileJson["model"].end(); ++it) { if (it.value().is_object()) { json& temp_model = it.value(); std::string model_name = temp_model["model"]; std::string vendor_name = temp_model["vendor"]; std::string nozzle_diameter = temp_model["nozzle_diameter"]; std::string selected; boost::trim(nozzle_diameter); std::string nozzle; bool enabled = false, first=true; while (nozzle_diameter.size() > 0) { auto pos = nozzle_diameter.find(';'); if (pos != std::string::npos) { nozzle = nozzle_diameter.substr(0, pos); enabled = m_appconfig_new.get_variant(vendor_name, model_name, nozzle); if (enabled) { if (!first) selected += ";"; selected += nozzle; first = false; } nozzle_diameter = nozzle_diameter.substr(pos + 1); boost::trim(nozzle_diameter); } else { enabled = m_appconfig_new.get_variant(vendor_name, model_name, nozzle_diameter); if (enabled) { if (!first) selected += ";"; selected += nozzle_diameter; } break; } } temp_model["nozzle_selected"] = selected; //m_ProfileJson["model"][a]["nozzle_selected"] } } if (m_ProfileJson["model"].size() == 1) { std::string strNozzle = m_ProfileJson["model"][0]["nozzle_diameter"]; m_ProfileJson["model"][0]["nozzle_selected"]=strNozzle; } for (auto it = m_ProfileJson["filament"].begin(); it != m_ProfileJson["filament"].end(); ++it) { //json temp_filament = it.value(); std::string filament_name = it.key(); if (enabled_filaments.find(filament_name) != enabled_filaments.end()) m_ProfileJson["filament"][filament_name]["selected"] = 1; } //----region m_Region = wxGetApp().app_config->get("region"); m_ProfileJson["region"] = m_Region; m_ProfileJson["network_plugin_install"] = wxGetApp().app_config->get("app","installed_networking"); m_ProfileJson["network_plugin_compability"] = wxGetApp().is_compatibility_version() ? "1" : "0"; network_plugin_ready = wxGetApp().is_compatibility_version(); StealthMode = wxGetApp().app_config->get_bool("app","stealth_mode"); m_ProfileJson["stealth_mode"] = StealthMode; } catch (std::exception &e) { BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << ", error: "<< e.what() <