Compare commits
48 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c0553042fd | ||
|
|
af8b02654b | ||
|
|
4779ec91d0 | ||
|
|
14d7f4af0e | ||
|
|
f9888b23dd | ||
|
|
411cdbb00f | ||
|
|
d859f07469 | ||
|
|
c111819093 | ||
|
|
aa8321d13c | ||
|
|
5e3bd972e5 | ||
|
|
ad39a04fff | ||
|
|
9a97e84296 | ||
|
|
1b7b0dcb13 | ||
|
|
d365a5060b | ||
|
|
b69589394a | ||
|
|
00f5791766 | ||
|
|
38cab2eda7 | ||
|
|
0db4d6e4e0 | ||
|
|
549712962f | ||
|
|
34b7002faf | ||
|
|
0e6f47b23c | ||
|
|
a372a89b5e | ||
|
|
239847aee7 | ||
|
|
813249c6a7 | ||
|
|
293ab28bce | ||
|
|
98e73cdec5 | ||
|
|
6d34bb9d25 | ||
|
|
479a5e4da9 | ||
|
|
4829454877 | ||
|
|
28664f6387 | ||
|
|
1a08a23a9c | ||
|
|
16f344ff1b | ||
|
|
693ce0e2e8 | ||
|
|
e6f533ea65 | ||
|
|
fcc21d63b0 | ||
|
|
afc0ffcb67 | ||
|
|
9ffb9840e1 | ||
|
|
4766ccf1b6 | ||
|
|
16b75c80a3 | ||
|
|
880f063046 | ||
|
|
723c11b886 | ||
|
|
5e074b1cf7 | ||
|
|
71d2fee36e | ||
|
|
7dc701464f | ||
|
|
fd876ef90f | ||
|
|
0597852178 | ||
|
|
81b1333091 | ||
|
|
7baebd79a6 |
28
CHANGELOG.md
28
CHANGELOG.md
@@ -1,5 +1,33 @@
|
||||
# CHANGELOG
|
||||
|
||||
## v0.11.4
|
||||
|
||||
- 增加 MP4 导出格式
|
||||
- 增加导出背景颜色参数
|
||||
- 增加日志输出 FFMpeg 参数字符串
|
||||
- 增加导出时任务栏图标执行动效
|
||||
- 修复预览面板移动模型时物理效果不同步的问题
|
||||
- 优化部分使用体验
|
||||
|
||||
## v0.11.3
|
||||
|
||||
- 增加模型隐藏设置属性
|
||||
- 加宽面板分割条 (4 -> 8 像素)
|
||||
- 优化属性面板分组显示
|
||||
- 增加调试纹理
|
||||
|
||||
## v0.11.2
|
||||
|
||||
- 增加皮肤切换
|
||||
- 优化模型缩放实现
|
||||
- 修复部分情况纹理加载异常
|
||||
|
||||
## v0.11.1
|
||||
|
||||
- 增加 GIF 导出格式
|
||||
- 增加逐个导出时可选自动时长
|
||||
- 优化使用体验
|
||||
|
||||
## v0.11.0
|
||||
|
||||
- 完成导出系统, 支持完整的单帧和帧序列导出功能
|
||||
|
||||
@@ -1,7 +1,3 @@
|
||||
Below is the translated English version of your README:
|
||||
|
||||
---
|
||||
|
||||
# [SpineViewer](https://github.com/ww-rm/SpineViewer)
|
||||
|
||||
[](https://github.com/ww-rm/SpineViewer/actions/workflows/dotnet-desktop.yml)
|
||||
@@ -28,7 +24,7 @@ You can also download the zip package with the `SelfContained` suffix, which can
|
||||
- [x] Frame Sequence
|
||||
- [x] Animated GIF
|
||||
- [ ] MKV
|
||||
- [ ] MP4
|
||||
- [x] MP4
|
||||
- [ ] MOV
|
||||
- [ ] WebM
|
||||
|
||||
@@ -98,4 +94,4 @@ For more detailed instructions and usage, please refer to the [Wiki](https://git
|
||||
|
||||
*If you like this project, please give it a :star: and share it with others! :)*
|
||||
|
||||
[](https://starchart.cc/ww-rm/SpineViewer)
|
||||
[](https://starchart.cc/ww-rm/SpineViewer)
|
||||
|
||||
@@ -24,7 +24,7 @@
|
||||
- [x] 帧序列
|
||||
- [x] GIF 动图
|
||||
- [ ] MKV
|
||||
- [ ] MP4
|
||||
- [x] MP4
|
||||
- [ ] MOV
|
||||
- [ ] WebM
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@ using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows.Forms;
|
||||
using System.IO;
|
||||
using SpineViewer.Spine;
|
||||
|
||||
namespace SpineViewer.Controls
|
||||
{
|
||||
@@ -33,14 +34,14 @@ namespace SpineViewer.Controls
|
||||
{
|
||||
if (File.Exists(path))
|
||||
{
|
||||
if (Spine.Spine.CommonSkelSuffix.Contains(Path.GetExtension(path).ToLower()))
|
||||
if (SpineHelper.CommonSkelSuffix.Contains(Path.GetExtension(path).ToLower()))
|
||||
listBox.Items.Add(Path.GetFullPath(path));
|
||||
}
|
||||
else if (Directory.Exists(path))
|
||||
{
|
||||
foreach (var file in Directory.EnumerateFiles(path, "*.*", SearchOption.AllDirectories))
|
||||
{
|
||||
if (Spine.Spine.CommonSkelSuffix.Contains(Path.GetExtension(file).ToLower()))
|
||||
if (SpineHelper.CommonSkelSuffix.Contains(Path.GetExtension(file).ToLower()))
|
||||
listBox.Items.Add(file);
|
||||
}
|
||||
}
|
||||
@@ -57,7 +58,7 @@ namespace SpineViewer.Controls
|
||||
{
|
||||
foreach (var file in Directory.EnumerateFiles(path, "*.*", SearchOption.AllDirectories))
|
||||
{
|
||||
if (Spine.Spine.CommonSkelSuffix.Contains(Path.GetExtension(file).ToLower()))
|
||||
if (SpineHelper.CommonSkelSuffix.Contains(Path.GetExtension(file).ToLower()))
|
||||
listBox.Items.Add(file);
|
||||
}
|
||||
}
|
||||
|
||||
40
SpineViewer/Controls/SpineListView.Designer.cs
generated
40
SpineViewer/Controls/SpineListView.Designer.cs
generated
@@ -44,8 +44,9 @@
|
||||
toolStripMenuItem_MoveTop = new ToolStripMenuItem();
|
||||
toolStripMenuItem_MoveBottom = new ToolStripMenuItem();
|
||||
toolStripSeparator3 = new ToolStripSeparator();
|
||||
toolStripMenuItem_SelectAll = new ToolStripMenuItem();
|
||||
toolStripMenuItem_CopyPreview = new ToolStripMenuItem();
|
||||
toolStripMenuItem_AddFromClipboard = new ToolStripMenuItem();
|
||||
toolStripMenuItem_SelectAll = new ToolStripMenuItem();
|
||||
toolStripSeparator4 = new ToolStripSeparator();
|
||||
toolStripMenuItem_ChangeView = new ToolStripMenuItem();
|
||||
toolStripMenuItem_LargeIconView = new ToolStripMenuItem();
|
||||
@@ -53,7 +54,6 @@
|
||||
toolStripMenuItem_DetailsView = new ToolStripMenuItem();
|
||||
imageList_LargeIcon = new ImageList(components);
|
||||
imageList_SmallIcon = new ImageList(components);
|
||||
toolStripMenuItem_AddFromClipboard = new ToolStripMenuItem();
|
||||
contextMenuStrip.SuspendLayout();
|
||||
SuspendLayout();
|
||||
//
|
||||
@@ -84,14 +84,14 @@
|
||||
// columnHeader_Name
|
||||
//
|
||||
columnHeader_Name.Text = "名称";
|
||||
columnHeader_Name.Width = 220;
|
||||
columnHeader_Name.Width = 300;
|
||||
//
|
||||
// contextMenuStrip
|
||||
//
|
||||
contextMenuStrip.ImageScalingSize = new Size(24, 24);
|
||||
contextMenuStrip.Items.AddRange(new ToolStripItem[] { toolStripMenuItem_Add, toolStripMenuItem_Insert, toolStripMenuItem_Remove, toolStripSeparator1, toolStripMenuItem_BatchAdd, toolStripMenuItem_RemoveAll, toolStripSeparator2, toolStripMenuItem_MoveUp, toolStripMenuItem_MoveDown, toolStripMenuItem_MoveTop, toolStripMenuItem_MoveBottom, toolStripSeparator3, toolStripMenuItem_CopyPreview, toolStripMenuItem_AddFromClipboard, toolStripMenuItem_SelectAll, toolStripSeparator4, toolStripMenuItem_ChangeView });
|
||||
contextMenuStrip.Name = "contextMenuStrip";
|
||||
contextMenuStrip.Size = new Size(329, 451);
|
||||
contextMenuStrip.Size = new Size(329, 418);
|
||||
contextMenuStrip.Closed += contextMenuStrip_Closed;
|
||||
contextMenuStrip.Opening += contextMenuStrip_Opening;
|
||||
//
|
||||
@@ -178,14 +178,6 @@
|
||||
toolStripSeparator3.Name = "toolStripSeparator3";
|
||||
toolStripSeparator3.Size = new Size(325, 6);
|
||||
//
|
||||
// toolStripMenuItem_SelectAll
|
||||
//
|
||||
toolStripMenuItem_SelectAll.Name = "toolStripMenuItem_SelectAll";
|
||||
toolStripMenuItem_SelectAll.ShortcutKeys = Keys.Control | Keys.A;
|
||||
toolStripMenuItem_SelectAll.Size = new Size(328, 30);
|
||||
toolStripMenuItem_SelectAll.Text = "全选";
|
||||
toolStripMenuItem_SelectAll.Click += toolStripMenuItem_SelectAll_Click;
|
||||
//
|
||||
// toolStripMenuItem_CopyPreview
|
||||
//
|
||||
toolStripMenuItem_CopyPreview.Name = "toolStripMenuItem_CopyPreview";
|
||||
@@ -194,6 +186,22 @@
|
||||
toolStripMenuItem_CopyPreview.Text = "复制预览图 (256x256)";
|
||||
toolStripMenuItem_CopyPreview.Click += toolStripMenuItem_CopyPreview_Click;
|
||||
//
|
||||
// toolStripMenuItem_AddFromClipboard
|
||||
//
|
||||
toolStripMenuItem_AddFromClipboard.Name = "toolStripMenuItem_AddFromClipboard";
|
||||
toolStripMenuItem_AddFromClipboard.ShortcutKeys = Keys.Control | Keys.V;
|
||||
toolStripMenuItem_AddFromClipboard.Size = new Size(328, 30);
|
||||
toolStripMenuItem_AddFromClipboard.Text = "从剪贴板添加";
|
||||
toolStripMenuItem_AddFromClipboard.Click += toolStripMenuItem_AddFromClipboard_Click;
|
||||
//
|
||||
// toolStripMenuItem_SelectAll
|
||||
//
|
||||
toolStripMenuItem_SelectAll.Name = "toolStripMenuItem_SelectAll";
|
||||
toolStripMenuItem_SelectAll.ShortcutKeys = Keys.Control | Keys.A;
|
||||
toolStripMenuItem_SelectAll.Size = new Size(328, 30);
|
||||
toolStripMenuItem_SelectAll.Text = "全选";
|
||||
toolStripMenuItem_SelectAll.Click += toolStripMenuItem_SelectAll_Click;
|
||||
//
|
||||
// toolStripSeparator4
|
||||
//
|
||||
toolStripSeparator4.Name = "toolStripSeparator4";
|
||||
@@ -242,14 +250,6 @@
|
||||
imageList_SmallIcon.ImageSize = new Size(48, 48);
|
||||
imageList_SmallIcon.TransparentColor = Color.Transparent;
|
||||
//
|
||||
// toolStripMenuItem_AddFromClipboard
|
||||
//
|
||||
toolStripMenuItem_AddFromClipboard.Name = "toolStripMenuItem_AddFromClipboard";
|
||||
toolStripMenuItem_AddFromClipboard.ShortcutKeys = Keys.Control | Keys.V;
|
||||
toolStripMenuItem_AddFromClipboard.Size = new Size(328, 30);
|
||||
toolStripMenuItem_AddFromClipboard.Text = "从剪贴板添加";
|
||||
toolStripMenuItem_AddFromClipboard.Click += toolStripMenuItem_AddFromClipboard_Click;
|
||||
//
|
||||
// SpineListView
|
||||
//
|
||||
AutoScaleDimensions = new SizeF(11F, 24F);
|
||||
|
||||
@@ -12,17 +12,11 @@ using SpineViewer.Spine;
|
||||
using System.Reflection;
|
||||
using System.Diagnostics;
|
||||
using System.Collections.Specialized;
|
||||
|
||||
using NLog;
|
||||
namespace SpineViewer.Controls
|
||||
{
|
||||
public partial class SpineListView : UserControl
|
||||
{
|
||||
/// <summary>
|
||||
/// 显示骨骼信息的属性面板
|
||||
/// </summary>
|
||||
[Category("自定义"), Description("用于显示骨骼属性的属性页")]
|
||||
public PropertyGrid? PropertyGrid { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Spine 列表只读视图, 访问时必须使用 lock 语句锁定视图本身
|
||||
/// </summary>
|
||||
@@ -33,12 +27,23 @@ namespace SpineViewer.Controls
|
||||
/// </summary>
|
||||
private readonly List<Spine.Spine> spines = [];
|
||||
|
||||
/// <summary>
|
||||
/// 日志器
|
||||
/// </summary>
|
||||
protected readonly Logger logger = LogManager.GetCurrentClassLogger();
|
||||
|
||||
public SpineListView()
|
||||
{
|
||||
InitializeComponent();
|
||||
Spines = spines.AsReadOnly();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 显示骨骼信息的属性面板
|
||||
/// </summary>
|
||||
[Category("自定义"), Description("用于显示骨骼属性的属性页")]
|
||||
public PropertyGrid? PropertyGrid { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 选中的索引
|
||||
/// </summary>
|
||||
@@ -88,12 +93,12 @@ namespace SpineViewer.Controls
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Program.Logger.Error(ex.ToString());
|
||||
Program.Logger.Error("Failed to load {} {}", result.SkelPath, result.AtlasPath);
|
||||
logger.Error(ex.ToString());
|
||||
logger.Error("Failed to load {} {}", result.SkelPath, result.AtlasPath);
|
||||
MessageBox.Error(ex.ToString(), "骨骼加载失败");
|
||||
}
|
||||
|
||||
Program.LogCurrentMemoryUsage();
|
||||
logger.LogCurrentProcessMemoryUsage();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -110,7 +115,7 @@ namespace SpineViewer.Controls
|
||||
/// <summary>
|
||||
/// 从结果批量添加
|
||||
/// </summary>
|
||||
public void BatchAdd(Dialogs.BatchOpenSpineDialogResult result)
|
||||
private void BatchAdd(Dialogs.BatchOpenSpineDialogResult result)
|
||||
{
|
||||
var progressDialog = new Dialogs.ProgressDialog();
|
||||
progressDialog.DoWork += BatchAdd_Work;
|
||||
@@ -158,24 +163,30 @@ namespace SpineViewer.Controls
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Program.Logger.Error(ex.ToString());
|
||||
Program.Logger.Error("Failed to load {}", skelPath);
|
||||
logger.Error(ex.ToString());
|
||||
logger.Error("Failed to load {}", skelPath);
|
||||
error++;
|
||||
}
|
||||
|
||||
worker.ReportProgress((int)((i + 1) * 100.0) / totalCount, $"已处理 {i + 1}/{totalCount}");
|
||||
}
|
||||
|
||||
if (error > 0)
|
||||
// 选中最后一项
|
||||
listView.Invoke(() =>
|
||||
{
|
||||
Program.Logger.Warn("Batch load {} successfully, {} failed", success, error);
|
||||
}
|
||||
else
|
||||
{
|
||||
Program.Logger.Info("{} skel loaded successfully", success);
|
||||
}
|
||||
if (listView.Items.Count > 0)
|
||||
{
|
||||
listView.SelectedIndices.Clear();
|
||||
listView.SelectedIndices.Add(listView.Items.Count - 1);
|
||||
}
|
||||
});
|
||||
|
||||
Program.LogCurrentMemoryUsage();
|
||||
if (error > 0)
|
||||
logger.Warn("Batch load {} successfully, {} failed", success, error);
|
||||
else
|
||||
logger.Info("{} skel loaded successfully", success);
|
||||
|
||||
logger.LogCurrentProcessMemoryUsage();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -188,14 +199,14 @@ namespace SpineViewer.Controls
|
||||
{
|
||||
if (File.Exists(path))
|
||||
{
|
||||
if (Spine.Spine.CommonSkelSuffix.Contains(Path.GetExtension(path).ToLower()))
|
||||
if (SpineHelper.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()))
|
||||
if (SpineHelper.CommonSkelSuffix.Contains(Path.GetExtension(file).ToLower()))
|
||||
validPaths.Add(file);
|
||||
}
|
||||
}
|
||||
@@ -208,11 +219,11 @@ namespace SpineViewer.Controls
|
||||
if (MessageBox.Quest($"共发现 {validPaths.Count} 个可加载骨骼,数量较多,是否一次性全部加载?") == DialogResult.Cancel)
|
||||
return;
|
||||
}
|
||||
BatchAdd(new Dialogs.BatchOpenSpineDialogResult(Spine.Version.Auto, validPaths.ToArray()));
|
||||
BatchAdd(new Dialogs.BatchOpenSpineDialogResult(SpineVersion.Auto, validPaths.ToArray()));
|
||||
}
|
||||
else if (validPaths.Count > 0)
|
||||
{
|
||||
Insert(new Dialogs.OpenSpineDialogResult(Spine.Version.Auto, validPaths[0]));
|
||||
Insert(new Dialogs.OpenSpineDialogResult(SpineVersion.Auto, validPaths[0]));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -495,13 +506,16 @@ namespace SpineViewer.Controls
|
||||
private void toolStripMenuItem_CopyPreview_Click(object sender, EventArgs e)
|
||||
{
|
||||
var fileDropList = new StringCollection();
|
||||
var tempDir = Path.Combine(Path.GetTempPath(), Process.GetCurrentProcess().ProcessName);
|
||||
Directory.CreateDirectory(tempDir);
|
||||
|
||||
lock (Spines)
|
||||
{
|
||||
foreach (int i in listView.SelectedIndices)
|
||||
{
|
||||
var a = Process.GetCurrentProcess();
|
||||
var spine = spines[i];
|
||||
var path = Path.Combine(Program.TempDir, $"{spine.ID}.png");
|
||||
var path = Path.Combine(tempDir, $"{spine.ID}.png");
|
||||
spine.Preview.Save(path);
|
||||
fileDropList.Add(path);
|
||||
}
|
||||
|
||||
@@ -54,32 +54,32 @@ namespace SpineViewer.Controls
|
||||
private class PreviewerProperty(SpinePreviewer previewer)
|
||||
{
|
||||
[TypeConverter(typeof(SizeConverter))]
|
||||
[Category("导出"), DisplayName("分辨率")]
|
||||
[Category("[0] 导出"), DisplayName("分辨率")]
|
||||
public Size Resolution { get => previewer.Resolution; set => previewer.Resolution = value; }
|
||||
|
||||
[TypeConverter(typeof(PointFConverter))]
|
||||
[Category("导出"), DisplayName("画面中心点")]
|
||||
[Category("[0] 导出"), DisplayName("画面中心点")]
|
||||
public PointF Center { get => previewer.Center; set => previewer.Center = value; }
|
||||
|
||||
[Category("导出"), DisplayName("缩放")]
|
||||
[Category("[0] 导出"), DisplayName("缩放")]
|
||||
public float Zoom { get => previewer.Zoom; set => previewer.Zoom = value; }
|
||||
|
||||
[Category("导出"), DisplayName("旋转")]
|
||||
[Category("[0] 导出"), DisplayName("旋转")]
|
||||
public float Rotation { get => previewer.Rotation; set => previewer.Rotation = value; }
|
||||
|
||||
[Category("导出"), DisplayName("水平翻转")]
|
||||
[Category("[0] 导出"), DisplayName("水平翻转")]
|
||||
public bool FlipX { get => previewer.FlipX; set => previewer.FlipX = value; }
|
||||
|
||||
[Category("导出"), DisplayName("垂直翻转")]
|
||||
[Category("[0] 导出"), DisplayName("垂直翻转")]
|
||||
public bool FlipY { get => previewer.FlipY; set => previewer.FlipY = value; }
|
||||
|
||||
[Category("导出"), DisplayName("仅渲染选中")]
|
||||
[Category("[0] 导出"), DisplayName("仅渲染选中")]
|
||||
public bool RenderSelectedOnly { get => previewer.RenderSelectedOnly; set => previewer.RenderSelectedOnly = value; }
|
||||
|
||||
[Category("预览"), DisplayName("显示坐标轴")]
|
||||
[Category("[1] 预览"), DisplayName("显示坐标轴")]
|
||||
public bool ShowAxis { get => previewer.ShowAxis; set => previewer.ShowAxis = value; }
|
||||
|
||||
[Category("预览"), DisplayName("最大帧率")]
|
||||
[Category("[1] 预览"), DisplayName("最大帧率")]
|
||||
public uint MaxFps { get => previewer.MaxFps; set => previewer.MaxFps = value; }
|
||||
}
|
||||
|
||||
@@ -371,6 +371,16 @@ namespace SpineViewer.Controls
|
||||
delta = Clock.ElapsedTime.AsSeconds();
|
||||
Clock.Restart();
|
||||
|
||||
// 停止更新的时候只是时间不前进, 但是坐标变换还是要更新, 否则无法移动对象
|
||||
if (!IsUpdating) delta = 0;
|
||||
|
||||
// 加上要快进的量
|
||||
lock (_forwardDeltaLock)
|
||||
{
|
||||
delta += forwardDelta;
|
||||
forwardDelta = 0;
|
||||
}
|
||||
|
||||
RenderWindow.Clear(BackgroundColor);
|
||||
|
||||
if (ShowAxis)
|
||||
@@ -389,24 +399,14 @@ namespace SpineViewer.Controls
|
||||
{
|
||||
lock (SpineListView.Spines)
|
||||
{
|
||||
var spines = SpineListView.Spines;
|
||||
for (int i = spines.Count - 1; i >= 0; i--)
|
||||
var spines = SpineListView.Spines.Where(sp => !sp.IsHidden).ToArray();
|
||||
for (int i = spines.Length - 1; i >= 0; i--)
|
||||
{
|
||||
if (cancelToken is not null && cancelToken.IsCancellationRequested)
|
||||
break; // 提前中止
|
||||
|
||||
var spine = spines[i];
|
||||
|
||||
// 停止更新的时候只是时间不前进, 但是坐标变换还是要更新, 否则无法移动对象
|
||||
if (!IsUpdating) delta = 0;
|
||||
|
||||
// 加上要快进的量
|
||||
lock (_forwardDeltaLock)
|
||||
{
|
||||
delta += forwardDelta;
|
||||
forwardDelta = 0;
|
||||
}
|
||||
|
||||
spine.Update(delta);
|
||||
|
||||
if (RenderSelectedOnly && !spine.IsSelected)
|
||||
@@ -487,9 +487,11 @@ namespace SpineViewer.Controls
|
||||
// 仅渲染选中模式禁止在画面里选择对象
|
||||
if (RenderSelectedOnly)
|
||||
{
|
||||
// 只在被选中的对象里判断是否有效命中
|
||||
bool hit = false;
|
||||
foreach (int i in SpineListView.SelectedIndices)
|
||||
{
|
||||
if (spines[i].IsHidden) continue;
|
||||
if (!spines[i].Bounds.Contains(src)) continue;
|
||||
hit = true;
|
||||
break;
|
||||
@@ -500,12 +502,13 @@ namespace SpineViewer.Controls
|
||||
}
|
||||
else
|
||||
{
|
||||
// 没有按下 Ctrl 键就只选中点击的那个, 所以先清空选中列表
|
||||
if ((ModifierKeys & Keys.Control) == 0)
|
||||
{
|
||||
// 没按 Ctrl 的情况下, 如果命中了已选中对象, 则就算普通命中
|
||||
bool hit = false;
|
||||
for (int i = 0; i < spines.Count; i++)
|
||||
{
|
||||
if (spines[i].IsHidden) continue;
|
||||
if (!spines[i].Bounds.Contains(src)) continue;
|
||||
|
||||
hit = true;
|
||||
@@ -524,10 +527,11 @@ namespace SpineViewer.Controls
|
||||
}
|
||||
else
|
||||
{
|
||||
// 按下 Ctrl 的情况就执行多选, 并且点空白处也不会清空选中
|
||||
for (int i = 0; i < spines.Count; i++)
|
||||
{
|
||||
if (!spines[i].Bounds.Contains(src))
|
||||
continue;
|
||||
if (spines[i].IsHidden) continue;
|
||||
if (!spines[i].Bounds.Contains(src)) continue;
|
||||
|
||||
SpineListView.SelectedIndices.Add(i);
|
||||
break;
|
||||
@@ -558,8 +562,12 @@ namespace SpineViewer.Controls
|
||||
{
|
||||
lock (SpineListView.Spines)
|
||||
{
|
||||
var spines = SpineListView.Spines;
|
||||
foreach (int i in SpineListView.SelectedIndices)
|
||||
SpineListView.Spines[i].Position += delta;
|
||||
{
|
||||
if (spines[i].IsHidden) continue;
|
||||
spines[i].Position += delta;
|
||||
}
|
||||
}
|
||||
}
|
||||
draggingSrc = dst;
|
||||
@@ -599,7 +607,7 @@ namespace SpineViewer.Controls
|
||||
lock (SpineListView.Spines)
|
||||
{
|
||||
foreach (var spine in SpineListView.Spines)
|
||||
spine.CurrentAnimation = spine.CurrentAnimation;
|
||||
spine.Track0Animation = spine.Track0Animation; // TODO: 多轨道重置
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -611,7 +619,7 @@ namespace SpineViewer.Controls
|
||||
lock (SpineListView.Spines)
|
||||
{
|
||||
foreach (var spine in SpineListView.Spines)
|
||||
spine.CurrentAnimation = spine.CurrentAnimation;
|
||||
spine.Track0Animation = spine.Track0Animation; // TODO: 多轨道重置
|
||||
}
|
||||
}
|
||||
IsUpdating = true;
|
||||
|
||||
@@ -15,12 +15,20 @@ namespace SpineViewer.Dialogs
|
||||
public AboutDialog()
|
||||
{
|
||||
InitializeComponent();
|
||||
Text = $"关于 {Program.Name}";
|
||||
Text = $"关于 {ProgramName}";
|
||||
label_Version.Text = $"v{InformationalVersion}";
|
||||
}
|
||||
|
||||
public string InformationalVersion =>
|
||||
Assembly.GetExecutingAssembly().GetCustomAttribute<AssemblyInformationalVersionAttribute>()?.InformationalVersion;
|
||||
public string ProgramName => Process.GetCurrentProcess().ProcessName;
|
||||
|
||||
public string InformationalVersion
|
||||
=> Assembly.GetExecutingAssembly().GetCustomAttribute<AssemblyInformationalVersionAttribute>()?.InformationalVersion;
|
||||
|
||||
public string ProgramUrl
|
||||
{
|
||||
get => linkLabel_RepoUrl.Text;
|
||||
set => linkLabel_RepoUrl.Text = value;
|
||||
}
|
||||
|
||||
private void linkLabel_RepoUrl_LinkClicked(object sender, LinkLabelLinkClickedEventArgs e)
|
||||
{
|
||||
|
||||
@@ -21,15 +21,15 @@ namespace SpineViewer.Dialogs
|
||||
public BatchOpenSpineDialog()
|
||||
{
|
||||
InitializeComponent();
|
||||
comboBox_Version.DataSource = VersionHelper.Names.ToList();
|
||||
comboBox_Version.DataSource = SpineHelper.Names.ToList();
|
||||
comboBox_Version.DisplayMember = "Value";
|
||||
comboBox_Version.ValueMember = "Key";
|
||||
comboBox_Version.SelectedValue = Spine.Version.Auto;
|
||||
comboBox_Version.SelectedValue = SpineVersion.Auto;
|
||||
}
|
||||
|
||||
private void button_Ok_Click(object sender, EventArgs e)
|
||||
{
|
||||
var version = (Spine.Version)comboBox_Version.SelectedValue;
|
||||
var version = (SpineVersion)comboBox_Version.SelectedValue;
|
||||
|
||||
var items = skelFileListBox.Items;
|
||||
|
||||
@@ -48,7 +48,7 @@ namespace SpineViewer.Dialogs
|
||||
}
|
||||
}
|
||||
|
||||
if (version != Spine.Version.Auto && !Spine.Spine.ImplementedVersions.Contains(version))
|
||||
if (version != SpineVersion.Auto && !Spine.Spine.HasImplementation(version))
|
||||
{
|
||||
MessageBox.Info($"{version.GetName()} 版本尚未实现(咕咕咕~)");
|
||||
return;
|
||||
@@ -67,12 +67,12 @@ namespace SpineViewer.Dialogs
|
||||
/// <summary>
|
||||
/// 批量打开对话框结果
|
||||
/// </summary>
|
||||
public class BatchOpenSpineDialogResult(Spine.Version version, string[] skelPaths)
|
||||
public class BatchOpenSpineDialogResult(SpineVersion version, string[] skelPaths)
|
||||
{
|
||||
/// <summary>
|
||||
/// 版本
|
||||
/// </summary>
|
||||
public Spine.Version Version => version;
|
||||
public SpineVersion Version => version;
|
||||
|
||||
/// <summary>
|
||||
/// 路径列表
|
||||
|
||||
@@ -44,7 +44,6 @@
|
||||
button_Cancel = new Button();
|
||||
label2 = new Label();
|
||||
skelFileListBox = new SpineViewer.Controls.SkelFileListBox();
|
||||
openFileDialog_Skel = new OpenFileDialog();
|
||||
panel.SuspendLayout();
|
||||
tableLayoutPanel1.SuspendLayout();
|
||||
flowLayoutPanel_TargetFormat.SuspendLayout();
|
||||
@@ -238,14 +237,6 @@
|
||||
skelFileListBox.Size = new Size(945, 264);
|
||||
skelFileListBox.TabIndex = 20;
|
||||
//
|
||||
// openFileDialog_Skel
|
||||
//
|
||||
openFileDialog_Skel.AddExtension = false;
|
||||
openFileDialog_Skel.AddToRecent = false;
|
||||
openFileDialog_Skel.Filter = "skel 文件 (*.skel; *.json)|*.skel;*.json|二进制文件 (*.skel)|*.skel|文本文件 (*.json)|*.json|所有文件 (*.*)|*.*";
|
||||
openFileDialog_Skel.Multiselect = true;
|
||||
openFileDialog_Skel.Title = "批量选择skel文件";
|
||||
//
|
||||
// ConvertFileFormatDialog
|
||||
//
|
||||
AcceptButton = button_Ok;
|
||||
@@ -281,7 +272,6 @@
|
||||
private TableLayoutPanel tableLayoutPanel2;
|
||||
private Button button_Ok;
|
||||
private Button button_Cancel;
|
||||
private OpenFileDialog openFileDialog_Skel;
|
||||
private Label label1;
|
||||
private Label label2;
|
||||
private FlowLayoutPanel flowLayoutPanel_TargetFormat;
|
||||
|
||||
@@ -22,24 +22,24 @@ namespace SpineViewer.Dialogs
|
||||
{
|
||||
InitializeComponent();
|
||||
|
||||
comboBox_SourceVersion.DataSource = VersionHelper.Names.ToList();
|
||||
comboBox_SourceVersion.DataSource = SpineHelper.Names.ToList();
|
||||
comboBox_SourceVersion.DisplayMember = "Value";
|
||||
comboBox_SourceVersion.ValueMember = "Key";
|
||||
comboBox_SourceVersion.SelectedValue = Spine.Version.Auto;
|
||||
comboBox_SourceVersion.SelectedValue = SpineVersion.Auto;
|
||||
|
||||
// 目标版本不包含自动
|
||||
var versionsWithoutAuto = VersionHelper.Names.ToDictionary();
|
||||
versionsWithoutAuto.Remove(Spine.Version.Auto);
|
||||
var versionsWithoutAuto = SpineHelper.Names.ToDictionary();
|
||||
versionsWithoutAuto.Remove(SpineVersion.Auto);
|
||||
comboBox_TargetVersion.DataSource = versionsWithoutAuto.ToList();
|
||||
comboBox_TargetVersion.DisplayMember = "Value";
|
||||
comboBox_TargetVersion.ValueMember = "Key";
|
||||
comboBox_TargetVersion.SelectedValue = Spine.Version.V38;
|
||||
comboBox_TargetVersion.SelectedValue = SpineVersion.V38;
|
||||
}
|
||||
|
||||
private void button_Ok_Click(object sender, EventArgs e)
|
||||
{
|
||||
var sourceVersion = (Spine.Version)comboBox_SourceVersion.SelectedValue;
|
||||
var targetVersion = (Spine.Version)comboBox_TargetVersion.SelectedValue;
|
||||
var sourceVersion = (SpineVersion)comboBox_SourceVersion.SelectedValue;
|
||||
var targetVersion = (SpineVersion)comboBox_TargetVersion.SelectedValue;
|
||||
var jsonTarget = radioButton_JsonTarget.Checked;
|
||||
|
||||
var items = skelFileListBox.Items;
|
||||
@@ -59,13 +59,13 @@ namespace SpineViewer.Dialogs
|
||||
}
|
||||
}
|
||||
|
||||
if (sourceVersion != Spine.Version.Auto && !SkeletonConverter.ImplementedVersions.Contains(sourceVersion))
|
||||
if (sourceVersion != SpineVersion.Auto && !SkeletonConverter.HasImplementation(sourceVersion))
|
||||
{
|
||||
MessageBox.Info($"{sourceVersion.GetName()} 版本尚未实现(咕咕咕~)");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!SkeletonConverter.ImplementedVersions.Contains(targetVersion))
|
||||
if (!SkeletonConverter.HasImplementation(targetVersion))
|
||||
{
|
||||
MessageBox.Info($"{targetVersion.GetName()} 版本尚未实现(咕咕咕~)");
|
||||
return;
|
||||
@@ -84,7 +84,7 @@ namespace SpineViewer.Dialogs
|
||||
/// <summary>
|
||||
/// 文件格式转换对话框结果包装类
|
||||
/// </summary>
|
||||
public class ConvertFileFormatDialogResult(string[] skelPaths, Spine.Version sourceVersion, Spine.Version targetVersion, bool jsonTarget)
|
||||
public class ConvertFileFormatDialogResult(string[] skelPaths, SpineVersion sourceVersion, SpineVersion targetVersion, bool jsonTarget)
|
||||
{
|
||||
/// <summary>
|
||||
/// 骨骼文件路径列表
|
||||
@@ -94,12 +94,12 @@ namespace SpineViewer.Dialogs
|
||||
/// <summary>
|
||||
/// 源版本
|
||||
/// </summary>
|
||||
public Spine.Version SourceVersion => sourceVersion;
|
||||
public SpineVersion SourceVersion => sourceVersion;
|
||||
|
||||
/// <summary>
|
||||
/// 目标版本
|
||||
/// </summary>
|
||||
public Spine.Version TargetVersion => targetVersion;
|
||||
public SpineVersion TargetVersion => targetVersion;
|
||||
|
||||
/// <summary>
|
||||
/// 目标格式是否为 Json
|
||||
|
||||
@@ -117,9 +117,6 @@
|
||||
<resheader name="writer">
|
||||
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
||||
</resheader>
|
||||
<metadata name="openFileDialog_Skel.TrayLocation" type="System.Drawing.Point, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a">
|
||||
<value>92, 26</value>
|
||||
</metadata>
|
||||
<assembly alias="System.Drawing" name="System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a" />
|
||||
<data name="$this.Icon" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
|
||||
<value>
|
||||
|
||||
@@ -39,7 +39,7 @@ namespace SpineViewer.Dialogs
|
||||
PropertyInfo? labelProp = category.GetType().GetProperty("Label", BindingFlags.Instance | BindingFlags.Public);
|
||||
if (labelProp == null) continue;
|
||||
string? label = labelProp.GetValue(category) as string;
|
||||
if (label != "导出") continue;
|
||||
if (label != "[0] 导出") continue;
|
||||
|
||||
// 获取该分组下的所有属性项
|
||||
PropertyInfo? gridItemsProp = category.GetType().GetProperty("GridItems", BindingFlags.Instance | BindingFlags.Public);
|
||||
|
||||
@@ -20,10 +20,10 @@ namespace SpineViewer.Dialogs
|
||||
public OpenSpineDialog()
|
||||
{
|
||||
InitializeComponent();
|
||||
comboBox_Version.DataSource = VersionHelper.Names.ToList();
|
||||
comboBox_Version.DataSource = SpineHelper.Names.ToList();
|
||||
comboBox_Version.DisplayMember = "Value";
|
||||
comboBox_Version.ValueMember = "Key";
|
||||
comboBox_Version.SelectedValue = Spine.Version.Auto;
|
||||
comboBox_Version.SelectedValue = SpineVersion.Auto;
|
||||
}
|
||||
|
||||
private void OpenSpineDialog_Load(object sender, EventArgs e)
|
||||
@@ -53,7 +53,7 @@ namespace SpineViewer.Dialogs
|
||||
{
|
||||
var skelPath = textBox_SkelPath.Text;
|
||||
var atlasPath = textBox_AtlasPath.Text;
|
||||
var version = (Spine.Version)comboBox_Version.SelectedValue;
|
||||
var version = (SpineVersion)comboBox_Version.SelectedValue;
|
||||
|
||||
if (!File.Exists(skelPath))
|
||||
{
|
||||
@@ -79,7 +79,7 @@ namespace SpineViewer.Dialogs
|
||||
atlasPath = Path.GetFullPath(atlasPath);
|
||||
}
|
||||
|
||||
if (version != Spine.Version.Auto && !Spine.Spine.ImplementedVersions.Contains(version))
|
||||
if (version != SpineVersion.Auto && !Spine.Spine.HasImplementation(version))
|
||||
{
|
||||
MessageBox.Info($"{version.GetName()} 版本尚未实现(咕咕咕~)");
|
||||
return;
|
||||
@@ -98,12 +98,12 @@ namespace SpineViewer.Dialogs
|
||||
/// <summary>
|
||||
/// 打开骨骼对话框结果
|
||||
/// </summary>
|
||||
public class OpenSpineDialogResult(Spine.Version version, string skelPath, string? atlasPath = null)
|
||||
public class OpenSpineDialogResult(SpineVersion version, string skelPath, string? atlasPath = null)
|
||||
{
|
||||
/// <summary>
|
||||
/// 版本
|
||||
/// </summary>
|
||||
public Spine.Version Version => version;
|
||||
public SpineVersion Version => version;
|
||||
|
||||
/// <summary>
|
||||
/// skel 文件路径
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using System;
|
||||
using NLog;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using System.Data;
|
||||
@@ -12,6 +13,13 @@ namespace SpineViewer.Dialogs
|
||||
{
|
||||
public partial class ProgressDialog : Form
|
||||
{
|
||||
private Logger logger = LogManager.GetCurrentClassLogger();
|
||||
|
||||
public ProgressDialog()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// BackgroundWorker.DoWork 接口暴露
|
||||
/// </summary>
|
||||
@@ -32,11 +40,6 @@ namespace SpineViewer.Dialogs
|
||||
/// </summary>
|
||||
public void RunWorkerAsync(object? argument) => backgroundWorker.RunWorkerAsync(argument);
|
||||
|
||||
public ProgressDialog()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
|
||||
private void backgroundWorker_ProgressChanged(object sender, ProgressChangedEventArgs e)
|
||||
{
|
||||
label_Tip.Text = e.UserState as string;
|
||||
@@ -47,7 +50,7 @@ namespace SpineViewer.Dialogs
|
||||
{
|
||||
if (e.Error != null)
|
||||
{
|
||||
Program.Logger.Error(e.Error.ToString());
|
||||
logger.Error(e.Error.ToString());
|
||||
MessageBox.Error(e.Error.ToString(), "执行出错");
|
||||
DialogResult = DialogResult.Abort;
|
||||
}
|
||||
|
||||
@@ -14,40 +14,18 @@ namespace SpineViewer.Exporter
|
||||
/// <summary>
|
||||
/// 导出参数基类
|
||||
/// </summary>
|
||||
public abstract class ExportArgs
|
||||
public abstract class ExportArgs : ImplementationResolver<ExportArgs, ExportImplementationAttribute, ExportType>
|
||||
{
|
||||
/// <summary>
|
||||
/// 实现类缓存
|
||||
/// </summary>
|
||||
private static readonly Dictionary<ExportType, Type> ImplementationTypes = [];
|
||||
|
||||
static ExportArgs()
|
||||
{
|
||||
var impTypes = Assembly.GetExecutingAssembly().GetTypes().Where(t => typeof(ExportArgs).IsAssignableFrom(t) && !t.IsAbstract);
|
||||
foreach (var type in impTypes)
|
||||
{
|
||||
var attr = type.GetCustomAttribute<ExportImplementationAttribute>();
|
||||
if (attr is not null)
|
||||
{
|
||||
if (ImplementationTypes.ContainsKey(attr.ExportType))
|
||||
throw new InvalidOperationException($"Multiple implementations found: {attr.ExportType}");
|
||||
ImplementationTypes[attr.ExportType] = type;
|
||||
}
|
||||
}
|
||||
Program.Logger.Debug("Find export args implementations: [{}]", string.Join(", ", ImplementationTypes.Keys));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 创建指定类型导出参数
|
||||
/// </summary>
|
||||
/// <param name="exportType">导出类型</param>
|
||||
/// <param name="resolution">分辨率</param>
|
||||
/// <param name="view">导出视图</param>
|
||||
/// <param name="renderSelectedOnly">仅渲染选中</param>
|
||||
/// <returns>返回与指定 <paramref name="exportType"/> 匹配的导出参数实例</returns>
|
||||
public static ExportArgs New(ExportType exportType, Size resolution, SFML.Graphics.View view, bool renderSelectedOnly)
|
||||
{
|
||||
if (!ImplementationTypes.TryGetValue(exportType, out var type))
|
||||
{
|
||||
throw new NotImplementedException($"Not implemented type: {exportType}");
|
||||
}
|
||||
return (ExportArgs)Activator.CreateInstance(type, resolution, view, renderSelectedOnly);
|
||||
}
|
||||
=> New(exportType, [resolution, view, renderSelectedOnly]);
|
||||
|
||||
public ExportArgs(Size resolution, SFML.Graphics.View view, bool renderSelectedOnly)
|
||||
{
|
||||
@@ -60,34 +38,42 @@ namespace SpineViewer.Exporter
|
||||
/// 输出文件夹
|
||||
/// </summary>
|
||||
[Editor(typeof(FolderNameEditor), typeof(UITypeEditor))]
|
||||
[Category("导出"), DisplayName("输出文件夹"), Description("逐个导出时可以留空,将逐个导出到模型自身所在目录")]
|
||||
[Category("[0] 导出"), DisplayName("输出文件夹"), Description("逐个导出时可以留空,将逐个导出到模型自身所在目录")]
|
||||
public string? OutputDir { get; set; } = null;
|
||||
|
||||
/// <summary>
|
||||
/// 导出单个
|
||||
/// </summary>
|
||||
[Category("导出"), DisplayName("导出单个"), Description("是否将模型在同一个画面上导出单个文件,否则逐个导出模型")]
|
||||
[Category("[0] 导出"), DisplayName("导出单个"), Description("是否将模型在同一个画面上导出单个文件,否则逐个导出模型")]
|
||||
public bool ExportSingle { get; set; } = false;
|
||||
|
||||
/// <summary>
|
||||
/// 画面分辨率
|
||||
/// </summary>
|
||||
[TypeConverter(typeof(SizeConverter))]
|
||||
[Category("导出"), DisplayName("分辨率"), Description("画面的宽高像素大小,请在预览画面参数面板进行调整")]
|
||||
[Category("[0] 导出"), DisplayName("分辨率"), Description("画面的宽高像素大小,请在预览画面参数面板进行调整")]
|
||||
public Size Resolution { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 渲染视窗
|
||||
/// </summary>
|
||||
[Category("导出"), DisplayName("视图"), Description("画面的视图参数,请在预览画面参数面板进行调整")]
|
||||
[Category("[0] 导出"), DisplayName("视图"), Description("画面的视图参数,请在预览画面参数面板进行调整")]
|
||||
public SFML.Graphics.View View { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 是否仅渲染选中
|
||||
/// </summary>
|
||||
[Category("导出"), DisplayName("仅渲染选中"), Description("是否仅导出选中的模型,请在预览画面参数面板进行调整")]
|
||||
[Category("[0] 导出"), DisplayName("仅渲染选中"), Description("是否仅导出选中的模型,请在预览画面参数面板进行调整")]
|
||||
public bool RenderSelectedOnly { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 背景颜色
|
||||
/// </summary>
|
||||
[Editor(typeof(SFMLColorEditor), typeof(UITypeEditor))]
|
||||
[TypeConverter(typeof(SFMLColorConverter))]
|
||||
[Category("[0] 导出"), DisplayName("背景颜色"), Description("要使用的背景色, 格式为 #RRGGBBAA")]
|
||||
public SFML.Graphics.Color BackgroundColor { get; set; } = SFML.Graphics.Color.Transparent;
|
||||
|
||||
/// <summary>
|
||||
/// 检查参数是否合法并规范化参数值, 否则返回用户错误原因
|
||||
/// </summary>
|
||||
|
||||
@@ -26,14 +26,9 @@ namespace SpineViewer.Exporter
|
||||
/// 导出实现类标记
|
||||
/// </summary>
|
||||
[AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = false)]
|
||||
public class ExportImplementationAttribute : Attribute
|
||||
public class ExportImplementationAttribute(ExportType exportType) : Attribute, IImplementationKey<ExportType>
|
||||
{
|
||||
public ExportType ExportType { get; }
|
||||
|
||||
public ExportImplementationAttribute(ExportType exportType)
|
||||
{
|
||||
ExportType = exportType;
|
||||
}
|
||||
public ExportType ImplementationKey { get; private set; } = exportType;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using SpineViewer.Spine;
|
||||
using NLog;
|
||||
using SpineViewer.Spine;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
@@ -6,63 +7,36 @@ using System.Linq;
|
||||
using System.Reflection;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using static System.Windows.Forms.VisualStyles.VisualStyleElement.StartPanel;
|
||||
|
||||
namespace SpineViewer.Exporter
|
||||
{
|
||||
/// <summary>
|
||||
/// 导出器基类
|
||||
/// </summary>
|
||||
public abstract class Exporter
|
||||
public abstract class Exporter(ExportArgs exportArgs) : ImplementationResolver<Exporter, ExportImplementationAttribute, ExportType>
|
||||
{
|
||||
/// <summary>
|
||||
/// 实现类缓存
|
||||
/// 创建指定类型导出器
|
||||
/// </summary>
|
||||
private static readonly Dictionary<ExportType, Type> ImplementationTypes = [];
|
||||
|
||||
static Exporter()
|
||||
{
|
||||
var impTypes = Assembly.GetExecutingAssembly().GetTypes().Where(t => typeof(Exporter).IsAssignableFrom(t) && !t.IsAbstract);
|
||||
foreach (var type in impTypes)
|
||||
{
|
||||
var attr = type.GetCustomAttribute<ExportImplementationAttribute>();
|
||||
if (attr is not null)
|
||||
{
|
||||
if (ImplementationTypes.ContainsKey(attr.ExportType))
|
||||
throw new InvalidOperationException($"Multiple implementations found: {attr.ExportType}");
|
||||
ImplementationTypes[attr.ExportType] = type;
|
||||
}
|
||||
}
|
||||
Program.Logger.Debug("Find exporter implementations: [{}]", string.Join(", ", ImplementationTypes.Keys));
|
||||
}
|
||||
/// <param name="exportType">导出类型</param>
|
||||
/// <param name="exportArgs">与 <paramref name="exportType"/> 匹配的导出参数</param>
|
||||
/// <returns>与 <paramref name="exportType"/> 匹配的导出器</returns>
|
||||
public static Exporter New(ExportType exportType, ExportArgs exportArgs) => New(exportType, [exportArgs]);
|
||||
|
||||
/// <summary>
|
||||
/// 创建指定类型导出参数
|
||||
/// 日志器
|
||||
/// </summary>
|
||||
public static Exporter New(ExportType exportType, ExportArgs exportArgs)
|
||||
{
|
||||
if (!ImplementationTypes.TryGetValue(exportType, out var type))
|
||||
{
|
||||
throw new NotImplementedException($"Not implemented type: {exportType}");
|
||||
}
|
||||
return (Exporter)Activator.CreateInstance(type, exportArgs);
|
||||
}
|
||||
protected Logger logger = LogManager.GetCurrentClassLogger();
|
||||
|
||||
/// <summary>
|
||||
/// 导出参数
|
||||
/// </summary>
|
||||
public ExportArgs ExportArgs { get; }
|
||||
public ExportArgs ExportArgs { get; } = exportArgs;
|
||||
|
||||
/// <summary>
|
||||
/// 可用于文件名的时间戳字符串
|
||||
/// </summary>
|
||||
protected readonly string timestamp;
|
||||
|
||||
public Exporter(ExportArgs exportArgs)
|
||||
{
|
||||
ExportArgs = exportArgs;
|
||||
timestamp = DateTime.Now.ToString("yyMMddHHmmss");
|
||||
}
|
||||
protected readonly string timestamp = DateTime.Now.ToString("yyMMddHHmmss");
|
||||
|
||||
/// <summary>
|
||||
/// 获取供渲染的 SFML.Graphics.RenderTexture
|
||||
@@ -82,7 +56,7 @@ namespace SpineViewer.Exporter
|
||||
{
|
||||
// tex 必须临时创建, 随用随取, 防止出现跨线程的情况
|
||||
using var tex = GetRenderTexture();
|
||||
tex.Clear(SFML.Graphics.Color.Transparent);
|
||||
tex.Clear(ExportArgs.BackgroundColor);
|
||||
tex.Draw(spine);
|
||||
tex.Display();
|
||||
return new(tex.Texture.CopyToImage());
|
||||
@@ -95,7 +69,7 @@ namespace SpineViewer.Exporter
|
||||
{
|
||||
// tex 必须临时创建, 随用随取, 防止出现跨线程的情况
|
||||
using var tex = GetRenderTexture();
|
||||
tex.Clear(SFML.Graphics.Color.Transparent);
|
||||
tex.Clear(ExportArgs.BackgroundColor);
|
||||
foreach (var spine in spinesToRender) tex.Draw(spine);
|
||||
tex.Display();
|
||||
return new(tex.Texture.CopyToImage());
|
||||
@@ -123,7 +97,7 @@ namespace SpineViewer.Exporter
|
||||
if (ExportArgs.ExportSingle) ExportSingle(spinesToRender, worker);
|
||||
else ExportIndividual(spinesToRender, worker);
|
||||
|
||||
Program.LogCurrentMemoryUsage();
|
||||
logger.LogCurrentProcessMemoryUsage();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,7 +20,7 @@ namespace SpineViewer.Exporter.Implementations.ExportArgs
|
||||
/// 单帧画面格式
|
||||
/// </summary>
|
||||
[TypeConverter(typeof(ImageFormatConverter))]
|
||||
[Category("单帧画面"), DisplayName("图像格式")]
|
||||
[Category("[1] 单帧画面"), DisplayName("图像格式")]
|
||||
public ImageFormat ImageFormat
|
||||
{
|
||||
get => imageFormat;
|
||||
@@ -35,14 +35,14 @@ namespace SpineViewer.Exporter.Implementations.ExportArgs
|
||||
/// <summary>
|
||||
/// 文件名后缀
|
||||
/// </summary>
|
||||
[Category("单帧画面"), DisplayName("文件名后缀"), Description("与图像格式匹配的文件名后缀")]
|
||||
[Category("[1] 单帧画面"), DisplayName("文件名后缀"), Description("与图像格式匹配的文件名后缀")]
|
||||
public string FileSuffix { get => imageFormat.GetSuffix(); }
|
||||
|
||||
/// <summary>
|
||||
/// DPI
|
||||
/// </summary>
|
||||
[TypeConverter(typeof(SizeFConverter))]
|
||||
[Category("单帧画面"), DisplayName("DPI"), Description("导出图像的每英寸像素数,用于调整图像的物理尺寸")]
|
||||
[Category("[1] 单帧画面"), DisplayName("DPI"), Description("导出图像的每英寸像素数,用于调整图像的物理尺寸")]
|
||||
public SizeF DPI
|
||||
{
|
||||
get => dpi;
|
||||
|
||||
@@ -20,7 +20,7 @@ namespace SpineViewer.Exporter.Implementations.ExportArgs
|
||||
/// 文件名后缀
|
||||
/// </summary>
|
||||
[TypeConverter(typeof(SFMLImageFileSuffixConverter))]
|
||||
[Category("帧序列参数"), DisplayName("文件名后缀"), Description("帧文件的后缀,同时决定帧图像格式")]
|
||||
[Category("[2] 帧序列参数"), DisplayName("文件名后缀"), Description("帧文件的后缀,同时决定帧图像格式")]
|
||||
public string FileSuffix { get; set; } = ".png";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,21 +15,24 @@ namespace SpineViewer.Exporter.Implementations.ExportArgs
|
||||
{
|
||||
public GifExportArgs(Size resolution, SFML.Graphics.View view, bool renderSelectedOnly) : base(resolution, view, renderSelectedOnly)
|
||||
{
|
||||
// 给一个纯白的背景
|
||||
BackgroundColor = new(255, 255, 255, 0);
|
||||
|
||||
// GIF 的帧率不能太高, 超过 50 帧反而会变慢
|
||||
FPS = 25;
|
||||
FPS = 12;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 调色板最大颜色数量
|
||||
/// </summary>
|
||||
[Category("GIF 参数"), DisplayName("调色板最大颜色数量"), Description("设置调色板使用的最大颜色数量, 越多则色彩保留程度越高")]
|
||||
[Category("[2] GIF 参数"), DisplayName("调色板最大颜色数量"), Description("设置调色板使用的最大颜色数量, 越多则色彩保留程度越高")]
|
||||
public uint MaxColors { get => maxColors; set => maxColors = Math.Clamp(value, 2, 256); }
|
||||
private uint maxColors = 256;
|
||||
|
||||
/// <summary>
|
||||
/// 透明度阈值
|
||||
/// </summary>
|
||||
[Category("GIF 参数"), DisplayName("透明度阈值"), Description("小于该值的像素点会被认为是透明像素")]
|
||||
[Category("[2] GIF 参数"), DisplayName("透明度阈值"), Description("小于该值的像素点会被认为是透明像素")]
|
||||
public byte AlphaThreshold { get => alphaThreshold; set => alphaThreshold = value; }
|
||||
private byte alphaThreshold = 128;
|
||||
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace SpineViewer.Exporter.Implementations.ExportArgs
|
||||
{
|
||||
/// <summary>
|
||||
/// MP4 导出参数
|
||||
/// </summary>
|
||||
[ExportImplementation(ExportType.MP4)]
|
||||
public class Mp4ExportArgs : VideoExportArgs
|
||||
{
|
||||
public Mp4ExportArgs(Size resolution, SFML.Graphics.View view, bool renderSelectedOnly) : base(resolution, view, renderSelectedOnly)
|
||||
{
|
||||
// MP4 默认用绿幕
|
||||
BackgroundColor = new(0, 255, 0, 0);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// CRF
|
||||
/// </summary>
|
||||
[Category("[2] MP4 参数"), DisplayName("CRF"), Description("Constant Rate Factor, 取值范围 0-63, 建议范围 18-28, 默认取值 23, 数值越小则输出质量越高")]
|
||||
public int CRF { get => crf; set => crf = Math.Clamp(value, 0, 63); }
|
||||
private int crf = 23;
|
||||
|
||||
/// <summary>
|
||||
/// 编码器 TODO: 增加其他编码器
|
||||
/// </summary>
|
||||
[Category("[2] MP4 参数"), DisplayName("编码器"), Description("要使用的编码器")]
|
||||
public string Codec { get => "libx264"; }
|
||||
}
|
||||
}
|
||||
@@ -17,7 +17,7 @@ namespace SpineViewer.Exporter.Implementations.ExportArgs
|
||||
/// <summary>
|
||||
/// 导出时长
|
||||
/// </summary>
|
||||
[Category("视频参数"), DisplayName("时长"), Description("可以从模型列表查看动画时长, 如果小于 0, 则在逐个导出时每个模型使用各自的当前动画时长")]
|
||||
[Category("[1] 视频参数"), DisplayName("时长"), Description("可以从模型列表查看动画时长, 如果小于 0, 则在逐个导出时每个模型使用各自的当前动画时长")]
|
||||
public float Duration
|
||||
{
|
||||
get => duration;
|
||||
@@ -28,7 +28,7 @@ namespace SpineViewer.Exporter.Implementations.ExportArgs
|
||||
/// <summary>
|
||||
/// 帧率
|
||||
/// </summary>
|
||||
[Category("视频参数"), DisplayName("帧率"), Description("每秒画面数")]
|
||||
[Category("[1] 视频参数"), DisplayName("帧率"), Description("每秒画面数")]
|
||||
public float FPS { get; set; } = 60;
|
||||
|
||||
public override string? Validate()
|
||||
|
||||
@@ -35,8 +35,8 @@ namespace SpineViewer.Exporter.Implementations.Exporter
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Program.Logger.Error(ex.ToString());
|
||||
Program.Logger.Error("Failed to save single frame");
|
||||
logger.Error(ex.ToString());
|
||||
logger.Error("Failed to save single frame");
|
||||
}
|
||||
worker?.ReportProgress(100, $"已处理 1/1");
|
||||
}
|
||||
@@ -68,8 +68,8 @@ namespace SpineViewer.Exporter.Implementations.Exporter
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Program.Logger.Error(ex.ToString());
|
||||
Program.Logger.Error("Failed to save frame {} {}", savePath, spine.SkelPath);
|
||||
logger.Error(ex.ToString());
|
||||
logger.Error("Failed to save frame {} {}", savePath, spine.SkelPath);
|
||||
error++;
|
||||
}
|
||||
|
||||
@@ -77,9 +77,9 @@ namespace SpineViewer.Exporter.Implementations.Exporter
|
||||
}
|
||||
|
||||
if (error > 0)
|
||||
Program.Logger.Warn("Frames save {} successfully, {} failed", success, error);
|
||||
logger.Warn("Frames save {} successfully, {} failed", success, error);
|
||||
else
|
||||
Program.Logger.Info("{} frames saved successfully", success);
|
||||
logger.Info("{} frames saved successfully", success);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -37,8 +37,8 @@ namespace SpineViewer.Exporter.Implementations.Exporter
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Program.Logger.Error(ex.ToString());
|
||||
Program.Logger.Error("Failed to save frame {}", savePath);
|
||||
logger.Error(ex.ToString());
|
||||
logger.Error("Failed to save frame {}", savePath);
|
||||
}
|
||||
finally
|
||||
{
|
||||
@@ -72,8 +72,8 @@ namespace SpineViewer.Exporter.Implementations.Exporter
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Program.Logger.Error(ex.ToString());
|
||||
Program.Logger.Error("Failed to save frame {} {}", savePath, spine.SkelPath);
|
||||
logger.Error(ex.ToString());
|
||||
logger.Error("Failed to save frame {} {}", savePath, spine.SkelPath);
|
||||
}
|
||||
finally
|
||||
{
|
||||
|
||||
@@ -8,6 +8,7 @@ using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using FFMpegCore.Arguments;
|
||||
using System.Diagnostics;
|
||||
|
||||
namespace SpineViewer.Exporter.Implementations.Exporter
|
||||
{
|
||||
@@ -30,17 +31,19 @@ namespace SpineViewer.Exporter.Implementations.Exporter
|
||||
var videoFramesSource = new RawVideoPipeSource(GetFrames(spinesToRender, worker)) { FrameRate = args.FPS };
|
||||
try
|
||||
{
|
||||
FFMpegArguments
|
||||
var ffmpegArgs = FFMpegArguments
|
||||
.FromPipeInput(videoFramesSource)
|
||||
.OutputToFile(savePath, true, options => options
|
||||
.ForceFormat("gif")
|
||||
.WithCustomArgument(args.FFMpegCoreCustomArguments))
|
||||
.ProcessSynchronously();
|
||||
.WithCustomArgument(args.FFMpegCoreCustomArguments));
|
||||
|
||||
logger.Info("FFMpeg arguments: {}", ffmpegArgs.Arguments);
|
||||
ffmpegArgs.ProcessSynchronously();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Program.Logger.Error(ex.ToString());
|
||||
Program.Logger.Error("Failed to export gif {}", savePath);
|
||||
logger.Error(ex.ToString());
|
||||
logger.Error("Failed to export gif {}", savePath);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -58,17 +61,19 @@ namespace SpineViewer.Exporter.Implementations.Exporter
|
||||
var videoFramesSource = new RawVideoPipeSource(GetFrames(spine, worker)) { FrameRate = args.FPS };
|
||||
try
|
||||
{
|
||||
FFMpegArguments
|
||||
var ffmpegArgs = FFMpegArguments
|
||||
.FromPipeInput(videoFramesSource)
|
||||
.OutputToFile(savePath, true, options => options
|
||||
.ForceFormat("gif")
|
||||
.WithCustomArgument(args.FFMpegCoreCustomArguments))
|
||||
.ProcessSynchronously();
|
||||
.WithCustomArgument(args.FFMpegCoreCustomArguments));
|
||||
|
||||
logger.Info("FFMpeg arguments: {}", ffmpegArgs.Arguments);
|
||||
ffmpegArgs.ProcessSynchronously();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Program.Logger.Error(ex.ToString());
|
||||
Program.Logger.Error("Failed to export gif {} {}", savePath, spine.SkelPath);
|
||||
logger.Error(ex.ToString());
|
||||
logger.Error("Failed to export gif {} {}", savePath, spine.SkelPath);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
85
SpineViewer/Exporter/Implementations/Exporter/Mp4Exporter.cs
Normal file
85
SpineViewer/Exporter/Implementations/Exporter/Mp4Exporter.cs
Normal file
@@ -0,0 +1,85 @@
|
||||
using FFMpegCore.Pipes;
|
||||
using FFMpegCore;
|
||||
using SpineViewer.Exporter.Implementations.ExportArgs;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using FFMpegCore.Arguments;
|
||||
using System.Diagnostics;
|
||||
|
||||
namespace SpineViewer.Exporter.Implementations.Exporter
|
||||
{
|
||||
/// <summary>
|
||||
/// MP4 导出器
|
||||
/// </summary>
|
||||
[ExportImplementation(ExportType.MP4)]
|
||||
public class Mp4Exporter : VideoExporter
|
||||
{
|
||||
public Mp4Exporter(Mp4ExportArgs exportArgs) : base(exportArgs) { }
|
||||
|
||||
protected override void ExportSingle(Spine.Spine[] spinesToRender, BackgroundWorker? worker = null)
|
||||
{
|
||||
var args = (Mp4ExportArgs)ExportArgs;
|
||||
|
||||
// 导出单个时必定提供输出文件夹
|
||||
var filename = $"{timestamp}_{args.FPS:f0}_{args.CRF}.mp4";
|
||||
var savePath = Path.Combine(args.OutputDir, filename);
|
||||
|
||||
var videoFramesSource = new RawVideoPipeSource(GetFrames(spinesToRender, worker)) { FrameRate = args.FPS };
|
||||
try
|
||||
{
|
||||
var ffmpegArgs = FFMpegArguments
|
||||
.FromPipeInput(videoFramesSource)
|
||||
.OutputToFile(savePath, true, options => options
|
||||
.ForceFormat("mp4")
|
||||
.WithVideoCodec(args.Codec)
|
||||
.WithConstantRateFactor(args.CRF)
|
||||
.WithFastStart());
|
||||
|
||||
logger.Info("FFMpeg arguments: {}", ffmpegArgs.Arguments);
|
||||
ffmpegArgs.ProcessSynchronously();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.Error(ex.ToString());
|
||||
logger.Error("Failed to export mp4 {}", savePath);
|
||||
}
|
||||
}
|
||||
|
||||
protected override void ExportIndividual(Spine.Spine[] spinesToRender, BackgroundWorker? worker = null)
|
||||
{
|
||||
var args = (Mp4ExportArgs)ExportArgs;
|
||||
foreach (var spine in spinesToRender)
|
||||
{
|
||||
if (worker?.CancellationPending == true) break; // 取消的日志在 GetFrames 里输出
|
||||
|
||||
// 如果提供了输出文件夹, 则全部导出到输出文件夹, 否则导出到各自的文件夹下
|
||||
var filename = $"{spine.Name}_{timestamp}_{args.FPS:f0}_{args.CRF}.mp4";
|
||||
var savePath = Path.Combine(args.OutputDir ?? spine.AssetsDir, filename);
|
||||
|
||||
var videoFramesSource = new RawVideoPipeSource(GetFrames(spine, worker)) { FrameRate = args.FPS };
|
||||
try
|
||||
{
|
||||
var ffmpegArgs = FFMpegArguments
|
||||
.FromPipeInput(videoFramesSource)
|
||||
.OutputToFile(savePath, true, options => options
|
||||
.ForceFormat("mp4")
|
||||
.WithVideoCodec(args.Codec)
|
||||
.WithConstantRateFactor(args.CRF)
|
||||
.WithFastStart());
|
||||
|
||||
logger.Info("FFMpeg arguments: {}", ffmpegArgs.Arguments);
|
||||
ffmpegArgs.ProcessSynchronously();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.Error(ex.ToString());
|
||||
logger.Error("Failed to export mp4 {} {}", savePath, spine.SkelPath);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -23,9 +23,9 @@ namespace SpineViewer.Exporter.Implementations.Exporter
|
||||
{
|
||||
var args = (VideoExportArgs)ExportArgs;
|
||||
|
||||
// 独立导出时如果 args.Duration 小于 0 则使用自己的动画时长
|
||||
// 独立导出时如果 args.Duration 小于 0 则使用 Track0 的动画时长
|
||||
var duration = args.Duration;
|
||||
if (duration < 0) duration = spine.GetAnimationDuration(spine.CurrentAnimation);
|
||||
if (duration < 0) duration = spine.GetAnimationDuration(spine.Track0Animation); // TODO: 也许可以使用所有轨道的最大值
|
||||
|
||||
float delta = 1f / args.FPS;
|
||||
int total = Math.Max(1, (int)(duration * args.FPS)); // 至少导出 1 帧
|
||||
@@ -35,7 +35,7 @@ namespace SpineViewer.Exporter.Implementations.Exporter
|
||||
{
|
||||
if (worker?.CancellationPending == true)
|
||||
{
|
||||
Program.Logger.Info("Export cancelled");
|
||||
logger.Info("Export cancelled");
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -61,7 +61,7 @@ namespace SpineViewer.Exporter.Implementations.Exporter
|
||||
{
|
||||
if (worker?.CancellationPending == true)
|
||||
{
|
||||
Program.Logger.Info("Export cancelled");
|
||||
logger.Info("Export cancelled");
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -75,7 +75,7 @@ namespace SpineViewer.Exporter.Implementations.Exporter
|
||||
public override void Export(Spine.Spine[] spines, BackgroundWorker? worker = null)
|
||||
{
|
||||
// 导出视频格式需要把模型时间都重置到 0
|
||||
foreach (var spine in spines) spine.CurrentAnimation = spine.CurrentAnimation;
|
||||
foreach (var spine in spines) spine.Track0Animation = spine.Track0Animation; // TODO: 多轨道重置
|
||||
base.Export(spines, worker);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,21 +13,91 @@ namespace SpineViewer.Exporter
|
||||
public class SFMLImageFileSuffixConverter : StringConverter
|
||||
{
|
||||
private readonly string[] supportedFileSuffix = [".png", ".jpg", ".tga", ".bmp"];
|
||||
public override bool GetStandardValuesSupported(ITypeDescriptorContext? context)
|
||||
{
|
||||
// 支持标准值列表
|
||||
return true;
|
||||
}
|
||||
|
||||
public override bool GetStandardValuesExclusive(ITypeDescriptorContext? context)
|
||||
{
|
||||
// 排他模式,只有下拉列表中的值可选
|
||||
return true;
|
||||
}
|
||||
public override bool GetStandardValuesSupported(ITypeDescriptorContext? context) => true;
|
||||
|
||||
public override bool GetStandardValuesExclusive(ITypeDescriptorContext? context) => true;
|
||||
|
||||
public override StandardValuesCollection? GetStandardValues(ITypeDescriptorContext? context)
|
||||
{
|
||||
return new StandardValuesCollection(supportedFileSuffix);
|
||||
}
|
||||
}
|
||||
|
||||
public class SFMLColorConverter : ExpandableObjectConverter
|
||||
{
|
||||
private class SFMLColorPropertyDescriptor : SimplePropertyDescriptor
|
||||
{
|
||||
public SFMLColorPropertyDescriptor(Type componentType, string name, Type propertyType) : base(componentType, name, propertyType) { }
|
||||
|
||||
public override object? GetValue(object? component)
|
||||
{
|
||||
return component?.GetType().GetField(Name)?.GetValue(component) ?? default;
|
||||
}
|
||||
|
||||
public override void SetValue(object? component, object? value)
|
||||
{
|
||||
component?.GetType().GetField(Name)?.SetValue(component, value);
|
||||
}
|
||||
}
|
||||
|
||||
public override bool CanConvertFrom(ITypeDescriptorContext? context, Type sourceType)
|
||||
{
|
||||
return sourceType == typeof(string) || base.CanConvertFrom(context, sourceType);
|
||||
}
|
||||
|
||||
public override object? ConvertFrom(ITypeDescriptorContext? context, CultureInfo? culture, object value)
|
||||
{
|
||||
if (value is string s)
|
||||
{
|
||||
s = s.Trim();
|
||||
if (s.StartsWith("#") && s.Length == 9)
|
||||
{
|
||||
try
|
||||
{
|
||||
// 解析 R, G, B, A 分量,注意16进制解析
|
||||
byte r = byte.Parse(s.Substring(1, 2), NumberStyles.HexNumber);
|
||||
byte g = byte.Parse(s.Substring(3, 2), NumberStyles.HexNumber);
|
||||
byte b = byte.Parse(s.Substring(5, 2), NumberStyles.HexNumber);
|
||||
byte a = byte.Parse(s.Substring(7, 2), NumberStyles.HexNumber);
|
||||
return new SFML.Graphics.Color(r, g, b, a);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
throw new FormatException("无法解析颜色,确保格式为 #RRGGBBAA", ex);
|
||||
}
|
||||
}
|
||||
throw new FormatException("格式错误,正确格式为 #RRGGBBAA");
|
||||
}
|
||||
return base.ConvertFrom(context, culture, value);
|
||||
}
|
||||
|
||||
public override bool CanConvertTo(ITypeDescriptorContext? context, Type? destinationType)
|
||||
{
|
||||
return destinationType == typeof(string) || base.CanConvertTo(context, destinationType);
|
||||
}
|
||||
|
||||
public override object? ConvertTo(ITypeDescriptorContext? context, CultureInfo? culture, object? value, Type destinationType)
|
||||
{
|
||||
if (destinationType == typeof(string) && value is SFML.Graphics.Color color)
|
||||
return $"#{color.R:X2}{color.G:X2}{color.B:X2}{color.A:X2}";
|
||||
return base.ConvertTo(context, culture, value, destinationType);
|
||||
}
|
||||
|
||||
public override PropertyDescriptorCollection GetProperties(ITypeDescriptorContext? context, object value, Attribute[]? attributes)
|
||||
{
|
||||
// 自定义属性集合
|
||||
var properties = new List<PropertyDescriptor>
|
||||
{
|
||||
// 定义 R, G, B, A 四个字段的描述器
|
||||
new SFMLColorPropertyDescriptor(typeof(SFML.Graphics.Color), "R", typeof(byte)),
|
||||
new SFMLColorPropertyDescriptor(typeof(SFML.Graphics.Color), "G", typeof(byte)),
|
||||
new SFMLColorPropertyDescriptor(typeof(SFML.Graphics.Color), "B", typeof(byte)),
|
||||
new SFMLColorPropertyDescriptor(typeof(SFML.Graphics.Color), "A", typeof(byte))
|
||||
};
|
||||
|
||||
// 返回自定义属性集合
|
||||
return new PropertyDescriptorCollection(properties.ToArray());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
66
SpineViewer/Exporter/UITypeEditor.cs
Normal file
66
SpineViewer/Exporter/UITypeEditor.cs
Normal file
@@ -0,0 +1,66 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using System.Drawing.Design;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace SpineViewer.Exporter
|
||||
{
|
||||
class SFMLColorEditor : UITypeEditor
|
||||
{
|
||||
public override bool GetPaintValueSupported(ITypeDescriptorContext? context) => true;
|
||||
|
||||
public override void PaintValue(PaintValueEventArgs e)
|
||||
{
|
||||
if (e.Value is SFML.Graphics.Color color)
|
||||
{
|
||||
// 定义颜色和透明度的绘制区域
|
||||
var colorBox = new Rectangle(e.Bounds.X, e.Bounds.Y, e.Bounds.Width / 2, e.Bounds.Height);
|
||||
var alphaBox = new Rectangle(e.Bounds.X + e.Bounds.Width / 2, e.Bounds.Y, e.Bounds.Width / 2, e.Bounds.Height);
|
||||
|
||||
// 转换为 System.Drawing.Color
|
||||
var drawColor = Color.FromArgb(color.A, color.R, color.G, color.B);
|
||||
|
||||
// 绘制纯颜色(RGB 部分)
|
||||
using (var brush = new SolidBrush(Color.FromArgb(color.R, color.G, color.B)))
|
||||
{
|
||||
e.Graphics.FillRectangle(brush, colorBox);
|
||||
e.Graphics.DrawRectangle(Pens.Black, colorBox);
|
||||
}
|
||||
|
||||
// 绘制带透明度效果的颜色
|
||||
using (var checkerBrush = CreateTransparencyBrush())
|
||||
{
|
||||
e.Graphics.FillRectangle(checkerBrush, alphaBox); // 背景棋盘格
|
||||
}
|
||||
using (var brush = new SolidBrush(drawColor))
|
||||
{
|
||||
e.Graphics.FillRectangle(brush, alphaBox); // 叠加透明颜色
|
||||
e.Graphics.DrawRectangle(Pens.Black, alphaBox);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
base.PaintValue(e);
|
||||
}
|
||||
}
|
||||
|
||||
// 创建一个透明背景的棋盘格图案画刷
|
||||
private static TextureBrush CreateTransparencyBrush()
|
||||
{
|
||||
var bitmap = new Bitmap(8, 8);
|
||||
using (var g = Graphics.FromImage(bitmap))
|
||||
{
|
||||
g.Clear(Color.White);
|
||||
using (var grayBrush = new SolidBrush(Color.LightGray))
|
||||
{
|
||||
g.FillRectangle(grayBrush, 0, 0, 4, 4);
|
||||
g.FillRectangle(grayBrush, 4, 4, 4, 4);
|
||||
}
|
||||
}
|
||||
return new TextureBrush(bitmap);
|
||||
}
|
||||
}
|
||||
}
|
||||
66
SpineViewer/ImplementationResolver.cs
Normal file
66
SpineViewer/ImplementationResolver.cs
Normal file
@@ -0,0 +1,66 @@
|
||||
using SpineViewer.Exporter;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace SpineViewer
|
||||
{
|
||||
public interface IImplementationKey<TKey>
|
||||
{
|
||||
TKey ImplementationKey { get; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 可以使用反射查找基类关联的所有实现类
|
||||
/// </summary>
|
||||
/// <typeparam name="TBase">所有实现类的基类型</typeparam>
|
||||
/// <typeparam name="TAttr">实现类类型属性标记类型</typeparam>
|
||||
/// /// <typeparam name="TKey">实现类类型标记类型</typeparam>
|
||||
public abstract class ImplementationResolver<TBase, TAttr, TKey> where TAttr : Attribute, IImplementationKey<TKey>
|
||||
{
|
||||
/// <summary>
|
||||
/// 实现类型缓存
|
||||
/// </summary>
|
||||
private static readonly Dictionary<TKey, Type> ImplementationTypes = new();
|
||||
|
||||
static ImplementationResolver()
|
||||
{
|
||||
var baseType = typeof(TBase);
|
||||
var impTypes = Assembly.GetExecutingAssembly().GetTypes().Where(t => baseType.IsAssignableFrom(t) && !t.IsAbstract);
|
||||
foreach (var type in impTypes)
|
||||
{
|
||||
var attr = type.GetCustomAttribute<TAttr>();
|
||||
if (attr is not null)
|
||||
{
|
||||
var key = attr.ImplementationKey;
|
||||
if (ImplementationTypes.ContainsKey(key))
|
||||
throw new InvalidOperationException($"Multiple implementations found for key: {key}");
|
||||
ImplementationTypes[key] = type;
|
||||
}
|
||||
}
|
||||
NLog.LogManager.GetCurrentClassLogger().Debug("Found implementations for {}: {}", baseType, string.Join(", ", ImplementationTypes.Keys));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 判断某种类型是否实现
|
||||
/// </summary>
|
||||
public static bool HasImplementation(TKey key) => ImplementationTypes.ContainsKey(key);
|
||||
|
||||
/// <summary>
|
||||
/// 根据实现类键和参数创建实例
|
||||
/// </summary>
|
||||
/// <param name="impKey"></param>
|
||||
/// <param name="args"></param>
|
||||
/// <returns></returns>
|
||||
/// <exception cref="NotImplementedException"></exception>
|
||||
protected static TBase New(TKey impKey, object?[] args)
|
||||
{
|
||||
if (!ImplementationTypes.TryGetValue(impKey, out var type))
|
||||
throw new NotImplementedException($"Not implemented type for {typeof(TBase)}: {impKey}");
|
||||
return (TBase)Activator.CreateInstance(type, args);
|
||||
}
|
||||
}
|
||||
}
|
||||
80
SpineViewer/MainForm.Designer.cs
generated
80
SpineViewer/MainForm.Designer.cs
generated
@@ -61,9 +61,9 @@
|
||||
spineListView = new SpineViewer.Controls.SpineListView();
|
||||
propertyGrid_Spine = new PropertyGrid();
|
||||
splitContainer_Config = new SplitContainer();
|
||||
groupBox_SkelConfig = new GroupBox();
|
||||
groupBox_PreviewConfig = new GroupBox();
|
||||
propertyGrid_Previewer = new PropertyGrid();
|
||||
groupBox_SkelConfig = new GroupBox();
|
||||
groupBox_Preview = new GroupBox();
|
||||
spinePreviewer = new SpineViewer.Controls.SpinePreviewer();
|
||||
panel_MainForm = new Panel();
|
||||
@@ -86,8 +86,8 @@
|
||||
splitContainer_Config.Panel1.SuspendLayout();
|
||||
splitContainer_Config.Panel2.SuspendLayout();
|
||||
splitContainer_Config.SuspendLayout();
|
||||
groupBox_SkelConfig.SuspendLayout();
|
||||
groupBox_PreviewConfig.SuspendLayout();
|
||||
groupBox_SkelConfig.SuspendLayout();
|
||||
groupBox_Preview.SuspendLayout();
|
||||
panel_MainForm.SuspendLayout();
|
||||
SuspendLayout();
|
||||
@@ -99,7 +99,7 @@
|
||||
menuStrip.Items.AddRange(new ToolStripItem[] { toolStripMenuItem_File, toolStripMenuItem_Tool, toolStripMenuItem_Download, toolStripMenuItem_Help });
|
||||
menuStrip.Location = new Point(0, 0);
|
||||
menuStrip.Name = "menuStrip";
|
||||
menuStrip.Size = new Size(1748, 32);
|
||||
menuStrip.Size = new Size(1778, 32);
|
||||
menuStrip.TabIndex = 0;
|
||||
menuStrip.Text = "菜单";
|
||||
//
|
||||
@@ -163,6 +163,7 @@
|
||||
toolStripMenuItem_ExportMkv.Name = "toolStripMenuItem_ExportMkv";
|
||||
toolStripMenuItem_ExportMkv.Size = new Size(270, 34);
|
||||
toolStripMenuItem_ExportMkv.Text = "MKV";
|
||||
toolStripMenuItem_ExportMkv.Visible = false;
|
||||
toolStripMenuItem_ExportMkv.Click += toolStripMenuItem_Export_Click;
|
||||
//
|
||||
// toolStripMenuItem_ExportMp4
|
||||
@@ -177,6 +178,7 @@
|
||||
toolStripMenuItem_ExportMov.Name = "toolStripMenuItem_ExportMov";
|
||||
toolStripMenuItem_ExportMov.Size = new Size(270, 34);
|
||||
toolStripMenuItem_ExportMov.Text = "MOV...";
|
||||
toolStripMenuItem_ExportMov.Visible = false;
|
||||
toolStripMenuItem_ExportMov.Click += toolStripMenuItem_Export_Click;
|
||||
//
|
||||
// toolStripMenuItem_ExportWebm
|
||||
@@ -184,6 +186,7 @@
|
||||
toolStripMenuItem_ExportWebm.Name = "toolStripMenuItem_ExportWebm";
|
||||
toolStripMenuItem_ExportWebm.Size = new Size(270, 34);
|
||||
toolStripMenuItem_ExportWebm.Text = "WebM...";
|
||||
toolStripMenuItem_ExportWebm.Visible = false;
|
||||
toolStripMenuItem_ExportWebm.Click += toolStripMenuItem_Export_Click;
|
||||
//
|
||||
// toolStripSeparator2
|
||||
@@ -263,7 +266,7 @@
|
||||
rtbLog.Margin = new Padding(3, 2, 3, 2);
|
||||
rtbLog.Name = "rtbLog";
|
||||
rtbLog.ReadOnly = true;
|
||||
rtbLog.Size = new Size(1728, 114);
|
||||
rtbLog.Size = new Size(1758, 134);
|
||||
rtbLog.TabIndex = 0;
|
||||
rtbLog.Text = "";
|
||||
rtbLog.WordWrap = false;
|
||||
@@ -272,6 +275,7 @@
|
||||
//
|
||||
splitContainer_MainForm.Cursor = Cursors.SizeNS;
|
||||
splitContainer_MainForm.Dock = DockStyle.Fill;
|
||||
splitContainer_MainForm.FixedPanel = FixedPanel.Panel2;
|
||||
splitContainer_MainForm.Location = new Point(10, 5);
|
||||
splitContainer_MainForm.Name = "splitContainer_MainForm";
|
||||
splitContainer_MainForm.Orientation = Orientation.Horizontal;
|
||||
@@ -285,8 +289,9 @@
|
||||
//
|
||||
splitContainer_MainForm.Panel2.Controls.Add(rtbLog);
|
||||
splitContainer_MainForm.Panel2.Cursor = Cursors.Default;
|
||||
splitContainer_MainForm.Size = new Size(1728, 997);
|
||||
splitContainer_MainForm.SplitterDistance = 879;
|
||||
splitContainer_MainForm.Size = new Size(1758, 1097);
|
||||
splitContainer_MainForm.SplitterDistance = 955;
|
||||
splitContainer_MainForm.SplitterWidth = 8;
|
||||
splitContainer_MainForm.TabIndex = 3;
|
||||
splitContainer_MainForm.TabStop = false;
|
||||
splitContainer_MainForm.SplitterMoved += splitContainer_SplitterMoved;
|
||||
@@ -296,6 +301,7 @@
|
||||
//
|
||||
splitContainer_Functional.Cursor = Cursors.SizeWE;
|
||||
splitContainer_Functional.Dock = DockStyle.Fill;
|
||||
splitContainer_Functional.FixedPanel = FixedPanel.Panel1;
|
||||
splitContainer_Functional.Location = new Point(0, 0);
|
||||
splitContainer_Functional.Name = "splitContainer_Functional";
|
||||
//
|
||||
@@ -308,8 +314,9 @@
|
||||
//
|
||||
splitContainer_Functional.Panel2.Controls.Add(groupBox_Preview);
|
||||
splitContainer_Functional.Panel2.Cursor = Cursors.Default;
|
||||
splitContainer_Functional.Size = new Size(1728, 879);
|
||||
splitContainer_Functional.SplitterDistance = 747;
|
||||
splitContainer_Functional.Size = new Size(1758, 955);
|
||||
splitContainer_Functional.SplitterDistance = 759;
|
||||
splitContainer_Functional.SplitterWidth = 8;
|
||||
splitContainer_Functional.TabIndex = 2;
|
||||
splitContainer_Functional.TabStop = false;
|
||||
splitContainer_Functional.SplitterMoved += splitContainer_SplitterMoved;
|
||||
@@ -331,8 +338,9 @@
|
||||
//
|
||||
splitContainer_Information.Panel2.Controls.Add(splitContainer_Config);
|
||||
splitContainer_Information.Panel2.Cursor = Cursors.Default;
|
||||
splitContainer_Information.Size = new Size(747, 879);
|
||||
splitContainer_Information.SplitterDistance = 399;
|
||||
splitContainer_Information.Size = new Size(759, 955);
|
||||
splitContainer_Information.SplitterDistance = 354;
|
||||
splitContainer_Information.SplitterWidth = 8;
|
||||
splitContainer_Information.TabIndex = 1;
|
||||
splitContainer_Information.TabStop = false;
|
||||
splitContainer_Information.SplitterMoved += splitContainer_SplitterMoved;
|
||||
@@ -344,7 +352,7 @@
|
||||
groupBox_SkelList.Dock = DockStyle.Fill;
|
||||
groupBox_SkelList.Location = new Point(0, 0);
|
||||
groupBox_SkelList.Name = "groupBox_SkelList";
|
||||
groupBox_SkelList.Size = new Size(399, 879);
|
||||
groupBox_SkelList.Size = new Size(354, 955);
|
||||
groupBox_SkelList.TabIndex = 0;
|
||||
groupBox_SkelList.TabStop = false;
|
||||
groupBox_SkelList.Text = "模型列表";
|
||||
@@ -355,7 +363,7 @@
|
||||
spineListView.Location = new Point(3, 26);
|
||||
spineListView.Name = "spineListView";
|
||||
spineListView.PropertyGrid = propertyGrid_Spine;
|
||||
spineListView.Size = new Size(393, 850);
|
||||
spineListView.Size = new Size(348, 926);
|
||||
spineListView.TabIndex = 0;
|
||||
//
|
||||
// propertyGrid_Spine
|
||||
@@ -364,7 +372,7 @@
|
||||
propertyGrid_Spine.HelpVisible = false;
|
||||
propertyGrid_Spine.Location = new Point(3, 26);
|
||||
propertyGrid_Spine.Name = "propertyGrid_Spine";
|
||||
propertyGrid_Spine.Size = new Size(338, 485);
|
||||
propertyGrid_Spine.Size = new Size(391, 592);
|
||||
propertyGrid_Spine.TabIndex = 0;
|
||||
propertyGrid_Spine.ToolbarVisible = false;
|
||||
propertyGrid_Spine.PropertyValueChanged += propertyGrid_PropertyValueChanged;
|
||||
@@ -373,44 +381,35 @@
|
||||
//
|
||||
splitContainer_Config.Cursor = Cursors.SizeNS;
|
||||
splitContainer_Config.Dock = DockStyle.Fill;
|
||||
splitContainer_Config.FixedPanel = FixedPanel.Panel1;
|
||||
splitContainer_Config.Location = new Point(0, 0);
|
||||
splitContainer_Config.Name = "splitContainer_Config";
|
||||
splitContainer_Config.Orientation = Orientation.Horizontal;
|
||||
//
|
||||
// splitContainer_Config.Panel1
|
||||
//
|
||||
splitContainer_Config.Panel1.Controls.Add(groupBox_SkelConfig);
|
||||
splitContainer_Config.Panel1.Controls.Add(groupBox_PreviewConfig);
|
||||
splitContainer_Config.Panel1.Cursor = Cursors.Default;
|
||||
//
|
||||
// splitContainer_Config.Panel2
|
||||
//
|
||||
splitContainer_Config.Panel2.Controls.Add(groupBox_PreviewConfig);
|
||||
splitContainer_Config.Panel2.Controls.Add(groupBox_SkelConfig);
|
||||
splitContainer_Config.Panel2.Cursor = Cursors.Default;
|
||||
splitContainer_Config.Size = new Size(344, 879);
|
||||
splitContainer_Config.SplitterDistance = 514;
|
||||
splitContainer_Config.Size = new Size(397, 955);
|
||||
splitContainer_Config.SplitterDistance = 326;
|
||||
splitContainer_Config.SplitterWidth = 8;
|
||||
splitContainer_Config.TabIndex = 0;
|
||||
splitContainer_Config.TabStop = false;
|
||||
splitContainer_Config.SplitterMoved += splitContainer_SplitterMoved;
|
||||
splitContainer_Config.MouseUp += splitContainer_MouseUp;
|
||||
//
|
||||
// groupBox_SkelConfig
|
||||
//
|
||||
groupBox_SkelConfig.Controls.Add(propertyGrid_Spine);
|
||||
groupBox_SkelConfig.Dock = DockStyle.Fill;
|
||||
groupBox_SkelConfig.Location = new Point(0, 0);
|
||||
groupBox_SkelConfig.Name = "groupBox_SkelConfig";
|
||||
groupBox_SkelConfig.Size = new Size(344, 514);
|
||||
groupBox_SkelConfig.TabIndex = 0;
|
||||
groupBox_SkelConfig.TabStop = false;
|
||||
groupBox_SkelConfig.Text = "模型参数";
|
||||
//
|
||||
// groupBox_PreviewConfig
|
||||
//
|
||||
groupBox_PreviewConfig.Controls.Add(propertyGrid_Previewer);
|
||||
groupBox_PreviewConfig.Dock = DockStyle.Fill;
|
||||
groupBox_PreviewConfig.Location = new Point(0, 0);
|
||||
groupBox_PreviewConfig.Name = "groupBox_PreviewConfig";
|
||||
groupBox_PreviewConfig.Size = new Size(344, 361);
|
||||
groupBox_PreviewConfig.Size = new Size(397, 326);
|
||||
groupBox_PreviewConfig.TabIndex = 1;
|
||||
groupBox_PreviewConfig.TabStop = false;
|
||||
groupBox_PreviewConfig.Text = "画面参数";
|
||||
@@ -421,18 +420,29 @@
|
||||
propertyGrid_Previewer.HelpVisible = false;
|
||||
propertyGrid_Previewer.Location = new Point(3, 26);
|
||||
propertyGrid_Previewer.Name = "propertyGrid_Previewer";
|
||||
propertyGrid_Previewer.Size = new Size(338, 332);
|
||||
propertyGrid_Previewer.Size = new Size(391, 297);
|
||||
propertyGrid_Previewer.TabIndex = 1;
|
||||
propertyGrid_Previewer.ToolbarVisible = false;
|
||||
propertyGrid_Previewer.PropertyValueChanged += propertyGrid_PropertyValueChanged;
|
||||
//
|
||||
// groupBox_SkelConfig
|
||||
//
|
||||
groupBox_SkelConfig.Controls.Add(propertyGrid_Spine);
|
||||
groupBox_SkelConfig.Dock = DockStyle.Fill;
|
||||
groupBox_SkelConfig.Location = new Point(0, 0);
|
||||
groupBox_SkelConfig.Name = "groupBox_SkelConfig";
|
||||
groupBox_SkelConfig.Size = new Size(397, 621);
|
||||
groupBox_SkelConfig.TabIndex = 0;
|
||||
groupBox_SkelConfig.TabStop = false;
|
||||
groupBox_SkelConfig.Text = "模型参数";
|
||||
//
|
||||
// groupBox_Preview
|
||||
//
|
||||
groupBox_Preview.Controls.Add(spinePreviewer);
|
||||
groupBox_Preview.Dock = DockStyle.Fill;
|
||||
groupBox_Preview.Location = new Point(0, 0);
|
||||
groupBox_Preview.Name = "groupBox_Preview";
|
||||
groupBox_Preview.Size = new Size(977, 879);
|
||||
groupBox_Preview.Size = new Size(991, 955);
|
||||
groupBox_Preview.TabIndex = 1;
|
||||
groupBox_Preview.TabStop = false;
|
||||
groupBox_Preview.Text = "预览画面";
|
||||
@@ -443,7 +453,7 @@
|
||||
spinePreviewer.Location = new Point(3, 26);
|
||||
spinePreviewer.Name = "spinePreviewer";
|
||||
spinePreviewer.PropertyGrid = propertyGrid_Previewer;
|
||||
spinePreviewer.Size = new Size(971, 850);
|
||||
spinePreviewer.Size = new Size(985, 926);
|
||||
spinePreviewer.SpineListView = spineListView;
|
||||
spinePreviewer.TabIndex = 0;
|
||||
//
|
||||
@@ -454,7 +464,7 @@
|
||||
panel_MainForm.Location = new Point(0, 32);
|
||||
panel_MainForm.Name = "panel_MainForm";
|
||||
panel_MainForm.Padding = new Padding(10, 5, 10, 10);
|
||||
panel_MainForm.Size = new Size(1748, 1012);
|
||||
panel_MainForm.Size = new Size(1778, 1112);
|
||||
panel_MainForm.TabIndex = 4;
|
||||
//
|
||||
// toolTip
|
||||
@@ -465,7 +475,7 @@
|
||||
//
|
||||
AutoScaleDimensions = new SizeF(11F, 24F);
|
||||
AutoScaleMode = AutoScaleMode.Font;
|
||||
ClientSize = new Size(1748, 1044);
|
||||
ClientSize = new Size(1778, 1144);
|
||||
Controls.Add(panel_MainForm);
|
||||
Controls.Add(menuStrip);
|
||||
Icon = (Icon)resources.GetObject("$this.Icon");
|
||||
@@ -495,8 +505,8 @@
|
||||
splitContainer_Config.Panel2.ResumeLayout(false);
|
||||
((System.ComponentModel.ISupportInitialize)splitContainer_Config).EndInit();
|
||||
splitContainer_Config.ResumeLayout(false);
|
||||
groupBox_SkelConfig.ResumeLayout(false);
|
||||
groupBox_PreviewConfig.ResumeLayout(false);
|
||||
groupBox_SkelConfig.ResumeLayout(false);
|
||||
groupBox_Preview.ResumeLayout(false);
|
||||
panel_MainForm.ResumeLayout(false);
|
||||
ResumeLayout(false);
|
||||
|
||||
@@ -12,8 +12,10 @@ using SpineViewer.Exporter;
|
||||
|
||||
namespace SpineViewer
|
||||
{
|
||||
public partial class MainForm : Form
|
||||
internal partial class MainForm : Form
|
||||
{
|
||||
private Logger logger = LogManager.GetCurrentClassLogger();
|
||||
|
||||
public MainForm()
|
||||
{
|
||||
InitializeComponent();
|
||||
@@ -27,6 +29,19 @@ namespace SpineViewer
|
||||
toolStripMenuItem_ExportMp4.Tag = ExportType.MP4;
|
||||
toolStripMenuItem_ExportMov.Tag = ExportType.MOV;
|
||||
toolStripMenuItem_ExportWebm.Tag = ExportType.WebM;
|
||||
|
||||
// 执行一些初始化工作
|
||||
try
|
||||
{
|
||||
Spine.Shader.Init();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.Error(ex.ToString());
|
||||
logger.Error("Failed to load fragment shader");
|
||||
MessageBox.Warn("Fragment shader 加载失败,预乘Alpha通道属性失效");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -87,15 +102,6 @@ namespace SpineViewer
|
||||
return;
|
||||
}
|
||||
|
||||
lock (spineListView.Spines)
|
||||
{
|
||||
if (spineListView.Spines.Count <= 0)
|
||||
{
|
||||
MessageBox.Info("请至少打开一个骨骼文件");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
var exportArgs = ExportArgs.New(type, spinePreviewer.Resolution, spinePreviewer.GetView(), spinePreviewer.RenderSelectedOnly);
|
||||
var exportDialog = new Dialogs.ExportDialog() { ExportArgs = exportArgs };
|
||||
if (exportDialog.ShowDialog() != DialogResult.OK)
|
||||
@@ -155,10 +161,12 @@ namespace SpineViewer
|
||||
{
|
||||
var worker = (BackgroundWorker)sender;
|
||||
var exporter = (Exporter.Exporter)e.Argument;
|
||||
Invoke(() => TaskbarManager.SetProgressState(Handle, TBPFLAG.TBPF_INDETERMINATE));
|
||||
spinePreviewer.StopRender();
|
||||
lock (spineListView.Spines) { exporter.Export(spineListView.Spines.ToArray(), (BackgroundWorker)sender); }
|
||||
lock (spineListView.Spines) { exporter.Export(spineListView.Spines.Where(sp => !sp.IsHidden).ToArray(), (BackgroundWorker)sender); }
|
||||
e.Cancel = worker.CancellationPending;
|
||||
spinePreviewer.StartRender();
|
||||
Invoke(() => TaskbarManager.SetProgressState(Handle, TBPFLAG.TBPF_NOPROGRESS));
|
||||
}
|
||||
|
||||
private void ConvertFileFormat_Work(object? sender, DoWorkEventArgs e)
|
||||
@@ -175,7 +183,7 @@ namespace SpineViewer
|
||||
int success = 0;
|
||||
int error = 0;
|
||||
|
||||
SkeletonConverter srcCvter = srcVersion != Spine.Version.Auto ? SkeletonConverter.New(srcVersion) : null;
|
||||
SkeletonConverter srcCvter = srcVersion != SpineVersion.Auto ? SkeletonConverter.New(srcVersion) : null;
|
||||
SkeletonConverter tgtCvter = SkeletonConverter.New(tgtVersion);
|
||||
|
||||
worker.ReportProgress(0, $"已处理 0/{totalCount}");
|
||||
@@ -192,12 +200,16 @@ namespace SpineViewer
|
||||
|
||||
try
|
||||
{
|
||||
if (srcVersion == Spine.Version.Auto)
|
||||
if (srcVersion == SpineVersion.Auto)
|
||||
{
|
||||
if (Spine.Spine.GetVersion(skelPath) is Spine.Version detectedSrcVersion)
|
||||
srcCvter = SkeletonConverter.New(detectedSrcVersion);
|
||||
else
|
||||
throw new InvalidDataException($"Auto version detection failed for {skelPath}, try to use a specific version");
|
||||
try
|
||||
{
|
||||
srcCvter = SkeletonConverter.New(SpineHelper.GetVersion(skelPath));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
throw new InvalidDataException($"Auto version detection failed for {skelPath}, try to use a specific version", ex);
|
||||
}
|
||||
}
|
||||
var root = srcCvter.Read(skelPath);
|
||||
root = srcCvter.ToVersion(root, tgtVersion);
|
||||
@@ -206,8 +218,8 @@ namespace SpineViewer
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Program.Logger.Error(ex.ToString());
|
||||
Program.Logger.Error("Failed to convert {}", skelPath);
|
||||
logger.Error(ex.ToString());
|
||||
logger.Error("Failed to convert {}", skelPath);
|
||||
error++;
|
||||
}
|
||||
|
||||
@@ -216,11 +228,11 @@ namespace SpineViewer
|
||||
|
||||
if (error > 0)
|
||||
{
|
||||
Program.Logger.Warn("Batch convert {} successfully, {} failed", success, error);
|
||||
logger.Warn("Batch convert {} successfully, {} failed", success, error);
|
||||
}
|
||||
else
|
||||
{
|
||||
Program.Logger.Info("{} skel converted successfully", success);
|
||||
logger.Info("{} skel converted successfully", success);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
21
SpineViewer/NLogExtension.cs
Normal file
21
SpineViewer/NLogExtension.cs
Normal file
@@ -0,0 +1,21 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace SpineViewer
|
||||
{
|
||||
public static class NLogExtension
|
||||
{
|
||||
/// <summary>
|
||||
/// 输出当前进程的内存占用
|
||||
/// </summary>
|
||||
public static void LogCurrentProcessMemoryUsage(this NLog.Logger logger)
|
||||
{
|
||||
var process = Process.GetCurrentProcess();
|
||||
logger.Info("Current memory usage for {}: {:F2} MB", process.ProcessName, process.WorkingSet64 / 1024.0 / 1024.0);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -5,35 +5,30 @@ namespace SpineViewer
|
||||
{
|
||||
internal static class Program
|
||||
{
|
||||
/// <summary>
|
||||
/// 程序路径
|
||||
/// </summary>
|
||||
public static readonly string FilePath = Environment.ProcessPath;
|
||||
///// <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 Name = Path.GetFileNameWithoutExtension(FilePath);
|
||||
|
||||
/// <summary>
|
||||
/// 程序目录
|
||||
/// </summary>
|
||||
public static readonly string RootDir = Path.GetDirectoryName(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;
|
||||
|
||||
/// <summary>
|
||||
/// 程序进程
|
||||
/// </summary>
|
||||
public static readonly Process Process = Process.GetCurrentProcess();
|
||||
///// <summary>
|
||||
///// 程序临时目录
|
||||
///// </summary>
|
||||
//public static readonly string TempDir = Directory.CreateDirectory(Path.Combine(Path.GetTempPath(), Name)).FullName;
|
||||
|
||||
/// <summary>
|
||||
/// 程序日志器
|
||||
/// </summary>
|
||||
public static readonly Logger Logger = LogManager.GetCurrentClassLogger();
|
||||
private static readonly Logger logger = LogManager.GetCurrentClassLogger();
|
||||
|
||||
/// <summary>
|
||||
/// 应用入口点
|
||||
@@ -41,8 +36,9 @@ namespace SpineViewer
|
||||
[STAThread]
|
||||
static void Main()
|
||||
{
|
||||
// 此处先初始化全局配置再触发静态字段 Logger 引用构造, 才能将配置应用到新的日志器上
|
||||
InitializeLogConfiguration();
|
||||
Logger.Info("Program Started");
|
||||
logger.Info("Program Started");
|
||||
|
||||
// To customize application configuration such as set high DPI settings or default font,
|
||||
// see https://aka.ms/applicationconfiguration.
|
||||
@@ -54,7 +50,7 @@ namespace SpineViewer
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.Fatal(ex.ToString());
|
||||
logger.Fatal(ex.ToString());
|
||||
MessageBox.Error(ex.ToString(), "程序已崩溃");
|
||||
}
|
||||
}
|
||||
@@ -82,10 +78,5 @@ namespace SpineViewer
|
||||
config.AddRule(LogLevel.Debug, LogLevel.Fatal, fileTarget);
|
||||
LogManager.Configuration = config;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 输出当前内存使用情况
|
||||
/// </summary>
|
||||
public static void LogCurrentMemoryUsage() => Logger.Info("Current memory usage: {:F2} MB", Process.WorkingSet64 / 1024.0 / 1024.0);
|
||||
}
|
||||
}
|
||||
@@ -11,7 +11,7 @@ using System.Globalization;
|
||||
|
||||
namespace SpineViewer.Spine.Implementations.SkeletonConverter
|
||||
{
|
||||
[SpineImplementation(Version.V38)]
|
||||
[SpineImplementation(SpineVersion.V38)]
|
||||
class SkeletonConverter38 : SpineViewer.Spine.SkeletonConverter
|
||||
{
|
||||
private BinaryReader reader = null;
|
||||
@@ -1286,11 +1286,11 @@ namespace SpineViewer.Spine.Implementations.SkeletonConverter
|
||||
base.WriteJson(root, jsonPath);
|
||||
}
|
||||
|
||||
public override JsonObject ToVersion(JsonObject root, Version version)
|
||||
public override JsonObject ToVersion(JsonObject root, SpineVersion version)
|
||||
{
|
||||
root = version switch
|
||||
{
|
||||
Version.V38 => root.DeepClone().AsObject(),
|
||||
SpineVersion.V38 => root.DeepClone().AsObject(),
|
||||
_ => throw new NotImplementedException(),
|
||||
};
|
||||
return root;
|
||||
|
||||
@@ -10,7 +10,7 @@ using SpineRuntime21;
|
||||
|
||||
namespace SpineViewer.Spine.Implementations.Spine
|
||||
{
|
||||
[SpineImplementation(Version.V21)]
|
||||
[SpineImplementation(SpineVersion.V21)]
|
||||
internal class Spine21 : SpineViewer.Spine.Spine
|
||||
{
|
||||
private static readonly Animation EmptyAnimation = new(EMPTY_ANIMATION, [], 0);
|
||||
@@ -26,8 +26,6 @@ namespace SpineViewer.Spine.Implementations.Spine
|
||||
texture.Repeated = true;
|
||||
|
||||
page.rendererObject = texture;
|
||||
page.width = (int)texture.Size.X;
|
||||
page.height = (int)texture.Size.Y;
|
||||
}
|
||||
|
||||
public void Unload(object texture)
|
||||
@@ -49,7 +47,7 @@ namespace SpineViewer.Spine.Implementations.Spine
|
||||
// 2.1.x 不支持剪裁
|
||||
//private SkeletonClipping clipping = new();
|
||||
|
||||
public Spine21(string skelPath, string? atlasPath = null) : base(skelPath, atlasPath)
|
||||
public Spine21(string skelPath, string atlasPath) : base(skelPath, atlasPath)
|
||||
{
|
||||
atlas = new Atlas(AtlasPath, textureLoader);
|
||||
try
|
||||
@@ -75,13 +73,15 @@ namespace SpineViewer.Spine.Implementations.Spine
|
||||
}
|
||||
}
|
||||
|
||||
animationStateData = new AnimationStateData(skeletonData);
|
||||
skeleton = new Skeleton(skeletonData);
|
||||
animationState = new AnimationState(animationStateData);
|
||||
foreach (var skin in skeletonData.Skins)
|
||||
skinNames.Add(skin.Name);
|
||||
|
||||
foreach (var anime in skeletonData.Animations)
|
||||
animationNames.Add(anime.Name);
|
||||
CurrentAnimation = DefaultAnimationName;
|
||||
|
||||
skeleton = new Skeleton(skeletonData);
|
||||
animationStateData = new AnimationStateData(skeletonData);
|
||||
animationState = new AnimationState(animationStateData);
|
||||
}
|
||||
|
||||
protected override void Dispose(bool disposing)
|
||||
@@ -109,7 +109,8 @@ namespace SpineViewer.Spine.Implementations.Spine
|
||||
var position = Position;
|
||||
var flipX = FlipX;
|
||||
var flipY = FlipY;
|
||||
var savedTrack0 = animationState.GetCurrent(0);
|
||||
var animation = Track0Animation; // TODO: 适配多轨道
|
||||
var skin = Skin;
|
||||
|
||||
var val = Math.Max(value, SCALE_MIN);
|
||||
if (skeletonBinary is not null)
|
||||
@@ -132,21 +133,8 @@ namespace SpineViewer.Spine.Implementations.Spine
|
||||
Position = position;
|
||||
FlipX = flipX;
|
||||
FlipY = flipY;
|
||||
|
||||
// 恢复原本 Track0 上所有动画
|
||||
if (savedTrack0 is not null)
|
||||
{
|
||||
var entry = animationState.SetAnimation(0, savedTrack0.Animation.Name, true);
|
||||
entry.Time = savedTrack0.Time;
|
||||
// 2.1.x 没有提供 Next 访问器,故放弃还原后续动画,问题不大,因为预览画面目前不需要连续播放不同动画,只需要循环同一个动画
|
||||
//var savedEntry = savedTrack0.Next;
|
||||
//while (savedEntry is not null)
|
||||
//{
|
||||
// entry = animationState.AddAnimation(0, savedEntry.Animation.Name, true, 0);
|
||||
// entry.Time = savedEntry.TrackTime;
|
||||
// savedEntry = savedEntry.Next;
|
||||
//}
|
||||
}
|
||||
Track0Animation = animation; // TODO: 适配多轨道
|
||||
Skin = skin;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -156,23 +144,36 @@ namespace SpineViewer.Spine.Implementations.Spine
|
||||
set
|
||||
{
|
||||
skeleton.X = value.X;
|
||||
skeleton.Y = value.Y;
|
||||
skeleton.Y = value.Y;
|
||||
Update(0);
|
||||
}
|
||||
}
|
||||
|
||||
public override bool FlipX
|
||||
{
|
||||
get => skeleton.FlipX;
|
||||
set => skeleton.FlipX = value;
|
||||
set { skeleton.FlipX = value; Update(0); }
|
||||
}
|
||||
|
||||
public override bool FlipY
|
||||
{
|
||||
get => skeleton.FlipY;
|
||||
set => skeleton.FlipY = value;
|
||||
set { skeleton.FlipY = value; Update(0); }
|
||||
}
|
||||
|
||||
public override string CurrentAnimation
|
||||
public override string Skin
|
||||
{
|
||||
get => skeleton.Skin?.Name ?? "default";
|
||||
set
|
||||
{
|
||||
if (!skinNames.Contains(value)) return;
|
||||
skeleton.SetSkin(value);
|
||||
skeleton.SetSlotsToSetupPose();
|
||||
Update(0);
|
||||
}
|
||||
}
|
||||
|
||||
public override string Track0Animation
|
||||
{
|
||||
get => animationState.GetCurrent(0)?.Animation.Name ?? EMPTY_ANIMATION;
|
||||
set
|
||||
@@ -330,10 +331,14 @@ namespace SpineViewer.Spine.Implementations.Spine
|
||||
if (vertexArray.VertexCount > 0)
|
||||
{
|
||||
if (UsePremultipliedAlpha && (states.BlendMode == BlendModeSFML.Normal || states.BlendMode == BlendModeSFML.Additive))
|
||||
states.Shader = FragmentShader;
|
||||
states.Shader = Shader.FragmentShader;
|
||||
else
|
||||
states.Shader = null;
|
||||
target.Draw(vertexArray, states);
|
||||
|
||||
// 调试纹理
|
||||
if (!IsDebug || DebugTexture)
|
||||
target.Draw(vertexArray, states);
|
||||
|
||||
vertexArray.Clear();
|
||||
}
|
||||
states.BlendMode = blendMode;
|
||||
@@ -375,12 +380,15 @@ namespace SpineViewer.Spine.Implementations.Spine
|
||||
}
|
||||
|
||||
if (UsePremultipliedAlpha && (states.BlendMode == BlendModeSFML.Normal || states.BlendMode == BlendModeSFML.Additive))
|
||||
states.Shader = FragmentShader;
|
||||
states.Shader = Shader.FragmentShader;
|
||||
else
|
||||
states.Shader = null;
|
||||
target.Draw(vertexArray, states);
|
||||
//clipping.ClipEnd();
|
||||
|
||||
// 调试纹理
|
||||
if (!IsDebug || DebugTexture)
|
||||
target.Draw(vertexArray, states);
|
||||
|
||||
// 包围盒
|
||||
if (IsDebug && IsSelected && DebugBounds)
|
||||
{
|
||||
|
||||
@@ -10,7 +10,7 @@ using SpineRuntime36;
|
||||
|
||||
namespace SpineViewer.Spine.Implementations.Spine
|
||||
{
|
||||
[SpineImplementation(Version.V36)]
|
||||
[SpineImplementation(SpineVersion.V36)]
|
||||
internal class Spine36 : SpineViewer.Spine.Spine
|
||||
{
|
||||
private static readonly Animation EmptyAnimation = new(EMPTY_ANIMATION, [], 0);
|
||||
@@ -26,8 +26,6 @@ namespace SpineViewer.Spine.Implementations.Spine
|
||||
texture.Repeated = true;
|
||||
|
||||
page.rendererObject = texture;
|
||||
page.width = (int)texture.Size.X;
|
||||
page.height = (int)texture.Size.Y;
|
||||
}
|
||||
|
||||
public void Unload(object texture)
|
||||
@@ -48,7 +46,7 @@ namespace SpineViewer.Spine.Implementations.Spine
|
||||
|
||||
private SkeletonClipping clipping = new();
|
||||
|
||||
public Spine36(string skelPath, string? atlasPath = null) : base(skelPath, atlasPath)
|
||||
public Spine36(string skelPath, string atlasPath) : base(skelPath, atlasPath)
|
||||
{
|
||||
atlas = new Atlas(AtlasPath, textureLoader);
|
||||
try
|
||||
@@ -74,13 +72,15 @@ namespace SpineViewer.Spine.Implementations.Spine
|
||||
}
|
||||
}
|
||||
|
||||
animationStateData = new AnimationStateData(skeletonData);
|
||||
skeleton = new Skeleton(skeletonData);
|
||||
animationState = new AnimationState(animationStateData);
|
||||
foreach (var skin in skeletonData.Skins)
|
||||
skinNames.Add(skin.Name);
|
||||
|
||||
foreach (var anime in skeletonData.Animations)
|
||||
animationNames.Add(anime.Name);
|
||||
CurrentAnimation = DefaultAnimationName;
|
||||
|
||||
skeleton = new Skeleton(skeletonData);
|
||||
animationStateData = new AnimationStateData(skeletonData);
|
||||
animationState = new AnimationState(animationStateData);
|
||||
}
|
||||
|
||||
protected override void Dispose(bool disposing)
|
||||
@@ -108,7 +108,8 @@ namespace SpineViewer.Spine.Implementations.Spine
|
||||
var position = Position;
|
||||
var flipX = FlipX;
|
||||
var flipY = FlipY;
|
||||
var savedTrack0 = animationState.GetCurrent(0);
|
||||
var animation = Track0Animation; // TODO: 适配多轨道
|
||||
var skin = Skin;
|
||||
|
||||
var val = Math.Max(value, SCALE_MIN);
|
||||
if (skeletonBinary is not null)
|
||||
@@ -131,20 +132,8 @@ namespace SpineViewer.Spine.Implementations.Spine
|
||||
Position = position;
|
||||
FlipX = flipX;
|
||||
FlipY = flipY;
|
||||
|
||||
// 恢复原本 Track0 上所有动画
|
||||
if (savedTrack0 is not null)
|
||||
{
|
||||
var entry = animationState.SetAnimation(0, savedTrack0.Animation.Name, true);
|
||||
entry.TrackTime = savedTrack0.TrackTime;
|
||||
var savedEntry = savedTrack0.Next;
|
||||
while (savedEntry is not null)
|
||||
{
|
||||
entry = animationState.AddAnimation(0, savedEntry.Animation.Name, true, 0);
|
||||
entry.TrackTime = savedEntry.TrackTime;
|
||||
savedEntry = savedEntry.Next;
|
||||
}
|
||||
}
|
||||
Track0Animation = animation; // TODO: 适配多轨道
|
||||
Skin = skin;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -154,23 +143,36 @@ namespace SpineViewer.Spine.Implementations.Spine
|
||||
set
|
||||
{
|
||||
skeleton.X = value.X;
|
||||
skeleton.Y = value.Y;
|
||||
skeleton.Y = value.Y;
|
||||
Update(0);
|
||||
}
|
||||
}
|
||||
|
||||
public override bool FlipX
|
||||
{
|
||||
get => skeleton.FlipX;
|
||||
set => skeleton.FlipX = value;
|
||||
set { skeleton.FlipX = value; Update(0); }
|
||||
}
|
||||
|
||||
public override bool FlipY
|
||||
{
|
||||
get => skeleton.FlipY;
|
||||
set => skeleton.FlipY = value;
|
||||
set { skeleton.FlipY = value; Update(0); }
|
||||
}
|
||||
|
||||
public override string CurrentAnimation
|
||||
public override string Skin
|
||||
{
|
||||
get => skeleton.Skin?.Name ?? "default";
|
||||
set
|
||||
{
|
||||
if (!skinNames.Contains(value)) return;
|
||||
skeleton.SetSkin(value);
|
||||
skeleton.SetSlotsToSetupPose();
|
||||
Update(0);
|
||||
}
|
||||
}
|
||||
|
||||
public override string Track0Animation
|
||||
{
|
||||
get => animationState.GetCurrent(0)?.Animation.Name ?? EMPTY_ANIMATION;
|
||||
set
|
||||
@@ -286,10 +288,14 @@ namespace SpineViewer.Spine.Implementations.Spine
|
||||
if (vertexArray.VertexCount > 0)
|
||||
{
|
||||
if (UsePremultipliedAlpha && (states.BlendMode == BlendModeSFML.Normal || states.BlendMode == BlendModeSFML.Additive))
|
||||
states.Shader = FragmentShader;
|
||||
states.Shader = Shader.FragmentShader;
|
||||
else
|
||||
states.Shader = null;
|
||||
target.Draw(vertexArray, states);
|
||||
|
||||
// 调试纹理
|
||||
if (!IsDebug || DebugTexture)
|
||||
target.Draw(vertexArray, states);
|
||||
|
||||
vertexArray.Clear();
|
||||
}
|
||||
states.BlendMode = blendMode;
|
||||
@@ -329,13 +335,16 @@ namespace SpineViewer.Spine.Implementations.Spine
|
||||
|
||||
clipping.ClipEnd(slot);
|
||||
}
|
||||
clipping.ClipEnd();
|
||||
|
||||
if (UsePremultipliedAlpha && (states.BlendMode == BlendModeSFML.Normal || states.BlendMode == BlendModeSFML.Additive))
|
||||
states.Shader = FragmentShader;
|
||||
states.Shader = Shader.FragmentShader;
|
||||
else
|
||||
states.Shader = null;
|
||||
target.Draw(vertexArray, states);
|
||||
clipping.ClipEnd();
|
||||
|
||||
// 调试纹理
|
||||
if (!IsDebug || DebugTexture)
|
||||
target.Draw(vertexArray, states);
|
||||
|
||||
// 包围盒
|
||||
if (IsDebug && IsSelected && DebugBounds)
|
||||
|
||||
@@ -7,7 +7,7 @@ using SpineRuntime37;
|
||||
|
||||
namespace SpineViewer.Spine.Implementations.Spine
|
||||
{
|
||||
[SpineImplementation(Version.V37)]
|
||||
[SpineImplementation(SpineVersion.V37)]
|
||||
internal class Spine37 : SpineViewer.Spine.Spine
|
||||
{
|
||||
private static readonly Animation EmptyAnimation = new(EMPTY_ANIMATION, [], 0);
|
||||
@@ -23,8 +23,6 @@ namespace SpineViewer.Spine.Implementations.Spine
|
||||
texture.Repeated = true;
|
||||
|
||||
page.rendererObject = texture;
|
||||
page.width = (int)texture.Size.X;
|
||||
page.height = (int)texture.Size.Y;
|
||||
}
|
||||
|
||||
public void Unload(object texture)
|
||||
@@ -46,7 +44,7 @@ namespace SpineViewer.Spine.Implementations.Spine
|
||||
|
||||
private SkeletonClipping clipping = new();
|
||||
|
||||
public Spine37(string skelPath, string? atlasPath = null) : base(skelPath, atlasPath)
|
||||
public Spine37(string skelPath, string atlasPath) : base(skelPath, atlasPath)
|
||||
{
|
||||
atlas = new Atlas(AtlasPath, textureLoader);
|
||||
try
|
||||
@@ -72,14 +70,15 @@ namespace SpineViewer.Spine.Implementations.Spine
|
||||
}
|
||||
}
|
||||
|
||||
animationStateData = new AnimationStateData(skeletonData);
|
||||
skeleton = new Skeleton(skeletonData);
|
||||
animationState = new AnimationState(animationStateData);
|
||||
foreach (var skin in skeletonData.Skins)
|
||||
skinNames.Add(skin.Name);
|
||||
|
||||
foreach (var anime in skeletonData.Animations)
|
||||
animationNames.Add(anime.Name);
|
||||
|
||||
CurrentAnimation = DefaultAnimationName;
|
||||
skeleton = new Skeleton(skeletonData);
|
||||
animationStateData = new AnimationStateData(skeletonData);
|
||||
animationState = new AnimationState(animationStateData);
|
||||
}
|
||||
|
||||
protected override void Dispose(bool disposing)
|
||||
@@ -92,58 +91,12 @@ namespace SpineViewer.Spine.Implementations.Spine
|
||||
|
||||
public override float Scale
|
||||
{
|
||||
get
|
||||
{
|
||||
if (skeletonBinary is not null)
|
||||
return skeletonBinary.Scale;
|
||||
else if (skeletonJson is not null)
|
||||
return skeletonJson.Scale;
|
||||
else
|
||||
return 1f;
|
||||
}
|
||||
get => Math.Abs(skeleton.ScaleX);
|
||||
set
|
||||
{
|
||||
// 保存状态
|
||||
var position = Position;
|
||||
var flipX = FlipX;
|
||||
var flipY = FlipY;
|
||||
var savedTrack0 = animationState.GetCurrent(0);
|
||||
|
||||
var val = Math.Max(value, SCALE_MIN);
|
||||
if (skeletonBinary is not null)
|
||||
{
|
||||
skeletonBinary.Scale = val;
|
||||
skeletonData = skeletonBinary.ReadSkeletonData(SkelPath);
|
||||
}
|
||||
else if (skeletonJson is not null)
|
||||
{
|
||||
skeletonJson.Scale = val;
|
||||
skeletonData = skeletonJson.ReadSkeletonData(SkelPath);
|
||||
}
|
||||
|
||||
// reload skel-dependent data
|
||||
animationStateData = new AnimationStateData(skeletonData) { DefaultMix = animationStateData.DefaultMix };
|
||||
skeleton = new Skeleton(skeletonData);
|
||||
animationState = new AnimationState(animationStateData);
|
||||
|
||||
// 恢复状态
|
||||
Position = position;
|
||||
FlipX = flipX;
|
||||
FlipY = flipY;
|
||||
|
||||
// 恢复原本 Track0 上所有动画
|
||||
if (savedTrack0 is not null)
|
||||
{
|
||||
var entry = animationState.SetAnimation(0, savedTrack0.Animation.Name, true);
|
||||
entry.TrackTime = savedTrack0.TrackTime;
|
||||
var savedEntry = savedTrack0.Next;
|
||||
while (savedEntry is not null)
|
||||
{
|
||||
entry = animationState.AddAnimation(0, savedEntry.Animation.Name, true, 0);
|
||||
entry.TrackTime = savedEntry.TrackTime;
|
||||
savedEntry = savedEntry.Next;
|
||||
}
|
||||
}
|
||||
skeleton.ScaleX = Math.Sign(skeleton.ScaleX) * value;
|
||||
skeleton.ScaleY = Math.Sign(skeleton.ScaleY) * value;
|
||||
Update(0);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -154,6 +107,7 @@ namespace SpineViewer.Spine.Implementations.Spine
|
||||
{
|
||||
skeleton.X = value.X;
|
||||
skeleton.Y = value.Y;
|
||||
Update(0);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -164,6 +118,7 @@ namespace SpineViewer.Spine.Implementations.Spine
|
||||
{
|
||||
if (skeleton.ScaleX > 0 && value || skeleton.ScaleX < 0 && !value)
|
||||
skeleton.ScaleX *= -1;
|
||||
Update(0);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -174,10 +129,23 @@ namespace SpineViewer.Spine.Implementations.Spine
|
||||
{
|
||||
if (skeleton.ScaleY > 0 && value || skeleton.ScaleY < 0 && !value)
|
||||
skeleton.ScaleY *= -1;
|
||||
Update(0);
|
||||
}
|
||||
}
|
||||
|
||||
public override string CurrentAnimation
|
||||
public override string Skin
|
||||
{
|
||||
get => skeleton.Skin?.Name ?? "default";
|
||||
set
|
||||
{
|
||||
if (!skinNames.Contains(value)) return;
|
||||
skeleton.SetSkin(value);
|
||||
skeleton.SetSlotsToSetupPose();
|
||||
Update(0);
|
||||
}
|
||||
}
|
||||
|
||||
public override string Track0Animation
|
||||
{
|
||||
get => animationState.GetCurrent(0)?.Animation.Name ?? EMPTY_ANIMATION;
|
||||
set
|
||||
@@ -294,10 +262,14 @@ namespace SpineViewer.Spine.Implementations.Spine
|
||||
{
|
||||
// XXX: 实测不用设置 sampler2D 的值也正确
|
||||
if (UsePremultipliedAlpha && (states.BlendMode == BlendModeSFML.Normal || states.BlendMode == BlendModeSFML.Additive))
|
||||
states.Shader = FragmentShader;
|
||||
states.Shader = Shader.FragmentShader;
|
||||
else
|
||||
states.Shader = null;
|
||||
target.Draw(vertexArray, states);
|
||||
|
||||
// 调试纹理
|
||||
if (!IsDebug || DebugTexture)
|
||||
target.Draw(vertexArray, states);
|
||||
|
||||
vertexArray.Clear();
|
||||
}
|
||||
states.BlendMode = blendMode;
|
||||
@@ -337,13 +309,16 @@ namespace SpineViewer.Spine.Implementations.Spine
|
||||
|
||||
clipping.ClipEnd(slot);
|
||||
}
|
||||
clipping.ClipEnd();
|
||||
|
||||
if (UsePremultipliedAlpha && (states.BlendMode == BlendModeSFML.Normal || states.BlendMode == BlendModeSFML.Additive))
|
||||
states.Shader = FragmentShader;
|
||||
states.Shader = Shader.FragmentShader;
|
||||
else
|
||||
states.Shader = null;
|
||||
target.Draw(vertexArray, states);
|
||||
clipping.ClipEnd();
|
||||
|
||||
// 调试纹理
|
||||
if (!IsDebug || DebugTexture)
|
||||
target.Draw(vertexArray, states);
|
||||
|
||||
// 包围盒
|
||||
if (IsDebug && IsSelected && DebugBounds)
|
||||
|
||||
@@ -10,7 +10,7 @@ using SpineRuntime38.Attachments;
|
||||
|
||||
namespace SpineViewer.Spine.Implementations.Spine
|
||||
{
|
||||
[SpineImplementation(Version.V38)]
|
||||
[SpineImplementation(SpineVersion.V38)]
|
||||
internal class Spine38 : SpineViewer.Spine.Spine
|
||||
{
|
||||
private static readonly Animation EmptyAnimation = new(EMPTY_ANIMATION, [], 0);
|
||||
@@ -26,8 +26,9 @@ namespace SpineViewer.Spine.Implementations.Spine
|
||||
texture.Repeated = true;
|
||||
|
||||
page.rendererObject = texture;
|
||||
page.width = (int)texture.Size.X;
|
||||
page.height = (int)texture.Size.Y;
|
||||
// 似乎是不需要设置的, 因为存在某些 png 和 atlas 大小不同的情况, 一般是有一些缩放, 如果设置了反而渲染异常
|
||||
// page.width = (int)texture.Size.X;
|
||||
// page.height = (int)texture.Size.Y;
|
||||
}
|
||||
|
||||
public void Unload(object texture)
|
||||
@@ -49,7 +50,7 @@ namespace SpineViewer.Spine.Implementations.Spine
|
||||
|
||||
private SkeletonClipping clipping = new();
|
||||
|
||||
public Spine38(string skelPath, string? atlasPath = null) : base(skelPath, atlasPath)
|
||||
public Spine38(string skelPath, string atlasPath) : base(skelPath, atlasPath)
|
||||
{
|
||||
atlas = new Atlas(AtlasPath, textureLoader);
|
||||
try
|
||||
@@ -75,14 +76,15 @@ namespace SpineViewer.Spine.Implementations.Spine
|
||||
}
|
||||
}
|
||||
|
||||
animationStateData = new AnimationStateData(skeletonData);
|
||||
skeleton = new Skeleton(skeletonData);
|
||||
animationState = new AnimationState(animationStateData);
|
||||
foreach (var skin in skeletonData.Skins)
|
||||
skinNames.Add(skin.Name);
|
||||
|
||||
foreach (var anime in skeletonData.Animations)
|
||||
animationNames.Add(anime.Name);
|
||||
|
||||
CurrentAnimation = DefaultAnimationName;
|
||||
skeleton = new Skeleton(skeletonData);
|
||||
animationStateData = new AnimationStateData(skeletonData);
|
||||
animationState = new AnimationState(animationStateData);
|
||||
}
|
||||
|
||||
protected override void Dispose(bool disposing)
|
||||
@@ -95,58 +97,12 @@ namespace SpineViewer.Spine.Implementations.Spine
|
||||
|
||||
public override float Scale
|
||||
{
|
||||
get
|
||||
{
|
||||
if (skeletonBinary is not null)
|
||||
return skeletonBinary.Scale;
|
||||
else if (skeletonJson is not null)
|
||||
return skeletonJson.Scale;
|
||||
else
|
||||
return 1f;
|
||||
}
|
||||
get => Math.Abs(skeleton.ScaleX);
|
||||
set
|
||||
{
|
||||
// 保存状态
|
||||
var position = Position;
|
||||
var flipX = FlipX;
|
||||
var flipY = FlipY;
|
||||
var savedTrack0 = animationState.GetCurrent(0);
|
||||
|
||||
var val = Math.Max(value, SCALE_MIN);
|
||||
if (skeletonBinary is not null)
|
||||
{
|
||||
skeletonBinary.Scale = val;
|
||||
skeletonData = skeletonBinary.ReadSkeletonData(SkelPath);
|
||||
}
|
||||
else if (skeletonJson is not null)
|
||||
{
|
||||
skeletonJson.Scale = val;
|
||||
skeletonData = skeletonJson.ReadSkeletonData(SkelPath);
|
||||
}
|
||||
|
||||
// reload skel-dependent data
|
||||
animationStateData = new AnimationStateData(skeletonData) { DefaultMix = animationStateData.DefaultMix };
|
||||
skeleton = new Skeleton(skeletonData);
|
||||
animationState = new AnimationState(animationStateData);
|
||||
|
||||
// 恢复状态
|
||||
Position = position;
|
||||
FlipX = flipX;
|
||||
FlipY = flipY;
|
||||
|
||||
// 恢复原本 Track0 上所有动画
|
||||
if (savedTrack0 is not null)
|
||||
{
|
||||
var entry = animationState.SetAnimation(0, savedTrack0.Animation.Name, true);
|
||||
entry.TrackTime = savedTrack0.TrackTime;
|
||||
var savedEntry = savedTrack0.Next;
|
||||
while (savedEntry is not null)
|
||||
{
|
||||
entry = animationState.AddAnimation(0, savedEntry.Animation.Name, true, 0);
|
||||
entry.TrackTime = savedEntry.TrackTime;
|
||||
savedEntry = savedEntry.Next;
|
||||
}
|
||||
}
|
||||
skeleton.ScaleX = Math.Sign(skeleton.ScaleX) * value;
|
||||
skeleton.ScaleY = Math.Sign(skeleton.ScaleY) * value;
|
||||
Update(0);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -157,6 +113,7 @@ namespace SpineViewer.Spine.Implementations.Spine
|
||||
{
|
||||
skeleton.X = value.X;
|
||||
skeleton.Y = value.Y;
|
||||
Update(0);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -167,6 +124,7 @@ namespace SpineViewer.Spine.Implementations.Spine
|
||||
{
|
||||
if (skeleton.ScaleX > 0 && value || skeleton.ScaleX < 0 && !value)
|
||||
skeleton.ScaleX *= -1;
|
||||
Update(0);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -177,10 +135,23 @@ namespace SpineViewer.Spine.Implementations.Spine
|
||||
{
|
||||
if (skeleton.ScaleY > 0 && value || skeleton.ScaleY < 0 && !value)
|
||||
skeleton.ScaleY *= -1;
|
||||
Update(0);
|
||||
}
|
||||
}
|
||||
|
||||
public override string CurrentAnimation
|
||||
public override string Skin
|
||||
{
|
||||
get => skeleton.Skin?.Name ?? "default";
|
||||
set
|
||||
{
|
||||
if (!skinNames.Contains(value)) return;
|
||||
skeleton.SetSkin(value);
|
||||
skeleton.SetSlotsToSetupPose();
|
||||
Update(0);
|
||||
}
|
||||
}
|
||||
|
||||
public override string Track0Animation
|
||||
{
|
||||
get => animationState.GetCurrent(0)?.Animation.Name ?? EMPTY_ANIMATION;
|
||||
set
|
||||
@@ -297,10 +268,14 @@ namespace SpineViewer.Spine.Implementations.Spine
|
||||
{
|
||||
// XXX: 实测不用设置 sampler2D 的值也正确
|
||||
if (UsePremultipliedAlpha && (states.BlendMode == BlendModeSFML.Normal || states.BlendMode == BlendModeSFML.Additive))
|
||||
states.Shader = FragmentShader;
|
||||
states.Shader = Shader.FragmentShader;
|
||||
else
|
||||
states.Shader = null;
|
||||
target.Draw(vertexArray, states);
|
||||
|
||||
// 调试纹理
|
||||
if (!IsDebug || DebugTexture)
|
||||
target.Draw(vertexArray, states);
|
||||
|
||||
vertexArray.Clear();
|
||||
}
|
||||
states.BlendMode = blendMode;
|
||||
@@ -340,15 +315,18 @@ namespace SpineViewer.Spine.Implementations.Spine
|
||||
|
||||
clipping.ClipEnd(slot);
|
||||
}
|
||||
|
||||
if (UsePremultipliedAlpha && (states.BlendMode == BlendModeSFML.Normal || states.BlendMode == BlendModeSFML.Additive))
|
||||
states.Shader = FragmentShader;
|
||||
else
|
||||
states.Shader = null;
|
||||
target.Draw(vertexArray, states);
|
||||
clipping.ClipEnd();
|
||||
|
||||
// 包围盒
|
||||
if (UsePremultipliedAlpha && (states.BlendMode == BlendModeSFML.Normal || states.BlendMode == BlendModeSFML.Additive))
|
||||
states.Shader = Shader.FragmentShader;
|
||||
else
|
||||
states.Shader = null;
|
||||
|
||||
// 调试纹理
|
||||
if (!IsDebug || DebugTexture)
|
||||
target.Draw(vertexArray, states);
|
||||
|
||||
// 调试包围盒
|
||||
if (IsDebug && IsSelected && DebugBounds)
|
||||
{
|
||||
var bounds = Bounds;
|
||||
|
||||
@@ -9,7 +9,7 @@ using SpineRuntime40;
|
||||
|
||||
namespace SpineViewer.Spine.Implementations.Spine
|
||||
{
|
||||
[SpineImplementation(Version.V40)]
|
||||
[SpineImplementation(SpineVersion.V40)]
|
||||
internal class Spine40 : SpineViewer.Spine.Spine
|
||||
{
|
||||
private static readonly Animation EmptyAnimation = new(EMPTY_ANIMATION, [], 0);
|
||||
@@ -25,8 +25,6 @@ namespace SpineViewer.Spine.Implementations.Spine
|
||||
texture.Repeated = true;
|
||||
|
||||
page.rendererObject = texture;
|
||||
page.width = (int)texture.Size.X;
|
||||
page.height = (int)texture.Size.Y;
|
||||
}
|
||||
|
||||
public void Unload(object texture)
|
||||
@@ -48,7 +46,7 @@ namespace SpineViewer.Spine.Implementations.Spine
|
||||
|
||||
private SkeletonClipping clipping = new();
|
||||
|
||||
public Spine40(string skelPath, string? atlasPath = null) : base(skelPath, atlasPath)
|
||||
public Spine40(string skelPath, string atlasPath) : base(skelPath, atlasPath)
|
||||
{
|
||||
atlas = new Atlas(AtlasPath, textureLoader);
|
||||
try
|
||||
@@ -74,14 +72,15 @@ namespace SpineViewer.Spine.Implementations.Spine
|
||||
}
|
||||
}
|
||||
|
||||
animationStateData = new AnimationStateData(skeletonData);
|
||||
skeleton = new Skeleton(skeletonData);
|
||||
animationState = new AnimationState(animationStateData);
|
||||
foreach (var skin in skeletonData.Skins)
|
||||
skinNames.Add(skin.Name);
|
||||
|
||||
foreach (var anime in skeletonData.Animations)
|
||||
animationNames.Add(anime.Name);
|
||||
|
||||
CurrentAnimation = DefaultAnimationName;
|
||||
skeleton = new Skeleton(skeletonData);
|
||||
animationStateData = new AnimationStateData(skeletonData);
|
||||
animationState = new AnimationState(animationStateData);
|
||||
}
|
||||
|
||||
protected override void Dispose(bool disposing)
|
||||
@@ -94,58 +93,12 @@ namespace SpineViewer.Spine.Implementations.Spine
|
||||
|
||||
public override float Scale
|
||||
{
|
||||
get
|
||||
{
|
||||
if (skeletonBinary is not null)
|
||||
return skeletonBinary.Scale;
|
||||
else if (skeletonJson is not null)
|
||||
return skeletonJson.Scale;
|
||||
else
|
||||
return 1f;
|
||||
}
|
||||
get => Math.Abs(skeleton.ScaleX);
|
||||
set
|
||||
{
|
||||
// 保存状态
|
||||
var position = Position;
|
||||
var flipX = FlipX;
|
||||
var flipY = FlipY;
|
||||
var savedTrack0 = animationState.GetCurrent(0);
|
||||
|
||||
var val = Math.Max(value, SCALE_MIN);
|
||||
if (skeletonBinary is not null)
|
||||
{
|
||||
skeletonBinary.Scale = val;
|
||||
skeletonData = skeletonBinary.ReadSkeletonData(SkelPath);
|
||||
}
|
||||
else if (skeletonJson is not null)
|
||||
{
|
||||
skeletonJson.Scale = val;
|
||||
skeletonData = skeletonJson.ReadSkeletonData(SkelPath);
|
||||
}
|
||||
|
||||
// reload skel-dependent data
|
||||
animationStateData = new AnimationStateData(skeletonData) { DefaultMix = animationStateData.DefaultMix };
|
||||
skeleton = new Skeleton(skeletonData);
|
||||
animationState = new AnimationState(animationStateData);
|
||||
|
||||
// 恢复状态
|
||||
Position = position;
|
||||
FlipX = flipX;
|
||||
FlipY = flipY;
|
||||
|
||||
// 恢复原本 Track0 上所有动画
|
||||
if (savedTrack0 is not null)
|
||||
{
|
||||
var entry = animationState.SetAnimation(0, savedTrack0.Animation.Name, true);
|
||||
entry.TrackTime = savedTrack0.TrackTime;
|
||||
var savedEntry = savedTrack0.Next;
|
||||
while (savedEntry is not null)
|
||||
{
|
||||
entry = animationState.AddAnimation(0, savedEntry.Animation.Name, true, 0);
|
||||
entry.TrackTime = savedEntry.TrackTime;
|
||||
savedEntry = savedEntry.Next;
|
||||
}
|
||||
}
|
||||
skeleton.ScaleX = Math.Sign(skeleton.ScaleX) * value;
|
||||
skeleton.ScaleY = Math.Sign(skeleton.ScaleY) * value;
|
||||
Update(0);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -156,6 +109,7 @@ namespace SpineViewer.Spine.Implementations.Spine
|
||||
{
|
||||
skeleton.X = value.X;
|
||||
skeleton.Y = value.Y;
|
||||
Update(0);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -166,6 +120,7 @@ namespace SpineViewer.Spine.Implementations.Spine
|
||||
{
|
||||
if (skeleton.ScaleX > 0 && value || skeleton.ScaleX < 0 && !value)
|
||||
skeleton.ScaleX *= -1;
|
||||
Update(0);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -176,10 +131,23 @@ namespace SpineViewer.Spine.Implementations.Spine
|
||||
{
|
||||
if (skeleton.ScaleY > 0 && value || skeleton.ScaleY < 0 && !value)
|
||||
skeleton.ScaleY *= -1;
|
||||
Update(0);
|
||||
}
|
||||
}
|
||||
|
||||
public override string CurrentAnimation
|
||||
public override string Skin
|
||||
{
|
||||
get => skeleton.Skin?.Name ?? "default";
|
||||
set
|
||||
{
|
||||
if (!skinNames.Contains(value)) return;
|
||||
skeleton.SetSkin(value);
|
||||
skeleton.SetSlotsToSetupPose();
|
||||
Update(0);
|
||||
}
|
||||
}
|
||||
|
||||
public override string Track0Animation
|
||||
{
|
||||
get => animationState.GetCurrent(0)?.Animation.Name ?? EMPTY_ANIMATION;
|
||||
set
|
||||
@@ -296,10 +264,14 @@ namespace SpineViewer.Spine.Implementations.Spine
|
||||
{
|
||||
// XXX: 实测不用设置 sampler2D 的值也正确
|
||||
if (UsePremultipliedAlpha && (states.BlendMode == BlendModeSFML.Normal || states.BlendMode == BlendModeSFML.Additive))
|
||||
states.Shader = FragmentShader;
|
||||
states.Shader = Shader.FragmentShader;
|
||||
else
|
||||
states.Shader = null;
|
||||
target.Draw(vertexArray, states);
|
||||
|
||||
// 调试纹理
|
||||
if (!IsDebug || DebugTexture)
|
||||
target.Draw(vertexArray, states);
|
||||
|
||||
vertexArray.Clear();
|
||||
}
|
||||
states.BlendMode = blendMode;
|
||||
@@ -339,13 +311,16 @@ namespace SpineViewer.Spine.Implementations.Spine
|
||||
|
||||
clipping.ClipEnd(slot);
|
||||
}
|
||||
clipping.ClipEnd();
|
||||
|
||||
if (UsePremultipliedAlpha && (states.BlendMode == BlendModeSFML.Normal || states.BlendMode == BlendModeSFML.Additive))
|
||||
states.Shader = FragmentShader;
|
||||
states.Shader = Shader.FragmentShader;
|
||||
else
|
||||
states.Shader = null;
|
||||
target.Draw(vertexArray, states);
|
||||
clipping.ClipEnd();
|
||||
|
||||
// 调试纹理
|
||||
if (!IsDebug || DebugTexture)
|
||||
target.Draw(vertexArray, states);
|
||||
|
||||
// 包围盒
|
||||
if (IsDebug && IsSelected && DebugBounds)
|
||||
|
||||
@@ -9,7 +9,7 @@ using SpineRuntime41;
|
||||
|
||||
namespace SpineViewer.Spine.Implementations.Spine
|
||||
{
|
||||
[SpineImplementation(Version.V41)]
|
||||
[SpineImplementation(SpineVersion.V41)]
|
||||
internal class Spine41 : SpineViewer.Spine.Spine
|
||||
{
|
||||
private static readonly Animation EmptyAnimation = new(EMPTY_ANIMATION, [], 0);
|
||||
@@ -25,8 +25,6 @@ namespace SpineViewer.Spine.Implementations.Spine
|
||||
texture.Repeated = true;
|
||||
|
||||
page.rendererObject = texture;
|
||||
page.width = (int)texture.Size.X;
|
||||
page.height = (int)texture.Size.Y;
|
||||
}
|
||||
|
||||
public void Unload(object texture)
|
||||
@@ -48,7 +46,7 @@ namespace SpineViewer.Spine.Implementations.Spine
|
||||
|
||||
private SkeletonClipping clipping = new();
|
||||
|
||||
public Spine41(string skelPath, string? atlasPath = null) : base(skelPath, atlasPath)
|
||||
public Spine41(string skelPath, string atlasPath) : base(skelPath, atlasPath)
|
||||
{
|
||||
atlas = new Atlas(AtlasPath, textureLoader);
|
||||
try
|
||||
@@ -74,14 +72,15 @@ namespace SpineViewer.Spine.Implementations.Spine
|
||||
}
|
||||
}
|
||||
|
||||
animationStateData = new AnimationStateData(skeletonData);
|
||||
skeleton = new Skeleton(skeletonData);
|
||||
animationState = new AnimationState(animationStateData);
|
||||
foreach (var skin in skeletonData.Skins)
|
||||
skinNames.Add(skin.Name);
|
||||
|
||||
foreach (var anime in skeletonData.Animations)
|
||||
animationNames.Add(anime.Name);
|
||||
|
||||
CurrentAnimation = DefaultAnimationName;
|
||||
skeleton = new Skeleton(skeletonData);
|
||||
animationStateData = new AnimationStateData(skeletonData);
|
||||
animationState = new AnimationState(animationStateData);
|
||||
}
|
||||
|
||||
protected override void Dispose(bool disposing)
|
||||
@@ -94,58 +93,12 @@ namespace SpineViewer.Spine.Implementations.Spine
|
||||
|
||||
public override float Scale
|
||||
{
|
||||
get
|
||||
{
|
||||
if (skeletonBinary is not null)
|
||||
return skeletonBinary.Scale;
|
||||
else if (skeletonJson is not null)
|
||||
return skeletonJson.Scale;
|
||||
else
|
||||
return 1f;
|
||||
}
|
||||
get => Math.Abs(skeleton.ScaleX);
|
||||
set
|
||||
{
|
||||
// 保存状态
|
||||
var position = Position;
|
||||
var flipX = FlipX;
|
||||
var flipY = FlipY;
|
||||
var savedTrack0 = animationState.GetCurrent(0);
|
||||
|
||||
var val = Math.Max(value, SCALE_MIN);
|
||||
if (skeletonBinary is not null)
|
||||
{
|
||||
skeletonBinary.Scale = val;
|
||||
skeletonData = skeletonBinary.ReadSkeletonData(SkelPath);
|
||||
}
|
||||
else if (skeletonJson is not null)
|
||||
{
|
||||
skeletonJson.Scale = val;
|
||||
skeletonData = skeletonJson.ReadSkeletonData(SkelPath);
|
||||
}
|
||||
|
||||
// reload skel-dependent data
|
||||
animationStateData = new AnimationStateData(skeletonData) { DefaultMix = animationStateData.DefaultMix };
|
||||
skeleton = new Skeleton(skeletonData);
|
||||
animationState = new AnimationState(animationStateData);
|
||||
|
||||
// 恢复状态
|
||||
Position = position;
|
||||
FlipX = flipX;
|
||||
FlipY = flipY;
|
||||
|
||||
// 恢复原本 Track0 上所有动画
|
||||
if (savedTrack0 is not null)
|
||||
{
|
||||
var entry = animationState.SetAnimation(0, savedTrack0.Animation.Name, true);
|
||||
entry.TrackTime = savedTrack0.TrackTime;
|
||||
var savedEntry = savedTrack0.Next;
|
||||
while (savedEntry is not null)
|
||||
{
|
||||
entry = animationState.AddAnimation(0, savedEntry.Animation.Name, true, 0);
|
||||
entry.TrackTime = savedEntry.TrackTime;
|
||||
savedEntry = savedEntry.Next;
|
||||
}
|
||||
}
|
||||
skeleton.ScaleX = Math.Sign(skeleton.ScaleX) * value;
|
||||
skeleton.ScaleY = Math.Sign(skeleton.ScaleY) * value;
|
||||
Update(0);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -156,6 +109,7 @@ namespace SpineViewer.Spine.Implementations.Spine
|
||||
{
|
||||
skeleton.X = value.X;
|
||||
skeleton.Y = value.Y;
|
||||
Update(0);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -166,6 +120,7 @@ namespace SpineViewer.Spine.Implementations.Spine
|
||||
{
|
||||
if (skeleton.ScaleX > 0 && value || skeleton.ScaleX < 0 && !value)
|
||||
skeleton.ScaleX *= -1;
|
||||
Update(0);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -176,10 +131,23 @@ namespace SpineViewer.Spine.Implementations.Spine
|
||||
{
|
||||
if (skeleton.ScaleY > 0 && value || skeleton.ScaleY < 0 && !value)
|
||||
skeleton.ScaleY *= -1;
|
||||
Update(0);
|
||||
}
|
||||
}
|
||||
|
||||
public override string CurrentAnimation
|
||||
public override string Skin
|
||||
{
|
||||
get => skeleton.Skin?.Name ?? "default";
|
||||
set
|
||||
{
|
||||
if (!skinNames.Contains(value)) return;
|
||||
skeleton.SetSkin(value);
|
||||
skeleton.SetSlotsToSetupPose();
|
||||
Update(0);
|
||||
}
|
||||
}
|
||||
|
||||
public override string Track0Animation
|
||||
{
|
||||
get => animationState.GetCurrent(0)?.Animation.Name ?? EMPTY_ANIMATION;
|
||||
set
|
||||
@@ -296,10 +264,14 @@ namespace SpineViewer.Spine.Implementations.Spine
|
||||
{
|
||||
// XXX: 实测不用设置 sampler2D 的值也正确
|
||||
if (UsePremultipliedAlpha && (states.BlendMode == BlendModeSFML.Normal || states.BlendMode == BlendModeSFML.Additive))
|
||||
states.Shader = FragmentShader;
|
||||
states.Shader = Shader.FragmentShader;
|
||||
else
|
||||
states.Shader = null;
|
||||
target.Draw(vertexArray, states);
|
||||
|
||||
// 调试纹理
|
||||
if (!IsDebug || DebugTexture)
|
||||
target.Draw(vertexArray, states);
|
||||
|
||||
vertexArray.Clear();
|
||||
}
|
||||
states.BlendMode = blendMode;
|
||||
@@ -339,13 +311,16 @@ namespace SpineViewer.Spine.Implementations.Spine
|
||||
|
||||
clipping.ClipEnd(slot);
|
||||
}
|
||||
clipping.ClipEnd();
|
||||
|
||||
if (UsePremultipliedAlpha && (states.BlendMode == BlendModeSFML.Normal || states.BlendMode == BlendModeSFML.Additive))
|
||||
states.Shader = FragmentShader;
|
||||
states.Shader = Shader.FragmentShader;
|
||||
else
|
||||
states.Shader = null;
|
||||
target.Draw(vertexArray, states);
|
||||
clipping.ClipEnd();
|
||||
|
||||
// 调试纹理
|
||||
if (!IsDebug || DebugTexture)
|
||||
target.Draw(vertexArray, states);
|
||||
|
||||
// 包围盒
|
||||
if (IsDebug && IsSelected && DebugBounds)
|
||||
|
||||
@@ -9,7 +9,7 @@ using SpineRuntime42;
|
||||
|
||||
namespace SpineViewer.Spine.Implementations.Spine
|
||||
{
|
||||
[SpineImplementation(Version.V42)]
|
||||
[SpineImplementation(SpineVersion.V42)]
|
||||
internal class Spine42 : SpineViewer.Spine.Spine
|
||||
{
|
||||
private static readonly Animation EmptyAnimation = new(EMPTY_ANIMATION, [], 0);
|
||||
@@ -25,8 +25,6 @@ namespace SpineViewer.Spine.Implementations.Spine
|
||||
texture.Repeated = true;
|
||||
|
||||
page.rendererObject = texture;
|
||||
page.width = (int)texture.Size.X;
|
||||
page.height = (int)texture.Size.Y;
|
||||
}
|
||||
|
||||
public void Unload(object texture)
|
||||
@@ -48,7 +46,7 @@ namespace SpineViewer.Spine.Implementations.Spine
|
||||
|
||||
private SkeletonClipping clipping = new();
|
||||
|
||||
public Spine42(string skelPath, string? atlasPath = null) : base(skelPath, atlasPath)
|
||||
public Spine42(string skelPath, string atlasPath) : base(skelPath, atlasPath)
|
||||
{
|
||||
atlas = new Atlas(AtlasPath, textureLoader);
|
||||
try
|
||||
@@ -74,14 +72,15 @@ namespace SpineViewer.Spine.Implementations.Spine
|
||||
}
|
||||
}
|
||||
|
||||
animationStateData = new AnimationStateData(skeletonData);
|
||||
skeleton = new Skeleton(skeletonData);
|
||||
animationState = new AnimationState(animationStateData);
|
||||
foreach (var skin in skeletonData.Skins)
|
||||
skinNames.Add(skin.Name);
|
||||
|
||||
foreach (var anime in skeletonData.Animations)
|
||||
animationNames.Add(anime.Name);
|
||||
|
||||
CurrentAnimation = DefaultAnimationName;
|
||||
skeleton = new Skeleton(skeletonData);
|
||||
animationStateData = new AnimationStateData(skeletonData);
|
||||
animationState = new AnimationState(animationStateData);
|
||||
}
|
||||
|
||||
protected override void Dispose(bool disposing)
|
||||
@@ -94,58 +93,12 @@ namespace SpineViewer.Spine.Implementations.Spine
|
||||
|
||||
public override float Scale
|
||||
{
|
||||
get
|
||||
{
|
||||
if (skeletonBinary is not null)
|
||||
return skeletonBinary.Scale;
|
||||
else if (skeletonJson is not null)
|
||||
return skeletonJson.Scale;
|
||||
else
|
||||
return 1f;
|
||||
}
|
||||
get => Math.Abs(skeleton.ScaleX);
|
||||
set
|
||||
{
|
||||
// 保存状态
|
||||
var position = Position;
|
||||
var flipX = FlipX;
|
||||
var flipY = FlipY;
|
||||
var savedTrack0 = animationState.GetCurrent(0);
|
||||
|
||||
var val = Math.Max(value, SCALE_MIN);
|
||||
if (skeletonBinary is not null)
|
||||
{
|
||||
skeletonBinary.Scale = val;
|
||||
skeletonData = skeletonBinary.ReadSkeletonData(SkelPath);
|
||||
}
|
||||
else if (skeletonJson is not null)
|
||||
{
|
||||
skeletonJson.Scale = val;
|
||||
skeletonData = skeletonJson.ReadSkeletonData(SkelPath);
|
||||
}
|
||||
|
||||
// reload skel-dependent data
|
||||
animationStateData = new AnimationStateData(skeletonData) { DefaultMix = animationStateData.DefaultMix };
|
||||
skeleton = new Skeleton(skeletonData);
|
||||
animationState = new AnimationState(animationStateData);
|
||||
|
||||
// 恢复状态
|
||||
Position = position;
|
||||
FlipX = flipX;
|
||||
FlipY = flipY;
|
||||
|
||||
// 恢复原本 Track0 上所有动画
|
||||
if (savedTrack0 is not null)
|
||||
{
|
||||
var entry = animationState.SetAnimation(0, savedTrack0.Animation.Name, true);
|
||||
entry.TrackTime = savedTrack0.TrackTime;
|
||||
var savedEntry = savedTrack0.Next;
|
||||
while (savedEntry is not null)
|
||||
{
|
||||
entry = animationState.AddAnimation(0, savedEntry.Animation.Name, true, 0);
|
||||
entry.TrackTime = savedEntry.TrackTime;
|
||||
savedEntry = savedEntry.Next;
|
||||
}
|
||||
}
|
||||
skeleton.ScaleX = Math.Sign(skeleton.ScaleX) * value;
|
||||
skeleton.ScaleY = Math.Sign(skeleton.ScaleY) * value;
|
||||
Update(0);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -156,6 +109,7 @@ namespace SpineViewer.Spine.Implementations.Spine
|
||||
{
|
||||
skeleton.X = value.X;
|
||||
skeleton.Y = value.Y;
|
||||
Update(0);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -166,6 +120,7 @@ namespace SpineViewer.Spine.Implementations.Spine
|
||||
{
|
||||
if (skeleton.ScaleX > 0 && value || skeleton.ScaleX < 0 && !value)
|
||||
skeleton.ScaleX *= -1;
|
||||
Update(0);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -176,10 +131,23 @@ namespace SpineViewer.Spine.Implementations.Spine
|
||||
{
|
||||
if (skeleton.ScaleY > 0 && value || skeleton.ScaleY < 0 && !value)
|
||||
skeleton.ScaleY *= -1;
|
||||
Update(0);
|
||||
}
|
||||
}
|
||||
|
||||
public override string CurrentAnimation
|
||||
public override string Skin
|
||||
{
|
||||
get => skeleton.Skin?.Name ?? "default";
|
||||
set
|
||||
{
|
||||
if (!skinNames.Contains(value)) return;
|
||||
skeleton.SetSkin(value);
|
||||
skeleton.SetSlotsToSetupPose();
|
||||
Update(0);
|
||||
}
|
||||
}
|
||||
|
||||
public override string Track0Animation
|
||||
{
|
||||
get => animationState.GetCurrent(0)?.Animation.Name ?? EMPTY_ANIMATION;
|
||||
set
|
||||
@@ -296,10 +264,14 @@ namespace SpineViewer.Spine.Implementations.Spine
|
||||
{
|
||||
// XXX: 实测不用设置 sampler2D 的值也正确
|
||||
if (UsePremultipliedAlpha && (states.BlendMode == BlendModeSFML.Normal || states.BlendMode == BlendModeSFML.Additive))
|
||||
states.Shader = FragmentShader;
|
||||
states.Shader = Shader.FragmentShader;
|
||||
else
|
||||
states.Shader = null;
|
||||
target.Draw(vertexArray, states);
|
||||
|
||||
// 调试纹理
|
||||
if (!IsDebug || DebugTexture)
|
||||
target.Draw(vertexArray, states);
|
||||
|
||||
vertexArray.Clear();
|
||||
}
|
||||
states.BlendMode = blendMode;
|
||||
@@ -339,13 +311,16 @@ namespace SpineViewer.Spine.Implementations.Spine
|
||||
|
||||
clipping.ClipEnd(slot);
|
||||
}
|
||||
clipping.ClipEnd();
|
||||
|
||||
if (UsePremultipliedAlpha && (states.BlendMode == BlendModeSFML.Normal || states.BlendMode == BlendModeSFML.Additive))
|
||||
states.Shader = FragmentShader;
|
||||
states.Shader = Shader.FragmentShader;
|
||||
else
|
||||
states.Shader = null;
|
||||
target.Draw(vertexArray, states);
|
||||
clipping.ClipEnd();
|
||||
|
||||
// 调试纹理
|
||||
if (!IsDebug || DebugTexture)
|
||||
target.Draw(vertexArray, states);
|
||||
|
||||
// 包围盒
|
||||
if (IsDebug && IsSelected && DebugBounds)
|
||||
|
||||
35
SpineViewer/Spine/Shader.cs
Normal file
35
SpineViewer/Spine/Shader.cs
Normal file
@@ -0,0 +1,35 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace SpineViewer.Spine
|
||||
{
|
||||
public static class Shader
|
||||
{
|
||||
/// <summary>
|
||||
/// 用于解决 PMA 和渐变动画问题的片段着色器
|
||||
/// </summary>
|
||||
private const string FRAGMENT_SHADER = (
|
||||
"uniform sampler2D t;" +
|
||||
"void main() { vec4 p = texture2D(t, gl_TexCoord[0].xy);" +
|
||||
"if (p.a > 0) p.rgb /= max(max(max(p.r, p.g), p.b), p.a);" +
|
||||
"gl_FragColor = gl_Color * p; }"
|
||||
);
|
||||
|
||||
/// <summary>
|
||||
/// 针对预乘 Alpha 通道的片段着色器
|
||||
/// </summary>
|
||||
public static SFML.Graphics.Shader? FragmentShader { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// 加载 Shader, 可能会存在异常导致着色器加载失败
|
||||
/// </summary>
|
||||
/// <exception cref="SFML.LoadingFailedException"></exception>
|
||||
public static void Init()
|
||||
{
|
||||
FragmentShader = SFML.Graphics.Shader.FromString(null, null, FRAGMENT_SHADER);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -15,46 +15,12 @@ namespace SpineViewer.Spine
|
||||
/// <summary>
|
||||
/// SkeletonConverter 基类, 使用静态方法 New 来创建具体版本对象
|
||||
/// </summary>
|
||||
public abstract class SkeletonConverter
|
||||
public abstract class SkeletonConverter : ImplementationResolver<SkeletonConverter, SpineImplementationAttribute, SpineVersion>
|
||||
{
|
||||
/// <summary>
|
||||
/// 实现类缓存
|
||||
/// </summary>
|
||||
private static readonly Dictionary<Version, Type> ImplementationTypes = [];
|
||||
public static readonly Dictionary<Version, Type>.KeyCollection ImplementedVersions;
|
||||
|
||||
/// <summary>
|
||||
/// 静态构造函数
|
||||
/// </summary>
|
||||
static SkeletonConverter()
|
||||
{
|
||||
// 遍历并缓存标记了 SpineImplementationAttribute 的类型
|
||||
var impTypes = Assembly.GetExecutingAssembly().GetTypes().Where(t => typeof(SkeletonConverter).IsAssignableFrom(t) && !t.IsAbstract);
|
||||
foreach (var type in impTypes)
|
||||
{
|
||||
var attr = type.GetCustomAttribute<SpineImplementationAttribute>();
|
||||
if (attr is not null)
|
||||
{
|
||||
if (ImplementationTypes.ContainsKey(attr.Version))
|
||||
throw new InvalidOperationException($"Multiple implementations found: {attr.Version}");
|
||||
ImplementationTypes[attr.Version] = type;
|
||||
}
|
||||
}
|
||||
Program.Logger.Debug("Find SkeletonConverter implementations: [{}]", string.Join(", ", ImplementationTypes.Keys));
|
||||
ImplementedVersions = ImplementationTypes.Keys;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 创建特定版本的 SkeletonConverter
|
||||
/// </summary>
|
||||
public static SkeletonConverter New(Version version)
|
||||
{
|
||||
if (!ImplementationTypes.TryGetValue(version, out var cvterType))
|
||||
{
|
||||
throw new NotImplementedException($"Not implemented version: {version}");
|
||||
}
|
||||
return (SkeletonConverter)Activator.CreateInstance(cvterType);
|
||||
}
|
||||
public static SkeletonConverter New(SpineVersion version) => New(version, []);
|
||||
|
||||
/// <summary>
|
||||
/// Json 格式控制
|
||||
@@ -123,7 +89,7 @@ namespace SpineViewer.Spine
|
||||
/// <summary>
|
||||
/// 转换到目标版本
|
||||
/// </summary>
|
||||
public abstract JsonObject ToVersion(JsonObject root, Version version);
|
||||
public abstract JsonObject ToVersion(JsonObject root, SpineVersion version);
|
||||
|
||||
/// <summary>
|
||||
/// 二进制骨骼文件读
|
||||
|
||||
@@ -21,311 +21,253 @@ namespace SpineViewer.Spine
|
||||
/// <summary>
|
||||
/// Spine 基类, 使用静态方法 New 来创建具体版本对象
|
||||
/// </summary>
|
||||
public abstract class Spine : SFML.Graphics.Drawable, IDisposable
|
||||
public abstract class Spine : ImplementationResolver<Spine, SpineImplementationAttribute, SpineVersion>, SFML.Graphics.Drawable, IDisposable
|
||||
{
|
||||
/// <summary>
|
||||
/// 常规骨骼文件后缀集合
|
||||
/// </summary>
|
||||
public static readonly ImmutableHashSet<string> CommonSkelSuffix = [".skel", ".json"];
|
||||
|
||||
/// <summary>
|
||||
/// 空动画标记
|
||||
/// </summary>
|
||||
public const string EMPTY_ANIMATION = "<Empty>";
|
||||
protected const string EMPTY_ANIMATION = "<Empty>";
|
||||
|
||||
/// <summary>
|
||||
/// 预览图宽
|
||||
/// </summary>
|
||||
public const uint PREVIEW_WIDTH = 256;
|
||||
protected const uint PREVIEW_WIDTH = 256;
|
||||
|
||||
/// <summary>
|
||||
/// 预览图高
|
||||
/// </summary>
|
||||
public const uint PREVIEW_HEIGHT = 256;
|
||||
protected const uint PREVIEW_HEIGHT = 256;
|
||||
|
||||
/// <summary>
|
||||
/// 缩放最小值
|
||||
/// </summary>
|
||||
public const float SCALE_MIN = 0.001f;
|
||||
|
||||
/// <summary>
|
||||
/// 实现类缓存
|
||||
/// </summary>
|
||||
private static readonly Dictionary<Version, Type> ImplementationTypes = [];
|
||||
public static readonly Dictionary<Version, Type>.KeyCollection ImplementedVersions;
|
||||
|
||||
/// <summary>
|
||||
/// 用于解决 PMA 和渐变动画问题的片段着色器
|
||||
/// </summary>
|
||||
private const string FRAGMENT_SHADER = (
|
||||
"uniform sampler2D t;" +
|
||||
"void main() { vec4 p = texture2D(t, gl_TexCoord[0].xy);" +
|
||||
"if (p.a > 0) p.rgb /= max(max(max(p.r, p.g), p.b), p.a);" +
|
||||
"gl_FragColor = gl_Color * p; }"
|
||||
);
|
||||
|
||||
/// <summary>
|
||||
/// 用于解决 PMA 和渐变动画问题的片段着色器
|
||||
/// </summary>
|
||||
protected static readonly SFML.Graphics.Shader? FragmentShader = null;
|
||||
|
||||
/// <summary>
|
||||
/// 静态构造函数
|
||||
/// </summary>
|
||||
static Spine()
|
||||
{
|
||||
// 遍历并缓存标记了 SpineImplementationAttribute 的类型
|
||||
var impTypes = Assembly.GetExecutingAssembly().GetTypes().Where(t => typeof(Spine).IsAssignableFrom(t) && !t.IsAbstract);
|
||||
foreach (var type in impTypes)
|
||||
{
|
||||
var attr = type.GetCustomAttribute<SpineImplementationAttribute>();
|
||||
if (attr is not null)
|
||||
{
|
||||
if (ImplementationTypes.ContainsKey(attr.Version))
|
||||
throw new InvalidOperationException($"Multiple implementations found: {attr.Version}");
|
||||
ImplementationTypes[attr.Version] = type;
|
||||
}
|
||||
}
|
||||
Program.Logger.Debug("Find Spine implementations: [{}]", string.Join(", ", ImplementationTypes.Keys));
|
||||
ImplementedVersions = ImplementationTypes.Keys;
|
||||
|
||||
// 加载 FragmentShader
|
||||
try
|
||||
{
|
||||
FragmentShader = SFML.Graphics.Shader.FromString(null, null, FRAGMENT_SHADER);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
FragmentShader = null;
|
||||
Program.Logger.Error(ex.ToString());
|
||||
Program.Logger.Error("Failed to load fragment shader");
|
||||
MessageBox.Warn("Fragment shader 加载失败,预乘Alpha通道属性失效");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 尝试检测骨骼文件版本
|
||||
/// </summary>
|
||||
public static Version? GetVersion(string skelPath)
|
||||
{
|
||||
string versionString = null;
|
||||
Version? version = null;
|
||||
using var input = File.OpenRead(skelPath);
|
||||
var reader = new SkeletonConverter.BinaryReader(input);
|
||||
|
||||
// try json format
|
||||
try
|
||||
{
|
||||
if (JsonNode.Parse(input) is JsonObject root && root.TryGetPropertyValue("skeleton", out var node) &&
|
||||
node is JsonObject _skeleton && _skeleton.TryGetPropertyValue("spine", out var _version))
|
||||
versionString = (string)_version;
|
||||
}
|
||||
catch { }
|
||||
|
||||
// try v4 binary format
|
||||
if (versionString is null)
|
||||
{
|
||||
try
|
||||
{
|
||||
input.Position = 0;
|
||||
var hash = reader.ReadLong();
|
||||
var versionPosition = input.Position;
|
||||
var versionByteCount = reader.ReadVarInt();
|
||||
input.Position = versionPosition;
|
||||
if (versionByteCount <= 13)
|
||||
versionString = reader.ReadString();
|
||||
}
|
||||
catch { }
|
||||
}
|
||||
|
||||
// try v3 binary format
|
||||
if (versionString is null)
|
||||
{
|
||||
try
|
||||
{
|
||||
input.Position = 0;
|
||||
var hash = reader.ReadString();
|
||||
versionString = reader.ReadString();
|
||||
}
|
||||
catch { }
|
||||
}
|
||||
|
||||
if (versionString is not null)
|
||||
{
|
||||
if (versionString.StartsWith("2.1.")) version = Version.V21;
|
||||
else if (versionString.StartsWith("3.6.")) version = Version.V36;
|
||||
else if (versionString.StartsWith("3.7.")) version = Version.V37;
|
||||
else if (versionString.StartsWith("3.8.")) version = Version.V38;
|
||||
else if (versionString.StartsWith("4.0.")) version = Version.V40;
|
||||
else if (versionString.StartsWith("4.1.")) version = Version.V41;
|
||||
else if (versionString.StartsWith("4.2.")) version = Version.V42;
|
||||
else if (versionString.StartsWith("4.3.")) version = Version.V43;
|
||||
else Program.Logger.Error("Unknown verison: {}, {}", versionString, skelPath);
|
||||
}
|
||||
|
||||
return version;
|
||||
}
|
||||
protected const float SCALE_MIN = 0.001f;
|
||||
|
||||
/// <summary>
|
||||
/// 创建特定版本的 Spine
|
||||
/// </summary>
|
||||
public static Spine New(Version version, string skelPath, string? atlasPath = null)
|
||||
public static Spine New(SpineVersion version, string skelPath, string? atlasPath = null)
|
||||
{
|
||||
if (version == Version.Auto)
|
||||
{
|
||||
if (GetVersion(skelPath) is Version detectedVersion)
|
||||
version = detectedVersion;
|
||||
else
|
||||
throw new InvalidDataException($"Auto version detection failed for {skelPath}, try to use a specific version");
|
||||
}
|
||||
if (!ImplementationTypes.TryGetValue(version, out var spineType))
|
||||
{
|
||||
throw new NotImplementedException($"Not implemented version: {version}");
|
||||
}
|
||||
return (Spine)Activator.CreateInstance(spineType, skelPath, atlasPath);
|
||||
if (version == SpineVersion.Auto) version = SpineHelper.GetVersion(skelPath);
|
||||
atlasPath ??= Path.ChangeExtension(skelPath, ".atlas");
|
||||
return New(version, [skelPath, atlasPath]).PostInit();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 构造函数
|
||||
/// </summary>
|
||||
public Spine(string skelPath, string atlasPath)
|
||||
{
|
||||
Version = GetType().GetCustomAttribute<SpineImplementationAttribute>().ImplementationKey;
|
||||
AssetsDir = Directory.GetParent(skelPath).FullName;
|
||||
SkelPath = Path.GetFullPath(skelPath);
|
||||
AtlasPath = Path.GetFullPath(atlasPath);
|
||||
Name = Path.GetFileNameWithoutExtension(skelPath);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 构造函数之后的初始化工作
|
||||
/// </summary>
|
||||
private Spine PostInit()
|
||||
{
|
||||
SkinNames = skinNames.AsReadOnly();
|
||||
AnimationNames = animationNames.AsReadOnly();
|
||||
|
||||
InitBounds = Bounds;
|
||||
|
||||
// XXX: tex 没办法在这里主动 Dispose
|
||||
// 批量添加在获取预览图的时候极大概率会和预览线程死锁
|
||||
// 虽然两边不会同时调用 Draw, 但是死锁似乎和 Draw 函数有关
|
||||
// 除此之外, 似乎还和 tex 的 Dispose 有关
|
||||
// 如果不对 tex 进行 Dispose, 那么不管是否 Draw 都正常不会死锁
|
||||
var tex = new SFML.Graphics.RenderTexture(PREVIEW_WIDTH, PREVIEW_HEIGHT);
|
||||
tex.SetView(InitBounds.GetView(PREVIEW_WIDTH, PREVIEW_HEIGHT));
|
||||
tex.Clear(SFML.Graphics.Color.Transparent);
|
||||
tex.Draw(this);
|
||||
tex.Display();
|
||||
|
||||
using (var img = tex.Texture.CopyToImage())
|
||||
{
|
||||
if (img.SaveToMemory(out var imgBuffer, "bmp"))
|
||||
{
|
||||
// 必须重复构造一个副本才能摆脱对流的依赖, 否则之后使用会报错
|
||||
using var stream = new MemoryStream(imgBuffer);
|
||||
using var bitmap = new Bitmap(stream);
|
||||
Preview = new Bitmap(bitmap);
|
||||
}
|
||||
}
|
||||
|
||||
// 取最后一个作为初始, 尽可能去显示非默认的内容
|
||||
Skin = SkinNames.Last();
|
||||
Track0Animation = AnimationNames.Last();
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
~Spine() { Dispose(false); }
|
||||
public void Dispose() { Dispose(true); GC.SuppressFinalize(this); }
|
||||
protected virtual void Dispose(bool disposing) { Preview?.Dispose(); }
|
||||
|
||||
#region 属性 | [0] 基本信息
|
||||
|
||||
/// <summary>
|
||||
/// 获取所属版本
|
||||
/// </summary>
|
||||
[TypeConverter(typeof(SpineVersionConverter))]
|
||||
[Category("[0] 基本信息"), DisplayName("运行时版本")]
|
||||
public SpineVersion Version { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 资源所在完整目录
|
||||
/// </summary>
|
||||
[Category("[0] 基本信息"), DisplayName("资源目录")]
|
||||
public string AssetsDir { get; }
|
||||
|
||||
/// <summary>
|
||||
/// skel 文件完整路径
|
||||
/// </summary>
|
||||
[Category("[0] 基本信息"), DisplayName("skel文件路径")]
|
||||
public string SkelPath { get; }
|
||||
|
||||
/// <summary>
|
||||
/// atlas 文件完整路径
|
||||
/// </summary>
|
||||
[Category("[0] 基本信息"), DisplayName("atlas文件路径")]
|
||||
public string AtlasPath { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 名称
|
||||
/// </summary>
|
||||
[Category("[0] 基本信息"), DisplayName("名称")]
|
||||
public string Name { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 获取所属文件版本
|
||||
/// </summary>
|
||||
[Category("[0] 基本信息"), DisplayName("文件版本")]
|
||||
public abstract string FileVersion { get; }
|
||||
|
||||
#endregion
|
||||
|
||||
#region 属性 | [1] 设置
|
||||
|
||||
/// <summary>
|
||||
/// 是否被隐藏, 被隐藏的模型将仅仅在列表显示, 不参与其他行为
|
||||
/// </summary>
|
||||
[Category("[1] 设置"), DisplayName("是否隐藏")]
|
||||
public bool IsHidden { get; set; } = false;
|
||||
|
||||
/// <summary>
|
||||
/// 是否使用预乘Alpha
|
||||
/// </summary>
|
||||
[Category("[1] 设置"), DisplayName("预乘Alpha通道")]
|
||||
public bool UsePremultipliedAlpha { get; set; } = true;
|
||||
|
||||
#endregion
|
||||
|
||||
#region 属性 | [2] 变换
|
||||
|
||||
/// <summary>
|
||||
/// 缩放比例
|
||||
/// </summary>
|
||||
[Category("[2] 变换"), DisplayName("缩放比例")]
|
||||
public abstract float Scale { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 位置
|
||||
/// </summary>
|
||||
[TypeConverter(typeof(PointFConverter))]
|
||||
[Category("[2] 变换"), DisplayName("位置")]
|
||||
public abstract PointF Position { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 水平翻转
|
||||
/// </summary>
|
||||
[Category("[2] 变换"), DisplayName("水平翻转")]
|
||||
public abstract bool FlipX { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 垂直翻转
|
||||
/// </summary>
|
||||
[Category("[2] 变换"), DisplayName("垂直翻转")]
|
||||
public abstract bool FlipY { get; set; }
|
||||
|
||||
#endregion
|
||||
|
||||
#region 属性 | [3] 动画
|
||||
|
||||
/// <summary>
|
||||
/// 包含的所有皮肤名称
|
||||
/// </summary>
|
||||
public ReadOnlyCollection<string> SkinNames { get; private set; }
|
||||
protected List<string> skinNames = [];
|
||||
|
||||
/// <summary>
|
||||
/// 使用的皮肤名称, 如果设置的皮肤不存在则忽略
|
||||
/// </summary>
|
||||
[TypeConverter(typeof(SkinConverter))]
|
||||
[Category("[3] 动画"), DisplayName("皮肤")]
|
||||
public abstract string Skin { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 包含的所有动画名称
|
||||
/// </summary>
|
||||
public ReadOnlyCollection<string> AnimationNames { get; private set; }
|
||||
protected List<string> animationNames = [EMPTY_ANIMATION];
|
||||
|
||||
/// <summary>
|
||||
/// 默认轨道动画名称, 如果设置的动画不存在则忽略
|
||||
/// </summary>
|
||||
[TypeConverter(typeof(AnimationConverter))]
|
||||
[Category("[3] 动画"), DisplayName("默认轨道动画")]
|
||||
public abstract string Track0Animation { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 默认轨道动画时长
|
||||
/// </summary>
|
||||
[Category("[3] 动画"), DisplayName("默认轨道动画时长")]
|
||||
public float Track0AnimationDuration { get => GetAnimationDuration(Track0Animation); } // TODO: 动画时长变成伪属性在面板显示
|
||||
|
||||
#endregion
|
||||
|
||||
#region 属性 | [4] 调试
|
||||
|
||||
/// <summary>
|
||||
/// 显示调试
|
||||
/// </summary>
|
||||
[Browsable(false)]
|
||||
public bool IsDebug { get; set; } = false;
|
||||
|
||||
/// <summary>
|
||||
/// 显示纹理
|
||||
/// </summary>
|
||||
[Category("[4] 调试"), DisplayName("显示纹理")]
|
||||
public bool DebugTexture { get; set; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// 显示包围盒
|
||||
/// </summary>
|
||||
[Category("[4] 调试"), DisplayName("显示包围盒")]
|
||||
public bool DebugBounds { get; set; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// 显示骨骼
|
||||
/// </summary>
|
||||
[Category("[4] 调试"), DisplayName("显示骨架")]
|
||||
public bool DebugBones { get; set; } = false;
|
||||
|
||||
#endregion
|
||||
|
||||
/// <summary>
|
||||
/// 标识符
|
||||
/// </summary>
|
||||
public readonly string ID = Guid.NewGuid().ToString();
|
||||
|
||||
/// <summary>
|
||||
/// 构造函数
|
||||
/// </summary>
|
||||
public Spine(string skelPath, string? atlasPath = null)
|
||||
{
|
||||
// 获取子类类型
|
||||
var type = GetType();
|
||||
var attr = type.GetCustomAttribute<SpineImplementationAttribute>();
|
||||
if (attr is null)
|
||||
{
|
||||
throw new InvalidOperationException($"Class {type.Name} has no SpineImplementationAttribute");
|
||||
}
|
||||
|
||||
atlasPath ??= Path.ChangeExtension(skelPath, ".atlas");
|
||||
|
||||
// 设置 Version
|
||||
Version = attr.Version;
|
||||
AssetsDir = Directory.GetParent(skelPath).FullName;
|
||||
SkelPath = Path.GetFullPath(skelPath);
|
||||
AtlasPath = Path.GetFullPath(atlasPath);
|
||||
Name = Path.GetFileNameWithoutExtension(skelPath);
|
||||
}
|
||||
|
||||
~Spine() { Dispose(false); }
|
||||
public void Dispose() { Dispose(true); GC.SuppressFinalize(this); }
|
||||
protected virtual void Dispose(bool disposing) { preview?.Dispose(); }
|
||||
|
||||
#region 属性 | 基本信息
|
||||
|
||||
/// <summary>
|
||||
/// 获取所属版本
|
||||
/// </summary>
|
||||
[TypeConverter(typeof(VersionConverter))]
|
||||
[Category("基本信息"), DisplayName("运行时版本")]
|
||||
public Version Version { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 资源所在完整目录
|
||||
/// </summary>
|
||||
[Category("基本信息"), DisplayName("资源目录")]
|
||||
public string AssetsDir { get; }
|
||||
|
||||
/// <summary>
|
||||
/// skel 文件完整路径
|
||||
/// </summary>
|
||||
[Category("基本信息"), DisplayName("skel文件路径")]
|
||||
public string SkelPath { get; }
|
||||
|
||||
/// <summary>
|
||||
/// atlas 文件完整路径
|
||||
/// </summary>
|
||||
[Category("基本信息"), DisplayName("atlas文件路径")]
|
||||
public string AtlasPath { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 名称
|
||||
/// </summary>
|
||||
[Category("基本信息"), DisplayName("名称")]
|
||||
public string Name { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 获取所属文件版本
|
||||
/// </summary>
|
||||
[Category("基本信息"), DisplayName("文件版本")]
|
||||
public abstract string FileVersion { get; }
|
||||
|
||||
#endregion
|
||||
|
||||
#region 属性 | 变换
|
||||
|
||||
/// <summary>
|
||||
/// 缩放比例
|
||||
/// </summary>
|
||||
[Category("变换"), DisplayName("缩放比例")]
|
||||
public abstract float Scale { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 位置
|
||||
/// </summary>
|
||||
[TypeConverter(typeof(PointFConverter))]
|
||||
[Category("变换"), DisplayName("位置")]
|
||||
public abstract PointF Position { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 水平翻转
|
||||
/// </summary>
|
||||
[Category("变换"), DisplayName("水平翻转")]
|
||||
public abstract bool FlipX { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 垂直翻转
|
||||
/// </summary>
|
||||
[Category("变换"), DisplayName("垂直翻转")]
|
||||
public abstract bool FlipY { get; set; }
|
||||
|
||||
#endregion
|
||||
|
||||
#region 属性 | 画面
|
||||
|
||||
/// <summary>
|
||||
/// 是否使用预乘Alpha
|
||||
/// </summary>
|
||||
[Category("画面"), DisplayName("预乘Alpha通道")]
|
||||
public bool UsePremultipliedAlpha { get; set; } = true;
|
||||
|
||||
#endregion
|
||||
|
||||
/// <summary>
|
||||
/// 包含的所有动画名称
|
||||
/// 是否被选中
|
||||
/// </summary>
|
||||
[Browsable(false)]
|
||||
public ReadOnlyCollection<string> AnimationNames { get => animationNames.AsReadOnly(); }
|
||||
protected List<string> animationNames = [EMPTY_ANIMATION];
|
||||
|
||||
/// <summary>
|
||||
/// 默认动画名称
|
||||
/// </summary>
|
||||
[Browsable(false)]
|
||||
public string DefaultAnimationName { get => animationNames.Last(); }
|
||||
|
||||
#region 属性 | 动画
|
||||
|
||||
/// <summary>
|
||||
/// 当前动画名称, 如果设置的动画不存在则忽略
|
||||
/// </summary>
|
||||
[TypeConverter(typeof(AnimationConverter))]
|
||||
[Category("动画"), DisplayName("当前动画")]
|
||||
public abstract string CurrentAnimation { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 当前动画时长
|
||||
/// </summary>
|
||||
[Category("动画"), DisplayName("当前动画时长")]
|
||||
public float CurrentAnimationDuration { get => GetAnimationDuration(CurrentAnimation); }
|
||||
|
||||
#endregion
|
||||
public bool IsSelected { get; set; } = false;
|
||||
|
||||
/// <summary>
|
||||
/// 骨骼包围盒
|
||||
@@ -337,55 +279,13 @@ namespace SpineViewer.Spine
|
||||
/// 初始状态下的骨骼包围盒
|
||||
/// </summary>
|
||||
[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;
|
||||
public RectangleF InitBounds { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// 骨骼预览图
|
||||
/// </summary>
|
||||
[Browsable(false)]
|
||||
public Image Preview
|
||||
{
|
||||
get
|
||||
{
|
||||
if (preview is null)
|
||||
{
|
||||
// XXX: tex 没办法在这里主动 Dispose
|
||||
// 批量添加在获取预览图的时候极大概率会和预览线程死锁
|
||||
// 虽然两边不会同时调用 Draw, 但是死锁似乎和 Draw 函数有关
|
||||
// 除此之外, 似乎还和 tex 的 Dispose 有关
|
||||
// 如果不对 tex 进行 Dispose, 那么不管是否 Draw 都正常不会死锁
|
||||
var tex = new SFML.Graphics.RenderTexture(PREVIEW_WIDTH, PREVIEW_HEIGHT);
|
||||
tex.SetView(InitBounds.GetView(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");
|
||||
using var stream = new MemoryStream(imgBuffer);
|
||||
preview = new Bitmap(new Bitmap(stream)); // 必须重复构造一个副本才能摆脱对流的依赖, 否则之后使用会报错
|
||||
}
|
||||
return preview;
|
||||
}
|
||||
}
|
||||
private Image preview = null;
|
||||
public Image Preview { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// 获取动画时长, 如果动画不存在则返回 0
|
||||
@@ -395,43 +295,8 @@ namespace SpineViewer.Spine
|
||||
/// <summary>
|
||||
/// 更新内部状态
|
||||
/// </summary>
|
||||
/// <param name="delta">时间间隔</param>
|
||||
public abstract void Update(float delta);
|
||||
|
||||
/// <summary>
|
||||
/// 是否被选中
|
||||
/// </summary>
|
||||
[Browsable(false)]
|
||||
public bool IsSelected { get; set; } = false;
|
||||
|
||||
/// <summary>
|
||||
/// 显示调试
|
||||
/// </summary>
|
||||
[Browsable(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>
|
||||
[Category("调试"), DisplayName("显示包围盒")]
|
||||
public bool DebugBounds { get; set; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// 显示骨骼
|
||||
/// </summary>
|
||||
[Category("调试"), DisplayName("显示骨骼(TODO)")]
|
||||
public bool DebugBones { get; set; } = false;
|
||||
|
||||
#region SFML.Graphics.Drawable 接口实现
|
||||
|
||||
/// <summary>
|
||||
@@ -444,6 +309,16 @@ namespace SpineViewer.Spine
|
||||
/// </summary>
|
||||
protected readonly SFML.Graphics.VertexArray vertexArray = new(SFML.Graphics.PrimitiveType.Triangles);
|
||||
|
||||
/// <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>
|
||||
/// SFML.Graphics.Drawable 接口实现
|
||||
/// </summary>
|
||||
|
||||
159
SpineViewer/Spine/SpineHelper.cs
Normal file
159
SpineViewer/Spine/SpineHelper.cs
Normal file
@@ -0,0 +1,159 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.ComponentModel;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using System.Text;
|
||||
using System.Text.Json.Nodes;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace SpineViewer.Spine
|
||||
{
|
||||
/// <summary>
|
||||
/// 支持的 Spine 版本
|
||||
/// </summary>
|
||||
public enum SpineVersion
|
||||
{
|
||||
[Description("<Auto>")] Auto = 0x0000,
|
||||
[Description("2.1.x")] V21 = 0x0201,
|
||||
[Description("3.6.x")] V36 = 0x0306,
|
||||
[Description("3.7.x")] V37 = 0x0307,
|
||||
[Description("3.8.x")] V38 = 0x0308,
|
||||
[Description("4.0.x")] V40 = 0x0400,
|
||||
[Description("4.1.x")] V41 = 0x0401,
|
||||
[Description("4.2.x")] V42 = 0x0402,
|
||||
[Description("4.3.x")] V43 = 0x0403,
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Spine 实现类标记
|
||||
/// </summary>
|
||||
[AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = false)]
|
||||
public class SpineImplementationAttribute(SpineVersion version) : Attribute, IImplementationKey<SpineVersion>
|
||||
{
|
||||
public SpineVersion ImplementationKey { get; private set; } = version;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Spine 版本静态辅助类
|
||||
/// </summary>
|
||||
public static class SpineHelper
|
||||
{
|
||||
/// <summary>
|
||||
/// 版本名称
|
||||
/// </summary>
|
||||
public static readonly ReadOnlyDictionary<SpineVersion, string> Names;
|
||||
private static readonly Dictionary<SpineVersion, string> names = [];
|
||||
|
||||
/// <summary>
|
||||
/// Runtime 版本字符串
|
||||
/// </summary>
|
||||
private static readonly Dictionary<SpineVersion, string> runtimes = [];
|
||||
|
||||
static SpineHelper()
|
||||
{
|
||||
// 初始化缓存
|
||||
foreach (var value in Enum.GetValues(typeof(SpineVersion)))
|
||||
{
|
||||
var field = typeof(SpineVersion).GetField(value.ToString());
|
||||
var attribute = field?.GetCustomAttribute<DescriptionAttribute>();
|
||||
names[(SpineVersion)value] = attribute?.Description ?? value.ToString();
|
||||
}
|
||||
Names = names.AsReadOnly();
|
||||
|
||||
runtimes[SpineVersion.V21] = Assembly.GetAssembly(typeof(SpineRuntime21.Skeleton)).GetCustomAttribute<AssemblyInformationalVersionAttribute>()?.InformationalVersion;
|
||||
runtimes[SpineVersion.V36] = Assembly.GetAssembly(typeof(SpineRuntime36.Skeleton)).GetCustomAttribute<AssemblyInformationalVersionAttribute>()?.InformationalVersion;
|
||||
runtimes[SpineVersion.V37] = Assembly.GetAssembly(typeof(SpineRuntime37.Skeleton)).GetCustomAttribute<AssemblyInformationalVersionAttribute>()?.InformationalVersion;
|
||||
runtimes[SpineVersion.V38] = Assembly.GetAssembly(typeof(SpineRuntime38.Skeleton)).GetCustomAttribute<AssemblyInformationalVersionAttribute>()?.InformationalVersion;
|
||||
runtimes[SpineVersion.V40] = Assembly.GetAssembly(typeof(SpineRuntime40.Skeleton)).GetCustomAttribute<AssemblyInformationalVersionAttribute>()?.InformationalVersion;
|
||||
runtimes[SpineVersion.V41] = Assembly.GetAssembly(typeof(SpineRuntime41.Skeleton)).GetCustomAttribute<AssemblyInformationalVersionAttribute>()?.InformationalVersion;
|
||||
runtimes[SpineVersion.V42] = Assembly.GetAssembly(typeof(SpineRuntime42.Skeleton)).GetCustomAttribute<AssemblyInformationalVersionAttribute>()?.InformationalVersion;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 版本字符串名称
|
||||
/// </summary>
|
||||
public static string GetName(this SpineVersion version)
|
||||
{
|
||||
return Names.TryGetValue(version, out var val) ? val : version.ToString();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Runtime 版本字符串名称
|
||||
/// </summary>
|
||||
public static string GetRuntime(this SpineVersion version)
|
||||
{
|
||||
return runtimes.TryGetValue(version, out var val) ? val : GetName(version);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 常规骨骼文件后缀集合
|
||||
/// </summary>
|
||||
public static readonly ImmutableHashSet<string> CommonSkelSuffix = [".skel", ".json"];
|
||||
|
||||
/// <summary>
|
||||
/// 尝试检测骨骼文件版本
|
||||
/// </summary>
|
||||
/// <param name="skelPath"></param>
|
||||
/// <returns></returns>
|
||||
/// <exception cref="InvalidDataException"></exception>
|
||||
public static SpineVersion GetVersion(string skelPath)
|
||||
{
|
||||
string versionString = null;
|
||||
using var input = File.OpenRead(skelPath);
|
||||
var reader = new SkeletonConverter.BinaryReader(input);
|
||||
|
||||
// try json format
|
||||
try
|
||||
{
|
||||
if (JsonNode.Parse(input) is JsonObject root && root.TryGetPropertyValue("skeleton", out var node) &&
|
||||
node is JsonObject _skeleton && _skeleton.TryGetPropertyValue("spine", out var _version))
|
||||
versionString = (string)_version;
|
||||
}
|
||||
catch { }
|
||||
|
||||
// try v4 binary format
|
||||
if (versionString is null)
|
||||
{
|
||||
try
|
||||
{
|
||||
input.Position = 0;
|
||||
var hash = reader.ReadLong();
|
||||
var versionPosition = input.Position;
|
||||
var versionByteCount = reader.ReadVarInt();
|
||||
input.Position = versionPosition;
|
||||
if (versionByteCount <= 13)
|
||||
versionString = reader.ReadString();
|
||||
}
|
||||
catch { }
|
||||
}
|
||||
|
||||
// try v3 binary format
|
||||
if (versionString is null)
|
||||
{
|
||||
try
|
||||
{
|
||||
input.Position = 0;
|
||||
var hash = reader.ReadString();
|
||||
versionString = reader.ReadString();
|
||||
}
|
||||
catch { }
|
||||
}
|
||||
|
||||
if (versionString is null)
|
||||
throw new InvalidDataException($"No verison detected: {skelPath}");
|
||||
|
||||
if (versionString.StartsWith("2.1.")) return SpineVersion.V21;
|
||||
else if (versionString.StartsWith("3.6.")) return SpineVersion.V36;
|
||||
else if (versionString.StartsWith("3.7.")) return SpineVersion.V37;
|
||||
else if (versionString.StartsWith("3.8.")) return SpineVersion.V38;
|
||||
else if (versionString.StartsWith("4.0.")) return SpineVersion.V40;
|
||||
else if (versionString.StartsWith("4.1.")) return SpineVersion.V41;
|
||||
else if (versionString.StartsWith("4.2.")) return SpineVersion.V42;
|
||||
else if (versionString.StartsWith("4.3.")) return SpineVersion.V43;
|
||||
else throw new InvalidDataException($"Unknown verison: {versionString}");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -9,35 +9,23 @@ using System.Threading.Tasks;
|
||||
|
||||
namespace SpineViewer.Spine
|
||||
{
|
||||
public class VersionConverter : EnumConverter
|
||||
public class SpineVersionConverter : EnumConverter
|
||||
{
|
||||
public VersionConverter() : base(typeof(Version)) { }
|
||||
public SpineVersionConverter() : base(typeof(SpineVersion)) { }
|
||||
|
||||
public override object? ConvertTo(ITypeDescriptorContext? context, CultureInfo? culture, object? value, Type? destinationType)
|
||||
{
|
||||
if (destinationType == typeof(string) && value is Version version)
|
||||
{
|
||||
// 调用自定义的 String() 方法
|
||||
if (destinationType == typeof(string) && value is SpineVersion version)
|
||||
return version.GetName();
|
||||
}
|
||||
|
||||
return base.ConvertTo(context, culture, value, destinationType);
|
||||
}
|
||||
}
|
||||
|
||||
public class AnimationConverter : StringConverter
|
||||
{
|
||||
public override bool GetStandardValuesSupported(ITypeDescriptorContext? context)
|
||||
{
|
||||
// 支持标准值列表
|
||||
return true;
|
||||
}
|
||||
public override bool GetStandardValuesSupported(ITypeDescriptorContext? context) => true;
|
||||
|
||||
public override bool GetStandardValuesExclusive(ITypeDescriptorContext? context)
|
||||
{
|
||||
// 排他模式,只有下拉列表中的值可选
|
||||
return true;
|
||||
}
|
||||
public override bool GetStandardValuesExclusive(ITypeDescriptorContext? context) => true;
|
||||
|
||||
public override StandardValuesCollection? GetStandardValues(ITypeDescriptorContext? context)
|
||||
{
|
||||
@@ -58,4 +46,30 @@ namespace SpineViewer.Spine
|
||||
return base.GetStandardValues(context);
|
||||
}
|
||||
}
|
||||
|
||||
public class SkinConverter : StringConverter
|
||||
{
|
||||
public override bool GetStandardValuesSupported(ITypeDescriptorContext? context) => true;
|
||||
|
||||
public override bool GetStandardValuesExclusive(ITypeDescriptorContext? context) => true;
|
||||
|
||||
public override StandardValuesCollection? GetStandardValues(ITypeDescriptorContext? context)
|
||||
{
|
||||
if (context?.Instance is Spine obj)
|
||||
{
|
||||
return new StandardValuesCollection(obj.SkinNames);
|
||||
}
|
||||
else if (context?.Instance is Spine[] spines)
|
||||
{
|
||||
if (spines.Length > 0)
|
||||
{
|
||||
IEnumerable<string> common = spines[0].SkinNames;
|
||||
foreach (var spine in spines.Skip(1))
|
||||
common = common.Intersect(spine.SkinNames);
|
||||
return new StandardValuesCollection(common.ToArray());
|
||||
}
|
||||
}
|
||||
return base.GetStandardValues(context);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,94 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.ComponentModel;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace SpineViewer.Spine
|
||||
{
|
||||
/// <summary>
|
||||
/// 支持的 Spine 版本
|
||||
/// </summary>
|
||||
public enum Version
|
||||
{
|
||||
[Description("<Auto>")] Auto = 0x0000,
|
||||
[Description("2.1.x")] V21 = 0x0201,
|
||||
[Description("3.6.x")] V36 = 0x0306,
|
||||
[Description("3.7.x")] V37 = 0x0307,
|
||||
[Description("3.8.x")] V38 = 0x0308,
|
||||
[Description("4.0.x")] V40 = 0x0400,
|
||||
[Description("4.1.x")] V41 = 0x0401,
|
||||
[Description("4.2.x")] V42 = 0x0402,
|
||||
[Description("4.3.x")] V43 = 0x0403,
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Spine 实现类标记
|
||||
/// </summary>
|
||||
[AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = false)]
|
||||
public class SpineImplementationAttribute : Attribute
|
||||
{
|
||||
public Version Version { get; }
|
||||
|
||||
public SpineImplementationAttribute(Version version)
|
||||
{
|
||||
Version = version;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Spine 版本静态辅助类
|
||||
/// </summary>
|
||||
public static class VersionHelper
|
||||
{
|
||||
/// <summary>
|
||||
/// 版本名称
|
||||
/// </summary>
|
||||
public static readonly ReadOnlyDictionary<Version, string> Names;
|
||||
private static readonly Dictionary<Version, string> names = [];
|
||||
|
||||
/// <summary>
|
||||
/// Runtime 版本字符串
|
||||
/// </summary>
|
||||
private static readonly Dictionary<Version, string> runtimes = [];
|
||||
|
||||
static VersionHelper()
|
||||
{
|
||||
// 初始化缓存
|
||||
foreach (var value in Enum.GetValues(typeof(Version)))
|
||||
{
|
||||
var field = typeof(Version).GetField(value.ToString());
|
||||
var attribute = field?.GetCustomAttribute<DescriptionAttribute>();
|
||||
names[(Version)value] = attribute?.Description ?? value.ToString();
|
||||
}
|
||||
Names = names.AsReadOnly();
|
||||
|
||||
runtimes[Version.V21] = Assembly.GetAssembly(typeof(SpineRuntime21.Skeleton)).GetCustomAttribute<AssemblyInformationalVersionAttribute>()?.InformationalVersion;
|
||||
runtimes[Version.V36] = Assembly.GetAssembly(typeof(SpineRuntime36.Skeleton)).GetCustomAttribute<AssemblyInformationalVersionAttribute>()?.InformationalVersion;
|
||||
runtimes[Version.V37] = Assembly.GetAssembly(typeof(SpineRuntime37.Skeleton)).GetCustomAttribute<AssemblyInformationalVersionAttribute>()?.InformationalVersion;
|
||||
runtimes[Version.V38] = Assembly.GetAssembly(typeof(SpineRuntime38.Skeleton)).GetCustomAttribute<AssemblyInformationalVersionAttribute>()?.InformationalVersion;
|
||||
runtimes[Version.V40] = Assembly.GetAssembly(typeof(SpineRuntime40.Skeleton)).GetCustomAttribute<AssemblyInformationalVersionAttribute>()?.InformationalVersion;
|
||||
runtimes[Version.V41] = Assembly.GetAssembly(typeof(SpineRuntime41.Skeleton)).GetCustomAttribute<AssemblyInformationalVersionAttribute>()?.InformationalVersion;
|
||||
runtimes[Version.V42] = Assembly.GetAssembly(typeof(SpineRuntime42.Skeleton)).GetCustomAttribute<AssemblyInformationalVersionAttribute>()?.InformationalVersion;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 版本字符串名称
|
||||
/// </summary>
|
||||
public static string GetName(this Version version)
|
||||
{
|
||||
return Names.TryGetValue(version, out var val) ? val : version.ToString();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Runtime 版本字符串名称
|
||||
/// </summary>
|
||||
public static string GetRuntime(this Version version)
|
||||
{
|
||||
return runtimes.TryGetValue(version, out var val) ? val : GetName(version);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -8,7 +8,7 @@
|
||||
<RuntimeIdentifier>win-x64</RuntimeIdentifier>
|
||||
<BaseOutputPath>$(SolutionDir)out</BaseOutputPath>
|
||||
<IncludeSourceRevisionInInformationalVersion>false</IncludeSourceRevisionInInformationalVersion>
|
||||
<Version>0.11.1</Version>
|
||||
<Version>0.11.4</Version>
|
||||
<OutputType>WinExe</OutputType>
|
||||
<UseWindowsForms>true</UseWindowsForms>
|
||||
<ApplicationIcon>appicon.ico</ApplicationIcon>
|
||||
|
||||
65
SpineViewer/TaskbarManager.cs
Normal file
65
SpineViewer/TaskbarManager.cs
Normal file
@@ -0,0 +1,65 @@
|
||||
using System;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Windows.Forms;
|
||||
|
||||
namespace SpineViewer
|
||||
{
|
||||
internal enum TBPFLAG
|
||||
{
|
||||
TBPF_NOPROGRESS = 0,
|
||||
TBPF_INDETERMINATE = 0x1,
|
||||
TBPF_NORMAL = 0x2,
|
||||
TBPF_ERROR = 0x4,
|
||||
TBPF_PAUSED = 0x8
|
||||
}
|
||||
|
||||
[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
|
||||
[ComImport, Guid("ea1afb91-9e28-4b86-90e9-9e9f8a5eefaf")]
|
||||
internal interface ITaskbarList3
|
||||
{
|
||||
// ITaskbarList
|
||||
void HrInit();
|
||||
void AddTab(IntPtr hwnd);
|
||||
void DeleteTab(IntPtr hwnd);
|
||||
void ActivateTab(IntPtr hwnd);
|
||||
void SetActiveAlt(IntPtr hwnd);
|
||||
// ITaskbarList2
|
||||
void MarkFullscreenWindow(IntPtr hwnd, [MarshalAs(UnmanagedType.Bool)] bool fFullscreen);
|
||||
// ITaskbarList3
|
||||
void SetProgressValue(IntPtr hwnd, ulong ullCompleted, ulong ullTotal);
|
||||
void SetProgressState(IntPtr hwnd, TBPFLAG tbpFlags);
|
||||
//void RegisterTab(IntPtr hwndTab, IntPtr hwndMDI);
|
||||
//void UnregisterTab(IntPtr hwndTab);
|
||||
//void SetTabOrder(IntPtr hwndTab, IntPtr hwndInsertBefore);
|
||||
//void SetTabActive(IntPtr hwndTab, IntPtr hwndMDI, uint dwReserved);
|
||||
//void ThumbBarAddButtons(IntPtr hwnd, uint cButtons, THUMBBUTTON[] pButton);
|
||||
//void ThumbBarUpdateButtons(IntPtr hwnd, uint cButtons, THUMBBUTTON[] pButton);
|
||||
//void ThumbBarSetImageList(IntPtr hwnd, IntPtr himl);
|
||||
//void SetOverlayIcon(IntPtr hwnd, IntPtr hIcon, string pszDescription);
|
||||
//void SetThumbnailTooltip(IntPtr hwnd, string pszTip);
|
||||
//void SetThumbnailClip(IntPtr hwnd, ref RECT prcClip);
|
||||
}
|
||||
|
||||
[ComImport, Guid("56FDF344-FD6D-11d0-958A-006097C9A090")]
|
||||
internal class TaskbarList { }
|
||||
|
||||
internal static class TaskbarManager
|
||||
{
|
||||
private static readonly ITaskbarList3 taskbarList = (ITaskbarList3)new TaskbarList();
|
||||
|
||||
static TaskbarManager()
|
||||
{
|
||||
taskbarList.HrInit();
|
||||
}
|
||||
|
||||
public static void SetProgressState(IntPtr windowHandle, TBPFLAG state)
|
||||
{
|
||||
taskbarList.SetProgressState(windowHandle, state);
|
||||
}
|
||||
|
||||
public static void SetProgressValue(IntPtr windowHandle, ulong completed, ulong total)
|
||||
{
|
||||
taskbarList.SetProgressValue(windowHandle, completed, total);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -9,7 +9,7 @@ using System.Threading.Tasks;
|
||||
|
||||
namespace SpineViewer
|
||||
{
|
||||
public class PointFConverter : TypeConverter
|
||||
public class PointFConverter : ExpandableObjectConverter
|
||||
{
|
||||
public override bool CanConvertTo(ITypeDescriptorContext? context, [NotNullWhen(true)] Type? destinationType)
|
||||
{
|
||||
@@ -44,12 +44,5 @@ namespace SpineViewer
|
||||
}
|
||||
return base.ConvertFrom(context, culture, value);
|
||||
}
|
||||
|
||||
public override PropertyDescriptorCollection GetProperties(ITypeDescriptorContext? context, object value, Attribute[]? attributes)
|
||||
{
|
||||
return TypeDescriptor.GetProperties(typeof(PointF), attributes);
|
||||
}
|
||||
|
||||
public override bool GetPropertiesSupported(ITypeDescriptorContext? context) => true;
|
||||
}
|
||||
}
|
||||
|
||||
BIN
img/preview.webp
BIN
img/preview.webp
Binary file not shown.
|
Before Width: | Height: | Size: 157 KiB After Width: | Height: | Size: 163 KiB |
Reference in New Issue
Block a user