Compare commits

..

44 Commits

Author SHA1 Message Date
ww-rm
1fec65b37d 更新至v0.11.5 2025-03-31 17:42:54 +08:00
ww-rm
9498e8f334 update readme 2025-03-31 17:42:44 +08:00
ww-rm
83b8411929 update changelog 2025-03-31 17:34:48 +08:00
ww-rm
e9accd13b3 增加所有导出格式 2025-03-31 17:30:20 +08:00
ww-rm
9e27a19258 允许多标记 2025-03-31 14:37:47 +08:00
ww-rm
252f3a5bea 优化显示 2025-03-31 02:07:02 +08:00
ww-rm
e0626bb126 增加项数显示 2025-03-31 01:58:30 +08:00
ww-rm
7ff62c7f40 增加错误日志 2025-03-31 01:45:16 +08:00
ww-rm
4b07e02acb 增加线程安全 2025-03-31 01:44:41 +08:00
ww-rm
4654d1d9c2 优化多项操作卡顿问题 2025-03-30 20:52:22 +08:00
ww-rm
ce1f75e8a5 增加报错调试 2025-03-30 20:07:29 +08:00
ww-rm
4d9aebc758 修复预览图不显示问题 2025-03-30 20:06:13 +08:00
ww-rm
e814368ef3 移除rid 2025-03-30 19:30:58 +08:00
ww-rm
bbbb02500f 增加StringEnumConverter 2025-03-30 17:26:17 +08:00
ww-rm
404f255f14 update readme 2025-03-30 15:22:48 +08:00
ww-rm
7a15e0d38a 隐藏不可见成员 2025-03-30 13:54:17 +08:00
ww-rm
bfe669bdd9 增加FFMpegCore版本信息 2025-03-30 12:07:28 +08:00
ww-rm
c0553042fd 更新至v0.11.4 2025-03-30 11:59:52 +08:00
ww-rm
af8b02654b update readme 2025-03-30 11:59:37 +08:00
ww-rm
4779ec91d0 update changelog 2025-03-30 11:59:13 +08:00
ww-rm
14d7f4af0e 增加MP4导出格式 2025-03-30 11:56:20 +08:00
ww-rm
f9888b23dd 设置GIF默认背景颜色为纯白透明背景 2025-03-30 11:56:06 +08:00
ww-rm
411cdbb00f 设置默认颜色为纯黑透明背景 2025-03-30 11:55:52 +08:00
ww-rm
d859f07469 增加导出时输出ffmpeg参数 2025-03-29 23:48:56 +08:00
ww-rm
c111819093 增加背景颜色参数 2025-03-29 22:17:08 +08:00
ww-rm
aa8321d13c 整理代码结构 2025-03-29 21:15:34 +08:00
ww-rm
5e3bd972e5 移动GetVersion至SpineHelper 2025-03-29 17:04:26 +08:00
ww-rm
ad39a04fff 重命名SpineVersion 2025-03-29 16:59:28 +08:00
ww-rm
9a97e84296 解耦对MessageBox的依赖,提供单独的Shader初始化函数 2025-03-29 16:51:05 +08:00
ww-rm
1b7b0dcb13 解耦日志器 2025-03-29 16:30:32 +08:00
ww-rm
d365a5060b small change 2025-03-29 15:34:56 +08:00
ww-rm
b69589394a 提取ImplementationResolver实现 2025-03-29 15:12:50 +08:00
ww-rm
00f5791766 增加导出时任务栏图标显示 2025-03-28 20:53:48 +08:00
ww-rm
38cab2eda7 修复可能的预览图资源泄漏 2025-03-27 23:31:03 +08:00
ww-rm
0db4d6e4e0 small change 2025-03-27 19:45:56 +08:00
ww-rm
549712962f 去除多余组件 2025-03-27 10:11:44 +08:00
ww-rm
34b7002faf 增加背景颜色选项 2025-03-27 10:08:16 +08:00
ww-rm
0e6f47b23c 预修改适配对多轨道动画 2025-03-27 09:56:21 +08:00
ww-rm
a372a89b5e 增加update0 2025-03-27 09:09:22 +08:00
ww-rm
239847aee7 皮肤更换后使用SetSlotsToSetupPose而不是SetToSetupPose 2025-03-27 09:03:09 +08:00
ww-rm
813249c6a7 调整布局 2025-03-26 21:09:52 +08:00
ww-rm
293ab28bce 更新预览图 2025-03-26 20:49:22 +08:00
ww-rm
98e73cdec5 调整面板比例 2025-03-26 20:48:23 +08:00
ww-rm
6d34bb9d25 移除无用引用 2025-03-26 20:37:27 +08:00
66 changed files with 1921 additions and 1283 deletions

View File

@@ -1,5 +1,21 @@
# CHANGELOG
## v0.11.5
- 导出格式全面支持
- 修复预览图不显示的问题
- 优化列表卡顿问题
- 模型列表增加数量显示
## v0.11.4
- 增加 MP4 导出格式
- 增加导出背景颜色参数
- 增加日志输出 FFMpeg 参数字符串
- 增加导出时任务栏图标执行动效
- 修复预览面板移动模型时物理效果不同步的问题
- 优化部分使用体验
## v0.11.3
- 增加模型隐藏设置属性

View File

@@ -4,7 +4,7 @@
[中文](README.md) | [English](README.en.md)
A *WYSIWYG* Spine file viewer and exporter.
*A WYSIWYG Spine file viewer and exporter.*
![previewer](img/preview.webp)
@@ -12,77 +12,80 @@ A *WYSIWYG* Spine file viewer and exporter.
## Installation
Go to the [Release](https://github.com/ww-rm/SpineViewer/releases) page to download the zip package.
Head over to the [Release](https://github.com/ww-rm/SpineViewer/releases) page to download the zip package.
The software requires the dependency framework [.NET Desktop Runtime 8.0.x](https://dotnet.microsoft.com/zh-cn/download/dotnet/8.0).
The software requires the dependency framework [.NET Desktop Runtime 8.0.x](https://dotnet.microsoft.com/en-us/download/dotnet/8.0).
You can also download the zip package with the `SelfContained` suffix, which can run independently.
Alternatively, you can download the package with the `SelfContained` suffix, which can run independently.
Exporting video formats such as GIF requires that ffmpeg is installed locally and added to your systems PATH. You can [click here to go to the FFmpeg-Windows download page](https://ffmpeg.org/download.html#build-windows) or directly download the latest version [ffmpeg-release-full.7z](https://www.gyan.dev/ffmpeg/builds/ffmpeg-release-full.7z).
## Supported Export Formats
- [x] Single Frame Image
- [x] Frame Sequence
- [x] Animated GIF
- [ ] MKV
- [ ] MP4
- [ ] MOV
- [ ] WebM
More formats are under development :rocket::rocket::rocket:
| Export Format | Suitable for Scenario |
| :------------: | :------------------------------------------------------------------------------------:|
| Single Frame | Supports generating high-definition model snapshots; you can manually adjust the frame. |
| Frame Sequence | Supports png sequence output with transparency and lossless compression. |
| GIF | Ideal for generating preview animations. |
| MP4 | The most common video format with the best compatibility. |
| WebM | Suitable for browser-based playback and supports transparent backgrounds. |
| MKV | For more experimental use. |
| MOV | For more experimental use. |
| Custom Export | In addition to the above presets, you can provide any FFmpeg parameters to meet complex custom needs. |
## Supported Spine Versions
| Version | View & Export | Format Conversion | Version Conversion |
| :-------: | :-----------: | :---------------: | :----------------: |
| `2.1.x` | :white_check_mark: | | |
| `3.1.x` | | | |
| `3.4.x` | | | |
| `3.5.x` | | | |
| `3.6.x` | :white_check_mark: | | |
| `3.7.x` | :white_check_mark: | | |
| `3.8.x` | :white_check_mark: | :white_check_mark: | |
| `4.1.x` | :white_check_mark: | | |
| `4.2.x` | :white_check_mark: | | |
| `4.3.x` | | | |
| Version | View & Export | Format Conversion | Version Conversion |
| :------: | :-------------------: | :------------------: | :-----------------: |
| `2.1.x` | :white_check_mark: | | |
| `3.1.x` | | | |
| `3.4.x` | | | |
| `3.5.x` | | | |
| `3.6.x` | :white_check_mark: | | |
| `3.7.x` | :white_check_mark: | | |
| `3.8.x` | :white_check_mark: | :white_check_mark: | |
| `4.1.x` | :white_check_mark: | | |
| `4.2.x` | :white_check_mark: | | |
| `4.3.x` | | | |
More versions are under development :rocket::rocket::rocket:
More versions are under development :rocket: :rocket: :rocket:
## Usage
## How to Use
### Importing Skeletons
### Importing Skeleton Files
There are three ways to import skeleton files:
- Drag and drop or paste the skeleton file/directory into the model list.
- Open skeleton files in batch from the File menu.
- Batch open skeleton files from the File menu.
- Select a single model to open from the File menu.
### Adjusting Preview Content
### Adjusting the Preview
The model list supports right-click menus and several hotkeys, and multiple models can be selected for batch adjustments of model parameters.
The model list supports context menus and some shortcuts, and you can multi-select to adjust parameters in bulk.
In addition to using the control panel for parameter settings, the preview window supports the following mouse actions:
In addition to using the panel for parameter settings, the preview screen supports several mouse actions:
- Left-click to select and drag models. Hold the `Ctrl` key to enable multi-selection, which is synchronized with the model list on the left.
- Left-click to select and drag models; hold the `Ctrl` key for multi-selection (which is synchronized with the list on the left).
- Right-click to drag the overall view.
- Use the scroll wheel to zoom in/out.
- "Render selected only" mode, in which the preview only includes selected models and the selection can only be changed via the model list on the left.
- Use the mouse wheel to zoom in and out.
- Render Selected mode: in this mode, the preview screen only shows the selected models and the selection state can only be changed from the list on the left.
The buttons below the preview window allow you to adjust the timeline, effectively serving as a simple player.
The buttons below the preview allow you to adjust the timeline, acting as a simple media player.
### Exporting Preview Content
### Exporting the Preview
Export follows the "What You See Is What You Get" principle—what you see in the live preview is exactly what gets exported.
Exporting follows the What You See Is What You Get principle the preview exactly reflects the output.
There are a few key parameters for exporting:
There are several key parameters for export:
- Render Selected Only: This option not only affects the preview mode but also the export; if enabled, only the selected models will be considered, and all other models will be ignored during export.
- Output Folder: This parameter is optional in some cases. If not provided, the output will be saved in each model's own directory. Otherwise, all output files will be saved to the specified folder.
- Single Export: By default, each model is exported individually in batch mode. If "Single Export" is selected, all exported models will be rendered on a single canvas, resulting in only one output file.
- Render Selected Only: This option affects both the preview and export. If enabled, only the selected models will be considered during export while ignoring the others.
- Output Folder: This parameter is optional in some cases. If not provided, the output files will be saved in each models own folder; otherwise, all outputs will be saved to the specified folder.
- Single Export: By default, each model is exported separately (i.e., batch operation on the model list). If Single Export is selected, all the exported models will be rendered on the same canvas, producing only one output file.
### More Information
For more detailed instructions and usage, please refer to the [Wiki](https://github.com/ww-rm/SpineViewer/wiki). If you encounter any issues or bugs, please open an [Issue](https://github.com/ww-rm/SpineViewer/issues).
For detailed instructions and usage notes, please see the [Wiki](https://github.com/ww-rm/SpineViewer/wiki). If you encounter any issues or bugs, feel free to open an [Issue](https://github.com/ww-rm/SpineViewer/issues).
## Acknowledgements
@@ -92,6 +95,6 @@ For more detailed instructions and usage, please refer to the [Wiki](https://git
---
*If you like this project, please give it a :star: and share it with others! :)*
*If you like this project, please give it a :star: and share it with others!*
[![Stargazers over time](https://starchart.cc/ww-rm/SpineViewer.svg?variant=adaptive)](https://starchart.cc/ww-rm/SpineViewer)
[![Stargazers over time](https://starchart.cc/ww-rm/SpineViewer.svg?variant=adaptive)](https://starchart.cc/ww-rm/SpineViewer)

View File

@@ -18,17 +18,20 @@
也可以下载带有 `SelfContained` 后缀的压缩包, 可以独立运行.
导出 GIF 等视频格式需要在本地安装 ffmpeg 命令行, 并且添加至环境变量, [点击前往 FFmpeg-Windows 下载页面](https://ffmpeg.org/download.html#build-windows), 也可以点这个下载最新版本 [ffmpeg-release-full.7z](https://www.gyan.dev/ffmpeg/builds/ffmpeg-release-full.7z).
## 导出格式支持
- [x] 单帧画面
- [x] 帧序列
- [x] GIF 动图
- [ ] MKV
- [ ] MP4
- [ ] MOV
- [ ] WebM
更多格式正在施工 :rocket::rocket::rocket:
| 导出格式 | 适用场景 |
| :---: | :---: |
| 单帧画面 | 支持生成高清模型画面图像, 可手动调节需要的一帧. |
| 帧序列 | 支持 png 格式帧序列, 可保留透明通道且无损压缩. |
| GIF | 适合生成预览动图. |
| MP4 | 最常见的视频格式, 兼容性最好. |
| WebM | 适合浏览器在线播放格式, 支持透明背景. |
| MKV | 适合折腾. |
| MOV | 适合折腾. |
| 自定义导出 | 除上述预设方案, 支持提供任意 FFmpeg 参数进行导出, 满足自定义复杂需求. |
## Spine 版本支持
@@ -45,7 +48,7 @@
| `4.2.x` | :white_check_mark: | | |
| `4.3.x` | | | |
更多版本正在施工 :rocket::rocket::rocket:
更多版本正在施工 :rocket: :rocket: :rocket:
## 使用方法

View File

@@ -5,7 +5,6 @@
<Nullable>enable</Nullable>
<Platforms>x64</Platforms>
<TargetFramework>net8.0-windows</TargetFramework>
<RuntimeIdentifier>win-x64</RuntimeIdentifier>
<BaseOutputPath>$(SolutionDir)out</BaseOutputPath>
<IncludeSourceRevisionInInformationalVersion>false</IncludeSourceRevisionInInformationalVersion>
<Version>2.1.25</Version>

View File

@@ -5,7 +5,6 @@
<Nullable>enable</Nullable>
<Platforms>x64</Platforms>
<TargetFramework>net8.0-windows</TargetFramework>
<RuntimeIdentifier>win-x64</RuntimeIdentifier>
<BaseOutputPath>$(SolutionDir)out</BaseOutputPath>
<IncludeSourceRevisionInInformationalVersion>false</IncludeSourceRevisionInInformationalVersion>
<Version>3.6.53</Version>

View File

@@ -5,7 +5,6 @@
<Nullable>enable</Nullable>
<Platforms>x64</Platforms>
<TargetFramework>net8.0-windows</TargetFramework>
<RuntimeIdentifier>win-x64</RuntimeIdentifier>
<BaseOutputPath>$(SolutionDir)out</BaseOutputPath>
<IncludeSourceRevisionInInformationalVersion>false</IncludeSourceRevisionInInformationalVersion>
<Version>3.7.94</Version>

View File

@@ -5,7 +5,6 @@
<Nullable>enable</Nullable>
<Platforms>x64</Platforms>
<TargetFramework>net8.0-windows</TargetFramework>
<RuntimeIdentifier>win-x64</RuntimeIdentifier>
<BaseOutputPath>$(SolutionDir)out</BaseOutputPath>
<IncludeSourceRevisionInInformationalVersion>false</IncludeSourceRevisionInInformationalVersion>
<Version>3.8.99</Version>

View File

@@ -5,7 +5,6 @@
<Nullable>enable</Nullable>
<Platforms>x64</Platforms>
<TargetFramework>net8.0-windows</TargetFramework>
<RuntimeIdentifier>win-x64</RuntimeIdentifier>
<BaseOutputPath>$(SolutionDir)out</BaseOutputPath>
<IncludeSourceRevisionInInformationalVersion>false</IncludeSourceRevisionInInformationalVersion>
<Version>4.0.64</Version>

View File

@@ -5,7 +5,6 @@
<Nullable>enable</Nullable>
<Platforms>x64</Platforms>
<TargetFramework>net8.0-windows</TargetFramework>
<RuntimeIdentifier>win-x64</RuntimeIdentifier>
<BaseOutputPath>$(SolutionDir)out</BaseOutputPath>
<IncludeSourceRevisionInInformationalVersion>false</IncludeSourceRevisionInInformationalVersion>
<Version>4.1.54</Version>

View File

@@ -5,7 +5,6 @@
<Nullable>enable</Nullable>
<Platforms>x64</Platforms>
<TargetFramework>net8.0-windows</TargetFramework>
<RuntimeIdentifier>win-x64</RuntimeIdentifier>
<BaseOutputPath>$(SolutionDir)out</BaseOutputPath>
<IncludeSourceRevisionInInformationalVersion>false</IncludeSourceRevisionInInformationalVersion>
<Version>4.2.74</Version>

View File

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

View File

@@ -44,8 +44,9 @@
toolStripMenuItem_MoveTop = new ToolStripMenuItem();
toolStripMenuItem_MoveBottom = new ToolStripMenuItem();
toolStripSeparator3 = new ToolStripSeparator();
toolStripMenuItem_SelectAll = new ToolStripMenuItem();
toolStripMenuItem_CopyPreview = new ToolStripMenuItem();
toolStripMenuItem_AddFromClipboard = new ToolStripMenuItem();
toolStripMenuItem_SelectAll = new ToolStripMenuItem();
toolStripSeparator4 = new ToolStripSeparator();
toolStripMenuItem_ChangeView = new ToolStripMenuItem();
toolStripMenuItem_LargeIconView = new ToolStripMenuItem();
@@ -53,8 +54,13 @@
toolStripMenuItem_DetailsView = new ToolStripMenuItem();
imageList_LargeIcon = new ImageList(components);
imageList_SmallIcon = new ImageList(components);
toolStripMenuItem_AddFromClipboard = new ToolStripMenuItem();
timer_SelectedIndexChangedDebounce = new System.Windows.Forms.Timer(components);
statusStrip = new StatusStrip();
toolStripStatusLabel_CountInfo = new ToolStripStatusLabel();
tableLayoutPanel = new TableLayoutPanel();
contextMenuStrip.SuspendLayout();
statusStrip.SuspendLayout();
tableLayoutPanel.SuspendLayout();
SuspendLayout();
//
// listView
@@ -68,9 +74,10 @@
listView.GridLines = true;
listView.LargeImageList = imageList_LargeIcon;
listView.Location = new Point(0, 0);
listView.Margin = new Padding(0);
listView.Name = "listView";
listView.ShowItemToolTips = true;
listView.Size = new Size(336, 445);
listView.Size = new Size(336, 414);
listView.SmallImageList = imageList_SmallIcon;
listView.TabIndex = 1;
listView.UseCompatibleStateImageBehavior = false;
@@ -84,14 +91,14 @@
// columnHeader_Name
//
columnHeader_Name.Text = "名称";
columnHeader_Name.Width = 220;
columnHeader_Name.Width = 300;
//
// contextMenuStrip
//
contextMenuStrip.ImageScalingSize = new Size(24, 24);
contextMenuStrip.Items.AddRange(new ToolStripItem[] { toolStripMenuItem_Add, toolStripMenuItem_Insert, toolStripMenuItem_Remove, toolStripSeparator1, toolStripMenuItem_BatchAdd, toolStripMenuItem_RemoveAll, toolStripSeparator2, toolStripMenuItem_MoveUp, toolStripMenuItem_MoveDown, toolStripMenuItem_MoveTop, toolStripMenuItem_MoveBottom, toolStripSeparator3, toolStripMenuItem_CopyPreview, toolStripMenuItem_AddFromClipboard, toolStripMenuItem_SelectAll, toolStripSeparator4, toolStripMenuItem_ChangeView });
contextMenuStrip.Name = "contextMenuStrip";
contextMenuStrip.Size = new Size(329, 451);
contextMenuStrip.Size = new Size(329, 418);
contextMenuStrip.Closed += contextMenuStrip_Closed;
contextMenuStrip.Opening += contextMenuStrip_Opening;
//
@@ -178,14 +185,6 @@
toolStripSeparator3.Name = "toolStripSeparator3";
toolStripSeparator3.Size = new Size(325, 6);
//
// toolStripMenuItem_SelectAll
//
toolStripMenuItem_SelectAll.Name = "toolStripMenuItem_SelectAll";
toolStripMenuItem_SelectAll.ShortcutKeys = Keys.Control | Keys.A;
toolStripMenuItem_SelectAll.Size = new Size(328, 30);
toolStripMenuItem_SelectAll.Text = "全选";
toolStripMenuItem_SelectAll.Click += toolStripMenuItem_SelectAll_Click;
//
// toolStripMenuItem_CopyPreview
//
toolStripMenuItem_CopyPreview.Name = "toolStripMenuItem_CopyPreview";
@@ -194,6 +193,22 @@
toolStripMenuItem_CopyPreview.Text = "复制预览图 (256x256)";
toolStripMenuItem_CopyPreview.Click += toolStripMenuItem_CopyPreview_Click;
//
// toolStripMenuItem_AddFromClipboard
//
toolStripMenuItem_AddFromClipboard.Name = "toolStripMenuItem_AddFromClipboard";
toolStripMenuItem_AddFromClipboard.ShortcutKeys = Keys.Control | Keys.V;
toolStripMenuItem_AddFromClipboard.Size = new Size(328, 30);
toolStripMenuItem_AddFromClipboard.Text = "从剪贴板添加";
toolStripMenuItem_AddFromClipboard.Click += toolStripMenuItem_AddFromClipboard_Click;
//
// toolStripMenuItem_SelectAll
//
toolStripMenuItem_SelectAll.Name = "toolStripMenuItem_SelectAll";
toolStripMenuItem_SelectAll.ShortcutKeys = Keys.Control | Keys.A;
toolStripMenuItem_SelectAll.Size = new Size(328, 30);
toolStripMenuItem_SelectAll.Text = "全选";
toolStripMenuItem_SelectAll.Click += toolStripMenuItem_SelectAll_Click;
//
// toolStripSeparator4
//
toolStripSeparator4.Name = "toolStripSeparator4";
@@ -242,22 +257,56 @@
imageList_SmallIcon.ImageSize = new Size(48, 48);
imageList_SmallIcon.TransparentColor = Color.Transparent;
//
// toolStripMenuItem_AddFromClipboard
// timer_SelectedIndexChangedDebounce
//
toolStripMenuItem_AddFromClipboard.Name = "toolStripMenuItem_AddFromClipboard";
toolStripMenuItem_AddFromClipboard.ShortcutKeys = Keys.Control | Keys.V;
toolStripMenuItem_AddFromClipboard.Size = new Size(328, 30);
toolStripMenuItem_AddFromClipboard.Text = "从剪贴板添加";
toolStripMenuItem_AddFromClipboard.Click += toolStripMenuItem_AddFromClipboard_Click;
timer_SelectedIndexChangedDebounce.Interval = 30;
timer_SelectedIndexChangedDebounce.Tick += timer_SelectedIndexChangedDebounce_Tick;
//
// statusStrip
//
statusStrip.Dock = DockStyle.Fill;
statusStrip.ImageScalingSize = new Size(24, 24);
statusStrip.Items.AddRange(new ToolStripItem[] { toolStripStatusLabel_CountInfo });
statusStrip.Location = new Point(0, 414);
statusStrip.Name = "statusStrip";
statusStrip.Size = new Size(336, 31);
statusStrip.SizingGrip = false;
statusStrip.TabIndex = 2;
statusStrip.Text = "statusStrip1";
//
// toolStripStatusLabel_CountInfo
//
toolStripStatusLabel_CountInfo.Name = "toolStripStatusLabel_CountInfo";
toolStripStatusLabel_CountInfo.Size = new Size(178, 24);
toolStripStatusLabel_CountInfo.Text = "已选择 0 项,共 0 项";
//
// tableLayoutPanel
//
tableLayoutPanel.ColumnCount = 1;
tableLayoutPanel.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 100F));
tableLayoutPanel.Controls.Add(listView, 0, 0);
tableLayoutPanel.Controls.Add(statusStrip, 0, 1);
tableLayoutPanel.Dock = DockStyle.Fill;
tableLayoutPanel.Location = new Point(0, 0);
tableLayoutPanel.Name = "tableLayoutPanel";
tableLayoutPanel.RowCount = 2;
tableLayoutPanel.RowStyles.Add(new RowStyle(SizeType.Percent, 100F));
tableLayoutPanel.RowStyles.Add(new RowStyle());
tableLayoutPanel.Size = new Size(336, 445);
tableLayoutPanel.TabIndex = 3;
//
// SpineListView
//
AutoScaleDimensions = new SizeF(11F, 24F);
AutoScaleMode = AutoScaleMode.Font;
Controls.Add(listView);
Controls.Add(tableLayoutPanel);
Name = "SpineListView";
Size = new Size(336, 445);
contextMenuStrip.ResumeLayout(false);
statusStrip.ResumeLayout(false);
statusStrip.PerformLayout();
tableLayoutPanel.ResumeLayout(false);
tableLayoutPanel.PerformLayout();
ResumeLayout(false);
}
@@ -287,5 +336,9 @@
private ToolStripMenuItem toolStripMenuItem_SelectAll;
private ToolStripSeparator toolStripSeparator4;
private ToolStripMenuItem toolStripMenuItem_AddFromClipboard;
private System.Windows.Forms.Timer timer_SelectedIndexChangedDebounce;
private StatusStrip statusStrip;
private ToolStripStatusLabel toolStripStatusLabel_CountInfo;
private TableLayoutPanel tableLayoutPanel;
}
}

View File

@@ -12,16 +12,11 @@ using SpineViewer.Spine;
using System.Reflection;
using System.Diagnostics;
using System.Collections.Specialized;
using NLog;
namespace SpineViewer.Controls
{
public partial class SpineListView : UserControl
{
/// <summary>
/// 显示骨骼信息的属性面板
/// </summary>
[Category("自定义"), Description("用于显示骨骼属性的属性页")]
public PropertyGrid? PropertyGrid { get; set; }
/// <summary>
/// Spine 列表只读视图, 访问时必须使用 lock 语句锁定视图本身
/// </summary>
@@ -32,12 +27,23 @@ namespace SpineViewer.Controls
/// </summary>
private readonly List<Spine.Spine> spines = [];
/// <summary>
/// 日志器
/// </summary>
protected readonly Logger logger = LogManager.GetCurrentClassLogger();
public SpineListView()
{
InitializeComponent();
Spines = spines.AsReadOnly();
}
/// <summary>
/// 显示骨骼信息的属性面板
/// </summary>
[Category("自定义"), Description("用于显示骨骼属性的属性页")]
public PropertyGrid? PropertyGrid { get; set; }
/// <summary>
/// 选中的索引
/// </summary>
@@ -87,12 +93,12 @@ namespace SpineViewer.Controls
}
catch (Exception ex)
{
Program.Logger.Error(ex.ToString());
Program.Logger.Error("Failed to load {} {}", result.SkelPath, result.AtlasPath);
logger.Error(ex.ToString());
logger.Error("Failed to load {} {}", result.SkelPath, result.AtlasPath);
MessageBox.Error(ex.ToString(), "骨骼加载失败");
}
Program.LogCurrentMemoryUsage();
logger.LogCurrentProcessMemoryUsage();
}
/// <summary>
@@ -109,7 +115,7 @@ namespace SpineViewer.Controls
/// <summary>
/// 从结果批量添加
/// </summary>
public void BatchAdd(Dialogs.BatchOpenSpineDialogResult result)
private void BatchAdd(Dialogs.BatchOpenSpineDialogResult result)
{
var progressDialog = new Dialogs.ProgressDialog();
progressDialog.DoWork += BatchAdd_Work;
@@ -157,8 +163,8 @@ namespace SpineViewer.Controls
}
catch (Exception ex)
{
Program.Logger.Error(ex.ToString());
Program.Logger.Error("Failed to load {}", skelPath);
logger.Error(ex.ToString());
logger.Error("Failed to load {}", skelPath);
error++;
}
@@ -176,11 +182,11 @@ namespace SpineViewer.Controls
});
if (error > 0)
Program.Logger.Warn("Batch load {} successfully, {} failed", success, error);
logger.Warn("Batch load {} successfully, {} failed", success, error);
else
Program.Logger.Info("{} skel loaded successfully", success);
logger.Info("{} skel loaded successfully", success);
Program.LogCurrentMemoryUsage();
logger.LogCurrentProcessMemoryUsage();
}
/// <summary>
@@ -193,14 +199,14 @@ namespace SpineViewer.Controls
{
if (File.Exists(path))
{
if (Spine.Spine.CommonSkelSuffix.Contains(Path.GetExtension(path).ToLower()))
if (SpineHelper.CommonSkelSuffix.Contains(Path.GetExtension(path).ToLower()))
validPaths.Add(path);
}
else if (Directory.Exists(path))
{
foreach (var file in Directory.EnumerateFiles(path, "*.*", SearchOption.AllDirectories))
{
if (Spine.Spine.CommonSkelSuffix.Contains(Path.GetExtension(file).ToLower()))
if (SpineHelper.CommonSkelSuffix.Contains(Path.GetExtension(file).ToLower()))
validPaths.Add(file);
}
}
@@ -213,15 +219,27 @@ namespace SpineViewer.Controls
if (MessageBox.Quest($"共发现 {validPaths.Count} 个可加载骨骼,数量较多,是否一次性全部加载?") == DialogResult.Cancel)
return;
}
BatchAdd(new Dialogs.BatchOpenSpineDialogResult(Spine.Version.Auto, validPaths.ToArray()));
BatchAdd(new Dialogs.BatchOpenSpineDialogResult(SpineVersion.Auto, validPaths.ToArray()));
}
else if (validPaths.Count > 0)
{
Insert(new Dialogs.OpenSpineDialogResult(Spine.Version.Auto, validPaths[0]));
Insert(new Dialogs.OpenSpineDialogResult(SpineVersion.Auto, validPaths[0]));
}
}
private void listView_SelectedIndexChanged(object sender, EventArgs e)
{
timer_SelectedIndexChangedDebounce.Stop();
timer_SelectedIndexChangedDebounce.Start();
}
private void timer_SelectedIndexChangedDebounce_Tick(object sender, EventArgs e)
{
timer_SelectedIndexChangedDebounce.Stop();
_listView_SelectedIndexChanged(listView, EventArgs.Empty);
}
private void _listView_SelectedIndexChanged(object sender, EventArgs e)
{
lock (Spines)
{
@@ -251,6 +269,8 @@ namespace SpineViewer.Controls
if (listView.SelectedItems.Count > 0)
listView.SelectedItems[0].EnsureVisible();
toolStripStatusLabel_CountInfo.Text = $"已选择 {listView.SelectedItems.Count} 项,共 {listView.Items.Count} 项";
}
private void listView_ItemDrag(object sender, ItemDragEventArgs e)
@@ -389,17 +409,19 @@ namespace SpineViewer.Controls
return;
}
foreach (var i in listView.SelectedIndices.Cast<int>().OrderByDescending(x => x))
lock (Spines)
{
listView.Items.RemoveAt(i);
lock (Spines)
listView.BeginUpdate();
foreach (var i in listView.SelectedIndices.Cast<int>().OrderByDescending(x => x))
{
listView.Items.RemoveAt(i);
var spine = spines[i];
spines.RemoveAt(i);
listView.SmallImageList.Images.RemoveByKey(spine.ID);
listView.LargeImageList.Images.RemoveByKey(spine.ID);
spine.Dispose();
}
listView.EndUpdate();
}
}
@@ -500,13 +522,16 @@ namespace SpineViewer.Controls
private void toolStripMenuItem_CopyPreview_Click(object sender, EventArgs e)
{
var fileDropList = new StringCollection();
var tempDir = Path.Combine(Path.GetTempPath(), Process.GetCurrentProcess().ProcessName);
Directory.CreateDirectory(tempDir);
lock (Spines)
{
foreach (int i in listView.SelectedIndices)
{
var a = Process.GetCurrentProcess();
var spine = spines[i];
var path = Path.Combine(Program.TempDir, $"{spine.ID}.png");
var path = Path.Combine(tempDir, $"{spine.ID}.png");
spine.Preview.Save(path);
fileDropList.Add(path);
}

View File

@@ -126,4 +126,10 @@
<metadata name="imageList_SmallIcon.TrayLocation" type="System.Drawing.Point, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a">
<value>252, 19</value>
</metadata>
<metadata name="timer_SelectedIndexChangedDebounce.TrayLocation" type="System.Drawing.Point, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a">
<value>771, 24</value>
</metadata>
<metadata name="statusStrip.TrayLocation" type="System.Drawing.Point, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a">
<value>1176, 24</value>
</metadata>
</root>

View File

@@ -9,6 +9,7 @@ using System.Threading.Tasks;
using System.Windows.Forms;
using System.Security.Policy;
using System.Diagnostics;
using NLog;
namespace SpineViewer.Controls
{
@@ -252,6 +253,11 @@ namespace SpineViewer.Controls
#endregion
/// <summary>
/// 日志器
/// </summary>
private Logger logger = LogManager.GetCurrentClassLogger();
public SpinePreviewer()
{
InitializeComponent();
@@ -422,6 +428,12 @@ namespace SpineViewer.Controls
RenderWindow.Display();
}
}
catch (Exception ex)
{
logger.Fatal(ex);
logger.Fatal("Render task stopped");
MessageBox.Error(ex.ToString(), "预览画面已停止渲染");
}
finally
{
RenderWindow.SetActive(false);
@@ -607,7 +619,7 @@ namespace SpineViewer.Controls
lock (SpineListView.Spines)
{
foreach (var spine in SpineListView.Spines)
spine.CurrentAnimation = spine.CurrentAnimation;
spine.Track0Animation = spine.Track0Animation; // TODO: 多轨道重置
}
}
}
@@ -619,7 +631,7 @@ namespace SpineViewer.Controls
lock (SpineListView.Spines)
{
foreach (var spine in SpineListView.Spines)
spine.CurrentAnimation = spine.CurrentAnimation;
spine.Track0Animation = spine.Track0Animation; // TODO: 多轨道重置
}
}
IsUpdating = true;

View File

@@ -15,12 +15,20 @@ namespace SpineViewer.Dialogs
public AboutDialog()
{
InitializeComponent();
Text = $"关于 {Program.Name}";
Text = $"关于 {ProgramName}";
label_Version.Text = $"v{InformationalVersion}";
}
public string InformationalVersion =>
Assembly.GetExecutingAssembly().GetCustomAttribute<AssemblyInformationalVersionAttribute>()?.InformationalVersion;
public string ProgramName => Process.GetCurrentProcess().ProcessName;
public string InformationalVersion
=> Assembly.GetExecutingAssembly().GetCustomAttribute<AssemblyInformationalVersionAttribute>()?.InformationalVersion;
public string ProgramUrl
{
get => linkLabel_RepoUrl.Text;
set => linkLabel_RepoUrl.Text = value;
}
private void linkLabel_RepoUrl_LinkClicked(object sender, LinkLabelLinkClickedEventArgs e)
{

View File

@@ -21,15 +21,15 @@ namespace SpineViewer.Dialogs
public BatchOpenSpineDialog()
{
InitializeComponent();
comboBox_Version.DataSource = VersionHelper.Names.ToList();
comboBox_Version.DataSource = SpineHelper.Names.ToList();
comboBox_Version.DisplayMember = "Value";
comboBox_Version.ValueMember = "Key";
comboBox_Version.SelectedValue = Spine.Version.Auto;
comboBox_Version.SelectedValue = SpineVersion.Auto;
}
private void button_Ok_Click(object sender, EventArgs e)
{
var version = (Spine.Version)comboBox_Version.SelectedValue;
var version = (SpineVersion)comboBox_Version.SelectedValue;
var items = skelFileListBox.Items;
@@ -48,7 +48,7 @@ namespace SpineViewer.Dialogs
}
}
if (version != Spine.Version.Auto && !Spine.Spine.ImplementedVersions.Contains(version))
if (version != SpineVersion.Auto && !Spine.Spine.HasImplementation(version))
{
MessageBox.Info($"{version.GetName()} 版本尚未实现(咕咕咕~");
return;
@@ -67,12 +67,12 @@ namespace SpineViewer.Dialogs
/// <summary>
/// 批量打开对话框结果
/// </summary>
public class BatchOpenSpineDialogResult(Spine.Version version, string[] skelPaths)
public class BatchOpenSpineDialogResult(SpineVersion version, string[] skelPaths)
{
/// <summary>
/// 版本
/// </summary>
public Spine.Version Version => version;
public SpineVersion Version => version;
/// <summary>
/// 路径列表

View File

@@ -44,7 +44,6 @@
button_Cancel = new Button();
label2 = new Label();
skelFileListBox = new SpineViewer.Controls.SkelFileListBox();
openFileDialog_Skel = new OpenFileDialog();
panel.SuspendLayout();
tableLayoutPanel1.SuspendLayout();
flowLayoutPanel_TargetFormat.SuspendLayout();
@@ -238,14 +237,6 @@
skelFileListBox.Size = new Size(945, 264);
skelFileListBox.TabIndex = 20;
//
// openFileDialog_Skel
//
openFileDialog_Skel.AddExtension = false;
openFileDialog_Skel.AddToRecent = false;
openFileDialog_Skel.Filter = "skel 文件 (*.skel; *.json)|*.skel;*.json|二进制文件 (*.skel)|*.skel|文本文件 (*.json)|*.json|所有文件 (*.*)|*.*";
openFileDialog_Skel.Multiselect = true;
openFileDialog_Skel.Title = "批量选择skel文件";
//
// ConvertFileFormatDialog
//
AcceptButton = button_Ok;
@@ -281,7 +272,6 @@
private TableLayoutPanel tableLayoutPanel2;
private Button button_Ok;
private Button button_Cancel;
private OpenFileDialog openFileDialog_Skel;
private Label label1;
private Label label2;
private FlowLayoutPanel flowLayoutPanel_TargetFormat;

View File

@@ -22,24 +22,24 @@ namespace SpineViewer.Dialogs
{
InitializeComponent();
comboBox_SourceVersion.DataSource = VersionHelper.Names.ToList();
comboBox_SourceVersion.DataSource = SpineHelper.Names.ToList();
comboBox_SourceVersion.DisplayMember = "Value";
comboBox_SourceVersion.ValueMember = "Key";
comboBox_SourceVersion.SelectedValue = Spine.Version.Auto;
comboBox_SourceVersion.SelectedValue = SpineVersion.Auto;
// 目标版本不包含自动
var versionsWithoutAuto = VersionHelper.Names.ToDictionary();
versionsWithoutAuto.Remove(Spine.Version.Auto);
var versionsWithoutAuto = SpineHelper.Names.ToDictionary();
versionsWithoutAuto.Remove(SpineVersion.Auto);
comboBox_TargetVersion.DataSource = versionsWithoutAuto.ToList();
comboBox_TargetVersion.DisplayMember = "Value";
comboBox_TargetVersion.ValueMember = "Key";
comboBox_TargetVersion.SelectedValue = Spine.Version.V38;
comboBox_TargetVersion.SelectedValue = SpineVersion.V38;
}
private void button_Ok_Click(object sender, EventArgs e)
{
var sourceVersion = (Spine.Version)comboBox_SourceVersion.SelectedValue;
var targetVersion = (Spine.Version)comboBox_TargetVersion.SelectedValue;
var sourceVersion = (SpineVersion)comboBox_SourceVersion.SelectedValue;
var targetVersion = (SpineVersion)comboBox_TargetVersion.SelectedValue;
var jsonTarget = radioButton_JsonTarget.Checked;
var items = skelFileListBox.Items;
@@ -59,13 +59,13 @@ namespace SpineViewer.Dialogs
}
}
if (sourceVersion != Spine.Version.Auto && !SkeletonConverter.ImplementedVersions.Contains(sourceVersion))
if (sourceVersion != SpineVersion.Auto && !SkeletonConverter.HasImplementation(sourceVersion))
{
MessageBox.Info($"{sourceVersion.GetName()} 版本尚未实现(咕咕咕~");
return;
}
if (!SkeletonConverter.ImplementedVersions.Contains(targetVersion))
if (!SkeletonConverter.HasImplementation(targetVersion))
{
MessageBox.Info($"{targetVersion.GetName()} 版本尚未实现(咕咕咕~");
return;
@@ -84,7 +84,7 @@ namespace SpineViewer.Dialogs
/// <summary>
/// 文件格式转换对话框结果包装类
/// </summary>
public class ConvertFileFormatDialogResult(string[] skelPaths, Spine.Version sourceVersion, Spine.Version targetVersion, bool jsonTarget)
public class ConvertFileFormatDialogResult(string[] skelPaths, SpineVersion sourceVersion, SpineVersion targetVersion, bool jsonTarget)
{
/// <summary>
/// 骨骼文件路径列表
@@ -94,12 +94,12 @@ namespace SpineViewer.Dialogs
/// <summary>
/// 源版本
/// </summary>
public Spine.Version SourceVersion => sourceVersion;
public SpineVersion SourceVersion => sourceVersion;
/// <summary>
/// 目标版本
/// </summary>
public Spine.Version TargetVersion => targetVersion;
public SpineVersion TargetVersion => targetVersion;
/// <summary>
/// 目标格式是否为 Json

View File

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

View File

@@ -26,7 +26,29 @@ namespace SpineViewer.Dialogs
private class DiagnosticsInformation
{
[Category("Versions")]
[Category("Hardware")]
public string CPU
{
get => Registry.GetValue(@"HKEY_LOCAL_MACHINE\HARDWARE\DESCRIPTION\System\CentralProcessor\0", "ProcessorNameString", "Unknown").ToString();
}
[Category("Hardware")]
public string Memory
{
get => $"{new Microsoft.VisualBasic.Devices.ComputerInfo().TotalPhysicalMemory / 1024f / 1024f / 1024f:F1} GB";
}
[Category("Hardware")]
public string GPU
{
get
{
var searcher = new ManagementObjectSearcher("SELECT Name FROM Win32_VideoController");
return string.Join("; ", searcher.Get().Cast<ManagementObject>().Select(mo => mo["Name"].ToString()));
}
}
[Category("Software")]
public string WindowsVersion
{
get
@@ -39,44 +61,28 @@ namespace SpineViewer.Dialogs
}
}
[Category("Versions")]
[Category("Software")]
public string Version
{
get => Assembly.GetExecutingAssembly().GetCustomAttribute<AssemblyInformationalVersionAttribute>()?.InformationalVersion;
}
[Category("Versions")]
[Category("Software")]
public string DotNetVersion
{
get => Environment.Version.ToString();
}
[Category("Versions")]
[Category("Software")]
public string SFMLVersion
{
get => typeof(SFML.ObjectBase).Assembly.GetName().Version.ToString();
}
[Category("Hardwares")]
public string CPU
[Category("Software")]
public string FFMpegCoreVersion
{
get => Registry.GetValue(@"HKEY_LOCAL_MACHINE\HARDWARE\DESCRIPTION\System\CentralProcessor\0", "ProcessorNameString", "Unknown").ToString();
}
[Category("Hardwares")]
public string Memory
{
get => $"{new Microsoft.VisualBasic.Devices.ComputerInfo().TotalPhysicalMemory / 1024f / 1024f / 1024f:F1} GB";
}
[Category("Hardwares")]
public string GPU
{
get
{
var searcher = new ManagementObjectSearcher("SELECT Name FROM Win32_VideoController");
return string.Join("; ", searcher.Get().Cast<ManagementObject>().Select(mo => mo["Name"].ToString()));
}
get => typeof(FFMpegCore.FFMpeg).Assembly.GetName().Version.ToString();
}
}

View File

@@ -20,10 +20,10 @@ namespace SpineViewer.Dialogs
public OpenSpineDialog()
{
InitializeComponent();
comboBox_Version.DataSource = VersionHelper.Names.ToList();
comboBox_Version.DataSource = SpineHelper.Names.ToList();
comboBox_Version.DisplayMember = "Value";
comboBox_Version.ValueMember = "Key";
comboBox_Version.SelectedValue = Spine.Version.Auto;
comboBox_Version.SelectedValue = SpineVersion.Auto;
}
private void OpenSpineDialog_Load(object sender, EventArgs e)
@@ -53,7 +53,7 @@ namespace SpineViewer.Dialogs
{
var skelPath = textBox_SkelPath.Text;
var atlasPath = textBox_AtlasPath.Text;
var version = (Spine.Version)comboBox_Version.SelectedValue;
var version = (SpineVersion)comboBox_Version.SelectedValue;
if (!File.Exists(skelPath))
{
@@ -65,7 +65,7 @@ namespace SpineViewer.Dialogs
skelPath = Path.GetFullPath(skelPath);
}
if (string.IsNullOrEmpty(atlasPath))
if (string.IsNullOrWhiteSpace(atlasPath))
{
atlasPath = null;
}
@@ -79,7 +79,7 @@ namespace SpineViewer.Dialogs
atlasPath = Path.GetFullPath(atlasPath);
}
if (version != Spine.Version.Auto && !Spine.Spine.ImplementedVersions.Contains(version))
if (version != SpineVersion.Auto && !Spine.Spine.HasImplementation(version))
{
MessageBox.Info($"{version.GetName()} 版本尚未实现(咕咕咕~");
return;
@@ -98,12 +98,12 @@ namespace SpineViewer.Dialogs
/// <summary>
/// 打开骨骼对话框结果
/// </summary>
public class OpenSpineDialogResult(Spine.Version version, string skelPath, string? atlasPath = null)
public class OpenSpineDialogResult(SpineVersion version, string skelPath, string? atlasPath = null)
{
/// <summary>
/// 版本
/// </summary>
public Spine.Version Version => version;
public SpineVersion Version => version;
/// <summary>
/// skel 文件路径

View File

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

View File

@@ -14,40 +14,18 @@ namespace SpineViewer.Exporter
/// <summary>
/// 导出参数基类
/// </summary>
public abstract class ExportArgs
public abstract class ExportArgs : ImplementationResolver<ExportArgs, ExportImplementationAttribute, ExportType>
{
/// <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>
/// <param name="exportType">导出类型</param>
/// <param name="resolution">分辨率</param>
/// <param name="view">导出视图</param>
/// <param name="renderSelectedOnly">仅渲染选中</param>
/// <returns>返回与指定 <paramref name="exportType"/> 匹配的导出参数实例</returns>
public static ExportArgs New(ExportType exportType, Size resolution, SFML.Graphics.View view, bool renderSelectedOnly)
{
if (!ImplementationTypes.TryGetValue(exportType, out var type))
{
throw new NotImplementedException($"Not implemented type: {exportType}");
}
return (ExportArgs)Activator.CreateInstance(type, resolution, view, renderSelectedOnly);
}
=> New(exportType, [resolution, view, renderSelectedOnly]);
public ExportArgs(Size resolution, SFML.Graphics.View view, bool renderSelectedOnly)
{
@@ -88,19 +66,27 @@ namespace SpineViewer.Exporter
[Category("[0] "), DisplayName(""), Description("")]
public bool RenderSelectedOnly { get; }
/// <summary>
/// 背景颜色
/// </summary>
[Editor(typeof(SFMLColorEditor), typeof(UITypeEditor))]
[TypeConverter(typeof(SFMLColorConverter))]
[Category("[0] "), DisplayName(""), Description("使, #RRGGBBAA")]
public SFML.Graphics.Color BackgroundColor { get; set; } = SFML.Graphics.Color.Transparent;
/// <summary>
/// 检查参数是否合法并规范化参数值, 否则返回用户错误原因
/// </summary>
public virtual string? Validate()
{
if (!string.IsNullOrEmpty(OutputDir) && File.Exists(OutputDir))
if (!string.IsNullOrWhiteSpace(OutputDir) && File.Exists(OutputDir))
return "输出文件夹无效";
if (!string.IsNullOrEmpty(OutputDir) && !Directory.Exists(OutputDir))
if (!string.IsNullOrWhiteSpace(OutputDir) && !Directory.Exists(OutputDir))
return $"文件夹 {OutputDir} 不存在";
if (ExportSingle && string.IsNullOrEmpty(OutputDir))
if (ExportSingle && string.IsNullOrWhiteSpace(OutputDir))
return "导出单个时必须提供输出文件夹";
OutputDir = string.IsNullOrEmpty(OutputDir) ? null : Path.GetFullPath(OutputDir);
OutputDir = string.IsNullOrWhiteSpace(OutputDir) ? null : Path.GetFullPath(OutputDir);
return null;
}
}

View File

@@ -15,25 +15,21 @@ namespace SpineViewer.Exporter
{
Frame,
FrameSequence,
GIF,
MKV,
MP4,
MOV,
WebM
Gif,
Mp4,
Webm,
Mkv,
Mov,
Custom,
}
/// <summary>
/// 导出实现类标记
/// </summary>
[AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = false)]
public class ExportImplementationAttribute : Attribute
[AttributeUsage(AttributeTargets.Class, AllowMultiple = true, Inherited = false)]
public class ExportImplementationAttribute(ExportType exportType) : Attribute, IImplementationKey<ExportType>
{
public ExportType ExportType { get; }
public ExportImplementationAttribute(ExportType exportType)
{
ExportType = exportType;
}
public ExportType ImplementationKey { get; private set; } = exportType;
}
/// <summary>

View File

@@ -1,4 +1,5 @@
using SpineViewer.Spine;
using NLog;
using SpineViewer.Spine;
using System;
using System.Collections.Generic;
using System.ComponentModel;
@@ -6,63 +7,36 @@ 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
public abstract class Exporter(ExportArgs exportArgs) : ImplementationResolver<Exporter, ExportImplementationAttribute, ExportType>
{
/// <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));
}
/// <param name="exportType">导出类型</param>
/// <param name="exportArgs">与 <paramref name="exportType"/> 匹配的导出参数</param>
/// <returns>与 <paramref name="exportType"/> 匹配的导出器</returns>
public static Exporter New(ExportType exportType, ExportArgs exportArgs) => New(exportType, [exportArgs]);
/// <summary>
/// 创建指定类型导出参数
/// 日志器
/// </summary>
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);
}
protected Logger logger = LogManager.GetCurrentClassLogger();
/// <summary>
/// 导出参数
/// </summary>
public ExportArgs ExportArgs { get; }
public ExportArgs ExportArgs { get; } = exportArgs;
/// <summary>
/// 可用于文件名的时间戳字符串
/// </summary>
protected readonly string timestamp;
public Exporter(ExportArgs exportArgs)
{
ExportArgs = exportArgs;
timestamp = DateTime.Now.ToString("yyMMddHHmmss");
}
protected readonly string timestamp = DateTime.Now.ToString("yyMMddHHmmss");
/// <summary>
/// 获取供渲染的 SFML.Graphics.RenderTexture
@@ -82,7 +56,7 @@ namespace SpineViewer.Exporter
{
// tex 必须临时创建, 随用随取, 防止出现跨线程的情况
using var tex = GetRenderTexture();
tex.Clear(SFML.Graphics.Color.Transparent);
tex.Clear(ExportArgs.BackgroundColor);
tex.Draw(spine);
tex.Display();
return new(tex.Texture.CopyToImage());
@@ -95,7 +69,7 @@ namespace SpineViewer.Exporter
{
// tex 必须临时创建, 随用随取, 防止出现跨线程的情况
using var tex = GetRenderTexture();
tex.Clear(SFML.Graphics.Color.Transparent);
tex.Clear(ExportArgs.BackgroundColor);
foreach (var spine in spinesToRender) tex.Draw(spine);
tex.Display();
return new(tex.Texture.CopyToImage());
@@ -123,7 +97,7 @@ namespace SpineViewer.Exporter
if (ExportArgs.ExportSingle) ExportSingle(spinesToRender, worker);
else ExportIndividual(spinesToRender, worker);
Program.LogCurrentMemoryUsage();
logger.LogCurrentProcessMemoryUsage();
}
}
}

View File

@@ -0,0 +1,38 @@
using FFMpegCore.Enums;
using FFMpegCore;
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>
/// FFmpeg 自定义视频导出参数
/// </summary>
[ExportImplementation(ExportType.Custom)]
public class CustomExportArgs : FFmpegVideoExportArgs
{
public CustomExportArgs(Size resolution, SFML.Graphics.View view, bool renderSelectedOnly) : base(resolution, view, renderSelectedOnly) { }
public override string Format => CustomFormat;
public override string Suffix => CustomSuffix;
public override string FileNameNoteSuffix => string.Empty;
/// <summary>
/// 文件格式
/// </summary>
[Category("[3] "), DisplayName(""), Description("")]
public string CustomFormat { get; set; } = "mp4";
/// <summary>
/// 文件名后缀
/// </summary>
[Category("[3] "), DisplayName(""), Description("")]
public string CustomSuffix { get; set; } = ".mp4";
}
}

View File

@@ -0,0 +1,59 @@
using FFMpegCore.Enums;
using FFMpegCore;
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>
/// 使用 FFmpeg 视频导出参数
/// </summary>
public abstract class FFmpegVideoExportArgs : VideoExportArgs
{
public FFmpegVideoExportArgs(Size resolution, SFML.Graphics.View view, bool renderSelectedOnly) : base(resolution, view, renderSelectedOnly) { }
/// <summary>
/// 文件格式
/// </summary>
[Category("[2] FFmpeg "), DisplayName(""), Description("")]
public abstract string Format { get; }
/// <summary>
/// 文件名后缀
/// </summary>
[Category("[2] FFmpeg "), DisplayName(""), Description("")]
public abstract string Suffix { get; }
/// <summary>
/// 文件名后缀
/// </summary>
[Category("[2] FFmpeg "), DisplayName(""), Description(" FFmpeg , , ")]
public string CustomArgument { get; set; }
/// <summary>
/// 获取输出附加选项
/// </summary>
public virtual void SetOutputOptions(FFMpegArgumentOptions options) => options.ForceFormat(Format).WithCustomArgument(CustomArgument);
/// <summary>
/// 要追加在文件名末尾的信息字串, 首尾不需要提供额外分隔符
/// </summary>
[Browsable(false)]
public abstract string FileNameNoteSuffix { get; }
public override string? Validate()
{
if (base.Validate() is string error)
return error;
if (string.IsNullOrWhiteSpace(Format))
return "需要提供有效的格式";
if (string.IsNullOrWhiteSpace(Suffix))
return "需要提供有效的文件名后缀";
return null;
}
}
}

View File

@@ -36,7 +36,7 @@ namespace SpineViewer.Exporter.Implementations.ExportArgs
/// 文件名后缀
/// </summary>
[Category("[1] "), DisplayName(""), Description("")]
public string FileSuffix { get => imageFormat.GetSuffix(); }
public string Suffix { get => imageFormat.GetSuffix(); }
/// <summary>
/// DPI

View File

@@ -19,8 +19,8 @@ namespace SpineViewer.Exporter.Implementations.ExportArgs
/// <summary>
/// 文件名后缀
/// </summary>
[TypeConverter(typeof(SFMLImageFileSuffixConverter))]
[TypeConverter(typeof(StringEnumConverter)), StringEnumConverter.StandardValues(".png", ".jpg", ".tga", ".bmp")]
[Category("[2] "), DisplayName(""), Description("")]
public string FileSuffix { get; set; } = ".png";
public string Suffix { get; set; } = ".png";
}
}

View File

@@ -1,4 +1,5 @@
using System;
using FFMpegCore;
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
@@ -10,42 +11,46 @@ namespace SpineViewer.Exporter.Implementations.ExportArgs
/// <summary>
/// GIF 导出参数
/// </summary>
[ExportImplementation(ExportType.GIF)]
public class GifExportArgs : VideoExportArgs
[ExportImplementation(ExportType.Gif)]
public class GifExportArgs : FFmpegVideoExportArgs
{
public GifExportArgs(Size resolution, SFML.Graphics.View view, bool renderSelectedOnly) : base(resolution, view, renderSelectedOnly)
{
// 给一个纯白的背景
BackgroundColor = new(255, 255, 255, 0);
// GIF 的帧率不能太高, 超过 50 帧反而会变慢
FPS = 25;
FPS = 12;
}
public override string Format => "gif";
public override string Suffix => ".gif";
/// <summary>
/// 调色板最大颜色数量
/// </summary>
[Category("[2] GIF "), DisplayName(""), Description("使, ")]
[Category("[3] "), DisplayName(""), Description("使, ")]
public uint MaxColors { get => maxColors; set => maxColors = Math.Clamp(value, 2, 256); }
private uint maxColors = 256;
/// <summary>
/// 透明度阈值
/// </summary>
[Category("[2] GIF "), DisplayName(""), Description("")]
[Category("[3] "), DisplayName(""), Description("")]
public byte AlphaThreshold { get => alphaThreshold; set => alphaThreshold = value; }
private byte alphaThreshold = 128;
/// <summary>
/// 获取构造好的 FFMpegCore 自定义参数
/// </summary>
[Browsable(false)]
public string FFMpegCoreCustomArguments
public override void SetOutputOptions(FFMpegArgumentOptions options)
{
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}\"";
}
base.SetOutputOptions(options);
var v = $"[0:v] split [s0][s1]";
var s0 = $"[s0] palettegen=reserve_transparent=1:max_colors={MaxColors} [p]";
var s1 = $"[s1][p] paletteuse=dither=bayer:alpha_threshold={AlphaThreshold}";
var customArgs = $"-filter_complex \"{v};{s0};{s1}\"";
options.WithCustomArgument(customArgs);
}
public override string FileNameNoteSuffix => $"{MaxColors}_{AlphaThreshold}";
}
}

View File

@@ -0,0 +1,57 @@
using FFMpegCore;
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>
/// MKV 导出参数
/// </summary>
[ExportImplementation(ExportType.Mkv)]
public class MkvExportArgs : FFmpegVideoExportArgs
{
public MkvExportArgs(Size resolution, SFML.Graphics.View view, bool renderSelectedOnly) : base(resolution, view, renderSelectedOnly)
{
BackgroundColor = new(0, 255, 0, 0);
}
public override string Format => "matroska";
public override string Suffix => ".mkv";
/// <summary>
/// 编码器
/// </summary>
[StringEnumConverter.StandardValues("libx264", "libx265", "libvpx-vp9", Customizable = true)]
[TypeConverter(typeof(StringEnumConverter))]
[Category("[3] "), DisplayName(""), Description("使")]
public string Codec { get; set; } = "libx265";
/// <summary>
/// CRF
/// </summary>
[Category("[3] "), DisplayName("CRF"), Description("-crf, 0-63, 18-28, 23, ")]
public int CRF { get => crf; set => crf = Math.Clamp(value, 0, 63); }
private int crf = 23;
/// <summary>
/// 像素格式
/// </summary>
[StringEnumConverter.StandardValues("yuv420p", "yuv422p", "yuv444p", "yuva420p", Customizable = true)]
[TypeConverter(typeof(StringEnumConverter))]
[Category("[3] "), DisplayName(""), Description("使")]
public string PixelFormat { get; set; } = "yuv444p";
public override void SetOutputOptions(FFMpegArgumentOptions options)
{
base.SetOutputOptions(options);
options.WithVideoCodec(Codec).WithConstantRateFactor(CRF).ForcePixelFormat(PixelFormat);
}
public override string FileNameNoteSuffix => $"{Codec}_{CRF}_{PixelFormat}";
}
}

View File

@@ -0,0 +1,58 @@
using FFMpegCore;
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>
/// MOV 导出参数
/// </summary>
[ExportImplementation(ExportType.Mov)]
public class MovExportArgs : FFmpegVideoExportArgs
{
public MovExportArgs(Size resolution, SFML.Graphics.View view, bool renderSelectedOnly) : base(resolution, view, renderSelectedOnly)
{
BackgroundColor = new(0, 255, 0, 0);
}
public override string Format => "mov";
public override string Suffix => ".mov";
/// <summary>
/// 编码器
/// </summary>
[StringEnumConverter.StandardValues("prores_ks", Customizable = true)]
[TypeConverter(typeof(StringEnumConverter))]
[Category("[3] "), DisplayName(""), Description("使")]
public string Codec { get; set; } = "prores_ks";
/// <summary>
/// 预设
/// </summary>
[StringEnumConverter.StandardValues("auto", "proxy", "lt", "standard", "hq", "4444", "444xq")]
[TypeConverter(typeof(StringEnumConverter))]
[Category("[3] "), DisplayName(""), Description("-profile, ")]
public string Profile { get; set; } = "auto";
/// <summary>
/// 像素格式
/// </summary>
[StringEnumConverter.StandardValues("yuv422p10le", "yuv444p10le", "yuva444p10le", Customizable = true)]
[TypeConverter(typeof(StringEnumConverter))]
[Category("[3] "), DisplayName(""), Description("使")]
public string PixelFormat { get; set; } = "yuva444p10le";
public override void SetOutputOptions(FFMpegArgumentOptions options)
{
base.SetOutputOptions(options);
options.WithFastStart().WithVideoCodec(Codec).WithCustomArgument($"-profile {Profile}").ForcePixelFormat(PixelFormat);
}
public override string FileNameNoteSuffix => $"{Codec}_{Profile}_{PixelFormat}";
}
}

View File

@@ -0,0 +1,57 @@
using FFMpegCore;
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace SpineViewer.Exporter.Implementations.ExportArgs
{
/// <summary>
/// MP4 导出参数
/// </summary>
[ExportImplementation(ExportType.Mp4)]
public class Mp4ExportArgs : FFmpegVideoExportArgs
{
public Mp4ExportArgs(Size resolution, SFML.Graphics.View view, bool renderSelectedOnly) : base(resolution, view, renderSelectedOnly)
{
BackgroundColor = new(0, 255, 0, 0);
}
public override string Format => "mp4";
public override string Suffix => ".mp4";
/// <summary>
/// 编码器
/// </summary>
[StringEnumConverter.StandardValues("libx264", "libx265", Customizable = true)]
[TypeConverter(typeof(StringEnumConverter))]
[Category("[3] "), DisplayName(""), Description("使")]
public string Codec { get; set; } = "libx264";
/// <summary>
/// CRF
/// </summary>
[Category("[3] "), DisplayName("CRF"), Description("-crf, 0-63, 18-28, 23, ")]
public int CRF { get => crf; set => crf = Math.Clamp(value, 0, 63); }
private int crf = 23;
/// <summary>
/// 像素格式
/// </summary>
[StringEnumConverter.StandardValues("yuv420p", "yuv422p", "yuv444p", Customizable = true)]
[TypeConverter(typeof(StringEnumConverter))]
[Category("[3] "), DisplayName(""), Description("使")]
public string PixelFormat { get; set; } = "yuv444p";
public override void SetOutputOptions(FFMpegArgumentOptions options)
{
base.SetOutputOptions(options);
options.WithFastStart().WithVideoCodec(Codec).WithConstantRateFactor(CRF).ForcePixelFormat(PixelFormat);
}
public override string FileNameNoteSuffix => $"{Codec}_{CRF}_{PixelFormat}";
}
}

View File

@@ -1,4 +1,6 @@
using System;
using FFMpegCore.Enums;
using FFMpegCore;
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;

View File

@@ -0,0 +1,58 @@
using FFMpegCore;
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>
/// WebM 导出参数
/// </summary>
[ExportImplementation(ExportType.Webm)]
public class WebmExportArgs : FFmpegVideoExportArgs
{
public WebmExportArgs(Size resolution, SFML.Graphics.View view, bool renderSelectedOnly) : base(resolution, view, renderSelectedOnly)
{
// 默认用透明黑背景
BackgroundColor = new(0, 0, 0, 0);
}
public override string Format => "webm";
public override string Suffix => ".webm";
/// <summary>
/// 编码器
/// </summary>
[StringEnumConverter.StandardValues("libvpx-vp9", Customizable = true)]
[TypeConverter(typeof(StringEnumConverter))]
[Category("[3] "), DisplayName(""), Description("使")]
public string Codec { get; set; } = "libvpx-vp9";
/// <summary>
/// CRF
/// </summary>
[Category("[3] "), DisplayName("CRF"), Description("Constant Rate Factor, 0-63, 18-28, 23, ")]
public int CRF { get => crf; set => crf = Math.Clamp(value, 0, 63); }
private int crf = 23;
/// <summary>
/// 像素格式
/// </summary>
[StringEnumConverter.StandardValues("yuv420p", "yuv422p", "yuv444p", "yuva420p", Customizable = true)]
[TypeConverter(typeof(StringEnumConverter))]
[Category("[3] "), DisplayName(""), Description("使")]
public string PixelFormat { get; set; } = "yuva420p";
public override void SetOutputOptions(FFMpegArgumentOptions options)
{
base.SetOutputOptions(options);
options.WithVideoCodec(Codec).WithConstantRateFactor(CRF).ForcePixelFormat(PixelFormat);
}
public override string FileNameNoteSuffix => $"{Codec}_{CRF}_{PixelFormat}";
}
}

View File

@@ -0,0 +1,87 @@
using FFMpegCore.Pipes;
using FFMpegCore;
using SpineViewer.Exporter.Implementations.ExportArgs;
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using FFMpegCore.Arguments;
using System.Diagnostics;
namespace SpineViewer.Exporter.Implementations.Exporter
{
/// <summary>
/// 使用 FFmpeg 的视频导出器
/// </summary>
[ExportImplementation(ExportType.Gif)]
[ExportImplementation(ExportType.Mp4)]
[ExportImplementation(ExportType.Webm)]
[ExportImplementation(ExportType.Mkv)]
[ExportImplementation(ExportType.Mov)]
[ExportImplementation(ExportType.Custom)]
public class FFmpegVideoExporter : VideoExporter
{
public FFmpegVideoExporter(FFmpegVideoExportArgs exportArgs) : base(exportArgs) { }
protected override void ExportSingle(Spine.Spine[] spinesToRender, BackgroundWorker? worker = null)
{
var args = (FFmpegVideoExportArgs)ExportArgs;
var noteSuffix = args.FileNameNoteSuffix;
if (!string.IsNullOrWhiteSpace(noteSuffix)) noteSuffix = $"_{noteSuffix}";
var filename = $"{timestamp}_{args.FPS:f0}{noteSuffix}{args.Suffix}";
// 导出单个时必定提供输出文件夹
var savePath = Path.Combine(args.OutputDir, filename);
var videoFramesSource = new RawVideoPipeSource(GetFrames(spinesToRender, worker)) { FrameRate = args.FPS };
try
{
var ffmpegArgs = FFMpegArguments.FromPipeInput(videoFramesSource).OutputToFile(savePath, true, args.SetOutputOptions);
logger.Info("FFmpeg arguments: {}", ffmpegArgs.Arguments);
ffmpegArgs.ProcessSynchronously();
}
catch (Exception ex)
{
logger.Error(ex.ToString());
logger.Error("Failed to export {} {}", args.Format, savePath);
}
}
protected override void ExportIndividual(Spine.Spine[] spinesToRender, BackgroundWorker? worker = null)
{
var args = (FFmpegVideoExportArgs)ExportArgs;
var noteSuffix = args.FileNameNoteSuffix;
if (!string.IsNullOrWhiteSpace(noteSuffix)) noteSuffix = $"_{noteSuffix}";
foreach (var spine in spinesToRender)
{
if (worker?.CancellationPending == true) break; // 取消的日志在 GetFrames 里输出
var filename = $"{spine.Name}_{timestamp}_{args.FPS:f0}{noteSuffix}{args.Suffix}";
// 如果提供了输出文件夹, 则全部导出到输出文件夹, 否则导出到各自的文件夹下
var savePath = Path.Combine(args.OutputDir ?? spine.AssetsDir, filename);
var videoFramesSource = new RawVideoPipeSource(GetFrames(spine, worker)) { FrameRate = args.FPS };
try
{
var ffmpegArgs = FFMpegArguments
.FromPipeInput(videoFramesSource)
.OutputToFile(savePath, true, args.SetOutputOptions);
logger.Info("FFmpeg arguments: {}", ffmpegArgs.Arguments);
ffmpegArgs.ProcessSynchronously();
}
catch (Exception ex)
{
logger.Error(ex.ToString());
logger.Error("Failed to export {} {} {}", args.Format, savePath, spine.SkelPath);
}
}
}
}
}

View File

@@ -22,7 +22,7 @@ namespace SpineViewer.Exporter.Implementations.Exporter
var args = (FrameExportArgs)ExportArgs;
// 导出单个时必定提供输出文件夹
var filename = $"frame_{timestamp}{args.FileSuffix}";
var filename = $"frame_{timestamp}{args.Suffix}";
var savePath = Path.Combine(args.OutputDir, filename);
worker?.ReportProgress(0, $"已处理 0/1");
@@ -35,8 +35,8 @@ namespace SpineViewer.Exporter.Implementations.Exporter
}
catch (Exception ex)
{
Program.Logger.Error(ex.ToString());
Program.Logger.Error("Failed to save single frame");
logger.Error(ex.ToString());
logger.Error("Failed to save single frame");
}
worker?.ReportProgress(100, $"已处理 1/1");
}
@@ -55,7 +55,7 @@ namespace SpineViewer.Exporter.Implementations.Exporter
var spine = spinesToRender[i];
// 逐个导出时如果提供了输出文件夹, 则全部导出到输出文件夹, 否则输出到各自的文件夹
var filename = $"{spine.Name}_{timestamp}{args.FileSuffix}";
var filename = $"{spine.Name}_{timestamp}{args.Suffix}";
var savePath = args.OutputDir is null ? Path.Combine(spine.AssetsDir, filename) : Path.Combine(args.OutputDir, filename);
try
@@ -68,8 +68,8 @@ namespace SpineViewer.Exporter.Implementations.Exporter
}
catch (Exception ex)
{
Program.Logger.Error(ex.ToString());
Program.Logger.Error("Failed to save frame {} {}", savePath, spine.SkelPath);
logger.Error(ex.ToString());
logger.Error("Failed to save frame {} {}", savePath, spine.SkelPath);
error++;
}
@@ -77,9 +77,9 @@ namespace SpineViewer.Exporter.Implementations.Exporter
}
if (error > 0)
Program.Logger.Warn("Frames save {} successfully, {} failed", success, error);
logger.Warn("Frames save {} successfully, {} failed", success, error);
else
Program.Logger.Info("{} frames saved successfully", success);
logger.Info("{} frames saved successfully", success);
}
}

View File

@@ -28,7 +28,7 @@ namespace SpineViewer.Exporter.Implementations.Exporter
int frameIdx = 0;
foreach (var frame in GetFrames(spinesToRender, worker))
{
var filename = $"frames_{timestamp}_{args.FPS:f0}_{frameIdx:d6}{args.FileSuffix}";
var filename = $"frames_{timestamp}_{args.FPS:f0}_{frameIdx:d6}{args.Suffix}";
var savePath = Path.Combine(saveDir, filename);
try
@@ -37,8 +37,8 @@ namespace SpineViewer.Exporter.Implementations.Exporter
}
catch (Exception ex)
{
Program.Logger.Error(ex.ToString());
Program.Logger.Error("Failed to save frame {}", savePath);
logger.Error(ex.ToString());
logger.Error("Failed to save frame {}", savePath);
}
finally
{
@@ -63,7 +63,7 @@ namespace SpineViewer.Exporter.Implementations.Exporter
int frameIdx = 0;
foreach (var frame in GetFrames(spine, worker))
{
var filename = $"{spine.Name}_{timestamp}_{args.FPS:f0}_{frameIdx:d6}{args.FileSuffix}";
var filename = $"{spine.Name}_{timestamp}_{args.FPS:f0}_{frameIdx:d6}{args.Suffix}";
var savePath = Path.Combine(saveDir, filename);
try
@@ -72,8 +72,8 @@ namespace SpineViewer.Exporter.Implementations.Exporter
}
catch (Exception ex)
{
Program.Logger.Error(ex.ToString());
Program.Logger.Error("Failed to save frame {} {}", savePath, spine.SkelPath);
logger.Error(ex.ToString());
logger.Error("Failed to save frame {} {}", savePath, spine.SkelPath);
}
finally
{

View File

@@ -1,76 +0,0 @@
using FFMpegCore.Pipes;
using FFMpegCore;
using SpineViewer.Exporter.Implementations.ExportArgs;
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using FFMpegCore.Arguments;
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);
}
}
}
}
}

View File

@@ -23,9 +23,9 @@ namespace SpineViewer.Exporter.Implementations.Exporter
{
var args = (VideoExportArgs)ExportArgs;
// 独立导出时如果 args.Duration 小于 0 则使用自己的动画时长
// 独立导出时如果 args.Duration 小于 0 则使用 Track0 的动画时长
var duration = args.Duration;
if (duration < 0) duration = spine.CurrentAnimationDuration;
if (duration < 0) duration = spine.GetAnimationDuration(spine.Track0Animation); // TODO: 也许可以使用所有轨道的最大值
float delta = 1f / args.FPS;
int total = Math.Max(1, (int)(duration * args.FPS)); // 至少导出 1 帧
@@ -35,7 +35,7 @@ namespace SpineViewer.Exporter.Implementations.Exporter
{
if (worker?.CancellationPending == true)
{
Program.Logger.Info("Export cancelled");
logger.Info("Export cancelled");
break;
}
@@ -61,7 +61,7 @@ namespace SpineViewer.Exporter.Implementations.Exporter
{
if (worker?.CancellationPending == true)
{
Program.Logger.Info("Export cancelled");
logger.Info("Export cancelled");
break;
}
@@ -75,7 +75,7 @@ namespace SpineViewer.Exporter.Implementations.Exporter
public override void Export(Spine.Spine[] spines, BackgroundWorker? worker = null)
{
// 导出视频格式需要把模型时间都重置到 0
foreach (var spine in spines) spine.CurrentAnimation = spine.CurrentAnimation;
foreach (var spine in spines) spine.Track0Animation = spine.Track0Animation; // TODO: 多轨道重置
base.Export(spines, worker);
}
}

View File

@@ -10,24 +10,80 @@ using System.Threading.Tasks;
namespace SpineViewer.Exporter
{
public class SFMLImageFileSuffixConverter : StringConverter
public class SFMLColorConverter : ExpandableObjectConverter
{
private readonly string[] supportedFileSuffix = [".png", ".jpg", ".tga", ".bmp"];
public override bool GetStandardValuesSupported(ITypeDescriptorContext? context)
private class SFMLColorPropertyDescriptor : SimplePropertyDescriptor
{
// 支持标准值列表
return true;
public SFMLColorPropertyDescriptor(Type componentType, string name, Type propertyType) : base(componentType, name, propertyType) { }
public override object? GetValue(object? component)
{
return component?.GetType().GetField(Name)?.GetValue(component) ?? default;
}
public override void SetValue(object? component, object? value)
{
component?.GetType().GetField(Name)?.SetValue(component, value);
}
}
public override bool GetStandardValuesExclusive(ITypeDescriptorContext? context)
public override bool CanConvertFrom(ITypeDescriptorContext? context, Type sourceType)
{
// 排他模式,只有下拉列表中的值可选
return true;
return sourceType == typeof(string) || base.CanConvertFrom(context, sourceType);
}
public override StandardValuesCollection? GetStandardValues(ITypeDescriptorContext? context)
public override object? ConvertFrom(ITypeDescriptorContext? context, CultureInfo? culture, object value)
{
return new StandardValuesCollection(supportedFileSuffix);
if (value is string s)
{
s = s.Trim();
if (s.StartsWith("#") && s.Length == 9)
{
try
{
// 解析 R, G, B, A 分量注意16进制解析
byte r = byte.Parse(s.Substring(1, 2), NumberStyles.HexNumber);
byte g = byte.Parse(s.Substring(3, 2), NumberStyles.HexNumber);
byte b = byte.Parse(s.Substring(5, 2), NumberStyles.HexNumber);
byte a = byte.Parse(s.Substring(7, 2), NumberStyles.HexNumber);
return new SFML.Graphics.Color(r, g, b, a);
}
catch (Exception ex)
{
throw new FormatException("无法解析颜色,确保格式为 #RRGGBBAA", ex);
}
}
throw new FormatException("格式错误,正确格式为 #RRGGBBAA");
}
return base.ConvertFrom(context, culture, value);
}
public override bool CanConvertTo(ITypeDescriptorContext? context, Type? destinationType)
{
return destinationType == typeof(string) || base.CanConvertTo(context, destinationType);
}
public override object? ConvertTo(ITypeDescriptorContext? context, CultureInfo? culture, object? value, Type destinationType)
{
if (destinationType == typeof(string) && value is SFML.Graphics.Color color)
return $"#{color.R:X2}{color.G:X2}{color.B:X2}{color.A:X2}";
return base.ConvertTo(context, culture, value, destinationType);
}
public override PropertyDescriptorCollection GetProperties(ITypeDescriptorContext? context, object value, Attribute[]? attributes)
{
// 自定义属性集合
var properties = new List<PropertyDescriptor>
{
// 定义 R, G, B, A 四个字段的描述器
new SFMLColorPropertyDescriptor(typeof(SFML.Graphics.Color), "R", typeof(byte)),
new SFMLColorPropertyDescriptor(typeof(SFML.Graphics.Color), "G", typeof(byte)),
new SFMLColorPropertyDescriptor(typeof(SFML.Graphics.Color), "B", typeof(byte)),
new SFMLColorPropertyDescriptor(typeof(SFML.Graphics.Color), "A", typeof(byte))
};
// 返回自定义属性集合
return new PropertyDescriptorCollection(properties.ToArray());
}
}
}

View File

@@ -0,0 +1,66 @@
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Drawing.Design;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace SpineViewer.Exporter
{
class SFMLColorEditor : UITypeEditor
{
public override bool GetPaintValueSupported(ITypeDescriptorContext? context) => true;
public override void PaintValue(PaintValueEventArgs e)
{
if (e.Value is SFML.Graphics.Color color)
{
// 定义颜色和透明度的绘制区域
var colorBox = new Rectangle(e.Bounds.X, e.Bounds.Y, e.Bounds.Width / 2, e.Bounds.Height);
var alphaBox = new Rectangle(e.Bounds.X + e.Bounds.Width / 2, e.Bounds.Y, e.Bounds.Width / 2, e.Bounds.Height);
// 转换为 System.Drawing.Color
var drawColor = Color.FromArgb(color.A, color.R, color.G, color.B);
// 绘制纯颜色RGB 部分)
using (var brush = new SolidBrush(Color.FromArgb(color.R, color.G, color.B)))
{
e.Graphics.FillRectangle(brush, colorBox);
e.Graphics.DrawRectangle(Pens.Black, colorBox);
}
// 绘制带透明度效果的颜色
using (var checkerBrush = CreateTransparencyBrush())
{
e.Graphics.FillRectangle(checkerBrush, alphaBox); // 背景棋盘格
}
using (var brush = new SolidBrush(drawColor))
{
e.Graphics.FillRectangle(brush, alphaBox); // 叠加透明颜色
e.Graphics.DrawRectangle(Pens.Black, alphaBox);
}
}
else
{
base.PaintValue(e);
}
}
// 创建一个透明背景的棋盘格图案画刷
private static TextureBrush CreateTransparencyBrush()
{
var bitmap = new Bitmap(8, 8);
using (var g = Graphics.FromImage(bitmap))
{
g.Clear(Color.White);
using (var grayBrush = new SolidBrush(Color.LightGray))
{
g.FillRectangle(grayBrush, 0, 0, 4, 4);
g.FillRectangle(grayBrush, 4, 4, 4, 4);
}
}
return new TextureBrush(bitmap);
}
}
}

View File

@@ -0,0 +1,65 @@
using SpineViewer.Exporter;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Text;
using System.Threading.Tasks;
namespace SpineViewer
{
public interface IImplementationKey<TKey>
{
TKey ImplementationKey { get; }
}
/// <summary>
/// 可以使用反射查找基类关联的所有实现类
/// </summary>
/// <typeparam name="TBase">所有实现类的基类型</typeparam>
/// <typeparam name="TAttr">实现类类型属性标记类型</typeparam>
/// /// <typeparam name="TKey">实现类类型标记类型</typeparam>
public abstract class ImplementationResolver<TBase, TAttr, TKey> where TAttr : Attribute, IImplementationKey<TKey>
{
/// <summary>
/// 实现类型缓存
/// </summary>
private static readonly Dictionary<TKey, Type> ImplementationTypes = new();
static ImplementationResolver()
{
var baseType = typeof(TBase);
var impTypes = Assembly.GetExecutingAssembly().GetTypes().Where(t => baseType.IsAssignableFrom(t) && !t.IsAbstract);
foreach (var type in impTypes)
{
foreach (var attr in type.GetCustomAttributes<TAttr>())
{
var key = attr.ImplementationKey;
if (ImplementationTypes.ContainsKey(key))
throw new InvalidOperationException($"Multiple implementations found for key: {key}");
ImplementationTypes[key] = type;
}
}
NLog.LogManager.GetCurrentClassLogger().Debug("Found implementations for {}: {}", baseType, string.Join(", ", ImplementationTypes.Keys));
}
/// <summary>
/// 判断某种类型是否实现
/// </summary>
public static bool HasImplementation(TKey key) => ImplementationTypes.ContainsKey(key);
/// <summary>
/// 根据实现类键和参数创建实例
/// </summary>
/// <param name="impKey"></param>
/// <param name="args"></param>
/// <returns></returns>
/// <exception cref="NotImplementedException"></exception>
protected static TBase New(TKey impKey, object?[] args)
{
if (!ImplementationTypes.TryGetValue(impKey, out var type))
throw new NotImplementedException($"Not implemented type for {typeof(TBase)}: {impKey}");
return (TBase)Activator.CreateInstance(type, args);
}
}
}

View File

@@ -43,6 +43,7 @@
toolStripMenuItem_ExportMp4 = new ToolStripMenuItem();
toolStripMenuItem_ExportMov = new ToolStripMenuItem();
toolStripMenuItem_ExportWebm = new ToolStripMenuItem();
toolStripMenuItem_ExportCustom = new ToolStripMenuItem();
toolStripSeparator2 = new ToolStripSeparator();
toolStripMenuItem_Exit = new ToolStripMenuItem();
toolStripMenuItem_Tool = new ToolStripMenuItem();
@@ -61,9 +62,9 @@
spineListView = new SpineViewer.Controls.SpineListView();
propertyGrid_Spine = new PropertyGrid();
splitContainer_Config = new SplitContainer();
groupBox_SkelConfig = new GroupBox();
groupBox_PreviewConfig = new GroupBox();
propertyGrid_Previewer = new PropertyGrid();
groupBox_SkelConfig = new GroupBox();
groupBox_Preview = new GroupBox();
spinePreviewer = new SpineViewer.Controls.SpinePreviewer();
panel_MainForm = new Panel();
@@ -86,8 +87,8 @@
splitContainer_Config.Panel1.SuspendLayout();
splitContainer_Config.Panel2.SuspendLayout();
splitContainer_Config.SuspendLayout();
groupBox_SkelConfig.SuspendLayout();
groupBox_PreviewConfig.SuspendLayout();
groupBox_SkelConfig.SuspendLayout();
groupBox_Preview.SuspendLayout();
panel_MainForm.SuspendLayout();
SuspendLayout();
@@ -99,7 +100,7 @@
menuStrip.Items.AddRange(new ToolStripItem[] { toolStripMenuItem_File, toolStripMenuItem_Tool, toolStripMenuItem_Download, toolStripMenuItem_Help });
menuStrip.Location = new Point(0, 0);
menuStrip.Name = "menuStrip";
menuStrip.Size = new Size(1748, 32);
menuStrip.Size = new Size(1778, 32);
menuStrip.TabIndex = 0;
menuStrip.Text = "菜单";
//
@@ -114,88 +115,95 @@
//
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.DropDownItems.AddRange(new ToolStripItem[] { toolStripMenuItem_ExportFrame, toolStripMenuItem_ExportFrameSequence, toolStripMenuItem_ExportGif, toolStripMenuItem_ExportMp4, toolStripMenuItem_ExportWebm, toolStripMenuItem_ExportMkv, toolStripMenuItem_ExportMov, toolStripMenuItem_ExportCustom });
toolStripMenuItem_Export.Name = "toolStripMenuItem_Export";
toolStripMenuItem_Export.Size = new Size(254, 34);
toolStripMenuItem_Export.Size = new Size(270, 34);
toolStripMenuItem_Export.Text = "导出(&E)";
//
// toolStripMenuItem_ExportFrame
//
toolStripMenuItem_ExportFrame.Name = "toolStripMenuItem_ExportFrame";
toolStripMenuItem_ExportFrame.Size = new Size(194, 34);
toolStripMenuItem_ExportFrame.Size = new Size(288, 34);
toolStripMenuItem_ExportFrame.Text = "单帧画面...";
toolStripMenuItem_ExportFrame.Click += toolStripMenuItem_Export_Click;
//
// toolStripMenuItem_ExportFrameSequence
//
toolStripMenuItem_ExportFrameSequence.Name = "toolStripMenuItem_ExportFrameSequence";
toolStripMenuItem_ExportFrameSequence.Size = new Size(194, 34);
toolStripMenuItem_ExportFrameSequence.Size = new Size(288, 34);
toolStripMenuItem_ExportFrameSequence.Text = "帧序列...";
toolStripMenuItem_ExportFrameSequence.Click += toolStripMenuItem_Export_Click;
//
// toolStripMenuItem_ExportGif
//
toolStripMenuItem_ExportGif.Name = "toolStripMenuItem_ExportGif";
toolStripMenuItem_ExportGif.Size = new Size(194, 34);
toolStripMenuItem_ExportGif.Size = new Size(288, 34);
toolStripMenuItem_ExportGif.Text = "GIF...";
toolStripMenuItem_ExportGif.Click += toolStripMenuItem_Export_Click;
//
// toolStripMenuItem_ExportMkv
//
toolStripMenuItem_ExportMkv.Name = "toolStripMenuItem_ExportMkv";
toolStripMenuItem_ExportMkv.Size = new Size(194, 34);
toolStripMenuItem_ExportMkv.Text = "MKV";
toolStripMenuItem_ExportMkv.Size = new Size(288, 34);
toolStripMenuItem_ExportMkv.Text = "MKV...";
toolStripMenuItem_ExportMkv.Click += toolStripMenuItem_Export_Click;
//
// toolStripMenuItem_ExportMp4
//
toolStripMenuItem_ExportMp4.Name = "toolStripMenuItem_ExportMp4";
toolStripMenuItem_ExportMp4.Size = new Size(194, 34);
toolStripMenuItem_ExportMp4.Size = new Size(288, 34);
toolStripMenuItem_ExportMp4.Text = "MP4...";
toolStripMenuItem_ExportMp4.Click += toolStripMenuItem_Export_Click;
//
// toolStripMenuItem_ExportMov
//
toolStripMenuItem_ExportMov.Name = "toolStripMenuItem_ExportMov";
toolStripMenuItem_ExportMov.Size = new Size(194, 34);
toolStripMenuItem_ExportMov.Size = new Size(288, 34);
toolStripMenuItem_ExportMov.Text = "MOV...";
toolStripMenuItem_ExportMov.Click += toolStripMenuItem_Export_Click;
//
// toolStripMenuItem_ExportWebm
//
toolStripMenuItem_ExportWebm.Name = "toolStripMenuItem_ExportWebm";
toolStripMenuItem_ExportWebm.Size = new Size(194, 34);
toolStripMenuItem_ExportWebm.Size = new Size(288, 34);
toolStripMenuItem_ExportWebm.Text = "WebM...";
toolStripMenuItem_ExportWebm.Click += toolStripMenuItem_Export_Click;
//
// toolStripMenuItem_ExportCustom
//
toolStripMenuItem_ExportCustom.Name = "toolStripMenuItem_ExportCustom";
toolStripMenuItem_ExportCustom.Size = new Size(288, 34);
toolStripMenuItem_ExportCustom.Text = "FFmpeg 自定义导出...";
toolStripMenuItem_ExportCustom.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;
//
@@ -263,7 +271,7 @@
rtbLog.Margin = new Padding(3, 2, 3, 2);
rtbLog.Name = "rtbLog";
rtbLog.ReadOnly = true;
rtbLog.Size = new Size(1728, 110);
rtbLog.Size = new Size(1758, 146);
rtbLog.TabIndex = 0;
rtbLog.Text = "";
rtbLog.WordWrap = false;
@@ -272,6 +280,7 @@
//
splitContainer_MainForm.Cursor = Cursors.SizeNS;
splitContainer_MainForm.Dock = DockStyle.Fill;
splitContainer_MainForm.FixedPanel = FixedPanel.Panel2;
splitContainer_MainForm.Location = new Point(10, 5);
splitContainer_MainForm.Name = "splitContainer_MainForm";
splitContainer_MainForm.Orientation = Orientation.Horizontal;
@@ -285,8 +294,8 @@
//
splitContainer_MainForm.Panel2.Controls.Add(rtbLog);
splitContainer_MainForm.Panel2.Cursor = Cursors.Default;
splitContainer_MainForm.Size = new Size(1728, 997);
splitContainer_MainForm.SplitterDistance = 879;
splitContainer_MainForm.Size = new Size(1758, 1097);
splitContainer_MainForm.SplitterDistance = 943;
splitContainer_MainForm.SplitterWidth = 8;
splitContainer_MainForm.TabIndex = 3;
splitContainer_MainForm.TabStop = false;
@@ -297,6 +306,7 @@
//
splitContainer_Functional.Cursor = Cursors.SizeWE;
splitContainer_Functional.Dock = DockStyle.Fill;
splitContainer_Functional.FixedPanel = FixedPanel.Panel1;
splitContainer_Functional.Location = new Point(0, 0);
splitContainer_Functional.Name = "splitContainer_Functional";
//
@@ -309,8 +319,8 @@
//
splitContainer_Functional.Panel2.Controls.Add(groupBox_Preview);
splitContainer_Functional.Panel2.Cursor = Cursors.Default;
splitContainer_Functional.Size = new Size(1728, 879);
splitContainer_Functional.SplitterDistance = 747;
splitContainer_Functional.Size = new Size(1758, 943);
splitContainer_Functional.SplitterDistance = 759;
splitContainer_Functional.SplitterWidth = 8;
splitContainer_Functional.TabIndex = 2;
splitContainer_Functional.TabStop = false;
@@ -333,8 +343,8 @@
//
splitContainer_Information.Panel2.Controls.Add(splitContainer_Config);
splitContainer_Information.Panel2.Cursor = Cursors.Default;
splitContainer_Information.Size = new Size(747, 879);
splitContainer_Information.SplitterDistance = 399;
splitContainer_Information.Size = new Size(759, 943);
splitContainer_Information.SplitterDistance = 354;
splitContainer_Information.SplitterWidth = 8;
splitContainer_Information.TabIndex = 1;
splitContainer_Information.TabStop = false;
@@ -347,7 +357,7 @@
groupBox_SkelList.Dock = DockStyle.Fill;
groupBox_SkelList.Location = new Point(0, 0);
groupBox_SkelList.Name = "groupBox_SkelList";
groupBox_SkelList.Size = new Size(399, 879);
groupBox_SkelList.Size = new Size(354, 943);
groupBox_SkelList.TabIndex = 0;
groupBox_SkelList.TabStop = false;
groupBox_SkelList.Text = "模型列表";
@@ -358,7 +368,7 @@
spineListView.Location = new Point(3, 26);
spineListView.Name = "spineListView";
spineListView.PropertyGrid = propertyGrid_Spine;
spineListView.Size = new Size(393, 850);
spineListView.Size = new Size(348, 914);
spineListView.TabIndex = 0;
//
// propertyGrid_Spine
@@ -367,7 +377,7 @@
propertyGrid_Spine.HelpVisible = false;
propertyGrid_Spine.Location = new Point(3, 26);
propertyGrid_Spine.Name = "propertyGrid_Spine";
propertyGrid_Spine.Size = new Size(334, 509);
propertyGrid_Spine.Size = new Size(391, 580);
propertyGrid_Spine.TabIndex = 0;
propertyGrid_Spine.ToolbarVisible = false;
propertyGrid_Spine.PropertyValueChanged += propertyGrid_PropertyValueChanged;
@@ -376,6 +386,7 @@
//
splitContainer_Config.Cursor = Cursors.SizeNS;
splitContainer_Config.Dock = DockStyle.Fill;
splitContainer_Config.FixedPanel = FixedPanel.Panel1;
splitContainer_Config.Location = new Point(0, 0);
splitContainer_Config.Name = "splitContainer_Config";
splitContainer_Config.Orientation = Orientation.Horizontal;
@@ -389,32 +400,21 @@
//
splitContainer_Config.Panel2.Controls.Add(groupBox_SkelConfig);
splitContainer_Config.Panel2.Cursor = Cursors.Default;
splitContainer_Config.Size = new Size(340, 879);
splitContainer_Config.SplitterDistance = 333;
splitContainer_Config.Size = new Size(397, 943);
splitContainer_Config.SplitterDistance = 326;
splitContainer_Config.SplitterWidth = 8;
splitContainer_Config.TabIndex = 0;
splitContainer_Config.TabStop = false;
splitContainer_Config.SplitterMoved += splitContainer_SplitterMoved;
splitContainer_Config.MouseUp += splitContainer_MouseUp;
//
// groupBox_SkelConfig
//
groupBox_SkelConfig.Controls.Add(propertyGrid_Spine);
groupBox_SkelConfig.Dock = DockStyle.Fill;
groupBox_SkelConfig.Location = new Point(0, 0);
groupBox_SkelConfig.Name = "groupBox_SkelConfig";
groupBox_SkelConfig.Size = new Size(340, 538);
groupBox_SkelConfig.TabIndex = 0;
groupBox_SkelConfig.TabStop = false;
groupBox_SkelConfig.Text = "模型参数";
//
// groupBox_PreviewConfig
//
groupBox_PreviewConfig.Controls.Add(propertyGrid_Previewer);
groupBox_PreviewConfig.Dock = DockStyle.Fill;
groupBox_PreviewConfig.Location = new Point(0, 0);
groupBox_PreviewConfig.Name = "groupBox_PreviewConfig";
groupBox_PreviewConfig.Size = new Size(340, 333);
groupBox_PreviewConfig.Size = new Size(397, 326);
groupBox_PreviewConfig.TabIndex = 1;
groupBox_PreviewConfig.TabStop = false;
groupBox_PreviewConfig.Text = "画面参数";
@@ -425,18 +425,29 @@
propertyGrid_Previewer.HelpVisible = false;
propertyGrid_Previewer.Location = new Point(3, 26);
propertyGrid_Previewer.Name = "propertyGrid_Previewer";
propertyGrid_Previewer.Size = new Size(334, 304);
propertyGrid_Previewer.Size = new Size(391, 297);
propertyGrid_Previewer.TabIndex = 1;
propertyGrid_Previewer.ToolbarVisible = false;
propertyGrid_Previewer.PropertyValueChanged += propertyGrid_PropertyValueChanged;
//
// groupBox_SkelConfig
//
groupBox_SkelConfig.Controls.Add(propertyGrid_Spine);
groupBox_SkelConfig.Dock = DockStyle.Fill;
groupBox_SkelConfig.Location = new Point(0, 0);
groupBox_SkelConfig.Name = "groupBox_SkelConfig";
groupBox_SkelConfig.Size = new Size(397, 609);
groupBox_SkelConfig.TabIndex = 0;
groupBox_SkelConfig.TabStop = false;
groupBox_SkelConfig.Text = "模型参数";
//
// groupBox_Preview
//
groupBox_Preview.Controls.Add(spinePreviewer);
groupBox_Preview.Dock = DockStyle.Fill;
groupBox_Preview.Location = new Point(0, 0);
groupBox_Preview.Name = "groupBox_Preview";
groupBox_Preview.Size = new Size(973, 879);
groupBox_Preview.Size = new Size(991, 943);
groupBox_Preview.TabIndex = 1;
groupBox_Preview.TabStop = false;
groupBox_Preview.Text = "预览画面";
@@ -447,7 +458,7 @@
spinePreviewer.Location = new Point(3, 26);
spinePreviewer.Name = "spinePreviewer";
spinePreviewer.PropertyGrid = propertyGrid_Previewer;
spinePreviewer.Size = new Size(967, 850);
spinePreviewer.Size = new Size(985, 914);
spinePreviewer.SpineListView = spineListView;
spinePreviewer.TabIndex = 0;
//
@@ -458,7 +469,7 @@
panel_MainForm.Location = new Point(0, 32);
panel_MainForm.Name = "panel_MainForm";
panel_MainForm.Padding = new Padding(10, 5, 10, 10);
panel_MainForm.Size = new Size(1748, 1012);
panel_MainForm.Size = new Size(1778, 1112);
panel_MainForm.TabIndex = 4;
//
// toolTip
@@ -469,7 +480,7 @@
//
AutoScaleDimensions = new SizeF(11F, 24F);
AutoScaleMode = AutoScaleMode.Font;
ClientSize = new Size(1748, 1044);
ClientSize = new Size(1778, 1144);
Controls.Add(panel_MainForm);
Controls.Add(menuStrip);
Icon = (Icon)resources.GetObject("$this.Icon");
@@ -499,8 +510,8 @@
splitContainer_Config.Panel2.ResumeLayout(false);
((System.ComponentModel.ISupportInitialize)splitContainer_Config).EndInit();
splitContainer_Config.ResumeLayout(false);
groupBox_SkelConfig.ResumeLayout(false);
groupBox_PreviewConfig.ResumeLayout(false);
groupBox_SkelConfig.ResumeLayout(false);
groupBox_Preview.ResumeLayout(false);
panel_MainForm.ResumeLayout(false);
ResumeLayout(false);
@@ -547,5 +558,6 @@
private ToolStripMenuItem toolStripMenuItem_ExportMov;
private ToolStripMenuItem toolStripMenuItem_ExportMkv;
private ToolStripMenuItem toolStripMenuItem_ExportWebm;
private ToolStripMenuItem toolStripMenuItem_ExportCustom;
}
}

View File

@@ -12,8 +12,10 @@ using SpineViewer.Exporter;
namespace SpineViewer
{
public partial class MainForm : Form
internal partial class MainForm : Form
{
private Logger logger = LogManager.GetCurrentClassLogger();
public MainForm()
{
InitializeComponent();
@@ -22,11 +24,25 @@ namespace SpineViewer
// 在此处将导出菜单需要的类绑定起来
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;
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;
toolStripMenuItem_ExportCustom.Tag = ExportType.Custom;
// 执行一些初始化工作
try
{
Spine.Shader.Init();
}
catch (Exception ex)
{
logger.Error(ex.ToString());
logger.Error("Failed to load fragment shader");
MessageBox.Warn("Fragment shader 加载失败预乘Alpha通道属性失效");
}
}
/// <summary>
@@ -146,10 +162,12 @@ namespace SpineViewer
{
var worker = (BackgroundWorker)sender;
var exporter = (Exporter.Exporter)e.Argument;
Invoke(() => TaskbarManager.SetProgressState(Handle, TBPFLAG.TBPF_INDETERMINATE));
spinePreviewer.StopRender();
lock (spineListView.Spines) { exporter.Export(spineListView.Spines.Where(sp => !sp.IsHidden).ToArray(), (BackgroundWorker)sender); }
e.Cancel = worker.CancellationPending;
spinePreviewer.StartRender();
Invoke(() => TaskbarManager.SetProgressState(Handle, TBPFLAG.TBPF_NOPROGRESS));
}
private void ConvertFileFormat_Work(object? sender, DoWorkEventArgs e)
@@ -166,7 +184,7 @@ namespace SpineViewer
int success = 0;
int error = 0;
SkeletonConverter srcCvter = srcVersion != Spine.Version.Auto ? SkeletonConverter.New(srcVersion) : null;
SkeletonConverter srcCvter = srcVersion != SpineVersion.Auto ? SkeletonConverter.New(srcVersion) : null;
SkeletonConverter tgtCvter = SkeletonConverter.New(tgtVersion);
worker.ReportProgress(0, $"已处理 0/{totalCount}");
@@ -183,12 +201,16 @@ namespace SpineViewer
try
{
if (srcVersion == Spine.Version.Auto)
if (srcVersion == SpineVersion.Auto)
{
if (Spine.Spine.GetVersion(skelPath) is Spine.Version detectedSrcVersion)
srcCvter = SkeletonConverter.New(detectedSrcVersion);
else
throw new InvalidDataException($"Auto version detection failed for {skelPath}, try to use a specific version");
try
{
srcCvter = SkeletonConverter.New(SpineHelper.GetVersion(skelPath));
}
catch (Exception ex)
{
throw new InvalidDataException($"Auto version detection failed for {skelPath}, try to use a specific version", ex);
}
}
var root = srcCvter.Read(skelPath);
root = srcCvter.ToVersion(root, tgtVersion);
@@ -197,8 +219,8 @@ namespace SpineViewer
}
catch (Exception ex)
{
Program.Logger.Error(ex.ToString());
Program.Logger.Error("Failed to convert {}", skelPath);
logger.Error(ex.ToString());
logger.Error("Failed to convert {}", skelPath);
error++;
}
@@ -207,11 +229,11 @@ namespace SpineViewer
if (error > 0)
{
Program.Logger.Warn("Batch convert {} successfully, {} failed", success, error);
logger.Warn("Batch convert {} successfully, {} failed", success, error);
}
else
{
Program.Logger.Info("{} skel converted successfully", success);
logger.Info("{} skel converted successfully", success);
}
}

View File

@@ -0,0 +1,21 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace SpineViewer
{
public static class NLogExtension
{
/// <summary>
/// 输出当前进程的内存占用
/// </summary>
public static void LogCurrentProcessMemoryUsage(this NLog.Logger logger)
{
var process = Process.GetCurrentProcess();
logger.Info("Current memory usage for {}: {:F2} MB", process.ProcessName, process.WorkingSet64 / 1024.0 / 1024.0);
}
}
}

View File

@@ -5,35 +5,30 @@ namespace SpineViewer
{
internal static class Program
{
/// <summary>
/// 程序路径
/// </summary>
public static readonly string FilePath = Environment.ProcessPath;
///// <summary>
///// 程序路径
///// </summary>
//public static readonly string FilePath = Environment.ProcessPath;
/// <summary>
/// 程序名
/// </summary>
public static readonly string Name = Path.GetFileNameWithoutExtension(FilePath);
///// <summary>
///// 程序名
///// </summary>
//public static readonly string Name = Path.GetFileNameWithoutExtension(FilePath);
/// <summary>
/// 程序目录
/// </summary>
public static readonly string RootDir = Path.GetDirectoryName(FilePath);
///// <summary>
///// 程序目录
///// </summary>
//public static readonly string RootDir = Path.GetDirectoryName(FilePath);
/// <summary>
/// 程序临时目录
/// </summary>
public static readonly string TempDir = Directory.CreateDirectory(Path.Combine(Path.GetTempPath(), Name)).FullName;
/// <summary>
/// 程序进程
/// </summary>
public static readonly Process Process = Process.GetCurrentProcess();
///// <summary>
///// 程序临时目录
///// </summary>
//public static readonly string TempDir = Directory.CreateDirectory(Path.Combine(Path.GetTempPath(), Name)).FullName;
/// <summary>
/// 程序日志器
/// </summary>
public static readonly Logger Logger = LogManager.GetCurrentClassLogger();
private static readonly Logger logger = LogManager.GetCurrentClassLogger();
/// <summary>
/// 应用入口点
@@ -41,8 +36,9 @@ namespace SpineViewer
[STAThread]
static void Main()
{
// 此处先初始化全局配置再触发静态字段 Logger 引用构造, 才能将配置应用到新的日志器上
InitializeLogConfiguration();
Logger.Info("Program Started");
logger.Info("Program Started");
// To customize application configuration such as set high DPI settings or default font,
// see https://aka.ms/applicationconfiguration.
@@ -54,7 +50,7 @@ namespace SpineViewer
}
catch (Exception ex)
{
Logger.Fatal(ex.ToString());
logger.Fatal(ex.ToString());
MessageBox.Error(ex.ToString(), "程序已崩溃");
}
}
@@ -82,10 +78,5 @@ namespace SpineViewer
config.AddRule(LogLevel.Debug, LogLevel.Fatal, fileTarget);
LogManager.Configuration = config;
}
/// <summary>
/// 输出当前内存使用情况
/// </summary>
public static void LogCurrentMemoryUsage() => Logger.Info("Current memory usage: {:F2} MB", Process.WorkingSet64 / 1024.0 / 1024.0);
}
}

View File

@@ -11,7 +11,7 @@ using System.Globalization;
namespace SpineViewer.Spine.Implementations.SkeletonConverter
{
[SpineImplementation(Version.V38)]
[SpineImplementation(SpineVersion.V38)]
class SkeletonConverter38 : SpineViewer.Spine.SkeletonConverter
{
private BinaryReader reader = null;
@@ -1286,11 +1286,11 @@ namespace SpineViewer.Spine.Implementations.SkeletonConverter
base.WriteJson(root, jsonPath);
}
public override JsonObject ToVersion(JsonObject root, Version version)
public override JsonObject ToVersion(JsonObject root, SpineVersion version)
{
root = version switch
{
Version.V38 => root.DeepClone().AsObject(),
SpineVersion.V38 => root.DeepClone().AsObject(),
_ => throw new NotImplementedException(),
};
return root;

View File

@@ -10,7 +10,7 @@ using SpineRuntime21;
namespace SpineViewer.Spine.Implementations.Spine
{
[SpineImplementation(Version.V21)]
[SpineImplementation(SpineVersion.V21)]
internal class Spine21 : SpineViewer.Spine.Spine
{
private static readonly Animation EmptyAnimation = new(EMPTY_ANIMATION, [], 0);
@@ -47,7 +47,7 @@ namespace SpineViewer.Spine.Implementations.Spine
// 2.1.x 不支持剪裁
//private SkeletonClipping clipping = new();
public Spine21(string skelPath, string? atlasPath = null) : base(skelPath, atlasPath)
public Spine21(string skelPath, string atlasPath) : base(skelPath, atlasPath)
{
atlas = new Atlas(AtlasPath, textureLoader);
try
@@ -76,16 +76,12 @@ namespace SpineViewer.Spine.Implementations.Spine
foreach (var skin in skeletonData.Skins)
skinNames.Add(skin.Name);
animationStateData = new AnimationStateData(skeletonData);
skeleton = new Skeleton(skeletonData);
animationState = new AnimationState(animationStateData);
foreach (var anime in skeletonData.Animations)
animationNames.Add(anime.Name);
// 取最后一个作为初始, 尽可能去显示非默认的内容
CurrentAnimation = animationNames.Last();
CurrentSkin = skinNames.Last();
skeleton = new Skeleton(skeletonData);
animationStateData = new AnimationStateData(skeletonData);
animationState = new AnimationState(animationStateData);
}
protected override void Dispose(bool disposing)
@@ -96,7 +92,7 @@ namespace SpineViewer.Spine.Implementations.Spine
public override string FileVersion { get => skeletonData.Version; }
public override float Scale
protected override float scale
{
get
{
@@ -110,11 +106,11 @@ namespace SpineViewer.Spine.Implementations.Spine
set
{
// 保存状态
var position = Position;
var flipX = FlipX;
var flipY = FlipY;
var animation = CurrentAnimation;
var skin = CurrentSkin;
var pos = position;
var fX = flipX;
var fY = flipY;
var animation = track0Animation; // TODO: 适配多轨道
var sk = skin;
var val = Math.Max(value, SCALE_MIN);
if (skeletonBinary is not null)
@@ -134,37 +130,48 @@ namespace SpineViewer.Spine.Implementations.Spine
animationState = new AnimationState(animationStateData);
// 恢复状态
Position = position;
FlipX = flipX;
FlipY = flipY;
CurrentAnimation = animation;
CurrentSkin = skin;
position = pos;
flipX = fX;
flipY = fY;
track0Animation = animation; // TODO: 适配多轨道
skin = sk;
}
}
public override PointF Position
{
get => new(skeleton.X, skeleton.Y);
set
{
skeleton.X = value.X;
skeleton.Y = value.Y;
}
protected override PointF position
{
get => new(skeleton.X, skeleton.Y);
set
{
skeleton.X = value.X;
skeleton.Y = value.Y;
}
}
public override bool FlipX
protected override bool flipX
{
get => skeleton.FlipX;
set => skeleton.FlipX = value;
}
public override bool FlipY
protected override bool flipY
{
get => skeleton.FlipY;
set => skeleton.FlipY = value;
}
public override string CurrentAnimation
protected override string skin
{
get => skeleton.Skin?.Name ?? "default";
set
{
if (!skinNames.Contains(value)) return;
skeleton.SetSkin(value);
skeleton.SetSlotsToSetupPose();
}
}
protected override string track0Animation
{
get => animationState.GetCurrent(0)?.Animation.Name ?? EMPTY_ANIMATION;
set
@@ -173,23 +180,10 @@ namespace SpineViewer.Spine.Implementations.Spine
animationState.SetAnimation(0, EmptyAnimation, false);
else if (animationNames.Contains(value))
animationState.SetAnimation(0, value, true);
Update(0);
}
}
public override string CurrentSkin
{
get => skeleton.Skin?.Name ?? "default";
set
{
if (!skinNames.Contains(value)) return;
skeleton.SetSkin(value);
skeleton.SetToSetupPose();
Update(0);
}
}
public override RectangleF Bounds
protected override RectangleF bounds
{
get
{
@@ -241,7 +235,7 @@ namespace SpineViewer.Spine.Implementations.Spine
public override float GetAnimationDuration(string name) { return skeletonData.FindAnimation(name)?.Duration ?? 0f; }
public override void Update(float delta)
protected override void update(float delta)
{
animationState.Update(delta);
animationState.Apply(skeleton);
@@ -261,7 +255,7 @@ namespace SpineViewer.Spine.Implementations.Spine
// };
//}
public override void Draw(SFML.Graphics.RenderTarget target, SFML.Graphics.RenderStates states)
protected override void draw(SFML.Graphics.RenderTarget target, SFML.Graphics.RenderStates states)
{
vertexArray.Clear();
states.Texture = null;
@@ -333,13 +327,13 @@ namespace SpineViewer.Spine.Implementations.Spine
{
if (vertexArray.VertexCount > 0)
{
if (UsePremultipliedAlpha && (states.BlendMode == BlendModeSFML.Normal || states.BlendMode == BlendModeSFML.Additive))
states.Shader = FragmentShader;
if (usePremultipliedAlpha && (states.BlendMode == BlendModeSFML.Normal || states.BlendMode == BlendModeSFML.Additive))
states.Shader = Shader.FragmentShader;
else
states.Shader = null;
// 调试纹理
if (!IsDebug || DebugTexture)
if (!isDebug || debugTexture)
target.Draw(vertexArray, states);
vertexArray.Clear();
@@ -382,24 +376,24 @@ namespace SpineViewer.Spine.Implementations.Spine
//clipping.ClipEnd(slot);
}
if (UsePremultipliedAlpha && (states.BlendMode == BlendModeSFML.Normal || states.BlendMode == BlendModeSFML.Additive))
states.Shader = FragmentShader;
if (usePremultipliedAlpha && (states.BlendMode == BlendModeSFML.Normal || states.BlendMode == BlendModeSFML.Additive))
states.Shader = Shader.FragmentShader;
else
states.Shader = null;
//clipping.ClipEnd();
// 调试纹理
if (!IsDebug || DebugTexture)
if (!isDebug || debugTexture)
target.Draw(vertexArray, states);
// 包围盒
if (IsDebug && IsSelected && DebugBounds)
if (isDebug && isSelected && debugBounds)
{
var bounds = Bounds;
boundsVertices[0] = boundsVertices[4] = new(new(bounds.Left, bounds.Top), BoundsColor);
boundsVertices[1] = new(new(bounds.Right, bounds.Top), BoundsColor);
boundsVertices[2] = new(new(bounds.Right, bounds.Bottom), BoundsColor);
boundsVertices[3] = new(new(bounds.Left, bounds.Bottom), BoundsColor);
var b = bounds;
boundsVertices[0] = boundsVertices[4] = new(new(b.Left, b.Top), BoundsColor);
boundsVertices[1] = new(new(b.Right, b.Top), BoundsColor);
boundsVertices[2] = new(new(b.Right, b.Bottom), BoundsColor);
boundsVertices[3] = new(new(b.Left, b.Bottom), BoundsColor);
target.Draw(boundsVertices);
}
}

View File

@@ -10,7 +10,7 @@ using SpineRuntime36;
namespace SpineViewer.Spine.Implementations.Spine
{
[SpineImplementation(Version.V36)]
[SpineImplementation(SpineVersion.V36)]
internal class Spine36 : SpineViewer.Spine.Spine
{
private static readonly Animation EmptyAnimation = new(EMPTY_ANIMATION, [], 0);
@@ -46,7 +46,7 @@ namespace SpineViewer.Spine.Implementations.Spine
private SkeletonClipping clipping = new();
public Spine36(string skelPath, string? atlasPath = null) : base(skelPath, atlasPath)
public Spine36(string skelPath, string atlasPath) : base(skelPath, atlasPath)
{
atlas = new Atlas(AtlasPath, textureLoader);
try
@@ -75,16 +75,12 @@ namespace SpineViewer.Spine.Implementations.Spine
foreach (var skin in skeletonData.Skins)
skinNames.Add(skin.Name);
animationStateData = new AnimationStateData(skeletonData);
skeleton = new Skeleton(skeletonData);
animationState = new AnimationState(animationStateData);
foreach (var anime in skeletonData.Animations)
animationNames.Add(anime.Name);
// 取最后一个作为初始, 尽可能去显示非默认的内容
CurrentAnimation = animationNames.Last();
CurrentSkin = skinNames.Last();
skeleton = new Skeleton(skeletonData);
animationStateData = new AnimationStateData(skeletonData);
animationState = new AnimationState(animationStateData);
}
protected override void Dispose(bool disposing)
@@ -95,7 +91,7 @@ namespace SpineViewer.Spine.Implementations.Spine
public override string FileVersion { get => skeletonData.Version; }
public override float Scale
protected override float scale
{
get
{
@@ -109,11 +105,11 @@ namespace SpineViewer.Spine.Implementations.Spine
set
{
// 保存状态
var position = Position;
var flipX = FlipX;
var flipY = FlipY;
var animation = CurrentAnimation;
var skin = CurrentSkin;
var pos = position;
var fX = flipX;
var fY = flipY;
var animation = track0Animation; // TODO: 适配多轨道
var sk = skin;
var val = Math.Max(value, SCALE_MIN);
if (skeletonBinary is not null)
@@ -133,37 +129,48 @@ namespace SpineViewer.Spine.Implementations.Spine
animationState = new AnimationState(animationStateData);
// 恢复状态
Position = position;
FlipX = flipX;
FlipY = flipY;
CurrentAnimation = animation;
CurrentSkin = skin;
position = pos;
flipX = fX;
flipY = fY;
track0Animation = animation; // TODO: 适配多轨道
skin = sk;
}
}
public override PointF Position
protected override PointF position
{
get => new(skeleton.X, skeleton.Y);
set
{
skeleton.X = value.X;
skeleton.Y = value.Y;
skeleton.Y = value.Y;
}
}
public override bool FlipX
protected override bool flipX
{
get => skeleton.FlipX;
set => skeleton.FlipX = value;
}
public override bool FlipY
protected override bool flipY
{
get => skeleton.FlipY;
set => skeleton.FlipY = value;
}
public override string CurrentAnimation
protected override string skin
{
get => skeleton.Skin?.Name ?? "default";
set
{
if (!skinNames.Contains(value)) return;
skeleton.SetSkin(value);
skeleton.SetSlotsToSetupPose();
}
}
protected override string track0Animation
{
get => animationState.GetCurrent(0)?.Animation.Name ?? EMPTY_ANIMATION;
set
@@ -172,23 +179,10 @@ namespace SpineViewer.Spine.Implementations.Spine
animationState.SetAnimation(0, EmptyAnimation, false);
else if (animationNames.Contains(value))
animationState.SetAnimation(0, value, true);
Update(0);
}
}
public override string CurrentSkin
{
get => skeleton.Skin?.Name ?? "default";
set
{
if (!skinNames.Contains(value)) return;
skeleton.SetSkin(value);
skeleton.SetToSetupPose();
Update(0);
}
}
public override RectangleF Bounds
protected override RectangleF bounds
{
get
{
@@ -200,7 +194,7 @@ namespace SpineViewer.Spine.Implementations.Spine
public override float GetAnimationDuration(string name) { return skeletonData.FindAnimation(name)?.Duration ?? 0f; }
public override void Update(float delta)
protected override void update(float delta)
{
animationState.Update(delta);
animationState.Apply(skeleton);
@@ -220,7 +214,7 @@ namespace SpineViewer.Spine.Implementations.Spine
};
}
public override void Draw(SFML.Graphics.RenderTarget target, SFML.Graphics.RenderStates states)
protected override void draw(SFML.Graphics.RenderTarget target, SFML.Graphics.RenderStates states)
{
vertexArray.Clear();
states.Texture = null;
@@ -290,13 +284,13 @@ namespace SpineViewer.Spine.Implementations.Spine
{
if (vertexArray.VertexCount > 0)
{
if (UsePremultipliedAlpha && (states.BlendMode == BlendModeSFML.Normal || states.BlendMode == BlendModeSFML.Additive))
states.Shader = FragmentShader;
if (usePremultipliedAlpha && (states.BlendMode == BlendModeSFML.Normal || states.BlendMode == BlendModeSFML.Additive))
states.Shader = Shader.FragmentShader;
else
states.Shader = null;
// 调试纹理
if (!IsDebug || DebugTexture)
if (!isDebug || debugTexture)
target.Draw(vertexArray, states);
vertexArray.Clear();
@@ -340,23 +334,23 @@ namespace SpineViewer.Spine.Implementations.Spine
}
clipping.ClipEnd();
if (UsePremultipliedAlpha && (states.BlendMode == BlendModeSFML.Normal || states.BlendMode == BlendModeSFML.Additive))
states.Shader = FragmentShader;
if (usePremultipliedAlpha && (states.BlendMode == BlendModeSFML.Normal || states.BlendMode == BlendModeSFML.Additive))
states.Shader = Shader.FragmentShader;
else
states.Shader = null;
// 调试纹理
if (!IsDebug || DebugTexture)
if (!isDebug || debugTexture)
target.Draw(vertexArray, states);
// 包围盒
if (IsDebug && IsSelected && DebugBounds)
if (isDebug && isSelected && debugBounds)
{
var bounds = Bounds;
boundsVertices[0] = boundsVertices[4] = new(new(bounds.Left, bounds.Top), BoundsColor);
boundsVertices[1] = new(new(bounds.Right, bounds.Top), BoundsColor);
boundsVertices[2] = new(new(bounds.Right, bounds.Bottom), BoundsColor);
boundsVertices[3] = new(new(bounds.Left, bounds.Bottom), BoundsColor);
var b = bounds;
boundsVertices[0] = boundsVertices[4] = new(new(b.Left, b.Top), BoundsColor);
boundsVertices[1] = new(new(b.Right, b.Top), BoundsColor);
boundsVertices[2] = new(new(b.Right, b.Bottom), BoundsColor);
boundsVertices[3] = new(new(b.Left, b.Bottom), BoundsColor);
target.Draw(boundsVertices);
}
}

View File

@@ -7,7 +7,7 @@ using SpineRuntime37;
namespace SpineViewer.Spine.Implementations.Spine
{
[SpineImplementation(Version.V37)]
[SpineImplementation(SpineVersion.V37)]
internal class Spine37 : SpineViewer.Spine.Spine
{
private static readonly Animation EmptyAnimation = new(EMPTY_ANIMATION, [], 0);
@@ -44,7 +44,7 @@ namespace SpineViewer.Spine.Implementations.Spine
private SkeletonClipping clipping = new();
public Spine37(string skelPath, string? atlasPath = null) : base(skelPath, atlasPath)
public Spine37(string skelPath, string atlasPath) : base(skelPath, atlasPath)
{
atlas = new Atlas(AtlasPath, textureLoader);
try
@@ -73,16 +73,12 @@ namespace SpineViewer.Spine.Implementations.Spine
foreach (var skin in skeletonData.Skins)
skinNames.Add(skin.Name);
animationStateData = new AnimationStateData(skeletonData);
skeleton = new Skeleton(skeletonData);
animationState = new AnimationState(animationStateData);
foreach (var anime in skeletonData.Animations)
animationNames.Add(anime.Name);
// 取最后一个作为初始, 尽可能去显示非默认的内容
CurrentAnimation = animationNames.Last();
CurrentSkin = skinNames.Last();
skeleton = new Skeleton(skeletonData);
animationStateData = new AnimationStateData(skeletonData);
animationState = new AnimationState(animationStateData);
}
protected override void Dispose(bool disposing)
@@ -93,64 +89,17 @@ namespace SpineViewer.Spine.Implementations.Spine
public override string FileVersion { get => skeletonData.Version; }
public override float Scale
protected override float scale
{
get
{
if (skeletonBinary is not null)
return skeletonBinary.Scale;
else if (skeletonJson is not null)
return skeletonJson.Scale;
else
return 1f;
}
get => Math.Abs(skeleton.ScaleX);
set
{
// 保存状态
var position = Position;
var flipX = FlipX;
var flipY = FlipY;
var savedTrack0 = animationState.GetCurrent(0);
var val = Math.Max(value, SCALE_MIN);
if (skeletonBinary is not null)
{
skeletonBinary.Scale = val;
skeletonData = skeletonBinary.ReadSkeletonData(SkelPath);
}
else if (skeletonJson is not null)
{
skeletonJson.Scale = val;
skeletonData = skeletonJson.ReadSkeletonData(SkelPath);
}
// reload skel-dependent data
animationStateData = new AnimationStateData(skeletonData) { DefaultMix = animationStateData.DefaultMix };
skeleton = new Skeleton(skeletonData);
animationState = new AnimationState(animationStateData);
// 恢复状态
Position = position;
FlipX = flipX;
FlipY = flipY;
// 恢复原本 Track0 上所有动画
if (savedTrack0 is not null)
{
var entry = animationState.SetAnimation(0, savedTrack0.Animation.Name, true);
entry.TrackTime = savedTrack0.TrackTime;
var savedEntry = savedTrack0.Next;
while (savedEntry is not null)
{
entry = animationState.AddAnimation(0, savedEntry.Animation.Name, true, 0);
entry.TrackTime = savedEntry.TrackTime;
savedEntry = savedEntry.Next;
}
}
skeleton.ScaleX = Math.Sign(skeleton.ScaleX) * value;
skeleton.ScaleY = Math.Sign(skeleton.ScaleY) * value;
}
}
public override PointF Position
protected override PointF position
{
get => new(skeleton.X, skeleton.Y);
set
@@ -160,7 +109,7 @@ namespace SpineViewer.Spine.Implementations.Spine
}
}
public override bool FlipX
protected override bool flipX
{
get => skeleton.ScaleX < 0;
set
@@ -170,7 +119,7 @@ namespace SpineViewer.Spine.Implementations.Spine
}
}
public override bool FlipY
protected override bool flipY
{
get => skeleton.ScaleY < 0;
set
@@ -180,7 +129,18 @@ namespace SpineViewer.Spine.Implementations.Spine
}
}
public override string CurrentAnimation
protected override string skin
{
get => skeleton.Skin?.Name ?? "default";
set
{
if (!skinNames.Contains(value)) return;
skeleton.SetSkin(value);
skeleton.SetSlotsToSetupPose();
}
}
protected override string track0Animation
{
get => animationState.GetCurrent(0)?.Animation.Name ?? EMPTY_ANIMATION;
set
@@ -189,23 +149,10 @@ namespace SpineViewer.Spine.Implementations.Spine
animationState.SetAnimation(0, EmptyAnimation, false);
else if (animationNames.Contains(value))
animationState.SetAnimation(0, value, true);
Update(0);
}
}
public override string CurrentSkin
{
get => skeleton.Skin?.Name ?? "default";
set
{
if (!skinNames.Contains(value)) return;
skeleton.SetSkin(value);
skeleton.SetToSetupPose();
Update(0);
}
}
public override RectangleF Bounds
protected override RectangleF bounds
{
get
{
@@ -217,7 +164,7 @@ namespace SpineViewer.Spine.Implementations.Spine
public override float GetAnimationDuration(string name) { return skeletonData.FindAnimation(name)?.Duration ?? 0f; }
public override void Update(float delta)
protected override void update(float delta)
{
animationState.Update(delta);
animationState.Apply(skeleton);
@@ -237,7 +184,7 @@ namespace SpineViewer.Spine.Implementations.Spine
};
}
public override void Draw(SFML.Graphics.RenderTarget target, SFML.Graphics.RenderStates states)
protected override void draw(SFML.Graphics.RenderTarget target, SFML.Graphics.RenderStates states)
{
vertexArray.Clear();
states.Texture = null;
@@ -308,13 +255,13 @@ namespace SpineViewer.Spine.Implementations.Spine
if (vertexArray.VertexCount > 0)
{
// XXX: 实测不用设置 sampler2D 的值也正确
if (UsePremultipliedAlpha && (states.BlendMode == BlendModeSFML.Normal || states.BlendMode == BlendModeSFML.Additive))
states.Shader = FragmentShader;
if (usePremultipliedAlpha && (states.BlendMode == BlendModeSFML.Normal || states.BlendMode == BlendModeSFML.Additive))
states.Shader = Shader.FragmentShader;
else
states.Shader = null;
// 调试纹理
if (!IsDebug || DebugTexture)
if (!isDebug || debugTexture)
target.Draw(vertexArray, states);
vertexArray.Clear();
@@ -358,23 +305,23 @@ namespace SpineViewer.Spine.Implementations.Spine
}
clipping.ClipEnd();
if (UsePremultipliedAlpha && (states.BlendMode == BlendModeSFML.Normal || states.BlendMode == BlendModeSFML.Additive))
states.Shader = FragmentShader;
if (usePremultipliedAlpha && (states.BlendMode == BlendModeSFML.Normal || states.BlendMode == BlendModeSFML.Additive))
states.Shader = Shader.FragmentShader;
else
states.Shader = null;
// 调试纹理
if (!IsDebug || DebugTexture)
if (!isDebug || debugTexture)
target.Draw(vertexArray, states);
// 包围盒
if (IsDebug && IsSelected && DebugBounds)
if (isDebug && isSelected && debugBounds)
{
var bounds = Bounds;
boundsVertices[0] = boundsVertices[4] = new(new(bounds.Left, bounds.Top), BoundsColor);
boundsVertices[1] = new(new(bounds.Right, bounds.Top), BoundsColor);
boundsVertices[2] = new(new(bounds.Right, bounds.Bottom), BoundsColor);
boundsVertices[3] = new(new(bounds.Left, bounds.Bottom), BoundsColor);
var b = bounds;
boundsVertices[0] = boundsVertices[4] = new(new(b.Left, b.Top), BoundsColor);
boundsVertices[1] = new(new(b.Right, b.Top), BoundsColor);
boundsVertices[2] = new(new(b.Right, b.Bottom), BoundsColor);
boundsVertices[3] = new(new(b.Left, b.Bottom), BoundsColor);
target.Draw(boundsVertices);
}
}

View File

@@ -10,7 +10,7 @@ using SpineRuntime38.Attachments;
namespace SpineViewer.Spine.Implementations.Spine
{
[SpineImplementation(Version.V38)]
[SpineImplementation(SpineVersion.V38)]
internal class Spine38 : SpineViewer.Spine.Spine
{
private static readonly Animation EmptyAnimation = new(EMPTY_ANIMATION, [], 0);
@@ -50,7 +50,7 @@ namespace SpineViewer.Spine.Implementations.Spine
private SkeletonClipping clipping = new();
public Spine38(string skelPath, string? atlasPath = null) : base(skelPath, atlasPath)
public Spine38(string skelPath, string atlasPath) : base(skelPath, atlasPath)
{
atlas = new Atlas(AtlasPath, textureLoader);
try
@@ -79,16 +79,12 @@ namespace SpineViewer.Spine.Implementations.Spine
foreach (var skin in skeletonData.Skins)
skinNames.Add(skin.Name);
animationStateData = new AnimationStateData(skeletonData);
skeleton = new Skeleton(skeletonData);
animationState = new AnimationState(animationStateData);
foreach (var anime in skeletonData.Animations)
animationNames.Add(anime.Name);
// 取最后一个作为初始, 尽可能去显示非默认的内容
CurrentAnimation = animationNames.Last();
CurrentSkin = skinNames.Last();
skeleton = new Skeleton(skeletonData);
animationStateData = new AnimationStateData(skeletonData);
animationState = new AnimationState(animationStateData);
}
protected override void Dispose(bool disposing)
@@ -99,7 +95,7 @@ namespace SpineViewer.Spine.Implementations.Spine
public override string FileVersion { get => skeletonData.Version; }
public override float Scale
protected override float scale
{
get => Math.Abs(skeleton.ScaleX);
set
@@ -109,7 +105,7 @@ namespace SpineViewer.Spine.Implementations.Spine
}
}
public override PointF Position
protected override PointF position
{
get => new(skeleton.X, skeleton.Y);
set
@@ -119,7 +115,7 @@ namespace SpineViewer.Spine.Implementations.Spine
}
}
public override bool FlipX
protected override bool flipX
{
get => skeleton.ScaleX < 0;
set
@@ -129,7 +125,7 @@ namespace SpineViewer.Spine.Implementations.Spine
}
}
public override bool FlipY
protected override bool flipY
{
get => skeleton.ScaleY < 0;
set
@@ -139,7 +135,18 @@ namespace SpineViewer.Spine.Implementations.Spine
}
}
public override string CurrentAnimation
protected override string skin
{
get => skeleton.Skin?.Name ?? "default";
set
{
if (!skinNames.Contains(value)) return;
skeleton.SetSkin(value);
skeleton.SetSlotsToSetupPose();
}
}
protected override string track0Animation
{
get => animationState.GetCurrent(0)?.Animation.Name ?? EMPTY_ANIMATION;
set
@@ -148,23 +155,10 @@ namespace SpineViewer.Spine.Implementations.Spine
animationState.SetAnimation(0, EmptyAnimation, false);
else if (animationNames.Contains(value))
animationState.SetAnimation(0, value, true);
Update(0);
}
}
public override string CurrentSkin
{
get => skeleton.Skin?.Name ?? "default";
set
{
if (!skinNames.Contains(value)) return;
skeleton.SetSkin(value);
skeleton.SetToSetupPose();
Update(0);
}
}
public override RectangleF Bounds
protected override RectangleF bounds
{
get
{
@@ -176,7 +170,7 @@ namespace SpineViewer.Spine.Implementations.Spine
public override float GetAnimationDuration(string name) { return skeletonData.FindAnimation(name)?.Duration ?? 0f; }
public override void Update(float delta)
protected override void update(float delta)
{
animationState.Update(delta);
animationState.Apply(skeleton);
@@ -196,7 +190,7 @@ namespace SpineViewer.Spine.Implementations.Spine
};
}
public override void Draw(SFML.Graphics.RenderTarget target, SFML.Graphics.RenderStates states)
protected override void draw(SFML.Graphics.RenderTarget target, SFML.Graphics.RenderStates states)
{
vertexArray.Clear();
states.Texture = null;
@@ -267,13 +261,13 @@ namespace SpineViewer.Spine.Implementations.Spine
if (vertexArray.VertexCount > 0)
{
// XXX: 实测不用设置 sampler2D 的值也正确
if (UsePremultipliedAlpha && (states.BlendMode == BlendModeSFML.Normal || states.BlendMode == BlendModeSFML.Additive))
states.Shader = FragmentShader;
if (usePremultipliedAlpha && (states.BlendMode == BlendModeSFML.Normal || states.BlendMode == BlendModeSFML.Additive))
states.Shader = Shader.FragmentShader;
else
states.Shader = null;
// 调试纹理
if (!IsDebug || DebugTexture)
if (!isDebug || debugTexture)
target.Draw(vertexArray, states);
vertexArray.Clear();
@@ -317,23 +311,23 @@ namespace SpineViewer.Spine.Implementations.Spine
}
clipping.ClipEnd();
if (UsePremultipliedAlpha && (states.BlendMode == BlendModeSFML.Normal || states.BlendMode == BlendModeSFML.Additive))
states.Shader = FragmentShader;
if (usePremultipliedAlpha && (states.BlendMode == BlendModeSFML.Normal || states.BlendMode == BlendModeSFML.Additive))
states.Shader = Shader.FragmentShader;
else
states.Shader = null;
// 调试纹理
if (!IsDebug || DebugTexture)
if (!isDebug || debugTexture)
target.Draw(vertexArray, states);
// 调试包围盒
if (IsDebug && IsSelected && DebugBounds)
if (isDebug && isSelected && debugBounds)
{
var bounds = Bounds;
boundsVertices[0] = boundsVertices[4] = new(new(bounds.Left, bounds.Top), BoundsColor);
boundsVertices[1] = new(new(bounds.Right, bounds.Top), BoundsColor);
boundsVertices[2] = new(new(bounds.Right, bounds.Bottom), BoundsColor);
boundsVertices[3] = new(new(bounds.Left, bounds.Bottom), BoundsColor);
var b = bounds;
boundsVertices[0] = boundsVertices[4] = new(new(b.Left, b.Top), BoundsColor);
boundsVertices[1] = new(new(b.Right, b.Top), BoundsColor);
boundsVertices[2] = new(new(b.Right, b.Bottom), BoundsColor);
boundsVertices[3] = new(new(b.Left, b.Bottom), BoundsColor);
target.Draw(boundsVertices);
}
}

View File

@@ -9,7 +9,7 @@ using SpineRuntime40;
namespace SpineViewer.Spine.Implementations.Spine
{
[SpineImplementation(Version.V40)]
[SpineImplementation(SpineVersion.V40)]
internal class Spine40 : SpineViewer.Spine.Spine
{
private static readonly Animation EmptyAnimation = new(EMPTY_ANIMATION, [], 0);
@@ -46,7 +46,7 @@ namespace SpineViewer.Spine.Implementations.Spine
private SkeletonClipping clipping = new();
public Spine40(string skelPath, string? atlasPath = null) : base(skelPath, atlasPath)
public Spine40(string skelPath, string atlasPath) : base(skelPath, atlasPath)
{
atlas = new Atlas(AtlasPath, textureLoader);
try
@@ -75,16 +75,12 @@ namespace SpineViewer.Spine.Implementations.Spine
foreach (var skin in skeletonData.Skins)
skinNames.Add(skin.Name);
animationStateData = new AnimationStateData(skeletonData);
skeleton = new Skeleton(skeletonData);
animationState = new AnimationState(animationStateData);
foreach (var anime in skeletonData.Animations)
animationNames.Add(anime.Name);
// 取最后一个作为初始, 尽可能去显示非默认的内容
CurrentAnimation = animationNames.Last();
CurrentSkin = skinNames.Last();
skeleton = new Skeleton(skeletonData);
animationStateData = new AnimationStateData(skeletonData);
animationState = new AnimationState(animationStateData);
}
protected override void Dispose(bool disposing)
@@ -95,7 +91,7 @@ namespace SpineViewer.Spine.Implementations.Spine
public override string FileVersion { get => skeletonData.Version; }
public override float Scale
protected override float scale
{
get => Math.Abs(skeleton.ScaleX);
set
@@ -105,7 +101,7 @@ namespace SpineViewer.Spine.Implementations.Spine
}
}
public override PointF Position
protected override PointF position
{
get => new(skeleton.X, skeleton.Y);
set
@@ -115,7 +111,7 @@ namespace SpineViewer.Spine.Implementations.Spine
}
}
public override bool FlipX
protected override bool flipX
{
get => skeleton.ScaleX < 0;
set
@@ -125,7 +121,7 @@ namespace SpineViewer.Spine.Implementations.Spine
}
}
public override bool FlipY
protected override bool flipY
{
get => skeleton.ScaleY < 0;
set
@@ -135,7 +131,18 @@ namespace SpineViewer.Spine.Implementations.Spine
}
}
public override string CurrentAnimation
protected override string skin
{
get => skeleton.Skin?.Name ?? "default";
set
{
if (!skinNames.Contains(value)) return;
skeleton.SetSkin(value);
skeleton.SetSlotsToSetupPose();
}
}
protected override string track0Animation
{
get => animationState.GetCurrent(0)?.Animation.Name ?? EMPTY_ANIMATION;
set
@@ -144,23 +151,10 @@ namespace SpineViewer.Spine.Implementations.Spine
animationState.SetAnimation(0, EmptyAnimation, false);
else if (animationNames.Contains(value))
animationState.SetAnimation(0, value, true);
Update(0);
}
}
public override string CurrentSkin
{
get => skeleton.Skin?.Name ?? "default";
set
{
if (!skinNames.Contains(value)) return;
skeleton.SetSkin(value);
skeleton.SetToSetupPose();
Update(0);
}
}
public override RectangleF Bounds
protected override RectangleF bounds
{
get
{
@@ -172,7 +166,7 @@ namespace SpineViewer.Spine.Implementations.Spine
public override float GetAnimationDuration(string name) { return skeletonData.FindAnimation(name)?.Duration ?? 0f; }
public override void Update(float delta)
protected override void update(float delta)
{
animationState.Update(delta);
animationState.Apply(skeleton);
@@ -192,7 +186,7 @@ namespace SpineViewer.Spine.Implementations.Spine
};
}
public override void Draw(SFML.Graphics.RenderTarget target, SFML.Graphics.RenderStates states)
protected override void draw(SFML.Graphics.RenderTarget target, SFML.Graphics.RenderStates states)
{
vertexArray.Clear();
states.Texture = null;
@@ -263,13 +257,13 @@ namespace SpineViewer.Spine.Implementations.Spine
if (vertexArray.VertexCount > 0)
{
// XXX: 实测不用设置 sampler2D 的值也正确
if (UsePremultipliedAlpha && (states.BlendMode == BlendModeSFML.Normal || states.BlendMode == BlendModeSFML.Additive))
states.Shader = FragmentShader;
if (usePremultipliedAlpha && (states.BlendMode == BlendModeSFML.Normal || states.BlendMode == BlendModeSFML.Additive))
states.Shader = Shader.FragmentShader;
else
states.Shader = null;
// 调试纹理
if (!IsDebug || DebugTexture)
if (!isDebug || debugTexture)
target.Draw(vertexArray, states);
vertexArray.Clear();
@@ -313,23 +307,23 @@ namespace SpineViewer.Spine.Implementations.Spine
}
clipping.ClipEnd();
if (UsePremultipliedAlpha && (states.BlendMode == BlendModeSFML.Normal || states.BlendMode == BlendModeSFML.Additive))
states.Shader = FragmentShader;
if (usePremultipliedAlpha && (states.BlendMode == BlendModeSFML.Normal || states.BlendMode == BlendModeSFML.Additive))
states.Shader = Shader.FragmentShader;
else
states.Shader = null;
// 调试纹理
if (!IsDebug || DebugTexture)
if (!isDebug || debugTexture)
target.Draw(vertexArray, states);
// 包围盒
if (IsDebug && IsSelected && DebugBounds)
if (isDebug && isSelected && debugBounds)
{
var bounds = Bounds;
boundsVertices[0] = boundsVertices[4] = new(new(bounds.Left, bounds.Top), BoundsColor);
boundsVertices[1] = new(new(bounds.Right, bounds.Top), BoundsColor);
boundsVertices[2] = new(new(bounds.Right, bounds.Bottom), BoundsColor);
boundsVertices[3] = new(new(bounds.Left, bounds.Bottom), BoundsColor);
var b = bounds;
boundsVertices[0] = boundsVertices[4] = new(new(b.Left, b.Top), BoundsColor);
boundsVertices[1] = new(new(b.Right, b.Top), BoundsColor);
boundsVertices[2] = new(new(b.Right, b.Bottom), BoundsColor);
boundsVertices[3] = new(new(b.Left, b.Bottom), BoundsColor);
target.Draw(boundsVertices);
}
}

View File

@@ -9,7 +9,7 @@ using SpineRuntime41;
namespace SpineViewer.Spine.Implementations.Spine
{
[SpineImplementation(Version.V41)]
[SpineImplementation(SpineVersion.V41)]
internal class Spine41 : SpineViewer.Spine.Spine
{
private static readonly Animation EmptyAnimation = new(EMPTY_ANIMATION, [], 0);
@@ -46,7 +46,7 @@ namespace SpineViewer.Spine.Implementations.Spine
private SkeletonClipping clipping = new();
public Spine41(string skelPath, string? atlasPath = null) : base(skelPath, atlasPath)
public Spine41(string skelPath, string atlasPath) : base(skelPath, atlasPath)
{
atlas = new Atlas(AtlasPath, textureLoader);
try
@@ -75,16 +75,12 @@ namespace SpineViewer.Spine.Implementations.Spine
foreach (var skin in skeletonData.Skins)
skinNames.Add(skin.Name);
animationStateData = new AnimationStateData(skeletonData);
skeleton = new Skeleton(skeletonData);
animationState = new AnimationState(animationStateData);
foreach (var anime in skeletonData.Animations)
animationNames.Add(anime.Name);
// 取最后一个作为初始, 尽可能去显示非默认的内容
CurrentAnimation = animationNames.Last();
CurrentSkin = skinNames.Last();
skeleton = new Skeleton(skeletonData);
animationStateData = new AnimationStateData(skeletonData);
animationState = new AnimationState(animationStateData);
}
protected override void Dispose(bool disposing)
@@ -95,7 +91,7 @@ namespace SpineViewer.Spine.Implementations.Spine
public override string FileVersion { get => skeletonData.Version; }
public override float Scale
protected override float scale
{
get => Math.Abs(skeleton.ScaleX);
set
@@ -105,7 +101,7 @@ namespace SpineViewer.Spine.Implementations.Spine
}
}
public override PointF Position
protected override PointF position
{
get => new(skeleton.X, skeleton.Y);
set
@@ -115,7 +111,7 @@ namespace SpineViewer.Spine.Implementations.Spine
}
}
public override bool FlipX
protected override bool flipX
{
get => skeleton.ScaleX < 0;
set
@@ -125,7 +121,7 @@ namespace SpineViewer.Spine.Implementations.Spine
}
}
public override bool FlipY
protected override bool flipY
{
get => skeleton.ScaleY < 0;
set
@@ -135,7 +131,18 @@ namespace SpineViewer.Spine.Implementations.Spine
}
}
public override string CurrentAnimation
protected override string skin
{
get => skeleton.Skin?.Name ?? "default";
set
{
if (!skinNames.Contains(value)) return;
skeleton.SetSkin(value);
skeleton.SetSlotsToSetupPose();
}
}
protected override string track0Animation
{
get => animationState.GetCurrent(0)?.Animation.Name ?? EMPTY_ANIMATION;
set
@@ -144,23 +151,10 @@ namespace SpineViewer.Spine.Implementations.Spine
animationState.SetAnimation(0, EmptyAnimation, false);
else if (animationNames.Contains(value))
animationState.SetAnimation(0, value, true);
Update(0);
}
}
public override string CurrentSkin
{
get => skeleton.Skin?.Name ?? "default";
set
{
if (!skinNames.Contains(value)) return;
skeleton.SetSkin(value);
skeleton.SetToSetupPose();
Update(0);
}
}
public override RectangleF Bounds
protected override RectangleF bounds
{
get
{
@@ -172,7 +166,7 @@ namespace SpineViewer.Spine.Implementations.Spine
public override float GetAnimationDuration(string name) { return skeletonData.FindAnimation(name)?.Duration ?? 0f; }
public override void Update(float delta)
protected override void update(float delta)
{
animationState.Update(delta);
animationState.Apply(skeleton);
@@ -192,7 +186,7 @@ namespace SpineViewer.Spine.Implementations.Spine
};
}
public override void Draw(SFML.Graphics.RenderTarget target, SFML.Graphics.RenderStates states)
protected override void draw(SFML.Graphics.RenderTarget target, SFML.Graphics.RenderStates states)
{
vertexArray.Clear();
states.Texture = null;
@@ -263,13 +257,13 @@ namespace SpineViewer.Spine.Implementations.Spine
if (vertexArray.VertexCount > 0)
{
// XXX: 实测不用设置 sampler2D 的值也正确
if (UsePremultipliedAlpha && (states.BlendMode == BlendModeSFML.Normal || states.BlendMode == BlendModeSFML.Additive))
states.Shader = FragmentShader;
if (usePremultipliedAlpha && (states.BlendMode == BlendModeSFML.Normal || states.BlendMode == BlendModeSFML.Additive))
states.Shader = Shader.FragmentShader;
else
states.Shader = null;
// 调试纹理
if (!IsDebug || DebugTexture)
if (!isDebug || debugTexture)
target.Draw(vertexArray, states);
vertexArray.Clear();
@@ -313,23 +307,23 @@ namespace SpineViewer.Spine.Implementations.Spine
}
clipping.ClipEnd();
if (UsePremultipliedAlpha && (states.BlendMode == BlendModeSFML.Normal || states.BlendMode == BlendModeSFML.Additive))
states.Shader = FragmentShader;
if (usePremultipliedAlpha && (states.BlendMode == BlendModeSFML.Normal || states.BlendMode == BlendModeSFML.Additive))
states.Shader = Shader.FragmentShader;
else
states.Shader = null;
// 调试纹理
if (!IsDebug || DebugTexture)
if (!isDebug || debugTexture)
target.Draw(vertexArray, states);
// 包围盒
if (IsDebug && IsSelected && DebugBounds)
if (isDebug && isSelected && debugBounds)
{
var bounds = Bounds;
boundsVertices[0] = boundsVertices[4] = new(new(bounds.Left, bounds.Top), BoundsColor);
boundsVertices[1] = new(new(bounds.Right, bounds.Top), BoundsColor);
boundsVertices[2] = new(new(bounds.Right, bounds.Bottom), BoundsColor);
boundsVertices[3] = new(new(bounds.Left, bounds.Bottom), BoundsColor);
var b = bounds;
boundsVertices[0] = boundsVertices[4] = new(new(b.Left, b.Top), BoundsColor);
boundsVertices[1] = new(new(b.Right, b.Top), BoundsColor);
boundsVertices[2] = new(new(b.Right, b.Bottom), BoundsColor);
boundsVertices[3] = new(new(b.Left, b.Bottom), BoundsColor);
target.Draw(boundsVertices);
}
}

View File

@@ -9,7 +9,7 @@ using SpineRuntime42;
namespace SpineViewer.Spine.Implementations.Spine
{
[SpineImplementation(Version.V42)]
[SpineImplementation(SpineVersion.V42)]
internal class Spine42 : SpineViewer.Spine.Spine
{
private static readonly Animation EmptyAnimation = new(EMPTY_ANIMATION, [], 0);
@@ -46,7 +46,7 @@ namespace SpineViewer.Spine.Implementations.Spine
private SkeletonClipping clipping = new();
public Spine42(string skelPath, string? atlasPath = null) : base(skelPath, atlasPath)
public Spine42(string skelPath, string atlasPath) : base(skelPath, atlasPath)
{
atlas = new Atlas(AtlasPath, textureLoader);
try
@@ -75,16 +75,12 @@ namespace SpineViewer.Spine.Implementations.Spine
foreach (var skin in skeletonData.Skins)
skinNames.Add(skin.Name);
animationStateData = new AnimationStateData(skeletonData);
skeleton = new Skeleton(skeletonData);
animationState = new AnimationState(animationStateData);
foreach (var anime in skeletonData.Animations)
animationNames.Add(anime.Name);
// 取最后一个作为初始, 尽可能去显示非默认的内容
CurrentAnimation = animationNames.Last();
CurrentSkin = skinNames.Last();
skeleton = new Skeleton(skeletonData);
animationStateData = new AnimationStateData(skeletonData);
animationState = new AnimationState(animationStateData);
}
protected override void Dispose(bool disposing)
@@ -95,7 +91,7 @@ namespace SpineViewer.Spine.Implementations.Spine
public override string FileVersion { get => skeletonData.Version; }
public override float Scale
protected override float scale
{
get => Math.Abs(skeleton.ScaleX);
set
@@ -105,7 +101,7 @@ namespace SpineViewer.Spine.Implementations.Spine
}
}
public override PointF Position
protected override PointF position
{
get => new(skeleton.X, skeleton.Y);
set
@@ -115,7 +111,7 @@ namespace SpineViewer.Spine.Implementations.Spine
}
}
public override bool FlipX
protected override bool flipX
{
get => skeleton.ScaleX < 0;
set
@@ -125,7 +121,7 @@ namespace SpineViewer.Spine.Implementations.Spine
}
}
public override bool FlipY
protected override bool flipY
{
get => skeleton.ScaleY < 0;
set
@@ -135,7 +131,18 @@ namespace SpineViewer.Spine.Implementations.Spine
}
}
public override string CurrentAnimation
protected override string skin
{
get => skeleton.Skin?.Name ?? "default";
set
{
if (!skinNames.Contains(value)) return;
skeleton.SetSkin(value);
skeleton.SetSlotsToSetupPose();
}
}
protected override string track0Animation
{
get => animationState.GetCurrent(0)?.Animation.Name ?? EMPTY_ANIMATION;
set
@@ -144,23 +151,10 @@ namespace SpineViewer.Spine.Implementations.Spine
animationState.SetAnimation(0, EmptyAnimation, false);
else if (animationNames.Contains(value))
animationState.SetAnimation(0, value, true);
Update(0);
}
}
public override string CurrentSkin
{
get => skeleton.Skin?.Name ?? "default";
set
{
if (!skinNames.Contains(value)) return;
skeleton.SetSkin(value);
skeleton.SetToSetupPose();
Update(0);
}
}
public override RectangleF Bounds
protected override RectangleF bounds
{
get
{
@@ -172,7 +166,7 @@ namespace SpineViewer.Spine.Implementations.Spine
public override float GetAnimationDuration(string name) { return skeletonData.FindAnimation(name)?.Duration ?? 0f; }
public override void Update(float delta)
protected override void update(float delta)
{
animationState.Update(delta);
animationState.Apply(skeleton);
@@ -192,7 +186,7 @@ namespace SpineViewer.Spine.Implementations.Spine
};
}
public override void Draw(SFML.Graphics.RenderTarget target, SFML.Graphics.RenderStates states)
protected override void draw(SFML.Graphics.RenderTarget target, SFML.Graphics.RenderStates states)
{
vertexArray.Clear();
states.Texture = null;
@@ -263,13 +257,13 @@ namespace SpineViewer.Spine.Implementations.Spine
if (vertexArray.VertexCount > 0)
{
// XXX: 实测不用设置 sampler2D 的值也正确
if (UsePremultipliedAlpha && (states.BlendMode == BlendModeSFML.Normal || states.BlendMode == BlendModeSFML.Additive))
states.Shader = FragmentShader;
if (usePremultipliedAlpha && (states.BlendMode == BlendModeSFML.Normal || states.BlendMode == BlendModeSFML.Additive))
states.Shader = Shader.FragmentShader;
else
states.Shader = null;
// 调试纹理
if (!IsDebug || DebugTexture)
if (!isDebug || debugTexture)
target.Draw(vertexArray, states);
vertexArray.Clear();
@@ -313,23 +307,23 @@ namespace SpineViewer.Spine.Implementations.Spine
}
clipping.ClipEnd();
if (UsePremultipliedAlpha && (states.BlendMode == BlendModeSFML.Normal || states.BlendMode == BlendModeSFML.Additive))
states.Shader = FragmentShader;
if (usePremultipliedAlpha && (states.BlendMode == BlendModeSFML.Normal || states.BlendMode == BlendModeSFML.Additive))
states.Shader = Shader.FragmentShader;
else
states.Shader = null;
// 调试纹理
if (!IsDebug || DebugTexture)
if (!isDebug || debugTexture)
target.Draw(vertexArray, states);
// 包围盒
if (IsDebug && IsSelected && DebugBounds)
if (isDebug && isSelected && debugBounds)
{
var bounds = Bounds;
boundsVertices[0] = boundsVertices[4] = new(new(bounds.Left, bounds.Top), BoundsColor);
boundsVertices[1] = new(new(bounds.Right, bounds.Top), BoundsColor);
boundsVertices[2] = new(new(bounds.Right, bounds.Bottom), BoundsColor);
boundsVertices[3] = new(new(bounds.Left, bounds.Bottom), BoundsColor);
var b = bounds;
boundsVertices[0] = boundsVertices[4] = new(new(b.Left, b.Top), BoundsColor);
boundsVertices[1] = new(new(b.Right, b.Top), BoundsColor);
boundsVertices[2] = new(new(b.Right, b.Bottom), BoundsColor);
boundsVertices[3] = new(new(b.Left, b.Bottom), BoundsColor);
target.Draw(boundsVertices);
}
}

View File

@@ -0,0 +1,35 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace SpineViewer.Spine
{
public static class Shader
{
/// <summary>
/// 用于解决 PMA 和渐变动画问题的片段着色器
/// </summary>
private const string FRAGMENT_SHADER = (
"uniform sampler2D t;" +
"void main() { vec4 p = texture2D(t, gl_TexCoord[0].xy);" +
"if (p.a > 0) p.rgb /= max(max(max(p.r, p.g), p.b), p.a);" +
"gl_FragColor = gl_Color * p; }"
);
/// <summary>
/// 针对预乘 Alpha 通道的片段着色器
/// </summary>
public static SFML.Graphics.Shader? FragmentShader { get; private set; }
/// <summary>
/// 加载 Shader, 可能会存在异常导致着色器加载失败
/// </summary>
/// <exception cref="SFML.LoadingFailedException"></exception>
public static void Init()
{
FragmentShader = SFML.Graphics.Shader.FromString(null, null, FRAGMENT_SHADER);
}
}
}

View File

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

View File

@@ -19,207 +19,109 @@ using SpineViewer.Exporter;
namespace SpineViewer.Spine
{
/// <summary>
/// Spine 基类, 使用静态方法 New 来创建具体版本对象
/// Spine 基类, 使用静态方法 New 来创建具体版本对象, 该类是线程安全的
/// </summary>
public abstract class Spine : SFML.Graphics.Drawable, IDisposable
public abstract class Spine : ImplementationResolver<Spine, SpineImplementationAttribute, SpineVersion>, SFML.Graphics.Drawable, IDisposable
{
/// <summary>
/// 常规骨骼文件后缀集合
/// </summary>
public static readonly ImmutableHashSet<string> CommonSkelSuffix = [".skel", ".json"];
/// <summary>
/// 空动画标记
/// </summary>
public const string EMPTY_ANIMATION = "<Empty>";
protected const string EMPTY_ANIMATION = "<Empty>";
/// <summary>
/// 预览图宽
/// </summary>
public const uint PREVIEW_WIDTH = 256;
protected const uint PREVIEW_WIDTH = 256;
/// <summary>
/// 预览图高
/// </summary>
public const uint PREVIEW_HEIGHT = 256;
protected const uint PREVIEW_HEIGHT = 256;
/// <summary>
/// 缩放最小值
/// </summary>
public const float SCALE_MIN = 0.001f;
/// <summary>
/// 实现类缓存
/// </summary>
private static readonly Dictionary<Version, Type> ImplementationTypes = [];
public static readonly Dictionary<Version, Type>.KeyCollection ImplementedVersions;
/// <summary>
/// 用于解决 PMA 和渐变动画问题的片段着色器
/// </summary>
private const string FRAGMENT_SHADER = (
"uniform sampler2D t;" +
"void main() { vec4 p = texture2D(t, gl_TexCoord[0].xy);" +
"if (p.a > 0) p.rgb /= max(max(max(p.r, p.g), p.b), p.a);" +
"gl_FragColor = gl_Color * p; }"
);
/// <summary>
/// 用于解决 PMA 和渐变动画问题的片段着色器
/// </summary>
protected static readonly SFML.Graphics.Shader? FragmentShader = null;
/// <summary>
/// 静态构造函数
/// </summary>
static Spine()
{
// 遍历并缓存标记了 SpineImplementationAttribute 的类型
var impTypes = Assembly.GetExecutingAssembly().GetTypes().Where(t => typeof(Spine).IsAssignableFrom(t) && !t.IsAbstract);
foreach (var type in impTypes)
{
var attr = type.GetCustomAttribute<SpineImplementationAttribute>();
if (attr is not null)
{
if (ImplementationTypes.ContainsKey(attr.Version))
throw new InvalidOperationException($"Multiple implementations found: {attr.Version}");
ImplementationTypes[attr.Version] = type;
}
}
Program.Logger.Debug("Find Spine implementations: [{}]", string.Join(", ", ImplementationTypes.Keys));
ImplementedVersions = ImplementationTypes.Keys;
// 加载 FragmentShader
try
{
FragmentShader = SFML.Graphics.Shader.FromString(null, null, FRAGMENT_SHADER);
}
catch (Exception ex)
{
FragmentShader = null;
Program.Logger.Error(ex.ToString());
Program.Logger.Error("Failed to load fragment shader");
MessageBox.Warn("Fragment shader 加载失败预乘Alpha通道属性失效");
}
}
/// <summary>
/// 尝试检测骨骼文件版本
/// </summary>
public static Version? GetVersion(string skelPath)
{
string versionString = null;
Version? version = null;
using var input = File.OpenRead(skelPath);
var reader = new SkeletonConverter.BinaryReader(input);
// try json format
try
{
if (JsonNode.Parse(input) is JsonObject root && root.TryGetPropertyValue("skeleton", out var node) &&
node is JsonObject _skeleton && _skeleton.TryGetPropertyValue("spine", out var _version))
versionString = (string)_version;
}
catch { }
// try v4 binary format
if (versionString is null)
{
try
{
input.Position = 0;
var hash = reader.ReadLong();
var versionPosition = input.Position;
var versionByteCount = reader.ReadVarInt();
input.Position = versionPosition;
if (versionByteCount <= 13)
versionString = reader.ReadString();
}
catch { }
}
// try v3 binary format
if (versionString is null)
{
try
{
input.Position = 0;
var hash = reader.ReadString();
versionString = reader.ReadString();
}
catch { }
}
if (versionString is not null)
{
if (versionString.StartsWith("2.1.")) version = Version.V21;
else if (versionString.StartsWith("3.6.")) version = Version.V36;
else if (versionString.StartsWith("3.7.")) version = Version.V37;
else if (versionString.StartsWith("3.8.")) version = Version.V38;
else if (versionString.StartsWith("4.0.")) version = Version.V40;
else if (versionString.StartsWith("4.1.")) version = Version.V41;
else if (versionString.StartsWith("4.2.")) version = Version.V42;
else if (versionString.StartsWith("4.3.")) version = Version.V43;
else Program.Logger.Error("Unknown verison: {}, {}", versionString, skelPath);
}
return version;
}
protected const float SCALE_MIN = 0.001f;
/// <summary>
/// 创建特定版本的 Spine
/// </summary>
public static Spine New(Version version, string skelPath, string? atlasPath = null)
public static Spine New(SpineVersion version, string skelPath, string? atlasPath = null)
{
if (version == Version.Auto)
{
if (GetVersion(skelPath) is Version detectedVersion)
version = detectedVersion;
else
throw new InvalidDataException($"Auto version detection failed for {skelPath}, try to use a specific version");
}
if (!ImplementationTypes.TryGetValue(version, out var spineType))
{
throw new NotImplementedException($"Not implemented version: {version}");
}
return (Spine)Activator.CreateInstance(spineType, skelPath, atlasPath);
if (version == SpineVersion.Auto) version = SpineHelper.GetVersion(skelPath);
atlasPath ??= Path.ChangeExtension(skelPath, ".atlas");
return New(version, [skelPath, atlasPath]).PostInit();
}
/// <summary>
/// 数据锁
/// </summary>
private readonly object _lock = new();
/// <summary>
/// 构造函数
/// </summary>
public Spine(string skelPath, string? atlasPath = null)
public Spine(string skelPath, string atlasPath)
{
// 获取子类类型
var type = GetType();
var attr = type.GetCustomAttribute<SpineImplementationAttribute>();
if (attr is null)
{
throw new InvalidOperationException($"Class {type.Name} has no SpineImplementationAttribute");
}
atlasPath ??= Path.ChangeExtension(skelPath, ".atlas");
// 设置 Version
Version = attr.Version;
Version = GetType().GetCustomAttribute<SpineImplementationAttribute>().ImplementationKey;
AssetsDir = Directory.GetParent(skelPath).FullName;
SkelPath = Path.GetFullPath(skelPath);
AtlasPath = Path.GetFullPath(atlasPath);
Name = Path.GetFileNameWithoutExtension(skelPath);
}
/// <summary>
/// 构造函数之后的初始化工作
/// </summary>
private Spine PostInit()
{
SkinNames = skinNames.AsReadOnly();
AnimationNames = animationNames.AsReadOnly();
// 必须 Update 一次否则包围盒还没有值
update(0);
// XXX: tex 没办法在这里主动 Dispose
// 批量添加在获取预览图的时候极大概率会和预览线程死锁
// 虽然两边不会同时调用 Draw, 但是死锁似乎和 Draw 函数有关
// 除此之外, 似乎还和 tex 的 Dispose 有关
// 如果不对 tex 进行 Dispose, 那么不管是否 Draw 都正常不会死锁
var tex = new SFML.Graphics.RenderTexture(PREVIEW_WIDTH, PREVIEW_HEIGHT);
tex.SetView(bounds.GetView(PREVIEW_WIDTH, PREVIEW_HEIGHT));
tex.Clear(SFML.Graphics.Color.Transparent);
tex.Draw(this);
tex.Display();
using (var img = tex.Texture.CopyToImage())
{
if (img.SaveToMemory(out var imgBuffer, "bmp"))
{
// 必须重复构造一个副本才能摆脱对流的依赖, 否则之后使用会报错
using var stream = new MemoryStream(imgBuffer);
using var bitmap = new Bitmap(stream);
Preview = new Bitmap(bitmap);
}
}
// 取最后一个作为初始, 尽可能去显示非默认的内容
skin = SkinNames.Last();
track0Animation = AnimationNames.Last();
return this;
}
~Spine() { Dispose(false); }
public void Dispose() { Dispose(true); GC.SuppressFinalize(this); }
protected virtual void Dispose(bool disposing) { preview?.Dispose(); }
protected virtual void Dispose(bool disposing) { Preview?.Dispose(); }
#region | [0]
/// <summary>
/// 获取所属版本
/// </summary>
[TypeConverter(typeof(VersionConverter))]
[TypeConverter(typeof(SpineVersionConverter))]
[Category("[0] "), DisplayName("")]
public Version Version { get; }
public SpineVersion Version { get; }
/// <summary>
/// 资源所在完整目录
@@ -259,13 +161,23 @@ namespace SpineViewer.Spine
/// 是否被隐藏, 被隐藏的模型将仅仅在列表显示, 不参与其他行为
/// </summary>
[Category("[1] "), DisplayName("")]
public bool IsHidden { get; set; } = false;
public bool IsHidden
{
get { lock (_lock) return isHidden; }
set { lock (_lock) isHidden = value; }
}
protected bool isHidden = false;
/// <summary>
/// 是否使用预乘Alpha
/// </summary>
[Category("[1] "), DisplayName("Alpha通道")]
public bool UsePremultipliedAlpha { get; set; } = true;
public bool UsePremultipliedAlpha
{
get { lock (_lock) return usePremultipliedAlpha; }
set { lock (_lock) usePremultipliedAlpha = value; }
}
protected bool usePremultipliedAlpha = true;
#endregion
@@ -275,64 +187,94 @@ namespace SpineViewer.Spine
/// 缩放比例
/// </summary>
[Category("[2] "), DisplayName("")]
public abstract float Scale { get; set; }
public float Scale
{
get { lock (_lock) return scale; }
set { lock (_lock) { scale = value; update(0); } }
}
protected abstract float scale { get; set; }
/// <summary>
/// 位置
/// </summary>
[TypeConverter(typeof(PointFConverter))]
[Category("[2] "), DisplayName("")]
public abstract PointF Position { get; set; }
public PointF Position
{
get { lock (_lock) return position; }
set { lock (_lock) { position = value; update(0); } }
}
protected abstract PointF position { get; set; }
/// <summary>
/// 水平翻转
/// </summary>
[Category("[2] "), DisplayName("")]
public abstract bool FlipX { get; set; }
public bool FlipX
{
get { lock (_lock) return flipX; }
set { lock (_lock) { flipX = value; update(0); } }
}
protected abstract bool flipX { get; set; }
/// <summary>
/// 垂直翻转
/// </summary>
[Category("[2] "), DisplayName("")]
public abstract bool FlipY { get; set; }
public bool FlipY
{
get { lock (_lock) return flipY; }
set { lock (_lock) { flipY = value; update(0); } }
}
protected abstract bool flipY { get; set; }
#endregion
#region | [3]
/// <summary>
/// 包含的所有动画名称
/// </summary>
[Browsable(false)]
public ReadOnlyCollection<string> AnimationNames { get => animationNames.AsReadOnly(); }
protected List<string> animationNames = [EMPTY_ANIMATION];
/// <summary>
/// 当前动画名称, 如果设置的动画不存在则忽略
/// </summary>
[TypeConverter(typeof(AnimationConverter))]
[Category("[3] "), DisplayName("")]
public abstract string CurrentAnimation { get; set; }
/// <summary>
/// 当前动画时长
/// </summary>
[Category("[3] "), DisplayName("")]
public float CurrentAnimationDuration { get => GetAnimationDuration(CurrentAnimation); }
/// <summary>
/// 包含的所有皮肤名称
/// </summary>
[Browsable(false)]
public ReadOnlyCollection<string> SkinNames { get => skinNames.AsReadOnly(); }
public ReadOnlyCollection<string> SkinNames { get; private set; }
protected List<string> skinNames = [];
/// <summary>
/// 当前皮肤名称, 如果设置的皮肤不存在则忽略
/// 使用的皮肤名称, 如果设置的皮肤不存在则忽略
/// </summary>
[TypeConverter(typeof(SkinConverter))]
[Category("[3] "), DisplayName("")]
public abstract string CurrentSkin { get; set; }
[Category("[3] "), DisplayName("")]
public string Skin
{
get { lock (_lock) return skin; }
set { lock (_lock) { skin = value; update(0); } }
}
protected abstract string skin { get; set; }
/// <summary>
/// 包含的所有动画名称
/// </summary>
[Browsable(false)]
public ReadOnlyCollection<string> AnimationNames { get; private set; }
protected List<string> animationNames = [EMPTY_ANIMATION];
/// <summary>
/// 默认轨道动画名称, 如果设置的动画不存在则忽略
/// </summary>
[TypeConverter(typeof(AnimationConverter))]
[Category("[3] "), DisplayName("")]
public string Track0Animation
{
get { lock (_lock) return track0Animation; }
set { lock (_lock) { track0Animation = value; update(0); } }
}
protected abstract string track0Animation { get; set; }
/// <summary>
/// 默认轨道动画时长
/// </summary>
[Category("[3] "), DisplayName("")]
public float Track0AnimationDuration { get => GetAnimationDuration(Track0Animation); } // TODO: 动画时长变成伪属性在面板显示
#endregion
@@ -342,35 +284,45 @@ namespace SpineViewer.Spine
/// 显示调试
/// </summary>
[Browsable(false)]
public bool IsDebug { get; set; } = false;
/// <summary>
/// 包围盒颜色
/// </summary>
protected static readonly SFML.Graphics.Color BoundsColor = new(120, 200, 0);
/// <summary>
/// 包围盒顶点数组
/// </summary>
protected readonly SFML.Graphics.VertexArray boundsVertices = new(SFML.Graphics.PrimitiveType.LineStrip, 5);
public bool IsDebug
{
get { lock (_lock) return isDebug; }
set { lock (_lock) isDebug = value; }
}
protected bool isDebug = false;
/// <summary>
/// 显示纹理
/// </summary>
[Category("[4] "), DisplayName("")]
public bool DebugTexture { get; set; } = true;
public bool DebugTexture
{
get { lock (_lock) return debugTexture; }
set { lock (_lock) debugTexture = value; }
}
protected bool debugTexture = true;
/// <summary>
/// 显示包围盒
/// </summary>
[Category("[4] "), DisplayName("")]
public bool DebugBounds { get; set; } = true;
public bool DebugBounds
{
get { lock (_lock) return debugBounds; }
set { lock (_lock) debugBounds = value; }
}
protected bool debugBounds = true;
/// <summary>
/// 显示骨骼
/// </summary>
[Category("[4] "), DisplayName("")]
public bool DebugBones { get; set; } = false;
[Category("[4] "), DisplayName("")]
public bool DebugBones
{
get { lock (_lock) return debugBones; }
set { lock (_lock) debugBones = value; }
}
protected bool debugBones = false;
#endregion
@@ -383,67 +335,25 @@ namespace SpineViewer.Spine
/// 是否被选中
/// </summary>
[Browsable(false)]
public bool IsSelected { get; set; } = false;
public bool IsSelected
{
get { lock (_lock) return isSelected; }
set { lock (_lock) isSelected = value; }
}
protected bool isSelected = false;
/// <summary>
/// 骨骼包围盒
/// </summary>
[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;
public RectangleF Bounds { get { lock (_lock) return bounds; } }
protected abstract RectangleF bounds { get; }
/// <summary>
/// 骨骼预览图
/// </summary>
[Browsable(false)]
public Image Preview
{
get
{
if (preview is null)
{
// XXX: tex 没办法在这里主动 Dispose
// 批量添加在获取预览图的时候极大概率会和预览线程死锁
// 虽然两边不会同时调用 Draw, 但是死锁似乎和 Draw 函数有关
// 除此之外, 似乎还和 tex 的 Dispose 有关
// 如果不对 tex 进行 Dispose, 那么不管是否 Draw 都正常不会死锁
var tex = new SFML.Graphics.RenderTexture(PREVIEW_WIDTH, PREVIEW_HEIGHT);
tex.SetView(InitBounds.GetView(PREVIEW_WIDTH, PREVIEW_HEIGHT));
tex.Clear(SFML.Graphics.Color.Transparent);
var tmp = CurrentAnimation;
CurrentAnimation = EMPTY_ANIMATION;
tex.Draw(this);
CurrentAnimation = tmp;
tex.Display();
using var img = tex.Texture.CopyToImage();
img.SaveToMemory(out var imgBuffer, "bmp");
using var stream = new MemoryStream(imgBuffer);
preview = new Bitmap(new Bitmap(stream)); // 必须重复构造一个副本才能摆脱对流的依赖, 否则之后使用会报错
}
return preview;
}
}
private Image preview = null;
public Image Preview { get; private set; }
/// <summary>
/// 获取动画时长, 如果动画不存在则返回 0
@@ -453,7 +363,8 @@ namespace SpineViewer.Spine
/// <summary>
/// 更新内部状态
/// </summary>
public abstract void Update(float delta);
public void Update(float delta) { lock (_lock) update(delta); }
protected abstract void update(float delta);
#region SFML.Graphics.Drawable
@@ -467,10 +378,21 @@ namespace SpineViewer.Spine
/// </summary>
protected readonly SFML.Graphics.VertexArray vertexArray = new(SFML.Graphics.PrimitiveType.Triangles);
/// <summary>
/// 包围盒颜色
/// </summary>
protected static readonly SFML.Graphics.Color BoundsColor = new(120, 200, 0);
/// <summary>
/// 包围盒顶点数组
/// </summary>
protected readonly SFML.Graphics.VertexArray boundsVertices = new(SFML.Graphics.PrimitiveType.LineStrip, 5);
/// <summary>
/// SFML.Graphics.Drawable 接口实现
/// </summary>
public abstract void Draw(SFML.Graphics.RenderTarget target, SFML.Graphics.RenderStates states);
public void Draw(SFML.Graphics.RenderTarget target, SFML.Graphics.RenderStates states) { lock (_lock) draw(target, states); }
protected abstract void draw(SFML.Graphics.RenderTarget target, SFML.Graphics.RenderStates states);
#endregion
}

View File

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

View File

@@ -9,35 +9,23 @@ using System.Threading.Tasks;
namespace SpineViewer.Spine
{
public class VersionConverter : EnumConverter
public class SpineVersionConverter : EnumConverter
{
public VersionConverter() : base(typeof(Version)) { }
public SpineVersionConverter() : base(typeof(SpineVersion)) { }
public override object? ConvertTo(ITypeDescriptorContext? context, CultureInfo? culture, object? value, Type? destinationType)
{
if (destinationType == typeof(string) && value is Version version)
{
// 调用自定义的 String() 方法
if (destinationType == typeof(string) && value is SpineVersion version)
return version.GetName();
}
return base.ConvertTo(context, culture, value, destinationType);
}
}
public class AnimationConverter : StringConverter
{
public override bool GetStandardValuesSupported(ITypeDescriptorContext? context)
{
// 支持标准值列表
return true;
}
public override bool GetStandardValuesSupported(ITypeDescriptorContext? context) => true;
public override bool GetStandardValuesExclusive(ITypeDescriptorContext? context)
{
// 排他模式,只有下拉列表中的值可选
return true;
}
public override bool GetStandardValuesExclusive(ITypeDescriptorContext? context) => true;
public override StandardValuesCollection? GetStandardValues(ITypeDescriptorContext? context)
{
@@ -61,17 +49,9 @@ namespace SpineViewer.Spine
public class SkinConverter : StringConverter
{
public override bool GetStandardValuesSupported(ITypeDescriptorContext? context)
{
// 支持标准值列表
return true;
}
public override bool GetStandardValuesSupported(ITypeDescriptorContext? context) => true;
public override bool GetStandardValuesExclusive(ITypeDescriptorContext? context)
{
// 排他模式,只有下拉列表中的值可选
return true;
}
public override bool GetStandardValuesExclusive(ITypeDescriptorContext? context) => true;
public override StandardValuesCollection? GetStandardValues(ITypeDescriptorContext? context)
{

View File

@@ -1,94 +0,0 @@
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Linq;
using System.Reflection;
using System.Text;
using System.Threading.Tasks;
namespace SpineViewer.Spine
{
/// <summary>
/// 支持的 Spine 版本
/// </summary>
public 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>
public static class VersionHelper
{
/// <summary>
/// 版本名称
/// </summary>
public static readonly ReadOnlyDictionary<Version, string> Names;
private static readonly Dictionary<Version, string> names = [];
/// <summary>
/// Runtime 版本字符串
/// </summary>
private static readonly Dictionary<Version, string> runtimes = [];
static VersionHelper()
{
// 初始化缓存
foreach (var value in Enum.GetValues(typeof(Version)))
{
var field = typeof(Version).GetField(value.ToString());
var attribute = field?.GetCustomAttribute<DescriptionAttribute>();
names[(Version)value] = attribute?.Description ?? value.ToString();
}
Names = names.AsReadOnly();
runtimes[Version.V21] = Assembly.GetAssembly(typeof(SpineRuntime21.Skeleton)).GetCustomAttribute<AssemblyInformationalVersionAttribute>()?.InformationalVersion;
runtimes[Version.V36] = Assembly.GetAssembly(typeof(SpineRuntime36.Skeleton)).GetCustomAttribute<AssemblyInformationalVersionAttribute>()?.InformationalVersion;
runtimes[Version.V37] = Assembly.GetAssembly(typeof(SpineRuntime37.Skeleton)).GetCustomAttribute<AssemblyInformationalVersionAttribute>()?.InformationalVersion;
runtimes[Version.V38] = Assembly.GetAssembly(typeof(SpineRuntime38.Skeleton)).GetCustomAttribute<AssemblyInformationalVersionAttribute>()?.InformationalVersion;
runtimes[Version.V40] = Assembly.GetAssembly(typeof(SpineRuntime40.Skeleton)).GetCustomAttribute<AssemblyInformationalVersionAttribute>()?.InformationalVersion;
runtimes[Version.V41] = Assembly.GetAssembly(typeof(SpineRuntime41.Skeleton)).GetCustomAttribute<AssemblyInformationalVersionAttribute>()?.InformationalVersion;
runtimes[Version.V42] = Assembly.GetAssembly(typeof(SpineRuntime42.Skeleton)).GetCustomAttribute<AssemblyInformationalVersionAttribute>()?.InformationalVersion;
}
/// <summary>
/// 版本字符串名称
/// </summary>
public static string GetName(this Version version)
{
return Names.TryGetValue(version, out var val) ? val : version.ToString();
}
/// <summary>
/// Runtime 版本字符串名称
/// </summary>
public static string GetRuntime(this Version version)
{
return runtimes.TryGetValue(version, out var val) ? val : GetName(version);
}
}
}

View File

@@ -5,10 +5,9 @@
<Nullable>enable</Nullable>
<Platforms>x64</Platforms>
<TargetFramework>net8.0-windows</TargetFramework>
<RuntimeIdentifier>win-x64</RuntimeIdentifier>
<BaseOutputPath>$(SolutionDir)out</BaseOutputPath>
<IncludeSourceRevisionInInformationalVersion>false</IncludeSourceRevisionInInformationalVersion>
<Version>0.11.3</Version>
<Version>0.11.5</Version>
<OutputType>WinExe</OutputType>
<UseWindowsForms>true</UseWindowsForms>
<ApplicationIcon>appicon.ico</ApplicationIcon>

View File

@@ -0,0 +1,65 @@
using System;
using System.Runtime.InteropServices;
using System.Windows.Forms;
namespace SpineViewer
{
internal enum TBPFLAG
{
TBPF_NOPROGRESS = 0,
TBPF_INDETERMINATE = 0x1,
TBPF_NORMAL = 0x2,
TBPF_ERROR = 0x4,
TBPF_PAUSED = 0x8
}
[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
[ComImport, Guid("ea1afb91-9e28-4b86-90e9-9e9f8a5eefaf")]
internal interface ITaskbarList3
{
// ITaskbarList
void HrInit();
void AddTab(IntPtr hwnd);
void DeleteTab(IntPtr hwnd);
void ActivateTab(IntPtr hwnd);
void SetActiveAlt(IntPtr hwnd);
// ITaskbarList2
void MarkFullscreenWindow(IntPtr hwnd, [MarshalAs(UnmanagedType.Bool)] bool fFullscreen);
// ITaskbarList3
void SetProgressValue(IntPtr hwnd, ulong ullCompleted, ulong ullTotal);
void SetProgressState(IntPtr hwnd, TBPFLAG tbpFlags);
//void RegisterTab(IntPtr hwndTab, IntPtr hwndMDI);
//void UnregisterTab(IntPtr hwndTab);
//void SetTabOrder(IntPtr hwndTab, IntPtr hwndInsertBefore);
//void SetTabActive(IntPtr hwndTab, IntPtr hwndMDI, uint dwReserved);
//void ThumbBarAddButtons(IntPtr hwnd, uint cButtons, THUMBBUTTON[] pButton);
//void ThumbBarUpdateButtons(IntPtr hwnd, uint cButtons, THUMBBUTTON[] pButton);
//void ThumbBarSetImageList(IntPtr hwnd, IntPtr himl);
//void SetOverlayIcon(IntPtr hwnd, IntPtr hIcon, string pszDescription);
//void SetThumbnailTooltip(IntPtr hwnd, string pszTip);
//void SetThumbnailClip(IntPtr hwnd, ref RECT prcClip);
}
[ComImport, Guid("56FDF344-FD6D-11d0-958A-006097C9A090")]
internal class TaskbarList { }
internal static class TaskbarManager
{
private static readonly ITaskbarList3 taskbarList = (ITaskbarList3)new TaskbarList();
static TaskbarManager()
{
taskbarList.HrInit();
}
public static void SetProgressState(IntPtr windowHandle, TBPFLAG state)
{
taskbarList.SetProgressState(windowHandle, state);
}
public static void SetProgressValue(IntPtr windowHandle, ulong completed, ulong total)
{
taskbarList.SetProgressValue(windowHandle, completed, total);
}
}
}

View File

@@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
@@ -9,7 +10,7 @@ using System.Threading.Tasks;
namespace SpineViewer
{
public class PointFConverter : TypeConverter
public class PointFConverter : ExpandableObjectConverter
{
public override bool CanConvertTo(ITypeDescriptorContext? context, [NotNullWhen(true)] Type? destinationType)
{
@@ -44,12 +45,56 @@ namespace SpineViewer
}
return base.ConvertFrom(context, culture, value);
}
}
public override PropertyDescriptorCollection GetProperties(ITypeDescriptorContext? context, object value, Attribute[]? attributes)
public class StringEnumConverter : StringConverter
{
/// <summary>
/// 字符串标准值列表属性
/// </summary>
[AttributeUsage(AttributeTargets.Property, AllowMultiple = false, Inherited = false)]
public class StandardValuesAttribute : Attribute
{
return TypeDescriptor.GetProperties(typeof(PointF), attributes);
/// <summary>
/// 标准值列表
/// </summary>
public ReadOnlyCollection<string> StandardValues { get; private set; }
private readonly List<string> standardValues = [];
/// <summary>
/// 是否允许用户自定义
/// </summary>
public bool Customizable { get; set; } = false;
/// <summary>
/// 字符串标准值列表
/// </summary>
/// <param name="values">允许的字符串标准值</param>
public StandardValuesAttribute(params string[] values)
{
standardValues.AddRange(values);
StandardValues = standardValues.AsReadOnly();
}
}
public override bool GetPropertiesSupported(ITypeDescriptorContext? context) => true;
public override bool GetStandardValuesSupported(ITypeDescriptorContext? context) => true;
public override bool GetStandardValuesExclusive(ITypeDescriptorContext? context)
{
var customizable = context?.PropertyDescriptor?.Attributes.OfType<StandardValuesAttribute>().FirstOrDefault()?.Customizable ?? false;
return !customizable;
}
public override StandardValuesCollection? GetStandardValues(ITypeDescriptorContext? context)
{
// 查找属性上的 StandardValuesAttribute
var attribute = context?.PropertyDescriptor?.Attributes.OfType<StandardValuesAttribute>().FirstOrDefault();
StandardValuesCollection result;
if (attribute != null)
result = new StandardValuesCollection(attribute.StandardValues);
else
result = new StandardValuesCollection(Array.Empty<string>());
return result;
}
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 157 KiB

After

Width:  |  Height:  |  Size: 163 KiB