Reset the CAD document with the project, and track its changes

New Project and Open Project went through Plater::priv::reset, which
drops ModelObjects but not the Model-level recipe, and never touched
the Design panel's document at all — so the previous design stayed
loaded and its next edit wrote itself into the new project.

A design that has not been committed to the plate has no ModelObjects,
so the project also read as clean: no autosave, and no unsaved-changes
prompt before the reset threw the design away.
This commit is contained in:
SoftFever
2026-08-27 23:35:18 +08:00
parent a1110b0050
commit ba1df32e88
3 changed files with 57 additions and 13 deletions
+20 -1
View File
@@ -5,6 +5,7 @@
#include "libslic3r/CAD/GeometryEngine.hpp" // face_by_index for face-extrude gizmo anchor
#include "libslic3r/TriangleMesh.hpp" // mesh import: STL/OBJ -> indexed_triangle_set
#include "libslic3r/Format/OBJ.hpp"
#include "libslic3r/Format/bbs_3mf.hpp" // put_other_changes: mark the project dirty outside the undo stack
#include <boost/algorithm/string/case_conv.hpp>
#include <boost/filesystem/path.hpp>
@@ -4620,7 +4621,16 @@ 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();
std::string recipe = m_doc.features.empty() ? std::string() : m_doc.serialize_recipe();
if (recipe == plater->model().cad_recipe)
return; // no change — this is the rehydrate of a project that was just opened
plater->model().cad_recipe = std::move(recipe);
// The plater's dirty flag rides its own undo/redo stack, which Design edits never touch, and
// a design that has not been committed to the plate has no ModelObjects either — so without
// this the project reads as clean: no autosave, and no "unsaved changes" prompt on quit.
// Same hook the auxiliary-files panel uses for project data that lives outside the model.
Slic3r::put_other_changes();
}
bool DesignPanel::recompute_guarded(const wxString& message)
@@ -7427,6 +7437,15 @@ void DesignPanel::on_new_design()
_L("Erase all features and bodies and start a new design? This cannot be undone."),
_L("New Design"), wxYES_NO | wxICON_EXCLAMATION);
if (dlg.ShowModal() != wxID_YES) return;
clear_document();
}
// The teardown behind New Design, without the confirmation. Also what New Project / Open
// Project run through Plater::priv::reset: the document lives here rather than in the Model,
// so without this it survives the project that produced it and the next Design edit writes
// the previous project's feature tree into the new one.
void DesignPanel::clear_document()
{
tool_cancel(); // leave any active tool / sketch / constrain cleanly
m_doc.clear(); // features + bodies + meshes + history
m_edit_index = -1;
+1
View File
@@ -51,6 +51,7 @@ public:
explicit DesignPanel(wxWindow* parent);
void on_tab_shown(); // re-sync bed to the active printer when the Design tab is activated
void on_tab_hidden(); // another tab took over: take the viewport status line down with us
void clear_document(); // New Project / Open Project: drop the document with the project
// 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).
+36 -12
View File
@@ -97,6 +97,9 @@
#include "wxExtensions.hpp"
#include "../Utils/PrintHost.hpp"
#include "MainFrame.hpp"
#ifdef SLIC3R_CAD
#include "slic3r/GUI/CAD/DesignPanel.hpp"
#endif
#include "format.hpp"
#include "3DScene.hpp"
#include "GLCanvas3D.hpp"
@@ -8343,6 +8346,9 @@ std::vector<size_t> Plater::priv::load_files(const std::vector<fs::path>& input_
int answer_convert_from_meters = wxOK_DEFAULT;
int answer_convert_from_imperial_units = wxOK_DEFAULT;
int tolal_model_count = 0;
// Whether one of the files being loaded here carried a CAD recipe. A statement about these
// files, not about the plater — q->model() may still hold the previous project's recipe.
bool loaded_cad_recipe = false;
int progress_percent = 0;
int total_files = input_files.size();
@@ -9290,11 +9296,15 @@ std::vector<size_t> Plater::priv::load_files(const std::vector<fs::path>& input_
auto loaded_idxs = load_model_objects(model.objects, is_project_file);
obj_idxs.insert(obj_idxs.end(), loaded_idxs.begin(), loaded_idxs.end());
// load_model_objects only transfers ModelObjects; carry the Model-level CAD
// recipe (Metadata/orca_cad.bin) onto the plater model so the Design tab
// can rehydrate the editable feature tree on reopen.
if (!model.cad_recipe.empty())
// load_model_objects only transfers ModelObjects; carry the Model-level CAD recipe
// onto the plater model so the Design tab can rehydrate the editable feature tree on
// reopen. Assigned unconditionally on the project-replacing path so that opening a
// project without a recipe clears whatever the previous one left behind; importing a
// plain model into the open project leaves the current recipe alone.
if (is_project_file) {
q->model().cad_recipe = model.cad_recipe;
loaded_cad_recipe = !model.cad_recipe.empty();
}
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << ":" << __LINE__ << boost::format(", finished load_model_objects");
wxString msg = wxString::Format(_L("Loading file: %s"), from_path(real_filename));
@@ -9512,12 +9522,12 @@ std::vector<size_t> Plater::priv::load_files(const std::vector<fs::path>& input_
// q->model().stl_design_country = "";
//}
// A CAD project legitimately carries no mesh: the model lives in the feature tree
// (Metadata/orca_cad.bin) until it is committed to the plate. Warning "no geometry data"
// for one is false, and it is the LAST thing a user sees after opening a design they spent an
// hour on — it reads as "your work is gone" when the recipe has in fact just been loaded and
// the Design tab will rehydrate it. Count the recipe as geometry.
if (tolal_model_count <= 0 && q->model().cad_recipe.empty() && !q->m_exported_file) {
// A CAD project legitimately carries no mesh: the model lives in the feature tree until it is
// committed to the plate. Warning "no geometry data" for one is false, and it is the LAST thing
// a user sees after opening a design they spent an hour on — it reads as "your work is gone"
// when the recipe has in fact just been loaded and the Design tab will rehydrate it. Count a
// recipe that came from THESE files as geometry.
if (tolal_model_count <= 0 && !loaded_cad_recipe && !q->m_exported_file) {
dlg.Hide();
if (!is_user_cancel) {
MessageDialog msg(wxGetApp().mainframe, _L("The file does not contain any geometry data."), _L("Warning"), wxYES | wxICON_WARNING);
@@ -10056,6 +10066,16 @@ void Plater::priv::reset(bool apply_presets_change)
// Stop and reset the Print content.
this->background_process.reset();
model.clear_objects();
// clear_objects() only drops the ModelObjects; the CAD recipe is Model-level state and would
// otherwise be written into every project saved for the rest of the session.
model.cad_recipe.clear();
#ifdef SLIC3R_CAD
// Same reason, one level up: the Design tab keeps the editable document, not the Model, so
// clearing the recipe alone leaves the tab showing the previous project's feature tree —
// and its next edit syncs that tree straight back into the new project.
if (wxGetApp().mainframe != nullptr && wxGetApp().mainframe->m_design_panel != nullptr)
wxGetApp().mainframe->m_design_panel->clear_document();
#endif
assemble_view->get_canvas3d()->reset_explosion_ratio();
update();
@@ -15508,8 +15528,12 @@ bool Plater::up_to_date(bool saved, bool backup)
Slic3r::clear_other_changes(backup);
return p->up_to_date(saved, backup);
}
return p->model.objects.empty() || (p->up_to_date(saved, backup) &&
!Slic3r::has_other_changes(backup));
// A Design-tab project is object-less until it is committed to the plate, but its feature
// tree is real work: treating it as an empty project skipped both the autosave and the
// "unsaved changes" prompt, so quitting threw it away without asking. Non-CAD projects
// never carry a recipe, so the empty-project shortcut is unchanged for them.
return (p->model.objects.empty() && p->model.cad_recipe.empty()) ||
(p->up_to_date(saved, backup) && !Slic3r::has_other_changes(backup));
}
bool Plater::add_model(bool imperial_units, std::string fname)