diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md
new file mode 100644
index 0000000..ddc7272
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/bug_report.md
@@ -0,0 +1,18 @@
+---
+name: 问题报告/Bug report
+about: 报告可能的程序错误/Create a report to help us improve
+title: ''
+labels: ''
+assignees: ''
+---
+
+## 问题描述/Describe the bug
+清晰完整的描述问题是什么以及如何发生的。/A clear and concise description of what the bug is.
+
+## 复现方式(可选)/To Reproduce (Optional)
+
+## 截图(可选)/Screenshots (Optional)**
+如果有必要,提供报错时的有关截图。/If applicable, add screenshots to help explain your problem.
+
+## 附件(可选)/Attachments (Optional)
+请将会**出现问题的文件**以及**日志文件**打包成一个 ZIP 后作为附件贴在 issue 内。/Please compress the problematic files and the log files into a single ZIP archive and attach it to this issue.
diff --git a/CHANGELOG.md b/CHANGELOG.md
index e4a420c..8e38e42 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,13 @@
# CHANGELOG
+## v0.15.12
+
+- 增加单个模型和单个轨道的时间因子
+- 增加单个轨道的 Alpha 混合参数
+- 调整轨道清除命令至右键菜单
+- 设置默认标签页为模型
+- 完善导入时的报错信息
+
## v0.15.11
- 修复自定义导出中参数构造错误
diff --git a/README.en.md b/README.en.md
index c6bdf0b..f4667ba 100644
--- a/README.en.md
+++ b/README.en.md
@@ -21,6 +21,8 @@ A simple and user-friendly Spine file viewer and exporter with multi-language su
* Skin and custom slot attachment settings.
* Custom slot visibility settings.
* Debug rendering support.
+* View/model/track time scale adjustment.
+* Track alpha blending parameter settings.
* Fullscreen preview mode.
* Export to single frame/image sequence/animated GIF/video formats.
* Automatic resolution batch export.
diff --git a/README.md b/README.md
index a731169..8d4e8e9 100644
--- a/README.md
+++ b/README.md
@@ -21,6 +21,8 @@
- 支持皮肤/自定义插槽附件设置
- 支持自定义插槽可见性
- 支持调试渲染
+- 支持画面/模型/轨道时间倍速设置
+- 支持设置轨道 Alpha 混合参数
- 支持全屏预览
- 支持单帧/动图/视频文件导出
- 支持自动分辨率批量导出
diff --git a/Spine/Spine.csproj b/Spine/Spine.csproj
index 54e5ef1..3a680d0 100644
--- a/Spine/Spine.csproj
+++ b/Spine/Spine.csproj
@@ -7,7 +7,7 @@
net8.0-windows
$(SolutionDir)out
false
- 0.15.11
+ 0.15.12
diff --git a/Spine/SpineObject.cs b/Spine/SpineObject.cs
index 5854cc2..fe8edb9 100644
--- a/Spine/SpineObject.cs
+++ b/Spine/SpineObject.cs
@@ -66,10 +66,10 @@ namespace Spine
}
catch (InvalidOperationException)
{
- throw new KeyNotFoundException($"Unrecognized skel suffix '{skelPath}'");
+ throw new KeyNotFoundException($"Unrecognized skel file suffix");
}
}
- else if (!File.Exists(atlasPath)) throw new FileNotFoundException($"{nameof(atlasPath)} not found", skelPath);
+ else if (!File.Exists(atlasPath)) throw new FileNotFoundException($"{nameof(atlasPath)} not found", atlasPath);
AtlasPath = Path.GetFullPath(atlasPath);
// 自动检测版本, 可能会抛出异常
@@ -105,13 +105,20 @@ namespace Spine
// 依然加载不成功就只能报错
if (_data is null || Version is null)
- throw new InvalidDataException($"Failed to load spine by existed versions: '{skelPath}', '{atlasPath}'");
+ throw new InvalidDataException($"Failed to load spine by existed versions");
}
else
{
// 根据版本实例化对象
Version = version;
- _data = SpineObjectData.New(Version, skelPath, atlasPath, textureLoader);
+ try
+ {
+ _data = SpineObjectData.New(Version, skelPath, atlasPath, textureLoader);
+ }
+ catch
+ {
+ throw new InvalidDataException($"Failed to load spine with version '{version}'");
+ }
}
// 创建状态实例
@@ -167,6 +174,7 @@ namespace Spine
// 拷贝渲染设置
UsePma = other.UsePma;
Physics = other.Physics;
+ _animationState.TimeScale = other._animationState.TimeScale;
// 拷贝皮肤加载情况
_skinLoadStatus = other._skinLoadStatus.ToDictionary();
diff --git a/SpineViewer/Extensions/SpineObjectExtension.cs b/SpineViewer/Extensions/SpineObjectExtension.cs
index 2d25ef8..0ee0602 100644
--- a/SpineViewer/Extensions/SpineObjectExtension.cs
+++ b/SpineViewer/Extensions/SpineObjectExtension.cs
@@ -21,6 +21,8 @@ namespace SpineViewer.Extensions
foreach (var tr in self.AnimationState.IterTracks().Where(t => t is not null))
{
var t = spineObject.AnimationState.SetAnimation(tr!.TrackIndex, tr.Animation, tr.Loop);
+ t.TimeScale = tr.TimeScale;
+ t.Alpha = tr.Alpha;
if (keepTrackTime)
t.TrackTime = tr.TrackTime;
}
@@ -38,7 +40,8 @@ namespace SpineViewer.Extensions
foreach (var e in self.AnimationState.IterTracks())
{
if (e is not null)
- self.AnimationState.SetAnimation(e.TrackIndex, e.Animation, e.Loop);
+ e.TrackTime = 0; // 直接重置时间能保留原本的 TrackEntry
+ //self.AnimationState.SetAnimation(e.TrackIndex, e.Animation, e.Loop);
}
self.Update(0);
}
@@ -65,7 +68,7 @@ namespace SpineViewer.Extensions
///
/// 按给定的帧率获取所有轨道第一个条目动画全时长包围盒大小, 是一个耗时操作, 如果可能的话最好缓存结果
///
- public static Rect GetAnimationBounds(this SpineObject self, float fps = 10)
+ public static Rect GetAnimationBounds(this SpineObject self, float fps = 30)
{
using var copy = self.Copy();
var bounds = copy.GetCurrentBounds();
diff --git a/SpineViewer/Models/SpineObjectConfigModel.cs b/SpineViewer/Models/SpineObjectConfigModel.cs
index 8b92b71..58dc5fd 100644
--- a/SpineViewer/Models/SpineObjectConfigModel.cs
+++ b/SpineViewer/Models/SpineObjectConfigModel.cs
@@ -17,6 +17,8 @@ namespace SpineViewer.Models
public string Physics { get; set; } = ISkeleton.Physics.Update.ToString();
+ public float TimeScale { get; set; } = 1f;
+
public float Scale { get; set; } = 1f;
public bool FlipX { get; set; }
@@ -54,5 +56,15 @@ namespace SpineViewer.Models
public bool DebugPoints { get; set; }
public bool DebugClippings { get; set; }
+
+ }
+
+ public class AnimationConfigModel
+ {
+ string Name { get; set; } = "";
+
+ float TimeScale { get; set; } = 1f;
+
+ float Alpha { get; set; } = 1f;
}
}
diff --git a/SpineViewer/Models/SpineObjectModel.cs b/SpineViewer/Models/SpineObjectModel.cs
index 71237f9..49b6acc 100644
--- a/SpineViewer/Models/SpineObjectModel.cs
+++ b/SpineViewer/Models/SpineObjectModel.cs
@@ -89,7 +89,7 @@ namespace SpineViewer.Models
public event EventHandler? SlotAttachmentChanged;
- public event EventHandler? AnimationChanged;
+ public event EventHandler? TrackPropertyChanged;
public SpineVersion Version => _spineObject.Version;
@@ -129,6 +129,12 @@ namespace SpineViewer.Models
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); }
+ }
+
///
/// 缩放倍数, 绝对值大小, 两个方向大小不一致时返回 -1, 设置时不会影响正负号
///
@@ -248,15 +254,59 @@ namespace SpineViewer.Models
public void SetAnimation(int index, string name)
{
bool changed = false;
+ float lastTimeScale = 1f;
+ float lastAlpha = 1f;
lock (_lock)
{
if (_spineObject.Data.AnimationsByName.ContainsKey(name))
{
- _spineObject.AnimationState.SetAnimation(index, name, true);
+ // 需要记录之前的轨道属性值并还原
+ 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;
changed = true;
}
}
- if (changed) AnimationChanged?.Invoke(this, new(index, name));
+ 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()
@@ -277,7 +327,7 @@ namespace SpineViewer.Models
public void ClearTrack(int index)
{
lock (_lock) _spineObject.AnimationState.ClearTrack(index);
- AnimationChanged?.Invoke(this, new(index, null));
+ TrackPropertyChanged?.Invoke(this, new(index, nameof(TrackPropertyChangedEventArgs.AnimationName)));
}
public void ResetAnimationsTime()
@@ -388,6 +438,7 @@ namespace SpineViewer.Models
UsePma = _spineObject.UsePma,
Physics = _spineObject.Physics.ToString(),
+ TimeScale = _spineObject.AnimationState.TimeScale,
DebugTexture = _spineObject.DebugTexture,
DebugBounds = _spineObject.DebugBounds,
@@ -427,6 +478,7 @@ namespace SpineViewer.Models
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));
+ 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))
@@ -450,7 +502,7 @@ namespace SpineViewer.Models
{
if (!string.IsNullOrEmpty(name))
_spineObject.AnimationState.SetAnimation(trackIndex, name, true);
- AnimationChanged?.Invoke(this, new(trackIndex, name));
+ TrackPropertyChanged?.Invoke(this, new(trackIndex, nameof(TrackPropertyChangedEventArgs.AnimationName)));
trackIndex++;
}
@@ -540,10 +592,23 @@ namespace SpineViewer.Models
public string? AttachmentName { get; } = attachmentName;
}
- public class AnimationChangedEventArgs(int trackIndex, string? animationName) : EventArgs
+ ///
+ /// 模型动画轨道属性变化事件参数, 需要检索 来确定发生变化的属性是什么
+ ///
+ /// 发生属性变化的轨道索引
+ /// 使用 nameof 设置发生改变的属性名
+ public class TrackPropertyChangedEventArgs(int trackIndex, string propertyName) : EventArgs
{
public int TrackIndex { get; } = trackIndex;
- public string? AnimationName { get; } = animationName;
+
+ ///
+ /// 发生变化的属性名, 将会使用 nameof 设置为属性名称字符串
+ ///
+ public string PropertyName { get; } = propertyName;
+
+ public string? AnimationName { get; }
+ public float TimeScale { get; } = 1f;
+ public float Alpha { get; } = 1f;
}
public class SpineObjectLoadOptions
diff --git a/SpineViewer/Resources/Strings/en.xaml b/SpineViewer/Resources/Strings/en.xaml
index f0aae8e..4844a81 100644
--- a/SpineViewer/Resources/Strings/en.xaml
+++ b/SpineViewer/Resources/Strings/en.xaml
@@ -64,6 +64,8 @@
Show
Premultiply Alpha
Physics
+ Time Scale
+ Time scale for a single model, must be positive.
Transform
Scale
@@ -84,6 +86,11 @@
Animation
Add
Insert
+ Clear
+ Time Scale
+ Time scale for a single track, must be positive.
+ Alpha Blending
+ Value range: 0–1. Similar to image blending, controls how animations from higher-index tracks mix into lower-index tracks.
Debug
Texture
@@ -106,6 +113,7 @@
Zoom
Rotation (Degrees)
Max FPS
+ Maximum frame rate of the preview. Set to 0 for no limit.
Playback Speed
Render Selected Only
Show Axis
diff --git a/SpineViewer/Resources/Strings/ja.xaml b/SpineViewer/Resources/Strings/ja.xaml
index caa0a12..946aa8b 100644
--- a/SpineViewer/Resources/Strings/ja.xaml
+++ b/SpineViewer/Resources/Strings/ja.xaml
@@ -64,6 +64,8 @@
表示
プレマルチプライドアルファ
物理
+ 時間スケール
+ 単一モデルの時間スケール。正の値のみ指定可能です。
変換
スケール
@@ -84,6 +86,11 @@
アニメーション
追加
挿入
+ 削除
+ 時間スケール
+ 単一トラックの時間スケール。正の値のみ指定可能です。
+ アルファ合成
+ 値の範囲:0~1。画像の合成と同様に、高インデックストラックのアニメーションが低インデックストラックにどの程度混合されるかを制御します。
デバッグ
テクスチャ
@@ -106,6 +113,7 @@
ズーム
回転(度)
最大FPS
+ プレビュー画面の最大フレームレート。0 に設定すると制限なし。
再生速度
選択のみレンダリング
座標軸を表示
diff --git a/SpineViewer/Resources/Strings/zh.xaml b/SpineViewer/Resources/Strings/zh.xaml
index 4131f7d..8b78d6e 100644
--- a/SpineViewer/Resources/Strings/zh.xaml
+++ b/SpineViewer/Resources/Strings/zh.xaml
@@ -64,6 +64,8 @@
显示
预乘Alpha通道
物理
+ 时间因子
+ 单个模型的时间因子,只能取正数
变换
缩放
@@ -84,6 +86,11 @@
动画
添加
插入
+ 删除
+ 时间因子
+ 单个轨道的时间因子,只能取正数
+ Alpha 混合
+ 取值范围 0-1,与图像混合类似,可以控制高索引轨道在低索引轨道中的动画混合比例
调试
Texture
@@ -106,6 +113,7 @@
缩放
旋转(角度)
最大帧率
+ 预览画面的最大帧率,设置为 0 时则无帧率限制
播放速度
仅渲染选中
显示坐标轴
diff --git a/SpineViewer/SpineViewer.csproj b/SpineViewer/SpineViewer.csproj
index 5be81f1..24d5086 100644
--- a/SpineViewer/SpineViewer.csproj
+++ b/SpineViewer/SpineViewer.csproj
@@ -7,7 +7,7 @@
net8.0-windows
$(SolutionDir)out
false
- 0.15.11
+ 0.15.12
WinExe
true
diff --git a/SpineViewer/ViewModels/MainWindow/SpineObjectTabViewModel.cs b/SpineViewer/ViewModels/MainWindow/SpineObjectTabViewModel.cs
index df0a56d..469ba99 100644
--- a/SpineViewer/ViewModels/MainWindow/SpineObjectTabViewModel.cs
+++ b/SpineViewer/ViewModels/MainWindow/SpineObjectTabViewModel.cs
@@ -31,7 +31,7 @@ namespace SpineViewer.ViewModels.MainWindow
foreach (var obj in _selectedObjects)
{
obj.PropertyChanged -= SingleModel_PropertyChanged;
- obj.AnimationChanged -= SingleModel_AnimationChanged;
+ obj.TrackPropertyChanged -= SingleModel_TrackPropChanged;
}
_skins.Clear();
_slots.Clear();
@@ -44,7 +44,7 @@ namespace SpineViewer.ViewModels.MainWindow
foreach (var obj in _selectedObjects)
{
obj.PropertyChanged += SingleModel_PropertyChanged;
- obj.AnimationChanged += SingleModel_AnimationChanged;
+ obj.TrackPropertyChanged += SingleModel_TrackPropChanged;
}
IEnumerable commonSkinNames = _selectedObjects[0].Skins;
@@ -74,6 +74,7 @@ namespace SpineViewer.ViewModels.MainWindow
OnPropertyChanged(nameof(IsShown));
OnPropertyChanged(nameof(UsePma));
OnPropertyChanged(nameof(Physics));
+ OnPropertyChanged(nameof(TimeScale));
OnPropertyChanged(nameof(Scale));
OnPropertyChanged(nameof(FlipX));
@@ -217,6 +218,25 @@ namespace SpineViewer.ViewModels.MainWindow
}
}
+ public float? TimeScale
+ {
+ get
+ {
+ if (_selectedObjects.Length <= 0) return null;
+ var val = _selectedObjects[0].TimeScale;
+ if (_selectedObjects.Skip(1).Any(it => it.TimeScale != val)) return null;
+ return val;
+ }
+
+ set
+ {
+ if (_selectedObjects.Length <= 0) return;
+ if (value is null) return;
+ foreach (var sp in _selectedObjects) sp.TimeScale = (float)value;
+ OnPropertyChanged();
+ }
+ }
+
public float? Scale
{
get
@@ -384,6 +404,27 @@ namespace SpineViewer.ViewModels.MainWindow
);
private RelayCommand? _cmd_InsertTrack;
+ public RelayCommand? Cmd_ClearTrack => _cmd_ClearTrack ??= new(
+ args =>
+ {
+ if (_selectedObjects.Length <= 0) return;
+ if (args is null) return;
+ if (args.Count <= 0) return;
+
+ foreach (var vm in args.OfType())
+ foreach (var sp in _selectedObjects)
+ sp.ClearTrack(vm.TrackIndex);
+ },
+ args =>
+ {
+ if (_selectedObjects.Length <= 0) return false;
+ if (args is null) return false;
+ if (args.Count <= 0) return false;
+ return true;
+ }
+ );
+ private RelayCommand? _cmd_ClearTrack;
+
public bool? DebugTexture
{
get
@@ -574,58 +615,67 @@ namespace SpineViewer.ViewModels.MainWindow
}
}
- ///
- /// 监听单个模型属性发生变化, 则更新聚合属性值
- ///
- private void SingleModel_PropertyChanged(object? sender, PropertyChangedEventArgs e)
+ private static readonly Dictionary _singleModelPropertyMap = new()
{
- if (e.PropertyName == nameof(SpineObjectModel.IsShown)) OnPropertyChanged(nameof(IsShown));
- else if (e.PropertyName == nameof(SpineObjectModel.UsePma)) OnPropertyChanged(nameof(UsePma));
- else if (e.PropertyName == nameof(SpineObjectModel.Physics)) OnPropertyChanged(nameof(Physics));
+ { nameof(SpineObjectModel.IsShown), nameof(IsShown) },
+ { nameof(SpineObjectModel.UsePma), nameof(UsePma) },
+ { nameof(SpineObjectModel.Physics), nameof(Physics) },
+ { nameof(SpineObjectModel.TimeScale), nameof(TimeScale) },
- else if (e.PropertyName == nameof(SpineObjectModel.Scale)) OnPropertyChanged(nameof(Scale));
- else if (e.PropertyName == nameof(SpineObjectModel.FlipX)) OnPropertyChanged(nameof(FlipX));
- else if (e.PropertyName == nameof(SpineObjectModel.FlipY)) OnPropertyChanged(nameof(FlipY));
- else if (e.PropertyName == nameof(SpineObjectModel.X)) OnPropertyChanged(nameof(X));
- else if (e.PropertyName == nameof(SpineObjectModel.Y)) OnPropertyChanged(nameof(Y));
+ { nameof(SpineObjectModel.Scale), nameof(Scale) },
+ { nameof(SpineObjectModel.FlipX), nameof(FlipX) },
+ { nameof(SpineObjectModel.FlipY), nameof(FlipY) },
+ { nameof(SpineObjectModel.X), nameof(X) },
+ { nameof(SpineObjectModel.Y), nameof(Y) },
// Skins 变化在 SkinViewModel 中监听
// Slots 变化在 SlotAttachmentViewModel 中监听
// AnimationTracks 变化在 AnimationTrackViewModel 中监听
- else if (e.PropertyName == nameof(SpineObjectModel.DebugTexture)) OnPropertyChanged(nameof(DebugTexture));
- else if (e.PropertyName == nameof(SpineObjectModel.DebugBounds)) OnPropertyChanged(nameof(DebugBounds));
- else if (e.PropertyName == nameof(SpineObjectModel.DebugBones)) OnPropertyChanged(nameof(DebugBones));
- else if (e.PropertyName == nameof(SpineObjectModel.DebugRegions)) OnPropertyChanged(nameof(DebugRegions));
- else if (e.PropertyName == nameof(SpineObjectModel.DebugMeshHulls)) OnPropertyChanged(nameof(DebugMeshHulls));
- else if (e.PropertyName == nameof(SpineObjectModel.DebugMeshes)) OnPropertyChanged(nameof(DebugMeshes));
- else if (e.PropertyName == nameof(SpineObjectModel.DebugBoundingBoxes)) OnPropertyChanged(nameof(DebugBoundingBoxes));
- else if (e.PropertyName == nameof(SpineObjectModel.DebugPaths)) OnPropertyChanged(nameof(DebugPaths));
- else if (e.PropertyName == nameof(SpineObjectModel.DebugPoints)) OnPropertyChanged(nameof(DebugPoints));
- else if (e.PropertyName == nameof(SpineObjectModel.DebugClippings)) OnPropertyChanged(nameof(DebugClippings));
+ { nameof(SpineObjectModel.DebugTexture), nameof(DebugTexture) },
+ { nameof(SpineObjectModel.DebugBounds), nameof(DebugBounds) },
+ { nameof(SpineObjectModel.DebugBones), nameof(DebugBones) },
+ { nameof(SpineObjectModel.DebugRegions), nameof(DebugRegions) },
+ { nameof(SpineObjectModel.DebugMeshHulls), nameof(DebugMeshHulls) },
+ { nameof(SpineObjectModel.DebugMeshes), nameof(DebugMeshes) },
+ { nameof(SpineObjectModel.DebugBoundingBoxes), nameof(DebugBoundingBoxes) },
+ { nameof(SpineObjectModel.DebugPaths), nameof(DebugPaths) },
+ { nameof(SpineObjectModel.DebugPoints), nameof(DebugPoints) },
+ { nameof(SpineObjectModel.DebugClippings), nameof(DebugClippings) },
+ };
+
+ ///
+ /// 监听单个模型属性发生变化, 则更新聚合属性值
+ ///
+ private void SingleModel_PropertyChanged(object? sender, PropertyChangedEventArgs e)
+ {
+ if (_singleModelPropertyMap.TryGetValue(e.PropertyName, out var targetProperty))
+ {
+ OnPropertyChanged(targetProperty);
+ }
}
///
/// 监听单个模型动画轨道发生变化, 则重建聚合后的动画列表
///
- ///
- ///
- private void SingleModel_AnimationChanged(object? sender, AnimationChangedEventArgs e)
+ private void SingleModel_TrackPropChanged(object? sender, TrackPropertyChangedEventArgs e)
{
- // XXX: 这里应该有更好的实现, 当 e.AnimationName == null 的时候代表删除轨道需要重新构建列表
- // 但是目前无法识别是否增加了轨道, 因此总是重建列表
-
- // 由于某些原因, 直接使用 Clear 会和 UI 逻辑冲突产生报错, 因此需要放到 Dispatcher 里延迟执行
- Application.Current.Dispatcher.BeginInvoke(
- () =>
- {
- _animationTracks.Clear();
- IEnumerable commonTrackIndices = _selectedObjects[0].GetTrackIndices();
- foreach (var obj in _selectedObjects.Skip(1)) commonTrackIndices = commonTrackIndices.Intersect(obj.GetTrackIndices());
- foreach (var idx in commonTrackIndices) _animationTracks.Add(new(idx, _selectedObjects));
- }
- );
+ if (e.PropertyName == nameof(TrackPropertyChangedEventArgs.AnimationName))
+ {
+ // XXX: 这里应该有更好的实现, 当 e.AnimationName == null 的时候代表删除轨道需要重新构建列表
+ // 但是目前无法识别是否增加了轨道, 因此总是重建列表
+ // 由于某些原因, 直接使用 Clear 会和 UI 逻辑冲突产生报错, 因此需要放到 Dispatcher 里延迟执行
+ Application.Current.Dispatcher.BeginInvoke(
+ () =>
+ {
+ _animationTracks.Clear();
+ IEnumerable commonTrackIndices = _selectedObjects[0].GetTrackIndices();
+ foreach (var obj in _selectedObjects.Skip(1)) commonTrackIndices = commonTrackIndices.Intersect(obj.GetTrackIndices());
+ foreach (var idx in commonTrackIndices) _animationTracks.Add(new(idx, _selectedObjects));
+ }
+ );
+ }
}
public class SkinViewModel : ObservableObject
@@ -798,21 +848,36 @@ namespace SpineViewer.ViewModels.MainWindow
// 使用弱引用, 则此 ViewModel 被释放时无需显式退订事件
foreach (var sp in _spines)
{
- WeakEventManager.AddHandler(
+ WeakEventManager.AddHandler(
sp,
- nameof(sp.AnimationChanged),
- SingleModel_AnimationChanged
+ nameof(sp.TrackPropertyChanged),
+ SingleModel_TrackPropChanged
);
}
}
- public RelayCommand Cmd_ClearTrack => _cmd_ClearTrack ??= new(() => { foreach (var sp in _spines) sp.ClearTrack(_trackIndex); });
- private RelayCommand? _cmd_ClearTrack;
-
public ReadOnlyCollection AnimationNames => _animationNames.AsReadOnly();
public int TrackIndex => _trackIndex;
+ public float? AnimationDuration
+ {
+ get
+ {
+ if (_spines.Length <= 0) return null;
+ var ani = _spines[0].GetAnimation(_trackIndex);
+ if (ani is null) return null;
+ var val = _spines[0].GetAnimationDuration(ani);
+ foreach (var sp in _spines.Skip(1))
+ {
+ var a = sp.GetAnimation(_trackIndex);
+ if (a is null) return null;
+ if (sp.GetAnimationDuration(a) != val) return null;
+ }
+ return val;
+ }
+ }
+
public string? AnimationName
{
get
@@ -834,27 +899,54 @@ namespace SpineViewer.ViewModels.MainWindow
}
}
- public float? AnimationDuration
+ public float? TrackTimeScale
{
get
{
+ // XXX: 空轨道和多选不相同都会返回 null
if (_spines.Length <= 0) return null;
- var ani = _spines[0].GetAnimation(_trackIndex);
- if (ani is null) return null;
- var val = _spines[0].GetAnimationDuration(ani);
- foreach (var sp in _spines.Skip(1))
- {
- var a = sp.GetAnimation(_trackIndex);
- if (a is null) return null;
- if (sp.GetAnimationDuration(a) != val) return null;
- }
+ var val = _spines[0].GetTrackTimeScale(_trackIndex);
+ if (_spines.Skip(1).Any(it => it.GetTrackTimeScale(_trackIndex) != val)) return null;
return val;
}
+
+ set
+ {
+ if (_spines.Length <= 0) return;
+ if (value is null) return;
+ foreach (var sp in _spines) sp.SetTrackTimeScale(_trackIndex, (float)value);
+ OnPropertyChanged();
+ }
}
- private void SingleModel_AnimationChanged(object? sender, AnimationChangedEventArgs e)
+ public float? TrackAlpha
{
- if (e.TrackIndex == _trackIndex) OnPropertyChanged(nameof(AnimationName));
+ get
+ {
+ // XXX: 空轨道和多选不相同都会返回 null
+ if (_spines.Length <= 0) return null;
+ var val = _spines[0].GetTrackAlpha(_trackIndex);
+ if (_spines.Skip(1).Any(it => it.GetTrackAlpha(_trackIndex) != val)) return null;
+ return val;
+ }
+
+ set
+ {
+ if (_spines.Length <= 0) return;
+ if (value is null) return;
+ foreach (var sp in _spines) sp.SetTrackAlpha(_trackIndex, (float)value);
+ OnPropertyChanged();
+ }
+ }
+
+ private void SingleModel_TrackPropChanged(object? sender, TrackPropertyChangedEventArgs e)
+ {
+ if (e.TrackIndex == _trackIndex)
+ {
+ if (e.PropertyName == nameof(TrackPropertyChangedEventArgs.AnimationName)) OnPropertyChanged(nameof(AnimationName));
+ else if (e.PropertyName == nameof(TrackPropertyChangedEventArgs.TimeScale)) OnPropertyChanged(nameof(TrackTimeScale));
+ else if (e.PropertyName == nameof(TrackPropertyChangedEventArgs.Alpha)) OnPropertyChanged(nameof(TrackAlpha));
+ }
}
}
}
diff --git a/SpineViewer/Views/MainWindow.xaml b/SpineViewer/Views/MainWindow.xaml
index a23091b..fc630dd 100644
--- a/SpineViewer/Views/MainWindow.xaml
+++ b/SpineViewer/Views/MainWindow.xaml
@@ -89,117 +89,6 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
@@ -421,6 +310,7 @@
+
@@ -434,6 +324,10 @@
+
+
+
+
@@ -503,7 +397,7 @@
-
+
@@ -541,7 +435,7 @@
-
+
@@ -568,6 +462,9 @@
+
@@ -575,19 +472,38 @@
-
+
+
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
@@ -660,6 +576,117 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
@@ -695,6 +722,7 @@
+
@@ -729,22 +757,24 @@
+
+
-
-
+
+
-
-
+
+
-
-
+
+
-
-
-
+
+
+