Compare commits
386 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
165be38cfe | ||
|
|
f1fb5fe5e1 | ||
|
|
17554d3a8e | ||
|
|
054ac3939b | ||
|
|
9566c0a6f5 | ||
|
|
b8509ccb69 | ||
|
|
1e043e4faa | ||
|
|
5b1c177f58 | ||
|
|
794de783db | ||
|
|
7f6aa26986 | ||
|
|
fcd809fda5 | ||
|
|
71ff8007fb | ||
|
|
a8261f4f58 | ||
|
|
bb6a6f3664 | ||
|
|
9c6af07d32 | ||
|
|
afc011adbb | ||
|
|
da42c14d35 | ||
|
|
a7438e2026 | ||
|
|
47e5314bb3 | ||
|
|
1978c1da11 | ||
|
|
914c9d0ea3 | ||
|
|
7134aebb7f | ||
|
|
abb32e9ed2 | ||
|
|
2c77e385c5 | ||
|
|
ba0f5ac124 | ||
|
|
c4f17e3f06 | ||
|
|
b802ec252a | ||
|
|
68779caab0 | ||
|
|
5d819114d0 | ||
|
|
46b3937236 | ||
|
|
6803b8cf4a | ||
|
|
bd9c5a176b | ||
|
|
76c1d96c87 | ||
|
|
304af805cb | ||
|
|
027d3af619 | ||
|
|
c612c01ac7 | ||
|
|
cd7855a877 | ||
|
|
cdd81e0bfb | ||
|
|
750a8b8aff | ||
|
|
cd86155878 | ||
|
|
16739c39d6 | ||
|
|
c7971a9829 | ||
|
|
44c4fc4b21 | ||
|
|
6f1c8e3320 | ||
|
|
8f818416ba | ||
|
|
de6858ca48 | ||
|
|
3fd3d2a378 | ||
|
|
706c9125e6 | ||
|
|
5f026b000c | ||
|
|
0b0d036f08 | ||
|
|
6b9017d535 | ||
|
|
5eb47e33ac | ||
|
|
4d31335da0 | ||
|
|
0b5e76a448 | ||
|
|
775268c01a | ||
|
|
b0b1c85047 | ||
|
|
5f08fc6695 | ||
|
|
2de3bdf12b | ||
|
|
3a424c7dc1 | ||
|
|
c3e2b37072 | ||
|
|
65bd11a346 | ||
|
|
e6e7fc539f | ||
|
|
6522d415b7 | ||
|
|
378c66a333 | ||
|
|
07204417a5 | ||
|
|
c9c909cdf9 | ||
|
|
a9f59a4d2f | ||
|
|
1d2513cef5 | ||
|
|
febb797ae2 | ||
|
|
68d279a7c3 | ||
|
|
d2d8b7955c | ||
|
|
2a55fd9c36 | ||
|
|
695d3c0735 | ||
|
|
ce95db469b | ||
|
|
5d187cf80f | ||
|
|
e704ebc224 | ||
|
|
ee36f8981c | ||
|
|
09dd220abf | ||
|
|
15bc2dc3b8 | ||
|
|
1deb74eca9 | ||
|
|
de76ce64ab | ||
|
|
94b4ba33e6 | ||
|
|
7ce8a115f4 | ||
|
|
c036a4bb45 | ||
|
|
aa62f30b05 | ||
|
|
3d967c9812 | ||
|
|
e87e9efb99 | ||
|
|
8c1f6fb4a6 | ||
|
|
df82ed8a00 | ||
|
|
d01e3920ba | ||
|
|
777cd5ea3f | ||
|
|
3b73aea5c0 | ||
|
|
168f7a8173 | ||
|
|
04437e2de2 | ||
|
|
2ec83b2e87 | ||
|
|
90bfaa7b56 | ||
|
|
2ae175abd0 | ||
|
|
e2a84d8f88 | ||
|
|
b6f9cd0c7c | ||
|
|
61b7b90722 | ||
|
|
093c159753 | ||
|
|
32d36c0757 | ||
|
|
94dabebf2b | ||
|
|
8e875d4f7e | ||
|
|
86c383f2cf | ||
|
|
b404d8e79a | ||
|
|
d32b480ef2 | ||
|
|
3654825f27 | ||
|
|
d7231e8a09 | ||
|
|
98161aaf2e | ||
|
|
7a942b16bc | ||
|
|
067719c69b | ||
|
|
f3fce53b91 | ||
|
|
e35903f436 | ||
|
|
64cfe5fdd7 | ||
|
|
dbe586cff8 | ||
|
|
3104733db0 | ||
|
|
9d4bdd1028 | ||
|
|
f8030b1645 | ||
|
|
0a999ceb41 | ||
|
|
64bd9907cb | ||
|
|
580eaf990d | ||
|
|
5ab232a961 | ||
|
|
e596cd7ea4 | ||
|
|
05c47a4daa | ||
|
|
5a8783b5f4 | ||
|
|
08bc171a72 | ||
|
|
7372f5fe08 | ||
|
|
6f032bdd05 | ||
|
|
153d3603d2 | ||
|
|
95261e6907 | ||
|
|
17b344376d | ||
|
|
0ed4e44878 | ||
|
|
b42c1832f0 | ||
|
|
058534ba67 | ||
|
|
204dcd6498 | ||
|
|
2c846c0db9 | ||
|
|
2faeb044e0 | ||
|
|
09c8e4f779 | ||
|
|
6994fa6be8 | ||
|
|
cc7beb7670 | ||
|
|
510653732d | ||
|
|
93e8178d67 | ||
|
|
cebc4864cc | ||
|
|
6ad0449376 | ||
|
|
c33c977326 | ||
|
|
f0299d365a | ||
|
|
6ecdca73f5 | ||
|
|
af6a709b2c | ||
|
|
d5c27450ef | ||
|
|
d10269fb07 | ||
|
|
53d987476e | ||
|
|
8b7866d37f | ||
|
|
bb529729b6 | ||
|
|
b7735d9ba8 | ||
|
|
ce744e2b84 | ||
|
|
631c92da3f | ||
|
|
b7063804e9 | ||
|
|
75d47c8419 | ||
|
|
114fb05e80 | ||
|
|
1fec65b37d | ||
|
|
9498e8f334 | ||
|
|
83b8411929 | ||
|
|
e9accd13b3 | ||
|
|
9e27a19258 | ||
|
|
252f3a5bea | ||
|
|
e0626bb126 | ||
|
|
7ff62c7f40 | ||
|
|
4b07e02acb | ||
|
|
4654d1d9c2 | ||
|
|
ce1f75e8a5 | ||
|
|
4d9aebc758 | ||
|
|
e814368ef3 | ||
|
|
bbbb02500f | ||
|
|
404f255f14 | ||
|
|
7a15e0d38a | ||
|
|
bfe669bdd9 | ||
|
|
c0553042fd | ||
|
|
af8b02654b | ||
|
|
4779ec91d0 | ||
|
|
14d7f4af0e | ||
|
|
f9888b23dd | ||
|
|
411cdbb00f | ||
|
|
d859f07469 | ||
|
|
c111819093 | ||
|
|
aa8321d13c | ||
|
|
5e3bd972e5 | ||
|
|
ad39a04fff | ||
|
|
9a97e84296 | ||
|
|
1b7b0dcb13 | ||
|
|
d365a5060b | ||
|
|
b69589394a | ||
|
|
00f5791766 | ||
|
|
38cab2eda7 | ||
|
|
0db4d6e4e0 | ||
|
|
549712962f | ||
|
|
34b7002faf | ||
|
|
0e6f47b23c | ||
|
|
a372a89b5e | ||
|
|
239847aee7 | ||
|
|
813249c6a7 | ||
|
|
293ab28bce | ||
|
|
98e73cdec5 | ||
|
|
6d34bb9d25 | ||
|
|
479a5e4da9 | ||
|
|
4829454877 | ||
|
|
28664f6387 | ||
|
|
1a08a23a9c | ||
|
|
16f344ff1b | ||
|
|
693ce0e2e8 | ||
|
|
e6f533ea65 | ||
|
|
fcc21d63b0 | ||
|
|
afc0ffcb67 | ||
|
|
9ffb9840e1 | ||
|
|
4766ccf1b6 | ||
|
|
16b75c80a3 | ||
|
|
880f063046 | ||
|
|
723c11b886 | ||
|
|
5e074b1cf7 | ||
|
|
71d2fee36e | ||
|
|
7dc701464f | ||
|
|
fd876ef90f | ||
|
|
0597852178 | ||
|
|
81b1333091 | ||
|
|
7baebd79a6 | ||
|
|
951d0e30ae | ||
|
|
711e172769 | ||
|
|
faa60f0ea1 | ||
|
|
99d81c4329 | ||
|
|
17904326f3 | ||
|
|
5ee74f39d8 | ||
|
|
72f898ed60 | ||
|
|
157eab5bac | ||
|
|
e1b0d0a2ad | ||
|
|
2c050ba031 | ||
|
|
41518b16b4 | ||
|
|
72a16dc95f | ||
|
|
3404c64f55 | ||
|
|
b9015422f8 | ||
|
|
a7441b968d | ||
|
|
2d44be31f7 | ||
|
|
c2cf25bb2b | ||
|
|
7c4c53dcb0 | ||
|
|
aceb3b17c8 | ||
|
|
adfcfdb1de | ||
|
|
da329723bc | ||
|
|
63eb53fa06 | ||
|
|
d32c824515 | ||
|
|
e9ee8c481c | ||
|
|
6d78e52605 | ||
|
|
90136a5562 | ||
|
|
1592767c8c | ||
|
|
afa6ce2113 | ||
|
|
50e6e414ee | ||
|
|
ba9b8edcdc | ||
|
|
d7a927475c | ||
|
|
afe210343f | ||
|
|
4e293daf62 | ||
|
|
f9d7fdc516 | ||
|
|
6a04f3955c | ||
|
|
dce3b1780c | ||
|
|
f47f3e9db6 | ||
|
|
4ac74acaf7 | ||
|
|
cf7588c288 | ||
|
|
ec7bdf4000 | ||
|
|
51cd97f782 | ||
|
|
a16f2f096d | ||
|
|
4e92f14551 | ||
|
|
8f6cc9ff44 | ||
|
|
f885df5c67 | ||
|
|
0ccb110e36 | ||
|
|
2c238dca9b | ||
|
|
3e0aa53fca | ||
|
|
12b4e44296 | ||
|
|
9a2cf4aefe | ||
|
|
0e2a116e0a | ||
|
|
7bf30eb54a | ||
|
|
8dda8c8ff3 | ||
|
|
988fdb22be | ||
|
|
1dd2c8fb4d | ||
|
|
2b39384b28 | ||
|
|
28d1275023 | ||
|
|
979181fc3b | ||
|
|
b374b88ad5 | ||
|
|
6643c19a20 | ||
|
|
7460874c81 | ||
|
|
13dd7511f6 | ||
|
|
f153d251c8 | ||
|
|
3442ace981 | ||
|
|
547cebf5a9 | ||
|
|
7a24d22bc6 | ||
|
|
8f5728afe4 | ||
|
|
41b5ac2c61 | ||
|
|
694ca3bf25 | ||
|
|
674d314b55 | ||
|
|
08a35cc5d1 | ||
|
|
176e5db4d9 | ||
|
|
2535a9ebf9 | ||
|
|
8ff99ee925 | ||
|
|
abc8218487 | ||
|
|
e4765750c3 | ||
|
|
02cddf556b | ||
|
|
e1e6d3c72d | ||
|
|
b401a16002 | ||
|
|
523b0ce295 | ||
|
|
abb06726f0 | ||
|
|
d9190e9418 | ||
|
|
9fe3761eca | ||
|
|
51824afba6 | ||
|
|
160a49ad5f | ||
|
|
9d4907d77e | ||
|
|
53d30e0503 | ||
|
|
9609a2fd5d | ||
|
|
66cf0efcb9 | ||
|
|
0129b9df31 | ||
|
|
a7a5521be1 | ||
|
|
f7f7211ca2 | ||
|
|
8c921a6ed5 | ||
|
|
f14ab870f7 | ||
|
|
26e81ffdb6 | ||
|
|
598a88203e | ||
|
|
914d02e754 | ||
|
|
5cf30f391b | ||
|
|
8de00cad76 | ||
|
|
e4c58f2f4e | ||
|
|
063dba30b6 | ||
|
|
01fa9287a1 | ||
|
|
008067fccb | ||
|
|
091301e945 | ||
|
|
145f4f3265 | ||
|
|
36d4e8c948 | ||
|
|
63de847a57 | ||
|
|
b3e1b7c902 | ||
|
|
2dbc235631 | ||
|
|
4d68b48367 | ||
|
|
65e63e2b2d | ||
|
|
58071e1de1 | ||
|
|
5009ef479f | ||
|
|
e5e9357649 | ||
|
|
a577474772 | ||
|
|
e960a09153 | ||
|
|
13d50f59c3 | ||
|
|
ed4c8475e9 | ||
|
|
2338bf4e15 | ||
|
|
267aa7ee63 | ||
|
|
3df7dbc769 | ||
|
|
5f12ab7e85 | ||
|
|
ac0adc5f95 | ||
|
|
208b702065 | ||
|
|
7e61fbfbac | ||
|
|
0591549727 | ||
|
|
a0833580f8 | ||
|
|
c622b60215 | ||
|
|
c228cf9072 | ||
|
|
4c68dd4904 | ||
|
|
32fde582fc | ||
|
|
2bf2509df7 | ||
|
|
07042189c8 | ||
|
|
d251c94638 | ||
|
|
b4119087fb | ||
|
|
e3959e80fb | ||
|
|
0495a2344c | ||
|
|
c781ec5a4f | ||
|
|
a58566735f | ||
|
|
b37e5c25c3 | ||
|
|
63a937a45b | ||
|
|
c920471c0c | ||
|
|
c4863ee09b | ||
|
|
c0b85c454e | ||
|
|
763a49a4d3 | ||
|
|
0e1540873c | ||
|
|
39dcc636ca | ||
|
|
342778c56e | ||
|
|
fd524891aa | ||
|
|
48cb60020c | ||
|
|
d502c592f7 | ||
|
|
e4377436a7 | ||
|
|
eb44c1271e | ||
|
|
20953c2dfc | ||
|
|
4c96a71124 | ||
|
|
295afdc874 | ||
|
|
a66280ce7b | ||
|
|
ce1ea1c5c5 | ||
|
|
16b58866fc | ||
|
|
b4821b9169 | ||
|
|
3686df0f40 |
7
.github/workflows/dotnet-desktop.yml
vendored
7
.github/workflows/dotnet-desktop.yml
vendored
@@ -1,4 +1,4 @@
|
||||
name: Build and Release WinForms
|
||||
name: Build & Release
|
||||
|
||||
on:
|
||||
push:
|
||||
@@ -47,9 +47,8 @@ jobs:
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
tag_name: ${{ github.ref_name }}
|
||||
release_name: Release ${{ github.ref_name }}
|
||||
body: 'Automated release build ${{ github.ref_name }}'
|
||||
tag_name: ${{ env.VERSION }}
|
||||
release_name: Release ${{ env.VERSION }}
|
||||
draft: false
|
||||
prerelease: false
|
||||
|
||||
|
||||
149
CHANGELOG.md
Normal file
149
CHANGELOG.md
Normal file
@@ -0,0 +1,149 @@
|
||||
# CHANGELOG
|
||||
|
||||
## v0.12.7
|
||||
|
||||
- 修复一些问题
|
||||
|
||||
## v0.12.6
|
||||
|
||||
- 增加全屏预览
|
||||
- 增加桌面投影 (实验性功能)
|
||||
- 增加预览画面背景色设置
|
||||
- 增加分辨率和颜色预设列表
|
||||
- 皮肤面板显示 default
|
||||
|
||||
## v0.12.5
|
||||
|
||||
- 增加插槽属性面板
|
||||
- 修改皮肤属性面板设置方式为True/False
|
||||
|
||||
## v0.12.4
|
||||
|
||||
- 增加导出自动分辨率参数
|
||||
- 增加导出边缘和填充参数
|
||||
- 增加导出内容溢出参数
|
||||
- 支持3.7及以下版本多皮肤功能
|
||||
- 增加3.8版本的骨骼文件二进制和文本格式互转
|
||||
- 增加格式转换输出文件夹参数
|
||||
- 修改打开对话框的默认文件后缀筛选为所有类型
|
||||
|
||||
## v0.12.3
|
||||
|
||||
- 增加按住 ctrl 缩放选中模型
|
||||
- 增加对骨骼/网格/剪裁的调试渲染
|
||||
- 换回以前的上下参数面板布局
|
||||
- 修改窗口缩放模式为 Font -> Dpi
|
||||
- 修复部分问题
|
||||
|
||||
## v0.12.2
|
||||
|
||||
- 模型参数分标签显示
|
||||
- 皮肤/动画列表使用右键菜单进行增删
|
||||
- 标题栏显示版本号
|
||||
- 增加 webp 和 avif 动图格式
|
||||
- 增加导出参数缓存
|
||||
- 动图默认帧率修改为 24 帧
|
||||
- 增加保留最后一帧参数
|
||||
|
||||
## v0.12.1
|
||||
|
||||
- 优化使用体验, 提供初始皮肤/动画空位
|
||||
- 修复预览画面分辨率调整时父容器尺寸获取错误
|
||||
|
||||
## v0.12.0
|
||||
|
||||
- 支持皮肤列表 (仅 3.8.x 及以上支持)
|
||||
- 支持多轨道动画
|
||||
- 动画和皮肤列表多选时改为取并集
|
||||
- 修复导出时没有正确处理预乘像素的问题
|
||||
|
||||
## v0.11.5
|
||||
|
||||
- 导出格式全面支持
|
||||
- 修复预览图不显示的问题
|
||||
- 优化列表卡顿问题
|
||||
- 模型列表增加数量显示
|
||||
|
||||
## v0.11.4
|
||||
|
||||
- 增加 MP4 导出格式
|
||||
- 增加导出背景颜色参数
|
||||
- 增加日志输出 FFMpeg 参数字符串
|
||||
- 增加导出时任务栏图标执行动效
|
||||
- 修复预览面板移动模型时物理效果不同步的问题
|
||||
- 优化部分使用体验
|
||||
|
||||
## v0.11.3
|
||||
|
||||
- 增加模型隐藏设置属性
|
||||
- 加宽面板分割条 (4 -> 8 像素)
|
||||
- 优化属性面板分组显示
|
||||
- 增加调试纹理
|
||||
|
||||
## v0.11.2
|
||||
|
||||
- 增加皮肤切换
|
||||
- 优化模型缩放实现
|
||||
- 修复部分情况纹理加载异常
|
||||
|
||||
## v0.11.1
|
||||
|
||||
- 增加 GIF 导出格式
|
||||
- 增加逐个导出时可选自动时长
|
||||
- 优化使用体验
|
||||
|
||||
## v0.11.0
|
||||
|
||||
- 完成导出系统, 支持完整的单帧和帧序列导出功能
|
||||
- 预览画面增加快进功能
|
||||
|
||||
## v0.10.9
|
||||
|
||||
- 预览图导出增加名称后缀参数
|
||||
|
||||
## v0.10.8
|
||||
|
||||
- 完善预览图导出
|
||||
- 优化骨骼文件选择
|
||||
|
||||
## v0.10.7
|
||||
|
||||
- 增加仅导出选中
|
||||
- 增加模型调试属性
|
||||
|
||||
## v0.10.6
|
||||
|
||||
- 增加文件夹检测
|
||||
- 增加从剪贴板添加(可复制本地文件/文件夹直接打开)
|
||||
- 修复预览图导致的批量添加可能卡死
|
||||
|
||||
## v0.10.5
|
||||
|
||||
- 修复一些问题
|
||||
|
||||
## v0.10.4
|
||||
|
||||
- 修复一些问题
|
||||
|
||||
## v0.10.3
|
||||
|
||||
- 增加自动版本检测
|
||||
- 增加文件拖放打开
|
||||
|
||||
## v0.10.2
|
||||
|
||||
- 增加列表右键菜单快捷键
|
||||
- 增加预览缩略图复制
|
||||
- 增加列表视图切换
|
||||
|
||||
## v0.10.1
|
||||
|
||||
- 增加列表预览图
|
||||
- 增加列表预览图导出
|
||||
|
||||
## v0.10.0
|
||||
|
||||
- 增加了画面和列表的选择联动,并删除了预览画面显示包围盒选项
|
||||
- 增加了骨骼文件格式转换功能,目前仅支持部分版本的不完整功能
|
||||
- 优化了部分使用体验
|
||||
|
||||
111
README.en.md
Normal file
111
README.en.md
Normal file
@@ -0,0 +1,111 @@
|
||||
# [SpineViewer](https://github.com/ww-rm/SpineViewer)
|
||||
|
||||
[](https://github.com/ww-rm/SpineViewer/actions/workflows/dotnet-desktop.yml)
|
||||
[](https://github.com/ww-rm/SpineViewer/releases)
|
||||
[](https://github.com/ww-rm/SpineViewer/releases)
|
||||
|
||||
[中文](README.md) | [English](README.en.md)
|
||||
|
||||
A *WYSIWYG* Spine file viewer & exporter.
|
||||
|
||||

|
||||
|
||||
## Features
|
||||
|
||||
- Supports multiple Spine file versions
|
||||
- Drag & drop or copy/paste to open files in batch
|
||||
- List-based skeleton view with render layer management
|
||||
- Multi-select list to batch-adjust skeleton parameters
|
||||
- Multi-track animation support
|
||||
- Skin / custom slot attachment configuration
|
||||
- Debug rendering mode
|
||||
- Fullscreen preview
|
||||
- Export to single-frame image, animated GIF/WebP/AVIF, video formats
|
||||
- Batch export at multiple resolutions
|
||||
- Custom FFmpeg export parameters
|
||||
- …and more
|
||||
|
||||
### Spine Version Support
|
||||
|
||||
| Version | View & Export | Format Conversion | Version Conversion |
|
||||
| :------: | :-----------: | :---------------: | :----------------: |
|
||||
| `2.1.x` | :white_check_mark: | | |
|
||||
| `3.6.x` | :white_check_mark: | | |
|
||||
| `3.7.x` | :white_check_mark: | | |
|
||||
| `3.8.x` | :white_check_mark: | :white_check_mark: | |
|
||||
| `4.0.x` | :white_check_mark: | | |
|
||||
| `4.1.x` | :white_check_mark: | | |
|
||||
| `4.2.x` | :white_check_mark: | | |
|
||||
| `4.3.x` | | | |
|
||||
|
||||
More versions coming soon 🚀🚀🚀
|
||||
|
||||
### Supported Export Formats
|
||||
|
||||
| Export Format | Use Case |
|
||||
| --------------------- | ----------------------------------------------------------------------------------------- |
|
||||
| Single Frame | Generate high‑resolution still images; pick any frame manually. |
|
||||
| Frame Sequence (PNG) | Lossless PNG sequences with alpha channel preserved. |
|
||||
| GIF / WebP / AVIF | Perfect for quick animated previews. |
|
||||
| MP4 | The most widely compatible video format. |
|
||||
| WebM | Browser‑friendly streaming with optional transparency. |
|
||||
| MKV / MOV | For those who like to tinker. |
|
||||
| Custom FFmpeg Command | Use any FFmpeg arguments for complex, tailored export workflows. |
|
||||
|
||||
## Installation
|
||||
|
||||
1. Go to the [Releases](https://github.com/ww-rm/SpineViewer/releases) page and download the ZIP.
|
||||
2. Make sure you have the [.NET Desktop Runtime 8.0.x](https://dotnet.microsoft.com/download/dotnet/8.0) installed.
|
||||
3. Alternatively, download the `SelfContained` ZIP, which runs standalone without any .NET prerequisites.
|
||||
4. To export GIF or other video formats, install the `ffmpeg` CLI and add it to your PATH.
|
||||
- Windows builds: see the [FFmpeg download page](https://ffmpeg.org/download.html#build-windows)
|
||||
- Direct download: [ffmpeg-release-full.7z](https://www.gyan.dev/ffmpeg/builds/ffmpeg-release-full.7z)
|
||||
|
||||
## Usage
|
||||
|
||||
### Importing Skeletons
|
||||
|
||||
You can import Spine skeletons in three ways:
|
||||
|
||||
- Drag & drop or paste skeleton files or folders onto the model list.
|
||||
- Use **File > Open** to batch‑open multiple skeleton files.
|
||||
- Use **File > Open Single Model** to open one at a time.
|
||||
|
||||
### Adjusting Content
|
||||
|
||||
- Right‑click menu and keyboard shortcuts are available in the model list. You can multi‑select to adjust parameters in batch.
|
||||
- In the preview pane, you can also use mouse controls:
|
||||
- **Left‑click & drag** to move a model; hold **Ctrl** to multi‑select (synced with the list).
|
||||
- **Right‑click & drag** to pan the entire scene.
|
||||
- **Mouse wheel** to zoom; hold **Ctrl** to zoom all selected models proportionally.
|
||||
- **“Render Selected Only”** mode shows only the selected models in preview; use the list to change selection.
|
||||
|
||||
Below the preview, playback controls let you scrub through the timeline like a basic player.
|
||||
|
||||
### Exporting Content
|
||||
|
||||
Exports follow the “what you see is what you get” principle—your real‑time preview is exactly what gets exported.
|
||||
|
||||
Key export options:
|
||||
|
||||
- **Render Selected Only**: includes only the selected models in both preview and export.
|
||||
- **Output Folder**: if unspecified, exports go into each model’s source folder; otherwise, everything exports to the chosen folder.
|
||||
- **Export Single**: by default, each model is exported separately; enable this to render all selected models together into a single output.
|
||||
- **Auto Resolution**: ignores preview resolution and viewport size—exports at the content’s actual bounds; for animations, matches the full animation area.
|
||||
|
||||
## More
|
||||
|
||||
Detailed usage and advanced tips are in the [Wiki](https://github.com/ww-rm/SpineViewer/wiki).
|
||||
Encounter a bug or have a feature request? Open an [Issue](https://github.com/ww-rm/SpineViewer/issues).
|
||||
|
||||
## Acknowledgements
|
||||
|
||||
- [spine-runtimes](https://github.com/EsotericSoftware/spine-runtimes)
|
||||
- [SFML.Net](https://github.com/SFML/SFML.Net)
|
||||
- [FFMpegCore](https://github.com/rosenbjerg/FFMpegCore)
|
||||
|
||||
---
|
||||
|
||||
If you find this project useful, please give it a ⭐ and share it with others!
|
||||
|
||||
[](https://starchart.cc/ww-rm/SpineViewer)
|
||||
114
README.md
114
README.md
@@ -1,65 +1,113 @@
|
||||
# SpineViewer
|
||||
# [SpineViewer](https://github.com/ww-rm/SpineViewer)
|
||||
|
||||
[](https://github.com/ww-rm/SpineViewer/actions/workflows/dotnet-desktop.yml)
|
||||
[](https://github.com/ww-rm/SpineViewer/releases)
|
||||
[](https://github.com/ww-rm/SpineViewer/releases)
|
||||
|
||||
[中文](README.md) | [English](README.en.md)
|
||||
|
||||
一个简单好用的 Spine 文件查看&导出程序.
|
||||
*所见即所得* 的 Spine 文件查看&导出程序.
|
||||
|
||||

|
||||

|
||||
|
||||
---
|
||||
## 功能
|
||||
|
||||
- 支持多版本 spine 文件
|
||||
- 支持拖拽/复制粘贴批量打开文件
|
||||
- 支持列表式多骨骼查看和渲染层级管理
|
||||
- 支持列表多选批量设置骨骼参数
|
||||
- 支持多轨道动画设置
|
||||
- 支持皮肤/自定义插槽附件设置
|
||||
- 支持调试渲染
|
||||
- 支持全屏预览
|
||||
- 支持单帧/动图/视频文件导出
|
||||
- 支持自动分辨率批量导出
|
||||
- 支持 FFmpeg 自定义导出
|
||||
- ...
|
||||
|
||||
### Spine 版本支持
|
||||
|
||||
| 版本 | 查看&导出 | 格式转换 | 版本转换 |
|
||||
| :---: | :---: | :---: | :---: |
|
||||
| `2.1.x` | :white_check_mark: | | |
|
||||
| `3.6.x` | :white_check_mark: | | |
|
||||
| `3.7.x` | :white_check_mark: | | |
|
||||
| `3.8.x` | :white_check_mark: | :white_check_mark: | |
|
||||
| `4.0.x` | :white_check_mark: | | |
|
||||
| `4.1.x` | :white_check_mark: | | |
|
||||
| `4.2.x` | :white_check_mark: | | |
|
||||
| `4.3.x` | | | |
|
||||
|
||||
更多版本正在施工 :rocket: :rocket: :rocket:
|
||||
|
||||
### 导出格式支持
|
||||
|
||||
| 导出格式 | 适用场景 |
|
||||
| --- | --- |
|
||||
| 单帧画面 | 支持生成高清模型画面图像, 可手动调节需要的一帧. |
|
||||
| 帧序列 | 支持 PNG 格式帧序列, 可保留透明通道且无损压缩. |
|
||||
| GIF/WebP/AVIF | 适合生成预览动图. |
|
||||
| MP4 | 最常见的视频格式, 兼容性最好. |
|
||||
| WebM | 适合浏览器在线播放格式, 支持透明背景. |
|
||||
| MKV/MOV | 适合折腾. |
|
||||
| 自定义导出 | 除上述预设方案, 支持提供任意 FFmpeg 参数进行导出, 满足自定义复杂需求. |
|
||||
|
||||
## 安装
|
||||
|
||||
前往 [Release](https://github.com/ww-rm/SpineViewer/releases) 界面下载压缩包.
|
||||
|
||||
`SelfContained` 可独立运行, `FrameworkDependent` 需要安装依赖框架 [.NET 桌面运行时 8.0.x](https://dotnet.microsoft.com/zh-cn/download/dotnet/8.0).
|
||||
软件需要安装依赖框架 [.NET 桌面运行时 8.0.x](https://dotnet.microsoft.com/zh-cn/download/dotnet/8.0).
|
||||
|
||||
## 功能
|
||||
也可以下载带有 `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...
|
||||
导出 GIF 等视频格式需要在本地安装 ffmpeg 命令行, 并且添加至环境变量, [点击前往 FFmpeg-Windows 下载页面](https://ffmpeg.org/download.html#build-windows), 也可以点这个下载最新版本 [ffmpeg-release-full.7z](https://www.gyan.dev/ffmpeg/builds/ffmpeg-release-full.7z).
|
||||
|
||||
## 使用方法
|
||||
|
||||
### 骨骼导入
|
||||
|
||||
**文件**菜单可以选择**打开**或者**批量打开**进行骨骼文件导入.
|
||||
有 3 种方式导入骨骼文件:
|
||||
|
||||
### 骨骼调整
|
||||
- 拖放/粘贴需要导入的骨骼文件/目录到模型列表
|
||||
- 从文件菜单里批量打开骨骼文件
|
||||
- 从文件菜单选择单个模型打开
|
||||
|
||||
在**模型列表**中选择一项或多项, 将会在**模型参数**面板显示可供调节的参数.
|
||||
### 内容调整
|
||||
|
||||
**模型列表**右键菜单可以对列表项进行增删调整, 也可以使用鼠标左键拖动调整顺序.
|
||||
模型列表支持右键菜单以及部分快捷键, 并且可以多选进行模型参数的批量调整.
|
||||
|
||||
### 画面调整
|
||||
预览画面除了使用面板进行参数设置外, 支持部分鼠标动作:
|
||||
|
||||
**预览画面**支持的鼠标操作:
|
||||
- 左键可以选择和拖拽模型, 按下 `Ctrl` 键可以实现多选, 与左侧列表选择是联动的.
|
||||
- 右键对整体画面进行拖动.
|
||||
- 滚轮进行画面缩放, 按住 `Ctrl` 可以对选中的模型进行批量缩放.
|
||||
- 仅渲染选中模式, 在该模式下, 预览画面仅包含被选中的模型, 并且只能通过左侧列表改变选中状态.
|
||||
|
||||
- 左键可以对骨骼进行拖动
|
||||
- 右键对画面进行拖动
|
||||
- 滚轮进行画面缩放
|
||||
预览画面下方按钮支持对画面时间进行调整, 可以当作一个简易的播放器.
|
||||
|
||||
除此之外, 也可以通过**画面参数**面板调节导出和预览时的画面参数.
|
||||
### 内容导出
|
||||
|
||||
在**功能**菜单中, 可以重置同步所有骨骼动画时间.
|
||||
导出遵循 "所见即所得" 原则, 即实时预览的画面就是你导出的画面.
|
||||
|
||||
### 动画导出
|
||||
导出有以下几个关键参数:
|
||||
|
||||
**文件**菜单中选择**导出**可以将目前加载的所有骨骼动画按照预览时的画面进行PNG帧序列导出.
|
||||
- 仅渲染选中. 这个参数不仅影响预览模式, 也影响导出, 如果仅渲染选中, 那么在导出时只有被选中的模型会被考虑, 忽略其他模型.
|
||||
- 输出文件夹. 这个参数某些时候可选, 当不提供时, 则将输出产物输出到每个模型各自的模型文件夹, 否则输出产物全部输出到提供的输出文件夹.
|
||||
- 导出单个. 默认是每个模型独立导出, 即对模型列表进行批量操作, 如果选择仅导出单个, 那么被导出的所有模型将在同一个画面上被渲染, 输出产物只有一份.
|
||||
- 自动分辨率. 该模式会忽略预览画面的分辨率和视区参数, 导出产物的分辨率与被导出内容的实际大小一致, 如果是动图或者视频则会与完整显示动画的必需大小一致.
|
||||
|
||||
可以在每个骨骼的**模型参数**中查看动画完整时长.
|
||||
### 更多
|
||||
|
||||
更为详细的使用方法和说明见 [Wiki](https://github.com/ww-rm/SpineViewer/wiki), 有使用上的问题或者 BUG 可以提个 [Issue](https://github.com/ww-rm/SpineViewer/issues).
|
||||
|
||||
## Acknowledgements
|
||||
|
||||
- [spine-runtimes](https://github.com/EsotericSoftware/spine-runtimes)
|
||||
- [SFML.Net](https://github.com/SFML/SFML.Net)
|
||||
- [FFMpegCore](https://github.com/rosenbjerg/FFMpegCore)
|
||||
|
||||
---
|
||||
|
||||
*如果你觉得这个项目不错请给个 :star:, 并分享给更多人知道! :)*
|
||||
|
||||
[](https://starchart.cc/ww-rm/SpineViewer)
|
||||
|
||||
723
SpineRuntimes/SpineRuntime21/Animation.cs
Normal file
723
SpineRuntimes/SpineRuntime21/Animation.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
300
SpineRuntimes/SpineRuntime21/AnimationState.cs
Normal file
300
SpineRuntimes/SpineRuntime21/AnimationState.cs
Normal file
@@ -0,0 +1,300 @@
|
||||
/******************************************************************************
|
||||
* 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 List<TrackEntry> Tracks => tracks;
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
70
SpineRuntimes/SpineRuntime21/AnimationStateData.cs
Normal file
70
SpineRuntimes/SpineRuntime21/AnimationStateData.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
288
SpineRuntimes/SpineRuntime21/Atlas.cs
Normal file
288
SpineRuntimes/SpineRuntime21/Atlas.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
46
SpineRuntimes/SpineRuntime21/Attachments/Attachment.cs
Normal file
46
SpineRuntimes/SpineRuntime21/Attachments/Attachment.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
47
SpineRuntimes/SpineRuntime21/Attachments/AttachmentLoader.cs
Normal file
47
SpineRuntimes/SpineRuntime21/Attachments/AttachmentLoader.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
35
SpineRuntimes/SpineRuntime21/Attachments/AttachmentType.cs
Normal file
35
SpineRuntimes/SpineRuntime21/Attachments/AttachmentType.cs
Normal 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
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
108
SpineRuntimes/SpineRuntime21/Attachments/MeshAttachment.cs
Normal file
108
SpineRuntimes/SpineRuntime21/Attachments/MeshAttachment.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
151
SpineRuntimes/SpineRuntime21/Attachments/RegionAttachment.cs
Normal file
151
SpineRuntimes/SpineRuntime21/Attachments/RegionAttachment.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
154
SpineRuntimes/SpineRuntime21/Bone.cs
Normal file
154
SpineRuntimes/SpineRuntime21/Bone.cs
Normal file
@@ -0,0 +1,154 @@
|
||||
/******************************************************************************
|
||||
* 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 readonly public bool yDown = false;
|
||||
|
||||
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 () {
|
||||
float sx = skeleton.scaleX, sy = skeleton.scaleY;
|
||||
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 {
|
||||
worldX = x * sx;
|
||||
worldY = y * sy;
|
||||
worldScaleX = scaleX;
|
||||
worldScaleY = scaleY;
|
||||
worldRotation = rotationIK;
|
||||
worldFlipX = (sx < 0) != flipX;
|
||||
worldFlipY = (sy < 0) != flipY;
|
||||
}
|
||||
float radians = worldRotation * (float)Math.PI / 180;
|
||||
float cos = (float)Math.Cos(radians);
|
||||
float sin = (float)Math.Sin(radians);
|
||||
m00 = cos * worldScaleX * sx;
|
||||
m01 = -sin * worldScaleY * sx;
|
||||
m10 = sin * worldScaleX * sy;
|
||||
m11 = cos * worldScaleY * sy;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
66
SpineRuntimes/SpineRuntime21/BoneData.cs
Normal file
66
SpineRuntimes/SpineRuntime21/BoneData.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
48
SpineRuntimes/SpineRuntime21/Event.cs
Normal file
48
SpineRuntimes/SpineRuntime21/Event.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
51
SpineRuntimes/SpineRuntime21/EventData.cs
Normal file
51
SpineRuntimes/SpineRuntime21/EventData.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
150
SpineRuntimes/SpineRuntime21/IkConstraint.cs
Normal file
150
SpineRuntimes/SpineRuntime21/IkConstraint.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
57
SpineRuntimes/SpineRuntime21/IkConstraintData.cs
Normal file
57
SpineRuntimes/SpineRuntime21/IkConstraintData.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
542
SpineRuntimes/SpineRuntime21/Json.cs
Normal file
542
SpineRuntimes/SpineRuntime21/Json.cs
Normal 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<object>, a Dictionary<string, object>, 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<string, object> / List<object></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());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
357
SpineRuntimes/SpineRuntime21/Skeleton.cs
Normal file
357
SpineRuntimes/SpineRuntime21/Skeleton.cs
Normal file
@@ -0,0 +1,357 @@
|
||||
/******************************************************************************
|
||||
* 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 float scaleX = 1, scaleY = 1;
|
||||
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 float ScaleX { get { return scaleX; } set { scaleX = value; } }
|
||||
public float ScaleY { get { return scaleY; } set { scaleY = value; } }
|
||||
|
||||
[Obsolete("Use ScaleX instead. FlipX is when ScaleX is negative.")]
|
||||
public bool FlipX { get { return scaleX < 0; } set { scaleX = value ? -1f : 1f; } }
|
||||
|
||||
[Obsolete("Use ScaleY instead. FlipY is when ScaleY is negative.")]
|
||||
public bool FlipY { get { return scaleY < 0; } set { scaleY = value ? -1f : 1f; } }
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
public void GetBounds(out float x, out float y, out float width, out float height)
|
||||
{
|
||||
float[] temp = new float[8];
|
||||
float minX = int.MaxValue, minY = int.MaxValue, maxX = int.MinValue, maxY = int.MinValue;
|
||||
for (int i = 0, n = drawOrder.Count; i < n; i++)
|
||||
{
|
||||
Slot slot = drawOrder[i];
|
||||
int verticesLength = 0;
|
||||
float[] vertices = null;
|
||||
Attachment attachment = slot.Attachment;
|
||||
if (attachment is RegionAttachment regionAttachment)
|
||||
{
|
||||
verticesLength = 8;
|
||||
vertices = temp;
|
||||
if (vertices.Length < 8) vertices = temp = new float[8];
|
||||
regionAttachment.ComputeWorldVertices(slot.Bone, temp);
|
||||
}
|
||||
else if (attachment is MeshAttachment meshAttachment)
|
||||
{
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
x = minX;
|
||||
y = minY;
|
||||
width = maxX - minX;
|
||||
height = maxY - minY;
|
||||
}
|
||||
}
|
||||
}
|
||||
663
SpineRuntimes/SpineRuntime21/SkeletonBinary.cs
Normal file
663
SpineRuntimes/SpineRuntime21/SkeletonBinary.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
215
SpineRuntimes/SpineRuntime21/SkeletonBounds.cs
Normal file
215
SpineRuntimes/SpineRuntime21/SkeletonBounds.cs
Normal 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];
|
||||
}
|
||||
}
|
||||
}
|
||||
158
SpineRuntimes/SpineRuntime21/SkeletonData.cs
Normal file
158
SpineRuntimes/SpineRuntime21/SkeletonData.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
641
SpineRuntimes/SpineRuntime21/SkeletonJson.cs
Normal file
641
SpineRuntimes/SpineRuntime21/SkeletonJson.cs
Normal 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]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
102
SpineRuntimes/SpineRuntime21/Skin.cs
Normal file
102
SpineRuntimes/SpineRuntime21/Skin.cs
Normal file
@@ -0,0 +1,102 @@
|
||||
/******************************************************************************
|
||||
* 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 Dictionary<KeyValuePair<int, String>, Attachment> Attachments { get { return attachments; } }
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
99
SpineRuntimes/SpineRuntime21/Slot.cs
Normal file
99
SpineRuntimes/SpineRuntime21/Slot.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
62
SpineRuntimes/SpineRuntime21/SlotData.cs
Normal file
62
SpineRuntimes/SpineRuntime21/SlotData.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
13
SpineRuntimes/SpineRuntime21/SpineRuntime21.csproj
Normal file
13
SpineRuntimes/SpineRuntime21/SpineRuntime21.csproj
Normal file
@@ -0,0 +1,13 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<Platforms>x64</Platforms>
|
||||
<TargetFramework>net8.0-windows</TargetFramework>
|
||||
<BaseOutputPath>$(SolutionDir)out</BaseOutputPath>
|
||||
<IncludeSourceRevisionInInformationalVersion>false</IncludeSourceRevisionInInformationalVersion>
|
||||
<Version>2.1.25</Version>
|
||||
</PropertyGroup>
|
||||
|
||||
</Project>
|
||||
@@ -152,27 +152,13 @@ namespace SpineRuntime36 {
|
||||
|
||||
Bone parent = this.parent;
|
||||
if (parent == null) { // Root bone.
|
||||
float rotationY = rotation + 90 + shearY;
|
||||
float la = MathUtils.CosDeg(rotation + shearX) * scaleX;
|
||||
float lb = MathUtils.CosDeg(rotationY) * scaleY;
|
||||
float lc = MathUtils.SinDeg(rotation + shearX) * scaleX;
|
||||
float ld = MathUtils.SinDeg(rotationY) * scaleY;
|
||||
if (skeleton.flipX) {
|
||||
x = -x;
|
||||
la = -la;
|
||||
lb = -lb;
|
||||
}
|
||||
if (skeleton.flipY != yDown) {
|
||||
y = -y;
|
||||
lc = -lc;
|
||||
ld = -ld;
|
||||
}
|
||||
a = la;
|
||||
b = lb;
|
||||
c = lc;
|
||||
d = ld;
|
||||
worldX = x + skeleton.x;
|
||||
worldY = y + skeleton.y;
|
||||
float rotationY = rotation + 90 + shearY, sx = skeleton.scaleX, sy = skeleton.scaleY;
|
||||
a = MathUtils.CosDeg(rotation + shearX) * scaleX * sx;
|
||||
b = MathUtils.CosDeg(rotationY) * scaleY * sx;
|
||||
c = MathUtils.SinDeg(rotation + shearX) * scaleX * sy;
|
||||
d = MathUtils.SinDeg(rotationY) * scaleY * sy;
|
||||
worldX = x * sx + skeleton.x;
|
||||
worldY = y * sy + skeleton.y;
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -228,13 +214,16 @@ namespace SpineRuntime36 {
|
||||
case TransformMode.NoScale:
|
||||
case TransformMode.NoScaleOrReflection: {
|
||||
float cos = MathUtils.CosDeg(rotation), sin = MathUtils.SinDeg(rotation);
|
||||
float za = pa * cos + pb * sin;
|
||||
float zc = pc * cos + pd * sin;
|
||||
float za = (pa * cos + pb * sin) / skeleton.scaleX;
|
||||
float zc = (pc * cos + pd * sin) / skeleton.scaleY;
|
||||
float s = (float)Math.Sqrt(za * za + zc * zc);
|
||||
if (s > 0.00001f) s = 1 / s;
|
||||
za *= s;
|
||||
zc *= s;
|
||||
s = (float)Math.Sqrt(za * za + zc * zc);
|
||||
if (data.transformMode == TransformMode.NoScale
|
||||
&& (pa * pd - pb * pc < 0) != (skeleton.scaleX < 0 != skeleton.scaleY < 0)) s = -s;
|
||||
|
||||
float r = MathUtils.PI / 2 + MathUtils.Atan2(zc, za);
|
||||
float zb = MathUtils.Cos(r) * s;
|
||||
float zd = MathUtils.Sin(r) * s;
|
||||
@@ -242,26 +231,18 @@ namespace SpineRuntime36 {
|
||||
float lb = MathUtils.CosDeg(90 + shearY) * scaleY;
|
||||
float lc = MathUtils.SinDeg(shearX) * scaleX;
|
||||
float ld = MathUtils.SinDeg(90 + shearY) * scaleY;
|
||||
if (data.transformMode != TransformMode.NoScaleOrReflection? pa * pd - pb* pc< 0 : skeleton.flipX != skeleton.flipY) {
|
||||
zb = -zb;
|
||||
zd = -zd;
|
||||
}
|
||||
a = za * la + zb * lc;
|
||||
b = za * lb + zb * ld;
|
||||
c = zc * la + zd * lc;
|
||||
d = zc * lb + zd * ld;
|
||||
return;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (skeleton.flipX) {
|
||||
a = -a;
|
||||
b = -b;
|
||||
}
|
||||
if (skeleton.flipY != Bone.yDown) {
|
||||
c = -c;
|
||||
d = -d;
|
||||
}
|
||||
a *= skeleton.scaleX;
|
||||
b *= skeleton.scaleX;
|
||||
c *= skeleton.scaleY;
|
||||
d *= skeleton.scaleY;
|
||||
}
|
||||
|
||||
public void SetToSetupPose () {
|
||||
|
||||
@@ -45,7 +45,7 @@ namespace SpineRuntime36 {
|
||||
internal Skin skin;
|
||||
internal float r = 1, g = 1, b = 1, a = 1;
|
||||
internal float time;
|
||||
internal bool flipX, flipY;
|
||||
internal float scaleX = 1, scaleY = 1;
|
||||
internal float x, y;
|
||||
|
||||
public SkeletonData Data { get { return data; } }
|
||||
@@ -64,8 +64,14 @@ namespace SpineRuntime36 {
|
||||
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 float ScaleX { get { return scaleX; } set { scaleX = value; } }
|
||||
public float ScaleY { get { return scaleY; } set { scaleY = value; } }
|
||||
|
||||
[Obsolete("Use ScaleX instead. FlipX is when ScaleX is negative.")]
|
||||
public bool FlipX { get { return scaleX < 0; } set { scaleX = value ? -1f : 1f; } }
|
||||
|
||||
[Obsolete("Use ScaleY instead. FlipY is when ScaleY is negative.")]
|
||||
public bool FlipY { get { return scaleY < 0; } set { scaleY = value ? -1f : 1f; } }
|
||||
|
||||
public Bone RootBone {
|
||||
get { return bones.Count == 0 ? null : bones.Items[0]; }
|
||||
|
||||
@@ -5,7 +5,6 @@
|
||||
<Nullable>enable</Nullable>
|
||||
<Platforms>x64</Platforms>
|
||||
<TargetFramework>net8.0-windows</TargetFramework>
|
||||
<RuntimeIdentifier>win-x64</RuntimeIdentifier>
|
||||
<BaseOutputPath>$(SolutionDir)out</BaseOutputPath>
|
||||
<IncludeSourceRevisionInInformationalVersion>false</IncludeSourceRevisionInInformationalVersion>
|
||||
<Version>3.6.53</Version>
|
||||
|
||||
@@ -5,7 +5,6 @@
|
||||
<Nullable>enable</Nullable>
|
||||
<Platforms>x64</Platforms>
|
||||
<TargetFramework>net8.0-windows</TargetFramework>
|
||||
<RuntimeIdentifier>win-x64</RuntimeIdentifier>
|
||||
<BaseOutputPath>$(SolutionDir)out</BaseOutputPath>
|
||||
<IncludeSourceRevisionInInformationalVersion>false</IncludeSourceRevisionInInformationalVersion>
|
||||
<Version>3.7.94</Version>
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -5,7 +5,6 @@
|
||||
<Nullable>enable</Nullable>
|
||||
<Platforms>x64</Platforms>
|
||||
<TargetFramework>net8.0-windows</TargetFramework>
|
||||
<RuntimeIdentifier>win-x64</RuntimeIdentifier>
|
||||
<BaseOutputPath>$(SolutionDir)out</BaseOutputPath>
|
||||
<IncludeSourceRevisionInInformationalVersion>false</IncludeSourceRevisionInInformationalVersion>
|
||||
<Version>3.8.99</Version>
|
||||
|
||||
@@ -5,7 +5,6 @@
|
||||
<Nullable>enable</Nullable>
|
||||
<Platforms>x64</Platforms>
|
||||
<TargetFramework>net8.0-windows</TargetFramework>
|
||||
<RuntimeIdentifier>win-x64</RuntimeIdentifier>
|
||||
<BaseOutputPath>$(SolutionDir)out</BaseOutputPath>
|
||||
<IncludeSourceRevisionInInformationalVersion>false</IncludeSourceRevisionInInformationalVersion>
|
||||
<Version>4.0.64</Version>
|
||||
|
||||
@@ -5,7 +5,6 @@
|
||||
<Nullable>enable</Nullable>
|
||||
<Platforms>x64</Platforms>
|
||||
<TargetFramework>net8.0-windows</TargetFramework>
|
||||
<RuntimeIdentifier>win-x64</RuntimeIdentifier>
|
||||
<BaseOutputPath>$(SolutionDir)out</BaseOutputPath>
|
||||
<IncludeSourceRevisionInInformationalVersion>false</IncludeSourceRevisionInInformationalVersion>
|
||||
<Version>4.1.54</Version>
|
||||
|
||||
@@ -5,7 +5,6 @@
|
||||
<Nullable>enable</Nullable>
|
||||
<Platforms>x64</Platforms>
|
||||
<TargetFramework>net8.0-windows</TargetFramework>
|
||||
<RuntimeIdentifier>win-x64</RuntimeIdentifier>
|
||||
<BaseOutputPath>$(SolutionDir)out</BaseOutputPath>
|
||||
<IncludeSourceRevisionInInformationalVersion>false</IncludeSourceRevisionInInformationalVersion>
|
||||
<Version>4.2.74</Version>
|
||||
|
||||
@@ -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}
|
||||
|
||||
197
SpineViewer/Controls/SkelFileListBox.Designer.cs
generated
Normal file
197
SpineViewer/Controls/SkelFileListBox.Designer.cs
generated
Normal 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";
|
||||
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;
|
||||
}
|
||||
}
|
||||
125
SpineViewer/Controls/SkelFileListBox.cs
Normal file
125
SpineViewer/Controls/SkelFileListBox.cs
Normal file
@@ -0,0 +1,125 @@
|
||||
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;
|
||||
using SpineViewer.Spine;
|
||||
|
||||
namespace SpineViewer.Controls
|
||||
{
|
||||
public partial class SkelFileListBox : UserControl
|
||||
{
|
||||
public SkelFileListBox()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// ListBox.Items
|
||||
/// </summary>
|
||||
[DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
|
||||
[Browsable(false)]
|
||||
public ListBox.ObjectCollection Items { get => listBox.Items; }
|
||||
|
||||
/// <summary>
|
||||
/// 从路径列表添加
|
||||
/// </summary>
|
||||
private void AddFromFileDrop(string[] paths)
|
||||
{
|
||||
foreach (var path in paths)
|
||||
{
|
||||
if (File.Exists(path))
|
||||
{
|
||||
if (SpineUtils.CommonSkelSuffix.Contains(Path.GetExtension(path).ToLower()))
|
||||
listBox.Items.Add(Path.GetFullPath(path));
|
||||
}
|
||||
else if (Directory.Exists(path))
|
||||
{
|
||||
foreach (var file in Directory.EnumerateFiles(path, "*.*", SearchOption.AllDirectories))
|
||||
{
|
||||
if (SpineUtils.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 (SpineUtils.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} 个文件";
|
||||
}
|
||||
}
|
||||
}
|
||||
129
SpineViewer/Controls/SkelFileListBox.resx
Normal file
129
SpineViewer/Controls/SkelFileListBox.resx
Normal 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>
|
||||
248
SpineViewer/Controls/SpineListView.Designer.cs
generated
248
SpineViewer/Controls/SpineListView.Designer.cs
generated
@@ -36,118 +36,277 @@
|
||||
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_CopyPreview = new ToolStripMenuItem();
|
||||
toolStripMenuItem_AddFromClipboard = new ToolStripMenuItem();
|
||||
toolStripMenuItem_SelectAll = 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);
|
||||
timer_SelectedIndexChangedDebounce = new System.Windows.Forms.Timer(components);
|
||||
statusStrip = new StatusStrip();
|
||||
toolStripStatusLabel_CountInfo = new ToolStripStatusLabel();
|
||||
tableLayoutPanel = new TableLayoutPanel();
|
||||
contextMenuStrip.SuspendLayout();
|
||||
statusStrip.SuspendLayout();
|
||||
tableLayoutPanel.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.Margin = new Padding(0);
|
||||
listView.Name = "listView";
|
||||
listView.ShowItemToolTips = true;
|
||||
listView.Size = new Size(336, 445);
|
||||
listView.Size = new Size(336, 414);
|
||||
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
|
||||
//
|
||||
columnHeader_Name.Text = "名称";
|
||||
columnHeader_Name.Width = 220;
|
||||
columnHeader_Name.Width = 300;
|
||||
//
|
||||
// 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(255, 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(254, 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(254, 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(254, 30);
|
||||
toolStripMenuItem_Remove.Text = "移除";
|
||||
toolStripMenuItem_Remove.Click += toolStripMenuItem_Remove_Click;
|
||||
//
|
||||
// toolStripSeparator1
|
||||
//
|
||||
toolStripSeparator1.Name = "toolStripSeparator1";
|
||||
toolStripSeparator1.Size = new Size(184, 6);
|
||||
toolStripSeparator1.Size = new Size(251, 6);
|
||||
//
|
||||
// toolStripMenuItem_BatchAdd
|
||||
//
|
||||
toolStripMenuItem_BatchAdd.Name = "toolStripMenuItem_BatchAdd";
|
||||
toolStripMenuItem_BatchAdd.Size = new Size(254, 30);
|
||||
toolStripMenuItem_BatchAdd.Text = "批量添加...";
|
||||
toolStripMenuItem_BatchAdd.Click += toolStripMenuItem_BatchAdd_Click;
|
||||
//
|
||||
// toolStripMenuItem_RemoveAll
|
||||
//
|
||||
toolStripMenuItem_RemoveAll.Name = "toolStripMenuItem_RemoveAll";
|
||||
toolStripMenuItem_RemoveAll.Size = new Size(254, 30);
|
||||
toolStripMenuItem_RemoveAll.Text = "移除全部";
|
||||
toolStripMenuItem_RemoveAll.Click += toolStripMenuItem_RemoveAll_Click;
|
||||
//
|
||||
// toolStripSeparator2
|
||||
//
|
||||
toolStripSeparator2.Name = "toolStripSeparator2";
|
||||
toolStripSeparator2.Size = new Size(251, 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(254, 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(254, 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(254, 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(254, 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(251, 6);
|
||||
//
|
||||
// toolStripMenuItem_CopyPreview
|
||||
//
|
||||
toolStripMenuItem_CopyPreview.Name = "toolStripMenuItem_CopyPreview";
|
||||
toolStripMenuItem_CopyPreview.ShortcutKeys = Keys.Control | Keys.C;
|
||||
toolStripMenuItem_CopyPreview.Size = new Size(254, 30);
|
||||
toolStripMenuItem_CopyPreview.Text = "复制预览图";
|
||||
toolStripMenuItem_CopyPreview.Click += toolStripMenuItem_CopyPreview_Click;
|
||||
//
|
||||
// toolStripMenuItem_AddFromClipboard
|
||||
//
|
||||
toolStripMenuItem_AddFromClipboard.Name = "toolStripMenuItem_AddFromClipboard";
|
||||
toolStripMenuItem_AddFromClipboard.ShortcutKeys = Keys.Control | Keys.V;
|
||||
toolStripMenuItem_AddFromClipboard.Size = new Size(254, 30);
|
||||
toolStripMenuItem_AddFromClipboard.Text = "从剪贴板添加";
|
||||
toolStripMenuItem_AddFromClipboard.Click += toolStripMenuItem_AddFromClipboard_Click;
|
||||
//
|
||||
// toolStripMenuItem_SelectAll
|
||||
//
|
||||
toolStripMenuItem_SelectAll.Name = "toolStripMenuItem_SelectAll";
|
||||
toolStripMenuItem_SelectAll.ShortcutKeys = Keys.Control | Keys.A;
|
||||
toolStripMenuItem_SelectAll.Size = new Size(254, 30);
|
||||
toolStripMenuItem_SelectAll.Text = "全选";
|
||||
toolStripMenuItem_SelectAll.Click += toolStripMenuItem_SelectAll_Click;
|
||||
//
|
||||
// toolStripSeparator4
|
||||
//
|
||||
toolStripSeparator4.Name = "toolStripSeparator4";
|
||||
toolStripSeparator4.Size = new Size(251, 6);
|
||||
//
|
||||
// toolStripMenuItem_ChangeView
|
||||
//
|
||||
toolStripMenuItem_ChangeView.DropDownItems.AddRange(new ToolStripItem[] { toolStripMenuItem_LargeIconView, toolStripMenuItem_ListView, toolStripMenuItem_DetailsView });
|
||||
toolStripMenuItem_ChangeView.Name = "toolStripMenuItem_ChangeView";
|
||||
toolStripMenuItem_ChangeView.Size = new Size(254, 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;
|
||||
//
|
||||
// timer_SelectedIndexChangedDebounce
|
||||
//
|
||||
timer_SelectedIndexChangedDebounce.Interval = 30;
|
||||
timer_SelectedIndexChangedDebounce.Tick += timer_SelectedIndexChangedDebounce_Tick;
|
||||
//
|
||||
// statusStrip
|
||||
//
|
||||
statusStrip.Dock = DockStyle.Fill;
|
||||
statusStrip.ImageScalingSize = new Size(24, 24);
|
||||
statusStrip.Items.AddRange(new ToolStripItem[] { toolStripStatusLabel_CountInfo });
|
||||
statusStrip.Location = new Point(0, 414);
|
||||
statusStrip.Name = "statusStrip";
|
||||
statusStrip.Size = new Size(336, 31);
|
||||
statusStrip.SizingGrip = false;
|
||||
statusStrip.TabIndex = 2;
|
||||
statusStrip.Text = "statusStrip1";
|
||||
//
|
||||
// toolStripStatusLabel_CountInfo
|
||||
//
|
||||
toolStripStatusLabel_CountInfo.Name = "toolStripStatusLabel_CountInfo";
|
||||
toolStripStatusLabel_CountInfo.Size = new Size(178, 24);
|
||||
toolStripStatusLabel_CountInfo.Text = "已选择 0 项,共 0 项";
|
||||
//
|
||||
// tableLayoutPanel
|
||||
//
|
||||
tableLayoutPanel.ColumnCount = 1;
|
||||
tableLayoutPanel.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 100F));
|
||||
tableLayoutPanel.Controls.Add(listView, 0, 0);
|
||||
tableLayoutPanel.Controls.Add(statusStrip, 0, 1);
|
||||
tableLayoutPanel.Dock = DockStyle.Fill;
|
||||
tableLayoutPanel.Location = new Point(0, 0);
|
||||
tableLayoutPanel.Name = "tableLayoutPanel";
|
||||
tableLayoutPanel.RowCount = 2;
|
||||
tableLayoutPanel.RowStyles.Add(new RowStyle(SizeType.Percent, 100F));
|
||||
tableLayoutPanel.RowStyles.Add(new RowStyle());
|
||||
tableLayoutPanel.Size = new Size(336, 445);
|
||||
tableLayoutPanel.TabIndex = 3;
|
||||
//
|
||||
// SpineListView
|
||||
//
|
||||
AutoScaleDimensions = new SizeF(11F, 24F);
|
||||
AutoScaleMode = AutoScaleMode.Font;
|
||||
Controls.Add(listView);
|
||||
Controls.Add(tableLayoutPanel);
|
||||
Name = "SpineListView";
|
||||
Size = new Size(336, 445);
|
||||
contextMenuStrip.ResumeLayout(false);
|
||||
statusStrip.ResumeLayout(false);
|
||||
statusStrip.PerformLayout();
|
||||
tableLayoutPanel.ResumeLayout(false);
|
||||
tableLayoutPanel.PerformLayout();
|
||||
ResumeLayout(false);
|
||||
}
|
||||
|
||||
@@ -164,5 +323,22 @@
|
||||
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;
|
||||
private System.Windows.Forms.Timer timer_SelectedIndexChangedDebounce;
|
||||
private StatusStrip statusStrip;
|
||||
private ToolStripStatusLabel toolStripStatusLabel_CountInfo;
|
||||
private TableLayoutPanel tableLayoutPanel;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,23 +11,35 @@ using System.Collections.ObjectModel;
|
||||
using SpineViewer.Spine;
|
||||
using System.Reflection;
|
||||
using System.Diagnostics;
|
||||
using System.Collections.Specialized;
|
||||
using NLog;
|
||||
using SpineViewer.Extensions;
|
||||
using SpineViewer.Utils;
|
||||
using SpineViewer.Spine.SpineView;
|
||||
|
||||
namespace SpineViewer.Controls
|
||||
{
|
||||
public partial class SpineListView : UserControl
|
||||
{
|
||||
[Category("自定义"), Description("用于显示骨骼属性的属性页")]
|
||||
public PropertyGrid? PropertyGrid { get; set; }
|
||||
/// <summary>
|
||||
/// 日志器
|
||||
/// </summary>
|
||||
private readonly Logger logger = LogManager.GetCurrentClassLogger();
|
||||
|
||||
/// <summary>
|
||||
/// 获取数组快照, 访问时必须使用 lock 语句锁定对象本身
|
||||
/// Spine 列表只读视图, 访问时必须使用 lock 语句锁定视图本身
|
||||
/// </summary>
|
||||
public readonly ReadOnlyCollection<Spine.Spine> Spines;
|
||||
public readonly ReadOnlyCollection<SpineObject> Spines;
|
||||
|
||||
/// <summary>
|
||||
/// Spine 列表, 访问时必须使用 lock 语句锁定 Spines
|
||||
/// Spine 列表, 访问时必须使用 lock 语句锁定只读视图 Spines
|
||||
/// </summary>
|
||||
private readonly List<Spine.Spine> spines = [];
|
||||
private readonly List<SpineObject> spines = [];
|
||||
|
||||
/// <summary>
|
||||
/// 用于属性页显示模型参数的包装类
|
||||
/// </summary>
|
||||
private readonly Dictionary<string, SpineObjectProperty> spinePropertyWrappers = [];
|
||||
|
||||
public SpineListView()
|
||||
{
|
||||
@@ -36,17 +48,39 @@ namespace SpineViewer.Controls
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 弹出添加对话框在指定位置之前插入一项
|
||||
/// 显示骨骼信息的属性面板
|
||||
/// </summary>
|
||||
[Category("自定义"), Description("用于显示模型属性的组合属性页")]
|
||||
public SpineViewPropertyGrid? SpinePropertyGrid { get; set; }
|
||||
|
||||
/// <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;
|
||||
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 = SpineObject.New(result.Version, result.SkelPath, result.AtlasPath);
|
||||
|
||||
// 如果索引无效则在末尾添加
|
||||
if (index < 0 || index > listView.Items.Count)
|
||||
@@ -54,7 +88,10 @@ namespace SpineViewer.Controls
|
||||
|
||||
// 锁定外部的读操作
|
||||
lock (Spines) { spines.Insert(index, spine); }
|
||||
listView.Items.Insert(index, new ListViewItem(spine.Name) { ToolTipText = spine.SkelPath });
|
||||
spinePropertyWrappers[spine.ID] = new(spine);
|
||||
listView.SmallImageList.Images.Add(spine.ID, spine.Preview);
|
||||
listView.LargeImageList.Images.Add(spine.ID, spine.Preview);
|
||||
listView.Items.Insert(index, new ListViewItem(spine.Name, spine.ID) { ToolTipText = spine.SkelPath });
|
||||
|
||||
// 选中新增项
|
||||
listView.SelectedIndices.Clear();
|
||||
@@ -62,20 +99,12 @@ 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);
|
||||
logger.Error(ex.ToString());
|
||||
logger.Error("Failed to load {} {}", result.SkelPath, result.AtlasPath);
|
||||
MessagePopup.Error(ex.ToString(), "骨骼加载失败");
|
||||
}
|
||||
|
||||
Program.Logger.Info($"Current memory usage: {Program.Process.WorkingSet64 / 1024.0 / 1024.0:F2} MB");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 弹出添加对话框
|
||||
/// </summary>
|
||||
public void Add()
|
||||
{
|
||||
Insert();
|
||||
logger.LogCurrentProcessMemoryUsage();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -84,19 +113,28 @@ namespace SpineViewer.Controls
|
||||
public void BatchAdd()
|
||||
{
|
||||
var openDialog = new Dialogs.BatchOpenSpineDialog();
|
||||
if (openDialog.ShowDialog() != DialogResult.OK)
|
||||
return;
|
||||
if (openDialog.ShowDialog() != DialogResult.OK) return;
|
||||
BatchAdd(openDialog.Result);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 从结果批量添加
|
||||
/// </summary>
|
||||
private 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;
|
||||
|
||||
@@ -117,60 +155,128 @@ namespace SpineViewer.Controls
|
||||
|
||||
try
|
||||
{
|
||||
var spine = Spine.Spine.New(version, skelPath);
|
||||
var spine = SpineObject.New(version, skelPath);
|
||||
var preview = spine.Preview;
|
||||
lock (Spines) { spines.Add(spine); }
|
||||
listView.Invoke(() => listView.Items.Add(new ListViewItem(spine.Name) { ToolTipText = spine.SkelPath }));
|
||||
spinePropertyWrappers[spine.ID] = new(spine);
|
||||
listView.Invoke(() =>
|
||||
{
|
||||
listView.SmallImageList.Images.Add(spine.ID, preview);
|
||||
listView.LargeImageList.Images.Add(spine.ID, preview);
|
||||
listView.Items.Add(new ListViewItem(spine.Name, spine.ID) { ToolTipText = spine.SkelPath });
|
||||
});
|
||||
success++;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Program.Logger.Error(ex.ToString());
|
||||
Program.Logger.Error("Failed to load {}", skelPath);
|
||||
logger.Error(ex.ToString());
|
||||
logger.Error("Failed to load {}", skelPath);
|
||||
error++;
|
||||
}
|
||||
|
||||
worker.ReportProgress((int)((i + 1) * 100.0) / totalCount, $"已处理 {i + 1}/{totalCount}");
|
||||
}
|
||||
|
||||
if (error > 0)
|
||||
// 选中最后一项
|
||||
listView.Invoke(() =>
|
||||
{
|
||||
Program.Logger.Warn("Batch load {} successfully, {} failed", success, error);
|
||||
if (listView.Items.Count > 0)
|
||||
{
|
||||
listView.SelectedIndices.Clear();
|
||||
listView.SelectedIndices.Add(listView.Items.Count - 1);
|
||||
}
|
||||
});
|
||||
|
||||
if (error > 0)
|
||||
logger.Warn("Batch load {} successfully, {} failed", success, error);
|
||||
else
|
||||
{
|
||||
Program.Logger.Info("{} skel loaded successfully", success);
|
||||
logger.Info("{} skel loaded successfully", success);
|
||||
|
||||
logger.LogCurrentProcessMemoryUsage();
|
||||
}
|
||||
|
||||
Program.Logger.Info($"Current memory usage: {Program.Process.WorkingSet64 / 1024.0 / 1024.0:F2} MB");
|
||||
/// <summary>
|
||||
/// 从拖放/复制的路径列表添加
|
||||
/// </summary>
|
||||
private void AddFromFileDrop(IEnumerable<string> paths)
|
||||
{
|
||||
List<string> validPaths = [];
|
||||
foreach (var path in paths)
|
||||
{
|
||||
if (File.Exists(path))
|
||||
{
|
||||
if (SpineUtils.CommonSkelSuffix.Contains(Path.GetExtension(path).ToLower()))
|
||||
validPaths.Add(path);
|
||||
}
|
||||
else if (Directory.Exists(path))
|
||||
{
|
||||
foreach (var file in Directory.EnumerateFiles(path, "*.*", SearchOption.AllDirectories))
|
||||
{
|
||||
if (SpineUtils.CommonSkelSuffix.Contains(Path.GetExtension(file).ToLower()))
|
||||
validPaths.Add(file);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (validPaths.Count > 1)
|
||||
{
|
||||
if (validPaths.Count > 100)
|
||||
{
|
||||
if (MessagePopup.Quest($"共发现 {validPaths.Count} 个可加载骨骼,数量较多,是否一次性全部加载?") == DialogResult.Cancel)
|
||||
return;
|
||||
}
|
||||
BatchAdd(new Dialogs.BatchOpenSpineDialogResult(SpineVersion.Auto, validPaths.ToArray()));
|
||||
}
|
||||
else if (validPaths.Count > 0)
|
||||
{
|
||||
Insert(new Dialogs.OpenSpineDialogResult(SpineVersion.Auto, validPaths[0]));
|
||||
}
|
||||
}
|
||||
|
||||
private void listView_SelectedIndexChanged(object sender, EventArgs e)
|
||||
{
|
||||
if (PropertyGrid is not null)
|
||||
timer_SelectedIndexChangedDebounce.Stop();
|
||||
timer_SelectedIndexChangedDebounce.Start();
|
||||
}
|
||||
|
||||
private void timer_SelectedIndexChangedDebounce_Tick(object sender, EventArgs e)
|
||||
{
|
||||
timer_SelectedIndexChangedDebounce.Stop();
|
||||
_listView_SelectedIndexChanged(listView, EventArgs.Empty);
|
||||
}
|
||||
|
||||
private void _listView_SelectedIndexChanged(object sender, EventArgs e)
|
||||
{
|
||||
lock (Spines)
|
||||
{
|
||||
if (SpinePropertyGrid is not null)
|
||||
{
|
||||
if (listView.SelectedIndices.Count <= 0)
|
||||
PropertyGrid.SelectedObject = null;
|
||||
SpinePropertyGrid.SelectedSpines = null;
|
||||
else if (listView.SelectedIndices.Count <= 1)
|
||||
PropertyGrid.SelectedObject = spines[listView.SelectedIndices[0]];
|
||||
SpinePropertyGrid.SelectedSpines = [spinePropertyWrappers[spines[listView.SelectedIndices[0]].ID]];
|
||||
else
|
||||
PropertyGrid.SelectedObjects = listView.SelectedIndices.Cast<int>().Select(index => spines[index]).ToArray();
|
||||
}
|
||||
}
|
||||
SpinePropertyGrid.SelectedSpines = listView.SelectedIndices.Cast<int>().Select(index => spinePropertyWrappers[spines[index].ID]).ToArray();
|
||||
}
|
||||
|
||||
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();
|
||||
|
||||
toolStripStatusLabel_CountInfo.Text = $"已选择 {listView.SelectedItems.Count} 项,共 {listView.Items.Count} 项";
|
||||
}
|
||||
|
||||
private void listView_ItemDrag(object sender, ItemDragEventArgs e)
|
||||
@@ -178,11 +284,20 @@ 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;
|
||||
|
||||
if (e.Data.GetDataPresent(DataFormats.Serializable))
|
||||
{
|
||||
// 获取鼠标位置并确定目标索引
|
||||
var point = listView.PointToClient(new(e.X, e.Y));
|
||||
var targetItem = listView.GetItemAt(point.X, point.Y);
|
||||
@@ -197,8 +312,11 @@ namespace SpineViewer.Controls
|
||||
targetItem.BackColor = Color.LightGray;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void listView_DragDrop(object sender, DragEventArgs e)
|
||||
{
|
||||
if (e.Data.GetDataPresent(DataFormats.Serializable))
|
||||
{
|
||||
// 获取拖放源项和目标项
|
||||
var draggedItem = (ListViewItem)e.Data.GetData(typeof(ListViewItem));
|
||||
@@ -236,16 +354,38 @@ namespace SpineViewer.Controls
|
||||
item.BackColor = listView.BackColor;
|
||||
}
|
||||
}
|
||||
else if (e.Data.GetDataPresent(DataFormats.FileDrop))
|
||||
{
|
||||
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_CopyPreview.Enabled = selectedCount > 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 +411,44 @@ namespace SpineViewer.Controls
|
||||
|
||||
if (listView.SelectedIndices.Count > 1)
|
||||
{
|
||||
if (MessageBox.Show($"确定移除所选 {listView.SelectedIndices.Count} 项吗?", "操作确认", MessageBoxButtons.OKCancel, MessageBoxIcon.Question) != DialogResult.OK)
|
||||
if (MessagePopup.Quest($"确定移除所选 {listView.SelectedIndices.Count} 项吗?") != DialogResult.OK)
|
||||
return;
|
||||
}
|
||||
|
||||
lock (Spines)
|
||||
{
|
||||
listView.BeginUpdate();
|
||||
foreach (var i in listView.SelectedIndices.Cast<int>().OrderByDescending(x => x))
|
||||
{
|
||||
listView.Items.RemoveAt(i);
|
||||
var spine = spines[i];
|
||||
spines.RemoveAt(i);
|
||||
spinePropertyWrappers.Remove(spine.ID);
|
||||
listView.SmallImageList.Images.RemoveByKey(spine.ID);
|
||||
listView.LargeImageList.Images.RemoveByKey(spine.ID);
|
||||
spine.Dispose();
|
||||
}
|
||||
listView.EndUpdate();
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
{
|
||||
spines[i].Dispose();
|
||||
spines.RemoveAt(i);
|
||||
var spine = spines[index];
|
||||
spines.RemoveAt(index);
|
||||
spines.Insert(0, spine);
|
||||
}
|
||||
listView.Items.RemoveAt(i);
|
||||
var item = listView.Items[index];
|
||||
listView.Items.RemoveAt(index);
|
||||
listView.Items.Insert(0, item);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -296,8 +462,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 +478,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 +511,80 @@ namespace SpineViewer.Controls
|
||||
if (listView.Items.Count <= 0)
|
||||
return;
|
||||
|
||||
if (MessageBox.Show($"确认移除所有 {listView.Items.Count} 项吗?", "操作确认", MessageBoxButtons.OKCancel, MessageBoxIcon.Question) != DialogResult.OK)
|
||||
if (MessagePopup.Quest($"确认移除所有 {listView.Items.Count} 项吗?") != DialogResult.OK)
|
||||
return;
|
||||
|
||||
listView.Items.Clear();
|
||||
lock (Spines)
|
||||
{
|
||||
foreach (var spine in spines) spine.Dispose();
|
||||
spines.Clear();
|
||||
spinePropertyWrappers.Clear();
|
||||
listView.SmallImageList.Images.Clear();
|
||||
listView.LargeImageList.Images.Clear();
|
||||
}
|
||||
if (SpinePropertyGrid is not null)
|
||||
SpinePropertyGrid.SelectedSpines = null;
|
||||
}
|
||||
|
||||
private void toolStripMenuItem_CopyPreview_Click(object sender, EventArgs e)
|
||||
{
|
||||
var fileDropList = new StringCollection();
|
||||
var tempDir = Path.Combine(Path.GetTempPath(), Process.GetCurrentProcess().ProcessName);
|
||||
Directory.CreateDirectory(tempDir);
|
||||
|
||||
lock (Spines)
|
||||
{
|
||||
foreach (var spine in spines)
|
||||
spine.Dispose();
|
||||
spines.Clear();
|
||||
foreach (int i in listView.SelectedIndices)
|
||||
{
|
||||
var a = Process.GetCurrentProcess();
|
||||
var spine = spines[i];
|
||||
var path = Path.Combine(tempDir, $"{spine.ID}.png");
|
||||
spine.Preview.Save(path);
|
||||
fileDropList.Add(path);
|
||||
}
|
||||
listView.Items.Clear();
|
||||
if (PropertyGrid is not null)
|
||||
PropertyGrid.SelectedObject = null;
|
||||
}
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
public class DefaultSpineConfig
|
||||
{
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -120,4 +120,16 @@
|
||||
<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>
|
||||
<metadata name="timer_SelectedIndexChangedDebounce.TrayLocation" type="System.Drawing.Point, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a">
|
||||
<value>771, 24</value>
|
||||
</metadata>
|
||||
<metadata name="statusStrip.TrayLocation" type="System.Drawing.Point, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a">
|
||||
<value>1176, 24</value>
|
||||
</metadata>
|
||||
</root>
|
||||
291
SpineViewer/Controls/SpinePreviewPanel.Designer.cs
generated
Normal file
291
SpineViewer/Controls/SpinePreviewPanel.Designer.cs
generated
Normal file
@@ -0,0 +1,291 @@
|
||||
namespace SpineViewer.Controls
|
||||
{
|
||||
partial class SpinePreviewPanel
|
||||
{
|
||||
/// <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();
|
||||
System.ComponentModel.ComponentResourceManager resources = new System.ComponentModel.ComponentResourceManager(typeof(SpinePreviewPanel));
|
||||
panel_Render = new Panel();
|
||||
tableLayoutPanel1 = new TableLayoutPanel();
|
||||
flowLayoutPanel1 = new FlowLayoutPanel();
|
||||
button_Stop = new Button();
|
||||
imageList = new ImageList(components);
|
||||
button_Restart = new Button();
|
||||
button_Start = new Button();
|
||||
button_ForwardStep = new Button();
|
||||
button_ForwardFast = new Button();
|
||||
button_FullScreen = new Button();
|
||||
panel_ViewContainer = new Panel();
|
||||
panel_RenderContainer = new Panel();
|
||||
toolTip = new ToolTip(components);
|
||||
spinePreviewFullScreenForm = new SpineViewer.Forms.SpinePreviewFullScreenForm();
|
||||
wallpaperForm = new WallpaperForm();
|
||||
tableLayoutPanel1.SuspendLayout();
|
||||
flowLayoutPanel1.SuspendLayout();
|
||||
panel_ViewContainer.SuspendLayout();
|
||||
panel_RenderContainer.SuspendLayout();
|
||||
SuspendLayout();
|
||||
//
|
||||
// panel_Render
|
||||
//
|
||||
panel_Render.BackColor = SystemColors.ControlDarkDark;
|
||||
panel_Render.Location = new Point(157, 136);
|
||||
panel_Render.Margin = new Padding(0);
|
||||
panel_Render.Name = "panel_Render";
|
||||
panel_Render.Size = new Size(320, 320);
|
||||
panel_Render.TabIndex = 1;
|
||||
panel_Render.MouseDown += panel_Render_MouseDown;
|
||||
panel_Render.MouseMove += panel_Render_MouseMove;
|
||||
panel_Render.MouseUp += panel_Render_MouseUp;
|
||||
panel_Render.MouseWheel += panel_Render_MouseWheel;
|
||||
//
|
||||
// tableLayoutPanel1
|
||||
//
|
||||
tableLayoutPanel1.ColumnCount = 1;
|
||||
tableLayoutPanel1.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 100F));
|
||||
tableLayoutPanel1.Controls.Add(flowLayoutPanel1, 0, 1);
|
||||
tableLayoutPanel1.Controls.Add(panel_ViewContainer, 0, 0);
|
||||
tableLayoutPanel1.Dock = DockStyle.Fill;
|
||||
tableLayoutPanel1.Location = new Point(0, 0);
|
||||
tableLayoutPanel1.Margin = new Padding(0);
|
||||
tableLayoutPanel1.Name = "tableLayoutPanel1";
|
||||
tableLayoutPanel1.RowCount = 2;
|
||||
tableLayoutPanel1.RowStyles.Add(new RowStyle(SizeType.Percent, 100F));
|
||||
tableLayoutPanel1.RowStyles.Add(new RowStyle());
|
||||
tableLayoutPanel1.Size = new Size(641, 636);
|
||||
tableLayoutPanel1.TabIndex = 2;
|
||||
//
|
||||
// flowLayoutPanel1
|
||||
//
|
||||
flowLayoutPanel1.Anchor = AnchorStyles.None;
|
||||
flowLayoutPanel1.AutoSize = true;
|
||||
flowLayoutPanel1.AutoSizeMode = AutoSizeMode.GrowAndShrink;
|
||||
flowLayoutPanel1.Controls.Add(button_Stop);
|
||||
flowLayoutPanel1.Controls.Add(button_Restart);
|
||||
flowLayoutPanel1.Controls.Add(button_Start);
|
||||
flowLayoutPanel1.Controls.Add(button_ForwardStep);
|
||||
flowLayoutPanel1.Controls.Add(button_ForwardFast);
|
||||
flowLayoutPanel1.Controls.Add(button_FullScreen);
|
||||
flowLayoutPanel1.Location = new Point(101, 594);
|
||||
flowLayoutPanel1.Margin = new Padding(0);
|
||||
flowLayoutPanel1.Name = "flowLayoutPanel1";
|
||||
flowLayoutPanel1.Size = new Size(438, 42);
|
||||
flowLayoutPanel1.TabIndex = 1;
|
||||
//
|
||||
// button_Stop
|
||||
//
|
||||
button_Stop.AutoSize = true;
|
||||
button_Stop.AutoSizeMode = AutoSizeMode.GrowAndShrink;
|
||||
button_Stop.ImageKey = "stop";
|
||||
button_Stop.ImageList = imageList;
|
||||
button_Stop.Location = new Point(3, 3);
|
||||
button_Stop.Name = "button_Stop";
|
||||
button_Stop.Padding = new Padding(15, 3, 15, 3);
|
||||
button_Stop.Size = new Size(67, 36);
|
||||
button_Stop.TabIndex = 0;
|
||||
toolTip.SetToolTip(button_Stop, "停止播放并重置时间到初始");
|
||||
button_Stop.UseVisualStyleBackColor = true;
|
||||
button_Stop.Click += button_Stop_Click;
|
||||
//
|
||||
// imageList
|
||||
//
|
||||
imageList.ColorDepth = ColorDepth.Depth32Bit;
|
||||
imageList.ImageStream = (ImageListStreamer)resources.GetObject("imageList.ImageStream");
|
||||
imageList.TransparentColor = Color.Transparent;
|
||||
imageList.Images.SetKeyName(0, "arrows-maximize");
|
||||
imageList.Images.SetKeyName(1, "forward-fast");
|
||||
imageList.Images.SetKeyName(2, "forward-step");
|
||||
imageList.Images.SetKeyName(3, "pause");
|
||||
imageList.Images.SetKeyName(4, "rotate-left");
|
||||
imageList.Images.SetKeyName(5, "start");
|
||||
imageList.Images.SetKeyName(6, "stop");
|
||||
//
|
||||
// button_Restart
|
||||
//
|
||||
button_Restart.AutoSize = true;
|
||||
button_Restart.AutoSizeMode = AutoSizeMode.GrowAndShrink;
|
||||
button_Restart.ImageKey = "rotate-left";
|
||||
button_Restart.ImageList = imageList;
|
||||
button_Restart.Location = new Point(76, 3);
|
||||
button_Restart.Name = "button_Restart";
|
||||
button_Restart.Padding = new Padding(15, 3, 15, 3);
|
||||
button_Restart.Size = new Size(67, 36);
|
||||
button_Restart.TabIndex = 1;
|
||||
toolTip.SetToolTip(button_Restart, "从头开始播放");
|
||||
button_Restart.UseVisualStyleBackColor = true;
|
||||
button_Restart.Click += button_Restart_Click;
|
||||
//
|
||||
// button_Start
|
||||
//
|
||||
button_Start.AutoSize = true;
|
||||
button_Start.AutoSizeMode = AutoSizeMode.GrowAndShrink;
|
||||
button_Start.BackgroundImageLayout = ImageLayout.Center;
|
||||
button_Start.ImageKey = "pause";
|
||||
button_Start.ImageList = imageList;
|
||||
button_Start.Location = new Point(149, 3);
|
||||
button_Start.Name = "button_Start";
|
||||
button_Start.Padding = new Padding(15, 3, 15, 3);
|
||||
button_Start.Size = new Size(67, 36);
|
||||
button_Start.TabIndex = 2;
|
||||
toolTip.SetToolTip(button_Start, "开始/暂停");
|
||||
button_Start.UseVisualStyleBackColor = true;
|
||||
button_Start.Click += button_Start_Click;
|
||||
//
|
||||
// button_ForwardStep
|
||||
//
|
||||
button_ForwardStep.AutoSize = true;
|
||||
button_ForwardStep.AutoSizeMode = AutoSizeMode.GrowAndShrink;
|
||||
button_ForwardStep.ImageKey = "forward-step";
|
||||
button_ForwardStep.ImageList = imageList;
|
||||
button_ForwardStep.Location = new Point(222, 3);
|
||||
button_ForwardStep.Name = "button_ForwardStep";
|
||||
button_ForwardStep.Padding = new Padding(15, 3, 15, 3);
|
||||
button_ForwardStep.Size = new Size(67, 36);
|
||||
button_ForwardStep.TabIndex = 3;
|
||||
toolTip.SetToolTip(button_ForwardStep, "快进 1 帧");
|
||||
button_ForwardStep.UseVisualStyleBackColor = true;
|
||||
button_ForwardStep.Click += button_ForwardStep_Click;
|
||||
//
|
||||
// button_ForwardFast
|
||||
//
|
||||
button_ForwardFast.AutoSize = true;
|
||||
button_ForwardFast.AutoSizeMode = AutoSizeMode.GrowAndShrink;
|
||||
button_ForwardFast.ImageKey = "forward-fast";
|
||||
button_ForwardFast.ImageList = imageList;
|
||||
button_ForwardFast.Location = new Point(295, 3);
|
||||
button_ForwardFast.Name = "button_ForwardFast";
|
||||
button_ForwardFast.Padding = new Padding(15, 3, 15, 3);
|
||||
button_ForwardFast.Size = new Size(67, 36);
|
||||
button_ForwardFast.TabIndex = 4;
|
||||
toolTip.SetToolTip(button_ForwardFast, "快进 10 帧");
|
||||
button_ForwardFast.UseVisualStyleBackColor = true;
|
||||
button_ForwardFast.Click += button_ForwardFast_Click;
|
||||
//
|
||||
// button_FullScreen
|
||||
//
|
||||
button_FullScreen.AutoSize = true;
|
||||
button_FullScreen.AutoSizeMode = AutoSizeMode.GrowAndShrink;
|
||||
button_FullScreen.ImageKey = "arrows-maximize";
|
||||
button_FullScreen.ImageList = imageList;
|
||||
button_FullScreen.Location = new Point(368, 3);
|
||||
button_FullScreen.Name = "button_FullScreen";
|
||||
button_FullScreen.Padding = new Padding(15, 3, 15, 3);
|
||||
button_FullScreen.Size = new Size(67, 36);
|
||||
button_FullScreen.TabIndex = 5;
|
||||
toolTip.SetToolTip(button_FullScreen, "全屏预览");
|
||||
button_FullScreen.UseVisualStyleBackColor = true;
|
||||
button_FullScreen.Click += button_FullScreen_Click;
|
||||
//
|
||||
// panel_ViewContainer
|
||||
//
|
||||
panel_ViewContainer.Controls.Add(panel_RenderContainer);
|
||||
panel_ViewContainer.Dock = DockStyle.Fill;
|
||||
panel_ViewContainer.Location = new Point(0, 0);
|
||||
panel_ViewContainer.Margin = new Padding(0);
|
||||
panel_ViewContainer.Name = "panel_ViewContainer";
|
||||
panel_ViewContainer.Size = new Size(641, 594);
|
||||
panel_ViewContainer.TabIndex = 6;
|
||||
//
|
||||
// panel_RenderContainer
|
||||
//
|
||||
panel_RenderContainer.BackColor = SystemColors.ControlDark;
|
||||
panel_RenderContainer.Controls.Add(panel_Render);
|
||||
panel_RenderContainer.Dock = DockStyle.Fill;
|
||||
panel_RenderContainer.Location = new Point(0, 0);
|
||||
panel_RenderContainer.Margin = new Padding(0);
|
||||
panel_RenderContainer.Name = "panel_RenderContainer";
|
||||
panel_RenderContainer.Size = new Size(641, 594);
|
||||
panel_RenderContainer.TabIndex = 0;
|
||||
panel_RenderContainer.SizeChanged += panel_RenderContainer_SizeChanged;
|
||||
//
|
||||
// spinePreviewFullScreenForm
|
||||
//
|
||||
spinePreviewFullScreenForm.ClientSize = new Size(2560, 1440);
|
||||
spinePreviewFullScreenForm.ControlBox = false;
|
||||
spinePreviewFullScreenForm.FormBorderStyle = FormBorderStyle.None;
|
||||
spinePreviewFullScreenForm.MaximizeBox = false;
|
||||
spinePreviewFullScreenForm.MinimizeBox = false;
|
||||
spinePreviewFullScreenForm.Name = "SpinePreviewFullScreenForm";
|
||||
spinePreviewFullScreenForm.ShowIcon = false;
|
||||
spinePreviewFullScreenForm.ShowInTaskbar = false;
|
||||
spinePreviewFullScreenForm.StartPosition = FormStartPosition.Manual;
|
||||
spinePreviewFullScreenForm.TopMost = true;
|
||||
spinePreviewFullScreenForm.Visible = false;
|
||||
spinePreviewFullScreenForm.FormClosing += spinePreviewFullScreenForm_FormClosing;
|
||||
spinePreviewFullScreenForm.KeyDown += spinePreviewFullScreenForm_KeyDown;
|
||||
//
|
||||
// wallpaperForm
|
||||
//
|
||||
wallpaperForm.ClientSize = new Size(0, 0);
|
||||
wallpaperForm.ControlBox = false;
|
||||
wallpaperForm.FormBorderStyle = FormBorderStyle.None;
|
||||
wallpaperForm.MaximizeBox = false;
|
||||
wallpaperForm.MinimizeBox = false;
|
||||
wallpaperForm.Name = "WallpaperForm";
|
||||
wallpaperForm.ShowIcon = false;
|
||||
wallpaperForm.ShowInTaskbar = false;
|
||||
wallpaperForm.StartPosition = FormStartPosition.Manual;
|
||||
wallpaperForm.Visible = false;
|
||||
wallpaperForm.WindowState = FormWindowState.Minimized;
|
||||
wallpaperForm.FormClosing += wallpaperForm_FormClosing;
|
||||
//
|
||||
// SpinePreviewPanel
|
||||
//
|
||||
AutoScaleDimensions = new SizeF(11F, 24F);
|
||||
AutoScaleMode = AutoScaleMode.Font;
|
||||
Controls.Add(tableLayoutPanel1);
|
||||
Name = "SpinePreviewPanel";
|
||||
Size = new Size(641, 636);
|
||||
tableLayoutPanel1.ResumeLayout(false);
|
||||
tableLayoutPanel1.PerformLayout();
|
||||
flowLayoutPanel1.ResumeLayout(false);
|
||||
flowLayoutPanel1.PerformLayout();
|
||||
panel_ViewContainer.ResumeLayout(false);
|
||||
panel_RenderContainer.ResumeLayout(false);
|
||||
ResumeLayout(false);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
private Panel panel_Render;
|
||||
private TableLayoutPanel tableLayoutPanel1;
|
||||
private Panel panel_RenderContainer;
|
||||
private FlowLayoutPanel flowLayoutPanel1;
|
||||
private Button button_Stop;
|
||||
private Button button_Start;
|
||||
private ImageList imageList;
|
||||
private ToolTip toolTip;
|
||||
private Button button_ForwardStep;
|
||||
private Button button_ForwardFast;
|
||||
private Button button_Restart;
|
||||
private Button button_FullScreen;
|
||||
private Panel panel_ViewContainer;
|
||||
private Forms.SpinePreviewFullScreenForm spinePreviewFullScreenForm;
|
||||
private WallpaperForm wallpaperForm;
|
||||
}
|
||||
}
|
||||
821
SpineViewer/Controls/SpinePreviewPanel.cs
Normal file
821
SpineViewer/Controls/SpinePreviewPanel.cs
Normal file
@@ -0,0 +1,821 @@
|
||||
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.Security.Policy;
|
||||
using System.Diagnostics;
|
||||
using NLog;
|
||||
using SpineViewer.Utils;
|
||||
using System.Drawing.Design;
|
||||
|
||||
namespace SpineViewer.Controls
|
||||
{
|
||||
public partial class SpinePreviewPanel : UserControl
|
||||
{
|
||||
public SpinePreviewPanel()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 日志器
|
||||
/// </summary>
|
||||
private readonly Logger logger = LogManager.GetCurrentClassLogger();
|
||||
|
||||
/// <summary>
|
||||
/// 要绑定的 Spine 列表控件
|
||||
/// </summary>
|
||||
[Category("自定义"), Description("相关联的 SpineListView")]
|
||||
public SpineListView? SpineListView { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 属性信息面板
|
||||
/// </summary>
|
||||
[Category("自定义"), Description("用于显示画面属性的属性页")]
|
||||
public PropertyGrid? PropertyGrid
|
||||
{
|
||||
get => propertyGrid;
|
||||
set
|
||||
{
|
||||
propertyGrid = value;
|
||||
if (propertyGrid is not null)
|
||||
propertyGrid.SelectedObject = new SpinePreviewPanelProperty(this);
|
||||
}
|
||||
}
|
||||
private PropertyGrid? propertyGrid;
|
||||
|
||||
#region 参数属性
|
||||
|
||||
/// <summary>
|
||||
/// 分辨率
|
||||
/// </summary>
|
||||
[DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
|
||||
[Browsable(false)]
|
||||
public Size Resolution
|
||||
{
|
||||
get => resolution;
|
||||
set
|
||||
{
|
||||
if (renderWindow is null) return;
|
||||
|
||||
if (value == resolution) return;
|
||||
if (value.Width <= 0) value.Width = 100;
|
||||
if (value.Height <= 0) value.Height = 100;
|
||||
|
||||
var previousZoom = Zoom;
|
||||
|
||||
float parentW = panel_Render.Parent.Width;
|
||||
float parentH = panel_Render.Parent.Height;
|
||||
float renderW = value.Width;
|
||||
float renderH = value.Height;
|
||||
float scale = Math.Min(parentW / renderW, parentH / renderH); // 两方向取较小值, 保证 parent 覆盖 render
|
||||
renderW *= scale;
|
||||
renderH *= scale;
|
||||
|
||||
panel_Render.Location = new((int)((parentW - renderW) / 2 + 0.5), (int)((parentH - renderH) / 2 + 0.5));
|
||||
panel_Render.Size = new((int)(renderW + 0.5), (int)(renderH + 0.5));
|
||||
resolution = value;
|
||||
|
||||
// 设置完 resolution 后还原缩放比例
|
||||
Zoom = previousZoom;
|
||||
|
||||
// 设置壁纸窗口分辨率
|
||||
using var view = renderWindow.GetView();
|
||||
wallpaperWindow.SetView(view);
|
||||
wallpaperForm.Size = value; // 必须两个 Size 都设置
|
||||
wallpaperWindow.Size = new((uint)value.Width, (uint)value.Height);
|
||||
}
|
||||
}
|
||||
private Size resolution = new(100, 100);
|
||||
|
||||
/// <summary>
|
||||
/// 画面中心点
|
||||
/// </summary>
|
||||
[DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
|
||||
[Browsable(false)]
|
||||
public PointF Center
|
||||
{
|
||||
get
|
||||
{
|
||||
if (renderWindow is null) return new(-1, -1);
|
||||
|
||||
using var view = renderWindow.GetView();
|
||||
var center = view.Center;
|
||||
return new(center.X, center.Y);
|
||||
}
|
||||
set
|
||||
{
|
||||
if (renderWindow is null) return;
|
||||
|
||||
using var view = renderWindow.GetView();
|
||||
view.Center = new(value.X, value.Y);
|
||||
renderWindow.SetView(view);
|
||||
wallpaperWindow.SetView(view);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 画面缩放
|
||||
/// </summary>
|
||||
[DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
|
||||
[Browsable(false)]
|
||||
public float Zoom
|
||||
{
|
||||
get
|
||||
{
|
||||
if (renderWindow is null) return -1;
|
||||
|
||||
using var view = renderWindow.GetView();
|
||||
return resolution.Width / Math.Abs(view.Size.X);
|
||||
}
|
||||
set
|
||||
{
|
||||
if (renderWindow is null) return;
|
||||
|
||||
value = Math.Clamp(value, 0.001f, 1000f);
|
||||
using var view = renderWindow.GetView();
|
||||
var signX = Math.Sign(view.Size.X);
|
||||
var signY = Math.Sign(view.Size.Y);
|
||||
view.Size = new(resolution.Width / value * signX, resolution.Height / value * signY);
|
||||
renderWindow.SetView(view);
|
||||
wallpaperWindow.SetView(view);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 画面旋转
|
||||
/// </summary>
|
||||
[Browsable(false)]
|
||||
[DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
|
||||
public float Rotation
|
||||
{
|
||||
get
|
||||
{
|
||||
if (renderWindow is null) return -1;
|
||||
|
||||
using var view = renderWindow.GetView();
|
||||
return view.Rotation;
|
||||
}
|
||||
set
|
||||
{
|
||||
if (renderWindow is null) return;
|
||||
|
||||
using var view = renderWindow.GetView();
|
||||
view.Rotation = value;
|
||||
renderWindow.SetView(view);
|
||||
wallpaperWindow.SetView(view);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 水平翻转
|
||||
/// </summary>
|
||||
[DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
|
||||
[Browsable(false)]
|
||||
public bool FlipX
|
||||
{
|
||||
get
|
||||
{
|
||||
if (renderWindow is null) return false;
|
||||
|
||||
using var view = renderWindow.GetView();
|
||||
return view.Size.X < 0;
|
||||
}
|
||||
set
|
||||
{
|
||||
if (renderWindow is null) return;
|
||||
|
||||
using var view = renderWindow.GetView();
|
||||
var size = view.Size;
|
||||
if (size.X > 0 && value || size.X < 0 && !value)
|
||||
size.X *= -1;
|
||||
view.Size = size;
|
||||
renderWindow.SetView(view);
|
||||
wallpaperWindow.SetView(view);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 垂直翻转
|
||||
/// </summary>
|
||||
[DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
|
||||
[Browsable(false)]
|
||||
public bool FlipY
|
||||
{
|
||||
get
|
||||
{
|
||||
if (renderWindow is null) return false;
|
||||
|
||||
using var view = renderWindow.GetView();
|
||||
return view.Size.Y < 0;
|
||||
}
|
||||
set
|
||||
{
|
||||
if (renderWindow is null) return;
|
||||
|
||||
using var view = renderWindow.GetView();
|
||||
var size = view.Size;
|
||||
if (size.Y > 0 && value || size.Y < 0 && !value)
|
||||
size.Y *= -1;
|
||||
view.Size = size;
|
||||
renderWindow.SetView(view);
|
||||
wallpaperWindow.SetView(view);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 仅渲染选中
|
||||
/// </summary>
|
||||
[DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
|
||||
[Browsable(false)]
|
||||
public bool RenderSelectedOnly { get; set; } = false;
|
||||
|
||||
/// <summary>
|
||||
/// 显示坐标轴
|
||||
/// </summary>
|
||||
[DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
|
||||
[Browsable(false)]
|
||||
public bool ShowAxis { get; set; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// 最大帧率
|
||||
/// </summary>
|
||||
[DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
|
||||
[Browsable(false)]
|
||||
public uint MaxFps
|
||||
{
|
||||
get => maxFps;
|
||||
set
|
||||
{
|
||||
if (renderWindow is null) return;
|
||||
|
||||
renderWindow.SetFramerateLimit(value);
|
||||
maxFps = value;
|
||||
}
|
||||
}
|
||||
private uint maxFps = 60;
|
||||
|
||||
/// <summary>
|
||||
/// 获取 View
|
||||
/// </summary>
|
||||
public SFML.Graphics.View GetView() => renderWindow.GetView();
|
||||
|
||||
/// <summary>
|
||||
/// 是否开启桌面投影
|
||||
/// </summary>
|
||||
[DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
|
||||
[Browsable(false)]
|
||||
public bool EnableDesktopProjection
|
||||
{
|
||||
get => enableDesktopProjection;
|
||||
set
|
||||
{
|
||||
if (renderWindow is null) return;
|
||||
|
||||
if (enableDesktopProjection == value) return;
|
||||
if (value)
|
||||
{
|
||||
var screenBounds = Screen.FromControl(this).Bounds;
|
||||
Resolution = screenBounds.Size;
|
||||
wallpaperWindow.Position = new(screenBounds.X, screenBounds.Y);
|
||||
wallpaperForm.Show();
|
||||
}
|
||||
else
|
||||
{
|
||||
wallpaperForm.Hide();
|
||||
}
|
||||
enableDesktopProjection = value;
|
||||
}
|
||||
}
|
||||
private bool enableDesktopProjection = false;
|
||||
|
||||
/// <summary>
|
||||
/// 预览画面背景色
|
||||
/// </summary>
|
||||
[DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
|
||||
[Browsable(false)]
|
||||
public SFML.Graphics.Color BackgroundColor { get; set; } = new(105, 105, 105);
|
||||
|
||||
#endregion
|
||||
|
||||
#region 渲染管理
|
||||
|
||||
/// <summary>
|
||||
/// 预览画面坐标轴颜色
|
||||
/// </summary>
|
||||
private static readonly SFML.Graphics.Color AxisColor = new(220, 220, 220);
|
||||
|
||||
/// <summary>
|
||||
/// 坐标轴顶点缓冲区
|
||||
/// </summary>
|
||||
private readonly SFML.Graphics.VertexArray axisVertices = new(SFML.Graphics.PrimitiveType.Lines, 2); // XXX: 暂时未使用 Dispose 释放
|
||||
|
||||
/// <summary>
|
||||
/// 渲染窗口
|
||||
/// </summary>
|
||||
private SFML.Graphics.RenderWindow renderWindow;
|
||||
|
||||
/// <summary>
|
||||
/// 壁纸窗口
|
||||
/// </summary>
|
||||
private SFML.Graphics.RenderWindow wallpaperWindow;
|
||||
|
||||
/// <summary>
|
||||
/// 帧间隔计时器
|
||||
/// </summary>
|
||||
private readonly SFML.System.Clock clock = new();
|
||||
|
||||
/// <summary>
|
||||
/// 渲染任务
|
||||
/// </summary>
|
||||
private Task? task = null;
|
||||
private CancellationTokenSource? cancelToken = null;
|
||||
|
||||
/// <summary>
|
||||
/// 是否更新画面
|
||||
/// </summary>
|
||||
[DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
|
||||
[Browsable(false)]
|
||||
public bool IsUpdating
|
||||
{
|
||||
get => isUpdating;
|
||||
private set
|
||||
{
|
||||
if (value == isUpdating) return;
|
||||
if (value)
|
||||
{
|
||||
button_Start.ImageKey = "pause";
|
||||
}
|
||||
else
|
||||
{
|
||||
button_Start.ImageKey = "start";
|
||||
}
|
||||
isUpdating = value;
|
||||
}
|
||||
}
|
||||
private bool isUpdating = true;
|
||||
|
||||
/// <summary>
|
||||
/// 快进时间量
|
||||
/// </summary>
|
||||
private float forwardDelta = 0;
|
||||
private object _forwardDeltaLock = new();
|
||||
|
||||
/// <summary>
|
||||
/// 开始渲染
|
||||
/// </summary>
|
||||
public void StartRender()
|
||||
{
|
||||
// 延迟到第一次开启渲染时进行初始化
|
||||
if (renderWindow is null)
|
||||
{
|
||||
renderWindow = new(panel_Render.Handle);
|
||||
renderWindow.SetActive(false);
|
||||
wallpaperWindow = new(wallpaperForm.Handle);
|
||||
wallpaperWindow.SetActive(false);
|
||||
|
||||
// 设置默认参数
|
||||
Resolution = new(2048, 2048);
|
||||
Zoom = 1;
|
||||
Center = new(0, 0);
|
||||
FlipY = true;
|
||||
MaxFps = 30;
|
||||
}
|
||||
|
||||
if (task is not null) return;
|
||||
cancelToken = new();
|
||||
task = Task.Run(RenderTask, cancelToken.Token);
|
||||
IsUpdating = true;
|
||||
if (enableDesktopProjection) wallpaperForm.Show();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 停止渲染
|
||||
/// </summary>
|
||||
public void StopRender()
|
||||
{
|
||||
if (wallpaperForm.InvokeRequired) wallpaperForm.Invoke(wallpaperForm.Hide);
|
||||
else wallpaperForm.Hide();
|
||||
IsUpdating = false;
|
||||
if (task is null || cancelToken is null)
|
||||
return;
|
||||
cancelToken.Cancel();
|
||||
task.Wait();
|
||||
cancelToken = null;
|
||||
task = null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 渲染任务
|
||||
/// </summary>
|
||||
private void RenderTask()
|
||||
{
|
||||
try
|
||||
{
|
||||
renderWindow.SetActive(true);
|
||||
wallpaperWindow.SetActive(true);
|
||||
|
||||
float delta;
|
||||
while (cancelToken is not null && !cancelToken.IsCancellationRequested)
|
||||
{
|
||||
// 必须让 SFML 有机会处理窗口消息, 例如位置和大小变化
|
||||
renderWindow.DispatchEvents();
|
||||
|
||||
delta = clock.ElapsedTime.AsSeconds();
|
||||
clock.Restart();
|
||||
|
||||
// 停止更新的时候只是时间不前进, 但是坐标变换还是要更新, 否则无法移动对象
|
||||
if (!IsUpdating) delta = 0;
|
||||
|
||||
// 加上要快进的量
|
||||
lock (_forwardDeltaLock)
|
||||
{
|
||||
delta += forwardDelta;
|
||||
forwardDelta = 0;
|
||||
}
|
||||
|
||||
renderWindow.Clear(BackgroundColor);
|
||||
if (enableDesktopProjection) wallpaperWindow.Clear(BackgroundColor);
|
||||
|
||||
if (ShowAxis)
|
||||
{
|
||||
// 画一个很长的坐标轴, 用 1e9 比较合适
|
||||
axisVertices[0] = new(new(-1e9f, 0), AxisColor);
|
||||
axisVertices[1] = new(new(1e9f, 0), AxisColor);
|
||||
renderWindow.Draw(axisVertices);
|
||||
axisVertices[0] = new(new(0, -1e9f), AxisColor);
|
||||
axisVertices[1] = new(new(0, 1e9f), AxisColor);
|
||||
renderWindow.Draw(axisVertices);
|
||||
}
|
||||
|
||||
// 渲染 Spine
|
||||
if (SpineListView is not null)
|
||||
{
|
||||
lock (SpineListView.Spines)
|
||||
{
|
||||
var spines = SpineListView.Spines.Where(sp => !sp.IsHidden).ToArray();
|
||||
for (int i = spines.Length - 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.EnableDebug = true;
|
||||
renderWindow.Draw(spine);
|
||||
spine.EnableDebug = false;
|
||||
|
||||
if (enableDesktopProjection) wallpaperWindow.Draw(spine);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
renderWindow.Display();
|
||||
|
||||
if (enableDesktopProjection) wallpaperWindow.Display();
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.Fatal(ex.ToString());
|
||||
logger.Fatal("Render task stopped");
|
||||
MessagePopup.Error(ex.ToString(), "预览画面已停止渲染");
|
||||
}
|
||||
finally
|
||||
{
|
||||
renderWindow.SetActive(false);
|
||||
wallpaperWindow.SetActive(false);
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
/// <summary>
|
||||
/// 画面拖放对象世界坐标源点
|
||||
/// </summary>
|
||||
private SFML.System.Vector2f? draggingSrc = null;
|
||||
|
||||
private void panel_RenderContainer_SizeChanged(object sender, EventArgs e)
|
||||
{
|
||||
if (renderWindow is null) return;
|
||||
|
||||
float parentW = panel_Render.Parent.Width;
|
||||
float parentH = panel_Render.Parent.Height;
|
||||
float renderW = panel_Render.Width;
|
||||
float renderH = panel_Render.Height;
|
||||
float scale = Math.Min(parentW / renderW, parentH / renderH); // 两方向取较小值, 保证 parent 覆盖 render
|
||||
renderW *= scale;
|
||||
renderH *= scale;
|
||||
|
||||
panel_Render.Location = new((int)((parentW - renderW) / 2 + 0.5), (int)((parentH - renderH) / 2 + 0.5));
|
||||
panel_Render.Size = new((int)(renderW + 0.5), (int)(renderH + 0.5));
|
||||
}
|
||||
|
||||
private void panel_Render_MouseDown(object sender, MouseEventArgs e)
|
||||
{
|
||||
if (renderWindow is null) return;
|
||||
|
||||
// 右键优先级高, 进入画面拖动模式, 需要重新记录源点
|
||||
if ((e.Button & MouseButtons.Right) != 0)
|
||||
{
|
||||
draggingSrc = renderWindow.MapPixelToCoords(new(e.X, e.Y));
|
||||
Cursor = Cursors.Hand;
|
||||
}
|
||||
// 按下了左键并且右键是松开的
|
||||
else if ((e.Button & MouseButtons.Left) != 0 && (MouseButtons & MouseButtons.Right) == 0)
|
||||
{
|
||||
draggingSrc = renderWindow.MapPixelToCoords(new(e.X, e.Y));
|
||||
var src = new PointF(((SFML.System.Vector2f)draggingSrc).X, ((SFML.System.Vector2f)draggingSrc).Y);
|
||||
|
||||
if (SpineListView is null)
|
||||
return;
|
||||
|
||||
lock (SpineListView.Spines)
|
||||
{
|
||||
var spines = SpineListView.Spines;
|
||||
|
||||
// 仅渲染选中模式禁止在画面里选择对象
|
||||
if (RenderSelectedOnly)
|
||||
{
|
||||
// 只在被选中的对象里判断是否有效命中
|
||||
bool hit = false;
|
||||
foreach (int i in SpineListView.SelectedIndices)
|
||||
{
|
||||
if (spines[i].IsHidden) continue;
|
||||
if (!spines[i].GetCurrentBounds().Contains(src)) continue;
|
||||
hit = true;
|
||||
break;
|
||||
}
|
||||
|
||||
// 如果没点到被选中的模型, 则不允许拖动
|
||||
if (!hit) draggingSrc = null;
|
||||
}
|
||||
else
|
||||
{
|
||||
if ((ModifierKeys & Keys.Control) == 0)
|
||||
{
|
||||
// 没按 Ctrl 的情况下, 如果命中了已选中对象, 则就算普通命中
|
||||
bool hit = false;
|
||||
for (int i = 0; i < spines.Count; i++)
|
||||
{
|
||||
if (spines[i].IsHidden) continue;
|
||||
if (!spines[i].GetCurrentBounds().Contains(src)) continue;
|
||||
|
||||
hit = true;
|
||||
|
||||
// 如果点到了没被选中的东西, 则清空原先选中的, 改为只选中这一次点的
|
||||
if (!SpineListView.SelectedIndices.Contains(i))
|
||||
{
|
||||
SpineListView.SelectedIndices.Clear();
|
||||
SpineListView.SelectedIndices.Add(i);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
// 如果点了空白的地方, 就清空选中列表
|
||||
if (!hit) SpineListView.SelectedIndices.Clear();
|
||||
}
|
||||
else
|
||||
{
|
||||
// 按下 Ctrl 的情况就执行多选, 并且点空白处也不会清空选中
|
||||
for (int i = 0; i < spines.Count; i++)
|
||||
{
|
||||
if (spines[i].IsHidden) continue;
|
||||
if (!spines[i].GetCurrentBounds().Contains(src)) continue;
|
||||
|
||||
SpineListView.SelectedIndices.Add(i);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void panel_Render_MouseMove(object sender, MouseEventArgs e)
|
||||
{
|
||||
if (renderWindow is null) return;
|
||||
|
||||
if (draggingSrc is null) return;
|
||||
|
||||
var src = (SFML.System.Vector2f)draggingSrc;
|
||||
var dst = renderWindow.MapPixelToCoords(new(e.X, e.Y));
|
||||
var _delta = dst - src;
|
||||
var delta = new SizeF(_delta.X, _delta.Y);
|
||||
|
||||
if ((e.Button & MouseButtons.Right) != 0)
|
||||
{
|
||||
Center -= delta;
|
||||
}
|
||||
else if ((e.Button & MouseButtons.Left) != 0)
|
||||
{
|
||||
if (SpineListView is not null)
|
||||
{
|
||||
lock (SpineListView.Spines)
|
||||
{
|
||||
var spines = SpineListView.Spines;
|
||||
foreach (int i in SpineListView.SelectedIndices)
|
||||
{
|
||||
if (spines[i].IsHidden) continue;
|
||||
spines[i].Position += delta;
|
||||
}
|
||||
}
|
||||
}
|
||||
draggingSrc = dst;
|
||||
}
|
||||
}
|
||||
|
||||
private void panel_Render_MouseUp(object sender, MouseEventArgs e)
|
||||
{
|
||||
// 右键高优先级, 结束画面拖动模式
|
||||
if ((e.Button & MouseButtons.Right) != 0)
|
||||
{
|
||||
SpineListView?.SpinePropertyGrid?.Refresh();
|
||||
|
||||
draggingSrc = null;
|
||||
Cursor = Cursors.Default;
|
||||
PropertyGrid?.Refresh();
|
||||
}
|
||||
// 按下了左键并且右键是松开的
|
||||
else if ((e.Button & MouseButtons.Left) != 0 && (MouseButtons & MouseButtons.Right) == 0)
|
||||
{
|
||||
draggingSrc = null;
|
||||
SpineListView?.SpinePropertyGrid?.Refresh();
|
||||
}
|
||||
}
|
||||
|
||||
private void panel_Render_MouseWheel(object sender, MouseEventArgs e)
|
||||
{
|
||||
var factor = (e.Delta > 0 ? 1.1f : 0.9f);
|
||||
if ((ModifierKeys & Keys.Control) == 0)
|
||||
{
|
||||
Zoom *= factor;
|
||||
PropertyGrid?.Refresh();
|
||||
}
|
||||
else
|
||||
{
|
||||
if (SpineListView is not null)
|
||||
{
|
||||
lock (SpineListView.Spines)
|
||||
{
|
||||
var spines = SpineListView.Spines;
|
||||
foreach (int i in SpineListView.SelectedIndices)
|
||||
{
|
||||
if (spines[i].IsHidden) continue;
|
||||
spines[i].Scale *= factor;
|
||||
}
|
||||
}
|
||||
SpineListView.SpinePropertyGrid?.Refresh();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void button_Stop_Click(object sender, EventArgs e)
|
||||
{
|
||||
IsUpdating = false;
|
||||
if (SpineListView is not null)
|
||||
{
|
||||
lock (SpineListView.Spines)
|
||||
{
|
||||
foreach (var spine in SpineListView.Spines)
|
||||
spine.ResetAnimationsTime();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void button_Restart_Click(object sender, EventArgs e)
|
||||
{
|
||||
if (SpineListView is not null)
|
||||
{
|
||||
lock (SpineListView.Spines)
|
||||
{
|
||||
foreach (var spine in SpineListView.Spines)
|
||||
spine.ResetAnimationsTime();
|
||||
}
|
||||
}
|
||||
IsUpdating = true;
|
||||
}
|
||||
|
||||
private void button_Start_Click(object sender, EventArgs e)
|
||||
{
|
||||
IsUpdating = !IsUpdating;
|
||||
}
|
||||
|
||||
private void button_ForwardStep_Click(object sender, EventArgs e)
|
||||
{
|
||||
lock (_forwardDeltaLock)
|
||||
{
|
||||
if (maxFps > 0)
|
||||
forwardDelta += 1f / maxFps;
|
||||
else
|
||||
forwardDelta += 0.001f;
|
||||
}
|
||||
}
|
||||
|
||||
private void button_ForwardFast_Click(object sender, EventArgs e)
|
||||
{
|
||||
lock (_forwardDeltaLock)
|
||||
{
|
||||
if (maxFps > 0)
|
||||
forwardDelta += 10f / maxFps;
|
||||
else
|
||||
forwardDelta += 0.01f;
|
||||
}
|
||||
}
|
||||
|
||||
private void button_FullScreen_Click(object sender, EventArgs e)
|
||||
{
|
||||
var screenBounds = Screen.FromControl(this).Bounds;
|
||||
Resolution = screenBounds.Size;
|
||||
PropertyGrid?.Refresh();
|
||||
|
||||
// PerfMonitorV2 模式下, 位置和大小需要分开设置
|
||||
// 因为目标位置的 DPI 可能发生变化, 因此在 WM_POSITIONCHANGED 之后会收到 WM_DPICHANGED
|
||||
// 进而导致一次额外的 WM_SIZE 消息, 其大小是 DPI 修改前的大小, 这个消息在此次设置之后发生
|
||||
// 因此如果同时设置位置和大小则大小可能设置失败
|
||||
spinePreviewFullScreenForm.Location = screenBounds.Location;
|
||||
spinePreviewFullScreenForm.Size = screenBounds.Size;
|
||||
spinePreviewFullScreenForm.Controls.Add(panel_RenderContainer);
|
||||
spinePreviewFullScreenForm.Show();
|
||||
}
|
||||
|
||||
private void spinePreviewFullScreenForm_KeyDown(object sender, KeyEventArgs e)
|
||||
{
|
||||
if (e.KeyCode == Keys.Escape)
|
||||
{
|
||||
spinePreviewFullScreenForm.Hide();
|
||||
panel_ViewContainer.Controls.Add(panel_RenderContainer);
|
||||
}
|
||||
}
|
||||
|
||||
private void spinePreviewFullScreenForm_FormClosing(object sender, FormClosingEventArgs e)
|
||||
{
|
||||
e.Cancel = e.CloseReason == CloseReason.UserClosing;
|
||||
}
|
||||
|
||||
private void wallpaperForm_FormClosing(object sender, FormClosingEventArgs e)
|
||||
{
|
||||
e.Cancel = e.CloseReason == CloseReason.UserClosing;
|
||||
}
|
||||
|
||||
//public void ClickStopButton() => button_Stop_Click(button_Stop, EventArgs.Empty);
|
||||
//public void ClickRestartButton() => button_Restart_Click(button_Restart, EventArgs.Empty);
|
||||
//public void ClickStartButton() => button_Start_Click(button_Start, EventArgs.Empty);
|
||||
//public void ClickForwardStepButton() => button_ForwardStep_Click(button_ForwardStep, EventArgs.Empty);
|
||||
//public void ClickForwardFastButton() => button_ForwardFast_Click(button_ForwardFast, EventArgs.Empty);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 用于在 PropertyGrid 上显示 <see cref="SpinePreviewPanel"/> 属性的包装类, 提供用户操作接口
|
||||
/// </summary>
|
||||
public class SpinePreviewPanelProperty(SpinePreviewPanel previewPanel)
|
||||
{
|
||||
[Browsable(false)]
|
||||
public SpinePreviewPanel PreviewPanel { get; } = previewPanel;
|
||||
|
||||
[RefreshProperties(RefreshProperties.All)]
|
||||
[TypeConverter(typeof(ResolutionConverter))]
|
||||
[Category("[0] 导出"), DisplayName("分辨率")]
|
||||
public Size Resolution { get => PreviewPanel.Resolution; set => PreviewPanel.Resolution = value; }
|
||||
|
||||
[TypeConverter(typeof(PointFConverter))]
|
||||
[Category("[0] 导出"), DisplayName("画面中心点")]
|
||||
public PointF Center { get => PreviewPanel.Center; set => PreviewPanel.Center = value; }
|
||||
|
||||
[Category("[0] 导出"), DisplayName("缩放")]
|
||||
public float Zoom { get => PreviewPanel.Zoom; set => PreviewPanel.Zoom = value; }
|
||||
|
||||
[Category("[0] 导出"), DisplayName("旋转")]
|
||||
public float Rotation { get => PreviewPanel.Rotation; set => PreviewPanel.Rotation = value; }
|
||||
|
||||
[Category("[0] 导出"), DisplayName("水平翻转")]
|
||||
public bool FlipX { get => PreviewPanel.FlipX; set => PreviewPanel.FlipX = value; }
|
||||
|
||||
[Category("[0] 导出"), DisplayName("垂直翻转")]
|
||||
public bool FlipY { get => PreviewPanel.FlipY; set => PreviewPanel.FlipY = value; }
|
||||
|
||||
[Category("[0] 导出"), DisplayName("仅渲染选中")]
|
||||
public bool RenderSelectedOnly { get => PreviewPanel.RenderSelectedOnly; set => PreviewPanel.RenderSelectedOnly = value; }
|
||||
|
||||
[Category("[1] 预览"), DisplayName("显示坐标轴")]
|
||||
public bool ShowAxis { get => PreviewPanel.ShowAxis; set => PreviewPanel.ShowAxis = value; }
|
||||
|
||||
[Category("[1] 预览"), DisplayName("最大帧率")]
|
||||
public uint MaxFps { get => PreviewPanel.MaxFps; set => PreviewPanel.MaxFps = value; }
|
||||
|
||||
[Editor(typeof(SFMLColorEditor), typeof(UITypeEditor))]
|
||||
[TypeConverter(typeof(SFMLColorConverter))]
|
||||
[Category("[1] 预览"), DisplayName("背景颜色")]
|
||||
public SFML.Graphics.Color BackgroundColor { get => PreviewPanel.BackgroundColor; set => PreviewPanel.BackgroundColor = value; }
|
||||
}
|
||||
}
|
||||
293
SpineViewer/Controls/SpinePreviewPanel.resx
Normal file
293
SpineViewer/Controls/SpinePreviewPanel.resx
Normal file
@@ -0,0 +1,293 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<root>
|
||||
<!--
|
||||
Microsoft ResX Schema
|
||||
|
||||
Version 2.0
|
||||
|
||||
The primary goals of this format is to allow a simple XML format
|
||||
that is mostly human readable. The generation and parsing of the
|
||||
various data types are done through the TypeConverter classes
|
||||
associated with the data types.
|
||||
|
||||
Example:
|
||||
|
||||
... ado.net/XML headers & schema ...
|
||||
<resheader name="resmimetype">text/microsoft-resx</resheader>
|
||||
<resheader name="version">2.0</resheader>
|
||||
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
|
||||
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
|
||||
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
|
||||
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
|
||||
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
|
||||
<value>[base64 mime encoded serialized .NET Framework object]</value>
|
||||
</data>
|
||||
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
|
||||
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
|
||||
<comment>This is a comment</comment>
|
||||
</data>
|
||||
|
||||
There are any number of "resheader" rows that contain simple
|
||||
name/value pairs.
|
||||
|
||||
Each data row contains a name, and value. The row also contains a
|
||||
type or mimetype. Type corresponds to a .NET class that support
|
||||
text/value conversion through the TypeConverter architecture.
|
||||
Classes that don't support this are serialized and stored with the
|
||||
mimetype set.
|
||||
|
||||
The mimetype is used for serialized objects, and tells the
|
||||
ResXResourceReader how to depersist the object. This is currently not
|
||||
extensible. For a given mimetype the value must be set accordingly:
|
||||
|
||||
Note - application/x-microsoft.net.object.binary.base64 is the format
|
||||
that the ResXResourceWriter will generate, however the reader can
|
||||
read any of the formats listed below.
|
||||
|
||||
mimetype: application/x-microsoft.net.object.binary.base64
|
||||
value : The object must be serialized with
|
||||
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
|
||||
: and then encoded with base64 encoding.
|
||||
|
||||
mimetype: application/x-microsoft.net.object.soap.base64
|
||||
value : The object must be serialized with
|
||||
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
|
||||
: and then encoded with base64 encoding.
|
||||
|
||||
mimetype: application/x-microsoft.net.object.bytearray.base64
|
||||
value : The object must be serialized into a byte array
|
||||
: using a System.ComponentModel.TypeConverter
|
||||
: and then encoded with base64 encoding.
|
||||
-->
|
||||
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
|
||||
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
|
||||
<xsd:element name="root" msdata:IsDataSet="true">
|
||||
<xsd:complexType>
|
||||
<xsd:choice maxOccurs="unbounded">
|
||||
<xsd:element name="metadata">
|
||||
<xsd:complexType>
|
||||
<xsd:sequence>
|
||||
<xsd:element name="value" type="xsd:string" minOccurs="0" />
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="name" use="required" type="xsd:string" />
|
||||
<xsd:attribute name="type" type="xsd:string" />
|
||||
<xsd:attribute name="mimetype" type="xsd:string" />
|
||||
<xsd:attribute ref="xml:space" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:element name="assembly">
|
||||
<xsd:complexType>
|
||||
<xsd:attribute name="alias" type="xsd:string" />
|
||||
<xsd:attribute name="name" type="xsd:string" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:element name="data">
|
||||
<xsd:complexType>
|
||||
<xsd:sequence>
|
||||
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
|
||||
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
|
||||
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
|
||||
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
|
||||
<xsd:attribute ref="xml:space" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:element name="resheader">
|
||||
<xsd:complexType>
|
||||
<xsd:sequence>
|
||||
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="name" type="xsd:string" use="required" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
</xsd:choice>
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
</xsd:schema>
|
||||
<resheader name="resmimetype">
|
||||
<value>text/microsoft-resx</value>
|
||||
</resheader>
|
||||
<resheader name="version">
|
||||
<value>2.0</value>
|
||||
</resheader>
|
||||
<resheader name="reader">
|
||||
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
||||
</resheader>
|
||||
<resheader name="writer">
|
||||
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
||||
</resheader>
|
||||
<metadata name="imageList.TrayLocation" type="System.Drawing.Point, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a">
|
||||
<value>17, 17</value>
|
||||
</metadata>
|
||||
<data name="imageList.ImageStream" mimetype="application/x-microsoft.net.object.binary.base64">
|
||||
<value>
|
||||
AAEAAAD/////AQAAAAAAAAAMAgAAAEZTeXN0ZW0uV2luZG93cy5Gb3JtcywgQ3VsdHVyZT1uZXV0cmFs
|
||||
LCBQdWJsaWNLZXlUb2tlbj1iNzdhNWM1NjE5MzRlMDg5BQEAAAAmU3lzdGVtLldpbmRvd3MuRm9ybXMu
|
||||
SW1hZ2VMaXN0U3RyZWFtZXIBAAAABERhdGEHAgIAAAAJAwAAAA8DAAAA8iMAAAJNU0Z0AUkBTAIBAQcB
|
||||
AAGQAQABkAEAAR8BAAEYAQAE/wEhAQAI/wFCAU0BNgcAATYDAAEoAwABfAMAATADAAEBAQABIAYAAV0+
|
||||
AAMEAQUDAAH/AwAB/wMAAf8DAAH/AwAB/wMAAf8DAAH/AwAB/wMAAf9DAAH/AwAB/wMAAf8DAAH/A1UB
|
||||
sWQAA1gB7wMAAf8DAAH/AwAB/wMAAf8DAAH/AwAB/wMAAf8DAAH/AwAB/wMAAf8DAAH/AwAB/wMAAf8D
|
||||
AAH/AwAB/wMAAf8DAAH/AwAB/wMAAf8DAAH/Ay0BRbcAAf8DAAH/AwAB/wMAAf8DAAH/AwAB/wMAAf8D
|
||||
AAH/AwAB/wMAAf8DAAH/AwAB/wMAAf87AAH/AwAB/wMAAf8DAAH/AwAB/wMAAf9cAANEAXgDAAH/AwAB
|
||||
/wMAAf8DAAH/AwAB/wMAAf8DAAH/AwAB/wMAAf8DAAH/AwAB/wMAAf8DAAH/AwAB/wMAAf8DAAH/AwAB
|
||||
/wMAAf8DAAH/AwAB/wMAAf8DAAH/swAB/wMAAf8DAAH/AwAB/wMAAf8XAAH/AwAB/wMAAf8DAAH/AwAB
|
||||
/zcAAf8DAAH/BwAB/wMAAf8DAAH/AwAB/wMAAf9XAAH/AwAB/wMAAf8DAAH/AwAB/wMAAf8DAAH/AwAB
|
||||
/wMAAf8DAAH/AwAB/wMAAf8DAAH/AwAB/wMAAf8DAAH/AwAB/wMAAf8DAAH/AwAB/wMAAf8DAAH/AwAB
|
||||
/wMAAf+vAAH/AwAB/wNRAaQnAAH/AwAB/wMAAf8DAAH/MwAB/wMAAf8IAANOAZcDAAH/AwAB/wMAAf8D
|
||||
AAH/Ay4BSE8AAf8DAAH/AwAB/0sAAf8DAAH/AwAB/+AAAxUBHQMAAf8DAAH/AwAB/y8AAf8DAAH/EwAB
|
||||
/wMAAf8DAAH/AwAB/wMAAf9LAAH/AwAB/wMAAf9LAAH/AwAB/wMAAf/kAAMmATgDAAH/AwAB/wMAAf8r
|
||||
AAH/AwAB/xsAAf8DAAH/AwAB/wMAAf8DAAH/QwAB/wMAAf8DAAH/SwAB/wMAAf8DAAH/6wAB/wMAAf8D
|
||||
AAH/KwAB/wMAAf8cAANCAfYDAAH/AwAB/wMAAf8DAAH/AwQBBTsAAf8DAAH/AwAB/0sAAf8DAAH/AwAB
|
||||
/+8AAf8DAAH/AwAB/ycAAf8DAAH/JwAB/wMAAf8DAAH/AwAB/wMAAf83AAH/AwAB/wMAAf9LAAH/AwAB
|
||||
/wMAAf/vAAH/AwAB/wMAAf8nAAH/AwAB/ygAAwcBCQMAAf8DAAH/AwAB/wMAAf8DYAHjLwAB/wMAAf8D
|
||||
AAH/SwAB/wMAAf8DAAH/7AADIAEtAwAB/wMAAf8nAAH/AwAB/zMAAf8DAAH/AwAB/wMAAf8DAAH/KwAB
|
||||
/wMAAf8DAAH/SwAB/wMAAf8DAAH/8wAB/wMAAf8nAAH/AwAB/zsAAf8DAAH/AwAB/wMAAf8nAAH/AwAB
|
||||
/wMAAf9LAAH/AwAB/wMAAf/zAAH/AwAB/ycAAf8DAAH/PAADPwFsAwAB/wMAAf8nAAH/AwAB/wMAAf9L
|
||||
AAH/AwAB/wMAAf/zAAH/AwAB/ycAAf8DAAH/OwAB/wMAAf8DAAH/AwAB/ycAAf8DAAH/AwAB/0sAAf8D
|
||||
AAH/AwAB/5sAAf8DAAH/AwAB/wMAAf8DAAH/AwAB/wMAAf8DAAH/NAADCAEKAwAB/wMAAf8nAAH/AwAB
|
||||
/zAAA10BzgMAAf8DAAH/AwAB/wMAAf8EAScAAf8DAAH/AwAB/0sAAf8DAAH/AwAB/5QAAwUBBgMAAf8D
|
||||
AAH/AwAB/wMAAf8DAAH/AwAB/wMAAf8DAAH/AwAB/zMAAf8DAAH/AwAB/ycAAf8DAAH/LwAB/wMAAf8D
|
||||
AAH/AwAB/wMAAf8vAAH/AwAB/wMAAf9LAAH/AwAB/wMAAf+UAAMFAQYDAAH/AwAB/wMAAf8DAAH/AwAB
|
||||
/wMAAf8DAAH/AwAB/wMAAf8zAAH/AwAB/wMAAf8nAAH/AwAB/ycAAf8DAAH/AwAB/wMAAf8DAAH/NwAB
|
||||
/wMAAf8DAAH/SwAB/wMAAf8DAAH/lAADBQEGAwAB/wMAAf8PAAH/AwAB/wMAAf8DFQEcLwAB/wMAAf8D
|
||||
AAH/KwAB/wMAAf8cAAM9AWkDAAH/AwAB/wMAAf8DAAH/A0MBdjsAAf8DAAH/AwAB/0sAAf8DAAH/AwAB
|
||||
/5QAAwUBBgMAAf8DAAH/CwAB/wMAAf8DAAH/AxMBGjMAAf8DAAH/AwAB/ysAAf8DAAH/GwAB/wMAAf8D
|
||||
AAH/AwAB/wMAAf9DAAH/AwAB/wMAAf9LAAH/AwAB/wMAAf+UAAMFAQYDAAH/AwAB/wcAAf8DAAH/AwAB
|
||||
/wMbASYzAAH/AwAB/wMAAf8vAAH/AwAB/xMAAf8DAAH/AwAB/wMAAf8DAAH/SwAB/wMAAf8DAAH/SwAB
|
||||
/wMAAf8DAAH/lAADBQEGAwAB/wMAAf8DAAH/AwAB/wMAAf8DAAH/AwAB/wMdASgkAANZAe4DAAH/AwAB
|
||||
/wMAAf8EAS8AAf8DAAH/CAADGAEhAwAB/wMAAf8DAAH/AwAB/wNcActPAAH/AwAB/wMAAf9LAAH/AwAB
|
||||
/wMAAf+UAAMFAQYDAAH/AwAB/wMAAf8DAAH/A2AB4wMAAf8DAAH/AwAB/wMAAf8DUAGfFwAB/wMAAf8D
|
||||
AAH/AwAB/wMAAf83AAH/AwAB/wcAAf8DAAH/AwAB/wMAAf8DAAH/VwAB/wMAAf8DAAH/AwAB/wMqAUAD
|
||||
KgFAAyoBQAMqAUADKgFAAyoBQAMqAUADKgFAAyoBQAMqAUADKgFAAyoBQAMqAUADKgFAAyoBQAMqAUAD
|
||||
AAH/AwAB/wMAAf8DAAH/mwAB/wMAAf8DAAH/Ax0BKQQAAwIBAwMAAf8DAAH/AwAB/wMAAf8DAAH/AwAB
|
||||
/wMAAf8DAAH/AwAB/wMAAf8DAAH/AwAB/wMAAf87AAH/AwAB/wMAAf8DAAH/AwAB/wMAAf8DBwEJWAAD
|
||||
WQHDAwAB/wMAAf8DAAH/AwAB/wMAAf8DAAH/AwAB/wMAAf8DAAH/AwAB/wMAAf8DAAH/AwAB/wMAAf8D
|
||||
AAH/AwAB/wMAAf8DAAH/AwAB/wMAAf8DAAH/AwAB/5wAAxIBFwMAAf8UAANKAYkDAAH/AwAB/wMAAf8D
|
||||
AAH/AwAB/wMAAf8DAAH/AwAB/wMAAf9DAAH/AwAB/wMAAf8DAAH/AyEB+2cAAf8DAAH/AwAB/wMAAf8D
|
||||
AAH/AwAB/wMAAf8DAAH/AwAB/wMAAf8DAAH/AwAB/wMAAf8DAAH/AwAB/wMAAf8DAAH/AwAB/wMAAf8D
|
||||
AAH/AwAB/wNbAcXIAANGAYADWgG/Ay4BSFQAA1oBv3QAA0YBgANaAb8DWgG/A1oBvwNaAb8DWgG/A1oB
|
||||
vwNaAb8DWgG/A1oBvwNaAb8DWgG/A1oBvwNaAb8DWgG/A1oBvwNaAb8DLgFIpwAB/wMAAf8DAAH/AwAB
|
||||
/wMAAf8DAAH/AwAB/wMAAf8DAAH/FAADPQFpAwAB/wMAAf8DAAH/AwAB/wMAAf8DAAH/AwAB/wMAAf8D
|
||||
AAH/EwAB/wMAAf8DAAH/LwAB/wMAAf8DAAH/KAADGQEiAwAB/wMAAf8jAAH/AwAB/wMAAf8oAAM/AW0D
|
||||
AAH/AwAB/zsAAf8DAAH/AwAB/wMAAf8DAAH/AwAB/wMAAf8TAAH/AwAB/wMAAf8DAAH/AwAB/wMAAf8D
|
||||
AAH/KwAB/wMAAf8DAAH/AwAB/wMAAf8DAAH/AwAB/wMAAf8DAAH/FAADAwEEAwAB/wMAAf8DAAH/AwAB
|
||||
/wMAAf8DAAH/AwAB/wMAAf8DAAH/DAADVAGrAwAB/wMAAf8DAAH/AwAB/yQAA1YBtgMAAf8DAAH/AwAB
|
||||
/wMAAf8kAAM1AfkDAAH/AwAB/xwAA1cB8QMAAf8DAAH/AwAB/wMAAf8DPAH4IwAB/wMAAf8DAAH/NwAB
|
||||
/wMAAf8DAAH/AwAB/wMAAf8DAAH/AwAB/wMAAf8DAAH/CwAB/wMAAf8DAAH/AwAB/wMAAf8DAAH/AwAB
|
||||
/wMAAf8DAAH/JwAB/wMAAf8DAAH/AwAB/wMAAf87AAH/AwAB/wMAAf8DAAH/AwAB/wwAA1QBqwMAAf8D
|
||||
AAH/AwAB/wMAAf8DAAH/IAADVgG1AwAB/wMAAf8DAAH/AwAB/wMAAf8gAAM1AfkDAAH/AwAB/xwAA1cB
|
||||
8QMAAf8DAAH/AwAB/wMAAf8DAAH/AwAB/x8AAf8DAAH/AwAB/zcAAf8DAAH/AwAB/wMAAf8DAAH/AwAB
|
||||
/wMAAf8DAAH/AwAB/wsAAf8DAAH/AwAB/wMAAf8DAAH/AwAB/wMAAf8DAAH/AwAB/ycAAf8DAAH/AwAB
|
||||
/wMAAf8DAAH/AxIB/jMAAf8DAAH/AwAB/wMAAf8DAAH/AwAB/wwAA1QBqwMAAf8DAAH/AwAB/wMAAf8D
|
||||
AAH/AwAB/wNZAbsYAANWAbUDAAH/AwAB/wMAAf8DAAH/AwAB/wMAAf8DUQGeGAADNQH5AwAB/wMAAf8c
|
||||
AANXAfEDAAH/AwAB/wMAAf8DAAH/AwAB/wMAAf8DAAH/A1ABnRcAAf8DAAH/AwAB/zcAAf8DAAH/AwAB
|
||||
/w8AAf8DAAH/AwAB/wsAAf8DAAH/AwAB/w8AAf8DAAH/AwAB/ycAAf8DAAH/A1YBswMAAf8DAAH/AwAB
|
||||
/wMAAf8rAAH/AwAB/wMAAf8DAAH/AwAB/wMAAf8DAAH/DAADVAGrAwAB/wMAAf8DPQFoAwAB/wMAAf8D
|
||||
AAH/AwAB/wMAAf8UAANWAbUDAAH/AwAB/wMzAVADAAH/AwAB/wMAAf8DAAH/AwAB/xQAAzUB+QMAAf8D
|
||||
AAH/HAADVwHxAwAB/wMAAf8DEQEWBAEDAAH/AwAB/wMAAf8DAAH/AwAB/xMAAf8DAAH/AwAB/zcAAf8D
|
||||
AAH/AwAB/w8AAf8DAAH/AwAB/wsAAf8DAAH/AwAB/w8AAf8DAAH/AwAB/ycAAf8DAAH/A1YBswcAAf8D
|
||||
AAH/AwAB/wMAAf8jAAH/AwAB/wMAAf8DAAH/BwAB/wMAAf8DAAH/DAADVAGrAwAB/wMAAf8DPQFoBwAB
|
||||
/wMAAf8DAAH/AwAB/wMAAf8QAANWAbUDAAH/AwAB/wMzAVAHAAH/AwAB/wMAAf8DAAH/AwAB/xAAAzUB
|
||||
+QMAAf8DAAH/HAADVwHxAwAB/wMAAf8DEQEWCwAB/wMAAf8DAAH/AwAB/wMAAf8PAAH/AwAB/wMAAf83
|
||||
AAH/AwAB/wMAAf8PAAH/AwAB/wMAAf8LAAH/AwAB/wMAAf8PAAH/AwAB/wMAAf8nAAH/AwAB/wNWAbML
|
||||
AAH/AwAB/wMAAf8DEgH+GwAB/wMAAf8DAAH/AwAB/wsAAf8DAAH/AwAB/wwAA1QBqwMAAf8DAAH/Az0B
|
||||
aAgAA2AB2wMAAf8DAAH/AwAB/wMAAf8MAANWAbUDAAH/AwAB/wMzAVAIAANgAeMDAAH/AwAB/wMAAf8D
|
||||
AAH/DAADNQH5AwAB/wMAAf8cAANXAfEDAAH/AwAB/wMRARYMAAMzAVIDAAH/AwAB/wMAAf8DAAH/AwAB
|
||||
/wcAAf8DAAH/AwAB/zcAAf8DAAH/AwAB/w8AAf8DAAH/AwAB/wsAAf8DAAH/AwAB/w8AAf8DAAH/AwAB
|
||||
/ycAAf8DAAH/A1YBsw8AAf8DAAH/AwAB/wMAAf8TAAH/AwAB/wMAAf8DAAH/DwAB/wMAAf8DAAH/DAAD
|
||||
VAGrAwAB/wMAAf8DPQFoDAADJQE3AwAB/wMAAf8DAAH/AwAB/wgAA1YBtQMAAf8DAAH/AzMBUAwAAzIB
|
||||
TwMAAf8DAAH/AwAB/wMAAf8IAAM1AfkDAAH/AwAB/xwAA1cB8QMAAf8DAAH/AxEBFhcAAf8DAAH/AwAB
|
||||
/wMAAf8DAAH/AxIB/gMAAf8DAAH/NwAB/wMAAf8DAAH/DwAB/wMAAf8DAAH/CwAB/wMAAf8DAAH/DwAB
|
||||
/wMAAf8DAAH/JwAB/wMAAf8XAAH/AwAB/wMAAf8DAAH/CwAB/wMAAf8DAAH/AwAB/xAAAwwBDwMAAf8D
|
||||
QgH2DAADVAGrAwAB/wMAAf8DPQFoFwAB/wMAAf8DAAH/AwAB/wQAA1YBtQMAAf8DAAH/AzMBUBcAAf8D
|
||||
AAH/AwAB/wMAAf8EAAM1AfkDAAH/AwAB/xwAA1cB8QMAAf8DAAH/AxEBFhgAA1oB6QMAAf8DAAH/AwAB
|
||||
/wMAAf8DAAH/AwAB/zcAAf8DAAH/AwAB/w8AAf8DAAH/AwAB/wsAAf8DAAH/AwAB/w8AAf8DAAH/AwAB
|
||||
/0cAAf8DAAH/AwAB/wMSAf4DAAH/AwAB/wMAAf8DAAH/LAADVAGrAwAB/wMAAf8DPQFoGwAB/wMAAf8D
|
||||
AAH/AwAB/wNdAc4DAAH/AwAB/wMzAVAbAAH/AwAB/wMAAf8DAAH/AyYB+gMAAf8DAAH/HAADVwHxAwAB
|
||||
/wMAAf8DEQEWIwAB/wMAAf8DAAH/AwAB/wMAAf83AAH/AwAB/wMAAf8PAAH/AwAB/wMAAf8LAAH/AwAB
|
||||
/wMAAf8PAAH/AwAB/wMAAf9LAAH/AwAB/wMAAf8DAAH/AwAB/wMAAf8wAANUAasDAAH/AwAB/wM9AWgf
|
||||
AAH/AwAB/wMAAf8DAAH/AwAB/wMAAf8DMwFQHwAB/wMAAf8DAAH/AwAB/wMAAf8DAAH/HAADVwHxAwAB
|
||||
/wMAAf8DEQEWJwAB/wMAAf8DAAH/AwAB/zcAAf8DAAH/AwAB/w8AAf8DAAH/AwAB/wsAAf8DAAH/AwAB
|
||||
/w8AAf8DAAH/AwAB/08AAf8DAAH/AwAB/wMAAf80AANUAasDAAH/AwAB/wM9AWgjAAH/AwAB/wMAAf8D
|
||||
AAH/AwAB/wMzAVAjAAH/AwAB/wMAAf8DAAH/AwAB/xwAA1cB8QMAAf8DAAH/AxEBFisAAf8DAAH/AwAB
|
||||
/zcAAf8DAAH/AwAB/w8AAf8DAAH/AwAB/wsAAf8DAAH/AwAB/w8AAf8DAAH/AwAB/0sAAf8DAAH/AwAB
|
||||
/wMAAf8DAAH/AxIB/jAAA1QBqwMAAf8DAAH/Az0BaB8AAf8DAAH/AwAB/wMAAf8DAAH/AwAB/wMzAVAf
|
||||
AAH/AwAB/wMAAf8DAAH/AwAB/wMAAf8cAANXAfEDAAH/AwAB/wMRARYkAANcAdkDAAH/AwAB/wMAAf83
|
||||
AAH/AwAB/wMAAf8PAAH/AwAB/wMAAf8LAAH/AwAB/wMAAf8PAAH/AwAB/wMAAf9HAAH/AwAB/wMAAf8D
|
||||
AAH/AwAB/wMAAf8DAAH/AwAB/ywAA1QBqwMAAf8DAAH/Az0BaBsAAf8DAAH/AwAB/wMAAf8DVwHxAwAB
|
||||
/wMAAf8DMwFQGwAB/wMAAf8DAAH/AwAB/wMSAf4DAAH/AwAB/xwAA1cB8QMAAf8DAAH/AxEBFiMAAf8D
|
||||
AAH/AwAB/wMAAf8DAAH/NwAB/wMAAf8DAAH/DwAB/wMAAf8DAAH/CwAB/wMAAf8DAAH/DwAB/wMAAf8D
|
||||
AAH/JwAB/wMAAf8XAAH/AwAB/wMAAf8DAAH/CwAB/wMAAf8DAAH/AwAB/xcAAf8DWAG4DAADVAGrAwAB
|
||||
/wMAAf8DPQFoFwAB/wMAAf8DAAH/AwAB/wQAA1YBtQMAAf8DAAH/AzMBUBcAAf8DAAH/AwAB/wMAAf8E
|
||||
AAM1AfkDAAH/AwAB/xwAA1cB8QMAAf8DAAH/AxEBFhgAAzABSgMAAf8DAAH/AwAB/wMAAf8DAAH/AwAB
|
||||
/zcAAf8DAAH/AwAB/w8AAf8DAAH/AwAB/wsAAf8DAAH/AwAB/w8AAf8DAAH/AwAB/ycAAf8DAAH/A1YB
|
||||
sw8AAf8DAAH/AwAB/wMAAf8TAAH/AwAB/wMAAf8DEgH+DwAB/wMAAf8DAAH/DAADVAGrAwAB/wMAAf8D
|
||||
PQFoEwAB/wMAAf8DAAH/AwAB/wgAA1YBtQMAAf8DAAH/AzMBUAwAAwkBDAMAAf8DAAH/AwAB/wMAAf8I
|
||||
AAM1AfkDAAH/AwAB/xwAA1cB8QMAAf8DAAH/AxEBFhcAAf8DAAH/AwAB/wMAAf8DAAH/AyQB/QMAAf8D
|
||||
AAH/NwAB/wMAAf8DAAH/DwAB/wMAAf8DAAH/CwAB/wMAAf8DAAH/DwAB/wMAAf8DAAH/JwAB/wMAAf8D
|
||||
VgGzCwAB/wMAAf8DAAH/AwAB/xsAAf8DAAH/AwAB/wMAAf8LAAH/AwAB/wMAAf8MAANUAasDAAH/AwAB
|
||||
/wM9AWgIAAM6AWADAAH/AwAB/wMAAf8DAAH/DAADVgG1AwAB/wMAAf8DMwFQCAADSwGMAwAB/wMAAf8D
|
||||
AAH/AwAB/wwAAzUB+QMAAf8DAAH/HAADVwHxAwAB/wMAAf8DEQEWDAADBwEJAwAB/wMAAf8DAAH/AwAB
|
||||
/wMAAf8HAAH/AwAB/wMAAf83AAH/AwAB/wMAAf8PAAH/AwAB/wMAAf8LAAH/AwAB/wMAAf8PAAH/AwAB
|
||||
/wMAAf8nAAH/AwAB/wNWAbMHAAH/AwAB/wMAAf8DAAH/IwAB/wMAAf8DAAH/AwAB/wcAAf8DAAH/AwAB
|
||||
/wwAA1QBqwMAAf8DAAH/Az0BaAcAAf8DAAH/AwAB/wMAAf8DAAH/EAADVgG1AwAB/wMAAf8DMwFQBwAB
|
||||
/wMAAf8DAAH/AwAB/wMAAf8QAAM1AfkDAAH/AwAB/xwAA1cB8QMAAf8DAAH/AxEBFgsAAf8DAAH/AwAB
|
||||
/wMAAf8DAAH/A08BmQsAAf8DAAH/AwAB/zcAAf8DAAH/AwAB/w8AAf8DAAH/AwAB/wsAAf8DAAH/AwAB
|
||||
/w8AAf8DAAH/AwAB/ycAAf8DAAH/A1YBswMAAf8DAAH/AwAB/wMAAf8rAAH/AwAB/wMAAf8DEgH+AwAB
|
||||
/wMAAf8DAAH/DAADVAGrAwAB/wMAAf8DPQFoAwAB/wMAAf8DAAH/AwAB/wMAAf8UAANWAbUDAAH/AwAB
|
||||
/wMzAVADAAH/AwAB/wMAAf8DAAH/AwAB/xQAAzUB+QMAAf8DAAH/HAADVwHxAwAB/wMAAf8DEQEWBwAB
|
||||
/wMAAf8DAAH/AwAB/wMAAf8TAAH/AwAB/wMAAf83AAH/AwAB/wMAAf8PAAH/AwAB/wMAAf8LAAH/AwAB
|
||||
/wMAAf8PAAH/AwAB/wMAAf8nAAH/AwAB/wMAAf8DAAH/AwAB/wMAAf8zAAH/AwAB/wMAAf8DAAH/AwAB
|
||||
/wMAAf8MAANUAasDAAH/AwAB/wMAAf8DAAH/AwAB/wMAAf8DTQH0GAADVgG1AwAB/wMAAf8DAAH/AwAB
|
||||
/wMAAf8DAAH/A10B7BgAAzUB+QMAAf8DAAH/HAADVwHxAwAB/wMAAf8DYQHrAwAB/wMAAf8DAAH/AwAB
|
||||
/wNNAfQXAAH/AwAB/wMAAf83AAH/AwAB/wMAAf8PAAH/AwAB/wMAAf8LAAH/AwAB/wMAAf8PAAH/AwAB
|
||||
/wMAAf8nAAH/AwAB/wMAAf8DAAH/AwAB/zsAAf8DAAH/AwAB/wMAAf8DAAH/DAADVAGrAwAB/wMAAf8D
|
||||
AAH/AwAB/wMAAf8DQwF3HAADVgG1AwAB/wMAAf8DAAH/AwAB/wMAAf8DMAFMHAADNQH5AwAB/wMAAf8c
|
||||
AANXAfEDAAH/AwAB/wMAAf8DAAH/AwAB/wMAAf8fAAH/AwAB/wMAAf83AAH/AwAB/wMAAf8DKgFAAyoB
|
||||
QAMqAUADAAH/AwAB/wMAAf8LAAH/AwAB/wMAAf8DKgFAAyoBQAMqAUADAAH/AwAB/wMAAf8nAAH/AwAB
|
||||
/wMAAf8DAAH/AwAB/wMAAf8DAAH/AwAB/wMAAf8bAAH/AwAB/wMAAf8DAAH/AwAB/wMAAf8DAAH/AwAB
|
||||
/wMAAf8MAANUAasDAAH/AwAB/wMAAf8DAAH/AwYBCCAAA1YBtgMAAf8DAAH/AwAB/wMAAf8DAgEDIAAD
|
||||
NQH5AwAB/wMAAf8cAANXAfEDAAH/AwAB/wMAAf8DAAH/AwAB/yMAAf8DAAH/AwAB/zcAAf8DAAH/AwAB
|
||||
/wMAAf8DAAH/AwAB/wMAAf8DAAH/AwAB/wsAAf8DAAH/AwAB/wMAAf8DAAH/AwAB/wMAAf8DAAH/AwAB
|
||||
/ycAAf8DAAH/AwAB/wMAAf8DAAH/AwAB/wMAAf8DAAH/AwAB/xQAA0wBkAMAAf8DAAH/AwAB/wMAAf8D
|
||||
AAH/AwAB/wMAAf8DAAH/AwAB/xMAAf8DAAH/AwAB/y8AAf8DAAH/AwAB/ygAAzYBWAMAAf8DAAH/IwAB
|
||||
/wMAAf8DAAH/Aw0BESQAA1UBrQMAAf8DAAH/OwAB/wMAAf8DAAH/AwAB/wMAAf8DAAH/AwAB/xMAAf8D
|
||||
AAH/AwAB/wMAAf8DAAH/AwAB/wMAAf8sAANaAb8DWgG/A1oBvwNaAb8DWgG/A1oBvwNaAb8DFQEcGAAD
|
||||
TwGZA1oBvwNaAb8DWgG/A1oBvwNaAb8DWgG/A1oBvxgAA04BlzQAA0sBjTAAAwwBDygAA1ABmjAAAygB
|
||||
PEAAAzgBWwNaAb8DWgG/A1oBvwMwAUwYAANEAXoDWgG/A1oBvwNaAb8DIAEtHAABQgFNAT4HAAE+AwAB
|
||||
KAMAAXwDAAEwAwABAQEAAQEGAAEDFgAD/wEAAf8B4AEHAf8B+AE/Av8B4AIAAXgEAAH/AcABAQH/AfgB
|
||||
HwL/AcACAAF4BAAB/wGDAeAB/wH5AQcC/wHAAgABOAQAAf8BjwH4AX8B+QGBAv8BxwH/Af4BOAQAAv8B
|
||||
/AE/AfkB4AL/AccB/wH+ATgEAAL/Af4BHwH5AfgBPwH/AccB/wH+ATgEAAP/AR8B+QH8AQ8B/wHHAf8B
|
||||
/gE4BAAD/wGPAfkB/wEHAf8BxwH/Af4BOAQAA/8BjwH5Af8BgQH/AccB/wH+ATgEAAP/AY8B+QH/AeAB
|
||||
/wHHAf8B/gE4BAAD/wHPAfkB/wH4AX8BxwH/Af4BOAQAA/8BzwH5Af8B/AF/AccB/wH+ATgEAAP/Ac8B
|
||||
+QH/AfgBfwHHAf8B/gE4BAAB8AEPAf8BjwH5Af8B4AF/AccB/wH+ATgEAAHgAQcB/wGPAfkB/wHBAf8B
|
||||
xwH/Af4BOAQAAeABBwH/AY8B+QH/AQcB/wHHAf8B/gE4BAAB4wGHAf8BHwH5AfwBDwH/AccB/wH+ATgE
|
||||
AAHjAQ8B/wEfAfkB+AE/Af8BxwH/Af4BOAQAAeIBHwH+AT8B+QHgAv8BxwH/Af4BOAQAAeABDwH4AT8B
|
||||
+QGBAv8BxwH/Af4BOAQAAeABAwHgAf8B+QEHAv8BwAIAATgEAAHwAYABAQH/AfgBDwL/AcACAAF4BAAB
|
||||
8wHgAQcB/wH4AT8C/wHgAgABeAQAAf8B/gE/Af8B/gP/AfgBAAEBAfgEAAHwAQcBwAEPAR8B/AF/AeMB
|
||||
/AF/AeMB/wHwAR4BAwLwAQcBwAEOAQ8B+AE/AeMB+AEfAeMB/wHgAQwBAQLwAX8B/gEOAQcB+AEfAeMB
|
||||
+AEPAeMB/wHgAQwBAQLwAT8B/AEOAQEB+AEHAeMB+AEDAeMB/wHjAYwBcQLwAR8B+AEOAQAB+AEDAeMB
|
||||
+AEBAeMB/wHjAYwBcQHwAfEBDwHwAY4BEAF4AUEB4wH4AWAB4wH/AeMBjAFxAfAB8QGHAeEBjgEYATgB
|
||||
YAHjAfgBcAEjAf8B4wGMAXEB8AHxAsMBjgEcARgBcAFjAfgBfAEDAf8B4wGMAXEB8AHzAeEBhwGOAR8B
|
||||
CAF8ASMB+AF+AQMB/wHjAYwBcQHwAf8B8AEPAf4BHwGAAX4BAwH4AX8BgwH/AeMBjAFxAfAB/wH4AR8B
|
||||
/gEfAcABfwEDAfgBfwHDAf8B4wGMAXEB8AH/AfwBPwH+AR8B4AF/AYMB+AF/AeMB/wHjAYwBcQHwAf8B
|
||||
+AEfAf4BHwHAAX8BAwH4AX8BwwH/AeMBjAFxAfAB/wHwAQ8B/gEfAYABfgEDAfgBfwGDAf8B4wGMAXEB
|
||||
8AHzAeEBhwHOAR8BCAF8ASMB+AF+AQMB/wHjAYwBcQHwAfECwwGOAR4BGAFwAWMB+AF8AQMB/wHjAYwB
|
||||
cQHwAfEBhwHhAY4BGAE4AWAB4wH4AXABIwH/AeMBjAFxAfAB8QEPAfABjgEQAXgBQQHjAfgBYAFjAf8B
|
||||
4wGMAXEC8AEfAfgBDgEAAfgBAwHjAfgBQQHjAf8B4wGMAXEC8AE/AfwBDgEBAfgBBwHjAfgBAwHjAf8B
|
||||
4wGMAXEC8AF/Af4BDgEDAfgBDwHjAfgBDwHjAf8B4AEMAQEC8AEHAeABDgEHAfgBHwHjAfgBHwHjAf8B
|
||||
4AEMAQEC8AEHAcABDwEfAfwBfwHjAfwBPwHjAf8B8AEeAQMB8AH4AQcB4AEfAb8B/gH/AfcB/gH/AfcB
|
||||
/wH4AT8BBwHwCw==
|
||||
</value>
|
||||
</data>
|
||||
<metadata name="toolTip.TrayLocation" type="System.Drawing.Point, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a">
|
||||
<value>165, 17</value>
|
||||
</metadata>
|
||||
<metadata name="spinePreviewFullScreenForm.TrayLocation" type="System.Drawing.Point, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a">
|
||||
<value>307, 18</value>
|
||||
</metadata>
|
||||
<metadata name="wallpaperForm.TrayLocation" type="System.Drawing.Point, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a">
|
||||
<value>618, 18</value>
|
||||
</metadata>
|
||||
</root>
|
||||
63
SpineViewer/Controls/SpinePreviewer.Designer.cs
generated
63
SpineViewer/Controls/SpinePreviewer.Designer.cs
generated
@@ -1,63 +0,0 @@
|
||||
namespace SpineViewer.Controls
|
||||
{
|
||||
partial class SpinePreviewer
|
||||
{
|
||||
/// <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()
|
||||
{
|
||||
panel = new Panel();
|
||||
SuspendLayout();
|
||||
//
|
||||
// panel
|
||||
//
|
||||
panel.BackColor = SystemColors.ControlDarkDark;
|
||||
panel.Location = new Point(160, 160);
|
||||
panel.Margin = new Padding(0);
|
||||
panel.Name = "panel";
|
||||
panel.Size = new Size(320, 320);
|
||||
panel.TabIndex = 1;
|
||||
panel.MouseDown += panel_MouseDown;
|
||||
panel.MouseMove += panel_MouseMove;
|
||||
panel.MouseUp += panel_MouseUp;
|
||||
panel.MouseWheel += panel_MouseWheel;
|
||||
//
|
||||
// SpinePreviewer
|
||||
//
|
||||
AutoScaleDimensions = new SizeF(11F, 24F);
|
||||
AutoScaleMode = AutoScaleMode.Font;
|
||||
BackColor = SystemColors.ControlDark;
|
||||
Controls.Add(panel);
|
||||
Name = "SpinePreviewer";
|
||||
Size = new Size(640, 640);
|
||||
SizeChanged += SpinePreviewer_SizeChanged;
|
||||
ResumeLayout(false);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
private Panel panel;
|
||||
}
|
||||
}
|
||||
@@ -1,465 +0,0 @@
|
||||
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.Security.Policy;
|
||||
using System.Diagnostics;
|
||||
using NLog.Targets;
|
||||
|
||||
namespace SpineViewer.Controls
|
||||
{
|
||||
public partial class SpinePreviewer : UserControl
|
||||
{
|
||||
/// <summary>
|
||||
/// 包装类, 用于 PropertyGrid 显示
|
||||
/// </summary>
|
||||
private class PreviewerProperty
|
||||
{
|
||||
private readonly SpinePreviewer previewer;
|
||||
|
||||
public PreviewerProperty(SpinePreviewer previewer) { this.previewer = previewer; }
|
||||
|
||||
[TypeConverter(typeof(SizeTypeConverter))]
|
||||
[Category("导出"), DisplayName("分辨率")]
|
||||
public Size Resolution { get => previewer.Resolution; set => previewer.Resolution = value; }
|
||||
|
||||
[TypeConverter(typeof(PointFTypeConverter))]
|
||||
[Category("导出"), DisplayName("画面中心点")]
|
||||
public PointF Center { get => previewer.Center; set => previewer.Center = value; }
|
||||
|
||||
[Category("导出"), DisplayName("缩放")]
|
||||
public float Zoom { get => previewer.Zoom; set => previewer.Zoom = value; }
|
||||
|
||||
[Category("导出"), DisplayName("旋转")]
|
||||
public float Rotation { get => previewer.Rotation; set => previewer.Rotation = value; }
|
||||
|
||||
[Category("导出"), DisplayName("水平翻转")]
|
||||
public bool FlipX { get => previewer.FlipX; set => previewer.FlipX = value; }
|
||||
|
||||
[Category("导出"), DisplayName("垂直翻转")]
|
||||
public bool FlipY { get => previewer.FlipY; set => previewer.FlipY = 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; }
|
||||
}
|
||||
|
||||
[Category("自定义"), Description("相关联的 SpineListView")]
|
||||
public SpineListView? SpineListView { get; set; }
|
||||
|
||||
[Category("自定义"), Description("用于显示画面属性的属性页")]
|
||||
public PropertyGrid? PropertyGrid
|
||||
{
|
||||
get => propertyGrid;
|
||||
set
|
||||
{
|
||||
propertyGrid = value;
|
||||
if (propertyGrid is not null)
|
||||
propertyGrid.SelectedObject = new PreviewerProperty(this);
|
||||
}
|
||||
}
|
||||
private PropertyGrid? propertyGrid;
|
||||
|
||||
public const float ZOOM_MAX = 1000f;
|
||||
public const float ZOOM_MIN = 0.001f;
|
||||
public const int BACKGROUND_CELL_SIZE = 10;
|
||||
|
||||
private static readonly SFML.Graphics.Color BackgroundColor = new(105, 105, 105);
|
||||
private static readonly SFML.Graphics.Color AxisColor = new(220, 220, 220);
|
||||
private static readonly SFML.Graphics.Color BoundsColor = new(120, 200, 0);
|
||||
|
||||
private readonly SFML.Graphics.VertexArray AxisVertex = new(SFML.Graphics.PrimitiveType.Lines, 2);
|
||||
private readonly SFML.Graphics.VertexArray BoundsRect = new(SFML.Graphics.PrimitiveType.LineStrip, 5);
|
||||
|
||||
private readonly SFML.Graphics.RenderWindow RenderWindow;
|
||||
private readonly SFML.System.Clock Clock = new();
|
||||
private SFML.System.Vector2f? draggingSrc = null;
|
||||
private Spine.Spine? draggingSpine = null;
|
||||
|
||||
private Task? task = null;
|
||||
private CancellationTokenSource? cancelToken = null;
|
||||
|
||||
/// <summary>
|
||||
/// 分辨率
|
||||
/// </summary>
|
||||
[DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
|
||||
[Browsable(false)]
|
||||
public Size Resolution
|
||||
{
|
||||
get => resolution;
|
||||
set
|
||||
{
|
||||
if (value.Width <= 0) value.Width = 100;
|
||||
if (value.Height <= 0) value.Height = 100;
|
||||
|
||||
float parentX = Width;
|
||||
float parentY = Height;
|
||||
float sizeX = value.Width;
|
||||
float sizeY = value.Height;
|
||||
|
||||
if ((sizeY / sizeX) < (parentY / parentX))
|
||||
{
|
||||
// 相同的 X, 子窗口 Y 更小
|
||||
sizeY = parentX * sizeY / sizeX;
|
||||
sizeX = parentX;
|
||||
}
|
||||
else
|
||||
{
|
||||
// 相同的 Y, 子窗口 X 更小
|
||||
sizeX = parentY * sizeX / sizeY;
|
||||
sizeY = parentY;
|
||||
}
|
||||
|
||||
// 必须通过 SFML 的方法调整窗口
|
||||
RenderWindow.Position = new((int)(parentX - sizeX) / 2, (int)(parentY - sizeY) / 2);
|
||||
RenderWindow.Size = new((uint)sizeX, (uint)sizeY);
|
||||
|
||||
// 将 view 的大小设置成于 resolution 相同的大小, 其余属性都不变
|
||||
var view = RenderWindow.GetView();
|
||||
var signX = Math.Sign(view.Size.X);
|
||||
var signY = Math.Sign(view.Size.Y);
|
||||
view.Size = new(value.Width * signX, value.Height * signY);
|
||||
RenderWindow.SetView(view);
|
||||
|
||||
resolution = value;
|
||||
}
|
||||
}
|
||||
private Size resolution = new(0, 0);
|
||||
|
||||
/// <summary>
|
||||
/// 画面中心点
|
||||
/// </summary>
|
||||
[DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
|
||||
[Browsable(false)]
|
||||
public PointF Center
|
||||
{
|
||||
get
|
||||
{
|
||||
var center = RenderWindow.GetView().Center;
|
||||
return new(center.X, center.Y);
|
||||
}
|
||||
set
|
||||
{
|
||||
var view = RenderWindow.GetView();
|
||||
view.Center = new(value.X, value.Y);
|
||||
RenderWindow.SetView(view);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 画面缩放
|
||||
/// </summary>
|
||||
[DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
|
||||
[Browsable(false)]
|
||||
public float Zoom
|
||||
{
|
||||
get => resolution.Width / Math.Abs(RenderWindow.GetView().Size.X);
|
||||
set
|
||||
{
|
||||
value = Math.Clamp(value, ZOOM_MIN, ZOOM_MAX);
|
||||
var view = RenderWindow.GetView();
|
||||
var signX = Math.Sign(view.Size.X);
|
||||
var signY = Math.Sign(view.Size.Y);
|
||||
view.Size = new(resolution.Width / value * signX, resolution.Height / value * signY);
|
||||
RenderWindow.SetView(view);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 画面旋转
|
||||
/// </summary>
|
||||
[Browsable(false)]
|
||||
[DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
|
||||
public float Rotation
|
||||
{
|
||||
get => RenderWindow.GetView().Rotation;
|
||||
set
|
||||
{
|
||||
var view = RenderWindow.GetView();
|
||||
view.Rotation = value;
|
||||
RenderWindow.SetView(view);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 水平翻转
|
||||
/// </summary>
|
||||
[DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
|
||||
[Browsable(false)]
|
||||
public bool FlipX
|
||||
{
|
||||
get => RenderWindow.GetView().Size.X < 0;
|
||||
set
|
||||
{
|
||||
var view = RenderWindow.GetView();
|
||||
var size = view.Size;
|
||||
if (size.X > 0 && value || size.X < 0 && !value)
|
||||
size.X *= -1;
|
||||
view.Size = size;
|
||||
RenderWindow.SetView(view);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 垂直翻转
|
||||
/// </summary>
|
||||
[DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
|
||||
[Browsable(false)]
|
||||
public bool FlipY
|
||||
{
|
||||
get => RenderWindow.GetView().Size.Y < 0;
|
||||
set
|
||||
{
|
||||
var view = RenderWindow.GetView();
|
||||
var size = view.Size;
|
||||
if (size.Y > 0 && value || size.Y < 0 && !value)
|
||||
size.Y *= -1;
|
||||
view.Size = size;
|
||||
RenderWindow.SetView(view);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 显示坐标轴
|
||||
/// </summary>
|
||||
[DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
|
||||
[Browsable(false)]
|
||||
public bool ShowAxis { get; set; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// 显示包围盒
|
||||
/// </summary>
|
||||
[DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
|
||||
[Browsable(false)]
|
||||
public bool ShowBounds { get; set; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// 最大帧率
|
||||
/// </summary>
|
||||
[DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
|
||||
[Browsable(false)]
|
||||
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();
|
||||
RenderWindow = new(panel.Handle);
|
||||
RenderWindow.SetActive(false);
|
||||
|
||||
// 设置默认参数
|
||||
Resolution = new(2048, 2048);
|
||||
Center = new(0, 0);
|
||||
FlipY = true;
|
||||
MaxFps = 30;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 开始预览
|
||||
/// </summary>
|
||||
public void StartPreview()
|
||||
{
|
||||
if (task is not null)
|
||||
return;
|
||||
cancelToken = new();
|
||||
task = Task.Run(RenderTask, cancelToken.Token);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 停止预览
|
||||
/// </summary>
|
||||
public void StopPreview()
|
||||
{
|
||||
if (task is null || cancelToken is null)
|
||||
return;
|
||||
cancelToken.Cancel();
|
||||
task.Wait();
|
||||
cancelToken = null;
|
||||
task = null;
|
||||
}
|
||||
|
||||
private void SpinePreviewer_SizeChanged(object sender, EventArgs e)
|
||||
{
|
||||
if (RenderWindow is null)
|
||||
return;
|
||||
|
||||
float parentX = Width;
|
||||
float parentY = Height;
|
||||
float sizeX = panel.Width;
|
||||
float sizeY = panel.Height;
|
||||
|
||||
if ((sizeY / sizeX) < (parentY / parentX))
|
||||
{
|
||||
// 相同的 X, 子窗口 Y 更小
|
||||
sizeY = parentX * sizeY / sizeX;
|
||||
sizeX = parentX;
|
||||
}
|
||||
else
|
||||
{
|
||||
// 相同的 Y, 子窗口 X 更小
|
||||
sizeX = parentY * sizeX / sizeY;
|
||||
sizeY = parentY;
|
||||
}
|
||||
|
||||
// 必须通过 SFML 的方法调整窗口
|
||||
RenderWindow.Position = new((int)(parentX - sizeX) / 2, (int)(parentY - sizeY) / 2);
|
||||
RenderWindow.Size = new((uint)sizeX, (uint)sizeY);
|
||||
}
|
||||
|
||||
private void panel_MouseDown(object sender, MouseEventArgs e)
|
||||
{
|
||||
// 右键优先级高, 进入画面拖动模式, 需要重新记录源点
|
||||
if ((e.Button & MouseButtons.Right) != 0)
|
||||
{
|
||||
draggingSpine = null;
|
||||
draggingSrc = RenderWindow.MapPixelToCoords(new(e.X, e.Y));
|
||||
Cursor = Cursors.Hand;
|
||||
}
|
||||
// 按下了左键并且右键是松开的
|
||||
else if ((e.Button & MouseButtons.Left) != 0 && (MouseButtons & MouseButtons.Right) == 0)
|
||||
{
|
||||
draggingSrc = RenderWindow.MapPixelToCoords(new(e.X, e.Y));
|
||||
var src = new PointF(((SFML.System.Vector2f)draggingSrc).X, ((SFML.System.Vector2f)draggingSrc).Y);
|
||||
|
||||
if (SpineListView is not null)
|
||||
{
|
||||
lock (SpineListView.Spines)
|
||||
{
|
||||
foreach (var spine in SpineListView.Spines)
|
||||
{
|
||||
if (spine.Bounds.Contains(src))
|
||||
{
|
||||
draggingSpine = spine;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void panel_MouseMove(object sender, MouseEventArgs e)
|
||||
{
|
||||
if (draggingSrc is null)
|
||||
return;
|
||||
|
||||
var src = (SFML.System.Vector2f)draggingSrc;
|
||||
var dst = RenderWindow.MapPixelToCoords(new(e.X, e.Y));
|
||||
var _delta = dst - src;
|
||||
var delta = new SizeF(_delta.X, _delta.Y);
|
||||
|
||||
if ((e.Button & MouseButtons.Right) != 0)
|
||||
{
|
||||
Center -= delta;
|
||||
}
|
||||
else if ((e.Button & MouseButtons.Left) != 0)
|
||||
{
|
||||
if (draggingSpine is not null)
|
||||
draggingSpine.Position += delta;
|
||||
draggingSrc = dst;
|
||||
}
|
||||
}
|
||||
|
||||
private void panel_MouseUp(object sender, MouseEventArgs e)
|
||||
{
|
||||
// 右键高优先级, 结束画面拖动模式
|
||||
if ((e.Button & MouseButtons.Right) != 0)
|
||||
{
|
||||
draggingSpine = null;
|
||||
SpineListView?.PropertyGrid?.Refresh();
|
||||
|
||||
draggingSrc = null;
|
||||
Cursor = Cursors.Default;
|
||||
PropertyGrid?.Refresh();
|
||||
}
|
||||
// 按下了左键并且右键是松开的
|
||||
else if ((e.Button & MouseButtons.Left) != 0 && (MouseButtons & MouseButtons.Right) == 0)
|
||||
{
|
||||
draggingSrc = null;
|
||||
draggingSpine = null;
|
||||
SpineListView?.PropertyGrid?.Refresh();
|
||||
}
|
||||
}
|
||||
|
||||
private void panel_MouseWheel(object sender, MouseEventArgs e)
|
||||
{
|
||||
Zoom *= (e.Delta > 0 ? 1.1f : 0.9f);
|
||||
PropertyGrid?.Refresh();
|
||||
}
|
||||
|
||||
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)
|
||||
{
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
319
SpineViewer/Controls/SpineViewPropertyGrid.Designer.cs
generated
Normal file
319
SpineViewer/Controls/SpineViewPropertyGrid.Designer.cs
generated
Normal file
@@ -0,0 +1,319 @@
|
||||
namespace SpineViewer.Controls
|
||||
{
|
||||
partial class SpineViewPropertyGrid
|
||||
{
|
||||
/// <summary>
|
||||
/// 必需的设计器变量。
|
||||
/// </summary>
|
||||
private System.ComponentModel.IContainer components = null;
|
||||
|
||||
/// <summary>
|
||||
/// 清理所有正在使用的资源。
|
||||
/// </summary>
|
||||
/// <param name="disposing">如果应释放托管资源,为 true;否则为 false。</param>
|
||||
protected override void Dispose(bool disposing)
|
||||
{
|
||||
if (disposing && (components != null))
|
||||
{
|
||||
components.Dispose();
|
||||
}
|
||||
base.Dispose(disposing);
|
||||
}
|
||||
|
||||
#region 组件设计器生成的代码
|
||||
|
||||
/// <summary>
|
||||
/// 设计器支持所需的方法 - 不要修改
|
||||
/// 使用代码编辑器修改此方法的内容。
|
||||
/// </summary>
|
||||
private void InitializeComponent()
|
||||
{
|
||||
components = new System.ComponentModel.Container();
|
||||
tabControl = new TabControl();
|
||||
tabPage_BaseInfo = new TabPage();
|
||||
propertyGrid_BaseInfo = new PropertyGrid();
|
||||
tabPage_Render = new TabPage();
|
||||
propertyGrid_Render = new PropertyGrid();
|
||||
tabPage_Transform = new TabPage();
|
||||
propertyGrid_Transform = new PropertyGrid();
|
||||
tabPage_Skin = new TabPage();
|
||||
propertyGrid_Skin = new PropertyGrid();
|
||||
contextMenuStrip_Skin = new ContextMenuStrip(components);
|
||||
toolStripMenuItem_ReloadSkins = new ToolStripMenuItem();
|
||||
tabPage_Slot = new TabPage();
|
||||
propertyGrid_Slot = new PropertyGrid();
|
||||
tabPage_Animation = new TabPage();
|
||||
propertyGrid_Animation = new PropertyGrid();
|
||||
contextMenuStrip_Animation = new ContextMenuStrip(components);
|
||||
toolStripMenuItem_AddAnimation = new ToolStripMenuItem();
|
||||
toolStripMenuItem_RemoveAnimation = new ToolStripMenuItem();
|
||||
tabPage_Debug = new TabPage();
|
||||
propertyGrid_Debug = new PropertyGrid();
|
||||
tabControl.SuspendLayout();
|
||||
tabPage_BaseInfo.SuspendLayout();
|
||||
tabPage_Render.SuspendLayout();
|
||||
tabPage_Transform.SuspendLayout();
|
||||
tabPage_Skin.SuspendLayout();
|
||||
contextMenuStrip_Skin.SuspendLayout();
|
||||
tabPage_Slot.SuspendLayout();
|
||||
tabPage_Animation.SuspendLayout();
|
||||
contextMenuStrip_Animation.SuspendLayout();
|
||||
tabPage_Debug.SuspendLayout();
|
||||
SuspendLayout();
|
||||
//
|
||||
// tabControl
|
||||
//
|
||||
tabControl.Alignment = TabAlignment.Bottom;
|
||||
tabControl.Controls.Add(tabPage_BaseInfo);
|
||||
tabControl.Controls.Add(tabPage_Render);
|
||||
tabControl.Controls.Add(tabPage_Transform);
|
||||
tabControl.Controls.Add(tabPage_Skin);
|
||||
tabControl.Controls.Add(tabPage_Slot);
|
||||
tabControl.Controls.Add(tabPage_Animation);
|
||||
tabControl.Controls.Add(tabPage_Debug);
|
||||
tabControl.Dock = DockStyle.Fill;
|
||||
tabControl.ItemSize = new Size(90, 35);
|
||||
tabControl.Location = new Point(0, 0);
|
||||
tabControl.Multiline = true;
|
||||
tabControl.Name = "tabControl";
|
||||
tabControl.Padding = new Point(0, 0);
|
||||
tabControl.SelectedIndex = 0;
|
||||
tabControl.Size = new Size(372, 448);
|
||||
tabControl.SizeMode = TabSizeMode.FillToRight;
|
||||
tabControl.TabIndex = 0;
|
||||
//
|
||||
// tabPage_BaseInfo
|
||||
//
|
||||
tabPage_BaseInfo.BackColor = SystemColors.Control;
|
||||
tabPage_BaseInfo.Controls.Add(propertyGrid_BaseInfo);
|
||||
tabPage_BaseInfo.Location = new Point(4, 4);
|
||||
tabPage_BaseInfo.Margin = new Padding(0);
|
||||
tabPage_BaseInfo.Name = "tabPage_BaseInfo";
|
||||
tabPage_BaseInfo.Size = new Size(364, 370);
|
||||
tabPage_BaseInfo.TabIndex = 0;
|
||||
tabPage_BaseInfo.Text = "基本信息";
|
||||
//
|
||||
// propertyGrid_BaseInfo
|
||||
//
|
||||
propertyGrid_BaseInfo.Dock = DockStyle.Fill;
|
||||
propertyGrid_BaseInfo.HelpVisible = false;
|
||||
propertyGrid_BaseInfo.Location = new Point(0, 0);
|
||||
propertyGrid_BaseInfo.Name = "propertyGrid_BaseInfo";
|
||||
propertyGrid_BaseInfo.PropertySort = PropertySort.Alphabetical;
|
||||
propertyGrid_BaseInfo.Size = new Size(364, 370);
|
||||
propertyGrid_BaseInfo.TabIndex = 0;
|
||||
propertyGrid_BaseInfo.ToolbarVisible = false;
|
||||
//
|
||||
// tabPage_Render
|
||||
//
|
||||
tabPage_Render.BackColor = SystemColors.Control;
|
||||
tabPage_Render.Controls.Add(propertyGrid_Render);
|
||||
tabPage_Render.Location = new Point(4, 4);
|
||||
tabPage_Render.Margin = new Padding(0);
|
||||
tabPage_Render.Name = "tabPage_Render";
|
||||
tabPage_Render.Size = new Size(364, 370);
|
||||
tabPage_Render.TabIndex = 1;
|
||||
tabPage_Render.Text = "渲染";
|
||||
//
|
||||
// propertyGrid_Render
|
||||
//
|
||||
propertyGrid_Render.Dock = DockStyle.Fill;
|
||||
propertyGrid_Render.HelpVisible = false;
|
||||
propertyGrid_Render.Location = new Point(0, 0);
|
||||
propertyGrid_Render.Name = "propertyGrid_Render";
|
||||
propertyGrid_Render.PropertySort = PropertySort.Alphabetical;
|
||||
propertyGrid_Render.Size = new Size(364, 370);
|
||||
propertyGrid_Render.TabIndex = 1;
|
||||
propertyGrid_Render.ToolbarVisible = false;
|
||||
//
|
||||
// tabPage_Transform
|
||||
//
|
||||
tabPage_Transform.BackColor = SystemColors.Control;
|
||||
tabPage_Transform.Controls.Add(propertyGrid_Transform);
|
||||
tabPage_Transform.Location = new Point(4, 4);
|
||||
tabPage_Transform.Margin = new Padding(0);
|
||||
tabPage_Transform.Name = "tabPage_Transform";
|
||||
tabPage_Transform.Size = new Size(364, 370);
|
||||
tabPage_Transform.TabIndex = 2;
|
||||
tabPage_Transform.Text = "变换";
|
||||
//
|
||||
// propertyGrid_Transform
|
||||
//
|
||||
propertyGrid_Transform.Dock = DockStyle.Fill;
|
||||
propertyGrid_Transform.HelpVisible = false;
|
||||
propertyGrid_Transform.Location = new Point(0, 0);
|
||||
propertyGrid_Transform.Name = "propertyGrid_Transform";
|
||||
propertyGrid_Transform.PropertySort = PropertySort.Alphabetical;
|
||||
propertyGrid_Transform.Size = new Size(364, 370);
|
||||
propertyGrid_Transform.TabIndex = 1;
|
||||
propertyGrid_Transform.ToolbarVisible = false;
|
||||
//
|
||||
// tabPage_Skin
|
||||
//
|
||||
tabPage_Skin.BackColor = SystemColors.Control;
|
||||
tabPage_Skin.Controls.Add(propertyGrid_Skin);
|
||||
tabPage_Skin.Location = new Point(4, 4);
|
||||
tabPage_Skin.Margin = new Padding(0);
|
||||
tabPage_Skin.Name = "tabPage_Skin";
|
||||
tabPage_Skin.Size = new Size(364, 370);
|
||||
tabPage_Skin.TabIndex = 3;
|
||||
tabPage_Skin.Text = "皮肤";
|
||||
//
|
||||
// propertyGrid_Skin
|
||||
//
|
||||
propertyGrid_Skin.ContextMenuStrip = contextMenuStrip_Skin;
|
||||
propertyGrid_Skin.Dock = DockStyle.Fill;
|
||||
propertyGrid_Skin.HelpVisible = false;
|
||||
propertyGrid_Skin.Location = new Point(0, 0);
|
||||
propertyGrid_Skin.Name = "propertyGrid_Skin";
|
||||
propertyGrid_Skin.PropertySort = PropertySort.NoSort;
|
||||
propertyGrid_Skin.Size = new Size(364, 370);
|
||||
propertyGrid_Skin.TabIndex = 1;
|
||||
propertyGrid_Skin.ToolbarVisible = false;
|
||||
//
|
||||
// contextMenuStrip_Skin
|
||||
//
|
||||
contextMenuStrip_Skin.ImageScalingSize = new Size(24, 24);
|
||||
contextMenuStrip_Skin.Items.AddRange(new ToolStripItem[] { toolStripMenuItem_ReloadSkins });
|
||||
contextMenuStrip_Skin.Name = "contextMenuStrip1";
|
||||
contextMenuStrip_Skin.Size = new Size(241, 67);
|
||||
//
|
||||
// toolStripMenuItem_ReloadSkins
|
||||
//
|
||||
toolStripMenuItem_ReloadSkins.Name = "toolStripMenuItem_ReloadSkins";
|
||||
toolStripMenuItem_ReloadSkins.Size = new Size(240, 30);
|
||||
toolStripMenuItem_ReloadSkins.Text = "重新加载皮肤";
|
||||
toolStripMenuItem_ReloadSkins.Click += toolStripMenuItem_ReloadSkins_Click;
|
||||
//
|
||||
// tabPage_Slot
|
||||
//
|
||||
tabPage_Slot.BackColor = SystemColors.Control;
|
||||
tabPage_Slot.Controls.Add(propertyGrid_Slot);
|
||||
tabPage_Slot.Location = new Point(4, 4);
|
||||
tabPage_Slot.Margin = new Padding(0);
|
||||
tabPage_Slot.Name = "tabPage_Slot";
|
||||
tabPage_Slot.Size = new Size(364, 370);
|
||||
tabPage_Slot.TabIndex = 6;
|
||||
tabPage_Slot.Text = "插槽";
|
||||
//
|
||||
// propertyGrid_Slot
|
||||
//
|
||||
propertyGrid_Slot.Dock = DockStyle.Fill;
|
||||
propertyGrid_Slot.HelpVisible = false;
|
||||
propertyGrid_Slot.Location = new Point(0, 0);
|
||||
propertyGrid_Slot.Name = "propertyGrid_Slot";
|
||||
propertyGrid_Slot.PropertySort = PropertySort.Alphabetical;
|
||||
propertyGrid_Slot.Size = new Size(364, 370);
|
||||
propertyGrid_Slot.TabIndex = 2;
|
||||
propertyGrid_Slot.ToolbarVisible = false;
|
||||
//
|
||||
// tabPage_Animation
|
||||
//
|
||||
tabPage_Animation.BackColor = SystemColors.Control;
|
||||
tabPage_Animation.Controls.Add(propertyGrid_Animation);
|
||||
tabPage_Animation.Location = new Point(4, 4);
|
||||
tabPage_Animation.Margin = new Padding(0);
|
||||
tabPage_Animation.Name = "tabPage_Animation";
|
||||
tabPage_Animation.Size = new Size(364, 370);
|
||||
tabPage_Animation.TabIndex = 4;
|
||||
tabPage_Animation.Text = "动画";
|
||||
//
|
||||
// propertyGrid_Animation
|
||||
//
|
||||
propertyGrid_Animation.ContextMenuStrip = contextMenuStrip_Animation;
|
||||
propertyGrid_Animation.Dock = DockStyle.Fill;
|
||||
propertyGrid_Animation.HelpVisible = false;
|
||||
propertyGrid_Animation.Location = new Point(0, 0);
|
||||
propertyGrid_Animation.Name = "propertyGrid_Animation";
|
||||
propertyGrid_Animation.PropertySort = PropertySort.NoSort;
|
||||
propertyGrid_Animation.Size = new Size(364, 370);
|
||||
propertyGrid_Animation.TabIndex = 1;
|
||||
propertyGrid_Animation.ToolbarVisible = false;
|
||||
//
|
||||
// contextMenuStrip_Animation
|
||||
//
|
||||
contextMenuStrip_Animation.ImageScalingSize = new Size(24, 24);
|
||||
contextMenuStrip_Animation.Items.AddRange(new ToolStripItem[] { toolStripMenuItem_AddAnimation, toolStripMenuItem_RemoveAnimation });
|
||||
contextMenuStrip_Animation.Name = "contextMenuStrip1";
|
||||
contextMenuStrip_Animation.Size = new Size(117, 64);
|
||||
contextMenuStrip_Animation.Opening += contextMenuStrip_Animation_Opening;
|
||||
//
|
||||
// toolStripMenuItem_AddAnimation
|
||||
//
|
||||
toolStripMenuItem_AddAnimation.Name = "toolStripMenuItem_AddAnimation";
|
||||
toolStripMenuItem_AddAnimation.Size = new Size(116, 30);
|
||||
toolStripMenuItem_AddAnimation.Text = "添加";
|
||||
toolStripMenuItem_AddAnimation.Click += toolStripMenuItem_AddAnimation_Click;
|
||||
//
|
||||
// toolStripMenuItem_RemoveAnimation
|
||||
//
|
||||
toolStripMenuItem_RemoveAnimation.Name = "toolStripMenuItem_RemoveAnimation";
|
||||
toolStripMenuItem_RemoveAnimation.Size = new Size(116, 30);
|
||||
toolStripMenuItem_RemoveAnimation.Text = "移除";
|
||||
toolStripMenuItem_RemoveAnimation.Click += toolStripMenuItem_RemoveAnimation_Click;
|
||||
//
|
||||
// tabPage_Debug
|
||||
//
|
||||
tabPage_Debug.BackColor = SystemColors.Control;
|
||||
tabPage_Debug.Controls.Add(propertyGrid_Debug);
|
||||
tabPage_Debug.Location = new Point(4, 4);
|
||||
tabPage_Debug.Name = "tabPage_Debug";
|
||||
tabPage_Debug.Size = new Size(364, 370);
|
||||
tabPage_Debug.TabIndex = 5;
|
||||
tabPage_Debug.Text = "调试";
|
||||
//
|
||||
// propertyGrid_Debug
|
||||
//
|
||||
propertyGrid_Debug.Dock = DockStyle.Fill;
|
||||
propertyGrid_Debug.HelpVisible = false;
|
||||
propertyGrid_Debug.Location = new Point(0, 0);
|
||||
propertyGrid_Debug.Name = "propertyGrid_Debug";
|
||||
propertyGrid_Debug.PropertySort = PropertySort.NoSort;
|
||||
propertyGrid_Debug.Size = new Size(364, 370);
|
||||
propertyGrid_Debug.TabIndex = 2;
|
||||
propertyGrid_Debug.ToolbarVisible = false;
|
||||
//
|
||||
// SpineViewPropertyGrid
|
||||
//
|
||||
AutoScaleDimensions = new SizeF(11F, 24F);
|
||||
AutoScaleMode = AutoScaleMode.Font;
|
||||
Controls.Add(tabControl);
|
||||
Name = "SpineViewPropertyGrid";
|
||||
Size = new Size(372, 448);
|
||||
tabControl.ResumeLayout(false);
|
||||
tabPage_BaseInfo.ResumeLayout(false);
|
||||
tabPage_Render.ResumeLayout(false);
|
||||
tabPage_Transform.ResumeLayout(false);
|
||||
tabPage_Skin.ResumeLayout(false);
|
||||
contextMenuStrip_Skin.ResumeLayout(false);
|
||||
tabPage_Slot.ResumeLayout(false);
|
||||
tabPage_Animation.ResumeLayout(false);
|
||||
contextMenuStrip_Animation.ResumeLayout(false);
|
||||
tabPage_Debug.ResumeLayout(false);
|
||||
ResumeLayout(false);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
private TabControl tabControl;
|
||||
private TabPage tabPage_BaseInfo;
|
||||
private TabPage tabPage_Render;
|
||||
private TabPage tabPage_Transform;
|
||||
private TabPage tabPage_Skin;
|
||||
private TabPage tabPage_Animation;
|
||||
private PropertyGrid propertyGrid_BaseInfo;
|
||||
private PropertyGrid propertyGrid_Render;
|
||||
private PropertyGrid propertyGrid_Transform;
|
||||
private PropertyGrid propertyGrid_Skin;
|
||||
private PropertyGrid propertyGrid_Animation;
|
||||
private ContextMenuStrip contextMenuStrip_Skin;
|
||||
private ContextMenuStrip contextMenuStrip_Animation;
|
||||
private ToolStripMenuItem toolStripMenuItem_ReloadSkins;
|
||||
private ToolStripMenuItem toolStripMenuItem_AddAnimation;
|
||||
private ToolStripMenuItem toolStripMenuItem_RemoveAnimation;
|
||||
private TabPage tabPage_Debug;
|
||||
private PropertyGrid propertyGrid_Debug;
|
||||
private TabPage tabPage_Slot;
|
||||
private PropertyGrid propertyGrid_Slot;
|
||||
}
|
||||
}
|
||||
107
SpineViewer/Controls/SpineViewPropertyGrid.cs
Normal file
107
SpineViewer/Controls/SpineViewPropertyGrid.cs
Normal file
@@ -0,0 +1,107 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using System.Drawing;
|
||||
using System.Data;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows.Forms;
|
||||
using SpineViewer.Utils;
|
||||
using SpineViewer.Spine.SpineView;
|
||||
|
||||
namespace SpineViewer.Controls
|
||||
{
|
||||
public partial class SpineViewPropertyGrid : UserControl
|
||||
{
|
||||
public SpineViewPropertyGrid()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 设置选中的对象列表, 可以赋值 null 来清空选中, 行为与 PropertyGrid.SelectedObjects 类似
|
||||
/// </summary>
|
||||
public SpineObjectProperty[] SelectedSpines
|
||||
{
|
||||
get => selectedSpines ?? [];
|
||||
set
|
||||
{
|
||||
if (value is null || value.Length <= 0)
|
||||
{
|
||||
selectedSpines = null;
|
||||
propertyGrid_BaseInfo.SelectedObject = null;
|
||||
propertyGrid_Render.SelectedObject = null;
|
||||
propertyGrid_Transform.SelectedObject = null;
|
||||
propertyGrid_Skin.SelectedObject = null;
|
||||
propertyGrid_Slot.SelectedObject = null;
|
||||
propertyGrid_Animation.SelectedObject = null;
|
||||
propertyGrid_Debug.SelectedObject = null;
|
||||
}
|
||||
else
|
||||
{
|
||||
selectedSpines = value;
|
||||
propertyGrid_BaseInfo.SelectedObjects = value.Select(e => e.BaseInfo).ToArray();
|
||||
propertyGrid_Render.SelectedObjects = value.Select(e => e.Render).ToArray();
|
||||
propertyGrid_Transform.SelectedObjects = value.Select(e => e.Transform).ToArray();
|
||||
propertyGrid_Skin.SelectedObjects = value.Select(e => e.Skin).ToArray();
|
||||
propertyGrid_Slot.SelectedObjects = value.Select(e => e.Slot).ToArray();
|
||||
propertyGrid_Animation.SelectedObjects = value.Select(e => e.Animation).ToArray();
|
||||
propertyGrid_Debug.SelectedObjects = value.Select(e => e.Debug).ToArray();
|
||||
}
|
||||
}
|
||||
}
|
||||
private SpineObjectProperty[]? selectedSpines = null;
|
||||
|
||||
private void contextMenuStrip_Animation_Opening(object sender, CancelEventArgs e)
|
||||
{
|
||||
if (selectedSpines?.Length == 1)
|
||||
{
|
||||
toolStripMenuItem_AddAnimation.Enabled = true;
|
||||
toolStripMenuItem_RemoveAnimation.Enabled = propertyGrid_Animation.SelectedGridItem.Value is TrackAnimationProperty;
|
||||
}
|
||||
else
|
||||
{
|
||||
toolStripMenuItem_AddAnimation.Enabled = false;
|
||||
toolStripMenuItem_RemoveAnimation.Enabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
private void toolStripMenuItem_ReloadSkins_Click(object sender, EventArgs e)
|
||||
{
|
||||
foreach (var sp in selectedSpines)
|
||||
sp.Spine.ReloadSkins();
|
||||
}
|
||||
|
||||
private void toolStripMenuItem_AddAnimation_Click(object sender, EventArgs e)
|
||||
{
|
||||
if (selectedSpines?.Length != 1) return;
|
||||
|
||||
var spine = selectedSpines[0].Animation.Spine;
|
||||
spine.SetAnimation(spine.GetTrackIndices().Max() + 1, spine.AnimationNames[0]);
|
||||
propertyGrid_Animation.Refresh();
|
||||
}
|
||||
|
||||
private void toolStripMenuItem_RemoveAnimation_Click(object sender, EventArgs e)
|
||||
{
|
||||
if (selectedSpines?.Length != 1) return;
|
||||
|
||||
if (propertyGrid_Animation.SelectedGridItem.Value is TrackAnimationProperty wrapper)
|
||||
{
|
||||
selectedSpines[0].Animation.Spine.ClearTrack(wrapper.Index);
|
||||
propertyGrid_Animation.Refresh();
|
||||
}
|
||||
}
|
||||
|
||||
public override void Refresh()
|
||||
{
|
||||
base.Refresh();
|
||||
propertyGrid_BaseInfo.Refresh();
|
||||
propertyGrid_Render.Refresh();
|
||||
propertyGrid_Transform.Refresh();
|
||||
propertyGrid_Skin.Refresh();
|
||||
propertyGrid_Animation.Refresh();
|
||||
propertyGrid_Debug.Refresh();
|
||||
}
|
||||
}
|
||||
}
|
||||
126
SpineViewer/Controls/SpineViewPropertyGrid.resx
Normal file
126
SpineViewer/Controls/SpineViewPropertyGrid.resx
Normal file
@@ -0,0 +1,126 @@
|
||||
<?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_Skin.TrayLocation" type="System.Drawing.Point, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a">
|
||||
<value>29, 26</value>
|
||||
</metadata>
|
||||
<metadata name="contextMenuStrip_Animation.TrayLocation" type="System.Drawing.Point, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a">
|
||||
<value>318, 25</value>
|
||||
</metadata>
|
||||
</root>
|
||||
@@ -1,4 +1,5 @@
|
||||
using System;
|
||||
using SpineViewer.Utils;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using System.Diagnostics;
|
||||
@@ -15,15 +16,19 @@ namespace SpineViewer.Dialogs
|
||||
public AboutDialog()
|
||||
{
|
||||
InitializeComponent();
|
||||
this.label_Version.Text = $"v{InformationalVersion}";
|
||||
Text = $"关于 {ProgramName}";
|
||||
label_Version.Text = $"v{InformationalVersion}";
|
||||
}
|
||||
|
||||
public string ProgramName => Process.GetCurrentProcess().ProcessName;
|
||||
|
||||
public string InformationalVersion
|
||||
=> Assembly.GetExecutingAssembly().GetCustomAttribute<AssemblyInformationalVersionAttribute>()?.InformationalVersion;
|
||||
|
||||
public string ProgramUrl
|
||||
{
|
||||
get
|
||||
{
|
||||
return Assembly.GetExecutingAssembly().GetCustomAttribute<AssemblyInformationalVersionAttribute>()?.InformationalVersion;
|
||||
}
|
||||
get => linkLabel_RepoUrl.Text;
|
||||
set => linkLabel_RepoUrl.Text = value;
|
||||
}
|
||||
|
||||
private void linkLabel_RepoUrl_LinkClicked(object sender, LinkLabelLinkClickedEventArgs e)
|
||||
@@ -36,7 +41,7 @@ namespace SpineViewer.Dialogs
|
||||
else
|
||||
{
|
||||
Clipboard.SetText(url);
|
||||
MessageBox.Show(this, "链接已复制到剪贴板,请前往浏览器进行访问", "提示信息", MessageBoxButtons.OK, MessageBoxIcon.Information);
|
||||
MessagePopup.Info("链接已复制到剪贴板,请前往浏览器进行访问");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
96
SpineViewer/Dialogs/BatchOpenSpineDialog.Designer.cs
generated
96
SpineViewer/Dialogs/BatchOpenSpineDialog.Designer.cs
generated
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
using SpineViewer.Spine;
|
||||
using SpineViewer.Utils;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
@@ -13,49 +14,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 = SpineUtils.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 = SpineVersion.Auto;
|
||||
}
|
||||
|
||||
private void button_Ok_Click(object sender, EventArgs e)
|
||||
{
|
||||
if (listBox_FilePath.Items.Count <= 0)
|
||||
var version = (SpineVersion)comboBox_Version.SelectedValue;
|
||||
|
||||
var items = skelFileListBox.Items;
|
||||
|
||||
if (items.Count <= 0)
|
||||
{
|
||||
MessageBox.Show("未选择任何文件", "错误信息", MessageBoxButtons.OK, MessageBoxIcon.Information);
|
||||
MessagePopup.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);
|
||||
MessagePopup.Info($"{p}", "skel文件不存在");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
SkelPaths = listBox_FilePath.Items.Cast<string>().ToArray();
|
||||
Version = (Spine.Version)comboBox_Version.SelectedValue;
|
||||
if (version != SpineVersion.Auto && !Spine.SpineObject.HasImplementation(version))
|
||||
{
|
||||
MessagePopup.Info($"{version.GetName()} 版本尚未实现(咕咕咕~)");
|
||||
return;
|
||||
}
|
||||
|
||||
Result = new(version, items.Cast<string>().ToArray());
|
||||
DialogResult = DialogResult.OK;
|
||||
}
|
||||
|
||||
@@ -64,4 +64,20 @@ namespace SpineViewer.Dialogs
|
||||
DialogResult = DialogResult.Cancel;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 批量打开对话框结果
|
||||
/// </summary>
|
||||
public class BatchOpenSpineDialogResult(SpineVersion version, string[] skelPaths)
|
||||
{
|
||||
/// <summary>
|
||||
/// 版本
|
||||
/// </summary>
|
||||
public SpineVersion Version => version;
|
||||
|
||||
/// <summary>
|
||||
/// 路径列表
|
||||
/// </summary>
|
||||
public string[] SkelPaths => skelPaths;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
351
SpineViewer/Dialogs/ConvertFileFormatDialog.Designer.cs
generated
Normal file
351
SpineViewer/Dialogs/ConvertFileFormatDialog.Designer.cs
generated
Normal file
@@ -0,0 +1,351 @@
|
||||
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();
|
||||
label5 = new Label();
|
||||
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();
|
||||
tableLayoutPanel3 = new TableLayoutPanel();
|
||||
textBox_OutputDir = new TextBox();
|
||||
button_SelectOutputDir = new Button();
|
||||
folderBrowserDialog_Output = new FolderBrowserDialog();
|
||||
panel.SuspendLayout();
|
||||
tableLayoutPanel1.SuspendLayout();
|
||||
flowLayoutPanel_TargetFormat.SuspendLayout();
|
||||
tableLayoutPanel2.SuspendLayout();
|
||||
tableLayoutPanel3.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, 702);
|
||||
panel.TabIndex = 2;
|
||||
//
|
||||
// tableLayoutPanel1
|
||||
//
|
||||
tableLayoutPanel1.ColumnCount = 2;
|
||||
tableLayoutPanel1.ColumnStyles.Add(new ColumnStyle());
|
||||
tableLayoutPanel1.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 100F));
|
||||
tableLayoutPanel1.Controls.Add(label5, 0, 2);
|
||||
tableLayoutPanel1.Controls.Add(comboBox_TargetVersion, 1, 4);
|
||||
tableLayoutPanel1.Controls.Add(flowLayoutPanel_TargetFormat, 1, 5);
|
||||
tableLayoutPanel1.Controls.Add(label1, 0, 4);
|
||||
tableLayoutPanel1.Controls.Add(label4, 0, 0);
|
||||
tableLayoutPanel1.Controls.Add(label3, 0, 3);
|
||||
tableLayoutPanel1.Controls.Add(comboBox_SourceVersion, 1, 3);
|
||||
tableLayoutPanel1.Controls.Add(tableLayoutPanel2, 0, 6);
|
||||
tableLayoutPanel1.Controls.Add(label2, 0, 5);
|
||||
tableLayoutPanel1.Controls.Add(skelFileListBox, 0, 1);
|
||||
tableLayoutPanel1.Controls.Add(tableLayoutPanel3, 1, 2);
|
||||
tableLayoutPanel1.Dock = DockStyle.Fill;
|
||||
tableLayoutPanel1.Location = new Point(50, 15);
|
||||
tableLayoutPanel1.Name = "tableLayoutPanel1";
|
||||
tableLayoutPanel1.RowCount = 7;
|
||||
tableLayoutPanel1.RowStyles.Add(new RowStyle());
|
||||
tableLayoutPanel1.RowStyles.Add(new RowStyle(SizeType.Percent, 100F));
|
||||
tableLayoutPanel1.RowStyles.Add(new RowStyle());
|
||||
tableLayoutPanel1.RowStyles.Add(new RowStyle());
|
||||
tableLayoutPanel1.RowStyles.Add(new RowStyle());
|
||||
tableLayoutPanel1.RowStyles.Add(new RowStyle());
|
||||
tableLayoutPanel1.RowStyles.Add(new RowStyle());
|
||||
tableLayoutPanel1.RowStyles.Add(new RowStyle(SizeType.Absolute, 20F));
|
||||
tableLayoutPanel1.Size = new Size(951, 677);
|
||||
tableLayoutPanel1.TabIndex = 1;
|
||||
//
|
||||
// label5
|
||||
//
|
||||
label5.Anchor = AnchorStyles.Left | AnchorStyles.Right;
|
||||
label5.AutoSize = true;
|
||||
label5.Location = new Point(3, 462);
|
||||
label5.Name = "label5";
|
||||
label5.Size = new Size(104, 24);
|
||||
label5.TabIndex = 23;
|
||||
label5.Text = "输出文件夹:";
|
||||
//
|
||||
// comboBox_TargetVersion
|
||||
//
|
||||
comboBox_TargetVersion.Anchor = AnchorStyles.Left;
|
||||
comboBox_TargetVersion.DropDownStyle = ComboBoxStyle.DropDownList;
|
||||
comboBox_TargetVersion.FormattingEnabled = true;
|
||||
comboBox_TargetVersion.Location = new Point(113, 535);
|
||||
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(110, 570);
|
||||
flowLayoutPanel_TargetFormat.Margin = new Padding(0);
|
||||
flowLayoutPanel_TargetFormat.Name = "flowLayoutPanel_TargetFormat";
|
||||
flowLayoutPanel_TargetFormat.Size = new Size(841, 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(21, 539);
|
||||
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(39, 501);
|
||||
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(113, 497);
|
||||
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, 634);
|
||||
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(21, 575);
|
||||
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, 394);
|
||||
skelFileListBox.TabIndex = 20;
|
||||
//
|
||||
// tableLayoutPanel3
|
||||
//
|
||||
tableLayoutPanel3.AutoSize = true;
|
||||
tableLayoutPanel3.AutoSizeMode = AutoSizeMode.GrowAndShrink;
|
||||
tableLayoutPanel3.ColumnCount = 3;
|
||||
tableLayoutPanel3.ColumnStyles.Add(new ColumnStyle());
|
||||
tableLayoutPanel3.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 100F));
|
||||
tableLayoutPanel3.ColumnStyles.Add(new ColumnStyle());
|
||||
tableLayoutPanel3.Controls.Add(textBox_OutputDir, 1, 0);
|
||||
tableLayoutPanel3.Controls.Add(button_SelectOutputDir, 2, 0);
|
||||
tableLayoutPanel3.Dock = DockStyle.Fill;
|
||||
tableLayoutPanel3.Location = new Point(110, 454);
|
||||
tableLayoutPanel3.Margin = new Padding(0);
|
||||
tableLayoutPanel3.Name = "tableLayoutPanel3";
|
||||
tableLayoutPanel3.RowCount = 1;
|
||||
tableLayoutPanel3.RowStyles.Add(new RowStyle(SizeType.Percent, 100F));
|
||||
tableLayoutPanel3.Size = new Size(841, 40);
|
||||
tableLayoutPanel3.TabIndex = 22;
|
||||
//
|
||||
// textBox_OutputDir
|
||||
//
|
||||
textBox_OutputDir.Anchor = AnchorStyles.Left | AnchorStyles.Right;
|
||||
textBox_OutputDir.Location = new Point(3, 5);
|
||||
textBox_OutputDir.Name = "textBox_OutputDir";
|
||||
textBox_OutputDir.Size = new Size(797, 30);
|
||||
textBox_OutputDir.TabIndex = 1;
|
||||
//
|
||||
// button_SelectOutputDir
|
||||
//
|
||||
button_SelectOutputDir.Anchor = AnchorStyles.Left | AnchorStyles.Right;
|
||||
button_SelectOutputDir.AutoSize = true;
|
||||
button_SelectOutputDir.AutoSizeMode = AutoSizeMode.GrowAndShrink;
|
||||
button_SelectOutputDir.Location = new Point(806, 3);
|
||||
button_SelectOutputDir.Name = "button_SelectOutputDir";
|
||||
button_SelectOutputDir.Size = new Size(32, 34);
|
||||
button_SelectOutputDir.TabIndex = 2;
|
||||
button_SelectOutputDir.Text = "...";
|
||||
button_SelectOutputDir.UseVisualStyleBackColor = true;
|
||||
button_SelectOutputDir.Click += button_SelectOutputDir_Click;
|
||||
//
|
||||
// ConvertFileFormatDialog
|
||||
//
|
||||
AcceptButton = button_Ok;
|
||||
AutoScaleDimensions = new SizeF(11F, 24F);
|
||||
AutoScaleMode = AutoScaleMode.Font;
|
||||
CancelButton = button_Cancel;
|
||||
ClientSize = new Size(1051, 702);
|
||||
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);
|
||||
tableLayoutPanel3.ResumeLayout(false);
|
||||
tableLayoutPanel3.PerformLayout();
|
||||
ResumeLayout(false);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
private Panel panel;
|
||||
private TableLayoutPanel tableLayoutPanel1;
|
||||
private Label label4;
|
||||
private Label label3;
|
||||
private ComboBox comboBox_SourceVersion;
|
||||
private TableLayoutPanel tableLayoutPanel2;
|
||||
private Button button_Ok;
|
||||
private Button button_Cancel;
|
||||
private 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;
|
||||
private FolderBrowserDialog folderBrowserDialog_Output;
|
||||
private TableLayoutPanel tableLayoutPanel3;
|
||||
private TextBox textBox_OutputDir;
|
||||
private Button button_SelectOutputDir;
|
||||
private Label label5;
|
||||
}
|
||||
}
|
||||
157
SpineViewer/Dialogs/ConvertFileFormatDialog.cs
Normal file
157
SpineViewer/Dialogs/ConvertFileFormatDialog.cs
Normal file
@@ -0,0 +1,157 @@
|
||||
using NLog;
|
||||
using SpineViewer.Spine;
|
||||
using SpineViewer.Utils;
|
||||
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
|
||||
{
|
||||
private readonly Logger logger = LogManager.GetCurrentClassLogger();
|
||||
|
||||
/// <summary>
|
||||
/// 对话框结果, 取消时为 null
|
||||
/// </summary>
|
||||
public ConvertFileFormatDialogResult Result { get; private set; }
|
||||
|
||||
public ConvertFileFormatDialog()
|
||||
{
|
||||
InitializeComponent();
|
||||
|
||||
comboBox_SourceVersion.DataSource = SpineUtils.Names.ToList();
|
||||
comboBox_SourceVersion.DisplayMember = "Value";
|
||||
comboBox_SourceVersion.ValueMember = "Key";
|
||||
comboBox_SourceVersion.SelectedValue = SpineVersion.Auto;
|
||||
|
||||
// 目标版本不包含自动
|
||||
var versionsWithoutAuto = SpineUtils.Names.ToDictionary();
|
||||
versionsWithoutAuto.Remove(SpineVersion.Auto);
|
||||
comboBox_TargetVersion.DataSource = versionsWithoutAuto.ToList();
|
||||
comboBox_TargetVersion.DisplayMember = "Value";
|
||||
comboBox_TargetVersion.ValueMember = "Key";
|
||||
comboBox_TargetVersion.SelectedValue = SpineVersion.V38;
|
||||
}
|
||||
|
||||
private void button_SelectOutputDir_Click(object sender, EventArgs e)
|
||||
{
|
||||
if (folderBrowserDialog_Output.ShowDialog() != DialogResult.OK)
|
||||
return;
|
||||
|
||||
textBox_OutputDir.Text = Path.GetFullPath(folderBrowserDialog_Output.SelectedPath);
|
||||
}
|
||||
|
||||
private void button_Ok_Click(object sender, EventArgs e)
|
||||
{
|
||||
var outputDir = textBox_OutputDir.Text;
|
||||
var sourceVersion = (SpineVersion)comboBox_SourceVersion.SelectedValue;
|
||||
var targetVersion = (SpineVersion)comboBox_TargetVersion.SelectedValue;
|
||||
var jsonTarget = radioButton_JsonTarget.Checked;
|
||||
|
||||
var items = skelFileListBox.Items;
|
||||
|
||||
if (items.Count <= 0)
|
||||
{
|
||||
MessagePopup.Info("未选择任何文件");
|
||||
return;
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(outputDir))
|
||||
{
|
||||
outputDir = null;
|
||||
}
|
||||
else
|
||||
{
|
||||
outputDir = Path.GetFullPath(outputDir);
|
||||
if (!Directory.Exists(outputDir))
|
||||
{
|
||||
if (MessagePopup.Quest("输出文件夹不存在,是否创建?") == DialogResult.OK)
|
||||
{
|
||||
try
|
||||
{
|
||||
Directory.CreateDirectory(outputDir);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.Error(ex.ToString());
|
||||
logger.Error("Failed to create output dir {}", outputDir);
|
||||
MessagePopup.Error(ex.ToString());
|
||||
return;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
foreach (string p in items)
|
||||
{
|
||||
if (!File.Exists(p))
|
||||
{
|
||||
MessagePopup.Info($"{p}", "skel文件不存在");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (sourceVersion != SpineVersion.Auto && !SkeletonConverter.HasImplementation(sourceVersion))
|
||||
{
|
||||
MessagePopup.Info($"{sourceVersion.GetName()} 版本尚未实现(咕咕咕~)");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!SkeletonConverter.HasImplementation(targetVersion))
|
||||
{
|
||||
MessagePopup.Info($"{targetVersion.GetName()} 版本尚未实现(咕咕咕~)");
|
||||
return;
|
||||
}
|
||||
|
||||
Result = new(outputDir, 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? outputDir, string[] skelPaths, SpineVersion sourceVersion, SpineVersion targetVersion, bool jsonTarget)
|
||||
{
|
||||
/// <summary>
|
||||
/// 输出文件夹, 如果为空, 则将转换后的文件转换到各自的文件夹下
|
||||
/// </summary>
|
||||
public string? OutputDir => outputDir;
|
||||
|
||||
/// <summary>
|
||||
/// 骨骼文件路径列表
|
||||
/// </summary>
|
||||
public string[] SkelPaths => skelPaths;
|
||||
|
||||
/// <summary>
|
||||
/// 源版本
|
||||
/// </summary>
|
||||
public SpineVersion SourceVersion => sourceVersion;
|
||||
|
||||
/// <summary>
|
||||
/// 目标版本
|
||||
/// </summary>
|
||||
public SpineVersion TargetVersion => targetVersion;
|
||||
|
||||
/// <summary>
|
||||
/// 目标格式是否为 Json
|
||||
/// </summary>
|
||||
public bool JsonTarget => jsonTarget;
|
||||
}
|
||||
}
|
||||
@@ -117,8 +117,8 @@
|
||||
<resheader name="writer">
|
||||
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
||||
</resheader>
|
||||
<metadata name="folderBrowserDialog.TrayLocation" type="System.Drawing.Point, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a">
|
||||
<value>17, 17</value>
|
||||
<metadata name="folderBrowserDialog_Output.TrayLocation" type="System.Drawing.Point, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a">
|
||||
<value>36, 22</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">
|
||||
@@ -1,4 +1,5 @@
|
||||
using Microsoft.Win32;
|
||||
using SpineViewer.Utils;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
@@ -26,7 +27,29 @@ namespace SpineViewer.Dialogs
|
||||
|
||||
private class DiagnosticsInformation
|
||||
{
|
||||
[Category("Versions")]
|
||||
[Category("Hardware")]
|
||||
public string CPU
|
||||
{
|
||||
get => Registry.GetValue(@"HKEY_LOCAL_MACHINE\HARDWARE\DESCRIPTION\System\CentralProcessor\0", "ProcessorNameString", "Unknown").ToString();
|
||||
}
|
||||
|
||||
[Category("Hardware")]
|
||||
public string Memory
|
||||
{
|
||||
get => $"{new Microsoft.VisualBasic.Devices.ComputerInfo().TotalPhysicalMemory / 1024f / 1024f / 1024f:F1} GB";
|
||||
}
|
||||
|
||||
[Category("Hardware")]
|
||||
public string GPU
|
||||
{
|
||||
get
|
||||
{
|
||||
var searcher = new ManagementObjectSearcher("SELECT Name FROM Win32_VideoController");
|
||||
return string.Join("; ", searcher.Get().Cast<ManagementObject>().Select(mo => mo["Name"].ToString()));
|
||||
}
|
||||
}
|
||||
|
||||
[Category("Software")]
|
||||
public string WindowsVersion
|
||||
{
|
||||
get
|
||||
@@ -39,54 +62,38 @@ namespace SpineViewer.Dialogs
|
||||
}
|
||||
}
|
||||
|
||||
[Category("Versions")]
|
||||
[Category("Software")]
|
||||
public string Version
|
||||
{
|
||||
get => Assembly.GetExecutingAssembly().GetCustomAttribute<AssemblyInformationalVersionAttribute>()?.InformationalVersion;
|
||||
}
|
||||
|
||||
[Category("Versions")]
|
||||
[Category("Software")]
|
||||
public string DotNetVersion
|
||||
{
|
||||
get => Environment.Version.ToString();
|
||||
}
|
||||
|
||||
[Category("Versions")]
|
||||
[Category("Software")]
|
||||
public string SFMLVersion
|
||||
{
|
||||
get => typeof(SFML.ObjectBase).Assembly.GetName().Version.ToString();
|
||||
}
|
||||
|
||||
[Category("Hardwares")]
|
||||
public string CPU
|
||||
[Category("Software")]
|
||||
public string FFMpegCoreVersion
|
||||
{
|
||||
get => Registry.GetValue(@"HKEY_LOCAL_MACHINE\HARDWARE\DESCRIPTION\System\CentralProcessor\0", "ProcessorNameString", "Unknown").ToString();
|
||||
}
|
||||
|
||||
[Category("Hardwares")]
|
||||
public string Memory
|
||||
{
|
||||
get => $"{new Microsoft.VisualBasic.Devices.ComputerInfo().TotalPhysicalMemory / 1024f / 1024f / 1024f:F1} GB";
|
||||
}
|
||||
|
||||
[Category("Hardwares")]
|
||||
public string GPU
|
||||
{
|
||||
get
|
||||
{
|
||||
var searcher = new ManagementObjectSearcher("SELECT Name FROM Win32_VideoController");
|
||||
return string.Join("; ", searcher.Get().Cast<ManagementObject>().Select(mo => mo["Name"].ToString()));
|
||||
}
|
||||
get => typeof(FFMpegCore.FFMpeg).Assembly.GetName().Version.ToString();
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
MessagePopup.Info("已复制");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
155
SpineViewer/Dialogs/ExportDialog.Designer.cs
generated
Normal file
155
SpineViewer/Dialogs/ExportDialog.Designer.cs
generated
Normal file
@@ -0,0 +1,155 @@
|
||||
namespace SpineViewer.Dialogs
|
||||
{
|
||||
partial class ExportDialog
|
||||
{
|
||||
/// <summary>
|
||||
/// Required designer variable.
|
||||
/// </summary>
|
||||
private System.ComponentModel.IContainer components = null;
|
||||
|
||||
/// <summary>
|
||||
/// Clean up any resources being used.
|
||||
/// </summary>
|
||||
/// <param name="disposing">true if managed resources should be disposed; otherwise, false.</param>
|
||||
protected override void Dispose(bool disposing)
|
||||
{
|
||||
if (disposing && (components != null))
|
||||
{
|
||||
components.Dispose();
|
||||
}
|
||||
base.Dispose(disposing);
|
||||
}
|
||||
|
||||
#region Windows Form Designer generated code
|
||||
|
||||
/// <summary>
|
||||
/// Required method for Designer support - do not modify
|
||||
/// the contents of this method with the code editor.
|
||||
/// </summary>
|
||||
private void InitializeComponent()
|
||||
{
|
||||
System.ComponentModel.ComponentResourceManager resources = new System.ComponentModel.ComponentResourceManager(typeof(ExportDialog));
|
||||
panel1 = new Panel();
|
||||
tableLayoutPanel1 = new TableLayoutPanel();
|
||||
propertyGrid_ExportArgs = new PropertyGrid();
|
||||
tableLayoutPanel2 = new TableLayoutPanel();
|
||||
button_Ok = new Button();
|
||||
button_Cancel = new Button();
|
||||
panel1.SuspendLayout();
|
||||
tableLayoutPanel1.SuspendLayout();
|
||||
tableLayoutPanel2.SuspendLayout();
|
||||
SuspendLayout();
|
||||
//
|
||||
// panel1
|
||||
//
|
||||
panel1.Controls.Add(tableLayoutPanel1);
|
||||
panel1.Dock = DockStyle.Fill;
|
||||
panel1.Location = new Point(0, 0);
|
||||
panel1.Name = "panel1";
|
||||
panel1.Padding = new Padding(50, 15, 50, 10);
|
||||
panel1.Size = new Size(793, 841);
|
||||
panel1.TabIndex = 2;
|
||||
//
|
||||
// tableLayoutPanel1
|
||||
//
|
||||
tableLayoutPanel1.AutoSize = true;
|
||||
tableLayoutPanel1.ColumnCount = 1;
|
||||
tableLayoutPanel1.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 100F));
|
||||
tableLayoutPanel1.Controls.Add(propertyGrid_ExportArgs, 0, 0);
|
||||
tableLayoutPanel1.Controls.Add(tableLayoutPanel2, 0, 1);
|
||||
tableLayoutPanel1.Dock = DockStyle.Fill;
|
||||
tableLayoutPanel1.Location = new Point(50, 15);
|
||||
tableLayoutPanel1.Name = "tableLayoutPanel1";
|
||||
tableLayoutPanel1.RowCount = 2;
|
||||
tableLayoutPanel1.RowStyles.Add(new RowStyle(SizeType.Percent, 100F));
|
||||
tableLayoutPanel1.RowStyles.Add(new RowStyle());
|
||||
tableLayoutPanel1.RowStyles.Add(new RowStyle(SizeType.Absolute, 20F));
|
||||
tableLayoutPanel1.RowStyles.Add(new RowStyle(SizeType.Absolute, 20F));
|
||||
tableLayoutPanel1.Size = new Size(693, 816);
|
||||
tableLayoutPanel1.TabIndex = 0;
|
||||
//
|
||||
// propertyGrid_ExportArgs
|
||||
//
|
||||
propertyGrid_ExportArgs.Dock = DockStyle.Fill;
|
||||
propertyGrid_ExportArgs.Location = new Point(3, 3);
|
||||
propertyGrid_ExportArgs.Name = "propertyGrid_ExportArgs";
|
||||
propertyGrid_ExportArgs.PropertySort = PropertySort.Categorized;
|
||||
propertyGrid_ExportArgs.Size = new Size(687, 737);
|
||||
propertyGrid_ExportArgs.TabIndex = 1;
|
||||
propertyGrid_ExportArgs.ToolbarVisible = false;
|
||||
//
|
||||
// tableLayoutPanel2
|
||||
//
|
||||
tableLayoutPanel2.AutoSize = true;
|
||||
tableLayoutPanel2.AutoSizeMode = AutoSizeMode.GrowAndShrink;
|
||||
tableLayoutPanel2.ColumnCount = 2;
|
||||
tableLayoutPanel2.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 50F));
|
||||
tableLayoutPanel2.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 50F));
|
||||
tableLayoutPanel2.Controls.Add(button_Ok, 0, 0);
|
||||
tableLayoutPanel2.Controls.Add(button_Cancel, 1, 0);
|
||||
tableLayoutPanel2.Dock = DockStyle.Bottom;
|
||||
tableLayoutPanel2.Location = new Point(3, 773);
|
||||
tableLayoutPanel2.Margin = new Padding(3, 30, 3, 3);
|
||||
tableLayoutPanel2.Name = "tableLayoutPanel2";
|
||||
tableLayoutPanel2.RowCount = 1;
|
||||
tableLayoutPanel2.RowStyles.Add(new RowStyle());
|
||||
tableLayoutPanel2.Size = new Size(687, 40);
|
||||
tableLayoutPanel2.TabIndex = 10;
|
||||
//
|
||||
// button_Ok
|
||||
//
|
||||
button_Ok.Anchor = AnchorStyles.Bottom | AnchorStyles.Right;
|
||||
button_Ok.Location = new Point(201, 3);
|
||||
button_Ok.Margin = new Padding(3, 3, 30, 3);
|
||||
button_Ok.Name = "button_Ok";
|
||||
button_Ok.Size = new Size(112, 34);
|
||||
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(373, 3);
|
||||
button_Cancel.Margin = new Padding(30, 3, 3, 3);
|
||||
button_Cancel.Name = "button_Cancel";
|
||||
button_Cancel.Size = new Size(112, 34);
|
||||
button_Cancel.TabIndex = 8;
|
||||
button_Cancel.Text = "取消";
|
||||
button_Cancel.UseVisualStyleBackColor = true;
|
||||
button_Cancel.Click += button_Cancel_Click;
|
||||
//
|
||||
// ExportDialog
|
||||
//
|
||||
AcceptButton = button_Ok;
|
||||
AutoScaleDimensions = new SizeF(11F, 24F);
|
||||
AutoScaleMode = AutoScaleMode.Font;
|
||||
CancelButton = button_Cancel;
|
||||
ClientSize = new Size(793, 841);
|
||||
Controls.Add(panel1);
|
||||
Icon = (Icon)resources.GetObject("$this.Icon");
|
||||
MaximizeBox = false;
|
||||
MinimizeBox = false;
|
||||
Name = "ExportDialog";
|
||||
ShowInTaskbar = false;
|
||||
StartPosition = FormStartPosition.CenterScreen;
|
||||
Text = "导出参数";
|
||||
panel1.ResumeLayout(false);
|
||||
panel1.PerformLayout();
|
||||
tableLayoutPanel1.ResumeLayout(false);
|
||||
tableLayoutPanel1.PerformLayout();
|
||||
tableLayoutPanel2.ResumeLayout(false);
|
||||
ResumeLayout(false);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
private Panel panel1;
|
||||
private TableLayoutPanel tableLayoutPanel1;
|
||||
private TableLayoutPanel tableLayoutPanel2;
|
||||
private Button button_Ok;
|
||||
private Button button_Cancel;
|
||||
private PropertyGrid propertyGrid_ExportArgs;
|
||||
}
|
||||
}
|
||||
79
SpineViewer/Dialogs/ExportDialog.cs
Normal file
79
SpineViewer/Dialogs/ExportDialog.cs
Normal file
@@ -0,0 +1,79 @@
|
||||
using SpineViewer.Utils;
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using System.Data;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using SpineViewer.Spine.SpineExporter;
|
||||
|
||||
namespace SpineViewer.Dialogs
|
||||
{
|
||||
public partial class ExportDialog : Form
|
||||
{
|
||||
private readonly ExporterProperty wrapper;
|
||||
|
||||
public ExportDialog(ExporterProperty wrapper)
|
||||
{
|
||||
InitializeComponent();
|
||||
this.wrapper = wrapper;
|
||||
propertyGrid_ExportArgs.SelectedObject = wrapper;
|
||||
|
||||
#region XXX: 通过反射默认高亮指定的项
|
||||
var categories = propertyGrid_ExportArgs.SelectedGridItem?.Parent?.Parent?.GridItems;
|
||||
if (categories is null) return;
|
||||
|
||||
foreach (var category in categories)
|
||||
{
|
||||
// 查找 "导出" 分组
|
||||
if (category == null) continue;
|
||||
PropertyInfo? labelProp = category.GetType().GetProperty("Label", BindingFlags.Instance | BindingFlags.Public);
|
||||
if (labelProp == null) continue;
|
||||
string? label = labelProp.GetValue(category) as string;
|
||||
if (label != "[0] 导出") continue;
|
||||
|
||||
// 获取该分组下的所有属性项
|
||||
PropertyInfo? gridItemsProp = category.GetType().GetProperty("GridItems", BindingFlags.Instance | BindingFlags.Public);
|
||||
if (gridItemsProp == null) continue;
|
||||
var gridItemsObj = gridItemsProp.GetValue(category);
|
||||
if (gridItemsObj is not IEnumerable gridItems) continue;
|
||||
|
||||
foreach (object item in gridItems)
|
||||
{
|
||||
if (item == null) continue;
|
||||
PropertyInfo? propDescProp = item.GetType().GetProperty("PropertyDescriptor", BindingFlags.Instance | BindingFlags.Public);
|
||||
if (propDescProp == null) continue;
|
||||
var propDesc = propDescProp.GetValue(item) as PropertyDescriptor;
|
||||
if (propDesc == null) continue;
|
||||
if (propDesc.Name == "OutputDir")
|
||||
{
|
||||
|
||||
if (item is GridItem gridItem)
|
||||
propertyGrid_ExportArgs.SelectedGridItem = gridItem; // 找到后,将此项设为选中项
|
||||
else
|
||||
propertyGrid_ExportArgs.SelectedGridItem = (GridItem)item; // 如果转换失败,则尝试直接赋值
|
||||
}
|
||||
return; // 设置成功后退出
|
||||
}
|
||||
}
|
||||
#endregion
|
||||
}
|
||||
|
||||
private void button_Ok_Click(object sender, EventArgs e)
|
||||
{
|
||||
if (wrapper.Exporter.Validate() is string error)
|
||||
{
|
||||
MessagePopup.Info(error, "参数错误");
|
||||
return;
|
||||
}
|
||||
DialogResult = DialogResult.OK;
|
||||
}
|
||||
|
||||
private void button_Cancel_Click(object sender, EventArgs e)
|
||||
{
|
||||
DialogResult = DialogResult.Cancel;
|
||||
}
|
||||
}
|
||||
}
|
||||
3267
SpineViewer/Dialogs/ExportDialog.resx
Normal file
3267
SpineViewer/Dialogs/ExportDialog.resx
Normal file
File diff suppressed because it is too large
Load Diff
269
SpineViewer/Dialogs/ExportPngDialog.Designer.cs
generated
269
SpineViewer/Dialogs/ExportPngDialog.Designer.cs
generated
@@ -1,269 +0,0 @@
|
||||
namespace SpineViewer.Dialogs
|
||||
{
|
||||
partial class ExportPngDialog
|
||||
{
|
||||
/// <summary>
|
||||
/// Required designer variable.
|
||||
/// </summary>
|
||||
private System.ComponentModel.IContainer components = null;
|
||||
|
||||
/// <summary>
|
||||
/// Clean up any resources being used.
|
||||
/// </summary>
|
||||
/// <param name="disposing">true if managed resources should be disposed; otherwise, false.</param>
|
||||
protected override void Dispose(bool disposing)
|
||||
{
|
||||
if (disposing && (components != null))
|
||||
{
|
||||
components.Dispose();
|
||||
}
|
||||
base.Dispose(disposing);
|
||||
}
|
||||
|
||||
#region Windows Form Designer generated code
|
||||
|
||||
/// <summary>
|
||||
/// Required method for Designer support - do not modify
|
||||
/// the contents of this method with the code editor.
|
||||
/// </summary>
|
||||
private void InitializeComponent()
|
||||
{
|
||||
System.ComponentModel.ComponentResourceManager resources = new System.ComponentModel.ComponentResourceManager(typeof(ExportPngDialog));
|
||||
panel1 = new Panel();
|
||||
tableLayoutPanel1 = new TableLayoutPanel();
|
||||
label4 = new Label();
|
||||
label1 = new Label();
|
||||
label2 = new Label();
|
||||
label3 = new Label();
|
||||
textBox_OutputDir = new TextBox();
|
||||
button_SelectOutputDir = new Button();
|
||||
tableLayoutPanel2 = new TableLayoutPanel();
|
||||
button_Ok = new Button();
|
||||
button_Cancel = new Button();
|
||||
numericUpDown_Duration = new NumericUpDown();
|
||||
numericUpDown_Fps = new NumericUpDown();
|
||||
folderBrowserDialog = new FolderBrowserDialog();
|
||||
panel1.SuspendLayout();
|
||||
tableLayoutPanel1.SuspendLayout();
|
||||
tableLayoutPanel2.SuspendLayout();
|
||||
((System.ComponentModel.ISupportInitialize)numericUpDown_Duration).BeginInit();
|
||||
((System.ComponentModel.ISupportInitialize)numericUpDown_Fps).BeginInit();
|
||||
SuspendLayout();
|
||||
//
|
||||
// panel1
|
||||
//
|
||||
panel1.Controls.Add(tableLayoutPanel1);
|
||||
panel1.Dock = DockStyle.Fill;
|
||||
panel1.Location = new Point(0, 0);
|
||||
panel1.Name = "panel1";
|
||||
panel1.Padding = new Padding(50, 15, 50, 10);
|
||||
panel1.Size = new Size(919, 276);
|
||||
panel1.TabIndex = 1;
|
||||
//
|
||||
// tableLayoutPanel1
|
||||
//
|
||||
tableLayoutPanel1.AutoSize = true;
|
||||
tableLayoutPanel1.ColumnCount = 4;
|
||||
tableLayoutPanel1.ColumnStyles.Add(new ColumnStyle());
|
||||
tableLayoutPanel1.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 50F));
|
||||
tableLayoutPanel1.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 50F));
|
||||
tableLayoutPanel1.ColumnStyles.Add(new ColumnStyle());
|
||||
tableLayoutPanel1.Controls.Add(label4, 0, 0);
|
||||
tableLayoutPanel1.Controls.Add(label1, 0, 1);
|
||||
tableLayoutPanel1.Controls.Add(label2, 0, 2);
|
||||
tableLayoutPanel1.Controls.Add(label3, 0, 3);
|
||||
tableLayoutPanel1.Controls.Add(textBox_OutputDir, 1, 1);
|
||||
tableLayoutPanel1.Controls.Add(button_SelectOutputDir, 3, 1);
|
||||
tableLayoutPanel1.Controls.Add(tableLayoutPanel2, 0, 4);
|
||||
tableLayoutPanel1.Controls.Add(numericUpDown_Duration, 1, 2);
|
||||
tableLayoutPanel1.Controls.Add(numericUpDown_Fps, 1, 3);
|
||||
tableLayoutPanel1.Dock = DockStyle.Fill;
|
||||
tableLayoutPanel1.Location = new Point(50, 15);
|
||||
tableLayoutPanel1.Name = "tableLayoutPanel1";
|
||||
tableLayoutPanel1.RowCount = 5;
|
||||
tableLayoutPanel1.RowStyles.Add(new RowStyle());
|
||||
tableLayoutPanel1.RowStyles.Add(new RowStyle());
|
||||
tableLayoutPanel1.RowStyles.Add(new RowStyle());
|
||||
tableLayoutPanel1.RowStyles.Add(new RowStyle());
|
||||
tableLayoutPanel1.RowStyles.Add(new RowStyle());
|
||||
tableLayoutPanel1.Size = new Size(819, 251);
|
||||
tableLayoutPanel1.TabIndex = 0;
|
||||
//
|
||||
// label4
|
||||
//
|
||||
label4.AutoSize = true;
|
||||
tableLayoutPanel1.SetColumnSpan(label4, 4);
|
||||
label4.Dock = DockStyle.Fill;
|
||||
label4.Location = new Point(15, 15);
|
||||
label4.Margin = new Padding(15);
|
||||
label4.Name = "label4";
|
||||
label4.Size = new Size(789, 24);
|
||||
label4.TabIndex = 11;
|
||||
label4.Text = "说明:时长不足一帧时仅导出第一帧";
|
||||
label4.TextAlign = ContentAlignment.MiddleCenter;
|
||||
//
|
||||
// label1
|
||||
//
|
||||
label1.Anchor = AnchorStyles.Right;
|
||||
label1.AutoSize = true;
|
||||
label1.Location = new Point(3, 62);
|
||||
label1.Name = "label1";
|
||||
label1.Size = new Size(104, 24);
|
||||
label1.TabIndex = 0;
|
||||
label1.Text = "输出文件夹:";
|
||||
//
|
||||
// label2
|
||||
//
|
||||
label2.Anchor = AnchorStyles.Right;
|
||||
label2.AutoSize = true;
|
||||
label2.Location = new Point(57, 100);
|
||||
label2.Name = "label2";
|
||||
label2.Size = new Size(50, 24);
|
||||
label2.TabIndex = 1;
|
||||
label2.Text = "时长:";
|
||||
//
|
||||
// label3
|
||||
//
|
||||
label3.Anchor = AnchorStyles.Right;
|
||||
label3.AutoSize = true;
|
||||
label3.Location = new Point(57, 136);
|
||||
label3.Name = "label3";
|
||||
label3.Size = new Size(50, 24);
|
||||
label3.TabIndex = 2;
|
||||
label3.Text = "帧率:";
|
||||
//
|
||||
// textBox_OutputDir
|
||||
//
|
||||
tableLayoutPanel1.SetColumnSpan(textBox_OutputDir, 2);
|
||||
textBox_OutputDir.Dock = DockStyle.Fill;
|
||||
textBox_OutputDir.Location = new Point(113, 57);
|
||||
textBox_OutputDir.Name = "textBox_OutputDir";
|
||||
textBox_OutputDir.Size = new Size(664, 30);
|
||||
textBox_OutputDir.TabIndex = 3;
|
||||
//
|
||||
// button_SelectOutputDir
|
||||
//
|
||||
button_SelectOutputDir.AutoSize = true;
|
||||
button_SelectOutputDir.AutoSizeMode = AutoSizeMode.GrowAndShrink;
|
||||
button_SelectOutputDir.Location = new Point(783, 57);
|
||||
button_SelectOutputDir.Name = "button_SelectOutputDir";
|
||||
button_SelectOutputDir.Size = new Size(32, 34);
|
||||
button_SelectOutputDir.TabIndex = 5;
|
||||
button_SelectOutputDir.Text = "...";
|
||||
button_SelectOutputDir.UseVisualStyleBackColor = true;
|
||||
button_SelectOutputDir.Click += button_SelectOutputDir_Click;
|
||||
//
|
||||
// tableLayoutPanel2
|
||||
//
|
||||
tableLayoutPanel2.AutoSize = true;
|
||||
tableLayoutPanel2.AutoSizeMode = AutoSizeMode.GrowAndShrink;
|
||||
tableLayoutPanel2.ColumnCount = 2;
|
||||
tableLayoutPanel1.SetColumnSpan(tableLayoutPanel2, 4);
|
||||
tableLayoutPanel2.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 50F));
|
||||
tableLayoutPanel2.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 50F));
|
||||
tableLayoutPanel2.Controls.Add(button_Ok, 0, 0);
|
||||
tableLayoutPanel2.Controls.Add(button_Cancel, 1, 0);
|
||||
tableLayoutPanel2.Dock = DockStyle.Bottom;
|
||||
tableLayoutPanel2.Location = new Point(3, 208);
|
||||
tableLayoutPanel2.Name = "tableLayoutPanel2";
|
||||
tableLayoutPanel2.RowCount = 1;
|
||||
tableLayoutPanel2.RowStyles.Add(new RowStyle());
|
||||
tableLayoutPanel2.Size = new Size(813, 40);
|
||||
tableLayoutPanel2.TabIndex = 10;
|
||||
//
|
||||
// button_Ok
|
||||
//
|
||||
button_Ok.Anchor = AnchorStyles.Bottom | AnchorStyles.Right;
|
||||
button_Ok.Location = new Point(264, 3);
|
||||
button_Ok.Margin = new Padding(3, 3, 30, 3);
|
||||
button_Ok.Name = "button_Ok";
|
||||
button_Ok.Size = new Size(112, 34);
|
||||
button_Ok.TabIndex = 7;
|
||||
button_Ok.Text = "确认";
|
||||
button_Ok.UseVisualStyleBackColor = true;
|
||||
button_Ok.Click += button_Ok_Click;
|
||||
//
|
||||
// button_Cancel
|
||||
//
|
||||
button_Cancel.Anchor = AnchorStyles.Bottom | AnchorStyles.Left;
|
||||
button_Cancel.Location = new Point(436, 3);
|
||||
button_Cancel.Margin = new Padding(30, 3, 3, 3);
|
||||
button_Cancel.Name = "button_Cancel";
|
||||
button_Cancel.Size = new Size(112, 34);
|
||||
button_Cancel.TabIndex = 8;
|
||||
button_Cancel.Text = "取消";
|
||||
button_Cancel.UseVisualStyleBackColor = true;
|
||||
button_Cancel.Click += button_Cancel_Click;
|
||||
//
|
||||
// numericUpDown_Duration
|
||||
//
|
||||
numericUpDown_Duration.Anchor = AnchorStyles.Left;
|
||||
numericUpDown_Duration.DecimalPlaces = 3;
|
||||
numericUpDown_Duration.Location = new Point(113, 97);
|
||||
numericUpDown_Duration.Maximum = new decimal(new int[] { 300, 0, 0, 0 });
|
||||
numericUpDown_Duration.Name = "numericUpDown_Duration";
|
||||
numericUpDown_Duration.Size = new Size(180, 30);
|
||||
numericUpDown_Duration.TabIndex = 12;
|
||||
numericUpDown_Duration.TextAlign = HorizontalAlignment.Right;
|
||||
numericUpDown_Duration.Value = new decimal(new int[] { 1, 0, 0, 0 });
|
||||
//
|
||||
// numericUpDown_Fps
|
||||
//
|
||||
numericUpDown_Fps.Anchor = AnchorStyles.Left;
|
||||
numericUpDown_Fps.Location = new Point(113, 133);
|
||||
numericUpDown_Fps.Maximum = new decimal(new int[] { 300, 0, 0, 0 });
|
||||
numericUpDown_Fps.Minimum = new decimal(new int[] { 1, 0, 0, 0 });
|
||||
numericUpDown_Fps.Name = "numericUpDown_Fps";
|
||||
numericUpDown_Fps.Size = new Size(180, 30);
|
||||
numericUpDown_Fps.TabIndex = 13;
|
||||
numericUpDown_Fps.TextAlign = HorizontalAlignment.Right;
|
||||
numericUpDown_Fps.Value = new decimal(new int[] { 60, 0, 0, 0 });
|
||||
//
|
||||
// folderBrowserDialog
|
||||
//
|
||||
folderBrowserDialog.AddToRecent = false;
|
||||
//
|
||||
// ExportPngDialog
|
||||
//
|
||||
AcceptButton = button_Ok;
|
||||
AutoScaleDimensions = new SizeF(11F, 24F);
|
||||
AutoScaleMode = AutoScaleMode.Font;
|
||||
CancelButton = button_Cancel;
|
||||
ClientSize = new Size(919, 276);
|
||||
Controls.Add(panel1);
|
||||
FormBorderStyle = FormBorderStyle.FixedDialog;
|
||||
Icon = (Icon)resources.GetObject("$this.Icon");
|
||||
MaximizeBox = false;
|
||||
MinimizeBox = false;
|
||||
Name = "ExportPngDialog";
|
||||
ShowInTaskbar = false;
|
||||
StartPosition = FormStartPosition.CenterScreen;
|
||||
Text = "导出PNG序列";
|
||||
panel1.ResumeLayout(false);
|
||||
panel1.PerformLayout();
|
||||
tableLayoutPanel1.ResumeLayout(false);
|
||||
tableLayoutPanel1.PerformLayout();
|
||||
tableLayoutPanel2.ResumeLayout(false);
|
||||
((System.ComponentModel.ISupportInitialize)numericUpDown_Duration).EndInit();
|
||||
((System.ComponentModel.ISupportInitialize)numericUpDown_Fps).EndInit();
|
||||
ResumeLayout(false);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
private Panel panel1;
|
||||
private TableLayoutPanel tableLayoutPanel1;
|
||||
private Label label4;
|
||||
private Label label1;
|
||||
private Label label2;
|
||||
private Label label3;
|
||||
private TextBox textBox_OutputDir;
|
||||
private Button button_SelectOutputDir;
|
||||
private TableLayoutPanel tableLayoutPanel2;
|
||||
private Button button_Ok;
|
||||
private Button button_Cancel;
|
||||
private NumericUpDown numericUpDown_Duration;
|
||||
private NumericUpDown numericUpDown_Fps;
|
||||
private FolderBrowserDialog folderBrowserDialog;
|
||||
}
|
||||
}
|
||||
@@ -1,75 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using System.Data;
|
||||
using System.Drawing;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows.Forms;
|
||||
|
||||
namespace SpineViewer.Dialogs
|
||||
{
|
||||
public partial class ExportPngDialog : Form
|
||||
{
|
||||
public string OutputDir { get; private set; }
|
||||
public float Duration { get; private set; }
|
||||
public uint Fps { get; private set; }
|
||||
|
||||
public ExportPngDialog()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
|
||||
private void button_SelectOutputDir_Click(object sender, EventArgs e)
|
||||
{
|
||||
folderBrowserDialog.InitialDirectory = textBox_OutputDir.Text;
|
||||
if (folderBrowserDialog.ShowDialog() == DialogResult.OK)
|
||||
{
|
||||
textBox_OutputDir.Text = Path.GetFullPath(folderBrowserDialog.SelectedPath);
|
||||
}
|
||||
}
|
||||
|
||||
private void button_Ok_Click(object sender, EventArgs e)
|
||||
{
|
||||
var outputDir = textBox_OutputDir.Text;
|
||||
if (File.Exists(outputDir))
|
||||
{
|
||||
MessageBox.Show("输出文件夹无效", "错误信息", MessageBoxButtons.OK, MessageBoxIcon.Information);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!Directory.Exists(outputDir))
|
||||
{
|
||||
if (MessageBox.Show($"文件夹 {outputDir} 不存在,是否创建?", "操作确认", MessageBoxButtons.OKCancel, MessageBoxIcon.Question) == DialogResult.OK)
|
||||
{
|
||||
try
|
||||
{
|
||||
Directory.CreateDirectory(outputDir);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Program.Logger.Error(ex.ToString());
|
||||
MessageBox.Show(ex.ToString(), "文件夹创建失败", MessageBoxButtons.OK, MessageBoxIcon.Error);
|
||||
return;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
OutputDir = Path.GetFullPath(outputDir);
|
||||
Duration = (float)numericUpDown_Duration.Value;
|
||||
Fps = (uint)numericUpDown_Fps.Value;
|
||||
|
||||
DialogResult = DialogResult.OK;
|
||||
}
|
||||
|
||||
private void button_Cancel_Click(object sender, EventArgs e)
|
||||
{
|
||||
DialogResult = DialogResult.Cancel;
|
||||
}
|
||||
}
|
||||
}
|
||||
7
SpineViewer/Dialogs/OpenSpineDialog.Designer.cs
generated
7
SpineViewer/Dialogs/OpenSpineDialog.Designer.cs
generated
@@ -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";
|
||||
openFileDialog_Skel.Title = "选择skel文件";
|
||||
//
|
||||
// openFileDialog_Atlas
|
||||
//
|
||||
openFileDialog_Atlas.AddExtension = false;
|
||||
openFileDialog_Atlas.AddToRecent = false;
|
||||
openFileDialog_Atlas.Filter = "atlas 文件 (*.atlas)|*.atlas|所有文件 (*.*)|*.*";
|
||||
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);
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using SpineViewer.Spine;
|
||||
using SpineViewer.Utils;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
@@ -8,22 +9,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 = SpineUtils.Names.ToList();
|
||||
comboBox_Version.DisplayMember = "Value";
|
||||
comboBox_Version.ValueMember = "Key";
|
||||
comboBox_Version.SelectedValue = Spine.Version.V38;
|
||||
comboBox_Version.SelectedValue = SpineVersion.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 +54,11 @@ namespace SpineViewer.Dialogs
|
||||
{
|
||||
var skelPath = textBox_SkelPath.Text;
|
||||
var atlasPath = textBox_AtlasPath.Text;
|
||||
var version = (SpineVersion)comboBox_Version.SelectedValue;
|
||||
|
||||
if (!File.Exists(skelPath))
|
||||
{
|
||||
MessageBox.Show($"{skelPath}", "skel文件不存在", MessageBoxButtons.OK, MessageBoxIcon.Information);
|
||||
MessagePopup.Info($"{skelPath}", "skel文件不存在");
|
||||
return;
|
||||
}
|
||||
else
|
||||
@@ -59,13 +66,13 @@ namespace SpineViewer.Dialogs
|
||||
skelPath = Path.GetFullPath(skelPath);
|
||||
}
|
||||
|
||||
if (string.IsNullOrEmpty(atlasPath))
|
||||
if (string.IsNullOrWhiteSpace(atlasPath))
|
||||
{
|
||||
atlasPath = null;
|
||||
}
|
||||
else if (!File.Exists(atlasPath))
|
||||
{
|
||||
MessageBox.Show($"{atlasPath}", "atlas文件不存在", MessageBoxButtons.OK, MessageBoxIcon.Information);
|
||||
MessagePopup.Info($"{atlasPath}", "atlas文件不存在");
|
||||
return;
|
||||
}
|
||||
else
|
||||
@@ -73,10 +80,13 @@ namespace SpineViewer.Dialogs
|
||||
atlasPath = Path.GetFullPath(atlasPath);
|
||||
}
|
||||
|
||||
SkelPath = skelPath;
|
||||
AtlasPath = atlasPath;
|
||||
Version = (Spine.Version)comboBox_Version.SelectedValue;
|
||||
if (version != SpineVersion.Auto && !Spine.SpineObject.HasImplementation(version))
|
||||
{
|
||||
MessagePopup.Info($"{version.GetName()} 版本尚未实现(咕咕咕~)");
|
||||
return;
|
||||
}
|
||||
|
||||
Result = new(version, skelPath, atlasPath);
|
||||
DialogResult = DialogResult.OK;
|
||||
}
|
||||
|
||||
@@ -85,4 +95,25 @@ namespace SpineViewer.Dialogs
|
||||
DialogResult = DialogResult.Cancel;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 打开骨骼对话框结果
|
||||
/// </summary>
|
||||
public class OpenSpineDialogResult(SpineVersion version, string skelPath, string? atlasPath = null)
|
||||
{
|
||||
/// <summary>
|
||||
/// 版本
|
||||
/// </summary>
|
||||
public SpineVersion Version => version;
|
||||
|
||||
/// <summary>
|
||||
/// skel 文件路径
|
||||
/// </summary>
|
||||
public string SkelPath => skelPath;
|
||||
|
||||
/// <summary>
|
||||
/// atlas 文件路径
|
||||
/// </summary>
|
||||
public string? AtlasPath => atlasPath;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
using System;
|
||||
using NLog;
|
||||
using SpineViewer.Utils;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using System.Data;
|
||||
@@ -12,21 +14,33 @@ namespace SpineViewer.Dialogs
|
||||
{
|
||||
public partial class ProgressDialog : Form
|
||||
{
|
||||
[Category("自定义"), Description("BackgroundWorker 的 DoWork 事件")]
|
||||
public event DoWorkEventHandler? DoWork
|
||||
{
|
||||
add { backgroundWorker.DoWork += value; }
|
||||
remove { backgroundWorker.DoWork -= value; }
|
||||
}
|
||||
|
||||
public void RunWorkerAsync() { backgroundWorker.RunWorkerAsync(); }
|
||||
public void RunWorkerAsync(object? argument) { backgroundWorker.RunWorkerAsync(argument); }
|
||||
private readonly Logger logger = LogManager.GetCurrentClassLogger();
|
||||
|
||||
public ProgressDialog()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// BackgroundWorker.DoWork 接口暴露
|
||||
/// </summary>
|
||||
[Category("自定义"), Description("BackgroundWorker 的 DoWork 事件")]
|
||||
public event DoWorkEventHandler? DoWork
|
||||
{
|
||||
add => backgroundWorker.DoWork += value;
|
||||
remove => backgroundWorker.DoWork -= value;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 启动后台执行
|
||||
/// </summary>
|
||||
public void RunWorkerAsync() => backgroundWorker.RunWorkerAsync();
|
||||
|
||||
/// <summary>
|
||||
/// 使用给定参数启动后台执行
|
||||
/// </summary>
|
||||
public void RunWorkerAsync(object? argument) => backgroundWorker.RunWorkerAsync(argument);
|
||||
|
||||
private void backgroundWorker_ProgressChanged(object sender, ProgressChangedEventArgs e)
|
||||
{
|
||||
label_Tip.Text = e.UserState as string;
|
||||
@@ -37,8 +51,8 @@ namespace SpineViewer.Dialogs
|
||||
{
|
||||
if (e.Error != null)
|
||||
{
|
||||
Program.Logger.Error(e.Error.ToString());
|
||||
MessageBox.Show(e.Error.ToString(), "执行出错", MessageBoxButtons.OK, MessageBoxIcon.Error);
|
||||
logger.Error(e.Error.ToString());
|
||||
MessagePopup.Error(e.Error.ToString(), "执行出错");
|
||||
DialogResult = DialogResult.Abort;
|
||||
}
|
||||
else if (e.Cancelled)
|
||||
|
||||
21
SpineViewer/Extensions/NLogExtension.cs
Normal file
21
SpineViewer/Extensions/NLogExtension.cs
Normal file
@@ -0,0 +1,21 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace SpineViewer.Extensions
|
||||
{
|
||||
public static class NLogExtension
|
||||
{
|
||||
/// <summary>
|
||||
/// 输出当前进程的内存占用
|
||||
/// </summary>
|
||||
public static void LogCurrentProcessMemoryUsage(this NLog.Logger logger)
|
||||
{
|
||||
var process = Process.GetCurrentProcess();
|
||||
logger.Info("Current memory usage for {}: {:F2} MB", process.ProcessName, process.WorkingSet64 / 1024.0 / 1024.0);
|
||||
}
|
||||
}
|
||||
}
|
||||
118
SpineViewer/Extensions/SFMLExtension.cs
Normal file
118
SpineViewer/Extensions/SFMLExtension.cs
Normal file
@@ -0,0 +1,118 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace SpineViewer.Extensions
|
||||
{
|
||||
public static class SFMLExtension
|
||||
{
|
||||
/// <summary>
|
||||
/// 获取并集范围
|
||||
/// </summary>
|
||||
public static RectangleF Union(this RectangleF bounds, RectangleF other)
|
||||
{
|
||||
var x = Math.Min(bounds.X, other.X);
|
||||
var y = Math.Min(bounds.Y, other.Y);
|
||||
var w = Math.Max(bounds.Right, other.Right) - x;
|
||||
var h = Math.Max(bounds.Bottom, other.Bottom) - y;
|
||||
return new(x, y, w, h);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取适合指定画布参数下能够覆盖包围盒的画布视区包围盒
|
||||
/// </summary>
|
||||
public static RectangleF GetCanvasBounds(this RectangleF bounds, Size resolution) => GetCanvasBounds(bounds, resolution, new(0), new(0));
|
||||
|
||||
/// <summary>
|
||||
/// 获取适合指定画布参数下能够覆盖包围盒的画布视区包围盒
|
||||
/// </summary>
|
||||
public static RectangleF GetCanvasBounds(this RectangleF bounds, Size resolution, Padding margin) => GetCanvasBounds(bounds, resolution, margin, new(0));
|
||||
|
||||
/// <summary>
|
||||
/// 获取适合指定画布参数下能够覆盖包围盒的画布视区包围盒
|
||||
/// </summary>
|
||||
public static RectangleF GetCanvasBounds(this RectangleF bounds, Size resolution, Padding margin, Padding padding)
|
||||
{
|
||||
float sizeW = bounds.Width;
|
||||
float sizeH = bounds.Height;
|
||||
float innerW = resolution.Width - padding.Horizontal;
|
||||
float innerH = resolution.Height - padding.Vertical;
|
||||
float scale = Math.Max(Math.Abs(sizeW / innerW), Math.Abs(sizeH / innerH)); // 取两方向上较大的缩放比, 以此让画布可以覆盖内容
|
||||
float scaleW = scale * Math.Sign(sizeW);
|
||||
float scaleH = scale * Math.Sign(sizeH);
|
||||
|
||||
innerW *= scaleW;
|
||||
innerH *= scaleH;
|
||||
|
||||
var x = bounds.X - (innerW - sizeW) / 2 - (margin.Left + padding.Left) * scaleW;
|
||||
var y = bounds.Y - (innerH - sizeH) / 2 - (margin.Top + padding.Top) * scaleH;
|
||||
var w = (resolution.Width + margin.Horizontal) * scaleW;
|
||||
var h = (resolution.Height + margin.Vertical) * scaleH;
|
||||
return new(x, y, w, h);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取 Winforms Bitmap 对象, 需要使用 Dispose 释放对象
|
||||
/// </summary>
|
||||
public static Bitmap CopyToBitmap(this SFML.Graphics.Image image)
|
||||
{
|
||||
image.SaveToMemory(out var imgBuffer, "bmp");
|
||||
using var stream = new MemoryStream(imgBuffer);
|
||||
using var bitmap = new Bitmap(stream);
|
||||
return new(bitmap); // 必须重复构造一个副本才能摆脱对流的依赖, 否则之后使用会报错
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取 Winforms Bitmap 对象, 需要使用 Dispose 释放对象
|
||||
/// </summary>
|
||||
public static Bitmap CopyToBitmap(this SFML.Graphics.Texture texture)
|
||||
{
|
||||
using var image = texture.CopyToImage();
|
||||
return image.CopyToBitmap();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取视区的包围盒
|
||||
/// </summary>
|
||||
public static RectangleF GetBounds(this SFML.Graphics.View view)
|
||||
{
|
||||
return new(
|
||||
view.Center.X - view.Size.X / 2,
|
||||
view.Center.Y - view.Size.Y / 2,
|
||||
view.Size.X,
|
||||
view.Size.Y
|
||||
);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 按画布设置视区, 边缘和填充区域将不会出现内容
|
||||
/// </summary>
|
||||
public static void SetViewport(this SFML.Graphics.View view, Size resolution, Padding margin) => SetViewport(view, resolution, margin, new(0));
|
||||
|
||||
/// <summary>
|
||||
/// 按画布设置视区, 边缘和填充区域将不会出现内容
|
||||
/// </summary>
|
||||
public static void SetViewport(this SFML.Graphics.View view, Size resolution, Padding margin, Padding padding)
|
||||
{
|
||||
var innerW = resolution.Width - padding.Horizontal;
|
||||
var innerH = resolution.Height - padding.Vertical;
|
||||
|
||||
float width = resolution.Width + margin.Horizontal;
|
||||
float height = resolution.Height + margin.Vertical;
|
||||
|
||||
view.Viewport = new(
|
||||
(margin.Left + padding.Left) / width,
|
||||
(margin.Top + padding.Top) / height,
|
||||
innerW / width,
|
||||
innerH / height
|
||||
);
|
||||
|
||||
var bounds = view.GetBounds().GetCanvasBounds(new(innerW, innerH));
|
||||
|
||||
view.Center = new(bounds.X + bounds.Width / 2, bounds.Y + bounds.Height / 2);
|
||||
view.Size = new(bounds.Width, bounds.Height);
|
||||
}
|
||||
}
|
||||
}
|
||||
51
SpineViewer/Forms/SpinePreviewFullScreenForm.Designer.cs
generated
Normal file
51
SpineViewer/Forms/SpinePreviewFullScreenForm.Designer.cs
generated
Normal file
@@ -0,0 +1,51 @@
|
||||
namespace SpineViewer.Forms
|
||||
{
|
||||
partial class SpinePreviewFullScreenForm
|
||||
{
|
||||
/// <summary>
|
||||
/// Required designer variable.
|
||||
/// </summary>
|
||||
private System.ComponentModel.IContainer components = null;
|
||||
|
||||
/// <summary>
|
||||
/// Clean up any resources being used.
|
||||
/// </summary>
|
||||
/// <param name="disposing">true if managed resources should be disposed; otherwise, false.</param>
|
||||
protected override void Dispose(bool disposing)
|
||||
{
|
||||
if (disposing && (components != null))
|
||||
{
|
||||
components.Dispose();
|
||||
}
|
||||
base.Dispose(disposing);
|
||||
}
|
||||
|
||||
#region Windows Form Designer generated code
|
||||
|
||||
/// <summary>
|
||||
/// Required method for Designer support - do not modify
|
||||
/// the contents of this method with the code editor.
|
||||
/// </summary>
|
||||
private void InitializeComponent()
|
||||
{
|
||||
SuspendLayout();
|
||||
//
|
||||
// SpinePreviewFullScreenForm
|
||||
//
|
||||
AutoScaleMode = AutoScaleMode.None;
|
||||
ClientSize = new Size(512, 512);
|
||||
ControlBox = false;
|
||||
FormBorderStyle = FormBorderStyle.None;
|
||||
MaximizeBox = false;
|
||||
MinimizeBox = false;
|
||||
Name = "SpinePreviewFullScreenForm";
|
||||
ShowIcon = false;
|
||||
ShowInTaskbar = false;
|
||||
StartPosition = FormStartPosition.Manual;
|
||||
TopMost = true;
|
||||
ResumeLayout(false);
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
35
SpineViewer/Forms/SpinePreviewFullScreenForm.cs
Normal file
35
SpineViewer/Forms/SpinePreviewFullScreenForm.cs
Normal file
@@ -0,0 +1,35 @@
|
||||
using SpineViewer.Natives;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using System.ComponentModel.Design;
|
||||
using System.Data;
|
||||
using System.Drawing;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows.Forms;
|
||||
|
||||
namespace SpineViewer.Forms
|
||||
{
|
||||
[ToolboxItem(true)]
|
||||
[Designer(typeof(ComponentDesigner), typeof(IDesigner))]
|
||||
[DesignTimeVisible(true)]
|
||||
public partial class SpinePreviewFullScreenForm: Form
|
||||
{
|
||||
public SpinePreviewFullScreenForm()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
|
||||
protected override CreateParams CreateParams
|
||||
{
|
||||
get
|
||||
{
|
||||
var cp = base.CreateParams;
|
||||
cp.ExStyle |= Win32.WS_EX_TOOLWINDOW;
|
||||
return cp;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
namespace SpineViewer
|
||||
{
|
||||
partial class MainForm
|
||||
partial class SpineViewerForm
|
||||
{
|
||||
/// <summary>
|
||||
/// Required designer variable.
|
||||
@@ -29,34 +29,52 @@
|
||||
private void InitializeComponent()
|
||||
{
|
||||
components = new System.ComponentModel.Container();
|
||||
System.ComponentModel.ComponentResourceManager resources = new System.ComponentModel.ComponentResourceManager(typeof(MainForm));
|
||||
System.ComponentModel.ComponentResourceManager resources = new System.ComponentModel.ComponentResourceManager(typeof(SpineViewerForm));
|
||||
menuStrip = new MenuStrip();
|
||||
toolStripMenuItem_File = new ToolStripMenuItem();
|
||||
toolStripMenuItem_Open = new ToolStripMenuItem();
|
||||
toolStripMenuItem_BatchOpen = new ToolStripMenuItem();
|
||||
toolStripSeparator1 = new ToolStripSeparator();
|
||||
toolStripMenuItem_Export = new ToolStripMenuItem();
|
||||
toolStripMenuItem_ExportFrame = new ToolStripMenuItem();
|
||||
toolStripMenuItem_ExportFrameSequence = new ToolStripMenuItem();
|
||||
toolStripSeparator4 = new ToolStripSeparator();
|
||||
toolStripMenuItem_ExportGif = new ToolStripMenuItem();
|
||||
toolStripMenuItem_ExportWebp = new ToolStripMenuItem();
|
||||
toolStripMenuItem_ExportAvif = new ToolStripMenuItem();
|
||||
toolStripSeparator5 = new ToolStripSeparator();
|
||||
toolStripMenuItem_ExportMp4 = new ToolStripMenuItem();
|
||||
toolStripMenuItem_ExportWebm = new ToolStripMenuItem();
|
||||
toolStripMenuItem_ExportMkv = new ToolStripMenuItem();
|
||||
toolStripMenuItem_ExportMov = new ToolStripMenuItem();
|
||||
toolStripSeparator6 = new ToolStripSeparator();
|
||||
toolStripMenuItem_ExportCustom = new ToolStripMenuItem();
|
||||
toolStripSeparator2 = new ToolStripSeparator();
|
||||
toolStripMenuItem_Exit = new ToolStripMenuItem();
|
||||
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();
|
||||
toolStripMenuItem_About = new ToolStripMenuItem();
|
||||
toolStripMenuItem_Debug = new ToolStripMenuItem();
|
||||
toolStripMenuItem_Experiment = new ToolStripMenuItem();
|
||||
toolStripMenuItem_DesktopProjection = new ToolStripMenuItem();
|
||||
rtbLog = new RichTextBox();
|
||||
splitContainer_MainForm = new SplitContainer();
|
||||
splitContainer_Functional = new SplitContainer();
|
||||
splitContainer_Information = new SplitContainer();
|
||||
groupBox_SkelList = new GroupBox();
|
||||
spineListView = new SpineViewer.Controls.SpineListView();
|
||||
propertyGrid_Spine = new PropertyGrid();
|
||||
spineViewPropertyGrid = new SpineViewer.Controls.SpineViewPropertyGrid();
|
||||
splitContainer_Config = new SplitContainer();
|
||||
groupBox_SkelConfig = new GroupBox();
|
||||
groupBox_PreviewConfig = new GroupBox();
|
||||
propertyGrid_Previewer = new PropertyGrid();
|
||||
groupBox_SkelConfig = new GroupBox();
|
||||
groupBox_Preview = new GroupBox();
|
||||
spinePreviewer = new SpineViewer.Controls.SpinePreviewer();
|
||||
spinePreviewPanel = new SpineViewer.Controls.SpinePreviewPanel();
|
||||
panel_MainForm = new Panel();
|
||||
toolTip = new ToolTip(components);
|
||||
menuStrip.SuspendLayout();
|
||||
@@ -77,8 +95,8 @@
|
||||
splitContainer_Config.Panel1.SuspendLayout();
|
||||
splitContainer_Config.Panel2.SuspendLayout();
|
||||
splitContainer_Config.SuspendLayout();
|
||||
groupBox_SkelConfig.SuspendLayout();
|
||||
groupBox_PreviewConfig.SuspendLayout();
|
||||
groupBox_SkelConfig.SuspendLayout();
|
||||
groupBox_Preview.SuspendLayout();
|
||||
panel_MainForm.SuspendLayout();
|
||||
SuspendLayout();
|
||||
@@ -87,10 +105,10 @@
|
||||
//
|
||||
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_Tool, toolStripMenuItem_Download, toolStripMenuItem_Help, toolStripMenuItem_Experiment });
|
||||
menuStrip.Location = new Point(0, 0);
|
||||
menuStrip.Name = "menuStrip";
|
||||
menuStrip.Size = new Size(1741, 32);
|
||||
menuStrip.Size = new Size(1778, 36);
|
||||
menuStrip.TabIndex = 0;
|
||||
menuStrip.Text = "菜单";
|
||||
//
|
||||
@@ -98,7 +116,7 @@
|
||||
//
|
||||
toolStripMenuItem_File.DropDownItems.AddRange(new ToolStripItem[] { toolStripMenuItem_Open, toolStripMenuItem_BatchOpen, toolStripSeparator1, toolStripMenuItem_Export, toolStripSeparator2, toolStripMenuItem_Exit });
|
||||
toolStripMenuItem_File.Name = "toolStripMenuItem_File";
|
||||
toolStripMenuItem_File.Size = new Size(84, 28);
|
||||
toolStripMenuItem_File.Size = new Size(84, 30);
|
||||
toolStripMenuItem_File.Text = "文件(&F)";
|
||||
//
|
||||
// toolStripMenuItem_Open
|
||||
@@ -123,11 +141,95 @@
|
||||
//
|
||||
// toolStripMenuItem_Export
|
||||
//
|
||||
toolStripMenuItem_Export.DropDownItems.AddRange(new ToolStripItem[] { toolStripMenuItem_ExportFrame, toolStripMenuItem_ExportFrameSequence, toolStripSeparator4, toolStripMenuItem_ExportGif, toolStripMenuItem_ExportWebp, toolStripMenuItem_ExportAvif, toolStripSeparator5, toolStripMenuItem_ExportMp4, toolStripMenuItem_ExportWebm, toolStripMenuItem_ExportMkv, toolStripMenuItem_ExportMov, toolStripSeparator6, toolStripMenuItem_ExportCustom });
|
||||
toolStripMenuItem_Export.Name = "toolStripMenuItem_Export";
|
||||
toolStripMenuItem_Export.ShortcutKeys = Keys.Control | Keys.S;
|
||||
toolStripMenuItem_Export.Size = new Size(254, 34);
|
||||
toolStripMenuItem_Export.Text = "导出(&E)...";
|
||||
toolStripMenuItem_Export.Click += toolStripMenuItem_Export_Click;
|
||||
toolStripMenuItem_Export.Text = "导出(&E)";
|
||||
//
|
||||
// toolStripMenuItem_ExportFrame
|
||||
//
|
||||
toolStripMenuItem_ExportFrame.Name = "toolStripMenuItem_ExportFrame";
|
||||
toolStripMenuItem_ExportFrame.Size = new Size(288, 34);
|
||||
toolStripMenuItem_ExportFrame.Text = "单帧画面...";
|
||||
toolStripMenuItem_ExportFrame.Click += toolStripMenuItem_ExportFrame_Click;
|
||||
//
|
||||
// toolStripMenuItem_ExportFrameSequence
|
||||
//
|
||||
toolStripMenuItem_ExportFrameSequence.Name = "toolStripMenuItem_ExportFrameSequence";
|
||||
toolStripMenuItem_ExportFrameSequence.Size = new Size(288, 34);
|
||||
toolStripMenuItem_ExportFrameSequence.Text = "帧序列...";
|
||||
toolStripMenuItem_ExportFrameSequence.Click += toolStripMenuItem_ExportFrameSequence_Click;
|
||||
//
|
||||
// toolStripSeparator4
|
||||
//
|
||||
toolStripSeparator4.Name = "toolStripSeparator4";
|
||||
toolStripSeparator4.Size = new Size(285, 6);
|
||||
//
|
||||
// toolStripMenuItem_ExportGif
|
||||
//
|
||||
toolStripMenuItem_ExportGif.Name = "toolStripMenuItem_ExportGif";
|
||||
toolStripMenuItem_ExportGif.Size = new Size(288, 34);
|
||||
toolStripMenuItem_ExportGif.Text = "GIF...";
|
||||
toolStripMenuItem_ExportGif.Click += toolStripMenuItem_ExportGif_Click;
|
||||
//
|
||||
// toolStripMenuItem_ExportWebp
|
||||
//
|
||||
toolStripMenuItem_ExportWebp.Name = "toolStripMenuItem_ExportWebp";
|
||||
toolStripMenuItem_ExportWebp.Size = new Size(288, 34);
|
||||
toolStripMenuItem_ExportWebp.Text = "WebP...";
|
||||
toolStripMenuItem_ExportWebp.Click += toolStripMenuItem_ExportWebp_Click;
|
||||
//
|
||||
// toolStripMenuItem_ExportAvif
|
||||
//
|
||||
toolStripMenuItem_ExportAvif.Name = "toolStripMenuItem_ExportAvif";
|
||||
toolStripMenuItem_ExportAvif.Size = new Size(288, 34);
|
||||
toolStripMenuItem_ExportAvif.Text = "AVIF...";
|
||||
toolStripMenuItem_ExportAvif.Click += toolStripMenuItem_ExportAvif_Click;
|
||||
//
|
||||
// toolStripSeparator5
|
||||
//
|
||||
toolStripSeparator5.Name = "toolStripSeparator5";
|
||||
toolStripSeparator5.Size = new Size(285, 6);
|
||||
//
|
||||
// toolStripMenuItem_ExportMp4
|
||||
//
|
||||
toolStripMenuItem_ExportMp4.Name = "toolStripMenuItem_ExportMp4";
|
||||
toolStripMenuItem_ExportMp4.Size = new Size(288, 34);
|
||||
toolStripMenuItem_ExportMp4.Text = "MP4...";
|
||||
toolStripMenuItem_ExportMp4.Click += toolStripMenuItem_ExportMp4_Click;
|
||||
//
|
||||
// toolStripMenuItem_ExportWebm
|
||||
//
|
||||
toolStripMenuItem_ExportWebm.Name = "toolStripMenuItem_ExportWebm";
|
||||
toolStripMenuItem_ExportWebm.Size = new Size(288, 34);
|
||||
toolStripMenuItem_ExportWebm.Text = "WebM...";
|
||||
toolStripMenuItem_ExportWebm.Click += toolStripMenuItem_ExportWebm_Click;
|
||||
//
|
||||
// toolStripMenuItem_ExportMkv
|
||||
//
|
||||
toolStripMenuItem_ExportMkv.Name = "toolStripMenuItem_ExportMkv";
|
||||
toolStripMenuItem_ExportMkv.Size = new Size(288, 34);
|
||||
toolStripMenuItem_ExportMkv.Text = "MKV...";
|
||||
toolStripMenuItem_ExportMkv.Click += toolStripMenuItem_ExportMkv_Click;
|
||||
//
|
||||
// toolStripMenuItem_ExportMov
|
||||
//
|
||||
toolStripMenuItem_ExportMov.Name = "toolStripMenuItem_ExportMov";
|
||||
toolStripMenuItem_ExportMov.Size = new Size(288, 34);
|
||||
toolStripMenuItem_ExportMov.Text = "MOV...";
|
||||
toolStripMenuItem_ExportMov.Click += toolStripMenuItem_ExportMov_Click;
|
||||
//
|
||||
// toolStripSeparator6
|
||||
//
|
||||
toolStripSeparator6.Name = "toolStripSeparator6";
|
||||
toolStripSeparator6.Size = new Size(285, 6);
|
||||
//
|
||||
// toolStripMenuItem_ExportCustom
|
||||
//
|
||||
toolStripMenuItem_ExportCustom.Name = "toolStripMenuItem_ExportCustom";
|
||||
toolStripMenuItem_ExportCustom.Size = new Size(288, 34);
|
||||
toolStripMenuItem_ExportCustom.Text = "FFmpeg 自定义导出...";
|
||||
toolStripMenuItem_ExportCustom.Click += toolStripMenuItem_ExportCustom_Click;
|
||||
//
|
||||
// toolStripSeparator2
|
||||
//
|
||||
@@ -142,46 +244,82 @@
|
||||
toolStripMenuItem_Exit.Text = "退出(&X)";
|
||||
toolStripMenuItem_Exit.Click += toolStripMenuItem_Exit_Click;
|
||||
//
|
||||
// toolStripMenuItem_Function
|
||||
// toolStripMenuItem_Tool
|
||||
//
|
||||
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_Tool.DropDownItems.AddRange(new ToolStripItem[] { toolStripMenuItem_ConvertFileFormat });
|
||||
toolStripMenuItem_Tool.Name = "toolStripMenuItem_Tool";
|
||||
toolStripMenuItem_Tool.Size = new Size(84, 30);
|
||||
toolStripMenuItem_Tool.Text = "工具(&T)";
|
||||
//
|
||||
// toolStripMenuItem_ResetAnimation
|
||||
// toolStripMenuItem_ConvertFileFormat
|
||||
//
|
||||
toolStripMenuItem_ResetAnimation.Name = "toolStripMenuItem_ResetAnimation";
|
||||
toolStripMenuItem_ResetAnimation.Size = new Size(242, 34);
|
||||
toolStripMenuItem_ResetAnimation.Text = "重置动画时间(&R)";
|
||||
toolStripMenuItem_ResetAnimation.Click += toolStripMenuItem_ResetAnimation_Click;
|
||||
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, 30);
|
||||
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 });
|
||||
toolStripMenuItem_Help.DropDownItems.AddRange(new ToolStripItem[] { toolStripMenuItem_Diagnostics, toolStripSeparator3, toolStripMenuItem_About, toolStripMenuItem_Debug });
|
||||
toolStripMenuItem_Help.Name = "toolStripMenuItem_Help";
|
||||
toolStripMenuItem_Help.Size = new Size(88, 28);
|
||||
toolStripMenuItem_Help.Size = new Size(88, 30);
|
||||
toolStripMenuItem_Help.Text = "帮助(&H)";
|
||||
//
|
||||
// toolStripMenuItem_Diagnostics
|
||||
//
|
||||
toolStripMenuItem_Diagnostics.Name = "toolStripMenuItem_Diagnostics";
|
||||
toolStripMenuItem_Diagnostics.Size = new Size(208, 34);
|
||||
toolStripMenuItem_Diagnostics.Size = new Size(270, 34);
|
||||
toolStripMenuItem_Diagnostics.Text = "诊断信息(&D)";
|
||||
toolStripMenuItem_Diagnostics.Click += toolStripMenuItem_Diagnostics_Click;
|
||||
//
|
||||
// toolStripSeparator3
|
||||
//
|
||||
toolStripSeparator3.Name = "toolStripSeparator3";
|
||||
toolStripSeparator3.Size = new Size(205, 6);
|
||||
toolStripSeparator3.Size = new Size(267, 6);
|
||||
//
|
||||
// toolStripMenuItem_About
|
||||
//
|
||||
toolStripMenuItem_About.Name = "toolStripMenuItem_About";
|
||||
toolStripMenuItem_About.Size = new Size(208, 34);
|
||||
toolStripMenuItem_About.Size = new Size(270, 34);
|
||||
toolStripMenuItem_About.Text = "关于(&A)";
|
||||
toolStripMenuItem_About.Click += toolStripMenuItem_About_Click;
|
||||
//
|
||||
// toolStripMenuItem_Debug
|
||||
//
|
||||
toolStripMenuItem_Debug.Name = "toolStripMenuItem_Debug";
|
||||
toolStripMenuItem_Debug.Size = new Size(270, 34);
|
||||
toolStripMenuItem_Debug.Text = "调试";
|
||||
toolStripMenuItem_Debug.Visible = false;
|
||||
toolStripMenuItem_Debug.Click += toolStripMenuItem_Debug_Click;
|
||||
//
|
||||
// toolStripMenuItem_Experiment
|
||||
//
|
||||
toolStripMenuItem_Experiment.DropDownItems.AddRange(new ToolStripItem[] { toolStripMenuItem_DesktopProjection });
|
||||
toolStripMenuItem_Experiment.Name = "toolStripMenuItem_Experiment";
|
||||
toolStripMenuItem_Experiment.Size = new Size(138, 30);
|
||||
toolStripMenuItem_Experiment.Text = "实验性功能(&E)";
|
||||
//
|
||||
// toolStripMenuItem_DesktopProjection
|
||||
//
|
||||
toolStripMenuItem_DesktopProjection.Name = "toolStripMenuItem_DesktopProjection";
|
||||
toolStripMenuItem_DesktopProjection.Size = new Size(182, 34);
|
||||
toolStripMenuItem_DesktopProjection.Text = "桌面投影";
|
||||
toolStripMenuItem_DesktopProjection.Click += toolStripMenuItem_DesktopProjection_Click;
|
||||
//
|
||||
// rtbLog
|
||||
//
|
||||
rtbLog.BackColor = SystemColors.Window;
|
||||
@@ -192,7 +330,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(1758, 158);
|
||||
rtbLog.TabIndex = 0;
|
||||
rtbLog.Text = "";
|
||||
rtbLog.WordWrap = false;
|
||||
@@ -201,6 +339,7 @@
|
||||
//
|
||||
splitContainer_MainForm.Cursor = Cursors.SizeNS;
|
||||
splitContainer_MainForm.Dock = DockStyle.Fill;
|
||||
splitContainer_MainForm.FixedPanel = FixedPanel.Panel2;
|
||||
splitContainer_MainForm.Location = new Point(10, 5);
|
||||
splitContainer_MainForm.Name = "splitContainer_MainForm";
|
||||
splitContainer_MainForm.Orientation = Orientation.Horizontal;
|
||||
@@ -214,8 +353,9 @@
|
||||
//
|
||||
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(1758, 1093);
|
||||
splitContainer_MainForm.SplitterDistance = 927;
|
||||
splitContainer_MainForm.SplitterWidth = 8;
|
||||
splitContainer_MainForm.TabIndex = 3;
|
||||
splitContainer_MainForm.TabStop = false;
|
||||
splitContainer_MainForm.SplitterMoved += splitContainer_SplitterMoved;
|
||||
@@ -225,6 +365,7 @@
|
||||
//
|
||||
splitContainer_Functional.Cursor = Cursors.SizeWE;
|
||||
splitContainer_Functional.Dock = DockStyle.Fill;
|
||||
splitContainer_Functional.FixedPanel = FixedPanel.Panel1;
|
||||
splitContainer_Functional.Location = new Point(0, 0);
|
||||
splitContainer_Functional.Name = "splitContainer_Functional";
|
||||
//
|
||||
@@ -237,8 +378,9 @@
|
||||
//
|
||||
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(1758, 927);
|
||||
splitContainer_Functional.SplitterDistance = 788;
|
||||
splitContainer_Functional.SplitterWidth = 8;
|
||||
splitContainer_Functional.TabIndex = 2;
|
||||
splitContainer_Functional.TabStop = false;
|
||||
splitContainer_Functional.SplitterMoved += splitContainer_SplitterMoved;
|
||||
@@ -260,8 +402,9 @@
|
||||
//
|
||||
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(788, 927);
|
||||
splitContainer_Information.SplitterDistance = 351;
|
||||
splitContainer_Information.SplitterWidth = 8;
|
||||
splitContainer_Information.TabIndex = 1;
|
||||
splitContainer_Information.TabStop = false;
|
||||
splitContainer_Information.SplitterMoved += splitContainer_SplitterMoved;
|
||||
@@ -273,7 +416,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(351, 927);
|
||||
groupBox_SkelList.TabIndex = 0;
|
||||
groupBox_SkelList.TabStop = false;
|
||||
groupBox_SkelList.Text = "模型列表";
|
||||
@@ -283,24 +426,20 @@
|
||||
spineListView.Dock = DockStyle.Fill;
|
||||
spineListView.Location = new Point(3, 26);
|
||||
spineListView.Name = "spineListView";
|
||||
spineListView.PropertyGrid = propertyGrid_Spine;
|
||||
spineListView.Size = new Size(321, 819);
|
||||
spineListView.Size = new Size(345, 898);
|
||||
spineListView.SpinePropertyGrid = spineViewPropertyGrid;
|
||||
spineListView.TabIndex = 0;
|
||||
//
|
||||
// propertyGrid_Spine
|
||||
// spineViewPropertyGrid
|
||||
//
|
||||
propertyGrid_Spine.Dock = DockStyle.Fill;
|
||||
propertyGrid_Spine.HelpVisible = false;
|
||||
propertyGrid_Spine.Location = new Point(3, 26);
|
||||
propertyGrid_Spine.Name = "propertyGrid_Spine";
|
||||
propertyGrid_Spine.Size = new Size(407, 470);
|
||||
propertyGrid_Spine.TabIndex = 0;
|
||||
propertyGrid_Spine.ToolbarVisible = false;
|
||||
propertyGrid_Spine.PropertyValueChanged += propertyGrid_PropertyValueChanged;
|
||||
spineViewPropertyGrid.Dock = DockStyle.Fill;
|
||||
spineViewPropertyGrid.Location = new Point(3, 26);
|
||||
spineViewPropertyGrid.Name = "spineViewPropertyGrid";
|
||||
spineViewPropertyGrid.Size = new Size(423, 575);
|
||||
spineViewPropertyGrid.TabIndex = 0;
|
||||
//
|
||||
// splitContainer_Config
|
||||
//
|
||||
splitContainer_Config.Cursor = Cursors.SizeNS;
|
||||
splitContainer_Config.Dock = DockStyle.Fill;
|
||||
splitContainer_Config.Location = new Point(0, 0);
|
||||
splitContainer_Config.Name = "splitContainer_Config";
|
||||
@@ -308,38 +447,24 @@
|
||||
//
|
||||
// splitContainer_Config.Panel1
|
||||
//
|
||||
splitContainer_Config.Panel1.Controls.Add(groupBox_SkelConfig);
|
||||
splitContainer_Config.Panel1.Cursor = Cursors.Default;
|
||||
splitContainer_Config.Panel1.Controls.Add(groupBox_PreviewConfig);
|
||||
//
|
||||
// splitContainer_Config.Panel2
|
||||
//
|
||||
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.Panel2.Controls.Add(groupBox_SkelConfig);
|
||||
splitContainer_Config.Size = new Size(429, 927);
|
||||
splitContainer_Config.SplitterDistance = 315;
|
||||
splitContainer_Config.SplitterWidth = 8;
|
||||
splitContainer_Config.TabIndex = 0;
|
||||
splitContainer_Config.TabStop = false;
|
||||
splitContainer_Config.SplitterMoved += splitContainer_SplitterMoved;
|
||||
splitContainer_Config.MouseUp += splitContainer_MouseUp;
|
||||
//
|
||||
// groupBox_SkelConfig
|
||||
//
|
||||
groupBox_SkelConfig.Controls.Add(propertyGrid_Spine);
|
||||
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.TabIndex = 0;
|
||||
groupBox_SkelConfig.TabStop = false;
|
||||
groupBox_SkelConfig.Text = "模型参数";
|
||||
//
|
||||
// groupBox_PreviewConfig
|
||||
//
|
||||
groupBox_PreviewConfig.Controls.Add(propertyGrid_Previewer);
|
||||
groupBox_PreviewConfig.Dock = DockStyle.Fill;
|
||||
groupBox_PreviewConfig.Location = new Point(0, 0);
|
||||
groupBox_PreviewConfig.Margin = new Padding(0);
|
||||
groupBox_PreviewConfig.Name = "groupBox_PreviewConfig";
|
||||
groupBox_PreviewConfig.Size = new Size(413, 345);
|
||||
groupBox_PreviewConfig.Size = new Size(429, 315);
|
||||
groupBox_PreviewConfig.TabIndex = 1;
|
||||
groupBox_PreviewConfig.TabStop = false;
|
||||
groupBox_PreviewConfig.Text = "画面参数";
|
||||
@@ -350,59 +475,68 @@
|
||||
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(423, 286);
|
||||
propertyGrid_Previewer.TabIndex = 1;
|
||||
propertyGrid_Previewer.ToolbarVisible = false;
|
||||
propertyGrid_Previewer.PropertyValueChanged += propertyGrid_PropertyValueChanged;
|
||||
//
|
||||
// groupBox_SkelConfig
|
||||
//
|
||||
groupBox_SkelConfig.Controls.Add(spineViewPropertyGrid);
|
||||
groupBox_SkelConfig.Dock = DockStyle.Fill;
|
||||
groupBox_SkelConfig.Location = new Point(0, 0);
|
||||
groupBox_SkelConfig.Margin = new Padding(0);
|
||||
groupBox_SkelConfig.Name = "groupBox_SkelConfig";
|
||||
groupBox_SkelConfig.Size = new Size(429, 604);
|
||||
groupBox_SkelConfig.TabIndex = 0;
|
||||
groupBox_SkelConfig.TabStop = false;
|
||||
groupBox_SkelConfig.Text = "模型参数";
|
||||
//
|
||||
// groupBox_Preview
|
||||
//
|
||||
groupBox_Preview.Controls.Add(spinePreviewer);
|
||||
groupBox_Preview.Controls.Add(spinePreviewPanel);
|
||||
groupBox_Preview.Dock = DockStyle.Fill;
|
||||
groupBox_Preview.Location = new Point(0, 0);
|
||||
groupBox_Preview.Name = "groupBox_Preview";
|
||||
groupBox_Preview.Size = new Size(973, 848);
|
||||
groupBox_Preview.Size = new Size(962, 927);
|
||||
groupBox_Preview.TabIndex = 1;
|
||||
groupBox_Preview.TabStop = false;
|
||||
groupBox_Preview.Text = "预览画面";
|
||||
//
|
||||
// spinePreviewer
|
||||
// spinePreviewPanel
|
||||
//
|
||||
spinePreviewer.BackColor = SystemColors.ControlDark;
|
||||
spinePreviewer.Dock = DockStyle.Fill;
|
||||
spinePreviewer.Location = new Point(3, 26);
|
||||
spinePreviewer.Name = "spinePreviewer";
|
||||
spinePreviewer.PropertyGrid = propertyGrid_Previewer;
|
||||
spinePreviewer.Size = new Size(967, 819);
|
||||
spinePreviewer.SpineListView = spineListView;
|
||||
spinePreviewer.TabIndex = 0;
|
||||
spinePreviewer.MouseUp += spinePreviewer_MouseUp;
|
||||
spinePreviewPanel.Dock = DockStyle.Fill;
|
||||
spinePreviewPanel.Location = new Point(3, 26);
|
||||
spinePreviewPanel.Name = "spinePreviewPanel";
|
||||
spinePreviewPanel.PropertyGrid = propertyGrid_Previewer;
|
||||
spinePreviewPanel.Size = new Size(956, 898);
|
||||
spinePreviewPanel.SpineListView = spineListView;
|
||||
spinePreviewPanel.TabIndex = 0;
|
||||
//
|
||||
// panel_MainForm
|
||||
//
|
||||
panel_MainForm.Controls.Add(splitContainer_MainForm);
|
||||
panel_MainForm.Dock = DockStyle.Fill;
|
||||
panel_MainForm.Location = new Point(0, 32);
|
||||
panel_MainForm.Location = new Point(0, 36);
|
||||
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(1778, 1108);
|
||||
panel_MainForm.TabIndex = 4;
|
||||
//
|
||||
// toolTip
|
||||
//
|
||||
toolTip.ShowAlways = true;
|
||||
//
|
||||
// MainForm
|
||||
// SpineViewerForm
|
||||
//
|
||||
AutoScaleDimensions = new SizeF(11F, 24F);
|
||||
AutoScaleMode = AutoScaleMode.Font;
|
||||
ClientSize = new Size(1741, 1005);
|
||||
AutoScaleDimensions = new SizeF(144F, 144F);
|
||||
AutoScaleMode = AutoScaleMode.Dpi;
|
||||
ClientSize = new Size(1778, 1144);
|
||||
Controls.Add(panel_MainForm);
|
||||
Controls.Add(menuStrip);
|
||||
Icon = (Icon)resources.GetObject("$this.Icon");
|
||||
MainMenuStrip = menuStrip;
|
||||
Margin = new Padding(3, 2, 3, 2);
|
||||
Name = "MainForm";
|
||||
Name = "SpineViewerForm";
|
||||
StartPosition = FormStartPosition.CenterScreen;
|
||||
Text = "SpineViewer";
|
||||
FormClosing += MainForm_FormClosing;
|
||||
@@ -426,8 +560,8 @@
|
||||
splitContainer_Config.Panel2.ResumeLayout(false);
|
||||
((System.ComponentModel.ISupportInitialize)splitContainer_Config).EndInit();
|
||||
splitContainer_Config.ResumeLayout(false);
|
||||
groupBox_SkelConfig.ResumeLayout(false);
|
||||
groupBox_PreviewConfig.ResumeLayout(false);
|
||||
groupBox_SkelConfig.ResumeLayout(false);
|
||||
groupBox_Preview.ResumeLayout(false);
|
||||
panel_MainForm.ResumeLayout(false);
|
||||
ResumeLayout(false);
|
||||
@@ -441,7 +575,6 @@
|
||||
private ToolStripMenuItem toolStripMenuItem_Open;
|
||||
private ToolStripMenuItem toolStripMenuItem_Exit;
|
||||
private ToolStripSeparator toolStripSeparator1;
|
||||
private ToolStripMenuItem toolStripMenuItem_Export;
|
||||
private ToolStripSeparator toolStripSeparator2;
|
||||
private RichTextBox rtbLog;
|
||||
private SplitContainer splitContainer_MainForm;
|
||||
@@ -449,7 +582,6 @@
|
||||
private SplitContainer splitContainer_Information;
|
||||
private GroupBox groupBox_SkelList;
|
||||
private GroupBox groupBox_SkelConfig;
|
||||
private SplitContainer splitContainer_Config;
|
||||
private GroupBox groupBox_PreviewConfig;
|
||||
private Panel panel_MainForm;
|
||||
private ToolStripMenuItem toolStripMenuItem_Help;
|
||||
@@ -457,13 +589,33 @@
|
||||
private ToolStripMenuItem toolStripMenuItem_BatchOpen;
|
||||
private GroupBox groupBox_Preview;
|
||||
private ToolTip toolTip;
|
||||
private PropertyGrid propertyGrid_Spine;
|
||||
private Controls.SpineListView spineListView;
|
||||
private PropertyGrid propertyGrid_Previewer;
|
||||
private Controls.SpinePreviewer spinePreviewer;
|
||||
private ToolStripMenuItem toolStripMenuItem_Function;
|
||||
private ToolStripMenuItem toolStripMenuItem_ResetAnimation;
|
||||
private Controls.SpinePreviewPanel spinePreviewPanel;
|
||||
private ToolStripMenuItem toolStripMenuItem_Diagnostics;
|
||||
private ToolStripSeparator toolStripSeparator3;
|
||||
private ToolStripMenuItem toolStripMenuItem_Download;
|
||||
private ToolStripMenuItem toolStripMenuItem_ManageResource;
|
||||
private ToolStripMenuItem toolStripMenuItem_Tool;
|
||||
private ToolStripMenuItem toolStripMenuItem_ConvertFileFormat;
|
||||
private ToolStripMenuItem toolStripMenuItem_Export;
|
||||
private ToolStripMenuItem toolStripMenuItem_ExportFrame;
|
||||
private ToolStripMenuItem toolStripMenuItem_ExportFrameSequence;
|
||||
private ToolStripMenuItem toolStripMenuItem_ExportGif;
|
||||
private ToolStripMenuItem toolStripMenuItem_ExportMp4;
|
||||
private ToolStripMenuItem toolStripMenuItem_ExportMov;
|
||||
private ToolStripMenuItem toolStripMenuItem_ExportMkv;
|
||||
private ToolStripMenuItem toolStripMenuItem_ExportWebm;
|
||||
private ToolStripMenuItem toolStripMenuItem_ExportCustom;
|
||||
private Controls.SpineViewPropertyGrid spineViewPropertyGrid;
|
||||
private ToolStripSeparator toolStripSeparator4;
|
||||
private ToolStripMenuItem toolStripMenuItem_ExportWebp;
|
||||
private ToolStripMenuItem toolStripMenuItem_ExportAvif;
|
||||
private ToolStripSeparator toolStripSeparator5;
|
||||
private ToolStripSeparator toolStripSeparator6;
|
||||
private SplitContainer splitContainer_Config;
|
||||
private ToolStripMenuItem toolStripMenuItem_Experiment;
|
||||
private ToolStripMenuItem toolStripMenuItem_DesktopProjection;
|
||||
private ToolStripMenuItem toolStripMenuItem_Debug;
|
||||
}
|
||||
}
|
||||
467
SpineViewer/Forms/SpineViewerForm.cs
Normal file
467
SpineViewer/Forms/SpineViewerForm.cs
Normal file
@@ -0,0 +1,467 @@
|
||||
using NLog;
|
||||
using SpineViewer.Spine;
|
||||
using System.ComponentModel;
|
||||
using System.Diagnostics;
|
||||
using SpineViewer.Natives;
|
||||
using SpineViewer.Utils;
|
||||
using SpineViewer.Spine.SpineExporter;
|
||||
|
||||
namespace SpineViewer
|
||||
{
|
||||
internal partial class SpineViewerForm : Form
|
||||
{
|
||||
private readonly Logger logger = LogManager.GetCurrentClassLogger();
|
||||
|
||||
private readonly Dictionary<string, Exporter> exporterCache = [];
|
||||
|
||||
public SpineViewerForm()
|
||||
{
|
||||
InitializeComponent();
|
||||
InitializeLogConfiguration();
|
||||
|
||||
// 执行一些初始化工作
|
||||
try
|
||||
{
|
||||
SFMLShader.Init();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.Error(ex.ToString());
|
||||
logger.Error("Failed to load fragment shader");
|
||||
MessagePopup.Warn("Fragment shader 加载失败,预乘Alpha通道属性失效");
|
||||
}
|
||||
|
||||
#if DEBUG
|
||||
toolStripMenuItem_Debug.Visible = true;
|
||||
#endif
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 初始化窗口日志器
|
||||
/// </summary>
|
||||
private void InitializeLogConfiguration()
|
||||
{
|
||||
// 窗口日志
|
||||
var rtbTarget = new NLog.Windows.Forms.RichTextBoxTarget
|
||||
{
|
||||
Name = "rtbTarget",
|
||||
TargetForm = this,
|
||||
TargetRichTextBox = rtbLog,
|
||||
AutoScroll = true,
|
||||
MaxLines = 3000,
|
||||
SupportLinks = true,
|
||||
Layout = "[${level:format=OneLetter}]${date:format=yyyy-MM-dd HH\\:mm\\:ss} - ${message}"
|
||||
};
|
||||
|
||||
rtbTarget.WordColoringRules.Add(new("[D]", "Gray", "Empty", FontStyle.Bold));
|
||||
rtbTarget.WordColoringRules.Add(new("[I]", "DimGray", "Empty", FontStyle.Bold));
|
||||
rtbTarget.WordColoringRules.Add(new("[W]", "DarkOrange", "Empty", FontStyle.Bold));
|
||||
rtbTarget.WordColoringRules.Add(new("[E]", "Red", "Empty", FontStyle.Bold));
|
||||
rtbTarget.WordColoringRules.Add(new("[F]", "DarkRed", "Empty", FontStyle.Bold));
|
||||
|
||||
LogManager.Configuration.AddTarget(rtbTarget);
|
||||
LogManager.Configuration.AddRule(LogLevel.Debug, LogLevel.Fatal, rtbTarget);
|
||||
LogManager.ReconfigExistingLoggers();
|
||||
}
|
||||
|
||||
private void MainForm_Load(object sender, EventArgs e)
|
||||
{
|
||||
spinePreviewPanel.StartRender();
|
||||
}
|
||||
|
||||
private void MainForm_FormClosing(object sender, FormClosingEventArgs e)
|
||||
{
|
||||
spinePreviewPanel.StopRender();
|
||||
}
|
||||
|
||||
private void toolStripMenuItem_Open_Click(object sender, EventArgs e)
|
||||
{
|
||||
spineListView.Add();
|
||||
}
|
||||
|
||||
private void toolStripMenuItem_BatchOpen_Click(object sender, EventArgs e)
|
||||
{
|
||||
spineListView.BatchAdd();
|
||||
}
|
||||
|
||||
#region private void toolStripMenuItem_ExportXXX_Click(object sender, EventArgs e)
|
||||
|
||||
private void toolStripMenuItem_ExportFrame_Click(object sender, EventArgs e)
|
||||
{
|
||||
if (spinePreviewPanel.IsUpdating && MessagePopup.Quest("画面仍在更新,建议手动暂停画面后导出固定的一帧,是否继续?") != DialogResult.OK)
|
||||
return;
|
||||
|
||||
var k = nameof(toolStripMenuItem_ExportFrame);
|
||||
if (!exporterCache.ContainsKey(k)) exporterCache[k] = new FrameExporter();
|
||||
|
||||
var exporter = exporterCache[k];
|
||||
using var view = spinePreviewPanel.GetView();
|
||||
exporter.Resolution = spinePreviewPanel.Resolution;
|
||||
exporter.PreviewerView = view;
|
||||
exporter.RenderSelectedOnly = spinePreviewPanel.RenderSelectedOnly;
|
||||
|
||||
var exportDialog = new Dialogs.ExportDialog(new FrameExporterProperty((FrameExporter)exporter));
|
||||
if (exportDialog.ShowDialog() != DialogResult.OK)
|
||||
return;
|
||||
|
||||
var progressDialog = new Dialogs.ProgressDialog();
|
||||
progressDialog.DoWork += Export_Work;
|
||||
progressDialog.RunWorkerAsync(exporter);
|
||||
progressDialog.ShowDialog();
|
||||
}
|
||||
|
||||
private void toolStripMenuItem_ExportFrameSequence_Click(object sender, EventArgs e)
|
||||
{
|
||||
var k = nameof(toolStripMenuItem_ExportFrameSequence);
|
||||
if (!exporterCache.ContainsKey(k)) exporterCache[k] = new FrameSequenceExporter();
|
||||
|
||||
var exporter = exporterCache[k];
|
||||
using var view = spinePreviewPanel.GetView();
|
||||
exporter.Resolution = spinePreviewPanel.Resolution;
|
||||
exporter.PreviewerView = view;
|
||||
exporter.RenderSelectedOnly = spinePreviewPanel.RenderSelectedOnly;
|
||||
|
||||
var exportDialog = new Dialogs.ExportDialog(new FrameSequenceExporterProperty((FrameSequenceExporter)exporter));
|
||||
if (exportDialog.ShowDialog() != DialogResult.OK)
|
||||
return;
|
||||
|
||||
var progressDialog = new Dialogs.ProgressDialog();
|
||||
progressDialog.DoWork += Export_Work;
|
||||
progressDialog.RunWorkerAsync(exporter);
|
||||
progressDialog.ShowDialog();
|
||||
}
|
||||
|
||||
private void toolStripMenuItem_ExportGif_Click(object sender, EventArgs e)
|
||||
{
|
||||
var k = nameof(toolStripMenuItem_ExportGif);
|
||||
if (!exporterCache.ContainsKey(k)) exporterCache[k] = new GifExporter();
|
||||
|
||||
var exporter = exporterCache[k];
|
||||
using var view = spinePreviewPanel.GetView();
|
||||
exporter.Resolution = spinePreviewPanel.Resolution;
|
||||
exporter.PreviewerView = view;
|
||||
exporter.RenderSelectedOnly = spinePreviewPanel.RenderSelectedOnly;
|
||||
|
||||
var exportDialog = new Dialogs.ExportDialog(new GifExporterProperty((GifExporter)exporter));
|
||||
if (exportDialog.ShowDialog() != DialogResult.OK)
|
||||
return;
|
||||
|
||||
var progressDialog = new Dialogs.ProgressDialog();
|
||||
progressDialog.DoWork += Export_Work;
|
||||
progressDialog.RunWorkerAsync(exporter);
|
||||
progressDialog.ShowDialog();
|
||||
}
|
||||
|
||||
private void toolStripMenuItem_ExportWebp_Click(object sender, EventArgs e)
|
||||
{
|
||||
var k = nameof(toolStripMenuItem_ExportWebp);
|
||||
if (!exporterCache.ContainsKey(k)) exporterCache[k] = new WebpExporter();
|
||||
|
||||
var exporter = exporterCache[k];
|
||||
using var view = spinePreviewPanel.GetView();
|
||||
exporter.Resolution = spinePreviewPanel.Resolution;
|
||||
exporter.PreviewerView = view;
|
||||
exporter.RenderSelectedOnly = spinePreviewPanel.RenderSelectedOnly;
|
||||
|
||||
var exportDialog = new Dialogs.ExportDialog(new WebpExporterProperty((WebpExporter)exporter));
|
||||
if (exportDialog.ShowDialog() != DialogResult.OK)
|
||||
return;
|
||||
|
||||
var progressDialog = new Dialogs.ProgressDialog();
|
||||
progressDialog.DoWork += Export_Work;
|
||||
progressDialog.RunWorkerAsync(exporter);
|
||||
progressDialog.ShowDialog();
|
||||
}
|
||||
|
||||
private void toolStripMenuItem_ExportAvif_Click(object sender, EventArgs e)
|
||||
{
|
||||
var k = nameof(toolStripMenuItem_ExportAvif);
|
||||
if (!exporterCache.ContainsKey(k)) exporterCache[k] = new AvifExporter();
|
||||
|
||||
var exporter = exporterCache[k];
|
||||
using var view = spinePreviewPanel.GetView();
|
||||
exporter.Resolution = spinePreviewPanel.Resolution;
|
||||
exporter.PreviewerView = view;
|
||||
exporter.RenderSelectedOnly = spinePreviewPanel.RenderSelectedOnly;
|
||||
|
||||
var exportDialog = new Dialogs.ExportDialog(new AvifExporterProperty((AvifExporter)exporter));
|
||||
if (exportDialog.ShowDialog() != DialogResult.OK)
|
||||
return;
|
||||
|
||||
var progressDialog = new Dialogs.ProgressDialog();
|
||||
progressDialog.DoWork += Export_Work;
|
||||
progressDialog.RunWorkerAsync(exporter);
|
||||
progressDialog.ShowDialog();
|
||||
}
|
||||
|
||||
private void toolStripMenuItem_ExportMp4_Click(object sender, EventArgs e)
|
||||
{
|
||||
var k = nameof(toolStripMenuItem_ExportMp4);
|
||||
if (!exporterCache.ContainsKey(k)) exporterCache[k] = new Mp4Exporter();
|
||||
|
||||
var exporter = exporterCache[k];
|
||||
using var view = spinePreviewPanel.GetView();
|
||||
exporter.Resolution = spinePreviewPanel.Resolution;
|
||||
exporter.PreviewerView = view;
|
||||
exporter.RenderSelectedOnly = spinePreviewPanel.RenderSelectedOnly;
|
||||
|
||||
var exportDialog = new Dialogs.ExportDialog(new Mp4ExporterProperty((Mp4Exporter)exporter));
|
||||
if (exportDialog.ShowDialog() != DialogResult.OK)
|
||||
return;
|
||||
|
||||
var progressDialog = new Dialogs.ProgressDialog();
|
||||
progressDialog.DoWork += Export_Work;
|
||||
progressDialog.RunWorkerAsync(exporter);
|
||||
progressDialog.ShowDialog();
|
||||
}
|
||||
|
||||
private void toolStripMenuItem_ExportWebm_Click(object sender, EventArgs e)
|
||||
{
|
||||
var k = nameof(toolStripMenuItem_ExportWebm);
|
||||
if (!exporterCache.ContainsKey(k)) exporterCache[k] = new WebmExporter();
|
||||
|
||||
var exporter = exporterCache[k];
|
||||
using var view = spinePreviewPanel.GetView();
|
||||
exporter.Resolution = spinePreviewPanel.Resolution;
|
||||
exporter.PreviewerView = view;
|
||||
exporter.RenderSelectedOnly = spinePreviewPanel.RenderSelectedOnly;
|
||||
|
||||
var exportDialog = new Dialogs.ExportDialog(new WebmExporterProperty((WebmExporter)exporter));
|
||||
if (exportDialog.ShowDialog() != DialogResult.OK)
|
||||
return;
|
||||
|
||||
var progressDialog = new Dialogs.ProgressDialog();
|
||||
progressDialog.DoWork += Export_Work;
|
||||
progressDialog.RunWorkerAsync(exporter);
|
||||
progressDialog.ShowDialog();
|
||||
}
|
||||
|
||||
private void toolStripMenuItem_ExportMkv_Click(object sender, EventArgs e)
|
||||
{
|
||||
var k = nameof(toolStripMenuItem_ExportMkv);
|
||||
if (!exporterCache.ContainsKey(k)) exporterCache[k] = new MkvExporter();
|
||||
|
||||
var exporter = exporterCache[k];
|
||||
using var view = spinePreviewPanel.GetView();
|
||||
exporter.Resolution = spinePreviewPanel.Resolution;
|
||||
exporter.PreviewerView = view;
|
||||
exporter.RenderSelectedOnly = spinePreviewPanel.RenderSelectedOnly;
|
||||
|
||||
var exportDialog = new Dialogs.ExportDialog(new MkvExporterProperty((MkvExporter)exporter));
|
||||
if (exportDialog.ShowDialog() != DialogResult.OK)
|
||||
return;
|
||||
|
||||
var progressDialog = new Dialogs.ProgressDialog();
|
||||
progressDialog.DoWork += Export_Work;
|
||||
progressDialog.RunWorkerAsync(exporter);
|
||||
progressDialog.ShowDialog();
|
||||
}
|
||||
|
||||
private void toolStripMenuItem_ExportMov_Click(object sender, EventArgs e)
|
||||
{
|
||||
var k = nameof(toolStripMenuItem_ExportMov);
|
||||
if (!exporterCache.ContainsKey(k)) exporterCache[k] = new MovExporter();
|
||||
|
||||
var exporter = exporterCache[k];
|
||||
using var view = spinePreviewPanel.GetView();
|
||||
exporter.Resolution = spinePreviewPanel.Resolution;
|
||||
exporter.PreviewerView = view;
|
||||
exporter.RenderSelectedOnly = spinePreviewPanel.RenderSelectedOnly;
|
||||
|
||||
var exportDialog = new Dialogs.ExportDialog(new MovExporterProperty((MovExporter)exporter));
|
||||
if (exportDialog.ShowDialog() != DialogResult.OK)
|
||||
return;
|
||||
|
||||
var progressDialog = new Dialogs.ProgressDialog();
|
||||
progressDialog.DoWork += Export_Work;
|
||||
progressDialog.RunWorkerAsync(exporter);
|
||||
progressDialog.ShowDialog();
|
||||
}
|
||||
|
||||
private void toolStripMenuItem_ExportCustom_Click(object sender, EventArgs e)
|
||||
{
|
||||
var k = nameof(toolStripMenuItem_ExportCustom);
|
||||
if (!exporterCache.ContainsKey(k)) exporterCache[k] = new CustomExporter();
|
||||
|
||||
var exporter = exporterCache[k];
|
||||
using var view = spinePreviewPanel.GetView();
|
||||
exporter.Resolution = spinePreviewPanel.Resolution;
|
||||
exporter.PreviewerView = view;
|
||||
exporter.RenderSelectedOnly = spinePreviewPanel.RenderSelectedOnly;
|
||||
|
||||
var exportDialog = new Dialogs.ExportDialog(new CustomExporterProperty((CustomExporter)exporter));
|
||||
if (exportDialog.ShowDialog() != DialogResult.OK)
|
||||
return;
|
||||
|
||||
var progressDialog = new Dialogs.ProgressDialog();
|
||||
progressDialog.DoWork += Export_Work;
|
||||
progressDialog.RunWorkerAsync(exporter);
|
||||
progressDialog.ShowDialog();
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
private void toolStripMenuItem_Exit_Click(object sender, EventArgs e)
|
||||
{
|
||||
Close();
|
||||
}
|
||||
|
||||
private void toolStripMenuItem_ConvertFileFormat_Click(object sender, EventArgs e)
|
||||
{
|
||||
var openDialog = new Dialogs.ConvertFileFormatDialog();
|
||||
if (openDialog.ShowDialog() != DialogResult.OK)
|
||||
return;
|
||||
|
||||
var progressDialog = new Dialogs.ProgressDialog();
|
||||
progressDialog.DoWork += ConvertFileFormat_Work;
|
||||
progressDialog.RunWorkerAsync(openDialog.Result);
|
||||
progressDialog.ShowDialog();
|
||||
}
|
||||
|
||||
private void toolStripMenuItem_ManageResource_Click(object sender, EventArgs e)
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
private void toolStripMenuItem_About_Click(object sender, EventArgs e)
|
||||
{
|
||||
using var dialog = new Dialogs.AboutDialog();
|
||||
dialog.ShowDialog();
|
||||
}
|
||||
|
||||
private void toolStripMenuItem_Diagnostics_Click(object sender, EventArgs e)
|
||||
{
|
||||
using var dialog = new Dialogs.DiagnosticsDialog();
|
||||
dialog.ShowDialog();
|
||||
}
|
||||
|
||||
private void splitContainer_SplitterMoved(object sender, SplitterEventArgs e) => ActiveControl = null;
|
||||
|
||||
private void splitContainer_MouseUp(object sender, MouseEventArgs e) => ActiveControl = null;
|
||||
|
||||
private void Export_Work(object? sender, DoWorkEventArgs e)
|
||||
{
|
||||
var worker = (BackgroundWorker)sender;
|
||||
var exporter = (Exporter)e.Argument;
|
||||
Invoke(() => TaskbarManager.SetProgressState(Handle, TBPFLAG.TBPF_INDETERMINATE));
|
||||
spinePreviewPanel.StopRender();
|
||||
lock (spineListView.Spines) { exporter.Export(spineListView.Spines.Where(sp => !sp.IsHidden).ToArray(), (BackgroundWorker)sender); }
|
||||
e.Cancel = worker.CancellationPending;
|
||||
spinePreviewPanel.StartRender();
|
||||
Invoke(() => TaskbarManager.SetProgressState(Handle, TBPFLAG.TBPF_NOPROGRESS));
|
||||
}
|
||||
|
||||
private void ConvertFileFormat_Work(object? sender, DoWorkEventArgs e)
|
||||
{
|
||||
var worker = sender as BackgroundWorker;
|
||||
var args = e.Argument as Dialogs.ConvertFileFormatDialogResult;
|
||||
var newSuffix = args.JsonTarget ? ".json" : ".skel";
|
||||
|
||||
int totalCount = args.SkelPaths.Length;
|
||||
int success = 0;
|
||||
int error = 0;
|
||||
|
||||
SkeletonConverter srcCvter = args.SourceVersion != SpineVersion.Auto ? SkeletonConverter.New(args.SourceVersion) : null;
|
||||
SkeletonConverter tgtCvter = SkeletonConverter.New(args.TargetVersion);
|
||||
|
||||
worker.ReportProgress(0, $"已处理 0/{totalCount}");
|
||||
for (int i = 0; i < totalCount; i++)
|
||||
{
|
||||
if (worker.CancellationPending)
|
||||
{
|
||||
e.Cancel = true;
|
||||
break;
|
||||
}
|
||||
|
||||
var skelPath = args.SkelPaths[i];
|
||||
var newPath = Path.ChangeExtension(skelPath, newSuffix);
|
||||
if (args.OutputDir is string outputDir) newPath = Path.Combine(outputDir, Path.GetFileName(newPath));
|
||||
|
||||
try
|
||||
{
|
||||
if (args.SourceVersion == SpineVersion.Auto)
|
||||
{
|
||||
try
|
||||
{
|
||||
srcCvter = SkeletonConverter.New(SpineUtils.GetVersion(skelPath));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
throw new InvalidDataException($"Auto version detection failed for {skelPath}, try to use a specific version", ex);
|
||||
}
|
||||
}
|
||||
var root = srcCvter.Read(skelPath);
|
||||
root = srcCvter.ToVersion(root, args.TargetVersion);
|
||||
if (args.JsonTarget) tgtCvter.WriteJson(root, newPath);
|
||||
else tgtCvter.WriteBinary(root, newPath);
|
||||
success++;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.Error(ex.ToString());
|
||||
logger.Error("Failed to convert {}", skelPath);
|
||||
error++;
|
||||
}
|
||||
|
||||
worker.ReportProgress((int)((i + 1) * 100.0) / totalCount, $"已处理 {i + 1}/{totalCount}");
|
||||
}
|
||||
|
||||
if (error > 0)
|
||||
{
|
||||
logger.Warn("Batch convert {} successfully, {} failed", success, error);
|
||||
}
|
||||
else
|
||||
{
|
||||
logger.Info("{} skel converted successfully", success);
|
||||
}
|
||||
}
|
||||
|
||||
private void toolStripMenuItem_DesktopProjection_Click(object sender, EventArgs e)
|
||||
{
|
||||
toolStripMenuItem_DesktopProjection.Checked = !toolStripMenuItem_DesktopProjection.Checked;
|
||||
spinePreviewPanel.EnableDesktopProjection = toolStripMenuItem_DesktopProjection.Checked;
|
||||
}
|
||||
|
||||
private void toolStripMenuItem_Debug_Click(object sender, EventArgs e)
|
||||
{
|
||||
#if DEBUG
|
||||
//var cvt = SkeletonConverter.New(SpineVersion.V38);
|
||||
//var root = cvt.ReadBinary(@"D:\ACGN\AzurLane_Export\AzurLane_Dynamic\docs\aerhangeersike\aerhangeersike_3\aerhangeersike_3 - 副本.skel");
|
||||
//cvt.WriteJson(root, @"D:\ACGN\AzurLane_Export\AzurLane_Dynamic\docs\aerhangeersike\aerhangeersike_3\aerhangeersike_3.json");
|
||||
|
||||
//root = cvt.ReadJson(@"D:\ACGN\AzurLane_Export\AzurLane_Dynamic\docs\aerhangeersike\aerhangeersike_3\aerhangeersike_3.json");
|
||||
//cvt.WriteBinary(root, @"D:\ACGN\AzurLane_Export\AzurLane_Dynamic\docs\aerhangeersike\aerhangeersike_3\aerhangeersike_3.skel");
|
||||
//var sp = SpineObject.New(SpineVersion.V38, @"D:\ACGN\AzurLane_Export\AzurLane_Dynamic\docs\aerhangeersike\aerhangeersike_3\aerhangeersike_3.skel");
|
||||
|
||||
//var cvt = SkeletonConverter.New(SpineVersion.V38);
|
||||
//var root = cvt.ReadJson(@"D:\ACGN\G\GirlsCreation\standing_spine\st4020069\st4020069.json");
|
||||
//cvt.WriteBinary(root, @"D:\ACGN\G\GirlsCreation\standing_spine\st4020069\st4020069.skel");
|
||||
//var sp = SpineObject.New(SpineVersion.V38, @"D:\ACGN\G\GirlsCreation\standing_spine\st4020069\st4020069.skel");
|
||||
//_Test();
|
||||
#endif
|
||||
}
|
||||
|
||||
//private void spinePreviewer_KeyDown(object sender, KeyEventArgs e)
|
||||
//{
|
||||
// switch (e.KeyCode)
|
||||
// {
|
||||
// case Keys.Space:
|
||||
// if ((ModifierKeys & Keys.Alt) != 0)
|
||||
// spinePreviewer.ClickStopButton();
|
||||
// else
|
||||
// spinePreviewer.ClickStartButton();
|
||||
// break;
|
||||
// case Keys.Right:
|
||||
// if ((ModifierKeys & Keys.Alt) != 0)
|
||||
// spinePreviewer.ClickForwardFastButton();
|
||||
// else
|
||||
// spinePreviewer.ClickForwardStepButton();
|
||||
// break;
|
||||
// case Keys.Left:
|
||||
// if ((ModifierKeys & Keys.Alt) != 0)
|
||||
// spinePreviewer.ClickRestartButton();
|
||||
// break;
|
||||
// }
|
||||
//}
|
||||
}
|
||||
}
|
||||
51
SpineViewer/Forms/WallpaperForm.Designer.cs
generated
Normal file
51
SpineViewer/Forms/WallpaperForm.Designer.cs
generated
Normal file
@@ -0,0 +1,51 @@
|
||||
namespace SpineViewer
|
||||
{
|
||||
partial class WallpaperForm
|
||||
{
|
||||
/// <summary>
|
||||
/// Required designer variable.
|
||||
/// </summary>
|
||||
private System.ComponentModel.IContainer components = null;
|
||||
|
||||
/// <summary>
|
||||
/// Clean up any resources being used.
|
||||
/// </summary>
|
||||
/// <param name="disposing">true if managed resources should be disposed; otherwise, false.</param>
|
||||
protected override void Dispose(bool disposing)
|
||||
{
|
||||
if (disposing && (components != null))
|
||||
{
|
||||
components.Dispose();
|
||||
}
|
||||
base.Dispose(disposing);
|
||||
}
|
||||
|
||||
#region Windows Form Designer generated code
|
||||
|
||||
/// <summary>
|
||||
/// Required method for Designer support - do not modify
|
||||
/// the contents of this method with the code editor.
|
||||
/// </summary>
|
||||
private void InitializeComponent()
|
||||
{
|
||||
SuspendLayout();
|
||||
//
|
||||
// WallpaperForm
|
||||
//
|
||||
AutoScaleMode = AutoScaleMode.None;
|
||||
ClientSize = new Size(512, 512);
|
||||
ControlBox = false;
|
||||
FormBorderStyle = FormBorderStyle.None;
|
||||
MaximizeBox = false;
|
||||
MinimizeBox = false;
|
||||
Name = "WallpaperForm";
|
||||
ShowIcon = false;
|
||||
ShowInTaskbar = false;
|
||||
StartPosition = FormStartPosition.Manual;
|
||||
WindowState = FormWindowState.Minimized;
|
||||
ResumeLayout(false);
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
73
SpineViewer/Forms/WallpaperForm.cs
Normal file
73
SpineViewer/Forms/WallpaperForm.cs
Normal file
@@ -0,0 +1,73 @@
|
||||
using SpineViewer.Natives;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using System.ComponentModel.Design;
|
||||
using System.Data;
|
||||
using System.Drawing;
|
||||
using System.Linq;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows.Forms;
|
||||
|
||||
namespace SpineViewer
|
||||
{
|
||||
[ToolboxItem(true)]
|
||||
[Designer(typeof(ComponentDesigner), typeof(IDesigner))]
|
||||
[DesignTimeVisible(true)]
|
||||
public partial class WallpaperForm: Form
|
||||
{
|
||||
public WallpaperForm()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
|
||||
protected override CreateParams CreateParams
|
||||
{
|
||||
get
|
||||
{
|
||||
var cp = base.CreateParams;
|
||||
cp.ExStyle |= Win32.WS_EX_TOOLWINDOW | Win32.WS_EX_LAYERED;
|
||||
return cp;
|
||||
}
|
||||
}
|
||||
|
||||
protected override void OnLoad(EventArgs e)
|
||||
{
|
||||
base.OnLoad(e);
|
||||
|
||||
// 设置成嵌入桌面
|
||||
var progman = Win32.FindWindow("Progman", null);
|
||||
if (progman != IntPtr.Zero)
|
||||
{
|
||||
// 确保 WorkerW 被创建
|
||||
Win32.SendMessageTimeout(progman, Win32.WM_SPAWN_WORKER, IntPtr.Zero, IntPtr.Zero, Win32.SMTO_NORMAL, 1000, out _);
|
||||
var workerW = Win32.GetWorkerW();
|
||||
if (workerW != IntPtr.Zero)
|
||||
{
|
||||
Win32.SetLayeredWindowAttributes(Handle, 0, 255, Win32.LWA_ALPHA);
|
||||
Win32.SetParent(Handle, workerW); // 嵌入之前必须保证有 WS_EX_LAYERED 标志
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
|
||||
[Browsable(false)]
|
||||
public byte LayeredWindowAlpha
|
||||
{
|
||||
get
|
||||
{
|
||||
uint crKey = 0;
|
||||
byte bAlpha = 255;
|
||||
uint dwFlags = Win32.LWA_ALPHA;
|
||||
Win32.GetLayeredWindowAttributes(Handle, ref crKey, ref bAlpha, ref dwFlags);
|
||||
return bAlpha;
|
||||
}
|
||||
set
|
||||
{
|
||||
Win32.SetLayeredWindowAttributes(Handle, 0, value, Win32.LWA_ALPHA);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
120
SpineViewer/Forms/WallpaperForm.resx
Normal file
120
SpineViewer/Forms/WallpaperForm.resx
Normal file
@@ -0,0 +1,120 @@
|
||||
<?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>
|
||||
</root>
|
||||
@@ -1,179 +0,0 @@
|
||||
using NLog;
|
||||
using SpineViewer.Spine;
|
||||
using System.ComponentModel;
|
||||
using System.Diagnostics;
|
||||
|
||||
namespace SpineViewer
|
||||
{
|
||||
public partial class MainForm : Form
|
||||
{
|
||||
public MainForm()
|
||||
{
|
||||
InitializeComponent();
|
||||
InitializeLogConfiguration();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// <20><>ʼ<EFBFBD><CABC><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>־<EFBFBD><D6BE>
|
||||
/// </summary>
|
||||
private void InitializeLogConfiguration()
|
||||
{
|
||||
// <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD>־
|
||||
var rtbTarget = new NLog.Windows.Forms.RichTextBoxTarget
|
||||
{
|
||||
Name = "rtbTarget",
|
||||
TargetForm = this,
|
||||
TargetRichTextBox = rtbLog,
|
||||
AutoScroll = true,
|
||||
MaxLines = 3000,
|
||||
SupportLinks = true,
|
||||
Layout = "[${level:format=OneLetter}]${date:format=yyyy-MM-dd HH\\:mm\\:ss} - ${message}"
|
||||
};
|
||||
|
||||
rtbTarget.WordColoringRules.Add(new("[D]", "Gray", "Empty", FontStyle.Bold));
|
||||
rtbTarget.WordColoringRules.Add(new("[I]", "DimGray", "Empty", FontStyle.Bold));
|
||||
rtbTarget.WordColoringRules.Add(new("[W]", "DarkOrange", "Empty", FontStyle.Bold));
|
||||
rtbTarget.WordColoringRules.Add(new("[E]", "Red", "Empty", FontStyle.Bold));
|
||||
rtbTarget.WordColoringRules.Add(new("[F]", "DarkRed", "Empty", FontStyle.Bold));
|
||||
|
||||
LogManager.Configuration.AddTarget(rtbTarget);
|
||||
LogManager.Configuration.AddRule(LogLevel.Debug, LogLevel.Fatal, rtbTarget);
|
||||
LogManager.ReconfigExistingLoggers();
|
||||
}
|
||||
|
||||
private void 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();
|
||||
}
|
||||
|
||||
private void MainForm_FormClosing(object sender, FormClosingEventArgs e)
|
||||
{
|
||||
spinePreviewer.StopPreview();
|
||||
}
|
||||
|
||||
private void toolStripMenuItem_Open_Click(object sender, EventArgs e)
|
||||
{
|
||||
spineListView.Add();
|
||||
}
|
||||
|
||||
private void toolStripMenuItem_BatchOpen_Click(object sender, EventArgs e)
|
||||
{
|
||||
spineListView.BatchAdd();
|
||||
}
|
||||
|
||||
private void toolStripMenuItem_Export_Click(object sender, EventArgs e)
|
||||
{
|
||||
lock (spineListView.Spines)
|
||||
{
|
||||
if (spineListView.Spines.Count <= 0)
|
||||
{
|
||||
MessageBox.Show("<22><><EFBFBD><EFBFBD><EFBFBD>ٴ<EFBFBD><D9B4><EFBFBD>һ<EFBFBD><D2BB><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ļ<EFBFBD>", "<22><>ʾ<EFBFBD><CABE>Ϣ", MessageBoxButtons.OK, MessageBoxIcon.Information);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
var exportDialog = new Dialogs.ExportPngDialog();
|
||||
if (exportDialog.ShowDialog() != DialogResult.OK)
|
||||
return;
|
||||
|
||||
var progressDialog = new Dialogs.ProgressDialog();
|
||||
progressDialog.DoWork += ExportPng_Work;
|
||||
progressDialog.RunWorkerAsync(exportDialog);
|
||||
progressDialog.ShowDialog();
|
||||
}
|
||||
|
||||
private void toolStripMenuItem_Exit_Click(object sender, EventArgs e)
|
||||
{
|
||||
Close();
|
||||
}
|
||||
|
||||
private void toolStripMenuItem_ResetAnimation_Click(object sender, EventArgs e)
|
||||
{
|
||||
lock (spineListView.Spines)
|
||||
{
|
||||
foreach (var spine in spineListView.Spines)
|
||||
spine.CurrentAnimation = spine.CurrentAnimation;
|
||||
}
|
||||
}
|
||||
|
||||
private void toolStripMenuItem_About_Click(object sender, EventArgs e)
|
||||
{
|
||||
(new Dialogs.AboutDialog()).ShowDialog();
|
||||
}
|
||||
|
||||
private void toolStripMenuItem_Diagnostics_Click(object sender, EventArgs e)
|
||||
{
|
||||
(new Dialogs.DiagnosticsDialog()).ShowDialog();
|
||||
}
|
||||
|
||||
private void splitContainer_SplitterMoved(object sender, SplitterEventArgs e) { ActiveControl = null; }
|
||||
|
||||
private void splitContainer_MouseUp(object sender, MouseEventArgs e) { ActiveControl = null; }
|
||||
|
||||
private void propertyGrid_PropertyValueChanged(object sender, PropertyValueChangedEventArgs e) { (sender as PropertyGrid)?.Refresh(); }
|
||||
|
||||
private void spinePreviewer_MouseUp(object sender, MouseEventArgs e) { propertyGrid_Spine.Refresh(); }
|
||||
}
|
||||
}
|
||||
65
SpineViewer/Natives/TaskbarManager.cs
Normal file
65
SpineViewer/Natives/TaskbarManager.cs
Normal file
@@ -0,0 +1,65 @@
|
||||
using System;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Windows.Forms;
|
||||
|
||||
namespace SpineViewer.Natives
|
||||
{
|
||||
internal enum TBPFLAG
|
||||
{
|
||||
TBPF_NOPROGRESS = 0,
|
||||
TBPF_INDETERMINATE = 0x1,
|
||||
TBPF_NORMAL = 0x2,
|
||||
TBPF_ERROR = 0x4,
|
||||
TBPF_PAUSED = 0x8
|
||||
}
|
||||
|
||||
[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
|
||||
[ComImport, Guid("ea1afb91-9e28-4b86-90e9-9e9f8a5eefaf")]
|
||||
internal interface ITaskbarList3
|
||||
{
|
||||
// ITaskbarList
|
||||
void HrInit();
|
||||
void AddTab(nint hwnd);
|
||||
void DeleteTab(nint hwnd);
|
||||
void ActivateTab(nint hwnd);
|
||||
void SetActiveAlt(nint hwnd);
|
||||
// ITaskbarList2
|
||||
void MarkFullscreenWindow(nint hwnd, [MarshalAs(UnmanagedType.Bool)] bool fFullscreen);
|
||||
// ITaskbarList3
|
||||
void SetProgressValue(nint hwnd, ulong ullCompleted, ulong ullTotal);
|
||||
void SetProgressState(nint hwnd, TBPFLAG tbpFlags);
|
||||
//void RegisterTab(IntPtr hwndTab, IntPtr hwndMDI);
|
||||
//void UnregisterTab(IntPtr hwndTab);
|
||||
//void SetTabOrder(IntPtr hwndTab, IntPtr hwndInsertBefore);
|
||||
//void SetTabActive(IntPtr hwndTab, IntPtr hwndMDI, uint dwReserved);
|
||||
//void ThumbBarAddButtons(IntPtr hwnd, uint cButtons, THUMBBUTTON[] pButton);
|
||||
//void ThumbBarUpdateButtons(IntPtr hwnd, uint cButtons, THUMBBUTTON[] pButton);
|
||||
//void ThumbBarSetImageList(IntPtr hwnd, IntPtr himl);
|
||||
//void SetOverlayIcon(IntPtr hwnd, IntPtr hIcon, string pszDescription);
|
||||
//void SetThumbnailTooltip(IntPtr hwnd, string pszTip);
|
||||
//void SetThumbnailClip(IntPtr hwnd, ref RECT prcClip);
|
||||
}
|
||||
|
||||
[ComImport, Guid("56FDF344-FD6D-11d0-958A-006097C9A090")]
|
||||
internal class TaskbarList { }
|
||||
|
||||
internal static class TaskbarManager
|
||||
{
|
||||
private static readonly ITaskbarList3 taskbarList = (ITaskbarList3)new TaskbarList();
|
||||
|
||||
static TaskbarManager()
|
||||
{
|
||||
taskbarList.HrInit();
|
||||
}
|
||||
|
||||
public static void SetProgressState(nint windowHandle, TBPFLAG state)
|
||||
{
|
||||
taskbarList.SetProgressState(windowHandle, state);
|
||||
}
|
||||
|
||||
public static void SetProgressValue(nint windowHandle, ulong completed, ulong total)
|
||||
{
|
||||
taskbarList.SetProgressValue(windowHandle, completed, total);
|
||||
}
|
||||
}
|
||||
}
|
||||
180
SpineViewer/Natives/Win32.cs
Normal file
180
SpineViewer/Natives/Win32.cs
Normal file
@@ -0,0 +1,180 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Drawing;
|
||||
using System.Linq;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace SpineViewer.Natives
|
||||
{
|
||||
/// <summary>
|
||||
/// Win32 Sdk 包装类
|
||||
/// </summary>
|
||||
public static class Win32
|
||||
{
|
||||
public const int GWL_STYLE = -16;
|
||||
public const int WS_SIZEBOX = 0x40000;
|
||||
public const int WS_BORDER = 0x800000;
|
||||
public const int WS_VISIBLE = 0x10000000;
|
||||
public const int WS_CHILD = 0x40000000;
|
||||
public const int WS_POPUP = unchecked((int)0x80000000);
|
||||
|
||||
public const int GWL_EXSTYLE = -20;
|
||||
public const int WS_EX_TOPMOST = 0x8;
|
||||
public const int WS_EX_TRANSPARENT = 0x20;
|
||||
public const int WS_EX_TOOLWINDOW = 0x80;
|
||||
public const int WS_EX_WINDOWEDGE = 0x100;
|
||||
public const int WS_EX_CLIENTEDGE = 0x200;
|
||||
public const int WS_EX_APPWINDOW = 0x40000;
|
||||
public const int WS_EX_LAYERED = 0x80000;
|
||||
public const int WS_EX_OVERLAPPEDWINDOW = WS_EX_WINDOWEDGE | WS_EX_CLIENTEDGE;
|
||||
public const int WS_EX_NOACTIVATE = 0x8000000;
|
||||
|
||||
public const uint LWA_COLORKEY = 0x1;
|
||||
public const uint LWA_ALPHA = 0x2;
|
||||
|
||||
public const byte AC_SRC_OVER = 0x00;
|
||||
public const byte AC_SRC_ALPHA = 0x01;
|
||||
|
||||
public const int ULW_COLORKEY = 0x00000001;
|
||||
public const int ULW_ALPHA = 0x00000002;
|
||||
public const int ULW_OPAQUE = 0x00000004;
|
||||
|
||||
public const nint HWND_TOPMOST = -1;
|
||||
|
||||
public const uint SWP_NOSIZE = 0x0001;
|
||||
public const uint SWP_NOMOVE = 0x0002;
|
||||
public const uint SWP_NOZORDER = 0x0004;
|
||||
public const uint SWP_FRAMECHANGED = 0x0020;
|
||||
|
||||
public const int WM_SPAWN_WORKER = 0x052C; // 一个未公开的神秘消息
|
||||
|
||||
public const uint SMTO_NORMAL = 0x0000;
|
||||
public const uint SMTO_BLOCK = 0x0001;
|
||||
public const uint SMTO_ABORTIFHUNG = 0x0002;
|
||||
public const uint SMTO_NOTIMEOUTIFNOTHUNG = 0x0008;
|
||||
|
||||
public const uint GA_PARENT = 1;
|
||||
public const uint GW_OWNER = 4;
|
||||
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
public struct POINT
|
||||
{
|
||||
public int x;
|
||||
public int y;
|
||||
}
|
||||
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
public struct SIZE
|
||||
{
|
||||
public int cx;
|
||||
public int cy;
|
||||
}
|
||||
|
||||
[StructLayout(LayoutKind.Sequential, Pack = 1)]
|
||||
public struct BLENDFUNCTION
|
||||
{
|
||||
public byte BlendOp;
|
||||
public byte BlendFlags;
|
||||
public byte SourceConstantAlpha;
|
||||
public byte AlphaFormat;
|
||||
}
|
||||
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
private struct LASTINPUTINFO
|
||||
{
|
||||
public uint cbSize;
|
||||
public uint dwTime;
|
||||
}
|
||||
|
||||
[DllImport("user32.dll", SetLastError = true)]
|
||||
public static extern nint GetDC(nint hWnd);
|
||||
|
||||
[DllImport("user32.dll", SetLastError = true)]
|
||||
public static extern int ReleaseDC(nint hWnd, nint hDC);
|
||||
|
||||
[DllImport("user32.dll", SetLastError = true)]
|
||||
public static extern int SetWindowLong(nint hWnd, int nIndex, int dwNewLong);
|
||||
|
||||
[DllImport("user32.dll", SetLastError = true)]
|
||||
public static extern int GetWindowLong(nint hWnd, int nIndex);
|
||||
|
||||
[DllImport("user32.dll", SetLastError = true)]
|
||||
public static extern bool GetLayeredWindowAttributes(nint hWnd, ref uint crKey, ref byte bAlpha, ref uint dwFlags);
|
||||
|
||||
[DllImport("user32.dll", SetLastError = true)]
|
||||
public static extern bool SetLayeredWindowAttributes(nint hWnd, uint pcrKey, byte pbAlpha, uint pdwFlags);
|
||||
|
||||
[DllImport("user32.dll", SetLastError = true)]
|
||||
public static extern bool UpdateLayeredWindow(nint hWnd, nint hdcDst, nint pptDst, ref SIZE psize, nint hdcSrc, ref POINT pptSrc, int crKey, ref BLENDFUNCTION pblend, int dwFlags);
|
||||
|
||||
[DllImport("user32.dll", SetLastError = true)]
|
||||
public static extern bool SetWindowPos(nint hWnd, nint hWndInsertAfter, int X, int Y, int cx, int cy, uint uFlags);
|
||||
|
||||
[DllImport("user32.dll", SetLastError = true)]
|
||||
public static extern uint GetDoubleClickTime();
|
||||
|
||||
[DllImport("user32.dll", SetLastError = true)]
|
||||
private static extern bool GetLastInputInfo(ref LASTINPUTINFO plii);
|
||||
|
||||
[DllImport("user32.dll", SetLastError = true)]
|
||||
public static extern nint FindWindow(string lpClassName, string lpWindowName);
|
||||
|
||||
[DllImport("user32.dll", SetLastError = true)]
|
||||
public static extern nint SendMessageTimeout(nint hWnd, uint Msg, nint wParam, nint lParam, uint fuFlags, uint uTimeout, out nint lpdwResult);
|
||||
|
||||
[DllImport("user32.dll", SetLastError = true)]
|
||||
public static extern nint FindWindowEx(nint parentHandle, nint childAfter, string className, string windowTitle);
|
||||
|
||||
[DllImport("user32.dll", SetLastError = true)]
|
||||
public static extern nint SetParent(nint hWndChild, nint hWndNewParent);
|
||||
|
||||
[DllImport("user32.dll", SetLastError = true)]
|
||||
public static extern nint GetParent(nint hWnd);
|
||||
|
||||
[DllImport("user32.dll", SetLastError = true)]
|
||||
public static extern nint GetAncestor(nint hWnd, uint gaFlags);
|
||||
|
||||
[DllImport("user32.dll", SetLastError = true)]
|
||||
public static extern nint GetWindow(nint hWnd, uint uCmd);
|
||||
|
||||
[DllImport("gdi32.dll", SetLastError = true)]
|
||||
public static extern nint CreateCompatibleDC(nint hdc);
|
||||
|
||||
[DllImport("gdi32.dll", SetLastError = true)]
|
||||
public static extern bool DeleteDC(nint hdc);
|
||||
|
||||
[DllImport("gdi32.dll", SetLastError = true)]
|
||||
public static extern nint SelectObject(nint hdc, nint hgdiobj);
|
||||
|
||||
[DllImport("gdi32.dll", SetLastError = true)]
|
||||
public static extern bool DeleteObject(nint hObject);
|
||||
|
||||
public static TimeSpan GetLastInputElapsedTime()
|
||||
{
|
||||
LASTINPUTINFO lastInputInfo = new();
|
||||
lastInputInfo.cbSize = (uint)Marshal.SizeOf(lastInputInfo);
|
||||
|
||||
uint idleTimeMillis = 1000;
|
||||
if (GetLastInputInfo(ref lastInputInfo))
|
||||
{
|
||||
uint tickCount = (uint)Environment.TickCount;
|
||||
uint lastInputTick = lastInputInfo.dwTime;
|
||||
idleTimeMillis = tickCount - lastInputTick;
|
||||
}
|
||||
return TimeSpan.FromMilliseconds(idleTimeMillis);
|
||||
}
|
||||
|
||||
public static nint GetWorkerW()
|
||||
{
|
||||
var progman = FindWindow("Progman", null);
|
||||
if (progman == nint.Zero)
|
||||
return nint.Zero;
|
||||
nint hWnd = FindWindowEx(progman, 0, "WorkerW", null);
|
||||
Debug.WriteLine($"HWND(Progman.WorkerW): {hWnd:x8}");
|
||||
return hWnd;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,21 +1,48 @@
|
||||
using NLog;
|
||||
using NLog;
|
||||
using SpineViewer.Utils;
|
||||
using System.Diagnostics;
|
||||
using System.Reflection;
|
||||
|
||||
namespace SpineViewer
|
||||
{
|
||||
internal static class Program
|
||||
{
|
||||
public static readonly Process Process = Process.GetCurrentProcess();
|
||||
public static readonly Logger Logger = LogManager.GetCurrentClassLogger();
|
||||
///// <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;
|
||||
|
||||
public static string Version => Assembly.GetExecutingAssembly().GetCustomAttribute<AssemblyInformationalVersionAttribute>()?.InformationalVersion;
|
||||
|
||||
/// <summary>
|
||||
/// The main entry point for the application.
|
||||
/// 程序日志器
|
||||
/// </summary>
|
||||
private static readonly Logger logger = LogManager.GetCurrentClassLogger();
|
||||
|
||||
/// <summary>
|
||||
/// 应用入口点
|
||||
/// </summary>
|
||||
[STAThread]
|
||||
static void Main()
|
||||
{
|
||||
// 此处先初始化全局配置再触发静态字段 Logger 引用构造, 才能将配置应用到新的日志器上
|
||||
InitializeLogConfiguration();
|
||||
Logger.Info("Program Started");
|
||||
logger.Info("Program Started");
|
||||
|
||||
// To customize application configuration such as set high DPI settings or default font,
|
||||
// see https://aka.ms/applicationconfiguration.
|
||||
@@ -23,23 +50,23 @@ namespace SpineViewer
|
||||
|
||||
try
|
||||
{
|
||||
Application.Run(new MainForm());
|
||||
Application.Run(new SpineViewerForm() { Text = $"SpineViewer - v{Version}"});
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.Fatal(ex.ToString());
|
||||
MessageBox.Show(ex.ToString(), "<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ѱ<EFBFBD><EFBFBD><EFBFBD>", MessageBoxButtons.OK, MessageBoxIcon.Stop);
|
||||
logger.Fatal(ex.ToString());
|
||||
MessagePopup.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,
|
||||
@@ -55,6 +82,5 @@ namespace SpineViewer
|
||||
config.AddRule(LogLevel.Debug, LogLevel.Fatal, fileTarget);
|
||||
LogManager.Configuration = config;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -1,64 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace SpineViewer.Spine
|
||||
{
|
||||
/// <summary>
|
||||
/// SFML 混合模式
|
||||
/// </summary>
|
||||
public static class BlendMode
|
||||
{
|
||||
/// <summary>
|
||||
/// Alpha Blend
|
||||
/// <code>
|
||||
/// res.c = src.c * src.a + dst.c * (1 - src.a)
|
||||
/// res.a = src.a * 1 + dst.a * (1 - src.a)
|
||||
/// </code>
|
||||
/// </summary>
|
||||
public static SFML.Graphics.BlendMode Normal = SFML.Graphics.BlendMode.Alpha;
|
||||
|
||||
/// <summary>
|
||||
/// Additive Blend
|
||||
/// <code>
|
||||
/// res.c = src.c * src.a + dst.c * 1
|
||||
/// res.a = src.a * 1 + dst.a * 1
|
||||
/// </code>
|
||||
/// </summary>
|
||||
public static SFML.Graphics.BlendMode Additive = SFML.Graphics.BlendMode.Add;
|
||||
|
||||
/// <summary>
|
||||
/// Multiply Blend (PremultipliedAlpha Only)
|
||||
/// <code>
|
||||
/// res.c = src.c * dst.c + dst.c * (1 - src.a)
|
||||
/// res.a = src.a * 1 + dst.a * (1 - src.a)
|
||||
/// </code>
|
||||
/// </summary>
|
||||
public static SFML.Graphics.BlendMode Multiply = new(
|
||||
SFML.Graphics.BlendMode.Factor.DstColor,
|
||||
SFML.Graphics.BlendMode.Factor.OneMinusSrcAlpha,
|
||||
SFML.Graphics.BlendMode.Equation.Add,
|
||||
SFML.Graphics.BlendMode.Factor.One,
|
||||
SFML.Graphics.BlendMode.Factor.OneMinusSrcAlpha,
|
||||
SFML.Graphics.BlendMode.Equation.Add
|
||||
);
|
||||
|
||||
/// <summary>
|
||||
/// Screen Blend (PremultipliedAlpha Only)
|
||||
/// <code>
|
||||
/// res.c = src.c * 1 + dst.c * (1 - src.c) = 1 - [(1 - src.c)(1 - dst.c)]
|
||||
/// res.a = src.a * 1 + dst.a * (1 - src.a)
|
||||
/// </code>
|
||||
/// </summary>
|
||||
public static SFML.Graphics.BlendMode Screen = new(
|
||||
SFML.Graphics.BlendMode.Factor.One,
|
||||
SFML.Graphics.BlendMode.Factor.OneMinusSrcColor,
|
||||
SFML.Graphics.BlendMode.Equation.Add,
|
||||
SFML.Graphics.BlendMode.Factor.One,
|
||||
SFML.Graphics.BlendMode.Factor.OneMinusSrcAlpha,
|
||||
SFML.Graphics.BlendMode.Equation.Add
|
||||
);
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,330 +0,0 @@
|
||||
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 SpineRuntime36;
|
||||
|
||||
namespace SpineViewer.Spine.Implementations
|
||||
{
|
||||
[SpineImplementation(Version.V36)]
|
||||
internal class Spine36 : Spine
|
||||
{
|
||||
private class TextureLoader : SpineRuntime36.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;
|
||||
|
||||
private SkeletonClipping clipping = new();
|
||||
|
||||
public Spine36(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 ArgumentException($"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 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.TrackTime = savedTrack0.TrackTime;
|
||||
var savedEntry = savedTrack0.Next;
|
||||
while (savedEntry is not null)
|
||||
{
|
||||
entry = animationState.AddAnimation(0, savedEntry.Animation.Name, true, 0);
|
||||
entry.TrackTime = 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 ?? DefaultAnimationName;
|
||||
set { if (animationNames.Contains(value)) { animationState.SetAnimation(0, value, true); Update(0); } }
|
||||
}
|
||||
|
||||
public override RectangleF Bounds
|
||||
{
|
||||
get
|
||||
{
|
||||
float[] _ = [];
|
||||
skeleton.GetBounds(out var x, out var y, out var w, out var h, ref _);
|
||||
return new RectangleF(x, y, w, h);
|
||||
}
|
||||
}
|
||||
|
||||
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(SpineRuntime36.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,
|
||||
_ => 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, 0);
|
||||
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.WorldVerticesLength > worldVertices.Length)
|
||||
worldVertices = worldVerticesBuffer = new float[meshAttachment.WorldVerticesLength * 2];
|
||||
meshAttachment.ComputeWorldVertices(slot, worldVertices);
|
||||
worldVerticesCount = meshAttachment.WorldVerticesLength / 2;
|
||||
worldTriangleIndices = meshAttachment.Triangles;
|
||||
worldTriangleIndicesLength = meshAttachment.Triangles.Length;
|
||||
uvs = meshAttachment.UVs;
|
||||
tintR *= meshAttachment.R;
|
||||
tintG *= meshAttachment.G;
|
||||
tintB *= meshAttachment.B;
|
||||
tintA *= meshAttachment.A;
|
||||
}
|
||||
else if (attachment is ClippingAttachment clippingAttachment)
|
||||
{
|
||||
clipping.ClipStart(slot, clippingAttachment);
|
||||
continue;
|
||||
}
|
||||
else
|
||||
{
|
||||
clipping.ClipEnd(slot);
|
||||
continue;
|
||||
}
|
||||
|
||||
SFML.Graphics.BlendMode blendMode = GetSFMLBlendMode(slot.Data.BlendMode);
|
||||
|
||||
states.Texture ??= texture;
|
||||
if (states.BlendMode != blendMode || states.Texture != texture)
|
||||
{
|
||||
if (vertexArray.VertexCount > 0)
|
||||
{
|
||||
if (UsePremultipliedAlpha && (states.BlendMode == BlendMode.Normal || states.BlendMode == BlendMode.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 == BlendMode.Normal || states.BlendMode == BlendMode.Additive))
|
||||
states.Shader = FragmentShader;
|
||||
else
|
||||
states.Shader = null;
|
||||
target.Draw(vertexArray, states);
|
||||
clipping.ClipEnd();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,338 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using SpineRuntime37;
|
||||
|
||||
namespace SpineViewer.Spine.Implementations
|
||||
{
|
||||
[SpineImplementation(Version.V37)]
|
||||
internal class Spine37 : Spine
|
||||
{
|
||||
private class TextureLoader : SpineRuntime37.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;
|
||||
|
||||
private SkeletonClipping clipping = new();
|
||||
|
||||
public Spine37(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 ArgumentException($"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 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.TrackTime = savedTrack0.TrackTime;
|
||||
var savedEntry = savedTrack0.Next;
|
||||
while (savedEntry is not null)
|
||||
{
|
||||
entry = animationState.AddAnimation(0, savedEntry.Animation.Name, true, 0);
|
||||
entry.TrackTime = 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.ScaleX < 0;
|
||||
set
|
||||
{
|
||||
if (skeleton.ScaleX > 0 && value || skeleton.ScaleX < 0 && !value)
|
||||
skeleton.ScaleX *= -1;
|
||||
}
|
||||
}
|
||||
|
||||
public override bool FlipY
|
||||
{
|
||||
get => skeleton.ScaleY < 0;
|
||||
set
|
||||
{
|
||||
if (skeleton.ScaleY > 0 && value || skeleton.ScaleY < 0 && !value)
|
||||
skeleton.ScaleY *= -1;
|
||||
}
|
||||
}
|
||||
|
||||
public override string CurrentAnimation
|
||||
{
|
||||
get => animationState.GetCurrent(0)?.Animation.Name ?? DefaultAnimationName;
|
||||
set { if (animationNames.Contains(value)) { animationState.SetAnimation(0, value, true); Update(0); } }
|
||||
}
|
||||
|
||||
public override RectangleF Bounds
|
||||
{
|
||||
get
|
||||
{
|
||||
float[] _ = [];
|
||||
skeleton.GetBounds(out var x, out var y, out var w, out var h, ref _);
|
||||
return new RectangleF(x, y, w, h);
|
||||
}
|
||||
}
|
||||
|
||||
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(SpineRuntime37.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,
|
||||
_ => 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, 0);
|
||||
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.WorldVerticesLength > worldVertices.Length)
|
||||
worldVertices = worldVerticesBuffer = new float[meshAttachment.WorldVerticesLength * 2];
|
||||
meshAttachment.ComputeWorldVertices(slot, worldVertices);
|
||||
worldVerticesCount = meshAttachment.WorldVerticesLength / 2;
|
||||
worldTriangleIndices = meshAttachment.Triangles;
|
||||
worldTriangleIndicesLength = meshAttachment.Triangles.Length;
|
||||
uvs = meshAttachment.UVs;
|
||||
tintR *= meshAttachment.R;
|
||||
tintG *= meshAttachment.G;
|
||||
tintB *= meshAttachment.B;
|
||||
tintA *= meshAttachment.A;
|
||||
}
|
||||
else if (attachment is ClippingAttachment clippingAttachment)
|
||||
{
|
||||
clipping.ClipStart(slot, clippingAttachment);
|
||||
continue;
|
||||
}
|
||||
else
|
||||
{
|
||||
clipping.ClipEnd(slot);
|
||||
continue;
|
||||
}
|
||||
|
||||
SFML.Graphics.BlendMode blendMode = GetSFMLBlendMode(slot.Data.BlendMode);
|
||||
|
||||
states.Texture ??= texture;
|
||||
if (states.BlendMode != blendMode || states.Texture != texture)
|
||||
{
|
||||
if (vertexArray.VertexCount > 0)
|
||||
{
|
||||
// XXX: 实测不用设置 sampler2D 的值也正确
|
||||
if (UsePremultipliedAlpha && (states.BlendMode == BlendMode.Normal || states.BlendMode == BlendMode.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 == BlendMode.Normal || states.BlendMode == BlendMode.Additive))
|
||||
states.Shader = FragmentShader;
|
||||
else
|
||||
states.Shader = null;
|
||||
target.Draw(vertexArray, states);
|
||||
clipping.ClipEnd();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,342 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using SpineRuntime38;
|
||||
using SpineRuntime38.Attachments;
|
||||
using SpineViewer.Spine;
|
||||
|
||||
namespace SpineViewer.Spine.Implementations
|
||||
{
|
||||
[SpineImplementation(Version.V38)]
|
||||
internal class Spine38 : Spine
|
||||
{
|
||||
private class TextureLoader : SpineRuntime38.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;
|
||||
|
||||
private SkeletonClipping clipping = new();
|
||||
|
||||
public Spine38(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 ArgumentException($"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 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.TrackTime = savedTrack0.TrackTime;
|
||||
var savedEntry = savedTrack0.Next;
|
||||
while (savedEntry is not null)
|
||||
{
|
||||
entry = animationState.AddAnimation(0, savedEntry.Animation.Name, true, 0);
|
||||
entry.TrackTime = 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.ScaleX < 0;
|
||||
set
|
||||
{
|
||||
if (skeleton.ScaleX > 0 && value || skeleton.ScaleX < 0 && !value)
|
||||
skeleton.ScaleX *= -1;
|
||||
}
|
||||
}
|
||||
|
||||
public override bool FlipY
|
||||
{
|
||||
get => skeleton.ScaleY < 0;
|
||||
set
|
||||
{
|
||||
if (skeleton.ScaleY > 0 && value || skeleton.ScaleY < 0 && !value)
|
||||
skeleton.ScaleY *= -1;
|
||||
}
|
||||
}
|
||||
|
||||
public override string CurrentAnimation
|
||||
{
|
||||
get => animationState.GetCurrent(0)?.Animation.Name ?? DefaultAnimationName;
|
||||
set { if (animationNames.Contains(value)) { animationState.SetAnimation(0, value, true); Update(0); } }
|
||||
}
|
||||
|
||||
public override RectangleF Bounds
|
||||
{
|
||||
get
|
||||
{
|
||||
float[] _ = [];
|
||||
skeleton.GetBounds(out var x, out var y, out var w, out var h, ref _);
|
||||
return new RectangleF(x, y, w, h);
|
||||
}
|
||||
}
|
||||
|
||||
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(SpineRuntime38.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,
|
||||
_ => 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, 0);
|
||||
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.WorldVerticesLength > worldVertices.Length)
|
||||
worldVertices = worldVerticesBuffer = new float[meshAttachment.WorldVerticesLength * 2];
|
||||
meshAttachment.ComputeWorldVertices(slot, worldVertices);
|
||||
worldVerticesCount = meshAttachment.WorldVerticesLength / 2;
|
||||
worldTriangleIndices = meshAttachment.Triangles;
|
||||
worldTriangleIndicesLength = meshAttachment.Triangles.Length;
|
||||
uvs = meshAttachment.UVs;
|
||||
tintR *= meshAttachment.R;
|
||||
tintG *= meshAttachment.G;
|
||||
tintB *= meshAttachment.B;
|
||||
tintA *= meshAttachment.A;
|
||||
}
|
||||
else if (attachment is ClippingAttachment clippingAttachment)
|
||||
{
|
||||
clipping.ClipStart(slot, clippingAttachment);
|
||||
continue;
|
||||
}
|
||||
else
|
||||
{
|
||||
clipping.ClipEnd(slot);
|
||||
continue;
|
||||
}
|
||||
|
||||
SFML.Graphics.BlendMode blendMode = GetSFMLBlendMode(slot.Data.BlendMode);
|
||||
|
||||
states.Texture ??= texture;
|
||||
if (states.BlendMode != blendMode || states.Texture != texture)
|
||||
{
|
||||
if (vertexArray.VertexCount > 0)
|
||||
{
|
||||
// XXX: 实测不用设置 sampler2D 的值也正确
|
||||
if (UsePremultipliedAlpha && (states.BlendMode == BlendMode.Normal || states.BlendMode == BlendMode.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 == BlendMode.Normal || states.BlendMode == BlendMode.Additive))
|
||||
states.Shader = FragmentShader;
|
||||
else
|
||||
states.Shader = null;
|
||||
target.Draw(vertexArray, states);
|
||||
clipping.ClipEnd();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,340 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using SpineRuntime40;
|
||||
|
||||
namespace SpineViewer.Spine.Implementations
|
||||
{
|
||||
[SpineImplementation(Version.V40)]
|
||||
internal class Spine40 : Spine
|
||||
{
|
||||
private class TextureLoader : SpineRuntime40.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;
|
||||
|
||||
private SkeletonClipping clipping = new();
|
||||
|
||||
public Spine40(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 ArgumentException($"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 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.TrackTime = savedTrack0.TrackTime;
|
||||
var savedEntry = savedTrack0.Next;
|
||||
while (savedEntry is not null)
|
||||
{
|
||||
entry = animationState.AddAnimation(0, savedEntry.Animation.Name, true, 0);
|
||||
entry.TrackTime = 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.ScaleX < 0;
|
||||
set
|
||||
{
|
||||
if (skeleton.ScaleX > 0 && value || skeleton.ScaleX < 0 && !value)
|
||||
skeleton.ScaleX *= -1;
|
||||
}
|
||||
}
|
||||
|
||||
public override bool FlipY
|
||||
{
|
||||
get => skeleton.ScaleY < 0;
|
||||
set
|
||||
{
|
||||
if (skeleton.ScaleY > 0 && value || skeleton.ScaleY < 0 && !value)
|
||||
skeleton.ScaleY *= -1;
|
||||
}
|
||||
}
|
||||
|
||||
public override string CurrentAnimation
|
||||
{
|
||||
get => animationState.GetCurrent(0)?.Animation.Name ?? DefaultAnimationName;
|
||||
set { if (animationNames.Contains(value)) { animationState.SetAnimation(0, value, true); Update(0); } }
|
||||
}
|
||||
|
||||
public override RectangleF Bounds
|
||||
{
|
||||
get
|
||||
{
|
||||
float[] _ = [];
|
||||
skeleton.GetBounds(out var x, out var y, out var w, out var h, ref _);
|
||||
return new RectangleF(x, y, w, h);
|
||||
}
|
||||
}
|
||||
|
||||
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(SpineRuntime40.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,
|
||||
_ => 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, 0);
|
||||
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.WorldVerticesLength > worldVertices.Length)
|
||||
worldVertices = worldVerticesBuffer = new float[meshAttachment.WorldVerticesLength * 2];
|
||||
meshAttachment.ComputeWorldVertices(slot, worldVertices);
|
||||
worldVerticesCount = meshAttachment.WorldVerticesLength / 2;
|
||||
worldTriangleIndices = meshAttachment.Triangles;
|
||||
worldTriangleIndicesLength = meshAttachment.Triangles.Length;
|
||||
uvs = meshAttachment.UVs;
|
||||
tintR *= meshAttachment.R;
|
||||
tintG *= meshAttachment.G;
|
||||
tintB *= meshAttachment.B;
|
||||
tintA *= meshAttachment.A;
|
||||
}
|
||||
else if (attachment is ClippingAttachment clippingAttachment)
|
||||
{
|
||||
clipping.ClipStart(slot, clippingAttachment);
|
||||
continue;
|
||||
}
|
||||
else
|
||||
{
|
||||
clipping.ClipEnd(slot);
|
||||
continue;
|
||||
}
|
||||
|
||||
SFML.Graphics.BlendMode blendMode = GetSFMLBlendMode(slot.Data.BlendMode);
|
||||
|
||||
states.Texture ??= texture;
|
||||
if (states.BlendMode != blendMode || states.Texture != texture)
|
||||
{
|
||||
if (vertexArray.VertexCount > 0)
|
||||
{
|
||||
// XXX: 实测不用设置 sampler2D 的值也正确
|
||||
if (UsePremultipliedAlpha && (states.BlendMode == BlendMode.Normal || states.BlendMode == BlendMode.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 == BlendMode.Normal || states.BlendMode == BlendMode.Additive))
|
||||
states.Shader = FragmentShader;
|
||||
else
|
||||
states.Shader = null;
|
||||
target.Draw(vertexArray, states);
|
||||
clipping.ClipEnd();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,340 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using SpineRuntime41;
|
||||
|
||||
namespace SpineViewer.Spine.Implementations
|
||||
{
|
||||
[SpineImplementation(Version.V41)]
|
||||
internal class Spine41 : Spine
|
||||
{
|
||||
private class TextureLoader : SpineRuntime41.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;
|
||||
|
||||
private SkeletonClipping clipping = new();
|
||||
|
||||
public Spine41(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 ArgumentException($"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 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.TrackTime = savedTrack0.TrackTime;
|
||||
var savedEntry = savedTrack0.Next;
|
||||
while (savedEntry is not null)
|
||||
{
|
||||
entry = animationState.AddAnimation(0, savedEntry.Animation.Name, true, 0);
|
||||
entry.TrackTime = 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.ScaleX < 0;
|
||||
set
|
||||
{
|
||||
if (skeleton.ScaleX > 0 && value || skeleton.ScaleX < 0 && !value)
|
||||
skeleton.ScaleX *= -1;
|
||||
}
|
||||
}
|
||||
|
||||
public override bool FlipY
|
||||
{
|
||||
get => skeleton.ScaleY < 0;
|
||||
set
|
||||
{
|
||||
if (skeleton.ScaleY > 0 && value || skeleton.ScaleY < 0 && !value)
|
||||
skeleton.ScaleY *= -1;
|
||||
}
|
||||
}
|
||||
|
||||
public override string CurrentAnimation
|
||||
{
|
||||
get => animationState.GetCurrent(0)?.Animation.Name ?? DefaultAnimationName;
|
||||
set { if (animationNames.Contains(value)) { animationState.SetAnimation(0, value, true); Update(0); } }
|
||||
}
|
||||
|
||||
public override RectangleF Bounds
|
||||
{
|
||||
get
|
||||
{
|
||||
float[] _ = [];
|
||||
skeleton.GetBounds(out var x, out var y, out var w, out var h, ref _);
|
||||
return new RectangleF(x, y, w, h);
|
||||
}
|
||||
}
|
||||
|
||||
public override float GetAnimationDuration(string name) { return skeletonData.FindAnimation(name)?.Duration ?? 0f; }
|
||||
|
||||
public override void Update(float delta)
|
||||
{
|
||||
//skeleton.Update(delta); // XXX: v4.1.x 没有 Update 方法
|
||||
animationState.Update(delta);
|
||||
animationState.Apply(skeleton);
|
||||
skeleton.UpdateWorldTransform();
|
||||
}
|
||||
|
||||
private SFML.Graphics.BlendMode GetSFMLBlendMode(SpineRuntime41.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,
|
||||
_ => 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.Region).page.rendererObject;
|
||||
|
||||
regionAttachment.ComputeWorldVertices(slot, worldVertices, 0);
|
||||
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.Region).page.rendererObject;
|
||||
|
||||
if (meshAttachment.WorldVerticesLength > worldVertices.Length)
|
||||
worldVertices = worldVerticesBuffer = new float[meshAttachment.WorldVerticesLength * 2];
|
||||
meshAttachment.ComputeWorldVertices(slot, worldVertices);
|
||||
worldVerticesCount = meshAttachment.WorldVerticesLength / 2;
|
||||
worldTriangleIndices = meshAttachment.Triangles;
|
||||
worldTriangleIndicesLength = meshAttachment.Triangles.Length;
|
||||
uvs = meshAttachment.UVs;
|
||||
tintR *= meshAttachment.R;
|
||||
tintG *= meshAttachment.G;
|
||||
tintB *= meshAttachment.B;
|
||||
tintA *= meshAttachment.A;
|
||||
}
|
||||
else if (attachment is ClippingAttachment clippingAttachment)
|
||||
{
|
||||
clipping.ClipStart(slot, clippingAttachment);
|
||||
continue;
|
||||
}
|
||||
else
|
||||
{
|
||||
clipping.ClipEnd(slot);
|
||||
continue;
|
||||
}
|
||||
|
||||
SFML.Graphics.BlendMode blendMode = GetSFMLBlendMode(slot.Data.BlendMode);
|
||||
|
||||
states.Texture ??= texture;
|
||||
if (states.BlendMode != blendMode || states.Texture != texture)
|
||||
{
|
||||
if (vertexArray.VertexCount > 0)
|
||||
{
|
||||
// XXX: 实测不用设置 sampler2D 的值也正确
|
||||
if (UsePremultipliedAlpha && (states.BlendMode == BlendMode.Normal || states.BlendMode == BlendMode.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 == BlendMode.Normal || states.BlendMode == BlendMode.Additive))
|
||||
states.Shader = FragmentShader;
|
||||
else
|
||||
states.Shader = null;
|
||||
target.Draw(vertexArray, states);
|
||||
clipping.ClipEnd();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,340 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using SpineRuntime42;
|
||||
|
||||
namespace SpineViewer.Spine.Implementations
|
||||
{
|
||||
[SpineImplementation(Version.V42)]
|
||||
internal class Spine42 : Spine
|
||||
{
|
||||
private class TextureLoader : SpineRuntime42.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;
|
||||
|
||||
private SkeletonClipping clipping = new();
|
||||
|
||||
public Spine42(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 ArgumentException($"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 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.TrackTime = savedTrack0.TrackTime;
|
||||
var savedEntry = savedTrack0.Next;
|
||||
while (savedEntry is not null)
|
||||
{
|
||||
entry = animationState.AddAnimation(0, savedEntry.Animation.Name, true, 0);
|
||||
entry.TrackTime = 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.ScaleX < 0;
|
||||
set
|
||||
{
|
||||
if (skeleton.ScaleX > 0 && value || skeleton.ScaleX < 0 && !value)
|
||||
skeleton.ScaleX *= -1;
|
||||
}
|
||||
}
|
||||
|
||||
public override bool FlipY
|
||||
{
|
||||
get => skeleton.ScaleY < 0;
|
||||
set
|
||||
{
|
||||
if (skeleton.ScaleY > 0 && value || skeleton.ScaleY < 0 && !value)
|
||||
skeleton.ScaleY *= -1;
|
||||
}
|
||||
}
|
||||
|
||||
public override string CurrentAnimation
|
||||
{
|
||||
get => animationState.GetCurrent(0)?.Animation.Name ?? DefaultAnimationName;
|
||||
set { if (animationNames.Contains(value)) { animationState.SetAnimation(0, value, true); Update(0); } }
|
||||
}
|
||||
|
||||
public override RectangleF Bounds
|
||||
{
|
||||
get
|
||||
{
|
||||
float[] _ = [];
|
||||
skeleton.GetBounds(out var x, out var y, out var w, out var h, ref _);
|
||||
return new RectangleF(x, y, w, h);
|
||||
}
|
||||
}
|
||||
|
||||
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(Skeleton.Physics.Update);
|
||||
}
|
||||
|
||||
private SFML.Graphics.BlendMode GetSFMLBlendMode(SpineRuntime42.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,
|
||||
_ => 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.Region).page.rendererObject;
|
||||
|
||||
regionAttachment.ComputeWorldVertices(slot, worldVertices, 0);
|
||||
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.Region).page.rendererObject;
|
||||
|
||||
if (meshAttachment.WorldVerticesLength > worldVertices.Length)
|
||||
worldVertices = worldVerticesBuffer = new float[meshAttachment.WorldVerticesLength * 2];
|
||||
meshAttachment.ComputeWorldVertices(slot, worldVertices);
|
||||
worldVerticesCount = meshAttachment.WorldVerticesLength / 2;
|
||||
worldTriangleIndices = meshAttachment.Triangles;
|
||||
worldTriangleIndicesLength = meshAttachment.Triangles.Length;
|
||||
uvs = meshAttachment.UVs;
|
||||
tintR *= meshAttachment.R;
|
||||
tintG *= meshAttachment.G;
|
||||
tintB *= meshAttachment.B;
|
||||
tintA *= meshAttachment.A;
|
||||
}
|
||||
else if (attachment is ClippingAttachment clippingAttachment)
|
||||
{
|
||||
clipping.ClipStart(slot, clippingAttachment);
|
||||
continue;
|
||||
}
|
||||
else
|
||||
{
|
||||
clipping.ClipEnd(slot);
|
||||
continue;
|
||||
}
|
||||
|
||||
SFML.Graphics.BlendMode blendMode = GetSFMLBlendMode(slot.Data.BlendMode);
|
||||
|
||||
states.Texture ??= texture;
|
||||
if (states.BlendMode != blendMode || states.Texture != texture)
|
||||
{
|
||||
if (vertexArray.VertexCount > 0)
|
||||
{
|
||||
// XXX: 实测不用设置 sampler2D 的值也正确
|
||||
if (UsePremultipliedAlpha && (states.BlendMode == BlendMode.Normal || states.BlendMode == BlendMode.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, 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 == BlendMode.Normal || states.BlendMode == BlendMode.Additive))
|
||||
states.Shader = FragmentShader;
|
||||
else
|
||||
states.Shader = null;
|
||||
target.Draw(vertexArray, states);
|
||||
clipping.ClipEnd();
|
||||
}
|
||||
}
|
||||
}
|
||||
560
SpineViewer/Spine/Implementations/SpineObject/SpineObject21.cs
Normal file
560
SpineViewer/Spine/Implementations/SpineObject/SpineObject21.cs
Normal file
@@ -0,0 +1,560 @@
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Frozen;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using SpineRuntime21;
|
||||
using SpineViewer.Extensions;
|
||||
using SpineViewer.Utils;
|
||||
|
||||
namespace SpineViewer.Spine.Implementations.SpineObject
|
||||
{
|
||||
[SpineImplementation(SpineVersion.V21)]
|
||||
internal class SpineObject21 : Spine.SpineObject
|
||||
{
|
||||
//private static 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}"),
|
||||
// };
|
||||
//}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
public void Unload(object texture)
|
||||
{
|
||||
((SFML.Graphics.Texture)texture).Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
private readonly static TextureLoader textureLoader = new();
|
||||
private static readonly Animation EmptyAnimation = new(EMPTY_ANIMATION, [], 0);
|
||||
|
||||
private readonly Atlas atlas;
|
||||
private readonly SkeletonBinary? skeletonBinary;
|
||||
private readonly SkeletonJson? skeletonJson;
|
||||
private SkeletonData skeletonData;
|
||||
private AnimationStateData animationStateData;
|
||||
|
||||
private Skeleton skeleton;
|
||||
private AnimationState animationState;
|
||||
|
||||
// 2.1.x 不支持剪裁
|
||||
//private SkeletonClipping clipping = new();
|
||||
|
||||
/// <summary>
|
||||
/// 所有插槽在所有皮肤中可用的附件集合
|
||||
/// </summary>
|
||||
private readonly Dictionary<string, Dictionary<string, Attachment>> slotAttachments = [];
|
||||
|
||||
public SpineObject21(string skelPath, string atlasPath) : 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}");
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var sk in skeletonData.Skins)
|
||||
{
|
||||
foreach (var (k, att) in sk.Attachments)
|
||||
{
|
||||
var slotName = skeletonData.Slots[k.Key].Name;
|
||||
if (!slotAttachments.TryGetValue(slotName, out var attachments))
|
||||
slotAttachments[slotName] = attachments = new() { [EMPTY_ATTACHMENT] = null };
|
||||
attachments[att.Name] = att;
|
||||
}
|
||||
}
|
||||
SlotAttachmentNames = slotAttachments.ToFrozenDictionary(item => item.Key, item => item.Value.Keys.ToImmutableArray());
|
||||
SkinNames = skeletonData.Skins.Select(v => v.Name).ToImmutableArray();
|
||||
AnimationNames = [EMPTY_ANIMATION, .. skeletonData.Animations.Select(v => v.Name)];
|
||||
|
||||
skeleton = new Skeleton(skeletonData) { Skin = new(Guid.NewGuid().ToString()) }; // 挂载一个空皮肤当作容器
|
||||
animationStateData = new AnimationStateData(skeletonData);
|
||||
animationState = new AnimationState(animationStateData);
|
||||
}
|
||||
|
||||
protected override void Dispose(bool disposing)
|
||||
{
|
||||
base.Dispose(disposing);
|
||||
atlas.Dispose();
|
||||
}
|
||||
|
||||
public override string FileVersion { get => skeletonData.Version; }
|
||||
|
||||
protected override float scale
|
||||
{
|
||||
get => Math.Abs(skeleton.ScaleX);
|
||||
set
|
||||
{
|
||||
skeleton.ScaleX = Math.Sign(skeleton.ScaleX) * value;
|
||||
skeleton.ScaleY = Math.Sign(skeleton.ScaleY) * value;
|
||||
}
|
||||
}
|
||||
|
||||
protected override PointF position
|
||||
{
|
||||
get => new(skeleton.X, skeleton.Y);
|
||||
set
|
||||
{
|
||||
skeleton.X = value.X;
|
||||
skeleton.Y = value.Y;
|
||||
}
|
||||
}
|
||||
|
||||
protected override bool flipX
|
||||
{
|
||||
get => skeleton.ScaleX < 0;
|
||||
set
|
||||
{
|
||||
if (skeleton.ScaleX > 0 && value || skeleton.ScaleX < 0 && !value)
|
||||
skeleton.ScaleX *= -1;
|
||||
}
|
||||
}
|
||||
|
||||
protected override bool flipY
|
||||
{
|
||||
get => skeleton.ScaleY < 0;
|
||||
set
|
||||
{
|
||||
if (skeleton.ScaleY > 0 && value || skeleton.ScaleY < 0 && !value)
|
||||
skeleton.ScaleY *= -1;
|
||||
}
|
||||
}
|
||||
|
||||
protected override string getSlotAttachment(string slot) => skeleton.FindSlot(slot)?.Attachment?.Name ?? EMPTY_ATTACHMENT;
|
||||
|
||||
protected override void setSlotAttachment(string slot, string name)
|
||||
{
|
||||
if (slotAttachments.TryGetValue(slot, out var attachments)
|
||||
&& attachments.TryGetValue(name, out var att)
|
||||
&& skeleton.FindSlot(slot) is Slot s)
|
||||
s.Attachment = att;
|
||||
}
|
||||
|
||||
protected override void addSkin(string name)
|
||||
{
|
||||
// default 不需要加载
|
||||
if (name != "default" && skeletonData.FindSkin(name) is Skin sk)
|
||||
{
|
||||
// XXX: 3.7 及以下不支持 AddSkin
|
||||
foreach (var (k, v) in sk.Attachments)
|
||||
skeleton.Skin.AddAttachment(k.Key, k.Value, v);
|
||||
}
|
||||
skeleton.SetSlotsToSetupPose();
|
||||
}
|
||||
|
||||
protected override void clearSkins()
|
||||
{
|
||||
skeleton.Skin.Attachments.Clear();
|
||||
skeleton.SetSlotsToSetupPose();
|
||||
}
|
||||
|
||||
protected override int[] getTrackIndices() => animationState.Tracks.Select((_, i) => i).Where(i => animationState.Tracks[i] is not null).ToArray();
|
||||
|
||||
protected override string getAnimation(int track) => animationState.GetCurrent(track)?.Animation.Name ?? EMPTY_ANIMATION;
|
||||
|
||||
protected override void setAnimation(int track, string name)
|
||||
{
|
||||
if (name == EMPTY_ANIMATION)
|
||||
animationState.SetAnimation(track, EmptyAnimation, true);
|
||||
else if (AnimationNames.Contains(name))
|
||||
animationState.SetAnimation(track, name, true);
|
||||
}
|
||||
|
||||
protected override void clearTrack(int i) => animationState.ClearTrack(i);
|
||||
|
||||
public override float GetAnimationDuration(string name) { return skeletonData.FindAnimation(name)?.Duration ?? 0f; }
|
||||
|
||||
protected override RectangleF getCurrentBounds()
|
||||
{
|
||||
skeleton.GetBounds(out var x, out var y, out var w, out var h);
|
||||
return new RectangleF(x, y, w, h);
|
||||
}
|
||||
|
||||
protected override RectangleF getBounds()
|
||||
{
|
||||
// 初始化临时对象
|
||||
var maxDuration = 0f;
|
||||
var tmpSkeleton = new Skeleton(skeletonData) { Skin = new(Guid.NewGuid().ToString()) };
|
||||
var tmpAnimationState = new AnimationState(animationStateData);
|
||||
tmpSkeleton.ScaleX = skeleton.ScaleX;
|
||||
tmpSkeleton.ScaleY = skeleton.ScaleY;
|
||||
tmpSkeleton.X = skeleton.X;
|
||||
tmpSkeleton.Y = skeleton.Y;
|
||||
foreach (var (name, _) in skinLoadStatus.Where(e => e.Value))
|
||||
{
|
||||
foreach (var (k, v) in skeletonData.FindSkin(name).Attachments)
|
||||
tmpSkeleton.Skin.AddAttachment(k.Key, k.Value, v);
|
||||
}
|
||||
foreach (var tr in animationState.Tracks.Select((_, i) => i).Where(i => animationState.Tracks[i] is not null))
|
||||
{
|
||||
var ani = animationState.GetCurrent(tr).Animation;
|
||||
tmpAnimationState.SetAnimation(tr, ani, true);
|
||||
if (ani.Duration > maxDuration) maxDuration = ani.Duration;
|
||||
}
|
||||
tmpSkeleton.SetSlotsToSetupPose();
|
||||
tmpAnimationState.Update(0);
|
||||
tmpAnimationState.Apply(tmpSkeleton);
|
||||
tmpSkeleton.Update(0);
|
||||
tmpSkeleton.UpdateWorldTransform();
|
||||
|
||||
// 按 10 帧每秒计算边框
|
||||
var bounds = getCurrentBounds();
|
||||
float[] _ = [];
|
||||
for (float tick = 0, delta = 0.1f; tick < maxDuration; tick += delta)
|
||||
{
|
||||
tmpSkeleton.GetBounds(out var x, out var y, out var w, out var h);
|
||||
bounds = bounds.Union(new(x, y, w, h));
|
||||
tmpAnimationState.Update(delta);
|
||||
tmpAnimationState.Apply(tmpSkeleton);
|
||||
tmpSkeleton.Update(delta);
|
||||
tmpSkeleton.UpdateWorldTransform();
|
||||
}
|
||||
|
||||
return bounds;
|
||||
}
|
||||
|
||||
protected override void update(float delta)
|
||||
{
|
||||
animationState.Update(delta);
|
||||
animationState.Apply(skeleton);
|
||||
skeleton.Update(delta);
|
||||
skeleton.UpdateWorldTransform();
|
||||
}
|
||||
|
||||
protected override void draw(SFML.Graphics.RenderTarget target, SFML.Graphics.RenderStates states)
|
||||
{
|
||||
triangleVertices.Clear();
|
||||
states.Texture = null;
|
||||
states.Shader = SFMLShader.GetSpineShader(usePma);
|
||||
|
||||
// 要用 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 ? SFMLBlendMode.AdditivePma : SFMLBlendMode.NormalPma;
|
||||
|
||||
states.Texture ??= texture;
|
||||
if (states.BlendMode != blendMode || states.Texture != texture)
|
||||
{
|
||||
if (triangleVertices.VertexCount > 0)
|
||||
{
|
||||
target.Draw(triangleVertices, states);
|
||||
triangleVertices.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;
|
||||
triangleVertices.Append(vertex);
|
||||
}
|
||||
|
||||
//clipping.ClipEnd(slot);
|
||||
}
|
||||
//clipping.ClipEnd();
|
||||
|
||||
target.Draw(triangleVertices, states);
|
||||
}
|
||||
|
||||
protected override void debugDraw(SFML.Graphics.RenderTarget target)
|
||||
{
|
||||
lineVertices.Clear();
|
||||
rectLineVertices.Clear();
|
||||
|
||||
if (debugRegions)
|
||||
{
|
||||
SFML.Graphics.Vertex vt = new() { Color = AttachmentLineColor };
|
||||
foreach (var slot in skeleton.Slots)
|
||||
{
|
||||
if (slot.Attachment is RegionAttachment regionAttachment)
|
||||
{
|
||||
regionAttachment.ComputeWorldVertices(slot.Bone, worldVerticesBuffer);
|
||||
|
||||
vt.Position.X = worldVerticesBuffer[0];
|
||||
vt.Position.Y = worldVerticesBuffer[1];
|
||||
lineVertices.Append(vt);
|
||||
|
||||
vt.Position.X = worldVerticesBuffer[2];
|
||||
vt.Position.Y = worldVerticesBuffer[3];
|
||||
lineVertices.Append(vt); lineVertices.Append(vt);
|
||||
|
||||
vt.Position.X = worldVerticesBuffer[4];
|
||||
vt.Position.Y = worldVerticesBuffer[5];
|
||||
lineVertices.Append(vt); lineVertices.Append(vt);
|
||||
|
||||
vt.Position.X = worldVerticesBuffer[6];
|
||||
vt.Position.Y = worldVerticesBuffer[7];
|
||||
lineVertices.Append(vt); lineVertices.Append(vt);
|
||||
|
||||
vt.Position.X = worldVerticesBuffer[0];
|
||||
vt.Position.Y = worldVerticesBuffer[1];
|
||||
lineVertices.Append(vt);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (debugMeshes)
|
||||
{
|
||||
SFML.Graphics.Vertex vt = new() { Color = MeshLineColor };
|
||||
foreach (var slot in skeleton.Slots)
|
||||
{
|
||||
if (slot.Attachment is MeshAttachment meshAttachment)
|
||||
{
|
||||
if (meshAttachment.Vertices.Length > worldVerticesBuffer.Length)
|
||||
worldVerticesBuffer = new float[meshAttachment.Vertices.Length * 2];
|
||||
|
||||
meshAttachment.ComputeWorldVertices(slot, worldVerticesBuffer);
|
||||
|
||||
var triangleIndices = meshAttachment.Triangles;
|
||||
for (int i = 0; i < triangleIndices.Length; i += 3)
|
||||
{
|
||||
var idx0 = triangleIndices[i] * 2;
|
||||
var idx1 = triangleIndices[i + 1] * 2;
|
||||
var idx2 = triangleIndices[i + 2] * 2;
|
||||
|
||||
vt.Position.X = worldVerticesBuffer[idx0];
|
||||
vt.Position.Y = worldVerticesBuffer[idx0 + 1];
|
||||
lineVertices.Append(vt);
|
||||
|
||||
vt.Position.X = worldVerticesBuffer[idx1];
|
||||
vt.Position.Y = worldVerticesBuffer[idx1 + 1];
|
||||
lineVertices.Append(vt); lineVertices.Append(vt);
|
||||
|
||||
vt.Position.X = worldVerticesBuffer[idx2];
|
||||
vt.Position.Y = worldVerticesBuffer[idx2 + 1];
|
||||
lineVertices.Append(vt); lineVertices.Append(vt);
|
||||
|
||||
vt.Position.X = worldVerticesBuffer[idx0];
|
||||
vt.Position.Y = worldVerticesBuffer[idx0 + 1];
|
||||
lineVertices.Append(vt);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (debugMeshHulls)
|
||||
{
|
||||
SFML.Graphics.Vertex vt = new() { Color = AttachmentLineColor };
|
||||
foreach (var slot in skeleton.Slots)
|
||||
{
|
||||
if (slot.Attachment is MeshAttachment meshAttachment)
|
||||
{
|
||||
if (meshAttachment.Vertices.Length > worldVerticesBuffer.Length)
|
||||
worldVerticesBuffer = new float[meshAttachment.Vertices.Length * 2];
|
||||
|
||||
meshAttachment.ComputeWorldVertices(slot, worldVerticesBuffer);
|
||||
|
||||
var hullLength = (meshAttachment.HullLength >> 1) << 1;
|
||||
|
||||
if (debugMeshHulls && hullLength > 2)
|
||||
{
|
||||
vt.Position.X = worldVerticesBuffer[0];
|
||||
vt.Position.Y = worldVerticesBuffer[1];
|
||||
lineVertices.Append(vt);
|
||||
|
||||
for (int i = 2; i < hullLength; i += 2)
|
||||
{
|
||||
vt.Position.X = worldVerticesBuffer[i];
|
||||
vt.Position.Y = worldVerticesBuffer[i + 1];
|
||||
lineVertices.Append(vt);
|
||||
lineVertices.Append(vt);
|
||||
}
|
||||
|
||||
vt.Position.X = worldVerticesBuffer[0];
|
||||
vt.Position.Y = worldVerticesBuffer[1];
|
||||
lineVertices.Append(vt);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (debugBoundingBoxes)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
if (debugPaths)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
if (debugClippings) { } // 没有剪裁附件
|
||||
|
||||
if (debugBounds)
|
||||
{
|
||||
var vt = new SFML.Graphics.Vertex() { Color = BoundsColor };
|
||||
var b = getCurrentBounds();
|
||||
|
||||
vt.Position.X = b.Left;
|
||||
vt.Position.Y = b.Top;
|
||||
lineVertices.Append(vt);
|
||||
|
||||
vt.Position.X = b.Right;
|
||||
vt.Position.Y = b.Top;
|
||||
lineVertices.Append(vt); lineVertices.Append(vt);
|
||||
|
||||
vt.Position.X = b.Right;
|
||||
vt.Position.Y = b.Bottom;
|
||||
lineVertices.Append(vt); lineVertices.Append(vt);
|
||||
|
||||
vt.Position.X = b.Left;
|
||||
vt.Position.Y = b.Bottom;
|
||||
lineVertices.Append(vt); lineVertices.Append(vt);
|
||||
|
||||
vt.Position.X = b.Left;
|
||||
vt.Position.Y = b.Top;
|
||||
lineVertices.Append(vt);
|
||||
}
|
||||
|
||||
// 骨骼线放最后画
|
||||
if (debugBones)
|
||||
{
|
||||
var width = scale;
|
||||
foreach (var bone in skeleton.Bones)
|
||||
{
|
||||
var boneLength = bone.Data.Length;
|
||||
var p1 = new SFML.System.Vector2f(bone.WorldX, bone.WorldY);
|
||||
var p2 = new SFML.System.Vector2f(bone.WorldX + boneLength * bone.M00, bone.WorldY + boneLength * bone.M10);
|
||||
AddRectLine(p1, p2, BoneLineColor, width);
|
||||
}
|
||||
}
|
||||
|
||||
target.Draw(lineVertices);
|
||||
target.Draw(rectLineVertices);
|
||||
|
||||
// 骨骼的点最后画, 层级处于骨骼线上面
|
||||
if (debugBones)
|
||||
{
|
||||
var radius = scale;
|
||||
foreach (var bone in skeleton.Bones)
|
||||
{
|
||||
DrawCirclePoint(target, new(bone.WorldX, bone.WorldY), BonePointColor, radius);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
587
SpineViewer/Spine/Implementations/SpineObject/SpineObject36.cs
Normal file
587
SpineViewer/Spine/Implementations/SpineObject/SpineObject36.cs
Normal file
@@ -0,0 +1,587 @@
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Frozen;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using SpineRuntime36;
|
||||
using SpineViewer.Extensions;
|
||||
using SpineViewer.Utils;
|
||||
|
||||
namespace SpineViewer.Spine.Implementations.SpineObject
|
||||
{
|
||||
[SpineImplementation(SpineVersion.V36)]
|
||||
internal class SpineObject36 : Spine.SpineObject
|
||||
{
|
||||
private static SFML.Graphics.BlendMode GetSFMLBlendMode(BlendMode spineBlendMode)
|
||||
{
|
||||
return spineBlendMode switch
|
||||
{
|
||||
BlendMode.Normal => SFMLBlendMode.NormalPma,
|
||||
BlendMode.Additive => SFMLBlendMode.AdditivePma,
|
||||
BlendMode.Multiply => SFMLBlendMode.MultiplyPma,
|
||||
BlendMode.Screen => SFMLBlendMode.ScreenPma,
|
||||
_ => throw new NotImplementedException($"{spineBlendMode}"),
|
||||
};
|
||||
}
|
||||
|
||||
private class TextureLoader : SpineRuntime36.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;
|
||||
}
|
||||
|
||||
public void Unload(object texture)
|
||||
{
|
||||
((SFML.Graphics.Texture)texture).Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
private static readonly TextureLoader textureLoader = new();
|
||||
private static readonly Animation EmptyAnimation = new(EMPTY_ANIMATION, [], 0);
|
||||
|
||||
private readonly Atlas atlas;
|
||||
private readonly SkeletonBinary? skeletonBinary;
|
||||
private readonly SkeletonJson? skeletonJson;
|
||||
private SkeletonData skeletonData;
|
||||
private AnimationStateData animationStateData;
|
||||
|
||||
private Skeleton skeleton;
|
||||
private AnimationState animationState;
|
||||
|
||||
private readonly SkeletonClipping clipping = new();
|
||||
|
||||
/// <summary>
|
||||
/// 所有插槽在所有皮肤中可用的附件集合
|
||||
/// </summary>
|
||||
private readonly Dictionary<string, Dictionary<string, Attachment>> slotAttachments = [];
|
||||
|
||||
public SpineObject36(string skelPath, string atlasPath) : 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}");
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var sk in skeletonData.Skins)
|
||||
{
|
||||
foreach (var (k, att) in sk.Attachments)
|
||||
{
|
||||
var slotName = skeletonData.Slots.Items[k.slotIndex].Name;
|
||||
if (!slotAttachments.TryGetValue(slotName, out var attachments))
|
||||
slotAttachments[slotName] = attachments = new() { [EMPTY_ATTACHMENT] = null };
|
||||
attachments[att.Name] = att;
|
||||
}
|
||||
}
|
||||
SlotAttachmentNames = slotAttachments.ToFrozenDictionary(item => item.Key, item => item.Value.Keys.ToImmutableArray());
|
||||
SkinNames = skeletonData.Skins.Select(v => v.Name).ToImmutableArray();
|
||||
AnimationNames = [EMPTY_ANIMATION, .. skeletonData.Animations.Select(v => v.Name)];
|
||||
|
||||
skeleton = new Skeleton(skeletonData) { Skin = new(Guid.NewGuid().ToString()) }; // 挂载一个空皮肤当作容器
|
||||
animationStateData = new AnimationStateData(skeletonData);
|
||||
animationState = new AnimationState(animationStateData);
|
||||
}
|
||||
|
||||
protected override void Dispose(bool disposing)
|
||||
{
|
||||
base.Dispose(disposing);
|
||||
atlas.Dispose();
|
||||
}
|
||||
|
||||
public override string FileVersion { get => skeletonData.Version; }
|
||||
|
||||
protected override float scale
|
||||
{
|
||||
get => Math.Abs(skeleton.ScaleX);
|
||||
set
|
||||
{
|
||||
skeleton.ScaleX = Math.Sign(skeleton.ScaleX) * value;
|
||||
skeleton.ScaleY = Math.Sign(skeleton.ScaleY) * value;
|
||||
}
|
||||
}
|
||||
|
||||
protected override PointF position
|
||||
{
|
||||
get => new(skeleton.X, skeleton.Y);
|
||||
set
|
||||
{
|
||||
skeleton.X = value.X;
|
||||
skeleton.Y = value.Y;
|
||||
}
|
||||
}
|
||||
|
||||
protected override bool flipX
|
||||
{
|
||||
get => skeleton.ScaleX < 0;
|
||||
set
|
||||
{
|
||||
if (skeleton.ScaleX > 0 && value || skeleton.ScaleX < 0 && !value)
|
||||
skeleton.ScaleX *= -1;
|
||||
}
|
||||
}
|
||||
|
||||
protected override bool flipY
|
||||
{
|
||||
get => skeleton.ScaleY < 0;
|
||||
set
|
||||
{
|
||||
if (skeleton.ScaleY > 0 && value || skeleton.ScaleY < 0 && !value)
|
||||
skeleton.ScaleY *= -1;
|
||||
}
|
||||
}
|
||||
|
||||
protected override string getSlotAttachment(string slot) => skeleton.FindSlot(slot)?.Attachment?.Name ?? EMPTY_ATTACHMENT;
|
||||
|
||||
protected override void setSlotAttachment(string slot, string name)
|
||||
{
|
||||
if (slotAttachments.TryGetValue(slot, out var attachments)
|
||||
&& attachments.TryGetValue(name, out var att)
|
||||
&& skeleton.FindSlot(slot) is Slot s)
|
||||
s.Attachment = att;
|
||||
}
|
||||
|
||||
protected override void addSkin(string name)
|
||||
{
|
||||
// default 不需要加载
|
||||
if (name != "default" && skeletonData.FindSkin(name) is Skin sk)
|
||||
{
|
||||
// XXX: 3.7 及以下不支持 AddSkin
|
||||
foreach (var (k, v) in sk.Attachments)
|
||||
skeleton.Skin.AddAttachment(k.slotIndex, k.name, v);
|
||||
}
|
||||
skeleton.SetSlotsToSetupPose();
|
||||
}
|
||||
|
||||
protected override void clearSkins()
|
||||
{
|
||||
skeleton.Skin.Attachments.Clear();
|
||||
skeleton.SetSlotsToSetupPose();
|
||||
}
|
||||
|
||||
protected override int[] getTrackIndices() => animationState.Tracks.Select((_, i) => i).Where(i => animationState.Tracks.Items[i] is not null).ToArray();
|
||||
|
||||
protected override string getAnimation(int track) => animationState.GetCurrent(track)?.Animation.Name ?? EMPTY_ANIMATION;
|
||||
|
||||
protected override void setAnimation(int track, string name)
|
||||
{
|
||||
if (name == EMPTY_ANIMATION)
|
||||
animationState.SetAnimation(track, EmptyAnimation, true);
|
||||
else if (AnimationNames.Contains(name))
|
||||
animationState.SetAnimation(track, name, true);
|
||||
}
|
||||
|
||||
protected override void clearTrack(int i) => animationState.ClearTrack(i);
|
||||
|
||||
public override float GetAnimationDuration(string name) { return skeletonData.FindAnimation(name)?.Duration ?? 0f; }
|
||||
|
||||
protected override RectangleF getCurrentBounds()
|
||||
{
|
||||
float[] _ = [];
|
||||
skeleton.GetBounds(out var x, out var y, out var w, out var h, ref _);
|
||||
return new RectangleF(x, y, w, h);
|
||||
}
|
||||
|
||||
protected override RectangleF getBounds()
|
||||
{
|
||||
// 初始化临时对象
|
||||
var maxDuration = 0f;
|
||||
var tmpSkeleton = new Skeleton(skeletonData) { Skin = new(Guid.NewGuid().ToString()) };
|
||||
var tmpAnimationState = new AnimationState(animationStateData);
|
||||
tmpSkeleton.ScaleX = skeleton.ScaleX;
|
||||
tmpSkeleton.ScaleY = skeleton.ScaleY;
|
||||
tmpSkeleton.X = skeleton.X;
|
||||
tmpSkeleton.Y = skeleton.Y;
|
||||
foreach (var (name, _) in skinLoadStatus.Where(e => e.Value))
|
||||
{
|
||||
foreach (var (k, v) in skeletonData.FindSkin(name).Attachments)
|
||||
tmpSkeleton.Skin.AddAttachment(k.slotIndex, k.name, v);
|
||||
}
|
||||
foreach (var tr in animationState.Tracks.Select((_, i) => i).Where(i => animationState.Tracks.Items[i] is not null))
|
||||
{
|
||||
var ani = animationState.GetCurrent(tr).Animation;
|
||||
tmpAnimationState.SetAnimation(tr, ani, true);
|
||||
if (ani.Duration > maxDuration) maxDuration = ani.Duration;
|
||||
}
|
||||
tmpSkeleton.SetSlotsToSetupPose();
|
||||
tmpAnimationState.Update(0);
|
||||
tmpAnimationState.Apply(tmpSkeleton);
|
||||
tmpSkeleton.Update(0);
|
||||
tmpSkeleton.UpdateWorldTransform();
|
||||
|
||||
// 按 10 帧每秒计算边框
|
||||
var bounds = getCurrentBounds();
|
||||
float[] _ = [];
|
||||
for (float tick = 0, delta = 0.1f; tick < maxDuration; tick += delta)
|
||||
{
|
||||
tmpSkeleton.GetBounds(out var x, out var y, out var w, out var h, ref _);
|
||||
bounds = bounds.Union(new(x, y, w, h));
|
||||
tmpAnimationState.Update(delta);
|
||||
tmpAnimationState.Apply(tmpSkeleton);
|
||||
tmpSkeleton.Update(delta);
|
||||
tmpSkeleton.UpdateWorldTransform();
|
||||
}
|
||||
|
||||
return bounds;
|
||||
}
|
||||
|
||||
protected override void update(float delta)
|
||||
{
|
||||
animationState.Update(delta);
|
||||
animationState.Apply(skeleton);
|
||||
skeleton.Update(delta);
|
||||
skeleton.UpdateWorldTransform();
|
||||
}
|
||||
|
||||
protected override void draw(SFML.Graphics.RenderTarget target, SFML.Graphics.RenderStates states)
|
||||
{
|
||||
triangleVertices.Clear();
|
||||
states.Texture = null;
|
||||
states.Shader = SFMLShader.GetSpineShader(usePma);
|
||||
|
||||
// 要用 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, 0);
|
||||
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.WorldVerticesLength > worldVertices.Length)
|
||||
worldVertices = worldVerticesBuffer = new float[meshAttachment.WorldVerticesLength * 2];
|
||||
meshAttachment.ComputeWorldVertices(slot, worldVertices);
|
||||
worldVerticesCount = meshAttachment.WorldVerticesLength / 2;
|
||||
worldTriangleIndices = meshAttachment.Triangles;
|
||||
worldTriangleIndicesLength = meshAttachment.Triangles.Length;
|
||||
uvs = meshAttachment.UVs;
|
||||
tintR *= meshAttachment.R;
|
||||
tintG *= meshAttachment.G;
|
||||
tintB *= meshAttachment.B;
|
||||
tintA *= meshAttachment.A;
|
||||
}
|
||||
else if (attachment is ClippingAttachment clippingAttachment)
|
||||
{
|
||||
clipping.ClipStart(slot, clippingAttachment);
|
||||
continue;
|
||||
}
|
||||
else
|
||||
{
|
||||
clipping.ClipEnd(slot);
|
||||
continue;
|
||||
}
|
||||
|
||||
SFML.Graphics.BlendMode blendMode = GetSFMLBlendMode(slot.Data.BlendMode);
|
||||
|
||||
states.Texture ??= texture;
|
||||
if (states.BlendMode != blendMode || states.Texture != texture)
|
||||
{
|
||||
if (triangleVertices.VertexCount > 0)
|
||||
{
|
||||
target.Draw(triangleVertices, states);
|
||||
triangleVertices.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;
|
||||
triangleVertices.Append(vertex);
|
||||
}
|
||||
|
||||
clipping.ClipEnd(slot);
|
||||
}
|
||||
clipping.ClipEnd();
|
||||
|
||||
target.Draw(triangleVertices, states);
|
||||
}
|
||||
|
||||
protected override void debugDraw(SFML.Graphics.RenderTarget target)
|
||||
{
|
||||
lineVertices.Clear();
|
||||
rectLineVertices.Clear();
|
||||
|
||||
if (debugRegions)
|
||||
{
|
||||
SFML.Graphics.Vertex vt = new() { Color = AttachmentLineColor };
|
||||
foreach (var slot in skeleton.Slots)
|
||||
{
|
||||
if (slot.Attachment is RegionAttachment regionAttachment)
|
||||
{
|
||||
regionAttachment.ComputeWorldVertices(slot.Bone, worldVerticesBuffer, 0);
|
||||
|
||||
vt.Position.X = worldVerticesBuffer[0];
|
||||
vt.Position.Y = worldVerticesBuffer[1];
|
||||
lineVertices.Append(vt);
|
||||
|
||||
vt.Position.X = worldVerticesBuffer[2];
|
||||
vt.Position.Y = worldVerticesBuffer[3];
|
||||
lineVertices.Append(vt); lineVertices.Append(vt);
|
||||
|
||||
vt.Position.X = worldVerticesBuffer[4];
|
||||
vt.Position.Y = worldVerticesBuffer[5];
|
||||
lineVertices.Append(vt); lineVertices.Append(vt);
|
||||
|
||||
vt.Position.X = worldVerticesBuffer[6];
|
||||
vt.Position.Y = worldVerticesBuffer[7];
|
||||
lineVertices.Append(vt); lineVertices.Append(vt);
|
||||
|
||||
vt.Position.X = worldVerticesBuffer[0];
|
||||
vt.Position.Y = worldVerticesBuffer[1];
|
||||
lineVertices.Append(vt);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (debugMeshes)
|
||||
{
|
||||
SFML.Graphics.Vertex vt = new() { Color = MeshLineColor };
|
||||
foreach (var slot in skeleton.Slots)
|
||||
{
|
||||
if (slot.Attachment is MeshAttachment meshAttachment)
|
||||
{
|
||||
if (meshAttachment.WorldVerticesLength > worldVerticesBuffer.Length)
|
||||
worldVerticesBuffer = new float[meshAttachment.WorldVerticesLength * 2];
|
||||
|
||||
meshAttachment.ComputeWorldVertices(slot, worldVerticesBuffer);
|
||||
|
||||
var triangleIndices = meshAttachment.Triangles;
|
||||
for (int i = 0; i < triangleIndices.Length; i += 3)
|
||||
{
|
||||
var idx0 = triangleIndices[i] * 2;
|
||||
var idx1 = triangleIndices[i + 1] * 2;
|
||||
var idx2 = triangleIndices[i + 2] * 2;
|
||||
|
||||
vt.Position.X = worldVerticesBuffer[idx0];
|
||||
vt.Position.Y = worldVerticesBuffer[idx0 + 1];
|
||||
lineVertices.Append(vt);
|
||||
|
||||
vt.Position.X = worldVerticesBuffer[idx1];
|
||||
vt.Position.Y = worldVerticesBuffer[idx1 + 1];
|
||||
lineVertices.Append(vt); lineVertices.Append(vt);
|
||||
|
||||
vt.Position.X = worldVerticesBuffer[idx2];
|
||||
vt.Position.Y = worldVerticesBuffer[idx2 + 1];
|
||||
lineVertices.Append(vt); lineVertices.Append(vt);
|
||||
|
||||
vt.Position.X = worldVerticesBuffer[idx0];
|
||||
vt.Position.Y = worldVerticesBuffer[idx0 + 1];
|
||||
lineVertices.Append(vt);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (debugMeshHulls)
|
||||
{
|
||||
SFML.Graphics.Vertex vt = new() { Color = AttachmentLineColor };
|
||||
foreach (var slot in skeleton.Slots)
|
||||
{
|
||||
if (slot.Attachment is MeshAttachment meshAttachment)
|
||||
{
|
||||
if (meshAttachment.WorldVerticesLength > worldVerticesBuffer.Length)
|
||||
worldVerticesBuffer = new float[meshAttachment.WorldVerticesLength * 2];
|
||||
|
||||
meshAttachment.ComputeWorldVertices(slot, worldVerticesBuffer);
|
||||
|
||||
var hullLength = (meshAttachment.HullLength >> 1) << 1;
|
||||
|
||||
if (debugMeshHulls && hullLength > 2)
|
||||
{
|
||||
vt.Position.X = worldVerticesBuffer[0];
|
||||
vt.Position.Y = worldVerticesBuffer[1];
|
||||
lineVertices.Append(vt);
|
||||
|
||||
for (int i = 2; i < hullLength; i += 2)
|
||||
{
|
||||
vt.Position.X = worldVerticesBuffer[i];
|
||||
vt.Position.Y = worldVerticesBuffer[i + 1];
|
||||
lineVertices.Append(vt);
|
||||
lineVertices.Append(vt);
|
||||
}
|
||||
|
||||
vt.Position.X = worldVerticesBuffer[0];
|
||||
vt.Position.Y = worldVerticesBuffer[1];
|
||||
lineVertices.Append(vt);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (debugBoundingBoxes)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
if (debugPaths)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
if (debugClippings)
|
||||
{
|
||||
SFML.Graphics.Vertex vt = new() { Color = ClippingLineColor };
|
||||
foreach (var slot in skeleton.Slots)
|
||||
{
|
||||
if (slot.Attachment is ClippingAttachment clippingAttachment)
|
||||
{
|
||||
if (clippingAttachment.WorldVerticesLength > worldVerticesBuffer.Length)
|
||||
worldVerticesBuffer = worldVerticesBuffer = new float[clippingAttachment.WorldVerticesLength * 2];
|
||||
|
||||
clippingAttachment.ComputeWorldVertices(slot, worldVerticesBuffer);
|
||||
|
||||
vt.Position.X = worldVerticesBuffer[0];
|
||||
vt.Position.Y = worldVerticesBuffer[1];
|
||||
lineVertices.Append(vt);
|
||||
|
||||
for (int i = 2; i < clippingAttachment.WorldVerticesLength; i += 2)
|
||||
{
|
||||
vt.Position.X = worldVerticesBuffer[i];
|
||||
vt.Position.Y = worldVerticesBuffer[i + 1];
|
||||
lineVertices.Append(vt);
|
||||
lineVertices.Append(vt);
|
||||
}
|
||||
|
||||
vt.Position.X = worldVerticesBuffer[0];
|
||||
vt.Position.Y = worldVerticesBuffer[1];
|
||||
lineVertices.Append(vt);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (debugBounds)
|
||||
{
|
||||
var vt = new SFML.Graphics.Vertex() { Color = BoundsColor };
|
||||
var b = getCurrentBounds();
|
||||
|
||||
vt.Position.X = b.Left;
|
||||
vt.Position.Y = b.Top;
|
||||
lineVertices.Append(vt);
|
||||
|
||||
vt.Position.X = b.Right;
|
||||
vt.Position.Y = b.Top;
|
||||
lineVertices.Append(vt); lineVertices.Append(vt);
|
||||
|
||||
vt.Position.X = b.Right;
|
||||
vt.Position.Y = b.Bottom;
|
||||
lineVertices.Append(vt); lineVertices.Append(vt);
|
||||
|
||||
vt.Position.X = b.Left;
|
||||
vt.Position.Y = b.Bottom;
|
||||
lineVertices.Append(vt); lineVertices.Append(vt);
|
||||
|
||||
vt.Position.X = b.Left;
|
||||
vt.Position.Y = b.Top;
|
||||
lineVertices.Append(vt);
|
||||
}
|
||||
|
||||
// 骨骼线放最后画
|
||||
if (debugBones)
|
||||
{
|
||||
var width = scale;
|
||||
foreach (var bone in skeleton.Bones)
|
||||
{
|
||||
var boneLength = bone.Data.Length;
|
||||
var p1 = new SFML.System.Vector2f(bone.WorldX, bone.WorldY);
|
||||
var p2 = new SFML.System.Vector2f(bone.WorldX + boneLength * bone.A, bone.WorldY + boneLength * bone.C);
|
||||
AddRectLine(p1, p2, BoneLineColor, width);
|
||||
}
|
||||
}
|
||||
|
||||
target.Draw(lineVertices);
|
||||
target.Draw(rectLineVertices);
|
||||
|
||||
// 骨骼的点最后画, 层级处于骨骼线上面
|
||||
if (debugBones)
|
||||
{
|
||||
var radius = scale;
|
||||
foreach (var bone in skeleton.Bones)
|
||||
{
|
||||
DrawCirclePoint(target, new(bone.WorldX, bone.WorldY), BonePointColor, radius);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
584
SpineViewer/Spine/Implementations/SpineObject/SpineObject37.cs
Normal file
584
SpineViewer/Spine/Implementations/SpineObject/SpineObject37.cs
Normal file
@@ -0,0 +1,584 @@
|
||||
using System;
|
||||
using System.Collections.Frozen;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using SpineRuntime37;
|
||||
using SpineViewer.Extensions;
|
||||
using SpineViewer.Utils;
|
||||
|
||||
namespace SpineViewer.Spine.Implementations.SpineObject
|
||||
{
|
||||
[SpineImplementation(SpineVersion.V37)]
|
||||
internal class SpineObject37 : Spine.SpineObject
|
||||
{
|
||||
private static SFML.Graphics.BlendMode GetSFMLBlendMode(BlendMode spineBlendMode)
|
||||
{
|
||||
return spineBlendMode switch
|
||||
{
|
||||
BlendMode.Normal => SFMLBlendMode.NormalPma,
|
||||
BlendMode.Additive => SFMLBlendMode.AdditivePma,
|
||||
BlendMode.Multiply => SFMLBlendMode.MultiplyPma,
|
||||
BlendMode.Screen => SFMLBlendMode.ScreenPma,
|
||||
_ => throw new NotImplementedException($"{spineBlendMode}"),
|
||||
};
|
||||
}
|
||||
|
||||
private class TextureLoader : SpineRuntime37.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;
|
||||
}
|
||||
|
||||
public void Unload(object texture)
|
||||
{
|
||||
((SFML.Graphics.Texture)texture).Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
private static readonly TextureLoader textureLoader = new();
|
||||
private static readonly Animation EmptyAnimation = new(EMPTY_ANIMATION, [], 0);
|
||||
|
||||
private readonly Atlas atlas;
|
||||
private readonly SkeletonBinary? skeletonBinary;
|
||||
private readonly SkeletonJson? skeletonJson;
|
||||
private readonly SkeletonData skeletonData;
|
||||
private readonly AnimationStateData animationStateData;
|
||||
|
||||
private readonly Skeleton skeleton;
|
||||
private readonly AnimationState animationState;
|
||||
|
||||
private readonly SkeletonClipping clipping = new();
|
||||
|
||||
/// <summary>
|
||||
/// 所有插槽在所有皮肤中可用的附件集合
|
||||
/// </summary>
|
||||
private readonly Dictionary<string, Dictionary<string, Attachment>> slotAttachments = [];
|
||||
|
||||
public SpineObject37(string skelPath, string atlasPath) : 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}");
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var sk in skeletonData.Skins)
|
||||
{
|
||||
foreach (var (k, att) in sk.Attachments)
|
||||
{
|
||||
var slotName = skeletonData.Slots.Items[k.slotIndex].Name;
|
||||
if (!slotAttachments.TryGetValue(slotName, out var attachments))
|
||||
slotAttachments[slotName] = attachments = new() { [EMPTY_ATTACHMENT] = null };
|
||||
attachments[att.Name] = att;
|
||||
}
|
||||
}
|
||||
SlotAttachmentNames = slotAttachments.ToFrozenDictionary(item => item.Key, item => item.Value.Keys.ToImmutableArray());
|
||||
SkinNames = skeletonData.Skins.Select(v => v.Name).ToImmutableArray();
|
||||
AnimationNames = [EMPTY_ANIMATION, .. skeletonData.Animations.Select(v => v.Name)];
|
||||
|
||||
skeleton = new Skeleton(skeletonData) { Skin = new(Guid.NewGuid().ToString()) }; // 挂载一个空皮肤当作容器
|
||||
animationStateData = new AnimationStateData(skeletonData);
|
||||
animationState = new AnimationState(animationStateData);
|
||||
}
|
||||
|
||||
protected override void Dispose(bool disposing)
|
||||
{
|
||||
base.Dispose(disposing);
|
||||
atlas.Dispose();
|
||||
}
|
||||
|
||||
public override string FileVersion { get => skeletonData.Version; }
|
||||
|
||||
protected override float scale
|
||||
{
|
||||
get => Math.Abs(skeleton.ScaleX);
|
||||
set
|
||||
{
|
||||
skeleton.ScaleX = Math.Sign(skeleton.ScaleX) * value;
|
||||
skeleton.ScaleY = Math.Sign(skeleton.ScaleY) * value;
|
||||
}
|
||||
}
|
||||
|
||||
protected override PointF position
|
||||
{
|
||||
get => new(skeleton.X, skeleton.Y);
|
||||
set
|
||||
{
|
||||
skeleton.X = value.X;
|
||||
skeleton.Y = value.Y;
|
||||
}
|
||||
}
|
||||
|
||||
protected override bool flipX
|
||||
{
|
||||
get => skeleton.ScaleX < 0;
|
||||
set
|
||||
{
|
||||
if (skeleton.ScaleX > 0 && value || skeleton.ScaleX < 0 && !value)
|
||||
skeleton.ScaleX *= -1;
|
||||
}
|
||||
}
|
||||
|
||||
protected override bool flipY
|
||||
{
|
||||
get => skeleton.ScaleY < 0;
|
||||
set
|
||||
{
|
||||
if (skeleton.ScaleY > 0 && value || skeleton.ScaleY < 0 && !value)
|
||||
skeleton.ScaleY *= -1;
|
||||
}
|
||||
}
|
||||
|
||||
protected override string getSlotAttachment(string slot) => skeleton.FindSlot(slot)?.Attachment?.Name ?? EMPTY_ATTACHMENT;
|
||||
|
||||
protected override void setSlotAttachment(string slot, string name)
|
||||
{
|
||||
if (slotAttachments.TryGetValue(slot, out var attachments)
|
||||
&& attachments.TryGetValue(name, out var att)
|
||||
&& skeleton.FindSlot(slot) is Slot s)
|
||||
s.Attachment = att;
|
||||
}
|
||||
|
||||
protected override void addSkin(string name)
|
||||
{
|
||||
// default 不需要加载
|
||||
if (name != "default" && skeletonData.FindSkin(name) is Skin sk)
|
||||
{
|
||||
// XXX: 3.7 及以下不支持 AddSkin
|
||||
foreach (var (k, v) in sk.Attachments)
|
||||
skeleton.Skin.AddAttachment(k.slotIndex, k.name, v);
|
||||
}
|
||||
skeleton.SetSlotsToSetupPose();
|
||||
}
|
||||
|
||||
protected override void clearSkins()
|
||||
{
|
||||
skeleton.Skin.Attachments.Clear();
|
||||
skeleton.SetSlotsToSetupPose();
|
||||
}
|
||||
|
||||
protected override int[] getTrackIndices() => animationState.Tracks.Select((_, i) => i).Where(i => animationState.Tracks.Items[i] is not null).ToArray();
|
||||
|
||||
protected override string getAnimation(int track) => animationState.GetCurrent(track)?.Animation.Name ?? EMPTY_ANIMATION;
|
||||
|
||||
protected override void setAnimation(int track, string name)
|
||||
{
|
||||
if (name == EMPTY_ANIMATION)
|
||||
animationState.SetAnimation(track, EmptyAnimation, true);
|
||||
else if (AnimationNames.Contains(name))
|
||||
animationState.SetAnimation(track, name, true);
|
||||
}
|
||||
|
||||
protected override void clearTrack(int i) => animationState.ClearTrack(i);
|
||||
|
||||
public override float GetAnimationDuration(string name) { return skeletonData.FindAnimation(name)?.Duration ?? 0f; }
|
||||
|
||||
protected override RectangleF getCurrentBounds()
|
||||
{
|
||||
float[] _ = [];
|
||||
skeleton.GetBounds(out var x, out var y, out var w, out var h, ref _);
|
||||
return new RectangleF(x, y, w, h);
|
||||
}
|
||||
|
||||
protected override RectangleF getBounds()
|
||||
{
|
||||
// 初始化临时对象
|
||||
var maxDuration = 0f;
|
||||
var tmpSkeleton = new Skeleton(skeletonData) { Skin = new(Guid.NewGuid().ToString()) };
|
||||
var tmpAnimationState = new AnimationState(animationStateData);
|
||||
tmpSkeleton.ScaleX = skeleton.ScaleX;
|
||||
tmpSkeleton.ScaleY = skeleton.ScaleY;
|
||||
tmpSkeleton.X = skeleton.X;
|
||||
tmpSkeleton.Y = skeleton.Y;
|
||||
foreach (var (name, _) in skinLoadStatus.Where(e => e.Value))
|
||||
{
|
||||
foreach (var (k, v) in skeletonData.FindSkin(name).Attachments)
|
||||
tmpSkeleton.Skin.AddAttachment(k.slotIndex, k.name, v);
|
||||
}
|
||||
foreach (var tr in animationState.Tracks.Select((_, i) => i).Where(i => animationState.Tracks.Items[i] is not null))
|
||||
{
|
||||
var ani = animationState.GetCurrent(tr).Animation;
|
||||
tmpAnimationState.SetAnimation(tr, ani, true);
|
||||
if (ani.Duration > maxDuration) maxDuration = ani.Duration;
|
||||
}
|
||||
tmpSkeleton.SetSlotsToSetupPose();
|
||||
tmpAnimationState.Update(0);
|
||||
tmpAnimationState.Apply(tmpSkeleton);
|
||||
tmpSkeleton.Update(0);
|
||||
tmpSkeleton.UpdateWorldTransform();
|
||||
|
||||
// 按 10 帧每秒计算边框
|
||||
var bounds = getCurrentBounds();
|
||||
float[] _ = [];
|
||||
for (float tick = 0, delta = 0.1f; tick < maxDuration; tick += delta)
|
||||
{
|
||||
tmpSkeleton.GetBounds(out var x, out var y, out var w, out var h, ref _);
|
||||
bounds = bounds.Union(new(x, y, w, h));
|
||||
tmpAnimationState.Update(delta);
|
||||
tmpAnimationState.Apply(tmpSkeleton);
|
||||
tmpSkeleton.Update(delta);
|
||||
tmpSkeleton.UpdateWorldTransform();
|
||||
}
|
||||
|
||||
return bounds;
|
||||
}
|
||||
|
||||
protected override void update(float delta)
|
||||
{
|
||||
animationState.Update(delta);
|
||||
animationState.Apply(skeleton);
|
||||
skeleton.Update(delta);
|
||||
skeleton.UpdateWorldTransform();
|
||||
}
|
||||
|
||||
protected override void draw(SFML.Graphics.RenderTarget target, SFML.Graphics.RenderStates states)
|
||||
{
|
||||
triangleVertices.Clear();
|
||||
states.Texture = null;
|
||||
states.Shader = SFMLShader.GetSpineShader(usePma);
|
||||
|
||||
// 要用 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, 0);
|
||||
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.WorldVerticesLength > worldVertices.Length)
|
||||
worldVertices = worldVerticesBuffer = new float[meshAttachment.WorldVerticesLength * 2];
|
||||
meshAttachment.ComputeWorldVertices(slot, worldVertices);
|
||||
worldVerticesCount = meshAttachment.WorldVerticesLength / 2;
|
||||
worldTriangleIndices = meshAttachment.Triangles;
|
||||
worldTriangleIndicesLength = meshAttachment.Triangles.Length;
|
||||
uvs = meshAttachment.UVs;
|
||||
tintR *= meshAttachment.R;
|
||||
tintG *= meshAttachment.G;
|
||||
tintB *= meshAttachment.B;
|
||||
tintA *= meshAttachment.A;
|
||||
}
|
||||
else if (attachment is ClippingAttachment clippingAttachment)
|
||||
{
|
||||
clipping.ClipStart(slot, clippingAttachment);
|
||||
continue;
|
||||
}
|
||||
else
|
||||
{
|
||||
clipping.ClipEnd(slot);
|
||||
continue;
|
||||
}
|
||||
|
||||
SFML.Graphics.BlendMode blendMode = GetSFMLBlendMode(slot.Data.BlendMode);
|
||||
|
||||
states.Texture ??= texture;
|
||||
if (states.BlendMode != blendMode || states.Texture != texture)
|
||||
{
|
||||
if (triangleVertices.VertexCount > 0)
|
||||
{
|
||||
target.Draw(triangleVertices, states);
|
||||
triangleVertices.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;
|
||||
triangleVertices.Append(vertex);
|
||||
}
|
||||
|
||||
clipping.ClipEnd(slot);
|
||||
}
|
||||
clipping.ClipEnd();
|
||||
|
||||
target.Draw(triangleVertices, states);
|
||||
}
|
||||
|
||||
protected override void debugDraw(SFML.Graphics.RenderTarget target)
|
||||
{
|
||||
lineVertices.Clear();
|
||||
rectLineVertices.Clear();
|
||||
|
||||
if (debugRegions)
|
||||
{
|
||||
SFML.Graphics.Vertex vt = new() { Color = AttachmentLineColor };
|
||||
foreach (var slot in skeleton.Slots)
|
||||
{
|
||||
if (slot.Attachment is RegionAttachment regionAttachment)
|
||||
{
|
||||
regionAttachment.ComputeWorldVertices(slot.Bone, worldVerticesBuffer, 0);
|
||||
|
||||
vt.Position.X = worldVerticesBuffer[0];
|
||||
vt.Position.Y = worldVerticesBuffer[1];
|
||||
lineVertices.Append(vt);
|
||||
|
||||
vt.Position.X = worldVerticesBuffer[2];
|
||||
vt.Position.Y = worldVerticesBuffer[3];
|
||||
lineVertices.Append(vt); lineVertices.Append(vt);
|
||||
|
||||
vt.Position.X = worldVerticesBuffer[4];
|
||||
vt.Position.Y = worldVerticesBuffer[5];
|
||||
lineVertices.Append(vt); lineVertices.Append(vt);
|
||||
|
||||
vt.Position.X = worldVerticesBuffer[6];
|
||||
vt.Position.Y = worldVerticesBuffer[7];
|
||||
lineVertices.Append(vt); lineVertices.Append(vt);
|
||||
|
||||
vt.Position.X = worldVerticesBuffer[0];
|
||||
vt.Position.Y = worldVerticesBuffer[1];
|
||||
lineVertices.Append(vt);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (debugMeshes)
|
||||
{
|
||||
SFML.Graphics.Vertex vt = new() { Color = MeshLineColor };
|
||||
foreach (var slot in skeleton.Slots)
|
||||
{
|
||||
if (slot.Attachment is MeshAttachment meshAttachment)
|
||||
{
|
||||
if (meshAttachment.WorldVerticesLength > worldVerticesBuffer.Length)
|
||||
worldVerticesBuffer = new float[meshAttachment.WorldVerticesLength * 2];
|
||||
|
||||
meshAttachment.ComputeWorldVertices(slot, worldVerticesBuffer);
|
||||
|
||||
var triangleIndices = meshAttachment.Triangles;
|
||||
for (int i = 0; i < triangleIndices.Length; i += 3)
|
||||
{
|
||||
var idx0 = triangleIndices[i] * 2;
|
||||
var idx1 = triangleIndices[i + 1] * 2;
|
||||
var idx2 = triangleIndices[i + 2] * 2;
|
||||
|
||||
vt.Position.X = worldVerticesBuffer[idx0];
|
||||
vt.Position.Y = worldVerticesBuffer[idx0 + 1];
|
||||
lineVertices.Append(vt);
|
||||
|
||||
vt.Position.X = worldVerticesBuffer[idx1];
|
||||
vt.Position.Y = worldVerticesBuffer[idx1 + 1];
|
||||
lineVertices.Append(vt); lineVertices.Append(vt);
|
||||
|
||||
vt.Position.X = worldVerticesBuffer[idx2];
|
||||
vt.Position.Y = worldVerticesBuffer[idx2 + 1];
|
||||
lineVertices.Append(vt); lineVertices.Append(vt);
|
||||
|
||||
vt.Position.X = worldVerticesBuffer[idx0];
|
||||
vt.Position.Y = worldVerticesBuffer[idx0 + 1];
|
||||
lineVertices.Append(vt);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (debugMeshHulls)
|
||||
{
|
||||
SFML.Graphics.Vertex vt = new() { Color = AttachmentLineColor };
|
||||
foreach (var slot in skeleton.Slots)
|
||||
{
|
||||
if (slot.Attachment is MeshAttachment meshAttachment)
|
||||
{
|
||||
if (meshAttachment.WorldVerticesLength > worldVerticesBuffer.Length)
|
||||
worldVerticesBuffer = new float[meshAttachment.WorldVerticesLength * 2];
|
||||
|
||||
meshAttachment.ComputeWorldVertices(slot, worldVerticesBuffer);
|
||||
|
||||
var hullLength = (meshAttachment.HullLength >> 1) << 1;
|
||||
|
||||
if (debugMeshHulls && hullLength > 2)
|
||||
{
|
||||
vt.Position.X = worldVerticesBuffer[0];
|
||||
vt.Position.Y = worldVerticesBuffer[1];
|
||||
lineVertices.Append(vt);
|
||||
|
||||
for (int i = 2; i < hullLength; i += 2)
|
||||
{
|
||||
vt.Position.X = worldVerticesBuffer[i];
|
||||
vt.Position.Y = worldVerticesBuffer[i + 1];
|
||||
lineVertices.Append(vt);
|
||||
lineVertices.Append(vt);
|
||||
}
|
||||
|
||||
vt.Position.X = worldVerticesBuffer[0];
|
||||
vt.Position.Y = worldVerticesBuffer[1];
|
||||
lineVertices.Append(vt);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (debugBoundingBoxes)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
if (debugPaths)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
if (debugClippings)
|
||||
{
|
||||
SFML.Graphics.Vertex vt = new() { Color = ClippingLineColor };
|
||||
foreach (var slot in skeleton.Slots)
|
||||
{
|
||||
if (slot.Attachment is ClippingAttachment clippingAttachment)
|
||||
{
|
||||
if (clippingAttachment.WorldVerticesLength > worldVerticesBuffer.Length)
|
||||
worldVerticesBuffer = worldVerticesBuffer = new float[clippingAttachment.WorldVerticesLength * 2];
|
||||
|
||||
clippingAttachment.ComputeWorldVertices(slot, worldVerticesBuffer);
|
||||
|
||||
vt.Position.X = worldVerticesBuffer[0];
|
||||
vt.Position.Y = worldVerticesBuffer[1];
|
||||
lineVertices.Append(vt);
|
||||
|
||||
for (int i = 2; i < clippingAttachment.WorldVerticesLength; i += 2)
|
||||
{
|
||||
vt.Position.X = worldVerticesBuffer[i];
|
||||
vt.Position.Y = worldVerticesBuffer[i + 1];
|
||||
lineVertices.Append(vt);
|
||||
lineVertices.Append(vt);
|
||||
}
|
||||
|
||||
vt.Position.X = worldVerticesBuffer[0];
|
||||
vt.Position.Y = worldVerticesBuffer[1];
|
||||
lineVertices.Append(vt);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (debugBounds)
|
||||
{
|
||||
var vt = new SFML.Graphics.Vertex() { Color = BoundsColor };
|
||||
var b = getCurrentBounds();
|
||||
|
||||
vt.Position.X = b.Left;
|
||||
vt.Position.Y = b.Top;
|
||||
lineVertices.Append(vt);
|
||||
|
||||
vt.Position.X = b.Right;
|
||||
vt.Position.Y = b.Top;
|
||||
lineVertices.Append(vt); lineVertices.Append(vt);
|
||||
|
||||
vt.Position.X = b.Right;
|
||||
vt.Position.Y = b.Bottom;
|
||||
lineVertices.Append(vt); lineVertices.Append(vt);
|
||||
|
||||
vt.Position.X = b.Left;
|
||||
vt.Position.Y = b.Bottom;
|
||||
lineVertices.Append(vt); lineVertices.Append(vt);
|
||||
|
||||
vt.Position.X = b.Left;
|
||||
vt.Position.Y = b.Top;
|
||||
lineVertices.Append(vt);
|
||||
}
|
||||
|
||||
// 骨骼线放最后画
|
||||
if (debugBones)
|
||||
{
|
||||
var width = scale;
|
||||
foreach (var bone in skeleton.Bones)
|
||||
{
|
||||
var boneLength = bone.Data.Length;
|
||||
var p1 = new SFML.System.Vector2f(bone.WorldX, bone.WorldY);
|
||||
var p2 = new SFML.System.Vector2f(bone.WorldX + boneLength * bone.A, bone.WorldY + boneLength * bone.C);
|
||||
AddRectLine(p1, p2, BoneLineColor, width);
|
||||
}
|
||||
}
|
||||
|
||||
target.Draw(lineVertices);
|
||||
target.Draw(rectLineVertices);
|
||||
|
||||
// 骨骼的点最后画, 层级处于骨骼线上面
|
||||
if (debugBones)
|
||||
{
|
||||
var radius = scale;
|
||||
foreach (var bone in skeleton.Bones)
|
||||
{
|
||||
DrawCirclePoint(target, new(bone.WorldX, bone.WorldY), BonePointColor, radius);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user