mirror of
https://github.com/OrcaSlicer/OrcaSlicer.git
synced 2026-09-27 19:01:02 +00:00
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:
co-authored by
Claude Opus 4.8
parent
c22351f63a
commit
2c0140afb9
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 = "";
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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>();
|
||||
|
||||
@@ -2652,6 +2652,29 @@ int DesignPanel::tree_icon_for(CadFeatureType t)
|
||||
void DesignPanel::on_tab_shown()
|
||||
{
|
||||
if (m_viewport) m_viewport->refresh_bed();
|
||||
|
||||
// Rehydrate the parametric model from a freshly loaded project (the 3MF carried the
|
||||
// recipe in Metadata/SnapOrca_cad.bin). Only when nothing is in progress here, so we
|
||||
// never clobber an active design when the user just toggles back to the Design tab.
|
||||
if (m_doc.features.empty()) {
|
||||
if (Plater* plater = wxGetApp().plater()) {
|
||||
const std::string& blob = plater->model().cad_recipe;
|
||||
if (!blob.empty()) load_recipe(blob);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void DesignPanel::load_recipe(const std::string& blob)
|
||||
{
|
||||
if (blob.empty()) return;
|
||||
if (!m_doc.deserialize_recipe(blob)) {
|
||||
m_status->SetLabel(_L("Could not restore the CAD model from this project"));
|
||||
return;
|
||||
}
|
||||
m_feature_counter = int(m_doc.features.size());
|
||||
feed_bodies(); // push the restored bodies into the viewport
|
||||
refresh_tree(); // rebuild the feature tree from the restored recipe
|
||||
set_status_ok();
|
||||
}
|
||||
|
||||
void DesignPanel::refresh_tree()
|
||||
@@ -4526,6 +4549,13 @@ void DesignPanel::on_commit()
|
||||
obj_list->load_mesh_object(m_disp_pick_mesh, "Design Body");
|
||||
}
|
||||
|
||||
// Persist the editable parametric recipe alongside the committed meshes so the
|
||||
// saved 3MF reopens with the full feature tree, not just the baked solid. An empty
|
||||
// doc clears it, keeping non-CAD projects clean.
|
||||
if (Plater* plater = wxGetApp().plater())
|
||||
plater->model().cad_recipe =
|
||||
m_doc.features.empty() ? std::string() : m_doc.serialize_recipe();
|
||||
|
||||
if (wxGetApp().mainframe != nullptr)
|
||||
wxGetApp().mainframe->select_tab(size_t(MainFrame::tp3DEditor));
|
||||
}
|
||||
|
||||
@@ -86,6 +86,9 @@ private:
|
||||
// a modal dialog editing the feature's placement transform in place.
|
||||
void on_transform_imported(int feat_idx);
|
||||
void on_commit();
|
||||
// Rehydrate the parametric model from a project's saved recipe (3MF
|
||||
// Metadata/SnapOrca_cad.bin): deserialize -> recompute -> refresh viewport + tree.
|
||||
void load_recipe(const std::string& blob);
|
||||
void refresh_tree();
|
||||
void set_status_ok();
|
||||
|
||||
|
||||
Reference in New Issue
Block a user