Compare commits
162 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
afc011adbb | ||
|
|
da42c14d35 | ||
|
|
a7438e2026 | ||
|
|
47e5314bb3 | ||
|
|
1978c1da11 | ||
|
|
914c9d0ea3 | ||
|
|
7134aebb7f | ||
|
|
abb32e9ed2 | ||
|
|
2c77e385c5 | ||
|
|
ba0f5ac124 | ||
|
|
c4f17e3f06 | ||
|
|
b802ec252a | ||
|
|
68779caab0 | ||
|
|
5d819114d0 | ||
|
|
46b3937236 | ||
|
|
6803b8cf4a | ||
|
|
bd9c5a176b | ||
|
|
76c1d96c87 | ||
|
|
304af805cb | ||
|
|
027d3af619 | ||
|
|
c612c01ac7 | ||
|
|
cd7855a877 | ||
|
|
cdd81e0bfb | ||
|
|
750a8b8aff | ||
|
|
cd86155878 | ||
|
|
16739c39d6 | ||
|
|
c7971a9829 | ||
|
|
44c4fc4b21 | ||
|
|
6f1c8e3320 | ||
|
|
8f818416ba | ||
|
|
de6858ca48 | ||
|
|
3fd3d2a378 | ||
|
|
706c9125e6 | ||
|
|
5f026b000c | ||
|
|
0b0d036f08 | ||
|
|
6b9017d535 | ||
|
|
5eb47e33ac | ||
|
|
4d31335da0 | ||
|
|
0b5e76a448 | ||
|
|
775268c01a | ||
|
|
b0b1c85047 | ||
|
|
5f08fc6695 | ||
|
|
2de3bdf12b | ||
|
|
3a424c7dc1 | ||
|
|
c3e2b37072 | ||
|
|
65bd11a346 | ||
|
|
e6e7fc539f | ||
|
|
6522d415b7 | ||
|
|
378c66a333 | ||
|
|
07204417a5 | ||
|
|
c9c909cdf9 | ||
|
|
a9f59a4d2f | ||
|
|
1d2513cef5 | ||
|
|
febb797ae2 | ||
|
|
68d279a7c3 | ||
|
|
d2d8b7955c | ||
|
|
2a55fd9c36 | ||
|
|
695d3c0735 | ||
|
|
ce95db469b | ||
|
|
5d187cf80f | ||
|
|
e704ebc224 | ||
|
|
ee36f8981c | ||
|
|
09dd220abf | ||
|
|
15bc2dc3b8 | ||
|
|
1deb74eca9 | ||
|
|
de76ce64ab | ||
|
|
94b4ba33e6 | ||
|
|
7ce8a115f4 | ||
|
|
c036a4bb45 | ||
|
|
aa62f30b05 | ||
|
|
3d967c9812 | ||
|
|
e87e9efb99 | ||
|
|
8c1f6fb4a6 | ||
|
|
df82ed8a00 | ||
|
|
d01e3920ba | ||
|
|
777cd5ea3f | ||
|
|
3b73aea5c0 | ||
|
|
168f7a8173 | ||
|
|
04437e2de2 | ||
|
|
2ec83b2e87 | ||
|
|
90bfaa7b56 | ||
|
|
2ae175abd0 | ||
|
|
e2a84d8f88 | ||
|
|
b6f9cd0c7c | ||
|
|
61b7b90722 | ||
|
|
093c159753 | ||
|
|
32d36c0757 | ||
|
|
94dabebf2b | ||
|
|
8e875d4f7e | ||
|
|
86c383f2cf | ||
|
|
b404d8e79a | ||
|
|
d32b480ef2 | ||
|
|
3654825f27 | ||
|
|
d7231e8a09 | ||
|
|
98161aaf2e | ||
|
|
7a942b16bc | ||
|
|
067719c69b | ||
|
|
f3fce53b91 | ||
|
|
e35903f436 | ||
|
|
64cfe5fdd7 | ||
|
|
dbe586cff8 | ||
|
|
3104733db0 | ||
|
|
9d4bdd1028 | ||
|
|
f8030b1645 | ||
|
|
0a999ceb41 | ||
|
|
64bd9907cb | ||
|
|
580eaf990d | ||
|
|
5ab232a961 | ||
|
|
e596cd7ea4 | ||
|
|
05c47a4daa | ||
|
|
5a8783b5f4 | ||
|
|
08bc171a72 | ||
|
|
7372f5fe08 | ||
|
|
6f032bdd05 | ||
|
|
153d3603d2 | ||
|
|
95261e6907 | ||
|
|
17b344376d | ||
|
|
0ed4e44878 | ||
|
|
b42c1832f0 | ||
|
|
058534ba67 | ||
|
|
204dcd6498 | ||
|
|
2c846c0db9 | ||
|
|
2faeb044e0 | ||
|
|
09c8e4f779 | ||
|
|
6994fa6be8 | ||
|
|
cc7beb7670 | ||
|
|
510653732d | ||
|
|
93e8178d67 | ||
|
|
cebc4864cc | ||
|
|
6ad0449376 | ||
|
|
c33c977326 | ||
|
|
f0299d365a | ||
|
|
6ecdca73f5 | ||
|
|
af6a709b2c | ||
|
|
d5c27450ef | ||
|
|
d10269fb07 | ||
|
|
53d987476e | ||
|
|
8b7866d37f | ||
|
|
bb529729b6 | ||
|
|
b7735d9ba8 | ||
|
|
ce744e2b84 | ||
|
|
631c92da3f | ||
|
|
b7063804e9 | ||
|
|
75d47c8419 | ||
|
|
114fb05e80 | ||
|
|
1fec65b37d | ||
|
|
9498e8f334 | ||
|
|
83b8411929 | ||
|
|
e9accd13b3 | ||
|
|
9e27a19258 | ||
|
|
252f3a5bea | ||
|
|
e0626bb126 | ||
|
|
7ff62c7f40 | ||
|
|
4b07e02acb | ||
|
|
4654d1d9c2 | ||
|
|
ce1f75e8a5 | ||
|
|
4d9aebc758 | ||
|
|
e814368ef3 | ||
|
|
bbbb02500f | ||
|
|
404f255f14 | ||
|
|
7a15e0d38a | ||
|
|
bfe669bdd9 |
60
CHANGELOG.md
60
CHANGELOG.md
@@ -1,5 +1,65 @@
|
||||
# CHANGELOG
|
||||
|
||||
## v0.12.6
|
||||
|
||||
- 增加全屏预览
|
||||
- 增加桌面投影 (实验性功能)
|
||||
- 增加预览画面背景色设置
|
||||
- 增加分辨率和颜色预设列表
|
||||
- 皮肤面板显示 default
|
||||
|
||||
## v0.12.5
|
||||
|
||||
- 增加插槽属性面板
|
||||
- 修改皮肤属性面板设置方式为True/False
|
||||
|
||||
## v0.12.4
|
||||
|
||||
- 增加导出自动分辨率参数
|
||||
- 增加导出边缘和填充参数
|
||||
- 增加导出内容溢出参数
|
||||
- 支持3.7及以下版本多皮肤功能
|
||||
- 增加3.8版本的骨骼文件二进制和文本格式互转
|
||||
- 增加格式转换输出文件夹参数
|
||||
- 修改打开对话框的默认文件后缀筛选为所有类型
|
||||
|
||||
## v0.12.3
|
||||
|
||||
- 增加按住 ctrl 缩放选中模型
|
||||
- 增加对骨骼/网格/剪裁的调试渲染
|
||||
- 换回以前的上下参数面板布局
|
||||
- 修改窗口缩放模式为 Font -> Dpi
|
||||
- 修复部分问题
|
||||
|
||||
## v0.12.2
|
||||
|
||||
- 模型参数分标签显示
|
||||
- 皮肤/动画列表使用右键菜单进行增删
|
||||
- 标题栏显示版本号
|
||||
- 增加 webp 和 avif 动图格式
|
||||
- 增加导出参数缓存
|
||||
- 动图默认帧率修改为 24 帧
|
||||
- 增加保留最后一帧参数
|
||||
|
||||
## v0.12.1
|
||||
|
||||
- 优化使用体验, 提供初始皮肤/动画空位
|
||||
- 修复预览画面分辨率调整时父容器尺寸获取错误
|
||||
|
||||
## v0.12.0
|
||||
|
||||
- 支持皮肤列表 (仅 3.8.x 及以上支持)
|
||||
- 支持多轨道动画
|
||||
- 动画和皮肤列表多选时改为取并集
|
||||
- 修复导出时没有正确处理预乘像素的问题
|
||||
|
||||
## v0.11.5
|
||||
|
||||
- 导出格式全面支持
|
||||
- 修复预览图不显示的问题
|
||||
- 优化列表卡顿问题
|
||||
- 模型列表增加数量显示
|
||||
|
||||
## v0.11.4
|
||||
|
||||
- 增加 MP4 导出格式
|
||||
|
||||
99
README.en.md
99
README.en.md
@@ -1,88 +1,97 @@
|
||||
# [SpineViewer](https://github.com/ww-rm/SpineViewer)
|
||||
|
||||
[](https://github.com/ww-rm/SpineViewer/actions/workflows/dotnet-desktop.yml)
|
||||
[](https://github.com/ww-rm/SpineViewer/releases)
|
||||
[](https://github.com/ww-rm/SpineViewer/releases)
|
||||
|
||||
[中文](README.md) | [English](README.en.md)
|
||||
|
||||
A *WYSIWYG* Spine file viewer and exporter.
|
||||
*A WYSIWYG Spine file viewer and exporter.*
|
||||
|
||||

|
||||
|
||||
---
|
||||
|
||||
:sparkles: v0.12.x New Feature: Support for multi-track animations and multi-skin list management :sparkles:
|
||||
|
||||
---
|
||||
|
||||
## Installation
|
||||
|
||||
Go to the [Release](https://github.com/ww-rm/SpineViewer/releases) page to download the zip package.
|
||||
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/zh-cn/download/dotnet/8.0).
|
||||
The software requires the dependency framework [.NET Desktop Runtime 8.0.x](https://dotnet.microsoft.com/en-us/download/dotnet/8.0).
|
||||
|
||||
You can also download the zip package with the `SelfContained` suffix, which can run independently.
|
||||
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 system’s 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
|
||||
|
||||
- [x] Single Frame Image
|
||||
- [x] Frame Sequence
|
||||
- [x] Animated GIF
|
||||
- [ ] MKV
|
||||
- [x] MP4
|
||||
- [ ] MOV
|
||||
- [ ] WebM
|
||||
|
||||
More formats are under development :rocket::rocket::rocket:
|
||||
| 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` | | | |
|
||||
| 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:
|
||||
More versions are under development :rocket: :rocket: :rocket:
|
||||
|
||||
## Usage
|
||||
## How to Use
|
||||
|
||||
### Importing Skeletons
|
||||
### Importing Skeleton Files
|
||||
|
||||
There are three ways to import skeleton files:
|
||||
|
||||
- Drag and drop or paste the skeleton file/directory into the model list.
|
||||
- Open skeleton files in batch from the File menu.
|
||||
- Batch open skeleton files from the File menu.
|
||||
- Select a single model to open from the File menu.
|
||||
|
||||
### Adjusting Preview Content
|
||||
### Adjusting the Preview
|
||||
|
||||
The model list supports right-click menus and several hotkeys, and multiple models can be selected for batch adjustments of model parameters.
|
||||
The model list supports context menus and some shortcuts, and you can multi-select to adjust parameters in bulk.
|
||||
|
||||
In addition to using the control panel for parameter settings, the preview window supports the following mouse actions:
|
||||
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 to enable multi-selection, which is synchronized with the model list on the left.
|
||||
- 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 scroll wheel to zoom in/out.
|
||||
- "Render selected only" mode, in which the preview only includes selected models and the selection can only be changed via the model list on the left.
|
||||
- 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 window allow you to adjust the timeline, effectively serving as a simple player.
|
||||
The buttons below the preview allow you to adjust the timeline, acting as a simple media player.
|
||||
|
||||
### Exporting Preview Content
|
||||
### Exporting the Preview
|
||||
|
||||
Export follows the "What You See Is What You Get" principle—what you see in the live preview is exactly what gets exported.
|
||||
Exporting follows the “What You See Is What You Get” principle – the preview exactly reflects the output.
|
||||
|
||||
There are a few key parameters for exporting:
|
||||
There are several key parameters for export:
|
||||
|
||||
- Render Selected Only: This option not only affects the preview mode but also the export; if enabled, only the selected models will be considered, and all other models will be ignored during export.
|
||||
- Output Folder: This parameter is optional in some cases. If not provided, the output will be saved in each model's own directory. Otherwise, all output files will be saved to the specified folder.
|
||||
- Single Export: By default, each model is exported individually in batch mode. If "Single Export" is selected, all exported models will be rendered on a single canvas, resulting in only one output file.
|
||||
- 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 model’s 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 more detailed instructions and usage, please refer to the [Wiki](https://github.com/ww-rm/SpineViewer/wiki). If you encounter any issues or bugs, please open an [Issue](https://github.com/ww-rm/SpineViewer/issues).
|
||||
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
|
||||
|
||||
@@ -92,6 +101,6 @@ For more detailed instructions and usage, please refer to the [Wiki](https://git
|
||||
|
||||
---
|
||||
|
||||
*If you like this project, please give it a :star: and share it with others! :)*
|
||||
*If you like this project, please give it a :star: and share it with others!*
|
||||
|
||||
[](https://starchart.cc/ww-rm/SpineViewer)
|
||||
[](https://starchart.cc/ww-rm/SpineViewer)
|
||||
26
README.md
26
README.md
@@ -1,6 +1,8 @@
|
||||
# [SpineViewer](https://github.com/ww-rm/SpineViewer)
|
||||
|
||||
[](https://github.com/ww-rm/SpineViewer/actions/workflows/dotnet-desktop.yml)
|
||||
[](https://github.com/ww-rm/SpineViewer/releases)
|
||||
[](https://github.com/ww-rm/SpineViewer/releases)
|
||||
|
||||
[中文](README.md) | [English](README.en.md)
|
||||
|
||||
@@ -8,8 +10,6 @@
|
||||
|
||||

|
||||
|
||||
---
|
||||
|
||||
## 安装
|
||||
|
||||
前往 [Release](https://github.com/ww-rm/SpineViewer/releases) 界面下载压缩包.
|
||||
@@ -18,17 +18,19 @@
|
||||
|
||||
也可以下载带有 `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).
|
||||
|
||||
## 导出格式支持
|
||||
|
||||
- [x] 单帧画面
|
||||
- [x] 帧序列
|
||||
- [x] GIF 动图
|
||||
- [ ] MKV
|
||||
- [x] MP4
|
||||
- [ ] MOV
|
||||
- [ ] WebM
|
||||
|
||||
更多格式正在施工 :rocket::rocket::rocket:
|
||||
| 导出格式 | 适用场景 |
|
||||
| --- | --- |
|
||||
| 单帧画面 | 支持生成高清模型画面图像, 可手动调节需要的一帧. |
|
||||
| 帧序列 | 支持 PNG 格式帧序列, 可保留透明通道且无损压缩. |
|
||||
| GIF/WebP/AVIF | 适合生成预览动图. |
|
||||
| MP4 | 最常见的视频格式, 兼容性最好. |
|
||||
| WebM | 适合浏览器在线播放格式, 支持透明背景. |
|
||||
| MKV/MOV | 适合折腾. |
|
||||
| 自定义导出 | 除上述预设方案, 支持提供任意 FFmpeg 参数进行导出, 满足自定义复杂需求. |
|
||||
|
||||
## Spine 版本支持
|
||||
|
||||
@@ -45,7 +47,7 @@
|
||||
| `4.2.x` | :white_check_mark: | | |
|
||||
| `4.3.x` | | | |
|
||||
|
||||
更多版本正在施工 :rocket::rocket::rocket:
|
||||
更多版本正在施工 :rocket: :rocket: :rocket:
|
||||
|
||||
## 使用方法
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -152,27 +152,13 @@ namespace SpineRuntime36 {
|
||||
|
||||
Bone parent = this.parent;
|
||||
if (parent == null) { // Root bone.
|
||||
float rotationY = rotation + 90 + shearY;
|
||||
float la = MathUtils.CosDeg(rotation + shearX) * scaleX;
|
||||
float lb = MathUtils.CosDeg(rotationY) * scaleY;
|
||||
float lc = MathUtils.SinDeg(rotation + shearX) * scaleX;
|
||||
float ld = MathUtils.SinDeg(rotationY) * scaleY;
|
||||
if (skeleton.flipX) {
|
||||
x = -x;
|
||||
la = -la;
|
||||
lb = -lb;
|
||||
}
|
||||
if (skeleton.flipY != yDown) {
|
||||
y = -y;
|
||||
lc = -lc;
|
||||
ld = -ld;
|
||||
}
|
||||
a = la;
|
||||
b = lb;
|
||||
c = lc;
|
||||
d = ld;
|
||||
worldX = x + skeleton.x;
|
||||
worldY = y + skeleton.y;
|
||||
float rotationY = rotation + 90 + shearY, sx = skeleton.scaleX, sy = skeleton.scaleY;
|
||||
a = MathUtils.CosDeg(rotation + shearX) * scaleX * sx;
|
||||
b = MathUtils.CosDeg(rotationY) * scaleY * sx;
|
||||
c = MathUtils.SinDeg(rotation + shearX) * scaleX * sy;
|
||||
d = MathUtils.SinDeg(rotationY) * scaleY * sy;
|
||||
worldX = x * sx + skeleton.x;
|
||||
worldY = y * sy + skeleton.y;
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -228,13 +214,16 @@ namespace SpineRuntime36 {
|
||||
case TransformMode.NoScale:
|
||||
case TransformMode.NoScaleOrReflection: {
|
||||
float cos = MathUtils.CosDeg(rotation), sin = MathUtils.SinDeg(rotation);
|
||||
float za = pa * cos + pb * sin;
|
||||
float zc = pc * cos + pd * sin;
|
||||
float za = (pa * cos + pb * sin) / skeleton.scaleX;
|
||||
float zc = (pc * cos + pd * sin) / skeleton.scaleY;
|
||||
float s = (float)Math.Sqrt(za * za + zc * zc);
|
||||
if (s > 0.00001f) s = 1 / s;
|
||||
za *= s;
|
||||
zc *= s;
|
||||
s = (float)Math.Sqrt(za * za + zc * zc);
|
||||
if (data.transformMode == TransformMode.NoScale
|
||||
&& (pa * pd - pb * pc < 0) != (skeleton.scaleX < 0 != skeleton.scaleY < 0)) s = -s;
|
||||
|
||||
float r = MathUtils.PI / 2 + MathUtils.Atan2(zc, za);
|
||||
float zb = MathUtils.Cos(r) * s;
|
||||
float zd = MathUtils.Sin(r) * s;
|
||||
@@ -242,26 +231,18 @@ namespace SpineRuntime36 {
|
||||
float lb = MathUtils.CosDeg(90 + shearY) * scaleY;
|
||||
float lc = MathUtils.SinDeg(shearX) * scaleX;
|
||||
float ld = MathUtils.SinDeg(90 + shearY) * scaleY;
|
||||
if (data.transformMode != TransformMode.NoScaleOrReflection? pa * pd - pb* pc< 0 : skeleton.flipX != skeleton.flipY) {
|
||||
zb = -zb;
|
||||
zd = -zd;
|
||||
}
|
||||
a = za * la + zb * lc;
|
||||
b = za * lb + zb * ld;
|
||||
c = zc * la + zd * lc;
|
||||
d = zc * lb + zd * ld;
|
||||
return;
|
||||
d = zc * lb + zd * ld;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (skeleton.flipX) {
|
||||
a = -a;
|
||||
b = -b;
|
||||
}
|
||||
if (skeleton.flipY != Bone.yDown) {
|
||||
c = -c;
|
||||
d = -d;
|
||||
}
|
||||
a *= skeleton.scaleX;
|
||||
b *= skeleton.scaleX;
|
||||
c *= skeleton.scaleY;
|
||||
d *= skeleton.scaleY;
|
||||
}
|
||||
|
||||
public void SetToSetupPose () {
|
||||
|
||||
@@ -45,8 +45,8 @@ namespace SpineRuntime36 {
|
||||
internal Skin skin;
|
||||
internal float r = 1, g = 1, b = 1, a = 1;
|
||||
internal float time;
|
||||
internal bool flipX, flipY;
|
||||
internal float x, y;
|
||||
internal float scaleX = 1, scaleY = 1;
|
||||
internal float x, y;
|
||||
|
||||
public SkeletonData Data { get { return data; } }
|
||||
public ExposedList<Bone> Bones { get { return bones; } }
|
||||
@@ -64,10 +64,16 @@ namespace SpineRuntime36 {
|
||||
public float Time { get { return time; } set { time = value; } }
|
||||
public float X { get { return x; } set { x = value; } }
|
||||
public float Y { get { return y; } set { y = value; } }
|
||||
public bool FlipX { get { return flipX; } set { flipX = value; } }
|
||||
public bool FlipY { get { return flipY; } set { flipY = value; } }
|
||||
public float ScaleX { get { return scaleX; } set { scaleX = value; } }
|
||||
public float ScaleY { get { return scaleY; } set { scaleY = value; } }
|
||||
|
||||
public Bone RootBone {
|
||||
[Obsolete("Use ScaleX instead. FlipX is when ScaleX is negative.")]
|
||||
public bool FlipX { get { return scaleX < 0; } set { scaleX = value ? -1f : 1f; } }
|
||||
|
||||
[Obsolete("Use ScaleY instead. FlipY is when ScaleY is negative.")]
|
||||
public bool FlipY { get { return scaleY < 0; } set { scaleY = value ? -1f : 1f; } }
|
||||
|
||||
public Bone RootBone {
|
||||
get { return bones.Count == 0 ? null : bones.Items[0]; }
|
||||
}
|
||||
|
||||
|
||||
@@ -5,7 +5,6 @@
|
||||
<Nullable>enable</Nullable>
|
||||
<Platforms>x64</Platforms>
|
||||
<TargetFramework>net8.0-windows</TargetFramework>
|
||||
<RuntimeIdentifier>win-x64</RuntimeIdentifier>
|
||||
<BaseOutputPath>$(SolutionDir)out</BaseOutputPath>
|
||||
<IncludeSourceRevisionInInformationalVersion>false</IncludeSourceRevisionInInformationalVersion>
|
||||
<Version>3.6.53</Version>
|
||||
|
||||
@@ -5,7 +5,6 @@
|
||||
<Nullable>enable</Nullable>
|
||||
<Platforms>x64</Platforms>
|
||||
<TargetFramework>net8.0-windows</TargetFramework>
|
||||
<RuntimeIdentifier>win-x64</RuntimeIdentifier>
|
||||
<BaseOutputPath>$(SolutionDir)out</BaseOutputPath>
|
||||
<IncludeSourceRevisionInInformationalVersion>false</IncludeSourceRevisionInInformationalVersion>
|
||||
<Version>3.7.94</Version>
|
||||
|
||||
@@ -5,7 +5,6 @@
|
||||
<Nullable>enable</Nullable>
|
||||
<Platforms>x64</Platforms>
|
||||
<TargetFramework>net8.0-windows</TargetFramework>
|
||||
<RuntimeIdentifier>win-x64</RuntimeIdentifier>
|
||||
<BaseOutputPath>$(SolutionDir)out</BaseOutputPath>
|
||||
<IncludeSourceRevisionInInformationalVersion>false</IncludeSourceRevisionInInformationalVersion>
|
||||
<Version>3.8.99</Version>
|
||||
|
||||
@@ -5,7 +5,6 @@
|
||||
<Nullable>enable</Nullable>
|
||||
<Platforms>x64</Platforms>
|
||||
<TargetFramework>net8.0-windows</TargetFramework>
|
||||
<RuntimeIdentifier>win-x64</RuntimeIdentifier>
|
||||
<BaseOutputPath>$(SolutionDir)out</BaseOutputPath>
|
||||
<IncludeSourceRevisionInInformationalVersion>false</IncludeSourceRevisionInInformationalVersion>
|
||||
<Version>4.0.64</Version>
|
||||
|
||||
@@ -5,7 +5,6 @@
|
||||
<Nullable>enable</Nullable>
|
||||
<Platforms>x64</Platforms>
|
||||
<TargetFramework>net8.0-windows</TargetFramework>
|
||||
<RuntimeIdentifier>win-x64</RuntimeIdentifier>
|
||||
<BaseOutputPath>$(SolutionDir)out</BaseOutputPath>
|
||||
<IncludeSourceRevisionInInformationalVersion>false</IncludeSourceRevisionInInformationalVersion>
|
||||
<Version>4.1.54</Version>
|
||||
|
||||
@@ -5,7 +5,6 @@
|
||||
<Nullable>enable</Nullable>
|
||||
<Platforms>x64</Platforms>
|
||||
<TargetFramework>net8.0-windows</TargetFramework>
|
||||
<RuntimeIdentifier>win-x64</RuntimeIdentifier>
|
||||
<BaseOutputPath>$(SolutionDir)out</BaseOutputPath>
|
||||
<IncludeSourceRevisionInInformationalVersion>false</IncludeSourceRevisionInInformationalVersion>
|
||||
<Version>4.2.74</Version>
|
||||
|
||||
2
SpineViewer/Controls/SkelFileListBox.Designer.cs
generated
2
SpineViewer/Controls/SkelFileListBox.Designer.cs
generated
@@ -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文件";
|
||||
//
|
||||
|
||||
@@ -17,13 +17,14 @@ namespace SpineViewer.Controls
|
||||
public SkelFileListBox()
|
||||
{
|
||||
InitializeComponent();
|
||||
Items = listBox.Items;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// ListBox.Items
|
||||
/// </summary>
|
||||
public readonly ListBox.ObjectCollection Items;
|
||||
[DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
|
||||
[Browsable(false)]
|
||||
public ListBox.ObjectCollection Items { get => listBox.Items; }
|
||||
|
||||
/// <summary>
|
||||
/// 从路径列表添加
|
||||
@@ -34,14 +35,14 @@ namespace SpineViewer.Controls
|
||||
{
|
||||
if (File.Exists(path))
|
||||
{
|
||||
if (SpineHelper.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 (SpineHelper.CommonSkelSuffix.Contains(Path.GetExtension(file).ToLower()))
|
||||
if (SpineUtils.CommonSkelSuffix.Contains(Path.GetExtension(file).ToLower()))
|
||||
listBox.Items.Add(file);
|
||||
}
|
||||
}
|
||||
@@ -58,7 +59,7 @@ namespace SpineViewer.Controls
|
||||
{
|
||||
foreach (var file in Directory.EnumerateFiles(path, "*.*", SearchOption.AllDirectories))
|
||||
{
|
||||
if (SpineHelper.CommonSkelSuffix.Contains(Path.GetExtension(file).ToLower()))
|
||||
if (SpineUtils.CommonSkelSuffix.Contains(Path.GetExtension(file).ToLower()))
|
||||
listBox.Items.Add(file);
|
||||
}
|
||||
}
|
||||
|
||||
95
SpineViewer/Controls/SpineListView.Designer.cs
generated
95
SpineViewer/Controls/SpineListView.Designer.cs
generated
@@ -54,7 +54,13 @@
|
||||
toolStripMenuItem_DetailsView = new ToolStripMenuItem();
|
||||
imageList_LargeIcon = new ImageList(components);
|
||||
imageList_SmallIcon = new ImageList(components);
|
||||
timer_SelectedIndexChangedDebounce = new System.Windows.Forms.Timer(components);
|
||||
statusStrip = new StatusStrip();
|
||||
toolStripStatusLabel_CountInfo = new ToolStripStatusLabel();
|
||||
tableLayoutPanel = new TableLayoutPanel();
|
||||
contextMenuStrip.SuspendLayout();
|
||||
statusStrip.SuspendLayout();
|
||||
tableLayoutPanel.SuspendLayout();
|
||||
SuspendLayout();
|
||||
//
|
||||
// listView
|
||||
@@ -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;
|
||||
@@ -91,21 +98,21 @@
|
||||
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, 418);
|
||||
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,28 +176,28 @@
|
||||
//
|
||||
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);
|
||||
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(328, 30);
|
||||
toolStripMenuItem_AddFromClipboard.Size = new Size(254, 30);
|
||||
toolStripMenuItem_AddFromClipboard.Text = "从剪贴板添加";
|
||||
toolStripMenuItem_AddFromClipboard.Click += toolStripMenuItem_AddFromClipboard_Click;
|
||||
//
|
||||
@@ -198,20 +205,20 @@
|
||||
//
|
||||
toolStripMenuItem_SelectAll.Name = "toolStripMenuItem_SelectAll";
|
||||
toolStripMenuItem_SelectAll.ShortcutKeys = Keys.Control | Keys.A;
|
||||
toolStripMenuItem_SelectAll.Size = new Size(328, 30);
|
||||
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
|
||||
@@ -250,14 +257,56 @@
|
||||
imageList_SmallIcon.ImageSize = new Size(48, 48);
|
||||
imageList_SmallIcon.TransparentColor = Color.Transparent;
|
||||
//
|
||||
// timer_SelectedIndexChangedDebounce
|
||||
//
|
||||
timer_SelectedIndexChangedDebounce.Interval = 30;
|
||||
timer_SelectedIndexChangedDebounce.Tick += timer_SelectedIndexChangedDebounce_Tick;
|
||||
//
|
||||
// statusStrip
|
||||
//
|
||||
statusStrip.Dock = DockStyle.Fill;
|
||||
statusStrip.ImageScalingSize = new Size(24, 24);
|
||||
statusStrip.Items.AddRange(new ToolStripItem[] { toolStripStatusLabel_CountInfo });
|
||||
statusStrip.Location = new Point(0, 414);
|
||||
statusStrip.Name = "statusStrip";
|
||||
statusStrip.Size = new Size(336, 31);
|
||||
statusStrip.SizingGrip = false;
|
||||
statusStrip.TabIndex = 2;
|
||||
statusStrip.Text = "statusStrip1";
|
||||
//
|
||||
// toolStripStatusLabel_CountInfo
|
||||
//
|
||||
toolStripStatusLabel_CountInfo.Name = "toolStripStatusLabel_CountInfo";
|
||||
toolStripStatusLabel_CountInfo.Size = new Size(178, 24);
|
||||
toolStripStatusLabel_CountInfo.Text = "已选择 0 项,共 0 项";
|
||||
//
|
||||
// tableLayoutPanel
|
||||
//
|
||||
tableLayoutPanel.ColumnCount = 1;
|
||||
tableLayoutPanel.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 100F));
|
||||
tableLayoutPanel.Controls.Add(listView, 0, 0);
|
||||
tableLayoutPanel.Controls.Add(statusStrip, 0, 1);
|
||||
tableLayoutPanel.Dock = DockStyle.Fill;
|
||||
tableLayoutPanel.Location = new Point(0, 0);
|
||||
tableLayoutPanel.Name = "tableLayoutPanel";
|
||||
tableLayoutPanel.RowCount = 2;
|
||||
tableLayoutPanel.RowStyles.Add(new RowStyle(SizeType.Percent, 100F));
|
||||
tableLayoutPanel.RowStyles.Add(new RowStyle());
|
||||
tableLayoutPanel.Size = new Size(336, 445);
|
||||
tableLayoutPanel.TabIndex = 3;
|
||||
//
|
||||
// SpineListView
|
||||
//
|
||||
AutoScaleDimensions = new SizeF(11F, 24F);
|
||||
AutoScaleMode = AutoScaleMode.Font;
|
||||
Controls.Add(listView);
|
||||
Controls.Add(tableLayoutPanel);
|
||||
Name = "SpineListView";
|
||||
Size = new Size(336, 445);
|
||||
contextMenuStrip.ResumeLayout(false);
|
||||
statusStrip.ResumeLayout(false);
|
||||
statusStrip.PerformLayout();
|
||||
tableLayoutPanel.ResumeLayout(false);
|
||||
tableLayoutPanel.PerformLayout();
|
||||
ResumeLayout(false);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,24 +13,33 @@ 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>
|
||||
private readonly Logger logger = LogManager.GetCurrentClassLogger();
|
||||
|
||||
/// <summary>
|
||||
/// Spine 列表只读视图, 访问时必须使用 lock 语句锁定视图本身
|
||||
/// </summary>
|
||||
public readonly ReadOnlyCollection<Spine.Spine> Spines;
|
||||
public readonly ReadOnlyCollection<SpineObject> Spines;
|
||||
|
||||
/// <summary>
|
||||
/// Spine 列表, 访问时必须使用 lock 语句锁定只读视图 Spines
|
||||
/// </summary>
|
||||
private readonly List<Spine.Spine> spines = [];
|
||||
private readonly List<SpineObject> spines = [];
|
||||
|
||||
/// <summary>
|
||||
/// 日志器
|
||||
/// 用于属性页显示模型参数的包装类
|
||||
/// </summary>
|
||||
protected readonly Logger logger = LogManager.GetCurrentClassLogger();
|
||||
private readonly Dictionary<string, SpineObjectProperty> spinePropertyWrappers = [];
|
||||
|
||||
public SpineListView()
|
||||
{
|
||||
@@ -41,8 +50,8 @@ namespace SpineViewer.Controls
|
||||
/// <summary>
|
||||
/// 显示骨骼信息的属性面板
|
||||
/// </summary>
|
||||
[Category("自定义"), Description("用于显示骨骼属性的属性页")]
|
||||
public PropertyGrid? PropertyGrid { get; set; }
|
||||
[Category("自定义"), Description("用于显示模型属性的组合属性页")]
|
||||
public SpineViewPropertyGrid? SpinePropertyGrid { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 选中的索引
|
||||
@@ -60,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);
|
||||
}
|
||||
|
||||
@@ -72,19 +80,17 @@ namespace SpineViewer.Controls
|
||||
{
|
||||
try
|
||||
{
|
||||
var spine = Spine.Spine.New(result.Version, result.SkelPath, result.AtlasPath);
|
||||
var 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 });
|
||||
|
||||
// 选中新增项
|
||||
@@ -95,7 +101,7 @@ namespace SpineViewer.Controls
|
||||
{
|
||||
logger.Error(ex.ToString());
|
||||
logger.Error("Failed to load {} {}", result.SkelPath, result.AtlasPath);
|
||||
MessageBox.Error(ex.ToString(), "骨骼加载失败");
|
||||
MessagePopup.Error(ex.ToString(), "骨骼加载失败");
|
||||
}
|
||||
|
||||
logger.LogCurrentProcessMemoryUsage();
|
||||
@@ -107,8 +113,7 @@ 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);
|
||||
}
|
||||
|
||||
@@ -150,9 +155,10 @@ namespace SpineViewer.Controls
|
||||
|
||||
try
|
||||
{
|
||||
var spine = Spine.Spine.New(version, skelPath);
|
||||
var spine = SpineObject.New(version, skelPath);
|
||||
var preview = spine.Preview;
|
||||
lock (Spines) { spines.Add(spine); }
|
||||
spinePropertyWrappers[spine.ID] = new(spine);
|
||||
listView.Invoke(() =>
|
||||
{
|
||||
listView.SmallImageList.Images.Add(spine.ID, preview);
|
||||
@@ -199,14 +205,14 @@ namespace SpineViewer.Controls
|
||||
{
|
||||
if (File.Exists(path))
|
||||
{
|
||||
if (SpineHelper.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 (SpineHelper.CommonSkelSuffix.Contains(Path.GetExtension(file).ToLower()))
|
||||
if (SpineUtils.CommonSkelSuffix.Contains(Path.GetExtension(file).ToLower()))
|
||||
validPaths.Add(file);
|
||||
}
|
||||
}
|
||||
@@ -216,7 +222,7 @@ 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(SpineVersion.Auto, validPaths.ToArray()));
|
||||
@@ -228,17 +234,29 @@ namespace SpineViewer.Controls
|
||||
}
|
||||
|
||||
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
|
||||
@@ -257,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)
|
||||
@@ -391,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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -488,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();
|
||||
@@ -496,11 +519,12 @@ 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)
|
||||
@@ -558,4 +582,9 @@ namespace SpineViewer.Controls
|
||||
listView.View = View.Details;
|
||||
}
|
||||
}
|
||||
|
||||
public class DefaultSpineConfig
|
||||
{
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
@@ -1,6 +1,6 @@
|
||||
namespace SpineViewer.Controls
|
||||
{
|
||||
partial class SpinePreviewer
|
||||
partial class SpinePreviewPanel
|
||||
{
|
||||
/// <summary>
|
||||
/// 必需的设计器变量。
|
||||
@@ -29,10 +29,9 @@
|
||||
private void InitializeComponent()
|
||||
{
|
||||
components = new System.ComponentModel.Container();
|
||||
System.ComponentModel.ComponentResourceManager resources = new System.ComponentModel.ComponentResourceManager(typeof(SpinePreviewer));
|
||||
panel = new Panel();
|
||||
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);
|
||||
@@ -40,31 +39,37 @@
|
||||
button_Start = new Button();
|
||||
button_ForwardStep = new Button();
|
||||
button_ForwardFast = new Button();
|
||||
button_FullScreen = new Button();
|
||||
panel_ViewContainer = new Panel();
|
||||
panel_RenderContainer = new Panel();
|
||||
toolTip = new ToolTip(components);
|
||||
spinePreviewFullScreenForm = new SpineViewer.Forms.SpinePreviewFullScreenForm();
|
||||
wallpaperForm = new WallpaperForm();
|
||||
tableLayoutPanel1.SuspendLayout();
|
||||
panel_Container.SuspendLayout();
|
||||
flowLayoutPanel1.SuspendLayout();
|
||||
panel_ViewContainer.SuspendLayout();
|
||||
panel_RenderContainer.SuspendLayout();
|
||||
SuspendLayout();
|
||||
//
|
||||
// panel
|
||||
// panel_Render
|
||||
//
|
||||
panel.BackColor = SystemColors.ControlDarkDark;
|
||||
panel.Location = new Point(157, 136);
|
||||
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;
|
||||
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.Controls.Add(panel_ViewContainer, 0, 0);
|
||||
tableLayoutPanel1.Dock = DockStyle.Fill;
|
||||
tableLayoutPanel1.Location = new Point(0, 0);
|
||||
tableLayoutPanel1.Margin = new Padding(0);
|
||||
@@ -75,17 +80,6 @@
|
||||
tableLayoutPanel1.Size = new Size(641, 636);
|
||||
tableLayoutPanel1.TabIndex = 2;
|
||||
//
|
||||
// panel_Container
|
||||
//
|
||||
panel_Container.BackColor = SystemColors.ControlDark;
|
||||
panel_Container.Controls.Add(panel);
|
||||
panel_Container.Dock = DockStyle.Fill;
|
||||
panel_Container.Location = new Point(0, 0);
|
||||
panel_Container.Margin = new Padding(0);
|
||||
panel_Container.Name = "panel_Container";
|
||||
panel_Container.Size = new Size(641, 594);
|
||||
panel_Container.TabIndex = 0;
|
||||
//
|
||||
// flowLayoutPanel1
|
||||
//
|
||||
flowLayoutPanel1.Anchor = AnchorStyles.None;
|
||||
@@ -96,10 +90,11 @@
|
||||
flowLayoutPanel1.Controls.Add(button_Start);
|
||||
flowLayoutPanel1.Controls.Add(button_ForwardStep);
|
||||
flowLayoutPanel1.Controls.Add(button_ForwardFast);
|
||||
flowLayoutPanel1.Location = new Point(138, 594);
|
||||
flowLayoutPanel1.Controls.Add(button_FullScreen);
|
||||
flowLayoutPanel1.Location = new Point(101, 594);
|
||||
flowLayoutPanel1.Margin = new Padding(0);
|
||||
flowLayoutPanel1.Name = "flowLayoutPanel1";
|
||||
flowLayoutPanel1.Size = new Size(365, 42);
|
||||
flowLayoutPanel1.Size = new Size(438, 42);
|
||||
flowLayoutPanel1.TabIndex = 1;
|
||||
//
|
||||
// button_Stop
|
||||
@@ -122,18 +117,19 @@
|
||||
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(0, "arrows-maximize");
|
||||
imageList.Images.SetKeyName(1, "forward-fast");
|
||||
imageList.Images.SetKeyName(2, "forward-step");
|
||||
imageList.Images.SetKeyName(3, "pause");
|
||||
imageList.Images.SetKeyName(4, "forward-step");
|
||||
imageList.Images.SetKeyName(5, "forward-fast");
|
||||
imageList.Images.SetKeyName(4, "rotate-left");
|
||||
imageList.Images.SetKeyName(5, "start");
|
||||
imageList.Images.SetKeyName(6, "stop");
|
||||
//
|
||||
// button_Restart
|
||||
//
|
||||
button_Restart.AutoSize = true;
|
||||
button_Restart.AutoSizeMode = AutoSizeMode.GrowAndShrink;
|
||||
button_Restart.ImageKey = "restart";
|
||||
button_Restart.ImageKey = "rotate-left";
|
||||
button_Restart.ImageList = imageList;
|
||||
button_Restart.Location = new Point(76, 3);
|
||||
button_Restart.Name = "button_Restart";
|
||||
@@ -190,27 +186,95 @@
|
||||
button_ForwardFast.UseVisualStyleBackColor = true;
|
||||
button_ForwardFast.Click += button_ForwardFast_Click;
|
||||
//
|
||||
// SpinePreviewer
|
||||
// button_FullScreen
|
||||
//
|
||||
button_FullScreen.AutoSize = true;
|
||||
button_FullScreen.AutoSizeMode = AutoSizeMode.GrowAndShrink;
|
||||
button_FullScreen.ImageKey = "arrows-maximize";
|
||||
button_FullScreen.ImageList = imageList;
|
||||
button_FullScreen.Location = new Point(368, 3);
|
||||
button_FullScreen.Name = "button_FullScreen";
|
||||
button_FullScreen.Padding = new Padding(15, 3, 15, 3);
|
||||
button_FullScreen.Size = new Size(67, 36);
|
||||
button_FullScreen.TabIndex = 5;
|
||||
toolTip.SetToolTip(button_FullScreen, "全屏预览");
|
||||
button_FullScreen.UseVisualStyleBackColor = true;
|
||||
button_FullScreen.Click += button_FullScreen_Click;
|
||||
//
|
||||
// panel_ViewContainer
|
||||
//
|
||||
panel_ViewContainer.Controls.Add(panel_RenderContainer);
|
||||
panel_ViewContainer.Dock = DockStyle.Fill;
|
||||
panel_ViewContainer.Location = new Point(0, 0);
|
||||
panel_ViewContainer.Margin = new Padding(0);
|
||||
panel_ViewContainer.Name = "panel_ViewContainer";
|
||||
panel_ViewContainer.Size = new Size(641, 594);
|
||||
panel_ViewContainer.TabIndex = 6;
|
||||
//
|
||||
// panel_RenderContainer
|
||||
//
|
||||
panel_RenderContainer.BackColor = SystemColors.ControlDark;
|
||||
panel_RenderContainer.Controls.Add(panel_Render);
|
||||
panel_RenderContainer.Dock = DockStyle.Fill;
|
||||
panel_RenderContainer.Location = new Point(0, 0);
|
||||
panel_RenderContainer.Margin = new Padding(0);
|
||||
panel_RenderContainer.Name = "panel_RenderContainer";
|
||||
panel_RenderContainer.Size = new Size(641, 594);
|
||||
panel_RenderContainer.TabIndex = 0;
|
||||
panel_RenderContainer.SizeChanged += panel_RenderContainer_SizeChanged;
|
||||
//
|
||||
// spinePreviewFullScreenForm
|
||||
//
|
||||
spinePreviewFullScreenForm.ClientSize = new Size(2560, 1440);
|
||||
spinePreviewFullScreenForm.ControlBox = false;
|
||||
spinePreviewFullScreenForm.FormBorderStyle = FormBorderStyle.None;
|
||||
spinePreviewFullScreenForm.MaximizeBox = false;
|
||||
spinePreviewFullScreenForm.MinimizeBox = false;
|
||||
spinePreviewFullScreenForm.Name = "SpinePreviewFullScreenForm";
|
||||
spinePreviewFullScreenForm.ShowIcon = false;
|
||||
spinePreviewFullScreenForm.ShowInTaskbar = false;
|
||||
spinePreviewFullScreenForm.StartPosition = FormStartPosition.Manual;
|
||||
spinePreviewFullScreenForm.TopMost = true;
|
||||
spinePreviewFullScreenForm.Visible = false;
|
||||
spinePreviewFullScreenForm.FormClosing += spinePreviewFullScreenForm_FormClosing;
|
||||
spinePreviewFullScreenForm.KeyDown += spinePreviewFullScreenForm_KeyDown;
|
||||
//
|
||||
// wallpaperForm
|
||||
//
|
||||
wallpaperForm.ClientSize = new Size(0, 0);
|
||||
wallpaperForm.ControlBox = false;
|
||||
wallpaperForm.FormBorderStyle = FormBorderStyle.None;
|
||||
wallpaperForm.MaximizeBox = false;
|
||||
wallpaperForm.MinimizeBox = false;
|
||||
wallpaperForm.Name = "WallpaperForm";
|
||||
wallpaperForm.ShowIcon = false;
|
||||
wallpaperForm.ShowInTaskbar = false;
|
||||
wallpaperForm.StartPosition = FormStartPosition.Manual;
|
||||
wallpaperForm.Visible = false;
|
||||
wallpaperForm.WindowState = FormWindowState.Minimized;
|
||||
wallpaperForm.FormClosing += wallpaperForm_FormClosing;
|
||||
//
|
||||
// SpinePreviewPanel
|
||||
//
|
||||
AutoScaleDimensions = new SizeF(11F, 24F);
|
||||
AutoScaleMode = AutoScaleMode.Font;
|
||||
Controls.Add(tableLayoutPanel1);
|
||||
Name = "SpinePreviewer";
|
||||
Name = "SpinePreviewPanel";
|
||||
Size = new Size(641, 636);
|
||||
SizeChanged += SpinePreviewer_SizeChanged;
|
||||
tableLayoutPanel1.ResumeLayout(false);
|
||||
tableLayoutPanel1.PerformLayout();
|
||||
panel_Container.ResumeLayout(false);
|
||||
flowLayoutPanel1.ResumeLayout(false);
|
||||
flowLayoutPanel1.PerformLayout();
|
||||
panel_ViewContainer.ResumeLayout(false);
|
||||
panel_RenderContainer.ResumeLayout(false);
|
||||
ResumeLayout(false);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
private Panel panel;
|
||||
private Panel panel_Render;
|
||||
private TableLayoutPanel tableLayoutPanel1;
|
||||
private Panel panel_Container;
|
||||
private Panel panel_RenderContainer;
|
||||
private FlowLayoutPanel flowLayoutPanel1;
|
||||
private Button button_Stop;
|
||||
private Button button_Start;
|
||||
@@ -219,5 +283,9 @@
|
||||
private Button button_ForwardStep;
|
||||
private Button button_ForwardFast;
|
||||
private Button button_Restart;
|
||||
private Button button_FullScreen;
|
||||
private Panel panel_ViewContainer;
|
||||
private Forms.SpinePreviewFullScreenForm spinePreviewFullScreenForm;
|
||||
private WallpaperForm wallpaperForm;
|
||||
}
|
||||
}
|
||||
@@ -9,11 +9,24 @@ using System.Threading.Tasks;
|
||||
using System.Windows.Forms;
|
||||
using System.Security.Policy;
|
||||
using System.Diagnostics;
|
||||
using NLog;
|
||||
using SpineViewer.Utils;
|
||||
using System.Drawing.Design;
|
||||
|
||||
namespace SpineViewer.Controls
|
||||
{
|
||||
public partial class SpinePreviewer : UserControl
|
||||
public partial class SpinePreviewPanel : UserControl
|
||||
{
|
||||
public SpinePreviewPanel()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 日志器
|
||||
/// </summary>
|
||||
private readonly Logger logger = LogManager.GetCurrentClassLogger();
|
||||
|
||||
/// <summary>
|
||||
/// 要绑定的 Spine 列表控件
|
||||
/// </summary>
|
||||
@@ -31,57 +44,12 @@ namespace SpineViewer.Controls
|
||||
{
|
||||
propertyGrid = value;
|
||||
if (propertyGrid is not null)
|
||||
propertyGrid.SelectedObject = new PreviewerProperty(this);
|
||||
propertyGrid.SelectedObject = new SpinePreviewPanelProperty(this);
|
||||
}
|
||||
}
|
||||
private PropertyGrid? propertyGrid;
|
||||
|
||||
#region 画面参数
|
||||
|
||||
/// <summary>
|
||||
/// 画面缩放最大值
|
||||
/// </summary>
|
||||
public const float ZOOM_MAX = 1000f;
|
||||
|
||||
/// <summary>
|
||||
/// 画面缩放最小值
|
||||
/// </summary>
|
||||
public const float ZOOM_MIN = 0.001f;
|
||||
|
||||
/// <summary>
|
||||
/// 包装类, 用于属性面板显示
|
||||
/// </summary>
|
||||
private class PreviewerProperty(SpinePreviewer previewer)
|
||||
{
|
||||
[TypeConverter(typeof(SizeConverter))]
|
||||
[Category("[0] 导出"), DisplayName("分辨率")]
|
||||
public Size Resolution { get => previewer.Resolution; set => previewer.Resolution = value; }
|
||||
|
||||
[TypeConverter(typeof(PointFConverter))]
|
||||
[Category("[0] 导出"), DisplayName("画面中心点")]
|
||||
public PointF Center { get => previewer.Center; set => previewer.Center = value; }
|
||||
|
||||
[Category("[0] 导出"), DisplayName("缩放")]
|
||||
public float Zoom { get => previewer.Zoom; set => previewer.Zoom = value; }
|
||||
|
||||
[Category("[0] 导出"), DisplayName("旋转")]
|
||||
public float Rotation { get => previewer.Rotation; set => previewer.Rotation = value; }
|
||||
|
||||
[Category("[0] 导出"), DisplayName("水平翻转")]
|
||||
public bool FlipX { get => previewer.FlipX; set => previewer.FlipX = value; }
|
||||
|
||||
[Category("[0] 导出"), DisplayName("垂直翻转")]
|
||||
public bool FlipY { get => previewer.FlipY; set => previewer.FlipY = value; }
|
||||
|
||||
[Category("[0] 导出"), DisplayName("仅渲染选中")]
|
||||
public bool RenderSelectedOnly { get => previewer.RenderSelectedOnly; set => previewer.RenderSelectedOnly = value; }
|
||||
|
||||
[Category("[1] 预览"), DisplayName("显示坐标轴")]
|
||||
public bool ShowAxis { get => previewer.ShowAxis; set => previewer.ShowAxis = value; }
|
||||
|
||||
[Category("[1] 预览"), DisplayName("最大帧率")]
|
||||
public uint MaxFps { get => previewer.MaxFps; set => previewer.MaxFps = value; }
|
||||
}
|
||||
#region 参数属性
|
||||
|
||||
/// <summary>
|
||||
/// 分辨率
|
||||
@@ -93,42 +61,38 @@ namespace SpineViewer.Controls
|
||||
get => resolution;
|
||||
set
|
||||
{
|
||||
if (renderWindow is null) return;
|
||||
|
||||
if (value == resolution) return;
|
||||
if (value.Width <= 0) value.Width = 100;
|
||||
if (value.Height <= 0) value.Height = 100;
|
||||
|
||||
float parentX = Width;
|
||||
float parentY = Height;
|
||||
float sizeX = value.Width;
|
||||
float sizeY = value.Height;
|
||||
var previousZoom = Zoom;
|
||||
|
||||
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 = value.Width;
|
||||
float renderH = value.Height;
|
||||
float scale = Math.Min(parentW / renderW, parentH / renderH); // 两方向取较小值, 保证 parent 覆盖 render
|
||||
renderW *= scale;
|
||||
renderH *= scale;
|
||||
|
||||
// 必须通过 SFML 的方法调整窗口
|
||||
RenderWindow.Position = new((int)(parentX - sizeX) / 2, (int)(parentY - sizeY) / 2);
|
||||
RenderWindow.Size = new((uint)sizeX, (uint)sizeY);
|
||||
|
||||
// 将 view 的大小设置成于 resolution 相同的大小, 其余属性都不变
|
||||
var view = RenderWindow.GetView();
|
||||
var signX = Math.Sign(view.Size.X);
|
||||
var signY = Math.Sign(view.Size.Y);
|
||||
view.Size = new(value.Width * signX, value.Height * signY);
|
||||
RenderWindow.SetView(view);
|
||||
|
||||
renderWindow.Position = new((int)((parentW - renderW) / 2 + 0.5), (int)((parentH - renderH) / 2 + 0.5));
|
||||
renderWindow.Size = new((uint)(renderW + 0.5), (uint)(renderH + 0.5));
|
||||
resolution = value;
|
||||
|
||||
// 设置完 resolution 后还原缩放比例
|
||||
Zoom = previousZoom;
|
||||
|
||||
// 设置壁纸窗口分辨率
|
||||
using var view = renderWindow.GetView();
|
||||
wallpaperWindow.SetView(view);
|
||||
wallpaperForm.Size = value; // 必须两个 Size 都设置
|
||||
wallpaperWindow.Size = new((uint)value.Width, (uint)value.Height);
|
||||
}
|
||||
}
|
||||
private Size resolution = new(0, 0);
|
||||
private Size resolution = new(100, 100);
|
||||
|
||||
/// <summary>
|
||||
/// 画面中心点
|
||||
@@ -139,14 +103,20 @@ namespace SpineViewer.Controls
|
||||
{
|
||||
get
|
||||
{
|
||||
var center = RenderWindow.GetView().Center;
|
||||
if (renderWindow is null) return new(-1, -1);
|
||||
|
||||
using var view = renderWindow.GetView();
|
||||
var center = view.Center;
|
||||
return new(center.X, center.Y);
|
||||
}
|
||||
set
|
||||
{
|
||||
var view = RenderWindow.GetView();
|
||||
if (renderWindow is null) return;
|
||||
|
||||
using var view = renderWindow.GetView();
|
||||
view.Center = new(value.X, value.Y);
|
||||
RenderWindow.SetView(view);
|
||||
renderWindow.SetView(view);
|
||||
wallpaperWindow.SetView(view);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -157,15 +127,24 @@ namespace SpineViewer.Controls
|
||||
[Browsable(false)]
|
||||
public float Zoom
|
||||
{
|
||||
get => resolution.Width / Math.Abs(RenderWindow.GetView().Size.X);
|
||||
get
|
||||
{
|
||||
if (renderWindow is null) return -1;
|
||||
|
||||
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();
|
||||
if (renderWindow is null) return;
|
||||
|
||||
value = Math.Clamp(value, 0.001f, 1000f);
|
||||
using var view = renderWindow.GetView();
|
||||
var signX = Math.Sign(view.Size.X);
|
||||
var signY = Math.Sign(view.Size.Y);
|
||||
view.Size = new(resolution.Width / value * signX, resolution.Height / value * signY);
|
||||
RenderWindow.SetView(view);
|
||||
renderWindow.SetView(view);
|
||||
wallpaperWindow.SetView(view);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -176,12 +155,21 @@ namespace SpineViewer.Controls
|
||||
[DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
|
||||
public float Rotation
|
||||
{
|
||||
get => RenderWindow.GetView().Rotation;
|
||||
get
|
||||
{
|
||||
if (renderWindow is null) return -1;
|
||||
|
||||
using var view = renderWindow.GetView();
|
||||
return view.Rotation;
|
||||
}
|
||||
set
|
||||
{
|
||||
var view = RenderWindow.GetView();
|
||||
if (renderWindow is null) return;
|
||||
|
||||
using var view = renderWindow.GetView();
|
||||
view.Rotation = value;
|
||||
RenderWindow.SetView(view);
|
||||
renderWindow.SetView(view);
|
||||
wallpaperWindow.SetView(view);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -192,15 +180,24 @@ namespace SpineViewer.Controls
|
||||
[Browsable(false)]
|
||||
public bool FlipX
|
||||
{
|
||||
get => RenderWindow.GetView().Size.X < 0;
|
||||
get
|
||||
{
|
||||
if (renderWindow is null) return false;
|
||||
|
||||
using var view = renderWindow.GetView();
|
||||
return view.Size.X < 0;
|
||||
}
|
||||
set
|
||||
{
|
||||
var view = RenderWindow.GetView();
|
||||
if (renderWindow is null) return;
|
||||
|
||||
using var view = renderWindow.GetView();
|
||||
var size = view.Size;
|
||||
if (size.X > 0 && value || size.X < 0 && !value)
|
||||
size.X *= -1;
|
||||
view.Size = size;
|
||||
RenderWindow.SetView(view);
|
||||
renderWindow.SetView(view);
|
||||
wallpaperWindow.SetView(view);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -211,15 +208,24 @@ namespace SpineViewer.Controls
|
||||
[Browsable(false)]
|
||||
public bool FlipY
|
||||
{
|
||||
get => RenderWindow.GetView().Size.Y < 0;
|
||||
get
|
||||
{
|
||||
if (renderWindow is null) return false;
|
||||
|
||||
using var view = renderWindow.GetView();
|
||||
return view.Size.Y < 0;
|
||||
}
|
||||
set
|
||||
{
|
||||
var view = RenderWindow.GetView();
|
||||
if (renderWindow is null) return;
|
||||
|
||||
using var view = renderWindow.GetView();
|
||||
var size = view.Size;
|
||||
if (size.Y > 0 && value || size.Y < 0 && !value)
|
||||
size.Y *= -1;
|
||||
view.Size = size;
|
||||
RenderWindow.SetView(view);
|
||||
renderWindow.SetView(view);
|
||||
wallpaperWindow.SetView(view);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -242,35 +248,88 @@ 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
|
||||
{
|
||||
if (renderWindow is null) return;
|
||||
|
||||
renderWindow.SetFramerateLimit(value);
|
||||
maxFps = value;
|
||||
}
|
||||
}
|
||||
private uint maxFps = 60;
|
||||
|
||||
/// <summary>
|
||||
/// 获取 View
|
||||
/// </summary>
|
||||
public SFML.Graphics.View GetView() => RenderWindow.GetView();
|
||||
public SFML.Graphics.View GetView() => renderWindow.GetView();
|
||||
|
||||
/// <summary>
|
||||
/// 是否开启桌面投影
|
||||
/// </summary>
|
||||
[DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
|
||||
[Browsable(false)]
|
||||
public bool EnableDesktopProjection
|
||||
{
|
||||
get => enableDesktopProjection;
|
||||
set
|
||||
{
|
||||
if (renderWindow is null) return;
|
||||
|
||||
if (enableDesktopProjection == value) return;
|
||||
if (value)
|
||||
{
|
||||
var screenBounds = Screen.FromControl(this).Bounds;
|
||||
Resolution = screenBounds.Size;
|
||||
wallpaperWindow.Position = new(screenBounds.X, screenBounds.Y);
|
||||
wallpaperForm.Show();
|
||||
}
|
||||
else
|
||||
{
|
||||
wallpaperForm.Hide();
|
||||
}
|
||||
enableDesktopProjection = value;
|
||||
}
|
||||
}
|
||||
private bool enableDesktopProjection = false;
|
||||
|
||||
/// <summary>
|
||||
/// 预览画面背景色
|
||||
/// </summary>
|
||||
[DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
|
||||
[Browsable(false)]
|
||||
public SFML.Graphics.Color BackgroundColor { get; set; } = new(105, 105, 105);
|
||||
|
||||
#endregion
|
||||
|
||||
public SpinePreviewer()
|
||||
{
|
||||
InitializeComponent();
|
||||
RenderWindow = new(panel.Handle);
|
||||
RenderWindow.SetActive(false);
|
||||
#region 渲染管理
|
||||
|
||||
// 设置默认参数
|
||||
Resolution = new(2048, 2048);
|
||||
Center = new(0, 0);
|
||||
FlipY = true;
|
||||
MaxFps = 30;
|
||||
}
|
||||
/// <summary>
|
||||
/// 预览画面坐标轴颜色
|
||||
/// </summary>
|
||||
private static readonly SFML.Graphics.Color AxisColor = new(220, 220, 220);
|
||||
|
||||
#region 渲染线程管理
|
||||
/// <summary>
|
||||
/// 坐标轴顶点缓冲区
|
||||
/// </summary>
|
||||
private readonly SFML.Graphics.VertexArray axisVertices = new(SFML.Graphics.PrimitiveType.Lines, 2); // XXX: 暂时未使用 Dispose 释放
|
||||
|
||||
/// <summary>
|
||||
/// 渲染窗口
|
||||
/// </summary>
|
||||
private readonly SFML.Graphics.RenderWindow RenderWindow;
|
||||
private SFML.Graphics.RenderWindow renderWindow;
|
||||
|
||||
/// <summary>
|
||||
/// 壁纸窗口
|
||||
/// </summary>
|
||||
private SFML.Graphics.RenderWindow wallpaperWindow;
|
||||
|
||||
/// <summary>
|
||||
/// 帧间隔计时器
|
||||
/// </summary>
|
||||
private readonly SFML.System.Clock clock = new();
|
||||
|
||||
/// <summary>
|
||||
/// 渲染任务
|
||||
@@ -278,39 +337,11 @@ namespace SpineViewer.Controls
|
||||
private Task? task = null;
|
||||
private CancellationTokenSource? cancelToken = null;
|
||||
|
||||
/// <summary>
|
||||
/// 开始渲染
|
||||
/// </summary>
|
||||
public void StartRender()
|
||||
{
|
||||
if (task is not null)
|
||||
return;
|
||||
cancelToken = new();
|
||||
task = Task.Run(RenderTask, cancelToken.Token);
|
||||
IsUpdating = true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 停止渲染
|
||||
/// </summary>
|
||||
public void StopRender()
|
||||
{
|
||||
IsUpdating = false;
|
||||
if (task is null || cancelToken is null)
|
||||
return;
|
||||
cancelToken.Cancel();
|
||||
task.Wait();
|
||||
cancelToken = null;
|
||||
task = null;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region 渲染更新管理
|
||||
|
||||
/// <summary>
|
||||
/// 是否更新画面
|
||||
/// </summary>
|
||||
[DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
|
||||
[Browsable(false)]
|
||||
public bool IsUpdating
|
||||
{
|
||||
get => isUpdating;
|
||||
@@ -337,24 +368,47 @@ namespace SpineViewer.Controls
|
||||
private object _forwardDeltaLock = new();
|
||||
|
||||
/// <summary>
|
||||
/// 预览画面背景色
|
||||
/// 开始渲染
|
||||
/// </summary>
|
||||
private static readonly SFML.Graphics.Color BackgroundColor = new(105, 105, 105);
|
||||
public void StartRender()
|
||||
{
|
||||
// 延迟到第一次开启渲染时进行初始化
|
||||
if (renderWindow is null)
|
||||
{
|
||||
renderWindow = new(panel_Render.Handle);
|
||||
renderWindow.SetActive(false);
|
||||
wallpaperWindow = new(wallpaperForm.Handle);
|
||||
wallpaperWindow.SetActive(false);
|
||||
|
||||
// 设置默认参数
|
||||
Resolution = new(2048, 2048);
|
||||
Zoom = 1;
|
||||
Center = new(0, 0);
|
||||
FlipY = true;
|
||||
MaxFps = 30;
|
||||
}
|
||||
|
||||
if (task is not null) return;
|
||||
cancelToken = new();
|
||||
task = Task.Run(RenderTask, cancelToken.Token);
|
||||
IsUpdating = true;
|
||||
if (enableDesktopProjection) wallpaperForm.Show();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 预览画面坐标轴颜色
|
||||
/// 停止渲染
|
||||
/// </summary>
|
||||
private static readonly SFML.Graphics.Color AxisColor = new(220, 220, 220);
|
||||
|
||||
/// <summary>
|
||||
/// 坐标轴顶点缓冲区
|
||||
/// </summary>
|
||||
private readonly SFML.Graphics.VertexArray AxisVertex = new(SFML.Graphics.PrimitiveType.Lines, 2);
|
||||
|
||||
/// <summary>
|
||||
/// 帧间隔计时器
|
||||
/// </summary>
|
||||
private readonly SFML.System.Clock Clock = new();
|
||||
public void StopRender()
|
||||
{
|
||||
wallpaperForm.Hide();
|
||||
IsUpdating = false;
|
||||
if (task is null || cancelToken is null)
|
||||
return;
|
||||
cancelToken.Cancel();
|
||||
task.Wait();
|
||||
cancelToken = null;
|
||||
task = null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 渲染任务
|
||||
@@ -363,13 +417,14 @@ namespace SpineViewer.Controls
|
||||
{
|
||||
try
|
||||
{
|
||||
RenderWindow.SetActive(true);
|
||||
renderWindow.SetActive(true);
|
||||
wallpaperWindow.SetActive(true);
|
||||
|
||||
float delta;
|
||||
while (cancelToken is not null && !cancelToken.IsCancellationRequested)
|
||||
{
|
||||
delta = Clock.ElapsedTime.AsSeconds();
|
||||
Clock.Restart();
|
||||
delta = clock.ElapsedTime.AsSeconds();
|
||||
clock.Restart();
|
||||
|
||||
// 停止更新的时候只是时间不前进, 但是坐标变换还是要更新, 否则无法移动对象
|
||||
if (!IsUpdating) delta = 0;
|
||||
@@ -381,17 +436,18 @@ namespace SpineViewer.Controls
|
||||
forwardDelta = 0;
|
||||
}
|
||||
|
||||
RenderWindow.Clear(BackgroundColor);
|
||||
renderWindow.Clear(BackgroundColor);
|
||||
if (enableDesktopProjection) wallpaperWindow.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
|
||||
@@ -412,19 +468,30 @@ namespace SpineViewer.Controls
|
||||
if (RenderSelectedOnly && !spine.IsSelected)
|
||||
continue;
|
||||
|
||||
spine.IsDebug = true;
|
||||
RenderWindow.Draw(spine);
|
||||
spine.IsDebug = false;
|
||||
spine.EnableDebug = true;
|
||||
renderWindow.Draw(spine);
|
||||
spine.EnableDebug = false;
|
||||
|
||||
if (enableDesktopProjection) wallpaperWindow.Draw(spine);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
RenderWindow.Display();
|
||||
renderWindow.Display();
|
||||
|
||||
if (enableDesktopProjection) wallpaperWindow.Display();
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.Fatal(ex.ToString());
|
||||
logger.Fatal("Render task stopped");
|
||||
MessagePopup.Error(ex.ToString(), "预览画面已停止渲染");
|
||||
}
|
||||
finally
|
||||
{
|
||||
RenderWindow.SetActive(false);
|
||||
renderWindow.SetActive(false);
|
||||
wallpaperWindow.SetActive(false);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -435,46 +502,37 @@ namespace SpineViewer.Controls
|
||||
/// </summary>
|
||||
private SFML.System.Vector2f? draggingSrc = null;
|
||||
|
||||
private void SpinePreviewer_SizeChanged(object sender, EventArgs e)
|
||||
private void panel_RenderContainer_SizeChanged(object sender, EventArgs e)
|
||||
{
|
||||
if (RenderWindow is null)
|
||||
return;
|
||||
if (renderWindow is null) return;
|
||||
|
||||
float parentX = panel.Parent.Width;
|
||||
float parentY = panel.Parent.Height;
|
||||
float sizeX = panel.Width;
|
||||
float sizeY = panel.Height;
|
||||
float parentW = panel_Render.Parent.Width;
|
||||
float parentH = panel_Render.Parent.Height;
|
||||
float renderW = panel_Render.Width;
|
||||
float renderH = panel_Render.Height;
|
||||
float scale = Math.Min(parentW / renderW, parentH / renderH); // 两方向取较小值, 保证 parent 覆盖 render
|
||||
renderW *= scale;
|
||||
renderH *= scale;
|
||||
|
||||
if ((sizeY / sizeX) < (parentY / parentX))
|
||||
{
|
||||
// 相同的 X, 子窗口 Y 更小
|
||||
sizeY = parentX * sizeY / sizeX;
|
||||
sizeX = parentX;
|
||||
}
|
||||
else
|
||||
{
|
||||
// 相同的 Y, 子窗口 X 更小
|
||||
sizeX = parentY * sizeX / sizeY;
|
||||
sizeY = parentY;
|
||||
}
|
||||
|
||||
// 必须通过 SFML 的方法调整窗口
|
||||
RenderWindow.Position = new((int)(parentX - sizeX) / 2, (int)(parentY - sizeY) / 2);
|
||||
RenderWindow.Size = new((uint)sizeX, (uint)sizeY);
|
||||
// 必须通过 SFML 的方法调整窗口, 此处必须四舍五入, 否则在全屏和小窗多次切换之后会有明显的舍入误差
|
||||
renderWindow.Position = new((int)((parentW - renderW) / 2 + 0.5), (int)((parentH - renderH) / 2 + 0.5));
|
||||
renderWindow.Size = new((uint)(renderW + 0.5), (uint)(renderH + 0.5));
|
||||
}
|
||||
|
||||
private void panel_MouseDown(object sender, MouseEventArgs e)
|
||||
private void panel_Render_MouseDown(object sender, MouseEventArgs e)
|
||||
{
|
||||
if (renderWindow is null) return;
|
||||
|
||||
// 右键优先级高, 进入画面拖动模式, 需要重新记录源点
|
||||
if ((e.Button & MouseButtons.Right) != 0)
|
||||
{
|
||||
draggingSrc = RenderWindow.MapPixelToCoords(new(e.X, e.Y));
|
||||
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)
|
||||
@@ -492,7 +550,7 @@ namespace SpineViewer.Controls
|
||||
foreach (int i in SpineListView.SelectedIndices)
|
||||
{
|
||||
if (spines[i].IsHidden) continue;
|
||||
if (!spines[i].Bounds.Contains(src)) continue;
|
||||
if (!spines[i].GetCurrentBounds().Contains(src)) continue;
|
||||
hit = true;
|
||||
break;
|
||||
}
|
||||
@@ -509,7 +567,7 @@ namespace SpineViewer.Controls
|
||||
for (int i = 0; i < spines.Count; i++)
|
||||
{
|
||||
if (spines[i].IsHidden) continue;
|
||||
if (!spines[i].Bounds.Contains(src)) continue;
|
||||
if (!spines[i].GetCurrentBounds().Contains(src)) continue;
|
||||
|
||||
hit = true;
|
||||
|
||||
@@ -531,7 +589,7 @@ namespace SpineViewer.Controls
|
||||
for (int i = 0; i < spines.Count; i++)
|
||||
{
|
||||
if (spines[i].IsHidden) continue;
|
||||
if (!spines[i].Bounds.Contains(src)) continue;
|
||||
if (!spines[i].GetCurrentBounds().Contains(src)) continue;
|
||||
|
||||
SpineListView.SelectedIndices.Add(i);
|
||||
break;
|
||||
@@ -542,13 +600,14 @@ 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;
|
||||
if (renderWindow is null) return;
|
||||
|
||||
if (draggingSrc is null) return;
|
||||
|
||||
var src = (SFML.System.Vector2f)draggingSrc;
|
||||
var dst = RenderWindow.MapPixelToCoords(new(e.X, e.Y));
|
||||
var dst = renderWindow.MapPixelToCoords(new(e.X, e.Y));
|
||||
var _delta = dst - src;
|
||||
var delta = new SizeF(_delta.X, _delta.Y);
|
||||
|
||||
@@ -574,12 +633,12 @@ namespace SpineViewer.Controls
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
@@ -589,14 +648,34 @@ 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)
|
||||
@@ -607,7 +686,7 @@ namespace SpineViewer.Controls
|
||||
lock (SpineListView.Spines)
|
||||
{
|
||||
foreach (var spine in SpineListView.Spines)
|
||||
spine.Track0Animation = spine.Track0Animation; // TODO: 多轨道重置
|
||||
spine.ResetAnimationsTime();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -619,7 +698,7 @@ namespace SpineViewer.Controls
|
||||
lock (SpineListView.Spines)
|
||||
{
|
||||
foreach (var spine in SpineListView.Spines)
|
||||
spine.Track0Animation = spine.Track0Animation; // TODO: 多轨道重置
|
||||
spine.ResetAnimationsTime();
|
||||
}
|
||||
}
|
||||
IsUpdating = true;
|
||||
@@ -634,7 +713,10 @@ namespace SpineViewer.Controls
|
||||
{
|
||||
lock (_forwardDeltaLock)
|
||||
{
|
||||
forwardDelta += 1f / maxFps;
|
||||
if (maxFps > 0)
|
||||
forwardDelta += 1f / maxFps;
|
||||
else
|
||||
forwardDelta += 0.001f;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -642,14 +724,90 @@ namespace SpineViewer.Controls
|
||||
{
|
||||
lock (_forwardDeltaLock)
|
||||
{
|
||||
forwardDelta += 10f / maxFps;
|
||||
if (maxFps > 0)
|
||||
forwardDelta += 10f / maxFps;
|
||||
else
|
||||
forwardDelta += 0.01f;
|
||||
}
|
||||
}
|
||||
|
||||
private void button_FullScreen_Click(object sender, EventArgs e)
|
||||
{
|
||||
var screenBounds = Screen.FromControl(this).Bounds;
|
||||
Resolution = screenBounds.Size;
|
||||
spinePreviewFullScreenForm.Controls.Add(panel_RenderContainer);
|
||||
spinePreviewFullScreenForm.Bounds = screenBounds;
|
||||
spinePreviewFullScreenForm.Show();
|
||||
PropertyGrid?.Refresh();
|
||||
}
|
||||
|
||||
private void spinePreviewFullScreenForm_KeyDown(object sender, KeyEventArgs e)
|
||||
{
|
||||
if (e.KeyCode == Keys.Escape)
|
||||
{
|
||||
spinePreviewFullScreenForm.Hide();
|
||||
panel_ViewContainer.Controls.Add(panel_RenderContainer);
|
||||
}
|
||||
}
|
||||
|
||||
private void spinePreviewFullScreenForm_FormClosing(object sender, FormClosingEventArgs e)
|
||||
{
|
||||
e.Cancel = e.CloseReason == CloseReason.UserClosing;
|
||||
}
|
||||
|
||||
private void wallpaperForm_FormClosing(object sender, FormClosingEventArgs e)
|
||||
{
|
||||
e.Cancel = e.CloseReason == CloseReason.UserClosing;
|
||||
}
|
||||
|
||||
//public void ClickStopButton() => button_Stop_Click(button_Stop, EventArgs.Empty);
|
||||
//public void ClickRestartButton() => button_Restart_Click(button_Restart, EventArgs.Empty);
|
||||
//public void ClickStartButton() => button_Start_Click(button_Start, EventArgs.Empty);
|
||||
//public void ClickForwardStepButton() => button_ForwardStep_Click(button_ForwardStep, EventArgs.Empty);
|
||||
//public void ClickForwardFastButton() => button_ForwardFast_Click(button_ForwardFast, EventArgs.Empty);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 用于在 PropertyGrid 上显示 <see cref="SpinePreviewPanel"/> 属性的包装类, 提供用户操作接口
|
||||
/// </summary>
|
||||
public class SpinePreviewPanelProperty(SpinePreviewPanel previewPanel)
|
||||
{
|
||||
[Browsable(false)]
|
||||
public SpinePreviewPanel PreviewPanel { get; } = previewPanel;
|
||||
|
||||
[RefreshProperties(RefreshProperties.All)]
|
||||
[TypeConverter(typeof(ResolutionConverter))]
|
||||
[Category("[0] 导出"), DisplayName("分辨率")]
|
||||
public Size Resolution { get => PreviewPanel.Resolution; set => PreviewPanel.Resolution = value; }
|
||||
|
||||
[TypeConverter(typeof(PointFConverter))]
|
||||
[Category("[0] 导出"), DisplayName("画面中心点")]
|
||||
public PointF Center { get => PreviewPanel.Center; set => PreviewPanel.Center = value; }
|
||||
|
||||
[Category("[0] 导出"), DisplayName("缩放")]
|
||||
public float Zoom { get => PreviewPanel.Zoom; set => PreviewPanel.Zoom = value; }
|
||||
|
||||
[Category("[0] 导出"), DisplayName("旋转")]
|
||||
public float Rotation { get => PreviewPanel.Rotation; set => PreviewPanel.Rotation = value; }
|
||||
|
||||
[Category("[0] 导出"), DisplayName("水平翻转")]
|
||||
public bool FlipX { get => PreviewPanel.FlipX; set => PreviewPanel.FlipX = value; }
|
||||
|
||||
[Category("[0] 导出"), DisplayName("垂直翻转")]
|
||||
public bool FlipY { get => PreviewPanel.FlipY; set => PreviewPanel.FlipY = value; }
|
||||
|
||||
[Category("[0] 导出"), DisplayName("仅渲染选中")]
|
||||
public bool RenderSelectedOnly { get => PreviewPanel.RenderSelectedOnly; set => PreviewPanel.RenderSelectedOnly = value; }
|
||||
|
||||
[Category("[1] 预览"), DisplayName("显示坐标轴")]
|
||||
public bool ShowAxis { get => PreviewPanel.ShowAxis; set => PreviewPanel.ShowAxis = value; }
|
||||
|
||||
[Category("[1] 预览"), DisplayName("最大帧率")]
|
||||
public uint MaxFps { get => PreviewPanel.MaxFps; set => PreviewPanel.MaxFps = value; }
|
||||
|
||||
[Editor(typeof(SFMLColorEditor), typeof(UITypeEditor))]
|
||||
[TypeConverter(typeof(SFMLColorConverter))]
|
||||
[Category("[1] 预览"), DisplayName("背景颜色")]
|
||||
public SFML.Graphics.Color BackgroundColor { get => PreviewPanel.BackgroundColor; set => PreviewPanel.BackgroundColor = value; }
|
||||
}
|
||||
}
|
||||
293
SpineViewer/Controls/SpinePreviewPanel.resx
Normal file
293
SpineViewer/Controls/SpinePreviewPanel.resx
Normal file
@@ -0,0 +1,293 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<root>
|
||||
<!--
|
||||
Microsoft ResX Schema
|
||||
|
||||
Version 2.0
|
||||
|
||||
The primary goals of this format is to allow a simple XML format
|
||||
that is mostly human readable. The generation and parsing of the
|
||||
various data types are done through the TypeConverter classes
|
||||
associated with the data types.
|
||||
|
||||
Example:
|
||||
|
||||
... ado.net/XML headers & schema ...
|
||||
<resheader name="resmimetype">text/microsoft-resx</resheader>
|
||||
<resheader name="version">2.0</resheader>
|
||||
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
|
||||
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
|
||||
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
|
||||
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
|
||||
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
|
||||
<value>[base64 mime encoded serialized .NET Framework object]</value>
|
||||
</data>
|
||||
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
|
||||
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
|
||||
<comment>This is a comment</comment>
|
||||
</data>
|
||||
|
||||
There are any number of "resheader" rows that contain simple
|
||||
name/value pairs.
|
||||
|
||||
Each data row contains a name, and value. The row also contains a
|
||||
type or mimetype. Type corresponds to a .NET class that support
|
||||
text/value conversion through the TypeConverter architecture.
|
||||
Classes that don't support this are serialized and stored with the
|
||||
mimetype set.
|
||||
|
||||
The mimetype is used for serialized objects, and tells the
|
||||
ResXResourceReader how to depersist the object. This is currently not
|
||||
extensible. For a given mimetype the value must be set accordingly:
|
||||
|
||||
Note - application/x-microsoft.net.object.binary.base64 is the format
|
||||
that the ResXResourceWriter will generate, however the reader can
|
||||
read any of the formats listed below.
|
||||
|
||||
mimetype: application/x-microsoft.net.object.binary.base64
|
||||
value : The object must be serialized with
|
||||
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
|
||||
: and then encoded with base64 encoding.
|
||||
|
||||
mimetype: application/x-microsoft.net.object.soap.base64
|
||||
value : The object must be serialized with
|
||||
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
|
||||
: and then encoded with base64 encoding.
|
||||
|
||||
mimetype: application/x-microsoft.net.object.bytearray.base64
|
||||
value : The object must be serialized into a byte array
|
||||
: using a System.ComponentModel.TypeConverter
|
||||
: and then encoded with base64 encoding.
|
||||
-->
|
||||
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
|
||||
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
|
||||
<xsd:element name="root" msdata:IsDataSet="true">
|
||||
<xsd:complexType>
|
||||
<xsd:choice maxOccurs="unbounded">
|
||||
<xsd:element name="metadata">
|
||||
<xsd:complexType>
|
||||
<xsd:sequence>
|
||||
<xsd:element name="value" type="xsd:string" minOccurs="0" />
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="name" use="required" type="xsd:string" />
|
||||
<xsd:attribute name="type" type="xsd:string" />
|
||||
<xsd:attribute name="mimetype" type="xsd:string" />
|
||||
<xsd:attribute ref="xml:space" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:element name="assembly">
|
||||
<xsd:complexType>
|
||||
<xsd:attribute name="alias" type="xsd:string" />
|
||||
<xsd:attribute name="name" type="xsd:string" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:element name="data">
|
||||
<xsd:complexType>
|
||||
<xsd:sequence>
|
||||
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
|
||||
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
|
||||
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
|
||||
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
|
||||
<xsd:attribute ref="xml:space" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:element name="resheader">
|
||||
<xsd:complexType>
|
||||
<xsd:sequence>
|
||||
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="name" type="xsd:string" use="required" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
</xsd:choice>
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
</xsd:schema>
|
||||
<resheader name="resmimetype">
|
||||
<value>text/microsoft-resx</value>
|
||||
</resheader>
|
||||
<resheader name="version">
|
||||
<value>2.0</value>
|
||||
</resheader>
|
||||
<resheader name="reader">
|
||||
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
||||
</resheader>
|
||||
<resheader name="writer">
|
||||
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
||||
</resheader>
|
||||
<metadata name="imageList.TrayLocation" type="System.Drawing.Point, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a">
|
||||
<value>17, 17</value>
|
||||
</metadata>
|
||||
<data name="imageList.ImageStream" mimetype="application/x-microsoft.net.object.binary.base64">
|
||||
<value>
|
||||
AAEAAAD/////AQAAAAAAAAAMAgAAAEZTeXN0ZW0uV2luZG93cy5Gb3JtcywgQ3VsdHVyZT1uZXV0cmFs
|
||||
LCBQdWJsaWNLZXlUb2tlbj1iNzdhNWM1NjE5MzRlMDg5BQEAAAAmU3lzdGVtLldpbmRvd3MuRm9ybXMu
|
||||
SW1hZ2VMaXN0U3RyZWFtZXIBAAAABERhdGEHAgIAAAAJAwAAAA8DAAAA8iMAAAJNU0Z0AUkBTAIBAQcB
|
||||
AAGQAQABkAEAAR8BAAEYAQAE/wEhAQAI/wFCAU0BNgcAATYDAAEoAwABfAMAATADAAEBAQABIAYAAV0+
|
||||
AAMEAQUDAAH/AwAB/wMAAf8DAAH/AwAB/wMAAf8DAAH/AwAB/wMAAf9DAAH/AwAB/wMAAf8DAAH/A1UB
|
||||
sWQAA1gB7wMAAf8DAAH/AwAB/wMAAf8DAAH/AwAB/wMAAf8DAAH/AwAB/wMAAf8DAAH/AwAB/wMAAf8D
|
||||
AAH/AwAB/wMAAf8DAAH/AwAB/wMAAf8DAAH/Ay0BRbcAAf8DAAH/AwAB/wMAAf8DAAH/AwAB/wMAAf8D
|
||||
AAH/AwAB/wMAAf8DAAH/AwAB/wMAAf87AAH/AwAB/wMAAf8DAAH/AwAB/wMAAf9cAANEAXgDAAH/AwAB
|
||||
/wMAAf8DAAH/AwAB/wMAAf8DAAH/AwAB/wMAAf8DAAH/AwAB/wMAAf8DAAH/AwAB/wMAAf8DAAH/AwAB
|
||||
/wMAAf8DAAH/AwAB/wMAAf8DAAH/swAB/wMAAf8DAAH/AwAB/wMAAf8XAAH/AwAB/wMAAf8DAAH/AwAB
|
||||
/zcAAf8DAAH/BwAB/wMAAf8DAAH/AwAB/wMAAf9XAAH/AwAB/wMAAf8DAAH/AwAB/wMAAf8DAAH/AwAB
|
||||
/wMAAf8DAAH/AwAB/wMAAf8DAAH/AwAB/wMAAf8DAAH/AwAB/wMAAf8DAAH/AwAB/wMAAf8DAAH/AwAB
|
||||
/wMAAf+vAAH/AwAB/wNRAaQnAAH/AwAB/wMAAf8DAAH/MwAB/wMAAf8IAANOAZcDAAH/AwAB/wMAAf8D
|
||||
AAH/Ay4BSE8AAf8DAAH/AwAB/0sAAf8DAAH/AwAB/+AAAxUBHQMAAf8DAAH/AwAB/y8AAf8DAAH/EwAB
|
||||
/wMAAf8DAAH/AwAB/wMAAf9LAAH/AwAB/wMAAf9LAAH/AwAB/wMAAf/kAAMmATgDAAH/AwAB/wMAAf8r
|
||||
AAH/AwAB/xsAAf8DAAH/AwAB/wMAAf8DAAH/QwAB/wMAAf8DAAH/SwAB/wMAAf8DAAH/6wAB/wMAAf8D
|
||||
AAH/KwAB/wMAAf8cAANCAfYDAAH/AwAB/wMAAf8DAAH/AwQBBTsAAf8DAAH/AwAB/0sAAf8DAAH/AwAB
|
||||
/+8AAf8DAAH/AwAB/ycAAf8DAAH/JwAB/wMAAf8DAAH/AwAB/wMAAf83AAH/AwAB/wMAAf9LAAH/AwAB
|
||||
/wMAAf/vAAH/AwAB/wMAAf8nAAH/AwAB/ygAAwcBCQMAAf8DAAH/AwAB/wMAAf8DYAHjLwAB/wMAAf8D
|
||||
AAH/SwAB/wMAAf8DAAH/7AADIAEtAwAB/wMAAf8nAAH/AwAB/zMAAf8DAAH/AwAB/wMAAf8DAAH/KwAB
|
||||
/wMAAf8DAAH/SwAB/wMAAf8DAAH/8wAB/wMAAf8nAAH/AwAB/zsAAf8DAAH/AwAB/wMAAf8nAAH/AwAB
|
||||
/wMAAf9LAAH/AwAB/wMAAf/zAAH/AwAB/ycAAf8DAAH/PAADPwFsAwAB/wMAAf8nAAH/AwAB/wMAAf9L
|
||||
AAH/AwAB/wMAAf/zAAH/AwAB/ycAAf8DAAH/OwAB/wMAAf8DAAH/AwAB/ycAAf8DAAH/AwAB/0sAAf8D
|
||||
AAH/AwAB/5sAAf8DAAH/AwAB/wMAAf8DAAH/AwAB/wMAAf8DAAH/NAADCAEKAwAB/wMAAf8nAAH/AwAB
|
||||
/zAAA10BzgMAAf8DAAH/AwAB/wMAAf8EAScAAf8DAAH/AwAB/0sAAf8DAAH/AwAB/5QAAwUBBgMAAf8D
|
||||
AAH/AwAB/wMAAf8DAAH/AwAB/wMAAf8DAAH/AwAB/zMAAf8DAAH/AwAB/ycAAf8DAAH/LwAB/wMAAf8D
|
||||
AAH/AwAB/wMAAf8vAAH/AwAB/wMAAf9LAAH/AwAB/wMAAf+UAAMFAQYDAAH/AwAB/wMAAf8DAAH/AwAB
|
||||
/wMAAf8DAAH/AwAB/wMAAf8zAAH/AwAB/wMAAf8nAAH/AwAB/ycAAf8DAAH/AwAB/wMAAf8DAAH/NwAB
|
||||
/wMAAf8DAAH/SwAB/wMAAf8DAAH/lAADBQEGAwAB/wMAAf8PAAH/AwAB/wMAAf8DFQEcLwAB/wMAAf8D
|
||||
AAH/KwAB/wMAAf8cAAM9AWkDAAH/AwAB/wMAAf8DAAH/A0MBdjsAAf8DAAH/AwAB/0sAAf8DAAH/AwAB
|
||||
/5QAAwUBBgMAAf8DAAH/CwAB/wMAAf8DAAH/AxMBGjMAAf8DAAH/AwAB/ysAAf8DAAH/GwAB/wMAAf8D
|
||||
AAH/AwAB/wMAAf9DAAH/AwAB/wMAAf9LAAH/AwAB/wMAAf+UAAMFAQYDAAH/AwAB/wcAAf8DAAH/AwAB
|
||||
/wMbASYzAAH/AwAB/wMAAf8vAAH/AwAB/xMAAf8DAAH/AwAB/wMAAf8DAAH/SwAB/wMAAf8DAAH/SwAB
|
||||
/wMAAf8DAAH/lAADBQEGAwAB/wMAAf8DAAH/AwAB/wMAAf8DAAH/AwAB/wMdASgkAANZAe4DAAH/AwAB
|
||||
/wMAAf8EAS8AAf8DAAH/CAADGAEhAwAB/wMAAf8DAAH/AwAB/wNcActPAAH/AwAB/wMAAf9LAAH/AwAB
|
||||
/wMAAf+UAAMFAQYDAAH/AwAB/wMAAf8DAAH/A2AB4wMAAf8DAAH/AwAB/wMAAf8DUAGfFwAB/wMAAf8D
|
||||
AAH/AwAB/wMAAf83AAH/AwAB/wcAAf8DAAH/AwAB/wMAAf8DAAH/VwAB/wMAAf8DAAH/AwAB/wMqAUAD
|
||||
KgFAAyoBQAMqAUADKgFAAyoBQAMqAUADKgFAAyoBQAMqAUADKgFAAyoBQAMqAUADKgFAAyoBQAMqAUAD
|
||||
AAH/AwAB/wMAAf8DAAH/mwAB/wMAAf8DAAH/Ax0BKQQAAwIBAwMAAf8DAAH/AwAB/wMAAf8DAAH/AwAB
|
||||
/wMAAf8DAAH/AwAB/wMAAf8DAAH/AwAB/wMAAf87AAH/AwAB/wMAAf8DAAH/AwAB/wMAAf8DBwEJWAAD
|
||||
WQHDAwAB/wMAAf8DAAH/AwAB/wMAAf8DAAH/AwAB/wMAAf8DAAH/AwAB/wMAAf8DAAH/AwAB/wMAAf8D
|
||||
AAH/AwAB/wMAAf8DAAH/AwAB/wMAAf8DAAH/AwAB/5wAAxIBFwMAAf8UAANKAYkDAAH/AwAB/wMAAf8D
|
||||
AAH/AwAB/wMAAf8DAAH/AwAB/wMAAf9DAAH/AwAB/wMAAf8DAAH/AyEB+2cAAf8DAAH/AwAB/wMAAf8D
|
||||
AAH/AwAB/wMAAf8DAAH/AwAB/wMAAf8DAAH/AwAB/wMAAf8DAAH/AwAB/wMAAf8DAAH/AwAB/wMAAf8D
|
||||
AAH/AwAB/wNbAcXIAANGAYADWgG/Ay4BSFQAA1oBv3QAA0YBgANaAb8DWgG/A1oBvwNaAb8DWgG/A1oB
|
||||
vwNaAb8DWgG/A1oBvwNaAb8DWgG/A1oBvwNaAb8DWgG/A1oBvwNaAb8DLgFIpwAB/wMAAf8DAAH/AwAB
|
||||
/wMAAf8DAAH/AwAB/wMAAf8DAAH/FAADPQFpAwAB/wMAAf8DAAH/AwAB/wMAAf8DAAH/AwAB/wMAAf8D
|
||||
AAH/EwAB/wMAAf8DAAH/LwAB/wMAAf8DAAH/KAADGQEiAwAB/wMAAf8jAAH/AwAB/wMAAf8oAAM/AW0D
|
||||
AAH/AwAB/zsAAf8DAAH/AwAB/wMAAf8DAAH/AwAB/wMAAf8TAAH/AwAB/wMAAf8DAAH/AwAB/wMAAf8D
|
||||
AAH/KwAB/wMAAf8DAAH/AwAB/wMAAf8DAAH/AwAB/wMAAf8DAAH/FAADAwEEAwAB/wMAAf8DAAH/AwAB
|
||||
/wMAAf8DAAH/AwAB/wMAAf8DAAH/DAADVAGrAwAB/wMAAf8DAAH/AwAB/yQAA1YBtgMAAf8DAAH/AwAB
|
||||
/wMAAf8kAAM1AfkDAAH/AwAB/xwAA1cB8QMAAf8DAAH/AwAB/wMAAf8DPAH4IwAB/wMAAf8DAAH/NwAB
|
||||
/wMAAf8DAAH/AwAB/wMAAf8DAAH/AwAB/wMAAf8DAAH/CwAB/wMAAf8DAAH/AwAB/wMAAf8DAAH/AwAB
|
||||
/wMAAf8DAAH/JwAB/wMAAf8DAAH/AwAB/wMAAf87AAH/AwAB/wMAAf8DAAH/AwAB/wwAA1QBqwMAAf8D
|
||||
AAH/AwAB/wMAAf8DAAH/IAADVgG1AwAB/wMAAf8DAAH/AwAB/wMAAf8gAAM1AfkDAAH/AwAB/xwAA1cB
|
||||
8QMAAf8DAAH/AwAB/wMAAf8DAAH/AwAB/x8AAf8DAAH/AwAB/zcAAf8DAAH/AwAB/wMAAf8DAAH/AwAB
|
||||
/wMAAf8DAAH/AwAB/wsAAf8DAAH/AwAB/wMAAf8DAAH/AwAB/wMAAf8DAAH/AwAB/ycAAf8DAAH/AwAB
|
||||
/wMAAf8DAAH/AxIB/jMAAf8DAAH/AwAB/wMAAf8DAAH/AwAB/wwAA1QBqwMAAf8DAAH/AwAB/wMAAf8D
|
||||
AAH/AwAB/wNZAbsYAANWAbUDAAH/AwAB/wMAAf8DAAH/AwAB/wMAAf8DUQGeGAADNQH5AwAB/wMAAf8c
|
||||
AANXAfEDAAH/AwAB/wMAAf8DAAH/AwAB/wMAAf8DAAH/A1ABnRcAAf8DAAH/AwAB/zcAAf8DAAH/AwAB
|
||||
/w8AAf8DAAH/AwAB/wsAAf8DAAH/AwAB/w8AAf8DAAH/AwAB/ycAAf8DAAH/A1YBswMAAf8DAAH/AwAB
|
||||
/wMAAf8rAAH/AwAB/wMAAf8DAAH/AwAB/wMAAf8DAAH/DAADVAGrAwAB/wMAAf8DPQFoAwAB/wMAAf8D
|
||||
AAH/AwAB/wMAAf8UAANWAbUDAAH/AwAB/wMzAVADAAH/AwAB/wMAAf8DAAH/AwAB/xQAAzUB+QMAAf8D
|
||||
AAH/HAADVwHxAwAB/wMAAf8DEQEWBAEDAAH/AwAB/wMAAf8DAAH/AwAB/xMAAf8DAAH/AwAB/zcAAf8D
|
||||
AAH/AwAB/w8AAf8DAAH/AwAB/wsAAf8DAAH/AwAB/w8AAf8DAAH/AwAB/ycAAf8DAAH/A1YBswcAAf8D
|
||||
AAH/AwAB/wMAAf8jAAH/AwAB/wMAAf8DAAH/BwAB/wMAAf8DAAH/DAADVAGrAwAB/wMAAf8DPQFoBwAB
|
||||
/wMAAf8DAAH/AwAB/wMAAf8QAANWAbUDAAH/AwAB/wMzAVAHAAH/AwAB/wMAAf8DAAH/AwAB/xAAAzUB
|
||||
+QMAAf8DAAH/HAADVwHxAwAB/wMAAf8DEQEWCwAB/wMAAf8DAAH/AwAB/wMAAf8PAAH/AwAB/wMAAf83
|
||||
AAH/AwAB/wMAAf8PAAH/AwAB/wMAAf8LAAH/AwAB/wMAAf8PAAH/AwAB/wMAAf8nAAH/AwAB/wNWAbML
|
||||
AAH/AwAB/wMAAf8DEgH+GwAB/wMAAf8DAAH/AwAB/wsAAf8DAAH/AwAB/wwAA1QBqwMAAf8DAAH/Az0B
|
||||
aAgAA2AB2wMAAf8DAAH/AwAB/wMAAf8MAANWAbUDAAH/AwAB/wMzAVAIAANgAeMDAAH/AwAB/wMAAf8D
|
||||
AAH/DAADNQH5AwAB/wMAAf8cAANXAfEDAAH/AwAB/wMRARYMAAMzAVIDAAH/AwAB/wMAAf8DAAH/AwAB
|
||||
/wcAAf8DAAH/AwAB/zcAAf8DAAH/AwAB/w8AAf8DAAH/AwAB/wsAAf8DAAH/AwAB/w8AAf8DAAH/AwAB
|
||||
/ycAAf8DAAH/A1YBsw8AAf8DAAH/AwAB/wMAAf8TAAH/AwAB/wMAAf8DAAH/DwAB/wMAAf8DAAH/DAAD
|
||||
VAGrAwAB/wMAAf8DPQFoDAADJQE3AwAB/wMAAf8DAAH/AwAB/wgAA1YBtQMAAf8DAAH/AzMBUAwAAzIB
|
||||
TwMAAf8DAAH/AwAB/wMAAf8IAAM1AfkDAAH/AwAB/xwAA1cB8QMAAf8DAAH/AxEBFhcAAf8DAAH/AwAB
|
||||
/wMAAf8DAAH/AxIB/gMAAf8DAAH/NwAB/wMAAf8DAAH/DwAB/wMAAf8DAAH/CwAB/wMAAf8DAAH/DwAB
|
||||
/wMAAf8DAAH/JwAB/wMAAf8XAAH/AwAB/wMAAf8DAAH/CwAB/wMAAf8DAAH/AwAB/xAAAwwBDwMAAf8D
|
||||
QgH2DAADVAGrAwAB/wMAAf8DPQFoFwAB/wMAAf8DAAH/AwAB/wQAA1YBtQMAAf8DAAH/AzMBUBcAAf8D
|
||||
AAH/AwAB/wMAAf8EAAM1AfkDAAH/AwAB/xwAA1cB8QMAAf8DAAH/AxEBFhgAA1oB6QMAAf8DAAH/AwAB
|
||||
/wMAAf8DAAH/AwAB/zcAAf8DAAH/AwAB/w8AAf8DAAH/AwAB/wsAAf8DAAH/AwAB/w8AAf8DAAH/AwAB
|
||||
/0cAAf8DAAH/AwAB/wMSAf4DAAH/AwAB/wMAAf8DAAH/LAADVAGrAwAB/wMAAf8DPQFoGwAB/wMAAf8D
|
||||
AAH/AwAB/wNdAc4DAAH/AwAB/wMzAVAbAAH/AwAB/wMAAf8DAAH/AyYB+gMAAf8DAAH/HAADVwHxAwAB
|
||||
/wMAAf8DEQEWIwAB/wMAAf8DAAH/AwAB/wMAAf83AAH/AwAB/wMAAf8PAAH/AwAB/wMAAf8LAAH/AwAB
|
||||
/wMAAf8PAAH/AwAB/wMAAf9LAAH/AwAB/wMAAf8DAAH/AwAB/wMAAf8wAANUAasDAAH/AwAB/wM9AWgf
|
||||
AAH/AwAB/wMAAf8DAAH/AwAB/wMAAf8DMwFQHwAB/wMAAf8DAAH/AwAB/wMAAf8DAAH/HAADVwHxAwAB
|
||||
/wMAAf8DEQEWJwAB/wMAAf8DAAH/AwAB/zcAAf8DAAH/AwAB/w8AAf8DAAH/AwAB/wsAAf8DAAH/AwAB
|
||||
/w8AAf8DAAH/AwAB/08AAf8DAAH/AwAB/wMAAf80AANUAasDAAH/AwAB/wM9AWgjAAH/AwAB/wMAAf8D
|
||||
AAH/AwAB/wMzAVAjAAH/AwAB/wMAAf8DAAH/AwAB/xwAA1cB8QMAAf8DAAH/AxEBFisAAf8DAAH/AwAB
|
||||
/zcAAf8DAAH/AwAB/w8AAf8DAAH/AwAB/wsAAf8DAAH/AwAB/w8AAf8DAAH/AwAB/0sAAf8DAAH/AwAB
|
||||
/wMAAf8DAAH/AxIB/jAAA1QBqwMAAf8DAAH/Az0BaB8AAf8DAAH/AwAB/wMAAf8DAAH/AwAB/wMzAVAf
|
||||
AAH/AwAB/wMAAf8DAAH/AwAB/wMAAf8cAANXAfEDAAH/AwAB/wMRARYkAANcAdkDAAH/AwAB/wMAAf83
|
||||
AAH/AwAB/wMAAf8PAAH/AwAB/wMAAf8LAAH/AwAB/wMAAf8PAAH/AwAB/wMAAf9HAAH/AwAB/wMAAf8D
|
||||
AAH/AwAB/wMAAf8DAAH/AwAB/ywAA1QBqwMAAf8DAAH/Az0BaBsAAf8DAAH/AwAB/wMAAf8DVwHxAwAB
|
||||
/wMAAf8DMwFQGwAB/wMAAf8DAAH/AwAB/wMSAf4DAAH/AwAB/xwAA1cB8QMAAf8DAAH/AxEBFiMAAf8D
|
||||
AAH/AwAB/wMAAf8DAAH/NwAB/wMAAf8DAAH/DwAB/wMAAf8DAAH/CwAB/wMAAf8DAAH/DwAB/wMAAf8D
|
||||
AAH/JwAB/wMAAf8XAAH/AwAB/wMAAf8DAAH/CwAB/wMAAf8DAAH/AwAB/xcAAf8DWAG4DAADVAGrAwAB
|
||||
/wMAAf8DPQFoFwAB/wMAAf8DAAH/AwAB/wQAA1YBtQMAAf8DAAH/AzMBUBcAAf8DAAH/AwAB/wMAAf8E
|
||||
AAM1AfkDAAH/AwAB/xwAA1cB8QMAAf8DAAH/AxEBFhgAAzABSgMAAf8DAAH/AwAB/wMAAf8DAAH/AwAB
|
||||
/zcAAf8DAAH/AwAB/w8AAf8DAAH/AwAB/wsAAf8DAAH/AwAB/w8AAf8DAAH/AwAB/ycAAf8DAAH/A1YB
|
||||
sw8AAf8DAAH/AwAB/wMAAf8TAAH/AwAB/wMAAf8DEgH+DwAB/wMAAf8DAAH/DAADVAGrAwAB/wMAAf8D
|
||||
PQFoEwAB/wMAAf8DAAH/AwAB/wgAA1YBtQMAAf8DAAH/AzMBUAwAAwkBDAMAAf8DAAH/AwAB/wMAAf8I
|
||||
AAM1AfkDAAH/AwAB/xwAA1cB8QMAAf8DAAH/AxEBFhcAAf8DAAH/AwAB/wMAAf8DAAH/AyQB/QMAAf8D
|
||||
AAH/NwAB/wMAAf8DAAH/DwAB/wMAAf8DAAH/CwAB/wMAAf8DAAH/DwAB/wMAAf8DAAH/JwAB/wMAAf8D
|
||||
VgGzCwAB/wMAAf8DAAH/AwAB/xsAAf8DAAH/AwAB/wMAAf8LAAH/AwAB/wMAAf8MAANUAasDAAH/AwAB
|
||||
/wM9AWgIAAM6AWADAAH/AwAB/wMAAf8DAAH/DAADVgG1AwAB/wMAAf8DMwFQCAADSwGMAwAB/wMAAf8D
|
||||
AAH/AwAB/wwAAzUB+QMAAf8DAAH/HAADVwHxAwAB/wMAAf8DEQEWDAADBwEJAwAB/wMAAf8DAAH/AwAB
|
||||
/wMAAf8HAAH/AwAB/wMAAf83AAH/AwAB/wMAAf8PAAH/AwAB/wMAAf8LAAH/AwAB/wMAAf8PAAH/AwAB
|
||||
/wMAAf8nAAH/AwAB/wNWAbMHAAH/AwAB/wMAAf8DAAH/IwAB/wMAAf8DAAH/AwAB/wcAAf8DAAH/AwAB
|
||||
/wwAA1QBqwMAAf8DAAH/Az0BaAcAAf8DAAH/AwAB/wMAAf8DAAH/EAADVgG1AwAB/wMAAf8DMwFQBwAB
|
||||
/wMAAf8DAAH/AwAB/wMAAf8QAAM1AfkDAAH/AwAB/xwAA1cB8QMAAf8DAAH/AxEBFgsAAf8DAAH/AwAB
|
||||
/wMAAf8DAAH/A08BmQsAAf8DAAH/AwAB/zcAAf8DAAH/AwAB/w8AAf8DAAH/AwAB/wsAAf8DAAH/AwAB
|
||||
/w8AAf8DAAH/AwAB/ycAAf8DAAH/A1YBswMAAf8DAAH/AwAB/wMAAf8rAAH/AwAB/wMAAf8DEgH+AwAB
|
||||
/wMAAf8DAAH/DAADVAGrAwAB/wMAAf8DPQFoAwAB/wMAAf8DAAH/AwAB/wMAAf8UAANWAbUDAAH/AwAB
|
||||
/wMzAVADAAH/AwAB/wMAAf8DAAH/AwAB/xQAAzUB+QMAAf8DAAH/HAADVwHxAwAB/wMAAf8DEQEWBwAB
|
||||
/wMAAf8DAAH/AwAB/wMAAf8TAAH/AwAB/wMAAf83AAH/AwAB/wMAAf8PAAH/AwAB/wMAAf8LAAH/AwAB
|
||||
/wMAAf8PAAH/AwAB/wMAAf8nAAH/AwAB/wMAAf8DAAH/AwAB/wMAAf8zAAH/AwAB/wMAAf8DAAH/AwAB
|
||||
/wMAAf8MAANUAasDAAH/AwAB/wMAAf8DAAH/AwAB/wMAAf8DTQH0GAADVgG1AwAB/wMAAf8DAAH/AwAB
|
||||
/wMAAf8DAAH/A10B7BgAAzUB+QMAAf8DAAH/HAADVwHxAwAB/wMAAf8DYQHrAwAB/wMAAf8DAAH/AwAB
|
||||
/wNNAfQXAAH/AwAB/wMAAf83AAH/AwAB/wMAAf8PAAH/AwAB/wMAAf8LAAH/AwAB/wMAAf8PAAH/AwAB
|
||||
/wMAAf8nAAH/AwAB/wMAAf8DAAH/AwAB/zsAAf8DAAH/AwAB/wMAAf8DAAH/DAADVAGrAwAB/wMAAf8D
|
||||
AAH/AwAB/wMAAf8DQwF3HAADVgG1AwAB/wMAAf8DAAH/AwAB/wMAAf8DMAFMHAADNQH5AwAB/wMAAf8c
|
||||
AANXAfEDAAH/AwAB/wMAAf8DAAH/AwAB/wMAAf8fAAH/AwAB/wMAAf83AAH/AwAB/wMAAf8DKgFAAyoB
|
||||
QAMqAUADAAH/AwAB/wMAAf8LAAH/AwAB/wMAAf8DKgFAAyoBQAMqAUADAAH/AwAB/wMAAf8nAAH/AwAB
|
||||
/wMAAf8DAAH/AwAB/wMAAf8DAAH/AwAB/wMAAf8bAAH/AwAB/wMAAf8DAAH/AwAB/wMAAf8DAAH/AwAB
|
||||
/wMAAf8MAANUAasDAAH/AwAB/wMAAf8DAAH/AwYBCCAAA1YBtgMAAf8DAAH/AwAB/wMAAf8DAgEDIAAD
|
||||
NQH5AwAB/wMAAf8cAANXAfEDAAH/AwAB/wMAAf8DAAH/AwAB/yMAAf8DAAH/AwAB/zcAAf8DAAH/AwAB
|
||||
/wMAAf8DAAH/AwAB/wMAAf8DAAH/AwAB/wsAAf8DAAH/AwAB/wMAAf8DAAH/AwAB/wMAAf8DAAH/AwAB
|
||||
/ycAAf8DAAH/AwAB/wMAAf8DAAH/AwAB/wMAAf8DAAH/AwAB/xQAA0wBkAMAAf8DAAH/AwAB/wMAAf8D
|
||||
AAH/AwAB/wMAAf8DAAH/AwAB/xMAAf8DAAH/AwAB/y8AAf8DAAH/AwAB/ygAAzYBWAMAAf8DAAH/IwAB
|
||||
/wMAAf8DAAH/Aw0BESQAA1UBrQMAAf8DAAH/OwAB/wMAAf8DAAH/AwAB/wMAAf8DAAH/AwAB/xMAAf8D
|
||||
AAH/AwAB/wMAAf8DAAH/AwAB/wMAAf8sAANaAb8DWgG/A1oBvwNaAb8DWgG/A1oBvwNaAb8DFQEcGAAD
|
||||
TwGZA1oBvwNaAb8DWgG/A1oBvwNaAb8DWgG/A1oBvxgAA04BlzQAA0sBjTAAAwwBDygAA1ABmjAAAygB
|
||||
PEAAAzgBWwNaAb8DWgG/A1oBvwMwAUwYAANEAXoDWgG/A1oBvwNaAb8DIAEtHAABQgFNAT4HAAE+AwAB
|
||||
KAMAAXwDAAEwAwABAQEAAQEGAAEDFgAD/wEAAf8B4AEHAf8B+AE/Av8B4AIAAXgEAAH/AcABAQH/AfgB
|
||||
HwL/AcACAAF4BAAB/wGDAeAB/wH5AQcC/wHAAgABOAQAAf8BjwH4AX8B+QGBAv8BxwH/Af4BOAQAAv8B
|
||||
/AE/AfkB4AL/AccB/wH+ATgEAAL/Af4BHwH5AfgBPwH/AccB/wH+ATgEAAP/AR8B+QH8AQ8B/wHHAf8B
|
||||
/gE4BAAD/wGPAfkB/wEHAf8BxwH/Af4BOAQAA/8BjwH5Af8BgQH/AccB/wH+ATgEAAP/AY8B+QH/AeAB
|
||||
/wHHAf8B/gE4BAAD/wHPAfkB/wH4AX8BxwH/Af4BOAQAA/8BzwH5Af8B/AF/AccB/wH+ATgEAAP/Ac8B
|
||||
+QH/AfgBfwHHAf8B/gE4BAAB8AEPAf8BjwH5Af8B4AF/AccB/wH+ATgEAAHgAQcB/wGPAfkB/wHBAf8B
|
||||
xwH/Af4BOAQAAeABBwH/AY8B+QH/AQcB/wHHAf8B/gE4BAAB4wGHAf8BHwH5AfwBDwH/AccB/wH+ATgE
|
||||
AAHjAQ8B/wEfAfkB+AE/Af8BxwH/Af4BOAQAAeIBHwH+AT8B+QHgAv8BxwH/Af4BOAQAAeABDwH4AT8B
|
||||
+QGBAv8BxwH/Af4BOAQAAeABAwHgAf8B+QEHAv8BwAIAATgEAAHwAYABAQH/AfgBDwL/AcACAAF4BAAB
|
||||
8wHgAQcB/wH4AT8C/wHgAgABeAQAAf8B/gE/Af8B/gP/AfgBAAEBAfgEAAHwAQcBwAEPAR8B/AF/AeMB
|
||||
/AF/AeMB/wHwAR4BAwLwAQcBwAEOAQ8B+AE/AeMB+AEfAeMB/wHgAQwBAQLwAX8B/gEOAQcB+AEfAeMB
|
||||
+AEPAeMB/wHgAQwBAQLwAT8B/AEOAQEB+AEHAeMB+AEDAeMB/wHjAYwBcQLwAR8B+AEOAQAB+AEDAeMB
|
||||
+AEBAeMB/wHjAYwBcQHwAfEBDwHwAY4BEAF4AUEB4wH4AWAB4wH/AeMBjAFxAfAB8QGHAeEBjgEYATgB
|
||||
YAHjAfgBcAEjAf8B4wGMAXEB8AHxAsMBjgEcARgBcAFjAfgBfAEDAf8B4wGMAXEB8AHzAeEBhwGOAR8B
|
||||
CAF8ASMB+AF+AQMB/wHjAYwBcQHwAf8B8AEPAf4BHwGAAX4BAwH4AX8BgwH/AeMBjAFxAfAB/wH4AR8B
|
||||
/gEfAcABfwEDAfgBfwHDAf8B4wGMAXEB8AH/AfwBPwH+AR8B4AF/AYMB+AF/AeMB/wHjAYwBcQHwAf8B
|
||||
+AEfAf4BHwHAAX8BAwH4AX8BwwH/AeMBjAFxAfAB/wHwAQ8B/gEfAYABfgEDAfgBfwGDAf8B4wGMAXEB
|
||||
8AHzAeEBhwHOAR8BCAF8ASMB+AF+AQMB/wHjAYwBcQHwAfECwwGOAR4BGAFwAWMB+AF8AQMB/wHjAYwB
|
||||
cQHwAfEBhwHhAY4BGAE4AWAB4wH4AXABIwH/AeMBjAFxAfAB8QEPAfABjgEQAXgBQQHjAfgBYAFjAf8B
|
||||
4wGMAXEC8AEfAfgBDgEAAfgBAwHjAfgBQQHjAf8B4wGMAXEC8AE/AfwBDgEBAfgBBwHjAfgBAwHjAf8B
|
||||
4wGMAXEC8AF/Af4BDgEDAfgBDwHjAfgBDwHjAf8B4AEMAQEC8AEHAeABDgEHAfgBHwHjAfgBHwHjAf8B
|
||||
4AEMAQEC8AEHAcABDwEfAfwBfwHjAfwBPwHjAf8B8AEeAQMB8AH4AQcB4AEfAb8B/gH/AfcB/gH/AfcB
|
||||
/wH4AT8BBwHwCw==
|
||||
</value>
|
||||
</data>
|
||||
<metadata name="toolTip.TrayLocation" type="System.Drawing.Point, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a">
|
||||
<value>165, 17</value>
|
||||
</metadata>
|
||||
<metadata name="spinePreviewFullScreenForm.TrayLocation" type="System.Drawing.Point, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a">
|
||||
<value>307, 18</value>
|
||||
</metadata>
|
||||
<metadata name="wallpaperForm.TrayLocation" type="System.Drawing.Point, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a">
|
||||
<value>618, 18</value>
|
||||
</metadata>
|
||||
</root>
|
||||
@@ -1,326 +0,0 @@
|
||||
<?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
|
||||
AAFwAQABcAEAAR8BAAEYAQAE/wEhAQAI/wFCAU0BNgcAATYDAAEoAwABfAMAATADAAEBAQABIAYAAV0q
|
||||
AAQCAy0BRQNbAc0DXQHoA1kBxgMyAU8DDwEUBAIYAAMOARIDQwF3A10BzwNbAc0DLQFFBAIYAANWAbID
|
||||
XQHoA1wB1gNDAXcDFgEeBAIYAAMKAQ0DSQGFA14B4wNfAeUDUQGeAyQBNAMJAQsEARgAAwsBDgM7AWQD
|
||||
XgHSA1YBsv8AEQAEAgMxAUwDYQHhAwAB/wMuAfkDYAHbA0QBewMeASoDBgEIFAADGAEhA1cBwgMhAfsD
|
||||
YQHhAzEBTAQCGAADWQHDAwAB/wMhAfwDXAHrA0gBhAMWAR4YAAMLAQ4DTQGSAysB+QMPAf4DUAHyA1gB
|
||||
ugM3AVoDEQEWAwIBAxQAAxcBHwNJAYYDXAHrA1kBw/8AEQAEAgMxAUwDYQHhAwAB/wMAAf8DHgH9A2AB
|
||||
4ANQAZoDLgFGAxEBFgMGAQcEAQgAAxoBJANbAc0DAAH/A2EB4QMxAUwEAhgAA1kBwwMAAf8DAAH/AwAB
|
||||
/wNbAdADPgFrAw8BEwMCAQMQAAMLAQ4DTQGSAysB+QMAAf8DAAH/AzkB9gNcAcsDRAF5Ax4BKgMGAQcM
|
||||
AAQBAxgBIQNKAYsDWwHsA1kBw/8AEQAEAgMxAUwDYQHhAwAB/wMuAfkDXAHnA0MB9QNWAe4DWQG7A0MB
|
||||
dwMoATsDDwEUBAEEAAMaASQDWwHNAwAB/wNhAeEDMQFMBAIYAANZAcMDIAH8A1AB8ANDAfUDLgH5A10B
|
||||
zgNDAXcDGgEjAwIBAwwAAwsBDgNNAZIDKwH5AwEB/wMkAfoDOgH4Ay4B+QNdAeMDTgGYAyQBNQMGAQgE
|
||||
AQQABAEDGAEhA0oBiwNbAewDWQHD/wARAAQCAzEBTANhAeEDAAH/A1wB2QM7AWMDWQG7Az0B9wM5AfgD
|
||||
XAHnA1sBxQNBAXMDEwEZAwIBAwMaASQDWwHNAwAB/wNhAeEDMQFMBAIYAANZAcMDXAHrA1ABmgNVAbED
|
||||
TQHzAyEB+wNbAeQDSwGPAxsBJQMDAQQIAAMLAQ4DTQGSAysB+QMfAf0DXAHZA1oBxwNDAfUDDwH+A1wB
|
||||
6wNSAaMDJQE3AwMBBAQABAEDGAEhA0oBiwNbAewDWQHD/wARAAQCAzEBTANhAeEDAAH/A1sBzQMaASQD
|
||||
FgEdA1UBrwMAAf8DAAH/AwAB/wMAAf8DVQGvAxYBHQMaASQDWwHNAwAB/wNhAeEDMQFMBAIYAANZAcMD
|
||||
WwHkAzsBZQMHAQkDSQGGAyEB+wMAAf8DAAH/A1kBwQMdASkIAAMLAQ4DTQGSAysB+QMrAfkDTQGSAzkE
|
||||
XgHiAwAB/wMAAf8DXQHjAzYBWAMFAQYEAAQBAxgBIQNKAYsDWwHsA1kBw/8AEQAEAgMxAUwDYQHhAwAB
|
||||
/wNbAc0DGgEkAwIBAwMTARoDRgF/A1sB3gMhAfsDDwH+AzkB+ANZAbsDOwFjA1wB2QMAAf8DYQHhAzEB
|
||||
TAQCGAADWQHDA1sB5AM7AWUDBwQJAQwDOgFhA18B1QNCAfUDLgH5A1sBygMyAU8DDwEUAw0BEQNNAZID
|
||||
KwH5AysB+QNNAZIDEQEWAy0BRANaAb8DUgHvAyEB+wNdAdwDPwFuAxYBHgMEAQUDGAEhA0oBiwNbAewD
|
||||
WQHD/wARAAQCAzEBTANhAeEDAAH/A1sBzQMaASQEAAQCAxMBGgM9AWcDWQHAA1IB7wMPAf4DPQH3A1wB
|
||||
5wMuAfkDAAH/A2EB4QMxAUwEAhgAA1kBwwNbAeQDOwFlAwcBCQQAAwsBDgMxAU0DWAG9AzkB9gMuAfkD
|
||||
YAHbA0QBeAMhAS8DTgGVAysB+QMrAfkDTQGSAwsBDgMGAQgDIAEuA00BkgNdAdwDQwH1A1oB6QNOAZYD
|
||||
KAE8AyABLQNLAY0DWwHsA1kBw/8AEQAEAgMxAUwDYQHhAwAB/wNbAc0DGgEkCAAEAgMMARADLwFJA1IB
|
||||
owNbAeQDPQH3Aw8B/gMAAf8DAAH/A2EB4QMxAUwEAhgAA1kBwwNbAeQDOwFlAwcBCQgAAxABFQM/AW0D
|
||||
XAHWAyQB+gMeAf0DXAHWA0QBeQNUAasDIQH7AysB+QNNAZIDCwEOBAADAwEEAxsBJgNBAXMDWgHEA0UB
|
||||
9ANWAe4DVQG0A0QBegNRAaIDVgHuA1kBw/8AEQAEAgMxAUwDYQHhAwAB/wNbAc0DGgEkEAADCAEKAyMB
|
||||
MwNEAXkDVwG8A18B5QMkAfoDAAH/A2EB4QMxAUwEAhgAA1kBwwNbAeQDOwFlAwcBCQgAAwMBBAMaASQD
|
||||
QgF0A14B0gMhAfsDOQH2A10B0QNgAeADHgH9AysB+QNNAZIDCwEOCAADAgEDAxIBFwM1AVUDWAG6A1UB
|
||||
8QM5AfYDWwHeA2AB2wM5AfgDWQHD/wARAAQCAzEBTANhAeEDAAH/A1sBzQMaASQUAAMCAQMDDgESAyMB
|
||||
MgM9AWgDXgHdAwAB/wNhAeEDMQFMBAIYAANZAcMDWwHkAzsBZQMHAQkMAAMCAQMDDwETAzQBUwNdAdED
|
||||
OQH4Ax8B/AMfAf0DAAH/AysB+QNNAZIDCwEODAAEAQMJAQwDIwEzA1UBrgMgAf0DHgH9Ax8B/AMPAf4D
|
||||
WQHD/wARAAQCAzEBTANhAeEDAAH/A1sBzQMaASQgAAMaASQDWwHNAwAB/wNhAeEDMQFMBAIYAANZAcMD
|
||||
WwHkAzsBZQMHAQkYAAMPARQDVwG5AwAB/wMAAf8DAAH/AysB+QNNAZIDCwEOGAADRwGAA1wB7QMAAf8D
|
||||
AAH/AwAB/wNZAcP/ABEABAIDMQFMA2EB4QMAAf8DWwHNAxoBJBQABAIDBwEJAxYBHQM1AVUDXAHZAwAB
|
||||
/wNhAeEDMQFMBAIYAANZAcMDWwHkAzsBZQMHAQkMAAQBAw0BEQM0AVMDXQHRAy4B+QMQAf4DDwH+AwAB
|
||||
/wMrAfkDTQGSAwsBDgwABAEDCQELAx4BKwNTAakDIAH9Ax4B/QMfAfwDDwH+A1kBw/8AEQAEAgMxAUwD
|
||||
YQHhAwAB/wNbAc0DGgEkEAADBwEJAxsBJgM0AVMDTQGSA14B3QMuAfkDAAH/A2EB4QMxAUwEAhgAA1kB
|
||||
wwNbAeQDOwFlAwcBCQgABAIDEAEVAzkBXgNbAc0DIQH7AyEB+wNcAecDVgHuAw8B/gMrAfkDTQGSAwsB
|
||||
DggAAwIBAwMSARcDNQFVA1gBuANVAfEDOQH2A1sB3gNgAdsDOQH4A1kBw/8AEQAEAgMxAUwDYQHhAwAB
|
||||
/wNbAc0DGgEkCAAEAgMMARADLgFGA00BkgNcAcgDXAHrAx4B/QMAAf8DAAH/A2EB4QMxAUwEAhgAA1kB
|
||||
wwNbAeQDOwFlAwcBCQgAAwkBDAMxAU4DWAG3A00B8wMeAf0DXgHdA04BlgNZAb4DHwH8AysB+QNNAZID
|
||||
CwEOBAADAwEEAxsBJgNBAXMDWgHEA0UB9ANWAe4DVQG0A0QBegNRAaIDVgHuA1kBw/8AEQAEAgMxAUwD
|
||||
YQHhAwAB/wNbAc0DGgEkBAAEAgMTARoDPQFnA1oBvwNcAesDJAH6AzoB9gNcAecDLgH5AwAB/wNhAeED
|
||||
MQFMBAIYAANZAcMDWwHkAzsBZQMHAQkEAAMLAQ4DLgFHA1UBrQNSAe8DOgH4A2AB2wNEAXoDJAE1A04B
|
||||
mAMjAfoDKwH5A00BkgMLAQ4DBgEIAyABLgNNAZIDXQHcA0MB9QNaAekDTgGWAygBPAMgAS0DSwGNA1sB
|
||||
7ANZAcP/ABEABAIDMQFMA2EB4QMAAf8DWwHNAxoBJAMCAQMDEwEaA0YBfwNbAd4DIQH7Aw8B/gM5AfgD
|
||||
WQG7AzsBYwNcAdkDAAH/A2EB4QMxAUwEAhgAA1kBwwNbAeQDOwFlAwcECQEMAzoBYQNdAdQDRQH0Ay4B
|
||||
+QNbAcoDMgFPAw8BFAMNAREDTQGSAysB+QMrAfkDTQGSAxEBFgMtAUQDWgG/A1IB7wMgAfwDYAHgA0AB
|
||||
cQMXAR8DBAEFAxgBIQNKAYsDWwHsA1kBw/8AEQAEAgMxAUwDYQHhAwAB/wNbAc0DGgEkAxYBHQNVAa8D
|
||||
AAH/AwAB/wMAAf8DAAH/A1UBrwMWAR0DGgEkA1sBzQMAAf8DYQHhAzEBTAQCGAADWQHDA1sB5AM7AWUD
|
||||
BwEJA0kBhgMhAfsDAAH/AwAB/wNZAcEDHQEpCAADCwEOA00BkgMrAfkDKwH5A00BkgM5BF4B4gMAAf8D
|
||||
AAH/A1gB7QNKAYsDGAEgCAEDGAEhA0oBiwNbAewDWQHD/wARAAQCAzEBTANhAeEDAAH/A1wB2QM7AWMD
|
||||
WQG7Az0B9wMgAfwDRQH0A1wB2QNGAX4DEwEaAwIBAwMaASQDWwHNAwAB/wNhAeEDMQFMBAIYAANZAcMD
|
||||
XAHrA1ABmgNVAbEDTQHzAyEB+wNfAeUDSwGPAxsBJQMDAQQIAAMLAQ4DTQGSAysB+QMhAfsDVgGzA0wB
|
||||
jgNcAesDDwH+A1AB8ANXAbkDLgFHAwYBCAQABAEDGAEhA0oBiwNbAewDWQHD/wARAAQCAzEBTANhAeED
|
||||
AAH/Ay4B+QNcAecDQwH1A1QB8QNbAdMDUQGkAzgBWwMTARkEAgQAAxoBJANbAc0DAAH/A2EB4QMxAUwE
|
||||
AhgAA1kBwwMgAfwDUAHwA0MB9QMfAf0DXgHXA0YBfgMaASQDAgEDDAADCwEOA00BkgMrAfkDDwH+A00B
|
||||
8wNWAe4DPQH3A10B4wNOAZgDJwE5AwgBCgQBBAAEAQMYASEDSgGLA1sB7ANZAcP/ABEABAIDMQFMA2EB
|
||||
4QMAAf8DAAH/Ax4B/QNhAeEDUgGjAzsBYwMhAS8DCgENBAIIAAMaASQDWwHNAwAB/wNhAeEDMQFMBAIY
|
||||
AANZAcMDAAH/AwAB/wMAAf8DXQHoA0oBiwMWAR0DBAEFEAADCwEOA00BkgMrAfkDAAH/AwAB/wM5AfYD
|
||||
XAHLA0QBeQMeASoDBgEHDAAEAQMYASEDSgGLA1sB7ANZAcP/ABEABAIDMQFMA2EB4QMAAf8DLgH5A2AB
|
||||
2wNFAXwDIQEwAwwBEAMDAQQQAAMaASMDXAHIAx8B/QNhAeEDMQFMBAIYAANZAcMDAAH/AyEB/ANcAesD
|
||||
TgGXAyMBMwQCFAADCwEOA00BkgMrAfkDDwH+A1AB8gNYAboDNwFaAxEBFgMCAQMUAAMXAR8DSQGGA1wB
|
||||
6wNZAcP/ABEABAIDLQFFA14B0gNQAfIDXQHJAzIBTwMQARUDAgEDGAADEwEaA1ABnwNbAeQDWwHQAy0B
|
||||
RQQCGAADVQG0A1cB7gNfAdoDRAF4AxgBIAMDAQQYAAMKAQ0DSQGGA10B6ANaAekDUAGfAyQBNAMJAQsE
|
||||
ARgAAwsBDgM7AWUDWwHTA1YBsv8AFQADAwEEAycBOgM/AW0DFAEbKAADDwETAzEBTgMdASkDAgEDHAAD
|
||||
DgESAzEBTQMjATMDBAEFJAADBgEHAygBPAMoATwDBgEHJAAEAQMHAQkDCwEOAwIBA/8ABQADAgEDAw8B
|
||||
FANJAYgDXwHlA10B6ANdAegDXQHoA10B6ANdAegDXQHoA10B6ANdAegDXQHoA10B6ANdAegDXQHoA10B
|
||||
6ANdAegDXQHoA10B6ANdAegDXQHoA1sB5ANGAX4DBQEGBAEoAAMCAQMDDQERAx8BLAMtAUYDVwG5A10B
|
||||
6ANdAegDXQHoA10B6ANdAegDXQHoA18B4wNZAb4DMgFPAw8BEwMCAQMwAAMTARkDRgF+A10B6ANdAegD
|
||||
WQHBAzkBXgMmATgDDwEUBAJcAAM5AV8DXgHiA1wB5wNdAegDXQHoA10B6ANdAegDXQHcAzwBZgMGAQgD
|
||||
BgEIAzwBZgNdAdwDXQHoA10B6ANdAegDXQHoA10B3ANQAZ0DJQE2IAADDAEQAzwBZgNdAc8DHgH9AyQB
|
||||
+gNSAe8DWAHtA1gB7QNYAe0DWAHtA1gB7QNYAe0DWAHtA1gB7QNYAe0DWAHtA1gB7QNYAe0DWAHtA1gB
|
||||
7QNSAe8DJAH6AyAB/ANWAbUDKwFCAwgBCiQABAIDGAEhA0ABcANaAb8DXQHfA1oB7QNSAe8DWwHsA14B
|
||||
5gNfAeUDWAHqA1gB7QNQAfADOgH2A10B0QNDAXcDHgErAwYBBywAAz0BaQNbAd4DAQH/Ay4B+QNcAewD
|
||||
WwHkA14B1wNEAXsDHgEqAwYBCFgAAz8BbAMfAf0DOQH4A1IB7wNYAe0DVgHuAyQB+gM5AfYDTwGbAx4B
|
||||
KwMgAS0DUAGdAzkB9gMkAfoDVgHuA1gB7QNSAe8DPQH3A1AB8gM7AWUgAAMTARkDUAGcAz0B9wM6AfYD
|
||||
XQHMA0sBjQNGAYADRgGAA0YBgANGAYADRgGAA0YBgANGAYADRgGAA0YBgANGAYADRgGAA0YBgANGAYAD
|
||||
RgGAA0sBjwNcAdkDHgH9A1QB8QNOAZQDEgEYJAADBQEGAzEBTgNbAdADUAHyA14B4gNVAa4DSgGKA0YB
|
||||
fwNDAXcDQwF2A0UBfANGAYADTwGbA14B3QNdAegDXwHaA04BlgMoATwDCQEMKAADRwGDAzkB+AMgAfwD
|
||||
WwHTA1MBpgNcAdkDLgH5A2AB4ANQAZoDLQFEAwsBDlQAAz8BbAMgAfwDWwHTA0wBjgNGAYADSgGKA10B
|
||||
3AMfAf0DXgHdAzoBYAM6AWIDXQHfAx4B/QNdAdwDSgGKA0YBgANMAY4DWwHTAyAB/AM/AWwgAAMUARsD
|
||||
UgGlAx0B/QNbAdADPwFsAxgBIQMOARIDDgESAw4BEgMOARIDDgESAw4BEgMOARIDDgESAw4BEgMOARID
|
||||
DgESAw4BEgMOARIDDgESAyABLgNXAbkDHgH9Ax0B/QNSAaUDFAEbJAADBQEGAzEBTQNaAccDXAHIA0AB
|
||||
bwMlATcDEwEaAw4BEgMNAREDDAEQAw4BEgMOARIDHQEoAzoBYANLAY8DWwHYA14B5gNWAbYDMQFNAwYB
|
||||
CCQAA0kBhgMhAfsDLgH5A1UBrgMqAUADPwFtA10B0QNDAfUDVgHuA1gBtwM2AVcDFgEdAwwBEAQCSAAD
|
||||
PwFsAy4B+QNVAa4DIAEtAw4BEgMbASUDWQG+AwAB/wNYAe0DPwFtAz8BbQNYAe0DAAH/A1kBvgMbASUD
|
||||
DgESAyABLQNVAa4DLgH5Az8BbCAAAxQBGwNSAaUDHQH9A1YBtgMoATwDCAEKOAADFgEeA1YBswMeAf0D
|
||||
HQH9A1IBpQMUARskAAQBAw8BFAMjATMDIQEwAwsBDgMDAQQEARQABAIDBwEJAx4BKgNLAYwDXQHfAz0B
|
||||
9wNTAakDKAE7JAADSQGGAyEB+wMuAfkDUwGnAxYBHgMMARADKgFAA1gBugNOAfMDRgH0A2AB2wNXAbwD
|
||||
QgF0AxMBGQMCAQNEAAM/AWwDLgH5A1MBpwMVARwEAAMPARQDVwG5AwAB/wNYAe0DPwFtAz8BbQNYAe0D
|
||||
AAH/A1cBuQMPARQEAAMVARwDUwGnAy4B+QM/AWwgAAMUARsDUgGlAx0B/QNVAbQDJgE4AwcBCTgAAxYB
|
||||
HgNWAbMDHgH9Ax0B/QNSAaUDFAEbYAADFQEcA1MBpwMuAfkDIQH7A0kBhiQAA0kBhgMhAfsDLgH5A1MB
|
||||
pwMVARwIAAMPARMDQwF2A1wB2QMfAf0DAAH/AwAB/wNVAa8DFgEdRAADPwFsAy4B+QNTAacDFQEcBAAD
|
||||
DwEUA1cBuQMAAf8DWAHtAz8BbQM/AW0DWAHtAwAB/wNXAbkDDwEUBAADFQEcA1MBpwMuAfkDPwFsIAAD
|
||||
FAEbA1IBpQMdAf0DVQG0AyYBOAMHAQk4AAMWAR4DVgGzAx4B/QMdAf0DUgGlAxQBG2AAAwIBAwMeASoD
|
||||
VQG0Az0B9wNSAaADGwElAwUBBhwAA0kBhgMhAfsDLgH5A1MBpwMVARwIAAQCAwkBCwMYASADRAF6A2AB
|
||||
2wM7AfcDPQH3A1kBuwMqAUADDgESAwUBBgQCNAADPwFsAy4B+QNTAacDFQEcBAADDwEUA1cBuQMAAf8D
|
||||
WAHtAz8BbQM/AW0DWAHtAwAB/wNXAbkDDwEUBAADFQEcA1MBpwMuAfkDPwFsIAADFAEbA1IBpQMdAf0D
|
||||
VQG0AyYBOAMHAQk4AAMWAR4DVgGzAx4B/QMdAf0DUgGlAxQBG2QAAw8BEwNAAW8DXQHUA1MB7wNMAZED
|
||||
EgEYHAADSQGGAyEB+wMuAfkDUwGnAxUBHBAABAIDDQERAzYBVwNYAbcDWwHsA1UB8QNdAdEDRAF5AzMB
|
||||
UAMbASYDBgEHMAADPwFsAy4B+QNTAacDFQEcBAADDwEUA1cBuQMAAf8DWAHtAz8BbQM/AW0DWAHtAwAB
|
||||
/wNXAbkDDwEUBAADFQEcA1MBpwMuAfkDPwFsIAADFAEbA1IBpQMdAf0DVQG0AyYBOAMHAQk4AAMWAR4D
|
||||
VgGzAx4B/QMdAf0DUgGlAxQBG2QAAwIBAwMPARMDUQGeAx0B/QNSAaUDFAEbHAADSQGGAyEB+wMuAfkD
|
||||
UwGnAxUBHBgAAwsBDgMtAUQDSwGPA1sBxQNhAeEDYQHhA1sBzQNMAZADKAE7AwkBDCwAAz8BbAMuAfkD
|
||||
UwGnAxUBHAQAAw8BFANXAbkDAAH/A1gB7QM/AW0DPwFtA1gB7QMAAf8DVwG5Aw8BFAQAAxUBHANTAacD
|
||||
LgH5Az8BbCAAAxQBGwNSAaUDHQH9A1UBtAMmATgDBwEJOAADFgEeA1YBswMeAf0DHQH9A1IBpQMUARts
|
||||
AANNAZMDHQH9A1IBpQMUARscAANJAYYDIQH7Ay4B+QNTAacDFQEcHAADBgEIAxkBIgMvAUkDQAFxA10B
|
||||
zANNAfMDWgHpA1YBtgMtAUQsAAM/AWwDLgH5A1MBpwMVARwEAAMPARQDVwG5AwAB/wNYAe0DPwFtAz8B
|
||||
bQNYAe0DAAH/A1cBuQMPARQEAAMVARwDUwGnAy4B+QM/AWwgAAMUARsDUgGlAx0B/QNVAbQDJgE4AwcB
|
||||
CTgAAxYBHgNWAbMDHgH9Ax0B/QNSAaUDFAEbbAADTQGTAx0B/QNSAaUDFAEbHAADSQGGAyEB+wMuAfkD
|
||||
UwGnAxUBHCAABAIDBAEFAwsBDgMwAUwDWQHBAzoB9gMsAfkDQQFyAwYBCCgAAz8BbAMuAfkDUwGnAxUB
|
||||
HAQAAw8BFANXAbkDAAH/A1gB7QM/AW0DPwFtA1gB7QMAAf8DVwG5Aw8BFAQAAxUBHANTAacDLgH5Az8B
|
||||
bCAAAxQBGwNSAaUDHQH9A1UBtAMmATgDBwEJOAADFgEeA1YBswMeAf0DHQH9A1IBpQMUARtsAANNAZMD
|
||||
HQH9A1IBpQMUARscAANJAYYDIQH7Ay4B+QNTAacDFQEcMAADFQEcA1MBpwMuAfkDWwHTAzoBYSgAAz8B
|
||||
bAMuAfkDUwGnAxUBHAQAAw8BFANXAbkDAAH/A1gB7QM/AW0DPwFtA1gB7QMAAf8DVwG5Aw8BFAQAAxUB
|
||||
HANTAacDLgH5Az8BbCAAAxQBGwNSAaUDHQH9A1UBtAMmATgDBwEJOAADFgEeA1YBswMeAf0DHQH9A1IB
|
||||
pQMUARsQAAQBAwQBBQMLAQ4DDwETAw8BEwMPARMDDwETAw8BEwMNAREDCQEMAwMBBAQBLAADTQGTAx0B
|
||||
/QNSAaUDFAEbHAADSQGGAyEB+wMuAfkDUwGnAxUBHCAABAEDAgEDAwsBDgMwAUwDWQHBAzoB9gMsAfkD
|
||||
QQFyAwYBCCgAAz8BbAMuAfkDUwGnAxUBHAQAAw8BFANXAbkDAAH/A1gB7QM/AW0DPwFtA1gB7QMAAf8D
|
||||
VwG5Aw8BFAQAAxUBHANTAacDLgH5Az8BbCAAAxQBGwNSAaUDHQH9A1UBtAMmATgDBwEJOAADFgEeA1YB
|
||||
swMeAf0DHQH9A1IBpQMUARsQAAMGAQgDJAE1Az4BawNGAX0DRgF+A0YBfgNGAX4DRgF+A0QBewM9AWkD
|
||||
JAE0AwkBDCwAA00BkwMdAf0DUgGlAxQBGxwAA0kBhgMhAfsDLgH5A1MBpwMVARwcAAMGAQgDEgEXAyMB
|
||||
MwM/AW4DWwHNAzoB9gNYAeoDVgG2Ay0BRCwAAz8BbAMuAfkDUwGnAxUBHAQAAw8BFANXAbkDAAH/A1gB
|
||||
7QM/AW0DPwFtA1gB7QMAAf8DVwG5Aw8BFAQAAxUBHANTAacDLgH5Az8BbCAAAxQBGwNSAaUDHQH9A1UB
|
||||
tAMmATgDBwEJOAADFgEeA1YBswMeAf0DHQH9A1IBpQMUARsQAAMQARUDRgF/A1wB2QNYAeoDXgHmA2EB
|
||||
4QNgAeADXgHiA1wB5wNhAeEDUAGdAyEBMCQAAwIBAwMPARMDUQGeAx0B/QNSAaUDFAEbHAADSQGGAyEB
|
||||
+wMuAfkDUwGnAxUBHBgAAwsBDgMtAUQDRgGBA1MBqQNdAd8DXQHoA18B2gNOAZYDKAE8AwkBDCwAAz8B
|
||||
bAMuAfkDUwGnAxUBHAQAAw8BFANXAbkDAAH/A1gB7QM/AW0DPwFtA1gB7QMAAf8DVwG5Aw8BFAQAAxUB
|
||||
HANTAacDLgH5Az8BbCAAAxQBGwNSAaUDHQH9A1UBtAMmATgDBwEJOAADFgEeA1YBswMeAf0DHQH9A1IB
|
||||
pQMUARsQAAMTARoDTgGYA0IB9QMAAf8DYAHgA1YBtgNZAb4DXgHXAz0B9wMgAfwDWgHHAysBQiQAAw8B
|
||||
EwNAAW8DXQHUA1MB7wNNAZIDEgEYHAADSQGGAyEB+wMuAfkDUwGnAxUBHBAABAIDDQERAzYBVwNYAbcD
|
||||
WAHqA1wB7ANfAdUDSwGPAzoBYAMeASsDBgEHMAADPwFsAy4B+QNTAacDFQEcBAADDwEUA1cBuQMAAf8D
|
||||
WAHtAz8BbQM/AW0DWAHtAwAB/wNXAbkDDwEUBAADFQEcA1MBpwMuAfkDPwFsIAADFAEbA1IBpQMdAf0D
|
||||
VQG0AyYBOAMHAQk4AAMWAR4DVgGzAx4B/QMdAf0DUgGlAxQBGxAAAxQBGwNQAZoDOQH2AwAB/wNRAaQD
|
||||
JAE0A0QBeANeAd0DPQH3A1sB3gM7AWMDDgESIAADAgEDAx4BKgNVAbQDPQH3A1EBoQMbASYDBQEGHAAD
|
||||
SQGGAyEB+wMuAfkDUwGnAxUBHAwABAEDEwEZA0QBegNgAdsDOwH3Az0B9wNZAbsDKwFCAxMBGQMIAQoD
|
||||
AgEDNAADPwFsAy4B+QNTAacDFQEcBAADDwEUA1cBuQMAAf8DWAHtAz8BbQM/AW0DWAHtAwAB/wNXAbkD
|
||||
DwEUBAADFQEcA1MBpwMuAfkDPwFsIAADFAEbA1IBpQMdAf0DVQG0AyYBOAMHAQk4AAMWAR4DVgGzAx4B
|
||||
/QMdAf0DUgGlAxQBGxAAAxQBGwNQAZoDOQH2AwAB/wNOAZUDMQFMA2EB4QMAAf8DWwHNAxoBJCgAAxUB
|
||||
HANTAacDLgH5AyEB+wNJAYYkAANJAYYDIQH7Ay4B+QNTAacDFQEcDAADCwEOA00BkgMrAfkDAAH/AwAB
|
||||
/wNVAa8DFgEdRAADPwFsAy4B+QNTAacDFQEcBAADDwEUA1cBuQMAAf8DWAHtAz8BbQM/AW0DWAHtAwAB
|
||||
/wNXAbkDDwEUBAADFQEcA1MBpwMuAfkDPwFsIAADFAEbA1IBpQMdAf0DVgG2AygBPAMIAQo4AAMWAR4D
|
||||
VgGzAx4B/QMdAf0DUgGlAxQBGxAAAxQBGwNQAZoDOQH2AwAB/wNDAfUDVAHvAyEB/AMRAf4DXQHMAx0B
|
||||
KQQCGAAEAgMGAQcDDwETAzIBTwNZAcADQgH1A10B0QM6AWAkAANJAYYDIQH7Ay4B+QNTAacDFgEeAwwB
|
||||
EAMqAUADVwG5A1oB6QNTAe8DWwHoA10BzwNGAX0DEwEaAwIBA0QAAz8BbAMuAfkDUwGnAxUBHAQAAw8B
|
||||
FANXAbkDAAH/A1gB7QM/AW0DPwFtA1gB7QMAAf8DVwG5Aw8BFAQAAxUBHANTAacDLgH5Az8BbCAAAxQB
|
||||
GwNSAaUDHQH9A1sB0AM/AWwDGAEhAw4BEgMOARIDDgESAw4BEgMOARIDDgESAw4BEgMOARIDDgESAw4B
|
||||
EgMOARIDDgESAw4BEgMOARIDIAEuA1cBuQMeAf0DHQH9A1IBpQMUARsQAAMUARsDUAGaAzkB9gMAAf8D
|
||||
AAH/AyMB+gNCAfUDHgH9A2AB4ANAAXEDHQEoAw4BEgMNAREDCQEMAwgBCgMLBA4BEgMcAScDOAFbA0QB
|
||||
eQNaAccDYQHhA1YBtgM0AVQDDAEPJAADSQGGAyEB+wMuAfkDVQGuAyoBQAM/AW0DXQHRA0MB9QNWAe4D
|
||||
WQG7A0QBeQMqAUADEQEWBAJIAAM/AWwDLgH5A1UBrgMgAS0DDgESAxsBJQNZAb4DAAH/A1gB7QM/AW0D
|
||||
PwFtA1gB7QMAAf8DWQG+AxsBJQMOARIDIAEtA1UBrgMuAfkDPwFsIAADEwEaA1EBpAMfAfwDOQH2A10B
|
||||
zANLAY0DRgGAA0YBgANGAYADRgGAA0YBgANGAYADRgGAA0YBgANGAYADRgGAA0YBgANGAYADRgGAA0YB
|
||||
gANLAY8DXAHZAx4B/QNFAfQDTgGYAxMBGRAAAxIBGANLAYwDWAHtAwAB/wMgAf0DXAHZA1UBsQNdAd8D
|
||||
QwH1A2AB4ANPAZsDRgGAA0QBegM5AV4DMwFQAz0BaANGAX4DUAGaA18B2gNbAeQDXwHaA08BmwMpAT4D
|
||||
CgENKAADSQGGAyEB+wMgAfwDWwHTA1MBpgNcAdkDLgH5A2AB4ANQAZoDLQFFAxABFQMGAQcEAUwAAz8B
|
||||
bAMgAfwDWwHTA0wBjgNGAYADSgGKA10B3AMeAf0DXQHfAzoBYgM6AWIDXQHfAx4B/QNdAdwDSgGKA0YB
|
||||
gANMAY4DWwHTAyAB/AM/AWwgAAMSARcDSgGLA1oB6gMPAf4DJAH6A1IB7wNYAe0DWAHtA1gB7QNYAe0D
|
||||
WAHtA1gB7QNYAe0DWAHtA1gB7QNYAe0DWAHtA1gB7QNYAe0DWAHtA1IB7wMkAfoDHgH9A1cBwgM0AVQD
|
||||
CwEOEAADCAEKAy4BSANVAbEDXgHiA2AB2wNPAZsDLQFEA0MBdwNdAcwDYQHhA1gB6gNYAe0DWgHpA1sB
|
||||
0wNcAcgDXwHaA1wB6wNOAfIDLgH5A14B0gNEAXgDIQEvAwcBCSwAA0IBdQNYAeoDAQH/Ay4B+QNQAfAD
|
||||
XAHsA1wB2QNEAXsDHgEqAwYBCFgAAz8BbAMBAf8DLgH5A1IB7wNYAe0DVgHuAyQB+gM5AfYDUAGdAyAB
|
||||
LQMgAS0DUAGdAzkB9gMkAfoDVgHuA1gB7QNSAe8DLgH5Aw8B/gM/AWwgAAMFAQYDGgEjA00BkwNgAeYD
|
||||
WgHtAzwB9gM9AfcDPQH3Az0B9wM9AfcDPQH3Az0B9wM9AfcDPQH3Az0B9wM9AfcDPQH3Az0B9wM9AfcD
|
||||
PQH3AzwB9gNaAe0DXwHlA0cBgwMKAQ0EAhAABAEDCAEKAx8BLAMuAUgDLQFEAxsBJQMHAQkDDwETAyMB
|
||||
MgMuAUcDVwG5A10B6ANaAekDVwHuA1UB8QNcAewDWgHpA10B6ANbAdADNAFTAw8BEwMCAQMwAAMWAR4D
|
||||
RwGDA1oB7QNVAfEDXwHVA0wBjgMrAUIDDwEUBAJcAAM6AWIDWgHpA1oB7QM8AfYDPQH3AzwB9gNcAewD
|
||||
XQHcAzwBZgMGAQgDBgEIAzwBZgNdAdwDWgHtAzwB9gM9AfcDPAH2A1oB7QNgAeYDOgFiLAAEAQMjATMD
|
||||
TgGXA1QBqwNUAasDVAGrA1QBqwNUAasDVAGrA1QBqwNUAasDVAGrA1QBqwNUAasDVAGrA1QBqwNUAasD
|
||||
TgGXAyMBMwQBTAADCQELAzkBXQNHAYIDJwE6AwQBBUgABAIDIgExAzoBYQMPARRwAAMDAQQDKAE7A04B
|
||||
mANUAasDUQGeAyEBLxQAAwMBBAMlATcDUAGfA1QBqwNOAZgDKAE7AwMBBBgAAUIBTQE+BwABPgMAASgD
|
||||
AAF8AwABMAMAAQEBAAEBBgABAxYAA/8BAAH8AQMB8AE/AQMB8AEPAcAIAAH8AQEB8AE/AQMB8AEHAcAI
|
||||
AAH8AQABMAE/AQAB8AEDAYAIAAH8AQABEAE/AQABcAEAAYAIAAH8AgABPwEAATABAAGACAAB/AIAAT8B
|
||||
AAEwAQABgAgAAfwCAAE/DAAB/AEIAQABPwEICwAB/AEMAQABPwEMAQABIAkAAfwBDwEAAT8BDAEAATAJ
|
||||
AAH8AQ8BgAE/AQ4BAAE4CQAB/AEPAfABPwEPAcABPwkAAfwBDwGAAT8BDgEAATgJAAH8AQ8BAAE/AQwB
|
||||
AAEwCQAB/AEMAQABPwEMAQABIAkAAfwBCAEAAT8BCAsAAfwCAAE/DAAB/AIAAT8BAAEwCgAB/AIAAT8B
|
||||
AAEwAQABgAgAAfwBAAEQAT8BAAFwAQABgAgAAfwBAAEwAT8BAAHwAQMBgAgAAfwBAAHwAT8BAQHwAQcB
|
||||
wAgAAfwBAwHwAT8BAwHwAQ8BwAgAAf4BHwH4AX8BDwH4AX8BwAgAAeACAAEHAf4BAAEBAf8B4AEPAv8B
|
||||
4AEAAQEB8AHgAgABBwH8AgAB/wHgAQcC/wHgAQABAQHwAeACAAEHAfwCAAF/AeABAwL/AeABAAEBAfAB
|
||||
4AIAAQcB/AIAAT8B4AEAAX8B/wHgAQABAQHwAeABfwH+AQcB/AEHAcABPwHgAQABPwH/AeEBAAEhAfAB
|
||||
4AF/Af4BBwL/AfgBPwHgAcABPwH/AeEBAAEhAfAB4AF/Af4BBwL/AfgBDwHgAcABAwH/AeEBAAEhAfAB
|
||||
4AF/Af4BBwL/AfwBDwHgAfABAQH/AeEBAAEhAfAB4AF/Af4BBwL/AfwBDwHgAfwBAAH/AeEBAAEhAfAB
|
||||
4AF/Af4BBwP/AQ8B4AH+AQAB/wHhAQABIQHwAeABfwH+AQcD/wEPAeAB/wEAAX8B4QEAASEB8AHgAX8B
|
||||
/gEHA/8BDwHgAf8B8AF/AeEBAAEhAfAB4AF/Af4BBwGAAQcB/wEPAeAB/wEAAX8B4QEAASEB8AHgAX8B
|
||||
/gEHAYABBwH/AQ8B4AH+AQAB/wHhAQABIQHwAeABfwH+AQcBgAEHAfwBDwHgAfwBAAH/AeEBAAEhAfAB
|
||||
4AF/Af4BBwGAAQcB/AEPAeAB8AEBAf8B4QEAASEB8AHgAX8B/gEHAYABBwH4AQ8C4AEDAf8B4QEAASEB
|
||||
8AHgAX8B/gEHAYABHwH4AT8C4AE/Af8B4QEAASEB8AHgAX8B/gEHAYABDwHAAT8B4AEAAT8B/wHhAQAB
|
||||
IQHwAeACAAEHAYACAAE/AeABAAF/Af8B4AEAAQEB8AHgAgABBwGAAgABfwHgAQAC/wHgAQABAQHwAeAC
|
||||
AAEHAYACAAH/AeABBwL/AeABAAEBAfAB4AIAAQcBgAEAAQEB/wHgAQ8C/wHgAQABAQHwAfwCAAE/Af8B
|
||||
+AE/Af8B8AP/AfABPgEDAfAL
|
||||
</value>
|
||||
</data>
|
||||
<metadata name="toolTip.TrayLocation" type="System.Drawing.Point, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a">
|
||||
<value>165, 17</value>
|
||||
</metadata>
|
||||
</root>
|
||||
319
SpineViewer/Controls/SpineViewPropertyGrid.Designer.cs
generated
Normal file
319
SpineViewer/Controls/SpineViewPropertyGrid.Designer.cs
generated
Normal file
@@ -0,0 +1,319 @@
|
||||
namespace SpineViewer.Controls
|
||||
{
|
||||
partial class SpineViewPropertyGrid
|
||||
{
|
||||
/// <summary>
|
||||
/// 必需的设计器变量。
|
||||
/// </summary>
|
||||
private System.ComponentModel.IContainer components = null;
|
||||
|
||||
/// <summary>
|
||||
/// 清理所有正在使用的资源。
|
||||
/// </summary>
|
||||
/// <param name="disposing">如果应释放托管资源,为 true;否则为 false。</param>
|
||||
protected override void Dispose(bool disposing)
|
||||
{
|
||||
if (disposing && (components != null))
|
||||
{
|
||||
components.Dispose();
|
||||
}
|
||||
base.Dispose(disposing);
|
||||
}
|
||||
|
||||
#region 组件设计器生成的代码
|
||||
|
||||
/// <summary>
|
||||
/// 设计器支持所需的方法 - 不要修改
|
||||
/// 使用代码编辑器修改此方法的内容。
|
||||
/// </summary>
|
||||
private void InitializeComponent()
|
||||
{
|
||||
components = new System.ComponentModel.Container();
|
||||
tabControl = new TabControl();
|
||||
tabPage_BaseInfo = new TabPage();
|
||||
propertyGrid_BaseInfo = new PropertyGrid();
|
||||
tabPage_Render = new TabPage();
|
||||
propertyGrid_Render = new PropertyGrid();
|
||||
tabPage_Transform = new TabPage();
|
||||
propertyGrid_Transform = new PropertyGrid();
|
||||
tabPage_Skin = new TabPage();
|
||||
propertyGrid_Skin = new PropertyGrid();
|
||||
contextMenuStrip_Skin = new ContextMenuStrip(components);
|
||||
toolStripMenuItem_ReloadSkins = new ToolStripMenuItem();
|
||||
tabPage_Slot = new TabPage();
|
||||
propertyGrid_Slot = new PropertyGrid();
|
||||
tabPage_Animation = new TabPage();
|
||||
propertyGrid_Animation = new PropertyGrid();
|
||||
contextMenuStrip_Animation = new ContextMenuStrip(components);
|
||||
toolStripMenuItem_AddAnimation = new ToolStripMenuItem();
|
||||
toolStripMenuItem_RemoveAnimation = new ToolStripMenuItem();
|
||||
tabPage_Debug = new TabPage();
|
||||
propertyGrid_Debug = new PropertyGrid();
|
||||
tabControl.SuspendLayout();
|
||||
tabPage_BaseInfo.SuspendLayout();
|
||||
tabPage_Render.SuspendLayout();
|
||||
tabPage_Transform.SuspendLayout();
|
||||
tabPage_Skin.SuspendLayout();
|
||||
contextMenuStrip_Skin.SuspendLayout();
|
||||
tabPage_Slot.SuspendLayout();
|
||||
tabPage_Animation.SuspendLayout();
|
||||
contextMenuStrip_Animation.SuspendLayout();
|
||||
tabPage_Debug.SuspendLayout();
|
||||
SuspendLayout();
|
||||
//
|
||||
// tabControl
|
||||
//
|
||||
tabControl.Alignment = TabAlignment.Bottom;
|
||||
tabControl.Controls.Add(tabPage_BaseInfo);
|
||||
tabControl.Controls.Add(tabPage_Render);
|
||||
tabControl.Controls.Add(tabPage_Transform);
|
||||
tabControl.Controls.Add(tabPage_Skin);
|
||||
tabControl.Controls.Add(tabPage_Slot);
|
||||
tabControl.Controls.Add(tabPage_Animation);
|
||||
tabControl.Controls.Add(tabPage_Debug);
|
||||
tabControl.Dock = DockStyle.Fill;
|
||||
tabControl.ItemSize = new Size(90, 35);
|
||||
tabControl.Location = new Point(0, 0);
|
||||
tabControl.Multiline = true;
|
||||
tabControl.Name = "tabControl";
|
||||
tabControl.Padding = new Point(0, 0);
|
||||
tabControl.SelectedIndex = 0;
|
||||
tabControl.Size = new Size(372, 448);
|
||||
tabControl.SizeMode = TabSizeMode.FillToRight;
|
||||
tabControl.TabIndex = 0;
|
||||
//
|
||||
// tabPage_BaseInfo
|
||||
//
|
||||
tabPage_BaseInfo.BackColor = SystemColors.Control;
|
||||
tabPage_BaseInfo.Controls.Add(propertyGrid_BaseInfo);
|
||||
tabPage_BaseInfo.Location = new Point(4, 4);
|
||||
tabPage_BaseInfo.Margin = new Padding(0);
|
||||
tabPage_BaseInfo.Name = "tabPage_BaseInfo";
|
||||
tabPage_BaseInfo.Size = new Size(364, 370);
|
||||
tabPage_BaseInfo.TabIndex = 0;
|
||||
tabPage_BaseInfo.Text = "基本信息";
|
||||
//
|
||||
// propertyGrid_BaseInfo
|
||||
//
|
||||
propertyGrid_BaseInfo.Dock = DockStyle.Fill;
|
||||
propertyGrid_BaseInfo.HelpVisible = false;
|
||||
propertyGrid_BaseInfo.Location = new Point(0, 0);
|
||||
propertyGrid_BaseInfo.Name = "propertyGrid_BaseInfo";
|
||||
propertyGrid_BaseInfo.PropertySort = PropertySort.Alphabetical;
|
||||
propertyGrid_BaseInfo.Size = new Size(364, 370);
|
||||
propertyGrid_BaseInfo.TabIndex = 0;
|
||||
propertyGrid_BaseInfo.ToolbarVisible = false;
|
||||
//
|
||||
// tabPage_Render
|
||||
//
|
||||
tabPage_Render.BackColor = SystemColors.Control;
|
||||
tabPage_Render.Controls.Add(propertyGrid_Render);
|
||||
tabPage_Render.Location = new Point(4, 4);
|
||||
tabPage_Render.Margin = new Padding(0);
|
||||
tabPage_Render.Name = "tabPage_Render";
|
||||
tabPage_Render.Size = new Size(364, 370);
|
||||
tabPage_Render.TabIndex = 1;
|
||||
tabPage_Render.Text = "渲染";
|
||||
//
|
||||
// propertyGrid_Render
|
||||
//
|
||||
propertyGrid_Render.Dock = DockStyle.Fill;
|
||||
propertyGrid_Render.HelpVisible = false;
|
||||
propertyGrid_Render.Location = new Point(0, 0);
|
||||
propertyGrid_Render.Name = "propertyGrid_Render";
|
||||
propertyGrid_Render.PropertySort = PropertySort.Alphabetical;
|
||||
propertyGrid_Render.Size = new Size(364, 370);
|
||||
propertyGrid_Render.TabIndex = 1;
|
||||
propertyGrid_Render.ToolbarVisible = false;
|
||||
//
|
||||
// tabPage_Transform
|
||||
//
|
||||
tabPage_Transform.BackColor = SystemColors.Control;
|
||||
tabPage_Transform.Controls.Add(propertyGrid_Transform);
|
||||
tabPage_Transform.Location = new Point(4, 4);
|
||||
tabPage_Transform.Margin = new Padding(0);
|
||||
tabPage_Transform.Name = "tabPage_Transform";
|
||||
tabPage_Transform.Size = new Size(364, 370);
|
||||
tabPage_Transform.TabIndex = 2;
|
||||
tabPage_Transform.Text = "变换";
|
||||
//
|
||||
// propertyGrid_Transform
|
||||
//
|
||||
propertyGrid_Transform.Dock = DockStyle.Fill;
|
||||
propertyGrid_Transform.HelpVisible = false;
|
||||
propertyGrid_Transform.Location = new Point(0, 0);
|
||||
propertyGrid_Transform.Name = "propertyGrid_Transform";
|
||||
propertyGrid_Transform.PropertySort = PropertySort.Alphabetical;
|
||||
propertyGrid_Transform.Size = new Size(364, 370);
|
||||
propertyGrid_Transform.TabIndex = 1;
|
||||
propertyGrid_Transform.ToolbarVisible = false;
|
||||
//
|
||||
// tabPage_Skin
|
||||
//
|
||||
tabPage_Skin.BackColor = SystemColors.Control;
|
||||
tabPage_Skin.Controls.Add(propertyGrid_Skin);
|
||||
tabPage_Skin.Location = new Point(4, 4);
|
||||
tabPage_Skin.Margin = new Padding(0);
|
||||
tabPage_Skin.Name = "tabPage_Skin";
|
||||
tabPage_Skin.Size = new Size(364, 370);
|
||||
tabPage_Skin.TabIndex = 3;
|
||||
tabPage_Skin.Text = "皮肤";
|
||||
//
|
||||
// propertyGrid_Skin
|
||||
//
|
||||
propertyGrid_Skin.ContextMenuStrip = contextMenuStrip_Skin;
|
||||
propertyGrid_Skin.Dock = DockStyle.Fill;
|
||||
propertyGrid_Skin.HelpVisible = false;
|
||||
propertyGrid_Skin.Location = new Point(0, 0);
|
||||
propertyGrid_Skin.Name = "propertyGrid_Skin";
|
||||
propertyGrid_Skin.PropertySort = PropertySort.NoSort;
|
||||
propertyGrid_Skin.Size = new Size(364, 370);
|
||||
propertyGrid_Skin.TabIndex = 1;
|
||||
propertyGrid_Skin.ToolbarVisible = false;
|
||||
//
|
||||
// contextMenuStrip_Skin
|
||||
//
|
||||
contextMenuStrip_Skin.ImageScalingSize = new Size(24, 24);
|
||||
contextMenuStrip_Skin.Items.AddRange(new ToolStripItem[] { toolStripMenuItem_ReloadSkins });
|
||||
contextMenuStrip_Skin.Name = "contextMenuStrip1";
|
||||
contextMenuStrip_Skin.Size = new Size(241, 67);
|
||||
//
|
||||
// toolStripMenuItem_ReloadSkins
|
||||
//
|
||||
toolStripMenuItem_ReloadSkins.Name = "toolStripMenuItem_ReloadSkins";
|
||||
toolStripMenuItem_ReloadSkins.Size = new Size(240, 30);
|
||||
toolStripMenuItem_ReloadSkins.Text = "重新加载皮肤";
|
||||
toolStripMenuItem_ReloadSkins.Click += toolStripMenuItem_ReloadSkins_Click;
|
||||
//
|
||||
// tabPage_Slot
|
||||
//
|
||||
tabPage_Slot.BackColor = SystemColors.Control;
|
||||
tabPage_Slot.Controls.Add(propertyGrid_Slot);
|
||||
tabPage_Slot.Location = new Point(4, 4);
|
||||
tabPage_Slot.Margin = new Padding(0);
|
||||
tabPage_Slot.Name = "tabPage_Slot";
|
||||
tabPage_Slot.Size = new Size(364, 370);
|
||||
tabPage_Slot.TabIndex = 6;
|
||||
tabPage_Slot.Text = "插槽";
|
||||
//
|
||||
// propertyGrid_Slot
|
||||
//
|
||||
propertyGrid_Slot.Dock = DockStyle.Fill;
|
||||
propertyGrid_Slot.HelpVisible = false;
|
||||
propertyGrid_Slot.Location = new Point(0, 0);
|
||||
propertyGrid_Slot.Name = "propertyGrid_Slot";
|
||||
propertyGrid_Slot.PropertySort = PropertySort.Alphabetical;
|
||||
propertyGrid_Slot.Size = new Size(364, 370);
|
||||
propertyGrid_Slot.TabIndex = 2;
|
||||
propertyGrid_Slot.ToolbarVisible = false;
|
||||
//
|
||||
// tabPage_Animation
|
||||
//
|
||||
tabPage_Animation.BackColor = SystemColors.Control;
|
||||
tabPage_Animation.Controls.Add(propertyGrid_Animation);
|
||||
tabPage_Animation.Location = new Point(4, 4);
|
||||
tabPage_Animation.Margin = new Padding(0);
|
||||
tabPage_Animation.Name = "tabPage_Animation";
|
||||
tabPage_Animation.Size = new Size(364, 370);
|
||||
tabPage_Animation.TabIndex = 4;
|
||||
tabPage_Animation.Text = "动画";
|
||||
//
|
||||
// propertyGrid_Animation
|
||||
//
|
||||
propertyGrid_Animation.ContextMenuStrip = contextMenuStrip_Animation;
|
||||
propertyGrid_Animation.Dock = DockStyle.Fill;
|
||||
propertyGrid_Animation.HelpVisible = false;
|
||||
propertyGrid_Animation.Location = new Point(0, 0);
|
||||
propertyGrid_Animation.Name = "propertyGrid_Animation";
|
||||
propertyGrid_Animation.PropertySort = PropertySort.NoSort;
|
||||
propertyGrid_Animation.Size = new Size(364, 370);
|
||||
propertyGrid_Animation.TabIndex = 1;
|
||||
propertyGrid_Animation.ToolbarVisible = false;
|
||||
//
|
||||
// contextMenuStrip_Animation
|
||||
//
|
||||
contextMenuStrip_Animation.ImageScalingSize = new Size(24, 24);
|
||||
contextMenuStrip_Animation.Items.AddRange(new ToolStripItem[] { toolStripMenuItem_AddAnimation, toolStripMenuItem_RemoveAnimation });
|
||||
contextMenuStrip_Animation.Name = "contextMenuStrip1";
|
||||
contextMenuStrip_Animation.Size = new Size(117, 64);
|
||||
contextMenuStrip_Animation.Opening += contextMenuStrip_Animation_Opening;
|
||||
//
|
||||
// toolStripMenuItem_AddAnimation
|
||||
//
|
||||
toolStripMenuItem_AddAnimation.Name = "toolStripMenuItem_AddAnimation";
|
||||
toolStripMenuItem_AddAnimation.Size = new Size(116, 30);
|
||||
toolStripMenuItem_AddAnimation.Text = "添加";
|
||||
toolStripMenuItem_AddAnimation.Click += toolStripMenuItem_AddAnimation_Click;
|
||||
//
|
||||
// toolStripMenuItem_RemoveAnimation
|
||||
//
|
||||
toolStripMenuItem_RemoveAnimation.Name = "toolStripMenuItem_RemoveAnimation";
|
||||
toolStripMenuItem_RemoveAnimation.Size = new Size(116, 30);
|
||||
toolStripMenuItem_RemoveAnimation.Text = "移除";
|
||||
toolStripMenuItem_RemoveAnimation.Click += toolStripMenuItem_RemoveAnimation_Click;
|
||||
//
|
||||
// tabPage_Debug
|
||||
//
|
||||
tabPage_Debug.BackColor = SystemColors.Control;
|
||||
tabPage_Debug.Controls.Add(propertyGrid_Debug);
|
||||
tabPage_Debug.Location = new Point(4, 4);
|
||||
tabPage_Debug.Name = "tabPage_Debug";
|
||||
tabPage_Debug.Size = new Size(364, 370);
|
||||
tabPage_Debug.TabIndex = 5;
|
||||
tabPage_Debug.Text = "调试";
|
||||
//
|
||||
// propertyGrid_Debug
|
||||
//
|
||||
propertyGrid_Debug.Dock = DockStyle.Fill;
|
||||
propertyGrid_Debug.HelpVisible = false;
|
||||
propertyGrid_Debug.Location = new Point(0, 0);
|
||||
propertyGrid_Debug.Name = "propertyGrid_Debug";
|
||||
propertyGrid_Debug.PropertySort = PropertySort.NoSort;
|
||||
propertyGrid_Debug.Size = new Size(364, 370);
|
||||
propertyGrid_Debug.TabIndex = 2;
|
||||
propertyGrid_Debug.ToolbarVisible = false;
|
||||
//
|
||||
// SpineViewPropertyGrid
|
||||
//
|
||||
AutoScaleDimensions = new SizeF(11F, 24F);
|
||||
AutoScaleMode = AutoScaleMode.Font;
|
||||
Controls.Add(tabControl);
|
||||
Name = "SpineViewPropertyGrid";
|
||||
Size = new Size(372, 448);
|
||||
tabControl.ResumeLayout(false);
|
||||
tabPage_BaseInfo.ResumeLayout(false);
|
||||
tabPage_Render.ResumeLayout(false);
|
||||
tabPage_Transform.ResumeLayout(false);
|
||||
tabPage_Skin.ResumeLayout(false);
|
||||
contextMenuStrip_Skin.ResumeLayout(false);
|
||||
tabPage_Slot.ResumeLayout(false);
|
||||
tabPage_Animation.ResumeLayout(false);
|
||||
contextMenuStrip_Animation.ResumeLayout(false);
|
||||
tabPage_Debug.ResumeLayout(false);
|
||||
ResumeLayout(false);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
private TabControl tabControl;
|
||||
private TabPage tabPage_BaseInfo;
|
||||
private TabPage tabPage_Render;
|
||||
private TabPage tabPage_Transform;
|
||||
private TabPage tabPage_Skin;
|
||||
private TabPage tabPage_Animation;
|
||||
private PropertyGrid propertyGrid_BaseInfo;
|
||||
private PropertyGrid propertyGrid_Render;
|
||||
private PropertyGrid propertyGrid_Transform;
|
||||
private PropertyGrid propertyGrid_Skin;
|
||||
private PropertyGrid propertyGrid_Animation;
|
||||
private ContextMenuStrip contextMenuStrip_Skin;
|
||||
private ContextMenuStrip contextMenuStrip_Animation;
|
||||
private ToolStripMenuItem toolStripMenuItem_ReloadSkins;
|
||||
private ToolStripMenuItem toolStripMenuItem_AddAnimation;
|
||||
private ToolStripMenuItem toolStripMenuItem_RemoveAnimation;
|
||||
private TabPage tabPage_Debug;
|
||||
private PropertyGrid propertyGrid_Debug;
|
||||
private TabPage tabPage_Slot;
|
||||
private PropertyGrid propertyGrid_Slot;
|
||||
}
|
||||
}
|
||||
107
SpineViewer/Controls/SpineViewPropertyGrid.cs
Normal file
107
SpineViewer/Controls/SpineViewPropertyGrid.cs
Normal file
@@ -0,0 +1,107 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using System.Drawing;
|
||||
using System.Data;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows.Forms;
|
||||
using SpineViewer.Utils;
|
||||
using SpineViewer.Spine.SpineView;
|
||||
|
||||
namespace SpineViewer.Controls
|
||||
{
|
||||
public partial class SpineViewPropertyGrid : UserControl
|
||||
{
|
||||
public SpineViewPropertyGrid()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 设置选中的对象列表, 可以赋值 null 来清空选中, 行为与 PropertyGrid.SelectedObjects 类似
|
||||
/// </summary>
|
||||
public SpineObjectProperty[] SelectedSpines
|
||||
{
|
||||
get => selectedSpines ?? [];
|
||||
set
|
||||
{
|
||||
if (value is null || value.Length <= 0)
|
||||
{
|
||||
selectedSpines = null;
|
||||
propertyGrid_BaseInfo.SelectedObject = null;
|
||||
propertyGrid_Render.SelectedObject = null;
|
||||
propertyGrid_Transform.SelectedObject = null;
|
||||
propertyGrid_Skin.SelectedObject = null;
|
||||
propertyGrid_Slot.SelectedObject = null;
|
||||
propertyGrid_Animation.SelectedObject = null;
|
||||
propertyGrid_Debug.SelectedObject = null;
|
||||
}
|
||||
else
|
||||
{
|
||||
selectedSpines = value;
|
||||
propertyGrid_BaseInfo.SelectedObjects = value.Select(e => e.BaseInfo).ToArray();
|
||||
propertyGrid_Render.SelectedObjects = value.Select(e => e.Render).ToArray();
|
||||
propertyGrid_Transform.SelectedObjects = value.Select(e => e.Transform).ToArray();
|
||||
propertyGrid_Skin.SelectedObjects = value.Select(e => e.Skin).ToArray();
|
||||
propertyGrid_Slot.SelectedObjects = value.Select(e => e.Slot).ToArray();
|
||||
propertyGrid_Animation.SelectedObjects = value.Select(e => e.Animation).ToArray();
|
||||
propertyGrid_Debug.SelectedObjects = value.Select(e => e.Debug).ToArray();
|
||||
}
|
||||
}
|
||||
}
|
||||
private SpineObjectProperty[]? selectedSpines = null;
|
||||
|
||||
private void contextMenuStrip_Animation_Opening(object sender, CancelEventArgs e)
|
||||
{
|
||||
if (selectedSpines?.Length == 1)
|
||||
{
|
||||
toolStripMenuItem_AddAnimation.Enabled = true;
|
||||
toolStripMenuItem_RemoveAnimation.Enabled = propertyGrid_Animation.SelectedGridItem.Value is TrackAnimationProperty;
|
||||
}
|
||||
else
|
||||
{
|
||||
toolStripMenuItem_AddAnimation.Enabled = false;
|
||||
toolStripMenuItem_RemoveAnimation.Enabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
private void toolStripMenuItem_ReloadSkins_Click(object sender, EventArgs e)
|
||||
{
|
||||
foreach (var sp in selectedSpines)
|
||||
sp.Spine.ReloadSkins();
|
||||
}
|
||||
|
||||
private void toolStripMenuItem_AddAnimation_Click(object sender, EventArgs e)
|
||||
{
|
||||
if (selectedSpines?.Length != 1) return;
|
||||
|
||||
var spine = selectedSpines[0].Animation.Spine;
|
||||
spine.SetAnimation(spine.GetTrackIndices().Max() + 1, spine.AnimationNames[0]);
|
||||
propertyGrid_Animation.Refresh();
|
||||
}
|
||||
|
||||
private void toolStripMenuItem_RemoveAnimation_Click(object sender, EventArgs e)
|
||||
{
|
||||
if (selectedSpines?.Length != 1) return;
|
||||
|
||||
if (propertyGrid_Animation.SelectedGridItem.Value is TrackAnimationProperty wrapper)
|
||||
{
|
||||
selectedSpines[0].Animation.Spine.ClearTrack(wrapper.Index);
|
||||
propertyGrid_Animation.Refresh();
|
||||
}
|
||||
}
|
||||
|
||||
public override void Refresh()
|
||||
{
|
||||
base.Refresh();
|
||||
propertyGrid_BaseInfo.Refresh();
|
||||
propertyGrid_Render.Refresh();
|
||||
propertyGrid_Transform.Refresh();
|
||||
propertyGrid_Skin.Refresh();
|
||||
propertyGrid_Animation.Refresh();
|
||||
propertyGrid_Debug.Refresh();
|
||||
}
|
||||
}
|
||||
}
|
||||
126
SpineViewer/Controls/SpineViewPropertyGrid.resx
Normal file
126
SpineViewer/Controls/SpineViewPropertyGrid.resx
Normal file
@@ -0,0 +1,126 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<root>
|
||||
<!--
|
||||
Microsoft ResX Schema
|
||||
|
||||
Version 2.0
|
||||
|
||||
The primary goals of this format is to allow a simple XML format
|
||||
that is mostly human readable. The generation and parsing of the
|
||||
various data types are done through the TypeConverter classes
|
||||
associated with the data types.
|
||||
|
||||
Example:
|
||||
|
||||
... ado.net/XML headers & schema ...
|
||||
<resheader name="resmimetype">text/microsoft-resx</resheader>
|
||||
<resheader name="version">2.0</resheader>
|
||||
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
|
||||
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
|
||||
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
|
||||
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
|
||||
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
|
||||
<value>[base64 mime encoded serialized .NET Framework object]</value>
|
||||
</data>
|
||||
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
|
||||
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
|
||||
<comment>This is a comment</comment>
|
||||
</data>
|
||||
|
||||
There are any number of "resheader" rows that contain simple
|
||||
name/value pairs.
|
||||
|
||||
Each data row contains a name, and value. The row also contains a
|
||||
type or mimetype. Type corresponds to a .NET class that support
|
||||
text/value conversion through the TypeConverter architecture.
|
||||
Classes that don't support this are serialized and stored with the
|
||||
mimetype set.
|
||||
|
||||
The mimetype is used for serialized objects, and tells the
|
||||
ResXResourceReader how to depersist the object. This is currently not
|
||||
extensible. For a given mimetype the value must be set accordingly:
|
||||
|
||||
Note - application/x-microsoft.net.object.binary.base64 is the format
|
||||
that the ResXResourceWriter will generate, however the reader can
|
||||
read any of the formats listed below.
|
||||
|
||||
mimetype: application/x-microsoft.net.object.binary.base64
|
||||
value : The object must be serialized with
|
||||
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
|
||||
: and then encoded with base64 encoding.
|
||||
|
||||
mimetype: application/x-microsoft.net.object.soap.base64
|
||||
value : The object must be serialized with
|
||||
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
|
||||
: and then encoded with base64 encoding.
|
||||
|
||||
mimetype: application/x-microsoft.net.object.bytearray.base64
|
||||
value : The object must be serialized into a byte array
|
||||
: using a System.ComponentModel.TypeConverter
|
||||
: and then encoded with base64 encoding.
|
||||
-->
|
||||
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
|
||||
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
|
||||
<xsd:element name="root" msdata:IsDataSet="true">
|
||||
<xsd:complexType>
|
||||
<xsd:choice maxOccurs="unbounded">
|
||||
<xsd:element name="metadata">
|
||||
<xsd:complexType>
|
||||
<xsd:sequence>
|
||||
<xsd:element name="value" type="xsd:string" minOccurs="0" />
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="name" use="required" type="xsd:string" />
|
||||
<xsd:attribute name="type" type="xsd:string" />
|
||||
<xsd:attribute name="mimetype" type="xsd:string" />
|
||||
<xsd:attribute ref="xml:space" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:element name="assembly">
|
||||
<xsd:complexType>
|
||||
<xsd:attribute name="alias" type="xsd:string" />
|
||||
<xsd:attribute name="name" type="xsd:string" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:element name="data">
|
||||
<xsd:complexType>
|
||||
<xsd:sequence>
|
||||
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
|
||||
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
|
||||
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
|
||||
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
|
||||
<xsd:attribute ref="xml:space" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:element name="resheader">
|
||||
<xsd:complexType>
|
||||
<xsd:sequence>
|
||||
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="name" type="xsd:string" use="required" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
</xsd:choice>
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
</xsd:schema>
|
||||
<resheader name="resmimetype">
|
||||
<value>text/microsoft-resx</value>
|
||||
</resheader>
|
||||
<resheader name="version">
|
||||
<value>2.0</value>
|
||||
</resheader>
|
||||
<resheader name="reader">
|
||||
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
||||
</resheader>
|
||||
<resheader name="writer">
|
||||
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
||||
</resheader>
|
||||
<metadata name="contextMenuStrip_Skin.TrayLocation" type="System.Drawing.Point, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a">
|
||||
<value>29, 26</value>
|
||||
</metadata>
|
||||
<metadata name="contextMenuStrip_Animation.TrayLocation" type="System.Drawing.Point, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a">
|
||||
<value>318, 25</value>
|
||||
</metadata>
|
||||
</root>
|
||||
@@ -1,4 +1,5 @@
|
||||
using System;
|
||||
using SpineViewer.Utils;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using System.Diagnostics;
|
||||
@@ -40,7 +41,7 @@ namespace SpineViewer.Dialogs
|
||||
else
|
||||
{
|
||||
Clipboard.SetText(url);
|
||||
MessageBox.Info("链接已复制到剪贴板,请前往浏览器进行访问");
|
||||
MessagePopup.Info("链接已复制到剪贴板,请前往浏览器进行访问");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using SpineViewer.Spine;
|
||||
using SpineViewer.Utils;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
@@ -21,7 +22,7 @@ namespace SpineViewer.Dialogs
|
||||
public BatchOpenSpineDialog()
|
||||
{
|
||||
InitializeComponent();
|
||||
comboBox_Version.DataSource = SpineHelper.Names.ToList();
|
||||
comboBox_Version.DataSource = SpineUtils.Names.ToList();
|
||||
comboBox_Version.DisplayMember = "Value";
|
||||
comboBox_Version.ValueMember = "Key";
|
||||
comboBox_Version.SelectedValue = SpineVersion.Auto;
|
||||
@@ -35,7 +36,7 @@ namespace SpineViewer.Dialogs
|
||||
|
||||
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 != SpineVersion.Auto && !Spine.Spine.HasImplementation(version))
|
||||
if (version != SpineVersion.Auto && !Spine.SpineObject.HasImplementation(version))
|
||||
{
|
||||
MessageBox.Info($"{version.GetName()} 版本尚未实现(咕咕咕~)");
|
||||
MessagePopup.Info($"{version.GetName()} 版本尚未实现(咕咕咕~)");
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
110
SpineViewer/Dialogs/ConvertFileFormatDialog.Designer.cs
generated
110
SpineViewer/Dialogs/ConvertFileFormatDialog.Designer.cs
generated
@@ -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,10 +45,15 @@
|
||||
button_Cancel = new Button();
|
||||
label2 = new Label();
|
||||
skelFileListBox = new SpineViewer.Controls.SkelFileListBox();
|
||||
tableLayoutPanel3 = new TableLayoutPanel();
|
||||
textBox_OutputDir = new TextBox();
|
||||
button_SelectOutputDir = new Button();
|
||||
folderBrowserDialog_Output = new FolderBrowserDialog();
|
||||
panel.SuspendLayout();
|
||||
tableLayoutPanel1.SuspendLayout();
|
||||
flowLayoutPanel_TargetFormat.SuspendLayout();
|
||||
tableLayoutPanel2.SuspendLayout();
|
||||
tableLayoutPanel3.SuspendLayout();
|
||||
SuspendLayout();
|
||||
//
|
||||
// panel
|
||||
@@ -57,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
|
||||
@@ -65,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;
|
||||
@@ -104,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
|
||||
@@ -135,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;
|
||||
@@ -151,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;
|
||||
@@ -169,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;
|
||||
@@ -186,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;
|
||||
@@ -222,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;
|
||||
@@ -234,16 +255,56 @@
|
||||
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;
|
||||
//
|
||||
// tableLayoutPanel3
|
||||
//
|
||||
tableLayoutPanel3.AutoSize = true;
|
||||
tableLayoutPanel3.AutoSizeMode = AutoSizeMode.GrowAndShrink;
|
||||
tableLayoutPanel3.ColumnCount = 3;
|
||||
tableLayoutPanel3.ColumnStyles.Add(new ColumnStyle());
|
||||
tableLayoutPanel3.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 100F));
|
||||
tableLayoutPanel3.ColumnStyles.Add(new ColumnStyle());
|
||||
tableLayoutPanel3.Controls.Add(textBox_OutputDir, 1, 0);
|
||||
tableLayoutPanel3.Controls.Add(button_SelectOutputDir, 2, 0);
|
||||
tableLayoutPanel3.Dock = DockStyle.Fill;
|
||||
tableLayoutPanel3.Location = new Point(110, 454);
|
||||
tableLayoutPanel3.Margin = new Padding(0);
|
||||
tableLayoutPanel3.Name = "tableLayoutPanel3";
|
||||
tableLayoutPanel3.RowCount = 1;
|
||||
tableLayoutPanel3.RowStyles.Add(new RowStyle(SizeType.Percent, 100F));
|
||||
tableLayoutPanel3.Size = new Size(841, 40);
|
||||
tableLayoutPanel3.TabIndex = 22;
|
||||
//
|
||||
// textBox_OutputDir
|
||||
//
|
||||
textBox_OutputDir.Anchor = AnchorStyles.Left | AnchorStyles.Right;
|
||||
textBox_OutputDir.Location = new Point(3, 5);
|
||||
textBox_OutputDir.Name = "textBox_OutputDir";
|
||||
textBox_OutputDir.Size = new Size(797, 30);
|
||||
textBox_OutputDir.TabIndex = 1;
|
||||
//
|
||||
// button_SelectOutputDir
|
||||
//
|
||||
button_SelectOutputDir.Anchor = AnchorStyles.Left | AnchorStyles.Right;
|
||||
button_SelectOutputDir.AutoSize = true;
|
||||
button_SelectOutputDir.AutoSizeMode = AutoSizeMode.GrowAndShrink;
|
||||
button_SelectOutputDir.Location = new Point(806, 3);
|
||||
button_SelectOutputDir.Name = "button_SelectOutputDir";
|
||||
button_SelectOutputDir.Size = new Size(32, 34);
|
||||
button_SelectOutputDir.TabIndex = 2;
|
||||
button_SelectOutputDir.Text = "...";
|
||||
button_SelectOutputDir.UseVisualStyleBackColor = true;
|
||||
button_SelectOutputDir.Click += button_SelectOutputDir_Click;
|
||||
//
|
||||
// ConvertFileFormatDialog
|
||||
//
|
||||
AcceptButton = button_Ok;
|
||||
AutoScaleDimensions = new SizeF(11F, 24F);
|
||||
AutoScaleMode = AutoScaleMode.Font;
|
||||
CancelButton = button_Cancel;
|
||||
ClientSize = new Size(1051, 538);
|
||||
ClientSize = new Size(1051, 702);
|
||||
Controls.Add(panel);
|
||||
FormBorderStyle = FormBorderStyle.FixedDialog;
|
||||
Icon = (Icon)resources.GetObject("$this.Icon");
|
||||
@@ -259,6 +320,8 @@
|
||||
flowLayoutPanel_TargetFormat.ResumeLayout(false);
|
||||
flowLayoutPanel_TargetFormat.PerformLayout();
|
||||
tableLayoutPanel2.ResumeLayout(false);
|
||||
tableLayoutPanel3.ResumeLayout(false);
|
||||
tableLayoutPanel3.PerformLayout();
|
||||
ResumeLayout(false);
|
||||
}
|
||||
|
||||
@@ -279,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;
|
||||
}
|
||||
}
|
||||
@@ -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,13 +26,13 @@ namespace SpineViewer.Dialogs
|
||||
{
|
||||
InitializeComponent();
|
||||
|
||||
comboBox_SourceVersion.DataSource = SpineHelper.Names.ToList();
|
||||
comboBox_SourceVersion.DataSource = SpineUtils.Names.ToList();
|
||||
comboBox_SourceVersion.DisplayMember = "Value";
|
||||
comboBox_SourceVersion.ValueMember = "Key";
|
||||
comboBox_SourceVersion.SelectedValue = SpineVersion.Auto;
|
||||
|
||||
// 目标版本不包含自动
|
||||
var versionsWithoutAuto = SpineHelper.Names.ToDictionary();
|
||||
var versionsWithoutAuto = SpineUtils.Names.ToDictionary();
|
||||
versionsWithoutAuto.Remove(SpineVersion.Auto);
|
||||
comboBox_TargetVersion.DataSource = versionsWithoutAuto.ToList();
|
||||
comboBox_TargetVersion.DisplayMember = "Value";
|
||||
@@ -36,8 +40,17 @@ namespace SpineViewer.Dialogs
|
||||
comboBox_TargetVersion.SelectedValue = SpineVersion.V38;
|
||||
}
|
||||
|
||||
private void button_SelectOutputDir_Click(object sender, EventArgs e)
|
||||
{
|
||||
if (folderBrowserDialog_Output.ShowDialog() != DialogResult.OK)
|
||||
return;
|
||||
|
||||
textBox_OutputDir.Text = Path.GetFullPath(folderBrowserDialog_Output.SelectedPath);
|
||||
}
|
||||
|
||||
private void button_Ok_Click(object sender, EventArgs e)
|
||||
{
|
||||
var outputDir = textBox_OutputDir.Text;
|
||||
var sourceVersion = (SpineVersion)comboBox_SourceVersion.SelectedValue;
|
||||
var targetVersion = (SpineVersion)comboBox_TargetVersion.SelectedValue;
|
||||
var jsonTarget = radioButton_JsonTarget.Checked;
|
||||
@@ -46,32 +59,62 @@ namespace SpineViewer.Dialogs
|
||||
|
||||
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 != SpineVersion.Auto && !SkeletonConverter.HasImplementation(sourceVersion))
|
||||
{
|
||||
MessageBox.Info($"{sourceVersion.GetName()} 版本尚未实现(咕咕咕~)");
|
||||
MessagePopup.Info($"{sourceVersion.GetName()} 版本尚未实现(咕咕咕~)");
|
||||
return;
|
||||
}
|
||||
|
||||
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, SpineVersion sourceVersion, SpineVersion targetVersion, bool jsonTarget)
|
||||
public class ConvertFileFormatDialogResult(string? outputDir, string[] skelPaths, SpineVersion sourceVersion, SpineVersion targetVersion, bool jsonTarget)
|
||||
{
|
||||
/// <summary>
|
||||
/// 输出文件夹, 如果为空, 则将转换后的文件转换到各自的文件夹下
|
||||
/// </summary>
|
||||
public string? OutputDir => outputDir;
|
||||
|
||||
/// <summary>
|
||||
/// 骨骼文件路径列表
|
||||
/// </summary>
|
||||
|
||||
@@ -117,6 +117,9 @@
|
||||
<resheader name="writer">
|
||||
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
||||
</resheader>
|
||||
<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">
|
||||
<value>
|
||||
|
||||
@@ -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("已复制");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
17
SpineViewer/Dialogs/ExportDialog.Designer.cs
generated
17
SpineViewer/Dialogs/ExportDialog.Designer.cs
generated
@@ -47,7 +47,7 @@
|
||||
panel1.Location = new Point(0, 0);
|
||||
panel1.Name = "panel1";
|
||||
panel1.Padding = new Padding(50, 15, 50, 10);
|
||||
panel1.Size = new Size(710, 698);
|
||||
panel1.Size = new Size(793, 841);
|
||||
panel1.TabIndex = 2;
|
||||
//
|
||||
// tableLayoutPanel1
|
||||
@@ -65,7 +65,7 @@
|
||||
tableLayoutPanel1.RowStyles.Add(new RowStyle());
|
||||
tableLayoutPanel1.RowStyles.Add(new RowStyle(SizeType.Absolute, 20F));
|
||||
tableLayoutPanel1.RowStyles.Add(new RowStyle(SizeType.Absolute, 20F));
|
||||
tableLayoutPanel1.Size = new Size(610, 673);
|
||||
tableLayoutPanel1.Size = new Size(693, 816);
|
||||
tableLayoutPanel1.TabIndex = 0;
|
||||
//
|
||||
// propertyGrid_ExportArgs
|
||||
@@ -74,7 +74,7 @@
|
||||
propertyGrid_ExportArgs.Location = new Point(3, 3);
|
||||
propertyGrid_ExportArgs.Name = "propertyGrid_ExportArgs";
|
||||
propertyGrid_ExportArgs.PropertySort = PropertySort.Categorized;
|
||||
propertyGrid_ExportArgs.Size = new Size(604, 594);
|
||||
propertyGrid_ExportArgs.Size = new Size(687, 737);
|
||||
propertyGrid_ExportArgs.TabIndex = 1;
|
||||
propertyGrid_ExportArgs.ToolbarVisible = false;
|
||||
//
|
||||
@@ -88,18 +88,18 @@
|
||||
tableLayoutPanel2.Controls.Add(button_Ok, 0, 0);
|
||||
tableLayoutPanel2.Controls.Add(button_Cancel, 1, 0);
|
||||
tableLayoutPanel2.Dock = DockStyle.Bottom;
|
||||
tableLayoutPanel2.Location = new Point(3, 630);
|
||||
tableLayoutPanel2.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(604, 40);
|
||||
tableLayoutPanel2.Size = new Size(687, 40);
|
||||
tableLayoutPanel2.TabIndex = 10;
|
||||
//
|
||||
// button_Ok
|
||||
//
|
||||
button_Ok.Anchor = AnchorStyles.Bottom | AnchorStyles.Right;
|
||||
button_Ok.Location = new Point(160, 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);
|
||||
@@ -111,7 +111,7 @@
|
||||
// button_Cancel
|
||||
//
|
||||
button_Cancel.Anchor = AnchorStyles.Bottom | AnchorStyles.Left;
|
||||
button_Cancel.Location = new Point(332, 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);
|
||||
@@ -126,9 +126,8 @@
|
||||
AutoScaleDimensions = new SizeF(11F, 24F);
|
||||
AutoScaleMode = AutoScaleMode.Font;
|
||||
CancelButton = button_Cancel;
|
||||
ClientSize = new Size(710, 698);
|
||||
ClientSize = new Size(793, 841);
|
||||
Controls.Add(panel1);
|
||||
FormBorderStyle = FormBorderStyle.FixedDialog;
|
||||
Icon = (Icon)resources.GetObject("$this.Icon");
|
||||
MaximizeBox = false;
|
||||
MinimizeBox = false;
|
||||
|
||||
@@ -1,84 +1,71 @@
|
||||
using SpineViewer.Exporter;
|
||||
using SpineViewer.Utils;
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using System.Data;
|
||||
using System.Diagnostics;
|
||||
using System.Drawing;
|
||||
using System.Drawing.Design;
|
||||
using System.Drawing.Imaging;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows.Forms;
|
||||
using SpineViewer.Spine.SpineExporter;
|
||||
|
||||
namespace SpineViewer.Dialogs
|
||||
{
|
||||
public partial class ExportDialog: Form
|
||||
public partial class ExportDialog : Form
|
||||
{
|
||||
/// <summary>
|
||||
/// 要绑定的导出参数
|
||||
/// </summary>
|
||||
public required ExportArgs ExportArgs
|
||||
{
|
||||
get => propertyGrid_ExportArgs.SelectedObject as ExportArgs;
|
||||
init
|
||||
{
|
||||
propertyGrid_ExportArgs.SelectedObject = value;
|
||||
private readonly ExporterProperty 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
|
||||
}
|
||||
}
|
||||
|
||||
public ExportDialog()
|
||||
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 (ExportArgs.Validate() is string error)
|
||||
{
|
||||
MessageBox.Info(error, "参数错误");
|
||||
if (wrapper.Exporter.Validate() is string error)
|
||||
{
|
||||
MessagePopup.Info(error, "参数错误");
|
||||
return;
|
||||
}
|
||||
DialogResult = DialogResult.OK;
|
||||
|
||||
4
SpineViewer/Dialogs/OpenSpineDialog.Designer.cs
generated
4
SpineViewer/Dialogs/OpenSpineDialog.Designer.cs
generated
@@ -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
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using SpineViewer.Spine;
|
||||
using SpineViewer.Utils;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
@@ -20,7 +21,7 @@ namespace SpineViewer.Dialogs
|
||||
public OpenSpineDialog()
|
||||
{
|
||||
InitializeComponent();
|
||||
comboBox_Version.DataSource = SpineHelper.Names.ToList();
|
||||
comboBox_Version.DataSource = SpineUtils.Names.ToList();
|
||||
comboBox_Version.DisplayMember = "Value";
|
||||
comboBox_Version.ValueMember = "Key";
|
||||
comboBox_Version.SelectedValue = SpineVersion.Auto;
|
||||
@@ -57,7 +58,7 @@ namespace SpineViewer.Dialogs
|
||||
|
||||
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 != SpineVersion.Auto && !Spine.Spine.HasImplementation(version))
|
||||
if (version != SpineVersion.Auto && !Spine.SpineObject.HasImplementation(version))
|
||||
{
|
||||
MessageBox.Info($"{version.GetName()} 版本尚未实现(咕咕咕~)");
|
||||
MessagePopup.Info($"{version.GetName()} 版本尚未实现(咕咕咕~)");
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using NLog;
|
||||
using SpineViewer.Utils;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
@@ -13,7 +14,7 @@ namespace SpineViewer.Dialogs
|
||||
{
|
||||
public partial class ProgressDialog : Form
|
||||
{
|
||||
private Logger logger = LogManager.GetCurrentClassLogger();
|
||||
private readonly Logger logger = LogManager.GetCurrentClassLogger();
|
||||
|
||||
public ProgressDialog()
|
||||
{
|
||||
@@ -51,7 +52,7 @@ namespace SpineViewer.Dialogs
|
||||
if (e.Error != null)
|
||||
{
|
||||
logger.Error(e.Error.ToString());
|
||||
MessageBox.Error(e.Error.ToString(), "执行出错");
|
||||
MessagePopup.Error(e.Error.ToString(), "执行出错");
|
||||
DialogResult = DialogResult.Abort;
|
||||
}
|
||||
else if (e.Cancelled)
|
||||
|
||||
@@ -1,93 +0,0 @@
|
||||
using SpineViewer.Spine;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using System.Drawing.Design;
|
||||
using System.Drawing.Imaging;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace SpineViewer.Exporter
|
||||
{
|
||||
/// <summary>
|
||||
/// 导出参数基类
|
||||
/// </summary>
|
||||
public abstract class ExportArgs : ImplementationResolver<ExportArgs, ExportImplementationAttribute, ExportType>
|
||||
{
|
||||
/// <summary>
|
||||
/// 创建指定类型导出参数
|
||||
/// </summary>
|
||||
/// <param name="exportType">导出类型</param>
|
||||
/// <param name="resolution">分辨率</param>
|
||||
/// <param name="view">导出视图</param>
|
||||
/// <param name="renderSelectedOnly">仅渲染选中</param>
|
||||
/// <returns>返回与指定 <paramref name="exportType"/> 匹配的导出参数实例</returns>
|
||||
public static ExportArgs New(ExportType exportType, Size resolution, SFML.Graphics.View view, bool renderSelectedOnly)
|
||||
=> New(exportType, [resolution, view, renderSelectedOnly]);
|
||||
|
||||
public ExportArgs(Size resolution, SFML.Graphics.View view, bool renderSelectedOnly)
|
||||
{
|
||||
Resolution = resolution;
|
||||
View = view;
|
||||
RenderSelectedOnly = renderSelectedOnly;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 输出文件夹
|
||||
/// </summary>
|
||||
[Editor(typeof(FolderNameEditor), typeof(UITypeEditor))]
|
||||
[Category("[0] 导出"), DisplayName("输出文件夹"), Description("逐个导出时可以留空,将逐个导出到模型自身所在目录")]
|
||||
public string? OutputDir { get; set; } = null;
|
||||
|
||||
/// <summary>
|
||||
/// 导出单个
|
||||
/// </summary>
|
||||
[Category("[0] 导出"), DisplayName("导出单个"), Description("是否将模型在同一个画面上导出单个文件,否则逐个导出模型")]
|
||||
public bool ExportSingle { get; set; } = false;
|
||||
|
||||
/// <summary>
|
||||
/// 画面分辨率
|
||||
/// </summary>
|
||||
[TypeConverter(typeof(SizeConverter))]
|
||||
[Category("[0] 导出"), DisplayName("分辨率"), Description("画面的宽高像素大小,请在预览画面参数面板进行调整")]
|
||||
public Size Resolution { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 渲染视窗
|
||||
/// </summary>
|
||||
[Category("[0] 导出"), DisplayName("视图"), Description("画面的视图参数,请在预览画面参数面板进行调整")]
|
||||
public SFML.Graphics.View View { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 是否仅渲染选中
|
||||
/// </summary>
|
||||
[Category("[0] 导出"), DisplayName("仅渲染选中"), Description("是否仅导出选中的模型,请在预览画面参数面板进行调整")]
|
||||
public bool RenderSelectedOnly { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 背景颜色
|
||||
/// </summary>
|
||||
[Editor(typeof(SFMLColorEditor), typeof(UITypeEditor))]
|
||||
[TypeConverter(typeof(SFMLColorConverter))]
|
||||
[Category("[0] 导出"), DisplayName("背景颜色"), Description("要使用的背景色, 格式为 #RRGGBBAA")]
|
||||
public SFML.Graphics.Color BackgroundColor { get; set; } = SFML.Graphics.Color.Transparent;
|
||||
|
||||
/// <summary>
|
||||
/// 检查参数是否合法并规范化参数值, 否则返回用户错误原因
|
||||
/// </summary>
|
||||
public virtual string? Validate()
|
||||
{
|
||||
if (!string.IsNullOrEmpty(OutputDir) && File.Exists(OutputDir))
|
||||
return "输出文件夹无效";
|
||||
if (!string.IsNullOrEmpty(OutputDir) && !Directory.Exists(OutputDir))
|
||||
return $"文件夹 {OutputDir} 不存在";
|
||||
if (ExportSingle && string.IsNullOrEmpty(OutputDir))
|
||||
return "导出单个时必须提供输出文件夹";
|
||||
|
||||
OutputDir = string.IsNullOrEmpty(OutputDir) ? null : Path.GetFullPath(OutputDir);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,135 +0,0 @@
|
||||
using FFMpegCore.Pipes;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Drawing.Imaging;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace SpineViewer.Exporter
|
||||
{
|
||||
/// <summary>
|
||||
/// 导出类型
|
||||
/// </summary>
|
||||
public enum ExportType
|
||||
{
|
||||
Frame,
|
||||
FrameSequence,
|
||||
GIF,
|
||||
MKV,
|
||||
MP4,
|
||||
MOV,
|
||||
WebM
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 导出实现类标记
|
||||
/// </summary>
|
||||
[AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = false)]
|
||||
public class ExportImplementationAttribute(ExportType exportType) : Attribute, IImplementationKey<ExportType>
|
||||
{
|
||||
public ExportType ImplementationKey { get; private set; } = exportType;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// SFML.Graphics.Image 帧对象包装类
|
||||
/// </summary>
|
||||
public class SFMLImageVideoFrame(SFML.Graphics.Image image) : IVideoFrame, IDisposable
|
||||
{
|
||||
public int Width => (int)image.Size.X;
|
||||
public int Height => (int)image.Size.Y;
|
||||
public string Format => "rgba";
|
||||
public void Serialize(Stream pipe) => pipe.Write(image.Pixels);
|
||||
public async Task SerializeAsync(Stream pipe, CancellationToken token) => await pipe.WriteAsync(image.Pixels, token);
|
||||
public void Dispose() => image.Dispose();
|
||||
|
||||
/// <summary>
|
||||
/// Save the contents of the image to a file
|
||||
/// </summary>
|
||||
/// <param name="filename">Path of the file to save (overwritten if already exist)</param>
|
||||
/// <returns>True if saving was successful</returns>
|
||||
public bool SaveToFile(string filename) => image.SaveToFile(filename);
|
||||
|
||||
/// <summary>
|
||||
/// Save the image to a buffer in memory The format of the image must be specified.
|
||||
/// The supported image formats are bmp, png, tga and jpg. This function fails if
|
||||
/// the image is empty, or if the format was invalid.
|
||||
/// </summary>
|
||||
/// <param name="output">Byte array filled with encoded data</param>
|
||||
/// <param name="format">Encoding format to use</param>
|
||||
/// <returns>True if saving was successful</returns>
|
||||
public bool SaveToMemory(out byte[] output, string format) => image.SaveToMemory(out output, format);
|
||||
|
||||
/// <summary>
|
||||
/// 获取 Winforms Bitmap 对象
|
||||
/// </summary>
|
||||
public Bitmap CopyToBitmap()
|
||||
{
|
||||
image.SaveToMemory(out var imgBuffer, "bmp");
|
||||
using var stream = new MemoryStream(imgBuffer);
|
||||
return new(new Bitmap(stream)); // 必须重复构造一个副本才能摆脱对流的依赖, 否则之后使用会报错
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 为帧导出创建的辅助类
|
||||
/// </summary>
|
||||
public static class ExportHelper
|
||||
{
|
||||
/// <summary>
|
||||
/// 根据 Bitmap 文件格式获取合适的文件后缀
|
||||
/// </summary>
|
||||
public static string GetSuffix(this ImageFormat imageFormat)
|
||||
{
|
||||
if (imageFormat == ImageFormat.Icon) return ".ico";
|
||||
else if (imageFormat == ImageFormat.Exif) return ".jpeg";
|
||||
else return $".{imageFormat.ToString().ToLower()}";
|
||||
}
|
||||
|
||||
#region 包围盒辅助函数
|
||||
|
||||
/// <summary>
|
||||
/// 获取某个包围盒下合适的视图
|
||||
/// </summary>
|
||||
public static SFML.Graphics.View GetView(this RectangleF bounds, Size resolution, Padding padding)
|
||||
=> bounds.GetView((uint)resolution.Width, (uint)resolution.Height, (uint)padding.Left, (uint)padding.Right, (uint)padding.Top, (uint)padding.Bottom);
|
||||
|
||||
/// <summary>
|
||||
/// 获取某个包围盒下合适的视图
|
||||
/// </summary>
|
||||
public static SFML.Graphics.View GetView(this RectangleF bounds, uint width, uint height, Padding padding)
|
||||
=> bounds.GetView(width, height, (uint)padding.Left, (uint)padding.Right, (uint)padding.Top, (uint)padding.Bottom);
|
||||
|
||||
/// <summary>
|
||||
/// 获取某个包围盒下合适的视图
|
||||
/// </summary>
|
||||
public static SFML.Graphics.View GetView(this RectangleF bounds, Size resolution, uint paddingL = 1, uint paddingR = 1, uint paddingT = 1, uint paddingB = 1)
|
||||
=> bounds.GetView((uint)resolution.Width, (uint)resolution.Height, paddingL, paddingR, paddingT, paddingB);
|
||||
|
||||
/// <summary>
|
||||
/// 获取某个包围盒下合适的视图
|
||||
/// </summary>
|
||||
public static SFML.Graphics.View GetView(this RectangleF bounds, uint width, uint height, uint paddingL = 1, uint paddingR = 1, uint paddingT = 1, uint paddingB = 1)
|
||||
{
|
||||
float sizeX = bounds.Width;
|
||||
float sizeY = bounds.Height;
|
||||
float innerW = width - paddingL - paddingR;
|
||||
float innerH = height - paddingT - paddingB;
|
||||
|
||||
float scale = 1;
|
||||
if (sizeY / sizeX < innerH / innerW)
|
||||
scale = sizeX / innerW; // 相同的 X, 视窗 Y 更大
|
||||
else
|
||||
scale = sizeY / innerH; // 相同的 Y, 视窗 X 更大
|
||||
|
||||
var x = bounds.X + bounds.Width / 2 + (paddingL - (float)paddingR) * scale;
|
||||
var y = bounds.Y + bounds.Height / 2 + (paddingT - (float)paddingB) * scale;
|
||||
var viewX = width * scale;
|
||||
var viewY = height * scale;
|
||||
|
||||
return new(new(x, y), new(viewX, -viewY));
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
@@ -1,103 +0,0 @@
|
||||
using NLog;
|
||||
using SpineViewer.Spine;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace SpineViewer.Exporter
|
||||
{
|
||||
/// <summary>
|
||||
/// 导出器基类
|
||||
/// </summary>
|
||||
public abstract class Exporter(ExportArgs exportArgs) : ImplementationResolver<Exporter, ExportImplementationAttribute, ExportType>
|
||||
{
|
||||
/// <summary>
|
||||
/// 创建指定类型导出器
|
||||
/// </summary>
|
||||
/// <param name="exportType">导出类型</param>
|
||||
/// <param name="exportArgs">与 <paramref name="exportType"/> 匹配的导出参数</param>
|
||||
/// <returns>与 <paramref name="exportType"/> 匹配的导出器</returns>
|
||||
public static Exporter New(ExportType exportType, ExportArgs exportArgs) => New(exportType, [exportArgs]);
|
||||
|
||||
/// <summary>
|
||||
/// 日志器
|
||||
/// </summary>
|
||||
protected Logger logger = LogManager.GetCurrentClassLogger();
|
||||
|
||||
/// <summary>
|
||||
/// 导出参数
|
||||
/// </summary>
|
||||
public ExportArgs ExportArgs { get; } = exportArgs;
|
||||
|
||||
/// <summary>
|
||||
/// 可用于文件名的时间戳字符串
|
||||
/// </summary>
|
||||
protected readonly string timestamp = DateTime.Now.ToString("yyMMddHHmmss");
|
||||
|
||||
/// <summary>
|
||||
/// 获取供渲染的 SFML.Graphics.RenderTexture
|
||||
/// </summary>
|
||||
private SFML.Graphics.RenderTexture GetRenderTexture()
|
||||
{
|
||||
var tex = new SFML.Graphics.RenderTexture((uint)ExportArgs.Resolution.Width, (uint)ExportArgs.Resolution.Height);
|
||||
tex.Clear(SFML.Graphics.Color.Transparent);
|
||||
tex.SetView(ExportArgs.View);
|
||||
return tex;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取单个模型的单帧画面
|
||||
/// </summary>
|
||||
protected SFMLImageVideoFrame GetFrame(Spine.Spine spine)
|
||||
{
|
||||
// tex 必须临时创建, 随用随取, 防止出现跨线程的情况
|
||||
using var tex = GetRenderTexture();
|
||||
tex.Clear(ExportArgs.BackgroundColor);
|
||||
tex.Draw(spine);
|
||||
tex.Display();
|
||||
return new(tex.Texture.CopyToImage());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取模型列表的单帧画面
|
||||
/// </summary>
|
||||
protected SFMLImageVideoFrame GetFrame(Spine.Spine[] spinesToRender)
|
||||
{
|
||||
// tex 必须临时创建, 随用随取, 防止出现跨线程的情况
|
||||
using var tex = GetRenderTexture();
|
||||
tex.Clear(ExportArgs.BackgroundColor);
|
||||
foreach (var spine in spinesToRender) tex.Draw(spine);
|
||||
tex.Display();
|
||||
return new(tex.Texture.CopyToImage());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 每个模型在同一个画面进行导出
|
||||
/// </summary>
|
||||
protected abstract void ExportSingle(Spine.Spine[] spinesToRender, BackgroundWorker? worker = null);
|
||||
|
||||
/// <summary>
|
||||
/// 每个模型独立导出
|
||||
/// </summary>
|
||||
protected abstract void ExportIndividual(Spine.Spine[] spinesToRender, BackgroundWorker? worker = null);
|
||||
|
||||
/// <summary>
|
||||
/// 执行导出
|
||||
/// </summary>
|
||||
/// <param name="spines">要进行导出的 Spine 列表</param>
|
||||
/// <param name="worker">用来执行该函数的 worker</param>
|
||||
public virtual void Export(Spine.Spine[] spines, BackgroundWorker? worker = null)
|
||||
{
|
||||
var spinesToRender = spines.Where(sp => !ExportArgs.RenderSelectedOnly || sp.IsSelected).Reverse().ToArray();
|
||||
|
||||
if (ExportArgs.ExportSingle) ExportSingle(spinesToRender, worker);
|
||||
else ExportIndividual(spinesToRender, worker);
|
||||
|
||||
logger.LogCurrentProcessMemoryUsage();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,58 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using System.Drawing.Imaging;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace SpineViewer.Exporter.Implementations.ExportArgs
|
||||
{
|
||||
/// <summary>
|
||||
/// 单帧画面导出参数
|
||||
/// </summary>
|
||||
[ExportImplementation(ExportType.Frame)]
|
||||
public class FrameExportArgs : SpineViewer.Exporter.ExportArgs
|
||||
{
|
||||
public FrameExportArgs(Size resolution, SFML.Graphics.View view, bool renderSelectedOnly) : base(resolution, view, renderSelectedOnly) { }
|
||||
|
||||
/// <summary>
|
||||
/// 单帧画面格式
|
||||
/// </summary>
|
||||
[TypeConverter(typeof(ImageFormatConverter))]
|
||||
[Category("[1] 单帧画面"), DisplayName("图像格式")]
|
||||
public ImageFormat ImageFormat
|
||||
{
|
||||
get => imageFormat;
|
||||
set
|
||||
{
|
||||
if (value == ImageFormat.MemoryBmp) value = ImageFormat.Bmp;
|
||||
imageFormat = value;
|
||||
}
|
||||
}
|
||||
private ImageFormat imageFormat = ImageFormat.Png;
|
||||
|
||||
/// <summary>
|
||||
/// 文件名后缀
|
||||
/// </summary>
|
||||
[Category("[1] 单帧画面"), DisplayName("文件名后缀"), Description("与图像格式匹配的文件名后缀")]
|
||||
public string FileSuffix { get => imageFormat.GetSuffix(); }
|
||||
|
||||
/// <summary>
|
||||
/// DPI
|
||||
/// </summary>
|
||||
[TypeConverter(typeof(SizeFConverter))]
|
||||
[Category("[1] 单帧画面"), DisplayName("DPI"), Description("导出图像的每英寸像素数,用于调整图像的物理尺寸")]
|
||||
public SizeF DPI
|
||||
{
|
||||
get => dpi;
|
||||
set
|
||||
{
|
||||
if (value.Width <= 0) value.Width = 144;
|
||||
if (value.Height <= 0) value.Height = 144;
|
||||
dpi = value;
|
||||
}
|
||||
}
|
||||
private SizeF dpi = new(144, 144);
|
||||
}
|
||||
}
|
||||
@@ -1,26 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using System.Drawing.Imaging;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace SpineViewer.Exporter.Implementations.ExportArgs
|
||||
{
|
||||
/// <summary>
|
||||
/// 帧序列导出参数
|
||||
/// </summary>
|
||||
[ExportImplementation(ExportType.FrameSequence)]
|
||||
public class FrameSequenceExportArgs : VideoExportArgs
|
||||
{
|
||||
public FrameSequenceExportArgs(Size resolution, SFML.Graphics.View view, bool renderSelectedOnly) : base(resolution, view, renderSelectedOnly) { }
|
||||
|
||||
/// <summary>
|
||||
/// 文件名后缀
|
||||
/// </summary>
|
||||
[TypeConverter(typeof(SFMLImageFileSuffixConverter))]
|
||||
[Category("[2] 帧序列参数"), DisplayName("文件名后缀"), Description("帧文件的后缀,同时决定帧图像格式")]
|
||||
public string FileSuffix { get; set; } = ".png";
|
||||
}
|
||||
}
|
||||
@@ -1,54 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace SpineViewer.Exporter.Implementations.ExportArgs
|
||||
{
|
||||
/// <summary>
|
||||
/// GIF 导出参数
|
||||
/// </summary>
|
||||
[ExportImplementation(ExportType.GIF)]
|
||||
public class GifExportArgs : VideoExportArgs
|
||||
{
|
||||
public GifExportArgs(Size resolution, SFML.Graphics.View view, bool renderSelectedOnly) : base(resolution, view, renderSelectedOnly)
|
||||
{
|
||||
// 给一个纯白的背景
|
||||
BackgroundColor = new(255, 255, 255, 0);
|
||||
|
||||
// GIF 的帧率不能太高, 超过 50 帧反而会变慢
|
||||
FPS = 12;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 调色板最大颜色数量
|
||||
/// </summary>
|
||||
[Category("[2] GIF 参数"), DisplayName("调色板最大颜色数量"), Description("设置调色板使用的最大颜色数量, 越多则色彩保留程度越高")]
|
||||
public uint MaxColors { get => maxColors; set => maxColors = Math.Clamp(value, 2, 256); }
|
||||
private uint maxColors = 256;
|
||||
|
||||
/// <summary>
|
||||
/// 透明度阈值
|
||||
/// </summary>
|
||||
[Category("[2] GIF 参数"), DisplayName("透明度阈值"), Description("小于该值的像素点会被认为是透明像素")]
|
||||
public byte AlphaThreshold { get => alphaThreshold; set => alphaThreshold = value; }
|
||||
private byte alphaThreshold = 128;
|
||||
|
||||
/// <summary>
|
||||
/// 获取构造好的 FFMpegCore 自定义参数
|
||||
/// </summary>
|
||||
[Browsable(false)]
|
||||
public string FFMpegCoreCustomArguments
|
||||
{
|
||||
get
|
||||
{
|
||||
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}";
|
||||
return $"-filter_complex \"{v};{s0};{s1}\"";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,35 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace SpineViewer.Exporter.Implementations.ExportArgs
|
||||
{
|
||||
/// <summary>
|
||||
/// MP4 导出参数
|
||||
/// </summary>
|
||||
[ExportImplementation(ExportType.MP4)]
|
||||
public class Mp4ExportArgs : VideoExportArgs
|
||||
{
|
||||
public Mp4ExportArgs(Size resolution, SFML.Graphics.View view, bool renderSelectedOnly) : base(resolution, view, renderSelectedOnly)
|
||||
{
|
||||
// MP4 默认用绿幕
|
||||
BackgroundColor = new(0, 255, 0, 0);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// CRF
|
||||
/// </summary>
|
||||
[Category("[2] MP4 参数"), DisplayName("CRF"), Description("Constant Rate Factor, 取值范围 0-63, 建议范围 18-28, 默认取值 23, 数值越小则输出质量越高")]
|
||||
public int CRF { get => crf; set => crf = Math.Clamp(value, 0, 63); }
|
||||
private int crf = 23;
|
||||
|
||||
/// <summary>
|
||||
/// 编码器 TODO: 增加其他编码器
|
||||
/// </summary>
|
||||
[Category("[2] MP4 参数"), DisplayName("编码器"), Description("要使用的编码器")]
|
||||
public string Codec { get => "libx264"; }
|
||||
}
|
||||
}
|
||||
@@ -1,43 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace SpineViewer.Exporter.Implementations.ExportArgs
|
||||
{
|
||||
/// <summary>
|
||||
/// 视频导出参数基类
|
||||
/// </summary>
|
||||
public abstract class VideoExportArgs : SpineViewer.Exporter.ExportArgs
|
||||
{
|
||||
public VideoExportArgs(Size resolution, SFML.Graphics.View view, bool renderSelectedOnly) : base(resolution, view, renderSelectedOnly) { }
|
||||
|
||||
/// <summary>
|
||||
/// 导出时长
|
||||
/// </summary>
|
||||
[Category("[1] 视频参数"), DisplayName("时长"), Description("可以从模型列表查看动画时长, 如果小于 0, 则在逐个导出时每个模型使用各自的当前动画时长")]
|
||||
public float Duration
|
||||
{
|
||||
get => duration;
|
||||
set => duration = value < 0 ? -1 : value;
|
||||
}
|
||||
private float duration = -1;
|
||||
|
||||
/// <summary>
|
||||
/// 帧率
|
||||
/// </summary>
|
||||
[Category("[1] 视频参数"), DisplayName("帧率"), Description("每秒画面数")]
|
||||
public float FPS { get; set; } = 60;
|
||||
|
||||
public override string? Validate()
|
||||
{
|
||||
if (base.Validate() is string error)
|
||||
return error;
|
||||
if (ExportSingle && Duration < 0)
|
||||
return "导出单个时导出时长不能为负数";
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,86 +0,0 @@
|
||||
using SpineViewer.Exporter.Implementations.ExportArgs;
|
||||
using SpineViewer.Spine;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace SpineViewer.Exporter.Implementations.Exporter
|
||||
{
|
||||
/// <summary>
|
||||
/// 单帧画面导出器
|
||||
/// </summary>
|
||||
[ExportImplementation(ExportType.Frame)]
|
||||
public class FrameExporter : SpineViewer.Exporter.Exporter
|
||||
{
|
||||
public FrameExporter(FrameExportArgs exportArgs) : base(exportArgs) { }
|
||||
|
||||
protected override void ExportSingle(Spine.Spine[] spinesToRender, BackgroundWorker? worker = null)
|
||||
{
|
||||
var args = (FrameExportArgs)ExportArgs;
|
||||
|
||||
// 导出单个时必定提供输出文件夹
|
||||
var filename = $"frame_{timestamp}{args.FileSuffix}";
|
||||
var savePath = Path.Combine(args.OutputDir, filename);
|
||||
|
||||
worker?.ReportProgress(0, $"已处理 0/1");
|
||||
try
|
||||
{
|
||||
using var frame = GetFrame(spinesToRender);
|
||||
using var img = frame.CopyToBitmap();
|
||||
img.SetResolution(args.DPI.Width, args.DPI.Height);
|
||||
img.Save(savePath, args.ImageFormat);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.Error(ex.ToString());
|
||||
logger.Error("Failed to save single frame");
|
||||
}
|
||||
worker?.ReportProgress(100, $"已处理 1/1");
|
||||
}
|
||||
|
||||
protected override void ExportIndividual(Spine.Spine[] spinesToRender, BackgroundWorker? worker = null)
|
||||
{
|
||||
var args = (FrameExportArgs)ExportArgs;
|
||||
|
||||
int total = spinesToRender.Length;
|
||||
int success = 0;
|
||||
int error = 0;
|
||||
|
||||
worker?.ReportProgress(0, $"已处理 0/{total}");
|
||||
for (int i = 0; i < total; i++)
|
||||
{
|
||||
var spine = spinesToRender[i];
|
||||
|
||||
// 逐个导出时如果提供了输出文件夹, 则全部导出到输出文件夹, 否则输出到各自的文件夹
|
||||
var filename = $"{spine.Name}_{timestamp}{args.FileSuffix}";
|
||||
var savePath = args.OutputDir is null ? Path.Combine(spine.AssetsDir, filename) : Path.Combine(args.OutputDir, filename);
|
||||
|
||||
try
|
||||
{
|
||||
using var frame = GetFrame(spine);
|
||||
using var img = frame.CopyToBitmap();
|
||||
img.SetResolution(args.DPI.Width, args.DPI.Height);
|
||||
img.Save(savePath, args.ImageFormat);
|
||||
success++;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.Error(ex.ToString());
|
||||
logger.Error("Failed to save 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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,81 +0,0 @@
|
||||
using FFMpegCore.Pipes;
|
||||
using FFMpegCore;
|
||||
using SpineViewer.Exporter.Implementations.ExportArgs;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using FFMpegCore.Arguments;
|
||||
using System.Diagnostics;
|
||||
|
||||
namespace SpineViewer.Exporter.Implementations.Exporter
|
||||
{
|
||||
/// <summary>
|
||||
/// GIF 动图导出器
|
||||
/// </summary>
|
||||
[ExportImplementation(ExportType.GIF)]
|
||||
public class GifExporter : VideoExporter
|
||||
{
|
||||
public GifExporter(GifExportArgs exportArgs) : base(exportArgs) { }
|
||||
|
||||
protected override void ExportSingle(Spine.Spine[] spinesToRender, BackgroundWorker? worker = null)
|
||||
{
|
||||
var args = (GifExportArgs)ExportArgs;
|
||||
|
||||
// 导出单个时必定提供输出文件夹
|
||||
var filename = $"{timestamp}_{args.FPS:f0}_{args.MaxColors}_{args.AlphaThreshold}.gif";
|
||||
var savePath = Path.Combine(args.OutputDir, filename);
|
||||
|
||||
var videoFramesSource = new RawVideoPipeSource(GetFrames(spinesToRender, worker)) { FrameRate = args.FPS };
|
||||
try
|
||||
{
|
||||
var ffmpegArgs = FFMpegArguments
|
||||
.FromPipeInput(videoFramesSource)
|
||||
.OutputToFile(savePath, true, options => options
|
||||
.ForceFormat("gif")
|
||||
.WithCustomArgument(args.FFMpegCoreCustomArguments));
|
||||
|
||||
logger.Info("FFMpeg arguments: {}", ffmpegArgs.Arguments);
|
||||
ffmpegArgs.ProcessSynchronously();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.Error(ex.ToString());
|
||||
logger.Error("Failed to export gif {}", savePath);
|
||||
}
|
||||
}
|
||||
|
||||
protected override void ExportIndividual(Spine.Spine[] spinesToRender, BackgroundWorker? worker = null)
|
||||
{
|
||||
var args = (GifExportArgs)ExportArgs;
|
||||
foreach (var spine in spinesToRender)
|
||||
{
|
||||
if (worker?.CancellationPending == true) break; // 取消的日志在 GetFrames 里输出
|
||||
|
||||
// 如果提供了输出文件夹, 则全部导出到输出文件夹, 否则导出到各自的文件夹下
|
||||
var filename = $"{spine.Name}_{timestamp}_{args.FPS:f0}_{args.MaxColors}_{args.AlphaThreshold}.gif";
|
||||
var savePath = Path.Combine(args.OutputDir ?? spine.AssetsDir, filename);
|
||||
|
||||
var videoFramesSource = new RawVideoPipeSource(GetFrames(spine, worker)) { FrameRate = args.FPS };
|
||||
try
|
||||
{
|
||||
var ffmpegArgs = FFMpegArguments
|
||||
.FromPipeInput(videoFramesSource)
|
||||
.OutputToFile(savePath, true, options => options
|
||||
.ForceFormat("gif")
|
||||
.WithCustomArgument(args.FFMpegCoreCustomArguments));
|
||||
|
||||
logger.Info("FFMpeg arguments: {}", ffmpegArgs.Arguments);
|
||||
ffmpegArgs.ProcessSynchronously();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.Error(ex.ToString());
|
||||
logger.Error("Failed to export gif {} {}", savePath, spine.SkelPath);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,85 +0,0 @@
|
||||
using FFMpegCore.Pipes;
|
||||
using FFMpegCore;
|
||||
using SpineViewer.Exporter.Implementations.ExportArgs;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using FFMpegCore.Arguments;
|
||||
using System.Diagnostics;
|
||||
|
||||
namespace SpineViewer.Exporter.Implementations.Exporter
|
||||
{
|
||||
/// <summary>
|
||||
/// MP4 导出器
|
||||
/// </summary>
|
||||
[ExportImplementation(ExportType.MP4)]
|
||||
public class Mp4Exporter : VideoExporter
|
||||
{
|
||||
public Mp4Exporter(Mp4ExportArgs exportArgs) : base(exportArgs) { }
|
||||
|
||||
protected override void ExportSingle(Spine.Spine[] spinesToRender, BackgroundWorker? worker = null)
|
||||
{
|
||||
var args = (Mp4ExportArgs)ExportArgs;
|
||||
|
||||
// 导出单个时必定提供输出文件夹
|
||||
var filename = $"{timestamp}_{args.FPS:f0}_{args.CRF}.mp4";
|
||||
var savePath = Path.Combine(args.OutputDir, filename);
|
||||
|
||||
var videoFramesSource = new RawVideoPipeSource(GetFrames(spinesToRender, worker)) { FrameRate = args.FPS };
|
||||
try
|
||||
{
|
||||
var ffmpegArgs = FFMpegArguments
|
||||
.FromPipeInput(videoFramesSource)
|
||||
.OutputToFile(savePath, true, options => options
|
||||
.ForceFormat("mp4")
|
||||
.WithVideoCodec(args.Codec)
|
||||
.WithConstantRateFactor(args.CRF)
|
||||
.WithFastStart());
|
||||
|
||||
logger.Info("FFMpeg arguments: {}", ffmpegArgs.Arguments);
|
||||
ffmpegArgs.ProcessSynchronously();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.Error(ex.ToString());
|
||||
logger.Error("Failed to export mp4 {}", savePath);
|
||||
}
|
||||
}
|
||||
|
||||
protected override void ExportIndividual(Spine.Spine[] spinesToRender, BackgroundWorker? worker = null)
|
||||
{
|
||||
var args = (Mp4ExportArgs)ExportArgs;
|
||||
foreach (var spine in spinesToRender)
|
||||
{
|
||||
if (worker?.CancellationPending == true) break; // 取消的日志在 GetFrames 里输出
|
||||
|
||||
// 如果提供了输出文件夹, 则全部导出到输出文件夹, 否则导出到各自的文件夹下
|
||||
var filename = $"{spine.Name}_{timestamp}_{args.FPS:f0}_{args.CRF}.mp4";
|
||||
var savePath = Path.Combine(args.OutputDir ?? spine.AssetsDir, filename);
|
||||
|
||||
var videoFramesSource = new RawVideoPipeSource(GetFrames(spine, worker)) { FrameRate = args.FPS };
|
||||
try
|
||||
{
|
||||
var ffmpegArgs = FFMpegArguments
|
||||
.FromPipeInput(videoFramesSource)
|
||||
.OutputToFile(savePath, true, options => options
|
||||
.ForceFormat("mp4")
|
||||
.WithVideoCodec(args.Codec)
|
||||
.WithConstantRateFactor(args.CRF)
|
||||
.WithFastStart());
|
||||
|
||||
logger.Info("FFMpeg arguments: {}", ffmpegArgs.Arguments);
|
||||
ffmpegArgs.ProcessSynchronously();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.Error(ex.ToString());
|
||||
logger.Error("Failed to export mp4 {} {}", savePath, spine.SkelPath);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,82 +0,0 @@
|
||||
using SpineViewer.Exporter.Implementations.ExportArgs;
|
||||
using SpineViewer.Spine;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace SpineViewer.Exporter.Implementations.Exporter
|
||||
{
|
||||
/// <summary>
|
||||
/// 视频导出基类
|
||||
/// </summary>
|
||||
public abstract class VideoExporter : SpineViewer.Exporter.Exporter
|
||||
{
|
||||
public VideoExporter(VideoExportArgs exportArgs) : base(exportArgs) { }
|
||||
|
||||
/// <summary>
|
||||
/// 生成单个模型的帧序列
|
||||
/// </summary>
|
||||
protected IEnumerable<SFMLImageVideoFrame> GetFrames(Spine.Spine spine, BackgroundWorker? worker = null)
|
||||
{
|
||||
var args = (VideoExportArgs)ExportArgs;
|
||||
|
||||
// 独立导出时如果 args.Duration 小于 0 则使用 Track0 的动画时长
|
||||
var duration = args.Duration;
|
||||
if (duration < 0) duration = spine.GetAnimationDuration(spine.Track0Animation); // TODO: 也许可以使用所有轨道的最大值
|
||||
|
||||
float delta = 1f / args.FPS;
|
||||
int total = Math.Max(1, (int)(duration * args.FPS)); // 至少导出 1 帧
|
||||
|
||||
worker?.ReportProgress(0, $"{spine.Name} 已处理 0/{total} 帧");
|
||||
for (int i = 0; i < total; i++)
|
||||
{
|
||||
if (worker?.CancellationPending == true)
|
||||
{
|
||||
logger.Info("Export cancelled");
|
||||
break;
|
||||
}
|
||||
|
||||
var frame = GetFrame(spine);
|
||||
spine.Update(delta);
|
||||
worker?.ReportProgress((int)((i + 1) * 100.0) / total, $"{spine.Name} 已处理 {i + 1}/{total} 帧");
|
||||
yield return frame;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 生成多个模型的帧序列
|
||||
/// </summary>
|
||||
protected IEnumerable<SFMLImageVideoFrame> GetFrames(Spine.Spine[] spinesToRender, BackgroundWorker? worker = null)
|
||||
{
|
||||
// 导出单个时必须根据 args.Duration 决定导出时长
|
||||
var args = (VideoExportArgs)ExportArgs;
|
||||
float delta = 1f / args.FPS;
|
||||
int total = Math.Max(1, (int)(args.Duration * args.FPS)); // 至少导出 1 帧
|
||||
|
||||
worker?.ReportProgress(0, $"已处理 0/{total} 帧");
|
||||
for (int i = 0; i < total; i++)
|
||||
{
|
||||
if (worker?.CancellationPending == true)
|
||||
{
|
||||
logger.Info("Export cancelled");
|
||||
break;
|
||||
}
|
||||
|
||||
var frame = GetFrame(spinesToRender);
|
||||
foreach (var spine in spinesToRender) spine.Update(delta);
|
||||
worker?.ReportProgress((int)((i + 1) * 100.0) / total, $"已处理 {i + 1}/{total} 帧");
|
||||
yield return frame;
|
||||
}
|
||||
}
|
||||
|
||||
public override void Export(Spine.Spine[] spines, BackgroundWorker? worker = null)
|
||||
{
|
||||
// 导出视频格式需要把模型时间都重置到 0
|
||||
foreach (var spine in spines) spine.Track0Animation = spine.Track0Animation; // TODO: 多轨道重置
|
||||
base.Export(spines, worker);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,103 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.ComponentModel;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace SpineViewer.Exporter
|
||||
{
|
||||
public class SFMLImageFileSuffixConverter : StringConverter
|
||||
{
|
||||
private readonly string[] supportedFileSuffix = [".png", ".jpg", ".tga", ".bmp"];
|
||||
|
||||
public override bool GetStandardValuesSupported(ITypeDescriptorContext? context) => true;
|
||||
|
||||
public override bool GetStandardValuesExclusive(ITypeDescriptorContext? context) => true;
|
||||
|
||||
public override StandardValuesCollection? GetStandardValues(ITypeDescriptorContext? context)
|
||||
{
|
||||
return new StandardValuesCollection(supportedFileSuffix);
|
||||
}
|
||||
}
|
||||
|
||||
public class SFMLColorConverter : ExpandableObjectConverter
|
||||
{
|
||||
private class SFMLColorPropertyDescriptor : SimplePropertyDescriptor
|
||||
{
|
||||
public SFMLColorPropertyDescriptor(Type componentType, string name, Type propertyType) : base(componentType, name, propertyType) { }
|
||||
|
||||
public override object? GetValue(object? component)
|
||||
{
|
||||
return component?.GetType().GetField(Name)?.GetValue(component) ?? default;
|
||||
}
|
||||
|
||||
public override void SetValue(object? component, object? value)
|
||||
{
|
||||
component?.GetType().GetField(Name)?.SetValue(component, value);
|
||||
}
|
||||
}
|
||||
|
||||
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 s)
|
||||
{
|
||||
s = s.Trim();
|
||||
if (s.StartsWith("#") && s.Length == 9)
|
||||
{
|
||||
try
|
||||
{
|
||||
// 解析 R, G, B, A 分量,注意16进制解析
|
||||
byte r = byte.Parse(s.Substring(1, 2), NumberStyles.HexNumber);
|
||||
byte g = byte.Parse(s.Substring(3, 2), NumberStyles.HexNumber);
|
||||
byte b = byte.Parse(s.Substring(5, 2), NumberStyles.HexNumber);
|
||||
byte a = byte.Parse(s.Substring(7, 2), NumberStyles.HexNumber);
|
||||
return new SFML.Graphics.Color(r, g, b, a);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
throw new FormatException("无法解析颜色,确保格式为 #RRGGBBAA", ex);
|
||||
}
|
||||
}
|
||||
throw new FormatException("格式错误,正确格式为 #RRGGBBAA");
|
||||
}
|
||||
return base.ConvertFrom(context, culture, value);
|
||||
}
|
||||
|
||||
public override bool CanConvertTo(ITypeDescriptorContext? context, 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 SFML.Graphics.Color color)
|
||||
return $"#{color.R:X2}{color.G:X2}{color.B:X2}{color.A:X2}";
|
||||
return base.ConvertTo(context, culture, value, destinationType);
|
||||
}
|
||||
|
||||
public override PropertyDescriptorCollection GetProperties(ITypeDescriptorContext? context, object value, Attribute[]? attributes)
|
||||
{
|
||||
// 自定义属性集合
|
||||
var properties = new List<PropertyDescriptor>
|
||||
{
|
||||
// 定义 R, G, B, A 四个字段的描述器
|
||||
new SFMLColorPropertyDescriptor(typeof(SFML.Graphics.Color), "R", typeof(byte)),
|
||||
new SFMLColorPropertyDescriptor(typeof(SFML.Graphics.Color), "G", typeof(byte)),
|
||||
new SFMLColorPropertyDescriptor(typeof(SFML.Graphics.Color), "B", typeof(byte)),
|
||||
new SFMLColorPropertyDescriptor(typeof(SFML.Graphics.Color), "A", typeof(byte))
|
||||
};
|
||||
|
||||
// 返回自定义属性集合
|
||||
return new PropertyDescriptorCollection(properties.ToArray());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -5,7 +5,7 @@ using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace SpineViewer
|
||||
namespace SpineViewer.Extensions
|
||||
{
|
||||
public static class NLogExtension
|
||||
{
|
||||
118
SpineViewer/Extensions/SFMLExtension.cs
Normal file
118
SpineViewer/Extensions/SFMLExtension.cs
Normal file
@@ -0,0 +1,118 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace SpineViewer.Extensions
|
||||
{
|
||||
public static class SFMLExtension
|
||||
{
|
||||
/// <summary>
|
||||
/// 获取并集范围
|
||||
/// </summary>
|
||||
public static RectangleF Union(this RectangleF bounds, RectangleF other)
|
||||
{
|
||||
var x = Math.Min(bounds.X, other.X);
|
||||
var y = Math.Min(bounds.Y, other.Y);
|
||||
var w = Math.Max(bounds.Right, other.Right) - x;
|
||||
var h = Math.Max(bounds.Bottom, other.Bottom) - y;
|
||||
return new(x, y, w, h);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取适合指定画布参数下能够覆盖包围盒的画布视区包围盒
|
||||
/// </summary>
|
||||
public static RectangleF GetCanvasBounds(this RectangleF bounds, Size resolution) => GetCanvasBounds(bounds, resolution, new(0), new(0));
|
||||
|
||||
/// <summary>
|
||||
/// 获取适合指定画布参数下能够覆盖包围盒的画布视区包围盒
|
||||
/// </summary>
|
||||
public static RectangleF GetCanvasBounds(this RectangleF bounds, Size resolution, Padding margin) => GetCanvasBounds(bounds, resolution, margin, new(0));
|
||||
|
||||
/// <summary>
|
||||
/// 获取适合指定画布参数下能够覆盖包围盒的画布视区包围盒
|
||||
/// </summary>
|
||||
public static RectangleF GetCanvasBounds(this RectangleF bounds, Size resolution, Padding margin, Padding padding)
|
||||
{
|
||||
float sizeW = bounds.Width;
|
||||
float sizeH = bounds.Height;
|
||||
float innerW = resolution.Width - padding.Horizontal;
|
||||
float innerH = resolution.Height - padding.Vertical;
|
||||
float scale = Math.Max(Math.Abs(sizeW / innerW), Math.Abs(sizeH / innerH)); // 取两方向上较大的缩放比, 以此让画布可以覆盖内容
|
||||
float scaleW = scale * Math.Sign(sizeW);
|
||||
float scaleH = scale * Math.Sign(sizeH);
|
||||
|
||||
innerW *= scaleW;
|
||||
innerH *= scaleH;
|
||||
|
||||
var x = bounds.X - (innerW - sizeW) / 2 - (margin.Left + padding.Left) * scaleW;
|
||||
var y = bounds.Y - (innerH - sizeH) / 2 - (margin.Top + padding.Top) * scaleH;
|
||||
var w = (resolution.Width + margin.Horizontal) * scaleW;
|
||||
var h = (resolution.Height + margin.Vertical) * scaleH;
|
||||
return new(x, y, w, h);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取 Winforms Bitmap 对象, 需要使用 Dispose 释放对象
|
||||
/// </summary>
|
||||
public static Bitmap CopyToBitmap(this SFML.Graphics.Image image)
|
||||
{
|
||||
image.SaveToMemory(out var imgBuffer, "bmp");
|
||||
using var stream = new MemoryStream(imgBuffer);
|
||||
using var bitmap = new Bitmap(stream);
|
||||
return new(bitmap); // 必须重复构造一个副本才能摆脱对流的依赖, 否则之后使用会报错
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取 Winforms Bitmap 对象, 需要使用 Dispose 释放对象
|
||||
/// </summary>
|
||||
public static Bitmap CopyToBitmap(this SFML.Graphics.Texture texture)
|
||||
{
|
||||
using var image = texture.CopyToImage();
|
||||
return image.CopyToBitmap();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取视区的包围盒
|
||||
/// </summary>
|
||||
public static RectangleF GetBounds(this SFML.Graphics.View view)
|
||||
{
|
||||
return new(
|
||||
view.Center.X - view.Size.X / 2,
|
||||
view.Center.Y - view.Size.Y / 2,
|
||||
view.Size.X,
|
||||
view.Size.Y
|
||||
);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 按画布设置视区, 边缘和填充区域将不会出现内容
|
||||
/// </summary>
|
||||
public static void SetViewport(this SFML.Graphics.View view, Size resolution, Padding margin) => SetViewport(view, resolution, margin, new(0));
|
||||
|
||||
/// <summary>
|
||||
/// 按画布设置视区, 边缘和填充区域将不会出现内容
|
||||
/// </summary>
|
||||
public static void SetViewport(this SFML.Graphics.View view, Size resolution, Padding margin, Padding padding)
|
||||
{
|
||||
var innerW = resolution.Width - padding.Horizontal;
|
||||
var innerH = resolution.Height - padding.Vertical;
|
||||
|
||||
float width = resolution.Width + margin.Horizontal;
|
||||
float height = resolution.Height + margin.Vertical;
|
||||
|
||||
view.Viewport = new(
|
||||
(margin.Left + padding.Left) / width,
|
||||
(margin.Top + padding.Top) / height,
|
||||
innerW / width,
|
||||
innerH / height
|
||||
);
|
||||
|
||||
var bounds = view.GetBounds().GetCanvasBounds(new(innerW, innerH));
|
||||
|
||||
view.Center = new(bounds.X + bounds.Width / 2, bounds.Y + bounds.Height / 2);
|
||||
view.Size = new(bounds.Width, bounds.Height);
|
||||
}
|
||||
}
|
||||
}
|
||||
51
SpineViewer/Forms/SpinePreviewFullScreenForm.Designer.cs
generated
Normal file
51
SpineViewer/Forms/SpinePreviewFullScreenForm.Designer.cs
generated
Normal file
@@ -0,0 +1,51 @@
|
||||
namespace SpineViewer.Forms
|
||||
{
|
||||
partial class SpinePreviewFullScreenForm
|
||||
{
|
||||
/// <summary>
|
||||
/// Required designer variable.
|
||||
/// </summary>
|
||||
private System.ComponentModel.IContainer components = null;
|
||||
|
||||
/// <summary>
|
||||
/// Clean up any resources being used.
|
||||
/// </summary>
|
||||
/// <param name="disposing">true if managed resources should be disposed; otherwise, false.</param>
|
||||
protected override void Dispose(bool disposing)
|
||||
{
|
||||
if (disposing && (components != null))
|
||||
{
|
||||
components.Dispose();
|
||||
}
|
||||
base.Dispose(disposing);
|
||||
}
|
||||
|
||||
#region Windows Form Designer generated code
|
||||
|
||||
/// <summary>
|
||||
/// Required method for Designer support - do not modify
|
||||
/// the contents of this method with the code editor.
|
||||
/// </summary>
|
||||
private void InitializeComponent()
|
||||
{
|
||||
SuspendLayout();
|
||||
//
|
||||
// SpinePreviewFullScreenForm
|
||||
//
|
||||
AutoScaleMode = AutoScaleMode.None;
|
||||
ClientSize = new Size(512, 512);
|
||||
ControlBox = false;
|
||||
FormBorderStyle = FormBorderStyle.None;
|
||||
MaximizeBox = false;
|
||||
MinimizeBox = false;
|
||||
Name = "SpinePreviewFullScreenForm";
|
||||
ShowIcon = false;
|
||||
ShowInTaskbar = false;
|
||||
StartPosition = FormStartPosition.Manual;
|
||||
TopMost = true;
|
||||
ResumeLayout(false);
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
24
SpineViewer/Forms/SpinePreviewFullScreenForm.cs
Normal file
24
SpineViewer/Forms/SpinePreviewFullScreenForm.cs
Normal file
@@ -0,0 +1,24 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using System.ComponentModel.Design;
|
||||
using System.Data;
|
||||
using System.Drawing;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows.Forms;
|
||||
|
||||
namespace SpineViewer.Forms
|
||||
{
|
||||
[ToolboxItem(true)]
|
||||
[Designer(typeof(ComponentDesigner), typeof(IDesigner))]
|
||||
[DesignTimeVisible(true)]
|
||||
public partial class SpinePreviewFullScreenForm: Form
|
||||
{
|
||||
public SpinePreviewFullScreenForm()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
}
|
||||
}
|
||||
120
SpineViewer/Forms/SpinePreviewFullScreenForm.resx
Normal file
120
SpineViewer/Forms/SpinePreviewFullScreenForm.resx
Normal file
@@ -0,0 +1,120 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<root>
|
||||
<!--
|
||||
Microsoft ResX Schema
|
||||
|
||||
Version 2.0
|
||||
|
||||
The primary goals of this format is to allow a simple XML format
|
||||
that is mostly human readable. The generation and parsing of the
|
||||
various data types are done through the TypeConverter classes
|
||||
associated with the data types.
|
||||
|
||||
Example:
|
||||
|
||||
... ado.net/XML headers & schema ...
|
||||
<resheader name="resmimetype">text/microsoft-resx</resheader>
|
||||
<resheader name="version">2.0</resheader>
|
||||
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
|
||||
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
|
||||
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
|
||||
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
|
||||
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
|
||||
<value>[base64 mime encoded serialized .NET Framework object]</value>
|
||||
</data>
|
||||
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
|
||||
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
|
||||
<comment>This is a comment</comment>
|
||||
</data>
|
||||
|
||||
There are any number of "resheader" rows that contain simple
|
||||
name/value pairs.
|
||||
|
||||
Each data row contains a name, and value. The row also contains a
|
||||
type or mimetype. Type corresponds to a .NET class that support
|
||||
text/value conversion through the TypeConverter architecture.
|
||||
Classes that don't support this are serialized and stored with the
|
||||
mimetype set.
|
||||
|
||||
The mimetype is used for serialized objects, and tells the
|
||||
ResXResourceReader how to depersist the object. This is currently not
|
||||
extensible. For a given mimetype the value must be set accordingly:
|
||||
|
||||
Note - application/x-microsoft.net.object.binary.base64 is the format
|
||||
that the ResXResourceWriter will generate, however the reader can
|
||||
read any of the formats listed below.
|
||||
|
||||
mimetype: application/x-microsoft.net.object.binary.base64
|
||||
value : The object must be serialized with
|
||||
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
|
||||
: and then encoded with base64 encoding.
|
||||
|
||||
mimetype: application/x-microsoft.net.object.soap.base64
|
||||
value : The object must be serialized with
|
||||
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
|
||||
: and then encoded with base64 encoding.
|
||||
|
||||
mimetype: application/x-microsoft.net.object.bytearray.base64
|
||||
value : The object must be serialized into a byte array
|
||||
: using a System.ComponentModel.TypeConverter
|
||||
: and then encoded with base64 encoding.
|
||||
-->
|
||||
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
|
||||
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
|
||||
<xsd:element name="root" msdata:IsDataSet="true">
|
||||
<xsd:complexType>
|
||||
<xsd:choice maxOccurs="unbounded">
|
||||
<xsd:element name="metadata">
|
||||
<xsd:complexType>
|
||||
<xsd:sequence>
|
||||
<xsd:element name="value" type="xsd:string" minOccurs="0" />
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="name" use="required" type="xsd:string" />
|
||||
<xsd:attribute name="type" type="xsd:string" />
|
||||
<xsd:attribute name="mimetype" type="xsd:string" />
|
||||
<xsd:attribute ref="xml:space" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:element name="assembly">
|
||||
<xsd:complexType>
|
||||
<xsd:attribute name="alias" type="xsd:string" />
|
||||
<xsd:attribute name="name" type="xsd:string" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:element name="data">
|
||||
<xsd:complexType>
|
||||
<xsd:sequence>
|
||||
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
|
||||
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
|
||||
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
|
||||
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
|
||||
<xsd:attribute ref="xml:space" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:element name="resheader">
|
||||
<xsd:complexType>
|
||||
<xsd:sequence>
|
||||
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="name" type="xsd:string" use="required" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
</xsd:choice>
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
</xsd:schema>
|
||||
<resheader name="resmimetype">
|
||||
<value>text/microsoft-resx</value>
|
||||
</resheader>
|
||||
<resheader name="version">
|
||||
<value>2.0</value>
|
||||
</resheader>
|
||||
<resheader name="reader">
|
||||
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
||||
</resheader>
|
||||
<resheader name="writer">
|
||||
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
||||
</resheader>
|
||||
</root>
|
||||
@@ -1,6 +1,6 @@
|
||||
namespace SpineViewer
|
||||
{
|
||||
partial class MainForm
|
||||
partial class SpineViewerForm
|
||||
{
|
||||
/// <summary>
|
||||
/// Required designer variable.
|
||||
@@ -29,7 +29,7 @@
|
||||
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();
|
||||
@@ -38,11 +38,17 @@
|
||||
toolStripMenuItem_Export = new ToolStripMenuItem();
|
||||
toolStripMenuItem_ExportFrame = new ToolStripMenuItem();
|
||||
toolStripMenuItem_ExportFrameSequence = new ToolStripMenuItem();
|
||||
toolStripSeparator4 = new ToolStripSeparator();
|
||||
toolStripMenuItem_ExportGif = new ToolStripMenuItem();
|
||||
toolStripMenuItem_ExportMkv = new ToolStripMenuItem();
|
||||
toolStripMenuItem_ExportWebp = new ToolStripMenuItem();
|
||||
toolStripMenuItem_ExportAvif = new ToolStripMenuItem();
|
||||
toolStripSeparator5 = new ToolStripSeparator();
|
||||
toolStripMenuItem_ExportMp4 = new ToolStripMenuItem();
|
||||
toolStripMenuItem_ExportMov = 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_Tool = new ToolStripMenuItem();
|
||||
@@ -53,21 +59,24 @@
|
||||
toolStripMenuItem_Diagnostics = new ToolStripMenuItem();
|
||||
toolStripSeparator3 = new ToolStripSeparator();
|
||||
toolStripMenuItem_About = new ToolStripMenuItem();
|
||||
toolStripMenuItem_Experiment = new ToolStripMenuItem();
|
||||
toolStripMenuItem_DesktopProjection = new ToolStripMenuItem();
|
||||
rtbLog = new RichTextBox();
|
||||
splitContainer_MainForm = new SplitContainer();
|
||||
splitContainer_Functional = new SplitContainer();
|
||||
splitContainer_Information = new SplitContainer();
|
||||
groupBox_SkelList = new GroupBox();
|
||||
spineListView = new SpineViewer.Controls.SpineListView();
|
||||
propertyGrid_Spine = new PropertyGrid();
|
||||
spineViewPropertyGrid = new SpineViewer.Controls.SpineViewPropertyGrid();
|
||||
splitContainer_Config = new SplitContainer();
|
||||
groupBox_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);
|
||||
toolStripMenuItem_Debug = new ToolStripMenuItem();
|
||||
menuStrip.SuspendLayout();
|
||||
((System.ComponentModel.ISupportInitialize)splitContainer_MainForm).BeginInit();
|
||||
splitContainer_MainForm.Panel1.SuspendLayout();
|
||||
@@ -96,7 +105,7 @@
|
||||
//
|
||||
menuStrip.BackColor = SystemColors.Control;
|
||||
menuStrip.ImageScalingSize = new Size(24, 24);
|
||||
menuStrip.Items.AddRange(new ToolStripItem[] { toolStripMenuItem_File, toolStripMenuItem_Tool, toolStripMenuItem_Download, toolStripMenuItem_Help });
|
||||
menuStrip.Items.AddRange(new ToolStripItem[] { toolStripMenuItem_File, toolStripMenuItem_Tool, toolStripMenuItem_Download, toolStripMenuItem_Help, toolStripMenuItem_Experiment });
|
||||
menuStrip.Location = new Point(0, 0);
|
||||
menuStrip.Name = "menuStrip";
|
||||
menuStrip.Size = new Size(1778, 32);
|
||||
@@ -114,91 +123,124 @@
|
||||
//
|
||||
toolStripMenuItem_Open.Name = "toolStripMenuItem_Open";
|
||||
toolStripMenuItem_Open.ShortcutKeys = Keys.Control | Keys.O;
|
||||
toolStripMenuItem_Open.Size = new Size(270, 34);
|
||||
toolStripMenuItem_Open.Size = new Size(254, 34);
|
||||
toolStripMenuItem_Open.Text = "打开(&O)...";
|
||||
toolStripMenuItem_Open.Click += toolStripMenuItem_Open_Click;
|
||||
//
|
||||
// toolStripMenuItem_BatchOpen
|
||||
//
|
||||
toolStripMenuItem_BatchOpen.Name = "toolStripMenuItem_BatchOpen";
|
||||
toolStripMenuItem_BatchOpen.Size = new Size(270, 34);
|
||||
toolStripMenuItem_BatchOpen.Size = new Size(254, 34);
|
||||
toolStripMenuItem_BatchOpen.Text = "批量打开(&B)...";
|
||||
toolStripMenuItem_BatchOpen.Click += toolStripMenuItem_BatchOpen_Click;
|
||||
//
|
||||
// toolStripSeparator1
|
||||
//
|
||||
toolStripSeparator1.Name = "toolStripSeparator1";
|
||||
toolStripSeparator1.Size = new Size(267, 6);
|
||||
toolStripSeparator1.Size = new Size(251, 6);
|
||||
//
|
||||
// toolStripMenuItem_Export
|
||||
//
|
||||
toolStripMenuItem_Export.DropDownItems.AddRange(new ToolStripItem[] { toolStripMenuItem_ExportFrame, toolStripMenuItem_ExportFrameSequence, toolStripMenuItem_ExportGif, toolStripMenuItem_ExportMkv, toolStripMenuItem_ExportMp4, toolStripMenuItem_ExportMov, toolStripMenuItem_ExportWebm });
|
||||
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.Size = new Size(270, 34);
|
||||
toolStripMenuItem_Export.Size = new Size(254, 34);
|
||||
toolStripMenuItem_Export.Text = "导出(&E)";
|
||||
//
|
||||
// toolStripMenuItem_ExportFrame
|
||||
//
|
||||
toolStripMenuItem_ExportFrame.Name = "toolStripMenuItem_ExportFrame";
|
||||
toolStripMenuItem_ExportFrame.Size = new Size(270, 34);
|
||||
toolStripMenuItem_ExportFrame.Size = new Size(288, 34);
|
||||
toolStripMenuItem_ExportFrame.Text = "单帧画面...";
|
||||
toolStripMenuItem_ExportFrame.Click += toolStripMenuItem_Export_Click;
|
||||
toolStripMenuItem_ExportFrame.Click += toolStripMenuItem_ExportFrame_Click;
|
||||
//
|
||||
// toolStripMenuItem_ExportFrameSequence
|
||||
//
|
||||
toolStripMenuItem_ExportFrameSequence.Name = "toolStripMenuItem_ExportFrameSequence";
|
||||
toolStripMenuItem_ExportFrameSequence.Size = new Size(270, 34);
|
||||
toolStripMenuItem_ExportFrameSequence.Size = new Size(288, 34);
|
||||
toolStripMenuItem_ExportFrameSequence.Text = "帧序列...";
|
||||
toolStripMenuItem_ExportFrameSequence.Click += toolStripMenuItem_Export_Click;
|
||||
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(270, 34);
|
||||
toolStripMenuItem_ExportGif.Size = new Size(288, 34);
|
||||
toolStripMenuItem_ExportGif.Text = "GIF...";
|
||||
toolStripMenuItem_ExportGif.Click += toolStripMenuItem_Export_Click;
|
||||
toolStripMenuItem_ExportGif.Click += toolStripMenuItem_ExportGif_Click;
|
||||
//
|
||||
// toolStripMenuItem_ExportMkv
|
||||
// toolStripMenuItem_ExportWebp
|
||||
//
|
||||
toolStripMenuItem_ExportMkv.Name = "toolStripMenuItem_ExportMkv";
|
||||
toolStripMenuItem_ExportMkv.Size = new Size(270, 34);
|
||||
toolStripMenuItem_ExportMkv.Text = "MKV";
|
||||
toolStripMenuItem_ExportMkv.Visible = false;
|
||||
toolStripMenuItem_ExportMkv.Click += toolStripMenuItem_Export_Click;
|
||||
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(270, 34);
|
||||
toolStripMenuItem_ExportMp4.Size = new Size(288, 34);
|
||||
toolStripMenuItem_ExportMp4.Text = "MP4...";
|
||||
toolStripMenuItem_ExportMp4.Click += toolStripMenuItem_Export_Click;
|
||||
//
|
||||
// toolStripMenuItem_ExportMov
|
||||
//
|
||||
toolStripMenuItem_ExportMov.Name = "toolStripMenuItem_ExportMov";
|
||||
toolStripMenuItem_ExportMov.Size = new Size(270, 34);
|
||||
toolStripMenuItem_ExportMov.Text = "MOV...";
|
||||
toolStripMenuItem_ExportMov.Visible = false;
|
||||
toolStripMenuItem_ExportMov.Click += toolStripMenuItem_Export_Click;
|
||||
toolStripMenuItem_ExportMp4.Click += toolStripMenuItem_ExportMp4_Click;
|
||||
//
|
||||
// toolStripMenuItem_ExportWebm
|
||||
//
|
||||
toolStripMenuItem_ExportWebm.Name = "toolStripMenuItem_ExportWebm";
|
||||
toolStripMenuItem_ExportWebm.Size = new Size(270, 34);
|
||||
toolStripMenuItem_ExportWebm.Size = new Size(288, 34);
|
||||
toolStripMenuItem_ExportWebm.Text = "WebM...";
|
||||
toolStripMenuItem_ExportWebm.Visible = false;
|
||||
toolStripMenuItem_ExportWebm.Click += toolStripMenuItem_Export_Click;
|
||||
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
|
||||
//
|
||||
toolStripSeparator2.Name = "toolStripSeparator2";
|
||||
toolStripSeparator2.Size = new Size(267, 6);
|
||||
toolStripSeparator2.Size = new Size(251, 6);
|
||||
//
|
||||
// toolStripMenuItem_Exit
|
||||
//
|
||||
toolStripMenuItem_Exit.Name = "toolStripMenuItem_Exit";
|
||||
toolStripMenuItem_Exit.ShortcutKeys = Keys.Alt | Keys.F4;
|
||||
toolStripMenuItem_Exit.Size = new Size(270, 34);
|
||||
toolStripMenuItem_Exit.Size = new Size(254, 34);
|
||||
toolStripMenuItem_Exit.Text = "退出(&X)";
|
||||
toolStripMenuItem_Exit.Click += toolStripMenuItem_Exit_Click;
|
||||
//
|
||||
@@ -232,7 +274,7 @@
|
||||
//
|
||||
// toolStripMenuItem_Help
|
||||
//
|
||||
toolStripMenuItem_Help.DropDownItems.AddRange(new ToolStripItem[] { toolStripMenuItem_Diagnostics, toolStripSeparator3, toolStripMenuItem_About });
|
||||
toolStripMenuItem_Help.DropDownItems.AddRange(new ToolStripItem[] { toolStripMenuItem_Diagnostics, toolStripSeparator3, toolStripMenuItem_About, toolStripMenuItem_Debug });
|
||||
toolStripMenuItem_Help.Name = "toolStripMenuItem_Help";
|
||||
toolStripMenuItem_Help.Size = new Size(88, 28);
|
||||
toolStripMenuItem_Help.Text = "帮助(&H)";
|
||||
@@ -240,22 +282,36 @@
|
||||
// toolStripMenuItem_Diagnostics
|
||||
//
|
||||
toolStripMenuItem_Diagnostics.Name = "toolStripMenuItem_Diagnostics";
|
||||
toolStripMenuItem_Diagnostics.Size = new Size(208, 34);
|
||||
toolStripMenuItem_Diagnostics.Size = new Size(270, 34);
|
||||
toolStripMenuItem_Diagnostics.Text = "诊断信息(&D)";
|
||||
toolStripMenuItem_Diagnostics.Click += toolStripMenuItem_Diagnostics_Click;
|
||||
//
|
||||
// toolStripSeparator3
|
||||
//
|
||||
toolStripSeparator3.Name = "toolStripSeparator3";
|
||||
toolStripSeparator3.Size = new Size(205, 6);
|
||||
toolStripSeparator3.Size = new Size(267, 6);
|
||||
//
|
||||
// toolStripMenuItem_About
|
||||
//
|
||||
toolStripMenuItem_About.Name = "toolStripMenuItem_About";
|
||||
toolStripMenuItem_About.Size = new Size(208, 34);
|
||||
toolStripMenuItem_About.Size = new Size(270, 34);
|
||||
toolStripMenuItem_About.Text = "关于(&A)";
|
||||
toolStripMenuItem_About.Click += toolStripMenuItem_About_Click;
|
||||
//
|
||||
// toolStripMenuItem_Experiment
|
||||
//
|
||||
toolStripMenuItem_Experiment.DropDownItems.AddRange(new ToolStripItem[] { toolStripMenuItem_DesktopProjection });
|
||||
toolStripMenuItem_Experiment.Name = "toolStripMenuItem_Experiment";
|
||||
toolStripMenuItem_Experiment.Size = new Size(138, 28);
|
||||
toolStripMenuItem_Experiment.Text = "实验性功能(&E)";
|
||||
//
|
||||
// toolStripMenuItem_DesktopProjection
|
||||
//
|
||||
toolStripMenuItem_DesktopProjection.Name = "toolStripMenuItem_DesktopProjection";
|
||||
toolStripMenuItem_DesktopProjection.Size = new Size(182, 34);
|
||||
toolStripMenuItem_DesktopProjection.Text = "桌面投影";
|
||||
toolStripMenuItem_DesktopProjection.Click += toolStripMenuItem_DesktopProjection_Click;
|
||||
//
|
||||
// rtbLog
|
||||
//
|
||||
rtbLog.BackColor = SystemColors.Window;
|
||||
@@ -266,7 +322,7 @@
|
||||
rtbLog.Margin = new Padding(3, 2, 3, 2);
|
||||
rtbLog.Name = "rtbLog";
|
||||
rtbLog.ReadOnly = true;
|
||||
rtbLog.Size = new Size(1758, 134);
|
||||
rtbLog.Size = new Size(1758, 154);
|
||||
rtbLog.TabIndex = 0;
|
||||
rtbLog.Text = "";
|
||||
rtbLog.WordWrap = false;
|
||||
@@ -290,7 +346,7 @@
|
||||
splitContainer_MainForm.Panel2.Controls.Add(rtbLog);
|
||||
splitContainer_MainForm.Panel2.Cursor = Cursors.Default;
|
||||
splitContainer_MainForm.Size = new Size(1758, 1097);
|
||||
splitContainer_MainForm.SplitterDistance = 955;
|
||||
splitContainer_MainForm.SplitterDistance = 935;
|
||||
splitContainer_MainForm.SplitterWidth = 8;
|
||||
splitContainer_MainForm.TabIndex = 3;
|
||||
splitContainer_MainForm.TabStop = false;
|
||||
@@ -314,8 +370,8 @@
|
||||
//
|
||||
splitContainer_Functional.Panel2.Controls.Add(groupBox_Preview);
|
||||
splitContainer_Functional.Panel2.Cursor = Cursors.Default;
|
||||
splitContainer_Functional.Size = new Size(1758, 955);
|
||||
splitContainer_Functional.SplitterDistance = 759;
|
||||
splitContainer_Functional.Size = new Size(1758, 935);
|
||||
splitContainer_Functional.SplitterDistance = 788;
|
||||
splitContainer_Functional.SplitterWidth = 8;
|
||||
splitContainer_Functional.TabIndex = 2;
|
||||
splitContainer_Functional.TabStop = false;
|
||||
@@ -338,8 +394,8 @@
|
||||
//
|
||||
splitContainer_Information.Panel2.Controls.Add(splitContainer_Config);
|
||||
splitContainer_Information.Panel2.Cursor = Cursors.Default;
|
||||
splitContainer_Information.Size = new Size(759, 955);
|
||||
splitContainer_Information.SplitterDistance = 354;
|
||||
splitContainer_Information.Size = new Size(788, 935);
|
||||
splitContainer_Information.SplitterDistance = 351;
|
||||
splitContainer_Information.SplitterWidth = 8;
|
||||
splitContainer_Information.TabIndex = 1;
|
||||
splitContainer_Information.TabStop = false;
|
||||
@@ -352,7 +408,7 @@
|
||||
groupBox_SkelList.Dock = DockStyle.Fill;
|
||||
groupBox_SkelList.Location = new Point(0, 0);
|
||||
groupBox_SkelList.Name = "groupBox_SkelList";
|
||||
groupBox_SkelList.Size = new Size(354, 955);
|
||||
groupBox_SkelList.Size = new Size(351, 935);
|
||||
groupBox_SkelList.TabIndex = 0;
|
||||
groupBox_SkelList.TabStop = false;
|
||||
groupBox_SkelList.Text = "模型列表";
|
||||
@@ -362,26 +418,21 @@
|
||||
spineListView.Dock = DockStyle.Fill;
|
||||
spineListView.Location = new Point(3, 26);
|
||||
spineListView.Name = "spineListView";
|
||||
spineListView.PropertyGrid = propertyGrid_Spine;
|
||||
spineListView.Size = new Size(348, 926);
|
||||
spineListView.Size = new Size(345, 906);
|
||||
spineListView.SpinePropertyGrid = spineViewPropertyGrid;
|
||||
spineListView.TabIndex = 0;
|
||||
//
|
||||
// propertyGrid_Spine
|
||||
// spineViewPropertyGrid
|
||||
//
|
||||
propertyGrid_Spine.Dock = DockStyle.Fill;
|
||||
propertyGrid_Spine.HelpVisible = false;
|
||||
propertyGrid_Spine.Location = new Point(3, 26);
|
||||
propertyGrid_Spine.Name = "propertyGrid_Spine";
|
||||
propertyGrid_Spine.Size = new Size(391, 592);
|
||||
propertyGrid_Spine.TabIndex = 0;
|
||||
propertyGrid_Spine.ToolbarVisible = false;
|
||||
propertyGrid_Spine.PropertyValueChanged += propertyGrid_PropertyValueChanged;
|
||||
spineViewPropertyGrid.Dock = DockStyle.Fill;
|
||||
spineViewPropertyGrid.Location = new Point(3, 26);
|
||||
spineViewPropertyGrid.Name = "spineViewPropertyGrid";
|
||||
spineViewPropertyGrid.Size = new Size(423, 580);
|
||||
spineViewPropertyGrid.TabIndex = 0;
|
||||
//
|
||||
// splitContainer_Config
|
||||
//
|
||||
splitContainer_Config.Cursor = Cursors.SizeNS;
|
||||
splitContainer_Config.Dock = DockStyle.Fill;
|
||||
splitContainer_Config.FixedPanel = FixedPanel.Panel1;
|
||||
splitContainer_Config.Location = new Point(0, 0);
|
||||
splitContainer_Config.Name = "splitContainer_Config";
|
||||
splitContainer_Config.Orientation = Orientation.Horizontal;
|
||||
@@ -389,27 +440,23 @@
|
||||
// splitContainer_Config.Panel1
|
||||
//
|
||||
splitContainer_Config.Panel1.Controls.Add(groupBox_PreviewConfig);
|
||||
splitContainer_Config.Panel1.Cursor = Cursors.Default;
|
||||
//
|
||||
// splitContainer_Config.Panel2
|
||||
//
|
||||
splitContainer_Config.Panel2.Controls.Add(groupBox_SkelConfig);
|
||||
splitContainer_Config.Panel2.Cursor = Cursors.Default;
|
||||
splitContainer_Config.Size = new Size(397, 955);
|
||||
splitContainer_Config.SplitterDistance = 326;
|
||||
splitContainer_Config.Size = new Size(429, 935);
|
||||
splitContainer_Config.SplitterDistance = 318;
|
||||
splitContainer_Config.SplitterWidth = 8;
|
||||
splitContainer_Config.TabIndex = 0;
|
||||
splitContainer_Config.TabStop = false;
|
||||
splitContainer_Config.SplitterMoved += splitContainer_SplitterMoved;
|
||||
splitContainer_Config.MouseUp += splitContainer_MouseUp;
|
||||
//
|
||||
// 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(397, 326);
|
||||
groupBox_PreviewConfig.Size = new Size(429, 318);
|
||||
groupBox_PreviewConfig.TabIndex = 1;
|
||||
groupBox_PreviewConfig.TabStop = false;
|
||||
groupBox_PreviewConfig.Text = "画面参数";
|
||||
@@ -420,42 +467,42 @@
|
||||
propertyGrid_Previewer.HelpVisible = false;
|
||||
propertyGrid_Previewer.Location = new Point(3, 26);
|
||||
propertyGrid_Previewer.Name = "propertyGrid_Previewer";
|
||||
propertyGrid_Previewer.Size = new Size(391, 297);
|
||||
propertyGrid_Previewer.Size = new Size(423, 289);
|
||||
propertyGrid_Previewer.TabIndex = 1;
|
||||
propertyGrid_Previewer.ToolbarVisible = false;
|
||||
propertyGrid_Previewer.PropertyValueChanged += propertyGrid_PropertyValueChanged;
|
||||
//
|
||||
// groupBox_SkelConfig
|
||||
//
|
||||
groupBox_SkelConfig.Controls.Add(propertyGrid_Spine);
|
||||
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(397, 621);
|
||||
groupBox_SkelConfig.Size = new Size(429, 609);
|
||||
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(991, 955);
|
||||
groupBox_Preview.Size = new Size(962, 935);
|
||||
groupBox_Preview.TabIndex = 1;
|
||||
groupBox_Preview.TabStop = false;
|
||||
groupBox_Preview.Text = "预览画面";
|
||||
//
|
||||
// spinePreviewer
|
||||
// spinePreviewPanel
|
||||
//
|
||||
spinePreviewer.Dock = DockStyle.Fill;
|
||||
spinePreviewer.Location = new Point(3, 26);
|
||||
spinePreviewer.Name = "spinePreviewer";
|
||||
spinePreviewer.PropertyGrid = propertyGrid_Previewer;
|
||||
spinePreviewer.Size = new Size(985, 926);
|
||||
spinePreviewer.SpineListView = spineListView;
|
||||
spinePreviewer.TabIndex = 0;
|
||||
spinePreviewPanel.Dock = DockStyle.Fill;
|
||||
spinePreviewPanel.Location = new Point(3, 26);
|
||||
spinePreviewPanel.Name = "spinePreviewPanel";
|
||||
spinePreviewPanel.PropertyGrid = propertyGrid_Previewer;
|
||||
spinePreviewPanel.Size = new Size(956, 906);
|
||||
spinePreviewPanel.SpineListView = spineListView;
|
||||
spinePreviewPanel.TabIndex = 0;
|
||||
//
|
||||
// panel_MainForm
|
||||
//
|
||||
@@ -471,17 +518,24 @@
|
||||
//
|
||||
toolTip.ShowAlways = true;
|
||||
//
|
||||
// MainForm
|
||||
// toolStripMenuItem_Debug
|
||||
//
|
||||
AutoScaleDimensions = new SizeF(11F, 24F);
|
||||
AutoScaleMode = AutoScaleMode.Font;
|
||||
toolStripMenuItem_Debug.Name = "toolStripMenuItem_Debug";
|
||||
toolStripMenuItem_Debug.Size = new Size(270, 34);
|
||||
toolStripMenuItem_Debug.Text = "调试";
|
||||
toolStripMenuItem_Debug.Visible = false;
|
||||
//
|
||||
// SpineViewerForm
|
||||
//
|
||||
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;
|
||||
@@ -527,7 +581,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;
|
||||
@@ -535,10 +588,9 @@
|
||||
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 Controls.SpinePreviewPanel spinePreviewPanel;
|
||||
private ToolStripMenuItem toolStripMenuItem_Diagnostics;
|
||||
private ToolStripSeparator toolStripSeparator3;
|
||||
private ToolStripMenuItem toolStripMenuItem_Download;
|
||||
@@ -553,5 +605,16 @@
|
||||
private ToolStripMenuItem toolStripMenuItem_ExportMov;
|
||||
private ToolStripMenuItem toolStripMenuItem_ExportMkv;
|
||||
private ToolStripMenuItem toolStripMenuItem_ExportWebm;
|
||||
private ToolStripMenuItem toolStripMenuItem_ExportCustom;
|
||||
private Controls.SpineViewPropertyGrid spineViewPropertyGrid;
|
||||
private ToolStripSeparator toolStripSeparator4;
|
||||
private ToolStripMenuItem toolStripMenuItem_ExportWebp;
|
||||
private ToolStripMenuItem toolStripMenuItem_ExportAvif;
|
||||
private ToolStripSeparator toolStripSeparator5;
|
||||
private ToolStripSeparator toolStripSeparator6;
|
||||
private SplitContainer splitContainer_Config;
|
||||
private ToolStripMenuItem toolStripMenuItem_Experiment;
|
||||
private ToolStripMenuItem toolStripMenuItem_DesktopProjection;
|
||||
private ToolStripMenuItem toolStripMenuItem_Debug;
|
||||
}
|
||||
}
|
||||
467
SpineViewer/Forms/SpineViewerForm.cs
Normal file
467
SpineViewer/Forms/SpineViewerForm.cs
Normal file
@@ -0,0 +1,467 @@
|
||||
using NLog;
|
||||
using SpineViewer.Spine;
|
||||
using System.ComponentModel;
|
||||
using System.Diagnostics;
|
||||
using SpineViewer.Natives;
|
||||
using SpineViewer.Utils;
|
||||
using SpineViewer.Spine.SpineExporter;
|
||||
|
||||
namespace SpineViewer
|
||||
{
|
||||
internal partial class SpineViewerForm : Form
|
||||
{
|
||||
private readonly Logger logger = LogManager.GetCurrentClassLogger();
|
||||
|
||||
private readonly Dictionary<string, Exporter> exporterCache = [];
|
||||
|
||||
public SpineViewerForm()
|
||||
{
|
||||
InitializeComponent();
|
||||
InitializeLogConfiguration();
|
||||
|
||||
// 执行一些初始化工作
|
||||
try
|
||||
{
|
||||
SFMLShader.Init();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.Error(ex.ToString());
|
||||
logger.Error("Failed to load fragment shader");
|
||||
MessagePopup.Warn("Fragment shader 加载失败,预乘Alpha通道属性失效");
|
||||
}
|
||||
|
||||
#if DEBUG
|
||||
toolStripMenuItem_Debug.Visible = true;
|
||||
#endif
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 初始化窗口日志器
|
||||
/// </summary>
|
||||
private void InitializeLogConfiguration()
|
||||
{
|
||||
// 窗口日志
|
||||
var rtbTarget = new NLog.Windows.Forms.RichTextBoxTarget
|
||||
{
|
||||
Name = "rtbTarget",
|
||||
TargetForm = this,
|
||||
TargetRichTextBox = rtbLog,
|
||||
AutoScroll = true,
|
||||
MaxLines = 3000,
|
||||
SupportLinks = true,
|
||||
Layout = "[${level:format=OneLetter}]${date:format=yyyy-MM-dd HH\\:mm\\:ss} - ${message}"
|
||||
};
|
||||
|
||||
rtbTarget.WordColoringRules.Add(new("[D]", "Gray", "Empty", FontStyle.Bold));
|
||||
rtbTarget.WordColoringRules.Add(new("[I]", "DimGray", "Empty", FontStyle.Bold));
|
||||
rtbTarget.WordColoringRules.Add(new("[W]", "DarkOrange", "Empty", FontStyle.Bold));
|
||||
rtbTarget.WordColoringRules.Add(new("[E]", "Red", "Empty", FontStyle.Bold));
|
||||
rtbTarget.WordColoringRules.Add(new("[F]", "DarkRed", "Empty", FontStyle.Bold));
|
||||
|
||||
LogManager.Configuration.AddTarget(rtbTarget);
|
||||
LogManager.Configuration.AddRule(LogLevel.Debug, LogLevel.Fatal, rtbTarget);
|
||||
LogManager.ReconfigExistingLoggers();
|
||||
}
|
||||
|
||||
private void MainForm_Load(object sender, EventArgs e)
|
||||
{
|
||||
spinePreviewPanel.StartRender();
|
||||
}
|
||||
|
||||
private void MainForm_FormClosing(object sender, FormClosingEventArgs e)
|
||||
{
|
||||
spinePreviewPanel.StopRender();
|
||||
}
|
||||
|
||||
private void toolStripMenuItem_Open_Click(object sender, EventArgs e)
|
||||
{
|
||||
spineListView.Add();
|
||||
}
|
||||
|
||||
private void toolStripMenuItem_BatchOpen_Click(object sender, EventArgs e)
|
||||
{
|
||||
spineListView.BatchAdd();
|
||||
}
|
||||
|
||||
#region private void toolStripMenuItem_ExportXXX_Click(object sender, EventArgs e)
|
||||
|
||||
private void toolStripMenuItem_ExportFrame_Click(object sender, EventArgs e)
|
||||
{
|
||||
if (spinePreviewPanel.IsUpdating && MessagePopup.Quest("画面仍在更新,建议手动暂停画面后导出固定的一帧,是否继续?") != DialogResult.OK)
|
||||
return;
|
||||
|
||||
var k = nameof(toolStripMenuItem_ExportFrame);
|
||||
if (!exporterCache.ContainsKey(k)) exporterCache[k] = new FrameExporter();
|
||||
|
||||
var exporter = exporterCache[k];
|
||||
using var view = spinePreviewPanel.GetView();
|
||||
exporter.Resolution = spinePreviewPanel.Resolution;
|
||||
exporter.PreviewerView = view;
|
||||
exporter.RenderSelectedOnly = spinePreviewPanel.RenderSelectedOnly;
|
||||
|
||||
var exportDialog = new Dialogs.ExportDialog(new FrameExporterProperty((FrameExporter)exporter));
|
||||
if (exportDialog.ShowDialog() != DialogResult.OK)
|
||||
return;
|
||||
|
||||
var progressDialog = new Dialogs.ProgressDialog();
|
||||
progressDialog.DoWork += Export_Work;
|
||||
progressDialog.RunWorkerAsync(exporter);
|
||||
progressDialog.ShowDialog();
|
||||
}
|
||||
|
||||
private void toolStripMenuItem_ExportFrameSequence_Click(object sender, EventArgs e)
|
||||
{
|
||||
var k = nameof(toolStripMenuItem_ExportFrameSequence);
|
||||
if (!exporterCache.ContainsKey(k)) exporterCache[k] = new FrameSequenceExporter();
|
||||
|
||||
var exporter = exporterCache[k];
|
||||
using var view = spinePreviewPanel.GetView();
|
||||
exporter.Resolution = spinePreviewPanel.Resolution;
|
||||
exporter.PreviewerView = view;
|
||||
exporter.RenderSelectedOnly = spinePreviewPanel.RenderSelectedOnly;
|
||||
|
||||
var exportDialog = new Dialogs.ExportDialog(new FrameSequenceExporterProperty((FrameSequenceExporter)exporter));
|
||||
if (exportDialog.ShowDialog() != DialogResult.OK)
|
||||
return;
|
||||
|
||||
var progressDialog = new Dialogs.ProgressDialog();
|
||||
progressDialog.DoWork += Export_Work;
|
||||
progressDialog.RunWorkerAsync(exporter);
|
||||
progressDialog.ShowDialog();
|
||||
}
|
||||
|
||||
private void toolStripMenuItem_ExportGif_Click(object sender, EventArgs e)
|
||||
{
|
||||
var k = nameof(toolStripMenuItem_ExportGif);
|
||||
if (!exporterCache.ContainsKey(k)) exporterCache[k] = new GifExporter();
|
||||
|
||||
var exporter = exporterCache[k];
|
||||
using var view = spinePreviewPanel.GetView();
|
||||
exporter.Resolution = spinePreviewPanel.Resolution;
|
||||
exporter.PreviewerView = view;
|
||||
exporter.RenderSelectedOnly = spinePreviewPanel.RenderSelectedOnly;
|
||||
|
||||
var exportDialog = new Dialogs.ExportDialog(new GifExporterProperty((GifExporter)exporter));
|
||||
if (exportDialog.ShowDialog() != DialogResult.OK)
|
||||
return;
|
||||
|
||||
var progressDialog = new Dialogs.ProgressDialog();
|
||||
progressDialog.DoWork += Export_Work;
|
||||
progressDialog.RunWorkerAsync(exporter);
|
||||
progressDialog.ShowDialog();
|
||||
}
|
||||
|
||||
private void toolStripMenuItem_ExportWebp_Click(object sender, EventArgs e)
|
||||
{
|
||||
var k = nameof(toolStripMenuItem_ExportWebp);
|
||||
if (!exporterCache.ContainsKey(k)) exporterCache[k] = new WebpExporter();
|
||||
|
||||
var exporter = exporterCache[k];
|
||||
using var view = spinePreviewPanel.GetView();
|
||||
exporter.Resolution = spinePreviewPanel.Resolution;
|
||||
exporter.PreviewerView = view;
|
||||
exporter.RenderSelectedOnly = spinePreviewPanel.RenderSelectedOnly;
|
||||
|
||||
var exportDialog = new Dialogs.ExportDialog(new WebpExporterProperty((WebpExporter)exporter));
|
||||
if (exportDialog.ShowDialog() != DialogResult.OK)
|
||||
return;
|
||||
|
||||
var progressDialog = new Dialogs.ProgressDialog();
|
||||
progressDialog.DoWork += Export_Work;
|
||||
progressDialog.RunWorkerAsync(exporter);
|
||||
progressDialog.ShowDialog();
|
||||
}
|
||||
|
||||
private void toolStripMenuItem_ExportAvif_Click(object sender, EventArgs e)
|
||||
{
|
||||
var k = nameof(toolStripMenuItem_ExportAvif);
|
||||
if (!exporterCache.ContainsKey(k)) exporterCache[k] = new AvifExporter();
|
||||
|
||||
var exporter = exporterCache[k];
|
||||
using var view = spinePreviewPanel.GetView();
|
||||
exporter.Resolution = spinePreviewPanel.Resolution;
|
||||
exporter.PreviewerView = view;
|
||||
exporter.RenderSelectedOnly = spinePreviewPanel.RenderSelectedOnly;
|
||||
|
||||
var exportDialog = new Dialogs.ExportDialog(new AvifExporterProperty((AvifExporter)exporter));
|
||||
if (exportDialog.ShowDialog() != DialogResult.OK)
|
||||
return;
|
||||
|
||||
var progressDialog = new Dialogs.ProgressDialog();
|
||||
progressDialog.DoWork += Export_Work;
|
||||
progressDialog.RunWorkerAsync(exporter);
|
||||
progressDialog.ShowDialog();
|
||||
}
|
||||
|
||||
private void toolStripMenuItem_ExportMp4_Click(object sender, EventArgs e)
|
||||
{
|
||||
var k = nameof(toolStripMenuItem_ExportMp4);
|
||||
if (!exporterCache.ContainsKey(k)) exporterCache[k] = new Mp4Exporter();
|
||||
|
||||
var exporter = exporterCache[k];
|
||||
using var view = spinePreviewPanel.GetView();
|
||||
exporter.Resolution = spinePreviewPanel.Resolution;
|
||||
exporter.PreviewerView = view;
|
||||
exporter.RenderSelectedOnly = spinePreviewPanel.RenderSelectedOnly;
|
||||
|
||||
var exportDialog = new Dialogs.ExportDialog(new Mp4ExporterProperty((Mp4Exporter)exporter));
|
||||
if (exportDialog.ShowDialog() != DialogResult.OK)
|
||||
return;
|
||||
|
||||
var progressDialog = new Dialogs.ProgressDialog();
|
||||
progressDialog.DoWork += Export_Work;
|
||||
progressDialog.RunWorkerAsync(exporter);
|
||||
progressDialog.ShowDialog();
|
||||
}
|
||||
|
||||
private void toolStripMenuItem_ExportWebm_Click(object sender, EventArgs e)
|
||||
{
|
||||
var k = nameof(toolStripMenuItem_ExportWebm);
|
||||
if (!exporterCache.ContainsKey(k)) exporterCache[k] = new WebmExporter();
|
||||
|
||||
var exporter = exporterCache[k];
|
||||
using var view = spinePreviewPanel.GetView();
|
||||
exporter.Resolution = spinePreviewPanel.Resolution;
|
||||
exporter.PreviewerView = view;
|
||||
exporter.RenderSelectedOnly = spinePreviewPanel.RenderSelectedOnly;
|
||||
|
||||
var exportDialog = new Dialogs.ExportDialog(new WebmExporterProperty((WebmExporter)exporter));
|
||||
if (exportDialog.ShowDialog() != DialogResult.OK)
|
||||
return;
|
||||
|
||||
var progressDialog = new Dialogs.ProgressDialog();
|
||||
progressDialog.DoWork += Export_Work;
|
||||
progressDialog.RunWorkerAsync(exporter);
|
||||
progressDialog.ShowDialog();
|
||||
}
|
||||
|
||||
private void toolStripMenuItem_ExportMkv_Click(object sender, EventArgs e)
|
||||
{
|
||||
var k = nameof(toolStripMenuItem_ExportMkv);
|
||||
if (!exporterCache.ContainsKey(k)) exporterCache[k] = new MkvExporter();
|
||||
|
||||
var exporter = exporterCache[k];
|
||||
using var view = spinePreviewPanel.GetView();
|
||||
exporter.Resolution = spinePreviewPanel.Resolution;
|
||||
exporter.PreviewerView = view;
|
||||
exporter.RenderSelectedOnly = spinePreviewPanel.RenderSelectedOnly;
|
||||
|
||||
var exportDialog = new Dialogs.ExportDialog(new MkvExporterProperty((MkvExporter)exporter));
|
||||
if (exportDialog.ShowDialog() != DialogResult.OK)
|
||||
return;
|
||||
|
||||
var progressDialog = new Dialogs.ProgressDialog();
|
||||
progressDialog.DoWork += Export_Work;
|
||||
progressDialog.RunWorkerAsync(exporter);
|
||||
progressDialog.ShowDialog();
|
||||
}
|
||||
|
||||
private void toolStripMenuItem_ExportMov_Click(object sender, EventArgs e)
|
||||
{
|
||||
var k = nameof(toolStripMenuItem_ExportMov);
|
||||
if (!exporterCache.ContainsKey(k)) exporterCache[k] = new MovExporter();
|
||||
|
||||
var exporter = exporterCache[k];
|
||||
using var view = spinePreviewPanel.GetView();
|
||||
exporter.Resolution = spinePreviewPanel.Resolution;
|
||||
exporter.PreviewerView = view;
|
||||
exporter.RenderSelectedOnly = spinePreviewPanel.RenderSelectedOnly;
|
||||
|
||||
var exportDialog = new Dialogs.ExportDialog(new MovExporterProperty((MovExporter)exporter));
|
||||
if (exportDialog.ShowDialog() != DialogResult.OK)
|
||||
return;
|
||||
|
||||
var progressDialog = new Dialogs.ProgressDialog();
|
||||
progressDialog.DoWork += Export_Work;
|
||||
progressDialog.RunWorkerAsync(exporter);
|
||||
progressDialog.ShowDialog();
|
||||
}
|
||||
|
||||
private void toolStripMenuItem_ExportCustom_Click(object sender, EventArgs e)
|
||||
{
|
||||
var k = nameof(toolStripMenuItem_ExportCustom);
|
||||
if (!exporterCache.ContainsKey(k)) exporterCache[k] = new CustomExporter();
|
||||
|
||||
var exporter = exporterCache[k];
|
||||
using var view = spinePreviewPanel.GetView();
|
||||
exporter.Resolution = spinePreviewPanel.Resolution;
|
||||
exporter.PreviewerView = view;
|
||||
exporter.RenderSelectedOnly = spinePreviewPanel.RenderSelectedOnly;
|
||||
|
||||
var exportDialog = new Dialogs.ExportDialog(new CustomExporterProperty((CustomExporter)exporter));
|
||||
if (exportDialog.ShowDialog() != DialogResult.OK)
|
||||
return;
|
||||
|
||||
var progressDialog = new Dialogs.ProgressDialog();
|
||||
progressDialog.DoWork += Export_Work;
|
||||
progressDialog.RunWorkerAsync(exporter);
|
||||
progressDialog.ShowDialog();
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
private void toolStripMenuItem_Exit_Click(object sender, EventArgs e)
|
||||
{
|
||||
Close();
|
||||
}
|
||||
|
||||
private void toolStripMenuItem_ConvertFileFormat_Click(object sender, EventArgs e)
|
||||
{
|
||||
var openDialog = new Dialogs.ConvertFileFormatDialog();
|
||||
if (openDialog.ShowDialog() != DialogResult.OK)
|
||||
return;
|
||||
|
||||
var progressDialog = new Dialogs.ProgressDialog();
|
||||
progressDialog.DoWork += ConvertFileFormat_Work;
|
||||
progressDialog.RunWorkerAsync(openDialog.Result);
|
||||
progressDialog.ShowDialog();
|
||||
}
|
||||
|
||||
private void toolStripMenuItem_ManageResource_Click(object sender, EventArgs e)
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
private void toolStripMenuItem_About_Click(object sender, EventArgs e)
|
||||
{
|
||||
using var dialog = new Dialogs.AboutDialog();
|
||||
dialog.ShowDialog();
|
||||
}
|
||||
|
||||
private void toolStripMenuItem_Diagnostics_Click(object sender, EventArgs e)
|
||||
{
|
||||
using var dialog = new Dialogs.DiagnosticsDialog();
|
||||
dialog.ShowDialog();
|
||||
}
|
||||
|
||||
private void splitContainer_SplitterMoved(object sender, SplitterEventArgs e) => ActiveControl = null;
|
||||
|
||||
private void splitContainer_MouseUp(object sender, MouseEventArgs e) => ActiveControl = null;
|
||||
|
||||
private void Export_Work(object? sender, DoWorkEventArgs e)
|
||||
{
|
||||
var worker = (BackgroundWorker)sender;
|
||||
var exporter = (Exporter)e.Argument;
|
||||
Invoke(() => TaskbarManager.SetProgressState(Handle, TBPFLAG.TBPF_INDETERMINATE));
|
||||
spinePreviewPanel.StopRender();
|
||||
lock (spineListView.Spines) { exporter.Export(spineListView.Spines.Where(sp => !sp.IsHidden).ToArray(), (BackgroundWorker)sender); }
|
||||
e.Cancel = worker.CancellationPending;
|
||||
spinePreviewPanel.StartRender();
|
||||
Invoke(() => TaskbarManager.SetProgressState(Handle, TBPFLAG.TBPF_NOPROGRESS));
|
||||
}
|
||||
|
||||
private void ConvertFileFormat_Work(object? sender, DoWorkEventArgs e)
|
||||
{
|
||||
var worker = sender as BackgroundWorker;
|
||||
var args = e.Argument as Dialogs.ConvertFileFormatDialogResult;
|
||||
var newSuffix = args.JsonTarget ? ".json" : ".skel";
|
||||
|
||||
int totalCount = args.SkelPaths.Length;
|
||||
int success = 0;
|
||||
int error = 0;
|
||||
|
||||
SkeletonConverter srcCvter = args.SourceVersion != SpineVersion.Auto ? SkeletonConverter.New(args.SourceVersion) : null;
|
||||
SkeletonConverter tgtCvter = SkeletonConverter.New(args.TargetVersion);
|
||||
|
||||
worker.ReportProgress(0, $"已处理 0/{totalCount}");
|
||||
for (int i = 0; i < totalCount; i++)
|
||||
{
|
||||
if (worker.CancellationPending)
|
||||
{
|
||||
e.Cancel = true;
|
||||
break;
|
||||
}
|
||||
|
||||
var skelPath = args.SkelPaths[i];
|
||||
var newPath = Path.ChangeExtension(skelPath, newSuffix);
|
||||
if (args.OutputDir is string outputDir) newPath = Path.Combine(outputDir, Path.GetFileName(newPath));
|
||||
|
||||
try
|
||||
{
|
||||
if (args.SourceVersion == SpineVersion.Auto)
|
||||
{
|
||||
try
|
||||
{
|
||||
srcCvter = SkeletonConverter.New(SpineUtils.GetVersion(skelPath));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
throw new InvalidDataException($"Auto version detection failed for {skelPath}, try to use a specific version", ex);
|
||||
}
|
||||
}
|
||||
var root = srcCvter.Read(skelPath);
|
||||
root = srcCvter.ToVersion(root, args.TargetVersion);
|
||||
if (args.JsonTarget) tgtCvter.WriteJson(root, newPath);
|
||||
else tgtCvter.WriteBinary(root, newPath);
|
||||
success++;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.Error(ex.ToString());
|
||||
logger.Error("Failed to convert {}", skelPath);
|
||||
error++;
|
||||
}
|
||||
|
||||
worker.ReportProgress((int)((i + 1) * 100.0) / totalCount, $"已处理 {i + 1}/{totalCount}");
|
||||
}
|
||||
|
||||
if (error > 0)
|
||||
{
|
||||
logger.Warn("Batch convert {} successfully, {} failed", success, error);
|
||||
}
|
||||
else
|
||||
{
|
||||
logger.Info("{} skel converted successfully", success);
|
||||
}
|
||||
}
|
||||
|
||||
private void toolStripMenuItem_DesktopProjection_Click(object sender, EventArgs e)
|
||||
{
|
||||
toolStripMenuItem_DesktopProjection.Checked = !toolStripMenuItem_DesktopProjection.Checked;
|
||||
spinePreviewPanel.EnableDesktopProjection = toolStripMenuItem_DesktopProjection.Checked;
|
||||
}
|
||||
|
||||
private void toolStripMenuItem_Debug_Click(object sender, EventArgs e)
|
||||
{
|
||||
#if DEBUG
|
||||
//var cvt = SkeletonConverter.New(SpineVersion.V38);
|
||||
//var root = cvt.ReadBinary(@"D:\ACGN\AzurLane_Export\AzurLane_Dynamic\docs\aerhangeersike\aerhangeersike_3\aerhangeersike_3 - 副本.skel");
|
||||
//cvt.WriteJson(root, @"D:\ACGN\AzurLane_Export\AzurLane_Dynamic\docs\aerhangeersike\aerhangeersike_3\aerhangeersike_3.json");
|
||||
|
||||
//root = cvt.ReadJson(@"D:\ACGN\AzurLane_Export\AzurLane_Dynamic\docs\aerhangeersike\aerhangeersike_3\aerhangeersike_3.json");
|
||||
//cvt.WriteBinary(root, @"D:\ACGN\AzurLane_Export\AzurLane_Dynamic\docs\aerhangeersike\aerhangeersike_3\aerhangeersike_3.skel");
|
||||
//var sp = SpineObject.New(SpineVersion.V38, @"D:\ACGN\AzurLane_Export\AzurLane_Dynamic\docs\aerhangeersike\aerhangeersike_3\aerhangeersike_3.skel");
|
||||
|
||||
//var cvt = SkeletonConverter.New(SpineVersion.V38);
|
||||
//var root = cvt.ReadJson(@"D:\ACGN\G\GirlsCreation\standing_spine\st4020069\st4020069.json");
|
||||
//cvt.WriteBinary(root, @"D:\ACGN\G\GirlsCreation\standing_spine\st4020069\st4020069.skel");
|
||||
//var sp = SpineObject.New(SpineVersion.V38, @"D:\ACGN\G\GirlsCreation\standing_spine\st4020069\st4020069.skel");
|
||||
//_Test();
|
||||
#endif
|
||||
}
|
||||
|
||||
//private void spinePreviewer_KeyDown(object sender, KeyEventArgs e)
|
||||
//{
|
||||
// switch (e.KeyCode)
|
||||
// {
|
||||
// case Keys.Space:
|
||||
// if ((ModifierKeys & Keys.Alt) != 0)
|
||||
// spinePreviewer.ClickStopButton();
|
||||
// else
|
||||
// spinePreviewer.ClickStartButton();
|
||||
// break;
|
||||
// case Keys.Right:
|
||||
// if ((ModifierKeys & Keys.Alt) != 0)
|
||||
// spinePreviewer.ClickForwardFastButton();
|
||||
// else
|
||||
// spinePreviewer.ClickForwardStepButton();
|
||||
// break;
|
||||
// case Keys.Left:
|
||||
// if ((ModifierKeys & Keys.Alt) != 0)
|
||||
// spinePreviewer.ClickRestartButton();
|
||||
// break;
|
||||
// }
|
||||
//}
|
||||
}
|
||||
}
|
||||
51
SpineViewer/Forms/WallpaperForm.Designer.cs
generated
Normal file
51
SpineViewer/Forms/WallpaperForm.Designer.cs
generated
Normal file
@@ -0,0 +1,51 @@
|
||||
namespace SpineViewer
|
||||
{
|
||||
partial class WallpaperForm
|
||||
{
|
||||
/// <summary>
|
||||
/// Required designer variable.
|
||||
/// </summary>
|
||||
private System.ComponentModel.IContainer components = null;
|
||||
|
||||
/// <summary>
|
||||
/// Clean up any resources being used.
|
||||
/// </summary>
|
||||
/// <param name="disposing">true if managed resources should be disposed; otherwise, false.</param>
|
||||
protected override void Dispose(bool disposing)
|
||||
{
|
||||
if (disposing && (components != null))
|
||||
{
|
||||
components.Dispose();
|
||||
}
|
||||
base.Dispose(disposing);
|
||||
}
|
||||
|
||||
#region Windows Form Designer generated code
|
||||
|
||||
/// <summary>
|
||||
/// Required method for Designer support - do not modify
|
||||
/// the contents of this method with the code editor.
|
||||
/// </summary>
|
||||
private void InitializeComponent()
|
||||
{
|
||||
SuspendLayout();
|
||||
//
|
||||
// WallpaperForm
|
||||
//
|
||||
AutoScaleMode = AutoScaleMode.None;
|
||||
ClientSize = new Size(512, 512);
|
||||
ControlBox = false;
|
||||
FormBorderStyle = FormBorderStyle.None;
|
||||
MaximizeBox = false;
|
||||
MinimizeBox = false;
|
||||
Name = "WallpaperForm";
|
||||
ShowIcon = false;
|
||||
ShowInTaskbar = false;
|
||||
StartPosition = FormStartPosition.Manual;
|
||||
WindowState = FormWindowState.Minimized;
|
||||
ResumeLayout(false);
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
73
SpineViewer/Forms/WallpaperForm.cs
Normal file
73
SpineViewer/Forms/WallpaperForm.cs
Normal file
@@ -0,0 +1,73 @@
|
||||
using SpineViewer.Natives;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using System.ComponentModel.Design;
|
||||
using System.Data;
|
||||
using System.Drawing;
|
||||
using System.Linq;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows.Forms;
|
||||
|
||||
namespace SpineViewer
|
||||
{
|
||||
[ToolboxItem(true)]
|
||||
[Designer(typeof(ComponentDesigner), typeof(IDesigner))]
|
||||
[DesignTimeVisible(true)]
|
||||
public partial class WallpaperForm: Form
|
||||
{
|
||||
public WallpaperForm()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
|
||||
protected override CreateParams CreateParams
|
||||
{
|
||||
get
|
||||
{
|
||||
var cp = base.CreateParams;
|
||||
cp.ExStyle = Win32.WS_EX_TOOLWINDOW | Win32.WS_EX_LAYERED;
|
||||
return cp;
|
||||
}
|
||||
}
|
||||
|
||||
protected override void OnLoad(EventArgs e)
|
||||
{
|
||||
base.OnLoad(e);
|
||||
|
||||
// 设置成嵌入桌面
|
||||
var progman = Win32.FindWindow("Progman", null);
|
||||
if (progman != IntPtr.Zero)
|
||||
{
|
||||
// 确保 WorkerW 被创建
|
||||
Win32.SendMessageTimeout(progman, Win32.WM_SPAWN_WORKER, IntPtr.Zero, IntPtr.Zero, Win32.SMTO_NORMAL, 1000, out _);
|
||||
var workerW = Win32.GetWorkerW();
|
||||
if (workerW != IntPtr.Zero)
|
||||
{
|
||||
Win32.SetLayeredWindowAttributes(Handle, 0, 255, Win32.LWA_ALPHA);
|
||||
Win32.SetParent(Handle, workerW); // 嵌入之前必须保证有 WS_EX_LAYERED 标志
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
|
||||
[Browsable(false)]
|
||||
public byte LayeredWindowAlpha
|
||||
{
|
||||
get
|
||||
{
|
||||
uint crKey = 0;
|
||||
byte bAlpha = 255;
|
||||
uint dwFlags = Win32.LWA_ALPHA;
|
||||
Win32.GetLayeredWindowAttributes(Handle, ref crKey, ref bAlpha, ref dwFlags);
|
||||
return bAlpha;
|
||||
}
|
||||
set
|
||||
{
|
||||
Win32.SetLayeredWindowAttributes(Handle, 0, value, Win32.LWA_ALPHA);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
120
SpineViewer/Forms/WallpaperForm.resx
Normal file
120
SpineViewer/Forms/WallpaperForm.resx
Normal file
@@ -0,0 +1,120 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<root>
|
||||
<!--
|
||||
Microsoft ResX Schema
|
||||
|
||||
Version 2.0
|
||||
|
||||
The primary goals of this format is to allow a simple XML format
|
||||
that is mostly human readable. The generation and parsing of the
|
||||
various data types are done through the TypeConverter classes
|
||||
associated with the data types.
|
||||
|
||||
Example:
|
||||
|
||||
... ado.net/XML headers & schema ...
|
||||
<resheader name="resmimetype">text/microsoft-resx</resheader>
|
||||
<resheader name="version">2.0</resheader>
|
||||
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
|
||||
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
|
||||
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
|
||||
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
|
||||
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
|
||||
<value>[base64 mime encoded serialized .NET Framework object]</value>
|
||||
</data>
|
||||
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
|
||||
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
|
||||
<comment>This is a comment</comment>
|
||||
</data>
|
||||
|
||||
There are any number of "resheader" rows that contain simple
|
||||
name/value pairs.
|
||||
|
||||
Each data row contains a name, and value. The row also contains a
|
||||
type or mimetype. Type corresponds to a .NET class that support
|
||||
text/value conversion through the TypeConverter architecture.
|
||||
Classes that don't support this are serialized and stored with the
|
||||
mimetype set.
|
||||
|
||||
The mimetype is used for serialized objects, and tells the
|
||||
ResXResourceReader how to depersist the object. This is currently not
|
||||
extensible. For a given mimetype the value must be set accordingly:
|
||||
|
||||
Note - application/x-microsoft.net.object.binary.base64 is the format
|
||||
that the ResXResourceWriter will generate, however the reader can
|
||||
read any of the formats listed below.
|
||||
|
||||
mimetype: application/x-microsoft.net.object.binary.base64
|
||||
value : The object must be serialized with
|
||||
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
|
||||
: and then encoded with base64 encoding.
|
||||
|
||||
mimetype: application/x-microsoft.net.object.soap.base64
|
||||
value : The object must be serialized with
|
||||
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
|
||||
: and then encoded with base64 encoding.
|
||||
|
||||
mimetype: application/x-microsoft.net.object.bytearray.base64
|
||||
value : The object must be serialized into a byte array
|
||||
: using a System.ComponentModel.TypeConverter
|
||||
: and then encoded with base64 encoding.
|
||||
-->
|
||||
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
|
||||
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
|
||||
<xsd:element name="root" msdata:IsDataSet="true">
|
||||
<xsd:complexType>
|
||||
<xsd:choice maxOccurs="unbounded">
|
||||
<xsd:element name="metadata">
|
||||
<xsd:complexType>
|
||||
<xsd:sequence>
|
||||
<xsd:element name="value" type="xsd:string" minOccurs="0" />
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="name" use="required" type="xsd:string" />
|
||||
<xsd:attribute name="type" type="xsd:string" />
|
||||
<xsd:attribute name="mimetype" type="xsd:string" />
|
||||
<xsd:attribute ref="xml:space" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:element name="assembly">
|
||||
<xsd:complexType>
|
||||
<xsd:attribute name="alias" type="xsd:string" />
|
||||
<xsd:attribute name="name" type="xsd:string" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:element name="data">
|
||||
<xsd:complexType>
|
||||
<xsd:sequence>
|
||||
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
|
||||
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
|
||||
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
|
||||
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
|
||||
<xsd:attribute ref="xml:space" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:element name="resheader">
|
||||
<xsd:complexType>
|
||||
<xsd:sequence>
|
||||
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="name" type="xsd:string" use="required" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
</xsd:choice>
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
</xsd:schema>
|
||||
<resheader name="resmimetype">
|
||||
<value>text/microsoft-resx</value>
|
||||
</resheader>
|
||||
<resheader name="version">
|
||||
<value>2.0</value>
|
||||
</resheader>
|
||||
<resheader name="reader">
|
||||
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
||||
</resheader>
|
||||
<resheader name="writer">
|
||||
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
||||
</resheader>
|
||||
</root>
|
||||
@@ -1,262 +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;
|
||||
using SpineViewer.Exporter;
|
||||
|
||||
namespace SpineViewer
|
||||
{
|
||||
internal partial class MainForm : Form
|
||||
{
|
||||
private Logger logger = LogManager.GetCurrentClassLogger();
|
||||
|
||||
public MainForm()
|
||||
{
|
||||
InitializeComponent();
|
||||
InitializeLogConfiguration();
|
||||
|
||||
// 在此处将导出菜单需要的类绑定起来
|
||||
toolStripMenuItem_ExportFrame.Tag = ExportType.Frame;
|
||||
toolStripMenuItem_ExportFrameSequence.Tag = ExportType.FrameSequence;
|
||||
toolStripMenuItem_ExportGif.Tag = ExportType.GIF;
|
||||
toolStripMenuItem_ExportMkv.Tag = ExportType.MKV;
|
||||
toolStripMenuItem_ExportMp4.Tag = ExportType.MP4;
|
||||
toolStripMenuItem_ExportMov.Tag = ExportType.MOV;
|
||||
toolStripMenuItem_ExportWebm.Tag = ExportType.WebM;
|
||||
|
||||
// 执行一些初始化工作
|
||||
try
|
||||
{
|
||||
Spine.Shader.Init();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.Error(ex.ToString());
|
||||
logger.Error("Failed to load fragment shader");
|
||||
MessageBox.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)
|
||||
{
|
||||
spinePreviewer.StartRender();
|
||||
}
|
||||
|
||||
private void MainForm_FormClosing(object sender, FormClosingEventArgs e)
|
||||
{
|
||||
spinePreviewer.StopRender();
|
||||
}
|
||||
|
||||
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)
|
||||
{
|
||||
ExportType type = (ExportType)((ToolStripMenuItem)sender).Tag;
|
||||
|
||||
if (type == ExportType.Frame && spinePreviewer.IsUpdating)
|
||||
{
|
||||
if (MessageBox.Quest("画面仍在更新,建议手动暂停画面后导出固定的一帧,是否继续?") != DialogResult.OK)
|
||||
return;
|
||||
}
|
||||
|
||||
var exportArgs = ExportArgs.New(type, spinePreviewer.Resolution, spinePreviewer.GetView(), spinePreviewer.RenderSelectedOnly);
|
||||
var exportDialog = new Dialogs.ExportDialog() { ExportArgs = exportArgs };
|
||||
if (exportDialog.ShowDialog() != DialogResult.OK)
|
||||
return;
|
||||
|
||||
var exporter = Exporter.Exporter.New(type, exportArgs);
|
||||
|
||||
var progressDialog = new Dialogs.ProgressDialog();
|
||||
progressDialog.DoWork += Export_Work;
|
||||
progressDialog.RunWorkerAsync(exporter);
|
||||
progressDialog.ShowDialog();
|
||||
}
|
||||
|
||||
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)
|
||||
{
|
||||
(new Dialogs.AboutDialog()).ShowDialog();
|
||||
}
|
||||
|
||||
private void toolStripMenuItem_Diagnostics_Click(object sender, EventArgs e)
|
||||
{
|
||||
(new Dialogs.DiagnosticsDialog()).ShowDialog();
|
||||
}
|
||||
|
||||
private void splitContainer_SplitterMoved(object sender, SplitterEventArgs e) => ActiveControl = null;
|
||||
|
||||
private void splitContainer_MouseUp(object sender, MouseEventArgs e) => ActiveControl = null;
|
||||
|
||||
private void propertyGrid_PropertyValueChanged(object sender, PropertyValueChangedEventArgs e)
|
||||
{
|
||||
// 用来解决对面板某些值修改之后, 其他被联动修改的值不会实时刷新的问题
|
||||
(sender as PropertyGrid)?.Refresh();
|
||||
}
|
||||
|
||||
private void Export_Work(object? sender, DoWorkEventArgs e)
|
||||
{
|
||||
var worker = (BackgroundWorker)sender;
|
||||
var exporter = (Exporter.Exporter)e.Argument;
|
||||
Invoke(() => TaskbarManager.SetProgressState(Handle, TBPFLAG.TBPF_INDETERMINATE));
|
||||
spinePreviewer.StopRender();
|
||||
lock (spineListView.Spines) { exporter.Export(spineListView.Spines.Where(sp => !sp.IsHidden).ToArray(), (BackgroundWorker)sender); }
|
||||
e.Cancel = worker.CancellationPending;
|
||||
spinePreviewer.StartRender();
|
||||
Invoke(() => TaskbarManager.SetProgressState(Handle, TBPFLAG.TBPF_NOPROGRESS));
|
||||
}
|
||||
|
||||
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 != SpineVersion.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 == SpineVersion.Auto)
|
||||
{
|
||||
try
|
||||
{
|
||||
srcCvter = SkeletonConverter.New(SpineHelper.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, tgtVersion);
|
||||
if (jsonTarget) tgtCvter.WriteJson(root, newPath); else tgtCvter.WriteBinary(root, newPath);
|
||||
success++;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.Error(ex.ToString());
|
||||
logger.Error("Failed to convert {}", skelPath);
|
||||
error++;
|
||||
}
|
||||
|
||||
worker.ReportProgress((int)((i + 1) * 100.0) / totalCount, $"已处理 {i + 1}/{totalCount}");
|
||||
}
|
||||
|
||||
if (error > 0)
|
||||
{
|
||||
logger.Warn("Batch convert {} successfully, {} failed", success, error);
|
||||
}
|
||||
else
|
||||
{
|
||||
logger.Info("{} skel converted successfully", success);
|
||||
}
|
||||
}
|
||||
|
||||
//private void 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;
|
||||
// }
|
||||
//}
|
||||
}
|
||||
}
|
||||
@@ -2,7 +2,7 @@
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Windows.Forms;
|
||||
|
||||
namespace SpineViewer
|
||||
namespace SpineViewer.Natives
|
||||
{
|
||||
internal enum TBPFLAG
|
||||
{
|
||||
@@ -19,15 +19,15 @@ namespace SpineViewer
|
||||
{
|
||||
// ITaskbarList
|
||||
void HrInit();
|
||||
void AddTab(IntPtr hwnd);
|
||||
void DeleteTab(IntPtr hwnd);
|
||||
void ActivateTab(IntPtr hwnd);
|
||||
void SetActiveAlt(IntPtr hwnd);
|
||||
void AddTab(nint hwnd);
|
||||
void DeleteTab(nint hwnd);
|
||||
void ActivateTab(nint hwnd);
|
||||
void SetActiveAlt(nint hwnd);
|
||||
// ITaskbarList2
|
||||
void MarkFullscreenWindow(IntPtr hwnd, [MarshalAs(UnmanagedType.Bool)] bool fFullscreen);
|
||||
void MarkFullscreenWindow(nint hwnd, [MarshalAs(UnmanagedType.Bool)] bool fFullscreen);
|
||||
// ITaskbarList3
|
||||
void SetProgressValue(IntPtr hwnd, ulong ullCompleted, ulong ullTotal);
|
||||
void SetProgressState(IntPtr hwnd, TBPFLAG tbpFlags);
|
||||
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);
|
||||
@@ -52,12 +52,12 @@ namespace SpineViewer
|
||||
taskbarList.HrInit();
|
||||
}
|
||||
|
||||
public static void SetProgressState(IntPtr windowHandle, TBPFLAG state)
|
||||
public static void SetProgressState(nint windowHandle, TBPFLAG state)
|
||||
{
|
||||
taskbarList.SetProgressState(windowHandle, state);
|
||||
}
|
||||
|
||||
public static void SetProgressValue(IntPtr windowHandle, ulong completed, ulong total)
|
||||
public static void SetProgressValue(nint windowHandle, ulong completed, ulong total)
|
||||
{
|
||||
taskbarList.SetProgressValue(windowHandle, completed, total);
|
||||
}
|
||||
180
SpineViewer/Natives/Win32.cs
Normal file
180
SpineViewer/Natives/Win32.cs
Normal file
@@ -0,0 +1,180 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Drawing;
|
||||
using System.Linq;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace SpineViewer.Natives
|
||||
{
|
||||
/// <summary>
|
||||
/// Win32 Sdk 包装类
|
||||
/// </summary>
|
||||
public static class Win32
|
||||
{
|
||||
public const int GWL_STYLE = -16;
|
||||
public const int WS_SIZEBOX = 0x40000;
|
||||
public const int WS_BORDER = 0x800000;
|
||||
public const int WS_VISIBLE = 0x10000000;
|
||||
public const int WS_CHILD = 0x40000000;
|
||||
public const int WS_POPUP = unchecked((int)0x80000000);
|
||||
|
||||
public const int GWL_EXSTYLE = -20;
|
||||
public const int WS_EX_TOPMOST = 0x8;
|
||||
public const int WS_EX_TRANSPARENT = 0x20;
|
||||
public const int WS_EX_TOOLWINDOW = 0x80;
|
||||
public const int WS_EX_WINDOWEDGE = 0x100;
|
||||
public const int WS_EX_CLIENTEDGE = 0x200;
|
||||
public const int WS_EX_APPWINDOW = 0x40000;
|
||||
public const int WS_EX_LAYERED = 0x80000;
|
||||
public const int WS_EX_OVERLAPPEDWINDOW = WS_EX_WINDOWEDGE | WS_EX_CLIENTEDGE;
|
||||
public const int WS_EX_NOACTIVATE = 0x8000000;
|
||||
|
||||
public const uint LWA_COLORKEY = 0x1;
|
||||
public const uint LWA_ALPHA = 0x2;
|
||||
|
||||
public const byte AC_SRC_OVER = 0x00;
|
||||
public const byte AC_SRC_ALPHA = 0x01;
|
||||
|
||||
public const int ULW_COLORKEY = 0x00000001;
|
||||
public const int ULW_ALPHA = 0x00000002;
|
||||
public const int ULW_OPAQUE = 0x00000004;
|
||||
|
||||
public const nint HWND_TOPMOST = -1;
|
||||
|
||||
public const uint SWP_NOSIZE = 0x0001;
|
||||
public const uint SWP_NOMOVE = 0x0002;
|
||||
public const uint SWP_NOZORDER = 0x0004;
|
||||
public const uint SWP_FRAMECHANGED = 0x0020;
|
||||
|
||||
public const int WM_SPAWN_WORKER = 0x052C; // 一个未公开的神秘消息
|
||||
|
||||
public const uint SMTO_NORMAL = 0x0000;
|
||||
public const uint SMTO_BLOCK = 0x0001;
|
||||
public const uint SMTO_ABORTIFHUNG = 0x0002;
|
||||
public const uint SMTO_NOTIMEOUTIFNOTHUNG = 0x0008;
|
||||
|
||||
public const uint GA_PARENT = 1;
|
||||
public const uint GW_OWNER = 4;
|
||||
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
public struct POINT
|
||||
{
|
||||
public int x;
|
||||
public int y;
|
||||
}
|
||||
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
public struct SIZE
|
||||
{
|
||||
public int cx;
|
||||
public int cy;
|
||||
}
|
||||
|
||||
[StructLayout(LayoutKind.Sequential, Pack = 1)]
|
||||
public struct BLENDFUNCTION
|
||||
{
|
||||
public byte BlendOp;
|
||||
public byte BlendFlags;
|
||||
public byte SourceConstantAlpha;
|
||||
public byte AlphaFormat;
|
||||
}
|
||||
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
private struct LASTINPUTINFO
|
||||
{
|
||||
public uint cbSize;
|
||||
public uint dwTime;
|
||||
}
|
||||
|
||||
[DllImport("user32.dll", SetLastError = true)]
|
||||
public static extern nint GetDC(nint hWnd);
|
||||
|
||||
[DllImport("user32.dll", SetLastError = true)]
|
||||
public static extern int ReleaseDC(nint hWnd, nint hDC);
|
||||
|
||||
[DllImport("user32.dll", SetLastError = true)]
|
||||
public static extern int SetWindowLong(nint hWnd, int nIndex, int dwNewLong);
|
||||
|
||||
[DllImport("user32.dll", SetLastError = true)]
|
||||
public static extern int GetWindowLong(nint hWnd, int nIndex);
|
||||
|
||||
[DllImport("user32.dll", SetLastError = true)]
|
||||
public static extern bool GetLayeredWindowAttributes(nint hWnd, ref uint crKey, ref byte bAlpha, ref uint dwFlags);
|
||||
|
||||
[DllImport("user32.dll", SetLastError = true)]
|
||||
public static extern bool SetLayeredWindowAttributes(nint hWnd, uint pcrKey, byte pbAlpha, uint pdwFlags);
|
||||
|
||||
[DllImport("user32.dll", SetLastError = true)]
|
||||
public static extern bool UpdateLayeredWindow(nint hWnd, nint hdcDst, nint pptDst, ref SIZE psize, nint hdcSrc, ref POINT pptSrc, int crKey, ref BLENDFUNCTION pblend, int dwFlags);
|
||||
|
||||
[DllImport("user32.dll", SetLastError = true)]
|
||||
public static extern bool SetWindowPos(nint hWnd, nint hWndInsertAfter, int X, int Y, int cx, int cy, uint uFlags);
|
||||
|
||||
[DllImport("user32.dll", SetLastError = true)]
|
||||
public static extern uint GetDoubleClickTime();
|
||||
|
||||
[DllImport("user32.dll", SetLastError = true)]
|
||||
private static extern bool GetLastInputInfo(ref LASTINPUTINFO plii);
|
||||
|
||||
[DllImport("user32.dll", SetLastError = true)]
|
||||
public static extern nint FindWindow(string lpClassName, string lpWindowName);
|
||||
|
||||
[DllImport("user32.dll", SetLastError = true)]
|
||||
public static extern nint SendMessageTimeout(nint hWnd, uint Msg, nint wParam, nint lParam, uint fuFlags, uint uTimeout, out nint lpdwResult);
|
||||
|
||||
[DllImport("user32.dll", SetLastError = true)]
|
||||
public static extern nint FindWindowEx(nint parentHandle, nint childAfter, string className, string windowTitle);
|
||||
|
||||
[DllImport("user32.dll", SetLastError = true)]
|
||||
public static extern nint SetParent(nint hWndChild, nint hWndNewParent);
|
||||
|
||||
[DllImport("user32.dll", SetLastError = true)]
|
||||
public static extern nint GetParent(nint hWnd);
|
||||
|
||||
[DllImport("user32.dll", SetLastError = true)]
|
||||
public static extern nint GetAncestor(nint hWnd, uint gaFlags);
|
||||
|
||||
[DllImport("user32.dll", SetLastError = true)]
|
||||
public static extern nint GetWindow(nint hWnd, uint uCmd);
|
||||
|
||||
[DllImport("gdi32.dll", SetLastError = true)]
|
||||
public static extern nint CreateCompatibleDC(nint hdc);
|
||||
|
||||
[DllImport("gdi32.dll", SetLastError = true)]
|
||||
public static extern bool DeleteDC(nint hdc);
|
||||
|
||||
[DllImport("gdi32.dll", SetLastError = true)]
|
||||
public static extern nint SelectObject(nint hdc, nint hgdiobj);
|
||||
|
||||
[DllImport("gdi32.dll", SetLastError = true)]
|
||||
public static extern bool DeleteObject(nint hObject);
|
||||
|
||||
public static TimeSpan GetLastInputElapsedTime()
|
||||
{
|
||||
LASTINPUTINFO lastInputInfo = new();
|
||||
lastInputInfo.cbSize = (uint)Marshal.SizeOf(lastInputInfo);
|
||||
|
||||
uint idleTimeMillis = 1000;
|
||||
if (GetLastInputInfo(ref lastInputInfo))
|
||||
{
|
||||
uint tickCount = (uint)Environment.TickCount;
|
||||
uint lastInputTick = lastInputInfo.dwTime;
|
||||
idleTimeMillis = tickCount - lastInputTick;
|
||||
}
|
||||
return TimeSpan.FromMilliseconds(idleTimeMillis);
|
||||
}
|
||||
|
||||
public static nint GetWorkerW()
|
||||
{
|
||||
var progman = FindWindow("Progman", null);
|
||||
if (progman == nint.Zero)
|
||||
return nint.Zero;
|
||||
nint hWnd = FindWindowEx(progman, 0, "WorkerW", null);
|
||||
Debug.WriteLine($"HWND(Progman.WorkerW): {hWnd:x8}");
|
||||
return hWnd;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,7 @@
|
||||
using NLog;
|
||||
using SpineViewer.Utils;
|
||||
using System.Diagnostics;
|
||||
using System.Reflection;
|
||||
|
||||
namespace SpineViewer
|
||||
{
|
||||
@@ -25,6 +27,8 @@ namespace SpineViewer
|
||||
///// </summary>
|
||||
//public static readonly string TempDir = Directory.CreateDirectory(Path.Combine(Path.GetTempPath(), Name)).FullName;
|
||||
|
||||
public static string Version => Assembly.GetExecutingAssembly().GetCustomAttribute<AssemblyInformationalVersionAttribute>()?.InformationalVersion;
|
||||
|
||||
/// <summary>
|
||||
/// 程序日志器
|
||||
/// </summary>
|
||||
@@ -46,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(), "程序已崩溃");
|
||||
MessagePopup.Error(ex.ToString(), "程序已崩溃");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -12,8 +12,45 @@ using System.Globalization;
|
||||
namespace SpineViewer.Spine.Implementations.SkeletonConverter
|
||||
{
|
||||
[SpineImplementation(SpineVersion.V38)]
|
||||
class SkeletonConverter38 : SpineViewer.Spine.SkeletonConverter
|
||||
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;
|
||||
@@ -44,6 +81,9 @@ namespace SpineViewer.Spine.Implementations.SkeletonConverter
|
||||
|
||||
idx2event.Clear();
|
||||
|
||||
// 清理临时属性
|
||||
foreach (var (_, data) in root["events"].AsObject()) data.AsObject().Remove("__name__");
|
||||
|
||||
return root;
|
||||
}
|
||||
|
||||
@@ -90,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);
|
||||
@@ -111,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;
|
||||
@@ -181,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();
|
||||
@@ -283,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();
|
||||
@@ -348,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();
|
||||
@@ -376,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;
|
||||
@@ -592,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)
|
||||
{
|
||||
@@ -600,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:
|
||||
@@ -671,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);
|
||||
@@ -719,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);
|
||||
}
|
||||
@@ -785,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";
|
||||
@@ -810,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)
|
||||
@@ -818,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();
|
||||
@@ -828,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()
|
||||
@@ -890,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;
|
||||
@@ -912,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);
|
||||
@@ -1018,6 +1068,7 @@ namespace SpineViewer.Spine.Implementations.SkeletonConverter
|
||||
{
|
||||
writer.WriteVarInt(0); // default 的 slotCount
|
||||
writer.WriteVarInt(0); // 其他皮肤数量
|
||||
skin2idx["default"] = skin2idx.Count;
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1029,6 +1080,7 @@ namespace SpineViewer.Spine.Implementations.SkeletonConverter
|
||||
{
|
||||
hasDefault = true;
|
||||
WriteSkin(skin, true);
|
||||
skin2idx["default"] = skin2idx.Count;
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -1045,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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1109,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;
|
||||
@@ -1119,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);
|
||||
@@ -1135,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);
|
||||
@@ -1199,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++;
|
||||
}
|
||||
}
|
||||
@@ -1210,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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1234,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);
|
||||
@@ -1267,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
|
||||
|
||||
@@ -1,404 +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(SpineVersion.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;
|
||||
}
|
||||
|
||||
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) : 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 skin in skeletonData.Skins)
|
||||
skinNames.Add(skin.Name);
|
||||
|
||||
foreach (var anime in skeletonData.Animations)
|
||||
animationNames.Add(anime.Name);
|
||||
|
||||
skeleton = new Skeleton(skeletonData);
|
||||
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; }
|
||||
|
||||
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 animation = Track0Animation; // TODO: 适配多轨道
|
||||
var skin = Skin;
|
||||
|
||||
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;
|
||||
Track0Animation = animation; // TODO: 适配多轨道
|
||||
Skin = skin;
|
||||
}
|
||||
}
|
||||
|
||||
public override PointF Position
|
||||
{
|
||||
get => new(skeleton.X, skeleton.Y);
|
||||
set
|
||||
{
|
||||
skeleton.X = value.X;
|
||||
skeleton.Y = value.Y;
|
||||
Update(0);
|
||||
}
|
||||
}
|
||||
|
||||
public override bool FlipX
|
||||
{
|
||||
get => skeleton.FlipX;
|
||||
set { skeleton.FlipX = value; Update(0); }
|
||||
}
|
||||
|
||||
public override bool FlipY
|
||||
{
|
||||
get => skeleton.FlipY;
|
||||
set { skeleton.FlipY = value; Update(0); }
|
||||
}
|
||||
|
||||
public override string Skin
|
||||
{
|
||||
get => skeleton.Skin?.Name ?? "default";
|
||||
set
|
||||
{
|
||||
if (!skinNames.Contains(value)) return;
|
||||
skeleton.SetSkin(value);
|
||||
skeleton.SetSlotsToSetupPose();
|
||||
Update(0);
|
||||
}
|
||||
}
|
||||
|
||||
public override string Track0Animation
|
||||
{
|
||||
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)
|
||||
{
|
||||
animationState.Update(delta);
|
||||
animationState.Apply(skeleton);
|
||||
skeleton.Update(delta);
|
||||
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 = Shader.FragmentShader;
|
||||
else
|
||||
states.Shader = null;
|
||||
|
||||
// 调试纹理
|
||||
if (!IsDebug || DebugTexture)
|
||||
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 = Shader.FragmentShader;
|
||||
else
|
||||
states.Shader = null;
|
||||
//clipping.ClipEnd();
|
||||
|
||||
// 调试纹理
|
||||
if (!IsDebug || DebugTexture)
|
||||
target.Draw(vertexArray, states);
|
||||
|
||||
// 包围盒
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,361 +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(SpineVersion.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;
|
||||
}
|
||||
|
||||
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) : 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 skin in skeletonData.Skins)
|
||||
skinNames.Add(skin.Name);
|
||||
|
||||
foreach (var anime in skeletonData.Animations)
|
||||
animationNames.Add(anime.Name);
|
||||
|
||||
skeleton = new Skeleton(skeletonData);
|
||||
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; }
|
||||
|
||||
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 animation = Track0Animation; // TODO: 适配多轨道
|
||||
var skin = Skin;
|
||||
|
||||
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;
|
||||
Track0Animation = animation; // TODO: 适配多轨道
|
||||
Skin = skin;
|
||||
}
|
||||
}
|
||||
|
||||
public override PointF Position
|
||||
{
|
||||
get => new(skeleton.X, skeleton.Y);
|
||||
set
|
||||
{
|
||||
skeleton.X = value.X;
|
||||
skeleton.Y = value.Y;
|
||||
Update(0);
|
||||
}
|
||||
}
|
||||
|
||||
public override bool FlipX
|
||||
{
|
||||
get => skeleton.FlipX;
|
||||
set { skeleton.FlipX = value; Update(0); }
|
||||
}
|
||||
|
||||
public override bool FlipY
|
||||
{
|
||||
get => skeleton.FlipY;
|
||||
set { skeleton.FlipY = value; Update(0); }
|
||||
}
|
||||
|
||||
public override string Skin
|
||||
{
|
||||
get => skeleton.Skin?.Name ?? "default";
|
||||
set
|
||||
{
|
||||
if (!skinNames.Contains(value)) return;
|
||||
skeleton.SetSkin(value);
|
||||
skeleton.SetSlotsToSetupPose();
|
||||
Update(0);
|
||||
}
|
||||
}
|
||||
|
||||
public override string Track0Animation
|
||||
{
|
||||
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)
|
||||
{
|
||||
animationState.Update(delta);
|
||||
animationState.Apply(skeleton);
|
||||
skeleton.Update(delta);
|
||||
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 = Shader.FragmentShader;
|
||||
else
|
||||
states.Shader = null;
|
||||
|
||||
// 调试纹理
|
||||
if (!IsDebug || DebugTexture)
|
||||
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);
|
||||
}
|
||||
clipping.ClipEnd();
|
||||
|
||||
if (UsePremultipliedAlpha && (states.BlendMode == BlendModeSFML.Normal || states.BlendMode == BlendModeSFML.Additive))
|
||||
states.Shader = Shader.FragmentShader;
|
||||
else
|
||||
states.Shader = null;
|
||||
|
||||
// 调试纹理
|
||||
if (!IsDebug || DebugTexture)
|
||||
target.Draw(vertexArray, states);
|
||||
|
||||
// 包围盒
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,335 +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(SpineVersion.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;
|
||||
}
|
||||
|
||||
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) : 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 skin in skeletonData.Skins)
|
||||
skinNames.Add(skin.Name);
|
||||
|
||||
foreach (var anime in skeletonData.Animations)
|
||||
animationNames.Add(anime.Name);
|
||||
|
||||
skeleton = new Skeleton(skeletonData);
|
||||
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; }
|
||||
|
||||
public override float Scale
|
||||
{
|
||||
get => Math.Abs(skeleton.ScaleX);
|
||||
set
|
||||
{
|
||||
skeleton.ScaleX = Math.Sign(skeleton.ScaleX) * value;
|
||||
skeleton.ScaleY = Math.Sign(skeleton.ScaleY) * value;
|
||||
Update(0);
|
||||
}
|
||||
}
|
||||
|
||||
public override PointF Position
|
||||
{
|
||||
get => new(skeleton.X, skeleton.Y);
|
||||
set
|
||||
{
|
||||
skeleton.X = value.X;
|
||||
skeleton.Y = value.Y;
|
||||
Update(0);
|
||||
}
|
||||
}
|
||||
|
||||
public override bool FlipX
|
||||
{
|
||||
get => skeleton.ScaleX < 0;
|
||||
set
|
||||
{
|
||||
if (skeleton.ScaleX > 0 && value || skeleton.ScaleX < 0 && !value)
|
||||
skeleton.ScaleX *= -1;
|
||||
Update(0);
|
||||
}
|
||||
}
|
||||
|
||||
public override bool FlipY
|
||||
{
|
||||
get => skeleton.ScaleY < 0;
|
||||
set
|
||||
{
|
||||
if (skeleton.ScaleY > 0 && value || skeleton.ScaleY < 0 && !value)
|
||||
skeleton.ScaleY *= -1;
|
||||
Update(0);
|
||||
}
|
||||
}
|
||||
|
||||
public override string Skin
|
||||
{
|
||||
get => skeleton.Skin?.Name ?? "default";
|
||||
set
|
||||
{
|
||||
if (!skinNames.Contains(value)) return;
|
||||
skeleton.SetSkin(value);
|
||||
skeleton.SetSlotsToSetupPose();
|
||||
Update(0);
|
||||
}
|
||||
}
|
||||
|
||||
public override string Track0Animation
|
||||
{
|
||||
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)
|
||||
{
|
||||
animationState.Update(delta);
|
||||
animationState.Apply(skeleton);
|
||||
skeleton.Update(delta);
|
||||
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 = Shader.FragmentShader;
|
||||
else
|
||||
states.Shader = null;
|
||||
|
||||
// 调试纹理
|
||||
if (!IsDebug || DebugTexture)
|
||||
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);
|
||||
}
|
||||
clipping.ClipEnd();
|
||||
|
||||
if (UsePremultipliedAlpha && (states.BlendMode == BlendModeSFML.Normal || states.BlendMode == BlendModeSFML.Additive))
|
||||
states.Shader = Shader.FragmentShader;
|
||||
else
|
||||
states.Shader = null;
|
||||
|
||||
// 调试纹理
|
||||
if (!IsDebug || DebugTexture)
|
||||
target.Draw(vertexArray, states);
|
||||
|
||||
// 包围盒
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,341 +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(SpineVersion.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;
|
||||
// 似乎是不需要设置的, 因为存在某些 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 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) : 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 skin in skeletonData.Skins)
|
||||
skinNames.Add(skin.Name);
|
||||
|
||||
foreach (var anime in skeletonData.Animations)
|
||||
animationNames.Add(anime.Name);
|
||||
|
||||
skeleton = new Skeleton(skeletonData);
|
||||
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; }
|
||||
|
||||
public override float Scale
|
||||
{
|
||||
get => Math.Abs(skeleton.ScaleX);
|
||||
set
|
||||
{
|
||||
skeleton.ScaleX = Math.Sign(skeleton.ScaleX) * value;
|
||||
skeleton.ScaleY = Math.Sign(skeleton.ScaleY) * value;
|
||||
Update(0);
|
||||
}
|
||||
}
|
||||
|
||||
public override PointF Position
|
||||
{
|
||||
get => new(skeleton.X, skeleton.Y);
|
||||
set
|
||||
{
|
||||
skeleton.X = value.X;
|
||||
skeleton.Y = value.Y;
|
||||
Update(0);
|
||||
}
|
||||
}
|
||||
|
||||
public override bool FlipX
|
||||
{
|
||||
get => skeleton.ScaleX < 0;
|
||||
set
|
||||
{
|
||||
if (skeleton.ScaleX > 0 && value || skeleton.ScaleX < 0 && !value)
|
||||
skeleton.ScaleX *= -1;
|
||||
Update(0);
|
||||
}
|
||||
}
|
||||
|
||||
public override bool FlipY
|
||||
{
|
||||
get => skeleton.ScaleY < 0;
|
||||
set
|
||||
{
|
||||
if (skeleton.ScaleY > 0 && value || skeleton.ScaleY < 0 && !value)
|
||||
skeleton.ScaleY *= -1;
|
||||
Update(0);
|
||||
}
|
||||
}
|
||||
|
||||
public override string Skin
|
||||
{
|
||||
get => skeleton.Skin?.Name ?? "default";
|
||||
set
|
||||
{
|
||||
if (!skinNames.Contains(value)) return;
|
||||
skeleton.SetSkin(value);
|
||||
skeleton.SetSlotsToSetupPose();
|
||||
Update(0);
|
||||
}
|
||||
}
|
||||
|
||||
public override string Track0Animation
|
||||
{
|
||||
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)
|
||||
{
|
||||
animationState.Update(delta);
|
||||
animationState.Apply(skeleton);
|
||||
skeleton.Update(delta);
|
||||
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 = Shader.FragmentShader;
|
||||
else
|
||||
states.Shader = null;
|
||||
|
||||
// 调试纹理
|
||||
if (!IsDebug || DebugTexture)
|
||||
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);
|
||||
}
|
||||
clipping.ClipEnd();
|
||||
|
||||
if (UsePremultipliedAlpha && (states.BlendMode == BlendModeSFML.Normal || states.BlendMode == BlendModeSFML.Additive))
|
||||
states.Shader = Shader.FragmentShader;
|
||||
else
|
||||
states.Shader = null;
|
||||
|
||||
// 调试纹理
|
||||
if (!IsDebug || DebugTexture)
|
||||
target.Draw(vertexArray, states);
|
||||
|
||||
// 调试包围盒
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,337 +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(SpineVersion.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;
|
||||
}
|
||||
|
||||
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) : 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 skin in skeletonData.Skins)
|
||||
skinNames.Add(skin.Name);
|
||||
|
||||
foreach (var anime in skeletonData.Animations)
|
||||
animationNames.Add(anime.Name);
|
||||
|
||||
skeleton = new Skeleton(skeletonData);
|
||||
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; }
|
||||
|
||||
public override float Scale
|
||||
{
|
||||
get => Math.Abs(skeleton.ScaleX);
|
||||
set
|
||||
{
|
||||
skeleton.ScaleX = Math.Sign(skeleton.ScaleX) * value;
|
||||
skeleton.ScaleY = Math.Sign(skeleton.ScaleY) * value;
|
||||
Update(0);
|
||||
}
|
||||
}
|
||||
|
||||
public override PointF Position
|
||||
{
|
||||
get => new(skeleton.X, skeleton.Y);
|
||||
set
|
||||
{
|
||||
skeleton.X = value.X;
|
||||
skeleton.Y = value.Y;
|
||||
Update(0);
|
||||
}
|
||||
}
|
||||
|
||||
public override bool FlipX
|
||||
{
|
||||
get => skeleton.ScaleX < 0;
|
||||
set
|
||||
{
|
||||
if (skeleton.ScaleX > 0 && value || skeleton.ScaleX < 0 && !value)
|
||||
skeleton.ScaleX *= -1;
|
||||
Update(0);
|
||||
}
|
||||
}
|
||||
|
||||
public override bool FlipY
|
||||
{
|
||||
get => skeleton.ScaleY < 0;
|
||||
set
|
||||
{
|
||||
if (skeleton.ScaleY > 0 && value || skeleton.ScaleY < 0 && !value)
|
||||
skeleton.ScaleY *= -1;
|
||||
Update(0);
|
||||
}
|
||||
}
|
||||
|
||||
public override string Skin
|
||||
{
|
||||
get => skeleton.Skin?.Name ?? "default";
|
||||
set
|
||||
{
|
||||
if (!skinNames.Contains(value)) return;
|
||||
skeleton.SetSkin(value);
|
||||
skeleton.SetSlotsToSetupPose();
|
||||
Update(0);
|
||||
}
|
||||
}
|
||||
|
||||
public override string Track0Animation
|
||||
{
|
||||
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)
|
||||
{
|
||||
animationState.Update(delta);
|
||||
animationState.Apply(skeleton);
|
||||
skeleton.Update(delta);
|
||||
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 = Shader.FragmentShader;
|
||||
else
|
||||
states.Shader = null;
|
||||
|
||||
// 调试纹理
|
||||
if (!IsDebug || DebugTexture)
|
||||
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);
|
||||
}
|
||||
clipping.ClipEnd();
|
||||
|
||||
if (UsePremultipliedAlpha && (states.BlendMode == BlendModeSFML.Normal || states.BlendMode == BlendModeSFML.Additive))
|
||||
states.Shader = Shader.FragmentShader;
|
||||
else
|
||||
states.Shader = null;
|
||||
|
||||
// 调试纹理
|
||||
if (!IsDebug || DebugTexture)
|
||||
target.Draw(vertexArray, states);
|
||||
|
||||
// 包围盒
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,337 +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(SpineVersion.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;
|
||||
}
|
||||
|
||||
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) : 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 skin in skeletonData.Skins)
|
||||
skinNames.Add(skin.Name);
|
||||
|
||||
foreach (var anime in skeletonData.Animations)
|
||||
animationNames.Add(anime.Name);
|
||||
|
||||
skeleton = new Skeleton(skeletonData);
|
||||
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; }
|
||||
|
||||
public override float Scale
|
||||
{
|
||||
get => Math.Abs(skeleton.ScaleX);
|
||||
set
|
||||
{
|
||||
skeleton.ScaleX = Math.Sign(skeleton.ScaleX) * value;
|
||||
skeleton.ScaleY = Math.Sign(skeleton.ScaleY) * value;
|
||||
Update(0);
|
||||
}
|
||||
}
|
||||
|
||||
public override PointF Position
|
||||
{
|
||||
get => new(skeleton.X, skeleton.Y);
|
||||
set
|
||||
{
|
||||
skeleton.X = value.X;
|
||||
skeleton.Y = value.Y;
|
||||
Update(0);
|
||||
}
|
||||
}
|
||||
|
||||
public override bool FlipX
|
||||
{
|
||||
get => skeleton.ScaleX < 0;
|
||||
set
|
||||
{
|
||||
if (skeleton.ScaleX > 0 && value || skeleton.ScaleX < 0 && !value)
|
||||
skeleton.ScaleX *= -1;
|
||||
Update(0);
|
||||
}
|
||||
}
|
||||
|
||||
public override bool FlipY
|
||||
{
|
||||
get => skeleton.ScaleY < 0;
|
||||
set
|
||||
{
|
||||
if (skeleton.ScaleY > 0 && value || skeleton.ScaleY < 0 && !value)
|
||||
skeleton.ScaleY *= -1;
|
||||
Update(0);
|
||||
}
|
||||
}
|
||||
|
||||
public override string Skin
|
||||
{
|
||||
get => skeleton.Skin?.Name ?? "default";
|
||||
set
|
||||
{
|
||||
if (!skinNames.Contains(value)) return;
|
||||
skeleton.SetSkin(value);
|
||||
skeleton.SetSlotsToSetupPose();
|
||||
Update(0);
|
||||
}
|
||||
}
|
||||
|
||||
public override string Track0Animation
|
||||
{
|
||||
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)
|
||||
{
|
||||
animationState.Update(delta);
|
||||
animationState.Apply(skeleton);
|
||||
//skeleton.Update(delta); // XXX: v4.1.x 没有 Update 方法
|
||||
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 = Shader.FragmentShader;
|
||||
else
|
||||
states.Shader = null;
|
||||
|
||||
// 调试纹理
|
||||
if (!IsDebug || DebugTexture)
|
||||
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);
|
||||
}
|
||||
clipping.ClipEnd();
|
||||
|
||||
if (UsePremultipliedAlpha && (states.BlendMode == BlendModeSFML.Normal || states.BlendMode == BlendModeSFML.Additive))
|
||||
states.Shader = Shader.FragmentShader;
|
||||
else
|
||||
states.Shader = null;
|
||||
|
||||
// 调试纹理
|
||||
if (!IsDebug || DebugTexture)
|
||||
target.Draw(vertexArray, states);
|
||||
|
||||
// 包围盒
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,337 +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(SpineVersion.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;
|
||||
}
|
||||
|
||||
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) : 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 skin in skeletonData.Skins)
|
||||
skinNames.Add(skin.Name);
|
||||
|
||||
foreach (var anime in skeletonData.Animations)
|
||||
animationNames.Add(anime.Name);
|
||||
|
||||
skeleton = new Skeleton(skeletonData);
|
||||
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; }
|
||||
|
||||
public override float Scale
|
||||
{
|
||||
get => Math.Abs(skeleton.ScaleX);
|
||||
set
|
||||
{
|
||||
skeleton.ScaleX = Math.Sign(skeleton.ScaleX) * value;
|
||||
skeleton.ScaleY = Math.Sign(skeleton.ScaleY) * value;
|
||||
Update(0);
|
||||
}
|
||||
}
|
||||
|
||||
public override PointF Position
|
||||
{
|
||||
get => new(skeleton.X, skeleton.Y);
|
||||
set
|
||||
{
|
||||
skeleton.X = value.X;
|
||||
skeleton.Y = value.Y;
|
||||
Update(0);
|
||||
}
|
||||
}
|
||||
|
||||
public override bool FlipX
|
||||
{
|
||||
get => skeleton.ScaleX < 0;
|
||||
set
|
||||
{
|
||||
if (skeleton.ScaleX > 0 && value || skeleton.ScaleX < 0 && !value)
|
||||
skeleton.ScaleX *= -1;
|
||||
Update(0);
|
||||
}
|
||||
}
|
||||
|
||||
public override bool FlipY
|
||||
{
|
||||
get => skeleton.ScaleY < 0;
|
||||
set
|
||||
{
|
||||
if (skeleton.ScaleY > 0 && value || skeleton.ScaleY < 0 && !value)
|
||||
skeleton.ScaleY *= -1;
|
||||
Update(0);
|
||||
}
|
||||
}
|
||||
|
||||
public override string Skin
|
||||
{
|
||||
get => skeleton.Skin?.Name ?? "default";
|
||||
set
|
||||
{
|
||||
if (!skinNames.Contains(value)) return;
|
||||
skeleton.SetSkin(value);
|
||||
skeleton.SetSlotsToSetupPose();
|
||||
Update(0);
|
||||
}
|
||||
}
|
||||
|
||||
public override string Track0Animation
|
||||
{
|
||||
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)
|
||||
{
|
||||
animationState.Update(delta);
|
||||
animationState.Apply(skeleton);
|
||||
skeleton.Update(delta);
|
||||
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 = Shader.FragmentShader;
|
||||
else
|
||||
states.Shader = null;
|
||||
|
||||
// 调试纹理
|
||||
if (!IsDebug || DebugTexture)
|
||||
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);
|
||||
}
|
||||
clipping.ClipEnd();
|
||||
|
||||
if (UsePremultipliedAlpha && (states.BlendMode == BlendModeSFML.Normal || states.BlendMode == BlendModeSFML.Additive))
|
||||
states.Shader = Shader.FragmentShader;
|
||||
else
|
||||
states.Shader = null;
|
||||
|
||||
// 调试纹理
|
||||
if (!IsDebug || DebugTexture)
|
||||
target.Draw(vertexArray, states);
|
||||
|
||||
// 包围盒
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
586
SpineViewer/Spine/Implementations/SpineObject/SpineObject21.cs
Normal file
586
SpineViewer/Spine/Implementations/SpineObject/SpineObject21.cs
Normal file
@@ -0,0 +1,586 @@
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Frozen;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using SpineRuntime21;
|
||||
using SpineViewer.Extensions;
|
||||
using SpineViewer.Utils;
|
||||
|
||||
namespace SpineViewer.Spine.Implementations.SpineObject
|
||||
{
|
||||
[SpineImplementation(SpineVersion.V21)]
|
||||
internal class SpineObject21 : Spine.SpineObject
|
||||
{
|
||||
//private static SFML.Graphics.BlendMode GetSFMLBlendMode(BlendMode spineBlendMode)
|
||||
//{
|
||||
// return spineBlendMode switch
|
||||
// {
|
||||
// BlendMode.Normal => BlendMode.Normal,
|
||||
// BlendMode.Additive => BlendMode.Additive,
|
||||
// BlendMode.Multiply => BlendMode.Multiply,
|
||||
// BlendMode.Screen => BlendMode.Screen,
|
||||
// _ => throw new NotImplementedException($"{spineBlendMode}"),
|
||||
// };
|
||||
//}
|
||||
|
||||
private class TextureLoader : SpineRuntime21.TextureLoader
|
||||
{
|
||||
public void Load(AtlasPage page, string path)
|
||||
{
|
||||
var texture = new SFML.Graphics.Texture(path);
|
||||
if (page.magFilter == TextureFilter.Linear)
|
||||
texture.Smooth = true;
|
||||
if (page.uWrap == TextureWrap.Repeat && page.vWrap == TextureWrap.Repeat)
|
||||
texture.Repeated = true;
|
||||
|
||||
page.rendererObject = texture;
|
||||
}
|
||||
|
||||
public void Unload(object texture)
|
||||
{
|
||||
((SFML.Graphics.Texture)texture).Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
private readonly static TextureLoader textureLoader = new();
|
||||
private static readonly Animation EmptyAnimation = new(EMPTY_ANIMATION, [], 0);
|
||||
|
||||
private readonly Atlas atlas;
|
||||
private readonly SkeletonBinary? skeletonBinary;
|
||||
private readonly SkeletonJson? skeletonJson;
|
||||
private SkeletonData skeletonData;
|
||||
private AnimationStateData animationStateData;
|
||||
|
||||
private Skeleton skeleton;
|
||||
private AnimationState animationState;
|
||||
|
||||
// 2.1.x 不支持剪裁
|
||||
//private SkeletonClipping clipping = new();
|
||||
|
||||
/// <summary>
|
||||
/// 所有插槽在所有皮肤中可用的附件集合
|
||||
/// </summary>
|
||||
private readonly Dictionary<string, Dictionary<string, Attachment>> slotAttachments = [];
|
||||
|
||||
public SpineObject21(string skelPath, string atlasPath) : base(skelPath, atlasPath)
|
||||
{
|
||||
atlas = new Atlas(AtlasPath, textureLoader);
|
||||
try
|
||||
{
|
||||
// 先尝试二进制文件
|
||||
skeletonJson = null;
|
||||
skeletonBinary = new SkeletonBinary(atlas);
|
||||
skeletonData = skeletonBinary.ReadSkeletonData(SkelPath);
|
||||
}
|
||||
catch
|
||||
{
|
||||
try
|
||||
{
|
||||
// 再尝试 Json 文件
|
||||
skeletonBinary = null;
|
||||
skeletonJson = new SkeletonJson(atlas);
|
||||
skeletonData = skeletonJson.ReadSkeletonData(SkelPath);
|
||||
}
|
||||
catch
|
||||
{
|
||||
// 都不行就报错
|
||||
throw new InvalidDataException($"Unknown skeleton file format {SkelPath}");
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var sk in skeletonData.Skins)
|
||||
{
|
||||
foreach (var (k, att) in sk.Attachments)
|
||||
{
|
||||
var slotName = skeletonData.Slots[k.Key].Name;
|
||||
if (!slotAttachments.TryGetValue(slotName, out var attachments))
|
||||
slotAttachments[slotName] = attachments = new() { [EMPTY_ATTACHMENT] = null };
|
||||
attachments[att.Name] = att;
|
||||
}
|
||||
}
|
||||
SlotAttachmentNames = slotAttachments.ToFrozenDictionary(item => item.Key, item => item.Value.Keys.ToImmutableArray());
|
||||
SkinNames = skeletonData.Skins.Select(v => v.Name).ToImmutableArray();
|
||||
AnimationNames = [EMPTY_ANIMATION, .. skeletonData.Animations.Select(v => v.Name)];
|
||||
|
||||
skeleton = new Skeleton(skeletonData) { Skin = new(Guid.NewGuid().ToString()) }; // 挂载一个空皮肤当作容器
|
||||
animationStateData = new AnimationStateData(skeletonData);
|
||||
animationState = new AnimationState(animationStateData);
|
||||
}
|
||||
|
||||
protected override void Dispose(bool disposing)
|
||||
{
|
||||
base.Dispose(disposing);
|
||||
atlas.Dispose();
|
||||
}
|
||||
|
||||
public override string FileVersion { get => skeletonData.Version; }
|
||||
|
||||
protected override float scale
|
||||
{
|
||||
get
|
||||
{
|
||||
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)
|
||||
{
|
||||
// default 不需要加载
|
||||
if (name != "default" && skeletonData.FindSkin(name) is Skin sk)
|
||||
{
|
||||
// XXX: 3.7 及以下不支持 AddSkin
|
||||
foreach (var (k, v) in sk.Attachments)
|
||||
skeleton.Skin.AddAttachment(k.Key, k.Value, v);
|
||||
}
|
||||
skeleton.SetSlotsToSetupPose();
|
||||
}
|
||||
|
||||
protected override void clearSkins()
|
||||
{
|
||||
skeleton.Skin.Attachments.Clear();
|
||||
skeleton.SetSlotsToSetupPose();
|
||||
}
|
||||
|
||||
protected override int[] getTrackIndices() => animationState.Tracks.Select((_, i) => i).Where(i => animationState.Tracks[i] is not null).ToArray();
|
||||
|
||||
protected override string getAnimation(int track) => animationState.GetCurrent(track)?.Animation.Name ?? EMPTY_ANIMATION;
|
||||
|
||||
protected override void setAnimation(int track, string name)
|
||||
{
|
||||
if (name == EMPTY_ANIMATION)
|
||||
animationState.SetAnimation(track, EmptyAnimation, 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
587
SpineViewer/Spine/Implementations/SpineObject/SpineObject36.cs
Normal file
587
SpineViewer/Spine/Implementations/SpineObject/SpineObject36.cs
Normal file
@@ -0,0 +1,587 @@
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Frozen;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using SpineRuntime36;
|
||||
using SpineViewer.Extensions;
|
||||
using SpineViewer.Utils;
|
||||
|
||||
namespace SpineViewer.Spine.Implementations.SpineObject
|
||||
{
|
||||
[SpineImplementation(SpineVersion.V36)]
|
||||
internal class SpineObject36 : Spine.SpineObject
|
||||
{
|
||||
private static SFML.Graphics.BlendMode GetSFMLBlendMode(BlendMode spineBlendMode)
|
||||
{
|
||||
return spineBlendMode switch
|
||||
{
|
||||
BlendMode.Normal => SFMLBlendMode.NormalPma,
|
||||
BlendMode.Additive => SFMLBlendMode.AdditivePma,
|
||||
BlendMode.Multiply => SFMLBlendMode.MultiplyPma,
|
||||
BlendMode.Screen => SFMLBlendMode.ScreenPma,
|
||||
_ => throw new NotImplementedException($"{spineBlendMode}"),
|
||||
};
|
||||
}
|
||||
|
||||
private class TextureLoader : SpineRuntime36.TextureLoader
|
||||
{
|
||||
public void Load(AtlasPage page, string path)
|
||||
{
|
||||
var texture = new SFML.Graphics.Texture(path);
|
||||
if (page.magFilter == TextureFilter.Linear)
|
||||
texture.Smooth = true;
|
||||
if (page.uWrap == TextureWrap.Repeat && page.vWrap == TextureWrap.Repeat)
|
||||
texture.Repeated = true;
|
||||
|
||||
page.rendererObject = texture;
|
||||
}
|
||||
|
||||
public void Unload(object texture)
|
||||
{
|
||||
((SFML.Graphics.Texture)texture).Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
private static readonly TextureLoader textureLoader = new();
|
||||
private static readonly Animation EmptyAnimation = new(EMPTY_ANIMATION, [], 0);
|
||||
|
||||
private readonly Atlas atlas;
|
||||
private readonly SkeletonBinary? skeletonBinary;
|
||||
private readonly SkeletonJson? skeletonJson;
|
||||
private SkeletonData skeletonData;
|
||||
private AnimationStateData animationStateData;
|
||||
|
||||
private Skeleton skeleton;
|
||||
private AnimationState animationState;
|
||||
|
||||
private readonly SkeletonClipping clipping = new();
|
||||
|
||||
/// <summary>
|
||||
/// 所有插槽在所有皮肤中可用的附件集合
|
||||
/// </summary>
|
||||
private readonly Dictionary<string, Dictionary<string, Attachment>> slotAttachments = [];
|
||||
|
||||
public SpineObject36(string skelPath, string atlasPath) : base(skelPath, atlasPath)
|
||||
{
|
||||
atlas = new Atlas(AtlasPath, textureLoader);
|
||||
try
|
||||
{
|
||||
// 先尝试二进制文件
|
||||
skeletonJson = null;
|
||||
skeletonBinary = new SkeletonBinary(atlas);
|
||||
skeletonData = skeletonBinary.ReadSkeletonData(SkelPath);
|
||||
}
|
||||
catch
|
||||
{
|
||||
try
|
||||
{
|
||||
// 再尝试 Json 文件
|
||||
skeletonBinary = null;
|
||||
skeletonJson = new SkeletonJson(atlas);
|
||||
skeletonData = skeletonJson.ReadSkeletonData(SkelPath);
|
||||
}
|
||||
catch
|
||||
{
|
||||
// 都不行就报错
|
||||
throw new InvalidDataException($"Unknown skeleton file format {SkelPath}");
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var sk in skeletonData.Skins)
|
||||
{
|
||||
foreach (var (k, att) in sk.Attachments)
|
||||
{
|
||||
var slotName = skeletonData.Slots.Items[k.slotIndex].Name;
|
||||
if (!slotAttachments.TryGetValue(slotName, out var attachments))
|
||||
slotAttachments[slotName] = attachments = new() { [EMPTY_ATTACHMENT] = null };
|
||||
attachments[att.Name] = att;
|
||||
}
|
||||
}
|
||||
SlotAttachmentNames = slotAttachments.ToFrozenDictionary(item => item.Key, item => item.Value.Keys.ToImmutableArray());
|
||||
SkinNames = skeletonData.Skins.Select(v => v.Name).ToImmutableArray();
|
||||
AnimationNames = [EMPTY_ANIMATION, .. skeletonData.Animations.Select(v => v.Name)];
|
||||
|
||||
skeleton = new Skeleton(skeletonData) { Skin = new(Guid.NewGuid().ToString()) }; // 挂载一个空皮肤当作容器
|
||||
animationStateData = new AnimationStateData(skeletonData);
|
||||
animationState = new AnimationState(animationStateData);
|
||||
}
|
||||
|
||||
protected override void Dispose(bool disposing)
|
||||
{
|
||||
base.Dispose(disposing);
|
||||
atlas.Dispose();
|
||||
}
|
||||
|
||||
public override string FileVersion { get => skeletonData.Version; }
|
||||
|
||||
protected override float scale
|
||||
{
|
||||
get => Math.Abs(skeleton.ScaleX);
|
||||
set
|
||||
{
|
||||
skeleton.ScaleX = Math.Sign(skeleton.ScaleX) * value;
|
||||
skeleton.ScaleY = Math.Sign(skeleton.ScaleY) * value;
|
||||
}
|
||||
}
|
||||
|
||||
protected override PointF position
|
||||
{
|
||||
get => new(skeleton.X, skeleton.Y);
|
||||
set
|
||||
{
|
||||
skeleton.X = value.X;
|
||||
skeleton.Y = value.Y;
|
||||
}
|
||||
}
|
||||
|
||||
protected override bool flipX
|
||||
{
|
||||
get => skeleton.ScaleX < 0;
|
||||
set
|
||||
{
|
||||
if (skeleton.ScaleX > 0 && value || skeleton.ScaleX < 0 && !value)
|
||||
skeleton.ScaleX *= -1;
|
||||
}
|
||||
}
|
||||
|
||||
protected override bool flipY
|
||||
{
|
||||
get => skeleton.ScaleY < 0;
|
||||
set
|
||||
{
|
||||
if (skeleton.ScaleY > 0 && value || skeleton.ScaleY < 0 && !value)
|
||||
skeleton.ScaleY *= -1;
|
||||
}
|
||||
}
|
||||
|
||||
protected override string getSlotAttachment(string slot) => skeleton.FindSlot(slot)?.Attachment?.Name ?? EMPTY_ATTACHMENT;
|
||||
|
||||
protected override void setSlotAttachment(string slot, string name)
|
||||
{
|
||||
if (slotAttachments.TryGetValue(slot, out var attachments)
|
||||
&& attachments.TryGetValue(name, out var att)
|
||||
&& skeleton.FindSlot(slot) is Slot s)
|
||||
s.Attachment = att;
|
||||
}
|
||||
|
||||
protected override void addSkin(string name)
|
||||
{
|
||||
// default 不需要加载
|
||||
if (name != "default" && skeletonData.FindSkin(name) is Skin sk)
|
||||
{
|
||||
// XXX: 3.7 及以下不支持 AddSkin
|
||||
foreach (var (k, v) in sk.Attachments)
|
||||
skeleton.Skin.AddAttachment(k.slotIndex, k.name, v);
|
||||
}
|
||||
skeleton.SetSlotsToSetupPose();
|
||||
}
|
||||
|
||||
protected override void clearSkins()
|
||||
{
|
||||
skeleton.Skin.Attachments.Clear();
|
||||
skeleton.SetSlotsToSetupPose();
|
||||
}
|
||||
|
||||
protected override int[] getTrackIndices() => animationState.Tracks.Select((_, i) => i).Where(i => animationState.Tracks.Items[i] is not null).ToArray();
|
||||
|
||||
protected override string getAnimation(int track) => animationState.GetCurrent(track)?.Animation.Name ?? EMPTY_ANIMATION;
|
||||
|
||||
protected override void setAnimation(int track, string name)
|
||||
{
|
||||
if (name == EMPTY_ANIMATION)
|
||||
animationState.SetAnimation(track, EmptyAnimation, 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
584
SpineViewer/Spine/Implementations/SpineObject/SpineObject37.cs
Normal file
584
SpineViewer/Spine/Implementations/SpineObject/SpineObject37.cs
Normal file
@@ -0,0 +1,584 @@
|
||||
using System;
|
||||
using System.Collections.Frozen;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using SpineRuntime37;
|
||||
using SpineViewer.Extensions;
|
||||
using SpineViewer.Utils;
|
||||
|
||||
namespace SpineViewer.Spine.Implementations.SpineObject
|
||||
{
|
||||
[SpineImplementation(SpineVersion.V37)]
|
||||
internal class SpineObject37 : Spine.SpineObject
|
||||
{
|
||||
private static SFML.Graphics.BlendMode GetSFMLBlendMode(BlendMode spineBlendMode)
|
||||
{
|
||||
return spineBlendMode switch
|
||||
{
|
||||
BlendMode.Normal => SFMLBlendMode.NormalPma,
|
||||
BlendMode.Additive => SFMLBlendMode.AdditivePma,
|
||||
BlendMode.Multiply => SFMLBlendMode.MultiplyPma,
|
||||
BlendMode.Screen => SFMLBlendMode.ScreenPma,
|
||||
_ => throw new NotImplementedException($"{spineBlendMode}"),
|
||||
};
|
||||
}
|
||||
|
||||
private class TextureLoader : SpineRuntime37.TextureLoader
|
||||
{
|
||||
public void Load(AtlasPage page, string path)
|
||||
{
|
||||
var texture = new SFML.Graphics.Texture(path);
|
||||
if (page.magFilter == TextureFilter.Linear)
|
||||
texture.Smooth = true;
|
||||
if (page.uWrap == TextureWrap.Repeat && page.vWrap == TextureWrap.Repeat)
|
||||
texture.Repeated = true;
|
||||
|
||||
page.rendererObject = texture;
|
||||
}
|
||||
|
||||
public void Unload(object texture)
|
||||
{
|
||||
((SFML.Graphics.Texture)texture).Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
private static readonly TextureLoader textureLoader = new();
|
||||
private static readonly Animation EmptyAnimation = new(EMPTY_ANIMATION, [], 0);
|
||||
|
||||
private readonly Atlas atlas;
|
||||
private readonly SkeletonBinary? skeletonBinary;
|
||||
private readonly SkeletonJson? skeletonJson;
|
||||
private readonly SkeletonData skeletonData;
|
||||
private readonly AnimationStateData animationStateData;
|
||||
|
||||
private readonly Skeleton skeleton;
|
||||
private readonly AnimationState animationState;
|
||||
|
||||
private readonly SkeletonClipping clipping = new();
|
||||
|
||||
/// <summary>
|
||||
/// 所有插槽在所有皮肤中可用的附件集合
|
||||
/// </summary>
|
||||
private readonly Dictionary<string, Dictionary<string, Attachment>> slotAttachments = [];
|
||||
|
||||
public SpineObject37(string skelPath, string atlasPath) : base(skelPath, atlasPath)
|
||||
{
|
||||
atlas = new Atlas(AtlasPath, textureLoader);
|
||||
try
|
||||
{
|
||||
// 先尝试二进制文件
|
||||
skeletonJson = null;
|
||||
skeletonBinary = new SkeletonBinary(atlas);
|
||||
skeletonData = skeletonBinary.ReadSkeletonData(SkelPath);
|
||||
}
|
||||
catch
|
||||
{
|
||||
try
|
||||
{
|
||||
// 再尝试 Json 文件
|
||||
skeletonBinary = null;
|
||||
skeletonJson = new SkeletonJson(atlas);
|
||||
skeletonData = skeletonJson.ReadSkeletonData(SkelPath);
|
||||
}
|
||||
catch
|
||||
{
|
||||
// 都不行就报错
|
||||
throw new InvalidDataException($"Unknown skeleton file format {SkelPath}");
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var sk in skeletonData.Skins)
|
||||
{
|
||||
foreach (var (k, att) in sk.Attachments)
|
||||
{
|
||||
var slotName = skeletonData.Slots.Items[k.slotIndex].Name;
|
||||
if (!slotAttachments.TryGetValue(slotName, out var attachments))
|
||||
slotAttachments[slotName] = attachments = new() { [EMPTY_ATTACHMENT] = null };
|
||||
attachments[att.Name] = att;
|
||||
}
|
||||
}
|
||||
SlotAttachmentNames = slotAttachments.ToFrozenDictionary(item => item.Key, item => item.Value.Keys.ToImmutableArray());
|
||||
SkinNames = skeletonData.Skins.Select(v => v.Name).ToImmutableArray();
|
||||
AnimationNames = [EMPTY_ANIMATION, .. skeletonData.Animations.Select(v => v.Name)];
|
||||
|
||||
skeleton = new Skeleton(skeletonData) { Skin = new(Guid.NewGuid().ToString()) }; // 挂载一个空皮肤当作容器
|
||||
animationStateData = new AnimationStateData(skeletonData);
|
||||
animationState = new AnimationState(animationStateData);
|
||||
}
|
||||
|
||||
protected override void Dispose(bool disposing)
|
||||
{
|
||||
base.Dispose(disposing);
|
||||
atlas.Dispose();
|
||||
}
|
||||
|
||||
public override string FileVersion { get => skeletonData.Version; }
|
||||
|
||||
protected override float scale
|
||||
{
|
||||
get => Math.Abs(skeleton.ScaleX);
|
||||
set
|
||||
{
|
||||
skeleton.ScaleX = Math.Sign(skeleton.ScaleX) * value;
|
||||
skeleton.ScaleY = Math.Sign(skeleton.ScaleY) * value;
|
||||
}
|
||||
}
|
||||
|
||||
protected override PointF position
|
||||
{
|
||||
get => new(skeleton.X, skeleton.Y);
|
||||
set
|
||||
{
|
||||
skeleton.X = value.X;
|
||||
skeleton.Y = value.Y;
|
||||
}
|
||||
}
|
||||
|
||||
protected override bool flipX
|
||||
{
|
||||
get => skeleton.ScaleX < 0;
|
||||
set
|
||||
{
|
||||
if (skeleton.ScaleX > 0 && value || skeleton.ScaleX < 0 && !value)
|
||||
skeleton.ScaleX *= -1;
|
||||
}
|
||||
}
|
||||
|
||||
protected override bool flipY
|
||||
{
|
||||
get => skeleton.ScaleY < 0;
|
||||
set
|
||||
{
|
||||
if (skeleton.ScaleY > 0 && value || skeleton.ScaleY < 0 && !value)
|
||||
skeleton.ScaleY *= -1;
|
||||
}
|
||||
}
|
||||
|
||||
protected override string getSlotAttachment(string slot) => skeleton.FindSlot(slot)?.Attachment?.Name ?? EMPTY_ATTACHMENT;
|
||||
|
||||
protected override void setSlotAttachment(string slot, string name)
|
||||
{
|
||||
if (slotAttachments.TryGetValue(slot, out var attachments)
|
||||
&& attachments.TryGetValue(name, out var att)
|
||||
&& skeleton.FindSlot(slot) is Slot s)
|
||||
s.Attachment = att;
|
||||
}
|
||||
|
||||
protected override void addSkin(string name)
|
||||
{
|
||||
// default 不需要加载
|
||||
if (name != "default" && skeletonData.FindSkin(name) is Skin sk)
|
||||
{
|
||||
// XXX: 3.7 及以下不支持 AddSkin
|
||||
foreach (var (k, v) in sk.Attachments)
|
||||
skeleton.Skin.AddAttachment(k.slotIndex, k.name, v);
|
||||
}
|
||||
skeleton.SetSlotsToSetupPose();
|
||||
}
|
||||
|
||||
protected override void clearSkins()
|
||||
{
|
||||
skeleton.Skin.Attachments.Clear();
|
||||
skeleton.SetSlotsToSetupPose();
|
||||
}
|
||||
|
||||
protected override int[] getTrackIndices() => animationState.Tracks.Select((_, i) => i).Where(i => animationState.Tracks.Items[i] is not null).ToArray();
|
||||
|
||||
protected override string getAnimation(int track) => animationState.GetCurrent(track)?.Animation.Name ?? EMPTY_ANIMATION;
|
||||
|
||||
protected override void setAnimation(int track, string name)
|
||||
{
|
||||
if (name == EMPTY_ANIMATION)
|
||||
animationState.SetAnimation(track, EmptyAnimation, 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
587
SpineViewer/Spine/Implementations/SpineObject/SpineObject38.cs
Normal file
587
SpineViewer/Spine/Implementations/SpineObject/SpineObject38.cs
Normal file
@@ -0,0 +1,587 @@
|
||||
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).ToImmutableArray();
|
||||
AnimationNames = [EMPTY_ANIMATION, .. skeletonData.Animations.Select(v => v.Name)];
|
||||
|
||||
skeleton = new Skeleton(skeletonData) { Skin = new(Guid.NewGuid().ToString()) }; // 挂载一个空皮肤当作容器
|
||||
animationStateData = new AnimationStateData(skeletonData);
|
||||
animationState = new AnimationState(animationStateData);
|
||||
}
|
||||
|
||||
protected override void Dispose(bool disposing)
|
||||
{
|
||||
base.Dispose(disposing);
|
||||
atlas.Dispose();
|
||||
}
|
||||
|
||||
public override string FileVersion { get => skeletonData.Version; }
|
||||
|
||||
protected override float scale
|
||||
{
|
||||
get => Math.Abs(skeleton.ScaleX);
|
||||
set
|
||||
{
|
||||
skeleton.ScaleX = Math.Sign(skeleton.ScaleX) * value;
|
||||
skeleton.ScaleY = Math.Sign(skeleton.ScaleY) * value;
|
||||
}
|
||||
}
|
||||
|
||||
protected override PointF position
|
||||
{
|
||||
get => new(skeleton.X, skeleton.Y);
|
||||
set
|
||||
{
|
||||
skeleton.X = value.X;
|
||||
skeleton.Y = value.Y;
|
||||
}
|
||||
}
|
||||
|
||||
protected override bool flipX
|
||||
{
|
||||
get => skeleton.ScaleX < 0;
|
||||
set
|
||||
{
|
||||
if (skeleton.ScaleX > 0 && value || skeleton.ScaleX < 0 && !value)
|
||||
skeleton.ScaleX *= -1;
|
||||
}
|
||||
}
|
||||
|
||||
protected override bool flipY
|
||||
{
|
||||
get => skeleton.ScaleY < 0;
|
||||
set
|
||||
{
|
||||
if (skeleton.ScaleY > 0 && value || skeleton.ScaleY < 0 && !value)
|
||||
skeleton.ScaleY *= -1;
|
||||
}
|
||||
}
|
||||
|
||||
protected override string getSlotAttachment(string slot) => skeleton.FindSlot(slot)?.Attachment?.Name ?? EMPTY_ATTACHMENT;
|
||||
|
||||
protected override void setSlotAttachment(string slot, string name)
|
||||
{
|
||||
if (slotAttachments.TryGetValue(slot, out var attachments)
|
||||
&& attachments.TryGetValue(name, out var att)
|
||||
&& skeleton.FindSlot(slot) is Slot s)
|
||||
s.Attachment = att;
|
||||
}
|
||||
|
||||
protected override void addSkin(string name)
|
||||
{
|
||||
// default 不需要加载
|
||||
if (name != "default" && skeletonData.FindSkin(name) is Skin sk)
|
||||
{
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
583
SpineViewer/Spine/Implementations/SpineObject/SpineObject40.cs
Normal file
583
SpineViewer/Spine/Implementations/SpineObject/SpineObject40.cs
Normal file
@@ -0,0 +1,583 @@
|
||||
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).ToImmutableArray();
|
||||
AnimationNames = [EMPTY_ANIMATION, .. skeletonData.Animations.Select(v => v.Name)];
|
||||
|
||||
skeleton = new Skeleton(skeletonData) { Skin = new(Guid.NewGuid().ToString()) }; // 挂载一个空皮肤当作容器
|
||||
animationStateData = new AnimationStateData(skeletonData);
|
||||
animationState = new AnimationState(animationStateData);
|
||||
}
|
||||
|
||||
protected override void Dispose(bool disposing)
|
||||
{
|
||||
base.Dispose(disposing);
|
||||
atlas.Dispose();
|
||||
}
|
||||
|
||||
public override string FileVersion { get => skeletonData.Version; }
|
||||
|
||||
protected override float scale
|
||||
{
|
||||
get => Math.Abs(skeleton.ScaleX);
|
||||
set
|
||||
{
|
||||
skeleton.ScaleX = Math.Sign(skeleton.ScaleX) * value;
|
||||
skeleton.ScaleY = Math.Sign(skeleton.ScaleY) * value;
|
||||
}
|
||||
}
|
||||
|
||||
protected override PointF position
|
||||
{
|
||||
get => new(skeleton.X, skeleton.Y);
|
||||
set
|
||||
{
|
||||
skeleton.X = value.X;
|
||||
skeleton.Y = value.Y;
|
||||
}
|
||||
}
|
||||
|
||||
protected override bool flipX
|
||||
{
|
||||
get => skeleton.ScaleX < 0;
|
||||
set
|
||||
{
|
||||
if (skeleton.ScaleX > 0 && value || skeleton.ScaleX < 0 && !value)
|
||||
skeleton.ScaleX *= -1;
|
||||
}
|
||||
}
|
||||
|
||||
protected override bool flipY
|
||||
{
|
||||
get => skeleton.ScaleY < 0;
|
||||
set
|
||||
{
|
||||
if (skeleton.ScaleY > 0 && value || skeleton.ScaleY < 0 && !value)
|
||||
skeleton.ScaleY *= -1;
|
||||
}
|
||||
}
|
||||
|
||||
protected override string getSlotAttachment(string slot) => skeleton.FindSlot(slot)?.Attachment?.Name ?? EMPTY_ATTACHMENT;
|
||||
|
||||
protected override void setSlotAttachment(string slot, string name)
|
||||
{
|
||||
if (slotAttachments.TryGetValue(slot, out var attachments)
|
||||
&& attachments.TryGetValue(name, out var att)
|
||||
&& skeleton.FindSlot(slot) is Slot s)
|
||||
s.Attachment = att;
|
||||
}
|
||||
|
||||
protected override void addSkin(string name)
|
||||
{
|
||||
// default 不需要加载
|
||||
if (name != "default" && skeletonData.FindSkin(name) is Skin sk)
|
||||
{
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
582
SpineViewer/Spine/Implementations/SpineObject/SpineObject41.cs
Normal file
582
SpineViewer/Spine/Implementations/SpineObject/SpineObject41.cs
Normal 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 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).ToImmutableArray();
|
||||
AnimationNames = [EMPTY_ANIMATION, .. skeletonData.Animations.Select(v => v.Name)];
|
||||
|
||||
skeleton = new Skeleton(skeletonData) { Skin = new(Guid.NewGuid().ToString()) }; // 挂载一个空皮肤当作容器
|
||||
animationStateData = new AnimationStateData(skeletonData);
|
||||
animationState = new AnimationState(animationStateData);
|
||||
}
|
||||
|
||||
protected override void Dispose(bool disposing)
|
||||
{
|
||||
base.Dispose(disposing);
|
||||
atlas.Dispose();
|
||||
}
|
||||
|
||||
public override string FileVersion { get => skeletonData.Version; }
|
||||
|
||||
protected override float scale
|
||||
{
|
||||
get => Math.Abs(skeleton.ScaleX);
|
||||
set
|
||||
{
|
||||
skeleton.ScaleX = Math.Sign(skeleton.ScaleX) * value;
|
||||
skeleton.ScaleY = Math.Sign(skeleton.ScaleY) * value;
|
||||
}
|
||||
}
|
||||
|
||||
protected override PointF position
|
||||
{
|
||||
get => new(skeleton.X, skeleton.Y);
|
||||
set
|
||||
{
|
||||
skeleton.X = value.X;
|
||||
skeleton.Y = value.Y;
|
||||
}
|
||||
}
|
||||
|
||||
protected override bool flipX
|
||||
{
|
||||
get => skeleton.ScaleX < 0;
|
||||
set
|
||||
{
|
||||
if (skeleton.ScaleX > 0 && value || skeleton.ScaleX < 0 && !value)
|
||||
skeleton.ScaleX *= -1;
|
||||
}
|
||||
}
|
||||
|
||||
protected override bool flipY
|
||||
{
|
||||
get => skeleton.ScaleY < 0;
|
||||
set
|
||||
{
|
||||
if (skeleton.ScaleY > 0 && value || skeleton.ScaleY < 0 && !value)
|
||||
skeleton.ScaleY *= -1;
|
||||
}
|
||||
}
|
||||
|
||||
protected override string getSlotAttachment(string slot) => skeleton.FindSlot(slot)?.Attachment?.Name ?? EMPTY_ATTACHMENT;
|
||||
|
||||
protected override void setSlotAttachment(string slot, string name)
|
||||
{
|
||||
if (slotAttachments.TryGetValue(slot, out var attachments)
|
||||
&& attachments.TryGetValue(name, out var att)
|
||||
&& skeleton.FindSlot(slot) is Slot s)
|
||||
s.Attachment = att;
|
||||
}
|
||||
|
||||
protected override void addSkin(string name)
|
||||
{
|
||||
// default 不需要加载
|
||||
if (name != "default" && skeletonData.FindSkin(name) is Skin sk)
|
||||
{
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
583
SpineViewer/Spine/Implementations/SpineObject/SpineObject42.cs
Normal file
583
SpineViewer/Spine/Implementations/SpineObject/SpineObject42.cs
Normal file
@@ -0,0 +1,583 @@
|
||||
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).ToImmutableArray();
|
||||
AnimationNames = [EMPTY_ANIMATION, .. skeletonData.Animations.Select(v => v.Name)];
|
||||
|
||||
skeleton = new Skeleton(skeletonData) { Skin = new(Guid.NewGuid().ToString()) }; // 挂载一个空皮肤当作容器
|
||||
animationStateData = new AnimationStateData(skeletonData);
|
||||
animationState = new AnimationState(animationStateData);
|
||||
}
|
||||
|
||||
protected override void Dispose(bool disposing)
|
||||
{
|
||||
base.Dispose(disposing);
|
||||
atlas.Dispose();
|
||||
}
|
||||
|
||||
public override string FileVersion { get => skeletonData.Version; }
|
||||
|
||||
protected override float scale
|
||||
{
|
||||
get => Math.Abs(skeleton.ScaleX);
|
||||
set
|
||||
{
|
||||
skeleton.ScaleX = Math.Sign(skeleton.ScaleX) * value;
|
||||
skeleton.ScaleY = Math.Sign(skeleton.ScaleY) * value;
|
||||
}
|
||||
}
|
||||
|
||||
protected override PointF position
|
||||
{
|
||||
get => new(skeleton.X, skeleton.Y);
|
||||
set
|
||||
{
|
||||
skeleton.X = value.X;
|
||||
skeleton.Y = value.Y;
|
||||
}
|
||||
}
|
||||
|
||||
protected override bool flipX
|
||||
{
|
||||
get => skeleton.ScaleX < 0;
|
||||
set
|
||||
{
|
||||
if (skeleton.ScaleX > 0 && value || skeleton.ScaleX < 0 && !value)
|
||||
skeleton.ScaleX *= -1;
|
||||
}
|
||||
}
|
||||
|
||||
protected override bool flipY
|
||||
{
|
||||
get => skeleton.ScaleY < 0;
|
||||
set
|
||||
{
|
||||
if (skeleton.ScaleY > 0 && value || skeleton.ScaleY < 0 && !value)
|
||||
skeleton.ScaleY *= -1;
|
||||
}
|
||||
}
|
||||
|
||||
protected override string getSlotAttachment(string slot) => skeleton.FindSlot(slot)?.Attachment?.Name ?? EMPTY_ATTACHMENT;
|
||||
|
||||
protected override void setSlotAttachment(string slot, string name)
|
||||
{
|
||||
if (slotAttachments.TryGetValue(slot, out var attachments)
|
||||
&& attachments.TryGetValue(name, out var att)
|
||||
&& skeleton.FindSlot(slot) is Slot s)
|
||||
s.Attachment = att;
|
||||
}
|
||||
|
||||
protected override void addSkin(string name)
|
||||
{
|
||||
// default 不需要加载
|
||||
if (name != "default" && skeletonData.FindSkin(name) is Skin sk)
|
||||
{
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,35 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace SpineViewer.Spine
|
||||
{
|
||||
public static class Shader
|
||||
{
|
||||
/// <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>
|
||||
/// 针对预乘 Alpha 通道的片段着色器
|
||||
/// </summary>
|
||||
public static SFML.Graphics.Shader? FragmentShader { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// 加载 Shader, 可能会存在异常导致着色器加载失败
|
||||
/// </summary>
|
||||
/// <exception cref="SFML.LoadingFailedException"></exception>
|
||||
public static void Init()
|
||||
{
|
||||
FragmentShader = SFML.Graphics.Shader.FromString(null, null, FRAGMENT_SHADER);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -9,6 +9,7 @@ using System.Text;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Nodes;
|
||||
using System.Text.Encodings.Web;
|
||||
using SpineViewer.Utils;
|
||||
|
||||
namespace SpineViewer.Spine
|
||||
{
|
||||
|
||||
@@ -1,329 +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;
|
||||
using SpineViewer.Exporter;
|
||||
|
||||
namespace SpineViewer.Spine
|
||||
{
|
||||
/// <summary>
|
||||
/// Spine 基类, 使用静态方法 New 来创建具体版本对象
|
||||
/// </summary>
|
||||
public abstract class Spine : ImplementationResolver<Spine, SpineImplementationAttribute, SpineVersion>, SFML.Graphics.Drawable, IDisposable
|
||||
{
|
||||
/// <summary>
|
||||
/// 空动画标记
|
||||
/// </summary>
|
||||
protected const string EMPTY_ANIMATION = "<Empty>";
|
||||
|
||||
/// <summary>
|
||||
/// 预览图宽
|
||||
/// </summary>
|
||||
protected const uint PREVIEW_WIDTH = 256;
|
||||
|
||||
/// <summary>
|
||||
/// 预览图高
|
||||
/// </summary>
|
||||
protected const uint PREVIEW_HEIGHT = 256;
|
||||
|
||||
/// <summary>
|
||||
/// 缩放最小值
|
||||
/// </summary>
|
||||
protected const float SCALE_MIN = 0.001f;
|
||||
|
||||
/// <summary>
|
||||
/// 创建特定版本的 Spine
|
||||
/// </summary>
|
||||
public static Spine New(SpineVersion version, string skelPath, string? atlasPath = null)
|
||||
{
|
||||
if (version == SpineVersion.Auto) version = SpineHelper.GetVersion(skelPath);
|
||||
atlasPath ??= Path.ChangeExtension(skelPath, ".atlas");
|
||||
return New(version, [skelPath, atlasPath]).PostInit();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 构造函数
|
||||
/// </summary>
|
||||
public Spine(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 Spine PostInit()
|
||||
{
|
||||
SkinNames = skinNames.AsReadOnly();
|
||||
AnimationNames = animationNames.AsReadOnly();
|
||||
|
||||
InitBounds = Bounds;
|
||||
|
||||
// XXX: tex 没办法在这里主动 Dispose
|
||||
// 批量添加在获取预览图的时候极大概率会和预览线程死锁
|
||||
// 虽然两边不会同时调用 Draw, 但是死锁似乎和 Draw 函数有关
|
||||
// 除此之外, 似乎还和 tex 的 Dispose 有关
|
||||
// 如果不对 tex 进行 Dispose, 那么不管是否 Draw 都正常不会死锁
|
||||
var tex = new SFML.Graphics.RenderTexture(PREVIEW_WIDTH, PREVIEW_HEIGHT);
|
||||
tex.SetView(InitBounds.GetView(PREVIEW_WIDTH, PREVIEW_HEIGHT));
|
||||
tex.Clear(SFML.Graphics.Color.Transparent);
|
||||
tex.Draw(this);
|
||||
tex.Display();
|
||||
|
||||
using (var img = tex.Texture.CopyToImage())
|
||||
{
|
||||
if (img.SaveToMemory(out var imgBuffer, "bmp"))
|
||||
{
|
||||
// 必须重复构造一个副本才能摆脱对流的依赖, 否则之后使用会报错
|
||||
using var stream = new MemoryStream(imgBuffer);
|
||||
using var bitmap = new Bitmap(stream);
|
||||
Preview = new Bitmap(bitmap);
|
||||
}
|
||||
}
|
||||
|
||||
// 取最后一个作为初始, 尽可能去显示非默认的内容
|
||||
Skin = SkinNames.Last();
|
||||
Track0Animation = AnimationNames.Last();
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
~Spine() { Dispose(false); }
|
||||
public void Dispose() { Dispose(true); GC.SuppressFinalize(this); }
|
||||
protected virtual void Dispose(bool disposing) { Preview?.Dispose(); }
|
||||
|
||||
#region 属性 | [0] 基本信息
|
||||
|
||||
/// <summary>
|
||||
/// 获取所属版本
|
||||
/// </summary>
|
||||
[TypeConverter(typeof(SpineVersionConverter))]
|
||||
[Category("[0] 基本信息"), DisplayName("运行时版本")]
|
||||
public SpineVersion Version { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 资源所在完整目录
|
||||
/// </summary>
|
||||
[Category("[0] 基本信息"), DisplayName("资源目录")]
|
||||
public string AssetsDir { get; }
|
||||
|
||||
/// <summary>
|
||||
/// skel 文件完整路径
|
||||
/// </summary>
|
||||
[Category("[0] 基本信息"), DisplayName("skel文件路径")]
|
||||
public string SkelPath { get; }
|
||||
|
||||
/// <summary>
|
||||
/// atlas 文件完整路径
|
||||
/// </summary>
|
||||
[Category("[0] 基本信息"), DisplayName("atlas文件路径")]
|
||||
public string AtlasPath { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 名称
|
||||
/// </summary>
|
||||
[Category("[0] 基本信息"), DisplayName("名称")]
|
||||
public string Name { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 获取所属文件版本
|
||||
/// </summary>
|
||||
[Category("[0] 基本信息"), DisplayName("文件版本")]
|
||||
public abstract string FileVersion { get; }
|
||||
|
||||
#endregion
|
||||
|
||||
#region 属性 | [1] 设置
|
||||
|
||||
/// <summary>
|
||||
/// 是否被隐藏, 被隐藏的模型将仅仅在列表显示, 不参与其他行为
|
||||
/// </summary>
|
||||
[Category("[1] 设置"), DisplayName("是否隐藏")]
|
||||
public bool IsHidden { get; set; } = false;
|
||||
|
||||
/// <summary>
|
||||
/// 是否使用预乘Alpha
|
||||
/// </summary>
|
||||
[Category("[1] 设置"), DisplayName("预乘Alpha通道")]
|
||||
public bool UsePremultipliedAlpha { get; set; } = true;
|
||||
|
||||
#endregion
|
||||
|
||||
#region 属性 | [2] 变换
|
||||
|
||||
/// <summary>
|
||||
/// 缩放比例
|
||||
/// </summary>
|
||||
[Category("[2] 变换"), DisplayName("缩放比例")]
|
||||
public abstract float Scale { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 位置
|
||||
/// </summary>
|
||||
[TypeConverter(typeof(PointFConverter))]
|
||||
[Category("[2] 变换"), DisplayName("位置")]
|
||||
public abstract PointF Position { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 水平翻转
|
||||
/// </summary>
|
||||
[Category("[2] 变换"), DisplayName("水平翻转")]
|
||||
public abstract bool FlipX { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 垂直翻转
|
||||
/// </summary>
|
||||
[Category("[2] 变换"), DisplayName("垂直翻转")]
|
||||
public abstract bool FlipY { get; set; }
|
||||
|
||||
#endregion
|
||||
|
||||
#region 属性 | [3] 动画
|
||||
|
||||
/// <summary>
|
||||
/// 包含的所有皮肤名称
|
||||
/// </summary>
|
||||
public ReadOnlyCollection<string> SkinNames { get; private set; }
|
||||
protected List<string> skinNames = [];
|
||||
|
||||
/// <summary>
|
||||
/// 使用的皮肤名称, 如果设置的皮肤不存在则忽略
|
||||
/// </summary>
|
||||
[TypeConverter(typeof(SkinConverter))]
|
||||
[Category("[3] 动画"), DisplayName("皮肤")]
|
||||
public abstract string Skin { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 包含的所有动画名称
|
||||
/// </summary>
|
||||
public ReadOnlyCollection<string> AnimationNames { get; private set; }
|
||||
protected List<string> animationNames = [EMPTY_ANIMATION];
|
||||
|
||||
/// <summary>
|
||||
/// 默认轨道动画名称, 如果设置的动画不存在则忽略
|
||||
/// </summary>
|
||||
[TypeConverter(typeof(AnimationConverter))]
|
||||
[Category("[3] 动画"), DisplayName("默认轨道动画")]
|
||||
public abstract string Track0Animation { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 默认轨道动画时长
|
||||
/// </summary>
|
||||
[Category("[3] 动画"), DisplayName("默认轨道动画时长")]
|
||||
public float Track0AnimationDuration { get => GetAnimationDuration(Track0Animation); } // TODO: 动画时长变成伪属性在面板显示
|
||||
|
||||
#endregion
|
||||
|
||||
#region 属性 | [4] 调试
|
||||
|
||||
/// <summary>
|
||||
/// 显示调试
|
||||
/// </summary>
|
||||
[Browsable(false)]
|
||||
public bool IsDebug { get; set; } = false;
|
||||
|
||||
/// <summary>
|
||||
/// 显示纹理
|
||||
/// </summary>
|
||||
[Category("[4] 调试"), DisplayName("显示纹理")]
|
||||
public bool DebugTexture { get; set; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// 显示包围盒
|
||||
/// </summary>
|
||||
[Category("[4] 调试"), DisplayName("显示包围盒")]
|
||||
public bool DebugBounds { get; set; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// 显示骨骼
|
||||
/// </summary>
|
||||
[Category("[4] 调试"), DisplayName("显示骨架")]
|
||||
public bool DebugBones { get; set; } = false;
|
||||
|
||||
#endregion
|
||||
|
||||
/// <summary>
|
||||
/// 标识符
|
||||
/// </summary>
|
||||
public readonly string ID = Guid.NewGuid().ToString();
|
||||
|
||||
/// <summary>
|
||||
/// 是否被选中
|
||||
/// </summary>
|
||||
[Browsable(false)]
|
||||
public bool IsSelected { get; set; } = false;
|
||||
|
||||
/// <summary>
|
||||
/// 骨骼包围盒
|
||||
/// </summary>
|
||||
[Browsable(false)]
|
||||
public abstract RectangleF Bounds { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 初始状态下的骨骼包围盒
|
||||
/// </summary>
|
||||
[Browsable(false)]
|
||||
public RectangleF InitBounds { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// 骨骼预览图
|
||||
/// </summary>
|
||||
[Browsable(false)]
|
||||
public Image Preview { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// 获取动画时长, 如果动画不存在则返回 0
|
||||
/// </summary>
|
||||
public abstract float GetAnimationDuration(string name);
|
||||
|
||||
/// <summary>
|
||||
/// 更新内部状态
|
||||
/// </summary>
|
||||
public abstract void Update(float delta);
|
||||
|
||||
#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>
|
||||
/// 包围盒颜色
|
||||
/// </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>
|
||||
/// SFML.Graphics.Drawable 接口实现
|
||||
/// </summary>
|
||||
public abstract void Draw(SFML.Graphics.RenderTarget target, SFML.Graphics.RenderStates states);
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
90
SpineViewer/Spine/SpineExporter/AvifExporter.cs
Normal file
90
SpineViewer/Spine/SpineExporter/AvifExporter.cs
Normal 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; }
|
||||
}
|
||||
}
|
||||
60
SpineViewer/Spine/SpineExporter/CustomExporter.cs
Normal file
60
SpineViewer/Spine/SpineExporter/CustomExporter.cs
Normal 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; }
|
||||
}
|
||||
}
|
||||
61
SpineViewer/Spine/SpineExporter/ExportHelper.cs
Normal file
61
SpineViewer/Spine/SpineExporter/ExportHelper.cs
Normal file
@@ -0,0 +1,61 @@
|
||||
using FFMpegCore.Pipes;
|
||||
using SpineViewer.Extensions;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Drawing.Imaging;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
|
||||
namespace SpineViewer.Spine.SpineExporter
|
||||
{
|
||||
/// <summary>
|
||||
/// SFML.Graphics.Image 帧对象包装类, 将接管给定的 image 对象生命周期
|
||||
/// </summary>
|
||||
public class SFMLImageVideoFrame(SFML.Graphics.Image image) : IVideoFrame, IDisposable
|
||||
{
|
||||
public int Width => (int)image.Size.X;
|
||||
public int Height => (int)image.Size.Y;
|
||||
public string Format => "rgba";
|
||||
public void Serialize(Stream pipe) => pipe.Write(image.Pixels);
|
||||
public async Task SerializeAsync(Stream pipe, CancellationToken token) => await pipe.WriteAsync(image.Pixels, token);
|
||||
public void Dispose() => image.Dispose();
|
||||
|
||||
/// <summary>
|
||||
/// Save the contents of the image to a file
|
||||
/// </summary>
|
||||
/// <param name="filename">Path of the file to save (overwritten if already exist)</param>
|
||||
/// <returns>True if saving was successful</returns>
|
||||
public bool SaveToFile(string filename) => image.SaveToFile(filename);
|
||||
|
||||
/// <summary>
|
||||
/// Save the image to a buffer in memory The format of the image must be specified.
|
||||
/// The supported image formats are bmp, png, tga and jpg. This function fails if
|
||||
/// the image is empty, or if the format was invalid.
|
||||
/// </summary>
|
||||
/// <param name="output">Byte array filled with encoded data</param>
|
||||
/// <param name="format">Encoding format to use</param>
|
||||
/// <returns>True if saving was successful</returns>
|
||||
public bool SaveToMemory(out byte[] output, string format) => image.SaveToMemory(out output, format);
|
||||
|
||||
/// <summary>
|
||||
/// 获取 Winforms Bitmap 对象
|
||||
/// </summary>
|
||||
public Bitmap CopyToBitmap() => image.CopyToBitmap();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 为帧导出创建的辅助类
|
||||
/// </summary>
|
||||
public static class ExportHelper
|
||||
{
|
||||
/// <summary>
|
||||
/// 根据 Bitmap 文件格式获取合适的文件后缀
|
||||
/// </summary>
|
||||
public static string GetSuffix(this ImageFormat imageFormat)
|
||||
{
|
||||
if (imageFormat == ImageFormat.Icon) return ".ico";
|
||||
else if (imageFormat == ImageFormat.Exif) return ".jpeg";
|
||||
else return $".{imageFormat.ToString().ToLower()}";
|
||||
}
|
||||
}
|
||||
}
|
||||
410
SpineViewer/Spine/SpineExporter/Exporter.cs
Normal file
410
SpineViewer/Spine/SpineExporter/Exporter.cs
Normal 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; }
|
||||
}
|
||||
}
|
||||
135
SpineViewer/Spine/SpineExporter/FFmpegVideoExporter.cs
Normal file
135
SpineViewer/Spine/SpineExporter/FFmpegVideoExporter.cs
Normal 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; }
|
||||
}
|
||||
}
|
||||
133
SpineViewer/Spine/SpineExporter/FrameExporter.cs
Normal file
133
SpineViewer/Spine/SpineExporter/FrameExporter.cs
Normal 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; }
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
using SpineViewer.Exporter.Implementations.ExportArgs;
|
||||
using SpineViewer.Spine;
|
||||
using SpineViewer.Spine;
|
||||
using SpineViewer.Utils;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
@@ -7,28 +7,28 @@ using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace SpineViewer.Exporter.Implementations.Exporter
|
||||
namespace SpineViewer.Spine.SpineExporter
|
||||
{
|
||||
/// <summary>
|
||||
/// 帧序列导出器
|
||||
/// </summary>
|
||||
[ExportImplementation(ExportType.FrameSequence)]
|
||||
public class FrameSequenceExporter : VideoExporter
|
||||
{
|
||||
public FrameSequenceExporter(FrameSequenceExportArgs exportArgs) : base(exportArgs) { }
|
||||
/// <summary>
|
||||
/// 文件名后缀, 同时决定帧图像格式, 支持的格式为 <c>".png", ".jpg", ".tga", ".bmp"</c>
|
||||
/// </summary>
|
||||
public string Suffix { get; set; } = ".png";
|
||||
|
||||
protected override void ExportSingle(Spine.Spine[] spinesToRender, BackgroundWorker? worker = null)
|
||||
protected override void ExportSingle(SpineObject[] spinesToRender, BackgroundWorker? worker = null)
|
||||
{
|
||||
var args = (FrameSequenceExportArgs)ExportArgs;
|
||||
|
||||
// 导出单个时必定提供输出文件夹,
|
||||
var saveDir = Path.Combine(args.OutputDir, $"frames_{timestamp}_{args.FPS:f0}");
|
||||
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}_{args.FPS:f0}_{frameIdx:d6}{args.FileSuffix}";
|
||||
var filename = $"frames_{timestamp}_{FPS:f0}_{frameIdx:d6}{Suffix}";
|
||||
var savePath = Path.Combine(saveDir, filename);
|
||||
|
||||
try
|
||||
@@ -48,22 +48,21 @@ namespace SpineViewer.Exporter.Implementations.Exporter
|
||||
}
|
||||
}
|
||||
|
||||
protected override void ExportIndividual(Spine.Spine[] spinesToRender, BackgroundWorker? worker = null)
|
||||
protected override void ExportIndividual(SpineObject[] spinesToRender, BackgroundWorker? worker = null)
|
||||
{
|
||||
var args = (FrameSequenceExportArgs)ExportArgs;
|
||||
foreach (var spine in spinesToRender)
|
||||
{
|
||||
if (worker?.CancellationPending == true) break; // 取消的日志在 GetFrames 里输出
|
||||
|
||||
// 如果提供了输出文件夹, 则全部导出到输出文件夹, 否则导出到各自的文件夹下
|
||||
var subDir = $"{spine.Name}_{timestamp}_{args.FPS:f0}";
|
||||
var saveDir = args.OutputDir is null ? Path.Combine(spine.AssetsDir, subDir) : Path.Combine(args.OutputDir, subDir);
|
||||
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}_{args.FPS:f0}_{frameIdx:d6}{args.FileSuffix}";
|
||||
var filename = $"{spine.Name}_{timestamp}_{FPS:f0}_{frameIdx:d6}{Suffix}";
|
||||
var savePath = Path.Combine(saveDir, filename);
|
||||
|
||||
try
|
||||
@@ -84,4 +83,17 @@ namespace SpineViewer.Exporter.Implementations.Exporter
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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; }
|
||||
}
|
||||
}
|
||||
79
SpineViewer/Spine/SpineExporter/GifExporter.cs
Normal file
79
SpineViewer/Spine/SpineExporter/GifExporter.cs
Normal 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; }
|
||||
}
|
||||
}
|
||||
78
SpineViewer/Spine/SpineExporter/MkvExporter.cs
Normal file
78
SpineViewer/Spine/SpineExporter/MkvExporter.cs
Normal 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; }
|
||||
}
|
||||
}
|
||||
79
SpineViewer/Spine/SpineExporter/MovExporter.cs
Normal file
79
SpineViewer/Spine/SpineExporter/MovExporter.cs
Normal 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; }
|
||||
}
|
||||
}
|
||||
78
SpineViewer/Spine/SpineExporter/Mp4Exporter.cs
Normal file
78
SpineViewer/Spine/SpineExporter/Mp4Exporter.cs
Normal 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; }
|
||||
}
|
||||
}
|
||||
169
SpineViewer/Spine/SpineExporter/VideoExporter.cs
Normal file
169
SpineViewer/Spine/SpineExporter/VideoExporter.cs
Normal 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; }
|
||||
}
|
||||
}
|
||||
79
SpineViewer/Spine/SpineExporter/WebmExporter.cs
Normal file
79
SpineViewer/Spine/SpineExporter/WebmExporter.cs
Normal 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; }
|
||||
}
|
||||
}
|
||||
101
SpineViewer/Spine/SpineExporter/WebpExporter.cs
Normal file
101
SpineViewer/Spine/SpineExporter/WebpExporter.cs
Normal 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; }
|
||||
}
|
||||
}
|
||||
567
SpineViewer/Spine/SpineObject.cs
Normal file
567
SpineViewer/Spine/SpineObject.cs
Normal 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();
|
||||
|
||||
// 初始化皮肤加载情况, 不需要记录 default
|
||||
foreach (var n in SkinNames.Where(v => v != "default")) 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 name == "default" || 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
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user