diff --git a/src/libslic3r/Model.cpp b/src/libslic3r/Model.cpp index 2533f288b..5e166b44f 100644 --- a/src/libslic3r/Model.cpp +++ b/src/libslic3r/Model.cpp @@ -22,19 +22,6 @@ namespace Slic3r { unsigned int Model::s_auto_extruder_id = 1; -// Unique object / instance ID for the wipe tower. -ObjectID wipe_tower_object_id() -{ - static ObjectBase mine; - return mine.id(); -} - -ObjectID wipe_tower_instance_id() -{ - static ObjectBase mine; - return mine.id(); -} - Model& Model::assign_copy(const Model &rhs) { this->copy_id(rhs); @@ -1068,11 +1055,11 @@ void ModelObject::mirror(Axis axis) } // This method could only be called before the meshes of this ModelVolumes are not shared! -void ModelObject::scale_mesh(const Vec3d &versor) +void ModelObject::scale_mesh_after_creation(const Vec3d &versor) { for (ModelVolume *v : this->volumes) { - v->scale_geometry(versor); + v->scale_geometry_after_creation(versor); v->set_offset(versor.cwiseProduct(v->get_offset())); } this->invalidate_bounding_box(); @@ -1562,8 +1549,8 @@ void ModelVolume::center_geometry_after_creation() Vec3d shift = this->mesh().bounding_box().center(); if (!shift.isApprox(Vec3d::Zero())) { - m_mesh->translate(-(float)shift(0), -(float)shift(1), -(float)shift(2)); - m_convex_hull->translate(-(float)shift(0), -(float)shift(1), -(float)shift(2)); + const_cast(m_mesh.get())->translate(-(float)shift(0), -(float)shift(1), -(float)shift(2)); + const_cast(m_convex_hull.get())->translate(-(float)shift(0), -(float)shift(1), -(float)shift(2)); translate(shift); } } @@ -1716,10 +1703,10 @@ void ModelVolume::mirror(Axis axis) } // This method could only be called before the meshes of this ModelVolumes are not shared! -void ModelVolume::scale_geometry(const Vec3d& versor) +void ModelVolume::scale_geometry_after_creation(const Vec3d& versor) { - m_mesh->scale(versor); - m_convex_hull->scale(versor); + const_cast(m_mesh.get())->scale(versor); + const_cast(m_convex_hull.get())->scale(versor); } void ModelVolume::transform_this_mesh(const Transform3d &mesh_trafo, bool fix_left_handed) diff --git a/src/libslic3r/Model.hpp b/src/libslic3r/Model.hpp index 850ea4202..8e9f7ecaa 100644 --- a/src/libslic3r/Model.hpp +++ b/src/libslic3r/Model.hpp @@ -27,6 +27,10 @@ class ModelVolume; class Print; class SLAPrint; +namespace UndoRedo { + class StackImpl; +} + typedef std::string t_model_material_id; typedef std::string t_model_material_attribute; typedef std::map t_model_material_attributes; @@ -36,10 +40,6 @@ typedef std::vector ModelObjectPtrs; typedef std::vector ModelVolumePtrs; typedef std::vector ModelInstancePtrs; -// Unique object / instance ID for the wipe tower. -extern ObjectID wipe_tower_object_id(); -extern ObjectID wipe_tower_instance_id(); - #define OBJECTBASE_DERIVED_COPY_MOVE_CLONE(TYPE) \ /* Copy a model, copy the IDs. The Print::apply() will call the TYPE::copy() method */ \ /* to make a private copy for background processing. */ \ @@ -75,7 +75,7 @@ private: \ void assign_new_unique_ids_recursive(); // Material, which may be shared across multiple ModelObjects of a single Model. -class ModelMaterial : public ObjectBase +class ModelMaterial final : public ObjectBase { public: // Attributes are defined by the AMF file format, but they don't seem to be used by Slic3r for any purpose. @@ -104,6 +104,7 @@ private: ModelMaterial& operator=(ModelMaterial &&rhs) = delete; friend class cereal::access; + friend class UndoRedo::StackImpl; ModelMaterial() : m_model(nullptr) {} template void serialize(Archive &ar) { ar(cereal::base_class(this)); ar(attributes, config); } }; @@ -112,7 +113,7 @@ private: // and possibly having multiple modifier volumes, each modifier volume with its set of parameters and materials. // Each ModelObject may be instantiated mutliple times, each instance having different placement on the print bed, // different rotation and different uniform scaling. -class ModelObject : public ObjectBase +class ModelObject final : public ObjectBase { friend class Model; public: @@ -211,7 +212,7 @@ public: void mirror(Axis axis); // This method could only be called before the meshes of this ModelVolumes are not shared! - void scale_mesh(const Vec3d& versor); + void scale_mesh_after_creation(const Vec3d& versor); size_t materials_count() const; size_t facets_count() const; @@ -240,12 +241,6 @@ public: // Get count of errors in the mesh( or all object's meshes, if volume index isn't defined) int get_mesh_errors_count(const int vol_idx = -1) const; -protected: - friend class Print; - friend class SLAPrint; - // Called by Print::apply() to set the model pointer after making a copy. - void set_model(Model *model) { m_model = model; } - private: ModelObject(Model *model) : m_model(model), origin_translation(Vec3d::Zero()), m_bounding_box_valid(false), m_raw_bounding_box_valid(false), m_raw_mesh_bounding_box_valid(false) {} @@ -272,7 +267,14 @@ private: mutable BoundingBoxf3 m_raw_mesh_bounding_box; mutable bool m_raw_mesh_bounding_box_valid; + // Called by Print::apply() to set the model pointer after making a copy. + friend class Print; + friend class SLAPrint; + void set_model(Model *model) { m_model = model; } + + // Undo / Redo through the cereal serialization library friend class cereal::access; + friend class UndoRedo::StackImpl; ModelObject() : m_model(nullptr), m_bounding_box_valid(false), m_raw_bounding_box_valid(false), m_raw_mesh_bounding_box_valid(false) {} template void serialize(Archive &ar) { ar(cereal::base_class(this)); @@ -292,17 +294,17 @@ enum class ModelVolumeType : int { // An object STL, or a modifier volume, over which a different set of parameters shall be applied. // ModelVolume instances are owned by a ModelObject. -class ModelVolume : public ObjectBase +class ModelVolume final : public ObjectBase { public: std::string name; // The triangular model. const TriangleMesh& mesh() const { return *m_mesh.get(); } - void set_mesh(const TriangleMesh &mesh) { m_mesh = std::make_shared(mesh); } - void set_mesh(TriangleMesh &&mesh) { m_mesh = std::make_shared(std::move(mesh)); } - void set_mesh(std::shared_ptr &mesh) { m_mesh = mesh; } - void set_mesh(std::unique_ptr &&mesh) { m_mesh = std::move(mesh); } - void reset_mesh() { m_mesh = std::make_shared(); } + void set_mesh(const TriangleMesh &mesh) { m_mesh = std::make_shared(mesh); } + void set_mesh(TriangleMesh &&mesh) { m_mesh = std::make_shared(std::move(mesh)); } + void set_mesh(std::shared_ptr &mesh) { m_mesh = mesh; } + void set_mesh(std::unique_ptr &&mesh) { m_mesh = std::move(mesh); } + void reset_mesh() { m_mesh = std::make_shared(); } // Configuration parameters specific to an object model geometry or a modifier volume, // overriding the global Slic3r settings and the ModelObject settings. DynamicPrintConfig config; @@ -340,7 +342,7 @@ public: void mirror(Axis axis); // This method could only be called before the meshes of this ModelVolumes are not shared! - void scale_geometry(const Vec3d& versor); + void scale_geometry_after_creation(const Vec3d& versor); // Translates the mesh and the convex hull so that the origin of their vertices is in the center of this volume's bounding box. // Attention! This method may only be called just after ModelVolume creation! It must not be called once the TriangleMesh of this ModelVolume is shared! @@ -400,15 +402,15 @@ protected: private: // Parent object owning this ModelVolume. - ModelObject* object; + ModelObject* object; // The triangular model. - std::shared_ptr m_mesh; + std::shared_ptr m_mesh; // Is it an object to be printed, or a modifier volume? - ModelVolumeType m_type; - t_model_material_id m_material_id; + ModelVolumeType m_type; + t_model_material_id m_material_id; // The convex hull of this model's mesh. - std::shared_ptr m_convex_hull; - Geometry::Transformation m_transformation; + std::shared_ptr m_convex_hull; + Geometry::Transformation m_transformation; // flag to optimize the checking if the volume is splittable // -1 -> is unknown value (before first cheking) @@ -443,16 +445,16 @@ private: ModelVolume& operator=(ModelVolume &rhs) = delete; friend class cereal::access; + friend class UndoRedo::StackImpl; ModelVolume() : object(nullptr) {} template void serialize(Archive &ar) { - ar(cereal::base_class(this)); ar(name, config, m_mesh, m_type, m_material_id, m_convex_hull, m_transformation, m_is_splittable); } }; // A single instance of a ModelObject. // Knows the affine transformation of an object. -class ModelInstance : public ObjectBase +class ModelInstance final : public ObjectBase { public: enum EPrintVolumeState : unsigned char @@ -538,6 +540,7 @@ private: ModelInstance& operator=(ModelInstance &&rhs) = delete; friend class cereal::access; + friend class UndoRedo::StackImpl; ModelInstance() : object(nullptr) {} template void serialize(Archive &ar) { ar(cereal::base_class(this)); @@ -550,7 +553,7 @@ private: // and with multiple modifier meshes. // A model groups multiple objects, each object having possibly multiple instances, // all objects may share mutliple materials. -class Model : public ObjectBase +class Model final : public ObjectBase { static unsigned int s_auto_extruder_id; @@ -633,6 +636,7 @@ private: OBJECTBASE_DERIVED_PRIVATE_COPY_MOVE(Model) friend class cereal::access; + friend class UndoRedo::StackImpl; template void serialize(Archive &ar) { ar(cereal::base_class(this), materials, objects); } diff --git a/src/libslic3r/ObjectID.cpp b/src/libslic3r/ObjectID.cpp index 90681a5a6..b188d84c0 100644 --- a/src/libslic3r/ObjectID.cpp +++ b/src/libslic3r/ObjectID.cpp @@ -4,6 +4,19 @@ namespace Slic3r { size_t ObjectBase::s_last_id = 0; +// Unique object / instance ID for the wipe tower. +ObjectID wipe_tower_object_id() +{ + static ObjectBase mine; + return mine.id(); +} + +ObjectID wipe_tower_instance_id() +{ + static ObjectBase mine; + return mine.id(); +} + } // namespace Slic3r // CEREAL_REGISTER_TYPE(Slic3r::ObjectBase) diff --git a/src/libslic3r/ObjectID.hpp b/src/libslic3r/ObjectID.hpp index 6f9c3fcff..f00d6f61e 100644 --- a/src/libslic3r/ObjectID.hpp +++ b/src/libslic3r/ObjectID.hpp @@ -9,6 +9,10 @@ namespace Slic3r { +namespace UndoRedo { + class StackImpl; +}; + // Unique identifier of a mutable object accross the application. // Used to synchronize the front end (UI) with the back end (BackgroundSlicingProcess / Print / PrintObject) // (for Model, ModelObject, ModelVolume, ModelInstance or ModelMaterial classes) @@ -75,6 +79,7 @@ private: friend ObjectID wipe_tower_object_id(); friend ObjectID wipe_tower_instance_id(); + friend class Slic3r::UndoRedo::StackImpl; friend class cereal::access; template void serialize(Archive &ar) { ar(m_id); } @@ -82,6 +87,10 @@ private: template static void load_and_construct(Archive & ar, cereal::construct &construct) { ObjectID id; ar(id); construct(id); } }; +// Unique object / instance ID for the wipe tower. +extern ObjectID wipe_tower_object_id(); +extern ObjectID wipe_tower_instance_id(); + } // namespace Slic3r #endif /* slic3r_ObjectID_hpp_ */ diff --git a/src/libslic3r/SLA/SLACommon.hpp b/src/libslic3r/SLA/SLACommon.hpp index 855802759..247e89dbb 100644 --- a/src/libslic3r/SLA/SLACommon.hpp +++ b/src/libslic3r/SLA/SLACommon.hpp @@ -43,6 +43,8 @@ struct SupportPoint { bool operator==(const SupportPoint& sp) const { return (pos==sp.pos) && head_front_radius==sp.head_front_radius && is_new_island==sp.is_new_island; } bool operator!=(const SupportPoint& sp) const { return !(sp == (*this)); } + + template void serialize(Archive &ar) { ar(pos, head_front_radius, is_new_island); } }; /// An index-triangle structure for libIGL functions. Also serves as an diff --git a/src/libslic3r/Slicing.hpp b/src/libslic3r/Slicing.hpp index fa5a12f9c..cd6affdeb 100644 --- a/src/libslic3r/Slicing.hpp +++ b/src/libslic3r/Slicing.hpp @@ -171,4 +171,9 @@ extern int generate_layer_height_texture( }; // namespace Slic3r +namespace cereal +{ + template void serialize(Archive& archive, Slic3r::t_layer_height_range &lhr) { archive(lhr.first, lhr.second); } +} + #endif /* slic3r_Slicing_hpp_ */ diff --git a/src/libslic3r/TriangleMesh.hpp b/src/libslic3r/TriangleMesh.hpp index 054a98935..1fc512893 100644 --- a/src/libslic3r/TriangleMesh.hpp +++ b/src/libslic3r/TriangleMesh.hpp @@ -195,4 +195,23 @@ TriangleMesh make_sphere(double rho, double fa=(2*PI/360)); } +// Serialization through the Cereal library +namespace cereal { + template struct specialize {}; + template void load(Archive &archive, Slic3r::TriangleMesh &mesh) { + stl_file &stl = mesh.stl; + stl.stats.type = inmemory; + archive(stl.stats.number_of_facets, stl.stats.original_num_facets); + stl_allocate(&stl); + archive.loadBinary((char*)stl.facet_start.data(), stl.facet_start.size() * 50); + stl_get_size(&stl); + mesh.repair(); + } + template void save(Archive &archive, const Slic3r::TriangleMesh &mesh) { + const stl_file& stl = mesh.stl; + archive(stl.stats.number_of_facets, stl.stats.original_num_facets); + archive.saveBinary((char*)stl.facet_start.data(), stl.facet_start.size() * 50); + } +} + #endif diff --git a/src/slic3r/CMakeLists.txt b/src/slic3r/CMakeLists.txt index 1867a8186..da1afdfee 100644 --- a/src/slic3r/CMakeLists.txt +++ b/src/slic3r/CMakeLists.txt @@ -146,6 +146,8 @@ set(SLIC3R_GUI_SOURCES Utils/PresetUpdater.hpp Utils/Time.cpp Utils/Time.hpp + Utils/UndoRedo.cpp + Utils/UndoRedo.hpp Utils/HexFile.cpp Utils/HexFile.hpp ) diff --git a/src/slic3r/GUI/Plater.cpp b/src/slic3r/GUI/Plater.cpp index 9d0c979b6..f08480559 100644 --- a/src/slic3r/GUI/Plater.cpp +++ b/src/slic3r/GUI/Plater.cpp @@ -69,6 +69,7 @@ #include "../Utils/ASCIIFolding.hpp" #include "../Utils/PrintHost.hpp" #include "../Utils/FixModelByWin10.hpp" +#include "../Utils/UndoRedo.hpp" #include // Needs to be last because reasons :-/ #include "WipeTowerDialog.hpp" @@ -1182,6 +1183,8 @@ const std::regex PlaterDropTarget::pattern_drop(".*[.](stl|obj|amf|3mf|prusa)", bool PlaterDropTarget::OnDropFiles(wxCoord x, wxCoord y, const wxArrayString &filenames) { + plater->take_snapshot(_(L("Load Files"))); + std::vector paths; for (const auto &filename : filenames) { @@ -1247,6 +1250,7 @@ struct Plater::priv Slic3r::Model model; PrinterTechnology printer_technology = ptFFF; Slic3r::GCodePreviewData gcode_preview_data; + Slic3r::UndoRedo::Stack undo_redo_stack; // GUI elements wxSizer* panel_sizer{ nullptr }; @@ -1545,6 +1549,10 @@ struct Plater::priv void split_object(); void split_volume(); void scale_selection_to_fit_print_volume(); + + void take_snapshot(const std::string& snapshot_name) { this->undo_redo_stack.take_snapshot(snapshot_name, model, view3D->get_canvas3d()->get_selection()); } + void take_snapshot(const wxString& snapshot_name) { this->take_snapshot(std::string(snapshot_name.ToUTF8().data())); } + bool background_processing_enabled() const { return this->get_config("background_processing") == "1"; } void update_print_volume_state(); void schedule_background_process(); @@ -1775,6 +1783,8 @@ Plater::priv::priv(Plater *q, MainFrame *main_frame) // updates camera type from .ini file camera.set_type(get_config("use_perspective_camera")); + + this->undo_redo_stack.initialize(model, view3D->get_canvas3d()->get_selection()); } void Plater::priv::update(bool force_full_scene_refresh) @@ -2139,7 +2149,7 @@ std::vector Plater::priv::load_model_objects(const ModelObjectPtrs &mode // the size of the object is too big -> this could lead to overflow when moving to clipper coordinates, // so scale down the mesh double inv = 1. / max_ratio; - object->scale_mesh(Vec3d(inv, inv, inv)); + object->scale_mesh_after_creation(Vec3d(inv, inv, inv)); object->origin_translation = Vec3d::Zero(); object->center_around_origin(); scaled_down = true; @@ -2355,6 +2365,8 @@ void Plater::priv::delete_object_from_model(size_t obj_idx) void Plater::priv::reset() { + this->take_snapshot(_(L("Reset Project"))); + set_project_filename(wxEmptyString); // Prevent toolpaths preview from rendering while we modify the Print object @@ -3515,6 +3527,8 @@ void Plater::new_project() void Plater::load_project() { + this->take_snapshot(_(L("Load Project"))); + wxString input_file; wxGetApp().load_project(this, input_file); @@ -3593,6 +3607,7 @@ void Plater::delete_object_from_model(size_t obj_idx) { p->delete_object_from_mo void Plater::remove_selected() { + this->take_snapshot(_(L("Delete Selected Objects"))); this->p->view3D->delete_selected(); } @@ -3600,6 +3615,8 @@ void Plater::increase_instances(size_t num) { if (! can_increase_instances()) { return; } + this->take_snapshot(_(L("Increase Instances"))); + int obj_idx = p->get_selected_object_idx(); ModelObject* model_object = p->model.objects[obj_idx]; @@ -3634,6 +3651,8 @@ void Plater::decrease_instances(size_t num) { if (! can_decrease_instances()) { return; } + this->take_snapshot(_(L("Decrease Instances"))); + int obj_idx = p->get_selected_object_idx(); ModelObject* model_object = p->model.objects[obj_idx]; @@ -3982,6 +4001,16 @@ void Plater::send_gcode() } } +void Plater::take_snapshot(const std::string &snapshot_name) +{ + p->take_snapshot(snapshot_name); +} + +void Plater::take_snapshot(const wxString &snapshot_name) +{ + p->take_snapshot(snapshot_name); +} + void Plater::on_extruders_change(int num_extruders) { auto& choices = sidebar().combos_filament(); diff --git a/src/slic3r/GUI/Plater.hpp b/src/slic3r/GUI/Plater.hpp index 2851af654..9a6bcda7b 100644 --- a/src/slic3r/GUI/Plater.hpp +++ b/src/slic3r/GUI/Plater.hpp @@ -179,6 +179,9 @@ public: void fix_through_netfabb(const int obj_idx, const int vol_idx = -1); void send_gcode(); + void take_snapshot(const std::string &snapshot_name); + void take_snapshot(const wxString &snapshot_name); + void on_extruders_change(int extruders_count); void on_config_change(const DynamicPrintConfig &config); // On activating the parent window. diff --git a/src/slic3r/GUI/Selection.hpp b/src/slic3r/GUI/Selection.hpp index c23f23e6b..17ae72356 100644 --- a/src/slic3r/GUI/Selection.hpp +++ b/src/slic3r/GUI/Selection.hpp @@ -3,6 +3,7 @@ #include #include "libslic3r/Geometry.hpp" +#include "libslic3r/ObjectID.hpp" #include "3DScene.hpp" #if ENABLE_RENDER_SELECTION_CENTER @@ -66,7 +67,7 @@ private: Enum m_value; }; -class Selection +class Selection : public Slic3r::ObjectBase { public: typedef std::set IndicesList; diff --git a/src/slic3r/Utils/UndoRedo.cpp b/src/slic3r/Utils/UndoRedo.cpp new file mode 100644 index 000000000..9263db65e --- /dev/null +++ b/src/slic3r/Utils/UndoRedo.cpp @@ -0,0 +1,559 @@ +#include "UndoRedo.hpp" + +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#define CEREAL_FUTURE_EXPERIMENTAL +#include + +#include + +#include + +namespace Slic3r { +namespace UndoRedo { + +// Time interval, start is closed, end is open. +struct Interval +{ +public: + Interval(size_t begin, size_t end) : m_begin(begin), m_end(end) {} + + size_t begin() const { return m_begin; } + size_t end() const { return m_end; } + + bool is_valid() const { return m_begin >= 0 && m_begin < m_end; } + // This interval comes strictly before the rhs interval. + bool strictly_before(const Interval &rhs) const { return this->is_valid() && rhs.is_valid() && m_end <= rhs.m_begin; } + // This interval comes strictly after the rhs interval. + bool strictly_after(const Interval &rhs) const { return this->is_valid() && rhs.is_valid() && rhs.m_end <= m_begin; } + + bool operator<(const Interval &rhs) const { return (m_begin < rhs.m_begin) || (m_begin == rhs.m_begin && m_end < rhs.m_end); } + bool operator==(const Interval &rhs) const { return m_begin == rhs.m_begin && m_end == rhs.m_end; } + + void trim_end(size_t new_end) { m_end = std::min(m_end, new_end); } + void extend_end(size_t new_end) { assert(new_end >= m_end); m_end = new_end; } + +private: + size_t m_begin; + size_t m_end; +}; + +// History of a single object tracked by the Undo / Redo stack. The object may be mutable or immutable. +class ObjectHistoryBase +{ +public: + virtual ~ObjectHistoryBase() {} + + // If the history is empty, the ObjectHistory object could be released. + virtual bool empty() = 0; + + // Release all data after the given timestamp. For the ImmutableObjectHistory, the shared pointer is NOT released. + virtual void relese_after_timestamp(size_t timestamp) = 0; + +#ifndef NDEBUG + virtual bool validate() = 0; +#endif /* NDEBUG */ +}; + +template class ObjectHistory : public ObjectHistoryBase +{ +public: + ~ObjectHistory() override {} + + // If the history is empty, the ObjectHistory object could be released. + bool empty() override { return m_history.empty(); } + + // Release all data after the given timestamp. The shared pointer is NOT released. + void relese_after_timestamp(size_t timestamp) override { + assert(! m_history.empty()); + assert(this->validate()); + // it points to an interval which either starts with timestamp, or follows the timestamp. + auto it = std::lower_bound(m_history.begin(), m_history.end(), T(timestamp, timestamp)); + if (it == m_history.end()) { + auto it_prev = it; + -- it_prev; + assert(it_prev->begin() < timestamp); + // Trim the last interval with timestamp. + it_prev->trim_end(timestamp); + } + m_history.erase(it, m_history.end()); + assert(this->validate()); + } + +protected: + std::vector m_history; +}; + +// Big objects (mainly the triangle meshes) are tracked by Slicer using the shared pointers +// and they are immutable. +// The Undo / Redo stack therefore may keep a shared pointer to these immutable objects +// and as long as the ref counter of these objects is higher than 1 (1 reference is held +// by the Undo / Redo stack), there is no cost associated to holding the object +// at the Undo / Redo stack. Once the reference counter drops to 1 (only the Undo / Redo +// stack holds the reference), the shared pointer may get serialized (and possibly compressed) +// and the shared pointer may be released. +// The history of a single immutable object may not be continuous, as an immutable object may +// be removed from the scene while being kept at the Copy / Paste stack. +template +class ImmutableObjectHistory : public ObjectHistory +{ +public: + ImmutableObjectHistory(std::shared_ptr shared_object) : m_shared_object(shared_object) {} + ~ImmutableObjectHistory() override {} + + void save(size_t active_snapshot_time, size_t current_time) { + assert(m_history.empty() || m_history.back().end() <= active_snapshot_time); + if (m_history.empty() || m_history.back().end() < active_snapshot_time) + m_history.emplace_back(active_snapshot_time, current_time + 1); + else + m_history.back().extend_end(current_time + 1); + } + + bool has_snapshot(size_t timestamp) { + if (m_history.empty()) + return false; + auto it = std::lower_bound(m_history.begin(), m_history.end(), Interval(timestamp, timestamp)); + if (it == m_history.end() || it->begin() >= timestamp) { + if (it == m_history.begin()) + return false; + -- it; + } + return timestamp >= it->begin() && timestamp < it->end(); + } + + bool is_serialized() const { return m_shared_object.get() == nullptr; } + const std::string& serialized_data() const { return m_serialized; } + std::shared_ptr& shared_ptr(StackImpl &stack) { + if (m_shared_object.get() == nullptr && ! this->m_serialized.empty()) { + // Deserialize the object. + std::istringstream iss(m_serialized); + { + Slic3r::UndoRedo::InputArchive archive(stack, iss); + std::unique_ptr::type> mesh(new std::remove_const::type()); + archive(*mesh.get()); + m_shared_object = std::move(mesh); + } + } + return m_shared_object; + } + +#ifndef NDEBUG + bool validate() override; +#endif /* NDEBUG */ + +private: + // Either the source object is held by a shared pointer and the m_serialized field is empty, + // or the shared pointer is null and the object is being serialized into m_serialized. + std::shared_ptr m_shared_object; + std::string m_serialized; +}; + +struct MutableHistoryInterval +{ +private: + struct Data + { + // Reference counter of this data chunk. We may have used shared_ptr, but the shared_ptr is thread safe + // with the associated cost of CPU cache invalidation on refcount change. + size_t refcnt; + size_t size; + char data[1]; + + bool matches(const std::string& rhs) { return this->size == rhs.size() && memcmp(this->data, rhs.data(), this->size) == 0; } + }; + + Interval m_interval; + Data *m_data; + +public: + MutableHistoryInterval(const Interval &interval, const std::string &input_data) : m_interval(interval), m_data(nullptr) { + m_data = (Data*)new char[offsetof(Data, data) + input_data.size()]; + m_data->refcnt = 1; + m_data->size = input_data.size(); + memcpy(m_data->data, input_data.data(), input_data.size()); + } + + MutableHistoryInterval(const Interval &interval, MutableHistoryInterval &other) : m_interval(interval), m_data(other.m_data) { + ++ m_data->refcnt; + } + + // as a key for std::lower_bound + MutableHistoryInterval(const size_t begin, const size_t end) : m_interval(begin, end), m_data(nullptr) {} + + MutableHistoryInterval(MutableHistoryInterval&& rhs) : m_interval(rhs.m_interval), m_data(rhs.m_data) { rhs.m_data = nullptr; } + MutableHistoryInterval& operator=(MutableHistoryInterval&& rhs) { m_interval = rhs.m_interval; m_data = rhs.m_data; rhs.m_data = nullptr; return *this; } + + ~MutableHistoryInterval() { + if (m_data != nullptr && -- m_data->refcnt == 0) + delete[] (char*)m_data; + } + + const Interval& interval() const { return m_interval; } + size_t begin() const { return m_interval.begin(); } + size_t end() const { return m_interval.end(); } + void trim_end(size_t timestamp) { m_interval.trim_end(timestamp); } + void extend_end(size_t timestamp) { m_interval.extend_end(timestamp); } + + bool operator<(const MutableHistoryInterval& rhs) const { return m_interval < rhs.m_interval; } + bool operator==(const MutableHistoryInterval& rhs) const { return m_interval == rhs.m_interval; } + + const char* data() const { return m_data->data; } + size_t size() const { return m_data->size; } + size_t refcnt() const { return m_data->refcnt; } + bool matches(const std::string& data) { return m_data->matches(data); } + +private: + MutableHistoryInterval(const MutableHistoryInterval &rhs); + MutableHistoryInterval& operator=(const MutableHistoryInterval &rhs); +}; + +// Smaller objects (Model, ModelObject, ModelInstance, ModelVolume, DynamicPrintConfig) +// are mutable and there is not tracking of the changes, therefore a snapshot needs to be +// taken every time and compared to the previous data at the Undo / Redo stack. +// The serialized data is stored if it is different from the last value on the stack, otherwise +// the serialized data is discarded. +// The history of a single mutable object may not be continuous, as an mutable object may +// be removed from the scene while being kept at the Copy / Paste stack, therefore an object snapshot +// with the same serialized object data may be shared by multiple history intervals. +template +class MutableObjectHistory : public ObjectHistory +{ +public: + ~MutableObjectHistory() override {} + + void save(size_t active_snapshot_time, size_t current_time, const std::string &data) { + assert(m_history.empty() || m_history.back().end() <= active_snapshot_time); + if (m_history.empty() || m_history.back().end() < active_snapshot_time) { + if (! m_history.empty() && m_history.back().matches(data)) + // Share the previous data by reference counting. + m_history.emplace_back(Interval(current_time, current_time + 1), m_history.back()); + else + // Allocate new data. + m_history.emplace_back(Interval(current_time, current_time + 1), data); + } else { + assert(! m_history.empty()); + assert(m_history.back().end() == active_snapshot_time); + if (m_history.back().matches(data)) + // Just extend the last interval using the old data. + m_history.back().extend_end(current_time + 1); + else + // Allocate new data time continuous with the previous data. + m_history.emplace_back(Interval(active_snapshot_time, current_time + 1), data); + } + } + + std::string load(size_t timestamp) const { + assert(! m_history.empty()); + auto it = std::lower_bound(m_history.begin(), m_history.end(), MutableHistoryInterval(timestamp, timestamp)); + if (it == m_history.end() || it->begin() >= timestamp) { + assert(it != m_history.begin()); + -- it; + } + assert(timestamp >= it->begin() && timestamp < it->end()); + return std::string(it->data(), it->data() + it->size()); + } + +#ifndef NDEBUG + bool validate() override; +#endif /* NDEBUG */ +}; + +#ifndef NDEBUG +template +bool ImmutableObjectHistory::validate() +{ + // The immutable object content is captured either by a shared object, or by its serialization, but not both. + assert(! m_shared_object == ! m_serialized.empty()); + // Verify that the history intervals are sorted and do not overlap. + if (! m_history.empty()) + for (size_t i = 1; i < m_history.size(); ++ i) + assert(m_history[i - 1].strictly_before(m_history[i])); + return true; +} +#endif /* NDEBUG */ + +#ifndef NDEBUG +template +bool MutableObjectHistory::validate() +{ + // Verify that the history intervals are sorted and do not overlap, and that the data reference counters are correct. + if (! m_history.empty()) { + std::map refcntrs; + assert(m_history.front().data() != nullptr); + ++ refcntrs[m_history.front().data()]; + for (size_t i = 1; i < m_history.size(); ++ i) { + assert(m_history[i - 1].interval().strictly_before(m_history[i].interval())); + ++ refcntrs[m_history[i].data()]; + } + for (const auto &hi : m_history) { + assert(hi.data() != nullptr); + assert(refcntrs[hi.data()] == hi.refcnt()); + } + } + return true; +} +#endif /* NDEBUG */ + +class StackImpl +{ +public: + // Stack needs to be initialized. An empty stack is not valid, there must be a "New Project" status stored at the beginning. + StackImpl() : m_active_snapshot_time(0), m_current_time(0) {} + + // The Undo / Redo stack is being initialized with an empty model and an empty selection. + // The first snapshot cannot be removed. + void initialize(const Slic3r::Model &model, const Slic3r::GUI::Selection &selection); + + // Store the current application state onto the Undo / Redo stack, remove all snapshots after m_active_snapshot_time. + void take_snapshot(const std::string &snapshot_name, const Slic3r::Model &model, const Slic3r::GUI::Selection &selection); + void load_snapshot(size_t timestamp, Slic3r::Model &model, Slic3r::GUI::Selection &selection); + + // Snapshot history (names with timestamps). + const std::vector& snapshots() const { return m_snapshots; } + +//protected: + void save_model(const Slic3r::Model &model, size_t snapshot_time); + void save_selection(const Slic3r::Model& model, const Slic3r::GUI::Selection &selection, size_t snapshot_time); + void load_model(const Slic3r::Model &model, size_t snapshot_time); + void load_selection(const Slic3r::GUI::Selection &selection, size_t snapshot_time); + + template ObjectID save_mutable_object(const T &object); + template ObjectID save_immutable_object(std::shared_ptr &object); + template T* load_mutable_object(const Slic3r::ObjectID id); + template std::shared_ptr load_immutable_object(const Slic3r::ObjectID id); + template void load_mutable_object(const Slic3r::ObjectID id, T &target); + +private: + template ObjectID immutable_object_id(const std::shared_ptr &ptr) { + return this->immutable_object_id_impl((const void*)ptr.get()); + } + ObjectID immutable_object_id_impl(const void *ptr) { + auto it = m_shared_ptr_to_object_id.find(ptr); + if (it == m_shared_ptr_to_object_id.end()) { + // Allocate a new temporary ObjectID for this shared pointer. + ObjectBase object_with_id; + it = m_shared_ptr_to_object_id.insert(it, std::make_pair(ptr, object_with_id.id())); + } + return it->second; + } + + // Each individual object (Model, ModelObject, ModelInstance, ModelVolume, Selection, TriangleMesh) + // is stored with its own history, referenced by the ObjectID. Immutable objects do not provide + // their own IDs, therefore there are temporary IDs generated for them and stored to m_shared_ptr_to_object_id. + std::map> m_objects; + std::map m_shared_ptr_to_object_id; + // Snapshot history (names with timestamps). + std::vector m_snapshots; + // Timestamp of the active snapshot. + size_t m_active_snapshot_time; + // Logical time counter. m_current_time is being incremented with each snapshot taken. + size_t m_current_time; +}; + +using InputArchive = cereal::UserDataAdapter; +using OutputArchive = cereal::UserDataAdapter; + +} // namespace UndoRedo + +class Model; +class ModelObject; +class ModelVolume; +class ModelInstance; +class ModelMaterial; + +} // namespace Slic3r + +namespace cereal +{ + // Let cereal know that there are load / save non-member functions declared for ModelObject*, ignore serialization of pointers triggering + // static assert, that cereal does not support serialization of raw pointers. + template struct specialize {}; + template struct specialize {}; + template struct specialize {}; + template struct specialize {}; + template struct specialize {}; + + // Store ObjectBase derived class onto the Undo / Redo stack as a separate object, + // store just the ObjectID to this stream. + template void save(BinaryOutputArchive& ar, T* const& ptr) + { + ar(cereal::get_user_data(ar).save_mutable_object(*ptr)); + } + + // Load ObjectBase derived class from the Undo / Redo stack as a separate object + // based on the ObjectID loaded from this stream. + template void load(BinaryInputArchive& ar, T*& ptr) + { + Slic3r::UndoRedo::StackImpl& stack = cereal::get_user_data(ar); + size_t id; + ar(id); + ptr = stack.load_mutable_object(Slic3r::ObjectID(id)); + } + + // Store ObjectBase derived class onto the Undo / Redo stack as a separate object, + // store just the ObjectID to this stream. + template void save(BinaryOutputArchive& ar, std::shared_ptr& ptr) + { + ar(cereal::get_user_data(ar).save_immutable_object(ptr)); + } + + // Load ObjectBase derived class from the Undo / Redo stack as a separate object + // based on the ObjectID loaded from this stream. + template void load(BinaryInputArchive& ar, std::shared_ptr& ptr) + { + Slic3r::UndoRedo::StackImpl& stack = cereal::get_user_data(ar); + size_t id; + ar(id); + ptr = std::const_pointer_cast(stack.load_immutable_object(Slic3r::ObjectID(id))); + } + +#if 0 + void save(BinaryOutputArchive &ar, const Slic3r::GUI::Selection &selection) + { + size_t num = selection.get_volume_idxs().size(); + ar(num); + for (unsigned int volume_idx : selection.get_volume_idxs()) { + const Slic3r::GLVolume::CompositeID &id = selection.get_volume(volume_idx)->composite_id; + ar(id.object_id, id.volume_id, id.instance_id); + } + } + + template void load(BinaryInputArchive &ar, Slic3r::GUI::Selection &selection) + { + size_t num; + ar(num); + for (size_t i = 0; i < num; ++ i) { + Slic3r::GLVolume::CompositeID id; + ar(id.object_id, id.volume_id, id.instance_id); + } + } +#endif +} + +#include +#include +#include + +namespace Slic3r { +namespace UndoRedo { + +template ObjectID StackImpl::save_mutable_object(const T &object) +{ + // First find or allocate a history stack for the ObjectID of this object instance. + auto it_object_history = m_objects.find(object.id()); + if (it_object_history == m_objects.end()) + it_object_history = m_objects.insert(it_object_history, std::make_pair(object.id(), std::unique_ptr>(new MutableObjectHistory()))); + auto *object_history = static_cast*>(it_object_history->second.get()); + // Then serialize the object into a string. + std::ostringstream oss; + { + Slic3r::UndoRedo::OutputArchive archive(*this, oss); + archive(object); + } + object_history->save(m_active_snapshot_time, m_current_time, oss.str()); + return object.id(); +} + +template ObjectID StackImpl::save_immutable_object(std::shared_ptr &object) +{ + // First allocate a temporary ObjectID for this pointer. + ObjectID object_id = this->immutable_object_id(object); + // and find or allocate a history stack for the ObjectID associated to this shared_ptr. + auto it_object_history = m_objects.find(object_id); + if (it_object_history == m_objects.end()) + it_object_history = m_objects.insert(it_object_history, ObjectID, std::unique_ptr>(new ImmutableObjectHistory(object))); + auto *object_history = ; + // Then save the interval. + static_cast*>(it_object_history->second.get())->save(m_active_snapshot_time, m_current_time); + return object_id; +} + +template T* StackImpl::load_mutable_object(const Slic3r::ObjectID id) +{ + T *target = new T(); + this->load_mutable_object(id, *target); + return target; +} + +template std::shared_ptr StackImpl::load_immutable_object(const Slic3r::ObjectID id) +{ + // First find a history stack for the ObjectID of this object instance. + auto it_object_history = m_objects.find(id); + assert(it_object_history != m_objects.end()); + auto *object_history = static_cast*>(it_object_history->second.get()); + assert(object_history->has_snapshot(m_active_snapshot_time)); + return object_history->shared_ptr(*this); +} + +template void StackImpl::load_mutable_object(const Slic3r::ObjectID id, T &target) +{ + // First find a history stack for the ObjectID of this object instance. + auto it_object_history = m_objects.find(id); + assert(it_object_history != m_objects.end()); + auto *object_history = static_cast*>(it_object_history->second.get()); + // Then get the data associated with the object history and m_active_snapshot_time. + std::istringstream iss(object_history->load(m_active_snapshot_time)); + Slic3r::UndoRedo::InputArchive archive(*this, iss); + archive(target); +} + +// The Undo / Redo stack is being initialized with an empty model and an empty selection. +// The first snapshot cannot be removed. +void StackImpl::initialize(const Slic3r::Model &model, const Slic3r::GUI::Selection &selection) +{ + assert(m_active_snapshot_time == 0); + assert(m_current_time == 0); + // The initial time interval will be <0, 1) + m_active_snapshot_time = SIZE_MAX; // let it overflow to zero in take_snapshot + m_current_time = 0; + this->take_snapshot("New Project", model, selection); +} + +// Store the current application state onto the Undo / Redo stack, remove all snapshots after m_active_snapshot_time. +void StackImpl::take_snapshot(const std::string &snapshot_name, const Slic3r::Model &model, const Slic3r::GUI::Selection &selection) +{ + // Release old snapshot data. + ++ m_active_snapshot_time; + for (auto &kvp : m_objects) + kvp.second->relese_after_timestamp(m_active_snapshot_time); + { + auto it = std::lower_bound(m_snapshots.begin(), m_snapshots.end(), Snapshot(m_active_snapshot_time)); + m_snapshots.erase(it, m_snapshots.end()); + } + // Take new snapshots. + this->save_mutable_object(model); +// this->save_mutable_object(selection); + // Save the snapshot info + m_snapshots.emplace_back(snapshot_name, m_current_time ++, model.id().id); +} + +void StackImpl::load_snapshot(size_t timestamp, Slic3r::Model &model, Slic3r::GUI::Selection &selection) +{ + // Find the snapshot by time. It must exist. + const auto it_snapshot = std::lower_bound(m_snapshots.begin(), m_snapshots.end(), Snapshot(timestamp)); + if (it_snapshot == m_snapshots.end() || it_snapshot->timestamp != timestamp) + throw std::runtime_error((boost::format("Snapshot with timestamp %1% does not exist") % timestamp).str()); + + this->load_mutable_object(ObjectID(it_snapshot->model_object_id), model); + this->load_mutable_object(selection.id(), selection); + this->m_active_snapshot_time = timestamp; +} + +// Wrappers of the private implementation. +Stack::Stack() : pimpl(new StackImpl()) {} +Stack::~Stack() {} +void Stack::initialize(const Slic3r::Model &model, const Slic3r::GUI::Selection &selection) { pimpl->initialize(model, selection); } +void Stack::take_snapshot(const std::string &snapshot_name, const Slic3r::Model &model, const Slic3r::GUI::Selection &selection) { pimpl->take_snapshot(snapshot_name, model, selection); } +void Stack::load_snapshot(size_t timestamp, Slic3r::Model &model, Slic3r::GUI::Selection &selection) { pimpl->load_snapshot(timestamp, model, selection); } +const std::vector& Stack::snapshots() const { return pimpl->snapshots(); } + +} // namespace UndoRedo +} // namespace Slic3r diff --git a/src/slic3r/Utils/UndoRedo.hpp b/src/slic3r/Utils/UndoRedo.hpp new file mode 100644 index 000000000..d452e777c --- /dev/null +++ b/src/slic3r/Utils/UndoRedo.hpp @@ -0,0 +1,58 @@ +#ifndef slic3r_Utils_UndoRedo_hpp_ +#define slic3r_Utils_UndoRedo_hpp_ + +#include +#include + +namespace Slic3r { + +class Model; + +namespace GUI { + class Selection; +} // namespace GUI + +namespace UndoRedo { + +struct Snapshot +{ + Snapshot(size_t timestamp) : timestamp(timestamp) {} + Snapshot(const std::string &name, size_t timestamp, size_t model_object_id) : name(name), timestamp(timestamp), model_object_id(model_object_id) {} + + std::string name; + size_t timestamp; + size_t model_object_id; + + bool operator< (const Snapshot &rhs) const { return this->timestamp < rhs.timestamp; } + bool operator==(const Snapshot &rhs) const { return this->timestamp == rhs.timestamp; } +}; + +class StackImpl; + +class Stack +{ +public: + // Stack needs to be initialized. An empty stack is not valid, there must be a "New Project" status stored at the beginning. + Stack(); + ~Stack(); + + // The Undo / Redo stack is being initialized with an empty model and an empty selection. + // The first snapshot cannot be removed. + void initialize(const Slic3r::Model &model, const Slic3r::GUI::Selection &selection); + + // Store the current application state onto the Undo / Redo stack, remove all snapshots after m_active_snapshot_time. + void take_snapshot(const std::string &snapshot_name, const Slic3r::Model &model, const Slic3r::GUI::Selection &selection); + void load_snapshot(size_t timestamp, Slic3r::Model &model, Slic3r::GUI::Selection &selection); + + // Snapshot history (names with timestamps). + const std::vector& snapshots() const; + +private: + friend class StackImpl; + std::unique_ptr pimpl; +}; + +}; // namespace UndoRedo +}; // namespace Slic3r + +#endif /* slic3r_Utils_UndoRedo_hpp_ */