Compare commits

..

18 Commits

Author SHA1 Message Date
ww-rm
0ccb110e36 fix bug 2025-03-24 00:20:53 +08:00
ww-rm
2c238dca9b 更新至v0.10.7 2025-03-24 00:17:02 +08:00
ww-rm
3e0aa53fca update changelog 2025-03-24 00:16:33 +08:00
ww-rm
12b4e44296 增加仅导出选中 2025-03-24 00:14:43 +08:00
ww-rm
9a2cf4aefe 增加region注释 2025-03-24 00:10:32 +08:00
ww-rm
0e2a116e0a 增加显示包围盒调试 2025-03-24 00:05:11 +08:00
ww-rm
7bf30eb54a 增加仅渲染选中模式 2025-03-24 00:01:28 +08:00
ww-rm
8dda8c8ff3 增加动画交集选择 2025-03-23 10:58:57 +08:00
ww-rm
988fdb22be 增加重载 2025-03-23 10:55:28 +08:00
ww-rm
1dd2c8fb4d fix bug 2025-03-23 10:19:50 +08:00
ww-rm
2b39384b28 重构以及增加注释 2025-03-23 01:34:44 +08:00
ww-rm
28d1275023 增加公开属性 2025-03-22 23:11:13 +08:00
ww-rm
979181fc3b small change 2025-03-22 21:12:47 +08:00
ww-rm
b374b88ad5 add writeskins 2025-03-22 21:08:14 +08:00
ww-rm
6643c19a20 remove useless member 2025-03-22 16:07:04 +08:00
ww-rm
7460874c81 增加注释 2025-03-22 12:38:17 +08:00
ww-rm
13dd7511f6 优化预览图获取 2025-03-21 20:35:39 +08:00
ww-rm
f153d251c8 增加预览图留白 2025-03-21 20:09:08 +08:00
26 changed files with 1014 additions and 425 deletions

View File

@@ -1,5 +1,10 @@
# CHANGELOG # CHANGELOG
## v0.10.7
- 增加仅导出选中
- 增加模型调试属性
## v0.10.6 ## v0.10.6
- 增加文件夹检测 - 增加文件夹检测

View File

@@ -17,16 +17,19 @@ namespace SpineViewer.Controls
{ {
public partial class SpineListView : UserControl public partial class SpineListView : UserControl
{ {
/// <summary>
/// 显示骨骼信息的属性面板
/// </summary>
[Category("自定义"), Description("用于显示骨骼属性的属性页")] [Category("自定义"), Description("用于显示骨骼属性的属性页")]
public PropertyGrid? PropertyGrid { get; set; } public PropertyGrid? PropertyGrid { get; set; }
/// <summary> /// <summary>
/// 获取数组快照, 访问时必须使用 lock 语句锁定对象本身 /// Spine 列表只读视图, 访问时必须使用 lock 语句锁定视图本身
/// </summary> /// </summary>
public readonly ReadOnlyCollection<Spine.Spine> Spines; public readonly ReadOnlyCollection<Spine.Spine> Spines;
/// <summary> /// <summary>
/// Spine 列表, 访问时必须使用 lock 语句锁定 Spines /// Spine 列表, 访问时必须使用 lock 语句锁定只读视图 Spines
/// </summary> /// </summary>
private readonly List<Spine.Spine> spines = []; private readonly List<Spine.Spine> spines = [];
@@ -37,16 +40,60 @@ namespace SpineViewer.Controls
} }
/// <summary> /// <summary>
/// listView.SelectedIndices /// 选中的索引
/// </summary> /// </summary>
public ListView.SelectedIndexCollection SelectedIndices { get => listView.SelectedIndices; } public ListView.SelectedIndexCollection SelectedIndices => listView.SelectedIndices;
/// <summary> /// <summary>
/// 弹出添加对话框 /// 弹出添加对话框在末尾添加
/// </summary> /// </summary>
public void Add() public void Add() => Insert();
/// <summary>
/// 弹出添加对话框在指定位置之前插入一项, 如果索引无效则在末尾添加
/// </summary>
private void Insert(int index = -1)
{ {
Insert(); var dialog = new Dialogs.OpenSpineDialog();
if (dialog.ShowDialog() != DialogResult.OK)
return;
Insert(dialog.Result, index);
}
/// <summary>
/// 从结果在指定位置之前插入一项, 如果索引无效则在末尾添加
/// </summary>
private void Insert(Dialogs.OpenSpineDialogResult result, int index = -1)
{
try
{
var spine = Spine.Spine.New(result.Version, result.SkelPath, result.AtlasPath);
// 如果索引无效则在末尾添加
if (index < 0 || index > listView.Items.Count)
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);
}
listView.Items.Insert(index, new ListViewItem(spine.Name, spine.ID) { ToolTipText = spine.SkelPath });
// 选中新增项
listView.SelectedIndices.Clear();
listView.SelectedIndices.Add(index);
}
catch (Exception ex)
{
Program.Logger.Error(ex.ToString());
Program.Logger.Error("Failed to load {} {}", result.SkelPath, result.AtlasPath);
MessageBox.Error(ex.ToString(), "骨骼加载失败");
}
Program.LogCurrentMemoryUsage();
} }
/// <summary> /// <summary>
@@ -57,18 +104,123 @@ namespace SpineViewer.Controls
var openDialog = new Dialogs.BatchOpenSpineDialog(); var openDialog = new Dialogs.BatchOpenSpineDialog();
if (openDialog.ShowDialog() != DialogResult.OK) if (openDialog.ShowDialog() != DialogResult.OK)
return; return;
BatchAdd(openDialog.Result);
}
/// <summary>
/// 从结果批量添加
/// </summary>
public void BatchAdd(Dialogs.BatchOpenSpineDialogResult result)
{
var progressDialog = new Dialogs.ProgressDialog(); var progressDialog = new Dialogs.ProgressDialog();
progressDialog.DoWork += BatchAdd_Work; progressDialog.DoWork += BatchAdd_Work;
progressDialog.RunWorkerAsync(openDialog.Result); progressDialog.RunWorkerAsync(result);
progressDialog.ShowDialog(); progressDialog.ShowDialog();
} }
/// <summary>
/// 批量添加后台任务
/// </summary>
private void BatchAdd_Work(object? sender, DoWorkEventArgs e)
{
var worker = sender as BackgroundWorker;
var arguments = e.Argument as Dialogs.BatchOpenSpineDialogResult;
var skelPaths = arguments.SkelPaths;
var version = arguments.Version;
int totalCount = skelPaths.Length;
int success = 0;
int error = 0;
worker.ReportProgress(0, $"已处理 0/{totalCount}");
for (int i = 0; i < totalCount; i++)
{
if (worker.CancellationPending)
{
e.Cancel = true;
break;
}
var skelPath = skelPaths[i];
try
{
var spine = Spine.Spine.New(version, skelPath);
var preview = spine.Preview;
lock (Spines) { spines.Add(spine); }
listView.Invoke(() =>
{
listView.SmallImageList.Images.Add(spine.ID, preview);
listView.LargeImageList.Images.Add(spine.ID, preview);
listView.Items.Add(new ListViewItem(spine.Name, spine.ID) { ToolTipText = spine.SkelPath });
});
success++;
}
catch (Exception ex)
{
Program.Logger.Error(ex.ToString());
Program.Logger.Error("Failed to load {}", skelPath);
error++;
}
worker.ReportProgress((int)((i + 1) * 100.0) / totalCount, $"已处理 {i + 1}/{totalCount}");
}
if (error > 0)
{
Program.Logger.Warn("Batch load {} successfully, {} failed", success, error);
}
else
{
Program.Logger.Info("{} skel loaded successfully", success);
}
Program.LogCurrentMemoryUsage();
}
/// <summary>
/// 从拖放/复制的路径列表添加
/// </summary>
private void AddFromFileDrop(IEnumerable<string> paths)
{
List<string> validPaths = [];
foreach (var path in paths)
{
if (File.Exists(path))
{
if (Spine.Spine.CommonSkelSuffix.Contains(Path.GetExtension(path).ToLower()))
validPaths.Add(path);
}
else if (Directory.Exists(path))
{
foreach (var file in Directory.EnumerateFiles(path, "*.*", SearchOption.AllDirectories))
{
if (Spine.Spine.CommonSkelSuffix.Contains(Path.GetExtension(file).ToLower()))
validPaths.Add(file);
}
}
}
if (validPaths.Count > 1)
{
if (validPaths.Count > 100)
{
if (MessageBox.Quest($"共发现 {validPaths.Count} 个可加载骨骼,数量较多,是否一次性全部加载?") == DialogResult.Cancel)
return;
}
BatchAdd(new Dialogs.BatchOpenSpineDialogResult(Spine.Version.Auto, validPaths.ToArray()));
}
else if (validPaths.Count > 0)
{
Insert(new Dialogs.OpenSpineDialogResult(Spine.Version.Auto, validPaths[0]));
}
}
private void listView_SelectedIndexChanged(object sender, EventArgs e) private void listView_SelectedIndexChanged(object sender, EventArgs e)
{ {
if (PropertyGrid is not null) lock (Spines)
{ {
lock (Spines) if (PropertyGrid is not null)
{ {
if (listView.SelectedIndices.Count <= 0) if (listView.SelectedIndices.Count <= 0)
PropertyGrid.SelectedObject = null; PropertyGrid.SelectedObject = null;
@@ -76,11 +228,11 @@ namespace SpineViewer.Controls
PropertyGrid.SelectedObject = spines[listView.SelectedIndices[0]]; PropertyGrid.SelectedObject = spines[listView.SelectedIndices[0]];
else else
PropertyGrid.SelectedObjects = listView.SelectedIndices.Cast<int>().Select(index => spines[index]).ToArray(); PropertyGrid.SelectedObjects = listView.SelectedIndices.Cast<int>().Select(index => spines[index]).ToArray();
// 标记选中的 Spine
for (int i = 0; i < spines.Count; i++)
spines[i].IsSelected = listView.SelectedIndices.Contains(i);
} }
// 标记选中的 Spine
for (int i = 0; i < spines.Count; i++)
spines[i].IsSelected = listView.SelectedIndices.Contains(i);
} }
// XXX: 图标显示的时候没法自动刷新顺序, 只能切换视图刷新, 不知道什么原理 // XXX: 图标显示的时候没法自动刷新顺序, 只能切换视图刷新, 不知道什么原理
@@ -227,12 +379,13 @@ namespace SpineViewer.Controls
if (listView.SelectedIndices.Count > 1) if (listView.SelectedIndices.Count > 1)
{ {
if (MessageBox.Show($"确定移除所选 {listView.SelectedIndices.Count} 项吗?", "操作确认", MessageBoxButtons.OKCancel, MessageBoxIcon.Question) != DialogResult.OK) if (MessageBox.Quest($"确定移除所选 {listView.SelectedIndices.Count} 项吗?") != DialogResult.OK)
return; return;
} }
foreach (var i in listView.SelectedIndices.Cast<int>().OrderByDescending(x => x)) foreach (var i in listView.SelectedIndices.Cast<int>().OrderByDescending(x => x))
{ {
listView.Items.RemoveAt(i);
lock (Spines) lock (Spines)
{ {
var spine = spines[i]; var spine = spines[i];
@@ -241,7 +394,6 @@ namespace SpineViewer.Controls
listView.LargeImageList.Images.RemoveByKey(spine.ID); listView.LargeImageList.Images.RemoveByKey(spine.ID);
spine.Dispose(); spine.Dispose();
} }
listView.Items.RemoveAt(i);
} }
} }
@@ -324,18 +476,17 @@ namespace SpineViewer.Controls
if (listView.Items.Count <= 0) if (listView.Items.Count <= 0)
return; return;
if (MessageBox.Show($"确认移除所有 {listView.Items.Count} 项吗?", "操作确认", MessageBoxButtons.OKCancel, MessageBoxIcon.Question) != DialogResult.OK) if (MessageBox.Quest($"确认移除所有 {listView.Items.Count} 项吗?") != DialogResult.OK)
return; return;
listView.Items.Clear();
lock (Spines) lock (Spines)
{ {
foreach (var spine in spines) foreach (var spine in spines) spine.Dispose();
spine.Dispose();
spines.Clear(); spines.Clear();
listView.SmallImageList.Images.Clear(); listView.SmallImageList.Images.Clear();
listView.LargeImageList.Images.Clear(); listView.LargeImageList.Images.Clear();
} }
listView.Items.Clear();
if (PropertyGrid is not null) if (PropertyGrid is not null)
PropertyGrid.SelectedObject = null; PropertyGrid.SelectedObject = null;
} }
@@ -393,145 +544,5 @@ namespace SpineViewer.Controls
{ {
listView.View = View.Details; listView.View = View.Details;
} }
/// <summary>
/// 弹出添加对话框在指定位置之前插入一项
/// </summary>
private void Insert(int index = -1)
{
var dialog = new Dialogs.OpenSpineDialog();
if (dialog.ShowDialog() != DialogResult.OK)
return;
Insert(dialog.Result, index);
}
private void Insert(Dialogs.OpenSpineDialogResult result, int index = -1)
{
try
{
var spine = Spine.Spine.New(result.Version, result.SkelPath, result.AtlasPath);
// 如果索引无效则在末尾添加
if (index < 0 || index > listView.Items.Count)
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);
}
listView.Items.Insert(index, new ListViewItem(spine.Name, spine.ID) { ToolTipText = spine.SkelPath });
// 选中新增项
listView.SelectedIndices.Clear();
listView.SelectedIndices.Add(index);
}
catch (Exception ex)
{
Program.Logger.Error(ex.ToString());
Program.Logger.Error("Failed to load {} {}", result.SkelPath, result.AtlasPath);
MessageBox.Show(ex.ToString(), "骨骼加载失败", MessageBoxButtons.OK, MessageBoxIcon.Error);
}
Program.Logger.Info($"Current memory usage: {Program.Process.WorkingSet64 / 1024.0 / 1024.0:F2} MB");
}
private void BatchAdd_Work(object? sender, DoWorkEventArgs e)
{
var worker = sender as BackgroundWorker;
var arguments = e.Argument as Dialogs.BatchOpenSpineDialogResult;
var skelPaths = arguments.SkelPaths;
var version = arguments.Version;
int totalCount = skelPaths.Length;
int success = 0;
int error = 0;
worker.ReportProgress(0, $"已处理 0/{totalCount}");
for (int i = 0; i < totalCount; i++)
{
if (worker.CancellationPending)
{
e.Cancel = true;
break;
}
var skelPath = skelPaths[i];
try
{
var spine = Spine.Spine.New(version, skelPath);
var preview = spine.Preview;
lock (Spines) { spines.Add(spine); }
listView.Invoke(() =>
{
listView.SmallImageList.Images.Add(spine.ID, preview);
listView.LargeImageList.Images.Add(spine.ID, preview);
listView.Items.Add(new ListViewItem(spine.Name, spine.ID) { ToolTipText = spine.SkelPath });
});
success++;
}
catch (Exception ex)
{
Program.Logger.Error(ex.ToString());
Program.Logger.Error("Failed to load {}", skelPath);
error++;
}
worker.ReportProgress((int)((i + 1) * 100.0) / totalCount, $"已处理 {i + 1}/{totalCount}");
}
if (error > 0)
{
Program.Logger.Warn("Batch load {} successfully, {} failed", success, error);
}
else
{
Program.Logger.Info("{} skel loaded successfully", success);
}
Program.Logger.Info($"Current memory usage: {Program.Process.WorkingSet64 / 1024.0 / 1024.0:F2} MB");
}
private void AddFromFileDrop(string[] paths)
{
List<string> validPaths = [];
foreach (var path in paths)
{
if (File.Exists(path))
{
if (Spine.Spine.CommonSkelSuffix.Contains(Path.GetExtension(path).ToLower()))
validPaths.Add(path);
}
else if (Directory.Exists(path))
{
foreach (var file in Directory.EnumerateFiles(path, "*.*", SearchOption.AllDirectories))
{
if (Spine.Spine.CommonSkelSuffix.Contains(Path.GetExtension(file).ToLower()))
validPaths.Add(file);
}
}
}
if (validPaths.Count > 1)
{
if (validPaths.Count > 100)
{
if (MessageBox.Show($"共发现 {validPaths.Count} 个可加载骨骼,数量较大,是否一次性全部加载?", "操作确认", MessageBoxButtons.OKCancel, MessageBoxIcon.Question) == DialogResult.Cancel)
return;
}
var progressDialog = new Dialogs.ProgressDialog();
progressDialog.DoWork += BatchAdd_Work;
progressDialog.RunWorkerAsync(new Dialogs.BatchOpenSpineDialogResult(Spine.Version.Auto, validPaths.ToArray()));
progressDialog.ShowDialog();
}
else if (validPaths.Count > 0)
{
Insert(new Dialogs.OpenSpineDialogResult(Spine.Version.Auto, validPaths[0]));
}
}
} }
} }

View File

@@ -16,14 +16,10 @@ namespace SpineViewer.Controls
public partial class SpinePreviewer : UserControl public partial class SpinePreviewer : UserControl
{ {
/// <summary> /// <summary>
/// 包装类, 用于 PropertyGrid 显示 /// 包装类, 用于属性面板显示
/// </summary> /// </summary>
private class PreviewerProperty private class PreviewerProperty(SpinePreviewer previewer)
{ {
private readonly SpinePreviewer previewer;
public PreviewerProperty(SpinePreviewer previewer) { this.previewer = previewer; }
[TypeConverter(typeof(SizeConverter))] [TypeConverter(typeof(SizeConverter))]
[Category("导出"), DisplayName("分辨率")] [Category("导出"), DisplayName("分辨率")]
public Size Resolution { get => previewer.Resolution; set => previewer.Resolution = value; } public Size Resolution { get => previewer.Resolution; set => previewer.Resolution = value; }
@@ -44,6 +40,9 @@ namespace SpineViewer.Controls
[Category("导出"), DisplayName("垂直翻转")] [Category("导出"), DisplayName("垂直翻转")]
public bool FlipY { get => previewer.FlipY; set => previewer.FlipY = value; } public bool FlipY { get => previewer.FlipY; set => previewer.FlipY = value; }
[Category("导出"), DisplayName("仅渲染选中")]
public bool RenderSelectedOnly { get => previewer.RenderSelectedOnly; set => previewer.RenderSelectedOnly = value; }
[Category("预览"), DisplayName("显示坐标轴")] [Category("预览"), DisplayName("显示坐标轴")]
public bool ShowAxis { get => previewer.ShowAxis; set => previewer.ShowAxis = value; } public bool ShowAxis { get => previewer.ShowAxis; set => previewer.ShowAxis = value; }
@@ -51,9 +50,15 @@ namespace SpineViewer.Controls
public uint MaxFps { get => previewer.MaxFps; set => previewer.MaxFps = value; } public uint MaxFps { get => previewer.MaxFps; set => previewer.MaxFps = value; }
} }
/// <summary>
/// 要绑定的 Spine 列表控件
/// </summary>
[Category("自定义"), Description("相关联的 SpineListView")] [Category("自定义"), Description("相关联的 SpineListView")]
public SpineListView? SpineListView { get; set; } public SpineListView? SpineListView { get; set; }
/// <summary>
/// 属性信息面板
/// </summary>
[Category("自定义"), Description("用于显示画面属性的属性页")] [Category("自定义"), Description("用于显示画面属性的属性页")]
public PropertyGrid? PropertyGrid public PropertyGrid? PropertyGrid
{ {
@@ -67,21 +72,49 @@ namespace SpineViewer.Controls
} }
private PropertyGrid? propertyGrid; private PropertyGrid? propertyGrid;
/// <summary>
/// 画面缩放最大值
/// </summary>
public const float ZOOM_MAX = 1000f; public const float ZOOM_MAX = 1000f;
/// <summary>
/// 画面缩放最小值
/// </summary>
public const float ZOOM_MIN = 0.001f; public const float ZOOM_MIN = 0.001f;
public const int BACKGROUND_CELL_SIZE = 10;
/// <summary>
/// 预览画面背景色
/// </summary>
private static readonly SFML.Graphics.Color BackgroundColor = new(105, 105, 105); private static readonly SFML.Graphics.Color BackgroundColor = new(105, 105, 105);
/// <summary>
/// 预览画面坐标轴颜色
/// </summary>
private static readonly SFML.Graphics.Color AxisColor = new(220, 220, 220); private static readonly SFML.Graphics.Color AxisColor = new(220, 220, 220);
private static readonly SFML.Graphics.Color BoundsColor = new(120, 200, 0);
/// <summary>
/// 坐标轴顶点缓冲区
/// </summary>
private readonly SFML.Graphics.VertexArray AxisVertex = new(SFML.Graphics.PrimitiveType.Lines, 2); private readonly SFML.Graphics.VertexArray AxisVertex = new(SFML.Graphics.PrimitiveType.Lines, 2);
private readonly SFML.Graphics.VertexArray BoundsRect = new(SFML.Graphics.PrimitiveType.LineStrip, 5);
/// <summary>
/// 渲染窗口
/// </summary>
private readonly SFML.Graphics.RenderWindow RenderWindow; private readonly SFML.Graphics.RenderWindow RenderWindow;
/// <summary>
/// 帧间隔计时器
/// </summary>
private readonly SFML.System.Clock Clock = new(); private readonly SFML.System.Clock Clock = new();
/// <summary>
/// 画面拖放对象世界坐标源点
/// </summary>
private SFML.System.Vector2f? draggingSrc = null; private SFML.System.Vector2f? draggingSrc = null;
/// <summary>
/// 渲染任务
/// </summary>
private Task? task = null; private Task? task = null;
private CancellationTokenSource? cancelToken = null; private CancellationTokenSource? cancelToken = null;
@@ -225,6 +258,13 @@ namespace SpineViewer.Controls
} }
} }
/// <summary>
/// 仅渲染选中
/// </summary>
[DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
[Browsable(false)]
public bool RenderSelectedOnly { get; set; } = false;
/// <summary> /// <summary>
/// 显示坐标轴 /// 显示坐标轴
/// </summary> /// </summary>
@@ -240,13 +280,6 @@ namespace SpineViewer.Controls
public uint MaxFps { get => maxFps; set { RenderWindow.SetFramerateLimit(value); maxFps = value; } } public uint MaxFps { get => maxFps; set { RenderWindow.SetFramerateLimit(value); maxFps = value; } }
private uint maxFps = 60; private uint maxFps = 60;
/// <summary>
/// RenderWindow.View
/// </summary>
[DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
[Browsable(false)]
public SFML.Graphics.View View { get => RenderWindow.GetView(); }
public SpinePreviewer() public SpinePreviewer()
{ {
InitializeComponent(); InitializeComponent();
@@ -260,6 +293,11 @@ namespace SpineViewer.Controls
MaxFps = 30; MaxFps = 30;
} }
/// <summary>
/// 预览画面帧参数
/// </summary>
public SpinePreviewerFrameArgs GetFrameArgs() => new(Resolution, RenderWindow.GetView(), RenderSelectedOnly);
/// <summary> /// <summary>
/// 开始预览 /// 开始预览
/// </summary> /// </summary>
@@ -284,6 +322,67 @@ namespace SpineViewer.Controls
task = null; task = null;
} }
/// <summary>
/// 渲染任务
/// </summary>
private void RenderTask()
{
try
{
RenderWindow.SetActive(true);
float delta;
while (cancelToken is not null && !cancelToken.IsCancellationRequested)
{
delta = Clock.ElapsedTime.AsSeconds();
Clock.Restart();
RenderWindow.Clear(BackgroundColor);
if (ShowAxis)
{
// 画一个很长的坐标轴, 用 1e9 比较合适
AxisVertex[0] = new(new(-1e9f, 0), AxisColor);
AxisVertex[1] = new(new(1e9f, 0), AxisColor);
RenderWindow.Draw(AxisVertex);
AxisVertex[0] = new(new(0, -1e9f), AxisColor);
AxisVertex[1] = new(new(0, 1e9f), AxisColor);
RenderWindow.Draw(AxisVertex);
}
// 渲染 Spine
if (SpineListView is not null)
{
lock (SpineListView.Spines)
{
var spines = SpineListView.Spines;
for (int i = spines.Count - 1; i >= 0; i--)
{
if (cancelToken is not null && cancelToken.IsCancellationRequested)
break; // 提前中止
var spine = spines[i];
spine.Update(delta);
if (RenderSelectedOnly && !spine.IsSelected)
continue;
spine.IsDebug = true;
RenderWindow.Draw(spine);
spine.IsDebug = false;
}
}
}
RenderWindow.Display();
}
}
finally
{
RenderWindow.SetActive(false);
}
}
private void SpinePreviewer_SizeChanged(object sender, EventArgs e) private void SpinePreviewer_SizeChanged(object sender, EventArgs e)
{ {
if (RenderWindow is null) if (RenderWindow is null)
@@ -326,45 +425,60 @@ namespace SpineViewer.Controls
draggingSrc = RenderWindow.MapPixelToCoords(new(e.X, e.Y)); draggingSrc = RenderWindow.MapPixelToCoords(new(e.X, e.Y));
var src = new PointF(((SFML.System.Vector2f)draggingSrc).X, ((SFML.System.Vector2f)draggingSrc).Y); var src = new PointF(((SFML.System.Vector2f)draggingSrc).X, ((SFML.System.Vector2f)draggingSrc).Y);
if (SpineListView is not null) if (SpineListView is null)
{ return;
lock (SpineListView.Spines)
{
var spines = SpineListView.Spines;
lock (SpineListView.Spines)
{
var spines = SpineListView.Spines;
// 仅渲染选中模式禁止在画面里选择对象
if (RenderSelectedOnly)
{
bool hit = false;
foreach (int i in SpineListView.SelectedIndices)
{
if (!spines[i].Bounds.Contains(src)) continue;
hit = true;
break;
}
// 如果没点到被选中的模型, 则不允许拖动
if (!hit) draggingSrc = null;
}
else
{
// 没有按下 Ctrl 键就只选中点击的那个, 所以先清空选中列表 // 没有按下 Ctrl 键就只选中点击的那个, 所以先清空选中列表
if ((ModifierKeys & Keys.Control) == 0) if ((ModifierKeys & Keys.Control) == 0)
{ {
bool hit = false; bool hit = false;
for (int i = 0; i < spines.Count; i++) for (int i = 0; i < spines.Count; i++)
{ {
if (spines[i].Bounds.Contains(src)) if (!spines[i].Bounds.Contains(src)) continue;
{
hit = true;
// 如果点到了没被选中的东西, 则清空原先选中的, 改为只选中这一次点的 hit = true;
if (!SpineListView.SelectedIndices.Contains(i))
{ // 如果点到了没被选中的东西, 则清空原先选中的, 改为只选中这一次点的
SpineListView.SelectedIndices.Clear(); if (!SpineListView.SelectedIndices.Contains(i))
SpineListView.SelectedIndices.Add(i); {
} SpineListView.SelectedIndices.Clear();
break; SpineListView.SelectedIndices.Add(i);
} }
break;
} }
// 如果点了空白的地方, 就清空选中列表 // 如果点了空白的地方, 就清空选中列表
if (!hit) if (!hit) SpineListView.SelectedIndices.Clear();
SpineListView.SelectedIndices.Clear();
} }
else else
{ {
for (int i = 0; i < spines.Count; i++) for (int i = 0; i < spines.Count; i++)
{ {
if (spines[i].Bounds.Contains(src)) if (!spines[i].Bounds.Contains(src))
{ continue;
SpineListView.SelectedIndices.Add(i);
break; SpineListView.SelectedIndices.Add(i);
} break;
} }
} }
} }
@@ -392,11 +506,8 @@ namespace SpineViewer.Controls
{ {
lock (SpineListView.Spines) lock (SpineListView.Spines)
{ {
foreach (var spine in SpineListView.Spines) foreach (int i in SpineListView.SelectedIndices)
{ SpineListView.Spines[i].Position += delta;
if (spine.IsSelected)
spine.Position += delta;
}
} }
} }
draggingSrc = dst; draggingSrc = dst;
@@ -427,68 +538,27 @@ namespace SpineViewer.Controls
Zoom *= (e.Delta > 0 ? 1.1f : 0.9f); Zoom *= (e.Delta > 0 ? 1.1f : 0.9f);
PropertyGrid?.Refresh(); PropertyGrid?.Refresh();
} }
}
private void RenderTask() /// <summary>
{ /// 预览画面帧参数
try /// </summary>
{ public class SpinePreviewerFrameArgs(Size resolution, SFML.Graphics.View view, bool renderSelectedOnly)
RenderWindow.SetActive(true); {
/// <summary>
/// 分辨率
/// </summary>
public Size Resolution => resolution;
float delta; /// <summary>
while (cancelToken is not null && !cancelToken.IsCancellationRequested) /// 渲染视窗
{ /// </summary>
delta = Clock.ElapsedTime.AsSeconds(); public SFML.Graphics.View View => view;
Clock.Restart();
RenderWindow.Clear(BackgroundColor); /// <summary>
/// 是否仅渲染/导出选中骨骼
if (ShowAxis) /// </summary>
{ public bool RenderSelectedOnly => renderSelectedOnly;
// 画一个很长的坐标轴, 用 1e9 比较合适
AxisVertex[0] = new(new(-1e9f, 0), AxisColor);
AxisVertex[1] = new(new(1e9f, 0), AxisColor);
RenderWindow.Draw(AxisVertex);
AxisVertex[0] = new(new(0, -1e9f), AxisColor);
AxisVertex[1] = new(new(0, 1e9f), AxisColor);
RenderWindow.Draw(AxisVertex);
}
// 渲染 Spine
if (SpineListView is not null)
{
lock (SpineListView.Spines)
{
var spines = SpineListView.Spines;
for (int i = spines.Count - 1; i >= 0; i--)
{
if (cancelToken is not null && cancelToken.IsCancellationRequested)
break; // 提前中止
var spine = spines[i];
spine.Update(delta);
RenderWindow.Draw(spine);
if (spine.IsSelected)
{
var bounds = spine.Bounds;
BoundsRect[0] = BoundsRect[4] = new(new(bounds.Left, bounds.Top), BoundsColor);
BoundsRect[1] = new(new(bounds.Right, bounds.Top), BoundsColor);
BoundsRect[2] = new(new(bounds.Right, bounds.Bottom), BoundsColor);
BoundsRect[3] = new(new(bounds.Left, bounds.Bottom), BoundsColor);
RenderWindow.Draw(BoundsRect);
}
}
}
}
RenderWindow.Display();
}
}
finally
{
RenderWindow.SetActive(false);
}
}
} }
} }

View File

@@ -15,16 +15,12 @@ namespace SpineViewer.Dialogs
public AboutDialog() public AboutDialog()
{ {
InitializeComponent(); InitializeComponent();
this.label_Version.Text = $"v{InformationalVersion}"; Text = $"关于 {Program.Name}";
label_Version.Text = $"v{InformationalVersion}";
} }
public string InformationalVersion public string InformationalVersion =>
{ Assembly.GetExecutingAssembly().GetCustomAttribute<AssemblyInformationalVersionAttribute>()?.InformationalVersion;
get
{
return Assembly.GetExecutingAssembly().GetCustomAttribute<AssemblyInformationalVersionAttribute>()?.InformationalVersion;
}
}
private void linkLabel_RepoUrl_LinkClicked(object sender, LinkLabelLinkClickedEventArgs e) private void linkLabel_RepoUrl_LinkClicked(object sender, LinkLabelLinkClickedEventArgs e)
{ {
@@ -36,7 +32,7 @@ namespace SpineViewer.Dialogs
else else
{ {
Clipboard.SetText(url); Clipboard.SetText(url);
MessageBox.Show(this, "链接已复制到剪贴板,请前往浏览器进行访问", "提示信息", MessageBoxButtons.OK, MessageBoxIcon.Information); MessageBox.Info("链接已复制到剪贴板,请前往浏览器进行访问");
} }
} }
} }

View File

@@ -13,6 +13,9 @@ namespace SpineViewer.Dialogs
{ {
public partial class BatchOpenSpineDialog : Form public partial class BatchOpenSpineDialog : Form
{ {
/// <summary>
/// 对话框结果, 取消时为 null
/// </summary>
public BatchOpenSpineDialogResult Result { get; private set; } public BatchOpenSpineDialogResult Result { get; private set; }
public BatchOpenSpineDialog() public BatchOpenSpineDialog()
@@ -46,7 +49,7 @@ namespace SpineViewer.Dialogs
if (listBox_FilePath.Items.Count <= 0) if (listBox_FilePath.Items.Count <= 0)
{ {
MessageBox.Show("未选择任何文件", "错误信息", MessageBoxButtons.OK, MessageBoxIcon.Information); MessageBox.Info("未选择任何文件");
return; return;
} }
@@ -54,14 +57,14 @@ namespace SpineViewer.Dialogs
{ {
if (!File.Exists(p)) if (!File.Exists(p))
{ {
MessageBox.Show($"{p}", "skel文件不存在", MessageBoxButtons.OK, MessageBoxIcon.Information); MessageBox.Info($"{p}", "skel文件不存在");
return; return;
} }
} }
if (version != Spine.Version.Auto && !Spine.Spine.ImplementedVersions.Contains(version)) if (version != Spine.Version.Auto && !Spine.Spine.ImplementedVersions.Contains(version))
{ {
MessageBox.Show($"{version.GetName()} 版本尚未实现(咕咕咕~", "错误信息", MessageBoxButtons.OK, MessageBoxIcon.Information); MessageBox.Info($"{version.GetName()} 版本尚未实现(咕咕咕~");
return; return;
} }
@@ -75,9 +78,19 @@ namespace SpineViewer.Dialogs
} }
} }
/// <summary>
/// 批量打开对话框结果
/// </summary>
public class BatchOpenSpineDialogResult(Spine.Version version, string[] skelPaths) public class BatchOpenSpineDialogResult(Spine.Version version, string[] skelPaths)
{ {
public Spine.Version Version { get; } = version; /// <summary>
public string[] SkelPaths { get; } = skelPaths; /// 版本
/// </summary>
public Spine.Version Version => version;
/// <summary>
/// 路径列表
/// </summary>
public string[] SkelPaths => skelPaths;
} }
} }

View File

@@ -13,6 +13,8 @@ namespace SpineViewer.Dialogs
{ {
public partial class ConvertFileFormatDialog : Form public partial class ConvertFileFormatDialog : Form
{ {
// TODO: 增加版本转换选项
// TODO: 使用结果包装类
public string[] SkelPaths { get; private set; } public string[] SkelPaths { get; private set; }
public Spine.Version SourceVersion { get; private set; } public Spine.Version SourceVersion { get; private set; }
public Spine.Version TargetVersion { get; private set; } public Spine.Version TargetVersion { get; private set; }
@@ -62,7 +64,7 @@ namespace SpineViewer.Dialogs
if (listBox_FilePath.Items.Count <= 0) if (listBox_FilePath.Items.Count <= 0)
{ {
MessageBox.Show("未选择任何文件", "错误信息", MessageBoxButtons.OK, MessageBoxIcon.Information); MessageBox.Info("未选择任何文件");
return; return;
} }
@@ -70,26 +72,26 @@ namespace SpineViewer.Dialogs
{ {
if (!File.Exists(p)) if (!File.Exists(p))
{ {
MessageBox.Show($"{p}", "skel文件不存在", MessageBoxButtons.OK, MessageBoxIcon.Information); MessageBox.Info($"{p}", "skel文件不存在");
return; return;
} }
} }
if (!SkeletonConverter.ImplementedVersions.Contains(sourceVersion)) if (!SkeletonConverter.ImplementedVersions.Contains(sourceVersion))
{ {
MessageBox.Show($"{sourceVersion.GetName()} 版本尚未实现(咕咕咕~", "错误信息", MessageBoxButtons.OK, MessageBoxIcon.Information); MessageBox.Info($"{sourceVersion.GetName()} 版本尚未实现(咕咕咕~");
return; return;
} }
if (!SkeletonConverter.ImplementedVersions.Contains(targetVersion)) if (!SkeletonConverter.ImplementedVersions.Contains(targetVersion))
{ {
MessageBox.Show($"{targetVersion.GetName()} 版本尚未实现(咕咕咕~", "错误信息", MessageBoxButtons.OK, MessageBoxIcon.Information); MessageBox.Info($"{targetVersion.GetName()} 版本尚未实现(咕咕咕~");
return; return;
} }
if (jsonSource == jsonTarget && sourceVersion == targetVersion) if (jsonSource == jsonTarget && sourceVersion == targetVersion)
{ {
MessageBox.Show($"不需要转换相同的格式和版本", "错误信息", MessageBoxButtons.OK, MessageBoxIcon.Information); MessageBox.Info($"不需要转换相同的格式和版本");
return; return;
} }

View File

@@ -82,11 +82,11 @@ namespace SpineViewer.Dialogs
private void button_Copy_Click(object sender, EventArgs e) private void button_Copy_Click(object sender, EventArgs e)
{ {
var selectedObject = propertyGrid.SelectedObject as DiagnosticsInformation; var selectedObject = (DiagnosticsInformation)propertyGrid.SelectedObject;
var properties = selectedObject.GetType().GetProperties(); var properties = selectedObject.GetType().GetProperties();
var result = string.Join(Environment.NewLine, properties.Select(p => $"{p.Name}\t{p.GetValue(selectedObject)?.ToString()}")); var result = string.Join(Environment.NewLine, properties.Select(p => $"{p.Name}\t{p.GetValue(selectedObject)?.ToString()}"));
Clipboard.SetText(result); Clipboard.SetText(result);
MessageBox.Show(this, "已复制", "提示信息", MessageBoxButtons.OK, MessageBoxIcon.Information); MessageBox.Info("已复制");
} }
} }
} }

View File

@@ -12,6 +12,8 @@ namespace SpineViewer.Dialogs
{ {
public partial class ExportPngDialog : Form public partial class ExportPngDialog : Form
{ {
// TODO: 该对话框要合并到统一的导出参数对话框
// TODO: 使用结果包装类
public string OutputDir { get; private set; } public string OutputDir { get; private set; }
public float Duration { get; private set; } public float Duration { get; private set; }
public uint Fps { get; private set; } public uint Fps { get; private set; }
@@ -40,27 +42,23 @@ namespace SpineViewer.Dialogs
var outputDir = textBox_OutputDir.Text; var outputDir = textBox_OutputDir.Text;
if (File.Exists(outputDir)) if (File.Exists(outputDir))
{ {
MessageBox.Show("输出文件夹无效", "错误信息", MessageBoxButtons.OK, MessageBoxIcon.Information); MessageBox.Info("输出文件夹无效");
return; return;
} }
if (!Directory.Exists(outputDir)) if (!Directory.Exists(outputDir))
{ {
if (MessageBox.Show($"文件夹 {outputDir} 不存在,是否创建?", "操作确认", MessageBoxButtons.OKCancel, MessageBoxIcon.Question) == DialogResult.OK) if (MessageBox.Quest($"文件夹 {outputDir} 不存在,是否创建?") != DialogResult.OK)
return;
try
{ {
try Directory.CreateDirectory(outputDir);
{
Directory.CreateDirectory(outputDir);
}
catch (Exception ex)
{
Program.Logger.Error(ex.ToString());
MessageBox.Show(ex.ToString(), "文件夹创建失败", MessageBoxButtons.OK, MessageBoxIcon.Error);
return;
}
} }
else catch (Exception ex)
{ {
Program.Logger.Error(ex.ToString());
MessageBox.Error(ex.ToString(), "文件夹创建失败");
return; return;
} }
} }

View File

@@ -12,6 +12,7 @@ namespace SpineViewer.Dialogs
{ {
public partial class ExportPreviewDialog: Form public partial class ExportPreviewDialog: Form
{ {
// TODO: 用单独的结果包装类
public string OutputDir { get; private set; } public string OutputDir { get; private set; }
public uint PreviewWidth { get; private set; } public uint PreviewWidth { get; private set; }
public uint PreviewHeight { get; private set; } public uint PreviewHeight { get; private set; }
@@ -40,27 +41,23 @@ namespace SpineViewer.Dialogs
var outputDir = textBox_OutputDir.Text; var outputDir = textBox_OutputDir.Text;
if (File.Exists(outputDir)) if (File.Exists(outputDir))
{ {
MessageBox.Show("输出文件夹无效", "错误信息", MessageBoxButtons.OK, MessageBoxIcon.Information); MessageBox.Info("输出文件夹无效");
return; return;
} }
if (!Directory.Exists(outputDir)) if (!Directory.Exists(outputDir))
{ {
if (MessageBox.Show($"文件夹 {outputDir} 不存在,是否创建?", "操作确认", MessageBoxButtons.OKCancel, MessageBoxIcon.Question) == DialogResult.OK) if (MessageBox.Quest($"文件夹 {outputDir} 不存在,是否创建?") != DialogResult.OK)
return;
try
{ {
try Directory.CreateDirectory(outputDir);
{
Directory.CreateDirectory(outputDir);
}
catch (Exception ex)
{
Program.Logger.Error(ex.ToString());
MessageBox.Show(ex.ToString(), "文件夹创建失败", MessageBoxButtons.OK, MessageBoxIcon.Error);
return;
}
} }
else catch (Exception ex)
{ {
Program.Logger.Error(ex.ToString());
MessageBox.Error(ex.ToString(), "文件夹创建失败");
return; return;
} }
} }

View File

@@ -12,6 +12,9 @@ namespace SpineViewer.Dialogs
{ {
public partial class OpenSpineDialog : Form public partial class OpenSpineDialog : Form
{ {
/// <summary>
/// 对话框结果
/// </summary>
public OpenSpineDialogResult Result { get; private set; } public OpenSpineDialogResult Result { get; private set; }
public OpenSpineDialog() public OpenSpineDialog()
@@ -54,7 +57,7 @@ namespace SpineViewer.Dialogs
if (!File.Exists(skelPath)) if (!File.Exists(skelPath))
{ {
MessageBox.Show($"{skelPath}", "skel文件不存在", MessageBoxButtons.OK, MessageBoxIcon.Information); MessageBox.Info($"{skelPath}", "skel文件不存在");
return; return;
} }
else else
@@ -68,7 +71,7 @@ namespace SpineViewer.Dialogs
} }
else if (!File.Exists(atlasPath)) else if (!File.Exists(atlasPath))
{ {
MessageBox.Show($"{atlasPath}", "atlas文件不存在", MessageBoxButtons.OK, MessageBoxIcon.Information); MessageBox.Info($"{atlasPath}", "atlas文件不存在");
return; return;
} }
else else
@@ -78,7 +81,7 @@ namespace SpineViewer.Dialogs
if (version != Spine.Version.Auto && !Spine.Spine.ImplementedVersions.Contains(version)) if (version != Spine.Version.Auto && !Spine.Spine.ImplementedVersions.Contains(version))
{ {
MessageBox.Show($"{version.GetName()} 版本尚未实现(咕咕咕~", "错误信息", MessageBoxButtons.OK, MessageBoxIcon.Information); MessageBox.Info($"{version.GetName()} 版本尚未实现(咕咕咕~");
return; return;
} }
@@ -92,10 +95,24 @@ namespace SpineViewer.Dialogs
} }
} }
/// <summary>
/// 打开骨骼对话框结果
/// </summary>
public class OpenSpineDialogResult(Spine.Version version, string skelPath, string? atlasPath = null) public class OpenSpineDialogResult(Spine.Version version, string skelPath, string? atlasPath = null)
{ {
public Spine.Version Version { get; } = version; /// <summary>
public string SkelPath { get; } = skelPath; /// 版本
public string? AtlasPath { get; } = atlasPath; /// </summary>
public Spine.Version Version => version;
/// <summary>
/// skel 文件路径
/// </summary>
public string SkelPath => skelPath;
/// <summary>
/// atlas 文件路径
/// </summary>
public string? AtlasPath => atlasPath;
} }
} }

View File

@@ -12,15 +12,25 @@ namespace SpineViewer.Dialogs
{ {
public partial class ProgressDialog : Form public partial class ProgressDialog : Form
{ {
/// <summary>
/// BackgroundWorker.DoWork 接口暴露
/// </summary>
[Category("自定义"), Description("BackgroundWorker 的 DoWork 事件")] [Category("自定义"), Description("BackgroundWorker 的 DoWork 事件")]
public event DoWorkEventHandler? DoWork public event DoWorkEventHandler? DoWork
{ {
add { backgroundWorker.DoWork += value; } add => backgroundWorker.DoWork += value;
remove { backgroundWorker.DoWork -= value; } remove => backgroundWorker.DoWork -= value;
} }
public void RunWorkerAsync() { backgroundWorker.RunWorkerAsync(); } /// <summary>
public void RunWorkerAsync(object? argument) { backgroundWorker.RunWorkerAsync(argument); } /// 启动后台执行
/// </summary>
public void RunWorkerAsync() => backgroundWorker.RunWorkerAsync();
/// <summary>
/// 使用给定参数启动后台执行
/// </summary>
public void RunWorkerAsync(object? argument) => backgroundWorker.RunWorkerAsync(argument);
public ProgressDialog() public ProgressDialog()
{ {
@@ -38,7 +48,7 @@ namespace SpineViewer.Dialogs
if (e.Error != null) if (e.Error != null)
{ {
Program.Logger.Error(e.Error.ToString()); Program.Logger.Error(e.Error.ToString());
MessageBox.Show(e.Error.ToString(), "执行出错", MessageBoxButtons.OK, MessageBoxIcon.Error); MessageBox.Error(e.Error.ToString(), "执行出错");
DialogResult = DialogResult.Abort; DialogResult = DialogResult.Abort;
} }
else if (e.Cancelled) else if (e.Cancelled)

View File

@@ -1,9 +1,13 @@
using NLog; using FFMpegCore.Pipes;
using FFMpegCore;
using NLog;
using SFML.System;
using SpineViewer.Spine; using SpineViewer.Spine;
using System.ComponentModel; using System.ComponentModel;
using System.Diagnostics; using System.Diagnostics;
using System.Text.Json; using System.Text.Json;
using System.Text.Json.Nodes; using System.Text.Json.Nodes;
using FFMpegCore.Enums;
namespace SpineViewer namespace SpineViewer
{ {
@@ -65,11 +69,12 @@ namespace SpineViewer
private void toolStripMenuItem_Export_Click(object sender, EventArgs e) private void toolStripMenuItem_Export_Click(object sender, EventArgs e)
{ {
// TODO: 改成统一导出调用
lock (spineListView.Spines) lock (spineListView.Spines)
{ {
if (spineListView.Spines.Count <= 0) if (spineListView.Spines.Count <= 0)
{ {
MessageBox.Show("请至少打开一个骨骼文件", "提示信息", MessageBoxButtons.OK, MessageBoxIcon.Information); MessageBox.Info("请至少打开一个骨骼文件");
return; return;
} }
} }
@@ -90,7 +95,7 @@ namespace SpineViewer
{ {
if (spineListView.Spines.Count <= 0) if (spineListView.Spines.Count <= 0)
{ {
MessageBox.Show("请至少打开一个骨骼文件", "提示信息", MessageBoxButtons.OK, MessageBoxIcon.Information); MessageBox.Info("请至少打开一个骨骼文件");
return; return;
} }
} }
@@ -131,9 +136,70 @@ namespace SpineViewer
progressDialog.ShowDialog(); progressDialog.ShowDialog();
} }
//IEnumerable<IVideoFrame> testExport(int fps)
//{
// var duration = 2f;
// var resolution = spinePreviewer.Resolution;
// var delta = 1f / fps;
// var frameCount = 1 + (int)(duration / delta); // 零帧开始导出
// var spinesReverse = spineListView.Spines.Reverse();
// // 重置动画时间
// foreach (var spine in spinesReverse)
// spine.CurrentAnimation = spine.CurrentAnimation;
// // 逐帧导出
// var success = 0;
// for (int frameIndex = 0; frameIndex < frameCount; frameIndex++)
// {
// using var tex = new SFML.Graphics.RenderTexture((uint)resolution.Width, (uint)resolution.Height);
// tex.SetView(spinePreviewer.View);
// tex.Clear(SFML.Graphics.Color.Transparent);
// foreach (var spine in spinesReverse)
// {
// tex.Draw(spine);
// spine.Update(delta);
// }
// tex.Display();
// Debug.WriteLine($"ThreadID: {Environment.CurrentManagedThreadId}");
// var frame = tex.Texture.CopyToFrame();
// tex.Dispose();
// yield return frame;
// success++;
// }
// Program.Logger.Info("Exporting done: {}/{}", success, frameCount);
//}
private void toolStripMenuItem_ManageResource_Click(object sender, EventArgs e) private void toolStripMenuItem_ManageResource_Click(object sender, EventArgs e)
{ {
//spinePreviewer.StopPreview();
//lock (spineListView.Spines)
//{
// //var fps = 24;
// ////foreach (var i in testExport(fps))
// //// _ = i;
// ////var t = testExport(fps).ToArray();
// ////var a = testExport(fps).GetEnumerator();
// ////while (a.MoveNext());
// //var videoFramesSource = new RawVideoPipeSource(testExport(fps)) { FrameRate = fps };
// //var outputPath = @"C:\Users\ljh\Desktop\test\a.mov";
// //var task = FFMpegArguments
// // .FromPipeInput(videoFramesSource)
// // .OutputToFile(outputPath, true
// // , options => options
// // //.WithCustomArgument("-vf \"split[s0][s1];[s0]palettegen=reserve_transparent=1[p];[s1][p]paletteuse=alpha_threshold=128\""))
// // .WithCustomArgument("-c:v prores_ks -profile:v 4444 -pix_fmt yuva444p10le"))
// // .ProcessAsynchronously();
// //task.Wait();
//}
//spinePreviewer.StartPreview();
} }
private void toolStripMenuItem_About_Click(object sender, EventArgs e) private void toolStripMenuItem_About_Click(object sender, EventArgs e)
@@ -146,13 +212,16 @@ namespace SpineViewer
(new Dialogs.DiagnosticsDialog()).ShowDialog(); (new Dialogs.DiagnosticsDialog()).ShowDialog();
} }
private void splitContainer_SplitterMoved(object sender, SplitterEventArgs e) { ActiveControl = null; } private void splitContainer_SplitterMoved(object sender, SplitterEventArgs e) => ActiveControl = null;
private void splitContainer_MouseUp(object sender, MouseEventArgs e) { ActiveControl = null; } private void splitContainer_MouseUp(object sender, MouseEventArgs e) => ActiveControl = null;
private void propertyGrid_PropertyValueChanged(object sender, PropertyValueChangedEventArgs e) { (sender as PropertyGrid)?.Refresh(); } private void propertyGrid_PropertyValueChanged(object sender, PropertyValueChangedEventArgs e) => (sender as PropertyGrid)?.Refresh();
private void spinePreviewer_MouseUp(object sender, MouseEventArgs e) { propertyGrid_Spine.Refresh(); } private void spinePreviewer_MouseUp(object sender, MouseEventArgs e)
{
propertyGrid_Spine.Refresh();
}
private void ExportPng_Work(object? sender, DoWorkEventArgs e) private void ExportPng_Work(object? sender, DoWorkEventArgs e)
{ {
@@ -163,9 +232,12 @@ namespace SpineViewer
var fps = arguments.Fps; var fps = arguments.Fps;
var timestamp = DateTime.Now.ToString("yyMMddHHmmss"); var timestamp = DateTime.Now.ToString("yyMMddHHmmss");
var resolution = spinePreviewer.Resolution; var frameArgs = spinePreviewer.GetFrameArgs();
var renderSelectedOnly = spinePreviewer.RenderSelectedOnly;
var resolution = frameArgs.Resolution;
var tex = new SFML.Graphics.RenderTexture((uint)resolution.Width, (uint)resolution.Height); var tex = new SFML.Graphics.RenderTexture((uint)resolution.Width, (uint)resolution.Height);
tex.SetView(spinePreviewer.View); tex.SetView(frameArgs.View);
var delta = 1f / fps; var delta = 1f / fps;
var frameCount = 1 + (int)(duration / delta); // 零帧开始导出 var frameCount = 1 + (int)(duration / delta); // 零帧开始导出
@@ -196,6 +268,9 @@ namespace SpineViewer
foreach (var spine in spinesReverse) foreach (var spine in spinesReverse)
{ {
if (renderSelectedOnly && !spine.IsSelected)
continue;
tex.Draw(spine); tex.Draw(spine);
spine.Update(delta); spine.Update(delta);
} }
@@ -223,6 +298,13 @@ namespace SpineViewer
var outputDir = arguments.OutputDir; var outputDir = arguments.OutputDir;
var width = arguments.PreviewWidth; var width = arguments.PreviewWidth;
var height = arguments.PreviewHeight; var height = arguments.PreviewHeight;
// TODO: 增加填充参数
var paddingL = 1u;
var paddingR = 1u;
var paddingT = 1u;
var paddingB = 1u;
var tex = new SFML.Graphics.RenderTexture(width, height);
int success = 0; int success = 0;
int error = 0; int error = 0;
@@ -241,11 +323,19 @@ namespace SpineViewer
} }
var spine = spines[i]; var spine = spines[i];
var tmp = spine.CurrentAnimation;
spine.CurrentAnimation = Spine.Spine.EMPTY_ANIMATION;
tex.SetView(spine.GetInitView(width, height, paddingL, paddingR, paddingT, paddingB));
tex.Clear(SFML.Graphics.Color.Transparent);
tex.Draw(spine);
tex.Display();
spine.CurrentAnimation = tmp;
try try
{ {
var preview = spine.GetPreview(width, height); using (var img = tex.Texture.CopyToImage())
var savePath = Path.Combine(outputDir, $"{spine.Name}.png"); {
preview.SaveToFile(savePath); img.SaveToFile(Path.Combine(outputDir, $"{spine.Name}.png"));
}
success++; success++;
} }
catch (Exception ex) catch (Exception ex)
@@ -269,7 +359,7 @@ namespace SpineViewer
Program.Logger.Info("{} preview saved successfully", success); Program.Logger.Info("{} preview saved successfully", success);
} }
Program.Logger.Info($"Current memory usage: {Program.Process.WorkingSet64 / 1024.0 / 1024.0:F2} MB"); Program.LogCurrentMemoryUsage();
} }
private void ConvertFileFormat_Work(object? sender, DoWorkEventArgs e) private void ConvertFileFormat_Work(object? sender, DoWorkEventArgs e)

39
SpineViewer/MessageBox.cs Normal file
View File

@@ -0,0 +1,39 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
namespace SpineViewer
{
/// <summary>
/// 弹窗消息静态类
/// </summary>
public static class MessageBox
{
/// <summary>
/// 提示弹窗
/// </summary>
public static void Info(string text, string title = "提示信息") =>
System.Windows.Forms.MessageBox.Show(text, title, MessageBoxButtons.OK, MessageBoxIcon.Information);
/// <summary>
/// 警告弹窗
/// </summary>
public static void Warn(string text, string title = "警告信息") =>
System.Windows.Forms.MessageBox.Show(text, title, MessageBoxButtons.OK, MessageBoxIcon.Warning);
/// <summary>
/// 错误弹窗
/// </summary>
public static void Error(string text, string title = "错误信息") =>
System.Windows.Forms.MessageBox.Show(text, title, MessageBoxButtons.OK, MessageBoxIcon.Error);
/// <summary>
/// 操作确认弹窗
/// </summary>
public static DialogResult Quest(string text, string title = "操作确认") =>
System.Windows.Forms.MessageBox.Show(text, title, MessageBoxButtons.OKCancel, MessageBoxIcon.Question);
}
}

View File

@@ -5,13 +5,38 @@ namespace SpineViewer
{ {
internal static class Program internal static class Program
{ {
public const string Name = "SpineViewer"; /// <summary>
/// 程序路径
/// </summary>
public static readonly string FilePath = Environment.ProcessPath;
/// <summary>
/// 程序名
/// </summary>
public static readonly string Name = Path.GetFileNameWithoutExtension(FilePath);
/// <summary>
/// 程序目录
/// </summary>
public static readonly string RootDir = Path.GetDirectoryName(FilePath);
/// <summary>
/// 程序临时目录
/// </summary>
public static readonly string TempDir = Directory.CreateDirectory(Path.Combine(Path.GetTempPath(), Name)).FullName; public static readonly string TempDir = Directory.CreateDirectory(Path.Combine(Path.GetTempPath(), Name)).FullName;
/// <summary>
/// 程序进程
/// </summary>
public static readonly Process Process = Process.GetCurrentProcess(); public static readonly Process Process = Process.GetCurrentProcess();
/// <summary>
/// 程序日志器
/// </summary>
public static readonly Logger Logger = LogManager.GetCurrentClassLogger(); public static readonly Logger Logger = LogManager.GetCurrentClassLogger();
/// <summary> /// <summary>
/// The main entry point for the application. /// 应用入口点
/// </summary> /// </summary>
[STAThread] [STAThread]
static void Main() static void Main()
@@ -30,7 +55,7 @@ namespace SpineViewer
catch (Exception ex) catch (Exception ex)
{ {
Logger.Fatal(ex.ToString()); Logger.Fatal(ex.ToString());
MessageBox.Show(ex.ToString(), "程序已崩溃", MessageBoxButtons.OK, MessageBoxIcon.Stop); MessageBox.Error(ex.ToString(), "程序已崩溃");
} }
} }
@@ -58,5 +83,9 @@ namespace SpineViewer
LogManager.Configuration = config; LogManager.Configuration = config;
} }
/// <summary>
/// 输出当前内存使用情况
/// </summary>
public static void LogCurrentMemoryUsage() => Logger.Info("Current memory usage: {:F2} MB", Process.WorkingSet64 / 1024.0 / 1024.0);
} }
} }

View File

@@ -8,6 +8,8 @@ using System.Text.Json;
using System.Text.Json.Nodes; using System.Text.Json.Nodes;
using SpineRuntime38.Attachments; using SpineRuntime38.Attachments;
using System.Globalization; using System.Globalization;
using static System.Runtime.InteropServices.JavaScript.JSType;
using System.IO;
namespace SpineViewer.Spine.Implementations.SkeletonConverter namespace SpineViewer.Spine.Implementations.SkeletonConverter
{ {
@@ -225,8 +227,8 @@ namespace SpineViewer.Spine.Implementations.SkeletonConverter
skin["name"] = reader.ReadStringRef(); skin["name"] = reader.ReadStringRef();
skin["bones"] = ReadNames(root["bones"].AsArray()); skin["bones"] = ReadNames(root["bones"].AsArray());
skin["ik"] = ReadNames(root["ik"].AsArray()); skin["ik"] = ReadNames(root["ik"].AsArray());
skin["transform"] = ReadNames(root["transform"].AsArray()); ; skin["transform"] = ReadNames(root["transform"].AsArray());
skin["path"] = ReadNames(root["path"].AsArray()); ; skin["path"] = ReadNames(root["path"].AsArray());
slotCount = reader.ReadVarInt(); slotCount = reader.ReadVarInt();
} }
@@ -1014,31 +1016,163 @@ namespace SpineViewer.Spine.Implementations.SkeletonConverter
private void WriteSkins() private void WriteSkins()
{ {
//JsonArray skins = [];
//// default skin
//if (ReadSkin(true) is JsonObject data)
// skins.Add(data);
//// other skins
//for (int n = reader.ReadVarInt(); n > 0; n--)
// skins.Add(ReadSkin());
//root["skins"] = skins;
if (!root.ContainsKey("skins")) if (!root.ContainsKey("skins"))
{
writer.WriteVarInt(0); // default 的 slotCount
writer.WriteVarInt(0); // 其他皮肤数量
return;
}
JsonArray skins = root["skins"].AsArray();
bool hasDefault = false;
foreach (JsonObject skin in skins)
{
if ((string)skin["name"] == "default")
{
hasDefault = true;
WriteSkin(skin, true);
break;
}
}
if (!hasDefault) writer.WriteVarInt(0);
int skinCount = hasDefault ? skins.Count - 1 : skins.Count;
if (skinCount <= 0)
{ {
writer.WriteVarInt(0); writer.WriteVarInt(0);
return; return;
} }
JsonArray skins = root["skins"].AsArray();
writer.WriteVarInt(skins.Count); writer.WriteVarInt(skinCount);
for (int i = 0, n = skins.Count; i < n; i++) foreach (JsonObject skin in skins)
{ {
throw new NotImplementedException(); if ((string)skin["name"] != "default")
WriteSkin(skin);
} }
} }
private void WriteSkin(JsonObject skin, bool isDefault = false)
{
JsonObject skinAttachments = null;
if (isDefault)
{
// 这里固定有一个给 default 的 count 值, 算是占位符, 如果是 0 则表示没有 default 的 skin
if (skin.TryGetPropertyValue("attachments", out var attachments)) skinAttachments = attachments.AsObject();
writer.WriteVarInt(skinAttachments?.Count ?? 0);
}
else
{
writer.WriteStringRef((string)skin["name"]);
if (skin.TryGetPropertyValue("bones", out var bones)) WriteNames(bone2idx, bones.AsArray()); else writer.WriteVarInt(0);
if (skin.TryGetPropertyValue("ik", out var ik)) WriteNames(ik2idx, ik.AsArray()); else writer.WriteVarInt(0);
if (skin.TryGetPropertyValue("transform", out var transform)) WriteNames(transform2idx, transform.AsArray()); else writer.WriteVarInt(0);
if (skin.TryGetPropertyValue("path", out var path)) WriteNames(path2idx, path.AsArray()); else writer.WriteVarInt(0);
if (skin.TryGetPropertyValue("attachments", out var attachments)) skinAttachments = attachments.AsObject();
writer.WriteVarInt(skinAttachments?.Count ?? 0);
}
if (skinAttachments is null)
return;
foreach (var (slotName, _slotAttachments) in skinAttachments)
{
JsonObject slotAttachments = _slotAttachments.AsObject();
writer.WriteVarInt(slot2idx[slotName]);
writer.WriteVarInt(slotAttachments.Count);
foreach (var (attachmentKey, attachment) in slotAttachments)
{
writer.WriteStringRef(attachmentKey);
WriteAttachment(attachment.AsObject(), attachmentKey);
}
}
}
private void WriteAttachment(JsonObject attachment, string keyName)
{
int vertexCount;
string name = keyName;
AttachmentType type = AttachmentType.Region;
if (attachment.TryGetPropertyValue("name", out var _name)) name = (string)_name;
if (attachment.TryGetPropertyValue("type", out var _type)) type = Enum.Parse<AttachmentType>((string)_type, true);
writer.WriteStringRef(name);
writer.WriteByte((byte)type);
switch (type)
{
case AttachmentType.Region:
if (attachment.TryGetPropertyValue("path", out var path1)) writer.WriteStringRef((string)path1); else writer.WriteStringRef(null);
if (attachment.TryGetPropertyValue("rotation", out var rotation1)) writer.WriteFloat((float)rotation1); else writer.WriteFloat(0);
if (attachment.TryGetPropertyValue("x", out var x1)) writer.WriteFloat((float)x1); else writer.WriteFloat(0);
if (attachment.TryGetPropertyValue("y", out var y1)) writer.WriteFloat((float)y1); else writer.WriteFloat(0);
if (attachment.TryGetPropertyValue("scaleX", out var scaleX)) writer.WriteFloat((float)scaleX); else writer.WriteFloat(1);
if (attachment.TryGetPropertyValue("scaleY", out var scaleY)) writer.WriteFloat((float)scaleY); else writer.WriteFloat(1);
if (attachment.TryGetPropertyValue("width", out var width)) writer.WriteFloat((float)width); else writer.WriteFloat(32);
if (attachment.TryGetPropertyValue("height", out var height)) writer.WriteFloat((float)height); else writer.WriteFloat(32);
if (attachment.TryGetPropertyValue("color", out var color1)) writer.WriteInt(int.Parse((string)color1, NumberStyles.HexNumber)); else writer.WriteInt(0);
break;
case AttachmentType.Boundingbox:
if (attachment.TryGetPropertyValue("vertexCount", out var _vertexCount1)) vertexCount = (int)_vertexCount1; else vertexCount = 0;
writer.WriteVarInt(vertexCount);
WriteVertices(attachment["vertices"].AsArray(), vertexCount);
if (nonessential) writer.WriteInt(0);
break;
case AttachmentType.Mesh:
if (attachment.TryGetPropertyValue("path", out var path2)) writer.WriteStringRef((string)path2); else writer.WriteStringRef(null);
if (attachment.TryGetPropertyValue("color", out var color2)) writer.WriteInt(int.Parse((string)color2, NumberStyles.HexNumber)); else writer.WriteInt(0);
if (attachment.TryGetPropertyValue("vertexCount", out var _vertexCount2)) vertexCount = (int)_vertexCount2; else vertexCount = 0;
writer.WriteVarInt(vertexCount);
WriteFloatArray(attachment["uvs"].AsArray(), vertexCount << 1); // vertexCount = uvs.Length
WriteShortArray(attachment["triangles"].AsArray());
WriteVertices(attachment["vertices"].AsArray(), vertexCount);
if (attachment.TryGetPropertyValue("hull", out var hull)) writer.WriteVarInt((int)hull); else writer.WriteVarInt(0);
if (nonessential)
{
if (attachment.TryGetPropertyValue("edges", out var edges)) WriteShortArray(edges.AsArray()); else writer.WriteVarInt(0);
if (attachment.TryGetPropertyValue("width", out var _width)) writer.WriteFloat((float)_width); else writer.WriteFloat(0);
if (attachment.TryGetPropertyValue("height", out var _height)) writer.WriteFloat((float)_height); else writer.WriteFloat(0);
}
break;
case AttachmentType.Linkedmesh:
if (attachment.TryGetPropertyValue("path", out var path3)) writer.WriteStringRef((string)path3); else writer.WriteStringRef(null);
if (attachment.TryGetPropertyValue("color", out var color3)) writer.WriteInt(int.Parse((string)color3, NumberStyles.HexNumber)); else writer.WriteInt(0);
if (attachment.TryGetPropertyValue("skin", out var skin)) writer.WriteStringRef((string)skin); else writer.WriteStringRef(null);
if (attachment.TryGetPropertyValue("parent", out var parent)) writer.WriteStringRef((string)parent); else writer.WriteStringRef(null);
if (attachment.TryGetPropertyValue("deform", out var deform)) writer.WriteBoolean((bool)deform); else writer.WriteBoolean(true);
if (nonessential)
{
if (attachment.TryGetPropertyValue("width", out var _width)) writer.WriteFloat((float)_width); else writer.WriteFloat(0);
if (attachment.TryGetPropertyValue("height", out var _height)) writer.WriteFloat((float)_height); else writer.WriteFloat(0);
}
break;
case AttachmentType.Path:
if (attachment.TryGetPropertyValue("closed", out var closed)) writer.WriteBoolean((bool)closed); else writer.WriteBoolean(false);
if (attachment.TryGetPropertyValue("constantSpeed", out var constantSpeed)) writer.WriteBoolean((bool)constantSpeed); else writer.WriteBoolean(true);
if (attachment.TryGetPropertyValue("vertexCount", out var _vertexCount3)) vertexCount = (int)_vertexCount3; else vertexCount = 0;
writer.WriteVarInt(vertexCount);
WriteVertices(attachment["vertices"].AsArray(), vertexCount);
WriteFloatArray(attachment["lengths"].AsArray(), vertexCount / 3);
if (nonessential) writer.WriteInt(0);
break;
case AttachmentType.Point:
if (attachment.TryGetPropertyValue("rotation", out var rotation2)) writer.WriteFloat((float)rotation2); else writer.WriteFloat(0);
if (attachment.TryGetPropertyValue("x", out var x2)) writer.WriteFloat((float)x2); else writer.WriteFloat(0);
if (attachment.TryGetPropertyValue("y", out var y2)) writer.WriteFloat((float)y2); else writer.WriteFloat(0);
if (nonessential) writer.WriteInt(0);
break;
case AttachmentType.Clipping:
writer.WriteVarInt(slot2idx[(string)attachment["end"]]);
if (attachment.TryGetPropertyValue("vertexCount", out var _vertexCount4)) vertexCount = (int)_vertexCount4; else vertexCount = 0;
writer.WriteVarInt(vertexCount);
WriteVertices(attachment["vertices"].AsArray(), vertexCount);
if (nonessential) writer.WriteInt(0);
break;
default:
throw new ArgumentOutOfRangeException($"Invalid attachment type: {type}");
}
}
private void WriteEvents() private void WriteEvents()
{ {
@@ -1089,8 +1223,50 @@ namespace SpineViewer.Spine.Implementations.SkeletonConverter
private void WriteNames(Dictionary<string, int> name2idx, JsonArray names) private void WriteNames(Dictionary<string, int> name2idx, JsonArray names)
{ {
writer.WriteVarInt(names.Count); writer.WriteVarInt(names.Count);
foreach (var name in names) foreach (string name in names)
writer.WriteVarInt(name2idx[(string)name]); writer.WriteVarInt(name2idx[name]);
}
public void WriteFloatArray(JsonArray array, int n)
{
for (int i = 0; i < n; i++)
writer.WriteFloat((float)array[i]);
}
public void WriteShortArray(JsonArray array)
{
writer.WriteVarInt(array.Count);
foreach (uint i in array)
{
writer.WriteByte((byte)(i >> 8));
writer.WriteByte((byte)i);
}
}
private void WriteVertices(JsonArray vertices, int vertexCount)
{
bool hasWeight = vertices.Count != (vertexCount << 1);
writer.WriteBoolean(hasWeight);
if (!hasWeight)
{
WriteFloatArray(vertices, vertexCount << 1);
}
else
{
int idx = 0;
for (int i = 0; i < vertexCount; i++)
{
var bonesCount = (int)vertices[idx++];
writer.WriteVarInt(bonesCount);
for (int j = 0; j < bonesCount; j++)
{
writer.WriteVarInt((int)vertices[idx++]);
writer.WriteFloat((float)vertices[idx++]);
writer.WriteFloat((float)vertices[idx++]);
writer.WriteFloat((float)vertices[idx++]);
}
}
}
} }
public override JsonObject ReadJson(string jsonPath) public override JsonObject ReadJson(string jsonPath)
@@ -1122,19 +1298,5 @@ namespace SpineViewer.Spine.Implementations.SkeletonConverter
return root; return root;
} }
//public void WriteFloatArray(float[] array)
//{
// foreach (var i in array)
// writer.WriteFloat(i);
//}
//public void WriteShortArray(int[] array)
//{
// foreach (var i in array)
// {
// writer.WriteByte((byte)(i >> 8));
// writer.WriteByte((byte)i);
// }
//}
} }
} }

View File

@@ -380,6 +380,17 @@ namespace SpineViewer.Spine.Implementations.Spine
states.Shader = null; states.Shader = null;
target.Draw(vertexArray, states); target.Draw(vertexArray, states);
//clipping.ClipEnd(); //clipping.ClipEnd();
// 包围盒
if (IsDebug && IsSelected && DebugBounds)
{
var bounds = Bounds;
boundsVertices[0] = boundsVertices[4] = new(new(bounds.Left, bounds.Top), BoundsColor);
boundsVertices[1] = new(new(bounds.Right, bounds.Top), BoundsColor);
boundsVertices[2] = new(new(bounds.Right, bounds.Bottom), BoundsColor);
boundsVertices[3] = new(new(bounds.Left, bounds.Bottom), BoundsColor);
target.Draw(boundsVertices);
}
} }
} }
} }

View File

@@ -336,6 +336,17 @@ namespace SpineViewer.Spine.Implementations.Spine
states.Shader = null; states.Shader = null;
target.Draw(vertexArray, states); target.Draw(vertexArray, states);
clipping.ClipEnd(); clipping.ClipEnd();
// 包围盒
if (IsDebug && IsSelected && DebugBounds)
{
var bounds = Bounds;
boundsVertices[0] = boundsVertices[4] = new(new(bounds.Left, bounds.Top), BoundsColor);
boundsVertices[1] = new(new(bounds.Right, bounds.Top), BoundsColor);
boundsVertices[2] = new(new(bounds.Right, bounds.Bottom), BoundsColor);
boundsVertices[3] = new(new(bounds.Left, bounds.Bottom), BoundsColor);
target.Draw(boundsVertices);
}
} }
} }
} }

View File

@@ -344,6 +344,17 @@ namespace SpineViewer.Spine.Implementations.Spine
states.Shader = null; states.Shader = null;
target.Draw(vertexArray, states); target.Draw(vertexArray, states);
clipping.ClipEnd(); clipping.ClipEnd();
// 包围盒
if (IsDebug && IsSelected && DebugBounds)
{
var bounds = Bounds;
boundsVertices[0] = boundsVertices[4] = new(new(bounds.Left, bounds.Top), BoundsColor);
boundsVertices[1] = new(new(bounds.Right, bounds.Top), BoundsColor);
boundsVertices[2] = new(new(bounds.Right, bounds.Bottom), BoundsColor);
boundsVertices[3] = new(new(bounds.Left, bounds.Bottom), BoundsColor);
target.Draw(boundsVertices);
}
} }
} }
} }

View File

@@ -347,6 +347,17 @@ namespace SpineViewer.Spine.Implementations.Spine
states.Shader = null; states.Shader = null;
target.Draw(vertexArray, states); target.Draw(vertexArray, states);
clipping.ClipEnd(); clipping.ClipEnd();
// 包围盒
if (IsDebug && IsSelected && DebugBounds)
{
var bounds = Bounds;
boundsVertices[0] = boundsVertices[4] = new(new(bounds.Left, bounds.Top), BoundsColor);
boundsVertices[1] = new(new(bounds.Right, bounds.Top), BoundsColor);
boundsVertices[2] = new(new(bounds.Right, bounds.Bottom), BoundsColor);
boundsVertices[3] = new(new(bounds.Left, bounds.Bottom), BoundsColor);
target.Draw(boundsVertices);
}
} }
} }
} }

View File

@@ -346,6 +346,17 @@ namespace SpineViewer.Spine.Implementations.Spine
states.Shader = null; states.Shader = null;
target.Draw(vertexArray, states); target.Draw(vertexArray, states);
clipping.ClipEnd(); clipping.ClipEnd();
// 包围盒
if (IsDebug && IsSelected && DebugBounds)
{
var bounds = Bounds;
boundsVertices[0] = boundsVertices[4] = new(new(bounds.Left, bounds.Top), BoundsColor);
boundsVertices[1] = new(new(bounds.Right, bounds.Top), BoundsColor);
boundsVertices[2] = new(new(bounds.Right, bounds.Bottom), BoundsColor);
boundsVertices[3] = new(new(bounds.Left, bounds.Bottom), BoundsColor);
target.Draw(boundsVertices);
}
} }
} }
} }

View File

@@ -346,6 +346,17 @@ namespace SpineViewer.Spine.Implementations.Spine
states.Shader = null; states.Shader = null;
target.Draw(vertexArray, states); target.Draw(vertexArray, states);
clipping.ClipEnd(); clipping.ClipEnd();
// 包围盒
if (IsDebug && IsSelected && DebugBounds)
{
var bounds = Bounds;
boundsVertices[0] = boundsVertices[4] = new(new(bounds.Left, bounds.Top), BoundsColor);
boundsVertices[1] = new(new(bounds.Right, bounds.Top), BoundsColor);
boundsVertices[2] = new(new(bounds.Right, bounds.Bottom), BoundsColor);
boundsVertices[3] = new(new(bounds.Left, bounds.Bottom), BoundsColor);
target.Draw(boundsVertices);
}
} }
} }
} }

View File

@@ -346,6 +346,17 @@ namespace SpineViewer.Spine.Implementations.Spine
states.Shader = null; states.Shader = null;
target.Draw(vertexArray, states); target.Draw(vertexArray, states);
clipping.ClipEnd(); clipping.ClipEnd();
// 包围盒
if (IsDebug && IsSelected && DebugBounds)
{
var bounds = Bounds;
boundsVertices[0] = boundsVertices[4] = new(new(bounds.Left, bounds.Top), BoundsColor);
boundsVertices[1] = new(new(bounds.Right, bounds.Top), BoundsColor);
boundsVertices[2] = new(new(bounds.Right, bounds.Bottom), BoundsColor);
boundsVertices[3] = new(new(bounds.Left, bounds.Bottom), BoundsColor);
target.Draw(boundsVertices);
}
} }
} }
} }

View File

@@ -8,8 +8,6 @@ using System.Text.RegularExpressions;
using System.Numerics; using System.Numerics;
using System.Collections; using System.Collections;
using System.Collections.ObjectModel; using System.Collections.ObjectModel;
using SFML.System;
using SFML.Window;
using System.ComponentModel; using System.ComponentModel;
using System.Reflection; using System.Reflection;
using System.Diagnostics.CodeAnalysis; using System.Diagnostics.CodeAnalysis;
@@ -49,9 +47,14 @@ namespace SpineViewer.Spine
public const string EMPTY_ANIMATION = "<Empty>"; public const string EMPTY_ANIMATION = "<Empty>";
/// <summary> /// <summary>
/// 预览图大小 /// 预览图
/// </summary> /// </summary>
public static readonly Size PREVIEW_SIZE = new(256, 256); public const uint PREVIEW_WIDTH = 256;
/// <summary>
/// 预览图高
/// </summary>
public const uint PREVIEW_HEIGHT = 256;
/// <summary> /// <summary>
/// 缩放最小值 /// 缩放最小值
@@ -109,7 +112,7 @@ namespace SpineViewer.Spine
FragmentShader = null; FragmentShader = null;
Program.Logger.Error(ex.ToString()); Program.Logger.Error(ex.ToString());
Program.Logger.Error("Failed to load fragment shader"); Program.Logger.Error("Failed to load fragment shader");
MessageBox.Show("Fragment shader 加载失败预乘Alpha通道属性失效", "错误信息", MessageBoxButtons.OK, MessageBoxIcon.Information); MessageBox.Warn("Fragment shader 加载失败预乘Alpha通道属性失效");
} }
} }
@@ -227,6 +230,8 @@ namespace SpineViewer.Spine
public void Dispose() { Dispose(true); GC.SuppressFinalize(this); } public void Dispose() { Dispose(true); GC.SuppressFinalize(this); }
protected virtual void Dispose(bool disposing) { preview?.Dispose(); } protected virtual void Dispose(bool disposing) { preview?.Dispose(); }
#region |
/// <summary> /// <summary>
/// 获取所属版本 /// 获取所属版本
/// </summary> /// </summary>
@@ -264,6 +269,10 @@ namespace SpineViewer.Spine
[Category("基本信息"), DisplayName("文件版本")] [Category("基本信息"), DisplayName("文件版本")]
public abstract string FileVersion { get; } public abstract string FileVersion { get; }
#endregion
#region |
/// <summary> /// <summary>
/// 缩放比例 /// 缩放比例
/// </summary> /// </summary>
@@ -289,12 +298,18 @@ namespace SpineViewer.Spine
[Category("变换"), DisplayName("垂直翻转")] [Category("变换"), DisplayName("垂直翻转")]
public abstract bool FlipY { get; set; } public abstract bool FlipY { get; set; }
#endregion
#region |
/// <summary> /// <summary>
/// 是否使用预乘Alpha /// 是否使用预乘Alpha
/// </summary> /// </summary>
[Category("画面"), DisplayName("预乘Alpha通道")] [Category("画面"), DisplayName("预乘Alpha通道")]
public bool UsePremultipliedAlpha { get; set; } = true; public bool UsePremultipliedAlpha { get; set; } = true;
#endregion
/// <summary> /// <summary>
/// 包含的所有动画名称 /// 包含的所有动画名称
/// </summary> /// </summary>
@@ -308,8 +323,10 @@ namespace SpineViewer.Spine
[Browsable(false)] [Browsable(false)]
public string DefaultAnimationName { get => animationNames.Last(); } public string DefaultAnimationName { get => animationNames.Last(); }
#region |
/// <summary> /// <summary>
/// 当前动画名称 /// 当前动画名称, 如果设置的动画不存在则忽略
/// </summary> /// </summary>
[TypeConverter(typeof(AnimationConverter))] [TypeConverter(typeof(AnimationConverter))]
[Category("动画"), DisplayName("当前动画")] [Category("动画"), DisplayName("当前动画")]
@@ -321,6 +338,8 @@ namespace SpineViewer.Spine
[Category("动画"), DisplayName("当前动画时长")] [Category("动画"), DisplayName("当前动画时长")]
public float CurrentAnimationDuration { get => GetAnimationDuration(CurrentAnimation); } public float CurrentAnimationDuration { get => GetAnimationDuration(CurrentAnimation); }
#endregion
/// <summary> /// <summary>
/// 骨骼包围盒 /// 骨骼包围盒
/// </summary> /// </summary>
@@ -337,7 +356,21 @@ namespace SpineViewer.Spine
{ {
if (preview is null) if (preview is null)
{ {
using var img = GetPreview((uint)PREVIEW_SIZE.Width, (uint)PREVIEW_SIZE.Height); // XXX: tex 没办法在这里主动 Dispose
// 批量添加在获取预览图的时候极大概率会和预览线程死锁
// 虽然两边不会同时调用 Draw, 但是死锁似乎和 Draw 函数有关
// 除此之外, 似乎还和 tex 的 Dispose 有关
// 如果不对 tex 进行 Dispose, 那么不管是否 Draw 都正常不会死锁
var tex = new SFML.Graphics.RenderTexture(PREVIEW_WIDTH, PREVIEW_HEIGHT);
tex.SetView(GetInitView(PREVIEW_WIDTH, PREVIEW_HEIGHT));
tex.Clear(SFML.Graphics.Color.Transparent);
var tmp = CurrentAnimation;
CurrentAnimation = EMPTY_ANIMATION;
tex.Draw(this);
CurrentAnimation = tmp;
tex.Display();
using var img = tex.Texture.CopyToImage();
img.SaveToMemory(out var imgBuffer, "bmp"); img.SaveToMemory(out var imgBuffer, "bmp");
using var stream = new MemoryStream(imgBuffer); using var stream = new MemoryStream(imgBuffer);
preview = new Bitmap(stream); preview = new Bitmap(stream);
@@ -347,43 +380,6 @@ namespace SpineViewer.Spine
} }
private Image preview = null; private Image preview = null;
/// <summary>
/// 获取指定尺寸的预览图
/// </summary>
public SFML.Graphics.Image GetPreview(uint width, uint height)
{
var curAnimation = CurrentAnimation;
CurrentAnimation = EMPTY_ANIMATION;
var bounds = Bounds;
float viewX = width;
float viewY = height;
float sizeX = bounds.Width;
float sizeY = bounds.Height;
var scale = 1f;
if ((sizeY / sizeX) < (viewY / viewX))
scale = sizeX / viewX;// 相同的 X, 视窗 Y 更大
else
scale = sizeY / viewY;// 相同的 Y, 视窗 X 更大
viewX *= scale;
viewY *= scale;
// XXX: 貌似无法使用 using 或者 Dispose 主动释放 tex 资源
// 在批量添加的中途, 如果触发 GC? 会卡死, 目前未知原因
var tex = new SFML.Graphics.RenderTexture(width, height);
var view = tex.GetView();
view.Center = new(bounds.X + viewX / 2, bounds.Y + viewY / 2);
view.Size = new(viewX, -viewY);
tex.SetView(view);
tex.Clear(SFML.Graphics.Color.Transparent);
tex.Draw(this);
tex.Display();
CurrentAnimation = curAnimation;
return tex.Texture.CopyToImage();
}
/// <summary> /// <summary>
/// 获取动画时长, 如果动画不存在则返回 0 /// 获取动画时长, 如果动画不存在则返回 0
/// </summary> /// </summary>
@@ -396,19 +392,51 @@ namespace SpineViewer.Spine
public abstract void Update(float delta); public abstract void Update(float delta);
/// <summary> /// <summary>
/// 顶点坐标缓冲区 /// 获取初始状态下合适的 View, 参数单位为像素
/// </summary> /// </summary>
protected float[] worldVerticesBuffer = new float[1024]; 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);
/// <summary> /// <summary>
/// 顶点缓冲区 /// 获取初始状态下合适的 View, 参数单位为像素
/// </summary> /// </summary>
protected SFML.Graphics.VertexArray vertexArray = new(SFML.Graphics.PrimitiveType.Triangles); 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);
/// <summary> /// <summary>
/// SFML.Graphics.Drawable 接口实现 /// 获取初始状态下合适的 View, 参数单位为像素
/// </summary> /// </summary>
public abstract void Draw(SFML.Graphics.RenderTarget target, SFML.Graphics.RenderStates states); 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);
/// <summary>
/// 获取初始状态下合适的 View, 参数单位为像素
/// </summary>
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));
}
/// <summary> /// <summary>
/// 是否被选中 /// 是否被选中
@@ -422,16 +450,45 @@ namespace SpineViewer.Spine
[Browsable(false)] [Browsable(false)]
public bool IsDebug { get; set; } = false; public bool IsDebug { get; set; } = false;
/// <summary>
/// 包围盒颜色
/// </summary>
protected static readonly SFML.Graphics.Color BoundsColor = new(120, 200, 0);
/// <summary>
/// 包围盒顶点数组
/// </summary>
protected readonly SFML.Graphics.VertexArray boundsVertices = new(SFML.Graphics.PrimitiveType.LineStrip, 5);
/// <summary> /// <summary>
/// 显示包围盒 /// 显示包围盒
/// </summary> /// </summary>
[Browsable(false)] [Category("调试"), DisplayName("显示包围盒")]
public bool DebugBounds { get; set; } = true; public bool DebugBounds { get; set; } = true;
/// <summary> /// <summary>
/// 显示骨骼 /// 显示骨骼
/// </summary> /// </summary>
[Browsable(false)] [Category("调试"), DisplayName("显示骨骼(TODO)")]
public bool DebugBones { get; set; } = true; public bool DebugBones { get; set; } = false;
#region SFML.Graphics.Drawable
/// <summary>
/// 顶点坐标缓冲区
/// </summary>
protected float[] worldVerticesBuffer = new float[1024];
/// <summary>
/// 顶点缓冲区
/// </summary>
protected readonly SFML.Graphics.VertexArray vertexArray = new(SFML.Graphics.PrimitiveType.Triangles);
/// <summary>
/// SFML.Graphics.Drawable 接口实现
/// </summary>
public abstract void Draw(SFML.Graphics.RenderTarget target, SFML.Graphics.RenderStates states);
#endregion
} }
} }

View File

@@ -42,7 +42,19 @@ namespace SpineViewer.Spine
public override StandardValuesCollection? GetStandardValues(ITypeDescriptorContext? context) public override StandardValuesCollection? GetStandardValues(ITypeDescriptorContext? context)
{ {
if (context?.Instance is Spine obj) if (context?.Instance is Spine obj)
{
return new StandardValuesCollection(obj.AnimationNames); return new StandardValuesCollection(obj.AnimationNames);
}
else if (context?.Instance is Spine[] spines)
{
if (spines.Length > 0)
{
IEnumerable<string> common = spines[0].AnimationNames;
foreach (var spine in spines.Skip(1))
common = common.Intersect(spine.AnimationNames);
return new StandardValuesCollection(common.ToArray());
}
}
return base.GetStandardValues(context); return base.GetStandardValues(context);
} }
} }

View File

@@ -9,6 +9,9 @@ using System.Threading.Tasks;
namespace SpineViewer.Spine namespace SpineViewer.Spine
{ {
/// <summary>
/// Spine 版本静态辅助类
/// </summary>
public static class VersionHelper public static class VersionHelper
{ {
/// <summary> /// <summary>

View File

@@ -8,7 +8,7 @@
<RuntimeIdentifier>win-x64</RuntimeIdentifier> <RuntimeIdentifier>win-x64</RuntimeIdentifier>
<BaseOutputPath>$(SolutionDir)out</BaseOutputPath> <BaseOutputPath>$(SolutionDir)out</BaseOutputPath>
<IncludeSourceRevisionInInformationalVersion>false</IncludeSourceRevisionInInformationalVersion> <IncludeSourceRevisionInInformationalVersion>false</IncludeSourceRevisionInInformationalVersion>
<Version>0.10.6</Version> <Version>0.10.7</Version>
<OutputType>WinExe</OutputType> <OutputType>WinExe</OutputType>
<UseWindowsForms>true</UseWindowsForms> <UseWindowsForms>true</UseWindowsForms>
<ApplicationIcon>appicon.ico</ApplicationIcon> <ApplicationIcon>appicon.ico</ApplicationIcon>
@@ -19,6 +19,7 @@
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="FFMpegCore" Version="5.2.0" />
<PackageReference Include="NLog.Windows.Forms" Version="5.2.3" /> <PackageReference Include="NLog.Windows.Forms" Version="5.2.3" />
<PackageReference Include="SFML.Net" Version="2.6.1" /> <PackageReference Include="SFML.Net" Version="2.6.1" />
<PackageReference Include="System.Management" Version="9.0.2" /> <PackageReference Include="System.Management" Version="9.0.2" />