Compare commits
32 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
951d0e30ae | ||
|
|
711e172769 | ||
|
|
faa60f0ea1 | ||
|
|
99d81c4329 | ||
|
|
17904326f3 | ||
|
|
5ee74f39d8 | ||
|
|
72f898ed60 | ||
|
|
157eab5bac | ||
|
|
e1b0d0a2ad | ||
|
|
2c050ba031 | ||
|
|
41518b16b4 | ||
|
|
72a16dc95f | ||
|
|
3404c64f55 | ||
|
|
b9015422f8 | ||
|
|
a7441b968d | ||
|
|
2d44be31f7 | ||
|
|
c2cf25bb2b | ||
|
|
7c4c53dcb0 | ||
|
|
aceb3b17c8 | ||
|
|
adfcfdb1de | ||
|
|
da329723bc | ||
|
|
63eb53fa06 | ||
|
|
d32c824515 | ||
|
|
e9ee8c481c | ||
|
|
6d78e52605 | ||
|
|
90136a5562 | ||
|
|
1592767c8c | ||
|
|
afa6ce2113 | ||
|
|
50e6e414ee | ||
|
|
ba9b8edcdc | ||
|
|
d7a927475c | ||
|
|
afe210343f |
@@ -1,5 +1,10 @@
|
||||
# CHANGELOG
|
||||
|
||||
## v0.11.0
|
||||
|
||||
- 完成导出系统, 支持完整的单帧和帧序列导出功能
|
||||
- 预览画面增加快进功能
|
||||
|
||||
## v0.10.9
|
||||
|
||||
- 预览图导出增加名称后缀参数
|
||||
|
||||
113
README.en.md
113
README.en.md
@@ -1,10 +1,14 @@
|
||||
Below is the translated English version of your README:
|
||||
|
||||
---
|
||||
|
||||
# [SpineViewer](https://github.com/ww-rm/SpineViewer)
|
||||
|
||||
[](https://github.com/ww-rm/SpineViewer/actions/workflows/dotnet-desktop.yml)
|
||||
|
||||
[中文](README.md) | [English](README.en.md)
|
||||
|
||||
A simple and user-friendly tool for viewing and exporting Spine files.
|
||||
A *WYSIWYG* Spine file viewer and exporter.
|
||||
|
||||

|
||||
|
||||
@@ -12,81 +16,86 @@ A simple and user-friendly tool for viewing and exporting Spine files.
|
||||
|
||||
## Installation
|
||||
|
||||
Go to the [Release](https://github.com/ww-rm/SpineViewer/releases) page to download the compressed package.
|
||||
Go 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).
|
||||
|
||||
You can also download the package with the `SelfContained` suffix, which can run independently.
|
||||
You can also download the zip package with the `SelfContained` suffix, which can run independently.
|
||||
|
||||
## Version Support
|
||||
## Supported Export Formats
|
||||
|
||||
| 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` | | | |
|
||||
- [x] Single Frame Image
|
||||
- [x] Frame Sequence
|
||||
- [x] Animated GIF
|
||||
- [ ] MKV
|
||||
- [ ] MP4
|
||||
- [ ] MOV
|
||||
- [ ] WebM
|
||||
|
||||
More formats are under development :rocket::rocket::rocket:
|
||||
|
||||
## Supported Spine Versions
|
||||
|
||||
| Version | View & Export | Format Conversion | Version Conversion |
|
||||
| :-------: | :-----------: | :---------------: | :----------------: |
|
||||
| `2.1.x` | :white_check_mark: | | |
|
||||
| `3.1.x` | | | |
|
||||
| `3.4.x` | | | |
|
||||
| `3.5.x` | | | |
|
||||
| `3.6.x` | :white_check_mark: | | |
|
||||
| `3.7.x` | :white_check_mark: | | |
|
||||
| `3.8.x` | :white_check_mark: | :white_check_mark: | |
|
||||
| `4.1.x` | :white_check_mark: | | |
|
||||
| `4.2.x` | :white_check_mark: | | |
|
||||
| `4.3.x` | | | |
|
||||
|
||||
More versions are under development :rocket::rocket::rocket:
|
||||
|
||||
## Usage
|
||||
|
||||
### Importing Skeletons
|
||||
|
||||
There are 3 ways to import skeleton files:
|
||||
There are three ways to import skeleton files:
|
||||
|
||||
- **Drag & Drop/Paste:**
|
||||
Drag and drop or paste the skeleton file/directory into the model list.
|
||||
This method automatically searches through the provided files and subdirectories. Although convenient, it relies on the file structure and has its limitations.
|
||||
|
||||
- Only standard files with `*.json`, `*.skel`, or `.atlas` extensions are automatically detected.
|
||||
- The skeleton file and atlas file must have the same name.
|
||||
- The version string in the skeleton file must not be modified.
|
||||
|
||||
- **Batch Open from the File Menu:**
|
||||
This method offers more file flexibility. You can drag and drop or paste files into the file selection dialog, and additional options are available.
|
||||
|
||||
- The filename restrictions are similar to the above, but you can use the panel’s file selection button to choose skeleton files with non-standard extensions.
|
||||
- You can set a fixed load version to handle cases where the version number has been modified.
|
||||
|
||||
- **Open a Single Model:**
|
||||
This method offers the highest degree of freedom, allowing you to select any skeleton file and atlas file without filename restrictions. You can also set the load version.
|
||||
- Drag and drop or paste the skeleton file/directory into the model list.
|
||||
- Open skeleton files in batch from the File menu.
|
||||
- Select a single model to open from the File menu.
|
||||
|
||||
### Adjusting Preview Content
|
||||
|
||||
The model list supports right-click menus and various shortcut keys, and you can select multiple models to adjust their parameters in bulk.
|
||||
The model list supports right-click menus and several hotkeys, and multiple models can be selected for batch adjustments of model parameters.
|
||||
|
||||
In addition to the parameter panel, the preview area supports several mouse actions:
|
||||
In addition to using the control panel for parameter settings, the preview window supports the following mouse actions:
|
||||
|
||||
- **Left-click:** Select and drag models. Holding down the `Ctrl` key enables multi-selection, which syncs with the model list.
|
||||
- **Right-click:** Drag the overall canvas.
|
||||
- **Scroll wheel:** Zoom the view.
|
||||
- **Selective Rendering:** The preview area supports a mode to render only the selected models. In this mode, only the selected models are displayed, and selection changes must be made through the model list.
|
||||
- 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.
|
||||
- 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.
|
||||
|
||||
In the function menu, you can reset and synchronize the animation time for all skeletons.
|
||||
The buttons below the preview window allow you to adjust the timeline, effectively serving as a simple player.
|
||||
|
||||
### Exporting Preview Content
|
||||
|
||||
Both preview images and videos can be exported.
|
||||
Export follows the "What You See Is What You Get" principle—what you see in the live preview is exactly what gets exported.
|
||||
|
||||
- **Preview Image:**
|
||||
The exported preview image shows the model in its default state, with one image per model.
|
||||
|
||||
- **Video (TODO: Currently only supports frame sequence export):**
|
||||
The complete animation duration for each skeleton can be viewed in the model parameters.
|
||||
|
||||
When the preview area is set to render only the selected models, the exported content will include only the models that are displayed.
|
||||
There are a few key parameters for exporting:
|
||||
|
||||
### Format & Version Conversion
|
||||
- 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.
|
||||
|
||||
You can use the tools menu to convert skeleton files. This feature supports conversion between binary and text formats, as well as between different versions.
|
||||
### More Information
|
||||
|
||||
Currently under development, it only supports converting `3.8.x` binary format to text format.
|
||||
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).
|
||||
|
||||
## Acknowledgements
|
||||
|
||||
- [spine-runtimes](https://github.com/EsotericSoftware/spine-runtimes)
|
||||
- [SFML.Net](https://github.com/SFML/SFML.Net)
|
||||
- [FFMpegCore](https://github.com/rosenbjerg/FFMpegCore)
|
||||
|
||||
---
|
||||
|
||||
*If you like this project, please give it a :star: and share it with more people! :)*
|
||||
*If you like this project, please give it a :star: and share it with others! :)*
|
||||
|
||||
[](https://starchart.cc/ww-rm/SpineViewer)
|
||||
57
README.md
57
README.md
@@ -4,7 +4,7 @@
|
||||
|
||||
[中文](README.md) | [English](README.en.md)
|
||||
|
||||
一个简单好用的 Spine 文件查看&导出程序.
|
||||
*所见即所得* 的 Spine 文件查看&导出程序.
|
||||
|
||||

|
||||
|
||||
@@ -18,7 +18,19 @@
|
||||
|
||||
也可以下载带有 `SelfContained` 后缀的压缩包, 可以独立运行.
|
||||
|
||||
## 版本支持
|
||||
## 导出格式支持
|
||||
|
||||
- [x] 单帧画面
|
||||
- [x] 帧序列
|
||||
- [x] GIF 动图
|
||||
- [ ] MKV
|
||||
- [ ] MP4
|
||||
- [ ] MOV
|
||||
- [ ] WebM
|
||||
|
||||
更多格式正在施工 :rocket::rocket::rocket:
|
||||
|
||||
## Spine 版本支持
|
||||
|
||||
| 版本 | 查看&导出 | 格式转换 | 版本转换 |
|
||||
| :---: | :---: | :---: | :---: |
|
||||
@@ -33,6 +45,8 @@
|
||||
| `4.2.x` | :white_check_mark: | | |
|
||||
| `4.3.x` | | | |
|
||||
|
||||
更多版本正在施工 :rocket::rocket::rocket:
|
||||
|
||||
## 使用方法
|
||||
|
||||
### 骨骼导入
|
||||
@@ -40,21 +54,8 @@
|
||||
有 3 种模式导入骨骼文件:
|
||||
|
||||
- 拖放/粘贴需要导入的骨骼文件/目录到模型列表
|
||||
|
||||
这种方式会自动查找传入的文件列表以及目录内的子级文件列表, 虽然方便但是依赖模型文件结构, 限制最多.
|
||||
|
||||
- 仅支持自动发现标准的 `*.json`/`*.skel`/`.atlas` 后缀文件.
|
||||
- 骨骼文件和 atlas 文件需要是同名.
|
||||
- 需要保证骨骼文件里的版本字符串不是魔改过的.
|
||||
- 从文件菜单里批量打开骨骼文件
|
||||
|
||||
这种方式提供一定程度的文件自由度, 文件选择框里同样支持拖放/粘贴, 但是多一些额外选项.
|
||||
|
||||
- 文件名限制条件与上面类似, 但是可以通过面板的选择文件按钮选择非标准后缀的骨骼文件.
|
||||
- 可以设置固定加载版本, 便于应对魔改过的版本号.
|
||||
- 选择单个模型打开
|
||||
|
||||
这种方式自由度最高, 允许选择任意的骨骼文件和 atlas 文件, 可以没有文件名限制, 并且也可以设置加载版本.
|
||||
- 从文件菜单选择单个模型打开
|
||||
|
||||
### 预览内容调整
|
||||
|
||||
@@ -65,26 +66,32 @@
|
||||
- 左键可以选择和拖拽模型, 按下 `Ctrl` 键可以实现多选, 与左侧列表选择是联动的.
|
||||
- 右键对整体画面进行拖动.
|
||||
- 滚轮进行画面缩放.
|
||||
- 预览画面支持仅渲染选中, 在该模式下, 画面仅显示被选中的模型, 并且只能通过左侧列表改变选中状态.
|
||||
- 仅渲染选中模式, 在该模式下, 预览画面仅包含被选中的模型, 并且只能通过左侧列表改变选中状态.
|
||||
|
||||
在功能菜单中, 可以重置同步所有骨骼动画时间.
|
||||
预览画面下方按钮支持对画面时间进行调整, 可以当作一个简易的播放器.
|
||||
|
||||
### 预览内容导出
|
||||
|
||||
支持预览图和视频的导出.
|
||||
导出遵循 "所见即所得" 原则, 即实时预览的画面就是你导出的画面.
|
||||
|
||||
预览图导出的内容是模型的默认状态画面, 每个模型一张单独的预览图.
|
||||
导出有以下几个关键参数:
|
||||
|
||||
视频(TODO: 目前仅支持帧序列导出), 可以在每个骨骼的模型参数中查看动画完整时长.
|
||||
- 仅渲染选中. 这个参数不仅影响预览模式, 也影响导出, 如果仅渲染选中, 那么在导出时只有被选中的模型会被考虑, 忽略其他模型.
|
||||
- 输出文件夹. 这个参数某些时候可选, 当不提供时, 则将输出产物输出到每个模型各自的模型文件夹, 否则输出产物全部输出到提供的输出文件夹.
|
||||
- 导出单个. 默认是每个模型独立导出, 即对模型列表进行批量操作, 如果选择仅导出单个, 那么被导出的所有模型将在同一个画面上被渲染, 输出产物只有一份.
|
||||
|
||||
当预览画面处于仅渲染选中状态时, 导出的内容仅包含被选中的模型, 也就是在画面中显示的内容.
|
||||
### 更多
|
||||
|
||||
### 格式与版本转换
|
||||
更为详细的使用方法和说明见 [Wiki](https://github.com/ww-rm/SpineViewer/wiki), 有使用上的问题或者 BUG 可以提个 [Issue](https://github.com/ww-rm/SpineViewer/issues).
|
||||
|
||||
可以通过工具菜单进行骨骼文件转换, 允许二进制和文本格式之间的转换, 以及不同版本间的转换.
|
||||
## Acknowledgements
|
||||
|
||||
目前处于施工中, 仅支持转换 `3.8.x` 二进制到文本格式.
|
||||
- [spine-runtimes](https://github.com/EsotericSoftware/spine-runtimes)
|
||||
- [SFML.Net](https://github.com/SFML/SFML.Net)
|
||||
- [FFMpegCore](https://github.com/rosenbjerg/FFMpegCore)
|
||||
|
||||
---
|
||||
|
||||
*如果你觉得这个项目不错请给个 :star:, 并分享给更多人知道! :)*
|
||||
|
||||
[](https://starchart.cc/ww-rm/SpineViewer)
|
||||
|
||||
@@ -341,6 +341,7 @@ namespace SpineViewer.Controls
|
||||
toolStripMenuItem_MoveDown.Enabled = selectedCount == 1 && selectedIndices[0] != itemsCount - 1;
|
||||
toolStripMenuItem_MoveBottom.Enabled = selectedCount == 1 && selectedIndices[0] != itemsCount - 1;
|
||||
toolStripMenuItem_RemoveAll.Enabled = itemsCount > 0;
|
||||
toolStripMenuItem_CopyPreview.Enabled = selectedCount > 0;
|
||||
|
||||
// 视图选项
|
||||
toolStripMenuItem_LargeIconView.Checked = listView.View == View.LargeIcon;
|
||||
@@ -500,10 +501,8 @@ namespace SpineViewer.Controls
|
||||
foreach (int i in listView.SelectedIndices)
|
||||
{
|
||||
var spine = spines[i];
|
||||
var image = spine.Preview;
|
||||
var path = Path.Combine(Program.TempDir, $"{spine.ID}.png");
|
||||
using (var clone = new Bitmap(image))
|
||||
clone.Save(path);
|
||||
spine.Preview.Save(path);
|
||||
fileDropList.Add(path);
|
||||
}
|
||||
}
|
||||
|
||||
168
SpineViewer/Controls/SpinePreviewer.Designer.cs
generated
168
SpineViewer/Controls/SpinePreviewer.Designer.cs
generated
@@ -28,13 +28,28 @@
|
||||
/// </summary>
|
||||
private void InitializeComponent()
|
||||
{
|
||||
components = new System.ComponentModel.Container();
|
||||
System.ComponentModel.ComponentResourceManager resources = new System.ComponentModel.ComponentResourceManager(typeof(SpinePreviewer));
|
||||
panel = new Panel();
|
||||
tableLayoutPanel1 = new TableLayoutPanel();
|
||||
panel_Container = new Panel();
|
||||
flowLayoutPanel1 = new FlowLayoutPanel();
|
||||
button_Stop = new Button();
|
||||
imageList = new ImageList(components);
|
||||
button_Restart = new Button();
|
||||
button_Start = new Button();
|
||||
button_ForwardStep = new Button();
|
||||
button_ForwardFast = new Button();
|
||||
toolTip = new ToolTip(components);
|
||||
tableLayoutPanel1.SuspendLayout();
|
||||
panel_Container.SuspendLayout();
|
||||
flowLayoutPanel1.SuspendLayout();
|
||||
SuspendLayout();
|
||||
//
|
||||
// panel
|
||||
//
|
||||
panel.BackColor = SystemColors.ControlDarkDark;
|
||||
panel.Location = new Point(160, 160);
|
||||
panel.Location = new Point(157, 136);
|
||||
panel.Margin = new Padding(0);
|
||||
panel.Name = "panel";
|
||||
panel.Size = new Size(320, 320);
|
||||
@@ -44,20 +59,165 @@
|
||||
panel.MouseUp += panel_MouseUp;
|
||||
panel.MouseWheel += panel_MouseWheel;
|
||||
//
|
||||
// tableLayoutPanel1
|
||||
//
|
||||
tableLayoutPanel1.ColumnCount = 1;
|
||||
tableLayoutPanel1.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 100F));
|
||||
tableLayoutPanel1.Controls.Add(panel_Container, 0, 0);
|
||||
tableLayoutPanel1.Controls.Add(flowLayoutPanel1, 0, 1);
|
||||
tableLayoutPanel1.Dock = DockStyle.Fill;
|
||||
tableLayoutPanel1.Location = new Point(0, 0);
|
||||
tableLayoutPanel1.Margin = new Padding(0);
|
||||
tableLayoutPanel1.Name = "tableLayoutPanel1";
|
||||
tableLayoutPanel1.RowCount = 2;
|
||||
tableLayoutPanel1.RowStyles.Add(new RowStyle(SizeType.Percent, 100F));
|
||||
tableLayoutPanel1.RowStyles.Add(new RowStyle());
|
||||
tableLayoutPanel1.Size = new Size(641, 636);
|
||||
tableLayoutPanel1.TabIndex = 2;
|
||||
//
|
||||
// panel_Container
|
||||
//
|
||||
panel_Container.BackColor = SystemColors.ControlDark;
|
||||
panel_Container.Controls.Add(panel);
|
||||
panel_Container.Dock = DockStyle.Fill;
|
||||
panel_Container.Location = new Point(0, 0);
|
||||
panel_Container.Margin = new Padding(0);
|
||||
panel_Container.Name = "panel_Container";
|
||||
panel_Container.Size = new Size(641, 594);
|
||||
panel_Container.TabIndex = 0;
|
||||
//
|
||||
// flowLayoutPanel1
|
||||
//
|
||||
flowLayoutPanel1.Anchor = AnchorStyles.None;
|
||||
flowLayoutPanel1.AutoSize = true;
|
||||
flowLayoutPanel1.AutoSizeMode = AutoSizeMode.GrowAndShrink;
|
||||
flowLayoutPanel1.Controls.Add(button_Stop);
|
||||
flowLayoutPanel1.Controls.Add(button_Restart);
|
||||
flowLayoutPanel1.Controls.Add(button_Start);
|
||||
flowLayoutPanel1.Controls.Add(button_ForwardStep);
|
||||
flowLayoutPanel1.Controls.Add(button_ForwardFast);
|
||||
flowLayoutPanel1.Location = new Point(138, 594);
|
||||
flowLayoutPanel1.Margin = new Padding(0);
|
||||
flowLayoutPanel1.Name = "flowLayoutPanel1";
|
||||
flowLayoutPanel1.Size = new Size(365, 42);
|
||||
flowLayoutPanel1.TabIndex = 1;
|
||||
//
|
||||
// button_Stop
|
||||
//
|
||||
button_Stop.AutoSize = true;
|
||||
button_Stop.AutoSizeMode = AutoSizeMode.GrowAndShrink;
|
||||
button_Stop.ImageKey = "stop";
|
||||
button_Stop.ImageList = imageList;
|
||||
button_Stop.Location = new Point(3, 3);
|
||||
button_Stop.Name = "button_Stop";
|
||||
button_Stop.Padding = new Padding(15, 3, 15, 3);
|
||||
button_Stop.Size = new Size(67, 36);
|
||||
button_Stop.TabIndex = 0;
|
||||
toolTip.SetToolTip(button_Stop, "停止播放并重置时间到初始");
|
||||
button_Stop.UseVisualStyleBackColor = true;
|
||||
button_Stop.Click += button_Stop_Click;
|
||||
//
|
||||
// imageList
|
||||
//
|
||||
imageList.ColorDepth = ColorDepth.Depth32Bit;
|
||||
imageList.ImageStream = (ImageListStreamer)resources.GetObject("imageList.ImageStream");
|
||||
imageList.TransparentColor = Color.Transparent;
|
||||
imageList.Images.SetKeyName(0, "stop");
|
||||
imageList.Images.SetKeyName(1, "restart");
|
||||
imageList.Images.SetKeyName(2, "start");
|
||||
imageList.Images.SetKeyName(3, "pause");
|
||||
imageList.Images.SetKeyName(4, "forward-step");
|
||||
imageList.Images.SetKeyName(5, "forward-fast");
|
||||
//
|
||||
// button_Restart
|
||||
//
|
||||
button_Restart.AutoSize = true;
|
||||
button_Restart.AutoSizeMode = AutoSizeMode.GrowAndShrink;
|
||||
button_Restart.ImageKey = "restart";
|
||||
button_Restart.ImageList = imageList;
|
||||
button_Restart.Location = new Point(76, 3);
|
||||
button_Restart.Name = "button_Restart";
|
||||
button_Restart.Padding = new Padding(15, 3, 15, 3);
|
||||
button_Restart.Size = new Size(67, 36);
|
||||
button_Restart.TabIndex = 1;
|
||||
toolTip.SetToolTip(button_Restart, "从头开始播放");
|
||||
button_Restart.UseVisualStyleBackColor = true;
|
||||
button_Restart.Click += button_Restart_Click;
|
||||
//
|
||||
// button_Start
|
||||
//
|
||||
button_Start.AutoSize = true;
|
||||
button_Start.AutoSizeMode = AutoSizeMode.GrowAndShrink;
|
||||
button_Start.BackgroundImageLayout = ImageLayout.Center;
|
||||
button_Start.ImageKey = "pause";
|
||||
button_Start.ImageList = imageList;
|
||||
button_Start.Location = new Point(149, 3);
|
||||
button_Start.Name = "button_Start";
|
||||
button_Start.Padding = new Padding(15, 3, 15, 3);
|
||||
button_Start.Size = new Size(67, 36);
|
||||
button_Start.TabIndex = 2;
|
||||
toolTip.SetToolTip(button_Start, "开始/暂停");
|
||||
button_Start.UseVisualStyleBackColor = true;
|
||||
button_Start.Click += button_Start_Click;
|
||||
//
|
||||
// button_ForwardStep
|
||||
//
|
||||
button_ForwardStep.AutoSize = true;
|
||||
button_ForwardStep.AutoSizeMode = AutoSizeMode.GrowAndShrink;
|
||||
button_ForwardStep.ImageKey = "forward-step";
|
||||
button_ForwardStep.ImageList = imageList;
|
||||
button_ForwardStep.Location = new Point(222, 3);
|
||||
button_ForwardStep.Name = "button_ForwardStep";
|
||||
button_ForwardStep.Padding = new Padding(15, 3, 15, 3);
|
||||
button_ForwardStep.Size = new Size(67, 36);
|
||||
button_ForwardStep.TabIndex = 3;
|
||||
toolTip.SetToolTip(button_ForwardStep, "快进 1 帧");
|
||||
button_ForwardStep.UseVisualStyleBackColor = true;
|
||||
button_ForwardStep.Click += button_ForwardStep_Click;
|
||||
//
|
||||
// button_ForwardFast
|
||||
//
|
||||
button_ForwardFast.AutoSize = true;
|
||||
button_ForwardFast.AutoSizeMode = AutoSizeMode.GrowAndShrink;
|
||||
button_ForwardFast.ImageKey = "forward-fast";
|
||||
button_ForwardFast.ImageList = imageList;
|
||||
button_ForwardFast.Location = new Point(295, 3);
|
||||
button_ForwardFast.Name = "button_ForwardFast";
|
||||
button_ForwardFast.Padding = new Padding(15, 3, 15, 3);
|
||||
button_ForwardFast.Size = new Size(67, 36);
|
||||
button_ForwardFast.TabIndex = 4;
|
||||
toolTip.SetToolTip(button_ForwardFast, "快进 10 帧");
|
||||
button_ForwardFast.UseVisualStyleBackColor = true;
|
||||
button_ForwardFast.Click += button_ForwardFast_Click;
|
||||
//
|
||||
// SpinePreviewer
|
||||
//
|
||||
AutoScaleDimensions = new SizeF(11F, 24F);
|
||||
AutoScaleMode = AutoScaleMode.Font;
|
||||
BackColor = SystemColors.ControlDark;
|
||||
Controls.Add(panel);
|
||||
Controls.Add(tableLayoutPanel1);
|
||||
Name = "SpinePreviewer";
|
||||
Size = new Size(640, 640);
|
||||
Size = new Size(641, 636);
|
||||
SizeChanged += SpinePreviewer_SizeChanged;
|
||||
tableLayoutPanel1.ResumeLayout(false);
|
||||
tableLayoutPanel1.PerformLayout();
|
||||
panel_Container.ResumeLayout(false);
|
||||
flowLayoutPanel1.ResumeLayout(false);
|
||||
flowLayoutPanel1.PerformLayout();
|
||||
ResumeLayout(false);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
private Panel panel;
|
||||
private TableLayoutPanel tableLayoutPanel1;
|
||||
private Panel panel_Container;
|
||||
private FlowLayoutPanel flowLayoutPanel1;
|
||||
private Button button_Stop;
|
||||
private Button button_Start;
|
||||
private ImageList imageList;
|
||||
private ToolTip toolTip;
|
||||
private Button button_ForwardStep;
|
||||
private Button button_ForwardFast;
|
||||
private Button button_Restart;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,12 +9,45 @@ using System.Threading.Tasks;
|
||||
using System.Windows.Forms;
|
||||
using System.Security.Policy;
|
||||
using System.Diagnostics;
|
||||
using NLog.Targets;
|
||||
|
||||
namespace SpineViewer.Controls
|
||||
{
|
||||
public partial class SpinePreviewer : UserControl
|
||||
{
|
||||
/// <summary>
|
||||
/// 要绑定的 Spine 列表控件
|
||||
/// </summary>
|
||||
[Category("自定义"), Description("相关联的 SpineListView")]
|
||||
public SpineListView? SpineListView { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 属性信息面板
|
||||
/// </summary>
|
||||
[Category("自定义"), Description("用于显示画面属性的属性页")]
|
||||
public PropertyGrid? PropertyGrid
|
||||
{
|
||||
get => propertyGrid;
|
||||
set
|
||||
{
|
||||
propertyGrid = value;
|
||||
if (propertyGrid is not null)
|
||||
propertyGrid.SelectedObject = new PreviewerProperty(this);
|
||||
}
|
||||
}
|
||||
private PropertyGrid? propertyGrid;
|
||||
|
||||
#region 画面参数
|
||||
|
||||
/// <summary>
|
||||
/// 画面缩放最大值
|
||||
/// </summary>
|
||||
public const float ZOOM_MAX = 1000f;
|
||||
|
||||
/// <summary>
|
||||
/// 画面缩放最小值
|
||||
/// </summary>
|
||||
public const float ZOOM_MIN = 0.001f;
|
||||
|
||||
/// <summary>
|
||||
/// 包装类, 用于属性面板显示
|
||||
/// </summary>
|
||||
@@ -50,74 +83,6 @@ namespace SpineViewer.Controls
|
||||
public uint MaxFps { get => previewer.MaxFps; set => previewer.MaxFps = value; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 要绑定的 Spine 列表控件
|
||||
/// </summary>
|
||||
[Category("自定义"), Description("相关联的 SpineListView")]
|
||||
public SpineListView? SpineListView { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 属性信息面板
|
||||
/// </summary>
|
||||
[Category("自定义"), Description("用于显示画面属性的属性页")]
|
||||
public PropertyGrid? PropertyGrid
|
||||
{
|
||||
get => propertyGrid;
|
||||
set
|
||||
{
|
||||
propertyGrid = value;
|
||||
if (propertyGrid is not null)
|
||||
propertyGrid.SelectedObject = new PreviewerProperty(this);
|
||||
}
|
||||
}
|
||||
private PropertyGrid? propertyGrid;
|
||||
|
||||
/// <summary>
|
||||
/// 画面缩放最大值
|
||||
/// </summary>
|
||||
public const float ZOOM_MAX = 1000f;
|
||||
|
||||
/// <summary>
|
||||
/// 画面缩放最小值
|
||||
/// </summary>
|
||||
public const float ZOOM_MIN = 0.001f;
|
||||
|
||||
/// <summary>
|
||||
/// 预览画面背景色
|
||||
/// </summary>
|
||||
private static readonly SFML.Graphics.Color BackgroundColor = new(105, 105, 105);
|
||||
|
||||
/// <summary>
|
||||
/// 预览画面坐标轴颜色
|
||||
/// </summary>
|
||||
private static readonly SFML.Graphics.Color AxisColor = new(220, 220, 220);
|
||||
|
||||
/// <summary>
|
||||
/// 坐标轴顶点缓冲区
|
||||
/// </summary>
|
||||
private readonly SFML.Graphics.VertexArray AxisVertex = new(SFML.Graphics.PrimitiveType.Lines, 2);
|
||||
|
||||
/// <summary>
|
||||
/// 渲染窗口
|
||||
/// </summary>
|
||||
private readonly SFML.Graphics.RenderWindow RenderWindow;
|
||||
|
||||
/// <summary>
|
||||
/// 帧间隔计时器
|
||||
/// </summary>
|
||||
private readonly SFML.System.Clock Clock = new();
|
||||
|
||||
/// <summary>
|
||||
/// 画面拖放对象世界坐标源点
|
||||
/// </summary>
|
||||
private SFML.System.Vector2f? draggingSrc = null;
|
||||
|
||||
/// <summary>
|
||||
/// 渲染任务
|
||||
/// </summary>
|
||||
private Task? task = null;
|
||||
private CancellationTokenSource? cancelToken = null;
|
||||
|
||||
/// <summary>
|
||||
/// 分辨率
|
||||
/// </summary>
|
||||
@@ -280,6 +245,13 @@ namespace SpineViewer.Controls
|
||||
public uint MaxFps { get => maxFps; set { RenderWindow.SetFramerateLimit(value); maxFps = value; } }
|
||||
private uint maxFps = 60;
|
||||
|
||||
/// <summary>
|
||||
/// 获取 View
|
||||
/// </summary>
|
||||
public SFML.Graphics.View GetView() => RenderWindow.GetView();
|
||||
|
||||
#endregion
|
||||
|
||||
public SpinePreviewer()
|
||||
{
|
||||
InitializeComponent();
|
||||
@@ -293,27 +265,37 @@ namespace SpineViewer.Controls
|
||||
MaxFps = 30;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 预览画面帧参数
|
||||
/// </summary>
|
||||
public SpinePreviewerFrameArgs GetFrameArgs() => new(Resolution, RenderWindow.GetView(), RenderSelectedOnly);
|
||||
#region 渲染线程管理
|
||||
|
||||
/// <summary>
|
||||
/// 开始预览
|
||||
/// 渲染窗口
|
||||
/// </summary>
|
||||
public void StartPreview()
|
||||
private readonly SFML.Graphics.RenderWindow RenderWindow;
|
||||
|
||||
/// <summary>
|
||||
/// 渲染任务
|
||||
/// </summary>
|
||||
private Task? task = null;
|
||||
private CancellationTokenSource? cancelToken = null;
|
||||
|
||||
/// <summary>
|
||||
/// 开始渲染
|
||||
/// </summary>
|
||||
public void StartRender()
|
||||
{
|
||||
if (task is not null)
|
||||
return;
|
||||
cancelToken = new();
|
||||
task = Task.Run(RenderTask, cancelToken.Token);
|
||||
IsUpdating = true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 停止预览
|
||||
/// 停止渲染
|
||||
/// </summary>
|
||||
public void StopPreview()
|
||||
public void StopRender()
|
||||
{
|
||||
IsUpdating = false;
|
||||
if (task is null || cancelToken is null)
|
||||
return;
|
||||
cancelToken.Cancel();
|
||||
@@ -322,6 +304,58 @@ namespace SpineViewer.Controls
|
||||
task = null;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region 渲染更新管理
|
||||
|
||||
/// <summary>
|
||||
/// 是否更新画面
|
||||
/// </summary>
|
||||
public bool IsUpdating
|
||||
{
|
||||
get => isUpdating;
|
||||
private set
|
||||
{
|
||||
if (value == isUpdating) return;
|
||||
if (value)
|
||||
{
|
||||
button_Start.ImageKey = "pause";
|
||||
}
|
||||
else
|
||||
{
|
||||
button_Start.ImageKey = "start";
|
||||
}
|
||||
isUpdating = value;
|
||||
}
|
||||
}
|
||||
private bool isUpdating = true;
|
||||
|
||||
/// <summary>
|
||||
/// 快进时间量
|
||||
/// </summary>
|
||||
private float forwardDelta = 0;
|
||||
private object _forwardDeltaLock = new();
|
||||
|
||||
/// <summary>
|
||||
/// 预览画面背景色
|
||||
/// </summary>
|
||||
private static readonly SFML.Graphics.Color BackgroundColor = new(105, 105, 105);
|
||||
|
||||
/// <summary>
|
||||
/// 预览画面坐标轴颜色
|
||||
/// </summary>
|
||||
private static readonly SFML.Graphics.Color AxisColor = new(220, 220, 220);
|
||||
|
||||
/// <summary>
|
||||
/// 坐标轴顶点缓冲区
|
||||
/// </summary>
|
||||
private readonly SFML.Graphics.VertexArray AxisVertex = new(SFML.Graphics.PrimitiveType.Lines, 2);
|
||||
|
||||
/// <summary>
|
||||
/// 帧间隔计时器
|
||||
/// </summary>
|
||||
private readonly SFML.System.Clock Clock = new();
|
||||
|
||||
/// <summary>
|
||||
/// 渲染任务
|
||||
/// </summary>
|
||||
@@ -362,6 +396,17 @@ namespace SpineViewer.Controls
|
||||
break; // 提前中止
|
||||
|
||||
var spine = spines[i];
|
||||
|
||||
// 停止更新的时候只是时间不前进, 但是坐标变换还是要更新, 否则无法移动对象
|
||||
if (!IsUpdating) delta = 0;
|
||||
|
||||
// 加上要快进的量
|
||||
lock (_forwardDeltaLock)
|
||||
{
|
||||
delta += forwardDelta;
|
||||
forwardDelta = 0;
|
||||
}
|
||||
|
||||
spine.Update(delta);
|
||||
|
||||
if (RenderSelectedOnly && !spine.IsSelected)
|
||||
@@ -383,13 +428,20 @@ namespace SpineViewer.Controls
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
/// <summary>
|
||||
/// 画面拖放对象世界坐标源点
|
||||
/// </summary>
|
||||
private SFML.System.Vector2f? draggingSrc = null;
|
||||
|
||||
private void SpinePreviewer_SizeChanged(object sender, EventArgs e)
|
||||
{
|
||||
if (RenderWindow is null)
|
||||
return;
|
||||
|
||||
float parentX = Width;
|
||||
float parentY = Height;
|
||||
float parentX = panel.Parent.Width;
|
||||
float parentY = panel.Parent.Height;
|
||||
float sizeX = panel.Width;
|
||||
float sizeY = panel.Height;
|
||||
|
||||
@@ -538,27 +590,58 @@ namespace SpineViewer.Controls
|
||||
Zoom *= (e.Delta > 0 ? 1.1f : 0.9f);
|
||||
PropertyGrid?.Refresh();
|
||||
}
|
||||
|
||||
private void button_Stop_Click(object sender, EventArgs e)
|
||||
{
|
||||
IsUpdating = false;
|
||||
if (SpineListView is not null)
|
||||
{
|
||||
lock (SpineListView.Spines)
|
||||
{
|
||||
foreach (var spine in SpineListView.Spines)
|
||||
spine.CurrentAnimation = spine.CurrentAnimation;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void button_Restart_Click(object sender, EventArgs e)
|
||||
{
|
||||
if (SpineListView is not null)
|
||||
{
|
||||
lock (SpineListView.Spines)
|
||||
{
|
||||
foreach (var spine in SpineListView.Spines)
|
||||
spine.CurrentAnimation = spine.CurrentAnimation;
|
||||
}
|
||||
}
|
||||
IsUpdating = true;
|
||||
}
|
||||
|
||||
private void button_Start_Click(object sender, EventArgs e)
|
||||
{
|
||||
IsUpdating = !IsUpdating;
|
||||
}
|
||||
|
||||
private void button_ForwardStep_Click(object sender, EventArgs e)
|
||||
{
|
||||
lock (_forwardDeltaLock)
|
||||
{
|
||||
forwardDelta += 1f / maxFps;
|
||||
}
|
||||
}
|
||||
|
||||
private void button_ForwardFast_Click(object sender, EventArgs e)
|
||||
{
|
||||
lock (_forwardDeltaLock)
|
||||
{
|
||||
forwardDelta += 10f / maxFps;
|
||||
}
|
||||
}
|
||||
|
||||
//public void ClickStopButton() => button_Stop_Click(button_Stop, EventArgs.Empty);
|
||||
//public void ClickRestartButton() => button_Restart_Click(button_Restart, EventArgs.Empty);
|
||||
//public void ClickStartButton() => button_Start_Click(button_Start, EventArgs.Empty);
|
||||
//public void ClickForwardStepButton() => button_ForwardStep_Click(button_ForwardStep, EventArgs.Empty);
|
||||
//public void ClickForwardFastButton() => button_ForwardFast_Click(button_ForwardFast, EventArgs.Empty);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 预览画面帧参数
|
||||
/// </summary>
|
||||
public class SpinePreviewerFrameArgs(Size resolution, SFML.Graphics.View view, bool renderSelectedOnly)
|
||||
{
|
||||
/// <summary>
|
||||
/// 分辨率
|
||||
/// </summary>
|
||||
public Size Resolution => resolution;
|
||||
|
||||
/// <summary>
|
||||
/// 渲染视窗
|
||||
/// </summary>
|
||||
public SFML.Graphics.View View => view;
|
||||
|
||||
/// <summary>
|
||||
/// 是否仅渲染/导出选中骨骼
|
||||
/// </summary>
|
||||
public bool RenderSelectedOnly => renderSelectedOnly;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -117,4 +117,210 @@
|
||||
<resheader name="writer">
|
||||
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
||||
</resheader>
|
||||
<metadata name="imageList.TrayLocation" type="System.Drawing.Point, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a">
|
||||
<value>17, 17</value>
|
||||
</metadata>
|
||||
<data name="imageList.ImageStream" mimetype="application/x-microsoft.net.object.binary.base64">
|
||||
<value>
|
||||
AAEAAAD/////AQAAAAAAAAAMAgAAAEZTeXN0ZW0uV2luZG93cy5Gb3JtcywgQ3VsdHVyZT1uZXV0cmFs
|
||||
LCBQdWJsaWNLZXlUb2tlbj1iNzdhNWM1NjE5MzRlMDg5BQEAAAAmU3lzdGVtLldpbmRvd3MuRm9ybXMu
|
||||
SW1hZ2VMaXN0U3RyZWFtZXIBAAAABERhdGEHAgIAAAAJAwAAAA8DAAAAHi0AAAJNU0Z0AUkBTAIBAQYB
|
||||
AAFwAQABcAEAAR8BAAEYAQAE/wEhAQAI/wFCAU0BNgcAATYDAAEoAwABfAMAATADAAEBAQABIAYAAV0q
|
||||
AAQCAy0BRQNbAc0DXQHoA1kBxgMyAU8DDwEUBAIYAAMOARIDQwF3A10BzwNbAc0DLQFFBAIYAANWAbID
|
||||
XQHoA1wB1gNDAXcDFgEeBAIYAAMKAQ0DSQGFA14B4wNfAeUDUQGeAyQBNAMJAQsEARgAAwsBDgM7AWQD
|
||||
XgHSA1YBsv8AEQAEAgMxAUwDYQHhAwAB/wMuAfkDYAHbA0QBewMeASoDBgEIFAADGAEhA1cBwgMhAfsD
|
||||
YQHhAzEBTAQCGAADWQHDAwAB/wMhAfwDXAHrA0gBhAMWAR4YAAMLAQ4DTQGSAysB+QMPAf4DUAHyA1gB
|
||||
ugM3AVoDEQEWAwIBAxQAAxcBHwNJAYYDXAHrA1kBw/8AEQAEAgMxAUwDYQHhAwAB/wMAAf8DHgH9A2AB
|
||||
4ANQAZoDLgFGAxEBFgMGAQcEAQgAAxoBJANbAc0DAAH/A2EB4QMxAUwEAhgAA1kBwwMAAf8DAAH/AwAB
|
||||
/wNbAdADPgFrAw8BEwMCAQMQAAMLAQ4DTQGSAysB+QMAAf8DAAH/AzkB9gNcAcsDRAF5Ax4BKgMGAQcM
|
||||
AAQBAxgBIQNKAYsDWwHsA1kBw/8AEQAEAgMxAUwDYQHhAwAB/wMuAfkDXAHnA0MB9QNWAe4DWQG7A0MB
|
||||
dwMoATsDDwEUBAEEAAMaASQDWwHNAwAB/wNhAeEDMQFMBAIYAANZAcMDIAH8A1AB8ANDAfUDLgH5A10B
|
||||
zgNDAXcDGgEjAwIBAwwAAwsBDgNNAZIDKwH5AwEB/wMkAfoDOgH4Ay4B+QNdAeMDTgGYAyQBNQMGAQgE
|
||||
AQQABAEDGAEhA0oBiwNbAewDWQHD/wARAAQCAzEBTANhAeEDAAH/A1wB2QM7AWMDWQG7Az0B9wM5AfgD
|
||||
XAHnA1sBxQNBAXMDEwEZAwIBAwMaASQDWwHNAwAB/wNhAeEDMQFMBAIYAANZAcMDXAHrA1ABmgNVAbED
|
||||
TQHzAyEB+wNbAeQDSwGPAxsBJQMDAQQIAAMLAQ4DTQGSAysB+QMfAf0DXAHZA1oBxwNDAfUDDwH+A1wB
|
||||
6wNSAaMDJQE3AwMBBAQABAEDGAEhA0oBiwNbAewDWQHD/wARAAQCAzEBTANhAeEDAAH/A1sBzQMaASQD
|
||||
FgEdA1UBrwMAAf8DAAH/AwAB/wMAAf8DVQGvAxYBHQMaASQDWwHNAwAB/wNhAeEDMQFMBAIYAANZAcMD
|
||||
WwHkAzsBZQMHAQkDSQGGAyEB+wMAAf8DAAH/A1kBwQMdASkIAAMLAQ4DTQGSAysB+QMrAfkDTQGSAzkE
|
||||
XgHiAwAB/wMAAf8DXQHjAzYBWAMFAQYEAAQBAxgBIQNKAYsDWwHsA1kBw/8AEQAEAgMxAUwDYQHhAwAB
|
||||
/wNbAc0DGgEkAwIBAwMTARoDRgF/A1sB3gMhAfsDDwH+AzkB+ANZAbsDOwFjA1wB2QMAAf8DYQHhAzEB
|
||||
TAQCGAADWQHDA1sB5AM7AWUDBwQJAQwDOgFhA18B1QNCAfUDLgH5A1sBygMyAU8DDwEUAw0BEQNNAZID
|
||||
KwH5AysB+QNNAZIDEQEWAy0BRANaAb8DUgHvAyEB+wNdAdwDPwFuAxYBHgMEAQUDGAEhA0oBiwNbAewD
|
||||
WQHD/wARAAQCAzEBTANhAeEDAAH/A1sBzQMaASQEAAQCAxMBGgM9AWcDWQHAA1IB7wMPAf4DPQH3A1wB
|
||||
5wMuAfkDAAH/A2EB4QMxAUwEAhgAA1kBwwNbAeQDOwFlAwcBCQQAAwsBDgMxAU0DWAG9AzkB9gMuAfkD
|
||||
YAHbA0QBeAMhAS8DTgGVAysB+QMrAfkDTQGSAwsBDgMGAQgDIAEuA00BkgNdAdwDQwH1A1oB6QNOAZYD
|
||||
KAE8AyABLQNLAY0DWwHsA1kBw/8AEQAEAgMxAUwDYQHhAwAB/wNbAc0DGgEkCAAEAgMMARADLwFJA1IB
|
||||
owNbAeQDPQH3Aw8B/gMAAf8DAAH/A2EB4QMxAUwEAhgAA1kBwwNbAeQDOwFlAwcBCQgAAxABFQM/AW0D
|
||||
XAHWAyQB+gMeAf0DXAHWA0QBeQNUAasDIQH7AysB+QNNAZIDCwEOBAADAwEEAxsBJgNBAXMDWgHEA0UB
|
||||
9ANWAe4DVQG0A0QBegNRAaIDVgHuA1kBw/8AEQAEAgMxAUwDYQHhAwAB/wNbAc0DGgEkEAADCAEKAyMB
|
||||
MwNEAXkDVwG8A18B5QMkAfoDAAH/A2EB4QMxAUwEAhgAA1kBwwNbAeQDOwFlAwcBCQgAAwMBBAMaASQD
|
||||
QgF0A14B0gMhAfsDOQH2A10B0QNgAeADHgH9AysB+QNNAZIDCwEOCAADAgEDAxIBFwM1AVUDWAG6A1UB
|
||||
8QM5AfYDWwHeA2AB2wM5AfgDWQHD/wARAAQCAzEBTANhAeEDAAH/A1sBzQMaASQUAAMCAQMDDgESAyMB
|
||||
MgM9AWgDXgHdAwAB/wNhAeEDMQFMBAIYAANZAcMDWwHkAzsBZQMHAQkMAAMCAQMDDwETAzQBUwNdAdED
|
||||
OQH4Ax8B/AMfAf0DAAH/AysB+QNNAZIDCwEODAAEAQMJAQwDIwEzA1UBrgMgAf0DHgH9Ax8B/AMPAf4D
|
||||
WQHD/wARAAQCAzEBTANhAeEDAAH/A1sBzQMaASQgAAMaASQDWwHNAwAB/wNhAeEDMQFMBAIYAANZAcMD
|
||||
WwHkAzsBZQMHAQkYAAMPARQDVwG5AwAB/wMAAf8DAAH/AysB+QNNAZIDCwEOGAADRwGAA1wB7QMAAf8D
|
||||
AAH/AwAB/wNZAcP/ABEABAIDMQFMA2EB4QMAAf8DWwHNAxoBJBQABAIDBwEJAxYBHQM1AVUDXAHZAwAB
|
||||
/wNhAeEDMQFMBAIYAANZAcMDWwHkAzsBZQMHAQkMAAQBAw0BEQM0AVMDXQHRAy4B+QMQAf4DDwH+AwAB
|
||||
/wMrAfkDTQGSAwsBDgwABAEDCQELAx4BKwNTAakDIAH9Ax4B/QMfAfwDDwH+A1kBw/8AEQAEAgMxAUwD
|
||||
YQHhAwAB/wNbAc0DGgEkEAADBwEJAxsBJgM0AVMDTQGSA14B3QMuAfkDAAH/A2EB4QMxAUwEAhgAA1kB
|
||||
wwNbAeQDOwFlAwcBCQgABAIDEAEVAzkBXgNbAc0DIQH7AyEB+wNcAecDVgHuAw8B/gMrAfkDTQGSAwsB
|
||||
DggAAwIBAwMSARcDNQFVA1gBuANVAfEDOQH2A1sB3gNgAdsDOQH4A1kBw/8AEQAEAgMxAUwDYQHhAwAB
|
||||
/wNbAc0DGgEkCAAEAgMMARADLgFGA00BkgNcAcgDXAHrAx4B/QMAAf8DAAH/A2EB4QMxAUwEAhgAA1kB
|
||||
wwNbAeQDOwFlAwcBCQgAAwkBDAMxAU4DWAG3A00B8wMeAf0DXgHdA04BlgNZAb4DHwH8AysB+QNNAZID
|
||||
CwEOBAADAwEEAxsBJgNBAXMDWgHEA0UB9ANWAe4DVQG0A0QBegNRAaIDVgHuA1kBw/8AEQAEAgMxAUwD
|
||||
YQHhAwAB/wNbAc0DGgEkBAAEAgMTARoDPQFnA1oBvwNcAesDJAH6AzoB9gNcAecDLgH5AwAB/wNhAeED
|
||||
MQFMBAIYAANZAcMDWwHkAzsBZQMHAQkEAAMLAQ4DLgFHA1UBrQNSAe8DOgH4A2AB2wNEAXoDJAE1A04B
|
||||
mAMjAfoDKwH5A00BkgMLAQ4DBgEIAyABLgNNAZIDXQHcA0MB9QNaAekDTgGWAygBPAMgAS0DSwGNA1sB
|
||||
7ANZAcP/ABEABAIDMQFMA2EB4QMAAf8DWwHNAxoBJAMCAQMDEwEaA0YBfwNbAd4DIQH7Aw8B/gM5AfgD
|
||||
WQG7AzsBYwNcAdkDAAH/A2EB4QMxAUwEAhgAA1kBwwNbAeQDOwFlAwcECQEMAzoBYQNdAdQDRQH0Ay4B
|
||||
+QNbAcoDMgFPAw8BFAMNAREDTQGSAysB+QMrAfkDTQGSAxEBFgMtAUQDWgG/A1IB7wMgAfwDYAHgA0AB
|
||||
cQMXAR8DBAEFAxgBIQNKAYsDWwHsA1kBw/8AEQAEAgMxAUwDYQHhAwAB/wNbAc0DGgEkAxYBHQNVAa8D
|
||||
AAH/AwAB/wMAAf8DAAH/A1UBrwMWAR0DGgEkA1sBzQMAAf8DYQHhAzEBTAQCGAADWQHDA1sB5AM7AWUD
|
||||
BwEJA0kBhgMhAfsDAAH/AwAB/wNZAcEDHQEpCAADCwEOA00BkgMrAfkDKwH5A00BkgM5BF4B4gMAAf8D
|
||||
AAH/A1gB7QNKAYsDGAEgCAEDGAEhA0oBiwNbAewDWQHD/wARAAQCAzEBTANhAeEDAAH/A1wB2QM7AWMD
|
||||
WQG7Az0B9wMgAfwDRQH0A1wB2QNGAX4DEwEaAwIBAwMaASQDWwHNAwAB/wNhAeEDMQFMBAIYAANZAcMD
|
||||
XAHrA1ABmgNVAbEDTQHzAyEB+wNfAeUDSwGPAxsBJQMDAQQIAAMLAQ4DTQGSAysB+QMhAfsDVgGzA0wB
|
||||
jgNcAesDDwH+A1AB8ANXAbkDLgFHAwYBCAQABAEDGAEhA0oBiwNbAewDWQHD/wARAAQCAzEBTANhAeED
|
||||
AAH/Ay4B+QNcAecDQwH1A1QB8QNbAdMDUQGkAzgBWwMTARkEAgQAAxoBJANbAc0DAAH/A2EB4QMxAUwE
|
||||
AhgAA1kBwwMgAfwDUAHwA0MB9QMfAf0DXgHXA0YBfgMaASQDAgEDDAADCwEOA00BkgMrAfkDDwH+A00B
|
||||
8wNWAe4DPQH3A10B4wNOAZgDJwE5AwgBCgQBBAAEAQMYASEDSgGLA1sB7ANZAcP/ABEABAIDMQFMA2EB
|
||||
4QMAAf8DAAH/Ax4B/QNhAeEDUgGjAzsBYwMhAS8DCgENBAIIAAMaASQDWwHNAwAB/wNhAeEDMQFMBAIY
|
||||
AANZAcMDAAH/AwAB/wMAAf8DXQHoA0oBiwMWAR0DBAEFEAADCwEOA00BkgMrAfkDAAH/AwAB/wM5AfYD
|
||||
XAHLA0QBeQMeASoDBgEHDAAEAQMYASEDSgGLA1sB7ANZAcP/ABEABAIDMQFMA2EB4QMAAf8DLgH5A2AB
|
||||
2wNFAXwDIQEwAwwBEAMDAQQQAAMaASMDXAHIAx8B/QNhAeEDMQFMBAIYAANZAcMDAAH/AyEB/ANcAesD
|
||||
TgGXAyMBMwQCFAADCwEOA00BkgMrAfkDDwH+A1AB8gNYAboDNwFaAxEBFgMCAQMUAAMXAR8DSQGGA1wB
|
||||
6wNZAcP/ABEABAIDLQFFA14B0gNQAfIDXQHJAzIBTwMQARUDAgEDGAADEwEaA1ABnwNbAeQDWwHQAy0B
|
||||
RQQCGAADVQG0A1cB7gNfAdoDRAF4AxgBIAMDAQQYAAMKAQ0DSQGGA10B6ANaAekDUAGfAyQBNAMJAQsE
|
||||
ARgAAwsBDgM7AWUDWwHTA1YBsv8AFQADAwEEAycBOgM/AW0DFAEbKAADDwETAzEBTgMdASkDAgEDHAAD
|
||||
DgESAzEBTQMjATMDBAEFJAADBgEHAygBPAMoATwDBgEHJAAEAQMHAQkDCwEOAwIBA/8ABQADAgEDAw8B
|
||||
FANJAYgDXwHlA10B6ANdAegDXQHoA10B6ANdAegDXQHoA10B6ANdAegDXQHoA10B6ANdAegDXQHoA10B
|
||||
6ANdAegDXQHoA10B6ANdAegDXQHoA1sB5ANGAX4DBQEGBAEoAAMCAQMDDQERAx8BLAMtAUYDVwG5A10B
|
||||
6ANdAegDXQHoA10B6ANdAegDXQHoA18B4wNZAb4DMgFPAw8BEwMCAQMwAAMTARkDRgF+A10B6ANdAegD
|
||||
WQHBAzkBXgMmATgDDwEUBAJcAAM5AV8DXgHiA1wB5wNdAegDXQHoA10B6ANdAegDXQHcAzwBZgMGAQgD
|
||||
BgEIAzwBZgNdAdwDXQHoA10B6ANdAegDXQHoA10B3ANQAZ0DJQE2IAADDAEQAzwBZgNdAc8DHgH9AyQB
|
||||
+gNSAe8DWAHtA1gB7QNYAe0DWAHtA1gB7QNYAe0DWAHtA1gB7QNYAe0DWAHtA1gB7QNYAe0DWAHtA1gB
|
||||
7QNSAe8DJAH6AyAB/ANWAbUDKwFCAwgBCiQABAIDGAEhA0ABcANaAb8DXQHfA1oB7QNSAe8DWwHsA14B
|
||||
5gNfAeUDWAHqA1gB7QNQAfADOgH2A10B0QNDAXcDHgErAwYBBywAAz0BaQNbAd4DAQH/Ay4B+QNcAewD
|
||||
WwHkA14B1wNEAXsDHgEqAwYBCFgAAz8BbAMfAf0DOQH4A1IB7wNYAe0DVgHuAyQB+gM5AfYDTwGbAx4B
|
||||
KwMgAS0DUAGdAzkB9gMkAfoDVgHuA1gB7QNSAe8DPQH3A1AB8gM7AWUgAAMTARkDUAGcAz0B9wM6AfYD
|
||||
XQHMA0sBjQNGAYADRgGAA0YBgANGAYADRgGAA0YBgANGAYADRgGAA0YBgANGAYADRgGAA0YBgANGAYAD
|
||||
RgGAA0sBjwNcAdkDHgH9A1QB8QNOAZQDEgEYJAADBQEGAzEBTgNbAdADUAHyA14B4gNVAa4DSgGKA0YB
|
||||
fwNDAXcDQwF2A0UBfANGAYADTwGbA14B3QNdAegDXwHaA04BlgMoATwDCQEMKAADRwGDAzkB+AMgAfwD
|
||||
WwHTA1MBpgNcAdkDLgH5A2AB4ANQAZoDLQFEAwsBDlQAAz8BbAMgAfwDWwHTA0wBjgNGAYADSgGKA10B
|
||||
3AMfAf0DXgHdAzoBYAM6AWIDXQHfAx4B/QNdAdwDSgGKA0YBgANMAY4DWwHTAyAB/AM/AWwgAAMUARsD
|
||||
UgGlAx0B/QNbAdADPwFsAxgBIQMOARIDDgESAw4BEgMOARIDDgESAw4BEgMOARIDDgESAw4BEgMOARID
|
||||
DgESAw4BEgMOARIDDgESAyABLgNXAbkDHgH9Ax0B/QNSAaUDFAEbJAADBQEGAzEBTQNaAccDXAHIA0AB
|
||||
bwMlATcDEwEaAw4BEgMNAREDDAEQAw4BEgMOARIDHQEoAzoBYANLAY8DWwHYA14B5gNWAbYDMQFNAwYB
|
||||
CCQAA0kBhgMhAfsDLgH5A1UBrgMqAUADPwFtA10B0QNDAfUDVgHuA1gBtwM2AVcDFgEdAwwBEAQCSAAD
|
||||
PwFsAy4B+QNVAa4DIAEtAw4BEgMbASUDWQG+AwAB/wNYAe0DPwFtAz8BbQNYAe0DAAH/A1kBvgMbASUD
|
||||
DgESAyABLQNVAa4DLgH5Az8BbCAAAxQBGwNSAaUDHQH9A1YBtgMoATwDCAEKOAADFgEeA1YBswMeAf0D
|
||||
HQH9A1IBpQMUARskAAQBAw8BFAMjATMDIQEwAwsBDgMDAQQEARQABAIDBwEJAx4BKgNLAYwDXQHfAz0B
|
||||
9wNTAakDKAE7JAADSQGGAyEB+wMuAfkDUwGnAxYBHgMMARADKgFAA1gBugNOAfMDRgH0A2AB2wNXAbwD
|
||||
QgF0AxMBGQMCAQNEAAM/AWwDLgH5A1MBpwMVARwEAAMPARQDVwG5AwAB/wNYAe0DPwFtAz8BbQNYAe0D
|
||||
AAH/A1cBuQMPARQEAAMVARwDUwGnAy4B+QM/AWwgAAMUARsDUgGlAx0B/QNVAbQDJgE4AwcBCTgAAxYB
|
||||
HgNWAbMDHgH9Ax0B/QNSAaUDFAEbYAADFQEcA1MBpwMuAfkDIQH7A0kBhiQAA0kBhgMhAfsDLgH5A1MB
|
||||
pwMVARwIAAMPARMDQwF2A1wB2QMfAf0DAAH/AwAB/wNVAa8DFgEdRAADPwFsAy4B+QNTAacDFQEcBAAD
|
||||
DwEUA1cBuQMAAf8DWAHtAz8BbQM/AW0DWAHtAwAB/wNXAbkDDwEUBAADFQEcA1MBpwMuAfkDPwFsIAAD
|
||||
FAEbA1IBpQMdAf0DVQG0AyYBOAMHAQk4AAMWAR4DVgGzAx4B/QMdAf0DUgGlAxQBG2AAAwIBAwMeASoD
|
||||
VQG0Az0B9wNSAaADGwElAwUBBhwAA0kBhgMhAfsDLgH5A1MBpwMVARwIAAQCAwkBCwMYASADRAF6A2AB
|
||||
2wM7AfcDPQH3A1kBuwMqAUADDgESAwUBBgQCNAADPwFsAy4B+QNTAacDFQEcBAADDwEUA1cBuQMAAf8D
|
||||
WAHtAz8BbQM/AW0DWAHtAwAB/wNXAbkDDwEUBAADFQEcA1MBpwMuAfkDPwFsIAADFAEbA1IBpQMdAf0D
|
||||
VQG0AyYBOAMHAQk4AAMWAR4DVgGzAx4B/QMdAf0DUgGlAxQBG2QAAw8BEwNAAW8DXQHUA1MB7wNMAZED
|
||||
EgEYHAADSQGGAyEB+wMuAfkDUwGnAxUBHBAABAIDDQERAzYBVwNYAbcDWwHsA1UB8QNdAdEDRAF5AzMB
|
||||
UAMbASYDBgEHMAADPwFsAy4B+QNTAacDFQEcBAADDwEUA1cBuQMAAf8DWAHtAz8BbQM/AW0DWAHtAwAB
|
||||
/wNXAbkDDwEUBAADFQEcA1MBpwMuAfkDPwFsIAADFAEbA1IBpQMdAf0DVQG0AyYBOAMHAQk4AAMWAR4D
|
||||
VgGzAx4B/QMdAf0DUgGlAxQBG2QAAwIBAwMPARMDUQGeAx0B/QNSAaUDFAEbHAADSQGGAyEB+wMuAfkD
|
||||
UwGnAxUBHBgAAwsBDgMtAUQDSwGPA1sBxQNhAeEDYQHhA1sBzQNMAZADKAE7AwkBDCwAAz8BbAMuAfkD
|
||||
UwGnAxUBHAQAAw8BFANXAbkDAAH/A1gB7QM/AW0DPwFtA1gB7QMAAf8DVwG5Aw8BFAQAAxUBHANTAacD
|
||||
LgH5Az8BbCAAAxQBGwNSAaUDHQH9A1UBtAMmATgDBwEJOAADFgEeA1YBswMeAf0DHQH9A1IBpQMUARts
|
||||
AANNAZMDHQH9A1IBpQMUARscAANJAYYDIQH7Ay4B+QNTAacDFQEcHAADBgEIAxkBIgMvAUkDQAFxA10B
|
||||
zANNAfMDWgHpA1YBtgMtAUQsAAM/AWwDLgH5A1MBpwMVARwEAAMPARQDVwG5AwAB/wNYAe0DPwFtAz8B
|
||||
bQNYAe0DAAH/A1cBuQMPARQEAAMVARwDUwGnAy4B+QM/AWwgAAMUARsDUgGlAx0B/QNVAbQDJgE4AwcB
|
||||
CTgAAxYBHgNWAbMDHgH9Ax0B/QNSAaUDFAEbbAADTQGTAx0B/QNSAaUDFAEbHAADSQGGAyEB+wMuAfkD
|
||||
UwGnAxUBHCAABAIDBAEFAwsBDgMwAUwDWQHBAzoB9gMsAfkDQQFyAwYBCCgAAz8BbAMuAfkDUwGnAxUB
|
||||
HAQAAw8BFANXAbkDAAH/A1gB7QM/AW0DPwFtA1gB7QMAAf8DVwG5Aw8BFAQAAxUBHANTAacDLgH5Az8B
|
||||
bCAAAxQBGwNSAaUDHQH9A1UBtAMmATgDBwEJOAADFgEeA1YBswMeAf0DHQH9A1IBpQMUARtsAANNAZMD
|
||||
HQH9A1IBpQMUARscAANJAYYDIQH7Ay4B+QNTAacDFQEcMAADFQEcA1MBpwMuAfkDWwHTAzoBYSgAAz8B
|
||||
bAMuAfkDUwGnAxUBHAQAAw8BFANXAbkDAAH/A1gB7QM/AW0DPwFtA1gB7QMAAf8DVwG5Aw8BFAQAAxUB
|
||||
HANTAacDLgH5Az8BbCAAAxQBGwNSAaUDHQH9A1UBtAMmATgDBwEJOAADFgEeA1YBswMeAf0DHQH9A1IB
|
||||
pQMUARsQAAQBAwQBBQMLAQ4DDwETAw8BEwMPARMDDwETAw8BEwMNAREDCQEMAwMBBAQBLAADTQGTAx0B
|
||||
/QNSAaUDFAEbHAADSQGGAyEB+wMuAfkDUwGnAxUBHCAABAEDAgEDAwsBDgMwAUwDWQHBAzoB9gMsAfkD
|
||||
QQFyAwYBCCgAAz8BbAMuAfkDUwGnAxUBHAQAAw8BFANXAbkDAAH/A1gB7QM/AW0DPwFtA1gB7QMAAf8D
|
||||
VwG5Aw8BFAQAAxUBHANTAacDLgH5Az8BbCAAAxQBGwNSAaUDHQH9A1UBtAMmATgDBwEJOAADFgEeA1YB
|
||||
swMeAf0DHQH9A1IBpQMUARsQAAMGAQgDJAE1Az4BawNGAX0DRgF+A0YBfgNGAX4DRgF+A0QBewM9AWkD
|
||||
JAE0AwkBDCwAA00BkwMdAf0DUgGlAxQBGxwAA0kBhgMhAfsDLgH5A1MBpwMVARwcAAMGAQgDEgEXAyMB
|
||||
MwM/AW4DWwHNAzoB9gNYAeoDVgG2Ay0BRCwAAz8BbAMuAfkDUwGnAxUBHAQAAw8BFANXAbkDAAH/A1gB
|
||||
7QM/AW0DPwFtA1gB7QMAAf8DVwG5Aw8BFAQAAxUBHANTAacDLgH5Az8BbCAAAxQBGwNSAaUDHQH9A1UB
|
||||
tAMmATgDBwEJOAADFgEeA1YBswMeAf0DHQH9A1IBpQMUARsQAAMQARUDRgF/A1wB2QNYAeoDXgHmA2EB
|
||||
4QNgAeADXgHiA1wB5wNhAeEDUAGdAyEBMCQAAwIBAwMPARMDUQGeAx0B/QNSAaUDFAEbHAADSQGGAyEB
|
||||
+wMuAfkDUwGnAxUBHBgAAwsBDgMtAUQDRgGBA1MBqQNdAd8DXQHoA18B2gNOAZYDKAE8AwkBDCwAAz8B
|
||||
bAMuAfkDUwGnAxUBHAQAAw8BFANXAbkDAAH/A1gB7QM/AW0DPwFtA1gB7QMAAf8DVwG5Aw8BFAQAAxUB
|
||||
HANTAacDLgH5Az8BbCAAAxQBGwNSAaUDHQH9A1UBtAMmATgDBwEJOAADFgEeA1YBswMeAf0DHQH9A1IB
|
||||
pQMUARsQAAMTARoDTgGYA0IB9QMAAf8DYAHgA1YBtgNZAb4DXgHXAz0B9wMgAfwDWgHHAysBQiQAAw8B
|
||||
EwNAAW8DXQHUA1MB7wNNAZIDEgEYHAADSQGGAyEB+wMuAfkDUwGnAxUBHBAABAIDDQERAzYBVwNYAbcD
|
||||
WAHqA1wB7ANfAdUDSwGPAzoBYAMeASsDBgEHMAADPwFsAy4B+QNTAacDFQEcBAADDwEUA1cBuQMAAf8D
|
||||
WAHtAz8BbQM/AW0DWAHtAwAB/wNXAbkDDwEUBAADFQEcA1MBpwMuAfkDPwFsIAADFAEbA1IBpQMdAf0D
|
||||
VQG0AyYBOAMHAQk4AAMWAR4DVgGzAx4B/QMdAf0DUgGlAxQBGxAAAxQBGwNQAZoDOQH2AwAB/wNRAaQD
|
||||
JAE0A0QBeANeAd0DPQH3A1sB3gM7AWMDDgESIAADAgEDAx4BKgNVAbQDPQH3A1EBoQMbASYDBQEGHAAD
|
||||
SQGGAyEB+wMuAfkDUwGnAxUBHAwABAEDEwEZA0QBegNgAdsDOwH3Az0B9wNZAbsDKwFCAxMBGQMIAQoD
|
||||
AgEDNAADPwFsAy4B+QNTAacDFQEcBAADDwEUA1cBuQMAAf8DWAHtAz8BbQM/AW0DWAHtAwAB/wNXAbkD
|
||||
DwEUBAADFQEcA1MBpwMuAfkDPwFsIAADFAEbA1IBpQMdAf0DVQG0AyYBOAMHAQk4AAMWAR4DVgGzAx4B
|
||||
/QMdAf0DUgGlAxQBGxAAAxQBGwNQAZoDOQH2AwAB/wNOAZUDMQFMA2EB4QMAAf8DWwHNAxoBJCgAAxUB
|
||||
HANTAacDLgH5AyEB+wNJAYYkAANJAYYDIQH7Ay4B+QNTAacDFQEcDAADCwEOA00BkgMrAfkDAAH/AwAB
|
||||
/wNVAa8DFgEdRAADPwFsAy4B+QNTAacDFQEcBAADDwEUA1cBuQMAAf8DWAHtAz8BbQM/AW0DWAHtAwAB
|
||||
/wNXAbkDDwEUBAADFQEcA1MBpwMuAfkDPwFsIAADFAEbA1IBpQMdAf0DVgG2AygBPAMIAQo4AAMWAR4D
|
||||
VgGzAx4B/QMdAf0DUgGlAxQBGxAAAxQBGwNQAZoDOQH2AwAB/wNDAfUDVAHvAyEB/AMRAf4DXQHMAx0B
|
||||
KQQCGAAEAgMGAQcDDwETAzIBTwNZAcADQgH1A10B0QM6AWAkAANJAYYDIQH7Ay4B+QNTAacDFgEeAwwB
|
||||
EAMqAUADVwG5A1oB6QNTAe8DWwHoA10BzwNGAX0DEwEaAwIBA0QAAz8BbAMuAfkDUwGnAxUBHAQAAw8B
|
||||
FANXAbkDAAH/A1gB7QM/AW0DPwFtA1gB7QMAAf8DVwG5Aw8BFAQAAxUBHANTAacDLgH5Az8BbCAAAxQB
|
||||
GwNSAaUDHQH9A1sB0AM/AWwDGAEhAw4BEgMOARIDDgESAw4BEgMOARIDDgESAw4BEgMOARIDDgESAw4B
|
||||
EgMOARIDDgESAw4BEgMOARIDIAEuA1cBuQMeAf0DHQH9A1IBpQMUARsQAAMUARsDUAGaAzkB9gMAAf8D
|
||||
AAH/AyMB+gNCAfUDHgH9A2AB4ANAAXEDHQEoAw4BEgMNAREDCQEMAwgBCgMLBA4BEgMcAScDOAFbA0QB
|
||||
eQNaAccDYQHhA1YBtgM0AVQDDAEPJAADSQGGAyEB+wMuAfkDVQGuAyoBQAM/AW0DXQHRA0MB9QNWAe4D
|
||||
WQG7A0QBeQMqAUADEQEWBAJIAAM/AWwDLgH5A1UBrgMgAS0DDgESAxsBJQNZAb4DAAH/A1gB7QM/AW0D
|
||||
PwFtA1gB7QMAAf8DWQG+AxsBJQMOARIDIAEtA1UBrgMuAfkDPwFsIAADEwEaA1EBpAMfAfwDOQH2A10B
|
||||
zANLAY0DRgGAA0YBgANGAYADRgGAA0YBgANGAYADRgGAA0YBgANGAYADRgGAA0YBgANGAYADRgGAA0YB
|
||||
gANLAY8DXAHZAx4B/QNFAfQDTgGYAxMBGRAAAxIBGANLAYwDWAHtAwAB/wMgAf0DXAHZA1UBsQNdAd8D
|
||||
QwH1A2AB4ANPAZsDRgGAA0QBegM5AV4DMwFQAz0BaANGAX4DUAGaA18B2gNbAeQDXwHaA08BmwMpAT4D
|
||||
CgENKAADSQGGAyEB+wMgAfwDWwHTA1MBpgNcAdkDLgH5A2AB4ANQAZoDLQFFAxABFQMGAQcEAUwAAz8B
|
||||
bAMgAfwDWwHTA0wBjgNGAYADSgGKA10B3AMeAf0DXQHfAzoBYgM6AWIDXQHfAx4B/QNdAdwDSgGKA0YB
|
||||
gANMAY4DWwHTAyAB/AM/AWwgAAMSARcDSgGLA1oB6gMPAf4DJAH6A1IB7wNYAe0DWAHtA1gB7QNYAe0D
|
||||
WAHtA1gB7QNYAe0DWAHtA1gB7QNYAe0DWAHtA1gB7QNYAe0DWAHtA1IB7wMkAfoDHgH9A1cBwgM0AVQD
|
||||
CwEOEAADCAEKAy4BSANVAbEDXgHiA2AB2wNPAZsDLQFEA0MBdwNdAcwDYQHhA1gB6gNYAe0DWgHpA1sB
|
||||
0wNcAcgDXwHaA1wB6wNOAfIDLgH5A14B0gNEAXgDIQEvAwcBCSwAA0IBdQNYAeoDAQH/Ay4B+QNQAfAD
|
||||
XAHsA1wB2QNEAXsDHgEqAwYBCFgAAz8BbAMBAf8DLgH5A1IB7wNYAe0DVgHuAyQB+gM5AfYDUAGdAyAB
|
||||
LQMgAS0DUAGdAzkB9gMkAfoDVgHuA1gB7QNSAe8DLgH5Aw8B/gM/AWwgAAMFAQYDGgEjA00BkwNgAeYD
|
||||
WgHtAzwB9gM9AfcDPQH3Az0B9wM9AfcDPQH3Az0B9wM9AfcDPQH3Az0B9wM9AfcDPQH3Az0B9wM9AfcD
|
||||
PQH3AzwB9gNaAe0DXwHlA0cBgwMKAQ0EAhAABAEDCAEKAx8BLAMuAUgDLQFEAxsBJQMHAQkDDwETAyMB
|
||||
MgMuAUcDVwG5A10B6ANaAekDVwHuA1UB8QNcAewDWgHpA10B6ANbAdADNAFTAw8BEwMCAQMwAAMWAR4D
|
||||
RwGDA1oB7QNVAfEDXwHVA0wBjgMrAUIDDwEUBAJcAAM6AWIDWgHpA1oB7QM8AfYDPQH3AzwB9gNcAewD
|
||||
XQHcAzwBZgMGAQgDBgEIAzwBZgNdAdwDWgHtAzwB9gM9AfcDPAH2A1oB7QNgAeYDOgFiLAAEAQMjATMD
|
||||
TgGXA1QBqwNUAasDVAGrA1QBqwNUAasDVAGrA1QBqwNUAasDVAGrA1QBqwNUAasDVAGrA1QBqwNUAasD
|
||||
TgGXAyMBMwQBTAADCQELAzkBXQNHAYIDJwE6AwQBBUgABAIDIgExAzoBYQMPARRwAAMDAQQDKAE7A04B
|
||||
mANUAasDUQGeAyEBLxQAAwMBBAMlATcDUAGfA1QBqwNOAZgDKAE7AwMBBBgAAUIBTQE+BwABPgMAASgD
|
||||
AAF8AwABMAMAAQEBAAEBBgABAxYAA/8BAAH8AQMB8AE/AQMB8AEPAcAIAAH8AQEB8AE/AQMB8AEHAcAI
|
||||
AAH8AQABMAE/AQAB8AEDAYAIAAH8AQABEAE/AQABcAEAAYAIAAH8AgABPwEAATABAAGACAAB/AIAAT8B
|
||||
AAEwAQABgAgAAfwCAAE/DAAB/AEIAQABPwEICwAB/AEMAQABPwEMAQABIAkAAfwBDwEAAT8BDAEAATAJ
|
||||
AAH8AQ8BgAE/AQ4BAAE4CQAB/AEPAfABPwEPAcABPwkAAfwBDwGAAT8BDgEAATgJAAH8AQ8BAAE/AQwB
|
||||
AAEwCQAB/AEMAQABPwEMAQABIAkAAfwBCAEAAT8BCAsAAfwCAAE/DAAB/AIAAT8BAAEwCgAB/AIAAT8B
|
||||
AAEwAQABgAgAAfwBAAEQAT8BAAFwAQABgAgAAfwBAAEwAT8BAAHwAQMBgAgAAfwBAAHwAT8BAQHwAQcB
|
||||
wAgAAfwBAwHwAT8BAwHwAQ8BwAgAAf4BHwH4AX8BDwH4AX8BwAgAAeACAAEHAf4BAAEBAf8B4AEPAv8B
|
||||
4AEAAQEB8AHgAgABBwH8AgAB/wHgAQcC/wHgAQABAQHwAeACAAEHAfwCAAF/AeABAwL/AeABAAEBAfAB
|
||||
4AIAAQcB/AIAAT8B4AEAAX8B/wHgAQABAQHwAeABfwH+AQcB/AEHAcABPwHgAQABPwH/AeEBAAEhAfAB
|
||||
4AF/Af4BBwL/AfgBPwHgAcABPwH/AeEBAAEhAfAB4AF/Af4BBwL/AfgBDwHgAcABAwH/AeEBAAEhAfAB
|
||||
4AF/Af4BBwL/AfwBDwHgAfABAQH/AeEBAAEhAfAB4AF/Af4BBwL/AfwBDwHgAfwBAAH/AeEBAAEhAfAB
|
||||
4AF/Af4BBwP/AQ8B4AH+AQAB/wHhAQABIQHwAeABfwH+AQcD/wEPAeAB/wEAAX8B4QEAASEB8AHgAX8B
|
||||
/gEHA/8BDwHgAf8B8AF/AeEBAAEhAfAB4AF/Af4BBwGAAQcB/wEPAeAB/wEAAX8B4QEAASEB8AHgAX8B
|
||||
/gEHAYABBwH/AQ8B4AH+AQAB/wHhAQABIQHwAeABfwH+AQcBgAEHAfwBDwHgAfwBAAH/AeEBAAEhAfAB
|
||||
4AF/Af4BBwGAAQcB/AEPAeAB8AEBAf8B4QEAASEB8AHgAX8B/gEHAYABBwH4AQ8C4AEDAf8B4QEAASEB
|
||||
8AHgAX8B/gEHAYABHwH4AT8C4AE/Af8B4QEAASEB8AHgAX8B/gEHAYABDwHAAT8B4AEAAT8B/wHhAQAB
|
||||
IQHwAeACAAEHAYACAAE/AeABAAF/Af8B4AEAAQEB8AHgAgABBwGAAgABfwHgAQAC/wHgAQABAQHwAeAC
|
||||
AAEHAYACAAH/AeABBwL/AeABAAEBAfAB4AIAAQcBgAEAAQEB/wHgAQ8C/wHgAQABAQHwAfwCAAE/Af8B
|
||||
+AE/Af8B8AP/AfABPgEDAfAL
|
||||
</value>
|
||||
</data>
|
||||
<metadata name="toolTip.TrayLocation" type="System.Drawing.Point, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a">
|
||||
<value>165, 17</value>
|
||||
</metadata>
|
||||
</root>
|
||||
@@ -1,6 +1,6 @@
|
||||
namespace SpineViewer.Dialogs
|
||||
{
|
||||
partial class ExportPreviewDialog
|
||||
partial class ExportDialog
|
||||
{
|
||||
/// <summary>
|
||||
/// Required designer variable.
|
||||
@@ -28,18 +28,13 @@
|
||||
/// </summary>
|
||||
private void InitializeComponent()
|
||||
{
|
||||
System.ComponentModel.ComponentResourceManager resources = new System.ComponentModel.ComponentResourceManager(typeof(ExportPreviewDialog));
|
||||
System.ComponentModel.ComponentResourceManager resources = new System.ComponentModel.ComponentResourceManager(typeof(ExportDialog));
|
||||
panel1 = new Panel();
|
||||
tableLayoutPanel1 = new TableLayoutPanel();
|
||||
propertyGrid = new PropertyGrid();
|
||||
label4 = new Label();
|
||||
label1 = new Label();
|
||||
textBox_OutputDir = new TextBox();
|
||||
button_SelectOutputDir = new Button();
|
||||
propertyGrid_ExportArgs = new PropertyGrid();
|
||||
tableLayoutPanel2 = new TableLayoutPanel();
|
||||
button_Ok = new Button();
|
||||
button_Cancel = new Button();
|
||||
folderBrowserDialog = new FolderBrowserDialog();
|
||||
panel1.SuspendLayout();
|
||||
tableLayoutPanel1.SuspendLayout();
|
||||
tableLayoutPanel2.SuspendLayout();
|
||||
@@ -52,112 +47,59 @@
|
||||
panel1.Location = new Point(0, 0);
|
||||
panel1.Name = "panel1";
|
||||
panel1.Padding = new Padding(50, 15, 50, 10);
|
||||
panel1.Size = new Size(914, 482);
|
||||
panel1.Size = new Size(710, 698);
|
||||
panel1.TabIndex = 2;
|
||||
//
|
||||
// tableLayoutPanel1
|
||||
//
|
||||
tableLayoutPanel1.AutoSize = true;
|
||||
tableLayoutPanel1.ColumnCount = 4;
|
||||
tableLayoutPanel1.ColumnStyles.Add(new ColumnStyle());
|
||||
tableLayoutPanel1.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 50F));
|
||||
tableLayoutPanel1.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 50F));
|
||||
tableLayoutPanel1.ColumnStyles.Add(new ColumnStyle());
|
||||
tableLayoutPanel1.Controls.Add(propertyGrid, 0, 2);
|
||||
tableLayoutPanel1.Controls.Add(label4, 0, 0);
|
||||
tableLayoutPanel1.Controls.Add(label1, 0, 1);
|
||||
tableLayoutPanel1.Controls.Add(textBox_OutputDir, 1, 1);
|
||||
tableLayoutPanel1.Controls.Add(button_SelectOutputDir, 3, 1);
|
||||
tableLayoutPanel1.Controls.Add(tableLayoutPanel2, 0, 3);
|
||||
tableLayoutPanel1.ColumnCount = 1;
|
||||
tableLayoutPanel1.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 100F));
|
||||
tableLayoutPanel1.Controls.Add(propertyGrid_ExportArgs, 0, 0);
|
||||
tableLayoutPanel1.Controls.Add(tableLayoutPanel2, 0, 1);
|
||||
tableLayoutPanel1.Dock = DockStyle.Fill;
|
||||
tableLayoutPanel1.Location = new Point(50, 15);
|
||||
tableLayoutPanel1.Name = "tableLayoutPanel1";
|
||||
tableLayoutPanel1.RowCount = 4;
|
||||
tableLayoutPanel1.RowStyles.Add(new RowStyle());
|
||||
tableLayoutPanel1.RowStyles.Add(new RowStyle());
|
||||
tableLayoutPanel1.RowCount = 2;
|
||||
tableLayoutPanel1.RowStyles.Add(new RowStyle(SizeType.Percent, 100F));
|
||||
tableLayoutPanel1.RowStyles.Add(new RowStyle());
|
||||
tableLayoutPanel1.Size = new Size(814, 457);
|
||||
tableLayoutPanel1.RowStyles.Add(new RowStyle(SizeType.Absolute, 20F));
|
||||
tableLayoutPanel1.RowStyles.Add(new RowStyle(SizeType.Absolute, 20F));
|
||||
tableLayoutPanel1.Size = new Size(610, 673);
|
||||
tableLayoutPanel1.TabIndex = 0;
|
||||
//
|
||||
// propertyGrid
|
||||
// propertyGrid_ExportArgs
|
||||
//
|
||||
tableLayoutPanel1.SetColumnSpan(propertyGrid, 4);
|
||||
propertyGrid.Dock = DockStyle.Fill;
|
||||
propertyGrid.HelpVisible = false;
|
||||
propertyGrid.Location = new Point(3, 97);
|
||||
propertyGrid.Name = "propertyGrid";
|
||||
propertyGrid.Size = new Size(808, 284);
|
||||
propertyGrid.TabIndex = 1;
|
||||
propertyGrid.ToolbarVisible = false;
|
||||
//
|
||||
// label4
|
||||
//
|
||||
label4.AutoSize = true;
|
||||
tableLayoutPanel1.SetColumnSpan(label4, 4);
|
||||
label4.Dock = DockStyle.Fill;
|
||||
label4.Location = new Point(15, 15);
|
||||
label4.Margin = new Padding(15);
|
||||
label4.Name = "label4";
|
||||
label4.Size = new Size(784, 24);
|
||||
label4.TabIndex = 11;
|
||||
label4.Text = "说明:输出文件夹为可选项,留空则将预览图输出到每个骨骼文件所在目录";
|
||||
label4.TextAlign = ContentAlignment.MiddleCenter;
|
||||
//
|
||||
// label1
|
||||
//
|
||||
label1.Anchor = AnchorStyles.Right;
|
||||
label1.AutoSize = true;
|
||||
label1.Location = new Point(3, 62);
|
||||
label1.Name = "label1";
|
||||
label1.Size = new Size(104, 24);
|
||||
label1.TabIndex = 0;
|
||||
label1.Text = "输出文件夹:";
|
||||
//
|
||||
// textBox_OutputDir
|
||||
//
|
||||
tableLayoutPanel1.SetColumnSpan(textBox_OutputDir, 2);
|
||||
textBox_OutputDir.Dock = DockStyle.Fill;
|
||||
textBox_OutputDir.Location = new Point(113, 57);
|
||||
textBox_OutputDir.Name = "textBox_OutputDir";
|
||||
textBox_OutputDir.Size = new Size(660, 30);
|
||||
textBox_OutputDir.TabIndex = 3;
|
||||
//
|
||||
// button_SelectOutputDir
|
||||
//
|
||||
button_SelectOutputDir.AutoSize = true;
|
||||
button_SelectOutputDir.AutoSizeMode = AutoSizeMode.GrowAndShrink;
|
||||
button_SelectOutputDir.Location = new Point(779, 57);
|
||||
button_SelectOutputDir.Name = "button_SelectOutputDir";
|
||||
button_SelectOutputDir.Size = new Size(32, 34);
|
||||
button_SelectOutputDir.TabIndex = 5;
|
||||
button_SelectOutputDir.Text = "...";
|
||||
button_SelectOutputDir.UseVisualStyleBackColor = true;
|
||||
button_SelectOutputDir.Click += button_SelectOutputDir_Click;
|
||||
propertyGrid_ExportArgs.Dock = DockStyle.Fill;
|
||||
propertyGrid_ExportArgs.Location = new Point(3, 3);
|
||||
propertyGrid_ExportArgs.Name = "propertyGrid_ExportArgs";
|
||||
propertyGrid_ExportArgs.PropertySort = PropertySort.Categorized;
|
||||
propertyGrid_ExportArgs.Size = new Size(604, 594);
|
||||
propertyGrid_ExportArgs.TabIndex = 1;
|
||||
propertyGrid_ExportArgs.ToolbarVisible = false;
|
||||
//
|
||||
// tableLayoutPanel2
|
||||
//
|
||||
tableLayoutPanel2.AutoSize = true;
|
||||
tableLayoutPanel2.AutoSizeMode = AutoSizeMode.GrowAndShrink;
|
||||
tableLayoutPanel2.ColumnCount = 2;
|
||||
tableLayoutPanel1.SetColumnSpan(tableLayoutPanel2, 4);
|
||||
tableLayoutPanel2.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 50F));
|
||||
tableLayoutPanel2.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 50F));
|
||||
tableLayoutPanel2.Controls.Add(button_Ok, 0, 0);
|
||||
tableLayoutPanel2.Controls.Add(button_Cancel, 1, 0);
|
||||
tableLayoutPanel2.Dock = DockStyle.Bottom;
|
||||
tableLayoutPanel2.Location = new Point(3, 414);
|
||||
tableLayoutPanel2.Location = new Point(3, 630);
|
||||
tableLayoutPanel2.Margin = new Padding(3, 30, 3, 3);
|
||||
tableLayoutPanel2.Name = "tableLayoutPanel2";
|
||||
tableLayoutPanel2.RowCount = 1;
|
||||
tableLayoutPanel2.RowStyles.Add(new RowStyle());
|
||||
tableLayoutPanel2.Size = new Size(808, 40);
|
||||
tableLayoutPanel2.Size = new Size(604, 40);
|
||||
tableLayoutPanel2.TabIndex = 10;
|
||||
//
|
||||
// button_Ok
|
||||
//
|
||||
button_Ok.Anchor = AnchorStyles.Bottom | AnchorStyles.Right;
|
||||
button_Ok.Location = new Point(262, 3);
|
||||
button_Ok.Location = new Point(160, 3);
|
||||
button_Ok.Margin = new Padding(3, 3, 30, 3);
|
||||
button_Ok.Name = "button_Ok";
|
||||
button_Ok.Size = new Size(112, 34);
|
||||
@@ -169,7 +111,7 @@
|
||||
// button_Cancel
|
||||
//
|
||||
button_Cancel.Anchor = AnchorStyles.Bottom | AnchorStyles.Left;
|
||||
button_Cancel.Location = new Point(434, 3);
|
||||
button_Cancel.Location = new Point(332, 3);
|
||||
button_Cancel.Margin = new Padding(30, 3, 3, 3);
|
||||
button_Cancel.Name = "button_Cancel";
|
||||
button_Cancel.Size = new Size(112, 34);
|
||||
@@ -178,26 +120,22 @@
|
||||
button_Cancel.UseVisualStyleBackColor = true;
|
||||
button_Cancel.Click += button_Cancel_Click;
|
||||
//
|
||||
// folderBrowserDialog
|
||||
//
|
||||
folderBrowserDialog.AddToRecent = false;
|
||||
//
|
||||
// ExportPreviewDialog
|
||||
// ExportDialog
|
||||
//
|
||||
AcceptButton = button_Ok;
|
||||
AutoScaleDimensions = new SizeF(11F, 24F);
|
||||
AutoScaleMode = AutoScaleMode.Font;
|
||||
CancelButton = button_Cancel;
|
||||
ClientSize = new Size(914, 482);
|
||||
ClientSize = new Size(710, 698);
|
||||
Controls.Add(panel1);
|
||||
FormBorderStyle = FormBorderStyle.FixedDialog;
|
||||
Icon = (Icon)resources.GetObject("$this.Icon");
|
||||
MaximizeBox = false;
|
||||
MinimizeBox = false;
|
||||
Name = "ExportPreviewDialog";
|
||||
Name = "ExportDialog";
|
||||
ShowInTaskbar = false;
|
||||
StartPosition = FormStartPosition.CenterScreen;
|
||||
Text = "导出预览图";
|
||||
Text = "导出参数";
|
||||
panel1.ResumeLayout(false);
|
||||
panel1.PerformLayout();
|
||||
tableLayoutPanel1.ResumeLayout(false);
|
||||
@@ -210,14 +148,9 @@
|
||||
|
||||
private Panel panel1;
|
||||
private TableLayoutPanel tableLayoutPanel1;
|
||||
private Label label4;
|
||||
private Label label1;
|
||||
private TextBox textBox_OutputDir;
|
||||
private Button button_SelectOutputDir;
|
||||
private TableLayoutPanel tableLayoutPanel2;
|
||||
private Button button_Ok;
|
||||
private Button button_Cancel;
|
||||
private FolderBrowserDialog folderBrowserDialog;
|
||||
private PropertyGrid propertyGrid;
|
||||
private PropertyGrid propertyGrid_ExportArgs;
|
||||
}
|
||||
}
|
||||
92
SpineViewer/Dialogs/ExportDialog.cs
Normal file
92
SpineViewer/Dialogs/ExportDialog.cs
Normal file
@@ -0,0 +1,92 @@
|
||||
using SpineViewer.Exporter;
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using System.Data;
|
||||
using System.Diagnostics;
|
||||
using System.Drawing;
|
||||
using System.Drawing.Design;
|
||||
using System.Drawing.Imaging;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows.Forms;
|
||||
|
||||
namespace SpineViewer.Dialogs
|
||||
{
|
||||
public partial class ExportDialog: Form
|
||||
{
|
||||
/// <summary>
|
||||
/// 要绑定的导出参数
|
||||
/// </summary>
|
||||
public required ExportArgs ExportArgs
|
||||
{
|
||||
get => propertyGrid_ExportArgs.SelectedObject as ExportArgs;
|
||||
init
|
||||
{
|
||||
propertyGrid_ExportArgs.SelectedObject = value;
|
||||
|
||||
#region XXX: 通过反射默认高亮指定的项
|
||||
var categories = propertyGrid_ExportArgs.SelectedGridItem?.Parent?.Parent?.GridItems;
|
||||
if (categories is null) return;
|
||||
|
||||
foreach (var category in categories)
|
||||
{
|
||||
// 查找 "导出" 分组
|
||||
if (category == null) continue;
|
||||
PropertyInfo? labelProp = category.GetType().GetProperty("Label", BindingFlags.Instance | BindingFlags.Public);
|
||||
if (labelProp == null) continue;
|
||||
string? label = labelProp.GetValue(category) as string;
|
||||
if (label != "导出") continue;
|
||||
|
||||
// 获取该分组下的所有属性项
|
||||
PropertyInfo? gridItemsProp = category.GetType().GetProperty("GridItems", BindingFlags.Instance | BindingFlags.Public);
|
||||
if (gridItemsProp == null) continue;
|
||||
var gridItemsObj = gridItemsProp.GetValue(category);
|
||||
if (gridItemsObj is not IEnumerable gridItems) continue;
|
||||
|
||||
foreach (object item in gridItems)
|
||||
{
|
||||
if (item == null) continue;
|
||||
PropertyInfo? propDescProp = item.GetType().GetProperty("PropertyDescriptor", BindingFlags.Instance | BindingFlags.Public);
|
||||
if (propDescProp == null) continue;
|
||||
var propDesc = propDescProp.GetValue(item) as PropertyDescriptor;
|
||||
if (propDesc == null) continue;
|
||||
if (propDesc.Name == "OutputDir")
|
||||
{
|
||||
|
||||
if (item is GridItem gridItem)
|
||||
propertyGrid_ExportArgs.SelectedGridItem = gridItem; // 找到后,将此项设为选中项
|
||||
else
|
||||
propertyGrid_ExportArgs.SelectedGridItem = (GridItem)item; // 如果转换失败,则尝试直接赋值
|
||||
}
|
||||
return; // 设置成功后退出
|
||||
}
|
||||
}
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
|
||||
public ExportDialog()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
|
||||
private void button_Ok_Click(object sender, EventArgs e)
|
||||
{
|
||||
if (ExportArgs.Validate() is string error)
|
||||
{
|
||||
MessageBox.Info(error, "参数错误");
|
||||
return;
|
||||
}
|
||||
DialogResult = DialogResult.OK;
|
||||
}
|
||||
|
||||
private void button_Cancel_Click(object sender, EventArgs e)
|
||||
{
|
||||
DialogResult = DialogResult.Cancel;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -117,9 +117,6 @@
|
||||
<resheader name="writer">
|
||||
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
||||
</resheader>
|
||||
<metadata name="folderBrowserDialog.TrayLocation" type="System.Drawing.Point, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a">
|
||||
<value>17, 17</value>
|
||||
</metadata>
|
||||
<assembly alias="System.Drawing" name="System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a" />
|
||||
<data name="$this.Icon" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
|
||||
<value>
|
||||
270
SpineViewer/Dialogs/ExportPngDialog.Designer.cs
generated
270
SpineViewer/Dialogs/ExportPngDialog.Designer.cs
generated
@@ -1,270 +0,0 @@
|
||||
namespace SpineViewer.Dialogs
|
||||
{
|
||||
partial class ExportPngDialog
|
||||
{
|
||||
/// <summary>
|
||||
/// Required designer variable.
|
||||
/// </summary>
|
||||
private System.ComponentModel.IContainer components = null;
|
||||
|
||||
/// <summary>
|
||||
/// Clean up any resources being used.
|
||||
/// </summary>
|
||||
/// <param name="disposing">true if managed resources should be disposed; otherwise, false.</param>
|
||||
protected override void Dispose(bool disposing)
|
||||
{
|
||||
if (disposing && (components != null))
|
||||
{
|
||||
components.Dispose();
|
||||
}
|
||||
base.Dispose(disposing);
|
||||
}
|
||||
|
||||
#region Windows Form Designer generated code
|
||||
|
||||
/// <summary>
|
||||
/// Required method for Designer support - do not modify
|
||||
/// the contents of this method with the code editor.
|
||||
/// </summary>
|
||||
private void InitializeComponent()
|
||||
{
|
||||
System.ComponentModel.ComponentResourceManager resources = new System.ComponentModel.ComponentResourceManager(typeof(ExportPngDialog));
|
||||
panel1 = new Panel();
|
||||
tableLayoutPanel1 = new TableLayoutPanel();
|
||||
label4 = new Label();
|
||||
label1 = new Label();
|
||||
label2 = new Label();
|
||||
label3 = new Label();
|
||||
textBox_OutputDir = new TextBox();
|
||||
button_SelectOutputDir = new Button();
|
||||
tableLayoutPanel2 = new TableLayoutPanel();
|
||||
button_Ok = new Button();
|
||||
button_Cancel = new Button();
|
||||
numericUpDown_Duration = new NumericUpDown();
|
||||
numericUpDown_Fps = new NumericUpDown();
|
||||
folderBrowserDialog = new FolderBrowserDialog();
|
||||
panel1.SuspendLayout();
|
||||
tableLayoutPanel1.SuspendLayout();
|
||||
tableLayoutPanel2.SuspendLayout();
|
||||
((System.ComponentModel.ISupportInitialize)numericUpDown_Duration).BeginInit();
|
||||
((System.ComponentModel.ISupportInitialize)numericUpDown_Fps).BeginInit();
|
||||
SuspendLayout();
|
||||
//
|
||||
// panel1
|
||||
//
|
||||
panel1.Controls.Add(tableLayoutPanel1);
|
||||
panel1.Dock = DockStyle.Fill;
|
||||
panel1.Location = new Point(0, 0);
|
||||
panel1.Name = "panel1";
|
||||
panel1.Padding = new Padding(50, 15, 50, 10);
|
||||
panel1.Size = new Size(919, 276);
|
||||
panel1.TabIndex = 1;
|
||||
//
|
||||
// tableLayoutPanel1
|
||||
//
|
||||
tableLayoutPanel1.AutoSize = true;
|
||||
tableLayoutPanel1.ColumnCount = 4;
|
||||
tableLayoutPanel1.ColumnStyles.Add(new ColumnStyle());
|
||||
tableLayoutPanel1.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 50F));
|
||||
tableLayoutPanel1.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 50F));
|
||||
tableLayoutPanel1.ColumnStyles.Add(new ColumnStyle());
|
||||
tableLayoutPanel1.Controls.Add(label4, 0, 0);
|
||||
tableLayoutPanel1.Controls.Add(label1, 0, 1);
|
||||
tableLayoutPanel1.Controls.Add(label2, 0, 2);
|
||||
tableLayoutPanel1.Controls.Add(label3, 0, 3);
|
||||
tableLayoutPanel1.Controls.Add(textBox_OutputDir, 1, 1);
|
||||
tableLayoutPanel1.Controls.Add(button_SelectOutputDir, 3, 1);
|
||||
tableLayoutPanel1.Controls.Add(tableLayoutPanel2, 0, 4);
|
||||
tableLayoutPanel1.Controls.Add(numericUpDown_Duration, 1, 2);
|
||||
tableLayoutPanel1.Controls.Add(numericUpDown_Fps, 1, 3);
|
||||
tableLayoutPanel1.Dock = DockStyle.Fill;
|
||||
tableLayoutPanel1.Location = new Point(50, 15);
|
||||
tableLayoutPanel1.Name = "tableLayoutPanel1";
|
||||
tableLayoutPanel1.RowCount = 5;
|
||||
tableLayoutPanel1.RowStyles.Add(new RowStyle());
|
||||
tableLayoutPanel1.RowStyles.Add(new RowStyle());
|
||||
tableLayoutPanel1.RowStyles.Add(new RowStyle());
|
||||
tableLayoutPanel1.RowStyles.Add(new RowStyle());
|
||||
tableLayoutPanel1.RowStyles.Add(new RowStyle());
|
||||
tableLayoutPanel1.Size = new Size(819, 251);
|
||||
tableLayoutPanel1.TabIndex = 0;
|
||||
//
|
||||
// label4
|
||||
//
|
||||
label4.AutoSize = true;
|
||||
tableLayoutPanel1.SetColumnSpan(label4, 4);
|
||||
label4.Dock = DockStyle.Fill;
|
||||
label4.Location = new Point(15, 15);
|
||||
label4.Margin = new Padding(15);
|
||||
label4.Name = "label4";
|
||||
label4.Size = new Size(789, 24);
|
||||
label4.TabIndex = 11;
|
||||
label4.Text = "说明:时长不足一帧时仅导出第一帧";
|
||||
label4.TextAlign = ContentAlignment.MiddleCenter;
|
||||
//
|
||||
// label1
|
||||
//
|
||||
label1.Anchor = AnchorStyles.Right;
|
||||
label1.AutoSize = true;
|
||||
label1.Location = new Point(3, 62);
|
||||
label1.Name = "label1";
|
||||
label1.Size = new Size(104, 24);
|
||||
label1.TabIndex = 0;
|
||||
label1.Text = "输出文件夹:";
|
||||
//
|
||||
// label2
|
||||
//
|
||||
label2.Anchor = AnchorStyles.Right;
|
||||
label2.AutoSize = true;
|
||||
label2.Location = new Point(57, 100);
|
||||
label2.Name = "label2";
|
||||
label2.Size = new Size(50, 24);
|
||||
label2.TabIndex = 1;
|
||||
label2.Text = "时长:";
|
||||
//
|
||||
// label3
|
||||
//
|
||||
label3.Anchor = AnchorStyles.Right;
|
||||
label3.AutoSize = true;
|
||||
label3.Location = new Point(57, 136);
|
||||
label3.Name = "label3";
|
||||
label3.Size = new Size(50, 24);
|
||||
label3.TabIndex = 2;
|
||||
label3.Text = "帧率:";
|
||||
//
|
||||
// textBox_OutputDir
|
||||
//
|
||||
tableLayoutPanel1.SetColumnSpan(textBox_OutputDir, 2);
|
||||
textBox_OutputDir.Dock = DockStyle.Fill;
|
||||
textBox_OutputDir.Location = new Point(113, 57);
|
||||
textBox_OutputDir.Name = "textBox_OutputDir";
|
||||
textBox_OutputDir.Size = new Size(664, 30);
|
||||
textBox_OutputDir.TabIndex = 3;
|
||||
//
|
||||
// button_SelectOutputDir
|
||||
//
|
||||
button_SelectOutputDir.AutoSize = true;
|
||||
button_SelectOutputDir.AutoSizeMode = AutoSizeMode.GrowAndShrink;
|
||||
button_SelectOutputDir.Location = new Point(783, 57);
|
||||
button_SelectOutputDir.Name = "button_SelectOutputDir";
|
||||
button_SelectOutputDir.Size = new Size(32, 34);
|
||||
button_SelectOutputDir.TabIndex = 5;
|
||||
button_SelectOutputDir.Text = "...";
|
||||
button_SelectOutputDir.UseVisualStyleBackColor = true;
|
||||
button_SelectOutputDir.Click += button_SelectOutputDir_Click;
|
||||
//
|
||||
// tableLayoutPanel2
|
||||
//
|
||||
tableLayoutPanel2.AutoSize = true;
|
||||
tableLayoutPanel2.AutoSizeMode = AutoSizeMode.GrowAndShrink;
|
||||
tableLayoutPanel2.ColumnCount = 2;
|
||||
tableLayoutPanel1.SetColumnSpan(tableLayoutPanel2, 4);
|
||||
tableLayoutPanel2.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 50F));
|
||||
tableLayoutPanel2.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 50F));
|
||||
tableLayoutPanel2.Controls.Add(button_Ok, 0, 0);
|
||||
tableLayoutPanel2.Controls.Add(button_Cancel, 1, 0);
|
||||
tableLayoutPanel2.Dock = DockStyle.Bottom;
|
||||
tableLayoutPanel2.Location = new Point(3, 208);
|
||||
tableLayoutPanel2.Name = "tableLayoutPanel2";
|
||||
tableLayoutPanel2.RowCount = 1;
|
||||
tableLayoutPanel2.RowStyles.Add(new RowStyle());
|
||||
tableLayoutPanel2.Size = new Size(813, 40);
|
||||
tableLayoutPanel2.TabIndex = 10;
|
||||
//
|
||||
// button_Ok
|
||||
//
|
||||
button_Ok.Anchor = AnchorStyles.Bottom | AnchorStyles.Right;
|
||||
button_Ok.Location = new Point(264, 3);
|
||||
button_Ok.Margin = new Padding(3, 3, 30, 3);
|
||||
button_Ok.Name = "button_Ok";
|
||||
button_Ok.Size = new Size(112, 34);
|
||||
button_Ok.TabIndex = 7;
|
||||
button_Ok.Text = "确认";
|
||||
button_Ok.UseVisualStyleBackColor = true;
|
||||
button_Ok.Click += button_Ok_Click;
|
||||
//
|
||||
// button_Cancel
|
||||
//
|
||||
button_Cancel.Anchor = AnchorStyles.Bottom | AnchorStyles.Left;
|
||||
button_Cancel.Location = new Point(436, 3);
|
||||
button_Cancel.Margin = new Padding(30, 3, 3, 3);
|
||||
button_Cancel.Name = "button_Cancel";
|
||||
button_Cancel.Size = new Size(112, 34);
|
||||
button_Cancel.TabIndex = 8;
|
||||
button_Cancel.Text = "取消";
|
||||
button_Cancel.UseVisualStyleBackColor = true;
|
||||
button_Cancel.Click += button_Cancel_Click;
|
||||
//
|
||||
// numericUpDown_Duration
|
||||
//
|
||||
numericUpDown_Duration.Anchor = AnchorStyles.Left;
|
||||
numericUpDown_Duration.DecimalPlaces = 3;
|
||||
numericUpDown_Duration.Location = new Point(113, 97);
|
||||
numericUpDown_Duration.Maximum = new decimal(new int[] { 300, 0, 0, 0 });
|
||||
numericUpDown_Duration.Name = "numericUpDown_Duration";
|
||||
numericUpDown_Duration.Size = new Size(180, 30);
|
||||
numericUpDown_Duration.TabIndex = 12;
|
||||
numericUpDown_Duration.TextAlign = HorizontalAlignment.Right;
|
||||
numericUpDown_Duration.Value = new decimal(new int[] { 1, 0, 0, 0 });
|
||||
//
|
||||
// numericUpDown_Fps
|
||||
//
|
||||
numericUpDown_Fps.Anchor = AnchorStyles.Left;
|
||||
numericUpDown_Fps.Location = new Point(113, 133);
|
||||
numericUpDown_Fps.Maximum = new decimal(new int[] { 300, 0, 0, 0 });
|
||||
numericUpDown_Fps.Minimum = new decimal(new int[] { 1, 0, 0, 0 });
|
||||
numericUpDown_Fps.Name = "numericUpDown_Fps";
|
||||
numericUpDown_Fps.Size = new Size(180, 30);
|
||||
numericUpDown_Fps.TabIndex = 13;
|
||||
numericUpDown_Fps.TextAlign = HorizontalAlignment.Right;
|
||||
numericUpDown_Fps.Value = new decimal(new int[] { 60, 0, 0, 0 });
|
||||
//
|
||||
// folderBrowserDialog
|
||||
//
|
||||
folderBrowserDialog.AddToRecent = false;
|
||||
//
|
||||
// ExportPngDialog
|
||||
//
|
||||
AcceptButton = button_Ok;
|
||||
AutoScaleDimensions = new SizeF(11F, 24F);
|
||||
AutoScaleMode = AutoScaleMode.Font;
|
||||
CancelButton = button_Cancel;
|
||||
ClientSize = new Size(919, 276);
|
||||
Controls.Add(panel1);
|
||||
FormBorderStyle = FormBorderStyle.FixedDialog;
|
||||
Icon = (Icon)resources.GetObject("$this.Icon");
|
||||
MaximizeBox = false;
|
||||
MinimizeBox = false;
|
||||
Name = "ExportPngDialog";
|
||||
ShowInTaskbar = false;
|
||||
StartPosition = FormStartPosition.CenterScreen;
|
||||
Text = "导出PNG序列";
|
||||
Load += ExportPngDialog_Load;
|
||||
panel1.ResumeLayout(false);
|
||||
panel1.PerformLayout();
|
||||
tableLayoutPanel1.ResumeLayout(false);
|
||||
tableLayoutPanel1.PerformLayout();
|
||||
tableLayoutPanel2.ResumeLayout(false);
|
||||
((System.ComponentModel.ISupportInitialize)numericUpDown_Duration).EndInit();
|
||||
((System.ComponentModel.ISupportInitialize)numericUpDown_Fps).EndInit();
|
||||
ResumeLayout(false);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
private Panel panel1;
|
||||
private TableLayoutPanel tableLayoutPanel1;
|
||||
private Label label4;
|
||||
private Label label1;
|
||||
private Label label2;
|
||||
private Label label3;
|
||||
private TextBox textBox_OutputDir;
|
||||
private Button button_SelectOutputDir;
|
||||
private TableLayoutPanel tableLayoutPanel2;
|
||||
private Button button_Ok;
|
||||
private Button button_Cancel;
|
||||
private NumericUpDown numericUpDown_Duration;
|
||||
private NumericUpDown numericUpDown_Fps;
|
||||
private FolderBrowserDialog folderBrowserDialog;
|
||||
}
|
||||
}
|
||||
@@ -1,78 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using System.Data;
|
||||
using System.Drawing;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows.Forms;
|
||||
|
||||
namespace SpineViewer.Dialogs
|
||||
{
|
||||
public partial class ExportPngDialog : Form
|
||||
{
|
||||
// TODO: 该对话框要合并到统一的导出参数对话框
|
||||
// TODO: 使用结果包装类
|
||||
public string OutputDir { get; private set; }
|
||||
public float Duration { get; private set; }
|
||||
public uint Fps { get; private set; }
|
||||
|
||||
public ExportPngDialog()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
|
||||
private void ExportPngDialog_Load(object sender, EventArgs e)
|
||||
{
|
||||
button_SelectOutputDir_Click(sender, e);
|
||||
}
|
||||
|
||||
private void button_SelectOutputDir_Click(object sender, EventArgs e)
|
||||
{
|
||||
folderBrowserDialog.InitialDirectory = textBox_OutputDir.Text;
|
||||
if (folderBrowserDialog.ShowDialog() == DialogResult.OK)
|
||||
{
|
||||
textBox_OutputDir.Text = Path.GetFullPath(folderBrowserDialog.SelectedPath);
|
||||
}
|
||||
}
|
||||
|
||||
private void button_Ok_Click(object sender, EventArgs e)
|
||||
{
|
||||
var outputDir = textBox_OutputDir.Text;
|
||||
if (File.Exists(outputDir))
|
||||
{
|
||||
MessageBox.Info("输出文件夹无效");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!Directory.Exists(outputDir))
|
||||
{
|
||||
if (MessageBox.Quest($"文件夹 {outputDir} 不存在,是否创建?") != DialogResult.OK)
|
||||
return;
|
||||
|
||||
try
|
||||
{
|
||||
Directory.CreateDirectory(outputDir);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Program.Logger.Error(ex.ToString());
|
||||
MessageBox.Error(ex.ToString(), "文件夹创建失败");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
OutputDir = Path.GetFullPath(outputDir);
|
||||
Duration = (float)numericUpDown_Duration.Value;
|
||||
Fps = (uint)numericUpDown_Fps.Value;
|
||||
|
||||
DialogResult = DialogResult.OK;
|
||||
}
|
||||
|
||||
private void button_Cancel_Click(object sender, EventArgs e)
|
||||
{
|
||||
DialogResult = DialogResult.Cancel;
|
||||
}
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,169 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using System.Data;
|
||||
using System.Drawing;
|
||||
using System.Drawing.Imaging;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows.Forms;
|
||||
|
||||
namespace SpineViewer.Dialogs
|
||||
{
|
||||
public partial class ExportPreviewDialog: Form
|
||||
{
|
||||
/// <summary>
|
||||
/// 对话框结果
|
||||
/// </summary>
|
||||
public readonly ExportPreviewDialogResult Result = new();
|
||||
|
||||
public ExportPreviewDialog()
|
||||
{
|
||||
InitializeComponent();
|
||||
propertyGrid.SelectedObject = Result;
|
||||
}
|
||||
|
||||
private void button_SelectOutputDir_Click(object sender, EventArgs e)
|
||||
{
|
||||
folderBrowserDialog.InitialDirectory = textBox_OutputDir.Text;
|
||||
if (folderBrowserDialog.ShowDialog() != DialogResult.OK)
|
||||
return;
|
||||
textBox_OutputDir.Text = Path.GetFullPath(folderBrowserDialog.SelectedPath);
|
||||
}
|
||||
|
||||
private void button_Ok_Click(object sender, EventArgs e)
|
||||
{
|
||||
var outputDir = textBox_OutputDir.Text;
|
||||
if (string.IsNullOrEmpty(outputDir))
|
||||
{
|
||||
outputDir = null;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (File.Exists(outputDir))
|
||||
{
|
||||
MessageBox.Info("输出文件夹无效");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!Directory.Exists(outputDir))
|
||||
{
|
||||
if (MessageBox.Quest($"文件夹 {outputDir} 不存在,是否创建?") != DialogResult.OK)
|
||||
return;
|
||||
|
||||
try
|
||||
{
|
||||
Directory.CreateDirectory(outputDir);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Program.Logger.Error(ex.ToString());
|
||||
MessageBox.Error(ex.ToString(), "文件夹创建失败");
|
||||
return;
|
||||
}
|
||||
}
|
||||
outputDir = Path.GetFullPath(outputDir);
|
||||
}
|
||||
|
||||
if (outputDir is null && string.IsNullOrEmpty(Result.NameSuffix))
|
||||
{
|
||||
MessageBox.Info("输出文件夹和名称后缀不可同时为空,存在文件覆盖风险");
|
||||
return;
|
||||
}
|
||||
|
||||
Result.OutputDir = outputDir;
|
||||
DialogResult = DialogResult.OK;
|
||||
}
|
||||
|
||||
private void button_Cancel_Click(object sender, EventArgs e)
|
||||
{
|
||||
DialogResult = DialogResult.Cancel;
|
||||
}
|
||||
}
|
||||
|
||||
public class ExportPreviewDialogResult
|
||||
{
|
||||
/// <summary>
|
||||
/// 输出路径
|
||||
/// </summary>
|
||||
[Browsable(false)]
|
||||
public string? OutputDir { get; set; } = null;
|
||||
|
||||
/// <summary>
|
||||
/// 预览图格式
|
||||
/// </summary>
|
||||
[TypeConverter(typeof(ImageFormatConverter))]
|
||||
[Category("图像"), DisplayName("预览图格式")]
|
||||
public ImageFormat ImageFormat
|
||||
{
|
||||
get => imageFormat;
|
||||
set
|
||||
{
|
||||
if (value == ImageFormat.MemoryBmp) value = ImageFormat.Bmp;
|
||||
imageFormat = value;
|
||||
}
|
||||
}
|
||||
private ImageFormat imageFormat = ImageFormat.Png;
|
||||
|
||||
/// <summary>
|
||||
/// 预览图分辨率
|
||||
/// </summary>
|
||||
[TypeConverter(typeof(SizeConverter))]
|
||||
[Category("图像"), DisplayName("分辨率")]
|
||||
public Size Resolution
|
||||
{
|
||||
get => resolution;
|
||||
set
|
||||
{
|
||||
if (value.Width <= 0) value.Width = 128;
|
||||
if (value.Height <= 0) value.Height = 128;
|
||||
resolution = value;
|
||||
}
|
||||
}
|
||||
private Size resolution = new(512, 512);
|
||||
|
||||
/// <summary>
|
||||
/// 四周填充像素值
|
||||
/// </summary>
|
||||
[TypeConverter(typeof(PaddingConverter))]
|
||||
[Category("图像"), DisplayName("四周填充像素值")]
|
||||
public Padding Padding
|
||||
{
|
||||
get => padding;
|
||||
set
|
||||
{
|
||||
if (value.Left <= 0) value.Left = 10;
|
||||
if (value.Right <= 0) value.Right = 10;
|
||||
if (value.Top <= 0) value.Top = 10;
|
||||
if (value.Bottom <= 0) value.Bottom = 10;
|
||||
padding = value;
|
||||
}
|
||||
}
|
||||
private Padding padding = new(1);
|
||||
|
||||
/// <summary>
|
||||
/// DPI
|
||||
/// </summary>
|
||||
[TypeConverter(typeof(SizeFConverter))]
|
||||
[Category("图像"), DisplayName("DPI")]
|
||||
public SizeF DPI
|
||||
{
|
||||
get => dpi;
|
||||
set
|
||||
{
|
||||
if (value.Width <= 0) value.Width = 144;
|
||||
if (value.Height <= 0) value.Height = 144;
|
||||
dpi = value;
|
||||
}
|
||||
}
|
||||
private SizeF dpi = new(144, 144);
|
||||
|
||||
/// <summary>
|
||||
/// 名称后缀
|
||||
/// </summary>
|
||||
[Category("其他"), DisplayName("名称后缀")]
|
||||
public string NameSuffix { get; set; } = "(preview)";
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,64 +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;
|
||||
using static System.Windows.Forms.VisualStyles.VisualStyleElement.StartPanel;
|
||||
|
||||
namespace SpineViewer
|
||||
{
|
||||
/// <summary>
|
||||
/// SFML.Graphics.Image 帧对象包装类
|
||||
/// </summary>
|
||||
public class SFMLImageVideoFrame(SFML.Graphics.Image image) : IVideoFrame, IDisposable
|
||||
{
|
||||
public int Width => (int)image.Size.X;
|
||||
public int Height => (int)image.Size.Y;
|
||||
public string Format => "rgba";
|
||||
public void Serialize(Stream pipe) => pipe.Write(image.Pixels);
|
||||
public async Task SerializeAsync(Stream pipe, CancellationToken token) => await pipe.WriteAsync(image.Pixels, token);
|
||||
public void Dispose() => image.Dispose();
|
||||
|
||||
/// <summary>
|
||||
/// Save the contents of the image to a file
|
||||
/// </summary>
|
||||
/// <param name="filename">Path of the file to save (overwritten if already exist)</param>
|
||||
/// <returns>True if saving was successful</returns>
|
||||
public bool SaveToFile(string filename) => image.SaveToFile(filename);
|
||||
|
||||
/// <summary>
|
||||
/// Save the image to a buffer in memory The format of the image must be specified.
|
||||
/// The supported image formats are bmp, png, tga and jpg. This function fails if
|
||||
/// the image is empty, or if the format was invalid.
|
||||
/// </summary>
|
||||
/// <param name="output">Byte array filled with encoded data</param>
|
||||
/// <param name="format">Encoding format to use</param>
|
||||
/// <returns>True if saving was successful</returns>
|
||||
public bool SaveToMemory(out byte[] output, string format) => image.SaveToMemory(out output, format);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 为帧导出创建的辅助类
|
||||
/// </summary>
|
||||
public static class ExportHelper
|
||||
{
|
||||
public static Bitmap CopyToBitmap(this SFML.Graphics.Texture tex)
|
||||
{
|
||||
using var img = tex.CopyToImage();
|
||||
img.SaveToMemory(out var imgBuffer, "bmp");
|
||||
using var stream = new MemoryStream(imgBuffer);
|
||||
return new Bitmap(stream);
|
||||
}
|
||||
|
||||
public static SFMLImageVideoFrame CopyToFrame(this SFML.Graphics.Texture tex) => new(tex.CopyToImage());
|
||||
|
||||
public static string GetSuffix(this ImageFormat imageFormat)
|
||||
{
|
||||
if (imageFormat == ImageFormat.Icon) return ".ico";
|
||||
else if (imageFormat == ImageFormat.Exif) return ".jpg";
|
||||
else return $".{imageFormat.ToString().ToLower()}";
|
||||
}
|
||||
}
|
||||
}
|
||||
107
SpineViewer/Exporter/ExportArgs.cs
Normal file
107
SpineViewer/Exporter/ExportArgs.cs
Normal file
@@ -0,0 +1,107 @@
|
||||
using SpineViewer.Spine;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using System.Drawing.Design;
|
||||
using System.Drawing.Imaging;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace SpineViewer.Exporter
|
||||
{
|
||||
/// <summary>
|
||||
/// 导出参数基类
|
||||
/// </summary>
|
||||
public abstract class ExportArgs
|
||||
{
|
||||
/// <summary>
|
||||
/// 实现类缓存
|
||||
/// </summary>
|
||||
private static readonly Dictionary<ExportType, Type> ImplementationTypes = [];
|
||||
|
||||
static ExportArgs()
|
||||
{
|
||||
var impTypes = Assembly.GetExecutingAssembly().GetTypes().Where(t => typeof(ExportArgs).IsAssignableFrom(t) && !t.IsAbstract);
|
||||
foreach (var type in impTypes)
|
||||
{
|
||||
var attr = type.GetCustomAttribute<ExportImplementationAttribute>();
|
||||
if (attr is not null)
|
||||
{
|
||||
if (ImplementationTypes.ContainsKey(attr.ExportType))
|
||||
throw new InvalidOperationException($"Multiple implementations found: {attr.ExportType}");
|
||||
ImplementationTypes[attr.ExportType] = type;
|
||||
}
|
||||
}
|
||||
Program.Logger.Debug("Find export args implementations: [{}]", string.Join(", ", ImplementationTypes.Keys));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 创建指定类型导出参数
|
||||
/// </summary>
|
||||
public static ExportArgs New(ExportType exportType, Size resolution, SFML.Graphics.View view, bool renderSelectedOnly)
|
||||
{
|
||||
if (!ImplementationTypes.TryGetValue(exportType, out var type))
|
||||
{
|
||||
throw new NotImplementedException($"Not implemented type: {exportType}");
|
||||
}
|
||||
return (ExportArgs)Activator.CreateInstance(type, resolution, view, renderSelectedOnly);
|
||||
}
|
||||
|
||||
public ExportArgs(Size resolution, SFML.Graphics.View view, bool renderSelectedOnly)
|
||||
{
|
||||
Resolution = resolution;
|
||||
View = view;
|
||||
RenderSelectedOnly = renderSelectedOnly;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 输出文件夹
|
||||
/// </summary>
|
||||
[Editor(typeof(FolderNameEditor), typeof(UITypeEditor))]
|
||||
[Category("导出"), DisplayName("输出文件夹"), Description("逐个导出时可以留空,将逐个导出到模型自身所在目录")]
|
||||
public string? OutputDir { get; set; } = null;
|
||||
|
||||
/// <summary>
|
||||
/// 导出单个
|
||||
/// </summary>
|
||||
[Category("导出"), DisplayName("导出单个"), Description("是否将模型在同一个画面上导出单个文件,否则逐个导出模型")]
|
||||
public bool ExportSingle { get; set; } = false;
|
||||
|
||||
/// <summary>
|
||||
/// 画面分辨率
|
||||
/// </summary>
|
||||
[TypeConverter(typeof(SizeConverter))]
|
||||
[Category("导出"), DisplayName("分辨率"), Description("画面的宽高像素大小,请在预览画面参数面板进行调整")]
|
||||
public Size Resolution { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 渲染视窗
|
||||
/// </summary>
|
||||
[Category("导出"), DisplayName("视图"), Description("画面的视图参数,请在预览画面参数面板进行调整")]
|
||||
public SFML.Graphics.View View { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 是否仅渲染选中
|
||||
/// </summary>
|
||||
[Category("导出"), DisplayName("仅渲染选中"), Description("是否仅导出选中的模型,请在预览画面参数面板进行调整")]
|
||||
public bool RenderSelectedOnly { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 检查参数是否合法并规范化参数值, 否则返回用户错误原因
|
||||
/// </summary>
|
||||
public virtual string? Validate()
|
||||
{
|
||||
if (!string.IsNullOrEmpty(OutputDir) && File.Exists(OutputDir))
|
||||
return "输出文件夹无效";
|
||||
if (!string.IsNullOrEmpty(OutputDir) && !Directory.Exists(OutputDir))
|
||||
return $"文件夹 {OutputDir} 不存在";
|
||||
if (ExportSingle && string.IsNullOrEmpty(OutputDir))
|
||||
return "导出单个时必须提供输出文件夹";
|
||||
|
||||
OutputDir = string.IsNullOrEmpty(OutputDir) ? null : Path.GetFullPath(OutputDir);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
140
SpineViewer/Exporter/ExportHelper.cs
Normal file
140
SpineViewer/Exporter/ExportHelper.cs
Normal file
@@ -0,0 +1,140 @@
|
||||
using FFMpegCore.Pipes;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Drawing.Imaging;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace SpineViewer.Exporter
|
||||
{
|
||||
/// <summary>
|
||||
/// 导出类型
|
||||
/// </summary>
|
||||
public enum ExportType
|
||||
{
|
||||
Frame,
|
||||
FrameSequence,
|
||||
GIF,
|
||||
MKV,
|
||||
MP4,
|
||||
MOV,
|
||||
WebM
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 导出实现类标记
|
||||
/// </summary>
|
||||
[AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = false)]
|
||||
public class ExportImplementationAttribute : Attribute
|
||||
{
|
||||
public ExportType ExportType { get; }
|
||||
|
||||
public ExportImplementationAttribute(ExportType exportType)
|
||||
{
|
||||
ExportType = exportType;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// SFML.Graphics.Image 帧对象包装类
|
||||
/// </summary>
|
||||
public class SFMLImageVideoFrame(SFML.Graphics.Image image) : IVideoFrame, IDisposable
|
||||
{
|
||||
public int Width => (int)image.Size.X;
|
||||
public int Height => (int)image.Size.Y;
|
||||
public string Format => "rgba";
|
||||
public void Serialize(Stream pipe) => pipe.Write(image.Pixels);
|
||||
public async Task SerializeAsync(Stream pipe, CancellationToken token) => await pipe.WriteAsync(image.Pixels, token);
|
||||
public void Dispose() => image.Dispose();
|
||||
|
||||
/// <summary>
|
||||
/// Save the contents of the image to a file
|
||||
/// </summary>
|
||||
/// <param name="filename">Path of the file to save (overwritten if already exist)</param>
|
||||
/// <returns>True if saving was successful</returns>
|
||||
public bool SaveToFile(string filename) => image.SaveToFile(filename);
|
||||
|
||||
/// <summary>
|
||||
/// Save the image to a buffer in memory The format of the image must be specified.
|
||||
/// The supported image formats are bmp, png, tga and jpg. This function fails if
|
||||
/// the image is empty, or if the format was invalid.
|
||||
/// </summary>
|
||||
/// <param name="output">Byte array filled with encoded data</param>
|
||||
/// <param name="format">Encoding format to use</param>
|
||||
/// <returns>True if saving was successful</returns>
|
||||
public bool SaveToMemory(out byte[] output, string format) => image.SaveToMemory(out output, format);
|
||||
|
||||
/// <summary>
|
||||
/// 获取 Winforms Bitmap 对象
|
||||
/// </summary>
|
||||
public Bitmap CopyToBitmap()
|
||||
{
|
||||
image.SaveToMemory(out var imgBuffer, "bmp");
|
||||
using var stream = new MemoryStream(imgBuffer);
|
||||
return new(new Bitmap(stream)); // 必须重复构造一个副本才能摆脱对流的依赖, 否则之后使用会报错
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 为帧导出创建的辅助类
|
||||
/// </summary>
|
||||
public static class ExportHelper
|
||||
{
|
||||
/// <summary>
|
||||
/// 根据 Bitmap 文件格式获取合适的文件后缀
|
||||
/// </summary>
|
||||
public static string GetSuffix(this ImageFormat imageFormat)
|
||||
{
|
||||
if (imageFormat == ImageFormat.Icon) return ".ico";
|
||||
else if (imageFormat == ImageFormat.Exif) return ".jpeg";
|
||||
else return $".{imageFormat.ToString().ToLower()}";
|
||||
}
|
||||
|
||||
#region 包围盒辅助函数
|
||||
|
||||
/// <summary>
|
||||
/// 获取某个包围盒下合适的视图
|
||||
/// </summary>
|
||||
public static SFML.Graphics.View GetView(this RectangleF bounds, Size resolution, Padding padding)
|
||||
=> bounds.GetView((uint)resolution.Width, (uint)resolution.Height, (uint)padding.Left, (uint)padding.Right, (uint)padding.Top, (uint)padding.Bottom);
|
||||
|
||||
/// <summary>
|
||||
/// 获取某个包围盒下合适的视图
|
||||
/// </summary>
|
||||
public static SFML.Graphics.View GetView(this RectangleF bounds, uint width, uint height, Padding padding)
|
||||
=> bounds.GetView(width, height, (uint)padding.Left, (uint)padding.Right, (uint)padding.Top, (uint)padding.Bottom);
|
||||
|
||||
/// <summary>
|
||||
/// 获取某个包围盒下合适的视图
|
||||
/// </summary>
|
||||
public static SFML.Graphics.View GetView(this RectangleF bounds, Size resolution, uint paddingL = 1, uint paddingR = 1, uint paddingT = 1, uint paddingB = 1)
|
||||
=> bounds.GetView((uint)resolution.Width, (uint)resolution.Height, paddingL, paddingR, paddingT, paddingB);
|
||||
|
||||
/// <summary>
|
||||
/// 获取某个包围盒下合适的视图
|
||||
/// </summary>
|
||||
public static SFML.Graphics.View GetView(this RectangleF bounds, uint width, uint height, uint paddingL = 1, uint paddingR = 1, uint paddingT = 1, uint paddingB = 1)
|
||||
{
|
||||
float sizeX = bounds.Width;
|
||||
float sizeY = bounds.Height;
|
||||
float innerW = width - paddingL - paddingR;
|
||||
float innerH = height - paddingT - paddingB;
|
||||
|
||||
float scale = 1;
|
||||
if (sizeY / sizeX < innerH / innerW)
|
||||
scale = sizeX / innerW; // 相同的 X, 视窗 Y 更大
|
||||
else
|
||||
scale = sizeY / innerH; // 相同的 Y, 视窗 X 更大
|
||||
|
||||
var x = bounds.X + bounds.Width / 2 + (paddingL - (float)paddingR) * scale;
|
||||
var y = bounds.Y + bounds.Height / 2 + (paddingT - (float)paddingB) * scale;
|
||||
var viewX = width * scale;
|
||||
var viewY = height * scale;
|
||||
|
||||
return new(new(x, y), new(viewX, -viewY));
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
129
SpineViewer/Exporter/Exporter.cs
Normal file
129
SpineViewer/Exporter/Exporter.cs
Normal file
@@ -0,0 +1,129 @@
|
||||
using SpineViewer.Spine;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using static System.Windows.Forms.VisualStyles.VisualStyleElement.StartPanel;
|
||||
|
||||
namespace SpineViewer.Exporter
|
||||
{
|
||||
/// <summary>
|
||||
/// 导出器基类
|
||||
/// </summary>
|
||||
public abstract class Exporter
|
||||
{
|
||||
/// <summary>
|
||||
/// 实现类缓存
|
||||
/// </summary>
|
||||
private static readonly Dictionary<ExportType, Type> ImplementationTypes = [];
|
||||
|
||||
static Exporter()
|
||||
{
|
||||
var impTypes = Assembly.GetExecutingAssembly().GetTypes().Where(t => typeof(Exporter).IsAssignableFrom(t) && !t.IsAbstract);
|
||||
foreach (var type in impTypes)
|
||||
{
|
||||
var attr = type.GetCustomAttribute<ExportImplementationAttribute>();
|
||||
if (attr is not null)
|
||||
{
|
||||
if (ImplementationTypes.ContainsKey(attr.ExportType))
|
||||
throw new InvalidOperationException($"Multiple implementations found: {attr.ExportType}");
|
||||
ImplementationTypes[attr.ExportType] = type;
|
||||
}
|
||||
}
|
||||
Program.Logger.Debug("Find exporter implementations: [{}]", string.Join(", ", ImplementationTypes.Keys));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 创建指定类型导出参数
|
||||
/// </summary>
|
||||
public static Exporter New(ExportType exportType, ExportArgs exportArgs)
|
||||
{
|
||||
if (!ImplementationTypes.TryGetValue(exportType, out var type))
|
||||
{
|
||||
throw new NotImplementedException($"Not implemented type: {exportType}");
|
||||
}
|
||||
return (Exporter)Activator.CreateInstance(type, exportArgs);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 导出参数
|
||||
/// </summary>
|
||||
public ExportArgs ExportArgs { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 可用于文件名的时间戳字符串
|
||||
/// </summary>
|
||||
protected readonly string timestamp;
|
||||
|
||||
public Exporter(ExportArgs exportArgs)
|
||||
{
|
||||
ExportArgs = exportArgs;
|
||||
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(SFML.Graphics.Color.Transparent);
|
||||
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(SFML.Graphics.Color.Transparent);
|
||||
foreach (var spine in spinesToRender) tex.Draw(spine);
|
||||
tex.Display();
|
||||
return new(tex.Texture.CopyToImage());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 每个模型在同一个画面进行导出
|
||||
/// </summary>
|
||||
protected abstract void ExportSingle(Spine.Spine[] spinesToRender, BackgroundWorker? worker = null);
|
||||
|
||||
/// <summary>
|
||||
/// 每个模型独立导出
|
||||
/// </summary>
|
||||
protected abstract void ExportIndividual(Spine.Spine[] spinesToRender, BackgroundWorker? worker = null);
|
||||
|
||||
/// <summary>
|
||||
/// 执行导出
|
||||
/// </summary>
|
||||
/// <param name="spines">要进行导出的 Spine 列表</param>
|
||||
/// <param name="worker">用来执行该函数的 worker</param>
|
||||
public virtual void Export(Spine.Spine[] spines, BackgroundWorker? worker = null)
|
||||
{
|
||||
var spinesToRender = spines.Where(sp => !ExportArgs.RenderSelectedOnly || sp.IsSelected).Reverse().ToArray();
|
||||
|
||||
if (ExportArgs.ExportSingle) ExportSingle(spinesToRender, worker);
|
||||
else ExportIndividual(spinesToRender, worker);
|
||||
|
||||
Program.LogCurrentMemoryUsage();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using System.Drawing.Imaging;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace SpineViewer.Exporter.Implementations.ExportArgs
|
||||
{
|
||||
/// <summary>
|
||||
/// 单帧画面导出参数
|
||||
/// </summary>
|
||||
[ExportImplementation(ExportType.Frame)]
|
||||
public class FrameExportArgs : SpineViewer.Exporter.ExportArgs
|
||||
{
|
||||
public FrameExportArgs(Size resolution, SFML.Graphics.View view, bool renderSelectedOnly) : base(resolution, view, renderSelectedOnly) { }
|
||||
|
||||
/// <summary>
|
||||
/// 单帧画面格式
|
||||
/// </summary>
|
||||
[TypeConverter(typeof(ImageFormatConverter))]
|
||||
[Category("单帧画面"), DisplayName("图像格式")]
|
||||
public ImageFormat ImageFormat
|
||||
{
|
||||
get => imageFormat;
|
||||
set
|
||||
{
|
||||
if (value == ImageFormat.MemoryBmp) value = ImageFormat.Bmp;
|
||||
imageFormat = value;
|
||||
}
|
||||
}
|
||||
private ImageFormat imageFormat = ImageFormat.Png;
|
||||
|
||||
/// <summary>
|
||||
/// 文件名后缀
|
||||
/// </summary>
|
||||
[Category("单帧画面"), DisplayName("文件名后缀"), Description("与图像格式匹配的文件名后缀")]
|
||||
public string FileSuffix { get => imageFormat.GetSuffix(); }
|
||||
|
||||
/// <summary>
|
||||
/// DPI
|
||||
/// </summary>
|
||||
[TypeConverter(typeof(SizeFConverter))]
|
||||
[Category("单帧画面"), DisplayName("DPI"), Description("导出图像的每英寸像素数,用于调整图像的物理尺寸")]
|
||||
public SizeF DPI
|
||||
{
|
||||
get => dpi;
|
||||
set
|
||||
{
|
||||
if (value.Width <= 0) value.Width = 144;
|
||||
if (value.Height <= 0) value.Height = 144;
|
||||
dpi = value;
|
||||
}
|
||||
}
|
||||
private SizeF dpi = new(144, 144);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using System.Drawing.Imaging;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace SpineViewer.Exporter.Implementations.ExportArgs
|
||||
{
|
||||
/// <summary>
|
||||
/// 帧序列导出参数
|
||||
/// </summary>
|
||||
[ExportImplementation(ExportType.FrameSequence)]
|
||||
public class FrameSequenceExportArgs : VideoExportArgs
|
||||
{
|
||||
public FrameSequenceExportArgs(Size resolution, SFML.Graphics.View view, bool renderSelectedOnly) : base(resolution, view, renderSelectedOnly) { }
|
||||
|
||||
/// <summary>
|
||||
/// 文件名后缀
|
||||
/// </summary>
|
||||
[TypeConverter(typeof(SFMLImageFileSuffixConverter))]
|
||||
[Category("帧序列参数"), DisplayName("文件名后缀"), Description("帧文件的后缀,同时决定帧图像格式")]
|
||||
public string FileSuffix { get; set; } = ".png";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
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)
|
||||
{
|
||||
// GIF 的帧率不能太高, 超过 50 帧反而会变慢
|
||||
FPS = 25;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 调色板最大颜色数量
|
||||
/// </summary>
|
||||
[Category("GIF 参数"), DisplayName("调色板最大颜色数量"), Description("设置调色板使用的最大颜色数量, 越多则色彩保留程度越高")]
|
||||
public uint MaxColors { get => maxColors; set => maxColors = Math.Clamp(value, 2, 256); }
|
||||
private uint maxColors = 256;
|
||||
|
||||
/// <summary>
|
||||
/// 透明度阈值
|
||||
/// </summary>
|
||||
[Category("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}\"";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace SpineViewer.Exporter.Implementations.ExportArgs
|
||||
{
|
||||
/// <summary>
|
||||
/// 视频导出参数基类
|
||||
/// </summary>
|
||||
public abstract class VideoExportArgs : SpineViewer.Exporter.ExportArgs
|
||||
{
|
||||
public VideoExportArgs(Size resolution, SFML.Graphics.View view, bool renderSelectedOnly) : base(resolution, view, renderSelectedOnly) { }
|
||||
|
||||
/// <summary>
|
||||
/// 导出时长
|
||||
/// </summary>
|
||||
[Category("视频参数"), DisplayName("时长"), Description("可以从模型列表查看动画时长, 如果小于 0, 则在逐个导出时每个模型使用各自的当前动画时长")]
|
||||
public float Duration
|
||||
{
|
||||
get => duration;
|
||||
set => duration = value < 0 ? -1 : value;
|
||||
}
|
||||
private float duration = -1;
|
||||
|
||||
/// <summary>
|
||||
/// 帧率
|
||||
/// </summary>
|
||||
[Category("视频参数"), 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
using SpineViewer.Exporter.Implementations.ExportArgs;
|
||||
using SpineViewer.Spine;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace SpineViewer.Exporter.Implementations.Exporter
|
||||
{
|
||||
/// <summary>
|
||||
/// 单帧画面导出器
|
||||
/// </summary>
|
||||
[ExportImplementation(ExportType.Frame)]
|
||||
public class FrameExporter : SpineViewer.Exporter.Exporter
|
||||
{
|
||||
public FrameExporter(FrameExportArgs exportArgs) : base(exportArgs) { }
|
||||
|
||||
protected override void ExportSingle(Spine.Spine[] spinesToRender, BackgroundWorker? worker = null)
|
||||
{
|
||||
var args = (FrameExportArgs)ExportArgs;
|
||||
|
||||
// 导出单个时必定提供输出文件夹
|
||||
var filename = $"frame_{timestamp}{args.FileSuffix}";
|
||||
var savePath = Path.Combine(args.OutputDir, filename);
|
||||
|
||||
worker?.ReportProgress(0, $"已处理 0/1");
|
||||
try
|
||||
{
|
||||
using var frame = GetFrame(spinesToRender);
|
||||
using var img = frame.CopyToBitmap();
|
||||
img.SetResolution(args.DPI.Width, args.DPI.Height);
|
||||
img.Save(savePath, args.ImageFormat);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Program.Logger.Error(ex.ToString());
|
||||
Program.Logger.Error("Failed to save single frame");
|
||||
}
|
||||
worker?.ReportProgress(100, $"已处理 1/1");
|
||||
}
|
||||
|
||||
protected override void ExportIndividual(Spine.Spine[] spinesToRender, BackgroundWorker? worker = null)
|
||||
{
|
||||
var args = (FrameExportArgs)ExportArgs;
|
||||
|
||||
int total = spinesToRender.Length;
|
||||
int success = 0;
|
||||
int error = 0;
|
||||
|
||||
worker?.ReportProgress(0, $"已处理 0/{total}");
|
||||
for (int i = 0; i < total; i++)
|
||||
{
|
||||
var spine = spinesToRender[i];
|
||||
|
||||
// 逐个导出时如果提供了输出文件夹, 则全部导出到输出文件夹, 否则输出到各自的文件夹
|
||||
var filename = $"{spine.Name}_{timestamp}{args.FileSuffix}";
|
||||
var savePath = args.OutputDir is null ? Path.Combine(spine.AssetsDir, filename) : Path.Combine(args.OutputDir, filename);
|
||||
|
||||
try
|
||||
{
|
||||
using var frame = GetFrame(spine);
|
||||
using var img = frame.CopyToBitmap();
|
||||
img.SetResolution(args.DPI.Width, args.DPI.Height);
|
||||
img.Save(savePath, args.ImageFormat);
|
||||
success++;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Program.Logger.Error(ex.ToString());
|
||||
Program.Logger.Error("Failed to save frame {} {}", savePath, spine.SkelPath);
|
||||
error++;
|
||||
}
|
||||
|
||||
worker?.ReportProgress((int)((i + 1) * 100.0) / total, $"已处理 {i + 1}/{total}");
|
||||
}
|
||||
|
||||
if (error > 0)
|
||||
Program.Logger.Warn("Frames save {} successfully, {} failed", success, error);
|
||||
else
|
||||
Program.Logger.Info("{} frames saved successfully", success);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
using SpineViewer.Exporter.Implementations.ExportArgs;
|
||||
using SpineViewer.Spine;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace SpineViewer.Exporter.Implementations.Exporter
|
||||
{
|
||||
/// <summary>
|
||||
/// 帧序列导出器
|
||||
/// </summary>
|
||||
[ExportImplementation(ExportType.FrameSequence)]
|
||||
public class FrameSequenceExporter : VideoExporter
|
||||
{
|
||||
public FrameSequenceExporter(FrameSequenceExportArgs exportArgs) : base(exportArgs) { }
|
||||
|
||||
protected override void ExportSingle(Spine.Spine[] spinesToRender, BackgroundWorker? worker = null)
|
||||
{
|
||||
var args = (FrameSequenceExportArgs)ExportArgs;
|
||||
|
||||
// 导出单个时必定提供输出文件夹,
|
||||
var saveDir = Path.Combine(args.OutputDir, $"frames_{timestamp}_{args.FPS:f0}");
|
||||
Directory.CreateDirectory(saveDir);
|
||||
|
||||
int frameIdx = 0;
|
||||
foreach (var frame in GetFrames(spinesToRender, worker))
|
||||
{
|
||||
var filename = $"frames_{timestamp}_{args.FPS:f0}_{frameIdx:d6}{args.FileSuffix}";
|
||||
var savePath = Path.Combine(saveDir, filename);
|
||||
|
||||
try
|
||||
{
|
||||
frame.SaveToFile(savePath);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Program.Logger.Error(ex.ToString());
|
||||
Program.Logger.Error("Failed to save frame {}", savePath);
|
||||
}
|
||||
finally
|
||||
{
|
||||
frame.Dispose();
|
||||
}
|
||||
frameIdx++;
|
||||
}
|
||||
}
|
||||
|
||||
protected override void ExportIndividual(Spine.Spine[] spinesToRender, BackgroundWorker? worker = null)
|
||||
{
|
||||
var args = (FrameSequenceExportArgs)ExportArgs;
|
||||
foreach (var spine in spinesToRender)
|
||||
{
|
||||
if (worker?.CancellationPending == true) break; // 取消的日志在 GetFrames 里输出
|
||||
|
||||
// 如果提供了输出文件夹, 则全部导出到输出文件夹, 否则导出到各自的文件夹下
|
||||
var subDir = $"{spine.Name}_{timestamp}_{args.FPS:f0}";
|
||||
var saveDir = args.OutputDir is null ? Path.Combine(spine.AssetsDir, subDir) : Path.Combine(args.OutputDir, subDir);
|
||||
Directory.CreateDirectory(saveDir);
|
||||
|
||||
int frameIdx = 0;
|
||||
foreach (var frame in GetFrames(spine, worker))
|
||||
{
|
||||
var filename = $"{spine.Name}_{timestamp}_{args.FPS:f0}_{frameIdx:d6}{args.FileSuffix}";
|
||||
var savePath = Path.Combine(saveDir, filename);
|
||||
|
||||
try
|
||||
{
|
||||
frame.SaveToFile(savePath);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Program.Logger.Error(ex.ToString());
|
||||
Program.Logger.Error("Failed to save frame {} {}", savePath, spine.SkelPath);
|
||||
}
|
||||
finally
|
||||
{
|
||||
frame.Dispose();
|
||||
}
|
||||
frameIdx++;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
76
SpineViewer/Exporter/Implementations/Exporter/GifExporter.cs
Normal file
76
SpineViewer/Exporter/Implementations/Exporter/GifExporter.cs
Normal file
@@ -0,0 +1,76 @@
|
||||
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;
|
||||
|
||||
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
|
||||
{
|
||||
FFMpegArguments
|
||||
.FromPipeInput(videoFramesSource)
|
||||
.OutputToFile(savePath, true, options => options
|
||||
.ForceFormat("gif")
|
||||
.WithCustomArgument(args.FFMpegCoreCustomArguments))
|
||||
.ProcessSynchronously();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Program.Logger.Error(ex.ToString());
|
||||
Program.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
|
||||
{
|
||||
FFMpegArguments
|
||||
.FromPipeInput(videoFramesSource)
|
||||
.OutputToFile(savePath, true, options => options
|
||||
.ForceFormat("gif")
|
||||
.WithCustomArgument(args.FFMpegCoreCustomArguments))
|
||||
.ProcessSynchronously();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Program.Logger.Error(ex.ToString());
|
||||
Program.Logger.Error("Failed to export gif {} {}", savePath, spine.SkelPath);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
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 则使用自己的动画时长
|
||||
var duration = args.Duration;
|
||||
if (duration < 0) duration = spine.GetAnimationDuration(spine.CurrentAnimation);
|
||||
|
||||
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)
|
||||
{
|
||||
Program.Logger.Info("Export cancelled");
|
||||
break;
|
||||
}
|
||||
|
||||
var frame = GetFrame(spine);
|
||||
spine.Update(delta);
|
||||
worker?.ReportProgress((int)((i + 1) * 100.0) / total, $"{spine.Name} 已处理 {i + 1}/{total} 帧");
|
||||
yield return frame;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 生成多个模型的帧序列
|
||||
/// </summary>
|
||||
protected IEnumerable<SFMLImageVideoFrame> GetFrames(Spine.Spine[] spinesToRender, BackgroundWorker? worker = null)
|
||||
{
|
||||
// 导出单个时必须根据 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)
|
||||
{
|
||||
Program.Logger.Info("Export cancelled");
|
||||
break;
|
||||
}
|
||||
|
||||
var frame = GetFrame(spinesToRender);
|
||||
foreach (var spine in spinesToRender) spine.Update(delta);
|
||||
worker?.ReportProgress((int)((i + 1) * 100.0) / total, $"已处理 {i + 1}/{total} 帧");
|
||||
yield return frame;
|
||||
}
|
||||
}
|
||||
|
||||
public override void Export(Spine.Spine[] spines, BackgroundWorker? worker = null)
|
||||
{
|
||||
// 导出视频格式需要把模型时间都重置到 0
|
||||
foreach (var spine in spines) spine.CurrentAnimation = spine.CurrentAnimation;
|
||||
base.Export(spines, worker);
|
||||
}
|
||||
}
|
||||
}
|
||||
33
SpineViewer/Exporter/TypeConverter.cs
Normal file
33
SpineViewer/Exporter/TypeConverter.cs
Normal file
@@ -0,0 +1,33 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.ComponentModel;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace SpineViewer.Exporter
|
||||
{
|
||||
public class SFMLImageFileSuffixConverter : StringConverter
|
||||
{
|
||||
private readonly string[] supportedFileSuffix = [".png", ".jpg", ".tga", ".bmp"];
|
||||
public override bool GetStandardValuesSupported(ITypeDescriptorContext? context)
|
||||
{
|
||||
// 支持标准值列表
|
||||
return true;
|
||||
}
|
||||
|
||||
public override bool GetStandardValuesExclusive(ITypeDescriptorContext? context)
|
||||
{
|
||||
// 排他模式,只有下拉列表中的值可选
|
||||
return true;
|
||||
}
|
||||
|
||||
public override StandardValuesCollection? GetStandardValues(ITypeDescriptorContext? context)
|
||||
{
|
||||
return new StandardValuesCollection(supportedFileSuffix);
|
||||
}
|
||||
}
|
||||
}
|
||||
111
SpineViewer/MainForm.Designer.cs
generated
111
SpineViewer/MainForm.Designer.cs
generated
@@ -36,11 +36,15 @@
|
||||
toolStripMenuItem_BatchOpen = new ToolStripMenuItem();
|
||||
toolStripSeparator1 = new ToolStripSeparator();
|
||||
toolStripMenuItem_Export = new ToolStripMenuItem();
|
||||
toolStripMenuItem_ExportPreview = new ToolStripMenuItem();
|
||||
toolStripMenuItem_ExportFrame = new ToolStripMenuItem();
|
||||
toolStripMenuItem_ExportFrameSequence = new ToolStripMenuItem();
|
||||
toolStripMenuItem_ExportGif = new ToolStripMenuItem();
|
||||
toolStripMenuItem_ExportMkv = new ToolStripMenuItem();
|
||||
toolStripMenuItem_ExportMp4 = new ToolStripMenuItem();
|
||||
toolStripMenuItem_ExportMov = new ToolStripMenuItem();
|
||||
toolStripMenuItem_ExportWebm = new ToolStripMenuItem();
|
||||
toolStripSeparator2 = new ToolStripSeparator();
|
||||
toolStripMenuItem_Exit = new ToolStripMenuItem();
|
||||
toolStripMenuItem_Function = new ToolStripMenuItem();
|
||||
toolStripMenuItem_ResetAnimation = new ToolStripMenuItem();
|
||||
toolStripMenuItem_Tool = new ToolStripMenuItem();
|
||||
toolStripMenuItem_ConvertFileFormat = new ToolStripMenuItem();
|
||||
toolStripMenuItem_Download = new ToolStripMenuItem();
|
||||
@@ -92,7 +96,7 @@
|
||||
//
|
||||
menuStrip.BackColor = SystemColors.Control;
|
||||
menuStrip.ImageScalingSize = new Size(24, 24);
|
||||
menuStrip.Items.AddRange(new ToolStripItem[] { toolStripMenuItem_File, toolStripMenuItem_Function, toolStripMenuItem_Tool, toolStripMenuItem_Download, toolStripMenuItem_Help });
|
||||
menuStrip.Items.AddRange(new ToolStripItem[] { toolStripMenuItem_File, toolStripMenuItem_Tool, toolStripMenuItem_Download, toolStripMenuItem_Help });
|
||||
menuStrip.Location = new Point(0, 0);
|
||||
menuStrip.Name = "menuStrip";
|
||||
menuStrip.Size = new Size(1748, 32);
|
||||
@@ -101,7 +105,7 @@
|
||||
//
|
||||
// toolStripMenuItem_File
|
||||
//
|
||||
toolStripMenuItem_File.DropDownItems.AddRange(new ToolStripItem[] { toolStripMenuItem_Open, toolStripMenuItem_BatchOpen, toolStripSeparator1, toolStripMenuItem_Export, toolStripMenuItem_ExportPreview, toolStripSeparator2, toolStripMenuItem_Exit });
|
||||
toolStripMenuItem_File.DropDownItems.AddRange(new ToolStripItem[] { toolStripMenuItem_Open, toolStripMenuItem_BatchOpen, toolStripSeparator1, toolStripMenuItem_Export, toolStripSeparator2, toolStripMenuItem_Exit });
|
||||
toolStripMenuItem_File.Name = "toolStripMenuItem_File";
|
||||
toolStripMenuItem_File.Size = new Size(84, 28);
|
||||
toolStripMenuItem_File.Text = "文件(&F)";
|
||||
@@ -110,64 +114,91 @@
|
||||
//
|
||||
toolStripMenuItem_Open.Name = "toolStripMenuItem_Open";
|
||||
toolStripMenuItem_Open.ShortcutKeys = Keys.Control | Keys.O;
|
||||
toolStripMenuItem_Open.Size = new Size(254, 34);
|
||||
toolStripMenuItem_Open.Size = new Size(270, 34);
|
||||
toolStripMenuItem_Open.Text = "打开(&O)...";
|
||||
toolStripMenuItem_Open.Click += toolStripMenuItem_Open_Click;
|
||||
//
|
||||
// toolStripMenuItem_BatchOpen
|
||||
//
|
||||
toolStripMenuItem_BatchOpen.Name = "toolStripMenuItem_BatchOpen";
|
||||
toolStripMenuItem_BatchOpen.Size = new Size(254, 34);
|
||||
toolStripMenuItem_BatchOpen.Size = new Size(270, 34);
|
||||
toolStripMenuItem_BatchOpen.Text = "批量打开(&B)...";
|
||||
toolStripMenuItem_BatchOpen.Click += toolStripMenuItem_BatchOpen_Click;
|
||||
//
|
||||
// toolStripSeparator1
|
||||
//
|
||||
toolStripSeparator1.Name = "toolStripSeparator1";
|
||||
toolStripSeparator1.Size = new Size(251, 6);
|
||||
toolStripSeparator1.Size = new Size(267, 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.Name = "toolStripMenuItem_Export";
|
||||
toolStripMenuItem_Export.ShortcutKeys = Keys.Control | Keys.S;
|
||||
toolStripMenuItem_Export.Size = new Size(254, 34);
|
||||
toolStripMenuItem_Export.Text = "导出(&E)...";
|
||||
toolStripMenuItem_Export.Click += toolStripMenuItem_Export_Click;
|
||||
toolStripMenuItem_Export.Size = new Size(270, 34);
|
||||
toolStripMenuItem_Export.Text = "导出(&E)";
|
||||
//
|
||||
// toolStripMenuItem_ExportPreview
|
||||
// toolStripMenuItem_ExportFrame
|
||||
//
|
||||
toolStripMenuItem_ExportPreview.Name = "toolStripMenuItem_ExportPreview";
|
||||
toolStripMenuItem_ExportPreview.Size = new Size(254, 34);
|
||||
toolStripMenuItem_ExportPreview.Text = "导出预览图(&P)...";
|
||||
toolStripMenuItem_ExportPreview.Click += toolStripMenuItem_ExportPreview_Click;
|
||||
toolStripMenuItem_ExportFrame.Name = "toolStripMenuItem_ExportFrame";
|
||||
toolStripMenuItem_ExportFrame.Size = new Size(270, 34);
|
||||
toolStripMenuItem_ExportFrame.Text = "单帧画面...";
|
||||
toolStripMenuItem_ExportFrame.Click += toolStripMenuItem_Export_Click;
|
||||
//
|
||||
// toolStripMenuItem_ExportFrameSequence
|
||||
//
|
||||
toolStripMenuItem_ExportFrameSequence.Name = "toolStripMenuItem_ExportFrameSequence";
|
||||
toolStripMenuItem_ExportFrameSequence.Size = new Size(270, 34);
|
||||
toolStripMenuItem_ExportFrameSequence.Text = "帧序列...";
|
||||
toolStripMenuItem_ExportFrameSequence.Click += toolStripMenuItem_Export_Click;
|
||||
//
|
||||
// toolStripMenuItem_ExportGif
|
||||
//
|
||||
toolStripMenuItem_ExportGif.Name = "toolStripMenuItem_ExportGif";
|
||||
toolStripMenuItem_ExportGif.Size = new Size(270, 34);
|
||||
toolStripMenuItem_ExportGif.Text = "GIF...";
|
||||
toolStripMenuItem_ExportGif.Click += toolStripMenuItem_Export_Click;
|
||||
//
|
||||
// toolStripMenuItem_ExportMkv
|
||||
//
|
||||
toolStripMenuItem_ExportMkv.Name = "toolStripMenuItem_ExportMkv";
|
||||
toolStripMenuItem_ExportMkv.Size = new Size(270, 34);
|
||||
toolStripMenuItem_ExportMkv.Text = "MKV";
|
||||
toolStripMenuItem_ExportMkv.Click += toolStripMenuItem_Export_Click;
|
||||
//
|
||||
// toolStripMenuItem_ExportMp4
|
||||
//
|
||||
toolStripMenuItem_ExportMp4.Name = "toolStripMenuItem_ExportMp4";
|
||||
toolStripMenuItem_ExportMp4.Size = new Size(270, 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.Click += toolStripMenuItem_Export_Click;
|
||||
//
|
||||
// toolStripMenuItem_ExportWebm
|
||||
//
|
||||
toolStripMenuItem_ExportWebm.Name = "toolStripMenuItem_ExportWebm";
|
||||
toolStripMenuItem_ExportWebm.Size = new Size(270, 34);
|
||||
toolStripMenuItem_ExportWebm.Text = "WebM...";
|
||||
toolStripMenuItem_ExportWebm.Click += toolStripMenuItem_Export_Click;
|
||||
//
|
||||
// toolStripSeparator2
|
||||
//
|
||||
toolStripSeparator2.Name = "toolStripSeparator2";
|
||||
toolStripSeparator2.Size = new Size(251, 6);
|
||||
toolStripSeparator2.Size = new Size(267, 6);
|
||||
//
|
||||
// toolStripMenuItem_Exit
|
||||
//
|
||||
toolStripMenuItem_Exit.Name = "toolStripMenuItem_Exit";
|
||||
toolStripMenuItem_Exit.ShortcutKeys = Keys.Alt | Keys.F4;
|
||||
toolStripMenuItem_Exit.Size = new Size(254, 34);
|
||||
toolStripMenuItem_Exit.Size = new Size(270, 34);
|
||||
toolStripMenuItem_Exit.Text = "退出(&X)";
|
||||
toolStripMenuItem_Exit.Click += toolStripMenuItem_Exit_Click;
|
||||
//
|
||||
// toolStripMenuItem_Function
|
||||
//
|
||||
toolStripMenuItem_Function.DropDownItems.AddRange(new ToolStripItem[] { toolStripMenuItem_ResetAnimation });
|
||||
toolStripMenuItem_Function.Name = "toolStripMenuItem_Function";
|
||||
toolStripMenuItem_Function.Size = new Size(87, 28);
|
||||
toolStripMenuItem_Function.Text = "功能(&G)";
|
||||
//
|
||||
// toolStripMenuItem_ResetAnimation
|
||||
//
|
||||
toolStripMenuItem_ResetAnimation.Name = "toolStripMenuItem_ResetAnimation";
|
||||
toolStripMenuItem_ResetAnimation.Size = new Size(242, 34);
|
||||
toolStripMenuItem_ResetAnimation.Text = "重置动画时间(&R)";
|
||||
toolStripMenuItem_ResetAnimation.Click += toolStripMenuItem_ResetAnimation_Click;
|
||||
//
|
||||
// toolStripMenuItem_Tool
|
||||
//
|
||||
toolStripMenuItem_Tool.DropDownItems.AddRange(new ToolStripItem[] { toolStripMenuItem_ConvertFileFormat });
|
||||
@@ -408,7 +439,6 @@
|
||||
//
|
||||
// spinePreviewer
|
||||
//
|
||||
spinePreviewer.BackColor = SystemColors.ControlDark;
|
||||
spinePreviewer.Dock = DockStyle.Fill;
|
||||
spinePreviewer.Location = new Point(3, 26);
|
||||
spinePreviewer.Name = "spinePreviewer";
|
||||
@@ -416,7 +446,6 @@
|
||||
spinePreviewer.Size = new Size(971, 850);
|
||||
spinePreviewer.SpineListView = spineListView;
|
||||
spinePreviewer.TabIndex = 0;
|
||||
spinePreviewer.MouseUp += spinePreviewer_MouseUp;
|
||||
//
|
||||
// panel_MainForm
|
||||
//
|
||||
@@ -481,7 +510,6 @@
|
||||
private ToolStripMenuItem toolStripMenuItem_Open;
|
||||
private ToolStripMenuItem toolStripMenuItem_Exit;
|
||||
private ToolStripSeparator toolStripSeparator1;
|
||||
private ToolStripMenuItem toolStripMenuItem_Export;
|
||||
private ToolStripSeparator toolStripSeparator2;
|
||||
private RichTextBox rtbLog;
|
||||
private SplitContainer splitContainer_MainForm;
|
||||
@@ -501,14 +529,19 @@
|
||||
private Controls.SpineListView spineListView;
|
||||
private PropertyGrid propertyGrid_Previewer;
|
||||
private Controls.SpinePreviewer spinePreviewer;
|
||||
private ToolStripMenuItem toolStripMenuItem_Function;
|
||||
private ToolStripMenuItem toolStripMenuItem_ResetAnimation;
|
||||
private ToolStripMenuItem toolStripMenuItem_Diagnostics;
|
||||
private ToolStripSeparator toolStripSeparator3;
|
||||
private ToolStripMenuItem toolStripMenuItem_Download;
|
||||
private ToolStripMenuItem toolStripMenuItem_ManageResource;
|
||||
private ToolStripMenuItem toolStripMenuItem_Tool;
|
||||
private ToolStripMenuItem toolStripMenuItem_ConvertFileFormat;
|
||||
private ToolStripMenuItem toolStripMenuItem_ExportPreview;
|
||||
private ToolStripMenuItem toolStripMenuItem_Export;
|
||||
private ToolStripMenuItem toolStripMenuItem_ExportFrame;
|
||||
private ToolStripMenuItem toolStripMenuItem_ExportFrameSequence;
|
||||
private ToolStripMenuItem toolStripMenuItem_ExportGif;
|
||||
private ToolStripMenuItem toolStripMenuItem_ExportMp4;
|
||||
private ToolStripMenuItem toolStripMenuItem_ExportMov;
|
||||
private ToolStripMenuItem toolStripMenuItem_ExportMkv;
|
||||
private ToolStripMenuItem toolStripMenuItem_ExportWebm;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ using System.Diagnostics;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Nodes;
|
||||
using FFMpegCore.Enums;
|
||||
using SpineViewer.Exporter;
|
||||
|
||||
namespace SpineViewer
|
||||
{
|
||||
@@ -17,6 +18,15 @@ namespace SpineViewer
|
||||
{
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -49,12 +59,12 @@ namespace SpineViewer
|
||||
|
||||
private void MainForm_Load(object sender, EventArgs e)
|
||||
{
|
||||
spinePreviewer.StartPreview();
|
||||
spinePreviewer.StartRender();
|
||||
}
|
||||
|
||||
private void MainForm_FormClosing(object sender, FormClosingEventArgs e)
|
||||
{
|
||||
spinePreviewer.StopPreview();
|
||||
spinePreviewer.StopRender();
|
||||
}
|
||||
|
||||
private void toolStripMenuItem_Open_Click(object sender, EventArgs e)
|
||||
@@ -69,7 +79,14 @@ namespace SpineViewer
|
||||
|
||||
private void toolStripMenuItem_Export_Click(object sender, EventArgs e)
|
||||
{
|
||||
// TODO: 改成统一导出调用
|
||||
ExportType type = (ExportType)((ToolStripMenuItem)sender).Tag;
|
||||
|
||||
if (type == ExportType.Frame && spinePreviewer.IsUpdating)
|
||||
{
|
||||
if (MessageBox.Quest("画面仍在更新,建议手动暂停画面后导出固定的一帧,是否继续?") != DialogResult.OK)
|
||||
return;
|
||||
}
|
||||
|
||||
lock (spineListView.Spines)
|
||||
{
|
||||
if (spineListView.Spines.Count <= 0)
|
||||
@@ -79,34 +96,16 @@ namespace SpineViewer
|
||||
}
|
||||
}
|
||||
|
||||
var exportDialog = new Dialogs.ExportPngDialog();
|
||||
var exportArgs = ExportArgs.New(type, spinePreviewer.Resolution, spinePreviewer.GetView(), spinePreviewer.RenderSelectedOnly);
|
||||
var exportDialog = new Dialogs.ExportDialog() { ExportArgs = exportArgs };
|
||||
if (exportDialog.ShowDialog() != DialogResult.OK)
|
||||
return;
|
||||
|
||||
var progressDialog = new Dialogs.ProgressDialog();
|
||||
progressDialog.DoWork += ExportPng_Work;
|
||||
progressDialog.RunWorkerAsync(exportDialog);
|
||||
progressDialog.ShowDialog();
|
||||
}
|
||||
|
||||
private void toolStripMenuItem_ExportPreview_Click(object sender, EventArgs e)
|
||||
{
|
||||
lock (spineListView.Spines)
|
||||
{
|
||||
if (spineListView.Spines.Count <= 0)
|
||||
{
|
||||
MessageBox.Info("请至少打开一个骨骼文件");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
var saveDialog = new Dialogs.ExportPreviewDialog();
|
||||
if (saveDialog.ShowDialog() != DialogResult.OK)
|
||||
return;
|
||||
var exporter = Exporter.Exporter.New(type, exportArgs);
|
||||
|
||||
var progressDialog = new Dialogs.ProgressDialog();
|
||||
progressDialog.DoWork += ExportPreview_Work;
|
||||
progressDialog.RunWorkerAsync(saveDialog.Result);
|
||||
progressDialog.DoWork += Export_Work;
|
||||
progressDialog.RunWorkerAsync(exporter);
|
||||
progressDialog.ShowDialog();
|
||||
}
|
||||
|
||||
@@ -115,15 +114,6 @@ namespace SpineViewer
|
||||
Close();
|
||||
}
|
||||
|
||||
private void toolStripMenuItem_ResetAnimation_Click(object sender, EventArgs e)
|
||||
{
|
||||
lock (spineListView.Spines)
|
||||
{
|
||||
foreach (var spine in spineListView.Spines)
|
||||
spine.CurrentAnimation = spine.CurrentAnimation;
|
||||
}
|
||||
}
|
||||
|
||||
private void toolStripMenuItem_ConvertFileFormat_Click(object sender, EventArgs e)
|
||||
{
|
||||
var openDialog = new Dialogs.ConvertFileFormatDialog();
|
||||
@@ -136,70 +126,9 @@ namespace SpineViewer
|
||||
progressDialog.ShowDialog();
|
||||
}
|
||||
|
||||
//IEnumerable<IVideoFrame> testExport(int fps)
|
||||
//{
|
||||
// var duration = 2f;
|
||||
// var resolution = spinePreviewer.Resolution;
|
||||
// var delta = 1f / fps;
|
||||
// var frameCount = 1 + (int)(duration / delta); // 零帧开始导出
|
||||
|
||||
// var spinesReverse = spineListView.Spines.Reverse();
|
||||
|
||||
// // 重置动画时间
|
||||
// foreach (var spine in spinesReverse)
|
||||
// spine.CurrentAnimation = spine.CurrentAnimation;
|
||||
|
||||
// // 逐帧导出
|
||||
// var success = 0;
|
||||
// for (int frameIndex = 0; frameIndex < frameCount; frameIndex++)
|
||||
// {
|
||||
// using var tex = new SFML.Graphics.RenderTexture((uint)resolution.Width, (uint)resolution.Height);
|
||||
// tex.SetView(spinePreviewer.View);
|
||||
// tex.Clear(SFML.Graphics.Color.Transparent);
|
||||
|
||||
// foreach (var spine in spinesReverse)
|
||||
// {
|
||||
// tex.Draw(spine);
|
||||
// spine.Update(delta);
|
||||
// }
|
||||
|
||||
// tex.Display();
|
||||
// Debug.WriteLine($"ThreadID: {Environment.CurrentManagedThreadId}");
|
||||
// var frame = tex.Texture.CopyToFrame();
|
||||
// tex.Dispose();
|
||||
// yield return frame;
|
||||
|
||||
// success++;
|
||||
// }
|
||||
|
||||
// Program.Logger.Info("Exporting done: {}/{}", success, frameCount);
|
||||
//}
|
||||
|
||||
private void toolStripMenuItem_ManageResource_Click(object sender, EventArgs e)
|
||||
{
|
||||
//spinePreviewer.StopPreview();
|
||||
|
||||
//lock (spineListView.Spines)
|
||||
//{
|
||||
// //var fps = 24;
|
||||
// ////foreach (var i in testExport(fps))
|
||||
// //// _ = i;
|
||||
// ////var t = testExport(fps).ToArray();
|
||||
// ////var a = testExport(fps).GetEnumerator();
|
||||
// ////while (a.MoveNext());
|
||||
// //var videoFramesSource = new RawVideoPipeSource(testExport(fps)) { FrameRate = fps };
|
||||
// //var outputPath = @"C:\Users\ljh\Desktop\test\a.mov";
|
||||
// //var task = FFMpegArguments
|
||||
// // .FromPipeInput(videoFramesSource)
|
||||
// // .OutputToFile(outputPath, true
|
||||
// // , options => options
|
||||
// // //.WithCustomArgument("-vf \"split[s0][s1];[s0]palettegen=reserve_transparent=1[p];[s1][p]paletteuse=alpha_threshold=128\""))
|
||||
// // .WithCustomArgument("-c:v prores_ks -profile:v 4444 -pix_fmt yuva444p10le"))
|
||||
// // .ProcessAsynchronously();
|
||||
// //task.Wait();
|
||||
//}
|
||||
|
||||
//spinePreviewer.StartPreview();
|
||||
}
|
||||
|
||||
private void toolStripMenuItem_About_Click(object sender, EventArgs e)
|
||||
@@ -216,158 +145,20 @@ namespace SpineViewer
|
||||
|
||||
private void splitContainer_MouseUp(object sender, MouseEventArgs e) => ActiveControl = null;
|
||||
|
||||
private void propertyGrid_PropertyValueChanged(object sender, PropertyValueChangedEventArgs e) => (sender as PropertyGrid)?.Refresh();
|
||||
|
||||
private void spinePreviewer_MouseUp(object sender, MouseEventArgs e)
|
||||
{
|
||||
propertyGrid_Spine.Refresh();
|
||||
private void propertyGrid_PropertyValueChanged(object sender, PropertyValueChangedEventArgs e)
|
||||
{
|
||||
// 用来解决对面板某些值修改之后, 其他被联动修改的值不会实时刷新的问题
|
||||
(sender as PropertyGrid)?.Refresh();
|
||||
}
|
||||
|
||||
private void ExportPng_Work(object? sender, DoWorkEventArgs e)
|
||||
private void Export_Work(object? sender, DoWorkEventArgs e)
|
||||
{
|
||||
var worker = sender as BackgroundWorker;
|
||||
var arguments = e.Argument as Dialogs.ExportPngDialog;
|
||||
var outputDir = arguments.OutputDir;
|
||||
var duration = arguments.Duration;
|
||||
var fps = arguments.Fps;
|
||||
var timestamp = DateTime.Now.ToString("yyMMddHHmmss");
|
||||
|
||||
var frameArgs = spinePreviewer.GetFrameArgs();
|
||||
var renderSelectedOnly = spinePreviewer.RenderSelectedOnly;
|
||||
|
||||
var resolution = frameArgs.Resolution;
|
||||
var tex = new SFML.Graphics.RenderTexture((uint)resolution.Width, (uint)resolution.Height);
|
||||
tex.SetView(frameArgs.View);
|
||||
var delta = 1f / fps;
|
||||
var frameCount = 1 + (int)(duration / delta); // 零帧开始导出
|
||||
|
||||
spinePreviewer.StopPreview();
|
||||
|
||||
lock (spineListView.Spines)
|
||||
{
|
||||
var spinesReverse = spineListView.Spines.Reverse();
|
||||
|
||||
// 重置动画时间
|
||||
foreach (var spine in spinesReverse)
|
||||
spine.CurrentAnimation = spine.CurrentAnimation;
|
||||
|
||||
Program.Logger.Info(
|
||||
"Begin exporting png frames to output dir {}, duration: {}, fps: {}, totally {} spines",
|
||||
[outputDir, duration, fps, spinesReverse.Count()]
|
||||
);
|
||||
|
||||
// 逐帧导出
|
||||
var success = 0;
|
||||
worker.ReportProgress(0, $"已处理 0/{frameCount}");
|
||||
for (int frameIndex = 0; frameIndex < frameCount; frameIndex++)
|
||||
{
|
||||
if (worker.CancellationPending)
|
||||
break;
|
||||
|
||||
tex.Clear(SFML.Graphics.Color.Transparent);
|
||||
|
||||
foreach (var spine in spinesReverse)
|
||||
{
|
||||
if (renderSelectedOnly && !spine.IsSelected)
|
||||
continue;
|
||||
|
||||
tex.Draw(spine);
|
||||
spine.Update(delta);
|
||||
}
|
||||
|
||||
tex.Display();
|
||||
using (var img = tex.Texture.CopyToImage())
|
||||
{
|
||||
img.SaveToFile(Path.Combine(outputDir, $"{timestamp}_{fps}_{frameIndex:d6}.png"));
|
||||
}
|
||||
|
||||
success++;
|
||||
worker.ReportProgress((int)((frameIndex + 1) * 100.0) / frameCount, $"已处理 {frameIndex + 1}/{frameCount}");
|
||||
}
|
||||
|
||||
Program.Logger.Info("Exporting done: {}/{}", success, frameCount);
|
||||
}
|
||||
|
||||
spinePreviewer.StartPreview();
|
||||
}
|
||||
|
||||
private void ExportPreview_Work(object? sender, DoWorkEventArgs e)
|
||||
{
|
||||
var worker = sender as BackgroundWorker;
|
||||
var arguments = e.Argument as Dialogs.ExportPreviewDialogResult;
|
||||
|
||||
var outputDir = arguments.OutputDir;
|
||||
var imageFormat = arguments.ImageFormat;
|
||||
var resolution = arguments.Resolution;
|
||||
var padding = arguments.Padding;
|
||||
var dpi = arguments.DPI;
|
||||
var nameSuffix = arguments.NameSuffix;
|
||||
var renderSelectedOnly = spinePreviewer.RenderSelectedOnly;
|
||||
|
||||
var tex = new SFML.Graphics.RenderTexture((uint)resolution.Width, (uint)resolution.Height);
|
||||
|
||||
int success = 0;
|
||||
int error = 0;
|
||||
spinePreviewer.StopPreview();
|
||||
lock (spineListView.Spines)
|
||||
{
|
||||
var spines = spineListView.Spines;
|
||||
int totalCount = spines.Count;
|
||||
worker.ReportProgress(0, $"已处理 0/{totalCount}");
|
||||
for (int i = 0; i < totalCount; i++)
|
||||
{
|
||||
if (worker.CancellationPending)
|
||||
{
|
||||
e.Cancel = true;
|
||||
break;
|
||||
}
|
||||
|
||||
var spine = spines[i];
|
||||
if (renderSelectedOnly && !spine.IsSelected)
|
||||
continue;
|
||||
|
||||
var filename = $"{spine.Name}{nameSuffix}{imageFormat.GetSuffix()}";
|
||||
var savePath = outputDir is null ? Path.Combine(spine.AssetsDir, filename) : Path.Combine(outputDir, filename);
|
||||
|
||||
var tmp = spine.CurrentAnimation;
|
||||
spine.CurrentAnimation = Spine.Spine.EMPTY_ANIMATION;
|
||||
tex.SetView(spine.GetInitView(resolution, padding));
|
||||
tex.Clear(SFML.Graphics.Color.Transparent);
|
||||
tex.Draw(spine);
|
||||
tex.Display();
|
||||
spine.CurrentAnimation = tmp;
|
||||
|
||||
try
|
||||
{
|
||||
using (var img = new Bitmap(tex.Texture.CopyToBitmap()))
|
||||
{
|
||||
img.SetResolution(dpi.Width, dpi.Height);
|
||||
img.Save(savePath, imageFormat);
|
||||
}
|
||||
success++;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Program.Logger.Error(ex.ToString());
|
||||
Program.Logger.Error("Failed to save preview {}", spine.SkelPath);
|
||||
error++;
|
||||
}
|
||||
|
||||
worker.ReportProgress((int)((i + 1) * 100.0) / totalCount, $"已处理 {i + 1}/{totalCount}");
|
||||
}
|
||||
}
|
||||
spinePreviewer.StartPreview();
|
||||
|
||||
if (error > 0)
|
||||
{
|
||||
Program.Logger.Warn("Preview save {} successfully, {} failed", success, error);
|
||||
}
|
||||
else
|
||||
{
|
||||
Program.Logger.Info("{} preview saved successfully", success);
|
||||
}
|
||||
|
||||
Program.LogCurrentMemoryUsage();
|
||||
var worker = (BackgroundWorker)sender;
|
||||
var exporter = (Exporter.Exporter)e.Argument;
|
||||
spinePreviewer.StopRender();
|
||||
lock (spineListView.Spines) { exporter.Export(spineListView.Spines.ToArray(), (BackgroundWorker)sender); }
|
||||
e.Cancel = worker.CancellationPending;
|
||||
spinePreviewer.StartRender();
|
||||
}
|
||||
|
||||
private void ConvertFileFormat_Work(object? sender, DoWorkEventArgs e)
|
||||
@@ -432,5 +223,28 @@ namespace SpineViewer
|
||||
Program.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;
|
||||
// }
|
||||
//}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,12 +8,10 @@ using System.Text.Json;
|
||||
using System.Text.Json.Nodes;
|
||||
using SpineRuntime38.Attachments;
|
||||
using System.Globalization;
|
||||
using static System.Runtime.InteropServices.JavaScript.JSType;
|
||||
using System.IO;
|
||||
|
||||
namespace SpineViewer.Spine.Implementations.SkeletonConverter
|
||||
{
|
||||
[SkeletonConverterImplementation(Version.V38)]
|
||||
[SpineImplementation(Version.V38)]
|
||||
class SkeletonConverter38 : SpineViewer.Spine.SkeletonConverter
|
||||
{
|
||||
private BinaryReader reader = null;
|
||||
|
||||
@@ -239,9 +239,9 @@ namespace SpineViewer.Spine.Implementations.Spine
|
||||
|
||||
public override void Update(float delta)
|
||||
{
|
||||
skeleton.Update(delta);
|
||||
animationState.Update(delta);
|
||||
animationState.Apply(skeleton);
|
||||
skeleton.Update(delta);
|
||||
skeleton.UpdateWorldTransform();
|
||||
}
|
||||
|
||||
|
||||
@@ -197,9 +197,9 @@ namespace SpineViewer.Spine.Implementations.Spine
|
||||
|
||||
public override void Update(float delta)
|
||||
{
|
||||
skeleton.Update(delta);
|
||||
animationState.Update(delta);
|
||||
animationState.Apply(skeleton);
|
||||
skeleton.Update(delta);
|
||||
skeleton.UpdateWorldTransform();
|
||||
}
|
||||
|
||||
|
||||
@@ -204,9 +204,9 @@ namespace SpineViewer.Spine.Implementations.Spine
|
||||
|
||||
public override void Update(float delta)
|
||||
{
|
||||
skeleton.Update(delta);
|
||||
animationState.Update(delta);
|
||||
animationState.Apply(skeleton);
|
||||
skeleton.Update(delta);
|
||||
skeleton.UpdateWorldTransform();
|
||||
}
|
||||
|
||||
|
||||
@@ -207,9 +207,9 @@ namespace SpineViewer.Spine.Implementations.Spine
|
||||
|
||||
public override void Update(float delta)
|
||||
{
|
||||
skeleton.Update(delta);
|
||||
animationState.Update(delta);
|
||||
animationState.Apply(skeleton);
|
||||
skeleton.Update(delta);
|
||||
skeleton.UpdateWorldTransform();
|
||||
}
|
||||
|
||||
|
||||
@@ -206,9 +206,9 @@ namespace SpineViewer.Spine.Implementations.Spine
|
||||
|
||||
public override void Update(float delta)
|
||||
{
|
||||
skeleton.Update(delta);
|
||||
animationState.Update(delta);
|
||||
animationState.Apply(skeleton);
|
||||
skeleton.Update(delta);
|
||||
skeleton.UpdateWorldTransform();
|
||||
}
|
||||
|
||||
|
||||
@@ -206,9 +206,9 @@ namespace SpineViewer.Spine.Implementations.Spine
|
||||
|
||||
public override void Update(float delta)
|
||||
{
|
||||
//skeleton.Update(delta); // XXX: v4.1.x 没有 Update 方法
|
||||
animationState.Update(delta);
|
||||
animationState.Apply(skeleton);
|
||||
//skeleton.Update(delta); // XXX: v4.1.x 没有 Update 方法
|
||||
skeleton.UpdateWorldTransform();
|
||||
}
|
||||
|
||||
|
||||
@@ -206,9 +206,9 @@ namespace SpineViewer.Spine.Implementations.Spine
|
||||
|
||||
public override void Update(float delta)
|
||||
{
|
||||
skeleton.Update(delta);
|
||||
animationState.Update(delta);
|
||||
animationState.Apply(skeleton);
|
||||
skeleton.Update(delta);
|
||||
skeleton.UpdateWorldTransform(Skeleton.Physics.Update);
|
||||
}
|
||||
|
||||
|
||||
@@ -12,20 +12,6 @@ using System.Text.Encodings.Web;
|
||||
|
||||
namespace SpineViewer.Spine
|
||||
{
|
||||
/// <summary>
|
||||
/// SkeletonConverter 实现类标记
|
||||
/// </summary>
|
||||
[AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = false)]
|
||||
public class SkeletonConverterImplementationAttribute : Attribute
|
||||
{
|
||||
public Version Version { get; }
|
||||
|
||||
public SkeletonConverterImplementationAttribute(Version version)
|
||||
{
|
||||
Version = version;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// SkeletonConverter 基类, 使用静态方法 New 来创建具体版本对象
|
||||
/// </summary>
|
||||
@@ -42,11 +28,11 @@ namespace SpineViewer.Spine
|
||||
/// </summary>
|
||||
static SkeletonConverter()
|
||||
{
|
||||
// 遍历并缓存标记了 SkeletonConverterImplementationAttribute 的类型
|
||||
// 遍历并缓存标记了 SpineImplementationAttribute 的类型
|
||||
var impTypes = Assembly.GetExecutingAssembly().GetTypes().Where(t => typeof(SkeletonConverter).IsAssignableFrom(t) && !t.IsAbstract);
|
||||
foreach (var type in impTypes)
|
||||
{
|
||||
var attr = type.GetCustomAttribute<SkeletonConverterImplementationAttribute>();
|
||||
var attr = type.GetCustomAttribute<SpineImplementationAttribute>();
|
||||
if (attr is not null)
|
||||
{
|
||||
if (ImplementationTypes.ContainsKey(attr.Version))
|
||||
|
||||
@@ -14,23 +14,10 @@ using System.Diagnostics.CodeAnalysis;
|
||||
using System.Globalization;
|
||||
using System.Text.Json.Nodes;
|
||||
using System.Collections.Immutable;
|
||||
using SpineViewer.Exporter;
|
||||
|
||||
namespace SpineViewer.Spine
|
||||
{
|
||||
/// <summary>
|
||||
/// Spine 实现类标记
|
||||
/// </summary>
|
||||
[AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = false)]
|
||||
public class SpineImplementationAttribute : Attribute
|
||||
{
|
||||
public Version Version { get; }
|
||||
|
||||
public SpineImplementationAttribute(Version version)
|
||||
{
|
||||
Version = version;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Spine 基类, 使用静态方法 New 来创建具体版本对象
|
||||
/// </summary>
|
||||
@@ -346,6 +333,26 @@ namespace SpineViewer.Spine
|
||||
[Browsable(false)]
|
||||
public abstract RectangleF Bounds { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 初始状态下的骨骼包围盒
|
||||
/// </summary>
|
||||
[Browsable(false)]
|
||||
public RectangleF InitBounds
|
||||
{
|
||||
get
|
||||
{
|
||||
if (initBounds is null)
|
||||
{
|
||||
var tmp = CurrentAnimation;
|
||||
CurrentAnimation = EMPTY_ANIMATION;
|
||||
initBounds = Bounds;
|
||||
CurrentAnimation = tmp;
|
||||
}
|
||||
return (RectangleF)initBounds;
|
||||
}
|
||||
}
|
||||
private RectangleF? initBounds = null;
|
||||
|
||||
/// <summary>
|
||||
/// 骨骼预览图
|
||||
/// </summary>
|
||||
@@ -362,7 +369,7 @@ namespace SpineViewer.Spine
|
||||
// 除此之外, 似乎还和 tex 的 Dispose 有关
|
||||
// 如果不对 tex 进行 Dispose, 那么不管是否 Draw 都正常不会死锁
|
||||
var tex = new SFML.Graphics.RenderTexture(PREVIEW_WIDTH, PREVIEW_HEIGHT);
|
||||
tex.SetView(GetInitView(PREVIEW_WIDTH, PREVIEW_HEIGHT));
|
||||
tex.SetView(InitBounds.GetView(PREVIEW_WIDTH, PREVIEW_HEIGHT));
|
||||
tex.Clear(SFML.Graphics.Color.Transparent);
|
||||
var tmp = CurrentAnimation;
|
||||
CurrentAnimation = EMPTY_ANIMATION;
|
||||
@@ -373,7 +380,7 @@ namespace SpineViewer.Spine
|
||||
using var img = tex.Texture.CopyToImage();
|
||||
img.SaveToMemory(out var imgBuffer, "bmp");
|
||||
using var stream = new MemoryStream(imgBuffer);
|
||||
preview = new Bitmap(stream);
|
||||
preview = new Bitmap(new Bitmap(stream)); // 必须重复构造一个副本才能摆脱对流的依赖, 否则之后使用会报错
|
||||
}
|
||||
return preview;
|
||||
}
|
||||
@@ -391,53 +398,6 @@ namespace SpineViewer.Spine
|
||||
/// <param name="delta">时间间隔</param>
|
||||
public abstract void Update(float delta);
|
||||
|
||||
/// <summary>
|
||||
/// 获取初始状态下合适的 View, 参数单位为像素
|
||||
/// </summary>
|
||||
public SFML.Graphics.View GetInitView(Size resolution, Padding padding) =>
|
||||
GetInitView((uint)resolution.Width, (uint)resolution.Height, (uint)padding.Left, (uint)padding.Right, (uint)padding.Top, (uint)padding.Bottom);
|
||||
|
||||
/// <summary>
|
||||
/// 获取初始状态下合适的 View, 参数单位为像素
|
||||
/// </summary>
|
||||
public SFML.Graphics.View GetInitView(uint width, uint height, Padding padding) =>
|
||||
GetInitView(width, height, (uint)padding.Left, (uint)padding.Right, (uint)padding.Top, (uint)padding.Bottom);
|
||||
|
||||
/// <summary>
|
||||
/// 获取初始状态下合适的 View, 参数单位为像素
|
||||
/// </summary>
|
||||
public SFML.Graphics.View GetInitView(Size resolution, uint paddingL = 1, uint paddingR = 1, uint paddingT = 1, uint paddingB = 1) =>
|
||||
GetInitView((uint)resolution.Width, (uint)resolution.Height, paddingL, paddingR, paddingT, paddingB);
|
||||
|
||||
/// <summary>
|
||||
/// 获取初始状态下合适的 View, 参数单位为像素
|
||||
/// </summary>
|
||||
public SFML.Graphics.View GetInitView(uint width, uint height, uint paddingL = 1, uint paddingR = 1, uint paddingT = 1, uint paddingB = 1)
|
||||
{
|
||||
var tmp = CurrentAnimation;
|
||||
CurrentAnimation = EMPTY_ANIMATION;
|
||||
var bounds = Bounds;
|
||||
CurrentAnimation = tmp;
|
||||
|
||||
float sizeX = bounds.Width;
|
||||
float sizeY = bounds.Height;
|
||||
float innerW = width - paddingL - paddingR;
|
||||
float innerH = height - paddingT - paddingB;
|
||||
|
||||
float scale = 1;
|
||||
if ((sizeY / sizeX) < (innerH / innerW))
|
||||
scale = sizeX / innerW; // 相同的 X, 视窗 Y 更大
|
||||
else
|
||||
scale = sizeY / innerH; // 相同的 Y, 视窗 X 更大
|
||||
|
||||
var x = bounds.X + bounds.Width / 2 + ((float)paddingL - (float)paddingR) * scale;
|
||||
var y = bounds.Y + bounds.Height / 2 + ((float)paddingT - (float)paddingB) * scale;
|
||||
var viewX = width * scale;
|
||||
var viewY = height * scale;
|
||||
|
||||
return new(new(x, y), new(viewX, -viewY));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 是否被选中
|
||||
/// </summary>
|
||||
|
||||
39
SpineViewer/Spine/UITypeEditor.cs
Normal file
39
SpineViewer/Spine/UITypeEditor.cs
Normal file
@@ -0,0 +1,39 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using System.Drawing.Design;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows.Forms.Design;
|
||||
|
||||
namespace SpineViewer.Spine
|
||||
{
|
||||
/// <summary>
|
||||
/// skel 文件路径编辑器
|
||||
/// </summary>
|
||||
public class SkelFileNameEditor : FileNameEditor
|
||||
{
|
||||
protected override void InitializeDialog(OpenFileDialog openFileDialog)
|
||||
{
|
||||
base.InitializeDialog(openFileDialog);
|
||||
openFileDialog.Title = "选择 skel 文件";
|
||||
openFileDialog.AddExtension = false;
|
||||
openFileDialog.Filter = "skel 文件 (*.skel; *.json)|*.skel;*.json|二进制文件 (*.skel)|*.skel|文本文件 (*.json)|*.json|所有文件 (*.*)|*.*";
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// atlas 文件路径编辑器
|
||||
/// </summary>
|
||||
public class AtlasFileNameEditor : FileNameEditor
|
||||
{
|
||||
protected override void InitializeDialog(OpenFileDialog openFileDialog)
|
||||
{
|
||||
base.InitializeDialog(openFileDialog);
|
||||
openFileDialog.Title = "选择 atlas 文件";
|
||||
openFileDialog.AddExtension = false;
|
||||
openFileDialog.Filter = "atlas 文件 (*.atlas)|*.atlas|所有文件 (*.*)|*.*";
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -9,6 +9,36 @@ using System.Threading.Tasks;
|
||||
|
||||
namespace SpineViewer.Spine
|
||||
{
|
||||
/// <summary>
|
||||
/// 支持的 Spine 版本
|
||||
/// </summary>
|
||||
public enum Version
|
||||
{
|
||||
[Description("<Auto>")] Auto = 0x0000,
|
||||
[Description("2.1.x")] V21 = 0x0201,
|
||||
[Description("3.6.x")] V36 = 0x0306,
|
||||
[Description("3.7.x")] V37 = 0x0307,
|
||||
[Description("3.8.x")] V38 = 0x0308,
|
||||
[Description("4.0.x")] V40 = 0x0400,
|
||||
[Description("4.1.x")] V41 = 0x0401,
|
||||
[Description("4.2.x")] V42 = 0x0402,
|
||||
[Description("4.3.x")] V43 = 0x0403,
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Spine 实现类标记
|
||||
/// </summary>
|
||||
[AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = false)]
|
||||
public class SpineImplementationAttribute : Attribute
|
||||
{
|
||||
public Version Version { get; }
|
||||
|
||||
public SpineImplementationAttribute(Version version)
|
||||
{
|
||||
Version = version;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Spine 版本静态辅助类
|
||||
/// </summary>
|
||||
@@ -61,20 +91,4 @@ namespace SpineViewer.Spine
|
||||
return runtimes.TryGetValue(version, out var val) ? val : GetName(version);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 支持的 Spine 版本
|
||||
/// </summary>
|
||||
public enum Version
|
||||
{
|
||||
[Description("<Auto>")] Auto = 0x0000,
|
||||
[Description("2.1.x")] V21 = 0x0201,
|
||||
[Description("3.6.x")] V36 = 0x0306,
|
||||
[Description("3.7.x")] V37 = 0x0307,
|
||||
[Description("3.8.x")] V38 = 0x0308,
|
||||
[Description("4.0.x")] V40 = 0x0400,
|
||||
[Description("4.1.x")] V41 = 0x0401,
|
||||
[Description("4.2.x")] V42 = 0x0402,
|
||||
[Description("4.3.x")] V43 = 0x0403,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,10 +8,11 @@
|
||||
<RuntimeIdentifier>win-x64</RuntimeIdentifier>
|
||||
<BaseOutputPath>$(SolutionDir)out</BaseOutputPath>
|
||||
<IncludeSourceRevisionInInformationalVersion>false</IncludeSourceRevisionInInformationalVersion>
|
||||
<Version>0.10.9</Version>
|
||||
<Version>0.11.1</Version>
|
||||
<OutputType>WinExe</OutputType>
|
||||
<UseWindowsForms>true</UseWindowsForms>
|
||||
<ApplicationIcon>appicon.ico</ApplicationIcon>
|
||||
<GenerateResourceWarnOnBinaryFormatterUse>false</GenerateResourceWarnOnBinaryFormatterUse>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
@@ -9,7 +9,7 @@ using System.Threading.Tasks;
|
||||
|
||||
namespace SpineViewer
|
||||
{
|
||||
public class PointFConverter : ExpandableObjectConverter
|
||||
public class PointFConverter : TypeConverter
|
||||
{
|
||||
public override bool CanConvertTo(ITypeDescriptorContext? context, [NotNullWhen(true)] Type? destinationType)
|
||||
{
|
||||
|
||||
38
SpineViewer/UITypeEditor.cs
Normal file
38
SpineViewer/UITypeEditor.cs
Normal file
@@ -0,0 +1,38 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using System.Drawing.Design;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows.Forms.Design;
|
||||
|
||||
namespace SpineViewer
|
||||
{
|
||||
/// <summary>
|
||||
/// 使用 FolderBrowserDialog 的文件夹路径编辑器
|
||||
/// </summary>
|
||||
public class FolderNameEditor : UITypeEditor
|
||||
{
|
||||
public override UITypeEditorEditStyle GetEditStyle(ITypeDescriptorContext? context)
|
||||
{
|
||||
// 指定编辑风格为 Modal 对话框, 提供右边用来点击的按钮
|
||||
return UITypeEditorEditStyle.Modal;
|
||||
}
|
||||
|
||||
public override object? EditValue(ITypeDescriptorContext? context, IServiceProvider provider, object? value)
|
||||
{
|
||||
// 重写 EditValue 方法,提供自定义的文件夹选择对话框逻辑
|
||||
using var dialog = new FolderBrowserDialog();
|
||||
|
||||
// 如果当前值为有效路径,则设置为初始选中路径
|
||||
if (value is string currentPath && Directory.Exists(currentPath))
|
||||
dialog.SelectedPath = currentPath;
|
||||
|
||||
if (dialog.ShowDialog() == DialogResult.OK)
|
||||
value = dialog.SelectedPath;
|
||||
|
||||
return value;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user