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

View File

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

View File

@@ -1,82 +1,68 @@
using SpineViewer.Exporter;
using SpineViewer.PropertyGridWrappers.Exporter;
using System;
using System.Collections;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Diagnostics;
using System.Drawing;
using System.Drawing.Design;
using System.Drawing.Imaging;
using System.Linq;
using System.Reflection;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms;
namespace SpineViewer.Dialogs
{
public partial class ExportDialog: Form
{
/// <summary>
/// 要绑定的导出参数
/// </summary>
public required ExportArgs ExportArgs
{
get => propertyGrid_ExportArgs.SelectedObject as ExportArgs;
init
{
propertyGrid_ExportArgs.SelectedObject = value;
private readonly ExporterWrapper wrapper;
#region XXX:
var categories = propertyGrid_ExportArgs.SelectedGridItem?.Parent?.Parent?.GridItems;
if (categories is null) return;
foreach (var category in categories)
{
// 查找 "导出" 分组
if (category == null) continue;
PropertyInfo? labelProp = category.GetType().GetProperty("Label", BindingFlags.Instance | BindingFlags.Public);
if (labelProp == null) continue;
string? label = labelProp.GetValue(category) as string;
if (label != "[0] 导出") continue;
// 获取该分组下的所有属性项
PropertyInfo? gridItemsProp = category.GetType().GetProperty("GridItems", BindingFlags.Instance | BindingFlags.Public);
if (gridItemsProp == null) continue;
var gridItemsObj = gridItemsProp.GetValue(category);
if (gridItemsObj is not IEnumerable gridItems) continue;
foreach (object item in gridItems)
{
if (item == null) continue;
PropertyInfo? propDescProp = item.GetType().GetProperty("PropertyDescriptor", BindingFlags.Instance | BindingFlags.Public);
if (propDescProp == null) continue;
var propDesc = propDescProp.GetValue(item) as PropertyDescriptor;
if (propDesc == null) continue;
if (propDesc.Name == "OutputDir")
{
if (item is GridItem gridItem)
propertyGrid_ExportArgs.SelectedGridItem = gridItem; // 找到后,将此项设为选中项
else
propertyGrid_ExportArgs.SelectedGridItem = (GridItem)item; // 如果转换失败,则尝试直接赋值
}
return; // 设置成功后退出
}
}
#endregion
}
}
public ExportDialog()
public ExportDialog(ExporterWrapper wrapper)
{
InitializeComponent();
this.wrapper = wrapper;
propertyGrid_ExportArgs.SelectedObject = wrapper;
#region XXX:
var categories = propertyGrid_ExportArgs.SelectedGridItem?.Parent?.Parent?.GridItems;
if (categories is null) return;
foreach (var category in categories)
{
// 查找 "导出" 分组
if (category == null) continue;
PropertyInfo? labelProp = category.GetType().GetProperty("Label", BindingFlags.Instance | BindingFlags.Public);
if (labelProp == null) continue;
string? label = labelProp.GetValue(category) as string;
if (label != "[0] 导出") continue;
// 获取该分组下的所有属性项
PropertyInfo? gridItemsProp = category.GetType().GetProperty("GridItems", BindingFlags.Instance | BindingFlags.Public);
if (gridItemsProp == null) continue;
var gridItemsObj = gridItemsProp.GetValue(category);
if (gridItemsObj is not IEnumerable gridItems) continue;
foreach (object item in gridItems)
{
if (item == null) continue;
PropertyInfo? propDescProp = item.GetType().GetProperty("PropertyDescriptor", BindingFlags.Instance | BindingFlags.Public);
if (propDescProp == null) continue;
var propDesc = propDescProp.GetValue(item) as PropertyDescriptor;
if (propDesc == null) continue;
if (propDesc.Name == "OutputDir")
{
if (item is GridItem gridItem)
propertyGrid_ExportArgs.SelectedGridItem = gridItem; // 找到后,将此项设为选中项
else
propertyGrid_ExportArgs.SelectedGridItem = (GridItem)item; // 如果转换失败,则尝试直接赋值
}
return; // 设置成功后退出
}
}
#endregion
}
private void button_Ok_Click(object sender, EventArgs e)
{
if (ExportArgs.Validate() is string error)
if (wrapper.Exporter.Validate() is string error)
{
MessageBox.Info(error, "参数错误");
return;

View File

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

View File

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

View File

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

View File

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

View File

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

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 SpineViewer.Extensions;
using System;
using System.Collections.Generic;
using System.Drawing.Imaging;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace SpineViewer.Exporter
{
/// <summary>
/// 导出类型
/// </summary>
public enum ExportType
{
Frame,
FrameSequence,
Gif,
Mp4,
Webm,
Mkv,
Mov,
Custom,
}
/// <summary>
/// 导出实现类标记
/// </summary>
[AttributeUsage(AttributeTargets.Class, AllowMultiple = true, Inherited = false)]
public class ExportImplementationAttribute(ExportType exportType) : Attribute, IImplementationKey<ExportType>
{
public ExportType ImplementationKey { get; private set; } = exportType;
}
/// <summary>
/// SFML.Graphics.Image 帧对象包装类, 将接管给定的 image 对象生命周期
/// </summary>

View File

@@ -1,7 +1,10 @@
using NLog;
using SpineViewer.Extensions;
using SpineViewer.PropertyGridWrappers;
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Drawing.Design;
using System.Linq;
using System.Reflection;
using System.Text;
@@ -12,44 +15,80 @@ namespace SpineViewer.Exporter
/// <summary>
/// 导出器基类
/// </summary>
public abstract class Exporter(ExportArgs exportArgs) : ImplementationResolver<Exporter, ExportImplementationAttribute, ExportType>
public abstract class Exporter : IDisposable
{
/// <summary>
/// 仅源像素混合模式
/// </summary>
private static readonly SFML.Graphics.BlendMode SrcOnlyBlendMode = new(SFML.Graphics.BlendMode.Factor.One, SFML.Graphics.BlendMode.Factor.Zero);
/// <summary>
/// 创建指定类型导出器
/// </summary>
/// <param name="exportType">导出类型</param>
/// <param name="exportArgs">与 <paramref name="exportType"/> 匹配的导出参数</param>
/// <returns>与 <paramref name="exportType"/> 匹配的导出器</returns>
public static Exporter New(ExportType exportType, ExportArgs exportArgs) => New(exportType, [exportArgs]);
/// <summary>
/// 日志器
/// </summary>
protected Logger logger = LogManager.GetCurrentClassLogger();
/// <summary>
/// 导出参数
/// </summary>
public ExportArgs ExportArgs { get; } = exportArgs;
protected readonly Logger logger = LogManager.GetCurrentClassLogger();
/// <summary>
/// 可用于文件名的时间戳字符串
/// </summary>
protected readonly string timestamp = DateTime.Now.ToString("yyMMddHHmmss");
~Exporter() { Dispose(false); }
public void Dispose() { Dispose(true); GC.SuppressFinalize(this); }
protected virtual void Dispose(bool disposing) { View.Dispose(); }
/// <summary>
/// 输出文件夹
/// </summary>
public string? OutputDir { get; set; } = null;
/// <summary>
/// 导出单个
/// </summary>
public bool IsExportSingle { get; set; } = false;
/// <summary>
/// 画面分辨率
/// </summary>
public Size Resolution { get; set; } = new(100, 100);
/// <summary>
/// 渲染视窗, 接管对象生命周期
/// </summary>
public SFML.Graphics.View View { get => view; set { view.Dispose(); view = value; } }
private SFML.Graphics.View view = new();
/// <summary>
/// 是否仅渲染选中
/// </summary>
public bool RenderSelectedOnly { get; set; } = false;
/// <summary>
/// 背景颜色
/// </summary>
public SFML.Graphics.Color BackgroundColor
{
get => backgroundColor;
set
{
backgroundColor = value;
var bcPma = value;
var a = bcPma.A / 255f;
bcPma.R = (byte)(bcPma.R * a);
bcPma.G = (byte)(bcPma.G * a);
bcPma.B = (byte)(bcPma.B * a);
BackgroundColorPma = bcPma;
}
}
private SFML.Graphics.Color backgroundColor = SFML.Graphics.Color.Transparent;
/// <summary>
/// 预乘后的背景颜色
/// </summary>
public SFML.Graphics.Color BackgroundColorPma { get; private set; } = SFML.Graphics.Color.Transparent;
/// <summary>
/// 获取供渲染的 SFML.Graphics.RenderTexture
/// </summary>
private SFML.Graphics.RenderTexture GetRenderTexture()
{
var tex = new SFML.Graphics.RenderTexture((uint)ExportArgs.Resolution.Width, (uint)ExportArgs.Resolution.Height);
var tex = new SFML.Graphics.RenderTexture((uint)Resolution.Width, (uint)Resolution.Height);
tex.Clear(SFML.Graphics.Color.Transparent);
tex.SetView(ExportArgs.View);
tex.SetView(View);
return tex;
}
@@ -67,12 +106,12 @@ namespace SpineViewer.Exporter
using var texPma = GetRenderTexture();
// 先将预乘结果准确绘制出来, 注意背景色也应当是预乘的
texPma.Clear(ExportArgs.BackgroundColorPma);
texPma.Clear(BackgroundColorPma);
foreach (var spine in spinesToRender) texPma.Draw(spine);
texPma.Display();
// 背景色透明度不为 1 时需要处理反预乘, 否则直接就是结果
if (ExportArgs.BackgroundColor.A < 255)
if (BackgroundColor.A < 255)
{
// 从预乘结果构造渲染对象, 并正确设置变换
using var view = texPma.GetView();
@@ -88,14 +127,14 @@ namespace SpineViewer.Exporter
// 混合模式用直接覆盖的方式, 保证得到的图像区域是反预乘的颜色和透明度, 同时使用反预乘着色器
var st = SFML.Graphics.RenderStates.Default;
st.BlendMode = SrcOnlyBlendMode; // 用源的颜色和透明度直接覆盖
st.Shader = Shader.InversePma;
st.BlendMode = SFMLBlendMode.SourceOnly;
st.Shader = SFMLShader.InversePma;
// 在最终结果上二次渲染非预乘画面
using var tex = GetRenderTexture();
// 将非预乘结果覆盖式绘制在目标对象上, 注意背景色应该用非预乘的
tex.Clear(ExportArgs.BackgroundColor);
tex.Clear(BackgroundColor);
tex.Draw(sp, st);
tex.Display();
return new(tex.Texture.CopyToImage());
@@ -116,16 +155,36 @@ namespace SpineViewer.Exporter
/// </summary>
protected abstract void ExportIndividual(Spine.Spine[] spinesToRender, BackgroundWorker? worker = null);
/// <summary>
/// 检查参数是否合法并规范化参数值, 否则返回用户错误原因
/// </summary>
public virtual string? Validate()
{
if (!string.IsNullOrWhiteSpace(OutputDir) && File.Exists(OutputDir))
return "输出文件夹无效";
if (!string.IsNullOrWhiteSpace(OutputDir) && !Directory.Exists(OutputDir))
return $"文件夹 {OutputDir} 不存在";
if (IsExportSingle && string.IsNullOrWhiteSpace(OutputDir))
return "导出单个时必须提供输出文件夹";
OutputDir = string.IsNullOrWhiteSpace(OutputDir) ? null : Path.GetFullPath(OutputDir);
return null;
}
/// <summary>
/// 执行导出
/// </summary>
/// <param name="spines">要进行导出的 Spine 列表</param>
/// <param name="worker">用来执行该函数的 worker</param>
/// <exception cref="ArgumentException"></exception>
public virtual void Export(Spine.Spine[] spines, BackgroundWorker? worker = null)
{
var spinesToRender = spines.Where(sp => !ExportArgs.RenderSelectedOnly || sp.IsSelected).Reverse().ToArray();
if (Validate() is string err)
throw new ArgumentException(err);
if (ExportArgs.ExportSingle) ExportSingle(spinesToRender, worker);
var spinesToRender = spines.Where(sp => !RenderSelectedOnly || sp.IsSelected).Reverse().ToArray();
if (IsExportSingle) ExportSingle(spinesToRender, worker);
else ExportIndividual(spinesToRender, worker);
logger.LogCurrentProcessMemoryUsage();

View File

@@ -1,6 +1,5 @@
using FFMpegCore.Pipes;
using FFMpegCore;
using SpineViewer.Exporter.Implementations.ExportArgs;
using System;
using System.Collections.Generic;
using System.ComponentModel;
@@ -9,36 +8,63 @@ using System.Text;
using System.Threading.Tasks;
using System.Diagnostics;
namespace SpineViewer.Exporter.Implementations.Exporter
namespace SpineViewer.Exporter
{
/// <summary>
/// 使用 FFmpeg 的视频导出器
/// </summary>
[ExportImplementation(ExportType.Gif)]
[ExportImplementation(ExportType.Mp4)]
[ExportImplementation(ExportType.Webm)]
[ExportImplementation(ExportType.Mkv)]
[ExportImplementation(ExportType.Mov)]
[ExportImplementation(ExportType.Custom)]
public class FFmpegVideoExporter : VideoExporter
public abstract class FFmpegVideoExporter : VideoExporter
{
public FFmpegVideoExporter(FFmpegVideoExportArgs exportArgs) : base(exportArgs) { }
/// <summary>
/// 文件格式
/// </summary>
public abstract string Format { get; }
/// <summary>
/// 文件名后缀
/// </summary>
public abstract string Suffix { get; }
/// <summary>
/// 文件名后缀
/// </summary>
public string CustomArgument { get; set; }
/// <summary>
/// 要追加在文件名末尾的信息字串, 首尾不需要提供额外分隔符
/// </summary>
public abstract string FileNameNoteSuffix { get; }
/// <summary>
/// 获取输出附加选项
/// </summary>
public virtual void SetOutputOptions(FFMpegArgumentOptions options) => options.ForceFormat(Format).WithCustomArgument(CustomArgument);
public override string? Validate()
{
if (base.Validate() is string error)
return error;
if (string.IsNullOrWhiteSpace(Format))
return "需要提供有效的格式";
if (string.IsNullOrWhiteSpace(Suffix))
return "需要提供有效的文件名后缀";
return null;
}
protected override void ExportSingle(Spine.Spine[] spinesToRender, BackgroundWorker? worker = null)
{
var args = (FFmpegVideoExportArgs)ExportArgs;
var noteSuffix = args.FileNameNoteSuffix;
var noteSuffix = FileNameNoteSuffix;
if (!string.IsNullOrWhiteSpace(noteSuffix)) noteSuffix = $"_{noteSuffix}";
var filename = $"{timestamp}_{args.FPS:f0}{noteSuffix}{args.Suffix}";
var filename = $"ffmpeg_{timestamp}_{FPS:f0}{noteSuffix}{Suffix}";
// 导出单个时必定提供输出文件夹
var savePath = Path.Combine(args.OutputDir, filename);
var savePath = Path.Combine(OutputDir, filename);
var videoFramesSource = new RawVideoPipeSource(GetFrames(spinesToRender, worker)) { FrameRate = args.FPS };
var videoFramesSource = new RawVideoPipeSource(GetFrames(spinesToRender, worker)) { FrameRate = FPS };
try
{
var ffmpegArgs = FFMpegArguments.FromPipeInput(videoFramesSource).OutputToFile(savePath, true, args.SetOutputOptions);
var ffmpegArgs = FFMpegArguments.FromPipeInput(videoFramesSource).OutputToFile(savePath, true, SetOutputOptions);
logger.Info("FFmpeg arguments: {}", ffmpegArgs.Arguments);
ffmpegArgs.ProcessSynchronously();
@@ -46,31 +72,30 @@ namespace SpineViewer.Exporter.Implementations.Exporter
catch (Exception ex)
{
logger.Error(ex.ToString());
logger.Error("Failed to export {} {}", args.Format, savePath);
logger.Error("Failed to export {} {}", Format, savePath);
}
}
protected override void ExportIndividual(Spine.Spine[] spinesToRender, BackgroundWorker? worker = null)
{
var args = (FFmpegVideoExportArgs)ExportArgs;
var noteSuffix = args.FileNameNoteSuffix;
var noteSuffix = FileNameNoteSuffix;
if (!string.IsNullOrWhiteSpace(noteSuffix)) noteSuffix = $"_{noteSuffix}";
foreach (var spine in spinesToRender)
{
if (worker?.CancellationPending == true) break; // 取消的日志在 GetFrames 里输出
var filename = $"{spine.Name}_{timestamp}_{args.FPS:f0}{noteSuffix}{args.Suffix}";
var filename = $"{spine.Name}_{timestamp}_{FPS:f0}{noteSuffix}{Suffix}";
// 如果提供了输出文件夹, 则全部导出到输出文件夹, 否则导出到各自的文件夹下
var savePath = Path.Combine(args.OutputDir ?? spine.AssetsDir, filename);
var savePath = Path.Combine(OutputDir ?? spine.AssetsDir, filename);
var videoFramesSource = new RawVideoPipeSource(GetFrames(spine, worker)) { FrameRate = args.FPS };
var videoFramesSource = new RawVideoPipeSource(GetFrames(spine, worker)) { FrameRate = FPS };
try
{
var ffmpegArgs = FFMpegArguments
.FromPipeInput(videoFramesSource)
.OutputToFile(savePath, true, args.SetOutputOptions);
.OutputToFile(savePath, true, SetOutputOptions);
logger.Info("FFmpeg arguments: {}", ffmpegArgs.Arguments);
ffmpegArgs.ProcessSynchronously();
@@ -78,7 +103,7 @@ namespace SpineViewer.Exporter.Implementations.Exporter
catch (Exception ex)
{
logger.Error(ex.ToString());
logger.Error("Failed to export {} {} {}", args.Format, savePath, spine.SkelPath);
logger.Error("Failed to export {} {} {}", Format, savePath, spine.SkelPath);
}
}
}

View File

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

View File

@@ -1,5 +1,4 @@
using SpineViewer.Exporter.Implementations.ExportArgs;
using SpineViewer.Spine;
using SpineViewer.Spine;
using System;
using System.Collections.Generic;
using System.ComponentModel;
@@ -7,28 +6,28 @@ using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace SpineViewer.Exporter.Implementations.Exporter
namespace SpineViewer.Exporter
{
/// <summary>
/// 帧序列导出器
/// </summary>
[ExportImplementation(ExportType.FrameSequence)]
public class FrameSequenceExporter : VideoExporter
{
public FrameSequenceExporter(FrameSequenceExportArgs exportArgs) : base(exportArgs) { }
/// <summary>
/// 文件名后缀, 同时决定帧图像格式, 支持的格式为 <c>".png", ".jpg", ".tga", ".bmp"</c>
/// </summary>
public string Suffix { get; set; } = ".png";
protected override void ExportSingle(Spine.Spine[] spinesToRender, BackgroundWorker? worker = null)
{
var args = (FrameSequenceExportArgs)ExportArgs;
// 导出单个时必定提供输出文件夹,
var saveDir = Path.Combine(args.OutputDir, $"frames_{timestamp}_{args.FPS:f0}");
var saveDir = Path.Combine(OutputDir, $"frames_{timestamp}_{FPS:f0}");
Directory.CreateDirectory(saveDir);
int frameIdx = 0;
foreach (var frame in GetFrames(spinesToRender, worker))
{
var filename = $"frames_{timestamp}_{args.FPS:f0}_{frameIdx:d6}{args.Suffix}";
var filename = $"frames_{timestamp}_{FPS:f0}_{frameIdx:d6}{Suffix}";
var savePath = Path.Combine(saveDir, filename);
try
@@ -50,20 +49,19 @@ namespace SpineViewer.Exporter.Implementations.Exporter
protected override void ExportIndividual(Spine.Spine[] spinesToRender, BackgroundWorker? worker = null)
{
var args = (FrameSequenceExportArgs)ExportArgs;
foreach (var spine in spinesToRender)
{
if (worker?.CancellationPending == true) break; // 取消的日志在 GetFrames 里输出
// 如果提供了输出文件夹, 则全部导出到输出文件夹, 否则导出到各自的文件夹下
var subDir = $"{spine.Name}_{timestamp}_{args.FPS:f0}";
var saveDir = args.OutputDir is null ? Path.Combine(spine.AssetsDir, subDir) : Path.Combine(args.OutputDir, subDir);
var subDir = $"{spine.Name}_{timestamp}_{FPS:f0}";
var saveDir = Path.Combine(OutputDir ?? spine.AssetsDir, subDir);
Directory.CreateDirectory(saveDir);
int frameIdx = 0;
foreach (var frame in GetFrames(spine, worker))
{
var filename = $"{spine.Name}_{timestamp}_{args.FPS:f0}_{frameIdx:d6}{args.Suffix}";
var filename = $"{spine.Name}_{timestamp}_{FPS:f0}_{frameIdx:d6}{Suffix}";
var savePath = Path.Combine(saveDir, filename);
try

View File

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

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

View File

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

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -3,6 +3,9 @@ using SpineViewer.Spine;
using System.ComponentModel;
using System.Diagnostics;
using SpineViewer.Exporter;
using SpineViewer.Extensions;
using System.Reflection.Metadata;
using SpineViewer.PropertyGridWrappers.Exporter;
namespace SpineViewer
{
@@ -15,20 +18,10 @@ namespace SpineViewer
InitializeComponent();
InitializeLogConfiguration();
// 在此处将导出菜单需要的类绑定起来
toolStripMenuItem_ExportFrame.Tag = ExportType.Frame;
toolStripMenuItem_ExportFrameSequence.Tag = ExportType.FrameSequence;
toolStripMenuItem_ExportGif.Tag = ExportType.Gif;
toolStripMenuItem_ExportMkv.Tag = ExportType.Mkv;
toolStripMenuItem_ExportMp4.Tag = ExportType.Mp4;
toolStripMenuItem_ExportMov.Tag = ExportType.Mov;
toolStripMenuItem_ExportWebm.Tag = ExportType.Webm;
toolStripMenuItem_ExportCustom.Tag = ExportType.Custom;
// 执行一些初始化工作
try
{
Shader.Init();
SFMLShader.Init();
}
catch (Exception ex)
{
@@ -87,22 +80,23 @@ namespace SpineViewer
spineListView.BatchAdd();
}
private void toolStripMenuItem_Export_Click(object sender, EventArgs e)
#region toolStripMenuItem_ExportXXX_Click
private void toolStripMenuItem_ExportFrame_Click(object sender, EventArgs e)
{
ExportType type = (ExportType)((ToolStripMenuItem)sender).Tag;
if (type == ExportType.Frame && spinePreviewer.IsUpdating)
{
if (MessageBox.Quest("画面仍在更新,建议手动暂停画面后导出固定的一帧,是否继续?") != DialogResult.OK)
return;
}
var exportArgs = ExportArgs.New(type, spinePreviewer.Resolution, spinePreviewer.GetView(), spinePreviewer.RenderSelectedOnly);
var exportDialog = new Dialogs.ExportDialog() { ExportArgs = exportArgs };
if (exportDialog.ShowDialog() != DialogResult.OK)
if (spinePreviewer.IsUpdating && MessageBox.Quest("画面仍在更新,建议手动暂停画面后导出固定的一帧,是否继续?") != DialogResult.OK)
return;
var exporter = Exporter.Exporter.New(type, exportArgs);
var exporter = new FrameExporter()
{
Resolution = spinePreviewer.Resolution,
View = spinePreviewer.GetView(),
RenderSelectedOnly = spinePreviewer.RenderSelectedOnly
};
var exportDialog = new Dialogs.ExportDialog(new FrameExporterWrapper(exporter));
if (exportDialog.ShowDialog() != DialogResult.OK)
return;
var progressDialog = new Dialogs.ProgressDialog();
progressDialog.DoWork += Export_Work;
@@ -110,6 +104,141 @@ namespace SpineViewer
progressDialog.ShowDialog();
}
private void toolStripMenuItem_ExportFrameSequence_Click(object sender, EventArgs e)
{
var exporter = new FrameSequenceExporter()
{
Resolution = spinePreviewer.Resolution,
View = spinePreviewer.GetView(),
RenderSelectedOnly = spinePreviewer.RenderSelectedOnly
};
var exportDialog = new Dialogs.ExportDialog(new FrameSequenceExporterWrapper(exporter));
if (exportDialog.ShowDialog() != DialogResult.OK)
return;
var progressDialog = new Dialogs.ProgressDialog();
progressDialog.DoWork += Export_Work;
progressDialog.RunWorkerAsync(exporter);
progressDialog.ShowDialog();
}
private void toolStripMenuItem_ExportGif_Click(object sender, EventArgs e)
{
var exporter = new GifExporter()
{
Resolution = spinePreviewer.Resolution,
View = spinePreviewer.GetView(),
RenderSelectedOnly = spinePreviewer.RenderSelectedOnly
};
var exportDialog = new Dialogs.ExportDialog(new GifExporterWrapper(exporter));
if (exportDialog.ShowDialog() != DialogResult.OK)
return;
var progressDialog = new Dialogs.ProgressDialog();
progressDialog.DoWork += Export_Work;
progressDialog.RunWorkerAsync(exporter);
progressDialog.ShowDialog();
}
private void toolStripMenuItem_ExportMp4_Click(object sender, EventArgs e)
{
var exporter = new Mp4Exporter()
{
Resolution = spinePreviewer.Resolution,
View = spinePreviewer.GetView(),
RenderSelectedOnly = spinePreviewer.RenderSelectedOnly
};
var exportDialog = new Dialogs.ExportDialog(new Mp4ExporterWrapper(exporter));
if (exportDialog.ShowDialog() != DialogResult.OK)
return;
var progressDialog = new Dialogs.ProgressDialog();
progressDialog.DoWork += Export_Work;
progressDialog.RunWorkerAsync(exporter);
progressDialog.ShowDialog();
}
private void toolStripMenuItem_ExportWebm_Click(object sender, EventArgs e)
{
var exporter = new WebmExporter()
{
Resolution = spinePreviewer.Resolution,
View = spinePreviewer.GetView(),
RenderSelectedOnly = spinePreviewer.RenderSelectedOnly
};
var exportDialog = new Dialogs.ExportDialog(new WebmExporterWrapper(exporter));
if (exportDialog.ShowDialog() != DialogResult.OK)
return;
var progressDialog = new Dialogs.ProgressDialog();
progressDialog.DoWork += Export_Work;
progressDialog.RunWorkerAsync(exporter);
progressDialog.ShowDialog();
}
private void toolStripMenuItem_ExportMkv_Click(object sender, EventArgs e)
{
var exporter = new MkvExporter()
{
Resolution = spinePreviewer.Resolution,
View = spinePreviewer.GetView(),
RenderSelectedOnly = spinePreviewer.RenderSelectedOnly
};
var exportDialog = new Dialogs.ExportDialog(new MkvExporterWrapper(exporter));
if (exportDialog.ShowDialog() != DialogResult.OK)
return;
var progressDialog = new Dialogs.ProgressDialog();
progressDialog.DoWork += Export_Work;
progressDialog.RunWorkerAsync(exporter);
progressDialog.ShowDialog();
}
private void toolStripMenuItem_ExportMov_Click(object sender, EventArgs e)
{
var exporter = new MovExporter()
{
Resolution = spinePreviewer.Resolution,
View = spinePreviewer.GetView(),
RenderSelectedOnly = spinePreviewer.RenderSelectedOnly
};
var exportDialog = new Dialogs.ExportDialog(new MovExporterWrapper(exporter));
if (exportDialog.ShowDialog() != DialogResult.OK)
return;
var progressDialog = new Dialogs.ProgressDialog();
progressDialog.DoWork += Export_Work;
progressDialog.RunWorkerAsync(exporter);
progressDialog.ShowDialog();
}
private void toolStripMenuItem_ExportCustom_Click(object sender, EventArgs e)
{
var exporter = new CustomExporter()
{
Resolution = spinePreviewer.Resolution,
View = spinePreviewer.GetView(),
RenderSelectedOnly = spinePreviewer.RenderSelectedOnly
};
var exportDialog = new Dialogs.ExportDialog(new CustomExporterWrapper(exporter));
if (exportDialog.ShowDialog() != DialogResult.OK)
return;
var progressDialog = new Dialogs.ProgressDialog();
progressDialog.DoWork += Export_Work;
progressDialog.RunWorkerAsync(exporter);
progressDialog.ShowDialog();
}
#endregion
private void toolStripMenuItem_Exit_Click(object sender, EventArgs e)
{
Close();
@@ -127,57 +256,21 @@ namespace SpineViewer
progressDialog.ShowDialog();
}
//private System.Windows.Forms.Timer timer = new();
//private PetForm pet = new PetForm();
//private IntPtr screenDC;
//private IntPtr memDC;
private void toolStripMenuItem_ManageResource_Click(object sender, EventArgs e)
{
// screenDC = Win32.GetDC(IntPtr.Zero);
// memDC = Win32.CreateCompatibleDC(screenDC);
// pet.Show();
// timer.Tick += Timer_Tick;
// timer.Enabled = true;
// timer.Interval = 50;
// timer.Start();
//}
//private void Timer_Tick(object? sender, EventArgs e)
//{
// using var tex = new SFML.Graphics.RenderTexture((uint)pet.Width, (uint)pet.Height);
// var v = spinePreviewer.GetView();
// tex.SetView(v);
// tex.Clear(new SFML.Graphics.Color(0, 0, 0, 0));
// lock (spineListView.Spines)
// {
// foreach (var sp in spineListView.Spines)
// tex.Draw(sp);
// }
// tex.Display();
// using var frame = new SFMLImageVideoFrame(tex.Texture.CopyToImage());
// using var bitmap = frame.CopyToBitmap();
// var newBitmap = bitmap.GetHbitmap(Color.FromArgb(0));
// var oldBitmap = Win32.SelectObject(memDC, newBitmap);
// Win32.SIZE size = new Win32.SIZE { cx = pet.Width, cy = pet.Height };
// Win32.POINT srcPos = new Win32.POINT { x = 0, y = 0 };
// Win32.BLENDFUNCTION blend = new Win32.BLENDFUNCTION { BlendOp = 0, BlendFlags = 0, SourceConstantAlpha = 255, AlphaFormat = Win32.AC_SRC_ALPHA };
// Win32.UpdateLayeredWindow(pet.Handle, screenDC, IntPtr.Zero, ref size, memDC, ref srcPos, 0, ref blend, Win32.ULW_ALPHA);
// Win32.SelectObject(memDC, oldBitmap);
// Win32.DeleteObject(newBitmap);
}
private void toolStripMenuItem_About_Click(object sender, EventArgs e)
{
(new Dialogs.AboutDialog()).ShowDialog();
using var dialog = new Dialogs.AboutDialog();
dialog.ShowDialog();
}
private void toolStripMenuItem_Diagnostics_Click(object sender, EventArgs e)
{
(new Dialogs.DiagnosticsDialog()).ShowDialog();
using var dialog = new Dialogs.DiagnosticsDialog();
dialog.ShowDialog();
}
private void splitContainer_SplitterMoved(object sender, SplitterEventArgs e) => ActiveControl = null;
@@ -269,6 +362,49 @@ namespace SpineViewer
}
}
//private System.Windows.Forms.Timer timer = new();
//private PetForm pet = new PetForm();
//private IntPtr screenDC;
//private IntPtr memDC;
//private void _Test()
//{
// screenDC = Win32.GetDC(IntPtr.Zero);
// memDC = Win32.CreateCompatibleDC(screenDC);
// pet.Show();
// timer.Tick += Timer_Tick;
// timer.Enabled = true;
// timer.Interval = 50;
// timer.Start();
//}
//private void Timer_Tick(object? sender, EventArgs e)
//{
// using var tex = new SFML.Graphics.RenderTexture((uint)pet.Width, (uint)pet.Height);
// var v = spinePreviewer.GetView();
// tex.SetView(v);
// tex.Clear(new SFML.Graphics.Color(0, 0, 0, 0));
// lock (spineListView.Spines)
// {
// foreach (var sp in spineListView.Spines)
// tex.Draw(sp);
// }
// tex.Display();
// using var frame = new SFMLImageVideoFrame(tex.Texture.CopyToImage());
// using var bitmap = frame.CopyToBitmap();
// var newBitmap = bitmap.GetHbitmap(Color.FromArgb(0));
// var oldBitmap = Win32.SelectObject(memDC, newBitmap);
// Win32.SIZE size = new Win32.SIZE { cx = pet.Width, cy = pet.Height };
// Win32.POINT srcPos = new Win32.POINT { x = 0, y = 0 };
// Win32.BLENDFUNCTION blend = new Win32.BLENDFUNCTION { BlendOp = 0, BlendFlags = 0, SourceConstantAlpha = 255, AlphaFormat = Win32.AC_SRC_ALPHA };
// Win32.UpdateLayeredWindow(pet.Handle, screenDC, IntPtr.Zero, ref size, memDC, ref srcPos, 0, ref blend, Win32.ULW_ALPHA);
// Win32.SelectObject(memDC, oldBitmap);
// Win32.DeleteObject(newBitmap);
//}
//private void spinePreviewer_KeyDown(object sender, KeyEventArgs e)
//{
// switch (e.KeyCode)

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

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.ComponentModel;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace SpineViewer.Spine
namespace SpineViewer.PropertyGridWrappers.Spine
{
/// <summary>
/// 对皮肤的包装类
/// </summary>
[TypeConverter(typeof(SkinWrapperConverter))]
public class SkinWrapper(Spine spine, int i)
public class SkinWrapper(SpineViewer.Spine.Spine spine, int i)
{
private readonly Spine spine = spine;
private readonly SpineViewer.Spine.Spine spine = spine;
[Browsable(false)]
public int Index { get; } = i;
@@ -32,60 +33,94 @@ namespace SpineViewer.Spine
return base.Equals(obj);
}
public override int GetHashCode() => (typeof(SkinWrapper).FullName + ToString()).GetHashCode();
}
/// <summary>
/// 皮肤属性描述符, 实现对属性的读取和赋值
/// </summary>
/// <param name="spine">关联的 Spine 对象</param>
public class SkinWrapperPropertyDescriptor(int i) : PropertyDescriptor($"Skin{i}", [new DisplayNameAttribute($"皮肤 {i}")])
{
private readonly int idx = i;
public override Type ComponentType => typeof(SkinManager);
public override bool IsReadOnly => false;
public override Type PropertyType => typeof(SkinWrapper);
public override bool CanResetValue(object component) => false;
public override void ResetValue(object component) { }
public override bool ShouldSerializeValue(object component) => false;
/// <summary>
/// 得到一个 SkinWrapper, 允许用户查看或者修改具体的属性值, 这个地方决定了在面板上看到的是一个对象及其属性
/// </summary>
public override object? GetValue(object? component)
{
if (component is SkinManager manager)
return manager.GetSkinWrapper(idx);
return null;
}
/// <summary>
/// 允许通过字符串赋值修改该位置的皮肤
/// </summary>
public override void SetValue(object? component, object? value)
{
if (component is SkinManager manager)
{
if (value is string s)
manager.Spine.ReplaceSkin(idx, s); // manager.SetSkinWrapper(idx, s);
}
}
public override int GetHashCode() => HashCode.Combine(typeof(SkinWrapper).FullName.GetHashCode(), ToString().GetHashCode());
}
/// <summary>
/// SkinManager 动态类型包装类, 用于提供对 Spine 皮肤列表的管理能力
/// </summary>
/// <param name="spine">关联的 Spine 对象</param>
public class SkinManager(Spine spine) : ICustomTypeDescriptor
public class SpineSkinWrapper(SpineViewer.Spine.Spine spine) : ICustomTypeDescriptor
{
private static readonly Dictionary<int, SkinWrapperPropertyDescriptor> pdCache = [];
/// <summary>
/// 皮肤属性描述符, 实现对属性的读取和赋值
/// </summary>
private class SkinWrapperPropertyDescriptor(int i, Attribute[]? attributes) : PropertyDescriptor($"Skin{i}", attributes)
{
private readonly int idx = i;
public Spine Spine { get; } = spine;
public override Type ComponentType => typeof(SpineSkinWrapper);
public override bool IsReadOnly => false;
public override Type PropertyType => typeof(SkinWrapper);
public override bool CanResetValue(object component) => false;
public override void ResetValue(object component) { }
public override bool ShouldSerializeValue(object component) => false;
/// <summary>
/// 得到一个 SkinWrapper, 允许用户查看或者修改具体的属性值, 这个地方决定了在面板上看到的是一个对象及其属性
/// </summary>
public override object? GetValue(object? component)
{
if (component is SpineSkinWrapper manager)
return manager.GetSkinWrapper(idx);
return null;
}
/// <summary>
/// 允许通过字符串赋值修改该位置的皮肤
/// </summary>
public override void SetValue(object? component, object? value)
{
if (component is SpineSkinWrapper manager)
{
if (value is string s)
manager.SetSkinWrapper(idx, s);
}
}
}
[Browsable(false)]
public SpineViewer.Spine.Spine Spine { get; } = spine;
/// <summary>
/// SkinWrapper 属性缓存
/// </summary>
private readonly Dictionary<int, SkinWrapper> skinWrapperProperties = [];
/// <summary>
/// 访问 SkinWrapper 属性 <c>SkinManager.Skin{i}</c>
/// </summary>
public SkinWrapper GetSkinWrapper(int i)
{
if (!skinWrapperProperties.ContainsKey(i))
skinWrapperProperties[i] = new SkinWrapper(Spine, i);
return skinWrapperProperties[i];
}
/// <summary>
/// 设置 SkinWrapper 属性 <c>SkinManager.Skin{i} = <paramref name="value"/></c>
/// </summary>
public void SetSkinWrapper(int i, string value) => Spine.ReplaceSkin(i, value);
/// <summary>
/// 在属性面板悬停可以显示已加载的皮肤列表
/// </summary>
public override string ToString() => $"[{string.Join(", ", Spine.GetLoadedSkins())}]";
public override bool Equals(object? obj)
{
if (obj is SpineSkinWrapper wrapper) return ToString() == wrapper.ToString();
return base.Equals(obj);
}
public override int GetHashCode() => HashCode.Combine(typeof(SpineSkinWrapper).FullName.GetHashCode(), ToString().GetHashCode());
#region ICustomTypeDescriptor
// XXX: 必须实现 ICustomTypeDescriptor 接口, 不能继承 CustomTypeDescriptor, 似乎继承下来的东西会有问题, 导致某些调用不正确
private static readonly Dictionary<int, SkinWrapperPropertyDescriptor> pdCache = [];
public AttributeCollection GetAttributes() => TypeDescriptor.GetAttributes(this, true);
public string? GetClassName() => TypeDescriptor.GetClassName(this, true);
public string? GetComponentName() => TypeDescriptor.GetComponentName(this, true);
@@ -99,37 +134,16 @@ namespace SpineViewer.Spine
public PropertyDescriptorCollection GetProperties() => GetProperties(null);
public PropertyDescriptorCollection GetProperties(Attribute[]? attributes)
{
var props = new List<SkinWrapperPropertyDescriptor>();
var props = new PropertyDescriptorCollection(TypeDescriptor.GetProperties(this, attributes, true).Cast<PropertyDescriptor>().ToArray());
for (var i = 0; i < Spine.GetLoadedSkins().Length; i++)
{
if (!pdCache.ContainsKey(i))
pdCache[i] = new SkinWrapperPropertyDescriptor(i);
pdCache[i] = new SkinWrapperPropertyDescriptor(i, [new DisplayNameAttribute($"皮肤 {i}")]);
props.Add(pdCache[i]);
}
return new PropertyDescriptorCollection(props.ToArray());
return props;
}
/// <summary>
/// 访问 SkinWrapper 属性 <c>SkinManager.Skin{i}</c>
/// </summary>
public SkinWrapper GetSkinWrapper(int i)
{
if (!skinWrapperProperties.ContainsKey(i))
skinWrapperProperties[i] = new SkinWrapper(Spine, i);
return skinWrapperProperties[i];
}
/// <summary>
/// 在属性面板悬停可以显示已加载的皮肤列表
/// </summary>
public override string ToString() => $"[{string.Join(", ", Spine.GetLoadedSkins())}]";
public override bool Equals(object? obj)
{
if (obj is SkinManager manager) return ToString() == manager.ToString();
return base.Equals(obj);
}
public override int GetHashCode() => (typeof(SkinManager).FullName + ToString()).GetHashCode();
#endregion
}
}

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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