diff --git a/Sofa/framework/Core/CMakeLists.txt b/Sofa/framework/Core/CMakeLists.txt
index 8e02de279d4..f200da01f0e 100644
--- a/Sofa/framework/Core/CMakeLists.txt
+++ b/Sofa/framework/Core/CMakeLists.txt
@@ -160,6 +160,7 @@ set(HEADER_FILES
${SRC_ROOT}/objectmodel/BaseLink.h
${SRC_ROOT}/objectmodel/BaseNode.h
${SRC_ROOT}/objectmodel/BaseObjectDescription.h
+ ${SRC_ROOT}/objectmodel/Snapshot.h
${SRC_ROOT}/objectmodel/ClassInfo.h
${SRC_ROOT}/objectmodel/ComponentState.h
${SRC_ROOT}/objectmodel/ConfigurationSetting.h
@@ -178,6 +179,7 @@ set(HEADER_FILES
${SRC_ROOT}/objectmodel/HapticDeviceEvent.h
${SRC_ROOT}/objectmodel/IdleEvent.h
${SRC_ROOT}/objectmodel/JoystickEvent.h
+ ${SRC_ROOT}/objectmodel/SnapshotJSONExporter.h
${SRC_ROOT}/objectmodel/KeypressedEvent.h
${SRC_ROOT}/objectmodel/KeyreleasedEvent.h
${SRC_ROOT}/objectmodel/lifecycle/DeprecatedData.h
@@ -311,6 +313,7 @@ set(SOURCE_FILES
${SRC_ROOT}/objectmodel/BaseLink.cpp
${SRC_ROOT}/objectmodel/BaseNode.cpp
${SRC_ROOT}/objectmodel/BaseObjectDescription.cpp
+ ${SRC_ROOT}/objectmodel/Snapshot.cpp
${SRC_ROOT}/objectmodel/ClassInfo.cpp
${SRC_ROOT}/objectmodel/ComponentState.cpp
${SRC_ROOT}/objectmodel/ConfigurationSetting.cpp
@@ -329,6 +332,7 @@ set(SOURCE_FILES
${SRC_ROOT}/objectmodel/HapticDeviceEvent.cpp
${SRC_ROOT}/objectmodel/IdleEvent.cpp
${SRC_ROOT}/objectmodel/JoystickEvent.cpp
+ ${SRC_ROOT}/objectmodel/SnapshotJSONExporter.cpp
${SRC_ROOT}/objectmodel/KeypressedEvent.cpp
${SRC_ROOT}/objectmodel/KeyreleasedEvent.cpp
${SRC_ROOT}/objectmodel/lifecycle/DeprecatedData.cpp
diff --git a/Sofa/framework/Core/simutest/CMakeLists.txt b/Sofa/framework/Core/simutest/CMakeLists.txt
index 5ec8342f6b6..6095de998c2 100644
--- a/Sofa/framework/Core/simutest/CMakeLists.txt
+++ b/Sofa/framework/Core/simutest/CMakeLists.txt
@@ -10,6 +10,7 @@ set(SOURCE_FILES
objectmodel/BaseContext_test.cpp
objectmodel/BaseLink_simutest.cpp
objectmodel/PathResolver_simutest.cpp
+ objectmodel/Snapshot_test.cpp
)
add_executable(${PROJECT_NAME} ${SOURCE_FILES})
diff --git a/Sofa/framework/Core/simutest/objectmodel/Snapshot_test.cpp b/Sofa/framework/Core/simutest/objectmodel/Snapshot_test.cpp
new file mode 100644
index 00000000000..eff4d4980e2
--- /dev/null
+++ b/Sofa/framework/Core/simutest/objectmodel/Snapshot_test.cpp
@@ -0,0 +1,541 @@
+/******************************************************************************
+* SOFA, Simulation Open-Framework Architecture *
+* (c) 2006 INRIA, USTL, UJF, CNRS, MGH *
+* *
+* This program is free software; you can redistribute it and/or modify it *
+* under the terms of the GNU Lesser General Public License as published by *
+* the Free Software Foundation; either version 2.1 of the License, or (at *
+* your option) any later version. *
+* *
+* This program is distributed in the hope that it will be useful, but WITHOUT *
+* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or *
+* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License *
+* for more details. *
+* *
+* You should have received a copy of the GNU Lesser General Public License *
+* along with this program. If not, see . *
+*******************************************************************************
+* Authors: The SOFA Team and external contributors (see Authors.txt) *
+* *
+* Contact information: contact@sofa-framework.org *
+******************************************************************************/
+#include
+#include
+#include
+using sofa::core::objectmodel::Base ;
+
+#include
+using sofa::testing::BaseSimulationTest ;
+using sofa::simulation::Node ;
+
+#include
+using sofa::core::objectmodel::BaseComponent;
+
+#include
+using sofa::core::objectmodel::Snapshot;
+
+#include
+using sofa::simulation::SaveSnapshotVisitor;
+
+#include
+using sofa::simulation::LoadSnapshotVisitor;
+
+#include
+
+#include
+using sofa::core::objectmodel::Data;
+using sofa::core::objectmodel::BaseLink;
+using sofa::core::objectmodel::MultiLink;
+
+#include
+#include
+
+/**
+* \brief Test component used by Snapshot unit tests
+*
+* This class provides a minimal implementation of a BaseComponent to validate the Snapshot API.
+* It contains helper methods to test the serialization of data and links, createSnapshotObject and findSnapshotObject.
+*
+* The component contains :
+* - a Data called "d_value" used to validate data serialization
+* - a MultiLink called "l_target" used to validate link serialization
+*/
+class TestComponent : public BaseComponent
+{
+public:
+ SOFA_CLASS(TestComponent, BaseComponent);
+
+ Data d_value;
+ sofa::MultiLink l_target;
+
+ TestComponent()
+ : d_value(initData(&d_value, 3.14f, "pi", "test value"))
+ , l_target(initLink("target","target test"))
+ {
+ this->setName("TestComponent");
+ }
+
+ ~TestComponent() override
+ {
+
+ }
+
+ void saveData(Snapshot::SnapshotObject& snapshot)
+ {
+ for (const auto& dataFields = this->getDataFields(); const auto& data : dataFields)
+ {
+ Snapshot::DataInfo dataInfo;
+ dataInfo.name = data->getName();
+ dataInfo.type = data->getValueTypeString();
+ dataInfo.value = data->getValueString();
+
+ snapshot.m_dataContainer.push_back(dataInfo);
+ }
+ }
+
+ void saveLinks(Snapshot::SnapshotObject& snapshot)
+ {
+ for (const auto& links = this->getLinks(); const auto& link : links)
+ {
+ Snapshot::LinkInfo linkInfo;
+ linkInfo.name = link->getName();
+ linkInfo.type = link->getValueTypeString();
+ linkInfo.value = link->getValueString();
+
+ snapshot.m_linkContainer.push_back(linkInfo);
+ }
+ }
+
+ std::shared_ptr createSnapshotObjectTest(const std::shared_ptr& parents) const
+ {
+
+ return this->createSnapshotObject(parents);
+ }
+
+ std::shared_ptr findSnapshotObjectTest(const std::shared_ptr& parents, const std::string& objectname)
+ {
+ return this->findSnapshotObject(parents, objectname);
+ }
+
+};
+
+class Snapshot_test: public BaseSimulationTest
+{
+public:
+ Snapshot_test() {}
+ ~Snapshot_test() override {}
+};
+
+/**
+ * @brief Test of saveDataIn
+ *
+ * This test verifies that saveDataIn save data correctly in a SnapshotObject.
+ *
+ * Test steps:
+ * 1. Create a component (Component) and a snapshot
+ * 2. Save component's data in the snapshot
+ * 3. Check if the snapshot contains the component with expected data
+ *
+ */
+TEST_F(Snapshot_test, saveDataIn)
+{
+ TestComponent Component;
+ auto snapshot = std::make_shared();
+ Component.saveData(*snapshot);
+ for (auto& data : snapshot->m_dataContainer)
+ {
+ if (data.name == "name")
+ {
+ EXPECT_EQ(data.value, "TestComponent");
+ }
+ if(data.name == "pi")
+ {
+ EXPECT_EQ(data.value, "3.14");
+ }
+ }
+}
+
+/**
+ * @brief Test of saveLinksIn
+ *
+ * This test verifies that saveLinksIn save links correctly in a SnapshotObject.
+ *
+ * Test steps:
+ * 1. Create a component (Component) and a snapshot
+ * 2. Save component's links in the snapshot
+ * 3. Check if the snapshot contains the component with expected links
+ *
+ */
+TEST_F(Snapshot_test, saveLinkIn)
+{
+
+ TestComponent tComponent;
+ auto snapshot = std::make_shared();
+
+ tComponent.saveLinks(*snapshot);
+ for (auto& link : snapshot->m_linkContainer)
+ {
+ if (link.name == "name")
+ {
+ EXPECT_EQ(link.value, "@./");
+ }
+ if (link.name == "slaves")
+ {
+ EXPECT_EQ(link.value, "");
+ }
+ if (link.name == "master")
+ {
+ EXPECT_EQ(link.value, "");
+ }
+ }
+}
+
+/**
+ * @brief Test of createSnapshotObject
+ *
+ * This test verifies that createSnapshotObject can find the SnapshotObject in a Snapshot with the component's name.
+ *
+ * Test steps:
+ * 1. Create a component (Component)
+ * 2. Create a SnapshotObject with the function createSnapshotObject
+ * 3. Save Component's data in the SnapshotObject
+ * 4. Verify if every data is correctly saved in the SnapshotObject
+ *
+ */
+TEST_F(Snapshot_test, createSnapshotObject)
+{
+ TestComponent Component;
+
+ auto snapshotParents = std::make_shared();
+ auto snapshotObject = Component.createSnapshotObjectTest(snapshotParents);
+
+ snapshotObject->m_name = "snapshotObject";
+ Component.saveData(*snapshotObject);
+
+ EXPECT_NE(snapshotObject, nullptr);
+ EXPECT_EQ(snapshotObject->m_name, "snapshotObject");
+ for (auto& data : snapshotObject->m_dataContainer)
+ {
+ if(data.name == "pi")
+ {
+ EXPECT_EQ(data.value, "3.14");
+ }
+ }
+}
+
+/**
+ * @brief Test of findSnapshotObject
+ *
+ * This test verifies that findSnapshotObject can find the SnapshotObject in a Snapshot with the component's name.
+ *
+ * Test steps:
+ * 1. Create a component (Component) and a graph of with a SnapshotNode
+ * 2. Save Component in the SnapshotNode (as a SnapshotObject)
+ * 3. Use findSnapshotObject to find the SnapshotObject corresponding to Component
+ * 4. Verify if the SnapshotObject has been correctly found
+ *
+ */
+TEST_F(Snapshot_test, findSnapshotObject)
+{
+ TestComponent Component;
+ auto snapshotNode = std::make_shared("root");
+ auto snapshotParents = std::make_shared();
+ snapshotParents->m_children.push_back(snapshotNode);
+
+ auto snapshot = Component.saveSnapshot(snapshotParents);
+ snapshotNode->m_components.push_back(snapshot);
+
+ auto expectedObject = Component.findSnapshotObjectTest(snapshotNode, "TestComponent");
+
+ EXPECT_NE(expectedObject, nullptr);
+ EXPECT_EQ(Component.getName(), expectedObject->m_name);
+}
+
+/**
+ * @brief Test of saveSnapshot
+ *
+ * This test verifies that saveSnapshot save the data to a previously saved snapshot.
+ *
+ * Test steps:
+ * 1. Create a component (Component) and a empty snapshot
+ * 2. Verify if the snapshot is created and empty
+ * 3. Save Component's data in the snapshot
+ * 4. Verify if the snapshot contains all the data from Component
+ *
+ */
+TEST_F(Snapshot_test, saveSnapshot)
+{
+ TestComponent Component;
+ auto snapshot = std::make_shared();
+ auto snapshotParents = std::make_shared();
+
+ ASSERT_NE(snapshot, nullptr);
+ EXPECT_EQ(snapshot->m_name, "");
+ EXPECT_TRUE(snapshot->m_dataContainer.empty());
+ EXPECT_TRUE(snapshot->m_linkContainer.empty());
+
+ snapshot = Component.saveSnapshot(snapshotParents);
+
+ EXPECT_EQ(snapshot->m_name, "TestComponent");
+ EXPECT_EQ(snapshot->m_dataContainer.size(), 7);
+ EXPECT_EQ(snapshot->m_dataContainer[0].name, "name");
+ EXPECT_EQ(snapshot->m_dataContainer[0].value, "TestComponent");
+ EXPECT_EQ(snapshot->m_dataContainer.back().name, "pi");
+ EXPECT_EQ(snapshot->m_dataContainer.back().value, "3.14");
+ EXPECT_EQ(snapshot->m_linkContainer[0].name, "context");
+ EXPECT_EQ(snapshot->m_linkContainer[0].value, "@./");
+ EXPECT_EQ(snapshot->m_linkContainer.back().name, "target");
+ EXPECT_EQ(snapshot->m_linkContainer.back().value, "");
+
+}
+
+/**
+ * @brief Test of saveSnapshot (change the default value)
+ *
+ * This test verifies that saveSnapshot save the data to a previously saved snapshot.
+ *
+ * Test steps:
+ * 1. Create a component (Component) and a empty snapshot
+ * 2. Change the default value before saving the snapshot
+ * 3. Save Component's data in the snapshot
+ * 4. Verify if the snapshot contains all the data from Component
+ *
+ */
+TEST_F(Snapshot_test, saveSnapshotBis)
+{
+ TestComponent Component;
+ auto snapshot = std::make_shared();
+ auto snapshotParents = std::make_shared();
+
+ EXPECT_EQ(Component.d_value.getValue(),3.14f);
+ Component.d_value.setValue(0.0f);
+
+ snapshot = Component.saveSnapshot(snapshotParents);
+
+ EXPECT_EQ(snapshot->m_name, "TestComponent");
+ EXPECT_EQ(snapshot->m_dataContainer.back().name, "pi");
+ EXPECT_EQ(snapshot->m_dataContainer.back().value, "0");
+
+}
+
+/**
+ * @brief Test of saveSnapshot (with SLAVE)
+ *
+ * This test verifies that saveSnapshot save a component with a slave.
+ *
+ * Test steps:
+ * 1. Create a component (Component) and a empty snapshot
+ * 2. Add a slave (called "Slave") to the component (Component)
+ * 3. Save Component in a snapshot
+ * 4. Verify if the snapshot contains Component and Slave, with the correct hierarchy
+ *
+ */
+TEST_F(Snapshot_test, saveSnapshotWithSlave)
+{
+ auto Component = sofa::core::objectmodel::New();
+ auto snapshot = std::make_shared();
+ auto snapshotParents = std::make_shared();
+
+ snapshot = Component->saveSnapshot(snapshotParents);
+
+ EXPECT_EQ(snapshot->m_linkContainer[1].name, "slaves");
+ EXPECT_EQ(snapshot->m_linkContainer[1].value, "");
+
+ auto Slave = sofa::core::objectmodel::New();
+ Slave->setName("Slave");
+ Component->addSlave(Slave);
+
+ snapshot = Component->saveSnapshot(snapshotParents);
+
+ EXPECT_EQ(snapshot->m_linkContainer[1].name, "slaves");
+ EXPECT_EQ(snapshot->m_linkContainer[1].value, "@SlaveTestComponent/Slave");
+
+ Component->removeSlave(Slave);
+}
+
+/**
+ * @brief Test of loadSnapshot (with SLAVE)
+ *
+ * This test verifies that loadSnapshot restores the state of a component with his slave.
+ *
+ * Test steps:
+ * 1. Create a component (Component) and a snapshot
+ * 2. Create and add a slave (called "Slave") to Component
+ * 3. Save Component in a snapshot
+ * 4. Change a value of a data from Slave (change d_value from 3.14f to 0.0f)
+ * 5. Load Component's data from the snapshot
+ * 5. Verify if Slave's data are restored
+ *
+ */
+TEST_F(Snapshot_test, loadSnapshotWithSlave)
+{
+ auto Component = sofa::core::objectmodel::New();
+ auto snapshot = std::make_shared();
+ auto snapshotNode = std::make_shared();
+ auto snapshotParents = std::make_shared();
+
+ auto Slave = sofa::core::objectmodel::New();
+ Slave->setName("Slave");
+ Component->addSlave(Slave);
+
+ auto saveVisitor = SaveSnapshotVisitor(nullptr, *snapshot);
+ saveVisitor.processObject(Component.get(),snapshotNode);
+
+ Slave->d_value.setValue(0.0f);
+ EXPECT_EQ(Slave->d_value.getValue(), 0.0f);
+
+ auto loadVisitor = LoadSnapshotVisitor(nullptr, *snapshot);
+ loadVisitor.processObject(Component.get(),snapshotNode);
+
+ EXPECT_EQ(Slave->d_value.getValue(), 3.14f);
+
+ Component->removeSlave(Slave);
+
+}
+
+/**
+ * @brief Test of loadDataSnapshot
+ *
+ * This test verifies that loadDataSnapshot restores the state of data to a previously saved snapshot
+ *
+ * Test steps:
+ * 1. Create a component (Component1) and a snapshot
+ * 2. Save Component1's data in the snapshot
+ * 3. Create another component (Component2) and change the data d_value
+ * 4. Load Component1's data into Component2 with loadDataSnapshot
+ * 5. Verify if Component2 has same value as Component1
+ *
+ */
+TEST_F(Snapshot_test, loadDataSnapshot)
+{
+ TestComponent Component1;
+ auto snapshot = std::make_shared();
+ auto snapshotParents = std::make_shared();
+
+ snapshot = Component1.saveSnapshot(snapshotParents);
+
+ TestComponent Component2;
+ Component2.d_value.setValue(0.0f);
+ Component2.loadSnapshot(snapshot);
+
+ EXPECT_EQ(Component2.d_value.getValue(), 3.14f);
+}
+
+/**
+ * @brief Test of loadLinkSnapshot
+ *
+ * This test verifies that loadLinkSnapshot restores the state of a link to a previously saved snapshot.
+ * First, it set up a graph "root" with 3 components: Component1, Component2 and Component3
+ * Each component holds a multi-link l_target pointing to other components
+ *
+ * Test steps:
+ * 1. Add Component2 to Component1's l_target link.
+ * 2. Save a snapshot of Component1 (l_target points to @Component2 only).
+ * 3. Add Component3 to Component1's l_target link.
+ * 4. Verify that l_target now points to both @Component2 and@Component3.
+ * 5. Restore Component1 from the snapshot.
+ * 6. Verify that l_target is back to pointing to @Component2 only,
+ * confirming that it @Component3 was correctly removed by loadLinkSnapshot.
+ *
+ */
+TEST_F(Snapshot_test, loadLinkSnapshot)
+{
+ const SceneInstance scene("root");
+ auto Component1 = sofa::core::objectmodel::New();
+ Component1->setName("Component1");
+ auto Component2 = sofa::core::objectmodel::New();
+ Component2->setName("Component2");
+ auto Component3 = sofa::core::objectmodel::New();
+ Component3->setName("Component3");
+ scene.root->addObject(Component1);
+ scene.root->addObject(Component2);
+ scene.root->addObject(Component3);
+
+ auto snapshotNode = std::make_shared("root");
+ auto snapshotParents = std::make_shared();
+ snapshotParents->m_children.push_back(snapshotNode);
+
+ TestComponent* ptr = Component2.get();
+ Component1->l_target.add(ptr);
+
+ EXPECT_EQ(Component1->l_target.getValueString(), "@Component2");
+
+ auto snapshotObject1 = std::make_shared();
+ snapshotObject1 = Component1->saveSnapshot(snapshotParents);
+
+ ptr = Component3.get();
+ Component1->l_target.add(ptr);
+
+ EXPECT_EQ(Component1->l_target.getValueString(), "@Component2 @Component3");
+
+ Component1->loadSnapshot(snapshotObject1);
+
+ EXPECT_EQ(Component1->l_target.getValueString(), "@Component2");
+}
+
+/**
+ * @brief Test of SnapshotJSONExporter
+ *
+ * This test verifies the behavior of the export and the import with SnapshotJSONExporter
+ *
+ * Test steps:
+ * 1. Init the scene
+ * 2. Create a snapshot (snapshot)
+ * 3. Run SaveSnapshotVisitor to save the state in m_snapshot
+ * 4. Export m_snapshot to a JSON file and check if the file is valid
+ * 5. Create another snapshot (snapshot_import)
+ * 6. Import the JSON file in snapshot_import
+ * 7. Compare snapshot_import with the scene
+ * 8. Compare snapshot and snapshot_import
+ *
+ */
+TEST_F(Snapshot_test, SnapshotJSONExporter)
+{
+ const std::string scene = R"(
+
+
+
+
+
+
+
+
+
+ )";
+
+ SceneInstance c("xml", scene) ;
+ c.initScene() ;
+
+ Node* root = c.root.get() ;
+
+ std::filesystem::path path = std::filesystem::temp_directory_path() / "test_file.json";
+ auto snapshot = std::make_shared();
+
+ auto visitor = SaveSnapshotVisitor(nullptr, *snapshot);
+ root->execute(visitor);
+
+ exportToJSON(*snapshot, path.string());
+
+ std::ifstream checkFile(path);
+ EXPECT_TRUE(checkFile.good());
+ checkFile.close();
+
+ auto snapshot_import = std::make_shared();
+ importFrom(*snapshot_import, path.string());
+
+ EXPECT_NE(snapshot_import->m_graphRoot,nullptr);
+
+ EXPECT_EQ(snapshot_import->m_graphRoot->m_name,"Root");
+ EXPECT_EQ(snapshot_import->m_graphRoot->m_components[1]->m_name,"DefaultAnimationLoop1");
+ EXPECT_EQ(snapshot_import->m_graphRoot->m_components[2]->m_name,"DefaultVisualManagerLoop1");
+ EXPECT_EQ(snapshot_import->m_graphRoot->m_children[0]->m_name,"child1");
+ EXPECT_EQ(snapshot_import->m_graphRoot->m_children[0]->m_components[0]->m_name,"MechanicalObject1");
+
+ EXPECT_EQ(snapshot_import->m_graphRoot->m_name,snapshot->m_graphRoot->m_name);
+ EXPECT_EQ(snapshot_import->m_graphRoot->m_components[1]->m_name,snapshot->m_graphRoot->m_components[1]->m_name);
+ EXPECT_EQ(snapshot_import->m_graphRoot->m_components[2]->m_name,snapshot->m_graphRoot->m_components[2]->m_name);
+ EXPECT_EQ(snapshot_import->m_graphRoot->m_children[0]->m_name,snapshot->m_graphRoot->m_children[0]->m_name);
+ EXPECT_EQ(snapshot_import->m_graphRoot->m_children[0]->m_components[0]->m_name,snapshot->m_graphRoot->m_children[0]->m_components[0]->m_name);
+
+ std::filesystem::remove(path);
+}
diff --git a/Sofa/framework/Core/src/sofa/core/objectmodel/Base.cpp b/Sofa/framework/Core/src/sofa/core/objectmodel/Base.cpp
index 7d201561cdf..f712ad3b7c7 100644
--- a/Sofa/framework/Core/src/sofa/core/objectmodel/Base.cpp
+++ b/Sofa/framework/Core/src/sofa/core/objectmodel/Base.cpp
@@ -20,8 +20,8 @@
* Contact information: contact@sofa-framework.org *
******************************************************************************/
#define SOFA_CORE_OBJECTMODEL_BASE_CPP
-#include
+#include
#include
#include
#include
@@ -37,11 +37,15 @@ using sofa::helper::logging::Message ;
#include
using sofa::helper::getClosestMatch;
+#include
+
#include