From 904796cf24eb64a336bdae22263260822b2be818 Mon Sep 17 00:00:00 2001 From: Lam Wei Lun Date: Thu, 3 Sep 2026 14:12:51 +0800 Subject: [PATCH] Correctness fixes. Remove hard-coded appends for printer settings --- src/libslic3r/Format/bbs_3mf.cpp | 119 +++++++++++++++++++++++++------ src/slic3r/GUI/Plater.cpp | 6 +- src/slic3r/GUI/Tab.cpp | 31 +++----- 3 files changed, 111 insertions(+), 45 deletions(-) diff --git a/src/libslic3r/Format/bbs_3mf.cpp b/src/libslic3r/Format/bbs_3mf.cpp index 4b9c666ce0..20e63e9d5e 100644 --- a/src/libslic3r/Format/bbs_3mf.cpp +++ b/src/libslic3r/Format/bbs_3mf.cpp @@ -6999,16 +6999,24 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result) // as From_Other and import the geometry silently. if (m_minimal_published) { // metadata_item_map is seeded from the input file's metadata_items above, so a - // project opened from a regular Orca/BBS 3MF still carries the Application / - // OrcaSlicer tags it came with. Erase them: skipping the overwrite is not enough, - // and an empty value would still emit a "present-looking" tag to old receivers. + // project opened from a regular Orca/BBS 3MF still carries the slicer-identifying + // tags it came with. Erase every one of them - not just the two most common - + // so a published 3MF is fully tag-less: old receivers classify it as From_Other + // and import the geometry silently instead of showing a baked-in "old version" + // popup, and no version marker survives to seed a later re-save. metadata_item_map.erase(BBL_APPLICATION_TAG); metadata_item_map.erase(ORCASLICER_TAG); + metadata_item_map.erase(BBS_3MF_VERSION); + metadata_item_map.erase(BBS_3MF_VERSION1); } else { 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); + // The Bambu 3MF version marker is part of the slicer identity: omit it for a minimal + // published file along with the tags erased above (skipping the overwrite alone would + // leave the value the source file seeded into metadata_item_map). + if (!m_minimal_published) + metadata_item_map[BBS_3MF_VERSION] = std::to_string(VERSION_BBS_3MF); if (!model.mk_name.empty()) { metadata_item_map[BBL_MAKERLAB_TAG] = xml_escape(model.mk_name); @@ -7031,10 +7039,11 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result) BOOST_LOG_TRIVIAL(info) << "bbs_3mf: save key= " << item.first << ", value = " << item.second; stream << " <" << METADATA_TAG << " name=\"" << item.first << "\">" << xml_escape(item.second) << "\n"; - if (item.first == BBL_APPLICATION_TAG) { + if (item.first == BBL_APPLICATION_TAG && !m_minimal_published) { // The OrcaSlicer tag is only written for files that carry the Application // tag, which a minimal published 3MF erases (see the map assignment above): - // the branch below is unreachable in minimal mode. + // the explicit !m_minimal_published guard keeps the tag-less guarantee from + // depending on that erase happening to run first. stream << " <" << METADATA_TAG << " name=\"" << ORCASLICER_TAG << "\">" << xml_escape(SoftFever_VERSION) << "\n"; } @@ -9213,6 +9222,68 @@ std::string bbs_3mf_get_thumbnail(const char *path) return data; } +namespace { + +// Parses just the model-file elements, mirroring the importer's +// _handle_start_metadata/_handle_end_metadata (attribute-order independent, entity-unescaped, +// whitespace tolerant). Stops the parser as soon as the published flag node is read so the +// geometry/resources that follow are skipped, which keeps the per-file cost small. +struct PublishedXmlProbe +{ + XML_Parser parser{nullptr}; + bool in_metadata{false}; + bool found{false}; + bool published{false}; + std::string curr_name; + std::string curr_value; + + static std::string attribute(const char** attrs, const char* key) + { + if (attrs == nullptr) + return std::string(); + // expat hands the attrs as a NULL-terminated {name, value, ...} array. + for (unsigned int a = 0; attrs[a] != nullptr; a += 2) + if (::strcmp(attrs[a], key) == 0 && attrs[a + 1] != nullptr) + return attrs[a + 1]; + return std::string(); + } + + static void XMLCALL start(void* user_data, const char* name, const char** attrs) + { + auto* self = static_cast(user_data); + if (::strcmp(name, METADATA_TAG) == 0) { + self->in_metadata = true; + self->curr_name = attribute(attrs, NAME_ATTR); + self->curr_value.clear(); + } else { + self->in_metadata = false; + } + } + + static void XMLCALL characters(void* user_data, const XML_Char* s, int len) + { + auto* self = static_cast(user_data); + if (self->in_metadata) + self->curr_value.append(s, len); + } + + static void XMLCALL end(void* user_data, const char* name) + { + auto* self = static_cast(user_data); + if (!self->in_metadata || ::strcmp(name, METADATA_TAG) != 0) + return; + self->in_metadata = false; + if (self->curr_name == ORCA_PUBLISHED_TAG) { + self->published = is_published_3mf_flag(xml_unescape(self->curr_value)); + self->found = true; + if (self->parser != nullptr) + XML_StopParser(self->parser, false); + } + } +}; + +} // namespace + bool bbs_3mf_is_published(const std::string &path) { mz_zip_archive archive; @@ -9234,7 +9305,8 @@ bool bbs_3mf_is_published(const std::string &path) if (!open_zip_reader(&archive, path)) return false; - // Read just the model XML and locate the published metadata node; no geometry parsing. + // Read just the model XML (the metadata node sits before the resources, so the probe below + // stops early) rather than by a raw substring match; no geometry parsing. int index = mz_zip_reader_locate_file(&archive, MODEL_FILE.c_str(), nullptr, 0); if (index < 0) return false; @@ -9245,22 +9317,29 @@ bool bbs_3mf_is_published(const std::string &path) 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) + XML_Parser parser = XML_ParserCreate(nullptr); + if (parser == nullptr) 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; + PublishedXmlProbe probe; + probe.parser = parser; + XML_SetUserData(parser, &probe); + XML_SetElementHandler(parser, PublishedXmlProbe::start, PublishedXmlProbe::end); + XML_SetCharacterDataHandler(parser, PublishedXmlProbe::characters); + // Never resolve external entities from a file we are only probing. + XML_SetExternalEntityRefHandler(parser, nullptr); + XML_SetEntityDeclHandler(parser, nullptr); - return is_published_3mf_flag(xml.substr(value_begin, value_end - value_begin)); + const XML_Status status = XML_Parse(parser, xml.data(), static_cast(xml.size()), 1); + // XML_StopParser(parser, false) from the end handler makes XML_Parse return + // XML_STATUS_ERROR with XML_ERROR_ABORTED - treat that as success (we stopped on the flag). + const bool parse_ok = (status == XML_STATUS_OK) || + (XML_GetErrorCode(parser) == XML_ERROR_ABORTED && probe.found); + XML_ParserFree(parser); + + if (!parse_ok) + return false; + return probe.published; } 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/Plater.cpp b/src/slic3r/GUI/Plater.cpp index a1915a1200..86815bb7ab 100644 --- a/src/slic3r/GUI/Plater.cpp +++ b/src/slic3r/GUI/Plater.cpp @@ -8752,7 +8752,9 @@ std::vector Plater::priv::load_files(const std::vector& input_ } Semver old_version(1, 5, 9); - if ((en_3mf_file_type == En3mfType::From_BBS || en_3mf_file_type == En3mfType::From_Orca) && (file_version < old_version) && load_model && load_config && !config_loaded.empty()) { + // A published 3MF has no project config to migrate: skip the old-version + // translations even if a slicer tag slipped through classification. + if ((en_3mf_file_type == En3mfType::From_BBS || en_3mf_file_type == En3mfType::From_Orca) && (file_version < old_version) && !published_config.published && load_model && load_config && !config_loaded.empty()) { translate_old = true; partplate_list.get_plate_size(current_width, current_depth, current_height); } @@ -8854,7 +8856,7 @@ std::vector Plater::priv::load_files(const std::vector& input_ { // BBS: modify the prime tower params for old version file Semver old_version3(2, 0, 0); - if ((en_3mf_file_type == En3mfType::From_BBS || en_3mf_file_type == En3mfType::From_Orca) && file_version < old_version3) { + if ((en_3mf_file_type == En3mfType::From_BBS || en_3mf_file_type == En3mfType::From_Orca) && !published_config.published && file_version < old_version3) { double old_filament_prime_volume = 0.; int filament_count = 0; { diff --git a/src/slic3r/GUI/Tab.cpp b/src/slic3r/GUI/Tab.cpp index 96382855ad..962d59d9ca 100644 --- a/src/slic3r/GUI/Tab.cpp +++ b/src/slic3r/GUI/Tab.cpp @@ -7,6 +7,7 @@ #include "libslic3r/FilamentMixer.hpp" #include "libslic3r/Utils.hpp" #include "libslic3r/Model.hpp" +#include "libslic3r/PublishSettings.hpp" #include "libslic3r/GCode/GCodeProcessor.hpp" #include "Search.hpp" @@ -5699,32 +5700,16 @@ if (is_marlin_flavor) optgroup->append_single_option_line("extruder_offset", "printer_extruder_basic_information#extruder-offset-position", extruder_idx); //BBS: don't show retract related config menu in machine page - // Keep this optgroup's options in sync with publishable_printer_retraction_options() - // in libslic3r/PublishSettings.hpp: the published-3MF printer allowlist is its union - // with the Z-Hop optgroup below. + // These optgroups are built from publishable_printer_retraction/z_hop_options() so the + // published-3MF printer allowlist (their union in libslic3r/PublishSettings.hpp) can + // never drift from what the machine page actually shows. optgroup = page->new_optgroup(L("Retraction"), L"param_retraction"); - optgroup->append_single_option_line("retraction_length", "printer_extruder_retraction#length", extruder_idx); - optgroup->append_single_option_line("retract_restart_extra", "printer_extruder_retraction#extra-length-on-restart", extruder_idx); - optgroup->append_single_option_line("retraction_speed", "printer_extruder_retraction#retraction-speed", extruder_idx); - optgroup->append_single_option_line("deretraction_speed", "printer_extruder_retraction#deretraction-speed", extruder_idx); - optgroup->append_single_option_line("retraction_minimum_travel", "printer_extruder_retraction#travel-distance-threshold", extruder_idx); - optgroup->append_single_option_line("retract_when_changing_layer", "printer_extruder_retraction#retract-on-layer-change", extruder_idx); - optgroup->append_single_option_line("wipe", "printer_extruder_retraction#wipe-while-retracting", extruder_idx); - optgroup->append_single_option_line("wipe_distance", "printer_extruder_retraction#wipe-distance", extruder_idx); - optgroup->append_single_option_line("retract_before_wipe", "printer_extruder_retraction#retract-amount-before-wipe", extruder_idx); - // Orca - optgroup->append_single_option_line("retract_after_wipe", "printer_extruder_retraction#retract-amount-after-wipe", extruder_idx); + for (const PublishablePrinterOption& opt : publishable_printer_retraction_options()) + optgroup->append_single_option_line(opt.key, opt.icon, extruder_idx); - // Keep this optgroup's options in sync with publishable_printer_z_hop_options() - // in libslic3r/PublishSettings.hpp: the published-3MF printer allowlist is its union - // with the Retraction optgroup above. optgroup = page->new_optgroup(L("Z-Hop"), L"param_extruder_lift_enforcement"); - optgroup->append_single_option_line("retract_lift_enforce", "printer_extruder_z_hop#on-surfaces", extruder_idx); - optgroup->append_single_option_line("z_hop_types", "printer_extruder_z_hop#z-hop-type", extruder_idx); - optgroup->append_single_option_line("z_hop", "printer_extruder_z_hop#z-hop-height", extruder_idx); - optgroup->append_single_option_line("travel_slope", "printer_extruder_z_hop#traveling-angle", extruder_idx); - optgroup->append_single_option_line("retract_lift_above", "printer_extruder_z_hop#only-lift-z-above", extruder_idx); - optgroup->append_single_option_line("retract_lift_below", "printer_extruder_z_hop#only-lift-z-below", extruder_idx); + for (const PublishablePrinterOption& opt : publishable_printer_z_hop_options()) + optgroup->append_single_option_line(opt.key, opt.icon, extruder_idx); optgroup = page->new_optgroup(L("Retraction when switching material"), L"param_retraction_material_change"); optgroup->append_single_option_line("retract_length_toolchange", "printer_extruder_retraction#retraction-when-switching-materials", extruder_idx);