Compare commits

..

77 Commits

Author SHA1 Message Date
ww-rm
0ccb110e36 fix bug 2025-03-24 00:20:53 +08:00
ww-rm
2c238dca9b 更新至v0.10.7 2025-03-24 00:17:02 +08:00
ww-rm
3e0aa53fca update changelog 2025-03-24 00:16:33 +08:00
ww-rm
12b4e44296 增加仅导出选中 2025-03-24 00:14:43 +08:00
ww-rm
9a2cf4aefe 增加region注释 2025-03-24 00:10:32 +08:00
ww-rm
0e2a116e0a 增加显示包围盒调试 2025-03-24 00:05:11 +08:00
ww-rm
7bf30eb54a 增加仅渲染选中模式 2025-03-24 00:01:28 +08:00
ww-rm
8dda8c8ff3 增加动画交集选择 2025-03-23 10:58:57 +08:00
ww-rm
988fdb22be 增加重载 2025-03-23 10:55:28 +08:00
ww-rm
1dd2c8fb4d fix bug 2025-03-23 10:19:50 +08:00
ww-rm
2b39384b28 重构以及增加注释 2025-03-23 01:34:44 +08:00
ww-rm
28d1275023 增加公开属性 2025-03-22 23:11:13 +08:00
ww-rm
979181fc3b small change 2025-03-22 21:12:47 +08:00
ww-rm
b374b88ad5 add writeskins 2025-03-22 21:08:14 +08:00
ww-rm
6643c19a20 remove useless member 2025-03-22 16:07:04 +08:00
ww-rm
7460874c81 增加注释 2025-03-22 12:38:17 +08:00
ww-rm
13dd7511f6 优化预览图获取 2025-03-21 20:35:39 +08:00
ww-rm
f153d251c8 增加预览图留白 2025-03-21 20:09:08 +08:00
ww-rm
3442ace981 更新至v0.10.6 2025-03-21 14:57:50 +08:00
ww-rm
547cebf5a9 update readme 2025-03-21 14:57:30 +08:00
ww-rm
7a24d22bc6 update changelog 2025-03-21 14:57:25 +08:00
ww-rm
8f5728afe4 fix bug 2025-03-21 14:45:45 +08:00
ww-rm
41b5ac2c61 优化渲染 2025-03-21 14:31:20 +08:00
ww-rm
694ca3bf25 refactor 2025-03-21 13:32:03 +08:00
ww-rm
674d314b55 增加文件夹查找 2025-03-21 13:24:18 +08:00
ww-rm
08a35cc5d1 增加ctrlV导入 2025-03-21 11:14:31 +08:00
ww-rm
176e5db4d9 fix bug 2025-03-21 01:40:42 +08:00
ww-rm
2535a9ebf9 增加运行时标识 2025-03-21 01:05:08 +08:00
ww-rm
8ff99ee925 small optimize 2025-03-21 00:27:38 +08:00
ww-rm
abc8218487 add log 2025-03-21 00:23:31 +08:00
ww-rm
e4765750c3 更新至v0.10.5 2025-03-21 00:04:32 +08:00
ww-rm
02cddf556b update changelog 2025-03-21 00:04:07 +08:00
ww-rm
e1e6d3c72d fix bug 2025-03-21 00:03:31 +08:00
ww-rm
b401a16002 更新至v0.10.4 2025-03-20 23:50:04 +08:00
ww-rm
523b0ce295 update changelog 2025-03-20 23:49:54 +08:00
ww-rm
abb06726f0 fix bug 2025-03-20 23:49:45 +08:00
ww-rm
d9190e9418 修复图标显示问题 2025-03-20 20:30:04 +08:00
ww-rm
9fe3761eca 修改默认大小 2025-03-20 20:22:07 +08:00
ww-rm
51824afba6 优化列表使用 2025-03-20 20:02:25 +08:00
ww-rm
160a49ad5f 更新至v0.10.3 2025-03-20 15:38:04 +08:00
ww-rm
9d4907d77e update changelog and readme 2025-03-20 15:37:49 +08:00
ww-rm
53d30e0503 修改字母快捷键 2025-03-20 15:34:17 +08:00
ww-rm
9609a2fd5d 增加拖放文件打开 2025-03-20 15:32:31 +08:00
ww-rm
66cf0efcb9 增加单独的结果包装类 2025-03-20 15:31:35 +08:00
ww-rm
0129b9df31 small change 2025-03-20 14:20:45 +08:00
ww-rm
a7a5521be1 增加自动版本 2025-03-20 14:20:26 +08:00
ww-rm
f7f7211ca2 增加自动版本 2025-03-20 14:04:19 +08:00
ww-rm
8c921a6ed5 修改错误类型 2025-03-20 14:03:33 +08:00
ww-rm
f14ab870f7 update 2025-03-20 11:05:05 +08:00
ww-rm
26e81ffdb6 update readme preview 2025-03-20 10:14:59 +08:00
ww-rm
598a88203e 更新至v0.10.2 2025-03-20 00:12:41 +08:00
ww-rm
914d02e754 优化列表右键菜单功能 2025-03-20 00:10:09 +08:00
ww-rm
5cf30f391b 增加置顶 2025-03-19 17:07:47 +08:00
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
41 changed files with 5876 additions and 587 deletions

View File

@@ -1,4 +1,4 @@
name: Build and Release
name: Build & Release
on:
push:
@@ -47,8 +47,8 @@ jobs:
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
tag_name: ${{ github.ref_name }}
release_name: Release ${{ github.ref_name }}
tag_name: ${{ env.VERSION }}
release_name: Release ${{ env.VERSION }}
draft: false
prerelease: false

View File

@@ -1,8 +1,43 @@
# CHANGELOG
## v0.10.7
- 增加仅导出选中
- 增加模型调试属性
## v0.10.6
- 增加文件夹检测
- 增加从剪贴板添加(可复制本地文件/文件夹直接打开)
- 修复预览图导致的批量添加可能卡死
## v0.10.5
- 修复一些问题
## v0.10.4
- 修复一些问题
## v0.10.3
- 增加自动版本检测
- 增加文件拖放打开
## v0.10.2
- 增加列表右键菜单快捷键
- 增加预览缩略图复制
- 增加列表视图切换
## v0.10.1
- 增加列表预览图
- 增加列表预览图导出
## v0.10.0
- <EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>˻<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>б<EFBFBD><EFBFBD><EFBFBD>ѡ<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ɾ<EFBFBD><EFBFBD><EFBFBD><EFBFBD>Ԥ<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ʾ<EFBFBD><EFBFBD>Χ<EFBFBD><EFBFBD>ѡ<EFBFBD><EFBFBD>
- <EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>˹<EFBFBD><EFBFBD><EFBFBD><EFBFBD>ļ<EFBFBD><EFBFBD><EFBFBD>ʽת<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ܣ<EFBFBD>Ŀǰ<EFBFBD><EFBFBD>֧<EFBFBD>ֲ<EFBFBD><EFBFBD>ְ汾<EFBFBD>IJ<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>
- <EFBFBD>Ż<EFBFBD><EFBFBD>˲<EFBFBD><EFBFBD><EFBFBD>ʹ<EFBFBD><EFBFBD><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.webp)
---

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.webp)
---
@@ -20,7 +20,7 @@
## 功能支持
| 版本 | 查看&导出 | 转换 |
| 版本 | 查看&导出 | 格式转换 |
| :---: | :---: | :---: |
| `2.1.x` | :white_check_mark: | |
| `3.1.x` | | |
@@ -28,15 +28,19 @@
| `3.5.x` | | |
| `3.6.x` | :white_check_mark: | |
| `3.7.x` | :white_check_mark: | |
| `3.8.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` | | |
- 支持文件拖放/复制到剪贴板打开
- 支持自动检测版本
- 支持列表缩略图预览
- 支持多骨骼文件动画预览
- 支持每个骨骼独立参数设置
- 支持动画PNG帧序列导出
- 支持缩放旋转等导出画面设置
- 支持对独立的骨骼文件进行格式转换
- Coming soon...
## 使用方法
@@ -45,6 +49,8 @@
**文件**菜单可以选择**打开**或者**批量打开**进行骨骼文件导入.
或者直接把要打开的骨骼文件拖进列表, 这种方式只支持 `.json``.skel` 后缀的文件, 其他的会被忽略.
### 骨骼调整
在**模型列表**中选择一项或多项, 将会在**模型参数**面板显示可供调节的参数.

View File

@@ -129,8 +129,8 @@ namespace SpineRuntime38 {
if (skeletonData.hash.Length == 0) skeletonData.hash = null;
skeletonData.version = input.ReadString();
if (skeletonData.version.Length == 0) skeletonData.version = null;
if ("3.8.75" == skeletonData.version)
throw new Exception("Unsupported skeleton data, please export with a newer version of Spine.");
//if ("3.8.75" == skeletonData.version)
// throw new Exception("Unsupported skeleton data, please export with a newer version of Spine.");
skeletonData.x = input.ReadFloat();
skeletonData.y = input.ReadFloat();
skeletonData.width = input.ReadFloat();

View File

@@ -100,8 +100,8 @@ namespace SpineRuntime38 {
var skeletonMap = (Dictionary<string, Object>)root["skeleton"];
skeletonData.hash = (string)skeletonMap["hash"];
skeletonData.version = (string)skeletonMap["spine"];
if ("3.8.75" == skeletonData.version)
throw new Exception("Unsupported skeleton data, please export with a newer version of Spine.");
//if ("3.8.75" == skeletonData.version)
// throw new Exception("Unsupported skeleton data, please export with a newer version of Spine.");
skeletonData.x = GetFloat(skeletonMap, "x", 0);
skeletonData.y = GetFloat(skeletonMap, "y", 0);
skeletonData.width = GetFloat(skeletonMap, "width", 0);

View File

@@ -36,34 +36,50 @@
toolStripMenuItem_Insert = new ToolStripMenuItem();
toolStripMenuItem_Remove = new ToolStripMenuItem();
toolStripSeparator1 = new ToolStripSeparator();
toolStripMenuItem_MoveUp = new ToolStripMenuItem();
toolStripMenuItem_MoveDown = new ToolStripMenuItem();
toolStripSeparator2 = new ToolStripSeparator();
toolStripMenuItem_BatchAdd = new ToolStripMenuItem();
toolStripMenuItem_RemoveAll = new ToolStripMenuItem();
toolStripSeparator2 = new ToolStripSeparator();
toolStripMenuItem_MoveUp = new ToolStripMenuItem();
toolStripMenuItem_MoveDown = new ToolStripMenuItem();
toolStripMenuItem_MoveTop = new ToolStripMenuItem();
toolStripMenuItem_MoveBottom = new ToolStripMenuItem();
toolStripSeparator3 = new ToolStripSeparator();
toolStripMenuItem_SelectAll = new ToolStripMenuItem();
toolStripMenuItem_CopyPreview = new ToolStripMenuItem();
toolStripSeparator4 = new ToolStripSeparator();
toolStripMenuItem_ChangeView = new ToolStripMenuItem();
toolStripMenuItem_LargeIconView = new ToolStripMenuItem();
toolStripMenuItem_ListView = new ToolStripMenuItem();
toolStripMenuItem_DetailsView = new ToolStripMenuItem();
imageList_LargeIcon = new ImageList(components);
imageList_SmallIcon = new ImageList(components);
toolStripMenuItem_AddFromClipboard = new ToolStripMenuItem();
contextMenuStrip.SuspendLayout();
SuspendLayout();
//
// listView
//
listView.Alignment = ListViewAlignment.Left;
listView.AllowDrop = true;
listView.Columns.AddRange(new ColumnHeader[] { columnHeader_Name });
listView.ContextMenuStrip = contextMenuStrip;
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;
listView.ItemDrag += listView_ItemDrag;
listView.SelectedIndexChanged += listView_SelectedIndexChanged;
listView.DragDrop += listView_DragDrop;
listView.DragEnter += listView_DragEnter;
listView.DragOver += listView_DragOver;
listView.KeyDown += listView_KeyDown;
//
// columnHeader_Name
//
@@ -73,72 +89,166 @@
// 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_BatchAdd, toolStripMenuItem_RemoveAll, toolStripSeparator2, toolStripMenuItem_MoveUp, toolStripMenuItem_MoveDown, toolStripMenuItem_MoveTop, toolStripMenuItem_MoveBottom, toolStripSeparator3, toolStripMenuItem_CopyPreview, toolStripMenuItem_AddFromClipboard, toolStripMenuItem_SelectAll, toolStripSeparator4, toolStripMenuItem_ChangeView });
contextMenuStrip.Name = "contextMenuStrip";
contextMenuStrip.Size = new Size(188, 226);
contextMenuStrip.Size = new Size(329, 451);
contextMenuStrip.Closed += contextMenuStrip_Closed;
contextMenuStrip.Opening += contextMenuStrip_Opening;
//
// toolStripMenuItem_Add
//
toolStripMenuItem_Add.Name = "toolStripMenuItem_Add";
toolStripMenuItem_Add.Size = new Size(187, 30);
toolStripMenuItem_Add.Text = "添加(&A)...";
toolStripMenuItem_Add.Size = new Size(328, 30);
toolStripMenuItem_Add.Text = "添加...";
toolStripMenuItem_Add.Click += toolStripMenuItem_Add_Click;
//
// toolStripMenuItem_Insert
//
toolStripMenuItem_Insert.Enabled = false;
toolStripMenuItem_Insert.Name = "toolStripMenuItem_Insert";
toolStripMenuItem_Insert.Size = new Size(187, 30);
toolStripMenuItem_Insert.Text = "插入(&I)...";
toolStripMenuItem_Insert.Size = new Size(328, 30);
toolStripMenuItem_Insert.Text = "插入...";
toolStripMenuItem_Insert.Click += toolStripMenuItem_Insert_Click;
//
// toolStripMenuItem_Remove
//
toolStripMenuItem_Remove.Enabled = false;
toolStripMenuItem_Remove.Name = "toolStripMenuItem_Remove";
toolStripMenuItem_Remove.Size = new Size(187, 30);
toolStripMenuItem_Remove.Text = "移除(&R)";
toolStripMenuItem_Remove.ShortcutKeys = Keys.Delete;
toolStripMenuItem_Remove.Size = new Size(328, 30);
toolStripMenuItem_Remove.Text = "移除";
toolStripMenuItem_Remove.Click += toolStripMenuItem_Remove_Click;
//
// toolStripSeparator1
//
toolStripSeparator1.Name = "toolStripSeparator1";
toolStripSeparator1.Size = new Size(184, 6);
toolStripSeparator1.Size = new Size(325, 6);
//
// toolStripMenuItem_BatchAdd
//
toolStripMenuItem_BatchAdd.Name = "toolStripMenuItem_BatchAdd";
toolStripMenuItem_BatchAdd.Size = new Size(328, 30);
toolStripMenuItem_BatchAdd.Text = "批量添加...";
toolStripMenuItem_BatchAdd.Click += toolStripMenuItem_BatchAdd_Click;
//
// toolStripMenuItem_RemoveAll
//
toolStripMenuItem_RemoveAll.Name = "toolStripMenuItem_RemoveAll";
toolStripMenuItem_RemoveAll.Size = new Size(328, 30);
toolStripMenuItem_RemoveAll.Text = "移除全部";
toolStripMenuItem_RemoveAll.Click += toolStripMenuItem_RemoveAll_Click;
//
// toolStripSeparator2
//
toolStripSeparator2.Name = "toolStripSeparator2";
toolStripSeparator2.Size = new Size(325, 6);
//
// toolStripMenuItem_MoveUp
//
toolStripMenuItem_MoveUp.Name = "toolStripMenuItem_MoveUp";
toolStripMenuItem_MoveUp.Size = new Size(187, 30);
toolStripMenuItem_MoveUp.Text = "上移(&U)";
toolStripMenuItem_MoveUp.ShortcutKeys = Keys.Alt | Keys.W;
toolStripMenuItem_MoveUp.Size = new Size(328, 30);
toolStripMenuItem_MoveUp.Text = "上移";
toolStripMenuItem_MoveUp.Click += toolStripMenuItem_MoveUp_Click;
//
// toolStripMenuItem_MoveDown
//
toolStripMenuItem_MoveDown.Name = "toolStripMenuItem_MoveDown";
toolStripMenuItem_MoveDown.Size = new Size(187, 30);
toolStripMenuItem_MoveDown.Text = "下移(&D)";
toolStripMenuItem_MoveDown.ShortcutKeys = Keys.Alt | Keys.S;
toolStripMenuItem_MoveDown.Size = new Size(328, 30);
toolStripMenuItem_MoveDown.Text = "下移";
toolStripMenuItem_MoveDown.Click += toolStripMenuItem_MoveDown_Click;
//
// toolStripSeparator2
// toolStripMenuItem_MoveTop
//
toolStripSeparator2.Name = "toolStripSeparator2";
toolStripSeparator2.Size = new Size(184, 6);
toolStripMenuItem_MoveTop.Name = "toolStripMenuItem_MoveTop";
toolStripMenuItem_MoveTop.ShortcutKeys = Keys.Alt | Keys.Shift | Keys.W;
toolStripMenuItem_MoveTop.Size = new Size(328, 30);
toolStripMenuItem_MoveTop.Text = "置顶";
toolStripMenuItem_MoveTop.Click += toolStripMenuItem_MoveTop_Click;
//
// toolStripMenuItem_BatchAdd
// toolStripMenuItem_MoveBottom
//
toolStripMenuItem_BatchAdd.Name = "toolStripMenuItem_BatchAdd";
toolStripMenuItem_BatchAdd.Size = new Size(187, 30);
toolStripMenuItem_BatchAdd.Text = "批量添加(&B)...";
toolStripMenuItem_BatchAdd.Click += toolStripMenuItem_BatchAdd_Click;
toolStripMenuItem_MoveBottom.Name = "toolStripMenuItem_MoveBottom";
toolStripMenuItem_MoveBottom.ShortcutKeys = Keys.Alt | Keys.Shift | Keys.S;
toolStripMenuItem_MoveBottom.Size = new Size(328, 30);
toolStripMenuItem_MoveBottom.Text = "置底";
toolStripMenuItem_MoveBottom.Click += toolStripMenuItem_MoveBottom_Click;
//
// toolStripMenuItem_RemoveAll
// toolStripSeparator3
//
toolStripMenuItem_RemoveAll.Enabled = false;
toolStripMenuItem_RemoveAll.Name = "toolStripMenuItem_RemoveAll";
toolStripMenuItem_RemoveAll.Size = new Size(187, 30);
toolStripMenuItem_RemoveAll.Text = "移除全部(&X)";
toolStripMenuItem_RemoveAll.Click += toolStripMenuItem_RemoveAll_Click;
toolStripSeparator3.Name = "toolStripSeparator3";
toolStripSeparator3.Size = new Size(325, 6);
//
// toolStripMenuItem_SelectAll
//
toolStripMenuItem_SelectAll.Name = "toolStripMenuItem_SelectAll";
toolStripMenuItem_SelectAll.ShortcutKeys = Keys.Control | Keys.A;
toolStripMenuItem_SelectAll.Size = new Size(328, 30);
toolStripMenuItem_SelectAll.Text = "全选";
toolStripMenuItem_SelectAll.Click += toolStripMenuItem_SelectAll_Click;
//
// toolStripMenuItem_CopyPreview
//
toolStripMenuItem_CopyPreview.Name = "toolStripMenuItem_CopyPreview";
toolStripMenuItem_CopyPreview.ShortcutKeys = Keys.Control | Keys.C;
toolStripMenuItem_CopyPreview.Size = new Size(328, 30);
toolStripMenuItem_CopyPreview.Text = "复制预览图 (256x256)";
toolStripMenuItem_CopyPreview.Click += toolStripMenuItem_CopyPreview_Click;
//
// toolStripSeparator4
//
toolStripSeparator4.Name = "toolStripSeparator4";
toolStripSeparator4.Size = new Size(325, 6);
//
// toolStripMenuItem_ChangeView
//
toolStripMenuItem_ChangeView.DropDownItems.AddRange(new ToolStripItem[] { toolStripMenuItem_LargeIconView, toolStripMenuItem_ListView, toolStripMenuItem_DetailsView });
toolStripMenuItem_ChangeView.Name = "toolStripMenuItem_ChangeView";
toolStripMenuItem_ChangeView.Size = new Size(328, 30);
toolStripMenuItem_ChangeView.Text = "切换视图";
//
// toolStripMenuItem_LargeIconView
//
toolStripMenuItem_LargeIconView.Name = "toolStripMenuItem_LargeIconView";
toolStripMenuItem_LargeIconView.ShortcutKeys = Keys.Alt | Keys.D1;
toolStripMenuItem_LargeIconView.Size = new Size(241, 34);
toolStripMenuItem_LargeIconView.Text = "大图标";
toolStripMenuItem_LargeIconView.Click += toolStripMenuItem_LargeIconView_Click;
//
// toolStripMenuItem_ListView
//
toolStripMenuItem_ListView.Name = "toolStripMenuItem_ListView";
toolStripMenuItem_ListView.ShortcutKeys = Keys.Alt | Keys.D2;
toolStripMenuItem_ListView.Size = new Size(241, 34);
toolStripMenuItem_ListView.Text = "列表";
toolStripMenuItem_ListView.Click += toolStripMenuItem_ListView_Click;
//
// toolStripMenuItem_DetailsView
//
toolStripMenuItem_DetailsView.Name = "toolStripMenuItem_DetailsView";
toolStripMenuItem_DetailsView.ShortcutKeys = Keys.Alt | Keys.D3;
toolStripMenuItem_DetailsView.Size = new Size(241, 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;
//
// toolStripMenuItem_AddFromClipboard
//
toolStripMenuItem_AddFromClipboard.Name = "toolStripMenuItem_AddFromClipboard";
toolStripMenuItem_AddFromClipboard.ShortcutKeys = Keys.Control | Keys.V;
toolStripMenuItem_AddFromClipboard.Size = new Size(328, 30);
toolStripMenuItem_AddFromClipboard.Text = "从剪贴板添加";
toolStripMenuItem_AddFromClipboard.Click += toolStripMenuItem_AddFromClipboard_Click;
//
// SpineListView
//
@@ -164,5 +274,18 @@
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_ListView;
private ToolStripMenuItem toolStripMenuItem_DetailsView;
private ToolStripMenuItem toolStripMenuItem_MoveTop;
private ToolStripMenuItem toolStripMenuItem_MoveBottom;
private ToolStripMenuItem toolStripMenuItem_CopyPreview;
private ToolStripMenuItem toolStripMenuItem_SelectAll;
private ToolStripSeparator toolStripSeparator4;
private ToolStripMenuItem toolStripMenuItem_AddFromClipboard;
}
}

View File

@@ -11,21 +11,25 @@ using System.Collections.ObjectModel;
using SpineViewer.Spine;
using System.Reflection;
using System.Diagnostics;
using System.Collections.Specialized;
namespace SpineViewer.Controls
{
public partial class SpineListView : UserControl
{
/// <summary>
/// 显示骨骼信息的属性面板
/// </summary>
[Category("自定义"), Description("用于显示骨骼属性的属性页")]
public PropertyGrid? PropertyGrid { get; set; }
/// <summary>
/// 获取数组快照, 访问时必须使用 lock 语句锁定对象本身
/// Spine 列表只读视图, 访问时必须使用 lock 语句锁定视图本身
/// </summary>
public readonly ReadOnlyCollection<Spine.Spine> Spines;
/// <summary>
/// Spine 列表, 访问时必须使用 lock 语句锁定 Spines
/// Spine 列表, 访问时必须使用 lock 语句锁定只读视图 Spines
/// </summary>
private readonly List<Spine.Spine> spines = [];
@@ -36,30 +40,47 @@ namespace SpineViewer.Controls
}
/// <summary>
/// listView.SelectedIndices
/// 选中的索引
/// </summary>
public ListView.SelectedIndexCollection SelectedIndices { get => listView.SelectedIndices; }
public ListView.SelectedIndexCollection SelectedIndices => listView.SelectedIndices;
/// <summary>
/// 弹出添加对话框在指定位置之前插入一项
/// 弹出添加对话框在末尾添加
/// </summary>
public void Add() => Insert();
/// <summary>
/// 弹出添加对话框在指定位置之前插入一项, 如果索引无效则在末尾添加
/// </summary>
private void Insert(int index = -1)
{
var dialog = new Dialogs.OpenSpineDialog();
if (dialog.ShowDialog() != DialogResult.OK)
return;
Insert(dialog.Result, index);
}
/// <summary>
/// 从结果在指定位置之前插入一项, 如果索引无效则在末尾添加
/// </summary>
private void Insert(Dialogs.OpenSpineDialogResult result, int index = -1)
{
try
{
var spine = Spine.Spine.New(dialog.Version, dialog.SkelPath, dialog.AtlasPath);
var spine = Spine.Spine.New(result.Version, result.SkelPath, result.AtlasPath);
// 如果索引无效则在末尾添加
if (index < 0 || index > listView.Items.Count)
index = listView.Items.Count;
// 锁定外部的读操作
lock (Spines) { spines.Insert(index, spine); }
listView.Items.Insert(index, new ListViewItem(spine.Name) { ToolTipText = spine.SkelPath });
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();
@@ -68,19 +89,11 @@ namespace SpineViewer.Controls
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.Error("Failed to load {} {}", result.SkelPath, result.AtlasPath);
MessageBox.Error(ex.ToString(), "骨骼加载失败");
}
Program.Logger.Info($"Current memory usage: {Program.Process.WorkingSet64 / 1024.0 / 1024.0:F2} MB");
}
/// <summary>
/// 弹出添加对话框
/// </summary>
public void Add()
{
Insert();
Program.LogCurrentMemoryUsage();
}
/// <summary>
@@ -91,17 +104,27 @@ namespace SpineViewer.Controls
var openDialog = new Dialogs.BatchOpenSpineDialog();
if (openDialog.ShowDialog() != DialogResult.OK)
return;
BatchAdd(openDialog.Result);
}
/// <summary>
/// 从结果批量添加
/// </summary>
public void BatchAdd(Dialogs.BatchOpenSpineDialogResult result)
{
var progressDialog = new Dialogs.ProgressDialog();
progressDialog.DoWork += BatchAdd_Work;
progressDialog.RunWorkerAsync(openDialog);
progressDialog.RunWorkerAsync(result);
progressDialog.ShowDialog();
}
/// <summary>
/// 批量添加后台任务
/// </summary>
private void BatchAdd_Work(object? sender, DoWorkEventArgs e)
{
var worker = sender as BackgroundWorker;
var arguments = e.Argument as Dialogs.BatchOpenSpineDialog;
var arguments = e.Argument as Dialogs.BatchOpenSpineDialogResult;
var skelPaths = arguments.SkelPaths;
var version = arguments.Version;
@@ -123,8 +146,14 @@ namespace SpineViewer.Controls
try
{
var spine = Spine.Spine.New(version, skelPath);
var preview = spine.Preview;
lock (Spines) { spines.Add(spine); }
listView.Invoke(() => listView.Items.Add(new ListViewItem(spine.Name) { ToolTipText = spine.SkelPath }));
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)
@@ -146,14 +175,52 @@ namespace SpineViewer.Controls
Program.Logger.Info("{} skel loaded successfully", success);
}
Program.Logger.Info($"Current memory usage: {Program.Process.WorkingSet64 / 1024.0 / 1024.0:F2} MB");
Program.LogCurrentMemoryUsage();
}
/// <summary>
/// 从拖放/复制的路径列表添加
/// </summary>
private void AddFromFileDrop(IEnumerable<string> paths)
{
List<string> validPaths = [];
foreach (var path in paths)
{
if (File.Exists(path))
{
if (Spine.Spine.CommonSkelSuffix.Contains(Path.GetExtension(path).ToLower()))
validPaths.Add(path);
}
else if (Directory.Exists(path))
{
foreach (var file in Directory.EnumerateFiles(path, "*.*", SearchOption.AllDirectories))
{
if (Spine.Spine.CommonSkelSuffix.Contains(Path.GetExtension(file).ToLower()))
validPaths.Add(file);
}
}
}
if (validPaths.Count > 1)
{
if (validPaths.Count > 100)
{
if (MessageBox.Quest($"共发现 {validPaths.Count} 个可加载骨骼,数量较多,是否一次性全部加载?") == DialogResult.Cancel)
return;
}
BatchAdd(new Dialogs.BatchOpenSpineDialogResult(Spine.Version.Auto, validPaths.ToArray()));
}
else if (validPaths.Count > 0)
{
Insert(new Dialogs.OpenSpineDialogResult(Spine.Version.Auto, validPaths[0]));
}
}
private void listView_SelectedIndexChanged(object sender, EventArgs e)
{
if (PropertyGrid is not null)
lock (Spines)
{
lock (Spines)
if (PropertyGrid is not null)
{
if (listView.SelectedIndices.Count <= 0)
PropertyGrid.SelectedObject = null;
@@ -161,25 +228,24 @@ 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);
}
}
}
private void listView_KeyDown(object sender, KeyEventArgs e)
{
if (e.Control && e.KeyCode == Keys.A)
// 标记选中的 Spine
for (int i = 0; i < spines.Count; i++)
spines[i].IsSelected = listView.SelectedIndices.Contains(i);
}
// XXX: 图标显示的时候没法自动刷新顺序, 只能切换视图刷新, 不知道什么原理
if (listView.View == View.LargeIcon)
{
listView.BeginUpdate();
foreach (ListViewItem item in listView.Items)
{
item.Selected = true;
}
listView.View = View.List;
listView.View = View.LargeIcon;
listView.EndUpdate();
}
if (listView.SelectedItems.Count > 0)
listView.SelectedItems[0].EnsureVisible();
}
private void listView_ItemDrag(object sender, ItemDragEventArgs e)
@@ -187,74 +253,107 @@ namespace SpineViewer.Controls
DoDragDrop(e.Item, DragDropEffects.Move);
}
private void listView_DragEnter(object sender, DragEventArgs e)
{
if (e.Data.GetDataPresent(DataFormats.Serializable))
e.Effect = DragDropEffects.Move;
else if (e.Data.GetDataPresent(DataFormats.FileDrop))
e.Effect = DragDropEffects.Copy;
else
e.Effect = DragDropEffects.None;
}
private void listView_DragOver(object sender, DragEventArgs e)
{
// 检查拖放目标是否有效
e.Effect = DragDropEffects.Move;
// 获取鼠标位置并确定目标索引
var point = listView.PointToClient(new(e.X, e.Y));
var targetItem = listView.GetItemAt(point.X, point.Y);
// 高亮目标项
if (targetItem != null)
if (e.Data.GetDataPresent(DataFormats.Serializable))
{
foreach (ListViewItem item in listView.Items)
// 获取鼠标位置并确定目标索引
var point = listView.PointToClient(new(e.X, e.Y));
var targetItem = listView.GetItemAt(point.X, point.Y);
// 高亮目标项
if (targetItem != null)
{
item.BackColor = listView.BackColor;
foreach (ListViewItem item in listView.Items)
{
item.BackColor = listView.BackColor;
}
targetItem.BackColor = Color.LightGray;
}
targetItem.BackColor = Color.LightGray;
}
}
private void listView_DragDrop(object sender, DragEventArgs e)
{
// 获取拖放源项和目标项
var draggedItem = (ListViewItem)e.Data.GetData(typeof(ListViewItem));
int draggedIndex = draggedItem.Index;
var point = listView.PointToClient(new Point(e.X, e.Y));
var targetItem = listView.GetItemAt(point.X, point.Y);
int targetIndex = targetItem is null ? listView.Items.Count : targetItem.Index;
if (e.Data.GetDataPresent(DataFormats.Serializable))
{
// 获取拖放源项和目标项
var draggedItem = (ListViewItem)e.Data.GetData(typeof(ListViewItem));
int draggedIndex = draggedItem.Index;
var point = listView.PointToClient(new Point(e.X, e.Y));
var targetItem = listView.GetItemAt(point.X, point.Y);
int targetIndex = targetItem is null ? listView.Items.Count : targetItem.Index;
if (targetIndex <= draggedIndex)
{
lock (Spines)
if (targetIndex <= draggedIndex)
{
var draggedSpine = spines[draggedIndex];
spines.RemoveAt(draggedIndex);
spines.Insert(targetIndex, draggedSpine);
lock (Spines)
{
var draggedSpine = spines[draggedIndex];
spines.RemoveAt(draggedIndex);
spines.Insert(targetIndex, draggedSpine);
}
listView.Items.RemoveAt(draggedIndex);
listView.Items.Insert(targetIndex, draggedItem);
}
listView.Items.RemoveAt(draggedIndex);
listView.Items.Insert(targetIndex, draggedItem);
}
else
{
lock (Spines)
else
{
var draggedSpine = spines[draggedIndex];
spines.RemoveAt(draggedIndex);
spines.Insert(targetIndex - 1, draggedSpine);
lock (Spines)
{
var draggedSpine = spines[draggedIndex];
spines.RemoveAt(draggedIndex);
spines.Insert(targetIndex - 1, draggedSpine);
}
listView.Items.RemoveAt(draggedIndex);
listView.Items.Insert(targetIndex - 1, draggedItem);
}
listView.Items.RemoveAt(draggedIndex);
listView.Items.Insert(targetIndex - 1, draggedItem);
}
// 重置背景颜色
foreach (ListViewItem item in listView.Items)
// 重置背景颜色
foreach (ListViewItem item in listView.Items)
{
item.BackColor = listView.BackColor;
}
}
else if (e.Data.GetDataPresent(DataFormats.FileDrop))
{
item.BackColor = listView.BackColor;
AddFromFileDrop((string[])e.Data.GetData(DataFormats.FileDrop));
}
}
private void contextMenuStrip_Opening(object sender, CancelEventArgs e)
{
var selectedCount = listView.SelectedIndices.Count;
var selectedIndices = listView.SelectedIndices;
var selectedCount = selectedIndices.Count;
var itemsCount = listView.Items.Count;
toolStripMenuItem_Insert.Enabled = selectedCount == 1;
toolStripMenuItem_Remove.Enabled = selectedCount >= 1;
toolStripMenuItem_MoveUp.Enabled = selectedCount == 1 && listView.SelectedIndices[0] != 0;
toolStripMenuItem_MoveDown.Enabled = selectedCount == 1 && listView.SelectedIndices[0] != itemsCount - 1;
toolStripMenuItem_MoveTop.Enabled = selectedCount == 1 && selectedIndices[0] != 0;
toolStripMenuItem_MoveUp.Enabled = selectedCount == 1 && selectedIndices[0] != 0;
toolStripMenuItem_MoveDown.Enabled = selectedCount == 1 && selectedIndices[0] != itemsCount - 1;
toolStripMenuItem_MoveBottom.Enabled = selectedCount == 1 && selectedIndices[0] != itemsCount - 1;
toolStripMenuItem_RemoveAll.Enabled = itemsCount > 0;
// 视图选项
toolStripMenuItem_LargeIconView.Checked = listView.View == View.LargeIcon;
toolStripMenuItem_ListView.Checked = listView.View == View.List;
toolStripMenuItem_DetailsView.Checked = listView.View == View.Details;
}
private void contextMenuStrip_Closed(object sender, ToolStripDropDownClosedEventArgs e)
{
// 不显示菜单的时候需要把菜单的各项功能启用, 这样才能正常捕获快捷键
foreach (var item in contextMenuStrip.Items)
if (item is ToolStripMenuItem tsmi)
tsmi.Enabled = true;
}
private void toolStripMenuItem_Add_Click(object sender, EventArgs e)
@@ -280,18 +379,41 @@ namespace SpineViewer.Controls
if (listView.SelectedIndices.Count > 1)
{
if (MessageBox.Show($"确定移除所选 {listView.SelectedIndices.Count} 项吗?", "操作确认", MessageBoxButtons.OKCancel, MessageBoxIcon.Question) != DialogResult.OK)
if (MessageBox.Quest($"确定移除所选 {listView.SelectedIndices.Count} 项吗?") != DialogResult.OK)
return;
}
foreach (var i in listView.SelectedIndices.Cast<int>().OrderByDescending(x => x))
{
listView.Items.RemoveAt(i);
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);
}
}
private void toolStripMenuItem_MoveTop_Click(object sender, EventArgs e)
{
if (listView.SelectedIndices.Count != 1)
return;
var index = listView.SelectedIndices[0];
if (index > 0)
{
lock (Spines)
{
var spine = spines[index];
spines.RemoveAt(index);
spines.Insert(0, spine);
}
var item = listView.Items[index];
listView.Items.RemoveAt(index);
listView.Items.Insert(0, item);
}
}
@@ -305,8 +427,10 @@ namespace SpineViewer.Controls
{
lock (Spines) { (spines[index - 1], spines[index]) = (spines[index], spines[index - 1]); }
var item = listView.Items[index];
listView.BeginUpdate();
listView.Items.RemoveAt(index);
listView.Items.Insert(index - 1, item);
listView.EndUpdate();
}
}
@@ -319,9 +443,31 @@ namespace SpineViewer.Controls
if (index < listView.Items.Count - 1)
{
lock (Spines) { (spines[index], spines[index + 1]) = (spines[index + 1], spines[index]); }
var item = listView.Items[index + 1];
listView.Items.RemoveAt(index + 1);
listView.Items.Insert(index, item);
var item = listView.Items[index];
listView.BeginUpdate();
listView.Items.RemoveAt(index);
listView.Items.Insert(index + 1, item);
listView.EndUpdate();
}
}
private void toolStripMenuItem_MoveBottom_Click(object sender, EventArgs e)
{
if (listView.SelectedIndices.Count != 1)
return;
var index = listView.SelectedIndices[0];
if (index < listView.Items.Count - 1)
{
lock (Spines)
{
var spine = spines[index];
spines.RemoveAt(index);
spines.Add(spine);
}
var item = listView.Items[index];
listView.Items.RemoveAt(index);
listView.Items.Add(item);
}
}
@@ -330,18 +476,73 @@ namespace SpineViewer.Controls
if (listView.Items.Count <= 0)
return;
if (MessageBox.Show($"确认移除所有 {listView.Items.Count} 项吗?", "操作确认", MessageBoxButtons.OKCancel, MessageBoxIcon.Question) != DialogResult.OK)
if (MessageBox.Quest($"确认移除所有 {listView.Items.Count} 项吗?") != DialogResult.OK)
return;
lock (Spines)
{
foreach (var spine in spines)
spine.Dispose();
spines.Clear();
}
listView.Items.Clear();
lock (Spines)
{
foreach (var spine in spines) spine.Dispose();
spines.Clear();
listView.SmallImageList.Images.Clear();
listView.LargeImageList.Images.Clear();
}
if (PropertyGrid is not null)
PropertyGrid.SelectedObject = null;
}
private void toolStripMenuItem_CopyPreview_Click(object sender, EventArgs e)
{
var fileDropList = new StringCollection();
lock (Spines)
{
foreach (int i in listView.SelectedIndices)
{
var spine = spines[i];
var image = spine.Preview;
var path = Path.Combine(Program.TempDir, $"{spine.ID}.png");
using (var clone = new Bitmap(image))
clone.Save(path);
fileDropList.Add(path);
}
}
if (fileDropList.Count > 0)
Clipboard.SetFileDropList(fileDropList);
}
private void toolStripMenuItem_AddFromClipboard_Click(object sender, EventArgs e)
{
if (Clipboard.ContainsFileDropList())
{
var fileDropList = Clipboard.GetFileDropList();
var paths = new string[fileDropList.Count];
fileDropList.CopyTo(paths, 0);
AddFromFileDrop(paths);
}
}
private void toolStripMenuItem_SelectAll_Click(object sender, EventArgs e)
{
listView.BeginUpdate();
foreach (ListViewItem item in listView.Items)
item.Selected = true;
listView.EndUpdate();
}
private void toolStripMenuItem_LargeIconView_Click(object sender, EventArgs e)
{
listView.View = View.LargeIcon;
}
private void toolStripMenuItem_ListView_Click(object sender, EventArgs e)
{
listView.View = View.List;
}
private void toolStripMenuItem_DetailsView_Click(object sender, EventArgs e)
{
listView.View = View.Details;
}
}
}

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>511, 20</value>
</metadata>
<metadata name="imageList_SmallIcon.TrayLocation" type="System.Drawing.Point, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a">
<value>252, 19</value>
</metadata>
</root>

View File

@@ -16,14 +16,10 @@ namespace SpineViewer.Controls
public partial class SpinePreviewer : UserControl
{
/// <summary>
/// 包装类, 用于 PropertyGrid 显示
/// 包装类, 用于属性面板显示
/// </summary>
private class PreviewerProperty
private class PreviewerProperty(SpinePreviewer previewer)
{
private readonly SpinePreviewer previewer;
public PreviewerProperty(SpinePreviewer previewer) { this.previewer = previewer; }
[TypeConverter(typeof(SizeConverter))]
[Category("导出"), DisplayName("分辨率")]
public Size Resolution { get => previewer.Resolution; set => previewer.Resolution = value; }
@@ -44,6 +40,9 @@ namespace SpineViewer.Controls
[Category("导出"), DisplayName("垂直翻转")]
public bool FlipY { get => previewer.FlipY; set => previewer.FlipY = value; }
[Category("导出"), DisplayName("仅渲染选中")]
public bool RenderSelectedOnly { get => previewer.RenderSelectedOnly; set => previewer.RenderSelectedOnly = value; }
[Category("预览"), DisplayName("显示坐标轴")]
public bool ShowAxis { get => previewer.ShowAxis; set => previewer.ShowAxis = value; }
@@ -51,9 +50,15 @@ namespace SpineViewer.Controls
public uint MaxFps { get => previewer.MaxFps; set => previewer.MaxFps = value; }
}
/// <summary>
/// 要绑定的 Spine 列表控件
/// </summary>
[Category("自定义"), Description("相关联的 SpineListView")]
public SpineListView? SpineListView { get; set; }
/// <summary>
/// 属性信息面板
/// </summary>
[Category("自定义"), Description("用于显示画面属性的属性页")]
public PropertyGrid? PropertyGrid
{
@@ -67,21 +72,49 @@ namespace SpineViewer.Controls
}
private PropertyGrid? propertyGrid;
/// <summary>
/// 画面缩放最大值
/// </summary>
public const float ZOOM_MAX = 1000f;
/// <summary>
/// 画面缩放最小值
/// </summary>
public const float ZOOM_MIN = 0.001f;
public const int BACKGROUND_CELL_SIZE = 10;
/// <summary>
/// 预览画面背景色
/// </summary>
private static readonly SFML.Graphics.Color BackgroundColor = new(105, 105, 105);
/// <summary>
/// 预览画面坐标轴颜色
/// </summary>
private static readonly SFML.Graphics.Color AxisColor = new(220, 220, 220);
private static readonly SFML.Graphics.Color BoundsColor = new(120, 200, 0);
/// <summary>
/// 坐标轴顶点缓冲区
/// </summary>
private readonly SFML.Graphics.VertexArray AxisVertex = new(SFML.Graphics.PrimitiveType.Lines, 2);
private readonly SFML.Graphics.VertexArray BoundsRect = new(SFML.Graphics.PrimitiveType.LineStrip, 5);
/// <summary>
/// 渲染窗口
/// </summary>
private readonly SFML.Graphics.RenderWindow RenderWindow;
/// <summary>
/// 帧间隔计时器
/// </summary>
private readonly SFML.System.Clock Clock = new();
/// <summary>
/// 画面拖放对象世界坐标源点
/// </summary>
private SFML.System.Vector2f? draggingSrc = null;
/// <summary>
/// 渲染任务
/// </summary>
private Task? task = null;
private CancellationTokenSource? cancelToken = null;
@@ -225,6 +258,13 @@ namespace SpineViewer.Controls
}
}
/// <summary>
/// 仅渲染选中
/// </summary>
[DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
[Browsable(false)]
public bool RenderSelectedOnly { get; set; } = false;
/// <summary>
/// 显示坐标轴
/// </summary>
@@ -240,13 +280,6 @@ namespace SpineViewer.Controls
public uint MaxFps { get => maxFps; set { RenderWindow.SetFramerateLimit(value); maxFps = value; } }
private uint maxFps = 60;
/// <summary>
/// RenderWindow.View
/// </summary>
[DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
[Browsable(false)]
public SFML.Graphics.View View { get => RenderWindow.GetView(); }
public SpinePreviewer()
{
InitializeComponent();
@@ -260,6 +293,11 @@ namespace SpineViewer.Controls
MaxFps = 30;
}
/// <summary>
/// 预览画面帧参数
/// </summary>
public SpinePreviewerFrameArgs GetFrameArgs() => new(Resolution, RenderWindow.GetView(), RenderSelectedOnly);
/// <summary>
/// 开始预览
/// </summary>
@@ -284,6 +322,67 @@ namespace SpineViewer.Controls
task = null;
}
/// <summary>
/// 渲染任务
/// </summary>
private void RenderTask()
{
try
{
RenderWindow.SetActive(true);
float delta;
while (cancelToken is not null && !cancelToken.IsCancellationRequested)
{
delta = Clock.ElapsedTime.AsSeconds();
Clock.Restart();
RenderWindow.Clear(BackgroundColor);
if (ShowAxis)
{
// 画一个很长的坐标轴, 用 1e9 比较合适
AxisVertex[0] = new(new(-1e9f, 0), AxisColor);
AxisVertex[1] = new(new(1e9f, 0), AxisColor);
RenderWindow.Draw(AxisVertex);
AxisVertex[0] = new(new(0, -1e9f), AxisColor);
AxisVertex[1] = new(new(0, 1e9f), AxisColor);
RenderWindow.Draw(AxisVertex);
}
// 渲染 Spine
if (SpineListView is not null)
{
lock (SpineListView.Spines)
{
var spines = SpineListView.Spines;
for (int i = spines.Count - 1; i >= 0; i--)
{
if (cancelToken is not null && cancelToken.IsCancellationRequested)
break; // 提前中止
var spine = spines[i];
spine.Update(delta);
if (RenderSelectedOnly && !spine.IsSelected)
continue;
spine.IsDebug = true;
RenderWindow.Draw(spine);
spine.IsDebug = false;
}
}
}
RenderWindow.Display();
}
}
finally
{
RenderWindow.SetActive(false);
}
}
private void SpinePreviewer_SizeChanged(object sender, EventArgs e)
{
if (RenderWindow is null)
@@ -326,45 +425,60 @@ namespace SpineViewer.Controls
draggingSrc = RenderWindow.MapPixelToCoords(new(e.X, e.Y));
var src = new PointF(((SFML.System.Vector2f)draggingSrc).X, ((SFML.System.Vector2f)draggingSrc).Y);
if (SpineListView is not null)
{
lock (SpineListView.Spines)
{
var spines = SpineListView.Spines;
if (SpineListView is null)
return;
lock (SpineListView.Spines)
{
var spines = SpineListView.Spines;
// 仅渲染选中模式禁止在画面里选择对象
if (RenderSelectedOnly)
{
bool hit = false;
foreach (int i in SpineListView.SelectedIndices)
{
if (!spines[i].Bounds.Contains(src)) continue;
hit = true;
break;
}
// 如果没点到被选中的模型, 则不允许拖动
if (!hit) draggingSrc = null;
}
else
{
// 没有按下 Ctrl 键就只选中点击的那个, 所以先清空选中列表
if ((ModifierKeys & Keys.Control) == 0)
{
bool hit = false;
for (int i = 0; i < spines.Count; i++)
{
if (spines[i].Bounds.Contains(src))
{
hit = true;
if (!spines[i].Bounds.Contains(src)) continue;
// 如果点到了没被选中的东西, 则清空原先选中的, 改为只选中这一次点的
if (!SpineListView.SelectedIndices.Contains(i))
{
SpineListView.SelectedIndices.Clear();
SpineListView.SelectedIndices.Add(i);
}
break;
hit = true;
// 如果点到了没被选中的东西, 则清空原先选中的, 改为只选中这一次点的
if (!SpineListView.SelectedIndices.Contains(i))
{
SpineListView.SelectedIndices.Clear();
SpineListView.SelectedIndices.Add(i);
}
break;
}
// 如果点了空白的地方, 就清空选中列表
if (!hit)
SpineListView.SelectedIndices.Clear();
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;
}
if (!spines[i].Bounds.Contains(src))
continue;
SpineListView.SelectedIndices.Add(i);
break;
}
}
}
@@ -392,11 +506,8 @@ namespace SpineViewer.Controls
{
lock (SpineListView.Spines)
{
foreach (var spine in SpineListView.Spines)
{
if (spine.IsSelected)
spine.Position += delta;
}
foreach (int i in SpineListView.SelectedIndices)
SpineListView.Spines[i].Position += delta;
}
}
draggingSrc = dst;
@@ -427,63 +538,27 @@ namespace SpineViewer.Controls
Zoom *= (e.Delta > 0 ? 1.1f : 0.9f);
PropertyGrid?.Refresh();
}
}
private void RenderTask()
{
try
{
RenderWindow.SetActive(true);
/// <summary>
/// 预览画面帧参数
/// </summary>
public class SpinePreviewerFrameArgs(Size resolution, SFML.Graphics.View view, bool renderSelectedOnly)
{
/// <summary>
/// 分辨率
/// </summary>
public Size Resolution => resolution;
float delta;
while (cancelToken is not null && !cancelToken.IsCancellationRequested)
{
delta = Clock.ElapsedTime.AsSeconds();
Clock.Restart();
/// <summary>
/// 渲染视窗
/// </summary>
public SFML.Graphics.View View => view;
RenderWindow.Clear(BackgroundColor);
if (ShowAxis)
{
// 画一个很长的坐标轴, 用 1e9 比较合适
AxisVertex[0] = new(new(-1e9f, 0), AxisColor);
AxisVertex[1] = new(new(1e9f, 0), AxisColor);
RenderWindow.Draw(AxisVertex);
AxisVertex[0] = new(new(0, -1e9f), AxisColor);
AxisVertex[1] = new(new(0, 1e9f), AxisColor);
RenderWindow.Draw(AxisVertex);
}
// 渲染 Spine
if (SpineListView is not null)
{
lock (SpineListView.Spines)
{
foreach (var spine in SpineListView.Spines.Reverse())
{
spine.Update(delta);
RenderWindow.Draw(spine);
if (spine.IsSelected)
{
var bounds = spine.Bounds;
BoundsRect[0] = BoundsRect[4] = new(new(bounds.Left, bounds.Top), BoundsColor);
BoundsRect[1] = new(new(bounds.Right, bounds.Top), BoundsColor);
BoundsRect[2] = new(new(bounds.Right, bounds.Bottom), BoundsColor);
BoundsRect[3] = new(new(bounds.Left, bounds.Bottom), BoundsColor);
RenderWindow.Draw(BoundsRect);
}
}
}
}
RenderWindow.Display();
}
}
finally
{
RenderWindow.SetActive(false);
}
}
/// <summary>
/// 是否仅渲染/导出选中骨骼
/// </summary>
public bool RenderSelectedOnly => renderSelectedOnly;
}
}

View File

@@ -15,16 +15,12 @@ namespace SpineViewer.Dialogs
public AboutDialog()
{
InitializeComponent();
this.label_Version.Text = $"v{InformationalVersion}";
Text = $"关于 {Program.Name}";
label_Version.Text = $"v{InformationalVersion}";
}
public string InformationalVersion
{
get
{
return Assembly.GetExecutingAssembly().GetCustomAttribute<AssemblyInformationalVersionAttribute>()?.InformationalVersion;
}
}
public string InformationalVersion =>
Assembly.GetExecutingAssembly().GetCustomAttribute<AssemblyInformationalVersionAttribute>()?.InformationalVersion;
private void linkLabel_RepoUrl_LinkClicked(object sender, LinkLabelLinkClickedEventArgs e)
{
@@ -36,7 +32,7 @@ namespace SpineViewer.Dialogs
else
{
Clipboard.SetText(url);
MessageBox.Show(this, "链接已复制到剪贴板,请前往浏览器进行访问", "提示信息", MessageBoxButtons.OK, MessageBoxIcon.Information);
MessageBox.Info("链接已复制到剪贴板,请前往浏览器进行访问");
}
}
}

View File

@@ -13,16 +13,18 @@ namespace SpineViewer.Dialogs
{
public partial class BatchOpenSpineDialog : Form
{
public string[] SkelPaths { get; private set; }
public Spine.Version Version { get; private set; }
/// <summary>
/// 对话框结果, 取消时为 null
/// </summary>
public BatchOpenSpineDialogResult Result { get; private set; }
public BatchOpenSpineDialog()
{
InitializeComponent();
comboBox_Version.DataSource = VersionHelper.Versions.ToList();
comboBox_Version.DataSource = VersionHelper.Names.ToList();
comboBox_Version.DisplayMember = "Value";
comboBox_Version.ValueMember = "Key";
comboBox_Version.SelectedValue = Spine.Version.V38;
comboBox_Version.SelectedValue = Spine.Version.Auto;
}
private void BatchOpenSpineDialog_Load(object sender, EventArgs e)
@@ -47,7 +49,7 @@ namespace SpineViewer.Dialogs
if (listBox_FilePath.Items.Count <= 0)
{
MessageBox.Show("未选择任何文件", "错误信息", MessageBoxButtons.OK, MessageBoxIcon.Information);
MessageBox.Info("未选择任何文件");
return;
}
@@ -55,20 +57,18 @@ namespace SpineViewer.Dialogs
{
if (!File.Exists(p))
{
MessageBox.Show($"{p}", "skel文件不存在", MessageBoxButtons.OK, MessageBoxIcon.Information);
MessageBox.Info($"{p}", "skel文件不存在");
return;
}
}
if (!Spine.Spine.ImplementedVersions.Contains(version))
if (version != Spine.Version.Auto && !Spine.Spine.ImplementedVersions.Contains(version))
{
MessageBox.Show($"{version.String()} 版本尚未实现(咕咕咕~", "错误信息", MessageBoxButtons.OK, MessageBoxIcon.Information);
MessageBox.Info($"{version.GetName()} 版本尚未实现(咕咕咕~");
return;
}
SkelPaths = listBox_FilePath.Items.Cast<string>().ToArray();
Version = version;
Result = new(version, listBox_FilePath.Items.Cast<string>().ToArray());
DialogResult = DialogResult.OK;
}
@@ -77,4 +77,20 @@ namespace SpineViewer.Dialogs
DialogResult = DialogResult.Cancel;
}
}
/// <summary>
/// 批量打开对话框结果
/// </summary>
public class BatchOpenSpineDialogResult(Spine.Version version, string[] skelPaths)
{
/// <summary>
/// 版本
/// </summary>
public Spine.Version Version => version;
/// <summary>
/// 路径列表
/// </summary>
public string[] SkelPaths => skelPaths;
}
}

View File

@@ -37,7 +37,7 @@
label1 = new Label();
label4 = new Label();
label3 = new Label();
comboBox_Version = new ComboBox();
comboBox_SourceVersion = new ComboBox();
tableLayoutPanel2 = new TableLayoutPanel();
button_Ok = new Button();
button_Cancel = new Button();
@@ -75,7 +75,7 @@
tableLayoutPanel1.Controls.Add(label1, 0, 4);
tableLayoutPanel1.Controls.Add(label4, 0, 0);
tableLayoutPanel1.Controls.Add(label3, 0, 3);
tableLayoutPanel1.Controls.Add(comboBox_Version, 1, 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);
@@ -166,14 +166,14 @@
//
// comboBox_Version
//
comboBox_Version.Anchor = AnchorStyles.Left;
comboBox_Version.DropDownStyle = ComboBoxStyle.DropDownList;
comboBox_Version.FormattingEnabled = true;
comboBox_Version.Location = new Point(146, 303);
comboBox_Version.Name = "comboBox_Version";
comboBox_Version.Size = new Size(182, 32);
comboBox_Version.Sorted = true;
comboBox_Version.TabIndex = 13;
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
//
@@ -320,6 +320,7 @@
ShowInTaskbar = false;
StartPosition = FormStartPosition.CenterScreen;
Text = "骨骼文件格式转换";
Load += ConvertFileFormatDialog_Load;
panel.ResumeLayout(false);
tableLayoutPanel1.ResumeLayout(false);
tableLayoutPanel1.PerformLayout();
@@ -337,7 +338,7 @@
private TableLayoutPanel tableLayoutPanel1;
private Label label4;
private Label label3;
private ComboBox comboBox_Version;
private ComboBox comboBox_SourceVersion;
private TableLayoutPanel tableLayoutPanel2;
private Button button_Ok;
private Button button_Cancel;

View File

@@ -13,17 +13,35 @@ namespace SpineViewer.Dialogs
{
public partial class ConvertFileFormatDialog : Form
{
// TODO: 增加版本转换选项
// TODO: 使用结果包装类
public string[] SkelPaths { get; private set; }
public Spine.Version Version { get; private set; }
public bool ConvertToJson { 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_Version.DataSource = VersionHelper.Versions.ToList();
comboBox_Version.DisplayMember = "Value";
comboBox_Version.ValueMember = "Key";
comboBox_Version.SelectedValue = Spine.Version.V38;
// XXX: 文件格式转换暂时不支持自动检测版本
var impVersions = VersionHelper.Names.ToDictionary();
impVersions.Remove(Spine.Version.Auto);
comboBox_SourceVersion.DataSource = impVersions.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)
@@ -39,11 +57,14 @@ namespace SpineViewer.Dialogs
private void button_Ok_Click(object sender, EventArgs e)
{
var version = (Spine.Version)comboBox_Version.SelectedValue;
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);
MessageBox.Info("未选择任何文件");
return;
}
@@ -51,21 +72,34 @@ namespace SpineViewer.Dialogs
{
if (!File.Exists(p))
{
MessageBox.Show($"{p}", "skel文件不存在", MessageBoxButtons.OK, MessageBoxIcon.Information);
MessageBox.Info($"{p}", "skel文件不存在");
return;
}
}
if (!Spine.Spine.ImplementedVersions.Contains(version) ||
!SkeletonConverter.ImplementedVersions.Contains(version))
if (!SkeletonConverter.ImplementedVersions.Contains(sourceVersion))
{
MessageBox.Show($"{version.String()} 版本尚未实现(咕咕咕~", "错误信息", MessageBoxButtons.OK, MessageBoxIcon.Information);
MessageBox.Info($"{sourceVersion.GetName()} 版本尚未实现(咕咕咕~");
return;
}
if (!SkeletonConverter.ImplementedVersions.Contains(targetVersion))
{
MessageBox.Info($"{targetVersion.GetName()} 版本尚未实现(咕咕咕~");
return;
}
if (jsonSource == jsonTarget && sourceVersion == targetVersion)
{
MessageBox.Info($"不需要转换相同的格式和版本");
return;
}
SkelPaths = listBox_FilePath.Items.Cast<string>().ToArray();
Version = version;
ConvertToJson = radioButton_BinarySource.Checked;
SourceVersion = sourceVersion;
TargetVersion = targetVersion;
JsonSource = jsonSource;
JsonTarget = jsonTarget;
DialogResult = DialogResult.OK;
}

View File

@@ -82,11 +82,11 @@ namespace SpineViewer.Dialogs
private void button_Copy_Click(object sender, EventArgs e)
{
var selectedObject = propertyGrid.SelectedObject as DiagnosticsInformation;
var selectedObject = (DiagnosticsInformation)propertyGrid.SelectedObject;
var properties = selectedObject.GetType().GetProperties();
var result = string.Join(Environment.NewLine, properties.Select(p => $"{p.Name}\t{p.GetValue(selectedObject)?.ToString()}"));
Clipboard.SetText(result);
MessageBox.Show(this, "已复制", "提示信息", MessageBoxButtons.OK, MessageBoxIcon.Information);
MessageBox.Info("已复制");
}
}
}

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

@@ -12,6 +12,8 @@ namespace SpineViewer.Dialogs
{
public partial class ExportPngDialog : Form
{
// TODO: 该对话框要合并到统一的导出参数对话框
// TODO: 使用结果包装类
public string OutputDir { get; private set; }
public float Duration { get; private set; }
public uint Fps { get; private set; }
@@ -21,6 +23,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;
@@ -35,27 +42,23 @@ namespace SpineViewer.Dialogs
var outputDir = textBox_OutputDir.Text;
if (File.Exists(outputDir))
{
MessageBox.Show("输出文件夹无效", "错误信息", MessageBoxButtons.OK, MessageBoxIcon.Information);
MessageBox.Info("输出文件夹无效");
return;
}
if (!Directory.Exists(outputDir))
{
if (MessageBox.Show($"文件夹 {outputDir} 不存在,是否创建?", "操作确认", MessageBoxButtons.OKCancel, MessageBoxIcon.Question) == DialogResult.OK)
if (MessageBox.Quest($"文件夹 {outputDir} 不存在,是否创建?") != DialogResult.OK)
return;
try
{
try
{
Directory.CreateDirectory(outputDir);
}
catch (Exception ex)
{
Program.Logger.Error(ex.ToString());
MessageBox.Show(ex.ToString(), "文件夹创建失败", MessageBoxButtons.OK, MessageBoxIcon.Error);
return;
}
Directory.CreateDirectory(outputDir);
}
else
catch (Exception ex)
{
Program.Logger.Error(ex.ToString());
MessageBox.Error(ex.ToString(), "文件夹创建失败");
return;
}
}

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,77 @@
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
{
// TODO: 用单独的结果包装类
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.Info("输出文件夹无效");
return;
}
if (!Directory.Exists(outputDir))
{
if (MessageBox.Quest($"文件夹 {outputDir} 不存在,是否创建?") != DialogResult.OK)
return;
try
{
Directory.CreateDirectory(outputDir);
}
catch (Exception ex)
{
Program.Logger.Error(ex.ToString());
MessageBox.Error(ex.ToString(), "文件夹创建失败");
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

@@ -12,17 +12,18 @@ namespace SpineViewer.Dialogs
{
public partial class OpenSpineDialog : Form
{
public string SkelPath { get; private set; }
public string? AtlasPath { get; private set; }
public Spine.Version Version { get; private set; }
/// <summary>
/// 对话框结果
/// </summary>
public OpenSpineDialogResult Result { get; private set; }
public OpenSpineDialog()
{
InitializeComponent();
comboBox_Version.DataSource = VersionHelper.Versions.ToList();
comboBox_Version.DataSource = VersionHelper.Names.ToList();
comboBox_Version.DisplayMember = "Value";
comboBox_Version.ValueMember = "Key";
comboBox_Version.SelectedValue = Spine.Version.V38;
comboBox_Version.SelectedValue = Spine.Version.Auto;
}
private void OpenSpineDialog_Load(object sender, EventArgs e)
@@ -56,7 +57,7 @@ namespace SpineViewer.Dialogs
if (!File.Exists(skelPath))
{
MessageBox.Show($"{skelPath}", "skel文件不存在", MessageBoxButtons.OK, MessageBoxIcon.Information);
MessageBox.Info($"{skelPath}", "skel文件不存在");
return;
}
else
@@ -70,7 +71,7 @@ namespace SpineViewer.Dialogs
}
else if (!File.Exists(atlasPath))
{
MessageBox.Show($"{atlasPath}", "atlas文件不存在", MessageBoxButtons.OK, MessageBoxIcon.Information);
MessageBox.Info($"{atlasPath}", "atlas文件不存在");
return;
}
else
@@ -78,16 +79,13 @@ namespace SpineViewer.Dialogs
atlasPath = Path.GetFullPath(atlasPath);
}
if (!Spine.Spine.ImplementedVersions.Contains(version))
if (version != Spine.Version.Auto && !Spine.Spine.ImplementedVersions.Contains(version))
{
MessageBox.Show($"{version.String()} 版本尚未实现(咕咕咕~", "错误信息", MessageBoxButtons.OK, MessageBoxIcon.Information);
MessageBox.Info($"{version.GetName()} 版本尚未实现(咕咕咕~");
return;
}
SkelPath = skelPath;
AtlasPath = atlasPath;
Version = version;
Result = new(version, skelPath, atlasPath);
DialogResult = DialogResult.OK;
}
@@ -96,4 +94,25 @@ namespace SpineViewer.Dialogs
DialogResult = DialogResult.Cancel;
}
}
/// <summary>
/// 打开骨骼对话框结果
/// </summary>
public class OpenSpineDialogResult(Spine.Version version, string skelPath, string? atlasPath = null)
{
/// <summary>
/// 版本
/// </summary>
public Spine.Version Version => version;
/// <summary>
/// skel 文件路径
/// </summary>
public string SkelPath => skelPath;
/// <summary>
/// atlas 文件路径
/// </summary>
public string? AtlasPath => atlasPath;
}
}

View File

@@ -12,15 +12,25 @@ namespace SpineViewer.Dialogs
{
public partial class ProgressDialog : Form
{
/// <summary>
/// BackgroundWorker.DoWork 接口暴露
/// </summary>
[Category("自定义"), Description("BackgroundWorker 的 DoWork 事件")]
public event DoWorkEventHandler? DoWork
{
add { backgroundWorker.DoWork += value; }
remove { backgroundWorker.DoWork -= value; }
add => backgroundWorker.DoWork += value;
remove => backgroundWorker.DoWork -= value;
}
public void RunWorkerAsync() { backgroundWorker.RunWorkerAsync(); }
public void RunWorkerAsync(object? argument) { backgroundWorker.RunWorkerAsync(argument); }
/// <summary>
/// 启动后台执行
/// </summary>
public void RunWorkerAsync() => backgroundWorker.RunWorkerAsync();
/// <summary>
/// 使用给定参数启动后台执行
/// </summary>
public void RunWorkerAsync(object? argument) => backgroundWorker.RunWorkerAsync(argument);
public ProgressDialog()
{
@@ -38,7 +48,7 @@ namespace SpineViewer.Dialogs
if (e.Error != null)
{
Program.Logger.Error(e.Error.ToString());
MessageBox.Show(e.Error.ToString(), "执行出错", MessageBoxButtons.OK, MessageBoxIcon.Error);
MessageBox.Error(e.Error.ToString(), "执行出错");
DialogResult = DialogResult.Abort;
}
else if (e.Cancelled)

View File

@@ -36,6 +36,7 @@
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();
@@ -94,13 +95,13 @@
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);
menuStrip.Size = new Size(1748, 32);
menuStrip.TabIndex = 0;
menuStrip.Text = "菜单";
//
// 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)";
@@ -133,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";
@@ -150,8 +158,8 @@
//
toolStripMenuItem_Function.DropDownItems.AddRange(new ToolStripItem[] { toolStripMenuItem_ResetAnimation });
toolStripMenuItem_Function.Name = "toolStripMenuItem_Function";
toolStripMenuItem_Function.Size = new Size(84, 28);
toolStripMenuItem_Function.Text = "功能(&F)";
toolStripMenuItem_Function.Size = new Size(87, 28);
toolStripMenuItem_Function.Text = "功能(&G)";
//
// toolStripMenuItem_ResetAnimation
//
@@ -170,7 +178,7 @@
// toolStripMenuItem_ConvertFileFormat
//
toolStripMenuItem_ConvertFileFormat.Name = "toolStripMenuItem_ConvertFileFormat";
toolStripMenuItem_ConvertFileFormat.Size = new Size(270, 34);
toolStripMenuItem_ConvertFileFormat.Size = new Size(254, 34);
toolStripMenuItem_ConvertFileFormat.Text = "转换文件格式(&C)...";
toolStripMenuItem_ConvertFileFormat.Click += toolStripMenuItem_ConvertFileFormat_Click;
//
@@ -186,6 +194,7 @@
toolStripMenuItem_ManageResource.Name = "toolStripMenuItem_ManageResource";
toolStripMenuItem_ManageResource.Size = new Size(260, 34);
toolStripMenuItem_ManageResource.Text = "管理下载资源(&M)...";
toolStripMenuItem_ManageResource.Click += toolStripMenuItem_ManageResource_Click;
//
// toolStripMenuItem_Help
//
@@ -223,7 +232,7 @@
rtbLog.Margin = new Padding(3, 2, 3, 2);
rtbLog.Name = "rtbLog";
rtbLog.ReadOnly = true;
rtbLog.Size = new Size(1721, 106);
rtbLog.Size = new Size(1728, 114);
rtbLog.TabIndex = 0;
rtbLog.Text = "";
rtbLog.WordWrap = false;
@@ -245,8 +254,8 @@
//
splitContainer_MainForm.Panel2.Controls.Add(rtbLog);
splitContainer_MainForm.Panel2.Cursor = Cursors.Default;
splitContainer_MainForm.Size = new Size(1721, 958);
splitContainer_MainForm.SplitterDistance = 848;
splitContainer_MainForm.Size = new Size(1728, 997);
splitContainer_MainForm.SplitterDistance = 879;
splitContainer_MainForm.TabIndex = 3;
splitContainer_MainForm.TabStop = false;
splitContainer_MainForm.SplitterMoved += splitContainer_SplitterMoved;
@@ -268,8 +277,8 @@
//
splitContainer_Functional.Panel2.Controls.Add(groupBox_Preview);
splitContainer_Functional.Panel2.Cursor = Cursors.Default;
splitContainer_Functional.Size = new Size(1721, 848);
splitContainer_Functional.SplitterDistance = 744;
splitContainer_Functional.Size = new Size(1728, 879);
splitContainer_Functional.SplitterDistance = 747;
splitContainer_Functional.TabIndex = 2;
splitContainer_Functional.TabStop = false;
splitContainer_Functional.SplitterMoved += splitContainer_SplitterMoved;
@@ -291,8 +300,8 @@
//
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.Size = new Size(747, 879);
splitContainer_Information.SplitterDistance = 399;
splitContainer_Information.TabIndex = 1;
splitContainer_Information.TabStop = false;
splitContainer_Information.SplitterMoved += splitContainer_SplitterMoved;
@@ -304,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(399, 879);
groupBox_SkelList.TabIndex = 0;
groupBox_SkelList.TabStop = false;
groupBox_SkelList.Text = "模型列表";
@@ -315,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(393, 850);
spineListView.TabIndex = 0;
//
// propertyGrid_Spine
@@ -324,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(338, 485);
propertyGrid_Spine.TabIndex = 0;
propertyGrid_Spine.ToolbarVisible = false;
propertyGrid_Spine.PropertyValueChanged += propertyGrid_PropertyValueChanged;
@@ -346,8 +355,8 @@
//
splitContainer_Config.Panel2.Controls.Add(groupBox_PreviewConfig);
splitContainer_Config.Panel2.Cursor = Cursors.Default;
splitContainer_Config.Size = new Size(413, 848);
splitContainer_Config.SplitterDistance = 499;
splitContainer_Config.Size = new Size(344, 879);
splitContainer_Config.SplitterDistance = 514;
splitContainer_Config.TabIndex = 0;
splitContainer_Config.TabStop = false;
splitContainer_Config.SplitterMoved += splitContainer_SplitterMoved;
@@ -359,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(344, 514);
groupBox_SkelConfig.TabIndex = 0;
groupBox_SkelConfig.TabStop = false;
groupBox_SkelConfig.Text = "模型参数";
@@ -370,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(344, 361);
groupBox_PreviewConfig.TabIndex = 1;
groupBox_PreviewConfig.TabStop = false;
groupBox_PreviewConfig.Text = "画面参数";
@@ -381,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(338, 332);
propertyGrid_Previewer.TabIndex = 1;
propertyGrid_Previewer.ToolbarVisible = false;
propertyGrid_Previewer.PropertyValueChanged += propertyGrid_PropertyValueChanged;
@@ -392,7 +401,7 @@
groupBox_Preview.Dock = DockStyle.Fill;
groupBox_Preview.Location = new Point(0, 0);
groupBox_Preview.Name = "groupBox_Preview";
groupBox_Preview.Size = new Size(973, 848);
groupBox_Preview.Size = new Size(977, 879);
groupBox_Preview.TabIndex = 1;
groupBox_Preview.TabStop = false;
groupBox_Preview.Text = "预览画面";
@@ -404,7 +413,7 @@
spinePreviewer.Location = new Point(3, 26);
spinePreviewer.Name = "spinePreviewer";
spinePreviewer.PropertyGrid = propertyGrid_Previewer;
spinePreviewer.Size = new Size(967, 819);
spinePreviewer.Size = new Size(971, 850);
spinePreviewer.SpineListView = spineListView;
spinePreviewer.TabIndex = 0;
spinePreviewer.MouseUp += spinePreviewer_MouseUp;
@@ -416,7 +425,7 @@
panel_MainForm.Location = new Point(0, 32);
panel_MainForm.Name = "panel_MainForm";
panel_MainForm.Padding = new Padding(10, 5, 10, 10);
panel_MainForm.Size = new Size(1741, 973);
panel_MainForm.Size = new Size(1748, 1012);
panel_MainForm.TabIndex = 4;
//
// toolTip
@@ -427,7 +436,7 @@
//
AutoScaleDimensions = new SizeF(11F, 24F);
AutoScaleMode = AutoScaleMode.Font;
ClientSize = new Size(1741, 1005);
ClientSize = new Size(1748, 1044);
Controls.Add(panel_MainForm);
Controls.Add(menuStrip);
Icon = (Icon)resources.GetObject("$this.Icon");
@@ -500,5 +509,6 @@
private ToolStripMenuItem toolStripMenuItem_ManageResource;
private ToolStripMenuItem toolStripMenuItem_Tool;
private ToolStripMenuItem toolStripMenuItem_ConvertFileFormat;
private ToolStripMenuItem toolStripMenuItem_ExportPreview;
}
}

View File

@@ -1,7 +1,13 @@
using NLog;
using FFMpegCore.Pipes;
using FFMpegCore;
using NLog;
using SFML.System;
using SpineViewer.Spine;
using System.ComponentModel;
using System.Diagnostics;
using System.Text.Json;
using System.Text.Json.Nodes;
using FFMpegCore.Enums;
namespace SpineViewer
{
@@ -41,6 +47,182 @@ namespace SpineViewer
LogManager.ReconfigExistingLoggers();
}
private void MainForm_Load(object sender, EventArgs e)
{
spinePreviewer.StartPreview();
}
private void MainForm_FormClosing(object sender, FormClosingEventArgs e)
{
spinePreviewer.StopPreview();
}
private void toolStripMenuItem_Open_Click(object sender, EventArgs e)
{
spineListView.Add();
}
private void toolStripMenuItem_BatchOpen_Click(object sender, EventArgs e)
{
spineListView.BatchAdd();
}
private void toolStripMenuItem_Export_Click(object sender, EventArgs e)
{
// TODO: 改成统一导出调用
lock (spineListView.Spines)
{
if (spineListView.Spines.Count <= 0)
{
MessageBox.Info("请至少打开一个骨骼文件");
return;
}
}
var exportDialog = new Dialogs.ExportPngDialog();
if (exportDialog.ShowDialog() != DialogResult.OK)
return;
var progressDialog = new Dialogs.ProgressDialog();
progressDialog.DoWork += ExportPng_Work;
progressDialog.RunWorkerAsync(exportDialog);
progressDialog.ShowDialog();
}
private void toolStripMenuItem_ExportPreview_Click(object sender, EventArgs e)
{
lock (spineListView.Spines)
{
if (spineListView.Spines.Count <= 0)
{
MessageBox.Info("请至少打开一个骨骼文件");
return;
}
}
var saveDialog = new Dialogs.ExportPreviewDialog();
if (saveDialog.ShowDialog() != DialogResult.OK)
return;
var progressDialog = new Dialogs.ProgressDialog();
progressDialog.DoWork += ExportPreview_Work;
progressDialog.RunWorkerAsync(saveDialog);
progressDialog.ShowDialog();
}
private void toolStripMenuItem_Exit_Click(object sender, EventArgs e)
{
Close();
}
private void toolStripMenuItem_ResetAnimation_Click(object sender, EventArgs e)
{
lock (spineListView.Spines)
{
foreach (var spine in spineListView.Spines)
spine.CurrentAnimation = spine.CurrentAnimation;
}
}
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();
}
//IEnumerable<IVideoFrame> testExport(int fps)
//{
// var duration = 2f;
// var resolution = spinePreviewer.Resolution;
// var delta = 1f / fps;
// var frameCount = 1 + (int)(duration / delta); // 零帧开始导出
// var spinesReverse = spineListView.Spines.Reverse();
// // 重置动画时间
// foreach (var spine in spinesReverse)
// spine.CurrentAnimation = spine.CurrentAnimation;
// // 逐帧导出
// var success = 0;
// for (int frameIndex = 0; frameIndex < frameCount; frameIndex++)
// {
// using var tex = new SFML.Graphics.RenderTexture((uint)resolution.Width, (uint)resolution.Height);
// tex.SetView(spinePreviewer.View);
// tex.Clear(SFML.Graphics.Color.Transparent);
// foreach (var spine in spinesReverse)
// {
// tex.Draw(spine);
// spine.Update(delta);
// }
// tex.Display();
// Debug.WriteLine($"ThreadID: {Environment.CurrentManagedThreadId}");
// var frame = tex.Texture.CopyToFrame();
// tex.Dispose();
// yield return frame;
// success++;
// }
// Program.Logger.Info("Exporting done: {}/{}", success, frameCount);
//}
private void toolStripMenuItem_ManageResource_Click(object sender, EventArgs e)
{
//spinePreviewer.StopPreview();
//lock (spineListView.Spines)
//{
// //var fps = 24;
// ////foreach (var i in testExport(fps))
// //// _ = i;
// ////var t = testExport(fps).ToArray();
// ////var a = testExport(fps).GetEnumerator();
// ////while (a.MoveNext());
// //var videoFramesSource = new RawVideoPipeSource(testExport(fps)) { FrameRate = fps };
// //var outputPath = @"C:\Users\ljh\Desktop\test\a.mov";
// //var task = FFMpegArguments
// // .FromPipeInput(videoFramesSource)
// // .OutputToFile(outputPath, true
// // , options => options
// // //.WithCustomArgument("-vf \"split[s0][s1];[s0]palettegen=reserve_transparent=1[p];[s1][p]paletteuse=alpha_threshold=128\""))
// // .WithCustomArgument("-c:v prores_ks -profile:v 4444 -pix_fmt yuva444p10le"))
// // .ProcessAsynchronously();
// //task.Wait();
//}
//spinePreviewer.StartPreview();
}
private void toolStripMenuItem_About_Click(object sender, EventArgs e)
{
(new Dialogs.AboutDialog()).ShowDialog();
}
private void toolStripMenuItem_Diagnostics_Click(object sender, EventArgs e)
{
(new Dialogs.DiagnosticsDialog()).ShowDialog();
}
private void splitContainer_SplitterMoved(object sender, SplitterEventArgs e) => ActiveControl = null;
private void splitContainer_MouseUp(object sender, MouseEventArgs e) => ActiveControl = null;
private void propertyGrid_PropertyValueChanged(object sender, PropertyValueChangedEventArgs e) => (sender as PropertyGrid)?.Refresh();
private void spinePreviewer_MouseUp(object sender, MouseEventArgs e)
{
propertyGrid_Spine.Refresh();
}
private void ExportPng_Work(object? sender, DoWorkEventArgs e)
{
var worker = sender as BackgroundWorker;
@@ -50,9 +232,12 @@ namespace SpineViewer
var fps = arguments.Fps;
var timestamp = DateTime.Now.ToString("yyMMddHHmmss");
var resolution = spinePreviewer.Resolution;
var frameArgs = spinePreviewer.GetFrameArgs();
var renderSelectedOnly = spinePreviewer.RenderSelectedOnly;
var resolution = frameArgs.Resolution;
var tex = new SFML.Graphics.RenderTexture((uint)resolution.Width, (uint)resolution.Height);
tex.SetView(spinePreviewer.View);
tex.SetView(frameArgs.View);
var delta = 1f / fps;
var frameCount = 1 + (int)(duration / delta); // 零帧开始导出
@@ -83,6 +268,9 @@ namespace SpineViewer
foreach (var spine in spinesReverse)
{
if (renderSelectedOnly && !spine.IsSelected)
continue;
tex.Draw(spine);
spine.Update(delta);
}
@@ -103,71 +291,75 @@ namespace SpineViewer
spinePreviewer.StartPreview();
}
private void MainForm_Load(object sender, EventArgs e)
private void ExportPreview_Work(object? sender, DoWorkEventArgs e)
{
spinePreviewer.StartPreview();
}
var worker = sender as BackgroundWorker;
var arguments = e.Argument as Dialogs.ExportPreviewDialog;
var outputDir = arguments.OutputDir;
var width = arguments.PreviewWidth;
var height = arguments.PreviewHeight;
// TODO: 增加填充参数
var paddingL = 1u;
var paddingR = 1u;
var paddingT = 1u;
var paddingB = 1u;
private void MainForm_FormClosing(object sender, FormClosingEventArgs e)
{
var tex = new SFML.Graphics.RenderTexture(width, height);
int success = 0;
int error = 0;
spinePreviewer.StopPreview();
}
private void toolStripMenuItem_Open_Click(object sender, EventArgs e)
{
spineListView.Add();
}
private void toolStripMenuItem_BatchOpen_Click(object sender, EventArgs e)
{
spineListView.BatchAdd();
}
private void toolStripMenuItem_Export_Click(object sender, EventArgs e)
{
lock (spineListView.Spines)
{
if (spineListView.Spines.Count <= 0)
var spines = spineListView.Spines;
int totalCount = spines.Count;
worker.ReportProgress(0, $"已处理 0/{totalCount}");
for (int i = 0; i < totalCount; i++)
{
MessageBox.Show("请至少打开一个骨骼文件", "提示信息", MessageBoxButtons.OK, MessageBoxIcon.Information);
return;
if (worker.CancellationPending)
{
e.Cancel = true;
break;
}
var spine = spines[i];
var tmp = spine.CurrentAnimation;
spine.CurrentAnimation = Spine.Spine.EMPTY_ANIMATION;
tex.SetView(spine.GetInitView(width, height, paddingL, paddingR, paddingT, paddingB));
tex.Clear(SFML.Graphics.Color.Transparent);
tex.Draw(spine);
tex.Display();
spine.CurrentAnimation = tmp;
try
{
using (var img = tex.Texture.CopyToImage())
{
img.SaveToFile(Path.Combine(outputDir, $"{spine.Name}.png"));
}
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}");
}
}
spinePreviewer.StartPreview();
var exportDialog = new Dialogs.ExportPngDialog();
if (exportDialog.ShowDialog() != DialogResult.OK)
return;
var progressDialog = new Dialogs.ProgressDialog();
progressDialog.DoWork += ExportPng_Work;
progressDialog.RunWorkerAsync(exportDialog);
progressDialog.ShowDialog();
}
private void toolStripMenuItem_Exit_Click(object sender, EventArgs e)
{
Close();
}
private void toolStripMenuItem_ResetAnimation_Click(object sender, EventArgs e)
{
lock (spineListView.Spines)
if (error > 0)
{
foreach (var spine in spineListView.Spines)
spine.CurrentAnimation = spine.CurrentAnimation;
Program.Logger.Warn("Preview save {} successfully, {} failed", success, error);
}
else
{
Program.Logger.Info("{} preview saved successfully", success);
}
}
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();
Program.LogCurrentMemoryUsage();
}
private void ConvertFileFormat_Work(object? sender, DoWorkEventArgs e)
@@ -175,15 +367,26 @@ namespace SpineViewer
var worker = sender as BackgroundWorker;
var arguments = e.Argument as Dialogs.ConvertFileFormatDialog;
var skelPaths = arguments.SkelPaths;
var version = arguments.Version;
var convertToJson = arguments.ConvertToJson;
var newSuffix = convertToJson ? ".json" : ".skel";
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 cvter = SkeletonConverter.New(version);
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++)
@@ -199,10 +402,9 @@ namespace SpineViewer
try
{
if (convertToJson)
cvter.BinaryToJson(skelPath, newPath);
else
cvter.JsonToBinary(skelPath, newPath);
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)
@@ -224,24 +426,5 @@ namespace SpineViewer
Program.Logger.Info("{} skel converted successfully", success);
}
}
private void toolStripMenuItem_About_Click(object sender, EventArgs e)
{
(new Dialogs.AboutDialog()).ShowDialog();
}
private void toolStripMenuItem_Diagnostics_Click(object sender, EventArgs e)
{
(new Dialogs.DiagnosticsDialog()).ShowDialog();
}
private void splitContainer_SplitterMoved(object sender, SplitterEventArgs e) { ActiveControl = null; }
private void splitContainer_MouseUp(object sender, MouseEventArgs e) { ActiveControl = null; }
private void propertyGrid_PropertyValueChanged(object sender, PropertyValueChangedEventArgs e) { (sender as PropertyGrid)?.Refresh(); }
private void spinePreviewer_MouseUp(object sender, MouseEventArgs e) { propertyGrid_Spine.Refresh(); }
}
}

39
SpineViewer/MessageBox.cs Normal file
View File

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

View File

@@ -5,11 +5,38 @@ namespace SpineViewer
{
internal static class Program
{
/// <summary>
/// 程序路径
/// </summary>
public static readonly string FilePath = Environment.ProcessPath;
/// <summary>
/// 程序名
/// </summary>
public static readonly string Name = Path.GetFileNameWithoutExtension(FilePath);
/// <summary>
/// 程序目录
/// </summary>
public static readonly string RootDir = Path.GetDirectoryName(FilePath);
/// <summary>
/// 程序临时目录
/// </summary>
public static readonly string TempDir = Directory.CreateDirectory(Path.Combine(Path.GetTempPath(), Name)).FullName;
/// <summary>
/// 程序进程
/// </summary>
public static readonly Process Process = Process.GetCurrentProcess();
/// <summary>
/// 程序日志器
/// </summary>
public static readonly Logger Logger = LogManager.GetCurrentClassLogger();
/// <summary>
/// The main entry point for the application.
/// 应用入口点
/// </summary>
[STAThread]
static void Main()
@@ -28,7 +55,7 @@ namespace SpineViewer
catch (Exception ex)
{
Logger.Fatal(ex.ToString());
MessageBox.Show(ex.ToString(), "程序已崩溃", MessageBoxButtons.OK, MessageBoxIcon.Stop);
MessageBox.Error(ex.ToString(), "程序已崩溃");
}
}
@@ -56,5 +83,9 @@ namespace SpineViewer
LogManager.Configuration = config;
}
/// <summary>
/// 输出当前内存使用情况
/// </summary>
public static void LogCurrentMemoryUsage() => Logger.Info("Current memory usage: {:F2} MB", Process.WorkingSet64 / 1024.0 / 1024.0);
}
}

View File

@@ -7,47 +7,55 @@ using SpineRuntime38;
using System.Text.Json;
using System.Text.Json.Nodes;
using SpineRuntime38.Attachments;
using System.Globalization;
using static System.Runtime.InteropServices.JavaScript.JSType;
using System.IO;
namespace SpineViewer.Spine.Implementations.SkeletonConverter
{
[SkeletonConverterImplementation(Version.V38)]
class SkeletonConverter38 : SpineViewer.Spine.SkeletonConverter
{
private SkeletonReader reader = null;
private BinaryReader reader = null;
private JsonObject root = null;
private bool nonessential = false;
private List<JsonObject> idx2Event = [];
protected override JsonObject ReadBinary(string binPath)
private readonly List<JsonObject> idx2event = [];
public override JsonObject ReadBinary(string binPath)
{
var root = new JsonObject();
using var input = File.OpenRead(binPath);
this.root = root;
reader = new(input);
var result = root = [];
root["skeleton"] = ReadSkeleton();
ReadSkeleton();
ReadStrings();
root["bones"] = ReadBones();
root["slots"] = ReadSlots();
root["ik"] = ReadIK();
root["transform"] = ReadTransform();
root["path"] = ReadPath();
root["skins"] = ReadSkins();
root["events"] = ReadEvents();
root["animations"] = ReadAnimations();
ReadBones();
ReadSlots();
ReadIK();
ReadTransform();
ReadPath();
ReadSkins();
ReadEvents();
ReadAnimations();
reader = null;
nonessential = false;
root = null;
this.root = null;
return result;
idx2event.Clear();
return root;
}
private JsonObject ReadSkeleton()
private void ReadSkeleton()
{
JsonObject skeleton = [];
skeleton["hash"] = reader.ReadString();
skeleton["spine"] = reader.ReadString();
var version = reader.ReadString();
if (version == "3.8.75") version = "3.8.76"; // replace 3.8.75 to another version to avoid detection in official runtime
skeleton["spine"] = version;
skeleton["x"] = reader.ReadFloat();
skeleton["y"] = reader.ReadFloat();
skeleton["width"] = reader.ReadFloat();
@@ -59,7 +67,7 @@ namespace SpineViewer.Spine.Implementations.SkeletonConverter
skeleton["images"] = reader.ReadString();
skeleton["audio"] = reader.ReadString();
}
return skeleton;
root["skeleton"] = skeleton;
}
private void ReadStrings()
@@ -68,14 +76,14 @@ namespace SpineViewer.Spine.Implementations.SkeletonConverter
reader.StringTable.Add(reader.ReadString());
}
private JsonArray ReadBones()
private void ReadBones()
{
JsonArray bones = [];
for (int i = 0, n = reader.ReadVarInt(); i < n; i++)
{
JsonObject data = [];
data["name"] = reader.ReadString();
if (i > 0) data["parent"] = bones[reader.ReadVarInt()]["name"].GetValue<string>();
if (i > 0) data["parent"] = (string)bones[reader.ReadVarInt()]["name"];
data["rotation"] = reader.ReadFloat();
data["x"] = reader.ReadFloat();
data["y"] = reader.ReadFloat();
@@ -89,10 +97,10 @@ namespace SpineViewer.Spine.Implementations.SkeletonConverter
if (nonessential) reader.ReadInt();
bones.Add(data);
}
return bones;
root["bones"] = bones;
}
private JsonArray ReadSlots()
private void ReadSlots()
{
JsonArray bones = root["bones"].AsArray();
JsonArray slots = [];
@@ -100,7 +108,7 @@ namespace SpineViewer.Spine.Implementations.SkeletonConverter
{
JsonObject data = [];
data["name"] = reader.ReadString();
data["bone"] = bones[reader.ReadVarInt()]["name"].GetValue<string>();
data["bone"] = (string)bones[reader.ReadVarInt()]["name"];
data["color"] = reader.ReadInt().ToString("x8"); // 0xrrggbbaa -> rrggbbaa
int dark = reader.ReadInt();
if (dark != -1) data["dark"] = dark.ToString("x6"); // 0x00rrggbb -> rrggbb
@@ -108,10 +116,10 @@ namespace SpineViewer.Spine.Implementations.SkeletonConverter
data["blend"] = ((BlendMode)reader.ReadVarInt()).ToString();
slots.Add(data);
}
return slots;
root["slots"] = slots;
}
private JsonArray ReadIK()
private void ReadIK()
{
JsonArray bones = root["bones"].AsArray();
JsonArray ik = [];
@@ -122,7 +130,7 @@ namespace SpineViewer.Spine.Implementations.SkeletonConverter
data["order"] = reader.ReadVarInt();
data["skin"] = reader.ReadBoolean();
data["bones"] = ReadNames(bones);
data["target"] = bones[reader.ReadVarInt()]["name"].GetValue<string>();
data["target"] = (string)bones[reader.ReadVarInt()]["name"];
data["mix"] = reader.ReadFloat();
data["softness"] = reader.ReadFloat();
data["bendPositive"] = reader.ReadSByte() > 0;
@@ -131,10 +139,10 @@ namespace SpineViewer.Spine.Implementations.SkeletonConverter
data["uniform"] = reader.ReadBoolean();
ik.Add(data);
}
return ik;
root["ik"] = ik;
}
private JsonArray ReadTransform()
private void ReadTransform()
{
JsonArray bones = root["bones"].AsArray();
JsonArray transform = [];
@@ -145,7 +153,7 @@ namespace SpineViewer.Spine.Implementations.SkeletonConverter
data["order"] = reader.ReadVarInt();
data["skin"] = reader.ReadBoolean();
data["bones"] = ReadNames(bones);
data["target"] = bones[reader.ReadVarInt()]["name"].GetValue<string>();
data["target"] = (string)bones[reader.ReadVarInt()]["name"];
data["local"] = reader.ReadBoolean();
data["relative"] = reader.ReadBoolean();
data["rotation"] = reader.ReadFloat();
@@ -160,10 +168,10 @@ namespace SpineViewer.Spine.Implementations.SkeletonConverter
data["shearMix"] = reader.ReadFloat();
transform.Add(data);
}
return transform;
root["transform"] = transform;
}
private JsonArray ReadPath()
private void ReadPath()
{
JsonArray bones = root["bones"].AsArray();
JsonArray path = [];
@@ -174,7 +182,7 @@ namespace SpineViewer.Spine.Implementations.SkeletonConverter
data["order"] = reader.ReadVarInt();
data["skin"] = reader.ReadBoolean();
data["bones"] = ReadNames(bones);
data["target"] = bones[reader.ReadVarInt()]["name"].GetValue<string>();
data["target"] = (string)bones[reader.ReadVarInt()]["name"];
data["positionMode"] = ((PositionMode)reader.ReadVarInt()).ToString();
data["spacingMode"] = ((SpacingMode)reader.ReadVarInt()).ToString();
data["rotateMode"] = ((RotateMode)reader.ReadVarInt()).ToString();
@@ -185,10 +193,10 @@ namespace SpineViewer.Spine.Implementations.SkeletonConverter
data["translateMix"] = reader.ReadFloat();
path.Add(data);
}
return path;
root["path"] = path;
}
private JsonArray ReadSkins()
private void ReadSkins()
{
JsonArray skins = [];
@@ -200,7 +208,7 @@ namespace SpineViewer.Spine.Implementations.SkeletonConverter
for (int n = reader.ReadVarInt(); n > 0; n--)
skins.Add(ReadSkin());
return skins;
root["skins"] = skins;
}
private JsonObject? ReadSkin(bool isDefault = false)
@@ -219,8 +227,8 @@ namespace SpineViewer.Spine.Implementations.SkeletonConverter
skin["name"] = reader.ReadStringRef();
skin["bones"] = ReadNames(root["bones"].AsArray());
skin["ik"] = ReadNames(root["ik"].AsArray());
skin["transform"] = ReadNames(root["transform"].AsArray()); ;
skin["path"] = ReadNames(root["path"].AsArray()); ;
skin["transform"] = ReadNames(root["transform"].AsArray());
skin["path"] = ReadNames(root["path"].AsArray());
slotCount = reader.ReadVarInt();
}
@@ -229,7 +237,7 @@ namespace SpineViewer.Spine.Implementations.SkeletonConverter
while (slotCount-- > 0)
{
JsonObject slotAttachments = [];
skinAttachments[slots[reader.ReadVarInt()]["name"].GetValue<string>()] = slotAttachments;
skinAttachments[(string)slots[reader.ReadVarInt()]["name"]] = slotAttachments;
for (int attachmentCount = reader.ReadVarInt(); attachmentCount > 0; attachmentCount--)
{
var attachmentKey = reader.ReadStringRef();
@@ -321,21 +329,21 @@ namespace SpineViewer.Spine.Implementations.SkeletonConverter
if (nonessential) reader.ReadInt();
break;
case AttachmentType.Clipping:
attachment["end"] = slots[reader.ReadVarInt()]["name"].GetValue<string>();
attachment["end"] = (string)slots[reader.ReadVarInt()]["name"];
vertexCount = reader.ReadVarInt();
attachment["vertexCount"] = vertexCount;
attachment["vertices"] = ReadVertices(vertexCount);
if (nonessential) reader.ReadInt();
break;
default:
throw new ArgumentException($"Invalid attachment type: {type}");
throw new ArgumentOutOfRangeException($"Invalid attachment type: {type}");
}
return attachment;
}
private JsonObject ReadEvents()
private void ReadEvents()
{
idx2Event.Clear();
idx2event.Clear();
JsonObject events = [];
for (int n = reader.ReadVarInt(); n > 0; n--)
{
@@ -352,12 +360,12 @@ namespace SpineViewer.Spine.Implementations.SkeletonConverter
data["volume"] = reader.ReadFloat();
data["balance"] = reader.ReadFloat();
}
idx2Event.Add(data);
idx2event.Add(data);
}
return events;
root["events"] = events;
}
private JsonObject ReadAnimations()
private void ReadAnimations()
{
JsonObject animations = [];
for (int n = reader.ReadVarInt(); n > 0; n--)
@@ -373,7 +381,7 @@ namespace SpineViewer.Spine.Implementations.SkeletonConverter
if (ReadDrawOrderTimelines() is JsonArray draworder) data["drawOrder"] = draworder;
if (ReadEventTimelines() is JsonArray events) data["events"] = events;
}
return animations;
root["animations"] = animations;
}
private JsonObject? ReadSlotTimelines()
@@ -384,7 +392,7 @@ namespace SpineViewer.Spine.Implementations.SkeletonConverter
for (int slotCount = reader.ReadVarInt(); slotCount > 0; slotCount--)
{
JsonObject timeline = [];
slotTimelines[slots[reader.ReadVarInt()]["name"].GetValue<string>()] = timeline;
slotTimelines[(string)slots[reader.ReadVarInt()]["name"]] = timeline;
for (int timelineCount = reader.ReadVarInt(); timelineCount > 0; timelineCount--)
{
JsonArray frames = [];
@@ -431,7 +439,7 @@ namespace SpineViewer.Spine.Implementations.SkeletonConverter
}
break;
default:
throw new ArgumentException($"Invalid slot timeline type: {type}");
throw new ArgumentOutOfRangeException($"Invalid slot timeline type: {type}");
}
}
}
@@ -447,7 +455,7 @@ namespace SpineViewer.Spine.Implementations.SkeletonConverter
for (int boneCount = reader.ReadVarInt(); boneCount > 0; boneCount--)
{
JsonObject timeline = [];
boneTimelines[bones[reader.ReadVarInt()]["name"].GetValue<string>()] = timeline;
boneTimelines[(string)bones[reader.ReadVarInt()]["name"]] = timeline;
for (int timelineCount = reader.ReadVarInt(); timelineCount > 0; timelineCount--)
{
JsonArray frames = [];
@@ -511,7 +519,7 @@ namespace SpineViewer.Spine.Implementations.SkeletonConverter
}
break;
default:
throw new ArgumentException($"Invalid bone timeline type: {type}");
throw new ArgumentOutOfRangeException($"Invalid bone timeline type: {type}");
}
}
}
@@ -527,7 +535,7 @@ namespace SpineViewer.Spine.Implementations.SkeletonConverter
for (int ikCount = reader.ReadVarInt(); ikCount > 0; ikCount--)
{
JsonArray frames = [];
ikTimelines[ik[reader.ReadVarInt()]["name"].GetValue<string>()] = frames;
ikTimelines[(string)ik[reader.ReadVarInt()]["name"]] = frames;
for (int frameCount = reader.ReadVarInt(); frameCount > 0; frameCount--)
{
var o = new JsonObject()
@@ -555,7 +563,7 @@ namespace SpineViewer.Spine.Implementations.SkeletonConverter
for (int transformCount = reader.ReadVarInt(); transformCount > 0; transformCount--)
{
JsonArray frames = [];
transformTimelines[transform[reader.ReadVarInt()]["name"].GetValue<string>()] = frames;
transformTimelines[(string)transform[reader.ReadVarInt()]["name"]] = frames;
for (int frameCount = reader.ReadVarInt(); frameCount > 0; frameCount--)
{
var o = new JsonObject()
@@ -582,7 +590,7 @@ namespace SpineViewer.Spine.Implementations.SkeletonConverter
for (int pathCount = reader.ReadVarInt(); pathCount > 0; pathCount--)
{
JsonObject timeline = [];
pathTimelines[path[reader.ReadVarInt()]["name"].GetValue<string>()] = timeline;
pathTimelines[(string)path[reader.ReadVarInt()]["name"]] = timeline;
for (int timelineCount = reader.ReadVarInt(); timelineCount > 0; timelineCount--)
{
JsonArray frames = [];
@@ -629,7 +637,7 @@ namespace SpineViewer.Spine.Implementations.SkeletonConverter
}
break;
default:
throw new ArgumentException($"Invalid path timeline type: {type}");
throw new ArgumentOutOfRangeException($"Invalid path timeline type: {type}");
}
}
}
@@ -643,18 +651,15 @@ namespace SpineViewer.Spine.Implementations.SkeletonConverter
JsonArray skins = root["skins"].AsArray();
JsonObject deformTimelines = [];
//for (int skinCount = reader.ReadVarInt(); skinCount > 0; skinCount--)
for (int i = 0, n = reader.ReadVarInt(); i < n; i++)
for (int skinCount = reader.ReadVarInt(); skinCount > 0; skinCount--)
{
JsonObject skinValue = [];
deformTimelines[skins[reader.ReadVarInt()]["name"].GetValue<string>()] = skinValue;
//for (int slotCount = reader.ReadVarInt(); slotCount > 0; slotCount--)
for (int ii = 0, nn = reader.ReadVarInt(); ii < nn; ii++)
deformTimelines[(string)skins[reader.ReadVarInt()]["name"]] = skinValue;
for (int slotCount = reader.ReadVarInt(); slotCount > 0; slotCount--)
{
JsonObject slotValue = [];
skinValue[slots[reader.ReadVarInt()]["name"].GetValue<string>()] = slotValue;
//for (int attachmentCount = reader.ReadVarInt(); attachmentCount > 0; attachmentCount--)
for (int iii = 0, nnn = reader.ReadVarInt(); iii < nnn; iii++)
skinValue[(string)slots[reader.ReadVarInt()]["name"]] = slotValue;
for (int attachmentCount = reader.ReadVarInt(); attachmentCount > 0; attachmentCount--)
{
JsonArray frames = [];
slotValue[reader.ReadStringRef()] = frames;
@@ -698,7 +703,7 @@ namespace SpineViewer.Spine.Implementations.SkeletonConverter
{
offsets.Add(new JsonObject()
{
["slot"] = slots[reader.ReadVarInt()]["name"].GetValue<string>(),
["slot"] = (string)slots[reader.ReadVarInt()]["name"],
["offset"] = reader.ReadVarInt(),
});
}
@@ -715,15 +720,15 @@ namespace SpineViewer.Spine.Implementations.SkeletonConverter
{
JsonObject data = [];
data["time"] = reader.ReadFloat();
JsonObject eventData = idx2Event[reader.ReadVarInt()].AsObject();
data["name"] = eventData["name"].GetValue<string>();
JsonObject eventData = idx2event[reader.ReadVarInt()].AsObject();
data["name"] = (string)eventData["name"];
data["int"] = reader.ReadVarInt();
data["float"] = reader.ReadFloat();
data["string"] = reader.ReadBoolean() ? reader.ReadString() : eventData["string"].GetValue<string>();
data["string"] = reader.ReadBoolean() ? reader.ReadString() : (string)eventData["string"];
if (eventData.ContainsKey("audio"))
{
data["volume"] = eventData["volume"].GetValue<string>();
data["balance"] = eventData["balance"].GetValue<string>();
data["volume"] = (string)eventData["volume"];
data["balance"] = (string)eventData["balance"];
}
eventTimelines.Add(data);
}
@@ -735,7 +740,7 @@ namespace SpineViewer.Spine.Implementations.SkeletonConverter
{
JsonArray names = [];
for (int n = reader.ReadVarInt(); n > 0; n--)
names.Add(array[reader.ReadVarInt()]["name"].GetValue<string>());
names.Add((string)array[reader.ReadVarInt()]["name"]);
return names;
}
@@ -797,28 +802,501 @@ namespace SpineViewer.Spine.Implementations.SkeletonConverter
obj["c4"] = reader.ReadFloat();
break;
default:
throw new ArgumentException($"Invalid curve type: {type}"); ;
throw new ArgumentOutOfRangeException($"Invalid curve type: {type}"); ;
}
}
protected override void WriteBinary(JsonObject root, string binPath)
private BinaryWriter writer;
private readonly Dictionary<string, int> bone2idx = [];
private readonly Dictionary<string, int> slot2idx = [];
private readonly Dictionary<string, int> ik2idx = [];
private readonly Dictionary<string, int> transform2idx = [];
private readonly Dictionary<string, int> path2idx = [];
private readonly Dictionary<string, int> event2idx = [];
public override void WriteBinary(JsonObject root, string binPath, bool nonessential = false)
{
throw new NotImplementedException();
this.nonessential = nonessential;
this.root = root;
using var outputBody = new MemoryStream(); // 先把主体写入内存缓冲区
writer = new(outputBody);
WriteBones();
WriteSlots();
WriteIK();
WriteTransform();
WritePath();
WriteSkins();
WriteEvents();
WriteAnimations();
//using var output = File.Create(binPath); // 将数据写入文件
//writer = new(output);
WriteSkeleton();
WriteStrings();
//output.Write(outputBody.GetBuffer());
writer = null;
this.root = null;
}
//public void WriteFloatArray(float[] array)
//{
// foreach (var i in array)
// writer.WriteFloat(i);
//}
private void WriteSkeleton()
{
JsonObject skeleton = root["skeleton"].AsObject();
writer.WriteString((string)skeleton["hash"]);
var version = (string)skeleton["spine"];
if (version == "3.8.75") version = "3.8.76"; // replace 3.8.75 to another version to avoid detection in official runtime
writer.WriteString(version);
if (skeleton.TryGetPropertyValue("x", out var x)) writer.WriteFloat((float)x); else writer.WriteFloat(0);
if (skeleton.TryGetPropertyValue("y", out var y)) writer.WriteFloat((float)y); else writer.WriteFloat(0);
if (skeleton.TryGetPropertyValue("width", out var width)) writer.WriteFloat((float)width); else writer.WriteFloat(0);
if (skeleton.TryGetPropertyValue("height", out var height)) writer.WriteFloat((float)height); else writer.WriteFloat(0);
writer.WriteBoolean(nonessential);
if (nonessential)
{
if (skeleton.TryGetPropertyValue("fps", out var fps)) writer.WriteFloat((float)fps); else writer.WriteFloat(30);
if (skeleton.TryGetPropertyValue("images", out var images)) writer.WriteString((string)images); else writer.WriteString(null);
if (skeleton.TryGetPropertyValue("audio", out var audio)) writer.WriteString((string)audio); else writer.WriteString(null);
}
}
private void WriteStrings()
{
writer.WriteVarInt(writer.StringTable.Count);
foreach (var s in writer.StringTable)
writer.WriteString(s);
}
private void WriteBones()
{
if (!root.ContainsKey("bones"))
{
writer.WriteVarInt(0);
return;
}
JsonArray bones = root["bones"].AsArray();
writer.WriteVarInt(bones.Count);
for (int i = 0, n = bones.Count; i < n; i++)
{
JsonObject data = bones[i].AsObject();
var name = (string)data["name"];
writer.WriteString(name);
if (i > 0) writer.WriteVarInt(bone2idx[(string)data["parent"]]);
if (data.TryGetPropertyValue("rotation", out var rotation)) writer.WriteFloat((float)rotation); else writer.WriteFloat(0);
if (data.TryGetPropertyValue("x", out var x)) writer.WriteFloat((float)x); else writer.WriteFloat(0);
if (data.TryGetPropertyValue("y", out var y)) writer.WriteFloat((float)y); else writer.WriteFloat(0);
if (data.TryGetPropertyValue("scaleX", out var scaleX)) writer.WriteFloat((float)scaleX); else writer.WriteFloat(1);
if (data.TryGetPropertyValue("scaleY", out var scaleY)) writer.WriteFloat((float)scaleY); else writer.WriteFloat(1);
if (data.TryGetPropertyValue("shearX", out var shearX)) writer.WriteFloat((float)shearX); else writer.WriteFloat(0);
if (data.TryGetPropertyValue("shearY", out var shearY)) writer.WriteFloat((float)shearY); else writer.WriteFloat(0);
if (data.TryGetPropertyValue("length", out var length)) writer.WriteFloat((float)length); else writer.WriteFloat(0);
if (data.TryGetPropertyValue("transform", out var transform)) writer.WriteVarInt((int)Enum.Parse<TransformMode>((string)transform, true)); else writer.WriteVarInt((int)TransformMode.Normal);
if (data.TryGetPropertyValue("skin", out var skin)) writer.WriteBoolean((bool)skin); else writer.WriteBoolean(false);
if (nonessential) writer.WriteInt(0);
bone2idx[name] = i;
}
}
private void WriteSlots()
{
if (!root.ContainsKey("slots"))
{
writer.WriteVarInt(0);
return;
}
JsonArray slots = root["slots"].AsArray();
writer.WriteVarInt(slots.Count);
for (int i = 0, n = slots.Count; i < n; i++)
{
JsonObject data = slots[i].AsObject();
var name = (string)data["name"];
writer.WriteString(name);
writer.WriteVarInt(bone2idx[(string)data["bone"]]);
if (data.TryGetPropertyValue("color", out var color)) writer.WriteInt(int.Parse((string)color, NumberStyles.HexNumber)); else writer.WriteInt(0);
if (data.TryGetPropertyValue("dark", out var dark)) writer.WriteInt(int.Parse((string)dark, NumberStyles.HexNumber)); else writer.WriteInt(-1);
if (data.TryGetPropertyValue("attachment", out var attachment)) writer.WriteStringRef((string)attachment); else writer.WriteStringRef(null);
if (data.TryGetPropertyValue("blend", out var blend)) writer.WriteVarInt((int)Enum.Parse<BlendMode>((string)blend, true)); else writer.WriteVarInt((int)BlendMode.Normal);
slot2idx[name] = i;
}
}
private void WriteIK()
{
if (!root.ContainsKey("ik"))
{
writer.WriteVarInt(0);
return;
}
JsonArray ik = root["ik"].AsArray();
writer.WriteVarInt(ik.Count);
for (int i = 0, n = ik.Count; i < n; i++)
{
JsonObject data = ik[i].AsObject();
var name = (string)data["name"];
writer.WriteString(name);
if (data.TryGetPropertyValue("order", out var order)) writer.WriteVarInt((int)order); else writer.WriteVarInt(0);
if (data.TryGetPropertyValue("skin", out var skin)) writer.WriteBoolean((bool)skin); else writer.WriteBoolean(false);
if (data.TryGetPropertyValue("bones", out var bones)) WriteNames(bone2idx, bones.AsArray()); else writer.WriteVarInt(0);
writer.WriteVarInt(bone2idx[(string)data["target"]]);
if (data.TryGetPropertyValue("mix", out var mix)) writer.WriteFloat((float)mix); else writer.WriteFloat(1);
if (data.TryGetPropertyValue("softness", out var softness)) writer.WriteFloat((float)softness); else writer.WriteFloat(0);
if (data.TryGetPropertyValue("bendPositive", out var bendPositive)) writer.WriteSByte((sbyte)((bool)bendPositive ? 1 : -1)); else writer.WriteSByte(1);
if (data.TryGetPropertyValue("compress", out var compress)) writer.WriteBoolean((bool)compress); else writer.WriteBoolean(false);
if (data.TryGetPropertyValue("stretch", out var stretch)) writer.WriteBoolean((bool)stretch); else writer.WriteBoolean(false);
if (data.TryGetPropertyValue("uniform", out var uniform)) writer.WriteBoolean((bool)uniform); else writer.WriteBoolean(false);
ik2idx[name] = i;
}
}
private void WriteTransform()
{
if (!root.ContainsKey("transform"))
{
writer.WriteVarInt(0);
return;
}
JsonArray transform = root["transform"].AsArray();
writer.WriteVarInt(transform.Count);
for (int i = 0, n = transform.Count; i < n; i++)
{
JsonObject data = transform[i].AsObject();
var name = (string)data["name"];
writer.WriteString(name);
if (data.TryGetPropertyValue("order", out var order)) writer.WriteVarInt((int)order); else writer.WriteVarInt(0);
if (data.TryGetPropertyValue("skin", out var skin)) writer.WriteBoolean((bool)skin); else writer.WriteBoolean(false);
if (data.TryGetPropertyValue("bones", out var bones)) WriteNames(bone2idx, bones.AsArray()); else writer.WriteVarInt(0);
writer.WriteVarInt(bone2idx[(string)data["target"]]);
if (data.TryGetPropertyValue("local", out var local)) writer.WriteBoolean((bool)local); else writer.WriteBoolean(false);
if (data.TryGetPropertyValue("relative", out var relative)) writer.WriteBoolean((bool)relative); else writer.WriteBoolean(false);
if (data.TryGetPropertyValue("rotation", out var rotation)) writer.WriteFloat((float)rotation); else writer.WriteFloat(0);
if (data.TryGetPropertyValue("x", out var x)) writer.WriteFloat((float)x); else writer.WriteFloat(0);
if (data.TryGetPropertyValue("y", out var y)) writer.WriteFloat((float)y); else writer.WriteFloat(0);
if (data.TryGetPropertyValue("scaleX", out var scaleX)) writer.WriteFloat((float)scaleX); else writer.WriteFloat(0);
if (data.TryGetPropertyValue("scaleY", out var scaleY)) writer.WriteFloat((float)scaleY); else writer.WriteFloat(0);
if (data.TryGetPropertyValue("shearY", out var shearY)) writer.WriteFloat((float)shearY); else writer.WriteFloat(0);
if (data.TryGetPropertyValue("rotateMix", out var rotateMix)) writer.WriteFloat((float)rotateMix); else writer.WriteFloat(1);
if (data.TryGetPropertyValue("translateMix", out var translateMix)) writer.WriteFloat((float)translateMix); else writer.WriteFloat(1);
if (data.TryGetPropertyValue("scaleMix", out var scaleMix)) writer.WriteFloat((float)scaleMix); else writer.WriteFloat(1);
if (data.TryGetPropertyValue("shearMix", out var shearMix)) writer.WriteFloat((float)shearMix); else writer.WriteFloat(1);
transform2idx[name] = i;
}
}
private void WritePath()
{
if (!root.ContainsKey("path"))
{
writer.WriteVarInt(0);
return;
}
JsonArray path = root["path"].AsArray();
writer.WriteVarInt(path.Count);
for (int i = 0, n = path.Count; i < n; i++)
{
JsonObject data = path[i].AsObject();
var name = (string)data["name"];
writer.WriteString(name);
if (data.TryGetPropertyValue("order", out var order)) writer.WriteVarInt((int)order); else writer.WriteVarInt(0);
if (data.TryGetPropertyValue("skin", out var skin)) writer.WriteBoolean((bool)skin); else writer.WriteBoolean(false);
if (data.TryGetPropertyValue("bones", out var bones)) WriteNames(bone2idx, bones.AsArray()); else writer.WriteVarInt(0);
writer.WriteVarInt(bone2idx[(string)data["target"]]);
if (data.TryGetPropertyValue("positionMode", out var positionMode)) writer.WriteVarInt((int)Enum.Parse<PositionMode>((string)positionMode, true)); else writer.WriteVarInt((int)PositionMode.Percent);
if (data.TryGetPropertyValue("spacingMode", out var spacingMode)) writer.WriteVarInt((int)Enum.Parse<SpacingMode>((string)spacingMode, true)); else writer.WriteVarInt((int)SpacingMode.Length);
if (data.TryGetPropertyValue("rotateMode", out var rotateMode)) writer.WriteVarInt((int)Enum.Parse<RotateMode>((string)rotateMode, true)); else writer.WriteVarInt((int)RotateMode.Tangent);
if (data.TryGetPropertyValue("rotation", out var rotation)) writer.WriteFloat((float)rotation); else writer.WriteFloat(0);
if (data.TryGetPropertyValue("position", out var position)) writer.WriteFloat((float)position); else writer.WriteFloat(0);
if (data.TryGetPropertyValue("spacing", out var spacing)) writer.WriteFloat((float)spacing); else writer.WriteFloat(0);
if (data.TryGetPropertyValue("rotateMix", out var rotateMix)) writer.WriteFloat((float)rotateMix); else writer.WriteFloat(1);
if (data.TryGetPropertyValue("translateMix", out var translateMix)) writer.WriteFloat((float)translateMix); else writer.WriteFloat(1);
path2idx[name] = i;
}
}
private void WriteSkins()
{
if (!root.ContainsKey("skins"))
{
writer.WriteVarInt(0); // default 的 slotCount
writer.WriteVarInt(0); // 其他皮肤数量
return;
}
JsonArray skins = root["skins"].AsArray();
bool hasDefault = false;
foreach (JsonObject skin in skins)
{
if ((string)skin["name"] == "default")
{
hasDefault = true;
WriteSkin(skin, true);
break;
}
}
if (!hasDefault) writer.WriteVarInt(0);
int skinCount = hasDefault ? skins.Count - 1 : skins.Count;
if (skinCount <= 0)
{
writer.WriteVarInt(0);
return;
}
writer.WriteVarInt(skinCount);
foreach (JsonObject skin in skins)
{
if ((string)skin["name"] != "default")
WriteSkin(skin);
}
}
private void WriteSkin(JsonObject skin, bool isDefault = false)
{
JsonObject skinAttachments = null;
if (isDefault)
{
// 这里固定有一个给 default 的 count 值, 算是占位符, 如果是 0 则表示没有 default 的 skin
if (skin.TryGetPropertyValue("attachments", out var attachments)) skinAttachments = attachments.AsObject();
writer.WriteVarInt(skinAttachments?.Count ?? 0);
}
else
{
writer.WriteStringRef((string)skin["name"]);
if (skin.TryGetPropertyValue("bones", out var bones)) WriteNames(bone2idx, bones.AsArray()); else writer.WriteVarInt(0);
if (skin.TryGetPropertyValue("ik", out var ik)) WriteNames(ik2idx, ik.AsArray()); else writer.WriteVarInt(0);
if (skin.TryGetPropertyValue("transform", out var transform)) WriteNames(transform2idx, transform.AsArray()); else writer.WriteVarInt(0);
if (skin.TryGetPropertyValue("path", out var path)) WriteNames(path2idx, path.AsArray()); else writer.WriteVarInt(0);
if (skin.TryGetPropertyValue("attachments", out var attachments)) skinAttachments = attachments.AsObject();
writer.WriteVarInt(skinAttachments?.Count ?? 0);
}
if (skinAttachments is null)
return;
foreach (var (slotName, _slotAttachments) in skinAttachments)
{
JsonObject slotAttachments = _slotAttachments.AsObject();
writer.WriteVarInt(slot2idx[slotName]);
writer.WriteVarInt(slotAttachments.Count);
foreach (var (attachmentKey, attachment) in slotAttachments)
{
writer.WriteStringRef(attachmentKey);
WriteAttachment(attachment.AsObject(), attachmentKey);
}
}
}
private void WriteAttachment(JsonObject attachment, string keyName)
{
int vertexCount;
string name = keyName;
AttachmentType type = AttachmentType.Region;
if (attachment.TryGetPropertyValue("name", out var _name)) name = (string)_name;
if (attachment.TryGetPropertyValue("type", out var _type)) type = Enum.Parse<AttachmentType>((string)_type, true);
writer.WriteStringRef(name);
writer.WriteByte((byte)type);
switch (type)
{
case AttachmentType.Region:
if (attachment.TryGetPropertyValue("path", out var path1)) writer.WriteStringRef((string)path1); else writer.WriteStringRef(null);
if (attachment.TryGetPropertyValue("rotation", out var rotation1)) writer.WriteFloat((float)rotation1); else writer.WriteFloat(0);
if (attachment.TryGetPropertyValue("x", out var x1)) writer.WriteFloat((float)x1); else writer.WriteFloat(0);
if (attachment.TryGetPropertyValue("y", out var y1)) writer.WriteFloat((float)y1); else writer.WriteFloat(0);
if (attachment.TryGetPropertyValue("scaleX", out var scaleX)) writer.WriteFloat((float)scaleX); else writer.WriteFloat(1);
if (attachment.TryGetPropertyValue("scaleY", out var scaleY)) writer.WriteFloat((float)scaleY); else writer.WriteFloat(1);
if (attachment.TryGetPropertyValue("width", out var width)) writer.WriteFloat((float)width); else writer.WriteFloat(32);
if (attachment.TryGetPropertyValue("height", out var height)) writer.WriteFloat((float)height); else writer.WriteFloat(32);
if (attachment.TryGetPropertyValue("color", out var color1)) writer.WriteInt(int.Parse((string)color1, NumberStyles.HexNumber)); else writer.WriteInt(0);
break;
case AttachmentType.Boundingbox:
if (attachment.TryGetPropertyValue("vertexCount", out var _vertexCount1)) vertexCount = (int)_vertexCount1; else vertexCount = 0;
writer.WriteVarInt(vertexCount);
WriteVertices(attachment["vertices"].AsArray(), vertexCount);
if (nonessential) writer.WriteInt(0);
break;
case AttachmentType.Mesh:
if (attachment.TryGetPropertyValue("path", out var path2)) writer.WriteStringRef((string)path2); else writer.WriteStringRef(null);
if (attachment.TryGetPropertyValue("color", out var color2)) writer.WriteInt(int.Parse((string)color2, NumberStyles.HexNumber)); else writer.WriteInt(0);
if (attachment.TryGetPropertyValue("vertexCount", out var _vertexCount2)) vertexCount = (int)_vertexCount2; else vertexCount = 0;
writer.WriteVarInt(vertexCount);
WriteFloatArray(attachment["uvs"].AsArray(), vertexCount << 1); // vertexCount = uvs.Length
WriteShortArray(attachment["triangles"].AsArray());
WriteVertices(attachment["vertices"].AsArray(), vertexCount);
if (attachment.TryGetPropertyValue("hull", out var hull)) writer.WriteVarInt((int)hull); else writer.WriteVarInt(0);
if (nonessential)
{
if (attachment.TryGetPropertyValue("edges", out var edges)) WriteShortArray(edges.AsArray()); else writer.WriteVarInt(0);
if (attachment.TryGetPropertyValue("width", out var _width)) writer.WriteFloat((float)_width); else writer.WriteFloat(0);
if (attachment.TryGetPropertyValue("height", out var _height)) writer.WriteFloat((float)_height); else writer.WriteFloat(0);
}
break;
case AttachmentType.Linkedmesh:
if (attachment.TryGetPropertyValue("path", out var path3)) writer.WriteStringRef((string)path3); else writer.WriteStringRef(null);
if (attachment.TryGetPropertyValue("color", out var color3)) writer.WriteInt(int.Parse((string)color3, NumberStyles.HexNumber)); else writer.WriteInt(0);
if (attachment.TryGetPropertyValue("skin", out var skin)) writer.WriteStringRef((string)skin); else writer.WriteStringRef(null);
if (attachment.TryGetPropertyValue("parent", out var parent)) writer.WriteStringRef((string)parent); else writer.WriteStringRef(null);
if (attachment.TryGetPropertyValue("deform", out var deform)) writer.WriteBoolean((bool)deform); else writer.WriteBoolean(true);
if (nonessential)
{
if (attachment.TryGetPropertyValue("width", out var _width)) writer.WriteFloat((float)_width); else writer.WriteFloat(0);
if (attachment.TryGetPropertyValue("height", out var _height)) writer.WriteFloat((float)_height); else writer.WriteFloat(0);
}
break;
case AttachmentType.Path:
if (attachment.TryGetPropertyValue("closed", out var closed)) writer.WriteBoolean((bool)closed); else writer.WriteBoolean(false);
if (attachment.TryGetPropertyValue("constantSpeed", out var constantSpeed)) writer.WriteBoolean((bool)constantSpeed); else writer.WriteBoolean(true);
if (attachment.TryGetPropertyValue("vertexCount", out var _vertexCount3)) vertexCount = (int)_vertexCount3; else vertexCount = 0;
writer.WriteVarInt(vertexCount);
WriteVertices(attachment["vertices"].AsArray(), vertexCount);
WriteFloatArray(attachment["lengths"].AsArray(), vertexCount / 3);
if (nonessential) writer.WriteInt(0);
break;
case AttachmentType.Point:
if (attachment.TryGetPropertyValue("rotation", out var rotation2)) writer.WriteFloat((float)rotation2); else writer.WriteFloat(0);
if (attachment.TryGetPropertyValue("x", out var x2)) writer.WriteFloat((float)x2); else writer.WriteFloat(0);
if (attachment.TryGetPropertyValue("y", out var y2)) writer.WriteFloat((float)y2); else writer.WriteFloat(0);
if (nonessential) writer.WriteInt(0);
break;
case AttachmentType.Clipping:
writer.WriteVarInt(slot2idx[(string)attachment["end"]]);
if (attachment.TryGetPropertyValue("vertexCount", out var _vertexCount4)) vertexCount = (int)_vertexCount4; else vertexCount = 0;
writer.WriteVarInt(vertexCount);
WriteVertices(attachment["vertices"].AsArray(), vertexCount);
if (nonessential) writer.WriteInt(0);
break;
default:
throw new ArgumentOutOfRangeException($"Invalid attachment type: {type}");
}
}
private void WriteEvents()
{
if (!root.ContainsKey("events"))
{
writer.WriteVarInt(0);
return;
}
JsonObject events = root["events"].AsObject();
writer.WriteVarInt(events.Count);
int i = 0;
foreach (var (name, _data) in events)
{
JsonObject data = _data.AsObject();
writer.WriteStringRef(name);
if (data.TryGetPropertyValue("int", out var @int)) writer.WriteVarInt((int)@int); else writer.WriteVarInt(0);
if (data.TryGetPropertyValue("float", out var @float)) writer.WriteFloat((float)@float); else writer.WriteFloat(0);
if (data.TryGetPropertyValue("string", out var @string)) writer.WriteString((string)@string); else writer.WriteString("");
if (data.TryGetPropertyValue("audio", out var _audio))
{
var audio = (string)_audio;
writer.WriteString(audio);
if (audio is not null)
{
if (data.TryGetPropertyValue("volume", out var volume)) writer.WriteFloat((float)volume); else writer.WriteFloat(1);
if (data.TryGetPropertyValue("balance", out var balance)) writer.WriteFloat((float)balance); else writer.WriteFloat(0);
}
}
event2idx[name] = i++;
}
}
private void WriteAnimations()
{
if (!root.ContainsKey("animations"))
{
writer.WriteVarInt(0);
return;
}
JsonArray animations = root["animations"].AsArray();
writer.WriteVarInt(animations.Count);
for (int i = 0, n = animations.Count; i < n; i++)
{
throw new NotImplementedException();
}
}
private void WriteNames(Dictionary<string, int> name2idx, JsonArray names)
{
writer.WriteVarInt(names.Count);
foreach (string name in names)
writer.WriteVarInt(name2idx[name]);
}
public void WriteFloatArray(JsonArray array, int n)
{
for (int i = 0; i < n; i++)
writer.WriteFloat((float)array[i]);
}
public void WriteShortArray(JsonArray array)
{
writer.WriteVarInt(array.Count);
foreach (uint i in array)
{
writer.WriteByte((byte)(i >> 8));
writer.WriteByte((byte)i);
}
}
private void WriteVertices(JsonArray vertices, int vertexCount)
{
bool hasWeight = vertices.Count != (vertexCount << 1);
writer.WriteBoolean(hasWeight);
if (!hasWeight)
{
WriteFloatArray(vertices, vertexCount << 1);
}
else
{
int idx = 0;
for (int i = 0; i < vertexCount; i++)
{
var bonesCount = (int)vertices[idx++];
writer.WriteVarInt(bonesCount);
for (int j = 0; j < bonesCount; j++)
{
writer.WriteVarInt((int)vertices[idx++]);
writer.WriteFloat((float)vertices[idx++]);
writer.WriteFloat((float)vertices[idx++]);
writer.WriteFloat((float)vertices[idx++]);
}
}
}
}
public override JsonObject ReadJson(string jsonPath)
{
// replace 3.8.75 to another version to avoid detection in official runtime
var root = base.ReadJson(jsonPath);
var skeleton = root["skeleton"].AsObject();
var version = (string)skeleton["spine"];
if (version == "3.8.75") skeleton["spine"] = "3.8.76";
return root;
}
public override void WriteJson(JsonObject root, string jsonPath)
{
// replace 3.8.75 to another version to avoid detection in official runtime
var skeleton = root["skeleton"].AsObject();
var version = (string)skeleton["spine"];
if (version == "3.8.75") skeleton["spine"] = "3.8.76";
base.WriteJson(root, jsonPath);
}
public override JsonObject ToVersion(JsonObject root, Version version)
{
root = version switch
{
Version.V38 => root.DeepClone().AsObject(),
_ => throw new NotImplementedException(),
};
return root;
}
//public void WriteShortArray(int[] array)
//{
// foreach (var i in array)
// {
// writer.WriteByte((byte)(i >> 8));
// writer.WriteByte((byte)i);
// }
//}
}
}

View File

@@ -13,6 +13,8 @@ namespace SpineViewer.Spine.Implementations.Spine
[SpineImplementation(Version.V21)]
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)
@@ -69,7 +71,7 @@ namespace SpineViewer.Spine.Implementations.Spine
catch
{
// 都不行就报错
throw new ArgumentException($"Unknown skeleton file format {SkelPath}");
throw new InvalidDataException($"Unknown skeleton file format {SkelPath}");
}
}
@@ -172,8 +174,15 @@ namespace SpineViewer.Spine.Implementations.Spine
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
@@ -371,6 +380,17 @@ namespace SpineViewer.Spine.Implementations.Spine
states.Shader = null;
target.Draw(vertexArray, states);
//clipping.ClipEnd();
// 包围盒
if (IsDebug && IsSelected && DebugBounds)
{
var bounds = Bounds;
boundsVertices[0] = boundsVertices[4] = new(new(bounds.Left, bounds.Top), BoundsColor);
boundsVertices[1] = new(new(bounds.Right, bounds.Top), BoundsColor);
boundsVertices[2] = new(new(bounds.Right, bounds.Bottom), BoundsColor);
boundsVertices[3] = new(new(bounds.Left, bounds.Bottom), BoundsColor);
target.Draw(boundsVertices);
}
}
}
}

View File

@@ -13,6 +13,8 @@ namespace SpineViewer.Spine.Implementations.Spine
[SpineImplementation(Version.V36)]
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)
@@ -68,7 +70,7 @@ namespace SpineViewer.Spine.Implementations.Spine
catch
{
// 都不行就报错
throw new ArgumentException($"Unknown skeleton file format {SkelPath}");
throw new InvalidDataException($"Unknown skeleton file format {SkelPath}");
}
}
@@ -170,8 +172,15 @@ namespace SpineViewer.Spine.Implementations.Spine
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
@@ -327,6 +336,17 @@ namespace SpineViewer.Spine.Implementations.Spine
states.Shader = null;
target.Draw(vertexArray, states);
clipping.ClipEnd();
// 包围盒
if (IsDebug && IsSelected && DebugBounds)
{
var bounds = Bounds;
boundsVertices[0] = boundsVertices[4] = new(new(bounds.Left, bounds.Top), BoundsColor);
boundsVertices[1] = new(new(bounds.Right, bounds.Top), BoundsColor);
boundsVertices[2] = new(new(bounds.Right, bounds.Bottom), BoundsColor);
boundsVertices[3] = new(new(bounds.Left, bounds.Bottom), BoundsColor);
target.Draw(boundsVertices);
}
}
}
}

View File

@@ -10,6 +10,8 @@ namespace SpineViewer.Spine.Implementations.Spine
[SpineImplementation(Version.V37)]
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)
@@ -66,7 +68,7 @@ namespace SpineViewer.Spine.Implementations.Spine
catch
{
// 都不行就报错
throw new ArgumentException($"Unknown skeleton file format {SkelPath}");
throw new InvalidDataException($"Unknown skeleton file format {SkelPath}");
}
}
@@ -177,8 +179,15 @@ namespace SpineViewer.Spine.Implementations.Spine
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
@@ -335,6 +344,17 @@ namespace SpineViewer.Spine.Implementations.Spine
states.Shader = null;
target.Draw(vertexArray, states);
clipping.ClipEnd();
// 包围盒
if (IsDebug && IsSelected && DebugBounds)
{
var bounds = Bounds;
boundsVertices[0] = boundsVertices[4] = new(new(bounds.Left, bounds.Top), BoundsColor);
boundsVertices[1] = new(new(bounds.Right, bounds.Top), BoundsColor);
boundsVertices[2] = new(new(bounds.Right, bounds.Bottom), BoundsColor);
boundsVertices[3] = new(new(bounds.Left, bounds.Bottom), BoundsColor);
target.Draw(boundsVertices);
}
}
}
}

View File

@@ -13,6 +13,8 @@ namespace SpineViewer.Spine.Implementations.Spine
[SpineImplementation(Version.V38)]
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)
@@ -69,7 +71,7 @@ namespace SpineViewer.Spine.Implementations.Spine
catch
{
// 都不行就报错
throw new ArgumentException($"Unknown skeleton file format {SkelPath}");
throw new InvalidDataException($"Unknown skeleton file format {SkelPath}");
}
}
@@ -180,8 +182,15 @@ namespace SpineViewer.Spine.Implementations.Spine
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
@@ -338,6 +347,17 @@ namespace SpineViewer.Spine.Implementations.Spine
states.Shader = null;
target.Draw(vertexArray, states);
clipping.ClipEnd();
// 包围盒
if (IsDebug && IsSelected && DebugBounds)
{
var bounds = Bounds;
boundsVertices[0] = boundsVertices[4] = new(new(bounds.Left, bounds.Top), BoundsColor);
boundsVertices[1] = new(new(bounds.Right, bounds.Top), BoundsColor);
boundsVertices[2] = new(new(bounds.Right, bounds.Bottom), BoundsColor);
boundsVertices[3] = new(new(bounds.Left, bounds.Bottom), BoundsColor);
target.Draw(boundsVertices);
}
}
}
}

View File

@@ -12,6 +12,8 @@ namespace SpineViewer.Spine.Implementations.Spine
[SpineImplementation(Version.V40)]
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)
@@ -68,7 +70,7 @@ namespace SpineViewer.Spine.Implementations.Spine
catch
{
// 都不行就报错
throw new ArgumentException($"Unknown skeleton file format {SkelPath}");
throw new InvalidDataException($"Unknown skeleton file format {SkelPath}");
}
}
@@ -179,8 +181,15 @@ namespace SpineViewer.Spine.Implementations.Spine
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
@@ -337,6 +346,17 @@ namespace SpineViewer.Spine.Implementations.Spine
states.Shader = null;
target.Draw(vertexArray, states);
clipping.ClipEnd();
// 包围盒
if (IsDebug && IsSelected && DebugBounds)
{
var bounds = Bounds;
boundsVertices[0] = boundsVertices[4] = new(new(bounds.Left, bounds.Top), BoundsColor);
boundsVertices[1] = new(new(bounds.Right, bounds.Top), BoundsColor);
boundsVertices[2] = new(new(bounds.Right, bounds.Bottom), BoundsColor);
boundsVertices[3] = new(new(bounds.Left, bounds.Bottom), BoundsColor);
target.Draw(boundsVertices);
}
}
}
}

View File

@@ -12,6 +12,8 @@ namespace SpineViewer.Spine.Implementations.Spine
[SpineImplementation(Version.V41)]
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)
@@ -68,7 +70,7 @@ namespace SpineViewer.Spine.Implementations.Spine
catch
{
// 都不行就报错
throw new ArgumentException($"Unknown skeleton file format {SkelPath}");
throw new InvalidDataException($"Unknown skeleton file format {SkelPath}");
}
}
@@ -179,8 +181,15 @@ namespace SpineViewer.Spine.Implementations.Spine
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
@@ -337,6 +346,17 @@ namespace SpineViewer.Spine.Implementations.Spine
states.Shader = null;
target.Draw(vertexArray, states);
clipping.ClipEnd();
// 包围盒
if (IsDebug && IsSelected && DebugBounds)
{
var bounds = Bounds;
boundsVertices[0] = boundsVertices[4] = new(new(bounds.Left, bounds.Top), BoundsColor);
boundsVertices[1] = new(new(bounds.Right, bounds.Top), BoundsColor);
boundsVertices[2] = new(new(bounds.Right, bounds.Bottom), BoundsColor);
boundsVertices[3] = new(new(bounds.Left, bounds.Bottom), BoundsColor);
target.Draw(boundsVertices);
}
}
}
}

View File

@@ -12,6 +12,8 @@ namespace SpineViewer.Spine.Implementations.Spine
[SpineImplementation(Version.V42)]
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)
@@ -68,7 +70,7 @@ namespace SpineViewer.Spine.Implementations.Spine
catch
{
// 都不行就报错
throw new ArgumentException($"Unknown skeleton file format {SkelPath}");
throw new InvalidDataException($"Unknown skeleton file format {SkelPath}");
}
}
@@ -179,8 +181,15 @@ namespace SpineViewer.Spine.Implementations.Spine
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
@@ -337,6 +346,17 @@ namespace SpineViewer.Spine.Implementations.Spine
states.Shader = null;
target.Draw(vertexArray, states);
clipping.ClipEnd();
// 包围盒
if (IsDebug && IsSelected && DebugBounds)
{
var bounds = Bounds;
boundsVertices[0] = boundsVertices[4] = new(new(bounds.Left, bounds.Top), BoundsColor);
boundsVertices[1] = new(new(bounds.Right, bounds.Top), BoundsColor);
boundsVertices[2] = new(new(bounds.Right, bounds.Bottom), BoundsColor);
boundsVertices[3] = new(new(bounds.Left, bounds.Bottom), BoundsColor);
target.Draw(boundsVertices);
}
}
}
}

View File

@@ -82,29 +82,29 @@ namespace SpineViewer.Spine
/// <summary>
/// 读取二进制骨骼文件并构造 Json 对象
/// </summary>
protected abstract JsonObject ReadBinary(string binPath);
public abstract JsonObject ReadBinary(string binPath);
/// <summary>
/// 将 Json 对象写入二进制骨骼文件
/// </summary>
protected abstract void WriteBinary(JsonObject root, string binPath);
public abstract void WriteBinary(JsonObject root, string binPath, bool nonessential = false);
/// <summary>
/// 读取 Json 对象
/// </summary>
private JsonObject ReadJson(string jsonPath)
public virtual 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");
throw new InvalidDataException($"{jsonPath} is not a valid json object");
}
/// <summary>
/// 写入 Json 对象
/// </summary>
private void WriteJson(JsonObject root, string jsonPath)
public virtual void WriteJson(JsonObject root, string jsonPath)
{
using var output = File.Create(jsonPath);
using var writer = new Utf8JsonWriter(output, jsonWriterOptions);
@@ -112,29 +112,21 @@ namespace SpineViewer.Spine
}
/// <summary>
/// 二进制转 Json 格式
/// 转换到目标版本
/// </summary>
public void BinaryToJson(string binPath, string jsonPath)
{
WriteJson(ReadBinary(binPath), jsonPath);
}
public abstract JsonObject ToVersion(JsonObject root, Version version);
/// <summary>
/// Json 转二进制格式
/// 二进制骨骼文件读
/// </summary>
public void JsonToBinary(string jsonPath, string binPath)
{
WriteBinary(ReadJson(jsonPath), binPath);
}
protected class SkeletonReader
public class BinaryReader
{
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 BinaryReader(Stream input) { this.input = input; }
public int Read()
{
int val = input.ReadByte();
@@ -230,14 +222,17 @@ namespace SpineViewer.Spine
}
}
protected class SkeletonWriter
/// <summary>
/// 二进制骨骼文件写
/// </summary>
protected class BinaryWriter
{
protected byte[] buffer = new byte[32];
protected byte[] bytesBigEndian = new byte[8];
public readonly List<string> Strings = new(32);
public readonly List<string> StringTable = new(32);
protected Stream output;
public SkeletonWriter(Stream output) { this.output = output; }
public BinaryWriter(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);
@@ -323,18 +318,18 @@ namespace SpineViewer.Spine
System.Text.Encoding.UTF8.GetBytes(val, 0, val.Length, buffer, 0);
WriteFully(buffer, 0, byteCount);
}
public void WriteStringRef(List<string> strings, string val)
public void WriteStringRef(string val)
{
if (val is null)
{
WriteVarInt(0);
return;
}
int index = strings.IndexOf(val);
int index = StringTable.IndexOf(val);
if (index < 0)
{
strings.Add(val);
index = strings.Count - 1;
StringTable.Add(val);
index = StringTable.Count - 1;
}
WriteVarInt(index + 1);
}

View File

@@ -8,12 +8,12 @@ using System.Text.RegularExpressions;
using System.Numerics;
using System.Collections;
using System.Collections.ObjectModel;
using SFML.System;
using SFML.Window;
using System.ComponentModel;
using System.Reflection;
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using System.Text.Json.Nodes;
using System.Collections.Immutable;
namespace SpineViewer.Spine
{
@@ -36,6 +36,31 @@ namespace SpineViewer.Spine
/// </summary>
public abstract class Spine : SFML.Graphics.Drawable, IDisposable
{
/// <summary>
/// 常规骨骼文件后缀集合
/// </summary>
public static readonly ImmutableHashSet<string> CommonSkelSuffix = [".skel", ".json"];
/// <summary>
/// 空动画标记
/// </summary>
public const string EMPTY_ANIMATION = "<Empty>";
/// <summary>
/// 预览图宽
/// </summary>
public const uint PREVIEW_WIDTH = 256;
/// <summary>
/// 预览图高
/// </summary>
public const uint PREVIEW_HEIGHT = 256;
/// <summary>
/// 缩放最小值
/// </summary>
public const float SCALE_MIN = 0.001f;
/// <summary>
/// 实现类缓存
/// </summary>
@@ -87,15 +112,85 @@ namespace SpineViewer.Spine
FragmentShader = null;
Program.Logger.Error(ex.ToString());
Program.Logger.Error("Failed to load fragment shader");
MessageBox.Show("Fragment shader 加载失败预乘Alpha通道属性失效", "错误信息", MessageBoxButtons.OK, MessageBoxIcon.Information);
MessageBox.Warn("Fragment shader 加载失败预乘Alpha通道属性失效");
}
}
/// <summary>
/// 尝试检测骨骼文件版本
/// </summary>
public static Version? GetVersion(string skelPath)
{
string versionString = null;
Version? version = null;
using var input = File.OpenRead(skelPath);
var reader = new SkeletonConverter.BinaryReader(input);
// try json format
try
{
if (JsonNode.Parse(input) is JsonObject root && root.TryGetPropertyValue("skeleton", out var node) &&
node is JsonObject _skeleton && _skeleton.TryGetPropertyValue("spine", out var _version))
versionString = (string)_version;
}
catch { }
// try v4 binary format
if (versionString is null)
{
try
{
input.Position = 0;
var hash = reader.ReadLong();
var versionPosition = input.Position;
var versionByteCount = reader.ReadVarInt();
input.Position = versionPosition;
if (versionByteCount <= 13)
versionString = reader.ReadString();
}
catch { }
}
// try v3 binary format
if (versionString is null)
{
try
{
input.Position = 0;
var hash = reader.ReadString();
versionString = reader.ReadString();
}
catch { }
}
if (versionString is not null)
{
if (versionString.StartsWith("2.1.")) version = Version.V21;
else if (versionString.StartsWith("3.6.")) version = Version.V36;
else if (versionString.StartsWith("3.7.")) version = Version.V37;
else if (versionString.StartsWith("3.8.")) version = Version.V38;
else if (versionString.StartsWith("4.0.")) version = Version.V40;
else if (versionString.StartsWith("4.1.")) version = Version.V41;
else if (versionString.StartsWith("4.2.")) version = Version.V42;
else if (versionString.StartsWith("4.3.")) version = Version.V43;
else Program.Logger.Error("Unknown verison: {}, {}", versionString, skelPath);
}
return version;
}
/// <summary>
/// 创建特定版本的 Spine
/// </summary>
public static Spine New(Version version, string skelPath, string? atlasPath = null)
{
if (version == Version.Auto)
{
if (GetVersion(skelPath) is Version detectedVersion)
version = detectedVersion;
else
throw new InvalidDataException($"Auto version detection failed for {skelPath}, try to use a specific version");
}
if (!ImplementationTypes.TryGetValue(version, out var spineType))
{
throw new NotImplementedException($"Not implemented version: {version}");
@@ -103,6 +198,11 @@ namespace SpineViewer.Spine
return (Spine)Activator.CreateInstance(spineType, skelPath, atlasPath);
}
/// <summary>
/// 标识符
/// </summary>
public readonly string ID = Guid.NewGuid().ToString();
/// <summary>
/// 构造函数
/// </summary>
@@ -113,13 +213,14 @@ namespace SpineViewer.Spine
var attr = type.GetCustomAttribute<SpineImplementationAttribute>();
if (attr is null)
{
throw new InvalidOperationException($"Class {type.Name} has no SpineImplementationAttribute.");
throw new InvalidOperationException($"Class {type.Name} has no SpineImplementationAttribute");
}
atlasPath ??= Path.ChangeExtension(skelPath, ".atlas");
// 设置 Version
Version = attr.Version;
AssetsDir = Directory.GetParent(skelPath).FullName;
SkelPath = Path.GetFullPath(skelPath);
AtlasPath = Path.GetFullPath(atlasPath);
Name = Path.GetFileNameWithoutExtension(skelPath);
@@ -127,13 +228,9 @@ namespace SpineViewer.Spine
~Spine() { Dispose(false); }
public void Dispose() { Dispose(true); GC.SuppressFinalize(this); }
protected virtual void Dispose(bool disposing) { }
protected virtual void Dispose(bool disposing) { preview?.Dispose(); }
/// <summary>
/// 缩放最小值
/// </summary>
[Browsable(false)]
public const float SCALE_MIN = 0.001f;
#region |
/// <summary>
/// 获取所属版本
@@ -142,6 +239,12 @@ namespace SpineViewer.Spine
[Category("基本信息"), DisplayName("运行时版本")]
public Version Version { get; }
/// <summary>
/// 资源所在完整目录
/// </summary>
[Category("基本信息"), DisplayName("资源目录")]
public string AssetsDir { get; }
/// <summary>
/// skel 文件完整路径
/// </summary>
@@ -154,6 +257,9 @@ namespace SpineViewer.Spine
[Category("基本信息"), DisplayName("atlas文件路径")]
public string AtlasPath { get; }
/// <summary>
/// 名称
/// </summary>
[Category("基本信息"), DisplayName("名称")]
public string Name { get; }
@@ -163,6 +269,10 @@ namespace SpineViewer.Spine
[Category("基本信息"), DisplayName("文件版本")]
public abstract string FileVersion { get; }
#endregion
#region |
/// <summary>
/// 缩放比例
/// </summary>
@@ -188,18 +298,24 @@ namespace SpineViewer.Spine
[Category("变换"), DisplayName("垂直翻转")]
public abstract bool FlipY { get; set; }
#endregion
#region |
/// <summary>
/// 是否使用预乘Alpha
/// </summary>
[Category("画面"), DisplayName("预乘Alpha通道")]
public bool UsePremultipliedAlpha { get; set; } = true;
#endregion
/// <summary>
/// 包含的所有动画名称
/// </summary>
[Browsable(false)]
public ReadOnlyCollection<string> AnimationNames { get => animationNames.AsReadOnly(); }
protected List<string> animationNames = [];
protected List<string> animationNames = [EMPTY_ANIMATION];
/// <summary>
/// 默认动画名称
@@ -207,8 +323,10 @@ namespace SpineViewer.Spine
[Browsable(false)]
public string DefaultAnimationName { get => animationNames.Last(); }
#region |
/// <summary>
/// 当前动画名称
/// 当前动画名称, 如果设置的动画不存在则忽略
/// </summary>
[TypeConverter(typeof(AnimationConverter))]
[Category("动画"), DisplayName("当前动画")]
@@ -220,12 +338,48 @@ namespace SpineViewer.Spine
[Category("动画"), DisplayName("当前动画时长")]
public float CurrentAnimationDuration { get => GetAnimationDuration(CurrentAnimation); }
#endregion
/// <summary>
/// 骨骼包围盒
/// </summary>
[Browsable(false)]
public abstract RectangleF Bounds { get; }
/// <summary>
/// 骨骼预览图
/// </summary>
[Browsable(false)]
public Image Preview
{
get
{
if (preview is null)
{
// XXX: tex 没办法在这里主动 Dispose
// 批量添加在获取预览图的时候极大概率会和预览线程死锁
// 虽然两边不会同时调用 Draw, 但是死锁似乎和 Draw 函数有关
// 除此之外, 似乎还和 tex 的 Dispose 有关
// 如果不对 tex 进行 Dispose, 那么不管是否 Draw 都正常不会死锁
var tex = new SFML.Graphics.RenderTexture(PREVIEW_WIDTH, PREVIEW_HEIGHT);
tex.SetView(GetInitView(PREVIEW_WIDTH, PREVIEW_HEIGHT));
tex.Clear(SFML.Graphics.Color.Transparent);
var tmp = CurrentAnimation;
CurrentAnimation = EMPTY_ANIMATION;
tex.Draw(this);
CurrentAnimation = tmp;
tex.Display();
using var img = tex.Texture.CopyToImage();
img.SaveToMemory(out var imgBuffer, "bmp");
using var stream = new MemoryStream(imgBuffer);
preview = new Bitmap(stream);
}
return preview;
}
}
private Image preview = null;
/// <summary>
/// 获取动画时长, 如果动画不存在则返回 0
/// </summary>
@@ -237,6 +391,89 @@ namespace SpineViewer.Spine
/// <param name="delta">时间间隔</param>
public abstract void Update(float delta);
/// <summary>
/// 获取初始状态下合适的 View, 参数单位为像素
/// </summary>
public SFML.Graphics.View GetInitView(Size resolution, Padding padding) =>
GetInitView((uint)resolution.Width, (uint)resolution.Height, (uint)padding.Left, (uint)padding.Right, (uint)padding.Top, (uint)padding.Bottom);
/// <summary>
/// 获取初始状态下合适的 View, 参数单位为像素
/// </summary>
public SFML.Graphics.View GetInitView(uint width, uint height, Padding padding) =>
GetInitView(width, height, (uint)padding.Left, (uint)padding.Right, (uint)padding.Top, (uint)padding.Bottom);
/// <summary>
/// 获取初始状态下合适的 View, 参数单位为像素
/// </summary>
public SFML.Graphics.View GetInitView(Size resolution, uint paddingL = 1, uint paddingR = 1, uint paddingT = 1, uint paddingB = 1) =>
GetInitView((uint)resolution.Width, (uint)resolution.Height, paddingL, paddingR, paddingT, paddingB);
/// <summary>
/// 获取初始状态下合适的 View, 参数单位为像素
/// </summary>
public SFML.Graphics.View GetInitView(uint width, uint height, uint paddingL = 1, uint paddingR = 1, uint paddingT = 1, uint paddingB = 1)
{
var tmp = CurrentAnimation;
CurrentAnimation = EMPTY_ANIMATION;
var bounds = Bounds;
CurrentAnimation = tmp;
float sizeX = bounds.Width;
float sizeY = bounds.Height;
float innerW = width - paddingL - paddingR;
float innerH = height - paddingT - paddingB;
float scale = 1;
if ((sizeY / sizeX) < (innerH / innerW))
scale = sizeX / innerW; // 相同的 X, 视窗 Y 更大
else
scale = sizeY / innerH; // 相同的 Y, 视窗 X 更大
var x = bounds.X + bounds.Width / 2 + ((float)paddingL - (float)paddingR) * scale;
var y = bounds.Y + bounds.Height / 2 + ((float)paddingT - (float)paddingB) * scale;
var viewX = width * scale;
var viewY = height * scale;
return new(new(x, y), new(viewX, -viewY));
}
/// <summary>
/// 是否被选中
/// </summary>
[Browsable(false)]
public bool IsSelected { get; set; } = false;
/// <summary>
/// 显示调试
/// </summary>
[Browsable(false)]
public bool IsDebug { get; set; } = false;
/// <summary>
/// 包围盒颜色
/// </summary>
protected static readonly SFML.Graphics.Color BoundsColor = new(120, 200, 0);
/// <summary>
/// 包围盒顶点数组
/// </summary>
protected readonly SFML.Graphics.VertexArray boundsVertices = new(SFML.Graphics.PrimitiveType.LineStrip, 5);
/// <summary>
/// 显示包围盒
/// </summary>
[Category("调试"), DisplayName("显示包围盒")]
public bool DebugBounds { get; set; } = true;
/// <summary>
/// 显示骨骼
/// </summary>
[Category("调试"), DisplayName("显示骨骼(TODO)")]
public bool DebugBones { get; set; } = false;
#region SFML.Graphics.Drawable
/// <summary>
/// 顶点坐标缓冲区
/// </summary>
@@ -245,17 +482,13 @@ namespace SpineViewer.Spine
/// <summary>
/// 顶点缓冲区
/// </summary>
protected SFML.Graphics.VertexArray vertexArray = new(SFML.Graphics.PrimitiveType.Triangles);
protected readonly SFML.Graphics.VertexArray vertexArray = new(SFML.Graphics.PrimitiveType.Triangles);
/// <summary>
/// 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;
#endregion
}
}

View File

@@ -18,7 +18,7 @@ namespace SpineViewer.Spine
if (destinationType == typeof(string) && value is Version version)
{
// 调用自定义的 String() 方法
return version.String();
return version.GetName();
}
return base.ConvertTo(context, culture, value, destinationType);
@@ -43,10 +43,18 @@ namespace SpineViewer.Spine
{
if (context?.Instance is Spine obj)
{
// 返回 AnimationNames 作为下拉选项
return new StandardValuesCollection(obj.AnimationNames);
}
else if (context?.Instance is Spine[] spines)
{
if (spines.Length > 0)
{
IEnumerable<string> common = spines[0].AnimationNames;
foreach (var spine in spines.Skip(1))
common = common.Intersect(spine.AnimationNames);
return new StandardValuesCollection(common.ToArray());
}
}
return base.GetStandardValues(context);
}
}

View File

@@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Linq;
using System.Reflection;
@@ -8,12 +9,21 @@ using System.Threading.Tasks;
namespace SpineViewer.Spine
{
/// <summary>
/// Spine 版本静态辅助类
/// </summary>
public static class VersionHelper
{
/// <summary>
/// 描述缓存
/// 版本名称
/// </summary>
public static readonly Dictionary<Version, string> Versions = [];
public static readonly ReadOnlyDictionary<Version, string> Names;
private static readonly Dictionary<Version, string> names = [];
/// <summary>
/// Runtime 版本字符串
/// </summary>
private static readonly Dictionary<Version, string> runtimes = [];
static VersionHelper()
{
@@ -22,16 +32,33 @@ namespace SpineViewer.Spine
{
var field = typeof(Version).GetField(value.ToString());
var attribute = field?.GetCustomAttribute<DescriptionAttribute>();
Versions[(Version)value] = attribute?.Description ?? value.ToString();
names[(Version)value] = attribute?.Description ?? value.ToString();
}
Names = names.AsReadOnly();
runtimes[Version.V21] = Assembly.GetAssembly(typeof(SpineRuntime21.Skeleton)).GetCustomAttribute<AssemblyInformationalVersionAttribute>()?.InformationalVersion;
runtimes[Version.V36] = Assembly.GetAssembly(typeof(SpineRuntime36.Skeleton)).GetCustomAttribute<AssemblyInformationalVersionAttribute>()?.InformationalVersion;
runtimes[Version.V37] = Assembly.GetAssembly(typeof(SpineRuntime37.Skeleton)).GetCustomAttribute<AssemblyInformationalVersionAttribute>()?.InformationalVersion;
runtimes[Version.V38] = Assembly.GetAssembly(typeof(SpineRuntime38.Skeleton)).GetCustomAttribute<AssemblyInformationalVersionAttribute>()?.InformationalVersion;
runtimes[Version.V40] = Assembly.GetAssembly(typeof(SpineRuntime40.Skeleton)).GetCustomAttribute<AssemblyInformationalVersionAttribute>()?.InformationalVersion;
runtimes[Version.V41] = Assembly.GetAssembly(typeof(SpineRuntime41.Skeleton)).GetCustomAttribute<AssemblyInformationalVersionAttribute>()?.InformationalVersion;
runtimes[Version.V42] = Assembly.GetAssembly(typeof(SpineRuntime42.Skeleton)).GetCustomAttribute<AssemblyInformationalVersionAttribute>()?.InformationalVersion;
}
/// <summary>
/// 版本字符串
/// 版本字符串名称
/// </summary>
public static string String(this Version version)
public static string GetName(this Version version)
{
return Versions.TryGetValue(version, out var description) ? description : version.ToString();
return Names.TryGetValue(version, out var val) ? val : version.ToString();
}
/// <summary>
/// Runtime 版本字符串名称
/// </summary>
public static string GetRuntime(this Version version)
{
return runtimes.TryGetValue(version, out var val) ? val : GetName(version);
}
}
@@ -40,6 +67,7 @@ namespace SpineViewer.Spine
/// </summary>
public enum Version
{
[Description("<Auto>")] Auto = 0x0000,
[Description("2.1.x")] V21 = 0x0201,
[Description("3.6.x")] V36 = 0x0306,
[Description("3.7.x")] V37 = 0x0307,

View File

@@ -8,7 +8,7 @@
<RuntimeIdentifier>win-x64</RuntimeIdentifier>
<BaseOutputPath>$(SolutionDir)out</BaseOutputPath>
<IncludeSourceRevisionInInformationalVersion>false</IncludeSourceRevisionInInformationalVersion>
<Version>0.10.0</Version>
<Version>0.10.7</Version>
<OutputType>WinExe</OutputType>
<UseWindowsForms>true</UseWindowsForms>
<ApplicationIcon>appicon.ico</ApplicationIcon>
@@ -19,6 +19,7 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="FFMpegCore" Version="5.2.0" />
<PackageReference Include="NLog.Windows.Forms" Version="5.2.3" />
<PackageReference Include="SFML.Net" Version="2.6.1" />
<PackageReference Include="System.Management" Version="9.0.2" />

BIN
img/preview.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 157 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 234 KiB