mirror of
https://github.com/OrcaSlicer/OrcaSlicer.git
synced 2026-09-27 19:01:02 +00:00
Published 3MF: silent geometry-only fallback in old versions via tag/config-less export
This commit is contained in:
@@ -1214,6 +1214,19 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result)
|
|||||||
// add backup & restore logic
|
// add backup & restore logic
|
||||||
bool _load_model_from_file(std::string filename, Model& model, PlateDataPtrs& plate_data_list, std::vector<Preset*>& project_presets, DynamicPrintConfig& config, ConfigSubstitutionContext& config_substitutions, Import3mfProgressFn proFn = nullptr,
|
bool _load_model_from_file(std::string filename, Model& model, PlateDataPtrs& plate_data_list, std::vector<Preset*>& project_presets, DynamicPrintConfig& config, ConfigSubstitutionContext& config_substitutions, Import3mfProgressFn proFn = nullptr,
|
||||||
BBLProject* project = nullptr, int plate_id = 0);
|
BBLProject* project = nullptr, int plate_id = 0);
|
||||||
|
|
||||||
|
// A minimal published 3MF carries no slicer tags (any tag would make old receivers show
|
||||||
|
// a baked-in, wrong "old version" popup on their geometry-only fallback), so it
|
||||||
|
// classifies as From_Other. It is still a fully structured OrcaSlicer file though:
|
||||||
|
// identified by its own metadata, it keeps BBS-grade geometry handling (no instance
|
||||||
|
// splitting, no transform baking, no renaming) in this build. Old receivers without the
|
||||||
|
// publish feature don't know the metadata and take their third-party geometry path.
|
||||||
|
// Reads the parse-time metadata: the model XML carries it before its resources, while
|
||||||
|
// m_model->model_info is only filled in after the whole XML has been parsed.
|
||||||
|
bool _is_published_3mf() const {
|
||||||
|
return this->model_info.metadata_items.find("published") != this->model_info.metadata_items.end();
|
||||||
|
}
|
||||||
|
|
||||||
bool _is_svg_shape_file(const std::string &filename) const;
|
bool _is_svg_shape_file(const std::string &filename) const;
|
||||||
bool _extract_from_archive(mz_zip_archive& archive, std::string const & path, std::function<bool (mz_zip_archive& archive, const mz_zip_archive_file_stat& stat)>, bool restore = false);
|
bool _extract_from_archive(mz_zip_archive& archive, std::string const & path, std::function<bool (mz_zip_archive& archive, const mz_zip_archive_file_stat& stat)>, bool restore = false);
|
||||||
bool _extract_xml_from_archive(mz_zip_archive& archive, std::string const & path, XML_StartElementHandler start_handler, XML_EndElementHandler end_handler);
|
bool _extract_xml_from_archive(mz_zip_archive& archive, std::string const & path, XML_StartElementHandler start_handler, XML_EndElementHandler end_handler);
|
||||||
@@ -2037,7 +2050,7 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result)
|
|||||||
|
|
||||||
lock.close();
|
lock.close();
|
||||||
|
|
||||||
if (!m_is_bbl_3mf) {
|
if (!m_is_bbl_3mf && !_is_published_3mf()) {
|
||||||
// if the 3mf was not produced by OrcaSlicer and there is more than one instance,
|
// if the 3mf was not produced by OrcaSlicer and there is more than one instance,
|
||||||
// split the object in as many objects as instances
|
// split the object in as many objects as instances
|
||||||
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << ":" << __LINE__ << boost::format(", found 3mf from other vendor, split as instance");
|
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << ":" << __LINE__ << boost::format(", found 3mf from other vendor, split as instance");
|
||||||
@@ -3591,7 +3604,7 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result)
|
|||||||
m_index_paths.insert({ object.first.second, object.first.first});
|
m_index_paths.insert({ object.first.second, object.first.first});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!m_is_bbl_3mf) {
|
if (!m_is_bbl_3mf && !_is_published_3mf()) {
|
||||||
// if the 3mf was not produced by OrcaSlicer and there is only one object,
|
// if the 3mf was not produced by OrcaSlicer and there is only one object,
|
||||||
// set the object name to match the filename
|
// set the object name to match the filename
|
||||||
if (m_model->objects.size() == 1)
|
if (m_model->objects.size() == 1)
|
||||||
@@ -5297,7 +5310,7 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result)
|
|||||||
|
|
||||||
TriangleMesh triangle_mesh(std::move(its), volume_data.mesh_stats);
|
TriangleMesh triangle_mesh(std::move(its), volume_data.mesh_stats);
|
||||||
|
|
||||||
if (!m_is_bbl_3mf) {
|
if (!m_is_bbl_3mf && !_is_published_3mf()) {
|
||||||
// if the 3mf was not produced by OrcaSlicer and there is only one instance,
|
// if the 3mf was not produced by OrcaSlicer and there is only one instance,
|
||||||
// bake the transformation into the geometry to allow the reload from disk command
|
// bake the transformation into the geometry to allow the reload from disk command
|
||||||
// to work properly
|
// to work properly
|
||||||
@@ -5943,7 +5956,7 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result)
|
|||||||
bool m_save_gcode { false }; // whether to save gcode for normal save
|
bool m_save_gcode { false }; // whether to save gcode for normal save
|
||||||
bool m_skip_model { false }; // skip model when exporting .gcode.3mf
|
bool m_skip_model { false }; // skip model when exporting .gcode.3mf
|
||||||
bool m_skip_auxiliary { false }; // skip normal axuiliary files
|
bool m_skip_auxiliary { false }; // skip normal axuiliary files
|
||||||
bool m_minimal_published { false }; // published 3MF: omit embedded preset files
|
bool m_minimal_published { false }; // published 3MF: omit the project config, the embedded preset files and the slicer tags
|
||||||
bool m_use_loaded_id { false }; // whether to use loaded id for identify_id
|
bool m_use_loaded_id { false }; // whether to use loaded id for identify_id
|
||||||
bool m_share_mesh { false }; // whether to share mesh between objects
|
bool m_share_mesh { false }; // whether to share mesh between objects
|
||||||
std::string m_thumbnail_middle = PRINTER_THUMBNAIL_MIDDLE_FILE;
|
std::string m_thumbnail_middle = PRINTER_THUMBNAIL_MIDDLE_FILE;
|
||||||
@@ -6453,7 +6466,10 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result)
|
|||||||
|
|
||||||
// Adds slic3r print config file ("Metadata/Slic3r_PE.config").
|
// Adds slic3r print config file ("Metadata/Slic3r_PE.config").
|
||||||
// This file contains the content of FullPrintConfig / SLAFullPrintConfig.
|
// This file contains the content of FullPrintConfig / SLAFullPrintConfig.
|
||||||
if (config != nullptr) {
|
// Omitted for minimal published 3MF: OrcaSlicer versions without the publish feature
|
||||||
|
// then fall back to importing the geometry only, and new versions read the published
|
||||||
|
// payload from the model metadata instead.
|
||||||
|
if (config != nullptr && !m_minimal_published) {
|
||||||
// BBS: change to json format
|
// BBS: change to json format
|
||||||
// if (!_add_print_config_file_to_archive(archive, *config)) {
|
// if (!_add_print_config_file_to_archive(archive, *config)) {
|
||||||
if (!_add_project_config_file_to_archive(archive, *config, model)) { return false; }
|
if (!_add_project_config_file_to_archive(archive, *config, model)) { return false; }
|
||||||
@@ -6939,8 +6955,13 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result)
|
|||||||
// Orca: PRIVACY: do not store creation & modification date in 3mf
|
// Orca: PRIVACY: do not store creation & modification date in 3mf
|
||||||
metadata_item_map[BBL_CREATION_DATE_TAG] = "";
|
metadata_item_map[BBL_CREATION_DATE_TAG] = "";
|
||||||
metadata_item_map[BBL_MODIFICATION_TAG] = "";
|
metadata_item_map[BBL_MODIFICATION_TAG] = "";
|
||||||
// Orca: Write the BambuStudio compatibility version string using SLIC3R_VERSION
|
// Orca: Write the BambuStudio compatibility version string using SLIC3R_VERSION.
|
||||||
metadata_item_map[BBL_APPLICATION_TAG] = (boost::format("%1%-%2%") % "BambuStudio" % SLIC3R_VERSION).str();
|
// A minimal published 3MF writes no slicer tags at all: any tag would route old
|
||||||
|
// receivers onto a geometry-only fallback whose baked-in popup misreports the
|
||||||
|
// file ("old OrcaSlicer version" / "BambuStudio"), while tag-less files classify
|
||||||
|
// as From_Other and import the geometry silently.
|
||||||
|
if (!m_minimal_published)
|
||||||
|
metadata_item_map[BBL_APPLICATION_TAG] = (boost::format("%1%-%2%") % "BambuStudio" % SLIC3R_VERSION).str();
|
||||||
}
|
}
|
||||||
metadata_item_map[BBS_3MF_VERSION] = std::to_string(VERSION_BBS_3MF);
|
metadata_item_map[BBS_3MF_VERSION] = std::to_string(VERSION_BBS_3MF);
|
||||||
|
|
||||||
@@ -6966,8 +6987,17 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result)
|
|||||||
stream << " <" << METADATA_TAG << " name=\"" << item.first << "\">"
|
stream << " <" << METADATA_TAG << " name=\"" << item.first << "\">"
|
||||||
<< xml_escape(item.second) << "</" << METADATA_TAG << ">\n";
|
<< xml_escape(item.second) << "</" << METADATA_TAG << ">\n";
|
||||||
if (item.first == BBL_APPLICATION_TAG) {
|
if (item.first == BBL_APPLICATION_TAG) {
|
||||||
stream << " <" << METADATA_TAG << " name=\"" << ORCASLICER_TAG << "\">"
|
// The OrcaSlicer tag is the version receivers compare against their own to
|
||||||
<< xml_escape(SoftFever_VERSION) << "</" << METADATA_TAG << ">\n";
|
// pick the import branch, and every graceful config-less branch of an
|
||||||
|
// Orca-classified file shows a baked-in "old OrcaSlicer version" popup. A
|
||||||
|
// minimal published 3MF omits the tag (together with the Application tag
|
||||||
|
// above): old receivers then classify it From_Other and import the geometry
|
||||||
|
// silently, while this build rebuilds the config from the published metadata
|
||||||
|
// payload before the branch runs.
|
||||||
|
if (!m_minimal_published) {
|
||||||
|
stream << " <" << METADATA_TAG << " name=\"" << ORCASLICER_TAG << "\">"
|
||||||
|
<< xml_escape(SoftFever_VERSION) << "</" << METADATA_TAG << ">\n";
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+123
-82
@@ -6881,6 +6881,11 @@ std::vector<size_t> Plater::priv::load_files(const std::vector<fs::path>& input_
|
|||||||
DynamicPrintConfig config;
|
DynamicPrintConfig config;
|
||||||
Semver file_version;
|
Semver file_version;
|
||||||
En3mfType en_3mf_file_type = En3mfType::From_BBS;
|
En3mfType en_3mf_file_type = En3mfType::From_BBS;
|
||||||
|
// BBS: a "published" 3MF carries a flag plus the author-selected setting keys;
|
||||||
|
// on load keep the user's current presets and overlay only those keys. Declared
|
||||||
|
// here (outside the config block below) so it stays alive for the embedded-preset
|
||||||
|
// gate, the metadata strip and the preset overlay after the block closes.
|
||||||
|
PublishedConfig published_config;
|
||||||
{
|
{
|
||||||
DynamicPrintConfig config_loaded;
|
DynamicPrintConfig config_loaded;
|
||||||
|
|
||||||
@@ -6908,6 +6913,102 @@ std::vector<size_t> Plater::priv::load_files(const std::vector<fs::path>& input_
|
|||||||
<< boost::format(", plate_data.size %1%, project_preset.size %2%, is_bbs_or_orca_3mf %3%, file_version %4% \n") % plate_data.size() %
|
<< boost::format(", plate_data.size %1%, project_preset.size %2%, is_bbs_or_orca_3mf %3%, file_version %4% \n") % plate_data.size() %
|
||||||
project_presets.size() % (en_3mf_file_type == En3mfType::From_BBS || en_3mf_file_type == En3mfType::From_Orca) % file_version.to_string();
|
project_presets.size() % (en_3mf_file_type == En3mfType::From_BBS || en_3mf_file_type == En3mfType::From_Orca) % file_version.to_string();
|
||||||
|
|
||||||
|
// BBS: a "published" 3MF carries a flag plus the author-selected setting keys;
|
||||||
|
// on load keep the user's current presets and overlay only those keys. Parsed
|
||||||
|
// here (before the version/fallback chain below) because a published file has
|
||||||
|
// no project_settings.config: its values travel in the published_config
|
||||||
|
// metadata payload, which must fill config_loaded before the chain decides
|
||||||
|
// whether to import geometry only.
|
||||||
|
if (model.model_info != nullptr) {
|
||||||
|
auto published_it = model.model_info->metadata_items.find("published");
|
||||||
|
if (published_it != model.model_info->metadata_items.end() &&
|
||||||
|
(published_it->second == "true" || published_it->second == "1")) {
|
||||||
|
published_config.published = true;
|
||||||
|
auto keys_it = model.model_info->metadata_items.find("published_keys");
|
||||||
|
if (keys_it != model.model_info->metadata_items.end()) {
|
||||||
|
try {
|
||||||
|
auto j = nlohmann::json::parse(keys_it->second);
|
||||||
|
if (j.is_array())
|
||||||
|
for (const auto &k : j)
|
||||||
|
if (k.is_string())
|
||||||
|
published_config.published_keys.emplace_back(k.get<std::string>());
|
||||||
|
} catch (...) {
|
||||||
|
// Ignore malformed published_keys; the project still loads normally.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
auto material_keys_it = model.model_info->metadata_items.find("published_material_keys");
|
||||||
|
if (material_keys_it != model.model_info->metadata_items.end()) {
|
||||||
|
try {
|
||||||
|
auto jm = nlohmann::json::parse(material_keys_it->second);
|
||||||
|
if (jm.is_array())
|
||||||
|
for (const auto &m : jm) {
|
||||||
|
// Malformed entries are skipped individually.
|
||||||
|
if (!m.is_object())
|
||||||
|
continue;
|
||||||
|
PublishedMaterialEntry entry;
|
||||||
|
const auto mat_it = m.find("material");
|
||||||
|
if (mat_it != m.end() && mat_it->is_object()) {
|
||||||
|
const auto &mat = *mat_it;
|
||||||
|
if (mat.contains("filament_type") && mat["filament_type"].is_string())
|
||||||
|
entry.filament_type = mat["filament_type"].get<std::string>();
|
||||||
|
if (mat.contains("filament_vendor") && mat["filament_vendor"].is_string())
|
||||||
|
entry.filament_vendor = mat["filament_vendor"].get<std::string>();
|
||||||
|
if (mat.contains("filament_id") && mat["filament_id"].is_string())
|
||||||
|
entry.filament_id = mat["filament_id"].get<std::string>();
|
||||||
|
if (mat.contains("setting_id") && mat["setting_id"].is_string())
|
||||||
|
entry.setting_id = mat["setting_id"].get<std::string>();
|
||||||
|
if (mat.contains("name") && mat["name"].is_string())
|
||||||
|
entry.preset_name = mat["name"].get<std::string>();
|
||||||
|
}
|
||||||
|
if (m.contains("slot") && m["slot"].is_number_integer())
|
||||||
|
entry.slot = m["slot"].get<int>();
|
||||||
|
const auto entry_keys_it = m.find("keys");
|
||||||
|
if (entry_keys_it != m.end() && entry_keys_it->is_array())
|
||||||
|
for (const auto &k : *entry_keys_it)
|
||||||
|
if (k.is_string())
|
||||||
|
entry.keys.emplace_back(k.get<std::string>());
|
||||||
|
// Fields always written by the current exporter.
|
||||||
|
if (m.contains("full") && m["full"].is_boolean())
|
||||||
|
entry.full = m["full"].get<bool>();
|
||||||
|
const auto entry_full_keys_it = m.find("full_keys");
|
||||||
|
if (entry_full_keys_it != m.end() && entry_full_keys_it->is_array())
|
||||||
|
for (const auto &k : *entry_full_keys_it)
|
||||||
|
if (k.is_string())
|
||||||
|
entry.full_keys.emplace_back(k.get<std::string>());
|
||||||
|
if (m.contains("publish_type") && m["publish_type"].is_boolean())
|
||||||
|
entry.publish_type = m["publish_type"].get<bool>();
|
||||||
|
if (m.contains("type") && m["type"].is_string())
|
||||||
|
entry.publish_type_value = m["type"].get<std::string>();
|
||||||
|
if (m.contains("publish_color") && m["publish_color"].is_boolean())
|
||||||
|
entry.publish_color = m["publish_color"].get<bool>();
|
||||||
|
if (m.contains("color") && m["color"].is_string())
|
||||||
|
entry.color = m["color"].get<std::string>();
|
||||||
|
published_config.material_keys.emplace_back(std::move(entry));
|
||||||
|
}
|
||||||
|
} catch (...) {
|
||||||
|
// Ignore malformed published_material_keys; the project still loads normally.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Rebuild the published values from the metadata payload: a published
|
||||||
|
// file carries no project_settings.config, so config_loaded is filled
|
||||||
|
// from here; a missing or malformed payload leaves it empty and the
|
||||||
|
// fallback chain below imports the geometry only.
|
||||||
|
auto payload_it = model.model_info->metadata_items.find("published_config");
|
||||||
|
if (payload_it != model.model_info->metadata_items.end()) {
|
||||||
|
try {
|
||||||
|
ConfigSubstitutions payload_substitutions = config_loaded.load_from_ini_string(payload_it->second, ForwardCompatibilitySubstitutionRule::Enable);
|
||||||
|
config_substitutions.substitutions.insert(config_substitutions.substitutions.end(),
|
||||||
|
std::make_move_iterator(payload_substitutions.begin()),
|
||||||
|
std::make_move_iterator(payload_substitutions.end()));
|
||||||
|
} catch (...) {
|
||||||
|
// Ignore malformed published_config; the project still loads normally.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 1. add extruder for prusa model if the number of existing extruders is not enough
|
// 1. add extruder for prusa model if the number of existing extruders is not enough
|
||||||
// 2. add extruder for BBS or Other model if only import geometry
|
// 2. add extruder for BBS or Other model if only import geometry
|
||||||
if (en_3mf_file_type == En3mfType::From_Prusa || (load_model && !load_config)) {
|
if (en_3mf_file_type == En3mfType::From_Prusa || (load_model && !load_config)) {
|
||||||
@@ -7071,7 +7172,7 @@ std::vector<size_t> Plater::priv::load_files(const std::vector<fs::path>& input_
|
|||||||
text += "\n";
|
text += "\n";
|
||||||
log_and_show_3mf_info(text, bambu_project_title);
|
log_and_show_3mf_info(text, bambu_project_title);
|
||||||
}
|
}
|
||||||
} else if (load_config) {
|
} else if (load_config && !published_config.published) {
|
||||||
// BambuStudio version is older or same as our SLIC3R_VERSION
|
// BambuStudio version is older or same as our SLIC3R_VERSION
|
||||||
wxString text = _L("The 3MF was created by BambuStudio. Some settings may differ from OrcaSlicer.");
|
wxString text = _L("The 3MF was created by BambuStudio. Some settings may differ from OrcaSlicer.");
|
||||||
log_and_show_3mf_info(text, bambu_project_title);
|
log_and_show_3mf_info(text, bambu_project_title);
|
||||||
@@ -7153,8 +7254,10 @@ std::vector<size_t> Plater::priv::load_files(const std::vector<fs::path>& input_
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// BBS:: project embedded presets
|
// BBS:: project embedded presets (skipped for published projects: the author's
|
||||||
if ((project_presets.size() > 0) && load_config) {
|
// embedded presets must not pollute the receiver's library, the overlay applies
|
||||||
|
// the published keys to the receiver's own presets instead).
|
||||||
|
if ((project_presets.size() > 0) && load_config && !published_config.published) {
|
||||||
// load project embedded presets
|
// load project embedded presets
|
||||||
PresetsConfigSubstitutions preset_substitutions;
|
PresetsConfigSubstitutions preset_substitutions;
|
||||||
PresetBundle & preset_bundle = *wxGetApp().preset_bundle;
|
PresetBundle & preset_bundle = *wxGetApp().preset_bundle;
|
||||||
@@ -7210,83 +7313,6 @@ std::vector<size_t> Plater::priv::load_files(const std::vector<fs::path>& input_
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// BBS: a "published" 3MF carries a flag plus the author-selected setting keys;
|
|
||||||
// on load keep the user's current presets and overlay only those keys.
|
|
||||||
PublishedConfig published_config;
|
|
||||||
if (model.model_info != nullptr) {
|
|
||||||
auto published_it = model.model_info->metadata_items.find("published");
|
|
||||||
if (published_it != model.model_info->metadata_items.end() &&
|
|
||||||
(published_it->second == "true" || published_it->second == "1")) {
|
|
||||||
published_config.published = true;
|
|
||||||
auto keys_it = model.model_info->metadata_items.find("published_keys");
|
|
||||||
if (keys_it != model.model_info->metadata_items.end()) {
|
|
||||||
try {
|
|
||||||
auto j = nlohmann::json::parse(keys_it->second);
|
|
||||||
if (j.is_array())
|
|
||||||
for (const auto &k : j)
|
|
||||||
if (k.is_string())
|
|
||||||
published_config.published_keys.emplace_back(k.get<std::string>());
|
|
||||||
} catch (...) {
|
|
||||||
// Ignore malformed published_keys; the project still loads normally.
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
auto material_keys_it = model.model_info->metadata_items.find("published_material_keys");
|
|
||||||
if (material_keys_it != model.model_info->metadata_items.end()) {
|
|
||||||
try {
|
|
||||||
auto jm = nlohmann::json::parse(material_keys_it->second);
|
|
||||||
if (jm.is_array())
|
|
||||||
for (const auto &m : jm) {
|
|
||||||
// Malformed entries are skipped individually.
|
|
||||||
if (!m.is_object())
|
|
||||||
continue;
|
|
||||||
PublishedMaterialEntry entry;
|
|
||||||
const auto mat_it = m.find("material");
|
|
||||||
if (mat_it != m.end() && mat_it->is_object()) {
|
|
||||||
const auto &mat = *mat_it;
|
|
||||||
if (mat.contains("filament_type") && mat["filament_type"].is_string())
|
|
||||||
entry.filament_type = mat["filament_type"].get<std::string>();
|
|
||||||
if (mat.contains("filament_vendor") && mat["filament_vendor"].is_string())
|
|
||||||
entry.filament_vendor = mat["filament_vendor"].get<std::string>();
|
|
||||||
if (mat.contains("filament_id") && mat["filament_id"].is_string())
|
|
||||||
entry.filament_id = mat["filament_id"].get<std::string>();
|
|
||||||
if (mat.contains("setting_id") && mat["setting_id"].is_string())
|
|
||||||
entry.setting_id = mat["setting_id"].get<std::string>();
|
|
||||||
if (mat.contains("name") && mat["name"].is_string())
|
|
||||||
entry.preset_name = mat["name"].get<std::string>();
|
|
||||||
}
|
|
||||||
if (m.contains("slot") && m["slot"].is_number_integer())
|
|
||||||
entry.slot = m["slot"].get<int>();
|
|
||||||
const auto entry_keys_it = m.find("keys");
|
|
||||||
if (entry_keys_it != m.end() && entry_keys_it->is_array())
|
|
||||||
for (const auto &k : *entry_keys_it)
|
|
||||||
if (k.is_string())
|
|
||||||
entry.keys.emplace_back(k.get<std::string>());
|
|
||||||
// Fields always written by the current exporter.
|
|
||||||
if (m.contains("full") && m["full"].is_boolean())
|
|
||||||
entry.full = m["full"].get<bool>();
|
|
||||||
const auto entry_full_keys_it = m.find("full_keys");
|
|
||||||
if (entry_full_keys_it != m.end() && entry_full_keys_it->is_array())
|
|
||||||
for (const auto &k : *entry_full_keys_it)
|
|
||||||
if (k.is_string())
|
|
||||||
entry.full_keys.emplace_back(k.get<std::string>());
|
|
||||||
if (m.contains("publish_type") && m["publish_type"].is_boolean())
|
|
||||||
entry.publish_type = m["publish_type"].get<bool>();
|
|
||||||
if (m.contains("type") && m["type"].is_string())
|
|
||||||
entry.publish_type_value = m["type"].get<std::string>();
|
|
||||||
if (m.contains("publish_color") && m["publish_color"].is_boolean())
|
|
||||||
entry.publish_color = m["publish_color"].get<bool>();
|
|
||||||
if (m.contains("color") && m["color"].is_string())
|
|
||||||
entry.color = m["color"].get<std::string>();
|
|
||||||
published_config.material_keys.emplace_back(std::move(entry));
|
|
||||||
}
|
|
||||||
} catch (...) {
|
|
||||||
// Ignore malformed published_material_keys; the project still loads normally.
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// BBS: a "published" 3MF loads as a new project: its path must not become the
|
// BBS: a "published" 3MF loads as a new project: its path must not become the
|
||||||
// project filename (Save/Ctrl-S would overwrite the shared file), and the
|
// project filename (Save/Ctrl-S would overwrite the shared file), and the
|
||||||
// published metadata is consumed above and stripped so a later save is a normal
|
// published metadata is consumed above and stripped so a later save is a normal
|
||||||
@@ -7297,6 +7323,7 @@ std::vector<size_t> Plater::priv::load_files(const std::vector<fs::path>& input_
|
|||||||
this->model.model_info->metadata_items.erase("published");
|
this->model.model_info->metadata_items.erase("published");
|
||||||
this->model.model_info->metadata_items.erase("published_keys");
|
this->model.model_info->metadata_items.erase("published_keys");
|
||||||
this->model.model_info->metadata_items.erase("published_material_keys");
|
this->model.model_info->metadata_items.erase("published_material_keys");
|
||||||
|
this->model.model_info->metadata_items.erase("published_config");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (load_config) {
|
if (load_config) {
|
||||||
@@ -16265,9 +16292,11 @@ int Plater::export_published_3mf(const std::vector<std::string>& published_keys,
|
|||||||
const bool had_published = had_model_info && (model.model_info->metadata_items.find("published") != model.model_info->metadata_items.end());
|
const bool had_published = had_model_info && (model.model_info->metadata_items.find("published") != model.model_info->metadata_items.end());
|
||||||
const bool had_published_keys = had_model_info && (model.model_info->metadata_items.find("published_keys") != model.model_info->metadata_items.end());
|
const bool had_published_keys = had_model_info && (model.model_info->metadata_items.find("published_keys") != model.model_info->metadata_items.end());
|
||||||
const bool had_material_keys = had_model_info && (model.model_info->metadata_items.find("published_material_keys") != model.model_info->metadata_items.end());
|
const bool had_material_keys = had_model_info && (model.model_info->metadata_items.find("published_material_keys") != model.model_info->metadata_items.end());
|
||||||
|
const bool had_payload = had_model_info && (model.model_info->metadata_items.find("published_config") != model.model_info->metadata_items.end());
|
||||||
const std::string prev_published = had_published ? model.model_info->metadata_items.at("published") : std::string();
|
const std::string prev_published = had_published ? model.model_info->metadata_items.at("published") : std::string();
|
||||||
const std::string prev_published_keys = had_published_keys ? model.model_info->metadata_items.at("published_keys") : std::string();
|
const std::string prev_published_keys = had_published_keys ? model.model_info->metadata_items.at("published_keys") : std::string();
|
||||||
const std::string prev_material_keys = had_material_keys ? model.model_info->metadata_items.at("published_material_keys") : std::string();
|
const std::string prev_material_keys = had_material_keys ? model.model_info->metadata_items.at("published_material_keys") : std::string();
|
||||||
|
const std::string prev_payload = had_payload ? model.model_info->metadata_items.at("published_config") : std::string();
|
||||||
if (model.model_info == nullptr)
|
if (model.model_info == nullptr)
|
||||||
model.model_info = std::make_shared<ModelInfo>();
|
model.model_info = std::make_shared<ModelInfo>();
|
||||||
model.model_info->metadata_items["published"] = "1";
|
model.model_info->metadata_items["published"] = "1";
|
||||||
@@ -16275,9 +16304,17 @@ int Plater::export_published_3mf(const std::vector<std::string>& published_keys,
|
|||||||
model.model_info->metadata_items["published_material_keys"] = jm.dump();
|
model.model_info->metadata_items["published_material_keys"] = jm.dump();
|
||||||
|
|
||||||
// Minimal published export: filter full_config to the published keys, material keys,
|
// Minimal published export: filter full_config to the published keys, material keys,
|
||||||
// identity fields and plate geometry keys, and omit project-embedded preset dumps.
|
// identity fields and plate geometry keys, and omit the project config file, the
|
||||||
|
// project-embedded preset dumps and the OrcaSlicer version tag from the archive. The
|
||||||
|
// filtered values are serialized into the published_config metadata payload instead, so
|
||||||
|
// OrcaSlicer versions without the publish feature fall back to importing the geometry only
|
||||||
|
// (keeping the receiver's presets) while new versions rebuild the config from the payload.
|
||||||
DynamicPrintConfig full_cfg = wxGetApp().preset_bundle->full_config_secure();
|
DynamicPrintConfig full_cfg = wxGetApp().preset_bundle->full_config_secure();
|
||||||
DynamicPrintConfig filtered_cfg = filter_published_config(full_cfg, published_keys, material_keys);
|
DynamicPrintConfig filtered_cfg = filter_published_config(full_cfg, published_keys, material_keys);
|
||||||
|
std::string payload;
|
||||||
|
for (const std::string &key : filtered_cfg.keys())
|
||||||
|
payload += key + " = " + filtered_cfg.opt_serialize(key) + "\n";
|
||||||
|
model.model_info->metadata_items["published_config"] = std::move(payload);
|
||||||
|
|
||||||
// Same file layout as save_project(), plus Silence (so export_3mf does not set the project
|
// Same file layout as save_project(), plus Silence (so export_3mf does not set the project
|
||||||
// filename on success, keeping this a pure export like export_core_3mf()) and MinimalPublished.
|
// filename on success, keeping this a pure export like export_core_3mf()) and MinimalPublished.
|
||||||
@@ -16286,7 +16323,7 @@ int Plater::export_published_3mf(const std::vector<std::string>& published_keys,
|
|||||||
if (full_pathnames)
|
if (full_pathnames)
|
||||||
save_strategy = save_strategy | SaveStrategy::FullPathSources;
|
save_strategy = save_strategy | SaveStrategy::FullPathSources;
|
||||||
|
|
||||||
const int ret = export_3mf(into_path(path), save_strategy, -1, nullptr, &filtered_cfg);
|
const int ret = export_3mf(into_path(path), save_strategy, -1, nullptr);
|
||||||
|
|
||||||
// Restore the previous metadata state (both on success and on failure).
|
// Restore the previous metadata state (both on success and on failure).
|
||||||
if (!had_model_info) {
|
if (!had_model_info) {
|
||||||
@@ -16304,6 +16341,10 @@ int Plater::export_published_3mf(const std::vector<std::string>& published_keys,
|
|||||||
model.model_info->metadata_items["published_material_keys"] = prev_material_keys;
|
model.model_info->metadata_items["published_material_keys"] = prev_material_keys;
|
||||||
else
|
else
|
||||||
model.model_info->metadata_items.erase("published_material_keys");
|
model.model_info->metadata_items.erase("published_material_keys");
|
||||||
|
if (had_payload)
|
||||||
|
model.model_info->metadata_items["published_config"] = prev_payload;
|
||||||
|
else
|
||||||
|
model.model_info->metadata_items.erase("published_config");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (ret < 0) {
|
if (ret < 0) {
|
||||||
|
|||||||
@@ -671,12 +671,15 @@ SCENARIO("Published 3MF round-trips the published_material_keys metadata", "[3mf
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
SCENARIO("Minimal published 3MF serialization filters config and omits embedded presets", "[3mf]") {
|
SCENARIO("Minimal published 3MF omits project config, preset dumps and slicer tags", "[3mf]") {
|
||||||
GIVEN("a full print configuration and published keys") {
|
GIVEN("a multi-instance model carrying published metadata and a published_config payload") {
|
||||||
Model model;
|
Model model;
|
||||||
std::string src_file = std::string(TEST_DATA_DIR) + "/test_3mf/Prusa.stl";
|
std::string src_file = std::string(TEST_DATA_DIR) + "/test_3mf/Prusa.stl";
|
||||||
REQUIRE(load_stl(src_file.c_str(), &model));
|
REQUIRE(load_stl(src_file.c_str(), &model));
|
||||||
model.add_default_instances();
|
model.add_default_instances();
|
||||||
|
// A second instance: tag-less third-party files get their multi-instance objects split,
|
||||||
|
// published files must not (the loader recognizes them by their metadata).
|
||||||
|
model.objects.front()->add_instance();
|
||||||
|
|
||||||
DynamicPrintConfig full_cfg = DynamicPrintConfig::full_print_config();
|
DynamicPrintConfig full_cfg = DynamicPrintConfig::full_print_config();
|
||||||
full_cfg.set_key_value("layer_height", new ConfigOptionFloat(0.24));
|
full_cfg.set_key_value("layer_height", new ConfigOptionFloat(0.24));
|
||||||
@@ -687,27 +690,30 @@ SCENARIO("Minimal published 3MF serialization filters config and omits embedded
|
|||||||
{ "PLA", "Generic", "GFL99", "", "Generic PLA", 0, { "filament_retraction_length" } }
|
{ "PLA", "Generic", "GFL99", "", "Generic PLA", 0, { "filament_retraction_length" } }
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// The payload builder keeps the published and identity keys and drops everything else.
|
||||||
DynamicPrintConfig filtered_cfg = filter_published_config(full_cfg, published_keys, material_keys);
|
DynamicPrintConfig filtered_cfg = filter_published_config(full_cfg, published_keys, material_keys);
|
||||||
|
|
||||||
// Filtered config keeps the published and identity keys...
|
|
||||||
REQUIRE(filtered_cfg.option("layer_height") != nullptr);
|
REQUIRE(filtered_cfg.option("layer_height") != nullptr);
|
||||||
REQUIRE(filtered_cfg.option("retraction_length") != nullptr);
|
REQUIRE(filtered_cfg.option("retraction_length") != nullptr);
|
||||||
REQUIRE(filtered_cfg.option("filament_colour") != nullptr);
|
REQUIRE(filtered_cfg.option("filament_colour") != nullptr);
|
||||||
REQUIRE(filtered_cfg.option("filament_type") != nullptr);
|
REQUIRE(filtered_cfg.option("filament_type") != nullptr);
|
||||||
REQUIRE(filtered_cfg.option("wipe_tower_x") != nullptr);
|
REQUIRE(filtered_cfg.option("wipe_tower_x") != nullptr);
|
||||||
|
|
||||||
// ...and drops everything else.
|
|
||||||
REQUIRE(filtered_cfg.option("sparse_infill_density") == nullptr);
|
REQUIRE(filtered_cfg.option("sparse_infill_density") == nullptr);
|
||||||
REQUIRE(filtered_cfg.option("machine_start_gcode") == nullptr);
|
REQUIRE(filtered_cfg.option("machine_start_gcode") == nullptr);
|
||||||
|
|
||||||
|
// Serialize the payload exactly like export_published_3mf does.
|
||||||
|
std::string payload;
|
||||||
|
for (const std::string &key : filtered_cfg.keys())
|
||||||
|
payload += key + " = " + filtered_cfg.opt_serialize(key) + "\n";
|
||||||
|
|
||||||
model.model_info = std::make_shared<ModelInfo>();
|
model.model_info = std::make_shared<ModelInfo>();
|
||||||
model.model_info->metadata_items["published"] = "1";
|
model.model_info->metadata_items["published"] = "1";
|
||||||
model.model_info->metadata_items["published_keys"] = R"(["layer_height","retraction_length"])";
|
model.model_info->metadata_items["published_keys"] = R"(["layer_height","retraction_length"])";
|
||||||
|
model.model_info->metadata_items["published_config"] = payload;
|
||||||
|
|
||||||
ScopedTemporaryDir backup_dir("orca_min_pub");
|
ScopedTemporaryDir backup_dir("orca_min_pub");
|
||||||
model.set_backup_path(backup_dir.string());
|
model.set_backup_path(backup_dir.string());
|
||||||
|
|
||||||
WHEN("stored using SaveStrategy::MinimalPublished") {
|
WHEN("stored using SaveStrategy::MinimalPublished and reloaded") {
|
||||||
ScopedTemporaryFile temp(".3mf");
|
ScopedTemporaryFile temp(".3mf");
|
||||||
const std::string test_file = temp.string();
|
const std::string test_file = temp.string();
|
||||||
|
|
||||||
@@ -736,12 +742,35 @@ SCENARIO("Minimal published 3MF serialization filters config and omits embedded
|
|||||||
bool loaded = load_bbs_3mf(test_file.c_str(), &dst_config, &ctxt, &dst_model, &dst_plates,
|
bool loaded = load_bbs_3mf(test_file.c_str(), &dst_config, &ctxt, &dst_model, &dst_plates,
|
||||||
&loaded_presets, &is_bbl_3mf, &is_orca_3mf, &file_version, nullptr,
|
&loaded_presets, &is_bbl_3mf, &is_orca_3mf, &file_version, nullptr,
|
||||||
LoadStrategy::LoadModel | LoadStrategy::LoadConfig);
|
LoadStrategy::LoadModel | LoadStrategy::LoadConfig);
|
||||||
THEN("the 3MF loads successfully without project embedded presets") {
|
THEN("the 3MF loads without project config or embedded presets") {
|
||||||
REQUIRE(loaded);
|
REQUIRE(loaded);
|
||||||
|
REQUIRE(dst_config.empty());
|
||||||
REQUIRE(loaded_presets.empty());
|
REQUIRE(loaded_presets.empty());
|
||||||
REQUIRE(dst_config.option("layer_height") != nullptr);
|
}
|
||||||
REQUIRE_THAT(dst_config.opt_float("layer_height"), Catch::Matchers::WithinAbs(0.24, 1e-6));
|
THEN("the file carries no slicer tags and classifies as a generic 3MF") {
|
||||||
REQUIRE(dst_config.option("sparse_infill_density") == nullptr);
|
REQUIRE_FALSE(is_bbl_3mf);
|
||||||
|
REQUIRE_FALSE(is_orca_3mf);
|
||||||
|
// No Application / OrcaSlicer tag: old receivers import the geometry silently
|
||||||
|
// instead of showing a baked-in, wrong "old version" popup.
|
||||||
|
REQUIRE_FALSE(file_version.valid());
|
||||||
|
}
|
||||||
|
THEN("the geometry keeps BBS-grade handling: instances are not split") {
|
||||||
|
REQUIRE(dst_model.objects.size() == 1);
|
||||||
|
REQUIRE(dst_model.objects.front()->instances.size() == 2);
|
||||||
|
}
|
||||||
|
THEN("the published metadata and payload round-trip unchanged") {
|
||||||
|
REQUIRE(dst_model.model_info != nullptr);
|
||||||
|
REQUIRE(dst_model.model_info->metadata_items["published"] == "1");
|
||||||
|
REQUIRE(dst_model.model_info->metadata_items["published_keys"] == R"(["layer_height","retraction_length"])");
|
||||||
|
REQUIRE(dst_model.model_info->metadata_items["published_config"] == payload);
|
||||||
|
}
|
||||||
|
THEN("the payload parses back to the published values") {
|
||||||
|
DynamicPrintConfig parsed_payload;
|
||||||
|
parsed_payload.load_from_ini_string(dst_model.model_info->metadata_items["published_config"], ForwardCompatibilitySubstitutionRule::Enable);
|
||||||
|
REQUIRE(parsed_payload.option("layer_height") != nullptr);
|
||||||
|
REQUIRE_THAT(parsed_payload.opt_float("layer_height"), Catch::Matchers::WithinAbs(0.24, 1e-6));
|
||||||
|
REQUIRE(parsed_payload.option("retraction_length") != nullptr);
|
||||||
|
REQUIRE_THAT(parsed_payload.opt<ConfigOptionFloats>("retraction_length")->get_at(0), Catch::Matchers::WithinAbs(1.2, 1e-6));
|
||||||
}
|
}
|
||||||
release_PlateData_list(dst_plates);
|
release_PlateData_list(dst_plates);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user