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='
'+
'
'+
'
'+
- '
'+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));