Compare commits

...

27 Commits

Author SHA1 Message Date
ww-rm
c0553042fd 更新至v0.11.4 2025-03-30 11:59:52 +08:00
ww-rm
af8b02654b update readme 2025-03-30 11:59:37 +08:00
ww-rm
4779ec91d0 update changelog 2025-03-30 11:59:13 +08:00
ww-rm
14d7f4af0e 增加MP4导出格式 2025-03-30 11:56:20 +08:00
ww-rm
f9888b23dd 设置GIF默认背景颜色为纯白透明背景 2025-03-30 11:56:06 +08:00
ww-rm
411cdbb00f 设置默认颜色为纯黑透明背景 2025-03-30 11:55:52 +08:00
ww-rm
d859f07469 增加导出时输出ffmpeg参数 2025-03-29 23:48:56 +08:00
ww-rm
c111819093 增加背景颜色参数 2025-03-29 22:17:08 +08:00
ww-rm
aa8321d13c 整理代码结构 2025-03-29 21:15:34 +08:00
ww-rm
5e3bd972e5 移动GetVersion至SpineHelper 2025-03-29 17:04:26 +08:00
ww-rm
ad39a04fff 重命名SpineVersion 2025-03-29 16:59:28 +08:00
ww-rm
9a97e84296 解耦对MessageBox的依赖,提供单独的Shader初始化函数 2025-03-29 16:51:05 +08:00
ww-rm
1b7b0dcb13 解耦日志器 2025-03-29 16:30:32 +08:00
ww-rm
d365a5060b small change 2025-03-29 15:34:56 +08:00
ww-rm
b69589394a 提取ImplementationResolver实现 2025-03-29 15:12:50 +08:00
ww-rm
00f5791766 增加导出时任务栏图标显示 2025-03-28 20:53:48 +08:00
ww-rm
38cab2eda7 修复可能的预览图资源泄漏 2025-03-27 23:31:03 +08:00
ww-rm
0db4d6e4e0 small change 2025-03-27 19:45:56 +08:00
ww-rm
549712962f 去除多余组件 2025-03-27 10:11:44 +08:00
ww-rm
34b7002faf 增加背景颜色选项 2025-03-27 10:08:16 +08:00
ww-rm
0e6f47b23c 预修改适配对多轨道动画 2025-03-27 09:56:21 +08:00
ww-rm
a372a89b5e 增加update0 2025-03-27 09:09:22 +08:00
ww-rm
239847aee7 皮肤更换后使用SetSlotsToSetupPose而不是SetToSetupPose 2025-03-27 09:03:09 +08:00
ww-rm
813249c6a7 调整布局 2025-03-26 21:09:52 +08:00
ww-rm
293ab28bce 更新预览图 2025-03-26 20:49:22 +08:00
ww-rm
98e73cdec5 调整面板比例 2025-03-26 20:48:23 +08:00
ww-rm
6d34bb9d25 移除无用引用 2025-03-26 20:37:27 +08:00
49 changed files with 1176 additions and 932 deletions

View File

@@ -1,5 +1,14 @@
# CHANGELOG # CHANGELOG
## v0.11.4
- 增加 MP4 导出格式
- 增加导出背景颜色参数
- 增加日志输出 FFMpeg 参数字符串
- 增加导出时任务栏图标执行动效
- 修复预览面板移动模型时物理效果不同步的问题
- 优化部分使用体验
## v0.11.3 ## v0.11.3
- 增加模型隐藏设置属性 - 增加模型隐藏设置属性

View File

@@ -24,7 +24,7 @@ You can also download the zip package with the `SelfContained` suffix, which can
- [x] Frame Sequence - [x] Frame Sequence
- [x] Animated GIF - [x] Animated GIF
- [ ] MKV - [ ] MKV
- [ ] MP4 - [x] MP4
- [ ] MOV - [ ] MOV
- [ ] WebM - [ ] WebM

View File

@@ -24,7 +24,7 @@
- [x] 帧序列 - [x] 帧序列
- [x] GIF 动图 - [x] GIF 动图
- [ ] MKV - [ ] MKV
- [ ] MP4 - [x] MP4
- [ ] MOV - [ ] MOV
- [ ] WebM - [ ] WebM

View File

@@ -8,6 +8,7 @@ using System.Text;
using System.Threading.Tasks; using System.Threading.Tasks;
using System.Windows.Forms; using System.Windows.Forms;
using System.IO; using System.IO;
using SpineViewer.Spine;
namespace SpineViewer.Controls namespace SpineViewer.Controls
{ {
@@ -33,14 +34,14 @@ namespace SpineViewer.Controls
{ {
if (File.Exists(path)) if (File.Exists(path))
{ {
if (Spine.Spine.CommonSkelSuffix.Contains(Path.GetExtension(path).ToLower())) if (SpineHelper.CommonSkelSuffix.Contains(Path.GetExtension(path).ToLower()))
listBox.Items.Add(Path.GetFullPath(path)); listBox.Items.Add(Path.GetFullPath(path));
} }
else if (Directory.Exists(path)) else if (Directory.Exists(path))
{ {
foreach (var file in Directory.EnumerateFiles(path, "*.*", SearchOption.AllDirectories)) foreach (var file in Directory.EnumerateFiles(path, "*.*", SearchOption.AllDirectories))
{ {
if (Spine.Spine.CommonSkelSuffix.Contains(Path.GetExtension(file).ToLower())) if (SpineHelper.CommonSkelSuffix.Contains(Path.GetExtension(file).ToLower()))
listBox.Items.Add(file); listBox.Items.Add(file);
} }
} }
@@ -57,7 +58,7 @@ namespace SpineViewer.Controls
{ {
foreach (var file in Directory.EnumerateFiles(path, "*.*", SearchOption.AllDirectories)) foreach (var file in Directory.EnumerateFiles(path, "*.*", SearchOption.AllDirectories))
{ {
if (Spine.Spine.CommonSkelSuffix.Contains(Path.GetExtension(file).ToLower())) if (SpineHelper.CommonSkelSuffix.Contains(Path.GetExtension(file).ToLower()))
listBox.Items.Add(file); listBox.Items.Add(file);
} }
} }

View File

@@ -44,8 +44,9 @@
toolStripMenuItem_MoveTop = new ToolStripMenuItem(); toolStripMenuItem_MoveTop = new ToolStripMenuItem();
toolStripMenuItem_MoveBottom = new ToolStripMenuItem(); toolStripMenuItem_MoveBottom = new ToolStripMenuItem();
toolStripSeparator3 = new ToolStripSeparator(); toolStripSeparator3 = new ToolStripSeparator();
toolStripMenuItem_SelectAll = new ToolStripMenuItem();
toolStripMenuItem_CopyPreview = new ToolStripMenuItem(); toolStripMenuItem_CopyPreview = new ToolStripMenuItem();
toolStripMenuItem_AddFromClipboard = new ToolStripMenuItem();
toolStripMenuItem_SelectAll = new ToolStripMenuItem();
toolStripSeparator4 = new ToolStripSeparator(); toolStripSeparator4 = new ToolStripSeparator();
toolStripMenuItem_ChangeView = new ToolStripMenuItem(); toolStripMenuItem_ChangeView = new ToolStripMenuItem();
toolStripMenuItem_LargeIconView = new ToolStripMenuItem(); toolStripMenuItem_LargeIconView = new ToolStripMenuItem();
@@ -53,7 +54,6 @@
toolStripMenuItem_DetailsView = new ToolStripMenuItem(); toolStripMenuItem_DetailsView = new ToolStripMenuItem();
imageList_LargeIcon = new ImageList(components); imageList_LargeIcon = new ImageList(components);
imageList_SmallIcon = new ImageList(components); imageList_SmallIcon = new ImageList(components);
toolStripMenuItem_AddFromClipboard = new ToolStripMenuItem();
contextMenuStrip.SuspendLayout(); contextMenuStrip.SuspendLayout();
SuspendLayout(); SuspendLayout();
// //
@@ -84,14 +84,14 @@
// columnHeader_Name // columnHeader_Name
// //
columnHeader_Name.Text = "名称"; columnHeader_Name.Text = "名称";
columnHeader_Name.Width = 220; columnHeader_Name.Width = 300;
// //
// contextMenuStrip // contextMenuStrip
// //
contextMenuStrip.ImageScalingSize = new Size(24, 24); contextMenuStrip.ImageScalingSize = new Size(24, 24);
contextMenuStrip.Items.AddRange(new ToolStripItem[] { toolStripMenuItem_Add, toolStripMenuItem_Insert, toolStripMenuItem_Remove, toolStripSeparator1, toolStripMenuItem_BatchAdd, toolStripMenuItem_RemoveAll, toolStripSeparator2, toolStripMenuItem_MoveUp, toolStripMenuItem_MoveDown, toolStripMenuItem_MoveTop, toolStripMenuItem_MoveBottom, toolStripSeparator3, toolStripMenuItem_CopyPreview, toolStripMenuItem_AddFromClipboard, toolStripMenuItem_SelectAll, toolStripSeparator4, toolStripMenuItem_ChangeView }); contextMenuStrip.Items.AddRange(new ToolStripItem[] { toolStripMenuItem_Add, toolStripMenuItem_Insert, toolStripMenuItem_Remove, toolStripSeparator1, toolStripMenuItem_BatchAdd, toolStripMenuItem_RemoveAll, toolStripSeparator2, toolStripMenuItem_MoveUp, toolStripMenuItem_MoveDown, toolStripMenuItem_MoveTop, toolStripMenuItem_MoveBottom, toolStripSeparator3, toolStripMenuItem_CopyPreview, toolStripMenuItem_AddFromClipboard, toolStripMenuItem_SelectAll, toolStripSeparator4, toolStripMenuItem_ChangeView });
contextMenuStrip.Name = "contextMenuStrip"; contextMenuStrip.Name = "contextMenuStrip";
contextMenuStrip.Size = new Size(329, 451); contextMenuStrip.Size = new Size(329, 418);
contextMenuStrip.Closed += contextMenuStrip_Closed; contextMenuStrip.Closed += contextMenuStrip_Closed;
contextMenuStrip.Opening += contextMenuStrip_Opening; contextMenuStrip.Opening += contextMenuStrip_Opening;
// //
@@ -178,14 +178,6 @@
toolStripSeparator3.Name = "toolStripSeparator3"; toolStripSeparator3.Name = "toolStripSeparator3";
toolStripSeparator3.Size = new Size(325, 6); toolStripSeparator3.Size = new Size(325, 6);
// //
// toolStripMenuItem_SelectAll
//
toolStripMenuItem_SelectAll.Name = "toolStripMenuItem_SelectAll";
toolStripMenuItem_SelectAll.ShortcutKeys = Keys.Control | Keys.A;
toolStripMenuItem_SelectAll.Size = new Size(328, 30);
toolStripMenuItem_SelectAll.Text = "全选";
toolStripMenuItem_SelectAll.Click += toolStripMenuItem_SelectAll_Click;
//
// toolStripMenuItem_CopyPreview // toolStripMenuItem_CopyPreview
// //
toolStripMenuItem_CopyPreview.Name = "toolStripMenuItem_CopyPreview"; toolStripMenuItem_CopyPreview.Name = "toolStripMenuItem_CopyPreview";
@@ -194,6 +186,22 @@
toolStripMenuItem_CopyPreview.Text = "复制预览图 (256x256)"; toolStripMenuItem_CopyPreview.Text = "复制预览图 (256x256)";
toolStripMenuItem_CopyPreview.Click += toolStripMenuItem_CopyPreview_Click; toolStripMenuItem_CopyPreview.Click += toolStripMenuItem_CopyPreview_Click;
// //
// toolStripMenuItem_AddFromClipboard
//
toolStripMenuItem_AddFromClipboard.Name = "toolStripMenuItem_AddFromClipboard";
toolStripMenuItem_AddFromClipboard.ShortcutKeys = Keys.Control | Keys.V;
toolStripMenuItem_AddFromClipboard.Size = new Size(328, 30);
toolStripMenuItem_AddFromClipboard.Text = "从剪贴板添加";
toolStripMenuItem_AddFromClipboard.Click += toolStripMenuItem_AddFromClipboard_Click;
//
// toolStripMenuItem_SelectAll
//
toolStripMenuItem_SelectAll.Name = "toolStripMenuItem_SelectAll";
toolStripMenuItem_SelectAll.ShortcutKeys = Keys.Control | Keys.A;
toolStripMenuItem_SelectAll.Size = new Size(328, 30);
toolStripMenuItem_SelectAll.Text = "全选";
toolStripMenuItem_SelectAll.Click += toolStripMenuItem_SelectAll_Click;
//
// toolStripSeparator4 // toolStripSeparator4
// //
toolStripSeparator4.Name = "toolStripSeparator4"; toolStripSeparator4.Name = "toolStripSeparator4";
@@ -242,14 +250,6 @@
imageList_SmallIcon.ImageSize = new Size(48, 48); imageList_SmallIcon.ImageSize = new Size(48, 48);
imageList_SmallIcon.TransparentColor = Color.Transparent; imageList_SmallIcon.TransparentColor = Color.Transparent;
// //
// toolStripMenuItem_AddFromClipboard
//
toolStripMenuItem_AddFromClipboard.Name = "toolStripMenuItem_AddFromClipboard";
toolStripMenuItem_AddFromClipboard.ShortcutKeys = Keys.Control | Keys.V;
toolStripMenuItem_AddFromClipboard.Size = new Size(328, 30);
toolStripMenuItem_AddFromClipboard.Text = "从剪贴板添加";
toolStripMenuItem_AddFromClipboard.Click += toolStripMenuItem_AddFromClipboard_Click;
//
// SpineListView // SpineListView
// //
AutoScaleDimensions = new SizeF(11F, 24F); AutoScaleDimensions = new SizeF(11F, 24F);

View File

@@ -12,16 +12,11 @@ using SpineViewer.Spine;
using System.Reflection; using System.Reflection;
using System.Diagnostics; using System.Diagnostics;
using System.Collections.Specialized; using System.Collections.Specialized;
using NLog;
namespace SpineViewer.Controls namespace SpineViewer.Controls
{ {
public partial class SpineListView : UserControl public partial class SpineListView : UserControl
{ {
/// <summary>
/// 显示骨骼信息的属性面板
/// </summary>
[Category("自定义"), Description("用于显示骨骼属性的属性页")]
public PropertyGrid? PropertyGrid { get; set; }
/// <summary> /// <summary>
/// Spine 列表只读视图, 访问时必须使用 lock 语句锁定视图本身 /// Spine 列表只读视图, 访问时必须使用 lock 语句锁定视图本身
/// </summary> /// </summary>
@@ -32,12 +27,23 @@ namespace SpineViewer.Controls
/// </summary> /// </summary>
private readonly List<Spine.Spine> spines = []; private readonly List<Spine.Spine> spines = [];
/// <summary>
/// 日志器
/// </summary>
protected readonly Logger logger = LogManager.GetCurrentClassLogger();
public SpineListView() public SpineListView()
{ {
InitializeComponent(); InitializeComponent();
Spines = spines.AsReadOnly(); Spines = spines.AsReadOnly();
} }
/// <summary>
/// 显示骨骼信息的属性面板
/// </summary>
[Category("自定义"), Description("用于显示骨骼属性的属性页")]
public PropertyGrid? PropertyGrid { get; set; }
/// <summary> /// <summary>
/// 选中的索引 /// 选中的索引
/// </summary> /// </summary>
@@ -87,12 +93,12 @@ namespace SpineViewer.Controls
} }
catch (Exception ex) catch (Exception ex)
{ {
Program.Logger.Error(ex.ToString()); logger.Error(ex.ToString());
Program.Logger.Error("Failed to load {} {}", result.SkelPath, result.AtlasPath); logger.Error("Failed to load {} {}", result.SkelPath, result.AtlasPath);
MessageBox.Error(ex.ToString(), "骨骼加载失败"); MessageBox.Error(ex.ToString(), "骨骼加载失败");
} }
Program.LogCurrentMemoryUsage(); logger.LogCurrentProcessMemoryUsage();
} }
/// <summary> /// <summary>
@@ -109,7 +115,7 @@ namespace SpineViewer.Controls
/// <summary> /// <summary>
/// 从结果批量添加 /// 从结果批量添加
/// </summary> /// </summary>
public void BatchAdd(Dialogs.BatchOpenSpineDialogResult result) private void BatchAdd(Dialogs.BatchOpenSpineDialogResult result)
{ {
var progressDialog = new Dialogs.ProgressDialog(); var progressDialog = new Dialogs.ProgressDialog();
progressDialog.DoWork += BatchAdd_Work; progressDialog.DoWork += BatchAdd_Work;
@@ -157,8 +163,8 @@ namespace SpineViewer.Controls
} }
catch (Exception ex) catch (Exception ex)
{ {
Program.Logger.Error(ex.ToString()); logger.Error(ex.ToString());
Program.Logger.Error("Failed to load {}", skelPath); logger.Error("Failed to load {}", skelPath);
error++; error++;
} }
@@ -176,11 +182,11 @@ namespace SpineViewer.Controls
}); });
if (error > 0) if (error > 0)
Program.Logger.Warn("Batch load {} successfully, {} failed", success, error); logger.Warn("Batch load {} successfully, {} failed", success, error);
else else
Program.Logger.Info("{} skel loaded successfully", success); logger.Info("{} skel loaded successfully", success);
Program.LogCurrentMemoryUsage(); logger.LogCurrentProcessMemoryUsage();
} }
/// <summary> /// <summary>
@@ -193,14 +199,14 @@ namespace SpineViewer.Controls
{ {
if (File.Exists(path)) if (File.Exists(path))
{ {
if (Spine.Spine.CommonSkelSuffix.Contains(Path.GetExtension(path).ToLower())) if (SpineHelper.CommonSkelSuffix.Contains(Path.GetExtension(path).ToLower()))
validPaths.Add(path); validPaths.Add(path);
} }
else if (Directory.Exists(path)) else if (Directory.Exists(path))
{ {
foreach (var file in Directory.EnumerateFiles(path, "*.*", SearchOption.AllDirectories)) foreach (var file in Directory.EnumerateFiles(path, "*.*", SearchOption.AllDirectories))
{ {
if (Spine.Spine.CommonSkelSuffix.Contains(Path.GetExtension(file).ToLower())) if (SpineHelper.CommonSkelSuffix.Contains(Path.GetExtension(file).ToLower()))
validPaths.Add(file); validPaths.Add(file);
} }
} }
@@ -213,11 +219,11 @@ namespace SpineViewer.Controls
if (MessageBox.Quest($"共发现 {validPaths.Count} 个可加载骨骼,数量较多,是否一次性全部加载?") == DialogResult.Cancel) if (MessageBox.Quest($"共发现 {validPaths.Count} 个可加载骨骼,数量较多,是否一次性全部加载?") == DialogResult.Cancel)
return; return;
} }
BatchAdd(new Dialogs.BatchOpenSpineDialogResult(Spine.Version.Auto, validPaths.ToArray())); BatchAdd(new Dialogs.BatchOpenSpineDialogResult(SpineVersion.Auto, validPaths.ToArray()));
} }
else if (validPaths.Count > 0) else if (validPaths.Count > 0)
{ {
Insert(new Dialogs.OpenSpineDialogResult(Spine.Version.Auto, validPaths[0])); Insert(new Dialogs.OpenSpineDialogResult(SpineVersion.Auto, validPaths[0]));
} }
} }
@@ -500,13 +506,16 @@ namespace SpineViewer.Controls
private void toolStripMenuItem_CopyPreview_Click(object sender, EventArgs e) private void toolStripMenuItem_CopyPreview_Click(object sender, EventArgs e)
{ {
var fileDropList = new StringCollection(); var fileDropList = new StringCollection();
var tempDir = Path.Combine(Path.GetTempPath(), Process.GetCurrentProcess().ProcessName);
Directory.CreateDirectory(tempDir);
lock (Spines) lock (Spines)
{ {
foreach (int i in listView.SelectedIndices) foreach (int i in listView.SelectedIndices)
{ {
var a = Process.GetCurrentProcess();
var spine = spines[i]; var spine = spines[i];
var path = Path.Combine(Program.TempDir, $"{spine.ID}.png"); var path = Path.Combine(tempDir, $"{spine.ID}.png");
spine.Preview.Save(path); spine.Preview.Save(path);
fileDropList.Add(path); fileDropList.Add(path);
} }

View File

@@ -607,7 +607,7 @@ namespace SpineViewer.Controls
lock (SpineListView.Spines) lock (SpineListView.Spines)
{ {
foreach (var spine in SpineListView.Spines) foreach (var spine in SpineListView.Spines)
spine.CurrentAnimation = spine.CurrentAnimation; spine.Track0Animation = spine.Track0Animation; // TODO: 多轨道重置
} }
} }
} }
@@ -619,7 +619,7 @@ namespace SpineViewer.Controls
lock (SpineListView.Spines) lock (SpineListView.Spines)
{ {
foreach (var spine in SpineListView.Spines) foreach (var spine in SpineListView.Spines)
spine.CurrentAnimation = spine.CurrentAnimation; spine.Track0Animation = spine.Track0Animation; // TODO: 多轨道重置
} }
} }
IsUpdating = true; IsUpdating = true;

View File

@@ -15,12 +15,20 @@ namespace SpineViewer.Dialogs
public AboutDialog() public AboutDialog()
{ {
InitializeComponent(); InitializeComponent();
Text = $"关于 {Program.Name}"; Text = $"关于 {ProgramName}";
label_Version.Text = $"v{InformationalVersion}"; label_Version.Text = $"v{InformationalVersion}";
} }
public string InformationalVersion => public string ProgramName => Process.GetCurrentProcess().ProcessName;
Assembly.GetExecutingAssembly().GetCustomAttribute<AssemblyInformationalVersionAttribute>()?.InformationalVersion;
public string InformationalVersion
=> Assembly.GetExecutingAssembly().GetCustomAttribute<AssemblyInformationalVersionAttribute>()?.InformationalVersion;
public string ProgramUrl
{
get => linkLabel_RepoUrl.Text;
set => linkLabel_RepoUrl.Text = value;
}
private void linkLabel_RepoUrl_LinkClicked(object sender, LinkLabelLinkClickedEventArgs e) private void linkLabel_RepoUrl_LinkClicked(object sender, LinkLabelLinkClickedEventArgs e)
{ {

View File

@@ -21,15 +21,15 @@ namespace SpineViewer.Dialogs
public BatchOpenSpineDialog() public BatchOpenSpineDialog()
{ {
InitializeComponent(); InitializeComponent();
comboBox_Version.DataSource = VersionHelper.Names.ToList(); comboBox_Version.DataSource = SpineHelper.Names.ToList();
comboBox_Version.DisplayMember = "Value"; comboBox_Version.DisplayMember = "Value";
comboBox_Version.ValueMember = "Key"; comboBox_Version.ValueMember = "Key";
comboBox_Version.SelectedValue = Spine.Version.Auto; comboBox_Version.SelectedValue = SpineVersion.Auto;
} }
private void button_Ok_Click(object sender, EventArgs e) private void button_Ok_Click(object sender, EventArgs e)
{ {
var version = (Spine.Version)comboBox_Version.SelectedValue; var version = (SpineVersion)comboBox_Version.SelectedValue;
var items = skelFileListBox.Items; var items = skelFileListBox.Items;
@@ -48,7 +48,7 @@ namespace SpineViewer.Dialogs
} }
} }
if (version != Spine.Version.Auto && !Spine.Spine.ImplementedVersions.Contains(version)) if (version != SpineVersion.Auto && !Spine.Spine.HasImplementation(version))
{ {
MessageBox.Info($"{version.GetName()} 版本尚未实现(咕咕咕~"); MessageBox.Info($"{version.GetName()} 版本尚未实现(咕咕咕~");
return; return;
@@ -67,12 +67,12 @@ namespace SpineViewer.Dialogs
/// <summary> /// <summary>
/// 批量打开对话框结果 /// 批量打开对话框结果
/// </summary> /// </summary>
public class BatchOpenSpineDialogResult(Spine.Version version, string[] skelPaths) public class BatchOpenSpineDialogResult(SpineVersion version, string[] skelPaths)
{ {
/// <summary> /// <summary>
/// 版本 /// 版本
/// </summary> /// </summary>
public Spine.Version Version => version; public SpineVersion Version => version;
/// <summary> /// <summary>
/// 路径列表 /// 路径列表

View File

@@ -44,7 +44,6 @@
button_Cancel = new Button(); button_Cancel = new Button();
label2 = new Label(); label2 = new Label();
skelFileListBox = new SpineViewer.Controls.SkelFileListBox(); skelFileListBox = new SpineViewer.Controls.SkelFileListBox();
openFileDialog_Skel = new OpenFileDialog();
panel.SuspendLayout(); panel.SuspendLayout();
tableLayoutPanel1.SuspendLayout(); tableLayoutPanel1.SuspendLayout();
flowLayoutPanel_TargetFormat.SuspendLayout(); flowLayoutPanel_TargetFormat.SuspendLayout();
@@ -238,14 +237,6 @@
skelFileListBox.Size = new Size(945, 264); skelFileListBox.Size = new Size(945, 264);
skelFileListBox.TabIndex = 20; skelFileListBox.TabIndex = 20;
// //
// openFileDialog_Skel
//
openFileDialog_Skel.AddExtension = false;
openFileDialog_Skel.AddToRecent = false;
openFileDialog_Skel.Filter = "skel 文件 (*.skel; *.json)|*.skel;*.json|二进制文件 (*.skel)|*.skel|文本文件 (*.json)|*.json|所有文件 (*.*)|*.*";
openFileDialog_Skel.Multiselect = true;
openFileDialog_Skel.Title = "批量选择skel文件";
//
// ConvertFileFormatDialog // ConvertFileFormatDialog
// //
AcceptButton = button_Ok; AcceptButton = button_Ok;
@@ -281,7 +272,6 @@
private TableLayoutPanel tableLayoutPanel2; private TableLayoutPanel tableLayoutPanel2;
private Button button_Ok; private Button button_Ok;
private Button button_Cancel; private Button button_Cancel;
private OpenFileDialog openFileDialog_Skel;
private Label label1; private Label label1;
private Label label2; private Label label2;
private FlowLayoutPanel flowLayoutPanel_TargetFormat; private FlowLayoutPanel flowLayoutPanel_TargetFormat;

View File

@@ -22,24 +22,24 @@ namespace SpineViewer.Dialogs
{ {
InitializeComponent(); InitializeComponent();
comboBox_SourceVersion.DataSource = VersionHelper.Names.ToList(); comboBox_SourceVersion.DataSource = SpineHelper.Names.ToList();
comboBox_SourceVersion.DisplayMember = "Value"; comboBox_SourceVersion.DisplayMember = "Value";
comboBox_SourceVersion.ValueMember = "Key"; comboBox_SourceVersion.ValueMember = "Key";
comboBox_SourceVersion.SelectedValue = Spine.Version.Auto; comboBox_SourceVersion.SelectedValue = SpineVersion.Auto;
// 目标版本不包含自动 // 目标版本不包含自动
var versionsWithoutAuto = VersionHelper.Names.ToDictionary(); var versionsWithoutAuto = SpineHelper.Names.ToDictionary();
versionsWithoutAuto.Remove(Spine.Version.Auto); versionsWithoutAuto.Remove(SpineVersion.Auto);
comboBox_TargetVersion.DataSource = versionsWithoutAuto.ToList(); comboBox_TargetVersion.DataSource = versionsWithoutAuto.ToList();
comboBox_TargetVersion.DisplayMember = "Value"; comboBox_TargetVersion.DisplayMember = "Value";
comboBox_TargetVersion.ValueMember = "Key"; comboBox_TargetVersion.ValueMember = "Key";
comboBox_TargetVersion.SelectedValue = Spine.Version.V38; comboBox_TargetVersion.SelectedValue = SpineVersion.V38;
} }
private void button_Ok_Click(object sender, EventArgs e) private void button_Ok_Click(object sender, EventArgs e)
{ {
var sourceVersion = (Spine.Version)comboBox_SourceVersion.SelectedValue; var sourceVersion = (SpineVersion)comboBox_SourceVersion.SelectedValue;
var targetVersion = (Spine.Version)comboBox_TargetVersion.SelectedValue; var targetVersion = (SpineVersion)comboBox_TargetVersion.SelectedValue;
var jsonTarget = radioButton_JsonTarget.Checked; var jsonTarget = radioButton_JsonTarget.Checked;
var items = skelFileListBox.Items; var items = skelFileListBox.Items;
@@ -59,13 +59,13 @@ namespace SpineViewer.Dialogs
} }
} }
if (sourceVersion != Spine.Version.Auto && !SkeletonConverter.ImplementedVersions.Contains(sourceVersion)) if (sourceVersion != SpineVersion.Auto && !SkeletonConverter.HasImplementation(sourceVersion))
{ {
MessageBox.Info($"{sourceVersion.GetName()} 版本尚未实现(咕咕咕~"); MessageBox.Info($"{sourceVersion.GetName()} 版本尚未实现(咕咕咕~");
return; return;
} }
if (!SkeletonConverter.ImplementedVersions.Contains(targetVersion)) if (!SkeletonConverter.HasImplementation(targetVersion))
{ {
MessageBox.Info($"{targetVersion.GetName()} 版本尚未实现(咕咕咕~"); MessageBox.Info($"{targetVersion.GetName()} 版本尚未实现(咕咕咕~");
return; return;
@@ -84,7 +84,7 @@ namespace SpineViewer.Dialogs
/// <summary> /// <summary>
/// 文件格式转换对话框结果包装类 /// 文件格式转换对话框结果包装类
/// </summary> /// </summary>
public class ConvertFileFormatDialogResult(string[] skelPaths, Spine.Version sourceVersion, Spine.Version targetVersion, bool jsonTarget) public class ConvertFileFormatDialogResult(string[] skelPaths, SpineVersion sourceVersion, SpineVersion targetVersion, bool jsonTarget)
{ {
/// <summary> /// <summary>
/// 骨骼文件路径列表 /// 骨骼文件路径列表
@@ -94,12 +94,12 @@ namespace SpineViewer.Dialogs
/// <summary> /// <summary>
/// 源版本 /// 源版本
/// </summary> /// </summary>
public Spine.Version SourceVersion => sourceVersion; public SpineVersion SourceVersion => sourceVersion;
/// <summary> /// <summary>
/// 目标版本 /// 目标版本
/// </summary> /// </summary>
public Spine.Version TargetVersion => targetVersion; public SpineVersion TargetVersion => targetVersion;
/// <summary> /// <summary>
/// 目标格式是否为 Json /// 目标格式是否为 Json

View File

@@ -117,9 +117,6 @@
<resheader name="writer"> <resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value> <value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader> </resheader>
<metadata name="openFileDialog_Skel.TrayLocation" type="System.Drawing.Point, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a">
<value>92, 26</value>
</metadata>
<assembly alias="System.Drawing" name="System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a" /> <assembly alias="System.Drawing" name="System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a" />
<data name="$this.Icon" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64"> <data name="$this.Icon" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
<value> <value>

View File

@@ -20,10 +20,10 @@ namespace SpineViewer.Dialogs
public OpenSpineDialog() public OpenSpineDialog()
{ {
InitializeComponent(); InitializeComponent();
comboBox_Version.DataSource = VersionHelper.Names.ToList(); comboBox_Version.DataSource = SpineHelper.Names.ToList();
comboBox_Version.DisplayMember = "Value"; comboBox_Version.DisplayMember = "Value";
comboBox_Version.ValueMember = "Key"; comboBox_Version.ValueMember = "Key";
comboBox_Version.SelectedValue = Spine.Version.Auto; comboBox_Version.SelectedValue = SpineVersion.Auto;
} }
private void OpenSpineDialog_Load(object sender, EventArgs e) private void OpenSpineDialog_Load(object sender, EventArgs e)
@@ -53,7 +53,7 @@ namespace SpineViewer.Dialogs
{ {
var skelPath = textBox_SkelPath.Text; var skelPath = textBox_SkelPath.Text;
var atlasPath = textBox_AtlasPath.Text; var atlasPath = textBox_AtlasPath.Text;
var version = (Spine.Version)comboBox_Version.SelectedValue; var version = (SpineVersion)comboBox_Version.SelectedValue;
if (!File.Exists(skelPath)) if (!File.Exists(skelPath))
{ {
@@ -79,7 +79,7 @@ namespace SpineViewer.Dialogs
atlasPath = Path.GetFullPath(atlasPath); atlasPath = Path.GetFullPath(atlasPath);
} }
if (version != Spine.Version.Auto && !Spine.Spine.ImplementedVersions.Contains(version)) if (version != SpineVersion.Auto && !Spine.Spine.HasImplementation(version))
{ {
MessageBox.Info($"{version.GetName()} 版本尚未实现(咕咕咕~"); MessageBox.Info($"{version.GetName()} 版本尚未实现(咕咕咕~");
return; return;
@@ -98,12 +98,12 @@ namespace SpineViewer.Dialogs
/// <summary> /// <summary>
/// 打开骨骼对话框结果 /// 打开骨骼对话框结果
/// </summary> /// </summary>
public class OpenSpineDialogResult(Spine.Version version, string skelPath, string? atlasPath = null) public class OpenSpineDialogResult(SpineVersion version, string skelPath, string? atlasPath = null)
{ {
/// <summary> /// <summary>
/// 版本 /// 版本
/// </summary> /// </summary>
public Spine.Version Version => version; public SpineVersion Version => version;
/// <summary> /// <summary>
/// skel 文件路径 /// skel 文件路径

View File

@@ -1,4 +1,5 @@
using System; using NLog;
using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.ComponentModel; using System.ComponentModel;
using System.Data; using System.Data;
@@ -12,6 +13,13 @@ namespace SpineViewer.Dialogs
{ {
public partial class ProgressDialog : Form public partial class ProgressDialog : Form
{ {
private Logger logger = LogManager.GetCurrentClassLogger();
public ProgressDialog()
{
InitializeComponent();
}
/// <summary> /// <summary>
/// BackgroundWorker.DoWork 接口暴露 /// BackgroundWorker.DoWork 接口暴露
/// </summary> /// </summary>
@@ -32,11 +40,6 @@ namespace SpineViewer.Dialogs
/// </summary> /// </summary>
public void RunWorkerAsync(object? argument) => backgroundWorker.RunWorkerAsync(argument); public void RunWorkerAsync(object? argument) => backgroundWorker.RunWorkerAsync(argument);
public ProgressDialog()
{
InitializeComponent();
}
private void backgroundWorker_ProgressChanged(object sender, ProgressChangedEventArgs e) private void backgroundWorker_ProgressChanged(object sender, ProgressChangedEventArgs e)
{ {
label_Tip.Text = e.UserState as string; label_Tip.Text = e.UserState as string;
@@ -47,7 +50,7 @@ namespace SpineViewer.Dialogs
{ {
if (e.Error != null) if (e.Error != null)
{ {
Program.Logger.Error(e.Error.ToString()); logger.Error(e.Error.ToString());
MessageBox.Error(e.Error.ToString(), "执行出错"); MessageBox.Error(e.Error.ToString(), "执行出错");
DialogResult = DialogResult.Abort; DialogResult = DialogResult.Abort;
} }

View File

@@ -14,40 +14,18 @@ namespace SpineViewer.Exporter
/// <summary> /// <summary>
/// 导出参数基类 /// 导出参数基类
/// </summary> /// </summary>
public abstract class ExportArgs public abstract class ExportArgs : ImplementationResolver<ExportArgs, ExportImplementationAttribute, ExportType>
{ {
/// <summary>
/// 实现类缓存
/// </summary>
private static readonly Dictionary<ExportType, Type> ImplementationTypes = [];
static ExportArgs()
{
var impTypes = Assembly.GetExecutingAssembly().GetTypes().Where(t => typeof(ExportArgs).IsAssignableFrom(t) && !t.IsAbstract);
foreach (var type in impTypes)
{
var attr = type.GetCustomAttribute<ExportImplementationAttribute>();
if (attr is not null)
{
if (ImplementationTypes.ContainsKey(attr.ExportType))
throw new InvalidOperationException($"Multiple implementations found: {attr.ExportType}");
ImplementationTypes[attr.ExportType] = type;
}
}
Program.Logger.Debug("Find export args implementations: [{}]", string.Join(", ", ImplementationTypes.Keys));
}
/// <summary> /// <summary>
/// 创建指定类型导出参数 /// 创建指定类型导出参数
/// </summary> /// </summary>
/// <param name="exportType">导出类型</param>
/// <param name="resolution">分辨率</param>
/// <param name="view">导出视图</param>
/// <param name="renderSelectedOnly">仅渲染选中</param>
/// <returns>返回与指定 <paramref name="exportType"/> 匹配的导出参数实例</returns>
public static ExportArgs New(ExportType exportType, Size resolution, SFML.Graphics.View view, bool renderSelectedOnly) public static ExportArgs New(ExportType exportType, Size resolution, SFML.Graphics.View view, bool renderSelectedOnly)
{ => New(exportType, [resolution, view, renderSelectedOnly]);
if (!ImplementationTypes.TryGetValue(exportType, out var type))
{
throw new NotImplementedException($"Not implemented type: {exportType}");
}
return (ExportArgs)Activator.CreateInstance(type, resolution, view, renderSelectedOnly);
}
public ExportArgs(Size resolution, SFML.Graphics.View view, bool renderSelectedOnly) public ExportArgs(Size resolution, SFML.Graphics.View view, bool renderSelectedOnly)
{ {
@@ -88,6 +66,14 @@ namespace SpineViewer.Exporter
[Category("[0] "), DisplayName(""), Description("")] [Category("[0] "), DisplayName(""), Description("")]
public bool RenderSelectedOnly { get; } public bool RenderSelectedOnly { get; }
/// <summary>
/// 背景颜色
/// </summary>
[Editor(typeof(SFMLColorEditor), typeof(UITypeEditor))]
[TypeConverter(typeof(SFMLColorConverter))]
[Category("[0] "), DisplayName(""), Description("使, #RRGGBBAA")]
public SFML.Graphics.Color BackgroundColor { get; set; } = SFML.Graphics.Color.Transparent;
/// <summary> /// <summary>
/// 检查参数是否合法并规范化参数值, 否则返回用户错误原因 /// 检查参数是否合法并规范化参数值, 否则返回用户错误原因
/// </summary> /// </summary>

View File

@@ -26,14 +26,9 @@ namespace SpineViewer.Exporter
/// 导出实现类标记 /// 导出实现类标记
/// </summary> /// </summary>
[AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = false)] [AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = false)]
public class ExportImplementationAttribute : Attribute public class ExportImplementationAttribute(ExportType exportType) : Attribute, IImplementationKey<ExportType>
{ {
public ExportType ExportType { get; } public ExportType ImplementationKey { get; private set; } = exportType;
public ExportImplementationAttribute(ExportType exportType)
{
ExportType = exportType;
}
} }
/// <summary> /// <summary>

View File

@@ -1,4 +1,5 @@
using SpineViewer.Spine; using NLog;
using SpineViewer.Spine;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.ComponentModel; using System.ComponentModel;
@@ -6,63 +7,36 @@ using System.Linq;
using System.Reflection; using System.Reflection;
using System.Text; using System.Text;
using System.Threading.Tasks; using System.Threading.Tasks;
using static System.Windows.Forms.VisualStyles.VisualStyleElement.StartPanel;
namespace SpineViewer.Exporter namespace SpineViewer.Exporter
{ {
/// <summary> /// <summary>
/// 导出器基类 /// 导出器基类
/// </summary> /// </summary>
public abstract class Exporter public abstract class Exporter(ExportArgs exportArgs) : ImplementationResolver<Exporter, ExportImplementationAttribute, ExportType>
{ {
/// <summary> /// <summary>
/// 实现类缓存 /// 创建指定类型导出器
/// </summary> /// </summary>
private static readonly Dictionary<ExportType, Type> ImplementationTypes = []; /// <param name="exportType">导出类型</param>
/// <param name="exportArgs">与 <paramref name="exportType"/> 匹配的导出参数</param>
static Exporter() /// <returns>与 <paramref name="exportType"/> 匹配的导出器</returns>
{ public static Exporter New(ExportType exportType, ExportArgs exportArgs) => New(exportType, [exportArgs]);
var impTypes = Assembly.GetExecutingAssembly().GetTypes().Where(t => typeof(Exporter).IsAssignableFrom(t) && !t.IsAbstract);
foreach (var type in impTypes)
{
var attr = type.GetCustomAttribute<ExportImplementationAttribute>();
if (attr is not null)
{
if (ImplementationTypes.ContainsKey(attr.ExportType))
throw new InvalidOperationException($"Multiple implementations found: {attr.ExportType}");
ImplementationTypes[attr.ExportType] = type;
}
}
Program.Logger.Debug("Find exporter implementations: [{}]", string.Join(", ", ImplementationTypes.Keys));
}
/// <summary> /// <summary>
/// 创建指定类型导出参数 /// 日志器
/// </summary> /// </summary>
public static Exporter New(ExportType exportType, ExportArgs exportArgs) protected Logger logger = LogManager.GetCurrentClassLogger();
{
if (!ImplementationTypes.TryGetValue(exportType, out var type))
{
throw new NotImplementedException($"Not implemented type: {exportType}");
}
return (Exporter)Activator.CreateInstance(type, exportArgs);
}
/// <summary> /// <summary>
/// 导出参数 /// 导出参数
/// </summary> /// </summary>
public ExportArgs ExportArgs { get; } public ExportArgs ExportArgs { get; } = exportArgs;
/// <summary> /// <summary>
/// 可用于文件名的时间戳字符串 /// 可用于文件名的时间戳字符串
/// </summary> /// </summary>
protected readonly string timestamp; protected readonly string timestamp = DateTime.Now.ToString("yyMMddHHmmss");
public Exporter(ExportArgs exportArgs)
{
ExportArgs = exportArgs;
timestamp = DateTime.Now.ToString("yyMMddHHmmss");
}
/// <summary> /// <summary>
/// 获取供渲染的 SFML.Graphics.RenderTexture /// 获取供渲染的 SFML.Graphics.RenderTexture
@@ -82,7 +56,7 @@ namespace SpineViewer.Exporter
{ {
// tex 必须临时创建, 随用随取, 防止出现跨线程的情况 // tex 必须临时创建, 随用随取, 防止出现跨线程的情况
using var tex = GetRenderTexture(); using var tex = GetRenderTexture();
tex.Clear(SFML.Graphics.Color.Transparent); tex.Clear(ExportArgs.BackgroundColor);
tex.Draw(spine); tex.Draw(spine);
tex.Display(); tex.Display();
return new(tex.Texture.CopyToImage()); return new(tex.Texture.CopyToImage());
@@ -95,7 +69,7 @@ namespace SpineViewer.Exporter
{ {
// tex 必须临时创建, 随用随取, 防止出现跨线程的情况 // tex 必须临时创建, 随用随取, 防止出现跨线程的情况
using var tex = GetRenderTexture(); using var tex = GetRenderTexture();
tex.Clear(SFML.Graphics.Color.Transparent); tex.Clear(ExportArgs.BackgroundColor);
foreach (var spine in spinesToRender) tex.Draw(spine); foreach (var spine in spinesToRender) tex.Draw(spine);
tex.Display(); tex.Display();
return new(tex.Texture.CopyToImage()); return new(tex.Texture.CopyToImage());
@@ -123,7 +97,7 @@ namespace SpineViewer.Exporter
if (ExportArgs.ExportSingle) ExportSingle(spinesToRender, worker); if (ExportArgs.ExportSingle) ExportSingle(spinesToRender, worker);
else ExportIndividual(spinesToRender, worker); else ExportIndividual(spinesToRender, worker);
Program.LogCurrentMemoryUsage(); logger.LogCurrentProcessMemoryUsage();
} }
} }
} }

View File

@@ -15,8 +15,11 @@ namespace SpineViewer.Exporter.Implementations.ExportArgs
{ {
public GifExportArgs(Size resolution, SFML.Graphics.View view, bool renderSelectedOnly) : base(resolution, view, renderSelectedOnly) public GifExportArgs(Size resolution, SFML.Graphics.View view, bool renderSelectedOnly) : base(resolution, view, renderSelectedOnly)
{ {
// 给一个纯白的背景
BackgroundColor = new(255, 255, 255, 0);
// GIF 的帧率不能太高, 超过 50 帧反而会变慢 // GIF 的帧率不能太高, 超过 50 帧反而会变慢
FPS = 25; FPS = 12;
} }
/// <summary> /// <summary>

View File

@@ -0,0 +1,35 @@
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace SpineViewer.Exporter.Implementations.ExportArgs
{
/// <summary>
/// MP4 导出参数
/// </summary>
[ExportImplementation(ExportType.MP4)]
public class Mp4ExportArgs : VideoExportArgs
{
public Mp4ExportArgs(Size resolution, SFML.Graphics.View view, bool renderSelectedOnly) : base(resolution, view, renderSelectedOnly)
{
// MP4 默认用绿幕
BackgroundColor = new(0, 255, 0, 0);
}
/// <summary>
/// CRF
/// </summary>
[Category("[2] MP4 "), DisplayName("CRF"), Description("Constant Rate Factor, 0-63, 18-28, 23, ")]
public int CRF { get => crf; set => crf = Math.Clamp(value, 0, 63); }
private int crf = 23;
/// <summary>
/// 编码器 TODO: 增加其他编码器
/// </summary>
[Category("[2] MP4 "), DisplayName(""), Description("使")]
public string Codec { get => "libx264"; }
}
}

View File

@@ -35,8 +35,8 @@ namespace SpineViewer.Exporter.Implementations.Exporter
} }
catch (Exception ex) catch (Exception ex)
{ {
Program.Logger.Error(ex.ToString()); logger.Error(ex.ToString());
Program.Logger.Error("Failed to save single frame"); logger.Error("Failed to save single frame");
} }
worker?.ReportProgress(100, $"已处理 1/1"); worker?.ReportProgress(100, $"已处理 1/1");
} }
@@ -68,8 +68,8 @@ namespace SpineViewer.Exporter.Implementations.Exporter
} }
catch (Exception ex) catch (Exception ex)
{ {
Program.Logger.Error(ex.ToString()); logger.Error(ex.ToString());
Program.Logger.Error("Failed to save frame {} {}", savePath, spine.SkelPath); logger.Error("Failed to save frame {} {}", savePath, spine.SkelPath);
error++; error++;
} }
@@ -77,9 +77,9 @@ namespace SpineViewer.Exporter.Implementations.Exporter
} }
if (error > 0) if (error > 0)
Program.Logger.Warn("Frames save {} successfully, {} failed", success, error); logger.Warn("Frames save {} successfully, {} failed", success, error);
else else
Program.Logger.Info("{} frames saved successfully", success); logger.Info("{} frames saved successfully", success);
} }
} }

View File

@@ -37,8 +37,8 @@ namespace SpineViewer.Exporter.Implementations.Exporter
} }
catch (Exception ex) catch (Exception ex)
{ {
Program.Logger.Error(ex.ToString()); logger.Error(ex.ToString());
Program.Logger.Error("Failed to save frame {}", savePath); logger.Error("Failed to save frame {}", savePath);
} }
finally finally
{ {
@@ -72,8 +72,8 @@ namespace SpineViewer.Exporter.Implementations.Exporter
} }
catch (Exception ex) catch (Exception ex)
{ {
Program.Logger.Error(ex.ToString()); logger.Error(ex.ToString());
Program.Logger.Error("Failed to save frame {} {}", savePath, spine.SkelPath); logger.Error("Failed to save frame {} {}", savePath, spine.SkelPath);
} }
finally finally
{ {

View File

@@ -8,6 +8,7 @@ using System.Linq;
using System.Text; using System.Text;
using System.Threading.Tasks; using System.Threading.Tasks;
using FFMpegCore.Arguments; using FFMpegCore.Arguments;
using System.Diagnostics;
namespace SpineViewer.Exporter.Implementations.Exporter namespace SpineViewer.Exporter.Implementations.Exporter
{ {
@@ -30,17 +31,19 @@ namespace SpineViewer.Exporter.Implementations.Exporter
var videoFramesSource = new RawVideoPipeSource(GetFrames(spinesToRender, worker)) { FrameRate = args.FPS }; var videoFramesSource = new RawVideoPipeSource(GetFrames(spinesToRender, worker)) { FrameRate = args.FPS };
try try
{ {
FFMpegArguments var ffmpegArgs = FFMpegArguments
.FromPipeInput(videoFramesSource) .FromPipeInput(videoFramesSource)
.OutputToFile(savePath, true, options => options .OutputToFile(savePath, true, options => options
.ForceFormat("gif") .ForceFormat("gif")
.WithCustomArgument(args.FFMpegCoreCustomArguments)) .WithCustomArgument(args.FFMpegCoreCustomArguments));
.ProcessSynchronously();
logger.Info("FFMpeg arguments: {}", ffmpegArgs.Arguments);
ffmpegArgs.ProcessSynchronously();
} }
catch (Exception ex) catch (Exception ex)
{ {
Program.Logger.Error(ex.ToString()); logger.Error(ex.ToString());
Program.Logger.Error("Failed to export gif {}", savePath); logger.Error("Failed to export gif {}", savePath);
} }
} }
@@ -58,17 +61,19 @@ namespace SpineViewer.Exporter.Implementations.Exporter
var videoFramesSource = new RawVideoPipeSource(GetFrames(spine, worker)) { FrameRate = args.FPS }; var videoFramesSource = new RawVideoPipeSource(GetFrames(spine, worker)) { FrameRate = args.FPS };
try try
{ {
FFMpegArguments var ffmpegArgs = FFMpegArguments
.FromPipeInput(videoFramesSource) .FromPipeInput(videoFramesSource)
.OutputToFile(savePath, true, options => options .OutputToFile(savePath, true, options => options
.ForceFormat("gif") .ForceFormat("gif")
.WithCustomArgument(args.FFMpegCoreCustomArguments)) .WithCustomArgument(args.FFMpegCoreCustomArguments));
.ProcessSynchronously();
logger.Info("FFMpeg arguments: {}", ffmpegArgs.Arguments);
ffmpegArgs.ProcessSynchronously();
} }
catch (Exception ex) catch (Exception ex)
{ {
Program.Logger.Error(ex.ToString()); logger.Error(ex.ToString());
Program.Logger.Error("Failed to export gif {} {}", savePath, spine.SkelPath); logger.Error("Failed to export gif {} {}", savePath, spine.SkelPath);
} }
} }
} }

View File

@@ -0,0 +1,85 @@
using FFMpegCore.Pipes;
using FFMpegCore;
using SpineViewer.Exporter.Implementations.ExportArgs;
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using FFMpegCore.Arguments;
using System.Diagnostics;
namespace SpineViewer.Exporter.Implementations.Exporter
{
/// <summary>
/// MP4 导出器
/// </summary>
[ExportImplementation(ExportType.MP4)]
public class Mp4Exporter : VideoExporter
{
public Mp4Exporter(Mp4ExportArgs exportArgs) : base(exportArgs) { }
protected override void ExportSingle(Spine.Spine[] spinesToRender, BackgroundWorker? worker = null)
{
var args = (Mp4ExportArgs)ExportArgs;
// 导出单个时必定提供输出文件夹
var filename = $"{timestamp}_{args.FPS:f0}_{args.CRF}.mp4";
var savePath = Path.Combine(args.OutputDir, filename);
var videoFramesSource = new RawVideoPipeSource(GetFrames(spinesToRender, worker)) { FrameRate = args.FPS };
try
{
var ffmpegArgs = FFMpegArguments
.FromPipeInput(videoFramesSource)
.OutputToFile(savePath, true, options => options
.ForceFormat("mp4")
.WithVideoCodec(args.Codec)
.WithConstantRateFactor(args.CRF)
.WithFastStart());
logger.Info("FFMpeg arguments: {}", ffmpegArgs.Arguments);
ffmpegArgs.ProcessSynchronously();
}
catch (Exception ex)
{
logger.Error(ex.ToString());
logger.Error("Failed to export mp4 {}", savePath);
}
}
protected override void ExportIndividual(Spine.Spine[] spinesToRender, BackgroundWorker? worker = null)
{
var args = (Mp4ExportArgs)ExportArgs;
foreach (var spine in spinesToRender)
{
if (worker?.CancellationPending == true) break; // 取消的日志在 GetFrames 里输出
// 如果提供了输出文件夹, 则全部导出到输出文件夹, 否则导出到各自的文件夹下
var filename = $"{spine.Name}_{timestamp}_{args.FPS:f0}_{args.CRF}.mp4";
var savePath = Path.Combine(args.OutputDir ?? spine.AssetsDir, filename);
var videoFramesSource = new RawVideoPipeSource(GetFrames(spine, worker)) { FrameRate = args.FPS };
try
{
var ffmpegArgs = FFMpegArguments
.FromPipeInput(videoFramesSource)
.OutputToFile(savePath, true, options => options
.ForceFormat("mp4")
.WithVideoCodec(args.Codec)
.WithConstantRateFactor(args.CRF)
.WithFastStart());
logger.Info("FFMpeg arguments: {}", ffmpegArgs.Arguments);
ffmpegArgs.ProcessSynchronously();
}
catch (Exception ex)
{
logger.Error(ex.ToString());
logger.Error("Failed to export mp4 {} {}", savePath, spine.SkelPath);
}
}
}
}
}

View File

@@ -23,9 +23,9 @@ namespace SpineViewer.Exporter.Implementations.Exporter
{ {
var args = (VideoExportArgs)ExportArgs; var args = (VideoExportArgs)ExportArgs;
// 独立导出时如果 args.Duration 小于 0 则使用自己的动画时长 // 独立导出时如果 args.Duration 小于 0 则使用 Track0 的动画时长
var duration = args.Duration; var duration = args.Duration;
if (duration < 0) duration = spine.CurrentAnimationDuration; if (duration < 0) duration = spine.GetAnimationDuration(spine.Track0Animation); // TODO: 也许可以使用所有轨道的最大值
float delta = 1f / args.FPS; float delta = 1f / args.FPS;
int total = Math.Max(1, (int)(duration * args.FPS)); // 至少导出 1 帧 int total = Math.Max(1, (int)(duration * args.FPS)); // 至少导出 1 帧
@@ -35,7 +35,7 @@ namespace SpineViewer.Exporter.Implementations.Exporter
{ {
if (worker?.CancellationPending == true) if (worker?.CancellationPending == true)
{ {
Program.Logger.Info("Export cancelled"); logger.Info("Export cancelled");
break; break;
} }
@@ -61,7 +61,7 @@ namespace SpineViewer.Exporter.Implementations.Exporter
{ {
if (worker?.CancellationPending == true) if (worker?.CancellationPending == true)
{ {
Program.Logger.Info("Export cancelled"); logger.Info("Export cancelled");
break; break;
} }
@@ -75,7 +75,7 @@ namespace SpineViewer.Exporter.Implementations.Exporter
public override void Export(Spine.Spine[] spines, BackgroundWorker? worker = null) public override void Export(Spine.Spine[] spines, BackgroundWorker? worker = null)
{ {
// 导出视频格式需要把模型时间都重置到 0 // 导出视频格式需要把模型时间都重置到 0
foreach (var spine in spines) spine.CurrentAnimation = spine.CurrentAnimation; foreach (var spine in spines) spine.Track0Animation = spine.Track0Animation; // TODO: 多轨道重置
base.Export(spines, worker); base.Export(spines, worker);
} }
} }

View File

@@ -13,21 +13,91 @@ namespace SpineViewer.Exporter
public class SFMLImageFileSuffixConverter : StringConverter public class SFMLImageFileSuffixConverter : StringConverter
{ {
private readonly string[] supportedFileSuffix = [".png", ".jpg", ".tga", ".bmp"]; private readonly string[] supportedFileSuffix = [".png", ".jpg", ".tga", ".bmp"];
public override bool GetStandardValuesSupported(ITypeDescriptorContext? context)
{
// 支持标准值列表
return true;
}
public override bool GetStandardValuesExclusive(ITypeDescriptorContext? context) public override bool GetStandardValuesSupported(ITypeDescriptorContext? context) => true;
{
// 排他模式,只有下拉列表中的值可选 public override bool GetStandardValuesExclusive(ITypeDescriptorContext? context) => true;
return true;
}
public override StandardValuesCollection? GetStandardValues(ITypeDescriptorContext? context) public override StandardValuesCollection? GetStandardValues(ITypeDescriptorContext? context)
{ {
return new StandardValuesCollection(supportedFileSuffix); return new StandardValuesCollection(supportedFileSuffix);
} }
} }
public class SFMLColorConverter : ExpandableObjectConverter
{
private class SFMLColorPropertyDescriptor : SimplePropertyDescriptor
{
public SFMLColorPropertyDescriptor(Type componentType, string name, Type propertyType) : base(componentType, name, propertyType) { }
public override object? GetValue(object? component)
{
return component?.GetType().GetField(Name)?.GetValue(component) ?? default;
}
public override void SetValue(object? component, object? value)
{
component?.GetType().GetField(Name)?.SetValue(component, value);
}
}
public override bool CanConvertFrom(ITypeDescriptorContext? context, Type sourceType)
{
return sourceType == typeof(string) || base.CanConvertFrom(context, sourceType);
}
public override object? ConvertFrom(ITypeDescriptorContext? context, CultureInfo? culture, object value)
{
if (value is string s)
{
s = s.Trim();
if (s.StartsWith("#") && s.Length == 9)
{
try
{
// 解析 R, G, B, A 分量注意16进制解析
byte r = byte.Parse(s.Substring(1, 2), NumberStyles.HexNumber);
byte g = byte.Parse(s.Substring(3, 2), NumberStyles.HexNumber);
byte b = byte.Parse(s.Substring(5, 2), NumberStyles.HexNumber);
byte a = byte.Parse(s.Substring(7, 2), NumberStyles.HexNumber);
return new SFML.Graphics.Color(r, g, b, a);
}
catch (Exception ex)
{
throw new FormatException("无法解析颜色,确保格式为 #RRGGBBAA", ex);
}
}
throw new FormatException("格式错误,正确格式为 #RRGGBBAA");
}
return base.ConvertFrom(context, culture, value);
}
public override bool CanConvertTo(ITypeDescriptorContext? context, Type? destinationType)
{
return destinationType == typeof(string) || base.CanConvertTo(context, destinationType);
}
public override object? ConvertTo(ITypeDescriptorContext? context, CultureInfo? culture, object? value, Type destinationType)
{
if (destinationType == typeof(string) && value is SFML.Graphics.Color color)
return $"#{color.R:X2}{color.G:X2}{color.B:X2}{color.A:X2}";
return base.ConvertTo(context, culture, value, destinationType);
}
public override PropertyDescriptorCollection GetProperties(ITypeDescriptorContext? context, object value, Attribute[]? attributes)
{
// 自定义属性集合
var properties = new List<PropertyDescriptor>
{
// 定义 R, G, B, A 四个字段的描述器
new SFMLColorPropertyDescriptor(typeof(SFML.Graphics.Color), "R", typeof(byte)),
new SFMLColorPropertyDescriptor(typeof(SFML.Graphics.Color), "G", typeof(byte)),
new SFMLColorPropertyDescriptor(typeof(SFML.Graphics.Color), "B", typeof(byte)),
new SFMLColorPropertyDescriptor(typeof(SFML.Graphics.Color), "A", typeof(byte))
};
// 返回自定义属性集合
return new PropertyDescriptorCollection(properties.ToArray());
}
}
} }

View File

@@ -0,0 +1,66 @@
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Drawing.Design;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace SpineViewer.Exporter
{
class SFMLColorEditor : UITypeEditor
{
public override bool GetPaintValueSupported(ITypeDescriptorContext? context) => true;
public override void PaintValue(PaintValueEventArgs e)
{
if (e.Value is SFML.Graphics.Color color)
{
// 定义颜色和透明度的绘制区域
var colorBox = new Rectangle(e.Bounds.X, e.Bounds.Y, e.Bounds.Width / 2, e.Bounds.Height);
var alphaBox = new Rectangle(e.Bounds.X + e.Bounds.Width / 2, e.Bounds.Y, e.Bounds.Width / 2, e.Bounds.Height);
// 转换为 System.Drawing.Color
var drawColor = Color.FromArgb(color.A, color.R, color.G, color.B);
// 绘制纯颜色RGB 部分)
using (var brush = new SolidBrush(Color.FromArgb(color.R, color.G, color.B)))
{
e.Graphics.FillRectangle(brush, colorBox);
e.Graphics.DrawRectangle(Pens.Black, colorBox);
}
// 绘制带透明度效果的颜色
using (var checkerBrush = CreateTransparencyBrush())
{
e.Graphics.FillRectangle(checkerBrush, alphaBox); // 背景棋盘格
}
using (var brush = new SolidBrush(drawColor))
{
e.Graphics.FillRectangle(brush, alphaBox); // 叠加透明颜色
e.Graphics.DrawRectangle(Pens.Black, alphaBox);
}
}
else
{
base.PaintValue(e);
}
}
// 创建一个透明背景的棋盘格图案画刷
private static TextureBrush CreateTransparencyBrush()
{
var bitmap = new Bitmap(8, 8);
using (var g = Graphics.FromImage(bitmap))
{
g.Clear(Color.White);
using (var grayBrush = new SolidBrush(Color.LightGray))
{
g.FillRectangle(grayBrush, 0, 0, 4, 4);
g.FillRectangle(grayBrush, 4, 4, 4, 4);
}
}
return new TextureBrush(bitmap);
}
}
}

View File

@@ -0,0 +1,66 @@
using SpineViewer.Exporter;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Text;
using System.Threading.Tasks;
namespace SpineViewer
{
public interface IImplementationKey<TKey>
{
TKey ImplementationKey { get; }
}
/// <summary>
/// 可以使用反射查找基类关联的所有实现类
/// </summary>
/// <typeparam name="TBase">所有实现类的基类型</typeparam>
/// <typeparam name="TAttr">实现类类型属性标记类型</typeparam>
/// /// <typeparam name="TKey">实现类类型标记类型</typeparam>
public abstract class ImplementationResolver<TBase, TAttr, TKey> where TAttr : Attribute, IImplementationKey<TKey>
{
/// <summary>
/// 实现类型缓存
/// </summary>
private static readonly Dictionary<TKey, Type> ImplementationTypes = new();
static ImplementationResolver()
{
var baseType = typeof(TBase);
var impTypes = Assembly.GetExecutingAssembly().GetTypes().Where(t => baseType.IsAssignableFrom(t) && !t.IsAbstract);
foreach (var type in impTypes)
{
var attr = type.GetCustomAttribute<TAttr>();
if (attr is not null)
{
var key = attr.ImplementationKey;
if (ImplementationTypes.ContainsKey(key))
throw new InvalidOperationException($"Multiple implementations found for key: {key}");
ImplementationTypes[key] = type;
}
}
NLog.LogManager.GetCurrentClassLogger().Debug("Found implementations for {}: {}", baseType, string.Join(", ", ImplementationTypes.Keys));
}
/// <summary>
/// 判断某种类型是否实现
/// </summary>
public static bool HasImplementation(TKey key) => ImplementationTypes.ContainsKey(key);
/// <summary>
/// 根据实现类键和参数创建实例
/// </summary>
/// <param name="impKey"></param>
/// <param name="args"></param>
/// <returns></returns>
/// <exception cref="NotImplementedException"></exception>
protected static TBase New(TKey impKey, object?[] args)
{
if (!ImplementationTypes.TryGetValue(impKey, out var type))
throw new NotImplementedException($"Not implemented type for {typeof(TBase)}: {impKey}");
return (TBase)Activator.CreateInstance(type, args);
}
}
}

View File

@@ -61,9 +61,9 @@
spineListView = new SpineViewer.Controls.SpineListView(); spineListView = new SpineViewer.Controls.SpineListView();
propertyGrid_Spine = new PropertyGrid(); propertyGrid_Spine = new PropertyGrid();
splitContainer_Config = new SplitContainer(); splitContainer_Config = new SplitContainer();
groupBox_SkelConfig = new GroupBox();
groupBox_PreviewConfig = new GroupBox(); groupBox_PreviewConfig = new GroupBox();
propertyGrid_Previewer = new PropertyGrid(); propertyGrid_Previewer = new PropertyGrid();
groupBox_SkelConfig = new GroupBox();
groupBox_Preview = new GroupBox(); groupBox_Preview = new GroupBox();
spinePreviewer = new SpineViewer.Controls.SpinePreviewer(); spinePreviewer = new SpineViewer.Controls.SpinePreviewer();
panel_MainForm = new Panel(); panel_MainForm = new Panel();
@@ -86,8 +86,8 @@
splitContainer_Config.Panel1.SuspendLayout(); splitContainer_Config.Panel1.SuspendLayout();
splitContainer_Config.Panel2.SuspendLayout(); splitContainer_Config.Panel2.SuspendLayout();
splitContainer_Config.SuspendLayout(); splitContainer_Config.SuspendLayout();
groupBox_SkelConfig.SuspendLayout();
groupBox_PreviewConfig.SuspendLayout(); groupBox_PreviewConfig.SuspendLayout();
groupBox_SkelConfig.SuspendLayout();
groupBox_Preview.SuspendLayout(); groupBox_Preview.SuspendLayout();
panel_MainForm.SuspendLayout(); panel_MainForm.SuspendLayout();
SuspendLayout(); SuspendLayout();
@@ -99,7 +99,7 @@
menuStrip.Items.AddRange(new ToolStripItem[] { toolStripMenuItem_File, toolStripMenuItem_Tool, toolStripMenuItem_Download, toolStripMenuItem_Help }); menuStrip.Items.AddRange(new ToolStripItem[] { toolStripMenuItem_File, toolStripMenuItem_Tool, toolStripMenuItem_Download, toolStripMenuItem_Help });
menuStrip.Location = new Point(0, 0); menuStrip.Location = new Point(0, 0);
menuStrip.Name = "menuStrip"; menuStrip.Name = "menuStrip";
menuStrip.Size = new Size(1748, 32); menuStrip.Size = new Size(1778, 32);
menuStrip.TabIndex = 0; menuStrip.TabIndex = 0;
menuStrip.Text = "菜单"; menuStrip.Text = "菜单";
// //
@@ -114,88 +114,91 @@
// //
toolStripMenuItem_Open.Name = "toolStripMenuItem_Open"; toolStripMenuItem_Open.Name = "toolStripMenuItem_Open";
toolStripMenuItem_Open.ShortcutKeys = Keys.Control | Keys.O; toolStripMenuItem_Open.ShortcutKeys = Keys.Control | Keys.O;
toolStripMenuItem_Open.Size = new Size(254, 34); toolStripMenuItem_Open.Size = new Size(270, 34);
toolStripMenuItem_Open.Text = "打开(&O)..."; toolStripMenuItem_Open.Text = "打开(&O)...";
toolStripMenuItem_Open.Click += toolStripMenuItem_Open_Click; toolStripMenuItem_Open.Click += toolStripMenuItem_Open_Click;
// //
// toolStripMenuItem_BatchOpen // toolStripMenuItem_BatchOpen
// //
toolStripMenuItem_BatchOpen.Name = "toolStripMenuItem_BatchOpen"; toolStripMenuItem_BatchOpen.Name = "toolStripMenuItem_BatchOpen";
toolStripMenuItem_BatchOpen.Size = new Size(254, 34); toolStripMenuItem_BatchOpen.Size = new Size(270, 34);
toolStripMenuItem_BatchOpen.Text = "批量打开(&B)..."; toolStripMenuItem_BatchOpen.Text = "批量打开(&B)...";
toolStripMenuItem_BatchOpen.Click += toolStripMenuItem_BatchOpen_Click; toolStripMenuItem_BatchOpen.Click += toolStripMenuItem_BatchOpen_Click;
// //
// toolStripSeparator1 // toolStripSeparator1
// //
toolStripSeparator1.Name = "toolStripSeparator1"; toolStripSeparator1.Name = "toolStripSeparator1";
toolStripSeparator1.Size = new Size(251, 6); toolStripSeparator1.Size = new Size(267, 6);
// //
// toolStripMenuItem_Export // toolStripMenuItem_Export
// //
toolStripMenuItem_Export.DropDownItems.AddRange(new ToolStripItem[] { toolStripMenuItem_ExportFrame, toolStripMenuItem_ExportFrameSequence, toolStripMenuItem_ExportGif, toolStripMenuItem_ExportMkv, toolStripMenuItem_ExportMp4, toolStripMenuItem_ExportMov, toolStripMenuItem_ExportWebm }); toolStripMenuItem_Export.DropDownItems.AddRange(new ToolStripItem[] { toolStripMenuItem_ExportFrame, toolStripMenuItem_ExportFrameSequence, toolStripMenuItem_ExportGif, toolStripMenuItem_ExportMkv, toolStripMenuItem_ExportMp4, toolStripMenuItem_ExportMov, toolStripMenuItem_ExportWebm });
toolStripMenuItem_Export.Name = "toolStripMenuItem_Export"; toolStripMenuItem_Export.Name = "toolStripMenuItem_Export";
toolStripMenuItem_Export.Size = new Size(254, 34); toolStripMenuItem_Export.Size = new Size(270, 34);
toolStripMenuItem_Export.Text = "导出(&E)"; toolStripMenuItem_Export.Text = "导出(&E)";
// //
// toolStripMenuItem_ExportFrame // toolStripMenuItem_ExportFrame
// //
toolStripMenuItem_ExportFrame.Name = "toolStripMenuItem_ExportFrame"; toolStripMenuItem_ExportFrame.Name = "toolStripMenuItem_ExportFrame";
toolStripMenuItem_ExportFrame.Size = new Size(194, 34); toolStripMenuItem_ExportFrame.Size = new Size(270, 34);
toolStripMenuItem_ExportFrame.Text = "单帧画面..."; toolStripMenuItem_ExportFrame.Text = "单帧画面...";
toolStripMenuItem_ExportFrame.Click += toolStripMenuItem_Export_Click; toolStripMenuItem_ExportFrame.Click += toolStripMenuItem_Export_Click;
// //
// toolStripMenuItem_ExportFrameSequence // toolStripMenuItem_ExportFrameSequence
// //
toolStripMenuItem_ExportFrameSequence.Name = "toolStripMenuItem_ExportFrameSequence"; toolStripMenuItem_ExportFrameSequence.Name = "toolStripMenuItem_ExportFrameSequence";
toolStripMenuItem_ExportFrameSequence.Size = new Size(194, 34); toolStripMenuItem_ExportFrameSequence.Size = new Size(270, 34);
toolStripMenuItem_ExportFrameSequence.Text = "帧序列..."; toolStripMenuItem_ExportFrameSequence.Text = "帧序列...";
toolStripMenuItem_ExportFrameSequence.Click += toolStripMenuItem_Export_Click; toolStripMenuItem_ExportFrameSequence.Click += toolStripMenuItem_Export_Click;
// //
// toolStripMenuItem_ExportGif // toolStripMenuItem_ExportGif
// //
toolStripMenuItem_ExportGif.Name = "toolStripMenuItem_ExportGif"; toolStripMenuItem_ExportGif.Name = "toolStripMenuItem_ExportGif";
toolStripMenuItem_ExportGif.Size = new Size(194, 34); toolStripMenuItem_ExportGif.Size = new Size(270, 34);
toolStripMenuItem_ExportGif.Text = "GIF..."; toolStripMenuItem_ExportGif.Text = "GIF...";
toolStripMenuItem_ExportGif.Click += toolStripMenuItem_Export_Click; toolStripMenuItem_ExportGif.Click += toolStripMenuItem_Export_Click;
// //
// toolStripMenuItem_ExportMkv // toolStripMenuItem_ExportMkv
// //
toolStripMenuItem_ExportMkv.Name = "toolStripMenuItem_ExportMkv"; toolStripMenuItem_ExportMkv.Name = "toolStripMenuItem_ExportMkv";
toolStripMenuItem_ExportMkv.Size = new Size(194, 34); toolStripMenuItem_ExportMkv.Size = new Size(270, 34);
toolStripMenuItem_ExportMkv.Text = "MKV"; toolStripMenuItem_ExportMkv.Text = "MKV";
toolStripMenuItem_ExportMkv.Visible = false;
toolStripMenuItem_ExportMkv.Click += toolStripMenuItem_Export_Click; toolStripMenuItem_ExportMkv.Click += toolStripMenuItem_Export_Click;
// //
// toolStripMenuItem_ExportMp4 // toolStripMenuItem_ExportMp4
// //
toolStripMenuItem_ExportMp4.Name = "toolStripMenuItem_ExportMp4"; toolStripMenuItem_ExportMp4.Name = "toolStripMenuItem_ExportMp4";
toolStripMenuItem_ExportMp4.Size = new Size(194, 34); toolStripMenuItem_ExportMp4.Size = new Size(270, 34);
toolStripMenuItem_ExportMp4.Text = "MP4..."; toolStripMenuItem_ExportMp4.Text = "MP4...";
toolStripMenuItem_ExportMp4.Click += toolStripMenuItem_Export_Click; toolStripMenuItem_ExportMp4.Click += toolStripMenuItem_Export_Click;
// //
// toolStripMenuItem_ExportMov // toolStripMenuItem_ExportMov
// //
toolStripMenuItem_ExportMov.Name = "toolStripMenuItem_ExportMov"; toolStripMenuItem_ExportMov.Name = "toolStripMenuItem_ExportMov";
toolStripMenuItem_ExportMov.Size = new Size(194, 34); toolStripMenuItem_ExportMov.Size = new Size(270, 34);
toolStripMenuItem_ExportMov.Text = "MOV..."; toolStripMenuItem_ExportMov.Text = "MOV...";
toolStripMenuItem_ExportMov.Visible = false;
toolStripMenuItem_ExportMov.Click += toolStripMenuItem_Export_Click; toolStripMenuItem_ExportMov.Click += toolStripMenuItem_Export_Click;
// //
// toolStripMenuItem_ExportWebm // toolStripMenuItem_ExportWebm
// //
toolStripMenuItem_ExportWebm.Name = "toolStripMenuItem_ExportWebm"; toolStripMenuItem_ExportWebm.Name = "toolStripMenuItem_ExportWebm";
toolStripMenuItem_ExportWebm.Size = new Size(194, 34); toolStripMenuItem_ExportWebm.Size = new Size(270, 34);
toolStripMenuItem_ExportWebm.Text = "WebM..."; toolStripMenuItem_ExportWebm.Text = "WebM...";
toolStripMenuItem_ExportWebm.Visible = false;
toolStripMenuItem_ExportWebm.Click += toolStripMenuItem_Export_Click; toolStripMenuItem_ExportWebm.Click += toolStripMenuItem_Export_Click;
// //
// toolStripSeparator2 // toolStripSeparator2
// //
toolStripSeparator2.Name = "toolStripSeparator2"; toolStripSeparator2.Name = "toolStripSeparator2";
toolStripSeparator2.Size = new Size(251, 6); toolStripSeparator2.Size = new Size(267, 6);
// //
// toolStripMenuItem_Exit // toolStripMenuItem_Exit
// //
toolStripMenuItem_Exit.Name = "toolStripMenuItem_Exit"; toolStripMenuItem_Exit.Name = "toolStripMenuItem_Exit";
toolStripMenuItem_Exit.ShortcutKeys = Keys.Alt | Keys.F4; toolStripMenuItem_Exit.ShortcutKeys = Keys.Alt | Keys.F4;
toolStripMenuItem_Exit.Size = new Size(254, 34); toolStripMenuItem_Exit.Size = new Size(270, 34);
toolStripMenuItem_Exit.Text = "退出(&X)"; toolStripMenuItem_Exit.Text = "退出(&X)";
toolStripMenuItem_Exit.Click += toolStripMenuItem_Exit_Click; toolStripMenuItem_Exit.Click += toolStripMenuItem_Exit_Click;
// //
@@ -263,7 +266,7 @@
rtbLog.Margin = new Padding(3, 2, 3, 2); rtbLog.Margin = new Padding(3, 2, 3, 2);
rtbLog.Name = "rtbLog"; rtbLog.Name = "rtbLog";
rtbLog.ReadOnly = true; rtbLog.ReadOnly = true;
rtbLog.Size = new Size(1728, 110); rtbLog.Size = new Size(1758, 134);
rtbLog.TabIndex = 0; rtbLog.TabIndex = 0;
rtbLog.Text = ""; rtbLog.Text = "";
rtbLog.WordWrap = false; rtbLog.WordWrap = false;
@@ -272,6 +275,7 @@
// //
splitContainer_MainForm.Cursor = Cursors.SizeNS; splitContainer_MainForm.Cursor = Cursors.SizeNS;
splitContainer_MainForm.Dock = DockStyle.Fill; splitContainer_MainForm.Dock = DockStyle.Fill;
splitContainer_MainForm.FixedPanel = FixedPanel.Panel2;
splitContainer_MainForm.Location = new Point(10, 5); splitContainer_MainForm.Location = new Point(10, 5);
splitContainer_MainForm.Name = "splitContainer_MainForm"; splitContainer_MainForm.Name = "splitContainer_MainForm";
splitContainer_MainForm.Orientation = Orientation.Horizontal; splitContainer_MainForm.Orientation = Orientation.Horizontal;
@@ -285,8 +289,8 @@
// //
splitContainer_MainForm.Panel2.Controls.Add(rtbLog); splitContainer_MainForm.Panel2.Controls.Add(rtbLog);
splitContainer_MainForm.Panel2.Cursor = Cursors.Default; splitContainer_MainForm.Panel2.Cursor = Cursors.Default;
splitContainer_MainForm.Size = new Size(1728, 997); splitContainer_MainForm.Size = new Size(1758, 1097);
splitContainer_MainForm.SplitterDistance = 879; splitContainer_MainForm.SplitterDistance = 955;
splitContainer_MainForm.SplitterWidth = 8; splitContainer_MainForm.SplitterWidth = 8;
splitContainer_MainForm.TabIndex = 3; splitContainer_MainForm.TabIndex = 3;
splitContainer_MainForm.TabStop = false; splitContainer_MainForm.TabStop = false;
@@ -297,6 +301,7 @@
// //
splitContainer_Functional.Cursor = Cursors.SizeWE; splitContainer_Functional.Cursor = Cursors.SizeWE;
splitContainer_Functional.Dock = DockStyle.Fill; splitContainer_Functional.Dock = DockStyle.Fill;
splitContainer_Functional.FixedPanel = FixedPanel.Panel1;
splitContainer_Functional.Location = new Point(0, 0); splitContainer_Functional.Location = new Point(0, 0);
splitContainer_Functional.Name = "splitContainer_Functional"; splitContainer_Functional.Name = "splitContainer_Functional";
// //
@@ -309,8 +314,8 @@
// //
splitContainer_Functional.Panel2.Controls.Add(groupBox_Preview); splitContainer_Functional.Panel2.Controls.Add(groupBox_Preview);
splitContainer_Functional.Panel2.Cursor = Cursors.Default; splitContainer_Functional.Panel2.Cursor = Cursors.Default;
splitContainer_Functional.Size = new Size(1728, 879); splitContainer_Functional.Size = new Size(1758, 955);
splitContainer_Functional.SplitterDistance = 747; splitContainer_Functional.SplitterDistance = 759;
splitContainer_Functional.SplitterWidth = 8; splitContainer_Functional.SplitterWidth = 8;
splitContainer_Functional.TabIndex = 2; splitContainer_Functional.TabIndex = 2;
splitContainer_Functional.TabStop = false; splitContainer_Functional.TabStop = false;
@@ -333,8 +338,8 @@
// //
splitContainer_Information.Panel2.Controls.Add(splitContainer_Config); splitContainer_Information.Panel2.Controls.Add(splitContainer_Config);
splitContainer_Information.Panel2.Cursor = Cursors.Default; splitContainer_Information.Panel2.Cursor = Cursors.Default;
splitContainer_Information.Size = new Size(747, 879); splitContainer_Information.Size = new Size(759, 955);
splitContainer_Information.SplitterDistance = 399; splitContainer_Information.SplitterDistance = 354;
splitContainer_Information.SplitterWidth = 8; splitContainer_Information.SplitterWidth = 8;
splitContainer_Information.TabIndex = 1; splitContainer_Information.TabIndex = 1;
splitContainer_Information.TabStop = false; splitContainer_Information.TabStop = false;
@@ -347,7 +352,7 @@
groupBox_SkelList.Dock = DockStyle.Fill; groupBox_SkelList.Dock = DockStyle.Fill;
groupBox_SkelList.Location = new Point(0, 0); groupBox_SkelList.Location = new Point(0, 0);
groupBox_SkelList.Name = "groupBox_SkelList"; groupBox_SkelList.Name = "groupBox_SkelList";
groupBox_SkelList.Size = new Size(399, 879); groupBox_SkelList.Size = new Size(354, 955);
groupBox_SkelList.TabIndex = 0; groupBox_SkelList.TabIndex = 0;
groupBox_SkelList.TabStop = false; groupBox_SkelList.TabStop = false;
groupBox_SkelList.Text = "模型列表"; groupBox_SkelList.Text = "模型列表";
@@ -358,7 +363,7 @@
spineListView.Location = new Point(3, 26); spineListView.Location = new Point(3, 26);
spineListView.Name = "spineListView"; spineListView.Name = "spineListView";
spineListView.PropertyGrid = propertyGrid_Spine; spineListView.PropertyGrid = propertyGrid_Spine;
spineListView.Size = new Size(393, 850); spineListView.Size = new Size(348, 926);
spineListView.TabIndex = 0; spineListView.TabIndex = 0;
// //
// propertyGrid_Spine // propertyGrid_Spine
@@ -367,7 +372,7 @@
propertyGrid_Spine.HelpVisible = false; propertyGrid_Spine.HelpVisible = false;
propertyGrid_Spine.Location = new Point(3, 26); propertyGrid_Spine.Location = new Point(3, 26);
propertyGrid_Spine.Name = "propertyGrid_Spine"; propertyGrid_Spine.Name = "propertyGrid_Spine";
propertyGrid_Spine.Size = new Size(334, 509); propertyGrid_Spine.Size = new Size(391, 592);
propertyGrid_Spine.TabIndex = 0; propertyGrid_Spine.TabIndex = 0;
propertyGrid_Spine.ToolbarVisible = false; propertyGrid_Spine.ToolbarVisible = false;
propertyGrid_Spine.PropertyValueChanged += propertyGrid_PropertyValueChanged; propertyGrid_Spine.PropertyValueChanged += propertyGrid_PropertyValueChanged;
@@ -376,6 +381,7 @@
// //
splitContainer_Config.Cursor = Cursors.SizeNS; splitContainer_Config.Cursor = Cursors.SizeNS;
splitContainer_Config.Dock = DockStyle.Fill; splitContainer_Config.Dock = DockStyle.Fill;
splitContainer_Config.FixedPanel = FixedPanel.Panel1;
splitContainer_Config.Location = new Point(0, 0); splitContainer_Config.Location = new Point(0, 0);
splitContainer_Config.Name = "splitContainer_Config"; splitContainer_Config.Name = "splitContainer_Config";
splitContainer_Config.Orientation = Orientation.Horizontal; splitContainer_Config.Orientation = Orientation.Horizontal;
@@ -389,32 +395,21 @@
// //
splitContainer_Config.Panel2.Controls.Add(groupBox_SkelConfig); splitContainer_Config.Panel2.Controls.Add(groupBox_SkelConfig);
splitContainer_Config.Panel2.Cursor = Cursors.Default; splitContainer_Config.Panel2.Cursor = Cursors.Default;
splitContainer_Config.Size = new Size(340, 879); splitContainer_Config.Size = new Size(397, 955);
splitContainer_Config.SplitterDistance = 333; splitContainer_Config.SplitterDistance = 326;
splitContainer_Config.SplitterWidth = 8; splitContainer_Config.SplitterWidth = 8;
splitContainer_Config.TabIndex = 0; splitContainer_Config.TabIndex = 0;
splitContainer_Config.TabStop = false; splitContainer_Config.TabStop = false;
splitContainer_Config.SplitterMoved += splitContainer_SplitterMoved; splitContainer_Config.SplitterMoved += splitContainer_SplitterMoved;
splitContainer_Config.MouseUp += splitContainer_MouseUp; splitContainer_Config.MouseUp += splitContainer_MouseUp;
// //
// groupBox_SkelConfig
//
groupBox_SkelConfig.Controls.Add(propertyGrid_Spine);
groupBox_SkelConfig.Dock = DockStyle.Fill;
groupBox_SkelConfig.Location = new Point(0, 0);
groupBox_SkelConfig.Name = "groupBox_SkelConfig";
groupBox_SkelConfig.Size = new Size(340, 538);
groupBox_SkelConfig.TabIndex = 0;
groupBox_SkelConfig.TabStop = false;
groupBox_SkelConfig.Text = "模型参数";
//
// groupBox_PreviewConfig // groupBox_PreviewConfig
// //
groupBox_PreviewConfig.Controls.Add(propertyGrid_Previewer); groupBox_PreviewConfig.Controls.Add(propertyGrid_Previewer);
groupBox_PreviewConfig.Dock = DockStyle.Fill; groupBox_PreviewConfig.Dock = DockStyle.Fill;
groupBox_PreviewConfig.Location = new Point(0, 0); groupBox_PreviewConfig.Location = new Point(0, 0);
groupBox_PreviewConfig.Name = "groupBox_PreviewConfig"; groupBox_PreviewConfig.Name = "groupBox_PreviewConfig";
groupBox_PreviewConfig.Size = new Size(340, 333); groupBox_PreviewConfig.Size = new Size(397, 326);
groupBox_PreviewConfig.TabIndex = 1; groupBox_PreviewConfig.TabIndex = 1;
groupBox_PreviewConfig.TabStop = false; groupBox_PreviewConfig.TabStop = false;
groupBox_PreviewConfig.Text = "画面参数"; groupBox_PreviewConfig.Text = "画面参数";
@@ -425,18 +420,29 @@
propertyGrid_Previewer.HelpVisible = false; propertyGrid_Previewer.HelpVisible = false;
propertyGrid_Previewer.Location = new Point(3, 26); propertyGrid_Previewer.Location = new Point(3, 26);
propertyGrid_Previewer.Name = "propertyGrid_Previewer"; propertyGrid_Previewer.Name = "propertyGrid_Previewer";
propertyGrid_Previewer.Size = new Size(334, 304); propertyGrid_Previewer.Size = new Size(391, 297);
propertyGrid_Previewer.TabIndex = 1; propertyGrid_Previewer.TabIndex = 1;
propertyGrid_Previewer.ToolbarVisible = false; propertyGrid_Previewer.ToolbarVisible = false;
propertyGrid_Previewer.PropertyValueChanged += propertyGrid_PropertyValueChanged; propertyGrid_Previewer.PropertyValueChanged += propertyGrid_PropertyValueChanged;
// //
// groupBox_SkelConfig
//
groupBox_SkelConfig.Controls.Add(propertyGrid_Spine);
groupBox_SkelConfig.Dock = DockStyle.Fill;
groupBox_SkelConfig.Location = new Point(0, 0);
groupBox_SkelConfig.Name = "groupBox_SkelConfig";
groupBox_SkelConfig.Size = new Size(397, 621);
groupBox_SkelConfig.TabIndex = 0;
groupBox_SkelConfig.TabStop = false;
groupBox_SkelConfig.Text = "模型参数";
//
// groupBox_Preview // groupBox_Preview
// //
groupBox_Preview.Controls.Add(spinePreviewer); groupBox_Preview.Controls.Add(spinePreviewer);
groupBox_Preview.Dock = DockStyle.Fill; groupBox_Preview.Dock = DockStyle.Fill;
groupBox_Preview.Location = new Point(0, 0); groupBox_Preview.Location = new Point(0, 0);
groupBox_Preview.Name = "groupBox_Preview"; groupBox_Preview.Name = "groupBox_Preview";
groupBox_Preview.Size = new Size(973, 879); groupBox_Preview.Size = new Size(991, 955);
groupBox_Preview.TabIndex = 1; groupBox_Preview.TabIndex = 1;
groupBox_Preview.TabStop = false; groupBox_Preview.TabStop = false;
groupBox_Preview.Text = "预览画面"; groupBox_Preview.Text = "预览画面";
@@ -447,7 +453,7 @@
spinePreviewer.Location = new Point(3, 26); spinePreviewer.Location = new Point(3, 26);
spinePreviewer.Name = "spinePreviewer"; spinePreviewer.Name = "spinePreviewer";
spinePreviewer.PropertyGrid = propertyGrid_Previewer; spinePreviewer.PropertyGrid = propertyGrid_Previewer;
spinePreviewer.Size = new Size(967, 850); spinePreviewer.Size = new Size(985, 926);
spinePreviewer.SpineListView = spineListView; spinePreviewer.SpineListView = spineListView;
spinePreviewer.TabIndex = 0; spinePreviewer.TabIndex = 0;
// //
@@ -458,7 +464,7 @@
panel_MainForm.Location = new Point(0, 32); panel_MainForm.Location = new Point(0, 32);
panel_MainForm.Name = "panel_MainForm"; panel_MainForm.Name = "panel_MainForm";
panel_MainForm.Padding = new Padding(10, 5, 10, 10); panel_MainForm.Padding = new Padding(10, 5, 10, 10);
panel_MainForm.Size = new Size(1748, 1012); panel_MainForm.Size = new Size(1778, 1112);
panel_MainForm.TabIndex = 4; panel_MainForm.TabIndex = 4;
// //
// toolTip // toolTip
@@ -469,7 +475,7 @@
// //
AutoScaleDimensions = new SizeF(11F, 24F); AutoScaleDimensions = new SizeF(11F, 24F);
AutoScaleMode = AutoScaleMode.Font; AutoScaleMode = AutoScaleMode.Font;
ClientSize = new Size(1748, 1044); ClientSize = new Size(1778, 1144);
Controls.Add(panel_MainForm); Controls.Add(panel_MainForm);
Controls.Add(menuStrip); Controls.Add(menuStrip);
Icon = (Icon)resources.GetObject("$this.Icon"); Icon = (Icon)resources.GetObject("$this.Icon");
@@ -499,8 +505,8 @@
splitContainer_Config.Panel2.ResumeLayout(false); splitContainer_Config.Panel2.ResumeLayout(false);
((System.ComponentModel.ISupportInitialize)splitContainer_Config).EndInit(); ((System.ComponentModel.ISupportInitialize)splitContainer_Config).EndInit();
splitContainer_Config.ResumeLayout(false); splitContainer_Config.ResumeLayout(false);
groupBox_SkelConfig.ResumeLayout(false);
groupBox_PreviewConfig.ResumeLayout(false); groupBox_PreviewConfig.ResumeLayout(false);
groupBox_SkelConfig.ResumeLayout(false);
groupBox_Preview.ResumeLayout(false); groupBox_Preview.ResumeLayout(false);
panel_MainForm.ResumeLayout(false); panel_MainForm.ResumeLayout(false);
ResumeLayout(false); ResumeLayout(false);

View File

@@ -12,8 +12,10 @@ using SpineViewer.Exporter;
namespace SpineViewer namespace SpineViewer
{ {
public partial class MainForm : Form internal partial class MainForm : Form
{ {
private Logger logger = LogManager.GetCurrentClassLogger();
public MainForm() public MainForm()
{ {
InitializeComponent(); InitializeComponent();
@@ -27,6 +29,19 @@ namespace SpineViewer
toolStripMenuItem_ExportMp4.Tag = ExportType.MP4; toolStripMenuItem_ExportMp4.Tag = ExportType.MP4;
toolStripMenuItem_ExportMov.Tag = ExportType.MOV; toolStripMenuItem_ExportMov.Tag = ExportType.MOV;
toolStripMenuItem_ExportWebm.Tag = ExportType.WebM; toolStripMenuItem_ExportWebm.Tag = ExportType.WebM;
// 执行一些初始化工作
try
{
Spine.Shader.Init();
}
catch (Exception ex)
{
logger.Error(ex.ToString());
logger.Error("Failed to load fragment shader");
MessageBox.Warn("Fragment shader 加载失败预乘Alpha通道属性失效");
}
} }
/// <summary> /// <summary>
@@ -146,10 +161,12 @@ namespace SpineViewer
{ {
var worker = (BackgroundWorker)sender; var worker = (BackgroundWorker)sender;
var exporter = (Exporter.Exporter)e.Argument; var exporter = (Exporter.Exporter)e.Argument;
Invoke(() => TaskbarManager.SetProgressState(Handle, TBPFLAG.TBPF_INDETERMINATE));
spinePreviewer.StopRender(); spinePreviewer.StopRender();
lock (spineListView.Spines) { exporter.Export(spineListView.Spines.Where(sp => !sp.IsHidden).ToArray(), (BackgroundWorker)sender); } lock (spineListView.Spines) { exporter.Export(spineListView.Spines.Where(sp => !sp.IsHidden).ToArray(), (BackgroundWorker)sender); }
e.Cancel = worker.CancellationPending; e.Cancel = worker.CancellationPending;
spinePreviewer.StartRender(); spinePreviewer.StartRender();
Invoke(() => TaskbarManager.SetProgressState(Handle, TBPFLAG.TBPF_NOPROGRESS));
} }
private void ConvertFileFormat_Work(object? sender, DoWorkEventArgs e) private void ConvertFileFormat_Work(object? sender, DoWorkEventArgs e)
@@ -166,7 +183,7 @@ namespace SpineViewer
int success = 0; int success = 0;
int error = 0; int error = 0;
SkeletonConverter srcCvter = srcVersion != Spine.Version.Auto ? SkeletonConverter.New(srcVersion) : null; SkeletonConverter srcCvter = srcVersion != SpineVersion.Auto ? SkeletonConverter.New(srcVersion) : null;
SkeletonConverter tgtCvter = SkeletonConverter.New(tgtVersion); SkeletonConverter tgtCvter = SkeletonConverter.New(tgtVersion);
worker.ReportProgress(0, $"已处理 0/{totalCount}"); worker.ReportProgress(0, $"已处理 0/{totalCount}");
@@ -183,12 +200,16 @@ namespace SpineViewer
try try
{ {
if (srcVersion == Spine.Version.Auto) if (srcVersion == SpineVersion.Auto)
{ {
if (Spine.Spine.GetVersion(skelPath) is Spine.Version detectedSrcVersion) try
srcCvter = SkeletonConverter.New(detectedSrcVersion); {
else srcCvter = SkeletonConverter.New(SpineHelper.GetVersion(skelPath));
throw new InvalidDataException($"Auto version detection failed for {skelPath}, try to use a specific version"); }
catch (Exception ex)
{
throw new InvalidDataException($"Auto version detection failed for {skelPath}, try to use a specific version", ex);
}
} }
var root = srcCvter.Read(skelPath); var root = srcCvter.Read(skelPath);
root = srcCvter.ToVersion(root, tgtVersion); root = srcCvter.ToVersion(root, tgtVersion);
@@ -197,8 +218,8 @@ namespace SpineViewer
} }
catch (Exception ex) catch (Exception ex)
{ {
Program.Logger.Error(ex.ToString()); logger.Error(ex.ToString());
Program.Logger.Error("Failed to convert {}", skelPath); logger.Error("Failed to convert {}", skelPath);
error++; error++;
} }
@@ -207,11 +228,11 @@ namespace SpineViewer
if (error > 0) if (error > 0)
{ {
Program.Logger.Warn("Batch convert {} successfully, {} failed", success, error); logger.Warn("Batch convert {} successfully, {} failed", success, error);
} }
else else
{ {
Program.Logger.Info("{} skel converted successfully", success); logger.Info("{} skel converted successfully", success);
} }
} }

View File

@@ -0,0 +1,21 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace SpineViewer
{
public static class NLogExtension
{
/// <summary>
/// 输出当前进程的内存占用
/// </summary>
public static void LogCurrentProcessMemoryUsage(this NLog.Logger logger)
{
var process = Process.GetCurrentProcess();
logger.Info("Current memory usage for {}: {:F2} MB", process.ProcessName, process.WorkingSet64 / 1024.0 / 1024.0);
}
}
}

View File

@@ -5,35 +5,30 @@ namespace SpineViewer
{ {
internal static class Program internal static class Program
{ {
/// <summary> ///// <summary>
/// 程序路径 ///// 程序路径
/// </summary> ///// </summary>
public static readonly string FilePath = Environment.ProcessPath; //public static readonly string FilePath = Environment.ProcessPath;
/// <summary> ///// <summary>
/// 程序名 ///// 程序名
/// </summary> ///// </summary>
public static readonly string Name = Path.GetFileNameWithoutExtension(FilePath); //public static readonly string Name = Path.GetFileNameWithoutExtension(FilePath);
/// <summary> ///// <summary>
/// 程序目录 ///// 程序目录
/// </summary> ///// </summary>
public static readonly string RootDir = Path.GetDirectoryName(FilePath); //public static readonly string RootDir = Path.GetDirectoryName(FilePath);
/// <summary> ///// <summary>
/// 程序临时目录 ///// 程序临时目录
/// </summary> ///// </summary>
public static readonly string TempDir = Directory.CreateDirectory(Path.Combine(Path.GetTempPath(), Name)).FullName; //public static readonly string TempDir = Directory.CreateDirectory(Path.Combine(Path.GetTempPath(), Name)).FullName;
/// <summary>
/// 程序进程
/// </summary>
public static readonly Process Process = Process.GetCurrentProcess();
/// <summary> /// <summary>
/// 程序日志器 /// 程序日志器
/// </summary> /// </summary>
public static readonly Logger Logger = LogManager.GetCurrentClassLogger(); private static readonly Logger logger = LogManager.GetCurrentClassLogger();
/// <summary> /// <summary>
/// 应用入口点 /// 应用入口点
@@ -41,8 +36,9 @@ namespace SpineViewer
[STAThread] [STAThread]
static void Main() static void Main()
{ {
// 此处先初始化全局配置再触发静态字段 Logger 引用构造, 才能将配置应用到新的日志器上
InitializeLogConfiguration(); InitializeLogConfiguration();
Logger.Info("Program Started"); logger.Info("Program Started");
// To customize application configuration such as set high DPI settings or default font, // To customize application configuration such as set high DPI settings or default font,
// see https://aka.ms/applicationconfiguration. // see https://aka.ms/applicationconfiguration.
@@ -54,7 +50,7 @@ namespace SpineViewer
} }
catch (Exception ex) catch (Exception ex)
{ {
Logger.Fatal(ex.ToString()); logger.Fatal(ex.ToString());
MessageBox.Error(ex.ToString(), "程序已崩溃"); MessageBox.Error(ex.ToString(), "程序已崩溃");
} }
} }
@@ -82,10 +78,5 @@ namespace SpineViewer
config.AddRule(LogLevel.Debug, LogLevel.Fatal, fileTarget); config.AddRule(LogLevel.Debug, LogLevel.Fatal, fileTarget);
LogManager.Configuration = config; LogManager.Configuration = config;
} }
/// <summary>
/// 输出当前内存使用情况
/// </summary>
public static void LogCurrentMemoryUsage() => Logger.Info("Current memory usage: {:F2} MB", Process.WorkingSet64 / 1024.0 / 1024.0);
} }
} }

View File

@@ -11,7 +11,7 @@ using System.Globalization;
namespace SpineViewer.Spine.Implementations.SkeletonConverter namespace SpineViewer.Spine.Implementations.SkeletonConverter
{ {
[SpineImplementation(Version.V38)] [SpineImplementation(SpineVersion.V38)]
class SkeletonConverter38 : SpineViewer.Spine.SkeletonConverter class SkeletonConverter38 : SpineViewer.Spine.SkeletonConverter
{ {
private BinaryReader reader = null; private BinaryReader reader = null;
@@ -1286,11 +1286,11 @@ namespace SpineViewer.Spine.Implementations.SkeletonConverter
base.WriteJson(root, jsonPath); base.WriteJson(root, jsonPath);
} }
public override JsonObject ToVersion(JsonObject root, Version version) public override JsonObject ToVersion(JsonObject root, SpineVersion version)
{ {
root = version switch root = version switch
{ {
Version.V38 => root.DeepClone().AsObject(), SpineVersion.V38 => root.DeepClone().AsObject(),
_ => throw new NotImplementedException(), _ => throw new NotImplementedException(),
}; };
return root; return root;

View File

@@ -10,7 +10,7 @@ using SpineRuntime21;
namespace SpineViewer.Spine.Implementations.Spine namespace SpineViewer.Spine.Implementations.Spine
{ {
[SpineImplementation(Version.V21)] [SpineImplementation(SpineVersion.V21)]
internal class Spine21 : SpineViewer.Spine.Spine internal class Spine21 : SpineViewer.Spine.Spine
{ {
private static readonly Animation EmptyAnimation = new(EMPTY_ANIMATION, [], 0); private static readonly Animation EmptyAnimation = new(EMPTY_ANIMATION, [], 0);
@@ -47,7 +47,7 @@ namespace SpineViewer.Spine.Implementations.Spine
// 2.1.x 不支持剪裁 // 2.1.x 不支持剪裁
//private SkeletonClipping clipping = new(); //private SkeletonClipping clipping = new();
public Spine21(string skelPath, string? atlasPath = null) : base(skelPath, atlasPath) public Spine21(string skelPath, string atlasPath) : base(skelPath, atlasPath)
{ {
atlas = new Atlas(AtlasPath, textureLoader); atlas = new Atlas(AtlasPath, textureLoader);
try try
@@ -76,16 +76,12 @@ namespace SpineViewer.Spine.Implementations.Spine
foreach (var skin in skeletonData.Skins) foreach (var skin in skeletonData.Skins)
skinNames.Add(skin.Name); skinNames.Add(skin.Name);
animationStateData = new AnimationStateData(skeletonData);
skeleton = new Skeleton(skeletonData);
animationState = new AnimationState(animationStateData);
foreach (var anime in skeletonData.Animations) foreach (var anime in skeletonData.Animations)
animationNames.Add(anime.Name); animationNames.Add(anime.Name);
// 取最后一个作为初始, 尽可能去显示非默认的内容 skeleton = new Skeleton(skeletonData);
CurrentAnimation = animationNames.Last(); animationStateData = new AnimationStateData(skeletonData);
CurrentSkin = skinNames.Last(); animationState = new AnimationState(animationStateData);
} }
protected override void Dispose(bool disposing) protected override void Dispose(bool disposing)
@@ -113,8 +109,8 @@ namespace SpineViewer.Spine.Implementations.Spine
var position = Position; var position = Position;
var flipX = FlipX; var flipX = FlipX;
var flipY = FlipY; var flipY = FlipY;
var animation = CurrentAnimation; var animation = Track0Animation; // TODO: 适配多轨道
var skin = CurrentSkin; var skin = Skin;
var val = Math.Max(value, SCALE_MIN); var val = Math.Max(value, SCALE_MIN);
if (skeletonBinary is not null) if (skeletonBinary is not null)
@@ -137,8 +133,8 @@ namespace SpineViewer.Spine.Implementations.Spine
Position = position; Position = position;
FlipX = flipX; FlipX = flipX;
FlipY = flipY; FlipY = flipY;
CurrentAnimation = animation; Track0Animation = animation; // TODO: 适配多轨道
CurrentSkin = skin; Skin = skin;
} }
} }
@@ -149,22 +145,35 @@ namespace SpineViewer.Spine.Implementations.Spine
{ {
skeleton.X = value.X; skeleton.X = value.X;
skeleton.Y = value.Y; skeleton.Y = value.Y;
Update(0);
} }
} }
public override bool FlipX public override bool FlipX
{ {
get => skeleton.FlipX; get => skeleton.FlipX;
set => skeleton.FlipX = value; set { skeleton.FlipX = value; Update(0); }
} }
public override bool FlipY public override bool FlipY
{ {
get => skeleton.FlipY; get => skeleton.FlipY;
set => skeleton.FlipY = value; set { skeleton.FlipY = value; Update(0); }
} }
public override string CurrentAnimation public override string Skin
{
get => skeleton.Skin?.Name ?? "default";
set
{
if (!skinNames.Contains(value)) return;
skeleton.SetSkin(value);
skeleton.SetSlotsToSetupPose();
Update(0);
}
}
public override string Track0Animation
{ {
get => animationState.GetCurrent(0)?.Animation.Name ?? EMPTY_ANIMATION; get => animationState.GetCurrent(0)?.Animation.Name ?? EMPTY_ANIMATION;
set set
@@ -177,18 +186,6 @@ namespace SpineViewer.Spine.Implementations.Spine
} }
} }
public override string CurrentSkin
{
get => skeleton.Skin?.Name ?? "default";
set
{
if (!skinNames.Contains(value)) return;
skeleton.SetSkin(value);
skeleton.SetToSetupPose();
Update(0);
}
}
public override RectangleF Bounds public override RectangleF Bounds
{ {
get get
@@ -334,7 +331,7 @@ namespace SpineViewer.Spine.Implementations.Spine
if (vertexArray.VertexCount > 0) if (vertexArray.VertexCount > 0)
{ {
if (UsePremultipliedAlpha && (states.BlendMode == BlendModeSFML.Normal || states.BlendMode == BlendModeSFML.Additive)) if (UsePremultipliedAlpha && (states.BlendMode == BlendModeSFML.Normal || states.BlendMode == BlendModeSFML.Additive))
states.Shader = FragmentShader; states.Shader = Shader.FragmentShader;
else else
states.Shader = null; states.Shader = null;
@@ -383,7 +380,7 @@ namespace SpineViewer.Spine.Implementations.Spine
} }
if (UsePremultipliedAlpha && (states.BlendMode == BlendModeSFML.Normal || states.BlendMode == BlendModeSFML.Additive)) if (UsePremultipliedAlpha && (states.BlendMode == BlendModeSFML.Normal || states.BlendMode == BlendModeSFML.Additive))
states.Shader = FragmentShader; states.Shader = Shader.FragmentShader;
else else
states.Shader = null; states.Shader = null;
//clipping.ClipEnd(); //clipping.ClipEnd();

View File

@@ -10,7 +10,7 @@ using SpineRuntime36;
namespace SpineViewer.Spine.Implementations.Spine namespace SpineViewer.Spine.Implementations.Spine
{ {
[SpineImplementation(Version.V36)] [SpineImplementation(SpineVersion.V36)]
internal class Spine36 : SpineViewer.Spine.Spine internal class Spine36 : SpineViewer.Spine.Spine
{ {
private static readonly Animation EmptyAnimation = new(EMPTY_ANIMATION, [], 0); private static readonly Animation EmptyAnimation = new(EMPTY_ANIMATION, [], 0);
@@ -46,7 +46,7 @@ namespace SpineViewer.Spine.Implementations.Spine
private SkeletonClipping clipping = new(); private SkeletonClipping clipping = new();
public Spine36(string skelPath, string? atlasPath = null) : base(skelPath, atlasPath) public Spine36(string skelPath, string atlasPath) : base(skelPath, atlasPath)
{ {
atlas = new Atlas(AtlasPath, textureLoader); atlas = new Atlas(AtlasPath, textureLoader);
try try
@@ -75,16 +75,12 @@ namespace SpineViewer.Spine.Implementations.Spine
foreach (var skin in skeletonData.Skins) foreach (var skin in skeletonData.Skins)
skinNames.Add(skin.Name); skinNames.Add(skin.Name);
animationStateData = new AnimationStateData(skeletonData);
skeleton = new Skeleton(skeletonData);
animationState = new AnimationState(animationStateData);
foreach (var anime in skeletonData.Animations) foreach (var anime in skeletonData.Animations)
animationNames.Add(anime.Name); animationNames.Add(anime.Name);
// 取最后一个作为初始, 尽可能去显示非默认的内容 skeleton = new Skeleton(skeletonData);
CurrentAnimation = animationNames.Last(); animationStateData = new AnimationStateData(skeletonData);
CurrentSkin = skinNames.Last(); animationState = new AnimationState(animationStateData);
} }
protected override void Dispose(bool disposing) protected override void Dispose(bool disposing)
@@ -112,8 +108,8 @@ namespace SpineViewer.Spine.Implementations.Spine
var position = Position; var position = Position;
var flipX = FlipX; var flipX = FlipX;
var flipY = FlipY; var flipY = FlipY;
var animation = CurrentAnimation; var animation = Track0Animation; // TODO: 适配多轨道
var skin = CurrentSkin; var skin = Skin;
var val = Math.Max(value, SCALE_MIN); var val = Math.Max(value, SCALE_MIN);
if (skeletonBinary is not null) if (skeletonBinary is not null)
@@ -136,8 +132,8 @@ namespace SpineViewer.Spine.Implementations.Spine
Position = position; Position = position;
FlipX = flipX; FlipX = flipX;
FlipY = flipY; FlipY = flipY;
CurrentAnimation = animation; Track0Animation = animation; // TODO: 适配多轨道
CurrentSkin = skin; Skin = skin;
} }
} }
@@ -148,22 +144,35 @@ namespace SpineViewer.Spine.Implementations.Spine
{ {
skeleton.X = value.X; skeleton.X = value.X;
skeleton.Y = value.Y; skeleton.Y = value.Y;
Update(0);
} }
} }
public override bool FlipX public override bool FlipX
{ {
get => skeleton.FlipX; get => skeleton.FlipX;
set => skeleton.FlipX = value; set { skeleton.FlipX = value; Update(0); }
} }
public override bool FlipY public override bool FlipY
{ {
get => skeleton.FlipY; get => skeleton.FlipY;
set => skeleton.FlipY = value; set { skeleton.FlipY = value; Update(0); }
} }
public override string CurrentAnimation public override string Skin
{
get => skeleton.Skin?.Name ?? "default";
set
{
if (!skinNames.Contains(value)) return;
skeleton.SetSkin(value);
skeleton.SetSlotsToSetupPose();
Update(0);
}
}
public override string Track0Animation
{ {
get => animationState.GetCurrent(0)?.Animation.Name ?? EMPTY_ANIMATION; get => animationState.GetCurrent(0)?.Animation.Name ?? EMPTY_ANIMATION;
set set
@@ -176,18 +185,6 @@ namespace SpineViewer.Spine.Implementations.Spine
} }
} }
public override string CurrentSkin
{
get => skeleton.Skin?.Name ?? "default";
set
{
if (!skinNames.Contains(value)) return;
skeleton.SetSkin(value);
skeleton.SetToSetupPose();
Update(0);
}
}
public override RectangleF Bounds public override RectangleF Bounds
{ {
get get
@@ -291,7 +288,7 @@ namespace SpineViewer.Spine.Implementations.Spine
if (vertexArray.VertexCount > 0) if (vertexArray.VertexCount > 0)
{ {
if (UsePremultipliedAlpha && (states.BlendMode == BlendModeSFML.Normal || states.BlendMode == BlendModeSFML.Additive)) if (UsePremultipliedAlpha && (states.BlendMode == BlendModeSFML.Normal || states.BlendMode == BlendModeSFML.Additive))
states.Shader = FragmentShader; states.Shader = Shader.FragmentShader;
else else
states.Shader = null; states.Shader = null;
@@ -341,7 +338,7 @@ namespace SpineViewer.Spine.Implementations.Spine
clipping.ClipEnd(); clipping.ClipEnd();
if (UsePremultipliedAlpha && (states.BlendMode == BlendModeSFML.Normal || states.BlendMode == BlendModeSFML.Additive)) if (UsePremultipliedAlpha && (states.BlendMode == BlendModeSFML.Normal || states.BlendMode == BlendModeSFML.Additive))
states.Shader = FragmentShader; states.Shader = Shader.FragmentShader;
else else
states.Shader = null; states.Shader = null;

View File

@@ -7,7 +7,7 @@ using SpineRuntime37;
namespace SpineViewer.Spine.Implementations.Spine namespace SpineViewer.Spine.Implementations.Spine
{ {
[SpineImplementation(Version.V37)] [SpineImplementation(SpineVersion.V37)]
internal class Spine37 : SpineViewer.Spine.Spine internal class Spine37 : SpineViewer.Spine.Spine
{ {
private static readonly Animation EmptyAnimation = new(EMPTY_ANIMATION, [], 0); private static readonly Animation EmptyAnimation = new(EMPTY_ANIMATION, [], 0);
@@ -44,7 +44,7 @@ namespace SpineViewer.Spine.Implementations.Spine
private SkeletonClipping clipping = new(); private SkeletonClipping clipping = new();
public Spine37(string skelPath, string? atlasPath = null) : base(skelPath, atlasPath) public Spine37(string skelPath, string atlasPath) : base(skelPath, atlasPath)
{ {
atlas = new Atlas(AtlasPath, textureLoader); atlas = new Atlas(AtlasPath, textureLoader);
try try
@@ -73,16 +73,12 @@ namespace SpineViewer.Spine.Implementations.Spine
foreach (var skin in skeletonData.Skins) foreach (var skin in skeletonData.Skins)
skinNames.Add(skin.Name); skinNames.Add(skin.Name);
animationStateData = new AnimationStateData(skeletonData);
skeleton = new Skeleton(skeletonData);
animationState = new AnimationState(animationStateData);
foreach (var anime in skeletonData.Animations) foreach (var anime in skeletonData.Animations)
animationNames.Add(anime.Name); animationNames.Add(anime.Name);
// 取最后一个作为初始, 尽可能去显示非默认的内容 skeleton = new Skeleton(skeletonData);
CurrentAnimation = animationNames.Last(); animationStateData = new AnimationStateData(skeletonData);
CurrentSkin = skinNames.Last(); animationState = new AnimationState(animationStateData);
} }
protected override void Dispose(bool disposing) protected override void Dispose(bool disposing)
@@ -95,58 +91,12 @@ namespace SpineViewer.Spine.Implementations.Spine
public override float Scale public override float Scale
{ {
get get => Math.Abs(skeleton.ScaleX);
{
if (skeletonBinary is not null)
return skeletonBinary.Scale;
else if (skeletonJson is not null)
return skeletonJson.Scale;
else
return 1f;
}
set set
{ {
// 保存状态 skeleton.ScaleX = Math.Sign(skeleton.ScaleX) * value;
var position = Position; skeleton.ScaleY = Math.Sign(skeleton.ScaleY) * value;
var flipX = FlipX; Update(0);
var flipY = FlipY;
var savedTrack0 = animationState.GetCurrent(0);
var val = Math.Max(value, SCALE_MIN);
if (skeletonBinary is not null)
{
skeletonBinary.Scale = val;
skeletonData = skeletonBinary.ReadSkeletonData(SkelPath);
}
else if (skeletonJson is not null)
{
skeletonJson.Scale = val;
skeletonData = skeletonJson.ReadSkeletonData(SkelPath);
}
// reload skel-dependent data
animationStateData = new AnimationStateData(skeletonData) { DefaultMix = animationStateData.DefaultMix };
skeleton = new Skeleton(skeletonData);
animationState = new AnimationState(animationStateData);
// 恢复状态
Position = position;
FlipX = flipX;
FlipY = flipY;
// 恢复原本 Track0 上所有动画
if (savedTrack0 is not null)
{
var entry = animationState.SetAnimation(0, savedTrack0.Animation.Name, true);
entry.TrackTime = savedTrack0.TrackTime;
var savedEntry = savedTrack0.Next;
while (savedEntry is not null)
{
entry = animationState.AddAnimation(0, savedEntry.Animation.Name, true, 0);
entry.TrackTime = savedEntry.TrackTime;
savedEntry = savedEntry.Next;
}
}
} }
} }
@@ -157,6 +107,7 @@ namespace SpineViewer.Spine.Implementations.Spine
{ {
skeleton.X = value.X; skeleton.X = value.X;
skeleton.Y = value.Y; skeleton.Y = value.Y;
Update(0);
} }
} }
@@ -167,6 +118,7 @@ namespace SpineViewer.Spine.Implementations.Spine
{ {
if (skeleton.ScaleX > 0 && value || skeleton.ScaleX < 0 && !value) if (skeleton.ScaleX > 0 && value || skeleton.ScaleX < 0 && !value)
skeleton.ScaleX *= -1; skeleton.ScaleX *= -1;
Update(0);
} }
} }
@@ -177,10 +129,23 @@ namespace SpineViewer.Spine.Implementations.Spine
{ {
if (skeleton.ScaleY > 0 && value || skeleton.ScaleY < 0 && !value) if (skeleton.ScaleY > 0 && value || skeleton.ScaleY < 0 && !value)
skeleton.ScaleY *= -1; skeleton.ScaleY *= -1;
Update(0);
} }
} }
public override string CurrentAnimation public override string Skin
{
get => skeleton.Skin?.Name ?? "default";
set
{
if (!skinNames.Contains(value)) return;
skeleton.SetSkin(value);
skeleton.SetSlotsToSetupPose();
Update(0);
}
}
public override string Track0Animation
{ {
get => animationState.GetCurrent(0)?.Animation.Name ?? EMPTY_ANIMATION; get => animationState.GetCurrent(0)?.Animation.Name ?? EMPTY_ANIMATION;
set set
@@ -193,18 +158,6 @@ namespace SpineViewer.Spine.Implementations.Spine
} }
} }
public override string CurrentSkin
{
get => skeleton.Skin?.Name ?? "default";
set
{
if (!skinNames.Contains(value)) return;
skeleton.SetSkin(value);
skeleton.SetToSetupPose();
Update(0);
}
}
public override RectangleF Bounds public override RectangleF Bounds
{ {
get get
@@ -309,7 +262,7 @@ namespace SpineViewer.Spine.Implementations.Spine
{ {
// XXX: 实测不用设置 sampler2D 的值也正确 // XXX: 实测不用设置 sampler2D 的值也正确
if (UsePremultipliedAlpha && (states.BlendMode == BlendModeSFML.Normal || states.BlendMode == BlendModeSFML.Additive)) if (UsePremultipliedAlpha && (states.BlendMode == BlendModeSFML.Normal || states.BlendMode == BlendModeSFML.Additive))
states.Shader = FragmentShader; states.Shader = Shader.FragmentShader;
else else
states.Shader = null; states.Shader = null;
@@ -359,7 +312,7 @@ namespace SpineViewer.Spine.Implementations.Spine
clipping.ClipEnd(); clipping.ClipEnd();
if (UsePremultipliedAlpha && (states.BlendMode == BlendModeSFML.Normal || states.BlendMode == BlendModeSFML.Additive)) if (UsePremultipliedAlpha && (states.BlendMode == BlendModeSFML.Normal || states.BlendMode == BlendModeSFML.Additive))
states.Shader = FragmentShader; states.Shader = Shader.FragmentShader;
else else
states.Shader = null; states.Shader = null;

View File

@@ -10,7 +10,7 @@ using SpineRuntime38.Attachments;
namespace SpineViewer.Spine.Implementations.Spine namespace SpineViewer.Spine.Implementations.Spine
{ {
[SpineImplementation(Version.V38)] [SpineImplementation(SpineVersion.V38)]
internal class Spine38 : SpineViewer.Spine.Spine internal class Spine38 : SpineViewer.Spine.Spine
{ {
private static readonly Animation EmptyAnimation = new(EMPTY_ANIMATION, [], 0); private static readonly Animation EmptyAnimation = new(EMPTY_ANIMATION, [], 0);
@@ -50,7 +50,7 @@ namespace SpineViewer.Spine.Implementations.Spine
private SkeletonClipping clipping = new(); private SkeletonClipping clipping = new();
public Spine38(string skelPath, string? atlasPath = null) : base(skelPath, atlasPath) public Spine38(string skelPath, string atlasPath) : base(skelPath, atlasPath)
{ {
atlas = new Atlas(AtlasPath, textureLoader); atlas = new Atlas(AtlasPath, textureLoader);
try try
@@ -79,16 +79,12 @@ namespace SpineViewer.Spine.Implementations.Spine
foreach (var skin in skeletonData.Skins) foreach (var skin in skeletonData.Skins)
skinNames.Add(skin.Name); skinNames.Add(skin.Name);
animationStateData = new AnimationStateData(skeletonData);
skeleton = new Skeleton(skeletonData);
animationState = new AnimationState(animationStateData);
foreach (var anime in skeletonData.Animations) foreach (var anime in skeletonData.Animations)
animationNames.Add(anime.Name); animationNames.Add(anime.Name);
// 取最后一个作为初始, 尽可能去显示非默认的内容 skeleton = new Skeleton(skeletonData);
CurrentAnimation = animationNames.Last(); animationStateData = new AnimationStateData(skeletonData);
CurrentSkin = skinNames.Last(); animationState = new AnimationState(animationStateData);
} }
protected override void Dispose(bool disposing) protected override void Dispose(bool disposing)
@@ -106,6 +102,7 @@ namespace SpineViewer.Spine.Implementations.Spine
{ {
skeleton.ScaleX = Math.Sign(skeleton.ScaleX) * value; skeleton.ScaleX = Math.Sign(skeleton.ScaleX) * value;
skeleton.ScaleY = Math.Sign(skeleton.ScaleY) * value; skeleton.ScaleY = Math.Sign(skeleton.ScaleY) * value;
Update(0);
} }
} }
@@ -116,6 +113,7 @@ namespace SpineViewer.Spine.Implementations.Spine
{ {
skeleton.X = value.X; skeleton.X = value.X;
skeleton.Y = value.Y; skeleton.Y = value.Y;
Update(0);
} }
} }
@@ -126,6 +124,7 @@ namespace SpineViewer.Spine.Implementations.Spine
{ {
if (skeleton.ScaleX > 0 && value || skeleton.ScaleX < 0 && !value) if (skeleton.ScaleX > 0 && value || skeleton.ScaleX < 0 && !value)
skeleton.ScaleX *= -1; skeleton.ScaleX *= -1;
Update(0);
} }
} }
@@ -136,10 +135,23 @@ namespace SpineViewer.Spine.Implementations.Spine
{ {
if (skeleton.ScaleY > 0 && value || skeleton.ScaleY < 0 && !value) if (skeleton.ScaleY > 0 && value || skeleton.ScaleY < 0 && !value)
skeleton.ScaleY *= -1; skeleton.ScaleY *= -1;
Update(0);
} }
} }
public override string CurrentAnimation public override string Skin
{
get => skeleton.Skin?.Name ?? "default";
set
{
if (!skinNames.Contains(value)) return;
skeleton.SetSkin(value);
skeleton.SetSlotsToSetupPose();
Update(0);
}
}
public override string Track0Animation
{ {
get => animationState.GetCurrent(0)?.Animation.Name ?? EMPTY_ANIMATION; get => animationState.GetCurrent(0)?.Animation.Name ?? EMPTY_ANIMATION;
set set
@@ -152,18 +164,6 @@ namespace SpineViewer.Spine.Implementations.Spine
} }
} }
public override string CurrentSkin
{
get => skeleton.Skin?.Name ?? "default";
set
{
if (!skinNames.Contains(value)) return;
skeleton.SetSkin(value);
skeleton.SetToSetupPose();
Update(0);
}
}
public override RectangleF Bounds public override RectangleF Bounds
{ {
get get
@@ -268,7 +268,7 @@ namespace SpineViewer.Spine.Implementations.Spine
{ {
// XXX: 实测不用设置 sampler2D 的值也正确 // XXX: 实测不用设置 sampler2D 的值也正确
if (UsePremultipliedAlpha && (states.BlendMode == BlendModeSFML.Normal || states.BlendMode == BlendModeSFML.Additive)) if (UsePremultipliedAlpha && (states.BlendMode == BlendModeSFML.Normal || states.BlendMode == BlendModeSFML.Additive))
states.Shader = FragmentShader; states.Shader = Shader.FragmentShader;
else else
states.Shader = null; states.Shader = null;
@@ -318,7 +318,7 @@ namespace SpineViewer.Spine.Implementations.Spine
clipping.ClipEnd(); clipping.ClipEnd();
if (UsePremultipliedAlpha && (states.BlendMode == BlendModeSFML.Normal || states.BlendMode == BlendModeSFML.Additive)) if (UsePremultipliedAlpha && (states.BlendMode == BlendModeSFML.Normal || states.BlendMode == BlendModeSFML.Additive))
states.Shader = FragmentShader; states.Shader = Shader.FragmentShader;
else else
states.Shader = null; states.Shader = null;

View File

@@ -9,7 +9,7 @@ using SpineRuntime40;
namespace SpineViewer.Spine.Implementations.Spine namespace SpineViewer.Spine.Implementations.Spine
{ {
[SpineImplementation(Version.V40)] [SpineImplementation(SpineVersion.V40)]
internal class Spine40 : SpineViewer.Spine.Spine internal class Spine40 : SpineViewer.Spine.Spine
{ {
private static readonly Animation EmptyAnimation = new(EMPTY_ANIMATION, [], 0); private static readonly Animation EmptyAnimation = new(EMPTY_ANIMATION, [], 0);
@@ -46,7 +46,7 @@ namespace SpineViewer.Spine.Implementations.Spine
private SkeletonClipping clipping = new(); private SkeletonClipping clipping = new();
public Spine40(string skelPath, string? atlasPath = null) : base(skelPath, atlasPath) public Spine40(string skelPath, string atlasPath) : base(skelPath, atlasPath)
{ {
atlas = new Atlas(AtlasPath, textureLoader); atlas = new Atlas(AtlasPath, textureLoader);
try try
@@ -75,16 +75,12 @@ namespace SpineViewer.Spine.Implementations.Spine
foreach (var skin in skeletonData.Skins) foreach (var skin in skeletonData.Skins)
skinNames.Add(skin.Name); skinNames.Add(skin.Name);
animationStateData = new AnimationStateData(skeletonData);
skeleton = new Skeleton(skeletonData);
animationState = new AnimationState(animationStateData);
foreach (var anime in skeletonData.Animations) foreach (var anime in skeletonData.Animations)
animationNames.Add(anime.Name); animationNames.Add(anime.Name);
// 取最后一个作为初始, 尽可能去显示非默认的内容 skeleton = new Skeleton(skeletonData);
CurrentAnimation = animationNames.Last(); animationStateData = new AnimationStateData(skeletonData);
CurrentSkin = skinNames.Last(); animationState = new AnimationState(animationStateData);
} }
protected override void Dispose(bool disposing) protected override void Dispose(bool disposing)
@@ -102,6 +98,7 @@ namespace SpineViewer.Spine.Implementations.Spine
{ {
skeleton.ScaleX = Math.Sign(skeleton.ScaleX) * value; skeleton.ScaleX = Math.Sign(skeleton.ScaleX) * value;
skeleton.ScaleY = Math.Sign(skeleton.ScaleY) * value; skeleton.ScaleY = Math.Sign(skeleton.ScaleY) * value;
Update(0);
} }
} }
@@ -112,6 +109,7 @@ namespace SpineViewer.Spine.Implementations.Spine
{ {
skeleton.X = value.X; skeleton.X = value.X;
skeleton.Y = value.Y; skeleton.Y = value.Y;
Update(0);
} }
} }
@@ -122,6 +120,7 @@ namespace SpineViewer.Spine.Implementations.Spine
{ {
if (skeleton.ScaleX > 0 && value || skeleton.ScaleX < 0 && !value) if (skeleton.ScaleX > 0 && value || skeleton.ScaleX < 0 && !value)
skeleton.ScaleX *= -1; skeleton.ScaleX *= -1;
Update(0);
} }
} }
@@ -132,10 +131,23 @@ namespace SpineViewer.Spine.Implementations.Spine
{ {
if (skeleton.ScaleY > 0 && value || skeleton.ScaleY < 0 && !value) if (skeleton.ScaleY > 0 && value || skeleton.ScaleY < 0 && !value)
skeleton.ScaleY *= -1; skeleton.ScaleY *= -1;
Update(0);
} }
} }
public override string CurrentAnimation public override string Skin
{
get => skeleton.Skin?.Name ?? "default";
set
{
if (!skinNames.Contains(value)) return;
skeleton.SetSkin(value);
skeleton.SetSlotsToSetupPose();
Update(0);
}
}
public override string Track0Animation
{ {
get => animationState.GetCurrent(0)?.Animation.Name ?? EMPTY_ANIMATION; get => animationState.GetCurrent(0)?.Animation.Name ?? EMPTY_ANIMATION;
set set
@@ -148,18 +160,6 @@ namespace SpineViewer.Spine.Implementations.Spine
} }
} }
public override string CurrentSkin
{
get => skeleton.Skin?.Name ?? "default";
set
{
if (!skinNames.Contains(value)) return;
skeleton.SetSkin(value);
skeleton.SetToSetupPose();
Update(0);
}
}
public override RectangleF Bounds public override RectangleF Bounds
{ {
get get
@@ -264,7 +264,7 @@ namespace SpineViewer.Spine.Implementations.Spine
{ {
// XXX: 实测不用设置 sampler2D 的值也正确 // XXX: 实测不用设置 sampler2D 的值也正确
if (UsePremultipliedAlpha && (states.BlendMode == BlendModeSFML.Normal || states.BlendMode == BlendModeSFML.Additive)) if (UsePremultipliedAlpha && (states.BlendMode == BlendModeSFML.Normal || states.BlendMode == BlendModeSFML.Additive))
states.Shader = FragmentShader; states.Shader = Shader.FragmentShader;
else else
states.Shader = null; states.Shader = null;
@@ -314,7 +314,7 @@ namespace SpineViewer.Spine.Implementations.Spine
clipping.ClipEnd(); clipping.ClipEnd();
if (UsePremultipliedAlpha && (states.BlendMode == BlendModeSFML.Normal || states.BlendMode == BlendModeSFML.Additive)) if (UsePremultipliedAlpha && (states.BlendMode == BlendModeSFML.Normal || states.BlendMode == BlendModeSFML.Additive))
states.Shader = FragmentShader; states.Shader = Shader.FragmentShader;
else else
states.Shader = null; states.Shader = null;

View File

@@ -9,7 +9,7 @@ using SpineRuntime41;
namespace SpineViewer.Spine.Implementations.Spine namespace SpineViewer.Spine.Implementations.Spine
{ {
[SpineImplementation(Version.V41)] [SpineImplementation(SpineVersion.V41)]
internal class Spine41 : SpineViewer.Spine.Spine internal class Spine41 : SpineViewer.Spine.Spine
{ {
private static readonly Animation EmptyAnimation = new(EMPTY_ANIMATION, [], 0); private static readonly Animation EmptyAnimation = new(EMPTY_ANIMATION, [], 0);
@@ -46,7 +46,7 @@ namespace SpineViewer.Spine.Implementations.Spine
private SkeletonClipping clipping = new(); private SkeletonClipping clipping = new();
public Spine41(string skelPath, string? atlasPath = null) : base(skelPath, atlasPath) public Spine41(string skelPath, string atlasPath) : base(skelPath, atlasPath)
{ {
atlas = new Atlas(AtlasPath, textureLoader); atlas = new Atlas(AtlasPath, textureLoader);
try try
@@ -75,16 +75,12 @@ namespace SpineViewer.Spine.Implementations.Spine
foreach (var skin in skeletonData.Skins) foreach (var skin in skeletonData.Skins)
skinNames.Add(skin.Name); skinNames.Add(skin.Name);
animationStateData = new AnimationStateData(skeletonData);
skeleton = new Skeleton(skeletonData);
animationState = new AnimationState(animationStateData);
foreach (var anime in skeletonData.Animations) foreach (var anime in skeletonData.Animations)
animationNames.Add(anime.Name); animationNames.Add(anime.Name);
// 取最后一个作为初始, 尽可能去显示非默认的内容 skeleton = new Skeleton(skeletonData);
CurrentAnimation = animationNames.Last(); animationStateData = new AnimationStateData(skeletonData);
CurrentSkin = skinNames.Last(); animationState = new AnimationState(animationStateData);
} }
protected override void Dispose(bool disposing) protected override void Dispose(bool disposing)
@@ -102,6 +98,7 @@ namespace SpineViewer.Spine.Implementations.Spine
{ {
skeleton.ScaleX = Math.Sign(skeleton.ScaleX) * value; skeleton.ScaleX = Math.Sign(skeleton.ScaleX) * value;
skeleton.ScaleY = Math.Sign(skeleton.ScaleY) * value; skeleton.ScaleY = Math.Sign(skeleton.ScaleY) * value;
Update(0);
} }
} }
@@ -112,6 +109,7 @@ namespace SpineViewer.Spine.Implementations.Spine
{ {
skeleton.X = value.X; skeleton.X = value.X;
skeleton.Y = value.Y; skeleton.Y = value.Y;
Update(0);
} }
} }
@@ -122,6 +120,7 @@ namespace SpineViewer.Spine.Implementations.Spine
{ {
if (skeleton.ScaleX > 0 && value || skeleton.ScaleX < 0 && !value) if (skeleton.ScaleX > 0 && value || skeleton.ScaleX < 0 && !value)
skeleton.ScaleX *= -1; skeleton.ScaleX *= -1;
Update(0);
} }
} }
@@ -132,10 +131,23 @@ namespace SpineViewer.Spine.Implementations.Spine
{ {
if (skeleton.ScaleY > 0 && value || skeleton.ScaleY < 0 && !value) if (skeleton.ScaleY > 0 && value || skeleton.ScaleY < 0 && !value)
skeleton.ScaleY *= -1; skeleton.ScaleY *= -1;
Update(0);
} }
} }
public override string CurrentAnimation public override string Skin
{
get => skeleton.Skin?.Name ?? "default";
set
{
if (!skinNames.Contains(value)) return;
skeleton.SetSkin(value);
skeleton.SetSlotsToSetupPose();
Update(0);
}
}
public override string Track0Animation
{ {
get => animationState.GetCurrent(0)?.Animation.Name ?? EMPTY_ANIMATION; get => animationState.GetCurrent(0)?.Animation.Name ?? EMPTY_ANIMATION;
set set
@@ -148,18 +160,6 @@ namespace SpineViewer.Spine.Implementations.Spine
} }
} }
public override string CurrentSkin
{
get => skeleton.Skin?.Name ?? "default";
set
{
if (!skinNames.Contains(value)) return;
skeleton.SetSkin(value);
skeleton.SetToSetupPose();
Update(0);
}
}
public override RectangleF Bounds public override RectangleF Bounds
{ {
get get
@@ -264,7 +264,7 @@ namespace SpineViewer.Spine.Implementations.Spine
{ {
// XXX: 实测不用设置 sampler2D 的值也正确 // XXX: 实测不用设置 sampler2D 的值也正确
if (UsePremultipliedAlpha && (states.BlendMode == BlendModeSFML.Normal || states.BlendMode == BlendModeSFML.Additive)) if (UsePremultipliedAlpha && (states.BlendMode == BlendModeSFML.Normal || states.BlendMode == BlendModeSFML.Additive))
states.Shader = FragmentShader; states.Shader = Shader.FragmentShader;
else else
states.Shader = null; states.Shader = null;
@@ -314,7 +314,7 @@ namespace SpineViewer.Spine.Implementations.Spine
clipping.ClipEnd(); clipping.ClipEnd();
if (UsePremultipliedAlpha && (states.BlendMode == BlendModeSFML.Normal || states.BlendMode == BlendModeSFML.Additive)) if (UsePremultipliedAlpha && (states.BlendMode == BlendModeSFML.Normal || states.BlendMode == BlendModeSFML.Additive))
states.Shader = FragmentShader; states.Shader = Shader.FragmentShader;
else else
states.Shader = null; states.Shader = null;

View File

@@ -9,7 +9,7 @@ using SpineRuntime42;
namespace SpineViewer.Spine.Implementations.Spine namespace SpineViewer.Spine.Implementations.Spine
{ {
[SpineImplementation(Version.V42)] [SpineImplementation(SpineVersion.V42)]
internal class Spine42 : SpineViewer.Spine.Spine internal class Spine42 : SpineViewer.Spine.Spine
{ {
private static readonly Animation EmptyAnimation = new(EMPTY_ANIMATION, [], 0); private static readonly Animation EmptyAnimation = new(EMPTY_ANIMATION, [], 0);
@@ -46,7 +46,7 @@ namespace SpineViewer.Spine.Implementations.Spine
private SkeletonClipping clipping = new(); private SkeletonClipping clipping = new();
public Spine42(string skelPath, string? atlasPath = null) : base(skelPath, atlasPath) public Spine42(string skelPath, string atlasPath) : base(skelPath, atlasPath)
{ {
atlas = new Atlas(AtlasPath, textureLoader); atlas = new Atlas(AtlasPath, textureLoader);
try try
@@ -75,16 +75,12 @@ namespace SpineViewer.Spine.Implementations.Spine
foreach (var skin in skeletonData.Skins) foreach (var skin in skeletonData.Skins)
skinNames.Add(skin.Name); skinNames.Add(skin.Name);
animationStateData = new AnimationStateData(skeletonData);
skeleton = new Skeleton(skeletonData);
animationState = new AnimationState(animationStateData);
foreach (var anime in skeletonData.Animations) foreach (var anime in skeletonData.Animations)
animationNames.Add(anime.Name); animationNames.Add(anime.Name);
// 取最后一个作为初始, 尽可能去显示非默认的内容 skeleton = new Skeleton(skeletonData);
CurrentAnimation = animationNames.Last(); animationStateData = new AnimationStateData(skeletonData);
CurrentSkin = skinNames.Last(); animationState = new AnimationState(animationStateData);
} }
protected override void Dispose(bool disposing) protected override void Dispose(bool disposing)
@@ -102,6 +98,7 @@ namespace SpineViewer.Spine.Implementations.Spine
{ {
skeleton.ScaleX = Math.Sign(skeleton.ScaleX) * value; skeleton.ScaleX = Math.Sign(skeleton.ScaleX) * value;
skeleton.ScaleY = Math.Sign(skeleton.ScaleY) * value; skeleton.ScaleY = Math.Sign(skeleton.ScaleY) * value;
Update(0);
} }
} }
@@ -112,6 +109,7 @@ namespace SpineViewer.Spine.Implementations.Spine
{ {
skeleton.X = value.X; skeleton.X = value.X;
skeleton.Y = value.Y; skeleton.Y = value.Y;
Update(0);
} }
} }
@@ -122,6 +120,7 @@ namespace SpineViewer.Spine.Implementations.Spine
{ {
if (skeleton.ScaleX > 0 && value || skeleton.ScaleX < 0 && !value) if (skeleton.ScaleX > 0 && value || skeleton.ScaleX < 0 && !value)
skeleton.ScaleX *= -1; skeleton.ScaleX *= -1;
Update(0);
} }
} }
@@ -132,10 +131,23 @@ namespace SpineViewer.Spine.Implementations.Spine
{ {
if (skeleton.ScaleY > 0 && value || skeleton.ScaleY < 0 && !value) if (skeleton.ScaleY > 0 && value || skeleton.ScaleY < 0 && !value)
skeleton.ScaleY *= -1; skeleton.ScaleY *= -1;
Update(0);
} }
} }
public override string CurrentAnimation public override string Skin
{
get => skeleton.Skin?.Name ?? "default";
set
{
if (!skinNames.Contains(value)) return;
skeleton.SetSkin(value);
skeleton.SetSlotsToSetupPose();
Update(0);
}
}
public override string Track0Animation
{ {
get => animationState.GetCurrent(0)?.Animation.Name ?? EMPTY_ANIMATION; get => animationState.GetCurrent(0)?.Animation.Name ?? EMPTY_ANIMATION;
set set
@@ -148,18 +160,6 @@ namespace SpineViewer.Spine.Implementations.Spine
} }
} }
public override string CurrentSkin
{
get => skeleton.Skin?.Name ?? "default";
set
{
if (!skinNames.Contains(value)) return;
skeleton.SetSkin(value);
skeleton.SetToSetupPose();
Update(0);
}
}
public override RectangleF Bounds public override RectangleF Bounds
{ {
get get
@@ -264,7 +264,7 @@ namespace SpineViewer.Spine.Implementations.Spine
{ {
// XXX: 实测不用设置 sampler2D 的值也正确 // XXX: 实测不用设置 sampler2D 的值也正确
if (UsePremultipliedAlpha && (states.BlendMode == BlendModeSFML.Normal || states.BlendMode == BlendModeSFML.Additive)) if (UsePremultipliedAlpha && (states.BlendMode == BlendModeSFML.Normal || states.BlendMode == BlendModeSFML.Additive))
states.Shader = FragmentShader; states.Shader = Shader.FragmentShader;
else else
states.Shader = null; states.Shader = null;
@@ -314,7 +314,7 @@ namespace SpineViewer.Spine.Implementations.Spine
clipping.ClipEnd(); clipping.ClipEnd();
if (UsePremultipliedAlpha && (states.BlendMode == BlendModeSFML.Normal || states.BlendMode == BlendModeSFML.Additive)) if (UsePremultipliedAlpha && (states.BlendMode == BlendModeSFML.Normal || states.BlendMode == BlendModeSFML.Additive))
states.Shader = FragmentShader; states.Shader = Shader.FragmentShader;
else else
states.Shader = null; states.Shader = null;

View File

@@ -0,0 +1,35 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace SpineViewer.Spine
{
public static class Shader
{
/// <summary>
/// 用于解决 PMA 和渐变动画问题的片段着色器
/// </summary>
private const string FRAGMENT_SHADER = (
"uniform sampler2D t;" +
"void main() { vec4 p = texture2D(t, gl_TexCoord[0].xy);" +
"if (p.a > 0) p.rgb /= max(max(max(p.r, p.g), p.b), p.a);" +
"gl_FragColor = gl_Color * p; }"
);
/// <summary>
/// 针对预乘 Alpha 通道的片段着色器
/// </summary>
public static SFML.Graphics.Shader? FragmentShader { get; private set; }
/// <summary>
/// 加载 Shader, 可能会存在异常导致着色器加载失败
/// </summary>
/// <exception cref="SFML.LoadingFailedException"></exception>
public static void Init()
{
FragmentShader = SFML.Graphics.Shader.FromString(null, null, FRAGMENT_SHADER);
}
}
}

View File

@@ -15,46 +15,12 @@ namespace SpineViewer.Spine
/// <summary> /// <summary>
/// SkeletonConverter 基类, 使用静态方法 New 来创建具体版本对象 /// SkeletonConverter 基类, 使用静态方法 New 来创建具体版本对象
/// </summary> /// </summary>
public abstract class SkeletonConverter public abstract class SkeletonConverter : ImplementationResolver<SkeletonConverter, SpineImplementationAttribute, SpineVersion>
{ {
/// <summary>
/// 实现类缓存
/// </summary>
private static readonly Dictionary<Version, Type> ImplementationTypes = [];
public static readonly Dictionary<Version, Type>.KeyCollection ImplementedVersions;
/// <summary>
/// 静态构造函数
/// </summary>
static SkeletonConverter()
{
// 遍历并缓存标记了 SpineImplementationAttribute 的类型
var impTypes = Assembly.GetExecutingAssembly().GetTypes().Where(t => typeof(SkeletonConverter).IsAssignableFrom(t) && !t.IsAbstract);
foreach (var type in impTypes)
{
var attr = type.GetCustomAttribute<SpineImplementationAttribute>();
if (attr is not null)
{
if (ImplementationTypes.ContainsKey(attr.Version))
throw new InvalidOperationException($"Multiple implementations found: {attr.Version}");
ImplementationTypes[attr.Version] = type;
}
}
Program.Logger.Debug("Find SkeletonConverter implementations: [{}]", string.Join(", ", ImplementationTypes.Keys));
ImplementedVersions = ImplementationTypes.Keys;
}
/// <summary> /// <summary>
/// 创建特定版本的 SkeletonConverter /// 创建特定版本的 SkeletonConverter
/// </summary> /// </summary>
public static SkeletonConverter New(Version version) public static SkeletonConverter New(SpineVersion version) => New(version, []);
{
if (!ImplementationTypes.TryGetValue(version, out var cvterType))
{
throw new NotImplementedException($"Not implemented version: {version}");
}
return (SkeletonConverter)Activator.CreateInstance(cvterType);
}
/// <summary> /// <summary>
/// Json 格式控制 /// Json 格式控制
@@ -123,7 +89,7 @@ namespace SpineViewer.Spine
/// <summary> /// <summary>
/// 转换到目标版本 /// 转换到目标版本
/// </summary> /// </summary>
public abstract JsonObject ToVersion(JsonObject root, Version version); public abstract JsonObject ToVersion(JsonObject root, SpineVersion version);
/// <summary> /// <summary>
/// 二进制骨骼文件读 /// 二进制骨骼文件读

View File

@@ -21,205 +21,101 @@ namespace SpineViewer.Spine
/// <summary> /// <summary>
/// Spine 基类, 使用静态方法 New 来创建具体版本对象 /// Spine 基类, 使用静态方法 New 来创建具体版本对象
/// </summary> /// </summary>
public abstract class Spine : SFML.Graphics.Drawable, IDisposable public abstract class Spine : ImplementationResolver<Spine, SpineImplementationAttribute, SpineVersion>, SFML.Graphics.Drawable, IDisposable
{ {
/// <summary>
/// 常规骨骼文件后缀集合
/// </summary>
public static readonly ImmutableHashSet<string> CommonSkelSuffix = [".skel", ".json"];
/// <summary> /// <summary>
/// 空动画标记 /// 空动画标记
/// </summary> /// </summary>
public const string EMPTY_ANIMATION = "<Empty>"; protected const string EMPTY_ANIMATION = "<Empty>";
/// <summary> /// <summary>
/// 预览图宽 /// 预览图宽
/// </summary> /// </summary>
public const uint PREVIEW_WIDTH = 256; protected const uint PREVIEW_WIDTH = 256;
/// <summary> /// <summary>
/// 预览图高 /// 预览图高
/// </summary> /// </summary>
public const uint PREVIEW_HEIGHT = 256; protected const uint PREVIEW_HEIGHT = 256;
/// <summary> /// <summary>
/// 缩放最小值 /// 缩放最小值
/// </summary> /// </summary>
public const float SCALE_MIN = 0.001f; protected const float SCALE_MIN = 0.001f;
/// <summary>
/// 实现类缓存
/// </summary>
private static readonly Dictionary<Version, Type> ImplementationTypes = [];
public static readonly Dictionary<Version, Type>.KeyCollection ImplementedVersions;
/// <summary>
/// 用于解决 PMA 和渐变动画问题的片段着色器
/// </summary>
private const string FRAGMENT_SHADER = (
"uniform sampler2D t;" +
"void main() { vec4 p = texture2D(t, gl_TexCoord[0].xy);" +
"if (p.a > 0) p.rgb /= max(max(max(p.r, p.g), p.b), p.a);" +
"gl_FragColor = gl_Color * p; }"
);
/// <summary>
/// 用于解决 PMA 和渐变动画问题的片段着色器
/// </summary>
protected static readonly SFML.Graphics.Shader? FragmentShader = null;
/// <summary>
/// 静态构造函数
/// </summary>
static Spine()
{
// 遍历并缓存标记了 SpineImplementationAttribute 的类型
var impTypes = Assembly.GetExecutingAssembly().GetTypes().Where(t => typeof(Spine).IsAssignableFrom(t) && !t.IsAbstract);
foreach (var type in impTypes)
{
var attr = type.GetCustomAttribute<SpineImplementationAttribute>();
if (attr is not null)
{
if (ImplementationTypes.ContainsKey(attr.Version))
throw new InvalidOperationException($"Multiple implementations found: {attr.Version}");
ImplementationTypes[attr.Version] = type;
}
}
Program.Logger.Debug("Find Spine implementations: [{}]", string.Join(", ", ImplementationTypes.Keys));
ImplementedVersions = ImplementationTypes.Keys;
// 加载 FragmentShader
try
{
FragmentShader = SFML.Graphics.Shader.FromString(null, null, FRAGMENT_SHADER);
}
catch (Exception ex)
{
FragmentShader = null;
Program.Logger.Error(ex.ToString());
Program.Logger.Error("Failed to load fragment shader");
MessageBox.Warn("Fragment shader 加载失败预乘Alpha通道属性失效");
}
}
/// <summary>
/// 尝试检测骨骼文件版本
/// </summary>
public static Version? GetVersion(string skelPath)
{
string versionString = null;
Version? version = null;
using var input = File.OpenRead(skelPath);
var reader = new SkeletonConverter.BinaryReader(input);
// try json format
try
{
if (JsonNode.Parse(input) is JsonObject root && root.TryGetPropertyValue("skeleton", out var node) &&
node is JsonObject _skeleton && _skeleton.TryGetPropertyValue("spine", out var _version))
versionString = (string)_version;
}
catch { }
// try v4 binary format
if (versionString is null)
{
try
{
input.Position = 0;
var hash = reader.ReadLong();
var versionPosition = input.Position;
var versionByteCount = reader.ReadVarInt();
input.Position = versionPosition;
if (versionByteCount <= 13)
versionString = reader.ReadString();
}
catch { }
}
// try v3 binary format
if (versionString is null)
{
try
{
input.Position = 0;
var hash = reader.ReadString();
versionString = reader.ReadString();
}
catch { }
}
if (versionString is not null)
{
if (versionString.StartsWith("2.1.")) version = Version.V21;
else if (versionString.StartsWith("3.6.")) version = Version.V36;
else if (versionString.StartsWith("3.7.")) version = Version.V37;
else if (versionString.StartsWith("3.8.")) version = Version.V38;
else if (versionString.StartsWith("4.0.")) version = Version.V40;
else if (versionString.StartsWith("4.1.")) version = Version.V41;
else if (versionString.StartsWith("4.2.")) version = Version.V42;
else if (versionString.StartsWith("4.3.")) version = Version.V43;
else Program.Logger.Error("Unknown verison: {}, {}", versionString, skelPath);
}
return version;
}
/// <summary> /// <summary>
/// 创建特定版本的 Spine /// 创建特定版本的 Spine
/// </summary> /// </summary>
public static Spine New(Version version, string skelPath, string? atlasPath = null) public static Spine New(SpineVersion version, string skelPath, string? atlasPath = null)
{ {
if (version == Version.Auto) if (version == SpineVersion.Auto) version = SpineHelper.GetVersion(skelPath);
{ atlasPath ??= Path.ChangeExtension(skelPath, ".atlas");
if (GetVersion(skelPath) is Version detectedVersion) return New(version, [skelPath, atlasPath]).PostInit();
version = detectedVersion;
else
throw new InvalidDataException($"Auto version detection failed for {skelPath}, try to use a specific version");
}
if (!ImplementationTypes.TryGetValue(version, out var spineType))
{
throw new NotImplementedException($"Not implemented version: {version}");
}
return (Spine)Activator.CreateInstance(spineType, skelPath, atlasPath);
} }
/// <summary> /// <summary>
/// 构造函数 /// 构造函数
/// </summary> /// </summary>
public Spine(string skelPath, string? atlasPath = null) public Spine(string skelPath, string atlasPath)
{ {
// 获取子类类型 Version = GetType().GetCustomAttribute<SpineImplementationAttribute>().ImplementationKey;
var type = GetType();
var attr = type.GetCustomAttribute<SpineImplementationAttribute>();
if (attr is null)
{
throw new InvalidOperationException($"Class {type.Name} has no SpineImplementationAttribute");
}
atlasPath ??= Path.ChangeExtension(skelPath, ".atlas");
// 设置 Version
Version = attr.Version;
AssetsDir = Directory.GetParent(skelPath).FullName; AssetsDir = Directory.GetParent(skelPath).FullName;
SkelPath = Path.GetFullPath(skelPath); SkelPath = Path.GetFullPath(skelPath);
AtlasPath = Path.GetFullPath(atlasPath); AtlasPath = Path.GetFullPath(atlasPath);
Name = Path.GetFileNameWithoutExtension(skelPath); Name = Path.GetFileNameWithoutExtension(skelPath);
} }
/// <summary>
/// 构造函数之后的初始化工作
/// </summary>
private Spine PostInit()
{
SkinNames = skinNames.AsReadOnly();
AnimationNames = animationNames.AsReadOnly();
InitBounds = Bounds;
// XXX: tex 没办法在这里主动 Dispose
// 批量添加在获取预览图的时候极大概率会和预览线程死锁
// 虽然两边不会同时调用 Draw, 但是死锁似乎和 Draw 函数有关
// 除此之外, 似乎还和 tex 的 Dispose 有关
// 如果不对 tex 进行 Dispose, 那么不管是否 Draw 都正常不会死锁
var tex = new SFML.Graphics.RenderTexture(PREVIEW_WIDTH, PREVIEW_HEIGHT);
tex.SetView(InitBounds.GetView(PREVIEW_WIDTH, PREVIEW_HEIGHT));
tex.Clear(SFML.Graphics.Color.Transparent);
tex.Draw(this);
tex.Display();
using (var img = tex.Texture.CopyToImage())
{
if (img.SaveToMemory(out var imgBuffer, "bmp"))
{
// 必须重复构造一个副本才能摆脱对流的依赖, 否则之后使用会报错
using var stream = new MemoryStream(imgBuffer);
using var bitmap = new Bitmap(stream);
Preview = new Bitmap(bitmap);
}
}
// 取最后一个作为初始, 尽可能去显示非默认的内容
Skin = SkinNames.Last();
Track0Animation = AnimationNames.Last();
return this;
}
~Spine() { Dispose(false); } ~Spine() { Dispose(false); }
public void Dispose() { Dispose(true); GC.SuppressFinalize(this); } public void Dispose() { Dispose(true); GC.SuppressFinalize(this); }
protected virtual void Dispose(bool disposing) { preview?.Dispose(); } protected virtual void Dispose(bool disposing) { Preview?.Dispose(); }
#region | [0] #region | [0]
/// <summary> /// <summary>
/// 获取所属版本 /// 获取所属版本
/// </summary> /// </summary>
[TypeConverter(typeof(VersionConverter))] [TypeConverter(typeof(SpineVersionConverter))]
[Category("[0] "), DisplayName("")] [Category("[0] "), DisplayName("")]
public Version Version { get; } public SpineVersion Version { get; }
/// <summary> /// <summary>
/// 资源所在完整目录 /// 资源所在完整目录
@@ -300,39 +196,37 @@ namespace SpineViewer.Spine
#region | [3] #region | [3]
/// <summary>
/// 包含的所有动画名称
/// </summary>
[Browsable(false)]
public ReadOnlyCollection<string> AnimationNames { get => animationNames.AsReadOnly(); }
protected List<string> animationNames = [EMPTY_ANIMATION];
/// <summary>
/// 当前动画名称, 如果设置的动画不存在则忽略
/// </summary>
[TypeConverter(typeof(AnimationConverter))]
[Category("[3] "), DisplayName("")]
public abstract string CurrentAnimation { get; set; }
/// <summary>
/// 当前动画时长
/// </summary>
[Category("[3] "), DisplayName("")]
public float CurrentAnimationDuration { get => GetAnimationDuration(CurrentAnimation); }
/// <summary> /// <summary>
/// 包含的所有皮肤名称 /// 包含的所有皮肤名称
/// </summary> /// </summary>
[Browsable(false)] public ReadOnlyCollection<string> SkinNames { get; private set; }
public ReadOnlyCollection<string> SkinNames { get => skinNames.AsReadOnly(); }
protected List<string> skinNames = []; protected List<string> skinNames = [];
/// <summary> /// <summary>
/// 当前皮肤名称, 如果设置的皮肤不存在则忽略 /// 使用的皮肤名称, 如果设置的皮肤不存在则忽略
/// </summary> /// </summary>
[TypeConverter(typeof(SkinConverter))] [TypeConverter(typeof(SkinConverter))]
[Category("[3] "), DisplayName("")] [Category("[3] "), DisplayName("")]
public abstract string CurrentSkin { get; set; } public abstract string Skin { get; set; }
/// <summary>
/// 包含的所有动画名称
/// </summary>
public ReadOnlyCollection<string> AnimationNames { get; private set; }
protected List<string> animationNames = [EMPTY_ANIMATION];
/// <summary>
/// 默认轨道动画名称, 如果设置的动画不存在则忽略
/// </summary>
[TypeConverter(typeof(AnimationConverter))]
[Category("[3] "), DisplayName("")]
public abstract string Track0Animation { get; set; }
/// <summary>
/// 默认轨道动画时长
/// </summary>
[Category("[3] "), DisplayName("")]
public float Track0AnimationDuration { get => GetAnimationDuration(Track0Animation); } // TODO: 动画时长变成伪属性在面板显示
#endregion #endregion
@@ -344,16 +238,6 @@ namespace SpineViewer.Spine
[Browsable(false)] [Browsable(false)]
public bool IsDebug { get; set; } = false; public bool IsDebug { get; set; } = false;
/// <summary>
/// 包围盒颜色
/// </summary>
protected static readonly SFML.Graphics.Color BoundsColor = new(120, 200, 0);
/// <summary>
/// 包围盒顶点数组
/// </summary>
protected readonly SFML.Graphics.VertexArray boundsVertices = new(SFML.Graphics.PrimitiveType.LineStrip, 5);
/// <summary> /// <summary>
/// 显示纹理 /// 显示纹理
/// </summary> /// </summary>
@@ -369,7 +253,7 @@ namespace SpineViewer.Spine
/// <summary> /// <summary>
/// 显示骨骼 /// 显示骨骼
/// </summary> /// </summary>
[Category("[4] "), DisplayName("")] [Category("[4] "), DisplayName("")]
public bool DebugBones { get; set; } = false; public bool DebugBones { get; set; } = false;
#endregion #endregion
@@ -395,55 +279,13 @@ namespace SpineViewer.Spine
/// 初始状态下的骨骼包围盒 /// 初始状态下的骨骼包围盒
/// </summary> /// </summary>
[Browsable(false)] [Browsable(false)]
public RectangleF InitBounds public RectangleF InitBounds { get; private set; }
{
get
{
if (initBounds is null)
{
var tmp = CurrentAnimation;
CurrentAnimation = EMPTY_ANIMATION;
initBounds = Bounds;
CurrentAnimation = tmp;
}
return (RectangleF)initBounds;
}
}
private RectangleF? initBounds = null;
/// <summary> /// <summary>
/// 骨骼预览图 /// 骨骼预览图
/// </summary> /// </summary>
[Browsable(false)] [Browsable(false)]
public Image Preview public Image Preview { get; private set; }
{
get
{
if (preview is null)
{
// XXX: tex 没办法在这里主动 Dispose
// 批量添加在获取预览图的时候极大概率会和预览线程死锁
// 虽然两边不会同时调用 Draw, 但是死锁似乎和 Draw 函数有关
// 除此之外, 似乎还和 tex 的 Dispose 有关
// 如果不对 tex 进行 Dispose, 那么不管是否 Draw 都正常不会死锁
var tex = new SFML.Graphics.RenderTexture(PREVIEW_WIDTH, PREVIEW_HEIGHT);
tex.SetView(InitBounds.GetView(PREVIEW_WIDTH, PREVIEW_HEIGHT));
tex.Clear(SFML.Graphics.Color.Transparent);
var tmp = CurrentAnimation;
CurrentAnimation = EMPTY_ANIMATION;
tex.Draw(this);
CurrentAnimation = tmp;
tex.Display();
using var img = tex.Texture.CopyToImage();
img.SaveToMemory(out var imgBuffer, "bmp");
using var stream = new MemoryStream(imgBuffer);
preview = new Bitmap(new Bitmap(stream)); // 必须重复构造一个副本才能摆脱对流的依赖, 否则之后使用会报错
}
return preview;
}
}
private Image preview = null;
/// <summary> /// <summary>
/// 获取动画时长, 如果动画不存在则返回 0 /// 获取动画时长, 如果动画不存在则返回 0
@@ -467,6 +309,16 @@ namespace SpineViewer.Spine
/// </summary> /// </summary>
protected readonly SFML.Graphics.VertexArray vertexArray = new(SFML.Graphics.PrimitiveType.Triangles); protected readonly SFML.Graphics.VertexArray vertexArray = new(SFML.Graphics.PrimitiveType.Triangles);
/// <summary>
/// 包围盒颜色
/// </summary>
protected static readonly SFML.Graphics.Color BoundsColor = new(120, 200, 0);
/// <summary>
/// 包围盒顶点数组
/// </summary>
protected readonly SFML.Graphics.VertexArray boundsVertices = new(SFML.Graphics.PrimitiveType.LineStrip, 5);
/// <summary> /// <summary>
/// SFML.Graphics.Drawable 接口实现 /// SFML.Graphics.Drawable 接口实现
/// </summary> /// </summary>

View File

@@ -0,0 +1,159 @@
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Linq;
using System.Reflection;
using System.Text;
using System.Text.Json.Nodes;
using System.Threading.Tasks;
namespace SpineViewer.Spine
{
/// <summary>
/// 支持的 Spine 版本
/// </summary>
public enum SpineVersion
{
[Description("<Auto>")] Auto = 0x0000,
[Description("2.1.x")] V21 = 0x0201,
[Description("3.6.x")] V36 = 0x0306,
[Description("3.7.x")] V37 = 0x0307,
[Description("3.8.x")] V38 = 0x0308,
[Description("4.0.x")] V40 = 0x0400,
[Description("4.1.x")] V41 = 0x0401,
[Description("4.2.x")] V42 = 0x0402,
[Description("4.3.x")] V43 = 0x0403,
}
/// <summary>
/// Spine 实现类标记
/// </summary>
[AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = false)]
public class SpineImplementationAttribute(SpineVersion version) : Attribute, IImplementationKey<SpineVersion>
{
public SpineVersion ImplementationKey { get; private set; } = version;
}
/// <summary>
/// Spine 版本静态辅助类
/// </summary>
public static class SpineHelper
{
/// <summary>
/// 版本名称
/// </summary>
public static readonly ReadOnlyDictionary<SpineVersion, string> Names;
private static readonly Dictionary<SpineVersion, string> names = [];
/// <summary>
/// Runtime 版本字符串
/// </summary>
private static readonly Dictionary<SpineVersion, string> runtimes = [];
static SpineHelper()
{
// 初始化缓存
foreach (var value in Enum.GetValues(typeof(SpineVersion)))
{
var field = typeof(SpineVersion).GetField(value.ToString());
var attribute = field?.GetCustomAttribute<DescriptionAttribute>();
names[(SpineVersion)value] = attribute?.Description ?? value.ToString();
}
Names = names.AsReadOnly();
runtimes[SpineVersion.V21] = Assembly.GetAssembly(typeof(SpineRuntime21.Skeleton)).GetCustomAttribute<AssemblyInformationalVersionAttribute>()?.InformationalVersion;
runtimes[SpineVersion.V36] = Assembly.GetAssembly(typeof(SpineRuntime36.Skeleton)).GetCustomAttribute<AssemblyInformationalVersionAttribute>()?.InformationalVersion;
runtimes[SpineVersion.V37] = Assembly.GetAssembly(typeof(SpineRuntime37.Skeleton)).GetCustomAttribute<AssemblyInformationalVersionAttribute>()?.InformationalVersion;
runtimes[SpineVersion.V38] = Assembly.GetAssembly(typeof(SpineRuntime38.Skeleton)).GetCustomAttribute<AssemblyInformationalVersionAttribute>()?.InformationalVersion;
runtimes[SpineVersion.V40] = Assembly.GetAssembly(typeof(SpineRuntime40.Skeleton)).GetCustomAttribute<AssemblyInformationalVersionAttribute>()?.InformationalVersion;
runtimes[SpineVersion.V41] = Assembly.GetAssembly(typeof(SpineRuntime41.Skeleton)).GetCustomAttribute<AssemblyInformationalVersionAttribute>()?.InformationalVersion;
runtimes[SpineVersion.V42] = Assembly.GetAssembly(typeof(SpineRuntime42.Skeleton)).GetCustomAttribute<AssemblyInformationalVersionAttribute>()?.InformationalVersion;
}
/// <summary>
/// 版本字符串名称
/// </summary>
public static string GetName(this SpineVersion version)
{
return Names.TryGetValue(version, out var val) ? val : version.ToString();
}
/// <summary>
/// Runtime 版本字符串名称
/// </summary>
public static string GetRuntime(this SpineVersion version)
{
return runtimes.TryGetValue(version, out var val) ? val : GetName(version);
}
/// <summary>
/// 常规骨骼文件后缀集合
/// </summary>
public static readonly ImmutableHashSet<string> CommonSkelSuffix = [".skel", ".json"];
/// <summary>
/// 尝试检测骨骼文件版本
/// </summary>
/// <param name="skelPath"></param>
/// <returns></returns>
/// <exception cref="InvalidDataException"></exception>
public static SpineVersion GetVersion(string skelPath)
{
string versionString = null;
using var input = File.OpenRead(skelPath);
var reader = new SkeletonConverter.BinaryReader(input);
// try json format
try
{
if (JsonNode.Parse(input) is JsonObject root && root.TryGetPropertyValue("skeleton", out var node) &&
node is JsonObject _skeleton && _skeleton.TryGetPropertyValue("spine", out var _version))
versionString = (string)_version;
}
catch { }
// try v4 binary format
if (versionString is null)
{
try
{
input.Position = 0;
var hash = reader.ReadLong();
var versionPosition = input.Position;
var versionByteCount = reader.ReadVarInt();
input.Position = versionPosition;
if (versionByteCount <= 13)
versionString = reader.ReadString();
}
catch { }
}
// try v3 binary format
if (versionString is null)
{
try
{
input.Position = 0;
var hash = reader.ReadString();
versionString = reader.ReadString();
}
catch { }
}
if (versionString is null)
throw new InvalidDataException($"No verison detected: {skelPath}");
if (versionString.StartsWith("2.1.")) return SpineVersion.V21;
else if (versionString.StartsWith("3.6.")) return SpineVersion.V36;
else if (versionString.StartsWith("3.7.")) return SpineVersion.V37;
else if (versionString.StartsWith("3.8.")) return SpineVersion.V38;
else if (versionString.StartsWith("4.0.")) return SpineVersion.V40;
else if (versionString.StartsWith("4.1.")) return SpineVersion.V41;
else if (versionString.StartsWith("4.2.")) return SpineVersion.V42;
else if (versionString.StartsWith("4.3.")) return SpineVersion.V43;
else throw new InvalidDataException($"Unknown verison: {versionString}");
}
}
}

View File

@@ -9,35 +9,23 @@ using System.Threading.Tasks;
namespace SpineViewer.Spine namespace SpineViewer.Spine
{ {
public class VersionConverter : EnumConverter public class SpineVersionConverter : EnumConverter
{ {
public VersionConverter() : base(typeof(Version)) { } public SpineVersionConverter() : base(typeof(SpineVersion)) { }
public override object? ConvertTo(ITypeDescriptorContext? context, CultureInfo? culture, object? value, Type? destinationType) public override object? ConvertTo(ITypeDescriptorContext? context, CultureInfo? culture, object? value, Type? destinationType)
{ {
if (destinationType == typeof(string) && value is Version version) if (destinationType == typeof(string) && value is SpineVersion version)
{
// 调用自定义的 String() 方法
return version.GetName(); return version.GetName();
}
return base.ConvertTo(context, culture, value, destinationType); return base.ConvertTo(context, culture, value, destinationType);
} }
} }
public class AnimationConverter : StringConverter public class AnimationConverter : StringConverter
{ {
public override bool GetStandardValuesSupported(ITypeDescriptorContext? context) public override bool GetStandardValuesSupported(ITypeDescriptorContext? context) => true;
{
// 支持标准值列表
return true;
}
public override bool GetStandardValuesExclusive(ITypeDescriptorContext? context) public override bool GetStandardValuesExclusive(ITypeDescriptorContext? context) => true;
{
// 排他模式,只有下拉列表中的值可选
return true;
}
public override StandardValuesCollection? GetStandardValues(ITypeDescriptorContext? context) public override StandardValuesCollection? GetStandardValues(ITypeDescriptorContext? context)
{ {
@@ -61,17 +49,9 @@ namespace SpineViewer.Spine
public class SkinConverter : StringConverter public class SkinConverter : StringConverter
{ {
public override bool GetStandardValuesSupported(ITypeDescriptorContext? context) public override bool GetStandardValuesSupported(ITypeDescriptorContext? context) => true;
{
// 支持标准值列表
return true;
}
public override bool GetStandardValuesExclusive(ITypeDescriptorContext? context) public override bool GetStandardValuesExclusive(ITypeDescriptorContext? context) => true;
{
// 排他模式,只有下拉列表中的值可选
return true;
}
public override StandardValuesCollection? GetStandardValues(ITypeDescriptorContext? context) public override StandardValuesCollection? GetStandardValues(ITypeDescriptorContext? context)
{ {

View File

@@ -1,94 +0,0 @@
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Linq;
using System.Reflection;
using System.Text;
using System.Threading.Tasks;
namespace SpineViewer.Spine
{
/// <summary>
/// 支持的 Spine 版本
/// </summary>
public enum Version
{
[Description("<Auto>")] Auto = 0x0000,
[Description("2.1.x")] V21 = 0x0201,
[Description("3.6.x")] V36 = 0x0306,
[Description("3.7.x")] V37 = 0x0307,
[Description("3.8.x")] V38 = 0x0308,
[Description("4.0.x")] V40 = 0x0400,
[Description("4.1.x")] V41 = 0x0401,
[Description("4.2.x")] V42 = 0x0402,
[Description("4.3.x")] V43 = 0x0403,
}
/// <summary>
/// Spine 实现类标记
/// </summary>
[AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = false)]
public class SpineImplementationAttribute : Attribute
{
public Version Version { get; }
public SpineImplementationAttribute(Version version)
{
Version = version;
}
}
/// <summary>
/// Spine 版本静态辅助类
/// </summary>
public static class VersionHelper
{
/// <summary>
/// 版本名称
/// </summary>
public static readonly ReadOnlyDictionary<Version, string> Names;
private static readonly Dictionary<Version, string> names = [];
/// <summary>
/// Runtime 版本字符串
/// </summary>
private static readonly Dictionary<Version, string> runtimes = [];
static VersionHelper()
{
// 初始化缓存
foreach (var value in Enum.GetValues(typeof(Version)))
{
var field = typeof(Version).GetField(value.ToString());
var attribute = field?.GetCustomAttribute<DescriptionAttribute>();
names[(Version)value] = attribute?.Description ?? value.ToString();
}
Names = names.AsReadOnly();
runtimes[Version.V21] = Assembly.GetAssembly(typeof(SpineRuntime21.Skeleton)).GetCustomAttribute<AssemblyInformationalVersionAttribute>()?.InformationalVersion;
runtimes[Version.V36] = Assembly.GetAssembly(typeof(SpineRuntime36.Skeleton)).GetCustomAttribute<AssemblyInformationalVersionAttribute>()?.InformationalVersion;
runtimes[Version.V37] = Assembly.GetAssembly(typeof(SpineRuntime37.Skeleton)).GetCustomAttribute<AssemblyInformationalVersionAttribute>()?.InformationalVersion;
runtimes[Version.V38] = Assembly.GetAssembly(typeof(SpineRuntime38.Skeleton)).GetCustomAttribute<AssemblyInformationalVersionAttribute>()?.InformationalVersion;
runtimes[Version.V40] = Assembly.GetAssembly(typeof(SpineRuntime40.Skeleton)).GetCustomAttribute<AssemblyInformationalVersionAttribute>()?.InformationalVersion;
runtimes[Version.V41] = Assembly.GetAssembly(typeof(SpineRuntime41.Skeleton)).GetCustomAttribute<AssemblyInformationalVersionAttribute>()?.InformationalVersion;
runtimes[Version.V42] = Assembly.GetAssembly(typeof(SpineRuntime42.Skeleton)).GetCustomAttribute<AssemblyInformationalVersionAttribute>()?.InformationalVersion;
}
/// <summary>
/// 版本字符串名称
/// </summary>
public static string GetName(this Version version)
{
return Names.TryGetValue(version, out var val) ? val : version.ToString();
}
/// <summary>
/// Runtime 版本字符串名称
/// </summary>
public static string GetRuntime(this Version version)
{
return runtimes.TryGetValue(version, out var val) ? val : GetName(version);
}
}
}

View File

@@ -8,7 +8,7 @@
<RuntimeIdentifier>win-x64</RuntimeIdentifier> <RuntimeIdentifier>win-x64</RuntimeIdentifier>
<BaseOutputPath>$(SolutionDir)out</BaseOutputPath> <BaseOutputPath>$(SolutionDir)out</BaseOutputPath>
<IncludeSourceRevisionInInformationalVersion>false</IncludeSourceRevisionInInformationalVersion> <IncludeSourceRevisionInInformationalVersion>false</IncludeSourceRevisionInInformationalVersion>
<Version>0.11.3</Version> <Version>0.11.4</Version>
<OutputType>WinExe</OutputType> <OutputType>WinExe</OutputType>
<UseWindowsForms>true</UseWindowsForms> <UseWindowsForms>true</UseWindowsForms>
<ApplicationIcon>appicon.ico</ApplicationIcon> <ApplicationIcon>appicon.ico</ApplicationIcon>

View File

@@ -0,0 +1,65 @@
using System;
using System.Runtime.InteropServices;
using System.Windows.Forms;
namespace SpineViewer
{
internal enum TBPFLAG
{
TBPF_NOPROGRESS = 0,
TBPF_INDETERMINATE = 0x1,
TBPF_NORMAL = 0x2,
TBPF_ERROR = 0x4,
TBPF_PAUSED = 0x8
}
[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
[ComImport, Guid("ea1afb91-9e28-4b86-90e9-9e9f8a5eefaf")]
internal interface ITaskbarList3
{
// ITaskbarList
void HrInit();
void AddTab(IntPtr hwnd);
void DeleteTab(IntPtr hwnd);
void ActivateTab(IntPtr hwnd);
void SetActiveAlt(IntPtr hwnd);
// ITaskbarList2
void MarkFullscreenWindow(IntPtr hwnd, [MarshalAs(UnmanagedType.Bool)] bool fFullscreen);
// ITaskbarList3
void SetProgressValue(IntPtr hwnd, ulong ullCompleted, ulong ullTotal);
void SetProgressState(IntPtr hwnd, TBPFLAG tbpFlags);
//void RegisterTab(IntPtr hwndTab, IntPtr hwndMDI);
//void UnregisterTab(IntPtr hwndTab);
//void SetTabOrder(IntPtr hwndTab, IntPtr hwndInsertBefore);
//void SetTabActive(IntPtr hwndTab, IntPtr hwndMDI, uint dwReserved);
//void ThumbBarAddButtons(IntPtr hwnd, uint cButtons, THUMBBUTTON[] pButton);
//void ThumbBarUpdateButtons(IntPtr hwnd, uint cButtons, THUMBBUTTON[] pButton);
//void ThumbBarSetImageList(IntPtr hwnd, IntPtr himl);
//void SetOverlayIcon(IntPtr hwnd, IntPtr hIcon, string pszDescription);
//void SetThumbnailTooltip(IntPtr hwnd, string pszTip);
//void SetThumbnailClip(IntPtr hwnd, ref RECT prcClip);
}
[ComImport, Guid("56FDF344-FD6D-11d0-958A-006097C9A090")]
internal class TaskbarList { }
internal static class TaskbarManager
{
private static readonly ITaskbarList3 taskbarList = (ITaskbarList3)new TaskbarList();
static TaskbarManager()
{
taskbarList.HrInit();
}
public static void SetProgressState(IntPtr windowHandle, TBPFLAG state)
{
taskbarList.SetProgressState(windowHandle, state);
}
public static void SetProgressValue(IntPtr windowHandle, ulong completed, ulong total)
{
taskbarList.SetProgressValue(windowHandle, completed, total);
}
}
}

View File

@@ -9,7 +9,7 @@ using System.Threading.Tasks;
namespace SpineViewer namespace SpineViewer
{ {
public class PointFConverter : TypeConverter public class PointFConverter : ExpandableObjectConverter
{ {
public override bool CanConvertTo(ITypeDescriptorContext? context, [NotNullWhen(true)] Type? destinationType) public override bool CanConvertTo(ITypeDescriptorContext? context, [NotNullWhen(true)] Type? destinationType)
{ {
@@ -44,12 +44,5 @@ namespace SpineViewer
} }
return base.ConvertFrom(context, culture, value); return base.ConvertFrom(context, culture, value);
} }
public override PropertyDescriptorCollection GetProperties(ITypeDescriptorContext? context, object value, Attribute[]? attributes)
{
return TypeDescriptor.GetProperties(typeof(PointF), attributes);
}
public override bool GetPropertiesSupported(ITypeDescriptorContext? context) => true;
} }
} }

Binary file not shown.

Before

Width:  |  Height:  |  Size: 157 KiB

After

Width:  |  Height:  |  Size: 163 KiB