重构
This commit is contained in:
@@ -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
|
||||
{
|
||||
/// <summary>
|
||||
/// 日志器
|
||||
/// </summary>
|
||||
private readonly Logger logger = LogManager.GetCurrentClassLogger();
|
||||
|
||||
/// <summary>
|
||||
/// Spine 列表只读视图, 访问时必须使用 lock 语句锁定视图本身
|
||||
/// </summary>
|
||||
@@ -29,9 +36,9 @@ namespace SpineViewer.Controls
|
||||
private readonly List<Spine.Spine> spines = [];
|
||||
|
||||
/// <summary>
|
||||
/// 日志器
|
||||
/// 用于属性页显示模型参数的包装类
|
||||
/// </summary>
|
||||
protected readonly Logger logger = LogManager.GetCurrentClassLogger();
|
||||
private readonly Dictionary<string, SpineWrapper> 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<int>().Select(index => spines[index]).ToArray();
|
||||
PropertyGrid.SelectedObjects = listView.SelectedIndices.Cast<int>().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();
|
||||
}
|
||||
|
||||
@@ -15,6 +15,24 @@ namespace SpineViewer.Controls
|
||||
{
|
||||
public partial class SpinePreviewer : UserControl
|
||||
{
|
||||
/// <summary>
|
||||
/// 日志器
|
||||
/// </summary>
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 要绑定的 Spine 列表控件
|
||||
/// </summary>
|
||||
@@ -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 画面参数
|
||||
|
||||
/// <summary>
|
||||
/// 画面缩放最大值
|
||||
/// </summary>
|
||||
public const float ZOOM_MAX = 1000f;
|
||||
|
||||
/// <summary>
|
||||
/// 画面缩放最小值
|
||||
/// </summary>
|
||||
public const float ZOOM_MIN = 0.001f;
|
||||
|
||||
/// <summary>
|
||||
/// 包装类, 用于属性面板显示
|
||||
/// </summary>
|
||||
private class PreviewerProperty(SpinePreviewer previewer)
|
||||
{
|
||||
[TypeConverter(typeof(SizeConverter))]
|
||||
[Category("[0] 导出"), DisplayName("分辨率")]
|
||||
public Size Resolution { get => previewer.Resolution; set => previewer.Resolution = value; }
|
||||
|
||||
[TypeConverter(typeof(PointFConverter))]
|
||||
[Category("[0] 导出"), DisplayName("画面中心点")]
|
||||
public PointF Center { get => previewer.Center; set => previewer.Center = value; }
|
||||
|
||||
[Category("[0] 导出"), DisplayName("缩放")]
|
||||
public float Zoom { get => previewer.Zoom; set => previewer.Zoom = value; }
|
||||
|
||||
[Category("[0] 导出"), DisplayName("旋转")]
|
||||
public float Rotation { get => previewer.Rotation; set => previewer.Rotation = value; }
|
||||
|
||||
[Category("[0] 导出"), DisplayName("水平翻转")]
|
||||
public bool FlipX { get => previewer.FlipX; set => previewer.FlipX = value; }
|
||||
|
||||
[Category("[0] 导出"), DisplayName("垂直翻转")]
|
||||
public bool FlipY { get => previewer.FlipY; set => previewer.FlipY = value; }
|
||||
|
||||
[Category("[0] 导出"), DisplayName("仅渲染选中")]
|
||||
public bool RenderSelectedOnly { get => previewer.RenderSelectedOnly; set => previewer.RenderSelectedOnly = value; }
|
||||
|
||||
[Category("[1] 预览"), DisplayName("显示坐标轴")]
|
||||
public bool ShowAxis { get => previewer.ShowAxis; set => previewer.ShowAxis = value; }
|
||||
|
||||
[Category("[1] 预览"), DisplayName("最大帧率")]
|
||||
public uint MaxFps { get => previewer.MaxFps; set => previewer.MaxFps = value; }
|
||||
}
|
||||
#region 参数属性
|
||||
|
||||
/// <summary>
|
||||
/// 分辨率
|
||||
@@ -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 渲染管理
|
||||
|
||||
/// <summary>
|
||||
/// 日志器
|
||||
/// 预览画面背景色
|
||||
/// </summary>
|
||||
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);
|
||||
/// <summary>
|
||||
/// 预览画面坐标轴颜色
|
||||
/// </summary>
|
||||
private static readonly SFML.Graphics.Color AxisColor = new(220, 220, 220);
|
||||
|
||||
// 设置默认参数
|
||||
Resolution = new(2048, 2048);
|
||||
Center = new(0, 0);
|
||||
FlipY = true;
|
||||
MaxFps = 30;
|
||||
}
|
||||
|
||||
#region 渲染线程管理
|
||||
/// <summary>
|
||||
/// 坐标轴顶点缓冲区
|
||||
/// </summary>
|
||||
private readonly SFML.Graphics.VertexArray AxisVertex = new(SFML.Graphics.PrimitiveType.Lines, 2);
|
||||
|
||||
/// <summary>
|
||||
/// 渲染窗口
|
||||
/// </summary>
|
||||
private readonly SFML.Graphics.RenderWindow RenderWindow;
|
||||
|
||||
/// <summary>
|
||||
/// 帧间隔计时器
|
||||
/// </summary>
|
||||
private readonly SFML.System.Clock Clock = new();
|
||||
|
||||
/// <summary>
|
||||
/// 渲染任务
|
||||
/// </summary>
|
||||
private Task? task = null;
|
||||
private CancellationTokenSource? cancelToken = null;
|
||||
|
||||
/// <summary>
|
||||
/// 开始渲染
|
||||
/// </summary>
|
||||
public void StartRender()
|
||||
{
|
||||
if (task is not null)
|
||||
return;
|
||||
cancelToken = new();
|
||||
task = Task.Run(RenderTask, cancelToken.Token);
|
||||
IsUpdating = true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 停止渲染
|
||||
/// </summary>
|
||||
public void StopRender()
|
||||
{
|
||||
IsUpdating = false;
|
||||
if (task is null || cancelToken is null)
|
||||
return;
|
||||
cancelToken.Cancel();
|
||||
task.Wait();
|
||||
cancelToken = null;
|
||||
task = null;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region 渲染更新管理
|
||||
|
||||
/// <summary>
|
||||
/// 是否更新画面
|
||||
/// </summary>
|
||||
[DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
|
||||
[Browsable(false)]
|
||||
public bool IsUpdating
|
||||
{
|
||||
get => isUpdating;
|
||||
@@ -360,24 +307,30 @@ namespace SpineViewer.Controls
|
||||
private object _forwardDeltaLock = new();
|
||||
|
||||
/// <summary>
|
||||
/// 预览画面背景色
|
||||
/// 开始渲染
|
||||
/// </summary>
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 预览画面坐标轴颜色
|
||||
/// 停止渲染
|
||||
/// </summary>
|
||||
private static readonly SFML.Graphics.Color AxisColor = new(220, 220, 220);
|
||||
|
||||
/// <summary>
|
||||
/// 坐标轴顶点缓冲区
|
||||
/// </summary>
|
||||
private readonly SFML.Graphics.VertexArray AxisVertex = new(SFML.Graphics.PrimitiveType.Lines, 2);
|
||||
|
||||
/// <summary>
|
||||
/// 帧间隔计时器
|
||||
/// </summary>
|
||||
private readonly SFML.System.Clock Clock = new();
|
||||
public void StopRender()
|
||||
{
|
||||
IsUpdating = false;
|
||||
if (task is null || cancelToken is null)
|
||||
return;
|
||||
cancelToken.Cancel();
|
||||
task.Wait();
|
||||
cancelToken = null;
|
||||
task = null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 渲染任务
|
||||
|
||||
@@ -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
|
||||
{
|
||||
/// <summary>
|
||||
/// 要绑定的导出参数
|
||||
/// </summary>
|
||||
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;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
namespace SpineViewer.Dialogs
|
||||
{
|
||||
partial class AnimationTracksEditorDialog
|
||||
partial class SpineAnimationEditorDialog
|
||||
{
|
||||
/// <summary>
|
||||
/// Required designer variable.
|
||||
@@ -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)
|
||||
@@ -1,6 +1,6 @@
|
||||
namespace SpineViewer.Dialogs
|
||||
{
|
||||
partial class SkinManagerEditorDialog
|
||||
partial class SpineSkinEditorDialog
|
||||
{
|
||||
/// <summary>
|
||||
/// Required designer variable.
|
||||
@@ -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)
|
||||
@@ -5,16 +5,13 @@ using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace SpineViewer.Exporter.Implementations.ExportArgs
|
||||
namespace SpineViewer.Exporter
|
||||
{
|
||||
/// <summary>
|
||||
/// FFmpeg 自定义视频导出参数
|
||||
/// </summary>
|
||||
[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
|
||||
/// <summary>
|
||||
/// 文件格式
|
||||
/// </summary>
|
||||
[Category("[3] 自定义参数"), DisplayName("文件格式"), Description("文件格式")]
|
||||
public string CustomFormat { get; set; } = "mp4";
|
||||
|
||||
/// <summary>
|
||||
/// 文件名后缀
|
||||
/// </summary>
|
||||
[Category("[3] 自定义参数"), DisplayName("文件名后缀"), Description("文件名后缀")]
|
||||
public string CustomSuffix { get; set; } = ".mp4";
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
{
|
||||
/// <summary>
|
||||
/// 导出参数基类
|
||||
/// </summary>
|
||||
public abstract class ExportArgs : ImplementationResolver<ExportArgs, ExportImplementationAttribute, ExportType>, IDisposable
|
||||
{
|
||||
/// <summary>
|
||||
/// 创建指定类型导出参数
|
||||
/// </summary>
|
||||
/// <param name="exportType">导出类型</param>
|
||||
/// <param name="resolution">分辨率</param>
|
||||
/// <param name="view">导出视图</param>
|
||||
/// <param name="renderSelectedOnly">仅渲染选中</param>
|
||||
/// <returns>返回与指定 <paramref name="exportType"/> 匹配的导出参数实例</returns>
|
||||
public static ExportArgs New(ExportType exportType, Size resolution, SFML.Graphics.View view, bool renderSelectedOnly)
|
||||
=> New(exportType, [resolution, view, renderSelectedOnly]);
|
||||
|
||||
public ExportArgs(Size resolution, SFML.Graphics.View view, bool renderSelectedOnly)
|
||||
{
|
||||
Resolution = resolution;
|
||||
View = view;
|
||||
RenderSelectedOnly = renderSelectedOnly;
|
||||
}
|
||||
|
||||
~ExportArgs() { Dispose(false); }
|
||||
public void Dispose() { Dispose(true); GC.SuppressFinalize(this); }
|
||||
protected virtual void Dispose(bool disposing) { View?.Dispose(); }
|
||||
|
||||
/// <summary>
|
||||
/// 输出文件夹
|
||||
/// </summary>
|
||||
[Editor(typeof(FolderNameEditor), typeof(UITypeEditor))]
|
||||
[Category("[0] 导出"), DisplayName("输出文件夹"), Description("逐个导出时可以留空,将逐个导出到模型自身所在目录")]
|
||||
public string? OutputDir { get; set; } = null;
|
||||
|
||||
/// <summary>
|
||||
/// 导出单个
|
||||
/// </summary>
|
||||
[Category("[0] 导出"), DisplayName("导出单个"), Description("是否将模型在同一个画面上导出单个文件,否则逐个导出模型")]
|
||||
public bool ExportSingle { get; set; } = false;
|
||||
|
||||
/// <summary>
|
||||
/// 画面分辨率
|
||||
/// </summary>
|
||||
[TypeConverter(typeof(SizeConverter))]
|
||||
[Category("[0] 导出"), DisplayName("分辨率"), Description("画面的宽高像素大小,请在预览画面参数面板进行调整")]
|
||||
public Size Resolution { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 渲染视窗
|
||||
/// </summary>
|
||||
[Category("[0] 导出"), DisplayName("视图"), Description("画面的视图参数,请在预览画面参数面板进行调整")]
|
||||
public SFML.Graphics.View View { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 是否仅渲染选中
|
||||
/// </summary>
|
||||
[Category("[0] 导出"), DisplayName("仅渲染选中"), Description("是否仅导出选中的模型,请在预览画面参数面板进行调整")]
|
||||
public bool RenderSelectedOnly { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 背景颜色
|
||||
/// </summary>
|
||||
[Editor(typeof(SFMLColorEditor), typeof(UITypeEditor))]
|
||||
[TypeConverter(typeof(SFMLColorConverter))]
|
||||
[Category("[0] 导出"), DisplayName("背景颜色"), Description("要使用的背景色, 格式为 #RRGGBBAA")]
|
||||
public SFML.Graphics.Color BackgroundColor
|
||||
{
|
||||
get => backgroundColor;
|
||||
set
|
||||
{
|
||||
backgroundColor = value;
|
||||
var bcPma = value;
|
||||
var a = bcPma.A / 255f;
|
||||
bcPma.R = (byte)(bcPma.R * a);
|
||||
bcPma.G = (byte)(bcPma.G * a);
|
||||
bcPma.B = (byte)(bcPma.B * a);
|
||||
BackgroundColorPma = bcPma;
|
||||
}
|
||||
}
|
||||
private SFML.Graphics.Color backgroundColor = SFML.Graphics.Color.Transparent;
|
||||
|
||||
/// <summary>
|
||||
/// 预乘后的背景颜色
|
||||
/// </summary>
|
||||
[Browsable(false)]
|
||||
public SFML.Graphics.Color BackgroundColorPma { get; private set; } = SFML.Graphics.Color.Transparent;
|
||||
|
||||
/// <summary>
|
||||
/// 检查参数是否合法并规范化参数值, 否则返回用户错误原因
|
||||
/// </summary>
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
{
|
||||
/// <summary>
|
||||
/// 导出类型
|
||||
/// </summary>
|
||||
public enum ExportType
|
||||
{
|
||||
Frame,
|
||||
FrameSequence,
|
||||
Gif,
|
||||
Mp4,
|
||||
Webm,
|
||||
Mkv,
|
||||
Mov,
|
||||
Custom,
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 导出实现类标记
|
||||
/// </summary>
|
||||
[AttributeUsage(AttributeTargets.Class, AllowMultiple = true, Inherited = false)]
|
||||
public class ExportImplementationAttribute(ExportType exportType) : Attribute, IImplementationKey<ExportType>
|
||||
{
|
||||
public ExportType ImplementationKey { get; private set; } = exportType;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// SFML.Graphics.Image 帧对象包装类, 将接管给定的 image 对象生命周期
|
||||
/// </summary>
|
||||
|
||||
@@ -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
|
||||
/// <summary>
|
||||
/// 导出器基类
|
||||
/// </summary>
|
||||
public abstract class Exporter(ExportArgs exportArgs) : ImplementationResolver<Exporter, ExportImplementationAttribute, ExportType>
|
||||
public abstract class Exporter : IDisposable
|
||||
{
|
||||
/// <summary>
|
||||
/// 仅源像素混合模式
|
||||
/// </summary>
|
||||
private static readonly SFML.Graphics.BlendMode SrcOnlyBlendMode = new(SFML.Graphics.BlendMode.Factor.One, SFML.Graphics.BlendMode.Factor.Zero);
|
||||
|
||||
/// <summary>
|
||||
/// 创建指定类型导出器
|
||||
/// </summary>
|
||||
/// <param name="exportType">导出类型</param>
|
||||
/// <param name="exportArgs">与 <paramref name="exportType"/> 匹配的导出参数</param>
|
||||
/// <returns>与 <paramref name="exportType"/> 匹配的导出器</returns>
|
||||
public static Exporter New(ExportType exportType, ExportArgs exportArgs) => New(exportType, [exportArgs]);
|
||||
|
||||
/// <summary>
|
||||
/// 日志器
|
||||
/// </summary>
|
||||
protected Logger logger = LogManager.GetCurrentClassLogger();
|
||||
|
||||
/// <summary>
|
||||
/// 导出参数
|
||||
/// </summary>
|
||||
public ExportArgs ExportArgs { get; } = exportArgs;
|
||||
protected readonly Logger logger = LogManager.GetCurrentClassLogger();
|
||||
|
||||
/// <summary>
|
||||
/// 可用于文件名的时间戳字符串
|
||||
/// </summary>
|
||||
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(); }
|
||||
|
||||
/// <summary>
|
||||
/// 输出文件夹
|
||||
/// </summary>
|
||||
public string? OutputDir { get; set; } = null;
|
||||
|
||||
/// <summary>
|
||||
/// 导出单个
|
||||
/// </summary>
|
||||
public bool IsExportSingle { get; set; } = false;
|
||||
|
||||
/// <summary>
|
||||
/// 画面分辨率
|
||||
/// </summary>
|
||||
public Size Resolution { get; set; } = new(100, 100);
|
||||
|
||||
/// <summary>
|
||||
/// 渲染视窗, 接管对象生命周期
|
||||
/// </summary>
|
||||
public SFML.Graphics.View View { get => view; set { view.Dispose(); view = value; } }
|
||||
private SFML.Graphics.View view = new();
|
||||
|
||||
/// <summary>
|
||||
/// 是否仅渲染选中
|
||||
/// </summary>
|
||||
public bool RenderSelectedOnly { get; set; } = false;
|
||||
|
||||
/// <summary>
|
||||
/// 背景颜色
|
||||
/// </summary>
|
||||
public SFML.Graphics.Color BackgroundColor
|
||||
{
|
||||
get => backgroundColor;
|
||||
set
|
||||
{
|
||||
backgroundColor = value;
|
||||
var bcPma = value;
|
||||
var a = bcPma.A / 255f;
|
||||
bcPma.R = (byte)(bcPma.R * a);
|
||||
bcPma.G = (byte)(bcPma.G * a);
|
||||
bcPma.B = (byte)(bcPma.B * a);
|
||||
BackgroundColorPma = bcPma;
|
||||
}
|
||||
}
|
||||
private SFML.Graphics.Color backgroundColor = SFML.Graphics.Color.Transparent;
|
||||
|
||||
/// <summary>
|
||||
/// 预乘后的背景颜色
|
||||
/// </summary>
|
||||
public SFML.Graphics.Color BackgroundColorPma { get; private set; } = SFML.Graphics.Color.Transparent;
|
||||
|
||||
/// <summary>
|
||||
/// 获取供渲染的 SFML.Graphics.RenderTexture
|
||||
/// </summary>
|
||||
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
|
||||
/// </summary>
|
||||
protected abstract void ExportIndividual(Spine.Spine[] spinesToRender, BackgroundWorker? worker = null);
|
||||
|
||||
/// <summary>
|
||||
/// 检查参数是否合法并规范化参数值, 否则返回用户错误原因
|
||||
/// </summary>
|
||||
public virtual string? Validate()
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(OutputDir) && File.Exists(OutputDir))
|
||||
return "输出文件夹无效";
|
||||
if (!string.IsNullOrWhiteSpace(OutputDir) && !Directory.Exists(OutputDir))
|
||||
return $"文件夹 {OutputDir} 不存在";
|
||||
if (IsExportSingle && string.IsNullOrWhiteSpace(OutputDir))
|
||||
return "导出单个时必须提供输出文件夹";
|
||||
|
||||
OutputDir = string.IsNullOrWhiteSpace(OutputDir) ? null : Path.GetFullPath(OutputDir);
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 执行导出
|
||||
/// </summary>
|
||||
/// <param name="spines">要进行导出的 Spine 列表</param>
|
||||
/// <param name="worker">用来执行该函数的 worker</param>
|
||||
/// <exception cref="ArgumentException"></exception>
|
||||
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();
|
||||
|
||||
@@ -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
|
||||
{
|
||||
/// <summary>
|
||||
/// 使用 FFmpeg 的视频导出器
|
||||
/// </summary>
|
||||
[ExportImplementation(ExportType.Gif)]
|
||||
[ExportImplementation(ExportType.Mp4)]
|
||||
[ExportImplementation(ExportType.Webm)]
|
||||
[ExportImplementation(ExportType.Mkv)]
|
||||
[ExportImplementation(ExportType.Mov)]
|
||||
[ExportImplementation(ExportType.Custom)]
|
||||
public class FFmpegVideoExporter : VideoExporter
|
||||
public abstract class FFmpegVideoExporter : VideoExporter
|
||||
{
|
||||
public FFmpegVideoExporter(FFmpegVideoExportArgs exportArgs) : base(exportArgs) { }
|
||||
/// <summary>
|
||||
/// 文件格式
|
||||
/// </summary>
|
||||
public abstract string Format { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 文件名后缀
|
||||
/// </summary>
|
||||
public abstract string Suffix { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 文件名后缀
|
||||
/// </summary>
|
||||
public string CustomArgument { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 要追加在文件名末尾的信息字串, 首尾不需要提供额外分隔符
|
||||
/// </summary>
|
||||
public abstract string FileNameNoteSuffix { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 获取输出附加选项
|
||||
/// </summary>
|
||||
public virtual void SetOutputOptions(FFMpegArgumentOptions options) => options.ForceFormat(Format).WithCustomArgument(CustomArgument);
|
||||
|
||||
public override string? Validate()
|
||||
{
|
||||
if (base.Validate() is string error)
|
||||
return error;
|
||||
if (string.IsNullOrWhiteSpace(Format))
|
||||
return "需要提供有效的格式";
|
||||
if (string.IsNullOrWhiteSpace(Suffix))
|
||||
return "需要提供有效的文件名后缀";
|
||||
return null;
|
||||
}
|
||||
|
||||
protected override void ExportSingle(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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
{
|
||||
/// <summary>
|
||||
/// 单帧画面导出器
|
||||
/// </summary>
|
||||
[ExportImplementation(ExportType.Frame)]
|
||||
public class FrameExporter : SpineViewer.Exporter.Exporter
|
||||
public class FrameExporter : Exporter
|
||||
{
|
||||
public FrameExporter(FrameExportArgs exportArgs) : base(exportArgs) { }
|
||||
/// <summary>
|
||||
/// 单帧画面格式
|
||||
/// </summary>
|
||||
public ImageFormat ImageFormat
|
||||
{
|
||||
get => imageFormat;
|
||||
set
|
||||
{
|
||||
if (value == ImageFormat.MemoryBmp) value = ImageFormat.Bmp;
|
||||
imageFormat = value;
|
||||
}
|
||||
}
|
||||
private ImageFormat imageFormat = ImageFormat.Png;
|
||||
|
||||
/// <summary>
|
||||
/// DPI
|
||||
/// </summary>
|
||||
public SizeF DPI
|
||||
{
|
||||
get => dpi;
|
||||
set
|
||||
{
|
||||
if (value.Width <= 0) value.Width = 144;
|
||||
if (value.Height <= 0) value.Height = 144;
|
||||
dpi = value;
|
||||
}
|
||||
}
|
||||
private SizeF dpi = new(144, 144);
|
||||
|
||||
protected override void ExportSingle(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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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
|
||||
{
|
||||
/// <summary>
|
||||
/// 帧序列导出器
|
||||
/// </summary>
|
||||
[ExportImplementation(ExportType.FrameSequence)]
|
||||
public class FrameSequenceExporter : VideoExporter
|
||||
{
|
||||
public FrameSequenceExporter(FrameSequenceExportArgs exportArgs) : base(exportArgs) { }
|
||||
/// <summary>
|
||||
/// 文件名后缀, 同时决定帧图像格式, 支持的格式为 <c>".png", ".jpg", ".tga", ".bmp"</c>
|
||||
/// </summary>
|
||||
public string Suffix { get; set; } = ".png";
|
||||
|
||||
protected override void ExportSingle(Spine.Spine[] spinesToRender, BackgroundWorker? worker = null)
|
||||
{
|
||||
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
|
||||
@@ -6,15 +6,14 @@ using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace SpineViewer.Exporter.Implementations.ExportArgs
|
||||
namespace SpineViewer.Exporter
|
||||
{
|
||||
/// <summary>
|
||||
/// GIF 导出参数
|
||||
/// </summary>
|
||||
[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
|
||||
/// <summary>
|
||||
/// 调色板最大颜色数量
|
||||
/// </summary>
|
||||
[Category("[3] 格式参数"), DisplayName("调色板最大颜色数量"), Description("设置调色板使用的最大颜色数量, 越多则色彩保留程度越高")]
|
||||
public uint MaxColors { get => maxColors; set => maxColors = Math.Clamp(value, 2, 256); }
|
||||
private uint maxColors = 256;
|
||||
|
||||
/// <summary>
|
||||
/// 透明度阈值
|
||||
/// </summary>
|
||||
[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}";
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
{
|
||||
/// <summary>
|
||||
/// 使用 FFmpeg 视频导出参数
|
||||
/// </summary>
|
||||
public abstract class FFmpegVideoExportArgs : VideoExportArgs
|
||||
{
|
||||
public FFmpegVideoExportArgs(Size resolution, SFML.Graphics.View view, bool renderSelectedOnly) : base(resolution, view, renderSelectedOnly) { }
|
||||
|
||||
/// <summary>
|
||||
/// 文件格式
|
||||
/// </summary>
|
||||
[Category("[2] FFmpeg 基本参数"), DisplayName("文件格式"), Description("-f, 文件格式")]
|
||||
public abstract string Format { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 文件名后缀
|
||||
/// </summary>
|
||||
[Category("[2] FFmpeg 基本参数"), DisplayName("文件名后缀"), Description("文件名后缀")]
|
||||
public abstract string Suffix { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 文件名后缀
|
||||
/// </summary>
|
||||
[Category("[2] FFmpeg 基本参数"), DisplayName("自定义参数"), Description("提供给 FFmpeg 的自定义参数, 除非很清楚自己在做什么, 否则请勿填写此参数")]
|
||||
public string CustomArgument { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 获取输出附加选项
|
||||
/// </summary>
|
||||
public virtual void SetOutputOptions(FFMpegArgumentOptions options) => options.ForceFormat(Format).WithCustomArgument(CustomArgument);
|
||||
|
||||
/// <summary>
|
||||
/// 要追加在文件名末尾的信息字串, 首尾不需要提供额外分隔符
|
||||
/// </summary>
|
||||
[Browsable(false)]
|
||||
public abstract string FileNameNoteSuffix { get; }
|
||||
|
||||
public override string? Validate()
|
||||
{
|
||||
if (base.Validate() is string error)
|
||||
return error;
|
||||
if (string.IsNullOrWhiteSpace(Format))
|
||||
return "需要提供有效的格式";
|
||||
if (string.IsNullOrWhiteSpace(Suffix))
|
||||
return "需要提供有效的文件名后缀";
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,58 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using System.Drawing.Imaging;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace SpineViewer.Exporter.Implementations.ExportArgs
|
||||
{
|
||||
/// <summary>
|
||||
/// 单帧画面导出参数
|
||||
/// </summary>
|
||||
[ExportImplementation(ExportType.Frame)]
|
||||
public class FrameExportArgs : SpineViewer.Exporter.ExportArgs
|
||||
{
|
||||
public FrameExportArgs(Size resolution, SFML.Graphics.View view, bool renderSelectedOnly) : base(resolution, view, renderSelectedOnly) { }
|
||||
|
||||
/// <summary>
|
||||
/// 单帧画面格式
|
||||
/// </summary>
|
||||
[TypeConverter(typeof(ImageFormatConverter))]
|
||||
[Category("[1] 单帧画面"), DisplayName("图像格式")]
|
||||
public ImageFormat ImageFormat
|
||||
{
|
||||
get => imageFormat;
|
||||
set
|
||||
{
|
||||
if (value == ImageFormat.MemoryBmp) value = ImageFormat.Bmp;
|
||||
imageFormat = value;
|
||||
}
|
||||
}
|
||||
private ImageFormat imageFormat = ImageFormat.Png;
|
||||
|
||||
/// <summary>
|
||||
/// 文件名后缀
|
||||
/// </summary>
|
||||
[Category("[1] 单帧画面"), DisplayName("文件名后缀"), Description("与图像格式匹配的文件名后缀")]
|
||||
public string Suffix { get => imageFormat.GetSuffix(); }
|
||||
|
||||
/// <summary>
|
||||
/// DPI
|
||||
/// </summary>
|
||||
[TypeConverter(typeof(SizeFConverter))]
|
||||
[Category("[1] 单帧画面"), DisplayName("DPI"), Description("导出图像的每英寸像素数,用于调整图像的物理尺寸")]
|
||||
public SizeF DPI
|
||||
{
|
||||
get => dpi;
|
||||
set
|
||||
{
|
||||
if (value.Width <= 0) value.Width = 144;
|
||||
if (value.Height <= 0) value.Height = 144;
|
||||
dpi = value;
|
||||
}
|
||||
}
|
||||
private SizeF dpi = new(144, 144);
|
||||
}
|
||||
}
|
||||
@@ -1,26 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using System.Drawing.Imaging;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace SpineViewer.Exporter.Implementations.ExportArgs
|
||||
{
|
||||
/// <summary>
|
||||
/// 帧序列导出参数
|
||||
/// </summary>
|
||||
[ExportImplementation(ExportType.FrameSequence)]
|
||||
public class FrameSequenceExportArgs : VideoExportArgs
|
||||
{
|
||||
public FrameSequenceExportArgs(Size resolution, SFML.Graphics.View view, bool renderSelectedOnly) : base(resolution, view, renderSelectedOnly) { }
|
||||
|
||||
/// <summary>
|
||||
/// 文件名后缀
|
||||
/// </summary>
|
||||
[TypeConverter(typeof(StringEnumConverter)), StringEnumConverter.StandardValues(".png", ".jpg", ".tga", ".bmp")]
|
||||
[Category("[2] 帧序列参数"), DisplayName("文件名后缀"), Description("帧文件的后缀,同时决定帧图像格式")]
|
||||
public string Suffix { get; set; } = ".png";
|
||||
}
|
||||
}
|
||||
@@ -1,43 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace SpineViewer.Exporter.Implementations.ExportArgs
|
||||
{
|
||||
/// <summary>
|
||||
/// 视频导出参数基类
|
||||
/// </summary>
|
||||
public abstract class VideoExportArgs : SpineViewer.Exporter.ExportArgs
|
||||
{
|
||||
public VideoExportArgs(Size resolution, SFML.Graphics.View view, bool renderSelectedOnly) : base(resolution, view, renderSelectedOnly) { }
|
||||
|
||||
/// <summary>
|
||||
/// 导出时长
|
||||
/// </summary>
|
||||
[Category("[1] 视频参数"), DisplayName("时长"), Description("可以从模型列表查看动画时长, 如果小于 0, 则在逐个导出时每个模型使用各自的所有轨道动画时长最大值")]
|
||||
public float Duration
|
||||
{
|
||||
get => duration;
|
||||
set => duration = value < 0 ? -1 : value;
|
||||
}
|
||||
private float duration = -1;
|
||||
|
||||
/// <summary>
|
||||
/// 帧率
|
||||
/// </summary>
|
||||
[Category("[1] 视频参数"), DisplayName("帧率"), Description("每秒画面数")]
|
||||
public float FPS { get; set; } = 60;
|
||||
|
||||
public override string? Validate()
|
||||
{
|
||||
if (base.Validate() is string error)
|
||||
return error;
|
||||
if (ExportSingle && Duration < 0)
|
||||
return "导出单个时导出时长不能为负数";
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -6,15 +6,14 @@ using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace SpineViewer.Exporter.Implementations.ExportArgs
|
||||
namespace SpineViewer.Exporter
|
||||
{
|
||||
/// <summary>
|
||||
/// MKV 导出参数
|
||||
/// </summary>
|
||||
[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
|
||||
/// <summary>
|
||||
/// 编码器
|
||||
/// </summary>
|
||||
[StringEnumConverter.StandardValues("libx264", "libx265", "libvpx-vp9", Customizable = true)]
|
||||
[TypeConverter(typeof(StringEnumConverter))]
|
||||
[Category("[3] 格式参数"), DisplayName("编码器"), Description("-c:v, 要使用的编码器")]
|
||||
public string Codec { get; set; } = "libx265";
|
||||
|
||||
/// <summary>
|
||||
/// CRF
|
||||
/// </summary>
|
||||
[Category("[3] 格式参数"), DisplayName("CRF"), Description("-crf, 取值范围 0-63, 建议范围 18-28, 默认取值 23, 数值越小则输出质量越高")]
|
||||
public int CRF { get => crf; set => crf = Math.Clamp(value, 0, 63); }
|
||||
private int crf = 23;
|
||||
|
||||
/// <summary>
|
||||
/// 像素格式
|
||||
/// </summary>
|
||||
[StringEnumConverter.StandardValues("yuv420p", "yuv422p", "yuv444p", "yuva420p", Customizable = true)]
|
||||
[TypeConverter(typeof(StringEnumConverter))]
|
||||
[Category("[3] 格式参数"), DisplayName("像素格式"), Description("-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}";
|
||||
}
|
||||
}
|
||||
@@ -6,15 +6,14 @@ using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace SpineViewer.Exporter.Implementations.ExportArgs
|
||||
namespace SpineViewer.Exporter
|
||||
{
|
||||
/// <summary>
|
||||
/// MOV 导出参数
|
||||
/// </summary>
|
||||
[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
|
||||
/// <summary>
|
||||
/// 编码器
|
||||
/// </summary>
|
||||
[StringEnumConverter.StandardValues("prores_ks", Customizable = true)]
|
||||
[TypeConverter(typeof(StringEnumConverter))]
|
||||
[Category("[3] 格式参数"), DisplayName("编码器"), Description("-c:v, 要使用的编码器")]
|
||||
public string Codec { get; set; } = "prores_ks";
|
||||
|
||||
/// <summary>
|
||||
/// 预设
|
||||
/// </summary>
|
||||
[StringEnumConverter.StandardValues("auto", "proxy", "lt", "standard", "hq", "4444", "4444xq")]
|
||||
[TypeConverter(typeof(StringEnumConverter))]
|
||||
[Category("[3] 格式参数"), DisplayName("预设"), Description("-profile, 预设配置")]
|
||||
public string Profile { get; set; } = "auto";
|
||||
|
||||
/// <summary>
|
||||
/// 像素格式
|
||||
/// </summary>
|
||||
[StringEnumConverter.StandardValues("yuv422p10le", "yuv444p10le", "yuva444p10le", Customizable = true)]
|
||||
[TypeConverter(typeof(StringEnumConverter))]
|
||||
[Category("[3] 格式参数"), DisplayName("像素格式"), Description("-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}";
|
||||
}
|
||||
}
|
||||
@@ -6,15 +6,14 @@ using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace SpineViewer.Exporter.Implementations.ExportArgs
|
||||
namespace SpineViewer.Exporter
|
||||
{
|
||||
/// <summary>
|
||||
/// MP4 导出参数
|
||||
/// </summary>
|
||||
[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
|
||||
/// <summary>
|
||||
/// 编码器
|
||||
/// </summary>
|
||||
[StringEnumConverter.StandardValues("libx264", "libx265", Customizable = true)]
|
||||
[TypeConverter(typeof(StringEnumConverter))]
|
||||
[Category("[3] 格式参数"), DisplayName("编码器"), Description("-c:v, 要使用的编码器")]
|
||||
public string Codec { get; set; } = "libx264";
|
||||
|
||||
/// <summary>
|
||||
/// CRF
|
||||
/// </summary>
|
||||
[Category("[3] 格式参数"), DisplayName("CRF"), Description("-crf, 取值范围 0-63, 建议范围 18-28, 默认取值 23, 数值越小则输出质量越高")]
|
||||
public int CRF { get => crf; set => crf = Math.Clamp(value, 0, 63); }
|
||||
private int crf = 23;
|
||||
|
||||
/// <summary>
|
||||
/// 像素格式
|
||||
/// </summary>
|
||||
[StringEnumConverter.StandardValues("yuv420p", "yuv422p", "yuv444p", Customizable = true)]
|
||||
[TypeConverter(typeof(StringEnumConverter))]
|
||||
[Category("[3] 格式参数"), DisplayName("像素格式"), Description("-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}";
|
||||
}
|
||||
}
|
||||
@@ -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<PropertyDescriptor>
|
||||
{
|
||||
// 定义 R, G, B, A 四个字段的描述器
|
||||
new SFMLColorPropertyDescriptor(typeof(SFML.Graphics.Color), "R", typeof(byte)),
|
||||
new SFMLColorPropertyDescriptor(typeof(SFML.Graphics.Color), "G", typeof(byte)),
|
||||
new SFMLColorPropertyDescriptor(typeof(SFML.Graphics.Color), "B", typeof(byte)),
|
||||
new SFMLColorPropertyDescriptor(typeof(SFML.Graphics.Color), "A", typeof(byte))
|
||||
};
|
||||
|
||||
// 返回自定义属性集合
|
||||
return new PropertyDescriptorCollection(properties.ToArray());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
{
|
||||
/// <summary>
|
||||
/// 视频导出基类
|
||||
/// </summary>
|
||||
public abstract class VideoExporter : SpineViewer.Exporter.Exporter
|
||||
public abstract class VideoExporter : Exporter
|
||||
{
|
||||
public VideoExporter(VideoExportArgs exportArgs) : base(exportArgs) { }
|
||||
/// <summary>
|
||||
/// 导出时长
|
||||
/// </summary>
|
||||
public float Duration { get => duration; set => duration = value < 0 ? -1 : value; }
|
||||
private float duration = -1;
|
||||
|
||||
/// <summary>
|
||||
/// 帧率
|
||||
/// </summary>
|
||||
public float FPS { get; set; } = 60;
|
||||
|
||||
public override string? Validate()
|
||||
{
|
||||
if (base.Validate() is string error)
|
||||
return error;
|
||||
if (IsExportSingle && Duration < 0)
|
||||
return "导出单个时导出时长不能为负数";
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 生成单个模型的帧序列
|
||||
/// </summary>
|
||||
protected IEnumerable<SFMLImageVideoFrame> GetFrames(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
|
||||
/// </summary>
|
||||
protected IEnumerable<SFMLImageVideoFrame> GetFrames(Spine.Spine[] spinesToRender, BackgroundWorker? worker = null)
|
||||
{
|
||||
// 导出单个时必须根据 args.Duration 决定导出时长
|
||||
var args = (VideoExportArgs)ExportArgs;
|
||||
float delta = 1f / args.FPS;
|
||||
int total = Math.Max(1, (int)(args.Duration * args.FPS)); // 至少导出 1 帧
|
||||
// 导出单个时必须根据 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++)
|
||||
@@ -6,15 +6,14 @@ using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace SpineViewer.Exporter.Implementations.ExportArgs
|
||||
namespace SpineViewer.Exporter
|
||||
{
|
||||
/// <summary>
|
||||
/// WebM 导出参数
|
||||
/// </summary>
|
||||
[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
|
||||
/// <summary>
|
||||
/// 编码器
|
||||
/// </summary>
|
||||
[StringEnumConverter.StandardValues("libvpx-vp9", Customizable = true)]
|
||||
[TypeConverter(typeof(StringEnumConverter))]
|
||||
[Category("[3] 格式参数"), DisplayName("编码器"), Description("-c:v, 要使用的编码器")]
|
||||
public string Codec { get; set; } = "libvpx-vp9";
|
||||
|
||||
/// <summary>
|
||||
/// CRF
|
||||
/// </summary>
|
||||
[Category("[3] 格式参数"), DisplayName("CRF"), Description("-crf, 取值范围 0-63, 建议范围 18-28, 默认取值 23, 数值越小则输出质量越高")]
|
||||
public int CRF { get => crf; set => crf = Math.Clamp(value, 0, 63); }
|
||||
private int crf = 23;
|
||||
|
||||
/// <summary>
|
||||
/// 像素格式
|
||||
/// </summary>
|
||||
[StringEnumConverter.StandardValues("yuv420p", "yuv422p", "yuv444p", "yuva420p", Customizable = true)]
|
||||
[TypeConverter(typeof(StringEnumConverter))]
|
||||
[Category("[3] 格式参数"), DisplayName("像素格式"), Description("-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}";
|
||||
}
|
||||
}
|
||||
@@ -5,7 +5,7 @@ using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace SpineViewer
|
||||
namespace SpineViewer.Extensions
|
||||
{
|
||||
public static class NLogExtension
|
||||
{
|
||||
@@ -4,12 +4,12 @@ using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace SpineViewer.Spine
|
||||
namespace SpineViewer.Extensions
|
||||
{
|
||||
/// <summary>
|
||||
/// SFML 混合模式, 预乘模式下输入和输出的像素值都是预乘的
|
||||
/// </summary>
|
||||
public static class BlendModeSFML
|
||||
public static class SFMLBlendMode
|
||||
{
|
||||
///// <summary>
|
||||
///// Normal Blend, 无预乘, 仅在 dst.a 是 1 时得到正确结果, 其余情况是有偏结果
|
||||
@@ -110,5 +110,13 @@ namespace SpineViewer.Spine
|
||||
SFML.Graphics.BlendMode.Factor.OneMinusSrcAlpha,
|
||||
SFML.Graphics.BlendMode.Equation.Add
|
||||
);
|
||||
|
||||
/// <summary>
|
||||
/// 仅源像素混合模式
|
||||
/// </summary>
|
||||
public static readonly SFML.Graphics.BlendMode SourceOnly = new(
|
||||
SFML.Graphics.BlendMode.Factor.One,
|
||||
SFML.Graphics.BlendMode.Factor.Zero
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -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
|
||||
{
|
||||
/// <summary>
|
||||
/// 用于非预乘纹理的 fragment shader, 乘上了插值后的透明度用于实现透明度变化(插值预乘), 并且输出预乘后的像素值
|
||||
50
SpineViewer/MainForm.Designer.cs
generated
50
SpineViewer/MainForm.Designer.cs
generated
@@ -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;
|
||||
//
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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;
|
||||
|
||||
/// <summary>
|
||||
/// 文件格式
|
||||
/// </summary>
|
||||
[Category("[3] 自定义参数"), DisplayName("文件格式"), Description("文件格式")]
|
||||
public string CustomFormat { get; set; } = "mp4";
|
||||
|
||||
/// <summary>
|
||||
/// 文件名后缀
|
||||
/// </summary>
|
||||
[Category("[3] 自定义参数"), DisplayName("文件名后缀"), Description("文件名后缀")]
|
||||
public string CustomSuffix { get; set; } = ".mp4";
|
||||
}
|
||||
}
|
||||
56
SpineViewer/PropertyGridWrappers/Exporter/ExporterWrapper.cs
Normal file
56
SpineViewer/PropertyGridWrappers/Exporter/ExporterWrapper.cs
Normal file
@@ -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;
|
||||
|
||||
/// <summary>
|
||||
/// 输出文件夹
|
||||
/// </summary>
|
||||
[Editor(typeof(FolderNameEditor), typeof(UITypeEditor))]
|
||||
[Category("[0] 导出"), DisplayName("输出文件夹"), Description("逐个导出时可以留空,将逐个导出到模型自身所在目录")]
|
||||
public string? OutputDir { get => Exporter.OutputDir; set => Exporter.OutputDir = value; }
|
||||
|
||||
/// <summary>
|
||||
/// 导出单个
|
||||
/// </summary>
|
||||
[Category("[0] 导出"), DisplayName("导出单个"), Description("是否将模型在同一个画面上导出单个文件,否则逐个导出模型")]
|
||||
public bool IsExportSingle { get => Exporter.IsExportSingle; set => Exporter.IsExportSingle = value; }
|
||||
|
||||
/// <summary>
|
||||
/// 画面分辨率
|
||||
/// </summary>
|
||||
[TypeConverter(typeof(SizeConverter))]
|
||||
[Category("[0] 导出"), DisplayName("分辨率"), Description("画面的宽高像素大小,请在预览画面参数面板进行调整")]
|
||||
public Size Resolution { get => Exporter.Resolution; }
|
||||
|
||||
/// <summary>
|
||||
/// 渲染视窗
|
||||
/// </summary>
|
||||
[Category("[0] 导出"), DisplayName("视图"), Description("画面的视图参数,请在预览画面参数面板进行调整")]
|
||||
public SFML.Graphics.View View { get => Exporter.View; }
|
||||
|
||||
/// <summary>
|
||||
/// 是否仅渲染选中
|
||||
/// </summary>
|
||||
[Category("[0] 导出"), DisplayName("仅渲染选中"), Description("是否仅导出选中的模型,请在预览画面参数面板进行调整")]
|
||||
public bool RenderSelectedOnly { get => Exporter.RenderSelectedOnly; }
|
||||
|
||||
/// <summary>
|
||||
/// 背景颜色
|
||||
/// </summary>
|
||||
[Editor(typeof(SFMLColorEditor), typeof(UITypeEditor))]
|
||||
[TypeConverter(typeof(SFMLColorConverter))]
|
||||
[Category("[0] 导出"), DisplayName("背景颜色"), Description("要使用的背景色, 格式为 #RRGGBBAA")]
|
||||
public SFML.Graphics.Color BackgroundColor { get => Exporter.BackgroundColor; set => Exporter.BackgroundColor = value; }
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
/// <summary>
|
||||
/// 文件格式
|
||||
/// </summary>
|
||||
[Category("[2] FFmpeg 基本参数"), DisplayName("文件格式"), Description("-f, 文件格式")]
|
||||
public string Format => Exporter.Format;
|
||||
|
||||
/// <summary>
|
||||
/// 文件名后缀
|
||||
/// </summary>
|
||||
[Category("[2] FFmpeg 基本参数"), DisplayName("文件名后缀"), Description("文件名后缀")]
|
||||
public string Suffix => Exporter.Format;
|
||||
|
||||
/// <summary>
|
||||
/// 文件名后缀
|
||||
/// </summary>
|
||||
[Category("[2] FFmpeg 基本参数"), DisplayName("自定义参数"), Description("提供给 FFmpeg 的自定义参数, 除非很清楚自己在做什么, 否则请勿填写此参数")]
|
||||
public string CustomArgument { get => Exporter.CustomArgument; set => Exporter.CustomArgument = value; }
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
/// <summary>
|
||||
/// 单帧画面格式
|
||||
/// </summary>
|
||||
[TypeConverter(typeof(ImageFormatConverter))]
|
||||
[Category("[1] 单帧画面"), DisplayName("图像格式")]
|
||||
public ImageFormat ImageFormat { get => Exporter.ImageFormat; set => Exporter.ImageFormat = value; }
|
||||
|
||||
/// <summary>
|
||||
/// 文件名后缀
|
||||
/// </summary>
|
||||
[Category("[1] 单帧画面"), DisplayName("文件名后缀"), Description("与图像格式匹配的文件名后缀")]
|
||||
public string Suffix { get => Exporter.ImageFormat.GetSuffix(); }
|
||||
|
||||
/// <summary>
|
||||
/// DPI
|
||||
/// </summary>
|
||||
[TypeConverter(typeof(SizeFConverter))]
|
||||
[Category("[1] 单帧画面"), DisplayName("DPI"), Description("导出图像的每英寸像素数,用于调整图像的物理尺寸")]
|
||||
public SizeF DPI { get => Exporter.DPI; set => Exporter.DPI = value; }
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
/// <summary>
|
||||
/// 文件名后缀
|
||||
/// </summary>
|
||||
[TypeConverter(typeof(StringEnumConverter)), StringEnumConverter.StandardValues(".png", ".jpg", ".tga", ".bmp")]
|
||||
[Category("[2] 帧序列参数"), DisplayName("文件名后缀"), Description("帧文件的后缀,同时决定帧图像格式")]
|
||||
public string Suffix { get => Exporter.Suffix; set => Exporter.Suffix = value; }
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
/// <summary>
|
||||
/// 调色板最大颜色数量
|
||||
/// </summary>
|
||||
[Category("[3] 格式参数"), DisplayName("调色板最大颜色数量"), Description("设置调色板使用的最大颜色数量, 越多则色彩保留程度越高")]
|
||||
public uint MaxColors { get => Exporter.MaxColors; set => Exporter.MaxColors = value; }
|
||||
|
||||
/// <summary>
|
||||
/// 透明度阈值
|
||||
/// </summary>
|
||||
[Category("[3] 格式参数"), DisplayName("透明度阈值"), Description("小于该值的像素点会被认为是透明像素")]
|
||||
public byte AlphaThreshold { get => Exporter.AlphaThreshold; set => Exporter.AlphaThreshold = value; }
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
/// <summary>
|
||||
/// 编码器
|
||||
/// </summary>
|
||||
[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; }
|
||||
|
||||
/// <summary>
|
||||
/// CRF
|
||||
/// </summary>
|
||||
[Category("[3] 格式参数"), DisplayName("CRF"), Description("-crf, 取值范围 0-63, 建议范围 18-28, 默认取值 23, 数值越小则输出质量越高")]
|
||||
public int CRF { get => Exporter.CRF; set => Exporter.CRF = value; }
|
||||
|
||||
/// <summary>
|
||||
/// 像素格式
|
||||
/// </summary>
|
||||
[StringEnumConverter.StandardValues("yuv420p", "yuv422p", "yuv444p", "yuva420p", Customizable = true)]
|
||||
[TypeConverter(typeof(StringEnumConverter))]
|
||||
[Category("[3] 格式参数"), DisplayName("像素格式"), Description("-pix_fmt, 要使用的像素格式")]
|
||||
public string PixelFormat { get => Exporter.PixelFormat; set => Exporter.PixelFormat = value; }
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
/// <summary>
|
||||
/// 编码器
|
||||
/// </summary>
|
||||
[StringEnumConverter.StandardValues("prores_ks", Customizable = true)]
|
||||
[TypeConverter(typeof(StringEnumConverter))]
|
||||
[Category("[3] 格式参数"), DisplayName("编码器"), Description("-c:v, 要使用的编码器")]
|
||||
public string Codec { get => Exporter.Codec; set => Exporter.Codec = value; }
|
||||
|
||||
/// <summary>
|
||||
/// 预设
|
||||
/// </summary>
|
||||
[StringEnumConverter.StandardValues("auto", "proxy", "lt", "standard", "hq", "4444", "4444xq")]
|
||||
[TypeConverter(typeof(StringEnumConverter))]
|
||||
[Category("[3] 格式参数"), DisplayName("预设"), Description("-profile, 预设配置")]
|
||||
public string Profile { get => Exporter.Profile; set => Exporter.Profile = value; }
|
||||
|
||||
/// <summary>
|
||||
/// 像素格式
|
||||
/// </summary>
|
||||
[StringEnumConverter.StandardValues("yuv422p10le", "yuv444p10le", "yuva444p10le", Customizable = true)]
|
||||
[TypeConverter(typeof(StringEnumConverter))]
|
||||
[Category("[3] 格式参数"), DisplayName("像素格式"), Description("-pix_fmt, 要使用的像素格式")]
|
||||
public string PixelFormat { get => Exporter.PixelFormat; set => Exporter.PixelFormat = value; }
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
/// <summary>
|
||||
/// 编码器
|
||||
/// </summary>
|
||||
[StringEnumConverter.StandardValues("libx264", "libx265", Customizable = true)]
|
||||
[TypeConverter(typeof(StringEnumConverter))]
|
||||
[Category("[3] 格式参数"), DisplayName("编码器"), Description("-c:v, 要使用的编码器")]
|
||||
public string Codec { get => Exporter.Codec; set => Exporter.Codec = value; }
|
||||
|
||||
/// <summary>
|
||||
/// CRF
|
||||
/// </summary>
|
||||
[Category("[3] 格式参数"), DisplayName("CRF"), Description("-crf, 取值范围 0-63, 建议范围 18-28, 默认取值 23, 数值越小则输出质量越高")]
|
||||
public int CRF { get => Exporter.CRF; set => Exporter.CRF = value; }
|
||||
|
||||
/// <summary>
|
||||
/// 像素格式
|
||||
/// </summary>
|
||||
[StringEnumConverter.StandardValues("yuv420p", "yuv422p", "yuv444p", Customizable = true)]
|
||||
[TypeConverter(typeof(StringEnumConverter))]
|
||||
[Category("[3] 格式参数"), DisplayName("像素格式"), Description("-pix_fmt, 要使用的像素格式")]
|
||||
public string PixelFormat { get => Exporter.PixelFormat; set => Exporter.PixelFormat = value; }
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
/// <summary>
|
||||
/// 导出时长
|
||||
/// </summary>
|
||||
[Category("[1] 视频参数"), DisplayName("时长"), Description("可以从模型列表查看动画时长, 如果小于 0, 则在逐个导出时每个模型使用各自的所有轨道动画时长最大值")]
|
||||
public float Duration { get => Exporter.Duration; set => Exporter.Duration = value; }
|
||||
|
||||
/// <summary>
|
||||
/// 帧率
|
||||
/// </summary>
|
||||
[Category("[1] 视频参数"), DisplayName("帧率"), Description("每秒画面数")]
|
||||
public float FPS { get => Exporter.FPS; set => Exporter.FPS = value; }
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
/// <summary>
|
||||
/// 编码器
|
||||
/// </summary>
|
||||
[StringEnumConverter.StandardValues("libvpx-vp9", Customizable = true)]
|
||||
[TypeConverter(typeof(StringEnumConverter))]
|
||||
[Category("[3] 格式参数"), DisplayName("编码器"), Description("-c:v, 要使用的编码器")]
|
||||
public string Codec { get => Exporter.Codec; set => Exporter.Codec = value; }
|
||||
|
||||
/// <summary>
|
||||
/// CRF
|
||||
/// </summary>
|
||||
[Category("[3] 格式参数"), DisplayName("CRF"), Description("-crf, 取值范围 0-63, 建议范围 18-28, 默认取值 23, 数值越小则输出质量越高")]
|
||||
public int CRF { get => Exporter.CRF; set => Exporter.CRF = value; }
|
||||
|
||||
/// <summary>
|
||||
/// 像素格式
|
||||
/// </summary>
|
||||
[StringEnumConverter.StandardValues("yuv420p", "yuv422p", "yuv444p", "yuva420p", Customizable = true)]
|
||||
[TypeConverter(typeof(StringEnumConverter))]
|
||||
[Category("[3] 格式参数"), DisplayName("像素格式"), Description("-pix_fmt, 要使用的像素格式")]
|
||||
public string PixelFormat { get => Exporter.PixelFormat; set => Exporter.PixelFormat = value; }
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
{
|
||||
/// <summary>
|
||||
/// 对轨道索引的包装类, 能够在面板上显示例如时长的属性, 但是处理该属性时按字符串去处理, 例如 ToString 和判断对象相等都是用动画名称实现逻辑
|
||||
/// 对轨道索引属性的包装类, 能够在面板上显示例如时长的属性, 但是处理该属性时按字符串去处理, 例如 ToString 和判断对象相等都是用动画名称实现逻辑
|
||||
/// </summary>
|
||||
/// <param name="spine"></param>
|
||||
/// <param name="i"></param>
|
||||
[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
|
||||
/// <summary>
|
||||
/// 哈希码需要和 Equals 行为类似
|
||||
/// </summary>
|
||||
public override int GetHashCode() => (typeof(TrackWrapper).FullName + ToString()).GetHashCode();
|
||||
public override int GetHashCode() => HashCode.Combine(typeof(TrackWrapper).FullName.GetHashCode(), ToString().GetHashCode());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 轨道属性描述符, 实现对属性的读取和赋值
|
||||
/// 用于在 PropertyGrid 上显示 Spine 动画列表的包装类
|
||||
/// </summary>
|
||||
/// <param name="i">轨道索引</param>
|
||||
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;
|
||||
|
||||
/// <summary>
|
||||
/// 得到一个轨道包装类, 允许用户查看或者修改具体的属性值, 这个地方决定了在面板上看到的是一个对象及其属性
|
||||
/// 轨道属性描述符, 实现对属性的读取和赋值
|
||||
/// </summary>
|
||||
public override object? GetValue(object? component)
|
||||
/// <param name="i">轨道索引</param>
|
||||
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;
|
||||
|
||||
/// <summary>
|
||||
/// 允许通过字符串赋值修改该轨道的动画, 这里决定了当其他地方的调用 (比如 Converter) 通过 value 来设置属性值的时候应该怎么处理
|
||||
/// </summary>
|
||||
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;
|
||||
|
||||
/// <summary>
|
||||
/// 得到一个轨道包装类, 允许用户查看或者修改具体的属性值, 这个地方决定了在面板上看到的是一个对象及其属性
|
||||
/// </summary>
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 允许通过字符串赋值修改该轨道的动画, 这里决定了当其他地方的调用 (比如 Converter) 通过 value 来设置属性值的时候应该怎么处理
|
||||
/// </summary>
|
||||
public override void SetValue(object? component, object? value)
|
||||
{
|
||||
if (component is SpineAnimationWrapper tracks)
|
||||
{
|
||||
if (value is string s)
|
||||
tracks.SetTrackWrapper(idx, s);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// AnimationTracks 动态类型包装类, 用于提供对 Spine 对象多轨道动画的访问能力, 不同轨道将动态生成属性
|
||||
/// </summary>
|
||||
/// <param name="spine">关联的 Spine 对象</param>
|
||||
public class AnimationTracks(Spine spine) : ICustomTypeDescriptor
|
||||
{
|
||||
private static readonly Dictionary<int, TrackWrapperPropertyDescriptor> pdCache = [];
|
||||
[Browsable(false)]
|
||||
public SpineViewer.Spine.Spine Spine { get; } = spine;
|
||||
|
||||
public Spine Spine { get; } = spine;
|
||||
/// <summary>
|
||||
/// 全轨道动画最大时长
|
||||
/// </summary>
|
||||
[DisplayName("全轨道最大时长")]
|
||||
public float AnimationTracksMaxDuration => Spine.GetTrackIndices().Select(i => Spine.GetAnimationDuration(Spine.GetAnimation(i))).Max();
|
||||
|
||||
/// <summary>
|
||||
/// TrackWrapper 属性对象缓存
|
||||
/// </summary>
|
||||
private readonly Dictionary<int, TrackWrapper> trackWrapperProperties = [];
|
||||
|
||||
/// <summary>
|
||||
/// 访问 TrackWrapper 属性 <c>AnimationTracks.Track{i}</c>
|
||||
/// </summary>
|
||||
public TrackWrapper GetTrackWrapper(int i)
|
||||
{
|
||||
if (!trackWrapperProperties.ContainsKey(i))
|
||||
trackWrapperProperties[i] = new TrackWrapper(Spine, i);
|
||||
return trackWrapperProperties[i];
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 设置 TrackWrapper 属性 <c>AnimationTracks.Track{i} = <paramref name="value"/></c>
|
||||
/// </summary>
|
||||
public void SetTrackWrapper(int i, string value) => Spine.SetAnimation(i, value);
|
||||
|
||||
/// <summary>
|
||||
/// 在属性面板悬停可以按轨道顺序显示动画名称
|
||||
/// </summary>
|
||||
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, 似乎继承下来的东西会有问题, 导致某些调用不正确
|
||||
|
||||
/// <summary>
|
||||
/// 属性描述符缓存
|
||||
/// </summary>
|
||||
private static readonly Dictionary<int, TrackWrapperPropertyDescriptor> 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<TrackWrapperPropertyDescriptor>();
|
||||
var props = new PropertyDescriptorCollection(TypeDescriptor.GetProperties(this, attributes, true).Cast<PropertyDescriptor>().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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 访问 TrackWrapper 属性 <c>AnimationTracks.Track{i}</c>
|
||||
/// </summary>
|
||||
public TrackWrapper GetTrackWrapper(int i)
|
||||
{
|
||||
if (!trackWrapperProperties.ContainsKey(i))
|
||||
trackWrapperProperties[i] = new TrackWrapper(Spine, i);
|
||||
return trackWrapperProperties[i];
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 在属性面板悬停可以按轨道顺序显示动画名称
|
||||
/// </summary>
|
||||
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
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
{
|
||||
/// <summary>
|
||||
/// 用于在 PropertyGrid 上显示 Spine 基本信息的包装类
|
||||
/// </summary>
|
||||
public class SpineBaseInfoWrapper(SpineViewer.Spine.Spine spine)
|
||||
{
|
||||
[Browsable(false)]
|
||||
public SpineViewer.Spine.Spine Spine { get; } = spine;
|
||||
|
||||
/// <summary>
|
||||
/// 获取所属版本
|
||||
/// </summary>
|
||||
[TypeConverter(typeof(SpineVersionConverter))]
|
||||
[DisplayName("运行时版本")]
|
||||
public SpineVersion Version => Spine.Version;
|
||||
|
||||
/// <summary>
|
||||
/// 资源所在完整目录
|
||||
/// </summary>
|
||||
[DisplayName("资源目录")]
|
||||
public string AssetsDir => Spine.AssetsDir;
|
||||
|
||||
/// <summary>
|
||||
/// skel 文件完整路径
|
||||
/// </summary>
|
||||
[DisplayName("skel文件路径")]
|
||||
public string SkelPath => Spine.SkelPath;
|
||||
|
||||
/// <summary>
|
||||
/// atlas 文件完整路径
|
||||
/// </summary>
|
||||
[DisplayName("atlas文件路径")]
|
||||
public string AtlasPath => Spine.AtlasPath;
|
||||
|
||||
/// <summary>
|
||||
/// 名称
|
||||
/// </summary>
|
||||
[DisplayName("名称")]
|
||||
public string Name => Spine.Name;
|
||||
|
||||
/// <summary>
|
||||
/// 获取所属文件版本
|
||||
/// </summary>
|
||||
[DisplayName("文件版本")]
|
||||
public string FileVersion => Spine.FileVersion;
|
||||
}
|
||||
}
|
||||
36
SpineViewer/PropertyGridWrappers/Spine/SpineDebugWrapper.cs
Normal file
36
SpineViewer/PropertyGridWrappers/Spine/SpineDebugWrapper.cs
Normal file
@@ -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
|
||||
{
|
||||
/// <summary>
|
||||
/// 用于在 PropertyGrid 上显示 Spine 调试属性的包装类
|
||||
/// </summary>
|
||||
public class SpineDebugWrapper(SpineViewer.Spine.Spine spine)
|
||||
{
|
||||
[Browsable(false)]
|
||||
public SpineViewer.Spine.Spine Spine { get; } = spine;
|
||||
|
||||
/// <summary>
|
||||
/// 显示纹理
|
||||
/// </summary>
|
||||
[DisplayName("纹理")]
|
||||
public bool DebugTexture { get => Spine.DebugTexture; set => Spine.DebugTexture = value; }
|
||||
|
||||
/// <summary>
|
||||
/// 显示包围盒
|
||||
/// </summary>
|
||||
[DisplayName("包围盒")]
|
||||
public bool DebugBounds { get => Spine.DebugBounds; set => Spine.DebugBounds = value; }
|
||||
|
||||
/// <summary>
|
||||
/// 显示骨骼
|
||||
/// </summary>
|
||||
[DisplayName("骨架")]
|
||||
public bool DebugBones { get => Spine.DebugBones; set => Spine.DebugBones = value; }
|
||||
}
|
||||
}
|
||||
30
SpineViewer/PropertyGridWrappers/Spine/SpineRenderWrapper.cs
Normal file
30
SpineViewer/PropertyGridWrappers/Spine/SpineRenderWrapper.cs
Normal file
@@ -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
|
||||
{
|
||||
/// <summary>
|
||||
/// 用于在 PropertyGrid 上显示 Spine 渲染设置的包装类
|
||||
/// </summary>
|
||||
public class SpineRenderWrapper(SpineViewer.Spine.Spine spine)
|
||||
{
|
||||
[Browsable(false)]
|
||||
public SpineViewer.Spine.Spine Spine { get; } = spine;
|
||||
|
||||
/// <summary>
|
||||
/// 是否被隐藏, 被隐藏的模型将仅仅在列表显示, 不参与其他行为
|
||||
/// </summary>
|
||||
[DisplayName("是否隐藏")]
|
||||
public bool IsHidden { get => Spine.IsHidden; set => Spine.IsHidden = value; }
|
||||
|
||||
/// <summary>
|
||||
/// 是否使用预乘Alpha
|
||||
/// </summary>
|
||||
[DisplayName("预乘Alpha通道")]
|
||||
public bool UsePremultipliedAlpha { get => Spine.UsePma; set => Spine.UsePma = value; }
|
||||
}
|
||||
}
|
||||
@@ -1,19 +1,20 @@
|
||||
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
|
||||
{
|
||||
/// <summary>
|
||||
/// 对皮肤的包装类
|
||||
/// </summary>
|
||||
[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;
|
||||
@@ -32,60 +33,94 @@ namespace SpineViewer.Spine
|
||||
return base.Equals(obj);
|
||||
}
|
||||
|
||||
public override int GetHashCode() => (typeof(SkinWrapper).FullName + ToString()).GetHashCode();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 皮肤属性描述符, 实现对属性的读取和赋值
|
||||
/// </summary>
|
||||
/// <param name="spine">关联的 Spine 对象</param>
|
||||
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;
|
||||
|
||||
/// <summary>
|
||||
/// 得到一个 SkinWrapper, 允许用户查看或者修改具体的属性值, 这个地方决定了在面板上看到的是一个对象及其属性
|
||||
/// </summary>
|
||||
public override object? GetValue(object? component)
|
||||
{
|
||||
if (component is SkinManager manager)
|
||||
return manager.GetSkinWrapper(idx);
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 允许通过字符串赋值修改该位置的皮肤
|
||||
/// </summary>
|
||||
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());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// SkinManager 动态类型包装类, 用于提供对 Spine 皮肤列表的管理能力
|
||||
/// </summary>
|
||||
/// <param name="spine">关联的 Spine 对象</param>
|
||||
public class SkinManager(Spine spine) : ICustomTypeDescriptor
|
||||
public class SpineSkinWrapper(SpineViewer.Spine.Spine spine) : ICustomTypeDescriptor
|
||||
{
|
||||
private static readonly Dictionary<int, SkinWrapperPropertyDescriptor> pdCache = [];
|
||||
/// <summary>
|
||||
/// 皮肤属性描述符, 实现对属性的读取和赋值
|
||||
/// </summary>
|
||||
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;
|
||||
|
||||
/// <summary>
|
||||
/// 得到一个 SkinWrapper, 允许用户查看或者修改具体的属性值, 这个地方决定了在面板上看到的是一个对象及其属性
|
||||
/// </summary>
|
||||
public override object? GetValue(object? component)
|
||||
{
|
||||
if (component is SpineSkinWrapper manager)
|
||||
return manager.GetSkinWrapper(idx);
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 允许通过字符串赋值修改该位置的皮肤
|
||||
/// </summary>
|
||||
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;
|
||||
|
||||
/// <summary>
|
||||
/// SkinWrapper 属性缓存
|
||||
/// </summary>
|
||||
private readonly Dictionary<int, SkinWrapper> skinWrapperProperties = [];
|
||||
|
||||
/// <summary>
|
||||
/// 访问 SkinWrapper 属性 <c>SkinManager.Skin{i}</c>
|
||||
/// </summary>
|
||||
public SkinWrapper GetSkinWrapper(int i)
|
||||
{
|
||||
if (!skinWrapperProperties.ContainsKey(i))
|
||||
skinWrapperProperties[i] = new SkinWrapper(Spine, i);
|
||||
return skinWrapperProperties[i];
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 设置 SkinWrapper 属性 <c>SkinManager.Skin{i} = <paramref name="value"/></c>
|
||||
/// </summary>
|
||||
public void SetSkinWrapper(int i, string value) => Spine.ReplaceSkin(i, value);
|
||||
|
||||
/// <summary>
|
||||
/// 在属性面板悬停可以显示已加载的皮肤列表
|
||||
/// </summary>
|
||||
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<int, SkinWrapperPropertyDescriptor> 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<SkinWrapperPropertyDescriptor>();
|
||||
var props = new PropertyDescriptorCollection(TypeDescriptor.GetProperties(this, attributes, true).Cast<PropertyDescriptor>().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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 访问 SkinWrapper 属性 <c>SkinManager.Skin{i}</c>
|
||||
/// </summary>
|
||||
public SkinWrapper GetSkinWrapper(int i)
|
||||
{
|
||||
if (!skinWrapperProperties.ContainsKey(i))
|
||||
skinWrapperProperties[i] = new SkinWrapper(Spine, i);
|
||||
return skinWrapperProperties[i];
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 在属性面板悬停可以显示已加载的皮肤列表
|
||||
/// </summary>
|
||||
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
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
{
|
||||
/// <summary>
|
||||
/// 用于在 PropertyGrid 上显示 Spine 空间变换的包装类
|
||||
/// </summary>
|
||||
public class SpineTransformWrapper(SpineViewer.Spine.Spine spine)
|
||||
{
|
||||
[Browsable(false)]
|
||||
public SpineViewer.Spine.Spine Spine { get; } = spine;
|
||||
|
||||
/// <summary>
|
||||
/// 缩放比例
|
||||
/// </summary>
|
||||
[DisplayName("缩放比例")]
|
||||
public float Scale { get => Spine.Scale; set => Spine.Scale = value; }
|
||||
|
||||
/// <summary>
|
||||
/// 位置
|
||||
/// </summary>
|
||||
[TypeConverter(typeof(PointFConverter))]
|
||||
[DisplayName("位置")]
|
||||
public PointF Position { get => Spine.Position; set => Spine.Position = value; }
|
||||
|
||||
/// <summary>
|
||||
/// 水平翻转
|
||||
/// </summary>
|
||||
[DisplayName("水平翻转")]
|
||||
public bool FlipX { get => Spine.FlipX; set => Spine.FlipX = value; }
|
||||
|
||||
/// <summary>
|
||||
/// 垂直翻转
|
||||
/// </summary>
|
||||
[DisplayName("垂直翻转")]
|
||||
public bool FlipY { get => Spine.FlipY; set => Spine.FlipY = value; }
|
||||
}
|
||||
}
|
||||
38
SpineViewer/PropertyGridWrappers/Spine/SpineWrapper.cs
Normal file
38
SpineViewer/PropertyGridWrappers/Spine/SpineWrapper.cs
Normal file
@@ -0,0 +1,38 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using System.Drawing.Design;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
48
SpineViewer/PropertyGridWrappers/SpinePreviewerWrapper.cs
Normal file
48
SpineViewer/PropertyGridWrappers/SpinePreviewerWrapper.cs
Normal file
@@ -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
|
||||
{
|
||||
/// <summary>
|
||||
/// 用于在 PropertyGrid 上显示 SpinePreviewe 属性的包装类
|
||||
/// </summary>
|
||||
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; }
|
||||
}
|
||||
}
|
||||
307
SpineViewer/PropertyGridWrappers/TypeConverter.cs
Normal file
307
SpineViewer/PropertyGridWrappers/TypeConverter.cs
Normal file
@@ -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
|
||||
{
|
||||
/// <summary>
|
||||
/// 字符串标准值列表属性
|
||||
/// </summary>
|
||||
[AttributeUsage(AttributeTargets.Property, AllowMultiple = false, Inherited = false)]
|
||||
public class StandardValuesAttribute : Attribute
|
||||
{
|
||||
/// <summary>
|
||||
/// 标准值列表
|
||||
/// </summary>
|
||||
public ReadOnlyCollection<string> StandardValues { get; private set; }
|
||||
private readonly List<string> standardValues = [];
|
||||
|
||||
/// <summary>
|
||||
/// 是否允许用户自定义
|
||||
/// </summary>
|
||||
public bool Customizable { get; set; } = false;
|
||||
|
||||
/// <summary>
|
||||
/// 字符串标准值列表
|
||||
/// </summary>
|
||||
/// <param name="values">允许的字符串标准值</param>
|
||||
public StandardValuesAttribute(params string[] values)
|
||||
{
|
||||
standardValues.AddRange(values);
|
||||
StandardValues = standardValues.AsReadOnly();
|
||||
}
|
||||
}
|
||||
|
||||
public override bool GetStandardValuesSupported(ITypeDescriptorContext? context) => true;
|
||||
|
||||
public override bool GetStandardValuesExclusive(ITypeDescriptorContext? context)
|
||||
{
|
||||
var customizable = context?.PropertyDescriptor?.Attributes.OfType<StandardValuesAttribute>().FirstOrDefault()?.Customizable ?? false;
|
||||
return !customizable;
|
||||
}
|
||||
|
||||
public override StandardValuesCollection? GetStandardValues(ITypeDescriptorContext? context)
|
||||
{
|
||||
// 查找属性上的 StandardValuesAttribute
|
||||
var attribute = context?.PropertyDescriptor?.Attributes.OfType<StandardValuesAttribute>().FirstOrDefault();
|
||||
StandardValuesCollection result;
|
||||
if (attribute != null)
|
||||
result = new StandardValuesCollection(attribute.StandardValues);
|
||||
else
|
||||
result = new StandardValuesCollection(Array.Empty<string>());
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
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<string> 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<string> 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);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 皮肤位包装类转换器, 实现字符串和包装类的互相转换, 并且提供标准值列表对属性进行设置, 同时还提供在面板上显示包装类属性的能力
|
||||
/// </summary>
|
||||
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<SpineSkinWrapper>().ToArray();
|
||||
if (managers.Length > 0)
|
||||
{
|
||||
IEnumerable<string> 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);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 轨道索引包装类转换器, 实现字符串和包装类的互相转换, 并且提供标准值列表对属性进行设置, 同时还提供在面板上显示包装类属性的能力
|
||||
/// </summary>
|
||||
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<SpineAnimationWrapper>().ToArray();
|
||||
if (animTracks.Length > 0)
|
||||
{
|
||||
IEnumerable<string> 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<PropertyDescriptor>
|
||||
{
|
||||
// 定义 R, G, B, A 四个字段的描述器
|
||||
new SFMLColorPropertyDescriptor(typeof(SFML.Graphics.Color), "R", typeof(byte)),
|
||||
new SFMLColorPropertyDescriptor(typeof(SFML.Graphics.Color), "G", typeof(byte)),
|
||||
new SFMLColorPropertyDescriptor(typeof(SFML.Graphics.Color), "B", typeof(byte)),
|
||||
new SFMLColorPropertyDescriptor(typeof(SFML.Graphics.Color), "A", typeof(byte))
|
||||
};
|
||||
|
||||
// 返回自定义属性集合
|
||||
return new PropertyDescriptorCollection(properties.ToArray());
|
||||
}
|
||||
}
|
||||
}
|
||||
172
SpineViewer/PropertyGridWrappers/UITypeEditor.cs
Normal file
172
SpineViewer/PropertyGridWrappers/UITypeEditor.cs
Normal file
@@ -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
|
||||
{
|
||||
/// <summary>
|
||||
/// 使用 FolderBrowserDialog 的文件夹路径编辑器
|
||||
/// </summary>
|
||||
public class FolderNameEditor : UITypeEditor
|
||||
{
|
||||
public override UITypeEditorEditStyle GetEditStyle(ITypeDescriptorContext? context)
|
||||
{
|
||||
// 指定编辑风格为 Modal 对话框, 提供右边用来点击的按钮
|
||||
return UITypeEditorEditStyle.Modal;
|
||||
}
|
||||
|
||||
public override object? EditValue(ITypeDescriptorContext? context, IServiceProvider provider, object? value)
|
||||
{
|
||||
// 重写 EditValue 方法,提供自定义的文件夹选择对话框逻辑
|
||||
using var dialog = new FolderBrowserDialog();
|
||||
|
||||
// 如果当前值为有效路径,则设置为初始选中路径
|
||||
if (value is string currentPath && Directory.Exists(currentPath))
|
||||
dialog.SelectedPath = currentPath;
|
||||
|
||||
if (dialog.ShowDialog() == DialogResult.OK)
|
||||
value = dialog.SelectedPath;
|
||||
|
||||
return value;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// skel 文件路径编辑器
|
||||
/// </summary>
|
||||
public class SkelFileNameEditor : FileNameEditor
|
||||
{
|
||||
protected override void InitializeDialog(OpenFileDialog openFileDialog)
|
||||
{
|
||||
base.InitializeDialog(openFileDialog);
|
||||
openFileDialog.Title = "选择 skel 文件";
|
||||
openFileDialog.AddExtension = false;
|
||||
openFileDialog.Filter = "skel 文件 (*.skel; *.json)|*.skel;*.json|二进制文件 (*.skel)|*.skel|文本文件 (*.json)|*.json|所有文件 (*.*)|*.*";
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// atlas 文件路径编辑器
|
||||
/// </summary>
|
||||
public class AtlasFileNameEditor : FileNameEditor
|
||||
{
|
||||
protected override void InitializeDialog(OpenFileDialog openFileDialog)
|
||||
{
|
||||
base.InitializeDialog(openFileDialog);
|
||||
openFileDialog.Title = "选择 atlas 文件";
|
||||
openFileDialog.AddExtension = false;
|
||||
openFileDialog.Filter = "atlas 文件 (*.atlas)|*.atlas|所有文件 (*.*)|*.*";
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 多轨道动画编辑器
|
||||
/// </summary>
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 多轨道动画编辑器
|
||||
/// </summary>
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
/// </summary>
|
||||
public abstract class Spine : ImplementationResolver<Spine, SpineImplementationAttribute, SpineVersion>, SFML.Graphics.Drawable, IDisposable
|
||||
{
|
||||
private readonly Logger logger = LogManager.GetCurrentClassLogger();
|
||||
private bool skinLoggerWarned = false;
|
||||
|
||||
/// <summary>
|
||||
/// 空动画标记
|
||||
/// </summary>
|
||||
@@ -36,8 +33,12 @@ namespace SpineViewer.Spine
|
||||
/// </summary>
|
||||
public static Spine New(SpineVersion version, string skelPath, string? atlasPath = null)
|
||||
{
|
||||
if (version == SpineVersion.Auto) version = SpineHelper.GetVersion(skelPath);
|
||||
atlasPath ??= Path.ChangeExtension(skelPath, ".atlas");
|
||||
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
|
||||
/// </summary>
|
||||
private readonly object _lock = new();
|
||||
|
||||
private readonly Logger logger = LogManager.GetCurrentClassLogger();
|
||||
private bool skinLoggerWarned = false;
|
||||
|
||||
/// <summary>
|
||||
/// 构造函数
|
||||
/// </summary>
|
||||
@@ -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] 基本信息
|
||||
/// <summary>
|
||||
/// 运行时唯一 ID
|
||||
/// </summary>
|
||||
public string ID { get; } = Guid.NewGuid().ToString();
|
||||
|
||||
/// <summary>
|
||||
/// 骨骼预览图, 并没有去除预乘, 画面可能偏暗
|
||||
/// </summary>
|
||||
public Image Preview { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// 获取所属版本
|
||||
/// </summary>
|
||||
[TypeConverter(typeof(SpineVersionConverter))]
|
||||
[Category("[0] 基本信息"), DisplayName("运行时版本")]
|
||||
public SpineVersion Version { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 资源所在完整目录
|
||||
/// </summary>
|
||||
[Category("[0] 基本信息"), DisplayName("资源目录")]
|
||||
public string AssetsDir { get; }
|
||||
|
||||
/// <summary>
|
||||
/// skel 文件完整路径
|
||||
/// </summary>
|
||||
[Category("[0] 基本信息"), DisplayName("skel文件路径")]
|
||||
public string SkelPath { get; }
|
||||
|
||||
/// <summary>
|
||||
/// atlas 文件完整路径
|
||||
/// </summary>
|
||||
[Category("[0] 基本信息"), DisplayName("atlas文件路径")]
|
||||
public string AtlasPath { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 名称
|
||||
/// </summary>
|
||||
[Category("[0] 基本信息"), DisplayName("名称")]
|
||||
public string Name { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 获取所属文件版本
|
||||
/// </summary>
|
||||
[Category("[0] 基本信息"), DisplayName("文件版本")]
|
||||
public abstract string FileVersion { get; }
|
||||
|
||||
#endregion
|
||||
|
||||
#region 属性 | [1] 设置
|
||||
|
||||
/// <summary>
|
||||
/// 是否被隐藏, 被隐藏的模型将仅仅在列表显示, 不参与其他行为
|
||||
/// </summary>
|
||||
[Category("[1] 设置"), DisplayName("是否隐藏")]
|
||||
public bool IsHidden
|
||||
{
|
||||
get { 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;
|
||||
|
||||
/// <summary>
|
||||
/// 是否使用预乘Alpha
|
||||
/// 是否使用预乘 Alpha
|
||||
/// </summary>
|
||||
[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] 变换
|
||||
/// <summary>
|
||||
/// 骨骼包围盒
|
||||
/// </summary>
|
||||
public RectangleF Bounds { get { lock (_lock) return bounds; } }
|
||||
protected abstract RectangleF bounds { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 缩放比例
|
||||
/// </summary>
|
||||
[Category("[2] 变换"), DisplayName("缩放比例")]
|
||||
public float Scale
|
||||
{
|
||||
get { lock (_lock) return scale; }
|
||||
@@ -182,8 +172,6 @@ namespace SpineViewer.Spine
|
||||
/// <summary>
|
||||
/// 位置
|
||||
/// </summary>
|
||||
[TypeConverter(typeof(PointFConverter))]
|
||||
[Category("[2] 变换"), DisplayName("位置")]
|
||||
public PointF Position
|
||||
{
|
||||
get { lock (_lock) return position; }
|
||||
@@ -194,7 +182,6 @@ namespace SpineViewer.Spine
|
||||
/// <summary>
|
||||
/// 水平翻转
|
||||
/// </summary>
|
||||
[Category("[2] 变换"), DisplayName("水平翻转")]
|
||||
public bool FlipX
|
||||
{
|
||||
get { lock (_lock) return flipX; }
|
||||
@@ -205,7 +192,6 @@ namespace SpineViewer.Spine
|
||||
/// <summary>
|
||||
/// 垂直翻转
|
||||
/// </summary>
|
||||
[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] 动画
|
||||
|
||||
/// <summary>
|
||||
/// 已加载皮肤列表
|
||||
/// </summary>
|
||||
[Editor(typeof(SkinManagerEditor), typeof(UITypeEditor))]
|
||||
[TypeConverter(typeof(ExpandableObjectConverter))]
|
||||
[Category("[3] 动画"), DisplayName("已加载皮肤列表")]
|
||||
public SkinManager SkinManager { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// 默认轨道动画名称, 如果设置的动画不存在则忽略
|
||||
/// </summary>
|
||||
[Browsable(false)]
|
||||
public string Track0Animation
|
||||
{
|
||||
get { lock (_lock) return getAnimation(0); }
|
||||
set { lock (_lock) { setAnimation(0, value); update(0); } }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 全轨道动画最大时长
|
||||
/// </summary>
|
||||
[Category("[3] 动画"), DisplayName("全轨道最大时长")]
|
||||
public float AnimationTracksMaxDuration { get { lock (_lock) return getTrackIndices().Select(i => GetAnimationDuration(getAnimation(i))).Max(); } }
|
||||
|
||||
/// <summary>
|
||||
/// 默认轨道动画时长
|
||||
/// </summary>
|
||||
[Editor(typeof(AnimationTracksEditor), typeof(UITypeEditor))]
|
||||
[TypeConverter(typeof(ExpandableObjectConverter))]
|
||||
[Category("[3] 动画"), DisplayName("多轨道动画管理")]
|
||||
public AnimationTracks AnimationTracks { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// 包含的所有皮肤名称
|
||||
/// </summary>
|
||||
[Browsable(false)]
|
||||
public ReadOnlyCollection<string> SkinNames { get; private set; }
|
||||
protected readonly List<string> skinNames = [];
|
||||
|
||||
/// <summary>
|
||||
/// 包含的所有动画名称
|
||||
/// </summary>
|
||||
public ReadOnlyCollection<string> AnimationNames { get; private set; }
|
||||
protected readonly List<string> animationNames = [EMPTY_ANIMATION];
|
||||
|
||||
/// <summary>
|
||||
/// 是否被选中
|
||||
/// </summary>
|
||||
public bool IsSelected
|
||||
{
|
||||
get { lock (_lock) return isSelected; }
|
||||
set { lock (_lock) { isSelected = value; update(0); } }
|
||||
}
|
||||
protected bool isSelected = false;
|
||||
|
||||
/// <summary>
|
||||
/// 显示调试
|
||||
/// </summary>
|
||||
public bool IsDebug
|
||||
{
|
||||
get { lock (_lock) return isDebug; }
|
||||
set { lock (_lock) { isDebug = value; update(0); } }
|
||||
}
|
||||
protected bool isDebug = false;
|
||||
|
||||
/// <summary>
|
||||
/// 显示纹理
|
||||
/// </summary>
|
||||
public bool DebugTexture
|
||||
{
|
||||
get { lock (_lock) return debugTexture; }
|
||||
set { lock (_lock) { debugTexture = value; update(0); } }
|
||||
}
|
||||
protected bool debugTexture = true;
|
||||
|
||||
/// <summary>
|
||||
/// 显示包围盒
|
||||
/// </summary>
|
||||
public bool DebugBounds
|
||||
{
|
||||
get { lock (_lock) return debugBounds; }
|
||||
set { lock (_lock) { debugBounds = value; update(0); } }
|
||||
}
|
||||
protected bool debugBounds = true;
|
||||
|
||||
/// <summary>
|
||||
/// 显示骨骼
|
||||
/// </summary>
|
||||
public bool DebugBones
|
||||
{
|
||||
get { lock (_lock) return debugBones; }
|
||||
set { lock (_lock) { debugBones = value; update(0); } }
|
||||
}
|
||||
protected bool debugBones = false;
|
||||
|
||||
/// <summary>
|
||||
/// 获取已加载的皮肤列表快照, 允许出现重复值
|
||||
/// </summary>
|
||||
@@ -328,13 +333,6 @@ namespace SpineViewer.Spine
|
||||
/// </summary>
|
||||
protected abstract void clearSkin();
|
||||
|
||||
/// <summary>
|
||||
/// 包含的所有动画名称
|
||||
/// </summary>
|
||||
[Browsable(false)]
|
||||
public ReadOnlyCollection<string> AnimationNames { get; private set; }
|
||||
protected readonly List<string> animationNames = [EMPTY_ANIMATION];
|
||||
|
||||
/// <summary>
|
||||
/// 获取所有非 null 的轨道索引快照
|
||||
/// </summary>
|
||||
@@ -369,85 +367,6 @@ namespace SpineViewer.Spine
|
||||
/// </summary>
|
||||
public void ResetAnimationsTime() { lock (_lock) { foreach (var i in getTrackIndices()) setAnimation(i, getAnimation(i)); update(0); } }
|
||||
|
||||
#endregion
|
||||
|
||||
#region 属性 | [4] 调试
|
||||
|
||||
/// <summary>
|
||||
/// 显示调试
|
||||
/// </summary>
|
||||
[Browsable(false)]
|
||||
public bool IsDebug
|
||||
{
|
||||
get { lock (_lock) return isDebug; }
|
||||
set { lock (_lock) isDebug = value; }
|
||||
}
|
||||
protected bool isDebug = false;
|
||||
|
||||
/// <summary>
|
||||
/// 显示纹理
|
||||
/// </summary>
|
||||
[Category("[4] 调试"), DisplayName("显示纹理")]
|
||||
public bool DebugTexture
|
||||
{
|
||||
get { lock (_lock) return debugTexture; }
|
||||
set { lock (_lock) debugTexture = value; }
|
||||
}
|
||||
protected bool debugTexture = true;
|
||||
|
||||
/// <summary>
|
||||
/// 显示包围盒
|
||||
/// </summary>
|
||||
[Category("[4] 调试"), DisplayName("显示包围盒")]
|
||||
public bool DebugBounds
|
||||
{
|
||||
get { lock (_lock) return debugBounds; }
|
||||
set { lock (_lock) debugBounds = value; }
|
||||
}
|
||||
protected bool debugBounds = true;
|
||||
|
||||
/// <summary>
|
||||
/// 显示骨骼
|
||||
/// </summary>
|
||||
[Category("[4] 调试"), DisplayName("显示骨架")]
|
||||
public bool DebugBones
|
||||
{
|
||||
get { lock (_lock) return debugBones; }
|
||||
set { lock (_lock) debugBones = value; }
|
||||
}
|
||||
protected bool debugBones = false;
|
||||
|
||||
#endregion
|
||||
|
||||
/// <summary>
|
||||
/// 标识符
|
||||
/// </summary>
|
||||
public readonly string ID = Guid.NewGuid().ToString();
|
||||
|
||||
/// <summary>
|
||||
/// 是否被选中
|
||||
/// </summary>
|
||||
[Browsable(false)]
|
||||
public bool IsSelected
|
||||
{
|
||||
get { lock (_lock) return isSelected; }
|
||||
set { lock (_lock) isSelected = value; }
|
||||
}
|
||||
protected bool isSelected = false;
|
||||
|
||||
/// <summary>
|
||||
/// 骨骼包围盒
|
||||
/// </summary>
|
||||
[Browsable(false)]
|
||||
public RectangleF Bounds { get { lock (_lock) return bounds; } }
|
||||
protected abstract RectangleF bounds { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 骨骼预览图, 并没有去除预乘, 画面可能偏暗
|
||||
/// </summary>
|
||||
[Browsable(false)]
|
||||
public Image Preview { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// 更新内部状态
|
||||
/// </summary>
|
||||
@@ -490,6 +409,5 @@ namespace SpineViewer.Spine
|
||||
protected abstract void draw(SFML.Graphics.RenderTarget target, SFML.Graphics.RenderStates states);
|
||||
|
||||
#endregion
|
||||
|
||||
}
|
||||
}
|
||||
@@ -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<string> 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<string> 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);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 轨道索引包装类转换器, 实现字符串和包装类的互相转换, 并且提供标准值列表对属性进行设置, 同时还提供在面板上显示包装类属性的能力
|
||||
/// </summary>
|
||||
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<AnimationTracks>().ToArray();
|
||||
if (animTracks.Length > 0)
|
||||
{
|
||||
IEnumerable<string> 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<SkinManager>().ToArray();
|
||||
if (managers.Length > 0)
|
||||
{
|
||||
IEnumerable<string> 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
{
|
||||
/// <summary>
|
||||
/// skel 文件路径编辑器
|
||||
/// </summary>
|
||||
public class SkelFileNameEditor : FileNameEditor
|
||||
{
|
||||
protected override void InitializeDialog(OpenFileDialog openFileDialog)
|
||||
{
|
||||
base.InitializeDialog(openFileDialog);
|
||||
openFileDialog.Title = "选择 skel 文件";
|
||||
openFileDialog.AddExtension = false;
|
||||
openFileDialog.Filter = "skel 文件 (*.skel; *.json)|*.skel;*.json|二进制文件 (*.skel)|*.skel|文本文件 (*.json)|*.json|所有文件 (*.*)|*.*";
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// atlas 文件路径编辑器
|
||||
/// </summary>
|
||||
public class AtlasFileNameEditor : FileNameEditor
|
||||
{
|
||||
protected override void InitializeDialog(OpenFileDialog openFileDialog)
|
||||
{
|
||||
base.InitializeDialog(openFileDialog);
|
||||
openFileDialog.Title = "选择 atlas 文件";
|
||||
openFileDialog.AddExtension = false;
|
||||
openFileDialog.Filter = "atlas 文件 (*.atlas)|*.atlas|所有文件 (*.*)|*.*";
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 多轨道动画编辑器
|
||||
/// </summary>
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 多轨道动画编辑器
|
||||
/// </summary>
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
{
|
||||
/// <summary>
|
||||
/// 字符串标准值列表属性
|
||||
/// </summary>
|
||||
[AttributeUsage(AttributeTargets.Property, AllowMultiple = false, Inherited = false)]
|
||||
public class StandardValuesAttribute : Attribute
|
||||
{
|
||||
/// <summary>
|
||||
/// 标准值列表
|
||||
/// </summary>
|
||||
public ReadOnlyCollection<string> StandardValues { get; private set; }
|
||||
private readonly List<string> standardValues = [];
|
||||
|
||||
/// <summary>
|
||||
/// 是否允许用户自定义
|
||||
/// </summary>
|
||||
public bool Customizable { get; set; } = false;
|
||||
|
||||
/// <summary>
|
||||
/// 字符串标准值列表
|
||||
/// </summary>
|
||||
/// <param name="values">允许的字符串标准值</param>
|
||||
public StandardValuesAttribute(params string[] values)
|
||||
{
|
||||
standardValues.AddRange(values);
|
||||
StandardValues = standardValues.AsReadOnly();
|
||||
}
|
||||
}
|
||||
|
||||
public override bool GetStandardValuesSupported(ITypeDescriptorContext? context) => true;
|
||||
|
||||
public override bool GetStandardValuesExclusive(ITypeDescriptorContext? context)
|
||||
{
|
||||
var customizable = context?.PropertyDescriptor?.Attributes.OfType<StandardValuesAttribute>().FirstOrDefault()?.Customizable ?? false;
|
||||
return !customizable;
|
||||
}
|
||||
|
||||
public override StandardValuesCollection? GetStandardValues(ITypeDescriptorContext? context)
|
||||
{
|
||||
// 查找属性上的 StandardValuesAttribute
|
||||
var attribute = context?.PropertyDescriptor?.Attributes.OfType<StandardValuesAttribute>().FirstOrDefault();
|
||||
StandardValuesCollection result;
|
||||
if (attribute != null)
|
||||
result = new StandardValuesCollection(attribute.StandardValues);
|
||||
else
|
||||
result = new StandardValuesCollection(Array.Empty<string>());
|
||||
return result;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
{
|
||||
/// <summary>
|
||||
/// 使用 FolderBrowserDialog 的文件夹路径编辑器
|
||||
/// </summary>
|
||||
public class FolderNameEditor : UITypeEditor
|
||||
{
|
||||
public override UITypeEditorEditStyle GetEditStyle(ITypeDescriptorContext? context)
|
||||
{
|
||||
// 指定编辑风格为 Modal 对话框, 提供右边用来点击的按钮
|
||||
return UITypeEditorEditStyle.Modal;
|
||||
}
|
||||
|
||||
public override object? EditValue(ITypeDescriptorContext? context, IServiceProvider provider, object? value)
|
||||
{
|
||||
// 重写 EditValue 方法,提供自定义的文件夹选择对话框逻辑
|
||||
using var dialog = new FolderBrowserDialog();
|
||||
|
||||
// 如果当前值为有效路径,则设置为初始选中路径
|
||||
if (value is string currentPath && Directory.Exists(currentPath))
|
||||
dialog.SelectedPath = currentPath;
|
||||
|
||||
if (dialog.ShowDialog() == DialogResult.OK)
|
||||
value = dialog.SelectedPath;
|
||||
|
||||
return value;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user