Files
SpineViewer/SpineViewer/Models/SpineObjectModel.cs
2025-10-02 10:32:24 +08:00

669 lines
26 KiB
C#

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.Interfaces;
using SpineViewer.Extensions;
namespace SpineViewer.Models
{
/// <summary>
/// 线程安全的模型对象类
/// </summary>
public class SpineObjectModel : ObservableObject, SFML.Graphics.Drawable, IDisposable
{
/// <summary>
/// 一些加载默认选项
/// </summary>
public static SpineObjectLoadOptions LoadOptions => _loadOptions;
private static readonly SpineObjectLoadOptions _loadOptions = new();
/// <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)
{
_spineObject = new(skelPath, atlasPath)
{
UsePma = _loadOptions.UsePma,
DebugTexture = _loadOptions.DebugTexture,
DebugBounds = _loadOptions.DebugBounds,
DebugRegions = _loadOptions.DebugRegions,
DebugMeshHulls = _loadOptions.DebugMeshHulls,
DebugMeshes = _loadOptions.DebugMeshes,
DebugBoundingBoxes = _loadOptions.DebugBoundingBoxes,
DebugPaths = _loadOptions.DebugPaths,
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();
// 默认自带一个动画
if (_spineObject.Data.Animations.Length > 0)
_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<SlotVisibleChangedEventArgs>? SlotVisibleChanged;
public event EventHandler<SlotAttachmentChangedEventArgs>? SlotAttachmentChanged;
public event EventHandler<TrackPropertyChangedEventArgs>? TrackPropertyChanged;
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) SetProperty(ref _isSelected, value); }
}
private bool _isSelected = false;
public bool IsShown
{
get { lock (_lock) return _isShown; }
set { lock (_lock) SetProperty(ref _isShown, value); }
}
private bool _isShown = _loadOptions.IsShown;
public bool UsePma
{
get { lock (_lock) return _spineObject.UsePma; }
set { lock (_lock) SetProperty(_spineObject.UsePma, value, v => _spineObject.UsePma = v); }
}
public ISkeleton.Physics Physics
{
get { lock (_lock) return _spineObject.Physics; }
set { lock (_lock) SetProperty(_spineObject.Physics, value, v => _spineObject.Physics = v); }
}
public float TimeScale
{
get { lock (_lock) return _spineObject.AnimationState.TimeScale; }
set { lock (_lock) SetProperty(_spineObject.AnimationState.TimeScale, Math.Clamp(value, 0.01f, 100f), v => _spineObject.AnimationState.TimeScale = 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, v => _spineObject.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, v => _spineObject.Skeleton.ScaleY *= -1); }
}
public float X
{
get { lock (_lock) return _spineObject.Skeleton.X; }
set { lock (_lock) SetProperty(_spineObject.Skeleton.X, value, v => _spineObject.Skeleton.X = v); }
}
public float Y
{
get { lock (_lock) return _spineObject.Skeleton.Y; }
set { lock (_lock) SetProperty(_spineObject.Skeleton.Y, value, v => _spineObject.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 bool GetSlotVisible(string slotName)
{
lock (_lock) return _spineObject.GetSlotVisible(slotName);
}
public bool SetSlotVisible(string slotName, bool visible)
{
bool changed = false;
lock (_lock) changed = _spineObject.SetSlotVisible(slotName, visible);
if (changed) SlotVisibleChanged?.Invoke(this, new(slotName, visible));
return changed;
}
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;
float lastTimeScale = 1f;
float lastAlpha = 1f;
lock (_lock)
{
if (_spineObject.Data.AnimationsByName.ContainsKey(name))
{
// 需要记录之前的轨道属性值并还原
if (_spineObject.AnimationState.GetCurrent(index) is ITrackEntry entry)
{
lastTimeScale = entry.TimeScale;
lastAlpha = entry.Alpha;
}
entry = _spineObject.AnimationState.SetAnimation(index, name, true);
entry.TimeScale = lastTimeScale;
entry.Alpha = lastAlpha;
// XXX(#105): 部分 3.4.02 版本模型在设置动画后出现附件残留, 因此强制进行一次 Setup
if (_spineObject.Version == SpineVersion.V34)
{
_spineObject.Skeleton.SetSlotsToSetupPose();
}
changed = true;
}
}
if (changed) TrackPropertyChanged?.Invoke(this, new(index, nameof(TrackPropertyChangedEventArgs.AnimationName)));
}
public float GetTrackTimeScale(int index)
{
lock (_lock) return _spineObject.AnimationState.GetCurrent(index)?.TimeScale ?? 1;
}
public void SetTrackTimeScale(int index, float scale)
{
lock (_lock)
{
if (_spineObject.AnimationState.GetCurrent(index) is ITrackEntry entry)
{
entry.TimeScale = Math.Clamp(scale, 0.01f, 100f);
TrackPropertyChanged?.Invoke(this, new(index, nameof(TrackPropertyChangedEventArgs.TimeScale)));
}
}
}
public float GetTrackAlpha(int index)
{
lock (_lock) return _spineObject.AnimationState.GetCurrent(index)?.Alpha ?? 1;
}
public void SetTrackAlpha(int index, float alpha)
{
lock (_lock)
{
if (_spineObject.AnimationState.GetCurrent(index) is ITrackEntry entry)
{
entry.Alpha = Math.Clamp(alpha, 0f, 1f);
TrackPropertyChanged?.Invoke(this, new(index, nameof(TrackPropertyChangedEventArgs.Alpha)));
}
}
}
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);
TrackPropertyChanged?.Invoke(this, new(index, nameof(TrackPropertyChangedEventArgs.AnimationName)));
}
public void ResetAnimationsTime()
{
lock (_lock) _spineObject.ResetAnimationsTime();
}
public bool EnableDebug
{
get { lock (_lock) return _spineObject.EnableDebug; }
set { lock (_lock) SetProperty(_spineObject.EnableDebug, value, v => _spineObject.EnableDebug = v); }
}
public bool DebugTexture
{
get { lock (_lock) return _spineObject.DebugTexture; }
set { lock (_lock) SetProperty(_spineObject.DebugTexture, value, v => _spineObject.DebugTexture = v); }
}
public bool DebugBounds
{
get { lock (_lock) return _spineObject.DebugBounds; }
set { lock (_lock) SetProperty(_spineObject.DebugBounds, value, v => _spineObject.DebugBounds = v); }
}
public bool DebugBones
{
get { lock (_lock) return _spineObject.DebugBones; }
set { lock (_lock) SetProperty(_spineObject.DebugBones, value, v => _spineObject.DebugBones = v); }
}
public bool DebugRegions
{
get { lock (_lock) return _spineObject.DebugRegions; }
set { lock (_lock) SetProperty(_spineObject.DebugRegions, value, v => _spineObject.DebugRegions = v); }
}
public bool DebugMeshHulls
{
get { lock (_lock) return _spineObject.DebugMeshHulls; }
set { lock (_lock) SetProperty(_spineObject.DebugMeshHulls, value, v => _spineObject.DebugMeshHulls = v); }
}
public bool DebugMeshes
{
get { lock (_lock) return _spineObject.DebugMeshes; }
set { lock (_lock) SetProperty(_spineObject.DebugMeshes, value, v => _spineObject.DebugMeshes = v); }
}
public bool DebugBoundingBoxes
{
get { lock (_lock) return _spineObject.DebugBoundingBoxes; }
set { lock (_lock) SetProperty(_spineObject.DebugBoundingBoxes, value, v => _spineObject.DebugBoundingBoxes = v); }
}
public bool DebugPaths
{
get { lock (_lock) return _spineObject.DebugPaths; }
set { lock (_lock) SetProperty(_spineObject.DebugPaths, value, v => _spineObject.DebugPaths = v); }
}
public bool DebugPoints
{
get { lock (_lock) return _spineObject.DebugPoints; }
set { lock (_lock) SetProperty(_spineObject.DebugPoints, value, v => _spineObject.DebugPoints = v); }
}
public bool DebugClippings
{
get { lock (_lock) return _spineObject.DebugClippings; }
set { lock (_lock) SetProperty(_spineObject.DebugClippings, value, v => _spineObject.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 bool HitTest(float x, float y)
{
lock (_lock) return _spineObject.Skeleton.HitTest(x, y);
}
public SpineObjectConfigModel ObjectConfig
{
get
{
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(),
TimeScale = _spineObject.AnimationState.TimeScale,
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;
config.DisabledSlots = _spineObject.Skeleton.Slots.Where(it => it.Disabled).Select(it => it.Name).ToList();
// XXX: 处理空动画
foreach (var tr in _spineObject.AnimationState.IterTracks())
{
if (tr is not null)
{
config.Animations.Add(new()
{
AnimationName = tr.Animation.Name,
TimeScale = tr.TimeScale,
Alpha = tr.Alpha
});
}
else
{
config.Animations.Add(null);
}
}
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));
SetProperty(_spineObject.AnimationState.TimeScale, value.TimeScale, v => _spineObject.AnimationState.TimeScale = v, nameof(TimeScale));
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));
foreach (var slotName in value.DisabledSlots)
if (_spineObject.SetSlotVisible(slotName, false))
SlotVisibleChanged?.Invoke(this, new(slotName, false));
// XXX: 处理空动画
_spineObject.AnimationState.ClearTracks();
int trackIndex = 0;
foreach (var trConfig in value.Animations)
{
if (trConfig is not null && !string.IsNullOrEmpty(trConfig.AnimationName))
{
var tr = _spineObject.AnimationState.SetAnimation(trackIndex, trConfig.AnimationName, true);
tr.TimeScale = trConfig.TimeScale;
tr.Alpha = trConfig.Alpha;
TrackPropertyChanged?.Invoke(this, new(trackIndex, nameof(TrackPropertyChangedEventArgs.AnimationName)));
}
trackIndex++;
}
// XXX(#105): 部分 3.4.02 版本模型在设置动画后出现附件残留, 因此强制进行一次 Setup
if (_spineObject.Version == SpineVersion.V34)
{
_spineObject.Skeleton.SetSlotsToSetupPose();
}
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 SpineObjectWorkspaceConfigModel WorkspaceConfig
{
get
{
return new()
{
SkelPath = SkelPath,
AtlasPath = AtlasPath,
IsShown = IsShown,
ObjectConfig = ObjectConfig
};
}
}
#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 SlotVisibleChangedEventArgs(string slotName, bool visible) : EventArgs
{
public string SlotName { get; } = slotName;
public bool Visible { get; } = visible;
}
public class SlotAttachmentChangedEventArgs(string slotName, string? attachmentName) : EventArgs
{
public string SlotName { get; } = slotName;
public string? AttachmentName { get; } = attachmentName;
}
/// <summary>
/// 模型动画轨道属性变化事件参数, 需要检索 <c><see cref="PropertyName"/></c> 来确定发生变化的属性是什么
/// </summary>
/// <param name="trackIndex">发生属性变化的轨道索引</param>
/// <param name="propertyName">使用 <c>nameof</c> 设置发生改变的属性名</param>
public class TrackPropertyChangedEventArgs(int trackIndex, string propertyName) : EventArgs
{
public int TrackIndex { get; } = trackIndex;
/// <summary>
/// 发生变化的属性名, 将会使用 <c>nameof</c> 设置为属性名称字符串
/// </summary>
public string PropertyName { get; } = propertyName;
public string? AnimationName { get; }
public float TimeScale { get; } = 1f;
public float Alpha { get; } = 1f;
}
public class SpineObjectLoadOptions
{
public bool IsShown { get; set; } = true;
public bool UsePma { get; set; }
public bool DebugTexture { get; set; } = true;
public bool DebugBounds { get; set; }
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; }
}
}