Compare commits

...

31 Commits

Author SHA1 Message Date
ww-rm
ac0adc5f95 更新至v0.10.0 2025-03-16 16:47:34 +08:00
ww-rm
208b702065 update changelog 2025-03-16 16:46:58 +08:00
ww-rm
7e61fbfbac 增加后缀筛选选项 2025-03-16 16:46:49 +08:00
ww-rm
0591549727 fix bug 2025-03-16 16:38:51 +08:00
ww-rm
a0833580f8 增加格式转换 2025-03-16 16:22:19 +08:00
ww-rm
c622b60215 增加版本检测 2025-03-16 16:12:36 +08:00
ww-rm
c228cf9072 add readanimation 2025-03-16 14:22:00 +08:00
ww-rm
4c68dd4904 add something 2025-03-16 01:40:19 +08:00
ww-rm
32fde582fc refactor 2025-03-14 18:55:56 +08:00
ww-rm
2bf2509df7 add read attachments 2025-03-14 01:12:22 +08:00
ww-rm
07042189c8 增加注释 2025-03-13 17:31:04 +08:00
ww-rm
d251c94638 some change 2025-03-13 17:29:56 +08:00
ww-rm
b4119087fb update readme 2025-03-13 14:53:32 +08:00
ww-rm
e3959e80fb add something 2025-03-13 14:32:37 +08:00
ww-rm
0495a2344c add something 2025-03-13 14:32:11 +08:00
ww-rm
c781ec5a4f fix bug 2025-03-13 14:23:00 +08:00
ww-rm
a58566735f 增加提示信息 2025-03-13 14:22:46 +08:00
ww-rm
b37e5c25c3 rename 2025-03-13 13:30:35 +08:00
ww-rm
63a937a45b optimize 2025-03-13 10:32:18 +08:00
ww-rm
c920471c0c change name 2025-03-13 02:32:53 +08:00
ww-rm
c4863ee09b add binary writer/reader 2025-03-13 01:31:24 +08:00
ww-rm
c0b85c454e 调整结构 2025-03-12 19:14:22 +08:00
ww-rm
763a49a4d3 增加一些按钮 2025-03-12 18:33:52 +08:00
ww-rm
0e1540873c 增加自动弹出选择文件 2025-03-12 18:33:07 +08:00
ww-rm
39dcc636ca 增加文件版本显示 2025-03-12 15:23:15 +08:00
ww-rm
342778c56e 增加画面和列表联动 2025-03-11 00:01:54 +08:00
ww-rm
fd524891aa 增加属性IsSelected 2025-03-11 00:00:23 +08:00
ww-rm
48cb60020c 增加属性SelectedIndices 2025-03-11 00:00:09 +08:00
ww-rm
d502c592f7 update readme 2025-03-06 11:16:59 +08:00
ww-rm
e4377436a7 修改文件编码 2025-03-06 00:00:56 +08:00
ww-rm
eb44c1271e 修改命名 2025-03-05 16:24:19 +08:00
31 changed files with 5233 additions and 183 deletions

8
CHANGELOG.md Normal file
View File

@@ -0,0 +1,8 @@
# CHANGELOG
## v0.10.0
- <20><><EFBFBD><EFBFBD><EFBFBD>˻<EFBFBD><CBBB><EFBFBD><EFBFBD><EFBFBD><EFBFBD>б<EFBFBD><D0B1><EFBFBD>ѡ<EFBFBD><D1A1><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ɾ<EFBFBD><C9BE><EFBFBD><EFBFBD>Ԥ<EFBFBD><D4A4><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ʾ<EFBFBD><CABE>Χ<EFBFBD><CEA7>ѡ<EFBFBD><D1A1>
- <20><><EFBFBD><EFBFBD><EFBFBD>˹<EFBFBD><CBB9><EFBFBD><EFBFBD>ļ<EFBFBD><C4BC><EFBFBD>ʽת<CABD><D7AA><EFBFBD><EFBFBD><EFBFBD>ܣ<EFBFBD>Ŀǰ<C4BF><C7B0>֧<EFBFBD>ֲ<EFBFBD><D6B2>ְ汾<D6B0>IJ<EFBFBD><C4B2><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>
- <20>Ż<EFBFBD><C5BB>˲<EFBFBD><CBB2><EFBFBD>ʹ<EFBFBD><CAB9><EFBFBD><EFBFBD><EFBFBD><EFBFBD>

View File

@@ -18,17 +18,21 @@
也可以下载带有 `SelfContained` 后缀的压缩包, 可以独立运行.
## 功能
## 功能支持
| 版本 | 查看&导出 | 转换 |
| :---: | :---: | :---: |
| `2.1.x` | :white_check_mark: | |
| `3.1.x` | | |
| `3.4.x` | | |
| `3.5.x` | | |
| `3.6.x` | :white_check_mark: | |
| `3.7.x` | :white_check_mark: | |
| `3.8.x` | :white_check_mark: | |
| `4.1.x` | :white_check_mark: | |
| `4.2.x` | :white_check_mark: | |
| `4.3.x` | | |
- 支持不同版本 Spine 查看
- [x] `v2.1.x`
- [x] `v3.6.x`
- [x] `v3.7.x`
- [x] `v3.8.x`
- [x] `v4.0.x`
- [x] `v4.1.x`
- [x] `v4.2.x`
- [ ] `v4.3.x`
- 支持多骨骼文件动画预览
- 支持每个骨骼独立参数设置
- 支持动画PNG帧序列导出

View File

@@ -15,6 +15,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution
ProjectSection(SolutionItems) = preProject
.editorconfig = .editorconfig
.gitignore = .gitignore
CHANGELOG.md = CHANGELOG.md
README.en.md = README.en.md
README.md = README.md
EndProjectSection

View File

@@ -35,6 +35,11 @@ namespace SpineViewer.Controls
Spines = spines.AsReadOnly();
}
/// <summary>
/// listView.SelectedIndices
/// </summary>
public ListView.SelectedIndexCollection SelectedIndices { get => listView.SelectedIndices; }
/// <summary>
/// 弹出添加对话框在指定位置之前插入一项
/// </summary>
@@ -156,6 +161,10 @@ namespace SpineViewer.Controls
PropertyGrid.SelectedObject = spines[listView.SelectedIndices[0]];
else
PropertyGrid.SelectedObjects = listView.SelectedIndices.Cast<int>().Select(index => spines[index]).ToArray();
// 标记选中的 Spine
for (int i = 0; i < spines.Count; i++)
spines[i].IsSelected = listView.SelectedIndices.Contains(i);
}
}
}

View File

@@ -24,11 +24,11 @@ namespace SpineViewer.Controls
public PreviewerProperty(SpinePreviewer previewer) { this.previewer = previewer; }
[TypeConverter(typeof(SizeTypeConverter))]
[TypeConverter(typeof(SizeConverter))]
[Category("导出"), DisplayName("分辨率")]
public Size Resolution { get => previewer.Resolution; set => previewer.Resolution = value; }
[TypeConverter(typeof(PointFTypeConverter))]
[TypeConverter(typeof(PointFConverter))]
[Category("导出"), DisplayName("画面中心点")]
public PointF Center { get => previewer.Center; set => previewer.Center = value; }
@@ -47,9 +47,6 @@ namespace SpineViewer.Controls
[Category("预览"), DisplayName("显示坐标轴")]
public bool ShowAxis { get => previewer.ShowAxis; set => previewer.ShowAxis = value; }
[Category("预览"), DisplayName("显示包围盒")]
public bool ShowBounds { get => previewer.ShowBounds; set => previewer.ShowBounds = value; }
[Category("预览"), DisplayName("最大帧率")]
public uint MaxFps { get => previewer.MaxFps; set => previewer.MaxFps = value; }
}
@@ -84,7 +81,6 @@ namespace SpineViewer.Controls
private readonly SFML.Graphics.RenderWindow RenderWindow;
private readonly SFML.System.Clock Clock = new();
private SFML.System.Vector2f? draggingSrc = null;
private Spine.Spine? draggingSpine = null;
private Task? task = null;
private CancellationTokenSource? cancelToken = null;
@@ -236,13 +232,6 @@ namespace SpineViewer.Controls
[Browsable(false)]
public bool ShowAxis { get; set; } = true;
/// <summary>
/// 显示包围盒
/// </summary>
[DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
[Browsable(false)]
public bool ShowBounds { get; set; } = true;
/// <summary>
/// 最大帧率
/// </summary>
@@ -328,7 +317,6 @@ namespace SpineViewer.Controls
// 右键优先级高, 进入画面拖动模式, 需要重新记录源点
if ((e.Button & MouseButtons.Right) != 0)
{
draggingSpine = null;
draggingSrc = RenderWindow.MapPixelToCoords(new(e.X, e.Y));
Cursor = Cursors.Hand;
}
@@ -342,14 +330,43 @@ namespace SpineViewer.Controls
{
lock (SpineListView.Spines)
{
foreach (var spine in SpineListView.Spines)
var spines = SpineListView.Spines;
// 没有按下 Ctrl 键就只选中点击的那个, 所以先清空选中列表
if ((ModifierKeys & Keys.Control) == 0)
{
if (spine.Bounds.Contains(src))
bool hit = false;
for (int i = 0; i < spines.Count; i++)
{
draggingSpine = spine;
if (spines[i].Bounds.Contains(src))
{
hit = true;
// 如果点到了没被选中的东西, 则清空原先选中的, 改为只选中这一次点的
if (!SpineListView.SelectedIndices.Contains(i))
{
SpineListView.SelectedIndices.Clear();
SpineListView.SelectedIndices.Add(i);
}
break;
}
}
// 如果点了空白的地方, 就清空选中列表
if (!hit)
SpineListView.SelectedIndices.Clear();
}
else
{
for (int i = 0; i < spines.Count; i++)
{
if (spines[i].Bounds.Contains(src))
{
SpineListView.SelectedIndices.Add(i);
break;
}
}
}
}
}
}
@@ -371,8 +388,17 @@ namespace SpineViewer.Controls
}
else if ((e.Button & MouseButtons.Left) != 0)
{
if (draggingSpine is not null)
draggingSpine.Position += delta;
if (SpineListView is not null)
{
lock (SpineListView.Spines)
{
foreach (var spine in SpineListView.Spines)
{
if (spine.IsSelected)
spine.Position += delta;
}
}
}
draggingSrc = dst;
}
}
@@ -382,7 +408,6 @@ namespace SpineViewer.Controls
// 右键高优先级, 结束画面拖动模式
if ((e.Button & MouseButtons.Right) != 0)
{
draggingSpine = null;
SpineListView?.PropertyGrid?.Refresh();
draggingSrc = null;
@@ -393,7 +418,6 @@ namespace SpineViewer.Controls
else if ((e.Button & MouseButtons.Left) != 0 && (MouseButtons & MouseButtons.Right) == 0)
{
draggingSrc = null;
draggingSpine = null;
SpineListView?.PropertyGrid?.Refresh();
}
}
@@ -439,7 +463,7 @@ namespace SpineViewer.Controls
spine.Update(delta);
RenderWindow.Draw(spine);
if (ShowBounds)
if (spine.IsSelected)
{
var bounds = spine.Bounds;
BoundsRect[0] = BoundsRect[4] = new(new(bounds.Left, bounds.Top), BoundsColor);

View File

@@ -194,8 +194,9 @@
//
openFileDialog_Skel.AddExtension = false;
openFileDialog_Skel.AddToRecent = false;
openFileDialog_Skel.Filter = "skel 文件 (*.skel; *.json)|*.skel;*.json|所有文件 (*.*)|*.*";
openFileDialog_Skel.Filter = "skel 文件 (*.skel; *.json)|*.skel;*.json|二进制文件 (*.skel)|*.skel|文本文件 (*.json)|*.json|所有文件 (*.*)|*.*";
openFileDialog_Skel.Multiselect = true;
openFileDialog_Skel.Title = "批量选择skel文件";
//
// BatchOpenSpineDialog
//
@@ -213,6 +214,7 @@
ShowInTaskbar = false;
StartPosition = FormStartPosition.CenterScreen;
Text = "批量打开骨骼";
Load += BatchOpenSpineDialog_Load;
panel.ResumeLayout(false);
tableLayoutPanel1.ResumeLayout(false);
tableLayoutPanel1.PerformLayout();

View File

@@ -25,6 +25,11 @@ namespace SpineViewer.Dialogs
comboBox_Version.SelectedValue = Spine.Version.V38;
}
private void BatchOpenSpineDialog_Load(object sender, EventArgs e)
{
button_SelectSkel_Click(sender, e);
}
private void button_SelectSkel_Click(object sender, EventArgs e)
{
if (openFileDialog_Skel.ShowDialog() == DialogResult.OK)
@@ -38,6 +43,8 @@ namespace SpineViewer.Dialogs
private void button_Ok_Click(object sender, EventArgs e)
{
var version = (Spine.Version)comboBox_Version.SelectedValue;
if (listBox_FilePath.Items.Count <= 0)
{
MessageBox.Show("未选择任何文件", "错误信息", MessageBoxButtons.OK, MessageBoxIcon.Information);
@@ -53,8 +60,14 @@ namespace SpineViewer.Dialogs
}
}
if (!Spine.Spine.ImplementedVersions.Contains(version))
{
MessageBox.Show($"{version.String()} 版本尚未实现(咕咕咕~", "错误信息", MessageBoxButtons.OK, MessageBoxIcon.Information);
return;
}
SkelPaths = listBox_FilePath.Items.Cast<string>().ToArray();
Version = (Spine.Version)comboBox_Version.SelectedValue;
Version = version;
DialogResult = DialogResult.OK;
}

View File

@@ -0,0 +1,357 @@
namespace SpineViewer.Dialogs
{
partial class ConvertFileFormatDialog
{
/// <summary>
/// Required designer variable.
/// </summary>
private System.ComponentModel.IContainer components = null;
/// <summary>
/// Clean up any resources being used.
/// </summary>
/// <param name="disposing">true if managed resources should be disposed; otherwise, false.</param>
protected override void Dispose(bool disposing)
{
if (disposing && (components != null))
{
components.Dispose();
}
base.Dispose(disposing);
}
#region Windows Form Designer generated code
/// <summary>
/// Required method for Designer support - do not modify
/// the contents of this method with the code editor.
/// </summary>
private void InitializeComponent()
{
System.ComponentModel.ComponentResourceManager resources = new System.ComponentModel.ComponentResourceManager(typeof(ConvertFileFormatDialog));
panel = new Panel();
tableLayoutPanel1 = new TableLayoutPanel();
flowLayoutPanel_TargetFormat = new FlowLayoutPanel();
radioButton_BinaryTarget = new RadioButton();
radioButton_JsonTarget = new RadioButton();
label1 = new Label();
label4 = new Label();
label3 = new Label();
comboBox_Version = new ComboBox();
tableLayoutPanel2 = new TableLayoutPanel();
button_Ok = new Button();
button_Cancel = new Button();
listBox_FilePath = new ListBox();
button_SelectSkel = new Button();
label_Tip = new Label();
label2 = new Label();
flowLayoutPanel_SourceFormat = new FlowLayoutPanel();
radioButton_BinarySource = new RadioButton();
radioButton_JsonSource = new RadioButton();
openFileDialog_Skel = new OpenFileDialog();
panel.SuspendLayout();
tableLayoutPanel1.SuspendLayout();
flowLayoutPanel_TargetFormat.SuspendLayout();
tableLayoutPanel2.SuspendLayout();
flowLayoutPanel_SourceFormat.SuspendLayout();
SuspendLayout();
//
// panel
//
panel.Controls.Add(tableLayoutPanel1);
panel.Dock = DockStyle.Fill;
panel.Location = new Point(0, 0);
panel.Name = "panel";
panel.Padding = new Padding(50, 15, 50, 10);
panel.Size = new Size(1039, 530);
panel.TabIndex = 2;
//
// tableLayoutPanel1
//
tableLayoutPanel1.ColumnCount = 2;
tableLayoutPanel1.ColumnStyles.Add(new ColumnStyle());
tableLayoutPanel1.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 100F));
tableLayoutPanel1.Controls.Add(flowLayoutPanel_TargetFormat, 1, 5);
tableLayoutPanel1.Controls.Add(label1, 0, 4);
tableLayoutPanel1.Controls.Add(label4, 0, 0);
tableLayoutPanel1.Controls.Add(label3, 0, 3);
tableLayoutPanel1.Controls.Add(comboBox_Version, 1, 3);
tableLayoutPanel1.Controls.Add(tableLayoutPanel2, 0, 6);
tableLayoutPanel1.Controls.Add(listBox_FilePath, 0, 2);
tableLayoutPanel1.Controls.Add(button_SelectSkel, 0, 1);
tableLayoutPanel1.Controls.Add(label_Tip, 1, 1);
tableLayoutPanel1.Controls.Add(label2, 0, 5);
tableLayoutPanel1.Controls.Add(flowLayoutPanel_SourceFormat, 1, 4);
tableLayoutPanel1.Dock = DockStyle.Fill;
tableLayoutPanel1.Location = new Point(50, 15);
tableLayoutPanel1.Name = "tableLayoutPanel1";
tableLayoutPanel1.RowCount = 7;
tableLayoutPanel1.RowStyles.Add(new RowStyle());
tableLayoutPanel1.RowStyles.Add(new RowStyle());
tableLayoutPanel1.RowStyles.Add(new RowStyle());
tableLayoutPanel1.RowStyles.Add(new RowStyle());
tableLayoutPanel1.RowStyles.Add(new RowStyle());
tableLayoutPanel1.RowStyles.Add(new RowStyle());
tableLayoutPanel1.RowStyles.Add(new RowStyle());
tableLayoutPanel1.Size = new Size(939, 505);
tableLayoutPanel1.TabIndex = 1;
//
// flowLayoutPanel_TargetFormat
//
flowLayoutPanel_TargetFormat.AutoSize = true;
flowLayoutPanel_TargetFormat.Controls.Add(radioButton_BinaryTarget);
flowLayoutPanel_TargetFormat.Controls.Add(radioButton_JsonTarget);
flowLayoutPanel_TargetFormat.Dock = DockStyle.Fill;
flowLayoutPanel_TargetFormat.Location = new Point(146, 381);
flowLayoutPanel_TargetFormat.Name = "flowLayoutPanel_TargetFormat";
flowLayoutPanel_TargetFormat.Size = new Size(790, 34);
flowLayoutPanel_TargetFormat.TabIndex = 19;
//
// radioButton_BinaryTarget
//
radioButton_BinaryTarget.AutoSize = true;
radioButton_BinaryTarget.Location = new Point(3, 3);
radioButton_BinaryTarget.Name = "radioButton_BinaryTarget";
radioButton_BinaryTarget.Size = new Size(151, 28);
radioButton_BinaryTarget.TabIndex = 17;
radioButton_BinaryTarget.Text = "二进制 (*.skel)";
radioButton_BinaryTarget.UseVisualStyleBackColor = true;
radioButton_BinaryTarget.CheckedChanged += radioButton_Target_CheckedChanged;
//
// radioButton_JsonTarget
//
radioButton_JsonTarget.AutoSize = true;
radioButton_JsonTarget.Checked = true;
radioButton_JsonTarget.Location = new Point(160, 3);
radioButton_JsonTarget.Name = "radioButton_JsonTarget";
radioButton_JsonTarget.Size = new Size(135, 28);
radioButton_JsonTarget.TabIndex = 18;
radioButton_JsonTarget.TabStop = true;
radioButton_JsonTarget.Text = "文本 (*.json)";
radioButton_JsonTarget.UseVisualStyleBackColor = true;
radioButton_JsonTarget.CheckedChanged += radioButton_Target_CheckedChanged;
//
// label1
//
label1.Anchor = AnchorStyles.Right;
label1.AutoSize = true;
label1.Location = new Point(72, 346);
label1.Name = "label1";
label1.Size = new Size(68, 24);
label1.TabIndex = 15;
label1.Text = "源格式:";
//
// label4
//
label4.AutoSize = true;
tableLayoutPanel1.SetColumnSpan(label4, 4);
label4.Dock = DockStyle.Fill;
label4.Location = new Point(15, 15);
label4.Margin = new Padding(15);
label4.Name = "label4";
label4.Size = new Size(909, 24);
label4.TabIndex = 14;
label4.Text = "说明:将在每个文件同级目录下生成目标格式后缀的文件,会覆盖已存在文件";
label4.TextAlign = ContentAlignment.MiddleCenter;
//
// label3
//
label3.Anchor = AnchorStyles.Right;
label3.AutoSize = true;
label3.Location = new Point(72, 307);
label3.Name = "label3";
label3.Size = new Size(68, 24);
label3.TabIndex = 12;
label3.Text = "源版本:";
//
// comboBox_Version
//
comboBox_Version.Anchor = AnchorStyles.Left;
comboBox_Version.DropDownStyle = ComboBoxStyle.DropDownList;
comboBox_Version.FormattingEnabled = true;
comboBox_Version.Location = new Point(146, 303);
comboBox_Version.Name = "comboBox_Version";
comboBox_Version.Size = new Size(182, 32);
comboBox_Version.Sorted = true;
comboBox_Version.TabIndex = 13;
//
// tableLayoutPanel2
//
tableLayoutPanel2.AutoSize = true;
tableLayoutPanel2.AutoSizeMode = AutoSizeMode.GrowAndShrink;
tableLayoutPanel2.ColumnCount = 2;
tableLayoutPanel1.SetColumnSpan(tableLayoutPanel2, 4);
tableLayoutPanel2.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 50F));
tableLayoutPanel2.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 50F));
tableLayoutPanel2.Controls.Add(button_Ok, 0, 0);
tableLayoutPanel2.Controls.Add(button_Cancel, 1, 0);
tableLayoutPanel2.Dock = DockStyle.Bottom;
tableLayoutPanel2.Location = new Point(3, 462);
tableLayoutPanel2.Name = "tableLayoutPanel2";
tableLayoutPanel2.RowCount = 1;
tableLayoutPanel2.RowStyles.Add(new RowStyle());
tableLayoutPanel2.Size = new Size(933, 40);
tableLayoutPanel2.TabIndex = 11;
//
// button_Ok
//
button_Ok.Anchor = AnchorStyles.Bottom | AnchorStyles.Right;
button_Ok.Location = new Point(324, 3);
button_Ok.Margin = new Padding(3, 3, 30, 3);
button_Ok.Name = "button_Ok";
button_Ok.Size = new Size(112, 34);
button_Ok.TabIndex = 7;
button_Ok.Text = "确认";
button_Ok.UseVisualStyleBackColor = true;
button_Ok.Click += button_Ok_Click;
//
// button_Cancel
//
button_Cancel.Anchor = AnchorStyles.Bottom | AnchorStyles.Left;
button_Cancel.Location = new Point(496, 3);
button_Cancel.Margin = new Padding(30, 3, 3, 3);
button_Cancel.Name = "button_Cancel";
button_Cancel.Size = new Size(112, 34);
button_Cancel.TabIndex = 8;
button_Cancel.Text = "取消";
button_Cancel.UseVisualStyleBackColor = true;
button_Cancel.Click += button_Cancel_Click;
//
// listBox_FilePath
//
tableLayoutPanel1.SetColumnSpan(listBox_FilePath, 2);
listBox_FilePath.Dock = DockStyle.Fill;
listBox_FilePath.FormattingEnabled = true;
listBox_FilePath.HorizontalScrollbar = true;
listBox_FilePath.ItemHeight = 24;
listBox_FilePath.Location = new Point(3, 97);
listBox_FilePath.Name = "listBox_FilePath";
listBox_FilePath.Size = new Size(933, 200);
listBox_FilePath.TabIndex = 2;
//
// button_SelectSkel
//
button_SelectSkel.Anchor = AnchorStyles.None;
button_SelectSkel.Location = new Point(3, 57);
button_SelectSkel.Name = "button_SelectSkel";
button_SelectSkel.Size = new Size(137, 34);
button_SelectSkel.TabIndex = 1;
button_SelectSkel.Text = "选择文件...";
button_SelectSkel.UseVisualStyleBackColor = true;
button_SelectSkel.Click += button_SelectSkel_Click;
//
// label_Tip
//
label_Tip.AutoSize = true;
label_Tip.Dock = DockStyle.Fill;
label_Tip.Location = new Point(146, 54);
label_Tip.Name = "label_Tip";
label_Tip.Size = new Size(790, 40);
label_Tip.TabIndex = 0;
label_Tip.Text = "已选择 0 个文件";
label_Tip.TextAlign = ContentAlignment.MiddleLeft;
//
// label2
//
label2.Anchor = AnchorStyles.Right;
label2.AutoSize = true;
label2.Location = new Point(54, 386);
label2.Name = "label2";
label2.Size = new Size(86, 24);
label2.TabIndex = 16;
label2.Text = "目标格式:";
//
// flowLayoutPanel_SourceFormat
//
flowLayoutPanel_SourceFormat.AutoSize = true;
flowLayoutPanel_SourceFormat.Controls.Add(radioButton_BinarySource);
flowLayoutPanel_SourceFormat.Controls.Add(radioButton_JsonSource);
flowLayoutPanel_SourceFormat.Dock = DockStyle.Fill;
flowLayoutPanel_SourceFormat.Location = new Point(146, 341);
flowLayoutPanel_SourceFormat.Name = "flowLayoutPanel_SourceFormat";
flowLayoutPanel_SourceFormat.Size = new Size(790, 34);
flowLayoutPanel_SourceFormat.TabIndex = 18;
//
// radioButton_BinarySource
//
radioButton_BinarySource.AutoSize = true;
radioButton_BinarySource.Checked = true;
radioButton_BinarySource.Location = new Point(3, 3);
radioButton_BinarySource.Name = "radioButton_BinarySource";
radioButton_BinarySource.Size = new Size(151, 28);
radioButton_BinarySource.TabIndex = 17;
radioButton_BinarySource.TabStop = true;
radioButton_BinarySource.Text = "二进制 (*.skel)";
radioButton_BinarySource.UseVisualStyleBackColor = true;
radioButton_BinarySource.CheckedChanged += radioButton_Source_CheckedChanged;
//
// radioButton_JsonSource
//
radioButton_JsonSource.AutoSize = true;
radioButton_JsonSource.Location = new Point(160, 3);
radioButton_JsonSource.Name = "radioButton_JsonSource";
radioButton_JsonSource.Size = new Size(135, 28);
radioButton_JsonSource.TabIndex = 18;
radioButton_JsonSource.Text = "文本 (*.json)";
radioButton_JsonSource.UseVisualStyleBackColor = true;
radioButton_JsonSource.CheckedChanged += radioButton_Source_CheckedChanged;
//
// 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;
AutoScaleDimensions = new SizeF(11F, 24F);
AutoScaleMode = AutoScaleMode.Font;
CancelButton = button_Cancel;
ClientSize = new Size(1039, 530);
Controls.Add(panel);
FormBorderStyle = FormBorderStyle.FixedDialog;
Icon = (Icon)resources.GetObject("$this.Icon");
MaximizeBox = false;
MinimizeBox = false;
Name = "ConvertFileFormatDialog";
ShowInTaskbar = false;
StartPosition = FormStartPosition.CenterScreen;
Text = "骨骼文件格式转换";
panel.ResumeLayout(false);
tableLayoutPanel1.ResumeLayout(false);
tableLayoutPanel1.PerformLayout();
flowLayoutPanel_TargetFormat.ResumeLayout(false);
flowLayoutPanel_TargetFormat.PerformLayout();
tableLayoutPanel2.ResumeLayout(false);
flowLayoutPanel_SourceFormat.ResumeLayout(false);
flowLayoutPanel_SourceFormat.PerformLayout();
ResumeLayout(false);
}
#endregion
private Panel panel;
private TableLayoutPanel tableLayoutPanel1;
private Label label4;
private Label label3;
private ComboBox comboBox_Version;
private TableLayoutPanel tableLayoutPanel2;
private Button button_Ok;
private Button button_Cancel;
private ListBox listBox_FilePath;
private Button button_SelectSkel;
private Label label_Tip;
private OpenFileDialog openFileDialog_Skel;
private Label label1;
private Label label2;
private RadioButton radioButton_BinarySource;
private FlowLayoutPanel flowLayoutPanel_SourceFormat;
private RadioButton radioButton_JsonSource;
private FlowLayoutPanel flowLayoutPanel_TargetFormat;
private RadioButton radioButton_BinaryTarget;
private RadioButton radioButton_JsonTarget;
}
}

View File

@@ -0,0 +1,94 @@
using SpineViewer.Spine;
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms;
namespace SpineViewer.Dialogs
{
public partial class ConvertFileFormatDialog : Form
{
public string[] SkelPaths { get; private set; }
public Spine.Version Version { get; private set; }
public bool ConvertToJson { get; private set; }
public ConvertFileFormatDialog()
{
InitializeComponent();
comboBox_Version.DataSource = VersionHelper.Versions.ToList();
comboBox_Version.DisplayMember = "Value";
comboBox_Version.ValueMember = "Key";
comboBox_Version.SelectedValue = Spine.Version.V38;
}
private void button_SelectSkel_Click(object sender, EventArgs e)
{
if (openFileDialog_Skel.ShowDialog() == DialogResult.OK)
{
listBox_FilePath.Items.Clear();
foreach (var p in openFileDialog_Skel.FileNames)
listBox_FilePath.Items.Add(Path.GetFullPath(p));
label_Tip.Text = $"已选择 {listBox_FilePath.Items.Count} 个文件";
}
}
private void button_Ok_Click(object sender, EventArgs e)
{
var version = (Spine.Version)comboBox_Version.SelectedValue;
if (listBox_FilePath.Items.Count <= 0)
{
MessageBox.Show("未选择任何文件", "错误信息", MessageBoxButtons.OK, MessageBoxIcon.Information);
return;
}
foreach (string p in listBox_FilePath.Items)
{
if (!File.Exists(p))
{
MessageBox.Show($"{p}", "skel文件不存在", MessageBoxButtons.OK, MessageBoxIcon.Information);
return;
}
}
if (!Spine.Spine.ImplementedVersions.Contains(version) ||
!SkeletonConverter.ImplementedVersions.Contains(version))
{
MessageBox.Show($"{version.String()} 版本尚未实现(咕咕咕~", "错误信息", MessageBoxButtons.OK, MessageBoxIcon.Information);
return;
}
SkelPaths = listBox_FilePath.Items.Cast<string>().ToArray();
Version = version;
ConvertToJson = radioButton_BinarySource.Checked;
DialogResult = DialogResult.OK;
}
private void button_Cancel_Click(object sender, EventArgs e)
{
DialogResult = DialogResult.Cancel;
}
private void radioButton_Source_CheckedChanged(object sender, EventArgs e)
{
if (radioButton_BinarySource.Checked)
radioButton_JsonTarget.Checked = true;
else
radioButton_BinaryTarget.Checked = true;
}
private void radioButton_Target_CheckedChanged(object sender, EventArgs e)
{
if (radioButton_BinaryTarget.Checked)
radioButton_JsonSource.Checked = true;
else
radioButton_BinarySource.Checked = true;
}
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -232,13 +232,15 @@
//
openFileDialog_Skel.AddExtension = false;
openFileDialog_Skel.AddToRecent = false;
openFileDialog_Skel.Filter = "skel 文件 (*.skel; *.json)|*.skel;*.json|所有文件 (*.*)|*.*";
openFileDialog_Skel.Filter = "skel 文件 (*.skel; *.json)|*.skel;*.json|二进制文件 (*.skel)|*.skel|文本文件 (*.json)|*.json|所有文件 (*.*)|*.*";
openFileDialog_Skel.Title = "选择skel文件";
//
// openFileDialog_Atlas
//
openFileDialog_Atlas.AddExtension = false;
openFileDialog_Atlas.AddToRecent = false;
openFileDialog_Atlas.Filter = "atlas 文件 (*.atlas)|*.atlas|所有文件 (*.*)|*.*";
openFileDialog_Atlas.Title = "选择atlas文件";
//
// OpenSpineDialog
//
@@ -256,6 +258,7 @@
ShowInTaskbar = false;
StartPosition = FormStartPosition.CenterScreen;
Text = "打开骨骼";
Load += OpenSpineDialog_Load;
panel1.ResumeLayout(false);
panel1.PerformLayout();
tableLayoutPanel1.ResumeLayout(false);

View File

@@ -8,7 +8,6 @@ using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms;
namespace SpineViewer.Dialogs
{
public partial class OpenSpineDialog : Form
@@ -26,6 +25,11 @@ namespace SpineViewer.Dialogs
comboBox_Version.SelectedValue = Spine.Version.V38;
}
private void OpenSpineDialog_Load(object sender, EventArgs e)
{
button_SelectSkel_Click(sender, e);
}
private void button_SelectSkel_Click(object sender, EventArgs e)
{
openFileDialog_Skel.InitialDirectory = Path.GetDirectoryName(textBox_SkelPath.Text);
@@ -48,6 +52,7 @@ namespace SpineViewer.Dialogs
{
var skelPath = textBox_SkelPath.Text;
var atlasPath = textBox_AtlasPath.Text;
var version = (Spine.Version)comboBox_Version.SelectedValue;
if (!File.Exists(skelPath))
{
@@ -73,9 +78,15 @@ namespace SpineViewer.Dialogs
atlasPath = Path.GetFullPath(atlasPath);
}
if (!Spine.Spine.ImplementedVersions.Contains(version))
{
MessageBox.Show($"{version.String()} 版本尚未实现(咕咕咕~", "错误信息", MessageBoxButtons.OK, MessageBoxIcon.Information);
return;
}
SkelPath = skelPath;
AtlasPath = atlasPath;
Version = (Spine.Version)comboBox_Version.SelectedValue;
Version = version;
DialogResult = DialogResult.OK;
}

View File

@@ -118,10 +118,10 @@
<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>75, 24</value>
<value>58, 25</value>
</metadata>
<metadata name="openFileDialog_Atlas.TrayLocation" type="System.Drawing.Point, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a">
<value>317, 22</value>
<value>349, 29</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">

View File

@@ -40,6 +40,10 @@
toolStripMenuItem_Exit = new ToolStripMenuItem();
toolStripMenuItem_Function = new ToolStripMenuItem();
toolStripMenuItem_ResetAnimation = new ToolStripMenuItem();
toolStripMenuItem_Tool = new ToolStripMenuItem();
toolStripMenuItem_ConvertFileFormat = new ToolStripMenuItem();
toolStripMenuItem_Download = new ToolStripMenuItem();
toolStripMenuItem_ManageResource = new ToolStripMenuItem();
toolStripMenuItem_Help = new ToolStripMenuItem();
toolStripMenuItem_Diagnostics = new ToolStripMenuItem();
toolStripSeparator3 = new ToolStripSeparator();
@@ -87,7 +91,7 @@
//
menuStrip.BackColor = SystemColors.Control;
menuStrip.ImageScalingSize = new Size(24, 24);
menuStrip.Items.AddRange(new ToolStripItem[] { toolStripMenuItem_File, toolStripMenuItem_Function, toolStripMenuItem_Help });
menuStrip.Items.AddRange(new ToolStripItem[] { toolStripMenuItem_File, toolStripMenuItem_Function, toolStripMenuItem_Tool, toolStripMenuItem_Download, toolStripMenuItem_Help });
menuStrip.Location = new Point(0, 0);
menuStrip.Name = "menuStrip";
menuStrip.Size = new Size(1741, 32);
@@ -156,6 +160,33 @@
toolStripMenuItem_ResetAnimation.Text = "重置动画时间(&R)";
toolStripMenuItem_ResetAnimation.Click += toolStripMenuItem_ResetAnimation_Click;
//
// toolStripMenuItem_Tool
//
toolStripMenuItem_Tool.DropDownItems.AddRange(new ToolStripItem[] { toolStripMenuItem_ConvertFileFormat });
toolStripMenuItem_Tool.Name = "toolStripMenuItem_Tool";
toolStripMenuItem_Tool.Size = new Size(84, 28);
toolStripMenuItem_Tool.Text = "工具(&T)";
//
// toolStripMenuItem_ConvertFileFormat
//
toolStripMenuItem_ConvertFileFormat.Name = "toolStripMenuItem_ConvertFileFormat";
toolStripMenuItem_ConvertFileFormat.Size = new Size(270, 34);
toolStripMenuItem_ConvertFileFormat.Text = "转换文件格式(&C)...";
toolStripMenuItem_ConvertFileFormat.Click += toolStripMenuItem_ConvertFileFormat_Click;
//
// toolStripMenuItem_Download
//
toolStripMenuItem_Download.DropDownItems.AddRange(new ToolStripItem[] { toolStripMenuItem_ManageResource });
toolStripMenuItem_Download.Name = "toolStripMenuItem_Download";
toolStripMenuItem_Download.Size = new Size(88, 28);
toolStripMenuItem_Download.Text = "下载(&D)";
//
// toolStripMenuItem_ManageResource
//
toolStripMenuItem_ManageResource.Name = "toolStripMenuItem_ManageResource";
toolStripMenuItem_ManageResource.Size = new Size(260, 34);
toolStripMenuItem_ManageResource.Text = "管理下载资源(&M)...";
//
// toolStripMenuItem_Help
//
toolStripMenuItem_Help.DropDownItems.AddRange(new ToolStripItem[] { toolStripMenuItem_Diagnostics, toolStripSeparator3, toolStripMenuItem_About });
@@ -465,5 +496,9 @@
private ToolStripMenuItem toolStripMenuItem_ResetAnimation;
private ToolStripMenuItem toolStripMenuItem_Diagnostics;
private ToolStripSeparator toolStripSeparator3;
private ToolStripMenuItem toolStripMenuItem_Download;
private ToolStripMenuItem toolStripMenuItem_ManageResource;
private ToolStripMenuItem toolStripMenuItem_Tool;
private ToolStripMenuItem toolStripMenuItem_ConvertFileFormat;
}
}

View File

@@ -1,4 +1,4 @@
using NLog;
using NLog;
using SpineViewer.Spine;
using System.ComponentModel;
using System.Diagnostics;
@@ -14,11 +14,11 @@ namespace SpineViewer
}
/// <summary>
/// <EFBFBD><EFBFBD>ʼ<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>־<EFBFBD><EFBFBD>
/// 初始化窗口日志器
/// </summary>
private void InitializeLogConfiguration()
{
// <EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>־
// 窗口日志
var rtbTarget = new NLog.Windows.Forms.RichTextBoxTarget
{
Name = "rtbTarget",
@@ -54,7 +54,7 @@ namespace SpineViewer
var tex = new SFML.Graphics.RenderTexture((uint)resolution.Width, (uint)resolution.Height);
tex.SetView(spinePreviewer.View);
var delta = 1f / fps;
var frameCount = 1 + (int)(duration / delta); // <EFBFBD><EFBFBD>֡<EFBFBD><EFBFBD>ʼ<EFBFBD><EFBFBD><EFBFBD><EFBFBD>
var frameCount = 1 + (int)(duration / delta); // 零帧开始导出
spinePreviewer.StopPreview();
@@ -62,7 +62,7 @@ namespace SpineViewer
{
var spinesReverse = spineListView.Spines.Reverse();
// <EFBFBD><EFBFBD><EFBFBD>ö<EFBFBD><EFBFBD><EFBFBD>ʱ<EFBFBD><EFBFBD>
// 重置动画时间
foreach (var spine in spinesReverse)
spine.CurrentAnimation = spine.CurrentAnimation;
@@ -71,9 +71,9 @@ namespace SpineViewer
[outputDir, duration, fps, spinesReverse.Count()]
);
// <EFBFBD><EFBFBD>֡<EFBFBD><EFBFBD><EFBFBD><EFBFBD>
// 逐帧导出
var success = 0;
worker.ReportProgress(0, $"<EFBFBD>Ѵ<EFBFBD><EFBFBD><EFBFBD> 0/{frameCount}");
worker.ReportProgress(0, $"已处理 0/{frameCount}");
for (int frameIndex = 0; frameIndex < frameCount; frameIndex++)
{
if (worker.CancellationPending)
@@ -94,7 +94,7 @@ namespace SpineViewer
}
success++;
worker.ReportProgress((int)((frameIndex + 1) * 100.0) / frameCount, $"<EFBFBD>Ѵ<EFBFBD><EFBFBD><EFBFBD> {frameIndex + 1}/{frameCount}");
worker.ReportProgress((int)((frameIndex + 1) * 100.0) / frameCount, $"已处理 {frameIndex + 1}/{frameCount}");
}
Program.Logger.Info("Exporting done: {}/{}", success, frameCount);
@@ -129,7 +129,7 @@ namespace SpineViewer
{
if (spineListView.Spines.Count <= 0)
{
MessageBox.Show("<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ٴ<EFBFBD><EFBFBD><EFBFBD>һ<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ļ<EFBFBD>", "<EFBFBD><EFBFBD>ʾ<EFBFBD><EFBFBD>Ϣ", MessageBoxButtons.OK, MessageBoxIcon.Information);
MessageBox.Show("请至少打开一个骨骼文件", "提示信息", MessageBoxButtons.OK, MessageBoxIcon.Information);
return;
}
}
@@ -158,6 +158,73 @@ namespace SpineViewer
}
}
private void toolStripMenuItem_ConvertFileFormat_Click(object sender, EventArgs e)
{
var openDialog = new Dialogs.ConvertFileFormatDialog();
if (openDialog.ShowDialog() != DialogResult.OK)
return;
var progressDialog = new Dialogs.ProgressDialog();
progressDialog.DoWork += ConvertFileFormat_Work;
progressDialog.RunWorkerAsync(openDialog);
progressDialog.ShowDialog();
}
private void ConvertFileFormat_Work(object? sender, DoWorkEventArgs e)
{
var worker = sender as BackgroundWorker;
var arguments = e.Argument as Dialogs.ConvertFileFormatDialog;
var skelPaths = arguments.SkelPaths;
var version = arguments.Version;
var convertToJson = arguments.ConvertToJson;
var newSuffix = convertToJson ? ".json" : ".skel";
int totalCount = skelPaths.Length;
int success = 0;
int error = 0;
SkeletonConverter cvter = SkeletonConverter.New(version);
worker.ReportProgress(0, $"已处理 0/{totalCount}");
for (int i = 0; i < totalCount; i++)
{
if (worker.CancellationPending)
{
e.Cancel = true;
break;
}
var skelPath = skelPaths[i];
var newPath = Path.ChangeExtension(skelPath, newSuffix);
try
{
if (convertToJson)
cvter.BinaryToJson(skelPath, newPath);
else
cvter.JsonToBinary(skelPath, newPath);
success++;
}
catch (Exception ex)
{
Program.Logger.Error(ex.ToString());
Program.Logger.Error("Failed to convert {}", skelPath);
error++;
}
worker.ReportProgress((int)((i + 1) * 100.0) / totalCount, $"已处理 {i + 1}/{totalCount}");
}
if (error > 0)
{
Program.Logger.Warn("Batch convert {} successfully, {} failed", success, error);
}
else
{
Program.Logger.Info("{} skel converted successfully", success);
}
}
private void toolStripMenuItem_About_Click(object sender, EventArgs e)
{
(new Dialogs.AboutDialog()).ShowDialog();
@@ -175,5 +242,6 @@ namespace SpineViewer
private void propertyGrid_PropertyValueChanged(object sender, PropertyValueChangedEventArgs e) { (sender as PropertyGrid)?.Refresh(); }
private void spinePreviewer_MouseUp(object sender, MouseEventArgs e) { propertyGrid_Spine.Refresh(); }
}
}

View File

@@ -1,4 +1,4 @@
using NLog;
using NLog;
using System.Diagnostics;
namespace SpineViewer
@@ -28,18 +28,18 @@ namespace SpineViewer
catch (Exception ex)
{
Logger.Fatal(ex.ToString());
MessageBox.Show(ex.ToString(), "<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ѱ<EFBFBD><EFBFBD><EFBFBD>", MessageBoxButtons.OK, MessageBoxIcon.Stop);
MessageBox.Show(ex.ToString(), "程序已崩溃", MessageBoxButtons.OK, MessageBoxIcon.Stop);
}
}
/// <summary>
/// <EFBFBD><EFBFBD>ʼ<EFBFBD><EFBFBD><EFBFBD><EFBFBD>־<EFBFBD><EFBFBD><EFBFBD><EFBFBD>
/// 初始化日志配置
/// </summary>
private static void InitializeLogConfiguration()
{
var config = new NLog.Config.LoggingConfiguration();
// <EFBFBD>ļ<EFBFBD><EFBFBD><EFBFBD>־
// 文件日志
var fileTarget = new NLog.Targets.FileTarget("fileTarget")
{
Encoding = System.Text.Encoding.UTF8,

View File

@@ -9,7 +9,7 @@ namespace SpineViewer.Spine
/// <summary>
/// SFML 混合模式
/// </summary>
public static class BlendMode
public static class BlendModeSFML
{
/// <summary>
/// Alpha Blend

View File

@@ -0,0 +1,824 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using SpineRuntime38;
using System.Text.Json;
using System.Text.Json.Nodes;
using SpineRuntime38.Attachments;
namespace SpineViewer.Spine.Implementations.SkeletonConverter
{
[SkeletonConverterImplementation(Version.V38)]
class SkeletonConverter38 : SpineViewer.Spine.SkeletonConverter
{
private SkeletonReader reader = null;
private JsonObject root = null;
private bool nonessential = false;
private List<JsonObject> idx2Event = [];
protected override JsonObject ReadBinary(string binPath)
{
using var input = File.OpenRead(binPath);
reader = new(input);
var result = root = [];
root["skeleton"] = ReadSkeleton();
ReadStrings();
root["bones"] = ReadBones();
root["slots"] = ReadSlots();
root["ik"] = ReadIK();
root["transform"] = ReadTransform();
root["path"] = ReadPath();
root["skins"] = ReadSkins();
root["events"] = ReadEvents();
root["animations"] = ReadAnimations();
reader = null;
nonessential = false;
root = null;
return result;
}
private JsonObject ReadSkeleton()
{
JsonObject skeleton = [];
skeleton["hash"] = reader.ReadString();
skeleton["spine"] = reader.ReadString();
skeleton["x"] = reader.ReadFloat();
skeleton["y"] = reader.ReadFloat();
skeleton["width"] = reader.ReadFloat();
skeleton["height"] = reader.ReadFloat();
nonessential = reader.ReadBoolean();
if (nonessential)
{
skeleton["fps"] = reader.ReadFloat();
skeleton["images"] = reader.ReadString();
skeleton["audio"] = reader.ReadString();
}
return skeleton;
}
private void ReadStrings()
{
for (int n = reader.ReadVarInt(); n > 0; n--)
reader.StringTable.Add(reader.ReadString());
}
private JsonArray ReadBones()
{
JsonArray bones = [];
for (int i = 0, n = reader.ReadVarInt(); i < n; i++)
{
JsonObject data = [];
data["name"] = reader.ReadString();
if (i > 0) data["parent"] = bones[reader.ReadVarInt()]["name"].GetValue<string>();
data["rotation"] = reader.ReadFloat();
data["x"] = reader.ReadFloat();
data["y"] = reader.ReadFloat();
data["scaleX"] = reader.ReadFloat();
data["scaleY"] = reader.ReadFloat();
data["shearX"] = reader.ReadFloat();
data["shearY"] = reader.ReadFloat();
data["length"] = reader.ReadFloat();
data["transform"] = SkeletonBinary.TransformModeValues[reader.ReadVarInt()].ToString();
data["skin"] = reader.ReadBoolean();
if (nonessential) reader.ReadInt();
bones.Add(data);
}
return bones;
}
private JsonArray ReadSlots()
{
JsonArray bones = root["bones"].AsArray();
JsonArray slots = [];
for (int n = reader.ReadVarInt(); n > 0; n--)
{
JsonObject data = [];
data["name"] = reader.ReadString();
data["bone"] = bones[reader.ReadVarInt()]["name"].GetValue<string>();
data["color"] = reader.ReadInt().ToString("x8"); // 0xrrggbbaa -> rrggbbaa
int dark = reader.ReadInt();
if (dark != -1) data["dark"] = dark.ToString("x6"); // 0x00rrggbb -> rrggbb
data["attachment"] = reader.ReadStringRef();
data["blend"] = ((BlendMode)reader.ReadVarInt()).ToString();
slots.Add(data);
}
return slots;
}
private JsonArray ReadIK()
{
JsonArray bones = root["bones"].AsArray();
JsonArray ik = [];
for (int n = reader.ReadVarInt(); n > 0; n--)
{
JsonObject data = [];
data["name"] = reader.ReadString();
data["order"] = reader.ReadVarInt();
data["skin"] = reader.ReadBoolean();
data["bones"] = ReadNames(bones);
data["target"] = bones[reader.ReadVarInt()]["name"].GetValue<string>();
data["mix"] = reader.ReadFloat();
data["softness"] = reader.ReadFloat();
data["bendPositive"] = reader.ReadSByte() > 0;
data["compress"] = reader.ReadBoolean();
data["stretch"] = reader.ReadBoolean();
data["uniform"] = reader.ReadBoolean();
ik.Add(data);
}
return ik;
}
private JsonArray ReadTransform()
{
JsonArray bones = root["bones"].AsArray();
JsonArray transform = [];
for (int n = reader.ReadVarInt(); n > 0; n--)
{
JsonObject data = [];
data["name"] = reader.ReadString();
data["order"] = reader.ReadVarInt();
data["skin"] = reader.ReadBoolean();
data["bones"] = ReadNames(bones);
data["target"] = bones[reader.ReadVarInt()]["name"].GetValue<string>();
data["local"] = reader.ReadBoolean();
data["relative"] = reader.ReadBoolean();
data["rotation"] = reader.ReadFloat();
data["x"] = reader.ReadFloat();
data["y"] = reader.ReadFloat();
data["scaleX"] = reader.ReadFloat();
data["scaleY"] = reader.ReadFloat();
data["shearY"] = reader.ReadFloat();
data["rotateMix"] = reader.ReadFloat();
data["translateMix"] = reader.ReadFloat();
data["scaleMix"] = reader.ReadFloat();
data["shearMix"] = reader.ReadFloat();
transform.Add(data);
}
return transform;
}
private JsonArray ReadPath()
{
JsonArray bones = root["bones"].AsArray();
JsonArray path = [];
for (int n = reader.ReadVarInt(); n > 0; n--)
{
JsonObject data = [];
data["name"] = reader.ReadString();
data["order"] = reader.ReadVarInt();
data["skin"] = reader.ReadBoolean();
data["bones"] = ReadNames(bones);
data["target"] = bones[reader.ReadVarInt()]["name"].GetValue<string>();
data["positionMode"] = ((PositionMode)reader.ReadVarInt()).ToString();
data["spacingMode"] = ((SpacingMode)reader.ReadVarInt()).ToString();
data["rotateMode"] = ((RotateMode)reader.ReadVarInt()).ToString();
data["rotation"] = reader.ReadFloat();
data["position"] = reader.ReadFloat();
data["spacing"] = reader.ReadFloat();
data["rotateMix"] = reader.ReadFloat();
data["translateMix"] = reader.ReadFloat();
path.Add(data);
}
return path;
}
private JsonArray ReadSkins()
{
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());
return skins;
}
private JsonObject? ReadSkin(bool isDefault = false)
{
JsonObject skin = [];
int slotCount;
if (isDefault)
{
// 这里固定有一个给 default 的 count 值, 算是占位符, 如果是 0 则表示没有 default 的 skin
skin["name"] = "default";
slotCount = reader.ReadVarInt();
if (slotCount <= 0) return null;
}
else
{
skin["name"] = reader.ReadStringRef();
skin["bones"] = ReadNames(root["bones"].AsArray());
skin["ik"] = ReadNames(root["ik"].AsArray());
skin["transform"] = ReadNames(root["transform"].AsArray()); ;
skin["path"] = ReadNames(root["path"].AsArray()); ;
slotCount = reader.ReadVarInt();
}
JsonArray slots = root["slots"].AsArray();
JsonObject skinAttachments = [];
while (slotCount-- > 0)
{
JsonObject slotAttachments = [];
skinAttachments[slots[reader.ReadVarInt()]["name"].GetValue<string>()] = slotAttachments;
for (int attachmentCount = reader.ReadVarInt(); attachmentCount > 0; attachmentCount--)
{
var attachmentKey = reader.ReadStringRef();
slotAttachments[attachmentKey] = ReadAttachment(attachmentKey);
}
}
skin["attachments"] = skinAttachments;
return skin;
}
private JsonObject ReadAttachment(string keyName)
{
JsonArray slots = root["slots"].AsArray();
JsonObject attachment = [];
int vertexCount;
string path;
string name = reader.ReadStringRef() ?? keyName;
var type = (AttachmentType)reader.ReadByte();
attachment["name"] = name;
attachment["type"] = type.ToString();
switch (type)
{
case AttachmentType.Region:
path = reader.ReadStringRef();
if (path is not null) attachment["path"] = path;
attachment["rotation"] = reader.ReadFloat();
attachment["x"] = reader.ReadFloat();
attachment["y"] = reader.ReadFloat();
attachment["scaleX"] = reader.ReadFloat();
attachment["scaleY"] = reader.ReadFloat();
attachment["width"] = reader.ReadFloat();
attachment["height"] = reader.ReadFloat();
attachment["color"] = reader.ReadInt().ToString("x8");
break;
case AttachmentType.Boundingbox:
vertexCount = reader.ReadVarInt();
attachment["vertexCount"] = vertexCount;
attachment["vertices"] = ReadVertices(vertexCount);
if (nonessential) reader.ReadInt();
break;
case AttachmentType.Mesh:
path = reader.ReadStringRef();
if (path is not null) attachment["path"] = path;
attachment["color"] = reader.ReadInt().ToString("x8");
vertexCount = reader.ReadVarInt();
attachment["uvs"] = ReadFloatArray(vertexCount << 1); // vertexCount = uvs.Length
attachment["triangles"] = ReadShortArray();
attachment["vertices"] = ReadVertices(vertexCount);
attachment["hull"] = reader.ReadVarInt();
if (nonessential)
{
attachment["edges"] = ReadShortArray();
attachment["width"] = reader.ReadFloat();
attachment["height"] = reader.ReadFloat();
}
break;
case AttachmentType.Linkedmesh:
path = reader.ReadStringRef();
if (path is not null) attachment["path"] = path;
attachment["color"] = reader.ReadInt().ToString("x8");
attachment["skin"] = reader.ReadStringRef();
attachment["parent"] = reader.ReadStringRef();
attachment["deform"] = reader.ReadBoolean();
if (nonessential)
{
attachment["width"] = reader.ReadFloat();
attachment["height"] = reader.ReadFloat();
}
// 补充缺失的必需 key
attachment["uvs"] = new JsonArray();
attachment["triangles"] = new JsonArray();
attachment["vertices"] = new JsonArray();
break;
case AttachmentType.Path:
attachment["closed"] = reader.ReadBoolean();
attachment["constantSpeed"] = reader.ReadBoolean();
vertexCount = reader.ReadVarInt();
attachment["vertexCount"] = vertexCount;
attachment["vertices"] = ReadVertices(vertexCount);
attachment["lengths"] = ReadFloatArray(vertexCount / 3);
if (nonessential) reader.ReadInt();
break;
case AttachmentType.Point:
attachment["rotation"] = reader.ReadFloat();
attachment["x"] = reader.ReadFloat();
attachment["y"] = reader.ReadFloat();
if (nonessential) reader.ReadInt();
break;
case AttachmentType.Clipping:
attachment["end"] = slots[reader.ReadVarInt()]["name"].GetValue<string>();
vertexCount = reader.ReadVarInt();
attachment["vertexCount"] = vertexCount;
attachment["vertices"] = ReadVertices(vertexCount);
if (nonessential) reader.ReadInt();
break;
default:
throw new ArgumentException($"Invalid attachment type: {type}");
}
return attachment;
}
private JsonObject ReadEvents()
{
idx2Event.Clear();
JsonObject events = [];
for (int n = reader.ReadVarInt(); n > 0; n--)
{
JsonObject data = [];
var name = reader.ReadStringRef();
events[name] = data;
data["name"] = name; // 额外增加的, 方便后面查找
data["int"] = reader.ReadVarInt(false);
data["float"] = reader.ReadFloat();
data["string"] = reader.ReadString();
if (reader.ReadString() is string audio)
{
data["audio"] = audio;
data["volume"] = reader.ReadFloat();
data["balance"] = reader.ReadFloat();
}
idx2Event.Add(data);
}
return events;
}
private JsonObject ReadAnimations()
{
JsonObject animations = [];
for (int n = reader.ReadVarInt(); n > 0; n--)
{
JsonObject data = [];
animations[reader.ReadString()] = data;
if (ReadSlotTimelines() is JsonObject slots) data["slots"] = slots;
if (ReadBoneTimelines() is JsonObject bones) data["bones"] = bones;
if (ReadIKTimelines() is JsonObject ik) data["ik"] = ik;
if (ReadTransformTimelines() is JsonObject transform) data["transform"] = transform;
if (ReadPathTimelines() is JsonObject path) data["path"] = path;
if (ReadDeformTimelines() is JsonObject deform) data["deform"] = deform;
if (ReadDrawOrderTimelines() is JsonArray draworder) data["drawOrder"] = draworder;
if (ReadEventTimelines() is JsonArray events) data["events"] = events;
}
return animations;
}
private JsonObject? ReadSlotTimelines()
{
JsonArray slots = root["slots"].AsArray();
JsonObject slotTimelines = [];
for (int slotCount = reader.ReadVarInt(); slotCount > 0; slotCount--)
{
JsonObject timeline = [];
slotTimelines[slots[reader.ReadVarInt()]["name"].GetValue<string>()] = timeline;
for (int timelineCount = reader.ReadVarInt(); timelineCount > 0; timelineCount--)
{
JsonArray frames = [];
var type = reader.ReadByte();
var frameCount = reader.ReadVarInt();
switch (type)
{
case SkeletonBinary.SLOT_ATTACHMENT:
timeline["attachment"] = frames;
while (frameCount-- > 0)
{
frames.Add(new JsonObject()
{
["time"] = reader.ReadFloat(),
["name"] = reader.ReadStringRef(),
});
}
break;
case SkeletonBinary.SLOT_COLOR:
timeline["color"] = frames;
while (frameCount-- > 0)
{
var o = new JsonObject()
{
["time"] = reader.ReadFloat(),
["color"] = reader.ReadInt().ToString("x8"),
};
if (frameCount > 0) ReadCurve(o);
frames.Add(o);
}
break;
case SkeletonBinary.SLOT_TWO_COLOR:
timeline["twoColor"] = frames;
while (frameCount-- > 0)
{
var o = new JsonObject()
{
["time"] = reader.ReadFloat(),
["light"] = reader.ReadInt().ToString("x8"),
["dark"] = reader.ReadInt().ToString("x6"),
};
if (frameCount > 0) ReadCurve(o);
frames.Add(o);
}
break;
default:
throw new ArgumentException($"Invalid slot timeline type: {type}");
}
}
}
return slotTimelines.Count > 0 ? slotTimelines : null;
}
private JsonObject? ReadBoneTimelines()
{
JsonArray bones = root["bones"].AsArray();
JsonObject boneTimelines = [];
for (int boneCount = reader.ReadVarInt(); boneCount > 0; boneCount--)
{
JsonObject timeline = [];
boneTimelines[bones[reader.ReadVarInt()]["name"].GetValue<string>()] = timeline;
for (int timelineCount = reader.ReadVarInt(); timelineCount > 0; timelineCount--)
{
JsonArray frames = [];
var type = reader.ReadByte();
var frameCount = reader.ReadVarInt();
switch (type)
{
case SkeletonBinary.BONE_ROTATE:
timeline["rotate"] = frames;
while (frameCount-- > 0)
{
var o = new JsonObject()
{
["time"] = reader.ReadFloat(),
["angle"] = reader.ReadFloat(),
};
if (frameCount > 0) ReadCurve(o);
frames.Add(o);
}
break;
case SkeletonBinary.BONE_TRANSLATE:
timeline["translate"] = frames;
while (frameCount-- > 0)
{
var o = new JsonObject()
{
["time"] = reader.ReadFloat(),
["x"] = reader.ReadFloat(),
["y"] = reader.ReadFloat(),
};
if (frameCount > 0) ReadCurve(o);
frames.Add(o);
}
break;
case SkeletonBinary.BONE_SCALE:
timeline["scale"] = frames;
while (frameCount-- > 0)
{
var o = new JsonObject()
{
["time"] = reader.ReadFloat(),
["x"] = reader.ReadFloat(),
["y"] = reader.ReadFloat(),
};
if (frameCount > 0) ReadCurve(o);
frames.Add(o);
}
break;
case SkeletonBinary.BONE_SHEAR:
timeline["shear"] = frames;
while (frameCount-- > 0)
{
var o = new JsonObject()
{
["time"] = reader.ReadFloat(),
["x"] = reader.ReadFloat(),
["y"] = reader.ReadFloat(),
};
if (frameCount > 0) ReadCurve(o);
frames.Add(o);
}
break;
default:
throw new ArgumentException($"Invalid bone timeline type: {type}");
}
}
}
return boneTimelines.Count > 0 ? boneTimelines : null;
}
private JsonObject? ReadIKTimelines()
{
JsonArray ik = root["ik"].AsArray();
JsonObject ikTimelines = [];
for (int ikCount = reader.ReadVarInt(); ikCount > 0; ikCount--)
{
JsonArray frames = [];
ikTimelines[ik[reader.ReadVarInt()]["name"].GetValue<string>()] = frames;
for (int frameCount = reader.ReadVarInt(); frameCount > 0; frameCount--)
{
var o = new JsonObject()
{
["time"] = reader.ReadFloat(),
["mix"] = reader.ReadFloat(),
["softness"] = reader.ReadFloat(),
["bendPositive"] = reader.ReadSByte() > 0,
["compress"] = reader.ReadBoolean(),
["stretch"] = reader.ReadBoolean(),
};
if (frameCount > 1) ReadCurve(o);
frames.Add(o);
}
}
return ikTimelines.Count > 0 ? ikTimelines : null;
}
private JsonObject? ReadTransformTimelines()
{
JsonArray transform = root["transform"].AsArray();
JsonObject transformTimelines = [];
for (int transformCount = reader.ReadVarInt(); transformCount > 0; transformCount--)
{
JsonArray frames = [];
transformTimelines[transform[reader.ReadVarInt()]["name"].GetValue<string>()] = frames;
for (int frameCount = reader.ReadVarInt(); frameCount > 0; frameCount--)
{
var o = new JsonObject()
{
["time"] = reader.ReadFloat(),
["rotateMix"] = reader.ReadFloat(),
["translateMix"] = reader.ReadFloat(),
["scaleMix"] = reader.ReadFloat(),
["shearMix"] = reader.ReadFloat(),
};
if (frameCount > 1) ReadCurve(o);
frames.Add(o);
}
}
return transformTimelines.Count > 0 ? transformTimelines : null;
}
private JsonObject? ReadPathTimelines()
{
JsonArray path = root["path"].AsArray();
JsonObject pathTimelines = [];
for (int pathCount = reader.ReadVarInt(); pathCount > 0; pathCount--)
{
JsonObject timeline = [];
pathTimelines[path[reader.ReadVarInt()]["name"].GetValue<string>()] = timeline;
for (int timelineCount = reader.ReadVarInt(); timelineCount > 0; timelineCount--)
{
JsonArray frames = [];
var type = reader.ReadByte();
var frameCount = reader.ReadVarInt();
switch (type)
{
case SkeletonBinary.PATH_POSITION:
timeline["position"] = frames;
while (frameCount-- > 0)
{
frames.Add(new JsonObject()
{
["time"] = reader.ReadFloat(),
["position"] = reader.ReadFloat(),
});
}
break;
case SkeletonBinary.PATH_SPACING:
timeline["spacing"] = frames;
while (frameCount-- > 0)
{
var o = new JsonObject()
{
["time"] = reader.ReadFloat(),
["spacing"] = reader.ReadFloat(),
};
if (frameCount > 0) ReadCurve(o);
frames.Add(o);
}
break;
case SkeletonBinary.PATH_MIX:
timeline["mix"] = frames;
while (frameCount-- > 0)
{
var o = new JsonObject()
{
["time"] = reader.ReadFloat(),
["rotateMix"] = reader.ReadFloat(),
["translateMix"] = reader.ReadFloat(),
};
if (frameCount > 0) ReadCurve(o);
frames.Add(o);
}
break;
default:
throw new ArgumentException($"Invalid path timeline type: {type}");
}
}
}
return pathTimelines.Count > 0 ? pathTimelines : null;
}
private JsonObject? ReadDeformTimelines()
{
JsonArray slots = root["slots"].AsArray();
JsonArray skins = root["skins"].AsArray();
JsonObject deformTimelines = [];
//for (int skinCount = reader.ReadVarInt(); skinCount > 0; skinCount--)
for (int i = 0, n = reader.ReadVarInt(); i < n; i++)
{
JsonObject skinValue = [];
deformTimelines[skins[reader.ReadVarInt()]["name"].GetValue<string>()] = skinValue;
//for (int slotCount = reader.ReadVarInt(); slotCount > 0; slotCount--)
for (int ii = 0, nn = reader.ReadVarInt(); ii < nn; ii++)
{
JsonObject slotValue = [];
skinValue[slots[reader.ReadVarInt()]["name"].GetValue<string>()] = slotValue;
//for (int attachmentCount = reader.ReadVarInt(); attachmentCount > 0; attachmentCount--)
for (int iii = 0, nnn = reader.ReadVarInt(); iii < nnn; iii++)
{
JsonArray frames = [];
slotValue[reader.ReadStringRef()] = frames;
var frameCount = reader.ReadVarInt();
while (frameCount-- > 0)
{
var o = new JsonObject()
{
["time"] = reader.ReadFloat(),
};
var end = reader.ReadVarInt();
if (end > 0)
{
var start = reader.ReadVarInt();
o["offset"] = start;
o["vertices"] = ReadFloatArray(end);
}
if (frameCount > 0) ReadCurve(o);
frames.Add(o);
}
}
}
}
return deformTimelines.Count > 0 ? deformTimelines : null;
}
private JsonArray? ReadDrawOrderTimelines()
{
JsonArray slots = root["slots"].AsArray();
JsonArray drawOrderTimelines = [];
for (int drawOrderCount = reader.ReadVarInt(); drawOrderCount > 0; drawOrderCount--)
{
JsonObject data = new()
{
["time"] = reader.ReadFloat()
};
JsonArray offsets = [];
data["offsets"] = offsets;
for (int offsetCount = reader.ReadVarInt(); offsetCount > 0; offsetCount--)
{
offsets.Add(new JsonObject()
{
["slot"] = slots[reader.ReadVarInt()]["name"].GetValue<string>(),
["offset"] = reader.ReadVarInt(),
});
}
drawOrderTimelines.Add(data);
}
return drawOrderTimelines.Count > 0 ? drawOrderTimelines : null;
}
private JsonArray? ReadEventTimelines()
{
JsonArray eventTimelines = [];
for (int eventCount = reader.ReadVarInt(); eventCount > 0; eventCount--)
{
JsonObject data = [];
data["time"] = reader.ReadFloat();
JsonObject eventData = idx2Event[reader.ReadVarInt()].AsObject();
data["name"] = eventData["name"].GetValue<string>();
data["int"] = reader.ReadVarInt();
data["float"] = reader.ReadFloat();
data["string"] = reader.ReadBoolean() ? reader.ReadString() : eventData["string"].GetValue<string>();
if (eventData.ContainsKey("audio"))
{
data["volume"] = eventData["volume"].GetValue<string>();
data["balance"] = eventData["balance"].GetValue<string>();
}
eventTimelines.Add(data);
}
return eventTimelines.Count > 0 ? eventTimelines : null;
}
private JsonArray ReadNames(JsonArray array)
{
JsonArray names = [];
for (int n = reader.ReadVarInt(); n > 0; n--)
names.Add(array[reader.ReadVarInt()]["name"].GetValue<string>());
return names;
}
private JsonArray ReadFloatArray(int n)
{
JsonArray array = [];
while (n-- > 0)
array.Add(reader.ReadFloat());
return array;
}
private JsonArray ReadShortArray()
{
JsonArray array = [];
for (int n = reader.ReadVarInt(); n > 0; n--)
array.Add((reader.ReadByte() << 8) | reader.ReadByte());
return array;
}
private JsonArray ReadVertices(int vertexCount)
{
JsonArray vertices = [];
if (!reader.ReadBoolean())
return ReadFloatArray(vertexCount << 1);
for (int i = 0; i < vertexCount; i++)
{
int bonesCount = reader.ReadVarInt();
vertices.Add(bonesCount);
for (int j = 0; j < bonesCount; j++)
{
vertices.Add(reader.ReadVarInt());
vertices.Add(reader.ReadFloat());
vertices.Add(reader.ReadFloat());
vertices.Add(reader.ReadFloat());
}
}
return vertices;
}
private void ReadCurve(JsonObject obj)
{
var type = reader.ReadByte();
switch (type)
{
case SkeletonBinary.CURVE_LINEAR:
obj["curve"] = 1 / 3f;
obj["c2"] = 1 / 3f;
obj["c3"] = 2 / 3f;
obj["c4"] = 2 / 3f;
break;
case SkeletonBinary.CURVE_STEPPED:
obj["curve"] = "stepped";
break;
case SkeletonBinary.CURVE_BEZIER:
obj["curve"] = reader.ReadFloat();
obj["c2"] = reader.ReadFloat();
obj["c3"] = reader.ReadFloat();
obj["c4"] = reader.ReadFloat();
break;
default:
throw new ArgumentException($"Invalid curve type: {type}"); ;
}
}
protected override void WriteBinary(JsonObject root, string binPath)
{
throw new NotImplementedException();
}
//public void WriteFloatArray(float[] array)
//{
// foreach (var i in array)
// writer.WriteFloat(i);
//}
//public void WriteShortArray(int[] array)
//{
// foreach (var i in array)
// {
// writer.WriteByte((byte)(i >> 8));
// writer.WriteByte((byte)i);
// }
//}
}
}

View File

@@ -8,10 +8,10 @@ using System.Text;
using System.Threading.Tasks;
using SpineRuntime21;
namespace SpineViewer.Spine.Implementations
namespace SpineViewer.Spine.Implementations.Spine
{
[SpineImplementation(Version.V21)]
internal class Spine21 : Spine
internal class Spine21 : SpineViewer.Spine.Spine
{
private class TextureLoader : SpineRuntime21.TextureLoader
{
@@ -88,6 +88,8 @@ namespace SpineViewer.Spine.Implementations
atlas.Dispose();
}
public override string FileVersion { get => skeletonData.Version; }
public override float Scale
{
get
@@ -234,14 +236,14 @@ namespace SpineViewer.Spine.Implementations
skeleton.UpdateWorldTransform();
}
//private SFML.Graphics.BlendMode GetSFMLBlendMode(SpineRuntime21.BlendMode spineBlendMode)
//private SFML.Graphics.BlendMode GetSFMLBlendMode(BlendMode spineBlendMode)
//{
// return spineBlendMode switch
// {
// SpineRuntime21.BlendMode.Normal => BlendMode.Normal,
// SpineRuntime21.BlendMode.Additive => BlendMode.Additive,
// SpineRuntime21.BlendMode.Multiply => BlendMode.Multiply,
// SpineRuntime21.BlendMode.Screen => BlendMode.Screen,
// BlendMode.Normal => BlendMode.Normal,
// BlendMode.Additive => BlendMode.Additive,
// BlendMode.Multiply => BlendMode.Multiply,
// BlendMode.Screen => BlendMode.Screen,
// _ => throw new NotImplementedException($"{spineBlendMode}"),
// };
//}
@@ -311,14 +313,14 @@ namespace SpineViewer.Spine.Implementations
}
// 似乎 2.1.x 也没有 BlendMode
SFML.Graphics.BlendMode blendMode = slot.Data.AdditiveBlending ? BlendMode.Additive : BlendMode.Normal;
SFML.Graphics.BlendMode blendMode = slot.Data.AdditiveBlending ? BlendModeSFML.Additive : BlendModeSFML.Normal;
states.Texture ??= texture;
if (states.BlendMode != blendMode || states.Texture != texture)
{
if (vertexArray.VertexCount > 0)
{
if (UsePremultipliedAlpha && (states.BlendMode == BlendMode.Normal || states.BlendMode == BlendMode.Additive))
if (UsePremultipliedAlpha && (states.BlendMode == BlendModeSFML.Normal || states.BlendMode == BlendModeSFML.Additive))
states.Shader = FragmentShader;
else
states.Shader = null;
@@ -363,7 +365,7 @@ namespace SpineViewer.Spine.Implementations
//clipping.ClipEnd(slot);
}
if (UsePremultipliedAlpha && (states.BlendMode == BlendMode.Normal || states.BlendMode == BlendMode.Additive))
if (UsePremultipliedAlpha && (states.BlendMode == BlendModeSFML.Normal || states.BlendMode == BlendModeSFML.Additive))
states.Shader = FragmentShader;
else
states.Shader = null;

View File

@@ -8,10 +8,10 @@ using System.Text;
using System.Threading.Tasks;
using SpineRuntime36;
namespace SpineViewer.Spine.Implementations
namespace SpineViewer.Spine.Implementations.Spine
{
[SpineImplementation(Version.V36)]
internal class Spine36 : Spine
internal class Spine36 : SpineViewer.Spine.Spine
{
private class TextureLoader : SpineRuntime36.TextureLoader
{
@@ -87,6 +87,8 @@ namespace SpineViewer.Spine.Implementations
atlas.Dispose();
}
public override string FileVersion { get => skeletonData.Version; }
public override float Scale
{
get
@@ -192,14 +194,14 @@ namespace SpineViewer.Spine.Implementations
skeleton.UpdateWorldTransform();
}
private SFML.Graphics.BlendMode GetSFMLBlendMode(SpineRuntime36.BlendMode spineBlendMode)
private SFML.Graphics.BlendMode GetSFMLBlendMode(BlendMode spineBlendMode)
{
return spineBlendMode switch
{
SpineRuntime36.BlendMode.Normal => BlendMode.Normal,
SpineRuntime36.BlendMode.Additive => BlendMode.Additive,
SpineRuntime36.BlendMode.Multiply => BlendMode.Multiply,
SpineRuntime36.BlendMode.Screen => BlendMode.Screen,
BlendMode.Normal => BlendModeSFML.Normal,
BlendMode.Additive => BlendModeSFML.Additive,
BlendMode.Multiply => BlendModeSFML.Multiply,
BlendMode.Screen => BlendModeSFML.Screen,
_ => throw new NotImplementedException($"{spineBlendMode}"),
};
}
@@ -274,7 +276,7 @@ namespace SpineViewer.Spine.Implementations
{
if (vertexArray.VertexCount > 0)
{
if (UsePremultipliedAlpha && (states.BlendMode == BlendMode.Normal || states.BlendMode == BlendMode.Additive))
if (UsePremultipliedAlpha && (states.BlendMode == BlendModeSFML.Normal || states.BlendMode == BlendModeSFML.Additive))
states.Shader = FragmentShader;
else
states.Shader = null;
@@ -319,7 +321,7 @@ namespace SpineViewer.Spine.Implementations
clipping.ClipEnd(slot);
}
if (UsePremultipliedAlpha && (states.BlendMode == BlendMode.Normal || states.BlendMode == BlendMode.Additive))
if (UsePremultipliedAlpha && (states.BlendMode == BlendModeSFML.Normal || states.BlendMode == BlendModeSFML.Additive))
states.Shader = FragmentShader;
else
states.Shader = null;

View File

@@ -5,10 +5,10 @@ using System.Text;
using System.Threading.Tasks;
using SpineRuntime37;
namespace SpineViewer.Spine.Implementations
namespace SpineViewer.Spine.Implementations.Spine
{
[SpineImplementation(Version.V37)]
internal class Spine37 : Spine
internal class Spine37 : SpineViewer.Spine.Spine
{
private class TextureLoader : SpineRuntime37.TextureLoader
{
@@ -86,6 +86,8 @@ namespace SpineViewer.Spine.Implementations
atlas.Dispose();
}
public override string FileVersion { get => skeletonData.Version; }
public override float Scale
{
get
@@ -199,14 +201,14 @@ namespace SpineViewer.Spine.Implementations
skeleton.UpdateWorldTransform();
}
private SFML.Graphics.BlendMode GetSFMLBlendMode(SpineRuntime37.BlendMode spineBlendMode)
private SFML.Graphics.BlendMode GetSFMLBlendMode(BlendMode spineBlendMode)
{
return spineBlendMode switch
{
SpineRuntime37.BlendMode.Normal => BlendMode.Normal,
SpineRuntime37.BlendMode.Additive => BlendMode.Additive,
SpineRuntime37.BlendMode.Multiply => BlendMode.Multiply,
SpineRuntime37.BlendMode.Screen => BlendMode.Screen,
BlendMode.Normal => BlendModeSFML.Normal,
BlendMode.Additive => BlendModeSFML.Additive,
BlendMode.Multiply => BlendModeSFML.Multiply,
BlendMode.Screen => BlendModeSFML.Screen,
_ => throw new NotImplementedException($"{spineBlendMode}"),
};
}
@@ -282,7 +284,7 @@ namespace SpineViewer.Spine.Implementations
if (vertexArray.VertexCount > 0)
{
// XXX: 实测不用设置 sampler2D 的值也正确
if (UsePremultipliedAlpha && (states.BlendMode == BlendMode.Normal || states.BlendMode == BlendMode.Additive))
if (UsePremultipliedAlpha && (states.BlendMode == BlendModeSFML.Normal || states.BlendMode == BlendModeSFML.Additive))
states.Shader = FragmentShader;
else
states.Shader = null;
@@ -327,7 +329,7 @@ namespace SpineViewer.Spine.Implementations
clipping.ClipEnd(slot);
}
if (UsePremultipliedAlpha && (states.BlendMode == BlendMode.Normal || states.BlendMode == BlendMode.Additive))
if (UsePremultipliedAlpha && (states.BlendMode == BlendModeSFML.Normal || states.BlendMode == BlendModeSFML.Additive))
states.Shader = FragmentShader;
else
states.Shader = null;

View File

@@ -7,12 +7,11 @@ using System.Text;
using System.Threading.Tasks;
using SpineRuntime38;
using SpineRuntime38.Attachments;
using SpineViewer.Spine;
namespace SpineViewer.Spine.Implementations
namespace SpineViewer.Spine.Implementations.Spine
{
[SpineImplementation(Version.V38)]
internal class Spine38 : Spine
internal class Spine38 : SpineViewer.Spine.Spine
{
private class TextureLoader : SpineRuntime38.TextureLoader
{
@@ -90,6 +89,8 @@ namespace SpineViewer.Spine.Implementations
atlas.Dispose();
}
public override string FileVersion { get => skeletonData.Version; }
public override float Scale
{
get
@@ -203,14 +204,14 @@ namespace SpineViewer.Spine.Implementations
skeleton.UpdateWorldTransform();
}
private SFML.Graphics.BlendMode GetSFMLBlendMode(SpineRuntime38.BlendMode spineBlendMode)
private SFML.Graphics.BlendMode GetSFMLBlendMode(BlendMode spineBlendMode)
{
return spineBlendMode switch
{
SpineRuntime38.BlendMode.Normal => BlendMode.Normal,
SpineRuntime38.BlendMode.Additive => BlendMode.Additive,
SpineRuntime38.BlendMode.Multiply => BlendMode.Multiply,
SpineRuntime38.BlendMode.Screen => BlendMode.Screen,
BlendMode.Normal => BlendModeSFML.Normal,
BlendMode.Additive => BlendModeSFML.Additive,
BlendMode.Multiply => BlendModeSFML.Multiply,
BlendMode.Screen => BlendModeSFML.Screen,
_ => throw new NotImplementedException($"{spineBlendMode}"),
};
}
@@ -286,7 +287,7 @@ namespace SpineViewer.Spine.Implementations
if (vertexArray.VertexCount > 0)
{
// XXX: 实测不用设置 sampler2D 的值也正确
if (UsePremultipliedAlpha && (states.BlendMode == BlendMode.Normal || states.BlendMode == BlendMode.Additive))
if (UsePremultipliedAlpha && (states.BlendMode == BlendModeSFML.Normal || states.BlendMode == BlendModeSFML.Additive))
states.Shader = FragmentShader;
else
states.Shader = null;
@@ -331,7 +332,7 @@ namespace SpineViewer.Spine.Implementations
clipping.ClipEnd(slot);
}
if (UsePremultipliedAlpha && (states.BlendMode == BlendMode.Normal || states.BlendMode == BlendMode.Additive))
if (UsePremultipliedAlpha && (states.BlendMode == BlendModeSFML.Normal || states.BlendMode == BlendModeSFML.Additive))
states.Shader = FragmentShader;
else
states.Shader = null;

View File

@@ -7,10 +7,10 @@ using System.Text;
using System.Threading.Tasks;
using SpineRuntime40;
namespace SpineViewer.Spine.Implementations
namespace SpineViewer.Spine.Implementations.Spine
{
[SpineImplementation(Version.V40)]
internal class Spine40 : Spine
internal class Spine40 : SpineViewer.Spine.Spine
{
private class TextureLoader : SpineRuntime40.TextureLoader
{
@@ -88,6 +88,8 @@ namespace SpineViewer.Spine.Implementations
atlas.Dispose();
}
public override string FileVersion { get => skeletonData.Version; }
public override float Scale
{
get
@@ -201,14 +203,14 @@ namespace SpineViewer.Spine.Implementations
skeleton.UpdateWorldTransform();
}
private SFML.Graphics.BlendMode GetSFMLBlendMode(SpineRuntime40.BlendMode spineBlendMode)
private SFML.Graphics.BlendMode GetSFMLBlendMode(BlendMode spineBlendMode)
{
return spineBlendMode switch
{
SpineRuntime40.BlendMode.Normal => BlendMode.Normal,
SpineRuntime40.BlendMode.Additive => BlendMode.Additive,
SpineRuntime40.BlendMode.Multiply => BlendMode.Multiply,
SpineRuntime40.BlendMode.Screen => BlendMode.Screen,
BlendMode.Normal => BlendModeSFML.Normal,
BlendMode.Additive => BlendModeSFML.Additive,
BlendMode.Multiply => BlendModeSFML.Multiply,
BlendMode.Screen => BlendModeSFML.Screen,
_ => throw new NotImplementedException($"{spineBlendMode}"),
};
}
@@ -284,7 +286,7 @@ namespace SpineViewer.Spine.Implementations
if (vertexArray.VertexCount > 0)
{
// XXX: 实测不用设置 sampler2D 的值也正确
if (UsePremultipliedAlpha && (states.BlendMode == BlendMode.Normal || states.BlendMode == BlendMode.Additive))
if (UsePremultipliedAlpha && (states.BlendMode == BlendModeSFML.Normal || states.BlendMode == BlendModeSFML.Additive))
states.Shader = FragmentShader;
else
states.Shader = null;
@@ -329,7 +331,7 @@ namespace SpineViewer.Spine.Implementations
clipping.ClipEnd(slot);
}
if (UsePremultipliedAlpha && (states.BlendMode == BlendMode.Normal || states.BlendMode == BlendMode.Additive))
if (UsePremultipliedAlpha && (states.BlendMode == BlendModeSFML.Normal || states.BlendMode == BlendModeSFML.Additive))
states.Shader = FragmentShader;
else
states.Shader = null;

View File

@@ -7,10 +7,10 @@ using System.Text;
using System.Threading.Tasks;
using SpineRuntime41;
namespace SpineViewer.Spine.Implementations
namespace SpineViewer.Spine.Implementations.Spine
{
[SpineImplementation(Version.V41)]
internal class Spine41 : Spine
internal class Spine41 : SpineViewer.Spine.Spine
{
private class TextureLoader : SpineRuntime41.TextureLoader
{
@@ -88,6 +88,8 @@ namespace SpineViewer.Spine.Implementations
atlas.Dispose();
}
public override string FileVersion { get => skeletonData.Version; }
public override float Scale
{
get
@@ -201,14 +203,14 @@ namespace SpineViewer.Spine.Implementations
skeleton.UpdateWorldTransform();
}
private SFML.Graphics.BlendMode GetSFMLBlendMode(SpineRuntime41.BlendMode spineBlendMode)
private SFML.Graphics.BlendMode GetSFMLBlendMode(BlendMode spineBlendMode)
{
return spineBlendMode switch
{
SpineRuntime41.BlendMode.Normal => BlendMode.Normal,
SpineRuntime41.BlendMode.Additive => BlendMode.Additive,
SpineRuntime41.BlendMode.Multiply => BlendMode.Multiply,
SpineRuntime41.BlendMode.Screen => BlendMode.Screen,
BlendMode.Normal => BlendModeSFML.Normal,
BlendMode.Additive => BlendModeSFML.Additive,
BlendMode.Multiply => BlendModeSFML.Multiply,
BlendMode.Screen => BlendModeSFML.Screen,
_ => throw new NotImplementedException($"{spineBlendMode}"),
};
}
@@ -284,7 +286,7 @@ namespace SpineViewer.Spine.Implementations
if (vertexArray.VertexCount > 0)
{
// XXX: 实测不用设置 sampler2D 的值也正确
if (UsePremultipliedAlpha && (states.BlendMode == BlendMode.Normal || states.BlendMode == BlendMode.Additive))
if (UsePremultipliedAlpha && (states.BlendMode == BlendModeSFML.Normal || states.BlendMode == BlendModeSFML.Additive))
states.Shader = FragmentShader;
else
states.Shader = null;
@@ -329,7 +331,7 @@ namespace SpineViewer.Spine.Implementations
clipping.ClipEnd(slot);
}
if (UsePremultipliedAlpha && (states.BlendMode == BlendMode.Normal || states.BlendMode == BlendMode.Additive))
if (UsePremultipliedAlpha && (states.BlendMode == BlendModeSFML.Normal || states.BlendMode == BlendModeSFML.Additive))
states.Shader = FragmentShader;
else
states.Shader = null;

View File

@@ -7,10 +7,10 @@ using System.Text;
using System.Threading.Tasks;
using SpineRuntime42;
namespace SpineViewer.Spine.Implementations
namespace SpineViewer.Spine.Implementations.Spine
{
[SpineImplementation(Version.V42)]
internal class Spine42 : Spine
internal class Spine42 : SpineViewer.Spine.Spine
{
private class TextureLoader : SpineRuntime42.TextureLoader
{
@@ -88,6 +88,8 @@ namespace SpineViewer.Spine.Implementations
atlas.Dispose();
}
public override string FileVersion { get => skeletonData.Version; }
public override float Scale
{
get
@@ -201,14 +203,14 @@ namespace SpineViewer.Spine.Implementations
skeleton.UpdateWorldTransform(Skeleton.Physics.Update);
}
private SFML.Graphics.BlendMode GetSFMLBlendMode(SpineRuntime42.BlendMode spineBlendMode)
private SFML.Graphics.BlendMode GetSFMLBlendMode(BlendMode spineBlendMode)
{
return spineBlendMode switch
{
SpineRuntime42.BlendMode.Normal => BlendMode.Normal,
SpineRuntime42.BlendMode.Additive => BlendMode.Additive,
SpineRuntime42.BlendMode.Multiply => BlendMode.Multiply,
SpineRuntime42.BlendMode.Screen => BlendMode.Screen,
BlendMode.Normal => BlendModeSFML.Normal,
BlendMode.Additive => BlendModeSFML.Additive,
BlendMode.Multiply => BlendModeSFML.Multiply,
BlendMode.Screen => BlendModeSFML.Screen,
_ => throw new NotImplementedException($"{spineBlendMode}"),
};
}
@@ -284,7 +286,7 @@ namespace SpineViewer.Spine.Implementations
if (vertexArray.VertexCount > 0)
{
// XXX: 实测不用设置 sampler2D 的值也正确
if (UsePremultipliedAlpha && (states.BlendMode == BlendMode.Normal || states.BlendMode == BlendMode.Additive))
if (UsePremultipliedAlpha && (states.BlendMode == BlendModeSFML.Normal || states.BlendMode == BlendModeSFML.Additive))
states.Shader = FragmentShader;
else
states.Shader = null;
@@ -329,7 +331,7 @@ namespace SpineViewer.Spine.Implementations
clipping.ClipEnd(slot);
}
if (UsePremultipliedAlpha && (states.BlendMode == BlendMode.Normal || states.BlendMode == BlendMode.Additive))
if (UsePremultipliedAlpha && (states.BlendMode == BlendModeSFML.Normal || states.BlendMode == BlendModeSFML.Additive))
states.Shader = FragmentShader;
else
states.Shader = null;

View File

@@ -0,0 +1,344 @@
using Microsoft.VisualBasic;
using System;
using System.Buffers.Binary;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Runtime.InteropServices;
using System.Text;
using System.Text.Json;
using System.Text.Json.Nodes;
using System.Text.Encodings.Web;
namespace SpineViewer.Spine
{
/// <summary>
/// SkeletonConverter 实现类标记
/// </summary>
[AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = false)]
public class SkeletonConverterImplementationAttribute : Attribute
{
public Version Version { get; }
public SkeletonConverterImplementationAttribute(Version version)
{
Version = version;
}
}
/// <summary>
/// SkeletonConverter 基类, 使用静态方法 New 来创建具体版本对象
/// </summary>
public abstract class SkeletonConverter
{
/// <summary>
/// 实现类缓存
/// </summary>
private static readonly Dictionary<Version, Type> ImplementationTypes = [];
public static readonly Dictionary<Version, Type>.KeyCollection ImplementedVersions;
/// <summary>
/// 静态构造函数
/// </summary>
static SkeletonConverter()
{
// 遍历并缓存标记了 SkeletonConverterImplementationAttribute 的类型
var impTypes = Assembly.GetExecutingAssembly().GetTypes().Where(t => typeof(SkeletonConverter).IsAssignableFrom(t) && !t.IsAbstract);
foreach (var type in impTypes)
{
var attr = type.GetCustomAttribute<SkeletonConverterImplementationAttribute>();
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);
}
/// <summary>
/// Json 格式控制
/// </summary>
private static readonly JsonWriterOptions jsonWriterOptions = new()
{
Indented = true,
Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping
};
/// <summary>
/// 读取二进制骨骼文件并构造 Json 对象
/// </summary>
protected abstract JsonObject ReadBinary(string binPath);
/// <summary>
/// 将 Json 对象写入二进制骨骼文件
/// </summary>
protected abstract void WriteBinary(JsonObject root, string binPath);
/// <summary>
/// 读取 Json 对象
/// </summary>
private JsonObject ReadJson(string jsonPath)
{
using var input = File.OpenRead(jsonPath);
if (JsonNode.Parse(input) is JsonObject root)
return root;
else
throw new InvalidOperationException($"{jsonPath} is not a valid json object");
}
/// <summary>
/// 写入 Json 对象
/// </summary>
private void WriteJson(JsonObject root, string jsonPath)
{
using var output = File.Create(jsonPath);
using var writer = new Utf8JsonWriter(output, jsonWriterOptions);
root.WriteTo(writer);
}
/// <summary>
/// 二进制转 Json 格式
/// </summary>
public void BinaryToJson(string binPath, string jsonPath)
{
WriteJson(ReadBinary(binPath), jsonPath);
}
/// <summary>
/// Json 转二进制格式
/// </summary>
public void JsonToBinary(string jsonPath, string binPath)
{
WriteBinary(ReadJson(jsonPath), binPath);
}
protected class SkeletonReader
{
protected byte[] buffer = new byte[32];
protected byte[] bytesBigEndian = new byte[8];
public readonly List<string> StringTable = new(32);
protected Stream input;
public SkeletonReader(Stream input) { this.input = input; }
public int Read()
{
int val = input.ReadByte();
if (val == -1) throw new EndOfStreamException();
return val;
}
public byte ReadByte() => (byte)Read();
public byte ReadUByte() => (byte)Read();
public sbyte ReadSByte() => (sbyte)ReadByte();
public bool ReadBoolean() => Read() != 0;
public float ReadFloat()
{
if (input.Read(bytesBigEndian, 0, 4) < 4) throw new EndOfStreamException();
buffer[3] = bytesBigEndian[0];
buffer[2] = bytesBigEndian[1];
buffer[1] = bytesBigEndian[2];
buffer[0] = bytesBigEndian[3];
return BitConverter.ToSingle(buffer, 0);
}
public int ReadInt()
{
if (input.Read(bytesBigEndian, 0, 4) < 4) throw new EndOfStreamException();
return (bytesBigEndian[0] << 24)
| (bytesBigEndian[1] << 16)
| (bytesBigEndian[2] << 8)
| bytesBigEndian[3];
}
public long ReadLong()
{
if (input.Read(bytesBigEndian, 0, 8) < 8) throw new EndOfStreamException();
return ((long)(bytesBigEndian[0]) << 56)
| ((long)(bytesBigEndian[1]) << 48)
| ((long)(bytesBigEndian[2]) << 40)
| ((long)(bytesBigEndian[3]) << 32)
| ((long)(bytesBigEndian[4]) << 24)
| ((long)(bytesBigEndian[5]) << 16)
| ((long)(bytesBigEndian[6]) << 8)
| (long)(bytesBigEndian[7]);
}
public int ReadVarInt(bool optimizePositive = true)
{
byte b = ReadByte();
int val = b & 0x7F;
if ((b & 0x80) != 0)
{
b = ReadByte();
val |= (b & 0x7F) << 7;
if ((b & 0x80) != 0)
{
b = ReadByte();
val |= (b & 0x7F) << 14;
if ((b & 0x80) != 0)
{
b = ReadByte();
val |= (b & 0x7F) << 21;
if ((b & 0x80) != 0)
val |= (ReadByte() & 0x7F) << 28;
}
}
}
// 最低位是符号, 根据符号得到全 1 或全 0
// 无符号右移, 符号按原样设置在最高位, 其他位与符号异或
return optimizePositive ? val : (val >>> 1) ^ -(val & 1);
}
public string ReadString()
{
int byteCount = ReadVarInt();
switch (byteCount)
{
case 0: return null;
case 1: return "";
}
byteCount--;
if (buffer.Length < byteCount) buffer = new byte[byteCount];
ReadFully(buffer, 0, byteCount);
return System.Text.Encoding.UTF8.GetString(buffer, 0, byteCount);
}
public string ReadStringRef()
{
int index = ReadVarInt();
return index == 0 ? null : StringTable[index - 1];
}
public void ReadFully(byte[] buffer, int offset, int length)
{
while (length > 0)
{
int count = input.Read(buffer, offset, length);
if (count <= 0) throw new EndOfStreamException();
offset += count;
length -= count;
}
}
}
protected class SkeletonWriter
{
protected byte[] buffer = new byte[32];
protected byte[] bytesBigEndian = new byte[8];
public readonly List<string> Strings = new(32);
protected Stream output;
public SkeletonWriter(Stream output) { this.output = output; }
public void Write(int val) => output.WriteByte((byte)val);
public void WriteByte(byte val) => output.WriteByte(val);
public void WriteUByte(byte val) => output.WriteByte(val);
public void WriteSByte(sbyte val) => output.WriteByte((byte)val);
public void WriteBoolean(bool val) => output.WriteByte((byte)(val ? 1 : 0));
public void WriteFloat(float val)
{
uint v = BitConverter.SingleToUInt32Bits(val);
bytesBigEndian[0] = (byte)(v >> 24);
bytesBigEndian[1] = (byte)(v >> 16);
bytesBigEndian[2] = (byte)(v >> 8);
bytesBigEndian[3] = (byte)v;
output.Write(bytesBigEndian, 0, 4);
}
public void WriteInt(int val)
{
bytesBigEndian[0] = (byte)(val >> 24);
bytesBigEndian[1] = (byte)(val >> 16);
bytesBigEndian[2] = (byte)(val >> 8);
bytesBigEndian[3] = (byte)val;
output.Write(bytesBigEndian, 0, 4);
}
public void WriteLong(long val)
{
bytesBigEndian[0] = (byte)(val >> 56);
bytesBigEndian[1] = (byte)(val >> 48);
bytesBigEndian[2] = (byte)(val >> 40);
bytesBigEndian[3] = (byte)(val >> 32);
bytesBigEndian[4] = (byte)(val >> 24);
bytesBigEndian[5] = (byte)(val >> 16);
bytesBigEndian[6] = (byte)(val >> 8);
bytesBigEndian[7] = (byte)val;
output.Write(bytesBigEndian, 0, 8);
}
public void WriteVarInt(int val, bool optimizePositive = true)
{
// 有符号右移, 会变成全 1 或者全 0 符号
// 其他位与符号异或, 符号按原样设置在最低位
if (!optimizePositive) val = (val << 1) ^ (val >> 31);
byte b = (byte)(val & 0x7F);
val >>>= 7;
if (val != 0)
{
output.WriteByte((byte)(b | 0x80));
b = (byte)(val & 0x7F);
val >>>= 7;
if (val != 0)
{
output.WriteByte((byte)(b | 0x80));
b = (byte)(val & 0x7F);
val >>>= 7;
if (val != 0)
{
output.WriteByte((byte)(b | 0x80));
b = (byte)(val & 0x7F);
val >>>= 7;
if (val != 0)
{
output.WriteByte((byte)(b | 0x80));
b = (byte)(val & 0x7F);
}
}
}
}
output.WriteByte(b);
}
public void WriteString(string val)
{
if (val == null)
{
WriteVarInt(0);
return;
}
if (val.Length == 0)
{
WriteVarInt(1);
return;
}
int byteCount = System.Text.Encoding.UTF8.GetByteCount(val);
WriteVarInt(byteCount + 1);
if (buffer.Length < byteCount) buffer = new byte[byteCount];
System.Text.Encoding.UTF8.GetBytes(val, 0, val.Length, buffer, 0);
WriteFully(buffer, 0, byteCount);
}
public void WriteStringRef(List<string> strings, string val)
{
if (val is null)
{
WriteVarInt(0);
return;
}
int index = strings.IndexOf(val);
if (index < 0)
{
strings.Add(val);
index = strings.Count - 1;
}
WriteVarInt(index + 1);
}
public void WriteFully(byte[] buffer, int offset, int length) => output.Write(buffer, offset, length);
}
}
}

View File

@@ -17,8 +17,6 @@ using System.Globalization;
namespace SpineViewer.Spine
{
/// <summary>
/// Spine 实现类标记
/// </summary>
@@ -42,6 +40,7 @@ namespace SpineViewer.Spine
/// 实现类缓存
/// </summary>
private static readonly Dictionary<Version, Type> ImplementationTypes = [];
public static readonly Dictionary<Version, Type>.KeyCollection ImplementedVersions;
/// <summary>
/// 用于解决 PMA 和渐变动画问题的片段着色器
@@ -70,10 +69,13 @@ namespace SpineViewer.Spine
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
@@ -136,8 +138,8 @@ namespace SpineViewer.Spine
/// <summary>
/// 获取所属版本
/// </summary>
[TypeConverter(typeof(VersionTypeConverter))]
[Category("基本信息"), DisplayName("版本")]
[TypeConverter(typeof(VersionConverter))]
[Category("基本信息"), DisplayName("运行时版本")]
public Version Version { get; }
/// <summary>
@@ -155,6 +157,12 @@ namespace SpineViewer.Spine
[Category("基本信息"), DisplayName("名称")]
public string Name { get; }
/// <summary>
/// 获取所属文件版本
/// </summary>
[Category("基本信息"), DisplayName("文件版本")]
public abstract string FileVersion { get; }
/// <summary>
/// 缩放比例
/// </summary>
@@ -164,7 +172,7 @@ namespace SpineViewer.Spine
/// <summary>
/// 位置
/// </summary>
[TypeConverter(typeof(PointFTypeConverter))]
[TypeConverter(typeof(PointFConverter))]
[Category("变换"), DisplayName("位置")]
public abstract PointF Position { get; set; }
@@ -202,7 +210,7 @@ namespace SpineViewer.Spine
/// <summary>
/// 当前动画名称
/// </summary>
[TypeConverter(typeof(AnimationTypeConverter))]
[TypeConverter(typeof(AnimationConverter))]
[Category("动画"), DisplayName("当前动画")]
public abstract string CurrentAnimation { get; set; }
@@ -243,5 +251,11 @@ namespace SpineViewer.Spine
/// SFML.Graphics.Drawable 接口实现
/// </summary>
public abstract void Draw(SFML.Graphics.RenderTarget target, SFML.Graphics.RenderStates states);
/// <summary>
/// 是否被选中
/// </summary>
[Browsable(false)]
public bool IsSelected { get; set; } = false;
}
}

View File

@@ -9,9 +9,9 @@ using System.Threading.Tasks;
namespace SpineViewer.Spine
{
public class VersionTypeConverter : EnumConverter
public class VersionConverter : EnumConverter
{
public VersionTypeConverter() : base(typeof(Version)) { }
public VersionConverter() : base(typeof(Version)) { }
public override object? ConvertTo(ITypeDescriptorContext? context, CultureInfo? culture, object? value, Type? destinationType)
{
@@ -25,7 +25,7 @@ namespace SpineViewer.Spine
}
}
public class AnimationTypeConverter : StringConverter
public class AnimationConverter : StringConverter
{
public override bool GetStandardValuesSupported(ITypeDescriptorContext? context)
{

View File

@@ -40,13 +40,13 @@ namespace SpineViewer.Spine
/// </summary>
public enum Version
{
[Description("v2.1.x")] V21 = 0x0201,
[Description("v3.6.x")] V36 = 0x0306,
[Description("v3.7.x")] V37 = 0x0307,
[Description("v3.8.x")] V38 = 0x0308,
[Description("v4.0.x")] V40 = 0x0400,
[Description("v4.1.x")] V41 = 0x0401,
[Description("v4.2.x")] V42 = 0x0402,
[Description("v4.3.x")] V43 = 0x0403,
[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,
}
}

View File

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

View File

@@ -9,7 +9,7 @@ using System.Threading.Tasks;
namespace SpineViewer
{
public class PointFTypeConverter : ExpandableObjectConverter
public class PointFConverter : ExpandableObjectConverter
{
public override bool CanConvertTo(ITypeDescriptorContext? context, [NotNullWhen(true)] Type? destinationType)
{
@@ -52,48 +52,4 @@ namespace SpineViewer
public override bool GetPropertiesSupported(ITypeDescriptorContext? context) => true;
}
public class SizeTypeConverter : ExpandableObjectConverter
{
public override bool CanConvertTo(ITypeDescriptorContext? context, [NotNullWhen(true)] Type? destinationType)
{
return destinationType == typeof(string) || base.CanConvertTo(context, destinationType);
}
public override object? ConvertTo(ITypeDescriptorContext? context, CultureInfo? culture, object? value, Type destinationType)
{
if (destinationType == typeof(string) && value is Size size)
{
return $"{size.Width}, {size.Height}";
}
return base.ConvertTo(context, culture, value, destinationType);
}
public override bool CanConvertFrom(ITypeDescriptorContext? context, Type sourceType)
{
return sourceType == typeof(string) || base.CanConvertFrom(context, sourceType);
}
public override object? ConvertFrom(ITypeDescriptorContext? context, CultureInfo? culture, object value)
{
if (value is string str)
{
var parts = str.Split(',');
if (parts.Length == 2 &&
int.TryParse(parts[0], out var width) &&
int.TryParse(parts[1], out var height))
{
return new Size(width, height);
}
}
return base.ConvertFrom(context, culture, value);
}
public override PropertyDescriptorCollection GetProperties(ITypeDescriptorContext? context, object value, Attribute[]? attributes)
{
return TypeDescriptor.GetProperties(typeof(Size), attributes);
}
public override bool GetPropertiesSupported(ITypeDescriptorContext? context) => true;
}
}