更换为wpf

This commit is contained in:
ww-rm
2025-05-27 15:55:10 +08:00
parent d0f629d9ba
commit 17257a0ffe
316 changed files with 19206 additions and 76537 deletions

View File

@@ -0,0 +1,88 @@
using Spine.SpineWrappers;
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.Text.Json.Nodes;
using System.Threading.Tasks;
namespace SpineViewer.Models
{
public class SpineObjectConfigModel
{
/// <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);
}
public bool UsePma { get; set; }
public string Physics { get; set; } = ISkeleton.Physics.Update.ToString();
public float Scale { get; set; } = 1f;
public bool FlipX { get; set; }
public bool FlipY { get; set; }
public float X { get; set; }
public float Y { get; set; }
public List<string> LoadedSkins { get; set; } = [];
public Dictionary<string, string?> SlotAttachment { get; set; } = [];
public List<string?> Animations { get; set; } = [];
public bool DebugTexture { get; set; } = true;
public bool DebugBounds { get; set; } = true;
public bool DebugBones { get; set; }
public bool DebugRegions { get; set; }
public bool DebugMeshHulls { get; set; }
public bool DebugMeshes { get; set; }
public bool DebugBoundingBoxes { get; set; }
public bool DebugPaths { get; set; }
public bool DebugPoints { get; set; }
public bool DebugClippings { get; set; }
}
}

View File

@@ -0,0 +1,478 @@
using System;
using System.Collections.Frozen;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.IO;
using System.Linq;
using System.Text;
using System.Text.Encodings.Web;
using System.Text.Json;
using System.Text.Json.Nodes;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using CommunityToolkit.Mvvm.ComponentModel;
using NLog;
using Spine;
using Spine.SpineWrappers;
using SpineViewer.Extensions;
namespace SpineViewer.Models
{
/// <summary>
/// 线程安全的模型对象类
/// </summary>
public class SpineObjectModel : ObservableObject, SFML.Graphics.Drawable, IDisposable
{
/// <summary>
/// 日志器
/// </summary>
private static readonly Logger _logger = LogManager.GetCurrentClassLogger();
private readonly object _lock = new();
private readonly SpineObject _spineObject;
private readonly ImmutableArray<string> _skins;
private readonly FrozenDictionary<string, ImmutableArray<string>> _slotAttachments;
private readonly ImmutableArray<string> _animations;
/// <summary>
/// 构造函数, 可能会抛出异常
/// </summary>
public SpineObjectModel(string skelPath, string? atlasPath = null, SpineVersion? version = null)
{
_spineObject = new(skelPath, atlasPath, version) { DebugNonTexture = false };
_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();
// 默认自带一个动画
if (_spineObject.Data.Animations.Length > 0)
_spineObject.AnimationState.SetAnimation(0, _spineObject.Data.Animations[0], true);
}
public event EventHandler<SkinStatusChangedEventArgs>? SkinStatusChanged;
public event EventHandler<SlotAttachmentChangedEventArgs>? SlotAttachmentChanged;
public event EventHandler<AnimationChangedEventArgs>? AnimationChanged;
public SpineVersion Version => _spineObject.Version;
public string AssetsDir => _spineObject.AssetsDir;
public string SkelPath => _spineObject.SkelPath;
public string AtlasPath => _spineObject.AtlasPath;
public string Name => _spineObject.Name;
public string FileVersion => _spineObject.Data.SkeletonVersion;
public bool IsSelected
{
get { lock (_lock) return _isSelected; }
set { lock (_lock) if (SetProperty(ref _isSelected, value)) _spineObject.DebugNonTexture = _isSelected; }
}
private bool _isSelected = false;
public bool IsShown
{
get { lock (_lock) return _isShown; }
set { lock (_lock) SetProperty(ref _isShown, value); }
}
private bool _isShown = true;
public bool UsePma
{
get { lock (_lock) return _spineObject.UsePma; }
set { lock (_lock) SetProperty(_spineObject.UsePma, value, _spineObject, (m, v) => m.UsePma = v); }
}
public ISkeleton.Physics Physics
{
get { lock (_lock) return _spineObject.Physics; }
set { lock (_lock) SetProperty(_spineObject.Physics, value, _spineObject, (m, v) => m.Physics = v); }
}
/// <summary>
/// 缩放倍数, 绝对值大小, 两个方向大小不一致时返回 -1, 设置时不会影响正负号
/// </summary>
public float Scale
{
get
{
lock (_lock)
{
var x = Math.Abs(_spineObject.Skeleton.ScaleX);
var y = Math.Abs(_spineObject.Skeleton.ScaleY);
return Math.Abs(x - y) < 1e-6 ? x : -1;
}
}
set
{
lock (_lock)
{
value = Math.Clamp(value, 0.001f, 1000f);
_spineObject.Skeleton.ScaleX = value * Math.Sign(_spineObject.Skeleton.ScaleX);
_spineObject.Skeleton.ScaleY = value * Math.Sign(_spineObject.Skeleton.ScaleY);
OnPropertyChanged();
}
}
}
/// <summary>
/// 水平翻转
/// </summary>
public bool FlipX
{
get { lock (_lock) return _spineObject.Skeleton.ScaleX < 0; }
set { lock (_lock) SetProperty(_spineObject.Skeleton.ScaleX < 0, value, _spineObject, (m, v) => m.Skeleton.ScaleX *= -1); }
}
/// <summary>
/// 垂直翻转
/// </summary>
public bool FlipY
{
get { lock (_lock) return _spineObject.Skeleton.ScaleY < 0; }
set { lock (_lock) SetProperty(_spineObject.Skeleton.ScaleY < 0, value, _spineObject, (m, v) => m.Skeleton.ScaleY *= -1); }
}
public float X
{
get { lock (_lock) return _spineObject.Skeleton.X; }
set { lock (_lock) SetProperty(_spineObject.Skeleton.X, value, _spineObject, (m, v) => m.Skeleton.X = v); }
}
public float Y
{
get { lock (_lock) return _spineObject.Skeleton.Y; }
set { lock (_lock) SetProperty(_spineObject.Skeleton.Y, value, _spineObject, (m, v) => m.Skeleton.Y = v); }
}
public ImmutableArray<string> Skins => _skins;
public bool GetSkinStatus(string skinName)
{
lock (_lock) return _spineObject.GetSkinStatus(skinName);
}
public bool SetSkinStatus(string skinName, bool status)
{
bool changed = false;
lock (_lock) changed = _spineObject.SetSkinStatus(skinName, status);
if (changed) SkinStatusChanged?.Invoke(this, new(skinName, status));
return changed;
}
public FrozenDictionary<string, ImmutableArray<string>> SlotAttachments => _slotAttachments;
public string? GetAttachment(string slotName)
{
lock (_lock) return _spineObject.GetAttachment(slotName);
}
public bool SetAttachment(string slotName, string? attachmentName)
{
bool changed = false;
lock (_lock) changed = _spineObject.SetAttachment(slotName, attachmentName);
if (changed) SlotAttachmentChanged?.Invoke(this, new(slotName, attachmentName));
return changed;
}
public ImmutableArray<string> Animations => _animations;
public float GetAnimationDuration(string name)
{
if (_spineObject.Data.AnimationsByName.TryGetValue(name, out var ani))
return ani.Duration;
return 0f;
}
public string? GetAnimation(int index)
{
lock (_lock) return _spineObject.AnimationState.GetCurrent(index)?.Animation.Name;
}
/// <summary>
/// 在指定轨道上设置循环动画, 会忽略不存在的动画
/// </summary>
public void SetAnimation(int index, string name)
{
bool changed = false;
lock (_lock)
{
if (_spineObject.Data.AnimationsByName.ContainsKey(name))
{
_spineObject.AnimationState.SetAnimation(index, name, true);
changed = true;
}
}
if (changed) AnimationChanged?.Invoke(this, new(index, name));
}
public int[] GetTrackIndices()
{
lock (_lock)
{
List<int> indices = [];
int idx = 0;
foreach (var e in _spineObject.AnimationState.IterTracks())
{
if (e is not null) indices.Add(idx);
idx++;
}
return indices.ToArray();
}
}
public void ClearTrack(int index)
{
lock (_lock) _spineObject.AnimationState.ClearTrack(index);
AnimationChanged?.Invoke(this, new(index, null));
}
public void ResetAnimationsTime()
{
lock (_lock) _spineObject.ResetAnimationsTime();
}
public bool EnableDebug
{
get { lock (_lock) return _spineObject.EnableDebug; }
set { lock (_lock) SetProperty(_spineObject.EnableDebug, value, _spineObject, (m, v) => m.EnableDebug = v); }
}
public bool DebugTexture
{
get { lock (_lock) return _spineObject.DebugTexture; }
set { lock (_lock) SetProperty(_spineObject.DebugTexture, value, _spineObject, (m, v) => m.DebugTexture = v); }
}
public bool DebugBounds
{
get { lock (_lock) return _spineObject.DebugBounds; }
set { lock (_lock) SetProperty(_spineObject.DebugBounds, value, _spineObject, (m, v) => m.DebugBounds = v); }
}
public bool DebugBones
{
get { lock (_lock) return _spineObject.DebugBones; }
set { lock (_lock) SetProperty(_spineObject.DebugBones, value, _spineObject, (m, v) => m.DebugBones = v); }
}
public bool DebugRegions
{
get { lock (_lock) return _spineObject.DebugRegions; }
set { lock (_lock) SetProperty(_spineObject.DebugRegions, value, _spineObject, (m, v) => m.DebugRegions = v); }
}
public bool DebugMeshHulls
{
get { lock (_lock) return _spineObject.DebugMeshHulls; }
set { lock (_lock) SetProperty(_spineObject.DebugMeshHulls, value, _spineObject, (m, v) => m.DebugMeshHulls = v); }
}
public bool DebugMeshes
{
get { lock (_lock) return _spineObject.DebugMeshes; }
set { lock (_lock) SetProperty(_spineObject.DebugMeshes, value, _spineObject, (m, v) => m.DebugMeshes = v); }
}
public bool DebugBoundingBoxes
{
get { lock (_lock) return _spineObject.DebugBoundingBoxes; }
set { lock (_lock) SetProperty(_spineObject.DebugBoundingBoxes, value, _spineObject, (m, v) => m.DebugBoundingBoxes = v); }
}
public bool DebugPaths
{
get { lock (_lock) return _spineObject.DebugPaths; }
set { lock (_lock) SetProperty(_spineObject.DebugPaths, value, _spineObject, (m, v) => m.DebugPaths = v); }
}
public bool DebugPoints
{
get { lock (_lock) return _spineObject.DebugPoints; }
set { lock (_lock) SetProperty(_spineObject.DebugPoints, value, _spineObject, (m, v) => m.DebugPoints = v); }
}
public bool DebugClippings
{
get { lock (_lock) return _spineObject.DebugClippings; }
set { lock (_lock) SetProperty(_spineObject.DebugClippings, value, _spineObject, (m, v) => m.DebugClippings = v); }
}
public void Update(float delta)
{
lock (_lock) _spineObject.Update(delta);
}
/// <summary>
/// 获取一个独立的内部对象, 继承所有状态
/// </summary>
public SpineObject GetSpineObject(bool keepTrackTime = false)
{
lock (_lock) return _spineObject.Copy(keepTrackTime);
}
/// <summary>
/// 获取当前状态包围盒
/// </summary>
public Rect GetCurrentBounds()
{
lock (_lock) return _spineObject.GetCurrentBounds();
}
/// <summary>
/// 导出参数对象
/// </summary>
public SpineObjectConfigModel Dump()
{
lock (_lock)
{
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,
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
};
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;
// XXX: 处理空动画
config.Animations.AddRange(_spineObject.AnimationState.IterTracks().Select(tr => tr?.Animation.Name));
return config;
}
}
/// <summary>
/// 从参数对象加载参数值
/// </summary>
public void Load(SpineObjectConfigModel config)
{
lock (_lock)
{
_spineObject.Skeleton.ScaleX = config.Scale;
_spineObject.Skeleton.ScaleY = config.Scale;
OnPropertyChanged(nameof(Scale));
SetProperty(_spineObject.Skeleton.ScaleX < 0, config.FlipX, _spineObject, (m, v) => m.Skeleton.ScaleX *= -1, nameof(FlipX));
SetProperty(_spineObject.Skeleton.ScaleY < 0, config.FlipY, _spineObject, (m, v) => m.Skeleton.ScaleY *= -1, nameof(FlipY));
SetProperty(_spineObject.Skeleton.X, config.X, _spineObject, (m, v) => m.Skeleton.X = v, nameof(X));
SetProperty(_spineObject.Skeleton.Y, config.Y, _spineObject, (m, v) => m.Skeleton.Y = v, nameof(Y));
SetProperty(_spineObject.UsePma, config.UsePma, _spineObject, (m, v) => m.UsePma = v, nameof(UsePma));
SetProperty(_spineObject.Physics, Enum.Parse<ISkeleton.Physics>(config.Physics ?? "Update", true), _spineObject, (m, v) => m.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)
{
if (!string.IsNullOrEmpty(name))
_spineObject.AnimationState.SetAnimation(trackIndex, name, true);
AnimationChanged?.Invoke(this, new(trackIndex, name));
trackIndex++;
}
SetProperty(_spineObject.DebugTexture, config.DebugTexture, _spineObject, (m, v) => m.DebugTexture = v, nameof(DebugTexture));
SetProperty(_spineObject.DebugBounds, config.DebugBounds, _spineObject, (m, v) => m.DebugBounds = v, nameof(DebugBounds));
SetProperty(_spineObject.DebugBones, config.DebugBones, _spineObject, (m, v) => m.DebugBones = v, nameof(DebugBones));
SetProperty(_spineObject.DebugRegions, config.DebugRegions, _spineObject, (m, v) => m.DebugRegions = v, nameof(DebugRegions));
SetProperty(_spineObject.DebugMeshHulls, config.DebugMeshHulls, _spineObject, (m, v) => m.DebugMeshHulls = v, nameof(DebugMeshHulls));
SetProperty(_spineObject.DebugMeshes, config.DebugMeshes, _spineObject, (m, v) => m.DebugMeshes = v, nameof(DebugMeshes));
SetProperty(_spineObject.DebugBoundingBoxes, config.DebugBoundingBoxes, _spineObject, (m, v) => m.DebugBoundingBoxes = v, nameof(DebugBoundingBoxes));
SetProperty(_spineObject.DebugPaths, config.DebugPaths, _spineObject, (m, v) => m.DebugPaths = v, nameof(DebugPaths));
SetProperty(_spineObject.DebugPoints, config.DebugPoints, _spineObject, (m, v) => m.DebugPoints = v, nameof(DebugPoints));
SetProperty(_spineObject.DebugClippings, config.DebugClippings, _spineObject, (m, v) => m.DebugClippings = v, nameof(DebugClippings));
}
}
#region SFML.Graphics.Drawable
public void Draw(SFML.Graphics.RenderTarget target, SFML.Graphics.RenderStates states)
{
lock (_lock) _spineObject.Draw(target, states);
}
#endregion
#region IDisposable
private bool _disposed = false;
protected virtual void Dispose(bool disposing)
{
if (_disposed) return;
if (disposing)
{
_spineObject.Dispose();
}
_disposed = true;
}
~SpineObjectModel()
{
Dispose(false);
}
public void Dispose()
{
Dispose(true);
if (_disposed)
{
GC.SuppressFinalize(this);
}
}
#endregion
}
public class SkinStatusChangedEventArgs(string name, bool status) : EventArgs
{
public string Name { get; } = name;
public bool Status { get; } = status;
}
public class SlotAttachmentChangedEventArgs(string slotName, string? attachmentName) : EventArgs
{
public string SlotName { get; } = slotName;
public string? AttachmentName { get; } = attachmentName;
}
public class AnimationChangedEventArgs(int trackIndex, string? animationName) : EventArgs
{
public int TrackIndex { get; } = trackIndex;
public string? AnimationName { get; } = animationName;
}
}