Compare commits

...

33 Commits

Author SHA1 Message Date
ww-rm
775268c01a 修复包围盒并集错误 2025-04-17 20:29:20 +08:00
ww-rm
b0b1c85047 更新注释和文本描述 2025-04-17 20:10:59 +08:00
ww-rm
5f08fc6695 更新至v0.12.4 2025-04-17 20:06:52 +08:00
ww-rm
2de3bdf12b 同步重命名 2025-04-17 20:06:38 +08:00
ww-rm
3a424c7dc1 update readme 2025-04-17 20:04:06 +08:00
ww-rm
c3e2b37072 update changelog 2025-04-17 20:03:46 +08:00
ww-rm
65bd11a346 增加自动分辨率 2025-04-17 20:00:15 +08:00
ww-rm
e6e7fc539f 修复Union错误 2025-04-17 19:58:14 +08:00
ww-rm
6522d415b7 增加AllowContentOverflow参数 2025-04-17 16:31:29 +08:00
ww-rm
378c66a333 small change 2025-04-17 16:30:45 +08:00
ww-rm
07204417a5 增加SetViewport 2025-04-17 16:30:27 +08:00
ww-rm
c9c909cdf9 增加padding和margin参数 2025-04-17 00:09:04 +08:00
ww-rm
a9f59a4d2f 增加对话框高度 2025-04-16 22:35:35 +08:00
ww-rm
1d2513cef5 增加padding和margin参数 2025-04-16 22:28:35 +08:00
ww-rm
febb797ae2 增加GetResolutionBounds方法 2025-04-16 21:37:09 +08:00
ww-rm
68d279a7c3 修改缩放公式 2025-04-16 21:25:26 +08:00
ww-rm
d2d8b7955c 增加GetBounds获取最大包围盒方法 2025-04-15 20:20:18 +08:00
ww-rm
2a55fd9c36 补充3.7及以下版本的多皮肤功能 2025-04-15 20:19:32 +08:00
ww-rm
695d3c0735 增加Attachments公开属性 2025-04-15 20:16:10 +08:00
ww-rm
ce95db469b 增加GetBounds 2025-04-15 20:15:50 +08:00
ww-rm
5d187cf80f 修复Path读取错误 2025-04-15 17:47:37 +08:00
ww-rm
e704ebc224 修正eventTimelines和原逻辑不一致的地方 2025-04-15 15:49:38 +08:00
ww-rm
ee36f8981c 修改GetBounds为GetCurrentBounds 2025-04-15 14:58:25 +08:00
ww-rm
09dd220abf 更改Bounds属性为GetBounds方法 2025-04-15 11:23:11 +08:00
ww-rm
15bc2dc3b8 完善文件转换功能 2025-04-14 23:52:39 +08:00
ww-rm
1deb74eca9 修正某些可能的字符大小写问题 2025-04-14 23:52:05 +08:00
ww-rm
de76ce64ab 增加输出文件夹选项 2025-04-14 23:50:46 +08:00
ww-rm
94b4ba33e6 修正curve读写 2025-04-14 21:48:23 +08:00
ww-rm
7ce8a115f4 修复流写入错误 2025-04-14 20:19:06 +08:00
ww-rm
c036a4bb45 增加v38二进制文件输出 2025-04-14 17:51:16 +08:00
ww-rm
aa62f30b05 增加报错输出 2025-04-14 17:11:56 +08:00
ww-rm
3d967c9812 修改默认打开骨骼文件后缀筛选器 2025-04-13 13:50:31 +08:00
ww-rm
e87e9efb99 补充点附件TODO 2025-04-13 00:37:29 +08:00
25 changed files with 1455 additions and 441 deletions

View File

@@ -1,5 +1,15 @@
# CHANGELOG
## v0.12.4
- 增加导出自动分辨率参数
- 增加导出边缘和填充参数
- 增加导出内容溢出参数
- 支持3.7及以下版本多皮肤功能
- 增加3.8版本的骨骼文件二进制和文本格式互转
- 增加格式转换输出文件夹参数
- 修改打开对话框的默认文件后缀筛选为所有类型
## v0.12.3
- 增加按住 ctrl 缩放选中模型

View File

@@ -10,12 +10,6 @@
![previewer](img/preview.webp)
---
:sparkles: v0.12.x 新增功能: 支持多轨道动画以及多皮肤列表管理 :sparkles:
---
## 安装
前往 [Release](https://github.com/ww-rm/SpineViewer/releases) 界面下载压缩包.

View File

@@ -302,5 +302,50 @@ namespace SpineRuntime21 {
public void Update (float delta) {
time += delta;
}
}
public void GetBounds(out float x, out float y, out float width, out float height)
{
float[] temp = new float[8];
float minX = int.MaxValue, minY = int.MaxValue, maxX = int.MinValue, maxY = int.MinValue;
for (int i = 0, n = drawOrder.Count; i < n; i++)
{
Slot slot = drawOrder[i];
int verticesLength = 0;
float[] vertices = null;
Attachment attachment = slot.Attachment;
if (attachment is RegionAttachment regionAttachment)
{
verticesLength = 8;
vertices = temp;
if (vertices.Length < 8) vertices = temp = new float[8];
regionAttachment.ComputeWorldVertices(slot.Bone, temp);
}
else if (attachment is MeshAttachment meshAttachment)
{
MeshAttachment mesh = meshAttachment;
verticesLength = mesh.Vertices.Length;
vertices = temp;
if (vertices.Length < verticesLength) vertices = temp = new float[verticesLength];
mesh.ComputeWorldVertices(slot, temp);
}
if (vertices != null)
{
for (int ii = 0; ii < verticesLength; ii += 2)
{
float vx = vertices[ii], vy = vertices[ii + 1];
minX = Math.Min(minX, vx);
minY = Math.Min(minY, vy);
maxX = Math.Max(maxX, vx);
maxY = Math.Max(maxY, vy);
}
}
}
x = minX;
y = minY;
width = maxX - minX;
height = maxY - minY;
}
}
}

View File

@@ -39,8 +39,9 @@ namespace SpineRuntime21 {
new Dictionary<KeyValuePair<int, String>, Attachment>(AttachmentComparer.Instance);
public String Name { get { return name; } }
public Dictionary<KeyValuePair<int, String>, Attachment> Attachments { get { return attachments; } }
public Skin (String name) {
public Skin (String name) {
if (name == null) throw new ArgumentNullException("name cannot be null.");
this.name = name;
}

View File

@@ -160,7 +160,7 @@
//
openFileDialog_Skel.AddExtension = false;
openFileDialog_Skel.AddToRecent = false;
openFileDialog_Skel.Filter = "skel 文件 (*.skel; *.json)|*.skel;*.json|二进制文件 (*.skel)|*.skel|文本文件 (*.json)|*.json|所有文件 (*.*)|*.*";
openFileDialog_Skel.Filter = "所有文件 (*.*)|*.*|skel 文件 (*.skel; *.json)|*.skel;*.json";
openFileDialog_Skel.Multiselect = true;
openFileDialog_Skel.Title = "批量选择skel文件";
//

View File

@@ -423,27 +423,17 @@ namespace SpineViewer.Controls
if (renderWindow is null)
return;
float parentX = panel_Render.Parent.Width;
float parentY = panel_Render.Parent.Height;
float sizeX = panel_Render.Width;
float sizeY = panel_Render.Height;
if ((sizeY / sizeX) < (parentY / parentX))
{
// 相同的 X, 子窗口 Y 更小
sizeY = parentX * sizeY / sizeX;
sizeX = parentX;
}
else
{
// 相同的 Y, 子窗口 X 更小
sizeX = parentY * sizeX / sizeY;
sizeY = parentY;
}
float parentW = panel_Render.Parent.Width;
float parentH = panel_Render.Parent.Height;
float renderW = panel_Render.Width;
float renderH = panel_Render.Height;
float scale = Math.Min(parentW / renderW, parentH / renderH); // 两方向取较小值, 保证 parent 覆盖 render
renderH *= scale;
renderW *= scale;
// 必须通过 SFML 的方法调整窗口
renderWindow.Position = new((int)(parentX - sizeX) / 2, (int)(parentY - sizeY) / 2);
renderWindow.Size = new((uint)sizeX, (uint)sizeY);
renderWindow.Position = new((int)(parentW - renderW) / 2, (int)(parentH - renderH) / 2);
renderWindow.Size = new((uint)renderW, (uint)renderH);
}
private void panel_Render_MouseDown(object sender, MouseEventArgs e)
@@ -475,7 +465,7 @@ namespace SpineViewer.Controls
foreach (int i in SpineListView.SelectedIndices)
{
if (spines[i].IsHidden) continue;
if (!spines[i].Bounds.Contains(src)) continue;
if (!spines[i].GetCurrentBounds().Contains(src)) continue;
hit = true;
break;
}
@@ -492,7 +482,7 @@ namespace SpineViewer.Controls
for (int i = 0; i < spines.Count; i++)
{
if (spines[i].IsHidden) continue;
if (!spines[i].Bounds.Contains(src)) continue;
if (!spines[i].GetCurrentBounds().Contains(src)) continue;
hit = true;
@@ -514,7 +504,7 @@ namespace SpineViewer.Controls
for (int i = 0; i < spines.Count; i++)
{
if (spines[i].IsHidden) continue;
if (!spines[i].Bounds.Contains(src)) continue;
if (!spines[i].GetCurrentBounds().Contains(src)) continue;
SpineListView.SelectedIndices.Add(i);
break;

View File

@@ -31,6 +31,7 @@
System.ComponentModel.ComponentResourceManager resources = new System.ComponentModel.ComponentResourceManager(typeof(ConvertFileFormatDialog));
panel = new Panel();
tableLayoutPanel1 = new TableLayoutPanel();
label5 = new Label();
comboBox_TargetVersion = new ComboBox();
flowLayoutPanel_TargetFormat = new FlowLayoutPanel();
radioButton_BinaryTarget = new RadioButton();
@@ -44,10 +45,15 @@
button_Cancel = new Button();
label2 = new Label();
skelFileListBox = new SpineViewer.Controls.SkelFileListBox();
tableLayoutPanel3 = new TableLayoutPanel();
textBox_OutputDir = new TextBox();
button_SelectOutputDir = new Button();
folderBrowserDialog_Output = new FolderBrowserDialog();
panel.SuspendLayout();
tableLayoutPanel1.SuspendLayout();
flowLayoutPanel_TargetFormat.SuspendLayout();
tableLayoutPanel2.SuspendLayout();
tableLayoutPanel3.SuspendLayout();
SuspendLayout();
//
// panel
@@ -57,7 +63,7 @@
panel.Location = new Point(0, 0);
panel.Name = "panel";
panel.Padding = new Padding(50, 15, 50, 10);
panel.Size = new Size(1051, 538);
panel.Size = new Size(1051, 702);
panel.TabIndex = 2;
//
// tableLayoutPanel1
@@ -65,34 +71,48 @@
tableLayoutPanel1.ColumnCount = 2;
tableLayoutPanel1.ColumnStyles.Add(new ColumnStyle());
tableLayoutPanel1.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 100F));
tableLayoutPanel1.Controls.Add(comboBox_TargetVersion, 1, 3);
tableLayoutPanel1.Controls.Add(flowLayoutPanel_TargetFormat, 1, 4);
tableLayoutPanel1.Controls.Add(label1, 0, 3);
tableLayoutPanel1.Controls.Add(label5, 0, 2);
tableLayoutPanel1.Controls.Add(comboBox_TargetVersion, 1, 4);
tableLayoutPanel1.Controls.Add(flowLayoutPanel_TargetFormat, 1, 5);
tableLayoutPanel1.Controls.Add(label1, 0, 4);
tableLayoutPanel1.Controls.Add(label4, 0, 0);
tableLayoutPanel1.Controls.Add(label3, 0, 2);
tableLayoutPanel1.Controls.Add(comboBox_SourceVersion, 1, 2);
tableLayoutPanel1.Controls.Add(tableLayoutPanel2, 0, 5);
tableLayoutPanel1.Controls.Add(label2, 0, 4);
tableLayoutPanel1.Controls.Add(label3, 0, 3);
tableLayoutPanel1.Controls.Add(comboBox_SourceVersion, 1, 3);
tableLayoutPanel1.Controls.Add(tableLayoutPanel2, 0, 6);
tableLayoutPanel1.Controls.Add(label2, 0, 5);
tableLayoutPanel1.Controls.Add(skelFileListBox, 0, 1);
tableLayoutPanel1.Controls.Add(tableLayoutPanel3, 1, 2);
tableLayoutPanel1.Dock = DockStyle.Fill;
tableLayoutPanel1.Location = new Point(50, 15);
tableLayoutPanel1.Name = "tableLayoutPanel1";
tableLayoutPanel1.RowCount = 6;
tableLayoutPanel1.RowCount = 7;
tableLayoutPanel1.RowStyles.Add(new RowStyle());
tableLayoutPanel1.RowStyles.Add(new RowStyle(SizeType.Percent, 100F));
tableLayoutPanel1.RowStyles.Add(new RowStyle());
tableLayoutPanel1.RowStyles.Add(new RowStyle());
tableLayoutPanel1.RowStyles.Add(new RowStyle());
tableLayoutPanel1.RowStyles.Add(new RowStyle());
tableLayoutPanel1.Size = new Size(951, 513);
tableLayoutPanel1.RowStyles.Add(new RowStyle());
tableLayoutPanel1.RowStyles.Add(new RowStyle(SizeType.Absolute, 20F));
tableLayoutPanel1.Size = new Size(951, 677);
tableLayoutPanel1.TabIndex = 1;
//
// label5
//
label5.Anchor = AnchorStyles.Left | AnchorStyles.Right;
label5.AutoSize = true;
label5.Location = new Point(3, 462);
label5.Name = "label5";
label5.Size = new Size(104, 24);
label5.TabIndex = 23;
label5.Text = "输出文件夹:";
//
// comboBox_TargetVersion
//
comboBox_TargetVersion.Anchor = AnchorStyles.Left;
comboBox_TargetVersion.DropDownStyle = ComboBoxStyle.DropDownList;
comboBox_TargetVersion.FormattingEnabled = true;
comboBox_TargetVersion.Location = new Point(95, 365);
comboBox_TargetVersion.Location = new Point(113, 535);
comboBox_TargetVersion.Name = "comboBox_TargetVersion";
comboBox_TargetVersion.Size = new Size(182, 32);
comboBox_TargetVersion.Sorted = true;
@@ -104,9 +124,10 @@
flowLayoutPanel_TargetFormat.Controls.Add(radioButton_BinaryTarget);
flowLayoutPanel_TargetFormat.Controls.Add(radioButton_JsonTarget);
flowLayoutPanel_TargetFormat.Dock = DockStyle.Fill;
flowLayoutPanel_TargetFormat.Location = new Point(95, 403);
flowLayoutPanel_TargetFormat.Location = new Point(110, 570);
flowLayoutPanel_TargetFormat.Margin = new Padding(0);
flowLayoutPanel_TargetFormat.Name = "flowLayoutPanel_TargetFormat";
flowLayoutPanel_TargetFormat.Size = new Size(853, 34);
flowLayoutPanel_TargetFormat.Size = new Size(841, 34);
flowLayoutPanel_TargetFormat.TabIndex = 19;
//
// radioButton_BinaryTarget
@@ -135,7 +156,7 @@
//
label1.Anchor = AnchorStyles.Right;
label1.AutoSize = true;
label1.Location = new Point(3, 369);
label1.Location = new Point(21, 539);
label1.Name = "label1";
label1.Size = new Size(86, 24);
label1.TabIndex = 15;
@@ -151,14 +172,14 @@
label4.Name = "label4";
label4.Size = new Size(921, 24);
label4.TabIndex = 14;
label4.Text = "说明:在每个文件同级目录下生成目标格式后缀的文件,会覆盖已存在文件";
label4.Text = "说明:输出文件夹留空则在每个文件同级目录下生成目标格式后缀的文件,视情况会覆盖已存在文件";
label4.TextAlign = ContentAlignment.MiddleCenter;
//
// label3
//
label3.Anchor = AnchorStyles.Right;
label3.AutoSize = true;
label3.Location = new Point(21, 331);
label3.Location = new Point(39, 501);
label3.Name = "label3";
label3.Size = new Size(68, 24);
label3.TabIndex = 12;
@@ -169,7 +190,7 @@
comboBox_SourceVersion.Anchor = AnchorStyles.Left;
comboBox_SourceVersion.DropDownStyle = ComboBoxStyle.DropDownList;
comboBox_SourceVersion.FormattingEnabled = true;
comboBox_SourceVersion.Location = new Point(95, 327);
comboBox_SourceVersion.Location = new Point(113, 497);
comboBox_SourceVersion.Name = "comboBox_SourceVersion";
comboBox_SourceVersion.Size = new Size(182, 32);
comboBox_SourceVersion.Sorted = true;
@@ -186,7 +207,7 @@
tableLayoutPanel2.Controls.Add(button_Ok, 0, 0);
tableLayoutPanel2.Controls.Add(button_Cancel, 1, 0);
tableLayoutPanel2.Dock = DockStyle.Fill;
tableLayoutPanel2.Location = new Point(3, 470);
tableLayoutPanel2.Location = new Point(3, 634);
tableLayoutPanel2.Margin = new Padding(3, 30, 3, 3);
tableLayoutPanel2.Name = "tableLayoutPanel2";
tableLayoutPanel2.RowCount = 1;
@@ -222,7 +243,7 @@
//
label2.Anchor = AnchorStyles.Right;
label2.AutoSize = true;
label2.Location = new Point(3, 408);
label2.Location = new Point(21, 575);
label2.Name = "label2";
label2.Size = new Size(86, 24);
label2.TabIndex = 16;
@@ -234,16 +255,56 @@
skelFileListBox.Dock = DockStyle.Fill;
skelFileListBox.Location = new Point(3, 57);
skelFileListBox.Name = "skelFileListBox";
skelFileListBox.Size = new Size(945, 264);
skelFileListBox.Size = new Size(945, 394);
skelFileListBox.TabIndex = 20;
//
// tableLayoutPanel3
//
tableLayoutPanel3.AutoSize = true;
tableLayoutPanel3.AutoSizeMode = AutoSizeMode.GrowAndShrink;
tableLayoutPanel3.ColumnCount = 3;
tableLayoutPanel3.ColumnStyles.Add(new ColumnStyle());
tableLayoutPanel3.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 100F));
tableLayoutPanel3.ColumnStyles.Add(new ColumnStyle());
tableLayoutPanel3.Controls.Add(textBox_OutputDir, 1, 0);
tableLayoutPanel3.Controls.Add(button_SelectOutputDir, 2, 0);
tableLayoutPanel3.Dock = DockStyle.Fill;
tableLayoutPanel3.Location = new Point(110, 454);
tableLayoutPanel3.Margin = new Padding(0);
tableLayoutPanel3.Name = "tableLayoutPanel3";
tableLayoutPanel3.RowCount = 1;
tableLayoutPanel3.RowStyles.Add(new RowStyle(SizeType.Percent, 100F));
tableLayoutPanel3.Size = new Size(841, 40);
tableLayoutPanel3.TabIndex = 22;
//
// textBox_OutputDir
//
textBox_OutputDir.Anchor = AnchorStyles.Left | AnchorStyles.Right;
textBox_OutputDir.Location = new Point(3, 5);
textBox_OutputDir.Name = "textBox_OutputDir";
textBox_OutputDir.Size = new Size(797, 30);
textBox_OutputDir.TabIndex = 1;
//
// button_SelectOutputDir
//
button_SelectOutputDir.Anchor = AnchorStyles.Left | AnchorStyles.Right;
button_SelectOutputDir.AutoSize = true;
button_SelectOutputDir.AutoSizeMode = AutoSizeMode.GrowAndShrink;
button_SelectOutputDir.Location = new Point(806, 3);
button_SelectOutputDir.Name = "button_SelectOutputDir";
button_SelectOutputDir.Size = new Size(32, 34);
button_SelectOutputDir.TabIndex = 2;
button_SelectOutputDir.Text = "...";
button_SelectOutputDir.UseVisualStyleBackColor = true;
button_SelectOutputDir.Click += button_SelectOutputDir_Click;
//
// ConvertFileFormatDialog
//
AcceptButton = button_Ok;
AutoScaleDimensions = new SizeF(11F, 24F);
AutoScaleMode = AutoScaleMode.Font;
CancelButton = button_Cancel;
ClientSize = new Size(1051, 538);
ClientSize = new Size(1051, 702);
Controls.Add(panel);
FormBorderStyle = FormBorderStyle.FixedDialog;
Icon = (Icon)resources.GetObject("$this.Icon");
@@ -259,6 +320,8 @@
flowLayoutPanel_TargetFormat.ResumeLayout(false);
flowLayoutPanel_TargetFormat.PerformLayout();
tableLayoutPanel2.ResumeLayout(false);
tableLayoutPanel3.ResumeLayout(false);
tableLayoutPanel3.PerformLayout();
ResumeLayout(false);
}
@@ -279,5 +342,10 @@
private RadioButton radioButton_JsonTarget;
private Controls.SkelFileListBox skelFileListBox;
private ComboBox comboBox_TargetVersion;
private FolderBrowserDialog folderBrowserDialog_Output;
private TableLayoutPanel tableLayoutPanel3;
private TextBox textBox_OutputDir;
private Button button_SelectOutputDir;
private Label label5;
}
}

View File

@@ -1,4 +1,5 @@
using SpineViewer.Spine;
using NLog;
using SpineViewer.Spine;
using SpineViewer.Utils;
using System;
using System.Collections.Generic;
@@ -14,6 +15,8 @@ namespace SpineViewer.Dialogs
{
public partial class ConvertFileFormatDialog : Form
{
private readonly Logger logger = LogManager.GetCurrentClassLogger();
/// <summary>
/// 对话框结果, 取消时为 null
/// </summary>
@@ -37,8 +40,17 @@ namespace SpineViewer.Dialogs
comboBox_TargetVersion.SelectedValue = SpineVersion.V38;
}
private void button_SelectOutputDir_Click(object sender, EventArgs e)
{
if (folderBrowserDialog_Output.ShowDialog() != DialogResult.OK)
return;
textBox_OutputDir.Text = Path.GetFullPath(folderBrowserDialog_Output.SelectedPath);
}
private void button_Ok_Click(object sender, EventArgs e)
{
var outputDir = textBox_OutputDir.Text;
var sourceVersion = (SpineVersion)comboBox_SourceVersion.SelectedValue;
var targetVersion = (SpineVersion)comboBox_TargetVersion.SelectedValue;
var jsonTarget = radioButton_JsonTarget.Checked;
@@ -51,6 +63,36 @@ namespace SpineViewer.Dialogs
return;
}
if (string.IsNullOrWhiteSpace(outputDir))
{
outputDir = null;
}
else
{
outputDir = Path.GetFullPath(outputDir);
if (!Directory.Exists(outputDir))
{
if (MessagePopup.Quest("输出文件夹不存在,是否创建?") == DialogResult.OK)
{
try
{
Directory.CreateDirectory(outputDir);
}
catch (Exception ex)
{
logger.Error(ex.ToString());
logger.Error("Failed to create output dir {}", outputDir);
MessagePopup.Error(ex.ToString());
return;
}
}
else
{
return;
}
}
}
foreach (string p in items)
{
if (!File.Exists(p))
@@ -72,7 +114,7 @@ namespace SpineViewer.Dialogs
return;
}
Result = new(items.Cast<string>().ToArray(), sourceVersion, targetVersion, jsonTarget);
Result = new(outputDir, items.Cast<string>().ToArray(), sourceVersion, targetVersion, jsonTarget);
DialogResult = DialogResult.OK;
}
@@ -85,8 +127,13 @@ namespace SpineViewer.Dialogs
/// <summary>
/// 文件格式转换对话框结果包装类
/// </summary>
public class ConvertFileFormatDialogResult(string[] skelPaths, SpineVersion sourceVersion, SpineVersion targetVersion, bool jsonTarget)
public class ConvertFileFormatDialogResult(string? outputDir, string[] skelPaths, SpineVersion sourceVersion, SpineVersion targetVersion, bool jsonTarget)
{
/// <summary>
/// 输出文件夹, 如果为空, 则将转换后的文件转换到各自的文件夹下
/// </summary>
public string? OutputDir => outputDir;
/// <summary>
/// 骨骼文件路径列表
/// </summary>

View File

@@ -117,6 +117,9 @@
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<metadata name="folderBrowserDialog_Output.TrayLocation" type="System.Drawing.Point, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a">
<value>36, 22</value>
</metadata>
<assembly alias="System.Drawing" name="System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a" />
<data name="$this.Icon" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
<value>

View File

@@ -47,7 +47,7 @@
panel1.Location = new Point(0, 0);
panel1.Name = "panel1";
panel1.Padding = new Padding(50, 15, 50, 10);
panel1.Size = new Size(793, 754);
panel1.Size = new Size(793, 841);
panel1.TabIndex = 2;
//
// tableLayoutPanel1
@@ -65,7 +65,7 @@
tableLayoutPanel1.RowStyles.Add(new RowStyle());
tableLayoutPanel1.RowStyles.Add(new RowStyle(SizeType.Absolute, 20F));
tableLayoutPanel1.RowStyles.Add(new RowStyle(SizeType.Absolute, 20F));
tableLayoutPanel1.Size = new Size(693, 729);
tableLayoutPanel1.Size = new Size(693, 816);
tableLayoutPanel1.TabIndex = 0;
//
// propertyGrid_ExportArgs
@@ -74,7 +74,7 @@
propertyGrid_ExportArgs.Location = new Point(3, 3);
propertyGrid_ExportArgs.Name = "propertyGrid_ExportArgs";
propertyGrid_ExportArgs.PropertySort = PropertySort.Categorized;
propertyGrid_ExportArgs.Size = new Size(687, 650);
propertyGrid_ExportArgs.Size = new Size(687, 737);
propertyGrid_ExportArgs.TabIndex = 1;
propertyGrid_ExportArgs.ToolbarVisible = false;
//
@@ -88,7 +88,7 @@
tableLayoutPanel2.Controls.Add(button_Ok, 0, 0);
tableLayoutPanel2.Controls.Add(button_Cancel, 1, 0);
tableLayoutPanel2.Dock = DockStyle.Bottom;
tableLayoutPanel2.Location = new Point(3, 686);
tableLayoutPanel2.Location = new Point(3, 773);
tableLayoutPanel2.Margin = new Padding(3, 30, 3, 3);
tableLayoutPanel2.Name = "tableLayoutPanel2";
tableLayoutPanel2.RowCount = 1;
@@ -126,7 +126,7 @@
AutoScaleDimensions = new SizeF(11F, 24F);
AutoScaleMode = AutoScaleMode.Font;
CancelButton = button_Cancel;
ClientSize = new Size(793, 754);
ClientSize = new Size(793, 841);
Controls.Add(panel1);
Icon = (Icon)resources.GetObject("$this.Icon");
MaximizeBox = false;

View File

@@ -232,14 +232,14 @@
//
openFileDialog_Skel.AddExtension = false;
openFileDialog_Skel.AddToRecent = false;
openFileDialog_Skel.Filter = "skel 文件 (*.skel; *.json)|*.skel;*.json|二进制文件 (*.skel)|*.skel|文本文件 (*.json)|*.json|所有文件 (*.*)|*.*";
openFileDialog_Skel.Filter = "所有文件 (*.*)|*.*|skel 文件 (*.skel; *.json)|*.skel;*.json";
openFileDialog_Skel.Title = "选择skel文件";
//
// openFileDialog_Atlas
//
openFileDialog_Atlas.AddExtension = false;
openFileDialog_Atlas.AddToRecent = false;
openFileDialog_Atlas.Filter = "atlas 文件 (*.atlas)|*.atlas|所有文件 (*.*)|*.*";
openFileDialog_Atlas.Filter = "所有文件 (*.*)|*.*|atlas 文件 (*.atlas)|*.atlas";
openFileDialog_Atlas.Title = "选择atlas文件";
//
// OpenSpineDialog

View File

@@ -8,6 +8,51 @@ namespace SpineViewer.Extensions
{
public static class SFMLExtension
{
/// <summary>
/// 获取并集范围
/// </summary>
public static RectangleF Union(this RectangleF bounds, RectangleF other)
{
var x = Math.Min(bounds.X, other.X);
var y = Math.Min(bounds.Y, other.Y);
var w = Math.Max(bounds.Right, other.Right) - x;
var h = Math.Max(bounds.Bottom, other.Bottom) - y;
return new(x, y, w, h);
}
/// <summary>
/// 获取适合指定画布参数下能够覆盖包围盒的画布视区包围盒
/// </summary>
public static RectangleF GetCanvasBounds(this RectangleF bounds, Size resolution) => GetCanvasBounds(bounds, resolution, new(0), new(0));
/// <summary>
/// 获取适合指定画布参数下能够覆盖包围盒的画布视区包围盒
/// </summary>
public static RectangleF GetCanvasBounds(this RectangleF bounds, Size resolution, Padding margin) => GetCanvasBounds(bounds, resolution, margin, new(0));
/// <summary>
/// 获取适合指定画布参数下能够覆盖包围盒的画布视区包围盒
/// </summary>
public static RectangleF GetCanvasBounds(this RectangleF bounds, Size resolution, Padding margin, Padding padding)
{
float sizeW = bounds.Width;
float sizeH = bounds.Height;
float innerW = resolution.Width - padding.Horizontal;
float innerH = resolution.Height - padding.Vertical;
float scale = Math.Max(Math.Abs(sizeW / innerW), Math.Abs(sizeH / innerH)); // 取两方向上较大的缩放比, 以此让画布可以覆盖内容
float scaleW = scale * Math.Sign(sizeW);
float scaleH = scale * Math.Sign(sizeH);
innerW *= scaleW;
innerH *= scaleH;
var x = bounds.X - (innerW - sizeW) / 2 - (margin.Left + padding.Left) * scaleW;
var y = bounds.Y - (innerH - sizeH) / 2 - (margin.Top + padding.Top) * scaleH;
var w = (resolution.Width + margin.Horizontal) * scaleW;
var h = (resolution.Height + margin.Vertical) * scaleH;
return new(x, y, w, h);
}
/// <summary>
/// 获取 Winforms Bitmap 对象, 需要使用 Dispose 释放对象
/// </summary>
@@ -29,45 +74,45 @@ namespace SpineViewer.Extensions
}
/// <summary>
/// 获取某个包围盒下合适的视图
/// 获取视区的包围盒
/// </summary>
public static SFML.Graphics.View GetView(this RectangleF bounds, Size resolution, Padding padding)
=> bounds.GetView((uint)resolution.Width, (uint)resolution.Height, (uint)padding.Left, (uint)padding.Right, (uint)padding.Top, (uint)padding.Bottom);
/// <summary>
/// 获取某个包围盒下合适的视图
/// </summary>
public static SFML.Graphics.View GetView(this RectangleF bounds, uint width, uint height, Padding padding)
=> bounds.GetView(width, height, (uint)padding.Left, (uint)padding.Right, (uint)padding.Top, (uint)padding.Bottom);
/// <summary>
/// 获取某个包围盒下合适的视图
/// </summary>
public static SFML.Graphics.View GetView(this RectangleF bounds, Size resolution, uint paddingL = 1, uint paddingR = 1, uint paddingT = 1, uint paddingB = 1)
=> bounds.GetView((uint)resolution.Width, (uint)resolution.Height, paddingL, paddingR, paddingT, paddingB);
/// <summary>
/// 获取某个包围盒下合适的视图
/// </summary>
public static SFML.Graphics.View GetView(this RectangleF bounds, uint width, uint height, uint paddingL = 1, uint paddingR = 1, uint paddingT = 1, uint paddingB = 1)
public static RectangleF GetBounds(this SFML.Graphics.View view)
{
float sizeX = bounds.Width;
float sizeY = bounds.Height;
float innerW = width - paddingL - paddingR;
float innerH = height - paddingT - paddingB;
return new(
view.Center.X - view.Size.X / 2,
view.Center.Y - view.Size.Y / 2,
view.Size.X,
view.Size.Y
);
}
float scale = 1;
if (sizeY / sizeX < innerH / innerW)
scale = sizeX / innerW; // 相同的 X, 视窗 Y 更大
else
scale = sizeY / innerH; // 相同的 Y, 视窗 X 更大
/// <summary>
/// 按画布设置视区, 边缘和填充区域将不会出现内容
/// </summary>
public static void SetViewport(this SFML.Graphics.View view, Size resolution, Padding margin) => SetViewport(view, resolution, margin, new(0));
var x = bounds.X + bounds.Width / 2 + (paddingL - (float)paddingR) * scale;
var y = bounds.Y + bounds.Height / 2 + (paddingT - (float)paddingB) * scale;
var viewX = width * scale;
var viewY = height * scale;
/// <summary>
/// 按画布设置视区, 边缘和填充区域将不会出现内容
/// </summary>
public static void SetViewport(this SFML.Graphics.View view, Size resolution, Padding margin, Padding padding)
{
var innerW = resolution.Width - padding.Horizontal;
var innerH = resolution.Height - padding.Vertical;
return new(new(x, y), new(viewX, -viewY));
float width = resolution.Width + margin.Horizontal;
float height = resolution.Height + margin.Vertical;
view.Viewport = new(
(margin.Left + padding.Left) / width,
(margin.Top + padding.Top) / height,
innerW / width,
innerH / height
);
var bounds = view.GetBounds().GetCanvasBounds(new(innerW, innerH));
view.Center = new(bounds.X + bounds.Width / 2, bounds.Y + bounds.Height / 2);
view.Size = new(bounds.Width, bounds.Height);
}
}
}

View File

@@ -92,8 +92,9 @@ namespace SpineViewer
if (!exporterCache.ContainsKey(k)) exporterCache[k] = new FrameExporter();
var exporter = exporterCache[k];
using var view = spinePreviewPanel.GetView();
exporter.Resolution = spinePreviewPanel.Resolution;
exporter.View = spinePreviewPanel.GetView();
exporter.PreviewerView = view;
exporter.RenderSelectedOnly = spinePreviewPanel.RenderSelectedOnly;
var exportDialog = new Dialogs.ExportDialog(new FrameExporterProperty((FrameExporter)exporter));
@@ -112,8 +113,9 @@ namespace SpineViewer
if (!exporterCache.ContainsKey(k)) exporterCache[k] = new FrameSequenceExporter();
var exporter = exporterCache[k];
using var view = spinePreviewPanel.GetView();
exporter.Resolution = spinePreviewPanel.Resolution;
exporter.View = spinePreviewPanel.GetView();
exporter.PreviewerView = view;
exporter.RenderSelectedOnly = spinePreviewPanel.RenderSelectedOnly;
var exportDialog = new Dialogs.ExportDialog(new FrameSequenceExporterProperty((FrameSequenceExporter)exporter));
@@ -132,8 +134,9 @@ namespace SpineViewer
if (!exporterCache.ContainsKey(k)) exporterCache[k] = new GifExporter();
var exporter = exporterCache[k];
using var view = spinePreviewPanel.GetView();
exporter.Resolution = spinePreviewPanel.Resolution;
exporter.View = spinePreviewPanel.GetView();
exporter.PreviewerView = view;
exporter.RenderSelectedOnly = spinePreviewPanel.RenderSelectedOnly;
var exportDialog = new Dialogs.ExportDialog(new GifExporterProperty((GifExporter)exporter));
@@ -152,8 +155,9 @@ namespace SpineViewer
if (!exporterCache.ContainsKey(k)) exporterCache[k] = new WebpExporter();
var exporter = exporterCache[k];
using var view = spinePreviewPanel.GetView();
exporter.Resolution = spinePreviewPanel.Resolution;
exporter.View = spinePreviewPanel.GetView();
exporter.PreviewerView = view;
exporter.RenderSelectedOnly = spinePreviewPanel.RenderSelectedOnly;
var exportDialog = new Dialogs.ExportDialog(new WebpExporterProperty((WebpExporter)exporter));
@@ -172,8 +176,9 @@ namespace SpineViewer
if (!exporterCache.ContainsKey(k)) exporterCache[k] = new AvifExporter();
var exporter = exporterCache[k];
using var view = spinePreviewPanel.GetView();
exporter.Resolution = spinePreviewPanel.Resolution;
exporter.View = spinePreviewPanel.GetView();
exporter.PreviewerView = view;
exporter.RenderSelectedOnly = spinePreviewPanel.RenderSelectedOnly;
var exportDialog = new Dialogs.ExportDialog(new AvifExporterProperty((AvifExporter)exporter));
@@ -192,8 +197,9 @@ namespace SpineViewer
if (!exporterCache.ContainsKey(k)) exporterCache[k] = new Mp4Exporter();
var exporter = exporterCache[k];
using var view = spinePreviewPanel.GetView();
exporter.Resolution = spinePreviewPanel.Resolution;
exporter.View = spinePreviewPanel.GetView();
exporter.PreviewerView = view;
exporter.RenderSelectedOnly = spinePreviewPanel.RenderSelectedOnly;
var exportDialog = new Dialogs.ExportDialog(new Mp4ExporterProperty((Mp4Exporter)exporter));
@@ -212,8 +218,9 @@ namespace SpineViewer
if (!exporterCache.ContainsKey(k)) exporterCache[k] = new WebmExporter();
var exporter = exporterCache[k];
using var view = spinePreviewPanel.GetView();
exporter.Resolution = spinePreviewPanel.Resolution;
exporter.View = spinePreviewPanel.GetView();
exporter.PreviewerView = view;
exporter.RenderSelectedOnly = spinePreviewPanel.RenderSelectedOnly;
var exportDialog = new Dialogs.ExportDialog(new WebmExporterProperty((WebmExporter)exporter));
@@ -232,8 +239,9 @@ namespace SpineViewer
if (!exporterCache.ContainsKey(k)) exporterCache[k] = new MkvExporter();
var exporter = exporterCache[k];
using var view = spinePreviewPanel.GetView();
exporter.Resolution = spinePreviewPanel.Resolution;
exporter.View = spinePreviewPanel.GetView();
exporter.PreviewerView = view;
exporter.RenderSelectedOnly = spinePreviewPanel.RenderSelectedOnly;
var exportDialog = new Dialogs.ExportDialog(new MkvExporterProperty((MkvExporter)exporter));
@@ -252,8 +260,9 @@ namespace SpineViewer
if (!exporterCache.ContainsKey(k)) exporterCache[k] = new MovExporter();
var exporter = exporterCache[k];
using var view = spinePreviewPanel.GetView();
exporter.Resolution = spinePreviewPanel.Resolution;
exporter.View = spinePreviewPanel.GetView();
exporter.PreviewerView = view;
exporter.RenderSelectedOnly = spinePreviewPanel.RenderSelectedOnly;
var exportDialog = new Dialogs.ExportDialog(new MovExporterProperty((MovExporter)exporter));
@@ -272,8 +281,9 @@ namespace SpineViewer
if (!exporterCache.ContainsKey(k)) exporterCache[k] = new CustomExporter();
var exporter = exporterCache[k];
using var view = spinePreviewPanel.GetView();
exporter.Resolution = spinePreviewPanel.Resolution;
exporter.View = spinePreviewPanel.GetView();
exporter.PreviewerView = view;
exporter.RenderSelectedOnly = spinePreviewPanel.RenderSelectedOnly;
var exportDialog = new Dialogs.ExportDialog(new CustomExporterProperty((CustomExporter)exporter));
@@ -347,19 +357,15 @@ namespace SpineViewer
private void ConvertFileFormat_Work(object? sender, DoWorkEventArgs e)
{
var worker = sender as BackgroundWorker;
var arguments = e.Argument as Dialogs.ConvertFileFormatDialogResult;
var skelPaths = arguments.SkelPaths;
var srcVersion = arguments.SourceVersion;
var tgtVersion = arguments.TargetVersion;
var jsonTarget = arguments.JsonTarget;
var newSuffix = jsonTarget ? ".json" : ".skel";
var args = e.Argument as Dialogs.ConvertFileFormatDialogResult;
var newSuffix = args.JsonTarget ? ".json" : ".skel";
int totalCount = skelPaths.Length;
int totalCount = args.SkelPaths.Length;
int success = 0;
int error = 0;
SkeletonConverter srcCvter = srcVersion != SpineVersion.Auto ? SkeletonConverter.New(srcVersion) : null;
SkeletonConverter tgtCvter = SkeletonConverter.New(tgtVersion);
SkeletonConverter srcCvter = args.SourceVersion != SpineVersion.Auto ? SkeletonConverter.New(args.SourceVersion) : null;
SkeletonConverter tgtCvter = SkeletonConverter.New(args.TargetVersion);
worker.ReportProgress(0, $"已处理 0/{totalCount}");
for (int i = 0; i < totalCount; i++)
@@ -370,12 +376,13 @@ namespace SpineViewer
break;
}
var skelPath = skelPaths[i];
var skelPath = args.SkelPaths[i];
var newPath = Path.ChangeExtension(skelPath, newSuffix);
if (args.OutputDir is string outputDir) newPath = Path.Combine(outputDir, Path.GetFileName(newPath));
try
{
if (srcVersion == SpineVersion.Auto)
if (args.SourceVersion == SpineVersion.Auto)
{
try
{
@@ -387,8 +394,9 @@ namespace SpineViewer
}
}
var root = srcCvter.Read(skelPath);
root = srcCvter.ToVersion(root, tgtVersion);
if (jsonTarget) tgtCvter.WriteJson(root, newPath); else tgtCvter.WriteBinary(root, newPath);
root = srcCvter.ToVersion(root, args.TargetVersion);
if (args.JsonTarget) tgtCvter.WriteJson(root, newPath);
else tgtCvter.WriteBinary(root, newPath);
success++;
}
catch (Exception ex)

View File

@@ -12,8 +12,45 @@ using System.Globalization;
namespace SpineViewer.Spine.Implementations.SkeletonConverter
{
[SpineImplementation(SpineVersion.V38)]
class SkeletonConverter38 : SpineViewer.Spine.SkeletonConverter
public class SkeletonConverter38 : Spine.SkeletonConverter
{
private static readonly Dictionary<TransformMode, string> TransformModeJsonValue = new()
{
[TransformMode.Normal] = "normal",
[TransformMode.OnlyTranslation] = "onlyTranslation",
[TransformMode.NoRotationOrReflection] = "noRotationOrReflection",
[TransformMode.NoScale] = "noScale",
[TransformMode.NoScaleOrReflection] = "noScaleOrReflection",
};
private static readonly Dictionary<BlendMode, string> BlendModeJsonValue = new()
{
[BlendMode.Normal] = "normal",
[BlendMode.Additive] = "additive",
[BlendMode.Multiply] = "multiply",
[BlendMode.Screen] = "screen",
};
private static readonly Dictionary<PositionMode, string> PositionModeJsonValue = new()
{
[PositionMode.Fixed] = "fixed",
[PositionMode.Percent] = "percent",
};
private static readonly Dictionary<SpacingMode, string> SpacingModeJsonValue = new()
{
[SpacingMode.Length] = "length",
[SpacingMode.Fixed] = "fixed",
[SpacingMode.Percent] = "percent",
};
private static readonly Dictionary<RotateMode, string> RotateModeJsonValue = new()
{
[RotateMode.Tangent] = "tangent",
[RotateMode.Chain] = "chain",
[RotateMode.ChainScale] = "chainScale",
};
private BinaryReader reader = null;
private JsonObject root = null;
private bool nonessential = false;
@@ -44,6 +81,9 @@ namespace SpineViewer.Spine.Implementations.SkeletonConverter
idx2event.Clear();
// 清理临时属性
foreach (var (_, data) in root["events"].AsObject()) data.AsObject().Remove("__name__");
return root;
}
@@ -90,7 +130,7 @@ namespace SpineViewer.Spine.Implementations.SkeletonConverter
data["shearX"] = reader.ReadFloat();
data["shearY"] = reader.ReadFloat();
data["length"] = reader.ReadFloat();
data["transform"] = SkeletonBinary.TransformModeValues[reader.ReadVarInt()].ToString();
data["transform"] = TransformModeJsonValue[SkeletonBinary.TransformModeValues[reader.ReadVarInt()]];
data["skin"] = reader.ReadBoolean();
if (nonessential) reader.ReadInt();
bones.Add(data);
@@ -111,7 +151,7 @@ namespace SpineViewer.Spine.Implementations.SkeletonConverter
int dark = reader.ReadInt();
if (dark != -1) data["dark"] = dark.ToString("x6"); // 0x00rrggbb -> rrggbb
data["attachment"] = reader.ReadStringRef();
data["blend"] = ((BlendMode)reader.ReadVarInt()).ToString();
data["blend"] = BlendModeJsonValue[((BlendMode)reader.ReadVarInt())];
slots.Add(data);
}
root["slots"] = slots;
@@ -181,9 +221,9 @@ namespace SpineViewer.Spine.Implementations.SkeletonConverter
data["skin"] = reader.ReadBoolean();
data["bones"] = ReadNames(bones);
data["target"] = (string)bones[reader.ReadVarInt()]["name"];
data["positionMode"] = ((PositionMode)reader.ReadVarInt()).ToString();
data["spacingMode"] = ((SpacingMode)reader.ReadVarInt()).ToString();
data["rotateMode"] = ((RotateMode)reader.ReadVarInt()).ToString();
data["positionMode"] = PositionModeJsonValue[((PositionMode)reader.ReadVarInt())];
data["spacingMode"] = SpacingModeJsonValue[((SpacingMode)reader.ReadVarInt())];
data["rotateMode"] = RotateModeJsonValue[((RotateMode)reader.ReadVarInt())];
data["rotation"] = reader.ReadFloat();
data["position"] = reader.ReadFloat();
data["spacing"] = reader.ReadFloat();
@@ -283,7 +323,7 @@ namespace SpineViewer.Spine.Implementations.SkeletonConverter
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["uvs"] = ReadFloatArray(vertexCount << 1); // vertexCount = uvs.Length >> 1
attachment["triangles"] = ReadShortArray();
attachment["vertices"] = ReadVertices(vertexCount);
attachment["hull"] = reader.ReadVarInt();
@@ -348,7 +388,7 @@ namespace SpineViewer.Spine.Implementations.SkeletonConverter
JsonObject data = [];
var name = reader.ReadStringRef();
events[name] = data;
data["name"] = name; // 额外增加的, 方便后面查找
data["__name__"] = name; // 数据里是不应该有这个字段的, 但是为了 ReadEventTimelines 里能够提供 name 字段, 临时增加了一下
data["int"] = reader.ReadVarInt(false);
data["float"] = reader.ReadFloat();
data["string"] = reader.ReadString();
@@ -376,7 +416,7 @@ namespace SpineViewer.Spine.Implementations.SkeletonConverter
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 (ReadDrawOrderTimelines() is JsonArray draworder) data["draworder"] = draworder;
if (ReadEventTimelines() is JsonArray events) data["events"] = events;
}
root["animations"] = animations;
@@ -592,7 +632,7 @@ namespace SpineViewer.Spine.Implementations.SkeletonConverter
for (int timelineCount = reader.ReadVarInt(); timelineCount > 0; timelineCount--)
{
JsonArray frames = [];
var type = reader.ReadByte();
var type = reader.ReadSByte();
var frameCount = reader.ReadVarInt();
switch (type)
{
@@ -600,11 +640,13 @@ namespace SpineViewer.Spine.Implementations.SkeletonConverter
timeline["position"] = frames;
while (frameCount-- > 0)
{
frames.Add(new JsonObject()
var o = new JsonObject()
{
["time"] = reader.ReadFloat(),
["position"] = reader.ReadFloat(),
});
};
if (frameCount > 0) ReadCurve(o);
frames.Add(o);
}
break;
case SkeletonBinary.PATH_SPACING:
@@ -671,8 +713,7 @@ namespace SpineViewer.Spine.Implementations.SkeletonConverter
var end = reader.ReadVarInt();
if (end > 0)
{
var start = reader.ReadVarInt();
o["offset"] = start;
o["offset"] = reader.ReadVarInt();
o["vertices"] = ReadFloatArray(end);
}
if (frameCount > 0) ReadCurve(o);
@@ -719,14 +760,14 @@ namespace SpineViewer.Spine.Implementations.SkeletonConverter
JsonObject data = [];
data["time"] = reader.ReadFloat();
JsonObject eventData = idx2event[reader.ReadVarInt()].AsObject();
data["name"] = (string)eventData["name"];
data["name"] = (string)eventData["__name__"];
data["int"] = reader.ReadVarInt();
data["float"] = reader.ReadFloat();
data["string"] = reader.ReadBoolean() ? reader.ReadString() : (string)eventData["string"];
if (reader.ReadBoolean()) data["string"] = reader.ReadString();
if (eventData.ContainsKey("audio"))
{
data["volume"] = (string)eventData["volume"];
data["balance"] = (string)eventData["balance"];
data["volume"] = reader.ReadFloat();
data["balance"] = reader.ReadFloat();
}
eventTimelines.Add(data);
}
@@ -785,10 +826,6 @@ namespace SpineViewer.Spine.Implementations.SkeletonConverter
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";
@@ -810,6 +847,7 @@ namespace SpineViewer.Spine.Implementations.SkeletonConverter
private readonly Dictionary<string, int> ik2idx = [];
private readonly Dictionary<string, int> transform2idx = [];
private readonly Dictionary<string, int> path2idx = [];
private readonly Dictionary<string, int> skin2idx = [];
private readonly Dictionary<string, int> event2idx = [];
public override void WriteBinary(JsonObject root, string binPath, bool nonessential = false)
@@ -818,7 +856,7 @@ namespace SpineViewer.Spine.Implementations.SkeletonConverter
this.root = root;
using var outputBody = new MemoryStream(); // 先把主体写入内存缓冲区
writer = new(outputBody);
BinaryWriter tmpWriter = writer = new (outputBody);
WriteBones();
WriteSlots();
@@ -828,16 +866,28 @@ namespace SpineViewer.Spine.Implementations.SkeletonConverter
WriteSkins();
WriteEvents();
WriteAnimations();
//using var output = File.Create(binPath); // 将数据写入文件
//writer = new(output);
using var output = File.Create(binPath); // 将数据写入文件
writer = new(output);
// 把字符串表保留过来
writer.StringTable.AddRange(tmpWriter.StringTable);
WriteSkeleton();
WriteStrings();
//output.Write(outputBody.GetBuffer());
outputBody.Seek(0, SeekOrigin.Begin);
outputBody.CopyTo(output);
writer = null;
this.root = null;
bone2idx.Clear();
slot2idx.Clear();
ik2idx.Clear();
transform2idx.Clear();
path2idx.Clear();
skin2idx.Clear();
event2idx.Clear();
}
private void WriteSkeleton()
@@ -890,7 +940,7 @@ namespace SpineViewer.Spine.Implementations.SkeletonConverter
if (data.TryGetPropertyValue("shearX", out var shearX)) writer.WriteFloat((float)shearX); else writer.WriteFloat(0);
if (data.TryGetPropertyValue("shearY", out var shearY)) writer.WriteFloat((float)shearY); else writer.WriteFloat(0);
if (data.TryGetPropertyValue("length", out var length)) writer.WriteFloat((float)length); else writer.WriteFloat(0);
if (data.TryGetPropertyValue("transform", out var transform)) writer.WriteVarInt((int)Enum.Parse<TransformMode>((string)transform, true)); else writer.WriteVarInt((int)TransformMode.Normal);
if (data.TryGetPropertyValue("transform", out var transform)) writer.WriteVarInt(Array.IndexOf(SkeletonBinary.TransformModeValues, Enum.Parse<TransformMode>((string)transform, true))); else writer.WriteVarInt(0);
if (data.TryGetPropertyValue("skin", out var skin)) writer.WriteBoolean((bool)skin); else writer.WriteBoolean(false);
if (nonessential) writer.WriteInt(0);
bone2idx[name] = i;
@@ -912,7 +962,7 @@ namespace SpineViewer.Spine.Implementations.SkeletonConverter
var name = (string)data["name"];
writer.WriteString(name);
writer.WriteVarInt(bone2idx[(string)data["bone"]]);
if (data.TryGetPropertyValue("color", out var color)) writer.WriteInt(int.Parse((string)color, NumberStyles.HexNumber)); else writer.WriteInt(0);
if (data.TryGetPropertyValue("color", out var color)) writer.WriteInt(int.Parse((string)color, NumberStyles.HexNumber)); else writer.WriteInt(-1); // 默认值是全 255
if (data.TryGetPropertyValue("dark", out var dark)) writer.WriteInt(int.Parse((string)dark, NumberStyles.HexNumber)); else writer.WriteInt(-1);
if (data.TryGetPropertyValue("attachment", out var attachment)) writer.WriteStringRef((string)attachment); else writer.WriteStringRef(null);
if (data.TryGetPropertyValue("blend", out var blend)) writer.WriteVarInt((int)Enum.Parse<BlendMode>((string)blend, true)); else writer.WriteVarInt((int)BlendMode.Normal);
@@ -1018,6 +1068,7 @@ namespace SpineViewer.Spine.Implementations.SkeletonConverter
{
writer.WriteVarInt(0); // default 的 slotCount
writer.WriteVarInt(0); // 其他皮肤数量
skin2idx["default"] = skin2idx.Count;
return;
}
@@ -1029,6 +1080,7 @@ namespace SpineViewer.Spine.Implementations.SkeletonConverter
{
hasDefault = true;
WriteSkin(skin, true);
skin2idx["default"] = skin2idx.Count;
break;
}
}
@@ -1045,8 +1097,12 @@ namespace SpineViewer.Spine.Implementations.SkeletonConverter
writer.WriteVarInt(skinCount);
foreach (JsonObject skin in skins)
{
if ((string)skin["name"] != "default")
var name = (string)skin["name"];
if (name != "default")
{
WriteSkin(skin);
skin2idx[name] = skin2idx.Count;
}
}
}
@@ -1109,7 +1165,7 @@ namespace SpineViewer.Spine.Implementations.SkeletonConverter
if (attachment.TryGetPropertyValue("scaleY", out var scaleY)) writer.WriteFloat((float)scaleY); else writer.WriteFloat(1);
if (attachment.TryGetPropertyValue("width", out var width)) writer.WriteFloat((float)width); else writer.WriteFloat(32);
if (attachment.TryGetPropertyValue("height", out var height)) writer.WriteFloat((float)height); else writer.WriteFloat(32);
if (attachment.TryGetPropertyValue("color", out var color1)) writer.WriteInt(int.Parse((string)color1, NumberStyles.HexNumber)); else writer.WriteInt(0);
if (attachment.TryGetPropertyValue("color", out var color1)) writer.WriteInt(int.Parse((string)color1, NumberStyles.HexNumber)); else writer.WriteInt(-1);
break;
case AttachmentType.Boundingbox:
if (attachment.TryGetPropertyValue("vertexCount", out var _vertexCount1)) vertexCount = (int)_vertexCount1; else vertexCount = 0;
@@ -1119,10 +1175,10 @@ namespace SpineViewer.Spine.Implementations.SkeletonConverter
break;
case AttachmentType.Mesh:
if (attachment.TryGetPropertyValue("path", out var path2)) writer.WriteStringRef((string)path2); else writer.WriteStringRef(null);
if (attachment.TryGetPropertyValue("color", out var color2)) writer.WriteInt(int.Parse((string)color2, NumberStyles.HexNumber)); else writer.WriteInt(0);
if (attachment.TryGetPropertyValue("vertexCount", out var _vertexCount2)) vertexCount = (int)_vertexCount2; else vertexCount = 0;
if (attachment.TryGetPropertyValue("color", out var color2)) writer.WriteInt(int.Parse((string)color2, NumberStyles.HexNumber)); else writer.WriteInt(-1);
vertexCount = attachment["uvs"].AsArray().Count >> 1;
writer.WriteVarInt(vertexCount);
WriteFloatArray(attachment["uvs"].AsArray(), vertexCount << 1); // vertexCount = uvs.Length
WriteFloatArray(attachment["uvs"].AsArray(), vertexCount << 1); // vertexCount = uvs.Length >> 1
WriteShortArray(attachment["triangles"].AsArray());
WriteVertices(attachment["vertices"].AsArray(), vertexCount);
if (attachment.TryGetPropertyValue("hull", out var hull)) writer.WriteVarInt((int)hull); else writer.WriteVarInt(0);
@@ -1135,7 +1191,7 @@ namespace SpineViewer.Spine.Implementations.SkeletonConverter
break;
case AttachmentType.Linkedmesh:
if (attachment.TryGetPropertyValue("path", out var path3)) writer.WriteStringRef((string)path3); else writer.WriteStringRef(null);
if (attachment.TryGetPropertyValue("color", out var color3)) writer.WriteInt(int.Parse((string)color3, NumberStyles.HexNumber)); else writer.WriteInt(0);
if (attachment.TryGetPropertyValue("color", out var color3)) writer.WriteInt(int.Parse((string)color3, NumberStyles.HexNumber)); else writer.WriteInt(-1);
if (attachment.TryGetPropertyValue("skin", out var skin)) writer.WriteStringRef((string)skin); else writer.WriteStringRef(null);
if (attachment.TryGetPropertyValue("parent", out var parent)) writer.WriteStringRef((string)parent); else writer.WriteStringRef(null);
if (attachment.TryGetPropertyValue("deform", out var deform)) writer.WriteBoolean((bool)deform); else writer.WriteBoolean(true);
@@ -1199,6 +1255,10 @@ namespace SpineViewer.Spine.Implementations.SkeletonConverter
if (data.TryGetPropertyValue("balance", out var balance)) writer.WriteFloat((float)balance); else writer.WriteFloat(0);
}
}
else
{
writer.WriteString(null);
}
event2idx[name] = i++;
}
}
@@ -1210,11 +1270,333 @@ namespace SpineViewer.Spine.Implementations.SkeletonConverter
writer.WriteVarInt(0);
return;
}
JsonArray animations = root["animations"].AsArray();
JsonObject animations = root["animations"].AsObject();
writer.WriteVarInt(animations.Count);
for (int i = 0, n = animations.Count; i < n; i++)
foreach (var (name, _data) in animations)
{
throw new NotImplementedException();
JsonObject data = _data.AsObject();
writer.WriteString(name);
if (data.TryGetPropertyValue("slots", out var slots)) WriteSlotTimelines(slots.AsObject()); else writer.WriteVarInt(0);
if (data.TryGetPropertyValue("bones", out var bones)) WriteBoneTimelines(bones.AsObject()); else writer.WriteVarInt(0);
if (data.TryGetPropertyValue("ik", out var ik)) WriteIKTimelines(ik.AsObject()); else writer.WriteVarInt(0);
if (data.TryGetPropertyValue("transform", out var transform)) WriteTransformTimelines(transform.AsObject()); else writer.WriteVarInt(0);
if (data.TryGetPropertyValue("path", out var path)) WritePathTimelines(path.AsObject()); else writer.WriteVarInt(0);
if (data.TryGetPropertyValue("deform", out var deform)) WriteDeformTimelines(deform.AsObject()); else writer.WriteVarInt(0);
if (data.TryGetPropertyValue("drawOrder", out var drawOrder)) WriteDrawOrderTimelines(drawOrder.AsArray()); else
if (data.TryGetPropertyValue("draworder", out var draworder)) WriteDrawOrderTimelines(draworder.AsArray()); else writer.WriteVarInt(0);
if (data.TryGetPropertyValue("events", out var events)) WriteEventTimelines(events.AsArray()); else writer.WriteVarInt(0);
}
}
private void WriteSlotTimelines(JsonObject slotTimelines)
{
writer.WriteVarInt(slotTimelines.Count);
foreach (var (name, _timeline) in slotTimelines)
{
JsonObject timeline = _timeline.AsObject();
writer.WriteVarInt(slot2idx[name]);
writer.WriteVarInt(timeline.Count);
foreach (var (type, _frames) in timeline)
{
JsonArray frames = _frames.AsArray();
if (type == "attachment")
{
writer.WriteByte(SkeletonBinary.SLOT_ATTACHMENT);
writer.WriteVarInt(frames.Count);
foreach (JsonObject o in frames)
{
if (o.TryGetPropertyValue("time", out var time)) writer.WriteFloat((float)time); else writer.WriteFloat(0);
writer.WriteStringRef((string)o["name"]);
}
}
else if (type == "color")
{
writer.WriteByte(SkeletonBinary.SLOT_COLOR);
writer.WriteVarInt(frames.Count);
for (int i = 0, n = frames.Count; i < n; i++)
{
JsonObject o = frames[i].AsObject();
if (o.TryGetPropertyValue("time", out var time)) writer.WriteFloat((float)time); else writer.WriteFloat(0);
writer.WriteInt(int.Parse((string)o["color"], NumberStyles.HexNumber));
if (i < n - 1) WriteCurve(o);
}
}
else if (type == "twoColor")
{
writer.WriteByte(SkeletonBinary.SLOT_TWO_COLOR);
writer.WriteVarInt(frames.Count);
for (int i = 0, n = frames.Count; i < n; i++)
{
JsonObject o = frames[i].AsObject();
if (o.TryGetPropertyValue("time", out var time)) writer.WriteFloat((float)time); else writer.WriteFloat(0);
writer.WriteInt(int.Parse((string)o["light"], NumberStyles.HexNumber));
writer.WriteInt(int.Parse((string)o["dark"], NumberStyles.HexNumber));
if (i < n - 1) WriteCurve(o);
}
}
}
}
}
private void WriteBoneTimelines(JsonObject boneTimelines)
{
writer.WriteVarInt(boneTimelines.Count);
foreach (var (name, _timeline) in boneTimelines)
{
JsonObject timeline = _timeline.AsObject();
writer.WriteVarInt(bone2idx[name]);
writer.WriteVarInt(timeline.Count);
foreach (var (type, _frames) in timeline)
{
JsonArray frames = _frames.AsArray();
if (type == "rotate")
{
writer.WriteByte(SkeletonBinary.BONE_ROTATE);
writer.WriteVarInt(frames.Count);
for (int i = 0, n = frames.Count; i < n; i++)
{
JsonObject o = frames[i].AsObject();
if (o.TryGetPropertyValue("time", out var time)) writer.WriteFloat((float)time); else writer.WriteFloat(0);
if (o.TryGetPropertyValue("angle", out var angle)) writer.WriteFloat((float)angle); else writer.WriteFloat(0);
if (i < n - 1) WriteCurve(o);
}
}
else if (type == "translate")
{
writer.WriteByte(SkeletonBinary.BONE_TRANSLATE);
writer.WriteVarInt(frames.Count);
for (int i = 0, n = frames.Count; i < n; i++)
{
JsonObject o = frames[i].AsObject();
if (o.TryGetPropertyValue("time", out var time)) writer.WriteFloat((float)time); else writer.WriteFloat(0);
if (o.TryGetPropertyValue("x", out var x)) writer.WriteFloat((float)x); else writer.WriteFloat(0);
if (o.TryGetPropertyValue("y", out var y)) writer.WriteFloat((float)y); else writer.WriteFloat(0);
if (i < n - 1) WriteCurve(o);
}
}
else if (type == "scale")
{
writer.WriteByte(SkeletonBinary.BONE_SCALE);
writer.WriteVarInt(frames.Count);
for (int i = 0, n = frames.Count; i < n; i++)
{
JsonObject o = frames[i].AsObject();
if (o.TryGetPropertyValue("time", out var time)) writer.WriteFloat((float)time); else writer.WriteFloat(0);
if (o.TryGetPropertyValue("x", out var x)) writer.WriteFloat((float)x); else writer.WriteFloat(1);
if (o.TryGetPropertyValue("y", out var y)) writer.WriteFloat((float)y); else writer.WriteFloat(1);
if (i < n - 1) WriteCurve(o);
}
}
else if (type == "shear")
{
writer.WriteByte(SkeletonBinary.BONE_SHEAR);
writer.WriteVarInt(frames.Count);
for (int i = 0, n = frames.Count; i < n; i++)
{
JsonObject o = frames[i].AsObject();
if (o.TryGetPropertyValue("time", out var time)) writer.WriteFloat((float)time); else writer.WriteFloat(0);
if (o.TryGetPropertyValue("x", out var x)) writer.WriteFloat((float)x); else writer.WriteFloat(0);
if (o.TryGetPropertyValue("y", out var y)) writer.WriteFloat((float)y); else writer.WriteFloat(0);
if (i < n - 1) WriteCurve(o);
}
}
}
}
}
private void WriteIKTimelines(JsonObject ikTimelines)
{
writer.WriteVarInt(ikTimelines.Count);
foreach (var (name, _frames) in ikTimelines)
{
JsonArray frames = _frames.AsArray();
writer.WriteVarInt(ik2idx[name]);
writer.WriteVarInt(frames.Count);
for (int i = 0, n = frames.Count; i < n; i++)
{
JsonObject o = frames[i].AsObject();
if (o.TryGetPropertyValue("time", out var time)) writer.WriteFloat((float)time); else writer.WriteFloat(0);
if (o.TryGetPropertyValue("mix", out var mix)) writer.WriteFloat((float)mix); else writer.WriteFloat(1);
if (o.TryGetPropertyValue("softness", out var softness)) writer.WriteFloat((float)softness); else writer.WriteFloat(0);
if (o.TryGetPropertyValue("bendPositive", out var bendPositive)) writer.WriteSByte((sbyte)((bool)bendPositive ? 1 : -1)); else writer.WriteSByte(1);
if (o.TryGetPropertyValue("compress", out var compress)) writer.WriteBoolean((bool)compress); else writer.WriteBoolean(false);
if (o.TryGetPropertyValue("stretch", out var stretch)) writer.WriteBoolean((bool)stretch); else writer.WriteBoolean(false);
if (i < n - 1) WriteCurve(o);
}
}
}
private void WriteTransformTimelines(JsonObject transformTimelines)
{
writer.WriteVarInt(transformTimelines.Count);
foreach (var (name, _frames) in transformTimelines)
{
JsonArray frames = _frames.AsArray();
writer.WriteVarInt(transform2idx[name]);
writer.WriteVarInt(frames.Count);
for (int i = 0, n = frames.Count; i < n; i++)
{
JsonObject o = frames[i].AsObject();
if (o.TryGetPropertyValue("time", out var time)) writer.WriteFloat((float)time); else writer.WriteFloat(0);
if (o.TryGetPropertyValue("rotateMix", out var rotateMix)) writer.WriteFloat((float)rotateMix); else writer.WriteFloat(1);
if (o.TryGetPropertyValue("translateMix", out var translateMix)) writer.WriteFloat((float)translateMix); else writer.WriteFloat(1);
if (o.TryGetPropertyValue("scaleMix", out var scaleMix)) writer.WriteFloat((float)scaleMix); else writer.WriteFloat(1);
if (o.TryGetPropertyValue("shearMix", out var shearMix)) writer.WriteFloat((float)shearMix); else writer.WriteFloat(1);
if (i < n - 1) WriteCurve(o);
}
}
}
private void WritePathTimelines(JsonObject pathTimelines)
{
writer.WriteVarInt(pathTimelines.Count);
foreach (var (name, _timeline) in pathTimelines)
{
JsonObject timeline = _timeline.AsObject();
writer.WriteVarInt(path2idx[name]);
writer.WriteVarInt(timeline.Count);
foreach (var (type, _frame) in timeline)
{
JsonArray frames = _frame.AsArray();
if (type == "position")
{
writer.WriteByte(SkeletonBinary.PATH_POSITION);
writer.WriteVarInt(frames.Count);
for (int i = 0, n = frames.Count; i < n; i++)
{
JsonObject o = frames[i].AsObject();
if (o.TryGetPropertyValue("time", out var time)) writer.WriteFloat((float)time); else writer.WriteFloat(0);
if (o.TryGetPropertyValue("position", out var position)) writer.WriteFloat((float)position); else writer.WriteFloat(0);
if (i < n - 1) WriteCurve(o);
}
}
else if (type == "spacing")
{
writer.WriteByte(SkeletonBinary.PATH_SPACING);
writer.WriteVarInt(frames.Count);
for (int i = 0, n = frames.Count; i < n; i++)
{
JsonObject o = frames[i].AsObject();
if (o.TryGetPropertyValue("time", out var time)) writer.WriteFloat((float)time); else writer.WriteFloat(0);
if (o.TryGetPropertyValue("spacing", out var spacing)) writer.WriteFloat((float)spacing); else writer.WriteFloat(0);
if (i < n - 1) WriteCurve(o);
}
}
else if (type == "mix")
{
writer.WriteByte(SkeletonBinary.PATH_MIX);
writer.WriteVarInt(frames.Count);
for (int i = 0, n = frames.Count; i < n; i++)
{
JsonObject o = frames[i].AsObject();
if (o.TryGetPropertyValue("time", out var time)) writer.WriteFloat((float)time); else writer.WriteFloat(0);
if (o.TryGetPropertyValue("rotateMix", out var rotateMix)) writer.WriteFloat((float)rotateMix); else writer.WriteFloat(1);
if (o.TryGetPropertyValue("translateMix", out var translateMix)) writer.WriteFloat((float)translateMix); else writer.WriteFloat(1);
if (i < n - 1) WriteCurve(o);
}
}
}
}
}
private void WriteDeformTimelines(JsonObject deformTimelines)
{
writer.WriteVarInt(deformTimelines.Count);
foreach (var (skinName, _skinValue) in deformTimelines)
{
JsonObject skinValue = _skinValue.AsObject();
writer.WriteVarInt(skin2idx[skinName]);
writer.WriteVarInt(skinValue.Count);
foreach (var (slotName, _slotValue) in skinValue)
{
JsonObject slotValue = _slotValue.AsObject();
writer.WriteVarInt(slot2idx[slotName]);
writer.WriteVarInt(slotValue.Count);
foreach (var (attachmentName, _frames) in slotValue)
{
JsonArray frames = _frames.AsArray();
writer.WriteStringRef(attachmentName);
writer.WriteVarInt(frames.Count);
for (int i = 0, n = frames.Count; i < n; i++)
{
JsonObject o = frames[i].AsObject();
if (o.TryGetPropertyValue("time", out var time)) writer.WriteFloat((float)time); else writer.WriteFloat(0);
if (o.TryGetPropertyValue("vertices", out var _vertices))
{
JsonArray vertices = _vertices.AsArray();
writer.WriteVarInt(vertices.Count);
if (vertices.Count > 0)
{
if (o.TryGetPropertyValue("offset", out var offset)) writer.WriteVarInt((int)offset); else writer.WriteVarInt(0);
WriteFloatArray(vertices, vertices.Count);
}
}
else
{
writer.WriteVarInt(0);
}
if (i < n - 1) WriteCurve(o);
}
}
}
}
}
private void WriteDrawOrderTimelines(JsonArray drawOrderTimelines)
{
writer.WriteVarInt(drawOrderTimelines.Count);
foreach (JsonObject data in drawOrderTimelines)
{
if (data.TryGetPropertyValue("time", out var time)) writer.WriteFloat((float)time); else writer.WriteFloat(0);
if (data.TryGetPropertyValue("offsets", out var _offsets))
{
JsonArray offsets = _offsets.AsArray();
writer.WriteVarInt(offsets.Count);
foreach (JsonObject o in offsets)
{
writer.WriteVarInt(slot2idx[(string)o["slot"]]);
writer.WriteVarInt((int)o["offset"]);
}
}
else
{
writer.WriteVarInt(0);
}
}
}
private void WriteEventTimelines(JsonArray eventTimelines)
{
JsonObject events = root["events"].AsObject();
writer.WriteVarInt(eventTimelines.Count);
foreach(JsonObject data in eventTimelines)
{
JsonObject eventData = events[(string)data["name"]].AsObject();
if (data.TryGetPropertyValue("time", out var time)) writer.WriteFloat((float)time); else writer.WriteFloat(0);
writer.WriteVarInt(event2idx[(string)data["name"]]);
if (data.TryGetPropertyValue("int", out var @int)) writer.WriteVarInt((int)@int); else
if (eventData.TryGetPropertyValue("int", out var @int2)) writer.WriteVarInt((int)@int2); else writer.WriteVarInt(0);
if (data.TryGetPropertyValue("float", out var @float)) writer.WriteFloat((float)@float); else
if (eventData.TryGetPropertyValue("float", out var @float2)) writer.WriteFloat((float)@float2); else writer.WriteFloat(0);
if (data.TryGetPropertyValue("string", out var @string))
{
writer.WriteBoolean(true);
writer.WriteString((string)@string);
}
else
{
writer.WriteBoolean(false);
}
if (eventData.ContainsKey("audio"))
{
if (data.TryGetPropertyValue("volume", out var volume)) writer.WriteFloat((float)volume); else
if (eventData.TryGetPropertyValue("volume", out var volume2)) writer.WriteFloat((float)volume2); else writer.WriteFloat(1);
if (data.TryGetPropertyValue("balance", out var balance)) writer.WriteFloat((float)balance); else
if (eventData.TryGetPropertyValue("balance", out var balance2)) writer.WriteFloat((float)balance2); else writer.WriteFloat(0);
}
}
}
@@ -1234,7 +1616,7 @@ namespace SpineViewer.Spine.Implementations.SkeletonConverter
public void WriteShortArray(JsonArray array)
{
writer.WriteVarInt(array.Count);
foreach (uint i in array)
foreach (int i in array)
{
writer.WriteByte((byte)(i >> 8));
writer.WriteByte((byte)i);
@@ -1267,6 +1649,29 @@ namespace SpineViewer.Spine.Implementations.SkeletonConverter
}
}
private void WriteCurve(JsonObject obj)
{
if (obj.TryGetPropertyValue("curve", out var curve))
{
if (curve.GetValueKind() == JsonValueKind.String)
{
writer.WriteByte(SkeletonBinary.CURVE_STEPPED);
}
else
{
writer.WriteByte(SkeletonBinary.CURVE_BEZIER);
writer.WriteFloat((float)curve);
if (obj.TryGetPropertyValue("c2", out var c2)) writer.WriteFloat((float)c2); else writer.WriteFloat(0);
if (obj.TryGetPropertyValue("c3", out var c3)) writer.WriteFloat((float)c3); else writer.WriteFloat(1);
if (obj.TryGetPropertyValue("c4", out var c4)) writer.WriteFloat((float)c4); else writer.WriteFloat(1);
}
}
else
{
writer.WriteByte(SkeletonBinary.CURVE_LINEAR);
}
}
public override JsonObject ReadJson(string jsonPath)
{
// replace 3.8.75 to another version to avoid detection in official runtime

View File

@@ -7,6 +7,7 @@ using System.Linq;
using System.Text;
using System.Threading.Tasks;
using SpineRuntime21;
using SpineViewer.Extensions;
using SpineViewer.Utils;
namespace SpineViewer.Spine.Implementations.SpineObject
@@ -14,7 +15,17 @@ namespace SpineViewer.Spine.Implementations.SpineObject
[SpineImplementation(SpineVersion.V21)]
internal class SpineObject21 : Spine.SpineObject
{
private static readonly Animation EmptyAnimation = new(EMPTY_ANIMATION, [], 0);
//private static SFML.Graphics.BlendMode GetSFMLBlendMode(BlendMode spineBlendMode)
//{
// return spineBlendMode switch
// {
// BlendMode.Normal => BlendMode.Normal,
// BlendMode.Additive => BlendMode.Additive,
// BlendMode.Multiply => BlendMode.Multiply,
// BlendMode.Screen => BlendMode.Screen,
// _ => throw new NotImplementedException($"{spineBlendMode}"),
// };
//}
private class TextureLoader : SpineRuntime21.TextureLoader
{
@@ -34,11 +45,13 @@ namespace SpineViewer.Spine.Implementations.SpineObject
((SFML.Graphics.Texture)texture).Dispose();
}
}
private static TextureLoader textureLoader = new();
private Atlas atlas;
private SkeletonBinary? skeletonBinary;
private SkeletonJson? skeletonJson;
private readonly static TextureLoader textureLoader = new();
private static readonly Animation EmptyAnimation = new(EMPTY_ANIMATION, [], 0);
private readonly Atlas atlas;
private readonly SkeletonBinary? skeletonBinary;
private readonly SkeletonJson? skeletonJson;
private SkeletonData skeletonData;
private AnimationStateData animationStateData;
@@ -80,7 +93,7 @@ namespace SpineViewer.Spine.Implementations.SpineObject
foreach (var anime in skeletonData.Animations)
animationNames.Add(anime.Name);
skeleton = new Skeleton(skeletonData);
skeleton = new Skeleton(skeletonData) { Skin = new(Guid.NewGuid().ToString()) }; // 挂载一个空皮肤当作容器
animationStateData = new AnimationStateData(skeletonData);
animationState = new AnimationState(animationStateData);
}
@@ -161,14 +174,18 @@ namespace SpineViewer.Spine.Implementations.SpineObject
protected override void addSkin(string name)
{
if (!skinNames.Contains(name)) return;
skeleton.SetSkin(name); // XXX: 3.7 及以下不支持 AddSkin
if (skeletonData.FindSkin(name) is Skin sk)
{
// XXX: 3.7 及以下不支持 AddSkin
foreach (var (k, v) in sk.Attachments)
skeleton.Skin.AddAttachment(k.Key, k.Value, v);
}
skeleton.SetSlotsToSetupPose();
}
protected override void clearSkin()
{
skeleton.SetSkin(skeletonData.DefaultSkin);
skeleton.Skin.Attachments.Clear();
skeleton.SetSlotsToSetupPose();
}
@@ -188,54 +205,53 @@ namespace SpineViewer.Spine.Implementations.SpineObject
public override float GetAnimationDuration(string name) { return skeletonData.FindAnimation(name)?.Duration ?? 0f; }
protected override RectangleF bounds
protected override RectangleF getCurrentBounds()
{
get
{
float[] temp = new float[8];
var drawOrderItems = skeleton.DrawOrder;
float minX = int.MaxValue, minY = int.MaxValue, maxX = int.MinValue, maxY = int.MinValue;
for (int i = 0, n = skeleton.DrawOrder.Count; i < n; i++)
{
Slot slot = drawOrderItems[i];
int verticesLength = 0;
float[] vertices = null;
Attachment attachment = slot.Attachment;
var regionAttachment = attachment as RegionAttachment;
if (regionAttachment != null)
{
verticesLength = 8;
vertices = temp;
if (vertices.Length < 8) vertices = temp = new float[8];
regionAttachment.ComputeWorldVertices(slot.Bone, temp);
}
else
{
var meshAttachment = attachment as MeshAttachment;
if (meshAttachment != null)
{
MeshAttachment mesh = meshAttachment;
verticesLength = mesh.Vertices.Length;
vertices = temp;
if (vertices.Length < verticesLength) vertices = temp = new float[verticesLength];
mesh.ComputeWorldVertices(slot, temp);
}
}
skeleton.GetBounds(out var x, out var y, out var w, out var h);
return new RectangleF(x, y, w, h);
}
if (vertices != null)
{
for (int ii = 0; ii < verticesLength; ii += 2)
{
float vx = vertices[ii], vy = vertices[ii + 1];
minX = Math.Min(minX, vx);
minY = Math.Min(minY, vy);
maxX = Math.Max(maxX, vx);
maxY = Math.Max(maxY, vy);
}
}
}
return new RectangleF(minX, minY, maxX - minX, maxY - minY);
protected override RectangleF getBounds()
{
// 初始化临时对象
var maxDuration = 0f;
var tmpSkeleton = new Skeleton(skeletonData) { Skin = new(Guid.NewGuid().ToString()) };
var tmpAnimationState = new AnimationState(animationStateData);
tmpSkeleton.FlipX = skeleton.FlipX;
tmpSkeleton.FlipY = skeleton.FlipY;
tmpSkeleton.X = skeleton.X;
tmpSkeleton.Y = skeleton.Y;
foreach (var name in loadedSkins)
{
foreach (var (k, v) in skeletonData.FindSkin(name).Attachments)
tmpSkeleton.Skin.AddAttachment(k.Key, k.Value, v);
}
foreach (var tr in animationState.Tracks.Select((_, i) => i).Where(i => animationState.Tracks[i] is not null))
{
var ani = animationState.GetCurrent(tr).Animation;
tmpAnimationState.SetAnimation(tr, ani, true);
if (ani.Duration > maxDuration) maxDuration = ani.Duration;
}
tmpSkeleton.SetSlotsToSetupPose();
tmpAnimationState.Update(0);
tmpAnimationState.Apply(tmpSkeleton);
tmpSkeleton.Update(0);
tmpSkeleton.UpdateWorldTransform();
// 按 10 帧每秒计算边框
var bounds = getCurrentBounds();
float[] _ = [];
for (float tick = 0, delta = 0.1f; tick < maxDuration; tick += delta)
{
tmpSkeleton.GetBounds(out var x, out var y, out var w, out var h);
bounds = bounds.Union(new(x, y, w, h));
tmpAnimationState.Update(delta);
tmpAnimationState.Apply(tmpSkeleton);
tmpSkeleton.Update(delta);
tmpSkeleton.UpdateWorldTransform();
}
return bounds;
}
protected override void update(float delta)
@@ -246,18 +262,6 @@ namespace SpineViewer.Spine.Implementations.SpineObject
skeleton.UpdateWorldTransform();
}
//private SFML.Graphics.BlendMode GetSFMLBlendMode(BlendMode spineBlendMode)
//{
// return spineBlendMode switch
// {
// BlendMode.Normal => BlendMode.Normal,
// BlendMode.Additive => BlendMode.Additive,
// BlendMode.Multiply => BlendMode.Multiply,
// BlendMode.Screen => BlendMode.Screen,
// _ => throw new NotImplementedException($"{spineBlendMode}"),
// };
//}
protected override void draw(SFML.Graphics.RenderTarget target, SFML.Graphics.RenderStates states)
{
triangleVertices.Clear();
@@ -503,7 +507,7 @@ namespace SpineViewer.Spine.Implementations.SpineObject
if (debugBounds)
{
var vt = new SFML.Graphics.Vertex() { Color = BoundsColor };
var b = bounds;
var b = getCurrentBounds();
vt.Position.X = b.Left;
vt.Position.Y = b.Top;

View File

@@ -7,6 +7,7 @@ using System.Linq;
using System.Text;
using System.Threading.Tasks;
using SpineRuntime36;
using SpineViewer.Extensions;
using SpineViewer.Utils;
namespace SpineViewer.Spine.Implementations.SpineObject
@@ -14,7 +15,17 @@ namespace SpineViewer.Spine.Implementations.SpineObject
[SpineImplementation(SpineVersion.V36)]
internal class SpineObject36 : Spine.SpineObject
{
private static readonly Animation EmptyAnimation = new(EMPTY_ANIMATION, [], 0);
private static SFML.Graphics.BlendMode GetSFMLBlendMode(BlendMode spineBlendMode)
{
return spineBlendMode switch
{
BlendMode.Normal => SFMLBlendMode.NormalPma,
BlendMode.Additive => SFMLBlendMode.AdditivePma,
BlendMode.Multiply => SFMLBlendMode.MultiplyPma,
BlendMode.Screen => SFMLBlendMode.ScreenPma,
_ => throw new NotImplementedException($"{spineBlendMode}"),
};
}
private class TextureLoader : SpineRuntime36.TextureLoader
{
@@ -34,18 +45,20 @@ namespace SpineViewer.Spine.Implementations.SpineObject
((SFML.Graphics.Texture)texture).Dispose();
}
}
private static TextureLoader textureLoader = new();
private Atlas atlas;
private SkeletonBinary? skeletonBinary;
private SkeletonJson? skeletonJson;
private static readonly TextureLoader textureLoader = new();
private static readonly Animation EmptyAnimation = new(EMPTY_ANIMATION, [], 0);
private readonly Atlas atlas;
private readonly SkeletonBinary? skeletonBinary;
private readonly SkeletonJson? skeletonJson;
private SkeletonData skeletonData;
private AnimationStateData animationStateData;
private Skeleton skeleton;
private AnimationState animationState;
private SkeletonClipping clipping = new();
private readonly SkeletonClipping clipping = new();
public SpineObject36(string skelPath, string atlasPath) : base(skelPath, atlasPath)
{
@@ -79,7 +92,7 @@ namespace SpineViewer.Spine.Implementations.SpineObject
foreach (var anime in skeletonData.Animations)
animationNames.Add(anime.Name);
skeleton = new Skeleton(skeletonData);
skeleton = new Skeleton(skeletonData) { Skin = new(Guid.NewGuid().ToString()) }; // 挂载一个空皮肤当作容器
animationStateData = new AnimationStateData(skeletonData);
animationState = new AnimationState(animationStateData);
}
@@ -160,14 +173,18 @@ namespace SpineViewer.Spine.Implementations.SpineObject
protected override void addSkin(string name)
{
if (!skinNames.Contains(name)) return;
skeleton.SetSkin(name); // XXX: 3.7 及以下不支持 AddSkin
if (skeletonData.FindSkin(name) is Skin sk)
{
// XXX: 3.7 及以下不支持 AddSkin
foreach (var (k, v) in sk.Attachments)
skeleton.Skin.AddAttachment(k.slotIndex, k.name, v);
}
skeleton.SetSlotsToSetupPose();
}
protected override void clearSkin()
{
skeleton.SetSkin(skeletonData.DefaultSkin);
skeleton.Skin.Attachments.Clear();
skeleton.SetSlotsToSetupPose();
}
@@ -187,14 +204,54 @@ namespace SpineViewer.Spine.Implementations.SpineObject
public override float GetAnimationDuration(string name) { return skeletonData.FindAnimation(name)?.Duration ?? 0f; }
protected override RectangleF bounds
protected override RectangleF getCurrentBounds()
{
get
float[] _ = [];
skeleton.GetBounds(out var x, out var y, out var w, out var h, ref _);
return new RectangleF(x, y, w, h);
}
protected override RectangleF getBounds()
{
// 初始化临时对象
var maxDuration = 0f;
var tmpSkeleton = new Skeleton(skeletonData) { Skin = new(Guid.NewGuid().ToString()) };
var tmpAnimationState = new AnimationState(animationStateData);
tmpSkeleton.FlipX = skeleton.FlipX;
tmpSkeleton.FlipY = skeleton.FlipY;
tmpSkeleton.X = skeleton.X;
tmpSkeleton.Y = skeleton.Y;
foreach (var name in loadedSkins)
{
float[] _ = [];
skeleton.GetBounds(out var x, out var y, out var w, out var h, ref _);
return new RectangleF(x, y, w, h);
foreach (var (k, v) in skeletonData.FindSkin(name).Attachments)
tmpSkeleton.Skin.AddAttachment(k.slotIndex, k.name, v);
}
foreach (var tr in animationState.Tracks.Select((_, i) => i).Where(i => animationState.Tracks.Items[i] is not null))
{
var ani = animationState.GetCurrent(tr).Animation;
tmpAnimationState.SetAnimation(tr, ani, true);
if (ani.Duration > maxDuration) maxDuration = ani.Duration;
}
tmpSkeleton.SetSlotsToSetupPose();
tmpAnimationState.Update(0);
tmpAnimationState.Apply(tmpSkeleton);
tmpSkeleton.Update(0);
tmpSkeleton.UpdateWorldTransform();
// 按 10 帧每秒计算边框
var bounds = getCurrentBounds();
float[] _ = [];
for (float tick = 0, delta = 0.1f; tick < maxDuration; tick += delta)
{
tmpSkeleton.GetBounds(out var x, out var y, out var w, out var h, ref _);
bounds = bounds.Union(new(x, y, w, h));
tmpAnimationState.Update(delta);
tmpAnimationState.Apply(tmpSkeleton);
tmpSkeleton.Update(delta);
tmpSkeleton.UpdateWorldTransform();
}
return bounds;
}
protected override void update(float delta)
@@ -205,18 +262,6 @@ namespace SpineViewer.Spine.Implementations.SpineObject
skeleton.UpdateWorldTransform();
}
private SFML.Graphics.BlendMode GetSFMLBlendMode(BlendMode spineBlendMode)
{
return spineBlendMode switch
{
BlendMode.Normal => SFMLBlendMode.NormalPma,
BlendMode.Additive => SFMLBlendMode.AdditivePma,
BlendMode.Multiply => SFMLBlendMode.MultiplyPma,
BlendMode.Screen => SFMLBlendMode.ScreenPma,
_ => throw new NotImplementedException($"{spineBlendMode}"),
};
}
protected override void draw(SFML.Graphics.RenderTarget target, SFML.Graphics.RenderStates states)
{
triangleVertices.Clear();
@@ -489,7 +534,7 @@ namespace SpineViewer.Spine.Implementations.SpineObject
if (debugBounds)
{
var vt = new SFML.Graphics.Vertex() { Color = BoundsColor };
var b = bounds;
var b = getCurrentBounds();
vt.Position.X = b.Left;
vt.Position.Y = b.Top;

View File

@@ -4,6 +4,7 @@ using System.Linq;
using System.Text;
using System.Threading.Tasks;
using SpineRuntime37;
using SpineViewer.Extensions;
using SpineViewer.Utils;
namespace SpineViewer.Spine.Implementations.SpineObject
@@ -11,7 +12,17 @@ namespace SpineViewer.Spine.Implementations.SpineObject
[SpineImplementation(SpineVersion.V37)]
internal class SpineObject37 : Spine.SpineObject
{
private static readonly Animation EmptyAnimation = new(EMPTY_ANIMATION, [], 0);
private static SFML.Graphics.BlendMode GetSFMLBlendMode(BlendMode spineBlendMode)
{
return spineBlendMode switch
{
BlendMode.Normal => SFMLBlendMode.NormalPma,
BlendMode.Additive => SFMLBlendMode.AdditivePma,
BlendMode.Multiply => SFMLBlendMode.MultiplyPma,
BlendMode.Screen => SFMLBlendMode.ScreenPma,
_ => throw new NotImplementedException($"{spineBlendMode}"),
};
}
private class TextureLoader : SpineRuntime37.TextureLoader
{
@@ -32,18 +43,19 @@ namespace SpineViewer.Spine.Implementations.SpineObject
}
}
private static TextureLoader textureLoader = new();
private static readonly TextureLoader textureLoader = new();
private static readonly Animation EmptyAnimation = new(EMPTY_ANIMATION, [], 0);
private Atlas atlas;
private SkeletonBinary? skeletonBinary;
private SkeletonJson? skeletonJson;
private SkeletonData skeletonData;
private AnimationStateData animationStateData;
private readonly Atlas atlas;
private readonly SkeletonBinary? skeletonBinary;
private readonly SkeletonJson? skeletonJson;
private readonly SkeletonData skeletonData;
private readonly AnimationStateData animationStateData;
private Skeleton skeleton;
private AnimationState animationState;
private readonly Skeleton skeleton;
private readonly AnimationState animationState;
private SkeletonClipping clipping = new();
private readonly SkeletonClipping clipping = new();
public SpineObject37(string skelPath, string atlasPath) : base(skelPath, atlasPath)
{
@@ -77,7 +89,7 @@ namespace SpineViewer.Spine.Implementations.SpineObject
foreach (var anime in skeletonData.Animations)
animationNames.Add(anime.Name);
skeleton = new Skeleton(skeletonData);
skeleton = new Skeleton(skeletonData) { Skin = new(Guid.NewGuid().ToString()) }; // 挂载一个空皮肤当作容器
animationStateData = new AnimationStateData(skeletonData);
animationState = new AnimationState(animationStateData);
}
@@ -132,14 +144,18 @@ namespace SpineViewer.Spine.Implementations.SpineObject
protected override void addSkin(string name)
{
if (!skinNames.Contains(name)) return;
skeleton.SetSkin(name); // XXX: 3.7 及以下不支持 AddSkin
if (skeletonData.FindSkin(name) is Skin sk)
{
// XXX: 3.7 及以下不支持 AddSkin
foreach (var (k, v) in sk.Attachments)
skeleton.Skin.AddAttachment(k.slotIndex, k.name, v);
}
skeleton.SetSlotsToSetupPose();
}
protected override void clearSkin()
{
skeleton.SetSkin(skeletonData.DefaultSkin);
skeleton.Skin.Attachments.Clear();
skeleton.SetSlotsToSetupPose();
}
@@ -159,14 +175,54 @@ namespace SpineViewer.Spine.Implementations.SpineObject
public override float GetAnimationDuration(string name) { return skeletonData.FindAnimation(name)?.Duration ?? 0f; }
protected override RectangleF bounds
protected override RectangleF getCurrentBounds()
{
get
float[] _ = [];
skeleton.GetBounds(out var x, out var y, out var w, out var h, ref _);
return new RectangleF(x, y, w, h);
}
protected override RectangleF getBounds()
{
// 初始化临时对象
var maxDuration = 0f;
var tmpSkeleton = new Skeleton(skeletonData) { Skin = new(Guid.NewGuid().ToString()) };
var tmpAnimationState = new AnimationState(animationStateData);
tmpSkeleton.ScaleX = skeleton.ScaleX;
tmpSkeleton.ScaleY = skeleton.ScaleY;
tmpSkeleton.X = skeleton.X;
tmpSkeleton.Y = skeleton.Y;
foreach (var name in loadedSkins)
{
float[] _ = [];
skeleton.GetBounds(out var x, out var y, out var w, out var h, ref _);
return new RectangleF(x, y, w, h);
foreach (var (k, v) in skeletonData.FindSkin(name).Attachments)
tmpSkeleton.Skin.AddAttachment(k.slotIndex, k.name, v);
}
foreach (var tr in animationState.Tracks.Select((_, i) => i).Where(i => animationState.Tracks.Items[i] is not null))
{
var ani = animationState.GetCurrent(tr).Animation;
tmpAnimationState.SetAnimation(tr, ani, true);
if (ani.Duration > maxDuration) maxDuration = ani.Duration;
}
tmpSkeleton.SetSlotsToSetupPose();
tmpAnimationState.Update(0);
tmpAnimationState.Apply(tmpSkeleton);
tmpSkeleton.Update(0);
tmpSkeleton.UpdateWorldTransform();
// 按 10 帧每秒计算边框
var bounds = getCurrentBounds();
float[] _ = [];
for (float tick = 0, delta = 0.1f; tick < maxDuration; tick += delta)
{
tmpSkeleton.GetBounds(out var x, out var y, out var w, out var h, ref _);
bounds = bounds.Union(new(x, y, w, h));
tmpAnimationState.Update(delta);
tmpAnimationState.Apply(tmpSkeleton);
tmpSkeleton.Update(delta);
tmpSkeleton.UpdateWorldTransform();
}
return bounds;
}
protected override void update(float delta)
@@ -177,18 +233,6 @@ namespace SpineViewer.Spine.Implementations.SpineObject
skeleton.UpdateWorldTransform();
}
private SFML.Graphics.BlendMode GetSFMLBlendMode(BlendMode spineBlendMode)
{
return spineBlendMode switch
{
BlendMode.Normal => SFMLBlendMode.NormalPma,
BlendMode.Additive => SFMLBlendMode.AdditivePma,
BlendMode.Multiply => SFMLBlendMode.MultiplyPma,
BlendMode.Screen => SFMLBlendMode.ScreenPma,
_ => throw new NotImplementedException($"{spineBlendMode}"),
};
}
protected override void draw(SFML.Graphics.RenderTarget target, SFML.Graphics.RenderStates states)
{
triangleVertices.Clear();
@@ -461,7 +505,7 @@ namespace SpineViewer.Spine.Implementations.SpineObject
if (debugBounds)
{
var vt = new SFML.Graphics.Vertex() { Color = BoundsColor };
var b = bounds;
var b = getCurrentBounds();
vt.Position.X = b.Left;
vt.Position.Y = b.Top;

View File

@@ -5,8 +5,10 @@ using System.IO;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Xml.Linq;
using SpineRuntime38;
using SpineRuntime38.Attachments;
using SpineViewer.Extensions;
using SpineViewer.Utils;
namespace SpineViewer.Spine.Implementations.SpineObject
@@ -14,7 +16,17 @@ namespace SpineViewer.Spine.Implementations.SpineObject
[SpineImplementation(SpineVersion.V38)]
internal class SpineObject38 : Spine.SpineObject
{
private static readonly Animation EmptyAnimation = new(EMPTY_ANIMATION, [], 0);
private static SFML.Graphics.BlendMode GetSFMLBlendMode(BlendMode spineBlendMode)
{
return spineBlendMode switch
{
BlendMode.Normal => SFMLBlendMode.NormalPma,
BlendMode.Additive => SFMLBlendMode.AdditivePma,
BlendMode.Multiply => SFMLBlendMode.MultiplyPma,
BlendMode.Screen => SFMLBlendMode.ScreenPma,
_ => throw new NotImplementedException($"{spineBlendMode}"),
};
}
private class TextureLoader : SpineRuntime38.TextureLoader
{
@@ -38,18 +50,19 @@ namespace SpineViewer.Spine.Implementations.SpineObject
}
}
private static TextureLoader textureLoader = new();
private static readonly TextureLoader textureLoader = new();
private static readonly Animation EmptyAnimation = new(EMPTY_ANIMATION, [], 0);
private Atlas atlas;
private SkeletonBinary? skeletonBinary;
private SkeletonJson? skeletonJson;
private SkeletonData skeletonData;
private AnimationStateData animationStateData;
private readonly Atlas atlas;
private readonly SkeletonBinary? skeletonBinary;
private readonly SkeletonJson? skeletonJson;
private readonly SkeletonData skeletonData;
private readonly AnimationStateData animationStateData;
private Skeleton skeleton;
private AnimationState animationState;
private readonly Skeleton skeleton;
private readonly AnimationState animationState;
private SkeletonClipping clipping = new();
private readonly SkeletonClipping clipping = new();
public SpineObject38(string skelPath, string atlasPath) : base(skelPath, atlasPath)
{
@@ -70,10 +83,10 @@ namespace SpineViewer.Spine.Implementations.SpineObject
skeletonJson = new SkeletonJson(atlas);
skeletonData = skeletonJson.ReadSkeletonData(SkelPath);
}
catch
catch (Exception ex)
{
// 都不行就报错
throw new InvalidDataException($"Unknown skeleton file format {SkelPath}");
throw new InvalidDataException($"Unknown skeleton file format {SkelPath}", ex);
}
}
@@ -167,14 +180,50 @@ namespace SpineViewer.Spine.Implementations.SpineObject
public override float GetAnimationDuration(string name) { return skeletonData.FindAnimation(name)?.Duration ?? 0f; }
protected override RectangleF bounds
protected override RectangleF getCurrentBounds()
{
get
float[] _ = [];
skeleton.GetBounds(out var x, out var y, out var w, out var h, ref _);
return new RectangleF(x, y, w, h);
}
protected override RectangleF getBounds()
{
// 初始化临时对象
var maxDuration = 0f;
var tmpSkeleton = new Skeleton(skeletonData) { Skin = new(Guid.NewGuid().ToString()) };
var tmpAnimationState = new AnimationState(animationStateData);
tmpSkeleton.ScaleX = skeleton.ScaleX;
tmpSkeleton.ScaleY = skeleton.ScaleY;
tmpSkeleton.X = skeleton.X;
tmpSkeleton.Y = skeleton.Y;
foreach (var sk in loadedSkins) tmpSkeleton.Skin.AddSkin(skeletonData.FindSkin(sk));
foreach (var tr in animationState.Tracks.Select((_, i) => i).Where(i => animationState.Tracks.Items[i] is not null))
{
float[] _ = [];
skeleton.GetBounds(out var x, out var y, out var w, out var h, ref _);
return new RectangleF(x, y, w, h);
var ani = animationState.GetCurrent(tr).Animation;
tmpAnimationState.SetAnimation(tr, ani, true);
if (ani.Duration > maxDuration) maxDuration = ani.Duration;
}
tmpSkeleton.SetSlotsToSetupPose();
tmpAnimationState.Update(0);
tmpAnimationState.Apply(tmpSkeleton);
tmpSkeleton.Update(0);
tmpSkeleton.UpdateWorldTransform();
// 按 10 帧每秒计算边框
var bounds = getCurrentBounds();
float[] _ = [];
for (float tick = 0, delta = 0.1f; tick < maxDuration; tick += delta)
{
tmpSkeleton.GetBounds(out var x, out var y, out var w, out var h, ref _);
bounds = bounds.Union(new(x, y, w, h));
tmpAnimationState.Update(delta);
tmpAnimationState.Apply(tmpSkeleton);
tmpSkeleton.Update(delta);
tmpSkeleton.UpdateWorldTransform();
}
return bounds;
}
protected override void update(float delta)
@@ -185,18 +234,6 @@ namespace SpineViewer.Spine.Implementations.SpineObject
skeleton.UpdateWorldTransform();
}
private SFML.Graphics.BlendMode GetSFMLBlendMode(BlendMode spineBlendMode)
{
return spineBlendMode switch
{
BlendMode.Normal => SFMLBlendMode.NormalPma,
BlendMode.Additive => SFMLBlendMode.AdditivePma,
BlendMode.Multiply => SFMLBlendMode.MultiplyPma,
BlendMode.Screen => SFMLBlendMode.ScreenPma,
_ => throw new NotImplementedException($"{spineBlendMode}"),
};
}
protected override void draw(SFML.Graphics.RenderTarget target, SFML.Graphics.RenderStates states)
{
triangleVertices.Clear();
@@ -469,7 +506,7 @@ namespace SpineViewer.Spine.Implementations.SpineObject
if (debugBounds)
{
var vt = new SFML.Graphics.Vertex() { Color = BoundsColor };
var b = bounds;
var b = getCurrentBounds();
vt.Position.X = b.Left;
vt.Position.Y = b.Top;

View File

@@ -6,6 +6,7 @@ using System.Linq;
using System.Text;
using System.Threading.Tasks;
using SpineRuntime40;
using SpineViewer.Extensions;
using SpineViewer.Utils;
namespace SpineViewer.Spine.Implementations.SpineObject
@@ -13,7 +14,17 @@ namespace SpineViewer.Spine.Implementations.SpineObject
[SpineImplementation(SpineVersion.V40)]
internal class SpineObject40 : Spine.SpineObject
{
private static readonly Animation EmptyAnimation = new(EMPTY_ANIMATION, [], 0);
private static SFML.Graphics.BlendMode GetSFMLBlendMode(BlendMode spineBlendMode)
{
return spineBlendMode switch
{
BlendMode.Normal => SFMLBlendMode.NormalPma,
BlendMode.Additive => SFMLBlendMode.AdditivePma,
BlendMode.Multiply => SFMLBlendMode.MultiplyPma,
BlendMode.Screen => SFMLBlendMode.ScreenPma,
_ => throw new NotImplementedException($"{spineBlendMode}"),
};
}
private class TextureLoader : SpineRuntime40.TextureLoader
{
@@ -34,18 +45,19 @@ namespace SpineViewer.Spine.Implementations.SpineObject
}
}
private static TextureLoader textureLoader = new();
private static readonly TextureLoader textureLoader = new();
private static readonly Animation EmptyAnimation = new(EMPTY_ANIMATION, [], 0);
private Atlas atlas;
private SkeletonBinary? skeletonBinary;
private SkeletonJson? skeletonJson;
private SkeletonData skeletonData;
private AnimationStateData animationStateData;
private readonly Atlas atlas;
private readonly SkeletonBinary? skeletonBinary;
private readonly SkeletonJson? skeletonJson;
private readonly SkeletonData skeletonData;
private readonly AnimationStateData animationStateData;
private Skeleton skeleton;
private AnimationState animationState;
private readonly Skeleton skeleton;
private readonly AnimationState animationState;
private SkeletonClipping clipping = new();
private readonly SkeletonClipping clipping = new();
public SpineObject40(string skelPath, string atlasPath) : base(skelPath, atlasPath)
{
@@ -163,14 +175,50 @@ namespace SpineViewer.Spine.Implementations.SpineObject
public override float GetAnimationDuration(string name) { return skeletonData.FindAnimation(name)?.Duration ?? 0f; }
protected override RectangleF bounds
protected override RectangleF getCurrentBounds()
{
get
float[] _ = [];
skeleton.GetBounds(out var x, out var y, out var w, out var h, ref _);
return new RectangleF(x, y, w, h);
}
protected override RectangleF getBounds()
{
// 初始化临时对象
var maxDuration = 0f;
var tmpSkeleton = new Skeleton(skeletonData) { Skin = new(Guid.NewGuid().ToString()) };
var tmpAnimationState = new AnimationState(animationStateData);
tmpSkeleton.ScaleX = skeleton.ScaleX;
tmpSkeleton.ScaleY = skeleton.ScaleY;
tmpSkeleton.X = skeleton.X;
tmpSkeleton.Y = skeleton.Y;
foreach (var sk in loadedSkins) tmpSkeleton.Skin.AddSkin(skeletonData.FindSkin(sk));
foreach (var tr in animationState.Tracks.Select((_, i) => i).Where(i => animationState.Tracks.Items[i] is not null))
{
float[] _ = [];
skeleton.GetBounds(out var x, out var y, out var w, out var h, ref _);
return new RectangleF(x, y, w, h);
var ani = animationState.GetCurrent(tr).Animation;
tmpAnimationState.SetAnimation(tr, ani, true);
if (ani.Duration > maxDuration) maxDuration = ani.Duration;
}
tmpSkeleton.SetSlotsToSetupPose();
tmpAnimationState.Update(0);
tmpAnimationState.Apply(tmpSkeleton);
tmpSkeleton.Update(0);
tmpSkeleton.UpdateWorldTransform();
// 按 10 帧每秒计算边框
var bounds = getCurrentBounds();
float[] _ = [];
for (float tick = 0, delta = 0.1f; tick < maxDuration; tick += delta)
{
tmpSkeleton.GetBounds(out var x, out var y, out var w, out var h, ref _);
bounds = bounds.Union(new(x, y, w, h));
tmpAnimationState.Update(delta);
tmpAnimationState.Apply(tmpSkeleton);
tmpSkeleton.Update(delta);
tmpSkeleton.UpdateWorldTransform();
}
return bounds;
}
protected override void update(float delta)
@@ -181,18 +229,6 @@ namespace SpineViewer.Spine.Implementations.SpineObject
skeleton.UpdateWorldTransform();
}
private SFML.Graphics.BlendMode GetSFMLBlendMode(BlendMode spineBlendMode)
{
return spineBlendMode switch
{
BlendMode.Normal => SFMLBlendMode.NormalPma,
BlendMode.Additive => SFMLBlendMode.AdditivePma,
BlendMode.Multiply => SFMLBlendMode.MultiplyPma,
BlendMode.Screen => SFMLBlendMode.ScreenPma,
_ => throw new NotImplementedException($"{spineBlendMode}"),
};
}
protected override void draw(SFML.Graphics.RenderTarget target, SFML.Graphics.RenderStates states)
{
triangleVertices.Clear();
@@ -465,7 +501,7 @@ namespace SpineViewer.Spine.Implementations.SpineObject
if (debugBounds)
{
var vt = new SFML.Graphics.Vertex() { Color = BoundsColor };
var b = bounds;
var b = getCurrentBounds();
vt.Position.X = b.Left;
vt.Position.Y = b.Top;

View File

@@ -6,6 +6,7 @@ using System.Linq;
using System.Text;
using System.Threading.Tasks;
using SpineRuntime41;
using SpineViewer.Extensions;
using SpineViewer.Utils;
namespace SpineViewer.Spine.Implementations.SpineObject
@@ -13,7 +14,17 @@ namespace SpineViewer.Spine.Implementations.SpineObject
[SpineImplementation(SpineVersion.V41)]
internal class SpineObject41 : Spine.SpineObject
{
private static readonly Animation EmptyAnimation = new(EMPTY_ANIMATION, [], 0);
private static SFML.Graphics.BlendMode GetSFMLBlendMode(BlendMode spineBlendMode)
{
return spineBlendMode switch
{
BlendMode.Normal => SFMLBlendMode.NormalPma,
BlendMode.Additive => SFMLBlendMode.AdditivePma,
BlendMode.Multiply => SFMLBlendMode.MultiplyPma,
BlendMode.Screen => SFMLBlendMode.ScreenPma,
_ => throw new NotImplementedException($"{spineBlendMode}"),
};
}
private class TextureLoader : SpineRuntime41.TextureLoader
{
@@ -35,17 +46,18 @@ namespace SpineViewer.Spine.Implementations.SpineObject
}
private static TextureLoader textureLoader = new();
private static readonly Animation EmptyAnimation = new(EMPTY_ANIMATION, [], 0);
private Atlas atlas;
private SkeletonBinary? skeletonBinary;
private SkeletonJson? skeletonJson;
private SkeletonData skeletonData;
private AnimationStateData animationStateData;
private readonly Atlas atlas;
private readonly SkeletonBinary? skeletonBinary;
private readonly SkeletonJson? skeletonJson;
private readonly SkeletonData skeletonData;
private readonly AnimationStateData animationStateData;
private Skeleton skeleton;
private AnimationState animationState;
private readonly Skeleton skeleton;
private readonly AnimationState animationState;
private SkeletonClipping clipping = new();
private readonly SkeletonClipping clipping = new();
public SpineObject41(string skelPath, string atlasPath) : base(skelPath, atlasPath)
{
@@ -163,14 +175,49 @@ namespace SpineViewer.Spine.Implementations.SpineObject
public override float GetAnimationDuration(string name) { return skeletonData.FindAnimation(name)?.Duration ?? 0f; }
protected override RectangleF bounds
protected override RectangleF getCurrentBounds()
{
get
float[] _ = [];
skeleton.GetBounds(out var x, out var y, out var w, out var h, ref _);
return new RectangleF(x, y, w, h);
}
protected override RectangleF getBounds()
{
// 初始化临时对象
var maxDuration = 0f;
var tmpSkeleton = new Skeleton(skeletonData) { Skin = new(Guid.NewGuid().ToString()) };
var tmpAnimationState = new AnimationState(animationStateData);
tmpSkeleton.ScaleX = skeleton.ScaleX;
tmpSkeleton.ScaleY = skeleton.ScaleY;
tmpSkeleton.X = skeleton.X;
tmpSkeleton.Y = skeleton.Y;
foreach (var sk in loadedSkins) tmpSkeleton.Skin.AddSkin(skeletonData.FindSkin(sk));
foreach (var tr in animationState.Tracks.Select((_, i) => i).Where(i => animationState.Tracks.Items[i] is not null))
{
float[] _ = [];
skeleton.GetBounds(out var x, out var y, out var w, out var h, ref _);
return new RectangleF(x, y, w, h);
var ani = animationState.GetCurrent(tr).Animation;
tmpAnimationState.SetAnimation(tr, ani, true);
if (ani.Duration > maxDuration) maxDuration = ani.Duration;
}
tmpSkeleton.SetSlotsToSetupPose();
tmpAnimationState.Update(0);
tmpAnimationState.Apply(tmpSkeleton);
//tmpSkeleton.Update(0);
tmpSkeleton.UpdateWorldTransform();
// 按 10 帧每秒计算边框
var bounds = getCurrentBounds();
float[] _ = [];
for (float tick = 0, delta = 0.1f; tick < maxDuration; tick += delta)
{
tmpSkeleton.GetBounds(out var x, out var y, out var w, out var h, ref _);
bounds = bounds.Union(new(x, y, w, h));
tmpAnimationState.Update(delta);
tmpAnimationState.Apply(tmpSkeleton);
tmpSkeleton.UpdateWorldTransform();
}
return bounds;
}
protected override void update(float delta)
@@ -181,18 +228,6 @@ namespace SpineViewer.Spine.Implementations.SpineObject
skeleton.UpdateWorldTransform();
}
private SFML.Graphics.BlendMode GetSFMLBlendMode(BlendMode spineBlendMode)
{
return spineBlendMode switch
{
BlendMode.Normal => SFMLBlendMode.NormalPma,
BlendMode.Additive => SFMLBlendMode.AdditivePma,
BlendMode.Multiply => SFMLBlendMode.MultiplyPma,
BlendMode.Screen => SFMLBlendMode.ScreenPma,
_ => throw new NotImplementedException($"{spineBlendMode}"),
};
}
protected override void draw(SFML.Graphics.RenderTarget target, SFML.Graphics.RenderStates states)
{
triangleVertices.Clear();
@@ -465,7 +500,7 @@ namespace SpineViewer.Spine.Implementations.SpineObject
if (debugBounds)
{
var vt = new SFML.Graphics.Vertex() { Color = BoundsColor };
var b = bounds;
var b = getCurrentBounds();
vt.Position.X = b.Left;
vt.Position.Y = b.Top;

View File

@@ -6,6 +6,7 @@ using System.Linq;
using System.Text;
using System.Threading.Tasks;
using SpineRuntime42;
using SpineViewer.Extensions;
using SpineViewer.Utils;
namespace SpineViewer.Spine.Implementations.SpineObject
@@ -13,7 +14,17 @@ namespace SpineViewer.Spine.Implementations.SpineObject
[SpineImplementation(SpineVersion.V42)]
internal class Spineobject42 : Spine.SpineObject
{
private static readonly Animation EmptyAnimation = new(EMPTY_ANIMATION, [], 0);
private SFML.Graphics.BlendMode GetSFMLBlendMode(BlendMode spineBlendMode)
{
return spineBlendMode switch
{
BlendMode.Normal => SFMLBlendMode.NormalPma,
BlendMode.Additive => SFMLBlendMode.AdditivePma,
BlendMode.Multiply => SFMLBlendMode.MultiplyPma,
BlendMode.Screen => SFMLBlendMode.ScreenPma,
_ => throw new NotImplementedException($"{spineBlendMode}"),
};
}
private class TextureLoader : SpineRuntime42.TextureLoader
{
@@ -34,18 +45,19 @@ namespace SpineViewer.Spine.Implementations.SpineObject
}
}
private static TextureLoader textureLoader = new();
private static readonly TextureLoader textureLoader = new();
private static readonly Animation EmptyAnimation = new(EMPTY_ANIMATION, [], 0);
private Atlas atlas;
private SkeletonBinary? skeletonBinary;
private SkeletonJson? skeletonJson;
private SkeletonData skeletonData;
private AnimationStateData animationStateData;
private readonly Atlas atlas;
private readonly SkeletonBinary? skeletonBinary;
private readonly SkeletonJson? skeletonJson;
private readonly SkeletonData skeletonData;
private readonly AnimationStateData animationStateData;
private Skeleton skeleton;
private AnimationState animationState;
private readonly Skeleton skeleton;
private readonly AnimationState animationState;
private SkeletonClipping clipping = new();
private readonly SkeletonClipping clipping = new();
public Spineobject42(string skelPath, string atlasPath) : base(skelPath, atlasPath)
{
@@ -163,14 +175,50 @@ namespace SpineViewer.Spine.Implementations.SpineObject
public override float GetAnimationDuration(string name) { return skeletonData.FindAnimation(name)?.Duration ?? 0f; }
protected override RectangleF bounds
protected override RectangleF getCurrentBounds()
{
get
float[] _ = [];
skeleton.GetBounds(out var x, out var y, out var w, out var h, ref _);
return new RectangleF(x, y, w, h);
}
protected override RectangleF getBounds()
{
// 初始化临时对象
var maxDuration = 0f;
var tmpSkeleton = new Skeleton(skeletonData) { Skin = new(Guid.NewGuid().ToString()) };
var tmpAnimationState = new AnimationState(animationStateData);
tmpSkeleton.ScaleX = skeleton.ScaleX;
tmpSkeleton.ScaleY = skeleton.ScaleY;
tmpSkeleton.X = skeleton.X;
tmpSkeleton.Y = skeleton.Y;
foreach (var sk in loadedSkins) tmpSkeleton.Skin.AddSkin(skeletonData.FindSkin(sk));
foreach (var tr in animationState.Tracks.Select((_, i) => i).Where(i => animationState.Tracks.Items[i] is not null))
{
float[] _ = [];
skeleton.GetBounds(out var x, out var y, out var w, out var h, ref _);
return new RectangleF(x, y, w, h);
var ani = animationState.GetCurrent(tr).Animation;
tmpAnimationState.SetAnimation(tr, ani, true);
if (ani.Duration > maxDuration) maxDuration = ani.Duration;
}
tmpSkeleton.SetSlotsToSetupPose();
tmpAnimationState.Update(0);
tmpAnimationState.Apply(tmpSkeleton);
tmpSkeleton.Update(0);
tmpSkeleton.UpdateWorldTransform(Skeleton.Physics.Update);
// 按 10 帧每秒计算边框
var bounds = getCurrentBounds();
float[] _ = [];
for (float tick = 0, delta = 0.1f; tick < maxDuration; tick += delta)
{
tmpSkeleton.GetBounds(out var x, out var y, out var w, out var h, ref _);
bounds = bounds.Union(new(x, y, w, h));
tmpAnimationState.Update(delta);
tmpAnimationState.Apply(tmpSkeleton);
tmpSkeleton.Update(delta);
tmpSkeleton.UpdateWorldTransform(Skeleton.Physics.Update);
}
return bounds;
}
protected override void update(float delta)
@@ -181,18 +229,6 @@ namespace SpineViewer.Spine.Implementations.SpineObject
skeleton.UpdateWorldTransform(Skeleton.Physics.Update);
}
private SFML.Graphics.BlendMode GetSFMLBlendMode(BlendMode spineBlendMode)
{
return spineBlendMode switch
{
BlendMode.Normal => SFMLBlendMode.NormalPma,
BlendMode.Additive => SFMLBlendMode.AdditivePma,
BlendMode.Multiply => SFMLBlendMode.MultiplyPma,
BlendMode.Screen => SFMLBlendMode.ScreenPma,
_ => throw new NotImplementedException($"{spineBlendMode}"),
};
}
protected override void draw(SFML.Graphics.RenderTarget target, SFML.Graphics.RenderStates states)
{
triangleVertices.Clear();
@@ -465,7 +501,7 @@ namespace SpineViewer.Spine.Implementations.SpineObject
if (debugBounds)
{
var vt = new SFML.Graphics.Vertex() { Color = BoundsColor };
var b = bounds;
var b = getCurrentBounds();
vt.Position.X = b.Left;
vt.Position.Y = b.Top;

View File

@@ -25,11 +25,26 @@ namespace SpineViewer.Spine.SpineExporter
/// <summary>
/// 可用于文件名的时间戳字符串
/// </summary>
protected readonly string timestamp = DateTime.Now.ToString("yyMMddHHmmss");
protected string timestamp = DateTime.Now.ToString("yyMMddHHmmss");
/// <summary>
/// 非自动分辨率下导出视区缓存
/// </summary>
private SFML.Graphics.View? exportViewCache = null;
/// <summary>
/// 模型分辨率缓存
/// </summary>
private readonly Dictionary<string, Size> spineResolutionCache = [];
/// <summary>
/// 自动分辨率下每个模型的导出视区缓存
/// </summary>
private readonly Dictionary<string, SFML.Graphics.View> spineViewCache = [];
~Exporter() { Dispose(false); }
public void Dispose() { Dispose(true); GC.SuppressFinalize(this); }
protected virtual void Dispose(bool disposing) { View.Dispose(); }
protected virtual void Dispose(bool disposing) { PreviewerView.Dispose(); }
/// <summary>
/// 输出文件夹
@@ -47,10 +62,10 @@ namespace SpineViewer.Spine.SpineExporter
public Size Resolution { get; set; } = new(100, 100);
/// <summary>
/// 渲染视窗, 接管对象生命周期
/// 预览画面的视区
/// </summary>
public SFML.Graphics.View View { get => view; set { view.Dispose(); view = value; } }
private SFML.Graphics.View view = new();
public SFML.Graphics.View PreviewerView { get => previewerView; set { previewerView.Dispose(); previewerView = new(value); } }
private SFML.Graphics.View previewerView = new();
/// <summary>
/// 是否仅渲染选中
@@ -82,14 +97,105 @@ namespace SpineViewer.Spine.SpineExporter
public SFML.Graphics.Color BackgroundColorPma { get; private set; } = SFML.Graphics.Color.Transparent;
/// <summary>
/// 获取供渲染的 SFML.Graphics.RenderTexture
/// 四周边缘距离, 单位为像素
/// </summary>
private SFML.Graphics.RenderTexture GetRenderTexture()
public Padding Margin
{
var tex = new SFML.Graphics.RenderTexture((uint)Resolution.Width, (uint)Resolution.Height);
tex.Clear(SFML.Graphics.Color.Transparent);
tex.SetView(View);
return tex;
get => margin;
set
{
if (value.Left < 0) value.Left = 0;
if (value.Right < 0) value.Right = 0;
if (value.Top < 0) value.Top = 0;
if (value.Bottom < 0) value.Bottom = 0;
margin = value;
}
}
private Padding margin = new(0);
/// <summary>
/// 四周填充距离, 单位为像素, 自动分辨率下忽略该值
/// </summary>
public Padding Padding
{
get => padding;
set
{
if (value.Left < 0) value.Left = 0;
if (value.Right < 0) value.Right = 0;
if (value.Top < 0) value.Top = 0;
if (value.Bottom < 0) value.Bottom = 0;
padding = value;
}
}
private Padding padding = new(0);
/// <summary>
/// 在使用预览画面分辨率的情况下, 允许内容溢出到边缘和填充区域, 自动分辨率下忽略该值
/// </summary>
public bool AllowContentOverflow { get; set; } = false;
/// <summary>
/// 自动分辨率, 将会忽略预览画面的分辨率和预览画面视区, 使用模型自身的包围盒, 四周填充和内容溢出会被忽略
/// </summary>
public bool AutoResolution { get; set; } = false;
/// <summary>
/// 获取导出渲染对象, 如果提供了模型列表则分辨率为模型大小, 否则是预览画面大小
/// </summary>
private SFML.Graphics.RenderTexture GetRenderTexture(SpineObject[]? spinesToRender = null)
{
if (spinesToRender is null)
{
if (exportViewCache is null)
{
// 记录缓存
exportViewCache = new SFML.Graphics.View(PreviewerView);
if (AllowContentOverflow)
{
var canvasBounds = exportViewCache.GetBounds().GetCanvasBounds(Resolution, Margin, Padding);
exportViewCache.Center = new(canvasBounds.X + canvasBounds.Width / 2, canvasBounds.Y + canvasBounds.Height / 2);
exportViewCache.Size = new(canvasBounds.Width, canvasBounds.Height);
}
else
{
exportViewCache.SetViewport(Resolution, Margin, Padding);
}
}
var tex = new SFML.Graphics.RenderTexture((uint)(Resolution.Width + Margin.Horizontal), (uint)(Resolution.Height + Margin.Vertical));
tex.SetView(exportViewCache);
return tex;
}
else
{
var cacheKey = string.Join("|", spinesToRender.Select(v => v.ID));
// 记录缓存
if (!spineViewCache.TryGetValue(cacheKey, out var spineView))
{
var spineBounds = spinesToRender[0].GetBounds();
foreach (var sp in spinesToRender.Skip(1))
spineBounds = spineBounds.Union(sp.GetBounds());
var spineResolution = new Size((int)Math.Ceiling(spineBounds.Width), (int)Math.Ceiling(spineBounds.Height));
var canvasBounds = spineBounds.GetCanvasBounds(spineResolution, Margin); // 忽略填充
spineResolutionCache[cacheKey] = spineResolution;
spineViewCache[cacheKey] = spineView = new SFML.Graphics.View(
new(canvasBounds.X + canvasBounds.Width / 2, canvasBounds.Y + canvasBounds.Height / 2),
new(canvasBounds.Width, -canvasBounds.Height)
);
logger.Info("Auto resolusion: ({}, {})", spineResolution.Width, spineResolution.Height);
}
var tex = new SFML.Graphics.RenderTexture(
(uint)(spineResolutionCache[cacheKey].Width + Margin.Horizontal),
(uint)(spineResolutionCache[cacheKey].Height + Margin.Vertical)
);
tex.SetView(spineViewCache[cacheKey]);
return tex;
}
}
/// <summary>
@@ -103,7 +209,7 @@ namespace SpineViewer.Spine.SpineExporter
protected SFMLImageVideoFrame GetFrame(SpineObject[] spinesToRender)
{
// RenderTexture 必须临时创建, 随用随取, 防止出现跨线程的情况
using var texPma = GetRenderTexture();
using var texPma = GetRenderTexture(AutoResolution ? spinesToRender : null);
// 先将预乘结果准确绘制出来, 注意背景色也应当是预乘的
texPma.Clear(BackgroundColorPma);
@@ -131,7 +237,7 @@ namespace SpineViewer.Spine.SpineExporter
st.Shader = SFMLShader.InversePma;
// 在最终结果上二次渲染非预乘画面
using var tex = GetRenderTexture();
using var tex = GetRenderTexture(AutoResolution ? spinesToRender : null);
// 将非预乘结果覆盖式绘制在目标对象上, 注意背景色应该用非预乘的
tex.Clear(BackgroundColor);
@@ -171,6 +277,15 @@ namespace SpineViewer.Spine.SpineExporter
return null;
}
private void ClearCache()
{
exportViewCache?.Dispose();
exportViewCache = null;
spineResolutionCache.Clear();
foreach (var v in spineViewCache.Values) v.Dispose();
spineViewCache.Clear();
}
/// <summary>
/// 执行导出
/// </summary>
@@ -179,13 +294,19 @@ namespace SpineViewer.Spine.SpineExporter
/// <exception cref="ArgumentException"></exception>
public virtual void Export(SpineObject[] spines, BackgroundWorker? worker = null)
{
if (Validate() is string err)
throw new ArgumentException(err);
if (Validate() is string err) throw new ArgumentException(err);
var spinesToRender = spines.Where(sp => !RenderSelectedOnly || sp.IsSelected).Reverse().ToArray();
if (spinesToRender.Length > 0)
{
ClearCache();
if (IsExportSingle) ExportSingle(spinesToRender, worker);
else ExportIndividual(spinesToRender, worker);
timestamp = DateTime.Now.ToString("yyMMddHHmmss"); // 刷新时间戳
if (IsExportSingle) ExportSingle(spinesToRender, worker);
else ExportIndividual(spinesToRender, worker);
ClearCache();
}
logger.LogCurrentProcessMemoryUsage();
}
@@ -220,10 +341,10 @@ namespace SpineViewer.Spine.SpineExporter
public Size Resolution { get => Exporter.Resolution; }
/// <summary>
/// 渲染视窗
/// 预览画面视区
/// </summary>
[Category("[0] "), DisplayName(""), Description("")]
public SFML.Graphics.View View { get => Exporter.View; }
[Category("[0] "), DisplayName(""), Description("")]
public SFML.Graphics.View View { get => Exporter.PreviewerView; }
/// <summary>
/// 是否仅渲染选中
@@ -238,5 +359,31 @@ namespace SpineViewer.Spine.SpineExporter
[TypeConverter(typeof(SFMLColorConverter))]
[Category("[0] "), DisplayName(""), Description("使, #RRGGBBAA")]
public SFML.Graphics.Color BackgroundColor { get => Exporter.BackgroundColor; set => Exporter.BackgroundColor = value; }
/// <summary>
/// 四周边缘距离
/// </summary>
[TypeConverter(typeof(PaddingConverter))]
[Category("[0] "), DisplayName(""), Description(" (Margin), ")]
public Padding Margin { get => Exporter.Margin; set => Exporter.Margin = value; }
/// <summary>
/// 四周填充距离
/// </summary>
[TypeConverter(typeof(PaddingConverter))]
[Category("[0] "), DisplayName(""), Description(" (Padding), , ")]
public Padding Padding { get => Exporter.Padding; set => Exporter.Padding = value; }
/// <summary>
/// 允许内容溢出到边缘和填充区域
/// </summary>
[Category("[0] "), DisplayName(""), Description("使, ")]
public bool AllowContentOverflow { get => Exporter.AllowContentOverflow; set => Exporter.AllowContentOverflow = value; }
/// <summary>
/// 自动分辨率
/// </summary>
[Category("[0] "), DisplayName(""), Description(", ")]
public bool AutoResolution { get => Exporter.AutoResolution; set => Exporter.AutoResolution = value; }
}
}

View File

@@ -20,14 +20,9 @@ namespace SpineViewer.Spine
protected const string EMPTY_ANIMATION = "<Empty>";
/// <summary>
/// 预览图
/// 预览图像素大小
/// </summary>
protected const uint PREVIEW_WIDTH = 256;
/// <summary>
/// 预览图高
/// </summary>
protected const uint PREVIEW_HEIGHT = 256;
protected static readonly Size PreviewResolution = new(256, 256);
/// <summary>
/// 创建特定版本的 Spine
@@ -52,7 +47,6 @@ namespace SpineViewer.Spine
/// 日志器
/// </summary>
protected readonly Logger logger = LogManager.GetCurrentClassLogger();
private bool skinLoggerWarned = false;
/// <summary>
/// 构造函数
@@ -82,8 +76,12 @@ namespace SpineViewer.Spine
// 虽然两边不会同时调用 Draw, 但是死锁似乎和 Draw 函数有关
// 除此之外, 似乎还和 tex 的 Dispose 有关
// 如果不对 tex 进行 Dispose, 那么不管是否 Draw 都正常不会死锁
var tex = new SFML.Graphics.RenderTexture(PREVIEW_WIDTH, PREVIEW_HEIGHT);
using var view = bounds.GetView(PREVIEW_WIDTH, PREVIEW_HEIGHT);
var tex = new SFML.Graphics.RenderTexture((uint)PreviewResolution.Width, (uint)PreviewResolution.Height);
var bounds = getCurrentBounds().GetCanvasBounds(PreviewResolution);
using var view = new SFML.Graphics.View(
new(bounds.X + bounds.Width / 2, bounds.Y + bounds.Height / 2),
new(bounds.Width, -bounds.Height)
);
tex.SetView(view);
tex.Clear(SFML.Graphics.Color.Transparent);
tex.Draw(this);
@@ -163,12 +161,6 @@ namespace SpineViewer.Spine
public bool UsePma { get { lock (_lock) return usePma; } set { lock (_lock) usePma = value; } }
protected bool usePma = false;
/// <summary>
/// 骨骼包围盒
/// </summary>
public RectangleF Bounds { get { lock (_lock) return bounds; } }
protected abstract RectangleF bounds { get; }
/// <summary>
/// 缩放比例
/// </summary>
@@ -321,6 +313,16 @@ namespace SpineViewer.Spine
}
protected bool debugPaths = false;
/// <summary>
/// 显示点附件
/// </summary>
public bool DebugPoints
{
get { lock (_lock) return debugPoints; }
set { lock (_lock) { debugPoints = value; update(0); } }
}
protected bool debugPoints = false;
/// <summary>
/// 显示剪裁附件网格线
/// </summary>
@@ -347,12 +349,6 @@ namespace SpineViewer.Spine
{
loadedSkins.Add(name);
reloadSkins();
if (!skinLoggerWarned && Version < SpineVersion.V38 && loadedSkins.Count > 1)
{
logger.Warn($"Multiplt skins not supported in SpineVersion {Version.GetName()}");
skinLoggerWarned = true;
}
}
}
@@ -437,6 +433,18 @@ namespace SpineViewer.Spine
/// </summary>
public void ResetAnimationsTime() { lock (_lock) { foreach (var i in getTrackIndices()) setAnimation(i, getAnimation(i)); update(0); } }
/// <summary>
/// 获取当前状态包围盒
/// </summary>
public RectangleF GetCurrentBounds() { lock (_lock) return getCurrentBounds(); }
protected abstract RectangleF getCurrentBounds();
/// <summary>
/// 获取当前参数下包围盒最大范围, 不是精确值
/// </summary>
public RectangleF GetBounds() { lock (_lock) return getBounds(); }
protected abstract RectangleF getBounds();
/// <summary>
/// 更新内部状态
/// </summary>

View File

@@ -64,6 +64,12 @@ namespace SpineViewer.Spine.SpineView
//[DisplayName("Paths")]
//public bool DebugPaths { get => Spine.DebugPaths; set => Spine.DebugPaths = value; }
///// <summary>
///// 显示点附件
///// </summary>
//[DisplayName("Points")]
//public bool DebugPoints { get => Spine.DebugPoints; set => Spine.DebugPoints = value; }
/// <summary>
/// 显示剪裁附件网格线
/// </summary>

View File

@@ -7,7 +7,7 @@
<TargetFramework>net8.0-windows</TargetFramework>
<BaseOutputPath>$(SolutionDir)out</BaseOutputPath>
<IncludeSourceRevisionInInformationalVersion>false</IncludeSourceRevisionInInformationalVersion>
<Version>0.12.3</Version>
<Version>0.12.4</Version>
<OutputType>WinExe</OutputType>
<UseWindowsForms>true</UseWindowsForms>
<ApplicationIcon>appicon.ico</ApplicationIcon>