Keep the CAD recipe in the one 3mf backend that actually runs

Format/3mf.cpp also saved and loaded it, but nothing calls its store_3mf and
its load_3mf only sees files fingerprinted as PrusaSlicer's, which never carry
a recipe. Its round-trip test only exercised that dead loop. The BBS backend,
which every save and load goes through, is untouched.
This commit is contained in:
SoftFever
2026-08-31 18:31:37 +08:00
parent 493befecc5
commit 507b45431c
2 changed files with 10 additions and 103 deletions
-37
View File
@@ -73,10 +73,6 @@ const std::string THUMBNAIL_FILE = "Metadata/thumbnail.png";
const std::string PRINT_CONFIG_FILE = "Metadata/Slic3r_PE.config";
const std::string MODEL_CONFIG_FILE = "Metadata/Slic3r_PE_model.config";
const std::string LAYER_HEIGHTS_PROFILE_FILE = "Metadata/Slic3r_PE_layer_heights_profile.txt";
const std::string CAD_RECIPE_FILE = "Metadata/orca_cad.bin";
// Read-only: the recipe entry's pre-rename name. A reader that knows only the new one drops the
// feature tree of every project written before the move, without a word. Never written.
const std::string LEGACY_CAD_RECIPE_FILE = "Metadata/SnapOrca_cad.bin";
const std::string LAYER_CONFIG_RANGES_FILE = "Metadata/Prusa_Slicer_layer_config_ranges.xml";
const std::string SLA_SUPPORT_POINTS_FILE = "Metadata/Slic3r_PE_sla_support_points.txt";
const std::string SLA_DRAIN_HOLES_FILE = "Metadata/Slic3r_PE_sla_drain_holes.txt";
@@ -809,18 +805,6 @@ ModelVolumeType type_from_string(const std::string &s)
return false;
}
}
if (boost::algorithm::iequals(name, CAD_RECIPE_FILE)
|| boost::algorithm::iequals(name, LEGACY_CAD_RECIPE_FILE)) {
if (stat.m_uncomp_size > 0) {
std::string buffer((size_t)stat.m_uncomp_size, 0);
if (mz_zip_reader_extract_file_to_mem(&archive, stat.m_filename,
(void*)buffer.data(), (size_t)stat.m_uncomp_size, 0) != 0) {
model.cad_recipe = std::move(buffer);
} else {
add_error("Error while reading CAD recipe data");
}
}
}
}
}
@@ -2333,7 +2317,6 @@ ModelVolumeType type_from_string(const std::string &s)
bool _add_mesh_to_object_stream(mz_zip_writer_staged_context &context, ModelObject& object, VolumeToOffsetsMap& volumes_offsets);
bool _add_build_to_model_stream(std::stringstream& stream, const BuildItemsList& build_items);
bool _add_layer_height_profile_file_to_archive(mz_zip_archive& archive, Model& model);
bool _add_cad_recipe_file_to_archive(mz_zip_archive& archive, Model& model);
bool _add_layer_config_ranges_file_to_archive(mz_zip_archive& archive, Model& model);
bool _add_sla_support_points_file_to_archive(mz_zip_archive& archive, Model& model);
bool _add_sla_drain_holes_file_to_archive(mz_zip_archive& archive, Model& model);
@@ -2404,13 +2387,6 @@ ModelVolumeType type_from_string(const std::string &s)
return false;
}
// Adds CAD recipe file ("Metadata/orca_cad.bin").
if (!_add_cad_recipe_file_to_archive(archive, model)) {
close_zip_writer(&archive);
boost::filesystem::remove(filename);
return false;
}
// Adds layer config ranges file ("Metadata/Slic3r_PE_layer_config_ranges.txt").
// All layer height profiles of all ModelObjects are stored here, indexed by 1 based index of the ModelObject in Model.
// The index differes from the index of an object ID of an object instance of a 3MF file!
@@ -2943,19 +2919,6 @@ ModelVolumeType type_from_string(const std::string &s)
return true;
}
bool _3MF_Exporter::_add_cad_recipe_file_to_archive(mz_zip_archive& archive, Model& model)
{
if (model.cad_recipe.empty())
return true;
if (!mz_zip_writer_add_mem(&archive, CAD_RECIPE_FILE.c_str(),
(const void*)model.cad_recipe.data(), model.cad_recipe.length(),
MZ_DEFAULT_COMPRESSION)) {
add_error("Unable to add CAD recipe file to archive");
return false;
}
return true;
}
bool _3MF_Exporter::_add_layer_config_ranges_file_to_archive(mz_zip_archive& archive, Model& model)
{
std::string out = "";
+10 -66
View File
@@ -145,8 +145,8 @@ SCENARIO("Export+Import geometry to/from 3mf file cycle", "[3mf]") {
}
}
// The recipe is an opaque binary blob (CadDocument::serialize_recipe()), so both 3mf backends
// have to carry it byte-for-byte — no XML/text mangling, embedded NULs intact.
// The recipe is an opaque binary blob (CadDocument::serialize_recipe()), so the 3mf backend has
// to carry it byte-for-byte — no XML/text mangling, embedded NULs intact.
static std::string make_cad_recipe()
{
// Built from an explicit length, not append(const char*), which would stop at the first
@@ -224,53 +224,11 @@ static void rename_cad_recipe_entry_to_legacy(const std::string& path)
out.finalize();
}
SCENARIO("CAD recipe blob survives a 3mf save/load cycle", "[3mf][CAD]") {
GIVEN("a model carrying a binary cad_recipe") {
Model src_model;
std::string src_file = std::string(TEST_DATA_DIR) + "/test_3mf/Prusa.stl";
REQUIRE(load_stl(src_file.c_str(), &src_model));
src_model.add_default_instances();
const std::string recipe = make_cad_recipe();
src_model.cad_recipe = recipe;
WHEN("the model is saved+loaded to/from a 3mf file") {
ScopedTemporaryFile temp(".3mf");
const std::string test_file = temp.string();
REQUIRE(store_3mf(test_file.c_str(), &src_model, nullptr, false));
Model dst_model;
DynamicPrintConfig dst_config;
ConfigSubstitutionContext ctxt{ ForwardCompatibilitySubstitutionRule::Disable };
REQUIRE(load_3mf(test_file.c_str(), dst_config, ctxt, &dst_model, false));
THEN("the recipe round-trips byte-for-byte") {
REQUIRE(dst_model.cad_recipe.size() == recipe.size());
REQUIRE(dst_model.cad_recipe == recipe);
}
}
WHEN("the same model is saved with no recipe") {
src_model.cad_recipe.clear();
ScopedTemporaryFile temp(".3mf");
const std::string test_file = temp.string();
REQUIRE(store_3mf(test_file.c_str(), &src_model, nullptr, false));
Model dst_model;
DynamicPrintConfig dst_config;
ConfigSubstitutionContext ctxt{ ForwardCompatibilitySubstitutionRule::Disable };
REQUIRE(load_3mf(test_file.c_str(), dst_config, ctxt, &dst_model, false));
THEN("nothing is written and nothing is read back") {
REQUIRE(dst_model.cad_recipe.empty());
}
}
}
}
// The GUI saves/loads projects via the BBS-native 3mf backend (store_bbs_3mf / load_bbs_3mf),
// NOT the PrusaSlicer 3mf.cpp. This locks in both halves: the archive entry is at the exact
// path the importer looks for, and the recipe comes back through the real importer.
// The recipe lives only in the BBS-native backend, because that is the only one that runs:
// store_bbs_3mf is the sole exporter the app calls, and 3mf.cpp's load_3mf is reached only for
// files fingerprinted as PrusaSlicer's, which never carry a recipe. This locks in both halves:
// the archive entry is at the exact path the importer looks for, and the recipe comes back
// through the real importer.
SCENARIO("CAD recipe is embedded in the BBS 3mf archive", "[3mf][CAD]") {
GIVEN("a model carrying a binary cad_recipe") {
Model model;
@@ -351,7 +309,8 @@ SCENARIO("CAD recipe is embedded in the BBS 3mf archive", "[3mf][CAD]") {
// The recipe entry was renamed from Metadata/SnapOrca_cad.bin to Metadata/orca_cad.bin. Nothing
// in the blob marks that move, so a reader that knows only the new name loads a project written
// before it with an empty cad_recipe and no error at all — a feature tree gone with no symptom
// but an empty Design tab. Both backends must still accept the old name; neither may write it.
// but an empty Design tab. The importer must still accept the old name; the exporter may never
// write it.
SCENARIO("a project saved under the pre-rename recipe name still loads", "[3mf][CAD]") {
GIVEN("a project whose recipe entry carries the old name") {
Model model;
@@ -361,22 +320,7 @@ SCENARIO("a project saved under the pre-rename recipe name still loads", "[3mf][
const std::string recipe = make_cad_recipe();
model.cad_recipe = recipe;
WHEN("it was written by the PrusaSlicer-format backend") {
ScopedTemporaryFile temp(".3mf");
const std::string test_file = temp.string();
REQUIRE(store_3mf(test_file.c_str(), &model, nullptr, false));
rename_cad_recipe_entry_to_legacy(test_file);
THEN("the recipe still comes back byte-for-byte") {
Model dst_model;
DynamicPrintConfig dst_config;
ConfigSubstitutionContext ctxt{ ForwardCompatibilitySubstitutionRule::Disable };
REQUIRE(load_3mf(test_file.c_str(), dst_config, ctxt, &dst_model, false));
REQUIRE(dst_model.cad_recipe == recipe);
}
}
WHEN("it was written by the BBS backend (the format the GUI uses)") {
WHEN("it was written by the BBS backend") {
ScopedTemporaryDir backup_dir("orca_cad_legacy");
model.set_backup_path(backup_dir.string());