Compare commits

...

114 Commits

Author SHA1 Message Date
ww-rm
e1b0d0a2ad small change 2025-03-26 02:53:09 +08:00
ww-rm
2c050ba031 update readme 2025-03-26 02:51:02 +08:00
ww-rm
41518b16b4 更新至v0.11.0 2025-03-26 02:45:01 +08:00
ww-rm
72a16dc95f 导出单个时也在子目录输出 2025-03-26 02:44:30 +08:00
ww-rm
3404c64f55 update changelog 2025-03-26 02:40:21 +08:00
ww-rm
b9015422f8 增加注释 2025-03-26 02:37:37 +08:00
ww-rm
a7441b968d 增加快进功能 2025-03-26 02:31:07 +08:00
ww-rm
2d44be31f7 优化Bitmap获取过程 2025-03-25 23:34:35 +08:00
ww-rm
c2cf25bb2b 统一导出类结构 2025-03-25 23:25:04 +08:00
ww-rm
7c4c53dcb0 简化时间标记 2025-03-25 18:46:17 +08:00
ww-rm
aceb3b17c8 统一调用 2025-03-25 18:42:24 +08:00
ww-rm
adfcfdb1de 优化显示 2025-03-25 11:24:37 +08:00
ww-rm
da329723bc 修改提示弹窗 2025-03-25 10:49:36 +08:00
ww-rm
63eb53fa06 调整命名空间 2025-03-25 00:53:32 +08:00
ww-rm
d32c824515 设置只读参数 2025-03-25 00:31:30 +08:00
ww-rm
e9ee8c481c 去除多余注释 2025-03-25 00:21:07 +08:00
ww-rm
6d78e52605 增加开始暂停图标按钮 2025-03-25 00:08:50 +08:00
ww-rm
90136a5562 重构导出 2025-03-24 23:05:01 +08:00
ww-rm
1592767c8c 增加注释 2025-03-24 21:14:10 +08:00
ww-rm
afa6ce2113 增加画面开始暂停 2025-03-24 21:01:07 +08:00
ww-rm
50e6e414ee 调整文件夹结构 2025-03-24 18:49:36 +08:00
ww-rm
ba9b8edcdc 增加文件夹路径编辑器 2025-03-24 18:49:01 +08:00
ww-rm
d7a927475c 修改父类 2025-03-24 18:48:16 +08:00
ww-rm
afe210343f 增加适合模型文件的UIEditor 2025-03-24 18:43:51 +08:00
ww-rm
4e293daf62 更新至v0.10.9 2025-03-24 15:18:09 +08:00
ww-rm
f9d7fdc516 update readme 2025-03-24 15:17:42 +08:00
ww-rm
6a04f3955c 完善预览图导出参数 2025-03-24 15:15:59 +08:00
ww-rm
dce3b1780c update readme 2025-03-24 14:44:44 +08:00
ww-rm
f47f3e9db6 更新至v0.10.8 2025-03-24 14:42:21 +08:00
ww-rm
4ac74acaf7 update changelog 2025-03-24 14:42:11 +08:00
ww-rm
cf7588c288 update readme 2025-03-24 14:41:25 +08:00
ww-rm
ec7bdf4000 预览图增加仅导出选中 2025-03-24 14:33:09 +08:00
ww-rm
51cd97f782 调整布局 2025-03-24 13:48:05 +08:00
ww-rm
a16f2f096d 完善预览图导出 2025-03-24 13:47:56 +08:00
ww-rm
4e92f14551 完善文件转换功能 2025-03-24 01:58:53 +08:00
ww-rm
8f6cc9ff44 增加任意格式读取 2025-03-24 01:57:49 +08:00
ww-rm
f885df5c67 增加文件选择控件 2025-03-24 01:57:13 +08:00
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
61 changed files with 4450 additions and 1346 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,57 @@
# CHANGELOG
## v0.11.0
- 完成导出系统, 支持完整的单帧和帧序列导出功能
- 预览画面增加快进功能
## v0.10.9
- 预览图导出增加名称后缀参数
## v0.10.8
- 完善预览图导出
- 优化骨骼文件选择
## 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,70 +1,92 @@
# 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)
[中文](README.md) | [English](README.en.md)
A simple and user-friendly Spine file viewer and exporter.
A simple and user-friendly tool for viewing and exporting Spine files.
![previewer](img/previewer.jpg)
![previewer](img/preview.webp)
---
## Installation
Download the zip package from the [Releases](https://github.com/ww-rm/SpineViewer/releases) page.
Go to the [Release](https://github.com/ww-rm/SpineViewer/releases) page to download the compressed package.
The application requires the [.NET Desktop Runtime 8.0.x](https://dotnet.microsoft.com/en-us/download/dotnet/8.0).
The software requires the dependency framework [.NET Desktop Runtime 8.0.x](https://dotnet.microsoft.com/zh-cn/download/dotnet/8.0).
Alternatively, you can download the zip package with the `SelfContained` suffix, which can run independently.
You can also download the package with the `SelfContained` suffix, which can run independently.
## Features
## Version Support
- Supports viewing Spine files of different versions:
- [x] `v2.1.x`
- [x] `v3.6.x`
- [x] `v3.7.x`
- [x] `v3.8.x`
- [x] `v4.0.x`
- [x] `v4.1.x`
- [x] `v4.2.x`
- [ ] `v4.3.x`
- Supports animation preview for multi-skeleton files
- Allows independent parameter settings for each skeleton
- Supports exporting animation as PNG frame sequences
- Provides export settings such as zoom and rotation
- More features coming soon...
| Version | View & Export | Format Conversion | Version Conversion |
| :-------: | :--------------------: | :--------------------: | :----------------: |
| `2.1.x` | :white_check_mark: | | |
| `3.1.x` | | | |
| `3.4.x` | | | |
| `3.5.x` | | | |
| `3.6.x` | :white_check_mark: | | |
| `3.7.x` | :white_check_mark: | | |
| `3.8.x` | :white_check_mark: | :white_check_mark: | |
| `4.1.x` | :white_check_mark: | | |
| `4.2.x` | :white_check_mark: | | |
| `4.3.x` | | | |
## Usage
### Importing Skeletons
Use the **File** menu to select **Open** or **Batch Open** to import skeleton files.
There are 3 ways to import skeleton files:
### Adjusting Skeletons
- **Drag & Drop/Paste:**
Drag and drop or paste the skeleton file/directory into the model list.
This method automatically searches through the provided files and subdirectories. Although convenient, it relies on the file structure and has its limitations.
- Only standard files with `*.json`, `*.skel`, or `.atlas` extensions are automatically detected.
- The skeleton file and atlas file must have the same name.
- The version string in the skeleton file must not be modified.
Select one or more items in the **Model List** to display adjustable parameters in the **Model Parameters** panel.
- **Batch Open from the File Menu:**
This method offers more file flexibility. You can drag and drop or paste files into the file selection dialog, and additional options are available.
- The filename restrictions are similar to the above, but you can use the panels file selection button to choose skeleton files with non-standard extensions.
- You can set a fixed load version to handle cases where the version number has been modified.
Right-clicking in the **Model List** allows you to add, delete, or adjust list items. You can also drag items with the left mouse button to rearrange them.
- **Open a Single Model:**
This method offers the highest degree of freedom, allowing you to select any skeleton file and atlas file without filename restrictions. You can also set the load version.
### Adjusting the View
### Adjusting Preview Content
Mouse operations supported in the **Preview** window:
The model list supports right-click menus and various shortcut keys, and you can select multiple models to adjust their parameters in bulk.
- Left-click to drag the skeleton
- Right-click to drag the view
- Scroll wheel to zoom in/out
In addition to the parameter panel, the preview area supports several mouse actions:
Additionally, you can adjust export and preview parameters through the **View Parameters** panel.
- **Left-click:** Select and drag models. Holding down the `Ctrl` key enables multi-selection, which syncs with the model list.
- **Right-click:** Drag the overall canvas.
- **Scroll wheel:** Zoom the view.
- **Selective Rendering:** The preview area supports a mode to render only the selected models. In this mode, only the selected models are displayed, and selection changes must be made through the model list.
In the **Functions** menu, you can reset and synchronize the animation time for all skeletons.
In the function menu, you can reset and synchronize the animation time for all skeletons.
### Exporting Animations
### Exporting Preview Content
Select **Export** from the **File** menu to export all loaded skeleton animations as PNG frame sequences, based on the current preview settings.
Both preview images and videos can be exported.
You can view the full duration of each animation in the **Model Parameters** of each skeleton.
- **Preview Image:**
The exported preview image shows the model in its default state, with one image per model.
- **Video (TODO: Currently only supports frame sequence export):**
The complete animation duration for each skeleton can be viewed in the model parameters.
When the preview area is set to render only the selected models, the exported content will include only the models that are displayed.
### Format & Version Conversion
You can use the tools menu to convert skeleton files. This feature supports conversion between binary and text formats, as well as between different versions.
Currently under development, it only supports converting `3.8.x` binary format to text format.
---
*If you find this project helpful, please give it a :star: and share it with others! :)*
*If you like this project, please give it a :star: and share it with more people! :)*

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)
---
@@ -18,56 +18,76 @@
也可以下载带有 `SelfContained` 后缀的压缩包, 可以独立运行.
## 功能支持
## 版本支持
| 版本 | 查看&导出 | 转换 |
| :---: | :---: | :---: |
| `2.1.x` | :white_check_mark: | |
| `3.1.x` | | |
| `3.4.x` | | |
| `3.5.x` | | |
| `3.6.x` | :white_check_mark: | |
| `3.7.x` | :white_check_mark: | |
| `3.8.x` | :white_check_mark: | |
| `4.1.x` | :white_check_mark: | |
| `4.2.x` | :white_check_mark: | |
| `4.3.x` | | |
- 支持多骨骼文件动画预览
- 支持每个骨骼独立参数设置
- 支持动画PNG帧序列导出
- 支持缩放旋转等导出画面设置
- Coming soon...
| 版本 | 查看&导出 | 格式转换 | 版本转换 |
| :---: | :---: | :---: | :---: |
| `2.1.x` | :white_check_mark: | | |
| `3.1.x` | | | |
| `3.4.x` | | | |
| `3.5.x` | | | |
| `3.6.x` | :white_check_mark: | | |
| `3.7.x` | :white_check_mark: | | |
| `3.8.x` | :white_check_mark: | :white_check_mark: | |
| `4.1.x` | :white_check_mark: | | |
| `4.2.x` | :white_check_mark: | | |
| `4.3.x` | | | |
## 使用方法
### 骨骼导入
**文件**菜单可以选择**打开**或者**批量打开**进行骨骼文件导入.
有 3 种模式导入骨骼文件:
### 骨骼调整
- 拖放/粘贴需要导入的骨骼文件/目录到模型列表
在**模型列表**中选择一项或多项, 将会在**模型参数**面板显示可供调节的参数.
这种方式会自动查找传入的文件列表以及目录内的子级文件列表, 虽然方便但是依赖模型文件结构, 限制最多.
**模型列表**右键菜单可以对列表项进行增删调整, 也可以使用鼠标左键拖动调整顺序.
- 仅支持自动发现标准的 `*.json`/`*.skel`/`.atlas` 后缀文件.
- 骨骼文件和 atlas 文件需要是同名.
- 需要保证骨骼文件里的版本字符串不是魔改过的.
- 从文件菜单里批量打开骨骼文件
### 画面调整
这种方式提供一定程度的文件自由度, 文件选择框里同样支持拖放/粘贴, 但是多一些额外选项.
**预览画面**支持的鼠标操作:
- 文件名限制条件与上面类似, 但是可以通过面板的选择文件按钮选择非标准后缀的骨骼文件.
- 可以设置固定加载版本, 便于应对魔改过的版本号.
- 选择单个模型打开
- 左键可以对骨骼进行拖动
- 右键对画面进行拖动
- 滚轮进行画面缩放
这种方式自由度最高, 允许选择任意的骨骼文件和 atlas 文件, 可以没有文件名限制, 并且也可以设置加载版本.
除此之外, 也可以通过**画面参数**面板调节导出和预览时的画面参数.
### 预览内容调整
在**功能**菜单中, 可以重置同步所有骨骼动画时间.
模型列表支持右键菜单以及部分快捷键, 并且可以多选进行模型参数的批量调整.
### 动画导出
预览画面除了使用面板进行参数设置外, 支持部分鼠标动作:
**文件**菜单中选择**导出**可以将目前加载的所有骨骼动画按照预览时的画面进行PNG帧序列导出.
- 左键可以选择和拖拽模型, 按下 `Ctrl` 键可以实现多选, 与左侧列表选择是联动的.
- 右键对整体画面进行拖动.
- 滚轮进行画面缩放.
- 预览画面支持仅渲染选中, 在该模式下, 画面仅显示被选中的模型, 并且只能通过左侧列表改变选中状态.
可以在每个骨骼的**模型参数**中查看动画完整时长.
在功能菜单中, 可以重置同步所有骨骼动画时间.
### 预览内容导出
导出遵循 "所见即所得" 原则, 即实时预览的画面就是你导出的画面.
导出有以下几个关键参数:
- 仅渲染选中. 这个参数不仅影响预览模式, 也影响导出, 如果仅渲染选中, 那么在导出时只有被选中的模型会被考虑, 忽略其他模型.
- 输出文件夹. 这个参数某些时候可选, 当不提供时, 则将输出产物输出到每个模型各自的模型文件夹, 否则输出产物全部输出到提供的输出文件夹.
- 导出单个. 默认是每个模型独立导出, 即对模型列表进行批量操作, 如果选择仅导出单个, 那么被导出的所有模型将在同一个画面上被渲染, 输出产物只有一份.
支持单帧画面以及不同格式的视频导出.
视频(TODO: 目前仅支持帧序列导出), 可以在每个骨骼的模型参数中查看动画完整时长.
### 格式与版本转换
可以通过工具菜单进行骨骼文件转换, 允许二进制和文本格式之间的转换, 以及不同版本间的转换.
目前处于施工中, 仅支持转换 `3.8.x` 二进制到文本格式.
---

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

@@ -0,0 +1,197 @@
namespace SpineViewer.Controls
{
partial class SkelFileListBox
{
/// <summary>
/// 必需的设计器变量。
/// </summary>
private System.ComponentModel.IContainer components = null;
/// <summary>
/// 清理所有正在使用的资源。
/// </summary>
/// <param name="disposing">如果应释放托管资源,为 true否则为 false。</param>
protected override void Dispose(bool disposing)
{
if (disposing && (components != null))
{
components.Dispose();
}
base.Dispose(disposing);
}
#region
/// <summary>
/// 设计器支持所需的方法 - 不要修改
/// 使用代码编辑器修改此方法的内容。
/// </summary>
private void InitializeComponent()
{
components = new System.ComponentModel.Container();
tableLayoutPanel1 = new TableLayoutPanel();
flowLayoutPanel1 = new FlowLayoutPanel();
button_AddFolder = new Button();
button_AddFile = new Button();
label_Tip = new Label();
listBox = new ListBox();
contextMenuStrip = new ContextMenuStrip(components);
toolStripMenuItem_SelectAll = new ToolStripMenuItem();
toolStripMenuItem_Paste = new ToolStripMenuItem();
toolStripMenuItem_Remove = new ToolStripMenuItem();
folderBrowserDialog = new FolderBrowserDialog();
openFileDialog_Skel = new OpenFileDialog();
tableLayoutPanel1.SuspendLayout();
flowLayoutPanel1.SuspendLayout();
contextMenuStrip.SuspendLayout();
SuspendLayout();
//
// tableLayoutPanel1
//
tableLayoutPanel1.ColumnCount = 1;
tableLayoutPanel1.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 100F));
tableLayoutPanel1.Controls.Add(flowLayoutPanel1, 0, 0);
tableLayoutPanel1.Controls.Add(listBox, 0, 1);
tableLayoutPanel1.Dock = DockStyle.Fill;
tableLayoutPanel1.Location = new Point(0, 0);
tableLayoutPanel1.Name = "tableLayoutPanel1";
tableLayoutPanel1.RowCount = 2;
tableLayoutPanel1.RowStyles.Add(new RowStyle());
tableLayoutPanel1.RowStyles.Add(new RowStyle(SizeType.Percent, 100F));
tableLayoutPanel1.Size = new Size(801, 394);
tableLayoutPanel1.TabIndex = 0;
//
// flowLayoutPanel1
//
flowLayoutPanel1.AutoSize = true;
flowLayoutPanel1.AutoSizeMode = AutoSizeMode.GrowAndShrink;
flowLayoutPanel1.Controls.Add(button_AddFolder);
flowLayoutPanel1.Controls.Add(button_AddFile);
flowLayoutPanel1.Controls.Add(label_Tip);
flowLayoutPanel1.Dock = DockStyle.Fill;
flowLayoutPanel1.Location = new Point(3, 3);
flowLayoutPanel1.Name = "flowLayoutPanel1";
flowLayoutPanel1.Size = new Size(795, 40);
flowLayoutPanel1.TabIndex = 1;
//
// button_AddFolder
//
button_AddFolder.AutoSize = true;
button_AddFolder.AutoSizeMode = AutoSizeMode.GrowAndShrink;
button_AddFolder.Location = new Point(3, 3);
button_AddFolder.Name = "button_AddFolder";
button_AddFolder.Size = new Size(122, 34);
button_AddFolder.TabIndex = 0;
button_AddFolder.Text = "添加文件夹...";
button_AddFolder.UseVisualStyleBackColor = true;
button_AddFolder.Click += button_AddFolder_Click;
//
// button_AddFile
//
button_AddFile.AutoSize = true;
button_AddFile.AutoSizeMode = AutoSizeMode.GrowAndShrink;
button_AddFile.Location = new Point(131, 3);
button_AddFile.Name = "button_AddFile";
button_AddFile.Size = new Size(104, 34);
button_AddFile.TabIndex = 1;
button_AddFile.Text = "添加文件...";
button_AddFile.UseVisualStyleBackColor = true;
button_AddFile.Click += button_AddFile_Click;
//
// label_Tip
//
label_Tip.Anchor = AnchorStyles.Left;
label_Tip.AutoSize = true;
label_Tip.Location = new Point(241, 8);
label_Tip.Name = "label_Tip";
label_Tip.Size = new Size(139, 24);
label_Tip.TabIndex = 3;
label_Tip.Text = "已添加 0 个文件";
label_Tip.TextAlign = ContentAlignment.MiddleCenter;
//
// listBox
//
listBox.AllowDrop = true;
listBox.ContextMenuStrip = contextMenuStrip;
listBox.Dock = DockStyle.Fill;
listBox.FormattingEnabled = true;
listBox.HorizontalScrollbar = true;
listBox.ItemHeight = 24;
listBox.Location = new Point(3, 49);
listBox.Name = "listBox";
listBox.SelectionMode = SelectionMode.MultiExtended;
listBox.Size = new Size(795, 342);
listBox.TabIndex = 0;
listBox.DragDrop += listBox_DragDrop;
listBox.DragEnter += listBox_DragEnter;
//
// contextMenuStrip
//
contextMenuStrip.ImageScalingSize = new Size(24, 24);
contextMenuStrip.Items.AddRange(new ToolStripItem[] { toolStripMenuItem_SelectAll, toolStripMenuItem_Paste, toolStripMenuItem_Remove });
contextMenuStrip.Name = "contextMenuStrip";
contextMenuStrip.Size = new Size(184, 94);
//
// toolStripMenuItem_SelectAll
//
toolStripMenuItem_SelectAll.Name = "toolStripMenuItem_SelectAll";
toolStripMenuItem_SelectAll.ShortcutKeys = Keys.Control | Keys.A;
toolStripMenuItem_SelectAll.Size = new Size(183, 30);
toolStripMenuItem_SelectAll.Text = "全选";
toolStripMenuItem_SelectAll.Click += toolStripMenuItem_SelectAll_Click;
//
// toolStripMenuItem_Paste
//
toolStripMenuItem_Paste.Name = "toolStripMenuItem_Paste";
toolStripMenuItem_Paste.ShortcutKeys = Keys.Control | Keys.V;
toolStripMenuItem_Paste.Size = new Size(183, 30);
toolStripMenuItem_Paste.Text = "粘贴";
toolStripMenuItem_Paste.Click += toolStripMenuItem_Paste_Click;
//
// toolStripMenuItem_Remove
//
toolStripMenuItem_Remove.Name = "toolStripMenuItem_Remove";
toolStripMenuItem_Remove.ShortcutKeys = Keys.Delete;
toolStripMenuItem_Remove.Size = new Size(183, 30);
toolStripMenuItem_Remove.Text = "移除";
toolStripMenuItem_Remove.Click += toolStripMenuItem_Remove_Click;
//
// openFileDialog_Skel
//
openFileDialog_Skel.AddExtension = false;
openFileDialog_Skel.AddToRecent = false;
openFileDialog_Skel.Filter = "skel 文件 (*.skel; *.json)|*.skel;*.json|二进制文件 (*.skel)|*.skel|文本文件 (*.json)|*.json|所有文件 (*.*)|*.*";
openFileDialog_Skel.Multiselect = true;
openFileDialog_Skel.Title = "批量选择skel文件";
//
// SkelFileListBox
//
AutoScaleDimensions = new SizeF(11F, 24F);
AutoScaleMode = AutoScaleMode.Font;
Controls.Add(tableLayoutPanel1);
Name = "SkelFileListBox";
Size = new Size(801, 394);
tableLayoutPanel1.ResumeLayout(false);
tableLayoutPanel1.PerformLayout();
flowLayoutPanel1.ResumeLayout(false);
flowLayoutPanel1.PerformLayout();
contextMenuStrip.ResumeLayout(false);
ResumeLayout(false);
}
#endregion
private TableLayoutPanel tableLayoutPanel1;
private ListBox listBox;
private FlowLayoutPanel flowLayoutPanel1;
private Button button_AddFolder;
private Button button_AddFile;
private FolderBrowserDialog folderBrowserDialog;
private Label label_Tip;
private ContextMenuStrip contextMenuStrip;
private OpenFileDialog openFileDialog_Skel;
private ToolStripMenuItem toolStripMenuItem_SelectAll;
private ToolStripMenuItem toolStripMenuItem_Paste;
private ToolStripMenuItem toolStripMenuItem_Remove;
}
}

View File

@@ -0,0 +1,123 @@
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Drawing;
using System.Data;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms;
using System.IO;
namespace SpineViewer.Controls
{
public partial class SkelFileListBox : UserControl
{
public SkelFileListBox()
{
InitializeComponent();
Items = listBox.Items;
}
/// <summary>
/// ListBox.Items
/// </summary>
public readonly ListBox.ObjectCollection Items;
/// <summary>
/// 从路径列表添加
/// </summary>
private void AddFromFileDrop(string[] paths)
{
foreach (var path in paths)
{
if (File.Exists(path))
{
if (Spine.Spine.CommonSkelSuffix.Contains(Path.GetExtension(path).ToLower()))
listBox.Items.Add(Path.GetFullPath(path));
}
else if (Directory.Exists(path))
{
foreach (var file in Directory.EnumerateFiles(path, "*.*", SearchOption.AllDirectories))
{
if (Spine.Spine.CommonSkelSuffix.Contains(Path.GetExtension(file).ToLower()))
listBox.Items.Add(file);
}
}
}
}
private void button_AddFolder_Click(object sender, EventArgs e)
{
if (folderBrowserDialog.ShowDialog() != DialogResult.OK)
return;
var path = folderBrowserDialog.SelectedPath;
if (Directory.Exists(path))
{
foreach (var file in Directory.EnumerateFiles(path, "*.*", SearchOption.AllDirectories))
{
if (Spine.Spine.CommonSkelSuffix.Contains(Path.GetExtension(file).ToLower()))
listBox.Items.Add(file);
}
}
label_Tip.Text = $"已选择 {listBox.Items.Count} 个文件";
}
private void button_AddFile_Click(object sender, EventArgs e)
{
if (openFileDialog_Skel.ShowDialog() != DialogResult.OK)
return;
foreach (var p in openFileDialog_Skel.FileNames)
listBox.Items.Add(Path.GetFullPath(p));
label_Tip.Text = $"已选择 {listBox.Items.Count} 个文件";
}
private void listBox_DragEnter(object sender, DragEventArgs e)
{
if (e.Data.GetDataPresent(DataFormats.FileDrop))
e.Effect = DragDropEffects.Copy;
else
e.Effect = DragDropEffects.None;
}
private void listBox_DragDrop(object sender, DragEventArgs e)
{
if (!e.Data.GetDataPresent(DataFormats.FileDrop))
return;
AddFromFileDrop((string[])e.Data.GetData(DataFormats.FileDrop));
label_Tip.Text = $"已选择 {listBox.Items.Count} 个文件";
}
private void toolStripMenuItem_SelectAll_Click(object sender, EventArgs e)
{
for (int i = 0; i < listBox.Items.Count; i++)
listBox.SelectedIndices.Add(i);
}
private void toolStripMenuItem_Paste_Click(object sender, EventArgs e)
{
if (!Clipboard.ContainsFileDropList())
return;
var fileDropList = Clipboard.GetFileDropList();
var paths = new string[fileDropList.Count];
fileDropList.CopyTo(paths, 0);
AddFromFileDrop(paths);
label_Tip.Text = $"已选择 {listBox.Items.Count} 个文件";
}
private void toolStripMenuItem_Remove_Click(object sender, EventArgs e)
{
var indices = new int[listBox.SelectedIndices.Count];
listBox.SelectedIndices.CopyTo(indices, 0);
for (int i = indices.Length - 1; i >= 0; i--)
listBox.Items.RemoveAt(indices[i]);
label_Tip.Text = $"已选择 {listBox.Items.Count} 个文件";
}
}
}

View File

@@ -0,0 +1,129 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<!--
Microsoft ResX Schema
Version 2.0
The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes
associated with the data types.
Example:
... ado.net/XML headers & schema ...
<resheader name="resmimetype">text/microsoft-resx</resheader>
<resheader name="version">2.0</resheader>
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
<value>[base64 mime encoded serialized .NET Framework object]</value>
</data>
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
<comment>This is a comment</comment>
</data>
There are any number of "resheader" rows that contain simple
name/value pairs.
Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the
mimetype set.
The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not
extensible. For a given mimetype the value must be set accordingly:
Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can
read any of the formats listed below.
mimetype: application/x-microsoft.net.object.binary.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.soap.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.bytearray.base64
value : The object must be serialized into a byte array
: using a System.ComponentModel.TypeConverter
: and then encoded with base64 encoding.
-->
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
<xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType>
<xsd:choice maxOccurs="unbounded">
<xsd:element name="metadata">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" />
</xsd:sequence>
<xsd:attribute name="name" use="required" type="xsd:string" />
<xsd:attribute name="type" type="xsd:string" />
<xsd:attribute name="mimetype" type="xsd:string" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="assembly">
<xsd:complexType>
<xsd:attribute name="alias" type="xsd:string" />
<xsd:attribute name="name" type="xsd:string" />
</xsd:complexType>
</xsd:element>
<xsd:element name="data">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="resheader">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" />
</xsd:complexType>
</xsd:element>
</xsd:choice>
</xsd:complexType>
</xsd:element>
</xsd:schema>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<resheader name="version">
<value>2.0</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<metadata name="contextMenuStrip.TrayLocation" type="System.Drawing.Point, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a">
<value>556, 18</value>
</metadata>
<metadata name="folderBrowserDialog.TrayLocation" type="System.Drawing.Point, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a">
<value>286, 21</value>
</metadata>
<metadata name="openFileDialog_Skel.TrayLocation" type="System.Drawing.Point, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a">
<value>31, 27</value>
</metadata>
</root>

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,71 @@ 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 path = Path.Combine(Program.TempDir, $"{spine.ID}.png");
spine.Preview.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

@@ -28,13 +28,28 @@
/// </summary>
private void InitializeComponent()
{
components = new System.ComponentModel.Container();
System.ComponentModel.ComponentResourceManager resources = new System.ComponentModel.ComponentResourceManager(typeof(SpinePreviewer));
panel = new Panel();
tableLayoutPanel1 = new TableLayoutPanel();
panel_Container = new Panel();
flowLayoutPanel1 = new FlowLayoutPanel();
button_Stop = new Button();
imageList = new ImageList(components);
button_Restart = new Button();
button_Start = new Button();
button_ForwardStep = new Button();
button_ForwardFast = new Button();
toolTip = new ToolTip(components);
tableLayoutPanel1.SuspendLayout();
panel_Container.SuspendLayout();
flowLayoutPanel1.SuspendLayout();
SuspendLayout();
//
// panel
//
panel.BackColor = SystemColors.ControlDarkDark;
panel.Location = new Point(160, 160);
panel.Location = new Point(157, 136);
panel.Margin = new Padding(0);
panel.Name = "panel";
panel.Size = new Size(320, 320);
@@ -44,20 +59,165 @@
panel.MouseUp += panel_MouseUp;
panel.MouseWheel += panel_MouseWheel;
//
// tableLayoutPanel1
//
tableLayoutPanel1.ColumnCount = 1;
tableLayoutPanel1.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 100F));
tableLayoutPanel1.Controls.Add(panel_Container, 0, 0);
tableLayoutPanel1.Controls.Add(flowLayoutPanel1, 0, 1);
tableLayoutPanel1.Dock = DockStyle.Fill;
tableLayoutPanel1.Location = new Point(0, 0);
tableLayoutPanel1.Margin = new Padding(0);
tableLayoutPanel1.Name = "tableLayoutPanel1";
tableLayoutPanel1.RowCount = 2;
tableLayoutPanel1.RowStyles.Add(new RowStyle(SizeType.Percent, 100F));
tableLayoutPanel1.RowStyles.Add(new RowStyle());
tableLayoutPanel1.Size = new Size(641, 636);
tableLayoutPanel1.TabIndex = 2;
//
// panel_Container
//
panel_Container.BackColor = SystemColors.ControlDark;
panel_Container.Controls.Add(panel);
panel_Container.Dock = DockStyle.Fill;
panel_Container.Location = new Point(0, 0);
panel_Container.Margin = new Padding(0);
panel_Container.Name = "panel_Container";
panel_Container.Size = new Size(641, 594);
panel_Container.TabIndex = 0;
//
// flowLayoutPanel1
//
flowLayoutPanel1.Anchor = AnchorStyles.None;
flowLayoutPanel1.AutoSize = true;
flowLayoutPanel1.AutoSizeMode = AutoSizeMode.GrowAndShrink;
flowLayoutPanel1.Controls.Add(button_Stop);
flowLayoutPanel1.Controls.Add(button_Restart);
flowLayoutPanel1.Controls.Add(button_Start);
flowLayoutPanel1.Controls.Add(button_ForwardStep);
flowLayoutPanel1.Controls.Add(button_ForwardFast);
flowLayoutPanel1.Location = new Point(138, 594);
flowLayoutPanel1.Margin = new Padding(0);
flowLayoutPanel1.Name = "flowLayoutPanel1";
flowLayoutPanel1.Size = new Size(365, 42);
flowLayoutPanel1.TabIndex = 1;
//
// button_Stop
//
button_Stop.AutoSize = true;
button_Stop.AutoSizeMode = AutoSizeMode.GrowAndShrink;
button_Stop.ImageKey = "stop";
button_Stop.ImageList = imageList;
button_Stop.Location = new Point(3, 3);
button_Stop.Name = "button_Stop";
button_Stop.Padding = new Padding(15, 3, 15, 3);
button_Stop.Size = new Size(67, 36);
button_Stop.TabIndex = 0;
toolTip.SetToolTip(button_Stop, "停止播放并重置时间到初始");
button_Stop.UseVisualStyleBackColor = true;
button_Stop.Click += button_Stop_Click;
//
// imageList
//
imageList.ColorDepth = ColorDepth.Depth32Bit;
imageList.ImageStream = (ImageListStreamer)resources.GetObject("imageList.ImageStream");
imageList.TransparentColor = Color.Transparent;
imageList.Images.SetKeyName(0, "stop");
imageList.Images.SetKeyName(1, "restart");
imageList.Images.SetKeyName(2, "start");
imageList.Images.SetKeyName(3, "pause");
imageList.Images.SetKeyName(4, "forward-step");
imageList.Images.SetKeyName(5, "forward-fast");
//
// button_Restart
//
button_Restart.AutoSize = true;
button_Restart.AutoSizeMode = AutoSizeMode.GrowAndShrink;
button_Restart.ImageKey = "restart";
button_Restart.ImageList = imageList;
button_Restart.Location = new Point(76, 3);
button_Restart.Name = "button_Restart";
button_Restart.Padding = new Padding(15, 3, 15, 3);
button_Restart.Size = new Size(67, 36);
button_Restart.TabIndex = 1;
toolTip.SetToolTip(button_Restart, "从头开始播放");
button_Restart.UseVisualStyleBackColor = true;
button_Restart.Click += button_Restart_Click;
//
// button_Start
//
button_Start.AutoSize = true;
button_Start.AutoSizeMode = AutoSizeMode.GrowAndShrink;
button_Start.BackgroundImageLayout = ImageLayout.Center;
button_Start.ImageKey = "pause";
button_Start.ImageList = imageList;
button_Start.Location = new Point(149, 3);
button_Start.Name = "button_Start";
button_Start.Padding = new Padding(15, 3, 15, 3);
button_Start.Size = new Size(67, 36);
button_Start.TabIndex = 2;
toolTip.SetToolTip(button_Start, "开始/暂停");
button_Start.UseVisualStyleBackColor = true;
button_Start.Click += button_Start_Click;
//
// button_ForwardStep
//
button_ForwardStep.AutoSize = true;
button_ForwardStep.AutoSizeMode = AutoSizeMode.GrowAndShrink;
button_ForwardStep.ImageKey = "forward-step";
button_ForwardStep.ImageList = imageList;
button_ForwardStep.Location = new Point(222, 3);
button_ForwardStep.Name = "button_ForwardStep";
button_ForwardStep.Padding = new Padding(15, 3, 15, 3);
button_ForwardStep.Size = new Size(67, 36);
button_ForwardStep.TabIndex = 3;
toolTip.SetToolTip(button_ForwardStep, "快进 1 帧");
button_ForwardStep.UseVisualStyleBackColor = true;
button_ForwardStep.Click += button_ForwardStep_Click;
//
// button_ForwardFast
//
button_ForwardFast.AutoSize = true;
button_ForwardFast.AutoSizeMode = AutoSizeMode.GrowAndShrink;
button_ForwardFast.ImageKey = "forward-fast";
button_ForwardFast.ImageList = imageList;
button_ForwardFast.Location = new Point(295, 3);
button_ForwardFast.Name = "button_ForwardFast";
button_ForwardFast.Padding = new Padding(15, 3, 15, 3);
button_ForwardFast.Size = new Size(67, 36);
button_ForwardFast.TabIndex = 4;
toolTip.SetToolTip(button_ForwardFast, "快进 10 帧");
button_ForwardFast.UseVisualStyleBackColor = true;
button_ForwardFast.Click += button_ForwardFast_Click;
//
// SpinePreviewer
//
AutoScaleDimensions = new SizeF(11F, 24F);
AutoScaleMode = AutoScaleMode.Font;
BackColor = SystemColors.ControlDark;
Controls.Add(panel);
Controls.Add(tableLayoutPanel1);
Name = "SpinePreviewer";
Size = new Size(640, 640);
Size = new Size(641, 636);
SizeChanged += SpinePreviewer_SizeChanged;
tableLayoutPanel1.ResumeLayout(false);
tableLayoutPanel1.PerformLayout();
panel_Container.ResumeLayout(false);
flowLayoutPanel1.ResumeLayout(false);
flowLayoutPanel1.PerformLayout();
ResumeLayout(false);
}
#endregion
private Panel panel;
private TableLayoutPanel tableLayoutPanel1;
private Panel panel_Container;
private FlowLayoutPanel flowLayoutPanel1;
private Button button_Stop;
private Button button_Start;
private ImageList imageList;
private ToolTip toolTip;
private Button button_ForwardStep;
private Button button_ForwardFast;
private Button button_Restart;
}
}

View File

@@ -9,21 +9,50 @@ using System.Threading.Tasks;
using System.Windows.Forms;
using System.Security.Policy;
using System.Diagnostics;
using NLog.Targets;
namespace SpineViewer.Controls
{
public partial class SpinePreviewer : UserControl
{
/// <summary>
/// 包装类, 用于 PropertyGrid 显示
/// 要绑定的 Spine 列表控件
/// </summary>
private class PreviewerProperty
[Category("自定义"), Description("相关联的 SpineListView")]
public SpineListView? SpineListView { get; set; }
/// <summary>
/// 属性信息面板
/// </summary>
[Category("自定义"), Description("用于显示画面属性的属性页")]
public PropertyGrid? PropertyGrid
{
private readonly SpinePreviewer previewer;
get => propertyGrid;
set
{
propertyGrid = value;
if (propertyGrid is not null)
propertyGrid.SelectedObject = new PreviewerProperty(this);
}
}
private PropertyGrid? propertyGrid;
public PreviewerProperty(SpinePreviewer previewer) { this.previewer = previewer; }
#region
/// <summary>
/// 画面缩放最大值
/// </summary>
public const float ZOOM_MAX = 1000f;
/// <summary>
/// 画面缩放最小值
/// </summary>
public const float ZOOM_MIN = 0.001f;
/// <summary>
/// 包装类, 用于属性面板显示
/// </summary>
private class PreviewerProperty(SpinePreviewer previewer)
{
[TypeConverter(typeof(SizeConverter))]
[Category("导出"), DisplayName("分辨率")]
public Size Resolution { get => previewer.Resolution; set => previewer.Resolution = value; }
@@ -44,6 +73,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,40 +83,6 @@ namespace SpineViewer.Controls
public uint MaxFps { get => previewer.MaxFps; set => previewer.MaxFps = value; }
}
[Category("自定义"), Description("相关联的 SpineListView")]
public SpineListView? SpineListView { get; set; }
[Category("自定义"), Description("用于显示画面属性的属性页")]
public PropertyGrid? PropertyGrid
{
get => propertyGrid;
set
{
propertyGrid = value;
if (propertyGrid is not null)
propertyGrid.SelectedObject = new PreviewerProperty(this);
}
}
private PropertyGrid? propertyGrid;
public const float ZOOM_MAX = 1000f;
public const float ZOOM_MIN = 0.001f;
public const int BACKGROUND_CELL_SIZE = 10;
private static readonly SFML.Graphics.Color BackgroundColor = new(105, 105, 105);
private static readonly SFML.Graphics.Color AxisColor = new(220, 220, 220);
private static readonly SFML.Graphics.Color BoundsColor = new(120, 200, 0);
private readonly SFML.Graphics.VertexArray AxisVertex = new(SFML.Graphics.PrimitiveType.Lines, 2);
private readonly SFML.Graphics.VertexArray BoundsRect = new(SFML.Graphics.PrimitiveType.LineStrip, 5);
private readonly SFML.Graphics.RenderWindow RenderWindow;
private readonly SFML.System.Clock Clock = new();
private SFML.System.Vector2f? draggingSrc = null;
private Task? task = null;
private CancellationTokenSource? cancelToken = null;
/// <summary>
/// 分辨率
/// </summary>
@@ -225,6 +223,13 @@ namespace SpineViewer.Controls
}
}
/// <summary>
/// 仅渲染选中
/// </summary>
[DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
[Browsable(false)]
public bool RenderSelectedOnly { get; set; } = false;
/// <summary>
/// 显示坐标轴
/// </summary>
@@ -241,11 +246,11 @@ namespace SpineViewer.Controls
private uint maxFps = 60;
/// <summary>
/// RenderWindow.View
/// 获取 View
/// </summary>
[DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
[Browsable(false)]
public SFML.Graphics.View View { get => RenderWindow.GetView(); }
public SFML.Graphics.View GetView() => RenderWindow.GetView();
#endregion
public SpinePreviewer()
{
@@ -260,22 +265,37 @@ namespace SpineViewer.Controls
MaxFps = 30;
}
#region 线
/// <summary>
/// 开始预览
/// 渲染窗口
/// </summary>
public void StartPreview()
private readonly SFML.Graphics.RenderWindow RenderWindow;
/// <summary>
/// 渲染任务
/// </summary>
private Task? task = null;
private CancellationTokenSource? cancelToken = null;
/// <summary>
/// 开始渲染
/// </summary>
public void StartRender()
{
if (task is not null)
return;
cancelToken = new();
task = Task.Run(RenderTask, cancelToken.Token);
IsUpdating = true;
}
/// <summary>
/// 停止预览
/// 停止渲染
/// </summary>
public void StopPreview()
public void StopRender()
{
IsUpdating = false;
if (task is null || cancelToken is null)
return;
cancelToken.Cancel();
@@ -284,13 +304,144 @@ namespace SpineViewer.Controls
task = null;
}
#endregion
#region
/// <summary>
/// 是否更新画面
/// </summary>
public bool IsUpdating
{
get => isUpdating;
private set
{
if (value == isUpdating) return;
if (value)
{
button_Start.ImageKey = "pause";
}
else
{
button_Start.ImageKey = "start";
}
isUpdating = value;
}
}
private bool isUpdating = true;
/// <summary>
/// 快进时间量
/// </summary>
private float forwardDelta = 0;
private object _forwardDeltaLock = new();
/// <summary>
/// 预览画面背景色
/// </summary>
private static readonly SFML.Graphics.Color BackgroundColor = new(105, 105, 105);
/// <summary>
/// 预览画面坐标轴颜色
/// </summary>
private static readonly SFML.Graphics.Color AxisColor = new(220, 220, 220);
/// <summary>
/// 坐标轴顶点缓冲区
/// </summary>
private readonly SFML.Graphics.VertexArray AxisVertex = new(SFML.Graphics.PrimitiveType.Lines, 2);
/// <summary>
/// 帧间隔计时器
/// </summary>
private readonly SFML.System.Clock Clock = new();
/// <summary>
/// 渲染任务
/// </summary>
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];
// 停止更新的时候只是时间不前进, 但是坐标变换还是要更新, 否则无法移动对象
if (!IsUpdating) delta = 0;
// 加上要快进的量
lock (_forwardDeltaLock)
{
delta += forwardDelta;
forwardDelta = 0;
}
spine.Update(delta);
if (RenderSelectedOnly && !spine.IsSelected)
continue;
spine.IsDebug = true;
RenderWindow.Draw(spine);
spine.IsDebug = false;
}
}
}
RenderWindow.Display();
}
}
finally
{
RenderWindow.SetActive(false);
}
}
#endregion
/// <summary>
/// 画面拖放对象世界坐标源点
/// </summary>
private SFML.System.Vector2f? draggingSrc = null;
private void SpinePreviewer_SizeChanged(object sender, EventArgs e)
{
if (RenderWindow is null)
return;
float parentX = Width;
float parentY = Height;
float parentX = panel.Parent.Width;
float parentY = panel.Parent.Height;
float sizeX = panel.Width;
float sizeY = panel.Height;
@@ -326,45 +477,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 +558,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;
@@ -428,62 +591,57 @@ namespace SpineViewer.Controls
PropertyGrid?.Refresh();
}
private void RenderTask()
private void button_Stop_Click(object sender, EventArgs e)
{
try
IsUpdating = false;
if (SpineListView is not null)
{
RenderWindow.SetActive(true);
float delta;
while (cancelToken is not null && !cancelToken.IsCancellationRequested)
lock (SpineListView.Spines)
{
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)
{
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();
foreach (var spine in SpineListView.Spines)
spine.CurrentAnimation = spine.CurrentAnimation;
}
}
finally
}
private void button_Restart_Click(object sender, EventArgs e)
{
if (SpineListView is not null)
{
RenderWindow.SetActive(false);
lock (SpineListView.Spines)
{
foreach (var spine in SpineListView.Spines)
spine.CurrentAnimation = spine.CurrentAnimation;
}
}
IsUpdating = true;
}
private void button_Start_Click(object sender, EventArgs e)
{
IsUpdating = !IsUpdating;
}
private void button_ForwardStep_Click(object sender, EventArgs e)
{
lock (_forwardDeltaLock)
{
forwardDelta += 1f / maxFps;
}
}
}
private void button_ForwardFast_Click(object sender, EventArgs e)
{
lock (_forwardDeltaLock)
{
forwardDelta += 10f / maxFps;
}
}
//public void ClickStopButton() => button_Stop_Click(button_Stop, EventArgs.Empty);
//public void ClickRestartButton() => button_Restart_Click(button_Restart, EventArgs.Empty);
//public void ClickStartButton() => button_Start_Click(button_Start, EventArgs.Empty);
//public void ClickForwardStepButton() => button_ForwardStep_Click(button_ForwardStep, EventArgs.Empty);
//public void ClickForwardFastButton() => button_ForwardFast_Click(button_ForwardFast, EventArgs.Empty);
}
}

View File

@@ -117,4 +117,210 @@
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<metadata name="imageList.TrayLocation" type="System.Drawing.Point, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a">
<value>17, 17</value>
</metadata>
<data name="imageList.ImageStream" mimetype="application/x-microsoft.net.object.binary.base64">
<value>
AAEAAAD/////AQAAAAAAAAAMAgAAAEZTeXN0ZW0uV2luZG93cy5Gb3JtcywgQ3VsdHVyZT1uZXV0cmFs
LCBQdWJsaWNLZXlUb2tlbj1iNzdhNWM1NjE5MzRlMDg5BQEAAAAmU3lzdGVtLldpbmRvd3MuRm9ybXMu
SW1hZ2VMaXN0U3RyZWFtZXIBAAAABERhdGEHAgIAAAAJAwAAAA8DAAAAHi0AAAJNU0Z0AUkBTAIBAQYB
AAFwAQABcAEAAR8BAAEYAQAE/wEhAQAI/wFCAU0BNgcAATYDAAEoAwABfAMAATADAAEBAQABIAYAAV0q
AAQCAy0BRQNbAc0DXQHoA1kBxgMyAU8DDwEUBAIYAAMOARIDQwF3A10BzwNbAc0DLQFFBAIYAANWAbID
XQHoA1wB1gNDAXcDFgEeBAIYAAMKAQ0DSQGFA14B4wNfAeUDUQGeAyQBNAMJAQsEARgAAwsBDgM7AWQD
XgHSA1YBsv8AEQAEAgMxAUwDYQHhAwAB/wMuAfkDYAHbA0QBewMeASoDBgEIFAADGAEhA1cBwgMhAfsD
YQHhAzEBTAQCGAADWQHDAwAB/wMhAfwDXAHrA0gBhAMWAR4YAAMLAQ4DTQGSAysB+QMPAf4DUAHyA1gB
ugM3AVoDEQEWAwIBAxQAAxcBHwNJAYYDXAHrA1kBw/8AEQAEAgMxAUwDYQHhAwAB/wMAAf8DHgH9A2AB
4ANQAZoDLgFGAxEBFgMGAQcEAQgAAxoBJANbAc0DAAH/A2EB4QMxAUwEAhgAA1kBwwMAAf8DAAH/AwAB
/wNbAdADPgFrAw8BEwMCAQMQAAMLAQ4DTQGSAysB+QMAAf8DAAH/AzkB9gNcAcsDRAF5Ax4BKgMGAQcM
AAQBAxgBIQNKAYsDWwHsA1kBw/8AEQAEAgMxAUwDYQHhAwAB/wMuAfkDXAHnA0MB9QNWAe4DWQG7A0MB
dwMoATsDDwEUBAEEAAMaASQDWwHNAwAB/wNhAeEDMQFMBAIYAANZAcMDIAH8A1AB8ANDAfUDLgH5A10B
zgNDAXcDGgEjAwIBAwwAAwsBDgNNAZIDKwH5AwEB/wMkAfoDOgH4Ay4B+QNdAeMDTgGYAyQBNQMGAQgE
AQQABAEDGAEhA0oBiwNbAewDWQHD/wARAAQCAzEBTANhAeEDAAH/A1wB2QM7AWMDWQG7Az0B9wM5AfgD
XAHnA1sBxQNBAXMDEwEZAwIBAwMaASQDWwHNAwAB/wNhAeEDMQFMBAIYAANZAcMDXAHrA1ABmgNVAbED
TQHzAyEB+wNbAeQDSwGPAxsBJQMDAQQIAAMLAQ4DTQGSAysB+QMfAf0DXAHZA1oBxwNDAfUDDwH+A1wB
6wNSAaMDJQE3AwMBBAQABAEDGAEhA0oBiwNbAewDWQHD/wARAAQCAzEBTANhAeEDAAH/A1sBzQMaASQD
FgEdA1UBrwMAAf8DAAH/AwAB/wMAAf8DVQGvAxYBHQMaASQDWwHNAwAB/wNhAeEDMQFMBAIYAANZAcMD
WwHkAzsBZQMHAQkDSQGGAyEB+wMAAf8DAAH/A1kBwQMdASkIAAMLAQ4DTQGSAysB+QMrAfkDTQGSAzkE
XgHiAwAB/wMAAf8DXQHjAzYBWAMFAQYEAAQBAxgBIQNKAYsDWwHsA1kBw/8AEQAEAgMxAUwDYQHhAwAB
/wNbAc0DGgEkAwIBAwMTARoDRgF/A1sB3gMhAfsDDwH+AzkB+ANZAbsDOwFjA1wB2QMAAf8DYQHhAzEB
TAQCGAADWQHDA1sB5AM7AWUDBwQJAQwDOgFhA18B1QNCAfUDLgH5A1sBygMyAU8DDwEUAw0BEQNNAZID
KwH5AysB+QNNAZIDEQEWAy0BRANaAb8DUgHvAyEB+wNdAdwDPwFuAxYBHgMEAQUDGAEhA0oBiwNbAewD
WQHD/wARAAQCAzEBTANhAeEDAAH/A1sBzQMaASQEAAQCAxMBGgM9AWcDWQHAA1IB7wMPAf4DPQH3A1wB
5wMuAfkDAAH/A2EB4QMxAUwEAhgAA1kBwwNbAeQDOwFlAwcBCQQAAwsBDgMxAU0DWAG9AzkB9gMuAfkD
YAHbA0QBeAMhAS8DTgGVAysB+QMrAfkDTQGSAwsBDgMGAQgDIAEuA00BkgNdAdwDQwH1A1oB6QNOAZYD
KAE8AyABLQNLAY0DWwHsA1kBw/8AEQAEAgMxAUwDYQHhAwAB/wNbAc0DGgEkCAAEAgMMARADLwFJA1IB
owNbAeQDPQH3Aw8B/gMAAf8DAAH/A2EB4QMxAUwEAhgAA1kBwwNbAeQDOwFlAwcBCQgAAxABFQM/AW0D
XAHWAyQB+gMeAf0DXAHWA0QBeQNUAasDIQH7AysB+QNNAZIDCwEOBAADAwEEAxsBJgNBAXMDWgHEA0UB
9ANWAe4DVQG0A0QBegNRAaIDVgHuA1kBw/8AEQAEAgMxAUwDYQHhAwAB/wNbAc0DGgEkEAADCAEKAyMB
MwNEAXkDVwG8A18B5QMkAfoDAAH/A2EB4QMxAUwEAhgAA1kBwwNbAeQDOwFlAwcBCQgAAwMBBAMaASQD
QgF0A14B0gMhAfsDOQH2A10B0QNgAeADHgH9AysB+QNNAZIDCwEOCAADAgEDAxIBFwM1AVUDWAG6A1UB
8QM5AfYDWwHeA2AB2wM5AfgDWQHD/wARAAQCAzEBTANhAeEDAAH/A1sBzQMaASQUAAMCAQMDDgESAyMB
MgM9AWgDXgHdAwAB/wNhAeEDMQFMBAIYAANZAcMDWwHkAzsBZQMHAQkMAAMCAQMDDwETAzQBUwNdAdED
OQH4Ax8B/AMfAf0DAAH/AysB+QNNAZIDCwEODAAEAQMJAQwDIwEzA1UBrgMgAf0DHgH9Ax8B/AMPAf4D
WQHD/wARAAQCAzEBTANhAeEDAAH/A1sBzQMaASQgAAMaASQDWwHNAwAB/wNhAeEDMQFMBAIYAANZAcMD
WwHkAzsBZQMHAQkYAAMPARQDVwG5AwAB/wMAAf8DAAH/AysB+QNNAZIDCwEOGAADRwGAA1wB7QMAAf8D
AAH/AwAB/wNZAcP/ABEABAIDMQFMA2EB4QMAAf8DWwHNAxoBJBQABAIDBwEJAxYBHQM1AVUDXAHZAwAB
/wNhAeEDMQFMBAIYAANZAcMDWwHkAzsBZQMHAQkMAAQBAw0BEQM0AVMDXQHRAy4B+QMQAf4DDwH+AwAB
/wMrAfkDTQGSAwsBDgwABAEDCQELAx4BKwNTAakDIAH9Ax4B/QMfAfwDDwH+A1kBw/8AEQAEAgMxAUwD
YQHhAwAB/wNbAc0DGgEkEAADBwEJAxsBJgM0AVMDTQGSA14B3QMuAfkDAAH/A2EB4QMxAUwEAhgAA1kB
wwNbAeQDOwFlAwcBCQgABAIDEAEVAzkBXgNbAc0DIQH7AyEB+wNcAecDVgHuAw8B/gMrAfkDTQGSAwsB
DggAAwIBAwMSARcDNQFVA1gBuANVAfEDOQH2A1sB3gNgAdsDOQH4A1kBw/8AEQAEAgMxAUwDYQHhAwAB
/wNbAc0DGgEkCAAEAgMMARADLgFGA00BkgNcAcgDXAHrAx4B/QMAAf8DAAH/A2EB4QMxAUwEAhgAA1kB
wwNbAeQDOwFlAwcBCQgAAwkBDAMxAU4DWAG3A00B8wMeAf0DXgHdA04BlgNZAb4DHwH8AysB+QNNAZID
CwEOBAADAwEEAxsBJgNBAXMDWgHEA0UB9ANWAe4DVQG0A0QBegNRAaIDVgHuA1kBw/8AEQAEAgMxAUwD
YQHhAwAB/wNbAc0DGgEkBAAEAgMTARoDPQFnA1oBvwNcAesDJAH6AzoB9gNcAecDLgH5AwAB/wNhAeED
MQFMBAIYAANZAcMDWwHkAzsBZQMHAQkEAAMLAQ4DLgFHA1UBrQNSAe8DOgH4A2AB2wNEAXoDJAE1A04B
mAMjAfoDKwH5A00BkgMLAQ4DBgEIAyABLgNNAZIDXQHcA0MB9QNaAekDTgGWAygBPAMgAS0DSwGNA1sB
7ANZAcP/ABEABAIDMQFMA2EB4QMAAf8DWwHNAxoBJAMCAQMDEwEaA0YBfwNbAd4DIQH7Aw8B/gM5AfgD
WQG7AzsBYwNcAdkDAAH/A2EB4QMxAUwEAhgAA1kBwwNbAeQDOwFlAwcECQEMAzoBYQNdAdQDRQH0Ay4B
+QNbAcoDMgFPAw8BFAMNAREDTQGSAysB+QMrAfkDTQGSAxEBFgMtAUQDWgG/A1IB7wMgAfwDYAHgA0AB
cQMXAR8DBAEFAxgBIQNKAYsDWwHsA1kBw/8AEQAEAgMxAUwDYQHhAwAB/wNbAc0DGgEkAxYBHQNVAa8D
AAH/AwAB/wMAAf8DAAH/A1UBrwMWAR0DGgEkA1sBzQMAAf8DYQHhAzEBTAQCGAADWQHDA1sB5AM7AWUD
BwEJA0kBhgMhAfsDAAH/AwAB/wNZAcEDHQEpCAADCwEOA00BkgMrAfkDKwH5A00BkgM5BF4B4gMAAf8D
AAH/A1gB7QNKAYsDGAEgCAEDGAEhA0oBiwNbAewDWQHD/wARAAQCAzEBTANhAeEDAAH/A1wB2QM7AWMD
WQG7Az0B9wMgAfwDRQH0A1wB2QNGAX4DEwEaAwIBAwMaASQDWwHNAwAB/wNhAeEDMQFMBAIYAANZAcMD
XAHrA1ABmgNVAbEDTQHzAyEB+wNfAeUDSwGPAxsBJQMDAQQIAAMLAQ4DTQGSAysB+QMhAfsDVgGzA0wB
jgNcAesDDwH+A1AB8ANXAbkDLgFHAwYBCAQABAEDGAEhA0oBiwNbAewDWQHD/wARAAQCAzEBTANhAeED
AAH/Ay4B+QNcAecDQwH1A1QB8QNbAdMDUQGkAzgBWwMTARkEAgQAAxoBJANbAc0DAAH/A2EB4QMxAUwE
AhgAA1kBwwMgAfwDUAHwA0MB9QMfAf0DXgHXA0YBfgMaASQDAgEDDAADCwEOA00BkgMrAfkDDwH+A00B
8wNWAe4DPQH3A10B4wNOAZgDJwE5AwgBCgQBBAAEAQMYASEDSgGLA1sB7ANZAcP/ABEABAIDMQFMA2EB
4QMAAf8DAAH/Ax4B/QNhAeEDUgGjAzsBYwMhAS8DCgENBAIIAAMaASQDWwHNAwAB/wNhAeEDMQFMBAIY
AANZAcMDAAH/AwAB/wMAAf8DXQHoA0oBiwMWAR0DBAEFEAADCwEOA00BkgMrAfkDAAH/AwAB/wM5AfYD
XAHLA0QBeQMeASoDBgEHDAAEAQMYASEDSgGLA1sB7ANZAcP/ABEABAIDMQFMA2EB4QMAAf8DLgH5A2AB
2wNFAXwDIQEwAwwBEAMDAQQQAAMaASMDXAHIAx8B/QNhAeEDMQFMBAIYAANZAcMDAAH/AyEB/ANcAesD
TgGXAyMBMwQCFAADCwEOA00BkgMrAfkDDwH+A1AB8gNYAboDNwFaAxEBFgMCAQMUAAMXAR8DSQGGA1wB
6wNZAcP/ABEABAIDLQFFA14B0gNQAfIDXQHJAzIBTwMQARUDAgEDGAADEwEaA1ABnwNbAeQDWwHQAy0B
RQQCGAADVQG0A1cB7gNfAdoDRAF4AxgBIAMDAQQYAAMKAQ0DSQGGA10B6ANaAekDUAGfAyQBNAMJAQsE
ARgAAwsBDgM7AWUDWwHTA1YBsv8AFQADAwEEAycBOgM/AW0DFAEbKAADDwETAzEBTgMdASkDAgEDHAAD
DgESAzEBTQMjATMDBAEFJAADBgEHAygBPAMoATwDBgEHJAAEAQMHAQkDCwEOAwIBA/8ABQADAgEDAw8B
FANJAYgDXwHlA10B6ANdAegDXQHoA10B6ANdAegDXQHoA10B6ANdAegDXQHoA10B6ANdAegDXQHoA10B
6ANdAegDXQHoA10B6ANdAegDXQHoA1sB5ANGAX4DBQEGBAEoAAMCAQMDDQERAx8BLAMtAUYDVwG5A10B
6ANdAegDXQHoA10B6ANdAegDXQHoA18B4wNZAb4DMgFPAw8BEwMCAQMwAAMTARkDRgF+A10B6ANdAegD
WQHBAzkBXgMmATgDDwEUBAJcAAM5AV8DXgHiA1wB5wNdAegDXQHoA10B6ANdAegDXQHcAzwBZgMGAQgD
BgEIAzwBZgNdAdwDXQHoA10B6ANdAegDXQHoA10B3ANQAZ0DJQE2IAADDAEQAzwBZgNdAc8DHgH9AyQB
+gNSAe8DWAHtA1gB7QNYAe0DWAHtA1gB7QNYAe0DWAHtA1gB7QNYAe0DWAHtA1gB7QNYAe0DWAHtA1gB
7QNSAe8DJAH6AyAB/ANWAbUDKwFCAwgBCiQABAIDGAEhA0ABcANaAb8DXQHfA1oB7QNSAe8DWwHsA14B
5gNfAeUDWAHqA1gB7QNQAfADOgH2A10B0QNDAXcDHgErAwYBBywAAz0BaQNbAd4DAQH/Ay4B+QNcAewD
WwHkA14B1wNEAXsDHgEqAwYBCFgAAz8BbAMfAf0DOQH4A1IB7wNYAe0DVgHuAyQB+gM5AfYDTwGbAx4B
KwMgAS0DUAGdAzkB9gMkAfoDVgHuA1gB7QNSAe8DPQH3A1AB8gM7AWUgAAMTARkDUAGcAz0B9wM6AfYD
XQHMA0sBjQNGAYADRgGAA0YBgANGAYADRgGAA0YBgANGAYADRgGAA0YBgANGAYADRgGAA0YBgANGAYAD
RgGAA0sBjwNcAdkDHgH9A1QB8QNOAZQDEgEYJAADBQEGAzEBTgNbAdADUAHyA14B4gNVAa4DSgGKA0YB
fwNDAXcDQwF2A0UBfANGAYADTwGbA14B3QNdAegDXwHaA04BlgMoATwDCQEMKAADRwGDAzkB+AMgAfwD
WwHTA1MBpgNcAdkDLgH5A2AB4ANQAZoDLQFEAwsBDlQAAz8BbAMgAfwDWwHTA0wBjgNGAYADSgGKA10B
3AMfAf0DXgHdAzoBYAM6AWIDXQHfAx4B/QNdAdwDSgGKA0YBgANMAY4DWwHTAyAB/AM/AWwgAAMUARsD
UgGlAx0B/QNbAdADPwFsAxgBIQMOARIDDgESAw4BEgMOARIDDgESAw4BEgMOARIDDgESAw4BEgMOARID
DgESAw4BEgMOARIDDgESAyABLgNXAbkDHgH9Ax0B/QNSAaUDFAEbJAADBQEGAzEBTQNaAccDXAHIA0AB
bwMlATcDEwEaAw4BEgMNAREDDAEQAw4BEgMOARIDHQEoAzoBYANLAY8DWwHYA14B5gNWAbYDMQFNAwYB
CCQAA0kBhgMhAfsDLgH5A1UBrgMqAUADPwFtA10B0QNDAfUDVgHuA1gBtwM2AVcDFgEdAwwBEAQCSAAD
PwFsAy4B+QNVAa4DIAEtAw4BEgMbASUDWQG+AwAB/wNYAe0DPwFtAz8BbQNYAe0DAAH/A1kBvgMbASUD
DgESAyABLQNVAa4DLgH5Az8BbCAAAxQBGwNSAaUDHQH9A1YBtgMoATwDCAEKOAADFgEeA1YBswMeAf0D
HQH9A1IBpQMUARskAAQBAw8BFAMjATMDIQEwAwsBDgMDAQQEARQABAIDBwEJAx4BKgNLAYwDXQHfAz0B
9wNTAakDKAE7JAADSQGGAyEB+wMuAfkDUwGnAxYBHgMMARADKgFAA1gBugNOAfMDRgH0A2AB2wNXAbwD
QgF0AxMBGQMCAQNEAAM/AWwDLgH5A1MBpwMVARwEAAMPARQDVwG5AwAB/wNYAe0DPwFtAz8BbQNYAe0D
AAH/A1cBuQMPARQEAAMVARwDUwGnAy4B+QM/AWwgAAMUARsDUgGlAx0B/QNVAbQDJgE4AwcBCTgAAxYB
HgNWAbMDHgH9Ax0B/QNSAaUDFAEbYAADFQEcA1MBpwMuAfkDIQH7A0kBhiQAA0kBhgMhAfsDLgH5A1MB
pwMVARwIAAMPARMDQwF2A1wB2QMfAf0DAAH/AwAB/wNVAa8DFgEdRAADPwFsAy4B+QNTAacDFQEcBAAD
DwEUA1cBuQMAAf8DWAHtAz8BbQM/AW0DWAHtAwAB/wNXAbkDDwEUBAADFQEcA1MBpwMuAfkDPwFsIAAD
FAEbA1IBpQMdAf0DVQG0AyYBOAMHAQk4AAMWAR4DVgGzAx4B/QMdAf0DUgGlAxQBG2AAAwIBAwMeASoD
VQG0Az0B9wNSAaADGwElAwUBBhwAA0kBhgMhAfsDLgH5A1MBpwMVARwIAAQCAwkBCwMYASADRAF6A2AB
2wM7AfcDPQH3A1kBuwMqAUADDgESAwUBBgQCNAADPwFsAy4B+QNTAacDFQEcBAADDwEUA1cBuQMAAf8D
WAHtAz8BbQM/AW0DWAHtAwAB/wNXAbkDDwEUBAADFQEcA1MBpwMuAfkDPwFsIAADFAEbA1IBpQMdAf0D
VQG0AyYBOAMHAQk4AAMWAR4DVgGzAx4B/QMdAf0DUgGlAxQBG2QAAw8BEwNAAW8DXQHUA1MB7wNMAZED
EgEYHAADSQGGAyEB+wMuAfkDUwGnAxUBHBAABAIDDQERAzYBVwNYAbcDWwHsA1UB8QNdAdEDRAF5AzMB
UAMbASYDBgEHMAADPwFsAy4B+QNTAacDFQEcBAADDwEUA1cBuQMAAf8DWAHtAz8BbQM/AW0DWAHtAwAB
/wNXAbkDDwEUBAADFQEcA1MBpwMuAfkDPwFsIAADFAEbA1IBpQMdAf0DVQG0AyYBOAMHAQk4AAMWAR4D
VgGzAx4B/QMdAf0DUgGlAxQBG2QAAwIBAwMPARMDUQGeAx0B/QNSAaUDFAEbHAADSQGGAyEB+wMuAfkD
UwGnAxUBHBgAAwsBDgMtAUQDSwGPA1sBxQNhAeEDYQHhA1sBzQNMAZADKAE7AwkBDCwAAz8BbAMuAfkD
UwGnAxUBHAQAAw8BFANXAbkDAAH/A1gB7QM/AW0DPwFtA1gB7QMAAf8DVwG5Aw8BFAQAAxUBHANTAacD
LgH5Az8BbCAAAxQBGwNSAaUDHQH9A1UBtAMmATgDBwEJOAADFgEeA1YBswMeAf0DHQH9A1IBpQMUARts
AANNAZMDHQH9A1IBpQMUARscAANJAYYDIQH7Ay4B+QNTAacDFQEcHAADBgEIAxkBIgMvAUkDQAFxA10B
zANNAfMDWgHpA1YBtgMtAUQsAAM/AWwDLgH5A1MBpwMVARwEAAMPARQDVwG5AwAB/wNYAe0DPwFtAz8B
bQNYAe0DAAH/A1cBuQMPARQEAAMVARwDUwGnAy4B+QM/AWwgAAMUARsDUgGlAx0B/QNVAbQDJgE4AwcB
CTgAAxYBHgNWAbMDHgH9Ax0B/QNSAaUDFAEbbAADTQGTAx0B/QNSAaUDFAEbHAADSQGGAyEB+wMuAfkD
UwGnAxUBHCAABAIDBAEFAwsBDgMwAUwDWQHBAzoB9gMsAfkDQQFyAwYBCCgAAz8BbAMuAfkDUwGnAxUB
HAQAAw8BFANXAbkDAAH/A1gB7QM/AW0DPwFtA1gB7QMAAf8DVwG5Aw8BFAQAAxUBHANTAacDLgH5Az8B
bCAAAxQBGwNSAaUDHQH9A1UBtAMmATgDBwEJOAADFgEeA1YBswMeAf0DHQH9A1IBpQMUARtsAANNAZMD
HQH9A1IBpQMUARscAANJAYYDIQH7Ay4B+QNTAacDFQEcMAADFQEcA1MBpwMuAfkDWwHTAzoBYSgAAz8B
bAMuAfkDUwGnAxUBHAQAAw8BFANXAbkDAAH/A1gB7QM/AW0DPwFtA1gB7QMAAf8DVwG5Aw8BFAQAAxUB
HANTAacDLgH5Az8BbCAAAxQBGwNSAaUDHQH9A1UBtAMmATgDBwEJOAADFgEeA1YBswMeAf0DHQH9A1IB
pQMUARsQAAQBAwQBBQMLAQ4DDwETAw8BEwMPARMDDwETAw8BEwMNAREDCQEMAwMBBAQBLAADTQGTAx0B
/QNSAaUDFAEbHAADSQGGAyEB+wMuAfkDUwGnAxUBHCAABAEDAgEDAwsBDgMwAUwDWQHBAzoB9gMsAfkD
QQFyAwYBCCgAAz8BbAMuAfkDUwGnAxUBHAQAAw8BFANXAbkDAAH/A1gB7QM/AW0DPwFtA1gB7QMAAf8D
VwG5Aw8BFAQAAxUBHANTAacDLgH5Az8BbCAAAxQBGwNSAaUDHQH9A1UBtAMmATgDBwEJOAADFgEeA1YB
swMeAf0DHQH9A1IBpQMUARsQAAMGAQgDJAE1Az4BawNGAX0DRgF+A0YBfgNGAX4DRgF+A0QBewM9AWkD
JAE0AwkBDCwAA00BkwMdAf0DUgGlAxQBGxwAA0kBhgMhAfsDLgH5A1MBpwMVARwcAAMGAQgDEgEXAyMB
MwM/AW4DWwHNAzoB9gNYAeoDVgG2Ay0BRCwAAz8BbAMuAfkDUwGnAxUBHAQAAw8BFANXAbkDAAH/A1gB
7QM/AW0DPwFtA1gB7QMAAf8DVwG5Aw8BFAQAAxUBHANTAacDLgH5Az8BbCAAAxQBGwNSAaUDHQH9A1UB
tAMmATgDBwEJOAADFgEeA1YBswMeAf0DHQH9A1IBpQMUARsQAAMQARUDRgF/A1wB2QNYAeoDXgHmA2EB
4QNgAeADXgHiA1wB5wNhAeEDUAGdAyEBMCQAAwIBAwMPARMDUQGeAx0B/QNSAaUDFAEbHAADSQGGAyEB
+wMuAfkDUwGnAxUBHBgAAwsBDgMtAUQDRgGBA1MBqQNdAd8DXQHoA18B2gNOAZYDKAE8AwkBDCwAAz8B
bAMuAfkDUwGnAxUBHAQAAw8BFANXAbkDAAH/A1gB7QM/AW0DPwFtA1gB7QMAAf8DVwG5Aw8BFAQAAxUB
HANTAacDLgH5Az8BbCAAAxQBGwNSAaUDHQH9A1UBtAMmATgDBwEJOAADFgEeA1YBswMeAf0DHQH9A1IB
pQMUARsQAAMTARoDTgGYA0IB9QMAAf8DYAHgA1YBtgNZAb4DXgHXAz0B9wMgAfwDWgHHAysBQiQAAw8B
EwNAAW8DXQHUA1MB7wNNAZIDEgEYHAADSQGGAyEB+wMuAfkDUwGnAxUBHBAABAIDDQERAzYBVwNYAbcD
WAHqA1wB7ANfAdUDSwGPAzoBYAMeASsDBgEHMAADPwFsAy4B+QNTAacDFQEcBAADDwEUA1cBuQMAAf8D
WAHtAz8BbQM/AW0DWAHtAwAB/wNXAbkDDwEUBAADFQEcA1MBpwMuAfkDPwFsIAADFAEbA1IBpQMdAf0D
VQG0AyYBOAMHAQk4AAMWAR4DVgGzAx4B/QMdAf0DUgGlAxQBGxAAAxQBGwNQAZoDOQH2AwAB/wNRAaQD
JAE0A0QBeANeAd0DPQH3A1sB3gM7AWMDDgESIAADAgEDAx4BKgNVAbQDPQH3A1EBoQMbASYDBQEGHAAD
SQGGAyEB+wMuAfkDUwGnAxUBHAwABAEDEwEZA0QBegNgAdsDOwH3Az0B9wNZAbsDKwFCAxMBGQMIAQoD
AgEDNAADPwFsAy4B+QNTAacDFQEcBAADDwEUA1cBuQMAAf8DWAHtAz8BbQM/AW0DWAHtAwAB/wNXAbkD
DwEUBAADFQEcA1MBpwMuAfkDPwFsIAADFAEbA1IBpQMdAf0DVQG0AyYBOAMHAQk4AAMWAR4DVgGzAx4B
/QMdAf0DUgGlAxQBGxAAAxQBGwNQAZoDOQH2AwAB/wNOAZUDMQFMA2EB4QMAAf8DWwHNAxoBJCgAAxUB
HANTAacDLgH5AyEB+wNJAYYkAANJAYYDIQH7Ay4B+QNTAacDFQEcDAADCwEOA00BkgMrAfkDAAH/AwAB
/wNVAa8DFgEdRAADPwFsAy4B+QNTAacDFQEcBAADDwEUA1cBuQMAAf8DWAHtAz8BbQM/AW0DWAHtAwAB
/wNXAbkDDwEUBAADFQEcA1MBpwMuAfkDPwFsIAADFAEbA1IBpQMdAf0DVgG2AygBPAMIAQo4AAMWAR4D
VgGzAx4B/QMdAf0DUgGlAxQBGxAAAxQBGwNQAZoDOQH2AwAB/wNDAfUDVAHvAyEB/AMRAf4DXQHMAx0B
KQQCGAAEAgMGAQcDDwETAzIBTwNZAcADQgH1A10B0QM6AWAkAANJAYYDIQH7Ay4B+QNTAacDFgEeAwwB
EAMqAUADVwG5A1oB6QNTAe8DWwHoA10BzwNGAX0DEwEaAwIBA0QAAz8BbAMuAfkDUwGnAxUBHAQAAw8B
FANXAbkDAAH/A1gB7QM/AW0DPwFtA1gB7QMAAf8DVwG5Aw8BFAQAAxUBHANTAacDLgH5Az8BbCAAAxQB
GwNSAaUDHQH9A1sB0AM/AWwDGAEhAw4BEgMOARIDDgESAw4BEgMOARIDDgESAw4BEgMOARIDDgESAw4B
EgMOARIDDgESAw4BEgMOARIDIAEuA1cBuQMeAf0DHQH9A1IBpQMUARsQAAMUARsDUAGaAzkB9gMAAf8D
AAH/AyMB+gNCAfUDHgH9A2AB4ANAAXEDHQEoAw4BEgMNAREDCQEMAwgBCgMLBA4BEgMcAScDOAFbA0QB
eQNaAccDYQHhA1YBtgM0AVQDDAEPJAADSQGGAyEB+wMuAfkDVQGuAyoBQAM/AW0DXQHRA0MB9QNWAe4D
WQG7A0QBeQMqAUADEQEWBAJIAAM/AWwDLgH5A1UBrgMgAS0DDgESAxsBJQNZAb4DAAH/A1gB7QM/AW0D
PwFtA1gB7QMAAf8DWQG+AxsBJQMOARIDIAEtA1UBrgMuAfkDPwFsIAADEwEaA1EBpAMfAfwDOQH2A10B
zANLAY0DRgGAA0YBgANGAYADRgGAA0YBgANGAYADRgGAA0YBgANGAYADRgGAA0YBgANGAYADRgGAA0YB
gANLAY8DXAHZAx4B/QNFAfQDTgGYAxMBGRAAAxIBGANLAYwDWAHtAwAB/wMgAf0DXAHZA1UBsQNdAd8D
QwH1A2AB4ANPAZsDRgGAA0QBegM5AV4DMwFQAz0BaANGAX4DUAGaA18B2gNbAeQDXwHaA08BmwMpAT4D
CgENKAADSQGGAyEB+wMgAfwDWwHTA1MBpgNcAdkDLgH5A2AB4ANQAZoDLQFFAxABFQMGAQcEAUwAAz8B
bAMgAfwDWwHTA0wBjgNGAYADSgGKA10B3AMeAf0DXQHfAzoBYgM6AWIDXQHfAx4B/QNdAdwDSgGKA0YB
gANMAY4DWwHTAyAB/AM/AWwgAAMSARcDSgGLA1oB6gMPAf4DJAH6A1IB7wNYAe0DWAHtA1gB7QNYAe0D
WAHtA1gB7QNYAe0DWAHtA1gB7QNYAe0DWAHtA1gB7QNYAe0DWAHtA1IB7wMkAfoDHgH9A1cBwgM0AVQD
CwEOEAADCAEKAy4BSANVAbEDXgHiA2AB2wNPAZsDLQFEA0MBdwNdAcwDYQHhA1gB6gNYAe0DWgHpA1sB
0wNcAcgDXwHaA1wB6wNOAfIDLgH5A14B0gNEAXgDIQEvAwcBCSwAA0IBdQNYAeoDAQH/Ay4B+QNQAfAD
XAHsA1wB2QNEAXsDHgEqAwYBCFgAAz8BbAMBAf8DLgH5A1IB7wNYAe0DVgHuAyQB+gM5AfYDUAGdAyAB
LQMgAS0DUAGdAzkB9gMkAfoDVgHuA1gB7QNSAe8DLgH5Aw8B/gM/AWwgAAMFAQYDGgEjA00BkwNgAeYD
WgHtAzwB9gM9AfcDPQH3Az0B9wM9AfcDPQH3Az0B9wM9AfcDPQH3Az0B9wM9AfcDPQH3Az0B9wM9AfcD
PQH3AzwB9gNaAe0DXwHlA0cBgwMKAQ0EAhAABAEDCAEKAx8BLAMuAUgDLQFEAxsBJQMHAQkDDwETAyMB
MgMuAUcDVwG5A10B6ANaAekDVwHuA1UB8QNcAewDWgHpA10B6ANbAdADNAFTAw8BEwMCAQMwAAMWAR4D
RwGDA1oB7QNVAfEDXwHVA0wBjgMrAUIDDwEUBAJcAAM6AWIDWgHpA1oB7QM8AfYDPQH3AzwB9gNcAewD
XQHcAzwBZgMGAQgDBgEIAzwBZgNdAdwDWgHtAzwB9gM9AfcDPAH2A1oB7QNgAeYDOgFiLAAEAQMjATMD
TgGXA1QBqwNUAasDVAGrA1QBqwNUAasDVAGrA1QBqwNUAasDVAGrA1QBqwNUAasDVAGrA1QBqwNUAasD
TgGXAyMBMwQBTAADCQELAzkBXQNHAYIDJwE6AwQBBUgABAIDIgExAzoBYQMPARRwAAMDAQQDKAE7A04B
mANUAasDUQGeAyEBLxQAAwMBBAMlATcDUAGfA1QBqwNOAZgDKAE7AwMBBBgAAUIBTQE+BwABPgMAASgD
AAF8AwABMAMAAQEBAAEBBgABAxYAA/8BAAH8AQMB8AE/AQMB8AEPAcAIAAH8AQEB8AE/AQMB8AEHAcAI
AAH8AQABMAE/AQAB8AEDAYAIAAH8AQABEAE/AQABcAEAAYAIAAH8AgABPwEAATABAAGACAAB/AIAAT8B
AAEwAQABgAgAAfwCAAE/DAAB/AEIAQABPwEICwAB/AEMAQABPwEMAQABIAkAAfwBDwEAAT8BDAEAATAJ
AAH8AQ8BgAE/AQ4BAAE4CQAB/AEPAfABPwEPAcABPwkAAfwBDwGAAT8BDgEAATgJAAH8AQ8BAAE/AQwB
AAEwCQAB/AEMAQABPwEMAQABIAkAAfwBCAEAAT8BCAsAAfwCAAE/DAAB/AIAAT8BAAEwCgAB/AIAAT8B
AAEwAQABgAgAAfwBAAEQAT8BAAFwAQABgAgAAfwBAAEwAT8BAAHwAQMBgAgAAfwBAAHwAT8BAQHwAQcB
wAgAAfwBAwHwAT8BAwHwAQ8BwAgAAf4BHwH4AX8BDwH4AX8BwAgAAeACAAEHAf4BAAEBAf8B4AEPAv8B
4AEAAQEB8AHgAgABBwH8AgAB/wHgAQcC/wHgAQABAQHwAeACAAEHAfwCAAF/AeABAwL/AeABAAEBAfAB
4AIAAQcB/AIAAT8B4AEAAX8B/wHgAQABAQHwAeABfwH+AQcB/AEHAcABPwHgAQABPwH/AeEBAAEhAfAB
4AF/Af4BBwL/AfgBPwHgAcABPwH/AeEBAAEhAfAB4AF/Af4BBwL/AfgBDwHgAcABAwH/AeEBAAEhAfAB
4AF/Af4BBwL/AfwBDwHgAfABAQH/AeEBAAEhAfAB4AF/Af4BBwL/AfwBDwHgAfwBAAH/AeEBAAEhAfAB
4AF/Af4BBwP/AQ8B4AH+AQAB/wHhAQABIQHwAeABfwH+AQcD/wEPAeAB/wEAAX8B4QEAASEB8AHgAX8B
/gEHA/8BDwHgAf8B8AF/AeEBAAEhAfAB4AF/Af4BBwGAAQcB/wEPAeAB/wEAAX8B4QEAASEB8AHgAX8B
/gEHAYABBwH/AQ8B4AH+AQAB/wHhAQABIQHwAeABfwH+AQcBgAEHAfwBDwHgAfwBAAH/AeEBAAEhAfAB
4AF/Af4BBwGAAQcB/AEPAeAB8AEBAf8B4QEAASEB8AHgAX8B/gEHAYABBwH4AQ8C4AEDAf8B4QEAASEB
8AHgAX8B/gEHAYABHwH4AT8C4AE/Af8B4QEAASEB8AHgAX8B/gEHAYABDwHAAT8B4AEAAT8B/wHhAQAB
IQHwAeACAAEHAYACAAE/AeABAAF/Af8B4AEAAQEB8AHgAgABBwGAAgABfwHgAQAC/wHgAQABAQHwAeAC
AAEHAYACAAH/AeABBwL/AeABAAEBAfAB4AIAAQcBgAEAAQEB/wHgAQ8C/wHgAQABAQHwAfwCAAE/Af8B
+AE/Af8B8AP/AfABPgEDAfAL
</value>
</data>
<metadata name="toolTip.TrayLocation" type="System.Drawing.Point, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a">
<value>165, 17</value>
</metadata>
</root>

View File

@@ -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

@@ -37,10 +37,7 @@
tableLayoutPanel2 = new TableLayoutPanel();
button_Ok = new Button();
button_Cancel = new Button();
listBox_FilePath = new ListBox();
button_SelectSkel = new Button();
label_Tip = new Label();
openFileDialog_Skel = new OpenFileDialog();
skelFileListBox = new SpineViewer.Controls.SkelFileListBox();
panel.SuspendLayout();
tableLayoutPanel1.SuspendLayout();
tableLayoutPanel2.SuspendLayout();
@@ -53,7 +50,7 @@
panel.Location = new Point(0, 0);
panel.Name = "panel";
panel.Padding = new Padding(50, 15, 50, 10);
panel.Size = new Size(1126, 449);
panel.Size = new Size(1042, 472);
panel.TabIndex = 1;
//
// tableLayoutPanel1
@@ -62,22 +59,19 @@
tableLayoutPanel1.ColumnStyles.Add(new ColumnStyle());
tableLayoutPanel1.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 100F));
tableLayoutPanel1.Controls.Add(label4, 0, 0);
tableLayoutPanel1.Controls.Add(label3, 0, 3);
tableLayoutPanel1.Controls.Add(comboBox_Version, 1, 3);
tableLayoutPanel1.Controls.Add(tableLayoutPanel2, 0, 4);
tableLayoutPanel1.Controls.Add(listBox_FilePath, 0, 2);
tableLayoutPanel1.Controls.Add(button_SelectSkel, 0, 1);
tableLayoutPanel1.Controls.Add(label_Tip, 1, 1);
tableLayoutPanel1.Controls.Add(label3, 0, 2);
tableLayoutPanel1.Controls.Add(comboBox_Version, 1, 2);
tableLayoutPanel1.Controls.Add(tableLayoutPanel2, 0, 3);
tableLayoutPanel1.Controls.Add(skelFileListBox, 0, 1);
tableLayoutPanel1.Dock = DockStyle.Fill;
tableLayoutPanel1.Location = new Point(50, 15);
tableLayoutPanel1.Name = "tableLayoutPanel1";
tableLayoutPanel1.RowCount = 5;
tableLayoutPanel1.RowCount = 3;
tableLayoutPanel1.RowStyles.Add(new RowStyle());
tableLayoutPanel1.RowStyles.Add(new RowStyle(SizeType.Percent, 100F));
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(1026, 424);
tableLayoutPanel1.Size = new Size(942, 447);
tableLayoutPanel1.TabIndex = 1;
//
// label4
@@ -88,7 +82,7 @@
label4.Location = new Point(15, 15);
label4.Margin = new Padding(15);
label4.Name = "label4";
label4.Size = new Size(996, 24);
label4.Size = new Size(912, 24);
label4.TabIndex = 14;
label4.Text = "说明批量导入只需要选择skel文件atlas文件需要在同目录下并且与skel文件名相同";
label4.TextAlign = ContentAlignment.MiddleCenter;
@@ -97,7 +91,7 @@
//
label3.Anchor = AnchorStyles.Right;
label3.AutoSize = true;
label3.Location = new Point(90, 307);
label3.Location = new Point(3, 343);
label3.Name = "label3";
label3.Size = new Size(50, 24);
label3.TabIndex = 12;
@@ -108,7 +102,7 @@
comboBox_Version.Anchor = AnchorStyles.Left;
comboBox_Version.DropDownStyle = ComboBoxStyle.DropDownList;
comboBox_Version.FormattingEnabled = true;
comboBox_Version.Location = new Point(146, 303);
comboBox_Version.Location = new Point(59, 339);
comboBox_Version.Name = "comboBox_Version";
comboBox_Version.Size = new Size(182, 32);
comboBox_Version.Sorted = true;
@@ -124,18 +118,19 @@
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, 381);
tableLayoutPanel2.Dock = DockStyle.Fill;
tableLayoutPanel2.Location = new Point(3, 404);
tableLayoutPanel2.Margin = new Padding(3, 30, 3, 3);
tableLayoutPanel2.Name = "tableLayoutPanel2";
tableLayoutPanel2.RowCount = 1;
tableLayoutPanel2.RowStyles.Add(new RowStyle());
tableLayoutPanel2.Size = new Size(1020, 40);
tableLayoutPanel2.Size = new Size(936, 40);
tableLayoutPanel2.TabIndex = 11;
//
// button_Ok
//
button_Ok.Anchor = AnchorStyles.Bottom | AnchorStyles.Right;
button_Ok.Location = new Point(368, 3);
button_Ok.Location = new Point(326, 3);
button_Ok.Margin = new Padding(3, 3, 30, 3);
button_Ok.Name = "button_Ok";
button_Ok.Size = new Size(112, 34);
@@ -147,7 +142,7 @@
// button_Cancel
//
button_Cancel.Anchor = AnchorStyles.Bottom | AnchorStyles.Left;
button_Cancel.Location = new Point(540, 3);
button_Cancel.Location = new Point(498, 3);
button_Cancel.Margin = new Padding(30, 3, 3, 3);
button_Cancel.Name = "button_Cancel";
button_Cancel.Size = new Size(112, 34);
@@ -156,47 +151,14 @@
button_Cancel.UseVisualStyleBackColor = true;
button_Cancel.Click += button_Cancel_Click;
//
// listBox_FilePath
// skelFileListBox
//
tableLayoutPanel1.SetColumnSpan(listBox_FilePath, 2);
listBox_FilePath.Dock = DockStyle.Fill;
listBox_FilePath.FormattingEnabled = true;
listBox_FilePath.HorizontalScrollbar = true;
listBox_FilePath.ItemHeight = 24;
listBox_FilePath.Location = new Point(3, 97);
listBox_FilePath.Name = "listBox_FilePath";
listBox_FilePath.Size = new Size(1020, 200);
listBox_FilePath.TabIndex = 2;
//
// button_SelectSkel
//
button_SelectSkel.Anchor = AnchorStyles.None;
button_SelectSkel.Location = new Point(3, 57);
button_SelectSkel.Name = "button_SelectSkel";
button_SelectSkel.Size = new Size(137, 34);
button_SelectSkel.TabIndex = 1;
button_SelectSkel.Text = "选择文件...";
button_SelectSkel.UseVisualStyleBackColor = true;
button_SelectSkel.Click += button_SelectSkel_Click;
//
// label_Tip
//
label_Tip.AutoSize = true;
label_Tip.Dock = DockStyle.Fill;
label_Tip.Location = new Point(146, 54);
label_Tip.Name = "label_Tip";
label_Tip.Size = new Size(877, 40);
label_Tip.TabIndex = 0;
label_Tip.Text = "已选择 0 个文件";
label_Tip.TextAlign = ContentAlignment.MiddleLeft;
//
// openFileDialog_Skel
//
openFileDialog_Skel.AddExtension = false;
openFileDialog_Skel.AddToRecent = false;
openFileDialog_Skel.Filter = "skel 文件 (*.skel; *.json)|*.skel;*.json|二进制文件 (*.skel)|*.skel|文本文件 (*.json)|*.json|所有文件 (*.*)|*.*";
openFileDialog_Skel.Multiselect = true;
openFileDialog_Skel.Title = "批量选择skel文件";
tableLayoutPanel1.SetColumnSpan(skelFileListBox, 2);
skelFileListBox.Dock = DockStyle.Fill;
skelFileListBox.Location = new Point(3, 57);
skelFileListBox.Name = "skelFileListBox";
skelFileListBox.Size = new Size(936, 276);
skelFileListBox.TabIndex = 15;
//
// BatchOpenSpineDialog
//
@@ -204,7 +166,7 @@
AutoScaleDimensions = new SizeF(11F, 24F);
AutoScaleMode = AutoScaleMode.Font;
CancelButton = button_Cancel;
ClientSize = new Size(1126, 449);
ClientSize = new Size(1042, 472);
Controls.Add(panel);
FormBorderStyle = FormBorderStyle.FixedDialog;
Icon = (Icon)resources.GetObject("$this.Icon");
@@ -214,7 +176,6 @@
ShowInTaskbar = false;
StartPosition = FormStartPosition.CenterScreen;
Text = "批量打开骨骼";
Load += BatchOpenSpineDialog_Load;
panel.ResumeLayout(false);
tableLayoutPanel1.ResumeLayout(false);
tableLayoutPanel1.PerformLayout();
@@ -225,15 +186,12 @@
#endregion
private Panel panel;
private TableLayoutPanel tableLayoutPanel1;
private Label label_Tip;
private ListBox listBox_FilePath;
private Button button_SelectSkel;
private TableLayoutPanel tableLayoutPanel2;
private Button button_Ok;
private Button button_Cancel;
private Label label3;
private ComboBox comboBox_Version;
private OpenFileDialog openFileDialog_Skel;
private Label label4;
private Controls.SkelFileListBox skelFileListBox;
}
}

View File

@@ -13,62 +13,48 @@ 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;
}
private void BatchOpenSpineDialog_Load(object sender, EventArgs e)
{
button_SelectSkel_Click(sender, e);
}
private void button_SelectSkel_Click(object sender, EventArgs e)
{
if (openFileDialog_Skel.ShowDialog() == DialogResult.OK)
{
listBox_FilePath.Items.Clear();
foreach (var p in openFileDialog_Skel.FileNames)
listBox_FilePath.Items.Add(Path.GetFullPath(p));
label_Tip.Text = $"已选择 {listBox_FilePath.Items.Count} 个文件";
}
comboBox_Version.SelectedValue = Spine.Version.Auto;
}
private void button_Ok_Click(object sender, EventArgs e)
{
var version = (Spine.Version)comboBox_Version.SelectedValue;
if (listBox_FilePath.Items.Count <= 0)
var items = skelFileListBox.Items;
if (items.Count <= 0)
{
MessageBox.Show("未选择任何文件", "错误信息", MessageBoxButtons.OK, MessageBoxIcon.Information);
MessageBox.Info("未选择任何文件");
return;
}
foreach (string p in listBox_FilePath.Items)
foreach (string p in items)
{
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, items.Cast<string>().ToArray());
DialogResult = DialogResult.OK;
}
@@ -77,4 +63,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

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

View File

@@ -31,29 +31,24 @@
System.ComponentModel.ComponentResourceManager resources = new System.ComponentModel.ComponentResourceManager(typeof(ConvertFileFormatDialog));
panel = new Panel();
tableLayoutPanel1 = new TableLayoutPanel();
comboBox_TargetVersion = new ComboBox();
flowLayoutPanel_TargetFormat = new FlowLayoutPanel();
radioButton_BinaryTarget = new RadioButton();
radioButton_JsonTarget = new RadioButton();
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();
listBox_FilePath = new ListBox();
button_SelectSkel = new Button();
label_Tip = new Label();
label2 = new Label();
flowLayoutPanel_SourceFormat = new FlowLayoutPanel();
radioButton_BinarySource = new RadioButton();
radioButton_JsonSource = new RadioButton();
skelFileListBox = new SpineViewer.Controls.SkelFileListBox();
openFileDialog_Skel = new OpenFileDialog();
panel.SuspendLayout();
tableLayoutPanel1.SuspendLayout();
flowLayoutPanel_TargetFormat.SuspendLayout();
tableLayoutPanel2.SuspendLayout();
flowLayoutPanel_SourceFormat.SuspendLayout();
SuspendLayout();
//
// panel
@@ -63,7 +58,7 @@
panel.Location = new Point(0, 0);
panel.Name = "panel";
panel.Padding = new Padding(50, 15, 50, 10);
panel.Size = new Size(1039, 530);
panel.Size = new Size(1051, 538);
panel.TabIndex = 2;
//
// tableLayoutPanel1
@@ -71,40 +66,48 @@
tableLayoutPanel1.ColumnCount = 2;
tableLayoutPanel1.ColumnStyles.Add(new ColumnStyle());
tableLayoutPanel1.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 100F));
tableLayoutPanel1.Controls.Add(flowLayoutPanel_TargetFormat, 1, 5);
tableLayoutPanel1.Controls.Add(label1, 0, 4);
tableLayoutPanel1.Controls.Add(comboBox_TargetVersion, 1, 3);
tableLayoutPanel1.Controls.Add(flowLayoutPanel_TargetFormat, 1, 4);
tableLayoutPanel1.Controls.Add(label1, 0, 3);
tableLayoutPanel1.Controls.Add(label4, 0, 0);
tableLayoutPanel1.Controls.Add(label3, 0, 3);
tableLayoutPanel1.Controls.Add(comboBox_Version, 1, 3);
tableLayoutPanel1.Controls.Add(tableLayoutPanel2, 0, 6);
tableLayoutPanel1.Controls.Add(listBox_FilePath, 0, 2);
tableLayoutPanel1.Controls.Add(button_SelectSkel, 0, 1);
tableLayoutPanel1.Controls.Add(label_Tip, 1, 1);
tableLayoutPanel1.Controls.Add(label2, 0, 5);
tableLayoutPanel1.Controls.Add(flowLayoutPanel_SourceFormat, 1, 4);
tableLayoutPanel1.Controls.Add(label3, 0, 2);
tableLayoutPanel1.Controls.Add(comboBox_SourceVersion, 1, 2);
tableLayoutPanel1.Controls.Add(tableLayoutPanel2, 0, 5);
tableLayoutPanel1.Controls.Add(label2, 0, 4);
tableLayoutPanel1.Controls.Add(skelFileListBox, 0, 1);
tableLayoutPanel1.Dock = DockStyle.Fill;
tableLayoutPanel1.Location = new Point(50, 15);
tableLayoutPanel1.Name = "tableLayoutPanel1";
tableLayoutPanel1.RowCount = 7;
tableLayoutPanel1.RowCount = 6;
tableLayoutPanel1.RowStyles.Add(new RowStyle());
tableLayoutPanel1.RowStyles.Add(new RowStyle(SizeType.Percent, 100F));
tableLayoutPanel1.RowStyles.Add(new RowStyle());
tableLayoutPanel1.RowStyles.Add(new RowStyle());
tableLayoutPanel1.RowStyles.Add(new RowStyle());
tableLayoutPanel1.RowStyles.Add(new RowStyle());
tableLayoutPanel1.RowStyles.Add(new RowStyle());
tableLayoutPanel1.RowStyles.Add(new RowStyle());
tableLayoutPanel1.RowStyles.Add(new RowStyle());
tableLayoutPanel1.Size = new Size(939, 505);
tableLayoutPanel1.Size = new Size(951, 513);
tableLayoutPanel1.TabIndex = 1;
//
// comboBox_TargetVersion
//
comboBox_TargetVersion.Anchor = AnchorStyles.Left;
comboBox_TargetVersion.DropDownStyle = ComboBoxStyle.DropDownList;
comboBox_TargetVersion.FormattingEnabled = true;
comboBox_TargetVersion.Location = new Point(95, 365);
comboBox_TargetVersion.Name = "comboBox_TargetVersion";
comboBox_TargetVersion.Size = new Size(182, 32);
comboBox_TargetVersion.Sorted = true;
comboBox_TargetVersion.TabIndex = 21;
//
// flowLayoutPanel_TargetFormat
//
flowLayoutPanel_TargetFormat.AutoSize = true;
flowLayoutPanel_TargetFormat.Controls.Add(radioButton_BinaryTarget);
flowLayoutPanel_TargetFormat.Controls.Add(radioButton_JsonTarget);
flowLayoutPanel_TargetFormat.Dock = DockStyle.Fill;
flowLayoutPanel_TargetFormat.Location = new Point(146, 381);
flowLayoutPanel_TargetFormat.Location = new Point(95, 403);
flowLayoutPanel_TargetFormat.Name = "flowLayoutPanel_TargetFormat";
flowLayoutPanel_TargetFormat.Size = new Size(790, 34);
flowLayoutPanel_TargetFormat.Size = new Size(853, 34);
flowLayoutPanel_TargetFormat.TabIndex = 19;
//
// radioButton_BinaryTarget
@@ -116,7 +119,6 @@
radioButton_BinaryTarget.TabIndex = 17;
radioButton_BinaryTarget.Text = "二进制 (*.skel)";
radioButton_BinaryTarget.UseVisualStyleBackColor = true;
radioButton_BinaryTarget.CheckedChanged += radioButton_Target_CheckedChanged;
//
// radioButton_JsonTarget
//
@@ -129,17 +131,16 @@
radioButton_JsonTarget.TabStop = true;
radioButton_JsonTarget.Text = "文本 (*.json)";
radioButton_JsonTarget.UseVisualStyleBackColor = true;
radioButton_JsonTarget.CheckedChanged += radioButton_Target_CheckedChanged;
//
// label1
//
label1.Anchor = AnchorStyles.Right;
label1.AutoSize = true;
label1.Location = new Point(72, 346);
label1.Location = new Point(3, 369);
label1.Name = "label1";
label1.Size = new Size(68, 24);
label1.Size = new Size(86, 24);
label1.TabIndex = 15;
label1.Text = "源格式:";
label1.Text = "目标版本:";
//
// label4
//
@@ -149,7 +150,7 @@
label4.Location = new Point(15, 15);
label4.Margin = new Padding(15);
label4.Name = "label4";
label4.Size = new Size(909, 24);
label4.Size = new Size(921, 24);
label4.TabIndex = 14;
label4.Text = "说明:将在每个文件同级目录下生成目标格式后缀的文件,会覆盖已存在文件";
label4.TextAlign = ContentAlignment.MiddleCenter;
@@ -158,22 +159,22 @@
//
label3.Anchor = AnchorStyles.Right;
label3.AutoSize = true;
label3.Location = new Point(72, 307);
label3.Location = new Point(21, 331);
label3.Name = "label3";
label3.Size = new Size(68, 24);
label3.TabIndex = 12;
label3.Text = "源版本:";
//
// comboBox_Version
// comboBox_SourceVersion
//
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(95, 327);
comboBox_SourceVersion.Name = "comboBox_SourceVersion";
comboBox_SourceVersion.Size = new Size(182, 32);
comboBox_SourceVersion.Sorted = true;
comboBox_SourceVersion.TabIndex = 13;
//
// tableLayoutPanel2
//
@@ -185,18 +186,19 @@
tableLayoutPanel2.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 50F));
tableLayoutPanel2.Controls.Add(button_Ok, 0, 0);
tableLayoutPanel2.Controls.Add(button_Cancel, 1, 0);
tableLayoutPanel2.Dock = DockStyle.Bottom;
tableLayoutPanel2.Location = new Point(3, 462);
tableLayoutPanel2.Dock = DockStyle.Fill;
tableLayoutPanel2.Location = new Point(3, 470);
tableLayoutPanel2.Margin = new Padding(3, 30, 3, 3);
tableLayoutPanel2.Name = "tableLayoutPanel2";
tableLayoutPanel2.RowCount = 1;
tableLayoutPanel2.RowStyles.Add(new RowStyle());
tableLayoutPanel2.Size = new Size(933, 40);
tableLayoutPanel2.Size = new Size(945, 40);
tableLayoutPanel2.TabIndex = 11;
//
// button_Ok
//
button_Ok.Anchor = AnchorStyles.Bottom | AnchorStyles.Right;
button_Ok.Location = new Point(324, 3);
button_Ok.Location = new Point(330, 3);
button_Ok.Margin = new Padding(3, 3, 30, 3);
button_Ok.Name = "button_Ok";
button_Ok.Size = new Size(112, 34);
@@ -208,7 +210,7 @@
// button_Cancel
//
button_Cancel.Anchor = AnchorStyles.Bottom | AnchorStyles.Left;
button_Cancel.Location = new Point(496, 3);
button_Cancel.Location = new Point(502, 3);
button_Cancel.Margin = new Padding(30, 3, 3, 3);
button_Cancel.Name = "button_Cancel";
button_Cancel.Size = new Size(112, 34);
@@ -217,84 +219,24 @@
button_Cancel.UseVisualStyleBackColor = true;
button_Cancel.Click += button_Cancel_Click;
//
// listBox_FilePath
//
tableLayoutPanel1.SetColumnSpan(listBox_FilePath, 2);
listBox_FilePath.Dock = DockStyle.Fill;
listBox_FilePath.FormattingEnabled = true;
listBox_FilePath.HorizontalScrollbar = true;
listBox_FilePath.ItemHeight = 24;
listBox_FilePath.Location = new Point(3, 97);
listBox_FilePath.Name = "listBox_FilePath";
listBox_FilePath.Size = new Size(933, 200);
listBox_FilePath.TabIndex = 2;
//
// button_SelectSkel
//
button_SelectSkel.Anchor = AnchorStyles.None;
button_SelectSkel.Location = new Point(3, 57);
button_SelectSkel.Name = "button_SelectSkel";
button_SelectSkel.Size = new Size(137, 34);
button_SelectSkel.TabIndex = 1;
button_SelectSkel.Text = "选择文件...";
button_SelectSkel.UseVisualStyleBackColor = true;
button_SelectSkel.Click += button_SelectSkel_Click;
//
// label_Tip
//
label_Tip.AutoSize = true;
label_Tip.Dock = DockStyle.Fill;
label_Tip.Location = new Point(146, 54);
label_Tip.Name = "label_Tip";
label_Tip.Size = new Size(790, 40);
label_Tip.TabIndex = 0;
label_Tip.Text = "已选择 0 个文件";
label_Tip.TextAlign = ContentAlignment.MiddleLeft;
//
// label2
//
label2.Anchor = AnchorStyles.Right;
label2.AutoSize = true;
label2.Location = new Point(54, 386);
label2.Location = new Point(3, 408);
label2.Name = "label2";
label2.Size = new Size(86, 24);
label2.TabIndex = 16;
label2.Text = "目标格式:";
//
// flowLayoutPanel_SourceFormat
// skelFileListBox
//
flowLayoutPanel_SourceFormat.AutoSize = true;
flowLayoutPanel_SourceFormat.Controls.Add(radioButton_BinarySource);
flowLayoutPanel_SourceFormat.Controls.Add(radioButton_JsonSource);
flowLayoutPanel_SourceFormat.Dock = DockStyle.Fill;
flowLayoutPanel_SourceFormat.Location = new Point(146, 341);
flowLayoutPanel_SourceFormat.Name = "flowLayoutPanel_SourceFormat";
flowLayoutPanel_SourceFormat.Size = new Size(790, 34);
flowLayoutPanel_SourceFormat.TabIndex = 18;
//
// radioButton_BinarySource
//
radioButton_BinarySource.AutoSize = true;
radioButton_BinarySource.Checked = true;
radioButton_BinarySource.Location = new Point(3, 3);
radioButton_BinarySource.Name = "radioButton_BinarySource";
radioButton_BinarySource.Size = new Size(151, 28);
radioButton_BinarySource.TabIndex = 17;
radioButton_BinarySource.TabStop = true;
radioButton_BinarySource.Text = "二进制 (*.skel)";
radioButton_BinarySource.UseVisualStyleBackColor = true;
radioButton_BinarySource.CheckedChanged += radioButton_Source_CheckedChanged;
//
// radioButton_JsonSource
//
radioButton_JsonSource.AutoSize = true;
radioButton_JsonSource.Location = new Point(160, 3);
radioButton_JsonSource.Name = "radioButton_JsonSource";
radioButton_JsonSource.Size = new Size(135, 28);
radioButton_JsonSource.TabIndex = 18;
radioButton_JsonSource.Text = "文本 (*.json)";
radioButton_JsonSource.UseVisualStyleBackColor = true;
radioButton_JsonSource.CheckedChanged += radioButton_Source_CheckedChanged;
tableLayoutPanel1.SetColumnSpan(skelFileListBox, 2);
skelFileListBox.Dock = DockStyle.Fill;
skelFileListBox.Location = new Point(3, 57);
skelFileListBox.Name = "skelFileListBox";
skelFileListBox.Size = new Size(945, 264);
skelFileListBox.TabIndex = 20;
//
// openFileDialog_Skel
//
@@ -310,7 +252,7 @@
AutoScaleDimensions = new SizeF(11F, 24F);
AutoScaleMode = AutoScaleMode.Font;
CancelButton = button_Cancel;
ClientSize = new Size(1039, 530);
ClientSize = new Size(1051, 538);
Controls.Add(panel);
FormBorderStyle = FormBorderStyle.FixedDialog;
Icon = (Icon)resources.GetObject("$this.Icon");
@@ -326,8 +268,6 @@
flowLayoutPanel_TargetFormat.ResumeLayout(false);
flowLayoutPanel_TargetFormat.PerformLayout();
tableLayoutPanel2.ResumeLayout(false);
flowLayoutPanel_SourceFormat.ResumeLayout(false);
flowLayoutPanel_SourceFormat.PerformLayout();
ResumeLayout(false);
}
@@ -337,21 +277,17 @@
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;
private ListBox listBox_FilePath;
private Button button_SelectSkel;
private Label label_Tip;
private OpenFileDialog openFileDialog_Skel;
private Label label1;
private Label label2;
private RadioButton radioButton_BinarySource;
private FlowLayoutPanel flowLayoutPanel_SourceFormat;
private RadioButton radioButton_JsonSource;
private FlowLayoutPanel flowLayoutPanel_TargetFormat;
private RadioButton radioButton_BinaryTarget;
private RadioButton radioButton_JsonTarget;
private Controls.SkelFileListBox skelFileListBox;
private ComboBox comboBox_TargetVersion;
}
}

View File

@@ -13,60 +13,65 @@ namespace SpineViewer.Dialogs
{
public partial class ConvertFileFormatDialog : Form
{
public string[] SkelPaths { get; private set; }
public Spine.Version Version { get; private set; }
public bool ConvertToJson { get; private set; }
/// <summary>
/// 对话框结果, 取消时为 null
/// </summary>
public ConvertFileFormatDialogResult Result { 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;
}
private void button_SelectSkel_Click(object sender, EventArgs e)
{
if (openFileDialog_Skel.ShowDialog() == DialogResult.OK)
{
listBox_FilePath.Items.Clear();
foreach (var p in openFileDialog_Skel.FileNames)
listBox_FilePath.Items.Add(Path.GetFullPath(p));
label_Tip.Text = $"已选择 {listBox_FilePath.Items.Count} 个文件";
}
comboBox_SourceVersion.DataSource = VersionHelper.Names.ToList();
comboBox_SourceVersion.DisplayMember = "Value";
comboBox_SourceVersion.ValueMember = "Key";
comboBox_SourceVersion.SelectedValue = Spine.Version.Auto;
// 目标版本不包含自动
var versionsWithoutAuto = VersionHelper.Names.ToDictionary();
versionsWithoutAuto.Remove(Spine.Version.Auto);
comboBox_TargetVersion.DataSource = versionsWithoutAuto.ToList();
comboBox_TargetVersion.DisplayMember = "Value";
comboBox_TargetVersion.ValueMember = "Key";
comboBox_TargetVersion.SelectedValue = Spine.Version.V38;
}
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_TargetVersion.SelectedValue;
var jsonTarget = radioButton_JsonTarget.Checked;
if (listBox_FilePath.Items.Count <= 0)
var items = skelFileListBox.Items;
if (items.Count <= 0)
{
MessageBox.Show("未选择任何文件", "错误信息", MessageBoxButtons.OK, MessageBoxIcon.Information);
MessageBox.Info("未选择任何文件");
return;
}
foreach (string p in listBox_FilePath.Items)
foreach (string p in items)
{
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 (sourceVersion != Spine.Version.Auto && !SkeletonConverter.ImplementedVersions.Contains(sourceVersion))
{
MessageBox.Show($"{version.String()} 版本尚未实现(咕咕咕~", "错误信息", MessageBoxButtons.OK, MessageBoxIcon.Information);
MessageBox.Info($"{sourceVersion.GetName()} 版本尚未实现(咕咕咕~");
return;
}
SkelPaths = listBox_FilePath.Items.Cast<string>().ToArray();
Version = version;
ConvertToJson = radioButton_BinarySource.Checked;
if (!SkeletonConverter.ImplementedVersions.Contains(targetVersion))
{
MessageBox.Info($"{targetVersion.GetName()} 版本尚未实现(咕咕咕~");
return;
}
Result = new(items.Cast<string>().ToArray(), sourceVersion, targetVersion, jsonTarget);
DialogResult = DialogResult.OK;
}
@@ -74,21 +79,31 @@ namespace SpineViewer.Dialogs
{
DialogResult = DialogResult.Cancel;
}
}
private void radioButton_Source_CheckedChanged(object sender, EventArgs e)
{
if (radioButton_BinarySource.Checked)
radioButton_JsonTarget.Checked = true;
else
radioButton_BinaryTarget.Checked = true;
}
/// <summary>
/// 文件格式转换对话框结果包装类
/// </summary>
public class ConvertFileFormatDialogResult(string[] skelPaths, Spine.Version sourceVersion, Spine.Version targetVersion, bool jsonTarget)
{
/// <summary>
/// 骨骼文件路径列表
/// </summary>
public string[] SkelPaths => skelPaths;
private void radioButton_Target_CheckedChanged(object sender, EventArgs e)
{
if (radioButton_BinaryTarget.Checked)
radioButton_JsonSource.Checked = true;
else
radioButton_BinarySource.Checked = true;
}
/// <summary>
/// 源版本
/// </summary>
public Spine.Version SourceVersion => sourceVersion;
/// <summary>
/// 目标版本
/// </summary>
public Spine.Version TargetVersion => targetVersion;
/// <summary>
/// 目标格式是否为 Json
/// </summary>
public bool JsonTarget => jsonTarget;
}
}

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

@@ -0,0 +1,156 @@
namespace SpineViewer.Dialogs
{
partial class ExportDialog
{
/// <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(ExportDialog));
panel1 = new Panel();
tableLayoutPanel1 = new TableLayoutPanel();
propertyGrid_ExportArgs = new PropertyGrid();
tableLayoutPanel2 = new TableLayoutPanel();
button_Ok = new Button();
button_Cancel = new Button();
panel1.SuspendLayout();
tableLayoutPanel1.SuspendLayout();
tableLayoutPanel2.SuspendLayout();
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(710, 698);
panel1.TabIndex = 2;
//
// tableLayoutPanel1
//
tableLayoutPanel1.AutoSize = true;
tableLayoutPanel1.ColumnCount = 1;
tableLayoutPanel1.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 100F));
tableLayoutPanel1.Controls.Add(propertyGrid_ExportArgs, 0, 0);
tableLayoutPanel1.Controls.Add(tableLayoutPanel2, 0, 1);
tableLayoutPanel1.Dock = DockStyle.Fill;
tableLayoutPanel1.Location = new Point(50, 15);
tableLayoutPanel1.Name = "tableLayoutPanel1";
tableLayoutPanel1.RowCount = 2;
tableLayoutPanel1.RowStyles.Add(new RowStyle(SizeType.Percent, 100F));
tableLayoutPanel1.RowStyles.Add(new RowStyle());
tableLayoutPanel1.RowStyles.Add(new RowStyle(SizeType.Absolute, 20F));
tableLayoutPanel1.RowStyles.Add(new RowStyle(SizeType.Absolute, 20F));
tableLayoutPanel1.Size = new Size(610, 673);
tableLayoutPanel1.TabIndex = 0;
//
// propertyGrid_ExportArgs
//
propertyGrid_ExportArgs.Dock = DockStyle.Fill;
propertyGrid_ExportArgs.Location = new Point(3, 3);
propertyGrid_ExportArgs.Name = "propertyGrid_ExportArgs";
propertyGrid_ExportArgs.PropertySort = PropertySort.Categorized;
propertyGrid_ExportArgs.Size = new Size(604, 594);
propertyGrid_ExportArgs.TabIndex = 1;
propertyGrid_ExportArgs.ToolbarVisible = false;
//
// tableLayoutPanel2
//
tableLayoutPanel2.AutoSize = true;
tableLayoutPanel2.AutoSizeMode = AutoSizeMode.GrowAndShrink;
tableLayoutPanel2.ColumnCount = 2;
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, 630);
tableLayoutPanel2.Margin = new Padding(3, 30, 3, 3);
tableLayoutPanel2.Name = "tableLayoutPanel2";
tableLayoutPanel2.RowCount = 1;
tableLayoutPanel2.RowStyles.Add(new RowStyle());
tableLayoutPanel2.Size = new Size(604, 40);
tableLayoutPanel2.TabIndex = 10;
//
// button_Ok
//
button_Ok.Anchor = AnchorStyles.Bottom | AnchorStyles.Right;
button_Ok.Location = new Point(160, 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(332, 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;
//
// ExportDialog
//
AcceptButton = button_Ok;
AutoScaleDimensions = new SizeF(11F, 24F);
AutoScaleMode = AutoScaleMode.Font;
CancelButton = button_Cancel;
ClientSize = new Size(710, 698);
Controls.Add(panel1);
FormBorderStyle = FormBorderStyle.FixedDialog;
Icon = (Icon)resources.GetObject("$this.Icon");
MaximizeBox = false;
MinimizeBox = false;
Name = "ExportDialog";
ShowInTaskbar = false;
StartPosition = FormStartPosition.CenterScreen;
Text = "导出参数";
panel1.ResumeLayout(false);
panel1.PerformLayout();
tableLayoutPanel1.ResumeLayout(false);
tableLayoutPanel1.PerformLayout();
tableLayoutPanel2.ResumeLayout(false);
ResumeLayout(false);
}
#endregion
private Panel panel1;
private TableLayoutPanel tableLayoutPanel1;
private TableLayoutPanel tableLayoutPanel2;
private Button button_Ok;
private Button button_Cancel;
private PropertyGrid propertyGrid_ExportArgs;
}
}

View File

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

View File

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

View File

@@ -1,269 +0,0 @@
namespace SpineViewer.Dialogs
{
partial class ExportPngDialog
{
/// <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(ExportPngDialog));
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_Duration = new NumericUpDown();
numericUpDown_Fps = new NumericUpDown();
folderBrowserDialog = new FolderBrowserDialog();
panel1.SuspendLayout();
tableLayoutPanel1.SuspendLayout();
tableLayoutPanel2.SuspendLayout();
((System.ComponentModel.ISupportInitialize)numericUpDown_Duration).BeginInit();
((System.ComponentModel.ISupportInitialize)numericUpDown_Fps).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 = 1;
//
// 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_Duration, 1, 2);
tableLayoutPanel1.Controls.Add(numericUpDown_Fps, 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(57, 100);
label2.Name = "label2";
label2.Size = new Size(50, 24);
label2.TabIndex = 1;
label2.Text = "时长:";
//
// label3
//
label3.Anchor = AnchorStyles.Right;
label3.AutoSize = true;
label3.Location = new Point(57, 136);
label3.Name = "label3";
label3.Size = new Size(50, 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_Duration
//
numericUpDown_Duration.Anchor = AnchorStyles.Left;
numericUpDown_Duration.DecimalPlaces = 3;
numericUpDown_Duration.Location = new Point(113, 97);
numericUpDown_Duration.Maximum = new decimal(new int[] { 300, 0, 0, 0 });
numericUpDown_Duration.Name = "numericUpDown_Duration";
numericUpDown_Duration.Size = new Size(180, 30);
numericUpDown_Duration.TabIndex = 12;
numericUpDown_Duration.TextAlign = HorizontalAlignment.Right;
numericUpDown_Duration.Value = new decimal(new int[] { 1, 0, 0, 0 });
//
// numericUpDown_Fps
//
numericUpDown_Fps.Anchor = AnchorStyles.Left;
numericUpDown_Fps.Location = new Point(113, 133);
numericUpDown_Fps.Maximum = new decimal(new int[] { 300, 0, 0, 0 });
numericUpDown_Fps.Minimum = new decimal(new int[] { 1, 0, 0, 0 });
numericUpDown_Fps.Name = "numericUpDown_Fps";
numericUpDown_Fps.Size = new Size(180, 30);
numericUpDown_Fps.TabIndex = 13;
numericUpDown_Fps.TextAlign = HorizontalAlignment.Right;
numericUpDown_Fps.Value = new decimal(new int[] { 60, 0, 0, 0 });
//
// folderBrowserDialog
//
folderBrowserDialog.AddToRecent = false;
//
// ExportPngDialog
//
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 = "ExportPngDialog";
ShowInTaskbar = false;
StartPosition = FormStartPosition.CenterScreen;
Text = "导出PNG序列";
panel1.ResumeLayout(false);
panel1.PerformLayout();
tableLayoutPanel1.ResumeLayout(false);
tableLayoutPanel1.PerformLayout();
tableLayoutPanel2.ResumeLayout(false);
((System.ComponentModel.ISupportInitialize)numericUpDown_Duration).EndInit();
((System.ComponentModel.ISupportInitialize)numericUpDown_Fps).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_Duration;
private NumericUpDown numericUpDown_Fps;
private FolderBrowserDialog folderBrowserDialog;
}
}

View File

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

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

@@ -0,0 +1,107 @@
using SpineViewer.Spine;
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Drawing.Design;
using System.Drawing.Imaging;
using System.Linq;
using System.Reflection;
using System.Text;
using System.Threading.Tasks;
namespace SpineViewer.Exporter
{
/// <summary>
/// 导出参数基类
/// </summary>
public abstract class ExportArgs
{
/// <summary>
/// 实现类缓存
/// </summary>
private static readonly Dictionary<ExportType, Type> ImplementationTypes = [];
static ExportArgs()
{
var impTypes = Assembly.GetExecutingAssembly().GetTypes().Where(t => typeof(ExportArgs).IsAssignableFrom(t) && !t.IsAbstract);
foreach (var type in impTypes)
{
var attr = type.GetCustomAttribute<ExportImplementationAttribute>();
if (attr is not null)
{
if (ImplementationTypes.ContainsKey(attr.ExportType))
throw new InvalidOperationException($"Multiple implementations found: {attr.ExportType}");
ImplementationTypes[attr.ExportType] = type;
}
}
Program.Logger.Debug("Find export args implementations: [{}]", string.Join(", ", ImplementationTypes.Keys));
}
/// <summary>
/// 创建指定类型导出参数
/// </summary>
public static ExportArgs New(ExportType exportType, Size resolution, SFML.Graphics.View view, bool renderSelectedOnly)
{
if (!ImplementationTypes.TryGetValue(exportType, out var type))
{
throw new NotImplementedException($"Not implemented type: {exportType}");
}
return (ExportArgs)Activator.CreateInstance(type, resolution, view, renderSelectedOnly);
}
public ExportArgs(Size resolution, SFML.Graphics.View view, bool renderSelectedOnly)
{
Resolution = resolution;
View = view;
RenderSelectedOnly = renderSelectedOnly;
}
/// <summary>
/// 输出文件夹
/// </summary>
[Editor(typeof(FolderNameEditor), typeof(UITypeEditor))]
[Category("导出"), DisplayName("输出文件夹"), Description("逐个导出时可以留空,将逐个导出到模型自身所在目录")]
public string? OutputDir { get; set; } = null;
/// <summary>
/// 导出单个
/// </summary>
[Category("导出"), DisplayName("导出单个"), Description("是否将模型在同一个画面上导出单个文件,否则逐个导出模型")]
public bool ExportSingle { get; set; } = false;
/// <summary>
/// 画面分辨率
/// </summary>
[TypeConverter(typeof(SizeConverter))]
[Category("导出"), DisplayName("分辨率"), Description("画面的宽高像素大小,请在预览画面参数面板进行调整")]
public Size Resolution { get; }
/// <summary>
/// 渲染视窗
/// </summary>
[Category("导出"), DisplayName("视图"), Description("画面的视图参数,请在预览画面参数面板进行调整")]
public SFML.Graphics.View View { get; }
/// <summary>
/// 是否仅渲染选中
/// </summary>
[Category("导出"), DisplayName("仅渲染选中"), Description("是否仅导出选中的模型,请在预览画面参数面板进行调整")]
public bool RenderSelectedOnly { get; }
/// <summary>
/// 检查参数是否合法并规范化参数值, 否则返回用户错误原因
/// </summary>
public virtual string? Validate()
{
if (!string.IsNullOrEmpty(OutputDir) && File.Exists(OutputDir))
return "输出文件夹无效";
if (!string.IsNullOrEmpty(OutputDir) && !Directory.Exists(OutputDir))
return $"文件夹 {OutputDir} 不存在";
if (ExportSingle && string.IsNullOrEmpty(OutputDir))
return "导出单个时必须提供输出文件夹";
OutputDir = string.IsNullOrEmpty(OutputDir) ? null : Path.GetFullPath(OutputDir);
return null;
}
}
}

View File

@@ -0,0 +1,140 @@
using FFMpegCore.Pipes;
using System;
using System.Collections.Generic;
using System.Drawing.Imaging;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace SpineViewer.Exporter
{
/// <summary>
/// 导出类型
/// </summary>
public enum ExportType
{
Frame,
FrameSequence,
GIF,
MKV,
MP4,
MOV,
WebM
}
/// <summary>
/// 导出实现类标记
/// </summary>
[AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = false)]
public class ExportImplementationAttribute : Attribute
{
public ExportType ExportType { get; }
public ExportImplementationAttribute(ExportType exportType)
{
ExportType = exportType;
}
}
/// <summary>
/// SFML.Graphics.Image 帧对象包装类
/// </summary>
public class SFMLImageVideoFrame(SFML.Graphics.Image image) : IVideoFrame, IDisposable
{
public int Width => (int)image.Size.X;
public int Height => (int)image.Size.Y;
public string Format => "rgba";
public void Serialize(Stream pipe) => pipe.Write(image.Pixels);
public async Task SerializeAsync(Stream pipe, CancellationToken token) => await pipe.WriteAsync(image.Pixels, token);
public void Dispose() => image.Dispose();
/// <summary>
/// Save the contents of the image to a file
/// </summary>
/// <param name="filename">Path of the file to save (overwritten if already exist)</param>
/// <returns>True if saving was successful</returns>
public bool SaveToFile(string filename) => image.SaveToFile(filename);
/// <summary>
/// Save the image to a buffer in memory The format of the image must be specified.
/// The supported image formats are bmp, png, tga and jpg. This function fails if
/// the image is empty, or if the format was invalid.
/// </summary>
/// <param name="output">Byte array filled with encoded data</param>
/// <param name="format">Encoding format to use</param>
/// <returns>True if saving was successful</returns>
public bool SaveToMemory(out byte[] output, string format) => image.SaveToMemory(out output, format);
/// <summary>
/// 获取 Winforms Bitmap 对象
/// </summary>
public Bitmap CopyToBitmap()
{
image.SaveToMemory(out var imgBuffer, "bmp");
using var stream = new MemoryStream(imgBuffer);
return new(new Bitmap(stream)); // 必须重复构造一个副本才能摆脱对流的依赖, 否则之后使用会报错
}
}
/// <summary>
/// 为帧导出创建的辅助类
/// </summary>
public static class ExportHelper
{
/// <summary>
/// 根据 Bitmap 文件格式获取合适的文件后缀
/// </summary>
public static string GetSuffix(this ImageFormat imageFormat)
{
if (imageFormat == ImageFormat.Icon) return ".ico";
else if (imageFormat == ImageFormat.Exif) return ".jpeg";
else return $".{imageFormat.ToString().ToLower()}";
}
#region
/// <summary>
/// 获取某个包围盒下合适的视图
/// </summary>
public static SFML.Graphics.View GetView(this RectangleF bounds, Size resolution, Padding padding)
=> bounds.GetView((uint)resolution.Width, (uint)resolution.Height, (uint)padding.Left, (uint)padding.Right, (uint)padding.Top, (uint)padding.Bottom);
/// <summary>
/// 获取某个包围盒下合适的视图
/// </summary>
public static SFML.Graphics.View GetView(this RectangleF bounds, uint width, uint height, Padding padding)
=> bounds.GetView(width, height, (uint)padding.Left, (uint)padding.Right, (uint)padding.Top, (uint)padding.Bottom);
/// <summary>
/// 获取某个包围盒下合适的视图
/// </summary>
public static SFML.Graphics.View GetView(this RectangleF bounds, Size resolution, uint paddingL = 1, uint paddingR = 1, uint paddingT = 1, uint paddingB = 1)
=> bounds.GetView((uint)resolution.Width, (uint)resolution.Height, paddingL, paddingR, paddingT, paddingB);
/// <summary>
/// 获取某个包围盒下合适的视图
/// </summary>
public static SFML.Graphics.View GetView(this RectangleF bounds, uint width, uint height, uint paddingL = 1, uint paddingR = 1, uint paddingT = 1, uint paddingB = 1)
{
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 + (paddingL - (float)paddingR) * scale;
var y = bounds.Y + bounds.Height / 2 + (paddingT - (float)paddingB) * scale;
var viewX = width * scale;
var viewY = height * scale;
return new(new(x, y), new(viewX, -viewY));
}
#endregion
}
}

View File

@@ -0,0 +1,126 @@
using SpineViewer.Spine;
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Reflection;
using System.Text;
using System.Threading.Tasks;
using static System.Windows.Forms.VisualStyles.VisualStyleElement.StartPanel;
namespace SpineViewer.Exporter
{
/// <summary>
/// 导出器基类
/// </summary>
public abstract class Exporter
{
/// <summary>
/// 实现类缓存
/// </summary>
private static readonly Dictionary<ExportType, Type> ImplementationTypes = [];
static Exporter()
{
var impTypes = Assembly.GetExecutingAssembly().GetTypes().Where(t => typeof(Exporter).IsAssignableFrom(t) && !t.IsAbstract);
foreach (var type in impTypes)
{
var attr = type.GetCustomAttribute<ExportImplementationAttribute>();
if (attr is not null)
{
if (ImplementationTypes.ContainsKey(attr.ExportType))
throw new InvalidOperationException($"Multiple implementations found: {attr.ExportType}");
ImplementationTypes[attr.ExportType] = type;
}
}
Program.Logger.Debug("Find exporter implementations: [{}]", string.Join(", ", ImplementationTypes.Keys));
}
/// <summary>
/// 创建指定类型导出参数
/// </summary>
public static Exporter New(ExportType exportType, ExportArgs exportArgs)
{
if (!ImplementationTypes.TryGetValue(exportType, out var type))
{
throw new NotImplementedException($"Not implemented type: {exportType}");
}
return (Exporter)Activator.CreateInstance(type, exportArgs);
}
/// <summary>
/// 导出参数
/// </summary>
public ExportArgs ExportArgs { get; }
/// <summary>
/// 渲染目标
/// </summary>
private SFML.Graphics.RenderTexture tex;
/// <summary>
/// 可用于文件名的时间戳字符串
/// </summary>
protected readonly string timestamp;
public Exporter(ExportArgs exportArgs)
{
ExportArgs = exportArgs;
timestamp = DateTime.Now.ToString("yyMMddHHmmss");
}
/// <summary>
/// 获取单个模型的单帧画面
/// </summary>
protected SFMLImageVideoFrame GetFrame(Spine.Spine spine)
{
tex.Clear(SFML.Graphics.Color.Transparent);
tex.Draw(spine);
tex.Display();
return new(tex.Texture.CopyToImage());
}
/// <summary>
/// 获取模型列表的单帧画面
/// </summary>
protected SFMLImageVideoFrame GetFrame(Spine.Spine[] spinesToRender)
{
tex.Clear(SFML.Graphics.Color.Transparent);
foreach (var spine in spinesToRender) tex.Draw(spine);
tex.Display();
return new(tex.Texture.CopyToImage());
}
/// <summary>
/// 每个模型在同一个画面进行导出
/// </summary>
protected abstract void ExportSingle(Spine.Spine[] spinesToRender, BackgroundWorker? worker = null);
/// <summary>
/// 每个模型独立导出
/// </summary>
protected abstract void ExportIndividual(Spine.Spine[] spinesToRender, BackgroundWorker? worker = null);
/// <summary>
/// 执行导出
/// </summary>
/// <param name="spines">要进行导出的 Spine 列表</param>
/// <param name="worker">用来执行该函数的 worker</param>
public virtual void Export(Spine.Spine[] spines, BackgroundWorker? worker = null)
{
var spinesToRender = spines.Where(sp => !ExportArgs.RenderSelectedOnly || sp.IsSelected).Reverse().ToArray();
// tex 必须临时创建, 防止出现跨线程的情况
using (tex = new SFML.Graphics.RenderTexture((uint)ExportArgs.Resolution.Width, (uint)ExportArgs.Resolution.Height))
{
tex.Clear(SFML.Graphics.Color.Transparent);
tex.SetView(ExportArgs.View);
if (ExportArgs.ExportSingle) ExportSingle(spinesToRender, worker);
else ExportIndividual(spinesToRender, worker);
}
tex = null;
Program.LogCurrentMemoryUsage();
}
}
}

View File

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

View File

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

View File

@@ -0,0 +1,30 @@
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace SpineViewer.Exporter.Implementations.ExportArgs
{
/// <summary>
/// 视频导出参数基类
/// </summary>
public abstract class VideoExportArgs : SpineViewer.Exporter.ExportArgs
{
public VideoExportArgs(Size resolution, SFML.Graphics.View view, bool renderSelectedOnly) : base(resolution, view, renderSelectedOnly) { }
/// <summary>
/// 导出时长
/// </summary>
[Category("视频参数"), DisplayName("时长"), Description("可以从模型列表查看动画时长")]
public float Duration { get => duration; set => duration = Math.Max(0, value); }
private float duration = 1;
/// <summary>
/// 帧率
/// </summary>
[Category("视频参数"), DisplayName("帧率"), Description("每秒画面数")]
public float FPS { get; set; } = 60;
}
}

View File

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

View File

@@ -0,0 +1,87 @@
using SpineViewer.Exporter.Implementations.ExportArgs;
using SpineViewer.Spine;
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace SpineViewer.Exporter.Implementations.Exporter
{
/// <summary>
/// 帧序列导出器
/// </summary>
[ExportImplementation(ExportType.FrameSequence)]
public class FrameSequenceExporter : VideoExporter
{
public FrameSequenceExporter(FrameSequenceExportArgs exportArgs) : base(exportArgs) { }
protected override void ExportSingle(Spine.Spine[] spinesToRender, BackgroundWorker? worker = null)
{
var args = (FrameSequenceExportArgs)ExportArgs;
// 导出单个时必定提供输出文件夹,
var saveDir = Path.Combine(args.OutputDir, $"frames_{timestamp}_{args.FPS:f0}");
Directory.CreateDirectory(saveDir);
int frameIdx = 0;
foreach (var frame in GetFrames(spinesToRender, worker))
{
var filename = $"frames_{timestamp}_{args.FPS:f0}_{frameIdx:d6}{args.FileSuffix}";
var savePath = Path.Combine(saveDir, filename);
try
{
frame.SaveToFile(savePath);
}
catch (Exception ex)
{
Program.Logger.Error(ex.ToString());
Program.Logger.Error("Failed to save frame {}", savePath);
}
finally
{
frame.Dispose();
}
frameIdx++;
}
}
protected override void ExportIndividual(Spine.Spine[] spinesToRender, BackgroundWorker? worker = null)
{
var args = (FrameSequenceExportArgs)ExportArgs;
foreach (var spine in spinesToRender)
{
if (worker?.CancellationPending == true) break; // 取消的日志在 GetFrames 里输出
// 如果提供了输出文件夹, 则全部导出到输出文件夹, 否则导出到各自的文件夹下
var subDir = $"{spine.Name}_{timestamp}_{args.FPS:f0}";
var saveDir = args.OutputDir is null ? Path.Combine(spine.AssetsDir, subDir) : Path.Combine(args.OutputDir, subDir);
Directory.CreateDirectory(saveDir);
int frameIdx = 0;
foreach (var frame in GetFrames(spine, worker))
{
var filename = $"{spine.Name}_{timestamp}_{args.FPS:f0}_{frameIdx:d6}{args.FileSuffix}";
var savePath = Path.Combine(saveDir, filename);
try
{
frame.SaveToFile(savePath);
}
catch (Exception ex)
{
Program.Logger.Error(ex.ToString());
Program.Logger.Error("Failed to save frame {} {}", savePath, spine.SkelPath);
}
finally
{
frame.Dispose();
}
frameIdx++;
}
}
}
}
}

View File

@@ -0,0 +1,76 @@
using SpineViewer.Exporter.Implementations.ExportArgs;
using SpineViewer.Spine;
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace SpineViewer.Exporter.Implementations.Exporter
{
/// <summary>
/// 视频导出基类
/// </summary>
public abstract class VideoExporter : SpineViewer.Exporter.Exporter
{
public VideoExporter(VideoExportArgs exportArgs) : base(exportArgs) { }
/// <summary>
/// 生成单个模型的帧序列
/// </summary>
protected IEnumerable<SFMLImageVideoFrame> GetFrames(Spine.Spine spine, BackgroundWorker? worker = null)
{
var args = (VideoExportArgs)ExportArgs;
float delta = 1f / args.FPS;
int total = 1 + (int)(args.Duration * args.FPS); // 至少导出 1 帧
worker?.ReportProgress(0, $"{spine.Name} 已处理 0/{total} 帧");
for (int i = 0; i < total; i++)
{
if (worker?.CancellationPending == true)
{
Program.Logger.Info("Export cancelled");
break;
}
var frame = GetFrame(spine);
spine.Update(delta);
worker?.ReportProgress((int)((i + 1) * 100.0) / total, $"{spine.Name} 已处理 {i + 1}/{total} 帧");
yield return frame;
}
}
/// <summary>
/// 生成多个模型的帧序列
/// </summary>
protected IEnumerable<SFMLImageVideoFrame> GetFrames(Spine.Spine[] spinesToRender, BackgroundWorker? worker = null)
{
var args = (VideoExportArgs)ExportArgs;
float delta = 1f / args.FPS;
int total = 1 + (int)(args.Duration * args.FPS); // 至少导出 1 帧
worker?.ReportProgress(0, $"已处理 0/{total} 帧");
for (int i = 0; i < total; i++)
{
if (worker?.CancellationPending == true)
{
Program.Logger.Info("Export cancelled");
break;
}
var frame = GetFrame(spinesToRender);
foreach (var spine in spinesToRender) spine.Update(delta);
worker?.ReportProgress((int)((i + 1) * 100.0) / total, $"已处理 {i + 1}/{total} 帧");
yield return frame;
}
}
public override void Export(Spine.Spine[] spines, BackgroundWorker? worker = null)
{
// 导出视频格式需要把模型时间都重置到 0
foreach (var spine in spines) spine.CurrentAnimation = spine.CurrentAnimation;
base.Export(spines, worker);
}
}
}

View File

@@ -0,0 +1,33 @@
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace SpineViewer.Exporter
{
public class SFMLImageFileSuffixConverter : StringConverter
{
private readonly string[] supportedFileSuffix = [".png", ".jpg", ".tga", ".bmp"];
public override bool GetStandardValuesSupported(ITypeDescriptorContext? context)
{
// 支持标准值列表
return true;
}
public override bool GetStandardValuesExclusive(ITypeDescriptorContext? context)
{
// 排他模式,只有下拉列表中的值可选
return true;
}
public override StandardValuesCollection? GetStandardValues(ITypeDescriptorContext? context)
{
return new StandardValuesCollection(supportedFileSuffix);
}
}
}

View File

@@ -36,10 +36,15 @@
toolStripMenuItem_BatchOpen = new ToolStripMenuItem();
toolStripSeparator1 = new ToolStripSeparator();
toolStripMenuItem_Export = new ToolStripMenuItem();
toolStripMenuItem_ExportFrame = new ToolStripMenuItem();
toolStripMenuItem_ExportFrameSequence = new ToolStripMenuItem();
toolStripMenuItem_ExportGif = new ToolStripMenuItem();
toolStripMenuItem_ExportMkv = new ToolStripMenuItem();
toolStripMenuItem_ExportMp4 = new ToolStripMenuItem();
toolStripMenuItem_ExportMov = new ToolStripMenuItem();
toolStripMenuItem_ExportWebm = new ToolStripMenuItem();
toolStripSeparator2 = new ToolStripSeparator();
toolStripMenuItem_Exit = new ToolStripMenuItem();
toolStripMenuItem_Function = new ToolStripMenuItem();
toolStripMenuItem_ResetAnimation = new ToolStripMenuItem();
toolStripMenuItem_Tool = new ToolStripMenuItem();
toolStripMenuItem_ConvertFileFormat = new ToolStripMenuItem();
toolStripMenuItem_Download = new ToolStripMenuItem();
@@ -91,10 +96,10 @@
//
menuStrip.BackColor = SystemColors.Control;
menuStrip.ImageScalingSize = new Size(24, 24);
menuStrip.Items.AddRange(new ToolStripItem[] { toolStripMenuItem_File, toolStripMenuItem_Function, toolStripMenuItem_Tool, toolStripMenuItem_Download, toolStripMenuItem_Help });
menuStrip.Items.AddRange(new ToolStripItem[] { toolStripMenuItem_File, toolStripMenuItem_Tool, toolStripMenuItem_Download, toolStripMenuItem_Help });
menuStrip.Location = new Point(0, 0);
menuStrip.Name = "menuStrip";
menuStrip.Size = new Size(1741, 32);
menuStrip.Size = new Size(1748, 32);
menuStrip.TabIndex = 0;
menuStrip.Text = "菜单";
//
@@ -127,11 +132,54 @@
//
// toolStripMenuItem_Export
//
toolStripMenuItem_Export.DropDownItems.AddRange(new ToolStripItem[] { toolStripMenuItem_ExportFrame, toolStripMenuItem_ExportFrameSequence, toolStripMenuItem_ExportGif, toolStripMenuItem_ExportMkv, toolStripMenuItem_ExportMp4, toolStripMenuItem_ExportMov, toolStripMenuItem_ExportWebm });
toolStripMenuItem_Export.Name = "toolStripMenuItem_Export";
toolStripMenuItem_Export.ShortcutKeys = Keys.Control | Keys.S;
toolStripMenuItem_Export.Size = new Size(254, 34);
toolStripMenuItem_Export.Text = "导出(&E)...";
toolStripMenuItem_Export.Click += toolStripMenuItem_Export_Click;
toolStripMenuItem_Export.Text = "导出(&E)";
//
// toolStripMenuItem_ExportFrame
//
toolStripMenuItem_ExportFrame.Name = "toolStripMenuItem_ExportFrame";
toolStripMenuItem_ExportFrame.Size = new Size(194, 34);
toolStripMenuItem_ExportFrame.Text = "单帧画面...";
toolStripMenuItem_ExportFrame.Click += toolStripMenuItem_Export_Click;
//
// toolStripMenuItem_ExportFrameSequence
//
toolStripMenuItem_ExportFrameSequence.Name = "toolStripMenuItem_ExportFrameSequence";
toolStripMenuItem_ExportFrameSequence.Size = new Size(194, 34);
toolStripMenuItem_ExportFrameSequence.Text = "帧序列...";
toolStripMenuItem_ExportFrameSequence.Click += toolStripMenuItem_Export_Click;
//
// toolStripMenuItem_ExportGif
//
toolStripMenuItem_ExportGif.Name = "toolStripMenuItem_ExportGif";
toolStripMenuItem_ExportGif.Size = new Size(194, 34);
toolStripMenuItem_ExportGif.Text = "GIF...";
//
// toolStripMenuItem_ExportMkv
//
toolStripMenuItem_ExportMkv.Name = "toolStripMenuItem_ExportMkv";
toolStripMenuItem_ExportMkv.Size = new Size(194, 34);
toolStripMenuItem_ExportMkv.Text = "MKV";
//
// toolStripMenuItem_ExportMp4
//
toolStripMenuItem_ExportMp4.Name = "toolStripMenuItem_ExportMp4";
toolStripMenuItem_ExportMp4.Size = new Size(194, 34);
toolStripMenuItem_ExportMp4.Text = "MP4...";
//
// toolStripMenuItem_ExportMov
//
toolStripMenuItem_ExportMov.Name = "toolStripMenuItem_ExportMov";
toolStripMenuItem_ExportMov.Size = new Size(194, 34);
toolStripMenuItem_ExportMov.Text = "MOV...";
//
// toolStripMenuItem_ExportWebm
//
toolStripMenuItem_ExportWebm.Name = "toolStripMenuItem_ExportWebm";
toolStripMenuItem_ExportWebm.Size = new Size(194, 34);
toolStripMenuItem_ExportWebm.Text = "WebM...";
//
// toolStripSeparator2
//
@@ -146,20 +194,6 @@
toolStripMenuItem_Exit.Text = "退出(&X)";
toolStripMenuItem_Exit.Click += toolStripMenuItem_Exit_Click;
//
// toolStripMenuItem_Function
//
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_ResetAnimation
//
toolStripMenuItem_ResetAnimation.Name = "toolStripMenuItem_ResetAnimation";
toolStripMenuItem_ResetAnimation.Size = new Size(242, 34);
toolStripMenuItem_ResetAnimation.Text = "重置动画时间(&R)";
toolStripMenuItem_ResetAnimation.Click += toolStripMenuItem_ResetAnimation_Click;
//
// toolStripMenuItem_Tool
//
toolStripMenuItem_Tool.DropDownItems.AddRange(new ToolStripItem[] { toolStripMenuItem_ConvertFileFormat });
@@ -170,7 +204,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 +220,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 +258,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 +280,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 +303,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 +326,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 +339,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 +350,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 +359,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 +381,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 +394,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 +405,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 +416,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,22 +427,20 @@
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 = "预览画面";
//
// spinePreviewer
//
spinePreviewer.BackColor = SystemColors.ControlDark;
spinePreviewer.Dock = DockStyle.Fill;
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;
//
// panel_MainForm
//
@@ -416,7 +449,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 +460,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");
@@ -472,7 +505,6 @@
private ToolStripMenuItem toolStripMenuItem_Open;
private ToolStripMenuItem toolStripMenuItem_Exit;
private ToolStripSeparator toolStripSeparator1;
private ToolStripMenuItem toolStripMenuItem_Export;
private ToolStripSeparator toolStripSeparator2;
private RichTextBox rtbLog;
private SplitContainer splitContainer_MainForm;
@@ -492,13 +524,19 @@
private Controls.SpineListView spineListView;
private PropertyGrid propertyGrid_Previewer;
private Controls.SpinePreviewer spinePreviewer;
private ToolStripMenuItem toolStripMenuItem_Function;
private ToolStripMenuItem toolStripMenuItem_ResetAnimation;
private ToolStripMenuItem toolStripMenuItem_Diagnostics;
private ToolStripSeparator toolStripSeparator3;
private ToolStripMenuItem toolStripMenuItem_Download;
private ToolStripMenuItem toolStripMenuItem_ManageResource;
private ToolStripMenuItem toolStripMenuItem_Tool;
private ToolStripMenuItem toolStripMenuItem_ConvertFileFormat;
private ToolStripMenuItem toolStripMenuItem_Export;
private ToolStripMenuItem toolStripMenuItem_ExportFrame;
private ToolStripMenuItem toolStripMenuItem_ExportFrameSequence;
private ToolStripMenuItem toolStripMenuItem_ExportGif;
private ToolStripMenuItem toolStripMenuItem_ExportMp4;
private ToolStripMenuItem toolStripMenuItem_ExportMov;
private ToolStripMenuItem toolStripMenuItem_ExportMkv;
private ToolStripMenuItem toolStripMenuItem_ExportWebm;
}
}

View File

@@ -1,7 +1,14 @@
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;
using SpineViewer.Exporter;
namespace SpineViewer
{
@@ -11,6 +18,10 @@ namespace SpineViewer
{
InitializeComponent();
InitializeLogConfiguration();
// 在此处将导出菜单需要的类绑定起来
toolStripMenuItem_ExportFrame.Tag = ExportType.Frame;
toolStripMenuItem_ExportFrameSequence.Tag = ExportType.FrameSequence;
}
/// <summary>
@@ -41,76 +52,14 @@ namespace SpineViewer
LogManager.ReconfigExistingLoggers();
}
private void ExportPng_Work(object? sender, DoWorkEventArgs e)
{
var worker = sender as BackgroundWorker;
var arguments = e.Argument as Dialogs.ExportPngDialog;
var outputDir = arguments.OutputDir;
var duration = arguments.Duration;
var fps = arguments.Fps;
var timestamp = DateTime.Now.ToString("yyMMddHHmmss");
var resolution = spinePreviewer.Resolution;
var tex = new SFML.Graphics.RenderTexture((uint)resolution.Width, (uint)resolution.Height);
tex.SetView(spinePreviewer.View);
var delta = 1f / fps;
var frameCount = 1 + (int)(duration / delta); // 零帧开始导出
spinePreviewer.StopPreview();
lock (spineListView.Spines)
{
var spinesReverse = spineListView.Spines.Reverse();
// 重置动画时间
foreach (var spine in spinesReverse)
spine.CurrentAnimation = spine.CurrentAnimation;
Program.Logger.Info(
"Begin exporting png frames to output dir {}, duration: {}, fps: {}, totally {} spines",
[outputDir, duration, fps, spinesReverse.Count()]
);
// 逐帧导出
var success = 0;
worker.ReportProgress(0, $"已处理 0/{frameCount}");
for (int frameIndex = 0; frameIndex < frameCount; frameIndex++)
{
if (worker.CancellationPending)
break;
tex.Clear(SFML.Graphics.Color.Transparent);
foreach (var spine in spinesReverse)
{
tex.Draw(spine);
spine.Update(delta);
}
tex.Display();
using (var img = tex.Texture.CopyToImage())
{
img.SaveToFile(Path.Combine(outputDir, $"{timestamp}_{fps}_{frameIndex:d6}.png"));
}
success++;
worker.ReportProgress((int)((frameIndex + 1) * 100.0) / frameCount, $"已处理 {frameIndex + 1}/{frameCount}");
}
Program.Logger.Info("Exporting done: {}/{}", success, frameCount);
}
spinePreviewer.StartPreview();
}
private void MainForm_Load(object sender, EventArgs e)
{
spinePreviewer.StartPreview();
spinePreviewer.StartRender();
}
private void MainForm_FormClosing(object sender, FormClosingEventArgs e)
{
spinePreviewer.StopPreview();
spinePreviewer.StopRender();
}
private void toolStripMenuItem_Open_Click(object sender, EventArgs e)
@@ -125,22 +74,33 @@ namespace SpineViewer
private void toolStripMenuItem_Export_Click(object sender, EventArgs e)
{
ExportType type = (ExportType)((ToolStripMenuItem)sender).Tag;
if (type == ExportType.Frame && spinePreviewer.IsUpdating)
{
if (MessageBox.Quest("画面仍在更新,建议手动暂停画面后导出固定的一帧,是否继续?") != DialogResult.OK)
return;
}
lock (spineListView.Spines)
{
if (spineListView.Spines.Count <= 0)
{
MessageBox.Show("请至少打开一个骨骼文件", "提示信息", MessageBoxButtons.OK, MessageBoxIcon.Information);
MessageBox.Info("请至少打开一个骨骼文件");
return;
}
}
var exportDialog = new Dialogs.ExportPngDialog();
var exportArgs = ExportArgs.New(type, spinePreviewer.Resolution, spinePreviewer.GetView(), spinePreviewer.RenderSelectedOnly);
var exportDialog = new Dialogs.ExportDialog() { ExportArgs = exportArgs };
if (exportDialog.ShowDialog() != DialogResult.OK)
return;
var exporter = Exporter.Exporter.New(type, exportArgs);
var progressDialog = new Dialogs.ProgressDialog();
progressDialog.DoWork += ExportPng_Work;
progressDialog.RunWorkerAsync(exportDialog);
progressDialog.DoWork += Export_Work;
progressDialog.RunWorkerAsync(exporter);
progressDialog.ShowDialog();
}
@@ -149,15 +109,6 @@ namespace SpineViewer
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();
@@ -166,24 +117,122 @@ namespace SpineViewer
var progressDialog = new Dialogs.ProgressDialog();
progressDialog.DoWork += ConvertFileFormat_Work;
progressDialog.RunWorkerAsync(openDialog);
progressDialog.RunWorkerAsync(openDialog.Result);
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 Export_Work(object? sender, DoWorkEventArgs e)
{
var worker = (BackgroundWorker)sender;
var exporter = (Exporter.Exporter)e.Argument;
spinePreviewer.StopRender();
lock (spineListView.Spines) { exporter.Export(spineListView.Spines.ToArray(), (BackgroundWorker)sender); }
e.Cancel = worker.CancellationPending;
spinePreviewer.StartRender();
}
private void ConvertFileFormat_Work(object? sender, DoWorkEventArgs e)
{
var worker = sender as BackgroundWorker;
var arguments = e.Argument as Dialogs.ConvertFileFormatDialog;
var arguments = e.Argument as Dialogs.ConvertFileFormatDialogResult;
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 jsonTarget = arguments.JsonTarget;
var newSuffix = jsonTarget ? ".json" : ".skel";
int totalCount = skelPaths.Length;
int success = 0;
int error = 0;
SkeletonConverter cvter = SkeletonConverter.New(version);
SkeletonConverter srcCvter = srcVersion != Spine.Version.Auto ? SkeletonConverter.New(srcVersion) : null;
SkeletonConverter tgtCvter = SkeletonConverter.New(tgtVersion);
worker.ReportProgress(0, $"已处理 0/{totalCount}");
for (int i = 0; i < totalCount; i++)
@@ -199,10 +248,16 @@ namespace SpineViewer
try
{
if (convertToJson)
cvter.BinaryToJson(skelPath, newPath);
else
cvter.JsonToBinary(skelPath, newPath);
if (srcVersion == Spine.Version.Auto)
{
if (Spine.Spine.GetVersion(skelPath) is Spine.Version detectedSrcVersion)
srcCvter = SkeletonConverter.New(detectedSrcVersion);
else
throw new InvalidDataException($"Auto version detection failed for {skelPath}, try to use a specific version");
}
var root = srcCvter.Read(skelPath);
root = srcCvter.ToVersion(root, tgtVersion);
if (jsonTarget) tgtCvter.WriteJson(root, newPath); else tgtCvter.WriteBinary(root, newPath);
success++;
}
catch (Exception ex)
@@ -225,23 +280,27 @@ namespace SpineViewer
}
}
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 spinePreviewer_KeyDown(object sender, KeyEventArgs e)
//{
// switch (e.KeyCode)
// {
// case Keys.Space:
// if ((ModifierKeys & Keys.Alt) != 0)
// spinePreviewer.ClickStopButton();
// else
// spinePreviewer.ClickStartButton();
// break;
// case Keys.Right:
// if ((ModifierKeys & Keys.Alt) != 0)
// spinePreviewer.ClickForwardFastButton();
// else
// spinePreviewer.ClickForwardStepButton();
// break;
// case Keys.Left:
// if ((ModifierKeys & Keys.Alt) != 0)
// spinePreviewer.ClickRestartButton();
// break;
// }
//}
}
}

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,53 @@ using SpineRuntime38;
using System.Text.Json;
using System.Text.Json.Nodes;
using SpineRuntime38.Attachments;
using System.Globalization;
namespace SpineViewer.Spine.Implementations.SkeletonConverter
{
[SkeletonConverterImplementation(Version.V38)]
[SpineImplementation(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 +65,7 @@ namespace SpineViewer.Spine.Implementations.SkeletonConverter
skeleton["images"] = reader.ReadString();
skeleton["audio"] = reader.ReadString();
}
return skeleton;
root["skeleton"] = skeleton;
}
private void ReadStrings()
@@ -68,14 +74,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 +95,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 +106,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 +114,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 +128,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 +137,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 +151,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 +166,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 +180,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 +191,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 +206,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 +225,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 +235,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 +327,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 +358,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 +379,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 +390,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 +437,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 +453,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 +517,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 +533,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 +561,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 +588,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 +635,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 +649,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 +701,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 +718,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 +738,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 +800,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

@@ -12,20 +12,6 @@ using System.Text.Encodings.Web;
namespace SpineViewer.Spine
{
/// <summary>
/// SkeletonConverter 实现类标记
/// </summary>
[AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = false)]
public class SkeletonConverterImplementationAttribute : Attribute
{
public Version Version { get; }
public SkeletonConverterImplementationAttribute(Version version)
{
Version = version;
}
}
/// <summary>
/// SkeletonConverter 基类, 使用静态方法 New 来创建具体版本对象
/// </summary>
@@ -42,11 +28,11 @@ namespace SpineViewer.Spine
/// </summary>
static SkeletonConverter()
{
// 遍历并缓存标记了 SkeletonConverterImplementationAttribute 的类型
// 遍历并缓存标记了 SpineImplementationAttribute 的类型
var impTypes = Assembly.GetExecutingAssembly().GetTypes().Where(t => typeof(SkeletonConverter).IsAssignableFrom(t) && !t.IsAbstract);
foreach (var type in impTypes)
{
var attr = type.GetCustomAttribute<SkeletonConverterImplementationAttribute>();
var attr = type.GetCustomAttribute<SpineImplementationAttribute>();
if (attr is not null)
{
if (ImplementationTypes.ContainsKey(attr.Version))
@@ -82,29 +68,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 +98,44 @@ namespace SpineViewer.Spine
}
/// <summary>
/// 二进制转 Json 格式
/// 读取骨骼文件
/// </summary>
public void BinaryToJson(string binPath, string jsonPath)
public JsonObject Read(string path)
{
WriteJson(ReadBinary(binPath), jsonPath);
try
{
return ReadBinary(path);
}
catch
{
try
{
return ReadJson(path);
}
catch
{
// 都不行就报错
throw new InvalidDataException($"Unknown skeleton file format {path}");
}
}
}
/// <summary>
/// Json 转二进制格式
/// 转换到目标版本
/// </summary>
public void JsonToBinary(string jsonPath, string binPath)
{
WriteBinary(ReadJson(jsonPath), binPath);
}
public abstract JsonObject ToVersion(JsonObject root, Version version);
protected class SkeletonReader
/// <summary>
/// 二进制骨骼文件读
/// </summary>
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 +231,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 +327,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,34 +8,46 @@ 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;
using SpineViewer.Exporter;
namespace SpineViewer.Spine
{
/// <summary>
/// Spine 实现类标记
/// </summary>
[AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = false)]
public class SpineImplementationAttribute : Attribute
{
public Version Version { get; }
public SpineImplementationAttribute(Version version)
{
Version = version;
}
}
/// <summary>
/// Spine 基类, 使用静态方法 New 来创建具体版本对象
/// </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 +99,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 +185,11 @@ namespace SpineViewer.Spine
return (Spine)Activator.CreateInstance(spineType, skelPath, atlasPath);
}
/// <summary>
/// 标识符
/// </summary>
public readonly string ID = Guid.NewGuid().ToString();
/// <summary>
/// 构造函数
/// </summary>
@@ -113,13 +200,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 +215,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 +226,12 @@ namespace SpineViewer.Spine
[Category("基本信息"), DisplayName("运行时版本")]
public Version Version { get; }
/// <summary>
/// 资源所在完整目录
/// </summary>
[Category("基本信息"), DisplayName("资源目录")]
public string AssetsDir { get; }
/// <summary>
/// skel 文件完整路径
/// </summary>
@@ -154,6 +244,9 @@ namespace SpineViewer.Spine
[Category("基本信息"), DisplayName("atlas文件路径")]
public string AtlasPath { get; }
/// <summary>
/// 名称
/// </summary>
[Category("基本信息"), DisplayName("名称")]
public string Name { get; }
@@ -163,6 +256,10 @@ namespace SpineViewer.Spine
[Category("基本信息"), DisplayName("文件版本")]
public abstract string FileVersion { get; }
#endregion
#region |
/// <summary>
/// 缩放比例
/// </summary>
@@ -188,18 +285,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 +310,10 @@ namespace SpineViewer.Spine
[Browsable(false)]
public string DefaultAnimationName { get => animationNames.Last(); }
#region |
/// <summary>
/// 当前动画名称
/// 当前动画名称, 如果设置的动画不存在则忽略
/// </summary>
[TypeConverter(typeof(AnimationConverter))]
[Category("动画"), DisplayName("当前动画")]
@@ -220,12 +325,68 @@ 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 RectangleF InitBounds
{
get
{
if (initBounds is null)
{
var tmp = CurrentAnimation;
CurrentAnimation = EMPTY_ANIMATION;
initBounds = Bounds;
CurrentAnimation = tmp;
}
return (RectangleF)initBounds;
}
}
private RectangleF? initBounds = null;
/// <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(InitBounds.GetView(PREVIEW_WIDTH, PREVIEW_HEIGHT));
tex.Clear(SFML.Graphics.Color.Transparent);
var tmp = CurrentAnimation;
CurrentAnimation = EMPTY_ANIMATION;
tex.Draw(this);
CurrentAnimation = tmp;
tex.Display();
using var img = tex.Texture.CopyToImage();
img.SaveToMemory(out var imgBuffer, "bmp");
using var stream = new MemoryStream(imgBuffer);
preview = new Bitmap(new Bitmap(stream)); // 必须重复构造一个副本才能摆脱对流的依赖, 否则之后使用会报错
}
return preview;
}
}
private Image preview = null;
/// <summary>
/// 获取动画时长, 如果动画不存在则返回 0
/// </summary>
@@ -237,6 +398,42 @@ namespace SpineViewer.Spine
/// <param name="delta">时间间隔</param>
public abstract void Update(float delta);
/// <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 +442,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

@@ -0,0 +1,39 @@
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Drawing.Design;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms.Design;
namespace SpineViewer.Spine
{
/// <summary>
/// skel 文件路径编辑器
/// </summary>
public class SkelFileNameEditor : FileNameEditor
{
protected override void InitializeDialog(OpenFileDialog openFileDialog)
{
base.InitializeDialog(openFileDialog);
openFileDialog.Title = "选择 skel 文件";
openFileDialog.AddExtension = false;
openFileDialog.Filter = "skel 文件 (*.skel; *.json)|*.skel;*.json|二进制文件 (*.skel)|*.skel|文本文件 (*.json)|*.json|所有文件 (*.*)|*.*";
}
}
/// <summary>
/// atlas 文件路径编辑器
/// </summary>
public class AtlasFileNameEditor : FileNameEditor
{
protected override void InitializeDialog(OpenFileDialog openFileDialog)
{
base.InitializeDialog(openFileDialog);
openFileDialog.Title = "选择 atlas 文件";
openFileDialog.AddExtension = false;
openFileDialog.Filter = "atlas 文件 (*.atlas)|*.atlas|所有文件 (*.*)|*.*";
}
}
}

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

View File

@@ -8,10 +8,11 @@
<RuntimeIdentifier>win-x64</RuntimeIdentifier>
<BaseOutputPath>$(SolutionDir)out</BaseOutputPath>
<IncludeSourceRevisionInInformationalVersion>false</IncludeSourceRevisionInInformationalVersion>
<Version>0.10.0</Version>
<Version>0.11.0</Version>
<OutputType>WinExe</OutputType>
<UseWindowsForms>true</UseWindowsForms>
<ApplicationIcon>appicon.ico</ApplicationIcon>
<GenerateResourceWarnOnBinaryFormatterUse>false</GenerateResourceWarnOnBinaryFormatterUse>
</PropertyGroup>
<ItemGroup>
@@ -19,6 +20,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" />

View File

@@ -9,7 +9,7 @@ using System.Threading.Tasks;
namespace SpineViewer
{
public class PointFConverter : ExpandableObjectConverter
public class PointFConverter : TypeConverter
{
public override bool CanConvertTo(ITypeDescriptorContext? context, [NotNullWhen(true)] Type? destinationType)
{

View File

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

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