Compare commits

...

49 Commits

Author SHA1 Message Date
ww-rm
750a8b8aff 更新至v0.12.5 2025-04-19 01:32:29 +08:00
ww-rm
cd86155878 修复问题 2025-04-19 01:32:19 +08:00
ww-rm
16739c39d6 修复小bug 2025-04-19 00:41:40 +08:00
ww-rm
c7971a9829 update readme 2025-04-19 00:19:56 +08:00
ww-rm
44c4fc4b21 update preview 2025-04-19 00:19:47 +08:00
ww-rm
6f1c8e3320 增加槽位属性面板 2025-04-19 00:12:27 +08:00
ww-rm
8f818416ba 优化缓存字典读取 2025-04-18 23:55:08 +08:00
ww-rm
de6858ca48 增加GetSlotAttachment/SetSlotAttachment 2025-04-18 23:15:46 +08:00
ww-rm
3fd3d2a378 增加SFMLColorConverter属性描述符缓存 2025-04-18 22:37:46 +08:00
ww-rm
706c9125e6 修改皮肤设置方式为GetSkinStatus/SetSkinStatus 2025-04-18 21:56:50 +08:00
ww-rm
5f026b000c 修改皮肤设置操作为布尔型 2025-04-18 21:23:26 +08:00
ww-rm
0b0d036f08 增加SlotAttachmentNames 2025-04-18 19:31:33 +08:00
ww-rm
6b9017d535 修改名字 2025-04-18 14:50:27 +08:00
ww-rm
5eb47e33ac 修正名字 2025-04-18 14:48:08 +08:00
ww-rm
4d31335da0 修复缩放之后皮肤null引用错误 2025-04-18 11:14:32 +08:00
ww-rm
0b5e76a448 增强分辨率缓存 2025-04-18 00:09:17 +08:00
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
33 changed files with 2027 additions and 757 deletions

View File

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

View File

@@ -12,7 +12,7 @@
---
:sparkles: v0.12.x 新增功能: 支持多轨道动画以及多皮肤列表管理 :sparkles:
:sparkles: `v0.12.5` 新特性: 支持自定义槽位附件 :sparkles:
---

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

@@ -39,8 +39,9 @@
tabPage_Skin = new TabPage();
propertyGrid_Skin = new PropertyGrid();
contextMenuStrip_Skin = new ContextMenuStrip(components);
toolStripMenuItem_AddSkin = new ToolStripMenuItem();
toolStripMenuItem_RemoveSkin = new ToolStripMenuItem();
toolStripMenuItem_ReloadSkins = new ToolStripMenuItem();
tabPage_Slot = new TabPage();
propertyGrid_Slot = new PropertyGrid();
tabPage_Animation = new TabPage();
propertyGrid_Animation = new PropertyGrid();
contextMenuStrip_Animation = new ContextMenuStrip(components);
@@ -54,6 +55,7 @@
tabPage_Transform.SuspendLayout();
tabPage_Skin.SuspendLayout();
contextMenuStrip_Skin.SuspendLayout();
tabPage_Slot.SuspendLayout();
tabPage_Animation.SuspendLayout();
contextMenuStrip_Animation.SuspendLayout();
tabPage_Debug.SuspendLayout();
@@ -66,6 +68,7 @@
tabControl.Controls.Add(tabPage_Render);
tabControl.Controls.Add(tabPage_Transform);
tabControl.Controls.Add(tabPage_Skin);
tabControl.Controls.Add(tabPage_Slot);
tabControl.Controls.Add(tabPage_Animation);
tabControl.Controls.Add(tabPage_Debug);
tabControl.Dock = DockStyle.Fill;
@@ -108,7 +111,7 @@
tabPage_Render.Location = new Point(4, 4);
tabPage_Render.Margin = new Padding(0);
tabPage_Render.Name = "tabPage_Render";
tabPage_Render.Size = new Size(364, 380);
tabPage_Render.Size = new Size(364, 370);
tabPage_Render.TabIndex = 1;
tabPage_Render.Text = "渲染";
//
@@ -119,7 +122,7 @@
propertyGrid_Render.Location = new Point(0, 0);
propertyGrid_Render.Name = "propertyGrid_Render";
propertyGrid_Render.PropertySort = PropertySort.Alphabetical;
propertyGrid_Render.Size = new Size(364, 380);
propertyGrid_Render.Size = new Size(364, 370);
propertyGrid_Render.TabIndex = 1;
propertyGrid_Render.ToolbarVisible = false;
//
@@ -130,7 +133,7 @@
tabPage_Transform.Location = new Point(4, 4);
tabPage_Transform.Margin = new Padding(0);
tabPage_Transform.Name = "tabPage_Transform";
tabPage_Transform.Size = new Size(364, 380);
tabPage_Transform.Size = new Size(364, 370);
tabPage_Transform.TabIndex = 2;
tabPage_Transform.Text = "变换";
//
@@ -141,7 +144,7 @@
propertyGrid_Transform.Location = new Point(0, 0);
propertyGrid_Transform.Name = "propertyGrid_Transform";
propertyGrid_Transform.PropertySort = PropertySort.Alphabetical;
propertyGrid_Transform.Size = new Size(364, 380);
propertyGrid_Transform.Size = new Size(364, 370);
propertyGrid_Transform.TabIndex = 1;
propertyGrid_Transform.ToolbarVisible = false;
//
@@ -152,7 +155,7 @@
tabPage_Skin.Location = new Point(4, 4);
tabPage_Skin.Margin = new Padding(0);
tabPage_Skin.Name = "tabPage_Skin";
tabPage_Skin.Size = new Size(364, 380);
tabPage_Skin.Size = new Size(364, 370);
tabPage_Skin.TabIndex = 3;
tabPage_Skin.Text = "皮肤";
//
@@ -164,31 +167,45 @@
propertyGrid_Skin.Location = new Point(0, 0);
propertyGrid_Skin.Name = "propertyGrid_Skin";
propertyGrid_Skin.PropertySort = PropertySort.NoSort;
propertyGrid_Skin.Size = new Size(364, 380);
propertyGrid_Skin.Size = new Size(364, 370);
propertyGrid_Skin.TabIndex = 1;
propertyGrid_Skin.ToolbarVisible = false;
//
// contextMenuStrip_Skin
//
contextMenuStrip_Skin.ImageScalingSize = new Size(24, 24);
contextMenuStrip_Skin.Items.AddRange(new ToolStripItem[] { toolStripMenuItem_AddSkin, toolStripMenuItem_RemoveSkin });
contextMenuStrip_Skin.Items.AddRange(new ToolStripItem[] { toolStripMenuItem_ReloadSkins });
contextMenuStrip_Skin.Name = "contextMenuStrip1";
contextMenuStrip_Skin.Size = new Size(117, 64);
contextMenuStrip_Skin.Opening += contextMenuStrip_Skin_Opening;
contextMenuStrip_Skin.Size = new Size(225, 34);
//
// toolStripMenuItem_AddSkin
// toolStripMenuItem_ReloadSkins
//
toolStripMenuItem_AddSkin.Name = "toolStripMenuItem_AddSkin";
toolStripMenuItem_AddSkin.Size = new Size(116, 30);
toolStripMenuItem_AddSkin.Text = "添加";
toolStripMenuItem_AddSkin.Click += toolStripMenuItem_AddSkin_Click;
toolStripMenuItem_ReloadSkins.Name = "toolStripMenuItem_ReloadSkins";
toolStripMenuItem_ReloadSkins.Size = new Size(224, 30);
toolStripMenuItem_ReloadSkins.Text = "重新加载所选皮肤";
toolStripMenuItem_ReloadSkins.Click += toolStripMenuItem_ReloadSkins_Click;
//
// toolStripMenuItem_RemoveSkin
// tabPage_Slot
//
toolStripMenuItem_RemoveSkin.Name = "toolStripMenuItem_RemoveSkin";
toolStripMenuItem_RemoveSkin.Size = new Size(116, 30);
toolStripMenuItem_RemoveSkin.Text = "移除";
toolStripMenuItem_RemoveSkin.Click += toolStripMenuItem_RemoveSkin_Click;
tabPage_Slot.BackColor = SystemColors.Control;
tabPage_Slot.Controls.Add(propertyGrid_Slot);
tabPage_Slot.Location = new Point(4, 4);
tabPage_Slot.Margin = new Padding(0);
tabPage_Slot.Name = "tabPage_Slot";
tabPage_Slot.Size = new Size(364, 370);
tabPage_Slot.TabIndex = 6;
tabPage_Slot.Text = "插槽";
//
// propertyGrid_Slot
//
propertyGrid_Slot.Dock = DockStyle.Fill;
propertyGrid_Slot.HelpVisible = false;
propertyGrid_Slot.Location = new Point(0, 0);
propertyGrid_Slot.Name = "propertyGrid_Slot";
propertyGrid_Slot.PropertySort = PropertySort.Alphabetical;
propertyGrid_Slot.Size = new Size(364, 370);
propertyGrid_Slot.TabIndex = 2;
propertyGrid_Slot.ToolbarVisible = false;
//
// tabPage_Animation
//
@@ -197,7 +214,7 @@
tabPage_Animation.Location = new Point(4, 4);
tabPage_Animation.Margin = new Padding(0);
tabPage_Animation.Name = "tabPage_Animation";
tabPage_Animation.Size = new Size(364, 380);
tabPage_Animation.Size = new Size(364, 370);
tabPage_Animation.TabIndex = 4;
tabPage_Animation.Text = "动画";
//
@@ -209,7 +226,7 @@
propertyGrid_Animation.Location = new Point(0, 0);
propertyGrid_Animation.Name = "propertyGrid_Animation";
propertyGrid_Animation.PropertySort = PropertySort.NoSort;
propertyGrid_Animation.Size = new Size(364, 380);
propertyGrid_Animation.Size = new Size(364, 370);
propertyGrid_Animation.TabIndex = 1;
propertyGrid_Animation.ToolbarVisible = false;
//
@@ -241,7 +258,7 @@
tabPage_Debug.Controls.Add(propertyGrid_Debug);
tabPage_Debug.Location = new Point(4, 4);
tabPage_Debug.Name = "tabPage_Debug";
tabPage_Debug.Size = new Size(364, 380);
tabPage_Debug.Size = new Size(364, 370);
tabPage_Debug.TabIndex = 5;
tabPage_Debug.Text = "调试";
//
@@ -252,16 +269,16 @@
propertyGrid_Debug.Location = new Point(0, 0);
propertyGrid_Debug.Name = "propertyGrid_Debug";
propertyGrid_Debug.PropertySort = PropertySort.NoSort;
propertyGrid_Debug.Size = new Size(364, 380);
propertyGrid_Debug.Size = new Size(364, 370);
propertyGrid_Debug.TabIndex = 2;
propertyGrid_Debug.ToolbarVisible = false;
//
// SpinePropertyGrid
// SpineViewPropertyGrid
//
AutoScaleDimensions = new SizeF(11F, 24F);
AutoScaleMode = AutoScaleMode.Font;
Controls.Add(tabControl);
Name = "SpinePropertyGrid";
Name = "SpineViewPropertyGrid";
Size = new Size(372, 448);
tabControl.ResumeLayout(false);
tabPage_BaseInfo.ResumeLayout(false);
@@ -269,6 +286,7 @@
tabPage_Transform.ResumeLayout(false);
tabPage_Skin.ResumeLayout(false);
contextMenuStrip_Skin.ResumeLayout(false);
tabPage_Slot.ResumeLayout(false);
tabPage_Animation.ResumeLayout(false);
contextMenuStrip_Animation.ResumeLayout(false);
tabPage_Debug.ResumeLayout(false);
@@ -290,11 +308,12 @@
private PropertyGrid propertyGrid_Animation;
private ContextMenuStrip contextMenuStrip_Skin;
private ContextMenuStrip contextMenuStrip_Animation;
private ToolStripMenuItem toolStripMenuItem_AddSkin;
private ToolStripMenuItem toolStripMenuItem_RemoveSkin;
private ToolStripMenuItem toolStripMenuItem_ReloadSkins;
private ToolStripMenuItem toolStripMenuItem_AddAnimation;
private ToolStripMenuItem toolStripMenuItem_RemoveAnimation;
private TabPage tabPage_Debug;
private PropertyGrid propertyGrid_Debug;
private TabPage tabPage_Slot;
private PropertyGrid propertyGrid_Slot;
}
}

View File

@@ -34,6 +34,7 @@ namespace SpineViewer.Controls
propertyGrid_Render.SelectedObject = null;
propertyGrid_Transform.SelectedObject = null;
propertyGrid_Skin.SelectedObject = null;
propertyGrid_Slot.SelectedObject = null;
propertyGrid_Animation.SelectedObject = null;
propertyGrid_Debug.SelectedObject = null;
}
@@ -44,6 +45,7 @@ namespace SpineViewer.Controls
propertyGrid_Render.SelectedObjects = value.Select(e => e.Render).ToArray();
propertyGrid_Transform.SelectedObjects = value.Select(e => e.Transform).ToArray();
propertyGrid_Skin.SelectedObjects = value.Select(e => e.Skin).ToArray();
propertyGrid_Slot.SelectedObjects = value.Select(e => e.Slot).ToArray();
propertyGrid_Animation.SelectedObjects = value.Select(e => e.Animation).ToArray();
propertyGrid_Debug.SelectedObjects = value.Select(e => e.Debug).ToArray();
}
@@ -51,20 +53,6 @@ namespace SpineViewer.Controls
}
private SpineObjectProperty[]? selectedSpines = null;
private void contextMenuStrip_Skin_Opening(object sender, CancelEventArgs e)
{
if (selectedSpines?.Length == 1)
{
toolStripMenuItem_AddSkin.Enabled = true;
toolStripMenuItem_RemoveSkin.Enabled = propertyGrid_Skin.SelectedGridItem.Value is SkinNameProperty;
}
else
{
toolStripMenuItem_AddSkin.Enabled = false;
toolStripMenuItem_RemoveSkin.Enabled = false;
}
}
private void contextMenuStrip_Animation_Opening(object sender, CancelEventArgs e)
{
if (selectedSpines?.Length == 1)
@@ -79,31 +67,10 @@ namespace SpineViewer.Controls
}
}
private void toolStripMenuItem_AddSkin_Click(object sender, EventArgs e)
private void toolStripMenuItem_ReloadSkins_Click(object sender, EventArgs e)
{
if (selectedSpines?.Length != 1) return;
var spine = selectedSpines[0].Skin.Spine;
if (spine.SkinNames.Count <= 0)
{
MessagePopup.Info("没有可用的皮肤");
return;
}
spine.LoadSkin(spine.SkinNames[0]);
propertyGrid_Skin.Refresh();
}
private void toolStripMenuItem_RemoveSkin_Click(object sender, EventArgs e)
{
if (selectedSpines?.Length != 1) return;
if (propertyGrid_Skin.SelectedGridItem.Value is SkinNameProperty wrapper)
{
selectedSpines[0].Skin.Spine.UnloadSkin(wrapper.Index);
propertyGrid_Skin.Refresh();
}
foreach (var sp in selectedSpines)
sp.Spine.ReloadSkins();
}
private void toolStripMenuItem_AddAnimation_Click(object sender, EventArgs e)

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();
@@ -829,15 +867,27 @@ namespace SpineViewer.Spine.Implementations.SkeletonConverter
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

@@ -1,12 +1,15 @@
using System;
using System.Collections;
using System.Collections.Frozen;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Diagnostics;
using System.IO;
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 +17,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 +47,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;
@@ -48,6 +63,11 @@ namespace SpineViewer.Spine.Implementations.SpineObject
// 2.1.x 不支持剪裁
//private SkeletonClipping clipping = new();
/// <summary>
/// 所有插槽在所有皮肤中可用的附件集合
/// </summary>
private readonly Dictionary<string, Dictionary<string, Attachment>> slotAttachments = [];
public SpineObject21(string skelPath, string atlasPath) : base(skelPath, atlasPath)
{
atlas = new Atlas(AtlasPath, textureLoader);
@@ -74,13 +94,21 @@ namespace SpineViewer.Spine.Implementations.SpineObject
}
}
foreach (var skin in skeletonData.Skins)
skinNames.Add(skin.Name);
foreach (var sk in skeletonData.Skins)
{
foreach (var (k, att) in sk.Attachments)
{
var slotName = skeletonData.Slots[k.Key].Name;
if (!slotAttachments.TryGetValue(slotName, out var attachments))
slotAttachments[slotName] = attachments = new() { [EMPTY_ATTACHMENT] = null };
attachments[att.Name] = att;
}
}
SlotAttachmentNames = slotAttachments.ToFrozenDictionary(item => item.Key, item => item.Value.Keys.ToImmutableArray());
SkinNames = skeletonData.Skins.Select(v => v.Name).Where(v => v != "default").ToImmutableArray();
AnimationNames = [EMPTY_ANIMATION, .. skeletonData.Animations.Select(v => v.Name)];
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);
}
@@ -124,15 +152,15 @@ namespace SpineViewer.Spine.Implementations.SpineObject
}
// reload skel-dependent data
skeleton = new Skeleton(skeletonData) { Skin = new(Guid.NewGuid().ToString()) };
animationStateData = new AnimationStateData(skeletonData) { DefaultMix = animationStateData.DefaultMix };
skeleton = new Skeleton(skeletonData);
animationState = new AnimationState(animationStateData);
// 恢复状态
position = pos;
flipX = fX;
flipY = fY;
foreach (var s in loadedSkins) addSkin(s);
reloadSkins();
for (int i = 0; i < animations.Length; i++) setAnimation(i, animations[i]);
}
}
@@ -159,16 +187,30 @@ namespace SpineViewer.Spine.Implementations.SpineObject
set => skeleton.FlipY = value;
}
protected override string getSlotAttachment(string slot) => skeleton.FindSlot(slot)?.Attachment?.Name ?? EMPTY_ATTACHMENT;
protected override void setSlotAttachment(string slot, string name)
{
if (slotAttachments.TryGetValue(slot, out var attachments)
&& attachments.TryGetValue(name, out var att)
&& skeleton.FindSlot(slot) is Slot s)
s.Attachment = att;
}
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()
protected override void clearSkins()
{
skeleton.SetSkin(skeletonData.DefaultSkin);
skeleton.Skin.Attachments.Clear();
skeleton.SetSlotsToSetupPose();
}
@@ -180,7 +222,7 @@ namespace SpineViewer.Spine.Implementations.SpineObject
{
if (name == EMPTY_ANIMATION)
animationState.SetAnimation(track, EmptyAnimation, false);
else if (animationNames.Contains(name))
else if (AnimationNames.Contains(name))
animationState.SetAnimation(track, name, true);
}
@@ -188,54 +230,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 skinLoadStatus.Where(e => e.Value))
{
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 +287,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 +532,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

@@ -1,12 +1,15 @@
using System;
using System.Collections;
using System.Collections.Frozen;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Diagnostics;
using System.IO;
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 +17,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 +47,25 @@ 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();
/// <summary>
/// 所有插槽在所有皮肤中可用的附件集合
/// </summary>
private readonly Dictionary<string, Dictionary<string, Attachment>> slotAttachments = [];
public SpineObject36(string skelPath, string atlasPath) : base(skelPath, atlasPath)
{
@@ -73,13 +93,21 @@ namespace SpineViewer.Spine.Implementations.SpineObject
}
}
foreach (var skin in skeletonData.Skins)
skinNames.Add(skin.Name);
foreach (var sk in skeletonData.Skins)
{
foreach (var (k, att) in sk.Attachments)
{
var slotName = skeletonData.Slots.Items[k.slotIndex].Name;
if (!slotAttachments.TryGetValue(slotName, out var attachments))
slotAttachments[slotName] = attachments = new() { [EMPTY_ATTACHMENT] = null };
attachments[att.Name] = att;
}
}
SlotAttachmentNames = slotAttachments.ToFrozenDictionary(item => item.Key, item => item.Value.Keys.ToImmutableArray());
SkinNames = skeletonData.Skins.Select(v => v.Name).Where(v => v != "default").ToImmutableArray();
AnimationNames = [EMPTY_ANIMATION, .. skeletonData.Animations.Select(v => v.Name)];
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);
}
@@ -123,15 +151,15 @@ namespace SpineViewer.Spine.Implementations.SpineObject
}
// reload skel-dependent data
skeleton = new Skeleton(skeletonData) { Skin = new(Guid.NewGuid().ToString()) };
animationStateData = new AnimationStateData(skeletonData) { DefaultMix = animationStateData.DefaultMix };
skeleton = new Skeleton(skeletonData);
animationState = new AnimationState(animationStateData);
// 恢复状态
position = pos;
flipX = fX;
flipY = fY;
foreach (var s in loadedSkins) addSkin(s);
reloadSkins();
for (int i = 0; i < animations.Length; i++) setAnimation(i, animations[i]);
}
}
@@ -158,16 +186,30 @@ namespace SpineViewer.Spine.Implementations.SpineObject
set => skeleton.FlipY = value;
}
protected override string getSlotAttachment(string slot) => skeleton.FindSlot(slot)?.Attachment?.Name ?? EMPTY_ATTACHMENT;
protected override void setSlotAttachment(string slot, string name)
{
if (slotAttachments.TryGetValue(slot, out var attachments)
&& attachments.TryGetValue(name, out var att)
&& skeleton.FindSlot(slot) is Slot s)
s.Attachment = att;
}
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()
protected override void clearSkins()
{
skeleton.SetSkin(skeletonData.DefaultSkin);
skeleton.Skin.Attachments.Clear();
skeleton.SetSlotsToSetupPose();
}
@@ -179,7 +221,7 @@ namespace SpineViewer.Spine.Implementations.SpineObject
{
if (name == EMPTY_ANIMATION)
animationState.SetAnimation(track, EmptyAnimation, false);
else if (animationNames.Contains(name))
else if (AnimationNames.Contains(name))
animationState.SetAnimation(track, name, true);
}
@@ -187,14 +229,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 skinLoadStatus.Where(e => e.Value))
{
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 +287,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 +559,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

@@ -1,9 +1,12 @@
using System;
using System.Collections.Frozen;
using System.Collections.Generic;
using System.Collections.Immutable;
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 +14,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 +45,24 @@ 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();
/// <summary>
/// 所有插槽在所有皮肤中可用的附件集合
/// </summary>
private readonly Dictionary<string, Dictionary<string, Attachment>> slotAttachments = [];
public SpineObject37(string skelPath, string atlasPath) : base(skelPath, atlasPath)
{
@@ -71,13 +90,21 @@ namespace SpineViewer.Spine.Implementations.SpineObject
}
}
foreach (var skin in skeletonData.Skins)
skinNames.Add(skin.Name);
foreach (var sk in skeletonData.Skins)
{
foreach (var (k, att) in sk.Attachments)
{
var slotName = skeletonData.Slots.Items[k.slotIndex].Name;
if (!slotAttachments.TryGetValue(slotName, out var attachments))
slotAttachments[slotName] = attachments = new() { [EMPTY_ATTACHMENT] = null };
attachments[att.Name] = att;
}
}
SlotAttachmentNames = slotAttachments.ToFrozenDictionary(item => item.Key, item => item.Value.Keys.ToImmutableArray());
SkinNames = skeletonData.Skins.Select(v => v.Name).Where(v => v != "default").ToImmutableArray();
AnimationNames = [EMPTY_ANIMATION, .. skeletonData.Animations.Select(v => v.Name)];
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);
}
@@ -130,16 +157,30 @@ namespace SpineViewer.Spine.Implementations.SpineObject
}
}
protected override string getSlotAttachment(string slot) => skeleton.FindSlot(slot)?.Attachment?.Name ?? EMPTY_ATTACHMENT;
protected override void setSlotAttachment(string slot, string name)
{
if (slotAttachments.TryGetValue(slot, out var attachments)
&& attachments.TryGetValue(name, out var att)
&& skeleton.FindSlot(slot) is Slot s)
s.Attachment = att;
}
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()
protected override void clearSkins()
{
skeleton.SetSkin(skeletonData.DefaultSkin);
skeleton.Skin.Attachments.Clear();
skeleton.SetSlotsToSetupPose();
}
@@ -151,7 +192,7 @@ namespace SpineViewer.Spine.Implementations.SpineObject
{
if (name == EMPTY_ANIMATION)
animationState.SetAnimation(track, EmptyAnimation, false);
else if (animationNames.Contains(name))
else if (AnimationNames.Contains(name))
animationState.SetAnimation(track, name, true);
}
@@ -159,14 +200,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 skinLoadStatus.Where(e => e.Value))
{
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 +258,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 +530,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

@@ -1,12 +1,16 @@
using System;
using System.Collections.Frozen;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Diagnostics;
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 +18,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 +52,24 @@ 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();
/// <summary>
/// 所有插槽在所有皮肤中可用的附件集合
/// </summary>
private readonly Dictionary<string, Dictionary<string, Attachment>> slotAttachments = [];
public SpineObject38(string skelPath, string atlasPath) : base(skelPath, atlasPath)
{
@@ -70,18 +90,26 @@ 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);
}
}
foreach (var skin in skeletonData.Skins)
skinNames.Add(skin.Name);
foreach (var anime in skeletonData.Animations)
animationNames.Add(anime.Name);
foreach (var sk in skeletonData.Skins)
{
foreach (var (k, att) in sk.Attachments)
{
var slotName = skeletonData.Slots.Items[k.SlotIndex].Name;
if (!slotAttachments.TryGetValue(slotName, out var attachments))
slotAttachments[slotName] = attachments = new() { [EMPTY_ATTACHMENT] = null };
attachments[att.Name] = att;
}
}
SlotAttachmentNames = slotAttachments.ToFrozenDictionary(item => item.Key, item => item.Value.Keys.ToImmutableArray());
SkinNames = skeletonData.Skins.Select(v => v.Name).Where(v => v != "default").ToImmutableArray();
AnimationNames = [EMPTY_ANIMATION, .. skeletonData.Animations.Select(v => v.Name)];
skeleton = new Skeleton(skeletonData) { Skin = new(Guid.NewGuid().ToString()) }; // 挂载一个空皮肤当作容器
animationStateData = new AnimationStateData(skeletonData);
@@ -136,6 +164,16 @@ namespace SpineViewer.Spine.Implementations.SpineObject
}
}
protected override string getSlotAttachment(string slot) => skeleton.FindSlot(slot)?.Attachment?.Name ?? EMPTY_ATTACHMENT;
protected override void setSlotAttachment(string slot, string name)
{
if (slotAttachments.TryGetValue(slot, out var attachments)
&& attachments.TryGetValue(name, out var att)
&& skeleton.FindSlot(slot) is Slot s)
s.Attachment = att;
}
protected override void addSkin(string name)
{
if (skeletonData.FindSkin(name) is Skin sk)
@@ -145,7 +183,7 @@ namespace SpineViewer.Spine.Implementations.SpineObject
}
}
protected override void clearSkin()
protected override void clearSkins()
{
skeleton.Skin.Clear();
skeleton.SetSlotsToSetupPose();
@@ -159,7 +197,7 @@ namespace SpineViewer.Spine.Implementations.SpineObject
{
if (name == EMPTY_ANIMATION)
animationState.SetAnimation(track, EmptyAnimation, false);
else if (animationNames.Contains(name))
else if (AnimationNames.Contains(name))
animationState.SetAnimation(track, name, true);
}
@@ -167,14 +205,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 skinLoadStatus.Where(e => e.Value)) 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 +259,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 +531,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

@@ -1,11 +1,14 @@
using System;
using System.Collections.Frozen;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Diagnostics;
using System.IO;
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 +16,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 +47,24 @@ 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();
/// <summary>
/// 所有插槽在所有皮肤中可用的附件集合
/// </summary>
private readonly Dictionary<string, Dictionary<string, Attachment>> slotAttachments = [];
public SpineObject40(string skelPath, string atlasPath) : base(skelPath, atlasPath)
{
@@ -73,11 +92,20 @@ namespace SpineViewer.Spine.Implementations.SpineObject
}
}
foreach (var skin in skeletonData.Skins)
skinNames.Add(skin.Name);
foreach (var anime in skeletonData.Animations)
animationNames.Add(anime.Name);
foreach (var sk in skeletonData.Skins)
{
foreach (var e in sk.Attachments)
{
var slotName = skeletonData.Slots.Items[e.SlotIndex].Name;
var att = e.Attachment;
if (!slotAttachments.TryGetValue(slotName, out var attachments))
slotAttachments[slotName] = attachments = new() { [EMPTY_ATTACHMENT] = null };
attachments[att.Name] = att;
}
}
SlotAttachmentNames = slotAttachments.ToFrozenDictionary(item => item.Key, item => item.Value.Keys.ToImmutableArray());
SkinNames = skeletonData.Skins.Select(v => v.Name).Where(v => v != "default").ToImmutableArray();
AnimationNames = [EMPTY_ANIMATION, .. skeletonData.Animations.Select(v => v.Name)];
skeleton = new Skeleton(skeletonData) { Skin = new(Guid.NewGuid().ToString()) }; // 挂载一个空皮肤当作容器
animationStateData = new AnimationStateData(skeletonData);
@@ -132,6 +160,16 @@ namespace SpineViewer.Spine.Implementations.SpineObject
}
}
protected override string getSlotAttachment(string slot) => skeleton.FindSlot(slot)?.Attachment?.Name ?? EMPTY_ATTACHMENT;
protected override void setSlotAttachment(string slot, string name)
{
if (slotAttachments.TryGetValue(slot, out var attachments)
&& attachments.TryGetValue(name, out var att)
&& skeleton.FindSlot(slot) is Slot s)
s.Attachment = att;
}
protected override void addSkin(string name)
{
if (skeletonData.FindSkin(name) is Skin sk)
@@ -141,7 +179,7 @@ namespace SpineViewer.Spine.Implementations.SpineObject
}
}
protected override void clearSkin()
protected override void clearSkins()
{
skeleton.Skin.Clear();
skeleton.SetSlotsToSetupPose();
@@ -155,7 +193,7 @@ namespace SpineViewer.Spine.Implementations.SpineObject
{
if (name == EMPTY_ANIMATION)
animationState.SetAnimation(track, EmptyAnimation, false);
else if (animationNames.Contains(name))
else if (AnimationNames.Contains(name))
animationState.SetAnimation(track, name, true);
}
@@ -163,14 +201,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 skinLoadStatus.Where(e => e.Value)) 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 +255,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 +527,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

@@ -1,11 +1,14 @@
using System;
using System.Collections.Frozen;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Diagnostics;
using System.IO;
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 +16,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 +48,23 @@ 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();
/// <summary>
/// 所有插槽在所有皮肤中可用的附件集合
/// </summary>
private readonly Dictionary<string, Dictionary<string, Attachment>> slotAttachments = [];
public SpineObject41(string skelPath, string atlasPath) : base(skelPath, atlasPath)
{
@@ -73,11 +92,20 @@ namespace SpineViewer.Spine.Implementations.SpineObject
}
}
foreach (var skin in skeletonData.Skins)
skinNames.Add(skin.Name);
foreach (var anime in skeletonData.Animations)
animationNames.Add(anime.Name);
foreach (var sk in skeletonData.Skins)
{
foreach (var e in sk.Attachments)
{
var slotName = skeletonData.Slots.Items[e.SlotIndex].Name;
var att = e.Attachment;
if (!slotAttachments.TryGetValue(slotName, out var attachments))
slotAttachments[slotName] = attachments = new() { [EMPTY_ATTACHMENT] = null };
attachments[att.Name] = att;
}
}
SlotAttachmentNames = slotAttachments.ToFrozenDictionary(item => item.Key, item => item.Value.Keys.ToImmutableArray());
SkinNames = skeletonData.Skins.Select(v => v.Name).Where(v => v != "default").ToImmutableArray();
AnimationNames = [EMPTY_ANIMATION, .. skeletonData.Animations.Select(v => v.Name)];
skeleton = new Skeleton(skeletonData) { Skin = new(Guid.NewGuid().ToString()) }; // 挂载一个空皮肤当作容器
animationStateData = new AnimationStateData(skeletonData);
@@ -132,6 +160,16 @@ namespace SpineViewer.Spine.Implementations.SpineObject
}
}
protected override string getSlotAttachment(string slot) => skeleton.FindSlot(slot)?.Attachment?.Name ?? EMPTY_ATTACHMENT;
protected override void setSlotAttachment(string slot, string name)
{
if (slotAttachments.TryGetValue(slot, out var attachments)
&& attachments.TryGetValue(name, out var att)
&& skeleton.FindSlot(slot) is Slot s)
s.Attachment = att;
}
protected override void addSkin(string name)
{
if (skeletonData.FindSkin(name) is Skin sk)
@@ -141,7 +179,7 @@ namespace SpineViewer.Spine.Implementations.SpineObject
}
}
protected override void clearSkin()
protected override void clearSkins()
{
skeleton.Skin.Clear();
skeleton.SetSlotsToSetupPose();
@@ -155,7 +193,7 @@ namespace SpineViewer.Spine.Implementations.SpineObject
{
if (name == EMPTY_ANIMATION)
animationState.SetAnimation(track, EmptyAnimation, false);
else if (animationNames.Contains(name))
else if (AnimationNames.Contains(name))
animationState.SetAnimation(track, name, true);
}
@@ -163,14 +201,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 skinLoadStatus.Where(e => e.Value)) 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 +254,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 +526,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

@@ -1,19 +1,32 @@
using System;
using System.Collections.Frozen;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using SpineRuntime42;
using SpineViewer.Extensions;
using SpineViewer.Utils;
namespace SpineViewer.Spine.Implementations.SpineObject
{
[SpineImplementation(SpineVersion.V42)]
internal class Spineobject42 : Spine.SpineObject
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,20 +47,26 @@ 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)
/// <summary>
/// 所有插槽在所有皮肤中可用的附件集合
/// </summary>
private readonly Dictionary<string, Dictionary<string, Attachment>> slotAttachments = [];
public SpineObject42(string skelPath, string atlasPath) : base(skelPath, atlasPath)
{
atlas = new Atlas(AtlasPath, textureLoader);
try
@@ -73,11 +92,20 @@ namespace SpineViewer.Spine.Implementations.SpineObject
}
}
foreach (var skin in skeletonData.Skins)
skinNames.Add(skin.Name);
foreach (var anime in skeletonData.Animations)
animationNames.Add(anime.Name);
foreach (var sk in skeletonData.Skins)
{
foreach (var e in sk.Attachments)
{
var slotName = skeletonData.Slots.Items[e.SlotIndex].Name;
var att = e.Attachment;
if (!slotAttachments.TryGetValue(slotName, out var attachments))
slotAttachments[slotName] = attachments = new() { [EMPTY_ATTACHMENT] = null };
attachments[att.Name] = att;
}
}
SlotAttachmentNames = slotAttachments.ToFrozenDictionary(item => item.Key, item => item.Value.Keys.ToImmutableArray());
SkinNames = skeletonData.Skins.Select(v => v.Name).Where(v => v != "default").ToImmutableArray();
AnimationNames = [EMPTY_ANIMATION, .. skeletonData.Animations.Select(v => v.Name)];
skeleton = new Skeleton(skeletonData) { Skin = new(Guid.NewGuid().ToString()) }; // 挂载一个空皮肤当作容器
animationStateData = new AnimationStateData(skeletonData);
@@ -132,6 +160,16 @@ namespace SpineViewer.Spine.Implementations.SpineObject
}
}
protected override string getSlotAttachment(string slot) => skeleton.FindSlot(slot)?.Attachment?.Name ?? EMPTY_ATTACHMENT;
protected override void setSlotAttachment(string slot, string name)
{
if (slotAttachments.TryGetValue(slot, out var attachments)
&& attachments.TryGetValue(name, out var att)
&& skeleton.FindSlot(slot) is Slot s)
s.Attachment = att;
}
protected override void addSkin(string name)
{
if (skeletonData.FindSkin(name) is Skin sk)
@@ -141,7 +179,7 @@ namespace SpineViewer.Spine.Implementations.SpineObject
}
}
protected override void clearSkin()
protected override void clearSkins()
{
skeleton.Skin.Clear();
skeleton.SetSlotsToSetupPose();
@@ -155,7 +193,7 @@ namespace SpineViewer.Spine.Implementations.SpineObject
{
if (name == EMPTY_ANIMATION)
animationState.SetAnimation(track, EmptyAnimation, false);
else if (animationNames.Contains(name))
else if (AnimationNames.Contains(name))
animationState.SetAnimation(track, name, true);
}
@@ -163,14 +201,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 skinLoadStatus.Where(e => e.Value)) 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 +255,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 +527,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>
/// 输出文件夹
@@ -44,13 +59,29 @@ namespace SpineViewer.Spine.SpineExporter
/// <summary>
/// 画面分辨率
/// </summary>
public Size Resolution { get; set; } = new(100, 100);
public Size Resolution
{
get => resolution;
set
{
if (value.Width <= 0) value.Width = 100;
if (value.Height <= 0) value.Height = 100;
resolution = value;
exportResolution = new(value.Width + Margin.Horizontal, value.Height + Margin.Vertical);
}
}
private Size resolution = new(100, 100);
/// <summary>
/// 渲染视窗, 接管对象生命周期
/// 包含边缘的分辨率
/// </summary>
public SFML.Graphics.View View { get => view; set { view.Dispose(); view = value; } }
private SFML.Graphics.View view = new();
private Size exportResolution = new(100, 100);
/// <summary>
/// 预览画面的视区
/// </summary>
public SFML.Graphics.View PreviewerView { get => previewerView; set { previewerView.Dispose(); previewerView = new(value); } }
private SFML.Graphics.View previewerView = new();
/// <summary>
/// 是否仅渲染选中
@@ -71,7 +102,7 @@ namespace SpineViewer.Spine.SpineExporter
bcPma.R = (byte)(bcPma.R * a);
bcPma.G = (byte)(bcPma.G * a);
bcPma.B = (byte)(bcPma.B * a);
BackgroundColorPma = bcPma;
backgroundColorPma = bcPma;
}
}
private SFML.Graphics.Color backgroundColor = SFML.Graphics.Color.Transparent;
@@ -79,16 +110,112 @@ namespace SpineViewer.Spine.SpineExporter
/// <summary>
/// 预乘后的背景颜色
/// </summary>
public SFML.Graphics.Color BackgroundColorPma { get; private set; } = SFML.Graphics.Color.Transparent;
private SFML.Graphics.Color backgroundColorPma = 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);
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;
exportResolution = new(Resolution.Width + value.Horizontal, Resolution.Height + value.Vertical);
}
}
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)
{
uint width;
uint height;
SFML.Graphics.View view;
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);
}
}
width = (uint)exportResolution.Width;
height = (uint)exportResolution.Height;
view = exportViewCache;
}
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] = new(spineResolution.Width + Margin.Horizontal, spineResolution.Height + Margin.Vertical);
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);
}
width = (uint)spineResolutionCache[cacheKey].Width;
height = (uint)spineResolutionCache[cacheKey].Height;
view = spineViewCache[cacheKey];
}
var tex = new SFML.Graphics.RenderTexture(width, height);
tex.SetView(view);
return tex;
}
@@ -103,10 +230,10 @@ namespace SpineViewer.Spine.SpineExporter
protected SFMLImageVideoFrame GetFrame(SpineObject[] spinesToRender)
{
// RenderTexture 必须临时创建, 随用随取, 防止出现跨线程的情况
using var texPma = GetRenderTexture();
using var texPma = GetRenderTexture(AutoResolution ? spinesToRender : null);
// 先将预乘结果准确绘制出来, 注意背景色也应当是预乘的
texPma.Clear(BackgroundColorPma);
texPma.Clear(backgroundColorPma);
foreach (var spine in spinesToRender) texPma.Draw(spine);
texPma.Display();
@@ -131,7 +258,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 +298,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 +315,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 +362,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 +380,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

@@ -6,6 +6,9 @@ using NLog;
using System.Xml.Linq;
using SpineViewer.Extensions;
using SpineViewer.Utils;
using System.Collections.Immutable;
using System.Collections.Frozen;
using System.Linq;
namespace SpineViewer.Spine
{
@@ -14,20 +17,20 @@ namespace SpineViewer.Spine
/// </summary>
public abstract class SpineObject : ImplementationResolver<SpineObject, SpineImplementationAttribute, SpineVersion>, SFML.Graphics.Drawable, IDisposable
{
/// <summary>
/// 空附件标记
/// </summary>
protected const string EMPTY_ATTACHMENT = "<Empty>";
/// <summary>
/// 空动画标记
/// </summary>
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 +55,6 @@ namespace SpineViewer.Spine
/// 日志器
/// </summary>
protected readonly Logger logger = LogManager.GetCurrentClassLogger();
private bool skinLoggerWarned = false;
/// <summary>
/// 构造函数
@@ -71,9 +73,6 @@ namespace SpineViewer.Spine
/// </summary>
private SpineObject PostInit()
{
SkinNames = skinNames.AsReadOnly();
AnimationNames = animationNames.AsReadOnly();
// 必须 Update 一次否则包围盒还没有值
update(0);
@@ -82,21 +81,23 @@ 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);
tex.Display();
Preview = tex.Texture.CopyToBitmap();
// 默认初始化10个空位
for (int i = 0; i < 10; i++)
{
setAnimation(i, AnimationNames.First());
loadedSkins.Add(SkinNames.First());
}
reloadSkins();
// 初始化皮肤加载情况
foreach (var n in SkinNames) skinLoadStatus[n] = false;
// 默认初始化10个动画空位
for (int i = 0; i < 10; i++) setAnimation(i, AnimationNames.First());
return this;
}
@@ -163,12 +164,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>
@@ -209,18 +204,6 @@ namespace SpineViewer.Spine
}
protected abstract bool flipY { get; set; }
/// <summary>
/// 包含的所有皮肤名称
/// </summary>
public ReadOnlyCollection<string> SkinNames { get; private set; }
protected readonly List<string> skinNames = [];
/// <summary>
/// 包含的所有动画名称
/// </summary>
public ReadOnlyCollection<string> AnimationNames { get; private set; }
protected readonly List<string> animationNames = [EMPTY_ANIMATION];
/// <summary>
/// 是否被选中
/// </summary>
@@ -321,6 +304,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>
@@ -332,76 +325,67 @@ namespace SpineViewer.Spine
protected bool debugClippings = false;
/// <summary>
/// 获取已加载的皮肤列表快照, 允许出现重复值
/// 所有插槽下可用的附件名
/// </summary>
public string[] GetLoadedSkins() { lock (_lock) return loadedSkins.ToArray(); }
protected readonly List<string> loadedSkins = [];
public FrozenDictionary<string, ImmutableArray<string>> SlotAttachmentNames { get; protected set; }
/// <summary>
/// 加载指定皮肤, 添加至列表末尾, 如果不存在则忽略, 允许加载重复的值
/// 包含的所有皮肤名称 (不含 default 默认皮肤)
/// </summary>
public void LoadSkin(string name)
{
if (!skinNames.Contains(name)) return;
lock (_lock)
{
loadedSkins.Add(name);
reloadSkins();
if (!skinLoggerWarned && Version < SpineVersion.V38 && loadedSkins.Count > 1)
{
logger.Warn($"Multiplt skins not supported in SpineVersion {Version.GetName()}");
skinLoggerWarned = true;
}
}
}
public ImmutableArray<string> SkinNames { get; protected set; }
/// <summary>
/// 卸载列表指定位置皮肤, 如果超出范围则忽略
/// 包含的所有动画名称
/// </summary>
public void UnloadSkin(int idx)
public ImmutableArray<string> AnimationNames { get; protected set; }
/// <summary>
/// 获取某个插槽当前加载的附件
/// </summary>
public string GetSlotAttachment(string slot) { lock (_lock) return getSlotAttachment(slot); }
protected abstract string getSlotAttachment(string slot);
/// <summary>
/// 设置某个插槽当前加载的附件
/// </summary>
public void SetSlotAttachment(string slot, string name) { lock (_lock) setSlotAttachment(slot, name); }
protected abstract void setSlotAttachment(string slot, string name);
/// <summary>
/// 皮肤的加载情况记录表
/// </summary>
protected readonly Dictionary<string, bool> skinLoadStatus = [];
/// <summary>
/// 查询皮肤加载状态, 皮肤不存在时返回 false
/// </summary>
public bool GetSkinStatus(string name) { lock (_lock) return skinLoadStatus.TryGetValue(name, out var status) && status; }
/// <summary>
/// 设置皮肤加载状态, 忽略不存在的皮肤
/// </summary>
public void SetSkinStatus(string name, bool status)
{
if (!skinLoadStatus.ContainsKey(name)) return;
lock (_lock)
{
if (idx < 0 || idx >= loadedSkins.Count) return;
loadedSkins.RemoveAt(idx);
skinLoadStatus[name] = status;
reloadSkins();
}
}
/// <summary>
/// 替换皮肤列表指定位置皮肤, 超出范围或者皮肤不存在则忽略
/// </summary>
public void ReplaceSkin(int idx, string name)
{
lock (_lock)
{
if (idx < 0 || idx >= loadedSkins.Count || !skinNames.Contains(name)) return;
loadedSkins[idx] = name;
reloadSkins();
}
}
/// <summary>
/// 重新加载现有皮肤列表, 用于刷新等操作
/// 刷新已加载皮肤
/// </summary>
public void ReloadSkins() { lock (_lock) reloadSkins(); }
private void reloadSkins()
protected void reloadSkins()
{
clearSkin();
foreach (var s in loadedSkins.Distinct()) addSkin(s);
clearSkins();
foreach (var (name, _) in skinLoadStatus.Where(e => e.Value)) addSkin(name);
update(0);
}
/// <summary>
/// 加载皮肤, 如果不存在则忽略
/// </summary>
protected abstract void addSkin(string name);
/// <summary>
/// 清空加载的所有皮肤
/// </summary>
protected abstract void clearSkin();
protected abstract void clearSkins();
/// <summary>
/// 获取所有非 null 的轨道索引快照
@@ -437,6 +421,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

@@ -85,9 +85,9 @@ namespace SpineViewer.Spine.SpineView
var props = new PropertyDescriptorCollection(TypeDescriptor.GetProperties(this, attributes, true).Cast<PropertyDescriptor>().ToArray());
foreach (var i in Spine.GetTrackIndices())
{
if (!pdCache.ContainsKey(i))
pdCache[i] = new TrackWrapperPropertyDescriptor(i, [new DisplayNameAttribute($"轨道 {i}")]);
props.Add(pdCache[i]);
if (!pdCache.TryGetValue(i, out var pd))
pdCache[i] = pd = new TrackWrapperPropertyDescriptor(i, [new DisplayNameAttribute($"轨道 {i}")]);
props.Add(pd);
}
return props;
}
@@ -186,17 +186,12 @@ namespace SpineViewer.Spine.SpineView
{
return new StandardValuesCollection(tracks.Spine.AnimationNames);
}
else if (context?.Instance is object[] instances && instances.All(x => x is SpineAnimationProperty))
else if (context?.Instance is object[] instances)
{
// XXX: 这里不知道为啥总是会得到 object[] 类型而不是具体的类型
var animTracks = instances.Cast<SpineAnimationProperty>().ToArray();
if (animTracks.Length > 0)
{
IEnumerable<string> common = animTracks[0].Spine.AnimationNames;
foreach (var t in animTracks.Skip(1))
common = common.Union(t.Spine.AnimationNames);
return new StandardValuesCollection(common.ToArray());
}
IEnumerable<string> common = [];
foreach (SpineAnimationProperty prop in instances.Where(inst => inst is SpineAnimationProperty))
common = common.Union(prop.Spine.AnimationNames);
return new StandardValuesCollection(common.ToArray());
}
return base.GetStandardValues(context);
}

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

@@ -27,6 +27,10 @@ namespace SpineViewer.Spine.SpineView
[DisplayName("皮肤")]
public SpineSkinProperty Skin { get; } = new(spine);
[TypeConverter(typeof(ExpandableObjectConverter))]
[DisplayName("插槽")]
public SpineSlotProperty Slot { get; } = new(spine);
[TypeConverter(typeof(ExpandableObjectConverter))]
[DisplayName("动画")]
public SpineAnimationProperty Animation { get; } = new(spine);

View File

@@ -10,7 +10,7 @@ using System.Threading.Tasks;
namespace SpineViewer.Spine.SpineView
{
/// <summary>
/// 皮肤列表动态类型包装类, 用于提供对 Spine 皮肤列表的管理能力
/// 皮肤动态类型包装类, 用于提供对 Spine 皮肤的管理能力
/// </summary>
/// <param name="spine">关联的 Spine 对象</param>
public class SpineSkinProperty(SpineObject spine) : ICustomTypeDescriptor
@@ -18,34 +18,10 @@ namespace SpineViewer.Spine.SpineView
[Browsable(false)]
public SpineObject Spine { get; } = spine;
/// <summary>
/// <see cref="SpineSkinProperty"/> 属性缓存
/// </summary>
private readonly Dictionary<int, SkinNameProperty> skinNameProperties = [];
/// <summary>
/// <c>this.Skin{i}</c>
/// </summary>
public SkinNameProperty GetSkinName(int i)
{
if (!skinNameProperties.ContainsKey(i))
skinNameProperties[i] = new SkinNameProperty(Spine, i);
return skinNameProperties[i];
}
/// <summary>
/// <c>this.Skin{i} = <paramref name="value"/></c>
/// </summary>
public void SetSkinName(int i, string value)
{
Spine.ReplaceSkin(i, value);
TypeDescriptor.Refresh(this);
}
/// <summary>
/// 在属性面板悬停可以显示已加载的皮肤列表
/// </summary>
public override string ToString() => $"[{string.Join(", ", Spine.GetLoadedSkins())}]";
public override string ToString() => $"[{string.Join(", ", Spine.SkinNames.Where(Spine.GetSkinStatus))}]";
public override bool Equals(object? obj)
{
@@ -59,7 +35,7 @@ namespace SpineViewer.Spine.SpineView
// XXX: 必须实现 ICustomTypeDescriptor 接口, 不能继承 CustomTypeDescriptor, 似乎继承下来的东西会有问题, 导致某些调用不正确
private static readonly Dictionary<int, SkinNamePropertyDescriptor> pdCache = [];
private static readonly Dictionary<string, SkinPropertyDescriptor> pdCache = [];
public AttributeCollection GetAttributes() => TypeDescriptor.GetAttributes(this, true);
public string? GetClassName() => TypeDescriptor.GetClassName(this, true);
@@ -75,112 +51,44 @@ namespace SpineViewer.Spine.SpineView
public PropertyDescriptorCollection GetProperties(Attribute[]? attributes)
{
var props = new PropertyDescriptorCollection(TypeDescriptor.GetProperties(this, attributes, true).Cast<PropertyDescriptor>().ToArray());
for (var i = 0; i < Spine.GetLoadedSkins().Length; i++)
foreach (var name in Spine.SkinNames)
{
if (!pdCache.ContainsKey(i))
pdCache[i] = new SkinNamePropertyDescriptor(i, [new DisplayNameAttribute($"皮肤 {i}")]);
props.Add(pdCache[i]);
if (!pdCache.TryGetValue(name, out var pd))
pdCache[name] = pd = new SkinPropertyDescriptor(name, [new DisplayNameAttribute(name)]);
props.Add(pd);
}
return props;
}
/// <summary>
/// 皮肤属性描述符, 实现对属性的读取和赋值
/// 皮肤属性描述符, 实现对皮肤的加载和卸载, <c><see cref="SpineSkinProperty"/>.{name}</c>
/// </summary>
private class SkinNamePropertyDescriptor(int i, Attribute[]? attributes) : PropertyDescriptor($"Skin{i}", attributes)
private class SkinPropertyDescriptor(string name, Attribute[]? attributes) : PropertyDescriptor(name, attributes)
{
private readonly int idx = i;
public override Type ComponentType => typeof(SpineSkinProperty);
public override bool IsReadOnly => false;
public override Type PropertyType => typeof(SkinNameProperty);
public override Type PropertyType => typeof(bool);
public override bool CanResetValue(object component) => false;
public override void ResetValue(object component) { }
public override bool ShouldSerializeValue(object component) => false;
/// <summary>
/// 得到一个 <see cref="SpineSkinProperty"/>, 允许用户查看或者修改具体的属性值, 这个地方决定了在面板上看到的是一个对象及其属性
/// </summary>
public override object? GetValue(object? component)
{
if (component is SpineSkinProperty prop)
return prop.GetSkinName(idx);
return prop.Spine.GetSkinStatus(Name);
return null;
}
/// <summary>
/// 允许通过字符串赋值修改该位置的皮肤
/// </summary>
public override void SetValue(object? component, object? value)
{
if (component is SpineSkinProperty prop)
{
if (value is string s)
prop.SetSkinName(idx, s);
if (value is bool s)
prop.Spine.SetSkinStatus(Name, s);
}
}
}
#endregion
}
/// <summary>
/// 对 <c><see cref="SpineSkinProperty"/>.Skin{i}</c> 属性的包装类
/// </summary>
[TypeConverter(typeof(SkinNamePropertyConverter))]
public class SkinNameProperty(SpineObject spine, int i)
{
private readonly SpineObject spine = spine;
[Browsable(false)]
public int Index { get; } = i;
public override string ToString()
{
var loadedSkins = spine.GetLoadedSkins();
if (Index >= 0 && Index < loadedSkins.Length)
return loadedSkins[Index];
return "!NULL"; // XXX: 预期应该不会发生
}
public override bool Equals(object? obj)
{
if (obj is SkinNameProperty) return ToString() == obj.ToString();
return base.Equals(obj);
}
public override int GetHashCode() => HashCode.Combine(typeof(SkinNameProperty).FullName.GetHashCode(), ToString().GetHashCode());
}
public class SkinNamePropertyConverter : StringConverter
{
// NOTE: 可以不用实现 ConvertTo/ConvertFrom, 因为属性实现了与字符串之间的互转
// ToString 实现了 ConvertTo
// SetValue 实现了从字符串设置属性
public override bool GetStandardValuesSupported(ITypeDescriptorContext? context) => true;
public override bool GetStandardValuesExclusive(ITypeDescriptorContext? context) => true;
public override StandardValuesCollection? GetStandardValues(ITypeDescriptorContext? context)
{
if (context?.Instance is SpineSkinProperty manager)
{
return new StandardValuesCollection(manager.Spine.SkinNames);
}
else if (context?.Instance is object[] instances && instances.All(x => x is SpineSkinProperty))
{
// XXX: 这里不知道为啥总是会得到 object[] 类型而不是具体的 SpineSkinWrapper[] 类型
var managers = instances.Cast<SpineSkinProperty>().ToArray();
if (managers.Length > 0)
{
IEnumerable<string> common = managers[0].Spine.SkinNames;
foreach (var t in managers.Skip(1))
common = common.Union(t.Spine.SkinNames);
return new StandardValuesCollection(common.ToArray());
}
}
return base.GetStandardValues(context);
}
}
}

View File

@@ -0,0 +1,170 @@
using SpineViewer.Spine;
using SpineViewer.Utils;
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace SpineViewer.Spine.SpineView
{
/// <summary>
/// 用于在 PropertyGrid 上显示插槽附件加载情况包装类
/// </summary>
public class SpineSlotProperty(SpineObject spine) : ICustomTypeDescriptor
{
[Browsable(false)]
public SpineObject Spine { get; } = spine;
/// <summary>
/// 显示所有插槽集合
/// </summary>
public override string ToString() => $"[{string.Join(", ", Spine.SlotAttachmentNames.Keys)}]";
public override bool Equals(object? obj)
{
if (obj is SpineAnimationProperty prop) return ToString() == prop.ToString();
return base.Equals(obj);
}
public override int GetHashCode() => HashCode.Combine(typeof(SpineAnimationProperty).FullName.GetHashCode(), ToString().GetHashCode());
#region ICustomTypeDescriptor
// XXX: 必须实现 ICustomTypeDescriptor 接口, 不能继承 CustomTypeDescriptor, 似乎继承下来的东西会有问题, 导致某些调用不正确
/// <summary>
/// 属性描述符缓存
/// </summary>
private static readonly Dictionary<string, SlotPropertyDescriptor> pdCache = [];
public AttributeCollection GetAttributes() => TypeDescriptor.GetAttributes(this, true);
public string? GetClassName() => TypeDescriptor.GetClassName(this, true);
public string? GetComponentName() => TypeDescriptor.GetComponentName(this, true);
public TypeConverter? GetConverter() => TypeDescriptor.GetConverter(this, true);
public EventDescriptor? GetDefaultEvent() => TypeDescriptor.GetDefaultEvent(this, true);
public PropertyDescriptor? GetDefaultProperty() => TypeDescriptor.GetDefaultProperty(this, true);
public object? GetEditor(Type editorBaseType) => TypeDescriptor.GetEditor(this, editorBaseType, true);
public EventDescriptorCollection GetEvents() => TypeDescriptor.GetEvents(this, true);
public EventDescriptorCollection GetEvents(Attribute[]? attributes) => TypeDescriptor.GetEvents(this, attributes, true);
public object? GetPropertyOwner(PropertyDescriptor? pd) => this;
public PropertyDescriptorCollection GetProperties() => GetProperties(null);
public PropertyDescriptorCollection GetProperties(Attribute[]? attributes)
{
var props = new PropertyDescriptorCollection(TypeDescriptor.GetProperties(this, attributes, true).Cast<PropertyDescriptor>().ToArray());
foreach (var slotName in Spine.SlotAttachmentNames.Keys)
{
if (!pdCache.TryGetValue(slotName, out var pd))
pdCache[slotName] = pd = new SlotPropertyDescriptor(slotName, [new DisplayNameAttribute($"{slotName}")]);
props.Add(pd);
}
return props;
}
/// <summary>
/// 插槽属性描述符, 实现对属性的读取和赋值
/// </summary>
internal class SlotPropertyDescriptor(string name, Attribute[]? attributes) : PropertyDescriptor(name, attributes)
{
public override Type ComponentType => typeof(SpineSlotProperty);
public override bool IsReadOnly => false;
public override Type PropertyType => typeof(SlotProperty);
public override bool CanResetValue(object component) => false;
public override void ResetValue(object component) { }
public override bool ShouldSerializeValue(object component) => false;
/// <summary>
/// 得到一个轨道包装类, 允许用户查看或者修改具体的属性值, 这个地方决定了在面板上看到的是一个对象及其属性
/// </summary>
public override object? GetValue(object? component)
{
if (component is SpineSlotProperty slots)
return slots.Spine.GetSlotAttachment(Name);
return null;
}
/// <summary>
/// 允许通过字符串赋值修改该轨道的动画, 这里决定了当其他地方的调用 (比如 Converter) 通过 value 来设置属性值的时候应该怎么处理
/// </summary>
public override void SetValue(object? component, object? value)
{
if (component is SpineSlotProperty slots)
{
if (value is string s)
slots.Spine.SetSlotAttachment(Name, s);
}
}
}
#endregion
}
/// <summary>
/// 对 <c><see cref="SpineSlotProperty"/>.{name}</c> 属性的包装类
/// </summary>
[TypeConverter(typeof(SlotPropertyConverter))]
public class SlotProperty(SpineObject spine, string name)
{
private readonly SpineObject spine = spine;
[Browsable(false)]
public string Name { get; } = name;
/// <summary>
/// 实现了默认的转为字符串的方式
/// </summary>
public override string ToString() => spine.GetSlotAttachment(Name);
/// <summary>
/// 影响了属性面板的判断, 当动画名称相同的时候认为两个对象是相同的, 这样属性面板可以在多选的时候正确显示相同取值的内容
/// </summary>
public override bool Equals(object? obj)
{
if (obj is SlotProperty) return ToString() == obj.ToString();
return base.Equals(obj);
}
/// <summary>
/// 哈希码需要和 Equals 行为类似
/// </summary>
public override int GetHashCode() => HashCode.Combine(typeof(SlotProperty).FullName.GetHashCode(), ToString().GetHashCode());
}
/// <summary>
/// 轨道索引包装类转换器, 实现字符串和包装类的互相转换, 并且提供标准值列表对属性进行设置, 同时还提供在面板上显示包装类属性的能力
/// </summary>
public class SlotPropertyConverter : StringConverter
{
// NOTE: 可以不用实现 ConvertTo/ConvertFrom, 因为属性实现了与字符串之间的互转
// ToString 实现了 ConvertTo
// SetValue 实现了从字符串设置属性
public override bool GetStandardValuesSupported(ITypeDescriptorContext? context) => true;
public override bool GetStandardValuesExclusive(ITypeDescriptorContext? context) => true;
public override StandardValuesCollection? GetStandardValues(ITypeDescriptorContext? context)
{
if (context?.PropertyDescriptor is PropertyDescriptor pd)
{
if (context?.Instance is SpineSlotProperty slots)
{
if (slots.Spine.SlotAttachmentNames.TryGetValue(pd.Name, out var names))
return new StandardValuesCollection(names);
}
else if (context?.Instance is object[] instances)
{
IEnumerable<string> common = [];
foreach (SpineSlotProperty prop in instances.Where(inst => inst is SpineSlotProperty))
{
if (prop.Spine.SlotAttachmentNames.TryGetValue(pd.Name, out var names))
common = common.Union(names);
}
return new StandardValuesCollection(common.ToArray());
}
}
return base.GetStandardValues(context);
}
}
}

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.5</Version>
<OutputType>WinExe</OutputType>
<UseWindowsForms>true</UseWindowsForms>
<ApplicationIcon>appicon.ico</ApplicationIcon>

View File

@@ -111,6 +111,8 @@ namespace SpineViewer.Utils
public override void SetValue(object? component, object? value) => component?.GetType().GetField(Name)?.SetValue(component, value);
}
private static PropertyDescriptorCollection pdCollection = null;
public override bool CanConvertFrom(ITypeDescriptorContext? context, Type sourceType)
{
return sourceType == typeof(string) || base.CanConvertFrom(context, sourceType);
@@ -156,18 +158,16 @@ namespace SpineViewer.Utils
public override PropertyDescriptorCollection GetProperties(ITypeDescriptorContext? context, object value, Attribute[]? attributes)
{
// 自定义属性集合
var properties = new List<PropertyDescriptor>
{
// 定义 R, G, B, A 四个字段的描述器
new SFMLColorPropertyDescriptor(typeof(SFML.Graphics.Color), "R", typeof(byte)),
new SFMLColorPropertyDescriptor(typeof(SFML.Graphics.Color), "G", typeof(byte)),
new SFMLColorPropertyDescriptor(typeof(SFML.Graphics.Color), "B", typeof(byte)),
new SFMLColorPropertyDescriptor(typeof(SFML.Graphics.Color), "A", typeof(byte))
};
// 返回自定义属性集合
return new PropertyDescriptorCollection(properties.ToArray());
pdCollection ??= new(
[
new SFMLColorPropertyDescriptor(typeof(SFML.Graphics.Color), "R", typeof(byte)),
new SFMLColorPropertyDescriptor(typeof(SFML.Graphics.Color), "G", typeof(byte)),
new SFMLColorPropertyDescriptor(typeof(SFML.Graphics.Color), "B", typeof(byte)),
new SFMLColorPropertyDescriptor(typeof(SFML.Graphics.Color), "A", typeof(byte))
],
true
);
return pdCollection;
}
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 160 KiB

After

Width:  |  Height:  |  Size: 134 KiB