Compare commits

..

55 Commits

Author SHA1 Message Date
ww-rm
8de00cad76 更新至v0.10.1 2025-03-19 16:48:23 +08:00
ww-rm
e4c58f2f4e update changelog 2025-03-19 16:48:15 +08:00
ww-rm
063dba30b6 优化使用 2025-03-19 16:45:56 +08:00
ww-rm
01fa9287a1 修改默认列表宽度 2025-03-19 16:36:34 +08:00
ww-rm
008067fccb 增加预览图导出功能 2025-03-19 16:29:10 +08:00
ww-rm
091301e945 增加预览图 2025-03-19 15:12:05 +08:00
ww-rm
145f4f3265 增加空动画 2025-03-19 13:12:57 +08:00
ww-rm
36d4e8c948 增加管理资源按钮 2025-03-19 00:06:03 +08:00
ww-rm
63de847a57 add something 2025-03-18 15:16:12 +08:00
ww-rm
b3e1b7c902 修改类型转换 2025-03-18 13:52:58 +08:00
ww-rm
2dbc235631 Merge branch 'main' of github.com:ww-rm/SpineViewer 2025-03-18 12:15:14 +08:00
ww-rm
4d68b48367 增加版本转换接口 2025-03-18 12:15:07 +08:00
ww-rm
65e63e2b2d Update dotnet-desktop.yml 2025-03-18 09:55:09 +08:00
ww-rm
58071e1de1 add something 2025-03-17 21:14:47 +08:00
ww-rm
5009ef479f add something 2025-03-17 21:11:09 +08:00
ww-rm
e5e9357649 Merge branch 'main' of github.com:ww-rm/SpineViewer 2025-03-17 20:57:26 +08:00
ww-rm
a577474772 修改函数格式 2025-03-17 20:57:19 +08:00
ww-rm
e960a09153 Update README.en.md 2025-03-17 17:33:43 +08:00
ww-rm
13d50f59c3 Update README.md 2025-03-17 17:32:31 +08:00
ww-rm
ed4c8475e9 add post process 2025-03-16 19:49:34 +08:00
ww-rm
2338bf4e15 fix check bug 2025-03-16 17:21:41 +08:00
ww-rm
267aa7ee63 增加自动打开 2025-03-16 17:13:14 +08:00
ww-rm
3df7dbc769 remove debug 2025-03-16 17:09:59 +08:00
ww-rm
5f12ab7e85 update readme 2025-03-16 16:58:11 +08:00
ww-rm
ac0adc5f95 更新至v0.10.0 2025-03-16 16:47:34 +08:00
ww-rm
208b702065 update changelog 2025-03-16 16:46:58 +08:00
ww-rm
7e61fbfbac 增加后缀筛选选项 2025-03-16 16:46:49 +08:00
ww-rm
0591549727 fix bug 2025-03-16 16:38:51 +08:00
ww-rm
a0833580f8 增加格式转换 2025-03-16 16:22:19 +08:00
ww-rm
c622b60215 增加版本检测 2025-03-16 16:12:36 +08:00
ww-rm
c228cf9072 add readanimation 2025-03-16 14:22:00 +08:00
ww-rm
4c68dd4904 add something 2025-03-16 01:40:19 +08:00
ww-rm
32fde582fc refactor 2025-03-14 18:55:56 +08:00
ww-rm
2bf2509df7 add read attachments 2025-03-14 01:12:22 +08:00
ww-rm
07042189c8 增加注释 2025-03-13 17:31:04 +08:00
ww-rm
d251c94638 some change 2025-03-13 17:29:56 +08:00
ww-rm
b4119087fb update readme 2025-03-13 14:53:32 +08:00
ww-rm
e3959e80fb add something 2025-03-13 14:32:37 +08:00
ww-rm
0495a2344c add something 2025-03-13 14:32:11 +08:00
ww-rm
c781ec5a4f fix bug 2025-03-13 14:23:00 +08:00
ww-rm
a58566735f 增加提示信息 2025-03-13 14:22:46 +08:00
ww-rm
b37e5c25c3 rename 2025-03-13 13:30:35 +08:00
ww-rm
63a937a45b optimize 2025-03-13 10:32:18 +08:00
ww-rm
c920471c0c change name 2025-03-13 02:32:53 +08:00
ww-rm
c4863ee09b add binary writer/reader 2025-03-13 01:31:24 +08:00
ww-rm
c0b85c454e 调整结构 2025-03-12 19:14:22 +08:00
ww-rm
763a49a4d3 增加一些按钮 2025-03-12 18:33:52 +08:00
ww-rm
0e1540873c 增加自动弹出选择文件 2025-03-12 18:33:07 +08:00
ww-rm
39dcc636ca 增加文件版本显示 2025-03-12 15:23:15 +08:00
ww-rm
342778c56e 增加画面和列表联动 2025-03-11 00:01:54 +08:00
ww-rm
fd524891aa 增加属性IsSelected 2025-03-11 00:00:23 +08:00
ww-rm
48cb60020c 增加属性SelectedIndices 2025-03-11 00:00:09 +08:00
ww-rm
d502c592f7 update readme 2025-03-06 11:16:59 +08:00
ww-rm
e4377436a7 修改文件编码 2025-03-06 00:00:56 +08:00
ww-rm
eb44c1271e 修改命名 2025-03-05 16:24:19 +08:00
42 changed files with 9674 additions and 359 deletions

View File

@@ -1,4 +1,4 @@
name: Build and Release
name: Build & Release
on:
push:

13
CHANGELOG.md Normal file
View File

@@ -0,0 +1,13 @@
# CHANGELOG
## v0.10.1
- <20><><EFBFBD><EFBFBD><EFBFBD>б<EFBFBD>Ԥ<EFBFBD><D4A4>ͼ
- <20><><EFBFBD><EFBFBD><EFBFBD>б<EFBFBD>Ԥ<EFBFBD><D4A4>ͼ<EFBFBD><CDBC><EFBFBD><EFBFBD>
## v0.10.0
- <20><><EFBFBD><EFBFBD><EFBFBD>˻<EFBFBD><CBBB><EFBFBD><EFBFBD><EFBFBD><EFBFBD>б<EFBFBD><D0B1><EFBFBD>ѡ<EFBFBD><D1A1><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ɾ<EFBFBD><C9BE><EFBFBD><EFBFBD>Ԥ<EFBFBD><D4A4><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ʾ<EFBFBD><CABE>Χ<EFBFBD><CEA7>ѡ<EFBFBD><D1A1>
- <20><><EFBFBD><EFBFBD><EFBFBD>˹<EFBFBD><CBB9><EFBFBD><EFBFBD>ļ<EFBFBD><C4BC><EFBFBD>ʽת<CABD><D7AA><EFBFBD><EFBFBD><EFBFBD>ܣ<EFBFBD>Ŀǰ<C4BF><C7B0>֧<EFBFBD>ֲ<EFBFBD><D6B2>ְ汾<D6B0>IJ<EFBFBD><C4B2><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>
- <20>Ż<EFBFBD><C5BB>˲<EFBFBD><CBB2><EFBFBD>ʹ<EFBFBD><CAB9><EFBFBD><EFBFBD><EFBFBD><EFBFBD>

View File

@@ -1,4 +1,4 @@
# SpineViewer
# [SpineViewer](https://github.com/ww-rm/SpineViewer)
[![Build and Release](https://github.com/ww-rm/SpineViewer/actions/workflows/dotnet-desktop.yml/badge.svg)](https://github.com/ww-rm/SpineViewer/actions/workflows/dotnet-desktop.yml)
@@ -6,7 +6,7 @@
A simple and user-friendly Spine file viewer and exporter.
![previewer](img/previewer.jpg)
![previewer](img/preview.jpg)
---

View File

@@ -1,4 +1,4 @@
# SpineViewer
# [SpineViewer](https://github.com/ww-rm/SpineViewer)
[![Build and Release](https://github.com/ww-rm/SpineViewer/actions/workflows/dotnet-desktop.yml/badge.svg)](https://github.com/ww-rm/SpineViewer/actions/workflows/dotnet-desktop.yml)
@@ -6,7 +6,7 @@
一个简单好用的 Spine 文件查看&导出程序.
![previewer](img/previewer.jpg)
![previewer](img/preview.jpg)
---
@@ -18,21 +18,26 @@
也可以下载带有 `SelfContained` 后缀的压缩包, 可以独立运行.
## 功能
## 功能支持
| 版本 | 查看&导出 | 格式转换 |
| :---: | :---: | :---: |
| `2.1.x` | :white_check_mark: | |
| `3.1.x` | | |
| `3.4.x` | | |
| `3.5.x` | | |
| `3.6.x` | :white_check_mark: | |
| `3.7.x` | :white_check_mark: | |
| `3.8.x` | :white_check_mark: | :white_check_mark: |
| `4.1.x` | :white_check_mark: | |
| `4.2.x` | :white_check_mark: | |
| `4.3.x` | | |
- 支持不同版本 Spine 查看
- [x] `v2.1.x`
- [x] `v3.6.x`
- [x] `v3.7.x`
- [x] `v3.8.x`
- [x] `v4.0.x`
- [x] `v4.1.x`
- [x] `v4.2.x`
- [ ] `v4.3.x`
- 支持多骨骼文件动画预览
- 支持每个骨骼独立参数设置
- 支持动画PNG帧序列导出
- 支持缩放旋转等导出画面设置
- 支持对独立的骨骼文件进行格式转换
- Coming soon...
## 使用方法

View File

@@ -15,6 +15,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution
ProjectSection(SolutionItems) = preProject
.editorconfig = .editorconfig
.gitignore = .gitignore
CHANGELOG.md = CHANGELOG.md
README.en.md = README.en.md
README.md = README.md
EndProjectSection

View File

@@ -41,6 +41,13 @@
toolStripSeparator2 = new ToolStripSeparator();
toolStripMenuItem_BatchAdd = new ToolStripMenuItem();
toolStripMenuItem_RemoveAll = new ToolStripMenuItem();
toolStripSeparator3 = new ToolStripSeparator();
toolStripMenuItem_ChangeView = new ToolStripMenuItem();
toolStripMenuItem_LargeIconView = new ToolStripMenuItem();
toolStripMenuItem_SmallIconView = new ToolStripMenuItem();
toolStripMenuItem_DetailsView = new ToolStripMenuItem();
imageList_LargeIcon = new ImageList(components);
imageList_SmallIcon = new ImageList(components);
contextMenuStrip.SuspendLayout();
SuspendLayout();
//
@@ -52,10 +59,12 @@
listView.Dock = DockStyle.Fill;
listView.FullRowSelect = true;
listView.GridLines = true;
listView.LargeImageList = imageList_LargeIcon;
listView.Location = new Point(0, 0);
listView.Name = "listView";
listView.ShowItemToolTips = true;
listView.Size = new Size(336, 445);
listView.SmallImageList = imageList_SmallIcon;
listView.TabIndex = 1;
listView.UseCompatibleStateImageBehavior = false;
listView.View = View.Details;
@@ -73,9 +82,9 @@
// contextMenuStrip
//
contextMenuStrip.ImageScalingSize = new Size(24, 24);
contextMenuStrip.Items.AddRange(new ToolStripItem[] { toolStripMenuItem_Add, toolStripMenuItem_Insert, toolStripMenuItem_Remove, toolStripSeparator1, toolStripMenuItem_MoveUp, toolStripMenuItem_MoveDown, toolStripSeparator2, toolStripMenuItem_BatchAdd, toolStripMenuItem_RemoveAll });
contextMenuStrip.Items.AddRange(new ToolStripItem[] { toolStripMenuItem_Add, toolStripMenuItem_Insert, toolStripMenuItem_Remove, toolStripSeparator1, toolStripMenuItem_MoveUp, toolStripMenuItem_MoveDown, toolStripSeparator2, toolStripMenuItem_BatchAdd, toolStripMenuItem_RemoveAll, toolStripSeparator3, toolStripMenuItem_ChangeView });
contextMenuStrip.Name = "contextMenuStrip";
contextMenuStrip.Size = new Size(188, 226);
contextMenuStrip.Size = new Size(188, 262);
contextMenuStrip.Opening += contextMenuStrip_Opening;
//
// toolStripMenuItem_Add
@@ -140,6 +149,51 @@
toolStripMenuItem_RemoveAll.Text = "移除全部(&X)";
toolStripMenuItem_RemoveAll.Click += toolStripMenuItem_RemoveAll_Click;
//
// toolStripSeparator3
//
toolStripSeparator3.Name = "toolStripSeparator3";
toolStripSeparator3.Size = new Size(184, 6);
//
// toolStripMenuItem_ChangeView
//
toolStripMenuItem_ChangeView.DropDownItems.AddRange(new ToolStripItem[] { toolStripMenuItem_LargeIconView, toolStripMenuItem_SmallIconView, toolStripMenuItem_DetailsView });
toolStripMenuItem_ChangeView.Name = "toolStripMenuItem_ChangeView";
toolStripMenuItem_ChangeView.Size = new Size(187, 30);
toolStripMenuItem_ChangeView.Text = "切换视图";
//
// toolStripMenuItem_LargeIconView
//
toolStripMenuItem_LargeIconView.Name = "toolStripMenuItem_LargeIconView";
toolStripMenuItem_LargeIconView.Size = new Size(164, 34);
toolStripMenuItem_LargeIconView.Text = "大图标";
toolStripMenuItem_LargeIconView.Click += toolStripMenuItem_LargeIconView_Click;
//
// toolStripMenuItem_SmallIconView
//
toolStripMenuItem_SmallIconView.Name = "toolStripMenuItem_SmallIconView";
toolStripMenuItem_SmallIconView.Size = new Size(164, 34);
toolStripMenuItem_SmallIconView.Text = "小图标";
toolStripMenuItem_SmallIconView.Click += toolStripMenuItem_SmallIconView_Click;
//
// toolStripMenuItem_DetailsView
//
toolStripMenuItem_DetailsView.Name = "toolStripMenuItem_DetailsView";
toolStripMenuItem_DetailsView.Size = new Size(164, 34);
toolStripMenuItem_DetailsView.Text = "列表";
toolStripMenuItem_DetailsView.Click += toolStripMenuItem_DetailsView_Click;
//
// imageList_LargeIcon
//
imageList_LargeIcon.ColorDepth = ColorDepth.Depth32Bit;
imageList_LargeIcon.ImageSize = new Size(96, 96);
imageList_LargeIcon.TransparentColor = Color.Transparent;
//
// imageList_SmallIcon
//
imageList_SmallIcon.ColorDepth = ColorDepth.Depth32Bit;
imageList_SmallIcon.ImageSize = new Size(48, 48);
imageList_SmallIcon.TransparentColor = Color.Transparent;
//
// SpineListView
//
AutoScaleDimensions = new SizeF(11F, 24F);
@@ -164,5 +218,12 @@
private ToolStripMenuItem toolStripMenuItem_MoveDown;
private ToolStripSeparator toolStripSeparator2;
private ColumnHeader columnHeader_Name;
private ImageList imageList_SmallIcon;
private ImageList imageList_LargeIcon;
private ToolStripSeparator toolStripSeparator3;
private ToolStripMenuItem toolStripMenuItem_ChangeView;
private ToolStripMenuItem toolStripMenuItem_LargeIconView;
private ToolStripMenuItem toolStripMenuItem_SmallIconView;
private ToolStripMenuItem toolStripMenuItem_DetailsView;
}
}

View File

@@ -36,39 +36,9 @@ namespace SpineViewer.Controls
}
/// <summary>
/// 弹出添加对话框在指定位置之前插入一项
/// listView.SelectedIndices
/// </summary>
private void Insert(int index = -1)
{
var dialog = new Dialogs.OpenSpineDialog();
if (dialog.ShowDialog() != DialogResult.OK)
return;
try
{
var spine = Spine.Spine.New(dialog.Version, dialog.SkelPath, dialog.AtlasPath);
// 如果索引无效则在末尾添加
if (index < 0 || index > listView.Items.Count)
index = listView.Items.Count;
// 锁定外部的读操作
lock (Spines) { spines.Insert(index, spine); }
listView.Items.Insert(index, new ListViewItem(spine.Name) { ToolTipText = spine.SkelPath });
// 选中新增项
listView.SelectedIndices.Clear();
listView.SelectedIndices.Add(index);
}
catch (Exception ex)
{
Program.Logger.Error(ex.ToString());
Program.Logger.Error("Failed to load {} {}", dialog.SkelPath, dialog.AtlasPath);
MessageBox.Show(ex.ToString(), "骨骼加载失败", MessageBoxButtons.OK, MessageBoxIcon.Error);
}
Program.Logger.Info($"Current memory usage: {Program.Process.WorkingSet64 / 1024.0 / 1024.0:F2} MB");
}
public ListView.SelectedIndexCollection SelectedIndices { get => listView.SelectedIndices; }
/// <summary>
/// 弹出添加对话框
@@ -93,55 +63,25 @@ namespace SpineViewer.Controls
progressDialog.ShowDialog();
}
private void BatchAdd_Work(object? sender, DoWorkEventArgs e)
public void ExportPreviews()
{
var worker = sender as BackgroundWorker;
var arguments = e.Argument as Dialogs.BatchOpenSpineDialog;
var skelPaths = arguments.SkelPaths;
var version = arguments.Version;
int totalCount = skelPaths.Length;
int success = 0;
int error = 0;
worker.ReportProgress(0, $"已处理 0/{totalCount}");
for (int i = 0; i < totalCount; i++)
lock (Spines)
{
if (worker.CancellationPending)
if (spines.Count <= 0)
{
e.Cancel = true;
break;
MessageBox.Show("请至少打开一个骨骼文件", "提示信息", MessageBoxButtons.OK, MessageBoxIcon.Information);
return;
}
var skelPath = skelPaths[i];
try
{
var spine = Spine.Spine.New(version, skelPath);
lock (Spines) { spines.Add(spine); }
listView.Invoke(() => listView.Items.Add(new ListViewItem(spine.Name) { ToolTipText = spine.SkelPath }));
success++;
}
catch (Exception ex)
{
Program.Logger.Error(ex.ToString());
Program.Logger.Error("Failed to load {}", skelPath);
error++;
}
worker.ReportProgress((int)((i + 1) * 100.0) / totalCount, $"已处理 {i + 1}/{totalCount}");
}
if (error > 0)
{
Program.Logger.Warn("Batch load {} successfully, {} failed", success, error);
}
else
{
Program.Logger.Info("{} skel loaded successfully", success);
}
var saveDialog = new Dialogs.ExportPreviewDialog();
if (saveDialog.ShowDialog() != DialogResult.OK)
return;
Program.Logger.Info($"Current memory usage: {Program.Process.WorkingSet64 / 1024.0 / 1024.0:F2} MB");
var progressDialog = new Dialogs.ProgressDialog();
progressDialog.DoWork += ExportPreview_Work;
progressDialog.RunWorkerAsync(saveDialog);
progressDialog.ShowDialog();
}
private void listView_SelectedIndexChanged(object sender, EventArgs e)
@@ -156,6 +96,10 @@ namespace SpineViewer.Controls
PropertyGrid.SelectedObject = spines[listView.SelectedIndices[0]];
else
PropertyGrid.SelectedObjects = listView.SelectedIndices.Cast<int>().Select(index => spines[index]).ToArray();
// 标记选中的 Spine
for (int i = 0; i < spines.Count; i++)
spines[i].IsSelected = listView.SelectedIndices.Contains(i);
}
}
}
@@ -246,6 +190,11 @@ namespace SpineViewer.Controls
toolStripMenuItem_MoveUp.Enabled = selectedCount == 1 && listView.SelectedIndices[0] != 0;
toolStripMenuItem_MoveDown.Enabled = selectedCount == 1 && listView.SelectedIndices[0] != itemsCount - 1;
toolStripMenuItem_RemoveAll.Enabled = itemsCount > 0;
// 视图选项
toolStripMenuItem_LargeIconView.Checked = listView.View == View.LargeIcon;
toolStripMenuItem_SmallIconView.Checked = listView.View == View.SmallIcon;
toolStripMenuItem_DetailsView.Checked = listView.View == View.Details;
}
private void toolStripMenuItem_Add_Click(object sender, EventArgs e)
@@ -279,8 +228,11 @@ namespace SpineViewer.Controls
{
lock (Spines)
{
spines[i].Dispose();
var spine = spines[i];
spines.RemoveAt(i);
listView.SmallImageList.Images.RemoveByKey(spine.ID);
listView.LargeImageList.Images.RemoveByKey(spine.ID);
spine.Dispose();
}
listView.Items.RemoveAt(i);
}
@@ -324,15 +276,182 @@ namespace SpineViewer.Controls
if (MessageBox.Show($"确认移除所有 {listView.Items.Count} 项吗?", "操作确认", MessageBoxButtons.OKCancel, MessageBoxIcon.Question) != DialogResult.OK)
return;
lock (Spines)
lock (Spines)
{
foreach (var spine in spines)
spine.Dispose();
spines.Clear();
spines.Clear();
listView.SmallImageList.Images.Clear();
listView.LargeImageList.Images.Clear();
}
listView.Items.Clear();
if (PropertyGrid is not null)
PropertyGrid.SelectedObject = null;
}
private void toolStripMenuItem_LargeIconView_Click(object sender, EventArgs e)
{
listView.View = View.LargeIcon;
}
private void toolStripMenuItem_SmallIconView_Click(object sender, EventArgs e)
{
listView.View = View.SmallIcon;
}
private void toolStripMenuItem_DetailsView_Click(object sender, EventArgs e)
{
listView.View = View.Details;
}
/// <summary>
/// 弹出添加对话框在指定位置之前插入一项
/// </summary>
private void Insert(int index = -1)
{
var dialog = new Dialogs.OpenSpineDialog();
if (dialog.ShowDialog() != DialogResult.OK)
return;
try
{
var spine = Spine.Spine.New(dialog.Version, dialog.SkelPath, dialog.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);
}
listView.Items.Insert(index, new ListViewItem(spine.Name, spine.ID) { ToolTipText = spine.SkelPath });
// 选中新增项
listView.SelectedIndices.Clear();
listView.SelectedIndices.Add(index);
}
catch (Exception ex)
{
Program.Logger.Error(ex.ToString());
Program.Logger.Error("Failed to load {} {}", dialog.SkelPath, dialog.AtlasPath);
MessageBox.Show(ex.ToString(), "骨骼加载失败", MessageBoxButtons.OK, MessageBoxIcon.Error);
}
Program.Logger.Info($"Current memory usage: {Program.Process.WorkingSet64 / 1024.0 / 1024.0:F2} MB");
}
private void BatchAdd_Work(object? sender, DoWorkEventArgs e)
{
var worker = sender as BackgroundWorker;
var arguments = e.Argument as Dialogs.BatchOpenSpineDialog;
var skelPaths = arguments.SkelPaths;
var version = arguments.Version;
int totalCount = skelPaths.Length;
int success = 0;
int error = 0;
worker.ReportProgress(0, $"已处理 0/{totalCount}");
for (int i = 0; i < totalCount; i++)
{
if (worker.CancellationPending)
{
e.Cancel = true;
break;
}
var skelPath = skelPaths[i];
try
{
var spine = Spine.Spine.New(version, skelPath);
var preview = spine.Preview;
lock (Spines) { spines.Add(spine); }
listView.Invoke(() =>
{
listView.SmallImageList.Images.Add(spine.ID, preview);
listView.LargeImageList.Images.Add(spine.ID, preview);
listView.Items.Add(new ListViewItem(spine.Name, spine.ID) { ToolTipText = spine.SkelPath });
});
success++;
}
catch (Exception ex)
{
Program.Logger.Error(ex.ToString());
Program.Logger.Error("Failed to load {}", skelPath);
error++;
}
worker.ReportProgress((int)((i + 1) * 100.0) / totalCount, $"已处理 {i + 1}/{totalCount}");
}
if (error > 0)
{
Program.Logger.Warn("Batch load {} successfully, {} failed", success, error);
}
else
{
Program.Logger.Info("{} skel loaded successfully", success);
}
Program.Logger.Info($"Current memory usage: {Program.Process.WorkingSet64 / 1024.0 / 1024.0:F2} MB");
}
private void ExportPreview_Work(object? sender, DoWorkEventArgs e)
{
var worker = sender as BackgroundWorker;
var arguments = e.Argument as Dialogs.ExportPreviewDialog;
var outputDir = arguments.OutputDir;
var width = arguments.PreviewWidth;
var height = arguments.PreviewHeight;
int success = 0;
int error = 0;
lock (Spines)
{
int totalCount = spines.Count;
worker.ReportProgress(0, $"已处理 0/{totalCount}");
for (int i = 0; i < totalCount; i++)
{
if (worker.CancellationPending)
{
e.Cancel = true;
break;
}
var spine = spines[i];
try
{
var preview = spine.GetPreview(width, height);
var savePath = Path.Combine(outputDir, $"{spine.Name}.png");
preview.SaveToFile(savePath);
success++;
}
catch (Exception ex)
{
Program.Logger.Error(ex.ToString());
Program.Logger.Error("Failed to save preview {}", spine.SkelPath);
error++;
}
worker.ReportProgress((int)((i + 1) * 100.0) / totalCount, $"已处理 {i + 1}/{totalCount}");
}
}
if (error > 0)
{
Program.Logger.Warn("Preview save {} successfully, {} failed", success, error);
}
else
{
Program.Logger.Info("{} preview saved successfully", success);
}
Program.Logger.Info($"Current memory usage: {Program.Process.WorkingSet64 / 1024.0 / 1024.0:F2} MB");
}
}
}

View File

@@ -120,4 +120,10 @@
<metadata name="contextMenuStrip.TrayLocation" type="System.Drawing.Point, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a">
<value>17, 17</value>
</metadata>
<metadata name="imageList_LargeIcon.TrayLocation" type="System.Drawing.Point, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a">
<value>552, 29</value>
</metadata>
<metadata name="imageList_SmallIcon.TrayLocation" type="System.Drawing.Point, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a">
<value>267, 34</value>
</metadata>
</root>

View File

@@ -24,11 +24,11 @@ namespace SpineViewer.Controls
public PreviewerProperty(SpinePreviewer previewer) { this.previewer = previewer; }
[TypeConverter(typeof(SizeTypeConverter))]
[TypeConverter(typeof(SizeConverter))]
[Category("导出"), DisplayName("分辨率")]
public Size Resolution { get => previewer.Resolution; set => previewer.Resolution = value; }
[TypeConverter(typeof(PointFTypeConverter))]
[TypeConverter(typeof(PointFConverter))]
[Category("导出"), DisplayName("画面中心点")]
public PointF Center { get => previewer.Center; set => previewer.Center = value; }
@@ -47,9 +47,6 @@ namespace SpineViewer.Controls
[Category("预览"), DisplayName("显示坐标轴")]
public bool ShowAxis { get => previewer.ShowAxis; set => previewer.ShowAxis = value; }
[Category("预览"), DisplayName("显示包围盒")]
public bool ShowBounds { get => previewer.ShowBounds; set => previewer.ShowBounds = value; }
[Category("预览"), DisplayName("最大帧率")]
public uint MaxFps { get => previewer.MaxFps; set => previewer.MaxFps = value; }
}
@@ -84,7 +81,6 @@ namespace SpineViewer.Controls
private readonly SFML.Graphics.RenderWindow RenderWindow;
private readonly SFML.System.Clock Clock = new();
private SFML.System.Vector2f? draggingSrc = null;
private Spine.Spine? draggingSpine = null;
private Task? task = null;
private CancellationTokenSource? cancelToken = null;
@@ -236,13 +232,6 @@ namespace SpineViewer.Controls
[Browsable(false)]
public bool ShowAxis { get; set; } = true;
/// <summary>
/// 显示包围盒
/// </summary>
[DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
[Browsable(false)]
public bool ShowBounds { get; set; } = true;
/// <summary>
/// 最大帧率
/// </summary>
@@ -328,7 +317,6 @@ namespace SpineViewer.Controls
// 右键优先级高, 进入画面拖动模式, 需要重新记录源点
if ((e.Button & MouseButtons.Right) != 0)
{
draggingSpine = null;
draggingSrc = RenderWindow.MapPixelToCoords(new(e.X, e.Y));
Cursor = Cursors.Hand;
}
@@ -342,12 +330,41 @@ namespace SpineViewer.Controls
{
lock (SpineListView.Spines)
{
foreach (var spine in SpineListView.Spines)
var spines = SpineListView.Spines;
// 没有按下 Ctrl 键就只选中点击的那个, 所以先清空选中列表
if ((ModifierKeys & Keys.Control) == 0)
{
if (spine.Bounds.Contains(src))
bool hit = false;
for (int i = 0; i < spines.Count; i++)
{
draggingSpine = spine;
break;
if (spines[i].Bounds.Contains(src))
{
hit = true;
// 如果点到了没被选中的东西, 则清空原先选中的, 改为只选中这一次点的
if (!SpineListView.SelectedIndices.Contains(i))
{
SpineListView.SelectedIndices.Clear();
SpineListView.SelectedIndices.Add(i);
}
break;
}
}
// 如果点了空白的地方, 就清空选中列表
if (!hit)
SpineListView.SelectedIndices.Clear();
}
else
{
for (int i = 0; i < spines.Count; i++)
{
if (spines[i].Bounds.Contains(src))
{
SpineListView.SelectedIndices.Add(i);
break;
}
}
}
}
@@ -371,8 +388,17 @@ namespace SpineViewer.Controls
}
else if ((e.Button & MouseButtons.Left) != 0)
{
if (draggingSpine is not null)
draggingSpine.Position += delta;
if (SpineListView is not null)
{
lock (SpineListView.Spines)
{
foreach (var spine in SpineListView.Spines)
{
if (spine.IsSelected)
spine.Position += delta;
}
}
}
draggingSrc = dst;
}
}
@@ -382,7 +408,6 @@ namespace SpineViewer.Controls
// 右键高优先级, 结束画面拖动模式
if ((e.Button & MouseButtons.Right) != 0)
{
draggingSpine = null;
SpineListView?.PropertyGrid?.Refresh();
draggingSrc = null;
@@ -393,7 +418,6 @@ namespace SpineViewer.Controls
else if ((e.Button & MouseButtons.Left) != 0 && (MouseButtons & MouseButtons.Right) == 0)
{
draggingSrc = null;
draggingSpine = null;
SpineListView?.PropertyGrid?.Refresh();
}
}
@@ -439,7 +463,7 @@ namespace SpineViewer.Controls
spine.Update(delta);
RenderWindow.Draw(spine);
if (ShowBounds)
if (spine.IsSelected)
{
var bounds = spine.Bounds;
BoundsRect[0] = BoundsRect[4] = new(new(bounds.Left, bounds.Top), BoundsColor);

View File

@@ -194,8 +194,9 @@
//
openFileDialog_Skel.AddExtension = false;
openFileDialog_Skel.AddToRecent = false;
openFileDialog_Skel.Filter = "skel 文件 (*.skel; *.json)|*.skel;*.json|所有文件 (*.*)|*.*";
openFileDialog_Skel.Filter = "skel 文件 (*.skel; *.json)|*.skel;*.json|二进制文件 (*.skel)|*.skel|文本文件 (*.json)|*.json|所有文件 (*.*)|*.*";
openFileDialog_Skel.Multiselect = true;
openFileDialog_Skel.Title = "批量选择skel文件";
//
// BatchOpenSpineDialog
//
@@ -213,6 +214,7 @@
ShowInTaskbar = false;
StartPosition = FormStartPosition.CenterScreen;
Text = "批量打开骨骼";
Load += BatchOpenSpineDialog_Load;
panel.ResumeLayout(false);
tableLayoutPanel1.ResumeLayout(false);
tableLayoutPanel1.PerformLayout();

View File

@@ -25,6 +25,11 @@ namespace SpineViewer.Dialogs
comboBox_Version.SelectedValue = Spine.Version.V38;
}
private void BatchOpenSpineDialog_Load(object sender, EventArgs e)
{
button_SelectSkel_Click(sender, e);
}
private void button_SelectSkel_Click(object sender, EventArgs e)
{
if (openFileDialog_Skel.ShowDialog() == DialogResult.OK)
@@ -38,6 +43,8 @@ namespace SpineViewer.Dialogs
private void button_Ok_Click(object sender, EventArgs e)
{
var version = (Spine.Version)comboBox_Version.SelectedValue;
if (listBox_FilePath.Items.Count <= 0)
{
MessageBox.Show("未选择任何文件", "错误信息", MessageBoxButtons.OK, MessageBoxIcon.Information);
@@ -53,8 +60,14 @@ namespace SpineViewer.Dialogs
}
}
if (!Spine.Spine.ImplementedVersions.Contains(version))
{
MessageBox.Show($"{version.String()} 版本尚未实现(咕咕咕~", "错误信息", MessageBoxButtons.OK, MessageBoxIcon.Information);
return;
}
SkelPaths = listBox_FilePath.Items.Cast<string>().ToArray();
Version = (Spine.Version)comboBox_Version.SelectedValue;
Version = version;
DialogResult = DialogResult.OK;
}

View File

@@ -0,0 +1,358 @@
namespace SpineViewer.Dialogs
{
partial class ConvertFileFormatDialog
{
/// <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()
{
System.ComponentModel.ComponentResourceManager resources = new System.ComponentModel.ComponentResourceManager(typeof(ConvertFileFormatDialog));
panel = new Panel();
tableLayoutPanel1 = new TableLayoutPanel();
flowLayoutPanel_TargetFormat = new FlowLayoutPanel();
radioButton_BinaryTarget = new RadioButton();
radioButton_JsonTarget = new RadioButton();
label1 = new Label();
label4 = new Label();
label3 = new Label();
comboBox_SourceVersion = new ComboBox();
tableLayoutPanel2 = new TableLayoutPanel();
button_Ok = new Button();
button_Cancel = new Button();
listBox_FilePath = new ListBox();
button_SelectSkel = new Button();
label_Tip = new Label();
label2 = new Label();
flowLayoutPanel_SourceFormat = new FlowLayoutPanel();
radioButton_BinarySource = new RadioButton();
radioButton_JsonSource = new RadioButton();
openFileDialog_Skel = new OpenFileDialog();
panel.SuspendLayout();
tableLayoutPanel1.SuspendLayout();
flowLayoutPanel_TargetFormat.SuspendLayout();
tableLayoutPanel2.SuspendLayout();
flowLayoutPanel_SourceFormat.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(1039, 530);
panel.TabIndex = 2;
//
// tableLayoutPanel1
//
tableLayoutPanel1.ColumnCount = 2;
tableLayoutPanel1.ColumnStyles.Add(new ColumnStyle());
tableLayoutPanel1.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 100F));
tableLayoutPanel1.Controls.Add(flowLayoutPanel_TargetFormat, 1, 5);
tableLayoutPanel1.Controls.Add(label1, 0, 4);
tableLayoutPanel1.Controls.Add(label4, 0, 0);
tableLayoutPanel1.Controls.Add(label3, 0, 3);
tableLayoutPanel1.Controls.Add(comboBox_SourceVersion, 1, 3);
tableLayoutPanel1.Controls.Add(tableLayoutPanel2, 0, 6);
tableLayoutPanel1.Controls.Add(listBox_FilePath, 0, 2);
tableLayoutPanel1.Controls.Add(button_SelectSkel, 0, 1);
tableLayoutPanel1.Controls.Add(label_Tip, 1, 1);
tableLayoutPanel1.Controls.Add(label2, 0, 5);
tableLayoutPanel1.Controls.Add(flowLayoutPanel_SourceFormat, 1, 4);
tableLayoutPanel1.Dock = DockStyle.Fill;
tableLayoutPanel1.Location = new Point(50, 15);
tableLayoutPanel1.Name = "tableLayoutPanel1";
tableLayoutPanel1.RowCount = 7;
tableLayoutPanel1.RowStyles.Add(new RowStyle());
tableLayoutPanel1.RowStyles.Add(new RowStyle());
tableLayoutPanel1.RowStyles.Add(new RowStyle());
tableLayoutPanel1.RowStyles.Add(new RowStyle());
tableLayoutPanel1.RowStyles.Add(new RowStyle());
tableLayoutPanel1.RowStyles.Add(new RowStyle());
tableLayoutPanel1.RowStyles.Add(new RowStyle());
tableLayoutPanel1.Size = new Size(939, 505);
tableLayoutPanel1.TabIndex = 1;
//
// flowLayoutPanel_TargetFormat
//
flowLayoutPanel_TargetFormat.AutoSize = true;
flowLayoutPanel_TargetFormat.Controls.Add(radioButton_BinaryTarget);
flowLayoutPanel_TargetFormat.Controls.Add(radioButton_JsonTarget);
flowLayoutPanel_TargetFormat.Dock = DockStyle.Fill;
flowLayoutPanel_TargetFormat.Location = new Point(146, 381);
flowLayoutPanel_TargetFormat.Name = "flowLayoutPanel_TargetFormat";
flowLayoutPanel_TargetFormat.Size = new Size(790, 34);
flowLayoutPanel_TargetFormat.TabIndex = 19;
//
// radioButton_BinaryTarget
//
radioButton_BinaryTarget.AutoSize = true;
radioButton_BinaryTarget.Location = new Point(3, 3);
radioButton_BinaryTarget.Name = "radioButton_BinaryTarget";
radioButton_BinaryTarget.Size = new Size(151, 28);
radioButton_BinaryTarget.TabIndex = 17;
radioButton_BinaryTarget.Text = "二进制 (*.skel)";
radioButton_BinaryTarget.UseVisualStyleBackColor = true;
radioButton_BinaryTarget.CheckedChanged += radioButton_Target_CheckedChanged;
//
// radioButton_JsonTarget
//
radioButton_JsonTarget.AutoSize = true;
radioButton_JsonTarget.Checked = true;
radioButton_JsonTarget.Location = new Point(160, 3);
radioButton_JsonTarget.Name = "radioButton_JsonTarget";
radioButton_JsonTarget.Size = new Size(135, 28);
radioButton_JsonTarget.TabIndex = 18;
radioButton_JsonTarget.TabStop = true;
radioButton_JsonTarget.Text = "文本 (*.json)";
radioButton_JsonTarget.UseVisualStyleBackColor = true;
radioButton_JsonTarget.CheckedChanged += radioButton_Target_CheckedChanged;
//
// label1
//
label1.Anchor = AnchorStyles.Right;
label1.AutoSize = true;
label1.Location = new Point(72, 346);
label1.Name = "label1";
label1.Size = new Size(68, 24);
label1.TabIndex = 15;
label1.Text = "源格式:";
//
// label4
//
label4.AutoSize = true;
tableLayoutPanel1.SetColumnSpan(label4, 4);
label4.Dock = DockStyle.Fill;
label4.Location = new Point(15, 15);
label4.Margin = new Padding(15);
label4.Name = "label4";
label4.Size = new Size(909, 24);
label4.TabIndex = 14;
label4.Text = "说明:将在每个文件同级目录下生成目标格式后缀的文件,会覆盖已存在文件";
label4.TextAlign = ContentAlignment.MiddleCenter;
//
// label3
//
label3.Anchor = AnchorStyles.Right;
label3.AutoSize = true;
label3.Location = new Point(72, 307);
label3.Name = "label3";
label3.Size = new Size(68, 24);
label3.TabIndex = 12;
label3.Text = "源版本:";
//
// comboBox_Version
//
comboBox_SourceVersion.Anchor = AnchorStyles.Left;
comboBox_SourceVersion.DropDownStyle = ComboBoxStyle.DropDownList;
comboBox_SourceVersion.FormattingEnabled = true;
comboBox_SourceVersion.Location = new Point(146, 303);
comboBox_SourceVersion.Name = "comboBox_Version";
comboBox_SourceVersion.Size = new Size(182, 32);
comboBox_SourceVersion.Sorted = true;
comboBox_SourceVersion.TabIndex = 13;
//
// tableLayoutPanel2
//
tableLayoutPanel2.AutoSize = true;
tableLayoutPanel2.AutoSizeMode = AutoSizeMode.GrowAndShrink;
tableLayoutPanel2.ColumnCount = 2;
tableLayoutPanel1.SetColumnSpan(tableLayoutPanel2, 4);
tableLayoutPanel2.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 50F));
tableLayoutPanel2.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 50F));
tableLayoutPanel2.Controls.Add(button_Ok, 0, 0);
tableLayoutPanel2.Controls.Add(button_Cancel, 1, 0);
tableLayoutPanel2.Dock = DockStyle.Bottom;
tableLayoutPanel2.Location = new Point(3, 462);
tableLayoutPanel2.Name = "tableLayoutPanel2";
tableLayoutPanel2.RowCount = 1;
tableLayoutPanel2.RowStyles.Add(new RowStyle());
tableLayoutPanel2.Size = new Size(933, 40);
tableLayoutPanel2.TabIndex = 11;
//
// button_Ok
//
button_Ok.Anchor = AnchorStyles.Bottom | AnchorStyles.Right;
button_Ok.Location = new Point(324, 3);
button_Ok.Margin = new Padding(3, 3, 30, 3);
button_Ok.Name = "button_Ok";
button_Ok.Size = new Size(112, 34);
button_Ok.TabIndex = 7;
button_Ok.Text = "确认";
button_Ok.UseVisualStyleBackColor = true;
button_Ok.Click += button_Ok_Click;
//
// button_Cancel
//
button_Cancel.Anchor = AnchorStyles.Bottom | AnchorStyles.Left;
button_Cancel.Location = new Point(496, 3);
button_Cancel.Margin = new Padding(30, 3, 3, 3);
button_Cancel.Name = "button_Cancel";
button_Cancel.Size = new Size(112, 34);
button_Cancel.TabIndex = 8;
button_Cancel.Text = "取消";
button_Cancel.UseVisualStyleBackColor = true;
button_Cancel.Click += button_Cancel_Click;
//
// listBox_FilePath
//
tableLayoutPanel1.SetColumnSpan(listBox_FilePath, 2);
listBox_FilePath.Dock = DockStyle.Fill;
listBox_FilePath.FormattingEnabled = true;
listBox_FilePath.HorizontalScrollbar = true;
listBox_FilePath.ItemHeight = 24;
listBox_FilePath.Location = new Point(3, 97);
listBox_FilePath.Name = "listBox_FilePath";
listBox_FilePath.Size = new Size(933, 200);
listBox_FilePath.TabIndex = 2;
//
// button_SelectSkel
//
button_SelectSkel.Anchor = AnchorStyles.None;
button_SelectSkel.Location = new Point(3, 57);
button_SelectSkel.Name = "button_SelectSkel";
button_SelectSkel.Size = new Size(137, 34);
button_SelectSkel.TabIndex = 1;
button_SelectSkel.Text = "选择文件...";
button_SelectSkel.UseVisualStyleBackColor = true;
button_SelectSkel.Click += button_SelectSkel_Click;
//
// label_Tip
//
label_Tip.AutoSize = true;
label_Tip.Dock = DockStyle.Fill;
label_Tip.Location = new Point(146, 54);
label_Tip.Name = "label_Tip";
label_Tip.Size = new Size(790, 40);
label_Tip.TabIndex = 0;
label_Tip.Text = "已选择 0 个文件";
label_Tip.TextAlign = ContentAlignment.MiddleLeft;
//
// label2
//
label2.Anchor = AnchorStyles.Right;
label2.AutoSize = true;
label2.Location = new Point(54, 386);
label2.Name = "label2";
label2.Size = new Size(86, 24);
label2.TabIndex = 16;
label2.Text = "目标格式:";
//
// flowLayoutPanel_SourceFormat
//
flowLayoutPanel_SourceFormat.AutoSize = true;
flowLayoutPanel_SourceFormat.Controls.Add(radioButton_BinarySource);
flowLayoutPanel_SourceFormat.Controls.Add(radioButton_JsonSource);
flowLayoutPanel_SourceFormat.Dock = DockStyle.Fill;
flowLayoutPanel_SourceFormat.Location = new Point(146, 341);
flowLayoutPanel_SourceFormat.Name = "flowLayoutPanel_SourceFormat";
flowLayoutPanel_SourceFormat.Size = new Size(790, 34);
flowLayoutPanel_SourceFormat.TabIndex = 18;
//
// radioButton_BinarySource
//
radioButton_BinarySource.AutoSize = true;
radioButton_BinarySource.Checked = true;
radioButton_BinarySource.Location = new Point(3, 3);
radioButton_BinarySource.Name = "radioButton_BinarySource";
radioButton_BinarySource.Size = new Size(151, 28);
radioButton_BinarySource.TabIndex = 17;
radioButton_BinarySource.TabStop = true;
radioButton_BinarySource.Text = "二进制 (*.skel)";
radioButton_BinarySource.UseVisualStyleBackColor = true;
radioButton_BinarySource.CheckedChanged += radioButton_Source_CheckedChanged;
//
// radioButton_JsonSource
//
radioButton_JsonSource.AutoSize = true;
radioButton_JsonSource.Location = new Point(160, 3);
radioButton_JsonSource.Name = "radioButton_JsonSource";
radioButton_JsonSource.Size = new Size(135, 28);
radioButton_JsonSource.TabIndex = 18;
radioButton_JsonSource.Text = "文本 (*.json)";
radioButton_JsonSource.UseVisualStyleBackColor = true;
radioButton_JsonSource.CheckedChanged += radioButton_Source_CheckedChanged;
//
// openFileDialog_Skel
//
openFileDialog_Skel.AddExtension = false;
openFileDialog_Skel.AddToRecent = false;
openFileDialog_Skel.Filter = "skel 文件 (*.skel; *.json)|*.skel;*.json|二进制文件 (*.skel)|*.skel|文本文件 (*.json)|*.json|所有文件 (*.*)|*.*";
openFileDialog_Skel.Multiselect = true;
openFileDialog_Skel.Title = "批量选择skel文件";
//
// ConvertFileFormatDialog
//
AcceptButton = button_Ok;
AutoScaleDimensions = new SizeF(11F, 24F);
AutoScaleMode = AutoScaleMode.Font;
CancelButton = button_Cancel;
ClientSize = new Size(1039, 530);
Controls.Add(panel);
FormBorderStyle = FormBorderStyle.FixedDialog;
Icon = (Icon)resources.GetObject("$this.Icon");
MaximizeBox = false;
MinimizeBox = false;
Name = "ConvertFileFormatDialog";
ShowInTaskbar = false;
StartPosition = FormStartPosition.CenterScreen;
Text = "骨骼文件格式转换";
Load += ConvertFileFormatDialog_Load;
panel.ResumeLayout(false);
tableLayoutPanel1.ResumeLayout(false);
tableLayoutPanel1.PerformLayout();
flowLayoutPanel_TargetFormat.ResumeLayout(false);
flowLayoutPanel_TargetFormat.PerformLayout();
tableLayoutPanel2.ResumeLayout(false);
flowLayoutPanel_SourceFormat.ResumeLayout(false);
flowLayoutPanel_SourceFormat.PerformLayout();
ResumeLayout(false);
}
#endregion
private Panel panel;
private TableLayoutPanel tableLayoutPanel1;
private Label label4;
private Label label3;
private ComboBox comboBox_SourceVersion;
private TableLayoutPanel tableLayoutPanel2;
private Button button_Ok;
private Button button_Cancel;
private ListBox listBox_FilePath;
private Button button_SelectSkel;
private Label label_Tip;
private OpenFileDialog openFileDialog_Skel;
private Label label1;
private Label label2;
private RadioButton radioButton_BinarySource;
private FlowLayoutPanel flowLayoutPanel_SourceFormat;
private RadioButton radioButton_JsonSource;
private FlowLayoutPanel flowLayoutPanel_TargetFormat;
private RadioButton radioButton_BinaryTarget;
private RadioButton radioButton_JsonTarget;
}
}

View File

@@ -0,0 +1,121 @@
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 ConvertFileFormatDialog : Form
{
public string[] SkelPaths { get; private set; }
public Spine.Version SourceVersion { get; private set; }
public Spine.Version TargetVersion { get; private set; }
public bool JsonSource { get; private set; }
public bool JsonTarget { get; private set; }
public ConvertFileFormatDialog()
{
InitializeComponent();
comboBox_SourceVersion.DataSource = VersionHelper.Versions.ToList();
comboBox_SourceVersion.DisplayMember = "Value";
comboBox_SourceVersion.ValueMember = "Key";
comboBox_SourceVersion.SelectedValue = Spine.Version.V38;
//comboBox_TargetVersion.DataSource = VersionHelper.Versions.ToList();
//comboBox_TargetVersion.DisplayMember = "Value";
//comboBox_TargetVersion.ValueMember = "Key";
//comboBox_TargetVersion.SelectedValue = Spine.Version.V38;
}
private void ConvertFileFormatDialog_Load(object sender, EventArgs e)
{
button_SelectSkel_Click(sender, e);
}
private void button_SelectSkel_Click(object sender, EventArgs e)
{
if (openFileDialog_Skel.ShowDialog() == DialogResult.OK)
{
listBox_FilePath.Items.Clear();
foreach (var p in openFileDialog_Skel.FileNames)
listBox_FilePath.Items.Add(Path.GetFullPath(p));
label_Tip.Text = $"已选择 {listBox_FilePath.Items.Count} 个文件";
}
}
private void button_Ok_Click(object sender, EventArgs e)
{
var sourceVersion = (Spine.Version)comboBox_SourceVersion.SelectedValue;
var targetVersion = (Spine.Version)comboBox_SourceVersion.SelectedValue; // TODO: 增加目标版本
var jsonSource = radioButton_JsonSource.Checked;
var jsonTarget = radioButton_JsonTarget.Checked;
if (listBox_FilePath.Items.Count <= 0)
{
MessageBox.Show("未选择任何文件", "错误信息", MessageBoxButtons.OK, MessageBoxIcon.Information);
return;
}
foreach (string p in listBox_FilePath.Items)
{
if (!File.Exists(p))
{
MessageBox.Show($"{p}", "skel文件不存在", MessageBoxButtons.OK, MessageBoxIcon.Information);
return;
}
}
if (!SkeletonConverter.ImplementedVersions.Contains(sourceVersion))
{
MessageBox.Show($"{sourceVersion.String()} 版本尚未实现(咕咕咕~", "错误信息", MessageBoxButtons.OK, MessageBoxIcon.Information);
return;
}
if (!SkeletonConverter.ImplementedVersions.Contains(targetVersion))
{
MessageBox.Show($"{targetVersion.String()} 版本尚未实现(咕咕咕~", "错误信息", MessageBoxButtons.OK, MessageBoxIcon.Information);
return;
}
if (jsonSource == jsonTarget && sourceVersion == targetVersion)
{
MessageBox.Show($"不需要转换相同的格式和版本", "错误信息", MessageBoxButtons.OK, MessageBoxIcon.Information);
return;
}
SkelPaths = listBox_FilePath.Items.Cast<string>().ToArray();
SourceVersion = sourceVersion;
TargetVersion = targetVersion;
JsonSource = jsonSource;
JsonTarget = jsonTarget;
DialogResult = DialogResult.OK;
}
private void button_Cancel_Click(object sender, EventArgs e)
{
DialogResult = DialogResult.Cancel;
}
private void radioButton_Source_CheckedChanged(object sender, EventArgs e)
{
if (radioButton_BinarySource.Checked)
radioButton_JsonTarget.Checked = true;
else
radioButton_BinaryTarget.Checked = true;
}
private void radioButton_Target_CheckedChanged(object sender, EventArgs e)
{
if (radioButton_BinaryTarget.Checked)
radioButton_JsonSource.Checked = true;
else
radioButton_BinarySource.Checked = true;
}
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -239,6 +239,7 @@
ShowInTaskbar = false;
StartPosition = FormStartPosition.CenterScreen;
Text = "导出PNG序列";
Load += ExportPngDialog_Load;
panel1.ResumeLayout(false);
panel1.PerformLayout();
tableLayoutPanel1.ResumeLayout(false);

View File

@@ -21,6 +21,11 @@ namespace SpineViewer.Dialogs
InitializeComponent();
}
private void ExportPngDialog_Load(object sender, EventArgs e)
{
button_SelectOutputDir_Click(sender, e);
}
private void button_SelectOutputDir_Click(object sender, EventArgs e)
{
folderBrowserDialog.InitialDirectory = textBox_OutputDir.Text;

View File

@@ -0,0 +1,270 @@
namespace SpineViewer.Dialogs
{
partial class ExportPreviewDialog
{
/// <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()
{
System.ComponentModel.ComponentResourceManager resources = new System.ComponentModel.ComponentResourceManager(typeof(ExportPreviewDialog));
panel1 = new Panel();
tableLayoutPanel1 = new TableLayoutPanel();
label4 = new Label();
label1 = new Label();
label2 = new Label();
label3 = new Label();
textBox_OutputDir = new TextBox();
button_SelectOutputDir = new Button();
tableLayoutPanel2 = new TableLayoutPanel();
button_Ok = new Button();
button_Cancel = new Button();
numericUpDown_Width = new NumericUpDown();
numericUpDown_Height = new NumericUpDown();
folderBrowserDialog = new FolderBrowserDialog();
panel1.SuspendLayout();
tableLayoutPanel1.SuspendLayout();
tableLayoutPanel2.SuspendLayout();
((System.ComponentModel.ISupportInitialize)numericUpDown_Width).BeginInit();
((System.ComponentModel.ISupportInitialize)numericUpDown_Height).BeginInit();
SuspendLayout();
//
// panel1
//
panel1.Controls.Add(tableLayoutPanel1);
panel1.Dock = DockStyle.Fill;
panel1.Location = new Point(0, 0);
panel1.Name = "panel1";
panel1.Padding = new Padding(50, 15, 50, 10);
panel1.Size = new Size(919, 276);
panel1.TabIndex = 2;
//
// tableLayoutPanel1
//
tableLayoutPanel1.AutoSize = true;
tableLayoutPanel1.ColumnCount = 4;
tableLayoutPanel1.ColumnStyles.Add(new ColumnStyle());
tableLayoutPanel1.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 50F));
tableLayoutPanel1.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 50F));
tableLayoutPanel1.ColumnStyles.Add(new ColumnStyle());
tableLayoutPanel1.Controls.Add(label4, 0, 0);
tableLayoutPanel1.Controls.Add(label1, 0, 1);
tableLayoutPanel1.Controls.Add(label2, 0, 2);
tableLayoutPanel1.Controls.Add(label3, 0, 3);
tableLayoutPanel1.Controls.Add(textBox_OutputDir, 1, 1);
tableLayoutPanel1.Controls.Add(button_SelectOutputDir, 3, 1);
tableLayoutPanel1.Controls.Add(tableLayoutPanel2, 0, 4);
tableLayoutPanel1.Controls.Add(numericUpDown_Width, 1, 2);
tableLayoutPanel1.Controls.Add(numericUpDown_Height, 1, 3);
tableLayoutPanel1.Dock = DockStyle.Fill;
tableLayoutPanel1.Location = new Point(50, 15);
tableLayoutPanel1.Name = "tableLayoutPanel1";
tableLayoutPanel1.RowCount = 5;
tableLayoutPanel1.RowStyles.Add(new RowStyle());
tableLayoutPanel1.RowStyles.Add(new RowStyle());
tableLayoutPanel1.RowStyles.Add(new RowStyle());
tableLayoutPanel1.RowStyles.Add(new RowStyle());
tableLayoutPanel1.RowStyles.Add(new RowStyle());
tableLayoutPanel1.Size = new Size(819, 251);
tableLayoutPanel1.TabIndex = 0;
//
// label4
//
label4.AutoSize = true;
tableLayoutPanel1.SetColumnSpan(label4, 4);
label4.Dock = DockStyle.Fill;
label4.Location = new Point(15, 15);
label4.Margin = new Padding(15);
label4.Name = "label4";
label4.Size = new Size(789, 24);
label4.TabIndex = 11;
label4.Text = "说明:导出的文件名与骨骼文件名相同";
label4.TextAlign = ContentAlignment.MiddleCenter;
//
// label1
//
label1.Anchor = AnchorStyles.Right;
label1.AutoSize = true;
label1.Location = new Point(3, 62);
label1.Name = "label1";
label1.Size = new Size(104, 24);
label1.TabIndex = 0;
label1.Text = "输出文件夹:";
//
// label2
//
label2.Anchor = AnchorStyles.Right;
label2.AutoSize = true;
label2.Location = new Point(75, 100);
label2.Name = "label2";
label2.Size = new Size(32, 24);
label2.TabIndex = 1;
label2.Text = "宽:";
//
// label3
//
label3.Anchor = AnchorStyles.Right;
label3.AutoSize = true;
label3.Location = new Point(75, 136);
label3.Name = "label3";
label3.Size = new Size(32, 24);
label3.TabIndex = 2;
label3.Text = "高:";
//
// textBox_OutputDir
//
tableLayoutPanel1.SetColumnSpan(textBox_OutputDir, 2);
textBox_OutputDir.Dock = DockStyle.Fill;
textBox_OutputDir.Location = new Point(113, 57);
textBox_OutputDir.Name = "textBox_OutputDir";
textBox_OutputDir.Size = new Size(664, 30);
textBox_OutputDir.TabIndex = 3;
//
// button_SelectOutputDir
//
button_SelectOutputDir.AutoSize = true;
button_SelectOutputDir.AutoSizeMode = AutoSizeMode.GrowAndShrink;
button_SelectOutputDir.Location = new Point(783, 57);
button_SelectOutputDir.Name = "button_SelectOutputDir";
button_SelectOutputDir.Size = new Size(32, 34);
button_SelectOutputDir.TabIndex = 5;
button_SelectOutputDir.Text = "...";
button_SelectOutputDir.UseVisualStyleBackColor = true;
button_SelectOutputDir.Click += button_SelectOutputDir_Click;
//
// tableLayoutPanel2
//
tableLayoutPanel2.AutoSize = true;
tableLayoutPanel2.AutoSizeMode = AutoSizeMode.GrowAndShrink;
tableLayoutPanel2.ColumnCount = 2;
tableLayoutPanel1.SetColumnSpan(tableLayoutPanel2, 4);
tableLayoutPanel2.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 50F));
tableLayoutPanel2.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 50F));
tableLayoutPanel2.Controls.Add(button_Ok, 0, 0);
tableLayoutPanel2.Controls.Add(button_Cancel, 1, 0);
tableLayoutPanel2.Dock = DockStyle.Bottom;
tableLayoutPanel2.Location = new Point(3, 208);
tableLayoutPanel2.Name = "tableLayoutPanel2";
tableLayoutPanel2.RowCount = 1;
tableLayoutPanel2.RowStyles.Add(new RowStyle());
tableLayoutPanel2.Size = new Size(813, 40);
tableLayoutPanel2.TabIndex = 10;
//
// button_Ok
//
button_Ok.Anchor = AnchorStyles.Bottom | AnchorStyles.Right;
button_Ok.Location = new Point(264, 3);
button_Ok.Margin = new Padding(3, 3, 30, 3);
button_Ok.Name = "button_Ok";
button_Ok.Size = new Size(112, 34);
button_Ok.TabIndex = 7;
button_Ok.Text = "确认";
button_Ok.UseVisualStyleBackColor = true;
button_Ok.Click += button_Ok_Click;
//
// button_Cancel
//
button_Cancel.Anchor = AnchorStyles.Bottom | AnchorStyles.Left;
button_Cancel.Location = new Point(436, 3);
button_Cancel.Margin = new Padding(30, 3, 3, 3);
button_Cancel.Name = "button_Cancel";
button_Cancel.Size = new Size(112, 34);
button_Cancel.TabIndex = 8;
button_Cancel.Text = "取消";
button_Cancel.UseVisualStyleBackColor = true;
button_Cancel.Click += button_Cancel_Click;
//
// numericUpDown_Width
//
numericUpDown_Width.Anchor = AnchorStyles.Left;
numericUpDown_Width.Location = new Point(113, 97);
numericUpDown_Width.Maximum = new decimal(new int[] { 4096, 0, 0, 0 });
numericUpDown_Width.Minimum = new decimal(new int[] { 32, 0, 0, 0 });
numericUpDown_Width.Name = "numericUpDown_Width";
numericUpDown_Width.Size = new Size(180, 30);
numericUpDown_Width.TabIndex = 12;
numericUpDown_Width.TextAlign = HorizontalAlignment.Right;
numericUpDown_Width.Value = new decimal(new int[] { 256, 0, 0, 0 });
//
// numericUpDown_Height
//
numericUpDown_Height.Anchor = AnchorStyles.Left;
numericUpDown_Height.Location = new Point(113, 133);
numericUpDown_Height.Maximum = new decimal(new int[] { 4096, 0, 0, 0 });
numericUpDown_Height.Minimum = new decimal(new int[] { 32, 0, 0, 0 });
numericUpDown_Height.Name = "numericUpDown_Height";
numericUpDown_Height.Size = new Size(180, 30);
numericUpDown_Height.TabIndex = 13;
numericUpDown_Height.TextAlign = HorizontalAlignment.Right;
numericUpDown_Height.Value = new decimal(new int[] { 256, 0, 0, 0 });
//
// folderBrowserDialog
//
folderBrowserDialog.AddToRecent = false;
//
// ExportPreviewDialog
//
AcceptButton = button_Ok;
AutoScaleDimensions = new SizeF(11F, 24F);
AutoScaleMode = AutoScaleMode.Font;
CancelButton = button_Cancel;
ClientSize = new Size(919, 276);
Controls.Add(panel1);
FormBorderStyle = FormBorderStyle.FixedDialog;
Icon = (Icon)resources.GetObject("$this.Icon");
MaximizeBox = false;
MinimizeBox = false;
Name = "ExportPreviewDialog";
ShowInTaskbar = false;
StartPosition = FormStartPosition.CenterScreen;
Text = "导出预览图";
Load += ExportPreviewDialog_Load;
panel1.ResumeLayout(false);
panel1.PerformLayout();
tableLayoutPanel1.ResumeLayout(false);
tableLayoutPanel1.PerformLayout();
tableLayoutPanel2.ResumeLayout(false);
((System.ComponentModel.ISupportInitialize)numericUpDown_Width).EndInit();
((System.ComponentModel.ISupportInitialize)numericUpDown_Height).EndInit();
ResumeLayout(false);
}
#endregion
private Panel panel1;
private TableLayoutPanel tableLayoutPanel1;
private Label label4;
private Label label1;
private Label label2;
private Label label3;
private TextBox textBox_OutputDir;
private Button button_SelectOutputDir;
private TableLayoutPanel tableLayoutPanel2;
private Button button_Ok;
private Button button_Cancel;
private NumericUpDown numericUpDown_Width;
private NumericUpDown numericUpDown_Height;
private FolderBrowserDialog folderBrowserDialog;
}
}

View File

@@ -0,0 +1,80 @@
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 ExportPreviewDialog: Form
{
public string OutputDir { get; private set; }
public uint PreviewWidth { get; private set; }
public uint PreviewHeight { get; private set; }
public ExportPreviewDialog()
{
InitializeComponent();
}
private void ExportPreviewDialog_Load(object sender, EventArgs e)
{
button_SelectOutputDir_Click(sender, e);
}
private void button_SelectOutputDir_Click(object sender, EventArgs e)
{
folderBrowserDialog.InitialDirectory = textBox_OutputDir.Text;
if (folderBrowserDialog.ShowDialog() == DialogResult.OK)
{
textBox_OutputDir.Text = Path.GetFullPath(folderBrowserDialog.SelectedPath);
}
}
private void button_Ok_Click(object sender, EventArgs e)
{
var outputDir = textBox_OutputDir.Text;
if (File.Exists(outputDir))
{
MessageBox.Show("输出文件夹无效", "错误信息", MessageBoxButtons.OK, MessageBoxIcon.Information);
return;
}
if (!Directory.Exists(outputDir))
{
if (MessageBox.Show($"文件夹 {outputDir} 不存在,是否创建?", "操作确认", MessageBoxButtons.OKCancel, MessageBoxIcon.Question) == DialogResult.OK)
{
try
{
Directory.CreateDirectory(outputDir);
}
catch (Exception ex)
{
Program.Logger.Error(ex.ToString());
MessageBox.Show(ex.ToString(), "文件夹创建失败", MessageBoxButtons.OK, MessageBoxIcon.Error);
return;
}
}
else
{
return;
}
}
OutputDir = Path.GetFullPath(outputDir);
PreviewWidth = (uint)numericUpDown_Width.Value;
PreviewHeight = (uint)numericUpDown_Height.Value;
DialogResult = DialogResult.OK;
}
private void button_Cancel_Click(object sender, EventArgs e)
{
DialogResult = DialogResult.Cancel;
}
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -232,13 +232,15 @@
//
openFileDialog_Skel.AddExtension = false;
openFileDialog_Skel.AddToRecent = false;
openFileDialog_Skel.Filter = "skel 文件 (*.skel; *.json)|*.skel;*.json|所有文件 (*.*)|*.*";
openFileDialog_Skel.Filter = "skel 文件 (*.skel; *.json)|*.skel;*.json|二进制文件 (*.skel)|*.skel|文本文件 (*.json)|*.json|所有文件 (*.*)|*.*";
openFileDialog_Skel.Title = "选择skel文件";
//
// openFileDialog_Atlas
//
openFileDialog_Atlas.AddExtension = false;
openFileDialog_Atlas.AddToRecent = false;
openFileDialog_Atlas.Filter = "atlas 文件 (*.atlas)|*.atlas|所有文件 (*.*)|*.*";
openFileDialog_Atlas.Title = "选择atlas文件";
//
// OpenSpineDialog
//
@@ -256,6 +258,7 @@
ShowInTaskbar = false;
StartPosition = FormStartPosition.CenterScreen;
Text = "打开骨骼";
Load += OpenSpineDialog_Load;
panel1.ResumeLayout(false);
panel1.PerformLayout();
tableLayoutPanel1.ResumeLayout(false);

View File

@@ -8,7 +8,6 @@ using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms;
namespace SpineViewer.Dialogs
{
public partial class OpenSpineDialog : Form
@@ -26,6 +25,11 @@ namespace SpineViewer.Dialogs
comboBox_Version.SelectedValue = Spine.Version.V38;
}
private void OpenSpineDialog_Load(object sender, EventArgs e)
{
button_SelectSkel_Click(sender, e);
}
private void button_SelectSkel_Click(object sender, EventArgs e)
{
openFileDialog_Skel.InitialDirectory = Path.GetDirectoryName(textBox_SkelPath.Text);
@@ -48,6 +52,7 @@ namespace SpineViewer.Dialogs
{
var skelPath = textBox_SkelPath.Text;
var atlasPath = textBox_AtlasPath.Text;
var version = (Spine.Version)comboBox_Version.SelectedValue;
if (!File.Exists(skelPath))
{
@@ -73,9 +78,15 @@ namespace SpineViewer.Dialogs
atlasPath = Path.GetFullPath(atlasPath);
}
if (!Spine.Spine.ImplementedVersions.Contains(version))
{
MessageBox.Show($"{version.String()} 版本尚未实现(咕咕咕~", "错误信息", MessageBoxButtons.OK, MessageBoxIcon.Information);
return;
}
SkelPath = skelPath;
AtlasPath = atlasPath;
Version = (Spine.Version)comboBox_Version.SelectedValue;
Version = version;
DialogResult = DialogResult.OK;
}

View File

@@ -118,10 +118,10 @@
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<metadata name="openFileDialog_Skel.TrayLocation" type="System.Drawing.Point, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a">
<value>75, 24</value>
<value>58, 25</value>
</metadata>
<metadata name="openFileDialog_Atlas.TrayLocation" type="System.Drawing.Point, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a">
<value>317, 22</value>
<value>349, 29</value>
</metadata>
<assembly alias="System.Drawing" name="System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a" />
<data name="$this.Icon" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">

View File

@@ -36,10 +36,15 @@
toolStripMenuItem_BatchOpen = new ToolStripMenuItem();
toolStripSeparator1 = new ToolStripSeparator();
toolStripMenuItem_Export = new ToolStripMenuItem();
toolStripMenuItem_ExportPreview = new ToolStripMenuItem();
toolStripSeparator2 = new ToolStripSeparator();
toolStripMenuItem_Exit = new ToolStripMenuItem();
toolStripMenuItem_Function = new ToolStripMenuItem();
toolStripMenuItem_ResetAnimation = new ToolStripMenuItem();
toolStripMenuItem_Tool = new ToolStripMenuItem();
toolStripMenuItem_ConvertFileFormat = new ToolStripMenuItem();
toolStripMenuItem_Download = new ToolStripMenuItem();
toolStripMenuItem_ManageResource = new ToolStripMenuItem();
toolStripMenuItem_Help = new ToolStripMenuItem();
toolStripMenuItem_Diagnostics = new ToolStripMenuItem();
toolStripSeparator3 = new ToolStripSeparator();
@@ -87,7 +92,7 @@
//
menuStrip.BackColor = SystemColors.Control;
menuStrip.ImageScalingSize = new Size(24, 24);
menuStrip.Items.AddRange(new ToolStripItem[] { toolStripMenuItem_File, toolStripMenuItem_Function, toolStripMenuItem_Help });
menuStrip.Items.AddRange(new ToolStripItem[] { toolStripMenuItem_File, toolStripMenuItem_Function, toolStripMenuItem_Tool, toolStripMenuItem_Download, toolStripMenuItem_Help });
menuStrip.Location = new Point(0, 0);
menuStrip.Name = "menuStrip";
menuStrip.Size = new Size(1741, 32);
@@ -96,7 +101,7 @@
//
// toolStripMenuItem_File
//
toolStripMenuItem_File.DropDownItems.AddRange(new ToolStripItem[] { toolStripMenuItem_Open, toolStripMenuItem_BatchOpen, toolStripSeparator1, toolStripMenuItem_Export, toolStripSeparator2, toolStripMenuItem_Exit });
toolStripMenuItem_File.DropDownItems.AddRange(new ToolStripItem[] { toolStripMenuItem_Open, toolStripMenuItem_BatchOpen, toolStripSeparator1, toolStripMenuItem_Export, toolStripMenuItem_ExportPreview, toolStripSeparator2, toolStripMenuItem_Exit });
toolStripMenuItem_File.Name = "toolStripMenuItem_File";
toolStripMenuItem_File.Size = new Size(84, 28);
toolStripMenuItem_File.Text = "文件(&F)";
@@ -129,6 +134,13 @@
toolStripMenuItem_Export.Text = "导出(&E)...";
toolStripMenuItem_Export.Click += toolStripMenuItem_Export_Click;
//
// toolStripMenuItem_ExportPreview
//
toolStripMenuItem_ExportPreview.Name = "toolStripMenuItem_ExportPreview";
toolStripMenuItem_ExportPreview.Size = new Size(254, 34);
toolStripMenuItem_ExportPreview.Text = "导出预览图(&P)...";
toolStripMenuItem_ExportPreview.Click += toolStripMenuItem_ExportPreview_Click;
//
// toolStripSeparator2
//
toolStripSeparator2.Name = "toolStripSeparator2";
@@ -156,6 +168,34 @@
toolStripMenuItem_ResetAnimation.Text = "重置动画时间(&R)";
toolStripMenuItem_ResetAnimation.Click += toolStripMenuItem_ResetAnimation_Click;
//
// toolStripMenuItem_Tool
//
toolStripMenuItem_Tool.DropDownItems.AddRange(new ToolStripItem[] { toolStripMenuItem_ConvertFileFormat });
toolStripMenuItem_Tool.Name = "toolStripMenuItem_Tool";
toolStripMenuItem_Tool.Size = new Size(84, 28);
toolStripMenuItem_Tool.Text = "工具(&T)";
//
// toolStripMenuItem_ConvertFileFormat
//
toolStripMenuItem_ConvertFileFormat.Name = "toolStripMenuItem_ConvertFileFormat";
toolStripMenuItem_ConvertFileFormat.Size = new Size(254, 34);
toolStripMenuItem_ConvertFileFormat.Text = "转换文件格式(&C)...";
toolStripMenuItem_ConvertFileFormat.Click += toolStripMenuItem_ConvertFileFormat_Click;
//
// toolStripMenuItem_Download
//
toolStripMenuItem_Download.DropDownItems.AddRange(new ToolStripItem[] { toolStripMenuItem_ManageResource });
toolStripMenuItem_Download.Name = "toolStripMenuItem_Download";
toolStripMenuItem_Download.Size = new Size(88, 28);
toolStripMenuItem_Download.Text = "下载(&D)";
//
// toolStripMenuItem_ManageResource
//
toolStripMenuItem_ManageResource.Name = "toolStripMenuItem_ManageResource";
toolStripMenuItem_ManageResource.Size = new Size(260, 34);
toolStripMenuItem_ManageResource.Text = "管理下载资源(&M)...";
toolStripMenuItem_ManageResource.Click += toolStripMenuItem_ManageResource_Click;
//
// toolStripMenuItem_Help
//
toolStripMenuItem_Help.DropDownItems.AddRange(new ToolStripItem[] { toolStripMenuItem_Diagnostics, toolStripSeparator3, toolStripMenuItem_About });
@@ -261,7 +301,7 @@
splitContainer_Information.Panel2.Controls.Add(splitContainer_Config);
splitContainer_Information.Panel2.Cursor = Cursors.Default;
splitContainer_Information.Size = new Size(744, 848);
splitContainer_Information.SplitterDistance = 327;
splitContainer_Information.SplitterDistance = 398;
splitContainer_Information.TabIndex = 1;
splitContainer_Information.TabStop = false;
splitContainer_Information.SplitterMoved += splitContainer_SplitterMoved;
@@ -273,7 +313,7 @@
groupBox_SkelList.Dock = DockStyle.Fill;
groupBox_SkelList.Location = new Point(0, 0);
groupBox_SkelList.Name = "groupBox_SkelList";
groupBox_SkelList.Size = new Size(327, 848);
groupBox_SkelList.Size = new Size(398, 848);
groupBox_SkelList.TabIndex = 0;
groupBox_SkelList.TabStop = false;
groupBox_SkelList.Text = "模型列表";
@@ -284,7 +324,7 @@
spineListView.Location = new Point(3, 26);
spineListView.Name = "spineListView";
spineListView.PropertyGrid = propertyGrid_Spine;
spineListView.Size = new Size(321, 819);
spineListView.Size = new Size(392, 819);
spineListView.TabIndex = 0;
//
// propertyGrid_Spine
@@ -293,7 +333,7 @@
propertyGrid_Spine.HelpVisible = false;
propertyGrid_Spine.Location = new Point(3, 26);
propertyGrid_Spine.Name = "propertyGrid_Spine";
propertyGrid_Spine.Size = new Size(407, 470);
propertyGrid_Spine.Size = new Size(336, 470);
propertyGrid_Spine.TabIndex = 0;
propertyGrid_Spine.ToolbarVisible = false;
propertyGrid_Spine.PropertyValueChanged += propertyGrid_PropertyValueChanged;
@@ -315,7 +355,7 @@
//
splitContainer_Config.Panel2.Controls.Add(groupBox_PreviewConfig);
splitContainer_Config.Panel2.Cursor = Cursors.Default;
splitContainer_Config.Size = new Size(413, 848);
splitContainer_Config.Size = new Size(342, 848);
splitContainer_Config.SplitterDistance = 499;
splitContainer_Config.TabIndex = 0;
splitContainer_Config.TabStop = false;
@@ -328,7 +368,7 @@
groupBox_SkelConfig.Dock = DockStyle.Fill;
groupBox_SkelConfig.Location = new Point(0, 0);
groupBox_SkelConfig.Name = "groupBox_SkelConfig";
groupBox_SkelConfig.Size = new Size(413, 499);
groupBox_SkelConfig.Size = new Size(342, 499);
groupBox_SkelConfig.TabIndex = 0;
groupBox_SkelConfig.TabStop = false;
groupBox_SkelConfig.Text = "模型参数";
@@ -339,7 +379,7 @@
groupBox_PreviewConfig.Dock = DockStyle.Fill;
groupBox_PreviewConfig.Location = new Point(0, 0);
groupBox_PreviewConfig.Name = "groupBox_PreviewConfig";
groupBox_PreviewConfig.Size = new Size(413, 345);
groupBox_PreviewConfig.Size = new Size(342, 345);
groupBox_PreviewConfig.TabIndex = 1;
groupBox_PreviewConfig.TabStop = false;
groupBox_PreviewConfig.Text = "画面参数";
@@ -350,7 +390,7 @@
propertyGrid_Previewer.HelpVisible = false;
propertyGrid_Previewer.Location = new Point(3, 26);
propertyGrid_Previewer.Name = "propertyGrid_Previewer";
propertyGrid_Previewer.Size = new Size(407, 316);
propertyGrid_Previewer.Size = new Size(336, 316);
propertyGrid_Previewer.TabIndex = 1;
propertyGrid_Previewer.ToolbarVisible = false;
propertyGrid_Previewer.PropertyValueChanged += propertyGrid_PropertyValueChanged;
@@ -465,5 +505,10 @@
private ToolStripMenuItem toolStripMenuItem_ResetAnimation;
private ToolStripMenuItem toolStripMenuItem_Diagnostics;
private ToolStripSeparator toolStripSeparator3;
private ToolStripMenuItem toolStripMenuItem_Download;
private ToolStripMenuItem toolStripMenuItem_ManageResource;
private ToolStripMenuItem toolStripMenuItem_Tool;
private ToolStripMenuItem toolStripMenuItem_ConvertFileFormat;
private ToolStripMenuItem toolStripMenuItem_ExportPreview;
}
}

View File

@@ -1,7 +1,9 @@
using NLog;
using NLog;
using SpineViewer.Spine;
using System.ComponentModel;
using System.Diagnostics;
using System.Text.Json;
using System.Text.Json.Nodes;
namespace SpineViewer
{
@@ -14,11 +16,11 @@ namespace SpineViewer
}
/// <summary>
/// <EFBFBD><EFBFBD>ʼ<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>־<EFBFBD><EFBFBD>
/// 初始化窗口日志器
/// </summary>
private void InitializeLogConfiguration()
{
// <EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>־
// 窗口日志
var rtbTarget = new NLog.Windows.Forms.RichTextBoxTarget
{
Name = "rtbTarget",
@@ -41,68 +43,6 @@ namespace SpineViewer
LogManager.ReconfigExistingLoggers();
}
private void ExportPng_Work(object? sender, DoWorkEventArgs e)
{
var worker = sender as BackgroundWorker;
var arguments = e.Argument as Dialogs.ExportPngDialog;
var outputDir = arguments.OutputDir;
var duration = arguments.Duration;
var fps = arguments.Fps;
var timestamp = DateTime.Now.ToString("yyMMddHHmmss");
var resolution = spinePreviewer.Resolution;
var tex = new SFML.Graphics.RenderTexture((uint)resolution.Width, (uint)resolution.Height);
tex.SetView(spinePreviewer.View);
var delta = 1f / fps;
var frameCount = 1 + (int)(duration / delta); // <20><>֡<EFBFBD><D6A1>ʼ<EFBFBD><CABC><EFBFBD><EFBFBD>
spinePreviewer.StopPreview();
lock (spineListView.Spines)
{
var spinesReverse = spineListView.Spines.Reverse();
// <20><><EFBFBD>ö<EFBFBD><C3B6><EFBFBD>ʱ<EFBFBD><CAB1>
foreach (var spine in spinesReverse)
spine.CurrentAnimation = spine.CurrentAnimation;
Program.Logger.Info(
"Begin exporting png frames to output dir {}, duration: {}, fps: {}, totally {} spines",
[outputDir, duration, fps, spinesReverse.Count()]
);
// <20><>֡<EFBFBD><D6A1><EFBFBD><EFBFBD>
var success = 0;
worker.ReportProgress(0, $"<22>Ѵ<EFBFBD><D1B4><EFBFBD> 0/{frameCount}");
for (int frameIndex = 0; frameIndex < frameCount; frameIndex++)
{
if (worker.CancellationPending)
break;
tex.Clear(SFML.Graphics.Color.Transparent);
foreach (var spine in spinesReverse)
{
tex.Draw(spine);
spine.Update(delta);
}
tex.Display();
using (var img = tex.Texture.CopyToImage())
{
img.SaveToFile(Path.Combine(outputDir, $"{timestamp}_{fps}_{frameIndex:d6}.png"));
}
success++;
worker.ReportProgress((int)((frameIndex + 1) * 100.0) / frameCount, $"<22>Ѵ<EFBFBD><D1B4><EFBFBD> {frameIndex + 1}/{frameCount}");
}
Program.Logger.Info("Exporting done: {}/{}", success, frameCount);
}
spinePreviewer.StartPreview();
}
private void MainForm_Load(object sender, EventArgs e)
{
spinePreviewer.StartPreview();
@@ -129,7 +69,7 @@ namespace SpineViewer
{
if (spineListView.Spines.Count <= 0)
{
MessageBox.Show("<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ٴ<EFBFBD><EFBFBD><EFBFBD>һ<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ļ<EFBFBD>", "<EFBFBD><EFBFBD>ʾ<EFBFBD><EFBFBD>Ϣ", MessageBoxButtons.OK, MessageBoxIcon.Information);
MessageBox.Show("请至少打开一个骨骼文件", "提示信息", MessageBoxButtons.OK, MessageBoxIcon.Information);
return;
}
}
@@ -144,6 +84,11 @@ namespace SpineViewer
progressDialog.ShowDialog();
}
private void toolStripMenuItem_ExportPreview_Click(object sender, EventArgs e)
{
spineListView.ExportPreviews();
}
private void toolStripMenuItem_Exit_Click(object sender, EventArgs e)
{
Close();
@@ -158,6 +103,23 @@ namespace SpineViewer
}
}
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);
progressDialog.ShowDialog();
}
private void toolStripMenuItem_ManageResource_Click(object sender, EventArgs e)
{
}
private void toolStripMenuItem_About_Click(object sender, EventArgs e)
{
(new Dialogs.AboutDialog()).ShowDialog();
@@ -175,5 +137,132 @@ namespace SpineViewer
private void propertyGrid_PropertyValueChanged(object sender, PropertyValueChangedEventArgs e) { (sender as PropertyGrid)?.Refresh(); }
private void spinePreviewer_MouseUp(object sender, MouseEventArgs e) { propertyGrid_Spine.Refresh(); }
private void ExportPng_Work(object? sender, DoWorkEventArgs e)
{
var worker = sender as BackgroundWorker;
var arguments = e.Argument as Dialogs.ExportPngDialog;
var outputDir = arguments.OutputDir;
var duration = arguments.Duration;
var fps = arguments.Fps;
var timestamp = DateTime.Now.ToString("yyMMddHHmmss");
var resolution = spinePreviewer.Resolution;
var tex = new SFML.Graphics.RenderTexture((uint)resolution.Width, (uint)resolution.Height);
tex.SetView(spinePreviewer.View);
var delta = 1f / fps;
var frameCount = 1 + (int)(duration / delta); // 零帧开始导出
spinePreviewer.StopPreview();
lock (spineListView.Spines)
{
var spinesReverse = spineListView.Spines.Reverse();
// 重置动画时间
foreach (var spine in spinesReverse)
spine.CurrentAnimation = spine.CurrentAnimation;
Program.Logger.Info(
"Begin exporting png frames to output dir {}, duration: {}, fps: {}, totally {} spines",
[outputDir, duration, fps, spinesReverse.Count()]
);
// 逐帧导出
var success = 0;
worker.ReportProgress(0, $"已处理 0/{frameCount}");
for (int frameIndex = 0; frameIndex < frameCount; frameIndex++)
{
if (worker.CancellationPending)
break;
tex.Clear(SFML.Graphics.Color.Transparent);
foreach (var spine in spinesReverse)
{
tex.Draw(spine);
spine.Update(delta);
}
tex.Display();
using (var img = tex.Texture.CopyToImage())
{
img.SaveToFile(Path.Combine(outputDir, $"{timestamp}_{fps}_{frameIndex:d6}.png"));
}
success++;
worker.ReportProgress((int)((frameIndex + 1) * 100.0) / frameCount, $"已处理 {frameIndex + 1}/{frameCount}");
}
Program.Logger.Info("Exporting done: {}/{}", success, frameCount);
}
spinePreviewer.StartPreview();
}
private void ConvertFileFormat_Work(object? sender, DoWorkEventArgs e)
{
var worker = sender as BackgroundWorker;
var arguments = e.Argument as Dialogs.ConvertFileFormatDialog;
var skelPaths = arguments.SkelPaths;
var srcVersion = arguments.SourceVersion;
var tgtVersion = arguments.TargetVersion;
var jsonSource = arguments.JsonSource;
var jsonTarget = arguments.JsonTarget;
var newSuffix = jsonTarget ? ".json" : ".skel";
if (jsonTarget == jsonSource)
{
if (tgtVersion == srcVersion)
return;
else
newSuffix += $".{tgtVersion.ToString().ToLower()}"; // TODO: 仅转换版本的情况下考虑文件覆盖问题
}
int totalCount = skelPaths.Length;
int success = 0;
int error = 0;
SkeletonConverter srcCvter = SkeletonConverter.New(srcVersion);
SkeletonConverter tgtCvter = tgtVersion == srcVersion ? srcCvter : 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
{
var root = jsonSource ? srcCvter.ReadJson(skelPath) : srcCvter.ReadBinary(skelPath);
if (tgtVersion != srcVersion) root = srcCvter.ToVersion(root, tgtVersion);
if (jsonTarget) tgtCvter.WriteJson(root, newPath); else tgtCvter.WriteBinary(root, newPath);
success++;
}
catch (Exception ex)
{
Program.Logger.Error(ex.ToString());
Program.Logger.Error("Failed to convert {}", skelPath);
error++;
}
worker.ReportProgress((int)((i + 1) * 100.0) / totalCount, $"已处理 {i + 1}/{totalCount}");
}
if (error > 0)
{
Program.Logger.Warn("Batch convert {} successfully, {} failed", success, error);
}
else
{
Program.Logger.Info("{} skel converted successfully", success);
}
}
}
}

View File

@@ -1,4 +1,4 @@
using NLog;
using NLog;
using System.Diagnostics;
namespace SpineViewer
@@ -28,18 +28,18 @@ namespace SpineViewer
catch (Exception ex)
{
Logger.Fatal(ex.ToString());
MessageBox.Show(ex.ToString(), "<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ѱ<EFBFBD><EFBFBD><EFBFBD>", MessageBoxButtons.OK, MessageBoxIcon.Stop);
MessageBox.Show(ex.ToString(), "程序已崩溃", MessageBoxButtons.OK, MessageBoxIcon.Stop);
}
}
/// <summary>
/// <EFBFBD><EFBFBD>ʼ<EFBFBD><EFBFBD><EFBFBD><EFBFBD>־<EFBFBD><EFBFBD><EFBFBD><EFBFBD>
/// 初始化日志配置
/// </summary>
private static void InitializeLogConfiguration()
{
var config = new NLog.Config.LoggingConfiguration();
// <EFBFBD>ļ<EFBFBD><EFBFBD><EFBFBD>־
// 文件日志
var fileTarget = new NLog.Targets.FileTarget("fileTarget")
{
Encoding = System.Text.Encoding.UTF8,

View File

@@ -9,7 +9,7 @@ namespace SpineViewer.Spine
/// <summary>
/// SFML 混合模式
/// </summary>
public static class BlendMode
public static class BlendModeSFML
{
/// <summary>
/// Alpha Blend

File diff suppressed because it is too large Load Diff

View File

@@ -8,11 +8,13 @@ using System.Text;
using System.Threading.Tasks;
using SpineRuntime21;
namespace SpineViewer.Spine.Implementations
namespace SpineViewer.Spine.Implementations.Spine
{
[SpineImplementation(Version.V21)]
internal class Spine21 : Spine
internal class Spine21 : SpineViewer.Spine.Spine
{
private static readonly Animation EmptyAnimation = new(EMPTY_ANIMATION, [], 0);
private class TextureLoader : SpineRuntime21.TextureLoader
{
public void Load(AtlasPage page, string path)
@@ -88,6 +90,8 @@ namespace SpineViewer.Spine.Implementations
atlas.Dispose();
}
public override string FileVersion { get => skeletonData.Version; }
public override float Scale
{
get
@@ -170,8 +174,15 @@ namespace SpineViewer.Spine.Implementations
public override string CurrentAnimation
{
get => animationState.GetCurrent(0)?.Animation.Name ?? DefaultAnimationName;
set { if (animationNames.Contains(value)) { animationState.SetAnimation(0, value, true); Update(0); } }
get => animationState.GetCurrent(0)?.Animation.Name ?? EMPTY_ANIMATION;
set
{
if (value == EMPTY_ANIMATION)
animationState.SetAnimation(0, EmptyAnimation, false);
else if (animationNames.Contains(value))
animationState.SetAnimation(0, value, true);
Update(0);
}
}
public override RectangleF Bounds
@@ -234,14 +245,14 @@ namespace SpineViewer.Spine.Implementations
skeleton.UpdateWorldTransform();
}
//private SFML.Graphics.BlendMode GetSFMLBlendMode(SpineRuntime21.BlendMode spineBlendMode)
//private SFML.Graphics.BlendMode GetSFMLBlendMode(BlendMode spineBlendMode)
//{
// return spineBlendMode switch
// {
// SpineRuntime21.BlendMode.Normal => BlendMode.Normal,
// SpineRuntime21.BlendMode.Additive => BlendMode.Additive,
// SpineRuntime21.BlendMode.Multiply => BlendMode.Multiply,
// SpineRuntime21.BlendMode.Screen => BlendMode.Screen,
// BlendMode.Normal => BlendMode.Normal,
// BlendMode.Additive => BlendMode.Additive,
// BlendMode.Multiply => BlendMode.Multiply,
// BlendMode.Screen => BlendMode.Screen,
// _ => throw new NotImplementedException($"{spineBlendMode}"),
// };
//}
@@ -311,14 +322,14 @@ namespace SpineViewer.Spine.Implementations
}
// 似乎 2.1.x 也没有 BlendMode
SFML.Graphics.BlendMode blendMode = slot.Data.AdditiveBlending ? BlendMode.Additive : BlendMode.Normal;
SFML.Graphics.BlendMode blendMode = slot.Data.AdditiveBlending ? BlendModeSFML.Additive : BlendModeSFML.Normal;
states.Texture ??= texture;
if (states.BlendMode != blendMode || states.Texture != texture)
{
if (vertexArray.VertexCount > 0)
{
if (UsePremultipliedAlpha && (states.BlendMode == BlendMode.Normal || states.BlendMode == BlendMode.Additive))
if (UsePremultipliedAlpha && (states.BlendMode == BlendModeSFML.Normal || states.BlendMode == BlendModeSFML.Additive))
states.Shader = FragmentShader;
else
states.Shader = null;
@@ -363,7 +374,7 @@ namespace SpineViewer.Spine.Implementations
//clipping.ClipEnd(slot);
}
if (UsePremultipliedAlpha && (states.BlendMode == BlendMode.Normal || states.BlendMode == BlendMode.Additive))
if (UsePremultipliedAlpha && (states.BlendMode == BlendModeSFML.Normal || states.BlendMode == BlendModeSFML.Additive))
states.Shader = FragmentShader;
else
states.Shader = null;

View File

@@ -8,11 +8,13 @@ using System.Text;
using System.Threading.Tasks;
using SpineRuntime36;
namespace SpineViewer.Spine.Implementations
namespace SpineViewer.Spine.Implementations.Spine
{
[SpineImplementation(Version.V36)]
internal class Spine36 : Spine
internal class Spine36 : SpineViewer.Spine.Spine
{
private static readonly Animation EmptyAnimation = new(EMPTY_ANIMATION, [], 0);
private class TextureLoader : SpineRuntime36.TextureLoader
{
public void Load(AtlasPage page, string path)
@@ -87,6 +89,8 @@ namespace SpineViewer.Spine.Implementations
atlas.Dispose();
}
public override string FileVersion { get => skeletonData.Version; }
public override float Scale
{
get
@@ -168,8 +172,15 @@ namespace SpineViewer.Spine.Implementations
public override string CurrentAnimation
{
get => animationState.GetCurrent(0)?.Animation.Name ?? DefaultAnimationName;
set { if (animationNames.Contains(value)) { animationState.SetAnimation(0, value, true); Update(0); } }
get => animationState.GetCurrent(0)?.Animation.Name ?? EMPTY_ANIMATION;
set
{
if (value == EMPTY_ANIMATION)
animationState.SetAnimation(0, EmptyAnimation, false);
else if (animationNames.Contains(value))
animationState.SetAnimation(0, value, true);
Update(0);
}
}
public override RectangleF Bounds
@@ -192,14 +203,14 @@ namespace SpineViewer.Spine.Implementations
skeleton.UpdateWorldTransform();
}
private SFML.Graphics.BlendMode GetSFMLBlendMode(SpineRuntime36.BlendMode spineBlendMode)
private SFML.Graphics.BlendMode GetSFMLBlendMode(BlendMode spineBlendMode)
{
return spineBlendMode switch
{
SpineRuntime36.BlendMode.Normal => BlendMode.Normal,
SpineRuntime36.BlendMode.Additive => BlendMode.Additive,
SpineRuntime36.BlendMode.Multiply => BlendMode.Multiply,
SpineRuntime36.BlendMode.Screen => BlendMode.Screen,
BlendMode.Normal => BlendModeSFML.Normal,
BlendMode.Additive => BlendModeSFML.Additive,
BlendMode.Multiply => BlendModeSFML.Multiply,
BlendMode.Screen => BlendModeSFML.Screen,
_ => throw new NotImplementedException($"{spineBlendMode}"),
};
}
@@ -274,7 +285,7 @@ namespace SpineViewer.Spine.Implementations
{
if (vertexArray.VertexCount > 0)
{
if (UsePremultipliedAlpha && (states.BlendMode == BlendMode.Normal || states.BlendMode == BlendMode.Additive))
if (UsePremultipliedAlpha && (states.BlendMode == BlendModeSFML.Normal || states.BlendMode == BlendModeSFML.Additive))
states.Shader = FragmentShader;
else
states.Shader = null;
@@ -319,7 +330,7 @@ namespace SpineViewer.Spine.Implementations
clipping.ClipEnd(slot);
}
if (UsePremultipliedAlpha && (states.BlendMode == BlendMode.Normal || states.BlendMode == BlendMode.Additive))
if (UsePremultipliedAlpha && (states.BlendMode == BlendModeSFML.Normal || states.BlendMode == BlendModeSFML.Additive))
states.Shader = FragmentShader;
else
states.Shader = null;

View File

@@ -5,11 +5,13 @@ using System.Text;
using System.Threading.Tasks;
using SpineRuntime37;
namespace SpineViewer.Spine.Implementations
namespace SpineViewer.Spine.Implementations.Spine
{
[SpineImplementation(Version.V37)]
internal class Spine37 : Spine
internal class Spine37 : SpineViewer.Spine.Spine
{
private static readonly Animation EmptyAnimation = new(EMPTY_ANIMATION, [], 0);
private class TextureLoader : SpineRuntime37.TextureLoader
{
public void Load(AtlasPage page, string path)
@@ -86,6 +88,8 @@ namespace SpineViewer.Spine.Implementations
atlas.Dispose();
}
public override string FileVersion { get => skeletonData.Version; }
public override float Scale
{
get
@@ -175,8 +179,15 @@ namespace SpineViewer.Spine.Implementations
public override string CurrentAnimation
{
get => animationState.GetCurrent(0)?.Animation.Name ?? DefaultAnimationName;
set { if (animationNames.Contains(value)) { animationState.SetAnimation(0, value, true); Update(0); } }
get => animationState.GetCurrent(0)?.Animation.Name ?? EMPTY_ANIMATION;
set
{
if (value == EMPTY_ANIMATION)
animationState.SetAnimation(0, EmptyAnimation, false);
else if (animationNames.Contains(value))
animationState.SetAnimation(0, value, true);
Update(0);
}
}
public override RectangleF Bounds
@@ -199,14 +210,14 @@ namespace SpineViewer.Spine.Implementations
skeleton.UpdateWorldTransform();
}
private SFML.Graphics.BlendMode GetSFMLBlendMode(SpineRuntime37.BlendMode spineBlendMode)
private SFML.Graphics.BlendMode GetSFMLBlendMode(BlendMode spineBlendMode)
{
return spineBlendMode switch
{
SpineRuntime37.BlendMode.Normal => BlendMode.Normal,
SpineRuntime37.BlendMode.Additive => BlendMode.Additive,
SpineRuntime37.BlendMode.Multiply => BlendMode.Multiply,
SpineRuntime37.BlendMode.Screen => BlendMode.Screen,
BlendMode.Normal => BlendModeSFML.Normal,
BlendMode.Additive => BlendModeSFML.Additive,
BlendMode.Multiply => BlendModeSFML.Multiply,
BlendMode.Screen => BlendModeSFML.Screen,
_ => throw new NotImplementedException($"{spineBlendMode}"),
};
}
@@ -282,7 +293,7 @@ namespace SpineViewer.Spine.Implementations
if (vertexArray.VertexCount > 0)
{
// XXX: 实测不用设置 sampler2D 的值也正确
if (UsePremultipliedAlpha && (states.BlendMode == BlendMode.Normal || states.BlendMode == BlendMode.Additive))
if (UsePremultipliedAlpha && (states.BlendMode == BlendModeSFML.Normal || states.BlendMode == BlendModeSFML.Additive))
states.Shader = FragmentShader;
else
states.Shader = null;
@@ -327,7 +338,7 @@ namespace SpineViewer.Spine.Implementations
clipping.ClipEnd(slot);
}
if (UsePremultipliedAlpha && (states.BlendMode == BlendMode.Normal || states.BlendMode == BlendMode.Additive))
if (UsePremultipliedAlpha && (states.BlendMode == BlendModeSFML.Normal || states.BlendMode == BlendModeSFML.Additive))
states.Shader = FragmentShader;
else
states.Shader = null;

View File

@@ -7,13 +7,14 @@ using System.Text;
using System.Threading.Tasks;
using SpineRuntime38;
using SpineRuntime38.Attachments;
using SpineViewer.Spine;
namespace SpineViewer.Spine.Implementations
namespace SpineViewer.Spine.Implementations.Spine
{
[SpineImplementation(Version.V38)]
internal class Spine38 : Spine
internal class Spine38 : SpineViewer.Spine.Spine
{
private static readonly Animation EmptyAnimation = new(EMPTY_ANIMATION, [], 0);
private class TextureLoader : SpineRuntime38.TextureLoader
{
public void Load(AtlasPage page, string path)
@@ -90,6 +91,8 @@ namespace SpineViewer.Spine.Implementations
atlas.Dispose();
}
public override string FileVersion { get => skeletonData.Version; }
public override float Scale
{
get
@@ -179,8 +182,15 @@ namespace SpineViewer.Spine.Implementations
public override string CurrentAnimation
{
get => animationState.GetCurrent(0)?.Animation.Name ?? DefaultAnimationName;
set { if (animationNames.Contains(value)) { animationState.SetAnimation(0, value, true); Update(0); } }
get => animationState.GetCurrent(0)?.Animation.Name ?? EMPTY_ANIMATION;
set
{
if (value == EMPTY_ANIMATION)
animationState.SetAnimation(0, EmptyAnimation, false);
else if (animationNames.Contains(value))
animationState.SetAnimation(0, value, true);
Update(0);
}
}
public override RectangleF Bounds
@@ -203,14 +213,14 @@ namespace SpineViewer.Spine.Implementations
skeleton.UpdateWorldTransform();
}
private SFML.Graphics.BlendMode GetSFMLBlendMode(SpineRuntime38.BlendMode spineBlendMode)
private SFML.Graphics.BlendMode GetSFMLBlendMode(BlendMode spineBlendMode)
{
return spineBlendMode switch
{
SpineRuntime38.BlendMode.Normal => BlendMode.Normal,
SpineRuntime38.BlendMode.Additive => BlendMode.Additive,
SpineRuntime38.BlendMode.Multiply => BlendMode.Multiply,
SpineRuntime38.BlendMode.Screen => BlendMode.Screen,
BlendMode.Normal => BlendModeSFML.Normal,
BlendMode.Additive => BlendModeSFML.Additive,
BlendMode.Multiply => BlendModeSFML.Multiply,
BlendMode.Screen => BlendModeSFML.Screen,
_ => throw new NotImplementedException($"{spineBlendMode}"),
};
}
@@ -286,7 +296,7 @@ namespace SpineViewer.Spine.Implementations
if (vertexArray.VertexCount > 0)
{
// XXX: 实测不用设置 sampler2D 的值也正确
if (UsePremultipliedAlpha && (states.BlendMode == BlendMode.Normal || states.BlendMode == BlendMode.Additive))
if (UsePremultipliedAlpha && (states.BlendMode == BlendModeSFML.Normal || states.BlendMode == BlendModeSFML.Additive))
states.Shader = FragmentShader;
else
states.Shader = null;
@@ -331,7 +341,7 @@ namespace SpineViewer.Spine.Implementations
clipping.ClipEnd(slot);
}
if (UsePremultipliedAlpha && (states.BlendMode == BlendMode.Normal || states.BlendMode == BlendMode.Additive))
if (UsePremultipliedAlpha && (states.BlendMode == BlendModeSFML.Normal || states.BlendMode == BlendModeSFML.Additive))
states.Shader = FragmentShader;
else
states.Shader = null;

View File

@@ -7,11 +7,13 @@ using System.Text;
using System.Threading.Tasks;
using SpineRuntime40;
namespace SpineViewer.Spine.Implementations
namespace SpineViewer.Spine.Implementations.Spine
{
[SpineImplementation(Version.V40)]
internal class Spine40 : Spine
internal class Spine40 : SpineViewer.Spine.Spine
{
private static readonly Animation EmptyAnimation = new(EMPTY_ANIMATION, [], 0);
private class TextureLoader : SpineRuntime40.TextureLoader
{
public void Load(AtlasPage page, string path)
@@ -88,6 +90,8 @@ namespace SpineViewer.Spine.Implementations
atlas.Dispose();
}
public override string FileVersion { get => skeletonData.Version; }
public override float Scale
{
get
@@ -177,8 +181,15 @@ namespace SpineViewer.Spine.Implementations
public override string CurrentAnimation
{
get => animationState.GetCurrent(0)?.Animation.Name ?? DefaultAnimationName;
set { if (animationNames.Contains(value)) { animationState.SetAnimation(0, value, true); Update(0); } }
get => animationState.GetCurrent(0)?.Animation.Name ?? EMPTY_ANIMATION;
set
{
if (value == EMPTY_ANIMATION)
animationState.SetAnimation(0, EmptyAnimation, false);
else if (animationNames.Contains(value))
animationState.SetAnimation(0, value, true);
Update(0);
}
}
public override RectangleF Bounds
@@ -201,14 +212,14 @@ namespace SpineViewer.Spine.Implementations
skeleton.UpdateWorldTransform();
}
private SFML.Graphics.BlendMode GetSFMLBlendMode(SpineRuntime40.BlendMode spineBlendMode)
private SFML.Graphics.BlendMode GetSFMLBlendMode(BlendMode spineBlendMode)
{
return spineBlendMode switch
{
SpineRuntime40.BlendMode.Normal => BlendMode.Normal,
SpineRuntime40.BlendMode.Additive => BlendMode.Additive,
SpineRuntime40.BlendMode.Multiply => BlendMode.Multiply,
SpineRuntime40.BlendMode.Screen => BlendMode.Screen,
BlendMode.Normal => BlendModeSFML.Normal,
BlendMode.Additive => BlendModeSFML.Additive,
BlendMode.Multiply => BlendModeSFML.Multiply,
BlendMode.Screen => BlendModeSFML.Screen,
_ => throw new NotImplementedException($"{spineBlendMode}"),
};
}
@@ -284,7 +295,7 @@ namespace SpineViewer.Spine.Implementations
if (vertexArray.VertexCount > 0)
{
// XXX: 实测不用设置 sampler2D 的值也正确
if (UsePremultipliedAlpha && (states.BlendMode == BlendMode.Normal || states.BlendMode == BlendMode.Additive))
if (UsePremultipliedAlpha && (states.BlendMode == BlendModeSFML.Normal || states.BlendMode == BlendModeSFML.Additive))
states.Shader = FragmentShader;
else
states.Shader = null;
@@ -329,7 +340,7 @@ namespace SpineViewer.Spine.Implementations
clipping.ClipEnd(slot);
}
if (UsePremultipliedAlpha && (states.BlendMode == BlendMode.Normal || states.BlendMode == BlendMode.Additive))
if (UsePremultipliedAlpha && (states.BlendMode == BlendModeSFML.Normal || states.BlendMode == BlendModeSFML.Additive))
states.Shader = FragmentShader;
else
states.Shader = null;

View File

@@ -7,11 +7,13 @@ using System.Text;
using System.Threading.Tasks;
using SpineRuntime41;
namespace SpineViewer.Spine.Implementations
namespace SpineViewer.Spine.Implementations.Spine
{
[SpineImplementation(Version.V41)]
internal class Spine41 : Spine
internal class Spine41 : SpineViewer.Spine.Spine
{
private static readonly Animation EmptyAnimation = new(EMPTY_ANIMATION, [], 0);
private class TextureLoader : SpineRuntime41.TextureLoader
{
public void Load(AtlasPage page, string path)
@@ -88,6 +90,8 @@ namespace SpineViewer.Spine.Implementations
atlas.Dispose();
}
public override string FileVersion { get => skeletonData.Version; }
public override float Scale
{
get
@@ -177,8 +181,15 @@ namespace SpineViewer.Spine.Implementations
public override string CurrentAnimation
{
get => animationState.GetCurrent(0)?.Animation.Name ?? DefaultAnimationName;
set { if (animationNames.Contains(value)) { animationState.SetAnimation(0, value, true); Update(0); } }
get => animationState.GetCurrent(0)?.Animation.Name ?? EMPTY_ANIMATION;
set
{
if (value == EMPTY_ANIMATION)
animationState.SetAnimation(0, EmptyAnimation, false);
else if (animationNames.Contains(value))
animationState.SetAnimation(0, value, true);
Update(0);
}
}
public override RectangleF Bounds
@@ -201,14 +212,14 @@ namespace SpineViewer.Spine.Implementations
skeleton.UpdateWorldTransform();
}
private SFML.Graphics.BlendMode GetSFMLBlendMode(SpineRuntime41.BlendMode spineBlendMode)
private SFML.Graphics.BlendMode GetSFMLBlendMode(BlendMode spineBlendMode)
{
return spineBlendMode switch
{
SpineRuntime41.BlendMode.Normal => BlendMode.Normal,
SpineRuntime41.BlendMode.Additive => BlendMode.Additive,
SpineRuntime41.BlendMode.Multiply => BlendMode.Multiply,
SpineRuntime41.BlendMode.Screen => BlendMode.Screen,
BlendMode.Normal => BlendModeSFML.Normal,
BlendMode.Additive => BlendModeSFML.Additive,
BlendMode.Multiply => BlendModeSFML.Multiply,
BlendMode.Screen => BlendModeSFML.Screen,
_ => throw new NotImplementedException($"{spineBlendMode}"),
};
}
@@ -284,7 +295,7 @@ namespace SpineViewer.Spine.Implementations
if (vertexArray.VertexCount > 0)
{
// XXX: 实测不用设置 sampler2D 的值也正确
if (UsePremultipliedAlpha && (states.BlendMode == BlendMode.Normal || states.BlendMode == BlendMode.Additive))
if (UsePremultipliedAlpha && (states.BlendMode == BlendModeSFML.Normal || states.BlendMode == BlendModeSFML.Additive))
states.Shader = FragmentShader;
else
states.Shader = null;
@@ -329,7 +340,7 @@ namespace SpineViewer.Spine.Implementations
clipping.ClipEnd(slot);
}
if (UsePremultipliedAlpha && (states.BlendMode == BlendMode.Normal || states.BlendMode == BlendMode.Additive))
if (UsePremultipliedAlpha && (states.BlendMode == BlendModeSFML.Normal || states.BlendMode == BlendModeSFML.Additive))
states.Shader = FragmentShader;
else
states.Shader = null;

View File

@@ -7,11 +7,13 @@ using System.Text;
using System.Threading.Tasks;
using SpineRuntime42;
namespace SpineViewer.Spine.Implementations
namespace SpineViewer.Spine.Implementations.Spine
{
[SpineImplementation(Version.V42)]
internal class Spine42 : Spine
internal class Spine42 : SpineViewer.Spine.Spine
{
private static readonly Animation EmptyAnimation = new(EMPTY_ANIMATION, [], 0);
private class TextureLoader : SpineRuntime42.TextureLoader
{
public void Load(AtlasPage page, string path)
@@ -88,6 +90,8 @@ namespace SpineViewer.Spine.Implementations
atlas.Dispose();
}
public override string FileVersion { get => skeletonData.Version; }
public override float Scale
{
get
@@ -177,8 +181,15 @@ namespace SpineViewer.Spine.Implementations
public override string CurrentAnimation
{
get => animationState.GetCurrent(0)?.Animation.Name ?? DefaultAnimationName;
set { if (animationNames.Contains(value)) { animationState.SetAnimation(0, value, true); Update(0); } }
get => animationState.GetCurrent(0)?.Animation.Name ?? EMPTY_ANIMATION;
set
{
if (value == EMPTY_ANIMATION)
animationState.SetAnimation(0, EmptyAnimation, false);
else if (animationNames.Contains(value))
animationState.SetAnimation(0, value, true);
Update(0);
}
}
public override RectangleF Bounds
@@ -201,14 +212,14 @@ namespace SpineViewer.Spine.Implementations
skeleton.UpdateWorldTransform(Skeleton.Physics.Update);
}
private SFML.Graphics.BlendMode GetSFMLBlendMode(SpineRuntime42.BlendMode spineBlendMode)
private SFML.Graphics.BlendMode GetSFMLBlendMode(BlendMode spineBlendMode)
{
return spineBlendMode switch
{
SpineRuntime42.BlendMode.Normal => BlendMode.Normal,
SpineRuntime42.BlendMode.Additive => BlendMode.Additive,
SpineRuntime42.BlendMode.Multiply => BlendMode.Multiply,
SpineRuntime42.BlendMode.Screen => BlendMode.Screen,
BlendMode.Normal => BlendModeSFML.Normal,
BlendMode.Additive => BlendModeSFML.Additive,
BlendMode.Multiply => BlendModeSFML.Multiply,
BlendMode.Screen => BlendModeSFML.Screen,
_ => throw new NotImplementedException($"{spineBlendMode}"),
};
}
@@ -284,7 +295,7 @@ namespace SpineViewer.Spine.Implementations
if (vertexArray.VertexCount > 0)
{
// XXX: 实测不用设置 sampler2D 的值也正确
if (UsePremultipliedAlpha && (states.BlendMode == BlendMode.Normal || states.BlendMode == BlendMode.Additive))
if (UsePremultipliedAlpha && (states.BlendMode == BlendModeSFML.Normal || states.BlendMode == BlendModeSFML.Additive))
states.Shader = FragmentShader;
else
states.Shader = null;
@@ -329,7 +340,7 @@ namespace SpineViewer.Spine.Implementations
clipping.ClipEnd(slot);
}
if (UsePremultipliedAlpha && (states.BlendMode == BlendMode.Normal || states.BlendMode == BlendMode.Additive))
if (UsePremultipliedAlpha && (states.BlendMode == BlendModeSFML.Normal || states.BlendMode == BlendModeSFML.Additive))
states.Shader = FragmentShader;
else
states.Shader = null;

View File

@@ -0,0 +1,333 @@
using Microsoft.VisualBasic;
using System;
using System.Buffers.Binary;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Runtime.InteropServices;
using System.Text;
using System.Text.Json;
using System.Text.Json.Nodes;
using System.Text.Encodings.Web;
namespace SpineViewer.Spine
{
/// <summary>
/// SkeletonConverter 实现类标记
/// </summary>
[AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = false)]
public class SkeletonConverterImplementationAttribute : Attribute
{
public Version Version { get; }
public SkeletonConverterImplementationAttribute(Version version)
{
Version = version;
}
}
/// <summary>
/// SkeletonConverter 基类, 使用静态方法 New 来创建具体版本对象
/// </summary>
public abstract class SkeletonConverter
{
/// <summary>
/// 实现类缓存
/// </summary>
private static readonly Dictionary<Version, Type> ImplementationTypes = [];
public static readonly Dictionary<Version, Type>.KeyCollection ImplementedVersions;
/// <summary>
/// 静态构造函数
/// </summary>
static SkeletonConverter()
{
// 遍历并缓存标记了 SkeletonConverterImplementationAttribute 的类型
var impTypes = Assembly.GetExecutingAssembly().GetTypes().Where(t => typeof(SkeletonConverter).IsAssignableFrom(t) && !t.IsAbstract);
foreach (var type in impTypes)
{
var attr = type.GetCustomAttribute<SkeletonConverterImplementationAttribute>();
if (attr is not null)
{
if (ImplementationTypes.ContainsKey(attr.Version))
throw new InvalidOperationException($"Multiple implementations found: {attr.Version}");
ImplementationTypes[attr.Version] = type;
}
}
Program.Logger.Debug("Find SkeletonConverter implementations: [{}]", string.Join(", ", ImplementationTypes.Keys));
ImplementedVersions = ImplementationTypes.Keys;
}
/// <summary>
/// 创建特定版本的 SkeletonConverter
/// </summary>
public static SkeletonConverter New(Version version)
{
if (!ImplementationTypes.TryGetValue(version, out var cvterType))
{
throw new NotImplementedException($"Not implemented version: {version}");
}
return (SkeletonConverter)Activator.CreateInstance(cvterType);
}
/// <summary>
/// Json 格式控制
/// </summary>
private static readonly JsonWriterOptions jsonWriterOptions = new()
{
Indented = true,
Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping
};
/// <summary>
/// 读取二进制骨骼文件并构造 Json 对象
/// </summary>
public abstract JsonObject ReadBinary(string binPath);
/// <summary>
/// 将 Json 对象写入二进制骨骼文件
/// </summary>
public abstract void WriteBinary(JsonObject root, string binPath, bool nonessential = false);
/// <summary>
/// 读取 Json 对象
/// </summary>
public JsonObject ReadJson(string jsonPath)
{
using var input = File.OpenRead(jsonPath);
if (JsonNode.Parse(input) is JsonObject root)
return root;
else
throw new InvalidOperationException($"{jsonPath} is not a valid json object");
}
/// <summary>
/// 写入 Json 对象
/// </summary>
public void WriteJson(JsonObject root, string jsonPath)
{
using var output = File.Create(jsonPath);
using var writer = new Utf8JsonWriter(output, jsonWriterOptions);
root.WriteTo(writer);
}
/// <summary>
/// 转换到目标版本
/// </summary>
public abstract JsonObject ToVersion(JsonObject root, Version version);
protected class SkeletonReader
{
protected byte[] buffer = new byte[32];
protected byte[] bytesBigEndian = new byte[8];
public readonly List<string> StringTable = new(32);
protected Stream input;
public SkeletonReader(Stream input) { this.input = input; }
public int Read()
{
int val = input.ReadByte();
if (val == -1) throw new EndOfStreamException();
return val;
}
public byte ReadByte() => (byte)Read();
public byte ReadUByte() => (byte)Read();
public sbyte ReadSByte() => (sbyte)ReadByte();
public bool ReadBoolean() => Read() != 0;
public float ReadFloat()
{
if (input.Read(bytesBigEndian, 0, 4) < 4) throw new EndOfStreamException();
buffer[3] = bytesBigEndian[0];
buffer[2] = bytesBigEndian[1];
buffer[1] = bytesBigEndian[2];
buffer[0] = bytesBigEndian[3];
return BitConverter.ToSingle(buffer, 0);
}
public int ReadInt()
{
if (input.Read(bytesBigEndian, 0, 4) < 4) throw new EndOfStreamException();
return (bytesBigEndian[0] << 24)
| (bytesBigEndian[1] << 16)
| (bytesBigEndian[2] << 8)
| bytesBigEndian[3];
}
public long ReadLong()
{
if (input.Read(bytesBigEndian, 0, 8) < 8) throw new EndOfStreamException();
return ((long)(bytesBigEndian[0]) << 56)
| ((long)(bytesBigEndian[1]) << 48)
| ((long)(bytesBigEndian[2]) << 40)
| ((long)(bytesBigEndian[3]) << 32)
| ((long)(bytesBigEndian[4]) << 24)
| ((long)(bytesBigEndian[5]) << 16)
| ((long)(bytesBigEndian[6]) << 8)
| (long)(bytesBigEndian[7]);
}
public int ReadVarInt(bool optimizePositive = true)
{
byte b = ReadByte();
int val = b & 0x7F;
if ((b & 0x80) != 0)
{
b = ReadByte();
val |= (b & 0x7F) << 7;
if ((b & 0x80) != 0)
{
b = ReadByte();
val |= (b & 0x7F) << 14;
if ((b & 0x80) != 0)
{
b = ReadByte();
val |= (b & 0x7F) << 21;
if ((b & 0x80) != 0)
val |= (ReadByte() & 0x7F) << 28;
}
}
}
// 最低位是符号, 根据符号得到全 1 或全 0
// 无符号右移, 符号按原样设置在最高位, 其他位与符号异或
return optimizePositive ? val : (val >>> 1) ^ -(val & 1);
}
public string ReadString()
{
int byteCount = ReadVarInt();
switch (byteCount)
{
case 0: return null;
case 1: return "";
}
byteCount--;
if (buffer.Length < byteCount) buffer = new byte[byteCount];
ReadFully(buffer, 0, byteCount);
return System.Text.Encoding.UTF8.GetString(buffer, 0, byteCount);
}
public string ReadStringRef()
{
int index = ReadVarInt();
return index == 0 ? null : StringTable[index - 1];
}
public void ReadFully(byte[] buffer, int offset, int length)
{
while (length > 0)
{
int count = input.Read(buffer, offset, length);
if (count <= 0) throw new EndOfStreamException();
offset += count;
length -= count;
}
}
}
protected class SkeletonWriter
{
protected byte[] buffer = new byte[32];
protected byte[] bytesBigEndian = new byte[8];
public readonly List<string> StringTable = new(32);
protected Stream output;
public SkeletonWriter(Stream output) { this.output = output; }
public void Write(int val) => output.WriteByte((byte)val);
public void WriteByte(byte val) => output.WriteByte(val);
public void WriteUByte(byte val) => output.WriteByte(val);
public void WriteSByte(sbyte val) => output.WriteByte((byte)val);
public void WriteBoolean(bool val) => output.WriteByte((byte)(val ? 1 : 0));
public void WriteFloat(float val)
{
uint v = BitConverter.SingleToUInt32Bits(val);
bytesBigEndian[0] = (byte)(v >> 24);
bytesBigEndian[1] = (byte)(v >> 16);
bytesBigEndian[2] = (byte)(v >> 8);
bytesBigEndian[3] = (byte)v;
output.Write(bytesBigEndian, 0, 4);
}
public void WriteInt(int val)
{
bytesBigEndian[0] = (byte)(val >> 24);
bytesBigEndian[1] = (byte)(val >> 16);
bytesBigEndian[2] = (byte)(val >> 8);
bytesBigEndian[3] = (byte)val;
output.Write(bytesBigEndian, 0, 4);
}
public void WriteLong(long val)
{
bytesBigEndian[0] = (byte)(val >> 56);
bytesBigEndian[1] = (byte)(val >> 48);
bytesBigEndian[2] = (byte)(val >> 40);
bytesBigEndian[3] = (byte)(val >> 32);
bytesBigEndian[4] = (byte)(val >> 24);
bytesBigEndian[5] = (byte)(val >> 16);
bytesBigEndian[6] = (byte)(val >> 8);
bytesBigEndian[7] = (byte)val;
output.Write(bytesBigEndian, 0, 8);
}
public void WriteVarInt(int val, bool optimizePositive = true)
{
// 有符号右移, 会变成全 1 或者全 0 符号
// 其他位与符号异或, 符号按原样设置在最低位
if (!optimizePositive) val = (val << 1) ^ (val >> 31);
byte b = (byte)(val & 0x7F);
val >>>= 7;
if (val != 0)
{
output.WriteByte((byte)(b | 0x80));
b = (byte)(val & 0x7F);
val >>>= 7;
if (val != 0)
{
output.WriteByte((byte)(b | 0x80));
b = (byte)(val & 0x7F);
val >>>= 7;
if (val != 0)
{
output.WriteByte((byte)(b | 0x80));
b = (byte)(val & 0x7F);
val >>>= 7;
if (val != 0)
{
output.WriteByte((byte)(b | 0x80));
b = (byte)(val & 0x7F);
}
}
}
}
output.WriteByte(b);
}
public void WriteString(string val)
{
if (val == null)
{
WriteVarInt(0);
return;
}
if (val.Length == 0)
{
WriteVarInt(1);
return;
}
int byteCount = System.Text.Encoding.UTF8.GetByteCount(val);
WriteVarInt(byteCount + 1);
if (buffer.Length < byteCount) buffer = new byte[byteCount];
System.Text.Encoding.UTF8.GetBytes(val, 0, val.Length, buffer, 0);
WriteFully(buffer, 0, byteCount);
}
public void WriteStringRef(string val)
{
if (val is null)
{
WriteVarInt(0);
return;
}
int index = StringTable.IndexOf(val);
if (index < 0)
{
StringTable.Add(val);
index = StringTable.Count - 1;
}
WriteVarInt(index + 1);
}
public void WriteFully(byte[] buffer, int offset, int length) => output.Write(buffer, offset, length);
}
}
}

View File

@@ -17,8 +17,6 @@ using System.Globalization;
namespace SpineViewer.Spine
{
/// <summary>
/// Spine 实现类标记
/// </summary>
@@ -38,10 +36,26 @@ namespace SpineViewer.Spine
/// </summary>
public abstract class Spine : SFML.Graphics.Drawable, IDisposable
{
/// <summary>
/// 空动画标记
/// </summary>
public const string EMPTY_ANIMATION = "<Empty>";
/// <summary>
/// 预览图大小
/// </summary>
public static readonly Size PREVIEW_SIZE = new(256, 256);
/// <summary>
/// 缩放最小值
/// </summary>
public const float SCALE_MIN = 0.001f;
/// <summary>
/// 实现类缓存
/// </summary>
private static readonly Dictionary<Version, Type> ImplementationTypes = [];
public static readonly Dictionary<Version, Type>.KeyCollection ImplementedVersions;
/// <summary>
/// 用于解决 PMA 和渐变动画问题的片段着色器
@@ -70,10 +84,13 @@ namespace SpineViewer.Spine
var attr = type.GetCustomAttribute<SpineImplementationAttribute>();
if (attr is not null)
{
if (ImplementationTypes.ContainsKey(attr.Version))
throw new InvalidOperationException($"Multiple implementations found: {attr.Version}");
ImplementationTypes[attr.Version] = type;
}
}
Program.Logger.Debug("Find Spine implementations: [{}]", string.Join(", ", ImplementationTypes.Keys));
ImplementedVersions = ImplementationTypes.Keys;
// 加载 FragmentShader
try
@@ -101,6 +118,11 @@ namespace SpineViewer.Spine
return (Spine)Activator.CreateInstance(spineType, skelPath, atlasPath);
}
/// <summary>
/// 标识符
/// </summary>
public readonly string ID = Guid.NewGuid().ToString();
/// <summary>
/// 构造函数
/// </summary>
@@ -125,19 +147,13 @@ namespace SpineViewer.Spine
~Spine() { Dispose(false); }
public void Dispose() { Dispose(true); GC.SuppressFinalize(this); }
protected virtual void Dispose(bool disposing) { }
/// <summary>
/// 缩放最小值
/// </summary>
[Browsable(false)]
public const float SCALE_MIN = 0.001f;
protected virtual void Dispose(bool disposing) { preview?.Dispose(); }
/// <summary>
/// 获取所属版本
/// </summary>
[TypeConverter(typeof(VersionTypeConverter))]
[Category("基本信息"), DisplayName("版本")]
[TypeConverter(typeof(VersionConverter))]
[Category("基本信息"), DisplayName("运行时版本")]
public Version Version { get; }
/// <summary>
@@ -155,6 +171,12 @@ namespace SpineViewer.Spine
[Category("基本信息"), DisplayName("名称")]
public string Name { get; }
/// <summary>
/// 获取所属文件版本
/// </summary>
[Category("基本信息"), DisplayName("文件版本")]
public abstract string FileVersion { get; }
/// <summary>
/// 缩放比例
/// </summary>
@@ -164,7 +186,7 @@ namespace SpineViewer.Spine
/// <summary>
/// 位置
/// </summary>
[TypeConverter(typeof(PointFTypeConverter))]
[TypeConverter(typeof(PointFConverter))]
[Category("变换"), DisplayName("位置")]
public abstract PointF Position { get; set; }
@@ -191,7 +213,7 @@ namespace SpineViewer.Spine
/// </summary>
[Browsable(false)]
public ReadOnlyCollection<string> AnimationNames { get => animationNames.AsReadOnly(); }
protected List<string> animationNames = [];
protected List<string> animationNames = [EMPTY_ANIMATION];
/// <summary>
/// 默认动画名称
@@ -202,7 +224,7 @@ namespace SpineViewer.Spine
/// <summary>
/// 当前动画名称
/// </summary>
[TypeConverter(typeof(AnimationTypeConverter))]
[TypeConverter(typeof(AnimationConverter))]
[Category("动画"), DisplayName("当前动画")]
public abstract string CurrentAnimation { get; set; }
@@ -218,6 +240,61 @@ namespace SpineViewer.Spine
[Browsable(false)]
public abstract RectangleF Bounds { get; }
/// <summary>
/// 骨骼预览图
/// </summary>
[Browsable(false)]
public Image Preview
{
get
{
if (preview is null)
{
using var img = GetPreview((uint)PREVIEW_SIZE.Width, (uint)PREVIEW_SIZE.Height);
img.SaveToMemory(out var imgBuffer, "bmp");
using var stream = new MemoryStream(imgBuffer);
preview = new Bitmap(stream);
}
return preview;
}
}
private Image preview = null;
/// <summary>
/// 获取指定尺寸的预览图
/// </summary>
public SFML.Graphics.Image GetPreview(uint width, uint height)
{
var curAnimation = CurrentAnimation;
CurrentAnimation = EMPTY_ANIMATION;
var bounds = Bounds;
float viewX = width;
float viewY = height;
float sizeX = bounds.Width;
float sizeY = bounds.Height;
var scale = 1f;
if ((sizeY / sizeX) < (viewY / viewX))
scale = sizeX / viewX;// 相同的 X, 视窗 Y 更大
else
scale = sizeY / viewY;// 相同的 Y, 视窗 X 更大
viewX *= scale;
viewY *= scale;
using var tex = new SFML.Graphics.RenderTexture(width, height);
var view = tex.GetView();
view.Center = new(bounds.X + viewX / 2, bounds.Y + viewY / 2);
view.Size = new(viewX, -viewY);
tex.SetView(view);
tex.Clear(SFML.Graphics.Color.Transparent);
tex.Draw(this);
tex.Display();
CurrentAnimation = curAnimation;
return tex.Texture.CopyToImage();
}
/// <summary>
/// 获取动画时长, 如果动画不存在则返回 0
/// </summary>
@@ -243,5 +320,11 @@ namespace SpineViewer.Spine
/// SFML.Graphics.Drawable 接口实现
/// </summary>
public abstract void Draw(SFML.Graphics.RenderTarget target, SFML.Graphics.RenderStates states);
/// <summary>
/// 是否被选中
/// </summary>
[Browsable(false)]
public bool IsSelected { get; set; } = false;
}
}

View File

@@ -9,9 +9,9 @@ using System.Threading.Tasks;
namespace SpineViewer.Spine
{
public class VersionTypeConverter : EnumConverter
public class VersionConverter : EnumConverter
{
public VersionTypeConverter() : base(typeof(Version)) { }
public VersionConverter() : base(typeof(Version)) { }
public override object? ConvertTo(ITypeDescriptorContext? context, CultureInfo? culture, object? value, Type? destinationType)
{
@@ -25,7 +25,7 @@ namespace SpineViewer.Spine
}
}
public class AnimationTypeConverter : StringConverter
public class AnimationConverter : StringConverter
{
public override bool GetStandardValuesSupported(ITypeDescriptorContext? context)
{
@@ -42,11 +42,7 @@ namespace SpineViewer.Spine
public override StandardValuesCollection? GetStandardValues(ITypeDescriptorContext? context)
{
if (context?.Instance is Spine obj)
{
// 返回 AnimationNames 作为下拉选项
return new StandardValuesCollection(obj.AnimationNames);
}
return base.GetStandardValues(context);
}
}

View File

@@ -40,13 +40,13 @@ namespace SpineViewer.Spine
/// </summary>
public enum Version
{
[Description("v2.1.x")] V21 = 0x0201,
[Description("v3.6.x")] V36 = 0x0306,
[Description("v3.7.x")] V37 = 0x0307,
[Description("v3.8.x")] V38 = 0x0308,
[Description("v4.0.x")] V40 = 0x0400,
[Description("v4.1.x")] V41 = 0x0401,
[Description("v4.2.x")] V42 = 0x0402,
[Description("v4.3.x")] V43 = 0x0403,
[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,
}
}

View File

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

View File

@@ -9,7 +9,7 @@ using System.Threading.Tasks;
namespace SpineViewer
{
public class PointFTypeConverter : ExpandableObjectConverter
public class PointFConverter : ExpandableObjectConverter
{
public override bool CanConvertTo(ITypeDescriptorContext? context, [NotNullWhen(true)] Type? destinationType)
{
@@ -52,48 +52,4 @@ namespace SpineViewer
public override bool GetPropertiesSupported(ITypeDescriptorContext? context) => true;
}
public class SizeTypeConverter : ExpandableObjectConverter
{
public override bool CanConvertTo(ITypeDescriptorContext? context, [NotNullWhen(true)] 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 Size size)
{
return $"{size.Width}, {size.Height}";
}
return base.ConvertTo(context, culture, value, destinationType);
}
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 str)
{
var parts = str.Split(',');
if (parts.Length == 2 &&
int.TryParse(parts[0], out var width) &&
int.TryParse(parts[1], out var height))
{
return new Size(width, height);
}
}
return base.ConvertFrom(context, culture, value);
}
public override PropertyDescriptorCollection GetProperties(ITypeDescriptorContext? context, object value, Attribute[]? attributes)
{
return TypeDescriptor.GetProperties(typeof(Size), attributes);
}
public override bool GetPropertiesSupported(ITypeDescriptorContext? context) => true;
}
}

BIN
img/preview.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 302 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 234 KiB