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
+52
View File
@@ -42,6 +42,10 @@
#include <cmath>
#include <stdexcept>
#include <algorithm>
#include <sstream>
#include <cereal/archives/binary.hpp>
#include <BRepTools.hxx>
namespace Slic3r {
@@ -1537,4 +1541,52 @@ bool CadDocument::preview(const CadFeature& candidate, TriangleMesh& out_mesh, s
return preview(candidate, out_mesh, ignore, err);
}
std::string brep_to_string(const TopoDS_Shape& s)
{
if (s.IsNull()) return {};
std::ostringstream oss;
BRepTools::Write(s, oss);
return oss.str();
}
TopoDS_Shape brep_from_string(const std::string& d)
{
if (d.empty()) return {};
std::istringstream iss(d);
TopoDS_Shape s;
BRep_Builder b;
BRepTools::Read(s, iss, b);
return s;
}
std::string CadDocument::serialize_recipe() const
{
std::ostringstream oss;
{
cereal::BinaryOutputArchive ar(oss);
uint32_t v = SNAPORCA_CAD_RECIPE_VERSION;
ar(v);
ar(features);
}
return oss.str();
}
bool CadDocument::deserialize_recipe(const std::string& blob)
{
try {
std::istringstream iss(blob);
cereal::BinaryInputArchive ar(iss);
uint32_t v;
ar(v);
if (v > SNAPORCA_CAD_RECIPE_VERSION)
return false;
ar(features);
return recompute();
} catch (const Standard_Failure&) {
return false;
} catch (...) {
return false;
}
}
} // namespace Slic3r
+60
View File
@@ -8,6 +8,9 @@
#include <TopoDS_Shape.hxx>
#include <TopoDS_Wire.hxx>
#include <cereal/cereal.hpp>
#include <cereal/types/vector.hpp>
#include <cereal/types/string.hpp>
#include <string>
#include <vector>
#include <utility>
@@ -20,6 +23,11 @@ enum class BooleanMode { New, Add, Cut, Intersect };
enum class ExtrudeEnd { Blind, Symmetric, TwoSided, ThroughAll, UpToFace, UpToVertex };
// Serialize a TopoDS_Shape to/from a BRep string (declared before CadFeature so its
// inline cereal save()/load() can resolve these non-dependent calls).
std::string brep_to_string(const TopoDS_Shape& s);
TopoDS_Shape brep_from_string(const std::string& d);
struct CadFeature {
CadFeatureType type{CadFeatureType::Sketch};
std::string name;
@@ -181,8 +189,56 @@ struct CadFeature {
bool cut_flip{false}; // flip the normal => swaps which side is "upper"
bool cut_keep_upper{true}; // keep the +normal half
bool cut_keep_lower{false}; // keep the -normal half (both => split into two bodies)
template<class Archive>
void save(Archive& ar) const {
std::string brep = (type == CadFeatureType::Import) ? brep_to_string(imported_solid) : std::string();
ar(type, name, enabled, shape, plane, width, height, radius,
profile, entities, constraints, entity_constraints, imported_regions,
import_offset, import_scale_x, import_scale_y, import_on_face, import_face_body,
sketch_ref, distance, symmetric, mode, extrude_end, distance2, taper_deg, flip,
up_to_face, extrude_src_face, up_to_point, target_body,
dressup_size, face_group, dressup_edge,
hole_diameter, hole_depth, hole_through, hole_x, hole_y,
thread_radius, thread_pitch, thread_height, thread_depth, thread_internal, thread_x, thread_y,
shell_thickness, shell_face,
draft_face, draft_angle,
revolve_angle, revolve_axis,
sweep_path_ref, loft_profile_refs, loft_ruled,
pattern_circular, pattern_count, pattern_spacing, pattern_dir, pattern_angle,
plane_base, plane_offset, plane_angle_tilt, plane_axis,
bool_tool_body, bool_keep_tool, bool_tolerance, bool_target_face, bool_tool_face,
cut_offset, cut_flip, cut_keep_upper, cut_keep_lower,
brep);
}
template<class Archive>
void load(Archive& ar) {
std::string brep;
ar(type, name, enabled, shape, plane, width, height, radius,
profile, entities, constraints, entity_constraints, imported_regions,
import_offset, import_scale_x, import_scale_y, import_on_face, import_face_body,
sketch_ref, distance, symmetric, mode, extrude_end, distance2, taper_deg, flip,
up_to_face, extrude_src_face, up_to_point, target_body,
dressup_size, face_group, dressup_edge,
hole_diameter, hole_depth, hole_through, hole_x, hole_y,
thread_radius, thread_pitch, thread_height, thread_depth, thread_internal, thread_x, thread_y,
shell_thickness, shell_face,
draft_face, draft_angle,
revolve_angle, revolve_axis,
sweep_path_ref, loft_profile_refs, loft_ruled,
pattern_circular, pattern_count, pattern_spacing, pattern_dir, pattern_angle,
plane_base, plane_offset, plane_angle_tilt, plane_axis,
bool_tool_body, bool_keep_tool, bool_tolerance, bool_target_face, bool_tool_face,
cut_offset, cut_flip, cut_keep_upper, cut_keep_lower,
brep);
imported_solid = brep_from_string(brep);
}
};
// Serialize a TopoDS_Shape to/from a BRep string for cereal persistence.
std::string brep_to_string(const TopoDS_Shape& s);
TopoDS_Shape brep_from_string(const std::string& d);
// One independent solid in a multi-body document.
struct CadBody {
TopoDS_Shape shape;
@@ -279,6 +335,10 @@ public:
void clear();
bool recompute(); // replay features -> body + display_mesh; false on error
static constexpr uint32_t SNAPORCA_CAD_RECIPE_VERSION = 1;
std::string serialize_recipe() const;
bool deserialize_recipe(const std::string& blob);
// Undo/redo of the feature recipe (Onshape-style Ctrl+Z). The caller marks a
// user-action boundary by calling checkpoint() BEFORE the mutation(s) for that
// action (add/delete/move/replace, or a direct features edit). undo()/redo() then
+33
View File
@@ -73,6 +73,7 @@ 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/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";
@@ -805,6 +806,17 @@ ModelVolumeType type_from_string(const std::string &s)
return false;
}
}
if (boost::algorithm::iequals(name, 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");
}
}
}
}
}
@@ -2317,6 +2329,7 @@ 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);
@@ -2387,6 +2400,13 @@ ModelVolumeType type_from_string(const std::string &s)
return false;
}
// Adds CAD recipe file ("Metadata/SnapOrca_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!
@@ -2919,6 +2939,19 @@ 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 = "";
+3
View File
@@ -105,6 +105,8 @@ Model& Model::assign_copy(const Model &rhs)
this->md_name = rhs.md_name;
this->md_value = rhs.md_value;
this->cad_recipe = rhs.cad_recipe;
return *this;
}
@@ -148,6 +150,7 @@ Model& Model::assign_copy(Model &&rhs)
rhs.model_info.reset();
this->profile_info = rhs.profile_info;
rhs.profile_info.reset();
this->cad_recipe = std::move(rhs.cad_recipe);
return *this;
}
+4
View File
@@ -1547,6 +1547,10 @@ public:
std::vector<std::string> md_name;
std::vector<std::string> md_value;
// SnapOrca: opaque parametric CAD recipe (CadDocument::serialize_recipe()),
// round-tripped through the 3MF as Metadata/SnapOrca_cad.bin. Empty for non-CAD projects.
std::string cad_recipe;
void SetDesigner(std::string designer, std::string designer_user_id) {
if (design_info == nullptr) {
design_info = std::make_shared<ModelDesignInfo>();