From 90136a556260cceafc0cf2ea58c53387d3bc1fbb Mon Sep 17 00:00:00 2001 From: ww-rm Date: Mon, 24 Mar 2025 23:05:01 +0800 Subject: [PATCH] =?UTF-8?q?=E9=87=8D=E6=9E=84=E5=AF=BC=E5=87=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- SpineViewer/Controls/SpinePreviewer.cs | 33 +--- ...g.Designer.cs => ExportDialog.Designer.cs} | 124 +++--------- SpineViewer/Dialogs/ExportDialog.cs | 47 +++++ ...rtPreviewDialog.resx => ExportDialog.resx} | 3 - SpineViewer/Dialogs/ExportPreviewDialog.cs | 169 ----------------- SpineViewer/ExportHelper/ExportArgs.cs | 153 +++++++++++++++ SpineViewer/ExportHelper/ExportHelper.cs | 42 +++++ SpineViewer/MainForm.Designer.cs | 86 +++++++-- SpineViewer/MainForm.cs | 178 +++++++++++------- SpineViewer/Spine/Spine.cs | 70 +++---- 10 files changed, 470 insertions(+), 435 deletions(-) rename SpineViewer/Dialogs/{ExportPreviewDialog.Designer.cs => ExportDialog.Designer.cs} (54%) create mode 100644 SpineViewer/Dialogs/ExportDialog.cs rename SpineViewer/Dialogs/{ExportPreviewDialog.resx => ExportDialog.resx} (99%) delete mode 100644 SpineViewer/Dialogs/ExportPreviewDialog.cs create mode 100644 SpineViewer/ExportHelper/ExportArgs.cs diff --git a/SpineViewer/Controls/SpinePreviewer.cs b/SpineViewer/Controls/SpinePreviewer.cs index 7e1262d..eacc056 100644 --- a/SpineViewer/Controls/SpinePreviewer.cs +++ b/SpineViewer/Controls/SpinePreviewer.cs @@ -9,7 +9,6 @@ using System.Threading.Tasks; using System.Windows.Forms; using System.Security.Policy; using System.Diagnostics; -using NLog.Targets; namespace SpineViewer.Controls { @@ -246,6 +245,11 @@ namespace SpineViewer.Controls public uint MaxFps { get => maxFps; set { RenderWindow.SetFramerateLimit(value); maxFps = value; } } private uint maxFps = 60; + /// + /// 获取 View + /// + public SFML.Graphics.View GetView() => RenderWindow.GetView(); + #endregion public SpinePreviewer() @@ -261,11 +265,6 @@ namespace SpineViewer.Controls MaxFps = 30; } - /// - /// 预览画面帧参数, TODO: 转移到统一导出参数 - /// - public SpinePreviewerFrameArgs GetFrameArgs() => new(Resolution, RenderWindow.GetView(), RenderSelectedOnly); - #region 渲染线程管理 /// @@ -595,26 +594,4 @@ namespace SpineViewer.Controls PauseUpdate(); } } - - /// - /// 预览画面帧参数 - /// - public class SpinePreviewerFrameArgs(Size resolution, SFML.Graphics.View view, bool renderSelectedOnly) - { - /// - /// 分辨率 - /// - public Size Resolution => resolution; - - /// - /// 渲染视窗 - /// - public SFML.Graphics.View View => view; - - /// - /// 是否仅渲染/导出选中骨骼 - /// - public bool RenderSelectedOnly => renderSelectedOnly; - } - } diff --git a/SpineViewer/Dialogs/ExportPreviewDialog.Designer.cs b/SpineViewer/Dialogs/ExportDialog.Designer.cs similarity index 54% rename from SpineViewer/Dialogs/ExportPreviewDialog.Designer.cs rename to SpineViewer/Dialogs/ExportDialog.Designer.cs index 0629ded..2568017 100644 --- a/SpineViewer/Dialogs/ExportPreviewDialog.Designer.cs +++ b/SpineViewer/Dialogs/ExportDialog.Designer.cs @@ -1,6 +1,6 @@ namespace SpineViewer.Dialogs { - partial class ExportPreviewDialog + partial class ExportDialog { /// /// Required designer variable. @@ -28,18 +28,13 @@ /// private void InitializeComponent() { - System.ComponentModel.ComponentResourceManager resources = new System.ComponentModel.ComponentResourceManager(typeof(ExportPreviewDialog)); + System.ComponentModel.ComponentResourceManager resources = new System.ComponentModel.ComponentResourceManager(typeof(ExportDialog)); panel1 = new Panel(); tableLayoutPanel1 = new TableLayoutPanel(); - propertyGrid = new PropertyGrid(); - label4 = new Label(); - label1 = new Label(); - textBox_OutputDir = new TextBox(); - button_SelectOutputDir = new Button(); + propertyGrid_ExportArgs = new PropertyGrid(); tableLayoutPanel2 = new TableLayoutPanel(); button_Ok = new Button(); button_Cancel = new Button(); - folderBrowserDialog = new FolderBrowserDialog(); panel1.SuspendLayout(); tableLayoutPanel1.SuspendLayout(); tableLayoutPanel2.SuspendLayout(); @@ -52,112 +47,58 @@ panel1.Location = new Point(0, 0); panel1.Name = "panel1"; panel1.Padding = new Padding(50, 15, 50, 10); - panel1.Size = new Size(914, 482); + panel1.Size = new Size(710, 698); panel1.TabIndex = 2; // // tableLayoutPanel1 // tableLayoutPanel1.AutoSize = true; - tableLayoutPanel1.ColumnCount = 4; - tableLayoutPanel1.ColumnStyles.Add(new ColumnStyle()); - tableLayoutPanel1.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 50F)); - tableLayoutPanel1.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 50F)); - tableLayoutPanel1.ColumnStyles.Add(new ColumnStyle()); - tableLayoutPanel1.Controls.Add(propertyGrid, 0, 2); - tableLayoutPanel1.Controls.Add(label4, 0, 0); - tableLayoutPanel1.Controls.Add(label1, 0, 1); - tableLayoutPanel1.Controls.Add(textBox_OutputDir, 1, 1); - tableLayoutPanel1.Controls.Add(button_SelectOutputDir, 3, 1); - tableLayoutPanel1.Controls.Add(tableLayoutPanel2, 0, 3); + tableLayoutPanel1.ColumnCount = 1; + tableLayoutPanel1.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 100F)); + tableLayoutPanel1.Controls.Add(propertyGrid_ExportArgs, 0, 0); + tableLayoutPanel1.Controls.Add(tableLayoutPanel2, 0, 1); tableLayoutPanel1.Dock = DockStyle.Fill; tableLayoutPanel1.Location = new Point(50, 15); tableLayoutPanel1.Name = "tableLayoutPanel1"; - tableLayoutPanel1.RowCount = 4; - tableLayoutPanel1.RowStyles.Add(new RowStyle()); - tableLayoutPanel1.RowStyles.Add(new RowStyle()); + tableLayoutPanel1.RowCount = 2; tableLayoutPanel1.RowStyles.Add(new RowStyle(SizeType.Percent, 100F)); tableLayoutPanel1.RowStyles.Add(new RowStyle()); - tableLayoutPanel1.Size = new Size(814, 457); + tableLayoutPanel1.RowStyles.Add(new RowStyle(SizeType.Absolute, 20F)); + tableLayoutPanel1.RowStyles.Add(new RowStyle(SizeType.Absolute, 20F)); + tableLayoutPanel1.Size = new Size(610, 673); tableLayoutPanel1.TabIndex = 0; // - // propertyGrid + // propertyGrid_ExportArgs // - tableLayoutPanel1.SetColumnSpan(propertyGrid, 4); - propertyGrid.Dock = DockStyle.Fill; - propertyGrid.HelpVisible = false; - propertyGrid.Location = new Point(3, 97); - propertyGrid.Name = "propertyGrid"; - propertyGrid.Size = new Size(808, 284); - propertyGrid.TabIndex = 1; - propertyGrid.ToolbarVisible = false; - // - // label4 - // - label4.AutoSize = true; - tableLayoutPanel1.SetColumnSpan(label4, 4); - label4.Dock = DockStyle.Fill; - label4.Location = new Point(15, 15); - label4.Margin = new Padding(15); - label4.Name = "label4"; - label4.Size = new Size(784, 24); - label4.TabIndex = 11; - label4.Text = "说明:输出文件夹为可选项,留空则将预览图输出到每个骨骼文件所在目录"; - label4.TextAlign = ContentAlignment.MiddleCenter; - // - // label1 - // - label1.Anchor = AnchorStyles.Right; - label1.AutoSize = true; - label1.Location = new Point(3, 62); - label1.Name = "label1"; - label1.Size = new Size(104, 24); - label1.TabIndex = 0; - label1.Text = "输出文件夹:"; - // - // textBox_OutputDir - // - tableLayoutPanel1.SetColumnSpan(textBox_OutputDir, 2); - textBox_OutputDir.Dock = DockStyle.Fill; - textBox_OutputDir.Location = new Point(113, 57); - textBox_OutputDir.Name = "textBox_OutputDir"; - textBox_OutputDir.Size = new Size(660, 30); - textBox_OutputDir.TabIndex = 3; - // - // button_SelectOutputDir - // - button_SelectOutputDir.AutoSize = true; - button_SelectOutputDir.AutoSizeMode = AutoSizeMode.GrowAndShrink; - button_SelectOutputDir.Location = new Point(779, 57); - button_SelectOutputDir.Name = "button_SelectOutputDir"; - button_SelectOutputDir.Size = new Size(32, 34); - button_SelectOutputDir.TabIndex = 5; - button_SelectOutputDir.Text = "..."; - button_SelectOutputDir.UseVisualStyleBackColor = true; - button_SelectOutputDir.Click += button_SelectOutputDir_Click; + propertyGrid_ExportArgs.Dock = DockStyle.Fill; + propertyGrid_ExportArgs.Location = new Point(3, 3); + propertyGrid_ExportArgs.Name = "propertyGrid_ExportArgs"; + propertyGrid_ExportArgs.Size = new Size(604, 594); + propertyGrid_ExportArgs.TabIndex = 1; + propertyGrid_ExportArgs.ToolbarVisible = false; // // tableLayoutPanel2 // tableLayoutPanel2.AutoSize = true; tableLayoutPanel2.AutoSizeMode = AutoSizeMode.GrowAndShrink; tableLayoutPanel2.ColumnCount = 2; - tableLayoutPanel1.SetColumnSpan(tableLayoutPanel2, 4); tableLayoutPanel2.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 50F)); tableLayoutPanel2.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 50F)); tableLayoutPanel2.Controls.Add(button_Ok, 0, 0); tableLayoutPanel2.Controls.Add(button_Cancel, 1, 0); tableLayoutPanel2.Dock = DockStyle.Bottom; - tableLayoutPanel2.Location = new Point(3, 414); + tableLayoutPanel2.Location = new Point(3, 630); tableLayoutPanel2.Margin = new Padding(3, 30, 3, 3); tableLayoutPanel2.Name = "tableLayoutPanel2"; tableLayoutPanel2.RowCount = 1; tableLayoutPanel2.RowStyles.Add(new RowStyle()); - tableLayoutPanel2.Size = new Size(808, 40); + tableLayoutPanel2.Size = new Size(604, 40); tableLayoutPanel2.TabIndex = 10; // // button_Ok // button_Ok.Anchor = AnchorStyles.Bottom | AnchorStyles.Right; - button_Ok.Location = new Point(262, 3); + button_Ok.Location = new Point(160, 3); button_Ok.Margin = new Padding(3, 3, 30, 3); button_Ok.Name = "button_Ok"; button_Ok.Size = new Size(112, 34); @@ -169,7 +110,7 @@ // button_Cancel // button_Cancel.Anchor = AnchorStyles.Bottom | AnchorStyles.Left; - button_Cancel.Location = new Point(434, 3); + button_Cancel.Location = new Point(332, 3); button_Cancel.Margin = new Padding(30, 3, 3, 3); button_Cancel.Name = "button_Cancel"; button_Cancel.Size = new Size(112, 34); @@ -178,26 +119,22 @@ button_Cancel.UseVisualStyleBackColor = true; button_Cancel.Click += button_Cancel_Click; // - // folderBrowserDialog - // - folderBrowserDialog.AddToRecent = false; - // - // ExportPreviewDialog + // ExportDialog // AcceptButton = button_Ok; AutoScaleDimensions = new SizeF(11F, 24F); AutoScaleMode = AutoScaleMode.Font; CancelButton = button_Cancel; - ClientSize = new Size(914, 482); + ClientSize = new Size(710, 698); Controls.Add(panel1); FormBorderStyle = FormBorderStyle.FixedDialog; Icon = (Icon)resources.GetObject("$this.Icon"); MaximizeBox = false; MinimizeBox = false; - Name = "ExportPreviewDialog"; + Name = "ExportDialog"; ShowInTaskbar = false; StartPosition = FormStartPosition.CenterScreen; - Text = "导出预览图"; + Text = "导出参数"; panel1.ResumeLayout(false); panel1.PerformLayout(); tableLayoutPanel1.ResumeLayout(false); @@ -210,14 +147,9 @@ private Panel panel1; private TableLayoutPanel tableLayoutPanel1; - private Label label4; - private Label label1; - private TextBox textBox_OutputDir; - private Button button_SelectOutputDir; private TableLayoutPanel tableLayoutPanel2; private Button button_Ok; private Button button_Cancel; - private FolderBrowserDialog folderBrowserDialog; - private PropertyGrid propertyGrid; + private PropertyGrid propertyGrid_ExportArgs; } } \ No newline at end of file diff --git a/SpineViewer/Dialogs/ExportDialog.cs b/SpineViewer/Dialogs/ExportDialog.cs new file mode 100644 index 0000000..194520e --- /dev/null +++ b/SpineViewer/Dialogs/ExportDialog.cs @@ -0,0 +1,47 @@ +using SpineViewer.ExportHelper; +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Data; +using System.Drawing; +using System.Drawing.Design; +using System.Drawing.Imaging; +using System.Linq; +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; + } + + public ExportDialog() + { + InitializeComponent(); + } + + private void button_Ok_Click(object sender, EventArgs e) + { + if (ExportArgs.Validate() is string error) + { + MessageBox.Info(error, "参数错误"); + return; + } + DialogResult = DialogResult.OK; + } + + private void button_Cancel_Click(object sender, EventArgs e) + { + DialogResult = DialogResult.Cancel; + } + } +} diff --git a/SpineViewer/Dialogs/ExportPreviewDialog.resx b/SpineViewer/Dialogs/ExportDialog.resx similarity index 99% rename from SpineViewer/Dialogs/ExportPreviewDialog.resx rename to SpineViewer/Dialogs/ExportDialog.resx index d2f2a21..55a66f3 100644 --- a/SpineViewer/Dialogs/ExportPreviewDialog.resx +++ b/SpineViewer/Dialogs/ExportDialog.resx @@ -117,9 +117,6 @@ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - 17, 17 - diff --git a/SpineViewer/Dialogs/ExportPreviewDialog.cs b/SpineViewer/Dialogs/ExportPreviewDialog.cs deleted file mode 100644 index 3519e53..0000000 --- a/SpineViewer/Dialogs/ExportPreviewDialog.cs +++ /dev/null @@ -1,169 +0,0 @@ -using System; -using System.Collections.Generic; -using System.ComponentModel; -using System.Data; -using System.Drawing; -using System.Drawing.Imaging; -using System.Linq; -using System.Text; -using System.Threading.Tasks; -using System.Windows.Forms; - -namespace SpineViewer.Dialogs -{ - public partial class ExportPreviewDialog: Form - { - /// - /// 对话框结果 - /// - public readonly ExportPreviewDialogResult Result = new(); - - public ExportPreviewDialog() - { - InitializeComponent(); - propertyGrid.SelectedObject = Result; - } - - private void button_SelectOutputDir_Click(object sender, EventArgs e) - { - folderBrowserDialog.InitialDirectory = textBox_OutputDir.Text; - if (folderBrowserDialog.ShowDialog() != DialogResult.OK) - return; - textBox_OutputDir.Text = Path.GetFullPath(folderBrowserDialog.SelectedPath); - } - - private void button_Ok_Click(object sender, EventArgs e) - { - var outputDir = textBox_OutputDir.Text; - if (string.IsNullOrEmpty(outputDir)) - { - outputDir = null; - } - else - { - if (File.Exists(outputDir)) - { - MessageBox.Info("输出文件夹无效"); - return; - } - - if (!Directory.Exists(outputDir)) - { - if (MessageBox.Quest($"文件夹 {outputDir} 不存在,是否创建?") != DialogResult.OK) - return; - - try - { - Directory.CreateDirectory(outputDir); - } - catch (Exception ex) - { - Program.Logger.Error(ex.ToString()); - MessageBox.Error(ex.ToString(), "文件夹创建失败"); - return; - } - } - outputDir = Path.GetFullPath(outputDir); - } - - if (outputDir is null && string.IsNullOrEmpty(Result.NameSuffix)) - { - MessageBox.Info("输出文件夹和名称后缀不可同时为空,存在文件覆盖风险"); - return; - } - - Result.OutputDir = outputDir; - DialogResult = DialogResult.OK; - } - - private void button_Cancel_Click(object sender, EventArgs e) - { - DialogResult = DialogResult.Cancel; - } - } - - public class ExportPreviewDialogResult - { - /// - /// 输出路径 - /// - [Browsable(false)] - public string? OutputDir { get; set; } = null; - - /// - /// 预览图格式 - /// - [TypeConverter(typeof(ImageFormatConverter))] - [Category("图像"), DisplayName("预览图格式")] - public ImageFormat ImageFormat - { - get => imageFormat; - set - { - if (value == ImageFormat.MemoryBmp) value = ImageFormat.Bmp; - imageFormat = value; - } - } - private ImageFormat imageFormat = ImageFormat.Png; - - /// - /// 预览图分辨率 - /// - [TypeConverter(typeof(SizeConverter))] - [Category("图像"), DisplayName("分辨率")] - public Size Resolution - { - get => resolution; - set - { - if (value.Width <= 0) value.Width = 128; - if (value.Height <= 0) value.Height = 128; - resolution = value; - } - } - private Size resolution = new(512, 512); - - /// - /// 四周填充像素值 - /// - [TypeConverter(typeof(PaddingConverter))] - [Category("图像"), DisplayName("四周填充像素值")] - public Padding Padding - { - get => padding; - set - { - if (value.Left <= 0) value.Left = 10; - if (value.Right <= 0) value.Right = 10; - if (value.Top <= 0) value.Top = 10; - if (value.Bottom <= 0) value.Bottom = 10; - padding = value; - } - } - private Padding padding = new(1); - - /// - /// DPI - /// - [TypeConverter(typeof(SizeFConverter))] - [Category("图像"), DisplayName("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); - - /// - /// 名称后缀 - /// - [Category("其他"), DisplayName("名称后缀")] - public string NameSuffix { get; set; } = "(preview)"; - } - -} diff --git a/SpineViewer/ExportHelper/ExportArgs.cs b/SpineViewer/ExportHelper/ExportArgs.cs new file mode 100644 index 0000000..a6210a8 --- /dev/null +++ b/SpineViewer/ExportHelper/ExportArgs.cs @@ -0,0 +1,153 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Drawing.Design; +using System.Drawing.Imaging; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace SpineViewer.ExportHelper +{ + /// + /// 导出参数基类 + /// + public abstract class ExportArgs + { + /// + /// 输出文件夹 + /// + [Editor(typeof(FolderNameEditor), typeof(UITypeEditor))] + [Category("导出"), DisplayName("输出文件夹"), Description("逐个导出时可以留空,将逐个导出到模型自身所在目录")] + public string? OutputDir { get; set; } = null; + + /// + /// 逐个导出 + /// + [Category("导出"), DisplayName("导出单个"), Description("是否将模型在同一个画面上导出单个文件,否则逐个导出模型")] + public bool ExportSingle { get; set; } = false; + + /// + /// 画面分辨率 + /// + [TypeConverter(typeof(SizeConverter))] + [Category("导出"), DisplayName("分辨率"), Description("画面的宽高像素大小")] + public required Size Resolution { get; init; } + + /// + /// 渲染视窗 + /// + [Browsable(false)] + public required SFML.Graphics.View View { get; init; } + + /// + /// 是否仅渲染选中 + /// + [Category("导出"), DisplayName("仅渲染选中"), Description("是否仅导出选中的模型")] + public required bool RenderSelectedOnly { get; init; } + + /// + /// 检查参数是否合法并规范化参数值, 否则返回用户错误原因 + /// + /// + public virtual string? Validate() + { + if (!string.IsNullOrEmpty(OutputDir) && File.Exists(OutputDir)) + return "输出文件夹无效"; + if (!string.IsNullOrEmpty(OutputDir) && !Directory.Exists(OutputDir)) + return $"文件夹 {OutputDir} 不存在"; + if (ExportSingle && string.IsNullOrEmpty(OutputDir)) + return "导出单个时必须提供输出文件夹"; + + OutputDir = string.IsNullOrEmpty(OutputDir) ? null : Path.GetFullPath(OutputDir); + return null; + } + } + + /// + /// 画面帧导出参数 + /// + public class ExportFrameArgs : ExportArgs + { + /// + /// 名称后缀 + /// + [Category("画面帧"), DisplayName("名称后缀"), Description("逐个导出时必须提供该值,否则存在文件覆盖风险")] + public string NameSuffix { get; set; } = "(preview)"; + + /// + /// 画面帧格式 + /// + [TypeConverter(typeof(ImageFormatConverter))] + [Category("画面帧"), DisplayName("图像格式")] + public ImageFormat ImageFormat + { + get => imageFormat; + set + { + if (value == ImageFormat.MemoryBmp) value = ImageFormat.Bmp; + imageFormat = value; + } + } + private ImageFormat imageFormat = ImageFormat.Png; + + /// + /// 文件名后缀 + /// + [Category("画面帧"), DisplayName("文件名后缀"), Description("与图像格式匹配的文件名后缀")] + public string FileSuffix { get => imageFormat.GetSuffix(); } + + /// + /// 四周填充像素值 + /// + [TypeConverter(typeof(PaddingConverter))] + [Category("画面帧"), DisplayName("四周填充像素值"), Description("在图内四周留出来的透明像素区域, 画面内容的可用范围是分辨率裁去填充区域")] + public Padding Padding + { + get => padding; + set + { + if (value.Left < 0) value.Left = 0; + if (value.Right < 0) value.Right = 0; + if (value.Top < 0) value.Top = 0; + if (value.Bottom < 0) value.Bottom = 0; + padding = value; + } + } + private Padding padding = new(1); + + /// + /// DPI + /// + [TypeConverter(typeof(SizeFConverter))] + [Category("画面帧"), 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); + + public override string? Validate() + { + if (base.Validate() is string error) + return error; + if (string.IsNullOrEmpty(OutputDir) && string.IsNullOrEmpty(NameSuffix)) + return "输出文件夹和名称后缀不可同时为空,存在文件覆盖风险"; + return null; + } + } + + /// + /// 视频导出参数基类 + /// + public abstract class ExportVideoArgs:ExportArgs + { + + } +} diff --git a/SpineViewer/ExportHelper/ExportHelper.cs b/SpineViewer/ExportHelper/ExportHelper.cs index 10beedc..c588a0f 100644 --- a/SpineViewer/ExportHelper/ExportHelper.cs +++ b/SpineViewer/ExportHelper/ExportHelper.cs @@ -38,5 +38,47 @@ namespace SpineViewer.ExportHelper else if (imageFormat == ImageFormat.Exif) return ".jpg"; else return $".{imageFormat.ToString().ToLower()}"; } + + /// + /// 获取某个包围盒下合适的视图 + /// + public static SFML.Graphics.View GetView(this RectangleF bounds, Size resolution, Padding padding) + => GetView(bounds, (uint)resolution.Width, (uint)resolution.Height, (uint)padding.Left, (uint)padding.Right, (uint)padding.Top, (uint)padding.Bottom); + + /// + /// 获取某个包围盒下合适的视图 + /// + public static SFML.Graphics.View GetView(this RectangleF bounds, uint width, uint height, Padding padding) + => GetView(bounds, width, height, (uint)padding.Left, (uint)padding.Right, (uint)padding.Top, (uint)padding.Bottom); + + /// + /// 获取某个包围盒下合适的视图 + /// + public static SFML.Graphics.View GetView(this RectangleF bounds, Size resolution, uint paddingL = 1, uint paddingR = 1, uint paddingT = 1, uint paddingB = 1) + => GetView(bounds, (uint)resolution.Width, (uint)resolution.Height, paddingL, paddingR, paddingT, paddingB); + + /// + /// 获取某个包围盒下合适的视图 + /// + public static SFML.Graphics.View GetView(this RectangleF bounds, uint width, uint height, uint paddingL = 1, uint paddingR = 1, uint paddingT = 1, uint paddingB = 1) + { + float sizeX = bounds.Width; + float sizeY = bounds.Height; + float innerW = width - paddingL - paddingR; + float innerH = height - paddingT - paddingB; + + float scale = 1; + if ((sizeY / sizeX) < (innerH / innerW)) + scale = sizeX / innerW; // 相同的 X, 视窗 Y 更大 + else + scale = sizeY / innerH; // 相同的 Y, 视窗 X 更大 + + var x = bounds.X + bounds.Width / 2 + ((float)paddingL - (float)paddingR) * scale; + var y = bounds.Y + bounds.Height / 2 + ((float)paddingT - (float)paddingB) * scale; + var viewX = width * scale; + var viewY = height * scale; + + return new(new(x, y), new(viewX, -viewY)); + } } } diff --git a/SpineViewer/MainForm.Designer.cs b/SpineViewer/MainForm.Designer.cs index 476d91f..b09b994 100644 --- a/SpineViewer/MainForm.Designer.cs +++ b/SpineViewer/MainForm.Designer.cs @@ -36,7 +36,13 @@ toolStripMenuItem_BatchOpen = new ToolStripMenuItem(); toolStripSeparator1 = new ToolStripSeparator(); toolStripMenuItem_Export = new ToolStripMenuItem(); - toolStripMenuItem_ExportPreview = new ToolStripMenuItem(); + toolStripMenuItem_ExportFrame = new ToolStripMenuItem(); + toolStripMenuItem_ExportFrames = new ToolStripMenuItem(); + toolStripMenuItem_ExportGif = new ToolStripMenuItem(); + toolStripMenuItem_ExportMkv = new ToolStripMenuItem(); + toolStripMenuItem_ExportMp4 = new ToolStripMenuItem(); + toolStripMenuItem_ExportMov = new ToolStripMenuItem(); + toolStripMenuItem_ExportWebm = new ToolStripMenuItem(); toolStripSeparator2 = new ToolStripSeparator(); toolStripMenuItem_Exit = new ToolStripMenuItem(); toolStripMenuItem_Tool = new ToolStripMenuItem(); @@ -99,7 +105,7 @@ // // toolStripMenuItem_File // - toolStripMenuItem_File.DropDownItems.AddRange(new ToolStripItem[] { toolStripMenuItem_Open, toolStripMenuItem_BatchOpen, toolStripSeparator1, toolStripMenuItem_Export, toolStripMenuItem_ExportPreview, toolStripSeparator2, toolStripMenuItem_Exit }); + toolStripMenuItem_File.DropDownItems.AddRange(new ToolStripItem[] { toolStripMenuItem_Open, toolStripMenuItem_BatchOpen, toolStripSeparator1, toolStripMenuItem_Export, toolStripSeparator2, toolStripMenuItem_Exit }); toolStripMenuItem_File.Name = "toolStripMenuItem_File"; toolStripMenuItem_File.Size = new Size(84, 28); toolStripMenuItem_File.Text = "文件(&F)"; @@ -108,47 +114,83 @@ // 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_ExportFrames, toolStripMenuItem_ExportGif, toolStripMenuItem_ExportMkv, toolStripMenuItem_ExportMp4, toolStripMenuItem_ExportMov, toolStripMenuItem_ExportWebm }); toolStripMenuItem_Export.Name = "toolStripMenuItem_Export"; - toolStripMenuItem_Export.ShortcutKeys = Keys.Control | Keys.S; - toolStripMenuItem_Export.Size = new Size(254, 34); - toolStripMenuItem_Export.Text = "导出(&E)..."; - toolStripMenuItem_Export.Click += toolStripMenuItem_Export_Click; + toolStripMenuItem_Export.Size = new Size(270, 34); + toolStripMenuItem_Export.Text = "导出(&E)"; // - // toolStripMenuItem_ExportPreview + // toolStripMenuItem_ExportFrame // - toolStripMenuItem_ExportPreview.Name = "toolStripMenuItem_ExportPreview"; - toolStripMenuItem_ExportPreview.Size = new Size(254, 34); - toolStripMenuItem_ExportPreview.Text = "导出预览图(&P)..."; - toolStripMenuItem_ExportPreview.Click += toolStripMenuItem_ExportPreview_Click; + toolStripMenuItem_ExportFrame.Name = "toolStripMenuItem_ExportFrame"; + toolStripMenuItem_ExportFrame.Size = new Size(270, 34); + toolStripMenuItem_ExportFrame.Text = "单帧画面..."; + toolStripMenuItem_ExportFrame.Click += toolStripMenuItem_ExportFrame_Click; + // + // toolStripMenuItem_ExportFrames + // + toolStripMenuItem_ExportFrames.Name = "toolStripMenuItem_ExportFrames"; + toolStripMenuItem_ExportFrames.Size = new Size(270, 34); + toolStripMenuItem_ExportFrames.Text = "帧序列..."; + toolStripMenuItem_ExportFrames.Click += toolStripMenuItem_ExportPng_Click; + // + // toolStripMenuItem_ExportGif + // + toolStripMenuItem_ExportGif.Name = "toolStripMenuItem_ExportGif"; + toolStripMenuItem_ExportGif.Size = new Size(270, 34); + toolStripMenuItem_ExportGif.Text = "GIF..."; + // + // toolStripMenuItem_ExportMkv + // + toolStripMenuItem_ExportMkv.Name = "toolStripMenuItem_ExportMkv"; + toolStripMenuItem_ExportMkv.Size = new Size(270, 34); + toolStripMenuItem_ExportMkv.Text = "MKV"; + // + // toolStripMenuItem_ExportMp4 + // + toolStripMenuItem_ExportMp4.Name = "toolStripMenuItem_ExportMp4"; + toolStripMenuItem_ExportMp4.Size = new Size(270, 34); + toolStripMenuItem_ExportMp4.Text = "MP4..."; + // + // toolStripMenuItem_ExportMov + // + toolStripMenuItem_ExportMov.Name = "toolStripMenuItem_ExportMov"; + toolStripMenuItem_ExportMov.Size = new Size(270, 34); + toolStripMenuItem_ExportMov.Text = "MOV..."; + // + // toolStripMenuItem_ExportWebm + // + toolStripMenuItem_ExportWebm.Name = "toolStripMenuItem_ExportWebm"; + toolStripMenuItem_ExportWebm.Size = new Size(270, 34); + toolStripMenuItem_ExportWebm.Text = "WebM..."; // // 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; // @@ -162,7 +204,7 @@ // toolStripMenuItem_ConvertFileFormat // toolStripMenuItem_ConvertFileFormat.Name = "toolStripMenuItem_ConvertFileFormat"; - toolStripMenuItem_ConvertFileFormat.Size = new Size(270, 34); + toolStripMenuItem_ConvertFileFormat.Size = new Size(254, 34); toolStripMenuItem_ConvertFileFormat.Text = "转换文件格式(&C)..."; toolStripMenuItem_ConvertFileFormat.Click += toolStripMenuItem_ConvertFileFormat_Click; // @@ -464,7 +506,6 @@ private ToolStripMenuItem toolStripMenuItem_Open; private ToolStripMenuItem toolStripMenuItem_Exit; private ToolStripSeparator toolStripSeparator1; - private ToolStripMenuItem toolStripMenuItem_Export; private ToolStripSeparator toolStripSeparator2; private RichTextBox rtbLog; private SplitContainer splitContainer_MainForm; @@ -490,6 +531,13 @@ private ToolStripMenuItem toolStripMenuItem_ManageResource; private ToolStripMenuItem toolStripMenuItem_Tool; private ToolStripMenuItem toolStripMenuItem_ConvertFileFormat; - private ToolStripMenuItem toolStripMenuItem_ExportPreview; + private ToolStripMenuItem toolStripMenuItem_Export; + private ToolStripMenuItem toolStripMenuItem_ExportFrame; + private ToolStripMenuItem toolStripMenuItem_ExportFrames; + private ToolStripMenuItem toolStripMenuItem_ExportGif; + private ToolStripMenuItem toolStripMenuItem_ExportMp4; + private ToolStripMenuItem toolStripMenuItem_ExportMov; + private ToolStripMenuItem toolStripMenuItem_ExportMkv; + private ToolStripMenuItem toolStripMenuItem_ExportWebm; } } diff --git a/SpineViewer/MainForm.cs b/SpineViewer/MainForm.cs index f75f145..b6f1268 100644 --- a/SpineViewer/MainForm.cs +++ b/SpineViewer/MainForm.cs @@ -8,6 +8,7 @@ using System.Diagnostics; using System.Text.Json; using System.Text.Json.Nodes; using FFMpegCore.Enums; +using SpineViewer.ExportHelper; namespace SpineViewer { @@ -49,12 +50,12 @@ namespace SpineViewer private void MainForm_Load(object sender, EventArgs e) { - spinePreviewer.StartPreview(); + spinePreviewer.StartRender(); } private void MainForm_FormClosing(object sender, FormClosingEventArgs e) { - spinePreviewer.StopPreview(); + spinePreviewer.StopRender(); } private void toolStripMenuItem_Open_Click(object sender, EventArgs e) @@ -67,7 +68,42 @@ namespace SpineViewer spineListView.BatchAdd(); } - private void toolStripMenuItem_Export_Click(object sender, EventArgs e) + private void toolStripMenuItem_ExportFrame_Click(object sender, EventArgs e) + { + lock (spineListView.Spines) + { + if (spineListView.Spines.Count <= 0) + { + MessageBox.Info("请至少打开一个骨骼文件"); + return; + } + } + + if (spinePreviewer.IsUpdating) + { + if (MessageBox.Quest("画面仍在更新,是否手动暂停画面后再导出画面帧?") == DialogResult.OK) + return; + } + + var exportDialog = new Dialogs.ExportDialog() + { + ExportArgs = new ExportFrameArgs() + { + Resolution = spinePreviewer.Resolution, + View = spinePreviewer.GetView(), + RenderSelectedOnly = spinePreviewer.RenderSelectedOnly, + } + }; + if (exportDialog.ShowDialog() != DialogResult.OK) + return; + + var progressDialog = new Dialogs.ProgressDialog(); + progressDialog.DoWork += ExportFrame_Work; + progressDialog.RunWorkerAsync(exportDialog.ExportArgs); + progressDialog.ShowDialog(); + } + + private void toolStripMenuItem_ExportPng_Click(object sender, EventArgs e) { // TODO: 改成统一导出调用 lock (spineListView.Spines) @@ -89,27 +125,6 @@ namespace SpineViewer progressDialog.ShowDialog(); } - private void toolStripMenuItem_ExportPreview_Click(object sender, EventArgs e) - { - lock (spineListView.Spines) - { - if (spineListView.Spines.Count <= 0) - { - MessageBox.Info("请至少打开一个骨骼文件"); - return; - } - } - - var saveDialog = new Dialogs.ExportPreviewDialog(); - if (saveDialog.ShowDialog() != DialogResult.OK) - return; - - var progressDialog = new Dialogs.ProgressDialog(); - progressDialog.DoWork += ExportPreview_Work; - progressDialog.RunWorkerAsync(saveDialog.Result); - progressDialog.ShowDialog(); - } - private void toolStripMenuItem_Exit_Click(object sender, EventArgs e) { Close(); @@ -223,16 +238,15 @@ namespace SpineViewer var fps = arguments.Fps; var timestamp = DateTime.Now.ToString("yyMMddHHmmss"); - var frameArgs = spinePreviewer.GetFrameArgs(); var renderSelectedOnly = spinePreviewer.RenderSelectedOnly; - var resolution = frameArgs.Resolution; + var resolution = spinePreviewer.Resolution; var tex = new SFML.Graphics.RenderTexture((uint)resolution.Width, (uint)resolution.Height); - tex.SetView(frameArgs.View); + tex.SetView(spinePreviewer.GetView()); var delta = 1f / fps; var frameCount = 1 + (int)(duration / delta); // 零帧开始导出 - spinePreviewer.StopPreview(); + spinePreviewer.StopRender(); lock (spineListView.Spines) { @@ -279,31 +293,27 @@ namespace SpineViewer Program.Logger.Info("Exporting done: {}/{}", success, frameCount); } - spinePreviewer.StartPreview(); + spinePreviewer.StartRender(); } - private void ExportPreview_Work(object? sender, DoWorkEventArgs e) + private void ExportFrame_Work(object? sender, DoWorkEventArgs e) { - var worker = sender as BackgroundWorker; - var arguments = e.Argument as Dialogs.ExportPreviewDialogResult; + var worker = (BackgroundWorker)sender; + var args = (ExportFrameArgs)e.Argument; - var outputDir = arguments.OutputDir; - var imageFormat = arguments.ImageFormat; - var resolution = arguments.Resolution; - var padding = arguments.Padding; - var dpi = arguments.DPI; - var nameSuffix = arguments.NameSuffix; - var renderSelectedOnly = spinePreviewer.RenderSelectedOnly; - - var tex = new SFML.Graphics.RenderTexture((uint)resolution.Width, (uint)resolution.Height); + using var tex = new SFML.Graphics.RenderTexture((uint)args.Resolution.Width, (uint)args.Resolution.Height); + tex.Clear(SFML.Graphics.Color.Transparent); + tex.SetView(args.View); int success = 0; int error = 0; - spinePreviewer.StopPreview(); + spinePreviewer.StopRender(); lock (spineListView.Spines) { - var spines = spineListView.Spines; - int totalCount = spines.Count; + // 根据是否仅渲染选中得到要渲染的模型数组 + var spines = spineListView.Spines.Where(sp => !args.RenderSelectedOnly || sp.IsSelected).Reverse().ToArray(); + + int totalCount = spines.Length; worker.ReportProgress(0, $"已处理 0/{totalCount}"); for (int i = 0; i < totalCount; i++) { @@ -314,49 +324,73 @@ namespace SpineViewer } var spine = spines[i]; - if (renderSelectedOnly && !spine.IsSelected) - continue; - - var filename = $"{spine.Name}{nameSuffix}{imageFormat.GetSuffix()}"; - var savePath = outputDir is null ? Path.Combine(spine.AssetsDir, filename) : Path.Combine(outputDir, filename); - - var tmp = spine.CurrentAnimation; - spine.CurrentAnimation = Spine.Spine.EMPTY_ANIMATION; - tex.SetView(spine.GetInitView(resolution, padding)); - tex.Clear(SFML.Graphics.Color.Transparent); tex.Draw(spine); + + if (args.ExportSingle) + { + // 导出单个则直接算成功 + success++; + } + else + { + // 逐个导出则立即渲染, 并且保存完之后需要清除画面 + tex.Display(); + + var filename = $"{spine.Name}{args.NameSuffix}{args.FileSuffix}"; + var savePath = args.OutputDir is null ? Path.Combine(spine.AssetsDir, filename) : Path.Combine(args.OutputDir, filename); + try + { + using (var img = new Bitmap(tex.Texture.CopyToBitmap())) + { + img.SetResolution(args.DPI.Width, args.DPI.Height); + img.Save(savePath, args.ImageFormat); + } + success++; + } + catch (Exception ex) + { + Program.Logger.Error(ex.ToString()); + Program.Logger.Error("Failed to save frame {}", spine.SkelPath); + error++; + } + + tex.Clear(SFML.Graphics.Color.Transparent); + } + + worker.ReportProgress((int)((i + 1) * 100.0) / totalCount, $"已处理 {i + 1}/{totalCount}"); + } + + // 导出单个 + if (args.ExportSingle) + { tex.Display(); - spine.CurrentAnimation = tmp; + + var filename = $"{DateTime.Now:yyMMddHHmmss}{args.NameSuffix}{args.FileSuffix}"; + var savePath = Path.Combine(args.OutputDir, filename); try { using (var img = new Bitmap(tex.Texture.CopyToBitmap())) { - img.SetResolution(dpi.Width, dpi.Height); - img.Save(savePath, imageFormat); + img.SetResolution(args.DPI.Width, args.DPI.Height); + img.Save(savePath, args.ImageFormat); } - success++; } catch (Exception ex) { Program.Logger.Error(ex.ToString()); - Program.Logger.Error("Failed to save preview {}", spine.SkelPath); - error++; + Program.Logger.Error("Failed to save single frame"); } - - worker.ReportProgress((int)((i + 1) * 100.0) / totalCount, $"已处理 {i + 1}/{totalCount}"); + } + else + { + if (error > 0) + Program.Logger.Warn("Frames save {} successfully, {} failed", success, error); + else + Program.Logger.Info("{} frames saved successfully", success); } } - spinePreviewer.StartPreview(); - - if (error > 0) - { - Program.Logger.Warn("Preview save {} successfully, {} failed", success, error); - } - else - { - Program.Logger.Info("{} preview saved successfully", success); - } + spinePreviewer.StartRender(); Program.LogCurrentMemoryUsage(); } diff --git a/SpineViewer/Spine/Spine.cs b/SpineViewer/Spine/Spine.cs index 436ac60..fd881f5 100644 --- a/SpineViewer/Spine/Spine.cs +++ b/SpineViewer/Spine/Spine.cs @@ -14,6 +14,7 @@ using System.Diagnostics.CodeAnalysis; using System.Globalization; using System.Text.Json.Nodes; using System.Collections.Immutable; +using SpineViewer.ExportHelper; namespace SpineViewer.Spine { @@ -346,6 +347,26 @@ namespace SpineViewer.Spine [Browsable(false)] public abstract RectangleF Bounds { get; } + /// + /// 初始状态下的骨骼包围盒 + /// + [Browsable(false)] + public RectangleF InitBounds + { + get + { + if (initBounds is null) + { + var tmp = CurrentAnimation; + CurrentAnimation = EMPTY_ANIMATION; + initBounds = Bounds; + CurrentAnimation = tmp; + } + return (RectangleF)initBounds; + } + } + private RectangleF? initBounds = null; + /// /// 骨骼预览图 /// @@ -362,7 +383,7 @@ namespace SpineViewer.Spine // 除此之外, 似乎还和 tex 的 Dispose 有关 // 如果不对 tex 进行 Dispose, 那么不管是否 Draw 都正常不会死锁 var tex = new SFML.Graphics.RenderTexture(PREVIEW_WIDTH, PREVIEW_HEIGHT); - tex.SetView(GetInitView(PREVIEW_WIDTH, PREVIEW_HEIGHT)); + tex.SetView(InitBounds.GetView(PREVIEW_WIDTH, PREVIEW_HEIGHT)); tex.Clear(SFML.Graphics.Color.Transparent); var tmp = CurrentAnimation; CurrentAnimation = EMPTY_ANIMATION; @@ -391,53 +412,6 @@ namespace SpineViewer.Spine /// 时间间隔 public abstract void Update(float delta); - /// - /// 获取初始状态下合适的 View, 参数单位为像素 - /// - public SFML.Graphics.View GetInitView(Size resolution, Padding padding) => - GetInitView((uint)resolution.Width, (uint)resolution.Height, (uint)padding.Left, (uint)padding.Right, (uint)padding.Top, (uint)padding.Bottom); - - /// - /// 获取初始状态下合适的 View, 参数单位为像素 - /// - public SFML.Graphics.View GetInitView(uint width, uint height, Padding padding) => - GetInitView(width, height, (uint)padding.Left, (uint)padding.Right, (uint)padding.Top, (uint)padding.Bottom); - - /// - /// 获取初始状态下合适的 View, 参数单位为像素 - /// - public SFML.Graphics.View GetInitView(Size resolution, uint paddingL = 1, uint paddingR = 1, uint paddingT = 1, uint paddingB = 1) => - GetInitView((uint)resolution.Width, (uint)resolution.Height, paddingL, paddingR, paddingT, paddingB); - - /// - /// 获取初始状态下合适的 View, 参数单位为像素 - /// - public SFML.Graphics.View GetInitView(uint width, uint height, uint paddingL = 1, uint paddingR = 1, uint paddingT = 1, uint paddingB = 1) - { - var tmp = CurrentAnimation; - CurrentAnimation = EMPTY_ANIMATION; - var bounds = Bounds; - CurrentAnimation = tmp; - - float sizeX = bounds.Width; - float sizeY = bounds.Height; - float innerW = width - paddingL - paddingR; - float innerH = height - paddingT - paddingB; - - float scale = 1; - if ((sizeY / sizeX) < (innerH / innerW)) - scale = sizeX / innerW; // 相同的 X, 视窗 Y 更大 - else - scale = sizeY / innerH; // 相同的 Y, 视窗 X 更大 - - var x = bounds.X + bounds.Width / 2 + ((float)paddingL - (float)paddingR) * scale; - var y = bounds.Y + bounds.Height / 2 + ((float)paddingT - (float)paddingB) * scale; - var viewX = width * scale; - var viewY = height * scale; - - return new(new(x, y), new(viewX, -viewY)); - } - /// /// 是否被选中 ///