Mirror CAD persistence (K1+K2+G1) from snaporca-cad onto mainline OrcaSlicer

Dual-fork mandate: port the parametric-recipe 3MF persistence from
snaporca-cad commits ed19eac+147e1c4 so a saved project reopens with the
editable CAD feature tree, not just the baked mesh.

Shared kernel/format/GUI (identical to snaporca-cad):
- CadDocument serialize_recipe/deserialize_recipe (cereal BinaryArchive,
  versioned) + CadFeature split save/load + imported_solid<->BRep string.
- Model::cad_recipe carried through 3MF zip entry Metadata/SnapOrca_cad.bin
  (writer + binary-verbatim reader branch).
- DesignPanel on_commit() stamps the recipe; on_tab_shown() rehydrates a
  loaded project via load_recipe() (deserialize -> feed_bodies + refresh_tree).

Mainline-only adapters (no snaporca-cad counterpart — Catch2 v3 vs v2):
- tests/libslic3r/test_caddocument.cpp: <catch2/catch_all.hpp> +
  `using Catch::Approx;` (v3 scopes Approx under Catch::).
- tests/libslic3r/CMakeLists.txt: register test_caddocument.cpp (the
  original CAD port had left it out of the test build).

Verified on behemoth (snaporca-deps toolchain): libslic3r_tests clean;
[CadDocument] 17/18 (only the pre-existing tangent-to-circle SIGABRT fails,
identical to snaporca-tkz); K1 serialize round-trip + version-reject pass
(13 assertions); new [3mf] "CAD recipe blob survives a 3mf save/load cycle"
passes byte-for-byte; orca-slicer GUI links clean (186/186, DesignPanel.cpp
compiled). Interactive :10 click-through pending (no Design-tab automation).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BVzKmX6Y1aEteit1HTXG4Q
This commit is contained in:
Tommaso Bianchi
2026-06-28 17:05:44 +02:00
co-authored by Claude Opus 4.8
parent c22351f63a
commit 2c0140afb9
10 changed files with 280 additions and 1 deletions
+36
View File
@@ -133,6 +133,42 @@ SCENARIO("Export+Import geometry to/from 3mf file cycle", "[3mf]") {
}
}
SCENARIO("CAD recipe blob survives a 3mf save/load cycle", "[3mf]") {
GIVEN("a model carrying a binary cad_recipe") {
Model src_model;
std::string src_file = std::string(TEST_DATA_DIR) + "/test_3mf/Prusa.stl";
load_stl(src_file.c_str(), &src_model);
src_model.add_default_instances();
// Binary payload with an embedded NUL to prove the carrier is byte-safe
// (no XML/text mangling) — mirrors the cereal blob from serialize_recipe().
std::string recipe;
recipe.push_back('\x01');
recipe.append("SNAPORCA");
recipe.push_back('\0');
recipe.append("\xff\xfe\x00\x10cad-features-blob");
src_model.cad_recipe = recipe;
WHEN("the model is saved+loaded to/from a 3mf file") {
std::string test_file = std::string(TEST_DATA_DIR) + "/test_3mf/cad_recipe.3mf";
store_3mf(test_file.c_str(), &src_model, nullptr, false);
Model dst_model;
DynamicPrintConfig dst_config;
{
ConfigSubstitutionContext ctxt{ ForwardCompatibilitySubstitutionRule::Disable };
load_3mf(test_file.c_str(), dst_config, ctxt, &dst_model, false);
}
boost::filesystem::remove(test_file);
THEN("the recipe round-trips byte-for-byte") {
REQUIRE(dst_model.cad_recipe.size() == recipe.size());
REQUIRE(dst_model.cad_recipe == recipe);
}
}
}
}
SCENARIO("2D convex hull of sinking object", "[3mf][.]") {
GIVEN("model") {
// load a model