Compare commits

..

17 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
46 changed files with 906 additions and 512 deletions

View File

@@ -1,5 +1,12 @@
# CHANGELOG
## v0.11.5
- 导出格式全面支持
- 修复预览图不显示的问题
- 优化列表卡顿问题
- 模型列表增加数量显示
## v0.11.4
- 增加 MP4 导出格式

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
- [x] MP4
- [ ] MOV
- [ ] WebM
More formats are under development :rocket::rocket::rocket:
| Export Format | Suitable for Scenario |
| :------------: | :------------------------------------------------------------------------------------:|
| Single Frame | Supports generating high-definition model snapshots; you can manually adjust the frame. |
| Frame Sequence | Supports png sequence output with transparency and lossless compression. |
| GIF | Ideal for generating preview animations. |
| MP4 | The most common video format with the best compatibility. |
| WebM | Suitable for browser-based playback and supports transparent backgrounds. |
| MKV | For more experimental use. |
| MOV | For more experimental use. |
| Custom Export | In addition to the above presets, you can provide any FFmpeg parameters to meet complex custom needs. |
## Supported Spine Versions
| Version | View & Export | Format Conversion | Version Conversion |
| :-------: | :-----------: | :---------------: | :----------------: |
| `2.1.x` | :white_check_mark: | | |
| `3.1.x` | | | |
| `3.4.x` | | | |
| `3.5.x` | | | |
| `3.6.x` | :white_check_mark: | | |
| `3.7.x` | :white_check_mark: | | |
| `3.8.x` | :white_check_mark: | :white_check_mark: | |
| `4.1.x` | :white_check_mark: | | |
| `4.2.x` | :white_check_mark: | | |
| `4.3.x` | | | |
| Version | View & Export | Format Conversion | Version Conversion |
| :------: | :-------------------: | :------------------: | :-----------------: |
| `2.1.x` | :white_check_mark: | | |
| `3.1.x` | | | |
| `3.4.x` | | | |
| `3.5.x` | | | |
| `3.6.x` | :white_check_mark: | | |
| `3.7.x` | :white_check_mark: | | |
| `3.8.x` | :white_check_mark: | :white_check_mark: | |
| `4.1.x` | :white_check_mark: | | |
| `4.2.x` | :white_check_mark: | | |
| `4.3.x` | | | |
More versions are under development :rocket::rocket::rocket:
More versions are under development :rocket: :rocket: :rocket:
## Usage
## How to Use
### Importing Skeletons
### Importing Skeleton Files
There are three ways to import skeleton files:
- Drag and drop or paste the skeleton file/directory into the model list.
- Open skeleton files in batch from the File menu.
- Batch open skeleton files from the File menu.
- Select a single model to open from the File menu.
### Adjusting Preview Content
### Adjusting the Preview
The model list supports right-click menus and several hotkeys, and multiple models can be selected for batch adjustments of model parameters.
The model list supports context menus and some shortcuts, and you can multi-select to adjust parameters in bulk.
In addition to using the control panel for parameter settings, the preview window supports the following mouse actions:
In addition to using the panel for parameter settings, the preview screen supports several mouse actions:
- Left-click to select and drag models. Hold the `Ctrl` key to enable multi-selection, which is synchronized with the model list on the left.
- Left-click to select and drag models; hold the `Ctrl` key for multi-selection (which is synchronized with the list on the left).
- Right-click to drag the overall view.
- Use the scroll wheel to zoom in/out.
- "Render selected only" mode, in which the preview only includes selected models and the selection can only be changed via the model list on the left.
- Use the mouse wheel to zoom in and out.
- Render Selected mode: in this mode, the preview screen only shows the selected models and the selection state can only be changed from the list on the left.
The buttons below the preview window allow you to adjust the timeline, effectively serving as a simple player.
The buttons below the preview allow you to adjust the timeline, acting as a simple media player.
### Exporting Preview Content
### Exporting the Preview
Export follows the "What You See Is What You Get" principle—what you see in the live preview is exactly what gets exported.
Exporting follows the What You See Is What You Get principle the preview exactly reflects the output.
There are a few key parameters for exporting:
There are several key parameters for export:
- Render Selected Only: This option not only affects the preview mode but also the export; if enabled, only the selected models will be considered, and all other models will be ignored during export.
- Output Folder: This parameter is optional in some cases. If not provided, the output will be saved in each model's own directory. Otherwise, all output files will be saved to the specified folder.
- Single Export: By default, each model is exported individually in batch mode. If "Single Export" is selected, all exported models will be rendered on a single canvas, resulting in only one output file.
- Render Selected Only: This option affects both the preview and export. If enabled, only the selected models will be considered during export while ignoring the others.
- Output Folder: This parameter is optional in some cases. If not provided, the output files will be saved in each 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
- [x] 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

@@ -54,7 +54,13 @@
toolStripMenuItem_DetailsView = new ToolStripMenuItem();
imageList_LargeIcon = new ImageList(components);
imageList_SmallIcon = new ImageList(components);
timer_SelectedIndexChangedDebounce = new System.Windows.Forms.Timer(components);
statusStrip = new StatusStrip();
toolStripStatusLabel_CountInfo = new ToolStripStatusLabel();
tableLayoutPanel = new TableLayoutPanel();
contextMenuStrip.SuspendLayout();
statusStrip.SuspendLayout();
tableLayoutPanel.SuspendLayout();
SuspendLayout();
//
// listView
@@ -68,9 +74,10 @@
listView.GridLines = true;
listView.LargeImageList = imageList_LargeIcon;
listView.Location = new Point(0, 0);
listView.Margin = new Padding(0);
listView.Name = "listView";
listView.ShowItemToolTips = true;
listView.Size = new Size(336, 445);
listView.Size = new Size(336, 414);
listView.SmallImageList = imageList_SmallIcon;
listView.TabIndex = 1;
listView.UseCompatibleStateImageBehavior = false;
@@ -250,14 +257,56 @@
imageList_SmallIcon.ImageSize = new Size(48, 48);
imageList_SmallIcon.TransparentColor = Color.Transparent;
//
// timer_SelectedIndexChangedDebounce
//
timer_SelectedIndexChangedDebounce.Interval = 30;
timer_SelectedIndexChangedDebounce.Tick += timer_SelectedIndexChangedDebounce_Tick;
//
// statusStrip
//
statusStrip.Dock = DockStyle.Fill;
statusStrip.ImageScalingSize = new Size(24, 24);
statusStrip.Items.AddRange(new ToolStripItem[] { toolStripStatusLabel_CountInfo });
statusStrip.Location = new Point(0, 414);
statusStrip.Name = "statusStrip";
statusStrip.Size = new Size(336, 31);
statusStrip.SizingGrip = false;
statusStrip.TabIndex = 2;
statusStrip.Text = "statusStrip1";
//
// toolStripStatusLabel_CountInfo
//
toolStripStatusLabel_CountInfo.Name = "toolStripStatusLabel_CountInfo";
toolStripStatusLabel_CountInfo.Size = new Size(178, 24);
toolStripStatusLabel_CountInfo.Text = "已选择 0 项,共 0 项";
//
// tableLayoutPanel
//
tableLayoutPanel.ColumnCount = 1;
tableLayoutPanel.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 100F));
tableLayoutPanel.Controls.Add(listView, 0, 0);
tableLayoutPanel.Controls.Add(statusStrip, 0, 1);
tableLayoutPanel.Dock = DockStyle.Fill;
tableLayoutPanel.Location = new Point(0, 0);
tableLayoutPanel.Name = "tableLayoutPanel";
tableLayoutPanel.RowCount = 2;
tableLayoutPanel.RowStyles.Add(new RowStyle(SizeType.Percent, 100F));
tableLayoutPanel.RowStyles.Add(new RowStyle());
tableLayoutPanel.Size = new Size(336, 445);
tableLayoutPanel.TabIndex = 3;
//
// SpineListView
//
AutoScaleDimensions = new SizeF(11F, 24F);
AutoScaleMode = AutoScaleMode.Font;
Controls.Add(listView);
Controls.Add(tableLayoutPanel);
Name = "SpineListView";
Size = new Size(336, 445);
contextMenuStrip.ResumeLayout(false);
statusStrip.ResumeLayout(false);
statusStrip.PerformLayout();
tableLayoutPanel.ResumeLayout(false);
tableLayoutPanel.PerformLayout();
ResumeLayout(false);
}
@@ -287,5 +336,9 @@
private ToolStripMenuItem toolStripMenuItem_SelectAll;
private ToolStripSeparator toolStripSeparator4;
private ToolStripMenuItem toolStripMenuItem_AddFromClipboard;
private System.Windows.Forms.Timer timer_SelectedIndexChangedDebounce;
private StatusStrip statusStrip;
private ToolStripStatusLabel toolStripStatusLabel_CountInfo;
private TableLayoutPanel tableLayoutPanel;
}
}

View File

@@ -228,6 +228,18 @@ namespace SpineViewer.Controls
}
private void listView_SelectedIndexChanged(object sender, EventArgs e)
{
timer_SelectedIndexChangedDebounce.Stop();
timer_SelectedIndexChangedDebounce.Start();
}
private void timer_SelectedIndexChangedDebounce_Tick(object sender, EventArgs e)
{
timer_SelectedIndexChangedDebounce.Stop();
_listView_SelectedIndexChanged(listView, EventArgs.Empty);
}
private void _listView_SelectedIndexChanged(object sender, EventArgs e)
{
lock (Spines)
{
@@ -257,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)
@@ -395,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();
}
}

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

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

@@ -65,7 +65,7 @@ namespace SpineViewer.Dialogs
skelPath = Path.GetFullPath(skelPath);
}
if (string.IsNullOrEmpty(atlasPath))
if (string.IsNullOrWhiteSpace(atlasPath))
{
atlasPath = null;
}

View File

@@ -79,14 +79,14 @@ namespace SpineViewer.Exporter
/// </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,17 +15,18 @@ 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)]
[AttributeUsage(AttributeTargets.Class, AllowMultiple = true, Inherited = false)]
public class ExportImplementationAttribute(ExportType exportType) : Attribute, IImplementationKey<ExportType>
{
public ExportType ImplementationKey { get; private set; } = exportType;

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,8 +11,8 @@ 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)
{
@@ -22,33 +23,34 @@ namespace SpineViewer.Exporter.Implementations.ExportArgs
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

@@ -1,4 +1,5 @@
using System;
using FFMpegCore;
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
@@ -10,26 +11,47 @@ namespace SpineViewer.Exporter.Implementations.ExportArgs
/// <summary>
/// MP4 导出参数
/// </summary>
[ExportImplementation(ExportType.MP4)]
public class Mp4ExportArgs : VideoExportArgs
[ExportImplementation(ExportType.Mp4)]
public class Mp4ExportArgs : FFmpegVideoExportArgs
{
public Mp4ExportArgs(Size resolution, SFML.Graphics.View view, bool renderSelectedOnly) : base(resolution, view, renderSelectedOnly)
public Mp4ExportArgs(Size resolution, SFML.Graphics.View view, bool renderSelectedOnly) : base(resolution, view, renderSelectedOnly)
{
// MP4 默认用绿幕
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("[2] MP4 "), DisplayName("CRF"), Description("Constant Rate Factor, 0-63, 18-28, 23, ")]
[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>
/// 编码器 TODO: 增加其他编码器
/// 像素格式
/// </summary>
[Category("[2] MP4 "), DisplayName(""), Description("使")]
public string Codec { get => "libx264"; }
[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

@@ -13,49 +13,57 @@ using System.Diagnostics;
namespace SpineViewer.Exporter.Implementations.Exporter
{
/// <summary>
/// GIF 动图导出器
/// 使用 FFmpeg 的视频导出器
/// </summary>
[ExportImplementation(ExportType.GIF)]
public class GifExporter : VideoExporter
[ExportImplementation(ExportType.Gif)]
[ExportImplementation(ExportType.Mp4)]
[ExportImplementation(ExportType.Webm)]
[ExportImplementation(ExportType.Mkv)]
[ExportImplementation(ExportType.Mov)]
[ExportImplementation(ExportType.Custom)]
public class FFmpegVideoExporter : VideoExporter
{
public GifExporter(GifExportArgs exportArgs) : base(exportArgs) { }
public FFmpegVideoExporter(FFmpegVideoExportArgs exportArgs) : base(exportArgs) { }
protected override void ExportSingle(Spine.Spine[] spinesToRender, BackgroundWorker? worker = null)
{
var args = (GifExportArgs)ExportArgs;
var args = (FFmpegVideoExportArgs)ExportArgs;
var noteSuffix = args.FileNameNoteSuffix;
if (!string.IsNullOrWhiteSpace(noteSuffix)) noteSuffix = $"_{noteSuffix}";
var filename = $"{timestamp}_{args.FPS:f0}{noteSuffix}{args.Suffix}";
// 导出单个时必定提供输出文件夹
var filename = $"{timestamp}_{args.FPS:f0}_{args.MaxColors}_{args.AlphaThreshold}.gif";
var savePath = Path.Combine(args.OutputDir, filename);
var videoFramesSource = new RawVideoPipeSource(GetFrames(spinesToRender, worker)) { FrameRate = args.FPS };
try
{
var ffmpegArgs = FFMpegArguments
.FromPipeInput(videoFramesSource)
.OutputToFile(savePath, true, options => options
.ForceFormat("gif")
.WithCustomArgument(args.FFMpegCoreCustomArguments));
var ffmpegArgs = FFMpegArguments.FromPipeInput(videoFramesSource).OutputToFile(savePath, true, args.SetOutputOptions);
logger.Info("FFMpeg arguments: {}", ffmpegArgs.Arguments);
logger.Info("FFmpeg arguments: {}", ffmpegArgs.Arguments);
ffmpegArgs.ProcessSynchronously();
}
catch (Exception ex)
{
logger.Error(ex.ToString());
logger.Error("Failed to export gif {}", savePath);
logger.Error("Failed to export {} {}", args.Format, savePath);
}
}
protected override void ExportIndividual(Spine.Spine[] spinesToRender, BackgroundWorker? worker = null)
{
var args = (GifExportArgs)ExportArgs;
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 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 };
@@ -63,17 +71,15 @@ namespace SpineViewer.Exporter.Implementations.Exporter
{
var ffmpegArgs = FFMpegArguments
.FromPipeInput(videoFramesSource)
.OutputToFile(savePath, true, options => options
.ForceFormat("gif")
.WithCustomArgument(args.FFMpegCoreCustomArguments));
.OutputToFile(savePath, true, args.SetOutputOptions);
logger.Info("FFMpeg arguments: {}", ffmpegArgs.Arguments);
logger.Info("FFmpeg arguments: {}", ffmpegArgs.Arguments);
ffmpegArgs.ProcessSynchronously();
}
catch (Exception ex)
{
logger.Error(ex.ToString());
logger.Error("Failed to export gif {} {}", savePath, spine.SkelPath);
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");
@@ -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

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

View File

@@ -1,85 +0,0 @@
using FFMpegCore.Pipes;
using FFMpegCore;
using SpineViewer.Exporter.Implementations.ExportArgs;
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using FFMpegCore.Arguments;
using System.Diagnostics;
namespace SpineViewer.Exporter.Implementations.Exporter
{
/// <summary>
/// MP4 导出器
/// </summary>
[ExportImplementation(ExportType.MP4)]
public class Mp4Exporter : VideoExporter
{
public Mp4Exporter(Mp4ExportArgs exportArgs) : base(exportArgs) { }
protected override void ExportSingle(Spine.Spine[] spinesToRender, BackgroundWorker? worker = null)
{
var args = (Mp4ExportArgs)ExportArgs;
// 导出单个时必定提供输出文件夹
var filename = $"{timestamp}_{args.FPS:f0}_{args.CRF}.mp4";
var savePath = Path.Combine(args.OutputDir, filename);
var videoFramesSource = new RawVideoPipeSource(GetFrames(spinesToRender, worker)) { FrameRate = args.FPS };
try
{
var ffmpegArgs = FFMpegArguments
.FromPipeInput(videoFramesSource)
.OutputToFile(savePath, true, options => options
.ForceFormat("mp4")
.WithVideoCodec(args.Codec)
.WithConstantRateFactor(args.CRF)
.WithFastStart());
logger.Info("FFMpeg arguments: {}", ffmpegArgs.Arguments);
ffmpegArgs.ProcessSynchronously();
}
catch (Exception ex)
{
logger.Error(ex.ToString());
logger.Error("Failed to export mp4 {}", savePath);
}
}
protected override void ExportIndividual(Spine.Spine[] spinesToRender, BackgroundWorker? worker = null)
{
var args = (Mp4ExportArgs)ExportArgs;
foreach (var spine in spinesToRender)
{
if (worker?.CancellationPending == true) break; // 取消的日志在 GetFrames 里输出
// 如果提供了输出文件夹, 则全部导出到输出文件夹, 否则导出到各自的文件夹下
var filename = $"{spine.Name}_{timestamp}_{args.FPS:f0}_{args.CRF}.mp4";
var savePath = Path.Combine(args.OutputDir ?? spine.AssetsDir, filename);
var videoFramesSource = new RawVideoPipeSource(GetFrames(spine, worker)) { FrameRate = args.FPS };
try
{
var ffmpegArgs = FFMpegArguments
.FromPipeInput(videoFramesSource)
.OutputToFile(savePath, true, options => options
.ForceFormat("mp4")
.WithVideoCodec(args.Codec)
.WithConstantRateFactor(args.CRF)
.WithFastStart());
logger.Info("FFMpeg arguments: {}", ffmpegArgs.Arguments);
ffmpegArgs.ProcessSynchronously();
}
catch (Exception ex)
{
logger.Error(ex.ToString());
logger.Error("Failed to export mp4 {} {}", savePath, spine.SkelPath);
}
}
}
}
}

View File

@@ -10,20 +10,6 @@ using System.Threading.Tasks;
namespace SpineViewer.Exporter
{
public class SFMLImageFileSuffixConverter : StringConverter
{
private readonly string[] supportedFileSuffix = [".png", ".jpg", ".tga", ".bmp"];
public override bool GetStandardValuesSupported(ITypeDescriptorContext? context) => true;
public override bool GetStandardValuesExclusive(ITypeDescriptorContext? context) => true;
public override StandardValuesCollection? GetStandardValues(ITypeDescriptorContext? context)
{
return new StandardValuesCollection(supportedFileSuffix);
}
}
public class SFMLColorConverter : ExpandableObjectConverter
{
private class SFMLColorPropertyDescriptor : SimplePropertyDescriptor

View File

@@ -32,8 +32,7 @@ namespace SpineViewer
var impTypes = Assembly.GetExecutingAssembly().GetTypes().Where(t => baseType.IsAssignableFrom(t) && !t.IsAbstract);
foreach (var type in impTypes)
{
var attr = type.GetCustomAttribute<TAttr>();
if (attr is not null)
foreach (var attr in type.GetCustomAttributes<TAttr>())
{
var key = attr.ImplementationKey;
if (ImplementationTypes.ContainsKey(key))

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();
@@ -132,7 +133,7 @@
//
// 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(270, 34);
toolStripMenuItem_Export.Text = "导出(&E)";
@@ -140,55 +141,59 @@
// toolStripMenuItem_ExportFrame
//
toolStripMenuItem_ExportFrame.Name = "toolStripMenuItem_ExportFrame";
toolStripMenuItem_ExportFrame.Size = new Size(270, 34);
toolStripMenuItem_ExportFrame.Size = new Size(288, 34);
toolStripMenuItem_ExportFrame.Text = "单帧画面...";
toolStripMenuItem_ExportFrame.Click += toolStripMenuItem_Export_Click;
//
// toolStripMenuItem_ExportFrameSequence
//
toolStripMenuItem_ExportFrameSequence.Name = "toolStripMenuItem_ExportFrameSequence";
toolStripMenuItem_ExportFrameSequence.Size = new Size(270, 34);
toolStripMenuItem_ExportFrameSequence.Size = new Size(288, 34);
toolStripMenuItem_ExportFrameSequence.Text = "帧序列...";
toolStripMenuItem_ExportFrameSequence.Click += toolStripMenuItem_Export_Click;
//
// toolStripMenuItem_ExportGif
//
toolStripMenuItem_ExportGif.Name = "toolStripMenuItem_ExportGif";
toolStripMenuItem_ExportGif.Size = new Size(270, 34);
toolStripMenuItem_ExportGif.Size = new Size(288, 34);
toolStripMenuItem_ExportGif.Text = "GIF...";
toolStripMenuItem_ExportGif.Click += toolStripMenuItem_Export_Click;
//
// toolStripMenuItem_ExportMkv
//
toolStripMenuItem_ExportMkv.Name = "toolStripMenuItem_ExportMkv";
toolStripMenuItem_ExportMkv.Size = new Size(270, 34);
toolStripMenuItem_ExportMkv.Text = "MKV";
toolStripMenuItem_ExportMkv.Visible = false;
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(270, 34);
toolStripMenuItem_ExportMp4.Size = new Size(288, 34);
toolStripMenuItem_ExportMp4.Text = "MP4...";
toolStripMenuItem_ExportMp4.Click += toolStripMenuItem_Export_Click;
//
// toolStripMenuItem_ExportMov
//
toolStripMenuItem_ExportMov.Name = "toolStripMenuItem_ExportMov";
toolStripMenuItem_ExportMov.Size = new Size(270, 34);
toolStripMenuItem_ExportMov.Size = new Size(288, 34);
toolStripMenuItem_ExportMov.Text = "MOV...";
toolStripMenuItem_ExportMov.Visible = false;
toolStripMenuItem_ExportMov.Click += toolStripMenuItem_Export_Click;
//
// toolStripMenuItem_ExportWebm
//
toolStripMenuItem_ExportWebm.Name = "toolStripMenuItem_ExportWebm";
toolStripMenuItem_ExportWebm.Size = new Size(270, 34);
toolStripMenuItem_ExportWebm.Size = new Size(288, 34);
toolStripMenuItem_ExportWebm.Text = "WebM...";
toolStripMenuItem_ExportWebm.Visible = false;
toolStripMenuItem_ExportWebm.Click += toolStripMenuItem_Export_Click;
//
// toolStripMenuItem_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";
@@ -266,7 +271,7 @@
rtbLog.Margin = new Padding(3, 2, 3, 2);
rtbLog.Name = "rtbLog";
rtbLog.ReadOnly = true;
rtbLog.Size = new Size(1758, 134);
rtbLog.Size = new Size(1758, 146);
rtbLog.TabIndex = 0;
rtbLog.Text = "";
rtbLog.WordWrap = false;
@@ -290,7 +295,7 @@
splitContainer_MainForm.Panel2.Controls.Add(rtbLog);
splitContainer_MainForm.Panel2.Cursor = Cursors.Default;
splitContainer_MainForm.Size = new Size(1758, 1097);
splitContainer_MainForm.SplitterDistance = 955;
splitContainer_MainForm.SplitterDistance = 943;
splitContainer_MainForm.SplitterWidth = 8;
splitContainer_MainForm.TabIndex = 3;
splitContainer_MainForm.TabStop = false;
@@ -314,7 +319,7 @@
//
splitContainer_Functional.Panel2.Controls.Add(groupBox_Preview);
splitContainer_Functional.Panel2.Cursor = Cursors.Default;
splitContainer_Functional.Size = new Size(1758, 955);
splitContainer_Functional.Size = new Size(1758, 943);
splitContainer_Functional.SplitterDistance = 759;
splitContainer_Functional.SplitterWidth = 8;
splitContainer_Functional.TabIndex = 2;
@@ -338,7 +343,7 @@
//
splitContainer_Information.Panel2.Controls.Add(splitContainer_Config);
splitContainer_Information.Panel2.Cursor = Cursors.Default;
splitContainer_Information.Size = new Size(759, 955);
splitContainer_Information.Size = new Size(759, 943);
splitContainer_Information.SplitterDistance = 354;
splitContainer_Information.SplitterWidth = 8;
splitContainer_Information.TabIndex = 1;
@@ -352,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(354, 955);
groupBox_SkelList.Size = new Size(354, 943);
groupBox_SkelList.TabIndex = 0;
groupBox_SkelList.TabStop = false;
groupBox_SkelList.Text = "模型列表";
@@ -363,7 +368,7 @@
spineListView.Location = new Point(3, 26);
spineListView.Name = "spineListView";
spineListView.PropertyGrid = propertyGrid_Spine;
spineListView.Size = new Size(348, 926);
spineListView.Size = new Size(348, 914);
spineListView.TabIndex = 0;
//
// propertyGrid_Spine
@@ -372,7 +377,7 @@
propertyGrid_Spine.HelpVisible = false;
propertyGrid_Spine.Location = new Point(3, 26);
propertyGrid_Spine.Name = "propertyGrid_Spine";
propertyGrid_Spine.Size = new Size(391, 592);
propertyGrid_Spine.Size = new Size(391, 580);
propertyGrid_Spine.TabIndex = 0;
propertyGrid_Spine.ToolbarVisible = false;
propertyGrid_Spine.PropertyValueChanged += propertyGrid_PropertyValueChanged;
@@ -395,7 +400,7 @@
//
splitContainer_Config.Panel2.Controls.Add(groupBox_SkelConfig);
splitContainer_Config.Panel2.Cursor = Cursors.Default;
splitContainer_Config.Size = new Size(397, 955);
splitContainer_Config.Size = new Size(397, 943);
splitContainer_Config.SplitterDistance = 326;
splitContainer_Config.SplitterWidth = 8;
splitContainer_Config.TabIndex = 0;
@@ -431,7 +436,7 @@
groupBox_SkelConfig.Dock = DockStyle.Fill;
groupBox_SkelConfig.Location = new Point(0, 0);
groupBox_SkelConfig.Name = "groupBox_SkelConfig";
groupBox_SkelConfig.Size = new Size(397, 621);
groupBox_SkelConfig.Size = new Size(397, 609);
groupBox_SkelConfig.TabIndex = 0;
groupBox_SkelConfig.TabStop = false;
groupBox_SkelConfig.Text = "模型参数";
@@ -442,7 +447,7 @@
groupBox_Preview.Dock = DockStyle.Fill;
groupBox_Preview.Location = new Point(0, 0);
groupBox_Preview.Name = "groupBox_Preview";
groupBox_Preview.Size = new Size(991, 955);
groupBox_Preview.Size = new Size(991, 943);
groupBox_Preview.TabIndex = 1;
groupBox_Preview.TabStop = false;
groupBox_Preview.Text = "预览画面";
@@ -453,7 +458,7 @@
spinePreviewer.Location = new Point(3, 26);
spinePreviewer.Name = "spinePreviewer";
spinePreviewer.PropertyGrid = propertyGrid_Previewer;
spinePreviewer.Size = new Size(985, 926);
spinePreviewer.Size = new Size(985, 914);
spinePreviewer.SpineListView = spineListView;
spinePreviewer.TabIndex = 0;
//
@@ -553,5 +558,6 @@
private ToolStripMenuItem toolStripMenuItem_ExportMov;
private ToolStripMenuItem toolStripMenuItem_ExportMkv;
private ToolStripMenuItem toolStripMenuItem_ExportWebm;
private ToolStripMenuItem toolStripMenuItem_ExportCustom;
}
}

View File

@@ -24,11 +24,12 @@ 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

View File

@@ -92,7 +92,7 @@ namespace SpineViewer.Spine.Implementations.Spine
public override string FileVersion { get => skeletonData.Version; }
public override float Scale
protected override float scale
{
get
{
@@ -106,11 +106,11 @@ namespace SpineViewer.Spine.Implementations.Spine
set
{
// 保存状态
var position = Position;
var flipX = FlipX;
var flipY = FlipY;
var animation = Track0Animation; // TODO: 适配多轨道
var skin = Skin;
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)
@@ -130,38 +130,37 @@ namespace SpineViewer.Spine.Implementations.Spine
animationState = new AnimationState(animationStateData);
// 恢复状态
Position = position;
FlipX = flipX;
FlipY = flipY;
Track0Animation = animation; // TODO: 适配多轨道
Skin = 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;
protected override PointF position
{
get => new(skeleton.X, skeleton.Y);
set
{
skeleton.X = value.X;
skeleton.Y = value.Y;
Update(0);
}
}
}
public override bool FlipX
protected override bool flipX
{
get => skeleton.FlipX;
set { skeleton.FlipX = value; Update(0); }
set => skeleton.FlipX = value;
}
public override bool FlipY
protected override bool flipY
{
get => skeleton.FlipY;
set { skeleton.FlipY = value; Update(0); }
set => skeleton.FlipY = value;
}
public override string Skin
protected override string skin
{
get => skeleton.Skin?.Name ?? "default";
set
@@ -169,11 +168,10 @@ namespace SpineViewer.Spine.Implementations.Spine
if (!skinNames.Contains(value)) return;
skeleton.SetSkin(value);
skeleton.SetSlotsToSetupPose();
Update(0);
}
}
public override string Track0Animation
protected override string track0Animation
{
get => animationState.GetCurrent(0)?.Animation.Name ?? EMPTY_ANIMATION;
set
@@ -182,11 +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 RectangleF Bounds
protected override RectangleF bounds
{
get
{
@@ -238,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);
@@ -258,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;
@@ -330,13 +327,13 @@ namespace SpineViewer.Spine.Implementations.Spine
{
if (vertexArray.VertexCount > 0)
{
if (UsePremultipliedAlpha && (states.BlendMode == BlendModeSFML.Normal || states.BlendMode == BlendModeSFML.Additive))
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();
@@ -379,24 +376,24 @@ namespace SpineViewer.Spine.Implementations.Spine
//clipping.ClipEnd(slot);
}
if (UsePremultipliedAlpha && (states.BlendMode == BlendModeSFML.Normal || states.BlendMode == BlendModeSFML.Additive))
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

@@ -91,7 +91,7 @@ namespace SpineViewer.Spine.Implementations.Spine
public override string FileVersion { get => skeletonData.Version; }
public override float Scale
protected override float scale
{
get
{
@@ -105,11 +105,11 @@ namespace SpineViewer.Spine.Implementations.Spine
set
{
// 保存状态
var position = Position;
var flipX = FlipX;
var flipY = FlipY;
var animation = Track0Animation; // TODO: 适配多轨道
var skin = Skin;
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)
@@ -129,38 +129,37 @@ namespace SpineViewer.Spine.Implementations.Spine
animationState = new AnimationState(animationStateData);
// 恢复状态
Position = position;
FlipX = flipX;
FlipY = flipY;
Track0Animation = animation; // TODO: 适配多轨道
Skin = 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;
Update(0);
}
}
public override bool FlipX
protected override bool flipX
{
get => skeleton.FlipX;
set { skeleton.FlipX = value; Update(0); }
set => skeleton.FlipX = value;
}
public override bool FlipY
protected override bool flipY
{
get => skeleton.FlipY;
set { skeleton.FlipY = value; Update(0); }
set => skeleton.FlipY = value;
}
public override string Skin
protected override string skin
{
get => skeleton.Skin?.Name ?? "default";
set
@@ -168,11 +167,10 @@ namespace SpineViewer.Spine.Implementations.Spine
if (!skinNames.Contains(value)) return;
skeleton.SetSkin(value);
skeleton.SetSlotsToSetupPose();
Update(0);
}
}
public override string Track0Animation
protected override string track0Animation
{
get => animationState.GetCurrent(0)?.Animation.Name ?? EMPTY_ANIMATION;
set
@@ -181,11 +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 RectangleF Bounds
protected override RectangleF bounds
{
get
{
@@ -197,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);
@@ -217,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;
@@ -287,13 +284,13 @@ namespace SpineViewer.Spine.Implementations.Spine
{
if (vertexArray.VertexCount > 0)
{
if (UsePremultipliedAlpha && (states.BlendMode == BlendModeSFML.Normal || states.BlendMode == BlendModeSFML.Additive))
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();
@@ -337,23 +334,23 @@ namespace SpineViewer.Spine.Implementations.Spine
}
clipping.ClipEnd();
if (UsePremultipliedAlpha && (states.BlendMode == BlendModeSFML.Normal || states.BlendMode == BlendModeSFML.Additive))
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

@@ -89,51 +89,47 @@ 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
{
skeleton.ScaleX = Math.Sign(skeleton.ScaleX) * value;
skeleton.ScaleY = Math.Sign(skeleton.ScaleY) * value;
Update(0);
}
}
public override PointF Position
protected override PointF position
{
get => new(skeleton.X, skeleton.Y);
set
{
skeleton.X = value.X;
skeleton.Y = value.Y;
Update(0);
}
}
public override bool FlipX
protected override bool flipX
{
get => skeleton.ScaleX < 0;
set
{
if (skeleton.ScaleX > 0 && value || skeleton.ScaleX < 0 && !value)
skeleton.ScaleX *= -1;
Update(0);
}
}
public override bool FlipY
protected override bool flipY
{
get => skeleton.ScaleY < 0;
set
{
if (skeleton.ScaleY > 0 && value || skeleton.ScaleY < 0 && !value)
skeleton.ScaleY *= -1;
Update(0);
}
}
public override string Skin
protected override string skin
{
get => skeleton.Skin?.Name ?? "default";
set
@@ -141,11 +137,10 @@ namespace SpineViewer.Spine.Implementations.Spine
if (!skinNames.Contains(value)) return;
skeleton.SetSkin(value);
skeleton.SetSlotsToSetupPose();
Update(0);
}
}
public override string Track0Animation
protected override string track0Animation
{
get => animationState.GetCurrent(0)?.Animation.Name ?? EMPTY_ANIMATION;
set
@@ -154,11 +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 RectangleF Bounds
protected override RectangleF bounds
{
get
{
@@ -170,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);
@@ -190,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;
@@ -261,13 +255,13 @@ namespace SpineViewer.Spine.Implementations.Spine
if (vertexArray.VertexCount > 0)
{
// XXX: 实测不用设置 sampler2D 的值也正确
if (UsePremultipliedAlpha && (states.BlendMode == BlendModeSFML.Normal || states.BlendMode == BlendModeSFML.Additive))
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();
@@ -311,23 +305,23 @@ namespace SpineViewer.Spine.Implementations.Spine
}
clipping.ClipEnd();
if (UsePremultipliedAlpha && (states.BlendMode == BlendModeSFML.Normal || states.BlendMode == BlendModeSFML.Additive))
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

@@ -95,51 +95,47 @@ 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
{
skeleton.ScaleX = Math.Sign(skeleton.ScaleX) * value;
skeleton.ScaleY = Math.Sign(skeleton.ScaleY) * value;
Update(0);
}
}
public override PointF Position
protected override PointF position
{
get => new(skeleton.X, skeleton.Y);
set
{
skeleton.X = value.X;
skeleton.Y = value.Y;
Update(0);
}
}
public override bool FlipX
protected override bool flipX
{
get => skeleton.ScaleX < 0;
set
{
if (skeleton.ScaleX > 0 && value || skeleton.ScaleX < 0 && !value)
skeleton.ScaleX *= -1;
Update(0);
}
}
public override bool FlipY
protected override bool flipY
{
get => skeleton.ScaleY < 0;
set
{
if (skeleton.ScaleY > 0 && value || skeleton.ScaleY < 0 && !value)
skeleton.ScaleY *= -1;
Update(0);
}
}
public override string Skin
protected override string skin
{
get => skeleton.Skin?.Name ?? "default";
set
@@ -147,11 +143,10 @@ namespace SpineViewer.Spine.Implementations.Spine
if (!skinNames.Contains(value)) return;
skeleton.SetSkin(value);
skeleton.SetSlotsToSetupPose();
Update(0);
}
}
public override string Track0Animation
protected override string track0Animation
{
get => animationState.GetCurrent(0)?.Animation.Name ?? EMPTY_ANIMATION;
set
@@ -160,11 +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 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))
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))
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

@@ -91,51 +91,47 @@ 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
{
skeleton.ScaleX = Math.Sign(skeleton.ScaleX) * value;
skeleton.ScaleY = Math.Sign(skeleton.ScaleY) * value;
Update(0);
}
}
public override PointF Position
protected override PointF position
{
get => new(skeleton.X, skeleton.Y);
set
{
skeleton.X = value.X;
skeleton.Y = value.Y;
Update(0);
}
}
public override bool FlipX
protected override bool flipX
{
get => skeleton.ScaleX < 0;
set
{
if (skeleton.ScaleX > 0 && value || skeleton.ScaleX < 0 && !value)
skeleton.ScaleX *= -1;
Update(0);
}
}
public override bool FlipY
protected override bool flipY
{
get => skeleton.ScaleY < 0;
set
{
if (skeleton.ScaleY > 0 && value || skeleton.ScaleY < 0 && !value)
skeleton.ScaleY *= -1;
Update(0);
}
}
public override string Skin
protected override string skin
{
get => skeleton.Skin?.Name ?? "default";
set
@@ -143,11 +139,10 @@ namespace SpineViewer.Spine.Implementations.Spine
if (!skinNames.Contains(value)) return;
skeleton.SetSkin(value);
skeleton.SetSlotsToSetupPose();
Update(0);
}
}
public override string Track0Animation
protected override string track0Animation
{
get => animationState.GetCurrent(0)?.Animation.Name ?? EMPTY_ANIMATION;
set
@@ -156,11 +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 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))
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))
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

@@ -91,51 +91,47 @@ 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
{
skeleton.ScaleX = Math.Sign(skeleton.ScaleX) * value;
skeleton.ScaleY = Math.Sign(skeleton.ScaleY) * value;
Update(0);
}
}
public override PointF Position
protected override PointF position
{
get => new(skeleton.X, skeleton.Y);
set
{
skeleton.X = value.X;
skeleton.Y = value.Y;
Update(0);
}
}
public override bool FlipX
protected override bool flipX
{
get => skeleton.ScaleX < 0;
set
{
if (skeleton.ScaleX > 0 && value || skeleton.ScaleX < 0 && !value)
skeleton.ScaleX *= -1;
Update(0);
}
}
public override bool FlipY
protected override bool flipY
{
get => skeleton.ScaleY < 0;
set
{
if (skeleton.ScaleY > 0 && value || skeleton.ScaleY < 0 && !value)
skeleton.ScaleY *= -1;
Update(0);
}
}
public override string Skin
protected override string skin
{
get => skeleton.Skin?.Name ?? "default";
set
@@ -143,11 +139,10 @@ namespace SpineViewer.Spine.Implementations.Spine
if (!skinNames.Contains(value)) return;
skeleton.SetSkin(value);
skeleton.SetSlotsToSetupPose();
Update(0);
}
}
public override string Track0Animation
protected override string track0Animation
{
get => animationState.GetCurrent(0)?.Animation.Name ?? EMPTY_ANIMATION;
set
@@ -156,11 +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 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))
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))
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

@@ -91,51 +91,47 @@ 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
{
skeleton.ScaleX = Math.Sign(skeleton.ScaleX) * value;
skeleton.ScaleY = Math.Sign(skeleton.ScaleY) * value;
Update(0);
}
}
public override PointF Position
protected override PointF position
{
get => new(skeleton.X, skeleton.Y);
set
{
skeleton.X = value.X;
skeleton.Y = value.Y;
Update(0);
}
}
public override bool FlipX
protected override bool flipX
{
get => skeleton.ScaleX < 0;
set
{
if (skeleton.ScaleX > 0 && value || skeleton.ScaleX < 0 && !value)
skeleton.ScaleX *= -1;
Update(0);
}
}
public override bool FlipY
protected override bool flipY
{
get => skeleton.ScaleY < 0;
set
{
if (skeleton.ScaleY > 0 && value || skeleton.ScaleY < 0 && !value)
skeleton.ScaleY *= -1;
Update(0);
}
}
public override string Skin
protected override string skin
{
get => skeleton.Skin?.Name ?? "default";
set
@@ -143,11 +139,10 @@ namespace SpineViewer.Spine.Implementations.Spine
if (!skinNames.Contains(value)) return;
skeleton.SetSkin(value);
skeleton.SetSlotsToSetupPose();
Update(0);
}
}
public override string Track0Animation
protected override string track0Animation
{
get => animationState.GetCurrent(0)?.Animation.Name ?? EMPTY_ANIMATION;
set
@@ -156,11 +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 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))
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))
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

@@ -19,7 +19,7 @@ using SpineViewer.Exporter;
namespace SpineViewer.Spine
{
/// <summary>
/// Spine 基类, 使用静态方法 New 来创建具体版本对象
/// Spine 基类, 使用静态方法 New 来创建具体版本对象, 该类是线程安全的
/// </summary>
public abstract class Spine : ImplementationResolver<Spine, SpineImplementationAttribute, SpineVersion>, SFML.Graphics.Drawable, IDisposable
{
@@ -53,6 +53,11 @@ namespace SpineViewer.Spine
return New(version, [skelPath, atlasPath]).PostInit();
}
/// <summary>
/// 数据锁
/// </summary>
private readonly object _lock = new();
/// <summary>
/// 构造函数
/// </summary>
@@ -73,7 +78,8 @@ namespace SpineViewer.Spine
SkinNames = skinNames.AsReadOnly();
AnimationNames = animationNames.AsReadOnly();
InitBounds = Bounds;
// 必须 Update 一次否则包围盒还没有值
update(0);
// XXX: tex 没办法在这里主动 Dispose
// 批量添加在获取预览图的时候极大概率会和预览线程死锁
@@ -81,7 +87,7 @@ namespace SpineViewer.Spine
// 除此之外, 似乎还和 tex 的 Dispose 有关
// 如果不对 tex 进行 Dispose, 那么不管是否 Draw 都正常不会死锁
var tex = new SFML.Graphics.RenderTexture(PREVIEW_WIDTH, PREVIEW_HEIGHT);
tex.SetView(InitBounds.GetView(PREVIEW_WIDTH, PREVIEW_HEIGHT));
tex.SetView(bounds.GetView(PREVIEW_WIDTH, PREVIEW_HEIGHT));
tex.Clear(SFML.Graphics.Color.Transparent);
tex.Draw(this);
tex.Display();
@@ -98,8 +104,8 @@ namespace SpineViewer.Spine
}
// 取最后一个作为初始, 尽可能去显示非默认的内容
Skin = SkinNames.Last();
Track0Animation = AnimationNames.Last();
skin = SkinNames.Last();
track0Animation = AnimationNames.Last();
return this;
}
@@ -155,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
@@ -171,26 +187,46 @@ 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
@@ -199,6 +235,7 @@ namespace SpineViewer.Spine
/// <summary>
/// 包含的所有皮肤名称
/// </summary>
[Browsable(false)]
public ReadOnlyCollection<string> SkinNames { get; private set; }
protected List<string> skinNames = [];
@@ -207,11 +244,17 @@ namespace SpineViewer.Spine
/// </summary>
[TypeConverter(typeof(SkinConverter))]
[Category("[3] "), DisplayName("")]
public abstract string Skin { get; set; }
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];
@@ -220,7 +263,12 @@ namespace SpineViewer.Spine
/// </summary>
[TypeConverter(typeof(AnimationConverter))]
[Category("[3] "), DisplayName("")]
public abstract string Track0Animation { get; set; }
public string Track0Animation
{
get { lock (_lock) return track0Animation; }
set { lock (_lock) { track0Animation = value; update(0); } }
}
protected abstract string track0Animation { get; set; }
/// <summary>
/// 默认轨道动画时长
@@ -236,25 +284,45 @@ namespace SpineViewer.Spine
/// 显示调试
/// </summary>
[Browsable(false)]
public bool IsDebug { get; set; } = false;
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;
public bool DebugBones
{
get { lock (_lock) return debugBones; }
set { lock (_lock) debugBones = value; }
}
protected bool debugBones = false;
#endregion
@@ -267,19 +335,19 @@ 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; private set; }
public RectangleF Bounds { get { lock (_lock) return bounds; } }
protected abstract RectangleF bounds { get; }
/// <summary>
/// 骨骼预览图
@@ -295,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
@@ -322,7 +391,8 @@ namespace SpineViewer.Spine
/// <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

@@ -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.4</Version>
<Version>0.11.5</Version>
<OutputType>WinExe</OutputType>
<UseWindowsForms>true</UseWindowsForms>
<ApplicationIcon>appicon.ico</ApplicationIcon>

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;
@@ -45,4 +46,55 @@ namespace SpineViewer
return base.ConvertFrom(context, culture, value);
}
}
public class StringEnumConverter : StringConverter
{
/// <summary>
/// 字符串标准值列表属性
/// </summary>
[AttributeUsage(AttributeTargets.Property, AllowMultiple = false, Inherited = false)]
public class StandardValuesAttribute : Attribute
{
/// <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 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;
}
}
}