Compare commits

..

219 Commits

Author SHA1 Message Date
ww-rm
750a8b8aff 更新至v0.12.5 2025-04-19 01:32:29 +08:00
ww-rm
cd86155878 修复问题 2025-04-19 01:32:19 +08:00
ww-rm
16739c39d6 修复小bug 2025-04-19 00:41:40 +08:00
ww-rm
c7971a9829 update readme 2025-04-19 00:19:56 +08:00
ww-rm
44c4fc4b21 update preview 2025-04-19 00:19:47 +08:00
ww-rm
6f1c8e3320 增加槽位属性面板 2025-04-19 00:12:27 +08:00
ww-rm
8f818416ba 优化缓存字典读取 2025-04-18 23:55:08 +08:00
ww-rm
de6858ca48 增加GetSlotAttachment/SetSlotAttachment 2025-04-18 23:15:46 +08:00
ww-rm
3fd3d2a378 增加SFMLColorConverter属性描述符缓存 2025-04-18 22:37:46 +08:00
ww-rm
706c9125e6 修改皮肤设置方式为GetSkinStatus/SetSkinStatus 2025-04-18 21:56:50 +08:00
ww-rm
5f026b000c 修改皮肤设置操作为布尔型 2025-04-18 21:23:26 +08:00
ww-rm
0b0d036f08 增加SlotAttachmentNames 2025-04-18 19:31:33 +08:00
ww-rm
6b9017d535 修改名字 2025-04-18 14:50:27 +08:00
ww-rm
5eb47e33ac 修正名字 2025-04-18 14:48:08 +08:00
ww-rm
4d31335da0 修复缩放之后皮肤null引用错误 2025-04-18 11:14:32 +08:00
ww-rm
0b5e76a448 增强分辨率缓存 2025-04-18 00:09:17 +08:00
ww-rm
775268c01a 修复包围盒并集错误 2025-04-17 20:29:20 +08:00
ww-rm
b0b1c85047 更新注释和文本描述 2025-04-17 20:10:59 +08:00
ww-rm
5f08fc6695 更新至v0.12.4 2025-04-17 20:06:52 +08:00
ww-rm
2de3bdf12b 同步重命名 2025-04-17 20:06:38 +08:00
ww-rm
3a424c7dc1 update readme 2025-04-17 20:04:06 +08:00
ww-rm
c3e2b37072 update changelog 2025-04-17 20:03:46 +08:00
ww-rm
65bd11a346 增加自动分辨率 2025-04-17 20:00:15 +08:00
ww-rm
e6e7fc539f 修复Union错误 2025-04-17 19:58:14 +08:00
ww-rm
6522d415b7 增加AllowContentOverflow参数 2025-04-17 16:31:29 +08:00
ww-rm
378c66a333 small change 2025-04-17 16:30:45 +08:00
ww-rm
07204417a5 增加SetViewport 2025-04-17 16:30:27 +08:00
ww-rm
c9c909cdf9 增加padding和margin参数 2025-04-17 00:09:04 +08:00
ww-rm
a9f59a4d2f 增加对话框高度 2025-04-16 22:35:35 +08:00
ww-rm
1d2513cef5 增加padding和margin参数 2025-04-16 22:28:35 +08:00
ww-rm
febb797ae2 增加GetResolutionBounds方法 2025-04-16 21:37:09 +08:00
ww-rm
68d279a7c3 修改缩放公式 2025-04-16 21:25:26 +08:00
ww-rm
d2d8b7955c 增加GetBounds获取最大包围盒方法 2025-04-15 20:20:18 +08:00
ww-rm
2a55fd9c36 补充3.7及以下版本的多皮肤功能 2025-04-15 20:19:32 +08:00
ww-rm
695d3c0735 增加Attachments公开属性 2025-04-15 20:16:10 +08:00
ww-rm
ce95db469b 增加GetBounds 2025-04-15 20:15:50 +08:00
ww-rm
5d187cf80f 修复Path读取错误 2025-04-15 17:47:37 +08:00
ww-rm
e704ebc224 修正eventTimelines和原逻辑不一致的地方 2025-04-15 15:49:38 +08:00
ww-rm
ee36f8981c 修改GetBounds为GetCurrentBounds 2025-04-15 14:58:25 +08:00
ww-rm
09dd220abf 更改Bounds属性为GetBounds方法 2025-04-15 11:23:11 +08:00
ww-rm
15bc2dc3b8 完善文件转换功能 2025-04-14 23:52:39 +08:00
ww-rm
1deb74eca9 修正某些可能的字符大小写问题 2025-04-14 23:52:05 +08:00
ww-rm
de76ce64ab 增加输出文件夹选项 2025-04-14 23:50:46 +08:00
ww-rm
94b4ba33e6 修正curve读写 2025-04-14 21:48:23 +08:00
ww-rm
7ce8a115f4 修复流写入错误 2025-04-14 20:19:06 +08:00
ww-rm
c036a4bb45 增加v38二进制文件输出 2025-04-14 17:51:16 +08:00
ww-rm
aa62f30b05 增加报错输出 2025-04-14 17:11:56 +08:00
ww-rm
3d967c9812 修改默认打开骨骼文件后缀筛选器 2025-04-13 13:50:31 +08:00
ww-rm
e87e9efb99 补充点附件TODO 2025-04-13 00:37:29 +08:00
ww-rm
8c1f6fb4a6 update preview 2025-04-13 00:25:16 +08:00
ww-rm
df82ed8a00 更新至v0.12.3 2025-04-13 00:22:36 +08:00
ww-rm
d01e3920ba update changelog 2025-04-13 00:22:02 +08:00
ww-rm
777cd5ea3f 增加部分调试渲染功能 2025-04-13 00:19:02 +08:00
ww-rm
3b73aea5c0 增加调试骨骼 2025-04-12 20:58:56 +08:00
ww-rm
168f7a8173 修复maxFps时快进帧除0错误 2025-04-12 20:57:11 +08:00
ww-rm
04437e2de2 修改文本描述 2025-04-12 13:28:12 +08:00
ww-rm
2ec83b2e87 增加ctrl滚轮对模型缩放 2025-04-12 12:58:21 +08:00
ww-rm
90bfaa7b56 整理结构 2025-04-12 11:35:36 +08:00
ww-rm
2ae175abd0 small change 2025-04-11 13:55:19 +08:00
ww-rm
e2a84d8f88 small change 2025-04-11 00:58:25 +08:00
ww-rm
b6f9cd0c7c small change 2025-04-11 00:07:09 +08:00
ww-rm
61b7b90722 修改缩放模式 2025-04-10 10:27:09 +08:00
ww-rm
093c159753 修改布局 2025-04-10 10:21:31 +08:00
ww-rm
32d36c0757 补充遗漏参数项 2025-04-09 15:56:51 +08:00
ww-rm
94dabebf2b 修正文字说明 2025-04-09 15:34:03 +08:00
ww-rm
8e875d4f7e update changelog 2025-04-09 15:25:15 +08:00
ww-rm
86c383f2cf 默认使用libwebp_anim编码器 2025-04-09 15:24:48 +08:00
ww-rm
b404d8e79a 增加最后一帧参数 2025-04-09 15:24:31 +08:00
ww-rm
d32b480ef2 更新readme 2025-04-09 14:00:26 +08:00
ww-rm
3654825f27 补充文字说明 2025-04-09 13:52:59 +08:00
ww-rm
d7231e8a09 更新至版本v0.12.2 2025-04-09 13:43:21 +08:00
ww-rm
98161aaf2e update changelog 2025-04-09 13:40:34 +08:00
ww-rm
7a942b16bc 调整动图默认帧率 2025-04-09 13:40:11 +08:00
ww-rm
067719c69b 补充面板属性刷新 2025-04-09 13:36:34 +08:00
ww-rm
f3fce53b91 增加导出参数缓存 2025-04-09 13:19:25 +08:00
ww-rm
e35903f436 增加webp和avif动图格式 2025-04-09 13:19:17 +08:00
ww-rm
64cfe5fdd7 限定readonly 2025-04-09 01:09:57 +08:00
ww-rm
dbe586cff8 调整结构 2025-04-09 01:07:17 +08:00
ww-rm
3104733db0 补充调试参数 2025-04-08 15:00:42 +08:00
ww-rm
9d4bdd1028 标题栏增加版本号 2025-04-08 13:51:10 +08:00
ww-rm
f8030b1645 整合参数标签页 2025-04-08 13:44:30 +08:00
ww-rm
0a999ceb41 增加 SpinePropertyGrid 2025-04-08 13:21:29 +08:00
ww-rm
64bd9907cb 重构 2025-04-07 15:06:23 +08:00
ww-rm
580eaf990d 测试代码 2025-04-05 11:57:44 +08:00
ww-rm
5ab232a961 更新至v0.12.1 2025-04-05 11:56:53 +08:00
ww-rm
e596cd7ea4 update changelog 2025-04-05 11:56:13 +08:00
ww-rm
05c47a4daa 增加初始动画皮肤空位 2025-04-05 11:52:34 +08:00
ww-rm
5a8783b5f4 增加确定按钮 2025-04-05 11:18:54 +08:00
ww-rm
08bc171a72 修复分辨率调整时父容器尺寸获取错误bug 2025-04-05 10:38:17 +08:00
ww-rm
7372f5fe08 optimize 2025-04-05 10:31:30 +08:00
ww-rm
6f032bdd05 optimize 2025-04-05 10:10:04 +08:00
ww-rm
153d3603d2 optimize 2025-04-05 09:40:02 +08:00
ww-rm
95261e6907 optimize 2025-04-05 01:53:39 +08:00
ww-rm
17b344376d update readme 2025-04-05 01:39:59 +08:00
ww-rm
0ed4e44878 更新至v0.12.0 2025-04-05 01:22:14 +08:00
ww-rm
b42c1832f0 update changelog 2025-04-05 01:16:00 +08:00
ww-rm
058534ba67 修复部分资源泄漏 2025-04-05 01:14:59 +08:00
ww-rm
204dcd6498 增加3.8及以上版本多皮肤支持 2025-04-05 00:57:04 +08:00
ww-rm
2c846c0db9 补充update0 2025-04-04 20:10:29 +08:00
ww-rm
2faeb044e0 增加 readonly 限定 2025-04-04 17:36:52 +08:00
ww-rm
09c8e4f779 修复导出过程中的PMA问题 2025-04-04 17:21:30 +08:00
ww-rm
6994fa6be8 修复shader问题 2025-04-04 11:26:23 +08:00
ww-rm
cc7beb7670 增加SFMLExtension 2025-04-03 20:14:13 +08:00
ww-rm
510653732d 移除冗余引用 2025-04-03 19:46:01 +08:00
ww-rm
93e8178d67 增加部分实验性样例代码 2025-04-03 19:39:13 +08:00
ww-rm
cebc4864cc 封装原生操作 2025-04-03 18:22:55 +08:00
ww-rm
6ad0449376 修复内存泄漏 2025-04-03 18:19:12 +08:00
ww-rm
c33c977326 update changelog 2025-04-03 15:02:18 +08:00
ww-rm
f0299d365a 补充update0 2025-04-03 09:55:51 +08:00
ww-rm
6ecdca73f5 不提供时长时按所有轨道动画时长最大值导出 2025-04-03 00:30:55 +08:00
ww-rm
af6a709b2c 完善动画时间重置功能 2025-04-03 00:30:33 +08:00
ww-rm
d5c27450ef 修改pma默认值为false 2025-04-03 00:15:44 +08:00
ww-rm
d10269fb07 small change 2025-04-03 00:13:36 +08:00
ww-rm
53d987476e 增加多轨道动画编辑 2025-04-02 23:59:18 +08:00
ww-rm
8b7866d37f 增加多轨方法 2025-04-02 11:43:25 +08:00
ww-rm
bb529729b6 增加Tracks公开属性 2025-04-02 11:41:23 +08:00
ww-rm
b7735d9ba8 改成并集动画皮肤列表 2025-04-01 21:07:52 +08:00
ww-rm
ce744e2b84 update badges 2025-03-31 22:56:50 +08:00
ww-rm
631c92da3f update readme 2025-03-31 22:18:06 +08:00
ww-rm
b7063804e9 add badges 2025-03-31 22:04:31 +08:00
ww-rm
75d47c8419 修改提示文本 2025-03-31 20:55:37 +08:00
ww-rm
114fb05e80 调整布局 2025-03-31 19:28:32 +08:00
ww-rm
1fec65b37d 更新至v0.11.5 2025-03-31 17:42:54 +08:00
ww-rm
9498e8f334 update readme 2025-03-31 17:42:44 +08:00
ww-rm
83b8411929 update changelog 2025-03-31 17:34:48 +08:00
ww-rm
e9accd13b3 增加所有导出格式 2025-03-31 17:30:20 +08:00
ww-rm
9e27a19258 允许多标记 2025-03-31 14:37:47 +08:00
ww-rm
252f3a5bea 优化显示 2025-03-31 02:07:02 +08:00
ww-rm
e0626bb126 增加项数显示 2025-03-31 01:58:30 +08:00
ww-rm
7ff62c7f40 增加错误日志 2025-03-31 01:45:16 +08:00
ww-rm
4b07e02acb 增加线程安全 2025-03-31 01:44:41 +08:00
ww-rm
4654d1d9c2 优化多项操作卡顿问题 2025-03-30 20:52:22 +08:00
ww-rm
ce1f75e8a5 增加报错调试 2025-03-30 20:07:29 +08:00
ww-rm
4d9aebc758 修复预览图不显示问题 2025-03-30 20:06:13 +08:00
ww-rm
e814368ef3 移除rid 2025-03-30 19:30:58 +08:00
ww-rm
bbbb02500f 增加StringEnumConverter 2025-03-30 17:26:17 +08:00
ww-rm
404f255f14 update readme 2025-03-30 15:22:48 +08:00
ww-rm
7a15e0d38a 隐藏不可见成员 2025-03-30 13:54:17 +08:00
ww-rm
bfe669bdd9 增加FFMpegCore版本信息 2025-03-30 12:07:28 +08:00
ww-rm
c0553042fd 更新至v0.11.4 2025-03-30 11:59:52 +08:00
ww-rm
af8b02654b update readme 2025-03-30 11:59:37 +08:00
ww-rm
4779ec91d0 update changelog 2025-03-30 11:59:13 +08:00
ww-rm
14d7f4af0e 增加MP4导出格式 2025-03-30 11:56:20 +08:00
ww-rm
f9888b23dd 设置GIF默认背景颜色为纯白透明背景 2025-03-30 11:56:06 +08:00
ww-rm
411cdbb00f 设置默认颜色为纯黑透明背景 2025-03-30 11:55:52 +08:00
ww-rm
d859f07469 增加导出时输出ffmpeg参数 2025-03-29 23:48:56 +08:00
ww-rm
c111819093 增加背景颜色参数 2025-03-29 22:17:08 +08:00
ww-rm
aa8321d13c 整理代码结构 2025-03-29 21:15:34 +08:00
ww-rm
5e3bd972e5 移动GetVersion至SpineHelper 2025-03-29 17:04:26 +08:00
ww-rm
ad39a04fff 重命名SpineVersion 2025-03-29 16:59:28 +08:00
ww-rm
9a97e84296 解耦对MessageBox的依赖,提供单独的Shader初始化函数 2025-03-29 16:51:05 +08:00
ww-rm
1b7b0dcb13 解耦日志器 2025-03-29 16:30:32 +08:00
ww-rm
d365a5060b small change 2025-03-29 15:34:56 +08:00
ww-rm
b69589394a 提取ImplementationResolver实现 2025-03-29 15:12:50 +08:00
ww-rm
00f5791766 增加导出时任务栏图标显示 2025-03-28 20:53:48 +08:00
ww-rm
38cab2eda7 修复可能的预览图资源泄漏 2025-03-27 23:31:03 +08:00
ww-rm
0db4d6e4e0 small change 2025-03-27 19:45:56 +08:00
ww-rm
549712962f 去除多余组件 2025-03-27 10:11:44 +08:00
ww-rm
34b7002faf 增加背景颜色选项 2025-03-27 10:08:16 +08:00
ww-rm
0e6f47b23c 预修改适配对多轨道动画 2025-03-27 09:56:21 +08:00
ww-rm
a372a89b5e 增加update0 2025-03-27 09:09:22 +08:00
ww-rm
239847aee7 皮肤更换后使用SetSlotsToSetupPose而不是SetToSetupPose 2025-03-27 09:03:09 +08:00
ww-rm
813249c6a7 调整布局 2025-03-26 21:09:52 +08:00
ww-rm
293ab28bce 更新预览图 2025-03-26 20:49:22 +08:00
ww-rm
98e73cdec5 调整面板比例 2025-03-26 20:48:23 +08:00
ww-rm
6d34bb9d25 移除无用引用 2025-03-26 20:37:27 +08:00
ww-rm
479a5e4da9 更新至v0.11.3 2025-03-26 20:33:48 +08:00
ww-rm
4829454877 update changelog 2025-03-26 20:33:31 +08:00
ww-rm
28664f6387 增加隐藏控制 2025-03-26 20:30:55 +08:00
ww-rm
1a08a23a9c 批量添加完成自动选中最后一项 2025-03-26 20:24:27 +08:00
ww-rm
16f344ff1b 增加纹理调试 2025-03-26 19:55:43 +08:00
ww-rm
693ce0e2e8 调整属性分组和注释 2025-03-26 19:52:29 +08:00
ww-rm
e6f533ea65 优化属性分组显示顺序 2025-03-26 18:54:35 +08:00
ww-rm
fcc21d63b0 优化排列顺序 2025-03-26 18:39:30 +08:00
ww-rm
afc0ffcb67 Merge branch 'main' of github.com:ww-rm/SpineViewer 2025-03-26 18:25:56 +08:00
ww-rm
9ffb9840e1 去除限制 2025-03-26 18:25:48 +08:00
ww-rm
4766ccf1b6 互换模型和画面参数面板位置 2025-03-26 18:23:59 +08:00
ww-rm
16b75c80a3 Update README.en.md 2025-03-26 17:00:08 +08:00
ww-rm
880f063046 优化分割条可感知宽度 2025-03-26 16:20:12 +08:00
ww-rm
723c11b886 更新至v0.11.2 2025-03-26 15:54:19 +08:00
ww-rm
5e074b1cf7 update changelog 2025-03-26 15:54:06 +08:00
ww-rm
71d2fee36e 修复纹理加载异常 2025-03-26 15:53:27 +08:00
ww-rm
7dc701464f 优化缩放实现 2025-03-26 15:43:54 +08:00
ww-rm
fd876ef90f 补充皮肤切换后的刷新 2025-03-26 15:25:55 +08:00
ww-rm
0597852178 增加皮肤属性 2025-03-26 15:20:59 +08:00
ww-rm
81b1333091 small change 2025-03-26 15:20:49 +08:00
ww-rm
7baebd79a6 update changelog 2025-03-26 14:32:07 +08:00
ww-rm
951d0e30ae 更新至v0.11.1 2025-03-26 14:28:57 +08:00
ww-rm
711e172769 优化显示 2025-03-26 13:57:08 +08:00
ww-rm
faa60f0ea1 update readme 2025-03-26 13:37:12 +08:00
ww-rm
99d81c4329 增加GIF导出格式 2025-03-26 13:10:51 +08:00
ww-rm
17904326f3 修复tex跨线程问题 2025-03-26 13:10:45 +08:00
ww-rm
5ee74f39d8 增加逐个导出时使用自动时长 2025-03-26 13:07:23 +08:00
ww-rm
72f898ed60 增加导出事件绑定 2025-03-26 10:30:57 +08:00
ww-rm
157eab5bac 修复Update顺序错误 2025-03-26 10:08:56 +08:00
ww-rm
e1b0d0a2ad small change 2025-03-26 02:53:09 +08:00
ww-rm
2c050ba031 update readme 2025-03-26 02:51:02 +08:00
ww-rm
41518b16b4 更新至v0.11.0 2025-03-26 02:45:01 +08:00
ww-rm
72a16dc95f 导出单个时也在子目录输出 2025-03-26 02:44:30 +08:00
ww-rm
3404c64f55 update changelog 2025-03-26 02:40:21 +08:00
ww-rm
b9015422f8 增加注释 2025-03-26 02:37:37 +08:00
ww-rm
a7441b968d 增加快进功能 2025-03-26 02:31:07 +08:00
ww-rm
2d44be31f7 优化Bitmap获取过程 2025-03-25 23:34:35 +08:00
ww-rm
c2cf25bb2b 统一导出类结构 2025-03-25 23:25:04 +08:00
ww-rm
7c4c53dcb0 简化时间标记 2025-03-25 18:46:17 +08:00
ww-rm
aceb3b17c8 统一调用 2025-03-25 18:42:24 +08:00
ww-rm
adfcfdb1de 优化显示 2025-03-25 11:24:37 +08:00
ww-rm
da329723bc 修改提示弹窗 2025-03-25 10:49:36 +08:00
ww-rm
63eb53fa06 调整命名空间 2025-03-25 00:53:32 +08:00
ww-rm
d32c824515 设置只读参数 2025-03-25 00:31:30 +08:00
ww-rm
e9ee8c481c 去除多余注释 2025-03-25 00:21:07 +08:00
ww-rm
6d78e52605 增加开始暂停图标按钮 2025-03-25 00:08:50 +08:00
ww-rm
90136a5562 重构导出 2025-03-24 23:05:01 +08:00
ww-rm
1592767c8c 增加注释 2025-03-24 21:14:10 +08:00
ww-rm
afa6ce2113 增加画面开始暂停 2025-03-24 21:01:07 +08:00
ww-rm
50e6e414ee 调整文件夹结构 2025-03-24 18:49:36 +08:00
ww-rm
ba9b8edcdc 增加文件夹路径编辑器 2025-03-24 18:49:01 +08:00
ww-rm
d7a927475c 修改父类 2025-03-24 18:48:16 +08:00
ww-rm
afe210343f 增加适合模型文件的UIEditor 2025-03-24 18:43:51 +08:00
107 changed files with 11593 additions and 8446 deletions

View File

@@ -1,5 +1,85 @@
# CHANGELOG
## 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
- 预览图导出增加名称后缀参数

View File

@@ -1,92 +1,106 @@
# [SpineViewer](https://github.com/ww-rm/SpineViewer)
[![Build and Release](https://github.com/ww-rm/SpineViewer/actions/workflows/dotnet-desktop.yml/badge.svg)](https://github.com/ww-rm/SpineViewer/actions/workflows/dotnet-desktop.yml)
[![GitHub Release](https://img.shields.io/github/v/release/ww-rm/SpineViewer?logo=github&logoColor=959da5&label=Release&labelColor=3f4850)](https://github.com/ww-rm/SpineViewer/releases)
[![Downloads](https://img.shields.io/github/downloads/ww-rm/SpineViewer/total?logo=github&logoColor=959da5&label=Downloads&labelColor=3f4850)](https://github.com/ww-rm/SpineViewer/releases)
[中文](README.md) | [English](README.en.md)
A simple and user-friendly tool for viewing and exporting Spine files.
*A WYSIWYG Spine file viewer and exporter.*
![previewer](img/preview.webp)
---
## Installation
Go to the [Release](https://github.com/ww-rm/SpineViewer/releases) page to download the compressed package.
The software requires the dependency framework [.NET Desktop Runtime 8.0.x](https://dotnet.microsoft.com/zh-cn/download/dotnet/8.0).
You can also download the package with the `SelfContained` suffix, which can run independently.
## Version Support
| Version | View & Export | Format Conversion | Version Conversion |
| :-------: | :--------------------: | :--------------------: | :----------------: |
| `2.1.x` | :white_check_mark: | | |
| `3.1.x` | | | |
| `3.4.x` | | | |
| `3.5.x` | | | |
| `3.6.x` | :white_check_mark: | | |
| `3.7.x` | :white_check_mark: | | |
| `3.8.x` | :white_check_mark: | :white_check_mark: | |
| `4.1.x` | :white_check_mark: | | |
| `4.2.x` | :white_check_mark: | | |
| `4.3.x` | | | |
## Usage
### Importing Skeletons
There are 3 ways to import skeleton files:
- **Drag & Drop/Paste:**
Drag and drop or paste the skeleton file/directory into the model list.
This method automatically searches through the provided files and subdirectories. Although convenient, it relies on the file structure and has its limitations.
- Only standard files with `*.json`, `*.skel`, or `.atlas` extensions are automatically detected.
- The skeleton file and atlas file must have the same name.
- The version string in the skeleton file must not be modified.
- **Batch Open from the File Menu:**
This method offers more file flexibility. You can drag and drop or paste files into the file selection dialog, and additional options are available.
- The filename restrictions are similar to the above, but you can use the panels file selection button to choose skeleton files with non-standard extensions.
- You can set a fixed load version to handle cases where the version number has been modified.
- **Open a Single Model:**
This method offers the highest degree of freedom, allowing you to select any skeleton file and atlas file without filename restrictions. You can also set the load version.
### Adjusting Preview Content
The model list supports right-click menus and various shortcut keys, and you can select multiple models to adjust their parameters in bulk.
In addition to the parameter panel, the preview area supports several mouse actions:
- **Left-click:** Select and drag models. Holding down the `Ctrl` key enables multi-selection, which syncs with the model list.
- **Right-click:** Drag the overall canvas.
- **Scroll wheel:** Zoom the view.
- **Selective Rendering:** The preview area supports a mode to render only the selected models. In this mode, only the selected models are displayed, and selection changes must be made through the model list.
In the function menu, you can reset and synchronize the animation time for all skeletons.
### Exporting Preview Content
Both preview images and videos can be exported.
- **Preview Image:**
The exported preview image shows the model in its default state, with one image per model.
- **Video (TODO: Currently only supports frame sequence export):**
The complete animation duration for each skeleton can be viewed in the model parameters.
When the preview area is set to render only the selected models, the exported content will include only the models that are displayed.
### Format & Version Conversion
You can use the tools menu to convert skeleton files. This feature supports conversion between binary and text formats, as well as between different versions.
Currently under development, it only supports converting `3.8.x` binary format to text format.
:sparkles: v0.12.x New Feature: Support for multi-track animations and multi-skin list management :sparkles:
---
*If you like this project, please give it a :star: and share it with more people! :)*
## Installation
Head over to the [Release](https://github.com/ww-rm/SpineViewer/releases) page to download the zip package.
The software requires the dependency framework [.NET Desktop Runtime 8.0.x](https://dotnet.microsoft.com/en-us/download/dotnet/8.0).
Alternatively, you can download the package with the `SelfContained` suffix, which can run independently.
Exporting video formats such as GIF requires that ffmpeg is installed locally and added to your systems PATH. You can [click here to go to the FFmpeg-Windows download page](https://ffmpeg.org/download.html#build-windows) or directly download the latest version [ffmpeg-release-full.7z](https://www.gyan.dev/ffmpeg/builds/ffmpeg-release-full.7z).
## Supported Export Formats
| Export Format | Suitable for Scenario |
| ------------ | ------------------------------------------------------------------------------------|
| Single Frame | Supports generating high-definition model snapshots; you can manually adjust the frame. |
| Frame Sequence | Supports png sequence output with transparency and lossless compression. |
| GIF | Ideal for generating preview animations. |
| MP4 | The most common video format with the best compatibility. |
| WebM | Suitable for browser-based playback and supports transparent backgrounds. |
| MKV | For more experimental use. |
| MOV | For more experimental use. |
| Custom Export | In addition to the above presets, you can provide any FFmpeg parameters to meet complex custom needs. |
## Supported Spine Versions
| Version | View & Export | Format Conversion | Version Conversion |
| :------: | :-------------------: | :------------------: | :-----------------: |
| `2.1.x` | :white_check_mark: | | |
| `3.1.x` | | | |
| `3.4.x` | | | |
| `3.5.x` | | | |
| `3.6.x` | :white_check_mark: | | |
| `3.7.x` | :white_check_mark: | | |
| `3.8.x` | :white_check_mark: | :white_check_mark: | |
| `4.1.x` | :white_check_mark: | | |
| `4.2.x` | :white_check_mark: | | |
| `4.3.x` | | | |
More versions are under development :rocket: :rocket: :rocket:
## How to Use
### Importing Skeleton Files
There are three ways to import skeleton files:
- Drag and drop or paste the skeleton file/directory into the model list.
- Batch open skeleton files from the File menu.
- Select a single model to open from the File menu.
### Adjusting the Preview
The model list supports context menus and some shortcuts, and you can multi-select to adjust parameters in bulk.
In addition to using the panel for parameter settings, the preview screen supports several mouse actions:
- Left-click to select and drag models; hold the `Ctrl` key for multi-selection (which is synchronized with the list on the left).
- Right-click to drag the overall view.
- Use the mouse wheel to zoom in and out.
- “Render Selected” mode: in this mode, the preview screen only shows the selected models and the selection state can only be changed from the list on the left.
The buttons below the preview allow you to adjust the timeline, acting as a simple media player.
### Exporting the Preview
Exporting follows the “What You See Is What You Get” principle the preview exactly reflects the output.
There are several key parameters for export:
- Render Selected Only: This option affects both the preview and export. If enabled, only the selected models will be considered during export while ignoring the others.
- Output Folder: This parameter is optional in some cases. If not provided, the output files will be saved in each models own folder; otherwise, all outputs will be saved to the specified folder.
- Single Export: By default, each model is exported separately (i.e., batch operation on the model list). If “Single Export” is selected, all the exported models will be rendered on the same canvas, producing only one output file.
### More Information
For detailed instructions and usage notes, please see the [Wiki](https://github.com/ww-rm/SpineViewer/wiki). If you encounter any issues or bugs, feel free to 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 like this project, please give it a :star: and share it with others!*
[![Stargazers over time](https://starchart.cc/ww-rm/SpineViewer.svg?variant=adaptive)](https://starchart.cc/ww-rm/SpineViewer)

View File

@@ -1,15 +1,21 @@
# [SpineViewer](https://github.com/ww-rm/SpineViewer)
[![Build and Release](https://github.com/ww-rm/SpineViewer/actions/workflows/dotnet-desktop.yml/badge.svg)](https://github.com/ww-rm/SpineViewer/actions/workflows/dotnet-desktop.yml)
[![GitHub Release](https://img.shields.io/github/v/release/ww-rm/SpineViewer?logo=github&logoColor=959da5&label=Release&labelColor=3f4850)](https://github.com/ww-rm/SpineViewer/releases)
[![Downloads](https://img.shields.io/github/downloads/ww-rm/SpineViewer/total?logo=github&logoColor=959da5&label=Downloads&labelColor=3f4850)](https://github.com/ww-rm/SpineViewer/releases)
[中文](README.md) | [English](README.en.md)
一个简单好用的 Spine 文件查看&导出程序.
*所见即所得* 的 Spine 文件查看&导出程序.
![previewer](img/preview.webp)
---
:sparkles: `v0.12.5` 新特性: 支持自定义槽位附件 :sparkles:
---
## 安装
前往 [Release](https://github.com/ww-rm/SpineViewer/releases) 界面下载压缩包.
@@ -18,7 +24,21 @@
也可以下载带有 `SelfContained` 后缀的压缩包, 可以独立运行.
## 版本支持
导出 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).
## 导出格式支持
| 导出格式 | 适用场景 |
| --- | --- |
| 单帧画面 | 支持生成高清模型画面图像, 可手动调节需要的一帧. |
| 帧序列 | 支持 PNG 格式帧序列, 可保留透明通道且无损压缩. |
| GIF/WebP/AVIF | 适合生成预览动图. |
| MP4 | 最常见的视频格式, 兼容性最好. |
| WebM | 适合浏览器在线播放格式, 支持透明背景. |
| MKV/MOV | 适合折腾. |
| 自定义导出 | 除上述预设方案, 支持提供任意 FFmpeg 参数进行导出, 满足自定义复杂需求. |
## Spine 版本支持
| 版本 | 查看&导出 | 格式转换 | 版本转换 |
| :---: | :---: | :---: | :---: |
@@ -33,6 +53,8 @@
| `4.2.x` | :white_check_mark: | | |
| `4.3.x` | | | |
更多版本正在施工 :rocket: :rocket: :rocket:
## 使用方法
### 骨骼导入
@@ -40,21 +62,8 @@
有 3 种模式导入骨骼文件:
- 拖放/粘贴需要导入的骨骼文件/目录到模型列表
这种方式会自动查找传入的文件列表以及目录内的子级文件列表, 虽然方便但是依赖模型文件结构, 限制最多.
- 仅支持自动发现标准的 `*.json`/`*.skel`/`.atlas` 后缀文件.
- 骨骼文件和 atlas 文件需要是同名.
- 需要保证骨骼文件里的版本字符串不是魔改过的.
- 从文件菜单里批量打开骨骼文件
这种方式提供一定程度的文件自由度, 文件选择框里同样支持拖放/粘贴, 但是多一些额外选项.
- 文件名限制条件与上面类似, 但是可以通过面板的选择文件按钮选择非标准后缀的骨骼文件.
- 可以设置固定加载版本, 便于应对魔改过的版本号.
- 选择单个模型打开
这种方式自由度最高, 允许选择任意的骨骼文件和 atlas 文件, 可以没有文件名限制, 并且也可以设置加载版本.
- 从文件菜单选择单个模型打开
### 预览内容调整
@@ -65,26 +74,32 @@
- 左键可以选择和拖拽模型, 按下 `Ctrl` 键可以实现多选, 与左侧列表选择是联动的.
- 右键对整体画面进行拖动.
- 滚轮进行画面缩放.
- 预览画面支持仅渲染选中, 在该模式下, 画面仅显示被选中的模型, 并且只能通过左侧列表改变选中状态.
- 仅渲染选中模式, 在该模式下, 预览画面仅包含被选中的模型, 并且只能通过左侧列表改变选中状态.
在功能菜单中, 可以重置同步所有骨骼动画时间.
预览画面下方按钮支持对画面时间进行调整, 可以当作一个简易的播放器.
### 预览内容导出
支持预览图和视频的导出.
导出遵循 "所见即所得" 原则, 即实时预览的画面就是你导出的画面.
预览图导出的内容是模型的默认状态画面, 每个模型一张单独的预览图.
导出有以下几个关键参数:
视频(TODO: 目前仅支持帧序列导出), 可以在每个骨骼的模型参数中查看动画完整时长.
- 仅渲染选中. 这个参数不仅影响预览模式, 也影响导出, 如果仅渲染选中, 那么在导出时只有被选中的模型会被考虑, 忽略其他模型.
- 输出文件夹. 这个参数某些时候可选, 当不提供时, 则将输出产物输出到每个模型各自的模型文件夹, 否则输出产物全部输出到提供的输出文件夹.
- 导出单个. 默认是每个模型独立导出, 即对模型列表进行批量操作, 如果选择仅导出单个, 那么被导出的所有模型将在同一个画面上被渲染, 输出产物只有一份.
当预览画面处于仅渲染选中状态时, 导出的内容仅包含被选中的模型, 也就是在画面中显示的内容.
### 更多
### 格式与版本转换
更为详细的使用方法和说明见 [Wiki](https://github.com/ww-rm/SpineViewer/wiki), 有使用上的问题或者 BUG 可以提个 [Issue](https://github.com/ww-rm/SpineViewer/issues).
可以通过工具菜单进行骨骼文件转换, 允许二进制和文本格式之间的转换, 以及不同版本间的转换.
## Acknowledgements
目前处于施工中, 仅支持转换 `3.8.x` 二进制到文本格式.
- [spine-runtimes](https://github.com/EsotericSoftware/spine-runtimes)
- [SFML.Net](https://github.com/SFML/SFML.Net)
- [FFMpegCore](https://github.com/rosenbjerg/FFMpegCore)
---
*如果你觉得这个项目不错请给个 :star:, 并分享给更多人知道! :)*
[![Stargazers over time](https://starchart.cc/ww-rm/SpineViewer.svg?variant=adaptive)](https://starchart.cc/ww-rm/SpineViewer)

View File

@@ -41,8 +41,9 @@ namespace SpineRuntime21 {
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 delegate void StartEndDelegate(AnimationState state, int trackIndex);
public event StartEndDelegate Start;
public event StartEndDelegate End;

View File

@@ -302,5 +302,50 @@ namespace SpineRuntime21 {
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;
}
}
}

View File

@@ -39,8 +39,9 @@ namespace SpineRuntime21 {
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) {
public Skin (String name) {
if (name == null) throw new ArgumentNullException("name cannot be null.");
this.name = name;
}

View File

@@ -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>2.1.25</Version>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -160,7 +160,7 @@
//
openFileDialog_Skel.AddExtension = false;
openFileDialog_Skel.AddToRecent = false;
openFileDialog_Skel.Filter = "skel 文件 (*.skel; *.json)|*.skel;*.json|二进制文件 (*.skel)|*.skel|文本文件 (*.json)|*.json|所有文件 (*.*)|*.*";
openFileDialog_Skel.Filter = "所有文件 (*.*)|*.*|skel 文件 (*.skel; *.json)|*.skel;*.json";
openFileDialog_Skel.Multiselect = true;
openFileDialog_Skel.Title = "批量选择skel文件";
//

View File

@@ -8,6 +8,7 @@ using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms;
using System.IO;
using SpineViewer.Spine;
namespace SpineViewer.Controls
{
@@ -33,14 +34,14 @@ namespace SpineViewer.Controls
{
if (File.Exists(path))
{
if (Spine.Spine.CommonSkelSuffix.Contains(Path.GetExtension(path).ToLower()))
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 (Spine.Spine.CommonSkelSuffix.Contains(Path.GetExtension(file).ToLower()))
if (SpineUtils.CommonSkelSuffix.Contains(Path.GetExtension(file).ToLower()))
listBox.Items.Add(file);
}
}
@@ -57,7 +58,7 @@ namespace SpineViewer.Controls
{
foreach (var file in Directory.EnumerateFiles(path, "*.*", SearchOption.AllDirectories))
{
if (Spine.Spine.CommonSkelSuffix.Contains(Path.GetExtension(file).ToLower()))
if (SpineUtils.CommonSkelSuffix.Contains(Path.GetExtension(file).ToLower()))
listBox.Items.Add(file);
}
}

View File

@@ -44,8 +44,9 @@
toolStripMenuItem_MoveTop = new ToolStripMenuItem();
toolStripMenuItem_MoveBottom = new ToolStripMenuItem();
toolStripSeparator3 = new ToolStripSeparator();
toolStripMenuItem_SelectAll = new ToolStripMenuItem();
toolStripMenuItem_CopyPreview = new ToolStripMenuItem();
toolStripMenuItem_AddFromClipboard = new ToolStripMenuItem();
toolStripMenuItem_SelectAll = new ToolStripMenuItem();
toolStripSeparator4 = new ToolStripSeparator();
toolStripMenuItem_ChangeView = new ToolStripMenuItem();
toolStripMenuItem_LargeIconView = new ToolStripMenuItem();
@@ -53,8 +54,13 @@
toolStripMenuItem_DetailsView = new ToolStripMenuItem();
imageList_LargeIcon = new ImageList(components);
imageList_SmallIcon = new ImageList(components);
toolStripMenuItem_AddFromClipboard = new ToolStripMenuItem();
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
@@ -68,9 +74,10 @@
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;
@@ -84,28 +91,28 @@
// 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_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(329, 451);
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(328, 30);
toolStripMenuItem_Add.Size = new Size(254, 30);
toolStripMenuItem_Add.Text = "添加...";
toolStripMenuItem_Add.Click += toolStripMenuItem_Add_Click;
//
// toolStripMenuItem_Insert
//
toolStripMenuItem_Insert.Name = "toolStripMenuItem_Insert";
toolStripMenuItem_Insert.Size = new Size(328, 30);
toolStripMenuItem_Insert.Size = new Size(254, 30);
toolStripMenuItem_Insert.Text = "插入...";
toolStripMenuItem_Insert.Click += toolStripMenuItem_Insert_Click;
//
@@ -113,39 +120,39 @@
//
toolStripMenuItem_Remove.Name = "toolStripMenuItem_Remove";
toolStripMenuItem_Remove.ShortcutKeys = Keys.Delete;
toolStripMenuItem_Remove.Size = new Size(328, 30);
toolStripMenuItem_Remove.Size = new Size(254, 30);
toolStripMenuItem_Remove.Text = "移除";
toolStripMenuItem_Remove.Click += toolStripMenuItem_Remove_Click;
//
// toolStripSeparator1
//
toolStripSeparator1.Name = "toolStripSeparator1";
toolStripSeparator1.Size = new Size(325, 6);
toolStripSeparator1.Size = new Size(251, 6);
//
// toolStripMenuItem_BatchAdd
//
toolStripMenuItem_BatchAdd.Name = "toolStripMenuItem_BatchAdd";
toolStripMenuItem_BatchAdd.Size = new Size(328, 30);
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(328, 30);
toolStripMenuItem_RemoveAll.Size = new Size(254, 30);
toolStripMenuItem_RemoveAll.Text = "移除全部";
toolStripMenuItem_RemoveAll.Click += toolStripMenuItem_RemoveAll_Click;
//
// toolStripSeparator2
//
toolStripSeparator2.Name = "toolStripSeparator2";
toolStripSeparator2.Size = new Size(325, 6);
toolStripSeparator2.Size = new Size(251, 6);
//
// toolStripMenuItem_MoveUp
//
toolStripMenuItem_MoveUp.Name = "toolStripMenuItem_MoveUp";
toolStripMenuItem_MoveUp.ShortcutKeys = Keys.Alt | Keys.W;
toolStripMenuItem_MoveUp.Size = new Size(328, 30);
toolStripMenuItem_MoveUp.Size = new Size(254, 30);
toolStripMenuItem_MoveUp.Text = "上移";
toolStripMenuItem_MoveUp.Click += toolStripMenuItem_MoveUp_Click;
//
@@ -153,7 +160,7 @@
//
toolStripMenuItem_MoveDown.Name = "toolStripMenuItem_MoveDown";
toolStripMenuItem_MoveDown.ShortcutKeys = Keys.Alt | Keys.S;
toolStripMenuItem_MoveDown.Size = new Size(328, 30);
toolStripMenuItem_MoveDown.Size = new Size(254, 30);
toolStripMenuItem_MoveDown.Text = "下移";
toolStripMenuItem_MoveDown.Click += toolStripMenuItem_MoveDown_Click;
//
@@ -161,7 +168,7 @@
//
toolStripMenuItem_MoveTop.Name = "toolStripMenuItem_MoveTop";
toolStripMenuItem_MoveTop.ShortcutKeys = Keys.Alt | Keys.Shift | Keys.W;
toolStripMenuItem_MoveTop.Size = new Size(328, 30);
toolStripMenuItem_MoveTop.Size = new Size(254, 30);
toolStripMenuItem_MoveTop.Text = "置顶";
toolStripMenuItem_MoveTop.Click += toolStripMenuItem_MoveTop_Click;
//
@@ -169,41 +176,49 @@
//
toolStripMenuItem_MoveBottom.Name = "toolStripMenuItem_MoveBottom";
toolStripMenuItem_MoveBottom.ShortcutKeys = Keys.Alt | Keys.Shift | Keys.S;
toolStripMenuItem_MoveBottom.Size = new Size(328, 30);
toolStripMenuItem_MoveBottom.Size = new Size(254, 30);
toolStripMenuItem_MoveBottom.Text = "置底";
toolStripMenuItem_MoveBottom.Click += toolStripMenuItem_MoveBottom_Click;
//
// toolStripSeparator3
//
toolStripSeparator3.Name = "toolStripSeparator3";
toolStripSeparator3.Size = new Size(325, 6);
//
// toolStripMenuItem_SelectAll
//
toolStripMenuItem_SelectAll.Name = "toolStripMenuItem_SelectAll";
toolStripMenuItem_SelectAll.ShortcutKeys = Keys.Control | Keys.A;
toolStripMenuItem_SelectAll.Size = new Size(328, 30);
toolStripMenuItem_SelectAll.Text = "全选";
toolStripMenuItem_SelectAll.Click += toolStripMenuItem_SelectAll_Click;
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(328, 30);
toolStripMenuItem_CopyPreview.Text = "复制预览图 (256x256)";
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(325, 6);
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(328, 30);
toolStripMenuItem_ChangeView.Size = new Size(254, 30);
toolStripMenuItem_ChangeView.Text = "切换视图";
//
// toolStripMenuItem_LargeIconView
@@ -242,22 +257,56 @@
imageList_SmallIcon.ImageSize = new Size(48, 48);
imageList_SmallIcon.TransparentColor = Color.Transparent;
//
// toolStripMenuItem_AddFromClipboard
// timer_SelectedIndexChangedDebounce
//
toolStripMenuItem_AddFromClipboard.Name = "toolStripMenuItem_AddFromClipboard";
toolStripMenuItem_AddFromClipboard.ShortcutKeys = Keys.Control | Keys.V;
toolStripMenuItem_AddFromClipboard.Size = new Size(328, 30);
toolStripMenuItem_AddFromClipboard.Text = "从剪贴板添加";
toolStripMenuItem_AddFromClipboard.Click += toolStripMenuItem_AddFromClipboard_Click;
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);
}
@@ -287,5 +336,9 @@
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;
}
}

View File

@@ -12,26 +12,34 @@ 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
{
/// <summary>
/// 显示骨骼信息的属性面板
/// 日志器
/// </summary>
[Category("自定义"), Description("用于显示骨骼属性的属性页")]
public PropertyGrid? PropertyGrid { get; set; }
private readonly Logger logger = LogManager.GetCurrentClassLogger();
/// <summary>
/// Spine 列表只读视图, 访问时必须使用 lock 语句锁定视图本身
/// </summary>
public readonly ReadOnlyCollection<Spine.Spine> Spines;
public readonly ReadOnlyCollection<Spine.SpineObject> Spines;
/// <summary>
/// Spine 列表, 访问时必须使用 lock 语句锁定只读视图 Spines
/// </summary>
private readonly List<Spine.Spine> spines = [];
private readonly List<Spine.SpineObject> spines = [];
/// <summary>
/// 用于属性页显示模型参数的包装类
/// </summary>
private readonly Dictionary<string, SpineObjectProperty> spinePropertyWrappers = [];
public SpineListView()
{
@@ -39,6 +47,12 @@ namespace SpineViewer.Controls
Spines = spines.AsReadOnly();
}
/// <summary>
/// 显示骨骼信息的属性面板
/// </summary>
[Category("自定义"), Description("用于显示模型属性的组合属性页")]
public SpineViewPropertyGrid? SpinePropertyGrid { get; set; }
/// <summary>
/// 选中的索引
/// </summary>
@@ -55,8 +69,7 @@ namespace SpineViewer.Controls
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);
}
@@ -67,19 +80,17 @@ namespace SpineViewer.Controls
{
try
{
var spine = Spine.Spine.New(result.Version, result.SkelPath, result.AtlasPath);
var spine = Spine.SpineObject.New(result.Version, result.SkelPath, result.AtlasPath);
// 如果索引无效则在末尾添加
if (index < 0 || index > listView.Items.Count)
index = listView.Items.Count;
// 锁定外部的读操作
lock (Spines)
{
spines.Insert(index, spine);
listView.SmallImageList.Images.Add(spine.ID, spine.Preview);
listView.LargeImageList.Images.Add(spine.ID, spine.Preview);
}
lock (Spines) { spines.Insert(index, spine); }
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 });
// 选中新增项
@@ -88,12 +99,12 @@ namespace SpineViewer.Controls
}
catch (Exception ex)
{
Program.Logger.Error(ex.ToString());
Program.Logger.Error("Failed to load {} {}", result.SkelPath, result.AtlasPath);
MessageBox.Error(ex.ToString(), "骨骼加载失败");
logger.Error(ex.ToString());
logger.Error("Failed to load {} {}", result.SkelPath, result.AtlasPath);
MessagePopup.Error(ex.ToString(), "骨骼加载失败");
}
Program.LogCurrentMemoryUsage();
logger.LogCurrentProcessMemoryUsage();
}
/// <summary>
@@ -102,15 +113,14 @@ 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>
public void BatchAdd(Dialogs.BatchOpenSpineDialogResult result)
private void BatchAdd(Dialogs.BatchOpenSpineDialogResult result)
{
var progressDialog = new Dialogs.ProgressDialog();
progressDialog.DoWork += BatchAdd_Work;
@@ -145,9 +155,10 @@ namespace SpineViewer.Controls
try
{
var spine = Spine.Spine.New(version, skelPath);
var spine = Spine.SpineObject.New(version, skelPath);
var preview = spine.Preview;
lock (Spines) { spines.Add(spine); }
spinePropertyWrappers[spine.ID] = new(spine);
listView.Invoke(() =>
{
listView.SmallImageList.Images.Add(spine.ID, preview);
@@ -158,24 +169,30 @@ namespace SpineViewer.Controls
}
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);
}
else
{
Program.Logger.Info("{} skel loaded successfully", success);
}
if (listView.Items.Count > 0)
{
listView.SelectedIndices.Clear();
listView.SelectedIndices.Add(listView.Items.Count - 1);
}
});
Program.LogCurrentMemoryUsage();
if (error > 0)
logger.Warn("Batch load {} successfully, {} failed", success, error);
else
logger.Info("{} skel loaded successfully", success);
logger.LogCurrentProcessMemoryUsage();
}
/// <summary>
@@ -188,14 +205,14 @@ namespace SpineViewer.Controls
{
if (File.Exists(path))
{
if (Spine.Spine.CommonSkelSuffix.Contains(Path.GetExtension(path).ToLower()))
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 (Spine.Spine.CommonSkelSuffix.Contains(Path.GetExtension(file).ToLower()))
if (SpineUtils.CommonSkelSuffix.Contains(Path.GetExtension(file).ToLower()))
validPaths.Add(file);
}
}
@@ -205,29 +222,41 @@ namespace SpineViewer.Controls
{
if (validPaths.Count > 100)
{
if (MessageBox.Quest($"共发现 {validPaths.Count} 个可加载骨骼,数量较多,是否一次性全部加载?") == DialogResult.Cancel)
if (MessagePopup.Quest($"共发现 {validPaths.Count} 个可加载骨骼,数量较多,是否一次性全部加载?") == DialogResult.Cancel)
return;
}
BatchAdd(new Dialogs.BatchOpenSpineDialogResult(Spine.Version.Auto, validPaths.ToArray()));
BatchAdd(new Dialogs.BatchOpenSpineDialogResult(SpineVersion.Auto, validPaths.ToArray()));
}
else if (validPaths.Count > 0)
{
Insert(new Dialogs.OpenSpineDialogResult(Spine.Version.Auto, validPaths[0]));
Insert(new Dialogs.OpenSpineDialogResult(SpineVersion.Auto, validPaths[0]));
}
}
private void listView_SelectedIndexChanged(object sender, EventArgs e)
{
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 (PropertyGrid is not null)
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();
}
// 标记选中的 Spine
@@ -246,6 +275,8 @@ namespace SpineViewer.Controls
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)
@@ -341,6 +372,7 @@ namespace SpineViewer.Controls
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;
@@ -379,21 +411,24 @@ namespace SpineViewer.Controls
if (listView.SelectedIndices.Count > 1)
{
if (MessageBox.Quest($"确定移除所选 {listView.SelectedIndices.Count} 项吗?") != DialogResult.OK)
if (MessagePopup.Quest($"确定移除所选 {listView.SelectedIndices.Count} 项吗?") != DialogResult.OK)
return;
}
foreach (var i in listView.SelectedIndices.Cast<int>().OrderByDescending(x => x))
lock (Spines)
{
listView.Items.RemoveAt(i);
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();
}
}
@@ -476,7 +511,7 @@ namespace SpineViewer.Controls
if (listView.Items.Count <= 0)
return;
if (MessageBox.Quest($"确认移除所有 {listView.Items.Count} 项吗?") != DialogResult.OK)
if (MessagePopup.Quest($"确认移除所有 {listView.Items.Count} 项吗?") != DialogResult.OK)
return;
listView.Items.Clear();
@@ -484,26 +519,28 @@ namespace SpineViewer.Controls
{
foreach (var spine in spines) spine.Dispose();
spines.Clear();
spinePropertyWrappers.Clear();
listView.SmallImageList.Images.Clear();
listView.LargeImageList.Images.Clear();
}
if (PropertyGrid is not null)
PropertyGrid.SelectedObject = null;
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 (int i in listView.SelectedIndices)
{
var a = Process.GetCurrentProcess();
var spine = spines[i];
var image = spine.Preview;
var path = Path.Combine(Program.TempDir, $"{spine.ID}.png");
using (var clone = new Bitmap(image))
clone.Save(path);
var path = Path.Combine(tempDir, $"{spine.ID}.png");
spine.Preview.Save(path);
fileDropList.Add(path);
}
}
@@ -545,4 +582,9 @@ namespace SpineViewer.Controls
listView.View = View.Details;
}
}
public class DefaultSpineConfig
{
}
}

View File

@@ -126,4 +126,10 @@
<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>

View File

@@ -0,0 +1,223 @@
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();
panel_Container = new Panel();
flowLayoutPanel1 = new FlowLayoutPanel();
button_Stop = new Button();
imageList = new ImageList(components);
button_Restart = new Button();
button_Start = new Button();
button_ForwardStep = new Button();
button_ForwardFast = new Button();
toolTip = new ToolTip(components);
tableLayoutPanel1.SuspendLayout();
panel_Container.SuspendLayout();
flowLayoutPanel1.SuspendLayout();
SuspendLayout();
//
// panel_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(panel_Container, 0, 0);
tableLayoutPanel1.Controls.Add(flowLayoutPanel1, 0, 1);
tableLayoutPanel1.Dock = DockStyle.Fill;
tableLayoutPanel1.Location = new Point(0, 0);
tableLayoutPanel1.Margin = new Padding(0);
tableLayoutPanel1.Name = "tableLayoutPanel1";
tableLayoutPanel1.RowCount = 2;
tableLayoutPanel1.RowStyles.Add(new RowStyle(SizeType.Percent, 100F));
tableLayoutPanel1.RowStyles.Add(new RowStyle());
tableLayoutPanel1.Size = new Size(641, 636);
tableLayoutPanel1.TabIndex = 2;
//
// panel_Container
//
panel_Container.BackColor = SystemColors.ControlDark;
panel_Container.Controls.Add(panel_Render);
panel_Container.Dock = DockStyle.Fill;
panel_Container.Location = new Point(0, 0);
panel_Container.Margin = new Padding(0);
panel_Container.Name = "panel_Container";
panel_Container.Size = new Size(641, 594);
panel_Container.TabIndex = 0;
//
// flowLayoutPanel1
//
flowLayoutPanel1.Anchor = AnchorStyles.None;
flowLayoutPanel1.AutoSize = true;
flowLayoutPanel1.AutoSizeMode = AutoSizeMode.GrowAndShrink;
flowLayoutPanel1.Controls.Add(button_Stop);
flowLayoutPanel1.Controls.Add(button_Restart);
flowLayoutPanel1.Controls.Add(button_Start);
flowLayoutPanel1.Controls.Add(button_ForwardStep);
flowLayoutPanel1.Controls.Add(button_ForwardFast);
flowLayoutPanel1.Location = new Point(138, 594);
flowLayoutPanel1.Margin = new Padding(0);
flowLayoutPanel1.Name = "flowLayoutPanel1";
flowLayoutPanel1.Size = new Size(365, 42);
flowLayoutPanel1.TabIndex = 1;
//
// button_Stop
//
button_Stop.AutoSize = true;
button_Stop.AutoSizeMode = AutoSizeMode.GrowAndShrink;
button_Stop.ImageKey = "stop";
button_Stop.ImageList = imageList;
button_Stop.Location = new Point(3, 3);
button_Stop.Name = "button_Stop";
button_Stop.Padding = new Padding(15, 3, 15, 3);
button_Stop.Size = new Size(67, 36);
button_Stop.TabIndex = 0;
toolTip.SetToolTip(button_Stop, "停止播放并重置时间到初始");
button_Stop.UseVisualStyleBackColor = true;
button_Stop.Click += button_Stop_Click;
//
// imageList
//
imageList.ColorDepth = ColorDepth.Depth32Bit;
imageList.ImageStream = (ImageListStreamer)resources.GetObject("imageList.ImageStream");
imageList.TransparentColor = Color.Transparent;
imageList.Images.SetKeyName(0, "stop");
imageList.Images.SetKeyName(1, "restart");
imageList.Images.SetKeyName(2, "start");
imageList.Images.SetKeyName(3, "pause");
imageList.Images.SetKeyName(4, "forward-step");
imageList.Images.SetKeyName(5, "forward-fast");
//
// button_Restart
//
button_Restart.AutoSize = true;
button_Restart.AutoSizeMode = AutoSizeMode.GrowAndShrink;
button_Restart.ImageKey = "restart";
button_Restart.ImageList = imageList;
button_Restart.Location = new Point(76, 3);
button_Restart.Name = "button_Restart";
button_Restart.Padding = new Padding(15, 3, 15, 3);
button_Restart.Size = new Size(67, 36);
button_Restart.TabIndex = 1;
toolTip.SetToolTip(button_Restart, "从头开始播放");
button_Restart.UseVisualStyleBackColor = true;
button_Restart.Click += button_Restart_Click;
//
// button_Start
//
button_Start.AutoSize = true;
button_Start.AutoSizeMode = AutoSizeMode.GrowAndShrink;
button_Start.BackgroundImageLayout = ImageLayout.Center;
button_Start.ImageKey = "pause";
button_Start.ImageList = imageList;
button_Start.Location = new Point(149, 3);
button_Start.Name = "button_Start";
button_Start.Padding = new Padding(15, 3, 15, 3);
button_Start.Size = new Size(67, 36);
button_Start.TabIndex = 2;
toolTip.SetToolTip(button_Start, "开始/暂停");
button_Start.UseVisualStyleBackColor = true;
button_Start.Click += button_Start_Click;
//
// button_ForwardStep
//
button_ForwardStep.AutoSize = true;
button_ForwardStep.AutoSizeMode = AutoSizeMode.GrowAndShrink;
button_ForwardStep.ImageKey = "forward-step";
button_ForwardStep.ImageList = imageList;
button_ForwardStep.Location = new Point(222, 3);
button_ForwardStep.Name = "button_ForwardStep";
button_ForwardStep.Padding = new Padding(15, 3, 15, 3);
button_ForwardStep.Size = new Size(67, 36);
button_ForwardStep.TabIndex = 3;
toolTip.SetToolTip(button_ForwardStep, "快进 1 帧");
button_ForwardStep.UseVisualStyleBackColor = true;
button_ForwardStep.Click += button_ForwardStep_Click;
//
// button_ForwardFast
//
button_ForwardFast.AutoSize = true;
button_ForwardFast.AutoSizeMode = AutoSizeMode.GrowAndShrink;
button_ForwardFast.ImageKey = "forward-fast";
button_ForwardFast.ImageList = imageList;
button_ForwardFast.Location = new Point(295, 3);
button_ForwardFast.Name = "button_ForwardFast";
button_ForwardFast.Padding = new Padding(15, 3, 15, 3);
button_ForwardFast.Size = new Size(67, 36);
button_ForwardFast.TabIndex = 4;
toolTip.SetToolTip(button_ForwardFast, "快进 10 帧");
button_ForwardFast.UseVisualStyleBackColor = true;
button_ForwardFast.Click += button_ForwardFast_Click;
//
// SpinePreviewPanel
//
AutoScaleDimensions = new SizeF(11F, 24F);
AutoScaleMode = AutoScaleMode.Font;
Controls.Add(tableLayoutPanel1);
Name = "SpinePreviewPanel";
Size = new Size(641, 636);
SizeChanged += SpinePreviewPanel_SizeChanged;
tableLayoutPanel1.ResumeLayout(false);
tableLayoutPanel1.PerformLayout();
panel_Container.ResumeLayout(false);
flowLayoutPanel1.ResumeLayout(false);
flowLayoutPanel1.PerformLayout();
ResumeLayout(false);
}
#endregion
private Panel panel_Render;
private TableLayoutPanel tableLayoutPanel1;
private Panel panel_Container;
private FlowLayoutPanel flowLayoutPanel1;
private Button button_Stop;
private Button button_Start;
private ImageList imageList;
private ToolTip toolTip;
private Button button_ForwardStep;
private Button button_ForwardFast;
private Button button_Restart;
}
}

View File

@@ -9,47 +9,31 @@ using System.Threading.Tasks;
using System.Windows.Forms;
using System.Security.Policy;
using System.Diagnostics;
using NLog.Targets;
using NLog;
using SpineViewer.Utils;
namespace SpineViewer.Controls
{
public partial class SpinePreviewer : UserControl
public partial class SpinePreviewPanel : UserControl
{
/// <summary>
/// 包装类, 用于属性面板显示
/// </summary>
private class PreviewerProperty(SpinePreviewer previewer)
public SpinePreviewPanel()
{
[TypeConverter(typeof(SizeConverter))]
[Category("导出"), DisplayName("分辨率")]
public Size Resolution { get => previewer.Resolution; set => previewer.Resolution = value; }
InitializeComponent();
renderWindow = new(panel_Render.Handle);
renderWindow.SetActive(false);
[TypeConverter(typeof(PointFConverter))]
[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 RenderSelectedOnly { get => previewer.RenderSelectedOnly; set => previewer.RenderSelectedOnly = value; }
[Category("预览"), DisplayName("显示坐标轴")]
public bool ShowAxis { get => previewer.ShowAxis; set => previewer.ShowAxis = value; }
[Category("预览"), DisplayName("最大帧率")]
public uint MaxFps { get => previewer.MaxFps; set => previewer.MaxFps = value; }
// 设置默认参数
Resolution = new(2048, 2048);
Center = new(0, 0);
FlipY = true;
MaxFps = 30;
}
/// <summary>
/// 日志器
/// </summary>
private readonly Logger logger = LogManager.GetCurrentClassLogger();
/// <summary>
/// 要绑定的 Spine 列表控件
/// </summary>
@@ -67,56 +51,12 @@ namespace SpineViewer.Controls
{
propertyGrid = value;
if (propertyGrid is not null)
propertyGrid.SelectedObject = new PreviewerProperty(this);
propertyGrid.SelectedObject = new SpinePreviewPanelProperty(this);
}
}
private PropertyGrid? propertyGrid;
/// <summary>
/// 画面缩放最大值
/// </summary>
public const float ZOOM_MAX = 1000f;
/// <summary>
/// 画面缩放最小值
/// </summary>
public const float ZOOM_MIN = 0.001f;
/// <summary>
/// 预览画面背景色
/// </summary>
private static readonly SFML.Graphics.Color BackgroundColor = new(105, 105, 105);
/// <summary>
/// 预览画面坐标轴颜色
/// </summary>
private static readonly SFML.Graphics.Color AxisColor = new(220, 220, 220);
/// <summary>
/// 坐标轴顶点缓冲区
/// </summary>
private readonly SFML.Graphics.VertexArray AxisVertex = new(SFML.Graphics.PrimitiveType.Lines, 2);
/// <summary>
/// 渲染窗口
/// </summary>
private readonly SFML.Graphics.RenderWindow RenderWindow;
/// <summary>
/// 帧间隔计时器
/// </summary>
private readonly SFML.System.Clock Clock = new();
/// <summary>
/// 画面拖放对象世界坐标源点
/// </summary>
private SFML.System.Vector2f? draggingSrc = null;
/// <summary>
/// 渲染任务
/// </summary>
private Task? task = null;
private CancellationTokenSource? cancelToken = null;
#region
/// <summary>
/// 分辨率
@@ -131,8 +71,8 @@ namespace SpineViewer.Controls
if (value.Width <= 0) value.Width = 100;
if (value.Height <= 0) value.Height = 100;
float parentX = Width;
float parentY = Height;
float parentX = panel_Render.Parent.Width;
float parentY = panel_Render.Parent.Height;
float sizeX = value.Width;
float sizeY = value.Height;
@@ -150,15 +90,15 @@ namespace SpineViewer.Controls
}
// 必须通过 SFML 的方法调整窗口
RenderWindow.Position = new((int)(parentX - sizeX) / 2, (int)(parentY - sizeY) / 2);
RenderWindow.Size = new((uint)sizeX, (uint)sizeY);
renderWindow.Position = new((int)(parentX - sizeX) / 2, (int)(parentY - sizeY) / 2);
renderWindow.Size = new((uint)sizeX, (uint)sizeY);
// 将 view 的大小设置成于 resolution 相同的大小, 其余属性都不变
var view = RenderWindow.GetView();
using 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);
renderWindow.SetView(view);
resolution = value;
}
@@ -174,14 +114,15 @@ namespace SpineViewer.Controls
{
get
{
var center = RenderWindow.GetView().Center;
using var view = renderWindow.GetView();
var center = view.Center;
return new(center.X, center.Y);
}
set
{
var view = RenderWindow.GetView();
using var view = renderWindow.GetView();
view.Center = new(value.X, value.Y);
RenderWindow.SetView(view);
renderWindow.SetView(view);
}
}
@@ -192,15 +133,19 @@ namespace SpineViewer.Controls
[Browsable(false)]
public float Zoom
{
get => resolution.Width / Math.Abs(RenderWindow.GetView().Size.X);
get
{
using var view = renderWindow.GetView();
return resolution.Width / Math.Abs(view.Size.X);
}
set
{
value = Math.Clamp(value, ZOOM_MIN, ZOOM_MAX);
var view = RenderWindow.GetView();
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);
renderWindow.SetView(view);
}
}
@@ -211,12 +156,16 @@ namespace SpineViewer.Controls
[DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
public float Rotation
{
get => RenderWindow.GetView().Rotation;
get
{
using var view = renderWindow.GetView();
return view.Rotation;
}
set
{
var view = RenderWindow.GetView();
using var view = renderWindow.GetView();
view.Rotation = value;
RenderWindow.SetView(view);
renderWindow.SetView(view);
}
}
@@ -227,15 +176,19 @@ namespace SpineViewer.Controls
[Browsable(false)]
public bool FlipX
{
get => RenderWindow.GetView().Size.X < 0;
get
{
using var view = renderWindow.GetView();
return view.Size.X < 0;
}
set
{
var view = RenderWindow.GetView();
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);
renderWindow.SetView(view);
}
}
@@ -246,15 +199,19 @@ namespace SpineViewer.Controls
[Browsable(false)]
public bool FlipY
{
get => RenderWindow.GetView().Size.Y < 0;
get
{
using var view = renderWindow.GetView();
return view.Size.Y < 0;
}
set
{
var view = RenderWindow.GetView();
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);
renderWindow.SetView(view);
}
}
@@ -277,43 +234,97 @@ namespace SpineViewer.Controls
/// </summary>
[DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
[Browsable(false)]
public uint MaxFps { get => maxFps; set { RenderWindow.SetFramerateLimit(value); maxFps = value; } }
public uint MaxFps { get => maxFps; set { renderWindow.SetFramerateLimit(value); maxFps = value; } }
private uint maxFps = 60;
public SpinePreviewer()
/// <summary>
/// 获取 View
/// </summary>
public SFML.Graphics.View GetView() => renderWindow.GetView();
#endregion
#region
/// <summary>
/// 预览画面背景色
/// </summary>
private static readonly SFML.Graphics.Color BackgroundColor = new(105, 105, 105);
/// <summary>
/// 预览画面坐标轴颜色
/// </summary>
private static readonly SFML.Graphics.Color AxisColor = new(220, 220, 220);
/// <summary>
/// 坐标轴顶点缓冲区
/// </summary>
private readonly SFML.Graphics.VertexArray axisVertices = new(SFML.Graphics.PrimitiveType.Lines, 2); // XXX: 暂时未使用 Dispose 释放
/// <summary>
/// 渲染窗口
/// </summary>
private readonly SFML.Graphics.RenderWindow renderWindow;
/// <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
{
InitializeComponent();
RenderWindow = new(panel.Handle);
RenderWindow.SetActive(false);
// 设置默认参数
Resolution = new(2048, 2048);
Center = new(0, 0);
FlipY = true;
MaxFps = 30;
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>
public SpinePreviewerFrameArgs GetFrameArgs() => new(Resolution, RenderWindow.GetView(), RenderSelectedOnly);
private float forwardDelta = 0;
private object _forwardDeltaLock = new();
/// <summary>
/// 开始预览
/// 开始渲染
/// </summary>
public void StartPreview()
public void StartRender()
{
if (task is not null)
return;
cancelToken = new();
task = Task.Run(RenderTask, cancelToken.Token);
IsUpdating = true;
}
/// <summary>
/// 停止预览
/// 停止渲染
/// </summary>
public void StopPreview()
public void StopRender()
{
IsUpdating = false;
if (task is null || cancelToken is null)
return;
cancelToken.Cancel();
@@ -329,25 +340,35 @@ namespace SpineViewer.Controls
{
try
{
RenderWindow.SetActive(true);
renderWindow.SetActive(true);
float delta;
while (cancelToken is not null && !cancelToken.IsCancellationRequested)
{
delta = Clock.ElapsedTime.AsSeconds();
Clock.Restart();
delta = clock.ElapsedTime.AsSeconds();
clock.Restart();
RenderWindow.Clear(BackgroundColor);
// 停止更新的时候只是时间不前进, 但是坐标变换还是要更新, 否则无法移动对象
if (!IsUpdating) delta = 0;
// 加上要快进的量
lock (_forwardDeltaLock)
{
delta += forwardDelta;
forwardDelta = 0;
}
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);
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
@@ -355,74 +376,78 @@ namespace SpineViewer.Controls
{
lock (SpineListView.Spines)
{
var spines = SpineListView.Spines;
for (int i = spines.Count - 1; i >= 0; i--)
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.IsDebug = true;
RenderWindow.Draw(spine);
spine.IsDebug = false;
spine.EnableDebug = true;
renderWindow.Draw(spine);
spine.EnableDebug = false;
}
}
}
RenderWindow.Display();
renderWindow.Display();
}
}
catch (Exception ex)
{
logger.Fatal(ex.ToString());
logger.Fatal("Render task stopped");
MessagePopup.Error(ex.ToString(), "预览画面已停止渲染");
}
finally
{
RenderWindow.SetActive(false);
renderWindow.SetActive(false);
}
}
private void SpinePreviewer_SizeChanged(object sender, EventArgs e)
#endregion
/// <summary>
/// 画面拖放对象世界坐标源点
/// </summary>
private SFML.System.Vector2f? draggingSrc = null;
private void SpinePreviewPanel_SizeChanged(object sender, EventArgs e)
{
if (RenderWindow is null)
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;
}
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
renderH *= scale;
renderW *= scale;
// 必须通过 SFML 的方法调整窗口
RenderWindow.Position = new((int)(parentX - sizeX) / 2, (int)(parentY - sizeY) / 2);
RenderWindow.Size = new((uint)sizeX, (uint)sizeY);
renderWindow.Position = new((int)(parentW - renderW) / 2, (int)(parentH - renderH) / 2);
renderWindow.Size = new((uint)renderW, (uint)renderH);
}
private void panel_MouseDown(object sender, MouseEventArgs e)
private void panel_Render_MouseDown(object sender, MouseEventArgs e)
{
// 右键优先级高, 进入画面拖动模式, 需要重新记录源点
if ((e.Button & MouseButtons.Right) != 0)
{
draggingSrc = RenderWindow.MapPixelToCoords(new(e.X, e.Y));
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));
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)
@@ -435,10 +460,12 @@ namespace SpineViewer.Controls
// 仅渲染选中模式禁止在画面里选择对象
if (RenderSelectedOnly)
{
// 只在被选中的对象里判断是否有效命中
bool hit = false;
foreach (int i in SpineListView.SelectedIndices)
{
if (!spines[i].Bounds.Contains(src)) continue;
if (spines[i].IsHidden) continue;
if (!spines[i].GetCurrentBounds().Contains(src)) continue;
hit = true;
break;
}
@@ -448,13 +475,14 @@ namespace SpineViewer.Controls
}
else
{
// 没有按下 Ctrl 键就只选中点击的那个, 所以先清空选中列表
if ((ModifierKeys & Keys.Control) == 0)
{
// 没按 Ctrl 的情况下, 如果命中了已选中对象, 则就算普通命中
bool hit = false;
for (int i = 0; i < spines.Count; i++)
{
if (!spines[i].Bounds.Contains(src)) continue;
if (spines[i].IsHidden) continue;
if (!spines[i].GetCurrentBounds().Contains(src)) continue;
hit = true;
@@ -472,10 +500,11 @@ namespace SpineViewer.Controls
}
else
{
// 按下 Ctrl 的情况就执行多选, 并且点空白处也不会清空选中
for (int i = 0; i < spines.Count; i++)
{
if (!spines[i].Bounds.Contains(src))
continue;
if (spines[i].IsHidden) continue;
if (!spines[i].GetCurrentBounds().Contains(src)) continue;
SpineListView.SelectedIndices.Add(i);
break;
@@ -486,13 +515,13 @@ namespace SpineViewer.Controls
}
}
private void panel_MouseMove(object sender, MouseEventArgs e)
private void panel_Render_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 dst = renderWindow.MapPixelToCoords(new(e.X, e.Y));
var _delta = dst - src;
var delta = new SizeF(_delta.X, _delta.Y);
@@ -506,20 +535,24 @@ namespace SpineViewer.Controls
{
lock (SpineListView.Spines)
{
var spines = SpineListView.Spines;
foreach (int i in SpineListView.SelectedIndices)
SpineListView.Spines[i].Position += delta;
{
if (spines[i].IsHidden) continue;
spines[i].Position += delta;
}
}
}
draggingSrc = dst;
}
}
private void panel_MouseUp(object sender, MouseEventArgs e)
private void panel_Render_MouseUp(object sender, MouseEventArgs e)
{
// 右键高优先级, 结束画面拖动模式
if ((e.Button & MouseButtons.Right) != 0)
{
SpineListView?.PropertyGrid?.Refresh();
SpineListView?.SpinePropertyGrid?.Refresh();
draggingSrc = null;
Cursor = Cursors.Default;
@@ -529,36 +562,131 @@ namespace SpineViewer.Controls
else if ((e.Button & MouseButtons.Left) != 0 && (MouseButtons & MouseButtons.Right) == 0)
{
draggingSrc = null;
SpineListView?.PropertyGrid?.Refresh();
SpineListView?.SpinePropertyGrid?.Refresh();
}
}
private void panel_MouseWheel(object sender, MouseEventArgs e)
private void panel_Render_MouseWheel(object sender, MouseEventArgs e)
{
Zoom *= (e.Delta > 0 ? 1.1f : 0.9f);
PropertyGrid?.Refresh();
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;
}
}
//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 SpinePreviewerFrameArgs(Size resolution, SFML.Graphics.View view, bool renderSelectedOnly)
public class SpinePreviewPanelProperty(SpinePreviewPanel previewPanel)
{
/// <summary>
/// 分辨率
/// </summary>
public Size Resolution => resolution;
[Browsable(false)]
public SpinePreviewPanel PreviewPanel { get; } = previewPanel;
/// <summary>
/// 渲染视窗
/// </summary>
public SFML.Graphics.View View => view;
[TypeConverter(typeof(SizeConverter))]
[Category("[0] "), DisplayName("")]
public Size Resolution { get => PreviewPanel.Resolution; set => PreviewPanel.Resolution = value; }
/// <summary>
/// 是否仅渲染/导出选中骨骼
/// </summary>
public bool RenderSelectedOnly => renderSelectedOnly;
[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; }
}
}

View File

@@ -0,0 +1,326 @@
<?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
SW1hZ2VMaXN0U3RyZWFtZXIBAAAABERhdGEHAgIAAAAJAwAAAA8DAAAAHi0AAAJNU0Z0AUkBTAIBAQYB
AAF4AQABeAEAAR8BAAEYAQAE/wEhAQAI/wFCAU0BNgcAATYDAAEoAwABfAMAATADAAEBAQABIAYAAV0q
AAQCAy0BRQNbAc0DXwHoA1kBxgMyAU8DDwEUBAIYAAMOARIDQwF3A10BzwNbAc0DLQFFBAIYAANWAbID
XwHoA1wB1gNDAXcDFgEeBAIYAAMKAQ0DSQGFA18B4wNfAeUDUQGeAyQBNAMJAQsEARgAAwsBDgM7AWQD
XgHSA1YBsv8AEQAEAgMxAUwDYgHhAwAB/wMxAfkDYAHbA0QBewMeASoDBgEIFAADGAEhA1cBwgMhAfsD
YgHhAzEBTAQCGAADWQHDAwAB/wMjAfwDXgHrA0gBhAMWAR4YAAMLAQ4DTQGSAy4B+QMQAf4DUwHyA1gB
ugM3AVoDEQEWAwIBAxQAAxcBHwNJAYYDXgHrA1kBw/8AEQAEAgMxAUwDYgHhAwAB/wMAAf8DIAH9A2AB
4ANQAZoDLgFGAxEBFgMGAQcEAQgAAxoBJANbAc0DAAH/A2IB4QMxAUwEAhgAA1kBwwMAAf8DAAH/AwAB
/wNbAdADPgFrAw8BEwMCAQMQAAMLAQ4DTQGSAy4B+QMAAf8DAAH/AzwB9gNcAcsDRAF5Ax4BKgMGAQcM
AAQBAxgBIQNKAYsDXAHsA1kBw/8AEQAEAgMxAUwDYgHhAwAB/wMxAfkDXAHnA0QB9QNXAe4DWQG7A0MB
dwMoATsDDwEUBAEEAAMaASQDWwHNAwAB/wNiAeEDMQFMBAIYAANZAcMDIgH8A1EB8ANEAfUDMQH5A10B
zgNDAXcDGgEjAwIBAwwAAwsBDgNNAZIDLgH5AwEB/wMlAfoDOwH4AzEB+QNeAeMDTgGYAyQBNQMGAQgE
AQQABAEDGAEhA0oBiwNcAewDWQHD/wARAAQCAzEBTANiAeEDAAH/A1wB2QM7AWMDWQG7Az8B9wM6AfgD
XAHnA1sBxQNBAXMDEwEZAwIBAwMaASQDWwHNAwAB/wNiAeEDMQFMBAIYAANZAcMDXgHrA1ABmgNVAbED
TgHzAyEB+wNbAeQDSwGPAxsBJQMDAQQIAAMLAQ4DTQGSAy4B+QMhAf0DXAHZA1oBxwNEAfUDEAH+A14B
6wNSAaMDJQE3AwMBBAQABAEDGAEhA0oBiwNcAewDWQHD/wARAAQCAzEBTANiAeEDAAH/A1sBzQMaASQD
FgEdA1UBrwMAAf8DAAH/AwAB/wMAAf8DVQGvAxYBHQMaASQDWwHNAwAB/wNiAeEDMQFMBAIYAANZAcMD
WwHkAzsBZQMHAQkDSQGGAyEB+wMAAf8DAAH/A1kBwQMdASkIAAMLAQ4DTQGSAy4B+QMuAfkDTQGSAzkE
XgHiAwAB/wMAAf8DXgHjAzYBWAMFAQYEAAQBAxgBIQNKAYsDXAHsA1kBw/8AEQAEAgMxAUwDYgHhAwAB
/wNbAc0DGgEkAwIBAwMTARoDRgF/A1sB3gMhAfsDEAH+AzoB+ANZAbsDOwFjA1wB2QMAAf8DYgHhAzEB
TAQCGAADWQHDA1sB5AM7AWUDBwQJAQwDOgFhA18B1QNDAfUDMQH5A1sBygMyAU8DDwEUAw0BEQNNAZID
LgH5Ay4B+QNNAZIDEQEWAy0BRANaAb8DVAHvAyEB+wNdAdwDPwFuAxYBHgMEAQUDGAEhA0oBiwNcAewD
WQHD/wARAAQCAzEBTANiAeEDAAH/A1sBzQMaASQEAAQCAxMBGgM9AWcDWQHAA1QB7wMQAf4DPwH3A1wB
5wMxAfkDAAH/A2IB4QMxAUwEAhgAA1kBwwNbAeQDOwFlAwcBCQQAAwsBDgMxAU0DWAG9AzwB9gMxAfkD
YAHbA0QBeAMhAS8DTgGVAy4B+QMuAfkDTQGSAwsBDgMGAQgDIAEuA00BkgNdAdwDRAH1A1oB6QNOAZYD
KAE8AyABLQNLAY0DXAHsA1kBw/8AEQAEAgMxAUwDYgHhAwAB/wNbAc0DGgEkCAAEAgMMARADLwFJA1IB
owNbAeQDPwH3AxAB/gMAAf8DAAH/A2IB4QMxAUwEAhgAA1kBwwNbAeQDOwFlAwcBCQgAAxABFQM/AW0D
XAHWAyUB+gMgAf0DXAHWA0QBeQNUAasDIQH7Ay4B+QNNAZIDCwEOBAADAwEEAxsBJgNBAXMDWgHEA0cB
9ANXAe4DVQG0A0QBegNRAaIDVwHuA1kBw/8AEQAEAgMxAUwDYgHhAwAB/wNbAc0DGgEkEAADCAEKAyMB
MwNEAXkDVwG8A18B5QMlAfoDAAH/A2IB4QMxAUwEAhgAA1kBwwNbAeQDOwFlAwcBCQgAAwMBBAMaASQD
QgF0A14B0gMhAfsDPAH2A10B0QNgAeADIAH9Ay4B+QNNAZIDCwEOCAADAgEDAxIBFwM1AVUDWAG6A1YB
8QM8AfYDWwHeA2AB2wM6AfgDWQHD/wARAAQCAzEBTANiAeEDAAH/A1sBzQMaASQUAAMCAQMDDgESAyMB
MgM9AWgDXgHdAwAB/wNiAeEDMQFMBAIYAANZAcMDWwHkAzsBZQMHAQkMAAMCAQMDDwETAzQBUwNdAdED
OgH4AyEB/AMhAf0DAAH/Ay4B+QNNAZIDCwEODAAEAQMJAQwDIwEzA1UBrgMiAf0DIAH9AyEB/AMQAf4D
WQHD/wARAAQCAzEBTANiAeEDAAH/A1sBzQMaASQgAAMaASQDWwHNAwAB/wNiAeEDMQFMBAIYAANZAcMD
WwHkAzsBZQMHAQkYAAMPARQDVwG5AwAB/wMAAf8DAAH/Ay4B+QNNAZIDCwEOGAADRwGAA14B7QMAAf8D
AAH/AwAB/wNZAcP/ABEABAIDMQFMA2IB4QMAAf8DWwHNAxoBJBQABAIDBwEJAxYBHQM1AVUDXAHZAwAB
/wNiAeEDMQFMBAIYAANZAcMDWwHkAzsBZQMHAQkMAAQBAw0BEQM0AVMDXQHRAzEB+QMRAf4DEAH+AwAB
/wMuAfkDTQGSAwsBDgwABAEDCQELAx4BKwNTAakDIgH9AyAB/QMhAfwDEAH+A1kBw/8AEQAEAgMxAUwD
YgHhAwAB/wNbAc0DGgEkEAADBwEJAxsBJgM0AVMDTQGSA14B3QMxAfkDAAH/A2IB4QMxAUwEAhgAA1kB
wwNbAeQDOwFlAwcBCQgABAIDEAEVAzkBXgNbAc0DIQH7AyEB+wNcAecDVwHuAxAB/gMuAfkDTQGSAwsB
DggAAwIBAwMSARcDNQFVA1gBuANWAfEDPAH2A1sB3gNgAdsDOgH4A1kBw/8AEQAEAgMxAUwDYgHhAwAB
/wNbAc0DGgEkCAAEAgMMARADLgFGA00BkgNcAcgDXgHrAyAB/QMAAf8DAAH/A2IB4QMxAUwEAhgAA1kB
wwNbAeQDOwFlAwcBCQgAAwkBDAMxAU4DWAG3A04B8wMgAf0DXgHdA04BlgNZAb4DIQH8Ay4B+QNNAZID
CwEOBAADAwEEAxsBJgNBAXMDWgHEA0cB9ANXAe4DVQG0A0QBegNRAaIDVwHuA1kBw/8AEQAEAgMxAUwD
YgHhAwAB/wNbAc0DGgEkBAAEAgMTARoDPQFnA1oBvwNeAesDJQH6Az0B9gNcAecDMQH5AwAB/wNiAeED
MQFMBAIYAANZAcMDWwHkAzsBZQMHAQkEAAMLAQ4DLgFHA1UBrQNUAe8DOwH4A2AB2wNEAXoDJAE1A04B
mAMkAfoDLgH5A00BkgMLAQ4DBgEIAyABLgNNAZIDXQHcA0QB9QNaAekDTgGWAygBPAMgAS0DSwGNA1wB
7ANZAcP/ABEABAIDMQFMA2IB4QMAAf8DWwHNAxoBJAMCAQMDEwEaA0YBfwNbAd4DIQH7AxAB/gM6AfgD
WQG7AzsBYwNcAdkDAAH/A2IB4QMxAUwEAhgAA1kBwwNbAeQDOwFlAwcECQEMAzoBYQNdAdQDRwH0AzEB
+QNbAcoDMgFPAw8BFAMNAREDTQGSAy4B+QMuAfkDTQGSAxEBFgMtAUQDWgG/A1QB7wMiAfwDYAHgA0AB
cQMXAR8DBAEFAxgBIQNKAYsDXAHsA1kBw/8AEQAEAgMxAUwDYgHhAwAB/wNbAc0DGgEkAxYBHQNVAa8D
AAH/AwAB/wMAAf8DAAH/A1UBrwMWAR0DGgEkA1sBzQMAAf8DYgHhAzEBTAQCGAADWQHDA1sB5AM7AWUD
BwEJA0kBhgMhAfsDAAH/AwAB/wNZAcEDHQEpCAADCwEOA00BkgMuAfkDLgH5A00BkgM5BF4B4gMAAf8D
AAH/A1oB7QNKAYsDGAEgCAEDGAEhA0oBiwNcAewDWQHD/wARAAQCAzEBTANiAeEDAAH/A1wB2QM7AWMD
WQG7Az8B9wMiAfwDRwH0A1wB2QNGAX4DEwEaAwIBAwMaASQDWwHNAwAB/wNiAeEDMQFMBAIYAANZAcMD
XgHrA1ABmgNVAbEDTgHzAyEB+wNfAeUDSwGPAxsBJQMDAQQIAAMLAQ4DTQGSAy4B+QMhAfsDVgGzA0wB
jgNeAesDEAH+A1EB8ANXAbkDLgFHAwYBCAQABAEDGAEhA0oBiwNcAewDWQHD/wARAAQCAzEBTANiAeED
AAH/AzEB+QNcAecDRAH1A1UB8QNbAdMDUQGkAzgBWwMTARkEAgQAAxoBJANbAc0DAAH/A2IB4QMxAUwE
AhgAA1kBwwMiAfwDUQHwA0QB9QMhAf0DXgHXA0YBfgMaASQDAgEDDAADCwEOA00BkgMuAfkDEAH+A04B
8wNXAe4DPwH3A14B4wNOAZgDJwE5AwgBCgQBBAAEAQMYASEDSgGLA1wB7ANZAcP/ABEABAIDMQFMA2IB
4QMAAf8DAAH/AyAB/QNiAeEDUgGjAzsBYwMhAS8DCgENBAIIAAMaASQDWwHNAwAB/wNiAeEDMQFMBAIY
AANZAcMDAAH/AwAB/wMAAf8DXwHoA0oBiwMWAR0DBAEFEAADCwEOA00BkgMuAfkDAAH/AwAB/wM8AfYD
XAHLA0QBeQMeASoDBgEHDAAEAQMYASEDSgGLA1wB7ANZAcP/ABEABAIDMQFMA2IB4QMAAf8DMQH5A2AB
2wNFAXwDIQEwAwwBEAMDAQQQAAMaASMDXAHIAyEB/QNiAeEDMQFMBAIYAANZAcMDAAH/AyMB/ANeAesD
TgGXAyMBMwQCFAADCwEOA00BkgMuAfkDEAH+A1MB8gNYAboDNwFaAxEBFgMCAQMUAAMXAR8DSQGGA14B
6wNZAcP/ABEABAIDLQFFA14B0gNTAfIDXQHJAzIBTwMQARUDAgEDGAADEwEaA1ABnwNbAeQDWwHQAy0B
RQQCGAADVQG0A1gB7gNfAdoDRAF4AxgBIAMDAQQYAAMKAQ0DSQGGA18B6ANaAekDUAGfAyQBNAMJAQsE
ARgAAwsBDgM7AWUDWwHTA1YBsv8AFQADAwEEAycBOgM/AW0DFAEbKAADDwETAzEBTgMdASkDAgEDHAAD
DgESAzEBTQMjATMDBAEFJAADBgEHAygBPAMoATwDBgEHJAAEAQMHAQkDCwEOAwIBA/8ABQADAgEDAw8B
FANJAYgDXwHlA18B6ANfAegDXwHoA18B6ANfAegDXwHoA18B6ANfAegDXwHoA18B6ANfAegDXwHoA18B
6ANfAegDXwHoA18B6ANfAegDXwHoA1sB5ANGAX4DBQEGBAEoAAMCAQMDDQERAx8BLAMtAUYDVwG5A18B
6ANfAegDXwHoA18B6ANfAegDXwHoA2AB4wNZAb4DMgFPAw8BEwMCAQMwAAMTARkDRgF+A18B6ANfAegD
WQHBAzkBXgMmATgDDwEUBAJcAAM5AV8DXgHiA1wB5wNfAegDXwHoA18B6ANfAegDXQHcAzwBZgMGAQgD
BgEIAzwBZgNdAdwDXwHoA18B6ANfAegDXwHoA10B3ANQAZ0DJQE2IAADDAEQAzwBZgNdAc8DIAH9AyUB
+gNUAe8DWgHtA1oB7QNaAe0DWgHtA1oB7QNaAe0DWgHtA1oB7QNaAe0DWgHtA1oB7QNaAe0DWgHtA1oB
7QNUAe8DJQH6AyIB/ANWAbUDKwFCAwgBCiQABAIDGAEhA0ABcANaAb8DXQHfA1wB7QNUAe8DXAHsA2AB
5gNfAeUDWgHqA1oB7QNRAfADPQH2A10B0QNDAXcDHgErAwYBBywAAz0BaQNbAd4DAQH/AzEB+QNdAewD
WwHkA14B1wNEAXsDHgEqAwYBCFgAAz8BbAMhAf0DOgH4A1QB7wNaAe0DVwHuAyUB+gM8AfYDTwGbAx4B
KwMgAS0DUAGdAzwB9gMlAfoDVwHuA1oB7QNUAe8DPwH3A1MB8gM7AWUgAAMTARkDUAGcAz8B9wM9AfYD
XQHMA0sBjQNGAYADRgGAA0YBgANGAYADRgGAA0YBgANGAYADRgGAA0YBgANGAYADRgGAA0YBgANGAYAD
RgGAA0sBjwNcAdkDIAH9A1UB8QNOAZQDEgEYJAADBQEGAzEBTgNbAdADUwHyA14B4gNVAa4DSgGKA0YB
fwNDAXcDQwF2A0UBfANGAYADTwGbA14B3QNfAegDXwHaA04BlgMoATwDCQEMKAADRwGDAzoB+AMiAfwD
WwHTA1MBpgNcAdkDMQH5A2AB4ANQAZoDLQFEAwsBDlQAAz8BbAMiAfwDWwHTA0wBjgNGAYADSgGKA10B
3AMhAf0DXgHdAzoBYAM6AWIDXQHfAyAB/QNdAdwDSgGKA0YBgANMAY4DWwHTAyIB/AM/AWwgAAMUARsD
UgGlAx8B/QNbAdADPwFsAxgBIQMOARIDDgESAw4BEgMOARIDDgESAw4BEgMOARIDDgESAw4BEgMOARID
DgESAw4BEgMOARIDDgESAyABLgNXAbkDIAH9Ax8B/QNSAaUDFAEbJAADBQEGAzEBTQNaAccDXAHIA0AB
bwMlATcDEwEaAw4BEgMNAREDDAEQAw4BEgMOARIDHQEoAzoBYANLAY8DWwHYA2AB5gNWAbYDMQFNAwYB
CCQAA0kBhgMhAfsDMQH5A1UBrgMqAUADPwFtA10B0QNEAfUDVwHuA1gBtwM2AVcDFgEdAwwBEAQCSAAD
PwFsAzEB+QNVAa4DIAEtAw4BEgMbASUDWQG+AwAB/wNaAe0DPwFtAz8BbQNaAe0DAAH/A1kBvgMbASUD
DgESAyABLQNVAa4DMQH5Az8BbCAAAxQBGwNSAaUDHwH9A1YBtgMoATwDCAEKOAADFgEeA1YBswMgAf0D
HwH9A1IBpQMUARskAAQBAw8BFAMjATMDIQEwAwsBDgMDAQQEARQABAIDBwEJAx4BKgNLAYwDXQHfAz8B
9wNTAakDKAE7JAADSQGGAyEB+wMxAfkDUwGnAxYBHgMMARADKgFAA1gBugNPAfMDSQH0A2AB2wNXAbwD
QgF0AxMBGQMCAQNEAAM/AWwDMQH5A1MBpwMVARwEAAMPARQDVwG5AwAB/wNaAe0DPwFtAz8BbQNaAe0D
AAH/A1cBuQMPARQEAAMVARwDUwGnAzEB+QM/AWwgAAMUARsDUgGlAx8B/QNVAbQDJgE4AwcBCTgAAxYB
HgNWAbMDIAH9Ax8B/QNSAaUDFAEbYAADFQEcA1MBpwMxAfkDIQH7A0kBhiQAA0kBhgMhAfsDMQH5A1MB
pwMVARwIAAMPARMDQwF2A1wB2QMhAf0DAAH/AwAB/wNVAa8DFgEdRAADPwFsAzEB+QNTAacDFQEcBAAD
DwEUA1cBuQMAAf8DWgHtAz8BbQM/AW0DWgHtAwAB/wNXAbkDDwEUBAADFQEcA1MBpwMxAfkDPwFsIAAD
FAEbA1IBpQMfAf0DVQG0AyYBOAMHAQk4AAMWAR4DVgGzAyAB/QMfAf0DUgGlAxQBG2AAAwIBAwMeASoD
VQG0Az8B9wNSAaADGwElAwUBBhwAA0kBhgMhAfsDMQH5A1MBpwMVARwIAAQCAwkBCwMYASADRAF6A2AB
2wM9AfcDPwH3A1kBuwMqAUADDgESAwUBBgQCNAADPwFsAzEB+QNTAacDFQEcBAADDwEUA1cBuQMAAf8D
WgHtAz8BbQM/AW0DWgHtAwAB/wNXAbkDDwEUBAADFQEcA1MBpwMxAfkDPwFsIAADFAEbA1IBpQMfAf0D
VQG0AyYBOAMHAQk4AAMWAR4DVgGzAyAB/QMfAf0DUgGlAxQBG2QAAw8BEwNAAW8DXQHUA1UB7wNMAZED
EgEYHAADSQGGAyEB+wMxAfkDUwGnAxUBHBAABAIDDQERAzYBVwNYAbcDXAHsA1YB8QNdAdEDRAF5AzMB
UAMbASYDBgEHMAADPwFsAzEB+QNTAacDFQEcBAADDwEUA1cBuQMAAf8DWgHtAz8BbQM/AW0DWgHtAwAB
/wNXAbkDDwEUBAADFQEcA1MBpwMxAfkDPwFsIAADFAEbA1IBpQMfAf0DVQG0AyYBOAMHAQk4AAMWAR4D
VgGzAyAB/QMfAf0DUgGlAxQBG2QAAwIBAwMPARMDUQGeAx8B/QNSAaUDFAEbHAADSQGGAyEB+wMxAfkD
UwGnAxUBHBgAAwsBDgMtAUQDSwGPA1sBxQNiAeEDYgHhA1sBzQNMAZADKAE7AwkBDCwAAz8BbAMxAfkD
UwGnAxUBHAQAAw8BFANXAbkDAAH/A1oB7QM/AW0DPwFtA1oB7QMAAf8DVwG5Aw8BFAQAAxUBHANTAacD
MQH5Az8BbCAAAxQBGwNSAaUDHwH9A1UBtAMmATgDBwEJOAADFgEeA1YBswMgAf0DHwH9A1IBpQMUARts
AANNAZMDHwH9A1IBpQMUARscAANJAYYDIQH7AzEB+QNTAacDFQEcHAADBgEIAxkBIgMvAUkDQAFxA10B
zANOAfMDWgHpA1YBtgMtAUQsAAM/AWwDMQH5A1MBpwMVARwEAAMPARQDVwG5AwAB/wNaAe0DPwFtAz8B
bQNaAe0DAAH/A1cBuQMPARQEAAMVARwDUwGnAzEB+QM/AWwgAAMUARsDUgGlAx8B/QNVAbQDJgE4AwcB
CTgAAxYBHgNWAbMDIAH9Ax8B/QNSAaUDFAEbbAADTQGTAx8B/QNSAaUDFAEbHAADSQGGAyEB+wMxAfkD
UwGnAxUBHCAABAIDBAEFAwsBDgMwAUwDWQHBAz0B9gMvAfkDQQFyAwYBCCgAAz8BbAMxAfkDUwGnAxUB
HAQAAw8BFANXAbkDAAH/A1oB7QM/AW0DPwFtA1oB7QMAAf8DVwG5Aw8BFAQAAxUBHANTAacDMQH5Az8B
bCAAAxQBGwNSAaUDHwH9A1UBtAMmATgDBwEJOAADFgEeA1YBswMgAf0DHwH9A1IBpQMUARtsAANNAZMD
HwH9A1IBpQMUARscAANJAYYDIQH7AzEB+QNTAacDFQEcMAADFQEcA1MBpwMxAfkDWwHTAzoBYSgAAz8B
bAMxAfkDUwGnAxUBHAQAAw8BFANXAbkDAAH/A1oB7QM/AW0DPwFtA1oB7QMAAf8DVwG5Aw8BFAQAAxUB
HANTAacDMQH5Az8BbCAAAxQBGwNSAaUDHwH9A1UBtAMmATgDBwEJOAADFgEeA1YBswMgAf0DHwH9A1IB
pQMUARsQAAQBAwQBBQMLAQ4DDwETAw8BEwMPARMDDwETAw8BEwMNAREDCQEMAwMBBAQBLAADTQGTAx8B
/QNSAaUDFAEbHAADSQGGAyEB+wMxAfkDUwGnAxUBHCAABAEDAgEDAwsBDgMwAUwDWQHBAz0B9gMvAfkD
QQFyAwYBCCgAAz8BbAMxAfkDUwGnAxUBHAQAAw8BFANXAbkDAAH/A1oB7QM/AW0DPwFtA1oB7QMAAf8D
VwG5Aw8BFAQAAxUBHANTAacDMQH5Az8BbCAAAxQBGwNSAaUDHwH9A1UBtAMmATgDBwEJOAADFgEeA1YB
swMgAf0DHwH9A1IBpQMUARsQAAMGAQgDJAE1Az4BawNGAX0DRgF+A0YBfgNGAX4DRgF+A0QBewM9AWkD
JAE0AwkBDCwAA00BkwMfAf0DUgGlAxQBGxwAA0kBhgMhAfsDMQH5A1MBpwMVARwcAAMGAQgDEgEXAyMB
MwM/AW4DWwHNAz0B9gNaAeoDVgG2Ay0BRCwAAz8BbAMxAfkDUwGnAxUBHAQAAw8BFANXAbkDAAH/A1oB
7QM/AW0DPwFtA1oB7QMAAf8DVwG5Aw8BFAQAAxUBHANTAacDMQH5Az8BbCAAAxQBGwNSAaUDHwH9A1UB
tAMmATgDBwEJOAADFgEeA1YBswMgAf0DHwH9A1IBpQMUARsQAAMQARUDRgF/A1wB2QNaAeoDYAHmA2IB
4QNgAeADXgHiA1wB5wNiAeEDUAGdAyEBMCQAAwIBAwMPARMDUQGeAx8B/QNSAaUDFAEbHAADSQGGAyEB
+wMxAfkDUwGnAxUBHBgAAwsBDgMtAUQDRgGBA1MBqQNdAd8DXwHoA18B2gNOAZYDKAE8AwkBDCwAAz8B
bAMxAfkDUwGnAxUBHAQAAw8BFANXAbkDAAH/A1oB7QM/AW0DPwFtA1oB7QMAAf8DVwG5Aw8BFAQAAxUB
HANTAacDMQH5Az8BbCAAAxQBGwNSAaUDHwH9A1UBtAMmATgDBwEJOAADFgEeA1YBswMgAf0DHwH9A1IB
pQMUARsQAAMTARoDTgGYA0MB9QMAAf8DYAHgA1YBtgNZAb4DXgHXAz8B9wMiAfwDWgHHAysBQiQAAw8B
EwNAAW8DXQHUA1UB7wNNAZIDEgEYHAADSQGGAyEB+wMxAfkDUwGnAxUBHBAABAIDDQERAzYBVwNYAbcD
WgHqA10B7ANfAdUDSwGPAzoBYAMeASsDBgEHMAADPwFsAzEB+QNTAacDFQEcBAADDwEUA1cBuQMAAf8D
WgHtAz8BbQM/AW0DWgHtAwAB/wNXAbkDDwEUBAADFQEcA1MBpwMxAfkDPwFsIAADFAEbA1IBpQMfAf0D
VQG0AyYBOAMHAQk4AAMWAR4DVgGzAyAB/QMfAf0DUgGlAxQBGxAAAxQBGwNQAZoDPAH2AwAB/wNRAaQD
JAE0A0QBeANeAd0DPwH3A1sB3gM7AWMDDgESIAADAgEDAx4BKgNVAbQDPwH3A1EBoQMbASYDBQEGHAAD
SQGGAyEB+wMxAfkDUwGnAxUBHAwABAEDEwEZA0QBegNgAdsDPQH3Az8B9wNZAbsDKwFCAxMBGQMIAQoD
AgEDNAADPwFsAzEB+QNTAacDFQEcBAADDwEUA1cBuQMAAf8DWgHtAz8BbQM/AW0DWgHtAwAB/wNXAbkD
DwEUBAADFQEcA1MBpwMxAfkDPwFsIAADFAEbA1IBpQMfAf0DVQG0AyYBOAMHAQk4AAMWAR4DVgGzAyAB
/QMfAf0DUgGlAxQBGxAAAxQBGwNQAZoDPAH2AwAB/wNOAZUDMQFMA2IB4QMAAf8DWwHNAxoBJCgAAxUB
HANTAacDMQH5AyEB+wNJAYYkAANJAYYDIQH7AzEB+QNTAacDFQEcDAADCwEOA00BkgMuAfkDAAH/AwAB
/wNVAa8DFgEdRAADPwFsAzEB+QNTAacDFQEcBAADDwEUA1cBuQMAAf8DWgHtAz8BbQM/AW0DWgHtAwAB
/wNXAbkDDwEUBAADFQEcA1MBpwMxAfkDPwFsIAADFAEbA1IBpQMfAf0DVgG2AygBPAMIAQo4AAMWAR4D
VgGzAyAB/QMfAf0DUgGlAxQBGxAAAxQBGwNQAZoDPAH2AwAB/wNEAfUDVgHvAyMB/AMSAf4DXQHMAx0B
KQQCGAAEAgMGAQcDDwETAzIBTwNZAcADQwH1A10B0QM6AWAkAANJAYYDIQH7AzEB+QNTAacDFgEeAwwB
EAMqAUADVwG5A1oB6QNVAe8DXQHoA10BzwNGAX0DEwEaAwIBA0QAAz8BbAMxAfkDUwGnAxUBHAQAAw8B
FANXAbkDAAH/A1oB7QM/AW0DPwFtA1oB7QMAAf8DVwG5Aw8BFAQAAxUBHANTAacDMQH5Az8BbCAAAxQB
GwNSAaUDHwH9A1sB0AM/AWwDGAEhAw4BEgMOARIDDgESAw4BEgMOARIDDgESAw4BEgMOARIDDgESAw4B
EgMOARIDDgESAw4BEgMOARIDIAEuA1cBuQMgAf0DHwH9A1IBpQMUARsQAAMUARsDUAGaAzwB9gMAAf8D
AAH/AyQB+gNDAfUDIAH9A2AB4ANAAXEDHQEoAw4BEgMNAREDCQEMAwgBCgMLBA4BEgMcAScDOAFbA0QB
eQNaAccDYgHhA1YBtgM0AVQDDAEPJAADSQGGAyEB+wMxAfkDVQGuAyoBQAM/AW0DXQHRA0QB9QNXAe4D
WQG7A0QBeQMqAUADEQEWBAJIAAM/AWwDMQH5A1UBrgMgAS0DDgESAxsBJQNZAb4DAAH/A1oB7QM/AW0D
PwFtA1oB7QMAAf8DWQG+AxsBJQMOARIDIAEtA1UBrgMxAfkDPwFsIAADEwEaA1EBpAMhAfwDPAH2A10B
zANLAY0DRgGAA0YBgANGAYADRgGAA0YBgANGAYADRgGAA0YBgANGAYADRgGAA0YBgANGAYADRgGAA0YB
gANLAY8DXAHZAyAB/QNHAfQDTgGYAxMBGRAAAxIBGANLAYwDWgHtAwAB/wMiAf0DXAHZA1UBsQNdAd8D
RAH1A2AB4ANPAZsDRgGAA0QBegM5AV4DMwFQAz0BaANGAX4DUAGaA18B2gNbAeQDXwHaA08BmwMpAT4D
CgENKAADSQGGAyEB+wMiAfwDWwHTA1MBpgNcAdkDMQH5A2AB4ANQAZoDLQFFAxABFQMGAQcEAUwAAz8B
bAMiAfwDWwHTA0wBjgNGAYADSgGKA10B3AMgAf0DXQHfAzoBYgM6AWIDXQHfAyAB/QNdAdwDSgGKA0YB
gANMAY4DWwHTAyIB/AM/AWwgAAMSARcDSgGLA1wB6gMQAf4DJQH6A1QB7wNaAe0DWgHtA1oB7QNaAe0D
WgHtA1oB7QNaAe0DWgHtA1oB7QNaAe0DWgHtA1oB7QNaAe0DWgHtA1QB7wMlAfoDIAH9A1cBwgM0AVQD
CwEOEAADCAEKAy4BSANVAbEDXgHiA2AB2wNPAZsDLQFEA0MBdwNdAcwDYgHhA1oB6gNaAe0DWgHpA1sB
0wNcAcgDXwHaA14B6wNQAfIDMQH5A14B0gNEAXgDIQEvAwcBCSwAA0IBdQNaAeoDAQH/AzEB+QNRAfAD
XQHsA1wB2QNEAXsDHgEqAwYBCFgAAz8BbAMBAf8DMQH5A1QB7wNaAe0DVwHuAyUB+gM8AfYDUAGdAyAB
LQMgAS0DUAGdAzwB9gMlAfoDVwHuA1oB7QNUAe8DMQH5AxAB/gM/AWwgAAMFAQYDGgEjA00BkwNhAeYD
XAHtAz8B9gM/AfcDPwH3Az8B9wM/AfcDPwH3Az8B9wM/AfcDPwH3Az8B9wM/AfcDPwH3Az8B9wM/AfcD
PwH3Az8B9gNcAe0DXwHlA0cBgwMKAQ0EAhAABAEDCAEKAx8BLAMuAUgDLQFEAxsBJQMHAQkDDwETAyMB
MgMuAUcDVwG5A18B6ANaAekDWAHuA1YB8QNdAewDWgHpA18B6ANbAdADNAFTAw8BEwMCAQMwAAMWAR4D
RwGDA1wB7QNWAfEDXwHVA0wBjgMrAUIDDwEUBAJcAAM6AWIDWgHpA1wB7QM/AfYDPwH3Az8B9gNdAewD
XQHcAzwBZgMGAQgDBgEIAzwBZgNdAdwDXAHtAz8B9gM/AfcDPwH2A1wB7QNhAeYDOgFiLAAEAQMjATMD
TgGXA1QBqwNUAasDVAGrA1QBqwNUAasDVAGrA1QBqwNUAasDVAGrA1QBqwNUAasDVAGrA1QBqwNUAasD
TgGXAyMBMwQBTAADCQELAzkBXQNHAYIDJwE6AwQBBUgABAIDIgExAzoBYQMPARRwAAMDAQQDKAE7A04B
mANUAasDUQGeAyEBLxQAAwMBBAMlATcDUAGfA1QBqwNOAZgDKAE7AwMBBBgAAUIBTQE+BwABPgMAASgD
AAF8AwABMAMAAQEBAAEBBgABAxYAA/8BAAH8AQMB8AE/AQMB8AEPAcAIAAH8AQEB8AE/AQMB8AEHAcAI
AAH8AQABMAE/AQAB8AEDAYAIAAH8AQABEAE/AQABcAEAAYAIAAH8AgABPwEAATABAAGACAAB/AIAAT8B
AAEwAQABgAgAAfwCAAE/DAAB/AEIAQABPwEICwAB/AEMAQABPwEMAQABIAkAAfwBDwEAAT8BDAEAATAJ
AAH8AQ8BgAE/AQ4BAAE4CQAB/AEPAfABPwEPAcABPwkAAfwBDwGAAT8BDgEAATgJAAH8AQ8BAAE/AQwB
AAEwCQAB/AEMAQABPwEMAQABIAkAAfwBCAEAAT8BCAsAAfwCAAE/DAAB/AIAAT8BAAEwCgAB/AIAAT8B
AAEwAQABgAgAAfwBAAEQAT8BAAFwAQABgAgAAfwBAAEwAT8BAAHwAQMBgAgAAfwBAAHwAT8BAQHwAQcB
wAgAAfwBAwHwAT8BAwHwAQ8BwAgAAf4BHwH4AX8BDwH4AX8BwAgAAeACAAEHAf4BAAEBAf8B4AEPAv8B
4AEAAQEB8AHgAgABBwH8AgAB/wHgAQcC/wHgAQABAQHwAeACAAEHAfwCAAF/AeABAwL/AeABAAEBAfAB
4AIAAQcB/AIAAT8B4AEAAX8B/wHgAQABAQHwAeABfwH+AQcB/AEHAcABPwHgAQABPwH/AeEBAAEhAfAB
4AF/Af4BBwL/AfgBPwHgAcABPwH/AeEBAAEhAfAB4AF/Af4BBwL/AfgBDwHgAcABAwH/AeEBAAEhAfAB
4AF/Af4BBwL/AfwBDwHgAfABAQH/AeEBAAEhAfAB4AF/Af4BBwL/AfwBDwHgAfwBAAH/AeEBAAEhAfAB
4AF/Af4BBwP/AQ8B4AH+AQAB/wHhAQABIQHwAeABfwH+AQcD/wEPAeAB/wEAAX8B4QEAASEB8AHgAX8B
/gEHA/8BDwHgAf8B8AF/AeEBAAEhAfAB4AF/Af4BBwGAAQcB/wEPAeAB/wEAAX8B4QEAASEB8AHgAX8B
/gEHAYABBwH/AQ8B4AH+AQAB/wHhAQABIQHwAeABfwH+AQcBgAEHAfwBDwHgAfwBAAH/AeEBAAEhAfAB
4AF/Af4BBwGAAQcB/AEPAeAB8AEBAf8B4QEAASEB8AHgAX8B/gEHAYABBwH4AQ8C4AEDAf8B4QEAASEB
8AHgAX8B/gEHAYABHwH4AT8C4AE/Af8B4QEAASEB8AHgAX8B/gEHAYABDwHAAT8B4AEAAT8B/wHhAQAB
IQHwAeACAAEHAYACAAE/AeABAAF/Af8B4AEAAQEB8AHgAgABBwGAAgABfwHgAQAC/wHgAQABAQHwAeAC
AAEHAYACAAH/AeABBwL/AeABAAEBAfAB4AIAAQcBgAEAAQEB/wHgAQ8C/wHgAQABAQHwAfwCAAE/Af8B
+AE/Af8B8AP/AfABPgEDAfAL
</value>
</data>
<metadata name="toolTip.TrayLocation" type="System.Drawing.Point, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a">
<value>165, 17</value>
</metadata>
</root>

View File

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

View 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(225, 34);
//
// toolStripMenuItem_ReloadSkins
//
toolStripMenuItem_ReloadSkins.Name = "toolStripMenuItem_ReloadSkins";
toolStripMenuItem_ReloadSkins.Size = new Size(224, 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;
}
}

View 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();
}
}
}

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

View File

@@ -1,4 +1,5 @@
using System;
using SpineViewer.Utils;
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Diagnostics;
@@ -15,12 +16,20 @@ namespace SpineViewer.Dialogs
public AboutDialog()
{
InitializeComponent();
Text = $"关于 {Program.Name}";
Text = $"关于 {ProgramName}";
label_Version.Text = $"v{InformationalVersion}";
}
public string InformationalVersion =>
Assembly.GetExecutingAssembly().GetCustomAttribute<AssemblyInformationalVersionAttribute>()?.InformationalVersion;
public string ProgramName => Process.GetCurrentProcess().ProcessName;
public string InformationalVersion
=> Assembly.GetExecutingAssembly().GetCustomAttribute<AssemblyInformationalVersionAttribute>()?.InformationalVersion;
public string ProgramUrl
{
get => linkLabel_RepoUrl.Text;
set => linkLabel_RepoUrl.Text = value;
}
private void linkLabel_RepoUrl_LinkClicked(object sender, LinkLabelLinkClickedEventArgs e)
{
@@ -32,7 +41,7 @@ namespace SpineViewer.Dialogs
else
{
Clipboard.SetText(url);
MessageBox.Info("链接已复制到剪贴板,请前往浏览器进行访问");
MessagePopup.Info("链接已复制到剪贴板,请前往浏览器进行访问");
}
}
}

View File

@@ -1,4 +1,5 @@
using SpineViewer.Spine;
using SpineViewer.Utils;
using System;
using System.Collections.Generic;
using System.ComponentModel;
@@ -21,21 +22,21 @@ namespace SpineViewer.Dialogs
public BatchOpenSpineDialog()
{
InitializeComponent();
comboBox_Version.DataSource = VersionHelper.Names.ToList();
comboBox_Version.DataSource = SpineUtils.Names.ToList();
comboBox_Version.DisplayMember = "Value";
comboBox_Version.ValueMember = "Key";
comboBox_Version.SelectedValue = Spine.Version.Auto;
comboBox_Version.SelectedValue = SpineVersion.Auto;
}
private void button_Ok_Click(object sender, EventArgs e)
{
var version = (Spine.Version)comboBox_Version.SelectedValue;
var version = (SpineVersion)comboBox_Version.SelectedValue;
var items = skelFileListBox.Items;
if (items.Count <= 0)
{
MessageBox.Info("未选择任何文件");
MessagePopup.Info("未选择任何文件");
return;
}
@@ -43,14 +44,14 @@ namespace SpineViewer.Dialogs
{
if (!File.Exists(p))
{
MessageBox.Info($"{p}", "skel文件不存在");
MessagePopup.Info($"{p}", "skel文件不存在");
return;
}
}
if (version != Spine.Version.Auto && !Spine.Spine.ImplementedVersions.Contains(version))
if (version != SpineVersion.Auto && !Spine.SpineObject.HasImplementation(version))
{
MessageBox.Info($"{version.GetName()} 版本尚未实现(咕咕咕~");
MessagePopup.Info($"{version.GetName()} 版本尚未实现(咕咕咕~");
return;
}
@@ -67,12 +68,12 @@ namespace SpineViewer.Dialogs
/// <summary>
/// 批量打开对话框结果
/// </summary>
public class BatchOpenSpineDialogResult(Spine.Version version, string[] skelPaths)
public class BatchOpenSpineDialogResult(SpineVersion version, string[] skelPaths)
{
/// <summary>
/// 版本
/// </summary>
public Spine.Version Version => version;
public SpineVersion Version => version;
/// <summary>
/// 路径列表

View File

@@ -31,6 +31,7 @@
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();
@@ -44,11 +45,15 @@
button_Cancel = new Button();
label2 = new Label();
skelFileListBox = new SpineViewer.Controls.SkelFileListBox();
openFileDialog_Skel = new OpenFileDialog();
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
@@ -58,7 +63,7 @@
panel.Location = new Point(0, 0);
panel.Name = "panel";
panel.Padding = new Padding(50, 15, 50, 10);
panel.Size = new Size(1051, 538);
panel.Size = new Size(1051, 702);
panel.TabIndex = 2;
//
// tableLayoutPanel1
@@ -66,34 +71,48 @@
tableLayoutPanel1.ColumnCount = 2;
tableLayoutPanel1.ColumnStyles.Add(new ColumnStyle());
tableLayoutPanel1.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 100F));
tableLayoutPanel1.Controls.Add(comboBox_TargetVersion, 1, 3);
tableLayoutPanel1.Controls.Add(flowLayoutPanel_TargetFormat, 1, 4);
tableLayoutPanel1.Controls.Add(label1, 0, 3);
tableLayoutPanel1.Controls.Add(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, 2);
tableLayoutPanel1.Controls.Add(comboBox_SourceVersion, 1, 2);
tableLayoutPanel1.Controls.Add(tableLayoutPanel2, 0, 5);
tableLayoutPanel1.Controls.Add(label2, 0, 4);
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 = 6;
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.Size = new Size(951, 513);
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(95, 365);
comboBox_TargetVersion.Location = new Point(113, 535);
comboBox_TargetVersion.Name = "comboBox_TargetVersion";
comboBox_TargetVersion.Size = new Size(182, 32);
comboBox_TargetVersion.Sorted = true;
@@ -105,9 +124,10 @@
flowLayoutPanel_TargetFormat.Controls.Add(radioButton_BinaryTarget);
flowLayoutPanel_TargetFormat.Controls.Add(radioButton_JsonTarget);
flowLayoutPanel_TargetFormat.Dock = DockStyle.Fill;
flowLayoutPanel_TargetFormat.Location = new Point(95, 403);
flowLayoutPanel_TargetFormat.Location = new Point(110, 570);
flowLayoutPanel_TargetFormat.Margin = new Padding(0);
flowLayoutPanel_TargetFormat.Name = "flowLayoutPanel_TargetFormat";
flowLayoutPanel_TargetFormat.Size = new Size(853, 34);
flowLayoutPanel_TargetFormat.Size = new Size(841, 34);
flowLayoutPanel_TargetFormat.TabIndex = 19;
//
// radioButton_BinaryTarget
@@ -136,7 +156,7 @@
//
label1.Anchor = AnchorStyles.Right;
label1.AutoSize = true;
label1.Location = new Point(3, 369);
label1.Location = new Point(21, 539);
label1.Name = "label1";
label1.Size = new Size(86, 24);
label1.TabIndex = 15;
@@ -152,14 +172,14 @@
label4.Name = "label4";
label4.Size = new Size(921, 24);
label4.TabIndex = 14;
label4.Text = "说明:在每个文件同级目录下生成目标格式后缀的文件,会覆盖已存在文件";
label4.Text = "说明:输出文件夹留空则在每个文件同级目录下生成目标格式后缀的文件,视情况会覆盖已存在文件";
label4.TextAlign = ContentAlignment.MiddleCenter;
//
// label3
//
label3.Anchor = AnchorStyles.Right;
label3.AutoSize = true;
label3.Location = new Point(21, 331);
label3.Location = new Point(39, 501);
label3.Name = "label3";
label3.Size = new Size(68, 24);
label3.TabIndex = 12;
@@ -170,7 +190,7 @@
comboBox_SourceVersion.Anchor = AnchorStyles.Left;
comboBox_SourceVersion.DropDownStyle = ComboBoxStyle.DropDownList;
comboBox_SourceVersion.FormattingEnabled = true;
comboBox_SourceVersion.Location = new Point(95, 327);
comboBox_SourceVersion.Location = new Point(113, 497);
comboBox_SourceVersion.Name = "comboBox_SourceVersion";
comboBox_SourceVersion.Size = new Size(182, 32);
comboBox_SourceVersion.Sorted = true;
@@ -187,7 +207,7 @@
tableLayoutPanel2.Controls.Add(button_Ok, 0, 0);
tableLayoutPanel2.Controls.Add(button_Cancel, 1, 0);
tableLayoutPanel2.Dock = DockStyle.Fill;
tableLayoutPanel2.Location = new Point(3, 470);
tableLayoutPanel2.Location = new Point(3, 634);
tableLayoutPanel2.Margin = new Padding(3, 30, 3, 3);
tableLayoutPanel2.Name = "tableLayoutPanel2";
tableLayoutPanel2.RowCount = 1;
@@ -223,7 +243,7 @@
//
label2.Anchor = AnchorStyles.Right;
label2.AutoSize = true;
label2.Location = new Point(3, 408);
label2.Location = new Point(21, 575);
label2.Name = "label2";
label2.Size = new Size(86, 24);
label2.TabIndex = 16;
@@ -235,16 +255,48 @@
skelFileListBox.Dock = DockStyle.Fill;
skelFileListBox.Location = new Point(3, 57);
skelFileListBox.Name = "skelFileListBox";
skelFileListBox.Size = new Size(945, 264);
skelFileListBox.Size = new Size(945, 394);
skelFileListBox.TabIndex = 20;
//
// openFileDialog_Skel
// tableLayoutPanel3
//
openFileDialog_Skel.AddExtension = false;
openFileDialog_Skel.AddToRecent = false;
openFileDialog_Skel.Filter = "skel 文件 (*.skel; *.json)|*.skel;*.json|二进制文件 (*.skel)|*.skel|文本文件 (*.json)|*.json|所有文件 (*.*)|*.*";
openFileDialog_Skel.Multiselect = true;
openFileDialog_Skel.Title = "批量选择skel文件";
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
//
@@ -252,7 +304,7 @@
AutoScaleDimensions = new SizeF(11F, 24F);
AutoScaleMode = AutoScaleMode.Font;
CancelButton = button_Cancel;
ClientSize = new Size(1051, 538);
ClientSize = new Size(1051, 702);
Controls.Add(panel);
FormBorderStyle = FormBorderStyle.FixedDialog;
Icon = (Icon)resources.GetObject("$this.Icon");
@@ -268,6 +320,8 @@
flowLayoutPanel_TargetFormat.ResumeLayout(false);
flowLayoutPanel_TargetFormat.PerformLayout();
tableLayoutPanel2.ResumeLayout(false);
tableLayoutPanel3.ResumeLayout(false);
tableLayoutPanel3.PerformLayout();
ResumeLayout(false);
}
@@ -281,7 +335,6 @@
private TableLayoutPanel tableLayoutPanel2;
private Button button_Ok;
private Button button_Cancel;
private OpenFileDialog openFileDialog_Skel;
private Label label1;
private Label label2;
private FlowLayoutPanel flowLayoutPanel_TargetFormat;
@@ -289,5 +342,10 @@
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;
}
}

View File

@@ -1,4 +1,6 @@
using SpineViewer.Spine;
using NLog;
using SpineViewer.Spine;
using SpineViewer.Utils;
using System;
using System.Collections.Generic;
using System.ComponentModel;
@@ -13,6 +15,8 @@ namespace SpineViewer.Dialogs
{
public partial class ConvertFileFormatDialog : Form
{
private readonly Logger logger = LogManager.GetCurrentClassLogger();
/// <summary>
/// 对话框结果, 取消时为 null
/// </summary>
@@ -22,56 +26,95 @@ namespace SpineViewer.Dialogs
{
InitializeComponent();
comboBox_SourceVersion.DataSource = VersionHelper.Names.ToList();
comboBox_SourceVersion.DataSource = SpineUtils.Names.ToList();
comboBox_SourceVersion.DisplayMember = "Value";
comboBox_SourceVersion.ValueMember = "Key";
comboBox_SourceVersion.SelectedValue = Spine.Version.Auto;
comboBox_SourceVersion.SelectedValue = SpineVersion.Auto;
// 目标版本不包含自动
var versionsWithoutAuto = VersionHelper.Names.ToDictionary();
versionsWithoutAuto.Remove(Spine.Version.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 = Spine.Version.V38;
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 sourceVersion = (Spine.Version)comboBox_SourceVersion.SelectedValue;
var targetVersion = (Spine.Version)comboBox_TargetVersion.SelectedValue;
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)
{
MessageBox.Info("未选择任何文件");
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))
{
MessageBox.Info($"{p}", "skel文件不存在");
MessagePopup.Info($"{p}", "skel文件不存在");
return;
}
}
if (sourceVersion != Spine.Version.Auto && !SkeletonConverter.ImplementedVersions.Contains(sourceVersion))
if (sourceVersion != SpineVersion.Auto && !SkeletonConverter.HasImplementation(sourceVersion))
{
MessageBox.Info($"{sourceVersion.GetName()} 版本尚未实现(咕咕咕~");
MessagePopup.Info($"{sourceVersion.GetName()} 版本尚未实现(咕咕咕~");
return;
}
if (!SkeletonConverter.ImplementedVersions.Contains(targetVersion))
if (!SkeletonConverter.HasImplementation(targetVersion))
{
MessageBox.Info($"{targetVersion.GetName()} 版本尚未实现(咕咕咕~");
MessagePopup.Info($"{targetVersion.GetName()} 版本尚未实现(咕咕咕~");
return;
}
Result = new(items.Cast<string>().ToArray(), sourceVersion, targetVersion, jsonTarget);
Result = new(outputDir, items.Cast<string>().ToArray(), sourceVersion, targetVersion, jsonTarget);
DialogResult = DialogResult.OK;
}
@@ -84,8 +127,13 @@ namespace SpineViewer.Dialogs
/// <summary>
/// 文件格式转换对话框结果包装类
/// </summary>
public class ConvertFileFormatDialogResult(string[] skelPaths, Spine.Version sourceVersion, Spine.Version targetVersion, bool jsonTarget)
public class ConvertFileFormatDialogResult(string? outputDir, string[] skelPaths, SpineVersion sourceVersion, SpineVersion targetVersion, bool jsonTarget)
{
/// <summary>
/// 输出文件夹, 如果为空, 则将转换后的文件转换到各自的文件夹下
/// </summary>
public string? OutputDir => outputDir;
/// <summary>
/// 骨骼文件路径列表
/// </summary>
@@ -94,12 +142,12 @@ namespace SpineViewer.Dialogs
/// <summary>
/// 源版本
/// </summary>
public Spine.Version SourceVersion => sourceVersion;
public SpineVersion SourceVersion => sourceVersion;
/// <summary>
/// 目标版本
/// </summary>
public Spine.Version TargetVersion => targetVersion;
public SpineVersion TargetVersion => targetVersion;
/// <summary>
/// 目标格式是否为 Json

View File

@@ -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="openFileDialog_Skel.TrayLocation" type="System.Drawing.Point, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a">
<value>92, 26</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">

View File

@@ -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,44 +62,28 @@ 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();
}
}
@@ -86,7 +93,7 @@ namespace SpineViewer.Dialogs
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.Info("已复制");
MessagePopup.Info("已复制");
}
}
}

View File

@@ -1,6 +1,6 @@
namespace SpineViewer.Dialogs
{
partial class ExportPreviewDialog
partial class ExportDialog
{
/// <summary>
/// Required designer variable.
@@ -28,18 +28,13 @@
/// </summary>
private void InitializeComponent()
{
System.ComponentModel.ComponentResourceManager resources = new System.ComponentModel.ComponentResourceManager(typeof(ExportPreviewDialog));
System.ComponentModel.ComponentResourceManager resources = new System.ComponentModel.ComponentResourceManager(typeof(ExportDialog));
panel1 = new Panel();
tableLayoutPanel1 = new TableLayoutPanel();
propertyGrid = new PropertyGrid();
label4 = new Label();
label1 = new Label();
textBox_OutputDir = new TextBox();
button_SelectOutputDir = new Button();
propertyGrid_ExportArgs = new PropertyGrid();
tableLayoutPanel2 = new TableLayoutPanel();
button_Ok = new Button();
button_Cancel = new Button();
folderBrowserDialog = new FolderBrowserDialog();
panel1.SuspendLayout();
tableLayoutPanel1.SuspendLayout();
tableLayoutPanel2.SuspendLayout();
@@ -52,112 +47,59 @@
panel1.Location = new Point(0, 0);
panel1.Name = "panel1";
panel1.Padding = new Padding(50, 15, 50, 10);
panel1.Size = new Size(914, 482);
panel1.Size = new Size(793, 841);
panel1.TabIndex = 2;
//
// tableLayoutPanel1
//
tableLayoutPanel1.AutoSize = true;
tableLayoutPanel1.ColumnCount = 4;
tableLayoutPanel1.ColumnStyles.Add(new ColumnStyle());
tableLayoutPanel1.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 50F));
tableLayoutPanel1.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 50F));
tableLayoutPanel1.ColumnStyles.Add(new ColumnStyle());
tableLayoutPanel1.Controls.Add(propertyGrid, 0, 2);
tableLayoutPanel1.Controls.Add(label4, 0, 0);
tableLayoutPanel1.Controls.Add(label1, 0, 1);
tableLayoutPanel1.Controls.Add(textBox_OutputDir, 1, 1);
tableLayoutPanel1.Controls.Add(button_SelectOutputDir, 3, 1);
tableLayoutPanel1.Controls.Add(tableLayoutPanel2, 0, 3);
tableLayoutPanel1.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 = 4;
tableLayoutPanel1.RowStyles.Add(new RowStyle());
tableLayoutPanel1.RowStyles.Add(new RowStyle());
tableLayoutPanel1.RowCount = 2;
tableLayoutPanel1.RowStyles.Add(new RowStyle(SizeType.Percent, 100F));
tableLayoutPanel1.RowStyles.Add(new RowStyle());
tableLayoutPanel1.Size = new Size(814, 457);
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
// propertyGrid_ExportArgs
//
tableLayoutPanel1.SetColumnSpan(propertyGrid, 4);
propertyGrid.Dock = DockStyle.Fill;
propertyGrid.HelpVisible = false;
propertyGrid.Location = new Point(3, 97);
propertyGrid.Name = "propertyGrid";
propertyGrid.Size = new Size(808, 284);
propertyGrid.TabIndex = 1;
propertyGrid.ToolbarVisible = false;
//
// label4
//
label4.AutoSize = true;
tableLayoutPanel1.SetColumnSpan(label4, 4);
label4.Dock = DockStyle.Fill;
label4.Location = new Point(15, 15);
label4.Margin = new Padding(15);
label4.Name = "label4";
label4.Size = new Size(784, 24);
label4.TabIndex = 11;
label4.Text = "说明:输出文件夹为可选项,留空则将预览图输出到每个骨骼文件所在目录";
label4.TextAlign = ContentAlignment.MiddleCenter;
//
// label1
//
label1.Anchor = AnchorStyles.Right;
label1.AutoSize = true;
label1.Location = new Point(3, 62);
label1.Name = "label1";
label1.Size = new Size(104, 24);
label1.TabIndex = 0;
label1.Text = "输出文件夹:";
//
// textBox_OutputDir
//
tableLayoutPanel1.SetColumnSpan(textBox_OutputDir, 2);
textBox_OutputDir.Dock = DockStyle.Fill;
textBox_OutputDir.Location = new Point(113, 57);
textBox_OutputDir.Name = "textBox_OutputDir";
textBox_OutputDir.Size = new Size(660, 30);
textBox_OutputDir.TabIndex = 3;
//
// button_SelectOutputDir
//
button_SelectOutputDir.AutoSize = true;
button_SelectOutputDir.AutoSizeMode = AutoSizeMode.GrowAndShrink;
button_SelectOutputDir.Location = new Point(779, 57);
button_SelectOutputDir.Name = "button_SelectOutputDir";
button_SelectOutputDir.Size = new Size(32, 34);
button_SelectOutputDir.TabIndex = 5;
button_SelectOutputDir.Text = "...";
button_SelectOutputDir.UseVisualStyleBackColor = true;
button_SelectOutputDir.Click += button_SelectOutputDir_Click;
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;
tableLayoutPanel1.SetColumnSpan(tableLayoutPanel2, 4);
tableLayoutPanel2.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 50F));
tableLayoutPanel2.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 50F));
tableLayoutPanel2.Controls.Add(button_Ok, 0, 0);
tableLayoutPanel2.Controls.Add(button_Cancel, 1, 0);
tableLayoutPanel2.Dock = DockStyle.Bottom;
tableLayoutPanel2.Location = new Point(3, 414);
tableLayoutPanel2.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(808, 40);
tableLayoutPanel2.Size = new Size(687, 40);
tableLayoutPanel2.TabIndex = 10;
//
// button_Ok
//
button_Ok.Anchor = AnchorStyles.Bottom | AnchorStyles.Right;
button_Ok.Location = new Point(262, 3);
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);
@@ -169,7 +111,7 @@
// button_Cancel
//
button_Cancel.Anchor = AnchorStyles.Bottom | AnchorStyles.Left;
button_Cancel.Location = new Point(434, 3);
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);
@@ -178,26 +120,21 @@
button_Cancel.UseVisualStyleBackColor = true;
button_Cancel.Click += button_Cancel_Click;
//
// folderBrowserDialog
//
folderBrowserDialog.AddToRecent = false;
//
// ExportPreviewDialog
// ExportDialog
//
AcceptButton = button_Ok;
AutoScaleDimensions = new SizeF(11F, 24F);
AutoScaleMode = AutoScaleMode.Font;
CancelButton = button_Cancel;
ClientSize = new Size(914, 482);
ClientSize = new Size(793, 841);
Controls.Add(panel1);
FormBorderStyle = FormBorderStyle.FixedDialog;
Icon = (Icon)resources.GetObject("$this.Icon");
MaximizeBox = false;
MinimizeBox = false;
Name = "ExportPreviewDialog";
Name = "ExportDialog";
ShowInTaskbar = false;
StartPosition = FormStartPosition.CenterScreen;
Text = "导出预览图";
Text = "导出参数";
panel1.ResumeLayout(false);
panel1.PerformLayout();
tableLayoutPanel1.ResumeLayout(false);
@@ -210,14 +147,9 @@
private Panel panel1;
private TableLayoutPanel tableLayoutPanel1;
private Label label4;
private Label label1;
private TextBox textBox_OutputDir;
private Button button_SelectOutputDir;
private TableLayoutPanel tableLayoutPanel2;
private Button button_Ok;
private Button button_Cancel;
private FolderBrowserDialog folderBrowserDialog;
private PropertyGrid propertyGrid;
private PropertyGrid propertyGrid_ExportArgs;
}
}

View 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;
}
}
}

View File

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

View File

@@ -1,270 +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序列";
Load += ExportPngDialog_Load;
panel1.ResumeLayout(false);
panel1.PerformLayout();
tableLayoutPanel1.ResumeLayout(false);
tableLayoutPanel1.PerformLayout();
tableLayoutPanel2.ResumeLayout(false);
((System.ComponentModel.ISupportInitialize)numericUpDown_Duration).EndInit();
((System.ComponentModel.ISupportInitialize)numericUpDown_Fps).EndInit();
ResumeLayout(false);
}
#endregion
private Panel panel1;
private TableLayoutPanel tableLayoutPanel1;
private Label label4;
private Label label1;
private Label label2;
private Label label3;
private TextBox textBox_OutputDir;
private Button button_SelectOutputDir;
private TableLayoutPanel tableLayoutPanel2;
private Button button_Ok;
private Button button_Cancel;
private NumericUpDown numericUpDown_Duration;
private NumericUpDown numericUpDown_Fps;
private FolderBrowserDialog folderBrowserDialog;
}
}

View File

@@ -1,78 +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
{
// TODO: 该对话框要合并到统一的导出参数对话框
// TODO: 使用结果包装类
public string OutputDir { get; private set; }
public float Duration { get; private set; }
public uint Fps { get; private set; }
public ExportPngDialog()
{
InitializeComponent();
}
private void ExportPngDialog_Load(object sender, EventArgs e)
{
button_SelectOutputDir_Click(sender, e);
}
private void button_SelectOutputDir_Click(object sender, EventArgs e)
{
folderBrowserDialog.InitialDirectory = textBox_OutputDir.Text;
if (folderBrowserDialog.ShowDialog() == DialogResult.OK)
{
textBox_OutputDir.Text = Path.GetFullPath(folderBrowserDialog.SelectedPath);
}
}
private void button_Ok_Click(object sender, EventArgs e)
{
var outputDir = textBox_OutputDir.Text;
if (File.Exists(outputDir))
{
MessageBox.Info("输出文件夹无效");
return;
}
if (!Directory.Exists(outputDir))
{
if (MessageBox.Quest($"文件夹 {outputDir} 不存在,是否创建?") != DialogResult.OK)
return;
try
{
Directory.CreateDirectory(outputDir);
}
catch (Exception ex)
{
Program.Logger.Error(ex.ToString());
MessageBox.Error(ex.ToString(), "文件夹创建失败");
return;
}
}
OutputDir = Path.GetFullPath(outputDir);
Duration = (float)numericUpDown_Duration.Value;
Fps = (uint)numericUpDown_Fps.Value;
DialogResult = DialogResult.OK;
}
private void button_Cancel_Click(object sender, EventArgs e)
{
DialogResult = DialogResult.Cancel;
}
}
}

View File

@@ -1,169 +0,0 @@
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Drawing.Imaging;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms;
namespace SpineViewer.Dialogs
{
public partial class ExportPreviewDialog: Form
{
/// <summary>
/// 对话框结果
/// </summary>
public readonly ExportPreviewDialogResult Result = new();
public ExportPreviewDialog()
{
InitializeComponent();
propertyGrid.SelectedObject = Result;
}
private void button_SelectOutputDir_Click(object sender, EventArgs e)
{
folderBrowserDialog.InitialDirectory = textBox_OutputDir.Text;
if (folderBrowserDialog.ShowDialog() != DialogResult.OK)
return;
textBox_OutputDir.Text = Path.GetFullPath(folderBrowserDialog.SelectedPath);
}
private void button_Ok_Click(object sender, EventArgs e)
{
var outputDir = textBox_OutputDir.Text;
if (string.IsNullOrEmpty(outputDir))
{
outputDir = null;
}
else
{
if (File.Exists(outputDir))
{
MessageBox.Info("输出文件夹无效");
return;
}
if (!Directory.Exists(outputDir))
{
if (MessageBox.Quest($"文件夹 {outputDir} 不存在,是否创建?") != DialogResult.OK)
return;
try
{
Directory.CreateDirectory(outputDir);
}
catch (Exception ex)
{
Program.Logger.Error(ex.ToString());
MessageBox.Error(ex.ToString(), "文件夹创建失败");
return;
}
}
outputDir = Path.GetFullPath(outputDir);
}
if (outputDir is null && string.IsNullOrEmpty(Result.NameSuffix))
{
MessageBox.Info("输出文件夹和名称后缀不可同时为空,存在文件覆盖风险");
return;
}
Result.OutputDir = outputDir;
DialogResult = DialogResult.OK;
}
private void button_Cancel_Click(object sender, EventArgs e)
{
DialogResult = DialogResult.Cancel;
}
}
public class ExportPreviewDialogResult
{
/// <summary>
/// 输出路径
/// </summary>
[Browsable(false)]
public string? OutputDir { get; set; } = null;
/// <summary>
/// 预览图格式
/// </summary>
[TypeConverter(typeof(ImageFormatConverter))]
[Category("图像"), DisplayName("预览图格式")]
public ImageFormat ImageFormat
{
get => imageFormat;
set
{
if (value == ImageFormat.MemoryBmp) value = ImageFormat.Bmp;
imageFormat = value;
}
}
private ImageFormat imageFormat = ImageFormat.Png;
/// <summary>
/// 预览图分辨率
/// </summary>
[TypeConverter(typeof(SizeConverter))]
[Category("图像"), DisplayName("分辨率")]
public Size Resolution
{
get => resolution;
set
{
if (value.Width <= 0) value.Width = 128;
if (value.Height <= 0) value.Height = 128;
resolution = value;
}
}
private Size resolution = new(512, 512);
/// <summary>
/// 四周填充像素值
/// </summary>
[TypeConverter(typeof(PaddingConverter))]
[Category("图像"), DisplayName("四周填充像素值")]
public Padding Padding
{
get => padding;
set
{
if (value.Left <= 0) value.Left = 10;
if (value.Right <= 0) value.Right = 10;
if (value.Top <= 0) value.Top = 10;
if (value.Bottom <= 0) value.Bottom = 10;
padding = value;
}
}
private Padding padding = new(1);
/// <summary>
/// DPI
/// </summary>
[TypeConverter(typeof(SizeFConverter))]
[Category("图像"), DisplayName("DPI")]
public SizeF DPI
{
get => dpi;
set
{
if (value.Width <= 0) value.Width = 144;
if (value.Height <= 0) value.Height = 144;
dpi = value;
}
}
private SizeF dpi = new(144, 144);
/// <summary>
/// 名称后缀
/// </summary>
[Category("其他"), DisplayName("名称后缀")]
public string NameSuffix { get; set; } = "(preview)";
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -232,14 +232,14 @@
//
openFileDialog_Skel.AddExtension = false;
openFileDialog_Skel.AddToRecent = false;
openFileDialog_Skel.Filter = "skel 文件 (*.skel; *.json)|*.skel;*.json|二进制文件 (*.skel)|*.skel|文本文件 (*.json)|*.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

View File

@@ -1,4 +1,5 @@
using SpineViewer.Spine;
using SpineViewer.Utils;
using System;
using System.Collections.Generic;
using System.ComponentModel;
@@ -20,10 +21,10 @@ namespace SpineViewer.Dialogs
public OpenSpineDialog()
{
InitializeComponent();
comboBox_Version.DataSource = VersionHelper.Names.ToList();
comboBox_Version.DataSource = SpineUtils.Names.ToList();
comboBox_Version.DisplayMember = "Value";
comboBox_Version.ValueMember = "Key";
comboBox_Version.SelectedValue = Spine.Version.Auto;
comboBox_Version.SelectedValue = SpineVersion.Auto;
}
private void OpenSpineDialog_Load(object sender, EventArgs e)
@@ -53,11 +54,11 @@ namespace SpineViewer.Dialogs
{
var skelPath = textBox_SkelPath.Text;
var atlasPath = textBox_AtlasPath.Text;
var version = (Spine.Version)comboBox_Version.SelectedValue;
var version = (SpineVersion)comboBox_Version.SelectedValue;
if (!File.Exists(skelPath))
{
MessageBox.Info($"{skelPath}", "skel文件不存在");
MessagePopup.Info($"{skelPath}", "skel文件不存在");
return;
}
else
@@ -65,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.Info($"{atlasPath}", "atlas文件不存在");
MessagePopup.Info($"{atlasPath}", "atlas文件不存在");
return;
}
else
@@ -79,9 +80,9 @@ namespace SpineViewer.Dialogs
atlasPath = Path.GetFullPath(atlasPath);
}
if (version != Spine.Version.Auto && !Spine.Spine.ImplementedVersions.Contains(version))
if (version != SpineVersion.Auto && !Spine.SpineObject.HasImplementation(version))
{
MessageBox.Info($"{version.GetName()} 版本尚未实现(咕咕咕~");
MessagePopup.Info($"{version.GetName()} 版本尚未实现(咕咕咕~");
return;
}
@@ -98,12 +99,12 @@ namespace SpineViewer.Dialogs
/// <summary>
/// 打开骨骼对话框结果
/// </summary>
public class OpenSpineDialogResult(Spine.Version version, string skelPath, string? atlasPath = null)
public class OpenSpineDialogResult(SpineVersion version, string skelPath, string? atlasPath = null)
{
/// <summary>
/// 版本
/// </summary>
public Spine.Version Version => version;
public SpineVersion Version => version;
/// <summary>
/// skel 文件路径

View File

@@ -1,4 +1,6 @@
using System;
using NLog;
using SpineViewer.Utils;
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
@@ -12,6 +14,13 @@ namespace SpineViewer.Dialogs
{
public partial class ProgressDialog : Form
{
private readonly Logger logger = LogManager.GetCurrentClassLogger();
public ProgressDialog()
{
InitializeComponent();
}
/// <summary>
/// BackgroundWorker.DoWork 接口暴露
/// </summary>
@@ -32,11 +41,6 @@ namespace SpineViewer.Dialogs
/// </summary>
public void RunWorkerAsync(object? argument) => backgroundWorker.RunWorkerAsync(argument);
public ProgressDialog()
{
InitializeComponent();
}
private void backgroundWorker_ProgressChanged(object sender, ProgressChangedEventArgs e)
{
label_Tip.Text = e.UserState as string;
@@ -47,8 +51,8 @@ namespace SpineViewer.Dialogs
{
if (e.Error != null)
{
Program.Logger.Error(e.Error.ToString());
MessageBox.Error(e.Error.ToString(), "执行出错");
logger.Error(e.Error.ToString());
MessagePopup.Error(e.Error.ToString(), "执行出错");
DialogResult = DialogResult.Abort;
}
else if (e.Cancelled)

View 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);
}
}
}

View 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);
}
}
}

50
SpineViewer/Forms/PetForm.Designer.cs generated Normal file
View File

@@ -0,0 +1,50 @@
namespace SpineViewer
{
partial class PetForm
{
/// <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();
//
// PetForm
//
AutoScaleMode = AutoScaleMode.None;
ClientSize = new Size(490, 456);
ControlBox = false;
MaximizeBox = false;
MinimizeBox = false;
Name = "PetForm";
ShowIcon = false;
ShowInTaskbar = false;
StartPosition = FormStartPosition.Manual;
Text = "PetForm";
ResumeLayout(false);
}
#endregion
}
}

View File

@@ -0,0 +1,49 @@
using SpineViewer.Natives;
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
{
public partial class PetForm: Form
{
public PetForm()
{
InitializeComponent();
}
protected override CreateParams CreateParams
{
get
{
//var style = Win32.GetWindowLong(hWnd, Win32.GWL_STYLE) | Win32.WS_POPUP;
//var exStyle = Win32.GetWindowLong(hWnd, Win32.GWL_EXSTYLE) | Win32.WS_EX_LAYERED | Win32.WS_EX_TOOLWINDOW | Win32.WS_EX_TOPMOST;
//Win32.SetWindowLong(hWnd, Win32.GWL_STYLE, style);
//Win32.SetWindowLong(hWnd, Win32.GWL_EXSTYLE, exStyle);
//Win32.SetLayeredWindowAttributes(hWnd, crKey, 255, Win32.LWA_COLORKEY | Win32.LWA_ALPHA);
//Win32.SetWindowPos(hWnd, Win32.HWND_TOPMOST, 0, 0, 0, 0, Win32.SWP_NOMOVE | Win32.SWP_NOSIZE);
var cp = base.CreateParams;
cp.ExStyle = Win32.WS_EX_LAYERED | Win32.WS_EX_TOPMOST;
cp.Style = Win32.WS_POPUP;
//cp.ExStyle |= Win32.WS_EX_LAYERED | Win32.WS_EX_TOOLWINDOW | Win32.WS_EX_TOPMOST;
return cp;
}
}
protected override void OnPaint(PaintEventArgs e)
{
;
}
protected override void OnPaintBackground(PaintEventArgs e)
{
;
}
}
}

View File

@@ -1,6 +1,6 @@
namespace SpineViewer
{
partial class MainForm
partial class SpineViewerForm
{
/// <summary>
/// Required designer variable.
@@ -29,18 +29,28 @@
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_ExportPreview = 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();
@@ -55,13 +65,13 @@
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();
@@ -82,8 +92,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();
@@ -92,16 +102,16 @@
//
menuStrip.BackColor = SystemColors.Control;
menuStrip.ImageScalingSize = new Size(24, 24);
menuStrip.Items.AddRange(new ToolStripItem[] { toolStripMenuItem_File, toolStripMenuItem_Function, toolStripMenuItem_Tool, toolStripMenuItem_Download, toolStripMenuItem_Help });
menuStrip.Items.AddRange(new ToolStripItem[] { toolStripMenuItem_File, toolStripMenuItem_Tool, toolStripMenuItem_Download, toolStripMenuItem_Help });
menuStrip.Location = new Point(0, 0);
menuStrip.Name = "menuStrip";
menuStrip.Size = new Size(1748, 32);
menuStrip.Size = new Size(1778, 32);
menuStrip.TabIndex = 0;
menuStrip.Text = "菜单";
//
// toolStripMenuItem_File
//
toolStripMenuItem_File.DropDownItems.AddRange(new ToolStripItem[] { toolStripMenuItem_Open, toolStripMenuItem_BatchOpen, toolStripSeparator1, toolStripMenuItem_Export, toolStripMenuItem_ExportPreview, toolStripSeparator2, toolStripMenuItem_Exit });
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.Text = "文件(&F)";
@@ -128,18 +138,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_ExportPreview
// toolStripMenuItem_ExportFrame
//
toolStripMenuItem_ExportPreview.Name = "toolStripMenuItem_ExportPreview";
toolStripMenuItem_ExportPreview.Size = new Size(254, 34);
toolStripMenuItem_ExportPreview.Text = "导出预览图(&P)...";
toolStripMenuItem_ExportPreview.Click += toolStripMenuItem_ExportPreview_Click;
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
//
@@ -154,20 +241,6 @@
toolStripMenuItem_Exit.Text = "退出(&X)";
toolStripMenuItem_Exit.Click += toolStripMenuItem_Exit_Click;
//
// toolStripMenuItem_Function
//
toolStripMenuItem_Function.DropDownItems.AddRange(new ToolStripItem[] { toolStripMenuItem_ResetAnimation });
toolStripMenuItem_Function.Name = "toolStripMenuItem_Function";
toolStripMenuItem_Function.Size = new Size(87, 28);
toolStripMenuItem_Function.Text = "功能(&G)";
//
// toolStripMenuItem_ResetAnimation
//
toolStripMenuItem_ResetAnimation.Name = "toolStripMenuItem_ResetAnimation";
toolStripMenuItem_ResetAnimation.Size = new Size(242, 34);
toolStripMenuItem_ResetAnimation.Text = "重置动画时间(&R)";
toolStripMenuItem_ResetAnimation.Click += toolStripMenuItem_ResetAnimation_Click;
//
// toolStripMenuItem_Tool
//
toolStripMenuItem_Tool.DropDownItems.AddRange(new ToolStripItem[] { toolStripMenuItem_ConvertFileFormat });
@@ -232,7 +305,7 @@
rtbLog.Margin = new Padding(3, 2, 3, 2);
rtbLog.Name = "rtbLog";
rtbLog.ReadOnly = true;
rtbLog.Size = new Size(1728, 114);
rtbLog.Size = new Size(1758, 142);
rtbLog.TabIndex = 0;
rtbLog.Text = "";
rtbLog.WordWrap = false;
@@ -241,6 +314,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;
@@ -254,8 +328,9 @@
//
splitContainer_MainForm.Panel2.Controls.Add(rtbLog);
splitContainer_MainForm.Panel2.Cursor = Cursors.Default;
splitContainer_MainForm.Size = new Size(1728, 997);
splitContainer_MainForm.SplitterDistance = 879;
splitContainer_MainForm.Size = new Size(1758, 1097);
splitContainer_MainForm.SplitterDistance = 947;
splitContainer_MainForm.SplitterWidth = 8;
splitContainer_MainForm.TabIndex = 3;
splitContainer_MainForm.TabStop = false;
splitContainer_MainForm.SplitterMoved += splitContainer_SplitterMoved;
@@ -265,6 +340,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";
//
@@ -277,8 +353,9 @@
//
splitContainer_Functional.Panel2.Controls.Add(groupBox_Preview);
splitContainer_Functional.Panel2.Cursor = Cursors.Default;
splitContainer_Functional.Size = new Size(1728, 879);
splitContainer_Functional.SplitterDistance = 747;
splitContainer_Functional.Size = new Size(1758, 947);
splitContainer_Functional.SplitterDistance = 788;
splitContainer_Functional.SplitterWidth = 8;
splitContainer_Functional.TabIndex = 2;
splitContainer_Functional.TabStop = false;
splitContainer_Functional.SplitterMoved += splitContainer_SplitterMoved;
@@ -300,8 +377,9 @@
//
splitContainer_Information.Panel2.Controls.Add(splitContainer_Config);
splitContainer_Information.Panel2.Cursor = Cursors.Default;
splitContainer_Information.Size = new Size(747, 879);
splitContainer_Information.SplitterDistance = 399;
splitContainer_Information.Size = new Size(788, 947);
splitContainer_Information.SplitterDistance = 351;
splitContainer_Information.SplitterWidth = 8;
splitContainer_Information.TabIndex = 1;
splitContainer_Information.TabStop = false;
splitContainer_Information.SplitterMoved += splitContainer_SplitterMoved;
@@ -313,7 +391,7 @@
groupBox_SkelList.Dock = DockStyle.Fill;
groupBox_SkelList.Location = new Point(0, 0);
groupBox_SkelList.Name = "groupBox_SkelList";
groupBox_SkelList.Size = new Size(399, 879);
groupBox_SkelList.Size = new Size(351, 947);
groupBox_SkelList.TabIndex = 0;
groupBox_SkelList.TabStop = false;
groupBox_SkelList.Text = "模型列表";
@@ -323,24 +401,20 @@
spineListView.Dock = DockStyle.Fill;
spineListView.Location = new Point(3, 26);
spineListView.Name = "spineListView";
spineListView.PropertyGrid = propertyGrid_Spine;
spineListView.Size = new Size(393, 850);
spineListView.Size = new Size(345, 918);
spineListView.SpinePropertyGrid = spineViewPropertyGrid;
spineListView.TabIndex = 0;
//
// propertyGrid_Spine
// spinePropertyGrid
//
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(338, 485);
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 = "spinePropertyGrid";
spineViewPropertyGrid.Size = new Size(423, 586);
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";
@@ -348,38 +422,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(344, 879);
splitContainer_Config.SplitterDistance = 514;
splitContainer_Config.Panel2.Controls.Add(groupBox_SkelConfig);
splitContainer_Config.Size = new Size(429, 947);
splitContainer_Config.SplitterDistance = 324;
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(344, 514);
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(344, 361);
groupBox_PreviewConfig.Size = new Size(429, 324);
groupBox_PreviewConfig.TabIndex = 1;
groupBox_PreviewConfig.TabStop = false;
groupBox_PreviewConfig.Text = "画面参数";
@@ -390,33 +450,43 @@
propertyGrid_Previewer.HelpVisible = false;
propertyGrid_Previewer.Location = new Point(3, 26);
propertyGrid_Previewer.Name = "propertyGrid_Previewer";
propertyGrid_Previewer.Size = new Size(338, 332);
propertyGrid_Previewer.Size = new Size(423, 295);
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, 615);
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(977, 879);
groupBox_Preview.Size = new Size(962, 947);
groupBox_Preview.TabIndex = 1;
groupBox_Preview.TabStop = false;
groupBox_Preview.Text = "预览画面";
//
// spinePreviewer
//
spinePreviewer.BackColor = SystemColors.ControlDark;
spinePreviewer.Dock = DockStyle.Fill;
spinePreviewer.Location = new Point(3, 26);
spinePreviewer.Name = "spinePreviewer";
spinePreviewer.PropertyGrid = propertyGrid_Previewer;
spinePreviewer.Size = new Size(971, 850);
spinePreviewer.SpineListView = spineListView;
spinePreviewer.TabIndex = 0;
spinePreviewer.MouseUp += spinePreviewer_MouseUp;
spinePreviewPanel.Dock = DockStyle.Fill;
spinePreviewPanel.Location = new Point(3, 26);
spinePreviewPanel.Name = "spinePreviewer";
spinePreviewPanel.PropertyGrid = propertyGrid_Previewer;
spinePreviewPanel.Size = new Size(956, 918);
spinePreviewPanel.SpineListView = spineListView;
spinePreviewPanel.TabIndex = 0;
//
// panel_MainForm
//
@@ -425,24 +495,24 @@
panel_MainForm.Location = new Point(0, 32);
panel_MainForm.Name = "panel_MainForm";
panel_MainForm.Padding = new Padding(10, 5, 10, 10);
panel_MainForm.Size = new Size(1748, 1012);
panel_MainForm.Size = new Size(1778, 1112);
panel_MainForm.TabIndex = 4;
//
// toolTip
//
toolTip.ShowAlways = true;
//
// MainForm
// SpineViewerForm
//
AutoScaleDimensions = new SizeF(11F, 24F);
AutoScaleMode = AutoScaleMode.Font;
ClientSize = new Size(1748, 1044);
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;
@@ -466,8 +536,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);
@@ -481,7 +551,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;
@@ -489,7 +558,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;
@@ -497,18 +565,30 @@
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_ExportPreview;
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;
}
}

View File

@@ -0,0 +1,488 @@
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通道属性失效");
}
}
/// <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 propertyGrid_PropertyValueChanged(object sender, PropertyValueChangedEventArgs e)
{
// 用来解决对面板某些值修改之后, 其他被联动修改的值不会实时刷新的问题
(sender as PropertyGrid)?.Refresh();
}
private void Export_Work(object? sender, DoWorkEventArgs e)
{
var worker = (BackgroundWorker)sender;
var exporter = (Exporter)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 System.Windows.Forms.Timer timer = new();
//private PetForm pet = new PetForm();
//private IntPtr screenDC;
//private IntPtr memDC;
//private void _Test()
//{
// screenDC = Win32.GetDC(IntPtr.Zero);
// memDC = Win32.CreateCompatibleDC(screenDC);
// pet.Show();
// timer.Tick += Timer_Tick;
// timer.Enabled = true;
// timer.Interval = 50;
// timer.Start();
//}
//private void Timer_Tick(object? sender, EventArgs e)
//{
// using var tex = new SFML.Graphics.RenderTexture((uint)pet.Width, (uint)pet.Height);
// var v = spinePreviewer.GetView();
// tex.SetView(v);
// tex.Clear(new SFML.Graphics.Color(0, 0, 0, 0));
// lock (spineListView.Spines)
// {
// foreach (var sp in spineListView.Spines)
// tex.Draw(sp);
// }
// tex.Display();
// using var frame = new SFMLImageVideoFrame(tex.Texture.CopyToImage());
// using var bitmap = frame.CopyToBitmap();
// var newBitmap = bitmap.GetHbitmap(Color.FromArgb(0));
// var oldBitmap = Win32.SelectObject(memDC, newBitmap);
// Win32.SIZE size = new Win32.SIZE { cx = pet.Width, cy = pet.Height };
// Win32.POINT srcPos = new Win32.POINT { x = 0, y = 0 };
// Win32.BLENDFUNCTION blend = new Win32.BLENDFUNCTION { BlendOp = 0, BlendFlags = 0, SourceConstantAlpha = 255, AlphaFormat = Win32.AC_SRC_ALPHA };
// Win32.UpdateLayeredWindow(pet.Handle, screenDC, IntPtr.Zero, ref size, memDC, ref srcPos, 0, ref blend, Win32.ULW_ALPHA);
// Win32.SelectObject(memDC, oldBitmap);
// Win32.DeleteObject(newBitmap);
//}
//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;
// }
//}
}
}

View File

@@ -1,436 +0,0 @@
using FFMpegCore.Pipes;
using FFMpegCore;
using NLog;
using SFML.System;
using SpineViewer.Spine;
using System.ComponentModel;
using System.Diagnostics;
using System.Text.Json;
using System.Text.Json.Nodes;
using FFMpegCore.Enums;
namespace SpineViewer
{
public partial class MainForm : Form
{
public MainForm()
{
InitializeComponent();
InitializeLogConfiguration();
}
/// <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)
{
spinePreviewer.StartPreview();
}
private void MainForm_FormClosing(object sender, FormClosingEventArgs e)
{
spinePreviewer.StopPreview();
}
private void toolStripMenuItem_Open_Click(object sender, EventArgs e)
{
spineListView.Add();
}
private void toolStripMenuItem_BatchOpen_Click(object sender, EventArgs e)
{
spineListView.BatchAdd();
}
private void toolStripMenuItem_Export_Click(object sender, EventArgs e)
{
// TODO: 改成统一导出调用
lock (spineListView.Spines)
{
if (spineListView.Spines.Count <= 0)
{
MessageBox.Info("请至少打开一个骨骼文件");
return;
}
}
var exportDialog = new Dialogs.ExportPngDialog();
if (exportDialog.ShowDialog() != DialogResult.OK)
return;
var progressDialog = new Dialogs.ProgressDialog();
progressDialog.DoWork += ExportPng_Work;
progressDialog.RunWorkerAsync(exportDialog);
progressDialog.ShowDialog();
}
private void toolStripMenuItem_ExportPreview_Click(object sender, EventArgs e)
{
lock (spineListView.Spines)
{
if (spineListView.Spines.Count <= 0)
{
MessageBox.Info("请至少打开一个骨骼文件");
return;
}
}
var saveDialog = new Dialogs.ExportPreviewDialog();
if (saveDialog.ShowDialog() != DialogResult.OK)
return;
var progressDialog = new Dialogs.ProgressDialog();
progressDialog.DoWork += ExportPreview_Work;
progressDialog.RunWorkerAsync(saveDialog.Result);
progressDialog.ShowDialog();
}
private void toolStripMenuItem_Exit_Click(object sender, EventArgs e)
{
Close();
}
private void toolStripMenuItem_ResetAnimation_Click(object sender, EventArgs e)
{
lock (spineListView.Spines)
{
foreach (var spine in spineListView.Spines)
spine.CurrentAnimation = spine.CurrentAnimation;
}
}
private void toolStripMenuItem_ConvertFileFormat_Click(object sender, EventArgs e)
{
var openDialog = new Dialogs.ConvertFileFormatDialog();
if (openDialog.ShowDialog() != DialogResult.OK)
return;
var progressDialog = new Dialogs.ProgressDialog();
progressDialog.DoWork += ConvertFileFormat_Work;
progressDialog.RunWorkerAsync(openDialog.Result);
progressDialog.ShowDialog();
}
//IEnumerable<IVideoFrame> testExport(int fps)
//{
// var duration = 2f;
// var resolution = spinePreviewer.Resolution;
// var delta = 1f / fps;
// var frameCount = 1 + (int)(duration / delta); // 零帧开始导出
// var spinesReverse = spineListView.Spines.Reverse();
// // 重置动画时间
// foreach (var spine in spinesReverse)
// spine.CurrentAnimation = spine.CurrentAnimation;
// // 逐帧导出
// var success = 0;
// for (int frameIndex = 0; frameIndex < frameCount; frameIndex++)
// {
// using var tex = new SFML.Graphics.RenderTexture((uint)resolution.Width, (uint)resolution.Height);
// tex.SetView(spinePreviewer.View);
// tex.Clear(SFML.Graphics.Color.Transparent);
// foreach (var spine in spinesReverse)
// {
// tex.Draw(spine);
// spine.Update(delta);
// }
// tex.Display();
// Debug.WriteLine($"ThreadID: {Environment.CurrentManagedThreadId}");
// var frame = tex.Texture.CopyToFrame();
// tex.Dispose();
// yield return frame;
// success++;
// }
// Program.Logger.Info("Exporting done: {}/{}", success, frameCount);
//}
private void toolStripMenuItem_ManageResource_Click(object sender, EventArgs e)
{
//spinePreviewer.StopPreview();
//lock (spineListView.Spines)
//{
// //var fps = 24;
// ////foreach (var i in testExport(fps))
// //// _ = i;
// ////var t = testExport(fps).ToArray();
// ////var a = testExport(fps).GetEnumerator();
// ////while (a.MoveNext());
// //var videoFramesSource = new RawVideoPipeSource(testExport(fps)) { FrameRate = fps };
// //var outputPath = @"C:\Users\ljh\Desktop\test\a.mov";
// //var task = FFMpegArguments
// // .FromPipeInput(videoFramesSource)
// // .OutputToFile(outputPath, true
// // , options => options
// // //.WithCustomArgument("-vf \"split[s0][s1];[s0]palettegen=reserve_transparent=1[p];[s1][p]paletteuse=alpha_threshold=128\""))
// // .WithCustomArgument("-c:v prores_ks -profile:v 4444 -pix_fmt yuva444p10le"))
// // .ProcessAsynchronously();
// //task.Wait();
//}
//spinePreviewer.StartPreview();
}
private void toolStripMenuItem_About_Click(object sender, EventArgs e)
{
(new Dialogs.AboutDialog()).ShowDialog();
}
private void toolStripMenuItem_Diagnostics_Click(object sender, EventArgs e)
{
(new Dialogs.DiagnosticsDialog()).ShowDialog();
}
private void splitContainer_SplitterMoved(object sender, SplitterEventArgs e) => ActiveControl = null;
private void splitContainer_MouseUp(object sender, MouseEventArgs e) => ActiveControl = null;
private void propertyGrid_PropertyValueChanged(object sender, PropertyValueChangedEventArgs e) => (sender as PropertyGrid)?.Refresh();
private void spinePreviewer_MouseUp(object sender, MouseEventArgs e)
{
propertyGrid_Spine.Refresh();
}
private void ExportPng_Work(object? sender, DoWorkEventArgs e)
{
var worker = sender as BackgroundWorker;
var arguments = e.Argument as Dialogs.ExportPngDialog;
var outputDir = arguments.OutputDir;
var duration = arguments.Duration;
var fps = arguments.Fps;
var timestamp = DateTime.Now.ToString("yyMMddHHmmss");
var frameArgs = spinePreviewer.GetFrameArgs();
var renderSelectedOnly = spinePreviewer.RenderSelectedOnly;
var resolution = frameArgs.Resolution;
var tex = new SFML.Graphics.RenderTexture((uint)resolution.Width, (uint)resolution.Height);
tex.SetView(frameArgs.View);
var delta = 1f / fps;
var frameCount = 1 + (int)(duration / delta); // 零帧开始导出
spinePreviewer.StopPreview();
lock (spineListView.Spines)
{
var spinesReverse = spineListView.Spines.Reverse();
// 重置动画时间
foreach (var spine in spinesReverse)
spine.CurrentAnimation = spine.CurrentAnimation;
Program.Logger.Info(
"Begin exporting png frames to output dir {}, duration: {}, fps: {}, totally {} spines",
[outputDir, duration, fps, spinesReverse.Count()]
);
// 逐帧导出
var success = 0;
worker.ReportProgress(0, $"已处理 0/{frameCount}");
for (int frameIndex = 0; frameIndex < frameCount; frameIndex++)
{
if (worker.CancellationPending)
break;
tex.Clear(SFML.Graphics.Color.Transparent);
foreach (var spine in spinesReverse)
{
if (renderSelectedOnly && !spine.IsSelected)
continue;
tex.Draw(spine);
spine.Update(delta);
}
tex.Display();
using (var img = tex.Texture.CopyToImage())
{
img.SaveToFile(Path.Combine(outputDir, $"{timestamp}_{fps}_{frameIndex:d6}.png"));
}
success++;
worker.ReportProgress((int)((frameIndex + 1) * 100.0) / frameCount, $"已处理 {frameIndex + 1}/{frameCount}");
}
Program.Logger.Info("Exporting done: {}/{}", success, frameCount);
}
spinePreviewer.StartPreview();
}
private void ExportPreview_Work(object? sender, DoWorkEventArgs e)
{
var worker = sender as BackgroundWorker;
var arguments = e.Argument as Dialogs.ExportPreviewDialogResult;
var outputDir = arguments.OutputDir;
var imageFormat = arguments.ImageFormat;
var resolution = arguments.Resolution;
var padding = arguments.Padding;
var dpi = arguments.DPI;
var nameSuffix = arguments.NameSuffix;
var renderSelectedOnly = spinePreviewer.RenderSelectedOnly;
var tex = new SFML.Graphics.RenderTexture((uint)resolution.Width, (uint)resolution.Height);
int success = 0;
int error = 0;
spinePreviewer.StopPreview();
lock (spineListView.Spines)
{
var spines = spineListView.Spines;
int totalCount = spines.Count;
worker.ReportProgress(0, $"已处理 0/{totalCount}");
for (int i = 0; i < totalCount; i++)
{
if (worker.CancellationPending)
{
e.Cancel = true;
break;
}
var spine = spines[i];
if (renderSelectedOnly && !spine.IsSelected)
continue;
var filename = $"{spine.Name}{nameSuffix}{imageFormat.GetSuffix()}";
var savePath = outputDir is null ? Path.Combine(spine.AssetsDir, filename) : Path.Combine(outputDir, filename);
var tmp = spine.CurrentAnimation;
spine.CurrentAnimation = Spine.Spine.EMPTY_ANIMATION;
tex.SetView(spine.GetInitView(resolution, padding));
tex.Clear(SFML.Graphics.Color.Transparent);
tex.Draw(spine);
tex.Display();
spine.CurrentAnimation = tmp;
try
{
using (var img = new Bitmap(tex.Texture.CopyToBitmap()))
{
img.SetResolution(dpi.Width, dpi.Height);
img.Save(savePath, imageFormat);
}
success++;
}
catch (Exception ex)
{
Program.Logger.Error(ex.ToString());
Program.Logger.Error("Failed to save preview {}", spine.SkelPath);
error++;
}
worker.ReportProgress((int)((i + 1) * 100.0) / totalCount, $"已处理 {i + 1}/{totalCount}");
}
}
spinePreviewer.StartPreview();
if (error > 0)
{
Program.Logger.Warn("Preview save {} successfully, {} failed", success, error);
}
else
{
Program.Logger.Info("{} preview saved successfully", success);
}
Program.LogCurrentMemoryUsage();
}
private void ConvertFileFormat_Work(object? sender, DoWorkEventArgs e)
{
var worker = sender as BackgroundWorker;
var arguments = e.Argument as Dialogs.ConvertFileFormatDialogResult;
var skelPaths = arguments.SkelPaths;
var srcVersion = arguments.SourceVersion;
var tgtVersion = arguments.TargetVersion;
var jsonTarget = arguments.JsonTarget;
var newSuffix = jsonTarget ? ".json" : ".skel";
int totalCount = skelPaths.Length;
int success = 0;
int error = 0;
SkeletonConverter srcCvter = srcVersion != Spine.Version.Auto ? SkeletonConverter.New(srcVersion) : null;
SkeletonConverter tgtCvter = SkeletonConverter.New(tgtVersion);
worker.ReportProgress(0, $"已处理 0/{totalCount}");
for (int i = 0; i < totalCount; i++)
{
if (worker.CancellationPending)
{
e.Cancel = true;
break;
}
var skelPath = skelPaths[i];
var newPath = Path.ChangeExtension(skelPath, newSuffix);
try
{
if (srcVersion == Spine.Version.Auto)
{
if (Spine.Spine.GetVersion(skelPath) is Spine.Version detectedSrcVersion)
srcCvter = SkeletonConverter.New(detectedSrcVersion);
else
throw new InvalidDataException($"Auto version detection failed for {skelPath}, try to use a specific version");
}
var root = srcCvter.Read(skelPath);
root = srcCvter.ToVersion(root, tgtVersion);
if (jsonTarget) tgtCvter.WriteJson(root, newPath); else tgtCvter.WriteBinary(root, newPath);
success++;
}
catch (Exception ex)
{
Program.Logger.Error(ex.ToString());
Program.Logger.Error("Failed to convert {}", skelPath);
error++;
}
worker.ReportProgress((int)((i + 1) * 100.0) / totalCount, $"已处理 {i + 1}/{totalCount}");
}
if (error > 0)
{
Program.Logger.Warn("Batch convert {} successfully, {} failed", success, error);
}
else
{
Program.Logger.Info("{} skel converted successfully", success);
}
}
}
}

View 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);
}
}
}

View File

@@ -0,0 +1,177 @@
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_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_LAYERED = 0x80000;
public const int WS_EX_OVERLAPPEDWINDOW = WS_EX_WINDOWEDGE | WS_EX_CLIENTEDGE;
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 uint SWP_REFRESHLONG = SWP_NOSIZE | SWP_NOMOVE | SWP_NOZORDER | SWP_FRAMECHANGED;
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:x8}");
return hWnd;
}
}
}

View File

@@ -1,39 +1,38 @@
using NLog;
using SpineViewer.Utils;
using System.Diagnostics;
using System.Reflection;
namespace SpineViewer
{
internal static class Program
{
/// <summary>
/// 程序路径
/// </summary>
public static readonly string FilePath = Environment.ProcessPath;
///// <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 Name = Path.GetFileNameWithoutExtension(FilePath);
/// <summary>
/// 程序目录
/// </summary>
public static readonly string RootDir = Path.GetDirectoryName(FilePath);
///// <summary>
///// 程序目录
///// </summary>
//public static readonly string RootDir = Path.GetDirectoryName(FilePath);
/// <summary>
/// 程序临时目录
/// </summary>
public static readonly string TempDir = Directory.CreateDirectory(Path.Combine(Path.GetTempPath(), Name)).FullName;
///// <summary>
///// 程序临时目录
///// </summary>
//public static readonly string TempDir = Directory.CreateDirectory(Path.Combine(Path.GetTempPath(), Name)).FullName;
/// <summary>
/// 程序进程
/// </summary>
public static readonly Process Process = Process.GetCurrentProcess();
public static string Version => Assembly.GetExecutingAssembly().GetCustomAttribute<AssemblyInformationalVersionAttribute>()?.InformationalVersion;
/// <summary>
/// 程序日志器
/// </summary>
public static readonly Logger Logger = LogManager.GetCurrentClassLogger();
private static readonly Logger logger = LogManager.GetCurrentClassLogger();
/// <summary>
/// 应用入口点
@@ -41,8 +40,9 @@ namespace SpineViewer
[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.
@@ -50,12 +50,12 @@ namespace SpineViewer
try
{
Application.Run(new MainForm());
Application.Run(new SpineViewerForm() { Text = $"SpineViewer - v{Version}"});
}
catch (Exception ex)
{
Logger.Fatal(ex.ToString());
MessageBox.Error(ex.ToString(), "程序已崩溃");
logger.Fatal(ex.ToString());
MessagePopup.Error(ex.ToString(), "程序已崩溃");
}
}
@@ -82,10 +82,5 @@ namespace SpineViewer
config.AddRule(LogLevel.Debug, LogLevel.Fatal, fileTarget);
LogManager.Configuration = config;
}
/// <summary>
/// 输出当前内存使用情况
/// </summary>
public static void LogCurrentMemoryUsage() => Logger.Info("Current memory usage: {:F2} MB", Process.WorkingSet64 / 1024.0 / 1024.0);
}
}

View File

@@ -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 BlendModeSFML
{
/// <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
);
}
}

View File

@@ -8,14 +8,49 @@ using System.Text.Json;
using System.Text.Json.Nodes;
using SpineRuntime38.Attachments;
using System.Globalization;
using static System.Runtime.InteropServices.JavaScript.JSType;
using System.IO;
namespace SpineViewer.Spine.Implementations.SkeletonConverter
{
[SkeletonConverterImplementation(Version.V38)]
class SkeletonConverter38 : SpineViewer.Spine.SkeletonConverter
[SpineImplementation(SpineVersion.V38)]
public class SkeletonConverter38 : Spine.SkeletonConverter
{
private static readonly Dictionary<TransformMode, string> TransformModeJsonValue = new()
{
[TransformMode.Normal] = "normal",
[TransformMode.OnlyTranslation] = "onlyTranslation",
[TransformMode.NoRotationOrReflection] = "noRotationOrReflection",
[TransformMode.NoScale] = "noScale",
[TransformMode.NoScaleOrReflection] = "noScaleOrReflection",
};
private static readonly Dictionary<BlendMode, string> BlendModeJsonValue = new()
{
[BlendMode.Normal] = "normal",
[BlendMode.Additive] = "additive",
[BlendMode.Multiply] = "multiply",
[BlendMode.Screen] = "screen",
};
private static readonly Dictionary<PositionMode, string> PositionModeJsonValue = new()
{
[PositionMode.Fixed] = "fixed",
[PositionMode.Percent] = "percent",
};
private static readonly Dictionary<SpacingMode, string> SpacingModeJsonValue = new()
{
[SpacingMode.Length] = "length",
[SpacingMode.Fixed] = "fixed",
[SpacingMode.Percent] = "percent",
};
private static readonly Dictionary<RotateMode, string> RotateModeJsonValue = new()
{
[RotateMode.Tangent] = "tangent",
[RotateMode.Chain] = "chain",
[RotateMode.ChainScale] = "chainScale",
};
private BinaryReader reader = null;
private JsonObject root = null;
private bool nonessential = false;
@@ -46,6 +81,9 @@ namespace SpineViewer.Spine.Implementations.SkeletonConverter
idx2event.Clear();
// 清理临时属性
foreach (var (_, data) in root["events"].AsObject()) data.AsObject().Remove("__name__");
return root;
}
@@ -92,7 +130,7 @@ namespace SpineViewer.Spine.Implementations.SkeletonConverter
data["shearX"] = reader.ReadFloat();
data["shearY"] = reader.ReadFloat();
data["length"] = reader.ReadFloat();
data["transform"] = SkeletonBinary.TransformModeValues[reader.ReadVarInt()].ToString();
data["transform"] = TransformModeJsonValue[SkeletonBinary.TransformModeValues[reader.ReadVarInt()]];
data["skin"] = reader.ReadBoolean();
if (nonessential) reader.ReadInt();
bones.Add(data);
@@ -113,7 +151,7 @@ namespace SpineViewer.Spine.Implementations.SkeletonConverter
int dark = reader.ReadInt();
if (dark != -1) data["dark"] = dark.ToString("x6"); // 0x00rrggbb -> rrggbb
data["attachment"] = reader.ReadStringRef();
data["blend"] = ((BlendMode)reader.ReadVarInt()).ToString();
data["blend"] = BlendModeJsonValue[((BlendMode)reader.ReadVarInt())];
slots.Add(data);
}
root["slots"] = slots;
@@ -183,9 +221,9 @@ namespace SpineViewer.Spine.Implementations.SkeletonConverter
data["skin"] = reader.ReadBoolean();
data["bones"] = ReadNames(bones);
data["target"] = (string)bones[reader.ReadVarInt()]["name"];
data["positionMode"] = ((PositionMode)reader.ReadVarInt()).ToString();
data["spacingMode"] = ((SpacingMode)reader.ReadVarInt()).ToString();
data["rotateMode"] = ((RotateMode)reader.ReadVarInt()).ToString();
data["positionMode"] = PositionModeJsonValue[((PositionMode)reader.ReadVarInt())];
data["spacingMode"] = SpacingModeJsonValue[((SpacingMode)reader.ReadVarInt())];
data["rotateMode"] = RotateModeJsonValue[((RotateMode)reader.ReadVarInt())];
data["rotation"] = reader.ReadFloat();
data["position"] = reader.ReadFloat();
data["spacing"] = reader.ReadFloat();
@@ -285,7 +323,7 @@ namespace SpineViewer.Spine.Implementations.SkeletonConverter
if (path is not null) attachment["path"] = path;
attachment["color"] = reader.ReadInt().ToString("x8");
vertexCount = reader.ReadVarInt();
attachment["uvs"] = ReadFloatArray(vertexCount << 1); // vertexCount = uvs.Length
attachment["uvs"] = ReadFloatArray(vertexCount << 1); // vertexCount = uvs.Length >> 1
attachment["triangles"] = ReadShortArray();
attachment["vertices"] = ReadVertices(vertexCount);
attachment["hull"] = reader.ReadVarInt();
@@ -350,7 +388,7 @@ namespace SpineViewer.Spine.Implementations.SkeletonConverter
JsonObject data = [];
var name = reader.ReadStringRef();
events[name] = data;
data["name"] = name; // 额外增加的, 方便后面查找
data["__name__"] = name; // 数据里是不应该有这个字段的, 但是为了 ReadEventTimelines 里能够提供 name 字段, 临时增加了一下
data["int"] = reader.ReadVarInt(false);
data["float"] = reader.ReadFloat();
data["string"] = reader.ReadString();
@@ -378,7 +416,7 @@ namespace SpineViewer.Spine.Implementations.SkeletonConverter
if (ReadTransformTimelines() is JsonObject transform) data["transform"] = transform;
if (ReadPathTimelines() is JsonObject path) data["path"] = path;
if (ReadDeformTimelines() is JsonObject deform) data["deform"] = deform;
if (ReadDrawOrderTimelines() is JsonArray draworder) data["drawOrder"] = draworder;
if (ReadDrawOrderTimelines() is JsonArray draworder) data["draworder"] = draworder;
if (ReadEventTimelines() is JsonArray events) data["events"] = events;
}
root["animations"] = animations;
@@ -594,7 +632,7 @@ namespace SpineViewer.Spine.Implementations.SkeletonConverter
for (int timelineCount = reader.ReadVarInt(); timelineCount > 0; timelineCount--)
{
JsonArray frames = [];
var type = reader.ReadByte();
var type = reader.ReadSByte();
var frameCount = reader.ReadVarInt();
switch (type)
{
@@ -602,11 +640,13 @@ namespace SpineViewer.Spine.Implementations.SkeletonConverter
timeline["position"] = frames;
while (frameCount-- > 0)
{
frames.Add(new JsonObject()
var o = new JsonObject()
{
["time"] = reader.ReadFloat(),
["position"] = reader.ReadFloat(),
});
};
if (frameCount > 0) ReadCurve(o);
frames.Add(o);
}
break;
case SkeletonBinary.PATH_SPACING:
@@ -673,8 +713,7 @@ namespace SpineViewer.Spine.Implementations.SkeletonConverter
var end = reader.ReadVarInt();
if (end > 0)
{
var start = reader.ReadVarInt();
o["offset"] = start;
o["offset"] = reader.ReadVarInt();
o["vertices"] = ReadFloatArray(end);
}
if (frameCount > 0) ReadCurve(o);
@@ -721,14 +760,14 @@ namespace SpineViewer.Spine.Implementations.SkeletonConverter
JsonObject data = [];
data["time"] = reader.ReadFloat();
JsonObject eventData = idx2event[reader.ReadVarInt()].AsObject();
data["name"] = (string)eventData["name"];
data["name"] = (string)eventData["__name__"];
data["int"] = reader.ReadVarInt();
data["float"] = reader.ReadFloat();
data["string"] = reader.ReadBoolean() ? reader.ReadString() : (string)eventData["string"];
if (reader.ReadBoolean()) data["string"] = reader.ReadString();
if (eventData.ContainsKey("audio"))
{
data["volume"] = (string)eventData["volume"];
data["balance"] = (string)eventData["balance"];
data["volume"] = reader.ReadFloat();
data["balance"] = reader.ReadFloat();
}
eventTimelines.Add(data);
}
@@ -787,10 +826,6 @@ namespace SpineViewer.Spine.Implementations.SkeletonConverter
switch (type)
{
case SkeletonBinary.CURVE_LINEAR:
obj["curve"] = 1 / 3f;
obj["c2"] = 1 / 3f;
obj["c3"] = 2 / 3f;
obj["c4"] = 2 / 3f;
break;
case SkeletonBinary.CURVE_STEPPED:
obj["curve"] = "stepped";
@@ -812,6 +847,7 @@ namespace SpineViewer.Spine.Implementations.SkeletonConverter
private readonly Dictionary<string, int> ik2idx = [];
private readonly Dictionary<string, int> transform2idx = [];
private readonly Dictionary<string, int> path2idx = [];
private readonly Dictionary<string, int> skin2idx = [];
private readonly Dictionary<string, int> event2idx = [];
public override void WriteBinary(JsonObject root, string binPath, bool nonessential = false)
@@ -820,7 +856,7 @@ namespace SpineViewer.Spine.Implementations.SkeletonConverter
this.root = root;
using var outputBody = new MemoryStream(); // 先把主体写入内存缓冲区
writer = new(outputBody);
BinaryWriter tmpWriter = writer = new (outputBody);
WriteBones();
WriteSlots();
@@ -830,16 +866,28 @@ namespace SpineViewer.Spine.Implementations.SkeletonConverter
WriteSkins();
WriteEvents();
WriteAnimations();
//using var output = File.Create(binPath); // 将数据写入文件
//writer = new(output);
using var output = File.Create(binPath); // 将数据写入文件
writer = new(output);
// 把字符串表保留过来
writer.StringTable.AddRange(tmpWriter.StringTable);
WriteSkeleton();
WriteStrings();
//output.Write(outputBody.GetBuffer());
outputBody.Seek(0, SeekOrigin.Begin);
outputBody.CopyTo(output);
writer = null;
this.root = null;
bone2idx.Clear();
slot2idx.Clear();
ik2idx.Clear();
transform2idx.Clear();
path2idx.Clear();
skin2idx.Clear();
event2idx.Clear();
}
private void WriteSkeleton()
@@ -892,7 +940,7 @@ namespace SpineViewer.Spine.Implementations.SkeletonConverter
if (data.TryGetPropertyValue("shearX", out var shearX)) writer.WriteFloat((float)shearX); else writer.WriteFloat(0);
if (data.TryGetPropertyValue("shearY", out var shearY)) writer.WriteFloat((float)shearY); else writer.WriteFloat(0);
if (data.TryGetPropertyValue("length", out var length)) writer.WriteFloat((float)length); else writer.WriteFloat(0);
if (data.TryGetPropertyValue("transform", out var transform)) writer.WriteVarInt((int)Enum.Parse<TransformMode>((string)transform, true)); else writer.WriteVarInt((int)TransformMode.Normal);
if (data.TryGetPropertyValue("transform", out var transform)) writer.WriteVarInt(Array.IndexOf(SkeletonBinary.TransformModeValues, Enum.Parse<TransformMode>((string)transform, true))); else writer.WriteVarInt(0);
if (data.TryGetPropertyValue("skin", out var skin)) writer.WriteBoolean((bool)skin); else writer.WriteBoolean(false);
if (nonessential) writer.WriteInt(0);
bone2idx[name] = i;
@@ -914,7 +962,7 @@ namespace SpineViewer.Spine.Implementations.SkeletonConverter
var name = (string)data["name"];
writer.WriteString(name);
writer.WriteVarInt(bone2idx[(string)data["bone"]]);
if (data.TryGetPropertyValue("color", out var color)) writer.WriteInt(int.Parse((string)color, NumberStyles.HexNumber)); else writer.WriteInt(0);
if (data.TryGetPropertyValue("color", out var color)) writer.WriteInt(int.Parse((string)color, NumberStyles.HexNumber)); else writer.WriteInt(-1); // 默认值是全 255
if (data.TryGetPropertyValue("dark", out var dark)) writer.WriteInt(int.Parse((string)dark, NumberStyles.HexNumber)); else writer.WriteInt(-1);
if (data.TryGetPropertyValue("attachment", out var attachment)) writer.WriteStringRef((string)attachment); else writer.WriteStringRef(null);
if (data.TryGetPropertyValue("blend", out var blend)) writer.WriteVarInt((int)Enum.Parse<BlendMode>((string)blend, true)); else writer.WriteVarInt((int)BlendMode.Normal);
@@ -1020,6 +1068,7 @@ namespace SpineViewer.Spine.Implementations.SkeletonConverter
{
writer.WriteVarInt(0); // default 的 slotCount
writer.WriteVarInt(0); // 其他皮肤数量
skin2idx["default"] = skin2idx.Count;
return;
}
@@ -1031,6 +1080,7 @@ namespace SpineViewer.Spine.Implementations.SkeletonConverter
{
hasDefault = true;
WriteSkin(skin, true);
skin2idx["default"] = skin2idx.Count;
break;
}
}
@@ -1047,8 +1097,12 @@ namespace SpineViewer.Spine.Implementations.SkeletonConverter
writer.WriteVarInt(skinCount);
foreach (JsonObject skin in skins)
{
if ((string)skin["name"] != "default")
var name = (string)skin["name"];
if (name != "default")
{
WriteSkin(skin);
skin2idx[name] = skin2idx.Count;
}
}
}
@@ -1111,7 +1165,7 @@ namespace SpineViewer.Spine.Implementations.SkeletonConverter
if (attachment.TryGetPropertyValue("scaleY", out var scaleY)) writer.WriteFloat((float)scaleY); else writer.WriteFloat(1);
if (attachment.TryGetPropertyValue("width", out var width)) writer.WriteFloat((float)width); else writer.WriteFloat(32);
if (attachment.TryGetPropertyValue("height", out var height)) writer.WriteFloat((float)height); else writer.WriteFloat(32);
if (attachment.TryGetPropertyValue("color", out var color1)) writer.WriteInt(int.Parse((string)color1, NumberStyles.HexNumber)); else writer.WriteInt(0);
if (attachment.TryGetPropertyValue("color", out var color1)) writer.WriteInt(int.Parse((string)color1, NumberStyles.HexNumber)); else writer.WriteInt(-1);
break;
case AttachmentType.Boundingbox:
if (attachment.TryGetPropertyValue("vertexCount", out var _vertexCount1)) vertexCount = (int)_vertexCount1; else vertexCount = 0;
@@ -1121,10 +1175,10 @@ namespace SpineViewer.Spine.Implementations.SkeletonConverter
break;
case AttachmentType.Mesh:
if (attachment.TryGetPropertyValue("path", out var path2)) writer.WriteStringRef((string)path2); else writer.WriteStringRef(null);
if (attachment.TryGetPropertyValue("color", out var color2)) writer.WriteInt(int.Parse((string)color2, NumberStyles.HexNumber)); else writer.WriteInt(0);
if (attachment.TryGetPropertyValue("vertexCount", out var _vertexCount2)) vertexCount = (int)_vertexCount2; else vertexCount = 0;
if (attachment.TryGetPropertyValue("color", out var color2)) writer.WriteInt(int.Parse((string)color2, NumberStyles.HexNumber)); else writer.WriteInt(-1);
vertexCount = attachment["uvs"].AsArray().Count >> 1;
writer.WriteVarInt(vertexCount);
WriteFloatArray(attachment["uvs"].AsArray(), vertexCount << 1); // vertexCount = uvs.Length
WriteFloatArray(attachment["uvs"].AsArray(), vertexCount << 1); // vertexCount = uvs.Length >> 1
WriteShortArray(attachment["triangles"].AsArray());
WriteVertices(attachment["vertices"].AsArray(), vertexCount);
if (attachment.TryGetPropertyValue("hull", out var hull)) writer.WriteVarInt((int)hull); else writer.WriteVarInt(0);
@@ -1137,7 +1191,7 @@ namespace SpineViewer.Spine.Implementations.SkeletonConverter
break;
case AttachmentType.Linkedmesh:
if (attachment.TryGetPropertyValue("path", out var path3)) writer.WriteStringRef((string)path3); else writer.WriteStringRef(null);
if (attachment.TryGetPropertyValue("color", out var color3)) writer.WriteInt(int.Parse((string)color3, NumberStyles.HexNumber)); else writer.WriteInt(0);
if (attachment.TryGetPropertyValue("color", out var color3)) writer.WriteInt(int.Parse((string)color3, NumberStyles.HexNumber)); else writer.WriteInt(-1);
if (attachment.TryGetPropertyValue("skin", out var skin)) writer.WriteStringRef((string)skin); else writer.WriteStringRef(null);
if (attachment.TryGetPropertyValue("parent", out var parent)) writer.WriteStringRef((string)parent); else writer.WriteStringRef(null);
if (attachment.TryGetPropertyValue("deform", out var deform)) writer.WriteBoolean((bool)deform); else writer.WriteBoolean(true);
@@ -1201,6 +1255,10 @@ namespace SpineViewer.Spine.Implementations.SkeletonConverter
if (data.TryGetPropertyValue("balance", out var balance)) writer.WriteFloat((float)balance); else writer.WriteFloat(0);
}
}
else
{
writer.WriteString(null);
}
event2idx[name] = i++;
}
}
@@ -1212,11 +1270,333 @@ namespace SpineViewer.Spine.Implementations.SkeletonConverter
writer.WriteVarInt(0);
return;
}
JsonArray animations = root["animations"].AsArray();
JsonObject animations = root["animations"].AsObject();
writer.WriteVarInt(animations.Count);
for (int i = 0, n = animations.Count; i < n; i++)
foreach (var (name, _data) in animations)
{
throw new NotImplementedException();
JsonObject data = _data.AsObject();
writer.WriteString(name);
if (data.TryGetPropertyValue("slots", out var slots)) WriteSlotTimelines(slots.AsObject()); else writer.WriteVarInt(0);
if (data.TryGetPropertyValue("bones", out var bones)) WriteBoneTimelines(bones.AsObject()); else writer.WriteVarInt(0);
if (data.TryGetPropertyValue("ik", out var ik)) WriteIKTimelines(ik.AsObject()); else writer.WriteVarInt(0);
if (data.TryGetPropertyValue("transform", out var transform)) WriteTransformTimelines(transform.AsObject()); else writer.WriteVarInt(0);
if (data.TryGetPropertyValue("path", out var path)) WritePathTimelines(path.AsObject()); else writer.WriteVarInt(0);
if (data.TryGetPropertyValue("deform", out var deform)) WriteDeformTimelines(deform.AsObject()); else writer.WriteVarInt(0);
if (data.TryGetPropertyValue("drawOrder", out var drawOrder)) WriteDrawOrderTimelines(drawOrder.AsArray()); else
if (data.TryGetPropertyValue("draworder", out var draworder)) WriteDrawOrderTimelines(draworder.AsArray()); else writer.WriteVarInt(0);
if (data.TryGetPropertyValue("events", out var events)) WriteEventTimelines(events.AsArray()); else writer.WriteVarInt(0);
}
}
private void WriteSlotTimelines(JsonObject slotTimelines)
{
writer.WriteVarInt(slotTimelines.Count);
foreach (var (name, _timeline) in slotTimelines)
{
JsonObject timeline = _timeline.AsObject();
writer.WriteVarInt(slot2idx[name]);
writer.WriteVarInt(timeline.Count);
foreach (var (type, _frames) in timeline)
{
JsonArray frames = _frames.AsArray();
if (type == "attachment")
{
writer.WriteByte(SkeletonBinary.SLOT_ATTACHMENT);
writer.WriteVarInt(frames.Count);
foreach (JsonObject o in frames)
{
if (o.TryGetPropertyValue("time", out var time)) writer.WriteFloat((float)time); else writer.WriteFloat(0);
writer.WriteStringRef((string)o["name"]);
}
}
else if (type == "color")
{
writer.WriteByte(SkeletonBinary.SLOT_COLOR);
writer.WriteVarInt(frames.Count);
for (int i = 0, n = frames.Count; i < n; i++)
{
JsonObject o = frames[i].AsObject();
if (o.TryGetPropertyValue("time", out var time)) writer.WriteFloat((float)time); else writer.WriteFloat(0);
writer.WriteInt(int.Parse((string)o["color"], NumberStyles.HexNumber));
if (i < n - 1) WriteCurve(o);
}
}
else if (type == "twoColor")
{
writer.WriteByte(SkeletonBinary.SLOT_TWO_COLOR);
writer.WriteVarInt(frames.Count);
for (int i = 0, n = frames.Count; i < n; i++)
{
JsonObject o = frames[i].AsObject();
if (o.TryGetPropertyValue("time", out var time)) writer.WriteFloat((float)time); else writer.WriteFloat(0);
writer.WriteInt(int.Parse((string)o["light"], NumberStyles.HexNumber));
writer.WriteInt(int.Parse((string)o["dark"], NumberStyles.HexNumber));
if (i < n - 1) WriteCurve(o);
}
}
}
}
}
private void WriteBoneTimelines(JsonObject boneTimelines)
{
writer.WriteVarInt(boneTimelines.Count);
foreach (var (name, _timeline) in boneTimelines)
{
JsonObject timeline = _timeline.AsObject();
writer.WriteVarInt(bone2idx[name]);
writer.WriteVarInt(timeline.Count);
foreach (var (type, _frames) in timeline)
{
JsonArray frames = _frames.AsArray();
if (type == "rotate")
{
writer.WriteByte(SkeletonBinary.BONE_ROTATE);
writer.WriteVarInt(frames.Count);
for (int i = 0, n = frames.Count; i < n; i++)
{
JsonObject o = frames[i].AsObject();
if (o.TryGetPropertyValue("time", out var time)) writer.WriteFloat((float)time); else writer.WriteFloat(0);
if (o.TryGetPropertyValue("angle", out var angle)) writer.WriteFloat((float)angle); else writer.WriteFloat(0);
if (i < n - 1) WriteCurve(o);
}
}
else if (type == "translate")
{
writer.WriteByte(SkeletonBinary.BONE_TRANSLATE);
writer.WriteVarInt(frames.Count);
for (int i = 0, n = frames.Count; i < n; i++)
{
JsonObject o = frames[i].AsObject();
if (o.TryGetPropertyValue("time", out var time)) writer.WriteFloat((float)time); else writer.WriteFloat(0);
if (o.TryGetPropertyValue("x", out var x)) writer.WriteFloat((float)x); else writer.WriteFloat(0);
if (o.TryGetPropertyValue("y", out var y)) writer.WriteFloat((float)y); else writer.WriteFloat(0);
if (i < n - 1) WriteCurve(o);
}
}
else if (type == "scale")
{
writer.WriteByte(SkeletonBinary.BONE_SCALE);
writer.WriteVarInt(frames.Count);
for (int i = 0, n = frames.Count; i < n; i++)
{
JsonObject o = frames[i].AsObject();
if (o.TryGetPropertyValue("time", out var time)) writer.WriteFloat((float)time); else writer.WriteFloat(0);
if (o.TryGetPropertyValue("x", out var x)) writer.WriteFloat((float)x); else writer.WriteFloat(1);
if (o.TryGetPropertyValue("y", out var y)) writer.WriteFloat((float)y); else writer.WriteFloat(1);
if (i < n - 1) WriteCurve(o);
}
}
else if (type == "shear")
{
writer.WriteByte(SkeletonBinary.BONE_SHEAR);
writer.WriteVarInt(frames.Count);
for (int i = 0, n = frames.Count; i < n; i++)
{
JsonObject o = frames[i].AsObject();
if (o.TryGetPropertyValue("time", out var time)) writer.WriteFloat((float)time); else writer.WriteFloat(0);
if (o.TryGetPropertyValue("x", out var x)) writer.WriteFloat((float)x); else writer.WriteFloat(0);
if (o.TryGetPropertyValue("y", out var y)) writer.WriteFloat((float)y); else writer.WriteFloat(0);
if (i < n - 1) WriteCurve(o);
}
}
}
}
}
private void WriteIKTimelines(JsonObject ikTimelines)
{
writer.WriteVarInt(ikTimelines.Count);
foreach (var (name, _frames) in ikTimelines)
{
JsonArray frames = _frames.AsArray();
writer.WriteVarInt(ik2idx[name]);
writer.WriteVarInt(frames.Count);
for (int i = 0, n = frames.Count; i < n; i++)
{
JsonObject o = frames[i].AsObject();
if (o.TryGetPropertyValue("time", out var time)) writer.WriteFloat((float)time); else writer.WriteFloat(0);
if (o.TryGetPropertyValue("mix", out var mix)) writer.WriteFloat((float)mix); else writer.WriteFloat(1);
if (o.TryGetPropertyValue("softness", out var softness)) writer.WriteFloat((float)softness); else writer.WriteFloat(0);
if (o.TryGetPropertyValue("bendPositive", out var bendPositive)) writer.WriteSByte((sbyte)((bool)bendPositive ? 1 : -1)); else writer.WriteSByte(1);
if (o.TryGetPropertyValue("compress", out var compress)) writer.WriteBoolean((bool)compress); else writer.WriteBoolean(false);
if (o.TryGetPropertyValue("stretch", out var stretch)) writer.WriteBoolean((bool)stretch); else writer.WriteBoolean(false);
if (i < n - 1) WriteCurve(o);
}
}
}
private void WriteTransformTimelines(JsonObject transformTimelines)
{
writer.WriteVarInt(transformTimelines.Count);
foreach (var (name, _frames) in transformTimelines)
{
JsonArray frames = _frames.AsArray();
writer.WriteVarInt(transform2idx[name]);
writer.WriteVarInt(frames.Count);
for (int i = 0, n = frames.Count; i < n; i++)
{
JsonObject o = frames[i].AsObject();
if (o.TryGetPropertyValue("time", out var time)) writer.WriteFloat((float)time); else writer.WriteFloat(0);
if (o.TryGetPropertyValue("rotateMix", out var rotateMix)) writer.WriteFloat((float)rotateMix); else writer.WriteFloat(1);
if (o.TryGetPropertyValue("translateMix", out var translateMix)) writer.WriteFloat((float)translateMix); else writer.WriteFloat(1);
if (o.TryGetPropertyValue("scaleMix", out var scaleMix)) writer.WriteFloat((float)scaleMix); else writer.WriteFloat(1);
if (o.TryGetPropertyValue("shearMix", out var shearMix)) writer.WriteFloat((float)shearMix); else writer.WriteFloat(1);
if (i < n - 1) WriteCurve(o);
}
}
}
private void WritePathTimelines(JsonObject pathTimelines)
{
writer.WriteVarInt(pathTimelines.Count);
foreach (var (name, _timeline) in pathTimelines)
{
JsonObject timeline = _timeline.AsObject();
writer.WriteVarInt(path2idx[name]);
writer.WriteVarInt(timeline.Count);
foreach (var (type, _frame) in timeline)
{
JsonArray frames = _frame.AsArray();
if (type == "position")
{
writer.WriteByte(SkeletonBinary.PATH_POSITION);
writer.WriteVarInt(frames.Count);
for (int i = 0, n = frames.Count; i < n; i++)
{
JsonObject o = frames[i].AsObject();
if (o.TryGetPropertyValue("time", out var time)) writer.WriteFloat((float)time); else writer.WriteFloat(0);
if (o.TryGetPropertyValue("position", out var position)) writer.WriteFloat((float)position); else writer.WriteFloat(0);
if (i < n - 1) WriteCurve(o);
}
}
else if (type == "spacing")
{
writer.WriteByte(SkeletonBinary.PATH_SPACING);
writer.WriteVarInt(frames.Count);
for (int i = 0, n = frames.Count; i < n; i++)
{
JsonObject o = frames[i].AsObject();
if (o.TryGetPropertyValue("time", out var time)) writer.WriteFloat((float)time); else writer.WriteFloat(0);
if (o.TryGetPropertyValue("spacing", out var spacing)) writer.WriteFloat((float)spacing); else writer.WriteFloat(0);
if (i < n - 1) WriteCurve(o);
}
}
else if (type == "mix")
{
writer.WriteByte(SkeletonBinary.PATH_MIX);
writer.WriteVarInt(frames.Count);
for (int i = 0, n = frames.Count; i < n; i++)
{
JsonObject o = frames[i].AsObject();
if (o.TryGetPropertyValue("time", out var time)) writer.WriteFloat((float)time); else writer.WriteFloat(0);
if (o.TryGetPropertyValue("rotateMix", out var rotateMix)) writer.WriteFloat((float)rotateMix); else writer.WriteFloat(1);
if (o.TryGetPropertyValue("translateMix", out var translateMix)) writer.WriteFloat((float)translateMix); else writer.WriteFloat(1);
if (i < n - 1) WriteCurve(o);
}
}
}
}
}
private void WriteDeformTimelines(JsonObject deformTimelines)
{
writer.WriteVarInt(deformTimelines.Count);
foreach (var (skinName, _skinValue) in deformTimelines)
{
JsonObject skinValue = _skinValue.AsObject();
writer.WriteVarInt(skin2idx[skinName]);
writer.WriteVarInt(skinValue.Count);
foreach (var (slotName, _slotValue) in skinValue)
{
JsonObject slotValue = _slotValue.AsObject();
writer.WriteVarInt(slot2idx[slotName]);
writer.WriteVarInt(slotValue.Count);
foreach (var (attachmentName, _frames) in slotValue)
{
JsonArray frames = _frames.AsArray();
writer.WriteStringRef(attachmentName);
writer.WriteVarInt(frames.Count);
for (int i = 0, n = frames.Count; i < n; i++)
{
JsonObject o = frames[i].AsObject();
if (o.TryGetPropertyValue("time", out var time)) writer.WriteFloat((float)time); else writer.WriteFloat(0);
if (o.TryGetPropertyValue("vertices", out var _vertices))
{
JsonArray vertices = _vertices.AsArray();
writer.WriteVarInt(vertices.Count);
if (vertices.Count > 0)
{
if (o.TryGetPropertyValue("offset", out var offset)) writer.WriteVarInt((int)offset); else writer.WriteVarInt(0);
WriteFloatArray(vertices, vertices.Count);
}
}
else
{
writer.WriteVarInt(0);
}
if (i < n - 1) WriteCurve(o);
}
}
}
}
}
private void WriteDrawOrderTimelines(JsonArray drawOrderTimelines)
{
writer.WriteVarInt(drawOrderTimelines.Count);
foreach (JsonObject data in drawOrderTimelines)
{
if (data.TryGetPropertyValue("time", out var time)) writer.WriteFloat((float)time); else writer.WriteFloat(0);
if (data.TryGetPropertyValue("offsets", out var _offsets))
{
JsonArray offsets = _offsets.AsArray();
writer.WriteVarInt(offsets.Count);
foreach (JsonObject o in offsets)
{
writer.WriteVarInt(slot2idx[(string)o["slot"]]);
writer.WriteVarInt((int)o["offset"]);
}
}
else
{
writer.WriteVarInt(0);
}
}
}
private void WriteEventTimelines(JsonArray eventTimelines)
{
JsonObject events = root["events"].AsObject();
writer.WriteVarInt(eventTimelines.Count);
foreach(JsonObject data in eventTimelines)
{
JsonObject eventData = events[(string)data["name"]].AsObject();
if (data.TryGetPropertyValue("time", out var time)) writer.WriteFloat((float)time); else writer.WriteFloat(0);
writer.WriteVarInt(event2idx[(string)data["name"]]);
if (data.TryGetPropertyValue("int", out var @int)) writer.WriteVarInt((int)@int); else
if (eventData.TryGetPropertyValue("int", out var @int2)) writer.WriteVarInt((int)@int2); else writer.WriteVarInt(0);
if (data.TryGetPropertyValue("float", out var @float)) writer.WriteFloat((float)@float); else
if (eventData.TryGetPropertyValue("float", out var @float2)) writer.WriteFloat((float)@float2); else writer.WriteFloat(0);
if (data.TryGetPropertyValue("string", out var @string))
{
writer.WriteBoolean(true);
writer.WriteString((string)@string);
}
else
{
writer.WriteBoolean(false);
}
if (eventData.ContainsKey("audio"))
{
if (data.TryGetPropertyValue("volume", out var volume)) writer.WriteFloat((float)volume); else
if (eventData.TryGetPropertyValue("volume", out var volume2)) writer.WriteFloat((float)volume2); else writer.WriteFloat(1);
if (data.TryGetPropertyValue("balance", out var balance)) writer.WriteFloat((float)balance); else
if (eventData.TryGetPropertyValue("balance", out var balance2)) writer.WriteFloat((float)balance2); else writer.WriteFloat(0);
}
}
}
@@ -1236,7 +1616,7 @@ namespace SpineViewer.Spine.Implementations.SkeletonConverter
public void WriteShortArray(JsonArray array)
{
writer.WriteVarInt(array.Count);
foreach (uint i in array)
foreach (int i in array)
{
writer.WriteByte((byte)(i >> 8));
writer.WriteByte((byte)i);
@@ -1269,6 +1649,29 @@ namespace SpineViewer.Spine.Implementations.SkeletonConverter
}
}
private void WriteCurve(JsonObject obj)
{
if (obj.TryGetPropertyValue("curve", out var curve))
{
if (curve.GetValueKind() == JsonValueKind.String)
{
writer.WriteByte(SkeletonBinary.CURVE_STEPPED);
}
else
{
writer.WriteByte(SkeletonBinary.CURVE_BEZIER);
writer.WriteFloat((float)curve);
if (obj.TryGetPropertyValue("c2", out var c2)) writer.WriteFloat((float)c2); else writer.WriteFloat(0);
if (obj.TryGetPropertyValue("c3", out var c3)) writer.WriteFloat((float)c3); else writer.WriteFloat(1);
if (obj.TryGetPropertyValue("c4", out var c4)) writer.WriteFloat((float)c4); else writer.WriteFloat(1);
}
}
else
{
writer.WriteByte(SkeletonBinary.CURVE_LINEAR);
}
}
public override JsonObject ReadJson(string jsonPath)
{
// replace 3.8.75 to another version to avoid detection in official runtime
@@ -1288,11 +1691,11 @@ namespace SpineViewer.Spine.Implementations.SkeletonConverter
base.WriteJson(root, jsonPath);
}
public override JsonObject ToVersion(JsonObject root, Version version)
public override JsonObject ToVersion(JsonObject root, SpineVersion version)
{
root = version switch
{
Version.V38 => root.DeepClone().AsObject(),
SpineVersion.V38 => root.DeepClone().AsObject(),
_ => throw new NotImplementedException(),
};
return root;

View File

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

View File

@@ -1,352 +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.Spine
{
[SpineImplementation(Version.V36)]
internal class Spine36 : SpineViewer.Spine.Spine
{
private static readonly Animation EmptyAnimation = new(EMPTY_ANIMATION, [], 0);
private class TextureLoader : SpineRuntime36.TextureLoader
{
public void Load(AtlasPage page, string path)
{
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 InvalidDataException($"Unknown skeleton file format {SkelPath}");
}
}
animationStateData = new AnimationStateData(skeletonData);
skeleton = new Skeleton(skeletonData);
animationState = new AnimationState(animationStateData);
foreach (var anime in skeletonData.Animations)
animationNames.Add(anime.Name);
CurrentAnimation = DefaultAnimationName;
}
protected override void Dispose(bool disposing)
{
base.Dispose(disposing);
atlas.Dispose();
}
public override string FileVersion { get => skeletonData.Version; }
public override float Scale
{
get
{
if (skeletonBinary is not null)
return skeletonBinary.Scale;
else if (skeletonJson is not null)
return skeletonJson.Scale;
else
return 1f;
}
set
{
// 保存状态
var position = Position;
var flipX = FlipX;
var flipY = FlipY;
var savedTrack0 = animationState.GetCurrent(0);
var val = Math.Max(value, SCALE_MIN);
if (skeletonBinary is not null)
{
skeletonBinary.Scale = val;
skeletonData = skeletonBinary.ReadSkeletonData(SkelPath);
}
else if (skeletonJson is not null)
{
skeletonJson.Scale = val;
skeletonData = skeletonJson.ReadSkeletonData(SkelPath);
}
// reload skel-dependent data
animationStateData = new AnimationStateData(skeletonData) { DefaultMix = animationStateData.DefaultMix };
skeleton = new Skeleton(skeletonData);
animationState = new AnimationState(animationStateData);
// 恢复状态
Position = position;
FlipX = flipX;
FlipY = flipY;
// 恢复原本 Track0 上所有动画
if (savedTrack0 is not null)
{
var entry = animationState.SetAnimation(0, savedTrack0.Animation.Name, true);
entry.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 ?? EMPTY_ANIMATION;
set
{
if (value == EMPTY_ANIMATION)
animationState.SetAnimation(0, EmptyAnimation, false);
else if (animationNames.Contains(value))
animationState.SetAnimation(0, value, true);
Update(0);
}
}
public override RectangleF Bounds
{
get
{
float[] _ = [];
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(BlendMode spineBlendMode)
{
return spineBlendMode switch
{
BlendMode.Normal => BlendModeSFML.Normal,
BlendMode.Additive => BlendModeSFML.Additive,
BlendMode.Multiply => BlendModeSFML.Multiply,
BlendMode.Screen => BlendModeSFML.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 == BlendModeSFML.Normal || states.BlendMode == BlendModeSFML.Additive))
states.Shader = FragmentShader;
else
states.Shader = null;
target.Draw(vertexArray, states);
vertexArray.Clear();
}
states.BlendMode = blendMode;
states.Texture = texture;
}
if (clipping.IsClipping)
{
// 这里必须单独记录 Count, 和 Items 的 Length 是不一致的
clipping.ClipTriangles(worldVertices, worldVerticesCount * 2, worldTriangleIndices, worldTriangleIndicesLength, uvs);
worldVertices = clipping.ClippedVertices.Items;
worldVerticesCount = clipping.ClippedVertices.Count / 2;
worldTriangleIndices = clipping.ClippedTriangles.Items;
worldTriangleIndicesLength = clipping.ClippedTriangles.Count;
uvs = clipping.ClippedUVs.Items;
}
var textureSizeX = texture.Size.X;
var textureSizeY = texture.Size.Y;
SFML.Graphics.Vertex vertex = new();
vertex.Color.R = (byte)(tintR * 255);
vertex.Color.G = (byte)(tintG * 255);
vertex.Color.B = (byte)(tintB * 255);
vertex.Color.A = (byte)(tintA * 255);
// 必须用 worldTriangleIndicesLength 不能直接 foreach
for (int i = 0; i < worldTriangleIndicesLength; i++)
{
var index = worldTriangleIndices[i] * 2;
vertex.Position.X = worldVertices[index];
vertex.Position.Y = worldVertices[index + 1];
vertex.TexCoords.X = uvs[index] * textureSizeX;
vertex.TexCoords.Y = uvs[index + 1] * textureSizeY;
vertexArray.Append(vertex);
}
clipping.ClipEnd(slot);
}
if (UsePremultipliedAlpha && (states.BlendMode == BlendModeSFML.Normal || states.BlendMode == BlendModeSFML.Additive))
states.Shader = FragmentShader;
else
states.Shader = null;
target.Draw(vertexArray, states);
clipping.ClipEnd();
// 包围盒
if (IsDebug && IsSelected && DebugBounds)
{
var bounds = Bounds;
boundsVertices[0] = boundsVertices[4] = new(new(bounds.Left, bounds.Top), BoundsColor);
boundsVertices[1] = new(new(bounds.Right, bounds.Top), BoundsColor);
boundsVertices[2] = new(new(bounds.Right, bounds.Bottom), BoundsColor);
boundsVertices[3] = new(new(bounds.Left, bounds.Bottom), BoundsColor);
target.Draw(boundsVertices);
}
}
}
}

View File

@@ -1,360 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using SpineRuntime37;
namespace SpineViewer.Spine.Implementations.Spine
{
[SpineImplementation(Version.V37)]
internal class Spine37 : SpineViewer.Spine.Spine
{
private static readonly Animation EmptyAnimation = new(EMPTY_ANIMATION, [], 0);
private class TextureLoader : SpineRuntime37.TextureLoader
{
public void Load(AtlasPage page, string path)
{
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 InvalidDataException($"Unknown skeleton file format {SkelPath}");
}
}
animationStateData = new AnimationStateData(skeletonData);
skeleton = new Skeleton(skeletonData);
animationState = new AnimationState(animationStateData);
foreach (var anime in skeletonData.Animations)
animationNames.Add(anime.Name);
CurrentAnimation = DefaultAnimationName;
}
protected override void Dispose(bool disposing)
{
base.Dispose(disposing);
atlas.Dispose();
}
public override string FileVersion { get => skeletonData.Version; }
public override float Scale
{
get
{
if (skeletonBinary is not null)
return skeletonBinary.Scale;
else if (skeletonJson is not null)
return skeletonJson.Scale;
else
return 1f;
}
set
{
// 保存状态
var position = Position;
var flipX = FlipX;
var flipY = FlipY;
var savedTrack0 = animationState.GetCurrent(0);
var val = Math.Max(value, SCALE_MIN);
if (skeletonBinary is not null)
{
skeletonBinary.Scale = val;
skeletonData = skeletonBinary.ReadSkeletonData(SkelPath);
}
else if (skeletonJson is not null)
{
skeletonJson.Scale = val;
skeletonData = skeletonJson.ReadSkeletonData(SkelPath);
}
// reload skel-dependent data
animationStateData = new AnimationStateData(skeletonData) { DefaultMix = animationStateData.DefaultMix };
skeleton = new Skeleton(skeletonData);
animationState = new AnimationState(animationStateData);
// 恢复状态
Position = position;
FlipX = flipX;
FlipY = flipY;
// 恢复原本 Track0 上所有动画
if (savedTrack0 is not null)
{
var entry = animationState.SetAnimation(0, savedTrack0.Animation.Name, true);
entry.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 ?? EMPTY_ANIMATION;
set
{
if (value == EMPTY_ANIMATION)
animationState.SetAnimation(0, EmptyAnimation, false);
else if (animationNames.Contains(value))
animationState.SetAnimation(0, value, true);
Update(0);
}
}
public override RectangleF Bounds
{
get
{
float[] _ = [];
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(BlendMode spineBlendMode)
{
return spineBlendMode switch
{
BlendMode.Normal => BlendModeSFML.Normal,
BlendMode.Additive => BlendModeSFML.Additive,
BlendMode.Multiply => BlendModeSFML.Multiply,
BlendMode.Screen => BlendModeSFML.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 == BlendModeSFML.Normal || states.BlendMode == BlendModeSFML.Additive))
states.Shader = FragmentShader;
else
states.Shader = null;
target.Draw(vertexArray, states);
vertexArray.Clear();
}
states.BlendMode = blendMode;
states.Texture = texture;
}
if (clipping.IsClipping)
{
// 这里必须单独记录 Count, 和 Items 的 Length 是不一致的
clipping.ClipTriangles(worldVertices, worldVerticesCount * 2, worldTriangleIndices, worldTriangleIndicesLength, uvs);
worldVertices = clipping.ClippedVertices.Items;
worldVerticesCount = clipping.ClippedVertices.Count / 2;
worldTriangleIndices = clipping.ClippedTriangles.Items;
worldTriangleIndicesLength = clipping.ClippedTriangles.Count;
uvs = clipping.ClippedUVs.Items;
}
var textureSizeX = texture.Size.X;
var textureSizeY = texture.Size.Y;
SFML.Graphics.Vertex vertex = new();
vertex.Color.R = (byte)(tintR * 255);
vertex.Color.G = (byte)(tintG * 255);
vertex.Color.B = (byte)(tintB * 255);
vertex.Color.A = (byte)(tintA * 255);
// 必须用 worldTriangleIndicesLength 不能直接 foreach
for (int i = 0; i < worldTriangleIndicesLength; i++)
{
var index = worldTriangleIndices[i] * 2;
vertex.Position.X = worldVertices[index];
vertex.Position.Y = worldVertices[index + 1];
vertex.TexCoords.X = uvs[index] * textureSizeX;
vertex.TexCoords.Y = uvs[index + 1] * textureSizeY;
vertexArray.Append(vertex);
}
clipping.ClipEnd(slot);
}
if (UsePremultipliedAlpha && (states.BlendMode == BlendModeSFML.Normal || states.BlendMode == BlendModeSFML.Additive))
states.Shader = FragmentShader;
else
states.Shader = null;
target.Draw(vertexArray, states);
clipping.ClipEnd();
// 包围盒
if (IsDebug && IsSelected && DebugBounds)
{
var bounds = Bounds;
boundsVertices[0] = boundsVertices[4] = new(new(bounds.Left, bounds.Top), BoundsColor);
boundsVertices[1] = new(new(bounds.Right, bounds.Top), BoundsColor);
boundsVertices[2] = new(new(bounds.Right, bounds.Bottom), BoundsColor);
boundsVertices[3] = new(new(bounds.Left, bounds.Bottom), BoundsColor);
target.Draw(boundsVertices);
}
}
}
}

View File

@@ -1,363 +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;
namespace SpineViewer.Spine.Implementations.Spine
{
[SpineImplementation(Version.V38)]
internal class Spine38 : SpineViewer.Spine.Spine
{
private static readonly Animation EmptyAnimation = new(EMPTY_ANIMATION, [], 0);
private class TextureLoader : SpineRuntime38.TextureLoader
{
public void Load(AtlasPage page, string path)
{
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 InvalidDataException($"Unknown skeleton file format {SkelPath}");
}
}
animationStateData = new AnimationStateData(skeletonData);
skeleton = new Skeleton(skeletonData);
animationState = new AnimationState(animationStateData);
foreach (var anime in skeletonData.Animations)
animationNames.Add(anime.Name);
CurrentAnimation = DefaultAnimationName;
}
protected override void Dispose(bool disposing)
{
base.Dispose(disposing);
atlas.Dispose();
}
public override string FileVersion { get => skeletonData.Version; }
public override float Scale
{
get
{
if (skeletonBinary is not null)
return skeletonBinary.Scale;
else if (skeletonJson is not null)
return skeletonJson.Scale;
else
return 1f;
}
set
{
// 保存状态
var position = Position;
var flipX = FlipX;
var flipY = FlipY;
var savedTrack0 = animationState.GetCurrent(0);
var val = Math.Max(value, SCALE_MIN);
if (skeletonBinary is not null)
{
skeletonBinary.Scale = val;
skeletonData = skeletonBinary.ReadSkeletonData(SkelPath);
}
else if (skeletonJson is not null)
{
skeletonJson.Scale = val;
skeletonData = skeletonJson.ReadSkeletonData(SkelPath);
}
// reload skel-dependent data
animationStateData = new AnimationStateData(skeletonData) { DefaultMix = animationStateData.DefaultMix };
skeleton = new Skeleton(skeletonData);
animationState = new AnimationState(animationStateData);
// 恢复状态
Position = position;
FlipX = flipX;
FlipY = flipY;
// 恢复原本 Track0 上所有动画
if (savedTrack0 is not null)
{
var entry = animationState.SetAnimation(0, savedTrack0.Animation.Name, true);
entry.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 ?? EMPTY_ANIMATION;
set
{
if (value == EMPTY_ANIMATION)
animationState.SetAnimation(0, EmptyAnimation, false);
else if (animationNames.Contains(value))
animationState.SetAnimation(0, value, true);
Update(0);
}
}
public override RectangleF Bounds
{
get
{
float[] _ = [];
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(BlendMode spineBlendMode)
{
return spineBlendMode switch
{
BlendMode.Normal => BlendModeSFML.Normal,
BlendMode.Additive => BlendModeSFML.Additive,
BlendMode.Multiply => BlendModeSFML.Multiply,
BlendMode.Screen => BlendModeSFML.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 == BlendModeSFML.Normal || states.BlendMode == BlendModeSFML.Additive))
states.Shader = FragmentShader;
else
states.Shader = null;
target.Draw(vertexArray, states);
vertexArray.Clear();
}
states.BlendMode = blendMode;
states.Texture = texture;
}
if (clipping.IsClipping)
{
// 这里必须单独记录 Count, 和 Items 的 Length 是不一致的
clipping.ClipTriangles(worldVertices, worldVerticesCount * 2, worldTriangleIndices, worldTriangleIndicesLength, uvs);
worldVertices = clipping.ClippedVertices.Items;
worldVerticesCount = clipping.ClippedVertices.Count / 2;
worldTriangleIndices = clipping.ClippedTriangles.Items;
worldTriangleIndicesLength = clipping.ClippedTriangles.Count;
uvs = clipping.ClippedUVs.Items;
}
var textureSizeX = texture.Size.X;
var textureSizeY = texture.Size.Y;
SFML.Graphics.Vertex vertex = new();
vertex.Color.R = (byte)(tintR * 255);
vertex.Color.G = (byte)(tintG * 255);
vertex.Color.B = (byte)(tintB * 255);
vertex.Color.A = (byte)(tintA * 255);
// 必须用 worldTriangleIndicesLength 不能直接 foreach
for (int i = 0; i < worldTriangleIndicesLength; i++)
{
var index = worldTriangleIndices[i] * 2;
vertex.Position.X = worldVertices[index];
vertex.Position.Y = worldVertices[index + 1];
vertex.TexCoords.X = uvs[index] * textureSizeX;
vertex.TexCoords.Y = uvs[index + 1] * textureSizeY;
vertexArray.Append(vertex);
}
clipping.ClipEnd(slot);
}
if (UsePremultipliedAlpha && (states.BlendMode == BlendModeSFML.Normal || states.BlendMode == BlendModeSFML.Additive))
states.Shader = FragmentShader;
else
states.Shader = null;
target.Draw(vertexArray, states);
clipping.ClipEnd();
// 包围盒
if (IsDebug && IsSelected && DebugBounds)
{
var bounds = Bounds;
boundsVertices[0] = boundsVertices[4] = new(new(bounds.Left, bounds.Top), BoundsColor);
boundsVertices[1] = new(new(bounds.Right, bounds.Top), BoundsColor);
boundsVertices[2] = new(new(bounds.Right, bounds.Bottom), BoundsColor);
boundsVertices[3] = new(new(bounds.Left, bounds.Bottom), BoundsColor);
target.Draw(boundsVertices);
}
}
}
}

View File

@@ -1,362 +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.Spine
{
[SpineImplementation(Version.V40)]
internal class Spine40 : SpineViewer.Spine.Spine
{
private static readonly Animation EmptyAnimation = new(EMPTY_ANIMATION, [], 0);
private class TextureLoader : SpineRuntime40.TextureLoader
{
public void Load(AtlasPage page, string path)
{
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 InvalidDataException($"Unknown skeleton file format {SkelPath}");
}
}
animationStateData = new AnimationStateData(skeletonData);
skeleton = new Skeleton(skeletonData);
animationState = new AnimationState(animationStateData);
foreach (var anime in skeletonData.Animations)
animationNames.Add(anime.Name);
CurrentAnimation = DefaultAnimationName;
}
protected override void Dispose(bool disposing)
{
base.Dispose(disposing);
atlas.Dispose();
}
public override string FileVersion { get => skeletonData.Version; }
public override float Scale
{
get
{
if (skeletonBinary is not null)
return skeletonBinary.Scale;
else if (skeletonJson is not null)
return skeletonJson.Scale;
else
return 1f;
}
set
{
// 保存状态
var position = Position;
var flipX = FlipX;
var flipY = FlipY;
var savedTrack0 = animationState.GetCurrent(0);
var val = Math.Max(value, SCALE_MIN);
if (skeletonBinary is not null)
{
skeletonBinary.Scale = val;
skeletonData = skeletonBinary.ReadSkeletonData(SkelPath);
}
else if (skeletonJson is not null)
{
skeletonJson.Scale = val;
skeletonData = skeletonJson.ReadSkeletonData(SkelPath);
}
// reload skel-dependent data
animationStateData = new AnimationStateData(skeletonData) { DefaultMix = animationStateData.DefaultMix };
skeleton = new Skeleton(skeletonData);
animationState = new AnimationState(animationStateData);
// 恢复状态
Position = position;
FlipX = flipX;
FlipY = flipY;
// 恢复原本 Track0 上所有动画
if (savedTrack0 is not null)
{
var entry = animationState.SetAnimation(0, savedTrack0.Animation.Name, true);
entry.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 ?? EMPTY_ANIMATION;
set
{
if (value == EMPTY_ANIMATION)
animationState.SetAnimation(0, EmptyAnimation, false);
else if (animationNames.Contains(value))
animationState.SetAnimation(0, value, true);
Update(0);
}
}
public override RectangleF Bounds
{
get
{
float[] _ = [];
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(BlendMode spineBlendMode)
{
return spineBlendMode switch
{
BlendMode.Normal => BlendModeSFML.Normal,
BlendMode.Additive => BlendModeSFML.Additive,
BlendMode.Multiply => BlendModeSFML.Multiply,
BlendMode.Screen => BlendModeSFML.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 == BlendModeSFML.Normal || states.BlendMode == BlendModeSFML.Additive))
states.Shader = FragmentShader;
else
states.Shader = null;
target.Draw(vertexArray, states);
vertexArray.Clear();
}
states.BlendMode = blendMode;
states.Texture = texture;
}
if (clipping.IsClipping)
{
// 这里必须单独记录 Count, 和 Items 的 Length 是不一致的
clipping.ClipTriangles(worldVertices, worldVerticesCount * 2, worldTriangleIndices, worldTriangleIndicesLength, uvs);
worldVertices = clipping.ClippedVertices.Items;
worldVerticesCount = clipping.ClippedVertices.Count / 2;
worldTriangleIndices = clipping.ClippedTriangles.Items;
worldTriangleIndicesLength = clipping.ClippedTriangles.Count;
uvs = clipping.ClippedUVs.Items;
}
var textureSizeX = texture.Size.X;
var textureSizeY = texture.Size.Y;
SFML.Graphics.Vertex vertex = new();
vertex.Color.R = (byte)(tintR * 255);
vertex.Color.G = (byte)(tintG * 255);
vertex.Color.B = (byte)(tintB * 255);
vertex.Color.A = (byte)(tintA * 255);
// 必须用 worldTriangleIndicesLength 不能直接 foreach
for (int i = 0; i < worldTriangleIndicesLength; i++)
{
var index = worldTriangleIndices[i] * 2;
vertex.Position.X = worldVertices[index];
vertex.Position.Y = worldVertices[index + 1];
vertex.TexCoords.X = uvs[index] * textureSizeX;
vertex.TexCoords.Y = uvs[index + 1] * textureSizeY;
vertexArray.Append(vertex);
}
clipping.ClipEnd(slot);
}
if (UsePremultipliedAlpha && (states.BlendMode == BlendModeSFML.Normal || states.BlendMode == BlendModeSFML.Additive))
states.Shader = FragmentShader;
else
states.Shader = null;
target.Draw(vertexArray, states);
clipping.ClipEnd();
// 包围盒
if (IsDebug && IsSelected && DebugBounds)
{
var bounds = Bounds;
boundsVertices[0] = boundsVertices[4] = new(new(bounds.Left, bounds.Top), BoundsColor);
boundsVertices[1] = new(new(bounds.Right, bounds.Top), BoundsColor);
boundsVertices[2] = new(new(bounds.Right, bounds.Bottom), BoundsColor);
boundsVertices[3] = new(new(bounds.Left, bounds.Bottom), BoundsColor);
target.Draw(boundsVertices);
}
}
}
}

View File

@@ -1,362 +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.Spine
{
[SpineImplementation(Version.V41)]
internal class Spine41 : SpineViewer.Spine.Spine
{
private static readonly Animation EmptyAnimation = new(EMPTY_ANIMATION, [], 0);
private class TextureLoader : SpineRuntime41.TextureLoader
{
public void Load(AtlasPage page, string path)
{
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 InvalidDataException($"Unknown skeleton file format {SkelPath}");
}
}
animationStateData = new AnimationStateData(skeletonData);
skeleton = new Skeleton(skeletonData);
animationState = new AnimationState(animationStateData);
foreach (var anime in skeletonData.Animations)
animationNames.Add(anime.Name);
CurrentAnimation = DefaultAnimationName;
}
protected override void Dispose(bool disposing)
{
base.Dispose(disposing);
atlas.Dispose();
}
public override string FileVersion { get => skeletonData.Version; }
public override float Scale
{
get
{
if (skeletonBinary is not null)
return skeletonBinary.Scale;
else if (skeletonJson is not null)
return skeletonJson.Scale;
else
return 1f;
}
set
{
// 保存状态
var position = Position;
var flipX = FlipX;
var flipY = FlipY;
var savedTrack0 = animationState.GetCurrent(0);
var val = Math.Max(value, SCALE_MIN);
if (skeletonBinary is not null)
{
skeletonBinary.Scale = val;
skeletonData = skeletonBinary.ReadSkeletonData(SkelPath);
}
else if (skeletonJson is not null)
{
skeletonJson.Scale = val;
skeletonData = skeletonJson.ReadSkeletonData(SkelPath);
}
// reload skel-dependent data
animationStateData = new AnimationStateData(skeletonData) { DefaultMix = animationStateData.DefaultMix };
skeleton = new Skeleton(skeletonData);
animationState = new AnimationState(animationStateData);
// 恢复状态
Position = position;
FlipX = flipX;
FlipY = flipY;
// 恢复原本 Track0 上所有动画
if (savedTrack0 is not null)
{
var entry = animationState.SetAnimation(0, savedTrack0.Animation.Name, true);
entry.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 ?? EMPTY_ANIMATION;
set
{
if (value == EMPTY_ANIMATION)
animationState.SetAnimation(0, EmptyAnimation, false);
else if (animationNames.Contains(value))
animationState.SetAnimation(0, value, true);
Update(0);
}
}
public override RectangleF Bounds
{
get
{
float[] _ = [];
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(BlendMode spineBlendMode)
{
return spineBlendMode switch
{
BlendMode.Normal => BlendModeSFML.Normal,
BlendMode.Additive => BlendModeSFML.Additive,
BlendMode.Multiply => BlendModeSFML.Multiply,
BlendMode.Screen => BlendModeSFML.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 == BlendModeSFML.Normal || states.BlendMode == BlendModeSFML.Additive))
states.Shader = FragmentShader;
else
states.Shader = null;
target.Draw(vertexArray, states);
vertexArray.Clear();
}
states.BlendMode = blendMode;
states.Texture = texture;
}
if (clipping.IsClipping)
{
// 这里必须单独记录 Count, 和 Items 的 Length 是不一致的
clipping.ClipTriangles(worldVertices, worldVerticesCount * 2, worldTriangleIndices, worldTriangleIndicesLength, uvs);
worldVertices = clipping.ClippedVertices.Items;
worldVerticesCount = clipping.ClippedVertices.Count / 2;
worldTriangleIndices = clipping.ClippedTriangles.Items;
worldTriangleIndicesLength = clipping.ClippedTriangles.Count;
uvs = clipping.ClippedUVs.Items;
}
var textureSizeX = texture.Size.X;
var textureSizeY = texture.Size.Y;
SFML.Graphics.Vertex vertex = new();
vertex.Color.R = (byte)(tintR * 255);
vertex.Color.G = (byte)(tintG * 255);
vertex.Color.B = (byte)(tintB * 255);
vertex.Color.A = (byte)(tintA * 255);
// 必须用 worldTriangleIndicesLength 不能直接 foreach
for (int i = 0; i < worldTriangleIndicesLength; i++)
{
var index = worldTriangleIndices[i] * 2;
vertex.Position.X = worldVertices[index];
vertex.Position.Y = worldVertices[index + 1];
vertex.TexCoords.X = uvs[index] * textureSizeX;
vertex.TexCoords.Y = uvs[index + 1] * textureSizeY;
vertexArray.Append(vertex);
}
clipping.ClipEnd(slot);
}
if (UsePremultipliedAlpha && (states.BlendMode == BlendModeSFML.Normal || states.BlendMode == BlendModeSFML.Additive))
states.Shader = FragmentShader;
else
states.Shader = null;
target.Draw(vertexArray, states);
clipping.ClipEnd();
// 包围盒
if (IsDebug && IsSelected && DebugBounds)
{
var bounds = Bounds;
boundsVertices[0] = boundsVertices[4] = new(new(bounds.Left, bounds.Top), BoundsColor);
boundsVertices[1] = new(new(bounds.Right, bounds.Top), BoundsColor);
boundsVertices[2] = new(new(bounds.Right, bounds.Bottom), BoundsColor);
boundsVertices[3] = new(new(bounds.Left, bounds.Bottom), BoundsColor);
target.Draw(boundsVertices);
}
}
}
}

View File

@@ -1,362 +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.Spine
{
[SpineImplementation(Version.V42)]
internal class Spine42 : SpineViewer.Spine.Spine
{
private static readonly Animation EmptyAnimation = new(EMPTY_ANIMATION, [], 0);
private class TextureLoader : SpineRuntime42.TextureLoader
{
public void Load(AtlasPage page, string path)
{
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 InvalidDataException($"Unknown skeleton file format {SkelPath}");
}
}
animationStateData = new AnimationStateData(skeletonData);
skeleton = new Skeleton(skeletonData);
animationState = new AnimationState(animationStateData);
foreach (var anime in skeletonData.Animations)
animationNames.Add(anime.Name);
CurrentAnimation = DefaultAnimationName;
}
protected override void Dispose(bool disposing)
{
base.Dispose(disposing);
atlas.Dispose();
}
public override string FileVersion { get => skeletonData.Version; }
public override float Scale
{
get
{
if (skeletonBinary is not null)
return skeletonBinary.Scale;
else if (skeletonJson is not null)
return skeletonJson.Scale;
else
return 1f;
}
set
{
// 保存状态
var position = Position;
var flipX = FlipX;
var flipY = FlipY;
var savedTrack0 = animationState.GetCurrent(0);
var val = Math.Max(value, SCALE_MIN);
if (skeletonBinary is not null)
{
skeletonBinary.Scale = val;
skeletonData = skeletonBinary.ReadSkeletonData(SkelPath);
}
else if (skeletonJson is not null)
{
skeletonJson.Scale = val;
skeletonData = skeletonJson.ReadSkeletonData(SkelPath);
}
// reload skel-dependent data
animationStateData = new AnimationStateData(skeletonData) { DefaultMix = animationStateData.DefaultMix };
skeleton = new Skeleton(skeletonData);
animationState = new AnimationState(animationStateData);
// 恢复状态
Position = position;
FlipX = flipX;
FlipY = flipY;
// 恢复原本 Track0 上所有动画
if (savedTrack0 is not null)
{
var entry = animationState.SetAnimation(0, savedTrack0.Animation.Name, true);
entry.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 ?? EMPTY_ANIMATION;
set
{
if (value == EMPTY_ANIMATION)
animationState.SetAnimation(0, EmptyAnimation, false);
else if (animationNames.Contains(value))
animationState.SetAnimation(0, value, true);
Update(0);
}
}
public override RectangleF Bounds
{
get
{
float[] _ = [];
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(BlendMode spineBlendMode)
{
return spineBlendMode switch
{
BlendMode.Normal => BlendModeSFML.Normal,
BlendMode.Additive => BlendModeSFML.Additive,
BlendMode.Multiply => BlendModeSFML.Multiply,
BlendMode.Screen => BlendModeSFML.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 == BlendModeSFML.Normal || states.BlendMode == BlendModeSFML.Additive))
states.Shader = FragmentShader;
else
states.Shader = null;
target.Draw(vertexArray, states);
vertexArray.Clear();
}
states.BlendMode = blendMode;
states.Texture = texture;
}
if (clipping.IsClipping)
{
// 这里必须单独记录 Count, 和 Items 的 Length 是不一致的
clipping.ClipTriangles(worldVertices, worldTriangleIndices, worldTriangleIndicesLength, uvs);
worldVertices = clipping.ClippedVertices.Items;
worldVerticesCount = clipping.ClippedVertices.Count / 2;
worldTriangleIndices = clipping.ClippedTriangles.Items;
worldTriangleIndicesLength = clipping.ClippedTriangles.Count;
uvs = clipping.ClippedUVs.Items;
}
var textureSizeX = texture.Size.X;
var textureSizeY = texture.Size.Y;
SFML.Graphics.Vertex vertex = new();
vertex.Color.R = (byte)(tintR * 255);
vertex.Color.G = (byte)(tintG * 255);
vertex.Color.B = (byte)(tintB * 255);
vertex.Color.A = (byte)(tintA * 255);
// 必须用 worldTriangleIndicesLength 不能直接 foreach
for (int i = 0; i < worldTriangleIndicesLength; i++)
{
var index = worldTriangleIndices[i] * 2;
vertex.Position.X = worldVertices[index];
vertex.Position.Y = worldVertices[index + 1];
vertex.TexCoords.X = uvs[index] * textureSizeX;
vertex.TexCoords.Y = uvs[index + 1] * textureSizeY;
vertexArray.Append(vertex);
}
clipping.ClipEnd(slot);
}
if (UsePremultipliedAlpha && (states.BlendMode == BlendModeSFML.Normal || states.BlendMode == BlendModeSFML.Additive))
states.Shader = FragmentShader;
else
states.Shader = null;
target.Draw(vertexArray, states);
clipping.ClipEnd();
// 包围盒
if (IsDebug && IsSelected && DebugBounds)
{
var bounds = Bounds;
boundsVertices[0] = boundsVertices[4] = new(new(bounds.Left, bounds.Top), BoundsColor);
boundsVertices[1] = new(new(bounds.Right, bounds.Top), BoundsColor);
boundsVertices[2] = new(new(bounds.Right, bounds.Bottom), BoundsColor);
boundsVertices[3] = new(new(bounds.Left, bounds.Bottom), BoundsColor);
target.Draw(boundsVertices);
}
}
}
}

View File

@@ -0,0 +1,585 @@
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).Where(v => v != "default").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
{
if (skeletonBinary is not null)
return skeletonBinary.Scale;
else if (skeletonJson is not null)
return skeletonJson.Scale;
else
return 1f;
}
set
{
// 保存状态
var pos = position;
var fX = flipX;
var fY = flipY;
var animations = animationState.Tracks.Where(te => te is not null).Select(te => te.Animation.Name).ToArray();
if (skeletonBinary is not null)
{
skeletonBinary.Scale = value;
skeletonData = skeletonBinary.ReadSkeletonData(SkelPath);
}
else if (skeletonJson is not null)
{
skeletonJson.Scale = value;
skeletonData = skeletonJson.ReadSkeletonData(SkelPath);
}
// reload skel-dependent data
skeleton = new Skeleton(skeletonData) { Skin = new(Guid.NewGuid().ToString()) };
animationStateData = new AnimationStateData(skeletonData) { DefaultMix = animationStateData.DefaultMix };
animationState = new AnimationState(animationStateData);
// 恢复状态
position = pos;
flipX = fX;
flipY = fY;
reloadSkins();
for (int i = 0; i < animations.Length; i++) setAnimation(i, animations[i]);
}
}
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.FlipX;
set => skeleton.FlipX = value;
}
protected override bool flipY
{
get => skeleton.FlipY;
set => skeleton.FlipY = value;
}
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)
{
if (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, false);
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.FlipX = skeleton.FlipX;
tmpSkeleton.FlipY = skeleton.FlipY;
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);
}
}
}
}
}

View File

@@ -0,0 +1,612 @@
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).Where(v => v != "default").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
{
if (skeletonBinary is not null)
return skeletonBinary.Scale;
else if (skeletonJson is not null)
return skeletonJson.Scale;
else
return 1f;
}
set
{
// 保存状态
var pos = position;
var fX = flipX;
var fY = flipY;
var animations = animationState.Tracks.Where(te => te is not null).Select(te => te.Animation.Name).ToArray();
if (skeletonBinary is not null)
{
skeletonBinary.Scale = value;
skeletonData = skeletonBinary.ReadSkeletonData(SkelPath);
}
else if (skeletonJson is not null)
{
skeletonJson.Scale = value;
skeletonData = skeletonJson.ReadSkeletonData(SkelPath);
}
// reload skel-dependent data
skeleton = new Skeleton(skeletonData) { Skin = new(Guid.NewGuid().ToString()) };
animationStateData = new AnimationStateData(skeletonData) { DefaultMix = animationStateData.DefaultMix };
animationState = new AnimationState(animationStateData);
// 恢复状态
position = pos;
flipX = fX;
flipY = fY;
reloadSkins();
for (int i = 0; i < animations.Length; i++) setAnimation(i, animations[i]);
}
}
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.FlipX;
set => skeleton.FlipX = value;
}
protected override bool flipY
{
get => skeleton.FlipY;
set => skeleton.FlipY = value;
}
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)
{
if (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, false);
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.FlipX = skeleton.FlipX;
tmpSkeleton.FlipY = skeleton.FlipY;
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);
}
}
}
}
}

View File

@@ -0,0 +1,583 @@
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).Where(v => v != "default").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)
{
if (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, false);
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);
}
}
}
}
}

View File

@@ -0,0 +1,586 @@
using System;
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 System.Xml.Linq;
using SpineRuntime38;
using SpineRuntime38.Attachments;
using SpineViewer.Extensions;
using SpineViewer.Utils;
namespace SpineViewer.Spine.Implementations.SpineObject
{
[SpineImplementation(SpineVersion.V38)]
internal class SpineObject38 : 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 : 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;
// 似乎是不需要设置的, 因为存在某些 png 和 atlas 大小不同的情况, 一般是有一些缩放, 如果设置了反而渲染异常
// page.width = (int)texture.Size.X;
// page.height = (int)texture.Size.Y;
}
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 SpineObject38(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 (Exception ex)
{
// 都不行就报错
throw new InvalidDataException($"Unknown skeleton file format {SkelPath}", ex);
}
}
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).Where(v => v != "default").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)
{
if (skeletonData.FindSkin(name) is Skin sk)
{
skeleton.Skin.AddSkin(sk);
skeleton.SetSlotsToSetupPose();
}
}
protected override void clearSkins()
{
skeleton.Skin.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, false);
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 (sk, _) in skinLoadStatus.Where(e => e.Value)) tmpSkeleton.Skin.AddSkin(skeletonData.FindSkin(sk));
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.Bone.Active && 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.Bone.Active && 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.Bone.Active && 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.Bone.Active && 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)
{
if (!bone.Active) continue;
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)
{
if (!bone.Active) continue;
DrawCirclePoint(target, new(bone.WorldX, bone.WorldY), BonePointColor, radius);
}
}
}
}
}

View File

@@ -0,0 +1,582 @@
using System;
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 SpineRuntime40;
using SpineViewer.Extensions;
using SpineViewer.Utils;
namespace SpineViewer.Spine.Implementations.SpineObject
{
[SpineImplementation(SpineVersion.V40)]
internal class SpineObject40 : 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 : 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;
}
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 SpineObject40(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 e in sk.Attachments)
{
var slotName = skeletonData.Slots.Items[e.SlotIndex].Name;
var att = e.Attachment;
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).Where(v => v != "default").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)
{
if (skeletonData.FindSkin(name) is Skin sk)
{
skeleton.Skin.AddSkin(sk);
skeleton.SetSlotsToSetupPose();
}
}
protected override void clearSkins()
{
skeleton.Skin.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, false);
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 (sk, _) in skinLoadStatus.Where(e => e.Value)) tmpSkeleton.Skin.AddSkin(skeletonData.FindSkin(sk));
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.Bone.Active && 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.Bone.Active && 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.Bone.Active && 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.Bone.Active && 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)
{
if (!bone.Active) continue;
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)
{
if (!bone.Active) continue;
DrawCirclePoint(target, new(bone.WorldX, bone.WorldY), BonePointColor, radius);
}
}
}
}
}

View File

@@ -0,0 +1,581 @@
using System;
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 SpineRuntime41;
using SpineViewer.Extensions;
using SpineViewer.Utils;
namespace SpineViewer.Spine.Implementations.SpineObject
{
[SpineImplementation(SpineVersion.V41)]
internal class SpineObject41 : 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 : 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;
}
public void Unload(object texture)
{
((SFML.Graphics.Texture)texture).Dispose();
}
}
private 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 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 SpineObject41(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 e in sk.Attachments)
{
var slotName = skeletonData.Slots.Items[e.SlotIndex].Name;
var att = e.Attachment;
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).Where(v => v != "default").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)
{
if (skeletonData.FindSkin(name) is Skin sk)
{
skeleton.Skin.AddSkin(sk);
skeleton.SetSlotsToSetupPose();
}
}
protected override void clearSkins()
{
skeleton.Skin.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, false);
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 (sk, _) in skinLoadStatus.Where(e => e.Value)) tmpSkeleton.Skin.AddSkin(skeletonData.FindSkin(sk));
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.UpdateWorldTransform();
}
return bounds;
}
protected override void update(float delta)
{
animationState.Update(delta);
animationState.Apply(skeleton);
//skeleton.Update(delta); // XXX: v4.1.x 没有 Update 方法
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.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 (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.Bone.Active && slot.Attachment is RegionAttachment regionAttachment)
{
regionAttachment.ComputeWorldVertices(slot, 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.Bone.Active && 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.Bone.Active && 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.Bone.Active && 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)
{
if (!bone.Active) continue;
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)
{
if (!bone.Active) continue;
DrawCirclePoint(target, new(bone.WorldX, bone.WorldY), BonePointColor, radius);
}
}
}
}
}

View File

@@ -0,0 +1,582 @@
using System;
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 SpineRuntime42;
using SpineViewer.Extensions;
using SpineViewer.Utils;
namespace SpineViewer.Spine.Implementations.SpineObject
{
[SpineImplementation(SpineVersion.V42)]
internal class SpineObject42 : Spine.SpineObject
{
private 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 : 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;
}
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 SpineObject42(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 e in sk.Attachments)
{
var slotName = skeletonData.Slots.Items[e.SlotIndex].Name;
var att = e.Attachment;
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).Where(v => v != "default").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)
{
if (skeletonData.FindSkin(name) is Skin sk)
{
skeleton.Skin.AddSkin(sk);
skeleton.SetSlotsToSetupPose();
}
}
protected override void clearSkins()
{
skeleton.Skin.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, false);
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 (sk, _) in skinLoadStatus.Where(e => e.Value)) tmpSkeleton.Skin.AddSkin(skeletonData.FindSkin(sk));
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(Skeleton.Physics.Update);
// 按 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(Skeleton.Physics.Update);
}
return bounds;
}
protected override void update(float delta)
{
animationState.Update(delta);
animationState.Apply(skeleton);
skeleton.Update(delta);
skeleton.UpdateWorldTransform(Skeleton.Physics.Update);
}
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.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 (triangleVertices.VertexCount > 0)
{
target.Draw(triangleVertices, states);
triangleVertices.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;
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.Bone.Active && slot.Attachment is RegionAttachment regionAttachment)
{
regionAttachment.ComputeWorldVertices(slot, 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.Bone.Active && 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.Bone.Active && 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.Bone.Active && 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)
{
if (!bone.Active) continue;
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)
{
if (!bone.Active) continue;
DrawCirclePoint(target, new(bone.WorldX, bone.WorldY), BonePointColor, radius);
}
}
}
}
}

View File

@@ -9,66 +9,19 @@ using System.Text;
using System.Text.Json;
using System.Text.Json.Nodes;
using System.Text.Encodings.Web;
using SpineViewer.Utils;
namespace SpineViewer.Spine
{
/// <summary>
/// SkeletonConverter 实现类标记
/// </summary>
[AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = false)]
public class SkeletonConverterImplementationAttribute : Attribute
{
public Version Version { get; }
public SkeletonConverterImplementationAttribute(Version version)
{
Version = version;
}
}
/// <summary>
/// SkeletonConverter 基类, 使用静态方法 New 来创建具体版本对象
/// </summary>
public abstract class SkeletonConverter
public abstract class SkeletonConverter : ImplementationResolver<SkeletonConverter, SpineImplementationAttribute, SpineVersion>
{
/// <summary>
/// 实现类缓存
/// </summary>
private static readonly Dictionary<Version, Type> ImplementationTypes = [];
public static readonly Dictionary<Version, Type>.KeyCollection ImplementedVersions;
/// <summary>
/// 静态构造函数
/// </summary>
static SkeletonConverter()
{
// 遍历并缓存标记了 SkeletonConverterImplementationAttribute 的类型
var impTypes = Assembly.GetExecutingAssembly().GetTypes().Where(t => typeof(SkeletonConverter).IsAssignableFrom(t) && !t.IsAbstract);
foreach (var type in impTypes)
{
var attr = type.GetCustomAttribute<SkeletonConverterImplementationAttribute>();
if (attr is not null)
{
if (ImplementationTypes.ContainsKey(attr.Version))
throw new InvalidOperationException($"Multiple implementations found: {attr.Version}");
ImplementationTypes[attr.Version] = type;
}
}
Program.Logger.Debug("Find SkeletonConverter implementations: [{}]", string.Join(", ", ImplementationTypes.Keys));
ImplementedVersions = ImplementationTypes.Keys;
}
/// <summary>
/// 创建特定版本的 SkeletonConverter
/// </summary>
public static SkeletonConverter New(Version version)
{
if (!ImplementationTypes.TryGetValue(version, out var cvterType))
{
throw new NotImplementedException($"Not implemented version: {version}");
}
return (SkeletonConverter)Activator.CreateInstance(cvterType);
}
public static SkeletonConverter New(SpineVersion version) => New(version, []);
/// <summary>
/// Json 格式控制
@@ -137,7 +90,7 @@ namespace SpineViewer.Spine
/// <summary>
/// 转换到目标版本
/// </summary>
public abstract JsonObject ToVersion(JsonObject root, Version version);
public abstract JsonObject ToVersion(JsonObject root, SpineVersion version);
/// <summary>
/// 二进制骨骼文件读

View File

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

View File

@@ -0,0 +1,90 @@
using FFMpegCore;
using SpineViewer.Utils;
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace SpineViewer.Spine.SpineExporter
{
/// <summary>
/// MP4 导出参数
/// </summary>
public class AvifExporter : FFmpegVideoExporter
{
public AvifExporter()
{
FPS = 24;
}
public override string Format => "avif";
public override string Suffix => ".avif";
/// <summary>
/// 编码器
/// </summary>
public string Codec { get; set; } = "av1_nvenc";
/// <summary>
/// CRF
/// </summary>
public int CRF { get => crf; set => crf = Math.Clamp(value, 0, 63); }
private int crf = 23;
/// <summary>
/// 像素格式
/// </summary>
public string PixelFormat { get; set; } = "yuv420p";
/// <summary>
/// 循环次数, 0 无限循环, 取值范围 [0, 65535]
/// </summary>
public int Loop { get => loop; set => loop = Math.Clamp(value, 0, 65535); }
private int loop = 0;
public override string FileNameNoteSuffix => $"{Codec}_{CRF}_{PixelFormat}";
public override void SetOutputOptions(FFMpegArgumentOptions options)
{
base.SetOutputOptions(options);
options.WithVideoCodec(Codec).ForcePixelFormat(PixelFormat).WithConstantRateFactor(CRF).WithCustomArgument($"-loop {Loop}");
}
}
public class AvifExporterProperty(AvifExporter exporter) : FFmpegVideoExporterProperty(exporter)
{
[Browsable(false)]
public override AvifExporter Exporter => (AvifExporter)base.Exporter;
/// <summary>
/// 编码器
/// </summary>
[StringEnumConverter.StandardValues("av1_nvenc", "av1_amf", "libaom-av1", Customizable = true)]
[TypeConverter(typeof(StringEnumConverter))]
[Category("[3] "), DisplayName(""), Description("-c:v, 使\n建议使用硬件加速, libaom-av1 ")]
public string Codec { get => Exporter.Codec; set => Exporter.Codec = value; }
/// <summary>
/// CRF
/// </summary>
[Category("[3] "), DisplayName("CRF"), Description("-crf, 0-63, 18-28, 23, ")]
public int CRF { get => Exporter.CRF; set => Exporter.CRF = value; }
/// <summary>
/// 像素格式
/// </summary>
[StringEnumConverter.StandardValues("yuv420p", "yuv422p", "yuv444p", Customizable = true)]
[TypeConverter(typeof(StringEnumConverter))]
[Category("[3] "), DisplayName(""), Description("-pix_fmt, 使")]
public string PixelFormat { get => Exporter.PixelFormat; set => Exporter.PixelFormat = value; }
/// <summary>
/// 循环次数
/// </summary>
[Category("[3] "), DisplayName(""), Description("-loop, , 0 , [0, 65535]")]
public int Loop { get => Exporter.Loop; set => Exporter.Loop = value; }
}
}

View File

@@ -0,0 +1,60 @@
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace SpineViewer.Spine.SpineExporter
{
/// <summary>
/// FFmpeg 自定义视频导出参数
/// </summary>
public class CustomExporter : FFmpegVideoExporter
{
public CustomExporter()
{
CustomArgument = "-c:v libx264 -crf 23 -pix_fmt yuv420p"; // 提供一个示例参数
}
public override string Format => CustomFormat;
public override string Suffix => CustomSuffix;
public override string FileNameNoteSuffix => string.Empty;
/// <summary>
/// 文件格式
/// </summary>
public string CustomFormat { get; set; } = "mp4";
/// <summary>
/// 文件名后缀
/// </summary>
public string CustomSuffix { get; set; } = ".mp4";
}
public class CustomExporterProperty(CustomExporter exporter) : FFmpegVideoExporterProperty(exporter)
{
[Browsable(false)]
public override CustomExporter Exporter => (CustomExporter)base.Exporter;
[Browsable(false)]
public override string Format => Exporter.Format;
[Browsable(false)]
public override string Suffix => Exporter.Suffix;
/// <summary>
/// 文件格式
/// </summary>
[Category("[2] FFmpeg "), DisplayName(""), Description("-f, ")]
public string CustomFormat { get => Exporter.CustomFormat; set => Exporter.CustomFormat = value; }
/// <summary>
/// 文件名后缀
/// </summary>
[Category("[2] FFmpeg "), DisplayName(""), Description("")]
public string CustomSuffix { get => Exporter.CustomSuffix; set => Exporter.CustomSuffix = value; }
}
}

View File

@@ -1,16 +1,15 @@
using FFMpegCore.Pipes;
using SpineViewer.Extensions;
using System;
using System.Collections.Generic;
using System.Drawing.Imaging;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using static System.Windows.Forms.VisualStyles.VisualStyleElement.StartPanel;
namespace SpineViewer
namespace SpineViewer.Spine.SpineExporter
{
/// <summary>
/// SFML.Graphics.Image 帧对象包装类
/// SFML.Graphics.Image 帧对象包装类, 将接管给定的 image 对象生命周期
/// </summary>
public class SFMLImageVideoFrame(SFML.Graphics.Image image) : IVideoFrame, IDisposable
{
@@ -37,6 +36,11 @@ namespace SpineViewer
/// <param name="format">Encoding format to use</param>
/// <returns>True if saving was successful</returns>
public bool SaveToMemory(out byte[] output, string format) => image.SaveToMemory(out output, format);
/// <summary>
/// 获取 Winforms Bitmap 对象
/// </summary>
public Bitmap CopyToBitmap() => image.CopyToBitmap();
}
/// <summary>
@@ -44,20 +48,13 @@ namespace SpineViewer
/// </summary>
public static class ExportHelper
{
public static Bitmap CopyToBitmap(this SFML.Graphics.Texture tex)
{
using var img = tex.CopyToImage();
img.SaveToMemory(out var imgBuffer, "bmp");
using var stream = new MemoryStream(imgBuffer);
return new Bitmap(stream);
}
public static SFMLImageVideoFrame CopyToFrame(this SFML.Graphics.Texture tex) => new(tex.CopyToImage());
/// <summary>
/// 根据 Bitmap 文件格式获取合适的文件后缀
/// </summary>
public static string GetSuffix(this ImageFormat imageFormat)
{
if (imageFormat == ImageFormat.Icon) return ".ico";
else if (imageFormat == ImageFormat.Exif) return ".jpg";
else if (imageFormat == ImageFormat.Exif) return ".jpeg";
else return $".{imageFormat.ToString().ToLower()}";
}
}

View File

@@ -0,0 +1,410 @@
using NLog;
using SpineViewer.Extensions;
using SpineViewer.Utils;
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Drawing.Design;
using System.Linq;
using System.Reflection;
using System.Text;
using System.Threading.Tasks;
namespace SpineViewer.Spine.SpineExporter
{
/// <summary>
/// 导出器基类
/// </summary>
public abstract class Exporter : IDisposable
{
/// <summary>
/// 日志器
/// </summary>
protected readonly Logger logger = LogManager.GetCurrentClassLogger();
/// <summary>
/// 可用于文件名的时间戳字符串
/// </summary>
protected string timestamp = DateTime.Now.ToString("yyMMddHHmmss");
/// <summary>
/// 非自动分辨率下导出视区缓存
/// </summary>
private SFML.Graphics.View? exportViewCache = null;
/// <summary>
/// 模型分辨率缓存
/// </summary>
private readonly Dictionary<string, Size> spineResolutionCache = [];
/// <summary>
/// 自动分辨率下每个模型的导出视区缓存
/// </summary>
private readonly Dictionary<string, SFML.Graphics.View> spineViewCache = [];
~Exporter() { Dispose(false); }
public void Dispose() { Dispose(true); GC.SuppressFinalize(this); }
protected virtual void Dispose(bool disposing) { PreviewerView.Dispose(); }
/// <summary>
/// 输出文件夹
/// </summary>
public string? OutputDir { get; set; } = null;
/// <summary>
/// 导出单个
/// </summary>
public bool IsExportSingle { get; set; } = false;
/// <summary>
/// 画面分辨率
/// </summary>
public Size Resolution
{
get => resolution;
set
{
if (value.Width <= 0) value.Width = 100;
if (value.Height <= 0) value.Height = 100;
resolution = value;
exportResolution = new(value.Width + Margin.Horizontal, value.Height + Margin.Vertical);
}
}
private Size resolution = new(100, 100);
/// <summary>
/// 包含边缘的分辨率
/// </summary>
private Size exportResolution = new(100, 100);
/// <summary>
/// 预览画面的视区
/// </summary>
public SFML.Graphics.View PreviewerView { get => previewerView; set { previewerView.Dispose(); previewerView = new(value); } }
private SFML.Graphics.View previewerView = new();
/// <summary>
/// 是否仅渲染选中
/// </summary>
public bool RenderSelectedOnly { get; set; } = false;
/// <summary>
/// 背景颜色
/// </summary>
public SFML.Graphics.Color BackgroundColor
{
get => backgroundColor;
set
{
backgroundColor = value;
var bcPma = value;
var a = bcPma.A / 255f;
bcPma.R = (byte)(bcPma.R * a);
bcPma.G = (byte)(bcPma.G * a);
bcPma.B = (byte)(bcPma.B * a);
backgroundColorPma = bcPma;
}
}
private SFML.Graphics.Color backgroundColor = SFML.Graphics.Color.Transparent;
/// <summary>
/// 预乘后的背景颜色
/// </summary>
private SFML.Graphics.Color backgroundColorPma = SFML.Graphics.Color.Transparent;
/// <summary>
/// 四周边缘距离, 单位为像素
/// </summary>
public Padding Margin
{
get => margin;
set
{
if (value.Left < 0) value.Left = 0;
if (value.Right < 0) value.Right = 0;
if (value.Top < 0) value.Top = 0;
if (value.Bottom < 0) value.Bottom = 0;
margin = value;
exportResolution = new(Resolution.Width + value.Horizontal, Resolution.Height + value.Vertical);
}
}
private Padding margin = new(0);
/// <summary>
/// 四周填充距离, 单位为像素, 自动分辨率下忽略该值
/// </summary>
public Padding Padding
{
get => padding;
set
{
if (value.Left < 0) value.Left = 0;
if (value.Right < 0) value.Right = 0;
if (value.Top < 0) value.Top = 0;
if (value.Bottom < 0) value.Bottom = 0;
padding = value;
}
}
private Padding padding = new(0);
/// <summary>
/// 在使用预览画面分辨率的情况下, 允许内容溢出到边缘和填充区域, 自动分辨率下忽略该值
/// </summary>
public bool AllowContentOverflow { get; set; } = false;
/// <summary>
/// 自动分辨率, 将会忽略预览画面的分辨率和预览画面视区, 使用模型自身的包围盒, 四周填充和内容溢出会被忽略
/// </summary>
public bool AutoResolution { get; set; } = false;
/// <summary>
/// 获取导出渲染对象, 如果提供了模型列表则分辨率为模型大小, 否则是预览画面大小
/// </summary>
private SFML.Graphics.RenderTexture GetRenderTexture(SpineObject[]? spinesToRender = null)
{
uint width;
uint height;
SFML.Graphics.View view;
if (spinesToRender is null)
{
if (exportViewCache is null)
{
// 记录缓存
exportViewCache = new SFML.Graphics.View(PreviewerView);
if (AllowContentOverflow)
{
var canvasBounds = exportViewCache.GetBounds().GetCanvasBounds(Resolution, Margin, Padding);
exportViewCache.Center = new(canvasBounds.X + canvasBounds.Width / 2, canvasBounds.Y + canvasBounds.Height / 2);
exportViewCache.Size = new(canvasBounds.Width, canvasBounds.Height);
}
else
{
exportViewCache.SetViewport(Resolution, Margin, Padding);
}
}
width = (uint)exportResolution.Width;
height = (uint)exportResolution.Height;
view = exportViewCache;
}
else
{
var cacheKey = string.Join("|", spinesToRender.Select(v => v.ID));
// 记录缓存
if (!spineViewCache.TryGetValue(cacheKey, out var spineView))
{
var spineBounds = spinesToRender[0].GetBounds();
foreach (var sp in spinesToRender.Skip(1))
spineBounds = spineBounds.Union(sp.GetBounds());
var spineResolution = new Size((int)Math.Ceiling(spineBounds.Width), (int)Math.Ceiling(spineBounds.Height));
var canvasBounds = spineBounds.GetCanvasBounds(spineResolution, Margin); // 忽略填充
spineResolutionCache[cacheKey] = new(spineResolution.Width + Margin.Horizontal, spineResolution.Height + Margin.Vertical);
spineViewCache[cacheKey] = spineView = new SFML.Graphics.View(
new(canvasBounds.X + canvasBounds.Width / 2, canvasBounds.Y + canvasBounds.Height / 2),
new(canvasBounds.Width, -canvasBounds.Height)
);
logger.Info("Auto resolusion: ({}, {})", spineResolution.Width, spineResolution.Height);
}
width = (uint)spineResolutionCache[cacheKey].Width;
height = (uint)spineResolutionCache[cacheKey].Height;
view = spineViewCache[cacheKey];
}
var tex = new SFML.Graphics.RenderTexture(width, height);
tex.SetView(view);
return tex;
}
/// <summary>
/// 获取单个模型的单帧画面
/// </summary>
protected SFMLImageVideoFrame GetFrame(SpineObject spine) => GetFrame([spine]);
/// <summary>
/// 获取模型列表的单帧画面
/// </summary>
protected SFMLImageVideoFrame GetFrame(SpineObject[] spinesToRender)
{
// RenderTexture 必须临时创建, 随用随取, 防止出现跨线程的情况
using var texPma = GetRenderTexture(AutoResolution ? spinesToRender : null);
// 先将预乘结果准确绘制出来, 注意背景色也应当是预乘的
texPma.Clear(backgroundColorPma);
foreach (var spine in spinesToRender) texPma.Draw(spine);
texPma.Display();
// 背景色透明度不为 1 时需要处理反预乘, 否则直接就是结果
if (BackgroundColor.A < 255)
{
// 从预乘结果构造渲染对象, 并正确设置变换
using var view = texPma.GetView();
using var img = texPma.Texture.CopyToImage();
using var texSprite = new SFML.Graphics.Texture(img);
using var sp = new SFML.Graphics.Sprite(texSprite)
{
Origin = new(texPma.Size.X / 2f, texPma.Size.Y / 2f),
Position = new(view.Center.X, view.Center.Y),
Scale = new(view.Size.X / texPma.Size.X, view.Size.Y / texPma.Size.Y),
Rotation = view.Rotation
};
// 混合模式用直接覆盖的方式, 保证得到的图像区域是反预乘的颜色和透明度, 同时使用反预乘着色器
var st = SFML.Graphics.RenderStates.Default;
st.BlendMode = SFMLBlendMode.SourceOnly;
st.Shader = SFMLShader.InversePma;
// 在最终结果上二次渲染非预乘画面
using var tex = GetRenderTexture(AutoResolution ? spinesToRender : null);
// 将非预乘结果覆盖式绘制在目标对象上, 注意背景色应该用非预乘的
tex.Clear(BackgroundColor);
tex.Draw(sp, st);
tex.Display();
return new(tex.Texture.CopyToImage());
}
else
{
return new(texPma.Texture.CopyToImage());
}
}
/// <summary>
/// 每个模型在同一个画面进行导出
/// </summary>
protected abstract void ExportSingle(SpineObject[] spinesToRender, BackgroundWorker? worker = null);
/// <summary>
/// 每个模型独立导出
/// </summary>
protected abstract void ExportIndividual(SpineObject[] spinesToRender, BackgroundWorker? worker = null);
/// <summary>
/// 检查参数是否合法并规范化参数值, 否则返回用户错误原因
/// </summary>
public virtual string? Validate()
{
if (!string.IsNullOrWhiteSpace(OutputDir) && File.Exists(OutputDir))
return "输出文件夹无效";
if (!string.IsNullOrWhiteSpace(OutputDir) && !Directory.Exists(OutputDir))
return $"文件夹 {OutputDir} 不存在";
if (IsExportSingle && string.IsNullOrWhiteSpace(OutputDir))
return "导出单个时必须提供输出文件夹";
OutputDir = string.IsNullOrWhiteSpace(OutputDir) ? null : Path.GetFullPath(OutputDir);
return null;
}
private void ClearCache()
{
exportViewCache?.Dispose();
exportViewCache = null;
spineResolutionCache.Clear();
foreach (var v in spineViewCache.Values) v.Dispose();
spineViewCache.Clear();
}
/// <summary>
/// 执行导出
/// </summary>
/// <param name="spines">要进行导出的 Spine 列表</param>
/// <param name="worker">用来执行该函数的 worker</param>
/// <exception cref="ArgumentException"></exception>
public virtual void Export(SpineObject[] spines, BackgroundWorker? worker = null)
{
if (Validate() is string err) throw new ArgumentException(err);
var spinesToRender = spines.Where(sp => !RenderSelectedOnly || sp.IsSelected).Reverse().ToArray();
if (spinesToRender.Length > 0)
{
ClearCache();
timestamp = DateTime.Now.ToString("yyMMddHHmmss"); // 刷新时间戳
if (IsExportSingle) ExportSingle(spinesToRender, worker);
else ExportIndividual(spinesToRender, worker);
ClearCache();
}
logger.LogCurrentProcessMemoryUsage();
}
}
/// <summary>
/// 用于在 PropertyGrid 上提供用户操作接口的包装类
/// </summary>
public class ExporterProperty(Exporter exporter)
{
[Browsable(false)]
public virtual Exporter Exporter { get; } = exporter;
/// <summary>
/// 输出文件夹
/// </summary>
[Editor(typeof(FolderNameEditor), typeof(UITypeEditor))]
[Category("[0] "), DisplayName(""), Description("")]
public string? OutputDir { get => Exporter.OutputDir; set => Exporter.OutputDir = value; }
/// <summary>
/// 导出单个
/// </summary>
[Category("[0] "), DisplayName(""), Description("")]
public bool IsExportSingle { get => Exporter.IsExportSingle; set => Exporter.IsExportSingle = value; }
/// <summary>
/// 画面分辨率
/// </summary>
[TypeConverter(typeof(SizeConverter))]
[Category("[0] "), DisplayName(""), Description("")]
public Size Resolution { get => Exporter.Resolution; }
/// <summary>
/// 预览画面视区
/// </summary>
[Category("[0] "), DisplayName(""), Description("")]
public SFML.Graphics.View View { get => Exporter.PreviewerView; }
/// <summary>
/// 是否仅渲染选中
/// </summary>
[Category("[0] "), DisplayName(""), Description("")]
public bool RenderSelectedOnly { get => Exporter.RenderSelectedOnly; }
/// <summary>
/// 背景颜色
/// </summary>
[Editor(typeof(SFMLColorEditor), typeof(UITypeEditor))]
[TypeConverter(typeof(SFMLColorConverter))]
[Category("[0] "), DisplayName(""), Description("使, #RRGGBBAA")]
public SFML.Graphics.Color BackgroundColor { get => Exporter.BackgroundColor; set => Exporter.BackgroundColor = value; }
/// <summary>
/// 四周边缘距离
/// </summary>
[TypeConverter(typeof(PaddingConverter))]
[Category("[0] "), DisplayName(""), Description(" (Margin), ")]
public Padding Margin { get => Exporter.Margin; set => Exporter.Margin = value; }
/// <summary>
/// 四周填充距离
/// </summary>
[TypeConverter(typeof(PaddingConverter))]
[Category("[0] "), DisplayName(""), Description(" (Padding), , ")]
public Padding Padding { get => Exporter.Padding; set => Exporter.Padding = value; }
/// <summary>
/// 允许内容溢出到边缘和填充区域
/// </summary>
[Category("[0] "), DisplayName(""), Description("使, ")]
public bool AllowContentOverflow { get => Exporter.AllowContentOverflow; set => Exporter.AllowContentOverflow = value; }
/// <summary>
/// 自动分辨率
/// </summary>
[Category("[0] "), DisplayName(""), Description(", ")]
public bool AutoResolution { get => Exporter.AutoResolution; set => Exporter.AutoResolution = value; }
}
}

View File

@@ -0,0 +1,135 @@
using FFMpegCore.Pipes;
using FFMpegCore;
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Diagnostics;
namespace SpineViewer.Spine.SpineExporter
{
/// <summary>
/// 使用 FFmpeg 的视频导出器
/// </summary>
public abstract class FFmpegVideoExporter : VideoExporter
{
/// <summary>
/// 文件格式
/// </summary>
public abstract string Format { get; }
/// <summary>
/// 文件名后缀
/// </summary>
public abstract string Suffix { get; }
/// <summary>
/// 文件名后缀
/// </summary>
public string CustomArgument { get; set; }
/// <summary>
/// 要追加在文件名末尾的信息字串, 首尾不需要提供额外分隔符
/// </summary>
public abstract string FileNameNoteSuffix { get; }
/// <summary>
/// 获取输出附加选项
/// </summary>
public virtual void SetOutputOptions(FFMpegArgumentOptions options) => options.ForceFormat(Format).WithCustomArgument(CustomArgument);
public override string? Validate()
{
if (base.Validate() is string error)
return error;
if (string.IsNullOrWhiteSpace(Format))
return "需要提供有效的格式";
if (string.IsNullOrWhiteSpace(Suffix))
return "需要提供有效的文件名后缀";
return null;
}
protected override void ExportSingle(SpineObject[] spinesToRender, BackgroundWorker? worker = null)
{
var noteSuffix = FileNameNoteSuffix;
if (!string.IsNullOrWhiteSpace(noteSuffix)) noteSuffix = $"_{noteSuffix}";
var filename = $"ffmpeg_{timestamp}_{FPS:f0}{noteSuffix}{Suffix}";
// 导出单个时必定提供输出文件夹
var savePath = Path.Combine(OutputDir, filename);
var videoFramesSource = new RawVideoPipeSource(GetFrames(spinesToRender, worker)) { FrameRate = FPS };
try
{
var ffmpegArgs = FFMpegArguments.FromPipeInput(videoFramesSource).OutputToFile(savePath, true, SetOutputOptions);
logger.Info("FFmpeg arguments: {}", ffmpegArgs.Arguments);
ffmpegArgs.ProcessSynchronously();
}
catch (Exception ex)
{
logger.Error(ex.ToString());
logger.Error("Failed to export {} {}", Format, savePath);
}
}
protected override void ExportIndividual(SpineObject[] spinesToRender, BackgroundWorker? worker = null)
{
var noteSuffix = FileNameNoteSuffix;
if (!string.IsNullOrWhiteSpace(noteSuffix)) noteSuffix = $"_{noteSuffix}";
foreach (var spine in spinesToRender)
{
if (worker?.CancellationPending == true) break; // 取消的日志在 GetFrames 里输出
var filename = $"{spine.Name}_{timestamp}_{FPS:f0}{noteSuffix}{Suffix}";
// 如果提供了输出文件夹, 则全部导出到输出文件夹, 否则导出到各自的文件夹下
var savePath = Path.Combine(OutputDir ?? spine.AssetsDir, filename);
var videoFramesSource = new RawVideoPipeSource(GetFrames(spine, worker)) { FrameRate = FPS };
try
{
var ffmpegArgs = FFMpegArguments
.FromPipeInput(videoFramesSource)
.OutputToFile(savePath, true, SetOutputOptions);
logger.Info("FFmpeg arguments: {}", ffmpegArgs.Arguments);
ffmpegArgs.ProcessSynchronously();
}
catch (Exception ex)
{
logger.Error(ex.ToString());
logger.Error("Failed to export {} {} {}", Format, savePath, spine.SkelPath);
}
}
}
}
public class FFmpegVideoExporterProperty(FFmpegVideoExporter exporter) : VideoExporterProperty(exporter)
{
[Browsable(false)]
public override FFmpegVideoExporter Exporter => (FFmpegVideoExporter)base.Exporter;
/// <summary>
/// 文件格式
/// </summary>
[Category("[2] FFmpeg "), DisplayName(""), Description("-f, ")]
public virtual string Format => Exporter.Format;
/// <summary>
/// 文件名后缀
/// </summary>
[Category("[2] FFmpeg "), DisplayName(""), Description("")]
public virtual string Suffix => Exporter.Suffix;
/// <summary>
/// 文件名后缀
/// </summary>
[Category("[2] FFmpeg "), DisplayName(""), Description("使 \"ffmpeg -h encoder=<编码器>\" 查看编码器支持的参数\n使用 \"ffmpeg -h muxer=<文件格式>\" 查看文件格式支持的参数")]
public string CustomArgument { get => Exporter.CustomArgument; set => Exporter.CustomArgument = value; }
}
}

View File

@@ -0,0 +1,133 @@
using SpineViewer.Spine;
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Drawing.Imaging;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace SpineViewer.Spine.SpineExporter
{
/// <summary>
/// 单帧画面导出器
/// </summary>
public class FrameExporter : Exporter
{
/// <summary>
/// 单帧画面格式
/// </summary>
public ImageFormat ImageFormat
{
get => imageFormat;
set
{
if (value == ImageFormat.MemoryBmp) value = ImageFormat.Bmp;
imageFormat = value;
}
}
private ImageFormat imageFormat = ImageFormat.Png;
/// <summary>
/// DPI
/// </summary>
public SizeF DPI
{
get => dpi;
set
{
if (value.Width <= 0) value.Width = 144;
if (value.Height <= 0) value.Height = 144;
dpi = value;
}
}
private SizeF dpi = new(144, 144);
protected override void ExportSingle(SpineObject[] spinesToRender, BackgroundWorker? worker = null)
{
// 导出单个时必定提供输出文件夹
var filename = $"frame_{timestamp}{ImageFormat.GetSuffix()}";
var savePath = Path.Combine(OutputDir, filename);
worker?.ReportProgress(0, $"已处理 0/1");
try
{
using var frame = GetFrame(spinesToRender);
using var img = frame.CopyToBitmap();
img.SetResolution(DPI.Width, DPI.Height);
img.Save(savePath, ImageFormat);
}
catch (Exception ex)
{
logger.Error(ex.ToString());
logger.Error("Failed to save single frame");
}
worker?.ReportProgress(100, $"已处理 1/1");
}
protected override void ExportIndividual(SpineObject[] spinesToRender, BackgroundWorker? worker = null)
{
int total = spinesToRender.Length;
int success = 0;
int error = 0;
worker?.ReportProgress(0, $"已处理 0/{total}");
for (int i = 0; i < total; i++)
{
var spine = spinesToRender[i];
// 逐个导出时如果提供了输出文件夹, 则全部导出到输出文件夹, 否则输出到各自的文件夹
var filename = $"{spine.Name}_{timestamp}{ImageFormat.GetSuffix()}";
var savePath = Path.Combine(OutputDir ?? spine.AssetsDir, filename);
try
{
using var frame = GetFrame(spine);
using var img = frame.CopyToBitmap();
img.SetResolution(DPI.Width, DPI.Height);
img.Save(savePath, ImageFormat);
success++;
}
catch (Exception ex)
{
logger.Error(ex.ToString());
logger.Error("Failed to save single frame {} {}", savePath, spine.SkelPath);
error++;
}
worker?.ReportProgress((int)((i + 1) * 100.0) / total, $"已处理 {i + 1}/{total}");
}
if (error > 0)
logger.Warn("Frames save {} successfully, {} failed", success, error);
else
logger.Info("{} frames saved successfully", success);
}
}
public class FrameExporterProperty(FrameExporter exporter) : ExporterProperty(exporter)
{
[Browsable(false)]
public override FrameExporter Exporter => (FrameExporter)base.Exporter;
/// <summary>
/// 单帧画面格式
/// </summary>
[TypeConverter(typeof(ImageFormatConverter))]
[Category("[1] "), DisplayName("")]
public ImageFormat ImageFormat { get => Exporter.ImageFormat; set => Exporter.ImageFormat = value; }
/// <summary>
/// 文件名后缀
/// </summary>
[Category("[1] "), DisplayName(""), Description("")]
public string Suffix { get => Exporter.ImageFormat.GetSuffix(); }
/// <summary>
/// DPI
/// </summary>
[TypeConverter(typeof(SizeFConverter))]
[Category("[1] "), DisplayName("DPI"), Description("")]
public SizeF DPI { get => Exporter.DPI; set => Exporter.DPI = value; }
}
}

View File

@@ -0,0 +1,99 @@
using SpineViewer.Spine;
using SpineViewer.Utils;
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace SpineViewer.Spine.SpineExporter
{
/// <summary>
/// 帧序列导出器
/// </summary>
public class FrameSequenceExporter : VideoExporter
{
/// <summary>
/// 文件名后缀, 同时决定帧图像格式, 支持的格式为 <c>".png", ".jpg", ".tga", ".bmp"</c>
/// </summary>
public string Suffix { get; set; } = ".png";
protected override void ExportSingle(SpineObject[] spinesToRender, BackgroundWorker? worker = null)
{
// 导出单个时必定提供输出文件夹,
var saveDir = Path.Combine(OutputDir, $"frames_{timestamp}_{FPS:f0}");
Directory.CreateDirectory(saveDir);
int frameIdx = 0;
foreach (var frame in GetFrames(spinesToRender, worker))
{
var filename = $"frames_{timestamp}_{FPS:f0}_{frameIdx:d6}{Suffix}";
var savePath = Path.Combine(saveDir, filename);
try
{
frame.SaveToFile(savePath);
}
catch (Exception ex)
{
logger.Error(ex.ToString());
logger.Error("Failed to save frame {}", savePath);
}
finally
{
frame.Dispose();
}
frameIdx++;
}
}
protected override void ExportIndividual(SpineObject[] spinesToRender, BackgroundWorker? worker = null)
{
foreach (var spine in spinesToRender)
{
if (worker?.CancellationPending == true) break; // 取消的日志在 GetFrames 里输出
// 如果提供了输出文件夹, 则全部导出到输出文件夹, 否则导出到各自的文件夹下
var subDir = $"{spine.Name}_{timestamp}_{FPS:f0}";
var saveDir = Path.Combine(OutputDir ?? spine.AssetsDir, subDir);
Directory.CreateDirectory(saveDir);
int frameIdx = 0;
foreach (var frame in GetFrames(spine, worker))
{
var filename = $"{spine.Name}_{timestamp}_{FPS:f0}_{frameIdx:d6}{Suffix}";
var savePath = Path.Combine(saveDir, filename);
try
{
frame.SaveToFile(savePath);
}
catch (Exception ex)
{
logger.Error(ex.ToString());
logger.Error("Failed to save frame {} {}", savePath, spine.SkelPath);
}
finally
{
frame.Dispose();
}
frameIdx++;
}
}
}
}
public class FrameSequenceExporterProperty(VideoExporter exporter) : VideoExporterProperty(exporter)
{
[Browsable(false)]
public override FrameSequenceExporter Exporter => (FrameSequenceExporter)base.Exporter;
/// <summary>
/// 文件名后缀
/// </summary>
[TypeConverter(typeof(StringEnumConverter)), StringEnumConverter.StandardValues(".png", ".jpg", ".tga", ".bmp")]
[Category("[2] "), DisplayName(""), Description("")]
public string Suffix { get => Exporter.Suffix; set => Exporter.Suffix = value; }
}
}

View File

@@ -0,0 +1,79 @@
using FFMpegCore;
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace SpineViewer.Spine.SpineExporter
{
/// <summary>
/// GIF 导出参数
/// </summary>
public class GifExporter : FFmpegVideoExporter
{
public GifExporter()
{
FPS = 24;
}
public override string Format => "gif";
public override string Suffix => ".gif";
/// <summary>
/// 调色板最大颜色数量
/// </summary>
public uint MaxColors { get => maxColors; set => maxColors = Math.Clamp(value, 2, 256); }
private uint maxColors = 256;
/// <summary>
/// 透明度阈值
/// </summary>
public byte AlphaThreshold { get => alphaThreshold; set => alphaThreshold = value; }
private byte alphaThreshold = 128;
/// <summary>
/// 循环次数, -1 不循环, 0 无限循环, 取值范围 [-1, 65535]
/// </summary>
public int Loop { get => loop; set => loop = Math.Clamp(value, -1, 65535); }
private int loop = 0;
public override string FileNameNoteSuffix => $"{MaxColors}_{AlphaThreshold}";
public override void SetOutputOptions(FFMpegArgumentOptions options)
{
base.SetOutputOptions(options);
var v = $"[0:v] split [s0][s1]";
var s0 = $"[s0] palettegen=reserve_transparent=1:max_colors={MaxColors} [p]";
var s1 = $"[s1][p] paletteuse=dither=bayer:alpha_threshold={AlphaThreshold}";
var customArgs = $"-filter_complex \"{v};{s0};{s1}\" -loop {Loop}";
options.WithCustomArgument(customArgs);
}
}
class GifExporterProperty(FFmpegVideoExporter exporter) : FFmpegVideoExporterProperty(exporter)
{
[Browsable(false)]
public override GifExporter Exporter => (GifExporter)base.Exporter;
/// <summary>
/// 调色板最大颜色数量
/// </summary>
[Category("[3] "), DisplayName(""), Description("使, ")]
public uint MaxColors { get => Exporter.MaxColors; set => Exporter.MaxColors = value; }
/// <summary>
/// 透明度阈值
/// </summary>
[Category("[3] "), DisplayName(""), Description("")]
public byte AlphaThreshold { get => Exporter.AlphaThreshold; set => Exporter.AlphaThreshold = value; }
/// <summary>
/// 透明度阈值
/// </summary>
[Category("[3] "), DisplayName(""), Description("-loop, , -1 , 0 , [-1, 65535]")]
public int Loop { get => Exporter.Loop; set => Exporter.Loop = value; }
}
}

View File

@@ -0,0 +1,78 @@
using FFMpegCore;
using SpineViewer.Utils;
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace SpineViewer.Spine.SpineExporter
{
/// <summary>
/// MKV 导出参数
/// </summary>
public class MkvExporter : FFmpegVideoExporter
{
public MkvExporter()
{
BackgroundColor = new(0, 255, 0);
}
public override string Format => "matroska";
public override string Suffix => ".mkv";
/// <summary>
/// 编码器
/// </summary>
public string Codec { get; set; } = "libx265";
/// <summary>
/// CRF
/// </summary>
public int CRF { get => crf; set => crf = Math.Clamp(value, 0, 63); }
private int crf = 23;
/// <summary>
/// 像素格式
/// </summary>
public string PixelFormat { get; set; } = "yuv444p";
public override string FileNameNoteSuffix => $"{Codec}_{CRF}_{PixelFormat}";
public override void SetOutputOptions(FFMpegArgumentOptions options)
{
base.SetOutputOptions(options);
options.WithVideoCodec(Codec).WithConstantRateFactor(CRF).ForcePixelFormat(PixelFormat);
}
}
public class MkvExporterProperty(FFmpegVideoExporter exporter) : FFmpegVideoExporterProperty(exporter)
{
[Browsable(false)]
public override MkvExporter Exporter => (MkvExporter)base.Exporter;
/// <summary>
/// 编码器
/// </summary>
[StringEnumConverter.StandardValues("libx264", "libx265", "libvpx-vp9", "av1_nvenc", Customizable = true)]
[TypeConverter(typeof(StringEnumConverter))]
[Category("[3] "), DisplayName(""), Description("-c:v, 使")]
public string Codec { get => Exporter.Codec; set => Exporter.Codec = value; }
/// <summary>
/// CRF
/// </summary>
[Category("[3] "), DisplayName("CRF"), Description("-crf, 0-63, 18-28, 23, ")]
public int CRF { get => Exporter.CRF; set => Exporter.CRF = value; }
/// <summary>
/// 像素格式
/// </summary>
[StringEnumConverter.StandardValues("yuv420p", "yuv422p", "yuv444p", "yuva420p", Customizable = true)]
[TypeConverter(typeof(StringEnumConverter))]
[Category("[3] "), DisplayName(""), Description("-pix_fmt, 使")]
public string PixelFormat { get => Exporter.PixelFormat; set => Exporter.PixelFormat = value; }
}
}

View File

@@ -0,0 +1,79 @@
using FFMpegCore;
using SpineViewer.Utils;
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace SpineViewer.Spine.SpineExporter
{
/// <summary>
/// MOV 导出参数
/// </summary>
public class MovExporter : FFmpegVideoExporter
{
public MovExporter()
{
BackgroundColor = new(0, 255, 0);
}
public override string Format => "mov";
public override string Suffix => ".mov";
/// <summary>
/// 编码器
/// </summary>
public string Codec { get; set; } = "prores_ks";
/// <summary>
/// 预设
/// </summary>
public string Profile { get; set; } = "auto";
/// <summary>
/// 像素格式
/// </summary>
public string PixelFormat { get; set; } = "yuva444p10le";
public override string FileNameNoteSuffix => $"{Codec}_{Profile}_{PixelFormat}";
public override void SetOutputOptions(FFMpegArgumentOptions options)
{
base.SetOutputOptions(options);
options.WithFastStart().WithVideoCodec(Codec).WithCustomArgument($"-profile {Profile}").ForcePixelFormat(PixelFormat);
}
}
public class MovExporterProperty(FFmpegVideoExporter exporter) : FFmpegVideoExporterProperty(exporter)
{
[Browsable(false)]
public override MovExporter Exporter => (MovExporter)base.Exporter;
/// <summary>
/// 编码器
/// </summary>
[StringEnumConverter.StandardValues("prores_ks", Customizable = true)]
[TypeConverter(typeof(StringEnumConverter))]
[Category("[3] "), DisplayName(""), Description("-c:v, 使")]
public string Codec { get => Exporter.Codec; set => Exporter.Codec = value; }
/// <summary>
/// 预设
/// </summary>
[StringEnumConverter.StandardValues("auto", "proxy", "lt", "standard", "hq", "4444", "4444xq")]
[TypeConverter(typeof(StringEnumConverter))]
[Category("[3] "), DisplayName(""), Description("-profile, ")]
public string Profile { get => Exporter.Profile; set => Exporter.Profile = value; }
/// <summary>
/// 像素格式
/// </summary>
[StringEnumConverter.StandardValues("yuv422p10le", "yuv444p10le", "yuva444p10le", Customizable = true)]
[TypeConverter(typeof(StringEnumConverter))]
[Category("[3] "), DisplayName(""), Description("-pix_fmt, 使")]
public string PixelFormat { get => Exporter.PixelFormat; set => Exporter.PixelFormat = value; }
}
}

View File

@@ -0,0 +1,78 @@
using FFMpegCore;
using SpineViewer.Utils;
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace SpineViewer.Spine.SpineExporter
{
/// <summary>
/// MP4 导出参数
/// </summary>
public class Mp4Exporter : FFmpegVideoExporter
{
public Mp4Exporter()
{
BackgroundColor = new(0, 255, 0);
}
public override string Format => "mp4";
public override string Suffix => ".mp4";
/// <summary>
/// 编码器
/// </summary>
public string Codec { get; set; } = "libx264";
/// <summary>
/// CRF
/// </summary>
public int CRF { get => crf; set => crf = Math.Clamp(value, 0, 63); }
private int crf = 23;
/// <summary>
/// 像素格式
/// </summary>
public string PixelFormat { get; set; } = "yuv444p";
public override string FileNameNoteSuffix => $"{Codec}_{CRF}_{PixelFormat}";
public override void SetOutputOptions(FFMpegArgumentOptions options)
{
base.SetOutputOptions(options);
options.WithFastStart().WithVideoCodec(Codec).WithConstantRateFactor(CRF).ForcePixelFormat(PixelFormat);
}
}
public class Mp4ExporterProperty(FFmpegVideoExporter exporter) : FFmpegVideoExporterProperty(exporter)
{
[Browsable(false)]
public override Mp4Exporter Exporter => (Mp4Exporter)base.Exporter;
/// <summary>
/// 编码器
/// </summary>
[StringEnumConverter.StandardValues("libx264", "libx265", Customizable = true)]
[TypeConverter(typeof(StringEnumConverter))]
[Category("[3] "), DisplayName(""), Description("-c:v, 使")]
public string Codec { get => Exporter.Codec; set => Exporter.Codec = value; }
/// <summary>
/// CRF
/// </summary>
[Category("[3] "), DisplayName("CRF"), Description("-crf, 0-63, 18-28, 23, ")]
public int CRF { get => Exporter.CRF; set => Exporter.CRF = value; }
/// <summary>
/// 像素格式
/// </summary>
[StringEnumConverter.StandardValues("yuv420p", "yuv422p", "yuv444p", Customizable = true)]
[TypeConverter(typeof(StringEnumConverter))]
[Category("[3] "), DisplayName(""), Description("-pix_fmt, 使")]
public string PixelFormat { get => Exporter.PixelFormat; set => Exporter.PixelFormat = value; }
}
}

View File

@@ -0,0 +1,169 @@
using SpineViewer.Spine;
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace SpineViewer.Spine.SpineExporter
{
/// <summary>
/// 视频导出基类
/// </summary>
public abstract class VideoExporter : Exporter
{
/// <summary>
/// 导出时长
/// </summary>
public float Duration { get => duration; set => duration = value < 0 ? -1 : value; }
private float duration = -1;
/// <summary>
/// 帧率
/// </summary>
public float FPS { get; set; } = 60;
/// <summary>
/// 是否保留最后一帧
/// </summary>
public bool KeepLast { get; set; } = true;
public override string? Validate()
{
if (base.Validate() is string error)
return error;
if (IsExportSingle && Duration < 0)
return "导出单个时导出时长不能为负数";
return null;
}
/// <summary>
/// 生成单个模型的帧序列
/// </summary>
protected IEnumerable<SFMLImageVideoFrame> GetFrames(SpineObject spine, BackgroundWorker? worker = null)
{
// 独立导出时如果 Duration 小于 0 则使用所有轨道上动画时长最大值
var duration = Duration;
if (duration < 0) duration = spine.GetTrackIndices().Select(i => spine.GetAnimationDuration(spine.GetAnimation(i))).Max();
float delta = 1f / FPS;
int total = (int)(duration * FPS); // 完整帧的数量
float deltaFinal = duration - delta * total; // 最后一帧时长
int final = KeepLast && deltaFinal > 1e-3 ? 1 : 0;
int frameCount = 1 + total + final; // 所有帧的数量 = 起始帧 + 完整帧 + 最后一帧
worker?.ReportProgress(0, $"{spine.Name} 已处理 0/{frameCount} 帧");
// 导出首帧
var firstFrame = GetFrame(spine);
worker?.ReportProgress(1 * 100 / frameCount, $"{spine.Name} 已处理 1/{frameCount} 帧");
yield return firstFrame;
// 导出完整帧
for (int i = 0; i < total; i++)
{
if (worker?.CancellationPending == true)
{
logger.Info("Export cancelled");
break;
}
spine.Update(delta);
var frame = GetFrame(spine);
worker?.ReportProgress((1 + i + 1) * 100 / frameCount, $"{spine.Name} 已处理 {1 + i + 1}/{frameCount} 帧");
yield return frame;
}
// 导出最后一帧
if (final > 0)
{
spine.Update(deltaFinal);
var finalFrame = GetFrame(spine);
worker?.ReportProgress(100, $"{spine.Name} 已处理 {frameCount}/{frameCount} 帧");
yield return finalFrame;
}
}
/// <summary>
/// 生成多个模型的帧序列
/// </summary>
protected IEnumerable<SFMLImageVideoFrame> GetFrames(SpineObject[] spinesToRender, BackgroundWorker? worker = null)
{
// 导出单个时必须根据 Duration 决定导出时长
var duration = Duration;
float delta = 1f / FPS;
int total = (int)(duration * FPS); // 完整帧的数量
float deltaFinal = duration - delta * total; // 最后一帧时长
int final = KeepLast && deltaFinal > 1e-3 ? 1 : 0;
int frameCount = 1 + total + final; // 所有帧的数量 = 起始帧 + 完整帧 + 最后一帧
worker?.ReportProgress(0, $"已处理 0/{frameCount} 帧");
// 导出首帧
var firstFrame = GetFrame(spinesToRender);
worker?.ReportProgress(1 * 100 / frameCount, $"已处理 1/{frameCount} 帧");
yield return firstFrame;
// 导出完整帧
for (int i = 0; i < total; i++)
{
if (worker?.CancellationPending == true)
{
logger.Info("Export cancelled");
break;
}
foreach (var spine in spinesToRender) spine.Update(delta);
var frame = GetFrame(spinesToRender);
worker?.ReportProgress((1 + i + 1) * 100 / frameCount, $"已处理 {1 + i + 1}/{frameCount} 帧");
yield return frame;
}
// 导出最后一帧
if (final > 0)
{
foreach (var spine in spinesToRender) spine.Update(delta);
var finalFrame = GetFrame(spinesToRender);
worker?.ReportProgress(100, $"已处理 {frameCount}/{frameCount} 帧");
yield return finalFrame;
}
}
public override void Export(SpineObject[] spines, BackgroundWorker? worker = null)
{
// 导出视频格式需要把模型时间都重置到 0
foreach (var spine in spines) spine.ResetAnimationsTime();
base.Export(spines, worker);
}
}
public class VideoExporterProperty(VideoExporter exporter) : ExporterProperty(exporter)
{
[Browsable(false)]
public override VideoExporter Exporter => (VideoExporter)base.Exporter;
/// <summary>
/// 导出时长
/// </summary>
[Category("[1] "), DisplayName(""), Description(", 0, 使")]
public float Duration { get => Exporter.Duration; set => Exporter.Duration = value; }
/// <summary>
/// 帧率
/// </summary>
[Category("[1] "), DisplayName(""), Description("")]
public float FPS { get => Exporter.FPS; set => Exporter.FPS = value; }
/// <summary>
/// 保留最后一帧
/// </summary>
[Category("[1] "), DisplayName(""), Description(", , 1")]
public bool KeepLast { get => Exporter.KeepLast; set => Exporter.KeepLast = value; }
}
}

View File

@@ -0,0 +1,79 @@
using FFMpegCore;
using SpineViewer.Utils;
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace SpineViewer.Spine.SpineExporter
{
/// <summary>
/// WebM 导出参数
/// </summary>
public class WebmExporter : FFmpegVideoExporter
{
public WebmExporter()
{
// 默认用透明黑背景
BackgroundColor = new(0, 0, 0, 0);
}
public override string Format => "webm";
public override string Suffix => ".webm";
/// <summary>
/// 编码器
/// </summary>
public string Codec { get; set; } = "libvpx-vp9";
/// <summary>
/// CRF
/// </summary>
public int CRF { get => crf; set => crf = Math.Clamp(value, 0, 63); }
private int crf = 23;
/// <summary>
/// 像素格式
/// </summary>
public string PixelFormat { get; set; } = "yuva420p";
public override string FileNameNoteSuffix => $"{Codec}_{CRF}_{PixelFormat}";
public override void SetOutputOptions(FFMpegArgumentOptions options)
{
base.SetOutputOptions(options);
options.WithVideoCodec(Codec).WithConstantRateFactor(CRF).ForcePixelFormat(PixelFormat);
}
}
public class WebmExporterProperty(WebmExporter exporter) : FFmpegVideoExporterProperty(exporter)
{
[Browsable(false)]
public override WebmExporter Exporter => (WebmExporter)base.Exporter;
/// <summary>
/// 编码器
/// </summary>
[StringEnumConverter.StandardValues("libvpx-vp9", Customizable = true)]
[TypeConverter(typeof(StringEnumConverter))]
[Category("[3] "), DisplayName(""), Description("-c:v, 使")]
public string Codec { get => Exporter.Codec; set => Exporter.Codec = value; }
/// <summary>
/// CRF
/// </summary>
[Category("[3] "), DisplayName("CRF"), Description("-crf, 0-63, 18-28, 23, ")]
public int CRF { get => Exporter.CRF; set => Exporter.CRF = value; }
/// <summary>
/// 像素格式
/// </summary>
[StringEnumConverter.StandardValues("yuv420p", "yuv422p", "yuv444p", "yuva420p", Customizable = true)]
[TypeConverter(typeof(StringEnumConverter))]
[Category("[3] "), DisplayName(""), Description("-pix_fmt, 使")]
public string PixelFormat { get => Exporter.PixelFormat; set => Exporter.PixelFormat = value; }
}
}

View File

@@ -0,0 +1,101 @@
using FFMpegCore;
using SpineViewer.Utils;
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace SpineViewer.Spine.SpineExporter
{
/// <summary>
/// MP4 导出参数
/// </summary>
public class WebpExporter : FFmpegVideoExporter
{
public WebpExporter()
{
FPS = 24;
}
public override string Format => "webp";
public override string Suffix => ".webp";
/// <summary>
/// 编码器
/// </summary>
public string Codec { get; set; } = "libwebp_anim";
/// <summary>
/// 是否无损
/// </summary>
public bool Lossless { get; set; } = false;
/// <summary>
/// 质量
/// </summary>
public int Quality { get => quality; set => quality = Math.Clamp(value, 0, 100); }
private int quality = 75;
/// <summary>
/// 像素格式
/// </summary>
public string PixelFormat { get; set; } = "yuva420p";
/// <summary>
/// 循环次数, 0 无限循环, 取值范围 [0, 65535]
/// </summary>
public int Loop { get => loop; set => loop = Math.Clamp(value, 0, 65535); }
private int loop = 0;
public override string FileNameNoteSuffix => $"{Codec}_{Quality}_{PixelFormat}";
public override void SetOutputOptions(FFMpegArgumentOptions options)
{
base.SetOutputOptions(options);
options.WithVideoCodec(Codec).ForcePixelFormat(PixelFormat).WithCustomArgument($"-lossless {(Lossless ? 1 : 0)} -quality {Quality} -loop {Loop}");
}
}
public class WebpExporterProperty(WebpExporter exporter) : FFmpegVideoExporterProperty(exporter)
{
[Browsable(false)]
public override WebpExporter Exporter => (WebpExporter)base.Exporter;
/// <summary>
/// 编码器
/// </summary>
[StringEnumConverter.StandardValues("libwebp_anim", "libwebp", Customizable = true)]
[TypeConverter(typeof(StringEnumConverter))]
[Category("[3] "), DisplayName(""), Description("-c:v, 使")]
public string Codec { get => Exporter.Codec; set => Exporter.Codec = value; }
/// <summary>
/// 是否无损
/// </summary>
[Category("[3] "), DisplayName(""), Description("-lossless, 0 , 1 ")]
public bool Lossless { get => Exporter.Lossless; set => Exporter.Lossless = value; }
/// <summary>
/// CRF
/// </summary>
[Category("[3] "), DisplayName(""), Description("-quality, 0-100, 75")]
public int Quality { get => Exporter.Quality; set => Exporter.Quality = value; }
/// <summary>
/// 像素格式
/// </summary>
[StringEnumConverter.StandardValues("yuv420p", "yuva420p", Customizable = true)]
[TypeConverter(typeof(StringEnumConverter))]
[Category("[3] "), DisplayName(""), Description("-pix_fmt, 使")]
public string PixelFormat { get => Exporter.PixelFormat; set => Exporter.PixelFormat = value; }
/// <summary>
/// 透明度阈值
/// </summary>
[Category("[3] "), DisplayName(""), Description("-loop, , 0 , [0, 65535]")]
public int Loop { get => Exporter.Loop; set => Exporter.Loop = value; }
}
}

View File

@@ -0,0 +1,567 @@
using System;
using System.Collections.ObjectModel;
using System.Reflection;
using System.Drawing.Design;
using NLog;
using System.Xml.Linq;
using SpineViewer.Extensions;
using SpineViewer.Utils;
using System.Collections.Immutable;
using System.Collections.Frozen;
using System.Linq;
namespace SpineViewer.Spine
{
/// <summary>
/// Spine 基类, 使用静态方法 New 来创建具体版本对象, 该类是线程安全的
/// </summary>
public abstract class SpineObject : ImplementationResolver<SpineObject, SpineImplementationAttribute, SpineVersion>, SFML.Graphics.Drawable, IDisposable
{
/// <summary>
/// 空附件标记
/// </summary>
protected const string EMPTY_ATTACHMENT = "<Empty>";
/// <summary>
/// 空动画标记
/// </summary>
protected const string EMPTY_ANIMATION = "<Empty>";
/// <summary>
/// 预览图像素大小
/// </summary>
protected static readonly Size PreviewResolution = new(256, 256);
/// <summary>
/// 创建特定版本的 Spine
/// </summary>
public static SpineObject New(SpineVersion version, string skelPath, string? atlasPath = null)
{
atlasPath ??= Path.ChangeExtension(skelPath, ".atlas");
skelPath = Path.GetFullPath(skelPath);
atlasPath = Path.GetFullPath(atlasPath);
if (version == SpineVersion.Auto) version = SpineUtils.GetVersion(skelPath);
if (!File.Exists(atlasPath)) throw new FileNotFoundException($"atlas file {atlasPath} not found");
return New(version, [skelPath, atlasPath]).PostInit();
}
/// <summary>
/// 数据锁
/// </summary>
private readonly object _lock = new();
/// <summary>
/// 日志器
/// </summary>
protected readonly Logger logger = LogManager.GetCurrentClassLogger();
/// <summary>
/// 构造函数
/// </summary>
public SpineObject(string skelPath, string atlasPath)
{
Version = GetType().GetCustomAttribute<SpineImplementationAttribute>().ImplementationKey;
AssetsDir = Directory.GetParent(skelPath).FullName;
SkelPath = Path.GetFullPath(skelPath);
AtlasPath = Path.GetFullPath(atlasPath);
Name = Path.GetFileNameWithoutExtension(skelPath);
}
/// <summary>
/// 构造函数之后的初始化工作
/// </summary>
private SpineObject PostInit()
{
// 必须 Update 一次否则包围盒还没有值
update(0);
// XXX: tex 没办法在这里主动 Dispose
// 批量添加在获取预览图的时候极大概率会和预览线程死锁
// 虽然两边不会同时调用 Draw, 但是死锁似乎和 Draw 函数有关
// 除此之外, 似乎还和 tex 的 Dispose 有关
// 如果不对 tex 进行 Dispose, 那么不管是否 Draw 都正常不会死锁
var tex = new SFML.Graphics.RenderTexture((uint)PreviewResolution.Width, (uint)PreviewResolution.Height);
var bounds = getCurrentBounds().GetCanvasBounds(PreviewResolution);
using var view = new SFML.Graphics.View(
new(bounds.X + bounds.Width / 2, bounds.Y + bounds.Height / 2),
new(bounds.Width, -bounds.Height)
);
tex.SetView(view);
tex.Clear(SFML.Graphics.Color.Transparent);
tex.Draw(this);
tex.Display();
Preview = tex.Texture.CopyToBitmap();
// 初始化皮肤加载情况
foreach (var n in SkinNames) skinLoadStatus[n] = false;
// 默认初始化10个动画空位
for (int i = 0; i < 10; i++) setAnimation(i, AnimationNames.First());
return this;
}
~SpineObject() { Dispose(false); }
public void Dispose() { Dispose(true); GC.SuppressFinalize(this); }
protected virtual void Dispose(bool disposing)
{
Preview?.Dispose();
triangleVertices.Dispose();
lineVertices.Dispose();
rectLineVertices.Dispose();
}
/// <summary>
/// 运行时唯一 ID
/// </summary>
public string ID { get; } = Guid.NewGuid().ToString();
/// <summary>
/// 骨骼预览图, 并没有去除预乘, 画面可能偏暗
/// </summary>
public Image Preview { get; private set; }
/// <summary>
/// 获取所属版本
/// </summary>
public SpineVersion Version { get; }
/// <summary>
/// 资源所在完整目录
/// </summary>
public string AssetsDir { get; }
/// <summary>
/// skel 文件完整路径
/// </summary>
public string SkelPath { get; }
/// <summary>
/// atlas 文件完整路径
/// </summary>
public string AtlasPath { get; }
/// <summary>
/// 名称
/// </summary>
public string Name { get; }
/// <summary>
/// 获取所属文件版本
/// </summary>
public abstract string FileVersion { get; }
/// <summary>
/// 是否被隐藏, 被隐藏的模型将仅仅在列表显示, 不参与其他行为
/// </summary>
public bool IsHidden { get { lock (_lock) return isHidden; } set { lock (_lock) isHidden = value; } }
protected bool isHidden = false;
/// <summary>
/// 是否使用预乘 Alpha
/// </summary>
public bool UsePma { get { lock (_lock) return usePma; } set { lock (_lock) usePma = value; } }
protected bool usePma = false;
/// <summary>
/// 缩放比例
/// </summary>
public float Scale
{
get { lock (_lock) return scale; }
set { lock (_lock) { scale = Math.Max(value, 0.001f); update(0); } }
}
protected abstract float scale { get; set; }
/// <summary>
/// 位置
/// </summary>
public PointF Position
{
get { lock (_lock) return position; }
set { lock (_lock) { position = value; update(0); } }
}
protected abstract PointF position { get; set; }
/// <summary>
/// 水平翻转
/// </summary>
public bool FlipX
{
get { lock (_lock) return flipX; }
set { lock (_lock) { flipX = value; update(0); } }
}
protected abstract bool flipX { get; set; }
/// <summary>
/// 垂直翻转
/// </summary>
public bool FlipY
{
get { lock (_lock) return flipY; }
set { lock (_lock) { flipY = value; update(0); } }
}
protected abstract bool flipY { get; set; }
/// <summary>
/// 是否被选中
/// </summary>
public bool IsSelected
{
get { lock (_lock) return isSelected; }
set { lock (_lock) { isSelected = value; update(0); } }
}
protected bool isSelected = false;
/// <summary>
/// 启用渲染调试
/// </summary>
public bool EnableDebug
{
get { lock (_lock) return enableDebug; }
set { lock (_lock) { enableDebug = value; update(0); } }
}
private bool enableDebug = false;
/// <summary>
/// 显示纹理
/// </summary>
public bool DebugTexture
{
get { lock (_lock) return debugTexture; }
set { lock (_lock) { debugTexture = value; update(0); } }
}
private bool debugTexture = true;
/// <summary>
/// 显示包围盒
/// </summary>
public bool DebugBounds
{
get { lock (_lock) return debugBounds; }
set { lock (_lock) { debugBounds = value; update(0); } }
}
protected bool debugBounds = true;
/// <summary>
/// 显示骨骼
/// </summary>
public bool DebugBones
{
get { lock (_lock) return debugBones; }
set { lock (_lock) { debugBones = value; update(0); } }
}
protected bool debugBones = false;
/// <summary>
/// 显示区域附件边框
/// </summary>
public bool DebugRegions
{
get { lock (_lock) return debugRegions; }
set { lock (_lock) { debugRegions = value; update(0); } }
}
protected bool debugRegions = false;
/// <summary>
/// 显示网格附件边框线
/// </summary>
public bool DebugMeshHulls
{
get { lock (_lock) return debugMeshHulls; }
set { lock (_lock) { debugMeshHulls = value; update(0); } }
}
protected bool debugMeshHulls = false;
/// <summary>
/// 显示网格附件网格线
/// </summary>
public bool DebugMeshes
{
get { lock (_lock) return debugMeshes; }
set { lock (_lock) { debugMeshes = value; update(0); } }
}
protected bool debugMeshes = false;
/// <summary>
/// 显示碰撞盒附件边框线
/// </summary>
public bool DebugBoundingBoxes
{
get { lock (_lock) return debugBoundingBoxes; }
set { lock (_lock) { debugBoundingBoxes = value; update(0); } }
}
protected bool debugBoundingBoxes = false;
/// <summary>
/// 显示路径附件网格线
/// </summary>
public bool DebugPaths
{
get { lock (_lock) return debugPaths; }
set { lock (_lock) { debugPaths = value; update(0); } }
}
protected bool debugPaths = false;
/// <summary>
/// 显示点附件
/// </summary>
public bool DebugPoints
{
get { lock (_lock) return debugPoints; }
set { lock (_lock) { debugPoints = value; update(0); } }
}
protected bool debugPoints = false;
/// <summary>
/// 显示剪裁附件网格线
/// </summary>
public bool DebugClippings
{
get { lock (_lock) return debugClippings; }
set { lock (_lock) { debugClippings = value; update(0); } }
}
protected bool debugClippings = false;
/// <summary>
/// 所有插槽下可用的附件名
/// </summary>
public FrozenDictionary<string, ImmutableArray<string>> SlotAttachmentNames { get; protected set; }
/// <summary>
/// 包含的所有皮肤名称 (不含 default 默认皮肤)
/// </summary>
public ImmutableArray<string> SkinNames { get; protected set; }
/// <summary>
/// 包含的所有动画名称
/// </summary>
public ImmutableArray<string> AnimationNames { get; protected set; }
/// <summary>
/// 获取某个插槽当前加载的附件
/// </summary>
public string GetSlotAttachment(string slot) { lock (_lock) return getSlotAttachment(slot); }
protected abstract string getSlotAttachment(string slot);
/// <summary>
/// 设置某个插槽当前加载的附件
/// </summary>
public void SetSlotAttachment(string slot, string name) { lock (_lock) setSlotAttachment(slot, name); }
protected abstract void setSlotAttachment(string slot, string name);
/// <summary>
/// 皮肤的加载情况记录表
/// </summary>
protected readonly Dictionary<string, bool> skinLoadStatus = [];
/// <summary>
/// 查询皮肤加载状态, 皮肤不存在时返回 false
/// </summary>
public bool GetSkinStatus(string name) { lock (_lock) return skinLoadStatus.TryGetValue(name, out var status) && status; }
/// <summary>
/// 设置皮肤加载状态, 忽略不存在的皮肤
/// </summary>
public void SetSkinStatus(string name, bool status)
{
if (!skinLoadStatus.ContainsKey(name)) return;
lock (_lock)
{
skinLoadStatus[name] = status;
reloadSkins();
}
}
/// <summary>
/// 刷新已加载皮肤
/// </summary>
public void ReloadSkins() { lock (_lock) reloadSkins(); }
protected void reloadSkins()
{
clearSkins();
foreach (var (name, _) in skinLoadStatus.Where(e => e.Value)) addSkin(name);
update(0);
}
protected abstract void addSkin(string name);
protected abstract void clearSkins();
/// <summary>
/// 获取所有非 null 的轨道索引快照
/// </summary>
public int[] GetTrackIndices() { lock (_lock) return getTrackIndices(); }
protected abstract int[] getTrackIndices();
/// <summary>
/// 获取指定轨道的当前动画, 如果没有, 应当返回空动画名称
/// </summary>
public string GetAnimation(int track) { lock (_lock) return getAnimation(track); }
protected abstract string getAnimation(int track);
/// <summary>
/// 设置某个轨道动画
/// </summary>
public void SetAnimation(int track, string name) { lock (_lock) { setAnimation(track, name); update(0); } }
protected abstract void setAnimation(int track, string name);
/// <summary>
/// 清除某个轨道, 与设置空动画不同, 是彻底删除轨道内的东西
/// </summary>
public void ClearTrack(int i) { lock (_lock) { clearTrack(i); update(0); } }
protected abstract void clearTrack(int i); // XXX: 清除轨道之后被加载的附件还是会保留, 不会自动卸下, 除非使用 SetSlotsToSetupPose
/// <summary>
/// 获取动画时长, 如果动画不存在则返回 0
/// </summary>
public abstract float GetAnimationDuration(string name);
/// <summary>
/// 重置所有轨道上的动画时间
/// </summary>
public void ResetAnimationsTime() { lock (_lock) { foreach (var i in getTrackIndices()) setAnimation(i, getAnimation(i)); update(0); } }
/// <summary>
/// 获取当前状态包围盒
/// </summary>
public RectangleF GetCurrentBounds() { lock (_lock) return getCurrentBounds(); }
protected abstract RectangleF getCurrentBounds();
/// <summary>
/// 获取当前参数下包围盒最大范围, 不是精确值
/// </summary>
public RectangleF GetBounds() { lock (_lock) return getBounds(); }
protected abstract RectangleF getBounds();
/// <summary>
/// 更新内部状态
/// </summary>
public void Update(float delta) { lock (_lock) update(delta); }
protected abstract void update(float delta);
#region SFML.Graphics.Drawable
/// <summary>
/// 包围盒颜色
/// </summary>
protected static readonly SFML.Graphics.Color BoundsColor = new(120, 200, 0);
/// <summary>
/// 骨骼点颜色
/// </summary>
protected static readonly SFML.Graphics.Color BonePointColor = new(0, 255, 0);
/// <summary>
/// 骨骼线颜色
/// </summary>
protected static readonly SFML.Graphics.Color BoneLineColor = new(255, 0, 0);
/// <summary>
/// 网格线颜色
/// </summary>
protected static readonly SFML.Graphics.Color MeshLineColor = new(255, 163, 0, 128);
/// <summary>
/// 附件边框线颜色
/// </summary>
protected static readonly SFML.Graphics.Color AttachmentLineColor = new(0, 0, 255, 128);
/// <summary>
/// 剪裁附件边框线颜色
/// </summary>
protected static readonly SFML.Graphics.Color ClippingLineColor = new(204, 0, 0);
/// <summary>
/// spine 顶点坐标缓冲区
/// </summary>
protected float[] worldVerticesBuffer = new float[1024];
/// <summary>
/// 三角形顶点缓冲区
/// </summary>
protected readonly SFML.Graphics.VertexArray triangleVertices = new(SFML.Graphics.PrimitiveType.Triangles);
/// <summary>
/// 无面积线条缓冲区
/// </summary>
protected readonly SFML.Graphics.VertexArray lineVertices = new(SFML.Graphics.PrimitiveType.Lines);
/// <summary>
/// 有半径圆点临时缓存对象
/// </summary>
private readonly SFML.Graphics.CircleShape circlePointShape = new();
/// <summary>
/// 有宽度线条缓冲区, 需要通过 <see cref="AddRectLine"/> 添加顶点
/// </summary>
protected readonly SFML.Graphics.VertexArray rectLineVertices = new(SFML.Graphics.PrimitiveType.Quads);
/// <summary>
/// 绘制有半径的实心圆点, 随模型一起缩放大小
/// </summary>
protected void DrawCirclePoint(SFML.Graphics.RenderTarget target, SFML.System.Vector2f p, SFML.Graphics.Color color, float radius = 1)
{
circlePointShape.Origin = new(radius, radius);
circlePointShape.Position = p;
circlePointShape.FillColor = color;
circlePointShape.Radius = radius;
target.Draw(circlePointShape);
}
/// <summary>
/// 绘制有宽度的实心线, 会随模型一起缩放粗细, 顶点被存储在 <see cref="rectLineVertices"/> 数组内
/// </summary>
protected void AddRectLine(SFML.System.Vector2f p1, SFML.System.Vector2f p2, SFML.Graphics.Color color, float width = 1)
{
var dx = p2.X - p1.X;
var dy = p2.Y - p1.Y;
var dt = (float)Math.Sqrt(dx * dx + dy * dy);
if (dt == 0) return;
var cosTheta = -dy / dt;
var sinTheta = dx / dt;
var halfWidth = width / 2;
var t = new SFML.System.Vector2f(halfWidth * cosTheta, halfWidth * sinTheta);
var v = new SFML.Graphics.Vertex() { Color = color };
v.Position = p1 + t; rectLineVertices.Append(v);
v.Position = p2 + t; rectLineVertices.Append(v);
v.Position = p2 - t; rectLineVertices.Append(v);
v.Position = p1 - t; rectLineVertices.Append(v);
}
/// <summary>
/// SFML.Graphics.Drawable 接口实现
/// <para>这个渲染实现绘制出来的像素将是预乘的, 当渲染的背景透明度是 1 时, 则等价于非预乘的结果, 即正常画面, 否则画面偏暗</para>
/// <para>可以用于 <see cref="SFML.Graphics.RenderWindow"/> 的渲染, 因为直接在窗口上绘制时窗口始终是不透明的</para>
/// </summary>
public void Draw(SFML.Graphics.RenderTarget target, SFML.Graphics.RenderStates states)
{
lock (_lock)
{
if (!enableDebug)
{
draw(target, states);
}
else
{
if (debugTexture) draw(target, states);
if (isSelected) debugDraw(target);
}
}
}
/// <summary>
/// 这个渲染实现绘制出来的像素将是预乘的, 当渲染的背景透明度是 1 时, 则等价于非预乘的结果, 即正常画面, 否则画面偏暗
/// <para>可以用于 <see cref="SFML.Graphics.RenderWindow"/> 的渲染, 因为直接在窗口上绘制时窗口始终是不透明的</para>
/// </summary>
protected abstract void draw(SFML.Graphics.RenderTarget target, SFML.Graphics.RenderStates states);
/// <summary>
/// 渲染调试内容
/// </summary>
protected abstract void debugDraw(SFML.Graphics.RenderTarget target);
#endregion
}
}

View File

@@ -0,0 +1,135 @@
using SpineViewer.Utils;
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Linq;
using System.Reflection;
using System.Text;
using System.Text.Json.Nodes;
using System.Threading.Tasks;
namespace SpineViewer.Spine
{
/// <summary>
/// Spine 版本静态辅助类
/// </summary>
public static class SpineUtils
{
/// <summary>
/// 版本名称
/// </summary>
public static readonly ReadOnlyDictionary<SpineVersion, string> Names;
private static readonly Dictionary<SpineVersion, string> names = [];
/// <summary>
/// Runtime 版本字符串
/// </summary>
private static readonly Dictionary<SpineVersion, string> runtimes = [];
static SpineUtils()
{
// 初始化缓存
foreach (var value in Enum.GetValues(typeof(SpineVersion)))
{
var field = typeof(SpineVersion).GetField(value.ToString());
var attribute = field?.GetCustomAttribute<DescriptionAttribute>();
names[(SpineVersion)value] = attribute?.Description ?? value.ToString();
}
Names = names.AsReadOnly();
runtimes[SpineVersion.V21] = Assembly.GetAssembly(typeof(SpineRuntime21.Skeleton)).GetCustomAttribute<AssemblyInformationalVersionAttribute>()?.InformationalVersion;
runtimes[SpineVersion.V36] = Assembly.GetAssembly(typeof(SpineRuntime36.Skeleton)).GetCustomAttribute<AssemblyInformationalVersionAttribute>()?.InformationalVersion;
runtimes[SpineVersion.V37] = Assembly.GetAssembly(typeof(SpineRuntime37.Skeleton)).GetCustomAttribute<AssemblyInformationalVersionAttribute>()?.InformationalVersion;
runtimes[SpineVersion.V38] = Assembly.GetAssembly(typeof(SpineRuntime38.Skeleton)).GetCustomAttribute<AssemblyInformationalVersionAttribute>()?.InformationalVersion;
runtimes[SpineVersion.V40] = Assembly.GetAssembly(typeof(SpineRuntime40.Skeleton)).GetCustomAttribute<AssemblyInformationalVersionAttribute>()?.InformationalVersion;
runtimes[SpineVersion.V41] = Assembly.GetAssembly(typeof(SpineRuntime41.Skeleton)).GetCustomAttribute<AssemblyInformationalVersionAttribute>()?.InformationalVersion;
runtimes[SpineVersion.V42] = Assembly.GetAssembly(typeof(SpineRuntime42.Skeleton)).GetCustomAttribute<AssemblyInformationalVersionAttribute>()?.InformationalVersion;
}
/// <summary>
/// 版本字符串名称
/// </summary>
public static string GetName(this SpineVersion version)
{
return Names.TryGetValue(version, out var val) ? val : version.ToString();
}
/// <summary>
/// Runtime 版本字符串名称
/// </summary>
public static string GetRuntime(this SpineVersion version)
{
return runtimes.TryGetValue(version, out var val) ? val : GetName(version);
}
/// <summary>
/// 常规骨骼文件后缀集合
/// </summary>
public static readonly ImmutableHashSet<string> CommonSkelSuffix = [".skel", ".json"];
/// <summary>
/// 尝试检测骨骼文件版本
/// </summary>
/// <param name="skelPath"></param>
/// <returns></returns>
/// <exception cref="InvalidDataException"></exception>
public static SpineVersion GetVersion(string skelPath)
{
string versionString = null;
using var input = File.OpenRead(skelPath);
var reader = new SkeletonConverter.BinaryReader(input);
// try json format
try
{
if (JsonNode.Parse(input) is JsonObject root && root.TryGetPropertyValue("skeleton", out var node) &&
node is JsonObject _skeleton && _skeleton.TryGetPropertyValue("spine", out var _version))
versionString = (string)_version;
}
catch { }
// try v4 binary format
if (versionString is null)
{
try
{
input.Position = 0;
var hash = reader.ReadLong();
var versionPosition = input.Position;
var versionByteCount = reader.ReadVarInt();
input.Position = versionPosition;
if (versionByteCount <= 13)
versionString = reader.ReadString();
}
catch { }
}
// try v3 binary format
if (versionString is null)
{
try
{
input.Position = 0;
var hash = reader.ReadString();
versionString = reader.ReadString();
}
catch { }
}
if (versionString is null)
throw new InvalidDataException($"No verison detected: {skelPath}");
if (versionString.StartsWith("2.1.")) return SpineVersion.V21;
else if (versionString.StartsWith("3.6.")) return SpineVersion.V36;
else if (versionString.StartsWith("3.7.")) return SpineVersion.V37;
else if (versionString.StartsWith("3.8.")) return SpineVersion.V38;
else if (versionString.StartsWith("4.0.")) return SpineVersion.V40;
else if (versionString.StartsWith("4.1.")) return SpineVersion.V41;
else if (versionString.StartsWith("4.2.")) return SpineVersion.V42;
else if (versionString.StartsWith("4.3.")) return SpineVersion.V43;
else throw new InvalidDataException($"Unknown verison: {versionString}");
}
}
}

View File

@@ -0,0 +1,52 @@
using SpineViewer.Utils;
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Globalization;
using System.Linq;
using System.Reflection;
using System.Text;
using System.Text.Json.Nodes;
using System.Threading.Tasks;
namespace SpineViewer.Spine
{
/// <summary>
/// 支持的 Spine 版本
/// </summary>
public enum SpineVersion
{
[Description("<Auto>")] Auto = 0x0000,
[Description("2.1.x")] V21 = 0x0201,
[Description("3.6.x")] V36 = 0x0306,
[Description("3.7.x")] V37 = 0x0307,
[Description("3.8.x")] V38 = 0x0308,
[Description("4.0.x")] V40 = 0x0400,
[Description("4.1.x")] V41 = 0x0401,
[Description("4.2.x")] V42 = 0x0402,
[Description("4.3.x")] V43 = 0x0403,
}
/// <summary>
/// Spine 实现类标记
/// </summary>
[AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = false)]
public class SpineImplementationAttribute(SpineVersion version) : Attribute, IImplementationKey<SpineVersion>
{
public SpineVersion ImplementationKey { get; private set; } = version;
}
public class SpineVersionConverter : EnumConverter
{
public SpineVersionConverter() : base(typeof(SpineVersion)) { }
public override object? ConvertTo(ITypeDescriptorContext? context, CultureInfo? culture, object? value, Type? destinationType)
{
if (destinationType == typeof(string) && value is SpineVersion version)
return version.GetName();
return base.ConvertTo(context, culture, value, destinationType);
}
}
}

View File

@@ -0,0 +1,199 @@
using SpineViewer.Spine;
using SpineViewer.Utils;
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace SpineViewer.Spine.SpineView
{
/// <summary>
/// 用于在 PropertyGrid 上显示 Spine 动画列表的包装类
/// </summary>
public class SpineAnimationProperty(SpineObject spine) : ICustomTypeDescriptor
{
[Browsable(false)]
public SpineObject Spine { get; } = spine;
/// <summary>
/// 全轨道动画最大时长
/// </summary>
[DisplayName("全轨道最大时长")]
public float AnimationTracksMaxDuration => Spine.GetTrackIndices().Select(i => Spine.GetAnimationDuration(Spine.GetAnimation(i))).Max();
/// <summary>
/// <see cref="TrackAnimationProperty"/> 属性对象缓存
/// </summary>
private readonly Dictionary<int, TrackAnimationProperty> trackAnimationProperties = [];
/// <summary>
/// <c>this.Track{i}</c>
/// </summary>
public TrackAnimationProperty GetTrackAnimation(int i)
{
if (!trackAnimationProperties.ContainsKey(i))
trackAnimationProperties[i] = new TrackAnimationProperty(Spine, i);
return trackAnimationProperties[i];
}
/// <summary>
/// <c>this.Track{i} = <paramref name="value"/></c>
/// </summary>
public void SetTrackAnimation(int i, string value)
{
Spine.SetAnimation(i, value);
TypeDescriptor.Refresh(this);
}
/// <summary>
/// 在属性面板悬停可以按轨道顺序显示动画名称
/// </summary>
public override string ToString() => $"[{string.Join(", ", Spine.GetTrackIndices().Select(Spine.GetAnimation))}]";
public override bool Equals(object? obj)
{
if (obj is SpineAnimationProperty prop) return ToString() == prop.ToString();
return base.Equals(obj);
}
public override int GetHashCode() => HashCode.Combine(typeof(SpineAnimationProperty).FullName.GetHashCode(), ToString().GetHashCode());
#region ICustomTypeDescriptor
// XXX: 必须实现 ICustomTypeDescriptor 接口, 不能继承 CustomTypeDescriptor, 似乎继承下来的东西会有问题, 导致某些调用不正确
/// <summary>
/// 属性描述符缓存
/// </summary>
private static readonly Dictionary<int, TrackWrapperPropertyDescriptor> pdCache = [];
public AttributeCollection GetAttributes() => TypeDescriptor.GetAttributes(this, true);
public string? GetClassName() => TypeDescriptor.GetClassName(this, true);
public string? GetComponentName() => TypeDescriptor.GetComponentName(this, true);
public TypeConverter? GetConverter() => TypeDescriptor.GetConverter(this, true);
public EventDescriptor? GetDefaultEvent() => TypeDescriptor.GetDefaultEvent(this, true);
public PropertyDescriptor? GetDefaultProperty() => TypeDescriptor.GetDefaultProperty(this, true);
public object? GetEditor(Type editorBaseType) => TypeDescriptor.GetEditor(this, editorBaseType, true);
public EventDescriptorCollection GetEvents() => TypeDescriptor.GetEvents(this, true);
public EventDescriptorCollection GetEvents(Attribute[]? attributes) => TypeDescriptor.GetEvents(this, attributes, true);
public object? GetPropertyOwner(PropertyDescriptor? pd) => this;
public PropertyDescriptorCollection GetProperties() => GetProperties(null);
public PropertyDescriptorCollection GetProperties(Attribute[]? attributes)
{
var props = new PropertyDescriptorCollection(TypeDescriptor.GetProperties(this, attributes, true).Cast<PropertyDescriptor>().ToArray());
foreach (var i in Spine.GetTrackIndices())
{
if (!pdCache.TryGetValue(i, out var pd))
pdCache[i] = pd = new TrackWrapperPropertyDescriptor(i, [new DisplayNameAttribute($"轨道 {i}")]);
props.Add(pd);
}
return props;
}
/// <summary>
/// 轨道属性描述符, 实现对属性的读取和赋值
/// </summary>
/// <param name="i">轨道索引</param>
private class TrackWrapperPropertyDescriptor(int i, Attribute[]? attributes) : PropertyDescriptor($"Track{i}", attributes)
{
private readonly int idx = i;
public override Type ComponentType => typeof(SpineAnimationProperty);
public override bool IsReadOnly => false;
public override Type PropertyType => typeof(TrackAnimationProperty);
public override bool CanResetValue(object component) => false;
public override void ResetValue(object component) { }
public override bool ShouldSerializeValue(object component) => false;
/// <summary>
/// 得到一个轨道包装类, 允许用户查看或者修改具体的属性值, 这个地方决定了在面板上看到的是一个对象及其属性
/// </summary>
public override object? GetValue(object? component)
{
if (component is SpineAnimationProperty tracks)
return tracks.GetTrackAnimation(idx);
return null;
}
/// <summary>
/// 允许通过字符串赋值修改该轨道的动画, 这里决定了当其他地方的调用 (比如 Converter) 通过 value 来设置属性值的时候应该怎么处理
/// </summary>
public override void SetValue(object? component, object? value)
{
if (component is SpineAnimationProperty tracks)
{
if (value is string s)
tracks.SetTrackAnimation(idx, s);
}
}
}
#endregion
}
/// <summary>
/// 对 <c><see cref="SpineAnimationProperty"/>.Track{i}</c> 属性的包装类
/// </summary>
[TypeConverter(typeof(TrackAnimationPropertyConverter))]
public class TrackAnimationProperty(SpineObject spine, int i)
{
private readonly SpineObject spine = spine;
[Browsable(false)]
public int Index { get; } = i;
[DisplayName("时长")]
public float Duration => spine.GetAnimationDuration(spine.GetAnimation(Index));
/// <summary>
/// 实现了默认的转为字符串的方式
/// </summary>
public override string ToString() => spine.GetAnimation(Index);
/// <summary>
/// 影响了属性面板的判断, 当动画名称相同的时候认为两个对象是相同的, 这样属性面板可以在多选的时候正确显示相同取值的内容
/// </summary>
public override bool Equals(object? obj)
{
if (obj is TrackAnimationProperty) return ToString() == obj.ToString();
return base.Equals(obj);
}
/// <summary>
/// 哈希码需要和 Equals 行为类似
/// </summary>
public override int GetHashCode() => HashCode.Combine(typeof(TrackAnimationProperty).FullName.GetHashCode(), ToString().GetHashCode());
}
/// <summary>
/// 轨道索引包装类转换器, 实现字符串和包装类的互相转换, 并且提供标准值列表对属性进行设置, 同时还提供在面板上显示包装类属性的能力
/// </summary>
public class TrackAnimationPropertyConverter : ExpandableObjectConverter
{
// NOTE: 可以不用实现 ConvertTo/ConvertFrom, 因为属性实现了与字符串之间的互转
// ToString 实现了 ConvertTo
// SetValue 实现了从字符串设置属性
public override bool GetStandardValuesSupported(ITypeDescriptorContext? context) => true;
public override bool GetStandardValuesExclusive(ITypeDescriptorContext? context) => true;
public override StandardValuesCollection? GetStandardValues(ITypeDescriptorContext? context)
{
if (context?.Instance is SpineAnimationProperty tracks)
{
return new StandardValuesCollection(tracks.Spine.AnimationNames);
}
else if (context?.Instance is object[] instances)
{
IEnumerable<string> common = [];
foreach (SpineAnimationProperty prop in instances.Where(inst => inst is SpineAnimationProperty))
common = common.Union(prop.Spine.AnimationNames);
return new StandardValuesCollection(common.ToArray());
}
return base.GetStandardValues(context);
}
}
}

View File

@@ -0,0 +1,57 @@
using SpineViewer.Spine;
using SpineViewer.Utils;
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace SpineViewer.Spine.SpineView
{
/// <summary>
/// 用于在 PropertyGrid 上显示 Spine 基本信息的包装类
/// </summary>
public class SpineBaseInfoProperty(SpineObject spine)
{
[Browsable(false)]
public SpineObject Spine { get; } = spine;
/// <summary>
/// 获取所属版本
/// </summary>
[TypeConverter(typeof(SpineVersionConverter))]
[DisplayName("运行时版本")]
public SpineVersion Version => Spine.Version;
/// <summary>
/// 资源所在完整目录
/// </summary>
[DisplayName("资源目录")]
public string AssetsDir => Spine.AssetsDir;
/// <summary>
/// skel 文件完整路径
/// </summary>
[DisplayName("skel文件路径")]
public string SkelPath => Spine.SkelPath;
/// <summary>
/// atlas 文件完整路径
/// </summary>
[DisplayName("atlas文件路径")]
public string AtlasPath => Spine.AtlasPath;
/// <summary>
/// 名称
/// </summary>
[DisplayName("名称")]
public string Name => Spine.Name;
/// <summary>
/// 获取所属文件版本
/// </summary>
[DisplayName("文件版本")]
public string FileVersion => Spine.FileVersion;
}
}

View File

@@ -0,0 +1,79 @@
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using SpineViewer.Spine;
namespace SpineViewer.Spine.SpineView
{
/// <summary>
/// 用于在 PropertyGrid 上显示 Spine 调试属性的包装类
/// </summary>
public class SpineDebugProperty(SpineObject spine)
{
[Browsable(false)]
public SpineObject Spine { get; } = spine;
/// <summary>
/// 显示纹理
/// </summary>
[DisplayName("Texture")]
public bool DebugTexture { get => Spine.DebugTexture; set => Spine.DebugTexture = value; }
/// <summary>
/// 显示包围盒
/// </summary>
[DisplayName("Bounds")]
public bool DebugBounds { get => Spine.DebugBounds; set => Spine.DebugBounds = value; }
/// <summary>
/// 显示骨骼
/// </summary>
[DisplayName("Bones")]
public bool DebugBones { get => Spine.DebugBones; set => Spine.DebugBones = value; }
/// <summary>
/// 显示区域附件边框线
/// </summary>
[DisplayName("Regions")]
public bool DebugRegions { get => Spine.DebugRegions; set => Spine.DebugRegions = value; }
/// <summary>
/// 显示网格附件边框线
/// </summary>
[DisplayName("MeshHulls")]
public bool DebugMeshHulls { get => Spine.DebugMeshHulls; set => Spine.DebugMeshHulls = value; }
/// <summary>
/// 显示网格附件网格线
/// </summary>
[DisplayName("Meshes")]
public bool DebugMeshes { get => Spine.DebugMeshes; set => Spine.DebugMeshes = value; }
///// <summary>
///// 显示碰撞盒附件边框线
///// </summary>
//[DisplayName("BoudingBoxes")]
//public bool DebugBoundingBoxes { get => Spine.DebugBoundingBoxes; set => Spine.DebugBoundingBoxes = value; }
///// <summary>
///// 显示路径附件网格线
///// </summary>
//[DisplayName("Paths")]
//public bool DebugPaths { get => Spine.DebugPaths; set => Spine.DebugPaths = value; }
///// <summary>
///// 显示点附件
///// </summary>
//[DisplayName("Points")]
//public bool DebugPoints { get => Spine.DebugPoints; set => Spine.DebugPoints = value; }
/// <summary>
/// 显示剪裁附件网格线
/// </summary>
[DisplayName("Clippings")]
public bool DebugClippings { get => Spine.DebugClippings; set => Spine.DebugClippings = value; }
}
}

View File

@@ -0,0 +1,41 @@
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Drawing.Design;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using SpineViewer.Spine;
namespace SpineViewer.Spine.SpineView
{
public class SpineObjectProperty(SpineObject spine)
{
[Browsable(false)]
public SpineObject Spine { get; } = spine;
[DisplayName("基本信息")]
public SpineBaseInfoProperty BaseInfo { get; } = new(spine);
[DisplayName("渲染")]
public SpineRenderProperty Render { get; } = new(spine);
[DisplayName("变换")]
public SpineTransformProperty Transform { get; } = new(spine);
[TypeConverter(typeof(ExpandableObjectConverter))]
[DisplayName("皮肤")]
public SpineSkinProperty Skin { get; } = new(spine);
[TypeConverter(typeof(ExpandableObjectConverter))]
[DisplayName("插槽")]
public SpineSlotProperty Slot { get; } = new(spine);
[TypeConverter(typeof(ExpandableObjectConverter))]
[DisplayName("动画")]
public SpineAnimationProperty Animation { get; } = new(spine);
[DisplayName("调试")]
public SpineDebugProperty Debug { get; } = new(spine);
}
}

View File

@@ -0,0 +1,31 @@
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using SpineViewer.Spine;
namespace SpineViewer.Spine.SpineView
{
/// <summary>
/// 用于在 PropertyGrid 上显示 Spine 渲染设置的包装类
/// </summary>
public class SpineRenderProperty(SpineObject spine)
{
[Browsable(false)]
public SpineObject Spine { get; } = spine;
/// <summary>
/// 是否被隐藏, 被隐藏的模型将仅仅在列表显示, 不参与其他行为
/// </summary>
[DisplayName("是否隐藏")]
public bool IsHidden { get => Spine.IsHidden; set => Spine.IsHidden = value; }
/// <summary>
/// 是否使用预乘Alpha
/// </summary>
[DisplayName("预乘Alpha通道")]
public bool UsePremultipliedAlpha { get => Spine.UsePma; set => Spine.UsePma = value; }
}
}

View File

@@ -0,0 +1,94 @@
using SpineViewer.Spine;
using SpineViewer.Utils;
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace SpineViewer.Spine.SpineView
{
/// <summary>
/// 皮肤动态类型包装类, 用于提供对 Spine 皮肤的管理能力
/// </summary>
/// <param name="spine">关联的 Spine 对象</param>
public class SpineSkinProperty(SpineObject spine) : ICustomTypeDescriptor
{
[Browsable(false)]
public SpineObject Spine { get; } = spine;
/// <summary>
/// 在属性面板悬停可以显示已加载的皮肤列表
/// </summary>
public override string ToString() => $"[{string.Join(", ", Spine.SkinNames.Where(Spine.GetSkinStatus))}]";
public override bool Equals(object? obj)
{
if (obj is SpineSkinProperty prop) return ToString() == prop.ToString();
return base.Equals(obj);
}
public override int GetHashCode() => HashCode.Combine(typeof(SpineSkinProperty).FullName.GetHashCode(), ToString().GetHashCode());
#region ICustomTypeDescriptor
// XXX: 必须实现 ICustomTypeDescriptor 接口, 不能继承 CustomTypeDescriptor, 似乎继承下来的东西会有问题, 导致某些调用不正确
private static readonly Dictionary<string, SkinPropertyDescriptor> pdCache = [];
public AttributeCollection GetAttributes() => TypeDescriptor.GetAttributes(this, true);
public string? GetClassName() => TypeDescriptor.GetClassName(this, true);
public string? GetComponentName() => TypeDescriptor.GetComponentName(this, true);
public TypeConverter? GetConverter() => TypeDescriptor.GetConverter(this, true);
public EventDescriptor? GetDefaultEvent() => TypeDescriptor.GetDefaultEvent(this, true);
public PropertyDescriptor? GetDefaultProperty() => TypeDescriptor.GetDefaultProperty(this, true);
public object? GetEditor(Type editorBaseType) => TypeDescriptor.GetEditor(this, editorBaseType, true);
public EventDescriptorCollection GetEvents() => TypeDescriptor.GetEvents(this, true);
public EventDescriptorCollection GetEvents(Attribute[]? attributes) => TypeDescriptor.GetEvents(this, attributes, true);
public object? GetPropertyOwner(PropertyDescriptor? pd) => this;
public PropertyDescriptorCollection GetProperties() => GetProperties(null);
public PropertyDescriptorCollection GetProperties(Attribute[]? attributes)
{
var props = new PropertyDescriptorCollection(TypeDescriptor.GetProperties(this, attributes, true).Cast<PropertyDescriptor>().ToArray());
foreach (var name in Spine.SkinNames)
{
if (!pdCache.TryGetValue(name, out var pd))
pdCache[name] = pd = new SkinPropertyDescriptor(name, [new DisplayNameAttribute(name)]);
props.Add(pd);
}
return props;
}
/// <summary>
/// 皮肤属性描述符, 实现对皮肤的加载和卸载, <c><see cref="SpineSkinProperty"/>.{name}</c>
/// </summary>
private class SkinPropertyDescriptor(string name, Attribute[]? attributes) : PropertyDescriptor(name, attributes)
{
public override Type ComponentType => typeof(SpineSkinProperty);
public override bool IsReadOnly => false;
public override Type PropertyType => typeof(bool);
public override bool CanResetValue(object component) => false;
public override void ResetValue(object component) { }
public override bool ShouldSerializeValue(object component) => false;
public override object? GetValue(object? component)
{
if (component is SpineSkinProperty prop)
return prop.Spine.GetSkinStatus(Name);
return null;
}
public override void SetValue(object? component, object? value)
{
if (component is SpineSkinProperty prop)
{
if (value is bool s)
prop.Spine.SetSkinStatus(Name, s);
}
}
}
#endregion
}
}

View File

@@ -0,0 +1,170 @@
using SpineViewer.Spine;
using SpineViewer.Utils;
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace SpineViewer.Spine.SpineView
{
/// <summary>
/// 用于在 PropertyGrid 上显示插槽附件加载情况包装类
/// </summary>
public class SpineSlotProperty(SpineObject spine) : ICustomTypeDescriptor
{
[Browsable(false)]
public SpineObject Spine { get; } = spine;
/// <summary>
/// 显示所有插槽集合
/// </summary>
public override string ToString() => $"[{string.Join(", ", Spine.SlotAttachmentNames.Keys)}]";
public override bool Equals(object? obj)
{
if (obj is SpineAnimationProperty prop) return ToString() == prop.ToString();
return base.Equals(obj);
}
public override int GetHashCode() => HashCode.Combine(typeof(SpineAnimationProperty).FullName.GetHashCode(), ToString().GetHashCode());
#region ICustomTypeDescriptor
// XXX: 必须实现 ICustomTypeDescriptor 接口, 不能继承 CustomTypeDescriptor, 似乎继承下来的东西会有问题, 导致某些调用不正确
/// <summary>
/// 属性描述符缓存
/// </summary>
private static readonly Dictionary<string, SlotPropertyDescriptor> pdCache = [];
public AttributeCollection GetAttributes() => TypeDescriptor.GetAttributes(this, true);
public string? GetClassName() => TypeDescriptor.GetClassName(this, true);
public string? GetComponentName() => TypeDescriptor.GetComponentName(this, true);
public TypeConverter? GetConverter() => TypeDescriptor.GetConverter(this, true);
public EventDescriptor? GetDefaultEvent() => TypeDescriptor.GetDefaultEvent(this, true);
public PropertyDescriptor? GetDefaultProperty() => TypeDescriptor.GetDefaultProperty(this, true);
public object? GetEditor(Type editorBaseType) => TypeDescriptor.GetEditor(this, editorBaseType, true);
public EventDescriptorCollection GetEvents() => TypeDescriptor.GetEvents(this, true);
public EventDescriptorCollection GetEvents(Attribute[]? attributes) => TypeDescriptor.GetEvents(this, attributes, true);
public object? GetPropertyOwner(PropertyDescriptor? pd) => this;
public PropertyDescriptorCollection GetProperties() => GetProperties(null);
public PropertyDescriptorCollection GetProperties(Attribute[]? attributes)
{
var props = new PropertyDescriptorCollection(TypeDescriptor.GetProperties(this, attributes, true).Cast<PropertyDescriptor>().ToArray());
foreach (var slotName in Spine.SlotAttachmentNames.Keys)
{
if (!pdCache.TryGetValue(slotName, out var pd))
pdCache[slotName] = pd = new SlotPropertyDescriptor(slotName, [new DisplayNameAttribute($"{slotName}")]);
props.Add(pd);
}
return props;
}
/// <summary>
/// 插槽属性描述符, 实现对属性的读取和赋值
/// </summary>
internal class SlotPropertyDescriptor(string name, Attribute[]? attributes) : PropertyDescriptor(name, attributes)
{
public override Type ComponentType => typeof(SpineSlotProperty);
public override bool IsReadOnly => false;
public override Type PropertyType => typeof(SlotProperty);
public override bool CanResetValue(object component) => false;
public override void ResetValue(object component) { }
public override bool ShouldSerializeValue(object component) => false;
/// <summary>
/// 得到一个轨道包装类, 允许用户查看或者修改具体的属性值, 这个地方决定了在面板上看到的是一个对象及其属性
/// </summary>
public override object? GetValue(object? component)
{
if (component is SpineSlotProperty slots)
return slots.Spine.GetSlotAttachment(Name);
return null;
}
/// <summary>
/// 允许通过字符串赋值修改该轨道的动画, 这里决定了当其他地方的调用 (比如 Converter) 通过 value 来设置属性值的时候应该怎么处理
/// </summary>
public override void SetValue(object? component, object? value)
{
if (component is SpineSlotProperty slots)
{
if (value is string s)
slots.Spine.SetSlotAttachment(Name, s);
}
}
}
#endregion
}
/// <summary>
/// 对 <c><see cref="SpineSlotProperty"/>.{name}</c> 属性的包装类
/// </summary>
[TypeConverter(typeof(SlotPropertyConverter))]
public class SlotProperty(SpineObject spine, string name)
{
private readonly SpineObject spine = spine;
[Browsable(false)]
public string Name { get; } = name;
/// <summary>
/// 实现了默认的转为字符串的方式
/// </summary>
public override string ToString() => spine.GetSlotAttachment(Name);
/// <summary>
/// 影响了属性面板的判断, 当动画名称相同的时候认为两个对象是相同的, 这样属性面板可以在多选的时候正确显示相同取值的内容
/// </summary>
public override bool Equals(object? obj)
{
if (obj is SlotProperty) return ToString() == obj.ToString();
return base.Equals(obj);
}
/// <summary>
/// 哈希码需要和 Equals 行为类似
/// </summary>
public override int GetHashCode() => HashCode.Combine(typeof(SlotProperty).FullName.GetHashCode(), ToString().GetHashCode());
}
/// <summary>
/// 轨道索引包装类转换器, 实现字符串和包装类的互相转换, 并且提供标准值列表对属性进行设置, 同时还提供在面板上显示包装类属性的能力
/// </summary>
public class SlotPropertyConverter : StringConverter
{
// NOTE: 可以不用实现 ConvertTo/ConvertFrom, 因为属性实现了与字符串之间的互转
// ToString 实现了 ConvertTo
// SetValue 实现了从字符串设置属性
public override bool GetStandardValuesSupported(ITypeDescriptorContext? context) => true;
public override bool GetStandardValuesExclusive(ITypeDescriptorContext? context) => true;
public override StandardValuesCollection? GetStandardValues(ITypeDescriptorContext? context)
{
if (context?.PropertyDescriptor is PropertyDescriptor pd)
{
if (context?.Instance is SpineSlotProperty slots)
{
if (slots.Spine.SlotAttachmentNames.TryGetValue(pd.Name, out var names))
return new StandardValuesCollection(names);
}
else if (context?.Instance is object[] instances)
{
IEnumerable<string> common = [];
foreach (SpineSlotProperty prop in instances.Where(inst => inst is SpineSlotProperty))
{
if (prop.Spine.SlotAttachmentNames.TryGetValue(pd.Name, out var names))
common = common.Union(names);
}
return new StandardValuesCollection(common.ToArray());
}
}
return base.GetStandardValues(context);
}
}
}

View File

@@ -0,0 +1,45 @@
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using SpineViewer.Spine;
using SpineViewer.Utils;
namespace SpineViewer.Spine.SpineView
{
/// <summary>
/// 用于在 PropertyGrid 上显示 Spine 空间变换的包装类
/// </summary>
public class SpineTransformProperty(SpineObject spine)
{
[Browsable(false)]
public SpineObject Spine { get; } = spine;
/// <summary>
/// 缩放比例
/// </summary>
[DisplayName("缩放比例")]
public float Scale { get => Spine.Scale; set => Spine.Scale = value; }
/// <summary>
/// 位置
/// </summary>
[TypeConverter(typeof(PointFConverter))]
[DisplayName("位置")]
public PointF Position { get => Spine.Position; set => Spine.Position = value; }
/// <summary>
/// 水平翻转
/// </summary>
[DisplayName("水平翻转")]
public bool FlipX { get => Spine.FlipX; set => Spine.FlipX = value; }
/// <summary>
/// 垂直翻转
/// </summary>
[DisplayName("垂直翻转")]
public bool FlipY { get => Spine.FlipY; set => Spine.FlipY = value; }
}
}

View File

@@ -1,61 +0,0 @@
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace SpineViewer.Spine
{
public class VersionConverter : EnumConverter
{
public VersionConverter() : base(typeof(Version)) { }
public override object? ConvertTo(ITypeDescriptorContext? context, CultureInfo? culture, object? value, Type? destinationType)
{
if (destinationType == typeof(string) && value is Version version)
{
// 调用自定义的 String() 方法
return version.GetName();
}
return base.ConvertTo(context, culture, value, destinationType);
}
}
public class AnimationConverter : StringConverter
{
public override bool GetStandardValuesSupported(ITypeDescriptorContext? context)
{
// 支持标准值列表
return true;
}
public override bool GetStandardValuesExclusive(ITypeDescriptorContext? context)
{
// 排他模式,只有下拉列表中的值可选
return true;
}
public override StandardValuesCollection? GetStandardValues(ITypeDescriptorContext? context)
{
if (context?.Instance is Spine obj)
{
return new StandardValuesCollection(obj.AnimationNames);
}
else if (context?.Instance is Spine[] spines)
{
if (spines.Length > 0)
{
IEnumerable<string> common = spines[0].AnimationNames;
foreach (var spine in spines.Skip(1))
common = common.Intersect(spine.AnimationNames);
return new StandardValuesCollection(common.ToArray());
}
}
return base.GetStandardValues(context);
}
}
}

View File

@@ -1,80 +0,0 @@
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Linq;
using System.Reflection;
using System.Text;
using System.Threading.Tasks;
namespace SpineViewer.Spine
{
/// <summary>
/// Spine 版本静态辅助类
/// </summary>
public static class VersionHelper
{
/// <summary>
/// 版本名称
/// </summary>
public static readonly ReadOnlyDictionary<Version, string> Names;
private static readonly Dictionary<Version, string> names = [];
/// <summary>
/// Runtime 版本字符串
/// </summary>
private static readonly Dictionary<Version, string> runtimes = [];
static VersionHelper()
{
// 初始化缓存
foreach (var value in Enum.GetValues(typeof(Version)))
{
var field = typeof(Version).GetField(value.ToString());
var attribute = field?.GetCustomAttribute<DescriptionAttribute>();
names[(Version)value] = attribute?.Description ?? value.ToString();
}
Names = names.AsReadOnly();
runtimes[Version.V21] = Assembly.GetAssembly(typeof(SpineRuntime21.Skeleton)).GetCustomAttribute<AssemblyInformationalVersionAttribute>()?.InformationalVersion;
runtimes[Version.V36] = Assembly.GetAssembly(typeof(SpineRuntime36.Skeleton)).GetCustomAttribute<AssemblyInformationalVersionAttribute>()?.InformationalVersion;
runtimes[Version.V37] = Assembly.GetAssembly(typeof(SpineRuntime37.Skeleton)).GetCustomAttribute<AssemblyInformationalVersionAttribute>()?.InformationalVersion;
runtimes[Version.V38] = Assembly.GetAssembly(typeof(SpineRuntime38.Skeleton)).GetCustomAttribute<AssemblyInformationalVersionAttribute>()?.InformationalVersion;
runtimes[Version.V40] = Assembly.GetAssembly(typeof(SpineRuntime40.Skeleton)).GetCustomAttribute<AssemblyInformationalVersionAttribute>()?.InformationalVersion;
runtimes[Version.V41] = Assembly.GetAssembly(typeof(SpineRuntime41.Skeleton)).GetCustomAttribute<AssemblyInformationalVersionAttribute>()?.InformationalVersion;
runtimes[Version.V42] = Assembly.GetAssembly(typeof(SpineRuntime42.Skeleton)).GetCustomAttribute<AssemblyInformationalVersionAttribute>()?.InformationalVersion;
}
/// <summary>
/// 版本字符串名称
/// </summary>
public static string GetName(this Version version)
{
return Names.TryGetValue(version, out var val) ? val : version.ToString();
}
/// <summary>
/// Runtime 版本字符串名称
/// </summary>
public static string GetRuntime(this Version version)
{
return runtimes.TryGetValue(version, out var val) ? val : GetName(version);
}
}
/// <summary>
/// 支持的 Spine 版本
/// </summary>
public enum Version
{
[Description("<Auto>")] Auto = 0x0000,
[Description("2.1.x")] V21 = 0x0201,
[Description("3.6.x")] V36 = 0x0306,
[Description("3.7.x")] V37 = 0x0307,
[Description("3.8.x")] V38 = 0x0308,
[Description("4.0.x")] V40 = 0x0400,
[Description("4.1.x")] V41 = 0x0401,
[Description("4.2.x")] V42 = 0x0402,
[Description("4.3.x")] V43 = 0x0403,
}
}

View File

@@ -5,13 +5,13 @@
<Nullable>enable</Nullable>
<Platforms>x64</Platforms>
<TargetFramework>net8.0-windows</TargetFramework>
<RuntimeIdentifier>win-x64</RuntimeIdentifier>
<BaseOutputPath>$(SolutionDir)out</BaseOutputPath>
<IncludeSourceRevisionInInformationalVersion>false</IncludeSourceRevisionInInformationalVersion>
<Version>0.10.9</Version>
<Version>0.12.5</Version>
<OutputType>WinExe</OutputType>
<UseWindowsForms>true</UseWindowsForms>
<ApplicationIcon>appicon.ico</ApplicationIcon>
<GenerateResourceWarnOnBinaryFormatterUse>false</GenerateResourceWarnOnBinaryFormatterUse>
</PropertyGroup>
<ItemGroup>

View File

@@ -1,55 +0,0 @@
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace SpineViewer
{
public class PointFConverter : ExpandableObjectConverter
{
public override bool CanConvertTo(ITypeDescriptorContext? context, [NotNullWhen(true)] Type? destinationType)
{
return destinationType == typeof(string) || base.CanConvertTo(context, destinationType);
}
public override object? ConvertTo(ITypeDescriptorContext? context, CultureInfo? culture, object? value, Type destinationType)
{
if (destinationType == typeof(string) && value is PointF point)
{
return $"{point.X}, {point.Y}";
}
return base.ConvertTo(context, culture, value, destinationType);
}
public override bool CanConvertFrom(ITypeDescriptorContext? context, Type sourceType)
{
return sourceType == typeof(string) || base.CanConvertFrom(context, sourceType);
}
public override object? ConvertFrom(ITypeDescriptorContext? context, CultureInfo? culture, object value)
{
if (value is string str)
{
var parts = str.Split(',');
if (parts.Length == 2 &&
float.TryParse(parts[0], out var x) &&
float.TryParse(parts[1], out var y))
{
return new PointF(x, y);
}
}
return base.ConvertFrom(context, culture, value);
}
public override PropertyDescriptorCollection GetProperties(ITypeDescriptorContext? context, object value, Attribute[]? attributes)
{
return TypeDescriptor.GetProperties(typeof(PointF), attributes);
}
public override bool GetPropertiesSupported(ITypeDescriptorContext? context) => true;
}
}

Some files were not shown because too many files have changed in this diff Show More