Compare commits

..

44 Commits

Author SHA1 Message Date
ww-rm
8c1f6fb4a6 update preview 2025-04-13 00:25:16 +08:00
ww-rm
df82ed8a00 更新至v0.12.3 2025-04-13 00:22:36 +08:00
ww-rm
d01e3920ba update changelog 2025-04-13 00:22:02 +08:00
ww-rm
777cd5ea3f 增加部分调试渲染功能 2025-04-13 00:19:02 +08:00
ww-rm
3b73aea5c0 增加调试骨骼 2025-04-12 20:58:56 +08:00
ww-rm
168f7a8173 修复maxFps时快进帧除0错误 2025-04-12 20:57:11 +08:00
ww-rm
04437e2de2 修改文本描述 2025-04-12 13:28:12 +08:00
ww-rm
2ec83b2e87 增加ctrl滚轮对模型缩放 2025-04-12 12:58:21 +08:00
ww-rm
90bfaa7b56 整理结构 2025-04-12 11:35:36 +08:00
ww-rm
2ae175abd0 small change 2025-04-11 13:55:19 +08:00
ww-rm
e2a84d8f88 small change 2025-04-11 00:58:25 +08:00
ww-rm
b6f9cd0c7c small change 2025-04-11 00:07:09 +08:00
ww-rm
61b7b90722 修改缩放模式 2025-04-10 10:27:09 +08:00
ww-rm
093c159753 修改布局 2025-04-10 10:21:31 +08:00
ww-rm
32d36c0757 补充遗漏参数项 2025-04-09 15:56:51 +08:00
ww-rm
94dabebf2b 修正文字说明 2025-04-09 15:34:03 +08:00
ww-rm
8e875d4f7e update changelog 2025-04-09 15:25:15 +08:00
ww-rm
86c383f2cf 默认使用libwebp_anim编码器 2025-04-09 15:24:48 +08:00
ww-rm
b404d8e79a 增加最后一帧参数 2025-04-09 15:24:31 +08:00
ww-rm
d32b480ef2 更新readme 2025-04-09 14:00:26 +08:00
ww-rm
3654825f27 补充文字说明 2025-04-09 13:52:59 +08:00
ww-rm
d7231e8a09 更新至版本v0.12.2 2025-04-09 13:43:21 +08:00
ww-rm
98161aaf2e update changelog 2025-04-09 13:40:34 +08:00
ww-rm
7a942b16bc 调整动图默认帧率 2025-04-09 13:40:11 +08:00
ww-rm
067719c69b 补充面板属性刷新 2025-04-09 13:36:34 +08:00
ww-rm
f3fce53b91 增加导出参数缓存 2025-04-09 13:19:25 +08:00
ww-rm
e35903f436 增加webp和avif动图格式 2025-04-09 13:19:17 +08:00
ww-rm
64cfe5fdd7 限定readonly 2025-04-09 01:09:57 +08:00
ww-rm
dbe586cff8 调整结构 2025-04-09 01:07:17 +08:00
ww-rm
3104733db0 补充调试参数 2025-04-08 15:00:42 +08:00
ww-rm
9d4bdd1028 标题栏增加版本号 2025-04-08 13:51:10 +08:00
ww-rm
f8030b1645 整合参数标签页 2025-04-08 13:44:30 +08:00
ww-rm
0a999ceb41 增加 SpinePropertyGrid 2025-04-08 13:21:29 +08:00
ww-rm
64bd9907cb 重构 2025-04-07 15:06:23 +08:00
ww-rm
580eaf990d 测试代码 2025-04-05 11:57:44 +08:00
ww-rm
5ab232a961 更新至v0.12.1 2025-04-05 11:56:53 +08:00
ww-rm
e596cd7ea4 update changelog 2025-04-05 11:56:13 +08:00
ww-rm
05c47a4daa 增加初始动画皮肤空位 2025-04-05 11:52:34 +08:00
ww-rm
5a8783b5f4 增加确定按钮 2025-04-05 11:18:54 +08:00
ww-rm
08bc171a72 修复分辨率调整时父容器尺寸获取错误bug 2025-04-05 10:38:17 +08:00
ww-rm
7372f5fe08 optimize 2025-04-05 10:31:30 +08:00
ww-rm
6f032bdd05 optimize 2025-04-05 10:10:04 +08:00
ww-rm
153d3603d2 optimize 2025-04-05 09:40:02 +08:00
ww-rm
95261e6907 optimize 2025-04-05 01:53:39 +08:00
92 changed files with 5669 additions and 3328 deletions

View File

@@ -1,5 +1,28 @@
# CHANGELOG
## v0.12.3
- 增加按住 ctrl 缩放选中模型
- 增加对骨骼/网格/剪裁的调试渲染
- 换回以前的上下参数面板布局
- 修改窗口缩放模式为 Font -> Dpi
- 修复部分问题
## v0.12.2
- 模型参数分标签显示
- 皮肤/动画列表使用右键菜单进行增删
- 标题栏显示版本号
- 增加 webp 和 avif 动图格式
- 增加导出参数缓存
- 动图默认帧率修改为 24 帧
- 增加保留最后一帧参数
## v0.12.1
- 优化使用体验, 提供初始皮肤/动画空位
- 修复预览画面分辨率调整时父容器尺寸获取错误
## v0.12.0
- 支持皮肤列表 (仅 3.8.x 及以上支持)

View File

@@ -31,12 +31,11 @@
| 导出格式 | 适用场景 |
| --- | --- |
| 单帧画面 | 支持生成高清模型画面图像, 可手动调节需要的一帧. |
| 帧序列 | 支持 png 格式帧序列, 可保留透明通道且无损压缩. |
| GIF | 适合生成预览动图. |
| 帧序列 | 支持 PNG 格式帧序列, 可保留透明通道且无损压缩. |
| GIF/WebP/AVIF | 适合生成预览动图. |
| MP4 | 最常见的视频格式, 兼容性最好. |
| WebM | 适合浏览器在线播放格式, 支持透明背景. |
| MKV | 适合折腾. |
| MOV | 适合折腾. |
| MKV/MOV | 适合折腾. |
| 自定义导出 | 除上述预设方案, 支持提供任意 FFmpeg 参数进行导出, 满足自定义复杂需求. |
## Spine 版本支持

View File

@@ -34,14 +34,14 @@ namespace SpineViewer.Controls
{
if (File.Exists(path))
{
if (SpineHelper.CommonSkelSuffix.Contains(Path.GetExtension(path).ToLower()))
if (SpineUtils.CommonSkelSuffix.Contains(Path.GetExtension(path).ToLower()))
listBox.Items.Add(Path.GetFullPath(path));
}
else if (Directory.Exists(path))
{
foreach (var file in Directory.EnumerateFiles(path, "*.*", SearchOption.AllDirectories))
{
if (SpineHelper.CommonSkelSuffix.Contains(Path.GetExtension(file).ToLower()))
if (SpineUtils.CommonSkelSuffix.Contains(Path.GetExtension(file).ToLower()))
listBox.Items.Add(file);
}
}
@@ -58,7 +58,7 @@ namespace SpineViewer.Controls
{
foreach (var file in Directory.EnumerateFiles(path, "*.*", SearchOption.AllDirectories))
{
if (SpineHelper.CommonSkelSuffix.Contains(Path.GetExtension(file).ToLower()))
if (SpineUtils.CommonSkelSuffix.Contains(Path.GetExtension(file).ToLower()))
listBox.Items.Add(file);
}
}

View File

@@ -98,21 +98,21 @@
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.Name = "contextMenuStrip";
contextMenuStrip.Size = new Size(329, 418);
contextMenuStrip.Size = new Size(255, 451);
contextMenuStrip.Closed += contextMenuStrip_Closed;
contextMenuStrip.Opening += contextMenuStrip_Opening;
//
// toolStripMenuItem_Add
//
toolStripMenuItem_Add.Name = "toolStripMenuItem_Add";
toolStripMenuItem_Add.Size = new Size(328, 30);
toolStripMenuItem_Add.Size = new Size(254, 30);
toolStripMenuItem_Add.Text = "添加...";
toolStripMenuItem_Add.Click += toolStripMenuItem_Add_Click;
//
// toolStripMenuItem_Insert
//
toolStripMenuItem_Insert.Name = "toolStripMenuItem_Insert";
toolStripMenuItem_Insert.Size = new Size(328, 30);
toolStripMenuItem_Insert.Size = new Size(254, 30);
toolStripMenuItem_Insert.Text = "插入...";
toolStripMenuItem_Insert.Click += toolStripMenuItem_Insert_Click;
//
@@ -120,39 +120,39 @@
//
toolStripMenuItem_Remove.Name = "toolStripMenuItem_Remove";
toolStripMenuItem_Remove.ShortcutKeys = Keys.Delete;
toolStripMenuItem_Remove.Size = new Size(328, 30);
toolStripMenuItem_Remove.Size = new Size(254, 30);
toolStripMenuItem_Remove.Text = "移除";
toolStripMenuItem_Remove.Click += toolStripMenuItem_Remove_Click;
//
// toolStripSeparator1
//
toolStripSeparator1.Name = "toolStripSeparator1";
toolStripSeparator1.Size = new Size(325, 6);
toolStripSeparator1.Size = new Size(251, 6);
//
// toolStripMenuItem_BatchAdd
//
toolStripMenuItem_BatchAdd.Name = "toolStripMenuItem_BatchAdd";
toolStripMenuItem_BatchAdd.Size = new Size(328, 30);
toolStripMenuItem_BatchAdd.Size = new Size(254, 30);
toolStripMenuItem_BatchAdd.Text = "批量添加...";
toolStripMenuItem_BatchAdd.Click += toolStripMenuItem_BatchAdd_Click;
//
// toolStripMenuItem_RemoveAll
//
toolStripMenuItem_RemoveAll.Name = "toolStripMenuItem_RemoveAll";
toolStripMenuItem_RemoveAll.Size = new Size(328, 30);
toolStripMenuItem_RemoveAll.Size = new Size(254, 30);
toolStripMenuItem_RemoveAll.Text = "移除全部";
toolStripMenuItem_RemoveAll.Click += toolStripMenuItem_RemoveAll_Click;
//
// toolStripSeparator2
//
toolStripSeparator2.Name = "toolStripSeparator2";
toolStripSeparator2.Size = new Size(325, 6);
toolStripSeparator2.Size = new Size(251, 6);
//
// toolStripMenuItem_MoveUp
//
toolStripMenuItem_MoveUp.Name = "toolStripMenuItem_MoveUp";
toolStripMenuItem_MoveUp.ShortcutKeys = Keys.Alt | Keys.W;
toolStripMenuItem_MoveUp.Size = new Size(328, 30);
toolStripMenuItem_MoveUp.Size = new Size(254, 30);
toolStripMenuItem_MoveUp.Text = "上移";
toolStripMenuItem_MoveUp.Click += toolStripMenuItem_MoveUp_Click;
//
@@ -160,7 +160,7 @@
//
toolStripMenuItem_MoveDown.Name = "toolStripMenuItem_MoveDown";
toolStripMenuItem_MoveDown.ShortcutKeys = Keys.Alt | Keys.S;
toolStripMenuItem_MoveDown.Size = new Size(328, 30);
toolStripMenuItem_MoveDown.Size = new Size(254, 30);
toolStripMenuItem_MoveDown.Text = "下移";
toolStripMenuItem_MoveDown.Click += toolStripMenuItem_MoveDown_Click;
//
@@ -168,7 +168,7 @@
//
toolStripMenuItem_MoveTop.Name = "toolStripMenuItem_MoveTop";
toolStripMenuItem_MoveTop.ShortcutKeys = Keys.Alt | Keys.Shift | Keys.W;
toolStripMenuItem_MoveTop.Size = new Size(328, 30);
toolStripMenuItem_MoveTop.Size = new Size(254, 30);
toolStripMenuItem_MoveTop.Text = "置顶";
toolStripMenuItem_MoveTop.Click += toolStripMenuItem_MoveTop_Click;
//
@@ -176,28 +176,28 @@
//
toolStripMenuItem_MoveBottom.Name = "toolStripMenuItem_MoveBottom";
toolStripMenuItem_MoveBottom.ShortcutKeys = Keys.Alt | Keys.Shift | Keys.S;
toolStripMenuItem_MoveBottom.Size = new Size(328, 30);
toolStripMenuItem_MoveBottom.Size = new Size(254, 30);
toolStripMenuItem_MoveBottom.Text = "置底";
toolStripMenuItem_MoveBottom.Click += toolStripMenuItem_MoveBottom_Click;
//
// toolStripSeparator3
//
toolStripSeparator3.Name = "toolStripSeparator3";
toolStripSeparator3.Size = new Size(325, 6);
toolStripSeparator3.Size = new Size(251, 6);
//
// toolStripMenuItem_CopyPreview
//
toolStripMenuItem_CopyPreview.Name = "toolStripMenuItem_CopyPreview";
toolStripMenuItem_CopyPreview.ShortcutKeys = Keys.Control | Keys.C;
toolStripMenuItem_CopyPreview.Size = new Size(328, 30);
toolStripMenuItem_CopyPreview.Text = "复制预览图 (256x256)";
toolStripMenuItem_CopyPreview.Size = new Size(254, 30);
toolStripMenuItem_CopyPreview.Text = "复制预览图";
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.Size = new Size(254, 30);
toolStripMenuItem_AddFromClipboard.Text = "从剪贴板添加";
toolStripMenuItem_AddFromClipboard.Click += toolStripMenuItem_AddFromClipboard_Click;
//
@@ -205,20 +205,20 @@
//
toolStripMenuItem_SelectAll.Name = "toolStripMenuItem_SelectAll";
toolStripMenuItem_SelectAll.ShortcutKeys = Keys.Control | Keys.A;
toolStripMenuItem_SelectAll.Size = new Size(328, 30);
toolStripMenuItem_SelectAll.Size = new Size(254, 30);
toolStripMenuItem_SelectAll.Text = "全选";
toolStripMenuItem_SelectAll.Click += toolStripMenuItem_SelectAll_Click;
//
// toolStripSeparator4
//
toolStripSeparator4.Name = "toolStripSeparator4";
toolStripSeparator4.Size = new Size(325, 6);
toolStripSeparator4.Size = new Size(251, 6);
//
// toolStripMenuItem_ChangeView
//
toolStripMenuItem_ChangeView.DropDownItems.AddRange(new ToolStripItem[] { toolStripMenuItem_LargeIconView, toolStripMenuItem_ListView, toolStripMenuItem_DetailsView });
toolStripMenuItem_ChangeView.Name = "toolStripMenuItem_ChangeView";
toolStripMenuItem_ChangeView.Size = new Size(328, 30);
toolStripMenuItem_ChangeView.Size = new Size(254, 30);
toolStripMenuItem_ChangeView.Text = "切换视图";
//
// toolStripMenuItem_LargeIconView

View File

@@ -13,25 +13,33 @@ using System.Reflection;
using System.Diagnostics;
using System.Collections.Specialized;
using NLog;
using SpineViewer.Extensions;
using SpineViewer.Utils;
using SpineViewer.Spine.SpineView;
namespace SpineViewer.Controls
{
public partial class SpineListView : UserControl
{
/// <summary>
/// 日志器
/// </summary>
private readonly Logger logger = LogManager.GetCurrentClassLogger();
/// <summary>
/// Spine 列表只读视图, 访问时必须使用 lock 语句锁定视图本身
/// </summary>
public readonly ReadOnlyCollection<Spine.Spine> Spines;
public readonly ReadOnlyCollection<Spine.SpineObject> Spines;
/// <summary>
/// Spine 列表, 访问时必须使用 lock 语句锁定只读视图 Spines
/// </summary>
private readonly List<Spine.Spine> spines = [];
private readonly List<Spine.SpineObject> spines = [];
/// <summary>
/// 日志器
/// 用于属性页显示模型参数的包装类
/// </summary>
protected readonly Logger logger = LogManager.GetCurrentClassLogger();
private readonly Dictionary<string, SpineObjectProperty> spinePropertyWrappers = [];
public SpineListView()
{
@@ -42,8 +50,8 @@ namespace SpineViewer.Controls
/// <summary>
/// 显示骨骼信息的属性面板
/// </summary>
[Category("自定义"), Description("用于显示骨骼属性的属性页")]
public PropertyGrid? PropertyGrid { get; set; }
[Category("自定义"), Description("用于显示模型属性的组合属性页")]
public SpineViewPropertyGrid? SpinePropertyGrid { get; set; }
/// <summary>
/// 选中的索引
@@ -61,8 +69,7 @@ namespace SpineViewer.Controls
private void Insert(int index = -1)
{
var dialog = new Dialogs.OpenSpineDialog();
if (dialog.ShowDialog() != DialogResult.OK)
return;
if (dialog.ShowDialog() != DialogResult.OK) return;
Insert(dialog.Result, index);
}
@@ -73,19 +80,17 @@ namespace SpineViewer.Controls
{
try
{
var spine = Spine.Spine.New(result.Version, result.SkelPath, result.AtlasPath);
var spine = Spine.SpineObject.New(result.Version, result.SkelPath, result.AtlasPath);
// 如果索引无效则在末尾添加
if (index < 0 || index > listView.Items.Count)
index = listView.Items.Count;
// 锁定外部的读操作
lock (Spines)
{
spines.Insert(index, spine);
listView.SmallImageList.Images.Add(spine.ID, spine.Preview);
listView.LargeImageList.Images.Add(spine.ID, spine.Preview);
}
lock (Spines) { spines.Insert(index, spine); }
spinePropertyWrappers[spine.ID] = new(spine);
listView.SmallImageList.Images.Add(spine.ID, spine.Preview);
listView.LargeImageList.Images.Add(spine.ID, spine.Preview);
listView.Items.Insert(index, new ListViewItem(spine.Name, spine.ID) { ToolTipText = spine.SkelPath });
// 选中新增项
@@ -96,7 +101,7 @@ namespace SpineViewer.Controls
{
logger.Error(ex.ToString());
logger.Error("Failed to load {} {}", result.SkelPath, result.AtlasPath);
MessageBox.Error(ex.ToString(), "骨骼加载失败");
MessagePopup.Error(ex.ToString(), "骨骼加载失败");
}
logger.LogCurrentProcessMemoryUsage();
@@ -108,8 +113,7 @@ namespace SpineViewer.Controls
public void BatchAdd()
{
var openDialog = new Dialogs.BatchOpenSpineDialog();
if (openDialog.ShowDialog() != DialogResult.OK)
return;
if (openDialog.ShowDialog() != DialogResult.OK) return;
BatchAdd(openDialog.Result);
}
@@ -151,9 +155,10 @@ namespace SpineViewer.Controls
try
{
var spine = Spine.Spine.New(version, skelPath);
var spine = Spine.SpineObject.New(version, skelPath);
var preview = spine.Preview;
lock (Spines) { spines.Add(spine); }
spinePropertyWrappers[spine.ID] = new(spine);
listView.Invoke(() =>
{
listView.SmallImageList.Images.Add(spine.ID, preview);
@@ -200,14 +205,14 @@ namespace SpineViewer.Controls
{
if (File.Exists(path))
{
if (SpineHelper.CommonSkelSuffix.Contains(Path.GetExtension(path).ToLower()))
if (SpineUtils.CommonSkelSuffix.Contains(Path.GetExtension(path).ToLower()))
validPaths.Add(path);
}
else if (Directory.Exists(path))
{
foreach (var file in Directory.EnumerateFiles(path, "*.*", SearchOption.AllDirectories))
{
if (SpineHelper.CommonSkelSuffix.Contains(Path.GetExtension(file).ToLower()))
if (SpineUtils.CommonSkelSuffix.Contains(Path.GetExtension(file).ToLower()))
validPaths.Add(file);
}
}
@@ -217,7 +222,7 @@ namespace SpineViewer.Controls
{
if (validPaths.Count > 100)
{
if (MessageBox.Quest($"共发现 {validPaths.Count} 个可加载骨骼,数量较多,是否一次性全部加载?") == DialogResult.Cancel)
if (MessagePopup.Quest($"共发现 {validPaths.Count} 个可加载骨骼,数量较多,是否一次性全部加载?") == DialogResult.Cancel)
return;
}
BatchAdd(new Dialogs.BatchOpenSpineDialogResult(SpineVersion.Auto, validPaths.ToArray()));
@@ -244,14 +249,14 @@ namespace SpineViewer.Controls
{
lock (Spines)
{
if (PropertyGrid is not null)
if (SpinePropertyGrid is not null)
{
if (listView.SelectedIndices.Count <= 0)
PropertyGrid.SelectedObject = null;
SpinePropertyGrid.SelectedSpines = null;
else if (listView.SelectedIndices.Count <= 1)
PropertyGrid.SelectedObject = spines[listView.SelectedIndices[0]];
SpinePropertyGrid.SelectedSpines = [spinePropertyWrappers[spines[listView.SelectedIndices[0]].ID]];
else
PropertyGrid.SelectedObjects = listView.SelectedIndices.Cast<int>().Select(index => spines[index]).ToArray();
SpinePropertyGrid.SelectedSpines = listView.SelectedIndices.Cast<int>().Select(index => spinePropertyWrappers[spines[index].ID]).ToArray();
}
// 标记选中的 Spine
@@ -406,7 +411,7 @@ namespace SpineViewer.Controls
if (listView.SelectedIndices.Count > 1)
{
if (MessageBox.Quest($"确定移除所选 {listView.SelectedIndices.Count} 项吗?") != DialogResult.OK)
if (MessagePopup.Quest($"确定移除所选 {listView.SelectedIndices.Count} 项吗?") != DialogResult.OK)
return;
}
@@ -418,6 +423,7 @@ namespace SpineViewer.Controls
listView.Items.RemoveAt(i);
var spine = spines[i];
spines.RemoveAt(i);
spinePropertyWrappers.Remove(spine.ID);
listView.SmallImageList.Images.RemoveByKey(spine.ID);
listView.LargeImageList.Images.RemoveByKey(spine.ID);
spine.Dispose();
@@ -505,7 +511,7 @@ namespace SpineViewer.Controls
if (listView.Items.Count <= 0)
return;
if (MessageBox.Quest($"确认移除所有 {listView.Items.Count} 项吗?") != DialogResult.OK)
if (MessagePopup.Quest($"确认移除所有 {listView.Items.Count} 项吗?") != DialogResult.OK)
return;
listView.Items.Clear();
@@ -513,11 +519,12 @@ namespace SpineViewer.Controls
{
foreach (var spine in spines) spine.Dispose();
spines.Clear();
spinePropertyWrappers.Clear();
listView.SmallImageList.Images.Clear();
listView.LargeImageList.Images.Clear();
}
if (PropertyGrid is not null)
PropertyGrid.SelectedObject = null;
if (SpinePropertyGrid is not null)
SpinePropertyGrid.SelectedSpines = null;
}
private void toolStripMenuItem_CopyPreview_Click(object sender, EventArgs e)
@@ -575,4 +582,9 @@ namespace SpineViewer.Controls
listView.View = View.Details;
}
}
public class DefaultSpineConfig
{
}
}

View File

@@ -1,6 +1,6 @@
namespace SpineViewer.Controls
{
partial class SpinePreviewer
partial class SpinePreviewPanel
{
/// <summary>
/// 必需的设计器变量。
@@ -29,8 +29,8 @@
private void InitializeComponent()
{
components = new System.ComponentModel.Container();
System.ComponentModel.ComponentResourceManager resources = new System.ComponentModel.ComponentResourceManager(typeof(SpinePreviewer));
panel = new Panel();
System.ComponentModel.ComponentResourceManager resources = new System.ComponentModel.ComponentResourceManager(typeof(SpinePreviewPanel));
panel_Render = new Panel();
tableLayoutPanel1 = new TableLayoutPanel();
panel_Container = new Panel();
flowLayoutPanel1 = new FlowLayoutPanel();
@@ -46,18 +46,18 @@
flowLayoutPanel1.SuspendLayout();
SuspendLayout();
//
// panel
// panel_Render
//
panel.BackColor = SystemColors.ControlDarkDark;
panel.Location = new Point(157, 136);
panel.Margin = new Padding(0);
panel.Name = "panel";
panel.Size = new Size(320, 320);
panel.TabIndex = 1;
panel.MouseDown += panel_MouseDown;
panel.MouseMove += panel_MouseMove;
panel.MouseUp += panel_MouseUp;
panel.MouseWheel += panel_MouseWheel;
panel_Render.BackColor = SystemColors.ControlDarkDark;
panel_Render.Location = new Point(157, 136);
panel_Render.Margin = new Padding(0);
panel_Render.Name = "panel_Render";
panel_Render.Size = new Size(320, 320);
panel_Render.TabIndex = 1;
panel_Render.MouseDown += panel_Render_MouseDown;
panel_Render.MouseMove += panel_Render_MouseMove;
panel_Render.MouseUp += panel_Render_MouseUp;
panel_Render.MouseWheel += panel_Render_MouseWheel;
//
// tableLayoutPanel1
//
@@ -78,7 +78,7 @@
// panel_Container
//
panel_Container.BackColor = SystemColors.ControlDark;
panel_Container.Controls.Add(panel);
panel_Container.Controls.Add(panel_Render);
panel_Container.Dock = DockStyle.Fill;
panel_Container.Location = new Point(0, 0);
panel_Container.Margin = new Padding(0);
@@ -190,14 +190,14 @@
button_ForwardFast.UseVisualStyleBackColor = true;
button_ForwardFast.Click += button_ForwardFast_Click;
//
// SpinePreviewer
// SpinePreviewPanel
//
AutoScaleDimensions = new SizeF(11F, 24F);
AutoScaleMode = AutoScaleMode.Font;
Controls.Add(tableLayoutPanel1);
Name = "SpinePreviewer";
Name = "SpinePreviewPanel";
Size = new Size(641, 636);
SizeChanged += SpinePreviewer_SizeChanged;
SizeChanged += SpinePreviewPanel_SizeChanged;
tableLayoutPanel1.ResumeLayout(false);
tableLayoutPanel1.PerformLayout();
panel_Container.ResumeLayout(false);
@@ -208,7 +208,7 @@
#endregion
private Panel panel;
private Panel panel_Render;
private TableLayoutPanel tableLayoutPanel1;
private Panel panel_Container;
private FlowLayoutPanel flowLayoutPanel1;

View File

@@ -10,11 +10,30 @@ using System.Windows.Forms;
using System.Security.Policy;
using System.Diagnostics;
using NLog;
using SpineViewer.Utils;
namespace SpineViewer.Controls
{
public partial class SpinePreviewer : UserControl
public partial class SpinePreviewPanel : UserControl
{
public SpinePreviewPanel()
{
InitializeComponent();
renderWindow = new(panel_Render.Handle);
renderWindow.SetActive(false);
// 设置默认参数
Resolution = new(2048, 2048);
Center = new(0, 0);
FlipY = true;
MaxFps = 30;
}
/// <summary>
/// 日志器
/// </summary>
private readonly Logger logger = LogManager.GetCurrentClassLogger();
/// <summary>
/// 要绑定的 Spine 列表控件
/// </summary>
@@ -32,57 +51,12 @@ namespace SpineViewer.Controls
{
propertyGrid = value;
if (propertyGrid is not null)
propertyGrid.SelectedObject = new PreviewerProperty(this);
propertyGrid.SelectedObject = new SpinePreviewPanelProperty(this);
}
}
private PropertyGrid? propertyGrid;
#region
/// <summary>
/// 画面缩放最大值
/// </summary>
public const float ZOOM_MAX = 1000f;
/// <summary>
/// 画面缩放最小值
/// </summary>
public const float ZOOM_MIN = 0.001f;
/// <summary>
/// 包装类, 用于属性面板显示
/// </summary>
private class PreviewerProperty(SpinePreviewer previewer)
{
[TypeConverter(typeof(SizeConverter))]
[Category("[0] "), DisplayName("")]
public Size Resolution { get => previewer.Resolution; set => previewer.Resolution = value; }
[TypeConverter(typeof(PointFConverter))]
[Category("[0] "), DisplayName("")]
public PointF Center { get => previewer.Center; set => previewer.Center = value; }
[Category("[0] "), DisplayName("")]
public float Zoom { get => previewer.Zoom; set => previewer.Zoom = value; }
[Category("[0] "), DisplayName("")]
public float Rotation { get => previewer.Rotation; set => previewer.Rotation = value; }
[Category("[0] "), DisplayName("")]
public bool FlipX { get => previewer.FlipX; set => previewer.FlipX = value; }
[Category("[0] "), DisplayName("")]
public bool FlipY { get => previewer.FlipY; set => previewer.FlipY = value; }
[Category("[0] "), DisplayName("")]
public bool RenderSelectedOnly { get => previewer.RenderSelectedOnly; set => previewer.RenderSelectedOnly = value; }
[Category("[1] "), DisplayName("")]
public bool ShowAxis { get => previewer.ShowAxis; set => previewer.ShowAxis = value; }
[Category("[1] "), DisplayName("")]
public uint MaxFps { get => previewer.MaxFps; set => previewer.MaxFps = value; }
}
#region
/// <summary>
/// 分辨率
@@ -97,8 +71,8 @@ namespace SpineViewer.Controls
if (value.Width <= 0) value.Width = 100;
if (value.Height <= 0) value.Height = 100;
float parentX = Width;
float parentY = Height;
float parentX = panel_Render.Parent.Width;
float parentY = panel_Render.Parent.Height;
float sizeX = value.Width;
float sizeY = value.Height;
@@ -116,15 +90,15 @@ namespace SpineViewer.Controls
}
// 必须通过 SFML 的方法调整窗口
RenderWindow.Position = new((int)(parentX - sizeX) / 2, (int)(parentY - sizeY) / 2);
RenderWindow.Size = new((uint)sizeX, (uint)sizeY);
renderWindow.Position = new((int)(parentX - sizeX) / 2, (int)(parentY - sizeY) / 2);
renderWindow.Size = new((uint)sizeX, (uint)sizeY);
// 将 view 的大小设置成于 resolution 相同的大小, 其余属性都不变
using var view = RenderWindow.GetView();
using var view = renderWindow.GetView();
var signX = Math.Sign(view.Size.X);
var signY = Math.Sign(view.Size.Y);
view.Size = new(value.Width * signX, value.Height * signY);
RenderWindow.SetView(view);
renderWindow.SetView(view);
resolution = value;
}
@@ -140,15 +114,15 @@ namespace SpineViewer.Controls
{
get
{
using var view = RenderWindow.GetView();
using var view = renderWindow.GetView();
var center = view.Center;
return new(center.X, center.Y);
}
set
{
using var view = RenderWindow.GetView();
using var view = renderWindow.GetView();
view.Center = new(value.X, value.Y);
RenderWindow.SetView(view);
renderWindow.SetView(view);
}
}
@@ -161,17 +135,17 @@ namespace SpineViewer.Controls
{
get
{
using var view = RenderWindow.GetView();
using var view = renderWindow.GetView();
return resolution.Width / Math.Abs(view.Size.X);
}
set
{
value = Math.Clamp(value, ZOOM_MIN, ZOOM_MAX);
using var view = RenderWindow.GetView();
value = Math.Clamp(value, 0.001f, 1000f);
using var view = renderWindow.GetView();
var signX = Math.Sign(view.Size.X);
var signY = Math.Sign(view.Size.Y);
view.Size = new(resolution.Width / value * signX, resolution.Height / value * signY);
RenderWindow.SetView(view);
renderWindow.SetView(view);
}
}
@@ -184,14 +158,14 @@ namespace SpineViewer.Controls
{
get
{
using var view = RenderWindow.GetView();
using var view = renderWindow.GetView();
return view.Rotation;
}
set
{
using var view = RenderWindow.GetView();
using var view = renderWindow.GetView();
view.Rotation = value;
RenderWindow.SetView(view);
renderWindow.SetView(view);
}
}
@@ -204,17 +178,17 @@ namespace SpineViewer.Controls
{
get
{
using var view = RenderWindow.GetView();
using var view = renderWindow.GetView();
return view.Size.X < 0;
}
set
{
using var view = RenderWindow.GetView();
using var view = renderWindow.GetView();
var size = view.Size;
if (size.X > 0 && value || size.X < 0 && !value)
size.X *= -1;
view.Size = size;
RenderWindow.SetView(view);
renderWindow.SetView(view);
}
}
@@ -227,17 +201,17 @@ namespace SpineViewer.Controls
{
get
{
using var view = RenderWindow.GetView();
using var view = renderWindow.GetView();
return view.Size.Y < 0;
}
set
{
using var view = RenderWindow.GetView();
using var view = renderWindow.GetView();
var size = view.Size;
if (size.Y > 0 && value || size.Y < 0 && !value)
size.Y *= -1;
view.Size = size;
RenderWindow.SetView(view);
renderWindow.SetView(view);
}
}
@@ -260,40 +234,42 @@ namespace SpineViewer.Controls
/// </summary>
[DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
[Browsable(false)]
public uint MaxFps { get => maxFps; set { RenderWindow.SetFramerateLimit(value); maxFps = value; } }
public uint MaxFps { get => maxFps; set { renderWindow.SetFramerateLimit(value); maxFps = value; } }
private uint maxFps = 60;
/// <summary>
/// 获取 View
/// </summary>
public SFML.Graphics.View GetView() => RenderWindow.GetView();
public SFML.Graphics.View GetView() => renderWindow.GetView();
#endregion
#region
/// <summary>
/// 日志器
/// 预览画面背景色
/// </summary>
private Logger logger = LogManager.GetCurrentClassLogger();
private static readonly SFML.Graphics.Color BackgroundColor = new(105, 105, 105);
public SpinePreviewer()
{
InitializeComponent();
RenderWindow = new(panel.Handle);
RenderWindow.SetActive(false);
/// <summary>
/// 预览画面坐标轴颜色
/// </summary>
private static readonly SFML.Graphics.Color AxisColor = new(220, 220, 220);
// 设置默认参数
Resolution = new(2048, 2048);
Center = new(0, 0);
FlipY = true;
MaxFps = 30;
}
#region 线
/// <summary>
/// 坐标轴顶点缓冲区
/// </summary>
private readonly SFML.Graphics.VertexArray axisVertices = new(SFML.Graphics.PrimitiveType.Lines, 2); // XXX: 暂时未使用 Dispose 释放
/// <summary>
/// 渲染窗口
/// </summary>
private readonly SFML.Graphics.RenderWindow RenderWindow;
private readonly SFML.Graphics.RenderWindow renderWindow;
/// <summary>
/// 帧间隔计时器
/// </summary>
private readonly SFML.System.Clock clock = new();
/// <summary>
/// 渲染任务
@@ -301,6 +277,36 @@ namespace SpineViewer.Controls
private Task? task = null;
private CancellationTokenSource? cancelToken = null;
/// <summary>
/// 是否更新画面
/// </summary>
[DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
[Browsable(false)]
public bool IsUpdating
{
get => isUpdating;
private set
{
if (value == isUpdating) return;
if (value)
{
button_Start.ImageKey = "pause";
}
else
{
button_Start.ImageKey = "start";
}
isUpdating = value;
}
}
private bool isUpdating = true;
/// <summary>
/// 快进时间量
/// </summary>
private float forwardDelta = 0;
private object _forwardDeltaLock = new();
/// <summary>
/// 开始渲染
/// </summary>
@@ -327,58 +333,6 @@ namespace SpineViewer.Controls
task = null;
}
#endregion
#region
/// <summary>
/// 是否更新画面
/// </summary>
public bool IsUpdating
{
get => isUpdating;
private set
{
if (value == isUpdating) return;
if (value)
{
button_Start.ImageKey = "pause";
}
else
{
button_Start.ImageKey = "start";
}
isUpdating = value;
}
}
private bool isUpdating = true;
/// <summary>
/// 快进时间量
/// </summary>
private float forwardDelta = 0;
private object _forwardDeltaLock = new();
/// <summary>
/// 预览画面背景色
/// </summary>
private static readonly SFML.Graphics.Color BackgroundColor = new(105, 105, 105);
/// <summary>
/// 预览画面坐标轴颜色
/// </summary>
private static readonly SFML.Graphics.Color AxisColor = new(220, 220, 220);
/// <summary>
/// 坐标轴顶点缓冲区
/// </summary>
private readonly SFML.Graphics.VertexArray AxisVertex = new(SFML.Graphics.PrimitiveType.Lines, 2);
/// <summary>
/// 帧间隔计时器
/// </summary>
private readonly SFML.System.Clock Clock = new();
/// <summary>
/// 渲染任务
/// </summary>
@@ -386,13 +340,13 @@ namespace SpineViewer.Controls
{
try
{
RenderWindow.SetActive(true);
renderWindow.SetActive(true);
float delta;
while (cancelToken is not null && !cancelToken.IsCancellationRequested)
{
delta = Clock.ElapsedTime.AsSeconds();
Clock.Restart();
delta = clock.ElapsedTime.AsSeconds();
clock.Restart();
// 停止更新的时候只是时间不前进, 但是坐标变换还是要更新, 否则无法移动对象
if (!IsUpdating) delta = 0;
@@ -404,17 +358,17 @@ namespace SpineViewer.Controls
forwardDelta = 0;
}
RenderWindow.Clear(BackgroundColor);
renderWindow.Clear(BackgroundColor);
if (ShowAxis)
{
// 画一个很长的坐标轴, 用 1e9 比较合适
AxisVertex[0] = new(new(-1e9f, 0), AxisColor);
AxisVertex[1] = new(new(1e9f, 0), AxisColor);
RenderWindow.Draw(AxisVertex);
AxisVertex[0] = new(new(0, -1e9f), AxisColor);
AxisVertex[1] = new(new(0, 1e9f), AxisColor);
RenderWindow.Draw(AxisVertex);
axisVertices[0] = new(new(-1e9f, 0), AxisColor);
axisVertices[1] = new(new(1e9f, 0), AxisColor);
renderWindow.Draw(axisVertices);
axisVertices[0] = new(new(0, -1e9f), AxisColor);
axisVertices[1] = new(new(0, 1e9f), AxisColor);
renderWindow.Draw(axisVertices);
}
// 渲染 Spine
@@ -435,25 +389,25 @@ namespace SpineViewer.Controls
if (RenderSelectedOnly && !spine.IsSelected)
continue;
spine.IsDebug = true;
RenderWindow.Draw(spine);
spine.IsDebug = false;
spine.EnableDebug = true;
renderWindow.Draw(spine);
spine.EnableDebug = false;
}
}
}
RenderWindow.Display();
renderWindow.Display();
}
}
catch (Exception ex)
{
logger.Fatal(ex);
logger.Fatal(ex.ToString());
logger.Fatal("Render task stopped");
MessageBox.Error(ex.ToString(), "预览画面已停止渲染");
MessagePopup.Error(ex.ToString(), "预览画面已停止渲染");
}
finally
{
RenderWindow.SetActive(false);
renderWindow.SetActive(false);
}
}
@@ -464,15 +418,15 @@ namespace SpineViewer.Controls
/// </summary>
private SFML.System.Vector2f? draggingSrc = null;
private void SpinePreviewer_SizeChanged(object sender, EventArgs e)
private void SpinePreviewPanel_SizeChanged(object sender, EventArgs e)
{
if (RenderWindow is null)
if (renderWindow is null)
return;
float parentX = panel.Parent.Width;
float parentY = panel.Parent.Height;
float sizeX = panel.Width;
float sizeY = panel.Height;
float parentX = panel_Render.Parent.Width;
float parentY = panel_Render.Parent.Height;
float sizeX = panel_Render.Width;
float sizeY = panel_Render.Height;
if ((sizeY / sizeX) < (parentY / parentX))
{
@@ -488,22 +442,22 @@ namespace SpineViewer.Controls
}
// 必须通过 SFML 的方法调整窗口
RenderWindow.Position = new((int)(parentX - sizeX) / 2, (int)(parentY - sizeY) / 2);
RenderWindow.Size = new((uint)sizeX, (uint)sizeY);
renderWindow.Position = new((int)(parentX - sizeX) / 2, (int)(parentY - sizeY) / 2);
renderWindow.Size = new((uint)sizeX, (uint)sizeY);
}
private void panel_MouseDown(object sender, MouseEventArgs e)
private void panel_Render_MouseDown(object sender, MouseEventArgs e)
{
// 右键优先级高, 进入画面拖动模式, 需要重新记录源点
if ((e.Button & MouseButtons.Right) != 0)
{
draggingSrc = RenderWindow.MapPixelToCoords(new(e.X, e.Y));
draggingSrc = renderWindow.MapPixelToCoords(new(e.X, e.Y));
Cursor = Cursors.Hand;
}
// 按下了左键并且右键是松开的
else if ((e.Button & MouseButtons.Left) != 0 && (MouseButtons & MouseButtons.Right) == 0)
{
draggingSrc = RenderWindow.MapPixelToCoords(new(e.X, e.Y));
draggingSrc = renderWindow.MapPixelToCoords(new(e.X, e.Y));
var src = new PointF(((SFML.System.Vector2f)draggingSrc).X, ((SFML.System.Vector2f)draggingSrc).Y);
if (SpineListView is null)
@@ -571,13 +525,13 @@ namespace SpineViewer.Controls
}
}
private void panel_MouseMove(object sender, MouseEventArgs e)
private void panel_Render_MouseMove(object sender, MouseEventArgs e)
{
if (draggingSrc is null)
return;
var src = (SFML.System.Vector2f)draggingSrc;
var dst = RenderWindow.MapPixelToCoords(new(e.X, e.Y));
var dst = renderWindow.MapPixelToCoords(new(e.X, e.Y));
var _delta = dst - src;
var delta = new SizeF(_delta.X, _delta.Y);
@@ -603,12 +557,12 @@ namespace SpineViewer.Controls
}
}
private void panel_MouseUp(object sender, MouseEventArgs e)
private void panel_Render_MouseUp(object sender, MouseEventArgs e)
{
// 右键高优先级, 结束画面拖动模式
if ((e.Button & MouseButtons.Right) != 0)
{
SpineListView?.PropertyGrid?.Refresh();
SpineListView?.SpinePropertyGrid?.Refresh();
draggingSrc = null;
Cursor = Cursors.Default;
@@ -618,14 +572,34 @@ namespace SpineViewer.Controls
else if ((e.Button & MouseButtons.Left) != 0 && (MouseButtons & MouseButtons.Right) == 0)
{
draggingSrc = null;
SpineListView?.PropertyGrid?.Refresh();
SpineListView?.SpinePropertyGrid?.Refresh();
}
}
private void panel_MouseWheel(object sender, MouseEventArgs e)
private void panel_Render_MouseWheel(object sender, MouseEventArgs e)
{
Zoom *= (e.Delta > 0 ? 1.1f : 0.9f);
PropertyGrid?.Refresh();
var factor = (e.Delta > 0 ? 1.1f : 0.9f);
if ((ModifierKeys & Keys.Control) == 0)
{
Zoom *= factor;
PropertyGrid?.Refresh();
}
else
{
if (SpineListView is not null)
{
lock (SpineListView.Spines)
{
var spines = SpineListView.Spines;
foreach (int i in SpineListView.SelectedIndices)
{
if (spines[i].IsHidden) continue;
spines[i].Scale *= factor;
}
}
SpineListView.SpinePropertyGrid?.Refresh();
}
}
}
private void button_Stop_Click(object sender, EventArgs e)
@@ -663,7 +637,10 @@ namespace SpineViewer.Controls
{
lock (_forwardDeltaLock)
{
forwardDelta += 1f / maxFps;
if (maxFps > 0)
forwardDelta += 1f / maxFps;
else
forwardDelta += 0.001f;
}
}
@@ -671,7 +648,10 @@ namespace SpineViewer.Controls
{
lock (_forwardDeltaLock)
{
forwardDelta += 10f / maxFps;
if (maxFps > 0)
forwardDelta += 10f / maxFps;
else
forwardDelta += 0.01f;
}
}
@@ -681,4 +661,42 @@ namespace SpineViewer.Controls
//public void ClickForwardStepButton() => button_ForwardStep_Click(button_ForwardStep, EventArgs.Empty);
//public void ClickForwardFastButton() => button_ForwardFast_Click(button_ForwardFast, EventArgs.Empty);
}
/// <summary>
/// 用于在 PropertyGrid 上显示 <see cref="SpinePreviewPanel"/> 属性的包装类, 提供用户操作接口
/// </summary>
public class SpinePreviewPanelProperty(SpinePreviewPanel previewPanel)
{
[Browsable(false)]
public SpinePreviewPanel PreviewPanel { get; } = previewPanel;
[TypeConverter(typeof(SizeConverter))]
[Category("[0] "), DisplayName("")]
public Size Resolution { get => PreviewPanel.Resolution; set => PreviewPanel.Resolution = value; }
[TypeConverter(typeof(PointFConverter))]
[Category("[0] "), DisplayName("")]
public PointF Center { get => PreviewPanel.Center; set => PreviewPanel.Center = value; }
[Category("[0] "), DisplayName("")]
public float Zoom { get => PreviewPanel.Zoom; set => PreviewPanel.Zoom = value; }
[Category("[0] "), DisplayName("")]
public float Rotation { get => PreviewPanel.Rotation; set => PreviewPanel.Rotation = value; }
[Category("[0] "), DisplayName("")]
public bool FlipX { get => PreviewPanel.FlipX; set => PreviewPanel.FlipX = value; }
[Category("[0] "), DisplayName("")]
public bool FlipY { get => PreviewPanel.FlipY; set => PreviewPanel.FlipY = value; }
[Category("[0] "), DisplayName("")]
public bool RenderSelectedOnly { get => PreviewPanel.RenderSelectedOnly; set => PreviewPanel.RenderSelectedOnly = value; }
[Category("[1] "), DisplayName("")]
public bool ShowAxis { get => PreviewPanel.ShowAxis; set => PreviewPanel.ShowAxis = value; }
[Category("[1] "), DisplayName("")]
public uint MaxFps { get => PreviewPanel.MaxFps; set => PreviewPanel.MaxFps = value; }
}
}

View File

@@ -0,0 +1,326 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<!--
Microsoft ResX Schema
Version 2.0
The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes
associated with the data types.
Example:
... ado.net/XML headers & schema ...
<resheader name="resmimetype">text/microsoft-resx</resheader>
<resheader name="version">2.0</resheader>
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
<value>[base64 mime encoded serialized .NET Framework object]</value>
</data>
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
<comment>This is a comment</comment>
</data>
There are any number of "resheader" rows that contain simple
name/value pairs.
Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the
mimetype set.
The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not
extensible. For a given mimetype the value must be set accordingly:
Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can
read any of the formats listed below.
mimetype: application/x-microsoft.net.object.binary.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.soap.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.bytearray.base64
value : The object must be serialized into a byte array
: using a System.ComponentModel.TypeConverter
: and then encoded with base64 encoding.
-->
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
<xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType>
<xsd:choice maxOccurs="unbounded">
<xsd:element name="metadata">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" />
</xsd:sequence>
<xsd:attribute name="name" use="required" type="xsd:string" />
<xsd:attribute name="type" type="xsd:string" />
<xsd:attribute name="mimetype" type="xsd:string" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="assembly">
<xsd:complexType>
<xsd:attribute name="alias" type="xsd:string" />
<xsd:attribute name="name" type="xsd:string" />
</xsd:complexType>
</xsd:element>
<xsd:element name="data">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="resheader">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" />
</xsd:complexType>
</xsd:element>
</xsd:choice>
</xsd:complexType>
</xsd:element>
</xsd:schema>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<resheader name="version">
<value>2.0</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<metadata name="imageList.TrayLocation" type="System.Drawing.Point, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a">
<value>17, 17</value>
</metadata>
<data name="imageList.ImageStream" mimetype="application/x-microsoft.net.object.binary.base64">
<value>
AAEAAAD/////AQAAAAAAAAAMAgAAAEZTeXN0ZW0uV2luZG93cy5Gb3JtcywgQ3VsdHVyZT1uZXV0cmFs
LCBQdWJsaWNLZXlUb2tlbj1iNzdhNWM1NjE5MzRlMDg5BQEAAAAmU3lzdGVtLldpbmRvd3MuRm9ybXMu
SW1hZ2VMaXN0U3RyZWFtZXIBAAAABERhdGEHAgIAAAAJAwAAAA8DAAAAHi0AAAJNU0Z0AUkBTAIBAQYB
AAF4AQABeAEAAR8BAAEYAQAE/wEhAQAI/wFCAU0BNgcAATYDAAEoAwABfAMAATADAAEBAQABIAYAAV0q
AAQCAy0BRQNbAc0DXwHoA1kBxgMyAU8DDwEUBAIYAAMOARIDQwF3A10BzwNbAc0DLQFFBAIYAANWAbID
XwHoA1wB1gNDAXcDFgEeBAIYAAMKAQ0DSQGFA18B4wNfAeUDUQGeAyQBNAMJAQsEARgAAwsBDgM7AWQD
XgHSA1YBsv8AEQAEAgMxAUwDYgHhAwAB/wMxAfkDYAHbA0QBewMeASoDBgEIFAADGAEhA1cBwgMhAfsD
YgHhAzEBTAQCGAADWQHDAwAB/wMjAfwDXgHrA0gBhAMWAR4YAAMLAQ4DTQGSAy4B+QMQAf4DUwHyA1gB
ugM3AVoDEQEWAwIBAxQAAxcBHwNJAYYDXgHrA1kBw/8AEQAEAgMxAUwDYgHhAwAB/wMAAf8DIAH9A2AB
4ANQAZoDLgFGAxEBFgMGAQcEAQgAAxoBJANbAc0DAAH/A2IB4QMxAUwEAhgAA1kBwwMAAf8DAAH/AwAB
/wNbAdADPgFrAw8BEwMCAQMQAAMLAQ4DTQGSAy4B+QMAAf8DAAH/AzwB9gNcAcsDRAF5Ax4BKgMGAQcM
AAQBAxgBIQNKAYsDXAHsA1kBw/8AEQAEAgMxAUwDYgHhAwAB/wMxAfkDXAHnA0QB9QNXAe4DWQG7A0MB
dwMoATsDDwEUBAEEAAMaASQDWwHNAwAB/wNiAeEDMQFMBAIYAANZAcMDIgH8A1EB8ANEAfUDMQH5A10B
zgNDAXcDGgEjAwIBAwwAAwsBDgNNAZIDLgH5AwEB/wMlAfoDOwH4AzEB+QNeAeMDTgGYAyQBNQMGAQgE
AQQABAEDGAEhA0oBiwNcAewDWQHD/wARAAQCAzEBTANiAeEDAAH/A1wB2QM7AWMDWQG7Az8B9wM6AfgD
XAHnA1sBxQNBAXMDEwEZAwIBAwMaASQDWwHNAwAB/wNiAeEDMQFMBAIYAANZAcMDXgHrA1ABmgNVAbED
TgHzAyEB+wNbAeQDSwGPAxsBJQMDAQQIAAMLAQ4DTQGSAy4B+QMhAf0DXAHZA1oBxwNEAfUDEAH+A14B
6wNSAaMDJQE3AwMBBAQABAEDGAEhA0oBiwNcAewDWQHD/wARAAQCAzEBTANiAeEDAAH/A1sBzQMaASQD
FgEdA1UBrwMAAf8DAAH/AwAB/wMAAf8DVQGvAxYBHQMaASQDWwHNAwAB/wNiAeEDMQFMBAIYAANZAcMD
WwHkAzsBZQMHAQkDSQGGAyEB+wMAAf8DAAH/A1kBwQMdASkIAAMLAQ4DTQGSAy4B+QMuAfkDTQGSAzkE
XgHiAwAB/wMAAf8DXgHjAzYBWAMFAQYEAAQBAxgBIQNKAYsDXAHsA1kBw/8AEQAEAgMxAUwDYgHhAwAB
/wNbAc0DGgEkAwIBAwMTARoDRgF/A1sB3gMhAfsDEAH+AzoB+ANZAbsDOwFjA1wB2QMAAf8DYgHhAzEB
TAQCGAADWQHDA1sB5AM7AWUDBwQJAQwDOgFhA18B1QNDAfUDMQH5A1sBygMyAU8DDwEUAw0BEQNNAZID
LgH5Ay4B+QNNAZIDEQEWAy0BRANaAb8DVAHvAyEB+wNdAdwDPwFuAxYBHgMEAQUDGAEhA0oBiwNcAewD
WQHD/wARAAQCAzEBTANiAeEDAAH/A1sBzQMaASQEAAQCAxMBGgM9AWcDWQHAA1QB7wMQAf4DPwH3A1wB
5wMxAfkDAAH/A2IB4QMxAUwEAhgAA1kBwwNbAeQDOwFlAwcBCQQAAwsBDgMxAU0DWAG9AzwB9gMxAfkD
YAHbA0QBeAMhAS8DTgGVAy4B+QMuAfkDTQGSAwsBDgMGAQgDIAEuA00BkgNdAdwDRAH1A1oB6QNOAZYD
KAE8AyABLQNLAY0DXAHsA1kBw/8AEQAEAgMxAUwDYgHhAwAB/wNbAc0DGgEkCAAEAgMMARADLwFJA1IB
owNbAeQDPwH3AxAB/gMAAf8DAAH/A2IB4QMxAUwEAhgAA1kBwwNbAeQDOwFlAwcBCQgAAxABFQM/AW0D
XAHWAyUB+gMgAf0DXAHWA0QBeQNUAasDIQH7Ay4B+QNNAZIDCwEOBAADAwEEAxsBJgNBAXMDWgHEA0cB
9ANXAe4DVQG0A0QBegNRAaIDVwHuA1kBw/8AEQAEAgMxAUwDYgHhAwAB/wNbAc0DGgEkEAADCAEKAyMB
MwNEAXkDVwG8A18B5QMlAfoDAAH/A2IB4QMxAUwEAhgAA1kBwwNbAeQDOwFlAwcBCQgAAwMBBAMaASQD
QgF0A14B0gMhAfsDPAH2A10B0QNgAeADIAH9Ay4B+QNNAZIDCwEOCAADAgEDAxIBFwM1AVUDWAG6A1YB
8QM8AfYDWwHeA2AB2wM6AfgDWQHD/wARAAQCAzEBTANiAeEDAAH/A1sBzQMaASQUAAMCAQMDDgESAyMB
MgM9AWgDXgHdAwAB/wNiAeEDMQFMBAIYAANZAcMDWwHkAzsBZQMHAQkMAAMCAQMDDwETAzQBUwNdAdED
OgH4AyEB/AMhAf0DAAH/Ay4B+QNNAZIDCwEODAAEAQMJAQwDIwEzA1UBrgMiAf0DIAH9AyEB/AMQAf4D
WQHD/wARAAQCAzEBTANiAeEDAAH/A1sBzQMaASQgAAMaASQDWwHNAwAB/wNiAeEDMQFMBAIYAANZAcMD
WwHkAzsBZQMHAQkYAAMPARQDVwG5AwAB/wMAAf8DAAH/Ay4B+QNNAZIDCwEOGAADRwGAA14B7QMAAf8D
AAH/AwAB/wNZAcP/ABEABAIDMQFMA2IB4QMAAf8DWwHNAxoBJBQABAIDBwEJAxYBHQM1AVUDXAHZAwAB
/wNiAeEDMQFMBAIYAANZAcMDWwHkAzsBZQMHAQkMAAQBAw0BEQM0AVMDXQHRAzEB+QMRAf4DEAH+AwAB
/wMuAfkDTQGSAwsBDgwABAEDCQELAx4BKwNTAakDIgH9AyAB/QMhAfwDEAH+A1kBw/8AEQAEAgMxAUwD
YgHhAwAB/wNbAc0DGgEkEAADBwEJAxsBJgM0AVMDTQGSA14B3QMxAfkDAAH/A2IB4QMxAUwEAhgAA1kB
wwNbAeQDOwFlAwcBCQgABAIDEAEVAzkBXgNbAc0DIQH7AyEB+wNcAecDVwHuAxAB/gMuAfkDTQGSAwsB
DggAAwIBAwMSARcDNQFVA1gBuANWAfEDPAH2A1sB3gNgAdsDOgH4A1kBw/8AEQAEAgMxAUwDYgHhAwAB
/wNbAc0DGgEkCAAEAgMMARADLgFGA00BkgNcAcgDXgHrAyAB/QMAAf8DAAH/A2IB4QMxAUwEAhgAA1kB
wwNbAeQDOwFlAwcBCQgAAwkBDAMxAU4DWAG3A04B8wMgAf0DXgHdA04BlgNZAb4DIQH8Ay4B+QNNAZID
CwEOBAADAwEEAxsBJgNBAXMDWgHEA0cB9ANXAe4DVQG0A0QBegNRAaIDVwHuA1kBw/8AEQAEAgMxAUwD
YgHhAwAB/wNbAc0DGgEkBAAEAgMTARoDPQFnA1oBvwNeAesDJQH6Az0B9gNcAecDMQH5AwAB/wNiAeED
MQFMBAIYAANZAcMDWwHkAzsBZQMHAQkEAAMLAQ4DLgFHA1UBrQNUAe8DOwH4A2AB2wNEAXoDJAE1A04B
mAMkAfoDLgH5A00BkgMLAQ4DBgEIAyABLgNNAZIDXQHcA0QB9QNaAekDTgGWAygBPAMgAS0DSwGNA1wB
7ANZAcP/ABEABAIDMQFMA2IB4QMAAf8DWwHNAxoBJAMCAQMDEwEaA0YBfwNbAd4DIQH7AxAB/gM6AfgD
WQG7AzsBYwNcAdkDAAH/A2IB4QMxAUwEAhgAA1kBwwNbAeQDOwFlAwcECQEMAzoBYQNdAdQDRwH0AzEB
+QNbAcoDMgFPAw8BFAMNAREDTQGSAy4B+QMuAfkDTQGSAxEBFgMtAUQDWgG/A1QB7wMiAfwDYAHgA0AB
cQMXAR8DBAEFAxgBIQNKAYsDXAHsA1kBw/8AEQAEAgMxAUwDYgHhAwAB/wNbAc0DGgEkAxYBHQNVAa8D
AAH/AwAB/wMAAf8DAAH/A1UBrwMWAR0DGgEkA1sBzQMAAf8DYgHhAzEBTAQCGAADWQHDA1sB5AM7AWUD
BwEJA0kBhgMhAfsDAAH/AwAB/wNZAcEDHQEpCAADCwEOA00BkgMuAfkDLgH5A00BkgM5BF4B4gMAAf8D
AAH/A1oB7QNKAYsDGAEgCAEDGAEhA0oBiwNcAewDWQHD/wARAAQCAzEBTANiAeEDAAH/A1wB2QM7AWMD
WQG7Az8B9wMiAfwDRwH0A1wB2QNGAX4DEwEaAwIBAwMaASQDWwHNAwAB/wNiAeEDMQFMBAIYAANZAcMD
XgHrA1ABmgNVAbEDTgHzAyEB+wNfAeUDSwGPAxsBJQMDAQQIAAMLAQ4DTQGSAy4B+QMhAfsDVgGzA0wB
jgNeAesDEAH+A1EB8ANXAbkDLgFHAwYBCAQABAEDGAEhA0oBiwNcAewDWQHD/wARAAQCAzEBTANiAeED
AAH/AzEB+QNcAecDRAH1A1UB8QNbAdMDUQGkAzgBWwMTARkEAgQAAxoBJANbAc0DAAH/A2IB4QMxAUwE
AhgAA1kBwwMiAfwDUQHwA0QB9QMhAf0DXgHXA0YBfgMaASQDAgEDDAADCwEOA00BkgMuAfkDEAH+A04B
8wNXAe4DPwH3A14B4wNOAZgDJwE5AwgBCgQBBAAEAQMYASEDSgGLA1wB7ANZAcP/ABEABAIDMQFMA2IB
4QMAAf8DAAH/AyAB/QNiAeEDUgGjAzsBYwMhAS8DCgENBAIIAAMaASQDWwHNAwAB/wNiAeEDMQFMBAIY
AANZAcMDAAH/AwAB/wMAAf8DXwHoA0oBiwMWAR0DBAEFEAADCwEOA00BkgMuAfkDAAH/AwAB/wM8AfYD
XAHLA0QBeQMeASoDBgEHDAAEAQMYASEDSgGLA1wB7ANZAcP/ABEABAIDMQFMA2IB4QMAAf8DMQH5A2AB
2wNFAXwDIQEwAwwBEAMDAQQQAAMaASMDXAHIAyEB/QNiAeEDMQFMBAIYAANZAcMDAAH/AyMB/ANeAesD
TgGXAyMBMwQCFAADCwEOA00BkgMuAfkDEAH+A1MB8gNYAboDNwFaAxEBFgMCAQMUAAMXAR8DSQGGA14B
6wNZAcP/ABEABAIDLQFFA14B0gNTAfIDXQHJAzIBTwMQARUDAgEDGAADEwEaA1ABnwNbAeQDWwHQAy0B
RQQCGAADVQG0A1gB7gNfAdoDRAF4AxgBIAMDAQQYAAMKAQ0DSQGGA18B6ANaAekDUAGfAyQBNAMJAQsE
ARgAAwsBDgM7AWUDWwHTA1YBsv8AFQADAwEEAycBOgM/AW0DFAEbKAADDwETAzEBTgMdASkDAgEDHAAD
DgESAzEBTQMjATMDBAEFJAADBgEHAygBPAMoATwDBgEHJAAEAQMHAQkDCwEOAwIBA/8ABQADAgEDAw8B
FANJAYgDXwHlA18B6ANfAegDXwHoA18B6ANfAegDXwHoA18B6ANfAegDXwHoA18B6ANfAegDXwHoA18B
6ANfAegDXwHoA18B6ANfAegDXwHoA1sB5ANGAX4DBQEGBAEoAAMCAQMDDQERAx8BLAMtAUYDVwG5A18B
6ANfAegDXwHoA18B6ANfAegDXwHoA2AB4wNZAb4DMgFPAw8BEwMCAQMwAAMTARkDRgF+A18B6ANfAegD
WQHBAzkBXgMmATgDDwEUBAJcAAM5AV8DXgHiA1wB5wNfAegDXwHoA18B6ANfAegDXQHcAzwBZgMGAQgD
BgEIAzwBZgNdAdwDXwHoA18B6ANfAegDXwHoA10B3ANQAZ0DJQE2IAADDAEQAzwBZgNdAc8DIAH9AyUB
+gNUAe8DWgHtA1oB7QNaAe0DWgHtA1oB7QNaAe0DWgHtA1oB7QNaAe0DWgHtA1oB7QNaAe0DWgHtA1oB
7QNUAe8DJQH6AyIB/ANWAbUDKwFCAwgBCiQABAIDGAEhA0ABcANaAb8DXQHfA1wB7QNUAe8DXAHsA2AB
5gNfAeUDWgHqA1oB7QNRAfADPQH2A10B0QNDAXcDHgErAwYBBywAAz0BaQNbAd4DAQH/AzEB+QNdAewD
WwHkA14B1wNEAXsDHgEqAwYBCFgAAz8BbAMhAf0DOgH4A1QB7wNaAe0DVwHuAyUB+gM8AfYDTwGbAx4B
KwMgAS0DUAGdAzwB9gMlAfoDVwHuA1oB7QNUAe8DPwH3A1MB8gM7AWUgAAMTARkDUAGcAz8B9wM9AfYD
XQHMA0sBjQNGAYADRgGAA0YBgANGAYADRgGAA0YBgANGAYADRgGAA0YBgANGAYADRgGAA0YBgANGAYAD
RgGAA0sBjwNcAdkDIAH9A1UB8QNOAZQDEgEYJAADBQEGAzEBTgNbAdADUwHyA14B4gNVAa4DSgGKA0YB
fwNDAXcDQwF2A0UBfANGAYADTwGbA14B3QNfAegDXwHaA04BlgMoATwDCQEMKAADRwGDAzoB+AMiAfwD
WwHTA1MBpgNcAdkDMQH5A2AB4ANQAZoDLQFEAwsBDlQAAz8BbAMiAfwDWwHTA0wBjgNGAYADSgGKA10B
3AMhAf0DXgHdAzoBYAM6AWIDXQHfAyAB/QNdAdwDSgGKA0YBgANMAY4DWwHTAyIB/AM/AWwgAAMUARsD
UgGlAx8B/QNbAdADPwFsAxgBIQMOARIDDgESAw4BEgMOARIDDgESAw4BEgMOARIDDgESAw4BEgMOARID
DgESAw4BEgMOARIDDgESAyABLgNXAbkDIAH9Ax8B/QNSAaUDFAEbJAADBQEGAzEBTQNaAccDXAHIA0AB
bwMlATcDEwEaAw4BEgMNAREDDAEQAw4BEgMOARIDHQEoAzoBYANLAY8DWwHYA2AB5gNWAbYDMQFNAwYB
CCQAA0kBhgMhAfsDMQH5A1UBrgMqAUADPwFtA10B0QNEAfUDVwHuA1gBtwM2AVcDFgEdAwwBEAQCSAAD
PwFsAzEB+QNVAa4DIAEtAw4BEgMbASUDWQG+AwAB/wNaAe0DPwFtAz8BbQNaAe0DAAH/A1kBvgMbASUD
DgESAyABLQNVAa4DMQH5Az8BbCAAAxQBGwNSAaUDHwH9A1YBtgMoATwDCAEKOAADFgEeA1YBswMgAf0D
HwH9A1IBpQMUARskAAQBAw8BFAMjATMDIQEwAwsBDgMDAQQEARQABAIDBwEJAx4BKgNLAYwDXQHfAz8B
9wNTAakDKAE7JAADSQGGAyEB+wMxAfkDUwGnAxYBHgMMARADKgFAA1gBugNPAfMDSQH0A2AB2wNXAbwD
QgF0AxMBGQMCAQNEAAM/AWwDMQH5A1MBpwMVARwEAAMPARQDVwG5AwAB/wNaAe0DPwFtAz8BbQNaAe0D
AAH/A1cBuQMPARQEAAMVARwDUwGnAzEB+QM/AWwgAAMUARsDUgGlAx8B/QNVAbQDJgE4AwcBCTgAAxYB
HgNWAbMDIAH9Ax8B/QNSAaUDFAEbYAADFQEcA1MBpwMxAfkDIQH7A0kBhiQAA0kBhgMhAfsDMQH5A1MB
pwMVARwIAAMPARMDQwF2A1wB2QMhAf0DAAH/AwAB/wNVAa8DFgEdRAADPwFsAzEB+QNTAacDFQEcBAAD
DwEUA1cBuQMAAf8DWgHtAz8BbQM/AW0DWgHtAwAB/wNXAbkDDwEUBAADFQEcA1MBpwMxAfkDPwFsIAAD
FAEbA1IBpQMfAf0DVQG0AyYBOAMHAQk4AAMWAR4DVgGzAyAB/QMfAf0DUgGlAxQBG2AAAwIBAwMeASoD
VQG0Az8B9wNSAaADGwElAwUBBhwAA0kBhgMhAfsDMQH5A1MBpwMVARwIAAQCAwkBCwMYASADRAF6A2AB
2wM9AfcDPwH3A1kBuwMqAUADDgESAwUBBgQCNAADPwFsAzEB+QNTAacDFQEcBAADDwEUA1cBuQMAAf8D
WgHtAz8BbQM/AW0DWgHtAwAB/wNXAbkDDwEUBAADFQEcA1MBpwMxAfkDPwFsIAADFAEbA1IBpQMfAf0D
VQG0AyYBOAMHAQk4AAMWAR4DVgGzAyAB/QMfAf0DUgGlAxQBG2QAAw8BEwNAAW8DXQHUA1UB7wNMAZED
EgEYHAADSQGGAyEB+wMxAfkDUwGnAxUBHBAABAIDDQERAzYBVwNYAbcDXAHsA1YB8QNdAdEDRAF5AzMB
UAMbASYDBgEHMAADPwFsAzEB+QNTAacDFQEcBAADDwEUA1cBuQMAAf8DWgHtAz8BbQM/AW0DWgHtAwAB
/wNXAbkDDwEUBAADFQEcA1MBpwMxAfkDPwFsIAADFAEbA1IBpQMfAf0DVQG0AyYBOAMHAQk4AAMWAR4D
VgGzAyAB/QMfAf0DUgGlAxQBG2QAAwIBAwMPARMDUQGeAx8B/QNSAaUDFAEbHAADSQGGAyEB+wMxAfkD
UwGnAxUBHBgAAwsBDgMtAUQDSwGPA1sBxQNiAeEDYgHhA1sBzQNMAZADKAE7AwkBDCwAAz8BbAMxAfkD
UwGnAxUBHAQAAw8BFANXAbkDAAH/A1oB7QM/AW0DPwFtA1oB7QMAAf8DVwG5Aw8BFAQAAxUBHANTAacD
MQH5Az8BbCAAAxQBGwNSAaUDHwH9A1UBtAMmATgDBwEJOAADFgEeA1YBswMgAf0DHwH9A1IBpQMUARts
AANNAZMDHwH9A1IBpQMUARscAANJAYYDIQH7AzEB+QNTAacDFQEcHAADBgEIAxkBIgMvAUkDQAFxA10B
zANOAfMDWgHpA1YBtgMtAUQsAAM/AWwDMQH5A1MBpwMVARwEAAMPARQDVwG5AwAB/wNaAe0DPwFtAz8B
bQNaAe0DAAH/A1cBuQMPARQEAAMVARwDUwGnAzEB+QM/AWwgAAMUARsDUgGlAx8B/QNVAbQDJgE4AwcB
CTgAAxYBHgNWAbMDIAH9Ax8B/QNSAaUDFAEbbAADTQGTAx8B/QNSAaUDFAEbHAADSQGGAyEB+wMxAfkD
UwGnAxUBHCAABAIDBAEFAwsBDgMwAUwDWQHBAz0B9gMvAfkDQQFyAwYBCCgAAz8BbAMxAfkDUwGnAxUB
HAQAAw8BFANXAbkDAAH/A1oB7QM/AW0DPwFtA1oB7QMAAf8DVwG5Aw8BFAQAAxUBHANTAacDMQH5Az8B
bCAAAxQBGwNSAaUDHwH9A1UBtAMmATgDBwEJOAADFgEeA1YBswMgAf0DHwH9A1IBpQMUARtsAANNAZMD
HwH9A1IBpQMUARscAANJAYYDIQH7AzEB+QNTAacDFQEcMAADFQEcA1MBpwMxAfkDWwHTAzoBYSgAAz8B
bAMxAfkDUwGnAxUBHAQAAw8BFANXAbkDAAH/A1oB7QM/AW0DPwFtA1oB7QMAAf8DVwG5Aw8BFAQAAxUB
HANTAacDMQH5Az8BbCAAAxQBGwNSAaUDHwH9A1UBtAMmATgDBwEJOAADFgEeA1YBswMgAf0DHwH9A1IB
pQMUARsQAAQBAwQBBQMLAQ4DDwETAw8BEwMPARMDDwETAw8BEwMNAREDCQEMAwMBBAQBLAADTQGTAx8B
/QNSAaUDFAEbHAADSQGGAyEB+wMxAfkDUwGnAxUBHCAABAEDAgEDAwsBDgMwAUwDWQHBAz0B9gMvAfkD
QQFyAwYBCCgAAz8BbAMxAfkDUwGnAxUBHAQAAw8BFANXAbkDAAH/A1oB7QM/AW0DPwFtA1oB7QMAAf8D
VwG5Aw8BFAQAAxUBHANTAacDMQH5Az8BbCAAAxQBGwNSAaUDHwH9A1UBtAMmATgDBwEJOAADFgEeA1YB
swMgAf0DHwH9A1IBpQMUARsQAAMGAQgDJAE1Az4BawNGAX0DRgF+A0YBfgNGAX4DRgF+A0QBewM9AWkD
JAE0AwkBDCwAA00BkwMfAf0DUgGlAxQBGxwAA0kBhgMhAfsDMQH5A1MBpwMVARwcAAMGAQgDEgEXAyMB
MwM/AW4DWwHNAz0B9gNaAeoDVgG2Ay0BRCwAAz8BbAMxAfkDUwGnAxUBHAQAAw8BFANXAbkDAAH/A1oB
7QM/AW0DPwFtA1oB7QMAAf8DVwG5Aw8BFAQAAxUBHANTAacDMQH5Az8BbCAAAxQBGwNSAaUDHwH9A1UB
tAMmATgDBwEJOAADFgEeA1YBswMgAf0DHwH9A1IBpQMUARsQAAMQARUDRgF/A1wB2QNaAeoDYAHmA2IB
4QNgAeADXgHiA1wB5wNiAeEDUAGdAyEBMCQAAwIBAwMPARMDUQGeAx8B/QNSAaUDFAEbHAADSQGGAyEB
+wMxAfkDUwGnAxUBHBgAAwsBDgMtAUQDRgGBA1MBqQNdAd8DXwHoA18B2gNOAZYDKAE8AwkBDCwAAz8B
bAMxAfkDUwGnAxUBHAQAAw8BFANXAbkDAAH/A1oB7QM/AW0DPwFtA1oB7QMAAf8DVwG5Aw8BFAQAAxUB
HANTAacDMQH5Az8BbCAAAxQBGwNSAaUDHwH9A1UBtAMmATgDBwEJOAADFgEeA1YBswMgAf0DHwH9A1IB
pQMUARsQAAMTARoDTgGYA0MB9QMAAf8DYAHgA1YBtgNZAb4DXgHXAz8B9wMiAfwDWgHHAysBQiQAAw8B
EwNAAW8DXQHUA1UB7wNNAZIDEgEYHAADSQGGAyEB+wMxAfkDUwGnAxUBHBAABAIDDQERAzYBVwNYAbcD
WgHqA10B7ANfAdUDSwGPAzoBYAMeASsDBgEHMAADPwFsAzEB+QNTAacDFQEcBAADDwEUA1cBuQMAAf8D
WgHtAz8BbQM/AW0DWgHtAwAB/wNXAbkDDwEUBAADFQEcA1MBpwMxAfkDPwFsIAADFAEbA1IBpQMfAf0D
VQG0AyYBOAMHAQk4AAMWAR4DVgGzAyAB/QMfAf0DUgGlAxQBGxAAAxQBGwNQAZoDPAH2AwAB/wNRAaQD
JAE0A0QBeANeAd0DPwH3A1sB3gM7AWMDDgESIAADAgEDAx4BKgNVAbQDPwH3A1EBoQMbASYDBQEGHAAD
SQGGAyEB+wMxAfkDUwGnAxUBHAwABAEDEwEZA0QBegNgAdsDPQH3Az8B9wNZAbsDKwFCAxMBGQMIAQoD
AgEDNAADPwFsAzEB+QNTAacDFQEcBAADDwEUA1cBuQMAAf8DWgHtAz8BbQM/AW0DWgHtAwAB/wNXAbkD
DwEUBAADFQEcA1MBpwMxAfkDPwFsIAADFAEbA1IBpQMfAf0DVQG0AyYBOAMHAQk4AAMWAR4DVgGzAyAB
/QMfAf0DUgGlAxQBGxAAAxQBGwNQAZoDPAH2AwAB/wNOAZUDMQFMA2IB4QMAAf8DWwHNAxoBJCgAAxUB
HANTAacDMQH5AyEB+wNJAYYkAANJAYYDIQH7AzEB+QNTAacDFQEcDAADCwEOA00BkgMuAfkDAAH/AwAB
/wNVAa8DFgEdRAADPwFsAzEB+QNTAacDFQEcBAADDwEUA1cBuQMAAf8DWgHtAz8BbQM/AW0DWgHtAwAB
/wNXAbkDDwEUBAADFQEcA1MBpwMxAfkDPwFsIAADFAEbA1IBpQMfAf0DVgG2AygBPAMIAQo4AAMWAR4D
VgGzAyAB/QMfAf0DUgGlAxQBGxAAAxQBGwNQAZoDPAH2AwAB/wNEAfUDVgHvAyMB/AMSAf4DXQHMAx0B
KQQCGAAEAgMGAQcDDwETAzIBTwNZAcADQwH1A10B0QM6AWAkAANJAYYDIQH7AzEB+QNTAacDFgEeAwwB
EAMqAUADVwG5A1oB6QNVAe8DXQHoA10BzwNGAX0DEwEaAwIBA0QAAz8BbAMxAfkDUwGnAxUBHAQAAw8B
FANXAbkDAAH/A1oB7QM/AW0DPwFtA1oB7QMAAf8DVwG5Aw8BFAQAAxUBHANTAacDMQH5Az8BbCAAAxQB
GwNSAaUDHwH9A1sB0AM/AWwDGAEhAw4BEgMOARIDDgESAw4BEgMOARIDDgESAw4BEgMOARIDDgESAw4B
EgMOARIDDgESAw4BEgMOARIDIAEuA1cBuQMgAf0DHwH9A1IBpQMUARsQAAMUARsDUAGaAzwB9gMAAf8D
AAH/AyQB+gNDAfUDIAH9A2AB4ANAAXEDHQEoAw4BEgMNAREDCQEMAwgBCgMLBA4BEgMcAScDOAFbA0QB
eQNaAccDYgHhA1YBtgM0AVQDDAEPJAADSQGGAyEB+wMxAfkDVQGuAyoBQAM/AW0DXQHRA0QB9QNXAe4D
WQG7A0QBeQMqAUADEQEWBAJIAAM/AWwDMQH5A1UBrgMgAS0DDgESAxsBJQNZAb4DAAH/A1oB7QM/AW0D
PwFtA1oB7QMAAf8DWQG+AxsBJQMOARIDIAEtA1UBrgMxAfkDPwFsIAADEwEaA1EBpAMhAfwDPAH2A10B
zANLAY0DRgGAA0YBgANGAYADRgGAA0YBgANGAYADRgGAA0YBgANGAYADRgGAA0YBgANGAYADRgGAA0YB
gANLAY8DXAHZAyAB/QNHAfQDTgGYAxMBGRAAAxIBGANLAYwDWgHtAwAB/wMiAf0DXAHZA1UBsQNdAd8D
RAH1A2AB4ANPAZsDRgGAA0QBegM5AV4DMwFQAz0BaANGAX4DUAGaA18B2gNbAeQDXwHaA08BmwMpAT4D
CgENKAADSQGGAyEB+wMiAfwDWwHTA1MBpgNcAdkDMQH5A2AB4ANQAZoDLQFFAxABFQMGAQcEAUwAAz8B
bAMiAfwDWwHTA0wBjgNGAYADSgGKA10B3AMgAf0DXQHfAzoBYgM6AWIDXQHfAyAB/QNdAdwDSgGKA0YB
gANMAY4DWwHTAyIB/AM/AWwgAAMSARcDSgGLA1wB6gMQAf4DJQH6A1QB7wNaAe0DWgHtA1oB7QNaAe0D
WgHtA1oB7QNaAe0DWgHtA1oB7QNaAe0DWgHtA1oB7QNaAe0DWgHtA1QB7wMlAfoDIAH9A1cBwgM0AVQD
CwEOEAADCAEKAy4BSANVAbEDXgHiA2AB2wNPAZsDLQFEA0MBdwNdAcwDYgHhA1oB6gNaAe0DWgHpA1sB
0wNcAcgDXwHaA14B6wNQAfIDMQH5A14B0gNEAXgDIQEvAwcBCSwAA0IBdQNaAeoDAQH/AzEB+QNRAfAD
XQHsA1wB2QNEAXsDHgEqAwYBCFgAAz8BbAMBAf8DMQH5A1QB7wNaAe0DVwHuAyUB+gM8AfYDUAGdAyAB
LQMgAS0DUAGdAzwB9gMlAfoDVwHuA1oB7QNUAe8DMQH5AxAB/gM/AWwgAAMFAQYDGgEjA00BkwNhAeYD
XAHtAz8B9gM/AfcDPwH3Az8B9wM/AfcDPwH3Az8B9wM/AfcDPwH3Az8B9wM/AfcDPwH3Az8B9wM/AfcD
PwH3Az8B9gNcAe0DXwHlA0cBgwMKAQ0EAhAABAEDCAEKAx8BLAMuAUgDLQFEAxsBJQMHAQkDDwETAyMB
MgMuAUcDVwG5A18B6ANaAekDWAHuA1YB8QNdAewDWgHpA18B6ANbAdADNAFTAw8BEwMCAQMwAAMWAR4D
RwGDA1wB7QNWAfEDXwHVA0wBjgMrAUIDDwEUBAJcAAM6AWIDWgHpA1wB7QM/AfYDPwH3Az8B9gNdAewD
XQHcAzwBZgMGAQgDBgEIAzwBZgNdAdwDXAHtAz8B9gM/AfcDPwH2A1wB7QNhAeYDOgFiLAAEAQMjATMD
TgGXA1QBqwNUAasDVAGrA1QBqwNUAasDVAGrA1QBqwNUAasDVAGrA1QBqwNUAasDVAGrA1QBqwNUAasD
TgGXAyMBMwQBTAADCQELAzkBXQNHAYIDJwE6AwQBBUgABAIDIgExAzoBYQMPARRwAAMDAQQDKAE7A04B
mANUAasDUQGeAyEBLxQAAwMBBAMlATcDUAGfA1QBqwNOAZgDKAE7AwMBBBgAAUIBTQE+BwABPgMAASgD
AAF8AwABMAMAAQEBAAEBBgABAxYAA/8BAAH8AQMB8AE/AQMB8AEPAcAIAAH8AQEB8AE/AQMB8AEHAcAI
AAH8AQABMAE/AQAB8AEDAYAIAAH8AQABEAE/AQABcAEAAYAIAAH8AgABPwEAATABAAGACAAB/AIAAT8B
AAEwAQABgAgAAfwCAAE/DAAB/AEIAQABPwEICwAB/AEMAQABPwEMAQABIAkAAfwBDwEAAT8BDAEAATAJ
AAH8AQ8BgAE/AQ4BAAE4CQAB/AEPAfABPwEPAcABPwkAAfwBDwGAAT8BDgEAATgJAAH8AQ8BAAE/AQwB
AAEwCQAB/AEMAQABPwEMAQABIAkAAfwBCAEAAT8BCAsAAfwCAAE/DAAB/AIAAT8BAAEwCgAB/AIAAT8B
AAEwAQABgAgAAfwBAAEQAT8BAAFwAQABgAgAAfwBAAEwAT8BAAHwAQMBgAgAAfwBAAHwAT8BAQHwAQcB
wAgAAfwBAwHwAT8BAwHwAQ8BwAgAAf4BHwH4AX8BDwH4AX8BwAgAAeACAAEHAf4BAAEBAf8B4AEPAv8B
4AEAAQEB8AHgAgABBwH8AgAB/wHgAQcC/wHgAQABAQHwAeACAAEHAfwCAAF/AeABAwL/AeABAAEBAfAB
4AIAAQcB/AIAAT8B4AEAAX8B/wHgAQABAQHwAeABfwH+AQcB/AEHAcABPwHgAQABPwH/AeEBAAEhAfAB
4AF/Af4BBwL/AfgBPwHgAcABPwH/AeEBAAEhAfAB4AF/Af4BBwL/AfgBDwHgAcABAwH/AeEBAAEhAfAB
4AF/Af4BBwL/AfwBDwHgAfABAQH/AeEBAAEhAfAB4AF/Af4BBwL/AfwBDwHgAfwBAAH/AeEBAAEhAfAB
4AF/Af4BBwP/AQ8B4AH+AQAB/wHhAQABIQHwAeABfwH+AQcD/wEPAeAB/wEAAX8B4QEAASEB8AHgAX8B
/gEHA/8BDwHgAf8B8AF/AeEBAAEhAfAB4AF/Af4BBwGAAQcB/wEPAeAB/wEAAX8B4QEAASEB8AHgAX8B
/gEHAYABBwH/AQ8B4AH+AQAB/wHhAQABIQHwAeABfwH+AQcBgAEHAfwBDwHgAfwBAAH/AeEBAAEhAfAB
4AF/Af4BBwGAAQcB/AEPAeAB8AEBAf8B4QEAASEB8AHgAX8B/gEHAYABBwH4AQ8C4AEDAf8B4QEAASEB
8AHgAX8B/gEHAYABHwH4AT8C4AE/Af8B4QEAASEB8AHgAX8B/gEHAYABDwHAAT8B4AEAAT8B/wHhAQAB
IQHwAeACAAEHAYACAAE/AeABAAF/Af8B4AEAAQEB8AHgAgABBwGAAgABfwHgAQAC/wHgAQABAQHwAeAC
AAEHAYACAAH/AeABBwL/AeABAAEBAfAB4AIAAQcBgAEAAQEB/wHgAQ8C/wHgAQABAQHwAfwCAAE/Af8B
+AE/Af8B8AP/AfABPgEDAfAL
</value>
</data>
<metadata name="toolTip.TrayLocation" type="System.Drawing.Point, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a">
<value>165, 17</value>
</metadata>
</root>

View File

@@ -1,326 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<!--
Microsoft ResX Schema
Version 2.0
The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes
associated with the data types.
Example:
... ado.net/XML headers & schema ...
<resheader name="resmimetype">text/microsoft-resx</resheader>
<resheader name="version">2.0</resheader>
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
<value>[base64 mime encoded serialized .NET Framework object]</value>
</data>
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
<comment>This is a comment</comment>
</data>
There are any number of "resheader" rows that contain simple
name/value pairs.
Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the
mimetype set.
The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not
extensible. For a given mimetype the value must be set accordingly:
Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can
read any of the formats listed below.
mimetype: application/x-microsoft.net.object.binary.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.soap.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.bytearray.base64
value : The object must be serialized into a byte array
: using a System.ComponentModel.TypeConverter
: and then encoded with base64 encoding.
-->
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
<xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType>
<xsd:choice maxOccurs="unbounded">
<xsd:element name="metadata">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" />
</xsd:sequence>
<xsd:attribute name="name" use="required" type="xsd:string" />
<xsd:attribute name="type" type="xsd:string" />
<xsd:attribute name="mimetype" type="xsd:string" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="assembly">
<xsd:complexType>
<xsd:attribute name="alias" type="xsd:string" />
<xsd:attribute name="name" type="xsd:string" />
</xsd:complexType>
</xsd:element>
<xsd:element name="data">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="resheader">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" />
</xsd:complexType>
</xsd:element>
</xsd:choice>
</xsd:complexType>
</xsd:element>
</xsd:schema>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<resheader name="version">
<value>2.0</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<metadata name="imageList.TrayLocation" type="System.Drawing.Point, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a">
<value>17, 17</value>
</metadata>
<data name="imageList.ImageStream" mimetype="application/x-microsoft.net.object.binary.base64">
<value>
AAEAAAD/////AQAAAAAAAAAMAgAAAEZTeXN0ZW0uV2luZG93cy5Gb3JtcywgQ3VsdHVyZT1uZXV0cmFs
LCBQdWJsaWNLZXlUb2tlbj1iNzdhNWM1NjE5MzRlMDg5BQEAAAAmU3lzdGVtLldpbmRvd3MuRm9ybXMu
SW1hZ2VMaXN0U3RyZWFtZXIBAAAABERhdGEHAgIAAAAJAwAAAA8DAAAAHi0AAAJNU0Z0AUkBTAIBAQYB
AAFwAQABcAEAAR8BAAEYAQAE/wEhAQAI/wFCAU0BNgcAATYDAAEoAwABfAMAATADAAEBAQABIAYAAV0q
AAQCAy0BRQNbAc0DXQHoA1kBxgMyAU8DDwEUBAIYAAMOARIDQwF3A10BzwNbAc0DLQFFBAIYAANWAbID
XQHoA1wB1gNDAXcDFgEeBAIYAAMKAQ0DSQGFA14B4wNfAeUDUQGeAyQBNAMJAQsEARgAAwsBDgM7AWQD
XgHSA1YBsv8AEQAEAgMxAUwDYQHhAwAB/wMuAfkDYAHbA0QBewMeASoDBgEIFAADGAEhA1cBwgMhAfsD
YQHhAzEBTAQCGAADWQHDAwAB/wMhAfwDXAHrA0gBhAMWAR4YAAMLAQ4DTQGSAysB+QMPAf4DUAHyA1gB
ugM3AVoDEQEWAwIBAxQAAxcBHwNJAYYDXAHrA1kBw/8AEQAEAgMxAUwDYQHhAwAB/wMAAf8DHgH9A2AB
4ANQAZoDLgFGAxEBFgMGAQcEAQgAAxoBJANbAc0DAAH/A2EB4QMxAUwEAhgAA1kBwwMAAf8DAAH/AwAB
/wNbAdADPgFrAw8BEwMCAQMQAAMLAQ4DTQGSAysB+QMAAf8DAAH/AzkB9gNcAcsDRAF5Ax4BKgMGAQcM
AAQBAxgBIQNKAYsDWwHsA1kBw/8AEQAEAgMxAUwDYQHhAwAB/wMuAfkDXAHnA0MB9QNWAe4DWQG7A0MB
dwMoATsDDwEUBAEEAAMaASQDWwHNAwAB/wNhAeEDMQFMBAIYAANZAcMDIAH8A1AB8ANDAfUDLgH5A10B
zgNDAXcDGgEjAwIBAwwAAwsBDgNNAZIDKwH5AwEB/wMkAfoDOgH4Ay4B+QNdAeMDTgGYAyQBNQMGAQgE
AQQABAEDGAEhA0oBiwNbAewDWQHD/wARAAQCAzEBTANhAeEDAAH/A1wB2QM7AWMDWQG7Az0B9wM5AfgD
XAHnA1sBxQNBAXMDEwEZAwIBAwMaASQDWwHNAwAB/wNhAeEDMQFMBAIYAANZAcMDXAHrA1ABmgNVAbED
TQHzAyEB+wNbAeQDSwGPAxsBJQMDAQQIAAMLAQ4DTQGSAysB+QMfAf0DXAHZA1oBxwNDAfUDDwH+A1wB
6wNSAaMDJQE3AwMBBAQABAEDGAEhA0oBiwNbAewDWQHD/wARAAQCAzEBTANhAeEDAAH/A1sBzQMaASQD
FgEdA1UBrwMAAf8DAAH/AwAB/wMAAf8DVQGvAxYBHQMaASQDWwHNAwAB/wNhAeEDMQFMBAIYAANZAcMD
WwHkAzsBZQMHAQkDSQGGAyEB+wMAAf8DAAH/A1kBwQMdASkIAAMLAQ4DTQGSAysB+QMrAfkDTQGSAzkE
XgHiAwAB/wMAAf8DXQHjAzYBWAMFAQYEAAQBAxgBIQNKAYsDWwHsA1kBw/8AEQAEAgMxAUwDYQHhAwAB
/wNbAc0DGgEkAwIBAwMTARoDRgF/A1sB3gMhAfsDDwH+AzkB+ANZAbsDOwFjA1wB2QMAAf8DYQHhAzEB
TAQCGAADWQHDA1sB5AM7AWUDBwQJAQwDOgFhA18B1QNCAfUDLgH5A1sBygMyAU8DDwEUAw0BEQNNAZID
KwH5AysB+QNNAZIDEQEWAy0BRANaAb8DUgHvAyEB+wNdAdwDPwFuAxYBHgMEAQUDGAEhA0oBiwNbAewD
WQHD/wARAAQCAzEBTANhAeEDAAH/A1sBzQMaASQEAAQCAxMBGgM9AWcDWQHAA1IB7wMPAf4DPQH3A1wB
5wMuAfkDAAH/A2EB4QMxAUwEAhgAA1kBwwNbAeQDOwFlAwcBCQQAAwsBDgMxAU0DWAG9AzkB9gMuAfkD
YAHbA0QBeAMhAS8DTgGVAysB+QMrAfkDTQGSAwsBDgMGAQgDIAEuA00BkgNdAdwDQwH1A1oB6QNOAZYD
KAE8AyABLQNLAY0DWwHsA1kBw/8AEQAEAgMxAUwDYQHhAwAB/wNbAc0DGgEkCAAEAgMMARADLwFJA1IB
owNbAeQDPQH3Aw8B/gMAAf8DAAH/A2EB4QMxAUwEAhgAA1kBwwNbAeQDOwFlAwcBCQgAAxABFQM/AW0D
XAHWAyQB+gMeAf0DXAHWA0QBeQNUAasDIQH7AysB+QNNAZIDCwEOBAADAwEEAxsBJgNBAXMDWgHEA0UB
9ANWAe4DVQG0A0QBegNRAaIDVgHuA1kBw/8AEQAEAgMxAUwDYQHhAwAB/wNbAc0DGgEkEAADCAEKAyMB
MwNEAXkDVwG8A18B5QMkAfoDAAH/A2EB4QMxAUwEAhgAA1kBwwNbAeQDOwFlAwcBCQgAAwMBBAMaASQD
QgF0A14B0gMhAfsDOQH2A10B0QNgAeADHgH9AysB+QNNAZIDCwEOCAADAgEDAxIBFwM1AVUDWAG6A1UB
8QM5AfYDWwHeA2AB2wM5AfgDWQHD/wARAAQCAzEBTANhAeEDAAH/A1sBzQMaASQUAAMCAQMDDgESAyMB
MgM9AWgDXgHdAwAB/wNhAeEDMQFMBAIYAANZAcMDWwHkAzsBZQMHAQkMAAMCAQMDDwETAzQBUwNdAdED
OQH4Ax8B/AMfAf0DAAH/AysB+QNNAZIDCwEODAAEAQMJAQwDIwEzA1UBrgMgAf0DHgH9Ax8B/AMPAf4D
WQHD/wARAAQCAzEBTANhAeEDAAH/A1sBzQMaASQgAAMaASQDWwHNAwAB/wNhAeEDMQFMBAIYAANZAcMD
WwHkAzsBZQMHAQkYAAMPARQDVwG5AwAB/wMAAf8DAAH/AysB+QNNAZIDCwEOGAADRwGAA1wB7QMAAf8D
AAH/AwAB/wNZAcP/ABEABAIDMQFMA2EB4QMAAf8DWwHNAxoBJBQABAIDBwEJAxYBHQM1AVUDXAHZAwAB
/wNhAeEDMQFMBAIYAANZAcMDWwHkAzsBZQMHAQkMAAQBAw0BEQM0AVMDXQHRAy4B+QMQAf4DDwH+AwAB
/wMrAfkDTQGSAwsBDgwABAEDCQELAx4BKwNTAakDIAH9Ax4B/QMfAfwDDwH+A1kBw/8AEQAEAgMxAUwD
YQHhAwAB/wNbAc0DGgEkEAADBwEJAxsBJgM0AVMDTQGSA14B3QMuAfkDAAH/A2EB4QMxAUwEAhgAA1kB
wwNbAeQDOwFlAwcBCQgABAIDEAEVAzkBXgNbAc0DIQH7AyEB+wNcAecDVgHuAw8B/gMrAfkDTQGSAwsB
DggAAwIBAwMSARcDNQFVA1gBuANVAfEDOQH2A1sB3gNgAdsDOQH4A1kBw/8AEQAEAgMxAUwDYQHhAwAB
/wNbAc0DGgEkCAAEAgMMARADLgFGA00BkgNcAcgDXAHrAx4B/QMAAf8DAAH/A2EB4QMxAUwEAhgAA1kB
wwNbAeQDOwFlAwcBCQgAAwkBDAMxAU4DWAG3A00B8wMeAf0DXgHdA04BlgNZAb4DHwH8AysB+QNNAZID
CwEOBAADAwEEAxsBJgNBAXMDWgHEA0UB9ANWAe4DVQG0A0QBegNRAaIDVgHuA1kBw/8AEQAEAgMxAUwD
YQHhAwAB/wNbAc0DGgEkBAAEAgMTARoDPQFnA1oBvwNcAesDJAH6AzoB9gNcAecDLgH5AwAB/wNhAeED
MQFMBAIYAANZAcMDWwHkAzsBZQMHAQkEAAMLAQ4DLgFHA1UBrQNSAe8DOgH4A2AB2wNEAXoDJAE1A04B
mAMjAfoDKwH5A00BkgMLAQ4DBgEIAyABLgNNAZIDXQHcA0MB9QNaAekDTgGWAygBPAMgAS0DSwGNA1sB
7ANZAcP/ABEABAIDMQFMA2EB4QMAAf8DWwHNAxoBJAMCAQMDEwEaA0YBfwNbAd4DIQH7Aw8B/gM5AfgD
WQG7AzsBYwNcAdkDAAH/A2EB4QMxAUwEAhgAA1kBwwNbAeQDOwFlAwcECQEMAzoBYQNdAdQDRQH0Ay4B
+QNbAcoDMgFPAw8BFAMNAREDTQGSAysB+QMrAfkDTQGSAxEBFgMtAUQDWgG/A1IB7wMgAfwDYAHgA0AB
cQMXAR8DBAEFAxgBIQNKAYsDWwHsA1kBw/8AEQAEAgMxAUwDYQHhAwAB/wNbAc0DGgEkAxYBHQNVAa8D
AAH/AwAB/wMAAf8DAAH/A1UBrwMWAR0DGgEkA1sBzQMAAf8DYQHhAzEBTAQCGAADWQHDA1sB5AM7AWUD
BwEJA0kBhgMhAfsDAAH/AwAB/wNZAcEDHQEpCAADCwEOA00BkgMrAfkDKwH5A00BkgM5BF4B4gMAAf8D
AAH/A1gB7QNKAYsDGAEgCAEDGAEhA0oBiwNbAewDWQHD/wARAAQCAzEBTANhAeEDAAH/A1wB2QM7AWMD
WQG7Az0B9wMgAfwDRQH0A1wB2QNGAX4DEwEaAwIBAwMaASQDWwHNAwAB/wNhAeEDMQFMBAIYAANZAcMD
XAHrA1ABmgNVAbEDTQHzAyEB+wNfAeUDSwGPAxsBJQMDAQQIAAMLAQ4DTQGSAysB+QMhAfsDVgGzA0wB
jgNcAesDDwH+A1AB8ANXAbkDLgFHAwYBCAQABAEDGAEhA0oBiwNbAewDWQHD/wARAAQCAzEBTANhAeED
AAH/Ay4B+QNcAecDQwH1A1QB8QNbAdMDUQGkAzgBWwMTARkEAgQAAxoBJANbAc0DAAH/A2EB4QMxAUwE
AhgAA1kBwwMgAfwDUAHwA0MB9QMfAf0DXgHXA0YBfgMaASQDAgEDDAADCwEOA00BkgMrAfkDDwH+A00B
8wNWAe4DPQH3A10B4wNOAZgDJwE5AwgBCgQBBAAEAQMYASEDSgGLA1sB7ANZAcP/ABEABAIDMQFMA2EB
4QMAAf8DAAH/Ax4B/QNhAeEDUgGjAzsBYwMhAS8DCgENBAIIAAMaASQDWwHNAwAB/wNhAeEDMQFMBAIY
AANZAcMDAAH/AwAB/wMAAf8DXQHoA0oBiwMWAR0DBAEFEAADCwEOA00BkgMrAfkDAAH/AwAB/wM5AfYD
XAHLA0QBeQMeASoDBgEHDAAEAQMYASEDSgGLA1sB7ANZAcP/ABEABAIDMQFMA2EB4QMAAf8DLgH5A2AB
2wNFAXwDIQEwAwwBEAMDAQQQAAMaASMDXAHIAx8B/QNhAeEDMQFMBAIYAANZAcMDAAH/AyEB/ANcAesD
TgGXAyMBMwQCFAADCwEOA00BkgMrAfkDDwH+A1AB8gNYAboDNwFaAxEBFgMCAQMUAAMXAR8DSQGGA1wB
6wNZAcP/ABEABAIDLQFFA14B0gNQAfIDXQHJAzIBTwMQARUDAgEDGAADEwEaA1ABnwNbAeQDWwHQAy0B
RQQCGAADVQG0A1cB7gNfAdoDRAF4AxgBIAMDAQQYAAMKAQ0DSQGGA10B6ANaAekDUAGfAyQBNAMJAQsE
ARgAAwsBDgM7AWUDWwHTA1YBsv8AFQADAwEEAycBOgM/AW0DFAEbKAADDwETAzEBTgMdASkDAgEDHAAD
DgESAzEBTQMjATMDBAEFJAADBgEHAygBPAMoATwDBgEHJAAEAQMHAQkDCwEOAwIBA/8ABQADAgEDAw8B
FANJAYgDXwHlA10B6ANdAegDXQHoA10B6ANdAegDXQHoA10B6ANdAegDXQHoA10B6ANdAegDXQHoA10B
6ANdAegDXQHoA10B6ANdAegDXQHoA1sB5ANGAX4DBQEGBAEoAAMCAQMDDQERAx8BLAMtAUYDVwG5A10B
6ANdAegDXQHoA10B6ANdAegDXQHoA18B4wNZAb4DMgFPAw8BEwMCAQMwAAMTARkDRgF+A10B6ANdAegD
WQHBAzkBXgMmATgDDwEUBAJcAAM5AV8DXgHiA1wB5wNdAegDXQHoA10B6ANdAegDXQHcAzwBZgMGAQgD
BgEIAzwBZgNdAdwDXQHoA10B6ANdAegDXQHoA10B3ANQAZ0DJQE2IAADDAEQAzwBZgNdAc8DHgH9AyQB
+gNSAe8DWAHtA1gB7QNYAe0DWAHtA1gB7QNYAe0DWAHtA1gB7QNYAe0DWAHtA1gB7QNYAe0DWAHtA1gB
7QNSAe8DJAH6AyAB/ANWAbUDKwFCAwgBCiQABAIDGAEhA0ABcANaAb8DXQHfA1oB7QNSAe8DWwHsA14B
5gNfAeUDWAHqA1gB7QNQAfADOgH2A10B0QNDAXcDHgErAwYBBywAAz0BaQNbAd4DAQH/Ay4B+QNcAewD
WwHkA14B1wNEAXsDHgEqAwYBCFgAAz8BbAMfAf0DOQH4A1IB7wNYAe0DVgHuAyQB+gM5AfYDTwGbAx4B
KwMgAS0DUAGdAzkB9gMkAfoDVgHuA1gB7QNSAe8DPQH3A1AB8gM7AWUgAAMTARkDUAGcAz0B9wM6AfYD
XQHMA0sBjQNGAYADRgGAA0YBgANGAYADRgGAA0YBgANGAYADRgGAA0YBgANGAYADRgGAA0YBgANGAYAD
RgGAA0sBjwNcAdkDHgH9A1QB8QNOAZQDEgEYJAADBQEGAzEBTgNbAdADUAHyA14B4gNVAa4DSgGKA0YB
fwNDAXcDQwF2A0UBfANGAYADTwGbA14B3QNdAegDXwHaA04BlgMoATwDCQEMKAADRwGDAzkB+AMgAfwD
WwHTA1MBpgNcAdkDLgH5A2AB4ANQAZoDLQFEAwsBDlQAAz8BbAMgAfwDWwHTA0wBjgNGAYADSgGKA10B
3AMfAf0DXgHdAzoBYAM6AWIDXQHfAx4B/QNdAdwDSgGKA0YBgANMAY4DWwHTAyAB/AM/AWwgAAMUARsD
UgGlAx0B/QNbAdADPwFsAxgBIQMOARIDDgESAw4BEgMOARIDDgESAw4BEgMOARIDDgESAw4BEgMOARID
DgESAw4BEgMOARIDDgESAyABLgNXAbkDHgH9Ax0B/QNSAaUDFAEbJAADBQEGAzEBTQNaAccDXAHIA0AB
bwMlATcDEwEaAw4BEgMNAREDDAEQAw4BEgMOARIDHQEoAzoBYANLAY8DWwHYA14B5gNWAbYDMQFNAwYB
CCQAA0kBhgMhAfsDLgH5A1UBrgMqAUADPwFtA10B0QNDAfUDVgHuA1gBtwM2AVcDFgEdAwwBEAQCSAAD
PwFsAy4B+QNVAa4DIAEtAw4BEgMbASUDWQG+AwAB/wNYAe0DPwFtAz8BbQNYAe0DAAH/A1kBvgMbASUD
DgESAyABLQNVAa4DLgH5Az8BbCAAAxQBGwNSAaUDHQH9A1YBtgMoATwDCAEKOAADFgEeA1YBswMeAf0D
HQH9A1IBpQMUARskAAQBAw8BFAMjATMDIQEwAwsBDgMDAQQEARQABAIDBwEJAx4BKgNLAYwDXQHfAz0B
9wNTAakDKAE7JAADSQGGAyEB+wMuAfkDUwGnAxYBHgMMARADKgFAA1gBugNOAfMDRgH0A2AB2wNXAbwD
QgF0AxMBGQMCAQNEAAM/AWwDLgH5A1MBpwMVARwEAAMPARQDVwG5AwAB/wNYAe0DPwFtAz8BbQNYAe0D
AAH/A1cBuQMPARQEAAMVARwDUwGnAy4B+QM/AWwgAAMUARsDUgGlAx0B/QNVAbQDJgE4AwcBCTgAAxYB
HgNWAbMDHgH9Ax0B/QNSAaUDFAEbYAADFQEcA1MBpwMuAfkDIQH7A0kBhiQAA0kBhgMhAfsDLgH5A1MB
pwMVARwIAAMPARMDQwF2A1wB2QMfAf0DAAH/AwAB/wNVAa8DFgEdRAADPwFsAy4B+QNTAacDFQEcBAAD
DwEUA1cBuQMAAf8DWAHtAz8BbQM/AW0DWAHtAwAB/wNXAbkDDwEUBAADFQEcA1MBpwMuAfkDPwFsIAAD
FAEbA1IBpQMdAf0DVQG0AyYBOAMHAQk4AAMWAR4DVgGzAx4B/QMdAf0DUgGlAxQBG2AAAwIBAwMeASoD
VQG0Az0B9wNSAaADGwElAwUBBhwAA0kBhgMhAfsDLgH5A1MBpwMVARwIAAQCAwkBCwMYASADRAF6A2AB
2wM7AfcDPQH3A1kBuwMqAUADDgESAwUBBgQCNAADPwFsAy4B+QNTAacDFQEcBAADDwEUA1cBuQMAAf8D
WAHtAz8BbQM/AW0DWAHtAwAB/wNXAbkDDwEUBAADFQEcA1MBpwMuAfkDPwFsIAADFAEbA1IBpQMdAf0D
VQG0AyYBOAMHAQk4AAMWAR4DVgGzAx4B/QMdAf0DUgGlAxQBG2QAAw8BEwNAAW8DXQHUA1MB7wNMAZED
EgEYHAADSQGGAyEB+wMuAfkDUwGnAxUBHBAABAIDDQERAzYBVwNYAbcDWwHsA1UB8QNdAdEDRAF5AzMB
UAMbASYDBgEHMAADPwFsAy4B+QNTAacDFQEcBAADDwEUA1cBuQMAAf8DWAHtAz8BbQM/AW0DWAHtAwAB
/wNXAbkDDwEUBAADFQEcA1MBpwMuAfkDPwFsIAADFAEbA1IBpQMdAf0DVQG0AyYBOAMHAQk4AAMWAR4D
VgGzAx4B/QMdAf0DUgGlAxQBG2QAAwIBAwMPARMDUQGeAx0B/QNSAaUDFAEbHAADSQGGAyEB+wMuAfkD
UwGnAxUBHBgAAwsBDgMtAUQDSwGPA1sBxQNhAeEDYQHhA1sBzQNMAZADKAE7AwkBDCwAAz8BbAMuAfkD
UwGnAxUBHAQAAw8BFANXAbkDAAH/A1gB7QM/AW0DPwFtA1gB7QMAAf8DVwG5Aw8BFAQAAxUBHANTAacD
LgH5Az8BbCAAAxQBGwNSAaUDHQH9A1UBtAMmATgDBwEJOAADFgEeA1YBswMeAf0DHQH9A1IBpQMUARts
AANNAZMDHQH9A1IBpQMUARscAANJAYYDIQH7Ay4B+QNTAacDFQEcHAADBgEIAxkBIgMvAUkDQAFxA10B
zANNAfMDWgHpA1YBtgMtAUQsAAM/AWwDLgH5A1MBpwMVARwEAAMPARQDVwG5AwAB/wNYAe0DPwFtAz8B
bQNYAe0DAAH/A1cBuQMPARQEAAMVARwDUwGnAy4B+QM/AWwgAAMUARsDUgGlAx0B/QNVAbQDJgE4AwcB
CTgAAxYBHgNWAbMDHgH9Ax0B/QNSAaUDFAEbbAADTQGTAx0B/QNSAaUDFAEbHAADSQGGAyEB+wMuAfkD
UwGnAxUBHCAABAIDBAEFAwsBDgMwAUwDWQHBAzoB9gMsAfkDQQFyAwYBCCgAAz8BbAMuAfkDUwGnAxUB
HAQAAw8BFANXAbkDAAH/A1gB7QM/AW0DPwFtA1gB7QMAAf8DVwG5Aw8BFAQAAxUBHANTAacDLgH5Az8B
bCAAAxQBGwNSAaUDHQH9A1UBtAMmATgDBwEJOAADFgEeA1YBswMeAf0DHQH9A1IBpQMUARtsAANNAZMD
HQH9A1IBpQMUARscAANJAYYDIQH7Ay4B+QNTAacDFQEcMAADFQEcA1MBpwMuAfkDWwHTAzoBYSgAAz8B
bAMuAfkDUwGnAxUBHAQAAw8BFANXAbkDAAH/A1gB7QM/AW0DPwFtA1gB7QMAAf8DVwG5Aw8BFAQAAxUB
HANTAacDLgH5Az8BbCAAAxQBGwNSAaUDHQH9A1UBtAMmATgDBwEJOAADFgEeA1YBswMeAf0DHQH9A1IB
pQMUARsQAAQBAwQBBQMLAQ4DDwETAw8BEwMPARMDDwETAw8BEwMNAREDCQEMAwMBBAQBLAADTQGTAx0B
/QNSAaUDFAEbHAADSQGGAyEB+wMuAfkDUwGnAxUBHCAABAEDAgEDAwsBDgMwAUwDWQHBAzoB9gMsAfkD
QQFyAwYBCCgAAz8BbAMuAfkDUwGnAxUBHAQAAw8BFANXAbkDAAH/A1gB7QM/AW0DPwFtA1gB7QMAAf8D
VwG5Aw8BFAQAAxUBHANTAacDLgH5Az8BbCAAAxQBGwNSAaUDHQH9A1UBtAMmATgDBwEJOAADFgEeA1YB
swMeAf0DHQH9A1IBpQMUARsQAAMGAQgDJAE1Az4BawNGAX0DRgF+A0YBfgNGAX4DRgF+A0QBewM9AWkD
JAE0AwkBDCwAA00BkwMdAf0DUgGlAxQBGxwAA0kBhgMhAfsDLgH5A1MBpwMVARwcAAMGAQgDEgEXAyMB
MwM/AW4DWwHNAzoB9gNYAeoDVgG2Ay0BRCwAAz8BbAMuAfkDUwGnAxUBHAQAAw8BFANXAbkDAAH/A1gB
7QM/AW0DPwFtA1gB7QMAAf8DVwG5Aw8BFAQAAxUBHANTAacDLgH5Az8BbCAAAxQBGwNSAaUDHQH9A1UB
tAMmATgDBwEJOAADFgEeA1YBswMeAf0DHQH9A1IBpQMUARsQAAMQARUDRgF/A1wB2QNYAeoDXgHmA2EB
4QNgAeADXgHiA1wB5wNhAeEDUAGdAyEBMCQAAwIBAwMPARMDUQGeAx0B/QNSAaUDFAEbHAADSQGGAyEB
+wMuAfkDUwGnAxUBHBgAAwsBDgMtAUQDRgGBA1MBqQNdAd8DXQHoA18B2gNOAZYDKAE8AwkBDCwAAz8B
bAMuAfkDUwGnAxUBHAQAAw8BFANXAbkDAAH/A1gB7QM/AW0DPwFtA1gB7QMAAf8DVwG5Aw8BFAQAAxUB
HANTAacDLgH5Az8BbCAAAxQBGwNSAaUDHQH9A1UBtAMmATgDBwEJOAADFgEeA1YBswMeAf0DHQH9A1IB
pQMUARsQAAMTARoDTgGYA0IB9QMAAf8DYAHgA1YBtgNZAb4DXgHXAz0B9wMgAfwDWgHHAysBQiQAAw8B
EwNAAW8DXQHUA1MB7wNNAZIDEgEYHAADSQGGAyEB+wMuAfkDUwGnAxUBHBAABAIDDQERAzYBVwNYAbcD
WAHqA1wB7ANfAdUDSwGPAzoBYAMeASsDBgEHMAADPwFsAy4B+QNTAacDFQEcBAADDwEUA1cBuQMAAf8D
WAHtAz8BbQM/AW0DWAHtAwAB/wNXAbkDDwEUBAADFQEcA1MBpwMuAfkDPwFsIAADFAEbA1IBpQMdAf0D
VQG0AyYBOAMHAQk4AAMWAR4DVgGzAx4B/QMdAf0DUgGlAxQBGxAAAxQBGwNQAZoDOQH2AwAB/wNRAaQD
JAE0A0QBeANeAd0DPQH3A1sB3gM7AWMDDgESIAADAgEDAx4BKgNVAbQDPQH3A1EBoQMbASYDBQEGHAAD
SQGGAyEB+wMuAfkDUwGnAxUBHAwABAEDEwEZA0QBegNgAdsDOwH3Az0B9wNZAbsDKwFCAxMBGQMIAQoD
AgEDNAADPwFsAy4B+QNTAacDFQEcBAADDwEUA1cBuQMAAf8DWAHtAz8BbQM/AW0DWAHtAwAB/wNXAbkD
DwEUBAADFQEcA1MBpwMuAfkDPwFsIAADFAEbA1IBpQMdAf0DVQG0AyYBOAMHAQk4AAMWAR4DVgGzAx4B
/QMdAf0DUgGlAxQBGxAAAxQBGwNQAZoDOQH2AwAB/wNOAZUDMQFMA2EB4QMAAf8DWwHNAxoBJCgAAxUB
HANTAacDLgH5AyEB+wNJAYYkAANJAYYDIQH7Ay4B+QNTAacDFQEcDAADCwEOA00BkgMrAfkDAAH/AwAB
/wNVAa8DFgEdRAADPwFsAy4B+QNTAacDFQEcBAADDwEUA1cBuQMAAf8DWAHtAz8BbQM/AW0DWAHtAwAB
/wNXAbkDDwEUBAADFQEcA1MBpwMuAfkDPwFsIAADFAEbA1IBpQMdAf0DVgG2AygBPAMIAQo4AAMWAR4D
VgGzAx4B/QMdAf0DUgGlAxQBGxAAAxQBGwNQAZoDOQH2AwAB/wNDAfUDVAHvAyEB/AMRAf4DXQHMAx0B
KQQCGAAEAgMGAQcDDwETAzIBTwNZAcADQgH1A10B0QM6AWAkAANJAYYDIQH7Ay4B+QNTAacDFgEeAwwB
EAMqAUADVwG5A1oB6QNTAe8DWwHoA10BzwNGAX0DEwEaAwIBA0QAAz8BbAMuAfkDUwGnAxUBHAQAAw8B
FANXAbkDAAH/A1gB7QM/AW0DPwFtA1gB7QMAAf8DVwG5Aw8BFAQAAxUBHANTAacDLgH5Az8BbCAAAxQB
GwNSAaUDHQH9A1sB0AM/AWwDGAEhAw4BEgMOARIDDgESAw4BEgMOARIDDgESAw4BEgMOARIDDgESAw4B
EgMOARIDDgESAw4BEgMOARIDIAEuA1cBuQMeAf0DHQH9A1IBpQMUARsQAAMUARsDUAGaAzkB9gMAAf8D
AAH/AyMB+gNCAfUDHgH9A2AB4ANAAXEDHQEoAw4BEgMNAREDCQEMAwgBCgMLBA4BEgMcAScDOAFbA0QB
eQNaAccDYQHhA1YBtgM0AVQDDAEPJAADSQGGAyEB+wMuAfkDVQGuAyoBQAM/AW0DXQHRA0MB9QNWAe4D
WQG7A0QBeQMqAUADEQEWBAJIAAM/AWwDLgH5A1UBrgMgAS0DDgESAxsBJQNZAb4DAAH/A1gB7QM/AW0D
PwFtA1gB7QMAAf8DWQG+AxsBJQMOARIDIAEtA1UBrgMuAfkDPwFsIAADEwEaA1EBpAMfAfwDOQH2A10B
zANLAY0DRgGAA0YBgANGAYADRgGAA0YBgANGAYADRgGAA0YBgANGAYADRgGAA0YBgANGAYADRgGAA0YB
gANLAY8DXAHZAx4B/QNFAfQDTgGYAxMBGRAAAxIBGANLAYwDWAHtAwAB/wMgAf0DXAHZA1UBsQNdAd8D
QwH1A2AB4ANPAZsDRgGAA0QBegM5AV4DMwFQAz0BaANGAX4DUAGaA18B2gNbAeQDXwHaA08BmwMpAT4D
CgENKAADSQGGAyEB+wMgAfwDWwHTA1MBpgNcAdkDLgH5A2AB4ANQAZoDLQFFAxABFQMGAQcEAUwAAz8B
bAMgAfwDWwHTA0wBjgNGAYADSgGKA10B3AMeAf0DXQHfAzoBYgM6AWIDXQHfAx4B/QNdAdwDSgGKA0YB
gANMAY4DWwHTAyAB/AM/AWwgAAMSARcDSgGLA1oB6gMPAf4DJAH6A1IB7wNYAe0DWAHtA1gB7QNYAe0D
WAHtA1gB7QNYAe0DWAHtA1gB7QNYAe0DWAHtA1gB7QNYAe0DWAHtA1IB7wMkAfoDHgH9A1cBwgM0AVQD
CwEOEAADCAEKAy4BSANVAbEDXgHiA2AB2wNPAZsDLQFEA0MBdwNdAcwDYQHhA1gB6gNYAe0DWgHpA1sB
0wNcAcgDXwHaA1wB6wNOAfIDLgH5A14B0gNEAXgDIQEvAwcBCSwAA0IBdQNYAeoDAQH/Ay4B+QNQAfAD
XAHsA1wB2QNEAXsDHgEqAwYBCFgAAz8BbAMBAf8DLgH5A1IB7wNYAe0DVgHuAyQB+gM5AfYDUAGdAyAB
LQMgAS0DUAGdAzkB9gMkAfoDVgHuA1gB7QNSAe8DLgH5Aw8B/gM/AWwgAAMFAQYDGgEjA00BkwNgAeYD
WgHtAzwB9gM9AfcDPQH3Az0B9wM9AfcDPQH3Az0B9wM9AfcDPQH3Az0B9wM9AfcDPQH3Az0B9wM9AfcD
PQH3AzwB9gNaAe0DXwHlA0cBgwMKAQ0EAhAABAEDCAEKAx8BLAMuAUgDLQFEAxsBJQMHAQkDDwETAyMB
MgMuAUcDVwG5A10B6ANaAekDVwHuA1UB8QNcAewDWgHpA10B6ANbAdADNAFTAw8BEwMCAQMwAAMWAR4D
RwGDA1oB7QNVAfEDXwHVA0wBjgMrAUIDDwEUBAJcAAM6AWIDWgHpA1oB7QM8AfYDPQH3AzwB9gNcAewD
XQHcAzwBZgMGAQgDBgEIAzwBZgNdAdwDWgHtAzwB9gM9AfcDPAH2A1oB7QNgAeYDOgFiLAAEAQMjATMD
TgGXA1QBqwNUAasDVAGrA1QBqwNUAasDVAGrA1QBqwNUAasDVAGrA1QBqwNUAasDVAGrA1QBqwNUAasD
TgGXAyMBMwQBTAADCQELAzkBXQNHAYIDJwE6AwQBBUgABAIDIgExAzoBYQMPARRwAAMDAQQDKAE7A04B
mANUAasDUQGeAyEBLxQAAwMBBAMlATcDUAGfA1QBqwNOAZgDKAE7AwMBBBgAAUIBTQE+BwABPgMAASgD
AAF8AwABMAMAAQEBAAEBBgABAxYAA/8BAAH8AQMB8AE/AQMB8AEPAcAIAAH8AQEB8AE/AQMB8AEHAcAI
AAH8AQABMAE/AQAB8AEDAYAIAAH8AQABEAE/AQABcAEAAYAIAAH8AgABPwEAATABAAGACAAB/AIAAT8B
AAEwAQABgAgAAfwCAAE/DAAB/AEIAQABPwEICwAB/AEMAQABPwEMAQABIAkAAfwBDwEAAT8BDAEAATAJ
AAH8AQ8BgAE/AQ4BAAE4CQAB/AEPAfABPwEPAcABPwkAAfwBDwGAAT8BDgEAATgJAAH8AQ8BAAE/AQwB
AAEwCQAB/AEMAQABPwEMAQABIAkAAfwBCAEAAT8BCAsAAfwCAAE/DAAB/AIAAT8BAAEwCgAB/AIAAT8B
AAEwAQABgAgAAfwBAAEQAT8BAAFwAQABgAgAAfwBAAEwAT8BAAHwAQMBgAgAAfwBAAHwAT8BAQHwAQcB
wAgAAfwBAwHwAT8BAwHwAQ8BwAgAAf4BHwH4AX8BDwH4AX8BwAgAAeACAAEHAf4BAAEBAf8B4AEPAv8B
4AEAAQEB8AHgAgABBwH8AgAB/wHgAQcC/wHgAQABAQHwAeACAAEHAfwCAAF/AeABAwL/AeABAAEBAfAB
4AIAAQcB/AIAAT8B4AEAAX8B/wHgAQABAQHwAeABfwH+AQcB/AEHAcABPwHgAQABPwH/AeEBAAEhAfAB
4AF/Af4BBwL/AfgBPwHgAcABPwH/AeEBAAEhAfAB4AF/Af4BBwL/AfgBDwHgAcABAwH/AeEBAAEhAfAB
4AF/Af4BBwL/AfwBDwHgAfABAQH/AeEBAAEhAfAB4AF/Af4BBwL/AfwBDwHgAfwBAAH/AeEBAAEhAfAB
4AF/Af4BBwP/AQ8B4AH+AQAB/wHhAQABIQHwAeABfwH+AQcD/wEPAeAB/wEAAX8B4QEAASEB8AHgAX8B
/gEHA/8BDwHgAf8B8AF/AeEBAAEhAfAB4AF/Af4BBwGAAQcB/wEPAeAB/wEAAX8B4QEAASEB8AHgAX8B
/gEHAYABBwH/AQ8B4AH+AQAB/wHhAQABIQHwAeABfwH+AQcBgAEHAfwBDwHgAfwBAAH/AeEBAAEhAfAB
4AF/Af4BBwGAAQcB/AEPAeAB8AEBAf8B4QEAASEB8AHgAX8B/gEHAYABBwH4AQ8C4AEDAf8B4QEAASEB
8AHgAX8B/gEHAYABHwH4AT8C4AE/Af8B4QEAASEB8AHgAX8B/gEHAYABDwHAAT8B4AEAAT8B/wHhAQAB
IQHwAeACAAEHAYACAAE/AeABAAF/Af8B4AEAAQEB8AHgAgABBwGAAgABfwHgAQAC/wHgAQABAQHwAeAC
AAEHAYACAAH/AeABBwL/AeABAAEBAfAB4AIAAQcBgAEAAQEB/wHgAQ8C/wHgAQABAQHwAfwCAAE/Af8B
+AE/Af8B8AP/AfABPgEDAfAL
</value>
</data>
<metadata name="toolTip.TrayLocation" type="System.Drawing.Point, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a">
<value>165, 17</value>
</metadata>
</root>

View File

@@ -0,0 +1,300 @@
namespace SpineViewer.Controls
{
partial class SpineViewPropertyGrid
{
/// <summary>
/// 必需的设计器变量。
/// </summary>
private System.ComponentModel.IContainer components = null;
/// <summary>
/// 清理所有正在使用的资源。
/// </summary>
/// <param name="disposing">如果应释放托管资源,为 true否则为 false。</param>
protected override void Dispose(bool disposing)
{
if (disposing && (components != null))
{
components.Dispose();
}
base.Dispose(disposing);
}
#region
/// <summary>
/// 设计器支持所需的方法 - 不要修改
/// 使用代码编辑器修改此方法的内容。
/// </summary>
private void InitializeComponent()
{
components = new System.ComponentModel.Container();
tabControl = new TabControl();
tabPage_BaseInfo = new TabPage();
propertyGrid_BaseInfo = new PropertyGrid();
tabPage_Render = new TabPage();
propertyGrid_Render = new PropertyGrid();
tabPage_Transform = new TabPage();
propertyGrid_Transform = new PropertyGrid();
tabPage_Skin = new TabPage();
propertyGrid_Skin = new PropertyGrid();
contextMenuStrip_Skin = new ContextMenuStrip(components);
toolStripMenuItem_AddSkin = new ToolStripMenuItem();
toolStripMenuItem_RemoveSkin = new ToolStripMenuItem();
tabPage_Animation = new TabPage();
propertyGrid_Animation = new PropertyGrid();
contextMenuStrip_Animation = new ContextMenuStrip(components);
toolStripMenuItem_AddAnimation = new ToolStripMenuItem();
toolStripMenuItem_RemoveAnimation = new ToolStripMenuItem();
tabPage_Debug = new TabPage();
propertyGrid_Debug = new PropertyGrid();
tabControl.SuspendLayout();
tabPage_BaseInfo.SuspendLayout();
tabPage_Render.SuspendLayout();
tabPage_Transform.SuspendLayout();
tabPage_Skin.SuspendLayout();
contextMenuStrip_Skin.SuspendLayout();
tabPage_Animation.SuspendLayout();
contextMenuStrip_Animation.SuspendLayout();
tabPage_Debug.SuspendLayout();
SuspendLayout();
//
// tabControl
//
tabControl.Alignment = TabAlignment.Bottom;
tabControl.Controls.Add(tabPage_BaseInfo);
tabControl.Controls.Add(tabPage_Render);
tabControl.Controls.Add(tabPage_Transform);
tabControl.Controls.Add(tabPage_Skin);
tabControl.Controls.Add(tabPage_Animation);
tabControl.Controls.Add(tabPage_Debug);
tabControl.Dock = DockStyle.Fill;
tabControl.ItemSize = new Size(90, 35);
tabControl.Location = new Point(0, 0);
tabControl.Multiline = true;
tabControl.Name = "tabControl";
tabControl.Padding = new Point(0, 0);
tabControl.SelectedIndex = 0;
tabControl.Size = new Size(372, 448);
tabControl.SizeMode = TabSizeMode.FillToRight;
tabControl.TabIndex = 0;
//
// tabPage_BaseInfo
//
tabPage_BaseInfo.BackColor = SystemColors.Control;
tabPage_BaseInfo.Controls.Add(propertyGrid_BaseInfo);
tabPage_BaseInfo.Location = new Point(4, 4);
tabPage_BaseInfo.Margin = new Padding(0);
tabPage_BaseInfo.Name = "tabPage_BaseInfo";
tabPage_BaseInfo.Size = new Size(364, 370);
tabPage_BaseInfo.TabIndex = 0;
tabPage_BaseInfo.Text = "基本信息";
//
// propertyGrid_BaseInfo
//
propertyGrid_BaseInfo.Dock = DockStyle.Fill;
propertyGrid_BaseInfo.HelpVisible = false;
propertyGrid_BaseInfo.Location = new Point(0, 0);
propertyGrid_BaseInfo.Name = "propertyGrid_BaseInfo";
propertyGrid_BaseInfo.PropertySort = PropertySort.Alphabetical;
propertyGrid_BaseInfo.Size = new Size(364, 370);
propertyGrid_BaseInfo.TabIndex = 0;
propertyGrid_BaseInfo.ToolbarVisible = false;
//
// tabPage_Render
//
tabPage_Render.BackColor = SystemColors.Control;
tabPage_Render.Controls.Add(propertyGrid_Render);
tabPage_Render.Location = new Point(4, 4);
tabPage_Render.Margin = new Padding(0);
tabPage_Render.Name = "tabPage_Render";
tabPage_Render.Size = new Size(364, 380);
tabPage_Render.TabIndex = 1;
tabPage_Render.Text = "渲染";
//
// propertyGrid_Render
//
propertyGrid_Render.Dock = DockStyle.Fill;
propertyGrid_Render.HelpVisible = false;
propertyGrid_Render.Location = new Point(0, 0);
propertyGrid_Render.Name = "propertyGrid_Render";
propertyGrid_Render.PropertySort = PropertySort.Alphabetical;
propertyGrid_Render.Size = new Size(364, 380);
propertyGrid_Render.TabIndex = 1;
propertyGrid_Render.ToolbarVisible = false;
//
// tabPage_Transform
//
tabPage_Transform.BackColor = SystemColors.Control;
tabPage_Transform.Controls.Add(propertyGrid_Transform);
tabPage_Transform.Location = new Point(4, 4);
tabPage_Transform.Margin = new Padding(0);
tabPage_Transform.Name = "tabPage_Transform";
tabPage_Transform.Size = new Size(364, 380);
tabPage_Transform.TabIndex = 2;
tabPage_Transform.Text = "变换";
//
// propertyGrid_Transform
//
propertyGrid_Transform.Dock = DockStyle.Fill;
propertyGrid_Transform.HelpVisible = false;
propertyGrid_Transform.Location = new Point(0, 0);
propertyGrid_Transform.Name = "propertyGrid_Transform";
propertyGrid_Transform.PropertySort = PropertySort.Alphabetical;
propertyGrid_Transform.Size = new Size(364, 380);
propertyGrid_Transform.TabIndex = 1;
propertyGrid_Transform.ToolbarVisible = false;
//
// tabPage_Skin
//
tabPage_Skin.BackColor = SystemColors.Control;
tabPage_Skin.Controls.Add(propertyGrid_Skin);
tabPage_Skin.Location = new Point(4, 4);
tabPage_Skin.Margin = new Padding(0);
tabPage_Skin.Name = "tabPage_Skin";
tabPage_Skin.Size = new Size(364, 380);
tabPage_Skin.TabIndex = 3;
tabPage_Skin.Text = "皮肤";
//
// propertyGrid_Skin
//
propertyGrid_Skin.ContextMenuStrip = contextMenuStrip_Skin;
propertyGrid_Skin.Dock = DockStyle.Fill;
propertyGrid_Skin.HelpVisible = false;
propertyGrid_Skin.Location = new Point(0, 0);
propertyGrid_Skin.Name = "propertyGrid_Skin";
propertyGrid_Skin.PropertySort = PropertySort.NoSort;
propertyGrid_Skin.Size = new Size(364, 380);
propertyGrid_Skin.TabIndex = 1;
propertyGrid_Skin.ToolbarVisible = false;
//
// contextMenuStrip_Skin
//
contextMenuStrip_Skin.ImageScalingSize = new Size(24, 24);
contextMenuStrip_Skin.Items.AddRange(new ToolStripItem[] { toolStripMenuItem_AddSkin, toolStripMenuItem_RemoveSkin });
contextMenuStrip_Skin.Name = "contextMenuStrip1";
contextMenuStrip_Skin.Size = new Size(117, 64);
contextMenuStrip_Skin.Opening += contextMenuStrip_Skin_Opening;
//
// toolStripMenuItem_AddSkin
//
toolStripMenuItem_AddSkin.Name = "toolStripMenuItem_AddSkin";
toolStripMenuItem_AddSkin.Size = new Size(116, 30);
toolStripMenuItem_AddSkin.Text = "添加";
toolStripMenuItem_AddSkin.Click += toolStripMenuItem_AddSkin_Click;
//
// toolStripMenuItem_RemoveSkin
//
toolStripMenuItem_RemoveSkin.Name = "toolStripMenuItem_RemoveSkin";
toolStripMenuItem_RemoveSkin.Size = new Size(116, 30);
toolStripMenuItem_RemoveSkin.Text = "移除";
toolStripMenuItem_RemoveSkin.Click += toolStripMenuItem_RemoveSkin_Click;
//
// tabPage_Animation
//
tabPage_Animation.BackColor = SystemColors.Control;
tabPage_Animation.Controls.Add(propertyGrid_Animation);
tabPage_Animation.Location = new Point(4, 4);
tabPage_Animation.Margin = new Padding(0);
tabPage_Animation.Name = "tabPage_Animation";
tabPage_Animation.Size = new Size(364, 380);
tabPage_Animation.TabIndex = 4;
tabPage_Animation.Text = "动画";
//
// propertyGrid_Animation
//
propertyGrid_Animation.ContextMenuStrip = contextMenuStrip_Animation;
propertyGrid_Animation.Dock = DockStyle.Fill;
propertyGrid_Animation.HelpVisible = false;
propertyGrid_Animation.Location = new Point(0, 0);
propertyGrid_Animation.Name = "propertyGrid_Animation";
propertyGrid_Animation.PropertySort = PropertySort.NoSort;
propertyGrid_Animation.Size = new Size(364, 380);
propertyGrid_Animation.TabIndex = 1;
propertyGrid_Animation.ToolbarVisible = false;
//
// contextMenuStrip_Animation
//
contextMenuStrip_Animation.ImageScalingSize = new Size(24, 24);
contextMenuStrip_Animation.Items.AddRange(new ToolStripItem[] { toolStripMenuItem_AddAnimation, toolStripMenuItem_RemoveAnimation });
contextMenuStrip_Animation.Name = "contextMenuStrip1";
contextMenuStrip_Animation.Size = new Size(117, 64);
contextMenuStrip_Animation.Opening += contextMenuStrip_Animation_Opening;
//
// toolStripMenuItem_AddAnimation
//
toolStripMenuItem_AddAnimation.Name = "toolStripMenuItem_AddAnimation";
toolStripMenuItem_AddAnimation.Size = new Size(116, 30);
toolStripMenuItem_AddAnimation.Text = "添加";
toolStripMenuItem_AddAnimation.Click += toolStripMenuItem_AddAnimation_Click;
//
// toolStripMenuItem_RemoveAnimation
//
toolStripMenuItem_RemoveAnimation.Name = "toolStripMenuItem_RemoveAnimation";
toolStripMenuItem_RemoveAnimation.Size = new Size(116, 30);
toolStripMenuItem_RemoveAnimation.Text = "移除";
toolStripMenuItem_RemoveAnimation.Click += toolStripMenuItem_RemoveAnimation_Click;
//
// tabPage_Debug
//
tabPage_Debug.BackColor = SystemColors.Control;
tabPage_Debug.Controls.Add(propertyGrid_Debug);
tabPage_Debug.Location = new Point(4, 4);
tabPage_Debug.Name = "tabPage_Debug";
tabPage_Debug.Size = new Size(364, 380);
tabPage_Debug.TabIndex = 5;
tabPage_Debug.Text = "调试";
//
// propertyGrid_Debug
//
propertyGrid_Debug.Dock = DockStyle.Fill;
propertyGrid_Debug.HelpVisible = false;
propertyGrid_Debug.Location = new Point(0, 0);
propertyGrid_Debug.Name = "propertyGrid_Debug";
propertyGrid_Debug.PropertySort = PropertySort.NoSort;
propertyGrid_Debug.Size = new Size(364, 380);
propertyGrid_Debug.TabIndex = 2;
propertyGrid_Debug.ToolbarVisible = false;
//
// SpinePropertyGrid
//
AutoScaleDimensions = new SizeF(11F, 24F);
AutoScaleMode = AutoScaleMode.Font;
Controls.Add(tabControl);
Name = "SpinePropertyGrid";
Size = new Size(372, 448);
tabControl.ResumeLayout(false);
tabPage_BaseInfo.ResumeLayout(false);
tabPage_Render.ResumeLayout(false);
tabPage_Transform.ResumeLayout(false);
tabPage_Skin.ResumeLayout(false);
contextMenuStrip_Skin.ResumeLayout(false);
tabPage_Animation.ResumeLayout(false);
contextMenuStrip_Animation.ResumeLayout(false);
tabPage_Debug.ResumeLayout(false);
ResumeLayout(false);
}
#endregion
private TabControl tabControl;
private TabPage tabPage_BaseInfo;
private TabPage tabPage_Render;
private TabPage tabPage_Transform;
private TabPage tabPage_Skin;
private TabPage tabPage_Animation;
private PropertyGrid propertyGrid_BaseInfo;
private PropertyGrid propertyGrid_Render;
private PropertyGrid propertyGrid_Transform;
private PropertyGrid propertyGrid_Skin;
private PropertyGrid propertyGrid_Animation;
private ContextMenuStrip contextMenuStrip_Skin;
private ContextMenuStrip contextMenuStrip_Animation;
private ToolStripMenuItem toolStripMenuItem_AddSkin;
private ToolStripMenuItem toolStripMenuItem_RemoveSkin;
private ToolStripMenuItem toolStripMenuItem_AddAnimation;
private ToolStripMenuItem toolStripMenuItem_RemoveAnimation;
private TabPage tabPage_Debug;
private PropertyGrid propertyGrid_Debug;
}
}

View File

@@ -0,0 +1,140 @@
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Drawing;
using System.Data;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms;
using SpineViewer.Utils;
using SpineViewer.Spine.SpineView;
namespace SpineViewer.Controls
{
public partial class SpineViewPropertyGrid : UserControl
{
public SpineViewPropertyGrid()
{
InitializeComponent();
}
/// <summary>
/// 设置选中的对象列表, 可以赋值 null 来清空选中, 行为与 PropertyGrid.SelectedObjects 类似
/// </summary>
public SpineObjectProperty[] SelectedSpines
{
get => selectedSpines ?? [];
set
{
if (value is null || value.Length <= 0)
{
selectedSpines = null;
propertyGrid_BaseInfo.SelectedObject = null;
propertyGrid_Render.SelectedObject = null;
propertyGrid_Transform.SelectedObject = null;
propertyGrid_Skin.SelectedObject = null;
propertyGrid_Animation.SelectedObject = null;
propertyGrid_Debug.SelectedObject = null;
}
else
{
selectedSpines = value;
propertyGrid_BaseInfo.SelectedObjects = value.Select(e => e.BaseInfo).ToArray();
propertyGrid_Render.SelectedObjects = value.Select(e => e.Render).ToArray();
propertyGrid_Transform.SelectedObjects = value.Select(e => e.Transform).ToArray();
propertyGrid_Skin.SelectedObjects = value.Select(e => e.Skin).ToArray();
propertyGrid_Animation.SelectedObjects = value.Select(e => e.Animation).ToArray();
propertyGrid_Debug.SelectedObjects = value.Select(e => e.Debug).ToArray();
}
}
}
private SpineObjectProperty[]? selectedSpines = null;
private void contextMenuStrip_Skin_Opening(object sender, CancelEventArgs e)
{
if (selectedSpines?.Length == 1)
{
toolStripMenuItem_AddSkin.Enabled = true;
toolStripMenuItem_RemoveSkin.Enabled = propertyGrid_Skin.SelectedGridItem.Value is SkinNameProperty;
}
else
{
toolStripMenuItem_AddSkin.Enabled = false;
toolStripMenuItem_RemoveSkin.Enabled = false;
}
}
private void contextMenuStrip_Animation_Opening(object sender, CancelEventArgs e)
{
if (selectedSpines?.Length == 1)
{
toolStripMenuItem_AddAnimation.Enabled = true;
toolStripMenuItem_RemoveAnimation.Enabled = propertyGrid_Animation.SelectedGridItem.Value is TrackAnimationProperty;
}
else
{
toolStripMenuItem_AddAnimation.Enabled = false;
toolStripMenuItem_RemoveAnimation.Enabled = false;
}
}
private void toolStripMenuItem_AddSkin_Click(object sender, EventArgs e)
{
if (selectedSpines?.Length != 1) return;
var spine = selectedSpines[0].Skin.Spine;
if (spine.SkinNames.Count <= 0)
{
MessagePopup.Info("没有可用的皮肤");
return;
}
spine.LoadSkin(spine.SkinNames[0]);
propertyGrid_Skin.Refresh();
}
private void toolStripMenuItem_RemoveSkin_Click(object sender, EventArgs e)
{
if (selectedSpines?.Length != 1) return;
if (propertyGrid_Skin.SelectedGridItem.Value is SkinNameProperty wrapper)
{
selectedSpines[0].Skin.Spine.UnloadSkin(wrapper.Index);
propertyGrid_Skin.Refresh();
}
}
private void toolStripMenuItem_AddAnimation_Click(object sender, EventArgs e)
{
if (selectedSpines?.Length != 1) return;
var spine = selectedSpines[0].Animation.Spine;
spine.SetAnimation(spine.GetTrackIndices().Max() + 1, spine.AnimationNames[0]);
propertyGrid_Animation.Refresh();
}
private void toolStripMenuItem_RemoveAnimation_Click(object sender, EventArgs e)
{
if (selectedSpines?.Length != 1) return;
if (propertyGrid_Animation.SelectedGridItem.Value is TrackAnimationProperty wrapper)
{
selectedSpines[0].Animation.Spine.ClearTrack(wrapper.Index);
propertyGrid_Animation.Refresh();
}
}
public override void Refresh()
{
base.Refresh();
propertyGrid_BaseInfo.Refresh();
propertyGrid_Render.Refresh();
propertyGrid_Transform.Refresh();
propertyGrid_Skin.Refresh();
propertyGrid_Animation.Refresh();
propertyGrid_Debug.Refresh();
}
}
}

View File

@@ -117,4 +117,10 @@
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<metadata name="contextMenuStrip_Skin.TrayLocation" type="System.Drawing.Point, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a">
<value>29, 26</value>
</metadata>
<metadata name="contextMenuStrip_Animation.TrayLocation" type="System.Drawing.Point, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a">
<value>318, 25</value>
</metadata>
</root>

View File

@@ -1,4 +1,5 @@
using System;
using SpineViewer.Utils;
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Diagnostics;
@@ -40,7 +41,7 @@ namespace SpineViewer.Dialogs
else
{
Clipboard.SetText(url);
MessageBox.Info("链接已复制到剪贴板,请前往浏览器进行访问");
MessagePopup.Info("链接已复制到剪贴板,请前往浏览器进行访问");
}
}
}

View File

@@ -1,139 +0,0 @@
namespace SpineViewer.Dialogs
{
partial class AnimationTracksEditorDialog
{
/// <summary>
/// Required designer variable.
/// </summary>
private System.ComponentModel.IContainer components = null;
/// <summary>
/// Clean up any resources being used.
/// </summary>
/// <param name="disposing">true if managed resources should be disposed; otherwise, false.</param>
protected override void Dispose(bool disposing)
{
if (disposing && (components != null))
{
components.Dispose();
}
base.Dispose(disposing);
}
#region Windows Form Designer generated code
/// <summary>
/// Required method for Designer support - do not modify
/// the contents of this method with the code editor.
/// </summary>
private void InitializeComponent()
{
panel = new Panel();
tableLayoutPanel1 = new TableLayoutPanel();
flowLayoutPanel1 = new FlowLayoutPanel();
button_Add = new Button();
button_Delete = new Button();
propertyGrid_AnimationTracks = new PropertyGrid();
panel.SuspendLayout();
tableLayoutPanel1.SuspendLayout();
flowLayoutPanel1.SuspendLayout();
SuspendLayout();
//
// panel
//
panel.Controls.Add(tableLayoutPanel1);
panel.Dock = DockStyle.Fill;
panel.Location = new Point(0, 0);
panel.Name = "panel";
panel.Padding = new Padding(50, 15, 50, 10);
panel.Size = new Size(666, 483);
panel.TabIndex = 0;
//
// tableLayoutPanel1
//
tableLayoutPanel1.ColumnCount = 1;
tableLayoutPanel1.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 100F));
tableLayoutPanel1.Controls.Add(flowLayoutPanel1, 0, 1);
tableLayoutPanel1.Controls.Add(propertyGrid_AnimationTracks, 0, 0);
tableLayoutPanel1.Dock = DockStyle.Fill;
tableLayoutPanel1.Location = new Point(50, 15);
tableLayoutPanel1.Name = "tableLayoutPanel1";
tableLayoutPanel1.RowCount = 2;
tableLayoutPanel1.RowStyles.Add(new RowStyle(SizeType.Percent, 100F));
tableLayoutPanel1.RowStyles.Add(new RowStyle());
tableLayoutPanel1.Size = new Size(566, 458);
tableLayoutPanel1.TabIndex = 0;
//
// flowLayoutPanel1
//
flowLayoutPanel1.AutoSize = true;
flowLayoutPanel1.Controls.Add(button_Add);
flowLayoutPanel1.Controls.Add(button_Delete);
flowLayoutPanel1.Dock = DockStyle.Fill;
flowLayoutPanel1.Location = new Point(3, 415);
flowLayoutPanel1.Name = "flowLayoutPanel1";
flowLayoutPanel1.Size = new Size(560, 40);
flowLayoutPanel1.TabIndex = 2;
//
// button_Add
//
button_Add.Location = new Point(3, 3);
button_Add.Name = "button_Add";
button_Add.Size = new Size(112, 34);
button_Add.TabIndex = 0;
button_Add.Text = "添加";
button_Add.UseVisualStyleBackColor = true;
button_Add.Click += button_Add_Click;
//
// button_Delete
//
button_Delete.Location = new Point(121, 3);
button_Delete.Name = "button_Delete";
button_Delete.Size = new Size(112, 34);
button_Delete.TabIndex = 1;
button_Delete.Text = "删除";
button_Delete.UseVisualStyleBackColor = true;
button_Delete.Click += button_Delete_Click;
//
// propertyGrid_AnimationTracks
//
propertyGrid_AnimationTracks.Dock = DockStyle.Fill;
propertyGrid_AnimationTracks.HelpVisible = false;
propertyGrid_AnimationTracks.Location = new Point(3, 3);
propertyGrid_AnimationTracks.Name = "propertyGrid_AnimationTracks";
propertyGrid_AnimationTracks.PropertySort = PropertySort.NoSort;
propertyGrid_AnimationTracks.Size = new Size(560, 406);
propertyGrid_AnimationTracks.TabIndex = 1;
propertyGrid_AnimationTracks.ToolbarVisible = false;
//
// AnimationTracksEditorDialog
//
AutoScaleDimensions = new SizeF(11F, 24F);
AutoScaleMode = AutoScaleMode.Font;
ClientSize = new Size(666, 483);
Controls.Add(panel);
FormBorderStyle = FormBorderStyle.FixedDialog;
MaximizeBox = false;
MinimizeBox = false;
Name = "AnimationTracksEditorDialog";
ShowIcon = false;
ShowInTaskbar = false;
StartPosition = FormStartPosition.CenterScreen;
Text = "多轨道动画实时编辑器";
panel.ResumeLayout(false);
tableLayoutPanel1.ResumeLayout(false);
tableLayoutPanel1.PerformLayout();
flowLayoutPanel1.ResumeLayout(false);
ResumeLayout(false);
}
#endregion
private Panel panel;
private TableLayoutPanel tableLayoutPanel1;
private FlowLayoutPanel flowLayoutPanel1;
private Button button_Add;
private Button button_Delete;
private PropertyGrid propertyGrid_AnimationTracks;
}
}

View File

@@ -1,43 +0,0 @@
using SpineViewer.Spine;
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms;
namespace SpineViewer.Dialogs
{
public partial class AnimationTracksEditorDialog : Form
{
private readonly Spine.Spine spine;
public AnimationTracksEditorDialog(Spine.Spine spine)
{
InitializeComponent();
this.spine = spine;
propertyGrid_AnimationTracks.SelectedObject = spine.AnimationTracks;
}
private void button_Add_Click(object sender, EventArgs e)
{
spine.SetAnimation(spine.GetTrackIndices().Max() + 1, spine.AnimationNames[0]);
propertyGrid_AnimationTracks.Refresh();
}
private void button_Delete_Click(object sender, EventArgs e)
{
if (propertyGrid_AnimationTracks.SelectedGridItem?.Value is TrackWrapper tr)
{
if (tr.Index == 0)
MessageBox.Info("必须保留轨道 0");
else
spine.ClearTrack(tr.Index);
}
propertyGrid_AnimationTracks.Refresh();
propertyGrid_AnimationTracks.SelectedGridItem = propertyGrid_AnimationTracks.SelectedGridItem?.Parent?.GridItems?.Cast<GridItem>().Last();
}
}
}

View File

@@ -1,4 +1,5 @@
using SpineViewer.Spine;
using SpineViewer.Utils;
using System;
using System.Collections.Generic;
using System.ComponentModel;
@@ -21,7 +22,7 @@ namespace SpineViewer.Dialogs
public BatchOpenSpineDialog()
{
InitializeComponent();
comboBox_Version.DataSource = SpineHelper.Names.ToList();
comboBox_Version.DataSource = SpineUtils.Names.ToList();
comboBox_Version.DisplayMember = "Value";
comboBox_Version.ValueMember = "Key";
comboBox_Version.SelectedValue = SpineVersion.Auto;
@@ -35,7 +36,7 @@ namespace SpineViewer.Dialogs
if (items.Count <= 0)
{
MessageBox.Info("未选择任何文件");
MessagePopup.Info("未选择任何文件");
return;
}
@@ -43,14 +44,14 @@ namespace SpineViewer.Dialogs
{
if (!File.Exists(p))
{
MessageBox.Info($"{p}", "skel文件不存在");
MessagePopup.Info($"{p}", "skel文件不存在");
return;
}
}
if (version != SpineVersion.Auto && !Spine.Spine.HasImplementation(version))
if (version != SpineVersion.Auto && !Spine.SpineObject.HasImplementation(version))
{
MessageBox.Info($"{version.GetName()} 版本尚未实现(咕咕咕~");
MessagePopup.Info($"{version.GetName()} 版本尚未实现(咕咕咕~");
return;
}

View File

@@ -1,4 +1,5 @@
using SpineViewer.Spine;
using SpineViewer.Utils;
using System;
using System.Collections.Generic;
using System.ComponentModel;
@@ -22,13 +23,13 @@ namespace SpineViewer.Dialogs
{
InitializeComponent();
comboBox_SourceVersion.DataSource = SpineHelper.Names.ToList();
comboBox_SourceVersion.DataSource = SpineUtils.Names.ToList();
comboBox_SourceVersion.DisplayMember = "Value";
comboBox_SourceVersion.ValueMember = "Key";
comboBox_SourceVersion.SelectedValue = SpineVersion.Auto;
// 目标版本不包含自动
var versionsWithoutAuto = SpineHelper.Names.ToDictionary();
var versionsWithoutAuto = SpineUtils.Names.ToDictionary();
versionsWithoutAuto.Remove(SpineVersion.Auto);
comboBox_TargetVersion.DataSource = versionsWithoutAuto.ToList();
comboBox_TargetVersion.DisplayMember = "Value";
@@ -46,7 +47,7 @@ namespace SpineViewer.Dialogs
if (items.Count <= 0)
{
MessageBox.Info("未选择任何文件");
MessagePopup.Info("未选择任何文件");
return;
}
@@ -54,20 +55,20 @@ namespace SpineViewer.Dialogs
{
if (!File.Exists(p))
{
MessageBox.Info($"{p}", "skel文件不存在");
MessagePopup.Info($"{p}", "skel文件不存在");
return;
}
}
if (sourceVersion != SpineVersion.Auto && !SkeletonConverter.HasImplementation(sourceVersion))
{
MessageBox.Info($"{sourceVersion.GetName()} 版本尚未实现(咕咕咕~");
MessagePopup.Info($"{sourceVersion.GetName()} 版本尚未实现(咕咕咕~");
return;
}
if (!SkeletonConverter.HasImplementation(targetVersion))
{
MessageBox.Info($"{targetVersion.GetName()} 版本尚未实现(咕咕咕~");
MessagePopup.Info($"{targetVersion.GetName()} 版本尚未实现(咕咕咕~");
return;
}

View File

@@ -1,4 +1,5 @@
using Microsoft.Win32;
using SpineViewer.Utils;
using System;
using System.Collections.Generic;
using System.ComponentModel;
@@ -92,7 +93,7 @@ namespace SpineViewer.Dialogs
var properties = selectedObject.GetType().GetProperties();
var result = string.Join(Environment.NewLine, properties.Select(p => $"{p.Name}\t{p.GetValue(selectedObject)?.ToString()}"));
Clipboard.SetText(result);
MessageBox.Info("已复制");
MessagePopup.Info("已复制");
}
}
}

View File

@@ -47,7 +47,7 @@
panel1.Location = new Point(0, 0);
panel1.Name = "panel1";
panel1.Padding = new Padding(50, 15, 50, 10);
panel1.Size = new Size(710, 698);
panel1.Size = new Size(793, 754);
panel1.TabIndex = 2;
//
// tableLayoutPanel1
@@ -65,7 +65,7 @@
tableLayoutPanel1.RowStyles.Add(new RowStyle());
tableLayoutPanel1.RowStyles.Add(new RowStyle(SizeType.Absolute, 20F));
tableLayoutPanel1.RowStyles.Add(new RowStyle(SizeType.Absolute, 20F));
tableLayoutPanel1.Size = new Size(610, 673);
tableLayoutPanel1.Size = new Size(693, 729);
tableLayoutPanel1.TabIndex = 0;
//
// propertyGrid_ExportArgs
@@ -74,7 +74,7 @@
propertyGrid_ExportArgs.Location = new Point(3, 3);
propertyGrid_ExportArgs.Name = "propertyGrid_ExportArgs";
propertyGrid_ExportArgs.PropertySort = PropertySort.Categorized;
propertyGrid_ExportArgs.Size = new Size(604, 594);
propertyGrid_ExportArgs.Size = new Size(687, 650);
propertyGrid_ExportArgs.TabIndex = 1;
propertyGrid_ExportArgs.ToolbarVisible = false;
//
@@ -88,18 +88,18 @@
tableLayoutPanel2.Controls.Add(button_Ok, 0, 0);
tableLayoutPanel2.Controls.Add(button_Cancel, 1, 0);
tableLayoutPanel2.Dock = DockStyle.Bottom;
tableLayoutPanel2.Location = new Point(3, 630);
tableLayoutPanel2.Location = new Point(3, 686);
tableLayoutPanel2.Margin = new Padding(3, 30, 3, 3);
tableLayoutPanel2.Name = "tableLayoutPanel2";
tableLayoutPanel2.RowCount = 1;
tableLayoutPanel2.RowStyles.Add(new RowStyle());
tableLayoutPanel2.Size = new Size(604, 40);
tableLayoutPanel2.Size = new Size(687, 40);
tableLayoutPanel2.TabIndex = 10;
//
// button_Ok
//
button_Ok.Anchor = AnchorStyles.Bottom | AnchorStyles.Right;
button_Ok.Location = new Point(160, 3);
button_Ok.Location = new Point(201, 3);
button_Ok.Margin = new Padding(3, 3, 30, 3);
button_Ok.Name = "button_Ok";
button_Ok.Size = new Size(112, 34);
@@ -111,7 +111,7 @@
// button_Cancel
//
button_Cancel.Anchor = AnchorStyles.Bottom | AnchorStyles.Left;
button_Cancel.Location = new Point(332, 3);
button_Cancel.Location = new Point(373, 3);
button_Cancel.Margin = new Padding(30, 3, 3, 3);
button_Cancel.Name = "button_Cancel";
button_Cancel.Size = new Size(112, 34);
@@ -126,9 +126,8 @@
AutoScaleDimensions = new SizeF(11F, 24F);
AutoScaleMode = AutoScaleMode.Font;
CancelButton = button_Cancel;
ClientSize = new Size(710, 698);
ClientSize = new Size(793, 754);
Controls.Add(panel1);
FormBorderStyle = FormBorderStyle.FixedDialog;
Icon = (Icon)resources.GetObject("$this.Icon");
MaximizeBox = false;
MinimizeBox = false;

View File

@@ -1,84 +1,71 @@
using SpineViewer.Exporter;
using SpineViewer.Utils;
using System;
using System.Collections;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Diagnostics;
using System.Drawing;
using System.Drawing.Design;
using System.Drawing.Imaging;
using System.Linq;
using System.Reflection;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms;
using SpineViewer.Spine.SpineExporter;
namespace SpineViewer.Dialogs
{
public partial class ExportDialog: Form
public partial class ExportDialog : Form
{
/// <summary>
/// 要绑定的导出参数
/// </summary>
public required ExportArgs ExportArgs
{
get => propertyGrid_ExportArgs.SelectedObject as ExportArgs;
init
{
propertyGrid_ExportArgs.SelectedObject = value;
private readonly ExporterProperty wrapper;
#region XXX:
var categories = propertyGrid_ExportArgs.SelectedGridItem?.Parent?.Parent?.GridItems;
if (categories is null) return;
foreach (var category in categories)
{
// 查找 "导出" 分组
if (category == null) continue;
PropertyInfo? labelProp = category.GetType().GetProperty("Label", BindingFlags.Instance | BindingFlags.Public);
if (labelProp == null) continue;
string? label = labelProp.GetValue(category) as string;
if (label != "[0] 导出") continue;
// 获取该分组下的所有属性项
PropertyInfo? gridItemsProp = category.GetType().GetProperty("GridItems", BindingFlags.Instance | BindingFlags.Public);
if (gridItemsProp == null) continue;
var gridItemsObj = gridItemsProp.GetValue(category);
if (gridItemsObj is not IEnumerable gridItems) continue;
foreach (object item in gridItems)
{
if (item == null) continue;
PropertyInfo? propDescProp = item.GetType().GetProperty("PropertyDescriptor", BindingFlags.Instance | BindingFlags.Public);
if (propDescProp == null) continue;
var propDesc = propDescProp.GetValue(item) as PropertyDescriptor;
if (propDesc == null) continue;
if (propDesc.Name == "OutputDir")
{
if (item is GridItem gridItem)
propertyGrid_ExportArgs.SelectedGridItem = gridItem; // 找到后,将此项设为选中项
else
propertyGrid_ExportArgs.SelectedGridItem = (GridItem)item; // 如果转换失败,则尝试直接赋值
}
return; // 设置成功后退出
}
}
#endregion
}
}
public ExportDialog()
public ExportDialog(ExporterProperty wrapper)
{
InitializeComponent();
this.wrapper = wrapper;
propertyGrid_ExportArgs.SelectedObject = wrapper;
#region XXX:
var categories = propertyGrid_ExportArgs.SelectedGridItem?.Parent?.Parent?.GridItems;
if (categories is null) return;
foreach (var category in categories)
{
// 查找 "导出" 分组
if (category == null) continue;
PropertyInfo? labelProp = category.GetType().GetProperty("Label", BindingFlags.Instance | BindingFlags.Public);
if (labelProp == null) continue;
string? label = labelProp.GetValue(category) as string;
if (label != "[0] 导出") continue;
// 获取该分组下的所有属性项
PropertyInfo? gridItemsProp = category.GetType().GetProperty("GridItems", BindingFlags.Instance | BindingFlags.Public);
if (gridItemsProp == null) continue;
var gridItemsObj = gridItemsProp.GetValue(category);
if (gridItemsObj is not IEnumerable gridItems) continue;
foreach (object item in gridItems)
{
if (item == null) continue;
PropertyInfo? propDescProp = item.GetType().GetProperty("PropertyDescriptor", BindingFlags.Instance | BindingFlags.Public);
if (propDescProp == null) continue;
var propDesc = propDescProp.GetValue(item) as PropertyDescriptor;
if (propDesc == null) continue;
if (propDesc.Name == "OutputDir")
{
if (item is GridItem gridItem)
propertyGrid_ExportArgs.SelectedGridItem = gridItem; // 找到后,将此项设为选中项
else
propertyGrid_ExportArgs.SelectedGridItem = (GridItem)item; // 如果转换失败,则尝试直接赋值
}
return; // 设置成功后退出
}
}
#endregion
}
private void button_Ok_Click(object sender, EventArgs e)
{
if (ExportArgs.Validate() is string error)
{
MessageBox.Info(error, "参数错误");
if (wrapper.Exporter.Validate() is string error)
{
MessagePopup.Info(error, "参数错误");
return;
}
DialogResult = DialogResult.OK;

View File

@@ -1,4 +1,5 @@
using SpineViewer.Spine;
using SpineViewer.Utils;
using System;
using System.Collections.Generic;
using System.ComponentModel;
@@ -20,7 +21,7 @@ namespace SpineViewer.Dialogs
public OpenSpineDialog()
{
InitializeComponent();
comboBox_Version.DataSource = SpineHelper.Names.ToList();
comboBox_Version.DataSource = SpineUtils.Names.ToList();
comboBox_Version.DisplayMember = "Value";
comboBox_Version.ValueMember = "Key";
comboBox_Version.SelectedValue = SpineVersion.Auto;
@@ -57,7 +58,7 @@ namespace SpineViewer.Dialogs
if (!File.Exists(skelPath))
{
MessageBox.Info($"{skelPath}", "skel文件不存在");
MessagePopup.Info($"{skelPath}", "skel文件不存在");
return;
}
else
@@ -71,7 +72,7 @@ namespace SpineViewer.Dialogs
}
else if (!File.Exists(atlasPath))
{
MessageBox.Info($"{atlasPath}", "atlas文件不存在");
MessagePopup.Info($"{atlasPath}", "atlas文件不存在");
return;
}
else
@@ -79,9 +80,9 @@ namespace SpineViewer.Dialogs
atlasPath = Path.GetFullPath(atlasPath);
}
if (version != SpineVersion.Auto && !Spine.Spine.HasImplementation(version))
if (version != SpineVersion.Auto && !Spine.SpineObject.HasImplementation(version))
{
MessageBox.Info($"{version.GetName()} 版本尚未实现(咕咕咕~");
MessagePopup.Info($"{version.GetName()} 版本尚未实现(咕咕咕~");
return;
}

View File

@@ -1,4 +1,5 @@
using NLog;
using SpineViewer.Utils;
using System;
using System.Collections.Generic;
using System.ComponentModel;
@@ -13,7 +14,7 @@ namespace SpineViewer.Dialogs
{
public partial class ProgressDialog : Form
{
private Logger logger = LogManager.GetCurrentClassLogger();
private readonly Logger logger = LogManager.GetCurrentClassLogger();
public ProgressDialog()
{
@@ -51,7 +52,7 @@ namespace SpineViewer.Dialogs
if (e.Error != null)
{
logger.Error(e.Error.ToString());
MessageBox.Error(e.Error.ToString(), "执行出错");
MessagePopup.Error(e.Error.ToString(), "执行出错");
DialogResult = DialogResult.Abort;
}
else if (e.Cancelled)

View File

@@ -1,139 +0,0 @@
namespace SpineViewer.Dialogs
{
partial class SkinManagerEditorDialog
{
/// <summary>
/// Required designer variable.
/// </summary>
private System.ComponentModel.IContainer components = null;
/// <summary>
/// Clean up any resources being used.
/// </summary>
/// <param name="disposing">true if managed resources should be disposed; otherwise, false.</param>
protected override void Dispose(bool disposing)
{
if (disposing && (components != null))
{
components.Dispose();
}
base.Dispose(disposing);
}
#region Windows Form Designer generated code
/// <summary>
/// Required method for Designer support - do not modify
/// the contents of this method with the code editor.
/// </summary>
private void InitializeComponent()
{
panel = new Panel();
tableLayoutPanel1 = new TableLayoutPanel();
flowLayoutPanel1 = new FlowLayoutPanel();
button_Add = new Button();
button_Delete = new Button();
propertyGrid_SkinManager = new PropertyGrid();
panel.SuspendLayout();
tableLayoutPanel1.SuspendLayout();
flowLayoutPanel1.SuspendLayout();
SuspendLayout();
//
// panel
//
panel.Controls.Add(tableLayoutPanel1);
panel.Dock = DockStyle.Fill;
panel.Location = new Point(0, 0);
panel.Name = "panel";
panel.Padding = new Padding(50, 15, 50, 10);
panel.Size = new Size(666, 483);
panel.TabIndex = 0;
//
// tableLayoutPanel1
//
tableLayoutPanel1.ColumnCount = 1;
tableLayoutPanel1.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 100F));
tableLayoutPanel1.Controls.Add(flowLayoutPanel1, 0, 1);
tableLayoutPanel1.Controls.Add(propertyGrid_SkinManager, 0, 0);
tableLayoutPanel1.Dock = DockStyle.Fill;
tableLayoutPanel1.Location = new Point(50, 15);
tableLayoutPanel1.Name = "tableLayoutPanel1";
tableLayoutPanel1.RowCount = 2;
tableLayoutPanel1.RowStyles.Add(new RowStyle(SizeType.Percent, 100F));
tableLayoutPanel1.RowStyles.Add(new RowStyle());
tableLayoutPanel1.Size = new Size(566, 458);
tableLayoutPanel1.TabIndex = 0;
//
// flowLayoutPanel1
//
flowLayoutPanel1.AutoSize = true;
flowLayoutPanel1.Controls.Add(button_Add);
flowLayoutPanel1.Controls.Add(button_Delete);
flowLayoutPanel1.Dock = DockStyle.Fill;
flowLayoutPanel1.Location = new Point(3, 415);
flowLayoutPanel1.Name = "flowLayoutPanel1";
flowLayoutPanel1.Size = new Size(560, 40);
flowLayoutPanel1.TabIndex = 2;
//
// button_Add
//
button_Add.Location = new Point(3, 3);
button_Add.Name = "button_Add";
button_Add.Size = new Size(112, 34);
button_Add.TabIndex = 0;
button_Add.Text = "添加";
button_Add.UseVisualStyleBackColor = true;
button_Add.Click += button_Add_Click;
//
// button_Delete
//
button_Delete.Location = new Point(121, 3);
button_Delete.Name = "button_Delete";
button_Delete.Size = new Size(112, 34);
button_Delete.TabIndex = 1;
button_Delete.Text = "删除";
button_Delete.UseVisualStyleBackColor = true;
button_Delete.Click += button_Delete_Click;
//
// propertyGrid_SkinManager
//
propertyGrid_SkinManager.Dock = DockStyle.Fill;
propertyGrid_SkinManager.HelpVisible = false;
propertyGrid_SkinManager.Location = new Point(3, 3);
propertyGrid_SkinManager.Name = "propertyGrid_SkinManager";
propertyGrid_SkinManager.PropertySort = PropertySort.NoSort;
propertyGrid_SkinManager.Size = new Size(560, 406);
propertyGrid_SkinManager.TabIndex = 1;
propertyGrid_SkinManager.ToolbarVisible = false;
//
// SkinManagerEditorDialog
//
AutoScaleDimensions = new SizeF(11F, 24F);
AutoScaleMode = AutoScaleMode.Font;
ClientSize = new Size(666, 483);
Controls.Add(panel);
FormBorderStyle = FormBorderStyle.FixedDialog;
MaximizeBox = false;
MinimizeBox = false;
Name = "SkinManagerEditorDialog";
ShowIcon = false;
ShowInTaskbar = false;
StartPosition = FormStartPosition.CenterScreen;
Text = "皮肤列表实时编辑器";
panel.ResumeLayout(false);
tableLayoutPanel1.ResumeLayout(false);
tableLayoutPanel1.PerformLayout();
flowLayoutPanel1.ResumeLayout(false);
ResumeLayout(false);
}
#endregion
private Panel panel;
private TableLayoutPanel tableLayoutPanel1;
private FlowLayoutPanel flowLayoutPanel1;
private Button button_Add;
private Button button_Delete;
private PropertyGrid propertyGrid_SkinManager;
}
}

View File

@@ -1,45 +0,0 @@
using SpineViewer.Spine;
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms;
namespace SpineViewer.Dialogs
{
public partial class SkinManagerEditorDialog : Form
{
private readonly Spine.Spine spine;
public SkinManagerEditorDialog(Spine.Spine spine)
{
InitializeComponent();
this.spine = spine;
propertyGrid_SkinManager.SelectedObject = spine.SkinManager;
}
private void button_Add_Click(object sender, EventArgs e)
{
if (spine.SkinNames.Count <= 0)
{
MessageBox.Info($"{spine.Name} 没有可用的皮肤");
return;
}
spine.LoadSkin(spine.SkinNames[0]);
propertyGrid_SkinManager.Refresh();
}
private void button_Delete_Click(object sender, EventArgs e)
{
if (propertyGrid_SkinManager.SelectedGridItem?.Value is SkinWrapper sk)
spine.UnloadSkin(sk.Index);
propertyGrid_SkinManager.Refresh();
if (propertyGrid_SkinManager.SelectedGridItem?.Parent?.GridItems?.Cast<GridItem>().Last() is GridItem gt)
propertyGrid_SkinManager.SelectedGridItem = gt;
}
}
}

View File

@@ -1,118 +0,0 @@
using SpineViewer.Spine;
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Drawing.Design;
using System.Drawing.Imaging;
using System.Linq;
using System.Reflection;
using System.Text;
using System.Threading.Tasks;
using static System.Windows.Forms.VisualStyles.VisualStyleElement.StartPanel;
namespace SpineViewer.Exporter
{
/// <summary>
/// 导出参数基类
/// </summary>
public abstract class ExportArgs : ImplementationResolver<ExportArgs, ExportImplementationAttribute, ExportType>, IDisposable
{
/// <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)
=> New(exportType, [resolution, view, renderSelectedOnly]);
public ExportArgs(Size resolution, SFML.Graphics.View view, bool renderSelectedOnly)
{
Resolution = resolution;
View = view;
RenderSelectedOnly = renderSelectedOnly;
}
~ExportArgs() { Dispose(false); }
public void Dispose() { Dispose(true); GC.SuppressFinalize(this); }
protected virtual void Dispose(bool disposing) { View?.Dispose(); }
/// <summary>
/// 输出文件夹
/// </summary>
[Editor(typeof(FolderNameEditor), typeof(UITypeEditor))]
[Category("[0] "), DisplayName(""), Description("")]
public string? OutputDir { get; set; } = null;
/// <summary>
/// 导出单个
/// </summary>
[Category("[0] "), DisplayName(""), Description("")]
public bool ExportSingle { get; set; } = false;
/// <summary>
/// 画面分辨率
/// </summary>
[TypeConverter(typeof(SizeConverter))]
[Category("[0] "), DisplayName(""), Description("")]
public Size Resolution { get; }
/// <summary>
/// 渲染视窗
/// </summary>
[Category("[0] "), DisplayName(""), Description("")]
public SFML.Graphics.View View { get; }
/// <summary>
/// 是否仅渲染选中
/// </summary>
[Category("[0] "), DisplayName(""), Description("")]
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 => backgroundColor;
set
{
backgroundColor = value;
var bcPma = value;
var a = bcPma.A / 255f;
bcPma.R = (byte)(bcPma.R * a);
bcPma.G = (byte)(bcPma.G * a);
bcPma.B = (byte)(bcPma.B * a);
BackgroundColorPma = bcPma;
}
}
private SFML.Graphics.Color backgroundColor = SFML.Graphics.Color.Transparent;
/// <summary>
/// 预乘后的背景颜色
/// </summary>
[Browsable(false)]
public SFML.Graphics.Color BackgroundColorPma { get; private set; } = SFML.Graphics.Color.Transparent;
/// <summary>
/// 检查参数是否合法并规范化参数值, 否则返回用户错误原因
/// </summary>
public virtual string? Validate()
{
if (!string.IsNullOrWhiteSpace(OutputDir) && File.Exists(OutputDir))
return "输出文件夹无效";
if (!string.IsNullOrWhiteSpace(OutputDir) && !Directory.Exists(OutputDir))
return $"文件夹 {OutputDir} 不存在";
if (ExportSingle && string.IsNullOrWhiteSpace(OutputDir))
return "导出单个时必须提供输出文件夹";
OutputDir = string.IsNullOrWhiteSpace(OutputDir) ? null : Path.GetFullPath(OutputDir);
return null;
}
}
}

View File

@@ -1,129 +0,0 @@
using NLog;
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Reflection;
using System.Text;
using System.Threading.Tasks;
namespace SpineViewer.Exporter
{
/// <summary>
/// 导出器基类
/// </summary>
public abstract class Exporter(ExportArgs exportArgs) : ImplementationResolver<Exporter, ExportImplementationAttribute, ExportType>
{
/// <summary>
/// 创建指定类型导出器
/// </summary>
/// <param name="exportType">导出类型</param>
/// <param name="exportArgs">与 <paramref name="exportType"/> 匹配的导出参数</param>
/// <returns>与 <paramref name="exportType"/> 匹配的导出器</returns>
public static Exporter New(ExportType exportType, ExportArgs exportArgs) => New(exportType, [exportArgs]);
/// <summary>
/// 日志器
/// </summary>
protected Logger logger = LogManager.GetCurrentClassLogger();
/// <summary>
/// 导出参数
/// </summary>
public ExportArgs ExportArgs { get; } = exportArgs;
/// <summary>
/// 可用于文件名的时间戳字符串
/// </summary>
protected readonly string timestamp = DateTime.Now.ToString("yyMMddHHmmss");
/// <summary>
/// 获取供渲染的 SFML.Graphics.RenderTexture
/// </summary>
private SFML.Graphics.RenderTexture GetRenderTexture()
{
var tex = new SFML.Graphics.RenderTexture((uint)ExportArgs.Resolution.Width, (uint)ExportArgs.Resolution.Height);
tex.Clear(SFML.Graphics.Color.Transparent);
tex.SetView(ExportArgs.View);
return tex;
}
/// <summary>
/// 获取单个模型的单帧画面
/// </summary>
protected SFMLImageVideoFrame GetFrame(Spine.Spine spine) => GetFrame([spine]);
/// <summary>
/// 获取模型列表的单帧画面
/// </summary>
protected SFMLImageVideoFrame GetFrame(Spine.Spine[] spinesToRender)
{
// RenderTexture 必须临时创建, 随用随取, 防止出现跨线程的情况
using var texPma = GetRenderTexture();
// 先将预乘结果准确绘制出来, 注意背景色也应当是预乘的
texPma.Clear(ExportArgs.BackgroundColorPma);
foreach (var spine in spinesToRender) texPma.Draw(spine);
texPma.Display();
// 背景色透明度不为 1 时需要处理反预乘, 否则直接就是结果
if (ExportArgs.BackgroundColor.A < 255)
{
// 从预乘结果构造渲染对象, 并正确设置变换
using var view = texPma.GetView();
using var img = texPma.Texture.CopyToImage();
using var texSprite = new SFML.Graphics.Texture(img);
using var sp = new SFML.Graphics.Sprite(texSprite)
{
Origin = new(texPma.Size.X / 2f, texPma.Size.Y / 2f),
Position = new(view.Center.X, view.Center.Y),
Scale = new(view.Size.X / texPma.Size.X, view.Size.Y / texPma.Size.Y),
Rotation = view.Rotation
};
// 混合模式用直接覆盖的方式, 保证得到的图像区域是反预乘的颜色和透明度, 同时使用反预乘着色器
var st = SFML.Graphics.RenderStates.Default;
st.BlendMode = new(SFML.Graphics.BlendMode.Factor.One, SFML.Graphics.BlendMode.Factor.Zero); // 用源的颜色和透明度直接覆盖
st.Shader = Shader.InversePma;
// 在最终结果上二次渲染非预乘画面
using var tex = GetRenderTexture();
// 将非预乘结果覆盖式绘制在目标对象上, 注意背景色应该用非预乘的
tex.Clear(ExportArgs.BackgroundColor);
tex.Draw(sp, st);
tex.Display();
return new(tex.Texture.CopyToImage());
}
else
{
return new(texPma.Texture.CopyToImage());
}
}
/// <summary>
/// 每个模型在同一个画面进行导出
/// </summary>
protected abstract void ExportSingle(Spine.Spine[] spinesToRender, BackgroundWorker? worker = null);
/// <summary>
/// 每个模型独立导出
/// </summary>
protected abstract void ExportIndividual(Spine.Spine[] spinesToRender, BackgroundWorker? worker = null);
/// <summary>
/// 执行导出
/// </summary>
/// <param name="spines">要进行导出的 Spine 列表</param>
/// <param name="worker">用来执行该函数的 worker</param>
public virtual void Export(Spine.Spine[] spines, BackgroundWorker? worker = null)
{
var spinesToRender = spines.Where(sp => !ExportArgs.RenderSelectedOnly || sp.IsSelected).Reverse().ToArray();
if (ExportArgs.ExportSingle) ExportSingle(spinesToRender, worker);
else ExportIndividual(spinesToRender, worker);
logger.LogCurrentProcessMemoryUsage();
}
}
}

View File

@@ -1,36 +0,0 @@
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>
/// FFmpeg 自定义视频导出参数
/// </summary>
[ExportImplementation(ExportType.Custom)]
public class CustomExportArgs : FFmpegVideoExportArgs
{
public CustomExportArgs(Size resolution, SFML.Graphics.View view, bool renderSelectedOnly) : base(resolution, view, renderSelectedOnly) { }
public override string Format => CustomFormat;
public override string Suffix => CustomSuffix;
public override string FileNameNoteSuffix => string.Empty;
/// <summary>
/// 文件格式
/// </summary>
[Category("[3] "), DisplayName(""), Description("")]
public string CustomFormat { get; set; } = "mp4";
/// <summary>
/// 文件名后缀
/// </summary>
[Category("[3] "), DisplayName(""), Description("")]
public string CustomSuffix { get; set; } = ".mp4";
}
}

View File

@@ -1,59 +0,0 @@
using FFMpegCore.Enums;
using FFMpegCore;
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>
/// 使用 FFmpeg 视频导出参数
/// </summary>
public abstract class FFmpegVideoExportArgs : VideoExportArgs
{
public FFmpegVideoExportArgs(Size resolution, SFML.Graphics.View view, bool renderSelectedOnly) : base(resolution, view, renderSelectedOnly) { }
/// <summary>
/// 文件格式
/// </summary>
[Category("[2] FFmpeg "), DisplayName(""), Description("-f, ")]
public abstract string Format { get; }
/// <summary>
/// 文件名后缀
/// </summary>
[Category("[2] FFmpeg "), DisplayName(""), Description("")]
public abstract string Suffix { get; }
/// <summary>
/// 文件名后缀
/// </summary>
[Category("[2] FFmpeg "), DisplayName(""), Description(" FFmpeg , , ")]
public string CustomArgument { get; set; }
/// <summary>
/// 获取输出附加选项
/// </summary>
public virtual void SetOutputOptions(FFMpegArgumentOptions options) => options.ForceFormat(Format).WithCustomArgument(CustomArgument);
/// <summary>
/// 要追加在文件名末尾的信息字串, 首尾不需要提供额外分隔符
/// </summary>
[Browsable(false)]
public abstract string FileNameNoteSuffix { get; }
public override string? Validate()
{
if (base.Validate() is string error)
return error;
if (string.IsNullOrWhiteSpace(Format))
return "需要提供有效的格式";
if (string.IsNullOrWhiteSpace(Suffix))
return "需要提供有效的文件名后缀";
return null;
}
}
}

View File

@@ -1,58 +0,0 @@
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Drawing.Imaging;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace SpineViewer.Exporter.Implementations.ExportArgs
{
/// <summary>
/// 单帧画面导出参数
/// </summary>
[ExportImplementation(ExportType.Frame)]
public class FrameExportArgs : SpineViewer.Exporter.ExportArgs
{
public FrameExportArgs(Size resolution, SFML.Graphics.View view, bool renderSelectedOnly) : base(resolution, view, renderSelectedOnly) { }
/// <summary>
/// 单帧画面格式
/// </summary>
[TypeConverter(typeof(ImageFormatConverter))]
[Category("[1] "), DisplayName("")]
public ImageFormat ImageFormat
{
get => imageFormat;
set
{
if (value == ImageFormat.MemoryBmp) value = ImageFormat.Bmp;
imageFormat = value;
}
}
private ImageFormat imageFormat = ImageFormat.Png;
/// <summary>
/// 文件名后缀
/// </summary>
[Category("[1] "), DisplayName(""), Description("")]
public string Suffix { get => imageFormat.GetSuffix(); }
/// <summary>
/// DPI
/// </summary>
[TypeConverter(typeof(SizeFConverter))]
[Category("[1] "), DisplayName("DPI"), Description("")]
public SizeF DPI
{
get => dpi;
set
{
if (value.Width <= 0) value.Width = 144;
if (value.Height <= 0) value.Height = 144;
dpi = value;
}
}
private SizeF dpi = new(144, 144);
}
}

View File

@@ -1,26 +0,0 @@
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Drawing.Imaging;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace SpineViewer.Exporter.Implementations.ExportArgs
{
/// <summary>
/// 帧序列导出参数
/// </summary>
[ExportImplementation(ExportType.FrameSequence)]
public class FrameSequenceExportArgs : VideoExportArgs
{
public FrameSequenceExportArgs(Size resolution, SFML.Graphics.View view, bool renderSelectedOnly) : base(resolution, view, renderSelectedOnly) { }
/// <summary>
/// 文件名后缀
/// </summary>
[TypeConverter(typeof(StringEnumConverter)), StringEnumConverter.StandardValues(".png", ".jpg", ".tga", ".bmp")]
[Category("[2] "), DisplayName(""), Description("")]
public string Suffix { get; set; } = ".png";
}
}

View File

@@ -1,43 +0,0 @@
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>
/// 视频导出参数基类
/// </summary>
public abstract class VideoExportArgs : SpineViewer.Exporter.ExportArgs
{
public VideoExportArgs(Size resolution, SFML.Graphics.View view, bool renderSelectedOnly) : base(resolution, view, renderSelectedOnly) { }
/// <summary>
/// 导出时长
/// </summary>
[Category("[1] "), DisplayName(""), Description(", 0, 使")]
public float Duration
{
get => duration;
set => duration = value < 0 ? -1 : value;
}
private float duration = -1;
/// <summary>
/// 帧率
/// </summary>
[Category("[1] "), DisplayName(""), Description("")]
public float FPS { get; set; } = 60;
public override string? Validate()
{
if (base.Validate() is string error)
return error;
if (ExportSingle && Duration < 0)
return "导出单个时导出时长不能为负数";
return null;
}
}
}

View File

@@ -1,86 +0,0 @@
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 System.Diagnostics;
namespace SpineViewer.Exporter.Implementations.Exporter
{
/// <summary>
/// 使用 FFmpeg 的视频导出器
/// </summary>
[ExportImplementation(ExportType.Gif)]
[ExportImplementation(ExportType.Mp4)]
[ExportImplementation(ExportType.Webm)]
[ExportImplementation(ExportType.Mkv)]
[ExportImplementation(ExportType.Mov)]
[ExportImplementation(ExportType.Custom)]
public class FFmpegVideoExporter : VideoExporter
{
public FFmpegVideoExporter(FFmpegVideoExportArgs exportArgs) : base(exportArgs) { }
protected override void ExportSingle(Spine.Spine[] spinesToRender, BackgroundWorker? worker = null)
{
var args = (FFmpegVideoExportArgs)ExportArgs;
var noteSuffix = args.FileNameNoteSuffix;
if (!string.IsNullOrWhiteSpace(noteSuffix)) noteSuffix = $"_{noteSuffix}";
var filename = $"{timestamp}_{args.FPS:f0}{noteSuffix}{args.Suffix}";
// 导出单个时必定提供输出文件夹
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, args.SetOutputOptions);
logger.Info("FFmpeg arguments: {}", ffmpegArgs.Arguments);
ffmpegArgs.ProcessSynchronously();
}
catch (Exception ex)
{
logger.Error(ex.ToString());
logger.Error("Failed to export {} {}", args.Format, savePath);
}
}
protected override void ExportIndividual(Spine.Spine[] spinesToRender, BackgroundWorker? worker = null)
{
var args = (FFmpegVideoExportArgs)ExportArgs;
var noteSuffix = args.FileNameNoteSuffix;
if (!string.IsNullOrWhiteSpace(noteSuffix)) noteSuffix = $"_{noteSuffix}";
foreach (var spine in spinesToRender)
{
if (worker?.CancellationPending == true) break; // 取消的日志在 GetFrames 里输出
var filename = $"{spine.Name}_{timestamp}_{args.FPS:f0}{noteSuffix}{args.Suffix}";
// 如果提供了输出文件夹, 则全部导出到输出文件夹, 否则导出到各自的文件夹下
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, args.SetOutputOptions);
logger.Info("FFmpeg arguments: {}", ffmpegArgs.Arguments);
ffmpegArgs.ProcessSynchronously();
}
catch (Exception ex)
{
logger.Error(ex.ToString());
logger.Error("Failed to export {} {} {}", args.Format, savePath, spine.SkelPath);
}
}
}
}
}

View File

@@ -1,86 +0,0 @@
using SpineViewer.Exporter.Implementations.ExportArgs;
using SpineViewer.Spine;
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace SpineViewer.Exporter.Implementations.Exporter
{
/// <summary>
/// 单帧画面导出器
/// </summary>
[ExportImplementation(ExportType.Frame)]
public class FrameExporter : SpineViewer.Exporter.Exporter
{
public FrameExporter(FrameExportArgs exportArgs) : base(exportArgs) { }
protected override void ExportSingle(Spine.Spine[] spinesToRender, BackgroundWorker? worker = null)
{
var args = (FrameExportArgs)ExportArgs;
// 导出单个时必定提供输出文件夹
var filename = $"frame_{timestamp}{args.Suffix}";
var savePath = Path.Combine(args.OutputDir, filename);
worker?.ReportProgress(0, $"已处理 0/1");
try
{
using var frame = GetFrame(spinesToRender);
using var img = frame.CopyToBitmap();
img.SetResolution(args.DPI.Width, args.DPI.Height);
img.Save(savePath, args.ImageFormat);
}
catch (Exception ex)
{
logger.Error(ex.ToString());
logger.Error("Failed to save single frame");
}
worker?.ReportProgress(100, $"已处理 1/1");
}
protected override void ExportIndividual(Spine.Spine[] spinesToRender, BackgroundWorker? worker = null)
{
var args = (FrameExportArgs)ExportArgs;
int total = spinesToRender.Length;
int success = 0;
int error = 0;
worker?.ReportProgress(0, $"已处理 0/{total}");
for (int i = 0; i < total; i++)
{
var spine = spinesToRender[i];
// 逐个导出时如果提供了输出文件夹, 则全部导出到输出文件夹, 否则输出到各自的文件夹
var filename = $"{spine.Name}_{timestamp}{args.Suffix}";
var savePath = args.OutputDir is null ? Path.Combine(spine.AssetsDir, filename) : Path.Combine(args.OutputDir, filename);
try
{
using var frame = GetFrame(spine);
using var img = frame.CopyToBitmap();
img.SetResolution(args.DPI.Width, args.DPI.Height);
img.Save(savePath, args.ImageFormat);
success++;
}
catch (Exception ex)
{
logger.Error(ex.ToString());
logger.Error("Failed to save frame {} {}", savePath, spine.SkelPath);
error++;
}
worker?.ReportProgress((int)((i + 1) * 100.0) / total, $"已处理 {i + 1}/{total}");
}
if (error > 0)
logger.Warn("Frames save {} successfully, {} failed", success, error);
else
logger.Info("{} frames saved successfully", success);
}
}
}

View File

@@ -1,82 +0,0 @@
using SpineViewer.Exporter.Implementations.ExportArgs;
using SpineViewer.Spine;
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace SpineViewer.Exporter.Implementations.Exporter
{
/// <summary>
/// 视频导出基类
/// </summary>
public abstract class VideoExporter : SpineViewer.Exporter.Exporter
{
public VideoExporter(VideoExportArgs exportArgs) : base(exportArgs) { }
/// <summary>
/// 生成单个模型的帧序列
/// </summary>
protected IEnumerable<SFMLImageVideoFrame> GetFrames(Spine.Spine spine, BackgroundWorker? worker = null)
{
var args = (VideoExportArgs)ExportArgs;
// 独立导出时如果 args.Duration 小于 0 则使用所有轨道上动画时长最大值
var duration = args.Duration;
if (duration < 0) duration = spine.GetTrackIndices().Select(i => spine.GetAnimationDuration(spine.GetAnimation(i))).Max();
float delta = 1f / args.FPS;
int total = Math.Max(1, (int)(duration * args.FPS)); // 至少导出 1 帧
worker?.ReportProgress(0, $"{spine.Name} 已处理 0/{total} 帧");
for (int i = 0; i < total; i++)
{
if (worker?.CancellationPending == true)
{
logger.Info("Export cancelled");
break;
}
var frame = GetFrame(spine);
spine.Update(delta);
worker?.ReportProgress((int)((i + 1) * 100.0) / total, $"{spine.Name} 已处理 {i + 1}/{total} 帧");
yield return frame;
}
}
/// <summary>
/// 生成多个模型的帧序列
/// </summary>
protected IEnumerable<SFMLImageVideoFrame> GetFrames(Spine.Spine[] spinesToRender, BackgroundWorker? worker = null)
{
// 导出单个时必须根据 args.Duration 决定导出时长
var args = (VideoExportArgs)ExportArgs;
float delta = 1f / args.FPS;
int total = Math.Max(1, (int)(args.Duration * args.FPS)); // 至少导出 1 帧
worker?.ReportProgress(0, $"已处理 0/{total} 帧");
for (int i = 0; i < total; i++)
{
if (worker?.CancellationPending == true)
{
logger.Info("Export cancelled");
break;
}
var frame = GetFrame(spinesToRender);
foreach (var spine in spinesToRender) spine.Update(delta);
worker?.ReportProgress((int)((i + 1) * 100.0) / total, $"已处理 {i + 1}/{total} 帧");
yield return frame;
}
}
public override void Export(Spine.Spine[] spines, BackgroundWorker? worker = null)
{
// 导出视频格式需要把模型时间都重置到 0
foreach (var spine in spines) spine.ResetAnimationsTime();
base.Export(spines, worker);
}
}
}

View File

@@ -1,89 +0,0 @@
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace SpineViewer.Exporter
{
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

@@ -5,7 +5,7 @@ using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace SpineViewer
namespace SpineViewer.Extensions
{
public static class NLogExtension
{

View File

@@ -4,7 +4,7 @@ using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace SpineViewer
namespace SpineViewer.Extensions
{
public static class SFMLExtension
{
@@ -25,7 +25,7 @@ namespace SpineViewer
public static Bitmap CopyToBitmap(this SFML.Graphics.Texture texture)
{
using var image = texture.CopyToImage();
return CopyToBitmap(image);
return image.CopyToBitmap();
}
/// <summary>

50
SpineViewer/Forms/PetForm.Designer.cs generated Normal file
View File

@@ -0,0 +1,50 @@
namespace SpineViewer
{
partial class PetForm
{
/// <summary>
/// Required designer variable.
/// </summary>
private System.ComponentModel.IContainer components = null;
/// <summary>
/// Clean up any resources being used.
/// </summary>
/// <param name="disposing">true if managed resources should be disposed; otherwise, false.</param>
protected override void Dispose(bool disposing)
{
if (disposing && (components != null))
{
components.Dispose();
}
base.Dispose(disposing);
}
#region Windows Form Designer generated code
/// <summary>
/// Required method for Designer support - do not modify
/// the contents of this method with the code editor.
/// </summary>
private void InitializeComponent()
{
SuspendLayout();
//
// PetForm
//
AutoScaleMode = AutoScaleMode.None;
ClientSize = new Size(490, 456);
ControlBox = false;
MaximizeBox = false;
MinimizeBox = false;
Name = "PetForm";
ShowIcon = false;
ShowInTaskbar = false;
StartPosition = FormStartPosition.Manual;
Text = "PetForm";
ResumeLayout(false);
}
#endregion
}
}

View File

@@ -0,0 +1,49 @@
using SpineViewer.Natives;
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms;
namespace SpineViewer
{
public partial class PetForm: Form
{
public PetForm()
{
InitializeComponent();
}
protected override CreateParams CreateParams
{
get
{
//var style = Win32.GetWindowLong(hWnd, Win32.GWL_STYLE) | Win32.WS_POPUP;
//var exStyle = Win32.GetWindowLong(hWnd, Win32.GWL_EXSTYLE) | Win32.WS_EX_LAYERED | Win32.WS_EX_TOOLWINDOW | Win32.WS_EX_TOPMOST;
//Win32.SetWindowLong(hWnd, Win32.GWL_STYLE, style);
//Win32.SetWindowLong(hWnd, Win32.GWL_EXSTYLE, exStyle);
//Win32.SetLayeredWindowAttributes(hWnd, crKey, 255, Win32.LWA_COLORKEY | Win32.LWA_ALPHA);
//Win32.SetWindowPos(hWnd, Win32.HWND_TOPMOST, 0, 0, 0, 0, Win32.SWP_NOMOVE | Win32.SWP_NOSIZE);
var cp = base.CreateParams;
cp.ExStyle = Win32.WS_EX_LAYERED | Win32.WS_EX_TOPMOST;
cp.Style = Win32.WS_POPUP;
//cp.ExStyle |= Win32.WS_EX_LAYERED | Win32.WS_EX_TOOLWINDOW | Win32.WS_EX_TOPMOST;
return cp;
}
}
protected override void OnPaint(PaintEventArgs e)
{
;
}
protected override void OnPaintBackground(PaintEventArgs e)
{
;
}
}
}

View File

@@ -1,6 +1,6 @@
namespace SpineViewer
{
partial class MainForm
partial class SpineViewerForm
{
/// <summary>
/// Required designer variable.
@@ -29,7 +29,7 @@
private void InitializeComponent()
{
components = new System.ComponentModel.Container();
System.ComponentModel.ComponentResourceManager resources = new System.ComponentModel.ComponentResourceManager(typeof(MainForm));
System.ComponentModel.ComponentResourceManager resources = new System.ComponentModel.ComponentResourceManager(typeof(SpineViewerForm));
menuStrip = new MenuStrip();
toolStripMenuItem_File = new ToolStripMenuItem();
toolStripMenuItem_Open = new ToolStripMenuItem();
@@ -38,11 +38,16 @@
toolStripMenuItem_Export = new ToolStripMenuItem();
toolStripMenuItem_ExportFrame = new ToolStripMenuItem();
toolStripMenuItem_ExportFrameSequence = new ToolStripMenuItem();
toolStripSeparator4 = new ToolStripSeparator();
toolStripMenuItem_ExportGif = new ToolStripMenuItem();
toolStripMenuItem_ExportWebp = new ToolStripMenuItem();
toolStripMenuItem_ExportAvif = new ToolStripMenuItem();
toolStripSeparator5 = new ToolStripSeparator();
toolStripMenuItem_ExportMp4 = new ToolStripMenuItem();
toolStripMenuItem_ExportWebm = new ToolStripMenuItem();
toolStripMenuItem_ExportMkv = new ToolStripMenuItem();
toolStripMenuItem_ExportMov = new ToolStripMenuItem();
toolStripSeparator6 = new ToolStripSeparator();
toolStripMenuItem_ExportCustom = new ToolStripMenuItem();
toolStripSeparator2 = new ToolStripSeparator();
toolStripMenuItem_Exit = new ToolStripMenuItem();
@@ -60,13 +65,13 @@
splitContainer_Information = new SplitContainer();
groupBox_SkelList = new GroupBox();
spineListView = new SpineViewer.Controls.SpineListView();
propertyGrid_Spine = new PropertyGrid();
spineViewPropertyGrid = new SpineViewer.Controls.SpineViewPropertyGrid();
splitContainer_Config = new SplitContainer();
groupBox_PreviewConfig = new GroupBox();
propertyGrid_Previewer = new PropertyGrid();
groupBox_SkelConfig = new GroupBox();
groupBox_Preview = new GroupBox();
spinePreviewer = new SpineViewer.Controls.SpinePreviewer();
spinePreviewPanel = new SpineViewer.Controls.SpinePreviewPanel();
panel_MainForm = new Panel();
toolTip = new ToolTip(components);
menuStrip.SuspendLayout();
@@ -133,7 +138,7 @@
//
// toolStripMenuItem_Export
//
toolStripMenuItem_Export.DropDownItems.AddRange(new ToolStripItem[] { toolStripMenuItem_ExportFrame, toolStripMenuItem_ExportFrameSequence, toolStripMenuItem_ExportGif, toolStripMenuItem_ExportMp4, toolStripMenuItem_ExportWebm, toolStripMenuItem_ExportMkv, toolStripMenuItem_ExportMov, toolStripMenuItem_ExportCustom });
toolStripMenuItem_Export.DropDownItems.AddRange(new ToolStripItem[] { toolStripMenuItem_ExportFrame, toolStripMenuItem_ExportFrameSequence, toolStripSeparator4, toolStripMenuItem_ExportGif, toolStripMenuItem_ExportWebp, toolStripMenuItem_ExportAvif, toolStripSeparator5, toolStripMenuItem_ExportMp4, toolStripMenuItem_ExportWebm, toolStripMenuItem_ExportMkv, toolStripMenuItem_ExportMov, toolStripSeparator6, toolStripMenuItem_ExportCustom });
toolStripMenuItem_Export.Name = "toolStripMenuItem_Export";
toolStripMenuItem_Export.Size = new Size(254, 34);
toolStripMenuItem_Export.Text = "导出(&E)";
@@ -143,56 +148,85 @@
toolStripMenuItem_ExportFrame.Name = "toolStripMenuItem_ExportFrame";
toolStripMenuItem_ExportFrame.Size = new Size(288, 34);
toolStripMenuItem_ExportFrame.Text = "单帧画面...";
toolStripMenuItem_ExportFrame.Click += toolStripMenuItem_Export_Click;
toolStripMenuItem_ExportFrame.Click += toolStripMenuItem_ExportFrame_Click;
//
// toolStripMenuItem_ExportFrameSequence
//
toolStripMenuItem_ExportFrameSequence.Name = "toolStripMenuItem_ExportFrameSequence";
toolStripMenuItem_ExportFrameSequence.Size = new Size(288, 34);
toolStripMenuItem_ExportFrameSequence.Text = "帧序列...";
toolStripMenuItem_ExportFrameSequence.Click += toolStripMenuItem_Export_Click;
toolStripMenuItem_ExportFrameSequence.Click += toolStripMenuItem_ExportFrameSequence_Click;
//
// toolStripSeparator4
//
toolStripSeparator4.Name = "toolStripSeparator4";
toolStripSeparator4.Size = new Size(285, 6);
//
// toolStripMenuItem_ExportGif
//
toolStripMenuItem_ExportGif.Name = "toolStripMenuItem_ExportGif";
toolStripMenuItem_ExportGif.Size = new Size(288, 34);
toolStripMenuItem_ExportGif.Text = "GIF...";
toolStripMenuItem_ExportGif.Click += toolStripMenuItem_Export_Click;
toolStripMenuItem_ExportGif.Click += toolStripMenuItem_ExportGif_Click;
//
// toolStripMenuItem_ExportWebp
//
toolStripMenuItem_ExportWebp.Name = "toolStripMenuItem_ExportWebp";
toolStripMenuItem_ExportWebp.Size = new Size(288, 34);
toolStripMenuItem_ExportWebp.Text = "WebP...";
toolStripMenuItem_ExportWebp.Click += toolStripMenuItem_ExportWebp_Click;
//
// toolStripMenuItem_ExportAvif
//
toolStripMenuItem_ExportAvif.Name = "toolStripMenuItem_ExportAvif";
toolStripMenuItem_ExportAvif.Size = new Size(288, 34);
toolStripMenuItem_ExportAvif.Text = "AVIF...";
toolStripMenuItem_ExportAvif.Click += toolStripMenuItem_ExportAvif_Click;
//
// toolStripSeparator5
//
toolStripSeparator5.Name = "toolStripSeparator5";
toolStripSeparator5.Size = new Size(285, 6);
//
// toolStripMenuItem_ExportMp4
//
toolStripMenuItem_ExportMp4.Name = "toolStripMenuItem_ExportMp4";
toolStripMenuItem_ExportMp4.Size = new Size(288, 34);
toolStripMenuItem_ExportMp4.Text = "MP4...";
toolStripMenuItem_ExportMp4.Click += toolStripMenuItem_Export_Click;
toolStripMenuItem_ExportMp4.Click += toolStripMenuItem_ExportMp4_Click;
//
// toolStripMenuItem_ExportWebm
//
toolStripMenuItem_ExportWebm.Name = "toolStripMenuItem_ExportWebm";
toolStripMenuItem_ExportWebm.Size = new Size(288, 34);
toolStripMenuItem_ExportWebm.Text = "WebM...";
toolStripMenuItem_ExportWebm.Click += toolStripMenuItem_Export_Click;
toolStripMenuItem_ExportWebm.Click += toolStripMenuItem_ExportWebm_Click;
//
// toolStripMenuItem_ExportMkv
//
toolStripMenuItem_ExportMkv.Name = "toolStripMenuItem_ExportMkv";
toolStripMenuItem_ExportMkv.Size = new Size(288, 34);
toolStripMenuItem_ExportMkv.Text = "MKV...";
toolStripMenuItem_ExportMkv.Click += toolStripMenuItem_Export_Click;
toolStripMenuItem_ExportMkv.Click += toolStripMenuItem_ExportMkv_Click;
//
// toolStripMenuItem_ExportMov
//
toolStripMenuItem_ExportMov.Name = "toolStripMenuItem_ExportMov";
toolStripMenuItem_ExportMov.Size = new Size(288, 34);
toolStripMenuItem_ExportMov.Text = "MOV...";
toolStripMenuItem_ExportMov.Click += toolStripMenuItem_Export_Click;
toolStripMenuItem_ExportMov.Click += toolStripMenuItem_ExportMov_Click;
//
// toolStripSeparator6
//
toolStripSeparator6.Name = "toolStripSeparator6";
toolStripSeparator6.Size = new Size(285, 6);
//
// toolStripMenuItem_ExportCustom
//
toolStripMenuItem_ExportCustom.Name = "toolStripMenuItem_ExportCustom";
toolStripMenuItem_ExportCustom.Size = new Size(288, 34);
toolStripMenuItem_ExportCustom.Text = "FFmpeg 自定义导出...";
toolStripMenuItem_ExportCustom.Click += toolStripMenuItem_Export_Click;
toolStripMenuItem_ExportCustom.Click += toolStripMenuItem_ExportCustom_Click;
//
// toolStripSeparator2
//
@@ -271,7 +305,7 @@
rtbLog.Margin = new Padding(3, 2, 3, 2);
rtbLog.Name = "rtbLog";
rtbLog.ReadOnly = true;
rtbLog.Size = new Size(1758, 120);
rtbLog.Size = new Size(1758, 142);
rtbLog.TabIndex = 0;
rtbLog.Text = "";
rtbLog.WordWrap = false;
@@ -295,7 +329,7 @@
splitContainer_MainForm.Panel2.Controls.Add(rtbLog);
splitContainer_MainForm.Panel2.Cursor = Cursors.Default;
splitContainer_MainForm.Size = new Size(1758, 1097);
splitContainer_MainForm.SplitterDistance = 969;
splitContainer_MainForm.SplitterDistance = 947;
splitContainer_MainForm.SplitterWidth = 8;
splitContainer_MainForm.TabIndex = 3;
splitContainer_MainForm.TabStop = false;
@@ -319,8 +353,8 @@
//
splitContainer_Functional.Panel2.Controls.Add(groupBox_Preview);
splitContainer_Functional.Panel2.Cursor = Cursors.Default;
splitContainer_Functional.Size = new Size(1758, 969);
splitContainer_Functional.SplitterDistance = 759;
splitContainer_Functional.Size = new Size(1758, 947);
splitContainer_Functional.SplitterDistance = 788;
splitContainer_Functional.SplitterWidth = 8;
splitContainer_Functional.TabIndex = 2;
splitContainer_Functional.TabStop = false;
@@ -343,8 +377,8 @@
//
splitContainer_Information.Panel2.Controls.Add(splitContainer_Config);
splitContainer_Information.Panel2.Cursor = Cursors.Default;
splitContainer_Information.Size = new Size(759, 969);
splitContainer_Information.SplitterDistance = 354;
splitContainer_Information.Size = new Size(788, 947);
splitContainer_Information.SplitterDistance = 351;
splitContainer_Information.SplitterWidth = 8;
splitContainer_Information.TabIndex = 1;
splitContainer_Information.TabStop = false;
@@ -357,7 +391,7 @@
groupBox_SkelList.Dock = DockStyle.Fill;
groupBox_SkelList.Location = new Point(0, 0);
groupBox_SkelList.Name = "groupBox_SkelList";
groupBox_SkelList.Size = new Size(354, 969);
groupBox_SkelList.Size = new Size(351, 947);
groupBox_SkelList.TabIndex = 0;
groupBox_SkelList.TabStop = false;
groupBox_SkelList.Text = "模型列表";
@@ -367,26 +401,21 @@
spineListView.Dock = DockStyle.Fill;
spineListView.Location = new Point(3, 26);
spineListView.Name = "spineListView";
spineListView.PropertyGrid = propertyGrid_Spine;
spineListView.Size = new Size(348, 940);
spineListView.Size = new Size(345, 918);
spineListView.SpinePropertyGrid = spineViewPropertyGrid;
spineListView.TabIndex = 0;
//
// propertyGrid_Spine
// spinePropertyGrid
//
propertyGrid_Spine.Dock = DockStyle.Fill;
propertyGrid_Spine.HelpVisible = false;
propertyGrid_Spine.Location = new Point(3, 26);
propertyGrid_Spine.Name = "propertyGrid_Spine";
propertyGrid_Spine.Size = new Size(391, 606);
propertyGrid_Spine.TabIndex = 0;
propertyGrid_Spine.ToolbarVisible = false;
propertyGrid_Spine.PropertyValueChanged += propertyGrid_PropertyValueChanged;
spineViewPropertyGrid.Dock = DockStyle.Fill;
spineViewPropertyGrid.Location = new Point(3, 26);
spineViewPropertyGrid.Name = "spinePropertyGrid";
spineViewPropertyGrid.Size = new Size(423, 586);
spineViewPropertyGrid.TabIndex = 0;
//
// splitContainer_Config
//
splitContainer_Config.Cursor = Cursors.SizeNS;
splitContainer_Config.Dock = DockStyle.Fill;
splitContainer_Config.FixedPanel = FixedPanel.Panel1;
splitContainer_Config.Location = new Point(0, 0);
splitContainer_Config.Name = "splitContainer_Config";
splitContainer_Config.Orientation = Orientation.Horizontal;
@@ -394,27 +423,23 @@
// splitContainer_Config.Panel1
//
splitContainer_Config.Panel1.Controls.Add(groupBox_PreviewConfig);
splitContainer_Config.Panel1.Cursor = Cursors.Default;
//
// splitContainer_Config.Panel2
//
splitContainer_Config.Panel2.Controls.Add(groupBox_SkelConfig);
splitContainer_Config.Panel2.Cursor = Cursors.Default;
splitContainer_Config.Size = new Size(397, 969);
splitContainer_Config.SplitterDistance = 326;
splitContainer_Config.Size = new Size(429, 947);
splitContainer_Config.SplitterDistance = 324;
splitContainer_Config.SplitterWidth = 8;
splitContainer_Config.TabIndex = 0;
splitContainer_Config.TabStop = false;
splitContainer_Config.SplitterMoved += splitContainer_SplitterMoved;
splitContainer_Config.MouseUp += splitContainer_MouseUp;
//
// groupBox_PreviewConfig
//
groupBox_PreviewConfig.Controls.Add(propertyGrid_Previewer);
groupBox_PreviewConfig.Dock = DockStyle.Fill;
groupBox_PreviewConfig.Location = new Point(0, 0);
groupBox_PreviewConfig.Margin = new Padding(0);
groupBox_PreviewConfig.Name = "groupBox_PreviewConfig";
groupBox_PreviewConfig.Size = new Size(397, 326);
groupBox_PreviewConfig.Size = new Size(429, 324);
groupBox_PreviewConfig.TabIndex = 1;
groupBox_PreviewConfig.TabStop = false;
groupBox_PreviewConfig.Text = "画面参数";
@@ -425,42 +450,43 @@
propertyGrid_Previewer.HelpVisible = false;
propertyGrid_Previewer.Location = new Point(3, 26);
propertyGrid_Previewer.Name = "propertyGrid_Previewer";
propertyGrid_Previewer.Size = new Size(391, 297);
propertyGrid_Previewer.Size = new Size(423, 295);
propertyGrid_Previewer.TabIndex = 1;
propertyGrid_Previewer.ToolbarVisible = false;
propertyGrid_Previewer.PropertyValueChanged += propertyGrid_PropertyValueChanged;
//
// groupBox_SkelConfig
//
groupBox_SkelConfig.Controls.Add(propertyGrid_Spine);
groupBox_SkelConfig.Controls.Add(spineViewPropertyGrid);
groupBox_SkelConfig.Dock = DockStyle.Fill;
groupBox_SkelConfig.Location = new Point(0, 0);
groupBox_SkelConfig.Margin = new Padding(0);
groupBox_SkelConfig.Name = "groupBox_SkelConfig";
groupBox_SkelConfig.Size = new Size(397, 635);
groupBox_SkelConfig.Size = new Size(429, 615);
groupBox_SkelConfig.TabIndex = 0;
groupBox_SkelConfig.TabStop = false;
groupBox_SkelConfig.Text = "模型参数";
//
// groupBox_Preview
//
groupBox_Preview.Controls.Add(spinePreviewer);
groupBox_Preview.Controls.Add(spinePreviewPanel);
groupBox_Preview.Dock = DockStyle.Fill;
groupBox_Preview.Location = new Point(0, 0);
groupBox_Preview.Name = "groupBox_Preview";
groupBox_Preview.Size = new Size(991, 969);
groupBox_Preview.Size = new Size(962, 947);
groupBox_Preview.TabIndex = 1;
groupBox_Preview.TabStop = false;
groupBox_Preview.Text = "预览画面";
//
// spinePreviewer
//
spinePreviewer.Dock = DockStyle.Fill;
spinePreviewer.Location = new Point(3, 26);
spinePreviewer.Name = "spinePreviewer";
spinePreviewer.PropertyGrid = propertyGrid_Previewer;
spinePreviewer.Size = new Size(985, 940);
spinePreviewer.SpineListView = spineListView;
spinePreviewer.TabIndex = 0;
spinePreviewPanel.Dock = DockStyle.Fill;
spinePreviewPanel.Location = new Point(3, 26);
spinePreviewPanel.Name = "spinePreviewer";
spinePreviewPanel.PropertyGrid = propertyGrid_Previewer;
spinePreviewPanel.Size = new Size(956, 918);
spinePreviewPanel.SpineListView = spineListView;
spinePreviewPanel.TabIndex = 0;
//
// panel_MainForm
//
@@ -476,17 +502,17 @@
//
toolTip.ShowAlways = true;
//
// MainForm
// SpineViewerForm
//
AutoScaleDimensions = new SizeF(11F, 24F);
AutoScaleMode = AutoScaleMode.Font;
AutoScaleDimensions = new SizeF(144F, 144F);
AutoScaleMode = AutoScaleMode.Dpi;
ClientSize = new Size(1778, 1144);
Controls.Add(panel_MainForm);
Controls.Add(menuStrip);
Icon = (Icon)resources.GetObject("$this.Icon");
MainMenuStrip = menuStrip;
Margin = new Padding(3, 2, 3, 2);
Name = "MainForm";
Name = "SpineViewerForm";
StartPosition = FormStartPosition.CenterScreen;
Text = "SpineViewer";
FormClosing += MainForm_FormClosing;
@@ -532,7 +558,6 @@
private SplitContainer splitContainer_Information;
private GroupBox groupBox_SkelList;
private GroupBox groupBox_SkelConfig;
private SplitContainer splitContainer_Config;
private GroupBox groupBox_PreviewConfig;
private Panel panel_MainForm;
private ToolStripMenuItem toolStripMenuItem_Help;
@@ -540,10 +565,9 @@
private ToolStripMenuItem toolStripMenuItem_BatchOpen;
private GroupBox groupBox_Preview;
private ToolTip toolTip;
private PropertyGrid propertyGrid_Spine;
private Controls.SpineListView spineListView;
private PropertyGrid propertyGrid_Previewer;
private Controls.SpinePreviewer spinePreviewer;
private Controls.SpinePreviewPanel spinePreviewPanel;
private ToolStripMenuItem toolStripMenuItem_Diagnostics;
private ToolStripSeparator toolStripSeparator3;
private ToolStripMenuItem toolStripMenuItem_Download;
@@ -559,5 +583,12 @@
private ToolStripMenuItem toolStripMenuItem_ExportMkv;
private ToolStripMenuItem toolStripMenuItem_ExportWebm;
private ToolStripMenuItem toolStripMenuItem_ExportCustom;
private Controls.SpineViewPropertyGrid spineViewPropertyGrid;
private ToolStripSeparator toolStripSeparator4;
private ToolStripMenuItem toolStripMenuItem_ExportWebp;
private ToolStripMenuItem toolStripMenuItem_ExportAvif;
private ToolStripSeparator toolStripSeparator5;
private ToolStripSeparator toolStripSeparator6;
private SplitContainer splitContainer_Config;
}
}

View File

@@ -0,0 +1,480 @@
using NLog;
using SpineViewer.Spine;
using System.ComponentModel;
using System.Diagnostics;
using SpineViewer.Natives;
using SpineViewer.Utils;
using SpineViewer.Spine.SpineExporter;
namespace SpineViewer
{
internal partial class SpineViewerForm : Form
{
private readonly Logger logger = LogManager.GetCurrentClassLogger();
private readonly Dictionary<string, Exporter> exporterCache = [];
public SpineViewerForm()
{
InitializeComponent();
InitializeLogConfiguration();
// 执行一些初始化工作
try
{
SFMLShader.Init();
}
catch (Exception ex)
{
logger.Error(ex.ToString());
logger.Error("Failed to load fragment shader");
MessagePopup.Warn("Fragment shader 加载失败预乘Alpha通道属性失效");
}
}
/// <summary>
/// 初始化窗口日志器
/// </summary>
private void InitializeLogConfiguration()
{
// 窗口日志
var rtbTarget = new NLog.Windows.Forms.RichTextBoxTarget
{
Name = "rtbTarget",
TargetForm = this,
TargetRichTextBox = rtbLog,
AutoScroll = true,
MaxLines = 3000,
SupportLinks = true,
Layout = "[${level:format=OneLetter}]${date:format=yyyy-MM-dd HH\\:mm\\:ss} - ${message}"
};
rtbTarget.WordColoringRules.Add(new("[D]", "Gray", "Empty", FontStyle.Bold));
rtbTarget.WordColoringRules.Add(new("[I]", "DimGray", "Empty", FontStyle.Bold));
rtbTarget.WordColoringRules.Add(new("[W]", "DarkOrange", "Empty", FontStyle.Bold));
rtbTarget.WordColoringRules.Add(new("[E]", "Red", "Empty", FontStyle.Bold));
rtbTarget.WordColoringRules.Add(new("[F]", "DarkRed", "Empty", FontStyle.Bold));
LogManager.Configuration.AddTarget(rtbTarget);
LogManager.Configuration.AddRule(LogLevel.Debug, LogLevel.Fatal, rtbTarget);
LogManager.ReconfigExistingLoggers();
}
private void MainForm_Load(object sender, EventArgs e)
{
spinePreviewPanel.StartRender();
}
private void MainForm_FormClosing(object sender, FormClosingEventArgs e)
{
spinePreviewPanel.StopRender();
}
private void toolStripMenuItem_Open_Click(object sender, EventArgs e)
{
spineListView.Add();
}
private void toolStripMenuItem_BatchOpen_Click(object sender, EventArgs e)
{
spineListView.BatchAdd();
}
#region private void toolStripMenuItem_ExportXXX_Click(object sender, EventArgs e)
private void toolStripMenuItem_ExportFrame_Click(object sender, EventArgs e)
{
if (spinePreviewPanel.IsUpdating && MessagePopup.Quest("画面仍在更新,建议手动暂停画面后导出固定的一帧,是否继续?") != DialogResult.OK)
return;
var k = nameof(toolStripMenuItem_ExportFrame);
if (!exporterCache.ContainsKey(k)) exporterCache[k] = new FrameExporter();
var exporter = exporterCache[k];
exporter.Resolution = spinePreviewPanel.Resolution;
exporter.View = spinePreviewPanel.GetView();
exporter.RenderSelectedOnly = spinePreviewPanel.RenderSelectedOnly;
var exportDialog = new Dialogs.ExportDialog(new FrameExporterProperty((FrameExporter)exporter));
if (exportDialog.ShowDialog() != DialogResult.OK)
return;
var progressDialog = new Dialogs.ProgressDialog();
progressDialog.DoWork += Export_Work;
progressDialog.RunWorkerAsync(exporter);
progressDialog.ShowDialog();
}
private void toolStripMenuItem_ExportFrameSequence_Click(object sender, EventArgs e)
{
var k = nameof(toolStripMenuItem_ExportFrameSequence);
if (!exporterCache.ContainsKey(k)) exporterCache[k] = new FrameSequenceExporter();
var exporter = exporterCache[k];
exporter.Resolution = spinePreviewPanel.Resolution;
exporter.View = spinePreviewPanel.GetView();
exporter.RenderSelectedOnly = spinePreviewPanel.RenderSelectedOnly;
var exportDialog = new Dialogs.ExportDialog(new FrameSequenceExporterProperty((FrameSequenceExporter)exporter));
if (exportDialog.ShowDialog() != DialogResult.OK)
return;
var progressDialog = new Dialogs.ProgressDialog();
progressDialog.DoWork += Export_Work;
progressDialog.RunWorkerAsync(exporter);
progressDialog.ShowDialog();
}
private void toolStripMenuItem_ExportGif_Click(object sender, EventArgs e)
{
var k = nameof(toolStripMenuItem_ExportGif);
if (!exporterCache.ContainsKey(k)) exporterCache[k] = new GifExporter();
var exporter = exporterCache[k];
exporter.Resolution = spinePreviewPanel.Resolution;
exporter.View = spinePreviewPanel.GetView();
exporter.RenderSelectedOnly = spinePreviewPanel.RenderSelectedOnly;
var exportDialog = new Dialogs.ExportDialog(new GifExporterProperty((GifExporter)exporter));
if (exportDialog.ShowDialog() != DialogResult.OK)
return;
var progressDialog = new Dialogs.ProgressDialog();
progressDialog.DoWork += Export_Work;
progressDialog.RunWorkerAsync(exporter);
progressDialog.ShowDialog();
}
private void toolStripMenuItem_ExportWebp_Click(object sender, EventArgs e)
{
var k = nameof(toolStripMenuItem_ExportWebp);
if (!exporterCache.ContainsKey(k)) exporterCache[k] = new WebpExporter();
var exporter = exporterCache[k];
exporter.Resolution = spinePreviewPanel.Resolution;
exporter.View = spinePreviewPanel.GetView();
exporter.RenderSelectedOnly = spinePreviewPanel.RenderSelectedOnly;
var exportDialog = new Dialogs.ExportDialog(new WebpExporterProperty((WebpExporter)exporter));
if (exportDialog.ShowDialog() != DialogResult.OK)
return;
var progressDialog = new Dialogs.ProgressDialog();
progressDialog.DoWork += Export_Work;
progressDialog.RunWorkerAsync(exporter);
progressDialog.ShowDialog();
}
private void toolStripMenuItem_ExportAvif_Click(object sender, EventArgs e)
{
var k = nameof(toolStripMenuItem_ExportAvif);
if (!exporterCache.ContainsKey(k)) exporterCache[k] = new AvifExporter();
var exporter = exporterCache[k];
exporter.Resolution = spinePreviewPanel.Resolution;
exporter.View = spinePreviewPanel.GetView();
exporter.RenderSelectedOnly = spinePreviewPanel.RenderSelectedOnly;
var exportDialog = new Dialogs.ExportDialog(new AvifExporterProperty((AvifExporter)exporter));
if (exportDialog.ShowDialog() != DialogResult.OK)
return;
var progressDialog = new Dialogs.ProgressDialog();
progressDialog.DoWork += Export_Work;
progressDialog.RunWorkerAsync(exporter);
progressDialog.ShowDialog();
}
private void toolStripMenuItem_ExportMp4_Click(object sender, EventArgs e)
{
var k = nameof(toolStripMenuItem_ExportMp4);
if (!exporterCache.ContainsKey(k)) exporterCache[k] = new Mp4Exporter();
var exporter = exporterCache[k];
exporter.Resolution = spinePreviewPanel.Resolution;
exporter.View = spinePreviewPanel.GetView();
exporter.RenderSelectedOnly = spinePreviewPanel.RenderSelectedOnly;
var exportDialog = new Dialogs.ExportDialog(new Mp4ExporterProperty((Mp4Exporter)exporter));
if (exportDialog.ShowDialog() != DialogResult.OK)
return;
var progressDialog = new Dialogs.ProgressDialog();
progressDialog.DoWork += Export_Work;
progressDialog.RunWorkerAsync(exporter);
progressDialog.ShowDialog();
}
private void toolStripMenuItem_ExportWebm_Click(object sender, EventArgs e)
{
var k = nameof(toolStripMenuItem_ExportWebm);
if (!exporterCache.ContainsKey(k)) exporterCache[k] = new WebmExporter();
var exporter = exporterCache[k];
exporter.Resolution = spinePreviewPanel.Resolution;
exporter.View = spinePreviewPanel.GetView();
exporter.RenderSelectedOnly = spinePreviewPanel.RenderSelectedOnly;
var exportDialog = new Dialogs.ExportDialog(new WebmExporterProperty((WebmExporter)exporter));
if (exportDialog.ShowDialog() != DialogResult.OK)
return;
var progressDialog = new Dialogs.ProgressDialog();
progressDialog.DoWork += Export_Work;
progressDialog.RunWorkerAsync(exporter);
progressDialog.ShowDialog();
}
private void toolStripMenuItem_ExportMkv_Click(object sender, EventArgs e)
{
var k = nameof(toolStripMenuItem_ExportMkv);
if (!exporterCache.ContainsKey(k)) exporterCache[k] = new MkvExporter();
var exporter = exporterCache[k];
exporter.Resolution = spinePreviewPanel.Resolution;
exporter.View = spinePreviewPanel.GetView();
exporter.RenderSelectedOnly = spinePreviewPanel.RenderSelectedOnly;
var exportDialog = new Dialogs.ExportDialog(new MkvExporterProperty((MkvExporter)exporter));
if (exportDialog.ShowDialog() != DialogResult.OK)
return;
var progressDialog = new Dialogs.ProgressDialog();
progressDialog.DoWork += Export_Work;
progressDialog.RunWorkerAsync(exporter);
progressDialog.ShowDialog();
}
private void toolStripMenuItem_ExportMov_Click(object sender, EventArgs e)
{
var k = nameof(toolStripMenuItem_ExportMov);
if (!exporterCache.ContainsKey(k)) exporterCache[k] = new MovExporter();
var exporter = exporterCache[k];
exporter.Resolution = spinePreviewPanel.Resolution;
exporter.View = spinePreviewPanel.GetView();
exporter.RenderSelectedOnly = spinePreviewPanel.RenderSelectedOnly;
var exportDialog = new Dialogs.ExportDialog(new MovExporterProperty((MovExporter)exporter));
if (exportDialog.ShowDialog() != DialogResult.OK)
return;
var progressDialog = new Dialogs.ProgressDialog();
progressDialog.DoWork += Export_Work;
progressDialog.RunWorkerAsync(exporter);
progressDialog.ShowDialog();
}
private void toolStripMenuItem_ExportCustom_Click(object sender, EventArgs e)
{
var k = nameof(toolStripMenuItem_ExportCustom);
if (!exporterCache.ContainsKey(k)) exporterCache[k] = new CustomExporter();
var exporter = exporterCache[k];
exporter.Resolution = spinePreviewPanel.Resolution;
exporter.View = spinePreviewPanel.GetView();
exporter.RenderSelectedOnly = spinePreviewPanel.RenderSelectedOnly;
var exportDialog = new Dialogs.ExportDialog(new CustomExporterProperty((CustomExporter)exporter));
if (exportDialog.ShowDialog() != DialogResult.OK)
return;
var progressDialog = new Dialogs.ProgressDialog();
progressDialog.DoWork += Export_Work;
progressDialog.RunWorkerAsync(exporter);
progressDialog.ShowDialog();
}
#endregion
private void toolStripMenuItem_Exit_Click(object sender, EventArgs e)
{
Close();
}
private void toolStripMenuItem_ConvertFileFormat_Click(object sender, EventArgs e)
{
var openDialog = new Dialogs.ConvertFileFormatDialog();
if (openDialog.ShowDialog() != DialogResult.OK)
return;
var progressDialog = new Dialogs.ProgressDialog();
progressDialog.DoWork += ConvertFileFormat_Work;
progressDialog.RunWorkerAsync(openDialog.Result);
progressDialog.ShowDialog();
}
private void toolStripMenuItem_ManageResource_Click(object sender, EventArgs e)
{
}
private void toolStripMenuItem_About_Click(object sender, EventArgs e)
{
using var dialog = new Dialogs.AboutDialog();
dialog.ShowDialog();
}
private void toolStripMenuItem_Diagnostics_Click(object sender, EventArgs e)
{
using var dialog = new Dialogs.DiagnosticsDialog();
dialog.ShowDialog();
}
private void splitContainer_SplitterMoved(object sender, SplitterEventArgs e) => ActiveControl = null;
private void splitContainer_MouseUp(object sender, MouseEventArgs e) => ActiveControl = null;
private void propertyGrid_PropertyValueChanged(object sender, PropertyValueChangedEventArgs e)
{
// 用来解决对面板某些值修改之后, 其他被联动修改的值不会实时刷新的问题
(sender as PropertyGrid)?.Refresh();
}
private void Export_Work(object? sender, DoWorkEventArgs e)
{
var worker = (BackgroundWorker)sender;
var exporter = (Exporter)e.Argument;
Invoke(() => TaskbarManager.SetProgressState(Handle, TBPFLAG.TBPF_INDETERMINATE));
spinePreviewPanel.StopRender();
lock (spineListView.Spines) { exporter.Export(spineListView.Spines.Where(sp => !sp.IsHidden).ToArray(), (BackgroundWorker)sender); }
e.Cancel = worker.CancellationPending;
spinePreviewPanel.StartRender();
Invoke(() => TaskbarManager.SetProgressState(Handle, TBPFLAG.TBPF_NOPROGRESS));
}
private void ConvertFileFormat_Work(object? sender, DoWorkEventArgs e)
{
var worker = sender as BackgroundWorker;
var arguments = e.Argument as Dialogs.ConvertFileFormatDialogResult;
var skelPaths = arguments.SkelPaths;
var srcVersion = arguments.SourceVersion;
var tgtVersion = arguments.TargetVersion;
var jsonTarget = arguments.JsonTarget;
var newSuffix = jsonTarget ? ".json" : ".skel";
int totalCount = skelPaths.Length;
int success = 0;
int error = 0;
SkeletonConverter srcCvter = srcVersion != SpineVersion.Auto ? SkeletonConverter.New(srcVersion) : null;
SkeletonConverter tgtCvter = SkeletonConverter.New(tgtVersion);
worker.ReportProgress(0, $"已处理 0/{totalCount}");
for (int i = 0; i < totalCount; i++)
{
if (worker.CancellationPending)
{
e.Cancel = true;
break;
}
var skelPath = skelPaths[i];
var newPath = Path.ChangeExtension(skelPath, newSuffix);
try
{
if (srcVersion == SpineVersion.Auto)
{
try
{
srcCvter = SkeletonConverter.New(SpineUtils.GetVersion(skelPath));
}
catch (Exception ex)
{
throw new InvalidDataException($"Auto version detection failed for {skelPath}, try to use a specific version", ex);
}
}
var root = srcCvter.Read(skelPath);
root = srcCvter.ToVersion(root, tgtVersion);
if (jsonTarget) tgtCvter.WriteJson(root, newPath); else tgtCvter.WriteBinary(root, newPath);
success++;
}
catch (Exception ex)
{
logger.Error(ex.ToString());
logger.Error("Failed to convert {}", skelPath);
error++;
}
worker.ReportProgress((int)((i + 1) * 100.0) / totalCount, $"已处理 {i + 1}/{totalCount}");
}
if (error > 0)
{
logger.Warn("Batch convert {} successfully, {} failed", success, error);
}
else
{
logger.Info("{} skel converted successfully", success);
}
}
//private System.Windows.Forms.Timer timer = new();
//private PetForm pet = new PetForm();
//private IntPtr screenDC;
//private IntPtr memDC;
//private void _Test()
//{
// screenDC = Win32.GetDC(IntPtr.Zero);
// memDC = Win32.CreateCompatibleDC(screenDC);
// pet.Show();
// timer.Tick += Timer_Tick;
// timer.Enabled = true;
// timer.Interval = 50;
// timer.Start();
//}
//private void Timer_Tick(object? sender, EventArgs e)
//{
// using var tex = new SFML.Graphics.RenderTexture((uint)pet.Width, (uint)pet.Height);
// var v = spinePreviewer.GetView();
// tex.SetView(v);
// tex.Clear(new SFML.Graphics.Color(0, 0, 0, 0));
// lock (spineListView.Spines)
// {
// foreach (var sp in spineListView.Spines)
// tex.Draw(sp);
// }
// tex.Display();
// using var frame = new SFMLImageVideoFrame(tex.Texture.CopyToImage());
// using var bitmap = frame.CopyToBitmap();
// var newBitmap = bitmap.GetHbitmap(Color.FromArgb(0));
// var oldBitmap = Win32.SelectObject(memDC, newBitmap);
// Win32.SIZE size = new Win32.SIZE { cx = pet.Width, cy = pet.Height };
// Win32.POINT srcPos = new Win32.POINT { x = 0, y = 0 };
// Win32.BLENDFUNCTION blend = new Win32.BLENDFUNCTION { BlendOp = 0, BlendFlags = 0, SourceConstantAlpha = 255, AlphaFormat = Win32.AC_SRC_ALPHA };
// Win32.UpdateLayeredWindow(pet.Handle, screenDC, IntPtr.Zero, ref size, memDC, ref srcPos, 0, ref blend, Win32.ULW_ALPHA);
// Win32.SelectObject(memDC, oldBitmap);
// Win32.DeleteObject(newBitmap);
//}
//private void spinePreviewer_KeyDown(object sender, KeyEventArgs e)
//{
// switch (e.KeyCode)
// {
// case Keys.Space:
// if ((ModifierKeys & Keys.Alt) != 0)
// spinePreviewer.ClickStopButton();
// else
// spinePreviewer.ClickStartButton();
// break;
// case Keys.Right:
// if ((ModifierKeys & Keys.Alt) != 0)
// spinePreviewer.ClickForwardFastButton();
// else
// spinePreviewer.ClickForwardStepButton();
// break;
// case Keys.Left:
// if ((ModifierKeys & Keys.Alt) != 0)
// spinePreviewer.ClickRestartButton();
// break;
// }
//}
}
}

View File

@@ -1,295 +0,0 @@
using NLog;
using SpineViewer.Spine;
using System.ComponentModel;
using System.Diagnostics;
using SpineViewer.Exporter;
namespace SpineViewer
{
internal partial class MainForm : Form
{
private Logger logger = LogManager.GetCurrentClassLogger();
public MainForm()
{
InitializeComponent();
InitializeLogConfiguration();
// 在此处将导出菜单需要的类绑定起来
toolStripMenuItem_ExportFrame.Tag = ExportType.Frame;
toolStripMenuItem_ExportFrameSequence.Tag = ExportType.FrameSequence;
toolStripMenuItem_ExportGif.Tag = ExportType.Gif;
toolStripMenuItem_ExportMkv.Tag = ExportType.Mkv;
toolStripMenuItem_ExportMp4.Tag = ExportType.Mp4;
toolStripMenuItem_ExportMov.Tag = ExportType.Mov;
toolStripMenuItem_ExportWebm.Tag = ExportType.Webm;
toolStripMenuItem_ExportCustom.Tag = ExportType.Custom;
// 执行一些初始化工作
try
{
Shader.Init();
}
catch (Exception ex)
{
logger.Error(ex.ToString());
logger.Error("Failed to load fragment shader");
MessageBox.Warn("Fragment shader 加载失败预乘Alpha通道属性失效");
}
}
/// <summary>
/// 初始化窗口日志器
/// </summary>
private void InitializeLogConfiguration()
{
// 窗口日志
var rtbTarget = new NLog.Windows.Forms.RichTextBoxTarget
{
Name = "rtbTarget",
TargetForm = this,
TargetRichTextBox = rtbLog,
AutoScroll = true,
MaxLines = 3000,
SupportLinks = true,
Layout = "[${level:format=OneLetter}]${date:format=yyyy-MM-dd HH\\:mm\\:ss} - ${message}"
};
rtbTarget.WordColoringRules.Add(new("[D]", "Gray", "Empty", FontStyle.Bold));
rtbTarget.WordColoringRules.Add(new("[I]", "DimGray", "Empty", FontStyle.Bold));
rtbTarget.WordColoringRules.Add(new("[W]", "DarkOrange", "Empty", FontStyle.Bold));
rtbTarget.WordColoringRules.Add(new("[E]", "Red", "Empty", FontStyle.Bold));
rtbTarget.WordColoringRules.Add(new("[F]", "DarkRed", "Empty", FontStyle.Bold));
LogManager.Configuration.AddTarget(rtbTarget);
LogManager.Configuration.AddRule(LogLevel.Debug, LogLevel.Fatal, rtbTarget);
LogManager.ReconfigExistingLoggers();
}
private void MainForm_Load(object sender, EventArgs e)
{
spinePreviewer.StartRender();
}
private void MainForm_FormClosing(object sender, FormClosingEventArgs e)
{
spinePreviewer.StopRender();
}
private void toolStripMenuItem_Open_Click(object sender, EventArgs e)
{
spineListView.Add();
}
private void toolStripMenuItem_BatchOpen_Click(object sender, EventArgs e)
{
spineListView.BatchAdd();
}
private void toolStripMenuItem_Export_Click(object sender, EventArgs e)
{
ExportType type = (ExportType)((ToolStripMenuItem)sender).Tag;
if (type == ExportType.Frame && spinePreviewer.IsUpdating)
{
if (MessageBox.Quest("画面仍在更新,建议手动暂停画面后导出固定的一帧,是否继续?") != DialogResult.OK)
return;
}
var exportArgs = ExportArgs.New(type, spinePreviewer.Resolution, spinePreviewer.GetView(), spinePreviewer.RenderSelectedOnly);
var exportDialog = new Dialogs.ExportDialog() { ExportArgs = exportArgs };
if (exportDialog.ShowDialog() != DialogResult.OK)
return;
var exporter = Exporter.Exporter.New(type, exportArgs);
var progressDialog = new Dialogs.ProgressDialog();
progressDialog.DoWork += Export_Work;
progressDialog.RunWorkerAsync(exporter);
progressDialog.ShowDialog();
}
private void toolStripMenuItem_Exit_Click(object sender, EventArgs e)
{
Close();
}
private void toolStripMenuItem_ConvertFileFormat_Click(object sender, EventArgs e)
{
var openDialog = new Dialogs.ConvertFileFormatDialog();
if (openDialog.ShowDialog() != DialogResult.OK)
return;
var progressDialog = new Dialogs.ProgressDialog();
progressDialog.DoWork += ConvertFileFormat_Work;
progressDialog.RunWorkerAsync(openDialog.Result);
progressDialog.ShowDialog();
}
//private System.Windows.Forms.Timer timer = new();
//private PetForm pet = new PetForm();
//private IntPtr screenDC;
//private IntPtr memDC;
private void toolStripMenuItem_ManageResource_Click(object sender, EventArgs e)
{
// screenDC = Win32.GetDC(IntPtr.Zero);
// memDC = Win32.CreateCompatibleDC(screenDC);
// pet.Show();
// timer.Tick += Timer_Tick;
// timer.Enabled = true;
// timer.Interval = 50;
// timer.Start();
//}
//private void Timer_Tick(object? sender, EventArgs e)
//{
// using var tex = new SFML.Graphics.RenderTexture((uint)pet.Width, (uint)pet.Height);
// var v = spinePreviewer.GetView();
// tex.SetView(v);
// tex.Clear(new SFML.Graphics.Color(0, 0, 0, 0));
// lock (spineListView.Spines)
// {
// foreach (var sp in spineListView.Spines)
// tex.Draw(sp);
// }
// tex.Display();
// using var frame = new SFMLImageVideoFrame(tex.Texture.CopyToImage());
// using var bitmap = frame.CopyToBitmap();
// var newBitmap = bitmap.GetHbitmap(Color.FromArgb(0));
// var oldBitmap = Win32.SelectObject(memDC, newBitmap);
// Win32.SIZE size = new Win32.SIZE { cx = pet.Width, cy = pet.Height };
// Win32.POINT srcPos = new Win32.POINT { x = 0, y = 0 };
// Win32.BLENDFUNCTION blend = new Win32.BLENDFUNCTION { BlendOp = 0, BlendFlags = 0, SourceConstantAlpha = 255, AlphaFormat = Win32.AC_SRC_ALPHA };
// Win32.UpdateLayeredWindow(pet.Handle, screenDC, IntPtr.Zero, ref size, memDC, ref srcPos, 0, ref blend, Win32.ULW_ALPHA);
// Win32.SelectObject(memDC, oldBitmap);
// Win32.DeleteObject(newBitmap);
}
private void toolStripMenuItem_About_Click(object sender, EventArgs e)
{
(new Dialogs.AboutDialog()).ShowDialog();
}
private void toolStripMenuItem_Diagnostics_Click(object sender, EventArgs e)
{
(new Dialogs.DiagnosticsDialog()).ShowDialog();
}
private void splitContainer_SplitterMoved(object sender, SplitterEventArgs e) => ActiveControl = null;
private void splitContainer_MouseUp(object sender, MouseEventArgs e) => ActiveControl = null;
private void propertyGrid_PropertyValueChanged(object sender, PropertyValueChangedEventArgs e)
{
// 用来解决对面板某些值修改之后, 其他被联动修改的值不会实时刷新的问题
(sender as PropertyGrid)?.Refresh();
}
private void Export_Work(object? sender, DoWorkEventArgs e)
{
var worker = (BackgroundWorker)sender;
var exporter = (Exporter.Exporter)e.Argument;
Invoke(() => TaskbarManager.SetProgressState(Handle, TBPFLAG.TBPF_INDETERMINATE));
spinePreviewer.StopRender();
lock (spineListView.Spines) { exporter.Export(spineListView.Spines.Where(sp => !sp.IsHidden).ToArray(), (BackgroundWorker)sender); }
e.Cancel = worker.CancellationPending;
spinePreviewer.StartRender();
Invoke(() => TaskbarManager.SetProgressState(Handle, TBPFLAG.TBPF_NOPROGRESS));
}
private void ConvertFileFormat_Work(object? sender, DoWorkEventArgs e)
{
var worker = sender as BackgroundWorker;
var arguments = e.Argument as Dialogs.ConvertFileFormatDialogResult;
var skelPaths = arguments.SkelPaths;
var srcVersion = arguments.SourceVersion;
var tgtVersion = arguments.TargetVersion;
var jsonTarget = arguments.JsonTarget;
var newSuffix = jsonTarget ? ".json" : ".skel";
int totalCount = skelPaths.Length;
int success = 0;
int error = 0;
SkeletonConverter srcCvter = srcVersion != SpineVersion.Auto ? SkeletonConverter.New(srcVersion) : null;
SkeletonConverter tgtCvter = SkeletonConverter.New(tgtVersion);
worker.ReportProgress(0, $"已处理 0/{totalCount}");
for (int i = 0; i < totalCount; i++)
{
if (worker.CancellationPending)
{
e.Cancel = true;
break;
}
var skelPath = skelPaths[i];
var newPath = Path.ChangeExtension(skelPath, newSuffix);
try
{
if (srcVersion == SpineVersion.Auto)
{
try
{
srcCvter = SkeletonConverter.New(SpineHelper.GetVersion(skelPath));
}
catch (Exception ex)
{
throw new InvalidDataException($"Auto version detection failed for {skelPath}, try to use a specific version", ex);
}
}
var root = srcCvter.Read(skelPath);
root = srcCvter.ToVersion(root, tgtVersion);
if (jsonTarget) tgtCvter.WriteJson(root, newPath); else tgtCvter.WriteBinary(root, newPath);
success++;
}
catch (Exception ex)
{
logger.Error(ex.ToString());
logger.Error("Failed to convert {}", skelPath);
error++;
}
worker.ReportProgress((int)((i + 1) * 100.0) / totalCount, $"已处理 {i + 1}/{totalCount}");
}
if (error > 0)
{
logger.Warn("Batch convert {} successfully, {} failed", success, error);
}
else
{
logger.Info("{} skel converted successfully", success);
}
}
//private void spinePreviewer_KeyDown(object sender, KeyEventArgs e)
//{
// switch (e.KeyCode)
// {
// case Keys.Space:
// if ((ModifierKeys & Keys.Alt) != 0)
// spinePreviewer.ClickStopButton();
// else
// spinePreviewer.ClickStartButton();
// break;
// case Keys.Right:
// if ((ModifierKeys & Keys.Alt) != 0)
// spinePreviewer.ClickForwardFastButton();
// else
// spinePreviewer.ClickForwardStepButton();
// break;
// case Keys.Left:
// if ((ModifierKeys & Keys.Alt) != 0)
// spinePreviewer.ClickRestartButton();
// break;
// }
//}
}
}

View File

@@ -2,7 +2,7 @@
using System.Runtime.InteropServices;
using System.Windows.Forms;
namespace SpineViewer
namespace SpineViewer.Natives
{
internal enum TBPFLAG
{
@@ -19,15 +19,15 @@ namespace SpineViewer
{
// ITaskbarList
void HrInit();
void AddTab(IntPtr hwnd);
void DeleteTab(IntPtr hwnd);
void ActivateTab(IntPtr hwnd);
void SetActiveAlt(IntPtr hwnd);
void AddTab(nint hwnd);
void DeleteTab(nint hwnd);
void ActivateTab(nint hwnd);
void SetActiveAlt(nint hwnd);
// ITaskbarList2
void MarkFullscreenWindow(IntPtr hwnd, [MarshalAs(UnmanagedType.Bool)] bool fFullscreen);
void MarkFullscreenWindow(nint hwnd, [MarshalAs(UnmanagedType.Bool)] bool fFullscreen);
// ITaskbarList3
void SetProgressValue(IntPtr hwnd, ulong ullCompleted, ulong ullTotal);
void SetProgressState(IntPtr hwnd, TBPFLAG tbpFlags);
void SetProgressValue(nint hwnd, ulong ullCompleted, ulong ullTotal);
void SetProgressState(nint hwnd, TBPFLAG tbpFlags);
//void RegisterTab(IntPtr hwndTab, IntPtr hwndMDI);
//void UnregisterTab(IntPtr hwndTab);
//void SetTabOrder(IntPtr hwndTab, IntPtr hwndInsertBefore);
@@ -52,12 +52,12 @@ namespace SpineViewer
taskbarList.HrInit();
}
public static void SetProgressState(IntPtr windowHandle, TBPFLAG state)
public static void SetProgressState(nint windowHandle, TBPFLAG state)
{
taskbarList.SetProgressState(windowHandle, state);
}
public static void SetProgressValue(IntPtr windowHandle, ulong completed, ulong total)
public static void SetProgressValue(nint windowHandle, ulong completed, ulong total)
{
taskbarList.SetProgressValue(windowHandle, completed, total);
}

View File

@@ -7,7 +7,7 @@ using System.Runtime.InteropServices;
using System.Text;
using System.Threading.Tasks;
namespace SpineViewer
namespace SpineViewer.Natives
{
/// <summary>
/// Win32 Sdk 包装类
@@ -38,7 +38,7 @@ namespace SpineViewer
public const int ULW_ALPHA = 0x00000002;
public const int ULW_OPAQUE = 0x00000004;
public const IntPtr HWND_TOPMOST = -1;
public const nint HWND_TOPMOST = -1;
public const uint SWP_NOSIZE = 0x0001;
public const uint SWP_NOMOVE = 0x0002;
@@ -87,28 +87,28 @@ namespace SpineViewer
}
[DllImport("user32.dll", SetLastError = true)]
public static extern IntPtr GetDC(IntPtr hWnd);
public static extern nint GetDC(nint hWnd);
[DllImport("user32.dll", SetLastError = true)]
public static extern int ReleaseDC(IntPtr hWnd, IntPtr hDC);
public static extern int ReleaseDC(nint hWnd, nint hDC);
[DllImport("user32.dll", SetLastError = true)]
public static extern int SetWindowLong(IntPtr hWnd, int nIndex, int dwNewLong);
public static extern int SetWindowLong(nint hWnd, int nIndex, int dwNewLong);
[DllImport("user32.dll", SetLastError = true)]
public static extern int GetWindowLong(IntPtr hWnd, int nIndex);
public static extern int GetWindowLong(nint hWnd, int nIndex);
[DllImport("user32.dll", SetLastError = true)]
public static extern bool GetLayeredWindowAttributes(IntPtr hWnd, ref uint crKey, ref byte bAlpha, ref uint dwFlags);
public static extern bool GetLayeredWindowAttributes(nint hWnd, ref uint crKey, ref byte bAlpha, ref uint dwFlags);
[DllImport("user32.dll", SetLastError = true)]
public static extern bool SetLayeredWindowAttributes(IntPtr hWnd, uint pcrKey, byte pbAlpha, uint pdwFlags);
public static extern bool SetLayeredWindowAttributes(nint hWnd, uint pcrKey, byte pbAlpha, uint pdwFlags);
[DllImport("user32.dll", SetLastError = true)]
public static extern bool UpdateLayeredWindow(IntPtr hWnd, IntPtr hdcDst, IntPtr pptDst, ref SIZE psize, IntPtr hdcSrc, ref POINT pptSrc, int crKey, ref BLENDFUNCTION pblend, int dwFlags);
public static extern bool UpdateLayeredWindow(nint hWnd, nint hdcDst, nint pptDst, ref SIZE psize, nint hdcSrc, ref POINT pptSrc, int crKey, ref BLENDFUNCTION pblend, int dwFlags);
[DllImport("user32.dll", SetLastError = true)]
public static extern bool SetWindowPos(IntPtr hWnd, IntPtr hWndInsertAfter, int X, int Y, int cx, int cy, uint uFlags);
public static extern bool SetWindowPos(nint hWnd, nint hWndInsertAfter, int X, int Y, int cx, int cy, uint uFlags);
[DllImport("user32.dll", SetLastError = true)]
public static extern uint GetDoubleClickTime();
@@ -117,37 +117,37 @@ namespace SpineViewer
private static extern bool GetLastInputInfo(ref LASTINPUTINFO plii);
[DllImport("user32.dll", SetLastError = true)]
public static extern IntPtr FindWindow(string lpClassName, string lpWindowName);
public static extern nint FindWindow(string lpClassName, string lpWindowName);
[DllImport("user32.dll", SetLastError = true)]
public static extern IntPtr SendMessageTimeout(IntPtr hWnd, uint Msg, IntPtr wParam, IntPtr lParam, uint fuFlags, uint uTimeout, out IntPtr lpdwResult);
public static extern nint SendMessageTimeout(nint hWnd, uint Msg, nint wParam, nint lParam, uint fuFlags, uint uTimeout, out nint lpdwResult);
[DllImport("user32.dll", SetLastError = true)]
public static extern IntPtr FindWindowEx(IntPtr parentHandle, IntPtr childAfter, string className, string windowTitle);
public static extern nint FindWindowEx(nint parentHandle, nint childAfter, string className, string windowTitle);
[DllImport("user32.dll", SetLastError = true)]
public static extern IntPtr SetParent(IntPtr hWndChild, IntPtr hWndNewParent);
public static extern nint SetParent(nint hWndChild, nint hWndNewParent);
[DllImport("user32.dll", SetLastError = true)]
public static extern IntPtr GetParent(IntPtr hWnd);
public static extern nint GetParent(nint hWnd);
[DllImport("user32.dll", SetLastError = true)]
public static extern IntPtr GetAncestor(IntPtr hWnd, uint gaFlags);
public static extern nint GetAncestor(nint hWnd, uint gaFlags);
[DllImport("user32.dll", SetLastError = true)]
public static extern IntPtr GetWindow(IntPtr hWnd, uint uCmd);
public static extern nint GetWindow(nint hWnd, uint uCmd);
[DllImport("gdi32.dll", SetLastError = true)]
public static extern IntPtr CreateCompatibleDC(IntPtr hdc);
public static extern nint CreateCompatibleDC(nint hdc);
[DllImport("gdi32.dll", SetLastError = true)]
public static extern bool DeleteDC(IntPtr hdc);
public static extern bool DeleteDC(nint hdc);
[DllImport("gdi32.dll", SetLastError = true)]
public static extern IntPtr SelectObject(IntPtr hdc, IntPtr hgdiobj);
public static extern nint SelectObject(nint hdc, nint hgdiobj);
[DllImport("gdi32.dll", SetLastError = true)]
public static extern bool DeleteObject(IntPtr hObject);
public static extern bool DeleteObject(nint hObject);
public static TimeSpan GetLastInputElapsedTime()
{
@@ -164,12 +164,12 @@ namespace SpineViewer
return TimeSpan.FromMilliseconds(idleTimeMillis);
}
public static IntPtr GetWorkerW()
public static nint GetWorkerW()
{
var progman = FindWindow("Progman", null);
if (progman == IntPtr.Zero)
return IntPtr.Zero;
IntPtr hWnd = FindWindowEx(progman, 0, "WorkerW", null);
if (progman == nint.Zero)
return nint.Zero;
nint hWnd = FindWindowEx(progman, 0, "WorkerW", null);
Debug.WriteLine($"{hWnd:x8}");
return hWnd;
}

View File

@@ -1,5 +1,7 @@
using NLog;
using SpineViewer.Utils;
using System.Diagnostics;
using System.Reflection;
namespace SpineViewer
{
@@ -25,6 +27,8 @@ namespace SpineViewer
///// </summary>
//public static readonly string TempDir = Directory.CreateDirectory(Path.Combine(Path.GetTempPath(), Name)).FullName;
public static string Version => Assembly.GetExecutingAssembly().GetCustomAttribute<AssemblyInformationalVersionAttribute>()?.InformationalVersion;
/// <summary>
/// 程序日志器
/// </summary>
@@ -46,12 +50,12 @@ namespace SpineViewer
try
{
Application.Run(new MainForm());
Application.Run(new SpineViewerForm() { Text = $"SpineViewer - v{Version}"});
}
catch (Exception ex)
{
logger.Fatal(ex.ToString());
MessageBox.Error(ex.ToString(), "程序已崩溃");
MessagePopup.Error(ex.ToString(), "程序已崩溃");
}
}

View File

@@ -1,124 +0,0 @@
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace SpineViewer.Spine
{
/// <summary>
/// 对轨道索引的包装类, 能够在面板上显示例如时长的属性, 但是处理该属性时按字符串去处理, 例如 ToString 和判断对象相等都是用动画名称实现逻辑
/// </summary>
/// <param name="spine"></param>
/// <param name="i"></param>
[TypeConverter(typeof(TrackWrapperConverter))]
public class TrackWrapper(Spine spine, int i)
{
private readonly Spine spine = spine;
[Browsable(false)]
public int Index { get; } = i;
[DisplayName("时长")]
public float Duration => spine.GetAnimationDuration(spine.GetAnimation(Index));
/// <summary>
/// 实现了默认的转为字符串的方式
/// </summary>
public override string ToString() => spine.GetAnimation(Index);
/// <summary>
/// 影响了属性面板的判断, 当动画名称相同的时候认为两个对象是相同的, 这样属性面板可以在多选的时候正确显示相同取值的内容
/// </summary>
public override bool Equals(object? obj)
{
if (obj is TrackWrapper) return ToString() == obj.ToString();
return base.Equals(obj);
}
/// <summary>
/// 哈希码需要和 Equals 行为类似
/// </summary>
public override int GetHashCode() => (typeof(TrackWrapper).FullName + ToString()).GetHashCode();
}
/// <summary>
/// 轨道属性描述符, 实现对属性的读取和赋值
/// </summary>
/// <param name="spine">关联的 Spine 对象</param>
/// <param name="i">轨道索引</param>
public class TrackWrapperPropertyDescriptor(Spine spine, int i) : PropertyDescriptor($"Track{i}", [new DisplayNameAttribute($"轨道 {i}")])
{
private readonly Spine spine = spine;
private readonly int idx = i;
public override Type ComponentType => typeof(AnimationTracksType);
public override bool IsReadOnly => false;
public override Type PropertyType => typeof(TrackWrapper);
public override bool CanResetValue(object component) => false;
public override void ResetValue(object component) { }
public override bool ShouldSerializeValue(object component) => false;
/// <summary>
/// 得到一个轨道包装类, 允许用户查看或者修改具体的属性值, 这个地方决定了在面板上看到的是一个对象及其属性
/// </summary>
public override object? GetValue(object? component) => new TrackWrapper(spine, idx);
/// <summary>
/// 允许通过字符串赋值修改该轨道的动画, 这里决定了当其他地方的调用 (比如 Converter) 通过 value 来设置属性值的时候应该怎么处理
/// </summary>
public override void SetValue(object? component, object? value)
{
if (value is string s) spine.SetAnimation(idx, s);
}
}
/// <summary>
/// AnimationTracks 动态类型包装类, 用于提供对 Spine 对象多轨道动画的访问能力, 不同轨道将动态生成属性
/// </summary>
/// <param name="spine">关联的 Spine 对象</param>
public class AnimationTracksType(Spine spine) : ICustomTypeDescriptor
{
private readonly Dictionary<int, TrackWrapperPropertyDescriptor> pdCache = [];
public Spine Spine { get; } = spine;
// XXX: 必须实现 ICustomTypeDescriptor 接口, 不能继承 CustomTypeDescriptor, 似乎继承下来的东西会有问题, 导致某些调用不正确
public AttributeCollection GetAttributes() => TypeDescriptor.GetAttributes(this, true);
public string? GetClassName() => TypeDescriptor.GetClassName(this, true);
public string? GetComponentName() => TypeDescriptor.GetComponentName(this, true);
public TypeConverter? GetConverter() => TypeDescriptor.GetConverter(this, true);
public EventDescriptor? GetDefaultEvent() => TypeDescriptor.GetDefaultEvent(this, true);
public PropertyDescriptor? GetDefaultProperty() => TypeDescriptor.GetDefaultProperty(this, true);
public object? GetEditor(Type editorBaseType) => TypeDescriptor.GetEditor(this, editorBaseType, true);
public EventDescriptorCollection GetEvents() => TypeDescriptor.GetEvents(this, true);
public EventDescriptorCollection GetEvents(Attribute[]? attributes) => TypeDescriptor.GetEvents(this, attributes, true);
public object? GetPropertyOwner(PropertyDescriptor? pd) => this;
public PropertyDescriptorCollection GetProperties() => GetProperties(null);
public PropertyDescriptorCollection GetProperties(Attribute[]? attributes)
{
var props = new List<TrackWrapperPropertyDescriptor>();
foreach (var i in Spine.GetTrackIndices())
{
if (!pdCache.ContainsKey(i))
pdCache[i] = new TrackWrapperPropertyDescriptor(Spine, i);
props.Add(pdCache[i]);
}
return new PropertyDescriptorCollection(props.ToArray());
}
/// <summary>
/// 在属性面板悬停可以按轨道顺序显示动画名称
/// </summary>
public override string ToString() => $"[{string.Join(", ", Spine.GetTrackIndices().Select(Spine.GetAnimation))}]";
public override bool Equals(object? obj)
{
if (obj is AnimationTracksType tracks) return ToString() == tracks.ToString();
return base.Equals(obj);
}
public override int GetHashCode() => (typeof(AnimationTracksType).FullName + ToString()).GetHashCode();
}
}

View File

@@ -7,11 +7,12 @@ using System.Linq;
using System.Text;
using System.Threading.Tasks;
using SpineRuntime21;
using SpineViewer.Utils;
namespace SpineViewer.Spine.Implementations.Spine
namespace SpineViewer.Spine.Implementations.SpineObject
{
[SpineImplementation(SpineVersion.V21)]
internal class Spine21 : SpineViewer.Spine.Spine
internal class SpineObject21 : Spine.SpineObject
{
private static readonly Animation EmptyAnimation = new(EMPTY_ANIMATION, [], 0);
@@ -47,7 +48,7 @@ namespace SpineViewer.Spine.Implementations.Spine
// 2.1.x 不支持剪裁
//private SkeletonClipping clipping = new();
public Spine21(string skelPath, string atlasPath) : base(skelPath, atlasPath)
public SpineObject21(string skelPath, string atlasPath) : base(skelPath, atlasPath)
{
atlas = new Atlas(AtlasPath, textureLoader);
try
@@ -111,15 +112,14 @@ namespace SpineViewer.Spine.Implementations.Spine
var fY = flipY;
var animations = animationState.Tracks.Where(te => te is not null).Select(te => te.Animation.Name).ToArray();
var val = Math.Max(value, SCALE_MIN);
if (skeletonBinary is not null)
{
skeletonBinary.Scale = val;
skeletonBinary.Scale = value;
skeletonData = skeletonBinary.ReadSkeletonData(SkelPath);
}
else if (skeletonJson is not null)
{
skeletonJson.Scale = val;
skeletonJson.Scale = value;
skeletonData = skeletonJson.ReadSkeletonData(SkelPath);
}
@@ -260,9 +260,9 @@ namespace SpineViewer.Spine.Implementations.Spine
protected override void draw(SFML.Graphics.RenderTarget target, SFML.Graphics.RenderStates states)
{
vertexArray.Clear();
triangleVertices.Clear();
states.Texture = null;
states.Shader = Shader.GetSpineShader(usePremultipliedAlpha);
states.Shader = SFMLShader.GetSpineShader(usePma);
// 要用 DrawOrder 而不是 Slots
foreach (var slot in skeleton.DrawOrder)
@@ -324,18 +324,15 @@ namespace SpineViewer.Spine.Implementations.Spine
}
// 似乎 2.1.x 也没有 BlendMode
SFML.Graphics.BlendMode blendMode = slot.Data.AdditiveBlending ? BlendModeSFML.AdditivePma : BlendModeSFML.NormalPma;
SFML.Graphics.BlendMode blendMode = slot.Data.AdditiveBlending ? SFMLBlendMode.AdditivePma : SFMLBlendMode.NormalPma;
states.Texture ??= texture;
if (states.BlendMode != blendMode || states.Texture != texture)
{
if (vertexArray.VertexCount > 0)
if (triangleVertices.VertexCount > 0)
{
// 调试纹理
if (!isDebug || debugTexture)
target.Draw(vertexArray, states);
vertexArray.Clear();
target.Draw(triangleVertices, states);
triangleVertices.Clear();
}
states.BlendMode = blendMode;
states.Texture = texture;
@@ -369,26 +366,190 @@ namespace SpineViewer.Spine.Implementations.Spine
vertex.Position.Y = worldVertices[index + 1];
vertex.TexCoords.X = uvs[index] * textureSizeX;
vertex.TexCoords.Y = uvs[index + 1] * textureSizeY;
vertexArray.Append(vertex);
triangleVertices.Append(vertex);
}
//clipping.ClipEnd(slot);
}
//clipping.ClipEnd();
// 调试纹理
if (!isDebug || debugTexture)
target.Draw(vertexArray, states);
target.Draw(triangleVertices, states);
}
// 包围盒
if (isDebug && isSelected && debugBounds)
protected override void debugDraw(SFML.Graphics.RenderTarget target)
{
lineVertices.Clear();
rectLineVertices.Clear();
if (debugRegions)
{
SFML.Graphics.Vertex vt = new() { Color = AttachmentLineColor };
foreach (var slot in skeleton.Slots)
{
if (slot.Attachment is RegionAttachment regionAttachment)
{
regionAttachment.ComputeWorldVertices(slot.Bone, worldVerticesBuffer);
vt.Position.X = worldVerticesBuffer[0];
vt.Position.Y = worldVerticesBuffer[1];
lineVertices.Append(vt);
vt.Position.X = worldVerticesBuffer[2];
vt.Position.Y = worldVerticesBuffer[3];
lineVertices.Append(vt); lineVertices.Append(vt);
vt.Position.X = worldVerticesBuffer[4];
vt.Position.Y = worldVerticesBuffer[5];
lineVertices.Append(vt); lineVertices.Append(vt);
vt.Position.X = worldVerticesBuffer[6];
vt.Position.Y = worldVerticesBuffer[7];
lineVertices.Append(vt); lineVertices.Append(vt);
vt.Position.X = worldVerticesBuffer[0];
vt.Position.Y = worldVerticesBuffer[1];
lineVertices.Append(vt);
}
}
}
if (debugMeshes)
{
SFML.Graphics.Vertex vt = new() { Color = MeshLineColor };
foreach (var slot in skeleton.Slots)
{
if (slot.Attachment is MeshAttachment meshAttachment)
{
if (meshAttachment.Vertices.Length > worldVerticesBuffer.Length)
worldVerticesBuffer = new float[meshAttachment.Vertices.Length * 2];
meshAttachment.ComputeWorldVertices(slot, worldVerticesBuffer);
var triangleIndices = meshAttachment.Triangles;
for (int i = 0; i < triangleIndices.Length; i += 3)
{
var idx0 = triangleIndices[i] * 2;
var idx1 = triangleIndices[i + 1] * 2;
var idx2 = triangleIndices[i + 2] * 2;
vt.Position.X = worldVerticesBuffer[idx0];
vt.Position.Y = worldVerticesBuffer[idx0 + 1];
lineVertices.Append(vt);
vt.Position.X = worldVerticesBuffer[idx1];
vt.Position.Y = worldVerticesBuffer[idx1 + 1];
lineVertices.Append(vt); lineVertices.Append(vt);
vt.Position.X = worldVerticesBuffer[idx2];
vt.Position.Y = worldVerticesBuffer[idx2 + 1];
lineVertices.Append(vt); lineVertices.Append(vt);
vt.Position.X = worldVerticesBuffer[idx0];
vt.Position.Y = worldVerticesBuffer[idx0 + 1];
lineVertices.Append(vt);
}
}
}
}
if (debugMeshHulls)
{
SFML.Graphics.Vertex vt = new() { Color = AttachmentLineColor };
foreach (var slot in skeleton.Slots)
{
if (slot.Attachment is MeshAttachment meshAttachment)
{
if (meshAttachment.Vertices.Length > worldVerticesBuffer.Length)
worldVerticesBuffer = new float[meshAttachment.Vertices.Length * 2];
meshAttachment.ComputeWorldVertices(slot, worldVerticesBuffer);
var hullLength = (meshAttachment.HullLength >> 1) << 1;
if (debugMeshHulls && hullLength > 2)
{
vt.Position.X = worldVerticesBuffer[0];
vt.Position.Y = worldVerticesBuffer[1];
lineVertices.Append(vt);
for (int i = 2; i < hullLength; i += 2)
{
vt.Position.X = worldVerticesBuffer[i];
vt.Position.Y = worldVerticesBuffer[i + 1];
lineVertices.Append(vt);
lineVertices.Append(vt);
}
vt.Position.X = worldVerticesBuffer[0];
vt.Position.Y = worldVerticesBuffer[1];
lineVertices.Append(vt);
}
}
}
}
if (debugBoundingBoxes)
{
throw new NotImplementedException();
}
if (debugPaths)
{
throw new NotImplementedException();
}
if (debugClippings) { } // 没有剪裁附件
if (debugBounds)
{
var vt = new SFML.Graphics.Vertex() { Color = BoundsColor };
var b = bounds;
boundsVertices[0] = boundsVertices[4] = new(new(b.Left, b.Top), BoundsColor);
boundsVertices[1] = new(new(b.Right, b.Top), BoundsColor);
boundsVertices[2] = new(new(b.Right, b.Bottom), BoundsColor);
boundsVertices[3] = new(new(b.Left, b.Bottom), BoundsColor);
target.Draw(boundsVertices);
vt.Position.X = b.Left;
vt.Position.Y = b.Top;
lineVertices.Append(vt);
vt.Position.X = b.Right;
vt.Position.Y = b.Top;
lineVertices.Append(vt); lineVertices.Append(vt);
vt.Position.X = b.Right;
vt.Position.Y = b.Bottom;
lineVertices.Append(vt); lineVertices.Append(vt);
vt.Position.X = b.Left;
vt.Position.Y = b.Bottom;
lineVertices.Append(vt); lineVertices.Append(vt);
vt.Position.X = b.Left;
vt.Position.Y = b.Top;
lineVertices.Append(vt);
}
// 骨骼线放最后画
if (debugBones)
{
var width = scale;
foreach (var bone in skeleton.Bones)
{
var boneLength = bone.Data.Length;
var p1 = new SFML.System.Vector2f(bone.WorldX, bone.WorldY);
var p2 = new SFML.System.Vector2f(bone.WorldX + boneLength * bone.M00, bone.WorldY + boneLength * bone.M10);
AddRectLine(p1, p2, BoneLineColor, width);
}
}
target.Draw(lineVertices);
target.Draw(rectLineVertices);
// 骨骼的点最后画, 层级处于骨骼线上面
if (debugBones)
{
var radius = scale;
foreach (var bone in skeleton.Bones)
{
DrawCirclePoint(target, new(bone.WorldX, bone.WorldY), BonePointColor, radius);
}
}
}
}

View File

@@ -7,11 +7,12 @@ using System.Linq;
using System.Text;
using System.Threading.Tasks;
using SpineRuntime36;
using SpineViewer.Utils;
namespace SpineViewer.Spine.Implementations.Spine
namespace SpineViewer.Spine.Implementations.SpineObject
{
[SpineImplementation(SpineVersion.V36)]
internal class Spine36 : SpineViewer.Spine.Spine
internal class SpineObject36 : Spine.SpineObject
{
private static readonly Animation EmptyAnimation = new(EMPTY_ANIMATION, [], 0);
@@ -46,7 +47,7 @@ namespace SpineViewer.Spine.Implementations.Spine
private SkeletonClipping clipping = new();
public Spine36(string skelPath, string atlasPath) : base(skelPath, atlasPath)
public SpineObject36(string skelPath, string atlasPath) : base(skelPath, atlasPath)
{
atlas = new Atlas(AtlasPath, textureLoader);
try
@@ -110,15 +111,14 @@ namespace SpineViewer.Spine.Implementations.Spine
var fY = flipY;
var animations = animationState.Tracks.Where(te => te is not null).Select(te => te.Animation.Name).ToArray();
var val = Math.Max(value, SCALE_MIN);
if (skeletonBinary is not null)
{
skeletonBinary.Scale = val;
skeletonBinary.Scale = value;
skeletonData = skeletonBinary.ReadSkeletonData(SkelPath);
}
else if (skeletonJson is not null)
{
skeletonJson.Scale = val;
skeletonJson.Scale = value;
skeletonData = skeletonJson.ReadSkeletonData(SkelPath);
}
@@ -209,19 +209,19 @@ namespace SpineViewer.Spine.Implementations.Spine
{
return spineBlendMode switch
{
BlendMode.Normal => BlendModeSFML.NormalPma,
BlendMode.Additive => BlendModeSFML.AdditivePma,
BlendMode.Multiply => BlendModeSFML.MultiplyPma,
BlendMode.Screen => BlendModeSFML.ScreenPma,
BlendMode.Normal => SFMLBlendMode.NormalPma,
BlendMode.Additive => SFMLBlendMode.AdditivePma,
BlendMode.Multiply => SFMLBlendMode.MultiplyPma,
BlendMode.Screen => SFMLBlendMode.ScreenPma,
_ => throw new NotImplementedException($"{spineBlendMode}"),
};
}
protected override void draw(SFML.Graphics.RenderTarget target, SFML.Graphics.RenderStates states)
{
vertexArray.Clear();
triangleVertices.Clear();
states.Texture = null;
states.Shader = Shader.GetSpineShader(usePremultipliedAlpha);
states.Shader = SFMLShader.GetSpineShader(usePma);
// 要用 DrawOrder 而不是 Slots
foreach (var slot in skeleton.DrawOrder)
@@ -286,13 +286,10 @@ namespace SpineViewer.Spine.Implementations.Spine
states.Texture ??= texture;
if (states.BlendMode != blendMode || states.Texture != texture)
{
if (vertexArray.VertexCount > 0)
if (triangleVertices.VertexCount > 0)
{
// 调试纹理
if (!isDebug || debugTexture)
target.Draw(vertexArray, states);
vertexArray.Clear();
target.Draw(triangleVertices, states);
triangleVertices.Clear();
}
states.BlendMode = blendMode;
states.Texture = texture;
@@ -326,26 +323,219 @@ namespace SpineViewer.Spine.Implementations.Spine
vertex.Position.Y = worldVertices[index + 1];
vertex.TexCoords.X = uvs[index] * textureSizeX;
vertex.TexCoords.Y = uvs[index + 1] * textureSizeY;
vertexArray.Append(vertex);
triangleVertices.Append(vertex);
}
clipping.ClipEnd(slot);
}
clipping.ClipEnd();
// 调试纹理
if (!isDebug || debugTexture)
target.Draw(vertexArray, states);
target.Draw(triangleVertices, states);
}
// 包围盒
if (isDebug && isSelected && debugBounds)
protected override void debugDraw(SFML.Graphics.RenderTarget target)
{
lineVertices.Clear();
rectLineVertices.Clear();
if (debugRegions)
{
SFML.Graphics.Vertex vt = new() { Color = AttachmentLineColor };
foreach (var slot in skeleton.Slots)
{
if (slot.Attachment is RegionAttachment regionAttachment)
{
regionAttachment.ComputeWorldVertices(slot.Bone, worldVerticesBuffer, 0);
vt.Position.X = worldVerticesBuffer[0];
vt.Position.Y = worldVerticesBuffer[1];
lineVertices.Append(vt);
vt.Position.X = worldVerticesBuffer[2];
vt.Position.Y = worldVerticesBuffer[3];
lineVertices.Append(vt); lineVertices.Append(vt);
vt.Position.X = worldVerticesBuffer[4];
vt.Position.Y = worldVerticesBuffer[5];
lineVertices.Append(vt); lineVertices.Append(vt);
vt.Position.X = worldVerticesBuffer[6];
vt.Position.Y = worldVerticesBuffer[7];
lineVertices.Append(vt); lineVertices.Append(vt);
vt.Position.X = worldVerticesBuffer[0];
vt.Position.Y = worldVerticesBuffer[1];
lineVertices.Append(vt);
}
}
}
if (debugMeshes)
{
SFML.Graphics.Vertex vt = new() { Color = MeshLineColor };
foreach (var slot in skeleton.Slots)
{
if (slot.Attachment is MeshAttachment meshAttachment)
{
if (meshAttachment.WorldVerticesLength > worldVerticesBuffer.Length)
worldVerticesBuffer = new float[meshAttachment.WorldVerticesLength * 2];
meshAttachment.ComputeWorldVertices(slot, worldVerticesBuffer);
var triangleIndices = meshAttachment.Triangles;
for (int i = 0; i < triangleIndices.Length; i += 3)
{
var idx0 = triangleIndices[i] * 2;
var idx1 = triangleIndices[i + 1] * 2;
var idx2 = triangleIndices[i + 2] * 2;
vt.Position.X = worldVerticesBuffer[idx0];
vt.Position.Y = worldVerticesBuffer[idx0 + 1];
lineVertices.Append(vt);
vt.Position.X = worldVerticesBuffer[idx1];
vt.Position.Y = worldVerticesBuffer[idx1 + 1];
lineVertices.Append(vt); lineVertices.Append(vt);
vt.Position.X = worldVerticesBuffer[idx2];
vt.Position.Y = worldVerticesBuffer[idx2 + 1];
lineVertices.Append(vt); lineVertices.Append(vt);
vt.Position.X = worldVerticesBuffer[idx0];
vt.Position.Y = worldVerticesBuffer[idx0 + 1];
lineVertices.Append(vt);
}
}
}
}
if (debugMeshHulls)
{
SFML.Graphics.Vertex vt = new() { Color = AttachmentLineColor };
foreach (var slot in skeleton.Slots)
{
if (slot.Attachment is MeshAttachment meshAttachment)
{
if (meshAttachment.WorldVerticesLength > worldVerticesBuffer.Length)
worldVerticesBuffer = new float[meshAttachment.WorldVerticesLength * 2];
meshAttachment.ComputeWorldVertices(slot, worldVerticesBuffer);
var hullLength = (meshAttachment.HullLength >> 1) << 1;
if (debugMeshHulls && hullLength > 2)
{
vt.Position.X = worldVerticesBuffer[0];
vt.Position.Y = worldVerticesBuffer[1];
lineVertices.Append(vt);
for (int i = 2; i < hullLength; i += 2)
{
vt.Position.X = worldVerticesBuffer[i];
vt.Position.Y = worldVerticesBuffer[i + 1];
lineVertices.Append(vt);
lineVertices.Append(vt);
}
vt.Position.X = worldVerticesBuffer[0];
vt.Position.Y = worldVerticesBuffer[1];
lineVertices.Append(vt);
}
}
}
}
if (debugBoundingBoxes)
{
throw new NotImplementedException();
}
if (debugPaths)
{
throw new NotImplementedException();
}
if (debugClippings)
{
SFML.Graphics.Vertex vt = new() { Color = ClippingLineColor };
foreach (var slot in skeleton.Slots)
{
if (slot.Attachment is ClippingAttachment clippingAttachment)
{
if (clippingAttachment.WorldVerticesLength > worldVerticesBuffer.Length)
worldVerticesBuffer = worldVerticesBuffer = new float[clippingAttachment.WorldVerticesLength * 2];
clippingAttachment.ComputeWorldVertices(slot, worldVerticesBuffer);
vt.Position.X = worldVerticesBuffer[0];
vt.Position.Y = worldVerticesBuffer[1];
lineVertices.Append(vt);
for (int i = 2; i < clippingAttachment.WorldVerticesLength; i += 2)
{
vt.Position.X = worldVerticesBuffer[i];
vt.Position.Y = worldVerticesBuffer[i + 1];
lineVertices.Append(vt);
lineVertices.Append(vt);
}
vt.Position.X = worldVerticesBuffer[0];
vt.Position.Y = worldVerticesBuffer[1];
lineVertices.Append(vt);
}
}
}
if (debugBounds)
{
var vt = new SFML.Graphics.Vertex() { Color = BoundsColor };
var b = bounds;
boundsVertices[0] = boundsVertices[4] = new(new(b.Left, b.Top), BoundsColor);
boundsVertices[1] = new(new(b.Right, b.Top), BoundsColor);
boundsVertices[2] = new(new(b.Right, b.Bottom), BoundsColor);
boundsVertices[3] = new(new(b.Left, b.Bottom), BoundsColor);
target.Draw(boundsVertices);
vt.Position.X = b.Left;
vt.Position.Y = b.Top;
lineVertices.Append(vt);
vt.Position.X = b.Right;
vt.Position.Y = b.Top;
lineVertices.Append(vt); lineVertices.Append(vt);
vt.Position.X = b.Right;
vt.Position.Y = b.Bottom;
lineVertices.Append(vt); lineVertices.Append(vt);
vt.Position.X = b.Left;
vt.Position.Y = b.Bottom;
lineVertices.Append(vt); lineVertices.Append(vt);
vt.Position.X = b.Left;
vt.Position.Y = b.Top;
lineVertices.Append(vt);
}
// 骨骼线放最后画
if (debugBones)
{
var width = scale;
foreach (var bone in skeleton.Bones)
{
var boneLength = bone.Data.Length;
var p1 = new SFML.System.Vector2f(bone.WorldX, bone.WorldY);
var p2 = new SFML.System.Vector2f(bone.WorldX + boneLength * bone.A, bone.WorldY + boneLength * bone.C);
AddRectLine(p1, p2, BoneLineColor, width);
}
}
target.Draw(lineVertices);
target.Draw(rectLineVertices);
// 骨骼的点最后画, 层级处于骨骼线上面
if (debugBones)
{
var radius = scale;
foreach (var bone in skeleton.Bones)
{
DrawCirclePoint(target, new(bone.WorldX, bone.WorldY), BonePointColor, radius);
}
}
}
}

View File

@@ -4,11 +4,12 @@ using System.Linq;
using System.Text;
using System.Threading.Tasks;
using SpineRuntime37;
using SpineViewer.Utils;
namespace SpineViewer.Spine.Implementations.Spine
namespace SpineViewer.Spine.Implementations.SpineObject
{
[SpineImplementation(SpineVersion.V37)]
internal class Spine37 : SpineViewer.Spine.Spine
internal class SpineObject37 : Spine.SpineObject
{
private static readonly Animation EmptyAnimation = new(EMPTY_ANIMATION, [], 0);
@@ -44,7 +45,7 @@ namespace SpineViewer.Spine.Implementations.Spine
private SkeletonClipping clipping = new();
public Spine37(string skelPath, string atlasPath) : base(skelPath, atlasPath)
public SpineObject37(string skelPath, string atlasPath) : base(skelPath, atlasPath)
{
atlas = new Atlas(AtlasPath, textureLoader);
try
@@ -180,19 +181,19 @@ namespace SpineViewer.Spine.Implementations.Spine
{
return spineBlendMode switch
{
BlendMode.Normal => BlendModeSFML.NormalPma,
BlendMode.Additive => BlendModeSFML.AdditivePma,
BlendMode.Multiply => BlendModeSFML.MultiplyPma,
BlendMode.Screen => BlendModeSFML.ScreenPma,
BlendMode.Normal => SFMLBlendMode.NormalPma,
BlendMode.Additive => SFMLBlendMode.AdditivePma,
BlendMode.Multiply => SFMLBlendMode.MultiplyPma,
BlendMode.Screen => SFMLBlendMode.ScreenPma,
_ => throw new NotImplementedException($"{spineBlendMode}"),
};
}
protected override void draw(SFML.Graphics.RenderTarget target, SFML.Graphics.RenderStates states)
{
vertexArray.Clear();
triangleVertices.Clear();
states.Texture = null;
states.Shader = Shader.GetSpineShader(usePremultipliedAlpha);
states.Shader = SFMLShader.GetSpineShader(usePma);
// 要用 DrawOrder 而不是 Slots
foreach (var slot in skeleton.DrawOrder)
@@ -257,13 +258,10 @@ namespace SpineViewer.Spine.Implementations.Spine
states.Texture ??= texture;
if (states.BlendMode != blendMode || states.Texture != texture)
{
if (vertexArray.VertexCount > 0)
if (triangleVertices.VertexCount > 0)
{
// 调试纹理
if (!isDebug || debugTexture)
target.Draw(vertexArray, states);
vertexArray.Clear();
target.Draw(triangleVertices, states);
triangleVertices.Clear();
}
states.BlendMode = blendMode;
states.Texture = texture;
@@ -297,26 +295,219 @@ namespace SpineViewer.Spine.Implementations.Spine
vertex.Position.Y = worldVertices[index + 1];
vertex.TexCoords.X = uvs[index] * textureSizeX;
vertex.TexCoords.Y = uvs[index + 1] * textureSizeY;
vertexArray.Append(vertex);
triangleVertices.Append(vertex);
}
clipping.ClipEnd(slot);
}
clipping.ClipEnd();
// 调试纹理
if (!isDebug || debugTexture)
target.Draw(vertexArray, states);
target.Draw(triangleVertices, states);
}
// 包围盒
if (isDebug && isSelected && debugBounds)
protected override void debugDraw(SFML.Graphics.RenderTarget target)
{
lineVertices.Clear();
rectLineVertices.Clear();
if (debugRegions)
{
SFML.Graphics.Vertex vt = new() { Color = AttachmentLineColor };
foreach (var slot in skeleton.Slots)
{
if (slot.Attachment is RegionAttachment regionAttachment)
{
regionAttachment.ComputeWorldVertices(slot.Bone, worldVerticesBuffer, 0);
vt.Position.X = worldVerticesBuffer[0];
vt.Position.Y = worldVerticesBuffer[1];
lineVertices.Append(vt);
vt.Position.X = worldVerticesBuffer[2];
vt.Position.Y = worldVerticesBuffer[3];
lineVertices.Append(vt); lineVertices.Append(vt);
vt.Position.X = worldVerticesBuffer[4];
vt.Position.Y = worldVerticesBuffer[5];
lineVertices.Append(vt); lineVertices.Append(vt);
vt.Position.X = worldVerticesBuffer[6];
vt.Position.Y = worldVerticesBuffer[7];
lineVertices.Append(vt); lineVertices.Append(vt);
vt.Position.X = worldVerticesBuffer[0];
vt.Position.Y = worldVerticesBuffer[1];
lineVertices.Append(vt);
}
}
}
if (debugMeshes)
{
SFML.Graphics.Vertex vt = new() { Color = MeshLineColor };
foreach (var slot in skeleton.Slots)
{
if (slot.Attachment is MeshAttachment meshAttachment)
{
if (meshAttachment.WorldVerticesLength > worldVerticesBuffer.Length)
worldVerticesBuffer = new float[meshAttachment.WorldVerticesLength * 2];
meshAttachment.ComputeWorldVertices(slot, worldVerticesBuffer);
var triangleIndices = meshAttachment.Triangles;
for (int i = 0; i < triangleIndices.Length; i += 3)
{
var idx0 = triangleIndices[i] * 2;
var idx1 = triangleIndices[i + 1] * 2;
var idx2 = triangleIndices[i + 2] * 2;
vt.Position.X = worldVerticesBuffer[idx0];
vt.Position.Y = worldVerticesBuffer[idx0 + 1];
lineVertices.Append(vt);
vt.Position.X = worldVerticesBuffer[idx1];
vt.Position.Y = worldVerticesBuffer[idx1 + 1];
lineVertices.Append(vt); lineVertices.Append(vt);
vt.Position.X = worldVerticesBuffer[idx2];
vt.Position.Y = worldVerticesBuffer[idx2 + 1];
lineVertices.Append(vt); lineVertices.Append(vt);
vt.Position.X = worldVerticesBuffer[idx0];
vt.Position.Y = worldVerticesBuffer[idx0 + 1];
lineVertices.Append(vt);
}
}
}
}
if (debugMeshHulls)
{
SFML.Graphics.Vertex vt = new() { Color = AttachmentLineColor };
foreach (var slot in skeleton.Slots)
{
if (slot.Attachment is MeshAttachment meshAttachment)
{
if (meshAttachment.WorldVerticesLength > worldVerticesBuffer.Length)
worldVerticesBuffer = new float[meshAttachment.WorldVerticesLength * 2];
meshAttachment.ComputeWorldVertices(slot, worldVerticesBuffer);
var hullLength = (meshAttachment.HullLength >> 1) << 1;
if (debugMeshHulls && hullLength > 2)
{
vt.Position.X = worldVerticesBuffer[0];
vt.Position.Y = worldVerticesBuffer[1];
lineVertices.Append(vt);
for (int i = 2; i < hullLength; i += 2)
{
vt.Position.X = worldVerticesBuffer[i];
vt.Position.Y = worldVerticesBuffer[i + 1];
lineVertices.Append(vt);
lineVertices.Append(vt);
}
vt.Position.X = worldVerticesBuffer[0];
vt.Position.Y = worldVerticesBuffer[1];
lineVertices.Append(vt);
}
}
}
}
if (debugBoundingBoxes)
{
throw new NotImplementedException();
}
if (debugPaths)
{
throw new NotImplementedException();
}
if (debugClippings)
{
SFML.Graphics.Vertex vt = new() { Color = ClippingLineColor };
foreach (var slot in skeleton.Slots)
{
if (slot.Attachment is ClippingAttachment clippingAttachment)
{
if (clippingAttachment.WorldVerticesLength > worldVerticesBuffer.Length)
worldVerticesBuffer = worldVerticesBuffer = new float[clippingAttachment.WorldVerticesLength * 2];
clippingAttachment.ComputeWorldVertices(slot, worldVerticesBuffer);
vt.Position.X = worldVerticesBuffer[0];
vt.Position.Y = worldVerticesBuffer[1];
lineVertices.Append(vt);
for (int i = 2; i < clippingAttachment.WorldVerticesLength; i += 2)
{
vt.Position.X = worldVerticesBuffer[i];
vt.Position.Y = worldVerticesBuffer[i + 1];
lineVertices.Append(vt);
lineVertices.Append(vt);
}
vt.Position.X = worldVerticesBuffer[0];
vt.Position.Y = worldVerticesBuffer[1];
lineVertices.Append(vt);
}
}
}
if (debugBounds)
{
var vt = new SFML.Graphics.Vertex() { Color = BoundsColor };
var b = bounds;
boundsVertices[0] = boundsVertices[4] = new(new(b.Left, b.Top), BoundsColor);
boundsVertices[1] = new(new(b.Right, b.Top), BoundsColor);
boundsVertices[2] = new(new(b.Right, b.Bottom), BoundsColor);
boundsVertices[3] = new(new(b.Left, b.Bottom), BoundsColor);
target.Draw(boundsVertices);
vt.Position.X = b.Left;
vt.Position.Y = b.Top;
lineVertices.Append(vt);
vt.Position.X = b.Right;
vt.Position.Y = b.Top;
lineVertices.Append(vt); lineVertices.Append(vt);
vt.Position.X = b.Right;
vt.Position.Y = b.Bottom;
lineVertices.Append(vt); lineVertices.Append(vt);
vt.Position.X = b.Left;
vt.Position.Y = b.Bottom;
lineVertices.Append(vt); lineVertices.Append(vt);
vt.Position.X = b.Left;
vt.Position.Y = b.Top;
lineVertices.Append(vt);
}
// 骨骼线放最后画
if (debugBones)
{
var width = scale;
foreach (var bone in skeleton.Bones)
{
var boneLength = bone.Data.Length;
var p1 = new SFML.System.Vector2f(bone.WorldX, bone.WorldY);
var p2 = new SFML.System.Vector2f(bone.WorldX + boneLength * bone.A, bone.WorldY + boneLength * bone.C);
AddRectLine(p1, p2, BoneLineColor, width);
}
}
target.Draw(lineVertices);
target.Draw(rectLineVertices);
// 骨骼的点最后画, 层级处于骨骼线上面
if (debugBones)
{
var radius = scale;
foreach (var bone in skeleton.Bones)
{
DrawCirclePoint(target, new(bone.WorldX, bone.WorldY), BonePointColor, radius);
}
}
}
}

View File

@@ -7,11 +7,12 @@ using System.Text;
using System.Threading.Tasks;
using SpineRuntime38;
using SpineRuntime38.Attachments;
using SpineViewer.Utils;
namespace SpineViewer.Spine.Implementations.Spine
namespace SpineViewer.Spine.Implementations.SpineObject
{
[SpineImplementation(SpineVersion.V38)]
internal class Spine38 : SpineViewer.Spine.Spine
internal class SpineObject38 : Spine.SpineObject
{
private static readonly Animation EmptyAnimation = new(EMPTY_ANIMATION, [], 0);
@@ -24,7 +25,7 @@ namespace SpineViewer.Spine.Implementations.Spine
texture.Smooth = true;
if (page.uWrap == TextureWrap.Repeat && page.vWrap == TextureWrap.Repeat)
texture.Repeated = true;
page.rendererObject = texture;
// 似乎是不需要设置的, 因为存在某些 png 和 atlas 大小不同的情况, 一般是有一些缩放, 如果设置了反而渲染异常
// page.width = (int)texture.Size.X;
@@ -50,7 +51,7 @@ namespace SpineViewer.Spine.Implementations.Spine
private SkeletonClipping clipping = new();
public Spine38(string skelPath, string atlasPath) : base(skelPath, atlasPath)
public SpineObject38(string skelPath, string atlasPath) : base(skelPath, atlasPath)
{
atlas = new Atlas(AtlasPath, textureLoader);
try
@@ -137,9 +138,11 @@ namespace SpineViewer.Spine.Implementations.Spine
protected override void addSkin(string name)
{
if (!skinNames.Contains(name)) return;
skeleton.Skin.AddSkin(skeletonData.FindSkin(name));
skeleton.SetSlotsToSetupPose();
if (skeletonData.FindSkin(name) is Skin sk)
{
skeleton.Skin.AddSkin(sk);
skeleton.SetSlotsToSetupPose();
}
}
protected override void clearSkin()
@@ -186,19 +189,19 @@ namespace SpineViewer.Spine.Implementations.Spine
{
return spineBlendMode switch
{
BlendMode.Normal => BlendModeSFML.NormalPma,
BlendMode.Additive => BlendModeSFML.AdditivePma,
BlendMode.Multiply => BlendModeSFML.MultiplyPma,
BlendMode.Screen => BlendModeSFML.ScreenPma,
BlendMode.Normal => SFMLBlendMode.NormalPma,
BlendMode.Additive => SFMLBlendMode.AdditivePma,
BlendMode.Multiply => SFMLBlendMode.MultiplyPma,
BlendMode.Screen => SFMLBlendMode.ScreenPma,
_ => throw new NotImplementedException($"{spineBlendMode}"),
};
}
protected override void draw(SFML.Graphics.RenderTarget target, SFML.Graphics.RenderStates states)
{
vertexArray.Clear();
triangleVertices.Clear();
states.Texture = null;
states.Shader = Shader.GetSpineShader(usePremultipliedAlpha);
states.Shader = SFMLShader.GetSpineShader(usePma);
// 要用 DrawOrder 而不是 Slots
foreach (var slot in skeleton.DrawOrder)
@@ -206,7 +209,7 @@ namespace SpineViewer.Spine.Implementations.Spine
var attachment = slot.Attachment;
SFML.Graphics.Texture texture;
float[] worldVertices = worldVerticesBuffer; // 顶点世界坐标, 连续的 [x0, y0, x1, y1, ...] 坐标值
int worldVerticesCount; // 等于顶点数组的长度除以 2
int[] worldTriangleIndices; // 三角形索引, 从顶点坐标数组取的时候要乘以 2, 最大值是 worldVerticesCount - 1
@@ -263,13 +266,10 @@ namespace SpineViewer.Spine.Implementations.Spine
states.Texture ??= texture;
if (states.BlendMode != blendMode || states.Texture != texture)
{
if (vertexArray.VertexCount > 0)
if (triangleVertices.VertexCount > 0)
{
// 调试纹理
if (!isDebug || debugTexture)
target.Draw(vertexArray, states);
vertexArray.Clear();
target.Draw(triangleVertices, states);
triangleVertices.Clear();
}
states.BlendMode = blendMode;
states.Texture = texture;
@@ -303,26 +303,221 @@ namespace SpineViewer.Spine.Implementations.Spine
vertex.Position.Y = worldVertices[index + 1];
vertex.TexCoords.X = uvs[index] * textureSizeX;
vertex.TexCoords.Y = uvs[index + 1] * textureSizeY;
vertexArray.Append(vertex);
triangleVertices.Append(vertex);
}
clipping.ClipEnd(slot);
}
clipping.ClipEnd();
// 调试纹理
if (!isDebug || debugTexture)
target.Draw(vertexArray, states);
target.Draw(triangleVertices, states);
}
// 调试包围盒
if (isDebug && isSelected && debugBounds)
protected override void debugDraw(SFML.Graphics.RenderTarget target)
{
lineVertices.Clear();
rectLineVertices.Clear();
if (debugRegions)
{
SFML.Graphics.Vertex vt = new() { Color = AttachmentLineColor };
foreach (var slot in skeleton.Slots)
{
if (slot.Bone.Active && slot.Attachment is RegionAttachment regionAttachment)
{
regionAttachment.ComputeWorldVertices(slot.Bone, worldVerticesBuffer, 0);
vt.Position.X = worldVerticesBuffer[0];
vt.Position.Y = worldVerticesBuffer[1];
lineVertices.Append(vt);
vt.Position.X = worldVerticesBuffer[2];
vt.Position.Y = worldVerticesBuffer[3];
lineVertices.Append(vt); lineVertices.Append(vt);
vt.Position.X = worldVerticesBuffer[4];
vt.Position.Y = worldVerticesBuffer[5];
lineVertices.Append(vt); lineVertices.Append(vt);
vt.Position.X = worldVerticesBuffer[6];
vt.Position.Y = worldVerticesBuffer[7];
lineVertices.Append(vt); lineVertices.Append(vt);
vt.Position.X = worldVerticesBuffer[0];
vt.Position.Y = worldVerticesBuffer[1];
lineVertices.Append(vt);
}
}
}
if (debugMeshes)
{
SFML.Graphics.Vertex vt = new() { Color = MeshLineColor };
foreach (var slot in skeleton.Slots)
{
if (slot.Bone.Active && slot.Attachment is MeshAttachment meshAttachment)
{
if (meshAttachment.WorldVerticesLength > worldVerticesBuffer.Length)
worldVerticesBuffer = new float[meshAttachment.WorldVerticesLength * 2];
meshAttachment.ComputeWorldVertices(slot, worldVerticesBuffer);
var triangleIndices = meshAttachment.Triangles;
for (int i = 0; i < triangleIndices.Length; i += 3)
{
var idx0 = triangleIndices[i] * 2;
var idx1 = triangleIndices[i + 1] * 2;
var idx2 = triangleIndices[i + 2] * 2;
vt.Position.X = worldVerticesBuffer[idx0];
vt.Position.Y = worldVerticesBuffer[idx0 + 1];
lineVertices.Append(vt);
vt.Position.X = worldVerticesBuffer[idx1];
vt.Position.Y = worldVerticesBuffer[idx1 + 1];
lineVertices.Append(vt); lineVertices.Append(vt);
vt.Position.X = worldVerticesBuffer[idx2];
vt.Position.Y = worldVerticesBuffer[idx2 + 1];
lineVertices.Append(vt); lineVertices.Append(vt);
vt.Position.X = worldVerticesBuffer[idx0];
vt.Position.Y = worldVerticesBuffer[idx0 + 1];
lineVertices.Append(vt);
}
}
}
}
if (debugMeshHulls)
{
SFML.Graphics.Vertex vt = new() { Color = AttachmentLineColor };
foreach (var slot in skeleton.Slots)
{
if (slot.Bone.Active && slot.Attachment is MeshAttachment meshAttachment)
{
if (meshAttachment.WorldVerticesLength > worldVerticesBuffer.Length)
worldVerticesBuffer = new float[meshAttachment.WorldVerticesLength * 2];
meshAttachment.ComputeWorldVertices(slot, worldVerticesBuffer);
var hullLength = (meshAttachment.HullLength >> 1) << 1;
if (debugMeshHulls && hullLength > 2)
{
vt.Position.X = worldVerticesBuffer[0];
vt.Position.Y = worldVerticesBuffer[1];
lineVertices.Append(vt);
for (int i = 2; i < hullLength; i += 2)
{
vt.Position.X = worldVerticesBuffer[i];
vt.Position.Y = worldVerticesBuffer[i + 1];
lineVertices.Append(vt);
lineVertices.Append(vt);
}
vt.Position.X = worldVerticesBuffer[0];
vt.Position.Y = worldVerticesBuffer[1];
lineVertices.Append(vt);
}
}
}
}
if (debugBoundingBoxes)
{
throw new NotImplementedException();
}
if (debugPaths)
{
throw new NotImplementedException();
}
if (debugClippings)
{
SFML.Graphics.Vertex vt = new() { Color = ClippingLineColor };
foreach (var slot in skeleton.Slots)
{
if (slot.Bone.Active && slot.Attachment is ClippingAttachment clippingAttachment)
{
if (clippingAttachment.WorldVerticesLength > worldVerticesBuffer.Length)
worldVerticesBuffer = worldVerticesBuffer = new float[clippingAttachment.WorldVerticesLength * 2];
clippingAttachment.ComputeWorldVertices(slot, worldVerticesBuffer);
vt.Position.X = worldVerticesBuffer[0];
vt.Position.Y = worldVerticesBuffer[1];
lineVertices.Append(vt);
for (int i = 2; i < clippingAttachment.WorldVerticesLength; i += 2)
{
vt.Position.X = worldVerticesBuffer[i];
vt.Position.Y = worldVerticesBuffer[i + 1];
lineVertices.Append(vt);
lineVertices.Append(vt);
}
vt.Position.X = worldVerticesBuffer[0];
vt.Position.Y = worldVerticesBuffer[1];
lineVertices.Append(vt);
}
}
}
if (debugBounds)
{
var vt = new SFML.Graphics.Vertex() { Color = BoundsColor };
var b = bounds;
boundsVertices[0] = boundsVertices[4] = new(new(b.Left, b.Top), BoundsColor);
boundsVertices[1] = new(new(b.Right, b.Top), BoundsColor);
boundsVertices[2] = new(new(b.Right, b.Bottom), BoundsColor);
boundsVertices[3] = new(new(b.Left, b.Bottom), BoundsColor);
target.Draw(boundsVertices);
vt.Position.X = b.Left;
vt.Position.Y = b.Top;
lineVertices.Append(vt);
vt.Position.X = b.Right;
vt.Position.Y = b.Top;
lineVertices.Append(vt); lineVertices.Append(vt);
vt.Position.X = b.Right;
vt.Position.Y = b.Bottom;
lineVertices.Append(vt); lineVertices.Append(vt);
vt.Position.X = b.Left;
vt.Position.Y = b.Bottom;
lineVertices.Append(vt); lineVertices.Append(vt);
vt.Position.X = b.Left;
vt.Position.Y = b.Top;
lineVertices.Append(vt);
}
// 骨骼线放最后画
if (debugBones)
{
var width = scale;
foreach (var bone in skeleton.Bones)
{
if (!bone.Active) continue;
var boneLength = bone.Data.Length;
var p1 = new SFML.System.Vector2f(bone.WorldX, bone.WorldY);
var p2 = new SFML.System.Vector2f(bone.WorldX + boneLength * bone.A, bone.WorldY + boneLength * bone.C);
AddRectLine(p1, p2, BoneLineColor, width);
}
}
target.Draw(lineVertices);
target.Draw(rectLineVertices);
// 骨骼的点最后画, 层级处于骨骼线上面
if (debugBones)
{
var radius = scale;
foreach (var bone in skeleton.Bones)
{
if (!bone.Active) continue;
DrawCirclePoint(target, new(bone.WorldX, bone.WorldY), BonePointColor, radius);
}
}
}
}

View File

@@ -6,11 +6,12 @@ using System.Linq;
using System.Text;
using System.Threading.Tasks;
using SpineRuntime40;
using SpineViewer.Utils;
namespace SpineViewer.Spine.Implementations.Spine
namespace SpineViewer.Spine.Implementations.SpineObject
{
[SpineImplementation(SpineVersion.V40)]
internal class Spine40 : SpineViewer.Spine.Spine
internal class SpineObject40 : Spine.SpineObject
{
private static readonly Animation EmptyAnimation = new(EMPTY_ANIMATION, [], 0);
@@ -46,7 +47,7 @@ namespace SpineViewer.Spine.Implementations.Spine
private SkeletonClipping clipping = new();
public Spine40(string skelPath, string atlasPath) : base(skelPath, atlasPath)
public SpineObject40(string skelPath, string atlasPath) : base(skelPath, atlasPath)
{
atlas = new Atlas(AtlasPath, textureLoader);
try
@@ -133,9 +134,11 @@ namespace SpineViewer.Spine.Implementations.Spine
protected override void addSkin(string name)
{
if (!skinNames.Contains(name)) return;
skeleton.Skin.AddSkin(skeletonData.FindSkin(name));
skeleton.SetSlotsToSetupPose();
if (skeletonData.FindSkin(name) is Skin sk)
{
skeleton.Skin.AddSkin(sk);
skeleton.SetSlotsToSetupPose();
}
}
protected override void clearSkin()
@@ -182,19 +185,19 @@ namespace SpineViewer.Spine.Implementations.Spine
{
return spineBlendMode switch
{
BlendMode.Normal => BlendModeSFML.NormalPma,
BlendMode.Additive => BlendModeSFML.AdditivePma,
BlendMode.Multiply => BlendModeSFML.MultiplyPma,
BlendMode.Screen => BlendModeSFML.ScreenPma,
BlendMode.Normal => SFMLBlendMode.NormalPma,
BlendMode.Additive => SFMLBlendMode.AdditivePma,
BlendMode.Multiply => SFMLBlendMode.MultiplyPma,
BlendMode.Screen => SFMLBlendMode.ScreenPma,
_ => throw new NotImplementedException($"{spineBlendMode}"),
};
}
protected override void draw(SFML.Graphics.RenderTarget target, SFML.Graphics.RenderStates states)
{
vertexArray.Clear();
triangleVertices.Clear();
states.Texture = null;
states.Shader = Shader.GetSpineShader(usePremultipliedAlpha);
states.Shader = SFMLShader.GetSpineShader(usePma);
// 要用 DrawOrder 而不是 Slots
foreach (var slot in skeleton.DrawOrder)
@@ -259,13 +262,10 @@ namespace SpineViewer.Spine.Implementations.Spine
states.Texture ??= texture;
if (states.BlendMode != blendMode || states.Texture != texture)
{
if (vertexArray.VertexCount > 0)
if (triangleVertices.VertexCount > 0)
{
// 调试纹理
if (!isDebug || debugTexture)
target.Draw(vertexArray, states);
vertexArray.Clear();
target.Draw(triangleVertices, states);
triangleVertices.Clear();
}
states.BlendMode = blendMode;
states.Texture = texture;
@@ -299,26 +299,221 @@ namespace SpineViewer.Spine.Implementations.Spine
vertex.Position.Y = worldVertices[index + 1];
vertex.TexCoords.X = uvs[index] * textureSizeX;
vertex.TexCoords.Y = uvs[index + 1] * textureSizeY;
vertexArray.Append(vertex);
triangleVertices.Append(vertex);
}
clipping.ClipEnd(slot);
}
clipping.ClipEnd();
// 调试纹理
if (!isDebug || debugTexture)
target.Draw(vertexArray, states);
target.Draw(triangleVertices, states);
}
// 包围盒
if (isDebug && isSelected && debugBounds)
protected override void debugDraw(SFML.Graphics.RenderTarget target)
{
lineVertices.Clear();
rectLineVertices.Clear();
if (debugRegions)
{
SFML.Graphics.Vertex vt = new() { Color = AttachmentLineColor };
foreach (var slot in skeleton.Slots)
{
if (slot.Bone.Active && slot.Attachment is RegionAttachment regionAttachment)
{
regionAttachment.ComputeWorldVertices(slot.Bone, worldVerticesBuffer, 0);
vt.Position.X = worldVerticesBuffer[0];
vt.Position.Y = worldVerticesBuffer[1];
lineVertices.Append(vt);
vt.Position.X = worldVerticesBuffer[2];
vt.Position.Y = worldVerticesBuffer[3];
lineVertices.Append(vt); lineVertices.Append(vt);
vt.Position.X = worldVerticesBuffer[4];
vt.Position.Y = worldVerticesBuffer[5];
lineVertices.Append(vt); lineVertices.Append(vt);
vt.Position.X = worldVerticesBuffer[6];
vt.Position.Y = worldVerticesBuffer[7];
lineVertices.Append(vt); lineVertices.Append(vt);
vt.Position.X = worldVerticesBuffer[0];
vt.Position.Y = worldVerticesBuffer[1];
lineVertices.Append(vt);
}
}
}
if (debugMeshes)
{
SFML.Graphics.Vertex vt = new() { Color = MeshLineColor };
foreach (var slot in skeleton.Slots)
{
if (slot.Bone.Active && slot.Attachment is MeshAttachment meshAttachment)
{
if (meshAttachment.WorldVerticesLength > worldVerticesBuffer.Length)
worldVerticesBuffer = new float[meshAttachment.WorldVerticesLength * 2];
meshAttachment.ComputeWorldVertices(slot, worldVerticesBuffer);
var triangleIndices = meshAttachment.Triangles;
for (int i = 0; i < triangleIndices.Length; i += 3)
{
var idx0 = triangleIndices[i] * 2;
var idx1 = triangleIndices[i + 1] * 2;
var idx2 = triangleIndices[i + 2] * 2;
vt.Position.X = worldVerticesBuffer[idx0];
vt.Position.Y = worldVerticesBuffer[idx0 + 1];
lineVertices.Append(vt);
vt.Position.X = worldVerticesBuffer[idx1];
vt.Position.Y = worldVerticesBuffer[idx1 + 1];
lineVertices.Append(vt); lineVertices.Append(vt);
vt.Position.X = worldVerticesBuffer[idx2];
vt.Position.Y = worldVerticesBuffer[idx2 + 1];
lineVertices.Append(vt); lineVertices.Append(vt);
vt.Position.X = worldVerticesBuffer[idx0];
vt.Position.Y = worldVerticesBuffer[idx0 + 1];
lineVertices.Append(vt);
}
}
}
}
if (debugMeshHulls)
{
SFML.Graphics.Vertex vt = new() { Color = AttachmentLineColor };
foreach (var slot in skeleton.Slots)
{
if (slot.Bone.Active && slot.Attachment is MeshAttachment meshAttachment)
{
if (meshAttachment.WorldVerticesLength > worldVerticesBuffer.Length)
worldVerticesBuffer = new float[meshAttachment.WorldVerticesLength * 2];
meshAttachment.ComputeWorldVertices(slot, worldVerticesBuffer);
var hullLength = (meshAttachment.HullLength >> 1) << 1;
if (debugMeshHulls && hullLength > 2)
{
vt.Position.X = worldVerticesBuffer[0];
vt.Position.Y = worldVerticesBuffer[1];
lineVertices.Append(vt);
for (int i = 2; i < hullLength; i += 2)
{
vt.Position.X = worldVerticesBuffer[i];
vt.Position.Y = worldVerticesBuffer[i + 1];
lineVertices.Append(vt);
lineVertices.Append(vt);
}
vt.Position.X = worldVerticesBuffer[0];
vt.Position.Y = worldVerticesBuffer[1];
lineVertices.Append(vt);
}
}
}
}
if (debugBoundingBoxes)
{
throw new NotImplementedException();
}
if (debugPaths)
{
throw new NotImplementedException();
}
if (debugClippings)
{
SFML.Graphics.Vertex vt = new() { Color = ClippingLineColor };
foreach (var slot in skeleton.Slots)
{
if (slot.Bone.Active && slot.Attachment is ClippingAttachment clippingAttachment)
{
if (clippingAttachment.WorldVerticesLength > worldVerticesBuffer.Length)
worldVerticesBuffer = worldVerticesBuffer = new float[clippingAttachment.WorldVerticesLength * 2];
clippingAttachment.ComputeWorldVertices(slot, worldVerticesBuffer);
vt.Position.X = worldVerticesBuffer[0];
vt.Position.Y = worldVerticesBuffer[1];
lineVertices.Append(vt);
for (int i = 2; i < clippingAttachment.WorldVerticesLength; i += 2)
{
vt.Position.X = worldVerticesBuffer[i];
vt.Position.Y = worldVerticesBuffer[i + 1];
lineVertices.Append(vt);
lineVertices.Append(vt);
}
vt.Position.X = worldVerticesBuffer[0];
vt.Position.Y = worldVerticesBuffer[1];
lineVertices.Append(vt);
}
}
}
if (debugBounds)
{
var vt = new SFML.Graphics.Vertex() { Color = BoundsColor };
var b = bounds;
boundsVertices[0] = boundsVertices[4] = new(new(b.Left, b.Top), BoundsColor);
boundsVertices[1] = new(new(b.Right, b.Top), BoundsColor);
boundsVertices[2] = new(new(b.Right, b.Bottom), BoundsColor);
boundsVertices[3] = new(new(b.Left, b.Bottom), BoundsColor);
target.Draw(boundsVertices);
vt.Position.X = b.Left;
vt.Position.Y = b.Top;
lineVertices.Append(vt);
vt.Position.X = b.Right;
vt.Position.Y = b.Top;
lineVertices.Append(vt); lineVertices.Append(vt);
vt.Position.X = b.Right;
vt.Position.Y = b.Bottom;
lineVertices.Append(vt); lineVertices.Append(vt);
vt.Position.X = b.Left;
vt.Position.Y = b.Bottom;
lineVertices.Append(vt); lineVertices.Append(vt);
vt.Position.X = b.Left;
vt.Position.Y = b.Top;
lineVertices.Append(vt);
}
// 骨骼线放最后画
if (debugBones)
{
var width = scale;
foreach (var bone in skeleton.Bones)
{
if (!bone.Active) continue;
var boneLength = bone.Data.Length;
var p1 = new SFML.System.Vector2f(bone.WorldX, bone.WorldY);
var p2 = new SFML.System.Vector2f(bone.WorldX + boneLength * bone.A, bone.WorldY + boneLength * bone.C);
AddRectLine(p1, p2, BoneLineColor, width);
}
}
target.Draw(lineVertices);
target.Draw(rectLineVertices);
// 骨骼的点最后画, 层级处于骨骼线上面
if (debugBones)
{
var radius = scale;
foreach (var bone in skeleton.Bones)
{
if (!bone.Active) continue;
DrawCirclePoint(target, new(bone.WorldX, bone.WorldY), BonePointColor, radius);
}
}
}
}

View File

@@ -6,11 +6,12 @@ using System.Linq;
using System.Text;
using System.Threading.Tasks;
using SpineRuntime41;
using SpineViewer.Utils;
namespace SpineViewer.Spine.Implementations.Spine
namespace SpineViewer.Spine.Implementations.SpineObject
{
[SpineImplementation(SpineVersion.V41)]
internal class Spine41 : SpineViewer.Spine.Spine
internal class SpineObject41 : Spine.SpineObject
{
private static readonly Animation EmptyAnimation = new(EMPTY_ANIMATION, [], 0);
@@ -46,7 +47,7 @@ namespace SpineViewer.Spine.Implementations.Spine
private SkeletonClipping clipping = new();
public Spine41(string skelPath, string atlasPath) : base(skelPath, atlasPath)
public SpineObject41(string skelPath, string atlasPath) : base(skelPath, atlasPath)
{
atlas = new Atlas(AtlasPath, textureLoader);
try
@@ -133,9 +134,11 @@ namespace SpineViewer.Spine.Implementations.Spine
protected override void addSkin(string name)
{
if (!skinNames.Contains(name)) return;
skeleton.Skin.AddSkin(skeletonData.FindSkin(name));
skeleton.SetSlotsToSetupPose();
if (skeletonData.FindSkin(name) is Skin sk)
{
skeleton.Skin.AddSkin(sk);
skeleton.SetSlotsToSetupPose();
}
}
protected override void clearSkin()
@@ -182,19 +185,19 @@ namespace SpineViewer.Spine.Implementations.Spine
{
return spineBlendMode switch
{
BlendMode.Normal => BlendModeSFML.NormalPma,
BlendMode.Additive => BlendModeSFML.AdditivePma,
BlendMode.Multiply => BlendModeSFML.MultiplyPma,
BlendMode.Screen => BlendModeSFML.ScreenPma,
BlendMode.Normal => SFMLBlendMode.NormalPma,
BlendMode.Additive => SFMLBlendMode.AdditivePma,
BlendMode.Multiply => SFMLBlendMode.MultiplyPma,
BlendMode.Screen => SFMLBlendMode.ScreenPma,
_ => throw new NotImplementedException($"{spineBlendMode}"),
};
}
protected override void draw(SFML.Graphics.RenderTarget target, SFML.Graphics.RenderStates states)
{
vertexArray.Clear();
triangleVertices.Clear();
states.Texture = null;
states.Shader = Shader.GetSpineShader(usePremultipliedAlpha);
states.Shader = SFMLShader.GetSpineShader(usePma);
// 要用 DrawOrder 而不是 Slots
foreach (var slot in skeleton.DrawOrder)
@@ -259,13 +262,10 @@ namespace SpineViewer.Spine.Implementations.Spine
states.Texture ??= texture;
if (states.BlendMode != blendMode || states.Texture != texture)
{
if (vertexArray.VertexCount > 0)
if (triangleVertices.VertexCount > 0)
{
// 调试纹理
if (!isDebug || debugTexture)
target.Draw(vertexArray, states);
vertexArray.Clear();
target.Draw(triangleVertices, states);
triangleVertices.Clear();
}
states.BlendMode = blendMode;
states.Texture = texture;
@@ -299,26 +299,221 @@ namespace SpineViewer.Spine.Implementations.Spine
vertex.Position.Y = worldVertices[index + 1];
vertex.TexCoords.X = uvs[index] * textureSizeX;
vertex.TexCoords.Y = uvs[index + 1] * textureSizeY;
vertexArray.Append(vertex);
triangleVertices.Append(vertex);
}
clipping.ClipEnd(slot);
}
clipping.ClipEnd();
// 调试纹理
if (!isDebug || debugTexture)
target.Draw(vertexArray, states);
target.Draw(triangleVertices, states);
}
// 包围盒
if (isDebug && isSelected && debugBounds)
protected override void debugDraw(SFML.Graphics.RenderTarget target)
{
lineVertices.Clear();
rectLineVertices.Clear();
if (debugRegions)
{
SFML.Graphics.Vertex vt = new() { Color = AttachmentLineColor };
foreach (var slot in skeleton.Slots)
{
if (slot.Bone.Active && slot.Attachment is RegionAttachment regionAttachment)
{
regionAttachment.ComputeWorldVertices(slot, worldVerticesBuffer, 0);
vt.Position.X = worldVerticesBuffer[0];
vt.Position.Y = worldVerticesBuffer[1];
lineVertices.Append(vt);
vt.Position.X = worldVerticesBuffer[2];
vt.Position.Y = worldVerticesBuffer[3];
lineVertices.Append(vt); lineVertices.Append(vt);
vt.Position.X = worldVerticesBuffer[4];
vt.Position.Y = worldVerticesBuffer[5];
lineVertices.Append(vt); lineVertices.Append(vt);
vt.Position.X = worldVerticesBuffer[6];
vt.Position.Y = worldVerticesBuffer[7];
lineVertices.Append(vt); lineVertices.Append(vt);
vt.Position.X = worldVerticesBuffer[0];
vt.Position.Y = worldVerticesBuffer[1];
lineVertices.Append(vt);
}
}
}
if (debugMeshes)
{
SFML.Graphics.Vertex vt = new() { Color = MeshLineColor };
foreach (var slot in skeleton.Slots)
{
if (slot.Bone.Active && slot.Attachment is MeshAttachment meshAttachment)
{
if (meshAttachment.WorldVerticesLength > worldVerticesBuffer.Length)
worldVerticesBuffer = new float[meshAttachment.WorldVerticesLength * 2];
meshAttachment.ComputeWorldVertices(slot, worldVerticesBuffer);
var triangleIndices = meshAttachment.Triangles;
for (int i = 0; i < triangleIndices.Length; i += 3)
{
var idx0 = triangleIndices[i] * 2;
var idx1 = triangleIndices[i + 1] * 2;
var idx2 = triangleIndices[i + 2] * 2;
vt.Position.X = worldVerticesBuffer[idx0];
vt.Position.Y = worldVerticesBuffer[idx0 + 1];
lineVertices.Append(vt);
vt.Position.X = worldVerticesBuffer[idx1];
vt.Position.Y = worldVerticesBuffer[idx1 + 1];
lineVertices.Append(vt); lineVertices.Append(vt);
vt.Position.X = worldVerticesBuffer[idx2];
vt.Position.Y = worldVerticesBuffer[idx2 + 1];
lineVertices.Append(vt); lineVertices.Append(vt);
vt.Position.X = worldVerticesBuffer[idx0];
vt.Position.Y = worldVerticesBuffer[idx0 + 1];
lineVertices.Append(vt);
}
}
}
}
if (debugMeshHulls)
{
SFML.Graphics.Vertex vt = new() { Color = AttachmentLineColor };
foreach (var slot in skeleton.Slots)
{
if (slot.Bone.Active && slot.Attachment is MeshAttachment meshAttachment)
{
if (meshAttachment.WorldVerticesLength > worldVerticesBuffer.Length)
worldVerticesBuffer = new float[meshAttachment.WorldVerticesLength * 2];
meshAttachment.ComputeWorldVertices(slot, worldVerticesBuffer);
var hullLength = (meshAttachment.HullLength >> 1) << 1;
if (debugMeshHulls && hullLength > 2)
{
vt.Position.X = worldVerticesBuffer[0];
vt.Position.Y = worldVerticesBuffer[1];
lineVertices.Append(vt);
for (int i = 2; i < hullLength; i += 2)
{
vt.Position.X = worldVerticesBuffer[i];
vt.Position.Y = worldVerticesBuffer[i + 1];
lineVertices.Append(vt);
lineVertices.Append(vt);
}
vt.Position.X = worldVerticesBuffer[0];
vt.Position.Y = worldVerticesBuffer[1];
lineVertices.Append(vt);
}
}
}
}
if (debugBoundingBoxes)
{
throw new NotImplementedException();
}
if (debugPaths)
{
throw new NotImplementedException();
}
if (debugClippings)
{
SFML.Graphics.Vertex vt = new() { Color = ClippingLineColor };
foreach (var slot in skeleton.Slots)
{
if (slot.Bone.Active && slot.Attachment is ClippingAttachment clippingAttachment)
{
if (clippingAttachment.WorldVerticesLength > worldVerticesBuffer.Length)
worldVerticesBuffer = worldVerticesBuffer = new float[clippingAttachment.WorldVerticesLength * 2];
clippingAttachment.ComputeWorldVertices(slot, worldVerticesBuffer);
vt.Position.X = worldVerticesBuffer[0];
vt.Position.Y = worldVerticesBuffer[1];
lineVertices.Append(vt);
for (int i = 2; i < clippingAttachment.WorldVerticesLength; i += 2)
{
vt.Position.X = worldVerticesBuffer[i];
vt.Position.Y = worldVerticesBuffer[i + 1];
lineVertices.Append(vt);
lineVertices.Append(vt);
}
vt.Position.X = worldVerticesBuffer[0];
vt.Position.Y = worldVerticesBuffer[1];
lineVertices.Append(vt);
}
}
}
if (debugBounds)
{
var vt = new SFML.Graphics.Vertex() { Color = BoundsColor };
var b = bounds;
boundsVertices[0] = boundsVertices[4] = new(new(b.Left, b.Top), BoundsColor);
boundsVertices[1] = new(new(b.Right, b.Top), BoundsColor);
boundsVertices[2] = new(new(b.Right, b.Bottom), BoundsColor);
boundsVertices[3] = new(new(b.Left, b.Bottom), BoundsColor);
target.Draw(boundsVertices);
vt.Position.X = b.Left;
vt.Position.Y = b.Top;
lineVertices.Append(vt);
vt.Position.X = b.Right;
vt.Position.Y = b.Top;
lineVertices.Append(vt); lineVertices.Append(vt);
vt.Position.X = b.Right;
vt.Position.Y = b.Bottom;
lineVertices.Append(vt); lineVertices.Append(vt);
vt.Position.X = b.Left;
vt.Position.Y = b.Bottom;
lineVertices.Append(vt); lineVertices.Append(vt);
vt.Position.X = b.Left;
vt.Position.Y = b.Top;
lineVertices.Append(vt);
}
// 骨骼线放最后画
if (debugBones)
{
var width = scale;
foreach (var bone in skeleton.Bones)
{
if (!bone.Active) continue;
var boneLength = bone.Data.Length;
var p1 = new SFML.System.Vector2f(bone.WorldX, bone.WorldY);
var p2 = new SFML.System.Vector2f(bone.WorldX + boneLength * bone.A, bone.WorldY + boneLength * bone.C);
AddRectLine(p1, p2, BoneLineColor, width);
}
}
target.Draw(lineVertices);
target.Draw(rectLineVertices);
// 骨骼的点最后画, 层级处于骨骼线上面
if (debugBones)
{
var radius = scale;
foreach (var bone in skeleton.Bones)
{
if (!bone.Active) continue;
DrawCirclePoint(target, new(bone.WorldX, bone.WorldY), BonePointColor, radius);
}
}
}
}

View File

@@ -6,11 +6,12 @@ using System.Linq;
using System.Text;
using System.Threading.Tasks;
using SpineRuntime42;
using SpineViewer.Utils;
namespace SpineViewer.Spine.Implementations.Spine
namespace SpineViewer.Spine.Implementations.SpineObject
{
[SpineImplementation(SpineVersion.V42)]
internal class Spine42 : SpineViewer.Spine.Spine
internal class Spineobject42 : Spine.SpineObject
{
private static readonly Animation EmptyAnimation = new(EMPTY_ANIMATION, [], 0);
@@ -46,7 +47,7 @@ namespace SpineViewer.Spine.Implementations.Spine
private SkeletonClipping clipping = new();
public Spine42(string skelPath, string atlasPath) : base(skelPath, atlasPath)
public Spineobject42(string skelPath, string atlasPath) : base(skelPath, atlasPath)
{
atlas = new Atlas(AtlasPath, textureLoader);
try
@@ -133,9 +134,11 @@ namespace SpineViewer.Spine.Implementations.Spine
protected override void addSkin(string name)
{
if (!skinNames.Contains(name)) return;
skeleton.Skin.AddSkin(skeletonData.FindSkin(name));
skeleton.SetSlotsToSetupPose();
if (skeletonData.FindSkin(name) is Skin sk)
{
skeleton.Skin.AddSkin(sk);
skeleton.SetSlotsToSetupPose();
}
}
protected override void clearSkin()
@@ -182,19 +185,19 @@ namespace SpineViewer.Spine.Implementations.Spine
{
return spineBlendMode switch
{
BlendMode.Normal => BlendModeSFML.NormalPma,
BlendMode.Additive => BlendModeSFML.AdditivePma,
BlendMode.Multiply => BlendModeSFML.MultiplyPma,
BlendMode.Screen => BlendModeSFML.ScreenPma,
BlendMode.Normal => SFMLBlendMode.NormalPma,
BlendMode.Additive => SFMLBlendMode.AdditivePma,
BlendMode.Multiply => SFMLBlendMode.MultiplyPma,
BlendMode.Screen => SFMLBlendMode.ScreenPma,
_ => throw new NotImplementedException($"{spineBlendMode}"),
};
}
protected override void draw(SFML.Graphics.RenderTarget target, SFML.Graphics.RenderStates states)
{
vertexArray.Clear();
triangleVertices.Clear();
states.Texture = null;
states.Shader = Shader.GetSpineShader(usePremultipliedAlpha);
states.Shader = SFMLShader.GetSpineShader(usePma);
// 要用 DrawOrder 而不是 Slots
foreach (var slot in skeleton.DrawOrder)
@@ -259,13 +262,10 @@ namespace SpineViewer.Spine.Implementations.Spine
states.Texture ??= texture;
if (states.BlendMode != blendMode || states.Texture != texture)
{
if (vertexArray.VertexCount > 0)
if (triangleVertices.VertexCount > 0)
{
// 调试纹理
if (!isDebug || debugTexture)
target.Draw(vertexArray, states);
vertexArray.Clear();
target.Draw(triangleVertices, states);
triangleVertices.Clear();
}
states.BlendMode = blendMode;
states.Texture = texture;
@@ -299,26 +299,221 @@ namespace SpineViewer.Spine.Implementations.Spine
vertex.Position.Y = worldVertices[index + 1];
vertex.TexCoords.X = uvs[index] * textureSizeX;
vertex.TexCoords.Y = uvs[index + 1] * textureSizeY;
vertexArray.Append(vertex);
triangleVertices.Append(vertex);
}
clipping.ClipEnd(slot);
}
clipping.ClipEnd();
// 调试纹理
if (!isDebug || debugTexture)
target.Draw(vertexArray, states);
target.Draw(triangleVertices, states);
}
// 包围盒
if (isDebug && isSelected && debugBounds)
protected override void debugDraw(SFML.Graphics.RenderTarget target)
{
lineVertices.Clear();
rectLineVertices.Clear();
if (debugRegions)
{
SFML.Graphics.Vertex vt = new() { Color = AttachmentLineColor };
foreach (var slot in skeleton.Slots)
{
if (slot.Bone.Active && slot.Attachment is RegionAttachment regionAttachment)
{
regionAttachment.ComputeWorldVertices(slot, worldVerticesBuffer, 0);
vt.Position.X = worldVerticesBuffer[0];
vt.Position.Y = worldVerticesBuffer[1];
lineVertices.Append(vt);
vt.Position.X = worldVerticesBuffer[2];
vt.Position.Y = worldVerticesBuffer[3];
lineVertices.Append(vt); lineVertices.Append(vt);
vt.Position.X = worldVerticesBuffer[4];
vt.Position.Y = worldVerticesBuffer[5];
lineVertices.Append(vt); lineVertices.Append(vt);
vt.Position.X = worldVerticesBuffer[6];
vt.Position.Y = worldVerticesBuffer[7];
lineVertices.Append(vt); lineVertices.Append(vt);
vt.Position.X = worldVerticesBuffer[0];
vt.Position.Y = worldVerticesBuffer[1];
lineVertices.Append(vt);
}
}
}
if (debugMeshes)
{
SFML.Graphics.Vertex vt = new() { Color = MeshLineColor };
foreach (var slot in skeleton.Slots)
{
if (slot.Bone.Active && slot.Attachment is MeshAttachment meshAttachment)
{
if (meshAttachment.WorldVerticesLength > worldVerticesBuffer.Length)
worldVerticesBuffer = new float[meshAttachment.WorldVerticesLength * 2];
meshAttachment.ComputeWorldVertices(slot, worldVerticesBuffer);
var triangleIndices = meshAttachment.Triangles;
for (int i = 0; i < triangleIndices.Length; i += 3)
{
var idx0 = triangleIndices[i] * 2;
var idx1 = triangleIndices[i + 1] * 2;
var idx2 = triangleIndices[i + 2] * 2;
vt.Position.X = worldVerticesBuffer[idx0];
vt.Position.Y = worldVerticesBuffer[idx0 + 1];
lineVertices.Append(vt);
vt.Position.X = worldVerticesBuffer[idx1];
vt.Position.Y = worldVerticesBuffer[idx1 + 1];
lineVertices.Append(vt); lineVertices.Append(vt);
vt.Position.X = worldVerticesBuffer[idx2];
vt.Position.Y = worldVerticesBuffer[idx2 + 1];
lineVertices.Append(vt); lineVertices.Append(vt);
vt.Position.X = worldVerticesBuffer[idx0];
vt.Position.Y = worldVerticesBuffer[idx0 + 1];
lineVertices.Append(vt);
}
}
}
}
if (debugMeshHulls)
{
SFML.Graphics.Vertex vt = new() { Color = AttachmentLineColor };
foreach (var slot in skeleton.Slots)
{
if (slot.Bone.Active && slot.Attachment is MeshAttachment meshAttachment)
{
if (meshAttachment.WorldVerticesLength > worldVerticesBuffer.Length)
worldVerticesBuffer = new float[meshAttachment.WorldVerticesLength * 2];
meshAttachment.ComputeWorldVertices(slot, worldVerticesBuffer);
var hullLength = (meshAttachment.HullLength >> 1) << 1;
if (debugMeshHulls && hullLength > 2)
{
vt.Position.X = worldVerticesBuffer[0];
vt.Position.Y = worldVerticesBuffer[1];
lineVertices.Append(vt);
for (int i = 2; i < hullLength; i += 2)
{
vt.Position.X = worldVerticesBuffer[i];
vt.Position.Y = worldVerticesBuffer[i + 1];
lineVertices.Append(vt);
lineVertices.Append(vt);
}
vt.Position.X = worldVerticesBuffer[0];
vt.Position.Y = worldVerticesBuffer[1];
lineVertices.Append(vt);
}
}
}
}
if (debugBoundingBoxes)
{
throw new NotImplementedException();
}
if (debugPaths)
{
throw new NotImplementedException();
}
if (debugClippings)
{
SFML.Graphics.Vertex vt = new() { Color = ClippingLineColor };
foreach (var slot in skeleton.Slots)
{
if (slot.Bone.Active && slot.Attachment is ClippingAttachment clippingAttachment)
{
if (clippingAttachment.WorldVerticesLength > worldVerticesBuffer.Length)
worldVerticesBuffer = worldVerticesBuffer = new float[clippingAttachment.WorldVerticesLength * 2];
clippingAttachment.ComputeWorldVertices(slot, worldVerticesBuffer);
vt.Position.X = worldVerticesBuffer[0];
vt.Position.Y = worldVerticesBuffer[1];
lineVertices.Append(vt);
for (int i = 2; i < clippingAttachment.WorldVerticesLength; i += 2)
{
vt.Position.X = worldVerticesBuffer[i];
vt.Position.Y = worldVerticesBuffer[i + 1];
lineVertices.Append(vt);
lineVertices.Append(vt);
}
vt.Position.X = worldVerticesBuffer[0];
vt.Position.Y = worldVerticesBuffer[1];
lineVertices.Append(vt);
}
}
}
if (debugBounds)
{
var vt = new SFML.Graphics.Vertex() { Color = BoundsColor };
var b = bounds;
boundsVertices[0] = boundsVertices[4] = new(new(b.Left, b.Top), BoundsColor);
boundsVertices[1] = new(new(b.Right, b.Top), BoundsColor);
boundsVertices[2] = new(new(b.Right, b.Bottom), BoundsColor);
boundsVertices[3] = new(new(b.Left, b.Bottom), BoundsColor);
target.Draw(boundsVertices);
vt.Position.X = b.Left;
vt.Position.Y = b.Top;
lineVertices.Append(vt);
vt.Position.X = b.Right;
vt.Position.Y = b.Top;
lineVertices.Append(vt); lineVertices.Append(vt);
vt.Position.X = b.Right;
vt.Position.Y = b.Bottom;
lineVertices.Append(vt); lineVertices.Append(vt);
vt.Position.X = b.Left;
vt.Position.Y = b.Bottom;
lineVertices.Append(vt); lineVertices.Append(vt);
vt.Position.X = b.Left;
vt.Position.Y = b.Top;
lineVertices.Append(vt);
}
// 骨骼线放最后画
if (debugBones)
{
var width = scale;
foreach (var bone in skeleton.Bones)
{
if (!bone.Active) continue;
var boneLength = bone.Data.Length;
var p1 = new SFML.System.Vector2f(bone.WorldX, bone.WorldY);
var p2 = new SFML.System.Vector2f(bone.WorldX + boneLength * bone.A, bone.WorldY + boneLength * bone.C);
AddRectLine(p1, p2, BoneLineColor, width);
}
}
target.Draw(lineVertices);
target.Draw(rectLineVertices);
// 骨骼的点最后画, 层级处于骨骼线上面
if (debugBones)
{
var radius = scale;
foreach (var bone in skeleton.Bones)
{
if (!bone.Active) continue;
DrawCirclePoint(target, new(bone.WorldX, bone.WorldY), BonePointColor, radius);
}
}
}
}

View File

@@ -9,6 +9,7 @@ using System.Text;
using System.Text.Json;
using System.Text.Json.Nodes;
using System.Text.Encodings.Web;
using SpineViewer.Utils;
namespace SpineViewer.Spine
{

View File

@@ -1,115 +0,0 @@
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace SpineViewer.Spine
{
/// <summary>
/// 对皮肤的包装类
/// </summary>
[TypeConverter(typeof(SkinWrapperConverter))]
public class SkinWrapper(Spine spine, int i)
{
private readonly Spine spine = spine;
[Browsable(false)]
public int Index { get; } = i;
public override string ToString()
{
var loadedSkins = spine.GetLoadedSkins();
if (Index >= 0 && Index < loadedSkins.Length)
return loadedSkins[Index];
return "!NULL"; // XXX: 预期应该不会发生
}
public override bool Equals(object? obj)
{
if (obj is SkinWrapper) return ToString() == obj.ToString();
return base.Equals(obj);
}
public override int GetHashCode() => (typeof(SkinWrapper).FullName + ToString()).GetHashCode();
}
/// <summary>
/// 皮肤属性描述符, 实现对属性的读取和赋值
/// </summary>
/// <param name="spine">关联的 Spine 对象</param>
public class SkinWrapperPropertyDescriptor(Spine spine, int i) : PropertyDescriptor($"Skin{i}", [new DisplayNameAttribute($"皮肤 {i}")])
{
private readonly Spine spine = spine;
private readonly int idx = i;
public override Type ComponentType => typeof(SkinManager);
public override bool IsReadOnly => false;
public override Type PropertyType => typeof(SkinWrapper);
public override bool CanResetValue(object component) => false;
public override void ResetValue(object component) { }
public override bool ShouldSerializeValue(object component) => false;
/// <summary>
/// 得到一个 SkinWrapper, 允许用户查看或者修改具体的属性值, 这个地方决定了在面板上看到的是一个对象及其属性
/// </summary>
public override object? GetValue(object? component) => new SkinWrapper(spine, idx);
/// <summary>
/// 允许通过字符串赋值修改该位置的皮肤
/// </summary>
public override void SetValue(object? component, object? value)
{
if (value is string s) spine.ReplaceSkin(idx, s);
}
}
/// <summary>
/// SkinManager 动态类型包装类, 用于提供对 Spine 皮肤列表的管理能力
/// </summary>
/// <param name="spine">关联的 Spine 对象</param>
public class SkinManager(Spine spine) : ICustomTypeDescriptor
{
private readonly Dictionary<int, SkinWrapperPropertyDescriptor> pdCache = [];
public Spine Spine { get; } = spine;
// XXX: 必须实现 ICustomTypeDescriptor 接口, 不能继承 CustomTypeDescriptor, 似乎继承下来的东西会有问题, 导致某些调用不正确
public AttributeCollection GetAttributes() => TypeDescriptor.GetAttributes(this, true);
public string? GetClassName() => TypeDescriptor.GetClassName(this, true);
public string? GetComponentName() => TypeDescriptor.GetComponentName(this, true);
public TypeConverter? GetConverter() => TypeDescriptor.GetConverter(this, true);
public EventDescriptor? GetDefaultEvent() => TypeDescriptor.GetDefaultEvent(this, true);
public PropertyDescriptor? GetDefaultProperty() => TypeDescriptor.GetDefaultProperty(this, true);
public object? GetEditor(Type editorBaseType) => TypeDescriptor.GetEditor(this, editorBaseType, true);
public EventDescriptorCollection GetEvents() => TypeDescriptor.GetEvents(this, true);
public EventDescriptorCollection GetEvents(Attribute[]? attributes) => TypeDescriptor.GetEvents(this, attributes, true);
public object? GetPropertyOwner(PropertyDescriptor? pd) => this;
public PropertyDescriptorCollection GetProperties() => GetProperties(null);
public PropertyDescriptorCollection GetProperties(Attribute[]? attributes)
{
var props = new List<SkinWrapperPropertyDescriptor>();
for (var i = 0; i < Spine.GetLoadedSkins().Length; i++)
{
if (!pdCache.ContainsKey(i))
pdCache[i] = new SkinWrapperPropertyDescriptor(Spine, i);
props.Add(pdCache[i]);
}
return new PropertyDescriptorCollection(props.ToArray());
}
/// <summary>
/// 在属性面板悬停可以显示已加载的皮肤列表
/// </summary>
public override string ToString() => $"[{string.Join(", ", Spine.GetLoadedSkins())}]";
public override bool Equals(object? obj)
{
if (obj is SkinManager manager) return ToString() == manager.ToString();
return base.Equals(obj);
}
public override int GetHashCode() => (typeof(SkinManager).FullName + ToString()).GetHashCode();
}
}

View File

@@ -0,0 +1,90 @@
using FFMpegCore;
using SpineViewer.Utils;
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace SpineViewer.Spine.SpineExporter
{
/// <summary>
/// MP4 导出参数
/// </summary>
public class AvifExporter : FFmpegVideoExporter
{
public AvifExporter()
{
FPS = 24;
}
public override string Format => "avif";
public override string Suffix => ".avif";
/// <summary>
/// 编码器
/// </summary>
public string Codec { get; set; } = "av1_nvenc";
/// <summary>
/// CRF
/// </summary>
public int CRF { get => crf; set => crf = Math.Clamp(value, 0, 63); }
private int crf = 23;
/// <summary>
/// 像素格式
/// </summary>
public string PixelFormat { get; set; } = "yuv420p";
/// <summary>
/// 循环次数, 0 无限循环, 取值范围 [0, 65535]
/// </summary>
public int Loop { get => loop; set => loop = Math.Clamp(value, 0, 65535); }
private int loop = 0;
public override string FileNameNoteSuffix => $"{Codec}_{CRF}_{PixelFormat}";
public override void SetOutputOptions(FFMpegArgumentOptions options)
{
base.SetOutputOptions(options);
options.WithVideoCodec(Codec).ForcePixelFormat(PixelFormat).WithConstantRateFactor(CRF).WithCustomArgument($"-loop {Loop}");
}
}
public class AvifExporterProperty(AvifExporter exporter) : FFmpegVideoExporterProperty(exporter)
{
[Browsable(false)]
public override AvifExporter Exporter => (AvifExporter)base.Exporter;
/// <summary>
/// 编码器
/// </summary>
[StringEnumConverter.StandardValues("av1_nvenc", "av1_amf", "libaom-av1", Customizable = true)]
[TypeConverter(typeof(StringEnumConverter))]
[Category("[3] "), DisplayName(""), Description("-c:v, 使\n建议使用硬件加速, libaom-av1 ")]
public string Codec { get => Exporter.Codec; set => Exporter.Codec = value; }
/// <summary>
/// CRF
/// </summary>
[Category("[3] "), DisplayName("CRF"), Description("-crf, 0-63, 18-28, 23, ")]
public int CRF { get => Exporter.CRF; set => Exporter.CRF = value; }
/// <summary>
/// 像素格式
/// </summary>
[StringEnumConverter.StandardValues("yuv420p", "yuv422p", "yuv444p", Customizable = true)]
[TypeConverter(typeof(StringEnumConverter))]
[Category("[3] "), DisplayName(""), Description("-pix_fmt, 使")]
public string PixelFormat { get => Exporter.PixelFormat; set => Exporter.PixelFormat = value; }
/// <summary>
/// 循环次数
/// </summary>
[Category("[3] "), DisplayName(""), Description("-loop, , 0 , [0, 65535]")]
public int Loop { get => Exporter.Loop; set => Exporter.Loop = value; }
}
}

View File

@@ -0,0 +1,60 @@
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace SpineViewer.Spine.SpineExporter
{
/// <summary>
/// FFmpeg 自定义视频导出参数
/// </summary>
public class CustomExporter : FFmpegVideoExporter
{
public CustomExporter()
{
CustomArgument = "-c:v libx264 -crf 23 -pix_fmt yuv420p"; // 提供一个示例参数
}
public override string Format => CustomFormat;
public override string Suffix => CustomSuffix;
public override string FileNameNoteSuffix => string.Empty;
/// <summary>
/// 文件格式
/// </summary>
public string CustomFormat { get; set; } = "mp4";
/// <summary>
/// 文件名后缀
/// </summary>
public string CustomSuffix { get; set; } = ".mp4";
}
public class CustomExporterProperty(CustomExporter exporter) : FFmpegVideoExporterProperty(exporter)
{
[Browsable(false)]
public override CustomExporter Exporter => (CustomExporter)base.Exporter;
[Browsable(false)]
public override string Format => Exporter.Format;
[Browsable(false)]
public override string Suffix => Exporter.Suffix;
/// <summary>
/// 文件格式
/// </summary>
[Category("[2] FFmpeg "), DisplayName(""), Description("-f, ")]
public string CustomFormat { get => Exporter.CustomFormat; set => Exporter.CustomFormat = value; }
/// <summary>
/// 文件名后缀
/// </summary>
[Category("[2] FFmpeg "), DisplayName(""), Description("")]
public string CustomSuffix { get => Exporter.CustomSuffix; set => Exporter.CustomSuffix = value; }
}
}

View File

@@ -1,37 +1,13 @@
using FFMpegCore.Pipes;
using SpineViewer.Extensions;
using System;
using System.Collections.Generic;
using System.Drawing.Imaging;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace SpineViewer.Exporter
namespace SpineViewer.Spine.SpineExporter
{
/// <summary>
/// 导出类型
/// </summary>
public enum ExportType
{
Frame,
FrameSequence,
Gif,
Mp4,
Webm,
Mkv,
Mov,
Custom,
}
/// <summary>
/// 导出实现类标记
/// </summary>
[AttributeUsage(AttributeTargets.Class, AllowMultiple = true, Inherited = false)]
public class ExportImplementationAttribute(ExportType exportType) : Attribute, IImplementationKey<ExportType>
{
public ExportType ImplementationKey { get; private set; } = exportType;
}
/// <summary>
/// SFML.Graphics.Image 帧对象包装类, 将接管给定的 image 对象生命周期
/// </summary>

View File

@@ -0,0 +1,242 @@
using NLog;
using SpineViewer.Extensions;
using SpineViewer.Utils;
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Drawing.Design;
using System.Linq;
using System.Reflection;
using System.Text;
using System.Threading.Tasks;
namespace SpineViewer.Spine.SpineExporter
{
/// <summary>
/// 导出器基类
/// </summary>
public abstract class Exporter : IDisposable
{
/// <summary>
/// 日志器
/// </summary>
protected readonly Logger logger = LogManager.GetCurrentClassLogger();
/// <summary>
/// 可用于文件名的时间戳字符串
/// </summary>
protected readonly string timestamp = DateTime.Now.ToString("yyMMddHHmmss");
~Exporter() { Dispose(false); }
public void Dispose() { Dispose(true); GC.SuppressFinalize(this); }
protected virtual void Dispose(bool disposing) { View.Dispose(); }
/// <summary>
/// 输出文件夹
/// </summary>
public string? OutputDir { get; set; } = null;
/// <summary>
/// 导出单个
/// </summary>
public bool IsExportSingle { get; set; } = false;
/// <summary>
/// 画面分辨率
/// </summary>
public Size Resolution { get; set; } = new(100, 100);
/// <summary>
/// 渲染视窗, 接管对象生命周期
/// </summary>
public SFML.Graphics.View View { get => view; set { view.Dispose(); view = value; } }
private SFML.Graphics.View view = new();
/// <summary>
/// 是否仅渲染选中
/// </summary>
public bool RenderSelectedOnly { get; set; } = false;
/// <summary>
/// 背景颜色
/// </summary>
public SFML.Graphics.Color BackgroundColor
{
get => backgroundColor;
set
{
backgroundColor = value;
var bcPma = value;
var a = bcPma.A / 255f;
bcPma.R = (byte)(bcPma.R * a);
bcPma.G = (byte)(bcPma.G * a);
bcPma.B = (byte)(bcPma.B * a);
BackgroundColorPma = bcPma;
}
}
private SFML.Graphics.Color backgroundColor = SFML.Graphics.Color.Transparent;
/// <summary>
/// 预乘后的背景颜色
/// </summary>
public SFML.Graphics.Color BackgroundColorPma { get; private set; } = SFML.Graphics.Color.Transparent;
/// <summary>
/// 获取供渲染的 SFML.Graphics.RenderTexture
/// </summary>
private SFML.Graphics.RenderTexture GetRenderTexture()
{
var tex = new SFML.Graphics.RenderTexture((uint)Resolution.Width, (uint)Resolution.Height);
tex.Clear(SFML.Graphics.Color.Transparent);
tex.SetView(View);
return tex;
}
/// <summary>
/// 获取单个模型的单帧画面
/// </summary>
protected SFMLImageVideoFrame GetFrame(SpineObject spine) => GetFrame([spine]);
/// <summary>
/// 获取模型列表的单帧画面
/// </summary>
protected SFMLImageVideoFrame GetFrame(SpineObject[] spinesToRender)
{
// RenderTexture 必须临时创建, 随用随取, 防止出现跨线程的情况
using var texPma = GetRenderTexture();
// 先将预乘结果准确绘制出来, 注意背景色也应当是预乘的
texPma.Clear(BackgroundColorPma);
foreach (var spine in spinesToRender) texPma.Draw(spine);
texPma.Display();
// 背景色透明度不为 1 时需要处理反预乘, 否则直接就是结果
if (BackgroundColor.A < 255)
{
// 从预乘结果构造渲染对象, 并正确设置变换
using var view = texPma.GetView();
using var img = texPma.Texture.CopyToImage();
using var texSprite = new SFML.Graphics.Texture(img);
using var sp = new SFML.Graphics.Sprite(texSprite)
{
Origin = new(texPma.Size.X / 2f, texPma.Size.Y / 2f),
Position = new(view.Center.X, view.Center.Y),
Scale = new(view.Size.X / texPma.Size.X, view.Size.Y / texPma.Size.Y),
Rotation = view.Rotation
};
// 混合模式用直接覆盖的方式, 保证得到的图像区域是反预乘的颜色和透明度, 同时使用反预乘着色器
var st = SFML.Graphics.RenderStates.Default;
st.BlendMode = SFMLBlendMode.SourceOnly;
st.Shader = SFMLShader.InversePma;
// 在最终结果上二次渲染非预乘画面
using var tex = GetRenderTexture();
// 将非预乘结果覆盖式绘制在目标对象上, 注意背景色应该用非预乘的
tex.Clear(BackgroundColor);
tex.Draw(sp, st);
tex.Display();
return new(tex.Texture.CopyToImage());
}
else
{
return new(texPma.Texture.CopyToImage());
}
}
/// <summary>
/// 每个模型在同一个画面进行导出
/// </summary>
protected abstract void ExportSingle(SpineObject[] spinesToRender, BackgroundWorker? worker = null);
/// <summary>
/// 每个模型独立导出
/// </summary>
protected abstract void ExportIndividual(SpineObject[] spinesToRender, BackgroundWorker? worker = null);
/// <summary>
/// 检查参数是否合法并规范化参数值, 否则返回用户错误原因
/// </summary>
public virtual string? Validate()
{
if (!string.IsNullOrWhiteSpace(OutputDir) && File.Exists(OutputDir))
return "输出文件夹无效";
if (!string.IsNullOrWhiteSpace(OutputDir) && !Directory.Exists(OutputDir))
return $"文件夹 {OutputDir} 不存在";
if (IsExportSingle && string.IsNullOrWhiteSpace(OutputDir))
return "导出单个时必须提供输出文件夹";
OutputDir = string.IsNullOrWhiteSpace(OutputDir) ? null : Path.GetFullPath(OutputDir);
return null;
}
/// <summary>
/// 执行导出
/// </summary>
/// <param name="spines">要进行导出的 Spine 列表</param>
/// <param name="worker">用来执行该函数的 worker</param>
/// <exception cref="ArgumentException"></exception>
public virtual void Export(SpineObject[] spines, BackgroundWorker? worker = null)
{
if (Validate() is string err)
throw new ArgumentException(err);
var spinesToRender = spines.Where(sp => !RenderSelectedOnly || sp.IsSelected).Reverse().ToArray();
if (IsExportSingle) ExportSingle(spinesToRender, worker);
else ExportIndividual(spinesToRender, worker);
logger.LogCurrentProcessMemoryUsage();
}
}
/// <summary>
/// 用于在 PropertyGrid 上提供用户操作接口的包装类
/// </summary>
public class ExporterProperty(Exporter exporter)
{
[Browsable(false)]
public virtual Exporter Exporter { get; } = exporter;
/// <summary>
/// 输出文件夹
/// </summary>
[Editor(typeof(FolderNameEditor), typeof(UITypeEditor))]
[Category("[0] "), DisplayName(""), Description("")]
public string? OutputDir { get => Exporter.OutputDir; set => Exporter.OutputDir = value; }
/// <summary>
/// 导出单个
/// </summary>
[Category("[0] "), DisplayName(""), Description("")]
public bool IsExportSingle { get => Exporter.IsExportSingle; set => Exporter.IsExportSingle = value; }
/// <summary>
/// 画面分辨率
/// </summary>
[TypeConverter(typeof(SizeConverter))]
[Category("[0] "), DisplayName(""), Description("")]
public Size Resolution { get => Exporter.Resolution; }
/// <summary>
/// 渲染视窗
/// </summary>
[Category("[0] "), DisplayName(""), Description("")]
public SFML.Graphics.View View { get => Exporter.View; }
/// <summary>
/// 是否仅渲染选中
/// </summary>
[Category("[0] "), DisplayName(""), Description("")]
public bool RenderSelectedOnly { get => Exporter.RenderSelectedOnly; }
/// <summary>
/// 背景颜色
/// </summary>
[Editor(typeof(SFMLColorEditor), typeof(UITypeEditor))]
[TypeConverter(typeof(SFMLColorConverter))]
[Category("[0] "), DisplayName(""), Description("使, #RRGGBBAA")]
public SFML.Graphics.Color BackgroundColor { get => Exporter.BackgroundColor; set => Exporter.BackgroundColor = value; }
}
}

View File

@@ -0,0 +1,135 @@
using FFMpegCore.Pipes;
using FFMpegCore;
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Diagnostics;
namespace SpineViewer.Spine.SpineExporter
{
/// <summary>
/// 使用 FFmpeg 的视频导出器
/// </summary>
public abstract class FFmpegVideoExporter : VideoExporter
{
/// <summary>
/// 文件格式
/// </summary>
public abstract string Format { get; }
/// <summary>
/// 文件名后缀
/// </summary>
public abstract string Suffix { get; }
/// <summary>
/// 文件名后缀
/// </summary>
public string CustomArgument { get; set; }
/// <summary>
/// 要追加在文件名末尾的信息字串, 首尾不需要提供额外分隔符
/// </summary>
public abstract string FileNameNoteSuffix { get; }
/// <summary>
/// 获取输出附加选项
/// </summary>
public virtual void SetOutputOptions(FFMpegArgumentOptions options) => options.ForceFormat(Format).WithCustomArgument(CustomArgument);
public override string? Validate()
{
if (base.Validate() is string error)
return error;
if (string.IsNullOrWhiteSpace(Format))
return "需要提供有效的格式";
if (string.IsNullOrWhiteSpace(Suffix))
return "需要提供有效的文件名后缀";
return null;
}
protected override void ExportSingle(SpineObject[] spinesToRender, BackgroundWorker? worker = null)
{
var noteSuffix = FileNameNoteSuffix;
if (!string.IsNullOrWhiteSpace(noteSuffix)) noteSuffix = $"_{noteSuffix}";
var filename = $"ffmpeg_{timestamp}_{FPS:f0}{noteSuffix}{Suffix}";
// 导出单个时必定提供输出文件夹
var savePath = Path.Combine(OutputDir, filename);
var videoFramesSource = new RawVideoPipeSource(GetFrames(spinesToRender, worker)) { FrameRate = FPS };
try
{
var ffmpegArgs = FFMpegArguments.FromPipeInput(videoFramesSource).OutputToFile(savePath, true, SetOutputOptions);
logger.Info("FFmpeg arguments: {}", ffmpegArgs.Arguments);
ffmpegArgs.ProcessSynchronously();
}
catch (Exception ex)
{
logger.Error(ex.ToString());
logger.Error("Failed to export {} {}", Format, savePath);
}
}
protected override void ExportIndividual(SpineObject[] spinesToRender, BackgroundWorker? worker = null)
{
var noteSuffix = FileNameNoteSuffix;
if (!string.IsNullOrWhiteSpace(noteSuffix)) noteSuffix = $"_{noteSuffix}";
foreach (var spine in spinesToRender)
{
if (worker?.CancellationPending == true) break; // 取消的日志在 GetFrames 里输出
var filename = $"{spine.Name}_{timestamp}_{FPS:f0}{noteSuffix}{Suffix}";
// 如果提供了输出文件夹, 则全部导出到输出文件夹, 否则导出到各自的文件夹下
var savePath = Path.Combine(OutputDir ?? spine.AssetsDir, filename);
var videoFramesSource = new RawVideoPipeSource(GetFrames(spine, worker)) { FrameRate = FPS };
try
{
var ffmpegArgs = FFMpegArguments
.FromPipeInput(videoFramesSource)
.OutputToFile(savePath, true, SetOutputOptions);
logger.Info("FFmpeg arguments: {}", ffmpegArgs.Arguments);
ffmpegArgs.ProcessSynchronously();
}
catch (Exception ex)
{
logger.Error(ex.ToString());
logger.Error("Failed to export {} {} {}", Format, savePath, spine.SkelPath);
}
}
}
}
public class FFmpegVideoExporterProperty(FFmpegVideoExporter exporter) : VideoExporterProperty(exporter)
{
[Browsable(false)]
public override FFmpegVideoExporter Exporter => (FFmpegVideoExporter)base.Exporter;
/// <summary>
/// 文件格式
/// </summary>
[Category("[2] FFmpeg "), DisplayName(""), Description("-f, ")]
public virtual string Format => Exporter.Format;
/// <summary>
/// 文件名后缀
/// </summary>
[Category("[2] FFmpeg "), DisplayName(""), Description("")]
public virtual string Suffix => Exporter.Suffix;
/// <summary>
/// 文件名后缀
/// </summary>
[Category("[2] FFmpeg "), DisplayName(""), Description("使 \"ffmpeg -h encoder=<编码器>\" 查看编码器支持的参数\n使用 \"ffmpeg -h muxer=<文件格式>\" 查看文件格式支持的参数")]
public string CustomArgument { get => Exporter.CustomArgument; set => Exporter.CustomArgument = value; }
}
}

View File

@@ -0,0 +1,133 @@
using SpineViewer.Spine;
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Drawing.Imaging;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace SpineViewer.Spine.SpineExporter
{
/// <summary>
/// 单帧画面导出器
/// </summary>
public class FrameExporter : Exporter
{
/// <summary>
/// 单帧画面格式
/// </summary>
public ImageFormat ImageFormat
{
get => imageFormat;
set
{
if (value == ImageFormat.MemoryBmp) value = ImageFormat.Bmp;
imageFormat = value;
}
}
private ImageFormat imageFormat = ImageFormat.Png;
/// <summary>
/// DPI
/// </summary>
public SizeF DPI
{
get => dpi;
set
{
if (value.Width <= 0) value.Width = 144;
if (value.Height <= 0) value.Height = 144;
dpi = value;
}
}
private SizeF dpi = new(144, 144);
protected override void ExportSingle(SpineObject[] spinesToRender, BackgroundWorker? worker = null)
{
// 导出单个时必定提供输出文件夹
var filename = $"frame_{timestamp}{ImageFormat.GetSuffix()}";
var savePath = Path.Combine(OutputDir, filename);
worker?.ReportProgress(0, $"已处理 0/1");
try
{
using var frame = GetFrame(spinesToRender);
using var img = frame.CopyToBitmap();
img.SetResolution(DPI.Width, DPI.Height);
img.Save(savePath, ImageFormat);
}
catch (Exception ex)
{
logger.Error(ex.ToString());
logger.Error("Failed to save single frame");
}
worker?.ReportProgress(100, $"已处理 1/1");
}
protected override void ExportIndividual(SpineObject[] spinesToRender, BackgroundWorker? worker = null)
{
int total = spinesToRender.Length;
int success = 0;
int error = 0;
worker?.ReportProgress(0, $"已处理 0/{total}");
for (int i = 0; i < total; i++)
{
var spine = spinesToRender[i];
// 逐个导出时如果提供了输出文件夹, 则全部导出到输出文件夹, 否则输出到各自的文件夹
var filename = $"{spine.Name}_{timestamp}{ImageFormat.GetSuffix()}";
var savePath = Path.Combine(OutputDir ?? spine.AssetsDir, filename);
try
{
using var frame = GetFrame(spine);
using var img = frame.CopyToBitmap();
img.SetResolution(DPI.Width, DPI.Height);
img.Save(savePath, ImageFormat);
success++;
}
catch (Exception ex)
{
logger.Error(ex.ToString());
logger.Error("Failed to save single frame {} {}", savePath, spine.SkelPath);
error++;
}
worker?.ReportProgress((int)((i + 1) * 100.0) / total, $"已处理 {i + 1}/{total}");
}
if (error > 0)
logger.Warn("Frames save {} successfully, {} failed", success, error);
else
logger.Info("{} frames saved successfully", success);
}
}
public class FrameExporterProperty(FrameExporter exporter) : ExporterProperty(exporter)
{
[Browsable(false)]
public override FrameExporter Exporter => (FrameExporter)base.Exporter;
/// <summary>
/// 单帧画面格式
/// </summary>
[TypeConverter(typeof(ImageFormatConverter))]
[Category("[1] "), DisplayName("")]
public ImageFormat ImageFormat { get => Exporter.ImageFormat; set => Exporter.ImageFormat = value; }
/// <summary>
/// 文件名后缀
/// </summary>
[Category("[1] "), DisplayName(""), Description("")]
public string Suffix { get => Exporter.ImageFormat.GetSuffix(); }
/// <summary>
/// DPI
/// </summary>
[TypeConverter(typeof(SizeFConverter))]
[Category("[1] "), DisplayName("DPI"), Description("")]
public SizeF DPI { get => Exporter.DPI; set => Exporter.DPI = value; }
}
}

View File

@@ -1,5 +1,5 @@
using SpineViewer.Exporter.Implementations.ExportArgs;
using SpineViewer.Spine;
using SpineViewer.Spine;
using SpineViewer.Utils;
using System;
using System.Collections.Generic;
using System.ComponentModel;
@@ -7,28 +7,28 @@ using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace SpineViewer.Exporter.Implementations.Exporter
namespace SpineViewer.Spine.SpineExporter
{
/// <summary>
/// 帧序列导出器
/// </summary>
[ExportImplementation(ExportType.FrameSequence)]
public class FrameSequenceExporter : VideoExporter
{
public FrameSequenceExporter(FrameSequenceExportArgs exportArgs) : base(exportArgs) { }
/// <summary>
/// 文件名后缀, 同时决定帧图像格式, 支持的格式为 <c>".png", ".jpg", ".tga", ".bmp"</c>
/// </summary>
public string Suffix { get; set; } = ".png";
protected override void ExportSingle(Spine.Spine[] spinesToRender, BackgroundWorker? worker = null)
protected override void ExportSingle(SpineObject[] spinesToRender, BackgroundWorker? worker = null)
{
var args = (FrameSequenceExportArgs)ExportArgs;
// 导出单个时必定提供输出文件夹,
var saveDir = Path.Combine(args.OutputDir, $"frames_{timestamp}_{args.FPS:f0}");
var saveDir = Path.Combine(OutputDir, $"frames_{timestamp}_{FPS:f0}");
Directory.CreateDirectory(saveDir);
int frameIdx = 0;
foreach (var frame in GetFrames(spinesToRender, worker))
{
var filename = $"frames_{timestamp}_{args.FPS:f0}_{frameIdx:d6}{args.Suffix}";
var filename = $"frames_{timestamp}_{FPS:f0}_{frameIdx:d6}{Suffix}";
var savePath = Path.Combine(saveDir, filename);
try
@@ -48,22 +48,21 @@ namespace SpineViewer.Exporter.Implementations.Exporter
}
}
protected override void ExportIndividual(Spine.Spine[] spinesToRender, BackgroundWorker? worker = null)
protected override void ExportIndividual(SpineObject[] spinesToRender, BackgroundWorker? worker = null)
{
var args = (FrameSequenceExportArgs)ExportArgs;
foreach (var spine in spinesToRender)
{
if (worker?.CancellationPending == true) break; // 取消的日志在 GetFrames 里输出
// 如果提供了输出文件夹, 则全部导出到输出文件夹, 否则导出到各自的文件夹下
var subDir = $"{spine.Name}_{timestamp}_{args.FPS:f0}";
var saveDir = args.OutputDir is null ? Path.Combine(spine.AssetsDir, subDir) : Path.Combine(args.OutputDir, subDir);
var subDir = $"{spine.Name}_{timestamp}_{FPS:f0}";
var saveDir = Path.Combine(OutputDir ?? spine.AssetsDir, subDir);
Directory.CreateDirectory(saveDir);
int frameIdx = 0;
foreach (var frame in GetFrames(spine, worker))
{
var filename = $"{spine.Name}_{timestamp}_{args.FPS:f0}_{frameIdx:d6}{args.Suffix}";
var filename = $"{spine.Name}_{timestamp}_{FPS:f0}_{frameIdx:d6}{Suffix}";
var savePath = Path.Combine(saveDir, filename);
try
@@ -84,4 +83,17 @@ namespace SpineViewer.Exporter.Implementations.Exporter
}
}
}
public class FrameSequenceExporterProperty(VideoExporter exporter) : VideoExporterProperty(exporter)
{
[Browsable(false)]
public override FrameSequenceExporter Exporter => (FrameSequenceExporter)base.Exporter;
/// <summary>
/// 文件名后缀
/// </summary>
[TypeConverter(typeof(StringEnumConverter)), StringEnumConverter.StandardValues(".png", ".jpg", ".tga", ".bmp")]
[Category("[2] "), DisplayName(""), Description("")]
public string Suffix { get => Exporter.Suffix; set => Exporter.Suffix = value; }
}
}

View File

@@ -6,18 +6,16 @@ using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace SpineViewer.Exporter.Implementations.ExportArgs
namespace SpineViewer.Spine.SpineExporter
{
/// <summary>
/// GIF 导出参数
/// </summary>
[ExportImplementation(ExportType.Gif)]
public class GifExportArgs : FFmpegVideoExportArgs
public class GifExporter : FFmpegVideoExporter
{
public GifExportArgs(Size resolution, SFML.Graphics.View view, bool renderSelectedOnly) : base(resolution, view, renderSelectedOnly)
public GifExporter()
{
// GIF 的帧率不能太高, 超过 50 帧反而会变慢
FPS = 12;
FPS = 24;
}
public override string Format => "gif";
@@ -27,27 +25,55 @@ namespace SpineViewer.Exporter.Implementations.ExportArgs
/// <summary>
/// 调色板最大颜色数量
/// </summary>
[Category("[3] "), DisplayName(""), Description("使, ")]
public uint MaxColors { get => maxColors; set => maxColors = Math.Clamp(value, 2, 256); }
private uint maxColors = 256;
/// <summary>
/// 透明度阈值
/// </summary>
[Category("[3] "), DisplayName(""), Description("")]
public byte AlphaThreshold { get => alphaThreshold; set => alphaThreshold = value; }
private byte alphaThreshold = 128;
/// <summary>
/// 循环次数, -1 不循环, 0 无限循环, 取值范围 [-1, 65535]
/// </summary>
public int Loop { get => loop; set => loop = Math.Clamp(value, -1, 65535); }
private int loop = 0;
public override string FileNameNoteSuffix => $"{MaxColors}_{AlphaThreshold}";
public override void SetOutputOptions(FFMpegArgumentOptions options)
{
base.SetOutputOptions(options);
var v = $"[0:v] split [s0][s1]";
var s0 = $"[s0] palettegen=reserve_transparent=1:max_colors={MaxColors} [p]";
var s1 = $"[s1][p] paletteuse=dither=bayer:alpha_threshold={AlphaThreshold}";
var customArgs = $"-filter_complex \"{v};{s0};{s1}\"";
var customArgs = $"-filter_complex \"{v};{s0};{s1}\" -loop {Loop}";
options.WithCustomArgument(customArgs);
}
}
public override string FileNameNoteSuffix => $"{MaxColors}_{AlphaThreshold}";
class GifExporterProperty(FFmpegVideoExporter exporter) : FFmpegVideoExporterProperty(exporter)
{
[Browsable(false)]
public override GifExporter Exporter => (GifExporter)base.Exporter;
/// <summary>
/// 调色板最大颜色数量
/// </summary>
[Category("[3] "), DisplayName(""), Description("使, ")]
public uint MaxColors { get => Exporter.MaxColors; set => Exporter.MaxColors = value; }
/// <summary>
/// 透明度阈值
/// </summary>
[Category("[3] "), DisplayName(""), Description("")]
public byte AlphaThreshold { get => Exporter.AlphaThreshold; set => Exporter.AlphaThreshold = value; }
/// <summary>
/// 透明度阈值
/// </summary>
[Category("[3] "), DisplayName(""), Description("-loop, , -1 , 0 , [-1, 65535]")]
public int Loop { get => Exporter.Loop; set => Exporter.Loop = value; }
}
}

View File

@@ -1,4 +1,5 @@
using FFMpegCore;
using SpineViewer.Utils;
using System;
using System.Collections.Generic;
using System.ComponentModel;
@@ -6,15 +7,14 @@ using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace SpineViewer.Exporter.Implementations.ExportArgs
namespace SpineViewer.Spine.SpineExporter
{
/// <summary>
/// MKV 导出参数
/// </summary>
[ExportImplementation(ExportType.Mkv)]
public class MkvExportArgs : FFmpegVideoExportArgs
public class MkvExporter : FFmpegVideoExporter
{
public MkvExportArgs(Size resolution, SFML.Graphics.View view, bool renderSelectedOnly) : base(resolution, view, renderSelectedOnly)
public MkvExporter()
{
BackgroundColor = new(0, 255, 0);
}
@@ -26,32 +26,53 @@ namespace SpineViewer.Exporter.Implementations.ExportArgs
/// <summary>
/// 编码器
/// </summary>
[StringEnumConverter.StandardValues("libx264", "libx265", "libvpx-vp9", Customizable = true)]
[TypeConverter(typeof(StringEnumConverter))]
[Category("[3] "), DisplayName(""), Description("-c:v, 使")]
public string Codec { get; set; } = "libx265";
/// <summary>
/// CRF
/// </summary>
[Category("[3] "), DisplayName("CRF"), Description("-crf, 0-63, 18-28, 23, ")]
public int CRF { get => crf; set => crf = Math.Clamp(value, 0, 63); }
private int crf = 23;
/// <summary>
/// 像素格式
/// </summary>
public string PixelFormat { get; set; } = "yuv444p";
public override string FileNameNoteSuffix => $"{Codec}_{CRF}_{PixelFormat}";
public override void SetOutputOptions(FFMpegArgumentOptions options)
{
base.SetOutputOptions(options);
options.WithVideoCodec(Codec).WithConstantRateFactor(CRF).ForcePixelFormat(PixelFormat);
}
}
public class MkvExporterProperty(FFmpegVideoExporter exporter) : FFmpegVideoExporterProperty(exporter)
{
[Browsable(false)]
public override MkvExporter Exporter => (MkvExporter)base.Exporter;
/// <summary>
/// 编码器
/// </summary>
[StringEnumConverter.StandardValues("libx264", "libx265", "libvpx-vp9", "av1_nvenc", Customizable = true)]
[TypeConverter(typeof(StringEnumConverter))]
[Category("[3] "), DisplayName(""), Description("-c:v, 使")]
public string Codec { get => Exporter.Codec; set => Exporter.Codec = value; }
/// <summary>
/// CRF
/// </summary>
[Category("[3] "), DisplayName("CRF"), Description("-crf, 0-63, 18-28, 23, ")]
public int CRF { get => Exporter.CRF; set => Exporter.CRF = value; }
/// <summary>
/// 像素格式
/// </summary>
[StringEnumConverter.StandardValues("yuv420p", "yuv422p", "yuv444p", "yuva420p", Customizable = true)]
[TypeConverter(typeof(StringEnumConverter))]
[Category("[3] "), DisplayName(""), Description("-pix_fmt, 使")]
public string PixelFormat { get; set; } = "yuv444p";
public override void SetOutputOptions(FFMpegArgumentOptions options)
{
base.SetOutputOptions(options);
options.WithVideoCodec(Codec).WithConstantRateFactor(CRF).ForcePixelFormat(PixelFormat);
}
public override string FileNameNoteSuffix => $"{Codec}_{CRF}_{PixelFormat}";
public string PixelFormat { get => Exporter.PixelFormat; set => Exporter.PixelFormat = value; }
}
}

View File

@@ -1,4 +1,5 @@
using FFMpegCore;
using SpineViewer.Utils;
using System;
using System.Collections.Generic;
using System.ComponentModel;
@@ -6,15 +7,14 @@ using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace SpineViewer.Exporter.Implementations.ExportArgs
namespace SpineViewer.Spine.SpineExporter
{
/// <summary>
/// MOV 导出参数
/// </summary>
[ExportImplementation(ExportType.Mov)]
public class MovExportArgs : FFmpegVideoExportArgs
public class MovExporter : FFmpegVideoExporter
{
public MovExportArgs(Size resolution, SFML.Graphics.View view, bool renderSelectedOnly) : base(resolution, view, renderSelectedOnly)
public MovExporter()
{
BackgroundColor = new(0, 255, 0);
}
@@ -23,13 +23,42 @@ namespace SpineViewer.Exporter.Implementations.ExportArgs
public override string Suffix => ".mov";
/// <summary>
/// 编码器
/// </summary>
public string Codec { get; set; } = "prores_ks";
/// <summary>
/// 预设
/// </summary>
public string Profile { get; set; } = "auto";
/// <summary>
/// 像素格式
/// </summary>
public string PixelFormat { get; set; } = "yuva444p10le";
public override string FileNameNoteSuffix => $"{Codec}_{Profile}_{PixelFormat}";
public override void SetOutputOptions(FFMpegArgumentOptions options)
{
base.SetOutputOptions(options);
options.WithFastStart().WithVideoCodec(Codec).WithCustomArgument($"-profile {Profile}").ForcePixelFormat(PixelFormat);
}
}
public class MovExporterProperty(FFmpegVideoExporter exporter) : FFmpegVideoExporterProperty(exporter)
{
[Browsable(false)]
public override MovExporter Exporter => (MovExporter)base.Exporter;
/// <summary>
/// 编码器
/// </summary>
[StringEnumConverter.StandardValues("prores_ks", Customizable = true)]
[TypeConverter(typeof(StringEnumConverter))]
[Category("[3] "), DisplayName(""), Description("-c:v, 使")]
public string Codec { get; set; } = "prores_ks";
public string Codec { get => Exporter.Codec; set => Exporter.Codec = value; }
/// <summary>
/// 预设
@@ -37,7 +66,7 @@ namespace SpineViewer.Exporter.Implementations.ExportArgs
[StringEnumConverter.StandardValues("auto", "proxy", "lt", "standard", "hq", "4444", "4444xq")]
[TypeConverter(typeof(StringEnumConverter))]
[Category("[3] "), DisplayName(""), Description("-profile, ")]
public string Profile { get; set; } = "auto";
public string Profile { get => Exporter.Profile; set => Exporter.Profile = value; }
/// <summary>
/// 像素格式
@@ -45,14 +74,6 @@ namespace SpineViewer.Exporter.Implementations.ExportArgs
[StringEnumConverter.StandardValues("yuv422p10le", "yuv444p10le", "yuva444p10le", Customizable = true)]
[TypeConverter(typeof(StringEnumConverter))]
[Category("[3] "), DisplayName(""), Description("-pix_fmt, 使")]
public string PixelFormat { get; set; } = "yuva444p10le";
public override void SetOutputOptions(FFMpegArgumentOptions options)
{
base.SetOutputOptions(options);
options.WithFastStart().WithVideoCodec(Codec).WithCustomArgument($"-profile {Profile}").ForcePixelFormat(PixelFormat);
}
public override string FileNameNoteSuffix => $"{Codec}_{Profile}_{PixelFormat}";
public string PixelFormat { get => Exporter.PixelFormat; set => Exporter.PixelFormat = value; }
}
}

View File

@@ -1,4 +1,5 @@
using FFMpegCore;
using SpineViewer.Utils;
using System;
using System.Collections.Generic;
using System.ComponentModel;
@@ -6,15 +7,14 @@ using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace SpineViewer.Exporter.Implementations.ExportArgs
namespace SpineViewer.Spine.SpineExporter
{
/// <summary>
/// MP4 导出参数
/// </summary>
[ExportImplementation(ExportType.Mp4)]
public class Mp4ExportArgs : FFmpegVideoExportArgs
public class Mp4Exporter : FFmpegVideoExporter
{
public Mp4ExportArgs(Size resolution, SFML.Graphics.View view, bool renderSelectedOnly) : base(resolution, view, renderSelectedOnly)
public Mp4Exporter()
{
BackgroundColor = new(0, 255, 0);
}
@@ -26,32 +26,53 @@ namespace SpineViewer.Exporter.Implementations.ExportArgs
/// <summary>
/// 编码器
/// </summary>
[StringEnumConverter.StandardValues("libx264", "libx265", Customizable = true)]
[TypeConverter(typeof(StringEnumConverter))]
[Category("[3] "), DisplayName(""), Description("-c:v, 使")]
public string Codec { get; set; } = "libx264";
/// <summary>
/// CRF
/// </summary>
[Category("[3] "), DisplayName("CRF"), Description("-crf, 0-63, 18-28, 23, ")]
public int CRF { get => crf; set => crf = Math.Clamp(value, 0, 63); }
private int crf = 23;
/// <summary>
/// 像素格式
/// </summary>
public string PixelFormat { get; set; } = "yuv444p";
public override string FileNameNoteSuffix => $"{Codec}_{CRF}_{PixelFormat}";
public override void SetOutputOptions(FFMpegArgumentOptions options)
{
base.SetOutputOptions(options);
options.WithFastStart().WithVideoCodec(Codec).WithConstantRateFactor(CRF).ForcePixelFormat(PixelFormat);
}
}
public class Mp4ExporterProperty(FFmpegVideoExporter exporter) : FFmpegVideoExporterProperty(exporter)
{
[Browsable(false)]
public override Mp4Exporter Exporter => (Mp4Exporter)base.Exporter;
/// <summary>
/// 编码器
/// </summary>
[StringEnumConverter.StandardValues("libx264", "libx265", Customizable = true)]
[TypeConverter(typeof(StringEnumConverter))]
[Category("[3] "), DisplayName(""), Description("-c:v, 使")]
public string Codec { get => Exporter.Codec; set => Exporter.Codec = value; }
/// <summary>
/// CRF
/// </summary>
[Category("[3] "), DisplayName("CRF"), Description("-crf, 0-63, 18-28, 23, ")]
public int CRF { get => Exporter.CRF; set => Exporter.CRF = value; }
/// <summary>
/// 像素格式
/// </summary>
[StringEnumConverter.StandardValues("yuv420p", "yuv422p", "yuv444p", Customizable = true)]
[TypeConverter(typeof(StringEnumConverter))]
[Category("[3] "), DisplayName(""), Description("-pix_fmt, 使")]
public string PixelFormat { get; set; } = "yuv444p";
public override void SetOutputOptions(FFMpegArgumentOptions options)
{
base.SetOutputOptions(options);
options.WithFastStart().WithVideoCodec(Codec).WithConstantRateFactor(CRF).ForcePixelFormat(PixelFormat);
}
public override string FileNameNoteSuffix => $"{Codec}_{CRF}_{PixelFormat}";
public string PixelFormat { get => Exporter.PixelFormat; set => Exporter.PixelFormat = value; }
}
}

View File

@@ -0,0 +1,169 @@
using SpineViewer.Spine;
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace SpineViewer.Spine.SpineExporter
{
/// <summary>
/// 视频导出基类
/// </summary>
public abstract class VideoExporter : Exporter
{
/// <summary>
/// 导出时长
/// </summary>
public float Duration { get => duration; set => duration = value < 0 ? -1 : value; }
private float duration = -1;
/// <summary>
/// 帧率
/// </summary>
public float FPS { get; set; } = 60;
/// <summary>
/// 是否保留最后一帧
/// </summary>
public bool KeepLast { get; set; } = true;
public override string? Validate()
{
if (base.Validate() is string error)
return error;
if (IsExportSingle && Duration < 0)
return "导出单个时导出时长不能为负数";
return null;
}
/// <summary>
/// 生成单个模型的帧序列
/// </summary>
protected IEnumerable<SFMLImageVideoFrame> GetFrames(SpineObject spine, BackgroundWorker? worker = null)
{
// 独立导出时如果 Duration 小于 0 则使用所有轨道上动画时长最大值
var duration = Duration;
if (duration < 0) duration = spine.GetTrackIndices().Select(i => spine.GetAnimationDuration(spine.GetAnimation(i))).Max();
float delta = 1f / FPS;
int total = (int)(duration * FPS); // 完整帧的数量
float deltaFinal = duration - delta * total; // 最后一帧时长
int final = KeepLast && deltaFinal > 1e-3 ? 1 : 0;
int frameCount = 1 + total + final; // 所有帧的数量 = 起始帧 + 完整帧 + 最后一帧
worker?.ReportProgress(0, $"{spine.Name} 已处理 0/{frameCount} 帧");
// 导出首帧
var firstFrame = GetFrame(spine);
worker?.ReportProgress(1 * 100 / frameCount, $"{spine.Name} 已处理 1/{frameCount} 帧");
yield return firstFrame;
// 导出完整帧
for (int i = 0; i < total; i++)
{
if (worker?.CancellationPending == true)
{
logger.Info("Export cancelled");
break;
}
spine.Update(delta);
var frame = GetFrame(spine);
worker?.ReportProgress((1 + i + 1) * 100 / frameCount, $"{spine.Name} 已处理 {1 + i + 1}/{frameCount} 帧");
yield return frame;
}
// 导出最后一帧
if (final > 0)
{
spine.Update(deltaFinal);
var finalFrame = GetFrame(spine);
worker?.ReportProgress(100, $"{spine.Name} 已处理 {frameCount}/{frameCount} 帧");
yield return finalFrame;
}
}
/// <summary>
/// 生成多个模型的帧序列
/// </summary>
protected IEnumerable<SFMLImageVideoFrame> GetFrames(SpineObject[] spinesToRender, BackgroundWorker? worker = null)
{
// 导出单个时必须根据 Duration 决定导出时长
var duration = Duration;
float delta = 1f / FPS;
int total = (int)(duration * FPS); // 完整帧的数量
float deltaFinal = duration - delta * total; // 最后一帧时长
int final = KeepLast && deltaFinal > 1e-3 ? 1 : 0;
int frameCount = 1 + total + final; // 所有帧的数量 = 起始帧 + 完整帧 + 最后一帧
worker?.ReportProgress(0, $"已处理 0/{frameCount} 帧");
// 导出首帧
var firstFrame = GetFrame(spinesToRender);
worker?.ReportProgress(1 * 100 / frameCount, $"已处理 1/{frameCount} 帧");
yield return firstFrame;
// 导出完整帧
for (int i = 0; i < total; i++)
{
if (worker?.CancellationPending == true)
{
logger.Info("Export cancelled");
break;
}
foreach (var spine in spinesToRender) spine.Update(delta);
var frame = GetFrame(spinesToRender);
worker?.ReportProgress((1 + i + 1) * 100 / frameCount, $"已处理 {1 + i + 1}/{frameCount} 帧");
yield return frame;
}
// 导出最后一帧
if (final > 0)
{
foreach (var spine in spinesToRender) spine.Update(delta);
var finalFrame = GetFrame(spinesToRender);
worker?.ReportProgress(100, $"已处理 {frameCount}/{frameCount} 帧");
yield return finalFrame;
}
}
public override void Export(SpineObject[] spines, BackgroundWorker? worker = null)
{
// 导出视频格式需要把模型时间都重置到 0
foreach (var spine in spines) spine.ResetAnimationsTime();
base.Export(spines, worker);
}
}
public class VideoExporterProperty(VideoExporter exporter) : ExporterProperty(exporter)
{
[Browsable(false)]
public override VideoExporter Exporter => (VideoExporter)base.Exporter;
/// <summary>
/// 导出时长
/// </summary>
[Category("[1] "), DisplayName(""), Description(", 0, 使")]
public float Duration { get => Exporter.Duration; set => Exporter.Duration = value; }
/// <summary>
/// 帧率
/// </summary>
[Category("[1] "), DisplayName(""), Description("")]
public float FPS { get => Exporter.FPS; set => Exporter.FPS = value; }
/// <summary>
/// 保留最后一帧
/// </summary>
[Category("[1] "), DisplayName(""), Description(", , 1")]
public bool KeepLast { get => Exporter.KeepLast; set => Exporter.KeepLast = value; }
}
}

View File

@@ -1,4 +1,5 @@
using FFMpegCore;
using SpineViewer.Utils;
using System;
using System.Collections.Generic;
using System.ComponentModel;
@@ -6,15 +7,14 @@ using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace SpineViewer.Exporter.Implementations.ExportArgs
namespace SpineViewer.Spine.SpineExporter
{
/// <summary>
/// WebM 导出参数
/// </summary>
[ExportImplementation(ExportType.Webm)]
public class WebmExportArgs : FFmpegVideoExportArgs
public class WebmExporter : FFmpegVideoExporter
{
public WebmExportArgs(Size resolution, SFML.Graphics.View view, bool renderSelectedOnly) : base(resolution, view, renderSelectedOnly)
public WebmExporter()
{
// 默认用透明黑背景
BackgroundColor = new(0, 0, 0, 0);
@@ -27,32 +27,53 @@ namespace SpineViewer.Exporter.Implementations.ExportArgs
/// <summary>
/// 编码器
/// </summary>
[StringEnumConverter.StandardValues("libvpx-vp9", Customizable = true)]
[TypeConverter(typeof(StringEnumConverter))]
[Category("[3] "), DisplayName(""), Description("-c:v, 使")]
public string Codec { get; set; } = "libvpx-vp9";
/// <summary>
/// CRF
/// </summary>
[Category("[3] "), DisplayName("CRF"), Description("-crf, 0-63, 18-28, 23, ")]
public int CRF { get => crf; set => crf = Math.Clamp(value, 0, 63); }
private int crf = 23;
/// <summary>
/// 像素格式
/// </summary>
public string PixelFormat { get; set; } = "yuva420p";
public override string FileNameNoteSuffix => $"{Codec}_{CRF}_{PixelFormat}";
public override void SetOutputOptions(FFMpegArgumentOptions options)
{
base.SetOutputOptions(options);
options.WithVideoCodec(Codec).WithConstantRateFactor(CRF).ForcePixelFormat(PixelFormat);
}
}
public class WebmExporterProperty(WebmExporter exporter) : FFmpegVideoExporterProperty(exporter)
{
[Browsable(false)]
public override WebmExporter Exporter => (WebmExporter)base.Exporter;
/// <summary>
/// 编码器
/// </summary>
[StringEnumConverter.StandardValues("libvpx-vp9", Customizable = true)]
[TypeConverter(typeof(StringEnumConverter))]
[Category("[3] "), DisplayName(""), Description("-c:v, 使")]
public string Codec { get => Exporter.Codec; set => Exporter.Codec = value; }
/// <summary>
/// CRF
/// </summary>
[Category("[3] "), DisplayName("CRF"), Description("-crf, 0-63, 18-28, 23, ")]
public int CRF { get => Exporter.CRF; set => Exporter.CRF = value; }
/// <summary>
/// 像素格式
/// </summary>
[StringEnumConverter.StandardValues("yuv420p", "yuv422p", "yuv444p", "yuva420p", Customizable = true)]
[TypeConverter(typeof(StringEnumConverter))]
[Category("[3] "), DisplayName(""), Description("-pix_fmt, 使")]
public string PixelFormat { get; set; } = "yuva420p";
public override void SetOutputOptions(FFMpegArgumentOptions options)
{
base.SetOutputOptions(options);
options.WithVideoCodec(Codec).WithConstantRateFactor(CRF).ForcePixelFormat(PixelFormat);
}
public override string FileNameNoteSuffix => $"{Codec}_{CRF}_{PixelFormat}";
public string PixelFormat { get => Exporter.PixelFormat; set => Exporter.PixelFormat = value; }
}
}

View File

@@ -0,0 +1,101 @@
using FFMpegCore;
using SpineViewer.Utils;
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace SpineViewer.Spine.SpineExporter
{
/// <summary>
/// MP4 导出参数
/// </summary>
public class WebpExporter : FFmpegVideoExporter
{
public WebpExporter()
{
FPS = 24;
}
public override string Format => "webp";
public override string Suffix => ".webp";
/// <summary>
/// 编码器
/// </summary>
public string Codec { get; set; } = "libwebp_anim";
/// <summary>
/// 是否无损
/// </summary>
public bool Lossless { get; set; } = false;
/// <summary>
/// 质量
/// </summary>
public int Quality { get => quality; set => quality = Math.Clamp(value, 0, 100); }
private int quality = 75;
/// <summary>
/// 像素格式
/// </summary>
public string PixelFormat { get; set; } = "yuva420p";
/// <summary>
/// 循环次数, 0 无限循环, 取值范围 [0, 65535]
/// </summary>
public int Loop { get => loop; set => loop = Math.Clamp(value, 0, 65535); }
private int loop = 0;
public override string FileNameNoteSuffix => $"{Codec}_{Quality}_{PixelFormat}";
public override void SetOutputOptions(FFMpegArgumentOptions options)
{
base.SetOutputOptions(options);
options.WithVideoCodec(Codec).ForcePixelFormat(PixelFormat).WithCustomArgument($"-lossless {(Lossless ? 1 : 0)} -quality {Quality} -loop {Loop}");
}
}
public class WebpExporterProperty(WebpExporter exporter) : FFmpegVideoExporterProperty(exporter)
{
[Browsable(false)]
public override WebpExporter Exporter => (WebpExporter)base.Exporter;
/// <summary>
/// 编码器
/// </summary>
[StringEnumConverter.StandardValues("libwebp_anim", "libwebp", Customizable = true)]
[TypeConverter(typeof(StringEnumConverter))]
[Category("[3] "), DisplayName(""), Description("-c:v, 使")]
public string Codec { get => Exporter.Codec; set => Exporter.Codec = value; }
/// <summary>
/// 是否无损
/// </summary>
[Category("[3] "), DisplayName(""), Description("-lossless, 0 , 1 ")]
public bool Lossless { get => Exporter.Lossless; set => Exporter.Lossless = value; }
/// <summary>
/// CRF
/// </summary>
[Category("[3] "), DisplayName(""), Description("-quality, 0-100, 75")]
public int Quality { get => Exporter.Quality; set => Exporter.Quality = value; }
/// <summary>
/// 像素格式
/// </summary>
[StringEnumConverter.StandardValues("yuv420p", "yuva420p", Customizable = true)]
[TypeConverter(typeof(StringEnumConverter))]
[Category("[3] "), DisplayName(""), Description("-pix_fmt, 使")]
public string PixelFormat { get => Exporter.PixelFormat; set => Exporter.PixelFormat = value; }
/// <summary>
/// 透明度阈值
/// </summary>
[Category("[3] "), DisplayName(""), Description("-loop, , 0 , [0, 65535]")]
public int Loop { get => Exporter.Loop; set => Exporter.Loop = value; }
}
}

View File

@@ -1,20 +1,19 @@
using System;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Reflection;
using System.Drawing.Design;
using NLog;
using System.Xml.Linq;
using SpineViewer.Extensions;
using SpineViewer.Utils;
namespace SpineViewer.Spine
{
/// <summary>
/// Spine 基类, 使用静态方法 New 来创建具体版本对象, 该类是线程安全的
/// </summary>
public abstract class Spine : ImplementationResolver<Spine, SpineImplementationAttribute, SpineVersion>, SFML.Graphics.Drawable, IDisposable
public abstract class SpineObject : ImplementationResolver<SpineObject, SpineImplementationAttribute, SpineVersion>, SFML.Graphics.Drawable, IDisposable
{
private readonly Logger logger = LogManager.GetCurrentClassLogger();
private bool skinLoggerWarned = false;
/// <summary>
/// 空动画标记
/// </summary>
@@ -30,18 +29,17 @@ namespace SpineViewer.Spine
/// </summary>
protected const uint PREVIEW_HEIGHT = 256;
/// <summary>
/// 缩放最小值
/// </summary>
protected const float SCALE_MIN = 0.001f;
/// <summary>
/// 创建特定版本的 Spine
/// </summary>
public static Spine New(SpineVersion version, string skelPath, string? atlasPath = null)
public static SpineObject New(SpineVersion version, string skelPath, string? atlasPath = null)
{
if (version == SpineVersion.Auto) version = SpineHelper.GetVersion(skelPath);
atlasPath ??= Path.ChangeExtension(skelPath, ".atlas");
skelPath = Path.GetFullPath(skelPath);
atlasPath = Path.GetFullPath(atlasPath);
if (version == SpineVersion.Auto) version = SpineUtils.GetVersion(skelPath);
if (!File.Exists(atlasPath)) throw new FileNotFoundException($"atlas file {atlasPath} not found");
return New(version, [skelPath, atlasPath]).PostInit();
}
@@ -50,10 +48,16 @@ namespace SpineViewer.Spine
/// </summary>
private readonly object _lock = new();
/// <summary>
/// 日志器
/// </summary>
protected readonly Logger logger = LogManager.GetCurrentClassLogger();
private bool skinLoggerWarned = false;
/// <summary>
/// 构造函数
/// </summary>
public Spine(string skelPath, string atlasPath)
public SpineObject(string skelPath, string atlasPath)
{
Version = GetType().GetCustomAttribute<SpineImplementationAttribute>().ImplementationKey;
AssetsDir = Directory.GetParent(skelPath).FullName;
@@ -65,12 +69,10 @@ namespace SpineViewer.Spine
/// <summary>
/// 构造函数之后的初始化工作
/// </summary>
private Spine PostInit()
private SpineObject PostInit()
{
SkinNames = skinNames.AsReadOnly();
SkinManager = new(this);
AnimationNames = animationNames.AsReadOnly();
AnimationTracks = new(this);
// 必须 Update 一次否则包围盒还没有值
update(0);
@@ -88,101 +90,98 @@ namespace SpineViewer.Spine
tex.Display();
Preview = tex.Texture.CopyToBitmap();
// 取最后一个作为初始, 尽可能去显示非默认的内容
setAnimation(0, AnimationNames.Last());
// 默认初始化10个空位
for (int i = 0; i < 10; i++)
{
setAnimation(i, AnimationNames.First());
loadedSkins.Add(SkinNames.First());
}
reloadSkins();
return this;
}
~Spine() { Dispose(false); }
~SpineObject() { Dispose(false); }
public void Dispose() { Dispose(true); GC.SuppressFinalize(this); }
protected virtual void Dispose(bool disposing) { Preview?.Dispose(); }
protected virtual void Dispose(bool disposing)
{
Preview?.Dispose();
triangleVertices.Dispose();
lineVertices.Dispose();
rectLineVertices.Dispose();
}
#region | [0]
/// <summary>
/// 运行时唯一 ID
/// </summary>
public string ID { get; } = Guid.NewGuid().ToString();
/// <summary>
/// 骨骼预览图, 并没有去除预乘, 画面可能偏暗
/// </summary>
public Image Preview { get; private set; }
/// <summary>
/// 获取所属版本
/// </summary>
[TypeConverter(typeof(SpineVersionConverter))]
[Category("[0] "), DisplayName("")]
public SpineVersion Version { get; }
/// <summary>
/// 资源所在完整目录
/// </summary>
[Category("[0] "), DisplayName("")]
public string AssetsDir { get; }
/// <summary>
/// skel 文件完整路径
/// </summary>
[Category("[0] "), DisplayName("skel文件路径")]
public string SkelPath { get; }
/// <summary>
/// atlas 文件完整路径
/// </summary>
[Category("[0] "), DisplayName("atlas文件路径")]
public string AtlasPath { get; }
/// <summary>
/// 名称
/// </summary>
[Category("[0] "), DisplayName("")]
public string Name { get; }
/// <summary>
/// 获取所属文件版本
/// </summary>
[Category("[0] "), DisplayName("")]
public abstract string FileVersion { get; }
#endregion
#region | [1]
/// <summary>
/// 是否被隐藏, 被隐藏的模型将仅仅在列表显示, 不参与其他行为
/// </summary>
[Category("[1] "), DisplayName("")]
public bool IsHidden
{
get { lock (_lock) return isHidden; }
set { lock (_lock) isHidden = value; }
}
public bool IsHidden { get { lock (_lock) return isHidden; } set { lock (_lock) isHidden = value; } }
protected bool isHidden = false;
/// <summary>
/// 是否使用预乘Alpha
/// 是否使用预乘 Alpha
/// </summary>
[Category("[1] "), DisplayName("Alpha通道")]
public bool UsePremultipliedAlpha
{
get { lock (_lock) return usePremultipliedAlpha; }
set { lock (_lock) usePremultipliedAlpha = value; }
}
protected bool usePremultipliedAlpha = false;
public bool UsePma { get { lock (_lock) return usePma; } set { lock (_lock) usePma = value; } }
protected bool usePma = false;
#endregion
#region | [2]
/// <summary>
/// 骨骼包围盒
/// </summary>
public RectangleF Bounds { get { lock (_lock) return bounds; } }
protected abstract RectangleF bounds { get; }
/// <summary>
/// 缩放比例
/// </summary>
[Category("[2] "), DisplayName("")]
public float Scale
{
get { lock (_lock) return scale; }
set { lock (_lock) { scale = value; update(0); } }
set { lock (_lock) { scale = Math.Max(value, 0.001f); update(0); } }
}
protected abstract float scale { get; set; }
/// <summary>
/// 位置
/// </summary>
[TypeConverter(typeof(PointFConverter))]
[Category("[2] "), DisplayName("")]
public PointF Position
{
get { lock (_lock) return position; }
@@ -193,7 +192,6 @@ namespace SpineViewer.Spine
/// <summary>
/// 水平翻转
/// </summary>
[Category("[2] "), DisplayName("")]
public bool FlipX
{
get { lock (_lock) return flipX; }
@@ -204,7 +202,6 @@ namespace SpineViewer.Spine
/// <summary>
/// 垂直翻转
/// </summary>
[Category("[2] "), DisplayName("")]
public bool FlipY
{
get { lock (_lock) return flipY; }
@@ -212,73 +209,149 @@ namespace SpineViewer.Spine
}
protected abstract bool flipY { get; set; }
#endregion
#region | [3]
/// <summary>
/// 已加载皮肤列表
/// </summary>
[Editor(typeof(SkinManagerEditor), typeof(UITypeEditor))]
[TypeConverter(typeof(ExpandableObjectConverter))]
[Category("[3] "), DisplayName("")]
public SkinManager SkinManager { get; private set; }
/// <summary>
/// 默认轨道动画名称, 如果设置的动画不存在则忽略
/// </summary>
[TypeConverter(typeof(AnimationConverter))]
[Category("[3] "), DisplayName(" 0 ")]
public string Track0Animation
{
get { lock (_lock) return getAnimation(0); }
set { lock (_lock) { setAnimation(0, value); update(0); } }
}
/// <summary>
/// 默认轨道动画时长
/// </summary>
[Category("[3] "), DisplayName(" 0 ")]
public float Track0AnimationDuration => GetAnimationDuration(Track0Animation);
/// <summary>
/// 默认轨道动画时长
/// </summary>
[Editor(typeof(AnimationTracksEditor), typeof(UITypeEditor))]
[TypeConverter(typeof(ExpandableObjectConverter))]
[Category("[3] "), DisplayName("")]
public AnimationTracksType AnimationTracks { get; private set; }
/// <summary>
/// 包含的所有皮肤名称
/// </summary>
[Browsable(false)]
public ReadOnlyCollection<string> SkinNames { get; private set; }
protected readonly List<string> skinNames = [];
/// <summary>
/// 包含的所有动画名称
/// </summary>
public ReadOnlyCollection<string> AnimationNames { get; private set; }
protected readonly List<string> animationNames = [EMPTY_ANIMATION];
/// <summary>
/// 是否被选中
/// </summary>
public bool IsSelected
{
get { lock (_lock) return isSelected; }
set { lock (_lock) { isSelected = value; update(0); } }
}
protected bool isSelected = false;
/// <summary>
/// 启用渲染调试
/// </summary>
public bool EnableDebug
{
get { lock (_lock) return enableDebug; }
set { lock (_lock) { enableDebug = value; update(0); } }
}
private bool enableDebug = false;
/// <summary>
/// 显示纹理
/// </summary>
public bool DebugTexture
{
get { lock (_lock) return debugTexture; }
set { lock (_lock) { debugTexture = value; update(0); } }
}
private bool debugTexture = true;
/// <summary>
/// 显示包围盒
/// </summary>
public bool DebugBounds
{
get { lock (_lock) return debugBounds; }
set { lock (_lock) { debugBounds = value; update(0); } }
}
protected bool debugBounds = true;
/// <summary>
/// 显示骨骼
/// </summary>
public bool DebugBones
{
get { lock (_lock) return debugBones; }
set { lock (_lock) { debugBones = value; update(0); } }
}
protected bool debugBones = false;
/// <summary>
/// 显示区域附件边框
/// </summary>
public bool DebugRegions
{
get { lock (_lock) return debugRegions; }
set { lock (_lock) { debugRegions = value; update(0); } }
}
protected bool debugRegions = false;
/// <summary>
/// 显示网格附件边框线
/// </summary>
public bool DebugMeshHulls
{
get { lock (_lock) return debugMeshHulls; }
set { lock (_lock) { debugMeshHulls = value; update(0); } }
}
protected bool debugMeshHulls = false;
/// <summary>
/// 显示网格附件网格线
/// </summary>
public bool DebugMeshes
{
get { lock (_lock) return debugMeshes; }
set { lock (_lock) { debugMeshes = value; update(0); } }
}
protected bool debugMeshes = false;
/// <summary>
/// 显示碰撞盒附件边框线
/// </summary>
public bool DebugBoundingBoxes
{
get { lock (_lock) return debugBoundingBoxes; }
set { lock (_lock) { debugBoundingBoxes = value; update(0); } }
}
protected bool debugBoundingBoxes = false;
/// <summary>
/// 显示路径附件网格线
/// </summary>
public bool DebugPaths
{
get { lock (_lock) return debugPaths; }
set { lock (_lock) { debugPaths = value; update(0); } }
}
protected bool debugPaths = false;
/// <summary>
/// 显示剪裁附件网格线
/// </summary>
public bool DebugClippings
{
get { lock (_lock) return debugClippings; }
set { lock (_lock) { debugClippings = value; update(0); } }
}
protected bool debugClippings = false;
/// <summary>
/// 获取已加载的皮肤列表快照, 允许出现重复值
/// </summary>
public string[] GetLoadedSkins() { lock (_lock) return loadedSkins.ToArray(); }
protected readonly List<string> loadedSkins = [];
/// <summary>
/// 加载指定皮肤, 添加至列表末尾, 如果不存在则忽略, 允许加载重复的值
/// </summary>
public void LoadSkin(string name)
public void LoadSkin(string name)
{
if (!skinNames.Contains(name)) return;
lock (_lock)
{
if (skinNames.Contains(name))
{
loadedSkins.Add(name);
reloadSkins();
loadedSkins.Add(name);
reloadSkins();
if (!skinLoggerWarned && Version <= SpineVersion.V37 && loadedSkins.Count > 1)
{
logger.Warn($"Multiplt skins not supported in SpineVersion {Version.GetName()}");
skinLoggerWarned = true;
}
if (!skinLoggerWarned && Version < SpineVersion.V38 && loadedSkins.Count > 1)
{
logger.Warn($"Multiplt skins not supported in SpineVersion {Version.GetName()}");
skinLoggerWarned = true;
}
}
}
@@ -290,11 +363,9 @@ namespace SpineViewer.Spine
{
lock (_lock)
{
if (idx >= 0 && idx < loadedSkins.Count)
{
loadedSkins.RemoveAt(idx);
reloadSkins();
}
if (idx < 0 || idx >= loadedSkins.Count) return;
loadedSkins.RemoveAt(idx);
reloadSkins();
}
}
@@ -305,11 +376,9 @@ namespace SpineViewer.Spine
{
lock (_lock)
{
if (idx >= 0 && idx < loadedSkins.Count && skinNames.Contains(name))
{
loadedSkins[idx] = name;
reloadSkins();
}
if (idx < 0 || idx >= loadedSkins.Count || !skinNames.Contains(name)) return;
loadedSkins[idx] = name;
reloadSkins();
}
}
@@ -325,7 +394,7 @@ namespace SpineViewer.Spine
}
/// <summary>
/// 加载皮肤, 之后需要使用 <see cref="setSlotsToSetupPose"/> 来复位
/// 加载皮肤, 如果不存在则忽略
/// </summary>
protected abstract void addSkin(string name);
@@ -334,13 +403,6 @@ namespace SpineViewer.Spine
/// </summary>
protected abstract void clearSkin();
/// <summary>
/// 包含的所有动画名称
/// </summary>
[Browsable(false)]
public ReadOnlyCollection<string> AnimationNames { get; private set; }
protected readonly List<string> animationNames = [EMPTY_ANIMATION];
/// <summary>
/// 获取所有非 null 的轨道索引快照
/// </summary>
@@ -375,85 +437,6 @@ namespace SpineViewer.Spine
/// </summary>
public void ResetAnimationsTime() { lock (_lock) { foreach (var i in getTrackIndices()) setAnimation(i, getAnimation(i)); update(0); } }
#endregion
#region | [4]
/// <summary>
/// 显示调试
/// </summary>
[Browsable(false)]
public bool IsDebug
{
get { lock (_lock) return isDebug; }
set { lock (_lock) isDebug = value; }
}
protected bool isDebug = false;
/// <summary>
/// 显示纹理
/// </summary>
[Category("[4] "), DisplayName("")]
public bool DebugTexture
{
get { lock (_lock) return debugTexture; }
set { lock (_lock) debugTexture = value; }
}
protected bool debugTexture = true;
/// <summary>
/// 显示包围盒
/// </summary>
[Category("[4] "), DisplayName("")]
public bool DebugBounds
{
get { lock (_lock) return debugBounds; }
set { lock (_lock) debugBounds = value; }
}
protected bool debugBounds = true;
/// <summary>
/// 显示骨骼
/// </summary>
[Category("[4] "), DisplayName("")]
public bool DebugBones
{
get { lock (_lock) return debugBones; }
set { lock (_lock) debugBones = value; }
}
protected bool debugBones = false;
#endregion
/// <summary>
/// 标识符
/// </summary>
public readonly string ID = Guid.NewGuid().ToString();
/// <summary>
/// 是否被选中
/// </summary>
[Browsable(false)]
public bool IsSelected
{
get { lock (_lock) return isSelected; }
set { lock (_lock) isSelected = value; }
}
protected bool isSelected = false;
/// <summary>
/// 骨骼包围盒
/// </summary>
[Browsable(false)]
public RectangleF Bounds { get { lock (_lock) return bounds; } }
protected abstract RectangleF bounds { get; }
/// <summary>
/// 骨骼预览图, 并没有去除预乘, 画面可能偏暗
/// </summary>
[Browsable(false)]
public Image Preview { get; private set; }
/// <summary>
/// 更新内部状态
/// </summary>
@@ -462,32 +445,115 @@ namespace SpineViewer.Spine
#region SFML.Graphics.Drawable
/// <summary>
/// 顶点坐标缓冲区
/// </summary>
protected float[] worldVerticesBuffer = new float[1024];
/// <summary>
/// 顶点缓冲区
/// </summary>
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);
protected static readonly SFML.Graphics.Color BonePointColor = new(0, 255, 0);
/// <summary>
/// 骨骼线颜色
/// </summary>
protected static readonly SFML.Graphics.Color BoneLineColor = new(255, 0, 0);
/// <summary>
/// 网格线颜色
/// </summary>
protected static readonly SFML.Graphics.Color MeshLineColor = new(255, 163, 0, 128);
/// <summary>
/// 附件边框线颜色
/// </summary>
protected static readonly SFML.Graphics.Color AttachmentLineColor = new(0, 0, 255, 128);
/// <summary>
/// 剪裁附件边框线颜色
/// </summary>
protected static readonly SFML.Graphics.Color ClippingLineColor = new(204, 0, 0);
/// <summary>
/// spine 顶点坐标缓冲区
/// </summary>
protected float[] worldVerticesBuffer = new float[1024];
/// <summary>
/// 三角形顶点缓冲区
/// </summary>
protected readonly SFML.Graphics.VertexArray triangleVertices = new(SFML.Graphics.PrimitiveType.Triangles);
/// <summary>
/// 无面积线条缓冲区
/// </summary>
protected readonly SFML.Graphics.VertexArray lineVertices = new(SFML.Graphics.PrimitiveType.Lines);
/// <summary>
/// 有半径圆点临时缓存对象
/// </summary>
private readonly SFML.Graphics.CircleShape circlePointShape = new();
/// <summary>
/// 有宽度线条缓冲区, 需要通过 <see cref="AddRectLine"/> 添加顶点
/// </summary>
protected readonly SFML.Graphics.VertexArray rectLineVertices = new(SFML.Graphics.PrimitiveType.Quads);
/// <summary>
/// 绘制有半径的实心圆点, 随模型一起缩放大小
/// </summary>
protected void DrawCirclePoint(SFML.Graphics.RenderTarget target, SFML.System.Vector2f p, SFML.Graphics.Color color, float radius = 1)
{
circlePointShape.Origin = new(radius, radius);
circlePointShape.Position = p;
circlePointShape.FillColor = color;
circlePointShape.Radius = radius;
target.Draw(circlePointShape);
}
/// <summary>
/// 绘制有宽度的实心线, 会随模型一起缩放粗细, 顶点被存储在 <see cref="rectLineVertices"/> 数组内
/// </summary>
protected void AddRectLine(SFML.System.Vector2f p1, SFML.System.Vector2f p2, SFML.Graphics.Color color, float width = 1)
{
var dx = p2.X - p1.X;
var dy = p2.Y - p1.Y;
var dt = (float)Math.Sqrt(dx * dx + dy * dy);
if (dt == 0) return;
var cosTheta = -dy / dt;
var sinTheta = dx / dt;
var halfWidth = width / 2;
var t = new SFML.System.Vector2f(halfWidth * cosTheta, halfWidth * sinTheta);
var v = new SFML.Graphics.Vertex() { Color = color };
v.Position = p1 + t; rectLineVertices.Append(v);
v.Position = p2 + t; rectLineVertices.Append(v);
v.Position = p2 - t; rectLineVertices.Append(v);
v.Position = p1 - t; rectLineVertices.Append(v);
}
/// <summary>
/// SFML.Graphics.Drawable 接口实现
/// <para>这个渲染实现绘制出来的像素将是预乘的, 当渲染的背景透明度是 1 时, 则等价于非预乘的结果, 即正常画面, 否则画面偏暗</para>
/// <para>可以用于 <see cref="SFML.Graphics.RenderWindow"/> 的渲染, 因为直接在窗口上绘制时窗口始终是不透明的</para>
/// </summary>
public void Draw(SFML.Graphics.RenderTarget target, SFML.Graphics.RenderStates states) { lock (_lock) draw(target, states); }
public void Draw(SFML.Graphics.RenderTarget target, SFML.Graphics.RenderStates states)
{
lock (_lock)
{
if (!enableDebug)
{
draw(target, states);
}
else
{
if (debugTexture) draw(target, states);
if (isSelected) debugDraw(target);
}
}
}
/// <summary>
/// 这个渲染实现绘制出来的像素将是预乘的, 当渲染的背景透明度是 1 时, 则等价于非预乘的结果, 即正常画面, 否则画面偏暗
@@ -495,7 +561,11 @@ namespace SpineViewer.Spine
/// </summary>
protected abstract void draw(SFML.Graphics.RenderTarget target, SFML.Graphics.RenderStates states);
#endregion
/// <summary>
/// 渲染调试内容
/// </summary>
protected abstract void debugDraw(SFML.Graphics.RenderTarget target);
#endregion
}
}

View File

@@ -1,4 +1,5 @@
using System;
using SpineViewer.Utils;
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Collections.ObjectModel;
@@ -11,35 +12,10 @@ 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
public static class SpineUtils
{
/// <summary>
/// 版本名称
@@ -52,7 +28,7 @@ namespace SpineViewer.Spine
/// </summary>
private static readonly Dictionary<SpineVersion, string> runtimes = [];
static SpineHelper()
static SpineUtils()
{
// 初始化缓存
foreach (var value in Enum.GetValues(typeof(SpineVersion)))

View File

@@ -0,0 +1,52 @@
using SpineViewer.Utils;
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Globalization;
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;
}
public class SpineVersionConverter : EnumConverter
{
public SpineVersionConverter() : base(typeof(SpineVersion)) { }
public override object? ConvertTo(ITypeDescriptorContext? context, CultureInfo? culture, object? value, Type? destinationType)
{
if (destinationType == typeof(string) && value is SpineVersion version)
return version.GetName();
return base.ConvertTo(context, culture, value, destinationType);
}
}
}

View File

@@ -0,0 +1,204 @@
using SpineViewer.Spine;
using SpineViewer.Utils;
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace SpineViewer.Spine.SpineView
{
/// <summary>
/// 用于在 PropertyGrid 上显示 Spine 动画列表的包装类
/// </summary>
public class SpineAnimationProperty(SpineObject spine) : ICustomTypeDescriptor
{
[Browsable(false)]
public SpineObject Spine { get; } = spine;
/// <summary>
/// 全轨道动画最大时长
/// </summary>
[DisplayName("全轨道最大时长")]
public float AnimationTracksMaxDuration => Spine.GetTrackIndices().Select(i => Spine.GetAnimationDuration(Spine.GetAnimation(i))).Max();
/// <summary>
/// <see cref="TrackAnimationProperty"/> 属性对象缓存
/// </summary>
private readonly Dictionary<int, TrackAnimationProperty> trackAnimationProperties = [];
/// <summary>
/// <c>this.Track{i}</c>
/// </summary>
public TrackAnimationProperty GetTrackAnimation(int i)
{
if (!trackAnimationProperties.ContainsKey(i))
trackAnimationProperties[i] = new TrackAnimationProperty(Spine, i);
return trackAnimationProperties[i];
}
/// <summary>
/// <c>this.Track{i} = <paramref name="value"/></c>
/// </summary>
public void SetTrackAnimation(int i, string value)
{
Spine.SetAnimation(i, value);
TypeDescriptor.Refresh(this);
}
/// <summary>
/// 在属性面板悬停可以按轨道顺序显示动画名称
/// </summary>
public override string ToString() => $"[{string.Join(", ", Spine.GetTrackIndices().Select(Spine.GetAnimation))}]";
public override bool Equals(object? obj)
{
if (obj is SpineAnimationProperty prop) return ToString() == prop.ToString();
return base.Equals(obj);
}
public override int GetHashCode() => HashCode.Combine(typeof(SpineAnimationProperty).FullName.GetHashCode(), ToString().GetHashCode());
#region ICustomTypeDescriptor
// XXX: 必须实现 ICustomTypeDescriptor 接口, 不能继承 CustomTypeDescriptor, 似乎继承下来的东西会有问题, 导致某些调用不正确
/// <summary>
/// 属性描述符缓存
/// </summary>
private static readonly Dictionary<int, TrackWrapperPropertyDescriptor> pdCache = [];
public AttributeCollection GetAttributes() => TypeDescriptor.GetAttributes(this, true);
public string? GetClassName() => TypeDescriptor.GetClassName(this, true);
public string? GetComponentName() => TypeDescriptor.GetComponentName(this, true);
public TypeConverter? GetConverter() => TypeDescriptor.GetConverter(this, true);
public EventDescriptor? GetDefaultEvent() => TypeDescriptor.GetDefaultEvent(this, true);
public PropertyDescriptor? GetDefaultProperty() => TypeDescriptor.GetDefaultProperty(this, true);
public object? GetEditor(Type editorBaseType) => TypeDescriptor.GetEditor(this, editorBaseType, true);
public EventDescriptorCollection GetEvents() => TypeDescriptor.GetEvents(this, true);
public EventDescriptorCollection GetEvents(Attribute[]? attributes) => TypeDescriptor.GetEvents(this, attributes, true);
public object? GetPropertyOwner(PropertyDescriptor? pd) => this;
public PropertyDescriptorCollection GetProperties() => GetProperties(null);
public PropertyDescriptorCollection GetProperties(Attribute[]? attributes)
{
var props = new PropertyDescriptorCollection(TypeDescriptor.GetProperties(this, attributes, true).Cast<PropertyDescriptor>().ToArray());
foreach (var i in Spine.GetTrackIndices())
{
if (!pdCache.ContainsKey(i))
pdCache[i] = new TrackWrapperPropertyDescriptor(i, [new DisplayNameAttribute($"轨道 {i}")]);
props.Add(pdCache[i]);
}
return props;
}
/// <summary>
/// 轨道属性描述符, 实现对属性的读取和赋值
/// </summary>
/// <param name="i">轨道索引</param>
private class TrackWrapperPropertyDescriptor(int i, Attribute[]? attributes) : PropertyDescriptor($"Track{i}", attributes)
{
private readonly int idx = i;
public override Type ComponentType => typeof(SpineAnimationProperty);
public override bool IsReadOnly => false;
public override Type PropertyType => typeof(TrackAnimationProperty);
public override bool CanResetValue(object component) => false;
public override void ResetValue(object component) { }
public override bool ShouldSerializeValue(object component) => false;
/// <summary>
/// 得到一个轨道包装类, 允许用户查看或者修改具体的属性值, 这个地方决定了在面板上看到的是一个对象及其属性
/// </summary>
public override object? GetValue(object? component)
{
if (component is SpineAnimationProperty tracks)
return tracks.GetTrackAnimation(idx);
return null;
}
/// <summary>
/// 允许通过字符串赋值修改该轨道的动画, 这里决定了当其他地方的调用 (比如 Converter) 通过 value 来设置属性值的时候应该怎么处理
/// </summary>
public override void SetValue(object? component, object? value)
{
if (component is SpineAnimationProperty tracks)
{
if (value is string s)
tracks.SetTrackAnimation(idx, s);
}
}
}
#endregion
}
/// <summary>
/// 对 <c><see cref="SpineAnimationProperty"/>.Track{i}</c> 属性的包装类
/// </summary>
[TypeConverter(typeof(TrackAnimationPropertyConverter))]
public class TrackAnimationProperty(SpineObject spine, int i)
{
private readonly SpineObject spine = spine;
[Browsable(false)]
public int Index { get; } = i;
[DisplayName("时长")]
public float Duration => spine.GetAnimationDuration(spine.GetAnimation(Index));
/// <summary>
/// 实现了默认的转为字符串的方式
/// </summary>
public override string ToString() => spine.GetAnimation(Index);
/// <summary>
/// 影响了属性面板的判断, 当动画名称相同的时候认为两个对象是相同的, 这样属性面板可以在多选的时候正确显示相同取值的内容
/// </summary>
public override bool Equals(object? obj)
{
if (obj is TrackAnimationProperty) return ToString() == obj.ToString();
return base.Equals(obj);
}
/// <summary>
/// 哈希码需要和 Equals 行为类似
/// </summary>
public override int GetHashCode() => HashCode.Combine(typeof(TrackAnimationProperty).FullName.GetHashCode(), ToString().GetHashCode());
}
/// <summary>
/// 轨道索引包装类转换器, 实现字符串和包装类的互相转换, 并且提供标准值列表对属性进行设置, 同时还提供在面板上显示包装类属性的能力
/// </summary>
public class TrackAnimationPropertyConverter : ExpandableObjectConverter
{
// NOTE: 可以不用实现 ConvertTo/ConvertFrom, 因为属性实现了与字符串之间的互转
// ToString 实现了 ConvertTo
// SetValue 实现了从字符串设置属性
public override bool GetStandardValuesSupported(ITypeDescriptorContext? context) => true;
public override bool GetStandardValuesExclusive(ITypeDescriptorContext? context) => true;
public override StandardValuesCollection? GetStandardValues(ITypeDescriptorContext? context)
{
if (context?.Instance is SpineAnimationProperty tracks)
{
return new StandardValuesCollection(tracks.Spine.AnimationNames);
}
else if (context?.Instance is object[] instances && instances.All(x => x is SpineAnimationProperty))
{
// XXX: 这里不知道为啥总是会得到 object[] 类型而不是具体的类型
var animTracks = instances.Cast<SpineAnimationProperty>().ToArray();
if (animTracks.Length > 0)
{
IEnumerable<string> common = animTracks[0].Spine.AnimationNames;
foreach (var t in animTracks.Skip(1))
common = common.Union(t.Spine.AnimationNames);
return new StandardValuesCollection(common.ToArray());
}
}
return base.GetStandardValues(context);
}
}
}

View File

@@ -0,0 +1,57 @@
using SpineViewer.Spine;
using SpineViewer.Utils;
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace SpineViewer.Spine.SpineView
{
/// <summary>
/// 用于在 PropertyGrid 上显示 Spine 基本信息的包装类
/// </summary>
public class SpineBaseInfoProperty(SpineObject spine)
{
[Browsable(false)]
public SpineObject Spine { get; } = spine;
/// <summary>
/// 获取所属版本
/// </summary>
[TypeConverter(typeof(SpineVersionConverter))]
[DisplayName("运行时版本")]
public SpineVersion Version => Spine.Version;
/// <summary>
/// 资源所在完整目录
/// </summary>
[DisplayName("资源目录")]
public string AssetsDir => Spine.AssetsDir;
/// <summary>
/// skel 文件完整路径
/// </summary>
[DisplayName("skel文件路径")]
public string SkelPath => Spine.SkelPath;
/// <summary>
/// atlas 文件完整路径
/// </summary>
[DisplayName("atlas文件路径")]
public string AtlasPath => Spine.AtlasPath;
/// <summary>
/// 名称
/// </summary>
[DisplayName("名称")]
public string Name => Spine.Name;
/// <summary>
/// 获取所属文件版本
/// </summary>
[DisplayName("文件版本")]
public string FileVersion => Spine.FileVersion;
}
}

View File

@@ -0,0 +1,73 @@
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using SpineViewer.Spine;
namespace SpineViewer.Spine.SpineView
{
/// <summary>
/// 用于在 PropertyGrid 上显示 Spine 调试属性的包装类
/// </summary>
public class SpineDebugProperty(SpineObject spine)
{
[Browsable(false)]
public SpineObject Spine { get; } = spine;
/// <summary>
/// 显示纹理
/// </summary>
[DisplayName("Texture")]
public bool DebugTexture { get => Spine.DebugTexture; set => Spine.DebugTexture = value; }
/// <summary>
/// 显示包围盒
/// </summary>
[DisplayName("Bounds")]
public bool DebugBounds { get => Spine.DebugBounds; set => Spine.DebugBounds = value; }
/// <summary>
/// 显示骨骼
/// </summary>
[DisplayName("Bones")]
public bool DebugBones { get => Spine.DebugBones; set => Spine.DebugBones = value; }
/// <summary>
/// 显示区域附件边框线
/// </summary>
[DisplayName("Regions")]
public bool DebugRegions { get => Spine.DebugRegions; set => Spine.DebugRegions = value; }
/// <summary>
/// 显示网格附件边框线
/// </summary>
[DisplayName("MeshHulls")]
public bool DebugMeshHulls { get => Spine.DebugMeshHulls; set => Spine.DebugMeshHulls = value; }
/// <summary>
/// 显示网格附件网格线
/// </summary>
[DisplayName("Meshes")]
public bool DebugMeshes { get => Spine.DebugMeshes; set => Spine.DebugMeshes = value; }
///// <summary>
///// 显示碰撞盒附件边框线
///// </summary>
//[DisplayName("BoudingBoxes")]
//public bool DebugBoundingBoxes { get => Spine.DebugBoundingBoxes; set => Spine.DebugBoundingBoxes = value; }
///// <summary>
///// 显示路径附件网格线
///// </summary>
//[DisplayName("Paths")]
//public bool DebugPaths { get => Spine.DebugPaths; set => Spine.DebugPaths = value; }
/// <summary>
/// 显示剪裁附件网格线
/// </summary>
[DisplayName("Clippings")]
public bool DebugClippings { get => Spine.DebugClippings; set => Spine.DebugClippings = value; }
}
}

View File

@@ -0,0 +1,37 @@
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Drawing.Design;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using SpineViewer.Spine;
namespace SpineViewer.Spine.SpineView
{
public class SpineObjectProperty(SpineObject spine)
{
[Browsable(false)]
public SpineObject Spine { get; } = spine;
[DisplayName("基本信息")]
public SpineBaseInfoProperty BaseInfo { get; } = new(spine);
[DisplayName("渲染")]
public SpineRenderProperty Render { get; } = new(spine);
[DisplayName("变换")]
public SpineTransformProperty Transform { get; } = new(spine);
[TypeConverter(typeof(ExpandableObjectConverter))]
[DisplayName("皮肤")]
public SpineSkinProperty Skin { get; } = new(spine);
[TypeConverter(typeof(ExpandableObjectConverter))]
[DisplayName("动画")]
public SpineAnimationProperty Animation { get; } = new(spine);
[DisplayName("调试")]
public SpineDebugProperty Debug { get; } = new(spine);
}
}

View File

@@ -0,0 +1,31 @@
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using SpineViewer.Spine;
namespace SpineViewer.Spine.SpineView
{
/// <summary>
/// 用于在 PropertyGrid 上显示 Spine 渲染设置的包装类
/// </summary>
public class SpineRenderProperty(SpineObject spine)
{
[Browsable(false)]
public SpineObject Spine { get; } = spine;
/// <summary>
/// 是否被隐藏, 被隐藏的模型将仅仅在列表显示, 不参与其他行为
/// </summary>
[DisplayName("是否隐藏")]
public bool IsHidden { get => Spine.IsHidden; set => Spine.IsHidden = value; }
/// <summary>
/// 是否使用预乘Alpha
/// </summary>
[DisplayName("预乘Alpha通道")]
public bool UsePremultipliedAlpha { get => Spine.UsePma; set => Spine.UsePma = value; }
}
}

View File

@@ -0,0 +1,186 @@
using SpineViewer.Spine;
using SpineViewer.Utils;
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace SpineViewer.Spine.SpineView
{
/// <summary>
/// 皮肤列表动态类型包装类, 用于提供对 Spine 皮肤列表的管理能力
/// </summary>
/// <param name="spine">关联的 Spine 对象</param>
public class SpineSkinProperty(SpineObject spine) : ICustomTypeDescriptor
{
[Browsable(false)]
public SpineObject Spine { get; } = spine;
/// <summary>
/// <see cref="SpineSkinProperty"/> 属性缓存
/// </summary>
private readonly Dictionary<int, SkinNameProperty> skinNameProperties = [];
/// <summary>
/// <c>this.Skin{i}</c>
/// </summary>
public SkinNameProperty GetSkinName(int i)
{
if (!skinNameProperties.ContainsKey(i))
skinNameProperties[i] = new SkinNameProperty(Spine, i);
return skinNameProperties[i];
}
/// <summary>
/// <c>this.Skin{i} = <paramref name="value"/></c>
/// </summary>
public void SetSkinName(int i, string value)
{
Spine.ReplaceSkin(i, value);
TypeDescriptor.Refresh(this);
}
/// <summary>
/// 在属性面板悬停可以显示已加载的皮肤列表
/// </summary>
public override string ToString() => $"[{string.Join(", ", Spine.GetLoadedSkins())}]";
public override bool Equals(object? obj)
{
if (obj is SpineSkinProperty prop) return ToString() == prop.ToString();
return base.Equals(obj);
}
public override int GetHashCode() => HashCode.Combine(typeof(SpineSkinProperty).FullName.GetHashCode(), ToString().GetHashCode());
#region ICustomTypeDescriptor
// XXX: 必须实现 ICustomTypeDescriptor 接口, 不能继承 CustomTypeDescriptor, 似乎继承下来的东西会有问题, 导致某些调用不正确
private static readonly Dictionary<int, SkinNamePropertyDescriptor> pdCache = [];
public AttributeCollection GetAttributes() => TypeDescriptor.GetAttributes(this, true);
public string? GetClassName() => TypeDescriptor.GetClassName(this, true);
public string? GetComponentName() => TypeDescriptor.GetComponentName(this, true);
public TypeConverter? GetConverter() => TypeDescriptor.GetConverter(this, true);
public EventDescriptor? GetDefaultEvent() => TypeDescriptor.GetDefaultEvent(this, true);
public PropertyDescriptor? GetDefaultProperty() => TypeDescriptor.GetDefaultProperty(this, true);
public object? GetEditor(Type editorBaseType) => TypeDescriptor.GetEditor(this, editorBaseType, true);
public EventDescriptorCollection GetEvents() => TypeDescriptor.GetEvents(this, true);
public EventDescriptorCollection GetEvents(Attribute[]? attributes) => TypeDescriptor.GetEvents(this, attributes, true);
public object? GetPropertyOwner(PropertyDescriptor? pd) => this;
public PropertyDescriptorCollection GetProperties() => GetProperties(null);
public PropertyDescriptorCollection GetProperties(Attribute[]? attributes)
{
var props = new PropertyDescriptorCollection(TypeDescriptor.GetProperties(this, attributes, true).Cast<PropertyDescriptor>().ToArray());
for (var i = 0; i < Spine.GetLoadedSkins().Length; i++)
{
if (!pdCache.ContainsKey(i))
pdCache[i] = new SkinNamePropertyDescriptor(i, [new DisplayNameAttribute($"皮肤 {i}")]);
props.Add(pdCache[i]);
}
return props;
}
/// <summary>
/// 皮肤属性描述符, 实现对属性的读取和赋值
/// </summary>
private class SkinNamePropertyDescriptor(int i, Attribute[]? attributes) : PropertyDescriptor($"Skin{i}", attributes)
{
private readonly int idx = i;
public override Type ComponentType => typeof(SpineSkinProperty);
public override bool IsReadOnly => false;
public override Type PropertyType => typeof(SkinNameProperty);
public override bool CanResetValue(object component) => false;
public override void ResetValue(object component) { }
public override bool ShouldSerializeValue(object component) => false;
/// <summary>
/// 得到一个 <see cref="SpineSkinProperty"/>, 允许用户查看或者修改具体的属性值, 这个地方决定了在面板上看到的是一个对象及其属性
/// </summary>
public override object? GetValue(object? component)
{
if (component is SpineSkinProperty prop)
return prop.GetSkinName(idx);
return null;
}
/// <summary>
/// 允许通过字符串赋值修改该位置的皮肤
/// </summary>
public override void SetValue(object? component, object? value)
{
if (component is SpineSkinProperty prop)
{
if (value is string s)
prop.SetSkinName(idx, s);
}
}
}
#endregion
}
/// <summary>
/// 对 <c><see cref="SpineSkinProperty"/>.Skin{i}</c> 属性的包装类
/// </summary>
[TypeConverter(typeof(SkinNamePropertyConverter))]
public class SkinNameProperty(SpineObject spine, int i)
{
private readonly SpineObject spine = spine;
[Browsable(false)]
public int Index { get; } = i;
public override string ToString()
{
var loadedSkins = spine.GetLoadedSkins();
if (Index >= 0 && Index < loadedSkins.Length)
return loadedSkins[Index];
return "!NULL"; // XXX: 预期应该不会发生
}
public override bool Equals(object? obj)
{
if (obj is SkinNameProperty) return ToString() == obj.ToString();
return base.Equals(obj);
}
public override int GetHashCode() => HashCode.Combine(typeof(SkinNameProperty).FullName.GetHashCode(), ToString().GetHashCode());
}
public class SkinNamePropertyConverter : StringConverter
{
// NOTE: 可以不用实现 ConvertTo/ConvertFrom, 因为属性实现了与字符串之间的互转
// ToString 实现了 ConvertTo
// SetValue 实现了从字符串设置属性
public override bool GetStandardValuesSupported(ITypeDescriptorContext? context) => true;
public override bool GetStandardValuesExclusive(ITypeDescriptorContext? context) => true;
public override StandardValuesCollection? GetStandardValues(ITypeDescriptorContext? context)
{
if (context?.Instance is SpineSkinProperty manager)
{
return new StandardValuesCollection(manager.Spine.SkinNames);
}
else if (context?.Instance is object[] instances && instances.All(x => x is SpineSkinProperty))
{
// XXX: 这里不知道为啥总是会得到 object[] 类型而不是具体的 SpineSkinWrapper[] 类型
var managers = instances.Cast<SpineSkinProperty>().ToArray();
if (managers.Length > 0)
{
IEnumerable<string> common = managers[0].Spine.SkinNames;
foreach (var t in managers.Skip(1))
common = common.Union(t.Spine.SkinNames);
return new StandardValuesCollection(common.ToArray());
}
}
return base.GetStandardValues(context);
}
}
}

View File

@@ -0,0 +1,45 @@
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using SpineViewer.Spine;
using SpineViewer.Utils;
namespace SpineViewer.Spine.SpineView
{
/// <summary>
/// 用于在 PropertyGrid 上显示 Spine 空间变换的包装类
/// </summary>
public class SpineTransformProperty(SpineObject spine)
{
[Browsable(false)]
public SpineObject Spine { get; } = spine;
/// <summary>
/// 缩放比例
/// </summary>
[DisplayName("缩放比例")]
public float Scale { get => Spine.Scale; set => Spine.Scale = value; }
/// <summary>
/// 位置
/// </summary>
[TypeConverter(typeof(PointFConverter))]
[DisplayName("位置")]
public PointF Position { get => Spine.Position; set => Spine.Position = value; }
/// <summary>
/// 水平翻转
/// </summary>
[DisplayName("水平翻转")]
public bool FlipX { get => Spine.FlipX; set => Spine.FlipX = value; }
/// <summary>
/// 垂直翻转
/// </summary>
[DisplayName("垂直翻转")]
public bool FlipY { get => Spine.FlipY; set => Spine.FlipY = value; }
}
}

View File

@@ -1,143 +0,0 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.ComponentModel;
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace SpineViewer.Spine
{
public class SpineVersionConverter : EnumConverter
{
public SpineVersionConverter() : base(typeof(SpineVersion)) { }
public override object? ConvertTo(ITypeDescriptorContext? context, CultureInfo? culture, object? value, Type? destinationType)
{
if (destinationType == typeof(string) && value is SpineVersion version)
return version.GetName();
return base.ConvertTo(context, culture, value, destinationType);
}
}
public class SkinConverter : StringConverter
{
public override bool GetStandardValuesSupported(ITypeDescriptorContext? context) => true;
public override bool GetStandardValuesExclusive(ITypeDescriptorContext? context) => true;
public override StandardValuesCollection? GetStandardValues(ITypeDescriptorContext? context)
{
if (context?.Instance is Spine obj)
{
return new StandardValuesCollection(obj.SkinNames);
}
else if (context?.Instance is Spine[] spines)
{
if (spines.Length > 0)
{
IEnumerable<string> common = spines[0].SkinNames;
foreach (var spine in spines.Skip(1))
common = common.Union(spine.SkinNames);
return new StandardValuesCollection(common.ToArray());
}
}
return base.GetStandardValues(context);
}
}
public class AnimationConverter : StringConverter
{
public override bool GetStandardValuesSupported(ITypeDescriptorContext? context) => true;
public override bool GetStandardValuesExclusive(ITypeDescriptorContext? context) => true;
public override StandardValuesCollection? GetStandardValues(ITypeDescriptorContext? context)
{
if (context?.Instance is Spine obj)
{
return new StandardValuesCollection(obj.AnimationNames);
}
else if (context?.Instance is Spine[] spines)
{
if (spines.Length > 0)
{
IEnumerable<string> common = spines[0].AnimationNames;
foreach (var spine in spines.Skip(1))
common = common.Union(spine.AnimationNames);
return new StandardValuesCollection(common.ToArray());
}
}
return base.GetStandardValues(context);
}
}
/// <summary>
/// 轨道索引包装类转换器, 实现字符串和包装类的互相转换, 并且提供标准值列表对属性进行设置, 同时还提供在面板上显示包装类属性的能力
/// </summary>
public class TrackWrapperConverter : ExpandableObjectConverter
{
// NOTE: 可以不用实现 ConvertTo/ConvertFrom, 因为属性实现了与字符串之间的互转
// ToString 实现了 ConvertTo
// SetValue 实现了从字符串设置属性
public override bool GetStandardValuesSupported(ITypeDescriptorContext? context) => true;
public override bool GetStandardValuesExclusive(ITypeDescriptorContext? context) => true;
public override StandardValuesCollection? GetStandardValues(ITypeDescriptorContext? context)
{
if (context.Instance is AnimationTracksType animTrack)
{
return new StandardValuesCollection(animTrack.Spine.AnimationNames);
}
else if (context.Instance is object[] instances && instances.All(x => x is AnimationTracksType))
{
// XXX: 这里不知道为啥总是会得到 object[] 类型而不是具体的 AnimationTracksType[] 类型
var animTracks = instances.Cast<AnimationTracksType>().ToArray();
if (animTracks.Length > 0)
{
IEnumerable<string> common = animTracks[0].Spine.AnimationNames;
foreach (var t in animTracks.Skip(1))
common = common.Union(t.Spine.AnimationNames);
return new StandardValuesCollection(common.ToArray());
}
}
return base.GetStandardValues(context);
}
}
public class SkinWrapperConverter : StringConverter
{
// NOTE: 可以不用实现 ConvertTo/ConvertFrom, 因为属性实现了与字符串之间的互转
// ToString 实现了 ConvertTo
// SetValue 实现了从字符串设置属性
public override bool GetStandardValuesSupported(ITypeDescriptorContext? context) => true;
public override bool GetStandardValuesExclusive(ITypeDescriptorContext? context) => true;
public override StandardValuesCollection? GetStandardValues(ITypeDescriptorContext? context)
{
if (context.Instance is SkinManager manager)
{
return new StandardValuesCollection(manager.Spine.SkinNames);
}
else if (context.Instance is object[] instances && instances.All(x => x is SkinManager))
{
// XXX: 这里不知道为啥总是会得到 object[] 类型而不是具体的 SkinManager[] 类型
var managers = instances.Cast<SkinManager>().ToArray();
if (managers.Length > 0)
{
IEnumerable<string> common = managers[0].Spine.SkinNames;
foreach (var t in managers.Skip(1))
common = common.Union(t.Spine.SkinNames);
return new StandardValuesCollection(common.ToArray());
}
}
return base.GetStandardValues(context);
}
}
}

View File

@@ -1,88 +0,0 @@
using SpineViewer.Dialogs;
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Drawing.Design;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms.Design;
namespace SpineViewer.Spine
{
/// <summary>
/// skel 文件路径编辑器
/// </summary>
public class SkelFileNameEditor : FileNameEditor
{
protected override void InitializeDialog(OpenFileDialog openFileDialog)
{
base.InitializeDialog(openFileDialog);
openFileDialog.Title = "选择 skel 文件";
openFileDialog.AddExtension = false;
openFileDialog.Filter = "skel 文件 (*.skel; *.json)|*.skel;*.json|二进制文件 (*.skel)|*.skel|文本文件 (*.json)|*.json|所有文件 (*.*)|*.*";
}
}
/// <summary>
/// atlas 文件路径编辑器
/// </summary>
public class AtlasFileNameEditor : FileNameEditor
{
protected override void InitializeDialog(OpenFileDialog openFileDialog)
{
base.InitializeDialog(openFileDialog);
openFileDialog.Title = "选择 atlas 文件";
openFileDialog.AddExtension = false;
openFileDialog.Filter = "atlas 文件 (*.atlas)|*.atlas|所有文件 (*.*)|*.*";
}
}
/// <summary>
/// 多轨道动画编辑器
/// </summary>
public class AnimationTracksEditor : UITypeEditor
{
public override UITypeEditorEditStyle GetEditStyle(ITypeDescriptorContext? context) => UITypeEditorEditStyle.Modal;
public override object? EditValue(ITypeDescriptorContext? context, IServiceProvider provider, object? value)
{
if (provider == null || context == null || context.Instance is not Spine)
return value;
IWindowsFormsEditorService editorService = provider.GetService(typeof(IWindowsFormsEditorService)) as IWindowsFormsEditorService;
if (editorService == null)
return value;
using (var dialog = new AnimationTracksEditorDialog((Spine)context.Instance))
editorService.ShowDialog(dialog);
TypeDescriptor.Refresh(context.Instance);
return value;
}
}
/// <summary>
/// 多轨道动画编辑器
/// </summary>
public class SkinManagerEditor : UITypeEditor
{
public override UITypeEditorEditStyle GetEditStyle(ITypeDescriptorContext? context) => UITypeEditorEditStyle.Modal;
public override object? EditValue(ITypeDescriptorContext? context, IServiceProvider provider, object? value)
{
if (provider == null || context == null || context.Instance is not Spine)
return value;
IWindowsFormsEditorService editorService = provider.GetService(typeof(IWindowsFormsEditorService)) as IWindowsFormsEditorService;
if (editorService == null)
return value;
using (var dialog = new SkinManagerEditorDialog((Spine)context.Instance))
editorService.ShowDialog(dialog);
TypeDescriptor.Refresh(context.Instance);
return value;
}
}
}

View File

@@ -7,7 +7,7 @@
<TargetFramework>net8.0-windows</TargetFramework>
<BaseOutputPath>$(SolutionDir)out</BaseOutputPath>
<IncludeSourceRevisionInInformationalVersion>false</IncludeSourceRevisionInInformationalVersion>
<Version>0.12.0</Version>
<Version>0.12.3</Version>
<OutputType>WinExe</OutputType>
<UseWindowsForms>true</UseWindowsForms>
<ApplicationIcon>appicon.ico</ApplicationIcon>

View File

@@ -1,38 +0,0 @@
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Drawing.Design;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms.Design;
namespace SpineViewer
{
/// <summary>
/// 使用 FolderBrowserDialog 的文件夹路径编辑器
/// </summary>
public class FolderNameEditor : UITypeEditor
{
public override UITypeEditorEditStyle GetEditStyle(ITypeDescriptorContext? context)
{
// 指定编辑风格为 Modal 对话框, 提供右边用来点击的按钮
return UITypeEditorEditStyle.Modal;
}
public override object? EditValue(ITypeDescriptorContext? context, IServiceProvider provider, object? value)
{
// 重写 EditValue 方法,提供自定义的文件夹选择对话框逻辑
using var dialog = new FolderBrowserDialog();
// 如果当前值为有效路径,则设置为初始选中路径
if (value is string currentPath && Directory.Exists(currentPath))
dialog.SelectedPath = currentPath;
if (dialog.ShowDialog() == DialogResult.OK)
value = dialog.SelectedPath;
return value;
}
}
}

View File

@@ -1,12 +1,11 @@
using SpineViewer.Exporter;
using System;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Text;
using System.Threading.Tasks;
namespace SpineViewer
namespace SpineViewer.Utils
{
public interface IImplementationKey<TKey>
{

View File

@@ -5,35 +5,35 @@ using System.Text;
using System.Threading.Tasks;
using System.Windows;
namespace SpineViewer
namespace SpineViewer.Utils
{
/// <summary>
/// 弹窗消息静态类
/// </summary>
public static class MessageBox
public static class MessagePopup
{
/// <summary>
/// 提示弹窗
/// </summary>
public static void Info(string text, string title = "提示信息") =>
System.Windows.Forms.MessageBox.Show(text, title, MessageBoxButtons.OK, MessageBoxIcon.Information);
MessageBox.Show(text, title, MessageBoxButtons.OK, MessageBoxIcon.Information);
/// <summary>
/// 警告弹窗
/// </summary>
public static void Warn(string text, string title = "警告信息") =>
System.Windows.Forms.MessageBox.Show(text, title, MessageBoxButtons.OK, MessageBoxIcon.Warning);
MessageBox.Show(text, title, MessageBoxButtons.OK, MessageBoxIcon.Warning);
/// <summary>
/// 错误弹窗
/// </summary>
public static void Error(string text, string title = "错误信息") =>
System.Windows.Forms.MessageBox.Show(text, title, MessageBoxButtons.OK, MessageBoxIcon.Error);
MessageBox.Show(text, title, MessageBoxButtons.OK, MessageBoxIcon.Error);
/// <summary>
/// 操作确认弹窗
/// </summary>
public static DialogResult Quest(string text, string title = "操作确认") =>
System.Windows.Forms.MessageBox.Show(text, title, MessageBoxButtons.OKCancel, MessageBoxIcon.Question);
MessageBox.Show(text, title, MessageBoxButtons.OKCancel, MessageBoxIcon.Question);
}
}

View File

@@ -4,12 +4,12 @@ using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace SpineViewer.Spine
namespace SpineViewer.Utils
{
/// <summary>
/// SFML 混合模式, 预乘模式下输入和输出的像素值都是预乘的
/// </summary>
public static class BlendModeSFML
public static class SFMLBlendMode
{
///// <summary>
///// Normal Blend, 无预乘, 仅在 dst.a 是 1 时得到正确结果, 其余情况是有偏结果
@@ -110,5 +110,13 @@ namespace SpineViewer.Spine
SFML.Graphics.BlendMode.Factor.OneMinusSrcAlpha,
SFML.Graphics.BlendMode.Equation.Add
);
/// <summary>
/// 仅源像素混合模式
/// </summary>
public static readonly SFML.Graphics.BlendMode SourceOnly = new(
SFML.Graphics.BlendMode.Factor.One,
SFML.Graphics.BlendMode.Factor.Zero
);
}
}

View File

@@ -4,9 +4,9 @@ using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace SpineViewer
namespace SpineViewer.Utils
{
public static class Shader
public static class SFMLShader
{
/// <summary>
/// 用于非预乘纹理的 fragment shader, 乘上了插值后的透明度用于实现透明度变化(插值预乘), 并且输出预乘后的像素值

View File

@@ -1,4 +1,6 @@
using System;
using SpineViewer.Spine;
using SpineViewer.Spine.SpineView;
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.ComponentModel;
@@ -8,7 +10,7 @@ using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace SpineViewer
namespace SpineViewer.Utils
{
public class PointFConverter : ExpandableObjectConverter
{
@@ -97,4 +99,75 @@ namespace SpineViewer
return result;
}
}
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) => 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

@@ -5,9 +5,65 @@ using System.Drawing.Design;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms.Design;
namespace SpineViewer.Exporter
namespace SpineViewer.Utils
{
/// <summary>
/// 使用 FolderBrowserDialog 的文件夹路径编辑器
/// </summary>
public class FolderNameEditor : UITypeEditor
{
public override UITypeEditorEditStyle GetEditStyle(ITypeDescriptorContext? context)
{
// 指定编辑风格为 Modal 对话框, 提供右边用来点击的按钮
return UITypeEditorEditStyle.Modal;
}
public override object? EditValue(ITypeDescriptorContext? context, IServiceProvider provider, object? value)
{
// 重写 EditValue 方法,提供自定义的文件夹选择对话框逻辑
using var dialog = new FolderBrowserDialog();
// 如果当前值为有效路径,则设置为初始选中路径
if (value is string currentPath && Directory.Exists(currentPath))
dialog.SelectedPath = currentPath;
if (dialog.ShowDialog() == DialogResult.OK)
value = dialog.SelectedPath;
return value;
}
}
///// <summary>
///// skel 文件路径编辑器
///// </summary>
//public class SkelFileNameEditor : FileNameEditor
//{
// protected override void InitializeDialog(OpenFileDialog openFileDialog)
// {
// base.InitializeDialog(openFileDialog);
// openFileDialog.Title = "选择 skel 文件";
// openFileDialog.AddExtension = false;
// openFileDialog.Filter = "skel 文件 (*.skel; *.json)|*.skel;*.json|二进制文件 (*.skel)|*.skel|文本文件 (*.json)|*.json|所有文件 (*.*)|*.*";
// }
//}
///// <summary>
///// atlas 文件路径编辑器
///// </summary>
//public class AtlasFileNameEditor : FileNameEditor
//{
// protected override void InitializeDialog(OpenFileDialog openFileDialog)
// {
// base.InitializeDialog(openFileDialog);
// openFileDialog.Title = "选择 atlas 文件";
// openFileDialog.AddExtension = false;
// openFileDialog.Filter = "atlas 文件 (*.atlas)|*.atlas|所有文件 (*.*)|*.*";
// }
//}
class SFMLColorEditor : UITypeEditor
{
public override bool GetPaintValueSupported(ITypeDescriptorContext? context) => true;

Binary file not shown.

Before

Width:  |  Height:  |  Size: 126 KiB

After

Width:  |  Height:  |  Size: 160 KiB