diff --git a/SpineViewer/Controls/SpineListView.cs b/SpineViewer/Controls/SpineListView.cs
index e7efe21..e5a5659 100644
--- a/SpineViewer/Controls/SpineListView.cs
+++ b/SpineViewer/Controls/SpineListView.cs
@@ -13,11 +13,18 @@ using System.Reflection;
using System.Diagnostics;
using System.Collections.Specialized;
using NLog;
+using SpineViewer.Extensions;
+using SpineViewer.PropertyGridWrappers.Spine;
namespace SpineViewer.Controls
{
public partial class SpineListView : UserControl
{
+ ///
+ /// 日志器
+ ///
+ private readonly Logger logger = LogManager.GetCurrentClassLogger();
+
///
/// Spine 列表只读视图, 访问时必须使用 lock 语句锁定视图本身
///
@@ -29,9 +36,9 @@ namespace SpineViewer.Controls
private readonly List spines = [];
///
- /// 日志器
+ /// 用于属性页显示模型参数的包装类
///
- protected readonly Logger logger = LogManager.GetCurrentClassLogger();
+ private readonly Dictionary spinePropertyWrappers = [];
public SpineListView()
{
@@ -61,8 +68,7 @@ namespace SpineViewer.Controls
private void Insert(int index = -1)
{
var dialog = new Dialogs.OpenSpineDialog();
- if (dialog.ShowDialog() != DialogResult.OK)
- return;
+ if (dialog.ShowDialog() != DialogResult.OK) return;
Insert(dialog.Result, index);
}
@@ -80,12 +86,10 @@ namespace SpineViewer.Controls
index = listView.Items.Count;
// 锁定外部的读操作
- lock (Spines)
- {
- spines.Insert(index, spine);
- listView.SmallImageList.Images.Add(spine.ID, spine.Preview);
- listView.LargeImageList.Images.Add(spine.ID, spine.Preview);
- }
+ lock (Spines) { spines.Insert(index, spine); }
+ spinePropertyWrappers[spine.ID] = new(spine);
+ listView.SmallImageList.Images.Add(spine.ID, spine.Preview);
+ listView.LargeImageList.Images.Add(spine.ID, spine.Preview);
listView.Items.Insert(index, new ListViewItem(spine.Name, spine.ID) { ToolTipText = spine.SkelPath });
// 选中新增项
@@ -108,8 +112,7 @@ namespace SpineViewer.Controls
public void BatchAdd()
{
var openDialog = new Dialogs.BatchOpenSpineDialog();
- if (openDialog.ShowDialog() != DialogResult.OK)
- return;
+ if (openDialog.ShowDialog() != DialogResult.OK) return;
BatchAdd(openDialog.Result);
}
@@ -154,6 +157,7 @@ namespace SpineViewer.Controls
var spine = Spine.Spine.New(version, skelPath);
var preview = spine.Preview;
lock (Spines) { spines.Add(spine); }
+ spinePropertyWrappers[spine.ID] = new(spine);
listView.Invoke(() =>
{
listView.SmallImageList.Images.Add(spine.ID, preview);
@@ -249,9 +253,9 @@ namespace SpineViewer.Controls
if (listView.SelectedIndices.Count <= 0)
PropertyGrid.SelectedObject = null;
else if (listView.SelectedIndices.Count <= 1)
- PropertyGrid.SelectedObject = spines[listView.SelectedIndices[0]];
+ PropertyGrid.SelectedObject = spinePropertyWrappers[spines[listView.SelectedIndices[0]].ID];
else
- PropertyGrid.SelectedObjects = listView.SelectedIndices.Cast().Select(index => spines[index]).ToArray();
+ PropertyGrid.SelectedObjects = listView.SelectedIndices.Cast().Select(index => spinePropertyWrappers[spines[index].ID]).ToArray();
}
// 标记选中的 Spine
@@ -418,6 +422,7 @@ namespace SpineViewer.Controls
listView.Items.RemoveAt(i);
var spine = spines[i];
spines.RemoveAt(i);
+ spinePropertyWrappers.Remove(spine.ID);
listView.SmallImageList.Images.RemoveByKey(spine.ID);
listView.LargeImageList.Images.RemoveByKey(spine.ID);
spine.Dispose();
@@ -513,6 +518,7 @@ namespace SpineViewer.Controls
{
foreach (var spine in spines) spine.Dispose();
spines.Clear();
+ spinePropertyWrappers.Clear();
listView.SmallImageList.Images.Clear();
listView.LargeImageList.Images.Clear();
}
diff --git a/SpineViewer/Controls/SpinePreviewer.cs b/SpineViewer/Controls/SpinePreviewer.cs
index 3e75a92..6b55487 100644
--- a/SpineViewer/Controls/SpinePreviewer.cs
+++ b/SpineViewer/Controls/SpinePreviewer.cs
@@ -15,6 +15,24 @@ namespace SpineViewer.Controls
{
public partial class SpinePreviewer : UserControl
{
+ ///
+ /// 日志器
+ ///
+ private readonly Logger logger = LogManager.GetCurrentClassLogger();
+
+ public SpinePreviewer()
+ {
+ InitializeComponent();
+ RenderWindow = new(panel.Handle);
+ RenderWindow.SetActive(false);
+
+ // 设置默认参数
+ Resolution = new(2048, 2048);
+ Center = new(0, 0);
+ FlipY = true;
+ MaxFps = 30;
+ }
+
///
/// 要绑定的 Spine 列表控件
///
@@ -32,57 +50,12 @@ namespace SpineViewer.Controls
{
propertyGrid = value;
if (propertyGrid is not null)
- propertyGrid.SelectedObject = new PreviewerProperty(this);
+ propertyGrid.SelectedObject = new PropertyGridWrappers.SpinePreviewerWrapper(this);
}
}
private PropertyGrid? propertyGrid;
- #region 画面参数
-
- ///
- /// 画面缩放最大值
- ///
- public const float ZOOM_MAX = 1000f;
-
- ///
- /// 画面缩放最小值
- ///
- public const float ZOOM_MIN = 0.001f;
-
- ///
- /// 包装类, 用于属性面板显示
- ///
- private class PreviewerProperty(SpinePreviewer previewer)
- {
- [TypeConverter(typeof(SizeConverter))]
- [Category("[0] 导出"), DisplayName("分辨率")]
- public Size Resolution { get => previewer.Resolution; set => previewer.Resolution = value; }
-
- [TypeConverter(typeof(PointFConverter))]
- [Category("[0] 导出"), DisplayName("画面中心点")]
- public PointF Center { get => previewer.Center; set => previewer.Center = value; }
-
- [Category("[0] 导出"), DisplayName("缩放")]
- public float Zoom { get => previewer.Zoom; set => previewer.Zoom = value; }
-
- [Category("[0] 导出"), DisplayName("旋转")]
- public float Rotation { get => previewer.Rotation; set => previewer.Rotation = value; }
-
- [Category("[0] 导出"), DisplayName("水平翻转")]
- public bool FlipX { get => previewer.FlipX; set => previewer.FlipX = value; }
-
- [Category("[0] 导出"), DisplayName("垂直翻转")]
- public bool FlipY { get => previewer.FlipY; set => previewer.FlipY = value; }
-
- [Category("[0] 导出"), DisplayName("仅渲染选中")]
- public bool RenderSelectedOnly { get => previewer.RenderSelectedOnly; set => previewer.RenderSelectedOnly = value; }
-
- [Category("[1] 预览"), DisplayName("显示坐标轴")]
- public bool ShowAxis { get => previewer.ShowAxis; set => previewer.ShowAxis = value; }
-
- [Category("[1] 预览"), DisplayName("最大帧率")]
- public uint MaxFps { get => previewer.MaxFps; set => previewer.MaxFps = value; }
- }
+ #region 参数属性
///
/// 分辨率
@@ -166,7 +139,7 @@ namespace SpineViewer.Controls
}
set
{
- value = Math.Clamp(value, ZOOM_MIN, ZOOM_MAX);
+ value = Math.Clamp(value, 0.001f, 1000f);
using var view = RenderWindow.GetView();
var signX = Math.Sign(view.Size.X);
var signY = Math.Sign(view.Size.Y);
@@ -270,70 +243,44 @@ namespace SpineViewer.Controls
#endregion
+ #region 渲染管理
+
///
- /// 日志器
+ /// 预览画面背景色
///
- private Logger logger = LogManager.GetCurrentClassLogger();
+ private static readonly SFML.Graphics.Color BackgroundColor = new(105, 105, 105);
- public SpinePreviewer()
- {
- InitializeComponent();
- RenderWindow = new(panel.Handle);
- RenderWindow.SetActive(false);
+ ///
+ /// 预览画面坐标轴颜色
+ ///
+ private static readonly SFML.Graphics.Color AxisColor = new(220, 220, 220);
- // 设置默认参数
- Resolution = new(2048, 2048);
- Center = new(0, 0);
- FlipY = true;
- MaxFps = 30;
- }
-
- #region 渲染线程管理
+ ///
+ /// 坐标轴顶点缓冲区
+ ///
+ private readonly SFML.Graphics.VertexArray AxisVertex = new(SFML.Graphics.PrimitiveType.Lines, 2);
///
/// 渲染窗口
///
private readonly SFML.Graphics.RenderWindow RenderWindow;
+ ///
+ /// 帧间隔计时器
+ ///
+ private readonly SFML.System.Clock Clock = new();
+
///
/// 渲染任务
///
private Task? task = null;
private CancellationTokenSource? cancelToken = null;
- ///
- /// 开始渲染
- ///
- public void StartRender()
- {
- if (task is not null)
- return;
- cancelToken = new();
- task = Task.Run(RenderTask, cancelToken.Token);
- IsUpdating = true;
- }
-
- ///
- /// 停止渲染
- ///
- public void StopRender()
- {
- IsUpdating = false;
- if (task is null || cancelToken is null)
- return;
- cancelToken.Cancel();
- task.Wait();
- cancelToken = null;
- task = null;
- }
-
- #endregion
-
- #region 渲染更新管理
-
///
/// 是否更新画面
///
+ [DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
+ [Browsable(false)]
public bool IsUpdating
{
get => isUpdating;
@@ -360,24 +307,30 @@ namespace SpineViewer.Controls
private object _forwardDeltaLock = new();
///
- /// 预览画面背景色
+ /// 开始渲染
///
- private static readonly SFML.Graphics.Color BackgroundColor = new(105, 105, 105);
+ public void StartRender()
+ {
+ if (task is not null)
+ return;
+ cancelToken = new();
+ task = Task.Run(RenderTask, cancelToken.Token);
+ IsUpdating = true;
+ }
///
- /// 预览画面坐标轴颜色
+ /// 停止渲染
///
- private static readonly SFML.Graphics.Color AxisColor = new(220, 220, 220);
-
- ///
- /// 坐标轴顶点缓冲区
- ///
- private readonly SFML.Graphics.VertexArray AxisVertex = new(SFML.Graphics.PrimitiveType.Lines, 2);
-
- ///
- /// 帧间隔计时器
- ///
- private readonly SFML.System.Clock Clock = new();
+ public void StopRender()
+ {
+ IsUpdating = false;
+ if (task is null || cancelToken is null)
+ return;
+ cancelToken.Cancel();
+ task.Wait();
+ cancelToken = null;
+ task = null;
+ }
///
/// 渲染任务
diff --git a/SpineViewer/Dialogs/ExportDialog.cs b/SpineViewer/Dialogs/ExportDialog.cs
index 05344f1..6dafa6f 100644
--- a/SpineViewer/Dialogs/ExportDialog.cs
+++ b/SpineViewer/Dialogs/ExportDialog.cs
@@ -1,82 +1,68 @@
-using SpineViewer.Exporter;
+using SpineViewer.PropertyGridWrappers.Exporter;
using System;
using System.Collections;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Diagnostics;
-using System.Drawing;
-using System.Drawing.Design;
-using System.Drawing.Imaging;
using System.Linq;
using System.Reflection;
-using System.Text;
-using System.Threading.Tasks;
-using System.Windows.Forms;
namespace SpineViewer.Dialogs
{
public partial class ExportDialog: Form
{
- ///
- /// 要绑定的导出参数
- ///
- public required ExportArgs ExportArgs
- {
- get => propertyGrid_ExportArgs.SelectedObject as ExportArgs;
- init
- {
- propertyGrid_ExportArgs.SelectedObject = value;
+ private readonly ExporterWrapper wrapper;
- #region XXX: 通过反射默认高亮指定的项
- var categories = propertyGrid_ExportArgs.SelectedGridItem?.Parent?.Parent?.GridItems;
- if (categories is null) return;
-
- foreach (var category in categories)
- {
- // 查找 "导出" 分组
- if (category == null) continue;
- PropertyInfo? labelProp = category.GetType().GetProperty("Label", BindingFlags.Instance | BindingFlags.Public);
- if (labelProp == null) continue;
- string? label = labelProp.GetValue(category) as string;
- if (label != "[0] 导出") continue;
-
- // 获取该分组下的所有属性项
- PropertyInfo? gridItemsProp = category.GetType().GetProperty("GridItems", BindingFlags.Instance | BindingFlags.Public);
- if (gridItemsProp == null) continue;
- var gridItemsObj = gridItemsProp.GetValue(category);
- if (gridItemsObj is not IEnumerable gridItems) continue;
-
- foreach (object item in gridItems)
- {
- if (item == null) continue;
- PropertyInfo? propDescProp = item.GetType().GetProperty("PropertyDescriptor", BindingFlags.Instance | BindingFlags.Public);
- if (propDescProp == null) continue;
- var propDesc = propDescProp.GetValue(item) as PropertyDescriptor;
- if (propDesc == null) continue;
- if (propDesc.Name == "OutputDir")
- {
-
- if (item is GridItem gridItem)
- propertyGrid_ExportArgs.SelectedGridItem = gridItem; // 找到后,将此项设为选中项
- else
- propertyGrid_ExportArgs.SelectedGridItem = (GridItem)item; // 如果转换失败,则尝试直接赋值
- }
- return; // 设置成功后退出
- }
- }
- #endregion
- }
- }
-
- public ExportDialog()
+ public ExportDialog(ExporterWrapper wrapper)
{
InitializeComponent();
+ this.wrapper = wrapper;
+ propertyGrid_ExportArgs.SelectedObject = wrapper;
+
+ #region XXX: 通过反射默认高亮指定的项
+ var categories = propertyGrid_ExportArgs.SelectedGridItem?.Parent?.Parent?.GridItems;
+ if (categories is null) return;
+
+ foreach (var category in categories)
+ {
+ // 查找 "导出" 分组
+ if (category == null) continue;
+ PropertyInfo? labelProp = category.GetType().GetProperty("Label", BindingFlags.Instance | BindingFlags.Public);
+ if (labelProp == null) continue;
+ string? label = labelProp.GetValue(category) as string;
+ if (label != "[0] 导出") continue;
+
+ // 获取该分组下的所有属性项
+ PropertyInfo? gridItemsProp = category.GetType().GetProperty("GridItems", BindingFlags.Instance | BindingFlags.Public);
+ if (gridItemsProp == null) continue;
+ var gridItemsObj = gridItemsProp.GetValue(category);
+ if (gridItemsObj is not IEnumerable gridItems) continue;
+
+ foreach (object item in gridItems)
+ {
+ if (item == null) continue;
+ PropertyInfo? propDescProp = item.GetType().GetProperty("PropertyDescriptor", BindingFlags.Instance | BindingFlags.Public);
+ if (propDescProp == null) continue;
+ var propDesc = propDescProp.GetValue(item) as PropertyDescriptor;
+ if (propDesc == null) continue;
+ if (propDesc.Name == "OutputDir")
+ {
+
+ if (item is GridItem gridItem)
+ propertyGrid_ExportArgs.SelectedGridItem = gridItem; // 找到后,将此项设为选中项
+ else
+ propertyGrid_ExportArgs.SelectedGridItem = (GridItem)item; // 如果转换失败,则尝试直接赋值
+ }
+ return; // 设置成功后退出
+ }
+ }
+ #endregion
}
private void button_Ok_Click(object sender, EventArgs e)
{
- if (ExportArgs.Validate() is string error)
+ if (wrapper.Exporter.Validate() is string error)
{
MessageBox.Info(error, "参数错误");
return;
diff --git a/SpineViewer/Dialogs/AnimationTracksEditorDialog.Designer.cs b/SpineViewer/Dialogs/SpineAnimationEditorDialog.Designer.cs
similarity index 99%
rename from SpineViewer/Dialogs/AnimationTracksEditorDialog.Designer.cs
rename to SpineViewer/Dialogs/SpineAnimationEditorDialog.Designer.cs
index f019e91..99ea2e2 100644
--- a/SpineViewer/Dialogs/AnimationTracksEditorDialog.Designer.cs
+++ b/SpineViewer/Dialogs/SpineAnimationEditorDialog.Designer.cs
@@ -1,6 +1,6 @@
namespace SpineViewer.Dialogs
{
- partial class AnimationTracksEditorDialog
+ partial class SpineAnimationEditorDialog
{
///
/// Required designer variable.
diff --git a/SpineViewer/Dialogs/AnimationTracksEditorDialog.cs b/SpineViewer/Dialogs/SpineAnimationEditorDialog.cs
similarity index 82%
rename from SpineViewer/Dialogs/AnimationTracksEditorDialog.cs
rename to SpineViewer/Dialogs/SpineAnimationEditorDialog.cs
index a4f66f0..64cba30 100644
--- a/SpineViewer/Dialogs/AnimationTracksEditorDialog.cs
+++ b/SpineViewer/Dialogs/SpineAnimationEditorDialog.cs
@@ -1,4 +1,5 @@
-using SpineViewer.Spine;
+using SpineViewer.PropertyGridWrappers.Spine;
+using SpineViewer.Spine;
using System;
using System.Collections.Generic;
using System.ComponentModel;
@@ -11,14 +12,14 @@ using System.Windows.Forms;
namespace SpineViewer.Dialogs
{
- public partial class AnimationTracksEditorDialog : Form
+ public partial class SpineAnimationEditorDialog : Form
{
private readonly Spine.Spine spine;
- public AnimationTracksEditorDialog(Spine.Spine spine)
+ public SpineAnimationEditorDialog(Spine.Spine spine)
{
InitializeComponent();
this.spine = spine;
- propertyGrid_AnimationTracks.SelectedObject = spine.AnimationTracks;
+ propertyGrid_AnimationTracks.SelectedObject = new SpineAnimationWrapper(spine);
}
private void button_Add_Click(object sender, EventArgs e)
diff --git a/SpineViewer/Dialogs/AnimationTracksEditorDialog.resx b/SpineViewer/Dialogs/SpineAnimationEditorDialog.resx
similarity index 100%
rename from SpineViewer/Dialogs/AnimationTracksEditorDialog.resx
rename to SpineViewer/Dialogs/SpineAnimationEditorDialog.resx
diff --git a/SpineViewer/Dialogs/SkinManagerEditorDialog.Designer.cs b/SpineViewer/Dialogs/SpineSkinEditorDialog.Designer.cs
similarity index 99%
rename from SpineViewer/Dialogs/SkinManagerEditorDialog.Designer.cs
rename to SpineViewer/Dialogs/SpineSkinEditorDialog.Designer.cs
index 1f9f1c6..2bb054e 100644
--- a/SpineViewer/Dialogs/SkinManagerEditorDialog.Designer.cs
+++ b/SpineViewer/Dialogs/SpineSkinEditorDialog.Designer.cs
@@ -1,6 +1,6 @@
namespace SpineViewer.Dialogs
{
- partial class SkinManagerEditorDialog
+ partial class SpineSkinEditorDialog
{
///
/// Required designer variable.
diff --git a/SpineViewer/Dialogs/SkinManagerEditorDialog.cs b/SpineViewer/Dialogs/SpineSkinEditorDialog.cs
similarity index 81%
rename from SpineViewer/Dialogs/SkinManagerEditorDialog.cs
rename to SpineViewer/Dialogs/SpineSkinEditorDialog.cs
index 66bf86b..fd84e59 100644
--- a/SpineViewer/Dialogs/SkinManagerEditorDialog.cs
+++ b/SpineViewer/Dialogs/SpineSkinEditorDialog.cs
@@ -1,4 +1,5 @@
-using SpineViewer.Spine;
+using SpineViewer.PropertyGridWrappers.Spine;
+using SpineViewer.Spine;
using System;
using System.Collections.Generic;
using System.ComponentModel;
@@ -11,14 +12,14 @@ using System.Windows.Forms;
namespace SpineViewer.Dialogs
{
- public partial class SkinManagerEditorDialog : Form
+ public partial class SpineSkinEditorDialog : Form
{
private readonly Spine.Spine spine;
- public SkinManagerEditorDialog(Spine.Spine spine)
+ public SpineSkinEditorDialog(Spine.Spine spine)
{
InitializeComponent();
this.spine = spine;
- propertyGrid_SkinManager.SelectedObject = spine.SkinManager;
+ propertyGrid_SkinManager.SelectedObject = new SpineSkinWrapper(spine); // TODO: 去掉对话框
}
private void button_Add_Click(object sender, EventArgs e)
diff --git a/SpineViewer/Dialogs/SkinManagerEditorDialog.resx b/SpineViewer/Dialogs/SpineSkinEditorDialog.resx
similarity index 100%
rename from SpineViewer/Dialogs/SkinManagerEditorDialog.resx
rename to SpineViewer/Dialogs/SpineSkinEditorDialog.resx
diff --git a/SpineViewer/Exporter/Implementations/ExportArgs/CustomExportArgs.cs b/SpineViewer/Exporter/CustomExporter.cs
similarity index 57%
rename from SpineViewer/Exporter/Implementations/ExportArgs/CustomExportArgs.cs
rename to SpineViewer/Exporter/CustomExporter.cs
index 047ab89..d270b84 100644
--- a/SpineViewer/Exporter/Implementations/ExportArgs/CustomExportArgs.cs
+++ b/SpineViewer/Exporter/CustomExporter.cs
@@ -5,16 +5,13 @@ using System.Linq;
using System.Text;
using System.Threading.Tasks;
-namespace SpineViewer.Exporter.Implementations.ExportArgs
+namespace SpineViewer.Exporter
{
///
/// FFmpeg 自定义视频导出参数
///
- [ExportImplementation(ExportType.Custom)]
- public class CustomExportArgs : FFmpegVideoExportArgs
+ public class CustomExporter : FFmpegVideoExporter
{
- public CustomExportArgs(Size resolution, SFML.Graphics.View view, bool renderSelectedOnly) : base(resolution, view, renderSelectedOnly) { }
-
public override string Format => CustomFormat;
public override string Suffix => CustomSuffix;
@@ -24,13 +21,11 @@ namespace SpineViewer.Exporter.Implementations.ExportArgs
///
/// 文件格式
///
- [Category("[3] 自定义参数"), DisplayName("文件格式"), Description("文件格式")]
public string CustomFormat { get; set; } = "mp4";
///
/// 文件名后缀
///
- [Category("[3] 自定义参数"), DisplayName("文件名后缀"), Description("文件名后缀")]
public string CustomSuffix { get; set; } = ".mp4";
}
}
diff --git a/SpineViewer/Exporter/ExportArgs.cs b/SpineViewer/Exporter/ExportArgs.cs
deleted file mode 100644
index 21f203f..0000000
--- a/SpineViewer/Exporter/ExportArgs.cs
+++ /dev/null
@@ -1,118 +0,0 @@
-using SpineViewer.Spine;
-using System;
-using System.Collections.Generic;
-using System.ComponentModel;
-using System.Drawing.Design;
-using System.Drawing.Imaging;
-using System.Linq;
-using System.Reflection;
-using System.Text;
-using System.Threading.Tasks;
-using static System.Windows.Forms.VisualStyles.VisualStyleElement.StartPanel;
-
-namespace SpineViewer.Exporter
-{
- ///
- /// 导出参数基类
- ///
- public abstract class ExportArgs : ImplementationResolver, IDisposable
- {
- ///
- /// 创建指定类型导出参数
- ///
- /// 导出类型
- /// 分辨率
- /// 导出视图
- /// 仅渲染选中
- /// 返回与指定 匹配的导出参数实例
- public static ExportArgs New(ExportType exportType, Size resolution, SFML.Graphics.View view, bool renderSelectedOnly)
- => New(exportType, [resolution, view, renderSelectedOnly]);
-
- public ExportArgs(Size resolution, SFML.Graphics.View view, bool renderSelectedOnly)
- {
- Resolution = resolution;
- View = view;
- RenderSelectedOnly = renderSelectedOnly;
- }
-
- ~ExportArgs() { Dispose(false); }
- public void Dispose() { Dispose(true); GC.SuppressFinalize(this); }
- protected virtual void Dispose(bool disposing) { View?.Dispose(); }
-
- ///
- /// 输出文件夹
- ///
- [Editor(typeof(FolderNameEditor), typeof(UITypeEditor))]
- [Category("[0] 导出"), DisplayName("输出文件夹"), Description("逐个导出时可以留空,将逐个导出到模型自身所在目录")]
- public string? OutputDir { get; set; } = null;
-
- ///
- /// 导出单个
- ///
- [Category("[0] 导出"), DisplayName("导出单个"), Description("是否将模型在同一个画面上导出单个文件,否则逐个导出模型")]
- public bool ExportSingle { get; set; } = false;
-
- ///
- /// 画面分辨率
- ///
- [TypeConverter(typeof(SizeConverter))]
- [Category("[0] 导出"), DisplayName("分辨率"), Description("画面的宽高像素大小,请在预览画面参数面板进行调整")]
- public Size Resolution { get; }
-
- ///
- /// 渲染视窗
- ///
- [Category("[0] 导出"), DisplayName("视图"), Description("画面的视图参数,请在预览画面参数面板进行调整")]
- public SFML.Graphics.View View { get; }
-
- ///
- /// 是否仅渲染选中
- ///
- [Category("[0] 导出"), DisplayName("仅渲染选中"), Description("是否仅导出选中的模型,请在预览画面参数面板进行调整")]
- public bool RenderSelectedOnly { get; }
-
- ///
- /// 背景颜色
- ///
- [Editor(typeof(SFMLColorEditor), typeof(UITypeEditor))]
- [TypeConverter(typeof(SFMLColorConverter))]
- [Category("[0] 导出"), DisplayName("背景颜色"), Description("要使用的背景色, 格式为 #RRGGBBAA")]
- public SFML.Graphics.Color BackgroundColor
- {
- get => backgroundColor;
- set
- {
- backgroundColor = value;
- var bcPma = value;
- var a = bcPma.A / 255f;
- bcPma.R = (byte)(bcPma.R * a);
- bcPma.G = (byte)(bcPma.G * a);
- bcPma.B = (byte)(bcPma.B * a);
- BackgroundColorPma = bcPma;
- }
- }
- private SFML.Graphics.Color backgroundColor = SFML.Graphics.Color.Transparent;
-
- ///
- /// 预乘后的背景颜色
- ///
- [Browsable(false)]
- public SFML.Graphics.Color BackgroundColorPma { get; private set; } = SFML.Graphics.Color.Transparent;
-
- ///
- /// 检查参数是否合法并规范化参数值, 否则返回用户错误原因
- ///
- public virtual string? Validate()
- {
- if (!string.IsNullOrWhiteSpace(OutputDir) && File.Exists(OutputDir))
- return "输出文件夹无效";
- if (!string.IsNullOrWhiteSpace(OutputDir) && !Directory.Exists(OutputDir))
- return $"文件夹 {OutputDir} 不存在";
- if (ExportSingle && string.IsNullOrWhiteSpace(OutputDir))
- return "导出单个时必须提供输出文件夹";
-
- OutputDir = string.IsNullOrWhiteSpace(OutputDir) ? null : Path.GetFullPath(OutputDir);
- return null;
- }
- }
-}
diff --git a/SpineViewer/Exporter/ExportHelper.cs b/SpineViewer/Exporter/ExportHelper.cs
index 2e13116..c864479 100644
--- a/SpineViewer/Exporter/ExportHelper.cs
+++ b/SpineViewer/Exporter/ExportHelper.cs
@@ -1,37 +1,13 @@
using FFMpegCore.Pipes;
+using SpineViewer.Extensions;
using System;
using System.Collections.Generic;
using System.Drawing.Imaging;
using System.Linq;
using System.Text;
-using System.Threading.Tasks;
namespace SpineViewer.Exporter
{
- ///
- /// 导出类型
- ///
- public enum ExportType
- {
- Frame,
- FrameSequence,
- Gif,
- Mp4,
- Webm,
- Mkv,
- Mov,
- Custom,
- }
-
- ///
- /// 导出实现类标记
- ///
- [AttributeUsage(AttributeTargets.Class, AllowMultiple = true, Inherited = false)]
- public class ExportImplementationAttribute(ExportType exportType) : Attribute, IImplementationKey
- {
- public ExportType ImplementationKey { get; private set; } = exportType;
- }
-
///
/// SFML.Graphics.Image 帧对象包装类, 将接管给定的 image 对象生命周期
///
diff --git a/SpineViewer/Exporter/Exporter.cs b/SpineViewer/Exporter/Exporter.cs
index 11171db..f5fc7ea 100644
--- a/SpineViewer/Exporter/Exporter.cs
+++ b/SpineViewer/Exporter/Exporter.cs
@@ -1,7 +1,10 @@
using NLog;
+using SpineViewer.Extensions;
+using SpineViewer.PropertyGridWrappers;
using System;
using System.Collections.Generic;
using System.ComponentModel;
+using System.Drawing.Design;
using System.Linq;
using System.Reflection;
using System.Text;
@@ -12,44 +15,80 @@ namespace SpineViewer.Exporter
///
/// 导出器基类
///
- public abstract class Exporter(ExportArgs exportArgs) : ImplementationResolver
+ public abstract class Exporter : IDisposable
{
- ///
- /// 仅源像素混合模式
- ///
- private static readonly SFML.Graphics.BlendMode SrcOnlyBlendMode = new(SFML.Graphics.BlendMode.Factor.One, SFML.Graphics.BlendMode.Factor.Zero);
-
- ///
- /// 创建指定类型导出器
- ///
- /// 导出类型
- /// 与 匹配的导出参数
- /// 与 匹配的导出器
- public static Exporter New(ExportType exportType, ExportArgs exportArgs) => New(exportType, [exportArgs]);
-
///
/// 日志器
///
- protected Logger logger = LogManager.GetCurrentClassLogger();
-
- ///
- /// 导出参数
- ///
- public ExportArgs ExportArgs { get; } = exportArgs;
+ protected readonly Logger logger = LogManager.GetCurrentClassLogger();
///
/// 可用于文件名的时间戳字符串
///
protected readonly string timestamp = DateTime.Now.ToString("yyMMddHHmmss");
+ ~Exporter() { Dispose(false); }
+ public void Dispose() { Dispose(true); GC.SuppressFinalize(this); }
+ protected virtual void Dispose(bool disposing) { View.Dispose(); }
+
+ ///
+ /// 输出文件夹
+ ///
+ public string? OutputDir { get; set; } = null;
+
+ ///
+ /// 导出单个
+ ///
+ public bool IsExportSingle { get; set; } = false;
+
+ ///
+ /// 画面分辨率
+ ///
+ public Size Resolution { get; set; } = new(100, 100);
+
+ ///
+ /// 渲染视窗, 接管对象生命周期
+ ///
+ public SFML.Graphics.View View { get => view; set { view.Dispose(); view = value; } }
+ private SFML.Graphics.View view = new();
+
+ ///
+ /// 是否仅渲染选中
+ ///
+ public bool RenderSelectedOnly { get; set; } = false;
+
+ ///
+ /// 背景颜色
+ ///
+ public SFML.Graphics.Color BackgroundColor
+ {
+ get => backgroundColor;
+ set
+ {
+ backgroundColor = value;
+ var bcPma = value;
+ var a = bcPma.A / 255f;
+ bcPma.R = (byte)(bcPma.R * a);
+ bcPma.G = (byte)(bcPma.G * a);
+ bcPma.B = (byte)(bcPma.B * a);
+ BackgroundColorPma = bcPma;
+ }
+ }
+ private SFML.Graphics.Color backgroundColor = SFML.Graphics.Color.Transparent;
+
+ ///
+ /// 预乘后的背景颜色
+ ///
+ public SFML.Graphics.Color BackgroundColorPma { get; private set; } = SFML.Graphics.Color.Transparent;
+
///
/// 获取供渲染的 SFML.Graphics.RenderTexture
///
private SFML.Graphics.RenderTexture GetRenderTexture()
{
- var tex = new SFML.Graphics.RenderTexture((uint)ExportArgs.Resolution.Width, (uint)ExportArgs.Resolution.Height);
+ var tex = new SFML.Graphics.RenderTexture((uint)Resolution.Width, (uint)Resolution.Height);
tex.Clear(SFML.Graphics.Color.Transparent);
- tex.SetView(ExportArgs.View);
+ tex.SetView(View);
return tex;
}
@@ -67,12 +106,12 @@ namespace SpineViewer.Exporter
using var texPma = GetRenderTexture();
// 先将预乘结果准确绘制出来, 注意背景色也应当是预乘的
- texPma.Clear(ExportArgs.BackgroundColorPma);
+ texPma.Clear(BackgroundColorPma);
foreach (var spine in spinesToRender) texPma.Draw(spine);
texPma.Display();
// 背景色透明度不为 1 时需要处理反预乘, 否则直接就是结果
- if (ExportArgs.BackgroundColor.A < 255)
+ if (BackgroundColor.A < 255)
{
// 从预乘结果构造渲染对象, 并正确设置变换
using var view = texPma.GetView();
@@ -88,14 +127,14 @@ namespace SpineViewer.Exporter
// 混合模式用直接覆盖的方式, 保证得到的图像区域是反预乘的颜色和透明度, 同时使用反预乘着色器
var st = SFML.Graphics.RenderStates.Default;
- st.BlendMode = SrcOnlyBlendMode; // 用源的颜色和透明度直接覆盖
- st.Shader = Shader.InversePma;
+ st.BlendMode = SFMLBlendMode.SourceOnly;
+ st.Shader = SFMLShader.InversePma;
// 在最终结果上二次渲染非预乘画面
using var tex = GetRenderTexture();
// 将非预乘结果覆盖式绘制在目标对象上, 注意背景色应该用非预乘的
- tex.Clear(ExportArgs.BackgroundColor);
+ tex.Clear(BackgroundColor);
tex.Draw(sp, st);
tex.Display();
return new(tex.Texture.CopyToImage());
@@ -116,16 +155,36 @@ namespace SpineViewer.Exporter
///
protected abstract void ExportIndividual(Spine.Spine[] spinesToRender, BackgroundWorker? worker = null);
+ ///
+ /// 检查参数是否合法并规范化参数值, 否则返回用户错误原因
+ ///
+ public virtual string? Validate()
+ {
+ if (!string.IsNullOrWhiteSpace(OutputDir) && File.Exists(OutputDir))
+ return "输出文件夹无效";
+ if (!string.IsNullOrWhiteSpace(OutputDir) && !Directory.Exists(OutputDir))
+ return $"文件夹 {OutputDir} 不存在";
+ if (IsExportSingle && string.IsNullOrWhiteSpace(OutputDir))
+ return "导出单个时必须提供输出文件夹";
+
+ OutputDir = string.IsNullOrWhiteSpace(OutputDir) ? null : Path.GetFullPath(OutputDir);
+ return null;
+ }
+
///
/// 执行导出
///
/// 要进行导出的 Spine 列表
/// 用来执行该函数的 worker
+ ///
public virtual void Export(Spine.Spine[] spines, BackgroundWorker? worker = null)
{
- var spinesToRender = spines.Where(sp => !ExportArgs.RenderSelectedOnly || sp.IsSelected).Reverse().ToArray();
+ if (Validate() is string err)
+ throw new ArgumentException(err);
- if (ExportArgs.ExportSingle) ExportSingle(spinesToRender, worker);
+ var spinesToRender = spines.Where(sp => !RenderSelectedOnly || sp.IsSelected).Reverse().ToArray();
+
+ if (IsExportSingle) ExportSingle(spinesToRender, worker);
else ExportIndividual(spinesToRender, worker);
logger.LogCurrentProcessMemoryUsage();
diff --git a/SpineViewer/Exporter/Implementations/Exporter/FFmpegVideoExporter.cs b/SpineViewer/Exporter/FFmpegVideoExporter.cs
similarity index 50%
rename from SpineViewer/Exporter/Implementations/Exporter/FFmpegVideoExporter.cs
rename to SpineViewer/Exporter/FFmpegVideoExporter.cs
index 7d8b420..eab9375 100644
--- a/SpineViewer/Exporter/Implementations/Exporter/FFmpegVideoExporter.cs
+++ b/SpineViewer/Exporter/FFmpegVideoExporter.cs
@@ -1,6 +1,5 @@
using FFMpegCore.Pipes;
using FFMpegCore;
-using SpineViewer.Exporter.Implementations.ExportArgs;
using System;
using System.Collections.Generic;
using System.ComponentModel;
@@ -9,36 +8,63 @@ using System.Text;
using System.Threading.Tasks;
using System.Diagnostics;
-namespace SpineViewer.Exporter.Implementations.Exporter
+namespace SpineViewer.Exporter
{
///
/// 使用 FFmpeg 的视频导出器
///
- [ExportImplementation(ExportType.Gif)]
- [ExportImplementation(ExportType.Mp4)]
- [ExportImplementation(ExportType.Webm)]
- [ExportImplementation(ExportType.Mkv)]
- [ExportImplementation(ExportType.Mov)]
- [ExportImplementation(ExportType.Custom)]
- public class FFmpegVideoExporter : VideoExporter
+ public abstract class FFmpegVideoExporter : VideoExporter
{
- public FFmpegVideoExporter(FFmpegVideoExportArgs exportArgs) : base(exportArgs) { }
+ ///
+ /// 文件格式
+ ///
+ public abstract string Format { get; }
+
+ ///
+ /// 文件名后缀
+ ///
+ public abstract string Suffix { get; }
+
+ ///
+ /// 文件名后缀
+ ///
+ public string CustomArgument { get; set; }
+
+ ///
+ /// 要追加在文件名末尾的信息字串, 首尾不需要提供额外分隔符
+ ///
+ public abstract string FileNameNoteSuffix { get; }
+
+ ///
+ /// 获取输出附加选项
+ ///
+ public virtual void SetOutputOptions(FFMpegArgumentOptions options) => options.ForceFormat(Format).WithCustomArgument(CustomArgument);
+
+ public override string? Validate()
+ {
+ if (base.Validate() is string error)
+ return error;
+ if (string.IsNullOrWhiteSpace(Format))
+ return "需要提供有效的格式";
+ if (string.IsNullOrWhiteSpace(Suffix))
+ return "需要提供有效的文件名后缀";
+ return null;
+ }
protected override void ExportSingle(Spine.Spine[] spinesToRender, BackgroundWorker? worker = null)
{
- var args = (FFmpegVideoExportArgs)ExportArgs;
- var noteSuffix = args.FileNameNoteSuffix;
+ var noteSuffix = FileNameNoteSuffix;
if (!string.IsNullOrWhiteSpace(noteSuffix)) noteSuffix = $"_{noteSuffix}";
- var filename = $"{timestamp}_{args.FPS:f0}{noteSuffix}{args.Suffix}";
+ var filename = $"ffmpeg_{timestamp}_{FPS:f0}{noteSuffix}{Suffix}";
// 导出单个时必定提供输出文件夹
- var savePath = Path.Combine(args.OutputDir, filename);
+ var savePath = Path.Combine(OutputDir, filename);
- var videoFramesSource = new RawVideoPipeSource(GetFrames(spinesToRender, worker)) { FrameRate = args.FPS };
+ var videoFramesSource = new RawVideoPipeSource(GetFrames(spinesToRender, worker)) { FrameRate = FPS };
try
{
- var ffmpegArgs = FFMpegArguments.FromPipeInput(videoFramesSource).OutputToFile(savePath, true, args.SetOutputOptions);
+ var ffmpegArgs = FFMpegArguments.FromPipeInput(videoFramesSource).OutputToFile(savePath, true, SetOutputOptions);
logger.Info("FFmpeg arguments: {}", ffmpegArgs.Arguments);
ffmpegArgs.ProcessSynchronously();
@@ -46,31 +72,30 @@ namespace SpineViewer.Exporter.Implementations.Exporter
catch (Exception ex)
{
logger.Error(ex.ToString());
- logger.Error("Failed to export {} {}", args.Format, savePath);
+ logger.Error("Failed to export {} {}", Format, savePath);
}
}
protected override void ExportIndividual(Spine.Spine[] spinesToRender, BackgroundWorker? worker = null)
{
- var args = (FFmpegVideoExportArgs)ExportArgs;
- var noteSuffix = args.FileNameNoteSuffix;
+ var noteSuffix = FileNameNoteSuffix;
if (!string.IsNullOrWhiteSpace(noteSuffix)) noteSuffix = $"_{noteSuffix}";
foreach (var spine in spinesToRender)
{
if (worker?.CancellationPending == true) break; // 取消的日志在 GetFrames 里输出
- var filename = $"{spine.Name}_{timestamp}_{args.FPS:f0}{noteSuffix}{args.Suffix}";
+ var filename = $"{spine.Name}_{timestamp}_{FPS:f0}{noteSuffix}{Suffix}";
// 如果提供了输出文件夹, 则全部导出到输出文件夹, 否则导出到各自的文件夹下
- var savePath = Path.Combine(args.OutputDir ?? spine.AssetsDir, filename);
+ var savePath = Path.Combine(OutputDir ?? spine.AssetsDir, filename);
- var videoFramesSource = new RawVideoPipeSource(GetFrames(spine, worker)) { FrameRate = args.FPS };
+ var videoFramesSource = new RawVideoPipeSource(GetFrames(spine, worker)) { FrameRate = FPS };
try
{
var ffmpegArgs = FFMpegArguments
.FromPipeInput(videoFramesSource)
- .OutputToFile(savePath, true, args.SetOutputOptions);
+ .OutputToFile(savePath, true, SetOutputOptions);
logger.Info("FFmpeg arguments: {}", ffmpegArgs.Arguments);
ffmpegArgs.ProcessSynchronously();
@@ -78,7 +103,7 @@ namespace SpineViewer.Exporter.Implementations.Exporter
catch (Exception ex)
{
logger.Error(ex.ToString());
- logger.Error("Failed to export {} {} {}", args.Format, savePath, spine.SkelPath);
+ logger.Error("Failed to export {} {} {}", Format, savePath, spine.SkelPath);
}
}
}
diff --git a/SpineViewer/Exporter/Implementations/Exporter/FrameExporter.cs b/SpineViewer/Exporter/FrameExporter.cs
similarity index 57%
rename from SpineViewer/Exporter/Implementations/Exporter/FrameExporter.cs
rename to SpineViewer/Exporter/FrameExporter.cs
index c0ed723..5080068 100644
--- a/SpineViewer/Exporter/Implementations/Exporter/FrameExporter.cs
+++ b/SpineViewer/Exporter/FrameExporter.cs
@@ -1,37 +1,61 @@
-using SpineViewer.Exporter.Implementations.ExportArgs;
-using SpineViewer.Spine;
+using SpineViewer.Spine;
using System;
using System.Collections.Generic;
using System.ComponentModel;
+using System.Drawing.Imaging;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
-namespace SpineViewer.Exporter.Implementations.Exporter
+namespace SpineViewer.Exporter
{
///
/// 单帧画面导出器
///
- [ExportImplementation(ExportType.Frame)]
- public class FrameExporter : SpineViewer.Exporter.Exporter
+ public class FrameExporter : Exporter
{
- public FrameExporter(FrameExportArgs exportArgs) : base(exportArgs) { }
+ ///
+ /// 单帧画面格式
+ ///
+ public ImageFormat ImageFormat
+ {
+ get => imageFormat;
+ set
+ {
+ if (value == ImageFormat.MemoryBmp) value = ImageFormat.Bmp;
+ imageFormat = value;
+ }
+ }
+ private ImageFormat imageFormat = ImageFormat.Png;
+
+ ///
+ /// DPI
+ ///
+ public SizeF DPI
+ {
+ get => dpi;
+ set
+ {
+ if (value.Width <= 0) value.Width = 144;
+ if (value.Height <= 0) value.Height = 144;
+ dpi = value;
+ }
+ }
+ private SizeF dpi = new(144, 144);
protected override void ExportSingle(Spine.Spine[] spinesToRender, BackgroundWorker? worker = null)
{
- var args = (FrameExportArgs)ExportArgs;
-
// 导出单个时必定提供输出文件夹
- var filename = $"frame_{timestamp}{args.Suffix}";
- var savePath = Path.Combine(args.OutputDir, filename);
+ var filename = $"frame_{timestamp}{ImageFormat.GetSuffix()}";
+ var savePath = Path.Combine(OutputDir, filename);
worker?.ReportProgress(0, $"已处理 0/1");
try
{
using var frame = GetFrame(spinesToRender);
using var img = frame.CopyToBitmap();
- img.SetResolution(args.DPI.Width, args.DPI.Height);
- img.Save(savePath, args.ImageFormat);
+ img.SetResolution(DPI.Width, DPI.Height);
+ img.Save(savePath, ImageFormat);
}
catch (Exception ex)
{
@@ -43,8 +67,6 @@ namespace SpineViewer.Exporter.Implementations.Exporter
protected override void ExportIndividual(Spine.Spine[] spinesToRender, BackgroundWorker? worker = null)
{
- var args = (FrameExportArgs)ExportArgs;
-
int total = spinesToRender.Length;
int success = 0;
int error = 0;
@@ -55,21 +77,21 @@ namespace SpineViewer.Exporter.Implementations.Exporter
var spine = spinesToRender[i];
// 逐个导出时如果提供了输出文件夹, 则全部导出到输出文件夹, 否则输出到各自的文件夹
- var filename = $"{spine.Name}_{timestamp}{args.Suffix}";
- var savePath = args.OutputDir is null ? Path.Combine(spine.AssetsDir, filename) : Path.Combine(args.OutputDir, filename);
+ var filename = $"{spine.Name}_{timestamp}{ImageFormat.GetSuffix()}";
+ var savePath = Path.Combine(OutputDir ?? spine.AssetsDir, filename);
try
{
using var frame = GetFrame(spine);
using var img = frame.CopyToBitmap();
- img.SetResolution(args.DPI.Width, args.DPI.Height);
- img.Save(savePath, args.ImageFormat);
+ img.SetResolution(DPI.Width, DPI.Height);
+ img.Save(savePath, ImageFormat);
success++;
}
catch (Exception ex)
{
logger.Error(ex.ToString());
- logger.Error("Failed to save frame {} {}", savePath, spine.SkelPath);
+ logger.Error("Failed to save single frame {} {}", savePath, spine.SkelPath);
error++;
}
@@ -82,5 +104,4 @@ namespace SpineViewer.Exporter.Implementations.Exporter
logger.Info("{} frames saved successfully", success);
}
}
-
}
diff --git a/SpineViewer/Exporter/Implementations/Exporter/FrameSequenceExporter.cs b/SpineViewer/Exporter/FrameSequenceExporter.cs
similarity index 71%
rename from SpineViewer/Exporter/Implementations/Exporter/FrameSequenceExporter.cs
rename to SpineViewer/Exporter/FrameSequenceExporter.cs
index 8609672..bdfac0a 100644
--- a/SpineViewer/Exporter/Implementations/Exporter/FrameSequenceExporter.cs
+++ b/SpineViewer/Exporter/FrameSequenceExporter.cs
@@ -1,5 +1,4 @@
-using SpineViewer.Exporter.Implementations.ExportArgs;
-using SpineViewer.Spine;
+using SpineViewer.Spine;
using System;
using System.Collections.Generic;
using System.ComponentModel;
@@ -7,28 +6,28 @@ using System.Linq;
using System.Text;
using System.Threading.Tasks;
-namespace SpineViewer.Exporter.Implementations.Exporter
+namespace SpineViewer.Exporter
{
///
/// 帧序列导出器
///
- [ExportImplementation(ExportType.FrameSequence)]
public class FrameSequenceExporter : VideoExporter
{
- public FrameSequenceExporter(FrameSequenceExportArgs exportArgs) : base(exportArgs) { }
+ ///
+ /// 文件名后缀, 同时决定帧图像格式, 支持的格式为 ".png", ".jpg", ".tga", ".bmp"
+ ///
+ public string Suffix { get; set; } = ".png";
protected override void ExportSingle(Spine.Spine[] spinesToRender, BackgroundWorker? worker = null)
{
- var args = (FrameSequenceExportArgs)ExportArgs;
-
// 导出单个时必定提供输出文件夹,
- var saveDir = Path.Combine(args.OutputDir, $"frames_{timestamp}_{args.FPS:f0}");
+ var saveDir = Path.Combine(OutputDir, $"frames_{timestamp}_{FPS:f0}");
Directory.CreateDirectory(saveDir);
int frameIdx = 0;
foreach (var frame in GetFrames(spinesToRender, worker))
{
- var filename = $"frames_{timestamp}_{args.FPS:f0}_{frameIdx:d6}{args.Suffix}";
+ var filename = $"frames_{timestamp}_{FPS:f0}_{frameIdx:d6}{Suffix}";
var savePath = Path.Combine(saveDir, filename);
try
@@ -50,20 +49,19 @@ namespace SpineViewer.Exporter.Implementations.Exporter
protected override void ExportIndividual(Spine.Spine[] spinesToRender, BackgroundWorker? worker = null)
{
- var args = (FrameSequenceExportArgs)ExportArgs;
foreach (var spine in spinesToRender)
{
if (worker?.CancellationPending == true) break; // 取消的日志在 GetFrames 里输出
// 如果提供了输出文件夹, 则全部导出到输出文件夹, 否则导出到各自的文件夹下
- var subDir = $"{spine.Name}_{timestamp}_{args.FPS:f0}";
- var saveDir = args.OutputDir is null ? Path.Combine(spine.AssetsDir, subDir) : Path.Combine(args.OutputDir, subDir);
+ var subDir = $"{spine.Name}_{timestamp}_{FPS:f0}";
+ var saveDir = Path.Combine(OutputDir ?? spine.AssetsDir, subDir);
Directory.CreateDirectory(saveDir);
int frameIdx = 0;
foreach (var frame in GetFrames(spine, worker))
{
- var filename = $"{spine.Name}_{timestamp}_{args.FPS:f0}_{frameIdx:d6}{args.Suffix}";
+ var filename = $"{spine.Name}_{timestamp}_{FPS:f0}_{frameIdx:d6}{Suffix}";
var savePath = Path.Combine(saveDir, filename);
try
diff --git a/SpineViewer/Exporter/Implementations/ExportArgs/GifExportArgs.cs b/SpineViewer/Exporter/GifExporter.cs
similarity index 70%
rename from SpineViewer/Exporter/Implementations/ExportArgs/GifExportArgs.cs
rename to SpineViewer/Exporter/GifExporter.cs
index 3919f6b..b33a291 100644
--- a/SpineViewer/Exporter/Implementations/ExportArgs/GifExportArgs.cs
+++ b/SpineViewer/Exporter/GifExporter.cs
@@ -6,15 +6,14 @@ using System.Linq;
using System.Text;
using System.Threading.Tasks;
-namespace SpineViewer.Exporter.Implementations.ExportArgs
+namespace SpineViewer.Exporter
{
///
/// GIF 导出参数
///
- [ExportImplementation(ExportType.Gif)]
- public class GifExportArgs : FFmpegVideoExportArgs
+ public class GifExporter : FFmpegVideoExporter
{
- public GifExportArgs(Size resolution, SFML.Graphics.View view, bool renderSelectedOnly) : base(resolution, view, renderSelectedOnly)
+ public GifExporter()
{
// GIF 的帧率不能太高, 超过 50 帧反而会变慢
FPS = 12;
@@ -27,17 +26,17 @@ namespace SpineViewer.Exporter.Implementations.ExportArgs
///
/// 调色板最大颜色数量
///
- [Category("[3] 格式参数"), DisplayName("调色板最大颜色数量"), Description("设置调色板使用的最大颜色数量, 越多则色彩保留程度越高")]
public uint MaxColors { get => maxColors; set => maxColors = Math.Clamp(value, 2, 256); }
private uint maxColors = 256;
///
/// 透明度阈值
///
- [Category("[3] 格式参数"), DisplayName("透明度阈值"), Description("小于该值的像素点会被认为是透明像素")]
public byte AlphaThreshold { get => alphaThreshold; set => alphaThreshold = value; }
private byte alphaThreshold = 128;
+ public override string FileNameNoteSuffix => $"{MaxColors}_{AlphaThreshold}";
+
public override void SetOutputOptions(FFMpegArgumentOptions options)
{
base.SetOutputOptions(options);
@@ -47,7 +46,5 @@ namespace SpineViewer.Exporter.Implementations.ExportArgs
var customArgs = $"-filter_complex \"{v};{s0};{s1}\"";
options.WithCustomArgument(customArgs);
}
-
- public override string FileNameNoteSuffix => $"{MaxColors}_{AlphaThreshold}";
}
}
diff --git a/SpineViewer/Exporter/Implementations/ExportArgs/FFmpegVideoExportArgs.cs b/SpineViewer/Exporter/Implementations/ExportArgs/FFmpegVideoExportArgs.cs
deleted file mode 100644
index 266205e..0000000
--- a/SpineViewer/Exporter/Implementations/ExportArgs/FFmpegVideoExportArgs.cs
+++ /dev/null
@@ -1,59 +0,0 @@
-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
-{
- ///
- /// 使用 FFmpeg 视频导出参数
- ///
- public abstract class FFmpegVideoExportArgs : VideoExportArgs
- {
- public FFmpegVideoExportArgs(Size resolution, SFML.Graphics.View view, bool renderSelectedOnly) : base(resolution, view, renderSelectedOnly) { }
-
- ///
- /// 文件格式
- ///
- [Category("[2] FFmpeg 基本参数"), DisplayName("文件格式"), Description("-f, 文件格式")]
- public abstract string Format { get; }
-
- ///
- /// 文件名后缀
- ///
- [Category("[2] FFmpeg 基本参数"), DisplayName("文件名后缀"), Description("文件名后缀")]
- public abstract string Suffix { get; }
-
- ///
- /// 文件名后缀
- ///
- [Category("[2] FFmpeg 基本参数"), DisplayName("自定义参数"), Description("提供给 FFmpeg 的自定义参数, 除非很清楚自己在做什么, 否则请勿填写此参数")]
- public string CustomArgument { get; set; }
-
- ///
- /// 获取输出附加选项
- ///
- public virtual void SetOutputOptions(FFMpegArgumentOptions options) => options.ForceFormat(Format).WithCustomArgument(CustomArgument);
-
- ///
- /// 要追加在文件名末尾的信息字串, 首尾不需要提供额外分隔符
- ///
- [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;
- }
- }
-}
diff --git a/SpineViewer/Exporter/Implementations/ExportArgs/FrameExportArgs.cs b/SpineViewer/Exporter/Implementations/ExportArgs/FrameExportArgs.cs
deleted file mode 100644
index ccd93cc..0000000
--- a/SpineViewer/Exporter/Implementations/ExportArgs/FrameExportArgs.cs
+++ /dev/null
@@ -1,58 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.ComponentModel;
-using System.Drawing.Imaging;
-using System.Linq;
-using System.Text;
-using System.Threading.Tasks;
-
-namespace SpineViewer.Exporter.Implementations.ExportArgs
-{
- ///
- /// 单帧画面导出参数
- ///
- [ExportImplementation(ExportType.Frame)]
- public class FrameExportArgs : SpineViewer.Exporter.ExportArgs
- {
- public FrameExportArgs(Size resolution, SFML.Graphics.View view, bool renderSelectedOnly) : base(resolution, view, renderSelectedOnly) { }
-
- ///
- /// 单帧画面格式
- ///
- [TypeConverter(typeof(ImageFormatConverter))]
- [Category("[1] 单帧画面"), DisplayName("图像格式")]
- public ImageFormat ImageFormat
- {
- get => imageFormat;
- set
- {
- if (value == ImageFormat.MemoryBmp) value = ImageFormat.Bmp;
- imageFormat = value;
- }
- }
- private ImageFormat imageFormat = ImageFormat.Png;
-
- ///
- /// 文件名后缀
- ///
- [Category("[1] 单帧画面"), DisplayName("文件名后缀"), Description("与图像格式匹配的文件名后缀")]
- public string Suffix { get => imageFormat.GetSuffix(); }
-
- ///
- /// DPI
- ///
- [TypeConverter(typeof(SizeFConverter))]
- [Category("[1] 单帧画面"), DisplayName("DPI"), Description("导出图像的每英寸像素数,用于调整图像的物理尺寸")]
- public SizeF DPI
- {
- get => dpi;
- set
- {
- if (value.Width <= 0) value.Width = 144;
- if (value.Height <= 0) value.Height = 144;
- dpi = value;
- }
- }
- private SizeF dpi = new(144, 144);
- }
-}
diff --git a/SpineViewer/Exporter/Implementations/ExportArgs/FrameSequenceExportArgs.cs b/SpineViewer/Exporter/Implementations/ExportArgs/FrameSequenceExportArgs.cs
deleted file mode 100644
index 48db452..0000000
--- a/SpineViewer/Exporter/Implementations/ExportArgs/FrameSequenceExportArgs.cs
+++ /dev/null
@@ -1,26 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.ComponentModel;
-using System.Drawing.Imaging;
-using System.Linq;
-using System.Text;
-using System.Threading.Tasks;
-
-namespace SpineViewer.Exporter.Implementations.ExportArgs
-{
- ///
- /// 帧序列导出参数
- ///
- [ExportImplementation(ExportType.FrameSequence)]
- public class FrameSequenceExportArgs : VideoExportArgs
- {
- public FrameSequenceExportArgs(Size resolution, SFML.Graphics.View view, bool renderSelectedOnly) : base(resolution, view, renderSelectedOnly) { }
-
- ///
- /// 文件名后缀
- ///
- [TypeConverter(typeof(StringEnumConverter)), StringEnumConverter.StandardValues(".png", ".jpg", ".tga", ".bmp")]
- [Category("[2] 帧序列参数"), DisplayName("文件名后缀"), Description("帧文件的后缀,同时决定帧图像格式")]
- public string Suffix { get; set; } = ".png";
- }
-}
diff --git a/SpineViewer/Exporter/Implementations/ExportArgs/VideoExportArgs.cs b/SpineViewer/Exporter/Implementations/ExportArgs/VideoExportArgs.cs
deleted file mode 100644
index 3847b48..0000000
--- a/SpineViewer/Exporter/Implementations/ExportArgs/VideoExportArgs.cs
+++ /dev/null
@@ -1,43 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.ComponentModel;
-using System.Linq;
-using System.Text;
-using System.Threading.Tasks;
-
-namespace SpineViewer.Exporter.Implementations.ExportArgs
-{
- ///
- /// 视频导出参数基类
- ///
- public abstract class VideoExportArgs : SpineViewer.Exporter.ExportArgs
- {
- public VideoExportArgs(Size resolution, SFML.Graphics.View view, bool renderSelectedOnly) : base(resolution, view, renderSelectedOnly) { }
-
- ///
- /// 导出时长
- ///
- [Category("[1] 视频参数"), DisplayName("时长"), Description("可以从模型列表查看动画时长, 如果小于 0, 则在逐个导出时每个模型使用各自的所有轨道动画时长最大值")]
- public float Duration
- {
- get => duration;
- set => duration = value < 0 ? -1 : value;
- }
- private float duration = -1;
-
- ///
- /// 帧率
- ///
- [Category("[1] 视频参数"), DisplayName("帧率"), Description("每秒画面数")]
- public float FPS { get; set; } = 60;
-
- public override string? Validate()
- {
- if (base.Validate() is string error)
- return error;
- if (ExportSingle && Duration < 0)
- return "导出单个时导出时长不能为负数";
- return null;
- }
- }
-}
diff --git a/SpineViewer/Exporter/Implementations/ExportArgs/MkvExportArgs.cs b/SpineViewer/Exporter/MkvExporter.cs
similarity index 53%
rename from SpineViewer/Exporter/Implementations/ExportArgs/MkvExportArgs.cs
rename to SpineViewer/Exporter/MkvExporter.cs
index 43bef4e..476d4e2 100644
--- a/SpineViewer/Exporter/Implementations/ExportArgs/MkvExportArgs.cs
+++ b/SpineViewer/Exporter/MkvExporter.cs
@@ -6,15 +6,14 @@ using System.Linq;
using System.Text;
using System.Threading.Tasks;
-namespace SpineViewer.Exporter.Implementations.ExportArgs
+namespace SpineViewer.Exporter
{
///
/// MKV 导出参数
///
- [ExportImplementation(ExportType.Mkv)]
- public class MkvExportArgs : FFmpegVideoExportArgs
+ public class MkvExporter : FFmpegVideoExporter
{
- public MkvExportArgs(Size resolution, SFML.Graphics.View view, bool renderSelectedOnly) : base(resolution, view, renderSelectedOnly)
+ public MkvExporter()
{
BackgroundColor = new(0, 255, 0);
}
@@ -26,32 +25,25 @@ namespace SpineViewer.Exporter.Implementations.ExportArgs
///
/// 编码器
///
- [StringEnumConverter.StandardValues("libx264", "libx265", "libvpx-vp9", Customizable = true)]
- [TypeConverter(typeof(StringEnumConverter))]
- [Category("[3] 格式参数"), DisplayName("编码器"), Description("-c:v, 要使用的编码器")]
public string Codec { get; set; } = "libx265";
///
/// CRF
///
- [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;
///
/// 像素格式
///
- [StringEnumConverter.StandardValues("yuv420p", "yuv422p", "yuv444p", "yuva420p", Customizable = true)]
- [TypeConverter(typeof(StringEnumConverter))]
- [Category("[3] 格式参数"), DisplayName("像素格式"), Description("-pix_fmt, 要使用的像素格式")]
public string PixelFormat { get; set; } = "yuv444p";
+ public override string FileNameNoteSuffix => $"{Codec}_{CRF}_{PixelFormat}";
+
public override void SetOutputOptions(FFMpegArgumentOptions options)
{
base.SetOutputOptions(options);
options.WithVideoCodec(Codec).WithConstantRateFactor(CRF).ForcePixelFormat(PixelFormat);
}
-
- public override string FileNameNoteSuffix => $"{Codec}_{CRF}_{PixelFormat}";
}
}
diff --git a/SpineViewer/Exporter/Implementations/ExportArgs/MovExportArgs.cs b/SpineViewer/Exporter/MovExporter.cs
similarity index 51%
rename from SpineViewer/Exporter/Implementations/ExportArgs/MovExportArgs.cs
rename to SpineViewer/Exporter/MovExporter.cs
index 9af32a1..958f889 100644
--- a/SpineViewer/Exporter/Implementations/ExportArgs/MovExportArgs.cs
+++ b/SpineViewer/Exporter/MovExporter.cs
@@ -6,15 +6,14 @@ using System.Linq;
using System.Text;
using System.Threading.Tasks;
-namespace SpineViewer.Exporter.Implementations.ExportArgs
+namespace SpineViewer.Exporter
{
///
/// MOV 导出参数
///
- [ExportImplementation(ExportType.Mov)]
- public class MovExportArgs : FFmpegVideoExportArgs
+ public class MovExporter : FFmpegVideoExporter
{
- public MovExportArgs(Size resolution, SFML.Graphics.View view, bool renderSelectedOnly) : base(resolution, view, renderSelectedOnly)
+ public MovExporter()
{
BackgroundColor = new(0, 255, 0);
}
@@ -26,33 +25,24 @@ namespace SpineViewer.Exporter.Implementations.ExportArgs
///
/// 编码器
///
- [StringEnumConverter.StandardValues("prores_ks", Customizable = true)]
- [TypeConverter(typeof(StringEnumConverter))]
- [Category("[3] 格式参数"), DisplayName("编码器"), Description("-c:v, 要使用的编码器")]
public string Codec { get; set; } = "prores_ks";
///
/// 预设
///
- [StringEnumConverter.StandardValues("auto", "proxy", "lt", "standard", "hq", "4444", "4444xq")]
- [TypeConverter(typeof(StringEnumConverter))]
- [Category("[3] 格式参数"), DisplayName("预设"), Description("-profile, 预设配置")]
public string Profile { get; set; } = "auto";
///
/// 像素格式
///
- [StringEnumConverter.StandardValues("yuv422p10le", "yuv444p10le", "yuva444p10le", Customizable = true)]
- [TypeConverter(typeof(StringEnumConverter))]
- [Category("[3] 格式参数"), DisplayName("像素格式"), Description("-pix_fmt, 要使用的像素格式")]
public string PixelFormat { get; set; } = "yuva444p10le";
+ public override string FileNameNoteSuffix => $"{Codec}_{Profile}_{PixelFormat}";
+
public override void SetOutputOptions(FFMpegArgumentOptions options)
{
base.SetOutputOptions(options);
options.WithFastStart().WithVideoCodec(Codec).WithCustomArgument($"-profile {Profile}").ForcePixelFormat(PixelFormat);
}
-
- public override string FileNameNoteSuffix => $"{Codec}_{Profile}_{PixelFormat}";
}
}
diff --git a/SpineViewer/Exporter/Implementations/ExportArgs/Mp4ExportArgs.cs b/SpineViewer/Exporter/Mp4Exporter.cs
similarity index 54%
rename from SpineViewer/Exporter/Implementations/ExportArgs/Mp4ExportArgs.cs
rename to SpineViewer/Exporter/Mp4Exporter.cs
index 2e928a2..e554ddc 100644
--- a/SpineViewer/Exporter/Implementations/ExportArgs/Mp4ExportArgs.cs
+++ b/SpineViewer/Exporter/Mp4Exporter.cs
@@ -6,15 +6,14 @@ using System.Linq;
using System.Text;
using System.Threading.Tasks;
-namespace SpineViewer.Exporter.Implementations.ExportArgs
+namespace SpineViewer.Exporter
{
///
/// MP4 导出参数
///
- [ExportImplementation(ExportType.Mp4)]
- public class Mp4ExportArgs : FFmpegVideoExportArgs
+ public class Mp4Exporter : FFmpegVideoExporter
{
- public Mp4ExportArgs(Size resolution, SFML.Graphics.View view, bool renderSelectedOnly) : base(resolution, view, renderSelectedOnly)
+ public Mp4Exporter()
{
BackgroundColor = new(0, 255, 0);
}
@@ -26,32 +25,25 @@ namespace SpineViewer.Exporter.Implementations.ExportArgs
///
/// 编码器
///
- [StringEnumConverter.StandardValues("libx264", "libx265", Customizable = true)]
- [TypeConverter(typeof(StringEnumConverter))]
- [Category("[3] 格式参数"), DisplayName("编码器"), Description("-c:v, 要使用的编码器")]
public string Codec { get; set; } = "libx264";
///
/// CRF
///
- [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;
///
/// 像素格式
///
- [StringEnumConverter.StandardValues("yuv420p", "yuv422p", "yuv444p", Customizable = true)]
- [TypeConverter(typeof(StringEnumConverter))]
- [Category("[3] 格式参数"), DisplayName("像素格式"), Description("-pix_fmt, 要使用的像素格式")]
public string PixelFormat { get; set; } = "yuv444p";
+ public override string FileNameNoteSuffix => $"{Codec}_{CRF}_{PixelFormat}";
+
public override void SetOutputOptions(FFMpegArgumentOptions options)
{
base.SetOutputOptions(options);
options.WithFastStart().WithVideoCodec(Codec).WithConstantRateFactor(CRF).ForcePixelFormat(PixelFormat);
}
-
- public override string FileNameNoteSuffix => $"{Codec}_{CRF}_{PixelFormat}";
}
}
diff --git a/SpineViewer/Exporter/TypeConverter.cs b/SpineViewer/Exporter/TypeConverter.cs
deleted file mode 100644
index d43dd67..0000000
--- a/SpineViewer/Exporter/TypeConverter.cs
+++ /dev/null
@@ -1,89 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.Collections.ObjectModel;
-using System.ComponentModel;
-using System.Diagnostics.CodeAnalysis;
-using System.Globalization;
-using System.Linq;
-using System.Text;
-using System.Threading.Tasks;
-
-namespace SpineViewer.Exporter
-{
- public class SFMLColorConverter : ExpandableObjectConverter
- {
- private class SFMLColorPropertyDescriptor : SimplePropertyDescriptor
- {
- public SFMLColorPropertyDescriptor(Type componentType, string name, Type propertyType) : base(componentType, name, propertyType) { }
-
- public override object? GetValue(object? component)
- {
- return component?.GetType().GetField(Name)?.GetValue(component) ?? default;
- }
-
- public override void SetValue(object? component, object? value)
- {
- component?.GetType().GetField(Name)?.SetValue(component, value);
- }
- }
-
- public override bool CanConvertFrom(ITypeDescriptorContext? context, Type sourceType)
- {
- return sourceType == typeof(string) || base.CanConvertFrom(context, sourceType);
- }
-
- public override object? ConvertFrom(ITypeDescriptorContext? context, CultureInfo? culture, object value)
- {
- if (value is string s)
- {
- s = s.Trim();
- if (s.StartsWith("#") && s.Length == 9)
- {
- try
- {
- // 解析 R, G, B, A 分量,注意16进制解析
- byte r = byte.Parse(s.Substring(1, 2), NumberStyles.HexNumber);
- byte g = byte.Parse(s.Substring(3, 2), NumberStyles.HexNumber);
- byte b = byte.Parse(s.Substring(5, 2), NumberStyles.HexNumber);
- byte a = byte.Parse(s.Substring(7, 2), NumberStyles.HexNumber);
- return new SFML.Graphics.Color(r, g, b, a);
- }
- catch (Exception ex)
- {
- throw new FormatException("无法解析颜色,确保格式为 #RRGGBBAA", ex);
- }
- }
- throw new FormatException("格式错误,正确格式为 #RRGGBBAA");
- }
- return base.ConvertFrom(context, culture, value);
- }
-
- public override bool CanConvertTo(ITypeDescriptorContext? context, Type? destinationType)
- {
- return destinationType == typeof(string) || base.CanConvertTo(context, destinationType);
- }
-
- public override object? ConvertTo(ITypeDescriptorContext? context, CultureInfo? culture, object? value, Type destinationType)
- {
- if (destinationType == typeof(string) && value is SFML.Graphics.Color color)
- return $"#{color.R:X2}{color.G:X2}{color.B:X2}{color.A:X2}";
- return base.ConvertTo(context, culture, value, destinationType);
- }
-
- public override PropertyDescriptorCollection GetProperties(ITypeDescriptorContext? context, object value, Attribute[]? attributes)
- {
- // 自定义属性集合
- var properties = new List
- {
- // 定义 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());
- }
- }
-}
diff --git a/SpineViewer/Exporter/UITypeEditor.cs b/SpineViewer/Exporter/UITypeEditor.cs
deleted file mode 100644
index e6e58b3..0000000
--- a/SpineViewer/Exporter/UITypeEditor.cs
+++ /dev/null
@@ -1,66 +0,0 @@
-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);
- }
- }
-}
diff --git a/SpineViewer/Exporter/Implementations/Exporter/VideoExporter.cs b/SpineViewer/Exporter/VideoExporter.cs
similarity index 66%
rename from SpineViewer/Exporter/Implementations/Exporter/VideoExporter.cs
rename to SpineViewer/Exporter/VideoExporter.cs
index aa6e273..0b9d93d 100644
--- a/SpineViewer/Exporter/Implementations/Exporter/VideoExporter.cs
+++ b/SpineViewer/Exporter/VideoExporter.cs
@@ -1,5 +1,4 @@
-using SpineViewer.Exporter.Implementations.ExportArgs;
-using SpineViewer.Spine;
+using SpineViewer.Spine;
using System;
using System.Collections.Generic;
using System.ComponentModel;
@@ -7,28 +6,44 @@ using System.Linq;
using System.Text;
using System.Threading.Tasks;
-namespace SpineViewer.Exporter.Implementations.Exporter
+namespace SpineViewer.Exporter
{
///
/// 视频导出基类
///
- public abstract class VideoExporter : SpineViewer.Exporter.Exporter
+ public abstract class VideoExporter : Exporter
{
- public VideoExporter(VideoExportArgs exportArgs) : base(exportArgs) { }
+ ///
+ /// 导出时长
+ ///
+ public float Duration { get => duration; set => duration = value < 0 ? -1 : value; }
+ private float duration = -1;
+
+ ///
+ /// 帧率
+ ///
+ public float FPS { get; set; } = 60;
+
+ public override string? Validate()
+ {
+ if (base.Validate() is string error)
+ return error;
+ if (IsExportSingle && Duration < 0)
+ return "导出单个时导出时长不能为负数";
+ return null;
+ }
///
/// 生成单个模型的帧序列
///
protected IEnumerable GetFrames(Spine.Spine spine, BackgroundWorker? worker = null)
{
- var args = (VideoExportArgs)ExportArgs;
-
- // 独立导出时如果 args.Duration 小于 0 则使用所有轨道上动画时长最大值
- var duration = args.Duration;
+ // 独立导出时如果 Duration 小于 0 则使用所有轨道上动画时长最大值
+ var duration = Duration;
if (duration < 0) duration = spine.GetTrackIndices().Select(i => spine.GetAnimationDuration(spine.GetAnimation(i))).Max();
- float delta = 1f / args.FPS;
- int total = Math.Max(1, (int)(duration * args.FPS)); // 至少导出 1 帧
+ float delta = 1f / FPS;
+ int total = Math.Max(1, (int)(duration * FPS)); // 至少导出 1 帧
worker?.ReportProgress(0, $"{spine.Name} 已处理 0/{total} 帧");
for (int i = 0; i < total; i++)
@@ -51,10 +66,9 @@ namespace SpineViewer.Exporter.Implementations.Exporter
///
protected IEnumerable GetFrames(Spine.Spine[] spinesToRender, BackgroundWorker? worker = null)
{
- // 导出单个时必须根据 args.Duration 决定导出时长
- var args = (VideoExportArgs)ExportArgs;
- float delta = 1f / args.FPS;
- int total = Math.Max(1, (int)(args.Duration * args.FPS)); // 至少导出 1 帧
+ // 导出单个时必须根据 Duration 决定导出时长
+ float delta = 1f / FPS;
+ int total = Math.Max(1, (int)(Duration * FPS)); // 至少导出 1 帧
worker?.ReportProgress(0, $"已处理 0/{total} 帧");
for (int i = 0; i < total; i++)
diff --git a/SpineViewer/Exporter/Implementations/ExportArgs/WebmExportArgs.cs b/SpineViewer/Exporter/WebmExporter.cs
similarity index 54%
rename from SpineViewer/Exporter/Implementations/ExportArgs/WebmExportArgs.cs
rename to SpineViewer/Exporter/WebmExporter.cs
index 77f64ca..b8097fd 100644
--- a/SpineViewer/Exporter/Implementations/ExportArgs/WebmExportArgs.cs
+++ b/SpineViewer/Exporter/WebmExporter.cs
@@ -6,15 +6,14 @@ using System.Linq;
using System.Text;
using System.Threading.Tasks;
-namespace SpineViewer.Exporter.Implementations.ExportArgs
+namespace SpineViewer.Exporter
{
///
/// WebM 导出参数
///
- [ExportImplementation(ExportType.Webm)]
- public class WebmExportArgs : FFmpegVideoExportArgs
+ public class WebmExporter : FFmpegVideoExporter
{
- public WebmExportArgs(Size resolution, SFML.Graphics.View view, bool renderSelectedOnly) : base(resolution, view, renderSelectedOnly)
+ public WebmExporter()
{
// 默认用透明黑背景
BackgroundColor = new(0, 0, 0, 0);
@@ -27,32 +26,25 @@ namespace SpineViewer.Exporter.Implementations.ExportArgs
///
/// 编码器
///
- [StringEnumConverter.StandardValues("libvpx-vp9", Customizable = true)]
- [TypeConverter(typeof(StringEnumConverter))]
- [Category("[3] 格式参数"), DisplayName("编码器"), Description("-c:v, 要使用的编码器")]
public string Codec { get; set; } = "libvpx-vp9";
///
/// CRF
///
- [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;
///
/// 像素格式
///
- [StringEnumConverter.StandardValues("yuv420p", "yuv422p", "yuv444p", "yuva420p", Customizable = true)]
- [TypeConverter(typeof(StringEnumConverter))]
- [Category("[3] 格式参数"), DisplayName("像素格式"), Description("-pix_fmt, 要使用的像素格式")]
public string PixelFormat { get; set; } = "yuva420p";
+ public override string FileNameNoteSuffix => $"{Codec}_{CRF}_{PixelFormat}";
+
public override void SetOutputOptions(FFMpegArgumentOptions options)
{
base.SetOutputOptions(options);
options.WithVideoCodec(Codec).WithConstantRateFactor(CRF).ForcePixelFormat(PixelFormat);
}
-
- public override string FileNameNoteSuffix => $"{Codec}_{CRF}_{PixelFormat}";
}
}
diff --git a/SpineViewer/NLogExtension.cs b/SpineViewer/Extensions/NLogExtension.cs
similarity index 94%
rename from SpineViewer/NLogExtension.cs
rename to SpineViewer/Extensions/NLogExtension.cs
index d6b2d6b..5b58765 100644
--- a/SpineViewer/NLogExtension.cs
+++ b/SpineViewer/Extensions/NLogExtension.cs
@@ -5,7 +5,7 @@ using System.Linq;
using System.Text;
using System.Threading.Tasks;
-namespace SpineViewer
+namespace SpineViewer.Extensions
{
public static class NLogExtension
{
diff --git a/SpineViewer/Spine/BlendModeSFML.cs b/SpineViewer/Extensions/SFMLBlendMode.cs
similarity index 93%
rename from SpineViewer/Spine/BlendModeSFML.cs
rename to SpineViewer/Extensions/SFMLBlendMode.cs
index 35fafd3..0936ecc 100644
--- a/SpineViewer/Spine/BlendModeSFML.cs
+++ b/SpineViewer/Extensions/SFMLBlendMode.cs
@@ -4,12 +4,12 @@ using System.Linq;
using System.Text;
using System.Threading.Tasks;
-namespace SpineViewer.Spine
+namespace SpineViewer.Extensions
{
///
/// SFML 混合模式, 预乘模式下输入和输出的像素值都是预乘的
///
- public static class BlendModeSFML
+ public static class SFMLBlendMode
{
/////
///// Normal Blend, 无预乘, 仅在 dst.a 是 1 时得到正确结果, 其余情况是有偏结果
@@ -110,5 +110,13 @@ namespace SpineViewer.Spine
SFML.Graphics.BlendMode.Factor.OneMinusSrcAlpha,
SFML.Graphics.BlendMode.Equation.Add
);
+
+ ///
+ /// 仅源像素混合模式
+ ///
+ public static readonly SFML.Graphics.BlendMode SourceOnly = new(
+ SFML.Graphics.BlendMode.Factor.One,
+ SFML.Graphics.BlendMode.Factor.Zero
+ );
}
}
diff --git a/SpineViewer/SFMLExtension.cs b/SpineViewer/Extensions/SFMLExtension.cs
similarity index 97%
rename from SpineViewer/SFMLExtension.cs
rename to SpineViewer/Extensions/SFMLExtension.cs
index a4fdf58..260158d 100644
--- a/SpineViewer/SFMLExtension.cs
+++ b/SpineViewer/Extensions/SFMLExtension.cs
@@ -4,7 +4,7 @@ using System.Linq;
using System.Text;
using System.Threading.Tasks;
-namespace SpineViewer
+namespace SpineViewer.Extensions
{
public static class SFMLExtension
{
@@ -25,7 +25,7 @@ namespace SpineViewer
public static Bitmap CopyToBitmap(this SFML.Graphics.Texture texture)
{
using var image = texture.CopyToImage();
- return CopyToBitmap(image);
+ return image.CopyToBitmap();
}
///
diff --git a/SpineViewer/Shader.cs b/SpineViewer/Extensions/SFMLShader.cs
similarity index 97%
rename from SpineViewer/Shader.cs
rename to SpineViewer/Extensions/SFMLShader.cs
index f62001c..eacaf36 100644
--- a/SpineViewer/Shader.cs
+++ b/SpineViewer/Extensions/SFMLShader.cs
@@ -4,9 +4,9 @@ using System.Linq;
using System.Text;
using System.Threading.Tasks;
-namespace SpineViewer
+namespace SpineViewer.Extensions
{
- public static class Shader
+ public static class SFMLShader
{
///
/// 用于非预乘纹理的 fragment shader, 乘上了插值后的透明度用于实现透明度变化(插值预乘), 并且输出预乘后的像素值
diff --git a/SpineViewer/MainForm.Designer.cs b/SpineViewer/MainForm.Designer.cs
index 59d4177..ee08d52 100644
--- a/SpineViewer/MainForm.Designer.cs
+++ b/SpineViewer/MainForm.Designer.cs
@@ -115,27 +115,27 @@
//
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_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
@@ -143,67 +143,67 @@
toolStripMenuItem_ExportFrame.Name = "toolStripMenuItem_ExportFrame";
toolStripMenuItem_ExportFrame.Size = new Size(288, 34);
toolStripMenuItem_ExportFrame.Text = "单帧画面...";
- toolStripMenuItem_ExportFrame.Click += toolStripMenuItem_Export_Click;
+ toolStripMenuItem_ExportFrame.Click += toolStripMenuItem_ExportFrame_Click;
//
// toolStripMenuItem_ExportFrameSequence
//
toolStripMenuItem_ExportFrameSequence.Name = "toolStripMenuItem_ExportFrameSequence";
toolStripMenuItem_ExportFrameSequence.Size = new Size(288, 34);
toolStripMenuItem_ExportFrameSequence.Text = "帧序列...";
- toolStripMenuItem_ExportFrameSequence.Click += toolStripMenuItem_Export_Click;
+ toolStripMenuItem_ExportFrameSequence.Click += toolStripMenuItem_ExportFrameSequence_Click;
//
// toolStripMenuItem_ExportGif
//
toolStripMenuItem_ExportGif.Name = "toolStripMenuItem_ExportGif";
toolStripMenuItem_ExportGif.Size = new Size(288, 34);
toolStripMenuItem_ExportGif.Text = "GIF...";
- toolStripMenuItem_ExportGif.Click += toolStripMenuItem_Export_Click;
+ toolStripMenuItem_ExportGif.Click += toolStripMenuItem_ExportGif_Click;
//
// toolStripMenuItem_ExportMp4
//
toolStripMenuItem_ExportMp4.Name = "toolStripMenuItem_ExportMp4";
toolStripMenuItem_ExportMp4.Size = new Size(288, 34);
toolStripMenuItem_ExportMp4.Text = "MP4...";
- toolStripMenuItem_ExportMp4.Click += toolStripMenuItem_Export_Click;
+ toolStripMenuItem_ExportMp4.Click += toolStripMenuItem_ExportMp4_Click;
//
// toolStripMenuItem_ExportWebm
//
toolStripMenuItem_ExportWebm.Name = "toolStripMenuItem_ExportWebm";
toolStripMenuItem_ExportWebm.Size = new Size(288, 34);
toolStripMenuItem_ExportWebm.Text = "WebM...";
- toolStripMenuItem_ExportWebm.Click += toolStripMenuItem_Export_Click;
+ toolStripMenuItem_ExportWebm.Click += toolStripMenuItem_ExportWebm_Click;
//
// toolStripMenuItem_ExportMkv
//
toolStripMenuItem_ExportMkv.Name = "toolStripMenuItem_ExportMkv";
toolStripMenuItem_ExportMkv.Size = new Size(288, 34);
toolStripMenuItem_ExportMkv.Text = "MKV...";
- toolStripMenuItem_ExportMkv.Click += toolStripMenuItem_Export_Click;
+ toolStripMenuItem_ExportMkv.Click += toolStripMenuItem_ExportMkv_Click;
//
// toolStripMenuItem_ExportMov
//
toolStripMenuItem_ExportMov.Name = "toolStripMenuItem_ExportMov";
toolStripMenuItem_ExportMov.Size = new Size(288, 34);
toolStripMenuItem_ExportMov.Text = "MOV...";
- toolStripMenuItem_ExportMov.Click += toolStripMenuItem_Export_Click;
+ toolStripMenuItem_ExportMov.Click += toolStripMenuItem_ExportMov_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;
+ toolStripMenuItem_ExportCustom.Click += toolStripMenuItem_ExportCustom_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;
//
@@ -271,7 +271,7 @@
rtbLog.Margin = new Padding(3, 2, 3, 2);
rtbLog.Name = "rtbLog";
rtbLog.ReadOnly = true;
- rtbLog.Size = new Size(1758, 120);
+ rtbLog.Size = new Size(1758, 124);
rtbLog.TabIndex = 0;
rtbLog.Text = "";
rtbLog.WordWrap = false;
@@ -295,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 = 969;
+ splitContainer_MainForm.SplitterDistance = 965;
splitContainer_MainForm.SplitterWidth = 8;
splitContainer_MainForm.TabIndex = 3;
splitContainer_MainForm.TabStop = false;
@@ -319,7 +319,7 @@
//
splitContainer_Functional.Panel2.Controls.Add(groupBox_Preview);
splitContainer_Functional.Panel2.Cursor = Cursors.Default;
- splitContainer_Functional.Size = new Size(1758, 969);
+ splitContainer_Functional.Size = new Size(1758, 965);
splitContainer_Functional.SplitterDistance = 759;
splitContainer_Functional.SplitterWidth = 8;
splitContainer_Functional.TabIndex = 2;
@@ -343,7 +343,7 @@
//
splitContainer_Information.Panel2.Controls.Add(splitContainer_Config);
splitContainer_Information.Panel2.Cursor = Cursors.Default;
- splitContainer_Information.Size = new Size(759, 969);
+ splitContainer_Information.Size = new Size(759, 965);
splitContainer_Information.SplitterDistance = 354;
splitContainer_Information.SplitterWidth = 8;
splitContainer_Information.TabIndex = 1;
@@ -357,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, 969);
+ groupBox_SkelList.Size = new Size(354, 965);
groupBox_SkelList.TabIndex = 0;
groupBox_SkelList.TabStop = false;
groupBox_SkelList.Text = "模型列表";
@@ -368,7 +368,7 @@
spineListView.Location = new Point(3, 26);
spineListView.Name = "spineListView";
spineListView.PropertyGrid = propertyGrid_Spine;
- spineListView.Size = new Size(348, 940);
+ spineListView.Size = new Size(348, 936);
spineListView.TabIndex = 0;
//
// propertyGrid_Spine
@@ -377,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, 606);
+ propertyGrid_Spine.Size = new Size(391, 602);
propertyGrid_Spine.TabIndex = 0;
propertyGrid_Spine.ToolbarVisible = false;
propertyGrid_Spine.PropertyValueChanged += propertyGrid_PropertyValueChanged;
@@ -400,7 +400,7 @@
//
splitContainer_Config.Panel2.Controls.Add(groupBox_SkelConfig);
splitContainer_Config.Panel2.Cursor = Cursors.Default;
- splitContainer_Config.Size = new Size(397, 969);
+ splitContainer_Config.Size = new Size(397, 965);
splitContainer_Config.SplitterDistance = 326;
splitContainer_Config.SplitterWidth = 8;
splitContainer_Config.TabIndex = 0;
@@ -436,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, 635);
+ groupBox_SkelConfig.Size = new Size(397, 631);
groupBox_SkelConfig.TabIndex = 0;
groupBox_SkelConfig.TabStop = false;
groupBox_SkelConfig.Text = "模型参数";
@@ -447,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, 969);
+ groupBox_Preview.Size = new Size(991, 965);
groupBox_Preview.TabIndex = 1;
groupBox_Preview.TabStop = false;
groupBox_Preview.Text = "预览画面";
@@ -458,7 +458,7 @@
spinePreviewer.Location = new Point(3, 26);
spinePreviewer.Name = "spinePreviewer";
spinePreviewer.PropertyGrid = propertyGrid_Previewer;
- spinePreviewer.Size = new Size(985, 940);
+ spinePreviewer.Size = new Size(985, 936);
spinePreviewer.SpineListView = spineListView;
spinePreviewer.TabIndex = 0;
//
diff --git a/SpineViewer/MainForm.cs b/SpineViewer/MainForm.cs
index 5ac910d..d57cbe3 100644
--- a/SpineViewer/MainForm.cs
+++ b/SpineViewer/MainForm.cs
@@ -3,6 +3,9 @@ using SpineViewer.Spine;
using System.ComponentModel;
using System.Diagnostics;
using SpineViewer.Exporter;
+using SpineViewer.Extensions;
+using System.Reflection.Metadata;
+using SpineViewer.PropertyGridWrappers.Exporter;
namespace SpineViewer
{
@@ -15,20 +18,10 @@ namespace SpineViewer
InitializeComponent();
InitializeLogConfiguration();
- // 在此处将导出菜单需要的类绑定起来
- toolStripMenuItem_ExportFrame.Tag = ExportType.Frame;
- toolStripMenuItem_ExportFrameSequence.Tag = ExportType.FrameSequence;
- toolStripMenuItem_ExportGif.Tag = ExportType.Gif;
- toolStripMenuItem_ExportMkv.Tag = ExportType.Mkv;
- toolStripMenuItem_ExportMp4.Tag = ExportType.Mp4;
- toolStripMenuItem_ExportMov.Tag = ExportType.Mov;
- toolStripMenuItem_ExportWebm.Tag = ExportType.Webm;
- toolStripMenuItem_ExportCustom.Tag = ExportType.Custom;
-
// 执行一些初始化工作
try
{
- Shader.Init();
+ SFMLShader.Init();
}
catch (Exception ex)
{
@@ -87,22 +80,23 @@ namespace SpineViewer
spineListView.BatchAdd();
}
- private void toolStripMenuItem_Export_Click(object sender, EventArgs e)
+ #region toolStripMenuItem_ExportXXX_Click
+
+ private void toolStripMenuItem_ExportFrame_Click(object sender, EventArgs e)
{
- ExportType type = (ExportType)((ToolStripMenuItem)sender).Tag;
-
- if (type == ExportType.Frame && spinePreviewer.IsUpdating)
- {
- if (MessageBox.Quest("画面仍在更新,建议手动暂停画面后导出固定的一帧,是否继续?") != DialogResult.OK)
- return;
- }
-
- var exportArgs = ExportArgs.New(type, spinePreviewer.Resolution, spinePreviewer.GetView(), spinePreviewer.RenderSelectedOnly);
- var exportDialog = new Dialogs.ExportDialog() { ExportArgs = exportArgs };
- if (exportDialog.ShowDialog() != DialogResult.OK)
+ if (spinePreviewer.IsUpdating && MessageBox.Quest("画面仍在更新,建议手动暂停画面后导出固定的一帧,是否继续?") != DialogResult.OK)
return;
- var exporter = Exporter.Exporter.New(type, exportArgs);
+ var exporter = new FrameExporter()
+ {
+ Resolution = spinePreviewer.Resolution,
+ View = spinePreviewer.GetView(),
+ RenderSelectedOnly = spinePreviewer.RenderSelectedOnly
+ };
+
+ var exportDialog = new Dialogs.ExportDialog(new FrameExporterWrapper(exporter));
+ if (exportDialog.ShowDialog() != DialogResult.OK)
+ return;
var progressDialog = new Dialogs.ProgressDialog();
progressDialog.DoWork += Export_Work;
@@ -110,6 +104,141 @@ namespace SpineViewer
progressDialog.ShowDialog();
}
+ private void toolStripMenuItem_ExportFrameSequence_Click(object sender, EventArgs e)
+ {
+ var exporter = new FrameSequenceExporter()
+ {
+ Resolution = spinePreviewer.Resolution,
+ View = spinePreviewer.GetView(),
+ RenderSelectedOnly = spinePreviewer.RenderSelectedOnly
+ };
+
+ var exportDialog = new Dialogs.ExportDialog(new FrameSequenceExporterWrapper(exporter));
+ if (exportDialog.ShowDialog() != DialogResult.OK)
+ return;
+
+ var progressDialog = new Dialogs.ProgressDialog();
+ progressDialog.DoWork += Export_Work;
+ progressDialog.RunWorkerAsync(exporter);
+ progressDialog.ShowDialog();
+ }
+
+ private void toolStripMenuItem_ExportGif_Click(object sender, EventArgs e)
+ {
+ var exporter = new GifExporter()
+ {
+ Resolution = spinePreviewer.Resolution,
+ View = spinePreviewer.GetView(),
+ RenderSelectedOnly = spinePreviewer.RenderSelectedOnly
+ };
+
+ var exportDialog = new Dialogs.ExportDialog(new GifExporterWrapper(exporter));
+ if (exportDialog.ShowDialog() != DialogResult.OK)
+ return;
+
+ var progressDialog = new Dialogs.ProgressDialog();
+ progressDialog.DoWork += Export_Work;
+ progressDialog.RunWorkerAsync(exporter);
+ progressDialog.ShowDialog();
+ }
+
+ private void toolStripMenuItem_ExportMp4_Click(object sender, EventArgs e)
+ {
+ var exporter = new Mp4Exporter()
+ {
+ Resolution = spinePreviewer.Resolution,
+ View = spinePreviewer.GetView(),
+ RenderSelectedOnly = spinePreviewer.RenderSelectedOnly
+ };
+
+ var exportDialog = new Dialogs.ExportDialog(new Mp4ExporterWrapper(exporter));
+ if (exportDialog.ShowDialog() != DialogResult.OK)
+ return;
+
+ var progressDialog = new Dialogs.ProgressDialog();
+ progressDialog.DoWork += Export_Work;
+ progressDialog.RunWorkerAsync(exporter);
+ progressDialog.ShowDialog();
+ }
+
+ private void toolStripMenuItem_ExportWebm_Click(object sender, EventArgs e)
+ {
+ var exporter = new WebmExporter()
+ {
+ Resolution = spinePreviewer.Resolution,
+ View = spinePreviewer.GetView(),
+ RenderSelectedOnly = spinePreviewer.RenderSelectedOnly
+ };
+
+ var exportDialog = new Dialogs.ExportDialog(new WebmExporterWrapper(exporter));
+ if (exportDialog.ShowDialog() != DialogResult.OK)
+ return;
+
+ var progressDialog = new Dialogs.ProgressDialog();
+ progressDialog.DoWork += Export_Work;
+ progressDialog.RunWorkerAsync(exporter);
+ progressDialog.ShowDialog();
+ }
+
+ private void toolStripMenuItem_ExportMkv_Click(object sender, EventArgs e)
+ {
+ var exporter = new MkvExporter()
+ {
+ Resolution = spinePreviewer.Resolution,
+ View = spinePreviewer.GetView(),
+ RenderSelectedOnly = spinePreviewer.RenderSelectedOnly
+ };
+
+ var exportDialog = new Dialogs.ExportDialog(new MkvExporterWrapper(exporter));
+ if (exportDialog.ShowDialog() != DialogResult.OK)
+ return;
+
+ var progressDialog = new Dialogs.ProgressDialog();
+ progressDialog.DoWork += Export_Work;
+ progressDialog.RunWorkerAsync(exporter);
+ progressDialog.ShowDialog();
+ }
+
+ private void toolStripMenuItem_ExportMov_Click(object sender, EventArgs e)
+ {
+ var exporter = new MovExporter()
+ {
+ Resolution = spinePreviewer.Resolution,
+ View = spinePreviewer.GetView(),
+ RenderSelectedOnly = spinePreviewer.RenderSelectedOnly
+ };
+
+ var exportDialog = new Dialogs.ExportDialog(new MovExporterWrapper(exporter));
+ if (exportDialog.ShowDialog() != DialogResult.OK)
+ return;
+
+ var progressDialog = new Dialogs.ProgressDialog();
+ progressDialog.DoWork += Export_Work;
+ progressDialog.RunWorkerAsync(exporter);
+ progressDialog.ShowDialog();
+ }
+
+ private void toolStripMenuItem_ExportCustom_Click(object sender, EventArgs e)
+ {
+ var exporter = new CustomExporter()
+ {
+ Resolution = spinePreviewer.Resolution,
+ View = spinePreviewer.GetView(),
+ RenderSelectedOnly = spinePreviewer.RenderSelectedOnly
+ };
+
+ var exportDialog = new Dialogs.ExportDialog(new CustomExporterWrapper(exporter));
+ if (exportDialog.ShowDialog() != DialogResult.OK)
+ return;
+
+ var progressDialog = new Dialogs.ProgressDialog();
+ progressDialog.DoWork += Export_Work;
+ progressDialog.RunWorkerAsync(exporter);
+ progressDialog.ShowDialog();
+ }
+
+ #endregion
+
private void toolStripMenuItem_Exit_Click(object sender, EventArgs e)
{
Close();
@@ -127,57 +256,21 @@ namespace SpineViewer
progressDialog.ShowDialog();
}
- //private System.Windows.Forms.Timer timer = new();
- //private PetForm pet = new PetForm();
- //private IntPtr screenDC;
- //private IntPtr memDC;
private void toolStripMenuItem_ManageResource_Click(object sender, EventArgs e)
{
- // screenDC = Win32.GetDC(IntPtr.Zero);
- // memDC = Win32.CreateCompatibleDC(screenDC);
- // pet.Show();
- // timer.Tick += Timer_Tick;
- // timer.Enabled = true;
- // timer.Interval = 50;
- // timer.Start();
- //}
- //private void Timer_Tick(object? sender, EventArgs e)
- //{
- // using var tex = new SFML.Graphics.RenderTexture((uint)pet.Width, (uint)pet.Height);
- // var v = spinePreviewer.GetView();
- // tex.SetView(v);
- // tex.Clear(new SFML.Graphics.Color(0, 0, 0, 0));
- // lock (spineListView.Spines)
- // {
- // foreach (var sp in spineListView.Spines)
- // tex.Draw(sp);
- // }
- // tex.Display();
- // using var frame = new SFMLImageVideoFrame(tex.Texture.CopyToImage());
- // using var bitmap = frame.CopyToBitmap();
-
- // var newBitmap = bitmap.GetHbitmap(Color.FromArgb(0));
- // var oldBitmap = Win32.SelectObject(memDC, newBitmap);
-
- // Win32.SIZE size = new Win32.SIZE { cx = pet.Width, cy = pet.Height };
- // Win32.POINT srcPos = new Win32.POINT { x = 0, y = 0 };
- // Win32.BLENDFUNCTION blend = new Win32.BLENDFUNCTION { BlendOp = 0, BlendFlags = 0, SourceConstantAlpha = 255, AlphaFormat = Win32.AC_SRC_ALPHA };
-
- // Win32.UpdateLayeredWindow(pet.Handle, screenDC, IntPtr.Zero, ref size, memDC, ref srcPos, 0, ref blend, Win32.ULW_ALPHA);
-
- // Win32.SelectObject(memDC, oldBitmap);
- // Win32.DeleteObject(newBitmap);
}
private void toolStripMenuItem_About_Click(object sender, EventArgs e)
{
- (new Dialogs.AboutDialog()).ShowDialog();
+ using var dialog = new Dialogs.AboutDialog();
+ dialog.ShowDialog();
}
private void toolStripMenuItem_Diagnostics_Click(object sender, EventArgs e)
{
- (new Dialogs.DiagnosticsDialog()).ShowDialog();
+ using var dialog = new Dialogs.DiagnosticsDialog();
+ dialog.ShowDialog();
}
private void splitContainer_SplitterMoved(object sender, SplitterEventArgs e) => ActiveControl = null;
@@ -269,6 +362,49 @@ namespace SpineViewer
}
}
+ //private System.Windows.Forms.Timer timer = new();
+ //private PetForm pet = new PetForm();
+ //private IntPtr screenDC;
+ //private IntPtr memDC;
+ //private void _Test()
+ //{
+ // screenDC = Win32.GetDC(IntPtr.Zero);
+ // memDC = Win32.CreateCompatibleDC(screenDC);
+ // pet.Show();
+ // timer.Tick += Timer_Tick;
+ // timer.Enabled = true;
+ // timer.Interval = 50;
+ // timer.Start();
+ //}
+
+ //private void Timer_Tick(object? sender, EventArgs e)
+ //{
+ // using var tex = new SFML.Graphics.RenderTexture((uint)pet.Width, (uint)pet.Height);
+ // var v = spinePreviewer.GetView();
+ // tex.SetView(v);
+ // tex.Clear(new SFML.Graphics.Color(0, 0, 0, 0));
+ // lock (spineListView.Spines)
+ // {
+ // foreach (var sp in spineListView.Spines)
+ // tex.Draw(sp);
+ // }
+ // tex.Display();
+ // using var frame = new SFMLImageVideoFrame(tex.Texture.CopyToImage());
+ // using var bitmap = frame.CopyToBitmap();
+
+ // var newBitmap = bitmap.GetHbitmap(Color.FromArgb(0));
+ // var oldBitmap = Win32.SelectObject(memDC, newBitmap);
+
+ // Win32.SIZE size = new Win32.SIZE { cx = pet.Width, cy = pet.Height };
+ // Win32.POINT srcPos = new Win32.POINT { x = 0, y = 0 };
+ // Win32.BLENDFUNCTION blend = new Win32.BLENDFUNCTION { BlendOp = 0, BlendFlags = 0, SourceConstantAlpha = 255, AlphaFormat = Win32.AC_SRC_ALPHA };
+
+ // Win32.UpdateLayeredWindow(pet.Handle, screenDC, IntPtr.Zero, ref size, memDC, ref srcPos, 0, ref blend, Win32.ULW_ALPHA);
+
+ // Win32.SelectObject(memDC, oldBitmap);
+ // Win32.DeleteObject(newBitmap);
+ //}
+
//private void spinePreviewer_KeyDown(object sender, KeyEventArgs e)
//{
// switch (e.KeyCode)
diff --git a/SpineViewer/PropertyGridWrappers/Exporter/CustomExporterWrapper.cs b/SpineViewer/PropertyGridWrappers/Exporter/CustomExporterWrapper.cs
new file mode 100644
index 0000000..261bcf5
--- /dev/null
+++ b/SpineViewer/PropertyGridWrappers/Exporter/CustomExporterWrapper.cs
@@ -0,0 +1,28 @@
+using SpineViewer.Exporter;
+using System;
+using System.Collections.Generic;
+using System.ComponentModel;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace SpineViewer.PropertyGridWrappers.Exporter
+{
+ public class CustomExporterWrapper(CustomExporter exporter) : FFmpegVideoExporterWrapper(exporter)
+ {
+ [Browsable(false)]
+ public override CustomExporter Exporter => (CustomExporter)base.Exporter;
+
+ ///
+ /// 文件格式
+ ///
+ [Category("[3] 自定义参数"), DisplayName("文件格式"), Description("文件格式")]
+ public string CustomFormat { get; set; } = "mp4";
+
+ ///
+ /// 文件名后缀
+ ///
+ [Category("[3] 自定义参数"), DisplayName("文件名后缀"), Description("文件名后缀")]
+ public string CustomSuffix { get; set; } = ".mp4";
+ }
+}
diff --git a/SpineViewer/PropertyGridWrappers/Exporter/ExporterWrapper.cs b/SpineViewer/PropertyGridWrappers/Exporter/ExporterWrapper.cs
new file mode 100644
index 0000000..a8c1aa8
--- /dev/null
+++ b/SpineViewer/PropertyGridWrappers/Exporter/ExporterWrapper.cs
@@ -0,0 +1,56 @@
+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.PropertyGridWrappers.Exporter
+{
+ public class ExporterWrapper(SpineViewer.Exporter.Exporter exporter)
+ {
+ [Browsable(false)]
+ public virtual SpineViewer.Exporter.Exporter Exporter { get; } = exporter;
+
+ ///
+ /// 输出文件夹
+ ///
+ [Editor(typeof(FolderNameEditor), typeof(UITypeEditor))]
+ [Category("[0] 导出"), DisplayName("输出文件夹"), Description("逐个导出时可以留空,将逐个导出到模型自身所在目录")]
+ public string? OutputDir { get => Exporter.OutputDir; set => Exporter.OutputDir = value; }
+
+ ///
+ /// 导出单个
+ ///
+ [Category("[0] 导出"), DisplayName("导出单个"), Description("是否将模型在同一个画面上导出单个文件,否则逐个导出模型")]
+ public bool IsExportSingle { get => Exporter.IsExportSingle; set => Exporter.IsExportSingle = value; }
+
+ ///
+ /// 画面分辨率
+ ///
+ [TypeConverter(typeof(SizeConverter))]
+ [Category("[0] 导出"), DisplayName("分辨率"), Description("画面的宽高像素大小,请在预览画面参数面板进行调整")]
+ public Size Resolution { get => Exporter.Resolution; }
+
+ ///
+ /// 渲染视窗
+ ///
+ [Category("[0] 导出"), DisplayName("视图"), Description("画面的视图参数,请在预览画面参数面板进行调整")]
+ public SFML.Graphics.View View { get => Exporter.View; }
+
+ ///
+ /// 是否仅渲染选中
+ ///
+ [Category("[0] 导出"), DisplayName("仅渲染选中"), Description("是否仅导出选中的模型,请在预览画面参数面板进行调整")]
+ public bool RenderSelectedOnly { get => Exporter.RenderSelectedOnly; }
+
+ ///
+ /// 背景颜色
+ ///
+ [Editor(typeof(SFMLColorEditor), typeof(UITypeEditor))]
+ [TypeConverter(typeof(SFMLColorConverter))]
+ [Category("[0] 导出"), DisplayName("背景颜色"), Description("要使用的背景色, 格式为 #RRGGBBAA")]
+ public SFML.Graphics.Color BackgroundColor { get => Exporter.BackgroundColor; set => Exporter.BackgroundColor = value; }
+ }
+}
diff --git a/SpineViewer/PropertyGridWrappers/Exporter/FFmpegVideoExporterWrapper.cs b/SpineViewer/PropertyGridWrappers/Exporter/FFmpegVideoExporterWrapper.cs
new file mode 100644
index 0000000..c9f5677
--- /dev/null
+++ b/SpineViewer/PropertyGridWrappers/Exporter/FFmpegVideoExporterWrapper.cs
@@ -0,0 +1,34 @@
+using SpineViewer.Exporter;
+using System;
+using System.Collections.Generic;
+using System.ComponentModel;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace SpineViewer.PropertyGridWrappers.Exporter
+{
+ public class FFmpegVideoExporterWrapper(FFmpegVideoExporter exporter) : VideoExporterWrapper(exporter)
+ {
+ [Browsable(false)]
+ public override FFmpegVideoExporter Exporter => (FFmpegVideoExporter)base.Exporter;
+
+ ///
+ /// 文件格式
+ ///
+ [Category("[2] FFmpeg 基本参数"), DisplayName("文件格式"), Description("-f, 文件格式")]
+ public string Format => Exporter.Format;
+
+ ///
+ /// 文件名后缀
+ ///
+ [Category("[2] FFmpeg 基本参数"), DisplayName("文件名后缀"), Description("文件名后缀")]
+ public string Suffix => Exporter.Format;
+
+ ///
+ /// 文件名后缀
+ ///
+ [Category("[2] FFmpeg 基本参数"), DisplayName("自定义参数"), Description("提供给 FFmpeg 的自定义参数, 除非很清楚自己在做什么, 否则请勿填写此参数")]
+ public string CustomArgument { get => Exporter.CustomArgument; set => Exporter.CustomArgument = value; }
+ }
+}
diff --git a/SpineViewer/PropertyGridWrappers/Exporter/FrameExporterWrapper.cs b/SpineViewer/PropertyGridWrappers/Exporter/FrameExporterWrapper.cs
new file mode 100644
index 0000000..ddf522d
--- /dev/null
+++ b/SpineViewer/PropertyGridWrappers/Exporter/FrameExporterWrapper.cs
@@ -0,0 +1,37 @@
+using SpineViewer.Exporter;
+using System;
+using System.Collections.Generic;
+using System.ComponentModel;
+using System.Drawing.Imaging;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace SpineViewer.PropertyGridWrappers.Exporter
+{
+ public class FrameExporterWrapper(FrameExporter exporter) : ExporterWrapper(exporter)
+ {
+ [Browsable(false)]
+ public override FrameExporter Exporter => (FrameExporter)base.Exporter;
+
+ ///
+ /// 单帧画面格式
+ ///
+ [TypeConverter(typeof(ImageFormatConverter))]
+ [Category("[1] 单帧画面"), DisplayName("图像格式")]
+ public ImageFormat ImageFormat { get => Exporter.ImageFormat; set => Exporter.ImageFormat = value; }
+
+ ///
+ /// 文件名后缀
+ ///
+ [Category("[1] 单帧画面"), DisplayName("文件名后缀"), Description("与图像格式匹配的文件名后缀")]
+ public string Suffix { get => Exporter.ImageFormat.GetSuffix(); }
+
+ ///
+ /// DPI
+ ///
+ [TypeConverter(typeof(SizeFConverter))]
+ [Category("[1] 单帧画面"), DisplayName("DPI"), Description("导出图像的每英寸像素数,用于调整图像的物理尺寸")]
+ public SizeF DPI { get => Exporter.DPI; set => Exporter.DPI = value; }
+ }
+}
diff --git a/SpineViewer/PropertyGridWrappers/Exporter/FrameSequenceExporterWrapper.cs b/SpineViewer/PropertyGridWrappers/Exporter/FrameSequenceExporterWrapper.cs
new file mode 100644
index 0000000..5e05570
--- /dev/null
+++ b/SpineViewer/PropertyGridWrappers/Exporter/FrameSequenceExporterWrapper.cs
@@ -0,0 +1,23 @@
+using SpineViewer.Exporter;
+using System;
+using System.Collections.Generic;
+using System.ComponentModel;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace SpineViewer.PropertyGridWrappers.Exporter
+{
+ public class FrameSequenceExporterWrapper(VideoExporter exporter) : VideoExporterWrapper(exporter)
+ {
+ [Browsable(false)]
+ public override FrameSequenceExporter Exporter => (FrameSequenceExporter)base.Exporter;
+
+ ///
+ /// 文件名后缀
+ ///
+ [TypeConverter(typeof(StringEnumConverter)), StringEnumConverter.StandardValues(".png", ".jpg", ".tga", ".bmp")]
+ [Category("[2] 帧序列参数"), DisplayName("文件名后缀"), Description("帧文件的后缀,同时决定帧图像格式")]
+ public string Suffix { get => Exporter.Suffix; set => Exporter.Suffix = value; }
+ }
+}
diff --git a/SpineViewer/PropertyGridWrappers/Exporter/GifExporterWrapper.cs b/SpineViewer/PropertyGridWrappers/Exporter/GifExporterWrapper.cs
new file mode 100644
index 0000000..fc1ba69
--- /dev/null
+++ b/SpineViewer/PropertyGridWrappers/Exporter/GifExporterWrapper.cs
@@ -0,0 +1,28 @@
+using SpineViewer.Exporter;
+using System;
+using System.Collections.Generic;
+using System.ComponentModel;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace SpineViewer.PropertyGridWrappers.Exporter
+{
+ class GifExporterWrapper(FFmpegVideoExporter exporter) : FFmpegVideoExporterWrapper(exporter)
+ {
+ [Browsable(false)]
+ public override GifExporter Exporter => (GifExporter)base.Exporter;
+
+ ///
+ /// 调色板最大颜色数量
+ ///
+ [Category("[3] 格式参数"), DisplayName("调色板最大颜色数量"), Description("设置调色板使用的最大颜色数量, 越多则色彩保留程度越高")]
+ public uint MaxColors { get => Exporter.MaxColors; set => Exporter.MaxColors = value; }
+
+ ///
+ /// 透明度阈值
+ ///
+ [Category("[3] 格式参数"), DisplayName("透明度阈值"), Description("小于该值的像素点会被认为是透明像素")]
+ public byte AlphaThreshold { get => Exporter.AlphaThreshold; set => Exporter.AlphaThreshold = value; }
+ }
+}
diff --git a/SpineViewer/PropertyGridWrappers/Exporter/MkvExporterWrapper.cs b/SpineViewer/PropertyGridWrappers/Exporter/MkvExporterWrapper.cs
new file mode 100644
index 0000000..5470098
--- /dev/null
+++ b/SpineViewer/PropertyGridWrappers/Exporter/MkvExporterWrapper.cs
@@ -0,0 +1,38 @@
+using SpineViewer.Exporter;
+using System;
+using System.Collections.Generic;
+using System.ComponentModel;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace SpineViewer.PropertyGridWrappers.Exporter
+{
+ public class MkvExporterWrapper(FFmpegVideoExporter exporter) : FFmpegVideoExporterWrapper(exporter)
+ {
+ [Browsable(false)]
+ public override MkvExporter Exporter => (MkvExporter)base.Exporter;
+
+ ///
+ /// 编码器
+ ///
+ [StringEnumConverter.StandardValues("libx264", "libx265", "libvpx-vp9", Customizable = true)]
+ [TypeConverter(typeof(StringEnumConverter))]
+ [Category("[3] 格式参数"), DisplayName("编码器"), Description("-c:v, 要使用的编码器")]
+ public string Codec { get => Exporter.Codec; set => Exporter.Codec = value; }
+
+ ///
+ /// CRF
+ ///
+ [Category("[3] 格式参数"), DisplayName("CRF"), Description("-crf, 取值范围 0-63, 建议范围 18-28, 默认取值 23, 数值越小则输出质量越高")]
+ public int CRF { get => Exporter.CRF; set => Exporter.CRF = value; }
+
+ ///
+ /// 像素格式
+ ///
+ [StringEnumConverter.StandardValues("yuv420p", "yuv422p", "yuv444p", "yuva420p", Customizable = true)]
+ [TypeConverter(typeof(StringEnumConverter))]
+ [Category("[3] 格式参数"), DisplayName("像素格式"), Description("-pix_fmt, 要使用的像素格式")]
+ public string PixelFormat { get => Exporter.PixelFormat; set => Exporter.PixelFormat = value; }
+ }
+}
diff --git a/SpineViewer/PropertyGridWrappers/Exporter/MovExporterWrapper.cs b/SpineViewer/PropertyGridWrappers/Exporter/MovExporterWrapper.cs
new file mode 100644
index 0000000..7bf3b68
--- /dev/null
+++ b/SpineViewer/PropertyGridWrappers/Exporter/MovExporterWrapper.cs
@@ -0,0 +1,40 @@
+using SpineViewer.Exporter;
+using System;
+using System.Collections.Generic;
+using System.ComponentModel;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace SpineViewer.PropertyGridWrappers.Exporter
+{
+ public class MovExporterWrapper(FFmpegVideoExporter exporter) : FFmpegVideoExporterWrapper(exporter)
+ {
+ [Browsable(false)]
+ public override MovExporter Exporter => (MovExporter)base.Exporter;
+
+ ///
+ /// 编码器
+ ///
+ [StringEnumConverter.StandardValues("prores_ks", Customizable = true)]
+ [TypeConverter(typeof(StringEnumConverter))]
+ [Category("[3] 格式参数"), DisplayName("编码器"), Description("-c:v, 要使用的编码器")]
+ public string Codec { get => Exporter.Codec; set => Exporter.Codec = value; }
+
+ ///
+ /// 预设
+ ///
+ [StringEnumConverter.StandardValues("auto", "proxy", "lt", "standard", "hq", "4444", "4444xq")]
+ [TypeConverter(typeof(StringEnumConverter))]
+ [Category("[3] 格式参数"), DisplayName("预设"), Description("-profile, 预设配置")]
+ public string Profile { get => Exporter.Profile; set => Exporter.Profile = value; }
+
+ ///
+ /// 像素格式
+ ///
+ [StringEnumConverter.StandardValues("yuv422p10le", "yuv444p10le", "yuva444p10le", Customizable = true)]
+ [TypeConverter(typeof(StringEnumConverter))]
+ [Category("[3] 格式参数"), DisplayName("像素格式"), Description("-pix_fmt, 要使用的像素格式")]
+ public string PixelFormat { get => Exporter.PixelFormat; set => Exporter.PixelFormat = value; }
+ }
+}
diff --git a/SpineViewer/PropertyGridWrappers/Exporter/Mp4ExporterWrapper.cs b/SpineViewer/PropertyGridWrappers/Exporter/Mp4ExporterWrapper.cs
new file mode 100644
index 0000000..be0529d
--- /dev/null
+++ b/SpineViewer/PropertyGridWrappers/Exporter/Mp4ExporterWrapper.cs
@@ -0,0 +1,38 @@
+using SpineViewer.Exporter;
+using System;
+using System.Collections.Generic;
+using System.ComponentModel;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace SpineViewer.PropertyGridWrappers.Exporter
+{
+ public class Mp4ExporterWrapper(FFmpegVideoExporter exporter) : FFmpegVideoExporterWrapper(exporter)
+ {
+ [Browsable(false)]
+ public override Mp4Exporter Exporter => (Mp4Exporter)base.Exporter;
+
+ ///
+ /// 编码器
+ ///
+ [StringEnumConverter.StandardValues("libx264", "libx265", Customizable = true)]
+ [TypeConverter(typeof(StringEnumConverter))]
+ [Category("[3] 格式参数"), DisplayName("编码器"), Description("-c:v, 要使用的编码器")]
+ public string Codec { get => Exporter.Codec; set => Exporter.Codec = value; }
+
+ ///
+ /// CRF
+ ///
+ [Category("[3] 格式参数"), DisplayName("CRF"), Description("-crf, 取值范围 0-63, 建议范围 18-28, 默认取值 23, 数值越小则输出质量越高")]
+ public int CRF { get => Exporter.CRF; set => Exporter.CRF = value; }
+
+ ///
+ /// 像素格式
+ ///
+ [StringEnumConverter.StandardValues("yuv420p", "yuv422p", "yuv444p", Customizable = true)]
+ [TypeConverter(typeof(StringEnumConverter))]
+ [Category("[3] 格式参数"), DisplayName("像素格式"), Description("-pix_fmt, 要使用的像素格式")]
+ public string PixelFormat { get => Exporter.PixelFormat; set => Exporter.PixelFormat = value; }
+ }
+}
diff --git a/SpineViewer/PropertyGridWrappers/Exporter/VideoExporterWrapper.cs b/SpineViewer/PropertyGridWrappers/Exporter/VideoExporterWrapper.cs
new file mode 100644
index 0000000..e9f14d9
--- /dev/null
+++ b/SpineViewer/PropertyGridWrappers/Exporter/VideoExporterWrapper.cs
@@ -0,0 +1,28 @@
+using SpineViewer.Exporter;
+using System;
+using System.Collections.Generic;
+using System.ComponentModel;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace SpineViewer.PropertyGridWrappers.Exporter
+{
+ public class VideoExporterWrapper(VideoExporter exporter) : ExporterWrapper(exporter)
+ {
+ [Browsable(false)]
+ public override VideoExporter Exporter => (VideoExporter)base.Exporter;
+
+ ///
+ /// 导出时长
+ ///
+ [Category("[1] 视频参数"), DisplayName("时长"), Description("可以从模型列表查看动画时长, 如果小于 0, 则在逐个导出时每个模型使用各自的所有轨道动画时长最大值")]
+ public float Duration { get => Exporter.Duration; set => Exporter.Duration = value; }
+
+ ///
+ /// 帧率
+ ///
+ [Category("[1] 视频参数"), DisplayName("帧率"), Description("每秒画面数")]
+ public float FPS { get => Exporter.FPS; set => Exporter.FPS = value; }
+ }
+}
diff --git a/SpineViewer/PropertyGridWrappers/Exporter/WebmExporterWrapper.cs b/SpineViewer/PropertyGridWrappers/Exporter/WebmExporterWrapper.cs
new file mode 100644
index 0000000..12e3cb0
--- /dev/null
+++ b/SpineViewer/PropertyGridWrappers/Exporter/WebmExporterWrapper.cs
@@ -0,0 +1,38 @@
+using SpineViewer.Exporter;
+using System;
+using System.Collections.Generic;
+using System.ComponentModel;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace SpineViewer.PropertyGridWrappers.Exporter
+{
+ public class WebmExporterWrapper(WebmExporter exporter) : FFmpegVideoExporterWrapper(exporter)
+ {
+ [Browsable(false)]
+ public override WebmExporter Exporter => (WebmExporter)base.Exporter;
+
+ ///
+ /// 编码器
+ ///
+ [StringEnumConverter.StandardValues("libvpx-vp9", Customizable = true)]
+ [TypeConverter(typeof(StringEnumConverter))]
+ [Category("[3] 格式参数"), DisplayName("编码器"), Description("-c:v, 要使用的编码器")]
+ public string Codec { get => Exporter.Codec; set => Exporter.Codec = value; }
+
+ ///
+ /// CRF
+ ///
+ [Category("[3] 格式参数"), DisplayName("CRF"), Description("-crf, 取值范围 0-63, 建议范围 18-28, 默认取值 23, 数值越小则输出质量越高")]
+ public int CRF { get => Exporter.CRF; set => Exporter.CRF = value; }
+
+ ///
+ /// 像素格式
+ ///
+ [StringEnumConverter.StandardValues("yuv420p", "yuv422p", "yuv444p", "yuva420p", Customizable = true)]
+ [TypeConverter(typeof(StringEnumConverter))]
+ [Category("[3] 格式参数"), DisplayName("像素格式"), Description("-pix_fmt, 要使用的像素格式")]
+ public string PixelFormat { get => Exporter.PixelFormat; set => Exporter.PixelFormat = value; }
+ }
+}
diff --git a/SpineViewer/Spine/AnimationTracks.cs b/SpineViewer/PropertyGridWrappers/Spine/SpineAnimationWrapper.cs
similarity index 50%
rename from SpineViewer/Spine/AnimationTracks.cs
rename to SpineViewer/PropertyGridWrappers/Spine/SpineAnimationWrapper.cs
index ec8c47c..eda45bd 100644
--- a/SpineViewer/Spine/AnimationTracks.cs
+++ b/SpineViewer/PropertyGridWrappers/Spine/SpineAnimationWrapper.cs
@@ -1,21 +1,22 @@
-using System;
+using SpineViewer.Spine;
+using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
-namespace SpineViewer.Spine
+namespace SpineViewer.PropertyGridWrappers.Spine
{
///
- /// 对轨道索引的包装类, 能够在面板上显示例如时长的属性, 但是处理该属性时按字符串去处理, 例如 ToString 和判断对象相等都是用动画名称实现逻辑
+ /// 对轨道索引属性的包装类, 能够在面板上显示例如时长的属性, 但是处理该属性时按字符串去处理, 例如 ToString 和判断对象相等都是用动画名称实现逻辑
///
///
///
[TypeConverter(typeof(TrackWrapperConverter))]
- public class TrackWrapper(Spine spine, int i)
+ public class TrackWrapper(SpineViewer.Spine.Spine spine, int i)
{
- private readonly Spine spine = spine;
+ private readonly SpineViewer.Spine.Spine spine = spine;
[Browsable(false)]
public int Index { get; } = i;
@@ -40,60 +41,103 @@ namespace SpineViewer.Spine
///
/// 哈希码需要和 Equals 行为类似
///
- public override int GetHashCode() => (typeof(TrackWrapper).FullName + ToString()).GetHashCode();
+ public override int GetHashCode() => HashCode.Combine(typeof(TrackWrapper).FullName.GetHashCode(), ToString().GetHashCode());
}
///
- /// 轨道属性描述符, 实现对属性的读取和赋值
+ /// 用于在 PropertyGrid 上显示 Spine 动画列表的包装类
///
- /// 轨道索引
- public class TrackWrapperPropertyDescriptor(int i) : PropertyDescriptor($"Track{i}", [new DisplayNameAttribute($"轨道 {i}")])
+ public class SpineAnimationWrapper(SpineViewer.Spine.Spine spine) : ICustomTypeDescriptor
{
- private readonly int idx = i;
-
- public override Type ComponentType => typeof(AnimationTracks);
- public override bool IsReadOnly => false;
- public override Type PropertyType => typeof(TrackWrapper);
- public override bool CanResetValue(object component) => false;
- public override void ResetValue(object component) { }
- public override bool ShouldSerializeValue(object component) => false;
-
///
- /// 得到一个轨道包装类, 允许用户查看或者修改具体的属性值, 这个地方决定了在面板上看到的是一个对象及其属性
+ /// 轨道属性描述符, 实现对属性的读取和赋值
///
- public override object? GetValue(object? component)
+ /// 轨道索引
+ private class TrackWrapperPropertyDescriptor(int i, Attribute[]? attributes) : PropertyDescriptor($"Track{i}", attributes)
{
- if (component is AnimationTracks tracks)
- return tracks.GetTrackWrapper(idx);
- return null;
- }
+ private readonly int idx = i;
- ///
- /// 允许通过字符串赋值修改该轨道的动画, 这里决定了当其他地方的调用 (比如 Converter) 通过 value 来设置属性值的时候应该怎么处理
- ///
- public override void SetValue(object? component, object? value)
- {
- if (component is AnimationTracks tracks)
+ public override Type ComponentType => typeof(SpineAnimationWrapper);
+ public override bool IsReadOnly => false;
+ public override Type PropertyType => typeof(TrackWrapper);
+ public override bool CanResetValue(object component) => false;
+ public override void ResetValue(object component) { }
+ public override bool ShouldSerializeValue(object component) => false;
+
+ ///
+ /// 得到一个轨道包装类, 允许用户查看或者修改具体的属性值, 这个地方决定了在面板上看到的是一个对象及其属性
+ ///
+ public override object? GetValue(object? component)
{
- if (value is string s)
- tracks.Spine.SetAnimation(idx, s); // tracks.SetTrackWrapper(idx, s);
+ if (component is SpineAnimationWrapper tracks)
+ return tracks.GetTrackWrapper(idx);
+ return null;
+ }
+
+ ///
+ /// 允许通过字符串赋值修改该轨道的动画, 这里决定了当其他地方的调用 (比如 Converter) 通过 value 来设置属性值的时候应该怎么处理
+ ///
+ public override void SetValue(object? component, object? value)
+ {
+ if (component is SpineAnimationWrapper tracks)
+ {
+ if (value is string s)
+ tracks.SetTrackWrapper(idx, s);
+ }
}
}
- }
- ///
- /// AnimationTracks 动态类型包装类, 用于提供对 Spine 对象多轨道动画的访问能力, 不同轨道将动态生成属性
- ///
- /// 关联的 Spine 对象
- public class AnimationTracks(Spine spine) : ICustomTypeDescriptor
- {
- private static readonly Dictionary pdCache = [];
+ [Browsable(false)]
+ public SpineViewer.Spine.Spine Spine { get; } = spine;
- public Spine Spine { get; } = spine;
+ ///
+ /// 全轨道动画最大时长
+ ///
+ [DisplayName("全轨道最大时长")]
+ public float AnimationTracksMaxDuration => Spine.GetTrackIndices().Select(i => Spine.GetAnimationDuration(Spine.GetAnimation(i))).Max();
+
+ ///
+ /// TrackWrapper 属性对象缓存
+ ///
private readonly Dictionary trackWrapperProperties = [];
+ ///
+ /// 访问 TrackWrapper 属性 AnimationTracks.Track{i}
+ ///
+ public TrackWrapper GetTrackWrapper(int i)
+ {
+ if (!trackWrapperProperties.ContainsKey(i))
+ trackWrapperProperties[i] = new TrackWrapper(Spine, i);
+ return trackWrapperProperties[i];
+ }
+
+ ///
+ /// 设置 TrackWrapper 属性 AnimationTracks.Track{i} =
+ ///
+ public void SetTrackWrapper(int i, string value) => Spine.SetAnimation(i, value);
+
+ ///
+ /// 在属性面板悬停可以按轨道顺序显示动画名称
+ ///
+ public override string ToString() => $"[{string.Join(", ", Spine.GetTrackIndices().Select(Spine.GetAnimation))}]";
+
+ public override bool Equals(object? obj)
+ {
+ if (obj is SpineAnimationWrapper wrapper) return ToString() == wrapper.ToString();
+ return base.Equals(obj);
+ }
+
+ public override int GetHashCode() => HashCode.Combine(typeof(SpineAnimationWrapper).FullName.GetHashCode(), ToString().GetHashCode());
+
+ #region ICustomTypeDescriptor 接口实现
+
// XXX: 必须实现 ICustomTypeDescriptor 接口, 不能继承 CustomTypeDescriptor, 似乎继承下来的东西会有问题, 导致某些调用不正确
+ ///
+ /// 属性描述符缓存
+ ///
+ private static readonly Dictionary pdCache = [];
+
public AttributeCollection GetAttributes() => TypeDescriptor.GetAttributes(this, true);
public string? GetClassName() => TypeDescriptor.GetClassName(this, true);
public string? GetComponentName() => TypeDescriptor.GetComponentName(this, true);
@@ -107,37 +151,16 @@ namespace SpineViewer.Spine
public PropertyDescriptorCollection GetProperties() => GetProperties(null);
public PropertyDescriptorCollection GetProperties(Attribute[]? attributes)
{
- var props = new List();
+ var props = new PropertyDescriptorCollection(TypeDescriptor.GetProperties(this, attributes, true).Cast().ToArray());
foreach (var i in Spine.GetTrackIndices())
{
if (!pdCache.ContainsKey(i))
- pdCache[i] = new TrackWrapperPropertyDescriptor(i);
+ pdCache[i] = new TrackWrapperPropertyDescriptor(i, [new DisplayNameAttribute($"轨道 {i}")]);
props.Add(pdCache[i]);
}
- return new PropertyDescriptorCollection(props.ToArray());
+ return props;
}
- ///
- /// 访问 TrackWrapper 属性 AnimationTracks.Track{i}
- ///
- public TrackWrapper GetTrackWrapper(int i)
- {
- if (!trackWrapperProperties.ContainsKey(i))
- trackWrapperProperties[i] = new TrackWrapper(Spine, i);
- return trackWrapperProperties[i];
- }
-
- ///
- /// 在属性面板悬停可以按轨道顺序显示动画名称
- ///
- public override string ToString() => $"[{string.Join(", ", Spine.GetTrackIndices().Select(Spine.GetAnimation))}]";
-
- public override bool Equals(object? obj)
- {
- if (obj is AnimationTracks tracks) return ToString() == tracks.ToString();
- return base.Equals(obj);
- }
-
- public override int GetHashCode() => (typeof(AnimationTracks).FullName + ToString()).GetHashCode();
+ #endregion
}
}
diff --git a/SpineViewer/PropertyGridWrappers/Spine/SpineBaseInfoWrapper.cs b/SpineViewer/PropertyGridWrappers/Spine/SpineBaseInfoWrapper.cs
new file mode 100644
index 0000000..18feccb
--- /dev/null
+++ b/SpineViewer/PropertyGridWrappers/Spine/SpineBaseInfoWrapper.cs
@@ -0,0 +1,56 @@
+using SpineViewer.Spine;
+using System;
+using System.Collections.Generic;
+using System.ComponentModel;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace SpineViewer.PropertyGridWrappers.Spine
+{
+ ///
+ /// 用于在 PropertyGrid 上显示 Spine 基本信息的包装类
+ ///
+ public class SpineBaseInfoWrapper(SpineViewer.Spine.Spine spine)
+ {
+ [Browsable(false)]
+ public SpineViewer.Spine.Spine Spine { get; } = spine;
+
+ ///
+ /// 获取所属版本
+ ///
+ [TypeConverter(typeof(SpineVersionConverter))]
+ [DisplayName("运行时版本")]
+ public SpineVersion Version => Spine.Version;
+
+ ///
+ /// 资源所在完整目录
+ ///
+ [DisplayName("资源目录")]
+ public string AssetsDir => Spine.AssetsDir;
+
+ ///
+ /// skel 文件完整路径
+ ///
+ [DisplayName("skel文件路径")]
+ public string SkelPath => Spine.SkelPath;
+
+ ///
+ /// atlas 文件完整路径
+ ///
+ [DisplayName("atlas文件路径")]
+ public string AtlasPath => Spine.AtlasPath;
+
+ ///
+ /// 名称
+ ///
+ [DisplayName("名称")]
+ public string Name => Spine.Name;
+
+ ///
+ /// 获取所属文件版本
+ ///
+ [DisplayName("文件版本")]
+ public string FileVersion => Spine.FileVersion;
+ }
+}
diff --git a/SpineViewer/PropertyGridWrappers/Spine/SpineDebugWrapper.cs b/SpineViewer/PropertyGridWrappers/Spine/SpineDebugWrapper.cs
new file mode 100644
index 0000000..2182e81
--- /dev/null
+++ b/SpineViewer/PropertyGridWrappers/Spine/SpineDebugWrapper.cs
@@ -0,0 +1,36 @@
+using System;
+using System.Collections.Generic;
+using System.ComponentModel;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace SpineViewer.PropertyGridWrappers.Spine
+{
+ ///
+ /// 用于在 PropertyGrid 上显示 Spine 调试属性的包装类
+ ///
+ public class SpineDebugWrapper(SpineViewer.Spine.Spine spine)
+ {
+ [Browsable(false)]
+ public SpineViewer.Spine.Spine Spine { get; } = spine;
+
+ ///
+ /// 显示纹理
+ ///
+ [DisplayName("纹理")]
+ public bool DebugTexture { get => Spine.DebugTexture; set => Spine.DebugTexture = value; }
+
+ ///
+ /// 显示包围盒
+ ///
+ [DisplayName("包围盒")]
+ public bool DebugBounds { get => Spine.DebugBounds; set => Spine.DebugBounds = value; }
+
+ ///
+ /// 显示骨骼
+ ///
+ [DisplayName("骨架")]
+ public bool DebugBones { get => Spine.DebugBones; set => Spine.DebugBones = value; }
+ }
+}
diff --git a/SpineViewer/PropertyGridWrappers/Spine/SpineRenderWrapper.cs b/SpineViewer/PropertyGridWrappers/Spine/SpineRenderWrapper.cs
new file mode 100644
index 0000000..11f3619
--- /dev/null
+++ b/SpineViewer/PropertyGridWrappers/Spine/SpineRenderWrapper.cs
@@ -0,0 +1,30 @@
+using System;
+using System.Collections.Generic;
+using System.ComponentModel;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace SpineViewer.PropertyGridWrappers.Spine
+{
+ ///
+ /// 用于在 PropertyGrid 上显示 Spine 渲染设置的包装类
+ ///
+ public class SpineRenderWrapper(SpineViewer.Spine.Spine spine)
+ {
+ [Browsable(false)]
+ public SpineViewer.Spine.Spine Spine { get; } = spine;
+
+ ///
+ /// 是否被隐藏, 被隐藏的模型将仅仅在列表显示, 不参与其他行为
+ ///
+ [DisplayName("是否隐藏")]
+ public bool IsHidden { get => Spine.IsHidden; set => Spine.IsHidden = value; }
+
+ ///
+ /// 是否使用预乘Alpha
+ ///
+ [DisplayName("预乘Alpha通道")]
+ public bool UsePremultipliedAlpha { get => Spine.UsePma; set => Spine.UsePma = value; }
+ }
+}
diff --git a/SpineViewer/Spine/SkinManager.cs b/SpineViewer/PropertyGridWrappers/Spine/SpineSkinWrapper.cs
similarity index 53%
rename from SpineViewer/Spine/SkinManager.cs
rename to SpineViewer/PropertyGridWrappers/Spine/SpineSkinWrapper.cs
index 7ca9331..18df3bc 100644
--- a/SpineViewer/Spine/SkinManager.cs
+++ b/SpineViewer/PropertyGridWrappers/Spine/SpineSkinWrapper.cs
@@ -1,28 +1,29 @@
-using System;
+using SpineViewer.Spine;
+using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
-namespace SpineViewer.Spine
+namespace SpineViewer.PropertyGridWrappers.Spine
{
///
/// 对皮肤的包装类
///
[TypeConverter(typeof(SkinWrapperConverter))]
- public class SkinWrapper(Spine spine, int i)
+ public class SkinWrapper(SpineViewer.Spine.Spine spine, int i)
{
- private readonly Spine spine = spine;
+ private readonly SpineViewer.Spine.Spine spine = spine;
[Browsable(false)]
public int Index { get; } = i;
- public override string ToString()
+ public override string ToString()
{
var loadedSkins = spine.GetLoadedSkins();
- if (Index >= 0 && Index < loadedSkins.Length)
- return loadedSkins[Index];
+ if (Index >= 0 && Index < loadedSkins.Length)
+ return loadedSkins[Index];
return "!NULL"; // XXX: 预期应该不会发生
}
@@ -32,60 +33,94 @@ namespace SpineViewer.Spine
return base.Equals(obj);
}
- public override int GetHashCode() => (typeof(SkinWrapper).FullName + ToString()).GetHashCode();
- }
-
- ///
- /// 皮肤属性描述符, 实现对属性的读取和赋值
- ///
- /// 关联的 Spine 对象
- public class SkinWrapperPropertyDescriptor(int i) : PropertyDescriptor($"Skin{i}", [new DisplayNameAttribute($"皮肤 {i}")])
- {
- private readonly int idx = i;
-
- public override Type ComponentType => typeof(SkinManager);
- public override bool IsReadOnly => false;
- public override Type PropertyType => typeof(SkinWrapper);
- public override bool CanResetValue(object component) => false;
- public override void ResetValue(object component) { }
- public override bool ShouldSerializeValue(object component) => false;
-
- ///
- /// 得到一个 SkinWrapper, 允许用户查看或者修改具体的属性值, 这个地方决定了在面板上看到的是一个对象及其属性
- ///
- public override object? GetValue(object? component)
- {
- if (component is SkinManager manager)
- return manager.GetSkinWrapper(idx);
- return null;
- }
-
- ///
- /// 允许通过字符串赋值修改该位置的皮肤
- ///
- public override void SetValue(object? component, object? value)
- {
- if (component is SkinManager manager)
- {
- if (value is string s)
- manager.Spine.ReplaceSkin(idx, s); // manager.SetSkinWrapper(idx, s);
- }
- }
+ public override int GetHashCode() => HashCode.Combine(typeof(SkinWrapper).FullName.GetHashCode(), ToString().GetHashCode());
}
///
/// SkinManager 动态类型包装类, 用于提供对 Spine 皮肤列表的管理能力
///
/// 关联的 Spine 对象
- public class SkinManager(Spine spine) : ICustomTypeDescriptor
+ public class SpineSkinWrapper(SpineViewer.Spine.Spine spine) : ICustomTypeDescriptor
{
- private static readonly Dictionary pdCache = [];
+ ///
+ /// 皮肤属性描述符, 实现对属性的读取和赋值
+ ///
+ private class SkinWrapperPropertyDescriptor(int i, Attribute[]? attributes) : PropertyDescriptor($"Skin{i}", attributes)
+ {
+ private readonly int idx = i;
- public Spine Spine { get; } = spine;
+ public override Type ComponentType => typeof(SpineSkinWrapper);
+ public override bool IsReadOnly => false;
+ public override Type PropertyType => typeof(SkinWrapper);
+ public override bool CanResetValue(object component) => false;
+ public override void ResetValue(object component) { }
+ public override bool ShouldSerializeValue(object component) => false;
+
+ ///
+ /// 得到一个 SkinWrapper, 允许用户查看或者修改具体的属性值, 这个地方决定了在面板上看到的是一个对象及其属性
+ ///
+ public override object? GetValue(object? component)
+ {
+ if (component is SpineSkinWrapper manager)
+ return manager.GetSkinWrapper(idx);
+ return null;
+ }
+
+ ///
+ /// 允许通过字符串赋值修改该位置的皮肤
+ ///
+ public override void SetValue(object? component, object? value)
+ {
+ if (component is SpineSkinWrapper manager)
+ {
+ if (value is string s)
+ manager.SetSkinWrapper(idx, s);
+ }
+ }
+ }
+
+ [Browsable(false)]
+ public SpineViewer.Spine.Spine Spine { get; } = spine;
+
+ ///
+ /// SkinWrapper 属性缓存
+ ///
private readonly Dictionary skinWrapperProperties = [];
+ ///
+ /// 访问 SkinWrapper 属性 SkinManager.Skin{i}
+ ///
+ public SkinWrapper GetSkinWrapper(int i)
+ {
+ if (!skinWrapperProperties.ContainsKey(i))
+ skinWrapperProperties[i] = new SkinWrapper(Spine, i);
+ return skinWrapperProperties[i];
+ }
+
+ ///
+ /// 设置 SkinWrapper 属性 SkinManager.Skin{i} =
+ ///
+ public void SetSkinWrapper(int i, string value) => Spine.ReplaceSkin(i, value);
+
+ ///
+ /// 在属性面板悬停可以显示已加载的皮肤列表
+ ///
+ public override string ToString() => $"[{string.Join(", ", Spine.GetLoadedSkins())}]";
+
+ public override bool Equals(object? obj)
+ {
+ if (obj is SpineSkinWrapper wrapper) return ToString() == wrapper.ToString();
+ return base.Equals(obj);
+ }
+
+ public override int GetHashCode() => HashCode.Combine(typeof(SpineSkinWrapper).FullName.GetHashCode(), ToString().GetHashCode());
+
+ #region ICustomTypeDescriptor 接口实现
+
// XXX: 必须实现 ICustomTypeDescriptor 接口, 不能继承 CustomTypeDescriptor, 似乎继承下来的东西会有问题, 导致某些调用不正确
+ private static readonly Dictionary pdCache = [];
+
public AttributeCollection GetAttributes() => TypeDescriptor.GetAttributes(this, true);
public string? GetClassName() => TypeDescriptor.GetClassName(this, true);
public string? GetComponentName() => TypeDescriptor.GetComponentName(this, true);
@@ -99,37 +134,16 @@ namespace SpineViewer.Spine
public PropertyDescriptorCollection GetProperties() => GetProperties(null);
public PropertyDescriptorCollection GetProperties(Attribute[]? attributes)
{
- var props = new List();
+ var props = new PropertyDescriptorCollection(TypeDescriptor.GetProperties(this, attributes, true).Cast().ToArray());
for (var i = 0; i < Spine.GetLoadedSkins().Length; i++)
{
if (!pdCache.ContainsKey(i))
- pdCache[i] = new SkinWrapperPropertyDescriptor(i);
+ pdCache[i] = new SkinWrapperPropertyDescriptor(i, [new DisplayNameAttribute($"皮肤 {i}")]);
props.Add(pdCache[i]);
}
- return new PropertyDescriptorCollection(props.ToArray());
+ return props;
}
- ///
- /// 访问 SkinWrapper 属性 SkinManager.Skin{i}
- ///
- public SkinWrapper GetSkinWrapper(int i)
- {
- if (!skinWrapperProperties.ContainsKey(i))
- skinWrapperProperties[i] = new SkinWrapper(Spine, i);
- return skinWrapperProperties[i];
- }
-
- ///
- /// 在属性面板悬停可以显示已加载的皮肤列表
- ///
- public override string ToString() => $"[{string.Join(", ", Spine.GetLoadedSkins())}]";
-
- public override bool Equals(object? obj)
- {
- if (obj is SkinManager manager) return ToString() == manager.ToString();
- return base.Equals(obj);
- }
-
- public override int GetHashCode() => (typeof(SkinManager).FullName + ToString()).GetHashCode();
+ #endregion
}
}
diff --git a/SpineViewer/PropertyGridWrappers/Spine/SpineTransformWrapper.cs b/SpineViewer/PropertyGridWrappers/Spine/SpineTransformWrapper.cs
new file mode 100644
index 0000000..7de63e7
--- /dev/null
+++ b/SpineViewer/PropertyGridWrappers/Spine/SpineTransformWrapper.cs
@@ -0,0 +1,43 @@
+using System;
+using System.Collections.Generic;
+using System.ComponentModel;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace SpineViewer.PropertyGridWrappers.Spine
+{
+ ///
+ /// 用于在 PropertyGrid 上显示 Spine 空间变换的包装类
+ ///
+ public class SpineTransformWrapper(SpineViewer.Spine.Spine spine)
+ {
+ [Browsable(false)]
+ public SpineViewer.Spine.Spine Spine { get; } = spine;
+
+ ///
+ /// 缩放比例
+ ///
+ [DisplayName("缩放比例")]
+ public float Scale { get => Spine.Scale; set => Spine.Scale = value; }
+
+ ///
+ /// 位置
+ ///
+ [TypeConverter(typeof(PointFConverter))]
+ [DisplayName("位置")]
+ public PointF Position { get => Spine.Position; set => Spine.Position = value; }
+
+ ///
+ /// 水平翻转
+ ///
+ [DisplayName("水平翻转")]
+ public bool FlipX { get => Spine.FlipX; set => Spine.FlipX = value; }
+
+ ///
+ /// 垂直翻转
+ ///
+ [DisplayName("垂直翻转")]
+ public bool FlipY { get => Spine.FlipY; set => Spine.FlipY = value; }
+ }
+}
diff --git a/SpineViewer/PropertyGridWrappers/Spine/SpineWrapper.cs b/SpineViewer/PropertyGridWrappers/Spine/SpineWrapper.cs
new file mode 100644
index 0000000..91f82ad
--- /dev/null
+++ b/SpineViewer/PropertyGridWrappers/Spine/SpineWrapper.cs
@@ -0,0 +1,38 @@
+using System;
+using System.Collections.Generic;
+using System.ComponentModel;
+using System.Drawing.Design;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace SpineViewer.PropertyGridWrappers.Spine
+{
+ public class SpineWrapper(SpineViewer.Spine.Spine spine)
+ {
+ [Browsable(false)]
+ public SpineViewer.Spine.Spine Spine { get; } = spine;
+
+ [TypeConverter(typeof(ExpandableObjectConverter))]
+ [DisplayName("基本信息")]
+ public SpineBaseInfoWrapper BaseInfo { get; } = new(spine);
+
+ [TypeConverter(typeof(ExpandableObjectConverter))]
+ [DisplayName("渲染")]
+ public SpineRenderWrapper Render { get; } = new(spine);
+
+ [TypeConverter(typeof(ExpandableObjectConverter))]
+ [DisplayName("变换")]
+ public SpineTransformWrapper Transform { get; } = new(spine);
+
+ [Editor(typeof(SpineSkinEditor), typeof(UITypeEditor))]
+ [TypeConverter(typeof(ExpandableObjectConverter))]
+ [DisplayName("皮肤")]
+ public SpineSkinWrapper Skin { get; } = new(spine);
+
+ [Editor(typeof(SpineAnimationEditor), typeof(UITypeEditor))]
+ [TypeConverter(typeof(ExpandableObjectConverter))]
+ [DisplayName("动画")]
+ public SpineAnimationWrapper Animation { get; } = new(spine);
+ }
+}
diff --git a/SpineViewer/PropertyGridWrappers/SpinePreviewerWrapper.cs b/SpineViewer/PropertyGridWrappers/SpinePreviewerWrapper.cs
new file mode 100644
index 0000000..a833123
--- /dev/null
+++ b/SpineViewer/PropertyGridWrappers/SpinePreviewerWrapper.cs
@@ -0,0 +1,48 @@
+using SpineViewer.Controls;
+using System;
+using System.Collections.Generic;
+using System.ComponentModel;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace SpineViewer.PropertyGridWrappers
+{
+ ///
+ /// 用于在 PropertyGrid 上显示 SpinePreviewe 属性的包装类
+ ///
+ public class SpinePreviewerWrapper(SpinePreviewer previewer)
+ {
+ [Browsable(false)]
+ public SpinePreviewer Previewer { get; } = previewer;
+
+ [TypeConverter(typeof(SizeConverter))]
+ [Category("[0] 导出"), DisplayName("分辨率")]
+ public Size Resolution { get => Previewer.Resolution; set => Previewer.Resolution = value; }
+
+ [TypeConverter(typeof(PointFConverter))]
+ [Category("[0] 导出"), DisplayName("画面中心点")]
+ public PointF Center { get => Previewer.Center; set => Previewer.Center = value; }
+
+ [Category("[0] 导出"), DisplayName("缩放")]
+ public float Zoom { get => Previewer.Zoom; set => Previewer.Zoom = value; }
+
+ [Category("[0] 导出"), DisplayName("旋转")]
+ public float Rotation { get => Previewer.Rotation; set => Previewer.Rotation = value; }
+
+ [Category("[0] 导出"), DisplayName("水平翻转")]
+ public bool FlipX { get => Previewer.FlipX; set => Previewer.FlipX = value; }
+
+ [Category("[0] 导出"), DisplayName("垂直翻转")]
+ public bool FlipY { get => Previewer.FlipY; set => Previewer.FlipY = value; }
+
+ [Category("[0] 导出"), DisplayName("仅渲染选中")]
+ public bool RenderSelectedOnly { get => Previewer.RenderSelectedOnly; set => Previewer.RenderSelectedOnly = value; }
+
+ [Category("[1] 预览"), DisplayName("显示坐标轴")]
+ public bool ShowAxis { get => Previewer.ShowAxis; set => Previewer.ShowAxis = value; }
+
+ [Category("[1] 预览"), DisplayName("最大帧率")]
+ public uint MaxFps { get => Previewer.MaxFps; set => Previewer.MaxFps = value; }
+ }
+}
diff --git a/SpineViewer/PropertyGridWrappers/TypeConverter.cs b/SpineViewer/PropertyGridWrappers/TypeConverter.cs
new file mode 100644
index 0000000..c951e1a
--- /dev/null
+++ b/SpineViewer/PropertyGridWrappers/TypeConverter.cs
@@ -0,0 +1,307 @@
+using SpineViewer.PropertyGridWrappers.Spine;
+using SpineViewer.Spine;
+using System;
+using System.Collections.Generic;
+using System.Collections.ObjectModel;
+using System.ComponentModel;
+using System.Diagnostics.CodeAnalysis;
+using System.Globalization;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace SpineViewer.PropertyGridWrappers
+{
+ public class PointFConverter : ExpandableObjectConverter
+ {
+ public override bool CanConvertTo(ITypeDescriptorContext? context, [NotNullWhen(true)] Type? destinationType)
+ {
+ return destinationType == typeof(string) || base.CanConvertTo(context, destinationType);
+ }
+
+ public override object? ConvertTo(ITypeDescriptorContext? context, CultureInfo? culture, object? value, Type destinationType)
+ {
+ if (destinationType == typeof(string) && value is PointF point)
+ {
+ return $"{point.X}, {point.Y}";
+ }
+ return base.ConvertTo(context, culture, value, destinationType);
+ }
+
+ public override bool CanConvertFrom(ITypeDescriptorContext? context, Type sourceType)
+ {
+ return sourceType == typeof(string) || base.CanConvertFrom(context, sourceType);
+ }
+
+ public override object? ConvertFrom(ITypeDescriptorContext? context, CultureInfo? culture, object value)
+ {
+ if (value is string str)
+ {
+ var parts = str.Split(',');
+ if (parts.Length == 2 &&
+ float.TryParse(parts[0], out var x) &&
+ float.TryParse(parts[1], out var y))
+ {
+ return new PointF(x, y);
+ }
+ }
+ return base.ConvertFrom(context, culture, value);
+ }
+ }
+
+ public class StringEnumConverter : StringConverter
+ {
+ ///
+ /// 字符串标准值列表属性
+ ///
+ [AttributeUsage(AttributeTargets.Property, AllowMultiple = false, Inherited = false)]
+ public class StandardValuesAttribute : Attribute
+ {
+ ///
+ /// 标准值列表
+ ///
+ public ReadOnlyCollection StandardValues { get; private set; }
+ private readonly List standardValues = [];
+
+ ///
+ /// 是否允许用户自定义
+ ///
+ public bool Customizable { get; set; } = false;
+
+ ///
+ /// 字符串标准值列表
+ ///
+ /// 允许的字符串标准值
+ 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().FirstOrDefault()?.Customizable ?? false;
+ return !customizable;
+ }
+
+ public override StandardValuesCollection? GetStandardValues(ITypeDescriptorContext? context)
+ {
+ // 查找属性上的 StandardValuesAttribute
+ var attribute = context?.PropertyDescriptor?.Attributes.OfType().FirstOrDefault();
+ StandardValuesCollection result;
+ if (attribute != null)
+ result = new StandardValuesCollection(attribute.StandardValues);
+ else
+ result = new StandardValuesCollection(Array.Empty());
+ return result;
+ }
+ }
+
+ public class SpineVersionConverter : EnumConverter
+ {
+ public SpineVersionConverter() : base(typeof(SpineVersion)) { }
+
+ public override object? ConvertTo(ITypeDescriptorContext? context, CultureInfo? culture, object? value, Type? destinationType)
+ {
+ if (destinationType == typeof(string) && value is SpineVersion version)
+ return version.GetName();
+ return base.ConvertTo(context, culture, value, destinationType);
+ }
+ }
+
+ public class SpineSkinNameConverter : StringConverter
+ {
+ public override bool GetStandardValuesSupported(ITypeDescriptorContext? context) => true;
+
+ public override bool GetStandardValuesExclusive(ITypeDescriptorContext? context) => true;
+
+ public override StandardValuesCollection? GetStandardValues(ITypeDescriptorContext? context)
+ {
+ if (context?.Instance is SpineViewer.Spine.Spine obj)
+ {
+ return new StandardValuesCollection(obj.SkinNames);
+ }
+ else if (context?.Instance is SpineViewer.Spine.Spine[] spines)
+ {
+ if (spines.Length > 0)
+ {
+ IEnumerable common = spines[0].SkinNames;
+ foreach (var spine in spines.Skip(1))
+ common = common.Union(spine.SkinNames);
+ return new StandardValuesCollection(common.ToArray());
+ }
+ }
+ return base.GetStandardValues(context);
+ }
+ }
+
+ public class SpineAnimationNameConverter : StringConverter
+ {
+ public override bool GetStandardValuesSupported(ITypeDescriptorContext? context) => true;
+
+ public override bool GetStandardValuesExclusive(ITypeDescriptorContext? context) => true;
+
+ public override StandardValuesCollection? GetStandardValues(ITypeDescriptorContext? context)
+ {
+ if (context?.Instance is SpineViewer.Spine.Spine obj)
+ {
+ return new StandardValuesCollection(obj.AnimationNames);
+ }
+ else if (context?.Instance is SpineViewer.Spine.Spine[] spines)
+ {
+ if (spines.Length > 0)
+ {
+ IEnumerable common = spines[0].AnimationNames;
+ foreach (var spine in spines.Skip(1))
+ common = common.Union(spine.AnimationNames);
+ return new StandardValuesCollection(common.ToArray());
+ }
+ }
+ return base.GetStandardValues(context);
+ }
+ }
+
+ ///
+ /// 皮肤位包装类转换器, 实现字符串和包装类的互相转换, 并且提供标准值列表对属性进行设置, 同时还提供在面板上显示包装类属性的能力
+ ///
+ public class SkinWrapperConverter : StringConverter
+ {
+ // NOTE: 可以不用实现 ConvertTo/ConvertFrom, 因为属性实现了与字符串之间的互转
+ // ToString 实现了 ConvertTo
+ // SetValue 实现了从字符串设置属性
+
+ public override bool GetStandardValuesSupported(ITypeDescriptorContext? context) => true;
+
+ public override bool GetStandardValuesExclusive(ITypeDescriptorContext? context) => true;
+
+ public override StandardValuesCollection? GetStandardValues(ITypeDescriptorContext? context)
+ {
+ if (context?.Instance is SpineSkinWrapper manager)
+ {
+ return new StandardValuesCollection(manager.Spine.SkinNames);
+ }
+ else if (context?.Instance is object[] instances && instances.All(x => x is SpineSkinWrapper))
+ {
+ // XXX: 这里不知道为啥总是会得到 object[] 类型而不是具体的 SpineSkinWrapper[] 类型
+ var managers = instances.Cast().ToArray();
+ if (managers.Length > 0)
+ {
+ IEnumerable common = managers[0].Spine.SkinNames;
+ foreach (var t in managers.Skip(1))
+ common = common.Union(t.Spine.SkinNames);
+ return new StandardValuesCollection(common.ToArray());
+ }
+ }
+ return base.GetStandardValues(context);
+ }
+ }
+
+ ///
+ /// 轨道索引包装类转换器, 实现字符串和包装类的互相转换, 并且提供标准值列表对属性进行设置, 同时还提供在面板上显示包装类属性的能力
+ ///
+ public class TrackWrapperConverter : ExpandableObjectConverter
+ {
+ // NOTE: 可以不用实现 ConvertTo/ConvertFrom, 因为属性实现了与字符串之间的互转
+ // ToString 实现了 ConvertTo
+ // SetValue 实现了从字符串设置属性
+
+ public override bool GetStandardValuesSupported(ITypeDescriptorContext? context) => true;
+
+ public override bool GetStandardValuesExclusive(ITypeDescriptorContext? context) => true;
+
+ public override StandardValuesCollection? GetStandardValues(ITypeDescriptorContext? context)
+ {
+ if (context?.Instance is SpineAnimationWrapper tracks)
+ {
+ return new StandardValuesCollection(tracks.Spine.AnimationNames);
+ }
+ else if (context?.Instance is object[] instances && instances.All(x => x is SpineAnimationWrapper))
+ {
+ // XXX: 这里不知道为啥总是会得到 object[] 类型而不是具体的类型
+ var animTracks = instances.Cast().ToArray();
+ if (animTracks.Length > 0)
+ {
+ IEnumerable common = animTracks[0].Spine.AnimationNames;
+ foreach (var t in animTracks.Skip(1))
+ common = common.Union(t.Spine.AnimationNames);
+ return new StandardValuesCollection(common.ToArray());
+ }
+ }
+ return base.GetStandardValues(context);
+ }
+ }
+
+ public class SFMLColorConverter : ExpandableObjectConverter
+ {
+ private class SFMLColorPropertyDescriptor : SimplePropertyDescriptor
+ {
+ public SFMLColorPropertyDescriptor(Type componentType, string name, Type propertyType) : base(componentType, name, propertyType) { }
+
+ public override object? GetValue(object? component) => component?.GetType().GetField(Name)?.GetValue(component) ?? default;
+
+ public override void SetValue(object? component, object? value) => component?.GetType().GetField(Name)?.SetValue(component, value);
+ }
+
+ public override bool CanConvertFrom(ITypeDescriptorContext? context, Type sourceType)
+ {
+ return sourceType == typeof(string) || base.CanConvertFrom(context, sourceType);
+ }
+
+ public override object? ConvertFrom(ITypeDescriptorContext? context, CultureInfo? culture, object value)
+ {
+ if (value is string s)
+ {
+ s = s.Trim();
+ if (s.StartsWith("#") && s.Length == 9)
+ {
+ try
+ {
+ // 解析 R, G, B, A 分量,注意16进制解析
+ byte r = byte.Parse(s.Substring(1, 2), NumberStyles.HexNumber);
+ byte g = byte.Parse(s.Substring(3, 2), NumberStyles.HexNumber);
+ byte b = byte.Parse(s.Substring(5, 2), NumberStyles.HexNumber);
+ byte a = byte.Parse(s.Substring(7, 2), NumberStyles.HexNumber);
+ return new SFML.Graphics.Color(r, g, b, a);
+ }
+ catch (Exception ex)
+ {
+ throw new FormatException("无法解析颜色,确保格式为 #RRGGBBAA", ex);
+ }
+ }
+ throw new FormatException("格式错误,正确格式为 #RRGGBBAA");
+ }
+ return base.ConvertFrom(context, culture, value);
+ }
+
+ public override bool CanConvertTo(ITypeDescriptorContext? context, Type? destinationType)
+ {
+ return destinationType == typeof(string) || base.CanConvertTo(context, destinationType);
+ }
+
+ public override object? ConvertTo(ITypeDescriptorContext? context, CultureInfo? culture, object? value, Type destinationType)
+ {
+ if (destinationType == typeof(string) && value is SFML.Graphics.Color color)
+ return $"#{color.R:X2}{color.G:X2}{color.B:X2}{color.A:X2}";
+ return base.ConvertTo(context, culture, value, destinationType);
+ }
+
+ public override PropertyDescriptorCollection GetProperties(ITypeDescriptorContext? context, object value, Attribute[]? attributes)
+ {
+ // 自定义属性集合
+ var properties = new List
+ {
+ // 定义 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());
+ }
+ }
+}
diff --git a/SpineViewer/PropertyGridWrappers/UITypeEditor.cs b/SpineViewer/PropertyGridWrappers/UITypeEditor.cs
new file mode 100644
index 0000000..7c5238b
--- /dev/null
+++ b/SpineViewer/PropertyGridWrappers/UITypeEditor.cs
@@ -0,0 +1,172 @@
+using SpineViewer.Dialogs;
+using SpineViewer.PropertyGridWrappers.Spine;
+using System;
+using System.Collections.Generic;
+using System.ComponentModel;
+using System.Drawing.Design;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using System.Windows.Forms.Design;
+
+namespace SpineViewer.PropertyGridWrappers
+{
+ ///
+ /// 使用 FolderBrowserDialog 的文件夹路径编辑器
+ ///
+ public class FolderNameEditor : UITypeEditor
+ {
+ public override UITypeEditorEditStyle GetEditStyle(ITypeDescriptorContext? context)
+ {
+ // 指定编辑风格为 Modal 对话框, 提供右边用来点击的按钮
+ return UITypeEditorEditStyle.Modal;
+ }
+
+ public override object? EditValue(ITypeDescriptorContext? context, IServiceProvider provider, object? value)
+ {
+ // 重写 EditValue 方法,提供自定义的文件夹选择对话框逻辑
+ using var dialog = new FolderBrowserDialog();
+
+ // 如果当前值为有效路径,则设置为初始选中路径
+ if (value is string currentPath && Directory.Exists(currentPath))
+ dialog.SelectedPath = currentPath;
+
+ if (dialog.ShowDialog() == DialogResult.OK)
+ value = dialog.SelectedPath;
+
+ return value;
+ }
+ }
+
+ ///
+ /// skel 文件路径编辑器
+ ///
+ public class SkelFileNameEditor : FileNameEditor
+ {
+ protected override void InitializeDialog(OpenFileDialog openFileDialog)
+ {
+ base.InitializeDialog(openFileDialog);
+ openFileDialog.Title = "选择 skel 文件";
+ openFileDialog.AddExtension = false;
+ openFileDialog.Filter = "skel 文件 (*.skel; *.json)|*.skel;*.json|二进制文件 (*.skel)|*.skel|文本文件 (*.json)|*.json|所有文件 (*.*)|*.*";
+ }
+ }
+
+ ///
+ /// atlas 文件路径编辑器
+ ///
+ public class AtlasFileNameEditor : FileNameEditor
+ {
+ protected override void InitializeDialog(OpenFileDialog openFileDialog)
+ {
+ base.InitializeDialog(openFileDialog);
+ openFileDialog.Title = "选择 atlas 文件";
+ openFileDialog.AddExtension = false;
+ openFileDialog.Filter = "atlas 文件 (*.atlas)|*.atlas|所有文件 (*.*)|*.*";
+ }
+ }
+
+ 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);
+ }
+ }
+
+ ///
+ /// 多轨道动画编辑器
+ ///
+ public class SpineSkinEditor : UITypeEditor
+ {
+ public override UITypeEditorEditStyle GetEditStyle(ITypeDescriptorContext? context) => UITypeEditorEditStyle.Modal;
+
+ public override object? EditValue(ITypeDescriptorContext? context, IServiceProvider provider, object? value)
+ {
+ if (provider == null || context == null || context.Instance is not SpineWrapper)
+ return value;
+
+ IWindowsFormsEditorService editorService = provider.GetService(typeof(IWindowsFormsEditorService)) as IWindowsFormsEditorService;
+ if (editorService == null)
+ return value;
+
+ using (var dialog = new SpineSkinEditorDialog(((SpineWrapper)context.Instance).Spine))
+ editorService.ShowDialog(dialog);
+
+ TypeDescriptor.Refresh(context.Instance);
+ return value;
+ }
+ }
+
+ ///
+ /// 多轨道动画编辑器
+ ///
+ public class SpineAnimationEditor : UITypeEditor
+ {
+ public override UITypeEditorEditStyle GetEditStyle(ITypeDescriptorContext? context) => UITypeEditorEditStyle.Modal;
+
+ public override object? EditValue(ITypeDescriptorContext? context, IServiceProvider provider, object? value)
+ {
+ if (provider == null || context == null || context.Instance is not SpineWrapper)
+ return value;
+
+ IWindowsFormsEditorService editorService = provider.GetService(typeof(IWindowsFormsEditorService)) as IWindowsFormsEditorService;
+ if (editorService == null)
+ return value;
+
+ using (var dialog = new SpineAnimationEditorDialog(((SpineWrapper)context.Instance).Spine))
+ editorService.ShowDialog(dialog);
+
+ TypeDescriptor.Refresh(context.Instance);
+ return value;
+ }
+ }
+}
diff --git a/SpineViewer/Spine/Implementations/Spine/Spine21.cs b/SpineViewer/Spine/Implementations/Spine/Spine21.cs
index 4811778..2a7cd6e 100644
--- a/SpineViewer/Spine/Implementations/Spine/Spine21.cs
+++ b/SpineViewer/Spine/Implementations/Spine/Spine21.cs
@@ -7,6 +7,7 @@ using System.Linq;
using System.Text;
using System.Threading.Tasks;
using SpineRuntime21;
+using SpineViewer.Extensions;
namespace SpineViewer.Spine.Implementations.Spine
{
@@ -261,7 +262,7 @@ namespace SpineViewer.Spine.Implementations.Spine
{
vertexArray.Clear();
states.Texture = null;
- states.Shader = Shader.GetSpineShader(usePremultipliedAlpha);
+ states.Shader = SFMLShader.GetSpineShader(usePma);
// 要用 DrawOrder 而不是 Slots
foreach (var slot in skeleton.DrawOrder)
@@ -323,7 +324,7 @@ namespace SpineViewer.Spine.Implementations.Spine
}
// 似乎 2.1.x 也没有 BlendMode
- SFML.Graphics.BlendMode blendMode = slot.Data.AdditiveBlending ? BlendModeSFML.AdditivePma : BlendModeSFML.NormalPma;
+ SFML.Graphics.BlendMode blendMode = slot.Data.AdditiveBlending ? SFMLBlendMode.AdditivePma : SFMLBlendMode.NormalPma;
states.Texture ??= texture;
if (states.BlendMode != blendMode || states.Texture != texture)
diff --git a/SpineViewer/Spine/Implementations/Spine/Spine36.cs b/SpineViewer/Spine/Implementations/Spine/Spine36.cs
index 7382e9a..72edbdd 100644
--- a/SpineViewer/Spine/Implementations/Spine/Spine36.cs
+++ b/SpineViewer/Spine/Implementations/Spine/Spine36.cs
@@ -7,6 +7,7 @@ using System.Linq;
using System.Text;
using System.Threading.Tasks;
using SpineRuntime36;
+using SpineViewer.Extensions;
namespace SpineViewer.Spine.Implementations.Spine
{
@@ -208,10 +209,10 @@ namespace SpineViewer.Spine.Implementations.Spine
{
return spineBlendMode switch
{
- BlendMode.Normal => BlendModeSFML.NormalPma,
- BlendMode.Additive => BlendModeSFML.AdditivePma,
- BlendMode.Multiply => BlendModeSFML.MultiplyPma,
- BlendMode.Screen => BlendModeSFML.ScreenPma,
+ BlendMode.Normal => SFMLBlendMode.NormalPma,
+ BlendMode.Additive => SFMLBlendMode.AdditivePma,
+ BlendMode.Multiply => SFMLBlendMode.MultiplyPma,
+ BlendMode.Screen => SFMLBlendMode.ScreenPma,
_ => throw new NotImplementedException($"{spineBlendMode}"),
};
}
@@ -220,7 +221,7 @@ namespace SpineViewer.Spine.Implementations.Spine
{
vertexArray.Clear();
states.Texture = null;
- states.Shader = Shader.GetSpineShader(usePremultipliedAlpha);
+ states.Shader = SFMLShader.GetSpineShader(usePma);
// 要用 DrawOrder 而不是 Slots
foreach (var slot in skeleton.DrawOrder)
diff --git a/SpineViewer/Spine/Implementations/Spine/Spine37.cs b/SpineViewer/Spine/Implementations/Spine/Spine37.cs
index 274be93..acebdf2 100644
--- a/SpineViewer/Spine/Implementations/Spine/Spine37.cs
+++ b/SpineViewer/Spine/Implementations/Spine/Spine37.cs
@@ -4,6 +4,7 @@ using System.Linq;
using System.Text;
using System.Threading.Tasks;
using SpineRuntime37;
+using SpineViewer.Extensions;
namespace SpineViewer.Spine.Implementations.Spine
{
@@ -180,10 +181,10 @@ namespace SpineViewer.Spine.Implementations.Spine
{
return spineBlendMode switch
{
- BlendMode.Normal => BlendModeSFML.NormalPma,
- BlendMode.Additive => BlendModeSFML.AdditivePma,
- BlendMode.Multiply => BlendModeSFML.MultiplyPma,
- BlendMode.Screen => BlendModeSFML.ScreenPma,
+ BlendMode.Normal => SFMLBlendMode.NormalPma,
+ BlendMode.Additive => SFMLBlendMode.AdditivePma,
+ BlendMode.Multiply => SFMLBlendMode.MultiplyPma,
+ BlendMode.Screen => SFMLBlendMode.ScreenPma,
_ => throw new NotImplementedException($"{spineBlendMode}"),
};
}
@@ -192,7 +193,7 @@ namespace SpineViewer.Spine.Implementations.Spine
{
vertexArray.Clear();
states.Texture = null;
- states.Shader = Shader.GetSpineShader(usePremultipliedAlpha);
+ states.Shader = SFMLShader.GetSpineShader(usePma);
// 要用 DrawOrder 而不是 Slots
foreach (var slot in skeleton.DrawOrder)
diff --git a/SpineViewer/Spine/Implementations/Spine/Spine38.cs b/SpineViewer/Spine/Implementations/Spine/Spine38.cs
index bcb923f..59d4536 100644
--- a/SpineViewer/Spine/Implementations/Spine/Spine38.cs
+++ b/SpineViewer/Spine/Implementations/Spine/Spine38.cs
@@ -7,6 +7,7 @@ using System.Text;
using System.Threading.Tasks;
using SpineRuntime38;
using SpineRuntime38.Attachments;
+using SpineViewer.Extensions;
namespace SpineViewer.Spine.Implementations.Spine
{
@@ -188,10 +189,10 @@ namespace SpineViewer.Spine.Implementations.Spine
{
return spineBlendMode switch
{
- BlendMode.Normal => BlendModeSFML.NormalPma,
- BlendMode.Additive => BlendModeSFML.AdditivePma,
- BlendMode.Multiply => BlendModeSFML.MultiplyPma,
- BlendMode.Screen => BlendModeSFML.ScreenPma,
+ BlendMode.Normal => SFMLBlendMode.NormalPma,
+ BlendMode.Additive => SFMLBlendMode.AdditivePma,
+ BlendMode.Multiply => SFMLBlendMode.MultiplyPma,
+ BlendMode.Screen => SFMLBlendMode.ScreenPma,
_ => throw new NotImplementedException($"{spineBlendMode}"),
};
}
@@ -200,7 +201,7 @@ namespace SpineViewer.Spine.Implementations.Spine
{
vertexArray.Clear();
states.Texture = null;
- states.Shader = Shader.GetSpineShader(usePremultipliedAlpha);
+ states.Shader = SFMLShader.GetSpineShader(usePma);
// 要用 DrawOrder 而不是 Slots
foreach (var slot in skeleton.DrawOrder)
diff --git a/SpineViewer/Spine/Implementations/Spine/Spine40.cs b/SpineViewer/Spine/Implementations/Spine/Spine40.cs
index f137e8f..19f53e7 100644
--- a/SpineViewer/Spine/Implementations/Spine/Spine40.cs
+++ b/SpineViewer/Spine/Implementations/Spine/Spine40.cs
@@ -6,6 +6,7 @@ using System.Linq;
using System.Text;
using System.Threading.Tasks;
using SpineRuntime40;
+using SpineViewer.Extensions;
namespace SpineViewer.Spine.Implementations.Spine
{
@@ -184,10 +185,10 @@ namespace SpineViewer.Spine.Implementations.Spine
{
return spineBlendMode switch
{
- BlendMode.Normal => BlendModeSFML.NormalPma,
- BlendMode.Additive => BlendModeSFML.AdditivePma,
- BlendMode.Multiply => BlendModeSFML.MultiplyPma,
- BlendMode.Screen => BlendModeSFML.ScreenPma,
+ BlendMode.Normal => SFMLBlendMode.NormalPma,
+ BlendMode.Additive => SFMLBlendMode.AdditivePma,
+ BlendMode.Multiply => SFMLBlendMode.MultiplyPma,
+ BlendMode.Screen => SFMLBlendMode.ScreenPma,
_ => throw new NotImplementedException($"{spineBlendMode}"),
};
}
@@ -196,7 +197,7 @@ namespace SpineViewer.Spine.Implementations.Spine
{
vertexArray.Clear();
states.Texture = null;
- states.Shader = Shader.GetSpineShader(usePremultipliedAlpha);
+ states.Shader = SFMLShader.GetSpineShader(usePma);
// 要用 DrawOrder 而不是 Slots
foreach (var slot in skeleton.DrawOrder)
diff --git a/SpineViewer/Spine/Implementations/Spine/Spine41.cs b/SpineViewer/Spine/Implementations/Spine/Spine41.cs
index 1962e7a..39bffcc 100644
--- a/SpineViewer/Spine/Implementations/Spine/Spine41.cs
+++ b/SpineViewer/Spine/Implementations/Spine/Spine41.cs
@@ -6,6 +6,7 @@ using System.Linq;
using System.Text;
using System.Threading.Tasks;
using SpineRuntime41;
+using SpineViewer.Extensions;
namespace SpineViewer.Spine.Implementations.Spine
{
@@ -184,10 +185,10 @@ namespace SpineViewer.Spine.Implementations.Spine
{
return spineBlendMode switch
{
- BlendMode.Normal => BlendModeSFML.NormalPma,
- BlendMode.Additive => BlendModeSFML.AdditivePma,
- BlendMode.Multiply => BlendModeSFML.MultiplyPma,
- BlendMode.Screen => BlendModeSFML.ScreenPma,
+ BlendMode.Normal => SFMLBlendMode.NormalPma,
+ BlendMode.Additive => SFMLBlendMode.AdditivePma,
+ BlendMode.Multiply => SFMLBlendMode.MultiplyPma,
+ BlendMode.Screen => SFMLBlendMode.ScreenPma,
_ => throw new NotImplementedException($"{spineBlendMode}"),
};
}
@@ -196,7 +197,7 @@ namespace SpineViewer.Spine.Implementations.Spine
{
vertexArray.Clear();
states.Texture = null;
- states.Shader = Shader.GetSpineShader(usePremultipliedAlpha);
+ states.Shader = SFMLShader.GetSpineShader(usePma);
// 要用 DrawOrder 而不是 Slots
foreach (var slot in skeleton.DrawOrder)
diff --git a/SpineViewer/Spine/Implementations/Spine/Spine42.cs b/SpineViewer/Spine/Implementations/Spine/Spine42.cs
index 64c1e46..45ed97a 100644
--- a/SpineViewer/Spine/Implementations/Spine/Spine42.cs
+++ b/SpineViewer/Spine/Implementations/Spine/Spine42.cs
@@ -6,6 +6,7 @@ using System.Linq;
using System.Text;
using System.Threading.Tasks;
using SpineRuntime42;
+using SpineViewer.Extensions;
namespace SpineViewer.Spine.Implementations.Spine
{
@@ -184,10 +185,10 @@ namespace SpineViewer.Spine.Implementations.Spine
{
return spineBlendMode switch
{
- BlendMode.Normal => BlendModeSFML.NormalPma,
- BlendMode.Additive => BlendModeSFML.AdditivePma,
- BlendMode.Multiply => BlendModeSFML.MultiplyPma,
- BlendMode.Screen => BlendModeSFML.ScreenPma,
+ BlendMode.Normal => SFMLBlendMode.NormalPma,
+ BlendMode.Additive => SFMLBlendMode.AdditivePma,
+ BlendMode.Multiply => SFMLBlendMode.MultiplyPma,
+ BlendMode.Screen => SFMLBlendMode.ScreenPma,
_ => throw new NotImplementedException($"{spineBlendMode}"),
};
}
@@ -196,7 +197,7 @@ namespace SpineViewer.Spine.Implementations.Spine
{
vertexArray.Clear();
states.Texture = null;
- states.Shader = Shader.GetSpineShader(usePremultipliedAlpha);
+ states.Shader = SFMLShader.GetSpineShader(usePma);
// 要用 DrawOrder 而不是 Slots
foreach (var slot in skeleton.DrawOrder)
diff --git a/SpineViewer/Spine/Spine.cs b/SpineViewer/Spine/Spine.cs
index dc24c71..29e6d03 100644
--- a/SpineViewer/Spine/Spine.cs
+++ b/SpineViewer/Spine/Spine.cs
@@ -1,10 +1,10 @@
using System;
using System.Collections.ObjectModel;
-using System.ComponentModel;
using System.Reflection;
using System.Drawing.Design;
using NLog;
using System.Xml.Linq;
+using SpineViewer.Extensions;
namespace SpineViewer.Spine
{
@@ -13,9 +13,6 @@ namespace SpineViewer.Spine
///
public abstract class Spine : ImplementationResolver, SFML.Graphics.Drawable, IDisposable
{
- private readonly Logger logger = LogManager.GetCurrentClassLogger();
- private bool skinLoggerWarned = false;
-
///
/// 空动画标记
///
@@ -36,8 +33,12 @@ namespace SpineViewer.Spine
///
public static Spine New(SpineVersion version, string skelPath, string? atlasPath = null)
{
- if (version == SpineVersion.Auto) version = SpineHelper.GetVersion(skelPath);
atlasPath ??= Path.ChangeExtension(skelPath, ".atlas");
+ skelPath = Path.GetFullPath(skelPath);
+ atlasPath = Path.GetFullPath(atlasPath);
+
+ if (version == SpineVersion.Auto) version = SpineHelper.GetVersion(skelPath);
+ if (!File.Exists(atlasPath)) throw new FileNotFoundException($"atlas file {atlasPath} not found");
return New(version, [skelPath, atlasPath]).PostInit();
}
@@ -46,6 +47,9 @@ namespace SpineViewer.Spine
///
private readonly object _lock = new();
+ private readonly Logger logger = LogManager.GetCurrentClassLogger();
+ private bool skinLoggerWarned = false;
+
///
/// 构造函数
///
@@ -64,9 +68,7 @@ namespace SpineViewer.Spine
private Spine PostInit()
{
SkinNames = skinNames.AsReadOnly();
- SkinManager = new(this);
AnimationNames = animationNames.AsReadOnly();
- AnimationTracks = new(this);
// 必须 Update 一次否则包围盒还没有值
update(0);
@@ -99,79 +101,67 @@ namespace SpineViewer.Spine
public void Dispose() { Dispose(true); GC.SuppressFinalize(this); }
protected virtual void Dispose(bool disposing) { Preview?.Dispose(); }
- #region 属性 | [0] 基本信息
+ ///
+ /// 运行时唯一 ID
+ ///
+ public string ID { get; } = Guid.NewGuid().ToString();
+
+ ///
+ /// 骨骼预览图, 并没有去除预乘, 画面可能偏暗
+ ///
+ public Image Preview { get; private set; }
///
/// 获取所属版本
///
- [TypeConverter(typeof(SpineVersionConverter))]
- [Category("[0] 基本信息"), DisplayName("运行时版本")]
public SpineVersion Version { get; }
///
/// 资源所在完整目录
///
- [Category("[0] 基本信息"), DisplayName("资源目录")]
public string AssetsDir { get; }
///
/// skel 文件完整路径
///
- [Category("[0] 基本信息"), DisplayName("skel文件路径")]
public string SkelPath { get; }
///
/// atlas 文件完整路径
///
- [Category("[0] 基本信息"), DisplayName("atlas文件路径")]
public string AtlasPath { get; }
///
/// 名称
///
- [Category("[0] 基本信息"), DisplayName("名称")]
public string Name { get; }
///
/// 获取所属文件版本
///
- [Category("[0] 基本信息"), DisplayName("文件版本")]
public abstract string FileVersion { get; }
- #endregion
-
- #region 属性 | [1] 设置
-
///
/// 是否被隐藏, 被隐藏的模型将仅仅在列表显示, 不参与其他行为
///
- [Category("[1] 设置"), DisplayName("是否隐藏")]
- public bool IsHidden
- {
- get { lock (_lock) return isHidden; }
- set { lock (_lock) isHidden = value; }
- }
+ public bool IsHidden { get { lock (_lock) return isHidden; } set { lock (_lock) isHidden = value; } }
protected bool isHidden = false;
///
- /// 是否使用预乘Alpha
+ /// 是否使用预乘 Alpha
///
- [Category("[1] 设置"), DisplayName("预乘Alpha通道")]
- public bool UsePremultipliedAlpha
- {
- get { lock (_lock) return usePremultipliedAlpha; }
- set { lock (_lock) usePremultipliedAlpha = value; }
- }
- protected bool usePremultipliedAlpha = false;
+ public bool UsePma { get { lock (_lock) return usePma; } set { lock (_lock) usePma = value; } }
+ protected bool usePma = false;
- #endregion
-
- #region 属性 | [2] 变换
+ ///
+ /// 骨骼包围盒
+ ///
+ public RectangleF Bounds { get { lock (_lock) return bounds; } }
+ protected abstract RectangleF bounds { get; }
///
/// 缩放比例
///
- [Category("[2] 变换"), DisplayName("缩放比例")]
public float Scale
{
get { lock (_lock) return scale; }
@@ -182,8 +172,6 @@ namespace SpineViewer.Spine
///
/// 位置
///
- [TypeConverter(typeof(PointFConverter))]
- [Category("[2] 变换"), DisplayName("位置")]
public PointF Position
{
get { lock (_lock) return position; }
@@ -194,7 +182,6 @@ namespace SpineViewer.Spine
///
/// 水平翻转
///
- [Category("[2] 变换"), DisplayName("水平翻转")]
public bool FlipX
{
get { lock (_lock) return flipX; }
@@ -205,7 +192,6 @@ namespace SpineViewer.Spine
///
/// 垂直翻转
///
- [Category("[2] 变换"), DisplayName("垂直翻转")]
public bool FlipY
{
get { lock (_lock) return flipY; }
@@ -213,49 +199,68 @@ namespace SpineViewer.Spine
}
protected abstract bool flipY { get; set; }
- #endregion
-
- #region 属性 | [3] 动画
-
- ///
- /// 已加载皮肤列表
- ///
- [Editor(typeof(SkinManagerEditor), typeof(UITypeEditor))]
- [TypeConverter(typeof(ExpandableObjectConverter))]
- [Category("[3] 动画"), DisplayName("已加载皮肤列表")]
- public SkinManager SkinManager { get; private set; }
-
- ///
- /// 默认轨道动画名称, 如果设置的动画不存在则忽略
- ///
- [Browsable(false)]
- public string Track0Animation
- {
- get { lock (_lock) return getAnimation(0); }
- set { lock (_lock) { setAnimation(0, value); update(0); } }
- }
-
- ///
- /// 全轨道动画最大时长
- ///
- [Category("[3] 动画"), DisplayName("全轨道最大时长")]
- public float AnimationTracksMaxDuration { get { lock (_lock) return getTrackIndices().Select(i => GetAnimationDuration(getAnimation(i))).Max(); } }
-
- ///
- /// 默认轨道动画时长
- ///
- [Editor(typeof(AnimationTracksEditor), typeof(UITypeEditor))]
- [TypeConverter(typeof(ExpandableObjectConverter))]
- [Category("[3] 动画"), DisplayName("多轨道动画管理")]
- public AnimationTracks AnimationTracks { get; private set; }
-
///
/// 包含的所有皮肤名称
///
- [Browsable(false)]
public ReadOnlyCollection SkinNames { get; private set; }
protected readonly List skinNames = [];
+ ///
+ /// 包含的所有动画名称
+ ///
+ public ReadOnlyCollection AnimationNames { get; private set; }
+ protected readonly List animationNames = [EMPTY_ANIMATION];
+
+ ///
+ /// 是否被选中
+ ///
+ public bool IsSelected
+ {
+ get { lock (_lock) return isSelected; }
+ set { lock (_lock) { isSelected = value; update(0); } }
+ }
+ protected bool isSelected = false;
+
+ ///
+ /// 显示调试
+ ///
+ public bool IsDebug
+ {
+ get { lock (_lock) return isDebug; }
+ set { lock (_lock) { isDebug = value; update(0); } }
+ }
+ protected bool isDebug = false;
+
+ ///
+ /// 显示纹理
+ ///
+ public bool DebugTexture
+ {
+ get { lock (_lock) return debugTexture; }
+ set { lock (_lock) { debugTexture = value; update(0); } }
+ }
+ protected bool debugTexture = true;
+
+ ///
+ /// 显示包围盒
+ ///
+ public bool DebugBounds
+ {
+ get { lock (_lock) return debugBounds; }
+ set { lock (_lock) { debugBounds = value; update(0); } }
+ }
+ protected bool debugBounds = true;
+
+ ///
+ /// 显示骨骼
+ ///
+ public bool DebugBones
+ {
+ get { lock (_lock) return debugBones; }
+ set { lock (_lock) { debugBones = value; update(0); } }
+ }
+ protected bool debugBones = false;
+
///
/// 获取已加载的皮肤列表快照, 允许出现重复值
///
@@ -328,13 +333,6 @@ namespace SpineViewer.Spine
///
protected abstract void clearSkin();
- ///
- /// 包含的所有动画名称
- ///
- [Browsable(false)]
- public ReadOnlyCollection AnimationNames { get; private set; }
- protected readonly List animationNames = [EMPTY_ANIMATION];
-
///
/// 获取所有非 null 的轨道索引快照
///
@@ -369,85 +367,6 @@ namespace SpineViewer.Spine
///
public void ResetAnimationsTime() { lock (_lock) { foreach (var i in getTrackIndices()) setAnimation(i, getAnimation(i)); update(0); } }
- #endregion
-
- #region 属性 | [4] 调试
-
- ///
- /// 显示调试
- ///
- [Browsable(false)]
- public bool IsDebug
- {
- get { lock (_lock) return isDebug; }
- set { lock (_lock) isDebug = value; }
- }
- protected bool isDebug = false;
-
- ///
- /// 显示纹理
- ///
- [Category("[4] 调试"), DisplayName("显示纹理")]
- public bool DebugTexture
- {
- get { lock (_lock) return debugTexture; }
- set { lock (_lock) debugTexture = value; }
- }
- protected bool debugTexture = true;
-
- ///
- /// 显示包围盒
- ///
- [Category("[4] 调试"), DisplayName("显示包围盒")]
- public bool DebugBounds
- {
- get { lock (_lock) return debugBounds; }
- set { lock (_lock) debugBounds = value; }
- }
- protected bool debugBounds = true;
-
- ///
- /// 显示骨骼
- ///
- [Category("[4] 调试"), DisplayName("显示骨架")]
- public bool DebugBones
- {
- get { lock (_lock) return debugBones; }
- set { lock (_lock) debugBones = value; }
- }
- protected bool debugBones = false;
-
- #endregion
-
- ///
- /// 标识符
- ///
- public readonly string ID = Guid.NewGuid().ToString();
-
- ///
- /// 是否被选中
- ///
- [Browsable(false)]
- public bool IsSelected
- {
- get { lock (_lock) return isSelected; }
- set { lock (_lock) isSelected = value; }
- }
- protected bool isSelected = false;
-
- ///
- /// 骨骼包围盒
- ///
- [Browsable(false)]
- public RectangleF Bounds { get { lock (_lock) return bounds; } }
- protected abstract RectangleF bounds { get; }
-
- ///
- /// 骨骼预览图, 并没有去除预乘, 画面可能偏暗
- ///
- [Browsable(false)]
- public Image Preview { get; private set; }
-
///
/// 更新内部状态
///
@@ -490,6 +409,5 @@ namespace SpineViewer.Spine
protected abstract void draw(SFML.Graphics.RenderTarget target, SFML.Graphics.RenderStates states);
#endregion
-
}
}
\ No newline at end of file
diff --git a/SpineViewer/Spine/TypeConverter.cs b/SpineViewer/Spine/TypeConverter.cs
deleted file mode 100644
index c3e45d9..0000000
--- a/SpineViewer/Spine/TypeConverter.cs
+++ /dev/null
@@ -1,143 +0,0 @@
-using System;
-using System.Collections;
-using System.Collections.Generic;
-using System.ComponentModel;
-using System.Diagnostics.CodeAnalysis;
-using System.Globalization;
-using System.Linq;
-using System.Text;
-using System.Threading.Tasks;
-
-namespace SpineViewer.Spine
-{
- public class SpineVersionConverter : EnumConverter
- {
- public SpineVersionConverter() : base(typeof(SpineVersion)) { }
-
- public override object? ConvertTo(ITypeDescriptorContext? context, CultureInfo? culture, object? value, Type? destinationType)
- {
- if (destinationType == typeof(string) && value is SpineVersion version)
- return version.GetName();
- return base.ConvertTo(context, culture, value, destinationType);
- }
- }
-
- public class SkinConverter : StringConverter
- {
- public override bool GetStandardValuesSupported(ITypeDescriptorContext? context) => true;
-
- public override bool GetStandardValuesExclusive(ITypeDescriptorContext? context) => true;
-
- public override StandardValuesCollection? GetStandardValues(ITypeDescriptorContext? context)
- {
- if (context?.Instance is Spine obj)
- {
- return new StandardValuesCollection(obj.SkinNames);
- }
- else if (context?.Instance is Spine[] spines)
- {
- if (spines.Length > 0)
- {
- IEnumerable common = spines[0].SkinNames;
- foreach (var spine in spines.Skip(1))
- common = common.Union(spine.SkinNames);
- return new StandardValuesCollection(common.ToArray());
- }
- }
- return base.GetStandardValues(context);
- }
- }
-
- public class AnimationConverter : StringConverter
- {
- public override bool GetStandardValuesSupported(ITypeDescriptorContext? context) => true;
-
- public override bool GetStandardValuesExclusive(ITypeDescriptorContext? context) => true;
-
- public override StandardValuesCollection? GetStandardValues(ITypeDescriptorContext? context)
- {
- if (context?.Instance is Spine obj)
- {
- return new StandardValuesCollection(obj.AnimationNames);
- }
- else if (context?.Instance is Spine[] spines)
- {
- if (spines.Length > 0)
- {
- IEnumerable common = spines[0].AnimationNames;
- foreach (var spine in spines.Skip(1))
- common = common.Union(spine.AnimationNames);
- return new StandardValuesCollection(common.ToArray());
- }
- }
- return base.GetStandardValues(context);
- }
- }
-
- ///
- /// 轨道索引包装类转换器, 实现字符串和包装类的互相转换, 并且提供标准值列表对属性进行设置, 同时还提供在面板上显示包装类属性的能力
- ///
- public class TrackWrapperConverter : ExpandableObjectConverter
- {
- // NOTE: 可以不用实现 ConvertTo/ConvertFrom, 因为属性实现了与字符串之间的互转
- // ToString 实现了 ConvertTo
- // SetValue 实现了从字符串设置属性
-
- public override bool GetStandardValuesSupported(ITypeDescriptorContext? context) => true;
-
- public override bool GetStandardValuesExclusive(ITypeDescriptorContext? context) => true;
-
- public override StandardValuesCollection? GetStandardValues(ITypeDescriptorContext? context)
- {
- if (context.Instance is AnimationTracks tracks)
- {
- return new StandardValuesCollection(tracks.Spine.AnimationNames);
- }
- else if (context.Instance is object[] instances && instances.All(x => x is AnimationTracks))
- {
- // XXX: 这里不知道为啥总是会得到 object[] 类型而不是具体的类型
- var animTracks = instances.Cast().ToArray();
- if (animTracks.Length > 0)
- {
- IEnumerable common = animTracks[0].Spine.AnimationNames;
- foreach (var t in animTracks.Skip(1))
- common = common.Union(t.Spine.AnimationNames);
- return new StandardValuesCollection(common.ToArray());
- }
- }
- return base.GetStandardValues(context);
- }
- }
-
- public class SkinWrapperConverter : StringConverter
- {
- // NOTE: 可以不用实现 ConvertTo/ConvertFrom, 因为属性实现了与字符串之间的互转
- // ToString 实现了 ConvertTo
- // SetValue 实现了从字符串设置属性
-
- public override bool GetStandardValuesSupported(ITypeDescriptorContext? context) => true;
-
- public override bool GetStandardValuesExclusive(ITypeDescriptorContext? context) => true;
-
- public override StandardValuesCollection? GetStandardValues(ITypeDescriptorContext? context)
- {
- if (context.Instance is SkinManager manager)
- {
- return new StandardValuesCollection(manager.Spine.SkinNames);
- }
- else if (context.Instance is object[] instances && instances.All(x => x is SkinManager))
- {
- // XXX: 这里不知道为啥总是会得到 object[] 类型而不是具体的 SkinManager[] 类型
- var managers = instances.Cast().ToArray();
- if (managers.Length > 0)
- {
- IEnumerable common = managers[0].Spine.SkinNames;
- foreach (var t in managers.Skip(1))
- common = common.Union(t.Spine.SkinNames);
- return new StandardValuesCollection(common.ToArray());
- }
- }
- return base.GetStandardValues(context);
- }
- }
-}
diff --git a/SpineViewer/Spine/UITypeEditor.cs b/SpineViewer/Spine/UITypeEditor.cs
deleted file mode 100644
index d8dcd1c..0000000
--- a/SpineViewer/Spine/UITypeEditor.cs
+++ /dev/null
@@ -1,88 +0,0 @@
-using SpineViewer.Dialogs;
-using System;
-using System.Collections.Generic;
-using System.ComponentModel;
-using System.Drawing.Design;
-using System.Linq;
-using System.Text;
-using System.Threading.Tasks;
-using System.Windows.Forms.Design;
-
-namespace SpineViewer.Spine
-{
- ///
- /// skel 文件路径编辑器
- ///
- public class SkelFileNameEditor : FileNameEditor
- {
- protected override void InitializeDialog(OpenFileDialog openFileDialog)
- {
- base.InitializeDialog(openFileDialog);
- openFileDialog.Title = "选择 skel 文件";
- openFileDialog.AddExtension = false;
- openFileDialog.Filter = "skel 文件 (*.skel; *.json)|*.skel;*.json|二进制文件 (*.skel)|*.skel|文本文件 (*.json)|*.json|所有文件 (*.*)|*.*";
- }
- }
-
- ///
- /// atlas 文件路径编辑器
- ///
- public class AtlasFileNameEditor : FileNameEditor
- {
- protected override void InitializeDialog(OpenFileDialog openFileDialog)
- {
- base.InitializeDialog(openFileDialog);
- openFileDialog.Title = "选择 atlas 文件";
- openFileDialog.AddExtension = false;
- openFileDialog.Filter = "atlas 文件 (*.atlas)|*.atlas|所有文件 (*.*)|*.*";
- }
- }
-
- ///
- /// 多轨道动画编辑器
- ///
- public class AnimationTracksEditor : UITypeEditor
- {
- public override UITypeEditorEditStyle GetEditStyle(ITypeDescriptorContext? context) => UITypeEditorEditStyle.Modal;
-
- public override object? EditValue(ITypeDescriptorContext? context, IServiceProvider provider, object? value)
- {
- if (provider == null || context == null || context.Instance is not Spine)
- return value;
-
- IWindowsFormsEditorService editorService = provider.GetService(typeof(IWindowsFormsEditorService)) as IWindowsFormsEditorService;
- if (editorService == null)
- return value;
-
- using (var dialog = new AnimationTracksEditorDialog((Spine)context.Instance))
- editorService.ShowDialog(dialog);
-
- TypeDescriptor.Refresh(context.Instance);
- return value;
- }
- }
-
- ///
- /// 多轨道动画编辑器
- ///
- public class SkinManagerEditor : UITypeEditor
- {
- public override UITypeEditorEditStyle GetEditStyle(ITypeDescriptorContext? context) => UITypeEditorEditStyle.Modal;
-
- public override object? EditValue(ITypeDescriptorContext? context, IServiceProvider provider, object? value)
- {
- if (provider == null || context == null || context.Instance is not Spine)
- return value;
-
- IWindowsFormsEditorService editorService = provider.GetService(typeof(IWindowsFormsEditorService)) as IWindowsFormsEditorService;
- if (editorService == null)
- return value;
-
- using (var dialog = new SkinManagerEditorDialog((Spine)context.Instance))
- editorService.ShowDialog(dialog);
-
- TypeDescriptor.Refresh(context.Instance);
- return value;
- }
- }
-}
diff --git a/SpineViewer/TypeConverter.cs b/SpineViewer/TypeConverter.cs
deleted file mode 100644
index 4192246..0000000
--- a/SpineViewer/TypeConverter.cs
+++ /dev/null
@@ -1,100 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.Collections.ObjectModel;
-using System.ComponentModel;
-using System.Diagnostics.CodeAnalysis;
-using System.Globalization;
-using System.Linq;
-using System.Text;
-using System.Threading.Tasks;
-
-namespace SpineViewer
-{
- public class PointFConverter : ExpandableObjectConverter
- {
- public override bool CanConvertTo(ITypeDescriptorContext? context, [NotNullWhen(true)] Type? destinationType)
- {
- return destinationType == typeof(string) || base.CanConvertTo(context, destinationType);
- }
-
- public override object? ConvertTo(ITypeDescriptorContext? context, CultureInfo? culture, object? value, Type destinationType)
- {
- if (destinationType == typeof(string) && value is PointF point)
- {
- return $"{point.X}, {point.Y}";
- }
- return base.ConvertTo(context, culture, value, destinationType);
- }
-
- public override bool CanConvertFrom(ITypeDescriptorContext? context, Type sourceType)
- {
- return sourceType == typeof(string) || base.CanConvertFrom(context, sourceType);
- }
-
- public override object? ConvertFrom(ITypeDescriptorContext? context, CultureInfo? culture, object value)
- {
- if (value is string str)
- {
- var parts = str.Split(',');
- if (parts.Length == 2 &&
- float.TryParse(parts[0], out var x) &&
- float.TryParse(parts[1], out var y))
- {
- return new PointF(x, y);
- }
- }
- return base.ConvertFrom(context, culture, value);
- }
- }
-
- public class StringEnumConverter : StringConverter
- {
- ///
- /// 字符串标准值列表属性
- ///
- [AttributeUsage(AttributeTargets.Property, AllowMultiple = false, Inherited = false)]
- public class StandardValuesAttribute : Attribute
- {
- ///
- /// 标准值列表
- ///
- public ReadOnlyCollection StandardValues { get; private set; }
- private readonly List standardValues = [];
-
- ///
- /// 是否允许用户自定义
- ///
- public bool Customizable { get; set; } = false;
-
- ///
- /// 字符串标准值列表
- ///
- /// 允许的字符串标准值
- 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().FirstOrDefault()?.Customizable ?? false;
- return !customizable;
- }
-
- public override StandardValuesCollection? GetStandardValues(ITypeDescriptorContext? context)
- {
- // 查找属性上的 StandardValuesAttribute
- var attribute = context?.PropertyDescriptor?.Attributes.OfType().FirstOrDefault();
- StandardValuesCollection result;
- if (attribute != null)
- result = new StandardValuesCollection(attribute.StandardValues);
- else
- result = new StandardValuesCollection(Array.Empty());
- return result;
- }
- }
-}
diff --git a/SpineViewer/UITypeEditor.cs b/SpineViewer/UITypeEditor.cs
deleted file mode 100644
index 8d0700e..0000000
--- a/SpineViewer/UITypeEditor.cs
+++ /dev/null
@@ -1,38 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.ComponentModel;
-using System.Drawing.Design;
-using System.Linq;
-using System.Text;
-using System.Threading.Tasks;
-using System.Windows.Forms.Design;
-
-namespace SpineViewer
-{
- ///
- /// 使用 FolderBrowserDialog 的文件夹路径编辑器
- ///
- public class FolderNameEditor : UITypeEditor
- {
- public override UITypeEditorEditStyle GetEditStyle(ITypeDescriptorContext? context)
- {
- // 指定编辑风格为 Modal 对话框, 提供右边用来点击的按钮
- return UITypeEditorEditStyle.Modal;
- }
-
- public override object? EditValue(ITypeDescriptorContext? context, IServiceProvider provider, object? value)
- {
- // 重写 EditValue 方法,提供自定义的文件夹选择对话框逻辑
- using var dialog = new FolderBrowserDialog();
-
- // 如果当前值为有效路径,则设置为初始选中路径
- if (value is string currentPath && Directory.Exists(currentPath))
- dialog.SelectedPath = currentPath;
-
- if (dialog.ShowDialog() == DialogResult.OK)
- value = dialog.SelectedPath;
-
- return value;
- }
- }
-}