From 7133d6b22557b7ba664c8b9005388867df4980f2 Mon Sep 17 00:00:00 2001 From: Lam Wei Lun Date: Tue, 1 Sep 2026 16:59:25 +0800 Subject: [PATCH] Added wiki and youtube guide link as placeholders. Add to recently opened in home screen after publishing. Show PUB badge. Add .published as a file save name hint. --- resources/web/homepage/css/home.css | 29 ++++++++++++++ resources/web/homepage/js/home.js | 5 ++- src/libslic3r/Format/bbs_3mf.cpp | 50 ++++++++++++++++++++++++ src/libslic3r/Format/bbs_3mf.hpp | 3 ++ src/slic3r/GUI/MainFrame.cpp | 15 ++++++- src/slic3r/GUI/MainFrame.hpp | 2 + src/slic3r/GUI/Plater.cpp | 39 +++++++++++------- src/slic3r/GUI/PublishSettingsDialog.cpp | 19 +++++++++ 8 files changed, 145 insertions(+), 17 deletions(-) diff --git a/resources/web/homepage/css/home.css b/resources/web/homepage/css/home.css index e99d22ec64..395dcc9356 100644 --- a/resources/web/homepage/css/home.css +++ b/resources/web/homepage/css/home.css @@ -572,6 +572,35 @@ body word-break: break-all; } +.FileNamePack +{ + display: flex; + align-items: center; + min-width: 0; + overflow: hidden; +} + +.FileNamePack .FileName +{ + flex: 1 1 auto; + min-width: 0; +} + +.FilePublishedBadge +{ + flex: 0 0 auto; + margin-right: 4px; + padding: 0 4px; + height: 16px; + line-height: 16px; + font-size: 10px; + font-weight: 600; + color: #FFFFFF; + background-color: #00AE42; + border-radius: 2px; + white-space: nowrap; +} + .FileDate { color: #A8A8A8; diff --git a/resources/web/homepage/js/home.js b/resources/web/homepage/js/home.js index f522c44bed..3cb5ff4fb3 100644 --- a/resources/web/homepage/js/home.js +++ b/resources/web/homepage/js/home.js @@ -219,15 +219,18 @@ function ShowRecentFileList( pList ) let sImg=OneFile["image"] || sImages[sPath]; let sTime=OneFile['time']; let sName=OneFile['project_name']; + let sPublished=OneFile['published'] == '1'; sImages[sPath] = sImg; //let index=sPath.lastIndexOf('\\')>0?sPath.lastIndexOf('\\'):sPath.lastIndexOf('\/'); //let sShortName=sPath.substring(index+1,sPath.length); + let sBadge=sPublished? 'PUB':''; + let TmpHtml='
'+ ''+ '
No Image
'+ - '
'+sName+'
'+ + '
'+sBadge+'
'+sName+'
'+ '
'+sTime+'
'+ '
'; diff --git a/src/libslic3r/Format/bbs_3mf.cpp b/src/libslic3r/Format/bbs_3mf.cpp index ed88511d9c..3a3ee20e6f 100644 --- a/src/libslic3r/Format/bbs_3mf.cpp +++ b/src/libslic3r/Format/bbs_3mf.cpp @@ -9205,6 +9205,56 @@ std::string bbs_3mf_get_thumbnail(const char *path) return data; } +bool bbs_3mf_is_published(const std::string &path) +{ + mz_zip_archive archive; + mz_zip_zero_struct(&archive); + + struct close_lock + { + mz_zip_archive *archive; + void close() + { + if (archive) { + close_zip_reader(archive); + archive = nullptr; + } + } + ~close_lock() { close(); } + } lock{&archive}; + + if (!open_zip_reader(&archive, path)) + return false; + + // Read just the model XML and locate the published metadata node; no geometry parsing. + int index = mz_zip_reader_locate_file(&archive, MODEL_FILE.c_str(), nullptr, 0); + if (index < 0) + return false; + mz_zip_archive_file_stat stat; + if (!mz_zip_reader_file_stat(&archive, index, &stat)) + return false; + std::string xml(stat.m_uncomp_size, '\0'); + if (!mz_zip_reader_extract_to_mem(&archive, index, xml.data(), xml.size(), 0)) + return false; + + const std::string needle = std::string(""; + size_t pos = xml.find(needle); + if (pos == std::string::npos) + return false; + pos += needle.size(); + size_t end = xml.find("", pos); + if (end == std::string::npos) + return false; + + size_t value_begin = pos, value_end = end; + while (value_begin < value_end && (xml[value_begin] == ' ' || xml[value_begin] == '\t' || xml[value_begin] == '\n' || xml[value_begin] == '\r')) + ++value_begin; + while (value_end > value_begin && (xml[value_end - 1] == ' ' || xml[value_end - 1] == '\t' || xml[value_end - 1] == '\n' || xml[value_end - 1] == '\r')) + --value_end; + + return is_published_3mf_flag(xml.substr(value_begin, value_end - value_begin)); +} + bool load_gcode_3mf_from_stream(std::istream &data, DynamicPrintConfig *config, Model *model, PlateDataPtrs *plate_data_list, Semver *file_version) { CNumericLocalesSetter locales_setter; diff --git a/src/libslic3r/Format/bbs_3mf.hpp b/src/libslic3r/Format/bbs_3mf.hpp index 2e1600737b..768ae7bcfe 100644 --- a/src/libslic3r/Format/bbs_3mf.hpp +++ b/src/libslic3r/Format/bbs_3mf.hpp @@ -293,6 +293,9 @@ extern bool load_bbs_3mf(const char* path, DynamicPrintConfig* config, ConfigSub extern std::string bbs_3mf_get_thumbnail(const char * path); +// Lightweight check: does this 3mf carry the "published" (orca_published == "1") marker? Only reads the 3D/3dmodel.model metadata node +extern bool bbs_3mf_is_published(const std::string &path); + extern bool load_gcode_3mf_from_stream(std::istream & data, DynamicPrintConfig* config, Model* model, PlateDataPtrs* plate_data_list, Semver* file_version); diff --git a/src/slic3r/GUI/MainFrame.cpp b/src/slic3r/GUI/MainFrame.cpp index 3eb36154fc..7d73d884b0 100644 --- a/src/slic3r/GUI/MainFrame.cpp +++ b/src/slic3r/GUI/MainFrame.cpp @@ -4237,15 +4237,23 @@ std::wstring MainFrame::FileHistory::GetThumbnailUrl(int index) const return wss.str(); } +bool MainFrame::FileHistory::GetPublished(int index) const +{ + return index >= 0 && index < static_cast(m_published_files.size()) && m_published_files[index]; +} + void MainFrame::FileHistory::AddFileToHistory(const wxString &file) { if (this->m_fileMaxFiles == 0) return; wxFileHistory::AddFileToHistory(file); - if (m_load_called) + if (m_load_called) { m_thumbnails.push_front(bbs_3mf_get_thumbnail(into_u8(file).c_str())); - else + m_published_files.push_front(bbs_3mf_is_published(into_u8(file))); + } else { m_thumbnails.push_front(""); + m_published_files.push_front(false); + } } void MainFrame::FileHistory::RemoveFileFromHistory(size_t i) @@ -4254,6 +4262,7 @@ void MainFrame::FileHistory::RemoveFileFromHistory(size_t i) return; wxFileHistory::RemoveFileFromHistory(i); m_thumbnails.erase(m_thumbnails.begin() + i); + m_published_files.erase(m_published_files.begin() + i); } size_t MainFrame::FileHistory::FindFileInHistory(const wxString & file) @@ -4269,6 +4278,7 @@ void MainFrame::FileHistory::LoadThumbnails() if (!thumbnail.empty()) { m_thumbnails[i] = thumbnail; } + m_published_files[i] = bbs_3mf_is_published(into_u8(GetHistoryFile(i))); } }); m_load_called = true; @@ -4289,6 +4299,7 @@ void MainFrame::get_recent_projects(boost::property_tree::wptree &tree, int imag std::wstring proj = m_recent_projects.GetHistoryFile(i).ToStdWstring(); item.put(L"project_name", proj.substr(proj.find_last_of(L"/\\") + 1)); item.put(L"path", proj); + item.put(L"published", m_recent_projects.GetPublished(i) ? L"1" : L"0"); boost::system::error_code ec; std::time_t t = boost::filesystem::last_write_time(proj, ec); if (!ec) { diff --git a/src/slic3r/GUI/MainFrame.hpp b/src/slic3r/GUI/MainFrame.hpp index c09a29abb3..278c7e7f30 100644 --- a/src/slic3r/GUI/MainFrame.hpp +++ b/src/slic3r/GUI/MainFrame.hpp @@ -179,6 +179,7 @@ class MainFrame : public DPIFrame { FileHistory(int max) : wxFileHistory(max) {} std::wstring GetThumbnailUrl(int index) const; + bool GetPublished(int index) const; virtual void AddFileToHistory(const wxString &file); virtual void RemoveFileFromHistory(size_t i); @@ -189,6 +190,7 @@ class MainFrame : public DPIFrame void SetMaxFiles(int max); private: std::deque m_thumbnails; + std::deque m_published_files; // parallel to m_thumbnails: is it a published 3mf? bool m_load_called = false; }; diff --git a/src/slic3r/GUI/Plater.cpp b/src/slic3r/GUI/Plater.cpp index d2c9c3589f..7a3789bc96 100644 --- a/src/slic3r/GUI/Plater.cpp +++ b/src/slic3r/GUI/Plater.cpp @@ -5596,11 +5596,11 @@ void Sidebar::add_custom_filament(wxColour new_col, const std::string& preset_na // Count off filament_is_mixed, not filament_presets or the combos: the extruder-count spinner // reaches this before the sidebar has rebuilt, and update_multi_material_filament_presets() // can have grown filament_presets alone. - auto *bundle = wxGetApp().preset_bundle; - size_t insert_pos = bundle->num_physical_filaments(); - size_t total = insert_pos + bundle->num_mixed_filaments(); - int filament_count = (int)(total + 1); - std::string new_color = new_col.GetAsString(wxC2S_HTML_SYNTAX).ToStdString(); + auto* bundle = wxGetApp().preset_bundle; + size_t insert_pos = bundle->num_physical_filaments(); + size_t total = insert_pos + bundle->num_mixed_filaments(); + int filament_count = (int) (total + 1); + std::string new_color = new_col.GetAsString(wxC2S_HTML_SYNTAX).ToStdString(); bundle->set_num_filaments(filament_count, new_color); // Maintain physical-first ordering: rotate the new slot from end to insert_pos. @@ -7018,7 +7018,7 @@ struct Plater::priv void handle_textured_mesh_import(Slic3r::Model& model, const std::vector& obj_idxs, std::function cancel_callback = {}); fs::path get_export_file_path(GUI::FileType file_type); - wxString get_export_file(GUI::FileType file_type, const wxString& title = {}); + wxString get_export_file(GUI::FileType file_type, const wxString& title = {}, bool published = false); // BBS void load_auxiliary_files(); @@ -9069,7 +9069,9 @@ std::vector Plater::priv::load_files(const std::vector& input_ for (const std::string& key : published_config.skipped_keys) message += "\n-" + key; // Informational: the load succeeded, these keys were skipped. - notify_manager->bbl_show_3mf_warn_notification(message, NotificationManager::NotificationLevel::WarningNotificationLevel); + notify_manager + ->bbl_show_3mf_warn_notification(message, + NotificationManager::NotificationLevel::WarningNotificationLevel); } // BBS: notify the user about slot materials that were replaced while @@ -9080,7 +9082,9 @@ std::vector Plater::priv::load_files(const std::vector& input_ for (const std::string& replacement : published_config.material_replacements) message += "\n-" + replacement; // Informational: the load succeeded, the slots were adapted. - notify_manager->bbl_show_3mf_warn_notification(message, NotificationManager::NotificationLevel::WarningNotificationLevel); + notify_manager + ->bbl_show_3mf_warn_notification(message, + NotificationManager::NotificationLevel::WarningNotificationLevel); } ConfigOption* bed_type_opt = preset_bundle->project_config.option("curr_bed_type"); @@ -10026,7 +10030,7 @@ fs::path Plater::priv::get_export_file_path(GUI::FileType file_type) return output_file; } -wxString Plater::priv::get_export_file(GUI::FileType file_type, const wxString& title) +wxString Plater::priv::get_export_file(GUI::FileType file_type, const wxString& title, bool published) { wxString wildcard; switch (file_type) { @@ -10060,7 +10064,10 @@ wxString Plater::priv::get_export_file(GUI::FileType file_type, const wxString& break; } case FT_3MF: { - output_file.replace_extension("3mf"); + // A published export is suggested as ".published.3mf" so the role is visible in the + // dialog and in the recent-files list. This is only a pre-filled suggestion; the user's + // typed filename wins, keeping a plain ".3mf" output fully valid. + output_file.replace_extension(published ? "published.3mf" : "3mf"); dlg_title = title.empty() ? _L("Save file as") : title; break; } @@ -18262,7 +18269,7 @@ void Plater::export_core_3mf() int Plater::export_published_3mf(const std::vector& published_keys, const std::vector& material_keys) { - wxString path = p->get_export_file(FT_3MF, _L("Publish 3MF file as:")); + wxString path = p->get_export_file(FT_3MF, _L("Publish 3MF file as:"), true); if (path.empty() || path == "") return wxID_CANCEL; @@ -18397,6 +18404,10 @@ int Plater::export_published_3mf(const std::vector& published_keys, return wxID_CANCEL; } restore_now(); + + // Register the exported file in the "Recently opened" list + wxGetApp().mainframe->add_to_recent_projects(path); + return wxID_YES; } @@ -21869,9 +21880,9 @@ void Plater::show_object_info() int non_manifold_edges = 0; auto mesh_errors = p->sidebar->obj_list()->get_mesh_errors_info(&info_manifold, &non_manifold_edges); - if (non_manifold_edges > 0) { - info_manifold += "\n" + _L("Tips:") + "\n" + _L("Use \"Fix Model\" to repair the mesh."); - } + if (non_manifold_edges > 0) { + info_manifold += "\n" + _L("Tips:") + "\n" + _L("Use \"Fix Model\" to repair the mesh."); + } info_manifold = "" + info_manifold + ""; info_text += into_u8(info_manifold); diff --git a/src/slic3r/GUI/PublishSettingsDialog.cpp b/src/slic3r/GUI/PublishSettingsDialog.cpp index fcc2816b06..6b641e34da 100644 --- a/src/slic3r/GUI/PublishSettingsDialog.cpp +++ b/src/slic3r/GUI/PublishSettingsDialog.cpp @@ -20,6 +20,7 @@ #include #include +#include #include #include #include @@ -515,6 +516,24 @@ PublishSettingsDialog::PublishSettingsDialog(wxWindow* parent) msg->Wrap(-1); w_sizer->Add(msg, 0, wxRIGHT | wxLEFT | wxTOP, FromDIP(10)); + // Guide link: opens the (still WIP) Publish 3MF docs in a new window/tab, keeping the dialog open. + wxStaticText* guide_link = new wxStaticText(this, wxID_ANY, _L("Publish 3MF Wiki Guide")); + guide_link->SetFont(Label::Body_13); + guide_link->SetForegroundColour(wxColour(0x1F, 0x8E, 0xEA)); + guide_link->SetCursor(wxCURSOR_HAND); + guide_link->Bind(wxEVT_LEFT_DOWN, [](wxMouseEvent&) { + wxLaunchDefaultBrowser("https://www.orcaslicer.com/wiki/publishing_3mf/publish_3mf.html", wxBROWSER_NEW_WINDOW); + }); + w_sizer->Add(guide_link, 0, wxRIGHT | wxLEFT | wxTOP, FromDIP(10)); + + // Placeholder guide link: a Publish 3MF video URL, right below the wiki guide link. + wxStaticText* video_link = new wxStaticText(this, wxID_ANY, _L("Publish 3MF YouTube Video (Placeholder)")); + video_link->SetFont(Label::Body_13); + video_link->SetForegroundColour(wxColour(0x1F, 0x8E, 0xEA)); + video_link->SetCursor(wxCURSOR_HAND); + video_link->Bind(wxEVT_LEFT_DOWN, [](wxMouseEvent&) { wxLaunchDefaultBrowser("https://www.youtube.com", wxBROWSER_NEW_WINDOW); }); + w_sizer->Add(video_link, 0, wxRIGHT | wxLEFT | wxTOP, FromDIP(10)); + w_sizer->Add(f_bar, 0, wxRIGHT | wxLEFT | wxTOP | wxEXPAND, FromDIP(10)); w_sizer->Add(m_outer_tabs, 0, wxRIGHT | wxLEFT | wxTOP | wxEXPAND, FromDIP(10)); w_sizer->Add(m_outer_host, 1, wxRIGHT | wxLEFT | wxTOP | wxEXPAND, FromDIP(10));