From b80f4e30362d5f02b894a9060a2d6b3eeee9a855 Mon Sep 17 00:00:00 2001 From: Tommaso Bianchi Date: Thu, 13 Aug 2026 15:54:57 +0200 Subject: [PATCH] Persist the CAD recipe on every save, not only on Commit to Plate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Modelling in the Design tab and pressing Ctrl+S saved a project with no feature history at all, and the app reported success. Found on the rig: a project saved after drawing a rectangle and a circle contained twelve archive entries, none of them Metadata/SnapOrca_cad.bin, and a 3dmodel.model with zero vertices. plater->model().cad_recipe was assigned in exactly one place — on_commit(), immediately after load_mesh_object. The 3MF exporter was never at fault: it faithfully wrote whatever the Model held, and on a save that had not gone through Commit to Plate that string had never been set. The recipe reached the Model only as a side effect of a different user action. It now tracks the document instead. sync_recipe_to_model() is called after a successful recompute, after tree edits (deletes, reorders and suppressions bypass recompute_guarded), and from on_commit, which delegates rather than repeating the rule. Only on success — a failed recompute leaves the document mid-edit, and persisting that would save a model the user never had. An empty document still clears it, so a non-CAD project carries no stale recipe. Doing it here rather than in the save path is deliberate: Ctrl+S, Save As, autosave and crash recovery all read model.cad_recipe, so keeping it current after each change makes every one of them correct at once, instead of teaching each save path to ask the Design tab. Commit to Plate means "send this to the slicer" — making saving depend on it was the bug, not the cure. Cost is one serialization per recompute, tens of KB against an OCCT rebuild that has just run. Why nothing caught it: the kernel round-trip tests serialize a CadDocument directly, and the 3MF tests exercise the exporter with a recipe already present. Neither can observe that the GUI never populates it, and every save in testing happened to follow a Commit to Plate. snaporca-vjk5. Reviewed and compiled (RC=0); persistence NOT yet confirmed on the rig — that check is the reason the issue stays open. --- src/slic3r/GUI/DesignPanel.cpp | 29 ++++++++++++++++++++++++++--- src/slic3r/GUI/DesignPanel.hpp | 2 ++ 2 files changed, 28 insertions(+), 3 deletions(-) diff --git a/src/slic3r/GUI/DesignPanel.cpp b/src/slic3r/GUI/DesignPanel.cpp index 1f1c4c2966..1fb8c34d48 100644 --- a/src/slic3r/GUI/DesignPanel.cpp +++ b/src/slic3r/GUI/DesignPanel.cpp @@ -4135,6 +4135,27 @@ static void run_off_ui_thread(wxWindow* parent, const wxString& message, const s // goes through recompute(), and on a heavy imported solid that is seconds of OCCT work — inline // it freezes the window. OCCT throws Standard_Failure, which is not a std::exception and would // terminate the process if it escaped the worker, so both are caught here. +// Keep the Model's copy of the recipe in step with the document. +// +// This used to be written in exactly ONE place — on_commit(), as a side effect of Commit to +// Plate — so a user who modelled for an hour and pressed Ctrl+S saved a project containing no +// feature history at all, and the app reported success (snaporca-vjk5). The 3MF exporter was +// never at fault: nothing had handed it a recipe. +// +// Every save path (Ctrl+S, Save As, autosave, crash recovery) reads model.cad_recipe, so +// keeping it current after each document change is what makes all of them correct at once, +// rather than teaching each one to ask the Design tab. Commit to Plate means "send this to the +// slicer"; making saving depend on it was the bug, not the cure. +// +// Cost is a serialization per recompute — tens of KB against an OCCT rebuild that just ran. +void DesignPanel::sync_recipe_to_model() +{ + Plater* plater = wxGetApp().plater(); + if (plater == nullptr) return; + // An empty document CLEARS it, so a non-CAD project never carries a stale recipe. + plater->model().cad_recipe = m_doc.features.empty() ? std::string() : m_doc.serialize_recipe(); +} + bool DesignPanel::recompute_guarded(const wxString& message) { bool ok = false; @@ -4150,6 +4171,9 @@ bool DesignPanel::recompute_guarded(const wxString& message) ok = false; } }); + // Only on success: a failed recompute leaves the document mid-edit, and persisting that + // would save a model the user never had. + if (ok) sync_recipe_to_model(); return ok; } @@ -6482,6 +6506,7 @@ void DesignPanel::after_tree_edit(bool ok) m_status->Refresh(); return; } + sync_recipe_to_model(); // deletes, reorders and suppressions change the document too m_status->SetForegroundColour(wxNullColour); if (m_doc.display_mesh.its.indices.empty()) { if (m_viewport != nullptr) m_viewport->clear_mesh(); @@ -8593,9 +8618,7 @@ void DesignPanel::on_commit() // 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(); + sync_recipe_to_model(); if (wxGetApp().mainframe != nullptr) wxGetApp().mainframe->select_tab(size_t(MainFrame::tp3DEditor)); diff --git a/src/slic3r/GUI/DesignPanel.hpp b/src/slic3r/GUI/DesignPanel.hpp index edb1e486db..76f2d61e00 100644 --- a/src/slic3r/GUI/DesignPanel.hpp +++ b/src/slic3r/GUI/DesignPanel.hpp @@ -53,6 +53,8 @@ public: void on_tab_hidden(); // another tab took over: take the viewport status line down with us // Rebuild off the UI thread (progress dialog only if it turns out to be slow), so a feature // op on a heavy imported solid does not freeze the window. Returns m_doc.recompute()'s result. + // Push the document's recipe into the Model so ANY save path persists it (snaporca-vjk5). + void sync_recipe_to_model(); bool recompute_guarded(const wxString& message); // MCP control hooks: let the external control server (McpControl.cpp) drive and