diff --git a/.gitignore b/.gitignore
index 4481bae5..3fdbaad5 100644
--- a/.gitignore
+++ b/.gitignore
@@ -179,3 +179,4 @@ Assets/StreamingAssets/EphysLink-*
Assets/Samples/*
Assets/settings_conversion.txt
.github/copilot-instructions.md
+.nuget/
diff --git a/Assets/Scripts/Models/SavedState.cs b/Assets/Scripts/Models/SavedState.cs
new file mode 100644
index 00000000..2b9ddfc3
--- /dev/null
+++ b/Assets/Scripts/Models/SavedState.cs
@@ -0,0 +1,18 @@
+using System;
+using Models.Scene;
+using Models.Settings;
+
+namespace Models
+{
+ ///
+ /// Represents the application state for save/load operations.
+ /// Contains scene, atlas, and rig state slices.
+ ///
+ [Serializable]
+ public class SavedState
+ {
+ public SceneState SceneState;
+ public RigState RigState;
+ public AtlasSettingsState AtlasSettingsState;
+ }
+}
diff --git a/Assets/Scripts/Services/FileStorageService.cs b/Assets/Scripts/Services/FileStorageService.cs
new file mode 100644
index 00000000..46accf28
--- /dev/null
+++ b/Assets/Scripts/Services/FileStorageService.cs
@@ -0,0 +1,66 @@
+using System;
+using System.IO;
+using UnityEngine;
+
+namespace Services
+{
+ ///
+ /// Provides methods to save and load data to/from local JSON files.
+ ///
+ public class FileStorageService
+ {
+ ///
+ /// Saves a value of type to a JSON file.
+ ///
+ /// The type of the value to save.
+ /// The full file path where the JSON file will be saved.
+ /// The value to save.
+ /// True if the save was successful, false otherwise.
+ public bool SaveToFile(string filePath, T value)
+ {
+ try
+ {
+ var json = JsonUtility.ToJson(value, true);
+ File.WriteAllText(filePath, json);
+ Debug.Log($"Saved state to: {filePath}");
+ return true;
+ }
+ catch (Exception e)
+ {
+ Debug.LogError($"Failed to save to file {filePath}: {e.Message}");
+ return false;
+ }
+ }
+
+ ///
+ /// Loads a value of type from a JSON file.
+ ///
+ /// The type of the value to load.
+ /// The full file path to the JSON file to load.
+ /// The loaded value, or default if loading failed.
+ /// True if the load was successful, false otherwise.
+ public bool LoadFromFile(string filePath, out T result) where T : new()
+ {
+ result = default;
+
+ try
+ {
+ if (!File.Exists(filePath))
+ {
+ Debug.LogWarning($"File not found: {filePath}");
+ return false;
+ }
+
+ var json = File.ReadAllText(filePath);
+ result = JsonUtility.FromJson(json);
+ Debug.Log($"Loaded state from: {filePath}");
+ return true;
+ }
+ catch (Exception e)
+ {
+ Debug.LogError($"Failed to load from file {filePath}: {e.Message}");
+ return false;
+ }
+ }
+ }
+}
diff --git a/Assets/Scripts/Services/StoreService.cs b/Assets/Scripts/Services/StoreService.cs
index 0b3244f6..268e36b8 100644
--- a/Assets/Scripts/Services/StoreService.cs
+++ b/Assets/Scripts/Services/StoreService.cs
@@ -12,6 +12,7 @@ namespace Services
public class StoreService
{
private readonly LocalStorageService _localStorageService;
+ private readonly FileStorageService _fileStorageService;
///
/// Gets the Redux store instance for partitioned application state.
@@ -23,9 +24,11 @@ public class StoreService
/// Loads initial state from local storage and configures the Redux store.
///
/// The local storage service for state persistence.
- public StoreService(LocalStorageService localStorageService)
+ /// The file storage service for save/load operations.
+ public StoreService(LocalStorageService localStorageService, FileStorageService fileStorageService)
{
_localStorageService = localStorageService;
+ _fileStorageService = fileStorageService;
// Initialize state in memory.
var initialMainState = _localStorageService.GetValue(
@@ -359,5 +362,43 @@ public void Save()
Store.GetState(SliceNames.ATLAS_SETTINGS_SLICE)
);
}
+
+ ///
+ /// Saves the current state to a JSON file.
+ ///
+ /// The full file path where to save the state.
+ /// True if the save was successful, false otherwise.
+ public bool SaveToFile(string filePath)
+ {
+ var savedState = new SavedState
+ {
+ SceneState = Store.GetState(SliceNames.SCENE_SLICE),
+ RigState = Store.GetState(SliceNames.RIG_SLICE),
+ AtlasSettingsState = Store.GetState(SliceNames.ATLAS_SETTINGS_SLICE)
+ };
+
+ return _fileStorageService.SaveToFile(filePath, savedState);
+ }
+
+ ///
+ /// Loads state from a JSON file and updates both the store and local storage.
+ ///
+ /// The full file path from which to load the state.
+ /// True if the load was successful, false otherwise.
+ public bool LoadFromFile(string filePath)
+ {
+ if (!_fileStorageService.LoadFromFile(filePath, out var savedState))
+ return false;
+
+ // Update local storage with the loaded state.
+ _localStorageService.SetValue(SliceNames.SCENE_SLICE, savedState.SceneState);
+ _localStorageService.SetValue(SliceNames.RIG_SLICE, savedState.RigState);
+ _localStorageService.SetValue(SliceNames.ATLAS_SETTINGS_SLICE, savedState.AtlasSettingsState);
+
+ // Note: The scene will be reloaded after this method returns to apply the loaded state.
+ // The store will be re-initialized from the updated local storage on scene reload.
+
+ return true;
+ }
}
}
diff --git a/Assets/Scripts/UI/PinpointAppBuilder.cs b/Assets/Scripts/UI/PinpointAppBuilder.cs
index e4e1dc08..e199bd8e 100644
--- a/Assets/Scripts/UI/PinpointAppBuilder.cs
+++ b/Assets/Scripts/UI/PinpointAppBuilder.cs
@@ -29,6 +29,7 @@ protected override void OnConfiguringApp(AppBuilder builder)
// Services.
builder.services.AddSingleton();
+ builder.services.AddSingleton();
builder.services.AddSingleton();
builder.services.AddSingleton();
builder.services.AddSingleton();
diff --git a/Assets/Scripts/UI/ViewModels/MainViewModel.cs b/Assets/Scripts/UI/ViewModels/MainViewModel.cs
index 11a413e6..78313943 100644
--- a/Assets/Scripts/UI/ViewModels/MainViewModel.cs
+++ b/Assets/Scripts/UI/ViewModels/MainViewModel.cs
@@ -2,10 +2,12 @@
using Models;
using Models.Scene;
using Services;
+using SimpleFileBrowser;
using Unity.AppUI.MVVM;
using Unity.AppUI.Redux;
using Unity.AppUI.UI;
using UnityEngine;
+using UnityEngine.SceneManagement;
using Utils.Types;
namespace UI.ViewModels
@@ -125,6 +127,68 @@ private void SetLeftSidePanelTabIndex(int index)
_storeService.Store.Dispatch(MainActions.SET_LEFT_SIDE_PANEL_TAB_INDEX, index);
}
+ [ICommand]
+ private void SaveStateToFile()
+ {
+ FileBrowser.ShowSaveDialog(
+ onSuccess: (paths) =>
+ {
+ if (paths.Length > 0)
+ {
+ var filePath = paths[0];
+ if (_storeService.SaveToFile(filePath))
+ {
+ Debug.Log($"Scene state saved successfully to: {filePath}");
+ }
+ else
+ {
+ Debug.LogError("Failed to save scene state");
+ }
+ }
+ },
+ onCancel: () => { },
+ pickMode: FileBrowser.PickMode.Files,
+ allowMultiSelection: false,
+ initialPath: null,
+ initialFilename: "scene_state.json",
+ title: "Save Scene State",
+ saveButtonText: "Save"
+ );
+ }
+
+ [ICommand]
+ private void LoadStateFromFile()
+ {
+ FileBrowser.ShowLoadDialog(
+ onSuccess: (paths) =>
+ {
+ if (paths.Length > 0)
+ {
+ var filePath = paths[0];
+ if (_storeService.LoadFromFile(filePath))
+ {
+ Debug.Log($"Scene state loaded successfully from: {filePath}");
+ Debug.Log("Reloading scene to apply loaded state...");
+
+ // Reload the scene to apply the loaded state from local storage
+ SceneManager.LoadScene(SceneManager.GetActiveScene().buildIndex);
+ }
+ else
+ {
+ Debug.LogError("Failed to load scene state");
+ }
+ }
+ },
+ onCancel: () => { },
+ pickMode: FileBrowser.PickMode.Files,
+ allowMultiSelection: false,
+ initialPath: null,
+ initialFilename: null,
+ title: "Load Scene State",
+ loadButtonText: "Load"
+ );
+ }
+
#endregion
}
}
diff --git a/Assets/Scripts/UI/Views/MainView.cs b/Assets/Scripts/UI/Views/MainView.cs
index 11d908f1..bab709bf 100644
--- a/Assets/Scripts/UI/Views/MainView.cs
+++ b/Assets/Scripts/UI/Views/MainView.cs
@@ -45,6 +45,8 @@ public MainView(MainViewModel mainViewModel)
var leftSidePanelCollapseButton = root.Q