This commit is contained in:
ww-rm
2025-04-07 15:06:23 +08:00
parent 580eaf990d
commit 64bd9907cb
67 changed files with 2040 additions and 1646 deletions

View File

@@ -13,11 +13,18 @@ using System.Reflection;
using System.Diagnostics; using System.Diagnostics;
using System.Collections.Specialized; using System.Collections.Specialized;
using NLog; using NLog;
using SpineViewer.Extensions;
using SpineViewer.PropertyGridWrappers.Spine;
namespace SpineViewer.Controls namespace SpineViewer.Controls
{ {
public partial class SpineListView : UserControl public partial class SpineListView : UserControl
{ {
/// <summary>
/// 日志器
/// </summary>
private readonly Logger logger = LogManager.GetCurrentClassLogger();
/// <summary> /// <summary>
/// Spine 列表只读视图, 访问时必须使用 lock 语句锁定视图本身 /// Spine 列表只读视图, 访问时必须使用 lock 语句锁定视图本身
/// </summary> /// </summary>
@@ -29,9 +36,9 @@ namespace SpineViewer.Controls
private readonly List<Spine.Spine> spines = []; private readonly List<Spine.Spine> spines = [];
/// <summary> /// <summary>
/// 日志器 /// 用于属性页显示模型参数的包装类
/// </summary> /// </summary>
protected readonly Logger logger = LogManager.GetCurrentClassLogger(); private readonly Dictionary<string, SpineWrapper> spinePropertyWrappers = [];
public SpineListView() public SpineListView()
{ {
@@ -61,8 +68,7 @@ namespace SpineViewer.Controls
private void Insert(int index = -1) private void Insert(int index = -1)
{ {
var dialog = new Dialogs.OpenSpineDialog(); var dialog = new Dialogs.OpenSpineDialog();
if (dialog.ShowDialog() != DialogResult.OK) if (dialog.ShowDialog() != DialogResult.OK) return;
return;
Insert(dialog.Result, index); Insert(dialog.Result, index);
} }
@@ -80,12 +86,10 @@ namespace SpineViewer.Controls
index = listView.Items.Count; index = listView.Items.Count;
// 锁定外部的读操作 // 锁定外部的读操作
lock (Spines) lock (Spines) { spines.Insert(index, spine); }
{ spinePropertyWrappers[spine.ID] = new(spine);
spines.Insert(index, spine); listView.SmallImageList.Images.Add(spine.ID, spine.Preview);
listView.SmallImageList.Images.Add(spine.ID, spine.Preview); listView.LargeImageList.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 }); listView.Items.Insert(index, new ListViewItem(spine.Name, spine.ID) { ToolTipText = spine.SkelPath });
// 选中新增项 // 选中新增项
@@ -108,8 +112,7 @@ namespace SpineViewer.Controls
public void BatchAdd() public void BatchAdd()
{ {
var openDialog = new Dialogs.BatchOpenSpineDialog(); var openDialog = new Dialogs.BatchOpenSpineDialog();
if (openDialog.ShowDialog() != DialogResult.OK) if (openDialog.ShowDialog() != DialogResult.OK) return;
return;
BatchAdd(openDialog.Result); BatchAdd(openDialog.Result);
} }
@@ -154,6 +157,7 @@ namespace SpineViewer.Controls
var spine = Spine.Spine.New(version, skelPath); var spine = Spine.Spine.New(version, skelPath);
var preview = spine.Preview; var preview = spine.Preview;
lock (Spines) { spines.Add(spine); } lock (Spines) { spines.Add(spine); }
spinePropertyWrappers[spine.ID] = new(spine);
listView.Invoke(() => listView.Invoke(() =>
{ {
listView.SmallImageList.Images.Add(spine.ID, preview); listView.SmallImageList.Images.Add(spine.ID, preview);
@@ -249,9 +253,9 @@ namespace SpineViewer.Controls
if (listView.SelectedIndices.Count <= 0) if (listView.SelectedIndices.Count <= 0)
PropertyGrid.SelectedObject = null; PropertyGrid.SelectedObject = null;
else if (listView.SelectedIndices.Count <= 1) else if (listView.SelectedIndices.Count <= 1)
PropertyGrid.SelectedObject = spines[listView.SelectedIndices[0]]; PropertyGrid.SelectedObject = spinePropertyWrappers[spines[listView.SelectedIndices[0]].ID];
else 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 // 标记选中的 Spine
@@ -418,6 +422,7 @@ namespace SpineViewer.Controls
listView.Items.RemoveAt(i); listView.Items.RemoveAt(i);
var spine = spines[i]; var spine = spines[i];
spines.RemoveAt(i); spines.RemoveAt(i);
spinePropertyWrappers.Remove(spine.ID);
listView.SmallImageList.Images.RemoveByKey(spine.ID); listView.SmallImageList.Images.RemoveByKey(spine.ID);
listView.LargeImageList.Images.RemoveByKey(spine.ID); listView.LargeImageList.Images.RemoveByKey(spine.ID);
spine.Dispose(); spine.Dispose();
@@ -513,6 +518,7 @@ namespace SpineViewer.Controls
{ {
foreach (var spine in spines) spine.Dispose(); foreach (var spine in spines) spine.Dispose();
spines.Clear(); spines.Clear();
spinePropertyWrappers.Clear();
listView.SmallImageList.Images.Clear(); listView.SmallImageList.Images.Clear();
listView.LargeImageList.Images.Clear(); listView.LargeImageList.Images.Clear();
} }

View File

@@ -15,6 +15,24 @@ namespace SpineViewer.Controls
{ {
public partial class SpinePreviewer : UserControl 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> /// <summary>
/// 要绑定的 Spine 列表控件 /// 要绑定的 Spine 列表控件
/// </summary> /// </summary>
@@ -32,57 +50,12 @@ namespace SpineViewer.Controls
{ {
propertyGrid = value; propertyGrid = value;
if (propertyGrid is not null) if (propertyGrid is not null)
propertyGrid.SelectedObject = new PreviewerProperty(this); propertyGrid.SelectedObject = new PropertyGridWrappers.SpinePreviewerWrapper(this);
} }
} }
private PropertyGrid? propertyGrid; private PropertyGrid? propertyGrid;
#region #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; }
}
/// <summary> /// <summary>
/// 分辨率 /// 分辨率
@@ -166,7 +139,7 @@ namespace SpineViewer.Controls
} }
set set
{ {
value = Math.Clamp(value, ZOOM_MIN, ZOOM_MAX); value = Math.Clamp(value, 0.001f, 1000f);
using var view = RenderWindow.GetView(); using var view = RenderWindow.GetView();
var signX = Math.Sign(view.Size.X); var signX = Math.Sign(view.Size.X);
var signY = Math.Sign(view.Size.Y); var signY = Math.Sign(view.Size.Y);
@@ -270,70 +243,44 @@ namespace SpineViewer.Controls
#endregion #endregion
#region
/// <summary> /// <summary>
/// 日志器 /// 预览画面背景色
/// </summary> /// </summary>
private Logger logger = LogManager.GetCurrentClassLogger(); private static readonly SFML.Graphics.Color BackgroundColor = new(105, 105, 105);
public SpinePreviewer() /// <summary>
{ /// 预览画面坐标轴颜色
InitializeComponent(); /// </summary>
RenderWindow = new(panel.Handle); private static readonly SFML.Graphics.Color AxisColor = new(220, 220, 220);
RenderWindow.SetActive(false);
// 设置默认参数 /// <summary>
Resolution = new(2048, 2048); /// 坐标轴顶点缓冲区
Center = new(0, 0); /// </summary>
FlipY = true; private readonly SFML.Graphics.VertexArray AxisVertex = new(SFML.Graphics.PrimitiveType.Lines, 2);
MaxFps = 30;
}
#region 线
/// <summary> /// <summary>
/// 渲染窗口 /// 渲染窗口
/// </summary> /// </summary>
private readonly SFML.Graphics.RenderWindow RenderWindow; private readonly SFML.Graphics.RenderWindow RenderWindow;
/// <summary>
/// 帧间隔计时器
/// </summary>
private readonly SFML.System.Clock Clock = new();
/// <summary> /// <summary>
/// 渲染任务 /// 渲染任务
/// </summary> /// </summary>
private Task? task = null; private Task? task = null;
private CancellationTokenSource? cancelToken = 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>
/// 是否更新画面 /// 是否更新画面
/// </summary> /// </summary>
[DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
[Browsable(false)]
public bool IsUpdating public bool IsUpdating
{ {
get => isUpdating; get => isUpdating;
@@ -360,24 +307,30 @@ namespace SpineViewer.Controls
private object _forwardDeltaLock = new(); private object _forwardDeltaLock = new();
/// <summary> /// <summary>
/// 预览画面背景色 /// 开始渲染
/// </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>
/// 预览画面坐标轴颜色 /// 停止渲染
/// </summary> /// </summary>
private static readonly SFML.Graphics.Color AxisColor = new(220, 220, 220); public void StopRender()
{
/// <summary> IsUpdating = false;
/// 坐标轴顶点缓冲区 if (task is null || cancelToken is null)
/// </summary> return;
private readonly SFML.Graphics.VertexArray AxisVertex = new(SFML.Graphics.PrimitiveType.Lines, 2); cancelToken.Cancel();
task.Wait();
/// <summary> cancelToken = null;
/// 帧间隔计时器 task = null;
/// </summary> }
private readonly SFML.System.Clock Clock = new();
/// <summary> /// <summary>
/// 渲染任务 /// 渲染任务

View File

@@ -1,82 +1,68 @@
using SpineViewer.Exporter; using SpineViewer.PropertyGridWrappers.Exporter;
using System; using System;
using System.Collections; using System.Collections;
using System.Collections.Generic; using System.Collections.Generic;
using System.ComponentModel; using System.ComponentModel;
using System.Data; using System.Data;
using System.Diagnostics; using System.Diagnostics;
using System.Drawing;
using System.Drawing.Design;
using System.Drawing.Imaging;
using System.Linq; using System.Linq;
using System.Reflection; using System.Reflection;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms;
namespace SpineViewer.Dialogs namespace SpineViewer.Dialogs
{ {
public partial class ExportDialog: Form public partial class ExportDialog: Form
{ {
/// <summary> private readonly ExporterWrapper wrapper;
/// 要绑定的导出参数
/// </summary>
public required ExportArgs ExportArgs
{
get => propertyGrid_ExportArgs.SelectedObject as ExportArgs;
init
{
propertyGrid_ExportArgs.SelectedObject = value;
#region XXX: public ExportDialog(ExporterWrapper wrapper)
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()
{ {
InitializeComponent(); 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) 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, "参数错误"); MessageBox.Info(error, "参数错误");
return; return;

View File

@@ -1,6 +1,6 @@
namespace SpineViewer.Dialogs namespace SpineViewer.Dialogs
{ {
partial class AnimationTracksEditorDialog partial class SpineAnimationEditorDialog
{ {
/// <summary> /// <summary>
/// Required designer variable. /// Required designer variable.

View File

@@ -1,4 +1,5 @@
using SpineViewer.Spine; using SpineViewer.PropertyGridWrappers.Spine;
using SpineViewer.Spine;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.ComponentModel; using System.ComponentModel;
@@ -11,14 +12,14 @@ using System.Windows.Forms;
namespace SpineViewer.Dialogs namespace SpineViewer.Dialogs
{ {
public partial class AnimationTracksEditorDialog : Form public partial class SpineAnimationEditorDialog : Form
{ {
private readonly Spine.Spine spine; private readonly Spine.Spine spine;
public AnimationTracksEditorDialog(Spine.Spine spine) public SpineAnimationEditorDialog(Spine.Spine spine)
{ {
InitializeComponent(); InitializeComponent();
this.spine = spine; this.spine = spine;
propertyGrid_AnimationTracks.SelectedObject = spine.AnimationTracks; propertyGrid_AnimationTracks.SelectedObject = new SpineAnimationWrapper(spine);
} }
private void button_Add_Click(object sender, EventArgs e) private void button_Add_Click(object sender, EventArgs e)

View File

@@ -1,6 +1,6 @@
namespace SpineViewer.Dialogs namespace SpineViewer.Dialogs
{ {
partial class SkinManagerEditorDialog partial class SpineSkinEditorDialog
{ {
/// <summary> /// <summary>
/// Required designer variable. /// Required designer variable.

View File

@@ -1,4 +1,5 @@
using SpineViewer.Spine; using SpineViewer.PropertyGridWrappers.Spine;
using SpineViewer.Spine;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.ComponentModel; using System.ComponentModel;
@@ -11,14 +12,14 @@ using System.Windows.Forms;
namespace SpineViewer.Dialogs namespace SpineViewer.Dialogs
{ {
public partial class SkinManagerEditorDialog : Form public partial class SpineSkinEditorDialog : Form
{ {
private readonly Spine.Spine spine; private readonly Spine.Spine spine;
public SkinManagerEditorDialog(Spine.Spine spine) public SpineSkinEditorDialog(Spine.Spine spine)
{ {
InitializeComponent(); InitializeComponent();
this.spine = spine; this.spine = spine;
propertyGrid_SkinManager.SelectedObject = spine.SkinManager; propertyGrid_SkinManager.SelectedObject = new SpineSkinWrapper(spine); // TODO: 去掉对话框
} }
private void button_Add_Click(object sender, EventArgs e) private void button_Add_Click(object sender, EventArgs e)

View File

@@ -5,16 +5,13 @@ using System.Linq;
using System.Text; using System.Text;
using System.Threading.Tasks; using System.Threading.Tasks;
namespace SpineViewer.Exporter.Implementations.ExportArgs namespace SpineViewer.Exporter
{ {
/// <summary> /// <summary>
/// FFmpeg 自定义视频导出参数 /// FFmpeg 自定义视频导出参数
/// </summary> /// </summary>
[ExportImplementation(ExportType.Custom)] public class CustomExporter : FFmpegVideoExporter
public class CustomExportArgs : FFmpegVideoExportArgs
{ {
public CustomExportArgs(Size resolution, SFML.Graphics.View view, bool renderSelectedOnly) : base(resolution, view, renderSelectedOnly) { }
public override string Format => CustomFormat; public override string Format => CustomFormat;
public override string Suffix => CustomSuffix; public override string Suffix => CustomSuffix;
@@ -24,13 +21,11 @@ namespace SpineViewer.Exporter.Implementations.ExportArgs
/// <summary> /// <summary>
/// 文件格式 /// 文件格式
/// </summary> /// </summary>
[Category("[3] "), DisplayName(""), Description("")]
public string CustomFormat { get; set; } = "mp4"; public string CustomFormat { get; set; } = "mp4";
/// <summary> /// <summary>
/// 文件名后缀 /// 文件名后缀
/// </summary> /// </summary>
[Category("[3] "), DisplayName(""), Description("")]
public string CustomSuffix { get; set; } = ".mp4"; public string CustomSuffix { get; set; } = ".mp4";
} }
} }

View File

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

View File

@@ -1,37 +1,13 @@
using FFMpegCore.Pipes; using FFMpegCore.Pipes;
using SpineViewer.Extensions;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Drawing.Imaging; using System.Drawing.Imaging;
using System.Linq; using System.Linq;
using System.Text; using System.Text;
using System.Threading.Tasks;
namespace SpineViewer.Exporter 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> /// <summary>
/// SFML.Graphics.Image 帧对象包装类, 将接管给定的 image 对象生命周期 /// SFML.Graphics.Image 帧对象包装类, 将接管给定的 image 对象生命周期
/// </summary> /// </summary>

View File

@@ -1,7 +1,10 @@
using NLog; using NLog;
using SpineViewer.Extensions;
using SpineViewer.PropertyGridWrappers;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.ComponentModel; using System.ComponentModel;
using System.Drawing.Design;
using System.Linq; using System.Linq;
using System.Reflection; using System.Reflection;
using System.Text; using System.Text;
@@ -12,44 +15,80 @@ namespace SpineViewer.Exporter
/// <summary> /// <summary>
/// 导出器基类 /// 导出器基类
/// </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>
/// 日志器 /// 日志器
/// </summary> /// </summary>
protected Logger logger = LogManager.GetCurrentClassLogger(); protected readonly Logger logger = LogManager.GetCurrentClassLogger();
/// <summary>
/// 导出参数
/// </summary>
public ExportArgs ExportArgs { get; } = exportArgs;
/// <summary> /// <summary>
/// 可用于文件名的时间戳字符串 /// 可用于文件名的时间戳字符串
/// </summary> /// </summary>
protected readonly string timestamp = DateTime.Now.ToString("yyMMddHHmmss"); 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> /// <summary>
/// 获取供渲染的 SFML.Graphics.RenderTexture /// 获取供渲染的 SFML.Graphics.RenderTexture
/// </summary> /// </summary>
private SFML.Graphics.RenderTexture GetRenderTexture() 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.Clear(SFML.Graphics.Color.Transparent);
tex.SetView(ExportArgs.View); tex.SetView(View);
return tex; return tex;
} }
@@ -67,12 +106,12 @@ namespace SpineViewer.Exporter
using var texPma = GetRenderTexture(); using var texPma = GetRenderTexture();
// 先将预乘结果准确绘制出来, 注意背景色也应当是预乘的 // 先将预乘结果准确绘制出来, 注意背景色也应当是预乘的
texPma.Clear(ExportArgs.BackgroundColorPma); texPma.Clear(BackgroundColorPma);
foreach (var spine in spinesToRender) texPma.Draw(spine); foreach (var spine in spinesToRender) texPma.Draw(spine);
texPma.Display(); texPma.Display();
// 背景色透明度不为 1 时需要处理反预乘, 否则直接就是结果 // 背景色透明度不为 1 时需要处理反预乘, 否则直接就是结果
if (ExportArgs.BackgroundColor.A < 255) if (BackgroundColor.A < 255)
{ {
// 从预乘结果构造渲染对象, 并正确设置变换 // 从预乘结果构造渲染对象, 并正确设置变换
using var view = texPma.GetView(); using var view = texPma.GetView();
@@ -88,14 +127,14 @@ namespace SpineViewer.Exporter
// 混合模式用直接覆盖的方式, 保证得到的图像区域是反预乘的颜色和透明度, 同时使用反预乘着色器 // 混合模式用直接覆盖的方式, 保证得到的图像区域是反预乘的颜色和透明度, 同时使用反预乘着色器
var st = SFML.Graphics.RenderStates.Default; var st = SFML.Graphics.RenderStates.Default;
st.BlendMode = SrcOnlyBlendMode; // 用源的颜色和透明度直接覆盖 st.BlendMode = SFMLBlendMode.SourceOnly;
st.Shader = Shader.InversePma; st.Shader = SFMLShader.InversePma;
// 在最终结果上二次渲染非预乘画面 // 在最终结果上二次渲染非预乘画面
using var tex = GetRenderTexture(); using var tex = GetRenderTexture();
// 将非预乘结果覆盖式绘制在目标对象上, 注意背景色应该用非预乘的 // 将非预乘结果覆盖式绘制在目标对象上, 注意背景色应该用非预乘的
tex.Clear(ExportArgs.BackgroundColor); tex.Clear(BackgroundColor);
tex.Draw(sp, st); tex.Draw(sp, st);
tex.Display(); tex.Display();
return new(tex.Texture.CopyToImage()); return new(tex.Texture.CopyToImage());
@@ -116,16 +155,36 @@ namespace SpineViewer.Exporter
/// </summary> /// </summary>
protected abstract void ExportIndividual(Spine.Spine[] spinesToRender, BackgroundWorker? worker = null); 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>
/// 执行导出 /// 执行导出
/// </summary> /// </summary>
/// <param name="spines">要进行导出的 Spine 列表</param> /// <param name="spines">要进行导出的 Spine 列表</param>
/// <param name="worker">用来执行该函数的 worker</param> /// <param name="worker">用来执行该函数的 worker</param>
/// <exception cref="ArgumentException"></exception>
public virtual void Export(Spine.Spine[] spines, BackgroundWorker? worker = null) 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); else ExportIndividual(spinesToRender, worker);
logger.LogCurrentProcessMemoryUsage(); logger.LogCurrentProcessMemoryUsage();

View File

@@ -1,6 +1,5 @@
using FFMpegCore.Pipes; using FFMpegCore.Pipes;
using FFMpegCore; using FFMpegCore;
using SpineViewer.Exporter.Implementations.ExportArgs;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.ComponentModel; using System.ComponentModel;
@@ -9,36 +8,63 @@ using System.Text;
using System.Threading.Tasks; using System.Threading.Tasks;
using System.Diagnostics; using System.Diagnostics;
namespace SpineViewer.Exporter.Implementations.Exporter namespace SpineViewer.Exporter
{ {
/// <summary> /// <summary>
/// 使用 FFmpeg 的视频导出器 /// 使用 FFmpeg 的视频导出器
/// </summary> /// </summary>
[ExportImplementation(ExportType.Gif)] public abstract class FFmpegVideoExporter : VideoExporter
[ExportImplementation(ExportType.Mp4)]
[ExportImplementation(ExportType.Webm)]
[ExportImplementation(ExportType.Mkv)]
[ExportImplementation(ExportType.Mov)]
[ExportImplementation(ExportType.Custom)]
public class FFmpegVideoExporter : VideoExporter
{ {
public FFmpegVideoExporter(FFmpegVideoExportArgs exportArgs) : base(exportArgs) { } /// <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) protected override void ExportSingle(Spine.Spine[] spinesToRender, BackgroundWorker? worker = null)
{ {
var args = (FFmpegVideoExportArgs)ExportArgs; var noteSuffix = FileNameNoteSuffix;
var noteSuffix = args.FileNameNoteSuffix;
if (!string.IsNullOrWhiteSpace(noteSuffix)) noteSuffix = $"_{noteSuffix}"; 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 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); logger.Info("FFmpeg arguments: {}", ffmpegArgs.Arguments);
ffmpegArgs.ProcessSynchronously(); ffmpegArgs.ProcessSynchronously();
@@ -46,31 +72,30 @@ namespace SpineViewer.Exporter.Implementations.Exporter
catch (Exception ex) catch (Exception ex)
{ {
logger.Error(ex.ToString()); 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) protected override void ExportIndividual(Spine.Spine[] spinesToRender, BackgroundWorker? worker = null)
{ {
var args = (FFmpegVideoExportArgs)ExportArgs; var noteSuffix = FileNameNoteSuffix;
var noteSuffix = args.FileNameNoteSuffix;
if (!string.IsNullOrWhiteSpace(noteSuffix)) noteSuffix = $"_{noteSuffix}"; if (!string.IsNullOrWhiteSpace(noteSuffix)) noteSuffix = $"_{noteSuffix}";
foreach (var spine in spinesToRender) foreach (var spine in spinesToRender)
{ {
if (worker?.CancellationPending == true) break; // 取消的日志在 GetFrames 里输出 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 try
{ {
var ffmpegArgs = FFMpegArguments var ffmpegArgs = FFMpegArguments
.FromPipeInput(videoFramesSource) .FromPipeInput(videoFramesSource)
.OutputToFile(savePath, true, args.SetOutputOptions); .OutputToFile(savePath, true, SetOutputOptions);
logger.Info("FFmpeg arguments: {}", ffmpegArgs.Arguments); logger.Info("FFmpeg arguments: {}", ffmpegArgs.Arguments);
ffmpegArgs.ProcessSynchronously(); ffmpegArgs.ProcessSynchronously();
@@ -78,7 +103,7 @@ namespace SpineViewer.Exporter.Implementations.Exporter
catch (Exception ex) catch (Exception ex)
{ {
logger.Error(ex.ToString()); logger.Error(ex.ToString());
logger.Error("Failed to export {} {} {}", args.Format, savePath, spine.SkelPath); logger.Error("Failed to export {} {} {}", Format, savePath, spine.SkelPath);
} }
} }
} }

View File

@@ -1,37 +1,61 @@
using SpineViewer.Exporter.Implementations.ExportArgs; using SpineViewer.Spine;
using SpineViewer.Spine;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.ComponentModel; using System.ComponentModel;
using System.Drawing.Imaging;
using System.Linq; using System.Linq;
using System.Text; using System.Text;
using System.Threading.Tasks; using System.Threading.Tasks;
namespace SpineViewer.Exporter.Implementations.Exporter namespace SpineViewer.Exporter
{ {
/// <summary> /// <summary>
/// 单帧画面导出器 /// 单帧画面导出器
/// </summary> /// </summary>
[ExportImplementation(ExportType.Frame)] public class FrameExporter : Exporter
public class FrameExporter : SpineViewer.Exporter.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) protected override void ExportSingle(Spine.Spine[] spinesToRender, BackgroundWorker? worker = null)
{ {
var args = (FrameExportArgs)ExportArgs;
// 导出单个时必定提供输出文件夹 // 导出单个时必定提供输出文件夹
var filename = $"frame_{timestamp}{args.Suffix}"; var filename = $"frame_{timestamp}{ImageFormat.GetSuffix()}";
var savePath = Path.Combine(args.OutputDir, filename); var savePath = Path.Combine(OutputDir, filename);
worker?.ReportProgress(0, $"已处理 0/1"); worker?.ReportProgress(0, $"已处理 0/1");
try try
{ {
using var frame = GetFrame(spinesToRender); using var frame = GetFrame(spinesToRender);
using var img = frame.CopyToBitmap(); using var img = frame.CopyToBitmap();
img.SetResolution(args.DPI.Width, args.DPI.Height); img.SetResolution(DPI.Width, DPI.Height);
img.Save(savePath, args.ImageFormat); img.Save(savePath, ImageFormat);
} }
catch (Exception ex) catch (Exception ex)
{ {
@@ -43,8 +67,6 @@ namespace SpineViewer.Exporter.Implementations.Exporter
protected override void ExportIndividual(Spine.Spine[] spinesToRender, BackgroundWorker? worker = null) protected override void ExportIndividual(Spine.Spine[] spinesToRender, BackgroundWorker? worker = null)
{ {
var args = (FrameExportArgs)ExportArgs;
int total = spinesToRender.Length; int total = spinesToRender.Length;
int success = 0; int success = 0;
int error = 0; int error = 0;
@@ -55,21 +77,21 @@ namespace SpineViewer.Exporter.Implementations.Exporter
var spine = spinesToRender[i]; var spine = spinesToRender[i];
// 逐个导出时如果提供了输出文件夹, 则全部导出到输出文件夹, 否则输出到各自的文件夹 // 逐个导出时如果提供了输出文件夹, 则全部导出到输出文件夹, 否则输出到各自的文件夹
var filename = $"{spine.Name}_{timestamp}{args.Suffix}"; var filename = $"{spine.Name}_{timestamp}{ImageFormat.GetSuffix()}";
var savePath = args.OutputDir is null ? Path.Combine(spine.AssetsDir, filename) : Path.Combine(args.OutputDir, filename); var savePath = Path.Combine(OutputDir ?? spine.AssetsDir, filename);
try try
{ {
using var frame = GetFrame(spine); using var frame = GetFrame(spine);
using var img = frame.CopyToBitmap(); using var img = frame.CopyToBitmap();
img.SetResolution(args.DPI.Width, args.DPI.Height); img.SetResolution(DPI.Width, DPI.Height);
img.Save(savePath, args.ImageFormat); img.Save(savePath, ImageFormat);
success++; success++;
} }
catch (Exception ex) catch (Exception ex)
{ {
logger.Error(ex.ToString()); logger.Error(ex.ToString());
logger.Error("Failed to save frame {} {}", savePath, spine.SkelPath); logger.Error("Failed to save single frame {} {}", savePath, spine.SkelPath);
error++; error++;
} }
@@ -82,5 +104,4 @@ namespace SpineViewer.Exporter.Implementations.Exporter
logger.Info("{} frames saved successfully", success); logger.Info("{} frames saved successfully", success);
} }
} }
} }

View File

@@ -1,5 +1,4 @@
using SpineViewer.Exporter.Implementations.ExportArgs; using SpineViewer.Spine;
using SpineViewer.Spine;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.ComponentModel; using System.ComponentModel;
@@ -7,28 +6,28 @@ using System.Linq;
using System.Text; using System.Text;
using System.Threading.Tasks; using System.Threading.Tasks;
namespace SpineViewer.Exporter.Implementations.Exporter namespace SpineViewer.Exporter
{ {
/// <summary> /// <summary>
/// 帧序列导出器 /// 帧序列导出器
/// </summary> /// </summary>
[ExportImplementation(ExportType.FrameSequence)]
public class FrameSequenceExporter : VideoExporter 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) 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); Directory.CreateDirectory(saveDir);
int frameIdx = 0; int frameIdx = 0;
foreach (var frame in GetFrames(spinesToRender, worker)) 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); var savePath = Path.Combine(saveDir, filename);
try try
@@ -50,20 +49,19 @@ namespace SpineViewer.Exporter.Implementations.Exporter
protected override void ExportIndividual(Spine.Spine[] spinesToRender, BackgroundWorker? worker = null) protected override void ExportIndividual(Spine.Spine[] spinesToRender, BackgroundWorker? worker = null)
{ {
var args = (FrameSequenceExportArgs)ExportArgs;
foreach (var spine in spinesToRender) foreach (var spine in spinesToRender)
{ {
if (worker?.CancellationPending == true) break; // 取消的日志在 GetFrames 里输出 if (worker?.CancellationPending == true) break; // 取消的日志在 GetFrames 里输出
// 如果提供了输出文件夹, 则全部导出到输出文件夹, 否则导出到各自的文件夹下 // 如果提供了输出文件夹, 则全部导出到输出文件夹, 否则导出到各自的文件夹下
var subDir = $"{spine.Name}_{timestamp}_{args.FPS:f0}"; var subDir = $"{spine.Name}_{timestamp}_{FPS:f0}";
var saveDir = args.OutputDir is null ? Path.Combine(spine.AssetsDir, subDir) : Path.Combine(args.OutputDir, subDir); var saveDir = Path.Combine(OutputDir ?? spine.AssetsDir, subDir);
Directory.CreateDirectory(saveDir); Directory.CreateDirectory(saveDir);
int frameIdx = 0; int frameIdx = 0;
foreach (var frame in GetFrames(spine, worker)) 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); var savePath = Path.Combine(saveDir, filename);
try try

View File

@@ -6,15 +6,14 @@ using System.Linq;
using System.Text; using System.Text;
using System.Threading.Tasks; using System.Threading.Tasks;
namespace SpineViewer.Exporter.Implementations.ExportArgs namespace SpineViewer.Exporter
{ {
/// <summary> /// <summary>
/// GIF 导出参数 /// GIF 导出参数
/// </summary> /// </summary>
[ExportImplementation(ExportType.Gif)] public class GifExporter : FFmpegVideoExporter
public class GifExportArgs : FFmpegVideoExportArgs
{ {
public GifExportArgs(Size resolution, SFML.Graphics.View view, bool renderSelectedOnly) : base(resolution, view, renderSelectedOnly) public GifExporter()
{ {
// GIF 的帧率不能太高, 超过 50 帧反而会变慢 // GIF 的帧率不能太高, 超过 50 帧反而会变慢
FPS = 12; FPS = 12;
@@ -27,17 +26,17 @@ namespace SpineViewer.Exporter.Implementations.ExportArgs
/// <summary> /// <summary>
/// 调色板最大颜色数量 /// 调色板最大颜色数量
/// </summary> /// </summary>
[Category("[3] "), DisplayName(""), Description("使, ")]
public uint MaxColors { get => maxColors; set => maxColors = Math.Clamp(value, 2, 256); } public uint MaxColors { get => maxColors; set => maxColors = Math.Clamp(value, 2, 256); }
private uint maxColors = 256; private uint maxColors = 256;
/// <summary> /// <summary>
/// 透明度阈值 /// 透明度阈值
/// </summary> /// </summary>
[Category("[3] "), DisplayName(""), Description("")]
public byte AlphaThreshold { get => alphaThreshold; set => alphaThreshold = value; } public byte AlphaThreshold { get => alphaThreshold; set => alphaThreshold = value; }
private byte alphaThreshold = 128; private byte alphaThreshold = 128;
public override string FileNameNoteSuffix => $"{MaxColors}_{AlphaThreshold}";
public override void SetOutputOptions(FFMpegArgumentOptions options) public override void SetOutputOptions(FFMpegArgumentOptions options)
{ {
base.SetOutputOptions(options); base.SetOutputOptions(options);
@@ -47,7 +46,5 @@ namespace SpineViewer.Exporter.Implementations.ExportArgs
var customArgs = $"-filter_complex \"{v};{s0};{s1}\""; var customArgs = $"-filter_complex \"{v};{s0};{s1}\"";
options.WithCustomArgument(customArgs); options.WithCustomArgument(customArgs);
} }
public override string FileNameNoteSuffix => $"{MaxColors}_{AlphaThreshold}";
} }
} }

View File

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

View File

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

View File

@@ -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";
}
}

View File

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

View File

@@ -6,15 +6,14 @@ using System.Linq;
using System.Text; using System.Text;
using System.Threading.Tasks; using System.Threading.Tasks;
namespace SpineViewer.Exporter.Implementations.ExportArgs namespace SpineViewer.Exporter
{ {
/// <summary> /// <summary>
/// MKV 导出参数 /// MKV 导出参数
/// </summary> /// </summary>
[ExportImplementation(ExportType.Mkv)] public class MkvExporter : FFmpegVideoExporter
public class MkvExportArgs : FFmpegVideoExportArgs
{ {
public MkvExportArgs(Size resolution, SFML.Graphics.View view, bool renderSelectedOnly) : base(resolution, view, renderSelectedOnly) public MkvExporter()
{ {
BackgroundColor = new(0, 255, 0); BackgroundColor = new(0, 255, 0);
} }
@@ -26,32 +25,25 @@ namespace SpineViewer.Exporter.Implementations.ExportArgs
/// <summary> /// <summary>
/// 编码器 /// 编码器
/// </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"; public string Codec { get; set; } = "libx265";
/// <summary> /// <summary>
/// CRF /// CRF
/// </summary> /// </summary>
[Category("[3] "), DisplayName("CRF"), Description("-crf, 0-63, 18-28, 23, ")]
public int CRF { get => crf; set => crf = Math.Clamp(value, 0, 63); } public int CRF { get => crf; set => crf = Math.Clamp(value, 0, 63); }
private int crf = 23; private int crf = 23;
/// <summary> /// <summary>
/// 像素格式 /// 像素格式
/// </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 string PixelFormat { get; set; } = "yuv444p";
public override string FileNameNoteSuffix => $"{Codec}_{CRF}_{PixelFormat}";
public override void SetOutputOptions(FFMpegArgumentOptions options) public override void SetOutputOptions(FFMpegArgumentOptions options)
{ {
base.SetOutputOptions(options); base.SetOutputOptions(options);
options.WithVideoCodec(Codec).WithConstantRateFactor(CRF).ForcePixelFormat(PixelFormat); options.WithVideoCodec(Codec).WithConstantRateFactor(CRF).ForcePixelFormat(PixelFormat);
} }
public override string FileNameNoteSuffix => $"{Codec}_{CRF}_{PixelFormat}";
} }
} }

View File

@@ -6,15 +6,14 @@ using System.Linq;
using System.Text; using System.Text;
using System.Threading.Tasks; using System.Threading.Tasks;
namespace SpineViewer.Exporter.Implementations.ExportArgs namespace SpineViewer.Exporter
{ {
/// <summary> /// <summary>
/// MOV 导出参数 /// MOV 导出参数
/// </summary> /// </summary>
[ExportImplementation(ExportType.Mov)] public class MovExporter : FFmpegVideoExporter
public class MovExportArgs : FFmpegVideoExportArgs
{ {
public MovExportArgs(Size resolution, SFML.Graphics.View view, bool renderSelectedOnly) : base(resolution, view, renderSelectedOnly) public MovExporter()
{ {
BackgroundColor = new(0, 255, 0); BackgroundColor = new(0, 255, 0);
} }
@@ -26,33 +25,24 @@ namespace SpineViewer.Exporter.Implementations.ExportArgs
/// <summary> /// <summary>
/// 编码器 /// 编码器
/// </summary> /// </summary>
[StringEnumConverter.StandardValues("prores_ks", Customizable = true)]
[TypeConverter(typeof(StringEnumConverter))]
[Category("[3] "), DisplayName(""), Description("-c:v, 使")]
public string Codec { get; set; } = "prores_ks"; public string Codec { get; set; } = "prores_ks";
/// <summary> /// <summary>
/// 预设 /// 预设
/// </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"; public string Profile { get; set; } = "auto";
/// <summary> /// <summary>
/// 像素格式 /// 像素格式
/// </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 string PixelFormat { get; set; } = "yuva444p10le";
public override string FileNameNoteSuffix => $"{Codec}_{Profile}_{PixelFormat}";
public override void SetOutputOptions(FFMpegArgumentOptions options) public override void SetOutputOptions(FFMpegArgumentOptions options)
{ {
base.SetOutputOptions(options); base.SetOutputOptions(options);
options.WithFastStart().WithVideoCodec(Codec).WithCustomArgument($"-profile {Profile}").ForcePixelFormat(PixelFormat); options.WithFastStart().WithVideoCodec(Codec).WithCustomArgument($"-profile {Profile}").ForcePixelFormat(PixelFormat);
} }
public override string FileNameNoteSuffix => $"{Codec}_{Profile}_{PixelFormat}";
} }
} }

View File

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

View File

@@ -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());
}
}
}

View File

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

View File

@@ -1,5 +1,4 @@
using SpineViewer.Exporter.Implementations.ExportArgs; using SpineViewer.Spine;
using SpineViewer.Spine;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.ComponentModel; using System.ComponentModel;
@@ -7,28 +6,44 @@ using System.Linq;
using System.Text; using System.Text;
using System.Threading.Tasks; using System.Threading.Tasks;
namespace SpineViewer.Exporter.Implementations.Exporter namespace SpineViewer.Exporter
{ {
/// <summary> /// <summary>
/// 视频导出基类 /// 视频导出基类
/// </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>
/// 生成单个模型的帧序列 /// 生成单个模型的帧序列
/// </summary> /// </summary>
protected IEnumerable<SFMLImageVideoFrame> GetFrames(Spine.Spine spine, BackgroundWorker? worker = null) protected IEnumerable<SFMLImageVideoFrame> GetFrames(Spine.Spine spine, BackgroundWorker? worker = null)
{ {
var args = (VideoExportArgs)ExportArgs; // 独立导出时如果 Duration 小于 0 则使用所有轨道上动画时长最大值
var duration = Duration;
// 独立导出时如果 args.Duration 小于 0 则使用所有轨道上动画时长最大值
var duration = args.Duration;
if (duration < 0) duration = spine.GetTrackIndices().Select(i => spine.GetAnimationDuration(spine.GetAnimation(i))).Max(); if (duration < 0) duration = spine.GetTrackIndices().Select(i => spine.GetAnimationDuration(spine.GetAnimation(i))).Max();
float delta = 1f / args.FPS; float delta = 1f / FPS;
int total = Math.Max(1, (int)(duration * args.FPS)); // 至少导出 1 帧 int total = Math.Max(1, (int)(duration * FPS)); // 至少导出 1 帧
worker?.ReportProgress(0, $"{spine.Name} 已处理 0/{total} 帧"); worker?.ReportProgress(0, $"{spine.Name} 已处理 0/{total} 帧");
for (int i = 0; i < total; i++) for (int i = 0; i < total; i++)
@@ -51,10 +66,9 @@ namespace SpineViewer.Exporter.Implementations.Exporter
/// </summary> /// </summary>
protected IEnumerable<SFMLImageVideoFrame> GetFrames(Spine.Spine[] spinesToRender, BackgroundWorker? worker = null) protected IEnumerable<SFMLImageVideoFrame> GetFrames(Spine.Spine[] spinesToRender, BackgroundWorker? worker = null)
{ {
// 导出单个时必须根据 args.Duration 决定导出时长 // 导出单个时必须根据 Duration 决定导出时长
var args = (VideoExportArgs)ExportArgs; float delta = 1f / FPS;
float delta = 1f / args.FPS; int total = Math.Max(1, (int)(Duration * FPS)); // 至少导出 1 帧
int total = Math.Max(1, (int)(args.Duration * args.FPS)); // 至少导出 1 帧
worker?.ReportProgress(0, $"已处理 0/{total} 帧"); worker?.ReportProgress(0, $"已处理 0/{total} 帧");
for (int i = 0; i < total; i++) for (int i = 0; i < total; i++)

View File

@@ -6,15 +6,14 @@ using System.Linq;
using System.Text; using System.Text;
using System.Threading.Tasks; using System.Threading.Tasks;
namespace SpineViewer.Exporter.Implementations.ExportArgs namespace SpineViewer.Exporter
{ {
/// <summary> /// <summary>
/// WebM 导出参数 /// WebM 导出参数
/// </summary> /// </summary>
[ExportImplementation(ExportType.Webm)] public class WebmExporter : FFmpegVideoExporter
public class WebmExportArgs : FFmpegVideoExportArgs
{ {
public WebmExportArgs(Size resolution, SFML.Graphics.View view, bool renderSelectedOnly) : base(resolution, view, renderSelectedOnly) public WebmExporter()
{ {
// 默认用透明黑背景 // 默认用透明黑背景
BackgroundColor = new(0, 0, 0, 0); BackgroundColor = new(0, 0, 0, 0);
@@ -27,32 +26,25 @@ namespace SpineViewer.Exporter.Implementations.ExportArgs
/// <summary> /// <summary>
/// 编码器 /// 编码器
/// </summary> /// </summary>
[StringEnumConverter.StandardValues("libvpx-vp9", Customizable = true)]
[TypeConverter(typeof(StringEnumConverter))]
[Category("[3] "), DisplayName(""), Description("-c:v, 使")]
public string Codec { get; set; } = "libvpx-vp9"; public string Codec { get; set; } = "libvpx-vp9";
/// <summary> /// <summary>
/// CRF /// CRF
/// </summary> /// </summary>
[Category("[3] "), DisplayName("CRF"), Description("-crf, 0-63, 18-28, 23, ")]
public int CRF { get => crf; set => crf = Math.Clamp(value, 0, 63); } public int CRF { get => crf; set => crf = Math.Clamp(value, 0, 63); }
private int crf = 23; private int crf = 23;
/// <summary> /// <summary>
/// 像素格式 /// 像素格式
/// </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 string PixelFormat { get; set; } = "yuva420p";
public override string FileNameNoteSuffix => $"{Codec}_{CRF}_{PixelFormat}";
public override void SetOutputOptions(FFMpegArgumentOptions options) public override void SetOutputOptions(FFMpegArgumentOptions options)
{ {
base.SetOutputOptions(options); base.SetOutputOptions(options);
options.WithVideoCodec(Codec).WithConstantRateFactor(CRF).ForcePixelFormat(PixelFormat); options.WithVideoCodec(Codec).WithConstantRateFactor(CRF).ForcePixelFormat(PixelFormat);
} }
public override string FileNameNoteSuffix => $"{Codec}_{CRF}_{PixelFormat}";
} }
} }

View File

@@ -5,7 +5,7 @@ using System.Linq;
using System.Text; using System.Text;
using System.Threading.Tasks; using System.Threading.Tasks;
namespace SpineViewer namespace SpineViewer.Extensions
{ {
public static class NLogExtension public static class NLogExtension
{ {

View File

@@ -4,12 +4,12 @@ using System.Linq;
using System.Text; using System.Text;
using System.Threading.Tasks; using System.Threading.Tasks;
namespace SpineViewer.Spine namespace SpineViewer.Extensions
{ {
/// <summary> /// <summary>
/// SFML 混合模式, 预乘模式下输入和输出的像素值都是预乘的 /// SFML 混合模式, 预乘模式下输入和输出的像素值都是预乘的
/// </summary> /// </summary>
public static class BlendModeSFML public static class SFMLBlendMode
{ {
///// <summary> ///// <summary>
///// Normal Blend, 无预乘, 仅在 dst.a 是 1 时得到正确结果, 其余情况是有偏结果 ///// Normal Blend, 无预乘, 仅在 dst.a 是 1 时得到正确结果, 其余情况是有偏结果
@@ -110,5 +110,13 @@ namespace SpineViewer.Spine
SFML.Graphics.BlendMode.Factor.OneMinusSrcAlpha, SFML.Graphics.BlendMode.Factor.OneMinusSrcAlpha,
SFML.Graphics.BlendMode.Equation.Add 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
);
} }
} }

View File

@@ -4,7 +4,7 @@ using System.Linq;
using System.Text; using System.Text;
using System.Threading.Tasks; using System.Threading.Tasks;
namespace SpineViewer namespace SpineViewer.Extensions
{ {
public static class SFMLExtension public static class SFMLExtension
{ {
@@ -25,7 +25,7 @@ namespace SpineViewer
public static Bitmap CopyToBitmap(this SFML.Graphics.Texture texture) public static Bitmap CopyToBitmap(this SFML.Graphics.Texture texture)
{ {
using var image = texture.CopyToImage(); using var image = texture.CopyToImage();
return CopyToBitmap(image); return image.CopyToBitmap();
} }
/// <summary> /// <summary>

View File

@@ -4,9 +4,9 @@ using System.Linq;
using System.Text; using System.Text;
using System.Threading.Tasks; using System.Threading.Tasks;
namespace SpineViewer namespace SpineViewer.Extensions
{ {
public static class Shader public static class SFMLShader
{ {
/// <summary> /// <summary>
/// 用于非预乘纹理的 fragment shader, 乘上了插值后的透明度用于实现透明度变化(插值预乘), 并且输出预乘后的像素值 /// 用于非预乘纹理的 fragment shader, 乘上了插值后的透明度用于实现透明度变化(插值预乘), 并且输出预乘后的像素值

View File

@@ -115,27 +115,27 @@
// //
toolStripMenuItem_Open.Name = "toolStripMenuItem_Open"; toolStripMenuItem_Open.Name = "toolStripMenuItem_Open";
toolStripMenuItem_Open.ShortcutKeys = Keys.Control | Keys.O; 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.Text = "打开(&O)...";
toolStripMenuItem_Open.Click += toolStripMenuItem_Open_Click; toolStripMenuItem_Open.Click += toolStripMenuItem_Open_Click;
// //
// toolStripMenuItem_BatchOpen // toolStripMenuItem_BatchOpen
// //
toolStripMenuItem_BatchOpen.Name = "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.Text = "批量打开(&B)...";
toolStripMenuItem_BatchOpen.Click += toolStripMenuItem_BatchOpen_Click; toolStripMenuItem_BatchOpen.Click += toolStripMenuItem_BatchOpen_Click;
// //
// toolStripSeparator1 // toolStripSeparator1
// //
toolStripSeparator1.Name = "toolStripSeparator1"; toolStripSeparator1.Name = "toolStripSeparator1";
toolStripSeparator1.Size = new Size(251, 6); toolStripSeparator1.Size = new Size(267, 6);
// //
// toolStripMenuItem_Export // 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.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.Name = "toolStripMenuItem_Export";
toolStripMenuItem_Export.Size = new Size(254, 34); toolStripMenuItem_Export.Size = new Size(270, 34);
toolStripMenuItem_Export.Text = "导出(&E)"; toolStripMenuItem_Export.Text = "导出(&E)";
// //
// toolStripMenuItem_ExportFrame // toolStripMenuItem_ExportFrame
@@ -143,67 +143,67 @@
toolStripMenuItem_ExportFrame.Name = "toolStripMenuItem_ExportFrame"; toolStripMenuItem_ExportFrame.Name = "toolStripMenuItem_ExportFrame";
toolStripMenuItem_ExportFrame.Size = new Size(288, 34); toolStripMenuItem_ExportFrame.Size = new Size(288, 34);
toolStripMenuItem_ExportFrame.Text = "单帧画面..."; toolStripMenuItem_ExportFrame.Text = "单帧画面...";
toolStripMenuItem_ExportFrame.Click += toolStripMenuItem_Export_Click; toolStripMenuItem_ExportFrame.Click += toolStripMenuItem_ExportFrame_Click;
// //
// toolStripMenuItem_ExportFrameSequence // toolStripMenuItem_ExportFrameSequence
// //
toolStripMenuItem_ExportFrameSequence.Name = "toolStripMenuItem_ExportFrameSequence"; toolStripMenuItem_ExportFrameSequence.Name = "toolStripMenuItem_ExportFrameSequence";
toolStripMenuItem_ExportFrameSequence.Size = new Size(288, 34); toolStripMenuItem_ExportFrameSequence.Size = new Size(288, 34);
toolStripMenuItem_ExportFrameSequence.Text = "帧序列..."; toolStripMenuItem_ExportFrameSequence.Text = "帧序列...";
toolStripMenuItem_ExportFrameSequence.Click += toolStripMenuItem_Export_Click; toolStripMenuItem_ExportFrameSequence.Click += toolStripMenuItem_ExportFrameSequence_Click;
// //
// toolStripMenuItem_ExportGif // toolStripMenuItem_ExportGif
// //
toolStripMenuItem_ExportGif.Name = "toolStripMenuItem_ExportGif"; toolStripMenuItem_ExportGif.Name = "toolStripMenuItem_ExportGif";
toolStripMenuItem_ExportGif.Size = new Size(288, 34); toolStripMenuItem_ExportGif.Size = new Size(288, 34);
toolStripMenuItem_ExportGif.Text = "GIF..."; toolStripMenuItem_ExportGif.Text = "GIF...";
toolStripMenuItem_ExportGif.Click += toolStripMenuItem_Export_Click; toolStripMenuItem_ExportGif.Click += toolStripMenuItem_ExportGif_Click;
// //
// toolStripMenuItem_ExportMp4 // toolStripMenuItem_ExportMp4
// //
toolStripMenuItem_ExportMp4.Name = "toolStripMenuItem_ExportMp4"; toolStripMenuItem_ExportMp4.Name = "toolStripMenuItem_ExportMp4";
toolStripMenuItem_ExportMp4.Size = new Size(288, 34); toolStripMenuItem_ExportMp4.Size = new Size(288, 34);
toolStripMenuItem_ExportMp4.Text = "MP4..."; toolStripMenuItem_ExportMp4.Text = "MP4...";
toolStripMenuItem_ExportMp4.Click += toolStripMenuItem_Export_Click; toolStripMenuItem_ExportMp4.Click += toolStripMenuItem_ExportMp4_Click;
// //
// toolStripMenuItem_ExportWebm // toolStripMenuItem_ExportWebm
// //
toolStripMenuItem_ExportWebm.Name = "toolStripMenuItem_ExportWebm"; toolStripMenuItem_ExportWebm.Name = "toolStripMenuItem_ExportWebm";
toolStripMenuItem_ExportWebm.Size = new Size(288, 34); toolStripMenuItem_ExportWebm.Size = new Size(288, 34);
toolStripMenuItem_ExportWebm.Text = "WebM..."; toolStripMenuItem_ExportWebm.Text = "WebM...";
toolStripMenuItem_ExportWebm.Click += toolStripMenuItem_Export_Click; toolStripMenuItem_ExportWebm.Click += toolStripMenuItem_ExportWebm_Click;
// //
// toolStripMenuItem_ExportMkv // toolStripMenuItem_ExportMkv
// //
toolStripMenuItem_ExportMkv.Name = "toolStripMenuItem_ExportMkv"; toolStripMenuItem_ExportMkv.Name = "toolStripMenuItem_ExportMkv";
toolStripMenuItem_ExportMkv.Size = new Size(288, 34); toolStripMenuItem_ExportMkv.Size = new Size(288, 34);
toolStripMenuItem_ExportMkv.Text = "MKV..."; toolStripMenuItem_ExportMkv.Text = "MKV...";
toolStripMenuItem_ExportMkv.Click += toolStripMenuItem_Export_Click; toolStripMenuItem_ExportMkv.Click += toolStripMenuItem_ExportMkv_Click;
// //
// toolStripMenuItem_ExportMov // toolStripMenuItem_ExportMov
// //
toolStripMenuItem_ExportMov.Name = "toolStripMenuItem_ExportMov"; toolStripMenuItem_ExportMov.Name = "toolStripMenuItem_ExportMov";
toolStripMenuItem_ExportMov.Size = new Size(288, 34); toolStripMenuItem_ExportMov.Size = new Size(288, 34);
toolStripMenuItem_ExportMov.Text = "MOV..."; toolStripMenuItem_ExportMov.Text = "MOV...";
toolStripMenuItem_ExportMov.Click += toolStripMenuItem_Export_Click; toolStripMenuItem_ExportMov.Click += toolStripMenuItem_ExportMov_Click;
// //
// toolStripMenuItem_ExportCustom // toolStripMenuItem_ExportCustom
// //
toolStripMenuItem_ExportCustom.Name = "toolStripMenuItem_ExportCustom"; toolStripMenuItem_ExportCustom.Name = "toolStripMenuItem_ExportCustom";
toolStripMenuItem_ExportCustom.Size = new Size(288, 34); toolStripMenuItem_ExportCustom.Size = new Size(288, 34);
toolStripMenuItem_ExportCustom.Text = "FFmpeg 自定义导出..."; toolStripMenuItem_ExportCustom.Text = "FFmpeg 自定义导出...";
toolStripMenuItem_ExportCustom.Click += toolStripMenuItem_Export_Click; toolStripMenuItem_ExportCustom.Click += toolStripMenuItem_ExportCustom_Click;
// //
// toolStripSeparator2 // toolStripSeparator2
// //
toolStripSeparator2.Name = "toolStripSeparator2"; toolStripSeparator2.Name = "toolStripSeparator2";
toolStripSeparator2.Size = new Size(251, 6); toolStripSeparator2.Size = new Size(267, 6);
// //
// toolStripMenuItem_Exit // toolStripMenuItem_Exit
// //
toolStripMenuItem_Exit.Name = "toolStripMenuItem_Exit"; toolStripMenuItem_Exit.Name = "toolStripMenuItem_Exit";
toolStripMenuItem_Exit.ShortcutKeys = Keys.Alt | Keys.F4; 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.Text = "退出(&X)";
toolStripMenuItem_Exit.Click += toolStripMenuItem_Exit_Click; toolStripMenuItem_Exit.Click += toolStripMenuItem_Exit_Click;
// //
@@ -271,7 +271,7 @@
rtbLog.Margin = new Padding(3, 2, 3, 2); rtbLog.Margin = new Padding(3, 2, 3, 2);
rtbLog.Name = "rtbLog"; rtbLog.Name = "rtbLog";
rtbLog.ReadOnly = true; rtbLog.ReadOnly = true;
rtbLog.Size = new Size(1758, 120); rtbLog.Size = new Size(1758, 124);
rtbLog.TabIndex = 0; rtbLog.TabIndex = 0;
rtbLog.Text = ""; rtbLog.Text = "";
rtbLog.WordWrap = false; rtbLog.WordWrap = false;
@@ -295,7 +295,7 @@
splitContainer_MainForm.Panel2.Controls.Add(rtbLog); splitContainer_MainForm.Panel2.Controls.Add(rtbLog);
splitContainer_MainForm.Panel2.Cursor = Cursors.Default; splitContainer_MainForm.Panel2.Cursor = Cursors.Default;
splitContainer_MainForm.Size = new Size(1758, 1097); splitContainer_MainForm.Size = new Size(1758, 1097);
splitContainer_MainForm.SplitterDistance = 969; splitContainer_MainForm.SplitterDistance = 965;
splitContainer_MainForm.SplitterWidth = 8; splitContainer_MainForm.SplitterWidth = 8;
splitContainer_MainForm.TabIndex = 3; splitContainer_MainForm.TabIndex = 3;
splitContainer_MainForm.TabStop = false; splitContainer_MainForm.TabStop = false;
@@ -319,7 +319,7 @@
// //
splitContainer_Functional.Panel2.Controls.Add(groupBox_Preview); splitContainer_Functional.Panel2.Controls.Add(groupBox_Preview);
splitContainer_Functional.Panel2.Cursor = Cursors.Default; 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.SplitterDistance = 759;
splitContainer_Functional.SplitterWidth = 8; splitContainer_Functional.SplitterWidth = 8;
splitContainer_Functional.TabIndex = 2; splitContainer_Functional.TabIndex = 2;
@@ -343,7 +343,7 @@
// //
splitContainer_Information.Panel2.Controls.Add(splitContainer_Config); splitContainer_Information.Panel2.Controls.Add(splitContainer_Config);
splitContainer_Information.Panel2.Cursor = Cursors.Default; 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.SplitterDistance = 354;
splitContainer_Information.SplitterWidth = 8; splitContainer_Information.SplitterWidth = 8;
splitContainer_Information.TabIndex = 1; splitContainer_Information.TabIndex = 1;
@@ -357,7 +357,7 @@
groupBox_SkelList.Dock = DockStyle.Fill; groupBox_SkelList.Dock = DockStyle.Fill;
groupBox_SkelList.Location = new Point(0, 0); groupBox_SkelList.Location = new Point(0, 0);
groupBox_SkelList.Name = "groupBox_SkelList"; 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.TabIndex = 0;
groupBox_SkelList.TabStop = false; groupBox_SkelList.TabStop = false;
groupBox_SkelList.Text = "模型列表"; groupBox_SkelList.Text = "模型列表";
@@ -368,7 +368,7 @@
spineListView.Location = new Point(3, 26); spineListView.Location = new Point(3, 26);
spineListView.Name = "spineListView"; spineListView.Name = "spineListView";
spineListView.PropertyGrid = propertyGrid_Spine; spineListView.PropertyGrid = propertyGrid_Spine;
spineListView.Size = new Size(348, 940); spineListView.Size = new Size(348, 936);
spineListView.TabIndex = 0; spineListView.TabIndex = 0;
// //
// propertyGrid_Spine // propertyGrid_Spine
@@ -377,7 +377,7 @@
propertyGrid_Spine.HelpVisible = false; propertyGrid_Spine.HelpVisible = false;
propertyGrid_Spine.Location = new Point(3, 26); propertyGrid_Spine.Location = new Point(3, 26);
propertyGrid_Spine.Name = "propertyGrid_Spine"; 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.TabIndex = 0;
propertyGrid_Spine.ToolbarVisible = false; propertyGrid_Spine.ToolbarVisible = false;
propertyGrid_Spine.PropertyValueChanged += propertyGrid_PropertyValueChanged; propertyGrid_Spine.PropertyValueChanged += propertyGrid_PropertyValueChanged;
@@ -400,7 +400,7 @@
// //
splitContainer_Config.Panel2.Controls.Add(groupBox_SkelConfig); splitContainer_Config.Panel2.Controls.Add(groupBox_SkelConfig);
splitContainer_Config.Panel2.Cursor = Cursors.Default; 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.SplitterDistance = 326;
splitContainer_Config.SplitterWidth = 8; splitContainer_Config.SplitterWidth = 8;
splitContainer_Config.TabIndex = 0; splitContainer_Config.TabIndex = 0;
@@ -436,7 +436,7 @@
groupBox_SkelConfig.Dock = DockStyle.Fill; groupBox_SkelConfig.Dock = DockStyle.Fill;
groupBox_SkelConfig.Location = new Point(0, 0); groupBox_SkelConfig.Location = new Point(0, 0);
groupBox_SkelConfig.Name = "groupBox_SkelConfig"; 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.TabIndex = 0;
groupBox_SkelConfig.TabStop = false; groupBox_SkelConfig.TabStop = false;
groupBox_SkelConfig.Text = "模型参数"; groupBox_SkelConfig.Text = "模型参数";
@@ -447,7 +447,7 @@
groupBox_Preview.Dock = DockStyle.Fill; groupBox_Preview.Dock = DockStyle.Fill;
groupBox_Preview.Location = new Point(0, 0); groupBox_Preview.Location = new Point(0, 0);
groupBox_Preview.Name = "groupBox_Preview"; 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.TabIndex = 1;
groupBox_Preview.TabStop = false; groupBox_Preview.TabStop = false;
groupBox_Preview.Text = "预览画面"; groupBox_Preview.Text = "预览画面";
@@ -458,7 +458,7 @@
spinePreviewer.Location = new Point(3, 26); spinePreviewer.Location = new Point(3, 26);
spinePreviewer.Name = "spinePreviewer"; spinePreviewer.Name = "spinePreviewer";
spinePreviewer.PropertyGrid = propertyGrid_Previewer; spinePreviewer.PropertyGrid = propertyGrid_Previewer;
spinePreviewer.Size = new Size(985, 940); spinePreviewer.Size = new Size(985, 936);
spinePreviewer.SpineListView = spineListView; spinePreviewer.SpineListView = spineListView;
spinePreviewer.TabIndex = 0; spinePreviewer.TabIndex = 0;
// //

View File

@@ -3,6 +3,9 @@ using SpineViewer.Spine;
using System.ComponentModel; using System.ComponentModel;
using System.Diagnostics; using System.Diagnostics;
using SpineViewer.Exporter; using SpineViewer.Exporter;
using SpineViewer.Extensions;
using System.Reflection.Metadata;
using SpineViewer.PropertyGridWrappers.Exporter;
namespace SpineViewer namespace SpineViewer
{ {
@@ -15,20 +18,10 @@ namespace SpineViewer
InitializeComponent(); InitializeComponent();
InitializeLogConfiguration(); 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 try
{ {
Shader.Init(); SFMLShader.Init();
} }
catch (Exception ex) catch (Exception ex)
{ {
@@ -87,22 +80,23 @@ namespace SpineViewer
spineListView.BatchAdd(); 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 (spinePreviewer.IsUpdating && MessageBox.Quest("画面仍在更新,建议手动暂停画面后导出固定的一帧,是否继续?") != DialogResult.OK)
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)
return; 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(); var progressDialog = new Dialogs.ProgressDialog();
progressDialog.DoWork += Export_Work; progressDialog.DoWork += Export_Work;
@@ -110,6 +104,141 @@ namespace SpineViewer
progressDialog.ShowDialog(); 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) private void toolStripMenuItem_Exit_Click(object sender, EventArgs e)
{ {
Close(); Close();
@@ -127,57 +256,21 @@ namespace SpineViewer
progressDialog.ShowDialog(); 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) 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) 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) 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; 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) //private void spinePreviewer_KeyDown(object sender, KeyEventArgs e)
//{ //{
// switch (e.KeyCode) // switch (e.KeyCode)

View File

@@ -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";
}
}

View 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; }
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,21 +1,22 @@
using System; using SpineViewer.Spine;
using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.ComponentModel; using System.ComponentModel;
using System.Linq; using System.Linq;
using System.Text; using System.Text;
using System.Threading.Tasks; using System.Threading.Tasks;
namespace SpineViewer.Spine namespace SpineViewer.PropertyGridWrappers.Spine
{ {
/// <summary> /// <summary>
/// 对轨道索引的包装类, 能够在面板上显示例如时长的属性, 但是处理该属性时按字符串去处理, 例如 ToString 和判断对象相等都是用动画名称实现逻辑 /// 对轨道索引属性的包装类, 能够在面板上显示例如时长的属性, 但是处理该属性时按字符串去处理, 例如 ToString 和判断对象相等都是用动画名称实现逻辑
/// </summary> /// </summary>
/// <param name="spine"></param> /// <param name="spine"></param>
/// <param name="i"></param> /// <param name="i"></param>
[TypeConverter(typeof(TrackWrapperConverter))] [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)] [Browsable(false)]
public int Index { get; } = i; public int Index { get; } = i;
@@ -40,60 +41,103 @@ namespace SpineViewer.Spine
/// <summary> /// <summary>
/// 哈希码需要和 Equals 行为类似 /// 哈希码需要和 Equals 行为类似
/// </summary> /// </summary>
public override int GetHashCode() => (typeof(TrackWrapper).FullName + ToString()).GetHashCode(); public override int GetHashCode() => HashCode.Combine(typeof(TrackWrapper).FullName.GetHashCode(), ToString().GetHashCode());
} }
/// <summary> /// <summary>
/// 轨道属性描述符, 实现对属性的读取和赋值 /// 用于在 PropertyGrid 上显示 Spine 动画列表的包装类
/// </summary> /// </summary>
/// <param name="i">轨道索引</param> public class SpineAnimationWrapper(SpineViewer.Spine.Spine spine) : ICustomTypeDescriptor
public class TrackWrapperPropertyDescriptor(int i) : PropertyDescriptor($"Track{i}", [new DisplayNameAttribute($"轨道 {i}")])
{ {
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>
/// 得到一个轨道包装类, 允许用户查看或者修改具体的属性值, 这个地方决定了在面板上看到的是一个对象及其属性 /// 轨道属性描述符, 实现对属性的读取和赋值
/// </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) private readonly int idx = i;
return tracks.GetTrackWrapper(idx);
return null;
}
/// <summary> public override Type ComponentType => typeof(SpineAnimationWrapper);
/// 允许通过字符串赋值修改该轨道的动画, 这里决定了当其他地方的调用 (比如 Converter) 通过 value 来设置属性值的时候应该怎么处理 public override bool IsReadOnly => false;
/// </summary> public override Type PropertyType => typeof(TrackWrapper);
public override void SetValue(object? component, object? value) public override bool CanResetValue(object component) => false;
{ public override void ResetValue(object component) { }
if (component is AnimationTracks tracks) public override bool ShouldSerializeValue(object component) => false;
/// <summary>
/// 得到一个轨道包装类, 允许用户查看或者修改具体的属性值, 这个地方决定了在面板上看到的是一个对象及其属性
/// </summary>
public override object? GetValue(object? component)
{ {
if (value is string s) if (component is SpineAnimationWrapper tracks)
tracks.Spine.SetAnimation(idx, s); // tracks.SetTrackWrapper(idx, s); 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> [Browsable(false)]
/// AnimationTracks 动态类型包装类, 用于提供对 Spine 对象多轨道动画的访问能力, 不同轨道将动态生成属性 public SpineViewer.Spine.Spine Spine { get; } = spine;
/// </summary>
/// <param name="spine">关联的 Spine 对象</param>
public class AnimationTracks(Spine spine) : ICustomTypeDescriptor
{
private static readonly Dictionary<int, TrackWrapperPropertyDescriptor> pdCache = [];
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 = []; 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, 似乎继承下来的东西会有问题, 导致某些调用不正确 // XXX: 必须实现 ICustomTypeDescriptor 接口, 不能继承 CustomTypeDescriptor, 似乎继承下来的东西会有问题, 导致某些调用不正确
/// <summary>
/// 属性描述符缓存
/// </summary>
private static readonly Dictionary<int, TrackWrapperPropertyDescriptor> pdCache = [];
public AttributeCollection GetAttributes() => TypeDescriptor.GetAttributes(this, true); public AttributeCollection GetAttributes() => TypeDescriptor.GetAttributes(this, true);
public string? GetClassName() => TypeDescriptor.GetClassName(this, true); public string? GetClassName() => TypeDescriptor.GetClassName(this, true);
public string? GetComponentName() => TypeDescriptor.GetComponentName(this, true); public string? GetComponentName() => TypeDescriptor.GetComponentName(this, true);
@@ -107,37 +151,16 @@ namespace SpineViewer.Spine
public PropertyDescriptorCollection GetProperties() => GetProperties(null); public PropertyDescriptorCollection GetProperties() => GetProperties(null);
public PropertyDescriptorCollection GetProperties(Attribute[]? attributes) 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()) foreach (var i in Spine.GetTrackIndices())
{ {
if (!pdCache.ContainsKey(i)) if (!pdCache.ContainsKey(i))
pdCache[i] = new TrackWrapperPropertyDescriptor(i); pdCache[i] = new TrackWrapperPropertyDescriptor(i, [new DisplayNameAttribute($"轨道 {i}")]);
props.Add(pdCache[i]); props.Add(pdCache[i]);
} }
return new PropertyDescriptorCollection(props.ToArray()); return props;
} }
/// <summary> #endregion
/// 访问 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();
} }
} }

View File

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

View 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; }
}
}

View 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; }
}
}

View File

@@ -1,19 +1,20 @@
using System; using SpineViewer.Spine;
using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.ComponentModel; using System.ComponentModel;
using System.Linq; using System.Linq;
using System.Text; using System.Text;
using System.Threading.Tasks; using System.Threading.Tasks;
namespace SpineViewer.Spine namespace SpineViewer.PropertyGridWrappers.Spine
{ {
/// <summary> /// <summary>
/// 对皮肤的包装类 /// 对皮肤的包装类
/// </summary> /// </summary>
[TypeConverter(typeof(SkinWrapperConverter))] [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)] [Browsable(false)]
public int Index { get; } = i; public int Index { get; } = i;
@@ -32,60 +33,94 @@ namespace SpineViewer.Spine
return base.Equals(obj); return base.Equals(obj);
} }
public override int GetHashCode() => (typeof(SkinWrapper).FullName + ToString()).GetHashCode(); public override int GetHashCode() => HashCode.Combine(typeof(SkinWrapper).FullName.GetHashCode(), 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);
}
}
} }
/// <summary> /// <summary>
/// SkinManager 动态类型包装类, 用于提供对 Spine 皮肤列表的管理能力 /// SkinManager 动态类型包装类, 用于提供对 Spine 皮肤列表的管理能力
/// </summary> /// </summary>
/// <param name="spine">关联的 Spine 对象</param> /// <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 = []; 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, 似乎继承下来的东西会有问题, 导致某些调用不正确 // XXX: 必须实现 ICustomTypeDescriptor 接口, 不能继承 CustomTypeDescriptor, 似乎继承下来的东西会有问题, 导致某些调用不正确
private static readonly Dictionary<int, SkinWrapperPropertyDescriptor> pdCache = [];
public AttributeCollection GetAttributes() => TypeDescriptor.GetAttributes(this, true); public AttributeCollection GetAttributes() => TypeDescriptor.GetAttributes(this, true);
public string? GetClassName() => TypeDescriptor.GetClassName(this, true); public string? GetClassName() => TypeDescriptor.GetClassName(this, true);
public string? GetComponentName() => TypeDescriptor.GetComponentName(this, true); public string? GetComponentName() => TypeDescriptor.GetComponentName(this, true);
@@ -99,37 +134,16 @@ namespace SpineViewer.Spine
public PropertyDescriptorCollection GetProperties() => GetProperties(null); public PropertyDescriptorCollection GetProperties() => GetProperties(null);
public PropertyDescriptorCollection GetProperties(Attribute[]? attributes) 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++) for (var i = 0; i < Spine.GetLoadedSkins().Length; i++)
{ {
if (!pdCache.ContainsKey(i)) if (!pdCache.ContainsKey(i))
pdCache[i] = new SkinWrapperPropertyDescriptor(i); pdCache[i] = new SkinWrapperPropertyDescriptor(i, [new DisplayNameAttribute($"皮肤 {i}")]);
props.Add(pdCache[i]); props.Add(pdCache[i]);
} }
return new PropertyDescriptorCollection(props.ToArray()); return props;
} }
/// <summary> #endregion
/// 访问 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();
} }
} }

View File

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

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

View 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; }
}
}

View 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());
}
}
}

View 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;
}
}
}

View File

@@ -7,6 +7,7 @@ using System.Linq;
using System.Text; using System.Text;
using System.Threading.Tasks; using System.Threading.Tasks;
using SpineRuntime21; using SpineRuntime21;
using SpineViewer.Extensions;
namespace SpineViewer.Spine.Implementations.Spine namespace SpineViewer.Spine.Implementations.Spine
{ {
@@ -261,7 +262,7 @@ namespace SpineViewer.Spine.Implementations.Spine
{ {
vertexArray.Clear(); vertexArray.Clear();
states.Texture = null; states.Texture = null;
states.Shader = Shader.GetSpineShader(usePremultipliedAlpha); states.Shader = SFMLShader.GetSpineShader(usePma);
// 要用 DrawOrder 而不是 Slots // 要用 DrawOrder 而不是 Slots
foreach (var slot in skeleton.DrawOrder) foreach (var slot in skeleton.DrawOrder)
@@ -323,7 +324,7 @@ namespace SpineViewer.Spine.Implementations.Spine
} }
// 似乎 2.1.x 也没有 BlendMode // 似乎 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; states.Texture ??= texture;
if (states.BlendMode != blendMode || states.Texture != texture) if (states.BlendMode != blendMode || states.Texture != texture)

View File

@@ -7,6 +7,7 @@ using System.Linq;
using System.Text; using System.Text;
using System.Threading.Tasks; using System.Threading.Tasks;
using SpineRuntime36; using SpineRuntime36;
using SpineViewer.Extensions;
namespace SpineViewer.Spine.Implementations.Spine namespace SpineViewer.Spine.Implementations.Spine
{ {
@@ -208,10 +209,10 @@ namespace SpineViewer.Spine.Implementations.Spine
{ {
return spineBlendMode switch return spineBlendMode switch
{ {
BlendMode.Normal => BlendModeSFML.NormalPma, BlendMode.Normal => SFMLBlendMode.NormalPma,
BlendMode.Additive => BlendModeSFML.AdditivePma, BlendMode.Additive => SFMLBlendMode.AdditivePma,
BlendMode.Multiply => BlendModeSFML.MultiplyPma, BlendMode.Multiply => SFMLBlendMode.MultiplyPma,
BlendMode.Screen => BlendModeSFML.ScreenPma, BlendMode.Screen => SFMLBlendMode.ScreenPma,
_ => throw new NotImplementedException($"{spineBlendMode}"), _ => throw new NotImplementedException($"{spineBlendMode}"),
}; };
} }
@@ -220,7 +221,7 @@ namespace SpineViewer.Spine.Implementations.Spine
{ {
vertexArray.Clear(); vertexArray.Clear();
states.Texture = null; states.Texture = null;
states.Shader = Shader.GetSpineShader(usePremultipliedAlpha); states.Shader = SFMLShader.GetSpineShader(usePma);
// 要用 DrawOrder 而不是 Slots // 要用 DrawOrder 而不是 Slots
foreach (var slot in skeleton.DrawOrder) foreach (var slot in skeleton.DrawOrder)

View File

@@ -4,6 +4,7 @@ using System.Linq;
using System.Text; using System.Text;
using System.Threading.Tasks; using System.Threading.Tasks;
using SpineRuntime37; using SpineRuntime37;
using SpineViewer.Extensions;
namespace SpineViewer.Spine.Implementations.Spine namespace SpineViewer.Spine.Implementations.Spine
{ {
@@ -180,10 +181,10 @@ namespace SpineViewer.Spine.Implementations.Spine
{ {
return spineBlendMode switch return spineBlendMode switch
{ {
BlendMode.Normal => BlendModeSFML.NormalPma, BlendMode.Normal => SFMLBlendMode.NormalPma,
BlendMode.Additive => BlendModeSFML.AdditivePma, BlendMode.Additive => SFMLBlendMode.AdditivePma,
BlendMode.Multiply => BlendModeSFML.MultiplyPma, BlendMode.Multiply => SFMLBlendMode.MultiplyPma,
BlendMode.Screen => BlendModeSFML.ScreenPma, BlendMode.Screen => SFMLBlendMode.ScreenPma,
_ => throw new NotImplementedException($"{spineBlendMode}"), _ => throw new NotImplementedException($"{spineBlendMode}"),
}; };
} }
@@ -192,7 +193,7 @@ namespace SpineViewer.Spine.Implementations.Spine
{ {
vertexArray.Clear(); vertexArray.Clear();
states.Texture = null; states.Texture = null;
states.Shader = Shader.GetSpineShader(usePremultipliedAlpha); states.Shader = SFMLShader.GetSpineShader(usePma);
// 要用 DrawOrder 而不是 Slots // 要用 DrawOrder 而不是 Slots
foreach (var slot in skeleton.DrawOrder) foreach (var slot in skeleton.DrawOrder)

View File

@@ -7,6 +7,7 @@ using System.Text;
using System.Threading.Tasks; using System.Threading.Tasks;
using SpineRuntime38; using SpineRuntime38;
using SpineRuntime38.Attachments; using SpineRuntime38.Attachments;
using SpineViewer.Extensions;
namespace SpineViewer.Spine.Implementations.Spine namespace SpineViewer.Spine.Implementations.Spine
{ {
@@ -188,10 +189,10 @@ namespace SpineViewer.Spine.Implementations.Spine
{ {
return spineBlendMode switch return spineBlendMode switch
{ {
BlendMode.Normal => BlendModeSFML.NormalPma, BlendMode.Normal => SFMLBlendMode.NormalPma,
BlendMode.Additive => BlendModeSFML.AdditivePma, BlendMode.Additive => SFMLBlendMode.AdditivePma,
BlendMode.Multiply => BlendModeSFML.MultiplyPma, BlendMode.Multiply => SFMLBlendMode.MultiplyPma,
BlendMode.Screen => BlendModeSFML.ScreenPma, BlendMode.Screen => SFMLBlendMode.ScreenPma,
_ => throw new NotImplementedException($"{spineBlendMode}"), _ => throw new NotImplementedException($"{spineBlendMode}"),
}; };
} }
@@ -200,7 +201,7 @@ namespace SpineViewer.Spine.Implementations.Spine
{ {
vertexArray.Clear(); vertexArray.Clear();
states.Texture = null; states.Texture = null;
states.Shader = Shader.GetSpineShader(usePremultipliedAlpha); states.Shader = SFMLShader.GetSpineShader(usePma);
// 要用 DrawOrder 而不是 Slots // 要用 DrawOrder 而不是 Slots
foreach (var slot in skeleton.DrawOrder) foreach (var slot in skeleton.DrawOrder)

View File

@@ -6,6 +6,7 @@ using System.Linq;
using System.Text; using System.Text;
using System.Threading.Tasks; using System.Threading.Tasks;
using SpineRuntime40; using SpineRuntime40;
using SpineViewer.Extensions;
namespace SpineViewer.Spine.Implementations.Spine namespace SpineViewer.Spine.Implementations.Spine
{ {
@@ -184,10 +185,10 @@ namespace SpineViewer.Spine.Implementations.Spine
{ {
return spineBlendMode switch return spineBlendMode switch
{ {
BlendMode.Normal => BlendModeSFML.NormalPma, BlendMode.Normal => SFMLBlendMode.NormalPma,
BlendMode.Additive => BlendModeSFML.AdditivePma, BlendMode.Additive => SFMLBlendMode.AdditivePma,
BlendMode.Multiply => BlendModeSFML.MultiplyPma, BlendMode.Multiply => SFMLBlendMode.MultiplyPma,
BlendMode.Screen => BlendModeSFML.ScreenPma, BlendMode.Screen => SFMLBlendMode.ScreenPma,
_ => throw new NotImplementedException($"{spineBlendMode}"), _ => throw new NotImplementedException($"{spineBlendMode}"),
}; };
} }
@@ -196,7 +197,7 @@ namespace SpineViewer.Spine.Implementations.Spine
{ {
vertexArray.Clear(); vertexArray.Clear();
states.Texture = null; states.Texture = null;
states.Shader = Shader.GetSpineShader(usePremultipliedAlpha); states.Shader = SFMLShader.GetSpineShader(usePma);
// 要用 DrawOrder 而不是 Slots // 要用 DrawOrder 而不是 Slots
foreach (var slot in skeleton.DrawOrder) foreach (var slot in skeleton.DrawOrder)

View File

@@ -6,6 +6,7 @@ using System.Linq;
using System.Text; using System.Text;
using System.Threading.Tasks; using System.Threading.Tasks;
using SpineRuntime41; using SpineRuntime41;
using SpineViewer.Extensions;
namespace SpineViewer.Spine.Implementations.Spine namespace SpineViewer.Spine.Implementations.Spine
{ {
@@ -184,10 +185,10 @@ namespace SpineViewer.Spine.Implementations.Spine
{ {
return spineBlendMode switch return spineBlendMode switch
{ {
BlendMode.Normal => BlendModeSFML.NormalPma, BlendMode.Normal => SFMLBlendMode.NormalPma,
BlendMode.Additive => BlendModeSFML.AdditivePma, BlendMode.Additive => SFMLBlendMode.AdditivePma,
BlendMode.Multiply => BlendModeSFML.MultiplyPma, BlendMode.Multiply => SFMLBlendMode.MultiplyPma,
BlendMode.Screen => BlendModeSFML.ScreenPma, BlendMode.Screen => SFMLBlendMode.ScreenPma,
_ => throw new NotImplementedException($"{spineBlendMode}"), _ => throw new NotImplementedException($"{spineBlendMode}"),
}; };
} }
@@ -196,7 +197,7 @@ namespace SpineViewer.Spine.Implementations.Spine
{ {
vertexArray.Clear(); vertexArray.Clear();
states.Texture = null; states.Texture = null;
states.Shader = Shader.GetSpineShader(usePremultipliedAlpha); states.Shader = SFMLShader.GetSpineShader(usePma);
// 要用 DrawOrder 而不是 Slots // 要用 DrawOrder 而不是 Slots
foreach (var slot in skeleton.DrawOrder) foreach (var slot in skeleton.DrawOrder)

View File

@@ -6,6 +6,7 @@ using System.Linq;
using System.Text; using System.Text;
using System.Threading.Tasks; using System.Threading.Tasks;
using SpineRuntime42; using SpineRuntime42;
using SpineViewer.Extensions;
namespace SpineViewer.Spine.Implementations.Spine namespace SpineViewer.Spine.Implementations.Spine
{ {
@@ -184,10 +185,10 @@ namespace SpineViewer.Spine.Implementations.Spine
{ {
return spineBlendMode switch return spineBlendMode switch
{ {
BlendMode.Normal => BlendModeSFML.NormalPma, BlendMode.Normal => SFMLBlendMode.NormalPma,
BlendMode.Additive => BlendModeSFML.AdditivePma, BlendMode.Additive => SFMLBlendMode.AdditivePma,
BlendMode.Multiply => BlendModeSFML.MultiplyPma, BlendMode.Multiply => SFMLBlendMode.MultiplyPma,
BlendMode.Screen => BlendModeSFML.ScreenPma, BlendMode.Screen => SFMLBlendMode.ScreenPma,
_ => throw new NotImplementedException($"{spineBlendMode}"), _ => throw new NotImplementedException($"{spineBlendMode}"),
}; };
} }
@@ -196,7 +197,7 @@ namespace SpineViewer.Spine.Implementations.Spine
{ {
vertexArray.Clear(); vertexArray.Clear();
states.Texture = null; states.Texture = null;
states.Shader = Shader.GetSpineShader(usePremultipliedAlpha); states.Shader = SFMLShader.GetSpineShader(usePma);
// 要用 DrawOrder 而不是 Slots // 要用 DrawOrder 而不是 Slots
foreach (var slot in skeleton.DrawOrder) foreach (var slot in skeleton.DrawOrder)

View File

@@ -1,10 +1,10 @@
using System; using System;
using System.Collections.ObjectModel; using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Reflection; using System.Reflection;
using System.Drawing.Design; using System.Drawing.Design;
using NLog; using NLog;
using System.Xml.Linq; using System.Xml.Linq;
using SpineViewer.Extensions;
namespace SpineViewer.Spine namespace SpineViewer.Spine
{ {
@@ -13,9 +13,6 @@ namespace SpineViewer.Spine
/// </summary> /// </summary>
public abstract class Spine : ImplementationResolver<Spine, SpineImplementationAttribute, SpineVersion>, SFML.Graphics.Drawable, IDisposable public abstract class Spine : ImplementationResolver<Spine, SpineImplementationAttribute, SpineVersion>, SFML.Graphics.Drawable, IDisposable
{ {
private readonly Logger logger = LogManager.GetCurrentClassLogger();
private bool skinLoggerWarned = false;
/// <summary> /// <summary>
/// 空动画标记 /// 空动画标记
/// </summary> /// </summary>
@@ -36,8 +33,12 @@ namespace SpineViewer.Spine
/// </summary> /// </summary>
public static Spine New(SpineVersion version, string skelPath, string? atlasPath = null) public static Spine New(SpineVersion version, string skelPath, string? atlasPath = null)
{ {
if (version == SpineVersion.Auto) version = SpineHelper.GetVersion(skelPath);
atlasPath ??= Path.ChangeExtension(skelPath, ".atlas"); 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(); return New(version, [skelPath, atlasPath]).PostInit();
} }
@@ -46,6 +47,9 @@ namespace SpineViewer.Spine
/// </summary> /// </summary>
private readonly object _lock = new(); private readonly object _lock = new();
private readonly Logger logger = LogManager.GetCurrentClassLogger();
private bool skinLoggerWarned = false;
/// <summary> /// <summary>
/// 构造函数 /// 构造函数
/// </summary> /// </summary>
@@ -64,9 +68,7 @@ namespace SpineViewer.Spine
private Spine PostInit() private Spine PostInit()
{ {
SkinNames = skinNames.AsReadOnly(); SkinNames = skinNames.AsReadOnly();
SkinManager = new(this);
AnimationNames = animationNames.AsReadOnly(); AnimationNames = animationNames.AsReadOnly();
AnimationTracks = new(this);
// 必须 Update 一次否则包围盒还没有值 // 必须 Update 一次否则包围盒还没有值
update(0); update(0);
@@ -99,79 +101,67 @@ namespace SpineViewer.Spine
public void Dispose() { Dispose(true); GC.SuppressFinalize(this); } public void Dispose() { Dispose(true); GC.SuppressFinalize(this); }
protected virtual void Dispose(bool disposing) { Preview?.Dispose(); } protected virtual void Dispose(bool disposing) { Preview?.Dispose(); }
#region | [0] /// <summary>
/// 运行时唯一 ID
/// </summary>
public string ID { get; } = Guid.NewGuid().ToString();
/// <summary>
/// 骨骼预览图, 并没有去除预乘, 画面可能偏暗
/// </summary>
public Image Preview { get; private set; }
/// <summary> /// <summary>
/// 获取所属版本 /// 获取所属版本
/// </summary> /// </summary>
[TypeConverter(typeof(SpineVersionConverter))]
[Category("[0] "), DisplayName("")]
public SpineVersion Version { get; } public SpineVersion Version { get; }
/// <summary> /// <summary>
/// 资源所在完整目录 /// 资源所在完整目录
/// </summary> /// </summary>
[Category("[0] "), DisplayName("")]
public string AssetsDir { get; } public string AssetsDir { get; }
/// <summary> /// <summary>
/// skel 文件完整路径 /// skel 文件完整路径
/// </summary> /// </summary>
[Category("[0] "), DisplayName("skel文件路径")]
public string SkelPath { get; } public string SkelPath { get; }
/// <summary> /// <summary>
/// atlas 文件完整路径 /// atlas 文件完整路径
/// </summary> /// </summary>
[Category("[0] "), DisplayName("atlas文件路径")]
public string AtlasPath { get; } public string AtlasPath { get; }
/// <summary> /// <summary>
/// 名称 /// 名称
/// </summary> /// </summary>
[Category("[0] "), DisplayName("")]
public string Name { get; } public string Name { get; }
/// <summary> /// <summary>
/// 获取所属文件版本 /// 获取所属文件版本
/// </summary> /// </summary>
[Category("[0] "), DisplayName("")]
public abstract string FileVersion { get; } public abstract string FileVersion { get; }
#endregion
#region | [1]
/// <summary> /// <summary>
/// 是否被隐藏, 被隐藏的模型将仅仅在列表显示, 不参与其他行为 /// 是否被隐藏, 被隐藏的模型将仅仅在列表显示, 不参与其他行为
/// </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; protected bool isHidden = false;
/// <summary> /// <summary>
/// 是否使用预乘Alpha /// 是否使用预乘 Alpha
/// </summary> /// </summary>
[Category("[1] "), DisplayName("Alpha通道")] public bool UsePma { get { lock (_lock) return usePma; } set { lock (_lock) usePma = value; } }
public bool UsePremultipliedAlpha protected bool usePma = false;
{
get { lock (_lock) return usePremultipliedAlpha; }
set { lock (_lock) usePremultipliedAlpha = value; }
}
protected bool usePremultipliedAlpha = false;
#endregion /// <summary>
/// 骨骼包围盒
#region | [2] /// </summary>
public RectangleF Bounds { get { lock (_lock) return bounds; } }
protected abstract RectangleF bounds { get; }
/// <summary> /// <summary>
/// 缩放比例 /// 缩放比例
/// </summary> /// </summary>
[Category("[2] "), DisplayName("")]
public float Scale public float Scale
{ {
get { lock (_lock) return scale; } get { lock (_lock) return scale; }
@@ -182,8 +172,6 @@ namespace SpineViewer.Spine
/// <summary> /// <summary>
/// 位置 /// 位置
/// </summary> /// </summary>
[TypeConverter(typeof(PointFConverter))]
[Category("[2] "), DisplayName("")]
public PointF Position public PointF Position
{ {
get { lock (_lock) return position; } get { lock (_lock) return position; }
@@ -194,7 +182,6 @@ namespace SpineViewer.Spine
/// <summary> /// <summary>
/// 水平翻转 /// 水平翻转
/// </summary> /// </summary>
[Category("[2] "), DisplayName("")]
public bool FlipX public bool FlipX
{ {
get { lock (_lock) return flipX; } get { lock (_lock) return flipX; }
@@ -205,7 +192,6 @@ namespace SpineViewer.Spine
/// <summary> /// <summary>
/// 垂直翻转 /// 垂直翻转
/// </summary> /// </summary>
[Category("[2] "), DisplayName("")]
public bool FlipY public bool FlipY
{ {
get { lock (_lock) return flipY; } get { lock (_lock) return flipY; }
@@ -213,49 +199,68 @@ namespace SpineViewer.Spine
} }
protected abstract bool flipY { get; set; } 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>
/// 包含的所有皮肤名称 /// 包含的所有皮肤名称
/// </summary> /// </summary>
[Browsable(false)]
public ReadOnlyCollection<string> SkinNames { get; private set; } public ReadOnlyCollection<string> SkinNames { get; private set; }
protected readonly List<string> skinNames = []; 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>
/// 获取已加载的皮肤列表快照, 允许出现重复值 /// 获取已加载的皮肤列表快照, 允许出现重复值
/// </summary> /// </summary>
@@ -328,13 +333,6 @@ namespace SpineViewer.Spine
/// </summary> /// </summary>
protected abstract void clearSkin(); protected abstract void clearSkin();
/// <summary>
/// 包含的所有动画名称
/// </summary>
[Browsable(false)]
public ReadOnlyCollection<string> AnimationNames { get; private set; }
protected readonly List<string> animationNames = [EMPTY_ANIMATION];
/// <summary> /// <summary>
/// 获取所有非 null 的轨道索引快照 /// 获取所有非 null 的轨道索引快照
/// </summary> /// </summary>
@@ -369,85 +367,6 @@ namespace SpineViewer.Spine
/// </summary> /// </summary>
public void ResetAnimationsTime() { lock (_lock) { foreach (var i in getTrackIndices()) setAnimation(i, getAnimation(i)); update(0); } } 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>
/// 更新内部状态 /// 更新内部状态
/// </summary> /// </summary>
@@ -490,6 +409,5 @@ namespace SpineViewer.Spine
protected abstract void draw(SFML.Graphics.RenderTarget target, SFML.Graphics.RenderStates states); protected abstract void draw(SFML.Graphics.RenderTarget target, SFML.Graphics.RenderStates states);
#endregion #endregion
} }
} }

View File

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

View File

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

View File

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

View File

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