增加工作区功能
This commit is contained in:
@@ -80,41 +80,5 @@ namespace SpineViewer.Models
|
||||
private AppLanguage _appLanguage;
|
||||
|
||||
#endregion
|
||||
|
||||
#region 序列化与反序列
|
||||
|
||||
/// <summary>
|
||||
/// 保存 Json 文件的格式参数
|
||||
/// </summary>
|
||||
private static readonly JsonSerializerOptions _jsonOptions = new()
|
||||
{
|
||||
WriteIndented = true,
|
||||
Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping,
|
||||
AllowTrailingCommas = true,
|
||||
ReadCommentHandling = JsonCommentHandling.Skip
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// 从文件反序列对象, 可能抛出异常
|
||||
/// </summary>
|
||||
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<PreferenceModel>(json, _jsonOptions);
|
||||
return model ?? throw new JsonException($"null data in file '{path}'");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 保存至文件, 可能抛出异常
|
||||
/// </summary>
|
||||
public void Serialize(string path)
|
||||
{
|
||||
Directory.CreateDirectory(Path.GetDirectoryName(path));
|
||||
var json = JsonSerializer.Serialize(this, _jsonOptions);
|
||||
File.WriteAllText(path, json, Encoding.UTF8);
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 序列化与反序列
|
||||
|
||||
/// <summary>
|
||||
/// 保存 Json 文件的格式参数
|
||||
/// </summary>
|
||||
private static readonly JsonSerializerOptions _jsonOptions = new()
|
||||
{
|
||||
WriteIndented = true,
|
||||
Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping,
|
||||
AllowTrailingCommas = true,
|
||||
ReadCommentHandling = JsonCommentHandling.Skip
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// 从文件反序列对象, 可能抛出异常
|
||||
/// </summary>
|
||||
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<SpineObjectConfigModel>(json, _jsonOptions);
|
||||
return model ?? throw new JsonException($"null data in file '{path}'");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 保存至文件, 可能抛出异常
|
||||
/// </summary>
|
||||
public void Serialize(string path)
|
||||
{
|
||||
Directory.CreateDirectory(Path.GetDirectoryName(path));
|
||||
var json = JsonSerializer.Serialize(this, _jsonOptions);
|
||||
File.WriteAllText(path, json, Encoding.UTF8);
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 从工作区配置进行构造
|
||||
/// </summary>
|
||||
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<SkinStatusChangedEventArgs>? SkinStatusChanged;
|
||||
|
||||
public event EventHandler<SlotAttachmentChangedEventArgs>? SlotAttachmentChanged;
|
||||
@@ -345,99 +357,107 @@ namespace SpineViewer.Models
|
||||
lock (_lock) return _spineObject.GetCurrentBounds();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 导出参数对象
|
||||
/// </summary>
|
||||
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<ISkeleton.Physics>(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));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 从参数对象加载参数值
|
||||
/// </summary>
|
||||
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<ISkeleton.Physics>(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
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
57
SpineViewer/Models/WorkspaceModel.cs
Normal file
57
SpineViewer/Models/WorkspaceModel.cs
Normal file
@@ -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<SpineObjectWorkspaceConfigModel> 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();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -11,6 +11,8 @@
|
||||
<s:String x:Key="Str_Experiment">Experimental Features</s:String>
|
||||
|
||||
<s:String x:Key="Str_Open">Open...</s:String>
|
||||
<s:String x:Key="Str_OpenWorkspace">Open Workspace...</s:String>
|
||||
<s:String x:Key="Str_SaveWorkspace">Save Workspace...</s:String>
|
||||
<s:String x:Key="Str_Preference">Preferences</s:String>
|
||||
<s:String x:Key="Str_PreferenceWithDots">Preferences...</s:String>
|
||||
<s:String x:Key="Str_Exit">Exit</s:String>
|
||||
|
||||
@@ -11,6 +11,8 @@
|
||||
<s:String x:Key="Str_Experiment">実験機能</s:String>
|
||||
|
||||
<s:String x:Key="Str_Open">開く...</s:String>
|
||||
<s:String x:Key="Str_OpenWorkspace">ワークスペースを開く...</s:String>
|
||||
<s:String x:Key="Str_SaveWorkspace">ワークスペースを保存...</s:String>
|
||||
<s:String x:Key="Str_Preference">設定</s:String>
|
||||
<s:String x:Key="Str_PreferenceWithDots">設定...</s:String>
|
||||
<s:String x:Key="Str_Exit">終了</s:String>
|
||||
|
||||
@@ -11,6 +11,8 @@
|
||||
<s:String x:Key="Str_Experiment">实验性功能</s:String>
|
||||
|
||||
<s:String x:Key="Str_Open">打开...</s:String>
|
||||
<s:String x:Key="Str_OpenWorkspace">打开工作区...</s:String>
|
||||
<s:String x:Key="Str_SaveWorkspace">保存工作区...</s:String>
|
||||
<s:String x:Key="Str_Preference">首选项</s:String>
|
||||
<s:String x:Key="Str_PreferenceWithDots">首选项...</s:String>
|
||||
<s:String x:Key="Str_Exit">退出</s:String>
|
||||
|
||||
@@ -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()
|
||||
{
|
||||
FileName = fileName,
|
||||
InitialDirectory = initialDirectory,
|
||||
DefaultExt = defaultExt,
|
||||
Filter = filter,
|
||||
DefaultExt = ".jcfg",
|
||||
Filter = "Json|*.jcfg;*.json|All|*.*",
|
||||
};
|
||||
if (dialog.ShowDialog() is true)
|
||||
{
|
||||
|
||||
88
SpineViewer/Utils/JsonHelper.cs
Normal file
88
SpineViewer/Utils/JsonHelper.cs
Normal file
@@ -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();
|
||||
|
||||
/// <summary>
|
||||
/// 保存 Json 文件的格式参数
|
||||
/// </summary>
|
||||
public static JsonSerializerOptions JsonOptions => _jsonOptions;
|
||||
private static readonly JsonSerializerOptions _jsonOptions = new()
|
||||
{
|
||||
WriteIndented = true,
|
||||
Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping,
|
||||
AllowTrailingCommas = true,
|
||||
ReadCommentHandling = JsonCommentHandling.Skip
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// 从文件反序列对象, 不会抛出异常
|
||||
/// </summary>
|
||||
public static bool Deserialize<T>(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<T>(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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 保存至文件, 不会抛出异常
|
||||
/// </summary>
|
||||
public static bool Serialize<T>(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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -6,7 +6,7 @@ using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows.Data;
|
||||
|
||||
namespace SpineViewer.Extensions
|
||||
namespace SpineViewer.Utils
|
||||
{
|
||||
public class ObservableCollectionWithLock<T> : ObservableCollection<T>
|
||||
{
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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<IList?> Cmd_Export => _cmd_Export ??= new(Export_Execute, args => args is not null && args.Count > 0);
|
||||
public RelayCommand<IList?> Cmd_Export => _cmd_Export ??= new(Export_Execute, Export_CanExecute);
|
||||
private RelayCommand<IList?>? _cmd_Export;
|
||||
|
||||
protected abstract void Export_Execute(IList? args);
|
||||
private void Export_Execute(IList? args)
|
||||
{
|
||||
if (!Export_CanExecute(args)) return;
|
||||
Export(args.Cast<SpineObjectModel>().ToArray());
|
||||
// XXX: 导出途中应该停掉渲染好一些, 让性能专注在导出上
|
||||
}
|
||||
|
||||
private bool Export_CanExecute(IList? args)
|
||||
{
|
||||
return args is not null && args.Count > 0;
|
||||
}
|
||||
|
||||
protected abstract void Export(SpineObjectModel[] models);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<SpineObjectModel>().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();
|
||||
}
|
||||
|
||||
@@ -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<SpineObjectModel>().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();
|
||||
}
|
||||
|
||||
@@ -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<SpineObjectModel>().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();
|
||||
}
|
||||
|
||||
@@ -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<SpineObjectModel>().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();
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
/// <summary>
|
||||
/// 打开工作区
|
||||
/// </summary>
|
||||
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<WorkspaceModel>(fileName, out var obj))
|
||||
{
|
||||
Workspace = obj;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 保存工作区
|
||||
/// </summary>
|
||||
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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 显示诊断信息对话框
|
||||
/// </summary>
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 调试命令
|
||||
/// </summary>
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -70,18 +63,8 @@ namespace SpineViewer.ViewModels.MainWindow
|
||||
/// </summary>
|
||||
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<PreferenceModel>(PreferenceFilePath, out var obj))
|
||||
Preference = obj;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 :)");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -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<SpineObjectConfigModel>(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
|
||||
/// 安全地在末尾添加一个模型, 发生错误会输出日志
|
||||
/// </summary>
|
||||
/// <returns>是否添加成功</returns>
|
||||
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<SpineObjectWorkspaceConfigModel> LoadedSpineObjects
|
||||
{
|
||||
get
|
||||
{
|
||||
List<SpineObjectWorkspaceConfigModel> loadedSpineObjects = [];
|
||||
lock (_spineObjectModels.Lock)
|
||||
{
|
||||
foreach (var sp in _spineObjectModels)
|
||||
{
|
||||
loadedSpineObjects.Add(sp.WorkspaceConfig);
|
||||
}
|
||||
}
|
||||
return loadedSpineObjects;
|
||||
}
|
||||
set
|
||||
{
|
||||
AddSpineObjectFromWorkspaceList(value);
|
||||
}
|
||||
}
|
||||
|
||||
private void AddSpineObjectFromWorkspaceList(List<SpineObjectWorkspaceConfigModel> 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<SpineObjectWorkspaceConfigModel> 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 @@
|
||||
</Trigger>
|
||||
</Style.Triggers>
|
||||
</Style>
|
||||
<ext:StringFormatMultiValueConverter x:Key="StrFmtCvter"/>
|
||||
<utils:StringFormatMultiValueConverter x:Key="StrFmtCvter"/>
|
||||
</Window.Resources>
|
||||
|
||||
<Window.TaskbarItemInfo>
|
||||
@@ -56,7 +56,8 @@
|
||||
<!-- 菜单 -->
|
||||
<Menu x:Name="_mainMenu">
|
||||
<MenuItem Header="{DynamicResource Str_File}">
|
||||
<MenuItem Header="{DynamicResource Str_Open}" InputGestureText="Ctrl+O"/>
|
||||
<MenuItem Header="{DynamicResource Str_OpenWorkspace}" Command="{Binding Cmd_OpenWorkspace}"/>
|
||||
<MenuItem Header="{DynamicResource Str_SaveWorkspace}" Command="{Binding Cmd_SaveWorkspace}"/>
|
||||
<Separator/>
|
||||
<MenuItem Header="{DynamicResource Str_PreferenceWithDots}" Command="{Binding PreferenceViewModel.Cmd_ShowPreferenceDialog}"/>
|
||||
<!--<MenuItem Header="{DynamicResource Str_Exit}" InputGestureText="Alt+F4"/>-->
|
||||
@@ -244,7 +245,6 @@
|
||||
</i:Interaction.Triggers>
|
||||
|
||||
<ListView.InputBindings>
|
||||
<KeyBinding Gesture="Ctrl+O" Command="{Binding Cmd_AddSpineObject}"/>
|
||||
<KeyBinding Gesture="Delete" Command="{Binding Cmd_RemoveSpineObject}" CommandParameter="{Binding SelectedItems, ElementName=_spinesListView}"/>
|
||||
<KeyBinding Gesture="Ctrl+V" Command="{Binding Cmd_AddSpineObjectFromClipboard}"/>
|
||||
<KeyBinding Gesture="Ctrl+R" Command="{Binding Cmd_ReloadSpineObject}" CommandParameter="{Binding SelectedItems, ElementName=_spinesListView}"/>
|
||||
@@ -257,7 +257,6 @@
|
||||
<ListView.ContextMenu>
|
||||
<ContextMenu>
|
||||
<MenuItem Header="{DynamicResource Str_AddSpineObject}"
|
||||
InputGestureText="Ctrl+O"
|
||||
Command="{Binding Cmd_AddSpineObject}"/>
|
||||
<MenuItem Header="{DynamicResource Str_RemoveSpineObject}"
|
||||
InputGestureText="Delete"
|
||||
|
||||
Reference in New Issue
Block a user