Compare commits

..

122 Commits

Author SHA1 Message Date
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
ww-rm
ac0adc5f95 更新至v0.10.0 2025-03-16 16:47:34 +08:00
ww-rm
208b702065 update changelog 2025-03-16 16:46:58 +08:00
ww-rm
7e61fbfbac 增加后缀筛选选项 2025-03-16 16:46:49 +08:00
ww-rm
0591549727 fix bug 2025-03-16 16:38:51 +08:00
ww-rm
a0833580f8 增加格式转换 2025-03-16 16:22:19 +08:00
ww-rm
c622b60215 增加版本检测 2025-03-16 16:12:36 +08:00
ww-rm
c228cf9072 add readanimation 2025-03-16 14:22:00 +08:00
ww-rm
4c68dd4904 add something 2025-03-16 01:40:19 +08:00
ww-rm
32fde582fc refactor 2025-03-14 18:55:56 +08:00
ww-rm
2bf2509df7 add read attachments 2025-03-14 01:12:22 +08:00
ww-rm
07042189c8 增加注释 2025-03-13 17:31:04 +08:00
ww-rm
d251c94638 some change 2025-03-13 17:29:56 +08:00
ww-rm
b4119087fb update readme 2025-03-13 14:53:32 +08:00
ww-rm
e3959e80fb add something 2025-03-13 14:32:37 +08:00
ww-rm
0495a2344c add something 2025-03-13 14:32:11 +08:00
ww-rm
c781ec5a4f fix bug 2025-03-13 14:23:00 +08:00
ww-rm
a58566735f 增加提示信息 2025-03-13 14:22:46 +08:00
ww-rm
b37e5c25c3 rename 2025-03-13 13:30:35 +08:00
ww-rm
63a937a45b optimize 2025-03-13 10:32:18 +08:00
ww-rm
c920471c0c change name 2025-03-13 02:32:53 +08:00
ww-rm
c4863ee09b add binary writer/reader 2025-03-13 01:31:24 +08:00
ww-rm
c0b85c454e 调整结构 2025-03-12 19:14:22 +08:00
ww-rm
763a49a4d3 增加一些按钮 2025-03-12 18:33:52 +08:00
ww-rm
0e1540873c 增加自动弹出选择文件 2025-03-12 18:33:07 +08:00
ww-rm
39dcc636ca 增加文件版本显示 2025-03-12 15:23:15 +08:00
ww-rm
342778c56e 增加画面和列表联动 2025-03-11 00:01:54 +08:00
ww-rm
fd524891aa 增加属性IsSelected 2025-03-11 00:00:23 +08:00
ww-rm
48cb60020c 增加属性SelectedIndices 2025-03-11 00:00:09 +08:00
ww-rm
d502c592f7 update readme 2025-03-06 11:16:59 +08:00
ww-rm
e4377436a7 修改文件编码 2025-03-06 00:00:56 +08:00
ww-rm
eb44c1271e 修改命名 2025-03-05 16:24:19 +08:00
ww-rm
20953c2dfc 更新至v0.9.6 2025-03-05 16:10:36 +08:00
ww-rm
4c96a71124 更新readme 2025-03-05 16:08:29 +08:00
ww-rm
295afdc874 增加2.1.x运行时 2025-03-05 16:07:40 +08:00
ww-rm
a66280ce7b update readme 2025-03-05 01:06:11 +08:00
ww-rm
ce1ea1c5c5 change name 2025-03-05 01:05:39 +08:00
81 changed files with 17282 additions and 678 deletions

View File

@@ -1,4 +1,4 @@
name: Build and Release WinForms
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

48
CHANGELOG.md Normal file
View File

@@ -0,0 +1,48 @@
# CHANGELOG
## 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
- 增加了画面和列表的选择联动,并删除了预览画面显示包围盒选项
- 增加了骨骼文件格式转换功能,目前仅支持部分版本的不完整功能
- 优化了部分使用体验

View File

@@ -1,67 +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] `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,10 +1,12 @@
# 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)
一个简单好用的 Spine 文件查看&导出程序.
![previewer](img/previewer.jpg)
![previewer](img/preview.webp)
---
@@ -16,51 +18,72 @@
也可以下载带有 `SelfContained` 后缀的压缩包, 可以独立运行.
## 功能
## 版本支持
- 支持不同版本 Spine 查看
- [x] `v3.6.x`
- [x] `v3.7.x`
- [x] `v3.8.x`
- [x] `v4.0.x`
- [x] `v4.1.x`
- [x] `v4.2.x`
- [ ] `v4.3.x`
- 支持多骨骼文件动画预览
- 支持每个骨骼独立参数设置
- 支持动画PNG帧序列导出
- 支持缩放旋转等导出画面设置
- Coming soon...
| 版本 | 查看&导出 | 格式转换 | 版本转换 |
| :---: | :---: | :---: | :---: |
| `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

@@ -0,0 +1,723 @@
/******************************************************************************
* Spine Runtimes Software License
* Version 2.1
*
* Copyright (c) 2013, Esoteric Software
* All rights reserved.
*
* You are granted a perpetual, non-exclusive, non-sublicensable and
* non-transferable license to install, execute and perform the Spine Runtimes
* Software (the "Software") solely for internal use. Without the written
* permission of Esoteric Software (typically granted by licensing Spine), you
* may not (a) modify, translate, adapt or otherwise create derivative works,
* improvements of the Software or develop new applications using the Software
* or (b) remove, delete, alter or obscure any trademarks or any copyright,
* trademark, patent or other intellectual property or proprietary rights
* notices on or in the Software, including any copy thereof. Redistributions
* in binary or source form must include this license and terms.
*
* THIS SOFTWARE IS PROVIDED BY ESOTERIC SOFTWARE "AS IS" AND ANY EXPRESS OR
* IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
* MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO
* EVENT SHALL ESOTERIC SOFTARE BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
* PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS;
* OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
* WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR
* OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
* ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*****************************************************************************/
using System;
using System.Collections.Generic;
namespace SpineRuntime21 {
public class Animation {
internal List<Timeline> timelines;
internal float duration;
internal String name;
public String Name { get { return name; } }
public List<Timeline> Timelines { get { return timelines; } set { timelines = value; } }
public float Duration { get { return duration; } set { duration = value; } }
public Animation (String name, List<Timeline> timelines, float duration) {
if (name == null) throw new ArgumentNullException("name cannot be null.");
if (timelines == null) throw new ArgumentNullException("timelines cannot be null.");
this.name = name;
this.timelines = timelines;
this.duration = duration;
}
/// <summary>Poses the skeleton at the specified time for this animation.</summary>
/// <param name="lastTime">The last time the animation was applied.</param>
/// <param name="events">Any triggered events are added.</param>
public void Apply (Skeleton skeleton, float lastTime, float time, bool loop, List<Event> events) {
if (skeleton == null) throw new ArgumentNullException("skeleton cannot be null.");
if (loop && duration != 0) {
time %= duration;
lastTime %= duration;
}
List<Timeline> timelines = this.timelines;
for (int i = 0, n = timelines.Count; i < n; i++)
timelines[i].Apply(skeleton, lastTime, time, events, 1);
}
/// <summary>Poses the skeleton at the specified time for this animation mixed with the current pose.</summary>
/// <param name="lastTime">The last time the animation was applied.</param>
/// <param name="events">Any triggered events are added.</param>
/// <param name="alpha">The amount of this animation that affects the current pose.</param>
public void Mix (Skeleton skeleton, float lastTime, float time, bool loop, List<Event> events, float alpha) {
if (skeleton == null) throw new ArgumentNullException("skeleton cannot be null.");
if (loop && duration != 0) {
time %= duration;
lastTime %= duration;
}
List<Timeline> timelines = this.timelines;
for (int i = 0, n = timelines.Count; i < n; i++)
timelines[i].Apply(skeleton, lastTime, time, events, alpha);
}
/// <param name="target">After the first and before the last entry.</param>
internal static int binarySearch (float[] values, float target, int step) {
int low = 0;
int high = values.Length / step - 2;
if (high == 0) return step;
int current = (int)((uint)high >> 1);
while (true) {
if (values[(current + 1) * step] <= target)
low = current + 1;
else
high = current;
if (low == high) return (low + 1) * step;
current = (int)((uint)(low + high) >> 1);
}
}
/// <param name="target">After the first and before the last entry.</param>
internal static int binarySearch (float[] values, float target) {
int low = 0;
int high = values.Length - 2;
if (high == 0) return 1;
int current = (int)((uint)high >> 1);
while (true) {
if (values[(current + 1)] <= target)
low = current + 1;
else
high = current;
if (low == high) return (low + 1);
current = (int)((uint)(low + high) >> 1);
}
}
internal static int linearSearch (float[] values, float target, int step) {
for (int i = 0, last = values.Length - step; i <= last; i += step)
if (values[i] > target) return i;
return -1;
}
}
public interface Timeline {
/// <summary>Sets the value(s) for the specified time.</summary>
/// <param name="events">May be null to not collect fired events.</param>
void Apply (Skeleton skeleton, float lastTime, float time, List<Event> events, float alpha);
}
/// <summary>Base class for frames that use an interpolation bezier curve.</summary>
abstract public class CurveTimeline : Timeline {
protected const float LINEAR = 0, STEPPED = 1, BEZIER = 2;
protected const int BEZIER_SEGMENTS = 10, BEZIER_SIZE = BEZIER_SEGMENTS * 2 - 1;
private float[] curves; // type, x, y, ...
public int FrameCount { get { return curves.Length / BEZIER_SIZE + 1; } }
public CurveTimeline (int frameCount) {
curves = new float[(frameCount - 1) * BEZIER_SIZE];
}
abstract public void Apply (Skeleton skeleton, float lastTime, float time, List<Event> firedEvents, float alpha);
public void SetLinear (int frameIndex) {
curves[frameIndex * BEZIER_SIZE] = LINEAR;
}
public void SetStepped (int frameIndex) {
curves[frameIndex * BEZIER_SIZE] = STEPPED;
}
/// <summary>Sets the control handle positions for an interpolation bezier curve used to transition from this keyframe to the next.
/// cx1 and cx2 are from 0 to 1, representing the percent of time between the two keyframes. cy1 and cy2 are the percent of
/// the difference between the keyframe's values.</summary>
public void SetCurve (int frameIndex, float cx1, float cy1, float cx2, float cy2) {
float subdiv1 = 1f / BEZIER_SEGMENTS, subdiv2 = subdiv1 * subdiv1, subdiv3 = subdiv2 * subdiv1;
float pre1 = 3 * subdiv1, pre2 = 3 * subdiv2, pre4 = 6 * subdiv2, pre5 = 6 * subdiv3;
float tmp1x = -cx1 * 2 + cx2, tmp1y = -cy1 * 2 + cy2, tmp2x = (cx1 - cx2) * 3 + 1, tmp2y = (cy1 - cy2) * 3 + 1;
float dfx = cx1 * pre1 + tmp1x * pre2 + tmp2x * subdiv3, dfy = cy1 * pre1 + tmp1y * pre2 + tmp2y * subdiv3;
float ddfx = tmp1x * pre4 + tmp2x * pre5, ddfy = tmp1y * pre4 + tmp2y * pre5;
float dddfx = tmp2x * pre5, dddfy = tmp2y * pre5;
int i = frameIndex * BEZIER_SIZE;
float[] curves = this.curves;
curves[i++] = BEZIER;
float x = dfx, y = dfy;
for (int n = i + BEZIER_SIZE - 1; i < n; i += 2) {
curves[i] = x;
curves[i + 1] = y;
dfx += ddfx;
dfy += ddfy;
ddfx += dddfx;
ddfy += dddfy;
x += dfx;
y += dfy;
}
}
public float GetCurvePercent (int frameIndex, float percent) {
float[] curves = this.curves;
int i = frameIndex * BEZIER_SIZE;
float type = curves[i];
if (type == LINEAR) return percent;
if (type == STEPPED) return 0;
i++;
float x = 0;
for (int start = i, n = i + BEZIER_SIZE - 1; i < n; i += 2) {
x = curves[i];
if (x >= percent) {
float prevX, prevY;
if (i == start) {
prevX = 0;
prevY = 0;
} else {
prevX = curves[i - 2];
prevY = curves[i - 1];
}
return prevY + (curves[i + 1] - prevY) * (percent - prevX) / (x - prevX);
}
}
float y = curves[i - 1];
return y + (1 - y) * (percent - x) / (1 - x); // Last point is 1,1.
}
public float GetCurveType (int frameIndex) {
return curves[frameIndex * BEZIER_SIZE];
}
}
public class RotateTimeline : CurveTimeline {
protected const int PREV_FRAME_TIME = -2;
protected const int FRAME_VALUE = 1;
internal int boneIndex;
internal float[] frames;
public int BoneIndex { get { return boneIndex; } set { boneIndex = value; } }
public float[] Frames { get { return frames; } set { frames = value; } } // time, value, ...
public RotateTimeline (int frameCount)
: base(frameCount) {
frames = new float[frameCount << 1];
}
/// <summary>Sets the time and value of the specified keyframe.</summary>
public void SetFrame (int frameIndex, float time, float angle) {
frameIndex *= 2;
frames[frameIndex] = time;
frames[frameIndex + 1] = angle;
}
override public void Apply (Skeleton skeleton, float lastTime, float time, List<Event> firedEvents, float alpha) {
float[] frames = this.frames;
if (time < frames[0]) return; // Time is before first frame.
Bone bone = skeleton.bones[boneIndex];
float amount;
if (time >= frames[frames.Length - 2]) { // Time is after last frame.
amount = bone.data.rotation + frames[frames.Length - 1] - bone.rotation;
while (amount > 180)
amount -= 360;
while (amount < -180)
amount += 360;
bone.rotation += amount * alpha;
return;
}
// Interpolate between the previous frame and the current frame.
int frameIndex = Animation.binarySearch(frames, time, 2);
float prevFrameValue = frames[frameIndex - 1];
float frameTime = frames[frameIndex];
float percent = 1 - (time - frameTime) / (frames[frameIndex + PREV_FRAME_TIME] - frameTime);
percent = GetCurvePercent((frameIndex >> 1) - 1, percent < 0 ? 0 : (percent > 1 ? 1 : percent));
amount = frames[frameIndex + FRAME_VALUE] - prevFrameValue;
while (amount > 180)
amount -= 360;
while (amount < -180)
amount += 360;
amount = bone.data.rotation + (prevFrameValue + amount * percent) - bone.rotation;
while (amount > 180)
amount -= 360;
while (amount < -180)
amount += 360;
bone.rotation += amount * alpha;
}
}
public class TranslateTimeline : CurveTimeline {
protected const int PREV_FRAME_TIME = -3;
protected const int FRAME_X = 1;
protected const int FRAME_Y = 2;
internal int boneIndex;
internal float[] frames;
public int BoneIndex { get { return boneIndex; } set { boneIndex = value; } }
public float[] Frames { get { return frames; } set { frames = value; } } // time, value, value, ...
public TranslateTimeline (int frameCount)
: base(frameCount) {
frames = new float[frameCount * 3];
}
/// <summary>Sets the time and value of the specified keyframe.</summary>
public void SetFrame (int frameIndex, float time, float x, float y) {
frameIndex *= 3;
frames[frameIndex] = time;
frames[frameIndex + 1] = x;
frames[frameIndex + 2] = y;
}
override public void Apply (Skeleton skeleton, float lastTime, float time, List<Event> firedEvents, float alpha) {
float[] frames = this.frames;
if (time < frames[0]) return; // Time is before first frame.
Bone bone = skeleton.bones[boneIndex];
if (time >= frames[frames.Length - 3]) { // Time is after last frame.
bone.x += (bone.data.x + frames[frames.Length - 2] - bone.x) * alpha;
bone.y += (bone.data.y + frames[frames.Length - 1] - bone.y) * alpha;
return;
}
// Interpolate between the previous frame and the current frame.
int frameIndex = Animation.binarySearch(frames, time, 3);
float prevFrameX = frames[frameIndex - 2];
float prevFrameY = frames[frameIndex - 1];
float frameTime = frames[frameIndex];
float percent = 1 - (time - frameTime) / (frames[frameIndex + PREV_FRAME_TIME] - frameTime);
percent = GetCurvePercent(frameIndex / 3 - 1, percent < 0 ? 0 : (percent > 1 ? 1 : percent));
bone.x += (bone.data.x + prevFrameX + (frames[frameIndex + FRAME_X] - prevFrameX) * percent - bone.x) * alpha;
bone.y += (bone.data.y + prevFrameY + (frames[frameIndex + FRAME_Y] - prevFrameY) * percent - bone.y) * alpha;
}
}
public class ScaleTimeline : TranslateTimeline {
public ScaleTimeline (int frameCount)
: base(frameCount) {
}
override public void Apply (Skeleton skeleton, float lastTime, float time, List<Event> firedEvents, float alpha) {
float[] frames = this.frames;
if (time < frames[0]) return; // Time is before first frame.
Bone bone = skeleton.bones[boneIndex];
if (time >= frames[frames.Length - 3]) { // Time is after last frame.
bone.scaleX += (bone.data.scaleX * frames[frames.Length - 2] - bone.scaleX) * alpha;
bone.scaleY += (bone.data.scaleY * frames[frames.Length - 1] - bone.scaleY) * alpha;
return;
}
// Interpolate between the previous frame and the current frame.
int frameIndex = Animation.binarySearch(frames, time, 3);
float prevFrameX = frames[frameIndex - 2];
float prevFrameY = frames[frameIndex - 1];
float frameTime = frames[frameIndex];
float percent = 1 - (time - frameTime) / (frames[frameIndex + PREV_FRAME_TIME] - frameTime);
percent = GetCurvePercent(frameIndex / 3 - 1, percent < 0 ? 0 : (percent > 1 ? 1 : percent));
bone.scaleX += (bone.data.scaleX * (prevFrameX + (frames[frameIndex + FRAME_X] - prevFrameX) * percent) - bone.scaleX) * alpha;
bone.scaleY += (bone.data.scaleY * (prevFrameY + (frames[frameIndex + FRAME_Y] - prevFrameY) * percent) - bone.scaleY) * alpha;
}
}
public class ColorTimeline : CurveTimeline {
protected const int PREV_FRAME_TIME = -5;
protected const int FRAME_R = 1;
protected const int FRAME_G = 2;
protected const int FRAME_B = 3;
protected const int FRAME_A = 4;
internal int slotIndex;
internal float[] frames;
public int SlotIndex { get { return slotIndex; } set { slotIndex = value; } }
public float[] Frames { get { return frames; } set { frames = value; } } // time, r, g, b, a, ...
public ColorTimeline (int frameCount)
: base(frameCount) {
frames = new float[frameCount * 5];
}
/// <summary>Sets the time and value of the specified keyframe.</summary>
public void SetFrame (int frameIndex, float time, float r, float g, float b, float a) {
frameIndex *= 5;
frames[frameIndex] = time;
frames[frameIndex + 1] = r;
frames[frameIndex + 2] = g;
frames[frameIndex + 3] = b;
frames[frameIndex + 4] = a;
}
override public void Apply (Skeleton skeleton, float lastTime, float time, List<Event> firedEvents, float alpha) {
float[] frames = this.frames;
if (time < frames[0]) return; // Time is before first frame.
float r, g, b, a;
if (time >= frames[frames.Length - 5]) {
// Time is after last frame.
int i = frames.Length - 1;
r = frames[i - 3];
g = frames[i - 2];
b = frames[i - 1];
a = frames[i];
} else {
// Interpolate between the previous frame and the current frame.
int frameIndex = Animation.binarySearch(frames, time, 5);
float prevFrameR = frames[frameIndex - 4];
float prevFrameG = frames[frameIndex - 3];
float prevFrameB = frames[frameIndex - 2];
float prevFrameA = frames[frameIndex - 1];
float frameTime = frames[frameIndex];
float percent = 1 - (time - frameTime) / (frames[frameIndex + PREV_FRAME_TIME] - frameTime);
percent = GetCurvePercent(frameIndex / 5 - 1, percent < 0 ? 0 : (percent > 1 ? 1 : percent));
r = prevFrameR + (frames[frameIndex + FRAME_R] - prevFrameR) * percent;
g = prevFrameG + (frames[frameIndex + FRAME_G] - prevFrameG) * percent;
b = prevFrameB + (frames[frameIndex + FRAME_B] - prevFrameB) * percent;
a = prevFrameA + (frames[frameIndex + FRAME_A] - prevFrameA) * percent;
}
Slot slot = skeleton.slots[slotIndex];
if (alpha < 1) {
slot.r += (r - slot.r) * alpha;
slot.g += (g - slot.g) * alpha;
slot.b += (b - slot.b) * alpha;
slot.a += (a - slot.a) * alpha;
} else {
slot.r = r;
slot.g = g;
slot.b = b;
slot.a = a;
}
}
}
public class AttachmentTimeline : Timeline {
internal int slotIndex;
internal float[] frames;
private String[] attachmentNames;
public int SlotIndex { get { return slotIndex; } set { slotIndex = value; } }
public float[] Frames { get { return frames; } set { frames = value; } } // time, ...
public String[] AttachmentNames { get { return attachmentNames; } set { attachmentNames = value; } }
public int FrameCount { get { return frames.Length; } }
public AttachmentTimeline (int frameCount) {
frames = new float[frameCount];
attachmentNames = new String[frameCount];
}
/// <summary>Sets the time and value of the specified keyframe.</summary>
public void SetFrame (int frameIndex, float time, String attachmentName) {
frames[frameIndex] = time;
attachmentNames[frameIndex] = attachmentName;
}
public void Apply (Skeleton skeleton, float lastTime, float time, List<Event> firedEvents, float alpha) {
float[] frames = this.frames;
if (time < frames[0]) {
if (lastTime > time) Apply(skeleton, lastTime, int.MaxValue, null, 0);
return;
} else if (lastTime > time) //
lastTime = -1;
int frameIndex = (time >= frames[frames.Length - 1] ? frames.Length : Animation.binarySearch(frames, time)) - 1;
if (frames[frameIndex] < lastTime) return;
String attachmentName = attachmentNames[frameIndex];
skeleton.slots[slotIndex].Attachment =
attachmentName == null ? null : skeleton.GetAttachment(slotIndex, attachmentName);
}
}
public class EventTimeline : Timeline {
internal float[] frames;
private Event[] events;
public float[] Frames { get { return frames; } set { frames = value; } } // time, ...
public Event[] Events { get { return events; } set { events = value; } }
public int FrameCount { get { return frames.Length; } }
public EventTimeline (int frameCount) {
frames = new float[frameCount];
events = new Event[frameCount];
}
/// <summary>Sets the time and value of the specified keyframe.</summary>
public void SetFrame (int frameIndex, float time, Event e) {
frames[frameIndex] = time;
events[frameIndex] = e;
}
/// <summary>Fires events for frames > lastTime and <= time.</summary>
public void Apply (Skeleton skeleton, float lastTime, float time, List<Event> firedEvents, float alpha) {
if (firedEvents == null) return;
float[] frames = this.frames;
int frameCount = frames.Length;
if (lastTime > time) { // Fire events after last time for looped animations.
Apply(skeleton, lastTime, int.MaxValue, firedEvents, alpha);
lastTime = -1f;
} else if (lastTime >= frames[frameCount - 1]) // Last time is after last frame.
return;
if (time < frames[0]) return; // Time is before first frame.
int frameIndex;
if (lastTime < frames[0])
frameIndex = 0;
else {
frameIndex = Animation.binarySearch(frames, lastTime);
float frame = frames[frameIndex];
while (frameIndex > 0) { // Fire multiple events with the same frame.
if (frames[frameIndex - 1] != frame) break;
frameIndex--;
}
}
for (; frameIndex < frameCount && time >= frames[frameIndex]; frameIndex++)
firedEvents.Add(events[frameIndex]);
}
}
public class DrawOrderTimeline : Timeline {
internal float[] frames;
private int[][] drawOrders;
public float[] Frames { get { return frames; } set { frames = value; } } // time, ...
public int[][] DrawOrders { get { return drawOrders; } set { drawOrders = value; } }
public int FrameCount { get { return frames.Length; } }
public DrawOrderTimeline (int frameCount) {
frames = new float[frameCount];
drawOrders = new int[frameCount][];
}
/// <summary>Sets the time and value of the specified keyframe.</summary>
/// <param name="drawOrder">May be null to use bind pose draw order.</param>
public void SetFrame (int frameIndex, float time, int[] drawOrder) {
frames[frameIndex] = time;
drawOrders[frameIndex] = drawOrder;
}
public void Apply (Skeleton skeleton, float lastTime, float time, List<Event> firedEvents, float alpha) {
float[] frames = this.frames;
if (time < frames[0]) return; // Time is before first frame.
int frameIndex;
if (time >= frames[frames.Length - 1]) // Time is after last frame.
frameIndex = frames.Length - 1;
else
frameIndex = Animation.binarySearch(frames, time) - 1;
List<Slot> drawOrder = skeleton.drawOrder;
List<Slot> slots = skeleton.slots;
int[] drawOrderToSetupIndex = drawOrders[frameIndex];
if (drawOrderToSetupIndex == null) {
drawOrder.Clear();
drawOrder.AddRange(slots);
} else {
for (int i = 0, n = drawOrderToSetupIndex.Length; i < n; i++)
drawOrder[i] = slots[drawOrderToSetupIndex[i]];
}
}
}
public class FFDTimeline : CurveTimeline {
internal int slotIndex;
internal float[] frames;
private float[][] frameVertices;
internal Attachment attachment;
public int SlotIndex { get { return slotIndex; } set { slotIndex = value; } }
public float[] Frames { get { return frames; } set { frames = value; } } // time, ...
public float[][] Vertices { get { return frameVertices; } set { frameVertices = value; } }
public Attachment Attachment { get { return attachment; } set { attachment = value; } }
public FFDTimeline (int frameCount)
: base(frameCount) {
frames = new float[frameCount];
frameVertices = new float[frameCount][];
}
/// <summary>Sets the time and value of the specified keyframe.</summary>
public void SetFrame (int frameIndex, float time, float[] vertices) {
frames[frameIndex] = time;
frameVertices[frameIndex] = vertices;
}
override public void Apply (Skeleton skeleton, float lastTime, float time, List<Event> firedEvents, float alpha) {
Slot slot = skeleton.slots[slotIndex];
if (slot.attachment != attachment) return;
float[] frames = this.frames;
if (time < frames[0]) return; // Time is before first frame.
float[][] frameVertices = this.frameVertices;
int vertexCount = frameVertices[0].Length;
float[] vertices = slot.attachmentVertices;
if (vertices.Length < vertexCount) {
vertices = new float[vertexCount];
slot.attachmentVertices = vertices;
}
if (vertices.Length != vertexCount) alpha = 1; // Don't mix from uninitialized slot vertices.
slot.attachmentVerticesCount = vertexCount;
if (time >= frames[frames.Length - 1]) { // Time is after last frame.
float[] lastVertices = frameVertices[frames.Length - 1];
if (alpha < 1) {
for (int i = 0; i < vertexCount; i++) {
float vertex = vertices[i];
vertices[i] = vertex + (lastVertices[i] - vertex) * alpha;
}
} else
Array.Copy(lastVertices, 0, vertices, 0, vertexCount);
return;
}
// Interpolate between the previous frame and the current frame.
int frameIndex = Animation.binarySearch(frames, time);
float frameTime = frames[frameIndex];
float percent = 1 - (time - frameTime) / (frames[frameIndex - 1] - frameTime);
percent = GetCurvePercent(frameIndex - 1, percent < 0 ? 0 : (percent > 1 ? 1 : percent));
float[] prevVertices = frameVertices[frameIndex - 1];
float[] nextVertices = frameVertices[frameIndex];
if (alpha < 1) {
for (int i = 0; i < vertexCount; i++) {
float prev = prevVertices[i];
float vertex = vertices[i];
vertices[i] = vertex + (prev + (nextVertices[i] - prev) * percent - vertex) * alpha;
}
} else {
for (int i = 0; i < vertexCount; i++) {
float prev = prevVertices[i];
vertices[i] = prev + (nextVertices[i] - prev) * percent;
}
}
}
}
public class IkConstraintTimeline : CurveTimeline {
private const int PREV_FRAME_TIME = -3;
private const int PREV_FRAME_MIX = -2;
private const int PREV_FRAME_BEND_DIRECTION = -1;
private const int FRAME_MIX = 1;
internal int ikConstraintIndex;
internal float[] frames;
public int IkConstraintIndex { get { return ikConstraintIndex; } set { ikConstraintIndex = value; } }
public float[] Frames { get { return frames; } set { frames = value; } } // time, mix, bendDirection, ...
public IkConstraintTimeline (int frameCount)
: base(frameCount) {
frames = new float[frameCount * 3];
}
/** Sets the time, mix and bend direction of the specified keyframe. */
public void SetFrame (int frameIndex, float time, float mix, int bendDirection) {
frameIndex *= 3;
frames[frameIndex] = time;
frames[frameIndex + 1] = mix;
frames[frameIndex + 2] = bendDirection;
}
override public void Apply (Skeleton skeleton, float lastTime, float time, List<Event> firedEvents, float alpha) {
float[] frames = this.frames;
if (time < frames[0]) return; // Time is before first frame.
IkConstraint ikConstraint = skeleton.ikConstraints[ikConstraintIndex];
if (time >= frames[frames.Length - 3]) { // Time is after last frame.
ikConstraint.mix += (frames[frames.Length - 2] - ikConstraint.mix) * alpha;
ikConstraint.bendDirection = (int)frames[frames.Length - 1];
return;
}
// Interpolate between the previous frame and the current frame.
int frameIndex = Animation.binarySearch(frames, time, 3);
float prevFrameMix = frames[frameIndex + PREV_FRAME_MIX];
float frameTime = frames[frameIndex];
float percent = 1 - (time - frameTime) / (frames[frameIndex + PREV_FRAME_TIME] - frameTime);
percent = GetCurvePercent(frameIndex / 3 - 1, percent < 0 ? 0 : (percent > 1 ? 1 : percent));
float mix = prevFrameMix + (frames[frameIndex + FRAME_MIX] - prevFrameMix) * percent;
ikConstraint.mix += (mix - ikConstraint.mix) * alpha;
ikConstraint.bendDirection = (int)frames[frameIndex + PREV_FRAME_BEND_DIRECTION];
}
}
public class FlipXTimeline : Timeline {
internal int boneIndex;
internal float[] frames;
public int BoneIndex { get { return boneIndex; } set { boneIndex = value; } }
public float[] Frames { get { return frames; } set { frames = value; } } // time, flip, ...
public int FrameCount { get { return frames.Length >> 1; } }
public FlipXTimeline (int frameCount) {
frames = new float[frameCount << 1];
}
/// <summary>Sets the time and value of the specified keyframe.</summary>
public void SetFrame (int frameIndex, float time, bool flip) {
frameIndex *= 2;
frames[frameIndex] = time;
frames[frameIndex + 1] = flip ? 1 : 0;
}
public void Apply (Skeleton skeleton, float lastTime, float time, List<Event> firedEvents, float alpha) {
float[] frames = this.frames;
if (time < frames[0]) {
if (lastTime > time) Apply(skeleton, lastTime, int.MaxValue, null, 0);
return;
} else if (lastTime > time) //
lastTime = -1;
int frameIndex = (time >= frames[frames.Length - 2] ? frames.Length : Animation.binarySearch(frames, time, 2)) - 2;
if (frames[frameIndex] < lastTime) return;
SetFlip(skeleton.bones[boneIndex], frames[frameIndex + 1] != 0);
}
virtual protected void SetFlip (Bone bone, bool flip) {
bone.flipX = flip;
}
}
public class FlipYTimeline : FlipXTimeline {
public FlipYTimeline (int frameCount)
: base(frameCount) {
}
override protected void SetFlip (Bone bone, bool flip) {
bone.flipY = flip;
}
}
}

View File

@@ -0,0 +1,299 @@
/******************************************************************************
* Spine Runtimes Software License
* Version 2.1
*
* Copyright (c) 2013, Esoteric Software
* All rights reserved.
*
* You are granted a perpetual, non-exclusive, non-sublicensable and
* non-transferable license to install, execute and perform the Spine Runtimes
* Software (the "Software") solely for internal use. Without the written
* permission of Esoteric Software (typically granted by licensing Spine), you
* may not (a) modify, translate, adapt or otherwise create derivative works,
* improvements of the Software or develop new applications using the Software
* or (b) remove, delete, alter or obscure any trademarks or any copyright,
* trademark, patent or other intellectual property or proprietary rights
* notices on or in the Software, including any copy thereof. Redistributions
* in binary or source form must include this license and terms.
*
* THIS SOFTWARE IS PROVIDED BY ESOTERIC SOFTWARE "AS IS" AND ANY EXPRESS OR
* IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
* MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO
* EVENT SHALL ESOTERIC SOFTARE BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
* PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS;
* OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
* WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR
* OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
* ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*****************************************************************************/
using System;
using System.Collections.Generic;
using System.Text;
namespace SpineRuntime21 {
public class AnimationState {
private AnimationStateData data;
private List<TrackEntry> tracks = new List<TrackEntry>();
private List<Event> events = new List<Event>();
private float timeScale = 1;
public AnimationStateData Data { get { return data; } }
public float TimeScale { get { return timeScale; } set { timeScale = value; } }
public delegate void StartEndDelegate(AnimationState state, int trackIndex);
public event StartEndDelegate Start;
public event StartEndDelegate End;
public delegate void EventDelegate(AnimationState state, int trackIndex, Event e);
public event EventDelegate Event;
public delegate void CompleteDelegate(AnimationState state, int trackIndex, int loopCount);
public event CompleteDelegate Complete;
public AnimationState (AnimationStateData data) {
if (data == null) throw new ArgumentNullException("data cannot be null.");
this.data = data;
}
public void Update (float delta) {
delta *= timeScale;
for (int i = 0; i < tracks.Count; i++) {
TrackEntry current = tracks[i];
if (current == null) continue;
float trackDelta = delta * current.timeScale;
float time = current.time + trackDelta;
float endTime = current.endTime;
current.time = time;
if (current.previous != null) {
current.previous.time += trackDelta;
current.mixTime += trackDelta;
}
// Check if completed the animation or a loop iteration.
if (current.loop ? (current.lastTime % endTime > time % endTime) : (current.lastTime < endTime && time >= endTime)) {
int count = (int)(time / endTime);
current.OnComplete(this, i, count);
if (Complete != null) Complete(this, i, count);
}
TrackEntry next = current.next;
if (next != null) {
next.time = current.lastTime - next.delay;
if (next.time >= 0) SetCurrent(i, next);
} else {
// End non-looping animation when it reaches its end time and there is no next entry.
if (!current.loop && current.lastTime >= current.endTime) ClearTrack(i);
}
}
}
public void Apply (Skeleton skeleton) {
List<Event> events = this.events;
for (int i = 0; i < tracks.Count; i++) {
TrackEntry current = tracks[i];
if (current == null) continue;
events.Clear();
float time = current.time;
bool loop = current.loop;
if (!loop && time > current.endTime) time = current.endTime;
TrackEntry previous = current.previous;
if (previous == null) {
if (current.mix == 1)
current.animation.Apply(skeleton, current.lastTime, time, loop, events);
else
current.animation.Mix(skeleton, current.lastTime, time, loop, events, current.mix);
} else {
float previousTime = previous.time;
if (!previous.loop && previousTime > previous.endTime) previousTime = previous.endTime;
previous.animation.Apply(skeleton, previousTime, previousTime, previous.loop, null);
float alpha = current.mixTime / current.mixDuration * current.mix;
if (alpha >= 1) {
alpha = 1;
current.previous = null;
}
current.animation.Mix(skeleton, current.lastTime, time, loop, events, alpha);
}
for (int ii = 0, nn = events.Count; ii < nn; ii++) {
Event e = events[ii];
current.OnEvent(this, i, e);
if (Event != null) Event(this, i, e);
}
current.lastTime = current.time;
}
}
public void ClearTracks () {
for (int i = 0, n = tracks.Count; i < n; i++)
ClearTrack(i);
tracks.Clear();
}
public void ClearTrack (int trackIndex) {
if (trackIndex >= tracks.Count) return;
TrackEntry current = tracks[trackIndex];
if (current == null) return;
current.OnEnd(this, trackIndex);
if (End != null) End(this, trackIndex);
tracks[trackIndex] = null;
}
private TrackEntry ExpandToIndex (int index) {
if (index < tracks.Count) return tracks[index];
while (index >= tracks.Count)
tracks.Add(null);
return null;
}
private void SetCurrent (int index, TrackEntry entry) {
TrackEntry current = ExpandToIndex(index);
if (current != null) {
TrackEntry previous = current.previous;
current.previous = null;
current.OnEnd(this, index);
if (End != null) End(this, index);
entry.mixDuration = data.GetMix(current.animation, entry.animation);
if (entry.mixDuration > 0) {
entry.mixTime = 0;
// If a mix is in progress, mix from the closest animation.
if (previous != null && current.mixTime / current.mixDuration < 0.5f)
entry.previous = previous;
else
entry.previous = current;
}
}
tracks[index] = entry;
entry.OnStart(this, index);
if (Start != null) Start(this, index);
}
public TrackEntry SetAnimation (int trackIndex, String animationName, bool loop) {
Animation animation = data.skeletonData.FindAnimation(animationName);
if (animation == null) throw new ArgumentException("Animation not found: " + animationName);
return SetAnimation(trackIndex, animation, loop);
}
/// <summary>Set the current animation. Any queued animations are cleared.</summary>
public TrackEntry SetAnimation (int trackIndex, Animation animation, bool loop) {
if (animation == null) throw new ArgumentException("animation cannot be null.");
TrackEntry entry = new TrackEntry();
entry.animation = animation;
entry.loop = loop;
entry.time = 0;
entry.endTime = animation.Duration;
SetCurrent(trackIndex, entry);
return entry;
}
public TrackEntry AddAnimation (int trackIndex, String animationName, bool loop, float delay) {
Animation animation = data.skeletonData.FindAnimation(animationName);
if (animation == null) throw new ArgumentException("Animation not found: " + animationName);
return AddAnimation(trackIndex, animation, loop, delay);
}
/// <summary>Adds an animation to be played delay seconds after the current or last queued animation.</summary>
/// <param name="delay">May be <= 0 to use duration of previous animation minus any mix duration plus the negative delay.</param>
public TrackEntry AddAnimation (int trackIndex, Animation animation, bool loop, float delay) {
if (animation == null) throw new ArgumentException("animation cannot be null.");
TrackEntry entry = new TrackEntry();
entry.animation = animation;
entry.loop = loop;
entry.time = 0;
entry.endTime = animation.Duration;
TrackEntry last = ExpandToIndex(trackIndex);
if (last != null) {
while (last.next != null)
last = last.next;
last.next = entry;
} else
tracks[trackIndex] = entry;
if (delay <= 0) {
if (last != null)
delay += last.endTime - data.GetMix(last.animation, animation);
else
delay = 0;
}
entry.delay = delay;
return entry;
}
/// <returns>May be null.</returns>
public TrackEntry GetCurrent (int trackIndex) {
if (trackIndex >= tracks.Count) return null;
return tracks[trackIndex];
}
override public String ToString () {
StringBuilder buffer = new StringBuilder();
for (int i = 0, n = tracks.Count; i < n; i++) {
TrackEntry entry = tracks[i];
if (entry == null) continue;
if (buffer.Length > 0) buffer.Append(", ");
buffer.Append(entry.ToString());
}
if (buffer.Length == 0) return "<none>";
return buffer.ToString();
}
}
public class TrackEntry {
internal TrackEntry next, previous;
internal Animation animation;
internal bool loop;
internal float delay, time, lastTime = -1, endTime, timeScale = 1;
internal float mixTime, mixDuration, mix = 1;
public Animation Animation { get { return animation; } }
public float Delay { get { return delay; } set { delay = value; } }
public float Time { get { return time; } set { time = value; } }
public float LastTime { get { return lastTime; } set { lastTime = value; } }
public float EndTime { get { return endTime; } set { endTime = value; } }
public float TimeScale { get { return timeScale; } set { timeScale = value; } }
public float Mix { get { return mix; } set { mix = value; } }
public bool Loop { get { return loop; } set { loop = value; } }
public event AnimationState.StartEndDelegate Start;
public event AnimationState.StartEndDelegate End;
public event AnimationState.EventDelegate Event;
public event AnimationState.CompleteDelegate Complete;
internal void OnStart (AnimationState state, int index) {
if (Start != null) Start(state, index);
}
internal void OnEnd (AnimationState state, int index) {
if (End != null) End(state, index);
}
internal void OnEvent (AnimationState state, int index, Event e) {
if (Event != null) Event(state, index, e);
}
internal void OnComplete (AnimationState state, int index, int loopCount) {
if (Complete != null) Complete(state, index, loopCount);
}
override public String ToString () {
return animation == null ? "<none>" : animation.name;
}
}
}

View File

@@ -0,0 +1,70 @@
/******************************************************************************
* Spine Runtimes Software License
* Version 2.1
*
* Copyright (c) 2013, Esoteric Software
* All rights reserved.
*
* You are granted a perpetual, non-exclusive, non-sublicensable and
* non-transferable license to install, execute and perform the Spine Runtimes
* Software (the "Software") solely for internal use. Without the written
* permission of Esoteric Software (typically granted by licensing Spine), you
* may not (a) modify, translate, adapt or otherwise create derivative works,
* improvements of the Software or develop new applications using the Software
* or (b) remove, delete, alter or obscure any trademarks or any copyright,
* trademark, patent or other intellectual property or proprietary rights
* notices on or in the Software, including any copy thereof. Redistributions
* in binary or source form must include this license and terms.
*
* THIS SOFTWARE IS PROVIDED BY ESOTERIC SOFTWARE "AS IS" AND ANY EXPRESS OR
* IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
* MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO
* EVENT SHALL ESOTERIC SOFTARE BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
* PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS;
* OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
* WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR
* OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
* ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*****************************************************************************/
using System;
using System.Collections.Generic;
namespace SpineRuntime21 {
public class AnimationStateData {
internal SkeletonData skeletonData;
private Dictionary<KeyValuePair<Animation, Animation>, float> animationToMixTime = new Dictionary<KeyValuePair<Animation, Animation>, float>();
internal float defaultMix;
public SkeletonData SkeletonData { get { return skeletonData; } }
public float DefaultMix { get { return defaultMix; } set { defaultMix = value; } }
public AnimationStateData (SkeletonData skeletonData) {
this.skeletonData = skeletonData;
}
public void SetMix (String fromName, String toName, float duration) {
Animation from = skeletonData.FindAnimation(fromName);
if (from == null) throw new ArgumentException("Animation not found: " + fromName);
Animation to = skeletonData.FindAnimation(toName);
if (to == null) throw new ArgumentException("Animation not found: " + toName);
SetMix(from, to, duration);
}
public void SetMix (Animation from, Animation to, float duration) {
if (from == null) throw new ArgumentNullException("from cannot be null.");
if (to == null) throw new ArgumentNullException("to cannot be null.");
KeyValuePair<Animation, Animation> key = new KeyValuePair<Animation, Animation>(from, to);
animationToMixTime.Remove(key);
animationToMixTime.Add(key, duration);
}
public float GetMix (Animation from, Animation to) {
KeyValuePair<Animation, Animation> key = new KeyValuePair<Animation, Animation>(from, to);
float duration;
if (animationToMixTime.TryGetValue(key, out duration)) return duration;
return defaultMix;
}
}
}

View File

@@ -0,0 +1,288 @@
/******************************************************************************
* Spine Runtimes Software License
* Version 2.1
*
* Copyright (c) 2013, Esoteric Software
* All rights reserved.
*
* You are granted a perpetual, non-exclusive, non-sublicensable and
* non-transferable license to install, execute and perform the Spine Runtimes
* Software (the "Software") solely for internal use. Without the written
* permission of Esoteric Software (typically granted by licensing Spine), you
* may not (a) modify, translate, adapt or otherwise create derivative works,
* improvements of the Software or develop new applications using the Software
* or (b) remove, delete, alter or obscure any trademarks or any copyright,
* trademark, patent or other intellectual property or proprietary rights
* notices on or in the Software, including any copy thereof. Redistributions
* in binary or source form must include this license and terms.
*
* THIS SOFTWARE IS PROVIDED BY ESOTERIC SOFTWARE "AS IS" AND ANY EXPRESS OR
* IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
* MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO
* EVENT SHALL ESOTERIC SOFTARE BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
* PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS;
* OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
* WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR
* OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
* ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*****************************************************************************/
using System;
using System.Collections.Generic;
using System.IO;
using System.Reflection;
#if WINDOWS_STOREAPP
using System.Threading.Tasks;
using Windows.Storage;
#endif
namespace SpineRuntime21 {
public class Atlas {
List<AtlasPage> pages = new List<AtlasPage>();
List<AtlasRegion> regions = new List<AtlasRegion>();
TextureLoader textureLoader;
#if WINDOWS_STOREAPP
private async Task ReadFile(string path, TextureLoader textureLoader) {
var folder = Windows.ApplicationModel.Package.Current.InstalledLocation;
var file = await folder.GetFileAsync(path).AsTask().ConfigureAwait(false);
using (var reader = new StreamReader(await file.OpenStreamForReadAsync().ConfigureAwait(false))) {
try {
Load(reader, Path.GetDirectoryName(path), textureLoader);
} catch (Exception ex) {
throw new Exception("Error reading atlas file: " + path, ex);
}
}
}
public Atlas(String path, TextureLoader textureLoader) {
this.ReadFile(path, textureLoader).Wait();
}
#else
public Atlas (String path, TextureLoader textureLoader) {
#if WINDOWS_PHONE
Stream stream = Microsoft.Xna.Framework.TitleContainer.OpenStream(path);
using (StreamReader reader = new StreamReader(stream))
{
#else
using (StreamReader reader = new StreamReader(path)) {
#endif
try {
Load(reader, Path.GetDirectoryName(path), textureLoader);
} catch (Exception ex) {
throw new Exception("Error reading atlas file: " + path, ex);
}
}
}
#endif
public Atlas (TextReader reader, String dir, TextureLoader textureLoader) {
Load(reader, dir, textureLoader);
}
public Atlas (List<AtlasPage> pages, List<AtlasRegion> regions) {
this.pages = pages;
this.regions = regions;
this.textureLoader = null;
}
private void Load (TextReader reader, String imagesDir, TextureLoader textureLoader) {
if (textureLoader == null) throw new ArgumentNullException("textureLoader cannot be null.");
this.textureLoader = textureLoader;
String[] tuple = new String[4];
AtlasPage page = null;
while (true) {
String line = reader.ReadLine();
if (line == null) break;
if (line.Trim().Length == 0)
page = null;
else if (page == null) {
page = new AtlasPage();
page.name = line;
if (readTuple(reader, tuple) == 2) { // size is only optional for an atlas packed with an old TexturePacker.
page.width = int.Parse(tuple[0]);
page.height = int.Parse(tuple[1]);
readTuple(reader, tuple);
}
page.format = (Format)Enum.Parse(typeof(Format), tuple[0], false);
readTuple(reader, tuple);
page.minFilter = (TextureFilter)Enum.Parse(typeof(TextureFilter), tuple[0], false);
page.magFilter = (TextureFilter)Enum.Parse(typeof(TextureFilter), tuple[1], false);
String direction = readValue(reader);
page.uWrap = TextureWrap.ClampToEdge;
page.vWrap = TextureWrap.ClampToEdge;
if (direction == "x")
page.uWrap = TextureWrap.Repeat;
else if (direction == "y")
page.vWrap = TextureWrap.Repeat;
else if (direction == "xy")
page.uWrap = page.vWrap = TextureWrap.Repeat;
textureLoader.Load(page, Path.Combine(imagesDir, line));
pages.Add(page);
} else {
AtlasRegion region = new AtlasRegion();
region.name = line;
region.page = page;
region.rotate = Boolean.Parse(readValue(reader));
readTuple(reader, tuple);
int x = int.Parse(tuple[0]);
int y = int.Parse(tuple[1]);
readTuple(reader, tuple);
int width = int.Parse(tuple[0]);
int height = int.Parse(tuple[1]);
region.u = x / (float)page.width;
region.v = y / (float)page.height;
if (region.rotate) {
region.u2 = (x + height) / (float)page.width;
region.v2 = (y + width) / (float)page.height;
} else {
region.u2 = (x + width) / (float)page.width;
region.v2 = (y + height) / (float)page.height;
}
region.x = x;
region.y = y;
region.width = Math.Abs(width);
region.height = Math.Abs(height);
if (readTuple(reader, tuple) == 4) { // split is optional
region.splits = new int[] {int.Parse(tuple[0]), int.Parse(tuple[1]),
int.Parse(tuple[2]), int.Parse(tuple[3])};
if (readTuple(reader, tuple) == 4) { // pad is optional, but only present with splits
region.pads = new int[] {int.Parse(tuple[0]), int.Parse(tuple[1]),
int.Parse(tuple[2]), int.Parse(tuple[3])};
readTuple(reader, tuple);
}
}
region.originalWidth = int.Parse(tuple[0]);
region.originalHeight = int.Parse(tuple[1]);
readTuple(reader, tuple);
region.offsetX = int.Parse(tuple[0]);
region.offsetY = int.Parse(tuple[1]);
region.index = int.Parse(readValue(reader));
regions.Add(region);
}
}
}
static String readValue (TextReader reader) {
String line = reader.ReadLine();
int colon = line.IndexOf(':');
if (colon == -1) throw new Exception("Invalid line: " + line);
return line.Substring(colon + 1).Trim();
}
/// <summary>Returns the number of tuple values read (1, 2 or 4).</summary>
static int readTuple (TextReader reader, String[] tuple) {
String line = reader.ReadLine();
int colon = line.IndexOf(':');
if (colon == -1) throw new Exception("Invalid line: " + line);
int i = 0, lastMatch = colon + 1;
for (; i < 3; i++) {
int comma = line.IndexOf(',', lastMatch);
if (comma == -1) break;
tuple[i] = line.Substring(lastMatch, comma - lastMatch).Trim();
lastMatch = comma + 1;
}
tuple[i] = line.Substring(lastMatch).Trim();
return i + 1;
}
public void FlipV () {
for (int i = 0, n = regions.Count; i < n; i++) {
AtlasRegion region = regions[i];
region.v = 1 - region.v;
region.v2 = 1 - region.v2;
}
}
/// <summary>Returns the first region found with the specified name. This method uses string comparison to find the region, so the result
/// should be cached rather than calling this method multiple times.</summary>
/// <returns>The region, or null.</returns>
public AtlasRegion FindRegion (String name) {
for (int i = 0, n = regions.Count; i < n; i++)
if (regions[i].name == name) return regions[i];
return null;
}
public void Dispose () {
if (textureLoader == null) return;
for (int i = 0, n = pages.Count; i < n; i++)
textureLoader.Unload(pages[i].rendererObject);
}
}
public enum Format {
Alpha,
Intensity,
LuminanceAlpha,
RGB565,
RGBA4444,
RGB888,
RGBA8888
}
public enum TextureFilter {
Nearest,
Linear,
MipMap,
MipMapNearestNearest,
MipMapLinearNearest,
MipMapNearestLinear,
MipMapLinearLinear
}
public enum TextureWrap {
MirroredRepeat,
ClampToEdge,
Repeat
}
public class AtlasPage {
public String name;
public Format format;
public TextureFilter minFilter;
public TextureFilter magFilter;
public TextureWrap uWrap;
public TextureWrap vWrap;
public Object rendererObject;
public int width, height;
}
public class AtlasRegion {
public AtlasPage page;
public String name;
public int x, y, width, height;
public float u, v, u2, v2;
public float offsetX, offsetY;
public int originalWidth, originalHeight;
public int index;
public bool rotate;
public int[] splits;
public int[] pads;
}
public interface TextureLoader {
void Load (AtlasPage page, String path);
void Unload (Object texture);
}
}

View File

@@ -0,0 +1,111 @@
/******************************************************************************
* Spine Runtimes Software License
* Version 2.1
*
* Copyright (c) 2013, Esoteric Software
* All rights reserved.
*
* You are granted a perpetual, non-exclusive, non-sublicensable and
* non-transferable license to install, execute and perform the Spine Runtimes
* Software (the "Software") solely for internal use. Without the written
* permission of Esoteric Software (typically granted by licensing Spine), you
* may not (a) modify, translate, adapt or otherwise create derivative works,
* improvements of the Software or develop new applications using the Software
* or (b) remove, delete, alter or obscure any trademarks or any copyright,
* trademark, patent or other intellectual property or proprietary rights
* notices on or in the Software, including any copy thereof. Redistributions
* in binary or source form must include this license and terms.
*
* THIS SOFTWARE IS PROVIDED BY ESOTERIC SOFTWARE "AS IS" AND ANY EXPRESS OR
* IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
* MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO
* EVENT SHALL ESOTERIC SOFTARE BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
* PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS;
* OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
* WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR
* OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
* ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*****************************************************************************/
using System;
namespace SpineRuntime21 {
public class AtlasAttachmentLoader : AttachmentLoader {
private Atlas[] atlasArray;
public AtlasAttachmentLoader (params Atlas[] atlasArray) {
if (atlasArray == null) throw new ArgumentNullException("atlas array cannot be null.");
this.atlasArray = atlasArray;
}
public RegionAttachment NewRegionAttachment (Skin skin, String name, String path) {
AtlasRegion region = FindRegion(path);
if (region == null) throw new Exception("Region not found in atlas: " + path + " (region attachment: " + name + ")");
RegionAttachment attachment = new RegionAttachment(name);
attachment.RendererObject = region;
attachment.SetUVs(region.u, region.v, region.u2, region.v2, region.rotate);
attachment.regionOffsetX = region.offsetX;
attachment.regionOffsetY = region.offsetY;
attachment.regionWidth = region.width;
attachment.regionHeight = region.height;
attachment.regionOriginalWidth = region.originalWidth;
attachment.regionOriginalHeight = region.originalHeight;
return attachment;
}
public MeshAttachment NewMeshAttachment (Skin skin, String name, String path) {
AtlasRegion region = FindRegion(path);
if (region == null) throw new Exception("Region not found in atlas: " + path + " (mesh attachment: " + name + ")");
MeshAttachment attachment = new MeshAttachment(name);
attachment.RendererObject = region;
attachment.RegionU = region.u;
attachment.RegionV = region.v;
attachment.RegionU2 = region.u2;
attachment.RegionV2 = region.v2;
attachment.RegionRotate = region.rotate;
attachment.regionOffsetX = region.offsetX;
attachment.regionOffsetY = region.offsetY;
attachment.regionWidth = region.width;
attachment.regionHeight = region.height;
attachment.regionOriginalWidth = region.originalWidth;
attachment.regionOriginalHeight = region.originalHeight;
return attachment;
}
public SkinnedMeshAttachment NewSkinnedMeshAttachment (Skin skin, String name, String path) {
AtlasRegion region = FindRegion(path);
if (region == null) throw new Exception("Region not found in atlas: " + path + " (skinned mesh attachment: " + name + ")");
SkinnedMeshAttachment attachment = new SkinnedMeshAttachment(name);
attachment.RendererObject = region;
attachment.RegionU = region.u;
attachment.RegionV = region.v;
attachment.RegionU2 = region.u2;
attachment.RegionV2 = region.v2;
attachment.RegionRotate = region.rotate;
attachment.regionOffsetX = region.offsetX;
attachment.regionOffsetY = region.offsetY;
attachment.regionWidth = region.width;
attachment.regionHeight = region.height;
attachment.regionOriginalWidth = region.originalWidth;
attachment.regionOriginalHeight = region.originalHeight;
return attachment;
}
public BoundingBoxAttachment NewBoundingBoxAttachment (Skin skin, String name) {
return new BoundingBoxAttachment(name);
}
public AtlasRegion FindRegion(string name) {
AtlasRegion region;
for (int i = 0; i < atlasArray.Length; i++) {
region = atlasArray[i].FindRegion(name);
if (region != null)
return region;
}
return null;
}
}
}

View File

@@ -0,0 +1,46 @@
/******************************************************************************
* Spine Runtimes Software License
* Version 2.1
*
* Copyright (c) 2013, Esoteric Software
* All rights reserved.
*
* You are granted a perpetual, non-exclusive, non-sublicensable and
* non-transferable license to install, execute and perform the Spine Runtimes
* Software (the "Software") solely for internal use. Without the written
* permission of Esoteric Software (typically granted by licensing Spine), you
* may not (a) modify, translate, adapt or otherwise create derivative works,
* improvements of the Software or develop new applications using the Software
* or (b) remove, delete, alter or obscure any trademarks or any copyright,
* trademark, patent or other intellectual property or proprietary rights
* notices on or in the Software, including any copy thereof. Redistributions
* in binary or source form must include this license and terms.
*
* THIS SOFTWARE IS PROVIDED BY ESOTERIC SOFTWARE "AS IS" AND ANY EXPRESS OR
* IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
* MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO
* EVENT SHALL ESOTERIC SOFTARE BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
* PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS;
* OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
* WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR
* OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
* ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*****************************************************************************/
using System;
namespace SpineRuntime21 {
abstract public class Attachment {
public String Name { get; private set; }
public Attachment (String name) {
if (name == null) throw new ArgumentNullException("name cannot be null.");
Name = name;
}
override public String ToString () {
return Name;
}
}
}

View File

@@ -0,0 +1,47 @@
/******************************************************************************
* Spine Runtimes Software License
* Version 2.1
*
* Copyright (c) 2013, Esoteric Software
* All rights reserved.
*
* You are granted a perpetual, non-exclusive, non-sublicensable and
* non-transferable license to install, execute and perform the Spine Runtimes
* Software (the "Software") solely for internal use. Without the written
* permission of Esoteric Software (typically granted by licensing Spine), you
* may not (a) modify, translate, adapt or otherwise create derivative works,
* improvements of the Software or develop new applications using the Software
* or (b) remove, delete, alter or obscure any trademarks or any copyright,
* trademark, patent or other intellectual property or proprietary rights
* notices on or in the Software, including any copy thereof. Redistributions
* in binary or source form must include this license and terms.
*
* THIS SOFTWARE IS PROVIDED BY ESOTERIC SOFTWARE "AS IS" AND ANY EXPRESS OR
* IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
* MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO
* EVENT SHALL ESOTERIC SOFTARE BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
* PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS;
* OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
* WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR
* OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
* ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*****************************************************************************/
using System;
namespace SpineRuntime21 {
public interface AttachmentLoader {
/// <return>May be null to not load any attachment.</return>
RegionAttachment NewRegionAttachment (Skin skin, String name, String path);
/// <return>May be null to not load any attachment.</return>
MeshAttachment NewMeshAttachment (Skin skin, String name, String path);
/// <return>May be null to not load any attachment.</return>
SkinnedMeshAttachment NewSkinnedMeshAttachment (Skin skin, String name, String path);
/// <return>May be null to not load any attachment.</return>
BoundingBoxAttachment NewBoundingBoxAttachment (Skin skin, String name);
}
}

View File

@@ -0,0 +1,35 @@
/******************************************************************************
* Spine Runtimes Software License
* Version 2.1
*
* Copyright (c) 2013, Esoteric Software
* All rights reserved.
*
* You are granted a perpetual, non-exclusive, non-sublicensable and
* non-transferable license to install, execute and perform the Spine Runtimes
* Software (the "Software") solely for internal use. Without the written
* permission of Esoteric Software (typically granted by licensing Spine), you
* may not (a) modify, translate, adapt or otherwise create derivative works,
* improvements of the Software or develop new applications using the Software
* or (b) remove, delete, alter or obscure any trademarks or any copyright,
* trademark, patent or other intellectual property or proprietary rights
* notices on or in the Software, including any copy thereof. Redistributions
* in binary or source form must include this license and terms.
*
* THIS SOFTWARE IS PROVIDED BY ESOTERIC SOFTWARE "AS IS" AND ANY EXPRESS OR
* IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
* MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO
* EVENT SHALL ESOTERIC SOFTARE BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
* PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS;
* OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
* WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR
* OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
* ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*****************************************************************************/
namespace SpineRuntime21 {
public enum AttachmentType {
region, boundingbox, mesh, skinnedmesh
}
}

View File

@@ -0,0 +1,60 @@
/******************************************************************************
* Spine Runtimes Software License
* Version 2.1
*
* Copyright (c) 2013, Esoteric Software
* All rights reserved.
*
* You are granted a perpetual, non-exclusive, non-sublicensable and
* non-transferable license to install, execute and perform the Spine Runtimes
* Software (the "Software") solely for internal use. Without the written
* permission of Esoteric Software (typically granted by licensing Spine), you
* may not (a) modify, translate, adapt or otherwise create derivative works,
* improvements of the Software or develop new applications using the Software
* or (b) remove, delete, alter or obscure any trademarks or any copyright,
* trademark, patent or other intellectual property or proprietary rights
* notices on or in the Software, including any copy thereof. Redistributions
* in binary or source form must include this license and terms.
*
* THIS SOFTWARE IS PROVIDED BY ESOTERIC SOFTWARE "AS IS" AND ANY EXPRESS OR
* IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
* MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO
* EVENT SHALL ESOTERIC SOFTARE BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
* PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS;
* OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
* WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR
* OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
* ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*****************************************************************************/
using System;
namespace SpineRuntime21 {
/// <summary>Attachment that has a polygon for bounds checking.</summary>
public class BoundingBoxAttachment : Attachment {
internal float[] vertices;
public float[] Vertices { get { return vertices; } set { vertices = value; } }
public BoundingBoxAttachment (string name)
: base(name) {
}
/// <param name="worldVertices">Must have at least the same length as this attachment's vertices.</param>
public void ComputeWorldVertices (Bone bone, float[] worldVertices) {
float x = bone.skeleton.x + bone.worldX, y = bone.skeleton.y + bone.worldY;
float m00 = bone.m00;
float m01 = bone.m01;
float m10 = bone.m10;
float m11 = bone.m11;
float[] vertices = this.vertices;
for (int i = 0, n = vertices.Length; i < n; i += 2) {
float px = vertices[i];
float py = vertices[i + 1];
worldVertices[i] = px * m00 + py * m01 + x;
worldVertices[i + 1] = px * m10 + py * m11 + y;
}
}
}
}

View File

@@ -0,0 +1,108 @@
/******************************************************************************
* Spine Runtimes Software License
* Version 2.1
*
* Copyright (c) 2013, Esoteric Software
* All rights reserved.
*
* You are granted a perpetual, non-exclusive, non-sublicensable and
* non-transferable license to install, execute and perform the Spine Runtimes
* Software (the "Software") solely for internal use. Without the written
* permission of Esoteric Software (typically granted by licensing Spine), you
* may not (a) modify, translate, adapt or otherwise create derivative works,
* improvements of the Software or develop new applications using the Software
* or (b) remove, delete, alter or obscure any trademarks or any copyright,
* trademark, patent or other intellectual property or proprietary rights
* notices on or in the Software, including any copy thereof. Redistributions
* in binary or source form must include this license and terms.
*
* THIS SOFTWARE IS PROVIDED BY ESOTERIC SOFTWARE "AS IS" AND ANY EXPRESS OR
* IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
* MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO
* EVENT SHALL ESOTERIC SOFTARE BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
* PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS;
* OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
* WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR
* OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
* ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*****************************************************************************/
using System;
namespace SpineRuntime21 {
/// <summary>Attachment that displays a texture region.</summary>
public class MeshAttachment : Attachment {
internal float[] vertices, uvs, regionUVs;
internal int[] triangles;
internal float regionOffsetX, regionOffsetY, regionWidth, regionHeight, regionOriginalWidth, regionOriginalHeight;
internal float r = 1, g = 1, b = 1, a = 1;
public int HullLength { get; set; }
public float[] Vertices { get { return vertices; } set { vertices = value; } }
public float[] RegionUVs { get { return regionUVs; } set { regionUVs = value; } }
public float[] UVs { get { return uvs; } set { uvs = value; } }
public int[] Triangles { get { return triangles; } set { triangles = value; } }
public float R { get { return r; } set { r = value; } }
public float G { get { return g; } set { g = value; } }
public float B { get { return b; } set { b = value; } }
public float A { get { return a; } set { a = value; } }
public String Path { get; set; }
public Object RendererObject { get; set; }
public float RegionU { get; set; }
public float RegionV { get; set; }
public float RegionU2 { get; set; }
public float RegionV2 { get; set; }
public bool RegionRotate { get; set; }
public float RegionOffsetX { get { return regionOffsetX; } set { regionOffsetX = value; } }
public float RegionOffsetY { get { return regionOffsetY; } set { regionOffsetY = value; } } // Pixels stripped from the bottom left, unrotated.
public float RegionWidth { get { return regionWidth; } set { regionWidth = value; } }
public float RegionHeight { get { return regionHeight; } set { regionHeight = value; } } // Unrotated, stripped size.
public float RegionOriginalWidth { get { return regionOriginalWidth; } set { regionOriginalWidth = value; } }
public float RegionOriginalHeight { get { return regionOriginalHeight; } set { regionOriginalHeight = value; } } // Unrotated, unstripped size.
// Nonessential.
public int[] Edges { get; set; }
public float Width { get; set; }
public float Height { get; set; }
public MeshAttachment (string name)
: base(name) {
}
public void UpdateUVs () {
float u = RegionU, v = RegionV, width = RegionU2 - RegionU, height = RegionV2 - RegionV;
float[] regionUVs = this.regionUVs;
if (this.uvs == null || this.uvs.Length != regionUVs.Length) this.uvs = new float[regionUVs.Length];
float[] uvs = this.uvs;
if (RegionRotate) {
for (int i = 0, n = uvs.Length; i < n; i += 2) {
uvs[i] = u + regionUVs[i + 1] * width;
uvs[i + 1] = v + height - regionUVs[i] * height;
}
} else {
for (int i = 0, n = uvs.Length; i < n; i += 2) {
uvs[i] = u + regionUVs[i] * width;
uvs[i + 1] = v + regionUVs[i + 1] * height;
}
}
}
public void ComputeWorldVertices (Slot slot, float[] worldVertices) {
Bone bone = slot.bone;
float x = bone.skeleton.x + bone.worldX, y = bone.skeleton.y + bone.worldY;
float m00 = bone.m00, m01 = bone.m01, m10 = bone.m10, m11 = bone.m11;
float[] vertices = this.vertices;
int verticesCount = vertices.Length;
if (slot.attachmentVerticesCount == verticesCount) vertices = slot.AttachmentVertices;
for (int i = 0; i < verticesCount; i += 2) {
float vx = vertices[i];
float vy = vertices[i + 1];
worldVertices[i] = vx * m00 + vy * m01 + x;
worldVertices[i + 1] = vx * m10 + vy * m11 + y;
}
}
}
}

View File

@@ -0,0 +1,151 @@
/******************************************************************************
* Spine Runtimes Software License
* Version 2.1
*
* Copyright (c) 2013, Esoteric Software
* All rights reserved.
*
* You are granted a perpetual, non-exclusive, non-sublicensable and
* non-transferable license to install, execute and perform the Spine Runtimes
* Software (the "Software") solely for internal use. Without the written
* permission of Esoteric Software (typically granted by licensing Spine), you
* may not (a) modify, translate, adapt or otherwise create derivative works,
* improvements of the Software or develop new applications using the Software
* or (b) remove, delete, alter or obscure any trademarks or any copyright,
* trademark, patent or other intellectual property or proprietary rights
* notices on or in the Software, including any copy thereof. Redistributions
* in binary or source form must include this license and terms.
*
* THIS SOFTWARE IS PROVIDED BY ESOTERIC SOFTWARE "AS IS" AND ANY EXPRESS OR
* IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
* MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO
* EVENT SHALL ESOTERIC SOFTARE BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
* PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS;
* OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
* WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR
* OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
* ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*****************************************************************************/
using System;
namespace SpineRuntime21 {
/// <summary>Attachment that displays a texture region.</summary>
public class RegionAttachment : Attachment {
public const int X1 = 0;
public const int Y1 = 1;
public const int X2 = 2;
public const int Y2 = 3;
public const int X3 = 4;
public const int Y3 = 5;
public const int X4 = 6;
public const int Y4 = 7;
internal float x, y, rotation, scaleX = 1, scaleY = 1, width, height;
internal float regionOffsetX, regionOffsetY, regionWidth, regionHeight, regionOriginalWidth, regionOriginalHeight;
internal float[] offset = new float[8], uvs = new float[8];
internal float r = 1, g = 1, b = 1, a = 1;
public float X { get { return x; } set { x = value; } }
public float Y { get { return y; } set { y = value; } }
public float Rotation { get { return rotation; } set { rotation = value; } }
public float ScaleX { get { return scaleX; } set { scaleX = value; } }
public float ScaleY { get { return scaleY; } set { scaleY = value; } }
public float Width { get { return width; } set { width = value; } }
public float Height { get { return height; } set { height = value; } }
public float R { get { return r; } set { r = value; } }
public float G { get { return g; } set { g = value; } }
public float B { get { return b; } set { b = value; } }
public float A { get { return a; } set { a = value; } }
public String Path { get; set; }
public Object RendererObject { get; set; }
public float RegionOffsetX { get { return regionOffsetX; } set { regionOffsetX = value; } }
public float RegionOffsetY { get { return regionOffsetY; } set { regionOffsetY = value; } } // Pixels stripped from the bottom left, unrotated.
public float RegionWidth { get { return regionWidth; } set { regionWidth = value; } }
public float RegionHeight { get { return regionHeight; } set { regionHeight = value; } } // Unrotated, stripped size.
public float RegionOriginalWidth { get { return regionOriginalWidth; } set { regionOriginalWidth = value; } }
public float RegionOriginalHeight { get { return regionOriginalHeight; } set { regionOriginalHeight = value; } } // Unrotated, unstripped size.
public float[] Offset { get { return offset; } }
public float[] UVs { get { return uvs; } }
public RegionAttachment (string name)
: base(name) {
}
public void SetUVs (float u, float v, float u2, float v2, bool rotate) {
float[] uvs = this.uvs;
if (rotate) {
uvs[X2] = u;
uvs[Y2] = v2;
uvs[X3] = u;
uvs[Y3] = v;
uvs[X4] = u2;
uvs[Y4] = v;
uvs[X1] = u2;
uvs[Y1] = v2;
} else {
uvs[X1] = u;
uvs[Y1] = v2;
uvs[X2] = u;
uvs[Y2] = v;
uvs[X3] = u2;
uvs[Y3] = v;
uvs[X4] = u2;
uvs[Y4] = v2;
}
}
public void UpdateOffset () {
float width = this.width;
float height = this.height;
float scaleX = this.scaleX;
float scaleY = this.scaleY;
float regionScaleX = width / regionOriginalWidth * scaleX;
float regionScaleY = height / regionOriginalHeight * scaleY;
float localX = -width / 2 * scaleX + regionOffsetX * regionScaleX;
float localY = -height / 2 * scaleY + regionOffsetY * regionScaleY;
float localX2 = localX + regionWidth * regionScaleX;
float localY2 = localY + regionHeight * regionScaleY;
float radians = rotation * (float)Math.PI / 180;
float cos = (float)Math.Cos(radians);
float sin = (float)Math.Sin(radians);
float x = this.x;
float y = this.y;
float localXCos = localX * cos + x;
float localXSin = localX * sin;
float localYCos = localY * cos + y;
float localYSin = localY * sin;
float localX2Cos = localX2 * cos + x;
float localX2Sin = localX2 * sin;
float localY2Cos = localY2 * cos + y;
float localY2Sin = localY2 * sin;
float[] offset = this.offset;
offset[X1] = localXCos - localYSin;
offset[Y1] = localYCos + localXSin;
offset[X2] = localXCos - localY2Sin;
offset[Y2] = localY2Cos + localXSin;
offset[X3] = localX2Cos - localY2Sin;
offset[Y3] = localY2Cos + localX2Sin;
offset[X4] = localX2Cos - localYSin;
offset[Y4] = localYCos + localX2Sin;
}
public void ComputeWorldVertices (Bone bone, float[] worldVertices) {
float x = bone.skeleton.x + bone.worldX, y = bone.skeleton.y + bone.worldY;
float m00 = bone.m00, m01 = bone.m01, m10 = bone.m10, m11 = bone.m11;
float[] offset = this.offset;
worldVertices[X1] = offset[X1] * m00 + offset[Y1] * m01 + x;
worldVertices[Y1] = offset[X1] * m10 + offset[Y1] * m11 + y;
worldVertices[X2] = offset[X2] * m00 + offset[Y2] * m01 + x;
worldVertices[Y2] = offset[X2] * m10 + offset[Y2] * m11 + y;
worldVertices[X3] = offset[X3] * m00 + offset[Y3] * m01 + x;
worldVertices[Y3] = offset[X3] * m10 + offset[Y3] * m11 + y;
worldVertices[X4] = offset[X4] * m00 + offset[Y4] * m01 + x;
worldVertices[Y4] = offset[X4] * m10 + offset[Y4] * m11 + y;
}
}
}

View File

@@ -0,0 +1,132 @@
/******************************************************************************
* Spine Runtimes Software License
* Version 2.1
*
* Copyright (c) 2013, Esoteric Software
* All rights reserved.
*
* You are granted a perpetual, non-exclusive, non-sublicensable and
* non-transferable license to install, execute and perform the Spine Runtimes
* Software (the "Software") solely for internal use. Without the written
* permission of Esoteric Software (typically granted by licensing Spine), you
* may not (a) modify, translate, adapt or otherwise create derivative works,
* improvements of the Software or develop new applications using the Software
* or (b) remove, delete, alter or obscure any trademarks or any copyright,
* trademark, patent or other intellectual property or proprietary rights
* notices on or in the Software, including any copy thereof. Redistributions
* in binary or source form must include this license and terms.
*
* THIS SOFTWARE IS PROVIDED BY ESOTERIC SOFTWARE "AS IS" AND ANY EXPRESS OR
* IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
* MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO
* EVENT SHALL ESOTERIC SOFTARE BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
* PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS;
* OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
* WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR
* OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
* ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*****************************************************************************/
using System;
using System.Collections.Generic;
namespace SpineRuntime21 {
/// <summary>Attachment that displays a texture region.</summary>
public class SkinnedMeshAttachment : Attachment {
internal int[] bones;
internal float[] weights, uvs, regionUVs;
internal int[] triangles;
internal float regionOffsetX, regionOffsetY, regionWidth, regionHeight, regionOriginalWidth, regionOriginalHeight;
internal float r = 1, g = 1, b = 1, a = 1;
public int HullLength { get; set; }
public int[] Bones { get { return bones; } set { bones = value; } }
public float[] Weights { get { return weights; } set { weights = value; } }
public float[] RegionUVs { get { return regionUVs; } set { regionUVs = value; } }
public float[] UVs { get { return uvs; } set { uvs = value; } }
public int[] Triangles { get { return triangles; } set { triangles = value; } }
public float R { get { return r; } set { r = value; } }
public float G { get { return g; } set { g = value; } }
public float B { get { return b; } set { b = value; } }
public float A { get { return a; } set { a = value; } }
public String Path { get; set; }
public Object RendererObject { get; set; }
public float RegionU { get; set; }
public float RegionV { get; set; }
public float RegionU2 { get; set; }
public float RegionV2 { get; set; }
public bool RegionRotate { get; set; }
public float RegionOffsetX { get { return regionOffsetX; } set { regionOffsetX = value; } }
public float RegionOffsetY { get { return regionOffsetY; } set { regionOffsetY = value; } } // Pixels stripped from the bottom left, unrotated.
public float RegionWidth { get { return regionWidth; } set { regionWidth = value; } }
public float RegionHeight { get { return regionHeight; } set { regionHeight = value; } } // Unrotated, stripped size.
public float RegionOriginalWidth { get { return regionOriginalWidth; } set { regionOriginalWidth = value; } }
public float RegionOriginalHeight { get { return regionOriginalHeight; } set { regionOriginalHeight = value; } } // Unrotated, unstripped size.
// Nonessential.
public int[] Edges { get; set; }
public float Width { get; set; }
public float Height { get; set; }
public SkinnedMeshAttachment (string name)
: base(name) {
}
public void UpdateUVs () {
float u = RegionU, v = RegionV, width = RegionU2 - RegionU, height = RegionV2 - RegionV;
float[] regionUVs = this.regionUVs;
if (this.uvs == null || this.uvs.Length != regionUVs.Length) this.uvs = new float[regionUVs.Length];
float[] uvs = this.uvs;
if (RegionRotate) {
for (int i = 0, n = uvs.Length; i < n; i += 2) {
uvs[i] = u + regionUVs[i + 1] * width;
uvs[i + 1] = v + height - regionUVs[i] * height;
}
} else {
for (int i = 0, n = uvs.Length; i < n; i += 2) {
uvs[i] = u + regionUVs[i] * width;
uvs[i + 1] = v + regionUVs[i + 1] * height;
}
}
}
public void ComputeWorldVertices (Slot slot, float[] worldVertices) {
Skeleton skeleton = slot.bone.skeleton;
List<Bone> skeletonBones = skeleton.bones;
float x = skeleton.x, y = skeleton.y;
float[] weights = this.weights;
int[] bones = this.bones;
if (slot.attachmentVerticesCount == 0) {
for (int w = 0, v = 0, b = 0, n = bones.Length; v < n; w += 2) {
float wx = 0, wy = 0;
int nn = bones[v++] + v;
for (; v < nn; v++, b += 3) {
Bone bone = skeletonBones[bones[v]];
float vx = weights[b], vy = weights[b + 1], weight = weights[b + 2];
wx += (vx * bone.m00 + vy * bone.m01 + bone.worldX) * weight;
wy += (vx * bone.m10 + vy * bone.m11 + bone.worldY) * weight;
}
worldVertices[w] = wx + x;
worldVertices[w + 1] = wy + y;
}
} else {
float[] ffd = slot.AttachmentVertices;
for (int w = 0, v = 0, b = 0, f = 0, n = bones.Length; v < n; w += 2) {
float wx = 0, wy = 0;
int nn = bones[v++] + v;
for (; v < nn; v++, b += 3, f += 2) {
Bone bone = skeletonBones[bones[v]];
float vx = weights[b] + ffd[f], vy = weights[b + 1] + ffd[f + 1], weight = weights[b + 2];
wx += (vx * bone.m00 + vy * bone.m01 + bone.worldX) * weight;
wy += (vx * bone.m10 + vy * bone.m11 + bone.worldY) * weight;
}
worldVertices[w] = wx + x;
worldVertices[w + 1] = wy + y;
}
}
}
}
}

View File

@@ -0,0 +1,165 @@
/******************************************************************************
* Spine Runtimes Software License
* Version 2.1
*
* Copyright (c) 2013, Esoteric Software
* All rights reserved.
*
* You are granted a perpetual, non-exclusive, non-sublicensable and
* non-transferable license to install, execute and perform the Spine Runtimes
* Software (the "Software") solely for internal use. Without the written
* permission of Esoteric Software (typically granted by licensing Spine), you
* may not (a) modify, translate, adapt or otherwise create derivative works,
* improvements of the Software or develop new applications using the Software
* or (b) remove, delete, alter or obscure any trademarks or any copyright,
* trademark, patent or other intellectual property or proprietary rights
* notices on or in the Software, including any copy thereof. Redistributions
* in binary or source form must include this license and terms.
*
* THIS SOFTWARE IS PROVIDED BY ESOTERIC SOFTWARE "AS IS" AND ANY EXPRESS OR
* IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
* MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO
* EVENT SHALL ESOTERIC SOFTARE BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
* PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS;
* OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
* WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR
* OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
* ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*****************************************************************************/
using System;
using System.Collections.Generic;
namespace SpineRuntime21 {
public class Bone{
static public bool yDown;
internal BoneData data;
internal Skeleton skeleton;
internal Bone parent;
internal List<Bone> children = new List<Bone>();
internal float x, y, rotation, rotationIK, scaleX, scaleY;
internal bool flipX, flipY;
internal float m00, m01, m10, m11;
internal float worldX, worldY, worldRotation, worldScaleX, worldScaleY;
internal bool worldFlipX, worldFlipY;
public BoneData Data { get { return data; } }
public Skeleton Skeleton { get { return skeleton; } }
public Bone Parent { get { return parent; } }
public List<Bone> Children { get { return children; } }
public float X { get { return x; } set { x = value; } }
public float Y { get { return y; } set { y = value; } }
/// <summary>The forward kinetics rotation.</summary>
public float Rotation { get { return rotation; } set { rotation = value; } }
/// <summary>The inverse kinetics rotation, as calculated by any IK constraints.</summary>
public float RotationIK { get { return rotationIK; } set { rotationIK = value; } }
public float ScaleX { get { return scaleX; } set { scaleX = value; } }
public float ScaleY { get { return scaleY; } set { scaleY = value; } }
public bool FlipX { get { return flipX; } set { flipX = value; } }
public bool FlipY { get { return flipY; } set { flipY = value; } }
public float M00 { get { return m00; } }
public float M01 { get { return m01; } }
public float M10 { get { return m10; } }
public float M11 { get { return m11; } }
public float WorldX { get { return worldX; } }
public float WorldY { get { return worldY; } }
public float WorldRotation { get { return worldRotation; } }
public float WorldScaleX { get { return worldScaleX; } }
public float WorldScaleY { get { return worldScaleY; } }
public bool WorldFlipX { get { return worldFlipX; } set { worldFlipX = value; } }
public bool WorldFlipY { get { return worldFlipY; } set { worldFlipY = value; } }
/// <param name="parent">May be null.</param>
public Bone (BoneData data, Skeleton skeleton, Bone parent) {
if (data == null) throw new ArgumentNullException("data cannot be null.");
if (skeleton == null) throw new ArgumentNullException("skeleton cannot be null.");
this.data = data;
this.skeleton = skeleton;
this.parent = parent;
SetToSetupPose();
}
/// <summary>Computes the world SRT using the parent bone and the local SRT.</summary>
public void UpdateWorldTransform () {
Bone parent = this.parent;
float x = this.x, y = this.y;
if (parent != null) {
worldX = x * parent.m00 + y * parent.m01 + parent.worldX;
worldY = x * parent.m10 + y * parent.m11 + parent.worldY;
if (data.inheritScale) {
worldScaleX = parent.worldScaleX * scaleX;
worldScaleY = parent.worldScaleY * scaleY;
} else {
worldScaleX = scaleX;
worldScaleY = scaleY;
}
worldRotation = data.inheritRotation ? parent.worldRotation + rotationIK : rotationIK;
worldFlipX = parent.worldFlipX != flipX;
worldFlipY = parent.worldFlipY != flipY;
} else {
Skeleton skeleton = this.skeleton;
bool skeletonFlipX = skeleton.flipX, skeletonFlipY = skeleton.flipY;
worldX = skeletonFlipX ? -x : x;
worldY = skeletonFlipY != yDown ? -y : y;
worldScaleX = scaleX;
worldScaleY = scaleY;
worldRotation = rotationIK;
worldFlipX = skeletonFlipX != flipX;
worldFlipY = skeletonFlipY != flipY;
}
float radians = worldRotation * (float)Math.PI / 180;
float cos = (float)Math.Cos(radians);
float sin = (float)Math.Sin(radians);
if (worldFlipX) {
m00 = -cos * worldScaleX;
m01 = sin * worldScaleY;
} else {
m00 = cos * worldScaleX;
m01 = -sin * worldScaleY;
}
if (worldFlipY != yDown) {
m10 = -sin * worldScaleX;
m11 = -cos * worldScaleY;
} else {
m10 = sin * worldScaleX;
m11 = cos * worldScaleY;
}
}
public void SetToSetupPose () {
BoneData data = this.data;
x = data.x;
y = data.y;
rotation = data.rotation;
rotationIK = rotation;
scaleX = data.scaleX;
scaleY = data.scaleY;
flipX = data.flipX;
flipY = data.flipY;
}
public void worldToLocal (float worldX, float worldY, out float localX, out float localY) {
float dx = worldX - this.worldX, dy = worldY - this.worldY;
float m00 = this.m00, m10 = this.m10, m01 = this.m01, m11 = this.m11;
if (worldFlipX != (worldFlipY != yDown)) {
m00 = -m00;
m11 = -m11;
}
float invDet = 1 / (m00 * m11 - m01 * m10);
localX = (dx * m00 * invDet - dy * m01 * invDet);
localY = (dy * m11 * invDet - dx * m10 * invDet);
}
public void localToWorld (float localX, float localY, out float worldX, out float worldY) {
worldX = localX * m00 + localY * m01 + this.worldX;
worldY = localX * m10 + localY * m11 + this.worldY;
}
override public String ToString () {
return data.name;
}
}
}

View File

@@ -0,0 +1,66 @@
/******************************************************************************
* Spine Runtimes Software License
* Version 2.1
*
* Copyright (c) 2013, Esoteric Software
* All rights reserved.
*
* You are granted a perpetual, non-exclusive, non-sublicensable and
* non-transferable license to install, execute and perform the Spine Runtimes
* Software (the "Software") solely for internal use. Without the written
* permission of Esoteric Software (typically granted by licensing Spine), you
* may not (a) modify, translate, adapt or otherwise create derivative works,
* improvements of the Software or develop new applications using the Software
* or (b) remove, delete, alter or obscure any trademarks or any copyright,
* trademark, patent or other intellectual property or proprietary rights
* notices on or in the Software, including any copy thereof. Redistributions
* in binary or source form must include this license and terms.
*
* THIS SOFTWARE IS PROVIDED BY ESOTERIC SOFTWARE "AS IS" AND ANY EXPRESS OR
* IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
* MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO
* EVENT SHALL ESOTERIC SOFTARE BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
* PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS;
* OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
* WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR
* OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
* ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*****************************************************************************/
using System;
namespace SpineRuntime21 {
public class BoneData {
internal BoneData parent;
internal String name;
internal float length, x, y, rotation, scaleX = 1, scaleY = 1;
internal bool flipX, flipY;
internal bool inheritScale = true, inheritRotation = true;
/// <summary>May be null.</summary>
public BoneData Parent { get { return parent; } }
public String Name { get { return name; } }
public float Length { get { return length; } set { length = value; } }
public float X { get { return x; } set { x = value; } }
public float Y { get { return y; } set { y = value; } }
public float Rotation { get { return rotation; } set { rotation = value; } }
public float ScaleX { get { return scaleX; } set { scaleX = value; } }
public float ScaleY { get { return scaleY; } set { scaleY = value; } }
public bool FlipX { get { return flipX; } set { flipX = value; } }
public bool FlipY { get { return flipY; } set { flipY = value; } }
public bool InheritScale { get { return inheritScale; } set { inheritScale = value; } }
public bool InheritRotation { get { return inheritRotation; } set { inheritRotation = value; } }
/// <param name="parent">May be null.</param>
public BoneData (String name, BoneData parent) {
if (name == null) throw new ArgumentNullException("name cannot be null.");
this.name = name;
this.parent = parent;
}
override public String ToString () {
return name;
}
}
}

View File

@@ -0,0 +1,48 @@
/******************************************************************************
* Spine Runtimes Software License
* Version 2.1
*
* Copyright (c) 2013, Esoteric Software
* All rights reserved.
*
* You are granted a perpetual, non-exclusive, non-sublicensable and
* non-transferable license to install, execute and perform the Spine Runtimes
* Software (the "Software") solely for internal use. Without the written
* permission of Esoteric Software (typically granted by licensing Spine), you
* may not (a) modify, translate, adapt or otherwise create derivative works,
* improvements of the Software or develop new applications using the Software
* or (b) remove, delete, alter or obscure any trademarks or any copyright,
* trademark, patent or other intellectual property or proprietary rights
* notices on or in the Software, including any copy thereof. Redistributions
* in binary or source form must include this license and terms.
*
* THIS SOFTWARE IS PROVIDED BY ESOTERIC SOFTWARE "AS IS" AND ANY EXPRESS OR
* IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
* MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO
* EVENT SHALL ESOTERIC SOFTARE BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
* PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS;
* OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
* WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR
* OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
* ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*****************************************************************************/
using System;
namespace SpineRuntime21 {
public class Event {
public EventData Data { get; private set; }
public int Int { get; set; }
public float Float { get; set; }
public String String { get; set; }
public Event (EventData data) {
Data = data;
}
override public String ToString () {
return Data.Name;
}
}
}

View File

@@ -0,0 +1,51 @@
/******************************************************************************
* Spine Runtimes Software License
* Version 2.1
*
* Copyright (c) 2013, Esoteric Software
* All rights reserved.
*
* You are granted a perpetual, non-exclusive, non-sublicensable and
* non-transferable license to install, execute and perform the Spine Runtimes
* Software (the "Software") solely for internal use. Without the written
* permission of Esoteric Software (typically granted by licensing Spine), you
* may not (a) modify, translate, adapt or otherwise create derivative works,
* improvements of the Software or develop new applications using the Software
* or (b) remove, delete, alter or obscure any trademarks or any copyright,
* trademark, patent or other intellectual property or proprietary rights
* notices on or in the Software, including any copy thereof. Redistributions
* in binary or source form must include this license and terms.
*
* THIS SOFTWARE IS PROVIDED BY ESOTERIC SOFTWARE "AS IS" AND ANY EXPRESS OR
* IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
* MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO
* EVENT SHALL ESOTERIC SOFTARE BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
* PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS;
* OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
* WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR
* OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
* ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*****************************************************************************/
using System;
namespace SpineRuntime21 {
public class EventData {
internal String name;
public String Name { get { return name; } }
public int Int { get; set; }
public float Float { get; set; }
public String String { get; set; }
public EventData (String name) {
if (name == null) throw new ArgumentNullException("name cannot be null.");
this.name = name;
}
override public String ToString () {
return Name;
}
}
}

View File

@@ -0,0 +1,150 @@
/******************************************************************************
* Spine Runtimes Software License
* Version 2.1
*
* Copyright (c) 2013, Esoteric Software
* All rights reserved.
*
* You are granted a perpetual, non-exclusive, non-sublicensable and
* non-transferable license to install, execute and perform the Spine Runtimes
* Software (the "Software") solely for internal use. Without the written
* permission of Esoteric Software (typically granted by licensing Spine), you
* may not (a) modify, translate, adapt or otherwise create derivative works,
* improvements of the Software or develop new applications using the Software
* or (b) remove, delete, alter or obscure any trademarks or any copyright,
* trademark, patent or other intellectual property or proprietary rights
* notices on or in the Software, including any copy thereof. Redistributions
* in binary or source form must include this license and terms.
*
* THIS SOFTWARE IS PROVIDED BY ESOTERIC SOFTWARE "AS IS" AND ANY EXPRESS OR
* IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
* MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO
* EVENT SHALL ESOTERIC SOFTARE BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
* PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS;
* OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
* WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR
* OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
* ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*****************************************************************************/
using System;
using System.Collections.Generic;
namespace SpineRuntime21 {
public class IkConstraint {
private const float radDeg = 180 / (float)Math.PI;
internal IkConstraintData data;
internal List<Bone> bones = new List<Bone>();
internal Bone target;
internal int bendDirection;
internal float mix;
public IkConstraintData Data { get { return data; } }
public List<Bone> Bones { get { return bones; } }
public Bone Target { get { return target; } set { target = value; } }
public int BendDirection { get { return bendDirection; } set { bendDirection = value; } }
public float Mix { get { return mix; } set { mix = value; } }
public IkConstraint (IkConstraintData data, Skeleton skeleton) {
if (data == null) throw new ArgumentNullException("data cannot be null.");
if (skeleton == null) throw new ArgumentNullException("skeleton cannot be null.");
this.data = data;
mix = data.mix;
bendDirection = data.bendDirection;
bones = new List<Bone>(data.bones.Count);
foreach (BoneData boneData in data.bones)
bones.Add(skeleton.FindBone(boneData.name));
target = skeleton.FindBone(data.target.name);
}
public void apply () {
Bone target = this.target;
List<Bone> bones = this.bones;
switch (bones.Count) {
case 1:
apply(bones[0], target.worldX, target.worldY, mix);
break;
case 2:
apply(bones[0], bones[1], target.worldX, target.worldY, bendDirection, mix);
break;
}
}
override public String ToString () {
return data.name;
}
/// <summary>Adjusts the bone rotation so the tip is as close to the target position as possible. The target is specified
/// in the world coordinate system.</summary>
static public void apply (Bone bone, float targetX, float targetY, float alpha) {
float parentRotation = (!bone.data.inheritRotation || bone.parent == null) ? 0 : bone.parent.worldRotation;
float rotation = bone.rotation;
float rotationIK = (float)Math.Atan2(targetY - bone.worldY, targetX - bone.worldX) * radDeg;
if (bone.worldFlipX != (bone.worldFlipY != Bone.yDown)) rotationIK = -rotationIK;
rotationIK -= parentRotation;
bone.rotationIK = rotation + (rotationIK - rotation) * alpha;
}
/// <summary>Adjusts the parent and child bone rotations so the tip of the child is as close to the target position as
/// possible. The target is specified in the world coordinate system.</summary>
/// <param name="child">Any descendant bone of the parent.</param>
static public void apply (Bone parent, Bone child, float targetX, float targetY, int bendDirection, float alpha) {
float childRotation = child.rotation, parentRotation = parent.rotation;
if (alpha == 0) {
child.rotationIK = childRotation;
parent.rotationIK = parentRotation;
return;
}
float positionX, positionY;
Bone parentParent = parent.parent;
if (parentParent != null) {
parentParent.worldToLocal(targetX, targetY, out positionX, out positionY);
targetX = (positionX - parent.x) * parentParent.worldScaleX;
targetY = (positionY - parent.y) * parentParent.worldScaleY;
} else {
targetX -= parent.x;
targetY -= parent.y;
}
if (child.parent == parent) {
positionX = child.x;
positionY = child.y;
} else {
child.parent.localToWorld(child.x, child.y, out positionX, out positionY);
parent.worldToLocal(positionX, positionY, out positionX, out positionY);
}
float childX = positionX * parent.worldScaleX, childY = positionY * parent.worldScaleY;
float offset = (float)Math.Atan2(childY, childX);
float len1 = (float)Math.Sqrt(childX * childX + childY * childY), len2 = child.data.length * child.worldScaleX;
// Based on code by Ryan Juckett with permission: Copyright (c) 2008-2009 Ryan Juckett, http://www.ryanjuckett.com/
float cosDenom = 2 * len1 * len2;
if (cosDenom < 0.0001f) {
child.rotationIK = childRotation + ((float)Math.Atan2(targetY, targetX) * radDeg - parentRotation - childRotation)
* alpha;
return;
}
float cos = (targetX * targetX + targetY * targetY - len1 * len1 - len2 * len2) / cosDenom;
if (cos < -1)
cos = -1;
else if (cos > 1)
cos = 1;
float childAngle = (float)Math.Acos(cos) * bendDirection;
float adjacent = len1 + len2 * cos, opposite = len2 * (float)Math.Sin(childAngle);
float parentAngle = (float)Math.Atan2(targetY * adjacent - targetX * opposite, targetX * adjacent + targetY * opposite);
float rotation = (parentAngle - offset) * radDeg - parentRotation;
if (rotation > 180)
rotation -= 360;
else if (rotation < -180) //
rotation += 360;
parent.rotationIK = parentRotation + rotation * alpha;
rotation = (childAngle + offset) * radDeg - childRotation;
if (rotation > 180)
rotation -= 360;
else if (rotation < -180) //
rotation += 360;
child.rotationIK = childRotation + (rotation + parent.worldRotation - child.parent.worldRotation) * alpha;
}
}
}

View File

@@ -0,0 +1,57 @@
/******************************************************************************
* Spine Runtimes Software License
* Version 2.1
*
* Copyright (c) 2013, Esoteric Software
* All rights reserved.
*
* You are granted a perpetual, non-exclusive, non-sublicensable and
* non-transferable license to install, execute and perform the Spine Runtimes
* Software (the "Software") solely for internal use. Without the written
* permission of Esoteric Software (typically granted by licensing Spine), you
* may not (a) modify, translate, adapt or otherwise create derivative works,
* improvements of the Software or develop new applications using the Software
* or (b) remove, delete, alter or obscure any trademarks or any copyright,
* trademark, patent or other intellectual property or proprietary rights
* notices on or in the Software, including any copy thereof. Redistributions
* in binary or source form must include this license and terms.
*
* THIS SOFTWARE IS PROVIDED BY ESOTERIC SOFTWARE "AS IS" AND ANY EXPRESS OR
* IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
* MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO
* EVENT SHALL ESOTERIC SOFTARE BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
* PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS;
* OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
* WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR
* OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
* ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*****************************************************************************/
using System;
using System.Collections.Generic;
namespace SpineRuntime21 {
public class IkConstraintData {
internal String name;
internal List<BoneData> bones = new List<BoneData>();
internal BoneData target;
internal int bendDirection = 1;
internal float mix = 1;
public String Name { get { return name; } }
public List<BoneData> Bones { get { return bones; } }
public BoneData Target { get { return target; } set { target = value; } }
public int BendDirection { get { return bendDirection; } set { bendDirection = value; } }
public float Mix { get { return mix; } set { mix = value; } }
public IkConstraintData (String name) {
if (name == null) throw new ArgumentNullException("name cannot be null.");
this.name = name;
}
override public String ToString () {
return name;
}
}
}

View File

@@ -0,0 +1,542 @@
/*
* Copyright (c) 2012 Calvin Rien
*
* Based on the JSON parser by Patrick van Bergen
* http://techblog.procurios.nl/k/618/news/view/14605/14863/How-do-I-write-my-own-parser-for-JSON.html
*
* Simplified it so that it doesn't throw exceptions
* and can be used in Unity iPhone with maximum code stripping.
*
* Permission is hereby granted, free of charge, to any person obtaining
* a copy of this software and associated documentation files (the
* "Software"), to deal in the Software without restriction, including
* without limitation the rights to use, copy, modify, merge, publish,
* distribute, sublicense, and/or sell copies of the Software, and to
* permit persons to whom the Software is furnished to do so, subject to
* the following conditions:
*
* The above copyright notice and this permission notice shall be
* included in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
using System;
using System.Collections;
using System.Collections.Generic;
using System.IO;
using System.Text;
using System.Globalization;
namespace SpineRuntime21
{
// Example usage:
//
// using UnityEngine;
// using System.Collections;
// using System.Collections.Generic;
// using MiniJSON;
//
// public class MiniJSONTest : MonoBehaviour {
// void Start () {
// var jsonString = "{ \"array\": [1.44,2,3], " +
// "\"object\": {\"key1\":\"value1\", \"key2\":256}, " +
// "\"string\": \"The quick brown fox \\\"jumps\\\" over the lazy dog \", " +
// "\"unicode\": \"\\u3041 Men\u00fa sesi\u00f3n\", " +
// "\"int\": 65536, " +
// "\"float\": 3.1415926, " +
// "\"bool\": true, " +
// "\"null\": null }";
//
// var dict = Json.Deserialize(jsonString) as Dictionary<string,object>;
//
// Debug.Log("deserialized: " + dict.GetType());
// Debug.Log("dict['array'][0]: " + ((List<object>) dict["array"])[0]);
// Debug.Log("dict['string']: " + (string) dict["string"]);
// Debug.Log("dict['float']: " + (float) dict["float"]);
// Debug.Log("dict['int']: " + (long) dict["int"]); // ints come out as longs
// Debug.Log("dict['unicode']: " + (string) dict["unicode"]);
//
// var str = Json.Serialize(dict);
//
// Debug.Log("serialized: " + str);
// }
// }
/// <summary>
/// This class encodes and decodes JSON strings.
/// Spec. details, see http://www.json.org/
///
/// JSON uses Arrays and Objects. These correspond here to the datatypes IList and IDictionary.
/// All numbers are parsed to floats.
/// </summary>
public static class Json {
/// <summary>
/// Parses the string json into a value
/// </summary>
/// <param name="json">A JSON string.</param>
/// <returns>An List&lt;object&gt;, a Dictionary&lt;string, object&gt;, a float, an integer,a string, null, true, or false</returns>
public static object Deserialize (TextReader json) {
if (json == null) {
return null;
}
return Parser.Parse(json);
}
sealed class Parser : IDisposable {
const string WHITE_SPACE = " \t\n\r";
const string WORD_BREAK = " \t\n\r{}[],:\"";
enum TOKEN {
NONE,
CURLY_OPEN,
CURLY_CLOSE,
SQUARED_OPEN,
SQUARED_CLOSE,
COLON,
COMMA,
STRING,
NUMBER,
TRUE,
FALSE,
NULL
};
TextReader json;
Parser(TextReader reader) {
json = reader;
}
public static object Parse (TextReader reader) {
using (var instance = new Parser(reader)) {
return instance.ParseValue();
}
}
public void Dispose() {
json.Dispose();
json = null;
}
Dictionary<string, object> ParseObject() {
Dictionary<string, object> table = new Dictionary<string, object>();
// ditch opening brace
json.Read();
// {
while (true) {
switch (NextToken) {
case TOKEN.NONE:
return null;
case TOKEN.COMMA:
continue;
case TOKEN.CURLY_CLOSE:
return table;
default:
// name
string name = ParseString();
if (name == null) {
return null;
}
// :
if (NextToken != TOKEN.COLON) {
return null;
}
// ditch the colon
json.Read();
// value
table[name] = ParseValue();
break;
}
}
}
List<object> ParseArray() {
List<object> array = new List<object>();
// ditch opening bracket
json.Read();
// [
var parsing = true;
while (parsing) {
TOKEN nextToken = NextToken;
switch (nextToken) {
case TOKEN.NONE:
return null;
case TOKEN.COMMA:
continue;
case TOKEN.SQUARED_CLOSE:
parsing = false;
break;
default:
object value = ParseByToken(nextToken);
array.Add(value);
break;
}
}
return array;
}
object ParseValue() {
TOKEN nextToken = NextToken;
return ParseByToken(nextToken);
}
object ParseByToken(TOKEN token) {
switch (token) {
case TOKEN.STRING:
return ParseString();
case TOKEN.NUMBER:
return ParseNumber();
case TOKEN.CURLY_OPEN:
return ParseObject();
case TOKEN.SQUARED_OPEN:
return ParseArray();
case TOKEN.TRUE:
return true;
case TOKEN.FALSE:
return false;
case TOKEN.NULL:
return null;
default:
return null;
}
}
string ParseString() {
StringBuilder s = new StringBuilder();
char c;
// ditch opening quote
json.Read();
bool parsing = true;
while (parsing) {
if (json.Peek() == -1) {
parsing = false;
break;
}
c = NextChar;
switch (c) {
case '"':
parsing = false;
break;
case '\\':
if (json.Peek() == -1) {
parsing = false;
break;
}
c = NextChar;
switch (c) {
case '"':
case '\\':
case '/':
s.Append(c);
break;
case 'b':
s.Append('\b');
break;
case 'f':
s.Append('\f');
break;
case 'n':
s.Append('\n');
break;
case 'r':
s.Append('\r');
break;
case 't':
s.Append('\t');
break;
case 'u':
var hex = new StringBuilder();
for (int i=0; i< 4; i++) {
hex.Append(NextChar);
}
s.Append((char) Convert.ToInt32(hex.ToString(), 16));
break;
}
break;
default:
s.Append(c);
break;
}
}
return s.ToString();
}
object ParseNumber() {
string number = NextWord;
float parsedFloat;
float.TryParse(number, NumberStyles.Float, CultureInfo.InvariantCulture, out parsedFloat);
return parsedFloat;
}
void EatWhitespace() {
while (WHITE_SPACE.IndexOf(PeekChar) != -1) {
json.Read();
if (json.Peek() == -1) {
break;
}
}
}
char PeekChar {
get {
return Convert.ToChar(json.Peek());
}
}
char NextChar {
get {
return Convert.ToChar(json.Read());
}
}
string NextWord {
get {
StringBuilder word = new StringBuilder();
while (WORD_BREAK.IndexOf(PeekChar) == -1) {
word.Append(NextChar);
if (json.Peek() == -1) {
break;
}
}
return word.ToString();
}
}
TOKEN NextToken {
get {
EatWhitespace();
if (json.Peek() == -1) {
return TOKEN.NONE;
}
char c = PeekChar;
switch (c) {
case '{':
return TOKEN.CURLY_OPEN;
case '}':
json.Read();
return TOKEN.CURLY_CLOSE;
case '[':
return TOKEN.SQUARED_OPEN;
case ']':
json.Read();
return TOKEN.SQUARED_CLOSE;
case ',':
json.Read();
return TOKEN.COMMA;
case '"':
return TOKEN.STRING;
case ':':
return TOKEN.COLON;
case '0':
case '1':
case '2':
case '3':
case '4':
case '5':
case '6':
case '7':
case '8':
case '9':
case '-':
return TOKEN.NUMBER;
}
string word = NextWord;
switch (word) {
case "false":
return TOKEN.FALSE;
case "true":
return TOKEN.TRUE;
case "null":
return TOKEN.NULL;
}
return TOKEN.NONE;
}
}
}
/// <summary>
/// Converts a IDictionary / IList object or a simple type (string, int, etc.) into a JSON string
/// </summary>
/// <param name="json">A Dictionary&lt;string, object&gt; / List&lt;object&gt;</param>
/// <returns>A JSON encoded string, or null if object 'json' is not serializable</returns>
public static string Serialize(object obj) {
return Serializer.Serialize(obj);
}
sealed class Serializer {
StringBuilder builder;
Serializer() {
builder = new StringBuilder();
}
public static string Serialize(object obj) {
var instance = new Serializer();
instance.SerializeValue(obj);
return instance.builder.ToString();
}
void SerializeValue(object value) {
IList asList;
IDictionary asDict;
string asStr;
if (value == null) {
builder.Append("null");
}
else if ((asStr = value as string) != null) {
SerializeString(asStr);
}
else if (value is bool) {
builder.Append(value.ToString().ToLower());
}
else if ((asList = value as IList) != null) {
SerializeArray(asList);
}
else if ((asDict = value as IDictionary) != null) {
SerializeObject(asDict);
}
else if (value is char) {
SerializeString(value.ToString());
}
else {
SerializeOther(value);
}
}
void SerializeObject(IDictionary obj) {
bool first = true;
builder.Append('{');
foreach (object e in obj.Keys) {
if (!first) {
builder.Append(',');
}
SerializeString(e.ToString());
builder.Append(':');
SerializeValue(obj[e]);
first = false;
}
builder.Append('}');
}
void SerializeArray(IList anArray) {
builder.Append('[');
bool first = true;
foreach (object obj in anArray) {
if (!first) {
builder.Append(',');
}
SerializeValue(obj);
first = false;
}
builder.Append(']');
}
void SerializeString(string str) {
builder.Append('\"');
char[] charArray = str.ToCharArray();
foreach (var c in charArray) {
switch (c) {
case '"':
builder.Append("\\\"");
break;
case '\\':
builder.Append("\\\\");
break;
case '\b':
builder.Append("\\b");
break;
case '\f':
builder.Append("\\f");
break;
case '\n':
builder.Append("\\n");
break;
case '\r':
builder.Append("\\r");
break;
case '\t':
builder.Append("\\t");
break;
default:
int codepoint = Convert.ToInt32(c);
if ((codepoint >= 32) && (codepoint <= 126)) {
builder.Append(c);
}
else {
builder.Append("\\u" + Convert.ToString(codepoint, 16).PadLeft(4, '0'));
}
break;
}
}
builder.Append('\"');
}
void SerializeOther(object value) {
if (value is float
|| value is int
|| value is uint
|| value is long
|| value is float
|| value is sbyte
|| value is byte
|| value is short
|| value is ushort
|| value is ulong
|| value is decimal) {
builder.Append(value.ToString());
}
else {
SerializeString(value.ToString());
}
}
}
}
}

View File

@@ -0,0 +1,306 @@
/******************************************************************************
* Spine Runtimes Software License
* Version 2.1
*
* Copyright (c) 2013, Esoteric Software
* All rights reserved.
*
* You are granted a perpetual, non-exclusive, non-sublicensable and
* non-transferable license to install, execute and perform the Spine Runtimes
* Software (the "Software") solely for internal use. Without the written
* permission of Esoteric Software (typically granted by licensing Spine), you
* may not (a) modify, translate, adapt or otherwise create derivative works,
* improvements of the Software or develop new applications using the Software
* or (b) remove, delete, alter or obscure any trademarks or any copyright,
* trademark, patent or other intellectual property or proprietary rights
* notices on or in the Software, including any copy thereof. Redistributions
* in binary or source form must include this license and terms.
*
* THIS SOFTWARE IS PROVIDED BY ESOTERIC SOFTWARE "AS IS" AND ANY EXPRESS OR
* IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
* MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO
* EVENT SHALL ESOTERIC SOFTARE BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
* PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS;
* OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
* WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR
* OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
* ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*****************************************************************************/
using System;
using System.Collections.Generic;
namespace SpineRuntime21 {
public class Skeleton {
internal SkeletonData data;
internal List<Bone> bones;
internal List<Slot> slots;
internal List<Slot> drawOrder;
internal List<IkConstraint> ikConstraints;
private List<List<Bone>> boneCache = new List<List<Bone>>();
internal Skin skin;
internal float r = 1, g = 1, b = 1, a = 1;
internal float time;
internal bool flipX, flipY;
internal float x, y;
public SkeletonData Data { get { return data; } }
public List<Bone> Bones { get { return bones; } }
public List<Slot> Slots { get { return slots; } }
public List<Slot> DrawOrder { get { return drawOrder; } }
public List<IkConstraint> IkConstraints { get { return ikConstraints; } set { ikConstraints = value; } }
public Skin Skin { get { return skin; } set { skin = value; } }
public float R { get { return r; } set { r = value; } }
public float G { get { return g; } set { g = value; } }
public float B { get { return b; } set { b = value; } }
public float A { get { return a; } set { a = value; } }
public float Time { get { return time; } set { time = value; } }
public float X { get { return x; } set { x = value; } }
public float Y { get { return y; } set { y = value; } }
public bool FlipX { get { return flipX; } set { flipX = value; } }
public bool FlipY { get { return flipY; } set { flipY = value; } }
public Bone RootBone {
get {
return bones.Count == 0 ? null : bones[0];
}
}
public Skeleton (SkeletonData data) {
if (data == null) throw new ArgumentNullException("data cannot be null.");
this.data = data;
bones = new List<Bone>(data.bones.Count);
foreach (BoneData boneData in data.bones) {
Bone parent = boneData.parent == null ? null : bones[data.bones.IndexOf(boneData.parent)];
Bone bone = new Bone(boneData, this, parent);
if (parent != null) parent.children.Add(bone);
bones.Add(bone);
}
slots = new List<Slot>(data.slots.Count);
drawOrder = new List<Slot>(data.slots.Count);
foreach (SlotData slotData in data.slots) {
Bone bone = bones[data.bones.IndexOf(slotData.boneData)];
Slot slot = new Slot(slotData, bone);
slots.Add(slot);
drawOrder.Add(slot);
}
ikConstraints = new List<IkConstraint>(data.ikConstraints.Count);
foreach (IkConstraintData ikConstraintData in data.ikConstraints)
ikConstraints.Add(new IkConstraint(ikConstraintData, this));
UpdateCache();
}
/// <summary>Caches information about bones and IK constraints. Must be called if bones or IK constraints are added or
/// removed.</summary>
public void UpdateCache () {
List<List<Bone>> boneCache = this.boneCache;
List<IkConstraint> ikConstraints = this.ikConstraints;
int ikConstraintsCount = ikConstraints.Count;
int arrayCount = ikConstraintsCount + 1;
if (boneCache.Count > arrayCount) boneCache.RemoveRange(arrayCount, boneCache.Count - arrayCount);
for (int i = 0, n = boneCache.Count; i < n; i++)
boneCache[i].Clear();
while (boneCache.Count < arrayCount)
boneCache.Add(new List<Bone>());
List<Bone> nonIkBones = boneCache[0];
for (int i = 0, n = bones.Count; i < n; i++) {
Bone bone = bones[i];
Bone current = bone;
do {
for (int ii = 0; ii < ikConstraintsCount; ii++) {
IkConstraint ikConstraint = ikConstraints[ii];
Bone parent = ikConstraint.bones[0];
Bone child = ikConstraint.bones[ikConstraint.bones.Count - 1];
while (true) {
if (current == child) {
boneCache[ii].Add(bone);
boneCache[ii + 1].Add(bone);
goto outer;
}
if (child == parent) break;
child = child.parent;
}
}
current = current.parent;
} while (current != null);
nonIkBones.Add(bone);
outer: {}
}
}
/// <summary>Updates the world transform for each bone and applies IK constraints.</summary>
public void UpdateWorldTransform () {
List<Bone> bones = this.bones;
for (int ii = 0, nn = bones.Count; ii < nn; ii++) {
Bone bone = bones[ii];
bone.rotationIK = bone.rotation;
}
List<List<Bone>> boneCache = this.boneCache;
List<IkConstraint> ikConstraints = this.ikConstraints;
int i = 0, last = boneCache.Count - 1;
while (true) {
List<Bone> updateBones = boneCache[i];
for (int ii = 0, nn = updateBones.Count; ii < nn; ii++)
updateBones[ii].UpdateWorldTransform();
if (i == last) break;
ikConstraints[i].apply();
i++;
}
}
/// <summary>Sets the bones and slots to their setup pose values.</summary>
public void SetToSetupPose () {
SetBonesToSetupPose();
SetSlotsToSetupPose();
}
public void SetBonesToSetupPose () {
List<Bone> bones = this.bones;
for (int i = 0, n = bones.Count; i < n; i++)
bones[i].SetToSetupPose();
List<IkConstraint> ikConstraints = this.ikConstraints;
for (int i = 0, n = ikConstraints.Count; i < n; i++) {
IkConstraint ikConstraint = ikConstraints[i];
ikConstraint.bendDirection = ikConstraint.data.bendDirection;
ikConstraint.mix = ikConstraint.data.mix;
}
}
public void SetSlotsToSetupPose () {
List<Slot> slots = this.slots;
drawOrder.Clear();
drawOrder.AddRange(slots);
for (int i = 0, n = slots.Count; i < n; i++)
slots[i].SetToSetupPose(i);
}
/// <returns>May be null.</returns>
public Bone FindBone (String boneName) {
if (boneName == null) throw new ArgumentNullException("boneName cannot be null.");
List<Bone> bones = this.bones;
for (int i = 0, n = bones.Count; i < n; i++) {
Bone bone = bones[i];
if (bone.data.name == boneName) return bone;
}
return null;
}
/// <returns>-1 if the bone was not found.</returns>
public int FindBoneIndex (String boneName) {
if (boneName == null) throw new ArgumentNullException("boneName cannot be null.");
List<Bone> bones = this.bones;
for (int i = 0, n = bones.Count; i < n; i++)
if (bones[i].data.name == boneName) return i;
return -1;
}
/// <returns>May be null.</returns>
public Slot FindSlot (String slotName) {
if (slotName == null) throw new ArgumentNullException("slotName cannot be null.");
List<Slot> slots = this.slots;
for (int i = 0, n = slots.Count; i < n; i++) {
Slot slot = slots[i];
if (slot.data.name == slotName) return slot;
}
return null;
}
/// <returns>-1 if the bone was not found.</returns>
public int FindSlotIndex (String slotName) {
if (slotName == null) throw new ArgumentNullException("slotName cannot be null.");
List<Slot> slots = this.slots;
for (int i = 0, n = slots.Count; i < n; i++)
if (slots[i].data.name.Equals(slotName)) return i;
return -1;
}
/// <summary>Sets a skin by name (see SetSkin).</summary>
public void SetSkin (String skinName) {
Skin skin = data.FindSkin(skinName);
if (skin == null) throw new ArgumentException("Skin not found: " + skinName);
SetSkin(skin);
}
/// <summary>Sets the skin used to look up attachments before looking in the {@link SkeletonData#getDefaultSkin() default
/// skin}. Attachmentsfrom the new skin are attached if the corresponding attachment from the old skin was attached. If
/// there was no old skin, each slot's setup mode attachment is attached from the new skin.</summary>
/// <param name="newSkin">May be null.</param>
public void SetSkin (Skin newSkin) {
if (newSkin != null) {
if (skin != null)
newSkin.AttachAll(this, skin);
else {
List<Slot> slots = this.slots;
for (int i = 0, n = slots.Count; i < n; i++) {
Slot slot = slots[i];
String name = slot.data.attachmentName;
if (name != null) {
Attachment attachment = newSkin.GetAttachment(i, name);
if (attachment != null) slot.Attachment = attachment;
}
}
}
}
skin = newSkin;
}
/// <returns>May be null.</returns>
public Attachment GetAttachment (String slotName, String attachmentName) {
return GetAttachment(data.FindSlotIndex(slotName), attachmentName);
}
/// <returns>May be null.</returns>
public Attachment GetAttachment (int slotIndex, String attachmentName) {
if (attachmentName == null) throw new ArgumentNullException("attachmentName cannot be null.");
if (skin != null) {
Attachment attachment = skin.GetAttachment(slotIndex, attachmentName);
if (attachment != null) return attachment;
}
if (data.defaultSkin != null) return data.defaultSkin.GetAttachment(slotIndex, attachmentName);
return null;
}
/// <param name="attachmentName">May be null.</param>
public void SetAttachment (String slotName, String attachmentName) {
if (slotName == null) throw new ArgumentNullException("slotName cannot be null.");
List<Slot> slots = this.slots;
for (int i = 0, n = slots.Count; i < n; i++) {
Slot slot = slots[i];
if (slot.data.name == slotName) {
Attachment attachment = null;
if (attachmentName != null) {
attachment = GetAttachment(i, attachmentName);
if (attachment == null) throw new Exception("Attachment not found: " + attachmentName + ", for slot: " + slotName);
}
slot.Attachment = attachment;
return;
}
}
throw new Exception("Slot not found: " + slotName);
}
/** @return May be null. */
public IkConstraint FindIkConstraint (String ikConstraintName) {
if (ikConstraintName == null) throw new ArgumentNullException("ikConstraintName cannot be null.");
List<IkConstraint> ikConstraints = this.ikConstraints;
for (int i = 0, n = ikConstraints.Count; i < n; i++) {
IkConstraint ikConstraint = ikConstraints[i];
if (ikConstraint.data.name == ikConstraintName) return ikConstraint;
}
return null;
}
public void Update (float delta) {
time += delta;
}
}
}

View File

@@ -0,0 +1,663 @@
/******************************************************************************
* Spine Runtimes Software License
* Version 2.1
*
* Copyright (c) 2013, Esoteric Software
* All rights reserved.
*
* You are granted a perpetual, non-exclusive, non-sublicensable and
* non-transferable license to install, execute and perform the Spine Runtimes
* Software (the "Software") solely for internal use. Without the written
* permission of Esoteric Software (typically granted by licensing Spine), you
* may not (a) modify, translate, adapt or otherwise create derivative works,
* improvements of the Software or develop new applications using the Software
* or (b) remove, delete, alter or obscure any trademarks or any copyright,
* trademark, patent or other intellectual property or proprietary rights
* notices on or in the Software, including any copy thereof. Redistributions
* in binary or source form must include this license and terms.
*
* THIS SOFTWARE IS PROVIDED BY ESOTERIC SOFTWARE "AS IS" AND ANY EXPRESS OR
* IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
* MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO
* EVENT SHALL ESOTERIC SOFTARE BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
* PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS;
* OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
* WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR
* OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
* ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*****************************************************************************/
using System;
using System.IO;
using System.Collections.Generic;
#if WINDOWS_STOREAPP
using System.Threading.Tasks;
using Windows.Storage;
#endif
namespace SpineRuntime21 {
public class SkeletonBinary {
public const int TIMELINE_SCALE = 0;
public const int TIMELINE_ROTATE = 1;
public const int TIMELINE_TRANSLATE = 2;
public const int TIMELINE_ATTACHMENT = 3;
public const int TIMELINE_COLOR = 4;
public const int TIMELINE_FLIPX = 5;
public const int TIMELINE_FLIPY = 6;
public const int CURVE_LINEAR = 0;
public const int CURVE_STEPPED = 1;
public const int CURVE_BEZIER = 2;
private AttachmentLoader attachmentLoader;
public float Scale { get; set; }
private char[] chars = new char[32];
private byte[] buffer = new byte[4];
public SkeletonBinary (params Atlas[] atlasArray)
: this(new AtlasAttachmentLoader(atlasArray)) {
}
public SkeletonBinary (AttachmentLoader attachmentLoader) {
if (attachmentLoader == null) throw new ArgumentNullException("attachmentLoader cannot be null.");
this.attachmentLoader = attachmentLoader;
Scale = 1;
}
#if WINDOWS_STOREAPP
private async Task<SkeletonData> ReadFile(string path) {
var folder = Windows.ApplicationModel.Package.Current.InstalledLocation;
using (var input = new BufferedStream(await folder.GetFileAsync(path).AsTask().ConfigureAwait(false))) {
SkeletonData skeletonData = ReadSkeletonData(input);
skeletonData.Name = Path.GetFileNameWithoutExtension(path);
return skeletonData;
}
}
public SkeletonData ReadSkeletonData (String path) {
return this.ReadFile(path).Result;
}
#else
public SkeletonData ReadSkeletonData (String path) {
#if WINDOWS_PHONE
using (var input = new BufferedStream(Microsoft.Xna.Framework.TitleContainer.OpenStream(path)))
{
#else
using (var input = new BufferedStream(new FileStream(path, FileMode.Open))) {
#endif
SkeletonData skeletonData = ReadSkeletonData(input);
skeletonData.name = Path.GetFileNameWithoutExtension(path);
return skeletonData;
}
}
#endif
public SkeletonData ReadSkeletonData (Stream input) {
if (input == null) throw new ArgumentNullException("input cannot be null.");
float scale = Scale;
var skeletonData = new SkeletonData();
skeletonData.hash = ReadString(input);
if (skeletonData.hash.Length == 0) skeletonData.hash = null;
skeletonData.version = ReadString(input);
if (skeletonData.version.Length == 0) skeletonData.version = null;
skeletonData.width = ReadFloat(input);
skeletonData.height = ReadFloat(input);
bool nonessential = ReadBoolean(input);
if (nonessential) {
skeletonData.imagesPath = ReadString(input);
if (skeletonData.imagesPath.Length == 0) skeletonData.imagesPath = null;
}
// Bones.
for (int i = 0, n = ReadInt(input, true); i < n; i++) {
String name = ReadString(input);
BoneData parent = null;
int parentIndex = ReadInt(input, true) - 1;
if (parentIndex != -1) parent = skeletonData.bones[parentIndex];
BoneData boneData = new BoneData(name, parent);
boneData.x = ReadFloat(input) * scale;
boneData.y = ReadFloat(input) * scale;
boneData.scaleX = ReadFloat(input);
boneData.scaleY = ReadFloat(input);
boneData.rotation = ReadFloat(input);
boneData.length = ReadFloat(input) * scale;
boneData.flipX = ReadBoolean(input);
boneData.flipY = ReadBoolean(input);
boneData.inheritScale = ReadBoolean(input);
boneData.inheritRotation = ReadBoolean(input);
if (nonessential) ReadInt(input); // Skip bone color.
skeletonData.bones.Add(boneData);
}
// IK constraints.
for (int i = 0, n = ReadInt(input, true); i < n; i++) {
IkConstraintData ikConstraintData = new IkConstraintData(ReadString(input));
for (int ii = 0, nn = ReadInt(input, true); ii < nn; ii++)
ikConstraintData.bones.Add(skeletonData.bones[ReadInt(input, true)]);
ikConstraintData.target = skeletonData.bones[ReadInt(input, true)];
ikConstraintData.mix = ReadFloat(input);
ikConstraintData.bendDirection = ReadSByte(input);
skeletonData.ikConstraints.Add(ikConstraintData);
}
// Slots.
for (int i = 0, n = ReadInt(input, true); i < n; i++) {
String slotName = ReadString(input);
BoneData boneData = skeletonData.bones[ReadInt(input, true)];
SlotData slotData = new SlotData(slotName, boneData);
int color = ReadInt(input);
slotData.r = ((color & 0xff000000) >> 24) / 255f;
slotData.g = ((color & 0x00ff0000) >> 16) / 255f;
slotData.b = ((color & 0x0000ff00) >> 8) / 255f;
slotData.a = ((color & 0x000000ff)) / 255f;
slotData.attachmentName = ReadString(input);
slotData.additiveBlending = ReadBoolean(input);
skeletonData.slots.Add(slotData);
}
// Default skin.
Skin defaultSkin = ReadSkin(input, "default", nonessential);
if (defaultSkin != null) {
skeletonData.defaultSkin = defaultSkin;
skeletonData.skins.Add(defaultSkin);
}
// Skins.
for (int i = 0, n = ReadInt(input, true); i < n; i++)
skeletonData.skins.Add(ReadSkin(input, ReadString(input), nonessential));
// Events.
for (int i = 0, n = ReadInt(input, true); i < n; i++) {
EventData eventData = new EventData(ReadString(input));
eventData.Int = ReadInt(input, false);
eventData.Float = ReadFloat(input);
eventData.String = ReadString(input);
skeletonData.events.Add(eventData);
}
// Animations.
for (int i = 0, n = ReadInt(input, true); i < n; i++)
ReadAnimation(ReadString(input), input, skeletonData);
skeletonData.bones.TrimExcess();
skeletonData.slots.TrimExcess();
skeletonData.skins.TrimExcess();
skeletonData.events.TrimExcess();
skeletonData.animations.TrimExcess();
skeletonData.ikConstraints.TrimExcess();
return skeletonData;
}
/** @return May be null. */
private Skin ReadSkin (Stream input, String skinName, bool nonessential) {
int slotCount = ReadInt(input, true);
if (slotCount == 0) return null;
Skin skin = new Skin(skinName);
for (int i = 0; i < slotCount; i++) {
int slotIndex = ReadInt(input, true);
for (int ii = 0, nn = ReadInt(input, true); ii < nn; ii++) {
String name = ReadString(input);
skin.AddAttachment(slotIndex, name, ReadAttachment(input, skin, name, nonessential));
}
}
return skin;
}
private Attachment ReadAttachment (Stream input, Skin skin, String attachmentName, bool nonessential) {
float scale = Scale;
String name = ReadString(input);
if (name == null) name = attachmentName;
switch ((AttachmentType)input.ReadByte()) {
case AttachmentType.region: {
String path = ReadString(input);
if (path == null) path = name;
RegionAttachment region = attachmentLoader.NewRegionAttachment(skin, name, path);
if (region == null) return null;
region.Path = path;
region.x = ReadFloat(input) * scale;
region.y = ReadFloat(input) * scale;
region.scaleX = ReadFloat(input);
region.scaleY = ReadFloat(input);
region.rotation = ReadFloat(input);
region.width = ReadFloat(input) * scale;
region.height = ReadFloat(input) * scale;
int color = ReadInt(input);
region.r = ((color & 0xff000000) >> 24) / 255f;
region.g = ((color & 0x00ff0000) >> 16) / 255f;
region.b = ((color & 0x0000ff00) >> 8) / 255f;
region.a = ((color & 0x000000ff)) / 255f;
region.UpdateOffset();
return region;
}
case AttachmentType.boundingbox: {
BoundingBoxAttachment box = attachmentLoader.NewBoundingBoxAttachment(skin, name);
if (box == null) return null;
box.vertices = ReadFloatArray(input, scale);
return box;
}
case AttachmentType.mesh: {
String path = ReadString(input);
if (path == null) path = name;
MeshAttachment mesh = attachmentLoader.NewMeshAttachment(skin, name, path);
if (mesh == null) return null;
mesh.Path = path;
mesh.regionUVs = ReadFloatArray(input, 1);
mesh.triangles = ReadShortArray(input);
mesh.vertices = ReadFloatArray(input, scale);
mesh.UpdateUVs();
int color = ReadInt(input);
mesh.r = ((color & 0xff000000) >> 24) / 255f;
mesh.g = ((color & 0x00ff0000) >> 16) / 255f;
mesh.b = ((color & 0x0000ff00) >> 8) / 255f;
mesh.a = ((color & 0x000000ff)) / 255f;
mesh.HullLength = ReadInt(input, true) * 2;
if (nonessential) {
mesh.Edges = ReadIntArray(input);
mesh.Width = ReadFloat(input) * scale;
mesh.Height = ReadFloat(input) * scale;
}
return mesh;
}
case AttachmentType.skinnedmesh: {
String path = ReadString(input);
if (path == null) path = name;
SkinnedMeshAttachment mesh = attachmentLoader.NewSkinnedMeshAttachment(skin, name, path);
if (mesh == null) return null;
mesh.Path = path;
float[] uvs = ReadFloatArray(input, 1);
int[] triangles = ReadShortArray(input);
int vertexCount = ReadInt(input, true);
var weights = new List<float>(uvs.Length * 3 * 3);
var bones = new List<int>(uvs.Length * 3);
for (int i = 0; i < vertexCount; i++) {
int boneCount = (int)ReadFloat(input);
bones.Add(boneCount);
for (int nn = i + boneCount * 4; i < nn; i += 4) {
bones.Add((int)ReadFloat(input));
weights.Add(ReadFloat(input) * scale);
weights.Add(ReadFloat(input) * scale);
weights.Add(ReadFloat(input));
}
}
mesh.bones = bones.ToArray();
mesh.weights = weights.ToArray();
mesh.triangles = triangles;
mesh.regionUVs = uvs;
mesh.UpdateUVs();
int color = ReadInt(input);
mesh.r = ((color & 0xff000000) >> 24) / 255f;
mesh.g = ((color & 0x00ff0000) >> 16) / 255f;
mesh.b = ((color & 0x0000ff00) >> 8) / 255f;
mesh.a = ((color & 0x000000ff)) / 255f;
mesh.HullLength = ReadInt(input, true) * 2;
if (nonessential) {
mesh.Edges = ReadIntArray(input);
mesh.Width = ReadFloat(input) * scale;
mesh.Height = ReadFloat(input) * scale;
}
return mesh;
}
}
return null;
}
private float[] ReadFloatArray (Stream input, float scale) {
int n = ReadInt(input, true);
float[] array = new float[n];
if (scale == 1) {
for (int i = 0; i < n; i++)
array[i] = ReadFloat(input);
} else {
for (int i = 0; i < n; i++)
array[i] = ReadFloat(input) * scale;
}
return array;
}
private int[] ReadShortArray (Stream input) {
int n = ReadInt(input, true);
int[] array = new int[n];
for (int i = 0; i < n; i++)
array[i] = (input.ReadByte() << 8) + input.ReadByte();
return array;
}
private int[] ReadIntArray (Stream input) {
int n = ReadInt(input, true);
int[] array = new int[n];
for (int i = 0; i < n; i++)
array[i] = ReadInt(input, true);
return array;
}
private void ReadAnimation (String name, Stream input, SkeletonData skeletonData) {
var timelines = new List<Timeline>();
float scale = Scale;
float duration = 0;
// Slot timelines.
for (int i = 0, n = ReadInt(input, true); i < n; i++) {
int slotIndex = ReadInt(input, true);
for (int ii = 0, nn = ReadInt(input, true); ii < nn; ii++) {
int timelineType = input.ReadByte();
int frameCount = ReadInt(input, true);
switch (timelineType) {
case TIMELINE_COLOR: {
ColorTimeline timeline = new ColorTimeline(frameCount);
timeline.slotIndex = slotIndex;
for (int frameIndex = 0; frameIndex < frameCount; frameIndex++) {
float time = ReadFloat(input);
int color = ReadInt(input);
float r = ((color & 0xff000000) >> 24) / 255f;
float g = ((color & 0x00ff0000) >> 16) / 255f;
float b = ((color & 0x0000ff00) >> 8) / 255f;
float a = ((color & 0x000000ff)) / 255f;
timeline.SetFrame(frameIndex, time, r, g, b, a);
if (frameIndex < frameCount - 1) ReadCurve(input, frameIndex, timeline);
}
timelines.Add(timeline);
duration = Math.Max(duration, timeline.frames[frameCount * 5 - 5]);
break;
}
case TIMELINE_ATTACHMENT: {
AttachmentTimeline timeline = new AttachmentTimeline(frameCount);
timeline.slotIndex = slotIndex;
for (int frameIndex = 0; frameIndex < frameCount; frameIndex++)
timeline.SetFrame(frameIndex, ReadFloat(input), ReadString(input));
timelines.Add(timeline);
duration = Math.Max(duration, timeline.frames[frameCount - 1]);
break;
}
}
}
}
// Bone timelines.
for (int i = 0, n = ReadInt(input, true); i < n; i++) {
int boneIndex = ReadInt(input, true);
for (int ii = 0, nn = ReadInt(input, true); ii < nn; ii++) {
int timelineType = input.ReadByte();
int frameCount = ReadInt(input, true);
switch (timelineType) {
case TIMELINE_ROTATE: {
RotateTimeline timeline = new RotateTimeline(frameCount);
timeline.boneIndex = boneIndex;
for (int frameIndex = 0; frameIndex < frameCount; frameIndex++) {
timeline.SetFrame(frameIndex, ReadFloat(input), ReadFloat(input));
if (frameIndex < frameCount - 1) ReadCurve(input, frameIndex, timeline);
}
timelines.Add(timeline);
duration = Math.Max(duration, timeline.frames[frameCount * 2 - 2]);
break;
}
case TIMELINE_TRANSLATE:
case TIMELINE_SCALE: {
TranslateTimeline timeline;
float timelineScale = 1;
if (timelineType == TIMELINE_SCALE)
timeline = new ScaleTimeline(frameCount);
else {
timeline = new TranslateTimeline(frameCount);
timelineScale = scale;
}
timeline.boneIndex = boneIndex;
for (int frameIndex = 0; frameIndex < frameCount; frameIndex++) {
timeline.SetFrame(frameIndex, ReadFloat(input), ReadFloat(input) * timelineScale, ReadFloat(input)
* timelineScale);
if (frameIndex < frameCount - 1) ReadCurve(input, frameIndex, timeline);
}
timelines.Add(timeline);
duration = Math.Max(duration, timeline.frames[frameCount * 3 - 3]);
break;
}
case TIMELINE_FLIPX:
case TIMELINE_FLIPY: {
FlipXTimeline timeline = timelineType == TIMELINE_FLIPX ? new FlipXTimeline(frameCount) : new FlipYTimeline(
frameCount);
timeline.boneIndex = boneIndex;
for (int frameIndex = 0; frameIndex < frameCount; frameIndex++)
timeline.SetFrame(frameIndex, ReadFloat(input), ReadBoolean(input));
timelines.Add(timeline);
duration = Math.Max(duration, timeline.frames[frameCount * 2 - 2]);
break;
}
}
}
}
// IK timelines.
for (int i = 0, n = ReadInt(input, true); i < n; i++) {
IkConstraintData ikConstraint = skeletonData.ikConstraints[ReadInt(input, true)];
int frameCount = ReadInt(input, true);
IkConstraintTimeline timeline = new IkConstraintTimeline(frameCount);
timeline.ikConstraintIndex = skeletonData.ikConstraints.IndexOf(ikConstraint);
for (int frameIndex = 0; frameIndex < frameCount; frameIndex++) {
timeline.SetFrame(frameIndex, ReadFloat(input), ReadFloat(input), ReadSByte(input));
if (frameIndex < frameCount - 1) ReadCurve(input, frameIndex, timeline);
}
timelines.Add(timeline);
duration = Math.Max(duration, timeline.frames[frameCount * 3 - 3]);
}
// FFD timelines.
for (int i = 0, n = ReadInt(input, true); i < n; i++) {
Skin skin = skeletonData.skins[ReadInt(input, true)];
for (int ii = 0, nn = ReadInt(input, true); ii < nn; ii++) {
int slotIndex = ReadInt(input, true);
for (int iii = 0, nnn = ReadInt(input, true); iii < nnn; iii++) {
Attachment attachment = skin.GetAttachment(slotIndex, ReadString(input));
int frameCount = ReadInt(input, true);
FFDTimeline timeline = new FFDTimeline(frameCount);
timeline.slotIndex = slotIndex;
timeline.attachment = attachment;
for (int frameIndex = 0; frameIndex < frameCount; frameIndex++) {
float time = ReadFloat(input);
float[] vertices;
int vertexCount;
if (attachment is MeshAttachment)
vertexCount = ((MeshAttachment)attachment).vertices.Length;
else
vertexCount = ((SkinnedMeshAttachment)attachment).weights.Length / 3 * 2;
int end = ReadInt(input, true);
if (end == 0) {
if (attachment is MeshAttachment)
vertices = ((MeshAttachment)attachment).vertices;
else
vertices = new float[vertexCount];
} else {
vertices = new float[vertexCount];
int start = ReadInt(input, true);
end += start;
if (scale == 1) {
for (int v = start; v < end; v++)
vertices[v] = ReadFloat(input);
} else {
for (int v = start; v < end; v++)
vertices[v] = ReadFloat(input) * scale;
}
if (attachment is MeshAttachment) {
float[] meshVertices = ((MeshAttachment)attachment).vertices;
for (int v = 0, vn = vertices.Length; v < vn; v++)
vertices[v] += meshVertices[v];
}
}
timeline.SetFrame(frameIndex, time, vertices);
if (frameIndex < frameCount - 1) ReadCurve(input, frameIndex, timeline);
}
timelines.Add(timeline);
duration = Math.Max(duration, timeline.frames[frameCount - 1]);
}
}
}
// Draw order timeline.
int drawOrderCount = ReadInt(input, true);
if (drawOrderCount > 0) {
DrawOrderTimeline timeline = new DrawOrderTimeline(drawOrderCount);
int slotCount = skeletonData.slots.Count;
for (int i = 0; i < drawOrderCount; i++) {
int offsetCount = ReadInt(input, true);
int[] drawOrder = new int[slotCount];
for (int ii = slotCount - 1; ii >= 0; ii--)
drawOrder[ii] = -1;
int[] unchanged = new int[slotCount - offsetCount];
int originalIndex = 0, unchangedIndex = 0;
for (int ii = 0; ii < offsetCount; ii++) {
int slotIndex = ReadInt(input, true);
// Collect unchanged items.
while (originalIndex != slotIndex)
unchanged[unchangedIndex++] = originalIndex++;
// Set changed items.
drawOrder[originalIndex + ReadInt(input, true)] = originalIndex++;
}
// Collect remaining unchanged items.
while (originalIndex < slotCount)
unchanged[unchangedIndex++] = originalIndex++;
// Fill in unchanged items.
for (int ii = slotCount - 1; ii >= 0; ii--)
if (drawOrder[ii] == -1) drawOrder[ii] = unchanged[--unchangedIndex];
timeline.SetFrame(i, ReadFloat(input), drawOrder);
}
timelines.Add(timeline);
duration = Math.Max(duration, timeline.frames[drawOrderCount - 1]);
}
// Event timeline.
int eventCount = ReadInt(input, true);
if (eventCount > 0) {
EventTimeline timeline = new EventTimeline(eventCount);
for (int i = 0; i < eventCount; i++) {
float time = ReadFloat(input);
EventData eventData = skeletonData.events[ReadInt(input, true)];
Event e = new Event(eventData);
e.Int = ReadInt(input, false);
e.Float = ReadFloat(input);
e.String = ReadBoolean(input) ? ReadString(input) : eventData.String;
timeline.SetFrame(i, time, e);
}
timelines.Add(timeline);
duration = Math.Max(duration, timeline.frames[eventCount - 1]);
}
timelines.TrimExcess();
skeletonData.animations.Add(new Animation(name, timelines, duration));
}
private void ReadCurve (Stream input, int frameIndex, CurveTimeline timeline) {
switch (input.ReadByte()) {
case CURVE_STEPPED:
timeline.SetStepped(frameIndex);
break;
case CURVE_BEZIER:
timeline.SetCurve(frameIndex, ReadFloat(input), ReadFloat(input), ReadFloat(input), ReadFloat(input));
break;
}
}
private sbyte ReadSByte (Stream input) {
int value = input.ReadByte();
if (value == -1) throw new EndOfStreamException();
return (sbyte)value;
}
private bool ReadBoolean (Stream input) {
return input.ReadByte() != 0;
}
private float ReadFloat (Stream input) {
buffer[3] = (byte)input.ReadByte();
buffer[2] = (byte)input.ReadByte();
buffer[1] = (byte)input.ReadByte();
buffer[0] = (byte)input.ReadByte();
return BitConverter.ToSingle(buffer, 0);
}
private int ReadInt (Stream input) {
return (input.ReadByte() << 24) + (input.ReadByte() << 16) + (input.ReadByte() << 8) + input.ReadByte();
}
private int ReadInt (Stream input, bool optimizePositive) {
int b = input.ReadByte();
int result = b & 0x7F;
if ((b & 0x80) != 0) {
b = input.ReadByte();
result |= (b & 0x7F) << 7;
if ((b & 0x80) != 0) {
b = input.ReadByte();
result |= (b & 0x7F) << 14;
if ((b & 0x80) != 0) {
b = input.ReadByte();
result |= (b & 0x7F) << 21;
if ((b & 0x80) != 0) {
b = input.ReadByte();
result |= (b & 0x7F) << 28;
}
}
}
}
return optimizePositive ? result : ((result >> 1) ^ -(result & 1));
}
private string ReadString (Stream input) {
int charCount = ReadInt(input, true);
switch (charCount) {
case 0:
return null;
case 1:
return "";
}
charCount--;
char[] chars = this.chars;
if (chars.Length < charCount) this.chars = chars = new char[charCount];
// Try to read 7 bit ASCII chars.
int charIndex = 0;
int b = 0;
while (charIndex < charCount) {
b = input.ReadByte();
if (b > 127) break;
chars[charIndex++] = (char)b;
}
// If a char was not ASCII, finish with slow path.
if (charIndex < charCount) ReadUtf8_slow(input, charCount, charIndex, b);
return new String(chars, 0, charCount);
}
private void ReadUtf8_slow (Stream input, int charCount, int charIndex, int b) {
char[] chars = this.chars;
while (true) {
switch (b >> 4) {
case 0:
case 1:
case 2:
case 3:
case 4:
case 5:
case 6:
case 7:
chars[charIndex] = (char)b;
break;
case 12:
case 13:
chars[charIndex] = (char)((b & 0x1F) << 6 | input.ReadByte() & 0x3F);
break;
case 14:
chars[charIndex] = (char)((b & 0x0F) << 12 | (input.ReadByte() & 0x3F) << 6 | input.ReadByte() & 0x3F);
break;
}
if (++charIndex >= charCount) break;
b = input.ReadByte() & 0xFF;
}
}
}
}

View File

@@ -0,0 +1,215 @@
/******************************************************************************
* Spine Runtimes Software License
* Version 2.1
*
* Copyright (c) 2013, Esoteric Software
* All rights reserved.
*
* You are granted a perpetual, non-exclusive, non-sublicensable and
* non-transferable license to install, execute and perform the Spine Runtimes
* Software (the "Software") solely for internal use. Without the written
* permission of Esoteric Software (typically granted by licensing Spine), you
* may not (a) modify, translate, adapt or otherwise create derivative works,
* improvements of the Software or develop new applications using the Software
* or (b) remove, delete, alter or obscure any trademarks or any copyright,
* trademark, patent or other intellectual property or proprietary rights
* notices on or in the Software, including any copy thereof. Redistributions
* in binary or source form must include this license and terms.
*
* THIS SOFTWARE IS PROVIDED BY ESOTERIC SOFTWARE "AS IS" AND ANY EXPRESS OR
* IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
* MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO
* EVENT SHALL ESOTERIC SOFTARE BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
* PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS;
* OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
* WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR
* OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
* ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*****************************************************************************/
using System;
using System.Collections.Generic;
namespace SpineRuntime21 {
public class SkeletonBounds {
private List<Polygon> polygonPool = new List<Polygon>();
private float minX, minY, maxX, maxY;
public List<BoundingBoxAttachment> BoundingBoxes { get; private set; }
public List<Polygon> Polygons { get; private set; }
public float MinX { get { return minX; } set { minX = value; } }
public float MinY { get { return minY; } set { minY = value; } }
public float MaxX { get { return maxX; } set { maxX = value; } }
public float MaxY { get { return maxY; } set { maxY = value; } }
public float Width { get { return maxX - minX; } }
public float Height { get { return maxY - minY; } }
public SkeletonBounds () {
BoundingBoxes = new List<BoundingBoxAttachment>();
Polygons = new List<Polygon>();
}
public void Update (Skeleton skeleton, bool updateAabb) {
List<BoundingBoxAttachment> boundingBoxes = BoundingBoxes;
List<Polygon> polygons = Polygons;
List<Slot> slots = skeleton.slots;
int slotCount = slots.Count;
boundingBoxes.Clear();
foreach (Polygon polygon in polygons)
polygonPool.Add(polygon);
polygons.Clear();
for (int i = 0; i < slotCount; i++) {
Slot slot = slots[i];
BoundingBoxAttachment boundingBox = slot.attachment as BoundingBoxAttachment;
if (boundingBox == null) continue;
boundingBoxes.Add(boundingBox);
Polygon polygon = null;
int poolCount = polygonPool.Count;
if (poolCount > 0) {
polygon = polygonPool[poolCount - 1];
polygonPool.RemoveAt(poolCount - 1);
} else
polygon = new Polygon();
polygons.Add(polygon);
int count = boundingBox.Vertices.Length;
polygon.Count = count;
if (polygon.Vertices.Length < count) polygon.Vertices = new float[count];
boundingBox.ComputeWorldVertices(slot.bone, polygon.Vertices);
}
if (updateAabb) aabbCompute();
}
private void aabbCompute () {
float minX = int.MaxValue, minY = int.MaxValue, maxX = int.MinValue, maxY = int.MinValue;
List<Polygon> polygons = Polygons;
for (int i = 0, n = polygons.Count; i < n; i++) {
Polygon polygon = polygons[i];
float[] vertices = polygon.Vertices;
for (int ii = 0, nn = polygon.Count; ii < nn; ii += 2) {
float x = vertices[ii];
float y = vertices[ii + 1];
minX = Math.Min(minX, x);
minY = Math.Min(minY, y);
maxX = Math.Max(maxX, x);
maxY = Math.Max(maxY, y);
}
}
this.minX = minX;
this.minY = minY;
this.maxX = maxX;
this.maxY = maxY;
}
/// <summary>Returns true if the axis aligned bounding box contains the point.</summary>
public bool AabbContainsPoint (float x, float y) {
return x >= minX && x <= maxX && y >= minY && y <= maxY;
}
/// <summary>Returns true if the axis aligned bounding box intersects the line segment.</summary>
public bool AabbIntersectsSegment (float x1, float y1, float x2, float y2) {
float minX = this.minX;
float minY = this.minY;
float maxX = this.maxX;
float maxY = this.maxY;
if ((x1 <= minX && x2 <= minX) || (y1 <= minY && y2 <= minY) || (x1 >= maxX && x2 >= maxX) || (y1 >= maxY && y2 >= maxY))
return false;
float m = (y2 - y1) / (x2 - x1);
float y = m * (minX - x1) + y1;
if (y > minY && y < maxY) return true;
y = m * (maxX - x1) + y1;
if (y > minY && y < maxY) return true;
float x = (minY - y1) / m + x1;
if (x > minX && x < maxX) return true;
x = (maxY - y1) / m + x1;
if (x > minX && x < maxX) return true;
return false;
}
/// <summary>Returns true if the axis aligned bounding box intersects the axis aligned bounding box of the specified bounds.</summary>
public bool AabbIntersectsSkeleton (SkeletonBounds bounds) {
return minX < bounds.maxX && maxX > bounds.minX && minY < bounds.maxY && maxY > bounds.minY;
}
/// <summary>Returns true if the polygon contains the point.</summary>
public bool ContainsPoint (Polygon polygon, float x, float y) {
float[] vertices = polygon.Vertices;
int nn = polygon.Count;
int prevIndex = nn - 2;
bool inside = false;
for (int ii = 0; ii < nn; ii += 2) {
float vertexY = vertices[ii + 1];
float prevY = vertices[prevIndex + 1];
if ((vertexY < y && prevY >= y) || (prevY < y && vertexY >= y)) {
float vertexX = vertices[ii];
if (vertexX + (y - vertexY) / (prevY - vertexY) * (vertices[prevIndex] - vertexX) < x) inside = !inside;
}
prevIndex = ii;
}
return inside;
}
/// <summary>Returns the first bounding box attachment that contains the point, or null. When doing many checks, it is usually more
/// efficient to only call this method if {@link #aabbContainsPoint(float, float)} returns true.</summary>
public BoundingBoxAttachment ContainsPoint (float x, float y) {
List<Polygon> polygons = Polygons;
for (int i = 0, n = polygons.Count; i < n; i++)
if (ContainsPoint(polygons[i], x, y)) return BoundingBoxes[i];
return null;
}
/// <summary>Returns the first bounding box attachment that contains the line segment, or null. When doing many checks, it is usually
/// more efficient to only call this method if {@link #aabbIntersectsSegment(float, float, float, float)} returns true.</summary>
public BoundingBoxAttachment IntersectsSegment (float x1, float y1, float x2, float y2) {
List<Polygon> polygons = Polygons;
for (int i = 0, n = polygons.Count; i < n; i++)
if (IntersectsSegment(polygons[i], x1, y1, x2, y2)) return BoundingBoxes[i];
return null;
}
/// <summary>Returns true if the polygon contains the line segment.</summary>
public bool IntersectsSegment (Polygon polygon, float x1, float y1, float x2, float y2) {
float[] vertices = polygon.Vertices;
int nn = polygon.Count;
float width12 = x1 - x2, height12 = y1 - y2;
float det1 = x1 * y2 - y1 * x2;
float x3 = vertices[nn - 2], y3 = vertices[nn - 1];
for (int ii = 0; ii < nn; ii += 2) {
float x4 = vertices[ii], y4 = vertices[ii + 1];
float det2 = x3 * y4 - y3 * x4;
float width34 = x3 - x4, height34 = y3 - y4;
float det3 = width12 * height34 - height12 * width34;
float x = (det1 * width34 - width12 * det2) / det3;
if (((x >= x3 && x <= x4) || (x >= x4 && x <= x3)) && ((x >= x1 && x <= x2) || (x >= x2 && x <= x1))) {
float y = (det1 * height34 - height12 * det2) / det3;
if (((y >= y3 && y <= y4) || (y >= y4 && y <= y3)) && ((y >= y1 && y <= y2) || (y >= y2 && y <= y1))) return true;
}
x3 = x4;
y3 = y4;
}
return false;
}
public Polygon getPolygon (BoundingBoxAttachment attachment) {
int index = BoundingBoxes.IndexOf(attachment);
return index == -1 ? null : Polygons[index];
}
}
public class Polygon {
public float[] Vertices { get; set; }
public int Count { get; set; }
public Polygon () {
Vertices = new float[16];
}
}
}

View File

@@ -0,0 +1,158 @@
/******************************************************************************
* Spine Runtimes Software License
* Version 2.1
*
* Copyright (c) 2013, Esoteric Software
* All rights reserved.
*
* You are granted a perpetual, non-exclusive, non-sublicensable and
* non-transferable license to install, execute and perform the Spine Runtimes
* Software (the "Software") solely for internal use. Without the written
* permission of Esoteric Software (typically granted by licensing Spine), you
* may not (a) modify, translate, adapt or otherwise create derivative works,
* improvements of the Software or develop new applications using the Software
* or (b) remove, delete, alter or obscure any trademarks or any copyright,
* trademark, patent or other intellectual property or proprietary rights
* notices on or in the Software, including any copy thereof. Redistributions
* in binary or source form must include this license and terms.
*
* THIS SOFTWARE IS PROVIDED BY ESOTERIC SOFTWARE "AS IS" AND ANY EXPRESS OR
* IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
* MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO
* EVENT SHALL ESOTERIC SOFTARE BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
* PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS;
* OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
* WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR
* OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
* ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*****************************************************************************/
using System;
using System.Collections.Generic;
namespace SpineRuntime21 {
public class SkeletonData {
internal String name;
internal List<BoneData> bones = new List<BoneData>();
internal List<SlotData> slots = new List<SlotData>();
internal List<Skin> skins = new List<Skin>();
internal Skin defaultSkin;
internal List<EventData> events = new List<EventData>();
internal List<Animation> animations = new List<Animation>();
internal List<IkConstraintData> ikConstraints = new List<IkConstraintData>();
internal float width, height;
internal String version, hash, imagesPath;
public String Name { get { return name; } set { name = value; } }
public List<BoneData> Bones { get { return bones; } } // Ordered parents first.
public List<SlotData> Slots { get { return slots; } } // Setup pose draw order.
public List<Skin> Skins { get { return skins; } set { skins = value; } }
/// <summary>May be null.</summary>
public Skin DefaultSkin { get { return defaultSkin; } set { defaultSkin = value; } }
public List<EventData> Events { get { return events; } set { events = value; } }
public List<Animation> Animations { get { return animations; } set { animations = value; } }
public List<IkConstraintData> IkConstraints { get { return ikConstraints; } set { ikConstraints = value; } }
public float Width { get { return width; } set { width = value; } }
public float Height { get { return height; } set { height = value; } }
/// <summary>The Spine version used to export this data.</summary>
public String Version { get { return version; } set { version = value; } }
public String Hash { get { return hash; } set { hash = value; } }
// --- Bones.
/// <returns>May be null.</returns>
public BoneData FindBone (String boneName) {
if (boneName == null) throw new ArgumentNullException("boneName cannot be null.");
List<BoneData> bones = this.bones;
for (int i = 0, n = bones.Count; i < n; i++) {
BoneData bone = bones[i];
if (bone.name == boneName) return bone;
}
return null;
}
/// <returns>-1 if the bone was not found.</returns>
public int FindBoneIndex (String boneName) {
if (boneName == null) throw new ArgumentNullException("boneName cannot be null.");
List<BoneData> bones = this.bones;
for (int i = 0, n = bones.Count; i < n; i++)
if (bones[i].name == boneName) return i;
return -1;
}
// --- Slots.
/// <returns>May be null.</returns>
public SlotData FindSlot (String slotName) {
if (slotName == null) throw new ArgumentNullException("slotName cannot be null.");
List<SlotData> slots = this.slots;
for (int i = 0, n = slots.Count; i < n; i++) {
SlotData slot = slots[i];
if (slot.name == slotName) return slot;
}
return null;
}
/// <returns>-1 if the bone was not found.</returns>
public int FindSlotIndex (String slotName) {
if (slotName == null) throw new ArgumentNullException("slotName cannot be null.");
List<SlotData> slots = this.slots;
for (int i = 0, n = slots.Count; i < n; i++)
if (slots[i].name == slotName) return i;
return -1;
}
// --- Skins.
/// <returns>May be null.</returns>
public Skin FindSkin (String skinName) {
if (skinName == null) throw new ArgumentNullException("skinName cannot be null.");
foreach (Skin skin in skins)
if (skin.name == skinName) return skin;
return null;
}
// --- Events.
/// <returns>May be null.</returns>
public EventData FindEvent (String eventDataName) {
if (eventDataName == null) throw new ArgumentNullException("eventDataName cannot be null.");
foreach (EventData eventData in events)
if (eventData.name == eventDataName) return eventData;
return null;
}
// --- Animations.
/// <returns>May be null.</returns>
public Animation FindAnimation (String animationName) {
if (animationName == null) throw new ArgumentNullException("animationName cannot be null.");
List<Animation> animations = this.animations;
for (int i = 0, n = animations.Count; i < n; i++) {
Animation animation = animations[i];
if (animation.name == animationName) return animation;
}
return null;
}
// --- IK constraints.
/// <returns>May be null.</returns>
public IkConstraintData FindIkConstraint (String ikConstraintName) {
if (ikConstraintName == null) throw new ArgumentNullException("ikConstraintName cannot be null.");
List<IkConstraintData> ikConstraints = this.ikConstraints;
for (int i = 0, n = ikConstraints.Count; i < n; i++) {
IkConstraintData ikConstraint = ikConstraints[i];
if (ikConstraint.name == ikConstraintName) return ikConstraint;
}
return null;
}
// ---
override public String ToString () {
return name ?? base.ToString();
}
}
}

View File

@@ -0,0 +1,641 @@
/******************************************************************************
* Spine Runtimes Software License
* Version 2.1
*
* Copyright (c) 2013, Esoteric Software
* All rights reserved.
*
* You are granted a perpetual, non-exclusive, non-sublicensable and
* non-transferable license to install, execute and perform the Spine Runtimes
* Software (the "Software") solely for internal use. Without the written
* permission of Esoteric Software (typically granted by licensing Spine), you
* may not (a) modify, translate, adapt or otherwise create derivative works,
* improvements of the Software or develop new applications using the Software
* or (b) remove, delete, alter or obscure any trademarks or any copyright,
* trademark, patent or other intellectual property or proprietary rights
* notices on or in the Software, including any copy thereof. Redistributions
* in binary or source form must include this license and terms.
*
* THIS SOFTWARE IS PROVIDED BY ESOTERIC SOFTWARE "AS IS" AND ANY EXPRESS OR
* IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
* MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO
* EVENT SHALL ESOTERIC SOFTARE BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
* PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS;
* OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
* WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR
* OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
* ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*****************************************************************************/
using System;
using System.IO;
using System.Collections.Generic;
#if WINDOWS_STOREAPP
using System.Threading.Tasks;
using Windows.Storage;
#endif
namespace SpineRuntime21 {
public class SkeletonJson {
private AttachmentLoader attachmentLoader;
public float Scale { get; set; }
public SkeletonJson (params Atlas[] atlasArray)
: this(new AtlasAttachmentLoader(atlasArray)) {
}
public SkeletonJson (AttachmentLoader attachmentLoader) {
if (attachmentLoader == null) throw new ArgumentNullException("attachmentLoader cannot be null.");
this.attachmentLoader = attachmentLoader;
Scale = 1;
}
#if WINDOWS_STOREAPP
private async Task<SkeletonData> ReadFile(string path) {
var folder = Windows.ApplicationModel.Package.Current.InstalledLocation;
var file = await folder.GetFileAsync(path).AsTask().ConfigureAwait(false);
using (var reader = new StreamReader(await file.OpenStreamForReadAsync().ConfigureAwait(false))) {
SkeletonData skeletonData = ReadSkeletonData(reader);
skeletonData.Name = Path.GetFileNameWithoutExtension(path);
return skeletonData;
}
}
public SkeletonData ReadSkeletonData (String path) {
return this.ReadFile(path).Result;
}
#else
public SkeletonData ReadSkeletonData (String path) {
#if WINDOWS_PHONE
Stream stream = Microsoft.Xna.Framework.TitleContainer.OpenStream(path);
using (StreamReader reader = new StreamReader(stream)) {
#else
using (StreamReader reader = new StreamReader(path)) {
#endif
SkeletonData skeletonData = ReadSkeletonData(reader);
skeletonData.name = Path.GetFileNameWithoutExtension(path);
return skeletonData;
}
}
#endif
public SkeletonData ReadSkeletonData (TextReader reader) {
if (reader == null) throw new ArgumentNullException("reader cannot be null.");
var skeletonData = new SkeletonData();
var root = Json.Deserialize(reader) as Dictionary<String, Object>;
if (root == null) throw new Exception("Invalid JSON.");
// Skeleton.
if (root.ContainsKey("skeleton")) {
var skeletonMap = (Dictionary<String, Object>)root["skeleton"];
skeletonData.hash = (String)skeletonMap["hash"];
skeletonData.version = (String)skeletonMap["spine"];
skeletonData.width = GetFloat(skeletonMap, "width", 0);
skeletonData.height = GetFloat(skeletonMap, "height", 0);
}
// Bones.
foreach (Dictionary<String, Object> boneMap in (List<Object>)root["bones"]) {
BoneData parent = null;
if (boneMap.ContainsKey("parent")) {
parent = skeletonData.FindBone((String)boneMap["parent"]);
if (parent == null)
throw new Exception("Parent bone not found: " + boneMap["parent"]);
}
var boneData = new BoneData((String)boneMap["name"], parent);
boneData.length = GetFloat(boneMap, "length", 0) * Scale;
boneData.x = GetFloat(boneMap, "x", 0) * Scale;
boneData.y = GetFloat(boneMap, "y", 0) * Scale;
boneData.rotation = GetFloat(boneMap, "rotation", 0);
boneData.scaleX = GetFloat(boneMap, "scaleX", 1);
boneData.scaleY = GetFloat(boneMap, "scaleY", 1);
boneData.flipX = GetBoolean(boneMap, "flipX", false);
boneData.flipY = GetBoolean(boneMap, "flipY", false);
boneData.inheritScale = GetBoolean(boneMap, "inheritScale", true);
boneData.inheritRotation = GetBoolean(boneMap, "inheritRotation", true);
skeletonData.bones.Add(boneData);
}
// IK constraints.
if (root.ContainsKey("ik")) {
foreach (Dictionary<String, Object> ikMap in (List<Object>)root["ik"]) {
IkConstraintData ikConstraintData = new IkConstraintData((String)ikMap["name"]);
foreach (String boneName in (List<Object>)ikMap["bones"]) {
BoneData bone = skeletonData.FindBone(boneName);
if (bone == null) throw new Exception("IK bone not found: " + boneName);
ikConstraintData.bones.Add(bone);
}
String targetName = (String)ikMap["target"];
ikConstraintData.target = skeletonData.FindBone(targetName);
if (ikConstraintData.target == null) throw new Exception("Target bone not found: " + targetName);
ikConstraintData.bendDirection = GetBoolean(ikMap, "bendPositive", true) ? 1 : -1;
ikConstraintData.mix = GetFloat(ikMap, "mix", 1);
skeletonData.ikConstraints.Add(ikConstraintData);
}
}
// Slots.
if (root.ContainsKey("slots")) {
foreach (Dictionary<String, Object> slotMap in (List<Object>)root["slots"]) {
var slotName = (String)slotMap["name"];
var boneName = (String)slotMap["bone"];
BoneData boneData = skeletonData.FindBone(boneName);
if (boneData == null)
throw new Exception("Slot bone not found: " + boneName);
var slotData = new SlotData(slotName, boneData);
if (slotMap.ContainsKey("color")) {
var color = (String)slotMap["color"];
slotData.r = ToColor(color, 0);
slotData.g = ToColor(color, 1);
slotData.b = ToColor(color, 2);
slotData.a = ToColor(color, 3);
}
if (slotMap.ContainsKey("attachment"))
slotData.attachmentName = (String)slotMap["attachment"];
if (slotMap.ContainsKey("additive"))
slotData.additiveBlending = (bool)slotMap["additive"];
skeletonData.slots.Add(slotData);
}
}
// Skins.
if (root.ContainsKey("skins")) {
foreach (KeyValuePair<String, Object> entry in (Dictionary<String, Object>)root["skins"]) {
var skin = new Skin(entry.Key);
foreach (KeyValuePair<String, Object> slotEntry in (Dictionary<String, Object>)entry.Value) {
int slotIndex = skeletonData.FindSlotIndex(slotEntry.Key);
foreach (KeyValuePair<String, Object> attachmentEntry in ((Dictionary<String, Object>)slotEntry.Value)) {
Attachment attachment = ReadAttachment(skin, attachmentEntry.Key, (Dictionary<String, Object>)attachmentEntry.Value);
if (attachment != null) skin.AddAttachment(slotIndex, attachmentEntry.Key, attachment);
}
}
skeletonData.skins.Add(skin);
if (skin.name == "default")
skeletonData.defaultSkin = skin;
}
}
// Events.
if (root.ContainsKey("events")) {
foreach (KeyValuePair<String, Object> entry in (Dictionary<String, Object>)root["events"]) {
var entryMap = (Dictionary<String, Object>)entry.Value;
var eventData = new EventData(entry.Key);
eventData.Int = GetInt(entryMap, "int", 0);
eventData.Float = GetFloat(entryMap, "float", 0);
eventData.String = GetString(entryMap, "string", null);
skeletonData.events.Add(eventData);
}
}
// Animations.
if (root.ContainsKey("animations")) {
foreach (KeyValuePair<String, Object> entry in (Dictionary<String, Object>)root["animations"])
ReadAnimation(entry.Key, (Dictionary<String, Object>)entry.Value, skeletonData);
}
skeletonData.bones.TrimExcess();
skeletonData.slots.TrimExcess();
skeletonData.skins.TrimExcess();
skeletonData.events.TrimExcess();
skeletonData.animations.TrimExcess();
skeletonData.ikConstraints.TrimExcess();
return skeletonData;
}
private Attachment ReadAttachment (Skin skin, String name, Dictionary<String, Object> map) {
if (map.ContainsKey("name"))
name = (String)map["name"];
var type = AttachmentType.region;
if (map.ContainsKey("type"))
type = (AttachmentType)Enum.Parse(typeof(AttachmentType), (String)map["type"], false);
String path = name;
if (map.ContainsKey("path"))
path = (String)map["path"];
switch (type) {
case AttachmentType.region:
RegionAttachment region = attachmentLoader.NewRegionAttachment(skin, name, path);
if (region == null) return null;
region.Path = path;
region.x = GetFloat(map, "x", 0) * Scale;
region.y = GetFloat(map, "y", 0) * Scale;
region.scaleX = GetFloat(map, "scaleX", 1);
region.scaleY = GetFloat(map, "scaleY", 1);
region.rotation = GetFloat(map, "rotation", 0);
region.width = GetFloat(map, "width", 32) * Scale;
region.height = GetFloat(map, "height", 32) * Scale;
region.UpdateOffset();
if (map.ContainsKey("color")) {
var color = (String)map["color"];
region.r = ToColor(color, 0);
region.g = ToColor(color, 1);
region.b = ToColor(color, 2);
region.a = ToColor(color, 3);
}
return region;
case AttachmentType.mesh: {
MeshAttachment mesh = attachmentLoader.NewMeshAttachment(skin, name, path);
if (mesh == null) return null;
mesh.Path = path;
mesh.vertices = GetFloatArray(map, "vertices", Scale);
mesh.triangles = GetIntArray(map, "triangles");
mesh.regionUVs = GetFloatArray(map, "uvs", 1);
mesh.UpdateUVs();
if (map.ContainsKey("color")) {
var color = (String)map["color"];
mesh.r = ToColor(color, 0);
mesh.g = ToColor(color, 1);
mesh.b = ToColor(color, 2);
mesh.a = ToColor(color, 3);
}
mesh.HullLength = GetInt(map, "hull", 0) * 2;
if (map.ContainsKey("edges")) mesh.Edges = GetIntArray(map, "edges");
mesh.Width = GetInt(map, "width", 0) * Scale;
mesh.Height = GetInt(map, "height", 0) * Scale;
return mesh;
}
case AttachmentType.skinnedmesh: {
SkinnedMeshAttachment mesh = attachmentLoader.NewSkinnedMeshAttachment(skin, name, path);
if (mesh == null) return null;
mesh.Path = path;
float[] uvs = GetFloatArray(map, "uvs", 1);
float[] vertices = GetFloatArray(map, "vertices", 1);
var weights = new List<float>(uvs.Length * 3 * 3);
var bones = new List<int>(uvs.Length * 3);
float scale = Scale;
for (int i = 0, n = vertices.Length; i < n; ) {
int boneCount = (int)vertices[i++];
bones.Add(boneCount);
for (int nn = i + boneCount * 4; i < nn; ) {
bones.Add((int)vertices[i]);
weights.Add(vertices[i + 1] * scale);
weights.Add(vertices[i + 2] * scale);
weights.Add(vertices[i + 3]);
i += 4;
}
}
mesh.bones = bones.ToArray();
mesh.weights = weights.ToArray();
mesh.triangles = GetIntArray(map, "triangles");
mesh.regionUVs = uvs;
mesh.UpdateUVs();
if (map.ContainsKey("color")) {
var color = (String)map["color"];
mesh.r = ToColor(color, 0);
mesh.g = ToColor(color, 1);
mesh.b = ToColor(color, 2);
mesh.a = ToColor(color, 3);
}
mesh.HullLength = GetInt(map, "hull", 0) * 2;
if (map.ContainsKey("edges")) mesh.Edges = GetIntArray(map, "edges");
mesh.Width = GetInt(map, "width", 0) * Scale;
mesh.Height = GetInt(map, "height", 0) * Scale;
return mesh;
}
case AttachmentType.boundingbox:
BoundingBoxAttachment box = attachmentLoader.NewBoundingBoxAttachment(skin, name);
if (box == null) return null;
box.vertices = GetFloatArray(map, "vertices", Scale);
return box;
}
return null;
}
private float[] GetFloatArray (Dictionary<String, Object> map, String name, float scale) {
var list = (List<Object>)map[name];
var values = new float[list.Count];
if (scale == 1) {
for (int i = 0, n = list.Count; i < n; i++)
values[i] = (float)list[i];
} else {
for (int i = 0, n = list.Count; i < n; i++)
values[i] = (float)list[i] * scale;
}
return values;
}
private int[] GetIntArray (Dictionary<String, Object> map, String name) {
var list = (List<Object>)map[name];
var values = new int[list.Count];
for (int i = 0, n = list.Count; i < n; i++)
values[i] = (int)(float)list[i];
return values;
}
private float GetFloat (Dictionary<String, Object> map, String name, float defaultValue) {
if (!map.ContainsKey(name))
return defaultValue;
return (float)map[name];
}
private int GetInt (Dictionary<String, Object> map, String name, int defaultValue) {
if (!map.ContainsKey(name))
return defaultValue;
return (int)(float)map[name];
}
private bool GetBoolean (Dictionary<String, Object> map, String name, bool defaultValue) {
if (!map.ContainsKey(name))
return defaultValue;
return (bool)map[name];
}
private String GetString (Dictionary<String, Object> map, String name, String defaultValue) {
if (!map.ContainsKey(name))
return defaultValue;
return (String)map[name];
}
private float ToColor (String hexString, int colorIndex) {
if (hexString.Length != 8)
throw new ArgumentException("Color hexidecimal length must be 8, recieved: " + hexString);
return Convert.ToInt32(hexString.Substring(colorIndex * 2, 2), 16) / (float)255;
}
private void ReadAnimation (String name, Dictionary<String, Object> map, SkeletonData skeletonData) {
var timelines = new List<Timeline>();
float duration = 0;
float scale = Scale;
if (map.ContainsKey("slots")) {
foreach (KeyValuePair<String, Object> entry in (Dictionary<String, Object>)map["slots"]) {
String slotName = entry.Key;
int slotIndex = skeletonData.FindSlotIndex(slotName);
var timelineMap = (Dictionary<String, Object>)entry.Value;
foreach (KeyValuePair<String, Object> timelineEntry in timelineMap) {
var values = (List<Object>)timelineEntry.Value;
var timelineName = (String)timelineEntry.Key;
if (timelineName == "color") {
var timeline = new ColorTimeline(values.Count);
timeline.slotIndex = slotIndex;
int frameIndex = 0;
foreach (Dictionary<String, Object> valueMap in values) {
float time = (float)valueMap["time"];
String c = (String)valueMap["color"];
timeline.SetFrame(frameIndex, time, ToColor(c, 0), ToColor(c, 1), ToColor(c, 2), ToColor(c, 3));
ReadCurve(timeline, frameIndex, valueMap);
frameIndex++;
}
timelines.Add(timeline);
duration = Math.Max(duration, timeline.frames[timeline.FrameCount * 5 - 5]);
} else if (timelineName == "attachment") {
var timeline = new AttachmentTimeline(values.Count);
timeline.slotIndex = slotIndex;
int frameIndex = 0;
foreach (Dictionary<String, Object> valueMap in values) {
float time = (float)valueMap["time"];
timeline.SetFrame(frameIndex++, time, (String)valueMap["name"]);
}
timelines.Add(timeline);
duration = Math.Max(duration, timeline.frames[timeline.FrameCount - 1]);
} else
throw new Exception("Invalid timeline type for a slot: " + timelineName + " (" + slotName + ")");
}
}
}
if (map.ContainsKey("bones")) {
foreach (KeyValuePair<String, Object> entry in (Dictionary<String, Object>)map["bones"]) {
String boneName = entry.Key;
int boneIndex = skeletonData.FindBoneIndex(boneName);
if (boneIndex == -1)
throw new Exception("Bone not found: " + boneName);
var timelineMap = (Dictionary<String, Object>)entry.Value;
foreach (KeyValuePair<String, Object> timelineEntry in timelineMap) {
var values = (List<Object>)timelineEntry.Value;
var timelineName = (String)timelineEntry.Key;
if (timelineName == "rotate") {
var timeline = new RotateTimeline(values.Count);
timeline.boneIndex = boneIndex;
int frameIndex = 0;
foreach (Dictionary<String, Object> valueMap in values) {
float time = (float)valueMap["time"];
timeline.SetFrame(frameIndex, time, (float)valueMap["angle"]);
ReadCurve(timeline, frameIndex, valueMap);
frameIndex++;
}
timelines.Add(timeline);
duration = Math.Max(duration, timeline.frames[timeline.FrameCount * 2 - 2]);
} else if (timelineName == "translate" || timelineName == "scale") {
TranslateTimeline timeline;
float timelineScale = 1;
if (timelineName == "scale")
timeline = new ScaleTimeline(values.Count);
else {
timeline = new TranslateTimeline(values.Count);
timelineScale = scale;
}
timeline.boneIndex = boneIndex;
int frameIndex = 0;
foreach (Dictionary<String, Object> valueMap in values) {
float time = (float)valueMap["time"];
float x = valueMap.ContainsKey("x") ? (float)valueMap["x"] : 0;
float y = valueMap.ContainsKey("y") ? (float)valueMap["y"] : 0;
timeline.SetFrame(frameIndex, time, (float)x * timelineScale, (float)y * timelineScale);
ReadCurve(timeline, frameIndex, valueMap);
frameIndex++;
}
timelines.Add(timeline);
duration = Math.Max(duration, timeline.frames[timeline.FrameCount * 3 - 3]);
} else if (timelineName == "flipX" || timelineName == "flipY") {
bool x = timelineName == "flipX";
var timeline = x ? new FlipXTimeline(values.Count) : new FlipYTimeline(values.Count);
timeline.boneIndex = boneIndex;
String field = x ? "x" : "y";
int frameIndex = 0;
foreach (Dictionary<String, Object> valueMap in values) {
float time = (float)valueMap["time"];
timeline.SetFrame(frameIndex, time, valueMap.ContainsKey(field) ? (bool)valueMap[field] : false);
frameIndex++;
}
timelines.Add(timeline);
duration = Math.Max(duration, timeline.frames[timeline.FrameCount * 2 - 2]);
} else
throw new Exception("Invalid timeline type for a bone: " + timelineName + " (" + boneName + ")");
}
}
}
if (map.ContainsKey("ik")) {
foreach (KeyValuePair<String, Object> ikMap in (Dictionary<String, Object>)map["ik"]) {
IkConstraintData ikConstraint = skeletonData.FindIkConstraint(ikMap.Key);
var values = (List<Object>)ikMap.Value;
var timeline = new IkConstraintTimeline(values.Count);
timeline.ikConstraintIndex = skeletonData.ikConstraints.IndexOf(ikConstraint);
int frameIndex = 0;
foreach (Dictionary<String, Object> valueMap in values) {
float time = (float)valueMap["time"];
float mix = valueMap.ContainsKey("mix") ? (float)valueMap["mix"] : 1;
bool bendPositive = valueMap.ContainsKey("bendPositive") ? (bool)valueMap["bendPositive"] : true;
timeline.SetFrame(frameIndex, time, mix, bendPositive ? 1 : -1);
ReadCurve(timeline, frameIndex, valueMap);
frameIndex++;
}
timelines.Add(timeline);
duration = Math.Max(duration, timeline.frames[timeline.FrameCount * 3 - 3]);
}
}
if (map.ContainsKey("ffd")) {
foreach (KeyValuePair<String, Object> ffdMap in (Dictionary<String, Object>)map["ffd"]) {
Skin skin = skeletonData.FindSkin(ffdMap.Key);
foreach (KeyValuePair<String, Object> slotMap in (Dictionary<String, Object>)ffdMap.Value) {
int slotIndex = skeletonData.FindSlotIndex(slotMap.Key);
foreach (KeyValuePair<String, Object> meshMap in (Dictionary<String, Object>)slotMap.Value) {
var values = (List<Object>)meshMap.Value;
var timeline = new FFDTimeline(values.Count);
Attachment attachment = skin.GetAttachment(slotIndex, meshMap.Key);
if (attachment == null) throw new Exception("FFD attachment not found: " + meshMap.Key);
timeline.slotIndex = slotIndex;
timeline.attachment = attachment;
int vertexCount;
if (attachment is MeshAttachment)
vertexCount = ((MeshAttachment)attachment).vertices.Length;
else
vertexCount = ((SkinnedMeshAttachment)attachment).Weights.Length / 3 * 2;
int frameIndex = 0;
foreach (Dictionary<String, Object> valueMap in values) {
float[] vertices;
if (!valueMap.ContainsKey("vertices")) {
if (attachment is MeshAttachment)
vertices = ((MeshAttachment)attachment).vertices;
else
vertices = new float[vertexCount];
} else {
var verticesValue = (List<Object>)valueMap["vertices"];
vertices = new float[vertexCount];
int start = GetInt(valueMap, "offset", 0);
if (scale == 1) {
for (int i = 0, n = verticesValue.Count; i < n; i++)
vertices[i + start] = (float)verticesValue[i];
} else {
for (int i = 0, n = verticesValue.Count; i < n; i++)
vertices[i + start] = (float)verticesValue[i] * scale;
}
if (attachment is MeshAttachment) {
float[] meshVertices = ((MeshAttachment)attachment).vertices;
for (int i = 0; i < vertexCount; i++)
vertices[i] += meshVertices[i];
}
}
timeline.SetFrame(frameIndex, (float)valueMap["time"], vertices);
ReadCurve(timeline, frameIndex, valueMap);
frameIndex++;
}
timelines.Add(timeline);
duration = Math.Max(duration, timeline.frames[timeline.FrameCount - 1]);
}
}
}
}
if (map.ContainsKey("drawOrder") || map.ContainsKey("draworder")) {
var values = (List<Object>)map[map.ContainsKey("drawOrder") ? "drawOrder" : "draworder"];
var timeline = new DrawOrderTimeline(values.Count);
int slotCount = skeletonData.slots.Count;
int frameIndex = 0;
foreach (Dictionary<String, Object> drawOrderMap in values) {
int[] drawOrder = null;
if (drawOrderMap.ContainsKey("offsets")) {
drawOrder = new int[slotCount];
for (int i = slotCount - 1; i >= 0; i--)
drawOrder[i] = -1;
var offsets = (List<Object>)drawOrderMap["offsets"];
int[] unchanged = new int[slotCount - offsets.Count];
int originalIndex = 0, unchangedIndex = 0;
foreach (Dictionary<String, Object> offsetMap in offsets) {
int slotIndex = skeletonData.FindSlotIndex((String)offsetMap["slot"]);
if (slotIndex == -1) throw new Exception("Slot not found: " + offsetMap["slot"]);
// Collect unchanged items.
while (originalIndex != slotIndex)
unchanged[unchangedIndex++] = originalIndex++;
// Set changed items.
int index = originalIndex + (int)(float)offsetMap["offset"];
drawOrder[index] = originalIndex++;
}
// Collect remaining unchanged items.
while (originalIndex < slotCount)
unchanged[unchangedIndex++] = originalIndex++;
// Fill in unchanged items.
for (int i = slotCount - 1; i >= 0; i--)
if (drawOrder[i] == -1) drawOrder[i] = unchanged[--unchangedIndex];
}
timeline.SetFrame(frameIndex++, (float)drawOrderMap["time"], drawOrder);
}
timelines.Add(timeline);
duration = Math.Max(duration, timeline.frames[timeline.FrameCount - 1]);
}
if (map.ContainsKey("events")) {
var eventsMap = (List<Object>)map["events"];
var timeline = new EventTimeline(eventsMap.Count);
int frameIndex = 0;
foreach (Dictionary<String, Object> eventMap in eventsMap) {
EventData eventData = skeletonData.FindEvent((String)eventMap["name"]);
if (eventData == null) throw new Exception("Event not found: " + eventMap["name"]);
var e = new Event(eventData);
e.Int = GetInt(eventMap, "int", eventData.Int);
e.Float = GetFloat(eventMap, "float", eventData.Float);
e.String = GetString(eventMap, "string", eventData.String);
timeline.SetFrame(frameIndex++, (float)eventMap["time"], e);
}
timelines.Add(timeline);
duration = Math.Max(duration, timeline.frames[timeline.FrameCount - 1]);
}
timelines.TrimExcess();
skeletonData.animations.Add(new Animation(name, timelines, duration));
}
private void ReadCurve (CurveTimeline timeline, int frameIndex, Dictionary<String, Object> valueMap) {
if (!valueMap.ContainsKey("curve"))
return;
Object curveObject = valueMap["curve"];
if (curveObject.Equals("stepped"))
timeline.SetStepped(frameIndex);
else if (curveObject is List<Object>) {
var curve = (List<Object>)curveObject;
timeline.SetCurve(frameIndex, (float)curve[0], (float)curve[1], (float)curve[2], (float)curve[3]);
}
}
}
}

View File

@@ -0,0 +1,101 @@
/******************************************************************************
* Spine Runtimes Software License
* Version 2.1
*
* Copyright (c) 2013, Esoteric Software
* All rights reserved.
*
* You are granted a perpetual, non-exclusive, non-sublicensable and
* non-transferable license to install, execute and perform the Spine Runtimes
* Software (the "Software") solely for internal use. Without the written
* permission of Esoteric Software (typically granted by licensing Spine), you
* may not (a) modify, translate, adapt or otherwise create derivative works,
* improvements of the Software or develop new applications using the Software
* or (b) remove, delete, alter or obscure any trademarks or any copyright,
* trademark, patent or other intellectual property or proprietary rights
* notices on or in the Software, including any copy thereof. Redistributions
* in binary or source form must include this license and terms.
*
* THIS SOFTWARE IS PROVIDED BY ESOTERIC SOFTWARE "AS IS" AND ANY EXPRESS OR
* IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
* MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO
* EVENT SHALL ESOTERIC SOFTARE BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
* PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS;
* OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
* WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR
* OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
* ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*****************************************************************************/
using System;
using System.Collections.Generic;
namespace SpineRuntime21 {
/// <summary>Stores attachments by slot index and attachment name.</summary>
public class Skin {
internal String name;
private Dictionary<KeyValuePair<int, String>, Attachment> attachments =
new Dictionary<KeyValuePair<int, String>, Attachment>(AttachmentComparer.Instance);
public String Name { get { return name; } }
public Skin (String name) {
if (name == null) throw new ArgumentNullException("name cannot be null.");
this.name = name;
}
public void AddAttachment (int slotIndex, String name, Attachment attachment) {
if (attachment == null) throw new ArgumentNullException("attachment cannot be null.");
attachments[new KeyValuePair<int, String>(slotIndex, name)] = attachment;
}
/// <returns>May be null.</returns>
public Attachment GetAttachment (int slotIndex, String name) {
Attachment attachment;
attachments.TryGetValue(new KeyValuePair<int, String>(slotIndex, name), out attachment);
return attachment;
}
public void FindNamesForSlot (int slotIndex, List<String> names) {
if (names == null) throw new ArgumentNullException("names cannot be null.");
foreach (KeyValuePair<int, String> key in attachments.Keys)
if (key.Key == slotIndex) names.Add(key.Value);
}
public void FindAttachmentsForSlot (int slotIndex, List<Attachment> attachments) {
if (attachments == null) throw new ArgumentNullException("attachments cannot be null.");
foreach (KeyValuePair<KeyValuePair<int, String>, Attachment> entry in this.attachments)
if (entry.Key.Key == slotIndex) attachments.Add(entry.Value);
}
override public String ToString () {
return name;
}
/// <summary>Attach all attachments from this skin if the corresponding attachment from the old skin is currently attached.</summary>
internal void AttachAll (Skeleton skeleton, Skin oldSkin) {
foreach (KeyValuePair<KeyValuePair<int, String>, Attachment> entry in oldSkin.attachments) {
int slotIndex = entry.Key.Key;
Slot slot = skeleton.slots[slotIndex];
if (slot.attachment == entry.Value) {
Attachment attachment = GetAttachment(slotIndex, entry.Key.Value);
if (attachment != null) slot.Attachment = attachment;
}
}
}
// Avoids boxing in the dictionary.
private class AttachmentComparer : IEqualityComparer<KeyValuePair<int, String>> {
internal static readonly AttachmentComparer Instance = new AttachmentComparer();
bool IEqualityComparer<KeyValuePair<int, string>>.Equals (KeyValuePair<int, string> o1, KeyValuePair<int, string> o2) {
return o1.Key == o2.Key && o1.Value == o2.Value;
}
int IEqualityComparer<KeyValuePair<int, string>>.GetHashCode (KeyValuePair<int, string> o) {
return o.Key;
}
}
}
}

View File

@@ -0,0 +1,99 @@
/******************************************************************************
* Spine Runtimes Software License
* Version 2.1
*
* Copyright (c) 2013, Esoteric Software
* All rights reserved.
*
* You are granted a perpetual, non-exclusive, non-sublicensable and
* non-transferable license to install, execute and perform the Spine Runtimes
* Software (the "Software") solely for internal use. Without the written
* permission of Esoteric Software (typically granted by licensing Spine), you
* may not (a) modify, translate, adapt or otherwise create derivative works,
* improvements of the Software or develop new applications using the Software
* or (b) remove, delete, alter or obscure any trademarks or any copyright,
* trademark, patent or other intellectual property or proprietary rights
* notices on or in the Software, including any copy thereof. Redistributions
* in binary or source form must include this license and terms.
*
* THIS SOFTWARE IS PROVIDED BY ESOTERIC SOFTWARE "AS IS" AND ANY EXPRESS OR
* IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
* MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO
* EVENT SHALL ESOTERIC SOFTARE BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
* PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS;
* OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
* WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR
* OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
* ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*****************************************************************************/
using System;
namespace SpineRuntime21 {
public class Slot {
internal SlotData data;
internal Bone bone;
internal float r, g, b, a;
internal Attachment attachment;
internal float attachmentTime;
internal float[] attachmentVertices = new float[0];
internal int attachmentVerticesCount;
public SlotData Data { get { return data; } }
public Bone Bone { get { return bone; } }
public Skeleton Skeleton { get { return bone.skeleton; } }
public float R { get { return r; } set { r = value; } }
public float G { get { return g; } set { g = value; } }
public float B { get { return b; } set { b = value; } }
public float A { get { return a; } set { a = value; } }
/// <summary>May be null.</summary>
public Attachment Attachment {
get {
return attachment;
}
set {
attachment = value;
attachmentTime = bone.skeleton.time;
attachmentVerticesCount = 0;
}
}
public float AttachmentTime {
get {
return bone.skeleton.time - attachmentTime;
}
set {
attachmentTime = bone.skeleton.time - value;
}
}
public float[] AttachmentVertices { get { return attachmentVertices; } set { attachmentVertices = value; } }
public int AttachmentVerticesCount { get { return attachmentVerticesCount; } set { attachmentVerticesCount = value; } }
public Slot (SlotData data, Bone bone) {
if (data == null) throw new ArgumentNullException("data cannot be null.");
if (bone == null) throw new ArgumentNullException("bone cannot be null.");
this.data = data;
this.bone = bone;
SetToSetupPose();
}
internal void SetToSetupPose (int slotIndex) {
r = data.r;
g = data.g;
b = data.b;
a = data.a;
Attachment = data.attachmentName == null ? null : bone.skeleton.GetAttachment(slotIndex, data.attachmentName);
}
public void SetToSetupPose () {
SetToSetupPose(bone.skeleton.data.slots.IndexOf(data));
}
override public String ToString () {
return data.name;
}
}
}

View File

@@ -0,0 +1,62 @@
/******************************************************************************
* Spine Runtimes Software License
* Version 2.1
*
* Copyright (c) 2013, Esoteric Software
* All rights reserved.
*
* You are granted a perpetual, non-exclusive, non-sublicensable and
* non-transferable license to install, execute and perform the Spine Runtimes
* Software (the "Software") solely for internal use. Without the written
* permission of Esoteric Software (typically granted by licensing Spine), you
* may not (a) modify, translate, adapt or otherwise create derivative works,
* improvements of the Software or develop new applications using the Software
* or (b) remove, delete, alter or obscure any trademarks or any copyright,
* trademark, patent or other intellectual property or proprietary rights
* notices on or in the Software, including any copy thereof. Redistributions
* in binary or source form must include this license and terms.
*
* THIS SOFTWARE IS PROVIDED BY ESOTERIC SOFTWARE "AS IS" AND ANY EXPRESS OR
* IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
* MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO
* EVENT SHALL ESOTERIC SOFTARE BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
* PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS;
* OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
* WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR
* OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
* ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*****************************************************************************/
using System;
namespace SpineRuntime21 {
public class SlotData {
internal String name;
internal BoneData boneData;
internal float r = 1, g = 1, b = 1, a = 1;
internal String attachmentName;
internal bool additiveBlending;
public String Name { get { return name; } }
public BoneData BoneData { get { return boneData; } }
public float R { get { return r; } set { r = value; } }
public float G { get { return g; } set { g = value; } }
public float B { get { return b; } set { b = value; } }
public float A { get { return a; } set { a = value; } }
/// <summary>May be null.</summary>
public String AttachmentName { get { return attachmentName; } set { attachmentName = value; } }
public bool AdditiveBlending { get { return additiveBlending; } set { additiveBlending = value; } }
public SlotData (String name, BoneData boneData) {
if (name == null) throw new ArgumentNullException("name cannot be null.");
if (boneData == null) throw new ArgumentNullException("boneData cannot be null.");
this.name = name;
this.boneData = boneData;
}
override public String ToString () {
return name;
}
}
}

View File

@@ -0,0 +1,14 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<Platforms>x64</Platforms>
<TargetFramework>net8.0-windows</TargetFramework>
<RuntimeIdentifier>win-x64</RuntimeIdentifier>
<BaseOutputPath>$(SolutionDir)out</BaseOutputPath>
<IncludeSourceRevisionInInformationalVersion>false</IncludeSourceRevisionInInformationalVersion>
<Version>2.1.25</Version>
</PropertyGroup>
</Project>

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

@@ -15,6 +15,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution
ProjectSection(SolutionItems) = preProject
.editorconfig = .editorconfig
.gitignore = .gitignore
CHANGELOG.md = CHANGELOG.md
README.en.md = README.en.md
README.md = README.md
EndProjectSection
EndProject
@@ -26,6 +28,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SpineRuntime41", "SpineRunt
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SpineRuntime42", "SpineRuntimes\SpineRuntime42\SpineRuntime42.csproj", "{1D96AAF6-AB7B-8050-4C7E-03431778628F}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SpineRuntime21", "SpineRuntimes\SpineRuntime21\SpineRuntime21.csproj", "{628CA98E-1D21-2282-C01E-0470CAF211E1}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|x64 = Debug|x64
@@ -60,6 +64,10 @@ Global
{1D96AAF6-AB7B-8050-4C7E-03431778628F}.Debug|x64.Build.0 = Debug|x64
{1D96AAF6-AB7B-8050-4C7E-03431778628F}.Release|x64.ActiveCfg = Release|x64
{1D96AAF6-AB7B-8050-4C7E-03431778628F}.Release|x64.Build.0 = Release|x64
{628CA98E-1D21-2282-C01E-0470CAF211E1}.Debug|x64.ActiveCfg = Debug|x64
{628CA98E-1D21-2282-C01E-0470CAF211E1}.Debug|x64.Build.0 = Debug|x64
{628CA98E-1D21-2282-C01E-0470CAF211E1}.Release|x64.ActiveCfg = Release|x64
{628CA98E-1D21-2282-C01E-0470CAF211E1}.Release|x64.Build.0 = Release|x64
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@@ -71,6 +79,7 @@ Global
{2E19353C-9C0B-85F7-4EF4-98A778A79059} = {EA2E1399-02BC-43BC-AD9F-42E23E9C3DA8}
{C7B93D57-A896-38B2-1D43-25B28502F756} = {EA2E1399-02BC-43BC-AD9F-42E23E9C3DA8}
{1D96AAF6-AB7B-8050-4C7E-03431778628F} = {EA2E1399-02BC-43BC-AD9F-42E23E9C3DA8}
{628CA98E-1D21-2282-C01E-0470CAF211E1} = {EA2E1399-02BC-43BC-AD9F-42E23E9C3DA8}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {91F0EFD1-4B07-4C3C-82D8-90432349D3A5}

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,25 +40,47 @@ namespace SpineViewer.Controls
}
/// <summary>
/// 弹出添加对话框在指定位置之前插入一项
/// 选中的索引
/// </summary>
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();
@@ -63,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>
@@ -86,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;
@@ -118,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)
@@ -141,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;
@@ -157,20 +229,23 @@ namespace SpineViewer.Controls
else
PropertyGrid.SelectedObjects = listView.SelectedIndices.Cast<int>().Select(index => spines[index]).ToArray();
}
}
}
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)
@@ -178,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)
@@ -271,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);
}
}
@@ -296,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();
}
}
@@ -310,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);
}
}
@@ -321,18 +476,73 @@ namespace SpineViewer.Controls
if (listView.Items.Count <= 0)
return;
if (MessageBox.Show($"确认移除所有 {listView.Items.Count} 项吗?", "操作确认", MessageBoxButtons.OKCancel, MessageBoxIcon.Question) != DialogResult.OK)
if (MessageBox.Quest($"确认移除所有 {listView.Items.Count} 项吗?") != DialogResult.OK)
return;
lock (Spines)
{
foreach (var spine in spines)
spine.Dispose();
spines.Clear();
}
listView.Items.Clear();
lock (Spines)
{
foreach (var spine in spines) spine.Dispose();
spines.Clear();
listView.SmallImageList.Images.Clear();
listView.LargeImageList.Images.Clear();
}
if (PropertyGrid is not null)
PropertyGrid.SelectedObject = null;
}
private void toolStripMenuItem_CopyPreview_Click(object sender, EventArgs e)
{
var fileDropList = new StringCollection();
lock (Spines)
{
foreach (int i in listView.SelectedIndices)
{
var spine = spines[i];
var image = spine.Preview;
var path = Path.Combine(Program.TempDir, $"{spine.ID}.png");
using (var clone = new Bitmap(image))
clone.Save(path);
fileDropList.Add(path);
}
}
if (fileDropList.Count > 0)
Clipboard.SetFileDropList(fileDropList);
}
private void toolStripMenuItem_AddFromClipboard_Click(object sender, EventArgs e)
{
if (Clipboard.ContainsFileDropList())
{
var fileDropList = Clipboard.GetFileDropList();
var paths = new string[fileDropList.Count];
fileDropList.CopyTo(paths, 0);
AddFromFileDrop(paths);
}
}
private void toolStripMenuItem_SelectAll_Click(object sender, EventArgs e)
{
listView.BeginUpdate();
foreach (ListViewItem item in listView.Items)
item.Selected = true;
listView.EndUpdate();
}
private void toolStripMenuItem_LargeIconView_Click(object sender, EventArgs e)
{
listView.View = View.LargeIcon;
}
private void toolStripMenuItem_ListView_Click(object sender, EventArgs e)
{
listView.View = View.List;
}
private void toolStripMenuItem_DetailsView_Click(object sender, EventArgs e)
{
listView.View = View.Details;
}
}
}

View File

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

View File

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

View File

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

View File

@@ -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,46 +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|所有文件 (*.*)|*.*";
openFileDialog_Skel.Multiselect = true;
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
//
@@ -203,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");
@@ -223,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,49 +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 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)
{
if (listBox_FilePath.Items.Count <= 0)
var version = (Spine.Version)comboBox_Version.SelectedValue;
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;
}
}
SkelPaths = listBox_FilePath.Items.Cast<string>().ToArray();
Version = (Spine.Version)comboBox_Version.SelectedValue;
if (version != Spine.Version.Auto && !Spine.Spine.ImplementedVersions.Contains(version))
{
MessageBox.Info($"{version.GetName()} 版本尚未实现(咕咕咕~");
return;
}
Result = new(version, items.Cast<string>().ToArray());
DialogResult = DialogResult.OK;
}
@@ -64,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

@@ -0,0 +1,293 @@
namespace SpineViewer.Dialogs
{
partial class ConvertFileFormatDialog
{
/// <summary>
/// Required designer variable.
/// </summary>
private System.ComponentModel.IContainer components = null;
/// <summary>
/// Clean up any resources being used.
/// </summary>
/// <param name="disposing">true if managed resources should be disposed; otherwise, false.</param>
protected override void Dispose(bool disposing)
{
if (disposing && (components != null))
{
components.Dispose();
}
base.Dispose(disposing);
}
#region Windows Form Designer generated code
/// <summary>
/// Required method for Designer support - do not modify
/// the contents of this method with the code editor.
/// </summary>
private void InitializeComponent()
{
System.ComponentModel.ComponentResourceManager resources = new System.ComponentModel.ComponentResourceManager(typeof(ConvertFileFormatDialog));
panel = new Panel();
tableLayoutPanel1 = new TableLayoutPanel();
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_SourceVersion = new ComboBox();
tableLayoutPanel2 = new TableLayoutPanel();
button_Ok = new Button();
button_Cancel = new Button();
label2 = new Label();
skelFileListBox = new SpineViewer.Controls.SkelFileListBox();
openFileDialog_Skel = new OpenFileDialog();
panel.SuspendLayout();
tableLayoutPanel1.SuspendLayout();
flowLayoutPanel_TargetFormat.SuspendLayout();
tableLayoutPanel2.SuspendLayout();
SuspendLayout();
//
// panel
//
panel.Controls.Add(tableLayoutPanel1);
panel.Dock = DockStyle.Fill;
panel.Location = new Point(0, 0);
panel.Name = "panel";
panel.Padding = new Padding(50, 15, 50, 10);
panel.Size = new Size(1051, 538);
panel.TabIndex = 2;
//
// tableLayoutPanel1
//
tableLayoutPanel1.ColumnCount = 2;
tableLayoutPanel1.ColumnStyles.Add(new ColumnStyle());
tableLayoutPanel1.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 100F));
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, 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 = 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.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(95, 403);
flowLayoutPanel_TargetFormat.Name = "flowLayoutPanel_TargetFormat";
flowLayoutPanel_TargetFormat.Size = new Size(853, 34);
flowLayoutPanel_TargetFormat.TabIndex = 19;
//
// radioButton_BinaryTarget
//
radioButton_BinaryTarget.AutoSize = true;
radioButton_BinaryTarget.Location = new Point(3, 3);
radioButton_BinaryTarget.Name = "radioButton_BinaryTarget";
radioButton_BinaryTarget.Size = new Size(151, 28);
radioButton_BinaryTarget.TabIndex = 17;
radioButton_BinaryTarget.Text = "二进制 (*.skel)";
radioButton_BinaryTarget.UseVisualStyleBackColor = true;
//
// radioButton_JsonTarget
//
radioButton_JsonTarget.AutoSize = true;
radioButton_JsonTarget.Checked = true;
radioButton_JsonTarget.Location = new Point(160, 3);
radioButton_JsonTarget.Name = "radioButton_JsonTarget";
radioButton_JsonTarget.Size = new Size(135, 28);
radioButton_JsonTarget.TabIndex = 18;
radioButton_JsonTarget.TabStop = true;
radioButton_JsonTarget.Text = "文本 (*.json)";
radioButton_JsonTarget.UseVisualStyleBackColor = true;
//
// label1
//
label1.Anchor = AnchorStyles.Right;
label1.AutoSize = true;
label1.Location = new Point(3, 369);
label1.Name = "label1";
label1.Size = new Size(86, 24);
label1.TabIndex = 15;
label1.Text = "目标版本:";
//
// label4
//
label4.AutoSize = true;
tableLayoutPanel1.SetColumnSpan(label4, 4);
label4.Dock = DockStyle.Fill;
label4.Location = new Point(15, 15);
label4.Margin = new Padding(15);
label4.Name = "label4";
label4.Size = new Size(921, 24);
label4.TabIndex = 14;
label4.Text = "说明:将在每个文件同级目录下生成目标格式后缀的文件,会覆盖已存在文件";
label4.TextAlign = ContentAlignment.MiddleCenter;
//
// label3
//
label3.Anchor = AnchorStyles.Right;
label3.AutoSize = true;
label3.Location = new Point(21, 331);
label3.Name = "label3";
label3.Size = new Size(68, 24);
label3.TabIndex = 12;
label3.Text = "源版本:";
//
// comboBox_SourceVersion
//
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
//
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.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(945, 40);
tableLayoutPanel2.TabIndex = 11;
//
// button_Ok
//
button_Ok.Anchor = AnchorStyles.Bottom | AnchorStyles.Right;
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);
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(502, 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;
//
// label2
//
label2.Anchor = AnchorStyles.Right;
label2.AutoSize = true;
label2.Location = new Point(3, 408);
label2.Name = "label2";
label2.Size = new Size(86, 24);
label2.TabIndex = 16;
label2.Text = "目标格式:";
//
// skelFileListBox
//
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
//
openFileDialog_Skel.AddExtension = false;
openFileDialog_Skel.AddToRecent = false;
openFileDialog_Skel.Filter = "skel 文件 (*.skel; *.json)|*.skel;*.json|二进制文件 (*.skel)|*.skel|文本文件 (*.json)|*.json|所有文件 (*.*)|*.*";
openFileDialog_Skel.Multiselect = true;
openFileDialog_Skel.Title = "批量选择skel文件";
//
// ConvertFileFormatDialog
//
AcceptButton = button_Ok;
AutoScaleDimensions = new SizeF(11F, 24F);
AutoScaleMode = AutoScaleMode.Font;
CancelButton = button_Cancel;
ClientSize = new Size(1051, 538);
Controls.Add(panel);
FormBorderStyle = FormBorderStyle.FixedDialog;
Icon = (Icon)resources.GetObject("$this.Icon");
MaximizeBox = false;
MinimizeBox = false;
Name = "ConvertFileFormatDialog";
ShowInTaskbar = false;
StartPosition = FormStartPosition.CenterScreen;
Text = "骨骼文件格式转换";
panel.ResumeLayout(false);
tableLayoutPanel1.ResumeLayout(false);
tableLayoutPanel1.PerformLayout();
flowLayoutPanel_TargetFormat.ResumeLayout(false);
flowLayoutPanel_TargetFormat.PerformLayout();
tableLayoutPanel2.ResumeLayout(false);
ResumeLayout(false);
}
#endregion
private Panel panel;
private TableLayoutPanel tableLayoutPanel1;
private Label label4;
private Label label3;
private ComboBox comboBox_SourceVersion;
private TableLayoutPanel tableLayoutPanel2;
private Button button_Ok;
private Button button_Cancel;
private OpenFileDialog openFileDialog_Skel;
private Label label1;
private Label label2;
private FlowLayoutPanel flowLayoutPanel_TargetFormat;
private RadioButton radioButton_BinaryTarget;
private RadioButton radioButton_JsonTarget;
private Controls.SkelFileListBox skelFileListBox;
private ComboBox comboBox_TargetVersion;
}
}

View File

@@ -0,0 +1,109 @@
using SpineViewer.Spine;
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms;
namespace SpineViewer.Dialogs
{
public partial class ConvertFileFormatDialog : Form
{
/// <summary>
/// 对话框结果, 取消时为 null
/// </summary>
public ConvertFileFormatDialogResult Result { get; private set; }
public ConvertFileFormatDialog()
{
InitializeComponent();
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 sourceVersion = (Spine.Version)comboBox_SourceVersion.SelectedValue;
var targetVersion = (Spine.Version)comboBox_TargetVersion.SelectedValue;
var jsonTarget = radioButton_JsonTarget.Checked;
var items = skelFileListBox.Items;
if (items.Count <= 0)
{
MessageBox.Info("未选择任何文件");
return;
}
foreach (string p in items)
{
if (!File.Exists(p))
{
MessageBox.Info($"{p}", "skel文件不存在");
return;
}
}
if (sourceVersion != Spine.Version.Auto && !SkeletonConverter.ImplementedVersions.Contains(sourceVersion))
{
MessageBox.Info($"{sourceVersion.GetName()} 版本尚未实现(咕咕咕~");
return;
}
if (!SkeletonConverter.ImplementedVersions.Contains(targetVersion))
{
MessageBox.Info($"{targetVersion.GetName()} 版本尚未实现(咕咕咕~");
return;
}
Result = new(items.Cast<string>().ToArray(), sourceVersion, targetVersion, jsonTarget);
DialogResult = DialogResult.OK;
}
private void button_Cancel_Click(object sender, EventArgs e)
{
DialogResult = DialogResult.Cancel;
}
}
/// <summary>
/// 文件格式转换对话框结果包装类
/// </summary>
public class ConvertFileFormatDialogResult(string[] skelPaths, Spine.Version sourceVersion, Spine.Version targetVersion, bool jsonTarget)
{
/// <summary>
/// 骨骼文件路径列表
/// </summary>
public string[] SkelPaths => skelPaths;
/// <summary>
/// 源版本
/// </summary>
public Spine.Version SourceVersion => sourceVersion;
/// <summary>
/// 目标版本
/// </summary>
public Spine.Version TargetVersion => targetVersion;
/// <summary>
/// 目标格式是否为 Json
/// </summary>
public bool JsonTarget => jsonTarget;
}
}

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,223 @@
namespace SpineViewer.Dialogs
{
partial class ExportPreviewDialog
{
/// <summary>
/// Required designer variable.
/// </summary>
private System.ComponentModel.IContainer components = null;
/// <summary>
/// Clean up any resources being used.
/// </summary>
/// <param name="disposing">true if managed resources should be disposed; otherwise, false.</param>
protected override void Dispose(bool disposing)
{
if (disposing && (components != null))
{
components.Dispose();
}
base.Dispose(disposing);
}
#region Windows Form Designer generated code
/// <summary>
/// Required method for Designer support - do not modify
/// the contents of this method with the code editor.
/// </summary>
private void InitializeComponent()
{
System.ComponentModel.ComponentResourceManager resources = new System.ComponentModel.ComponentResourceManager(typeof(ExportPreviewDialog));
panel1 = new Panel();
tableLayoutPanel1 = new TableLayoutPanel();
propertyGrid = new PropertyGrid();
label4 = new Label();
label1 = new Label();
textBox_OutputDir = new TextBox();
button_SelectOutputDir = new Button();
tableLayoutPanel2 = new TableLayoutPanel();
button_Ok = new Button();
button_Cancel = new Button();
folderBrowserDialog = new FolderBrowserDialog();
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(914, 482);
panel1.TabIndex = 2;
//
// tableLayoutPanel1
//
tableLayoutPanel1.AutoSize = true;
tableLayoutPanel1.ColumnCount = 4;
tableLayoutPanel1.ColumnStyles.Add(new ColumnStyle());
tableLayoutPanel1.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 50F));
tableLayoutPanel1.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 50F));
tableLayoutPanel1.ColumnStyles.Add(new ColumnStyle());
tableLayoutPanel1.Controls.Add(propertyGrid, 0, 2);
tableLayoutPanel1.Controls.Add(label4, 0, 0);
tableLayoutPanel1.Controls.Add(label1, 0, 1);
tableLayoutPanel1.Controls.Add(textBox_OutputDir, 1, 1);
tableLayoutPanel1.Controls.Add(button_SelectOutputDir, 3, 1);
tableLayoutPanel1.Controls.Add(tableLayoutPanel2, 0, 3);
tableLayoutPanel1.Dock = DockStyle.Fill;
tableLayoutPanel1.Location = new Point(50, 15);
tableLayoutPanel1.Name = "tableLayoutPanel1";
tableLayoutPanel1.RowCount = 4;
tableLayoutPanel1.RowStyles.Add(new RowStyle());
tableLayoutPanel1.RowStyles.Add(new RowStyle());
tableLayoutPanel1.RowStyles.Add(new RowStyle(SizeType.Percent, 100F));
tableLayoutPanel1.RowStyles.Add(new RowStyle());
tableLayoutPanel1.Size = new Size(814, 457);
tableLayoutPanel1.TabIndex = 0;
//
// propertyGrid
//
tableLayoutPanel1.SetColumnSpan(propertyGrid, 4);
propertyGrid.Dock = DockStyle.Fill;
propertyGrid.HelpVisible = false;
propertyGrid.Location = new Point(3, 97);
propertyGrid.Name = "propertyGrid";
propertyGrid.Size = new Size(808, 284);
propertyGrid.TabIndex = 1;
propertyGrid.ToolbarVisible = false;
//
// 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(784, 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 = "输出文件夹:";
//
// 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(660, 30);
textBox_OutputDir.TabIndex = 3;
//
// button_SelectOutputDir
//
button_SelectOutputDir.AutoSize = true;
button_SelectOutputDir.AutoSizeMode = AutoSizeMode.GrowAndShrink;
button_SelectOutputDir.Location = new Point(779, 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, 414);
tableLayoutPanel2.Margin = new Padding(3, 30, 3, 3);
tableLayoutPanel2.Name = "tableLayoutPanel2";
tableLayoutPanel2.RowCount = 1;
tableLayoutPanel2.RowStyles.Add(new RowStyle());
tableLayoutPanel2.Size = new Size(808, 40);
tableLayoutPanel2.TabIndex = 10;
//
// button_Ok
//
button_Ok.Anchor = AnchorStyles.Bottom | AnchorStyles.Right;
button_Ok.Location = new Point(262, 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(434, 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;
//
// folderBrowserDialog
//
folderBrowserDialog.AddToRecent = false;
//
// ExportPreviewDialog
//
AcceptButton = button_Ok;
AutoScaleDimensions = new SizeF(11F, 24F);
AutoScaleMode = AutoScaleMode.Font;
CancelButton = button_Cancel;
ClientSize = new Size(914, 482);
Controls.Add(panel1);
FormBorderStyle = FormBorderStyle.FixedDialog;
Icon = (Icon)resources.GetObject("$this.Icon");
MaximizeBox = false;
MinimizeBox = false;
Name = "ExportPreviewDialog";
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 Label label4;
private Label label1;
private TextBox textBox_OutputDir;
private Button button_SelectOutputDir;
private TableLayoutPanel tableLayoutPanel2;
private Button button_Ok;
private Button button_Cancel;
private FolderBrowserDialog folderBrowserDialog;
private PropertyGrid propertyGrid;
}
}

View File

@@ -0,0 +1,156 @@
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Drawing.Imaging;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms;
namespace SpineViewer.Dialogs
{
public partial class ExportPreviewDialog: Form
{
/// <summary>
/// 对话框结果
/// </summary>
public readonly ExportPreviewDialogResult Result = new();
public ExportPreviewDialog()
{
InitializeComponent();
propertyGrid.SelectedObject = Result;
}
private void button_SelectOutputDir_Click(object sender, EventArgs e)
{
folderBrowserDialog.InitialDirectory = textBox_OutputDir.Text;
if (folderBrowserDialog.ShowDialog() != DialogResult.OK)
return;
textBox_OutputDir.Text = Path.GetFullPath(folderBrowserDialog.SelectedPath);
}
private void button_Ok_Click(object sender, EventArgs e)
{
var outputDir = textBox_OutputDir.Text;
if (string.IsNullOrEmpty(outputDir))
{
Result.OutputDir = null;
}
else
{
if (File.Exists(outputDir))
{
MessageBox.Info("输出文件夹无效");
return;
}
if (!Directory.Exists(outputDir))
{
if (MessageBox.Quest($"文件夹 {outputDir} 不存在,是否创建?") != DialogResult.OK)
return;
try
{
Directory.CreateDirectory(outputDir);
}
catch (Exception ex)
{
Program.Logger.Error(ex.ToString());
MessageBox.Error(ex.ToString(), "文件夹创建失败");
return;
}
}
Result.OutputDir = Path.GetFullPath(outputDir);
}
DialogResult = DialogResult.OK;
}
private void button_Cancel_Click(object sender, EventArgs e)
{
DialogResult = DialogResult.Cancel;
}
}
public class ExportPreviewDialogResult
{
/// <summary>
/// 输出路径
/// </summary>
[Browsable(false)]
public string? OutputDir { get; set; } = null;
/// <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>
[TypeConverter(typeof(SizeConverter))]
[Category("导出参数"), DisplayName("分辨率")]
public Size Resolution
{
get => resolution;
set
{
if (value.Width <= 0) value.Width = 128;
if (value.Height <= 0) value.Height = 128;
resolution = value;
}
}
private Size resolution = new(512, 512);
/// <summary>
/// 四周填充像素值
/// </summary>
[TypeConverter(typeof(PaddingConverter))]
[Category("导出参数"), DisplayName("四周填充像素值")]
public Padding Padding
{
get => padding;
set
{
if (value.Left <= 0) value.Left = 10;
if (value.Right <= 0) value.Right = 10;
if (value.Top <= 0) value.Top = 10;
if (value.Bottom <= 0) value.Bottom = 10;
padding = value;
}
}
private Padding padding = new(1);
/// <summary>
/// DPI
/// </summary>
[TypeConverter(typeof(SizeFConverter))]
[Category("导出参数"), DisplayName("DPI")]
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);
}
}

File diff suppressed because it is too large Load Diff

View File

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

View File

@@ -8,22 +8,27 @@ using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms;
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)
{
button_SelectSkel_Click(sender, e);
}
private void button_SelectSkel_Click(object sender, EventArgs e)
@@ -48,10 +53,11 @@ namespace SpineViewer.Dialogs
{
var skelPath = textBox_SkelPath.Text;
var atlasPath = textBox_AtlasPath.Text;
var version = (Spine.Version)comboBox_Version.SelectedValue;
if (!File.Exists(skelPath))
{
MessageBox.Show($"{skelPath}", "skel文件不存在", MessageBoxButtons.OK, MessageBoxIcon.Information);
MessageBox.Info($"{skelPath}", "skel文件不存在");
return;
}
else
@@ -65,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
@@ -73,10 +79,13 @@ namespace SpineViewer.Dialogs
atlasPath = Path.GetFullPath(atlasPath);
}
SkelPath = skelPath;
AtlasPath = atlasPath;
Version = (Spine.Version)comboBox_Version.SelectedValue;
if (version != Spine.Version.Auto && !Spine.Spine.ImplementedVersions.Contains(version))
{
MessageBox.Info($"{version.GetName()} 版本尚未实现(咕咕咕~");
return;
}
Result = new(version, skelPath, atlasPath);
DialogResult = DialogResult.OK;
}
@@ -85,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

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

View File

@@ -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,64 @@
using FFMpegCore.Pipes;
using System;
using System.Collections.Generic;
using System.Drawing.Imaging;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using static System.Windows.Forms.VisualStyles.VisualStyleElement.StartPanel;
namespace SpineViewer
{
/// <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>
/// 为帧导出创建的辅助类
/// </summary>
public static class ExportHelper
{
public static Bitmap CopyToBitmap(this SFML.Graphics.Texture tex)
{
using var img = tex.CopyToImage();
img.SaveToMemory(out var imgBuffer, "bmp");
using var stream = new MemoryStream(imgBuffer);
return new Bitmap(stream);
}
public static SFMLImageVideoFrame CopyToFrame(this SFML.Graphics.Texture tex) => new(tex.CopyToImage());
public static string GetSuffix(this ImageFormat imageFormat)
{
if (imageFormat == ImageFormat.Icon) return ".ico";
else if (imageFormat == ImageFormat.Exif) return ".jpg";
else return $".{imageFormat.ToString().ToLower()}";
}
}
}

View File

@@ -36,10 +36,15 @@
toolStripMenuItem_BatchOpen = new ToolStripMenuItem();
toolStripSeparator1 = new ToolStripSeparator();
toolStripMenuItem_Export = new ToolStripMenuItem();
toolStripMenuItem_ExportPreview = new ToolStripMenuItem();
toolStripSeparator2 = new ToolStripSeparator();
toolStripMenuItem_Exit = new ToolStripMenuItem();
toolStripMenuItem_Function = new ToolStripMenuItem();
toolStripMenuItem_ResetAnimation = new ToolStripMenuItem();
toolStripMenuItem_Tool = new ToolStripMenuItem();
toolStripMenuItem_ConvertFileFormat = new ToolStripMenuItem();
toolStripMenuItem_Download = new ToolStripMenuItem();
toolStripMenuItem_ManageResource = new ToolStripMenuItem();
toolStripMenuItem_Help = new ToolStripMenuItem();
toolStripMenuItem_Diagnostics = new ToolStripMenuItem();
toolStripSeparator3 = new ToolStripSeparator();
@@ -87,16 +92,16 @@
//
menuStrip.BackColor = SystemColors.Control;
menuStrip.ImageScalingSize = new Size(24, 24);
menuStrip.Items.AddRange(new ToolStripItem[] { toolStripMenuItem_File, toolStripMenuItem_Function, toolStripMenuItem_Help });
menuStrip.Items.AddRange(new ToolStripItem[] { toolStripMenuItem_File, toolStripMenuItem_Function, toolStripMenuItem_Tool, toolStripMenuItem_Download, toolStripMenuItem_Help });
menuStrip.Location = new Point(0, 0);
menuStrip.Name = "menuStrip";
menuStrip.Size = new Size(1741, 32);
menuStrip.Size = new Size(1748, 32);
menuStrip.TabIndex = 0;
menuStrip.Text = "菜单";
//
// toolStripMenuItem_File
//
toolStripMenuItem_File.DropDownItems.AddRange(new ToolStripItem[] { toolStripMenuItem_Open, toolStripMenuItem_BatchOpen, toolStripSeparator1, toolStripMenuItem_Export, toolStripSeparator2, toolStripMenuItem_Exit });
toolStripMenuItem_File.DropDownItems.AddRange(new ToolStripItem[] { toolStripMenuItem_Open, toolStripMenuItem_BatchOpen, toolStripSeparator1, toolStripMenuItem_Export, toolStripMenuItem_ExportPreview, toolStripSeparator2, toolStripMenuItem_Exit });
toolStripMenuItem_File.Name = "toolStripMenuItem_File";
toolStripMenuItem_File.Size = new Size(84, 28);
toolStripMenuItem_File.Text = "文件(&F)";
@@ -129,6 +134,13 @@
toolStripMenuItem_Export.Text = "导出(&E)...";
toolStripMenuItem_Export.Click += toolStripMenuItem_Export_Click;
//
// toolStripMenuItem_ExportPreview
//
toolStripMenuItem_ExportPreview.Name = "toolStripMenuItem_ExportPreview";
toolStripMenuItem_ExportPreview.Size = new Size(254, 34);
toolStripMenuItem_ExportPreview.Text = "导出预览图(&P)...";
toolStripMenuItem_ExportPreview.Click += toolStripMenuItem_ExportPreview_Click;
//
// toolStripSeparator2
//
toolStripSeparator2.Name = "toolStripSeparator2";
@@ -146,8 +158,8 @@
//
toolStripMenuItem_Function.DropDownItems.AddRange(new ToolStripItem[] { toolStripMenuItem_ResetAnimation });
toolStripMenuItem_Function.Name = "toolStripMenuItem_Function";
toolStripMenuItem_Function.Size = new Size(84, 28);
toolStripMenuItem_Function.Text = "功能(&F)";
toolStripMenuItem_Function.Size = new Size(87, 28);
toolStripMenuItem_Function.Text = "功能(&G)";
//
// toolStripMenuItem_ResetAnimation
//
@@ -156,6 +168,34 @@
toolStripMenuItem_ResetAnimation.Text = "重置动画时间(&R)";
toolStripMenuItem_ResetAnimation.Click += toolStripMenuItem_ResetAnimation_Click;
//
// toolStripMenuItem_Tool
//
toolStripMenuItem_Tool.DropDownItems.AddRange(new ToolStripItem[] { toolStripMenuItem_ConvertFileFormat });
toolStripMenuItem_Tool.Name = "toolStripMenuItem_Tool";
toolStripMenuItem_Tool.Size = new Size(84, 28);
toolStripMenuItem_Tool.Text = "工具(&T)";
//
// toolStripMenuItem_ConvertFileFormat
//
toolStripMenuItem_ConvertFileFormat.Name = "toolStripMenuItem_ConvertFileFormat";
toolStripMenuItem_ConvertFileFormat.Size = new Size(254, 34);
toolStripMenuItem_ConvertFileFormat.Text = "转换文件格式(&C)...";
toolStripMenuItem_ConvertFileFormat.Click += toolStripMenuItem_ConvertFileFormat_Click;
//
// toolStripMenuItem_Download
//
toolStripMenuItem_Download.DropDownItems.AddRange(new ToolStripItem[] { toolStripMenuItem_ManageResource });
toolStripMenuItem_Download.Name = "toolStripMenuItem_Download";
toolStripMenuItem_Download.Size = new Size(88, 28);
toolStripMenuItem_Download.Text = "下载(&D)";
//
// toolStripMenuItem_ManageResource
//
toolStripMenuItem_ManageResource.Name = "toolStripMenuItem_ManageResource";
toolStripMenuItem_ManageResource.Size = new Size(260, 34);
toolStripMenuItem_ManageResource.Text = "管理下载资源(&M)...";
toolStripMenuItem_ManageResource.Click += toolStripMenuItem_ManageResource_Click;
//
// toolStripMenuItem_Help
//
toolStripMenuItem_Help.DropDownItems.AddRange(new ToolStripItem[] { toolStripMenuItem_Diagnostics, toolStripSeparator3, toolStripMenuItem_About });
@@ -192,7 +232,7 @@
rtbLog.Margin = new Padding(3, 2, 3, 2);
rtbLog.Name = "rtbLog";
rtbLog.ReadOnly = true;
rtbLog.Size = new Size(1721, 106);
rtbLog.Size = new Size(1728, 114);
rtbLog.TabIndex = 0;
rtbLog.Text = "";
rtbLog.WordWrap = false;
@@ -214,8 +254,8 @@
//
splitContainer_MainForm.Panel2.Controls.Add(rtbLog);
splitContainer_MainForm.Panel2.Cursor = Cursors.Default;
splitContainer_MainForm.Size = new Size(1721, 958);
splitContainer_MainForm.SplitterDistance = 848;
splitContainer_MainForm.Size = new Size(1728, 997);
splitContainer_MainForm.SplitterDistance = 879;
splitContainer_MainForm.TabIndex = 3;
splitContainer_MainForm.TabStop = false;
splitContainer_MainForm.SplitterMoved += splitContainer_SplitterMoved;
@@ -237,8 +277,8 @@
//
splitContainer_Functional.Panel2.Controls.Add(groupBox_Preview);
splitContainer_Functional.Panel2.Cursor = Cursors.Default;
splitContainer_Functional.Size = new Size(1721, 848);
splitContainer_Functional.SplitterDistance = 744;
splitContainer_Functional.Size = new Size(1728, 879);
splitContainer_Functional.SplitterDistance = 747;
splitContainer_Functional.TabIndex = 2;
splitContainer_Functional.TabStop = false;
splitContainer_Functional.SplitterMoved += splitContainer_SplitterMoved;
@@ -260,8 +300,8 @@
//
splitContainer_Information.Panel2.Controls.Add(splitContainer_Config);
splitContainer_Information.Panel2.Cursor = Cursors.Default;
splitContainer_Information.Size = new Size(744, 848);
splitContainer_Information.SplitterDistance = 327;
splitContainer_Information.Size = new Size(747, 879);
splitContainer_Information.SplitterDistance = 399;
splitContainer_Information.TabIndex = 1;
splitContainer_Information.TabStop = false;
splitContainer_Information.SplitterMoved += splitContainer_SplitterMoved;
@@ -273,7 +313,7 @@
groupBox_SkelList.Dock = DockStyle.Fill;
groupBox_SkelList.Location = new Point(0, 0);
groupBox_SkelList.Name = "groupBox_SkelList";
groupBox_SkelList.Size = new Size(327, 848);
groupBox_SkelList.Size = new Size(399, 879);
groupBox_SkelList.TabIndex = 0;
groupBox_SkelList.TabStop = false;
groupBox_SkelList.Text = "模型列表";
@@ -284,7 +324,7 @@
spineListView.Location = new Point(3, 26);
spineListView.Name = "spineListView";
spineListView.PropertyGrid = propertyGrid_Spine;
spineListView.Size = new Size(321, 819);
spineListView.Size = new Size(393, 850);
spineListView.TabIndex = 0;
//
// propertyGrid_Spine
@@ -293,7 +333,7 @@
propertyGrid_Spine.HelpVisible = false;
propertyGrid_Spine.Location = new Point(3, 26);
propertyGrid_Spine.Name = "propertyGrid_Spine";
propertyGrid_Spine.Size = new Size(407, 470);
propertyGrid_Spine.Size = new Size(338, 485);
propertyGrid_Spine.TabIndex = 0;
propertyGrid_Spine.ToolbarVisible = false;
propertyGrid_Spine.PropertyValueChanged += propertyGrid_PropertyValueChanged;
@@ -315,8 +355,8 @@
//
splitContainer_Config.Panel2.Controls.Add(groupBox_PreviewConfig);
splitContainer_Config.Panel2.Cursor = Cursors.Default;
splitContainer_Config.Size = new Size(413, 848);
splitContainer_Config.SplitterDistance = 499;
splitContainer_Config.Size = new Size(344, 879);
splitContainer_Config.SplitterDistance = 514;
splitContainer_Config.TabIndex = 0;
splitContainer_Config.TabStop = false;
splitContainer_Config.SplitterMoved += splitContainer_SplitterMoved;
@@ -328,7 +368,7 @@
groupBox_SkelConfig.Dock = DockStyle.Fill;
groupBox_SkelConfig.Location = new Point(0, 0);
groupBox_SkelConfig.Name = "groupBox_SkelConfig";
groupBox_SkelConfig.Size = new Size(413, 499);
groupBox_SkelConfig.Size = new Size(344, 514);
groupBox_SkelConfig.TabIndex = 0;
groupBox_SkelConfig.TabStop = false;
groupBox_SkelConfig.Text = "模型参数";
@@ -339,7 +379,7 @@
groupBox_PreviewConfig.Dock = DockStyle.Fill;
groupBox_PreviewConfig.Location = new Point(0, 0);
groupBox_PreviewConfig.Name = "groupBox_PreviewConfig";
groupBox_PreviewConfig.Size = new Size(413, 345);
groupBox_PreviewConfig.Size = new Size(344, 361);
groupBox_PreviewConfig.TabIndex = 1;
groupBox_PreviewConfig.TabStop = false;
groupBox_PreviewConfig.Text = "画面参数";
@@ -350,7 +390,7 @@
propertyGrid_Previewer.HelpVisible = false;
propertyGrid_Previewer.Location = new Point(3, 26);
propertyGrid_Previewer.Name = "propertyGrid_Previewer";
propertyGrid_Previewer.Size = new Size(407, 316);
propertyGrid_Previewer.Size = new Size(338, 332);
propertyGrid_Previewer.TabIndex = 1;
propertyGrid_Previewer.ToolbarVisible = false;
propertyGrid_Previewer.PropertyValueChanged += propertyGrid_PropertyValueChanged;
@@ -361,7 +401,7 @@
groupBox_Preview.Dock = DockStyle.Fill;
groupBox_Preview.Location = new Point(0, 0);
groupBox_Preview.Name = "groupBox_Preview";
groupBox_Preview.Size = new Size(973, 848);
groupBox_Preview.Size = new Size(977, 879);
groupBox_Preview.TabIndex = 1;
groupBox_Preview.TabStop = false;
groupBox_Preview.Text = "预览画面";
@@ -373,7 +413,7 @@
spinePreviewer.Location = new Point(3, 26);
spinePreviewer.Name = "spinePreviewer";
spinePreviewer.PropertyGrid = propertyGrid_Previewer;
spinePreviewer.Size = new Size(967, 819);
spinePreviewer.Size = new Size(971, 850);
spinePreviewer.SpineListView = spineListView;
spinePreviewer.TabIndex = 0;
spinePreviewer.MouseUp += spinePreviewer_MouseUp;
@@ -385,7 +425,7 @@
panel_MainForm.Location = new Point(0, 32);
panel_MainForm.Name = "panel_MainForm";
panel_MainForm.Padding = new Padding(10, 5, 10, 10);
panel_MainForm.Size = new Size(1741, 973);
panel_MainForm.Size = new Size(1748, 1012);
panel_MainForm.TabIndex = 4;
//
// toolTip
@@ -396,7 +436,7 @@
//
AutoScaleDimensions = new SizeF(11F, 24F);
AutoScaleMode = AutoScaleMode.Font;
ClientSize = new Size(1741, 1005);
ClientSize = new Size(1748, 1044);
Controls.Add(panel_MainForm);
Controls.Add(menuStrip);
Icon = (Icon)resources.GetObject("$this.Icon");
@@ -465,5 +505,10 @@
private ToolStripMenuItem toolStripMenuItem_ResetAnimation;
private ToolStripMenuItem toolStripMenuItem_Diagnostics;
private ToolStripSeparator toolStripSeparator3;
private ToolStripMenuItem toolStripMenuItem_Download;
private ToolStripMenuItem toolStripMenuItem_ManageResource;
private ToolStripMenuItem toolStripMenuItem_Tool;
private ToolStripMenuItem toolStripMenuItem_ConvertFileFormat;
private ToolStripMenuItem toolStripMenuItem_ExportPreview;
}
}

View File

@@ -1,7 +1,13 @@
using FFMpegCore.Pipes;
using FFMpegCore;
using NLog;
using SFML.System;
using SpineViewer.Spine;
using System.ComponentModel;
using System.Diagnostics;
using System.Text.Json;
using System.Text.Json.Nodes;
using FFMpegCore.Enums;
namespace SpineViewer
{
@@ -14,11 +20,11 @@ namespace SpineViewer
}
/// <summary>
/// <EFBFBD><EFBFBD>ʼ<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>־<EFBFBD><EFBFBD>
/// 初始化窗口日志器
/// </summary>
private void InitializeLogConfiguration()
{
// <EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>־
// 窗口日志
var rtbTarget = new NLog.Windows.Forms.RichTextBoxTarget
{
Name = "rtbTarget",
@@ -41,68 +47,6 @@ namespace SpineViewer
LogManager.ReconfigExistingLoggers();
}
private void ExportPng_Work(object? sender, DoWorkEventArgs e)
{
var worker = sender as BackgroundWorker;
var arguments = e.Argument as Dialogs.ExportPngDialog;
var outputDir = arguments.OutputDir;
var duration = arguments.Duration;
var fps = arguments.Fps;
var timestamp = DateTime.Now.ToString("yyMMddHHmmss");
var resolution = spinePreviewer.Resolution;
var tex = new SFML.Graphics.RenderTexture((uint)resolution.Width, (uint)resolution.Height);
tex.SetView(spinePreviewer.View);
var delta = 1f / fps;
var frameCount = 1 + (int)(duration / delta); // <20><>֡<EFBFBD><D6A1>ʼ<EFBFBD><CABC><EFBFBD><EFBFBD>
spinePreviewer.StopPreview();
lock (spineListView.Spines)
{
var spinesReverse = spineListView.Spines.Reverse();
// <20><><EFBFBD>ö<EFBFBD><C3B6><EFBFBD>ʱ<EFBFBD><CAB1>
foreach (var spine in spinesReverse)
spine.CurrentAnimation = spine.CurrentAnimation;
Program.Logger.Info(
"Begin exporting png frames to output dir {}, duration: {}, fps: {}, totally {} spines",
[outputDir, duration, fps, spinesReverse.Count()]
);
// <20><>֡<EFBFBD><D6A1><EFBFBD><EFBFBD>
var success = 0;
worker.ReportProgress(0, $"<22>Ѵ<EFBFBD><D1B4><EFBFBD> 0/{frameCount}");
for (int frameIndex = 0; frameIndex < frameCount; frameIndex++)
{
if (worker.CancellationPending)
break;
tex.Clear(SFML.Graphics.Color.Transparent);
foreach (var spine in spinesReverse)
{
tex.Draw(spine);
spine.Update(delta);
}
tex.Display();
using (var img = tex.Texture.CopyToImage())
{
img.SaveToFile(Path.Combine(outputDir, $"{timestamp}_{fps}_{frameIndex:d6}.png"));
}
success++;
worker.ReportProgress((int)((frameIndex + 1) * 100.0) / frameCount, $"<22>Ѵ<EFBFBD><D1B4><EFBFBD> {frameIndex + 1}/{frameCount}");
}
Program.Logger.Info("Exporting done: {}/{}", success, frameCount);
}
spinePreviewer.StartPreview();
}
private void MainForm_Load(object sender, EventArgs e)
{
spinePreviewer.StartPreview();
@@ -125,11 +69,12 @@ namespace SpineViewer
private void toolStripMenuItem_Export_Click(object sender, EventArgs e)
{
// TODO: 改成统一导出调用
lock (spineListView.Spines)
{
if (spineListView.Spines.Count <= 0)
{
MessageBox.Show("<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ٴ<EFBFBD><EFBFBD><EFBFBD>һ<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ļ<EFBFBD>", "<22><>ʾ<EFBFBD><CABE>Ϣ", MessageBoxButtons.OK, MessageBoxIcon.Information);
MessageBox.Info("请至少打开一个骨骼文件");
return;
}
}
@@ -144,6 +89,27 @@ namespace SpineViewer
progressDialog.ShowDialog();
}
private void toolStripMenuItem_ExportPreview_Click(object sender, EventArgs e)
{
lock (spineListView.Spines)
{
if (spineListView.Spines.Count <= 0)
{
MessageBox.Info("请至少打开一个骨骼文件");
return;
}
}
var saveDialog = new Dialogs.ExportPreviewDialog();
if (saveDialog.ShowDialog() != DialogResult.OK)
return;
var progressDialog = new Dialogs.ProgressDialog();
progressDialog.DoWork += ExportPreview_Work;
progressDialog.RunWorkerAsync(saveDialog.Result);
progressDialog.ShowDialog();
}
private void toolStripMenuItem_Exit_Click(object sender, EventArgs e)
{
Close();
@@ -158,6 +124,84 @@ namespace SpineViewer
}
}
private void toolStripMenuItem_ConvertFileFormat_Click(object sender, EventArgs e)
{
var openDialog = new Dialogs.ConvertFileFormatDialog();
if (openDialog.ShowDialog() != DialogResult.OK)
return;
var progressDialog = new Dialogs.ProgressDialog();
progressDialog.DoWork += ConvertFileFormat_Work;
progressDialog.RunWorkerAsync(openDialog.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();
@@ -168,12 +212,224 @@ namespace SpineViewer
(new Dialogs.DiagnosticsDialog()).ShowDialog();
}
private void splitContainer_SplitterMoved(object sender, SplitterEventArgs e) { ActiveControl = null; }
private void splitContainer_SplitterMoved(object sender, SplitterEventArgs e) => ActiveControl = null;
private void splitContainer_MouseUp(object sender, MouseEventArgs 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 propertyGrid_PropertyValueChanged(object sender, PropertyValueChangedEventArgs e) => (sender as PropertyGrid)?.Refresh();
private void spinePreviewer_MouseUp(object sender, MouseEventArgs e) { propertyGrid_Spine.Refresh(); }
private void spinePreviewer_MouseUp(object sender, MouseEventArgs e)
{
propertyGrid_Spine.Refresh();
}
private void ExportPng_Work(object? sender, DoWorkEventArgs e)
{
var worker = sender as BackgroundWorker;
var arguments = e.Argument as Dialogs.ExportPngDialog;
var outputDir = arguments.OutputDir;
var duration = arguments.Duration;
var fps = arguments.Fps;
var timestamp = DateTime.Now.ToString("yyMMddHHmmss");
var frameArgs = spinePreviewer.GetFrameArgs();
var renderSelectedOnly = spinePreviewer.RenderSelectedOnly;
var resolution = frameArgs.Resolution;
var tex = new SFML.Graphics.RenderTexture((uint)resolution.Width, (uint)resolution.Height);
tex.SetView(frameArgs.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)
{
if (renderSelectedOnly && !spine.IsSelected)
continue;
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 ExportPreview_Work(object? sender, DoWorkEventArgs e)
{
var worker = sender as BackgroundWorker;
var arguments = e.Argument as Dialogs.ExportPreviewDialogResult;
var outputDir = arguments.OutputDir;
var imageFormat = arguments.ImageFormat;
var resolution = arguments.Resolution;
var padding = arguments.Padding;
var dpi = arguments.DPI;
var renderSelectedOnly = spinePreviewer.RenderSelectedOnly;
var tex = new SFML.Graphics.RenderTexture((uint)resolution.Width, (uint)resolution.Height);
int success = 0;
int error = 0;
spinePreviewer.StopPreview();
lock (spineListView.Spines)
{
var spines = spineListView.Spines;
int totalCount = spines.Count;
worker.ReportProgress(0, $"已处理 0/{totalCount}");
for (int i = 0; i < totalCount; i++)
{
if (worker.CancellationPending)
{
e.Cancel = true;
break;
}
var spine = spines[i];
if (renderSelectedOnly && !spine.IsSelected)
continue;
var filename = $"(preview) {spine.Name}{imageFormat.GetSuffix()}"; // 加上 preview 是为了防止覆盖同名的 png 文件
var savePath = outputDir is null ? Path.Combine(spine.AssetsDir, filename) : Path.Combine(outputDir, filename);
var tmp = spine.CurrentAnimation;
spine.CurrentAnimation = Spine.Spine.EMPTY_ANIMATION;
tex.SetView(spine.GetInitView(resolution, padding));
tex.Clear(SFML.Graphics.Color.Transparent);
tex.Draw(spine);
tex.Display();
spine.CurrentAnimation = tmp;
try
{
using (var img = new Bitmap(tex.Texture.CopyToBitmap()))
{
img.SetResolution(dpi.Width, dpi.Height);
img.Save(savePath, imageFormat);
}
success++;
}
catch (Exception ex)
{
Program.Logger.Error(ex.ToString());
Program.Logger.Error("Failed to save preview {}", spine.SkelPath);
error++;
}
worker.ReportProgress((int)((i + 1) * 100.0) / totalCount, $"已处理 {i + 1}/{totalCount}");
}
}
spinePreviewer.StartPreview();
if (error > 0)
{
Program.Logger.Warn("Preview save {} successfully, {} failed", success, error);
}
else
{
Program.Logger.Info("{} preview saved successfully", success);
}
Program.LogCurrentMemoryUsage();
}
private void ConvertFileFormat_Work(object? sender, DoWorkEventArgs e)
{
var worker = sender as BackgroundWorker;
var arguments = e.Argument as Dialogs.ConvertFileFormatDialogResult;
var skelPaths = arguments.SkelPaths;
var srcVersion = arguments.SourceVersion;
var tgtVersion = arguments.TargetVersion;
var jsonTarget = arguments.JsonTarget;
var newSuffix = jsonTarget ? ".json" : ".skel";
int totalCount = skelPaths.Length;
int success = 0;
int error = 0;
SkeletonConverter srcCvter = srcVersion != Spine.Version.Auto ? SkeletonConverter.New(srcVersion) : null;
SkeletonConverter tgtCvter = SkeletonConverter.New(tgtVersion);
worker.ReportProgress(0, $"已处理 0/{totalCount}");
for (int i = 0; i < totalCount; i++)
{
if (worker.CancellationPending)
{
e.Cancel = true;
break;
}
var skelPath = skelPaths[i];
var newPath = Path.ChangeExtension(skelPath, newSuffix);
try
{
if (srcVersion == 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)
{
Program.Logger.Error(ex.ToString());
Program.Logger.Error("Failed to convert {}", skelPath);
error++;
}
worker.ReportProgress((int)((i + 1) * 100.0) / totalCount, $"已处理 {i + 1}/{totalCount}");
}
if (error > 0)
{
Program.Logger.Warn("Batch convert {} successfully, {} failed", success, error);
}
else
{
Program.Logger.Info("{} skel converted successfully", success);
}
}
}
}

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

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

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

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,396 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using SpineRuntime21;
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)
{
var texture = new SFML.Graphics.Texture(path);
if (page.magFilter == TextureFilter.Linear)
texture.Smooth = true;
if (page.uWrap == TextureWrap.Repeat && page.vWrap == TextureWrap.Repeat)
texture.Repeated = true;
page.rendererObject = texture;
page.width = (int)texture.Size.X;
page.height = (int)texture.Size.Y;
}
public void Unload(object texture)
{
((SFML.Graphics.Texture)texture).Dispose();
}
}
private static TextureLoader textureLoader = new();
private Atlas atlas;
private SkeletonBinary? skeletonBinary;
private SkeletonJson? skeletonJson;
private SkeletonData skeletonData;
private AnimationStateData animationStateData;
private Skeleton skeleton;
private AnimationState animationState;
// 2.1.x 不支持剪裁
//private SkeletonClipping clipping = new();
public Spine21(string skelPath, string? atlasPath = null) : base(skelPath, atlasPath)
{
atlas = new Atlas(AtlasPath, textureLoader);
try
{
// 先尝试二进制文件
skeletonJson = null;
skeletonBinary = new SkeletonBinary(atlas);
skeletonData = skeletonBinary.ReadSkeletonData(SkelPath);
}
catch
{
try
{
// 再尝试 Json 文件
skeletonBinary = null;
skeletonJson = new SkeletonJson(atlas);
skeletonData = skeletonJson.ReadSkeletonData(SkelPath);
}
catch
{
// 都不行就报错
throw new InvalidDataException($"Unknown skeleton file format {SkelPath}");
}
}
animationStateData = new AnimationStateData(skeletonData);
skeleton = new Skeleton(skeletonData);
animationState = new AnimationState(animationStateData);
foreach (var anime in skeletonData.Animations)
animationNames.Add(anime.Name);
CurrentAnimation = DefaultAnimationName;
}
protected override void Dispose(bool disposing)
{
base.Dispose(disposing);
atlas.Dispose();
}
public override string FileVersion { get => skeletonData.Version; }
public override float Scale
{
get
{
if (skeletonBinary is not null)
return skeletonBinary.Scale;
else if (skeletonJson is not null)
return skeletonJson.Scale;
else
return 1f;
}
set
{
// 保存状态
var position = Position;
var flipX = FlipX;
var flipY = FlipY;
var savedTrack0 = animationState.GetCurrent(0);
var val = Math.Max(value, SCALE_MIN);
if (skeletonBinary is not null)
{
skeletonBinary.Scale = val;
skeletonData = skeletonBinary.ReadSkeletonData(SkelPath);
}
else if (skeletonJson is not null)
{
skeletonJson.Scale = val;
skeletonData = skeletonJson.ReadSkeletonData(SkelPath);
}
// reload skel-dependent data
animationStateData = new AnimationStateData(skeletonData) { DefaultMix = animationStateData.DefaultMix };
skeleton = new Skeleton(skeletonData);
animationState = new AnimationState(animationStateData);
// 恢复状态
Position = position;
FlipX = flipX;
FlipY = flipY;
// 恢复原本 Track0 上所有动画
if (savedTrack0 is not null)
{
var entry = animationState.SetAnimation(0, savedTrack0.Animation.Name, true);
entry.Time = savedTrack0.Time;
// 2.1.x 没有提供 Next 访问器,故放弃还原后续动画,问题不大,因为预览画面目前不需要连续播放不同动画,只需要循环同一个动画
//var savedEntry = savedTrack0.Next;
//while (savedEntry is not null)
//{
// entry = animationState.AddAnimation(0, savedEntry.Animation.Name, true, 0);
// entry.Time = savedEntry.TrackTime;
// savedEntry = savedEntry.Next;
//}
}
}
}
public override PointF Position
{
get => new(skeleton.X, skeleton.Y);
set
{
skeleton.X = value.X;
skeleton.Y = value.Y;
}
}
public override bool FlipX
{
get => skeleton.FlipX;
set => skeleton.FlipX = value;
}
public override bool FlipY
{
get => skeleton.FlipY;
set => skeleton.FlipY = value;
}
public override string CurrentAnimation
{
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
{
get
{
float[] temp = new float[8];
var drawOrderItems = skeleton.DrawOrder;
float minX = int.MaxValue, minY = int.MaxValue, maxX = int.MinValue, maxY = int.MinValue;
for (int i = 0, n = skeleton.DrawOrder.Count; i < n; i++)
{
Slot slot = drawOrderItems[i];
int verticesLength = 0;
float[] vertices = null;
Attachment attachment = slot.Attachment;
var regionAttachment = attachment as RegionAttachment;
if (regionAttachment != null)
{
verticesLength = 8;
vertices = temp;
if (vertices.Length < 8) vertices = temp = new float[8];
regionAttachment.ComputeWorldVertices(slot.Bone, temp);
}
else
{
var meshAttachment = attachment as MeshAttachment;
if (meshAttachment != null)
{
MeshAttachment mesh = meshAttachment;
verticesLength = mesh.Vertices.Length;
vertices = temp;
if (vertices.Length < verticesLength) vertices = temp = new float[verticesLength];
mesh.ComputeWorldVertices(slot, temp);
}
}
if (vertices != null)
{
for (int ii = 0; ii < verticesLength; ii += 2)
{
float vx = vertices[ii], vy = vertices[ii + 1];
minX = Math.Min(minX, vx);
minY = Math.Min(minY, vy);
maxX = Math.Max(maxX, vx);
maxY = Math.Max(maxY, vy);
}
}
}
return new RectangleF(minX, minY, maxX - minX, maxY - minY);
}
}
public override float GetAnimationDuration(string name) { return skeletonData.FindAnimation(name)?.Duration ?? 0f; }
public override void Update(float delta)
{
skeleton.Update(delta);
animationState.Update(delta);
animationState.Apply(skeleton);
skeleton.UpdateWorldTransform();
}
//private SFML.Graphics.BlendMode GetSFMLBlendMode(BlendMode spineBlendMode)
//{
// return spineBlendMode switch
// {
// BlendMode.Normal => BlendMode.Normal,
// BlendMode.Additive => BlendMode.Additive,
// BlendMode.Multiply => BlendMode.Multiply,
// BlendMode.Screen => BlendMode.Screen,
// _ => throw new NotImplementedException($"{spineBlendMode}"),
// };
//}
public override void Draw(SFML.Graphics.RenderTarget target, SFML.Graphics.RenderStates states)
{
vertexArray.Clear();
states.Texture = null;
// 要用 DrawOrder 而不是 Slots
foreach (var slot in skeleton.DrawOrder)
{
var attachment = slot.Attachment;
SFML.Graphics.Texture texture;
float[] worldVertices = worldVerticesBuffer; // 顶点世界坐标, 连续的 [x0, y0, x1, y1, ...] 坐标值
int worldVerticesCount; // 等于顶点数组的长度除以 2
int[] worldTriangleIndices; // 三角形索引, 从顶点坐标数组取的时候要乘以 2, 最大值是 worldVerticesCount - 1
int worldTriangleIndicesLength; // 三角形索引数组长度
float[] uvs; // 纹理坐标
float tintR = skeleton.R * slot.R;
float tintG = skeleton.G * slot.G;
float tintB = skeleton.B * slot.B;
float tintA = skeleton.A * slot.A;
if (attachment is RegionAttachment regionAttachment)
{
texture = (SFML.Graphics.Texture)((AtlasRegion)regionAttachment.RendererObject).page.rendererObject;
regionAttachment.ComputeWorldVertices(slot.Bone, worldVertices);
worldVerticesCount = 4;
worldTriangleIndices = [0, 1, 2, 2, 3, 0];
worldTriangleIndicesLength = 6;
uvs = regionAttachment.UVs;
tintR *= regionAttachment.R;
tintG *= regionAttachment.G;
tintB *= regionAttachment.B;
tintA *= regionAttachment.A;
}
else if (attachment is MeshAttachment meshAttachment)
{
texture = (SFML.Graphics.Texture)((AtlasRegion)meshAttachment.RendererObject).page.rendererObject;
if (meshAttachment.Vertices.Length > worldVertices.Length)
worldVertices = worldVerticesBuffer = new float[meshAttachment.Vertices.Length * 2];
meshAttachment.ComputeWorldVertices(slot, worldVertices);
worldVerticesCount = meshAttachment.Vertices.Length / 2;
worldTriangleIndices = meshAttachment.Triangles;
worldTriangleIndicesLength = meshAttachment.Triangles.Length;
uvs = meshAttachment.UVs;
tintR *= meshAttachment.R;
tintG *= meshAttachment.G;
tintB *= meshAttachment.B;
tintA *= meshAttachment.A;
}
// 2.1.x 不支持剪裁
//else if (attachment is ClippingAttachment clippingAttachment)
//{
// clipping.ClipStart(slot, clippingAttachment);
// continue;
//}
else
{
//clipping.ClipEnd(slot);
continue;
}
// 似乎 2.1.x 也没有 BlendMode
SFML.Graphics.BlendMode blendMode = slot.Data.AdditiveBlending ? BlendModeSFML.Additive : BlendModeSFML.Normal;
states.Texture ??= texture;
if (states.BlendMode != blendMode || states.Texture != texture)
{
if (vertexArray.VertexCount > 0)
{
if (UsePremultipliedAlpha && (states.BlendMode == BlendModeSFML.Normal || states.BlendMode == BlendModeSFML.Additive))
states.Shader = FragmentShader;
else
states.Shader = null;
target.Draw(vertexArray, states);
vertexArray.Clear();
}
states.BlendMode = blendMode;
states.Texture = texture;
}
//if (clipping.IsClipping)
//{
// // 这里必须单独记录 Count, 和 Items 的 Length 是不一致的
// clipping.ClipTriangles(worldVertices, worldVerticesCount * 2, worldTriangleIndices, worldTriangleIndicesLength, uvs);
// worldVertices = clipping.ClippedVertices.Items;
// worldVerticesCount = clipping.ClippedVertices.Count / 2;
// worldTriangleIndices = clipping.ClippedTriangles.Items;
// worldTriangleIndicesLength = clipping.ClippedTriangles.Count;
// uvs = clipping.ClippedUVs.Items;
//}
var textureSizeX = texture.Size.X;
var textureSizeY = texture.Size.Y;
SFML.Graphics.Vertex vertex = new();
vertex.Color.R = (byte)(tintR * 255);
vertex.Color.G = (byte)(tintG * 255);
vertex.Color.B = (byte)(tintB * 255);
vertex.Color.A = (byte)(tintA * 255);
// 必须用 worldTriangleIndicesLength 不能直接 foreach
for (int i = 0; i < worldTriangleIndicesLength; i++)
{
var index = worldTriangleIndices[i] * 2;
vertex.Position.X = worldVertices[index];
vertex.Position.Y = worldVertices[index + 1];
vertex.TexCoords.X = uvs[index] * textureSizeX;
vertex.TexCoords.Y = uvs[index + 1] * textureSizeY;
vertexArray.Append(vertex);
}
//clipping.ClipEnd(slot);
}
if (UsePremultipliedAlpha && (states.BlendMode == BlendModeSFML.Normal || states.BlendMode == BlendModeSFML.Additive))
states.Shader = FragmentShader;
else
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

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

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

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

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

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

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

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

View File

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

View File

@@ -9,23 +9,23 @@ using System.Threading.Tasks;
namespace SpineViewer.Spine
{
public class VersionTypeConverter : EnumConverter
public class VersionConverter : EnumConverter
{
public VersionTypeConverter() : base(typeof(Version)) { }
public VersionConverter() : base(typeof(Version)) { }
public override object? ConvertTo(ITypeDescriptorContext? context, CultureInfo? culture, object? value, Type? destinationType)
{
if (destinationType == typeof(string) && value is Version version)
{
// 调用自定义的 String() 方法
return version.String();
return version.GetName();
}
return base.ConvertTo(context, culture, value, destinationType);
}
}
public class AnimationTypeConverter : StringConverter
public class AnimationConverter : StringConverter
{
public override bool GetStandardValuesSupported(ITypeDescriptorContext? context)
{
@@ -43,10 +43,18 @@ namespace SpineViewer.Spine
{
if (context?.Instance is Spine obj)
{
// 返回 AnimationNames 作为下拉选项
return new StandardValuesCollection(obj.AnimationNames);
}
else if (context?.Instance is Spine[] spines)
{
if (spines.Length > 0)
{
IEnumerable<string> common = spines[0].AnimationNames;
foreach (var spine in spines.Skip(1))
common = common.Intersect(spine.AnimationNames);
return new StandardValuesCollection(common.ToArray());
}
}
return base.GetStandardValues(context);
}
}

View File

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

View File

@@ -8,7 +8,7 @@
<RuntimeIdentifier>win-x64</RuntimeIdentifier>
<BaseOutputPath>$(SolutionDir)out</BaseOutputPath>
<IncludeSourceRevisionInInformationalVersion>false</IncludeSourceRevisionInInformationalVersion>
<Version>0.9.5</Version>
<Version>0.10.8</Version>
<OutputType>WinExe</OutputType>
<UseWindowsForms>true</UseWindowsForms>
<ApplicationIcon>appicon.ico</ApplicationIcon>
@@ -19,12 +19,14 @@
</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" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\SpineRuntimes\SpineRuntime21\SpineRuntime21.csproj" />
<ProjectReference Include="..\SpineRuntimes\SpineRuntime36\SpineRuntime36.csproj" />
<ProjectReference Include="..\SpineRuntimes\SpineRuntime37\SpineRuntime37.csproj" />
<ProjectReference Include="..\SpineRuntimes\SpineRuntime38\SpineRuntime38.csproj" />

View File

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

BIN
img/preview.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 157 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 234 KiB