Compare commits

..

11 Commits

Author SHA1 Message Date
ww-rm
160a49ad5f 更新至v0.10.3 2025-03-20 15:38:04 +08:00
ww-rm
9d4907d77e update changelog and readme 2025-03-20 15:37:49 +08:00
ww-rm
53d30e0503 修改字母快捷键 2025-03-20 15:34:17 +08:00
ww-rm
9609a2fd5d 增加拖放文件打开 2025-03-20 15:32:31 +08:00
ww-rm
66cf0efcb9 增加单独的结果包装类 2025-03-20 15:31:35 +08:00
ww-rm
0129b9df31 small change 2025-03-20 14:20:45 +08:00
ww-rm
a7a5521be1 增加自动版本 2025-03-20 14:20:26 +08:00
ww-rm
f7f7211ca2 增加自动版本 2025-03-20 14:04:19 +08:00
ww-rm
8c921a6ed5 修改错误类型 2025-03-20 14:03:33 +08:00
ww-rm
f14ab870f7 update 2025-03-20 11:05:05 +08:00
ww-rm
26e81ffdb6 update readme preview 2025-03-20 10:14:59 +08:00
24 changed files with 260 additions and 109 deletions

View File

@@ -47,8 +47,8 @@ jobs:
env: env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with: with:
tag_name: ${{ github.ref_name }} tag_name: ${{ env.VERSION }}
release_name: Release ${{ github.ref_name }} release_name: Release ${{ env.VERSION }}
draft: false draft: false
prerelease: false prerelease: false

View File

@@ -1,5 +1,10 @@
# CHANGELOG # CHANGELOG
## v0.10.3
- <20><><EFBFBD><EFBFBD><EFBFBD>Զ<EFBFBD><D4B6><EFBFBD><E6B1BE><EFBFBD><EFBFBD>
- <20><><EFBFBD><EFBFBD><EFBFBD>ļ<EFBFBD><C4BC>ϷŴ<CFB7><C5B4><EFBFBD>
## v0.10.2 ## v0.10.2
- <20><><EFBFBD><EFBFBD><EFBFBD>б<EFBFBD><D0B1>Ҽ<EFBFBD><D2BC>˵<EFBFBD><CBB5><EFBFBD><EFBFBD>ݼ<EFBFBD> - <20><><EFBFBD><EFBFBD><EFBFBD>б<EFBFBD><D0B1>Ҽ<EFBFBD><D2BC>˵<EFBFBD><CBB5><EFBFBD><EFBFBD>ݼ<EFBFBD>

View File

@@ -6,7 +6,7 @@
A simple and user-friendly Spine file viewer and exporter. A simple and user-friendly Spine file viewer and exporter.
![previewer](img/preview.jpg) ![previewer](img/preview.webp)
--- ---

View File

@@ -6,7 +6,7 @@
一个简单好用的 Spine 文件查看&导出程序. 一个简单好用的 Spine 文件查看&导出程序.
![previewer](img/preview.jpg) ![previewer](img/preview.webp)
--- ---
@@ -33,6 +33,9 @@
| `4.2.x` | :white_check_mark: | | | `4.2.x` | :white_check_mark: | |
| `4.3.x` | | | | `4.3.x` | | |
- 支持文件拖放打开
- 支持自动检测版本
- 支持列表缩略图预览
- 支持多骨骼文件动画预览 - 支持多骨骼文件动画预览
- 支持每个骨骼独立参数设置 - 支持每个骨骼独立参数设置
- 支持动画PNG帧序列导出 - 支持动画PNG帧序列导出
@@ -46,6 +49,8 @@
**文件**菜单可以选择**打开**或者**批量打开**进行骨骼文件导入. **文件**菜单可以选择**打开**或者**批量打开**进行骨骼文件导入.
或者直接把要打开的骨骼文件拖进列表, 这种方式只支持 `.json``.skel` 后缀的文件, 其他的会被忽略.
### 骨骼调整 ### 骨骼调整
在**模型列表**中选择一项或多项, 将会在**模型参数**面板显示可供调节的参数. 在**模型列表**中选择一项或多项, 将会在**模型参数**面板显示可供调节的参数.

View File

@@ -44,15 +44,15 @@
toolStripMenuItem_MoveTop = new ToolStripMenuItem(); toolStripMenuItem_MoveTop = new ToolStripMenuItem();
toolStripMenuItem_MoveBottom = new ToolStripMenuItem(); toolStripMenuItem_MoveBottom = new ToolStripMenuItem();
toolStripSeparator3 = new ToolStripSeparator(); toolStripSeparator3 = new ToolStripSeparator();
toolStripMenuItem_SelectAll = new ToolStripMenuItem();
toolStripMenuItem_CopyPreview = new ToolStripMenuItem(); toolStripMenuItem_CopyPreview = new ToolStripMenuItem();
toolStripSeparator4 = new ToolStripSeparator();
toolStripMenuItem_ChangeView = new ToolStripMenuItem(); toolStripMenuItem_ChangeView = new ToolStripMenuItem();
toolStripMenuItem_LargeIconView = new ToolStripMenuItem(); toolStripMenuItem_LargeIconView = new ToolStripMenuItem();
toolStripMenuItem_SmallIconView = new ToolStripMenuItem(); toolStripMenuItem_SmallIconView = new ToolStripMenuItem();
toolStripMenuItem_DetailsView = new ToolStripMenuItem(); toolStripMenuItem_DetailsView = new ToolStripMenuItem();
imageList_LargeIcon = new ImageList(components); imageList_LargeIcon = new ImageList(components);
imageList_SmallIcon = new ImageList(components); imageList_SmallIcon = new ImageList(components);
toolStripSeparator4 = new ToolStripSeparator();
toolStripMenuItem_SelectAll = new ToolStripMenuItem();
contextMenuStrip.SuspendLayout(); contextMenuStrip.SuspendLayout();
SuspendLayout(); SuspendLayout();
// //
@@ -76,6 +76,7 @@
listView.ItemDrag += listView_ItemDrag; listView.ItemDrag += listView_ItemDrag;
listView.SelectedIndexChanged += listView_SelectedIndexChanged; listView.SelectedIndexChanged += listView_SelectedIndexChanged;
listView.DragDrop += listView_DragDrop; listView.DragDrop += listView_DragDrop;
listView.DragEnter += listView_DragEnter;
listView.DragOver += listView_DragOver; listView.DragOver += listView_DragOver;
// //
// columnHeader_Name // columnHeader_Name
@@ -88,7 +89,7 @@
contextMenuStrip.ImageScalingSize = new Size(24, 24); 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_SelectAll, toolStripMenuItem_CopyPreview, toolStripSeparator4, toolStripMenuItem_ChangeView }); 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_SelectAll, toolStripMenuItem_CopyPreview, toolStripSeparator4, toolStripMenuItem_ChangeView });
contextMenuStrip.Name = "contextMenuStrip"; contextMenuStrip.Name = "contextMenuStrip";
contextMenuStrip.Size = new Size(329, 421); contextMenuStrip.Size = new Size(329, 388);
contextMenuStrip.Closed += contextMenuStrip_Closed; contextMenuStrip.Closed += contextMenuStrip_Closed;
contextMenuStrip.Opening += contextMenuStrip_Opening; contextMenuStrip.Opening += contextMenuStrip_Opening;
// //
@@ -174,6 +175,14 @@
toolStripSeparator3.Name = "toolStripSeparator3"; toolStripSeparator3.Name = "toolStripSeparator3";
toolStripSeparator3.Size = new Size(325, 6); 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
// //
toolStripMenuItem_CopyPreview.Name = "toolStripMenuItem_CopyPreview"; toolStripMenuItem_CopyPreview.Name = "toolStripMenuItem_CopyPreview";
@@ -182,6 +191,11 @@
toolStripMenuItem_CopyPreview.Text = "复制预览图 (256x256)"; toolStripMenuItem_CopyPreview.Text = "复制预览图 (256x256)";
toolStripMenuItem_CopyPreview.Click += toolStripMenuItem_CopyPreview_Click; toolStripMenuItem_CopyPreview.Click += toolStripMenuItem_CopyPreview_Click;
// //
// toolStripSeparator4
//
toolStripSeparator4.Name = "toolStripSeparator4";
toolStripSeparator4.Size = new Size(325, 6);
//
// toolStripMenuItem_ChangeView // toolStripMenuItem_ChangeView
// //
toolStripMenuItem_ChangeView.DropDownItems.AddRange(new ToolStripItem[] { toolStripMenuItem_LargeIconView, toolStripMenuItem_SmallIconView, toolStripMenuItem_DetailsView }); toolStripMenuItem_ChangeView.DropDownItems.AddRange(new ToolStripItem[] { toolStripMenuItem_LargeIconView, toolStripMenuItem_SmallIconView, toolStripMenuItem_DetailsView });
@@ -193,7 +207,7 @@
// //
toolStripMenuItem_LargeIconView.Name = "toolStripMenuItem_LargeIconView"; toolStripMenuItem_LargeIconView.Name = "toolStripMenuItem_LargeIconView";
toolStripMenuItem_LargeIconView.ShortcutKeys = Keys.Alt | Keys.D1; toolStripMenuItem_LargeIconView.ShortcutKeys = Keys.Alt | Keys.D1;
toolStripMenuItem_LargeIconView.Size = new Size(270, 34); toolStripMenuItem_LargeIconView.Size = new Size(223, 34);
toolStripMenuItem_LargeIconView.Text = "大图标"; toolStripMenuItem_LargeIconView.Text = "大图标";
toolStripMenuItem_LargeIconView.Click += toolStripMenuItem_LargeIconView_Click; toolStripMenuItem_LargeIconView.Click += toolStripMenuItem_LargeIconView_Click;
// //
@@ -201,7 +215,7 @@
// //
toolStripMenuItem_SmallIconView.Name = "toolStripMenuItem_SmallIconView"; toolStripMenuItem_SmallIconView.Name = "toolStripMenuItem_SmallIconView";
toolStripMenuItem_SmallIconView.ShortcutKeys = Keys.Alt | Keys.D2; toolStripMenuItem_SmallIconView.ShortcutKeys = Keys.Alt | Keys.D2;
toolStripMenuItem_SmallIconView.Size = new Size(270, 34); toolStripMenuItem_SmallIconView.Size = new Size(223, 34);
toolStripMenuItem_SmallIconView.Text = "小图标"; toolStripMenuItem_SmallIconView.Text = "小图标";
toolStripMenuItem_SmallIconView.Click += toolStripMenuItem_SmallIconView_Click; toolStripMenuItem_SmallIconView.Click += toolStripMenuItem_SmallIconView_Click;
// //
@@ -209,7 +223,7 @@
// //
toolStripMenuItem_DetailsView.Name = "toolStripMenuItem_DetailsView"; toolStripMenuItem_DetailsView.Name = "toolStripMenuItem_DetailsView";
toolStripMenuItem_DetailsView.ShortcutKeys = Keys.Alt | Keys.D3; toolStripMenuItem_DetailsView.ShortcutKeys = Keys.Alt | Keys.D3;
toolStripMenuItem_DetailsView.Size = new Size(270, 34); toolStripMenuItem_DetailsView.Size = new Size(223, 34);
toolStripMenuItem_DetailsView.Text = "列表"; toolStripMenuItem_DetailsView.Text = "列表";
toolStripMenuItem_DetailsView.Click += toolStripMenuItem_DetailsView_Click; toolStripMenuItem_DetailsView.Click += toolStripMenuItem_DetailsView_Click;
// //
@@ -225,19 +239,6 @@
imageList_SmallIcon.ImageSize = new Size(48, 48); imageList_SmallIcon.ImageSize = new Size(48, 48);
imageList_SmallIcon.TransparentColor = Color.Transparent; imageList_SmallIcon.TransparentColor = Color.Transparent;
// //
// toolStripSeparator4
//
toolStripSeparator4.Name = "toolStripSeparator4";
toolStripSeparator4.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;
//
// SpineListView // SpineListView
// //
AutoScaleDimensions = new SizeF(11F, 24F); AutoScaleDimensions = new SizeF(11F, 24F);

View File

@@ -60,7 +60,7 @@ namespace SpineViewer.Controls
var progressDialog = new Dialogs.ProgressDialog(); var progressDialog = new Dialogs.ProgressDialog();
progressDialog.DoWork += BatchAdd_Work; progressDialog.DoWork += BatchAdd_Work;
progressDialog.RunWorkerAsync(openDialog); progressDialog.RunWorkerAsync(openDialog.Result);
progressDialog.ShowDialog(); progressDialog.ShowDialog();
} }
@@ -113,62 +113,95 @@ namespace SpineViewer.Controls
DoDragDrop(e.Item, DragDropEffects.Move); DoDragDrop(e.Item, DragDropEffects.Move);
} }
private void listView_DragEnter(object sender, DragEventArgs e)
{
if (e.Data.GetDataPresent(DataFormats.Serializable))
e.Effect = DragDropEffects.Move;
else if (e.Data.GetDataPresent(DataFormats.FileDrop))
e.Effect = DragDropEffects.Copy;
else
e.Effect = DragDropEffects.None;
}
private void listView_DragOver(object sender, DragEventArgs e) private void listView_DragOver(object sender, DragEventArgs e)
{ {
// 检查拖放目标是否有效 if (e.Data.GetDataPresent(DataFormats.Serializable))
e.Effect = DragDropEffects.Move; {
// 获取鼠标位置并确定目标索引
var point = listView.PointToClient(new(e.X, e.Y));
var targetItem = listView.GetItemAt(point.X, point.Y);
// 获取鼠标位置并确定目标索引 // 高亮目标项
var point = listView.PointToClient(new(e.X, e.Y)); if (targetItem != null)
var targetItem = listView.GetItemAt(point.X, point.Y);
// 高亮目标项
if (targetItem != null)
{
foreach (ListViewItem item in listView.Items)
{ {
item.BackColor = listView.BackColor; foreach (ListViewItem item in listView.Items)
{
item.BackColor = listView.BackColor;
}
targetItem.BackColor = Color.LightGray;
} }
targetItem.BackColor = Color.LightGray;
} }
} }
private void listView_DragDrop(object sender, DragEventArgs e) private void listView_DragDrop(object sender, DragEventArgs e)
{ {
// 获取拖放源项和目标项 if (e.Data.GetDataPresent(DataFormats.Serializable))
var draggedItem = (ListViewItem)e.Data.GetData(typeof(ListViewItem)); {
int draggedIndex = draggedItem.Index; // 获取拖放源项和目标项
var point = listView.PointToClient(new Point(e.X, e.Y)); var draggedItem = (ListViewItem)e.Data.GetData(typeof(ListViewItem));
var targetItem = listView.GetItemAt(point.X, point.Y); int draggedIndex = draggedItem.Index;
int targetIndex = targetItem is null ? listView.Items.Count : targetItem.Index; var point = listView.PointToClient(new Point(e.X, e.Y));
var targetItem = listView.GetItemAt(point.X, point.Y);
int targetIndex = targetItem is null ? listView.Items.Count : targetItem.Index;
if (targetIndex <= draggedIndex) if (targetIndex <= draggedIndex)
{
lock (Spines)
{ {
var draggedSpine = spines[draggedIndex]; lock (Spines)
spines.RemoveAt(draggedIndex); {
spines.Insert(targetIndex, draggedSpine); var draggedSpine = spines[draggedIndex];
spines.RemoveAt(draggedIndex);
spines.Insert(targetIndex, draggedSpine);
}
listView.Items.RemoveAt(draggedIndex);
listView.Items.Insert(targetIndex, draggedItem);
} }
listView.Items.RemoveAt(draggedIndex); else
listView.Items.Insert(targetIndex, draggedItem);
}
else
{
lock (Spines)
{ {
var draggedSpine = spines[draggedIndex]; lock (Spines)
spines.RemoveAt(draggedIndex); {
spines.Insert(targetIndex - 1, draggedSpine); var draggedSpine = spines[draggedIndex];
spines.RemoveAt(draggedIndex);
spines.Insert(targetIndex - 1, draggedSpine);
}
listView.Items.RemoveAt(draggedIndex);
listView.Items.Insert(targetIndex - 1, draggedItem);
} }
listView.Items.RemoveAt(draggedIndex);
listView.Items.Insert(targetIndex - 1, draggedItem);
}
// 重置背景颜色 // 重置背景颜色
foreach (ListViewItem item in listView.Items) foreach (ListViewItem item in listView.Items)
{
item.BackColor = listView.BackColor;
}
}
else if (e.Data.GetDataPresent(DataFormats.FileDrop))
{ {
item.BackColor = listView.BackColor; var validPaths = ((string[])e.Data.GetData(DataFormats.FileDrop)).Where(
path => File.Exists(path) &&
(Path.GetExtension(path).Equals(".skel", StringComparison.OrdinalIgnoreCase) ||
Path.GetExtension(path).Equals(".json", StringComparison.OrdinalIgnoreCase))
).ToArray();
if (validPaths.Length > 1)
{
var progressDialog = new Dialogs.ProgressDialog();
progressDialog.DoWork += BatchAdd_Work;
progressDialog.RunWorkerAsync(new Dialogs.BatchOpenSpineDialogResult(Spine.Version.Auto, validPaths));
progressDialog.ShowDialog();
}
else if (validPaths.Length > 0)
{
Insert(new Dialogs.OpenSpineDialogResult(Spine.Version.Auto, validPaths[0]));
}
} }
} }
@@ -386,9 +419,14 @@ namespace SpineViewer.Controls
if (dialog.ShowDialog() != DialogResult.OK) if (dialog.ShowDialog() != DialogResult.OK)
return; return;
Insert(dialog.Result, index);
}
private void Insert(Dialogs.OpenSpineDialogResult result, int index = -1)
{
try try
{ {
var spine = Spine.Spine.New(dialog.Version, dialog.SkelPath, dialog.AtlasPath); var spine = Spine.Spine.New(result.Version, result.SkelPath, result.AtlasPath);
// 如果索引无效则在末尾添加 // 如果索引无效则在末尾添加
if (index < 0 || index > listView.Items.Count) if (index < 0 || index > listView.Items.Count)
@@ -410,7 +448,7 @@ namespace SpineViewer.Controls
catch (Exception ex) catch (Exception ex)
{ {
Program.Logger.Error(ex.ToString()); Program.Logger.Error(ex.ToString());
Program.Logger.Error("Failed to load {} {}", dialog.SkelPath, dialog.AtlasPath); Program.Logger.Error("Failed to load {} {}", result.SkelPath, result.AtlasPath);
MessageBox.Show(ex.ToString(), "骨骼加载失败", MessageBoxButtons.OK, MessageBoxIcon.Error); MessageBox.Show(ex.ToString(), "骨骼加载失败", MessageBoxButtons.OK, MessageBoxIcon.Error);
} }
@@ -420,7 +458,7 @@ namespace SpineViewer.Controls
private void BatchAdd_Work(object? sender, DoWorkEventArgs e) private void BatchAdd_Work(object? sender, DoWorkEventArgs e)
{ {
var worker = sender as BackgroundWorker; var worker = sender as BackgroundWorker;
var arguments = e.Argument as Dialogs.BatchOpenSpineDialog; var arguments = e.Argument as Dialogs.BatchOpenSpineDialogResult;
var skelPaths = arguments.SkelPaths; var skelPaths = arguments.SkelPaths;
var version = arguments.Version; var version = arguments.Version;

View File

@@ -13,8 +13,7 @@ namespace SpineViewer.Dialogs
{ {
public partial class BatchOpenSpineDialog : Form public partial class BatchOpenSpineDialog : Form
{ {
public string[] SkelPaths { get; private set; } public BatchOpenSpineDialogResult Result { get; private set; }
public Spine.Version Version { get; private set; }
public BatchOpenSpineDialog() public BatchOpenSpineDialog()
{ {
@@ -22,7 +21,7 @@ namespace SpineViewer.Dialogs
comboBox_Version.DataSource = VersionHelper.Versions.ToList(); comboBox_Version.DataSource = VersionHelper.Versions.ToList();
comboBox_Version.DisplayMember = "Value"; comboBox_Version.DisplayMember = "Value";
comboBox_Version.ValueMember = "Key"; comboBox_Version.ValueMember = "Key";
comboBox_Version.SelectedValue = Spine.Version.V38; comboBox_Version.SelectedValue = Spine.Version.Auto;
} }
private void BatchOpenSpineDialog_Load(object sender, EventArgs e) private void BatchOpenSpineDialog_Load(object sender, EventArgs e)
@@ -60,15 +59,13 @@ namespace SpineViewer.Dialogs
} }
} }
if (!Spine.Spine.ImplementedVersions.Contains(version)) if (version != Spine.Version.Auto && !Spine.Spine.ImplementedVersions.Contains(version))
{ {
MessageBox.Show($"{version.String()} 版本尚未实现(咕咕咕~", "错误信息", MessageBoxButtons.OK, MessageBoxIcon.Information); MessageBox.Show($"{version.String()} 版本尚未实现(咕咕咕~", "错误信息", MessageBoxButtons.OK, MessageBoxIcon.Information);
return; return;
} }
SkelPaths = listBox_FilePath.Items.Cast<string>().ToArray(); Result = new(version, listBox_FilePath.Items.Cast<string>().ToArray());
Version = version;
DialogResult = DialogResult.OK; DialogResult = DialogResult.OK;
} }
@@ -77,4 +74,10 @@ namespace SpineViewer.Dialogs
DialogResult = DialogResult.Cancel; DialogResult = DialogResult.Cancel;
} }
} }
public class BatchOpenSpineDialogResult(Spine.Version version, string[] skelPaths)
{
public Spine.Version Version { get; } = version;
public string[] SkelPaths { get; } = skelPaths;
}
} }

View File

@@ -22,7 +22,12 @@ namespace SpineViewer.Dialogs
public ConvertFileFormatDialog() public ConvertFileFormatDialog()
{ {
InitializeComponent(); InitializeComponent();
comboBox_SourceVersion.DataSource = VersionHelper.Versions.ToList();
// XXX: 文件格式转换暂时不支持自动检测版本
var impVersions = VersionHelper.Versions.ToDictionary();
impVersions.Remove(Spine.Version.Auto);
comboBox_SourceVersion.DataSource = impVersions.ToList();
comboBox_SourceVersion.DisplayMember = "Value"; comboBox_SourceVersion.DisplayMember = "Value";
comboBox_SourceVersion.ValueMember = "Key"; comboBox_SourceVersion.ValueMember = "Key";
comboBox_SourceVersion.SelectedValue = Spine.Version.V38; comboBox_SourceVersion.SelectedValue = Spine.Version.V38;

View File

@@ -12,9 +12,7 @@ namespace SpineViewer.Dialogs
{ {
public partial class OpenSpineDialog : Form public partial class OpenSpineDialog : Form
{ {
public string SkelPath { get; private set; } public OpenSpineDialogResult Result { get; private set; }
public string? AtlasPath { get; private set; }
public Spine.Version Version { get; private set; }
public OpenSpineDialog() public OpenSpineDialog()
{ {
@@ -22,7 +20,7 @@ namespace SpineViewer.Dialogs
comboBox_Version.DataSource = VersionHelper.Versions.ToList(); comboBox_Version.DataSource = VersionHelper.Versions.ToList();
comboBox_Version.DisplayMember = "Value"; comboBox_Version.DisplayMember = "Value";
comboBox_Version.ValueMember = "Key"; comboBox_Version.ValueMember = "Key";
comboBox_Version.SelectedValue = Spine.Version.V38; comboBox_Version.SelectedValue = Spine.Version.Auto;
} }
private void OpenSpineDialog_Load(object sender, EventArgs e) private void OpenSpineDialog_Load(object sender, EventArgs e)
@@ -78,16 +76,13 @@ namespace SpineViewer.Dialogs
atlasPath = Path.GetFullPath(atlasPath); atlasPath = Path.GetFullPath(atlasPath);
} }
if (!Spine.Spine.ImplementedVersions.Contains(version)) if (version != Spine.Version.Auto && !Spine.Spine.ImplementedVersions.Contains(version))
{ {
MessageBox.Show($"{version.String()} 版本尚未实现(咕咕咕~", "错误信息", MessageBoxButtons.OK, MessageBoxIcon.Information); MessageBox.Show($"{version.String()} 版本尚未实现(咕咕咕~", "错误信息", MessageBoxButtons.OK, MessageBoxIcon.Information);
return; return;
} }
SkelPath = skelPath; Result = new(version, skelPath, atlasPath);
AtlasPath = atlasPath;
Version = version;
DialogResult = DialogResult.OK; DialogResult = DialogResult.OK;
} }
@@ -96,4 +91,11 @@ namespace SpineViewer.Dialogs
DialogResult = DialogResult.Cancel; DialogResult = DialogResult.Cancel;
} }
} }
public class OpenSpineDialogResult(Spine.Version version, string skelPath, string? atlasPath = null)
{
public Spine.Version Version { get; } = version;
public string SkelPath { get; } = skelPath;
public string? AtlasPath { get; } = atlasPath;
}
} }

View File

@@ -158,13 +158,13 @@
// //
toolStripMenuItem_Function.DropDownItems.AddRange(new ToolStripItem[] { toolStripMenuItem_ResetAnimation }); toolStripMenuItem_Function.DropDownItems.AddRange(new ToolStripItem[] { toolStripMenuItem_ResetAnimation });
toolStripMenuItem_Function.Name = "toolStripMenuItem_Function"; toolStripMenuItem_Function.Name = "toolStripMenuItem_Function";
toolStripMenuItem_Function.Size = new Size(84, 28); toolStripMenuItem_Function.Size = new Size(87, 28);
toolStripMenuItem_Function.Text = "功能(&F)"; toolStripMenuItem_Function.Text = "功能(&G)";
// //
// toolStripMenuItem_ResetAnimation // toolStripMenuItem_ResetAnimation
// //
toolStripMenuItem_ResetAnimation.Name = "toolStripMenuItem_ResetAnimation"; toolStripMenuItem_ResetAnimation.Name = "toolStripMenuItem_ResetAnimation";
toolStripMenuItem_ResetAnimation.Size = new Size(242, 34); toolStripMenuItem_ResetAnimation.Size = new Size(270, 34);
toolStripMenuItem_ResetAnimation.Text = "重置动画时间(&R)"; toolStripMenuItem_ResetAnimation.Text = "重置动画时间(&R)";
toolStripMenuItem_ResetAnimation.Click += toolStripMenuItem_ResetAnimation_Click; toolStripMenuItem_ResetAnimation.Click += toolStripMenuItem_ResetAnimation_Click;
// //

View File

@@ -14,7 +14,7 @@ namespace SpineViewer.Spine.Implementations.SkeletonConverter
[SkeletonConverterImplementation(Version.V38)] [SkeletonConverterImplementation(Version.V38)]
class SkeletonConverter38 : SpineViewer.Spine.SkeletonConverter class SkeletonConverter38 : SpineViewer.Spine.SkeletonConverter
{ {
private SkeletonReader reader = null; private BinaryReader reader = null;
private JsonObject root = null; private JsonObject root = null;
private bool nonessential = false; private bool nonessential = false;
@@ -332,7 +332,7 @@ namespace SpineViewer.Spine.Implementations.SkeletonConverter
if (nonessential) reader.ReadInt(); if (nonessential) reader.ReadInt();
break; break;
default: default:
throw new ArgumentException($"Invalid attachment type: {type}"); throw new ArgumentOutOfRangeException($"Invalid attachment type: {type}");
} }
return attachment; return attachment;
} }
@@ -435,7 +435,7 @@ namespace SpineViewer.Spine.Implementations.SkeletonConverter
} }
break; break;
default: default:
throw new ArgumentException($"Invalid slot timeline type: {type}"); throw new ArgumentOutOfRangeException($"Invalid slot timeline type: {type}");
} }
} }
} }
@@ -515,7 +515,7 @@ namespace SpineViewer.Spine.Implementations.SkeletonConverter
} }
break; break;
default: default:
throw new ArgumentException($"Invalid bone timeline type: {type}"); throw new ArgumentOutOfRangeException($"Invalid bone timeline type: {type}");
} }
} }
} }
@@ -633,7 +633,7 @@ namespace SpineViewer.Spine.Implementations.SkeletonConverter
} }
break; break;
default: default:
throw new ArgumentException($"Invalid path timeline type: {type}"); throw new ArgumentOutOfRangeException($"Invalid path timeline type: {type}");
} }
} }
} }
@@ -798,11 +798,11 @@ namespace SpineViewer.Spine.Implementations.SkeletonConverter
obj["c4"] = reader.ReadFloat(); obj["c4"] = reader.ReadFloat();
break; break;
default: default:
throw new ArgumentException($"Invalid curve type: {type}"); ; throw new ArgumentOutOfRangeException($"Invalid curve type: {type}"); ;
} }
} }
private SkeletonWriter writer; private BinaryWriter writer;
private readonly Dictionary<string, int> bone2idx = []; private readonly Dictionary<string, int> bone2idx = [];
private readonly Dictionary<string, int> slot2idx = []; private readonly Dictionary<string, int> slot2idx = [];
private readonly Dictionary<string, int> ik2idx = []; private readonly Dictionary<string, int> ik2idx = [];
@@ -1010,6 +1010,18 @@ namespace SpineViewer.Spine.Implementations.SkeletonConverter
private void WriteSkins() private void WriteSkins()
{ {
//JsonArray skins = [];
//// default skin
//if (ReadSkin(true) is JsonObject data)
// skins.Add(data);
//// other skins
//for (int n = reader.ReadVarInt(); n > 0; n--)
// skins.Add(ReadSkin());
//root["skins"] = skins;
if (!root.ContainsKey("skins")) if (!root.ContainsKey("skins"))
{ {
writer.WriteVarInt(0); writer.WriteVarInt(0);
@@ -1023,6 +1035,7 @@ namespace SpineViewer.Spine.Implementations.SkeletonConverter
} }
} }
private void WriteEvents() private void WriteEvents()
{ {
if (!root.ContainsKey("events")) if (!root.ContainsKey("events"))

View File

@@ -71,7 +71,7 @@ namespace SpineViewer.Spine.Implementations.Spine
catch catch
{ {
// 都不行就报错 // 都不行就报错
throw new ArgumentException($"Unknown skeleton file format {SkelPath}"); throw new InvalidDataException($"Unknown skeleton file format {SkelPath}");
} }
} }

View File

@@ -70,7 +70,7 @@ namespace SpineViewer.Spine.Implementations.Spine
catch catch
{ {
// 都不行就报错 // 都不行就报错
throw new ArgumentException($"Unknown skeleton file format {SkelPath}"); throw new InvalidDataException($"Unknown skeleton file format {SkelPath}");
} }
} }

View File

@@ -68,7 +68,7 @@ namespace SpineViewer.Spine.Implementations.Spine
catch catch
{ {
// 都不行就报错 // 都不行就报错
throw new ArgumentException($"Unknown skeleton file format {SkelPath}"); throw new InvalidDataException($"Unknown skeleton file format {SkelPath}");
} }
} }

View File

@@ -71,7 +71,7 @@ namespace SpineViewer.Spine.Implementations.Spine
catch catch
{ {
// 都不行就报错 // 都不行就报错
throw new ArgumentException($"Unknown skeleton file format {SkelPath}"); throw new InvalidDataException($"Unknown skeleton file format {SkelPath}");
} }
} }

View File

@@ -70,7 +70,7 @@ namespace SpineViewer.Spine.Implementations.Spine
catch catch
{ {
// 都不行就报错 // 都不行就报错
throw new ArgumentException($"Unknown skeleton file format {SkelPath}"); throw new InvalidDataException($"Unknown skeleton file format {SkelPath}");
} }
} }

View File

@@ -70,7 +70,7 @@ namespace SpineViewer.Spine.Implementations.Spine
catch catch
{ {
// 都不行就报错 // 都不行就报错
throw new ArgumentException($"Unknown skeleton file format {SkelPath}"); throw new InvalidDataException($"Unknown skeleton file format {SkelPath}");
} }
} }

View File

@@ -70,7 +70,7 @@ namespace SpineViewer.Spine.Implementations.Spine
catch catch
{ {
// 都不行就报错 // 都不行就报错
throw new ArgumentException($"Unknown skeleton file format {SkelPath}"); throw new InvalidDataException($"Unknown skeleton file format {SkelPath}");
} }
} }

View File

@@ -98,7 +98,7 @@ namespace SpineViewer.Spine
if (JsonNode.Parse(input) is JsonObject root) if (JsonNode.Parse(input) is JsonObject root)
return root; return root;
else else
throw new InvalidOperationException($"{jsonPath} is not a valid json object"); throw new InvalidDataException($"{jsonPath} is not a valid json object");
} }
/// <summary> /// <summary>
@@ -116,14 +116,17 @@ namespace SpineViewer.Spine
/// </summary> /// </summary>
public abstract JsonObject ToVersion(JsonObject root, Version version); public abstract JsonObject ToVersion(JsonObject root, Version version);
protected class SkeletonReader /// <summary>
/// 二进制骨骼文件读
/// </summary>
public class BinaryReader
{ {
protected byte[] buffer = new byte[32]; protected byte[] buffer = new byte[32];
protected byte[] bytesBigEndian = new byte[8]; protected byte[] bytesBigEndian = new byte[8];
public readonly List<string> StringTable = new(32); public readonly List<string> StringTable = new(32);
protected Stream input; protected Stream input;
public SkeletonReader(Stream input) { this.input = input; } public BinaryReader(Stream input) { this.input = input; }
public int Read() public int Read()
{ {
int val = input.ReadByte(); int val = input.ReadByte();
@@ -219,14 +222,17 @@ namespace SpineViewer.Spine
} }
} }
protected class SkeletonWriter /// <summary>
/// 二进制骨骼文件写
/// </summary>
protected class BinaryWriter
{ {
protected byte[] buffer = new byte[32]; protected byte[] buffer = new byte[32];
protected byte[] bytesBigEndian = new byte[8]; protected byte[] bytesBigEndian = new byte[8];
public readonly List<string> StringTable = new(32); public readonly List<string> StringTable = new(32);
protected Stream output; protected Stream output;
public SkeletonWriter(Stream output) { this.output = output; } public BinaryWriter(Stream output) { this.output = output; }
public void Write(int val) => output.WriteByte((byte)val); public void Write(int val) => output.WriteByte((byte)val);
public void WriteByte(byte val) => output.WriteByte(val); public void WriteByte(byte val) => output.WriteByte(val);
public void WriteUByte(byte val) => output.WriteByte(val); public void WriteUByte(byte val) => output.WriteByte(val);

View File

@@ -14,6 +14,7 @@ using System.ComponentModel;
using System.Reflection; using System.Reflection;
using System.Diagnostics.CodeAnalysis; using System.Diagnostics.CodeAnalysis;
using System.Globalization; using System.Globalization;
using System.Text.Json.Nodes;
namespace SpineViewer.Spine namespace SpineViewer.Spine
{ {
@@ -106,11 +107,79 @@ namespace SpineViewer.Spine
} }
} }
/// <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("spine", out var node))
versionString = (string)node;
}
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;
}
return version;
}
/// <summary> /// <summary>
/// 创建特定版本的 Spine /// 创建特定版本的 Spine
/// </summary> /// </summary>
public static Spine New(Version version, string skelPath, string? atlasPath = null) public static Spine New(Version 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)) if (!ImplementationTypes.TryGetValue(version, out var spineType))
{ {
throw new NotImplementedException($"Not implemented version: {version}"); throw new NotImplementedException($"Not implemented version: {version}");
@@ -133,7 +202,7 @@ namespace SpineViewer.Spine
var attr = type.GetCustomAttribute<SpineImplementationAttribute>(); var attr = type.GetCustomAttribute<SpineImplementationAttribute>();
if (attr is null) if (attr is null)
{ {
throw new InvalidOperationException($"Class {type.Name} has no SpineImplementationAttribute."); throw new InvalidOperationException($"Class {type.Name} has no SpineImplementationAttribute");
} }
atlasPath ??= Path.ChangeExtension(skelPath, ".atlas"); atlasPath ??= Path.ChangeExtension(skelPath, ".atlas");

View File

@@ -1,5 +1,6 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.ComponentModel; using System.ComponentModel;
using System.Linq; using System.Linq;
using System.Reflection; using System.Reflection;
@@ -13,7 +14,8 @@ namespace SpineViewer.Spine
/// <summary> /// <summary>
/// 描述缓存 /// 描述缓存
/// </summary> /// </summary>
public static readonly Dictionary<Version, string> Versions = []; public static readonly ReadOnlyDictionary<Version, string> Versions;
private static readonly Dictionary<Version, string> versions = [];
static VersionHelper() static VersionHelper()
{ {
@@ -22,8 +24,9 @@ namespace SpineViewer.Spine
{ {
var field = typeof(Version).GetField(value.ToString()); var field = typeof(Version).GetField(value.ToString());
var attribute = field?.GetCustomAttribute<DescriptionAttribute>(); var attribute = field?.GetCustomAttribute<DescriptionAttribute>();
Versions[(Version)value] = attribute?.Description ?? value.ToString(); versions[(Version)value] = attribute?.Description ?? value.ToString();
} }
Versions = versions.AsReadOnly();
} }
/// <summary> /// <summary>
@@ -40,6 +43,7 @@ namespace SpineViewer.Spine
/// </summary> /// </summary>
public enum Version public enum Version
{ {
[Description("<Auto>")] Auto = 0x0000,
[Description("2.1.x")] V21 = 0x0201, [Description("2.1.x")] V21 = 0x0201,
[Description("3.6.x")] V36 = 0x0306, [Description("3.6.x")] V36 = 0x0306,
[Description("3.7.x")] V37 = 0x0307, [Description("3.7.x")] V37 = 0x0307,

View File

@@ -8,7 +8,7 @@
<RuntimeIdentifier>win-x64</RuntimeIdentifier> <RuntimeIdentifier>win-x64</RuntimeIdentifier>
<BaseOutputPath>$(SolutionDir)out</BaseOutputPath> <BaseOutputPath>$(SolutionDir)out</BaseOutputPath>
<IncludeSourceRevisionInInformationalVersion>false</IncludeSourceRevisionInInformationalVersion> <IncludeSourceRevisionInInformationalVersion>false</IncludeSourceRevisionInInformationalVersion>
<Version>0.10.2</Version> <Version>0.10.3</Version>
<OutputType>WinExe</OutputType> <OutputType>WinExe</OutputType>
<UseWindowsForms>true</UseWindowsForms> <UseWindowsForms>true</UseWindowsForms>
<ApplicationIcon>appicon.ico</ApplicationIcon> <ApplicationIcon>appicon.ico</ApplicationIcon>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 302 KiB

BIN
img/preview.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 157 KiB