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