diff --git a/SpineViewer/Models/PreferenceModel.cs b/SpineViewer/Models/PreferenceModel.cs
index 2a90d8a..afe9d3f 100644
--- a/SpineViewer/Models/PreferenceModel.cs
+++ b/SpineViewer/Models/PreferenceModel.cs
@@ -80,41 +80,5 @@ namespace SpineViewer.Models
private AppLanguage _appLanguage;
#endregion
-
- #region 序列化与反序列
-
- ///
- /// 保存 Json 文件的格式参数
- ///
- private static readonly JsonSerializerOptions _jsonOptions = new()
- {
- WriteIndented = true,
- Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping,
- AllowTrailingCommas = true,
- ReadCommentHandling = JsonCommentHandling.Skip
- };
-
- ///
- /// 从文件反序列对象, 可能抛出异常
- ///
- public static PreferenceModel Deserialize(string path)
- {
- if (!File.Exists(path)) throw new FileNotFoundException("Preference file not found", path);
- var json = File.ReadAllText(path, Encoding.UTF8);
- var model = JsonSerializer.Deserialize(json, _jsonOptions);
- return model ?? throw new JsonException($"null data in file '{path}'");
- }
-
- ///
- /// 保存至文件, 可能抛出异常
- ///
- public void Serialize(string path)
- {
- Directory.CreateDirectory(Path.GetDirectoryName(path));
- var json = JsonSerializer.Serialize(this, _jsonOptions);
- File.WriteAllText(path, json, Encoding.UTF8);
- }
-
- #endregion
}
}
diff --git a/SpineViewer/Models/SpineObjectConfigModel.cs b/SpineViewer/Models/SpineObjectConfigModel.cs
index b776704..27a1730 100644
--- a/SpineViewer/Models/SpineObjectConfigModel.cs
+++ b/SpineViewer/Models/SpineObjectConfigModel.cs
@@ -13,8 +13,6 @@ namespace SpineViewer.Models
{
public class SpineObjectConfigModel
{
- public bool IsShown { get; set; } = true;
-
public bool UsePma { get; set; }
public string Physics { get; set; } = ISkeleton.Physics.Update.ToString();
@@ -54,41 +52,5 @@ namespace SpineViewer.Models
public bool DebugPoints { get; set; }
public bool DebugClippings { get; set; }
-
- #region 序列化与反序列
-
- ///
- /// 保存 Json 文件的格式参数
- ///
- private static readonly JsonSerializerOptions _jsonOptions = new()
- {
- WriteIndented = true,
- Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping,
- AllowTrailingCommas = true,
- ReadCommentHandling = JsonCommentHandling.Skip
- };
-
- ///
- /// 从文件反序列对象, 可能抛出异常
- ///
- public static SpineObjectConfigModel Deserialize(string path)
- {
- if (!File.Exists(path)) throw new FileNotFoundException("Config file not found", path);
- var json = File.ReadAllText(path, Encoding.UTF8);
- var model = JsonSerializer.Deserialize(json, _jsonOptions);
- return model ?? throw new JsonException($"null data in file '{path}'");
- }
-
- ///
- /// 保存至文件, 可能抛出异常
- ///
- public void Serialize(string path)
- {
- Directory.CreateDirectory(Path.GetDirectoryName(path));
- var json = JsonSerializer.Serialize(this, _jsonOptions);
- File.WriteAllText(path, json, Encoding.UTF8);
- }
-
- #endregion
}
}
diff --git a/SpineViewer/Models/SpineObjectModel.cs b/SpineViewer/Models/SpineObjectModel.cs
index a12b474..292a6db 100644
--- a/SpineViewer/Models/SpineObjectModel.cs
+++ b/SpineViewer/Models/SpineObjectModel.cs
@@ -61,7 +61,6 @@ namespace SpineViewer.Models
DebugPoints = _loadOptions.DebugPoints,
DebugClippings = _loadOptions.DebugClippings
};
-
_skins = _spineObject.Data.Skins.Select(v => v.Name).ToImmutableArray();
_slotAttachments = _spineObject.Data.SlotAttachments.ToFrozenDictionary(it => it.Key, it => it.Value.Keys);
_animations = _spineObject.Data.Animations.Select(v => v.Name).ToImmutableArray();
@@ -71,6 +70,19 @@ namespace SpineViewer.Models
_spineObject.AnimationState.SetAnimation(0, _spineObject.Data.Animations[0], true);
}
+ ///
+ /// 从工作区配置进行构造
+ ///
+ public SpineObjectModel(SpineObjectWorkspaceConfigModel cfg)
+ {
+ _spineObject = new(cfg.SkelPath, cfg.AtlasPath);
+ _skins = _spineObject.Data.Skins.Select(v => v.Name).ToImmutableArray();
+ _slotAttachments = _spineObject.Data.SlotAttachments.ToFrozenDictionary(it => it.Key, it => it.Value.Keys);
+ _animations = _spineObject.Data.Animations.Select(v => v.Name).ToImmutableArray();
+ ObjectConfig = cfg.ObjectConfig;
+ _isShown = cfg.IsShown;
+ }
+
public event EventHandler? SkinStatusChanged;
public event EventHandler? SlotAttachmentChanged;
@@ -345,99 +357,107 @@ namespace SpineViewer.Models
lock (_lock) return _spineObject.GetCurrentBounds();
}
- ///
- /// 导出参数对象
- ///
- public SpineObjectConfigModel Dump()
+ public SpineObjectConfigModel ObjectConfig
{
- lock (_lock)
+ get
{
- SpineObjectConfigModel config = new()
+ lock (_lock)
{
- Scale = Math.Abs(_spineObject.Skeleton.ScaleX),
- FlipX = _spineObject.Skeleton.ScaleX < 0,
- FlipY = _spineObject.Skeleton.ScaleY < 0,
- X = _spineObject.Skeleton.X,
- Y = _spineObject.Skeleton.Y,
+ SpineObjectConfigModel config = new()
+ {
+ Scale = Math.Abs(_spineObject.Skeleton.ScaleX),
+ FlipX = _spineObject.Skeleton.ScaleX < 0,
+ FlipY = _spineObject.Skeleton.ScaleY < 0,
+ X = _spineObject.Skeleton.X,
+ Y = _spineObject.Skeleton.Y,
- IsShown = _isShown,
- UsePma = _spineObject.UsePma,
- Physics = _spineObject.Physics.ToString(),
+ UsePma = _spineObject.UsePma,
+ Physics = _spineObject.Physics.ToString(),
- DebugTexture = _spineObject.DebugTexture,
- DebugBounds = _spineObject.DebugBounds,
- DebugBones = _spineObject.DebugBones,
- DebugRegions = _spineObject.DebugRegions,
- DebugMeshHulls = _spineObject.DebugMeshHulls,
- DebugMeshes = _spineObject.DebugMeshes,
- DebugBoundingBoxes = _spineObject.DebugBoundingBoxes,
- DebugPaths = _spineObject.DebugPaths,
- DebugPoints = _spineObject.DebugPoints,
- DebugClippings = _spineObject.DebugClippings
- };
+ DebugTexture = _spineObject.DebugTexture,
+ DebugBounds = _spineObject.DebugBounds,
+ DebugBones = _spineObject.DebugBones,
+ DebugRegions = _spineObject.DebugRegions,
+ DebugMeshHulls = _spineObject.DebugMeshHulls,
+ DebugMeshes = _spineObject.DebugMeshes,
+ DebugBoundingBoxes = _spineObject.DebugBoundingBoxes,
+ DebugPaths = _spineObject.DebugPaths,
+ DebugPoints = _spineObject.DebugPoints,
+ DebugClippings = _spineObject.DebugClippings
+ };
- config.LoadedSkins.AddRange(_spineObject.Data.Skins.Select(it => it.Name).Where(_spineObject.GetSkinStatus));
+ config.LoadedSkins.AddRange(_spineObject.Data.Skins.Select(it => it.Name).Where(_spineObject.GetSkinStatus));
- foreach (var slot in _spineObject.Skeleton.Slots) config.SlotAttachment[slot.Name] = slot.Attachment?.Name;
+ foreach (var slot in _spineObject.Skeleton.Slots) config.SlotAttachment[slot.Name] = slot.Attachment?.Name;
- // XXX: 处理空动画
- config.Animations.AddRange(_spineObject.AnimationState.IterTracks().Select(tr => tr?.Animation.Name));
+ // XXX: 处理空动画
+ config.Animations.AddRange(_spineObject.AnimationState.IterTracks().Select(tr => tr?.Animation.Name));
- return config;
+ return config;
+ }
+ }
+ set
+ {
+
+ lock (_lock)
+ {
+ _spineObject.Skeleton.ScaleX = value.Scale;
+ _spineObject.Skeleton.ScaleY = value.Scale;
+ OnPropertyChanged(nameof(Scale));
+ SetProperty(_spineObject.Skeleton.ScaleX < 0, value.FlipX, v => _spineObject.Skeleton.ScaleX *= -1, nameof(FlipX));
+ SetProperty(_spineObject.Skeleton.ScaleY < 0, value.FlipY, v => _spineObject.Skeleton.ScaleY *= -1, nameof(FlipY));
+ SetProperty(_spineObject.Skeleton.X, value.X, v => _spineObject.Skeleton.X = v, nameof(X));
+ SetProperty(_spineObject.Skeleton.Y, value.Y, v => _spineObject.Skeleton.Y = v, nameof(Y));
+ SetProperty(_spineObject.UsePma, value.UsePma, v => _spineObject.UsePma = v, nameof(UsePma));
+ SetProperty(_spineObject.Physics, Enum.Parse(value.Physics ?? "Update", true), v => _spineObject.Physics = v, nameof(Physics));
+
+ foreach (var name in _spineObject.Data.Skins.Select(v => v.Name).Except(value.LoadedSkins))
+ if (_spineObject.SetSkinStatus(name, false))
+ SkinStatusChanged?.Invoke(this, new(name, false));
+ foreach (var name in value.LoadedSkins)
+ if (_spineObject.SetSkinStatus(name, true))
+ SkinStatusChanged?.Invoke(this, new(name, true));
+
+ foreach (var (slotName, attachmentName) in value.SlotAttachment)
+ if (_spineObject.SetAttachment(slotName, attachmentName))
+ SlotAttachmentChanged?.Invoke(this, new(slotName, attachmentName));
+
+ // XXX: 处理空动画
+ _spineObject.AnimationState.ClearTracks();
+ int trackIndex = 0;
+ foreach (var name in value.Animations)
+ {
+ if (!string.IsNullOrEmpty(name))
+ _spineObject.AnimationState.SetAnimation(trackIndex, name, true);
+ AnimationChanged?.Invoke(this, new(trackIndex, name));
+ trackIndex++;
+ }
+
+ SetProperty(_spineObject.DebugTexture, value.DebugTexture, v => _spineObject.DebugTexture = v, nameof(DebugTexture));
+ SetProperty(_spineObject.DebugBounds, value.DebugBounds, v => _spineObject.DebugBounds = v, nameof(DebugBounds));
+ SetProperty(_spineObject.DebugBones, value.DebugBones, v => _spineObject.DebugBones = v, nameof(DebugBones));
+ SetProperty(_spineObject.DebugRegions, value.DebugRegions, v => _spineObject.DebugRegions = v, nameof(DebugRegions));
+ SetProperty(_spineObject.DebugMeshHulls, value.DebugMeshHulls, v => _spineObject.DebugMeshHulls = v, nameof(DebugMeshHulls));
+ SetProperty(_spineObject.DebugMeshes, value.DebugMeshes, v => _spineObject.DebugMeshes = v, nameof(DebugMeshes));
+ SetProperty(_spineObject.DebugBoundingBoxes, value.DebugBoundingBoxes, v => _spineObject.DebugBoundingBoxes = v, nameof(DebugBoundingBoxes));
+ SetProperty(_spineObject.DebugPaths, value.DebugPaths, v => _spineObject.DebugPaths = v, nameof(DebugPaths));
+ SetProperty(_spineObject.DebugPoints, value.DebugPoints, v => _spineObject.DebugPoints = v, nameof(DebugPoints));
+ SetProperty(_spineObject.DebugClippings, value.DebugClippings, v => _spineObject.DebugClippings = v, nameof(DebugClippings));
+ }
}
}
- ///
- /// 从参数对象加载参数值
- ///
- public void Load(SpineObjectConfigModel config)
+ public SpineObjectWorkspaceConfigModel WorkspaceConfig
{
- lock (_lock)
+ get
{
- _spineObject.Skeleton.ScaleX = config.Scale;
- _spineObject.Skeleton.ScaleY = config.Scale;
- OnPropertyChanged(nameof(Scale));
- SetProperty(_spineObject.Skeleton.ScaleX < 0, config.FlipX, v => _spineObject.Skeleton.ScaleX *= -1, nameof(FlipX));
- SetProperty(_spineObject.Skeleton.ScaleY < 0, config.FlipY, v => _spineObject.Skeleton.ScaleY *= -1, nameof(FlipY));
- SetProperty(_spineObject.Skeleton.X, config.X, v => _spineObject.Skeleton.X = v, nameof(X));
- SetProperty(_spineObject.Skeleton.Y, config.Y, v => _spineObject.Skeleton.Y = v, nameof(Y));
-
- IsShown = config.IsShown;
- SetProperty(_spineObject.UsePma, config.UsePma, v => _spineObject.UsePma = v, nameof(UsePma));
- SetProperty(_spineObject.Physics, Enum.Parse(config.Physics ?? "Update", true), v => _spineObject.Physics = v, nameof(Physics));
-
- foreach (var name in _spineObject.Data.Skins.Select(v => v.Name).Except(config.LoadedSkins))
- if (_spineObject.SetSkinStatus(name, false))
- SkinStatusChanged?.Invoke(this, new(name, false));
- foreach (var name in config.LoadedSkins)
- if (_spineObject.SetSkinStatus(name, true))
- SkinStatusChanged?.Invoke(this, new(name, true));
-
- foreach (var (slotName, attachmentName) in config.SlotAttachment)
- if (_spineObject.SetAttachment(slotName, attachmentName))
- SlotAttachmentChanged?.Invoke(this, new(slotName, attachmentName));
-
- // XXX: 处理空动画
- _spineObject.AnimationState.ClearTracks();
- int trackIndex = 0;
- foreach (var name in config.Animations)
+ return new()
{
- if (!string.IsNullOrEmpty(name))
- _spineObject.AnimationState.SetAnimation(trackIndex, name, true);
- AnimationChanged?.Invoke(this, new(trackIndex, name));
- trackIndex++;
- }
-
- SetProperty(_spineObject.DebugTexture, config.DebugTexture, v => _spineObject.DebugTexture = v, nameof(DebugTexture));
- SetProperty(_spineObject.DebugBounds, config.DebugBounds, v => _spineObject.DebugBounds = v, nameof(DebugBounds));
- SetProperty(_spineObject.DebugBones, config.DebugBones, v => _spineObject.DebugBones = v, nameof(DebugBones));
- SetProperty(_spineObject.DebugRegions, config.DebugRegions, v => _spineObject.DebugRegions = v, nameof(DebugRegions));
- SetProperty(_spineObject.DebugMeshHulls, config.DebugMeshHulls, v => _spineObject.DebugMeshHulls = v, nameof(DebugMeshHulls));
- SetProperty(_spineObject.DebugMeshes, config.DebugMeshes, v => _spineObject.DebugMeshes = v, nameof(DebugMeshes));
- SetProperty(_spineObject.DebugBoundingBoxes, config.DebugBoundingBoxes, v => _spineObject.DebugBoundingBoxes = v, nameof(DebugBoundingBoxes));
- SetProperty(_spineObject.DebugPaths, config.DebugPaths, v => _spineObject.DebugPaths = v, nameof(DebugPaths));
- SetProperty(_spineObject.DebugPoints, config.DebugPoints, v => _spineObject.DebugPoints = v, nameof(DebugPoints));
- SetProperty(_spineObject.DebugClippings, config.DebugClippings, v => _spineObject.DebugClippings = v, nameof(DebugClippings));
+ SkelPath = SkelPath,
+ AtlasPath = AtlasPath,
+ IsShown = IsShown,
+ ObjectConfig = ObjectConfig
+ };
}
}
diff --git a/SpineViewer/Models/WorkspaceModel.cs b/SpineViewer/Models/WorkspaceModel.cs
new file mode 100644
index 0000000..2a42630
--- /dev/null
+++ b/SpineViewer/Models/WorkspaceModel.cs
@@ -0,0 +1,57 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Text;
+using System.Text.Encodings.Web;
+using System.Text.Json;
+using System.Threading.Tasks;
+using System.Windows.Media;
+
+namespace SpineViewer.Models
+{
+ public class WorkspaceModel
+ {
+ public RendererWorkspaceConfigModel RendererConfig { get; set; } = new();
+ public List LoadedSpineObjects { get; set; } = [];
+ }
+
+ public class RendererWorkspaceConfigModel
+ {
+ public uint ResolutionX { get; set; } = 100;
+
+ public uint ResolutionY { get; set; } = 100;
+
+ public float CenterX { get; set; }
+
+ public float CenterY { get; set; }
+
+ public float Zoom { get; set; } = 1f;
+
+ public float Rotation { get; set; }
+
+ public bool FlipX { get; set; }
+
+ public bool FlipY { get; set; } = true;
+
+ public uint MaxFps { get; set; } = 30;
+
+ public bool ShowAxis { get; set; } = true;
+
+ public Color BackgroundColor { get; set; }
+
+ // TODO: 背景图片
+ //public string? BackgroundImagePath { get; set; }
+
+ //public ? BackgroundImageDisplayMode { get; set; }
+ }
+
+ public class SpineObjectWorkspaceConfigModel
+ {
+ public string SkelPath { get; set; } = "";
+ public string AtlasPath { get; set; } = "";
+ public bool IsShown { get; set; } = true;
+ public SpineObjectConfigModel ObjectConfig { get; set; } = new();
+ }
+
+}
diff --git a/SpineViewer/Resources/Strings/en.xaml b/SpineViewer/Resources/Strings/en.xaml
index 2b1301e..cadde50 100644
--- a/SpineViewer/Resources/Strings/en.xaml
+++ b/SpineViewer/Resources/Strings/en.xaml
@@ -11,6 +11,8 @@
Experimental Features
Open...
+ Open Workspace...
+ Save Workspace...
Preferences
Preferences...
Exit
diff --git a/SpineViewer/Resources/Strings/ja.xaml b/SpineViewer/Resources/Strings/ja.xaml
index 50d9bab..83a119a 100644
--- a/SpineViewer/Resources/Strings/ja.xaml
+++ b/SpineViewer/Resources/Strings/ja.xaml
@@ -11,6 +11,8 @@
実験機能
開く...
+ ワークスペースを開く...
+ ワークスペースを保存...
設定
設定...
終了
diff --git a/SpineViewer/Resources/Strings/zh.xaml b/SpineViewer/Resources/Strings/zh.xaml
index 789e986..983b8e4 100644
--- a/SpineViewer/Resources/Strings/zh.xaml
+++ b/SpineViewer/Resources/Strings/zh.xaml
@@ -11,6 +11,8 @@
实验性功能
打开...
+ 打开工作区...
+ 保存工作区...
首选项
首选项...
退出
diff --git a/SpineViewer/Services/DialogService.cs b/SpineViewer/Services/DialogService.cs
index e818f5d..25ac263 100644
--- a/SpineViewer/Services/DialogService.cs
+++ b/SpineViewer/Services/DialogService.cs
@@ -78,12 +78,12 @@ namespace SpineViewer.Services
return false;
}
- public static bool ShowOpenFileDialog(out string? fileName, string initialDirectory = "", string filter = "All|*.*")
+ public static bool ShowOpenJsonDialog(out string? fileName, string initialDirectory = "")
{
var dialog = new OpenFileDialog()
{
InitialDirectory = initialDirectory,
- Filter = filter
+ Filter = "Json|*.jcfg;*.json|All|*.*"
};
if (dialog.ShowDialog() is true)
{
@@ -94,14 +94,14 @@ namespace SpineViewer.Services
return false;
}
- public static bool ShowSaveFileDialog(ref string? fileName, string initialDirectory = "", string defaultExt = "", string filter = "All|*.*")
+ public static bool ShowSaveJsonDialog(ref string? fileName, string initialDirectory = "")
{
- var dialog = new SaveFileDialog()
- {
+ var dialog = new SaveFileDialog()
+ {
FileName = fileName,
InitialDirectory = initialDirectory,
- DefaultExt = defaultExt,
- Filter = filter,
+ DefaultExt = ".jcfg",
+ Filter = "Json|*.jcfg;*.json|All|*.*",
};
if (dialog.ShowDialog() is true)
{
diff --git a/SpineViewer/Utils/JsonHelper.cs b/SpineViewer/Utils/JsonHelper.cs
new file mode 100644
index 0000000..14fbd6f
--- /dev/null
+++ b/SpineViewer/Utils/JsonHelper.cs
@@ -0,0 +1,88 @@
+using Microsoft.Win32;
+using NLog;
+using SpineViewer.Models;
+using SpineViewer.Services;
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Text;
+using System.Text.Encodings.Web;
+using System.Text.Json;
+using System.Threading.Tasks;
+
+namespace SpineViewer.Utils
+{
+ public static class JsonHelper
+ {
+ private static readonly Logger _logger = LogManager.GetCurrentClassLogger();
+
+ ///
+ /// 保存 Json 文件的格式参数
+ ///
+ public static JsonSerializerOptions JsonOptions => _jsonOptions;
+ private static readonly JsonSerializerOptions _jsonOptions = new()
+ {
+ WriteIndented = true,
+ Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping,
+ AllowTrailingCommas = true,
+ ReadCommentHandling = JsonCommentHandling.Skip
+ };
+
+ ///
+ /// 从文件反序列对象, 不会抛出异常
+ ///
+ public static bool Deserialize(string path, out T obj)
+ {
+ if (!File.Exists(path))
+ {
+ _logger.Error("Json file {0} not found", path);
+ MessagePopupService.Error($"Json file {path} not found");
+ }
+ else
+ {
+ try
+ {
+ var json = File.ReadAllText(path, Encoding.UTF8);
+ var model = JsonSerializer.Deserialize(json, _jsonOptions);
+ if (model is T m)
+ {
+ obj = m;
+ return true;
+ }
+ _logger.Error("Null data in file {0}", path);
+ MessagePopupService.Error($"Null data in file {path}");
+ }
+ catch (Exception ex)
+ {
+ _logger.Error("Failed to read json file {0}, {1}", path, ex.Message);
+ _logger.Trace(ex.ToString());
+ MessagePopupService.Error($"Failed to read json file {path}, {ex.ToString()}");
+ }
+ }
+ obj = default;
+ return false;
+ }
+
+ ///
+ /// 保存至文件, 不会抛出异常
+ ///
+ public static bool Serialize(T obj, string path)
+ {
+ try
+ {
+ Directory.CreateDirectory(Path.GetDirectoryName(path));
+ var json = JsonSerializer.Serialize(obj, _jsonOptions);
+ File.WriteAllText(path, json, Encoding.UTF8);
+ }
+ catch (Exception ex)
+ {
+ _logger.Error("Failed to save json file {0}, {1}", path, ex.Message);
+ _logger.Trace(ex.ToString());
+ MessagePopupService.Error($"Failed to save json file {path}, {ex.ToString()}");
+ return false;
+ }
+ return true;
+ }
+ }
+}
diff --git a/SpineViewer/Extensions/ObservableCollectionWithLock.cs b/SpineViewer/Utils/ObservableCollectionWithLock.cs
similarity index 94%
rename from SpineViewer/Extensions/ObservableCollectionWithLock.cs
rename to SpineViewer/Utils/ObservableCollectionWithLock.cs
index b6a48b6..37dc297 100644
--- a/SpineViewer/Extensions/ObservableCollectionWithLock.cs
+++ b/SpineViewer/Utils/ObservableCollectionWithLock.cs
@@ -6,7 +6,7 @@ using System.Text;
using System.Threading.Tasks;
using System.Windows.Data;
-namespace SpineViewer.Extensions
+namespace SpineViewer.Utils
{
public class ObservableCollectionWithLock : ObservableCollection
{
diff --git a/SpineViewer/Extensions/StringFormatMultiValueConverter.cs b/SpineViewer/Utils/StringFormatMultiValueConverter.cs
similarity index 88%
rename from SpineViewer/Extensions/StringFormatMultiValueConverter.cs
rename to SpineViewer/Utils/StringFormatMultiValueConverter.cs
index dada843..a3ce857 100644
--- a/SpineViewer/Extensions/StringFormatMultiValueConverter.cs
+++ b/SpineViewer/Utils/StringFormatMultiValueConverter.cs
@@ -7,7 +7,7 @@ using System.Threading.Tasks;
using System.Windows;
using System.Windows.Data;
-namespace SpineViewer.Extensions
+namespace SpineViewer.Utils
{
public class StringFormatMultiValueConverter : IMultiValueConverter
{
@@ -16,7 +16,7 @@ namespace SpineViewer.Extensions
if (values == null || values.Length <= 0)
return DependencyProperty.UnsetValue;
- if (App.Current.TryFindResource(parameter) is string format)
+ if (Application.Current.TryFindResource(parameter) is string format)
{
return string.Format(culture, format, values);
}
diff --git a/SpineViewer/ViewModels/Exporters/BaseExporterViewModel.cs b/SpineViewer/ViewModels/Exporters/BaseExporterViewModel.cs
index 4baff6e..c078c48 100644
--- a/SpineViewer/ViewModels/Exporters/BaseExporterViewModel.cs
+++ b/SpineViewer/ViewModels/Exporters/BaseExporterViewModel.cs
@@ -5,6 +5,7 @@ using SFMLRenderer;
using Spine;
using Spine.Exporters;
using SpineViewer.Extensions;
+using SpineViewer.Models;
using SpineViewer.Resources;
using SpineViewer.ViewModels.MainWindow;
using System;
@@ -134,9 +135,21 @@ namespace SpineViewer.ViewModels.Exporters
return null;
}
- public RelayCommand Cmd_Export => _cmd_Export ??= new(Export_Execute, args => args is not null && args.Count > 0);
+ public RelayCommand Cmd_Export => _cmd_Export ??= new(Export_Execute, Export_CanExecute);
private RelayCommand? _cmd_Export;
- protected abstract void Export_Execute(IList? args);
+ private void Export_Execute(IList? args)
+ {
+ if (!Export_CanExecute(args)) return;
+ Export(args.Cast().ToArray());
+ // XXX: 导出途中应该停掉渲染好一些, 让性能专注在导出上
+ }
+
+ private bool Export_CanExecute(IList? args)
+ {
+ return args is not null && args.Count > 0;
+ }
+
+ protected abstract void Export(SpineObjectModel[] models);
}
}
diff --git a/SpineViewer/ViewModels/Exporters/CustomFFmpegExporterViewModel.cs b/SpineViewer/ViewModels/Exporters/CustomFFmpegExporterViewModel.cs
index 085d426..2b5caa9 100644
--- a/SpineViewer/ViewModels/Exporters/CustomFFmpegExporterViewModel.cs
+++ b/SpineViewer/ViewModels/Exporters/CustomFFmpegExporterViewModel.cs
@@ -47,11 +47,10 @@ namespace SpineViewer.ViewModels.Exporters
return null;
}
- protected override void Export_Execute(IList? args)
+ protected override void Export(SpineObjectModel[] models)
{
- if (args is null || args.Count <= 0) return;
if (!DialogService.ShowCustomFFmpegExporterDialog(this)) return;
- SpineObject[] spines = args.Cast().Select(m => m.GetSpineObject()).ToArray();
+ SpineObject[] spines = models.Select(m => m.GetSpineObject()).ToArray();
ProgressService.RunAsync((pr, ct) => ExportTask(spines, pr, ct), AppResource.Str_CustomFFmpegExporterTitle);
foreach (var sp in spines) sp.Dispose();
}
diff --git a/SpineViewer/ViewModels/Exporters/FFmpegVideoExporterViewModel.cs b/SpineViewer/ViewModels/Exporters/FFmpegVideoExporterViewModel.cs
index 3ed8111..d206531 100644
--- a/SpineViewer/ViewModels/Exporters/FFmpegVideoExporterViewModel.cs
+++ b/SpineViewer/ViewModels/Exporters/FFmpegVideoExporterViewModel.cs
@@ -34,11 +34,10 @@ namespace SpineViewer.ViewModels.Exporters
private string FormatSuffix => $".{_format.ToString().ToLower()}";
- protected override void Export_Execute(IList? args)
+ protected override void Export(SpineObjectModel[] models)
{
- if (args is null || args.Count <= 0) return;
if (!DialogService.ShowFFmpegVideoExporterDialog(this)) return;
- SpineObject[] spines = args.Cast().Select(m => m.GetSpineObject()).ToArray();
+ SpineObject[] spines = models.Select(m => m.GetSpineObject()).ToArray();
ProgressService.RunAsync((pr, ct) => ExportTask(spines, pr, ct), AppResource.Str_FFmpegVideoExporterTitle);
foreach (var sp in spines) sp.Dispose();
}
diff --git a/SpineViewer/ViewModels/Exporters/FrameExporterViewModel.cs b/SpineViewer/ViewModels/Exporters/FrameExporterViewModel.cs
index 2f3d9aa..d0e752c 100644
--- a/SpineViewer/ViewModels/Exporters/FrameExporterViewModel.cs
+++ b/SpineViewer/ViewModels/Exporters/FrameExporterViewModel.cs
@@ -38,11 +38,10 @@ namespace SpineViewer.ViewModels.Exporters
}
}
- protected override void Export_Execute(IList? args)
+ protected override void Export(SpineObjectModel[] models)
{
- if (args is null || args.Count <= 0) return;
if (!DialogService.ShowFrameExporterDialog(this)) return;
- SpineObject[] spines = args.Cast().Select(m => m.GetSpineObject(true)).ToArray();
+ SpineObject[] spines = models.Select(m => m.GetSpineObject(true)).ToArray();
ProgressService.RunAsync((pr, ct) => ExportTask(spines, pr, ct), AppResource.Str_FrameExporterTitle);
foreach (var sp in spines) sp.Dispose();
}
diff --git a/SpineViewer/ViewModels/Exporters/FrameSequenceExporterViewModel.cs b/SpineViewer/ViewModels/Exporters/FrameSequenceExporterViewModel.cs
index 81483ac..add965f 100644
--- a/SpineViewer/ViewModels/Exporters/FrameSequenceExporterViewModel.cs
+++ b/SpineViewer/ViewModels/Exporters/FrameSequenceExporterViewModel.cs
@@ -17,11 +17,10 @@ namespace SpineViewer.ViewModels.Exporters
{
public class FrameSequenceExporterViewModel(MainWindowViewModel vmMain) : VideoExporterViewModel(vmMain)
{
- protected override void Export_Execute(IList? args)
+ protected override void Export(SpineObjectModel[] models)
{
- if (args is null || args.Count <= 0) return;
if (!DialogService.ShowFrameSequenceExporterDialog(this)) return;
- SpineObject[] spines = args.Cast().Select(m => m.GetSpineObject()).ToArray();
+ SpineObject[] spines = models.Select(m => m.GetSpineObject()).ToArray();
ProgressService.RunAsync((pr, ct) => ExportTask(spines, pr, ct), AppResource.Str_FrameSequenceExporterTitle);
foreach (var sp in spines) sp.Dispose();
}
diff --git a/SpineViewer/ViewModels/MainWindow/MainWindowViewModel.cs b/SpineViewer/ViewModels/MainWindow/MainWindowViewModel.cs
index 04f8b81..e5155ed 100644
--- a/SpineViewer/ViewModels/MainWindow/MainWindowViewModel.cs
+++ b/SpineViewer/ViewModels/MainWindow/MainWindowViewModel.cs
@@ -1,22 +1,10 @@
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
-using HandyControl.Controls;
using NLog;
using SFMLRenderer;
-using Spine;
-using Spine.Exporters;
-using SpineViewer.Extensions;
using SpineViewer.Models;
using SpineViewer.Services;
-using SpineViewer.ViewModels.Exporters;
-using System;
-using System.Collections;
-using System.Collections.ObjectModel;
-using System.Diagnostics;
-using System.IO;
-using System.Windows;
-using System.Windows.Controls;
-using System.Windows.Data;
+using SpineViewer.Utils;
using System.Windows.Shell;
namespace SpineViewer.ViewModels.MainWindow
@@ -84,6 +72,34 @@ namespace SpineViewer.ViewModels.MainWindow
public SFMLRendererViewModel SFMLRendererViewModel => _sfmlRendererViewModel;
private readonly SFMLRendererViewModel _sfmlRendererViewModel;
+ ///
+ /// 打开工作区
+ ///
+ public RelayCommand Cmd_OpenWorkspace => _cmd_OpenWorkspace ??= new(OpenWorkspace_Execute);
+ private RelayCommand? _cmd_OpenWorkspace;
+
+ private void OpenWorkspace_Execute()
+ {
+ if (!DialogService.ShowOpenJsonDialog(out var fileName)) return;
+ if (JsonHelper.Deserialize(fileName, out var obj))
+ {
+ Workspace = obj;
+ }
+ }
+
+ ///
+ /// 保存工作区
+ ///
+ public RelayCommand Cmd_SaveWorkspace => _cmd_SaveWorkspace ??= new(SaveWorkspace_Execute);
+ private RelayCommand? _cmd_SaveWorkspace;
+
+ private void SaveWorkspace_Execute()
+ {
+ string fileName = "workspace.jcfg";
+ if (!DialogService.ShowSaveJsonDialog(ref fileName)) return;
+ JsonHelper.Serialize(Workspace, fileName);
+ }
+
///
/// 显示诊断信息对话框
///
@@ -96,6 +112,23 @@ namespace SpineViewer.ViewModels.MainWindow
public RelayCommand Cmd_ShowAboutDialog => _cmd_ShowAboutDialog ??= new(() => { DialogService.ShowAboutDialog(); });
private RelayCommand? _cmd_ShowAboutDialog;
+ public WorkspaceModel Workspace
+ {
+ get
+ {
+ return new()
+ {
+ RendererConfig = _sfmlRendererViewModel.WorkspaceConfig,
+ LoadedSpineObjects = _spineObjectListViewModel.LoadedSpineObjects
+ };
+ }
+ set
+ {
+ _sfmlRendererViewModel.WorkspaceConfig = value.RendererConfig;
+ _spineObjectListViewModel.LoadedSpineObjects = value.LoadedSpineObjects;
+ }
+ }
+
///
/// 调试命令
///
diff --git a/SpineViewer/ViewModels/MainWindow/PreferenceViewModel.cs b/SpineViewer/ViewModels/MainWindow/PreferenceViewModel.cs
index 257662b..87377ba 100644
--- a/SpineViewer/ViewModels/MainWindow/PreferenceViewModel.cs
+++ b/SpineViewer/ViewModels/MainWindow/PreferenceViewModel.cs
@@ -4,6 +4,7 @@ using NLog;
using Spine.SpineWrappers;
using SpineViewer.Models;
using SpineViewer.Services;
+using SpineViewer.Utils;
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
@@ -49,15 +50,7 @@ namespace SpineViewer.ViewModels.MainWindow
private static void SavePreference(PreferenceModel m)
{
- try
- {
- m.Serialize(PreferenceFilePath);
- }
- catch (Exception ex)
- {
- _logger.Error("Failed to save preference to {0}, {1}", PreferenceFilePath, ex.Message);
- _logger.Trace(ex.ToString());
- }
+ JsonHelper.Serialize(m, PreferenceFilePath);
}
///
@@ -70,18 +63,8 @@ namespace SpineViewer.ViewModels.MainWindow
///
public void LoadPreference()
{
- if (!File.Exists(PreferenceFilePath)) return;
-
- try
- {
- var m = PreferenceModel.Deserialize(PreferenceFilePath);
- Preference = m;
- }
- catch (Exception ex)
- {
- _logger.Error("Failed to load preference from {0}, {1}", PreferenceFilePath, ex.Message);
- _logger.Trace(ex.ToString());
- }
+ if (JsonHelper.Deserialize(PreferenceFilePath, out var obj))
+ Preference = obj;
}
///
diff --git a/SpineViewer/ViewModels/MainWindow/SFMLRendererViewModel.cs b/SpineViewer/ViewModels/MainWindow/SFMLRendererViewModel.cs
index 7125d4d..54a7d60 100644
--- a/SpineViewer/ViewModels/MainWindow/SFMLRendererViewModel.cs
+++ b/SpineViewer/ViewModels/MainWindow/SFMLRendererViewModel.cs
@@ -7,6 +7,7 @@ using SpineViewer.Extensions;
using SpineViewer.Models;
using SpineViewer.Resources;
using SpineViewer.Services;
+using SpineViewer.Utils;
using System;
using System.Collections.Generic;
using System.Collections.Specialized;
@@ -423,5 +424,41 @@ namespace SpineViewer.ViewModels.MainWindow
_renderer.SetActive(false);
}
}
+
+ public RendererWorkspaceConfigModel WorkspaceConfig
+ {
+ // TODO: 背景图片
+ get
+ {
+ return new()
+ {
+ ResolutionX = ResolutionX,
+ ResolutionY = ResolutionY,
+ CenterX = CenterX,
+ CenterY = CenterY,
+ Zoom = Zoom,
+ Rotation = Rotation,
+ FlipX = FlipX,
+ FlipY = FlipY,
+ MaxFps = MaxFps,
+ ShowAxis = ShowAxis,
+ BackgroundColor = BackgroundColor,
+ };
+ }
+ set
+ {
+ ResolutionX = value.ResolutionX;
+ ResolutionY = value.ResolutionY;
+ CenterX = value.CenterX;
+ CenterY = value.CenterY;
+ Zoom = value.Zoom;
+ Rotation = value.Rotation;
+ FlipX = value.FlipX;
+ FlipY = value.FlipY;
+ MaxFps = value.MaxFps;
+ ShowAxis = value.ShowAxis;
+ BackgroundColor = value.BackgroundColor;
+ }
+ }
}
}
diff --git a/SpineViewer/ViewModels/MainWindow/SpineObjectListViewModel.cs b/SpineViewer/ViewModels/MainWindow/SpineObjectListViewModel.cs
index 135b850..d058952 100644
--- a/SpineViewer/ViewModels/MainWindow/SpineObjectListViewModel.cs
+++ b/SpineViewer/ViewModels/MainWindow/SpineObjectListViewModel.cs
@@ -6,6 +6,7 @@ using SpineViewer.Extensions;
using SpineViewer.Models;
using SpineViewer.Resources;
using SpineViewer.Services;
+using SpineViewer.Utils;
using SpineViewer.ViewModels.Exporters;
using System;
using System.Collections;
@@ -100,7 +101,7 @@ namespace SpineViewer.ViewModels.MainWindow
private void AddSpineObject_Execute()
{
- throw new NotImplementedException();
+ MessagePopupService.Info("Not Implemented, try next version :)");
}
///
@@ -170,7 +171,7 @@ namespace SpineViewer.ViewModels.MainWindow
try
{
var spNew = new SpineObjectModel(sp.SkelPath, sp.AtlasPath);
- spNew.Load(sp.Dump());
+ spNew.ObjectConfig = sp.ObjectConfig;
_spineObjectModels[idx] = spNew;
sp.Dispose();
}
@@ -224,7 +225,7 @@ namespace SpineViewer.ViewModels.MainWindow
try
{
var spNew = new SpineObjectModel(sp.SkelPath, sp.AtlasPath);
- spNew.Load(sp.Dump());
+ spNew.ObjectConfig = sp.ObjectConfig;
_spineObjectModels[idx] = spNew;
sp.Dispose();
success++;
@@ -312,7 +313,7 @@ namespace SpineViewer.ViewModels.MainWindow
{
if (!CopySpineObjectConfig_CanExecute(args)) return;
var sp = (SpineObjectModel)args[0];
- _copiedSpineObjectConfigModel = sp.Dump();
+ _copiedSpineObjectConfigModel = sp.ObjectConfig;
_logger.Info("Copy config from model: {0}", sp.Name);
}
@@ -334,7 +335,7 @@ namespace SpineViewer.ViewModels.MainWindow
if (!ApplySpineObjectConfig_CanExecute(args)) return;
foreach (SpineObjectModel sp in args)
{
- sp.Load(_copiedSpineObjectConfigModel);
+ sp.ObjectConfig = _copiedSpineObjectConfigModel;
_logger.Info("Apply config to model: {0}", sp.Name);
}
}
@@ -353,22 +354,15 @@ namespace SpineViewer.ViewModels.MainWindow
private void ApplySpineObjectConfigFromFile_Execute(IList? args)
{
if (!ApplySpineObjectConfigFromFile_CanExecute(args)) return;
- if (!DialogService.ShowOpenFileDialog(out var fileName, filter: "Json Config|*.jcfg|All|*.*")) return;
- try
+ if (!DialogService.ShowOpenJsonDialog(out var fileName)) return;
+ if (JsonHelper.Deserialize(fileName, out var config))
{
- var config = SpineObjectConfigModel.Deserialize(fileName);
foreach (SpineObjectModel sp in args)
{
- sp.Load(config);
+ sp.ObjectConfig = config;
_logger.Info("Apply config to model: {0}", sp.Name);
}
}
- catch (Exception ex)
- {
- _logger.Error("Failed to apply config file {0}, {1}", fileName, ex.Message);
- _logger.Trace(ex.ToString());
- return;
- }
}
private bool ApplySpineObjectConfigFromFile_CanExecute(IList? args)
@@ -385,27 +379,11 @@ namespace SpineViewer.ViewModels.MainWindow
{
if (!SaveSpineObjectConfigToFile_CanExecute(args)) return;
var sp = (SpineObjectModel)args[0];
- var config = sp.Dump();
+ var config = sp.ObjectConfig;
string fileName = $"{Path.ChangeExtension(Path.GetFileName(sp.SkelPath), ".jcfg")}";
- if (!DialogService.ShowSaveFileDialog(
- ref fileName,
- initialDirectory: sp.AssetsDir,
- defaultExt: ".jcfg",
- filter:"Json Config|*.jcfg|All|*.*")
- )
- return;
- try
- {
- sp.Dump().Serialize(fileName);
- _logger.Info("{0} config save to {1}", sp.Name, fileName);
- }
- catch (Exception ex)
- {
- _logger.Error("Failed to save config file {0}, {1}", fileName, ex.Message);
- _logger.Trace(ex.ToString());
- return;
- }
+ if (!DialogService.ShowSaveJsonDialog(ref fileName, sp.AssetsDir)) return;
+ JsonHelper.Serialize(sp.ObjectConfig, fileName);
}
private bool SaveSpineObjectConfigToFile_CanExecute(IList? args)
@@ -505,7 +483,7 @@ namespace SpineViewer.ViewModels.MainWindow
/// 安全地在末尾添加一个模型, 发生错误会输出日志
///
/// 是否添加成功
- public bool AddSpineObject(string skelPath, string? atlasPath = null)
+ private bool AddSpineObject(string skelPath, string? atlasPath = null)
{
try
{
@@ -520,5 +498,112 @@ namespace SpineViewer.ViewModels.MainWindow
}
return false;
}
+
+ private bool AddSpineObject(SpineObjectWorkspaceConfigModel cfg)
+ {
+ try
+ {
+ var sp = new SpineObjectModel(cfg);
+ lock (_spineObjectModels.Lock) _spineObjectModels.Add(sp);
+ return true;
+ }
+ catch (Exception ex)
+ {
+ _logger.Trace(ex.ToString());
+ _logger.Error("Failed to load: {0}, {1}", cfg.SkelPath, ex.Message);
+ }
+ return false;
+ }
+
+ public List LoadedSpineObjects
+ {
+ get
+ {
+ List loadedSpineObjects = [];
+ lock (_spineObjectModels.Lock)
+ {
+ foreach (var sp in _spineObjectModels)
+ {
+ loadedSpineObjects.Add(sp.WorkspaceConfig);
+ }
+ }
+ return loadedSpineObjects;
+ }
+ set
+ {
+ AddSpineObjectFromWorkspaceList(value);
+ }
+ }
+
+ private void AddSpineObjectFromWorkspaceList(List models)
+ {
+ lock (_spineObjectModels.Lock)
+ {
+ var spines = _spineObjectModels.ToArray();
+ _spineObjectModels.Clear();
+ foreach (var sp in spines)
+ {
+ sp.Dispose();
+ }
+ }
+
+ if (models.Count > 1)
+ {
+ ProgressService.RunAsync((pr, ct) => AddSpineObjectFromWorkspaceListTask(
+ models, pr, ct),
+ AppResource.Str_AddSpineObjectsTitle
+ );
+ }
+ else if (models.Count > 0)
+ {
+ AddSpineObject(models[0]);
+ _logger.LogCurrentProcessMemoryUsage();
+ }
+ }
+
+ private void AddSpineObjectFromWorkspaceListTask(List models, IProgressReporter reporter, CancellationToken ct)
+ {
+ int totalCount = models.Count;
+ int success = 0;
+ int error = 0;
+
+ _vmMain.ProgressState = TaskbarItemProgressState.Normal;
+ _vmMain.ProgressValue = 0;
+
+ reporter.Total = totalCount;
+ reporter.Done = 0;
+ reporter.ProgressText = $"[0/{totalCount}]";
+ for (int i = 0; i < totalCount; i++)
+ {
+ if (ct.IsCancellationRequested) break;
+
+ var cfg = models[i];
+ reporter.ProgressText = $"[{i}/{totalCount}] {cfg}";
+
+ if (AddSpineObject(cfg))
+ success++;
+ else
+ error++;
+
+ reporter.Done = i + 1;
+ reporter.ProgressText = $"[{i + 1}/{totalCount}] {cfg}";
+ _vmMain.ProgressValue = (i + 1f) / totalCount;
+ }
+ _vmMain.ProgressState = TaskbarItemProgressState.None;
+
+ if (error > 0)
+ _logger.Warn("Batch load {0} successfully, {1} failed", success, error);
+ else
+ _logger.Info("{0} skel loaded successfully", success);
+
+ _logger.LogCurrentProcessMemoryUsage();
+
+ // 从工作区加载需要同步一次时间轴
+ lock (_spineObjectModels.Lock)
+ {
+ foreach (var sp in _spineObjectModels)
+ sp.ResetAnimationsTime();
+ }
+ }
}
}
diff --git a/SpineViewer/ViewModels/MainWindow/SpineObjectTabViewModel.cs b/SpineViewer/ViewModels/MainWindow/SpineObjectTabViewModel.cs
index 6b24919..19fb81a 100644
--- a/SpineViewer/ViewModels/MainWindow/SpineObjectTabViewModel.cs
+++ b/SpineViewer/ViewModels/MainWindow/SpineObjectTabViewModel.cs
@@ -786,7 +786,7 @@ namespace SpineViewer.ViewModels.MainWindow
{
get
{
- /// XXX: 空轨道和多选不相同都会返回 null
+ // XXX: 空轨道和多选不相同都会返回 null
if (_spines.Length <= 0) return null;
var val = _spines[0].GetAnimation(_trackIndex);
if (_spines.Skip(1).Any(it => it.GetAnimation(_trackIndex) != val)) return null;
diff --git a/SpineViewer/Views/MainWindow.xaml b/SpineViewer/Views/MainWindow.xaml
index 3462723..b9db125 100644
--- a/SpineViewer/Views/MainWindow.xaml
+++ b/SpineViewer/Views/MainWindow.xaml
@@ -6,7 +6,7 @@
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:hc="https://handyorg.github.io/handycontrol"
xmlns:vm="clr-namespace:SpineViewer.ViewModels.MainWindow"
- xmlns:ext="clr-namespace:SpineViewer.Extensions"
+ xmlns:utils="clr-namespace:SpineViewer.Utils"
xmlns:SFMLRenderer="clr-namespace:SFMLRenderer;assembly=SFMLRenderer"
mc:Ignorable="d"
Title="{Binding Title}"
@@ -35,7 +35,7 @@
-
+
@@ -56,7 +56,8 @@