Compare commits
87 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
afc011adbb | ||
|
|
da42c14d35 | ||
|
|
a7438e2026 | ||
|
|
47e5314bb3 | ||
|
|
1978c1da11 | ||
|
|
914c9d0ea3 | ||
|
|
7134aebb7f | ||
|
|
abb32e9ed2 | ||
|
|
2c77e385c5 | ||
|
|
ba0f5ac124 | ||
|
|
c4f17e3f06 | ||
|
|
b802ec252a | ||
|
|
68779caab0 | ||
|
|
5d819114d0 | ||
|
|
46b3937236 | ||
|
|
6803b8cf4a | ||
|
|
bd9c5a176b | ||
|
|
76c1d96c87 | ||
|
|
304af805cb | ||
|
|
027d3af619 | ||
|
|
c612c01ac7 | ||
|
|
cd7855a877 | ||
|
|
cdd81e0bfb | ||
|
|
750a8b8aff | ||
|
|
cd86155878 | ||
|
|
16739c39d6 | ||
|
|
c7971a9829 | ||
|
|
44c4fc4b21 | ||
|
|
6f1c8e3320 | ||
|
|
8f818416ba | ||
|
|
de6858ca48 | ||
|
|
3fd3d2a378 | ||
|
|
706c9125e6 | ||
|
|
5f026b000c | ||
|
|
0b0d036f08 | ||
|
|
6b9017d535 | ||
|
|
5eb47e33ac | ||
|
|
4d31335da0 | ||
|
|
0b5e76a448 | ||
|
|
775268c01a | ||
|
|
b0b1c85047 | ||
|
|
5f08fc6695 | ||
|
|
2de3bdf12b | ||
|
|
3a424c7dc1 | ||
|
|
c3e2b37072 | ||
|
|
65bd11a346 | ||
|
|
e6e7fc539f | ||
|
|
6522d415b7 | ||
|
|
378c66a333 | ||
|
|
07204417a5 | ||
|
|
c9c909cdf9 | ||
|
|
a9f59a4d2f | ||
|
|
1d2513cef5 | ||
|
|
febb797ae2 | ||
|
|
68d279a7c3 | ||
|
|
d2d8b7955c | ||
|
|
2a55fd9c36 | ||
|
|
695d3c0735 | ||
|
|
ce95db469b | ||
|
|
5d187cf80f | ||
|
|
e704ebc224 | ||
|
|
ee36f8981c | ||
|
|
09dd220abf | ||
|
|
15bc2dc3b8 | ||
|
|
1deb74eca9 | ||
|
|
de76ce64ab | ||
|
|
94b4ba33e6 | ||
|
|
7ce8a115f4 | ||
|
|
c036a4bb45 | ||
|
|
aa62f30b05 | ||
|
|
3d967c9812 | ||
|
|
e87e9efb99 | ||
|
|
8c1f6fb4a6 | ||
|
|
df82ed8a00 | ||
|
|
d01e3920ba | ||
|
|
777cd5ea3f | ||
|
|
3b73aea5c0 | ||
|
|
168f7a8173 | ||
|
|
04437e2de2 | ||
|
|
2ec83b2e87 | ||
|
|
90bfaa7b56 | ||
|
|
2ae175abd0 | ||
|
|
e2a84d8f88 | ||
|
|
b6f9cd0c7c | ||
|
|
61b7b90722 | ||
|
|
093c159753 | ||
|
|
32d36c0757 |
31
CHANGELOG.md
31
CHANGELOG.md
@@ -1,5 +1,36 @@
|
||||
# CHANGELOG
|
||||
|
||||
## v0.12.6
|
||||
|
||||
- 增加全屏预览
|
||||
- 增加桌面投影 (实验性功能)
|
||||
- 增加预览画面背景色设置
|
||||
- 增加分辨率和颜色预设列表
|
||||
- 皮肤面板显示 default
|
||||
|
||||
## v0.12.5
|
||||
|
||||
- 增加插槽属性面板
|
||||
- 修改皮肤属性面板设置方式为True/False
|
||||
|
||||
## v0.12.4
|
||||
|
||||
- 增加导出自动分辨率参数
|
||||
- 增加导出边缘和填充参数
|
||||
- 增加导出内容溢出参数
|
||||
- 支持3.7及以下版本多皮肤功能
|
||||
- 增加3.8版本的骨骼文件二进制和文本格式互转
|
||||
- 增加格式转换输出文件夹参数
|
||||
- 修改打开对话框的默认文件后缀筛选为所有类型
|
||||
|
||||
## v0.12.3
|
||||
|
||||
- 增加按住 ctrl 缩放选中模型
|
||||
- 增加对骨骼/网格/剪裁的调试渲染
|
||||
- 换回以前的上下参数面板布局
|
||||
- 修改窗口缩放模式为 Font -> Dpi
|
||||
- 修复部分问题
|
||||
|
||||
## v0.12.2
|
||||
|
||||
- 模型参数分标签显示
|
||||
|
||||
@@ -10,12 +10,6 @@
|
||||
|
||||

|
||||
|
||||
---
|
||||
|
||||
:sparkles: v0.12.x 新增功能: 支持多轨道动画以及多皮肤列表管理 :sparkles:
|
||||
|
||||
---
|
||||
|
||||
## 安装
|
||||
|
||||
前往 [Release](https://github.com/ww-rm/SpineViewer/releases) 界面下载压缩包.
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -152,27 +152,13 @@ namespace SpineRuntime36 {
|
||||
|
||||
Bone parent = this.parent;
|
||||
if (parent == null) { // Root bone.
|
||||
float rotationY = rotation + 90 + shearY;
|
||||
float la = MathUtils.CosDeg(rotation + shearX) * scaleX;
|
||||
float lb = MathUtils.CosDeg(rotationY) * scaleY;
|
||||
float lc = MathUtils.SinDeg(rotation + shearX) * scaleX;
|
||||
float ld = MathUtils.SinDeg(rotationY) * scaleY;
|
||||
if (skeleton.flipX) {
|
||||
x = -x;
|
||||
la = -la;
|
||||
lb = -lb;
|
||||
}
|
||||
if (skeleton.flipY != yDown) {
|
||||
y = -y;
|
||||
lc = -lc;
|
||||
ld = -ld;
|
||||
}
|
||||
a = la;
|
||||
b = lb;
|
||||
c = lc;
|
||||
d = ld;
|
||||
worldX = x + skeleton.x;
|
||||
worldY = y + skeleton.y;
|
||||
float rotationY = rotation + 90 + shearY, sx = skeleton.scaleX, sy = skeleton.scaleY;
|
||||
a = MathUtils.CosDeg(rotation + shearX) * scaleX * sx;
|
||||
b = MathUtils.CosDeg(rotationY) * scaleY * sx;
|
||||
c = MathUtils.SinDeg(rotation + shearX) * scaleX * sy;
|
||||
d = MathUtils.SinDeg(rotationY) * scaleY * sy;
|
||||
worldX = x * sx + skeleton.x;
|
||||
worldY = y * sy + skeleton.y;
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -228,13 +214,16 @@ namespace SpineRuntime36 {
|
||||
case TransformMode.NoScale:
|
||||
case TransformMode.NoScaleOrReflection: {
|
||||
float cos = MathUtils.CosDeg(rotation), sin = MathUtils.SinDeg(rotation);
|
||||
float za = pa * cos + pb * sin;
|
||||
float zc = pc * cos + pd * sin;
|
||||
float za = (pa * cos + pb * sin) / skeleton.scaleX;
|
||||
float zc = (pc * cos + pd * sin) / skeleton.scaleY;
|
||||
float s = (float)Math.Sqrt(za * za + zc * zc);
|
||||
if (s > 0.00001f) s = 1 / s;
|
||||
za *= s;
|
||||
zc *= s;
|
||||
s = (float)Math.Sqrt(za * za + zc * zc);
|
||||
if (data.transformMode == TransformMode.NoScale
|
||||
&& (pa * pd - pb * pc < 0) != (skeleton.scaleX < 0 != skeleton.scaleY < 0)) s = -s;
|
||||
|
||||
float r = MathUtils.PI / 2 + MathUtils.Atan2(zc, za);
|
||||
float zb = MathUtils.Cos(r) * s;
|
||||
float zd = MathUtils.Sin(r) * s;
|
||||
@@ -242,26 +231,18 @@ namespace SpineRuntime36 {
|
||||
float lb = MathUtils.CosDeg(90 + shearY) * scaleY;
|
||||
float lc = MathUtils.SinDeg(shearX) * scaleX;
|
||||
float ld = MathUtils.SinDeg(90 + shearY) * scaleY;
|
||||
if (data.transformMode != TransformMode.NoScaleOrReflection? pa * pd - pb* pc< 0 : skeleton.flipX != skeleton.flipY) {
|
||||
zb = -zb;
|
||||
zd = -zd;
|
||||
}
|
||||
a = za * la + zb * lc;
|
||||
b = za * lb + zb * ld;
|
||||
c = zc * la + zd * lc;
|
||||
d = zc * lb + zd * ld;
|
||||
return;
|
||||
d = zc * lb + zd * ld;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (skeleton.flipX) {
|
||||
a = -a;
|
||||
b = -b;
|
||||
}
|
||||
if (skeleton.flipY != Bone.yDown) {
|
||||
c = -c;
|
||||
d = -d;
|
||||
}
|
||||
a *= skeleton.scaleX;
|
||||
b *= skeleton.scaleX;
|
||||
c *= skeleton.scaleY;
|
||||
d *= skeleton.scaleY;
|
||||
}
|
||||
|
||||
public void SetToSetupPose () {
|
||||
|
||||
@@ -45,8 +45,8 @@ namespace SpineRuntime36 {
|
||||
internal Skin skin;
|
||||
internal float r = 1, g = 1, b = 1, a = 1;
|
||||
internal float time;
|
||||
internal bool flipX, flipY;
|
||||
internal float x, y;
|
||||
internal float scaleX = 1, scaleY = 1;
|
||||
internal float x, y;
|
||||
|
||||
public SkeletonData Data { get { return data; } }
|
||||
public ExposedList<Bone> Bones { get { return bones; } }
|
||||
@@ -64,10 +64,16 @@ namespace SpineRuntime36 {
|
||||
public float Time { get { return time; } set { time = value; } }
|
||||
public float X { get { return x; } set { x = value; } }
|
||||
public float Y { get { return y; } set { y = value; } }
|
||||
public bool FlipX { get { return flipX; } set { flipX = value; } }
|
||||
public bool FlipY { get { return flipY; } set { flipY = value; } }
|
||||
public float ScaleX { get { return scaleX; } set { scaleX = value; } }
|
||||
public float ScaleY { get { return scaleY; } set { scaleY = value; } }
|
||||
|
||||
public Bone RootBone {
|
||||
[Obsolete("Use ScaleX instead. FlipX is when ScaleX is negative.")]
|
||||
public bool FlipX { get { return scaleX < 0; } set { scaleX = value ? -1f : 1f; } }
|
||||
|
||||
[Obsolete("Use ScaleY instead. FlipY is when ScaleY is negative.")]
|
||||
public bool FlipY { get { return scaleY < 0; } set { scaleY = value ? -1f : 1f; } }
|
||||
|
||||
public Bone RootBone {
|
||||
get { return bones.Count == 0 ? null : bones.Items[0]; }
|
||||
}
|
||||
|
||||
|
||||
2
SpineViewer/Controls/SkelFileListBox.Designer.cs
generated
2
SpineViewer/Controls/SkelFileListBox.Designer.cs
generated
@@ -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文件";
|
||||
//
|
||||
|
||||
@@ -17,13 +17,14 @@ namespace SpineViewer.Controls
|
||||
public SkelFileListBox()
|
||||
{
|
||||
InitializeComponent();
|
||||
Items = listBox.Items;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// ListBox.Items
|
||||
/// </summary>
|
||||
public readonly ListBox.ObjectCollection Items;
|
||||
[DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
|
||||
[Browsable(false)]
|
||||
public ListBox.ObjectCollection Items { get => listBox.Items; }
|
||||
|
||||
/// <summary>
|
||||
/// 从路径列表添加
|
||||
@@ -34,14 +35,14 @@ namespace SpineViewer.Controls
|
||||
{
|
||||
if (File.Exists(path))
|
||||
{
|
||||
if (SpineHelper.CommonSkelSuffix.Contains(Path.GetExtension(path).ToLower()))
|
||||
if (SpineUtils.CommonSkelSuffix.Contains(Path.GetExtension(path).ToLower()))
|
||||
listBox.Items.Add(Path.GetFullPath(path));
|
||||
}
|
||||
else if (Directory.Exists(path))
|
||||
{
|
||||
foreach (var file in Directory.EnumerateFiles(path, "*.*", SearchOption.AllDirectories))
|
||||
{
|
||||
if (SpineHelper.CommonSkelSuffix.Contains(Path.GetExtension(file).ToLower()))
|
||||
if (SpineUtils.CommonSkelSuffix.Contains(Path.GetExtension(file).ToLower()))
|
||||
listBox.Items.Add(file);
|
||||
}
|
||||
}
|
||||
@@ -58,7 +59,7 @@ namespace SpineViewer.Controls
|
||||
{
|
||||
foreach (var file in Directory.EnumerateFiles(path, "*.*", SearchOption.AllDirectories))
|
||||
{
|
||||
if (SpineHelper.CommonSkelSuffix.Contains(Path.GetExtension(file).ToLower()))
|
||||
if (SpineUtils.CommonSkelSuffix.Contains(Path.GetExtension(file).ToLower()))
|
||||
listBox.Items.Add(file);
|
||||
}
|
||||
}
|
||||
|
||||
38
SpineViewer/Controls/SpineListView.Designer.cs
generated
38
SpineViewer/Controls/SpineListView.Designer.cs
generated
@@ -98,21 +98,21 @@
|
||||
contextMenuStrip.ImageScalingSize = new Size(24, 24);
|
||||
contextMenuStrip.Items.AddRange(new ToolStripItem[] { toolStripMenuItem_Add, toolStripMenuItem_Insert, toolStripMenuItem_Remove, toolStripSeparator1, toolStripMenuItem_BatchAdd, toolStripMenuItem_RemoveAll, toolStripSeparator2, toolStripMenuItem_MoveUp, toolStripMenuItem_MoveDown, toolStripMenuItem_MoveTop, toolStripMenuItem_MoveBottom, toolStripSeparator3, toolStripMenuItem_CopyPreview, toolStripMenuItem_AddFromClipboard, toolStripMenuItem_SelectAll, toolStripSeparator4, toolStripMenuItem_ChangeView });
|
||||
contextMenuStrip.Name = "contextMenuStrip";
|
||||
contextMenuStrip.Size = new Size(329, 418);
|
||||
contextMenuStrip.Size = new Size(255, 451);
|
||||
contextMenuStrip.Closed += contextMenuStrip_Closed;
|
||||
contextMenuStrip.Opening += contextMenuStrip_Opening;
|
||||
//
|
||||
// toolStripMenuItem_Add
|
||||
//
|
||||
toolStripMenuItem_Add.Name = "toolStripMenuItem_Add";
|
||||
toolStripMenuItem_Add.Size = new Size(328, 30);
|
||||
toolStripMenuItem_Add.Size = new Size(254, 30);
|
||||
toolStripMenuItem_Add.Text = "添加...";
|
||||
toolStripMenuItem_Add.Click += toolStripMenuItem_Add_Click;
|
||||
//
|
||||
// toolStripMenuItem_Insert
|
||||
//
|
||||
toolStripMenuItem_Insert.Name = "toolStripMenuItem_Insert";
|
||||
toolStripMenuItem_Insert.Size = new Size(328, 30);
|
||||
toolStripMenuItem_Insert.Size = new Size(254, 30);
|
||||
toolStripMenuItem_Insert.Text = "插入...";
|
||||
toolStripMenuItem_Insert.Click += toolStripMenuItem_Insert_Click;
|
||||
//
|
||||
@@ -120,39 +120,39 @@
|
||||
//
|
||||
toolStripMenuItem_Remove.Name = "toolStripMenuItem_Remove";
|
||||
toolStripMenuItem_Remove.ShortcutKeys = Keys.Delete;
|
||||
toolStripMenuItem_Remove.Size = new Size(328, 30);
|
||||
toolStripMenuItem_Remove.Size = new Size(254, 30);
|
||||
toolStripMenuItem_Remove.Text = "移除";
|
||||
toolStripMenuItem_Remove.Click += toolStripMenuItem_Remove_Click;
|
||||
//
|
||||
// toolStripSeparator1
|
||||
//
|
||||
toolStripSeparator1.Name = "toolStripSeparator1";
|
||||
toolStripSeparator1.Size = new Size(325, 6);
|
||||
toolStripSeparator1.Size = new Size(251, 6);
|
||||
//
|
||||
// toolStripMenuItem_BatchAdd
|
||||
//
|
||||
toolStripMenuItem_BatchAdd.Name = "toolStripMenuItem_BatchAdd";
|
||||
toolStripMenuItem_BatchAdd.Size = new Size(328, 30);
|
||||
toolStripMenuItem_BatchAdd.Size = new Size(254, 30);
|
||||
toolStripMenuItem_BatchAdd.Text = "批量添加...";
|
||||
toolStripMenuItem_BatchAdd.Click += toolStripMenuItem_BatchAdd_Click;
|
||||
//
|
||||
// toolStripMenuItem_RemoveAll
|
||||
//
|
||||
toolStripMenuItem_RemoveAll.Name = "toolStripMenuItem_RemoveAll";
|
||||
toolStripMenuItem_RemoveAll.Size = new Size(328, 30);
|
||||
toolStripMenuItem_RemoveAll.Size = new Size(254, 30);
|
||||
toolStripMenuItem_RemoveAll.Text = "移除全部";
|
||||
toolStripMenuItem_RemoveAll.Click += toolStripMenuItem_RemoveAll_Click;
|
||||
//
|
||||
// toolStripSeparator2
|
||||
//
|
||||
toolStripSeparator2.Name = "toolStripSeparator2";
|
||||
toolStripSeparator2.Size = new Size(325, 6);
|
||||
toolStripSeparator2.Size = new Size(251, 6);
|
||||
//
|
||||
// toolStripMenuItem_MoveUp
|
||||
//
|
||||
toolStripMenuItem_MoveUp.Name = "toolStripMenuItem_MoveUp";
|
||||
toolStripMenuItem_MoveUp.ShortcutKeys = Keys.Alt | Keys.W;
|
||||
toolStripMenuItem_MoveUp.Size = new Size(328, 30);
|
||||
toolStripMenuItem_MoveUp.Size = new Size(254, 30);
|
||||
toolStripMenuItem_MoveUp.Text = "上移";
|
||||
toolStripMenuItem_MoveUp.Click += toolStripMenuItem_MoveUp_Click;
|
||||
//
|
||||
@@ -160,7 +160,7 @@
|
||||
//
|
||||
toolStripMenuItem_MoveDown.Name = "toolStripMenuItem_MoveDown";
|
||||
toolStripMenuItem_MoveDown.ShortcutKeys = Keys.Alt | Keys.S;
|
||||
toolStripMenuItem_MoveDown.Size = new Size(328, 30);
|
||||
toolStripMenuItem_MoveDown.Size = new Size(254, 30);
|
||||
toolStripMenuItem_MoveDown.Text = "下移";
|
||||
toolStripMenuItem_MoveDown.Click += toolStripMenuItem_MoveDown_Click;
|
||||
//
|
||||
@@ -168,7 +168,7 @@
|
||||
//
|
||||
toolStripMenuItem_MoveTop.Name = "toolStripMenuItem_MoveTop";
|
||||
toolStripMenuItem_MoveTop.ShortcutKeys = Keys.Alt | Keys.Shift | Keys.W;
|
||||
toolStripMenuItem_MoveTop.Size = new Size(328, 30);
|
||||
toolStripMenuItem_MoveTop.Size = new Size(254, 30);
|
||||
toolStripMenuItem_MoveTop.Text = "置顶";
|
||||
toolStripMenuItem_MoveTop.Click += toolStripMenuItem_MoveTop_Click;
|
||||
//
|
||||
@@ -176,28 +176,28 @@
|
||||
//
|
||||
toolStripMenuItem_MoveBottom.Name = "toolStripMenuItem_MoveBottom";
|
||||
toolStripMenuItem_MoveBottom.ShortcutKeys = Keys.Alt | Keys.Shift | Keys.S;
|
||||
toolStripMenuItem_MoveBottom.Size = new Size(328, 30);
|
||||
toolStripMenuItem_MoveBottom.Size = new Size(254, 30);
|
||||
toolStripMenuItem_MoveBottom.Text = "置底";
|
||||
toolStripMenuItem_MoveBottom.Click += toolStripMenuItem_MoveBottom_Click;
|
||||
//
|
||||
// toolStripSeparator3
|
||||
//
|
||||
toolStripSeparator3.Name = "toolStripSeparator3";
|
||||
toolStripSeparator3.Size = new Size(325, 6);
|
||||
toolStripSeparator3.Size = new Size(251, 6);
|
||||
//
|
||||
// toolStripMenuItem_CopyPreview
|
||||
//
|
||||
toolStripMenuItem_CopyPreview.Name = "toolStripMenuItem_CopyPreview";
|
||||
toolStripMenuItem_CopyPreview.ShortcutKeys = Keys.Control | Keys.C;
|
||||
toolStripMenuItem_CopyPreview.Size = new Size(328, 30);
|
||||
toolStripMenuItem_CopyPreview.Text = "复制预览图 (256x256)";
|
||||
toolStripMenuItem_CopyPreview.Size = new Size(254, 30);
|
||||
toolStripMenuItem_CopyPreview.Text = "复制预览图";
|
||||
toolStripMenuItem_CopyPreview.Click += toolStripMenuItem_CopyPreview_Click;
|
||||
//
|
||||
// toolStripMenuItem_AddFromClipboard
|
||||
//
|
||||
toolStripMenuItem_AddFromClipboard.Name = "toolStripMenuItem_AddFromClipboard";
|
||||
toolStripMenuItem_AddFromClipboard.ShortcutKeys = Keys.Control | Keys.V;
|
||||
toolStripMenuItem_AddFromClipboard.Size = new Size(328, 30);
|
||||
toolStripMenuItem_AddFromClipboard.Size = new Size(254, 30);
|
||||
toolStripMenuItem_AddFromClipboard.Text = "从剪贴板添加";
|
||||
toolStripMenuItem_AddFromClipboard.Click += toolStripMenuItem_AddFromClipboard_Click;
|
||||
//
|
||||
@@ -205,20 +205,20 @@
|
||||
//
|
||||
toolStripMenuItem_SelectAll.Name = "toolStripMenuItem_SelectAll";
|
||||
toolStripMenuItem_SelectAll.ShortcutKeys = Keys.Control | Keys.A;
|
||||
toolStripMenuItem_SelectAll.Size = new Size(328, 30);
|
||||
toolStripMenuItem_SelectAll.Size = new Size(254, 30);
|
||||
toolStripMenuItem_SelectAll.Text = "全选";
|
||||
toolStripMenuItem_SelectAll.Click += toolStripMenuItem_SelectAll_Click;
|
||||
//
|
||||
// toolStripSeparator4
|
||||
//
|
||||
toolStripSeparator4.Name = "toolStripSeparator4";
|
||||
toolStripSeparator4.Size = new Size(325, 6);
|
||||
toolStripSeparator4.Size = new Size(251, 6);
|
||||
//
|
||||
// toolStripMenuItem_ChangeView
|
||||
//
|
||||
toolStripMenuItem_ChangeView.DropDownItems.AddRange(new ToolStripItem[] { toolStripMenuItem_LargeIconView, toolStripMenuItem_ListView, toolStripMenuItem_DetailsView });
|
||||
toolStripMenuItem_ChangeView.Name = "toolStripMenuItem_ChangeView";
|
||||
toolStripMenuItem_ChangeView.Size = new Size(328, 30);
|
||||
toolStripMenuItem_ChangeView.Size = new Size(254, 30);
|
||||
toolStripMenuItem_ChangeView.Text = "切换视图";
|
||||
//
|
||||
// toolStripMenuItem_LargeIconView
|
||||
|
||||
@@ -14,8 +14,8 @@ using System.Diagnostics;
|
||||
using System.Collections.Specialized;
|
||||
using NLog;
|
||||
using SpineViewer.Extensions;
|
||||
using SpineViewer.PropertyGridWrappers.Spine;
|
||||
using SpineViewer.Utilities;
|
||||
using SpineViewer.Utils;
|
||||
using SpineViewer.Spine.SpineView;
|
||||
|
||||
namespace SpineViewer.Controls
|
||||
{
|
||||
@@ -29,17 +29,17 @@ namespace SpineViewer.Controls
|
||||
/// <summary>
|
||||
/// Spine 列表只读视图, 访问时必须使用 lock 语句锁定视图本身
|
||||
/// </summary>
|
||||
public readonly ReadOnlyCollection<Spine.Spine> Spines;
|
||||
public readonly ReadOnlyCollection<SpineObject> Spines;
|
||||
|
||||
/// <summary>
|
||||
/// Spine 列表, 访问时必须使用 lock 语句锁定只读视图 Spines
|
||||
/// </summary>
|
||||
private readonly List<Spine.Spine> spines = [];
|
||||
private readonly List<SpineObject> spines = [];
|
||||
|
||||
/// <summary>
|
||||
/// 用于属性页显示模型参数的包装类
|
||||
/// </summary>
|
||||
private readonly Dictionary<string, SpineWrapper> spinePropertyWrappers = [];
|
||||
private readonly Dictionary<string, SpineObjectProperty> spinePropertyWrappers = [];
|
||||
|
||||
public SpineListView()
|
||||
{
|
||||
@@ -51,7 +51,7 @@ namespace SpineViewer.Controls
|
||||
/// 显示骨骼信息的属性面板
|
||||
/// </summary>
|
||||
[Category("自定义"), Description("用于显示模型属性的组合属性页")]
|
||||
public SpinePropertyGrid? SpinePropertyGrid { get; set; }
|
||||
public SpineViewPropertyGrid? SpinePropertyGrid { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 选中的索引
|
||||
@@ -80,7 +80,7 @@ namespace SpineViewer.Controls
|
||||
{
|
||||
try
|
||||
{
|
||||
var spine = Spine.Spine.New(result.Version, result.SkelPath, result.AtlasPath);
|
||||
var spine = SpineObject.New(result.Version, result.SkelPath, result.AtlasPath);
|
||||
|
||||
// 如果索引无效则在末尾添加
|
||||
if (index < 0 || index > listView.Items.Count)
|
||||
@@ -155,7 +155,7 @@ namespace SpineViewer.Controls
|
||||
|
||||
try
|
||||
{
|
||||
var spine = Spine.Spine.New(version, skelPath);
|
||||
var spine = SpineObject.New(version, skelPath);
|
||||
var preview = spine.Preview;
|
||||
lock (Spines) { spines.Add(spine); }
|
||||
spinePropertyWrappers[spine.ID] = new(spine);
|
||||
@@ -205,14 +205,14 @@ namespace SpineViewer.Controls
|
||||
{
|
||||
if (File.Exists(path))
|
||||
{
|
||||
if (SpineHelper.CommonSkelSuffix.Contains(Path.GetExtension(path).ToLower()))
|
||||
if (SpineUtils.CommonSkelSuffix.Contains(Path.GetExtension(path).ToLower()))
|
||||
validPaths.Add(path);
|
||||
}
|
||||
else if (Directory.Exists(path))
|
||||
{
|
||||
foreach (var file in Directory.EnumerateFiles(path, "*.*", SearchOption.AllDirectories))
|
||||
{
|
||||
if (SpineHelper.CommonSkelSuffix.Contains(Path.GetExtension(file).ToLower()))
|
||||
if (SpineUtils.CommonSkelSuffix.Contains(Path.GetExtension(file).ToLower()))
|
||||
validPaths.Add(file);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
namespace SpineViewer.Controls
|
||||
{
|
||||
partial class SpinePreviewer
|
||||
partial class SpinePreviewPanel
|
||||
{
|
||||
/// <summary>
|
||||
/// 必需的设计器变量。
|
||||
@@ -29,10 +29,9 @@
|
||||
private void InitializeComponent()
|
||||
{
|
||||
components = new System.ComponentModel.Container();
|
||||
System.ComponentModel.ComponentResourceManager resources = new System.ComponentModel.ComponentResourceManager(typeof(SpinePreviewer));
|
||||
panel = new Panel();
|
||||
System.ComponentModel.ComponentResourceManager resources = new System.ComponentModel.ComponentResourceManager(typeof(SpinePreviewPanel));
|
||||
panel_Render = new Panel();
|
||||
tableLayoutPanel1 = new TableLayoutPanel();
|
||||
panel_Container = new Panel();
|
||||
flowLayoutPanel1 = new FlowLayoutPanel();
|
||||
button_Stop = new Button();
|
||||
imageList = new ImageList(components);
|
||||
@@ -40,31 +39,37 @@
|
||||
button_Start = new Button();
|
||||
button_ForwardStep = new Button();
|
||||
button_ForwardFast = new Button();
|
||||
button_FullScreen = new Button();
|
||||
panel_ViewContainer = new Panel();
|
||||
panel_RenderContainer = new Panel();
|
||||
toolTip = new ToolTip(components);
|
||||
spinePreviewFullScreenForm = new SpineViewer.Forms.SpinePreviewFullScreenForm();
|
||||
wallpaperForm = new WallpaperForm();
|
||||
tableLayoutPanel1.SuspendLayout();
|
||||
panel_Container.SuspendLayout();
|
||||
flowLayoutPanel1.SuspendLayout();
|
||||
panel_ViewContainer.SuspendLayout();
|
||||
panel_RenderContainer.SuspendLayout();
|
||||
SuspendLayout();
|
||||
//
|
||||
// panel
|
||||
// panel_Render
|
||||
//
|
||||
panel.BackColor = SystemColors.ControlDarkDark;
|
||||
panel.Location = new Point(157, 136);
|
||||
panel.Margin = new Padding(0);
|
||||
panel.Name = "panel";
|
||||
panel.Size = new Size(320, 320);
|
||||
panel.TabIndex = 1;
|
||||
panel.MouseDown += panel_MouseDown;
|
||||
panel.MouseMove += panel_MouseMove;
|
||||
panel.MouseUp += panel_MouseUp;
|
||||
panel.MouseWheel += panel_MouseWheel;
|
||||
panel_Render.BackColor = SystemColors.ControlDarkDark;
|
||||
panel_Render.Location = new Point(157, 136);
|
||||
panel_Render.Margin = new Padding(0);
|
||||
panel_Render.Name = "panel_Render";
|
||||
panel_Render.Size = new Size(320, 320);
|
||||
panel_Render.TabIndex = 1;
|
||||
panel_Render.MouseDown += panel_Render_MouseDown;
|
||||
panel_Render.MouseMove += panel_Render_MouseMove;
|
||||
panel_Render.MouseUp += panel_Render_MouseUp;
|
||||
panel_Render.MouseWheel += panel_Render_MouseWheel;
|
||||
//
|
||||
// tableLayoutPanel1
|
||||
//
|
||||
tableLayoutPanel1.ColumnCount = 1;
|
||||
tableLayoutPanel1.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 100F));
|
||||
tableLayoutPanel1.Controls.Add(panel_Container, 0, 0);
|
||||
tableLayoutPanel1.Controls.Add(flowLayoutPanel1, 0, 1);
|
||||
tableLayoutPanel1.Controls.Add(panel_ViewContainer, 0, 0);
|
||||
tableLayoutPanel1.Dock = DockStyle.Fill;
|
||||
tableLayoutPanel1.Location = new Point(0, 0);
|
||||
tableLayoutPanel1.Margin = new Padding(0);
|
||||
@@ -75,17 +80,6 @@
|
||||
tableLayoutPanel1.Size = new Size(641, 636);
|
||||
tableLayoutPanel1.TabIndex = 2;
|
||||
//
|
||||
// panel_Container
|
||||
//
|
||||
panel_Container.BackColor = SystemColors.ControlDark;
|
||||
panel_Container.Controls.Add(panel);
|
||||
panel_Container.Dock = DockStyle.Fill;
|
||||
panel_Container.Location = new Point(0, 0);
|
||||
panel_Container.Margin = new Padding(0);
|
||||
panel_Container.Name = "panel_Container";
|
||||
panel_Container.Size = new Size(641, 594);
|
||||
panel_Container.TabIndex = 0;
|
||||
//
|
||||
// flowLayoutPanel1
|
||||
//
|
||||
flowLayoutPanel1.Anchor = AnchorStyles.None;
|
||||
@@ -96,10 +90,11 @@
|
||||
flowLayoutPanel1.Controls.Add(button_Start);
|
||||
flowLayoutPanel1.Controls.Add(button_ForwardStep);
|
||||
flowLayoutPanel1.Controls.Add(button_ForwardFast);
|
||||
flowLayoutPanel1.Location = new Point(138, 594);
|
||||
flowLayoutPanel1.Controls.Add(button_FullScreen);
|
||||
flowLayoutPanel1.Location = new Point(101, 594);
|
||||
flowLayoutPanel1.Margin = new Padding(0);
|
||||
flowLayoutPanel1.Name = "flowLayoutPanel1";
|
||||
flowLayoutPanel1.Size = new Size(365, 42);
|
||||
flowLayoutPanel1.Size = new Size(438, 42);
|
||||
flowLayoutPanel1.TabIndex = 1;
|
||||
//
|
||||
// button_Stop
|
||||
@@ -122,18 +117,19 @@
|
||||
imageList.ColorDepth = ColorDepth.Depth32Bit;
|
||||
imageList.ImageStream = (ImageListStreamer)resources.GetObject("imageList.ImageStream");
|
||||
imageList.TransparentColor = Color.Transparent;
|
||||
imageList.Images.SetKeyName(0, "stop");
|
||||
imageList.Images.SetKeyName(1, "restart");
|
||||
imageList.Images.SetKeyName(2, "start");
|
||||
imageList.Images.SetKeyName(0, "arrows-maximize");
|
||||
imageList.Images.SetKeyName(1, "forward-fast");
|
||||
imageList.Images.SetKeyName(2, "forward-step");
|
||||
imageList.Images.SetKeyName(3, "pause");
|
||||
imageList.Images.SetKeyName(4, "forward-step");
|
||||
imageList.Images.SetKeyName(5, "forward-fast");
|
||||
imageList.Images.SetKeyName(4, "rotate-left");
|
||||
imageList.Images.SetKeyName(5, "start");
|
||||
imageList.Images.SetKeyName(6, "stop");
|
||||
//
|
||||
// button_Restart
|
||||
//
|
||||
button_Restart.AutoSize = true;
|
||||
button_Restart.AutoSizeMode = AutoSizeMode.GrowAndShrink;
|
||||
button_Restart.ImageKey = "restart";
|
||||
button_Restart.ImageKey = "rotate-left";
|
||||
button_Restart.ImageList = imageList;
|
||||
button_Restart.Location = new Point(76, 3);
|
||||
button_Restart.Name = "button_Restart";
|
||||
@@ -190,27 +186,95 @@
|
||||
button_ForwardFast.UseVisualStyleBackColor = true;
|
||||
button_ForwardFast.Click += button_ForwardFast_Click;
|
||||
//
|
||||
// SpinePreviewer
|
||||
// button_FullScreen
|
||||
//
|
||||
button_FullScreen.AutoSize = true;
|
||||
button_FullScreen.AutoSizeMode = AutoSizeMode.GrowAndShrink;
|
||||
button_FullScreen.ImageKey = "arrows-maximize";
|
||||
button_FullScreen.ImageList = imageList;
|
||||
button_FullScreen.Location = new Point(368, 3);
|
||||
button_FullScreen.Name = "button_FullScreen";
|
||||
button_FullScreen.Padding = new Padding(15, 3, 15, 3);
|
||||
button_FullScreen.Size = new Size(67, 36);
|
||||
button_FullScreen.TabIndex = 5;
|
||||
toolTip.SetToolTip(button_FullScreen, "全屏预览");
|
||||
button_FullScreen.UseVisualStyleBackColor = true;
|
||||
button_FullScreen.Click += button_FullScreen_Click;
|
||||
//
|
||||
// panel_ViewContainer
|
||||
//
|
||||
panel_ViewContainer.Controls.Add(panel_RenderContainer);
|
||||
panel_ViewContainer.Dock = DockStyle.Fill;
|
||||
panel_ViewContainer.Location = new Point(0, 0);
|
||||
panel_ViewContainer.Margin = new Padding(0);
|
||||
panel_ViewContainer.Name = "panel_ViewContainer";
|
||||
panel_ViewContainer.Size = new Size(641, 594);
|
||||
panel_ViewContainer.TabIndex = 6;
|
||||
//
|
||||
// panel_RenderContainer
|
||||
//
|
||||
panel_RenderContainer.BackColor = SystemColors.ControlDark;
|
||||
panel_RenderContainer.Controls.Add(panel_Render);
|
||||
panel_RenderContainer.Dock = DockStyle.Fill;
|
||||
panel_RenderContainer.Location = new Point(0, 0);
|
||||
panel_RenderContainer.Margin = new Padding(0);
|
||||
panel_RenderContainer.Name = "panel_RenderContainer";
|
||||
panel_RenderContainer.Size = new Size(641, 594);
|
||||
panel_RenderContainer.TabIndex = 0;
|
||||
panel_RenderContainer.SizeChanged += panel_RenderContainer_SizeChanged;
|
||||
//
|
||||
// spinePreviewFullScreenForm
|
||||
//
|
||||
spinePreviewFullScreenForm.ClientSize = new Size(2560, 1440);
|
||||
spinePreviewFullScreenForm.ControlBox = false;
|
||||
spinePreviewFullScreenForm.FormBorderStyle = FormBorderStyle.None;
|
||||
spinePreviewFullScreenForm.MaximizeBox = false;
|
||||
spinePreviewFullScreenForm.MinimizeBox = false;
|
||||
spinePreviewFullScreenForm.Name = "SpinePreviewFullScreenForm";
|
||||
spinePreviewFullScreenForm.ShowIcon = false;
|
||||
spinePreviewFullScreenForm.ShowInTaskbar = false;
|
||||
spinePreviewFullScreenForm.StartPosition = FormStartPosition.Manual;
|
||||
spinePreviewFullScreenForm.TopMost = true;
|
||||
spinePreviewFullScreenForm.Visible = false;
|
||||
spinePreviewFullScreenForm.FormClosing += spinePreviewFullScreenForm_FormClosing;
|
||||
spinePreviewFullScreenForm.KeyDown += spinePreviewFullScreenForm_KeyDown;
|
||||
//
|
||||
// wallpaperForm
|
||||
//
|
||||
wallpaperForm.ClientSize = new Size(0, 0);
|
||||
wallpaperForm.ControlBox = false;
|
||||
wallpaperForm.FormBorderStyle = FormBorderStyle.None;
|
||||
wallpaperForm.MaximizeBox = false;
|
||||
wallpaperForm.MinimizeBox = false;
|
||||
wallpaperForm.Name = "WallpaperForm";
|
||||
wallpaperForm.ShowIcon = false;
|
||||
wallpaperForm.ShowInTaskbar = false;
|
||||
wallpaperForm.StartPosition = FormStartPosition.Manual;
|
||||
wallpaperForm.Visible = false;
|
||||
wallpaperForm.WindowState = FormWindowState.Minimized;
|
||||
wallpaperForm.FormClosing += wallpaperForm_FormClosing;
|
||||
//
|
||||
// SpinePreviewPanel
|
||||
//
|
||||
AutoScaleDimensions = new SizeF(11F, 24F);
|
||||
AutoScaleMode = AutoScaleMode.Font;
|
||||
Controls.Add(tableLayoutPanel1);
|
||||
Name = "SpinePreviewer";
|
||||
Name = "SpinePreviewPanel";
|
||||
Size = new Size(641, 636);
|
||||
SizeChanged += SpinePreviewer_SizeChanged;
|
||||
tableLayoutPanel1.ResumeLayout(false);
|
||||
tableLayoutPanel1.PerformLayout();
|
||||
panel_Container.ResumeLayout(false);
|
||||
flowLayoutPanel1.ResumeLayout(false);
|
||||
flowLayoutPanel1.PerformLayout();
|
||||
panel_ViewContainer.ResumeLayout(false);
|
||||
panel_RenderContainer.ResumeLayout(false);
|
||||
ResumeLayout(false);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
private Panel panel;
|
||||
private Panel panel_Render;
|
||||
private TableLayoutPanel tableLayoutPanel1;
|
||||
private Panel panel_Container;
|
||||
private Panel panel_RenderContainer;
|
||||
private FlowLayoutPanel flowLayoutPanel1;
|
||||
private Button button_Stop;
|
||||
private Button button_Start;
|
||||
@@ -219,5 +283,9 @@
|
||||
private Button button_ForwardStep;
|
||||
private Button button_ForwardFast;
|
||||
private Button button_Restart;
|
||||
private Button button_FullScreen;
|
||||
private Panel panel_ViewContainer;
|
||||
private Forms.SpinePreviewFullScreenForm spinePreviewFullScreenForm;
|
||||
private WallpaperForm wallpaperForm;
|
||||
}
|
||||
}
|
||||
@@ -10,30 +10,23 @@ using System.Windows.Forms;
|
||||
using System.Security.Policy;
|
||||
using System.Diagnostics;
|
||||
using NLog;
|
||||
using SpineViewer.Utilities;
|
||||
using SpineViewer.Utils;
|
||||
using System.Drawing.Design;
|
||||
|
||||
namespace SpineViewer.Controls
|
||||
{
|
||||
public partial class SpinePreviewer : UserControl
|
||||
public partial class SpinePreviewPanel : UserControl
|
||||
{
|
||||
public SpinePreviewPanel()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 日志器
|
||||
/// </summary>
|
||||
private readonly Logger logger = LogManager.GetCurrentClassLogger();
|
||||
|
||||
public SpinePreviewer()
|
||||
{
|
||||
InitializeComponent();
|
||||
RenderWindow = new(panel.Handle);
|
||||
RenderWindow.SetActive(false);
|
||||
|
||||
// 设置默认参数
|
||||
Resolution = new(2048, 2048);
|
||||
Center = new(0, 0);
|
||||
FlipY = true;
|
||||
MaxFps = 30;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 要绑定的 Spine 列表控件
|
||||
/// </summary>
|
||||
@@ -51,7 +44,7 @@ namespace SpineViewer.Controls
|
||||
{
|
||||
propertyGrid = value;
|
||||
if (propertyGrid is not null)
|
||||
propertyGrid.SelectedObject = new PropertyGridWrappers.SpinePreviewerWrapper(this);
|
||||
propertyGrid.SelectedObject = new SpinePreviewPanelProperty(this);
|
||||
}
|
||||
}
|
||||
private PropertyGrid? propertyGrid;
|
||||
@@ -68,42 +61,38 @@ namespace SpineViewer.Controls
|
||||
get => resolution;
|
||||
set
|
||||
{
|
||||
if (renderWindow is null) return;
|
||||
|
||||
if (value == resolution) return;
|
||||
if (value.Width <= 0) value.Width = 100;
|
||||
if (value.Height <= 0) value.Height = 100;
|
||||
|
||||
float parentX = panel.Parent.Width;
|
||||
float parentY = panel.Parent.Height;
|
||||
float sizeX = value.Width;
|
||||
float sizeY = value.Height;
|
||||
var previousZoom = Zoom;
|
||||
|
||||
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 = value.Width;
|
||||
float renderH = value.Height;
|
||||
float scale = Math.Min(parentW / renderW, parentH / renderH); // 两方向取较小值, 保证 parent 覆盖 render
|
||||
renderW *= scale;
|
||||
renderH *= scale;
|
||||
|
||||
// 必须通过 SFML 的方法调整窗口
|
||||
RenderWindow.Position = new((int)(parentX - sizeX) / 2, (int)(parentY - sizeY) / 2);
|
||||
RenderWindow.Size = new((uint)sizeX, (uint)sizeY);
|
||||
|
||||
// 将 view 的大小设置成于 resolution 相同的大小, 其余属性都不变
|
||||
using var view = RenderWindow.GetView();
|
||||
var signX = Math.Sign(view.Size.X);
|
||||
var signY = Math.Sign(view.Size.Y);
|
||||
view.Size = new(value.Width * signX, value.Height * signY);
|
||||
RenderWindow.SetView(view);
|
||||
|
||||
renderWindow.Position = new((int)((parentW - renderW) / 2 + 0.5), (int)((parentH - renderH) / 2 + 0.5));
|
||||
renderWindow.Size = new((uint)(renderW + 0.5), (uint)(renderH + 0.5));
|
||||
resolution = value;
|
||||
|
||||
// 设置完 resolution 后还原缩放比例
|
||||
Zoom = previousZoom;
|
||||
|
||||
// 设置壁纸窗口分辨率
|
||||
using var view = renderWindow.GetView();
|
||||
wallpaperWindow.SetView(view);
|
||||
wallpaperForm.Size = value; // 必须两个 Size 都设置
|
||||
wallpaperWindow.Size = new((uint)value.Width, (uint)value.Height);
|
||||
}
|
||||
}
|
||||
private Size resolution = new(0, 0);
|
||||
private Size resolution = new(100, 100);
|
||||
|
||||
/// <summary>
|
||||
/// 画面中心点
|
||||
@@ -114,15 +103,20 @@ namespace SpineViewer.Controls
|
||||
{
|
||||
get
|
||||
{
|
||||
using var view = RenderWindow.GetView();
|
||||
if (renderWindow is null) return new(-1, -1);
|
||||
|
||||
using var view = renderWindow.GetView();
|
||||
var center = view.Center;
|
||||
return new(center.X, center.Y);
|
||||
}
|
||||
set
|
||||
{
|
||||
using var view = RenderWindow.GetView();
|
||||
if (renderWindow is null) return;
|
||||
|
||||
using var view = renderWindow.GetView();
|
||||
view.Center = new(value.X, value.Y);
|
||||
RenderWindow.SetView(view);
|
||||
renderWindow.SetView(view);
|
||||
wallpaperWindow.SetView(view);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -135,17 +129,22 @@ namespace SpineViewer.Controls
|
||||
{
|
||||
get
|
||||
{
|
||||
using var view = RenderWindow.GetView();
|
||||
if (renderWindow is null) return -1;
|
||||
|
||||
using var view = renderWindow.GetView();
|
||||
return resolution.Width / Math.Abs(view.Size.X);
|
||||
}
|
||||
set
|
||||
{
|
||||
if (renderWindow is null) return;
|
||||
|
||||
value = Math.Clamp(value, 0.001f, 1000f);
|
||||
using var view = RenderWindow.GetView();
|
||||
using var view = renderWindow.GetView();
|
||||
var signX = Math.Sign(view.Size.X);
|
||||
var signY = Math.Sign(view.Size.Y);
|
||||
view.Size = new(resolution.Width / value * signX, resolution.Height / value * signY);
|
||||
RenderWindow.SetView(view);
|
||||
renderWindow.SetView(view);
|
||||
wallpaperWindow.SetView(view);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -158,14 +157,19 @@ namespace SpineViewer.Controls
|
||||
{
|
||||
get
|
||||
{
|
||||
using var view = RenderWindow.GetView();
|
||||
if (renderWindow is null) return -1;
|
||||
|
||||
using var view = renderWindow.GetView();
|
||||
return view.Rotation;
|
||||
}
|
||||
set
|
||||
{
|
||||
using var view = RenderWindow.GetView();
|
||||
if (renderWindow is null) return;
|
||||
|
||||
using var view = renderWindow.GetView();
|
||||
view.Rotation = value;
|
||||
RenderWindow.SetView(view);
|
||||
renderWindow.SetView(view);
|
||||
wallpaperWindow.SetView(view);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -178,17 +182,22 @@ namespace SpineViewer.Controls
|
||||
{
|
||||
get
|
||||
{
|
||||
using var view = RenderWindow.GetView();
|
||||
if (renderWindow is null) return false;
|
||||
|
||||
using var view = renderWindow.GetView();
|
||||
return view.Size.X < 0;
|
||||
}
|
||||
set
|
||||
{
|
||||
using var view = RenderWindow.GetView();
|
||||
if (renderWindow is null) return;
|
||||
|
||||
using var view = renderWindow.GetView();
|
||||
var size = view.Size;
|
||||
if (size.X > 0 && value || size.X < 0 && !value)
|
||||
size.X *= -1;
|
||||
view.Size = size;
|
||||
RenderWindow.SetView(view);
|
||||
renderWindow.SetView(view);
|
||||
wallpaperWindow.SetView(view);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -201,17 +210,22 @@ namespace SpineViewer.Controls
|
||||
{
|
||||
get
|
||||
{
|
||||
using var view = RenderWindow.GetView();
|
||||
if (renderWindow is null) return false;
|
||||
|
||||
using var view = renderWindow.GetView();
|
||||
return view.Size.Y < 0;
|
||||
}
|
||||
set
|
||||
{
|
||||
using var view = RenderWindow.GetView();
|
||||
if (renderWindow is null) return;
|
||||
|
||||
using var view = renderWindow.GetView();
|
||||
var size = view.Size;
|
||||
if (size.Y > 0 && value || size.Y < 0 && !value)
|
||||
size.Y *= -1;
|
||||
view.Size = size;
|
||||
RenderWindow.SetView(view);
|
||||
renderWindow.SetView(view);
|
||||
wallpaperWindow.SetView(view);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -234,22 +248,63 @@ namespace SpineViewer.Controls
|
||||
/// </summary>
|
||||
[DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
|
||||
[Browsable(false)]
|
||||
public uint MaxFps { get => maxFps; set { RenderWindow.SetFramerateLimit(value); maxFps = value; } }
|
||||
public uint MaxFps
|
||||
{
|
||||
get => maxFps;
|
||||
set
|
||||
{
|
||||
if (renderWindow is null) return;
|
||||
|
||||
renderWindow.SetFramerateLimit(value);
|
||||
maxFps = value;
|
||||
}
|
||||
}
|
||||
private uint maxFps = 60;
|
||||
|
||||
/// <summary>
|
||||
/// 获取 View
|
||||
/// </summary>
|
||||
public SFML.Graphics.View GetView() => RenderWindow.GetView();
|
||||
public SFML.Graphics.View GetView() => renderWindow.GetView();
|
||||
|
||||
#endregion
|
||||
/// <summary>
|
||||
/// 是否开启桌面投影
|
||||
/// </summary>
|
||||
[DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
|
||||
[Browsable(false)]
|
||||
public bool EnableDesktopProjection
|
||||
{
|
||||
get => enableDesktopProjection;
|
||||
set
|
||||
{
|
||||
if (renderWindow is null) return;
|
||||
|
||||
#region 渲染管理
|
||||
if (enableDesktopProjection == value) return;
|
||||
if (value)
|
||||
{
|
||||
var screenBounds = Screen.FromControl(this).Bounds;
|
||||
Resolution = screenBounds.Size;
|
||||
wallpaperWindow.Position = new(screenBounds.X, screenBounds.Y);
|
||||
wallpaperForm.Show();
|
||||
}
|
||||
else
|
||||
{
|
||||
wallpaperForm.Hide();
|
||||
}
|
||||
enableDesktopProjection = value;
|
||||
}
|
||||
}
|
||||
private bool enableDesktopProjection = false;
|
||||
|
||||
/// <summary>
|
||||
/// 预览画面背景色
|
||||
/// </summary>
|
||||
private static readonly SFML.Graphics.Color BackgroundColor = new(105, 105, 105);
|
||||
[DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
|
||||
[Browsable(false)]
|
||||
public SFML.Graphics.Color BackgroundColor { get; set; } = new(105, 105, 105);
|
||||
|
||||
#endregion
|
||||
|
||||
#region 渲染管理
|
||||
|
||||
/// <summary>
|
||||
/// 预览画面坐标轴颜色
|
||||
@@ -259,17 +314,22 @@ namespace SpineViewer.Controls
|
||||
/// <summary>
|
||||
/// 坐标轴顶点缓冲区
|
||||
/// </summary>
|
||||
private readonly SFML.Graphics.VertexArray AxisVertex = new(SFML.Graphics.PrimitiveType.Lines, 2);
|
||||
private readonly SFML.Graphics.VertexArray axisVertices = new(SFML.Graphics.PrimitiveType.Lines, 2); // XXX: 暂时未使用 Dispose 释放
|
||||
|
||||
/// <summary>
|
||||
/// 渲染窗口
|
||||
/// </summary>
|
||||
private readonly SFML.Graphics.RenderWindow RenderWindow;
|
||||
private SFML.Graphics.RenderWindow renderWindow;
|
||||
|
||||
/// <summary>
|
||||
/// 壁纸窗口
|
||||
/// </summary>
|
||||
private SFML.Graphics.RenderWindow wallpaperWindow;
|
||||
|
||||
/// <summary>
|
||||
/// 帧间隔计时器
|
||||
/// </summary>
|
||||
private readonly SFML.System.Clock Clock = new();
|
||||
private readonly SFML.System.Clock clock = new();
|
||||
|
||||
/// <summary>
|
||||
/// 渲染任务
|
||||
@@ -312,11 +372,27 @@ namespace SpineViewer.Controls
|
||||
/// </summary>
|
||||
public void StartRender()
|
||||
{
|
||||
if (task is not null)
|
||||
return;
|
||||
// 延迟到第一次开启渲染时进行初始化
|
||||
if (renderWindow is null)
|
||||
{
|
||||
renderWindow = new(panel_Render.Handle);
|
||||
renderWindow.SetActive(false);
|
||||
wallpaperWindow = new(wallpaperForm.Handle);
|
||||
wallpaperWindow.SetActive(false);
|
||||
|
||||
// 设置默认参数
|
||||
Resolution = new(2048, 2048);
|
||||
Zoom = 1;
|
||||
Center = new(0, 0);
|
||||
FlipY = true;
|
||||
MaxFps = 30;
|
||||
}
|
||||
|
||||
if (task is not null) return;
|
||||
cancelToken = new();
|
||||
task = Task.Run(RenderTask, cancelToken.Token);
|
||||
IsUpdating = true;
|
||||
if (enableDesktopProjection) wallpaperForm.Show();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -324,6 +400,7 @@ namespace SpineViewer.Controls
|
||||
/// </summary>
|
||||
public void StopRender()
|
||||
{
|
||||
wallpaperForm.Hide();
|
||||
IsUpdating = false;
|
||||
if (task is null || cancelToken is null)
|
||||
return;
|
||||
@@ -340,13 +417,14 @@ namespace SpineViewer.Controls
|
||||
{
|
||||
try
|
||||
{
|
||||
RenderWindow.SetActive(true);
|
||||
renderWindow.SetActive(true);
|
||||
wallpaperWindow.SetActive(true);
|
||||
|
||||
float delta;
|
||||
while (cancelToken is not null && !cancelToken.IsCancellationRequested)
|
||||
{
|
||||
delta = Clock.ElapsedTime.AsSeconds();
|
||||
Clock.Restart();
|
||||
delta = clock.ElapsedTime.AsSeconds();
|
||||
clock.Restart();
|
||||
|
||||
// 停止更新的时候只是时间不前进, 但是坐标变换还是要更新, 否则无法移动对象
|
||||
if (!IsUpdating) delta = 0;
|
||||
@@ -358,17 +436,18 @@ namespace SpineViewer.Controls
|
||||
forwardDelta = 0;
|
||||
}
|
||||
|
||||
RenderWindow.Clear(BackgroundColor);
|
||||
renderWindow.Clear(BackgroundColor);
|
||||
if (enableDesktopProjection) wallpaperWindow.Clear(BackgroundColor);
|
||||
|
||||
if (ShowAxis)
|
||||
{
|
||||
// 画一个很长的坐标轴, 用 1e9 比较合适
|
||||
AxisVertex[0] = new(new(-1e9f, 0), AxisColor);
|
||||
AxisVertex[1] = new(new(1e9f, 0), AxisColor);
|
||||
RenderWindow.Draw(AxisVertex);
|
||||
AxisVertex[0] = new(new(0, -1e9f), AxisColor);
|
||||
AxisVertex[1] = new(new(0, 1e9f), AxisColor);
|
||||
RenderWindow.Draw(AxisVertex);
|
||||
axisVertices[0] = new(new(-1e9f, 0), AxisColor);
|
||||
axisVertices[1] = new(new(1e9f, 0), AxisColor);
|
||||
renderWindow.Draw(axisVertices);
|
||||
axisVertices[0] = new(new(0, -1e9f), AxisColor);
|
||||
axisVertices[1] = new(new(0, 1e9f), AxisColor);
|
||||
renderWindow.Draw(axisVertices);
|
||||
}
|
||||
|
||||
// 渲染 Spine
|
||||
@@ -389,25 +468,30 @@ namespace SpineViewer.Controls
|
||||
if (RenderSelectedOnly && !spine.IsSelected)
|
||||
continue;
|
||||
|
||||
spine.IsDebug = true;
|
||||
RenderWindow.Draw(spine);
|
||||
spine.IsDebug = false;
|
||||
spine.EnableDebug = true;
|
||||
renderWindow.Draw(spine);
|
||||
spine.EnableDebug = false;
|
||||
|
||||
if (enableDesktopProjection) wallpaperWindow.Draw(spine);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
RenderWindow.Display();
|
||||
renderWindow.Display();
|
||||
|
||||
if (enableDesktopProjection) wallpaperWindow.Display();
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.Fatal(ex);
|
||||
logger.Fatal(ex.ToString());
|
||||
logger.Fatal("Render task stopped");
|
||||
MessagePopup.Error(ex.ToString(), "预览画面已停止渲染");
|
||||
}
|
||||
finally
|
||||
{
|
||||
RenderWindow.SetActive(false);
|
||||
renderWindow.SetActive(false);
|
||||
wallpaperWindow.SetActive(false);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -418,46 +502,37 @@ namespace SpineViewer.Controls
|
||||
/// </summary>
|
||||
private SFML.System.Vector2f? draggingSrc = null;
|
||||
|
||||
private void SpinePreviewer_SizeChanged(object sender, EventArgs e)
|
||||
private void panel_RenderContainer_SizeChanged(object sender, EventArgs e)
|
||||
{
|
||||
if (RenderWindow is null)
|
||||
return;
|
||||
if (renderWindow is null) return;
|
||||
|
||||
float parentX = panel.Parent.Width;
|
||||
float parentY = panel.Parent.Height;
|
||||
float sizeX = panel.Width;
|
||||
float sizeY = panel.Height;
|
||||
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
|
||||
renderW *= scale;
|
||||
renderH *= scale;
|
||||
|
||||
if ((sizeY / sizeX) < (parentY / parentX))
|
||||
{
|
||||
// 相同的 X, 子窗口 Y 更小
|
||||
sizeY = parentX * sizeY / sizeX;
|
||||
sizeX = parentX;
|
||||
}
|
||||
else
|
||||
{
|
||||
// 相同的 Y, 子窗口 X 更小
|
||||
sizeX = parentY * sizeX / sizeY;
|
||||
sizeY = parentY;
|
||||
}
|
||||
|
||||
// 必须通过 SFML 的方法调整窗口
|
||||
RenderWindow.Position = new((int)(parentX - sizeX) / 2, (int)(parentY - sizeY) / 2);
|
||||
RenderWindow.Size = new((uint)sizeX, (uint)sizeY);
|
||||
// 必须通过 SFML 的方法调整窗口, 此处必须四舍五入, 否则在全屏和小窗多次切换之后会有明显的舍入误差
|
||||
renderWindow.Position = new((int)((parentW - renderW) / 2 + 0.5), (int)((parentH - renderH) / 2 + 0.5));
|
||||
renderWindow.Size = new((uint)(renderW + 0.5), (uint)(renderH + 0.5));
|
||||
}
|
||||
|
||||
private void panel_MouseDown(object sender, MouseEventArgs e)
|
||||
private void panel_Render_MouseDown(object sender, MouseEventArgs e)
|
||||
{
|
||||
if (renderWindow is null) return;
|
||||
|
||||
// 右键优先级高, 进入画面拖动模式, 需要重新记录源点
|
||||
if ((e.Button & MouseButtons.Right) != 0)
|
||||
{
|
||||
draggingSrc = RenderWindow.MapPixelToCoords(new(e.X, e.Y));
|
||||
draggingSrc = renderWindow.MapPixelToCoords(new(e.X, e.Y));
|
||||
Cursor = Cursors.Hand;
|
||||
}
|
||||
// 按下了左键并且右键是松开的
|
||||
else if ((e.Button & MouseButtons.Left) != 0 && (MouseButtons & MouseButtons.Right) == 0)
|
||||
{
|
||||
draggingSrc = RenderWindow.MapPixelToCoords(new(e.X, e.Y));
|
||||
draggingSrc = renderWindow.MapPixelToCoords(new(e.X, e.Y));
|
||||
var src = new PointF(((SFML.System.Vector2f)draggingSrc).X, ((SFML.System.Vector2f)draggingSrc).Y);
|
||||
|
||||
if (SpineListView is null)
|
||||
@@ -475,7 +550,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 +567,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 +589,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;
|
||||
@@ -525,13 +600,14 @@ namespace SpineViewer.Controls
|
||||
}
|
||||
}
|
||||
|
||||
private void panel_MouseMove(object sender, MouseEventArgs e)
|
||||
private void panel_Render_MouseMove(object sender, MouseEventArgs e)
|
||||
{
|
||||
if (draggingSrc is null)
|
||||
return;
|
||||
if (renderWindow is null) return;
|
||||
|
||||
if (draggingSrc is null) return;
|
||||
|
||||
var src = (SFML.System.Vector2f)draggingSrc;
|
||||
var dst = RenderWindow.MapPixelToCoords(new(e.X, e.Y));
|
||||
var dst = renderWindow.MapPixelToCoords(new(e.X, e.Y));
|
||||
var _delta = dst - src;
|
||||
var delta = new SizeF(_delta.X, _delta.Y);
|
||||
|
||||
@@ -557,7 +633,7 @@ namespace SpineViewer.Controls
|
||||
}
|
||||
}
|
||||
|
||||
private void panel_MouseUp(object sender, MouseEventArgs e)
|
||||
private void panel_Render_MouseUp(object sender, MouseEventArgs e)
|
||||
{
|
||||
// 右键高优先级, 结束画面拖动模式
|
||||
if ((e.Button & MouseButtons.Right) != 0)
|
||||
@@ -576,10 +652,30 @@ namespace SpineViewer.Controls
|
||||
}
|
||||
}
|
||||
|
||||
private void panel_MouseWheel(object sender, MouseEventArgs e)
|
||||
private void panel_Render_MouseWheel(object sender, MouseEventArgs e)
|
||||
{
|
||||
Zoom *= (e.Delta > 0 ? 1.1f : 0.9f);
|
||||
PropertyGrid?.Refresh();
|
||||
var factor = (e.Delta > 0 ? 1.1f : 0.9f);
|
||||
if ((ModifierKeys & Keys.Control) == 0)
|
||||
{
|
||||
Zoom *= factor;
|
||||
PropertyGrid?.Refresh();
|
||||
}
|
||||
else
|
||||
{
|
||||
if (SpineListView is not null)
|
||||
{
|
||||
lock (SpineListView.Spines)
|
||||
{
|
||||
var spines = SpineListView.Spines;
|
||||
foreach (int i in SpineListView.SelectedIndices)
|
||||
{
|
||||
if (spines[i].IsHidden) continue;
|
||||
spines[i].Scale *= factor;
|
||||
}
|
||||
}
|
||||
SpineListView.SpinePropertyGrid?.Refresh();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void button_Stop_Click(object sender, EventArgs e)
|
||||
@@ -617,7 +713,10 @@ namespace SpineViewer.Controls
|
||||
{
|
||||
lock (_forwardDeltaLock)
|
||||
{
|
||||
forwardDelta += 1f / maxFps;
|
||||
if (maxFps > 0)
|
||||
forwardDelta += 1f / maxFps;
|
||||
else
|
||||
forwardDelta += 0.001f;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -625,14 +724,90 @@ namespace SpineViewer.Controls
|
||||
{
|
||||
lock (_forwardDeltaLock)
|
||||
{
|
||||
forwardDelta += 10f / maxFps;
|
||||
if (maxFps > 0)
|
||||
forwardDelta += 10f / maxFps;
|
||||
else
|
||||
forwardDelta += 0.01f;
|
||||
}
|
||||
}
|
||||
|
||||
private void button_FullScreen_Click(object sender, EventArgs e)
|
||||
{
|
||||
var screenBounds = Screen.FromControl(this).Bounds;
|
||||
Resolution = screenBounds.Size;
|
||||
spinePreviewFullScreenForm.Controls.Add(panel_RenderContainer);
|
||||
spinePreviewFullScreenForm.Bounds = screenBounds;
|
||||
spinePreviewFullScreenForm.Show();
|
||||
PropertyGrid?.Refresh();
|
||||
}
|
||||
|
||||
private void spinePreviewFullScreenForm_KeyDown(object sender, KeyEventArgs e)
|
||||
{
|
||||
if (e.KeyCode == Keys.Escape)
|
||||
{
|
||||
spinePreviewFullScreenForm.Hide();
|
||||
panel_ViewContainer.Controls.Add(panel_RenderContainer);
|
||||
}
|
||||
}
|
||||
|
||||
private void spinePreviewFullScreenForm_FormClosing(object sender, FormClosingEventArgs e)
|
||||
{
|
||||
e.Cancel = e.CloseReason == CloseReason.UserClosing;
|
||||
}
|
||||
|
||||
private void wallpaperForm_FormClosing(object sender, FormClosingEventArgs e)
|
||||
{
|
||||
e.Cancel = e.CloseReason == CloseReason.UserClosing;
|
||||
}
|
||||
|
||||
//public void ClickStopButton() => button_Stop_Click(button_Stop, EventArgs.Empty);
|
||||
//public void ClickRestartButton() => button_Restart_Click(button_Restart, EventArgs.Empty);
|
||||
//public void ClickStartButton() => button_Start_Click(button_Start, EventArgs.Empty);
|
||||
//public void ClickForwardStepButton() => button_ForwardStep_Click(button_ForwardStep, EventArgs.Empty);
|
||||
//public void ClickForwardFastButton() => button_ForwardFast_Click(button_ForwardFast, EventArgs.Empty);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 用于在 PropertyGrid 上显示 <see cref="SpinePreviewPanel"/> 属性的包装类, 提供用户操作接口
|
||||
/// </summary>
|
||||
public class SpinePreviewPanelProperty(SpinePreviewPanel previewPanel)
|
||||
{
|
||||
[Browsable(false)]
|
||||
public SpinePreviewPanel PreviewPanel { get; } = previewPanel;
|
||||
|
||||
[RefreshProperties(RefreshProperties.All)]
|
||||
[TypeConverter(typeof(ResolutionConverter))]
|
||||
[Category("[0] 导出"), DisplayName("分辨率")]
|
||||
public Size Resolution { get => PreviewPanel.Resolution; set => PreviewPanel.Resolution = value; }
|
||||
|
||||
[TypeConverter(typeof(PointFConverter))]
|
||||
[Category("[0] 导出"), DisplayName("画面中心点")]
|
||||
public PointF Center { get => PreviewPanel.Center; set => PreviewPanel.Center = value; }
|
||||
|
||||
[Category("[0] 导出"), DisplayName("缩放")]
|
||||
public float Zoom { get => PreviewPanel.Zoom; set => PreviewPanel.Zoom = value; }
|
||||
|
||||
[Category("[0] 导出"), DisplayName("旋转")]
|
||||
public float Rotation { get => PreviewPanel.Rotation; set => PreviewPanel.Rotation = value; }
|
||||
|
||||
[Category("[0] 导出"), DisplayName("水平翻转")]
|
||||
public bool FlipX { get => PreviewPanel.FlipX; set => PreviewPanel.FlipX = value; }
|
||||
|
||||
[Category("[0] 导出"), DisplayName("垂直翻转")]
|
||||
public bool FlipY { get => PreviewPanel.FlipY; set => PreviewPanel.FlipY = value; }
|
||||
|
||||
[Category("[0] 导出"), DisplayName("仅渲染选中")]
|
||||
public bool RenderSelectedOnly { get => PreviewPanel.RenderSelectedOnly; set => PreviewPanel.RenderSelectedOnly = value; }
|
||||
|
||||
[Category("[1] 预览"), DisplayName("显示坐标轴")]
|
||||
public bool ShowAxis { get => PreviewPanel.ShowAxis; set => PreviewPanel.ShowAxis = value; }
|
||||
|
||||
[Category("[1] 预览"), DisplayName("最大帧率")]
|
||||
public uint MaxFps { get => PreviewPanel.MaxFps; set => PreviewPanel.MaxFps = value; }
|
||||
|
||||
[Editor(typeof(SFMLColorEditor), typeof(UITypeEditor))]
|
||||
[TypeConverter(typeof(SFMLColorConverter))]
|
||||
[Category("[1] 预览"), DisplayName("背景颜色")]
|
||||
public SFML.Graphics.Color BackgroundColor { get => PreviewPanel.BackgroundColor; set => PreviewPanel.BackgroundColor = value; }
|
||||
}
|
||||
}
|
||||
293
SpineViewer/Controls/SpinePreviewPanel.resx
Normal file
293
SpineViewer/Controls/SpinePreviewPanel.resx
Normal file
@@ -0,0 +1,293 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<root>
|
||||
<!--
|
||||
Microsoft ResX Schema
|
||||
|
||||
Version 2.0
|
||||
|
||||
The primary goals of this format is to allow a simple XML format
|
||||
that is mostly human readable. The generation and parsing of the
|
||||
various data types are done through the TypeConverter classes
|
||||
associated with the data types.
|
||||
|
||||
Example:
|
||||
|
||||
... ado.net/XML headers & schema ...
|
||||
<resheader name="resmimetype">text/microsoft-resx</resheader>
|
||||
<resheader name="version">2.0</resheader>
|
||||
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
|
||||
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
|
||||
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
|
||||
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
|
||||
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
|
||||
<value>[base64 mime encoded serialized .NET Framework object]</value>
|
||||
</data>
|
||||
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
|
||||
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
|
||||
<comment>This is a comment</comment>
|
||||
</data>
|
||||
|
||||
There are any number of "resheader" rows that contain simple
|
||||
name/value pairs.
|
||||
|
||||
Each data row contains a name, and value. The row also contains a
|
||||
type or mimetype. Type corresponds to a .NET class that support
|
||||
text/value conversion through the TypeConverter architecture.
|
||||
Classes that don't support this are serialized and stored with the
|
||||
mimetype set.
|
||||
|
||||
The mimetype is used for serialized objects, and tells the
|
||||
ResXResourceReader how to depersist the object. This is currently not
|
||||
extensible. For a given mimetype the value must be set accordingly:
|
||||
|
||||
Note - application/x-microsoft.net.object.binary.base64 is the format
|
||||
that the ResXResourceWriter will generate, however the reader can
|
||||
read any of the formats listed below.
|
||||
|
||||
mimetype: application/x-microsoft.net.object.binary.base64
|
||||
value : The object must be serialized with
|
||||
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
|
||||
: and then encoded with base64 encoding.
|
||||
|
||||
mimetype: application/x-microsoft.net.object.soap.base64
|
||||
value : The object must be serialized with
|
||||
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
|
||||
: and then encoded with base64 encoding.
|
||||
|
||||
mimetype: application/x-microsoft.net.object.bytearray.base64
|
||||
value : The object must be serialized into a byte array
|
||||
: using a System.ComponentModel.TypeConverter
|
||||
: and then encoded with base64 encoding.
|
||||
-->
|
||||
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
|
||||
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
|
||||
<xsd:element name="root" msdata:IsDataSet="true">
|
||||
<xsd:complexType>
|
||||
<xsd:choice maxOccurs="unbounded">
|
||||
<xsd:element name="metadata">
|
||||
<xsd:complexType>
|
||||
<xsd:sequence>
|
||||
<xsd:element name="value" type="xsd:string" minOccurs="0" />
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="name" use="required" type="xsd:string" />
|
||||
<xsd:attribute name="type" type="xsd:string" />
|
||||
<xsd:attribute name="mimetype" type="xsd:string" />
|
||||
<xsd:attribute ref="xml:space" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:element name="assembly">
|
||||
<xsd:complexType>
|
||||
<xsd:attribute name="alias" type="xsd:string" />
|
||||
<xsd:attribute name="name" type="xsd:string" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:element name="data">
|
||||
<xsd:complexType>
|
||||
<xsd:sequence>
|
||||
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
|
||||
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
|
||||
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
|
||||
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
|
||||
<xsd:attribute ref="xml:space" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:element name="resheader">
|
||||
<xsd:complexType>
|
||||
<xsd:sequence>
|
||||
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="name" type="xsd:string" use="required" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
</xsd:choice>
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
</xsd:schema>
|
||||
<resheader name="resmimetype">
|
||||
<value>text/microsoft-resx</value>
|
||||
</resheader>
|
||||
<resheader name="version">
|
||||
<value>2.0</value>
|
||||
</resheader>
|
||||
<resheader name="reader">
|
||||
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
||||
</resheader>
|
||||
<resheader name="writer">
|
||||
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
||||
</resheader>
|
||||
<metadata name="imageList.TrayLocation" type="System.Drawing.Point, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a">
|
||||
<value>17, 17</value>
|
||||
</metadata>
|
||||
<data name="imageList.ImageStream" mimetype="application/x-microsoft.net.object.binary.base64">
|
||||
<value>
|
||||
AAEAAAD/////AQAAAAAAAAAMAgAAAEZTeXN0ZW0uV2luZG93cy5Gb3JtcywgQ3VsdHVyZT1uZXV0cmFs
|
||||
LCBQdWJsaWNLZXlUb2tlbj1iNzdhNWM1NjE5MzRlMDg5BQEAAAAmU3lzdGVtLldpbmRvd3MuRm9ybXMu
|
||||
SW1hZ2VMaXN0U3RyZWFtZXIBAAAABERhdGEHAgIAAAAJAwAAAA8DAAAA8iMAAAJNU0Z0AUkBTAIBAQcB
|
||||
AAGQAQABkAEAAR8BAAEYAQAE/wEhAQAI/wFCAU0BNgcAATYDAAEoAwABfAMAATADAAEBAQABIAYAAV0+
|
||||
AAMEAQUDAAH/AwAB/wMAAf8DAAH/AwAB/wMAAf8DAAH/AwAB/wMAAf9DAAH/AwAB/wMAAf8DAAH/A1UB
|
||||
sWQAA1gB7wMAAf8DAAH/AwAB/wMAAf8DAAH/AwAB/wMAAf8DAAH/AwAB/wMAAf8DAAH/AwAB/wMAAf8D
|
||||
AAH/AwAB/wMAAf8DAAH/AwAB/wMAAf8DAAH/Ay0BRbcAAf8DAAH/AwAB/wMAAf8DAAH/AwAB/wMAAf8D
|
||||
AAH/AwAB/wMAAf8DAAH/AwAB/wMAAf87AAH/AwAB/wMAAf8DAAH/AwAB/wMAAf9cAANEAXgDAAH/AwAB
|
||||
/wMAAf8DAAH/AwAB/wMAAf8DAAH/AwAB/wMAAf8DAAH/AwAB/wMAAf8DAAH/AwAB/wMAAf8DAAH/AwAB
|
||||
/wMAAf8DAAH/AwAB/wMAAf8DAAH/swAB/wMAAf8DAAH/AwAB/wMAAf8XAAH/AwAB/wMAAf8DAAH/AwAB
|
||||
/zcAAf8DAAH/BwAB/wMAAf8DAAH/AwAB/wMAAf9XAAH/AwAB/wMAAf8DAAH/AwAB/wMAAf8DAAH/AwAB
|
||||
/wMAAf8DAAH/AwAB/wMAAf8DAAH/AwAB/wMAAf8DAAH/AwAB/wMAAf8DAAH/AwAB/wMAAf8DAAH/AwAB
|
||||
/wMAAf+vAAH/AwAB/wNRAaQnAAH/AwAB/wMAAf8DAAH/MwAB/wMAAf8IAANOAZcDAAH/AwAB/wMAAf8D
|
||||
AAH/Ay4BSE8AAf8DAAH/AwAB/0sAAf8DAAH/AwAB/+AAAxUBHQMAAf8DAAH/AwAB/y8AAf8DAAH/EwAB
|
||||
/wMAAf8DAAH/AwAB/wMAAf9LAAH/AwAB/wMAAf9LAAH/AwAB/wMAAf/kAAMmATgDAAH/AwAB/wMAAf8r
|
||||
AAH/AwAB/xsAAf8DAAH/AwAB/wMAAf8DAAH/QwAB/wMAAf8DAAH/SwAB/wMAAf8DAAH/6wAB/wMAAf8D
|
||||
AAH/KwAB/wMAAf8cAANCAfYDAAH/AwAB/wMAAf8DAAH/AwQBBTsAAf8DAAH/AwAB/0sAAf8DAAH/AwAB
|
||||
/+8AAf8DAAH/AwAB/ycAAf8DAAH/JwAB/wMAAf8DAAH/AwAB/wMAAf83AAH/AwAB/wMAAf9LAAH/AwAB
|
||||
/wMAAf/vAAH/AwAB/wMAAf8nAAH/AwAB/ygAAwcBCQMAAf8DAAH/AwAB/wMAAf8DYAHjLwAB/wMAAf8D
|
||||
AAH/SwAB/wMAAf8DAAH/7AADIAEtAwAB/wMAAf8nAAH/AwAB/zMAAf8DAAH/AwAB/wMAAf8DAAH/KwAB
|
||||
/wMAAf8DAAH/SwAB/wMAAf8DAAH/8wAB/wMAAf8nAAH/AwAB/zsAAf8DAAH/AwAB/wMAAf8nAAH/AwAB
|
||||
/wMAAf9LAAH/AwAB/wMAAf/zAAH/AwAB/ycAAf8DAAH/PAADPwFsAwAB/wMAAf8nAAH/AwAB/wMAAf9L
|
||||
AAH/AwAB/wMAAf/zAAH/AwAB/ycAAf8DAAH/OwAB/wMAAf8DAAH/AwAB/ycAAf8DAAH/AwAB/0sAAf8D
|
||||
AAH/AwAB/5sAAf8DAAH/AwAB/wMAAf8DAAH/AwAB/wMAAf8DAAH/NAADCAEKAwAB/wMAAf8nAAH/AwAB
|
||||
/zAAA10BzgMAAf8DAAH/AwAB/wMAAf8EAScAAf8DAAH/AwAB/0sAAf8DAAH/AwAB/5QAAwUBBgMAAf8D
|
||||
AAH/AwAB/wMAAf8DAAH/AwAB/wMAAf8DAAH/AwAB/zMAAf8DAAH/AwAB/ycAAf8DAAH/LwAB/wMAAf8D
|
||||
AAH/AwAB/wMAAf8vAAH/AwAB/wMAAf9LAAH/AwAB/wMAAf+UAAMFAQYDAAH/AwAB/wMAAf8DAAH/AwAB
|
||||
/wMAAf8DAAH/AwAB/wMAAf8zAAH/AwAB/wMAAf8nAAH/AwAB/ycAAf8DAAH/AwAB/wMAAf8DAAH/NwAB
|
||||
/wMAAf8DAAH/SwAB/wMAAf8DAAH/lAADBQEGAwAB/wMAAf8PAAH/AwAB/wMAAf8DFQEcLwAB/wMAAf8D
|
||||
AAH/KwAB/wMAAf8cAAM9AWkDAAH/AwAB/wMAAf8DAAH/A0MBdjsAAf8DAAH/AwAB/0sAAf8DAAH/AwAB
|
||||
/5QAAwUBBgMAAf8DAAH/CwAB/wMAAf8DAAH/AxMBGjMAAf8DAAH/AwAB/ysAAf8DAAH/GwAB/wMAAf8D
|
||||
AAH/AwAB/wMAAf9DAAH/AwAB/wMAAf9LAAH/AwAB/wMAAf+UAAMFAQYDAAH/AwAB/wcAAf8DAAH/AwAB
|
||||
/wMbASYzAAH/AwAB/wMAAf8vAAH/AwAB/xMAAf8DAAH/AwAB/wMAAf8DAAH/SwAB/wMAAf8DAAH/SwAB
|
||||
/wMAAf8DAAH/lAADBQEGAwAB/wMAAf8DAAH/AwAB/wMAAf8DAAH/AwAB/wMdASgkAANZAe4DAAH/AwAB
|
||||
/wMAAf8EAS8AAf8DAAH/CAADGAEhAwAB/wMAAf8DAAH/AwAB/wNcActPAAH/AwAB/wMAAf9LAAH/AwAB
|
||||
/wMAAf+UAAMFAQYDAAH/AwAB/wMAAf8DAAH/A2AB4wMAAf8DAAH/AwAB/wMAAf8DUAGfFwAB/wMAAf8D
|
||||
AAH/AwAB/wMAAf83AAH/AwAB/wcAAf8DAAH/AwAB/wMAAf8DAAH/VwAB/wMAAf8DAAH/AwAB/wMqAUAD
|
||||
KgFAAyoBQAMqAUADKgFAAyoBQAMqAUADKgFAAyoBQAMqAUADKgFAAyoBQAMqAUADKgFAAyoBQAMqAUAD
|
||||
AAH/AwAB/wMAAf8DAAH/mwAB/wMAAf8DAAH/Ax0BKQQAAwIBAwMAAf8DAAH/AwAB/wMAAf8DAAH/AwAB
|
||||
/wMAAf8DAAH/AwAB/wMAAf8DAAH/AwAB/wMAAf87AAH/AwAB/wMAAf8DAAH/AwAB/wMAAf8DBwEJWAAD
|
||||
WQHDAwAB/wMAAf8DAAH/AwAB/wMAAf8DAAH/AwAB/wMAAf8DAAH/AwAB/wMAAf8DAAH/AwAB/wMAAf8D
|
||||
AAH/AwAB/wMAAf8DAAH/AwAB/wMAAf8DAAH/AwAB/5wAAxIBFwMAAf8UAANKAYkDAAH/AwAB/wMAAf8D
|
||||
AAH/AwAB/wMAAf8DAAH/AwAB/wMAAf9DAAH/AwAB/wMAAf8DAAH/AyEB+2cAAf8DAAH/AwAB/wMAAf8D
|
||||
AAH/AwAB/wMAAf8DAAH/AwAB/wMAAf8DAAH/AwAB/wMAAf8DAAH/AwAB/wMAAf8DAAH/AwAB/wMAAf8D
|
||||
AAH/AwAB/wNbAcXIAANGAYADWgG/Ay4BSFQAA1oBv3QAA0YBgANaAb8DWgG/A1oBvwNaAb8DWgG/A1oB
|
||||
vwNaAb8DWgG/A1oBvwNaAb8DWgG/A1oBvwNaAb8DWgG/A1oBvwNaAb8DLgFIpwAB/wMAAf8DAAH/AwAB
|
||||
/wMAAf8DAAH/AwAB/wMAAf8DAAH/FAADPQFpAwAB/wMAAf8DAAH/AwAB/wMAAf8DAAH/AwAB/wMAAf8D
|
||||
AAH/EwAB/wMAAf8DAAH/LwAB/wMAAf8DAAH/KAADGQEiAwAB/wMAAf8jAAH/AwAB/wMAAf8oAAM/AW0D
|
||||
AAH/AwAB/zsAAf8DAAH/AwAB/wMAAf8DAAH/AwAB/wMAAf8TAAH/AwAB/wMAAf8DAAH/AwAB/wMAAf8D
|
||||
AAH/KwAB/wMAAf8DAAH/AwAB/wMAAf8DAAH/AwAB/wMAAf8DAAH/FAADAwEEAwAB/wMAAf8DAAH/AwAB
|
||||
/wMAAf8DAAH/AwAB/wMAAf8DAAH/DAADVAGrAwAB/wMAAf8DAAH/AwAB/yQAA1YBtgMAAf8DAAH/AwAB
|
||||
/wMAAf8kAAM1AfkDAAH/AwAB/xwAA1cB8QMAAf8DAAH/AwAB/wMAAf8DPAH4IwAB/wMAAf8DAAH/NwAB
|
||||
/wMAAf8DAAH/AwAB/wMAAf8DAAH/AwAB/wMAAf8DAAH/CwAB/wMAAf8DAAH/AwAB/wMAAf8DAAH/AwAB
|
||||
/wMAAf8DAAH/JwAB/wMAAf8DAAH/AwAB/wMAAf87AAH/AwAB/wMAAf8DAAH/AwAB/wwAA1QBqwMAAf8D
|
||||
AAH/AwAB/wMAAf8DAAH/IAADVgG1AwAB/wMAAf8DAAH/AwAB/wMAAf8gAAM1AfkDAAH/AwAB/xwAA1cB
|
||||
8QMAAf8DAAH/AwAB/wMAAf8DAAH/AwAB/x8AAf8DAAH/AwAB/zcAAf8DAAH/AwAB/wMAAf8DAAH/AwAB
|
||||
/wMAAf8DAAH/AwAB/wsAAf8DAAH/AwAB/wMAAf8DAAH/AwAB/wMAAf8DAAH/AwAB/ycAAf8DAAH/AwAB
|
||||
/wMAAf8DAAH/AxIB/jMAAf8DAAH/AwAB/wMAAf8DAAH/AwAB/wwAA1QBqwMAAf8DAAH/AwAB/wMAAf8D
|
||||
AAH/AwAB/wNZAbsYAANWAbUDAAH/AwAB/wMAAf8DAAH/AwAB/wMAAf8DUQGeGAADNQH5AwAB/wMAAf8c
|
||||
AANXAfEDAAH/AwAB/wMAAf8DAAH/AwAB/wMAAf8DAAH/A1ABnRcAAf8DAAH/AwAB/zcAAf8DAAH/AwAB
|
||||
/w8AAf8DAAH/AwAB/wsAAf8DAAH/AwAB/w8AAf8DAAH/AwAB/ycAAf8DAAH/A1YBswMAAf8DAAH/AwAB
|
||||
/wMAAf8rAAH/AwAB/wMAAf8DAAH/AwAB/wMAAf8DAAH/DAADVAGrAwAB/wMAAf8DPQFoAwAB/wMAAf8D
|
||||
AAH/AwAB/wMAAf8UAANWAbUDAAH/AwAB/wMzAVADAAH/AwAB/wMAAf8DAAH/AwAB/xQAAzUB+QMAAf8D
|
||||
AAH/HAADVwHxAwAB/wMAAf8DEQEWBAEDAAH/AwAB/wMAAf8DAAH/AwAB/xMAAf8DAAH/AwAB/zcAAf8D
|
||||
AAH/AwAB/w8AAf8DAAH/AwAB/wsAAf8DAAH/AwAB/w8AAf8DAAH/AwAB/ycAAf8DAAH/A1YBswcAAf8D
|
||||
AAH/AwAB/wMAAf8jAAH/AwAB/wMAAf8DAAH/BwAB/wMAAf8DAAH/DAADVAGrAwAB/wMAAf8DPQFoBwAB
|
||||
/wMAAf8DAAH/AwAB/wMAAf8QAANWAbUDAAH/AwAB/wMzAVAHAAH/AwAB/wMAAf8DAAH/AwAB/xAAAzUB
|
||||
+QMAAf8DAAH/HAADVwHxAwAB/wMAAf8DEQEWCwAB/wMAAf8DAAH/AwAB/wMAAf8PAAH/AwAB/wMAAf83
|
||||
AAH/AwAB/wMAAf8PAAH/AwAB/wMAAf8LAAH/AwAB/wMAAf8PAAH/AwAB/wMAAf8nAAH/AwAB/wNWAbML
|
||||
AAH/AwAB/wMAAf8DEgH+GwAB/wMAAf8DAAH/AwAB/wsAAf8DAAH/AwAB/wwAA1QBqwMAAf8DAAH/Az0B
|
||||
aAgAA2AB2wMAAf8DAAH/AwAB/wMAAf8MAANWAbUDAAH/AwAB/wMzAVAIAANgAeMDAAH/AwAB/wMAAf8D
|
||||
AAH/DAADNQH5AwAB/wMAAf8cAANXAfEDAAH/AwAB/wMRARYMAAMzAVIDAAH/AwAB/wMAAf8DAAH/AwAB
|
||||
/wcAAf8DAAH/AwAB/zcAAf8DAAH/AwAB/w8AAf8DAAH/AwAB/wsAAf8DAAH/AwAB/w8AAf8DAAH/AwAB
|
||||
/ycAAf8DAAH/A1YBsw8AAf8DAAH/AwAB/wMAAf8TAAH/AwAB/wMAAf8DAAH/DwAB/wMAAf8DAAH/DAAD
|
||||
VAGrAwAB/wMAAf8DPQFoDAADJQE3AwAB/wMAAf8DAAH/AwAB/wgAA1YBtQMAAf8DAAH/AzMBUAwAAzIB
|
||||
TwMAAf8DAAH/AwAB/wMAAf8IAAM1AfkDAAH/AwAB/xwAA1cB8QMAAf8DAAH/AxEBFhcAAf8DAAH/AwAB
|
||||
/wMAAf8DAAH/AxIB/gMAAf8DAAH/NwAB/wMAAf8DAAH/DwAB/wMAAf8DAAH/CwAB/wMAAf8DAAH/DwAB
|
||||
/wMAAf8DAAH/JwAB/wMAAf8XAAH/AwAB/wMAAf8DAAH/CwAB/wMAAf8DAAH/AwAB/xAAAwwBDwMAAf8D
|
||||
QgH2DAADVAGrAwAB/wMAAf8DPQFoFwAB/wMAAf8DAAH/AwAB/wQAA1YBtQMAAf8DAAH/AzMBUBcAAf8D
|
||||
AAH/AwAB/wMAAf8EAAM1AfkDAAH/AwAB/xwAA1cB8QMAAf8DAAH/AxEBFhgAA1oB6QMAAf8DAAH/AwAB
|
||||
/wMAAf8DAAH/AwAB/zcAAf8DAAH/AwAB/w8AAf8DAAH/AwAB/wsAAf8DAAH/AwAB/w8AAf8DAAH/AwAB
|
||||
/0cAAf8DAAH/AwAB/wMSAf4DAAH/AwAB/wMAAf8DAAH/LAADVAGrAwAB/wMAAf8DPQFoGwAB/wMAAf8D
|
||||
AAH/AwAB/wNdAc4DAAH/AwAB/wMzAVAbAAH/AwAB/wMAAf8DAAH/AyYB+gMAAf8DAAH/HAADVwHxAwAB
|
||||
/wMAAf8DEQEWIwAB/wMAAf8DAAH/AwAB/wMAAf83AAH/AwAB/wMAAf8PAAH/AwAB/wMAAf8LAAH/AwAB
|
||||
/wMAAf8PAAH/AwAB/wMAAf9LAAH/AwAB/wMAAf8DAAH/AwAB/wMAAf8wAANUAasDAAH/AwAB/wM9AWgf
|
||||
AAH/AwAB/wMAAf8DAAH/AwAB/wMAAf8DMwFQHwAB/wMAAf8DAAH/AwAB/wMAAf8DAAH/HAADVwHxAwAB
|
||||
/wMAAf8DEQEWJwAB/wMAAf8DAAH/AwAB/zcAAf8DAAH/AwAB/w8AAf8DAAH/AwAB/wsAAf8DAAH/AwAB
|
||||
/w8AAf8DAAH/AwAB/08AAf8DAAH/AwAB/wMAAf80AANUAasDAAH/AwAB/wM9AWgjAAH/AwAB/wMAAf8D
|
||||
AAH/AwAB/wMzAVAjAAH/AwAB/wMAAf8DAAH/AwAB/xwAA1cB8QMAAf8DAAH/AxEBFisAAf8DAAH/AwAB
|
||||
/zcAAf8DAAH/AwAB/w8AAf8DAAH/AwAB/wsAAf8DAAH/AwAB/w8AAf8DAAH/AwAB/0sAAf8DAAH/AwAB
|
||||
/wMAAf8DAAH/AxIB/jAAA1QBqwMAAf8DAAH/Az0BaB8AAf8DAAH/AwAB/wMAAf8DAAH/AwAB/wMzAVAf
|
||||
AAH/AwAB/wMAAf8DAAH/AwAB/wMAAf8cAANXAfEDAAH/AwAB/wMRARYkAANcAdkDAAH/AwAB/wMAAf83
|
||||
AAH/AwAB/wMAAf8PAAH/AwAB/wMAAf8LAAH/AwAB/wMAAf8PAAH/AwAB/wMAAf9HAAH/AwAB/wMAAf8D
|
||||
AAH/AwAB/wMAAf8DAAH/AwAB/ywAA1QBqwMAAf8DAAH/Az0BaBsAAf8DAAH/AwAB/wMAAf8DVwHxAwAB
|
||||
/wMAAf8DMwFQGwAB/wMAAf8DAAH/AwAB/wMSAf4DAAH/AwAB/xwAA1cB8QMAAf8DAAH/AxEBFiMAAf8D
|
||||
AAH/AwAB/wMAAf8DAAH/NwAB/wMAAf8DAAH/DwAB/wMAAf8DAAH/CwAB/wMAAf8DAAH/DwAB/wMAAf8D
|
||||
AAH/JwAB/wMAAf8XAAH/AwAB/wMAAf8DAAH/CwAB/wMAAf8DAAH/AwAB/xcAAf8DWAG4DAADVAGrAwAB
|
||||
/wMAAf8DPQFoFwAB/wMAAf8DAAH/AwAB/wQAA1YBtQMAAf8DAAH/AzMBUBcAAf8DAAH/AwAB/wMAAf8E
|
||||
AAM1AfkDAAH/AwAB/xwAA1cB8QMAAf8DAAH/AxEBFhgAAzABSgMAAf8DAAH/AwAB/wMAAf8DAAH/AwAB
|
||||
/zcAAf8DAAH/AwAB/w8AAf8DAAH/AwAB/wsAAf8DAAH/AwAB/w8AAf8DAAH/AwAB/ycAAf8DAAH/A1YB
|
||||
sw8AAf8DAAH/AwAB/wMAAf8TAAH/AwAB/wMAAf8DEgH+DwAB/wMAAf8DAAH/DAADVAGrAwAB/wMAAf8D
|
||||
PQFoEwAB/wMAAf8DAAH/AwAB/wgAA1YBtQMAAf8DAAH/AzMBUAwAAwkBDAMAAf8DAAH/AwAB/wMAAf8I
|
||||
AAM1AfkDAAH/AwAB/xwAA1cB8QMAAf8DAAH/AxEBFhcAAf8DAAH/AwAB/wMAAf8DAAH/AyQB/QMAAf8D
|
||||
AAH/NwAB/wMAAf8DAAH/DwAB/wMAAf8DAAH/CwAB/wMAAf8DAAH/DwAB/wMAAf8DAAH/JwAB/wMAAf8D
|
||||
VgGzCwAB/wMAAf8DAAH/AwAB/xsAAf8DAAH/AwAB/wMAAf8LAAH/AwAB/wMAAf8MAANUAasDAAH/AwAB
|
||||
/wM9AWgIAAM6AWADAAH/AwAB/wMAAf8DAAH/DAADVgG1AwAB/wMAAf8DMwFQCAADSwGMAwAB/wMAAf8D
|
||||
AAH/AwAB/wwAAzUB+QMAAf8DAAH/HAADVwHxAwAB/wMAAf8DEQEWDAADBwEJAwAB/wMAAf8DAAH/AwAB
|
||||
/wMAAf8HAAH/AwAB/wMAAf83AAH/AwAB/wMAAf8PAAH/AwAB/wMAAf8LAAH/AwAB/wMAAf8PAAH/AwAB
|
||||
/wMAAf8nAAH/AwAB/wNWAbMHAAH/AwAB/wMAAf8DAAH/IwAB/wMAAf8DAAH/AwAB/wcAAf8DAAH/AwAB
|
||||
/wwAA1QBqwMAAf8DAAH/Az0BaAcAAf8DAAH/AwAB/wMAAf8DAAH/EAADVgG1AwAB/wMAAf8DMwFQBwAB
|
||||
/wMAAf8DAAH/AwAB/wMAAf8QAAM1AfkDAAH/AwAB/xwAA1cB8QMAAf8DAAH/AxEBFgsAAf8DAAH/AwAB
|
||||
/wMAAf8DAAH/A08BmQsAAf8DAAH/AwAB/zcAAf8DAAH/AwAB/w8AAf8DAAH/AwAB/wsAAf8DAAH/AwAB
|
||||
/w8AAf8DAAH/AwAB/ycAAf8DAAH/A1YBswMAAf8DAAH/AwAB/wMAAf8rAAH/AwAB/wMAAf8DEgH+AwAB
|
||||
/wMAAf8DAAH/DAADVAGrAwAB/wMAAf8DPQFoAwAB/wMAAf8DAAH/AwAB/wMAAf8UAANWAbUDAAH/AwAB
|
||||
/wMzAVADAAH/AwAB/wMAAf8DAAH/AwAB/xQAAzUB+QMAAf8DAAH/HAADVwHxAwAB/wMAAf8DEQEWBwAB
|
||||
/wMAAf8DAAH/AwAB/wMAAf8TAAH/AwAB/wMAAf83AAH/AwAB/wMAAf8PAAH/AwAB/wMAAf8LAAH/AwAB
|
||||
/wMAAf8PAAH/AwAB/wMAAf8nAAH/AwAB/wMAAf8DAAH/AwAB/wMAAf8zAAH/AwAB/wMAAf8DAAH/AwAB
|
||||
/wMAAf8MAANUAasDAAH/AwAB/wMAAf8DAAH/AwAB/wMAAf8DTQH0GAADVgG1AwAB/wMAAf8DAAH/AwAB
|
||||
/wMAAf8DAAH/A10B7BgAAzUB+QMAAf8DAAH/HAADVwHxAwAB/wMAAf8DYQHrAwAB/wMAAf8DAAH/AwAB
|
||||
/wNNAfQXAAH/AwAB/wMAAf83AAH/AwAB/wMAAf8PAAH/AwAB/wMAAf8LAAH/AwAB/wMAAf8PAAH/AwAB
|
||||
/wMAAf8nAAH/AwAB/wMAAf8DAAH/AwAB/zsAAf8DAAH/AwAB/wMAAf8DAAH/DAADVAGrAwAB/wMAAf8D
|
||||
AAH/AwAB/wMAAf8DQwF3HAADVgG1AwAB/wMAAf8DAAH/AwAB/wMAAf8DMAFMHAADNQH5AwAB/wMAAf8c
|
||||
AANXAfEDAAH/AwAB/wMAAf8DAAH/AwAB/wMAAf8fAAH/AwAB/wMAAf83AAH/AwAB/wMAAf8DKgFAAyoB
|
||||
QAMqAUADAAH/AwAB/wMAAf8LAAH/AwAB/wMAAf8DKgFAAyoBQAMqAUADAAH/AwAB/wMAAf8nAAH/AwAB
|
||||
/wMAAf8DAAH/AwAB/wMAAf8DAAH/AwAB/wMAAf8bAAH/AwAB/wMAAf8DAAH/AwAB/wMAAf8DAAH/AwAB
|
||||
/wMAAf8MAANUAasDAAH/AwAB/wMAAf8DAAH/AwYBCCAAA1YBtgMAAf8DAAH/AwAB/wMAAf8DAgEDIAAD
|
||||
NQH5AwAB/wMAAf8cAANXAfEDAAH/AwAB/wMAAf8DAAH/AwAB/yMAAf8DAAH/AwAB/zcAAf8DAAH/AwAB
|
||||
/wMAAf8DAAH/AwAB/wMAAf8DAAH/AwAB/wsAAf8DAAH/AwAB/wMAAf8DAAH/AwAB/wMAAf8DAAH/AwAB
|
||||
/ycAAf8DAAH/AwAB/wMAAf8DAAH/AwAB/wMAAf8DAAH/AwAB/xQAA0wBkAMAAf8DAAH/AwAB/wMAAf8D
|
||||
AAH/AwAB/wMAAf8DAAH/AwAB/xMAAf8DAAH/AwAB/y8AAf8DAAH/AwAB/ygAAzYBWAMAAf8DAAH/IwAB
|
||||
/wMAAf8DAAH/Aw0BESQAA1UBrQMAAf8DAAH/OwAB/wMAAf8DAAH/AwAB/wMAAf8DAAH/AwAB/xMAAf8D
|
||||
AAH/AwAB/wMAAf8DAAH/AwAB/wMAAf8sAANaAb8DWgG/A1oBvwNaAb8DWgG/A1oBvwNaAb8DFQEcGAAD
|
||||
TwGZA1oBvwNaAb8DWgG/A1oBvwNaAb8DWgG/A1oBvxgAA04BlzQAA0sBjTAAAwwBDygAA1ABmjAAAygB
|
||||
PEAAAzgBWwNaAb8DWgG/A1oBvwMwAUwYAANEAXoDWgG/A1oBvwNaAb8DIAEtHAABQgFNAT4HAAE+AwAB
|
||||
KAMAAXwDAAEwAwABAQEAAQEGAAEDFgAD/wEAAf8B4AEHAf8B+AE/Av8B4AIAAXgEAAH/AcABAQH/AfgB
|
||||
HwL/AcACAAF4BAAB/wGDAeAB/wH5AQcC/wHAAgABOAQAAf8BjwH4AX8B+QGBAv8BxwH/Af4BOAQAAv8B
|
||||
/AE/AfkB4AL/AccB/wH+ATgEAAL/Af4BHwH5AfgBPwH/AccB/wH+ATgEAAP/AR8B+QH8AQ8B/wHHAf8B
|
||||
/gE4BAAD/wGPAfkB/wEHAf8BxwH/Af4BOAQAA/8BjwH5Af8BgQH/AccB/wH+ATgEAAP/AY8B+QH/AeAB
|
||||
/wHHAf8B/gE4BAAD/wHPAfkB/wH4AX8BxwH/Af4BOAQAA/8BzwH5Af8B/AF/AccB/wH+ATgEAAP/Ac8B
|
||||
+QH/AfgBfwHHAf8B/gE4BAAB8AEPAf8BjwH5Af8B4AF/AccB/wH+ATgEAAHgAQcB/wGPAfkB/wHBAf8B
|
||||
xwH/Af4BOAQAAeABBwH/AY8B+QH/AQcB/wHHAf8B/gE4BAAB4wGHAf8BHwH5AfwBDwH/AccB/wH+ATgE
|
||||
AAHjAQ8B/wEfAfkB+AE/Af8BxwH/Af4BOAQAAeIBHwH+AT8B+QHgAv8BxwH/Af4BOAQAAeABDwH4AT8B
|
||||
+QGBAv8BxwH/Af4BOAQAAeABAwHgAf8B+QEHAv8BwAIAATgEAAHwAYABAQH/AfgBDwL/AcACAAF4BAAB
|
||||
8wHgAQcB/wH4AT8C/wHgAgABeAQAAf8B/gE/Af8B/gP/AfgBAAEBAfgEAAHwAQcBwAEPAR8B/AF/AeMB
|
||||
/AF/AeMB/wHwAR4BAwLwAQcBwAEOAQ8B+AE/AeMB+AEfAeMB/wHgAQwBAQLwAX8B/gEOAQcB+AEfAeMB
|
||||
+AEPAeMB/wHgAQwBAQLwAT8B/AEOAQEB+AEHAeMB+AEDAeMB/wHjAYwBcQLwAR8B+AEOAQAB+AEDAeMB
|
||||
+AEBAeMB/wHjAYwBcQHwAfEBDwHwAY4BEAF4AUEB4wH4AWAB4wH/AeMBjAFxAfAB8QGHAeEBjgEYATgB
|
||||
YAHjAfgBcAEjAf8B4wGMAXEB8AHxAsMBjgEcARgBcAFjAfgBfAEDAf8B4wGMAXEB8AHzAeEBhwGOAR8B
|
||||
CAF8ASMB+AF+AQMB/wHjAYwBcQHwAf8B8AEPAf4BHwGAAX4BAwH4AX8BgwH/AeMBjAFxAfAB/wH4AR8B
|
||||
/gEfAcABfwEDAfgBfwHDAf8B4wGMAXEB8AH/AfwBPwH+AR8B4AF/AYMB+AF/AeMB/wHjAYwBcQHwAf8B
|
||||
+AEfAf4BHwHAAX8BAwH4AX8BwwH/AeMBjAFxAfAB/wHwAQ8B/gEfAYABfgEDAfgBfwGDAf8B4wGMAXEB
|
||||
8AHzAeEBhwHOAR8BCAF8ASMB+AF+AQMB/wHjAYwBcQHwAfECwwGOAR4BGAFwAWMB+AF8AQMB/wHjAYwB
|
||||
cQHwAfEBhwHhAY4BGAE4AWAB4wH4AXABIwH/AeMBjAFxAfAB8QEPAfABjgEQAXgBQQHjAfgBYAFjAf8B
|
||||
4wGMAXEC8AEfAfgBDgEAAfgBAwHjAfgBQQHjAf8B4wGMAXEC8AE/AfwBDgEBAfgBBwHjAfgBAwHjAf8B
|
||||
4wGMAXEC8AF/Af4BDgEDAfgBDwHjAfgBDwHjAf8B4AEMAQEC8AEHAeABDgEHAfgBHwHjAfgBHwHjAf8B
|
||||
4AEMAQEC8AEHAcABDwEfAfwBfwHjAfwBPwHjAf8B8AEeAQMB8AH4AQcB4AEfAb8B/gH/AfcB/gH/AfcB
|
||||
/wH4AT8BBwHwCw==
|
||||
</value>
|
||||
</data>
|
||||
<metadata name="toolTip.TrayLocation" type="System.Drawing.Point, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a">
|
||||
<value>165, 17</value>
|
||||
</metadata>
|
||||
<metadata name="spinePreviewFullScreenForm.TrayLocation" type="System.Drawing.Point, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a">
|
||||
<value>307, 18</value>
|
||||
</metadata>
|
||||
<metadata name="wallpaperForm.TrayLocation" type="System.Drawing.Point, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a">
|
||||
<value>618, 18</value>
|
||||
</metadata>
|
||||
</root>
|
||||
@@ -1,326 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<root>
|
||||
<!--
|
||||
Microsoft ResX Schema
|
||||
|
||||
Version 2.0
|
||||
|
||||
The primary goals of this format is to allow a simple XML format
|
||||
that is mostly human readable. The generation and parsing of the
|
||||
various data types are done through the TypeConverter classes
|
||||
associated with the data types.
|
||||
|
||||
Example:
|
||||
|
||||
... ado.net/XML headers & schema ...
|
||||
<resheader name="resmimetype">text/microsoft-resx</resheader>
|
||||
<resheader name="version">2.0</resheader>
|
||||
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
|
||||
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
|
||||
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
|
||||
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
|
||||
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
|
||||
<value>[base64 mime encoded serialized .NET Framework object]</value>
|
||||
</data>
|
||||
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
|
||||
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
|
||||
<comment>This is a comment</comment>
|
||||
</data>
|
||||
|
||||
There are any number of "resheader" rows that contain simple
|
||||
name/value pairs.
|
||||
|
||||
Each data row contains a name, and value. The row also contains a
|
||||
type or mimetype. Type corresponds to a .NET class that support
|
||||
text/value conversion through the TypeConverter architecture.
|
||||
Classes that don't support this are serialized and stored with the
|
||||
mimetype set.
|
||||
|
||||
The mimetype is used for serialized objects, and tells the
|
||||
ResXResourceReader how to depersist the object. This is currently not
|
||||
extensible. For a given mimetype the value must be set accordingly:
|
||||
|
||||
Note - application/x-microsoft.net.object.binary.base64 is the format
|
||||
that the ResXResourceWriter will generate, however the reader can
|
||||
read any of the formats listed below.
|
||||
|
||||
mimetype: application/x-microsoft.net.object.binary.base64
|
||||
value : The object must be serialized with
|
||||
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
|
||||
: and then encoded with base64 encoding.
|
||||
|
||||
mimetype: application/x-microsoft.net.object.soap.base64
|
||||
value : The object must be serialized with
|
||||
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
|
||||
: and then encoded with base64 encoding.
|
||||
|
||||
mimetype: application/x-microsoft.net.object.bytearray.base64
|
||||
value : The object must be serialized into a byte array
|
||||
: using a System.ComponentModel.TypeConverter
|
||||
: and then encoded with base64 encoding.
|
||||
-->
|
||||
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
|
||||
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
|
||||
<xsd:element name="root" msdata:IsDataSet="true">
|
||||
<xsd:complexType>
|
||||
<xsd:choice maxOccurs="unbounded">
|
||||
<xsd:element name="metadata">
|
||||
<xsd:complexType>
|
||||
<xsd:sequence>
|
||||
<xsd:element name="value" type="xsd:string" minOccurs="0" />
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="name" use="required" type="xsd:string" />
|
||||
<xsd:attribute name="type" type="xsd:string" />
|
||||
<xsd:attribute name="mimetype" type="xsd:string" />
|
||||
<xsd:attribute ref="xml:space" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:element name="assembly">
|
||||
<xsd:complexType>
|
||||
<xsd:attribute name="alias" type="xsd:string" />
|
||||
<xsd:attribute name="name" type="xsd:string" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:element name="data">
|
||||
<xsd:complexType>
|
||||
<xsd:sequence>
|
||||
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
|
||||
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
|
||||
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
|
||||
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
|
||||
<xsd:attribute ref="xml:space" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:element name="resheader">
|
||||
<xsd:complexType>
|
||||
<xsd:sequence>
|
||||
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="name" type="xsd:string" use="required" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
</xsd:choice>
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
</xsd:schema>
|
||||
<resheader name="resmimetype">
|
||||
<value>text/microsoft-resx</value>
|
||||
</resheader>
|
||||
<resheader name="version">
|
||||
<value>2.0</value>
|
||||
</resheader>
|
||||
<resheader name="reader">
|
||||
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
||||
</resheader>
|
||||
<resheader name="writer">
|
||||
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
||||
</resheader>
|
||||
<metadata name="imageList.TrayLocation" type="System.Drawing.Point, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a">
|
||||
<value>17, 17</value>
|
||||
</metadata>
|
||||
<data name="imageList.ImageStream" mimetype="application/x-microsoft.net.object.binary.base64">
|
||||
<value>
|
||||
AAEAAAD/////AQAAAAAAAAAMAgAAAEZTeXN0ZW0uV2luZG93cy5Gb3JtcywgQ3VsdHVyZT1uZXV0cmFs
|
||||
LCBQdWJsaWNLZXlUb2tlbj1iNzdhNWM1NjE5MzRlMDg5BQEAAAAmU3lzdGVtLldpbmRvd3MuRm9ybXMu
|
||||
SW1hZ2VMaXN0U3RyZWFtZXIBAAAABERhdGEHAgIAAAAJAwAAAA8DAAAAHi0AAAJNU0Z0AUkBTAIBAQYB
|
||||
AAFwAQABcAEAAR8BAAEYAQAE/wEhAQAI/wFCAU0BNgcAATYDAAEoAwABfAMAATADAAEBAQABIAYAAV0q
|
||||
AAQCAy0BRQNbAc0DXQHoA1kBxgMyAU8DDwEUBAIYAAMOARIDQwF3A10BzwNbAc0DLQFFBAIYAANWAbID
|
||||
XQHoA1wB1gNDAXcDFgEeBAIYAAMKAQ0DSQGFA14B4wNfAeUDUQGeAyQBNAMJAQsEARgAAwsBDgM7AWQD
|
||||
XgHSA1YBsv8AEQAEAgMxAUwDYQHhAwAB/wMuAfkDYAHbA0QBewMeASoDBgEIFAADGAEhA1cBwgMhAfsD
|
||||
YQHhAzEBTAQCGAADWQHDAwAB/wMhAfwDXAHrA0gBhAMWAR4YAAMLAQ4DTQGSAysB+QMPAf4DUAHyA1gB
|
||||
ugM3AVoDEQEWAwIBAxQAAxcBHwNJAYYDXAHrA1kBw/8AEQAEAgMxAUwDYQHhAwAB/wMAAf8DHgH9A2AB
|
||||
4ANQAZoDLgFGAxEBFgMGAQcEAQgAAxoBJANbAc0DAAH/A2EB4QMxAUwEAhgAA1kBwwMAAf8DAAH/AwAB
|
||||
/wNbAdADPgFrAw8BEwMCAQMQAAMLAQ4DTQGSAysB+QMAAf8DAAH/AzkB9gNcAcsDRAF5Ax4BKgMGAQcM
|
||||
AAQBAxgBIQNKAYsDWwHsA1kBw/8AEQAEAgMxAUwDYQHhAwAB/wMuAfkDXAHnA0MB9QNWAe4DWQG7A0MB
|
||||
dwMoATsDDwEUBAEEAAMaASQDWwHNAwAB/wNhAeEDMQFMBAIYAANZAcMDIAH8A1AB8ANDAfUDLgH5A10B
|
||||
zgNDAXcDGgEjAwIBAwwAAwsBDgNNAZIDKwH5AwEB/wMkAfoDOgH4Ay4B+QNdAeMDTgGYAyQBNQMGAQgE
|
||||
AQQABAEDGAEhA0oBiwNbAewDWQHD/wARAAQCAzEBTANhAeEDAAH/A1wB2QM7AWMDWQG7Az0B9wM5AfgD
|
||||
XAHnA1sBxQNBAXMDEwEZAwIBAwMaASQDWwHNAwAB/wNhAeEDMQFMBAIYAANZAcMDXAHrA1ABmgNVAbED
|
||||
TQHzAyEB+wNbAeQDSwGPAxsBJQMDAQQIAAMLAQ4DTQGSAysB+QMfAf0DXAHZA1oBxwNDAfUDDwH+A1wB
|
||||
6wNSAaMDJQE3AwMBBAQABAEDGAEhA0oBiwNbAewDWQHD/wARAAQCAzEBTANhAeEDAAH/A1sBzQMaASQD
|
||||
FgEdA1UBrwMAAf8DAAH/AwAB/wMAAf8DVQGvAxYBHQMaASQDWwHNAwAB/wNhAeEDMQFMBAIYAANZAcMD
|
||||
WwHkAzsBZQMHAQkDSQGGAyEB+wMAAf8DAAH/A1kBwQMdASkIAAMLAQ4DTQGSAysB+QMrAfkDTQGSAzkE
|
||||
XgHiAwAB/wMAAf8DXQHjAzYBWAMFAQYEAAQBAxgBIQNKAYsDWwHsA1kBw/8AEQAEAgMxAUwDYQHhAwAB
|
||||
/wNbAc0DGgEkAwIBAwMTARoDRgF/A1sB3gMhAfsDDwH+AzkB+ANZAbsDOwFjA1wB2QMAAf8DYQHhAzEB
|
||||
TAQCGAADWQHDA1sB5AM7AWUDBwQJAQwDOgFhA18B1QNCAfUDLgH5A1sBygMyAU8DDwEUAw0BEQNNAZID
|
||||
KwH5AysB+QNNAZIDEQEWAy0BRANaAb8DUgHvAyEB+wNdAdwDPwFuAxYBHgMEAQUDGAEhA0oBiwNbAewD
|
||||
WQHD/wARAAQCAzEBTANhAeEDAAH/A1sBzQMaASQEAAQCAxMBGgM9AWcDWQHAA1IB7wMPAf4DPQH3A1wB
|
||||
5wMuAfkDAAH/A2EB4QMxAUwEAhgAA1kBwwNbAeQDOwFlAwcBCQQAAwsBDgMxAU0DWAG9AzkB9gMuAfkD
|
||||
YAHbA0QBeAMhAS8DTgGVAysB+QMrAfkDTQGSAwsBDgMGAQgDIAEuA00BkgNdAdwDQwH1A1oB6QNOAZYD
|
||||
KAE8AyABLQNLAY0DWwHsA1kBw/8AEQAEAgMxAUwDYQHhAwAB/wNbAc0DGgEkCAAEAgMMARADLwFJA1IB
|
||||
owNbAeQDPQH3Aw8B/gMAAf8DAAH/A2EB4QMxAUwEAhgAA1kBwwNbAeQDOwFlAwcBCQgAAxABFQM/AW0D
|
||||
XAHWAyQB+gMeAf0DXAHWA0QBeQNUAasDIQH7AysB+QNNAZIDCwEOBAADAwEEAxsBJgNBAXMDWgHEA0UB
|
||||
9ANWAe4DVQG0A0QBegNRAaIDVgHuA1kBw/8AEQAEAgMxAUwDYQHhAwAB/wNbAc0DGgEkEAADCAEKAyMB
|
||||
MwNEAXkDVwG8A18B5QMkAfoDAAH/A2EB4QMxAUwEAhgAA1kBwwNbAeQDOwFlAwcBCQgAAwMBBAMaASQD
|
||||
QgF0A14B0gMhAfsDOQH2A10B0QNgAeADHgH9AysB+QNNAZIDCwEOCAADAgEDAxIBFwM1AVUDWAG6A1UB
|
||||
8QM5AfYDWwHeA2AB2wM5AfgDWQHD/wARAAQCAzEBTANhAeEDAAH/A1sBzQMaASQUAAMCAQMDDgESAyMB
|
||||
MgM9AWgDXgHdAwAB/wNhAeEDMQFMBAIYAANZAcMDWwHkAzsBZQMHAQkMAAMCAQMDDwETAzQBUwNdAdED
|
||||
OQH4Ax8B/AMfAf0DAAH/AysB+QNNAZIDCwEODAAEAQMJAQwDIwEzA1UBrgMgAf0DHgH9Ax8B/AMPAf4D
|
||||
WQHD/wARAAQCAzEBTANhAeEDAAH/A1sBzQMaASQgAAMaASQDWwHNAwAB/wNhAeEDMQFMBAIYAANZAcMD
|
||||
WwHkAzsBZQMHAQkYAAMPARQDVwG5AwAB/wMAAf8DAAH/AysB+QNNAZIDCwEOGAADRwGAA1wB7QMAAf8D
|
||||
AAH/AwAB/wNZAcP/ABEABAIDMQFMA2EB4QMAAf8DWwHNAxoBJBQABAIDBwEJAxYBHQM1AVUDXAHZAwAB
|
||||
/wNhAeEDMQFMBAIYAANZAcMDWwHkAzsBZQMHAQkMAAQBAw0BEQM0AVMDXQHRAy4B+QMQAf4DDwH+AwAB
|
||||
/wMrAfkDTQGSAwsBDgwABAEDCQELAx4BKwNTAakDIAH9Ax4B/QMfAfwDDwH+A1kBw/8AEQAEAgMxAUwD
|
||||
YQHhAwAB/wNbAc0DGgEkEAADBwEJAxsBJgM0AVMDTQGSA14B3QMuAfkDAAH/A2EB4QMxAUwEAhgAA1kB
|
||||
wwNbAeQDOwFlAwcBCQgABAIDEAEVAzkBXgNbAc0DIQH7AyEB+wNcAecDVgHuAw8B/gMrAfkDTQGSAwsB
|
||||
DggAAwIBAwMSARcDNQFVA1gBuANVAfEDOQH2A1sB3gNgAdsDOQH4A1kBw/8AEQAEAgMxAUwDYQHhAwAB
|
||||
/wNbAc0DGgEkCAAEAgMMARADLgFGA00BkgNcAcgDXAHrAx4B/QMAAf8DAAH/A2EB4QMxAUwEAhgAA1kB
|
||||
wwNbAeQDOwFlAwcBCQgAAwkBDAMxAU4DWAG3A00B8wMeAf0DXgHdA04BlgNZAb4DHwH8AysB+QNNAZID
|
||||
CwEOBAADAwEEAxsBJgNBAXMDWgHEA0UB9ANWAe4DVQG0A0QBegNRAaIDVgHuA1kBw/8AEQAEAgMxAUwD
|
||||
YQHhAwAB/wNbAc0DGgEkBAAEAgMTARoDPQFnA1oBvwNcAesDJAH6AzoB9gNcAecDLgH5AwAB/wNhAeED
|
||||
MQFMBAIYAANZAcMDWwHkAzsBZQMHAQkEAAMLAQ4DLgFHA1UBrQNSAe8DOgH4A2AB2wNEAXoDJAE1A04B
|
||||
mAMjAfoDKwH5A00BkgMLAQ4DBgEIAyABLgNNAZIDXQHcA0MB9QNaAekDTgGWAygBPAMgAS0DSwGNA1sB
|
||||
7ANZAcP/ABEABAIDMQFMA2EB4QMAAf8DWwHNAxoBJAMCAQMDEwEaA0YBfwNbAd4DIQH7Aw8B/gM5AfgD
|
||||
WQG7AzsBYwNcAdkDAAH/A2EB4QMxAUwEAhgAA1kBwwNbAeQDOwFlAwcECQEMAzoBYQNdAdQDRQH0Ay4B
|
||||
+QNbAcoDMgFPAw8BFAMNAREDTQGSAysB+QMrAfkDTQGSAxEBFgMtAUQDWgG/A1IB7wMgAfwDYAHgA0AB
|
||||
cQMXAR8DBAEFAxgBIQNKAYsDWwHsA1kBw/8AEQAEAgMxAUwDYQHhAwAB/wNbAc0DGgEkAxYBHQNVAa8D
|
||||
AAH/AwAB/wMAAf8DAAH/A1UBrwMWAR0DGgEkA1sBzQMAAf8DYQHhAzEBTAQCGAADWQHDA1sB5AM7AWUD
|
||||
BwEJA0kBhgMhAfsDAAH/AwAB/wNZAcEDHQEpCAADCwEOA00BkgMrAfkDKwH5A00BkgM5BF4B4gMAAf8D
|
||||
AAH/A1gB7QNKAYsDGAEgCAEDGAEhA0oBiwNbAewDWQHD/wARAAQCAzEBTANhAeEDAAH/A1wB2QM7AWMD
|
||||
WQG7Az0B9wMgAfwDRQH0A1wB2QNGAX4DEwEaAwIBAwMaASQDWwHNAwAB/wNhAeEDMQFMBAIYAANZAcMD
|
||||
XAHrA1ABmgNVAbEDTQHzAyEB+wNfAeUDSwGPAxsBJQMDAQQIAAMLAQ4DTQGSAysB+QMhAfsDVgGzA0wB
|
||||
jgNcAesDDwH+A1AB8ANXAbkDLgFHAwYBCAQABAEDGAEhA0oBiwNbAewDWQHD/wARAAQCAzEBTANhAeED
|
||||
AAH/Ay4B+QNcAecDQwH1A1QB8QNbAdMDUQGkAzgBWwMTARkEAgQAAxoBJANbAc0DAAH/A2EB4QMxAUwE
|
||||
AhgAA1kBwwMgAfwDUAHwA0MB9QMfAf0DXgHXA0YBfgMaASQDAgEDDAADCwEOA00BkgMrAfkDDwH+A00B
|
||||
8wNWAe4DPQH3A10B4wNOAZgDJwE5AwgBCgQBBAAEAQMYASEDSgGLA1sB7ANZAcP/ABEABAIDMQFMA2EB
|
||||
4QMAAf8DAAH/Ax4B/QNhAeEDUgGjAzsBYwMhAS8DCgENBAIIAAMaASQDWwHNAwAB/wNhAeEDMQFMBAIY
|
||||
AANZAcMDAAH/AwAB/wMAAf8DXQHoA0oBiwMWAR0DBAEFEAADCwEOA00BkgMrAfkDAAH/AwAB/wM5AfYD
|
||||
XAHLA0QBeQMeASoDBgEHDAAEAQMYASEDSgGLA1sB7ANZAcP/ABEABAIDMQFMA2EB4QMAAf8DLgH5A2AB
|
||||
2wNFAXwDIQEwAwwBEAMDAQQQAAMaASMDXAHIAx8B/QNhAeEDMQFMBAIYAANZAcMDAAH/AyEB/ANcAesD
|
||||
TgGXAyMBMwQCFAADCwEOA00BkgMrAfkDDwH+A1AB8gNYAboDNwFaAxEBFgMCAQMUAAMXAR8DSQGGA1wB
|
||||
6wNZAcP/ABEABAIDLQFFA14B0gNQAfIDXQHJAzIBTwMQARUDAgEDGAADEwEaA1ABnwNbAeQDWwHQAy0B
|
||||
RQQCGAADVQG0A1cB7gNfAdoDRAF4AxgBIAMDAQQYAAMKAQ0DSQGGA10B6ANaAekDUAGfAyQBNAMJAQsE
|
||||
ARgAAwsBDgM7AWUDWwHTA1YBsv8AFQADAwEEAycBOgM/AW0DFAEbKAADDwETAzEBTgMdASkDAgEDHAAD
|
||||
DgESAzEBTQMjATMDBAEFJAADBgEHAygBPAMoATwDBgEHJAAEAQMHAQkDCwEOAwIBA/8ABQADAgEDAw8B
|
||||
FANJAYgDXwHlA10B6ANdAegDXQHoA10B6ANdAegDXQHoA10B6ANdAegDXQHoA10B6ANdAegDXQHoA10B
|
||||
6ANdAegDXQHoA10B6ANdAegDXQHoA1sB5ANGAX4DBQEGBAEoAAMCAQMDDQERAx8BLAMtAUYDVwG5A10B
|
||||
6ANdAegDXQHoA10B6ANdAegDXQHoA18B4wNZAb4DMgFPAw8BEwMCAQMwAAMTARkDRgF+A10B6ANdAegD
|
||||
WQHBAzkBXgMmATgDDwEUBAJcAAM5AV8DXgHiA1wB5wNdAegDXQHoA10B6ANdAegDXQHcAzwBZgMGAQgD
|
||||
BgEIAzwBZgNdAdwDXQHoA10B6ANdAegDXQHoA10B3ANQAZ0DJQE2IAADDAEQAzwBZgNdAc8DHgH9AyQB
|
||||
+gNSAe8DWAHtA1gB7QNYAe0DWAHtA1gB7QNYAe0DWAHtA1gB7QNYAe0DWAHtA1gB7QNYAe0DWAHtA1gB
|
||||
7QNSAe8DJAH6AyAB/ANWAbUDKwFCAwgBCiQABAIDGAEhA0ABcANaAb8DXQHfA1oB7QNSAe8DWwHsA14B
|
||||
5gNfAeUDWAHqA1gB7QNQAfADOgH2A10B0QNDAXcDHgErAwYBBywAAz0BaQNbAd4DAQH/Ay4B+QNcAewD
|
||||
WwHkA14B1wNEAXsDHgEqAwYBCFgAAz8BbAMfAf0DOQH4A1IB7wNYAe0DVgHuAyQB+gM5AfYDTwGbAx4B
|
||||
KwMgAS0DUAGdAzkB9gMkAfoDVgHuA1gB7QNSAe8DPQH3A1AB8gM7AWUgAAMTARkDUAGcAz0B9wM6AfYD
|
||||
XQHMA0sBjQNGAYADRgGAA0YBgANGAYADRgGAA0YBgANGAYADRgGAA0YBgANGAYADRgGAA0YBgANGAYAD
|
||||
RgGAA0sBjwNcAdkDHgH9A1QB8QNOAZQDEgEYJAADBQEGAzEBTgNbAdADUAHyA14B4gNVAa4DSgGKA0YB
|
||||
fwNDAXcDQwF2A0UBfANGAYADTwGbA14B3QNdAegDXwHaA04BlgMoATwDCQEMKAADRwGDAzkB+AMgAfwD
|
||||
WwHTA1MBpgNcAdkDLgH5A2AB4ANQAZoDLQFEAwsBDlQAAz8BbAMgAfwDWwHTA0wBjgNGAYADSgGKA10B
|
||||
3AMfAf0DXgHdAzoBYAM6AWIDXQHfAx4B/QNdAdwDSgGKA0YBgANMAY4DWwHTAyAB/AM/AWwgAAMUARsD
|
||||
UgGlAx0B/QNbAdADPwFsAxgBIQMOARIDDgESAw4BEgMOARIDDgESAw4BEgMOARIDDgESAw4BEgMOARID
|
||||
DgESAw4BEgMOARIDDgESAyABLgNXAbkDHgH9Ax0B/QNSAaUDFAEbJAADBQEGAzEBTQNaAccDXAHIA0AB
|
||||
bwMlATcDEwEaAw4BEgMNAREDDAEQAw4BEgMOARIDHQEoAzoBYANLAY8DWwHYA14B5gNWAbYDMQFNAwYB
|
||||
CCQAA0kBhgMhAfsDLgH5A1UBrgMqAUADPwFtA10B0QNDAfUDVgHuA1gBtwM2AVcDFgEdAwwBEAQCSAAD
|
||||
PwFsAy4B+QNVAa4DIAEtAw4BEgMbASUDWQG+AwAB/wNYAe0DPwFtAz8BbQNYAe0DAAH/A1kBvgMbASUD
|
||||
DgESAyABLQNVAa4DLgH5Az8BbCAAAxQBGwNSAaUDHQH9A1YBtgMoATwDCAEKOAADFgEeA1YBswMeAf0D
|
||||
HQH9A1IBpQMUARskAAQBAw8BFAMjATMDIQEwAwsBDgMDAQQEARQABAIDBwEJAx4BKgNLAYwDXQHfAz0B
|
||||
9wNTAakDKAE7JAADSQGGAyEB+wMuAfkDUwGnAxYBHgMMARADKgFAA1gBugNOAfMDRgH0A2AB2wNXAbwD
|
||||
QgF0AxMBGQMCAQNEAAM/AWwDLgH5A1MBpwMVARwEAAMPARQDVwG5AwAB/wNYAe0DPwFtAz8BbQNYAe0D
|
||||
AAH/A1cBuQMPARQEAAMVARwDUwGnAy4B+QM/AWwgAAMUARsDUgGlAx0B/QNVAbQDJgE4AwcBCTgAAxYB
|
||||
HgNWAbMDHgH9Ax0B/QNSAaUDFAEbYAADFQEcA1MBpwMuAfkDIQH7A0kBhiQAA0kBhgMhAfsDLgH5A1MB
|
||||
pwMVARwIAAMPARMDQwF2A1wB2QMfAf0DAAH/AwAB/wNVAa8DFgEdRAADPwFsAy4B+QNTAacDFQEcBAAD
|
||||
DwEUA1cBuQMAAf8DWAHtAz8BbQM/AW0DWAHtAwAB/wNXAbkDDwEUBAADFQEcA1MBpwMuAfkDPwFsIAAD
|
||||
FAEbA1IBpQMdAf0DVQG0AyYBOAMHAQk4AAMWAR4DVgGzAx4B/QMdAf0DUgGlAxQBG2AAAwIBAwMeASoD
|
||||
VQG0Az0B9wNSAaADGwElAwUBBhwAA0kBhgMhAfsDLgH5A1MBpwMVARwIAAQCAwkBCwMYASADRAF6A2AB
|
||||
2wM7AfcDPQH3A1kBuwMqAUADDgESAwUBBgQCNAADPwFsAy4B+QNTAacDFQEcBAADDwEUA1cBuQMAAf8D
|
||||
WAHtAz8BbQM/AW0DWAHtAwAB/wNXAbkDDwEUBAADFQEcA1MBpwMuAfkDPwFsIAADFAEbA1IBpQMdAf0D
|
||||
VQG0AyYBOAMHAQk4AAMWAR4DVgGzAx4B/QMdAf0DUgGlAxQBG2QAAw8BEwNAAW8DXQHUA1MB7wNMAZED
|
||||
EgEYHAADSQGGAyEB+wMuAfkDUwGnAxUBHBAABAIDDQERAzYBVwNYAbcDWwHsA1UB8QNdAdEDRAF5AzMB
|
||||
UAMbASYDBgEHMAADPwFsAy4B+QNTAacDFQEcBAADDwEUA1cBuQMAAf8DWAHtAz8BbQM/AW0DWAHtAwAB
|
||||
/wNXAbkDDwEUBAADFQEcA1MBpwMuAfkDPwFsIAADFAEbA1IBpQMdAf0DVQG0AyYBOAMHAQk4AAMWAR4D
|
||||
VgGzAx4B/QMdAf0DUgGlAxQBG2QAAwIBAwMPARMDUQGeAx0B/QNSAaUDFAEbHAADSQGGAyEB+wMuAfkD
|
||||
UwGnAxUBHBgAAwsBDgMtAUQDSwGPA1sBxQNhAeEDYQHhA1sBzQNMAZADKAE7AwkBDCwAAz8BbAMuAfkD
|
||||
UwGnAxUBHAQAAw8BFANXAbkDAAH/A1gB7QM/AW0DPwFtA1gB7QMAAf8DVwG5Aw8BFAQAAxUBHANTAacD
|
||||
LgH5Az8BbCAAAxQBGwNSAaUDHQH9A1UBtAMmATgDBwEJOAADFgEeA1YBswMeAf0DHQH9A1IBpQMUARts
|
||||
AANNAZMDHQH9A1IBpQMUARscAANJAYYDIQH7Ay4B+QNTAacDFQEcHAADBgEIAxkBIgMvAUkDQAFxA10B
|
||||
zANNAfMDWgHpA1YBtgMtAUQsAAM/AWwDLgH5A1MBpwMVARwEAAMPARQDVwG5AwAB/wNYAe0DPwFtAz8B
|
||||
bQNYAe0DAAH/A1cBuQMPARQEAAMVARwDUwGnAy4B+QM/AWwgAAMUARsDUgGlAx0B/QNVAbQDJgE4AwcB
|
||||
CTgAAxYBHgNWAbMDHgH9Ax0B/QNSAaUDFAEbbAADTQGTAx0B/QNSAaUDFAEbHAADSQGGAyEB+wMuAfkD
|
||||
UwGnAxUBHCAABAIDBAEFAwsBDgMwAUwDWQHBAzoB9gMsAfkDQQFyAwYBCCgAAz8BbAMuAfkDUwGnAxUB
|
||||
HAQAAw8BFANXAbkDAAH/A1gB7QM/AW0DPwFtA1gB7QMAAf8DVwG5Aw8BFAQAAxUBHANTAacDLgH5Az8B
|
||||
bCAAAxQBGwNSAaUDHQH9A1UBtAMmATgDBwEJOAADFgEeA1YBswMeAf0DHQH9A1IBpQMUARtsAANNAZMD
|
||||
HQH9A1IBpQMUARscAANJAYYDIQH7Ay4B+QNTAacDFQEcMAADFQEcA1MBpwMuAfkDWwHTAzoBYSgAAz8B
|
||||
bAMuAfkDUwGnAxUBHAQAAw8BFANXAbkDAAH/A1gB7QM/AW0DPwFtA1gB7QMAAf8DVwG5Aw8BFAQAAxUB
|
||||
HANTAacDLgH5Az8BbCAAAxQBGwNSAaUDHQH9A1UBtAMmATgDBwEJOAADFgEeA1YBswMeAf0DHQH9A1IB
|
||||
pQMUARsQAAQBAwQBBQMLAQ4DDwETAw8BEwMPARMDDwETAw8BEwMNAREDCQEMAwMBBAQBLAADTQGTAx0B
|
||||
/QNSAaUDFAEbHAADSQGGAyEB+wMuAfkDUwGnAxUBHCAABAEDAgEDAwsBDgMwAUwDWQHBAzoB9gMsAfkD
|
||||
QQFyAwYBCCgAAz8BbAMuAfkDUwGnAxUBHAQAAw8BFANXAbkDAAH/A1gB7QM/AW0DPwFtA1gB7QMAAf8D
|
||||
VwG5Aw8BFAQAAxUBHANTAacDLgH5Az8BbCAAAxQBGwNSAaUDHQH9A1UBtAMmATgDBwEJOAADFgEeA1YB
|
||||
swMeAf0DHQH9A1IBpQMUARsQAAMGAQgDJAE1Az4BawNGAX0DRgF+A0YBfgNGAX4DRgF+A0QBewM9AWkD
|
||||
JAE0AwkBDCwAA00BkwMdAf0DUgGlAxQBGxwAA0kBhgMhAfsDLgH5A1MBpwMVARwcAAMGAQgDEgEXAyMB
|
||||
MwM/AW4DWwHNAzoB9gNYAeoDVgG2Ay0BRCwAAz8BbAMuAfkDUwGnAxUBHAQAAw8BFANXAbkDAAH/A1gB
|
||||
7QM/AW0DPwFtA1gB7QMAAf8DVwG5Aw8BFAQAAxUBHANTAacDLgH5Az8BbCAAAxQBGwNSAaUDHQH9A1UB
|
||||
tAMmATgDBwEJOAADFgEeA1YBswMeAf0DHQH9A1IBpQMUARsQAAMQARUDRgF/A1wB2QNYAeoDXgHmA2EB
|
||||
4QNgAeADXgHiA1wB5wNhAeEDUAGdAyEBMCQAAwIBAwMPARMDUQGeAx0B/QNSAaUDFAEbHAADSQGGAyEB
|
||||
+wMuAfkDUwGnAxUBHBgAAwsBDgMtAUQDRgGBA1MBqQNdAd8DXQHoA18B2gNOAZYDKAE8AwkBDCwAAz8B
|
||||
bAMuAfkDUwGnAxUBHAQAAw8BFANXAbkDAAH/A1gB7QM/AW0DPwFtA1gB7QMAAf8DVwG5Aw8BFAQAAxUB
|
||||
HANTAacDLgH5Az8BbCAAAxQBGwNSAaUDHQH9A1UBtAMmATgDBwEJOAADFgEeA1YBswMeAf0DHQH9A1IB
|
||||
pQMUARsQAAMTARoDTgGYA0IB9QMAAf8DYAHgA1YBtgNZAb4DXgHXAz0B9wMgAfwDWgHHAysBQiQAAw8B
|
||||
EwNAAW8DXQHUA1MB7wNNAZIDEgEYHAADSQGGAyEB+wMuAfkDUwGnAxUBHBAABAIDDQERAzYBVwNYAbcD
|
||||
WAHqA1wB7ANfAdUDSwGPAzoBYAMeASsDBgEHMAADPwFsAy4B+QNTAacDFQEcBAADDwEUA1cBuQMAAf8D
|
||||
WAHtAz8BbQM/AW0DWAHtAwAB/wNXAbkDDwEUBAADFQEcA1MBpwMuAfkDPwFsIAADFAEbA1IBpQMdAf0D
|
||||
VQG0AyYBOAMHAQk4AAMWAR4DVgGzAx4B/QMdAf0DUgGlAxQBGxAAAxQBGwNQAZoDOQH2AwAB/wNRAaQD
|
||||
JAE0A0QBeANeAd0DPQH3A1sB3gM7AWMDDgESIAADAgEDAx4BKgNVAbQDPQH3A1EBoQMbASYDBQEGHAAD
|
||||
SQGGAyEB+wMuAfkDUwGnAxUBHAwABAEDEwEZA0QBegNgAdsDOwH3Az0B9wNZAbsDKwFCAxMBGQMIAQoD
|
||||
AgEDNAADPwFsAy4B+QNTAacDFQEcBAADDwEUA1cBuQMAAf8DWAHtAz8BbQM/AW0DWAHtAwAB/wNXAbkD
|
||||
DwEUBAADFQEcA1MBpwMuAfkDPwFsIAADFAEbA1IBpQMdAf0DVQG0AyYBOAMHAQk4AAMWAR4DVgGzAx4B
|
||||
/QMdAf0DUgGlAxQBGxAAAxQBGwNQAZoDOQH2AwAB/wNOAZUDMQFMA2EB4QMAAf8DWwHNAxoBJCgAAxUB
|
||||
HANTAacDLgH5AyEB+wNJAYYkAANJAYYDIQH7Ay4B+QNTAacDFQEcDAADCwEOA00BkgMrAfkDAAH/AwAB
|
||||
/wNVAa8DFgEdRAADPwFsAy4B+QNTAacDFQEcBAADDwEUA1cBuQMAAf8DWAHtAz8BbQM/AW0DWAHtAwAB
|
||||
/wNXAbkDDwEUBAADFQEcA1MBpwMuAfkDPwFsIAADFAEbA1IBpQMdAf0DVgG2AygBPAMIAQo4AAMWAR4D
|
||||
VgGzAx4B/QMdAf0DUgGlAxQBGxAAAxQBGwNQAZoDOQH2AwAB/wNDAfUDVAHvAyEB/AMRAf4DXQHMAx0B
|
||||
KQQCGAAEAgMGAQcDDwETAzIBTwNZAcADQgH1A10B0QM6AWAkAANJAYYDIQH7Ay4B+QNTAacDFgEeAwwB
|
||||
EAMqAUADVwG5A1oB6QNTAe8DWwHoA10BzwNGAX0DEwEaAwIBA0QAAz8BbAMuAfkDUwGnAxUBHAQAAw8B
|
||||
FANXAbkDAAH/A1gB7QM/AW0DPwFtA1gB7QMAAf8DVwG5Aw8BFAQAAxUBHANTAacDLgH5Az8BbCAAAxQB
|
||||
GwNSAaUDHQH9A1sB0AM/AWwDGAEhAw4BEgMOARIDDgESAw4BEgMOARIDDgESAw4BEgMOARIDDgESAw4B
|
||||
EgMOARIDDgESAw4BEgMOARIDIAEuA1cBuQMeAf0DHQH9A1IBpQMUARsQAAMUARsDUAGaAzkB9gMAAf8D
|
||||
AAH/AyMB+gNCAfUDHgH9A2AB4ANAAXEDHQEoAw4BEgMNAREDCQEMAwgBCgMLBA4BEgMcAScDOAFbA0QB
|
||||
eQNaAccDYQHhA1YBtgM0AVQDDAEPJAADSQGGAyEB+wMuAfkDVQGuAyoBQAM/AW0DXQHRA0MB9QNWAe4D
|
||||
WQG7A0QBeQMqAUADEQEWBAJIAAM/AWwDLgH5A1UBrgMgAS0DDgESAxsBJQNZAb4DAAH/A1gB7QM/AW0D
|
||||
PwFtA1gB7QMAAf8DWQG+AxsBJQMOARIDIAEtA1UBrgMuAfkDPwFsIAADEwEaA1EBpAMfAfwDOQH2A10B
|
||||
zANLAY0DRgGAA0YBgANGAYADRgGAA0YBgANGAYADRgGAA0YBgANGAYADRgGAA0YBgANGAYADRgGAA0YB
|
||||
gANLAY8DXAHZAx4B/QNFAfQDTgGYAxMBGRAAAxIBGANLAYwDWAHtAwAB/wMgAf0DXAHZA1UBsQNdAd8D
|
||||
QwH1A2AB4ANPAZsDRgGAA0QBegM5AV4DMwFQAz0BaANGAX4DUAGaA18B2gNbAeQDXwHaA08BmwMpAT4D
|
||||
CgENKAADSQGGAyEB+wMgAfwDWwHTA1MBpgNcAdkDLgH5A2AB4ANQAZoDLQFFAxABFQMGAQcEAUwAAz8B
|
||||
bAMgAfwDWwHTA0wBjgNGAYADSgGKA10B3AMeAf0DXQHfAzoBYgM6AWIDXQHfAx4B/QNdAdwDSgGKA0YB
|
||||
gANMAY4DWwHTAyAB/AM/AWwgAAMSARcDSgGLA1oB6gMPAf4DJAH6A1IB7wNYAe0DWAHtA1gB7QNYAe0D
|
||||
WAHtA1gB7QNYAe0DWAHtA1gB7QNYAe0DWAHtA1gB7QNYAe0DWAHtA1IB7wMkAfoDHgH9A1cBwgM0AVQD
|
||||
CwEOEAADCAEKAy4BSANVAbEDXgHiA2AB2wNPAZsDLQFEA0MBdwNdAcwDYQHhA1gB6gNYAe0DWgHpA1sB
|
||||
0wNcAcgDXwHaA1wB6wNOAfIDLgH5A14B0gNEAXgDIQEvAwcBCSwAA0IBdQNYAeoDAQH/Ay4B+QNQAfAD
|
||||
XAHsA1wB2QNEAXsDHgEqAwYBCFgAAz8BbAMBAf8DLgH5A1IB7wNYAe0DVgHuAyQB+gM5AfYDUAGdAyAB
|
||||
LQMgAS0DUAGdAzkB9gMkAfoDVgHuA1gB7QNSAe8DLgH5Aw8B/gM/AWwgAAMFAQYDGgEjA00BkwNgAeYD
|
||||
WgHtAzwB9gM9AfcDPQH3Az0B9wM9AfcDPQH3Az0B9wM9AfcDPQH3Az0B9wM9AfcDPQH3Az0B9wM9AfcD
|
||||
PQH3AzwB9gNaAe0DXwHlA0cBgwMKAQ0EAhAABAEDCAEKAx8BLAMuAUgDLQFEAxsBJQMHAQkDDwETAyMB
|
||||
MgMuAUcDVwG5A10B6ANaAekDVwHuA1UB8QNcAewDWgHpA10B6ANbAdADNAFTAw8BEwMCAQMwAAMWAR4D
|
||||
RwGDA1oB7QNVAfEDXwHVA0wBjgMrAUIDDwEUBAJcAAM6AWIDWgHpA1oB7QM8AfYDPQH3AzwB9gNcAewD
|
||||
XQHcAzwBZgMGAQgDBgEIAzwBZgNdAdwDWgHtAzwB9gM9AfcDPAH2A1oB7QNgAeYDOgFiLAAEAQMjATMD
|
||||
TgGXA1QBqwNUAasDVAGrA1QBqwNUAasDVAGrA1QBqwNUAasDVAGrA1QBqwNUAasDVAGrA1QBqwNUAasD
|
||||
TgGXAyMBMwQBTAADCQELAzkBXQNHAYIDJwE6AwQBBUgABAIDIgExAzoBYQMPARRwAAMDAQQDKAE7A04B
|
||||
mANUAasDUQGeAyEBLxQAAwMBBAMlATcDUAGfA1QBqwNOAZgDKAE7AwMBBBgAAUIBTQE+BwABPgMAASgD
|
||||
AAF8AwABMAMAAQEBAAEBBgABAxYAA/8BAAH8AQMB8AE/AQMB8AEPAcAIAAH8AQEB8AE/AQMB8AEHAcAI
|
||||
AAH8AQABMAE/AQAB8AEDAYAIAAH8AQABEAE/AQABcAEAAYAIAAH8AgABPwEAATABAAGACAAB/AIAAT8B
|
||||
AAEwAQABgAgAAfwCAAE/DAAB/AEIAQABPwEICwAB/AEMAQABPwEMAQABIAkAAfwBDwEAAT8BDAEAATAJ
|
||||
AAH8AQ8BgAE/AQ4BAAE4CQAB/AEPAfABPwEPAcABPwkAAfwBDwGAAT8BDgEAATgJAAH8AQ8BAAE/AQwB
|
||||
AAEwCQAB/AEMAQABPwEMAQABIAkAAfwBCAEAAT8BCAsAAfwCAAE/DAAB/AIAAT8BAAEwCgAB/AIAAT8B
|
||||
AAEwAQABgAgAAfwBAAEQAT8BAAFwAQABgAgAAfwBAAEwAT8BAAHwAQMBgAgAAfwBAAHwAT8BAQHwAQcB
|
||||
wAgAAfwBAwHwAT8BAwHwAQ8BwAgAAf4BHwH4AX8BDwH4AX8BwAgAAeACAAEHAf4BAAEBAf8B4AEPAv8B
|
||||
4AEAAQEB8AHgAgABBwH8AgAB/wHgAQcC/wHgAQABAQHwAeACAAEHAfwCAAF/AeABAwL/AeABAAEBAfAB
|
||||
4AIAAQcB/AIAAT8B4AEAAX8B/wHgAQABAQHwAeABfwH+AQcB/AEHAcABPwHgAQABPwH/AeEBAAEhAfAB
|
||||
4AF/Af4BBwL/AfgBPwHgAcABPwH/AeEBAAEhAfAB4AF/Af4BBwL/AfgBDwHgAcABAwH/AeEBAAEhAfAB
|
||||
4AF/Af4BBwL/AfwBDwHgAfABAQH/AeEBAAEhAfAB4AF/Af4BBwL/AfwBDwHgAfwBAAH/AeEBAAEhAfAB
|
||||
4AF/Af4BBwP/AQ8B4AH+AQAB/wHhAQABIQHwAeABfwH+AQcD/wEPAeAB/wEAAX8B4QEAASEB8AHgAX8B
|
||||
/gEHA/8BDwHgAf8B8AF/AeEBAAEhAfAB4AF/Af4BBwGAAQcB/wEPAeAB/wEAAX8B4QEAASEB8AHgAX8B
|
||||
/gEHAYABBwH/AQ8B4AH+AQAB/wHhAQABIQHwAeABfwH+AQcBgAEHAfwBDwHgAfwBAAH/AeEBAAEhAfAB
|
||||
4AF/Af4BBwGAAQcB/AEPAeAB8AEBAf8B4QEAASEB8AHgAX8B/gEHAYABBwH4AQ8C4AEDAf8B4QEAASEB
|
||||
8AHgAX8B/gEHAYABHwH4AT8C4AE/Af8B4QEAASEB8AHgAX8B/gEHAYABDwHAAT8B4AEAAT8B/wHhAQAB
|
||||
IQHwAeACAAEHAYACAAE/AeABAAF/Af8B4AEAAQEB8AHgAgABBwGAAgABfwHgAQAC/wHgAQABAQHwAeAC
|
||||
AAEHAYACAAH/AeABBwL/AeABAAEBAfAB4AIAAQcBgAEAAQEB/wHgAQ8C/wHgAQABAQHwAfwCAAE/Af8B
|
||||
+AE/Af8B8AP/AfABPgEDAfAL
|
||||
</value>
|
||||
</data>
|
||||
<metadata name="toolTip.TrayLocation" type="System.Drawing.Point, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a">
|
||||
<value>165, 17</value>
|
||||
</metadata>
|
||||
</root>
|
||||
@@ -1,6 +1,6 @@
|
||||
namespace SpineViewer.Controls
|
||||
{
|
||||
partial class SpinePropertyGrid
|
||||
partial class SpineViewPropertyGrid
|
||||
{
|
||||
/// <summary>
|
||||
/// 必需的设计器变量。
|
||||
@@ -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,10 +68,11 @@
|
||||
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;
|
||||
tabControl.ItemSize = new Size(100, 35);
|
||||
tabControl.ItemSize = new Size(90, 35);
|
||||
tabControl.Location = new Point(0, 0);
|
||||
tabControl.Multiline = true;
|
||||
tabControl.Name = "tabControl";
|
||||
@@ -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(437, 405);
|
||||
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(437, 405);
|
||||
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(437, 405);
|
||||
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(437, 405);
|
||||
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(437, 405);
|
||||
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(437, 405);
|
||||
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(241, 67);
|
||||
//
|
||||
// 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(240, 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(437, 405);
|
||||
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(437, 405);
|
||||
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(437, 405);
|
||||
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(437, 405);
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -7,14 +7,14 @@ using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows.Forms;
|
||||
using SpineViewer.PropertyGridWrappers.Spine;
|
||||
using SpineViewer.Utilities;
|
||||
using SpineViewer.Utils;
|
||||
using SpineViewer.Spine.SpineView;
|
||||
|
||||
namespace SpineViewer.Controls
|
||||
{
|
||||
public partial class SpinePropertyGrid : UserControl
|
||||
public partial class SpineViewPropertyGrid : UserControl
|
||||
{
|
||||
public SpinePropertyGrid()
|
||||
public SpineViewPropertyGrid()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
@@ -22,7 +22,7 @@ namespace SpineViewer.Controls
|
||||
/// <summary>
|
||||
/// 设置选中的对象列表, 可以赋值 null 来清空选中, 行为与 PropertyGrid.SelectedObjects 类似
|
||||
/// </summary>
|
||||
public SpineWrapper[] SelectedSpines
|
||||
public SpineObjectProperty[] SelectedSpines
|
||||
{
|
||||
get => selectedSpines ?? [];
|
||||
set
|
||||
@@ -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,33 +45,20 @@ 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
private SpineWrapper[]? 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 SkinWrapper;
|
||||
}
|
||||
else
|
||||
{
|
||||
toolStripMenuItem_AddSkin.Enabled = false;
|
||||
toolStripMenuItem_RemoveSkin.Enabled = false;
|
||||
}
|
||||
}
|
||||
private SpineObjectProperty[]? selectedSpines = null;
|
||||
|
||||
private void contextMenuStrip_Animation_Opening(object sender, CancelEventArgs e)
|
||||
{
|
||||
if (selectedSpines?.Length == 1)
|
||||
{
|
||||
toolStripMenuItem_AddAnimation.Enabled = true;
|
||||
toolStripMenuItem_RemoveAnimation.Enabled = propertyGrid_Animation.SelectedGridItem.Value is TrackWrapper;
|
||||
toolStripMenuItem_RemoveAnimation.Enabled = propertyGrid_Animation.SelectedGridItem.Value is TrackAnimationProperty;
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -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 SkinWrapper 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)
|
||||
@@ -119,11 +86,22 @@ namespace SpineViewer.Controls
|
||||
{
|
||||
if (selectedSpines?.Length != 1) return;
|
||||
|
||||
if (propertyGrid_Animation.SelectedGridItem.Value is TrackWrapper wrapper)
|
||||
if (propertyGrid_Animation.SelectedGridItem.Value is TrackAnimationProperty wrapper)
|
||||
{
|
||||
selectedSpines[0].Animation.Spine.ClearTrack(wrapper.Index);
|
||||
propertyGrid_Animation.Refresh();
|
||||
}
|
||||
}
|
||||
|
||||
public override void Refresh()
|
||||
{
|
||||
base.Refresh();
|
||||
propertyGrid_BaseInfo.Refresh();
|
||||
propertyGrid_Render.Refresh();
|
||||
propertyGrid_Transform.Refresh();
|
||||
propertyGrid_Skin.Refresh();
|
||||
propertyGrid_Animation.Refresh();
|
||||
propertyGrid_Debug.Refresh();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
using SpineViewer.Utilities;
|
||||
using SpineViewer.Utils;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
using SpineViewer.Spine;
|
||||
using SpineViewer.Utilities;
|
||||
using SpineViewer.Utils;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
@@ -22,7 +22,7 @@ namespace SpineViewer.Dialogs
|
||||
public BatchOpenSpineDialog()
|
||||
{
|
||||
InitializeComponent();
|
||||
comboBox_Version.DataSource = SpineHelper.Names.ToList();
|
||||
comboBox_Version.DataSource = SpineUtils.Names.ToList();
|
||||
comboBox_Version.DisplayMember = "Value";
|
||||
comboBox_Version.ValueMember = "Key";
|
||||
comboBox_Version.SelectedValue = SpineVersion.Auto;
|
||||
@@ -49,7 +49,7 @@ namespace SpineViewer.Dialogs
|
||||
}
|
||||
}
|
||||
|
||||
if (version != SpineVersion.Auto && !Spine.Spine.HasImplementation(version))
|
||||
if (version != SpineVersion.Auto && !Spine.SpineObject.HasImplementation(version))
|
||||
{
|
||||
MessagePopup.Info($"{version.GetName()} 版本尚未实现(咕咕咕~)");
|
||||
return;
|
||||
|
||||
110
SpineViewer/Dialogs/ConvertFileFormatDialog.Designer.cs
generated
110
SpineViewer/Dialogs/ConvertFileFormatDialog.Designer.cs
generated
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
using SpineViewer.Spine;
|
||||
using SpineViewer.Utilities;
|
||||
using NLog;
|
||||
using SpineViewer.Spine;
|
||||
using SpineViewer.Utils;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
@@ -14,6 +15,8 @@ namespace SpineViewer.Dialogs
|
||||
{
|
||||
public partial class ConvertFileFormatDialog : Form
|
||||
{
|
||||
private readonly Logger logger = LogManager.GetCurrentClassLogger();
|
||||
|
||||
/// <summary>
|
||||
/// 对话框结果, 取消时为 null
|
||||
/// </summary>
|
||||
@@ -23,13 +26,13 @@ namespace SpineViewer.Dialogs
|
||||
{
|
||||
InitializeComponent();
|
||||
|
||||
comboBox_SourceVersion.DataSource = SpineHelper.Names.ToList();
|
||||
comboBox_SourceVersion.DataSource = SpineUtils.Names.ToList();
|
||||
comboBox_SourceVersion.DisplayMember = "Value";
|
||||
comboBox_SourceVersion.ValueMember = "Key";
|
||||
comboBox_SourceVersion.SelectedValue = SpineVersion.Auto;
|
||||
|
||||
// 目标版本不包含自动
|
||||
var versionsWithoutAuto = SpineHelper.Names.ToDictionary();
|
||||
var versionsWithoutAuto = SpineUtils.Names.ToDictionary();
|
||||
versionsWithoutAuto.Remove(SpineVersion.Auto);
|
||||
comboBox_TargetVersion.DataSource = versionsWithoutAuto.ToList();
|
||||
comboBox_TargetVersion.DisplayMember = "Value";
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
using Microsoft.Win32;
|
||||
using SpineViewer.Utilities;
|
||||
using SpineViewer.Utils;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
|
||||
10
SpineViewer/Dialogs/ExportDialog.Designer.cs
generated
10
SpineViewer/Dialogs/ExportDialog.Designer.cs
generated
@@ -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;
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
using SpineViewer.PropertyGridWrappers.Exporter;
|
||||
using SpineViewer.Utilities;
|
||||
using SpineViewer.Utils;
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
@@ -8,14 +7,15 @@ using System.Data;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using SpineViewer.Spine.SpineExporter;
|
||||
|
||||
namespace SpineViewer.Dialogs
|
||||
{
|
||||
public partial class ExportDialog : Form
|
||||
{
|
||||
private readonly ExporterWrapper wrapper;
|
||||
private readonly ExporterProperty wrapper;
|
||||
|
||||
public ExportDialog(ExporterWrapper wrapper)
|
||||
public ExportDialog(ExporterProperty wrapper)
|
||||
{
|
||||
InitializeComponent();
|
||||
this.wrapper = wrapper;
|
||||
|
||||
4
SpineViewer/Dialogs/OpenSpineDialog.Designer.cs
generated
4
SpineViewer/Dialogs/OpenSpineDialog.Designer.cs
generated
@@ -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
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
using SpineViewer.Spine;
|
||||
using SpineViewer.Utilities;
|
||||
using SpineViewer.Utils;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
@@ -21,7 +21,7 @@ namespace SpineViewer.Dialogs
|
||||
public OpenSpineDialog()
|
||||
{
|
||||
InitializeComponent();
|
||||
comboBox_Version.DataSource = SpineHelper.Names.ToList();
|
||||
comboBox_Version.DataSource = SpineUtils.Names.ToList();
|
||||
comboBox_Version.DisplayMember = "Value";
|
||||
comboBox_Version.ValueMember = "Key";
|
||||
comboBox_Version.SelectedValue = SpineVersion.Auto;
|
||||
@@ -80,7 +80,7 @@ namespace SpineViewer.Dialogs
|
||||
atlasPath = Path.GetFullPath(atlasPath);
|
||||
}
|
||||
|
||||
if (version != SpineVersion.Auto && !Spine.Spine.HasImplementation(version))
|
||||
if (version != SpineVersion.Auto && !Spine.SpineObject.HasImplementation(version))
|
||||
{
|
||||
MessagePopup.Info($"{version.GetName()} 版本尚未实现(咕咕咕~)");
|
||||
return;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
using NLog;
|
||||
using SpineViewer.Utilities;
|
||||
using SpineViewer.Utils;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
|
||||
@@ -1,55 +0,0 @@
|
||||
using FFMpegCore;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace SpineViewer.Exporter
|
||||
{
|
||||
/// <summary>
|
||||
/// MP4 导出参数
|
||||
/// </summary>
|
||||
public class AvifExporter : FFmpegVideoExporter
|
||||
{
|
||||
public AvifExporter()
|
||||
{
|
||||
FPS = 24;
|
||||
}
|
||||
|
||||
public override string Format => "avif";
|
||||
|
||||
public override string Suffix => ".avif";
|
||||
|
||||
/// <summary>
|
||||
/// 编码器
|
||||
/// </summary>
|
||||
public string Codec { get; set; } = "av1_nvenc";
|
||||
|
||||
/// <summary>
|
||||
/// CRF
|
||||
/// </summary>
|
||||
public int CRF { get => crf; set => crf = Math.Clamp(value, 0, 63); }
|
||||
private int crf = 23;
|
||||
|
||||
/// <summary>
|
||||
/// 像素格式
|
||||
/// </summary>
|
||||
public string PixelFormat { get; set; } = "yuv420p";
|
||||
|
||||
/// <summary>
|
||||
/// 循环次数, 0 无限循环, 取值范围 [0, 65535]
|
||||
/// </summary>
|
||||
public int Loop { get => loop; set => loop = Math.Clamp(value, 0, 65535); }
|
||||
private int loop = 0;
|
||||
|
||||
public override string FileNameNoteSuffix => $"{Codec}_{CRF}_{PixelFormat}";
|
||||
|
||||
public override void SetOutputOptions(FFMpegArgumentOptions options)
|
||||
{
|
||||
base.SetOutputOptions(options);
|
||||
options.WithVideoCodec(Codec).WithConstantRateFactor(CRF).WithCustomArgument($"-loop {Loop}");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,36 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace SpineViewer.Exporter
|
||||
{
|
||||
/// <summary>
|
||||
/// FFmpeg 自定义视频导出参数
|
||||
/// </summary>
|
||||
public class CustomExporter : FFmpegVideoExporter
|
||||
{
|
||||
public CustomExporter()
|
||||
{
|
||||
CustomArgument = "-c:v libx264 -crf 23 -pix_fmt yuv420p"; // 提供一个示例参数
|
||||
}
|
||||
|
||||
public override string Format => CustomFormat;
|
||||
|
||||
public override string Suffix => CustomSuffix;
|
||||
|
||||
public override string FileNameNoteSuffix => string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 文件格式
|
||||
/// </summary>
|
||||
public string CustomFormat { get; set; } = "mp4";
|
||||
|
||||
/// <summary>
|
||||
/// 文件名后缀
|
||||
/// </summary>
|
||||
public string CustomSuffix { get; set; } = ".mp4";
|
||||
}
|
||||
}
|
||||
@@ -1,194 +0,0 @@
|
||||
using NLog;
|
||||
using SpineViewer.Extensions;
|
||||
using SpineViewer.PropertyGridWrappers;
|
||||
using SpineViewer.Utilities;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using System.Drawing.Design;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace SpineViewer.Exporter
|
||||
{
|
||||
/// <summary>
|
||||
/// 导出器基类
|
||||
/// </summary>
|
||||
public abstract class Exporter : IDisposable
|
||||
{
|
||||
/// <summary>
|
||||
/// 日志器
|
||||
/// </summary>
|
||||
protected readonly Logger logger = LogManager.GetCurrentClassLogger();
|
||||
|
||||
/// <summary>
|
||||
/// 可用于文件名的时间戳字符串
|
||||
/// </summary>
|
||||
protected readonly string timestamp = DateTime.Now.ToString("yyMMddHHmmss");
|
||||
|
||||
~Exporter() { Dispose(false); }
|
||||
public void Dispose() { Dispose(true); GC.SuppressFinalize(this); }
|
||||
protected virtual void Dispose(bool disposing) { View.Dispose(); }
|
||||
|
||||
/// <summary>
|
||||
/// 输出文件夹
|
||||
/// </summary>
|
||||
public string? OutputDir { get; set; } = null;
|
||||
|
||||
/// <summary>
|
||||
/// 导出单个
|
||||
/// </summary>
|
||||
public bool IsExportSingle { get; set; } = false;
|
||||
|
||||
/// <summary>
|
||||
/// 画面分辨率
|
||||
/// </summary>
|
||||
public Size Resolution { get; set; } = new(100, 100);
|
||||
|
||||
/// <summary>
|
||||
/// 渲染视窗, 接管对象生命周期
|
||||
/// </summary>
|
||||
public SFML.Graphics.View View { get => view; set { view.Dispose(); view = value; } }
|
||||
private SFML.Graphics.View view = new();
|
||||
|
||||
/// <summary>
|
||||
/// 是否仅渲染选中
|
||||
/// </summary>
|
||||
public bool RenderSelectedOnly { get; set; } = false;
|
||||
|
||||
/// <summary>
|
||||
/// 背景颜色
|
||||
/// </summary>
|
||||
public SFML.Graphics.Color BackgroundColor
|
||||
{
|
||||
get => backgroundColor;
|
||||
set
|
||||
{
|
||||
backgroundColor = value;
|
||||
var bcPma = value;
|
||||
var a = bcPma.A / 255f;
|
||||
bcPma.R = (byte)(bcPma.R * a);
|
||||
bcPma.G = (byte)(bcPma.G * a);
|
||||
bcPma.B = (byte)(bcPma.B * a);
|
||||
BackgroundColorPma = bcPma;
|
||||
}
|
||||
}
|
||||
private SFML.Graphics.Color backgroundColor = SFML.Graphics.Color.Transparent;
|
||||
|
||||
/// <summary>
|
||||
/// 预乘后的背景颜色
|
||||
/// </summary>
|
||||
public SFML.Graphics.Color BackgroundColorPma { get; private set; } = SFML.Graphics.Color.Transparent;
|
||||
|
||||
/// <summary>
|
||||
/// 获取供渲染的 SFML.Graphics.RenderTexture
|
||||
/// </summary>
|
||||
private SFML.Graphics.RenderTexture GetRenderTexture()
|
||||
{
|
||||
var tex = new SFML.Graphics.RenderTexture((uint)Resolution.Width, (uint)Resolution.Height);
|
||||
tex.Clear(SFML.Graphics.Color.Transparent);
|
||||
tex.SetView(View);
|
||||
return tex;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取单个模型的单帧画面
|
||||
/// </summary>
|
||||
protected SFMLImageVideoFrame GetFrame(Spine.Spine spine) => GetFrame([spine]);
|
||||
|
||||
/// <summary>
|
||||
/// 获取模型列表的单帧画面
|
||||
/// </summary>
|
||||
protected SFMLImageVideoFrame GetFrame(Spine.Spine[] spinesToRender)
|
||||
{
|
||||
// RenderTexture 必须临时创建, 随用随取, 防止出现跨线程的情况
|
||||
using var texPma = GetRenderTexture();
|
||||
|
||||
// 先将预乘结果准确绘制出来, 注意背景色也应当是预乘的
|
||||
texPma.Clear(BackgroundColorPma);
|
||||
foreach (var spine in spinesToRender) texPma.Draw(spine);
|
||||
texPma.Display();
|
||||
|
||||
// 背景色透明度不为 1 时需要处理反预乘, 否则直接就是结果
|
||||
if (BackgroundColor.A < 255)
|
||||
{
|
||||
// 从预乘结果构造渲染对象, 并正确设置变换
|
||||
using var view = texPma.GetView();
|
||||
using var img = texPma.Texture.CopyToImage();
|
||||
using var texSprite = new SFML.Graphics.Texture(img);
|
||||
using var sp = new SFML.Graphics.Sprite(texSprite)
|
||||
{
|
||||
Origin = new(texPma.Size.X / 2f, texPma.Size.Y / 2f),
|
||||
Position = new(view.Center.X, view.Center.Y),
|
||||
Scale = new(view.Size.X / texPma.Size.X, view.Size.Y / texPma.Size.Y),
|
||||
Rotation = view.Rotation
|
||||
};
|
||||
|
||||
// 混合模式用直接覆盖的方式, 保证得到的图像区域是反预乘的颜色和透明度, 同时使用反预乘着色器
|
||||
var st = SFML.Graphics.RenderStates.Default;
|
||||
st.BlendMode = SFMLBlendMode.SourceOnly;
|
||||
st.Shader = SFMLShader.InversePma;
|
||||
|
||||
// 在最终结果上二次渲染非预乘画面
|
||||
using var tex = GetRenderTexture();
|
||||
|
||||
// 将非预乘结果覆盖式绘制在目标对象上, 注意背景色应该用非预乘的
|
||||
tex.Clear(BackgroundColor);
|
||||
tex.Draw(sp, st);
|
||||
tex.Display();
|
||||
return new(tex.Texture.CopyToImage());
|
||||
}
|
||||
else
|
||||
{
|
||||
return new(texPma.Texture.CopyToImage());
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 每个模型在同一个画面进行导出
|
||||
/// </summary>
|
||||
protected abstract void ExportSingle(Spine.Spine[] spinesToRender, BackgroundWorker? worker = null);
|
||||
|
||||
/// <summary>
|
||||
/// 每个模型独立导出
|
||||
/// </summary>
|
||||
protected abstract void ExportIndividual(Spine.Spine[] spinesToRender, BackgroundWorker? worker = null);
|
||||
|
||||
/// <summary>
|
||||
/// 检查参数是否合法并规范化参数值, 否则返回用户错误原因
|
||||
/// </summary>
|
||||
public virtual string? Validate()
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(OutputDir) && File.Exists(OutputDir))
|
||||
return "输出文件夹无效";
|
||||
if (!string.IsNullOrWhiteSpace(OutputDir) && !Directory.Exists(OutputDir))
|
||||
return $"文件夹 {OutputDir} 不存在";
|
||||
if (IsExportSingle && string.IsNullOrWhiteSpace(OutputDir))
|
||||
return "导出单个时必须提供输出文件夹";
|
||||
|
||||
OutputDir = string.IsNullOrWhiteSpace(OutputDir) ? null : Path.GetFullPath(OutputDir);
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 执行导出
|
||||
/// </summary>
|
||||
/// <param name="spines">要进行导出的 Spine 列表</param>
|
||||
/// <param name="worker">用来执行该函数的 worker</param>
|
||||
/// <exception cref="ArgumentException"></exception>
|
||||
public virtual void Export(Spine.Spine[] spines, BackgroundWorker? worker = null)
|
||||
{
|
||||
if (Validate() is string err)
|
||||
throw new ArgumentException(err);
|
||||
|
||||
var spinesToRender = spines.Where(sp => !RenderSelectedOnly || sp.IsSelected).Reverse().ToArray();
|
||||
|
||||
if (IsExportSingle) ExportSingle(spinesToRender, worker);
|
||||
else ExportIndividual(spinesToRender, worker);
|
||||
|
||||
logger.LogCurrentProcessMemoryUsage();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,49 +0,0 @@
|
||||
using FFMpegCore;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace SpineViewer.Exporter
|
||||
{
|
||||
/// <summary>
|
||||
/// MKV 导出参数
|
||||
/// </summary>
|
||||
public class MkvExporter : FFmpegVideoExporter
|
||||
{
|
||||
public MkvExporter()
|
||||
{
|
||||
BackgroundColor = new(0, 255, 0);
|
||||
}
|
||||
|
||||
public override string Format => "matroska";
|
||||
|
||||
public override string Suffix => ".mkv";
|
||||
|
||||
/// <summary>
|
||||
/// 编码器
|
||||
/// </summary>
|
||||
public string Codec { get; set; } = "libx265";
|
||||
|
||||
/// <summary>
|
||||
/// CRF
|
||||
/// </summary>
|
||||
public int CRF { get => crf; set => crf = Math.Clamp(value, 0, 63); }
|
||||
private int crf = 23;
|
||||
|
||||
/// <summary>
|
||||
/// 像素格式
|
||||
/// </summary>
|
||||
public string PixelFormat { get; set; } = "yuv444p";
|
||||
|
||||
public override string FileNameNoteSuffix => $"{Codec}_{CRF}_{PixelFormat}";
|
||||
|
||||
public override void SetOutputOptions(FFMpegArgumentOptions options)
|
||||
{
|
||||
base.SetOutputOptions(options);
|
||||
options.WithVideoCodec(Codec).WithConstantRateFactor(CRF).ForcePixelFormat(PixelFormat);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,48 +0,0 @@
|
||||
using FFMpegCore;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace SpineViewer.Exporter
|
||||
{
|
||||
/// <summary>
|
||||
/// MOV 导出参数
|
||||
/// </summary>
|
||||
public class MovExporter : FFmpegVideoExporter
|
||||
{
|
||||
public MovExporter()
|
||||
{
|
||||
BackgroundColor = new(0, 255, 0);
|
||||
}
|
||||
|
||||
public override string Format => "mov";
|
||||
|
||||
public override string Suffix => ".mov";
|
||||
|
||||
/// <summary>
|
||||
/// 编码器
|
||||
/// </summary>
|
||||
public string Codec { get; set; } = "prores_ks";
|
||||
|
||||
/// <summary>
|
||||
/// 预设
|
||||
/// </summary>
|
||||
public string Profile { get; set; } = "auto";
|
||||
|
||||
/// <summary>
|
||||
/// 像素格式
|
||||
/// </summary>
|
||||
public string PixelFormat { get; set; } = "yuva444p10le";
|
||||
|
||||
public override string FileNameNoteSuffix => $"{Codec}_{Profile}_{PixelFormat}";
|
||||
|
||||
public override void SetOutputOptions(FFMpegArgumentOptions options)
|
||||
{
|
||||
base.SetOutputOptions(options);
|
||||
options.WithFastStart().WithVideoCodec(Codec).WithCustomArgument($"-profile {Profile}").ForcePixelFormat(PixelFormat);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,49 +0,0 @@
|
||||
using FFMpegCore;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace SpineViewer.Exporter
|
||||
{
|
||||
/// <summary>
|
||||
/// MP4 导出参数
|
||||
/// </summary>
|
||||
public class Mp4Exporter : FFmpegVideoExporter
|
||||
{
|
||||
public Mp4Exporter()
|
||||
{
|
||||
BackgroundColor = new(0, 255, 0);
|
||||
}
|
||||
|
||||
public override string Format => "mp4";
|
||||
|
||||
public override string Suffix => ".mp4";
|
||||
|
||||
/// <summary>
|
||||
/// 编码器
|
||||
/// </summary>
|
||||
public string Codec { get; set; } = "libx264";
|
||||
|
||||
/// <summary>
|
||||
/// CRF
|
||||
/// </summary>
|
||||
public int CRF { get => crf; set => crf = Math.Clamp(value, 0, 63); }
|
||||
private int crf = 23;
|
||||
|
||||
/// <summary>
|
||||
/// 像素格式
|
||||
/// </summary>
|
||||
public string PixelFormat { get; set; } = "yuv444p";
|
||||
|
||||
public override string FileNameNoteSuffix => $"{Codec}_{CRF}_{PixelFormat}";
|
||||
|
||||
public override void SetOutputOptions(FFMpegArgumentOptions options)
|
||||
{
|
||||
base.SetOutputOptions(options);
|
||||
options.WithFastStart().WithVideoCodec(Codec).WithConstantRateFactor(CRF).ForcePixelFormat(PixelFormat);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,50 +0,0 @@
|
||||
using FFMpegCore;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace SpineViewer.Exporter
|
||||
{
|
||||
/// <summary>
|
||||
/// WebM 导出参数
|
||||
/// </summary>
|
||||
public class WebmExporter : FFmpegVideoExporter
|
||||
{
|
||||
public WebmExporter()
|
||||
{
|
||||
// 默认用透明黑背景
|
||||
BackgroundColor = new(0, 0, 0, 0);
|
||||
}
|
||||
|
||||
public override string Format => "webm";
|
||||
|
||||
public override string Suffix => ".webm";
|
||||
|
||||
/// <summary>
|
||||
/// 编码器
|
||||
/// </summary>
|
||||
public string Codec { get; set; } = "libvpx-vp9";
|
||||
|
||||
/// <summary>
|
||||
/// CRF
|
||||
/// </summary>
|
||||
public int CRF { get => crf; set => crf = Math.Clamp(value, 0, 63); }
|
||||
private int crf = 23;
|
||||
|
||||
/// <summary>
|
||||
/// 像素格式
|
||||
/// </summary>
|
||||
public string PixelFormat { get; set; } = "yuva420p";
|
||||
|
||||
public override string FileNameNoteSuffix => $"{Codec}_{CRF}_{PixelFormat}";
|
||||
|
||||
public override void SetOutputOptions(FFMpegArgumentOptions options)
|
||||
{
|
||||
base.SetOutputOptions(options);
|
||||
options.WithVideoCodec(Codec).WithConstantRateFactor(CRF).ForcePixelFormat(PixelFormat);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,60 +0,0 @@
|
||||
using FFMpegCore;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace SpineViewer.Exporter
|
||||
{
|
||||
/// <summary>
|
||||
/// MP4 导出参数
|
||||
/// </summary>
|
||||
public class WebpExporter : FFmpegVideoExporter
|
||||
{
|
||||
public WebpExporter()
|
||||
{
|
||||
FPS = 24;
|
||||
}
|
||||
|
||||
public override string Format => "webp";
|
||||
|
||||
public override string Suffix => ".webp";
|
||||
|
||||
/// <summary>
|
||||
/// 编码器
|
||||
/// </summary>
|
||||
public string Codec { get; set; } = "libwebp_anim";
|
||||
|
||||
/// <summary>
|
||||
/// 是否无损
|
||||
/// </summary>
|
||||
public bool Lossless { get; set; } = false;
|
||||
|
||||
/// <summary>
|
||||
/// 质量
|
||||
/// </summary>
|
||||
public int Quality { get => quality; set => quality = Math.Clamp(value, 0, 100); }
|
||||
private int quality = 75;
|
||||
|
||||
/// <summary>
|
||||
/// 像素格式
|
||||
/// </summary>
|
||||
public string PixelFormat { get; set; } = "yuva420p";
|
||||
|
||||
/// <summary>
|
||||
/// 循环次数, 0 无限循环, 取值范围 [0, 65535]
|
||||
/// </summary>
|
||||
public int Loop { get => loop; set => loop = Math.Clamp(value, 0, 65535); }
|
||||
private int loop = 0;
|
||||
|
||||
public override string FileNameNoteSuffix => $"{Codec}_{Quality}_{PixelFormat}";
|
||||
|
||||
public override void SetOutputOptions(FFMpegArgumentOptions options)
|
||||
{
|
||||
base.SetOutputOptions(options);
|
||||
options.WithVideoCodec(Codec).WithCustomArgument($"-lossless {(Lossless ? 1 : 0)} -quality {Quality} -loop {Loop}");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,49 +0,0 @@
|
||||
using SpineViewer.Natives;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using System.Data;
|
||||
using System.Drawing;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows.Forms;
|
||||
|
||||
namespace SpineViewer
|
||||
{
|
||||
public partial class PetForm: Form
|
||||
{
|
||||
public PetForm()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
|
||||
protected override CreateParams CreateParams
|
||||
{
|
||||
get
|
||||
{
|
||||
//var style = Win32.GetWindowLong(hWnd, Win32.GWL_STYLE) | Win32.WS_POPUP;
|
||||
//var exStyle = Win32.GetWindowLong(hWnd, Win32.GWL_EXSTYLE) | Win32.WS_EX_LAYERED | Win32.WS_EX_TOOLWINDOW | Win32.WS_EX_TOPMOST;
|
||||
//Win32.SetWindowLong(hWnd, Win32.GWL_STYLE, style);
|
||||
//Win32.SetWindowLong(hWnd, Win32.GWL_EXSTYLE, exStyle);
|
||||
//Win32.SetLayeredWindowAttributes(hWnd, crKey, 255, Win32.LWA_COLORKEY | Win32.LWA_ALPHA);
|
||||
//Win32.SetWindowPos(hWnd, Win32.HWND_TOPMOST, 0, 0, 0, 0, Win32.SWP_NOMOVE | Win32.SWP_NOSIZE);
|
||||
var cp = base.CreateParams;
|
||||
cp.ExStyle = Win32.WS_EX_LAYERED | Win32.WS_EX_TOPMOST;
|
||||
cp.Style = Win32.WS_POPUP;
|
||||
//cp.ExStyle |= Win32.WS_EX_LAYERED | Win32.WS_EX_TOOLWINDOW | Win32.WS_EX_TOPMOST;
|
||||
return cp;
|
||||
}
|
||||
}
|
||||
|
||||
protected override void OnPaint(PaintEventArgs e)
|
||||
{
|
||||
;
|
||||
}
|
||||
|
||||
protected override void OnPaintBackground(PaintEventArgs e)
|
||||
{
|
||||
;
|
||||
}
|
||||
}
|
||||
}
|
||||
51
SpineViewer/Forms/SpinePreviewFullScreenForm.Designer.cs
generated
Normal file
51
SpineViewer/Forms/SpinePreviewFullScreenForm.Designer.cs
generated
Normal file
@@ -0,0 +1,51 @@
|
||||
namespace SpineViewer.Forms
|
||||
{
|
||||
partial class SpinePreviewFullScreenForm
|
||||
{
|
||||
/// <summary>
|
||||
/// Required designer variable.
|
||||
/// </summary>
|
||||
private System.ComponentModel.IContainer components = null;
|
||||
|
||||
/// <summary>
|
||||
/// Clean up any resources being used.
|
||||
/// </summary>
|
||||
/// <param name="disposing">true if managed resources should be disposed; otherwise, false.</param>
|
||||
protected override void Dispose(bool disposing)
|
||||
{
|
||||
if (disposing && (components != null))
|
||||
{
|
||||
components.Dispose();
|
||||
}
|
||||
base.Dispose(disposing);
|
||||
}
|
||||
|
||||
#region Windows Form Designer generated code
|
||||
|
||||
/// <summary>
|
||||
/// Required method for Designer support - do not modify
|
||||
/// the contents of this method with the code editor.
|
||||
/// </summary>
|
||||
private void InitializeComponent()
|
||||
{
|
||||
SuspendLayout();
|
||||
//
|
||||
// SpinePreviewFullScreenForm
|
||||
//
|
||||
AutoScaleMode = AutoScaleMode.None;
|
||||
ClientSize = new Size(512, 512);
|
||||
ControlBox = false;
|
||||
FormBorderStyle = FormBorderStyle.None;
|
||||
MaximizeBox = false;
|
||||
MinimizeBox = false;
|
||||
Name = "SpinePreviewFullScreenForm";
|
||||
ShowIcon = false;
|
||||
ShowInTaskbar = false;
|
||||
StartPosition = FormStartPosition.Manual;
|
||||
TopMost = true;
|
||||
ResumeLayout(false);
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
24
SpineViewer/Forms/SpinePreviewFullScreenForm.cs
Normal file
24
SpineViewer/Forms/SpinePreviewFullScreenForm.cs
Normal file
@@ -0,0 +1,24 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using System.ComponentModel.Design;
|
||||
using System.Data;
|
||||
using System.Drawing;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows.Forms;
|
||||
|
||||
namespace SpineViewer.Forms
|
||||
{
|
||||
[ToolboxItem(true)]
|
||||
[Designer(typeof(ComponentDesigner), typeof(IDesigner))]
|
||||
[DesignTimeVisible(true)]
|
||||
public partial class SpinePreviewFullScreenForm: Form
|
||||
{
|
||||
public SpinePreviewFullScreenForm()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
}
|
||||
}
|
||||
255
SpineViewer/Forms/SpineViewerForm.Designer.cs
generated
255
SpineViewer/Forms/SpineViewerForm.Designer.cs
generated
@@ -38,11 +38,16 @@
|
||||
toolStripMenuItem_Export = new ToolStripMenuItem();
|
||||
toolStripMenuItem_ExportFrame = new ToolStripMenuItem();
|
||||
toolStripMenuItem_ExportFrameSequence = new ToolStripMenuItem();
|
||||
toolStripSeparator4 = new ToolStripSeparator();
|
||||
toolStripMenuItem_ExportGif = new ToolStripMenuItem();
|
||||
toolStripMenuItem_ExportWebp = new ToolStripMenuItem();
|
||||
toolStripMenuItem_ExportAvif = new ToolStripMenuItem();
|
||||
toolStripSeparator5 = new ToolStripSeparator();
|
||||
toolStripMenuItem_ExportMp4 = new ToolStripMenuItem();
|
||||
toolStripMenuItem_ExportWebm = new ToolStripMenuItem();
|
||||
toolStripMenuItem_ExportMkv = new ToolStripMenuItem();
|
||||
toolStripMenuItem_ExportMov = new ToolStripMenuItem();
|
||||
toolStripSeparator6 = new ToolStripSeparator();
|
||||
toolStripMenuItem_ExportCustom = new ToolStripMenuItem();
|
||||
toolStripSeparator2 = new ToolStripSeparator();
|
||||
toolStripMenuItem_Exit = new ToolStripMenuItem();
|
||||
@@ -54,28 +59,24 @@
|
||||
toolStripMenuItem_Diagnostics = new ToolStripMenuItem();
|
||||
toolStripSeparator3 = new ToolStripSeparator();
|
||||
toolStripMenuItem_About = new ToolStripMenuItem();
|
||||
toolStripMenuItem_Experiment = new ToolStripMenuItem();
|
||||
toolStripMenuItem_DesktopProjection = new ToolStripMenuItem();
|
||||
rtbLog = new RichTextBox();
|
||||
splitContainer_MainForm = new SplitContainer();
|
||||
splitContainer_Functional = new SplitContainer();
|
||||
splitContainer_Information = new SplitContainer();
|
||||
groupBox_SkelList = new GroupBox();
|
||||
spineListView = new SpineViewer.Controls.SpineListView();
|
||||
spinePropertyGrid = new SpineViewer.Controls.SpinePropertyGrid();
|
||||
tabControl_Config = new TabControl();
|
||||
tabPage_Previewer = new TabPage();
|
||||
spineViewPropertyGrid = new SpineViewer.Controls.SpineViewPropertyGrid();
|
||||
splitContainer_Config = new SplitContainer();
|
||||
groupBox_PreviewConfig = new GroupBox();
|
||||
propertyGrid_Previewer = new PropertyGrid();
|
||||
tabPage_SpineProperty = new TabPage();
|
||||
groupBox_SkelConfig = new GroupBox();
|
||||
groupBox_Preview = new GroupBox();
|
||||
spinePreviewer = new SpineViewer.Controls.SpinePreviewer();
|
||||
spinePreviewPanel = new SpineViewer.Controls.SpinePreviewPanel();
|
||||
panel_MainForm = new Panel();
|
||||
toolTip = new ToolTip(components);
|
||||
toolStripSeparator4 = new ToolStripSeparator();
|
||||
toolStripSeparator5 = new ToolStripSeparator();
|
||||
toolStripSeparator6 = new ToolStripSeparator();
|
||||
toolStripMenuItem_ExportWebp = new ToolStripMenuItem();
|
||||
toolStripMenuItem_ExportAvif = new ToolStripMenuItem();
|
||||
toolStripMenuItem_Debug = new ToolStripMenuItem();
|
||||
menuStrip.SuspendLayout();
|
||||
((System.ComponentModel.ISupportInitialize)splitContainer_MainForm).BeginInit();
|
||||
splitContainer_MainForm.Panel1.SuspendLayout();
|
||||
@@ -90,10 +91,11 @@
|
||||
splitContainer_Information.Panel2.SuspendLayout();
|
||||
splitContainer_Information.SuspendLayout();
|
||||
groupBox_SkelList.SuspendLayout();
|
||||
tabControl_Config.SuspendLayout();
|
||||
tabPage_Previewer.SuspendLayout();
|
||||
((System.ComponentModel.ISupportInitialize)splitContainer_Config).BeginInit();
|
||||
splitContainer_Config.Panel1.SuspendLayout();
|
||||
splitContainer_Config.Panel2.SuspendLayout();
|
||||
splitContainer_Config.SuspendLayout();
|
||||
groupBox_PreviewConfig.SuspendLayout();
|
||||
tabPage_SpineProperty.SuspendLayout();
|
||||
groupBox_SkelConfig.SuspendLayout();
|
||||
groupBox_Preview.SuspendLayout();
|
||||
panel_MainForm.SuspendLayout();
|
||||
@@ -103,7 +105,7 @@
|
||||
//
|
||||
menuStrip.BackColor = SystemColors.Control;
|
||||
menuStrip.ImageScalingSize = new Size(24, 24);
|
||||
menuStrip.Items.AddRange(new ToolStripItem[] { toolStripMenuItem_File, toolStripMenuItem_Tool, toolStripMenuItem_Download, toolStripMenuItem_Help });
|
||||
menuStrip.Items.AddRange(new ToolStripItem[] { toolStripMenuItem_File, toolStripMenuItem_Tool, toolStripMenuItem_Download, toolStripMenuItem_Help, toolStripMenuItem_Experiment });
|
||||
menuStrip.Location = new Point(0, 0);
|
||||
menuStrip.Name = "menuStrip";
|
||||
menuStrip.Size = new Size(1778, 32);
|
||||
@@ -121,27 +123,27 @@
|
||||
//
|
||||
toolStripMenuItem_Open.Name = "toolStripMenuItem_Open";
|
||||
toolStripMenuItem_Open.ShortcutKeys = Keys.Control | Keys.O;
|
||||
toolStripMenuItem_Open.Size = new Size(270, 34);
|
||||
toolStripMenuItem_Open.Size = new Size(254, 34);
|
||||
toolStripMenuItem_Open.Text = "打开(&O)...";
|
||||
toolStripMenuItem_Open.Click += toolStripMenuItem_Open_Click;
|
||||
//
|
||||
// toolStripMenuItem_BatchOpen
|
||||
//
|
||||
toolStripMenuItem_BatchOpen.Name = "toolStripMenuItem_BatchOpen";
|
||||
toolStripMenuItem_BatchOpen.Size = new Size(270, 34);
|
||||
toolStripMenuItem_BatchOpen.Size = new Size(254, 34);
|
||||
toolStripMenuItem_BatchOpen.Text = "批量打开(&B)...";
|
||||
toolStripMenuItem_BatchOpen.Click += toolStripMenuItem_BatchOpen_Click;
|
||||
//
|
||||
// toolStripSeparator1
|
||||
//
|
||||
toolStripSeparator1.Name = "toolStripSeparator1";
|
||||
toolStripSeparator1.Size = new Size(267, 6);
|
||||
toolStripSeparator1.Size = new Size(251, 6);
|
||||
//
|
||||
// toolStripMenuItem_Export
|
||||
//
|
||||
toolStripMenuItem_Export.DropDownItems.AddRange(new ToolStripItem[] { toolStripMenuItem_ExportFrame, toolStripMenuItem_ExportFrameSequence, toolStripSeparator4, toolStripMenuItem_ExportGif, toolStripMenuItem_ExportWebp, toolStripMenuItem_ExportAvif, toolStripSeparator5, toolStripMenuItem_ExportMp4, toolStripMenuItem_ExportWebm, toolStripMenuItem_ExportMkv, toolStripMenuItem_ExportMov, toolStripSeparator6, toolStripMenuItem_ExportCustom });
|
||||
toolStripMenuItem_Export.Name = "toolStripMenuItem_Export";
|
||||
toolStripMenuItem_Export.Size = new Size(270, 34);
|
||||
toolStripMenuItem_Export.Size = new Size(254, 34);
|
||||
toolStripMenuItem_Export.Text = "导出(&E)";
|
||||
//
|
||||
// toolStripMenuItem_ExportFrame
|
||||
@@ -158,6 +160,11 @@
|
||||
toolStripMenuItem_ExportFrameSequence.Text = "帧序列...";
|
||||
toolStripMenuItem_ExportFrameSequence.Click += toolStripMenuItem_ExportFrameSequence_Click;
|
||||
//
|
||||
// toolStripSeparator4
|
||||
//
|
||||
toolStripSeparator4.Name = "toolStripSeparator4";
|
||||
toolStripSeparator4.Size = new Size(285, 6);
|
||||
//
|
||||
// toolStripMenuItem_ExportGif
|
||||
//
|
||||
toolStripMenuItem_ExportGif.Name = "toolStripMenuItem_ExportGif";
|
||||
@@ -165,6 +172,25 @@
|
||||
toolStripMenuItem_ExportGif.Text = "GIF...";
|
||||
toolStripMenuItem_ExportGif.Click += toolStripMenuItem_ExportGif_Click;
|
||||
//
|
||||
// toolStripMenuItem_ExportWebp
|
||||
//
|
||||
toolStripMenuItem_ExportWebp.Name = "toolStripMenuItem_ExportWebp";
|
||||
toolStripMenuItem_ExportWebp.Size = new Size(288, 34);
|
||||
toolStripMenuItem_ExportWebp.Text = "WebP...";
|
||||
toolStripMenuItem_ExportWebp.Click += toolStripMenuItem_ExportWebp_Click;
|
||||
//
|
||||
// toolStripMenuItem_ExportAvif
|
||||
//
|
||||
toolStripMenuItem_ExportAvif.Name = "toolStripMenuItem_ExportAvif";
|
||||
toolStripMenuItem_ExportAvif.Size = new Size(288, 34);
|
||||
toolStripMenuItem_ExportAvif.Text = "AVIF...";
|
||||
toolStripMenuItem_ExportAvif.Click += toolStripMenuItem_ExportAvif_Click;
|
||||
//
|
||||
// toolStripSeparator5
|
||||
//
|
||||
toolStripSeparator5.Name = "toolStripSeparator5";
|
||||
toolStripSeparator5.Size = new Size(285, 6);
|
||||
//
|
||||
// toolStripMenuItem_ExportMp4
|
||||
//
|
||||
toolStripMenuItem_ExportMp4.Name = "toolStripMenuItem_ExportMp4";
|
||||
@@ -193,6 +219,11 @@
|
||||
toolStripMenuItem_ExportMov.Text = "MOV...";
|
||||
toolStripMenuItem_ExportMov.Click += toolStripMenuItem_ExportMov_Click;
|
||||
//
|
||||
// toolStripSeparator6
|
||||
//
|
||||
toolStripSeparator6.Name = "toolStripSeparator6";
|
||||
toolStripSeparator6.Size = new Size(285, 6);
|
||||
//
|
||||
// toolStripMenuItem_ExportCustom
|
||||
//
|
||||
toolStripMenuItem_ExportCustom.Name = "toolStripMenuItem_ExportCustom";
|
||||
@@ -203,13 +234,13 @@
|
||||
// toolStripSeparator2
|
||||
//
|
||||
toolStripSeparator2.Name = "toolStripSeparator2";
|
||||
toolStripSeparator2.Size = new Size(267, 6);
|
||||
toolStripSeparator2.Size = new Size(251, 6);
|
||||
//
|
||||
// toolStripMenuItem_Exit
|
||||
//
|
||||
toolStripMenuItem_Exit.Name = "toolStripMenuItem_Exit";
|
||||
toolStripMenuItem_Exit.ShortcutKeys = Keys.Alt | Keys.F4;
|
||||
toolStripMenuItem_Exit.Size = new Size(270, 34);
|
||||
toolStripMenuItem_Exit.Size = new Size(254, 34);
|
||||
toolStripMenuItem_Exit.Text = "退出(&X)";
|
||||
toolStripMenuItem_Exit.Click += toolStripMenuItem_Exit_Click;
|
||||
//
|
||||
@@ -243,7 +274,7 @@
|
||||
//
|
||||
// toolStripMenuItem_Help
|
||||
//
|
||||
toolStripMenuItem_Help.DropDownItems.AddRange(new ToolStripItem[] { toolStripMenuItem_Diagnostics, toolStripSeparator3, toolStripMenuItem_About });
|
||||
toolStripMenuItem_Help.DropDownItems.AddRange(new ToolStripItem[] { toolStripMenuItem_Diagnostics, toolStripSeparator3, toolStripMenuItem_About, toolStripMenuItem_Debug });
|
||||
toolStripMenuItem_Help.Name = "toolStripMenuItem_Help";
|
||||
toolStripMenuItem_Help.Size = new Size(88, 28);
|
||||
toolStripMenuItem_Help.Text = "帮助(&H)";
|
||||
@@ -251,22 +282,36 @@
|
||||
// toolStripMenuItem_Diagnostics
|
||||
//
|
||||
toolStripMenuItem_Diagnostics.Name = "toolStripMenuItem_Diagnostics";
|
||||
toolStripMenuItem_Diagnostics.Size = new Size(208, 34);
|
||||
toolStripMenuItem_Diagnostics.Size = new Size(270, 34);
|
||||
toolStripMenuItem_Diagnostics.Text = "诊断信息(&D)";
|
||||
toolStripMenuItem_Diagnostics.Click += toolStripMenuItem_Diagnostics_Click;
|
||||
//
|
||||
// toolStripSeparator3
|
||||
//
|
||||
toolStripSeparator3.Name = "toolStripSeparator3";
|
||||
toolStripSeparator3.Size = new Size(205, 6);
|
||||
toolStripSeparator3.Size = new Size(267, 6);
|
||||
//
|
||||
// toolStripMenuItem_About
|
||||
//
|
||||
toolStripMenuItem_About.Name = "toolStripMenuItem_About";
|
||||
toolStripMenuItem_About.Size = new Size(208, 34);
|
||||
toolStripMenuItem_About.Size = new Size(270, 34);
|
||||
toolStripMenuItem_About.Text = "关于(&A)";
|
||||
toolStripMenuItem_About.Click += toolStripMenuItem_About_Click;
|
||||
//
|
||||
// toolStripMenuItem_Experiment
|
||||
//
|
||||
toolStripMenuItem_Experiment.DropDownItems.AddRange(new ToolStripItem[] { toolStripMenuItem_DesktopProjection });
|
||||
toolStripMenuItem_Experiment.Name = "toolStripMenuItem_Experiment";
|
||||
toolStripMenuItem_Experiment.Size = new Size(138, 28);
|
||||
toolStripMenuItem_Experiment.Text = "实验性功能(&E)";
|
||||
//
|
||||
// toolStripMenuItem_DesktopProjection
|
||||
//
|
||||
toolStripMenuItem_DesktopProjection.Name = "toolStripMenuItem_DesktopProjection";
|
||||
toolStripMenuItem_DesktopProjection.Size = new Size(182, 34);
|
||||
toolStripMenuItem_DesktopProjection.Text = "桌面投影";
|
||||
toolStripMenuItem_DesktopProjection.Click += toolStripMenuItem_DesktopProjection_Click;
|
||||
//
|
||||
// rtbLog
|
||||
//
|
||||
rtbLog.BackColor = SystemColors.Window;
|
||||
@@ -277,7 +322,7 @@
|
||||
rtbLog.Margin = new Padding(3, 2, 3, 2);
|
||||
rtbLog.Name = "rtbLog";
|
||||
rtbLog.ReadOnly = true;
|
||||
rtbLog.Size = new Size(1758, 172);
|
||||
rtbLog.Size = new Size(1758, 154);
|
||||
rtbLog.TabIndex = 0;
|
||||
rtbLog.Text = "";
|
||||
rtbLog.WordWrap = false;
|
||||
@@ -301,7 +346,7 @@
|
||||
splitContainer_MainForm.Panel2.Controls.Add(rtbLog);
|
||||
splitContainer_MainForm.Panel2.Cursor = Cursors.Default;
|
||||
splitContainer_MainForm.Size = new Size(1758, 1097);
|
||||
splitContainer_MainForm.SplitterDistance = 917;
|
||||
splitContainer_MainForm.SplitterDistance = 935;
|
||||
splitContainer_MainForm.SplitterWidth = 8;
|
||||
splitContainer_MainForm.TabIndex = 3;
|
||||
splitContainer_MainForm.TabStop = false;
|
||||
@@ -325,8 +370,8 @@
|
||||
//
|
||||
splitContainer_Functional.Panel2.Controls.Add(groupBox_Preview);
|
||||
splitContainer_Functional.Panel2.Cursor = Cursors.Default;
|
||||
splitContainer_Functional.Size = new Size(1758, 917);
|
||||
splitContainer_Functional.SplitterDistance = 759;
|
||||
splitContainer_Functional.Size = new Size(1758, 935);
|
||||
splitContainer_Functional.SplitterDistance = 788;
|
||||
splitContainer_Functional.SplitterWidth = 8;
|
||||
splitContainer_Functional.TabIndex = 2;
|
||||
splitContainer_Functional.TabStop = false;
|
||||
@@ -347,10 +392,10 @@
|
||||
//
|
||||
// splitContainer_Information.Panel2
|
||||
//
|
||||
splitContainer_Information.Panel2.Controls.Add(tabControl_Config);
|
||||
splitContainer_Information.Panel2.Controls.Add(splitContainer_Config);
|
||||
splitContainer_Information.Panel2.Cursor = Cursors.Default;
|
||||
splitContainer_Information.Size = new Size(759, 917);
|
||||
splitContainer_Information.SplitterDistance = 354;
|
||||
splitContainer_Information.Size = new Size(788, 935);
|
||||
splitContainer_Information.SplitterDistance = 351;
|
||||
splitContainer_Information.SplitterWidth = 8;
|
||||
splitContainer_Information.TabIndex = 1;
|
||||
splitContainer_Information.TabStop = false;
|
||||
@@ -363,7 +408,7 @@
|
||||
groupBox_SkelList.Dock = DockStyle.Fill;
|
||||
groupBox_SkelList.Location = new Point(0, 0);
|
||||
groupBox_SkelList.Name = "groupBox_SkelList";
|
||||
groupBox_SkelList.Size = new Size(354, 917);
|
||||
groupBox_SkelList.Size = new Size(351, 935);
|
||||
groupBox_SkelList.TabIndex = 0;
|
||||
groupBox_SkelList.TabStop = false;
|
||||
groupBox_SkelList.Text = "模型列表";
|
||||
@@ -373,42 +418,36 @@
|
||||
spineListView.Dock = DockStyle.Fill;
|
||||
spineListView.Location = new Point(3, 26);
|
||||
spineListView.Name = "spineListView";
|
||||
spineListView.Size = new Size(348, 888);
|
||||
spineListView.SpinePropertyGrid = spinePropertyGrid;
|
||||
spineListView.Size = new Size(345, 906);
|
||||
spineListView.SpinePropertyGrid = spineViewPropertyGrid;
|
||||
spineListView.TabIndex = 0;
|
||||
//
|
||||
// spinePropertyGrid
|
||||
// spineViewPropertyGrid
|
||||
//
|
||||
spinePropertyGrid.Dock = DockStyle.Fill;
|
||||
spinePropertyGrid.Location = new Point(3, 26);
|
||||
spinePropertyGrid.Name = "spinePropertyGrid";
|
||||
spinePropertyGrid.Size = new Size(383, 849);
|
||||
spinePropertyGrid.TabIndex = 0;
|
||||
spineViewPropertyGrid.Dock = DockStyle.Fill;
|
||||
spineViewPropertyGrid.Location = new Point(3, 26);
|
||||
spineViewPropertyGrid.Name = "spineViewPropertyGrid";
|
||||
spineViewPropertyGrid.Size = new Size(423, 580);
|
||||
spineViewPropertyGrid.TabIndex = 0;
|
||||
//
|
||||
// tabControl_Config
|
||||
// splitContainer_Config
|
||||
//
|
||||
tabControl_Config.Alignment = TabAlignment.Bottom;
|
||||
tabControl_Config.Controls.Add(tabPage_Previewer);
|
||||
tabControl_Config.Controls.Add(tabPage_SpineProperty);
|
||||
tabControl_Config.Dock = DockStyle.Fill;
|
||||
tabControl_Config.ItemSize = new Size(100, 35);
|
||||
tabControl_Config.Location = new Point(0, 0);
|
||||
tabControl_Config.Multiline = true;
|
||||
tabControl_Config.Name = "tabControl_Config";
|
||||
tabControl_Config.Padding = new Point(0, 0);
|
||||
tabControl_Config.SelectedIndex = 0;
|
||||
tabControl_Config.Size = new Size(397, 917);
|
||||
tabControl_Config.TabIndex = 0;
|
||||
splitContainer_Config.Dock = DockStyle.Fill;
|
||||
splitContainer_Config.Location = new Point(0, 0);
|
||||
splitContainer_Config.Name = "splitContainer_Config";
|
||||
splitContainer_Config.Orientation = Orientation.Horizontal;
|
||||
//
|
||||
// tabPage_Previewer
|
||||
// splitContainer_Config.Panel1
|
||||
//
|
||||
tabPage_Previewer.Controls.Add(groupBox_PreviewConfig);
|
||||
tabPage_Previewer.Location = new Point(4, 4);
|
||||
tabPage_Previewer.Margin = new Padding(0);
|
||||
tabPage_Previewer.Name = "tabPage_Previewer";
|
||||
tabPage_Previewer.Size = new Size(389, 874);
|
||||
tabPage_Previewer.TabIndex = 0;
|
||||
tabPage_Previewer.Text = "画面参数";
|
||||
splitContainer_Config.Panel1.Controls.Add(groupBox_PreviewConfig);
|
||||
//
|
||||
// splitContainer_Config.Panel2
|
||||
//
|
||||
splitContainer_Config.Panel2.Controls.Add(groupBox_SkelConfig);
|
||||
splitContainer_Config.Size = new Size(429, 935);
|
||||
splitContainer_Config.SplitterDistance = 318;
|
||||
splitContainer_Config.SplitterWidth = 8;
|
||||
splitContainer_Config.TabIndex = 0;
|
||||
//
|
||||
// groupBox_PreviewConfig
|
||||
//
|
||||
@@ -417,7 +456,7 @@
|
||||
groupBox_PreviewConfig.Location = new Point(0, 0);
|
||||
groupBox_PreviewConfig.Margin = new Padding(0);
|
||||
groupBox_PreviewConfig.Name = "groupBox_PreviewConfig";
|
||||
groupBox_PreviewConfig.Size = new Size(389, 874);
|
||||
groupBox_PreviewConfig.Size = new Size(429, 318);
|
||||
groupBox_PreviewConfig.TabIndex = 1;
|
||||
groupBox_PreviewConfig.TabStop = false;
|
||||
groupBox_PreviewConfig.Text = "画面参数";
|
||||
@@ -428,54 +467,42 @@
|
||||
propertyGrid_Previewer.HelpVisible = false;
|
||||
propertyGrid_Previewer.Location = new Point(3, 26);
|
||||
propertyGrid_Previewer.Name = "propertyGrid_Previewer";
|
||||
propertyGrid_Previewer.Size = new Size(383, 845);
|
||||
propertyGrid_Previewer.Size = new Size(423, 289);
|
||||
propertyGrid_Previewer.TabIndex = 1;
|
||||
propertyGrid_Previewer.ToolbarVisible = false;
|
||||
propertyGrid_Previewer.PropertyValueChanged += propertyGrid_PropertyValueChanged;
|
||||
//
|
||||
// tabPage_SpineProperty
|
||||
//
|
||||
tabPage_SpineProperty.BackColor = SystemColors.Control;
|
||||
tabPage_SpineProperty.Controls.Add(groupBox_SkelConfig);
|
||||
tabPage_SpineProperty.Location = new Point(4, 4);
|
||||
tabPage_SpineProperty.Margin = new Padding(0);
|
||||
tabPage_SpineProperty.Name = "tabPage_SpineProperty";
|
||||
tabPage_SpineProperty.Size = new Size(389, 878);
|
||||
tabPage_SpineProperty.TabIndex = 1;
|
||||
tabPage_SpineProperty.Text = "模型参数";
|
||||
//
|
||||
// groupBox_SkelConfig
|
||||
//
|
||||
groupBox_SkelConfig.Controls.Add(spinePropertyGrid);
|
||||
groupBox_SkelConfig.Controls.Add(spineViewPropertyGrid);
|
||||
groupBox_SkelConfig.Dock = DockStyle.Fill;
|
||||
groupBox_SkelConfig.Location = new Point(0, 0);
|
||||
groupBox_SkelConfig.Margin = new Padding(0);
|
||||
groupBox_SkelConfig.Name = "groupBox_SkelConfig";
|
||||
groupBox_SkelConfig.Size = new Size(389, 878);
|
||||
groupBox_SkelConfig.Size = new Size(429, 609);
|
||||
groupBox_SkelConfig.TabIndex = 0;
|
||||
groupBox_SkelConfig.TabStop = false;
|
||||
groupBox_SkelConfig.Text = "模型参数";
|
||||
//
|
||||
// groupBox_Preview
|
||||
//
|
||||
groupBox_Preview.Controls.Add(spinePreviewer);
|
||||
groupBox_Preview.Controls.Add(spinePreviewPanel);
|
||||
groupBox_Preview.Dock = DockStyle.Fill;
|
||||
groupBox_Preview.Location = new Point(0, 0);
|
||||
groupBox_Preview.Name = "groupBox_Preview";
|
||||
groupBox_Preview.Size = new Size(991, 917);
|
||||
groupBox_Preview.Size = new Size(962, 935);
|
||||
groupBox_Preview.TabIndex = 1;
|
||||
groupBox_Preview.TabStop = false;
|
||||
groupBox_Preview.Text = "预览画面";
|
||||
//
|
||||
// spinePreviewer
|
||||
// spinePreviewPanel
|
||||
//
|
||||
spinePreviewer.Dock = DockStyle.Fill;
|
||||
spinePreviewer.Location = new Point(3, 26);
|
||||
spinePreviewer.Name = "spinePreviewer";
|
||||
spinePreviewer.PropertyGrid = propertyGrid_Previewer;
|
||||
spinePreviewer.Size = new Size(985, 888);
|
||||
spinePreviewer.SpineListView = spineListView;
|
||||
spinePreviewer.TabIndex = 0;
|
||||
spinePreviewPanel.Dock = DockStyle.Fill;
|
||||
spinePreviewPanel.Location = new Point(3, 26);
|
||||
spinePreviewPanel.Name = "spinePreviewPanel";
|
||||
spinePreviewPanel.PropertyGrid = propertyGrid_Previewer;
|
||||
spinePreviewPanel.Size = new Size(956, 906);
|
||||
spinePreviewPanel.SpineListView = spineListView;
|
||||
spinePreviewPanel.TabIndex = 0;
|
||||
//
|
||||
// panel_MainForm
|
||||
//
|
||||
@@ -491,39 +518,17 @@
|
||||
//
|
||||
toolTip.ShowAlways = true;
|
||||
//
|
||||
// toolStripSeparator4
|
||||
// toolStripMenuItem_Debug
|
||||
//
|
||||
toolStripSeparator4.Name = "toolStripSeparator4";
|
||||
toolStripSeparator4.Size = new Size(285, 6);
|
||||
//
|
||||
// toolStripSeparator5
|
||||
//
|
||||
toolStripSeparator5.Name = "toolStripSeparator5";
|
||||
toolStripSeparator5.Size = new Size(285, 6);
|
||||
//
|
||||
// toolStripSeparator6
|
||||
//
|
||||
toolStripSeparator6.Name = "toolStripSeparator6";
|
||||
toolStripSeparator6.Size = new Size(285, 6);
|
||||
//
|
||||
// toolStripMenuItem_ExportWebp
|
||||
//
|
||||
toolStripMenuItem_ExportWebp.Name = "toolStripMenuItem_ExportWebp";
|
||||
toolStripMenuItem_ExportWebp.Size = new Size(288, 34);
|
||||
toolStripMenuItem_ExportWebp.Text = "WebP...";
|
||||
toolStripMenuItem_ExportWebp.Click += toolStripMenuItem_ExportWebp_Click;
|
||||
//
|
||||
// toolStripMenuItem_ExportAvif
|
||||
//
|
||||
toolStripMenuItem_ExportAvif.Name = "toolStripMenuItem_ExportAvif";
|
||||
toolStripMenuItem_ExportAvif.Size = new Size(288, 34);
|
||||
toolStripMenuItem_ExportAvif.Text = "AVIF...";
|
||||
toolStripMenuItem_ExportAvif.Click += toolStripMenuItem_ExportAvif_Click;
|
||||
toolStripMenuItem_Debug.Name = "toolStripMenuItem_Debug";
|
||||
toolStripMenuItem_Debug.Size = new Size(270, 34);
|
||||
toolStripMenuItem_Debug.Text = "调试";
|
||||
toolStripMenuItem_Debug.Visible = false;
|
||||
//
|
||||
// SpineViewerForm
|
||||
//
|
||||
AutoScaleDimensions = new SizeF(11F, 24F);
|
||||
AutoScaleMode = AutoScaleMode.Font;
|
||||
AutoScaleDimensions = new SizeF(144F, 144F);
|
||||
AutoScaleMode = AutoScaleMode.Dpi;
|
||||
ClientSize = new Size(1778, 1144);
|
||||
Controls.Add(panel_MainForm);
|
||||
Controls.Add(menuStrip);
|
||||
@@ -550,10 +555,11 @@
|
||||
((System.ComponentModel.ISupportInitialize)splitContainer_Information).EndInit();
|
||||
splitContainer_Information.ResumeLayout(false);
|
||||
groupBox_SkelList.ResumeLayout(false);
|
||||
tabControl_Config.ResumeLayout(false);
|
||||
tabPage_Previewer.ResumeLayout(false);
|
||||
splitContainer_Config.Panel1.ResumeLayout(false);
|
||||
splitContainer_Config.Panel2.ResumeLayout(false);
|
||||
((System.ComponentModel.ISupportInitialize)splitContainer_Config).EndInit();
|
||||
splitContainer_Config.ResumeLayout(false);
|
||||
groupBox_PreviewConfig.ResumeLayout(false);
|
||||
tabPage_SpineProperty.ResumeLayout(false);
|
||||
groupBox_SkelConfig.ResumeLayout(false);
|
||||
groupBox_Preview.ResumeLayout(false);
|
||||
panel_MainForm.ResumeLayout(false);
|
||||
@@ -584,7 +590,7 @@
|
||||
private ToolTip toolTip;
|
||||
private Controls.SpineListView spineListView;
|
||||
private PropertyGrid propertyGrid_Previewer;
|
||||
private Controls.SpinePreviewer spinePreviewer;
|
||||
private Controls.SpinePreviewPanel spinePreviewPanel;
|
||||
private ToolStripMenuItem toolStripMenuItem_Diagnostics;
|
||||
private ToolStripSeparator toolStripSeparator3;
|
||||
private ToolStripMenuItem toolStripMenuItem_Download;
|
||||
@@ -600,14 +606,15 @@
|
||||
private ToolStripMenuItem toolStripMenuItem_ExportMkv;
|
||||
private ToolStripMenuItem toolStripMenuItem_ExportWebm;
|
||||
private ToolStripMenuItem toolStripMenuItem_ExportCustom;
|
||||
private Controls.SpinePropertyGrid spinePropertyGrid;
|
||||
private TabControl tabControl_Config;
|
||||
private TabPage tabPage_Previewer;
|
||||
private TabPage tabPage_SpineProperty;
|
||||
private Controls.SpineViewPropertyGrid spineViewPropertyGrid;
|
||||
private ToolStripSeparator toolStripSeparator4;
|
||||
private ToolStripMenuItem toolStripMenuItem_ExportWebp;
|
||||
private ToolStripMenuItem toolStripMenuItem_ExportAvif;
|
||||
private ToolStripSeparator toolStripSeparator5;
|
||||
private ToolStripSeparator toolStripSeparator6;
|
||||
private SplitContainer splitContainer_Config;
|
||||
private ToolStripMenuItem toolStripMenuItem_Experiment;
|
||||
private ToolStripMenuItem toolStripMenuItem_DesktopProjection;
|
||||
private ToolStripMenuItem toolStripMenuItem_Debug;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,11 +2,9 @@
|
||||
using SpineViewer.Spine;
|
||||
using System.ComponentModel;
|
||||
using System.Diagnostics;
|
||||
using SpineViewer.Exporter;
|
||||
using System.Reflection.Metadata;
|
||||
using SpineViewer.PropertyGridWrappers.Exporter;
|
||||
using SpineViewer.Utilities;
|
||||
using SpineViewer.Natives;
|
||||
using SpineViewer.Utils;
|
||||
using SpineViewer.Spine.SpineExporter;
|
||||
|
||||
namespace SpineViewer
|
||||
{
|
||||
@@ -14,7 +12,7 @@ namespace SpineViewer
|
||||
{
|
||||
private readonly Logger logger = LogManager.GetCurrentClassLogger();
|
||||
|
||||
private readonly Dictionary<string, Exporter.Exporter> exporterCache = [];
|
||||
private readonly Dictionary<string, Exporter> exporterCache = [];
|
||||
|
||||
public SpineViewerForm()
|
||||
{
|
||||
@@ -33,6 +31,9 @@ namespace SpineViewer
|
||||
MessagePopup.Warn("Fragment shader 加载失败,预乘Alpha通道属性失效");
|
||||
}
|
||||
|
||||
#if DEBUG
|
||||
toolStripMenuItem_Debug.Visible = true;
|
||||
#endif
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -65,12 +66,12 @@ namespace SpineViewer
|
||||
|
||||
private void MainForm_Load(object sender, EventArgs e)
|
||||
{
|
||||
spinePreviewer.StartRender();
|
||||
spinePreviewPanel.StartRender();
|
||||
}
|
||||
|
||||
private void MainForm_FormClosing(object sender, FormClosingEventArgs e)
|
||||
{
|
||||
spinePreviewer.StopRender();
|
||||
spinePreviewPanel.StopRender();
|
||||
}
|
||||
|
||||
private void toolStripMenuItem_Open_Click(object sender, EventArgs e)
|
||||
@@ -87,18 +88,19 @@ namespace SpineViewer
|
||||
|
||||
private void toolStripMenuItem_ExportFrame_Click(object sender, EventArgs e)
|
||||
{
|
||||
if (spinePreviewer.IsUpdating && MessagePopup.Quest("画面仍在更新,建议手动暂停画面后导出固定的一帧,是否继续?") != DialogResult.OK)
|
||||
if (spinePreviewPanel.IsUpdating && MessagePopup.Quest("画面仍在更新,建议手动暂停画面后导出固定的一帧,是否继续?") != DialogResult.OK)
|
||||
return;
|
||||
|
||||
var k = nameof(toolStripMenuItem_ExportFrame);
|
||||
if (!exporterCache.ContainsKey(k)) exporterCache[k] = new FrameExporter();
|
||||
|
||||
var exporter = exporterCache[k];
|
||||
exporter.Resolution = spinePreviewer.Resolution;
|
||||
exporter.View = spinePreviewer.GetView();
|
||||
exporter.RenderSelectedOnly = spinePreviewer.RenderSelectedOnly;
|
||||
using var view = spinePreviewPanel.GetView();
|
||||
exporter.Resolution = spinePreviewPanel.Resolution;
|
||||
exporter.PreviewerView = view;
|
||||
exporter.RenderSelectedOnly = spinePreviewPanel.RenderSelectedOnly;
|
||||
|
||||
var exportDialog = new Dialogs.ExportDialog(new FrameExporterWrapper((FrameExporter)exporter));
|
||||
var exportDialog = new Dialogs.ExportDialog(new FrameExporterProperty((FrameExporter)exporter));
|
||||
if (exportDialog.ShowDialog() != DialogResult.OK)
|
||||
return;
|
||||
|
||||
@@ -114,11 +116,12 @@ namespace SpineViewer
|
||||
if (!exporterCache.ContainsKey(k)) exporterCache[k] = new FrameSequenceExporter();
|
||||
|
||||
var exporter = exporterCache[k];
|
||||
exporter.Resolution = spinePreviewer.Resolution;
|
||||
exporter.View = spinePreviewer.GetView();
|
||||
exporter.RenderSelectedOnly = spinePreviewer.RenderSelectedOnly;
|
||||
using var view = spinePreviewPanel.GetView();
|
||||
exporter.Resolution = spinePreviewPanel.Resolution;
|
||||
exporter.PreviewerView = view;
|
||||
exporter.RenderSelectedOnly = spinePreviewPanel.RenderSelectedOnly;
|
||||
|
||||
var exportDialog = new Dialogs.ExportDialog(new FrameSequenceExporterWrapper((FrameSequenceExporter)exporter));
|
||||
var exportDialog = new Dialogs.ExportDialog(new FrameSequenceExporterProperty((FrameSequenceExporter)exporter));
|
||||
if (exportDialog.ShowDialog() != DialogResult.OK)
|
||||
return;
|
||||
|
||||
@@ -134,11 +137,12 @@ namespace SpineViewer
|
||||
if (!exporterCache.ContainsKey(k)) exporterCache[k] = new GifExporter();
|
||||
|
||||
var exporter = exporterCache[k];
|
||||
exporter.Resolution = spinePreviewer.Resolution;
|
||||
exporter.View = spinePreviewer.GetView();
|
||||
exporter.RenderSelectedOnly = spinePreviewer.RenderSelectedOnly;
|
||||
using var view = spinePreviewPanel.GetView();
|
||||
exporter.Resolution = spinePreviewPanel.Resolution;
|
||||
exporter.PreviewerView = view;
|
||||
exporter.RenderSelectedOnly = spinePreviewPanel.RenderSelectedOnly;
|
||||
|
||||
var exportDialog = new Dialogs.ExportDialog(new GifExporterWrapper((GifExporter)exporter));
|
||||
var exportDialog = new Dialogs.ExportDialog(new GifExporterProperty((GifExporter)exporter));
|
||||
if (exportDialog.ShowDialog() != DialogResult.OK)
|
||||
return;
|
||||
|
||||
@@ -154,11 +158,12 @@ namespace SpineViewer
|
||||
if (!exporterCache.ContainsKey(k)) exporterCache[k] = new WebpExporter();
|
||||
|
||||
var exporter = exporterCache[k];
|
||||
exporter.Resolution = spinePreviewer.Resolution;
|
||||
exporter.View = spinePreviewer.GetView();
|
||||
exporter.RenderSelectedOnly = spinePreviewer.RenderSelectedOnly;
|
||||
using var view = spinePreviewPanel.GetView();
|
||||
exporter.Resolution = spinePreviewPanel.Resolution;
|
||||
exporter.PreviewerView = view;
|
||||
exporter.RenderSelectedOnly = spinePreviewPanel.RenderSelectedOnly;
|
||||
|
||||
var exportDialog = new Dialogs.ExportDialog(new WebpExporterWrapper((WebpExporter)exporter));
|
||||
var exportDialog = new Dialogs.ExportDialog(new WebpExporterProperty((WebpExporter)exporter));
|
||||
if (exportDialog.ShowDialog() != DialogResult.OK)
|
||||
return;
|
||||
|
||||
@@ -174,11 +179,12 @@ namespace SpineViewer
|
||||
if (!exporterCache.ContainsKey(k)) exporterCache[k] = new AvifExporter();
|
||||
|
||||
var exporter = exporterCache[k];
|
||||
exporter.Resolution = spinePreviewer.Resolution;
|
||||
exporter.View = spinePreviewer.GetView();
|
||||
exporter.RenderSelectedOnly = spinePreviewer.RenderSelectedOnly;
|
||||
using var view = spinePreviewPanel.GetView();
|
||||
exporter.Resolution = spinePreviewPanel.Resolution;
|
||||
exporter.PreviewerView = view;
|
||||
exporter.RenderSelectedOnly = spinePreviewPanel.RenderSelectedOnly;
|
||||
|
||||
var exportDialog = new Dialogs.ExportDialog(new AvifExporterWrapper((AvifExporter)exporter));
|
||||
var exportDialog = new Dialogs.ExportDialog(new AvifExporterProperty((AvifExporter)exporter));
|
||||
if (exportDialog.ShowDialog() != DialogResult.OK)
|
||||
return;
|
||||
|
||||
@@ -194,11 +200,12 @@ namespace SpineViewer
|
||||
if (!exporterCache.ContainsKey(k)) exporterCache[k] = new Mp4Exporter();
|
||||
|
||||
var exporter = exporterCache[k];
|
||||
exporter.Resolution = spinePreviewer.Resolution;
|
||||
exporter.View = spinePreviewer.GetView();
|
||||
exporter.RenderSelectedOnly = spinePreviewer.RenderSelectedOnly;
|
||||
using var view = spinePreviewPanel.GetView();
|
||||
exporter.Resolution = spinePreviewPanel.Resolution;
|
||||
exporter.PreviewerView = view;
|
||||
exporter.RenderSelectedOnly = spinePreviewPanel.RenderSelectedOnly;
|
||||
|
||||
var exportDialog = new Dialogs.ExportDialog(new Mp4ExporterWrapper((Mp4Exporter)exporter));
|
||||
var exportDialog = new Dialogs.ExportDialog(new Mp4ExporterProperty((Mp4Exporter)exporter));
|
||||
if (exportDialog.ShowDialog() != DialogResult.OK)
|
||||
return;
|
||||
|
||||
@@ -214,11 +221,12 @@ namespace SpineViewer
|
||||
if (!exporterCache.ContainsKey(k)) exporterCache[k] = new WebmExporter();
|
||||
|
||||
var exporter = exporterCache[k];
|
||||
exporter.Resolution = spinePreviewer.Resolution;
|
||||
exporter.View = spinePreviewer.GetView();
|
||||
exporter.RenderSelectedOnly = spinePreviewer.RenderSelectedOnly;
|
||||
using var view = spinePreviewPanel.GetView();
|
||||
exporter.Resolution = spinePreviewPanel.Resolution;
|
||||
exporter.PreviewerView = view;
|
||||
exporter.RenderSelectedOnly = spinePreviewPanel.RenderSelectedOnly;
|
||||
|
||||
var exportDialog = new Dialogs.ExportDialog(new WebmExporterWrapper((WebmExporter)exporter));
|
||||
var exportDialog = new Dialogs.ExportDialog(new WebmExporterProperty((WebmExporter)exporter));
|
||||
if (exportDialog.ShowDialog() != DialogResult.OK)
|
||||
return;
|
||||
|
||||
@@ -234,11 +242,12 @@ namespace SpineViewer
|
||||
if (!exporterCache.ContainsKey(k)) exporterCache[k] = new MkvExporter();
|
||||
|
||||
var exporter = exporterCache[k];
|
||||
exporter.Resolution = spinePreviewer.Resolution;
|
||||
exporter.View = spinePreviewer.GetView();
|
||||
exporter.RenderSelectedOnly = spinePreviewer.RenderSelectedOnly;
|
||||
using var view = spinePreviewPanel.GetView();
|
||||
exporter.Resolution = spinePreviewPanel.Resolution;
|
||||
exporter.PreviewerView = view;
|
||||
exporter.RenderSelectedOnly = spinePreviewPanel.RenderSelectedOnly;
|
||||
|
||||
var exportDialog = new Dialogs.ExportDialog(new MkvExporterWrapper((MkvExporter)exporter));
|
||||
var exportDialog = new Dialogs.ExportDialog(new MkvExporterProperty((MkvExporter)exporter));
|
||||
if (exportDialog.ShowDialog() != DialogResult.OK)
|
||||
return;
|
||||
|
||||
@@ -254,11 +263,12 @@ namespace SpineViewer
|
||||
if (!exporterCache.ContainsKey(k)) exporterCache[k] = new MovExporter();
|
||||
|
||||
var exporter = exporterCache[k];
|
||||
exporter.Resolution = spinePreviewer.Resolution;
|
||||
exporter.View = spinePreviewer.GetView();
|
||||
exporter.RenderSelectedOnly = spinePreviewer.RenderSelectedOnly;
|
||||
using var view = spinePreviewPanel.GetView();
|
||||
exporter.Resolution = spinePreviewPanel.Resolution;
|
||||
exporter.PreviewerView = view;
|
||||
exporter.RenderSelectedOnly = spinePreviewPanel.RenderSelectedOnly;
|
||||
|
||||
var exportDialog = new Dialogs.ExportDialog(new MovExporterWrapper((MovExporter)exporter));
|
||||
var exportDialog = new Dialogs.ExportDialog(new MovExporterProperty((MovExporter)exporter));
|
||||
if (exportDialog.ShowDialog() != DialogResult.OK)
|
||||
return;
|
||||
|
||||
@@ -274,11 +284,12 @@ namespace SpineViewer
|
||||
if (!exporterCache.ContainsKey(k)) exporterCache[k] = new CustomExporter();
|
||||
|
||||
var exporter = exporterCache[k];
|
||||
exporter.Resolution = spinePreviewer.Resolution;
|
||||
exporter.View = spinePreviewer.GetView();
|
||||
exporter.RenderSelectedOnly = spinePreviewer.RenderSelectedOnly;
|
||||
using var view = spinePreviewPanel.GetView();
|
||||
exporter.Resolution = spinePreviewPanel.Resolution;
|
||||
exporter.PreviewerView = view;
|
||||
exporter.RenderSelectedOnly = spinePreviewPanel.RenderSelectedOnly;
|
||||
|
||||
var exportDialog = new Dialogs.ExportDialog(new CustomExporterWrapper((CustomExporter)exporter));
|
||||
var exportDialog = new Dialogs.ExportDialog(new CustomExporterProperty((CustomExporter)exporter));
|
||||
if (exportDialog.ShowDialog() != DialogResult.OK)
|
||||
return;
|
||||
|
||||
@@ -328,40 +339,30 @@ namespace SpineViewer
|
||||
|
||||
private void splitContainer_MouseUp(object sender, MouseEventArgs e) => ActiveControl = null;
|
||||
|
||||
private void propertyGrid_PropertyValueChanged(object sender, PropertyValueChangedEventArgs e)
|
||||
{
|
||||
// 用来解决对面板某些值修改之后, 其他被联动修改的值不会实时刷新的问题
|
||||
(sender as PropertyGrid)?.Refresh();
|
||||
}
|
||||
|
||||
private void Export_Work(object? sender, DoWorkEventArgs e)
|
||||
{
|
||||
var worker = (BackgroundWorker)sender;
|
||||
var exporter = (Exporter.Exporter)e.Argument;
|
||||
var exporter = (Exporter)e.Argument;
|
||||
Invoke(() => TaskbarManager.SetProgressState(Handle, TBPFLAG.TBPF_INDETERMINATE));
|
||||
spinePreviewer.StopRender();
|
||||
spinePreviewPanel.StopRender();
|
||||
lock (spineListView.Spines) { exporter.Export(spineListView.Spines.Where(sp => !sp.IsHidden).ToArray(), (BackgroundWorker)sender); }
|
||||
e.Cancel = worker.CancellationPending;
|
||||
spinePreviewer.StartRender();
|
||||
spinePreviewPanel.StartRender();
|
||||
Invoke(() => TaskbarManager.SetProgressState(Handle, TBPFLAG.TBPF_NOPROGRESS));
|
||||
}
|
||||
|
||||
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++)
|
||||
@@ -372,16 +373,17 @@ 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
|
||||
{
|
||||
srcCvter = SkeletonConverter.New(SpineHelper.GetVersion(skelPath));
|
||||
srcCvter = SkeletonConverter.New(SpineUtils.GetVersion(skelPath));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
@@ -389,8 +391,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)
|
||||
@@ -413,48 +416,30 @@ namespace SpineViewer
|
||||
}
|
||||
}
|
||||
|
||||
//private System.Windows.Forms.Timer timer = new();
|
||||
//private PetForm pet = new PetForm();
|
||||
//private IntPtr screenDC;
|
||||
//private IntPtr memDC;
|
||||
//private void _Test()
|
||||
//{
|
||||
// screenDC = Win32.GetDC(IntPtr.Zero);
|
||||
// memDC = Win32.CreateCompatibleDC(screenDC);
|
||||
// pet.Show();
|
||||
// timer.Tick += Timer_Tick;
|
||||
// timer.Enabled = true;
|
||||
// timer.Interval = 50;
|
||||
// timer.Start();
|
||||
//}
|
||||
private void toolStripMenuItem_DesktopProjection_Click(object sender, EventArgs e)
|
||||
{
|
||||
toolStripMenuItem_DesktopProjection.Checked = !toolStripMenuItem_DesktopProjection.Checked;
|
||||
spinePreviewPanel.EnableDesktopProjection = toolStripMenuItem_DesktopProjection.Checked;
|
||||
}
|
||||
|
||||
//private void Timer_Tick(object? sender, EventArgs e)
|
||||
//{
|
||||
// using var tex = new SFML.Graphics.RenderTexture((uint)pet.Width, (uint)pet.Height);
|
||||
// var v = spinePreviewer.GetView();
|
||||
// tex.SetView(v);
|
||||
// tex.Clear(new SFML.Graphics.Color(0, 0, 0, 0));
|
||||
// lock (spineListView.Spines)
|
||||
// {
|
||||
// foreach (var sp in spineListView.Spines)
|
||||
// tex.Draw(sp);
|
||||
// }
|
||||
// tex.Display();
|
||||
// using var frame = new SFMLImageVideoFrame(tex.Texture.CopyToImage());
|
||||
// using var bitmap = frame.CopyToBitmap();
|
||||
private void toolStripMenuItem_Debug_Click(object sender, EventArgs e)
|
||||
{
|
||||
#if DEBUG
|
||||
//var cvt = SkeletonConverter.New(SpineVersion.V38);
|
||||
//var root = cvt.ReadBinary(@"D:\ACGN\AzurLane_Export\AzurLane_Dynamic\docs\aerhangeersike\aerhangeersike_3\aerhangeersike_3 - 副本.skel");
|
||||
//cvt.WriteJson(root, @"D:\ACGN\AzurLane_Export\AzurLane_Dynamic\docs\aerhangeersike\aerhangeersike_3\aerhangeersike_3.json");
|
||||
|
||||
// var newBitmap = bitmap.GetHbitmap(Color.FromArgb(0));
|
||||
// var oldBitmap = Win32.SelectObject(memDC, newBitmap);
|
||||
//root = cvt.ReadJson(@"D:\ACGN\AzurLane_Export\AzurLane_Dynamic\docs\aerhangeersike\aerhangeersike_3\aerhangeersike_3.json");
|
||||
//cvt.WriteBinary(root, @"D:\ACGN\AzurLane_Export\AzurLane_Dynamic\docs\aerhangeersike\aerhangeersike_3\aerhangeersike_3.skel");
|
||||
//var sp = SpineObject.New(SpineVersion.V38, @"D:\ACGN\AzurLane_Export\AzurLane_Dynamic\docs\aerhangeersike\aerhangeersike_3\aerhangeersike_3.skel");
|
||||
|
||||
// Win32.SIZE size = new Win32.SIZE { cx = pet.Width, cy = pet.Height };
|
||||
// Win32.POINT srcPos = new Win32.POINT { x = 0, y = 0 };
|
||||
// Win32.BLENDFUNCTION blend = new Win32.BLENDFUNCTION { BlendOp = 0, BlendFlags = 0, SourceConstantAlpha = 255, AlphaFormat = Win32.AC_SRC_ALPHA };
|
||||
|
||||
// Win32.UpdateLayeredWindow(pet.Handle, screenDC, IntPtr.Zero, ref size, memDC, ref srcPos, 0, ref blend, Win32.ULW_ALPHA);
|
||||
|
||||
// Win32.SelectObject(memDC, oldBitmap);
|
||||
// Win32.DeleteObject(newBitmap);
|
||||
//}
|
||||
//var cvt = SkeletonConverter.New(SpineVersion.V38);
|
||||
//var root = cvt.ReadJson(@"D:\ACGN\G\GirlsCreation\standing_spine\st4020069\st4020069.json");
|
||||
//cvt.WriteBinary(root, @"D:\ACGN\G\GirlsCreation\standing_spine\st4020069\st4020069.skel");
|
||||
//var sp = SpineObject.New(SpineVersion.V38, @"D:\ACGN\G\GirlsCreation\standing_spine\st4020069\st4020069.skel");
|
||||
//_Test();
|
||||
#endif
|
||||
}
|
||||
|
||||
//private void spinePreviewer_KeyDown(object sender, KeyEventArgs e)
|
||||
//{
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
namespace SpineViewer
|
||||
{
|
||||
partial class PetForm
|
||||
partial class WallpaperForm
|
||||
{
|
||||
/// <summary>
|
||||
/// Required designer variable.
|
||||
@@ -30,18 +30,19 @@
|
||||
{
|
||||
SuspendLayout();
|
||||
//
|
||||
// PetForm
|
||||
// WallpaperForm
|
||||
//
|
||||
AutoScaleMode = AutoScaleMode.None;
|
||||
ClientSize = new Size(490, 456);
|
||||
ClientSize = new Size(512, 512);
|
||||
ControlBox = false;
|
||||
FormBorderStyle = FormBorderStyle.None;
|
||||
MaximizeBox = false;
|
||||
MinimizeBox = false;
|
||||
Name = "PetForm";
|
||||
Name = "WallpaperForm";
|
||||
ShowIcon = false;
|
||||
ShowInTaskbar = false;
|
||||
StartPosition = FormStartPosition.Manual;
|
||||
Text = "PetForm";
|
||||
WindowState = FormWindowState.Minimized;
|
||||
ResumeLayout(false);
|
||||
}
|
||||
|
||||
73
SpineViewer/Forms/WallpaperForm.cs
Normal file
73
SpineViewer/Forms/WallpaperForm.cs
Normal file
@@ -0,0 +1,73 @@
|
||||
using SpineViewer.Natives;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using System.ComponentModel.Design;
|
||||
using System.Data;
|
||||
using System.Drawing;
|
||||
using System.Linq;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows.Forms;
|
||||
|
||||
namespace SpineViewer
|
||||
{
|
||||
[ToolboxItem(true)]
|
||||
[Designer(typeof(ComponentDesigner), typeof(IDesigner))]
|
||||
[DesignTimeVisible(true)]
|
||||
public partial class WallpaperForm: Form
|
||||
{
|
||||
public WallpaperForm()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
|
||||
protected override CreateParams CreateParams
|
||||
{
|
||||
get
|
||||
{
|
||||
var cp = base.CreateParams;
|
||||
cp.ExStyle = Win32.WS_EX_TOOLWINDOW | Win32.WS_EX_LAYERED;
|
||||
return cp;
|
||||
}
|
||||
}
|
||||
|
||||
protected override void OnLoad(EventArgs e)
|
||||
{
|
||||
base.OnLoad(e);
|
||||
|
||||
// 设置成嵌入桌面
|
||||
var progman = Win32.FindWindow("Progman", null);
|
||||
if (progman != IntPtr.Zero)
|
||||
{
|
||||
// 确保 WorkerW 被创建
|
||||
Win32.SendMessageTimeout(progman, Win32.WM_SPAWN_WORKER, IntPtr.Zero, IntPtr.Zero, Win32.SMTO_NORMAL, 1000, out _);
|
||||
var workerW = Win32.GetWorkerW();
|
||||
if (workerW != IntPtr.Zero)
|
||||
{
|
||||
Win32.SetLayeredWindowAttributes(Handle, 0, 255, Win32.LWA_ALPHA);
|
||||
Win32.SetParent(Handle, workerW); // 嵌入之前必须保证有 WS_EX_LAYERED 标志
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
|
||||
[Browsable(false)]
|
||||
public byte LayeredWindowAlpha
|
||||
{
|
||||
get
|
||||
{
|
||||
uint crKey = 0;
|
||||
byte bAlpha = 255;
|
||||
uint dwFlags = Win32.LWA_ALPHA;
|
||||
Win32.GetLayeredWindowAttributes(Handle, ref crKey, ref bAlpha, ref dwFlags);
|
||||
return bAlpha;
|
||||
}
|
||||
set
|
||||
{
|
||||
Win32.SetLayeredWindowAttributes(Handle, 0, value, Win32.LWA_ALPHA);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
120
SpineViewer/Forms/WallpaperForm.resx
Normal file
120
SpineViewer/Forms/WallpaperForm.resx
Normal file
@@ -0,0 +1,120 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<root>
|
||||
<!--
|
||||
Microsoft ResX Schema
|
||||
|
||||
Version 2.0
|
||||
|
||||
The primary goals of this format is to allow a simple XML format
|
||||
that is mostly human readable. The generation and parsing of the
|
||||
various data types are done through the TypeConverter classes
|
||||
associated with the data types.
|
||||
|
||||
Example:
|
||||
|
||||
... ado.net/XML headers & schema ...
|
||||
<resheader name="resmimetype">text/microsoft-resx</resheader>
|
||||
<resheader name="version">2.0</resheader>
|
||||
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
|
||||
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
|
||||
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
|
||||
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
|
||||
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
|
||||
<value>[base64 mime encoded serialized .NET Framework object]</value>
|
||||
</data>
|
||||
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
|
||||
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
|
||||
<comment>This is a comment</comment>
|
||||
</data>
|
||||
|
||||
There are any number of "resheader" rows that contain simple
|
||||
name/value pairs.
|
||||
|
||||
Each data row contains a name, and value. The row also contains a
|
||||
type or mimetype. Type corresponds to a .NET class that support
|
||||
text/value conversion through the TypeConverter architecture.
|
||||
Classes that don't support this are serialized and stored with the
|
||||
mimetype set.
|
||||
|
||||
The mimetype is used for serialized objects, and tells the
|
||||
ResXResourceReader how to depersist the object. This is currently not
|
||||
extensible. For a given mimetype the value must be set accordingly:
|
||||
|
||||
Note - application/x-microsoft.net.object.binary.base64 is the format
|
||||
that the ResXResourceWriter will generate, however the reader can
|
||||
read any of the formats listed below.
|
||||
|
||||
mimetype: application/x-microsoft.net.object.binary.base64
|
||||
value : The object must be serialized with
|
||||
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
|
||||
: and then encoded with base64 encoding.
|
||||
|
||||
mimetype: application/x-microsoft.net.object.soap.base64
|
||||
value : The object must be serialized with
|
||||
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
|
||||
: and then encoded with base64 encoding.
|
||||
|
||||
mimetype: application/x-microsoft.net.object.bytearray.base64
|
||||
value : The object must be serialized into a byte array
|
||||
: using a System.ComponentModel.TypeConverter
|
||||
: and then encoded with base64 encoding.
|
||||
-->
|
||||
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
|
||||
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
|
||||
<xsd:element name="root" msdata:IsDataSet="true">
|
||||
<xsd:complexType>
|
||||
<xsd:choice maxOccurs="unbounded">
|
||||
<xsd:element name="metadata">
|
||||
<xsd:complexType>
|
||||
<xsd:sequence>
|
||||
<xsd:element name="value" type="xsd:string" minOccurs="0" />
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="name" use="required" type="xsd:string" />
|
||||
<xsd:attribute name="type" type="xsd:string" />
|
||||
<xsd:attribute name="mimetype" type="xsd:string" />
|
||||
<xsd:attribute ref="xml:space" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:element name="assembly">
|
||||
<xsd:complexType>
|
||||
<xsd:attribute name="alias" type="xsd:string" />
|
||||
<xsd:attribute name="name" type="xsd:string" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:element name="data">
|
||||
<xsd:complexType>
|
||||
<xsd:sequence>
|
||||
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
|
||||
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
|
||||
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
|
||||
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
|
||||
<xsd:attribute ref="xml:space" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:element name="resheader">
|
||||
<xsd:complexType>
|
||||
<xsd:sequence>
|
||||
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="name" type="xsd:string" use="required" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
</xsd:choice>
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
</xsd:schema>
|
||||
<resheader name="resmimetype">
|
||||
<value>text/microsoft-resx</value>
|
||||
</resheader>
|
||||
<resheader name="version">
|
||||
<value>2.0</value>
|
||||
</resheader>
|
||||
<resheader name="reader">
|
||||
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
||||
</resheader>
|
||||
<resheader name="writer">
|
||||
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
||||
</resheader>
|
||||
</root>
|
||||
@@ -17,6 +17,8 @@ namespace SpineViewer.Natives
|
||||
public const int GWL_STYLE = -16;
|
||||
public const int WS_SIZEBOX = 0x40000;
|
||||
public const int WS_BORDER = 0x800000;
|
||||
public const int WS_VISIBLE = 0x10000000;
|
||||
public const int WS_CHILD = 0x40000000;
|
||||
public const int WS_POPUP = unchecked((int)0x80000000);
|
||||
|
||||
public const int GWL_EXSTYLE = -20;
|
||||
@@ -25,8 +27,10 @@ namespace SpineViewer.Natives
|
||||
public const int WS_EX_TOOLWINDOW = 0x80;
|
||||
public const int WS_EX_WINDOWEDGE = 0x100;
|
||||
public const int WS_EX_CLIENTEDGE = 0x200;
|
||||
public const int WS_EX_APPWINDOW = 0x40000;
|
||||
public const int WS_EX_LAYERED = 0x80000;
|
||||
public const int WS_EX_OVERLAPPEDWINDOW = WS_EX_WINDOWEDGE | WS_EX_CLIENTEDGE;
|
||||
public const int WS_EX_NOACTIVATE = 0x8000000;
|
||||
|
||||
public const uint LWA_COLORKEY = 0x1;
|
||||
public const uint LWA_ALPHA = 0x2;
|
||||
@@ -44,7 +48,6 @@ namespace SpineViewer.Natives
|
||||
public const uint SWP_NOMOVE = 0x0002;
|
||||
public const uint SWP_NOZORDER = 0x0004;
|
||||
public const uint SWP_FRAMECHANGED = 0x0020;
|
||||
public const uint SWP_REFRESHLONG = SWP_NOSIZE | SWP_NOMOVE | SWP_NOZORDER | SWP_FRAMECHANGED;
|
||||
|
||||
public const int WM_SPAWN_WORKER = 0x052C; // 一个未公开的神秘消息
|
||||
|
||||
@@ -170,7 +173,7 @@ namespace SpineViewer.Natives
|
||||
if (progman == nint.Zero)
|
||||
return nint.Zero;
|
||||
nint hWnd = FindWindowEx(progman, 0, "WorkerW", null);
|
||||
Debug.WriteLine($"{hWnd:x8}");
|
||||
Debug.WriteLine($"HWND(Progman.WorkerW): {hWnd:x8}");
|
||||
return hWnd;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
using NLog;
|
||||
using SpineViewer.Utilities;
|
||||
using SpineViewer.Utils;
|
||||
using System.Diagnostics;
|
||||
using System.Reflection;
|
||||
|
||||
|
||||
@@ -1,56 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using System.Drawing.Design;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace SpineViewer.PropertyGridWrappers.Exporter
|
||||
{
|
||||
public class ExporterWrapper(SpineViewer.Exporter.Exporter exporter)
|
||||
{
|
||||
[Browsable(false)]
|
||||
public virtual SpineViewer.Exporter.Exporter Exporter { get; } = exporter;
|
||||
|
||||
/// <summary>
|
||||
/// 输出文件夹
|
||||
/// </summary>
|
||||
[Editor(typeof(FolderNameEditor), typeof(UITypeEditor))]
|
||||
[Category("[0] 导出"), DisplayName("输出文件夹"), Description("逐个导出时可以留空,将逐个导出到模型自身所在目录")]
|
||||
public string? OutputDir { get => Exporter.OutputDir; set => Exporter.OutputDir = value; }
|
||||
|
||||
/// <summary>
|
||||
/// 导出单个
|
||||
/// </summary>
|
||||
[Category("[0] 导出"), DisplayName("导出单个"), Description("是否将模型在同一个画面上导出单个文件,否则逐个导出模型")]
|
||||
public bool IsExportSingle { get => Exporter.IsExportSingle; set => Exporter.IsExportSingle = value; }
|
||||
|
||||
/// <summary>
|
||||
/// 画面分辨率
|
||||
/// </summary>
|
||||
[TypeConverter(typeof(SizeConverter))]
|
||||
[Category("[0] 导出"), DisplayName("分辨率"), Description("画面的宽高像素大小,请在预览画面参数面板进行调整")]
|
||||
public Size Resolution { get => Exporter.Resolution; }
|
||||
|
||||
/// <summary>
|
||||
/// 渲染视窗
|
||||
/// </summary>
|
||||
[Category("[0] 导出"), DisplayName("视图"), Description("画面的视图参数,请在预览画面参数面板进行调整")]
|
||||
public SFML.Graphics.View View { get => Exporter.View; }
|
||||
|
||||
/// <summary>
|
||||
/// 是否仅渲染选中
|
||||
/// </summary>
|
||||
[Category("[0] 导出"), DisplayName("仅渲染选中"), Description("是否仅导出选中的模型,请在预览画面参数面板进行调整")]
|
||||
public bool RenderSelectedOnly { get => Exporter.RenderSelectedOnly; }
|
||||
|
||||
/// <summary>
|
||||
/// 背景颜色
|
||||
/// </summary>
|
||||
[Editor(typeof(SFMLColorEditor), typeof(UITypeEditor))]
|
||||
[TypeConverter(typeof(SFMLColorConverter))]
|
||||
[Category("[0] 导出"), DisplayName("背景颜色"), Description("要使用的背景色, 格式为 #RRGGBBAA")]
|
||||
public SFML.Graphics.Color BackgroundColor { get => Exporter.BackgroundColor; set => Exporter.BackgroundColor = value; }
|
||||
}
|
||||
}
|
||||
@@ -1,34 +0,0 @@
|
||||
using SpineViewer.Exporter;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace SpineViewer.PropertyGridWrappers.Exporter
|
||||
{
|
||||
public class FFmpegVideoExporterWrapper(FFmpegVideoExporter exporter) : VideoExporterWrapper(exporter)
|
||||
{
|
||||
[Browsable(false)]
|
||||
public override FFmpegVideoExporter Exporter => (FFmpegVideoExporter)base.Exporter;
|
||||
|
||||
/// <summary>
|
||||
/// 文件格式
|
||||
/// </summary>
|
||||
[Category("[2] FFmpeg 基本参数"), DisplayName("文件格式"), Description("-f, 文件格式")]
|
||||
public virtual string Format => Exporter.Format;
|
||||
|
||||
/// <summary>
|
||||
/// 文件名后缀
|
||||
/// </summary>
|
||||
[Category("[2] FFmpeg 基本参数"), DisplayName("文件名后缀"), Description("文件名后缀")]
|
||||
public virtual string Suffix => Exporter.Suffix;
|
||||
|
||||
/// <summary>
|
||||
/// 文件名后缀
|
||||
/// </summary>
|
||||
[Category("[2] FFmpeg 基本参数"), DisplayName("自定义参数"), Description("使用 \"ffmpeg -h encoder=<编码器>\" 查看编码器支持的参数\n使用 \"ffmpeg -h muxer=<文件格式>\" 查看文件格式支持的参数")]
|
||||
public string CustomArgument { get => Exporter.CustomArgument; set => Exporter.CustomArgument = value; }
|
||||
}
|
||||
}
|
||||
@@ -1,37 +0,0 @@
|
||||
using SpineViewer.Exporter;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using System.Drawing.Imaging;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace SpineViewer.PropertyGridWrappers.Exporter
|
||||
{
|
||||
public class FrameExporterWrapper(FrameExporter exporter) : ExporterWrapper(exporter)
|
||||
{
|
||||
[Browsable(false)]
|
||||
public override FrameExporter Exporter => (FrameExporter)base.Exporter;
|
||||
|
||||
/// <summary>
|
||||
/// 单帧画面格式
|
||||
/// </summary>
|
||||
[TypeConverter(typeof(ImageFormatConverter))]
|
||||
[Category("[1] 单帧画面"), DisplayName("图像格式")]
|
||||
public ImageFormat ImageFormat { get => Exporter.ImageFormat; set => Exporter.ImageFormat = value; }
|
||||
|
||||
/// <summary>
|
||||
/// 文件名后缀
|
||||
/// </summary>
|
||||
[Category("[1] 单帧画面"), DisplayName("文件名后缀"), Description("与图像格式匹配的文件名后缀")]
|
||||
public string Suffix { get => Exporter.ImageFormat.GetSuffix(); }
|
||||
|
||||
/// <summary>
|
||||
/// DPI
|
||||
/// </summary>
|
||||
[TypeConverter(typeof(SizeFConverter))]
|
||||
[Category("[1] 单帧画面"), DisplayName("DPI"), Description("导出图像的每英寸像素数,用于调整图像的物理尺寸")]
|
||||
public SizeF DPI { get => Exporter.DPI; set => Exporter.DPI = value; }
|
||||
}
|
||||
}
|
||||
@@ -1,23 +0,0 @@
|
||||
using SpineViewer.Exporter;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace SpineViewer.PropertyGridWrappers.Exporter
|
||||
{
|
||||
public class FrameSequenceExporterWrapper(VideoExporter exporter) : VideoExporterWrapper(exporter)
|
||||
{
|
||||
[Browsable(false)]
|
||||
public override FrameSequenceExporter Exporter => (FrameSequenceExporter)base.Exporter;
|
||||
|
||||
/// <summary>
|
||||
/// 文件名后缀
|
||||
/// </summary>
|
||||
[TypeConverter(typeof(StringEnumConverter)), StringEnumConverter.StandardValues(".png", ".jpg", ".tga", ".bmp")]
|
||||
[Category("[2] 帧序列参数"), DisplayName("文件名后缀"), Description("帧文件的后缀,同时决定帧图像格式")]
|
||||
public string Suffix { get => Exporter.Suffix; set => Exporter.Suffix = value; }
|
||||
}
|
||||
}
|
||||
@@ -1,34 +0,0 @@
|
||||
using SpineViewer.Exporter;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace SpineViewer.PropertyGridWrappers.Exporter
|
||||
{
|
||||
class GifExporterWrapper(FFmpegVideoExporter exporter) : FFmpegVideoExporterWrapper(exporter)
|
||||
{
|
||||
[Browsable(false)]
|
||||
public override GifExporter Exporter => (GifExporter)base.Exporter;
|
||||
|
||||
/// <summary>
|
||||
/// 调色板最大颜色数量
|
||||
/// </summary>
|
||||
[Category("[3] 格式参数"), DisplayName("调色板最大颜色数量"), Description("设置调色板使用的最大颜色数量, 越多则色彩保留程度越高")]
|
||||
public uint MaxColors { get => Exporter.MaxColors; set => Exporter.MaxColors = value; }
|
||||
|
||||
/// <summary>
|
||||
/// 透明度阈值
|
||||
/// </summary>
|
||||
[Category("[3] 格式参数"), DisplayName("透明度阈值"), Description("小于该值的像素点会被认为是透明像素")]
|
||||
public byte AlphaThreshold { get => Exporter.AlphaThreshold; set => Exporter.AlphaThreshold = value; }
|
||||
|
||||
/// <summary>
|
||||
/// 透明度阈值
|
||||
/// </summary>
|
||||
[Category("[3] 格式参数"), DisplayName("循环次数"), Description("-loop, 循环次数, -1 不循环, 0 无限循环, 取值范围 [-1, 65535]")]
|
||||
public int Loop { get => Exporter.Loop; set => Exporter.Loop = value; }
|
||||
}
|
||||
}
|
||||
@@ -1,34 +0,0 @@
|
||||
using SpineViewer.Exporter;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace SpineViewer.PropertyGridWrappers.Exporter
|
||||
{
|
||||
public class VideoExporterWrapper(VideoExporter exporter) : ExporterWrapper(exporter)
|
||||
{
|
||||
[Browsable(false)]
|
||||
public override VideoExporter Exporter => (VideoExporter)base.Exporter;
|
||||
|
||||
/// <summary>
|
||||
/// 导出时长
|
||||
/// </summary>
|
||||
[Category("[1] 视频参数"), DisplayName("时长"), Description("可以从模型列表查看动画时长, 如果小于 0, 则在逐个导出时每个模型使用各自的所有轨道动画时长最大值")]
|
||||
public float Duration { get => Exporter.Duration; set => Exporter.Duration = value; }
|
||||
|
||||
/// <summary>
|
||||
/// 帧率
|
||||
/// </summary>
|
||||
[Category("[1] 视频参数"), DisplayName("帧率"), Description("每秒画面数")]
|
||||
public float FPS { get => Exporter.FPS; set => Exporter.FPS = value; }
|
||||
|
||||
/// <summary>
|
||||
/// 保留最后一帧
|
||||
/// </summary>
|
||||
[Category("[1] 视频参数"), DisplayName("保留最后一帧"), Description("当设置保留最后一帧时, 动图会更为连贯, 但是帧数可能比预期帧数多 1")]
|
||||
public bool KeepLast { get => Exporter.KeepLast; set => Exporter.KeepLast = value; }
|
||||
}
|
||||
}
|
||||
@@ -1,36 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace SpineViewer.PropertyGridWrappers.Spine
|
||||
{
|
||||
/// <summary>
|
||||
/// 用于在 PropertyGrid 上显示 Spine 调试属性的包装类
|
||||
/// </summary>
|
||||
public class SpineDebugWrapper(SpineViewer.Spine.Spine spine)
|
||||
{
|
||||
[Browsable(false)]
|
||||
public SpineViewer.Spine.Spine Spine { get; } = spine;
|
||||
|
||||
/// <summary>
|
||||
/// 显示纹理
|
||||
/// </summary>
|
||||
[DisplayName("纹理")]
|
||||
public bool DebugTexture { get => Spine.DebugTexture; set => Spine.DebugTexture = value; }
|
||||
|
||||
/// <summary>
|
||||
/// 显示包围盒
|
||||
/// </summary>
|
||||
[DisplayName("包围盒")]
|
||||
public bool DebugBounds { get => Spine.DebugBounds; set => Spine.DebugBounds = value; }
|
||||
|
||||
/// <summary>
|
||||
/// 显示骨骼
|
||||
/// </summary>
|
||||
[DisplayName("骨架")]
|
||||
public bool DebugBones { get => Spine.DebugBones; set => Spine.DebugBones = value; }
|
||||
}
|
||||
}
|
||||
@@ -1,153 +0,0 @@
|
||||
using SpineViewer.Spine;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace SpineViewer.PropertyGridWrappers.Spine
|
||||
{
|
||||
/// <summary>
|
||||
/// 对皮肤属性的包装类
|
||||
/// </summary>
|
||||
[TypeConverter(typeof(SkinWrapperConverter))]
|
||||
public class SkinWrapper(SpineViewer.Spine.Spine spine, int i)
|
||||
{
|
||||
private readonly SpineViewer.Spine.Spine 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 SkinWrapper) return ToString() == obj.ToString();
|
||||
return base.Equals(obj);
|
||||
}
|
||||
|
||||
public override int GetHashCode() => HashCode.Combine(typeof(SkinWrapper).FullName.GetHashCode(), ToString().GetHashCode());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 皮肤列表动态类型包装类, 用于提供对 Spine 皮肤列表的管理能力
|
||||
/// </summary>
|
||||
/// <param name="spine">关联的 Spine 对象</param>
|
||||
public class SpineSkinWrapper(SpineViewer.Spine.Spine spine) : ICustomTypeDescriptor
|
||||
{
|
||||
/// <summary>
|
||||
/// 皮肤属性描述符, 实现对属性的读取和赋值
|
||||
/// </summary>
|
||||
private class SkinWrapperPropertyDescriptor(int i, Attribute[]? attributes) : PropertyDescriptor($"Skin{i}", attributes)
|
||||
{
|
||||
private readonly int idx = i;
|
||||
|
||||
public override Type ComponentType => typeof(SpineSkinWrapper);
|
||||
public override bool IsReadOnly => false;
|
||||
public override Type PropertyType => typeof(SkinWrapper);
|
||||
public override bool CanResetValue(object component) => false;
|
||||
public override void ResetValue(object component) { }
|
||||
public override bool ShouldSerializeValue(object component) => false;
|
||||
|
||||
/// <summary>
|
||||
/// 得到一个 SkinWrapper, 允许用户查看或者修改具体的属性值, 这个地方决定了在面板上看到的是一个对象及其属性
|
||||
/// </summary>
|
||||
public override object? GetValue(object? component)
|
||||
{
|
||||
if (component is SpineSkinWrapper manager)
|
||||
return manager.GetSkinWrapper(idx);
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 允许通过字符串赋值修改该位置的皮肤
|
||||
/// </summary>
|
||||
public override void SetValue(object? component, object? value)
|
||||
{
|
||||
if (component is SpineSkinWrapper manager)
|
||||
{
|
||||
if (value is string s)
|
||||
manager.SetSkinWrapper(idx, s);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[Browsable(false)]
|
||||
public SpineViewer.Spine.Spine Spine { get; } = spine;
|
||||
|
||||
/// <summary>
|
||||
/// SkinWrapper 属性缓存
|
||||
/// </summary>
|
||||
private readonly Dictionary<int, SkinWrapper> skinWrapperProperties = [];
|
||||
|
||||
/// <summary>
|
||||
/// 访问 SkinWrapper 属性 <c>SkinManager.Skin{i}</c>
|
||||
/// </summary>
|
||||
public SkinWrapper GetSkinWrapper(int i)
|
||||
{
|
||||
if (!skinWrapperProperties.ContainsKey(i))
|
||||
skinWrapperProperties[i] = new SkinWrapper(Spine, i);
|
||||
return skinWrapperProperties[i];
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 设置 SkinWrapper 属性 <c>SkinManager.Skin{i} = <paramref name="value"/></c>
|
||||
/// </summary>
|
||||
public void SetSkinWrapper(int i, string value)
|
||||
{
|
||||
Spine.ReplaceSkin(i, value);
|
||||
TypeDescriptor.Refresh(this);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 在属性面板悬停可以显示已加载的皮肤列表
|
||||
/// </summary>
|
||||
public override string ToString() => $"[{string.Join(", ", Spine.GetLoadedSkins())}]";
|
||||
|
||||
public override bool Equals(object? obj)
|
||||
{
|
||||
if (obj is SpineSkinWrapper wrapper) return ToString() == wrapper.ToString();
|
||||
return base.Equals(obj);
|
||||
}
|
||||
|
||||
public override int GetHashCode() => HashCode.Combine(typeof(SpineSkinWrapper).FullName.GetHashCode(), ToString().GetHashCode());
|
||||
|
||||
#region ICustomTypeDescriptor 接口实现
|
||||
|
||||
// XXX: 必须实现 ICustomTypeDescriptor 接口, 不能继承 CustomTypeDescriptor, 似乎继承下来的东西会有问题, 导致某些调用不正确
|
||||
|
||||
private static readonly Dictionary<int, SkinWrapperPropertyDescriptor> 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());
|
||||
for (var i = 0; i < Spine.GetLoadedSkins().Length; i++)
|
||||
{
|
||||
if (!pdCache.ContainsKey(i))
|
||||
pdCache[i] = new SkinWrapperPropertyDescriptor(i, [new DisplayNameAttribute($"皮肤 {i}")]);
|
||||
props.Add(pdCache[i]);
|
||||
}
|
||||
return props;
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
@@ -1,36 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using System.Drawing.Design;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace SpineViewer.PropertyGridWrappers.Spine
|
||||
{
|
||||
public class SpineWrapper(SpineViewer.Spine.Spine spine)
|
||||
{
|
||||
[Browsable(false)]
|
||||
public SpineViewer.Spine.Spine Spine { get; } = spine;
|
||||
|
||||
[DisplayName("基本信息")]
|
||||
public SpineBaseInfoWrapper BaseInfo { get; } = new(spine);
|
||||
|
||||
[DisplayName("渲染")]
|
||||
public SpineRenderWrapper Render { get; } = new(spine);
|
||||
|
||||
[DisplayName("变换")]
|
||||
public SpineTransformWrapper Transform { get; } = new(spine);
|
||||
|
||||
[TypeConverter(typeof(ExpandableObjectConverter))]
|
||||
[DisplayName("皮肤")]
|
||||
public SpineSkinWrapper Skin { get; } = new(spine);
|
||||
|
||||
[TypeConverter(typeof(ExpandableObjectConverter))]
|
||||
[DisplayName("动画")]
|
||||
public SpineAnimationWrapper Animation { get; } = new(spine);
|
||||
|
||||
[DisplayName("调试")]
|
||||
public SpineDebugWrapper Debug { get; } = new(spine);
|
||||
}
|
||||
}
|
||||
@@ -1,48 +0,0 @@
|
||||
using SpineViewer.Controls;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace SpineViewer.PropertyGridWrappers
|
||||
{
|
||||
/// <summary>
|
||||
/// 用于在 PropertyGrid 上显示 SpinePreviewe 属性的包装类
|
||||
/// </summary>
|
||||
public class SpinePreviewerWrapper(SpinePreviewer previewer)
|
||||
{
|
||||
[Browsable(false)]
|
||||
public SpinePreviewer Previewer { get; } = previewer;
|
||||
|
||||
[TypeConverter(typeof(SizeConverter))]
|
||||
[Category("[0] 导出"), DisplayName("分辨率")]
|
||||
public Size Resolution { get => Previewer.Resolution; set => Previewer.Resolution = value; }
|
||||
|
||||
[TypeConverter(typeof(PointFConverter))]
|
||||
[Category("[0] 导出"), DisplayName("画面中心点")]
|
||||
public PointF Center { get => Previewer.Center; set => Previewer.Center = value; }
|
||||
|
||||
[Category("[0] 导出"), DisplayName("缩放")]
|
||||
public float Zoom { get => Previewer.Zoom; set => Previewer.Zoom = value; }
|
||||
|
||||
[Category("[0] 导出"), DisplayName("旋转")]
|
||||
public float Rotation { get => Previewer.Rotation; set => Previewer.Rotation = value; }
|
||||
|
||||
[Category("[0] 导出"), DisplayName("水平翻转")]
|
||||
public bool FlipX { get => Previewer.FlipX; set => Previewer.FlipX = value; }
|
||||
|
||||
[Category("[0] 导出"), DisplayName("垂直翻转")]
|
||||
public bool FlipY { get => Previewer.FlipY; set => Previewer.FlipY = value; }
|
||||
|
||||
[Category("[0] 导出"), DisplayName("仅渲染选中")]
|
||||
public bool RenderSelectedOnly { get => Previewer.RenderSelectedOnly; set => Previewer.RenderSelectedOnly = value; }
|
||||
|
||||
[Category("[1] 预览"), DisplayName("显示坐标轴")]
|
||||
public bool ShowAxis { get => Previewer.ShowAxis; set => Previewer.ShowAxis = value; }
|
||||
|
||||
[Category("[1] 预览"), DisplayName("最大帧率")]
|
||||
public uint MaxFps { get => Previewer.MaxFps; set => Previewer.MaxFps = value; }
|
||||
}
|
||||
}
|
||||
@@ -1,307 +0,0 @@
|
||||
using SpineViewer.PropertyGridWrappers.Spine;
|
||||
using SpineViewer.Spine;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.ComponentModel;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace SpineViewer.PropertyGridWrappers
|
||||
{
|
||||
public class PointFConverter : ExpandableObjectConverter
|
||||
{
|
||||
public override bool CanConvertTo(ITypeDescriptorContext? context, [NotNullWhen(true)] Type? destinationType)
|
||||
{
|
||||
return destinationType == typeof(string) || base.CanConvertTo(context, destinationType);
|
||||
}
|
||||
|
||||
public override object? ConvertTo(ITypeDescriptorContext? context, CultureInfo? culture, object? value, Type destinationType)
|
||||
{
|
||||
if (destinationType == typeof(string) && value is PointF point)
|
||||
{
|
||||
return $"{point.X}, {point.Y}";
|
||||
}
|
||||
return base.ConvertTo(context, culture, value, destinationType);
|
||||
}
|
||||
|
||||
public override bool CanConvertFrom(ITypeDescriptorContext? context, Type sourceType)
|
||||
{
|
||||
return sourceType == typeof(string) || base.CanConvertFrom(context, sourceType);
|
||||
}
|
||||
|
||||
public override object? ConvertFrom(ITypeDescriptorContext? context, CultureInfo? culture, object value)
|
||||
{
|
||||
if (value is string str)
|
||||
{
|
||||
var parts = str.Split(',');
|
||||
if (parts.Length == 2 &&
|
||||
float.TryParse(parts[0], out var x) &&
|
||||
float.TryParse(parts[1], out var y))
|
||||
{
|
||||
return new PointF(x, y);
|
||||
}
|
||||
}
|
||||
return base.ConvertFrom(context, culture, value);
|
||||
}
|
||||
}
|
||||
|
||||
public class StringEnumConverter : StringConverter
|
||||
{
|
||||
/// <summary>
|
||||
/// 字符串标准值列表属性
|
||||
/// </summary>
|
||||
[AttributeUsage(AttributeTargets.Property, AllowMultiple = false, Inherited = false)]
|
||||
public class StandardValuesAttribute : Attribute
|
||||
{
|
||||
/// <summary>
|
||||
/// 标准值列表
|
||||
/// </summary>
|
||||
public ReadOnlyCollection<string> StandardValues { get; private set; }
|
||||
private readonly List<string> standardValues = [];
|
||||
|
||||
/// <summary>
|
||||
/// 是否允许用户自定义
|
||||
/// </summary>
|
||||
public bool Customizable { get; set; } = false;
|
||||
|
||||
/// <summary>
|
||||
/// 字符串标准值列表
|
||||
/// </summary>
|
||||
/// <param name="values">允许的字符串标准值</param>
|
||||
public StandardValuesAttribute(params string[] values)
|
||||
{
|
||||
standardValues.AddRange(values);
|
||||
StandardValues = standardValues.AsReadOnly();
|
||||
}
|
||||
}
|
||||
|
||||
public override bool GetStandardValuesSupported(ITypeDescriptorContext? context) => true;
|
||||
|
||||
public override bool GetStandardValuesExclusive(ITypeDescriptorContext? context)
|
||||
{
|
||||
var customizable = context?.PropertyDescriptor?.Attributes.OfType<StandardValuesAttribute>().FirstOrDefault()?.Customizable ?? false;
|
||||
return !customizable;
|
||||
}
|
||||
|
||||
public override StandardValuesCollection? GetStandardValues(ITypeDescriptorContext? context)
|
||||
{
|
||||
// 查找属性上的 StandardValuesAttribute
|
||||
var attribute = context?.PropertyDescriptor?.Attributes.OfType<StandardValuesAttribute>().FirstOrDefault();
|
||||
StandardValuesCollection result;
|
||||
if (attribute != null)
|
||||
result = new StandardValuesCollection(attribute.StandardValues);
|
||||
else
|
||||
result = new StandardValuesCollection(Array.Empty<string>());
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
public class SpineVersionConverter : EnumConverter
|
||||
{
|
||||
public SpineVersionConverter() : base(typeof(SpineVersion)) { }
|
||||
|
||||
public override object? ConvertTo(ITypeDescriptorContext? context, CultureInfo? culture, object? value, Type? destinationType)
|
||||
{
|
||||
if (destinationType == typeof(string) && value is SpineVersion version)
|
||||
return version.GetName();
|
||||
return base.ConvertTo(context, culture, value, destinationType);
|
||||
}
|
||||
}
|
||||
|
||||
public class SpineSkinNameConverter : StringConverter
|
||||
{
|
||||
public override bool GetStandardValuesSupported(ITypeDescriptorContext? context) => true;
|
||||
|
||||
public override bool GetStandardValuesExclusive(ITypeDescriptorContext? context) => true;
|
||||
|
||||
public override StandardValuesCollection? GetStandardValues(ITypeDescriptorContext? context)
|
||||
{
|
||||
if (context?.Instance is SpineViewer.Spine.Spine obj)
|
||||
{
|
||||
return new StandardValuesCollection(obj.SkinNames);
|
||||
}
|
||||
else if (context?.Instance is SpineViewer.Spine.Spine[] spines)
|
||||
{
|
||||
if (spines.Length > 0)
|
||||
{
|
||||
IEnumerable<string> common = spines[0].SkinNames;
|
||||
foreach (var spine in spines.Skip(1))
|
||||
common = common.Union(spine.SkinNames);
|
||||
return new StandardValuesCollection(common.ToArray());
|
||||
}
|
||||
}
|
||||
return base.GetStandardValues(context);
|
||||
}
|
||||
}
|
||||
|
||||
public class SpineAnimationNameConverter : StringConverter
|
||||
{
|
||||
public override bool GetStandardValuesSupported(ITypeDescriptorContext? context) => true;
|
||||
|
||||
public override bool GetStandardValuesExclusive(ITypeDescriptorContext? context) => true;
|
||||
|
||||
public override StandardValuesCollection? GetStandardValues(ITypeDescriptorContext? context)
|
||||
{
|
||||
if (context?.Instance is SpineViewer.Spine.Spine obj)
|
||||
{
|
||||
return new StandardValuesCollection(obj.AnimationNames);
|
||||
}
|
||||
else if (context?.Instance is SpineViewer.Spine.Spine[] spines)
|
||||
{
|
||||
if (spines.Length > 0)
|
||||
{
|
||||
IEnumerable<string> common = spines[0].AnimationNames;
|
||||
foreach (var spine in spines.Skip(1))
|
||||
common = common.Union(spine.AnimationNames);
|
||||
return new StandardValuesCollection(common.ToArray());
|
||||
}
|
||||
}
|
||||
return base.GetStandardValues(context);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 皮肤位包装类转换器, 实现字符串和包装类的互相转换, 并且提供标准值列表对属性进行设置, 同时还提供在面板上显示包装类属性的能力
|
||||
/// </summary>
|
||||
public class SkinWrapperConverter : 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 SpineSkinWrapper manager)
|
||||
{
|
||||
return new StandardValuesCollection(manager.Spine.SkinNames);
|
||||
}
|
||||
else if (context?.Instance is object[] instances && instances.All(x => x is SpineSkinWrapper))
|
||||
{
|
||||
// XXX: 这里不知道为啥总是会得到 object[] 类型而不是具体的 SpineSkinWrapper[] 类型
|
||||
var managers = instances.Cast<SpineSkinWrapper>().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);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 轨道索引包装类转换器, 实现字符串和包装类的互相转换, 并且提供标准值列表对属性进行设置, 同时还提供在面板上显示包装类属性的能力
|
||||
/// </summary>
|
||||
public class TrackWrapperConverter : ExpandableObjectConverter
|
||||
{
|
||||
// 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 SpineAnimationWrapper tracks)
|
||||
{
|
||||
return new StandardValuesCollection(tracks.Spine.AnimationNames);
|
||||
}
|
||||
else if (context?.Instance is object[] instances && instances.All(x => x is SpineAnimationWrapper))
|
||||
{
|
||||
// XXX: 这里不知道为啥总是会得到 object[] 类型而不是具体的类型
|
||||
var animTracks = instances.Cast<SpineAnimationWrapper>().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());
|
||||
}
|
||||
}
|
||||
return base.GetStandardValues(context);
|
||||
}
|
||||
}
|
||||
|
||||
public class SFMLColorConverter : ExpandableObjectConverter
|
||||
{
|
||||
private class SFMLColorPropertyDescriptor : SimplePropertyDescriptor
|
||||
{
|
||||
public SFMLColorPropertyDescriptor(Type componentType, string name, Type propertyType) : base(componentType, name, propertyType) { }
|
||||
|
||||
public override object? GetValue(object? component) => component?.GetType().GetField(Name)?.GetValue(component) ?? default;
|
||||
|
||||
public override void SetValue(object? component, object? value) => component?.GetType().GetField(Name)?.SetValue(component, value);
|
||||
}
|
||||
|
||||
public override bool CanConvertFrom(ITypeDescriptorContext? context, Type sourceType)
|
||||
{
|
||||
return sourceType == typeof(string) || base.CanConvertFrom(context, sourceType);
|
||||
}
|
||||
|
||||
public override object? ConvertFrom(ITypeDescriptorContext? context, CultureInfo? culture, object value)
|
||||
{
|
||||
if (value is string s)
|
||||
{
|
||||
s = s.Trim();
|
||||
if (s.StartsWith("#") && s.Length == 9)
|
||||
{
|
||||
try
|
||||
{
|
||||
// 解析 R, G, B, A 分量,注意16进制解析
|
||||
byte r = byte.Parse(s.Substring(1, 2), NumberStyles.HexNumber);
|
||||
byte g = byte.Parse(s.Substring(3, 2), NumberStyles.HexNumber);
|
||||
byte b = byte.Parse(s.Substring(5, 2), NumberStyles.HexNumber);
|
||||
byte a = byte.Parse(s.Substring(7, 2), NumberStyles.HexNumber);
|
||||
return new SFML.Graphics.Color(r, g, b, a);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
throw new FormatException("无法解析颜色,确保格式为 #RRGGBBAA", ex);
|
||||
}
|
||||
}
|
||||
throw new FormatException("格式错误,正确格式为 #RRGGBBAA");
|
||||
}
|
||||
return base.ConvertFrom(context, culture, value);
|
||||
}
|
||||
|
||||
public override bool CanConvertTo(ITypeDescriptorContext? context, Type? destinationType)
|
||||
{
|
||||
return destinationType == typeof(string) || base.CanConvertTo(context, destinationType);
|
||||
}
|
||||
|
||||
public override object? ConvertTo(ITypeDescriptorContext? context, CultureInfo? culture, object? value, Type destinationType)
|
||||
{
|
||||
if (destinationType == typeof(string) && value is SFML.Graphics.Color color)
|
||||
return $"#{color.R:X2}{color.G:X2}{color.B:X2}{color.A:X2}";
|
||||
return base.ConvertTo(context, culture, value, destinationType);
|
||||
}
|
||||
|
||||
public override PropertyDescriptorCollection GetProperties(ITypeDescriptorContext? context, object value, Attribute[]? attributes)
|
||||
{
|
||||
// 自定义属性集合
|
||||
var properties = new List<PropertyDescriptor>
|
||||
{
|
||||
// 定义 R, G, B, A 四个字段的描述器
|
||||
new SFMLColorPropertyDescriptor(typeof(SFML.Graphics.Color), "R", typeof(byte)),
|
||||
new SFMLColorPropertyDescriptor(typeof(SFML.Graphics.Color), "G", typeof(byte)),
|
||||
new SFMLColorPropertyDescriptor(typeof(SFML.Graphics.Color), "B", typeof(byte)),
|
||||
new SFMLColorPropertyDescriptor(typeof(SFML.Graphics.Color), "A", typeof(byte))
|
||||
};
|
||||
|
||||
// 返回自定义属性集合
|
||||
return new PropertyDescriptorCollection(properties.ToArray());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -12,8 +12,45 @@ using System.Globalization;
|
||||
namespace SpineViewer.Spine.Implementations.SkeletonConverter
|
||||
{
|
||||
[SpineImplementation(SpineVersion.V38)]
|
||||
class SkeletonConverter38 : SpineViewer.Spine.SkeletonConverter
|
||||
public class SkeletonConverter38 : Spine.SkeletonConverter
|
||||
{
|
||||
private static readonly Dictionary<TransformMode, string> TransformModeJsonValue = new()
|
||||
{
|
||||
[TransformMode.Normal] = "normal",
|
||||
[TransformMode.OnlyTranslation] = "onlyTranslation",
|
||||
[TransformMode.NoRotationOrReflection] = "noRotationOrReflection",
|
||||
[TransformMode.NoScale] = "noScale",
|
||||
[TransformMode.NoScaleOrReflection] = "noScaleOrReflection",
|
||||
};
|
||||
|
||||
private static readonly Dictionary<BlendMode, string> BlendModeJsonValue = new()
|
||||
{
|
||||
[BlendMode.Normal] = "normal",
|
||||
[BlendMode.Additive] = "additive",
|
||||
[BlendMode.Multiply] = "multiply",
|
||||
[BlendMode.Screen] = "screen",
|
||||
};
|
||||
|
||||
private static readonly Dictionary<PositionMode, string> PositionModeJsonValue = new()
|
||||
{
|
||||
[PositionMode.Fixed] = "fixed",
|
||||
[PositionMode.Percent] = "percent",
|
||||
};
|
||||
|
||||
private static readonly Dictionary<SpacingMode, string> SpacingModeJsonValue = new()
|
||||
{
|
||||
[SpacingMode.Length] = "length",
|
||||
[SpacingMode.Fixed] = "fixed",
|
||||
[SpacingMode.Percent] = "percent",
|
||||
};
|
||||
|
||||
private static readonly Dictionary<RotateMode, string> RotateModeJsonValue = new()
|
||||
{
|
||||
[RotateMode.Tangent] = "tangent",
|
||||
[RotateMode.Chain] = "chain",
|
||||
[RotateMode.ChainScale] = "chainScale",
|
||||
};
|
||||
|
||||
private BinaryReader reader = null;
|
||||
private JsonObject root = null;
|
||||
private bool nonessential = false;
|
||||
@@ -44,6 +81,9 @@ namespace SpineViewer.Spine.Implementations.SkeletonConverter
|
||||
|
||||
idx2event.Clear();
|
||||
|
||||
// 清理临时属性
|
||||
foreach (var (_, data) in root["events"].AsObject()) data.AsObject().Remove("__name__");
|
||||
|
||||
return root;
|
||||
}
|
||||
|
||||
@@ -90,7 +130,7 @@ namespace SpineViewer.Spine.Implementations.SkeletonConverter
|
||||
data["shearX"] = reader.ReadFloat();
|
||||
data["shearY"] = reader.ReadFloat();
|
||||
data["length"] = reader.ReadFloat();
|
||||
data["transform"] = SkeletonBinary.TransformModeValues[reader.ReadVarInt()].ToString();
|
||||
data["transform"] = TransformModeJsonValue[SkeletonBinary.TransformModeValues[reader.ReadVarInt()]];
|
||||
data["skin"] = reader.ReadBoolean();
|
||||
if (nonessential) reader.ReadInt();
|
||||
bones.Add(data);
|
||||
@@ -111,7 +151,7 @@ namespace SpineViewer.Spine.Implementations.SkeletonConverter
|
||||
int dark = reader.ReadInt();
|
||||
if (dark != -1) data["dark"] = dark.ToString("x6"); // 0x00rrggbb -> rrggbb
|
||||
data["attachment"] = reader.ReadStringRef();
|
||||
data["blend"] = ((BlendMode)reader.ReadVarInt()).ToString();
|
||||
data["blend"] = BlendModeJsonValue[((BlendMode)reader.ReadVarInt())];
|
||||
slots.Add(data);
|
||||
}
|
||||
root["slots"] = slots;
|
||||
@@ -181,9 +221,9 @@ namespace SpineViewer.Spine.Implementations.SkeletonConverter
|
||||
data["skin"] = reader.ReadBoolean();
|
||||
data["bones"] = ReadNames(bones);
|
||||
data["target"] = (string)bones[reader.ReadVarInt()]["name"];
|
||||
data["positionMode"] = ((PositionMode)reader.ReadVarInt()).ToString();
|
||||
data["spacingMode"] = ((SpacingMode)reader.ReadVarInt()).ToString();
|
||||
data["rotateMode"] = ((RotateMode)reader.ReadVarInt()).ToString();
|
||||
data["positionMode"] = PositionModeJsonValue[((PositionMode)reader.ReadVarInt())];
|
||||
data["spacingMode"] = SpacingModeJsonValue[((SpacingMode)reader.ReadVarInt())];
|
||||
data["rotateMode"] = RotateModeJsonValue[((RotateMode)reader.ReadVarInt())];
|
||||
data["rotation"] = reader.ReadFloat();
|
||||
data["position"] = reader.ReadFloat();
|
||||
data["spacing"] = reader.ReadFloat();
|
||||
@@ -283,7 +323,7 @@ namespace SpineViewer.Spine.Implementations.SkeletonConverter
|
||||
if (path is not null) attachment["path"] = path;
|
||||
attachment["color"] = reader.ReadInt().ToString("x8");
|
||||
vertexCount = reader.ReadVarInt();
|
||||
attachment["uvs"] = ReadFloatArray(vertexCount << 1); // vertexCount = uvs.Length
|
||||
attachment["uvs"] = ReadFloatArray(vertexCount << 1); // vertexCount = uvs.Length >> 1
|
||||
attachment["triangles"] = ReadShortArray();
|
||||
attachment["vertices"] = ReadVertices(vertexCount);
|
||||
attachment["hull"] = reader.ReadVarInt();
|
||||
@@ -348,7 +388,7 @@ namespace SpineViewer.Spine.Implementations.SkeletonConverter
|
||||
JsonObject data = [];
|
||||
var name = reader.ReadStringRef();
|
||||
events[name] = data;
|
||||
data["name"] = name; // 额外增加的, 方便后面查找
|
||||
data["__name__"] = name; // 数据里是不应该有这个字段的, 但是为了 ReadEventTimelines 里能够提供 name 字段, 临时增加了一下
|
||||
data["int"] = reader.ReadVarInt(false);
|
||||
data["float"] = reader.ReadFloat();
|
||||
data["string"] = reader.ReadString();
|
||||
@@ -376,7 +416,7 @@ namespace SpineViewer.Spine.Implementations.SkeletonConverter
|
||||
if (ReadTransformTimelines() is JsonObject transform) data["transform"] = transform;
|
||||
if (ReadPathTimelines() is JsonObject path) data["path"] = path;
|
||||
if (ReadDeformTimelines() is JsonObject deform) data["deform"] = deform;
|
||||
if (ReadDrawOrderTimelines() is JsonArray draworder) data["drawOrder"] = draworder;
|
||||
if (ReadDrawOrderTimelines() is JsonArray draworder) data["draworder"] = draworder;
|
||||
if (ReadEventTimelines() is JsonArray events) data["events"] = events;
|
||||
}
|
||||
root["animations"] = animations;
|
||||
@@ -592,7 +632,7 @@ namespace SpineViewer.Spine.Implementations.SkeletonConverter
|
||||
for (int timelineCount = reader.ReadVarInt(); timelineCount > 0; timelineCount--)
|
||||
{
|
||||
JsonArray frames = [];
|
||||
var type = reader.ReadByte();
|
||||
var type = reader.ReadSByte();
|
||||
var frameCount = reader.ReadVarInt();
|
||||
switch (type)
|
||||
{
|
||||
@@ -600,11 +640,13 @@ namespace SpineViewer.Spine.Implementations.SkeletonConverter
|
||||
timeline["position"] = frames;
|
||||
while (frameCount-- > 0)
|
||||
{
|
||||
frames.Add(new JsonObject()
|
||||
var o = new JsonObject()
|
||||
{
|
||||
["time"] = reader.ReadFloat(),
|
||||
["position"] = reader.ReadFloat(),
|
||||
});
|
||||
};
|
||||
if (frameCount > 0) ReadCurve(o);
|
||||
frames.Add(o);
|
||||
}
|
||||
break;
|
||||
case SkeletonBinary.PATH_SPACING:
|
||||
@@ -671,8 +713,7 @@ namespace SpineViewer.Spine.Implementations.SkeletonConverter
|
||||
var end = reader.ReadVarInt();
|
||||
if (end > 0)
|
||||
{
|
||||
var start = reader.ReadVarInt();
|
||||
o["offset"] = start;
|
||||
o["offset"] = reader.ReadVarInt();
|
||||
o["vertices"] = ReadFloatArray(end);
|
||||
}
|
||||
if (frameCount > 0) ReadCurve(o);
|
||||
@@ -719,14 +760,14 @@ namespace SpineViewer.Spine.Implementations.SkeletonConverter
|
||||
JsonObject data = [];
|
||||
data["time"] = reader.ReadFloat();
|
||||
JsonObject eventData = idx2event[reader.ReadVarInt()].AsObject();
|
||||
data["name"] = (string)eventData["name"];
|
||||
data["name"] = (string)eventData["__name__"];
|
||||
data["int"] = reader.ReadVarInt();
|
||||
data["float"] = reader.ReadFloat();
|
||||
data["string"] = reader.ReadBoolean() ? reader.ReadString() : (string)eventData["string"];
|
||||
if (reader.ReadBoolean()) data["string"] = reader.ReadString();
|
||||
if (eventData.ContainsKey("audio"))
|
||||
{
|
||||
data["volume"] = (string)eventData["volume"];
|
||||
data["balance"] = (string)eventData["balance"];
|
||||
data["volume"] = reader.ReadFloat();
|
||||
data["balance"] = reader.ReadFloat();
|
||||
}
|
||||
eventTimelines.Add(data);
|
||||
}
|
||||
@@ -785,10 +826,6 @@ namespace SpineViewer.Spine.Implementations.SkeletonConverter
|
||||
switch (type)
|
||||
{
|
||||
case SkeletonBinary.CURVE_LINEAR:
|
||||
obj["curve"] = 1 / 3f;
|
||||
obj["c2"] = 1 / 3f;
|
||||
obj["c3"] = 2 / 3f;
|
||||
obj["c4"] = 2 / 3f;
|
||||
break;
|
||||
case SkeletonBinary.CURVE_STEPPED:
|
||||
obj["curve"] = "stepped";
|
||||
@@ -810,6 +847,7 @@ namespace SpineViewer.Spine.Implementations.SkeletonConverter
|
||||
private readonly Dictionary<string, int> ik2idx = [];
|
||||
private readonly Dictionary<string, int> transform2idx = [];
|
||||
private readonly Dictionary<string, int> path2idx = [];
|
||||
private readonly Dictionary<string, int> skin2idx = [];
|
||||
private readonly Dictionary<string, int> event2idx = [];
|
||||
|
||||
public override void WriteBinary(JsonObject root, string binPath, bool nonessential = false)
|
||||
@@ -818,7 +856,7 @@ namespace SpineViewer.Spine.Implementations.SkeletonConverter
|
||||
this.root = root;
|
||||
|
||||
using var outputBody = new MemoryStream(); // 先把主体写入内存缓冲区
|
||||
writer = new(outputBody);
|
||||
BinaryWriter tmpWriter = writer = new (outputBody);
|
||||
|
||||
WriteBones();
|
||||
WriteSlots();
|
||||
@@ -828,16 +866,28 @@ namespace SpineViewer.Spine.Implementations.SkeletonConverter
|
||||
WriteSkins();
|
||||
WriteEvents();
|
||||
WriteAnimations();
|
||||
|
||||
//using var output = File.Create(binPath); // 将数据写入文件
|
||||
//writer = new(output);
|
||||
|
||||
using var output = File.Create(binPath); // 将数据写入文件
|
||||
writer = new(output);
|
||||
|
||||
// 把字符串表保留过来
|
||||
writer.StringTable.AddRange(tmpWriter.StringTable);
|
||||
|
||||
WriteSkeleton();
|
||||
WriteStrings();
|
||||
//output.Write(outputBody.GetBuffer());
|
||||
outputBody.Seek(0, SeekOrigin.Begin);
|
||||
outputBody.CopyTo(output);
|
||||
|
||||
writer = null;
|
||||
this.root = null;
|
||||
|
||||
bone2idx.Clear();
|
||||
slot2idx.Clear();
|
||||
ik2idx.Clear();
|
||||
transform2idx.Clear();
|
||||
path2idx.Clear();
|
||||
skin2idx.Clear();
|
||||
event2idx.Clear();
|
||||
}
|
||||
|
||||
private void WriteSkeleton()
|
||||
@@ -890,7 +940,7 @@ namespace SpineViewer.Spine.Implementations.SkeletonConverter
|
||||
if (data.TryGetPropertyValue("shearX", out var shearX)) writer.WriteFloat((float)shearX); else writer.WriteFloat(0);
|
||||
if (data.TryGetPropertyValue("shearY", out var shearY)) writer.WriteFloat((float)shearY); else writer.WriteFloat(0);
|
||||
if (data.TryGetPropertyValue("length", out var length)) writer.WriteFloat((float)length); else writer.WriteFloat(0);
|
||||
if (data.TryGetPropertyValue("transform", out var transform)) writer.WriteVarInt((int)Enum.Parse<TransformMode>((string)transform, true)); else writer.WriteVarInt((int)TransformMode.Normal);
|
||||
if (data.TryGetPropertyValue("transform", out var transform)) writer.WriteVarInt(Array.IndexOf(SkeletonBinary.TransformModeValues, Enum.Parse<TransformMode>((string)transform, true))); else writer.WriteVarInt(0);
|
||||
if (data.TryGetPropertyValue("skin", out var skin)) writer.WriteBoolean((bool)skin); else writer.WriteBoolean(false);
|
||||
if (nonessential) writer.WriteInt(0);
|
||||
bone2idx[name] = i;
|
||||
@@ -912,7 +962,7 @@ namespace SpineViewer.Spine.Implementations.SkeletonConverter
|
||||
var name = (string)data["name"];
|
||||
writer.WriteString(name);
|
||||
writer.WriteVarInt(bone2idx[(string)data["bone"]]);
|
||||
if (data.TryGetPropertyValue("color", out var color)) writer.WriteInt(int.Parse((string)color, NumberStyles.HexNumber)); else writer.WriteInt(0);
|
||||
if (data.TryGetPropertyValue("color", out var color)) writer.WriteInt(int.Parse((string)color, NumberStyles.HexNumber)); else writer.WriteInt(-1); // 默认值是全 255
|
||||
if (data.TryGetPropertyValue("dark", out var dark)) writer.WriteInt(int.Parse((string)dark, NumberStyles.HexNumber)); else writer.WriteInt(-1);
|
||||
if (data.TryGetPropertyValue("attachment", out var attachment)) writer.WriteStringRef((string)attachment); else writer.WriteStringRef(null);
|
||||
if (data.TryGetPropertyValue("blend", out var blend)) writer.WriteVarInt((int)Enum.Parse<BlendMode>((string)blend, true)); else writer.WriteVarInt((int)BlendMode.Normal);
|
||||
@@ -1018,6 +1068,7 @@ namespace SpineViewer.Spine.Implementations.SkeletonConverter
|
||||
{
|
||||
writer.WriteVarInt(0); // default 的 slotCount
|
||||
writer.WriteVarInt(0); // 其他皮肤数量
|
||||
skin2idx["default"] = skin2idx.Count;
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1029,6 +1080,7 @@ namespace SpineViewer.Spine.Implementations.SkeletonConverter
|
||||
{
|
||||
hasDefault = true;
|
||||
WriteSkin(skin, true);
|
||||
skin2idx["default"] = skin2idx.Count;
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -1045,8 +1097,12 @@ namespace SpineViewer.Spine.Implementations.SkeletonConverter
|
||||
writer.WriteVarInt(skinCount);
|
||||
foreach (JsonObject skin in skins)
|
||||
{
|
||||
if ((string)skin["name"] != "default")
|
||||
var name = (string)skin["name"];
|
||||
if (name != "default")
|
||||
{
|
||||
WriteSkin(skin);
|
||||
skin2idx[name] = skin2idx.Count;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1109,7 +1165,7 @@ namespace SpineViewer.Spine.Implementations.SkeletonConverter
|
||||
if (attachment.TryGetPropertyValue("scaleY", out var scaleY)) writer.WriteFloat((float)scaleY); else writer.WriteFloat(1);
|
||||
if (attachment.TryGetPropertyValue("width", out var width)) writer.WriteFloat((float)width); else writer.WriteFloat(32);
|
||||
if (attachment.TryGetPropertyValue("height", out var height)) writer.WriteFloat((float)height); else writer.WriteFloat(32);
|
||||
if (attachment.TryGetPropertyValue("color", out var color1)) writer.WriteInt(int.Parse((string)color1, NumberStyles.HexNumber)); else writer.WriteInt(0);
|
||||
if (attachment.TryGetPropertyValue("color", out var color1)) writer.WriteInt(int.Parse((string)color1, NumberStyles.HexNumber)); else writer.WriteInt(-1);
|
||||
break;
|
||||
case AttachmentType.Boundingbox:
|
||||
if (attachment.TryGetPropertyValue("vertexCount", out var _vertexCount1)) vertexCount = (int)_vertexCount1; else vertexCount = 0;
|
||||
@@ -1119,10 +1175,10 @@ namespace SpineViewer.Spine.Implementations.SkeletonConverter
|
||||
break;
|
||||
case AttachmentType.Mesh:
|
||||
if (attachment.TryGetPropertyValue("path", out var path2)) writer.WriteStringRef((string)path2); else writer.WriteStringRef(null);
|
||||
if (attachment.TryGetPropertyValue("color", out var color2)) writer.WriteInt(int.Parse((string)color2, NumberStyles.HexNumber)); else writer.WriteInt(0);
|
||||
if (attachment.TryGetPropertyValue("vertexCount", out var _vertexCount2)) vertexCount = (int)_vertexCount2; else vertexCount = 0;
|
||||
if (attachment.TryGetPropertyValue("color", out var color2)) writer.WriteInt(int.Parse((string)color2, NumberStyles.HexNumber)); else writer.WriteInt(-1);
|
||||
vertexCount = attachment["uvs"].AsArray().Count >> 1;
|
||||
writer.WriteVarInt(vertexCount);
|
||||
WriteFloatArray(attachment["uvs"].AsArray(), vertexCount << 1); // vertexCount = uvs.Length
|
||||
WriteFloatArray(attachment["uvs"].AsArray(), vertexCount << 1); // vertexCount = uvs.Length >> 1
|
||||
WriteShortArray(attachment["triangles"].AsArray());
|
||||
WriteVertices(attachment["vertices"].AsArray(), vertexCount);
|
||||
if (attachment.TryGetPropertyValue("hull", out var hull)) writer.WriteVarInt((int)hull); else writer.WriteVarInt(0);
|
||||
@@ -1135,7 +1191,7 @@ namespace SpineViewer.Spine.Implementations.SkeletonConverter
|
||||
break;
|
||||
case AttachmentType.Linkedmesh:
|
||||
if (attachment.TryGetPropertyValue("path", out var path3)) writer.WriteStringRef((string)path3); else writer.WriteStringRef(null);
|
||||
if (attachment.TryGetPropertyValue("color", out var color3)) writer.WriteInt(int.Parse((string)color3, NumberStyles.HexNumber)); else writer.WriteInt(0);
|
||||
if (attachment.TryGetPropertyValue("color", out var color3)) writer.WriteInt(int.Parse((string)color3, NumberStyles.HexNumber)); else writer.WriteInt(-1);
|
||||
if (attachment.TryGetPropertyValue("skin", out var skin)) writer.WriteStringRef((string)skin); else writer.WriteStringRef(null);
|
||||
if (attachment.TryGetPropertyValue("parent", out var parent)) writer.WriteStringRef((string)parent); else writer.WriteStringRef(null);
|
||||
if (attachment.TryGetPropertyValue("deform", out var deform)) writer.WriteBoolean((bool)deform); else writer.WriteBoolean(true);
|
||||
@@ -1199,6 +1255,10 @@ namespace SpineViewer.Spine.Implementations.SkeletonConverter
|
||||
if (data.TryGetPropertyValue("balance", out var balance)) writer.WriteFloat((float)balance); else writer.WriteFloat(0);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
writer.WriteString(null);
|
||||
}
|
||||
event2idx[name] = i++;
|
||||
}
|
||||
}
|
||||
@@ -1210,11 +1270,333 @@ namespace SpineViewer.Spine.Implementations.SkeletonConverter
|
||||
writer.WriteVarInt(0);
|
||||
return;
|
||||
}
|
||||
JsonArray animations = root["animations"].AsArray();
|
||||
|
||||
JsonObject animations = root["animations"].AsObject();
|
||||
writer.WriteVarInt(animations.Count);
|
||||
for (int i = 0, n = animations.Count; i < n; i++)
|
||||
foreach (var (name, _data) in animations)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
JsonObject data = _data.AsObject();
|
||||
writer.WriteString(name);
|
||||
if (data.TryGetPropertyValue("slots", out var slots)) WriteSlotTimelines(slots.AsObject()); else writer.WriteVarInt(0);
|
||||
if (data.TryGetPropertyValue("bones", out var bones)) WriteBoneTimelines(bones.AsObject()); else writer.WriteVarInt(0);
|
||||
if (data.TryGetPropertyValue("ik", out var ik)) WriteIKTimelines(ik.AsObject()); else writer.WriteVarInt(0);
|
||||
if (data.TryGetPropertyValue("transform", out var transform)) WriteTransformTimelines(transform.AsObject()); else writer.WriteVarInt(0);
|
||||
if (data.TryGetPropertyValue("path", out var path)) WritePathTimelines(path.AsObject()); else writer.WriteVarInt(0);
|
||||
if (data.TryGetPropertyValue("deform", out var deform)) WriteDeformTimelines(deform.AsObject()); else writer.WriteVarInt(0);
|
||||
if (data.TryGetPropertyValue("drawOrder", out var drawOrder)) WriteDrawOrderTimelines(drawOrder.AsArray()); else
|
||||
if (data.TryGetPropertyValue("draworder", out var draworder)) WriteDrawOrderTimelines(draworder.AsArray()); else writer.WriteVarInt(0);
|
||||
if (data.TryGetPropertyValue("events", out var events)) WriteEventTimelines(events.AsArray()); else writer.WriteVarInt(0);
|
||||
}
|
||||
}
|
||||
|
||||
private void WriteSlotTimelines(JsonObject slotTimelines)
|
||||
{
|
||||
writer.WriteVarInt(slotTimelines.Count);
|
||||
foreach (var (name, _timeline) in slotTimelines)
|
||||
{
|
||||
JsonObject timeline = _timeline.AsObject();
|
||||
writer.WriteVarInt(slot2idx[name]);
|
||||
writer.WriteVarInt(timeline.Count);
|
||||
foreach (var (type, _frames) in timeline)
|
||||
{
|
||||
JsonArray frames = _frames.AsArray();
|
||||
if (type == "attachment")
|
||||
{
|
||||
writer.WriteByte(SkeletonBinary.SLOT_ATTACHMENT);
|
||||
writer.WriteVarInt(frames.Count);
|
||||
foreach (JsonObject o in frames)
|
||||
{
|
||||
if (o.TryGetPropertyValue("time", out var time)) writer.WriteFloat((float)time); else writer.WriteFloat(0);
|
||||
writer.WriteStringRef((string)o["name"]);
|
||||
}
|
||||
}
|
||||
else if (type == "color")
|
||||
{
|
||||
writer.WriteByte(SkeletonBinary.SLOT_COLOR);
|
||||
writer.WriteVarInt(frames.Count);
|
||||
for (int i = 0, n = frames.Count; i < n; i++)
|
||||
{
|
||||
JsonObject o = frames[i].AsObject();
|
||||
if (o.TryGetPropertyValue("time", out var time)) writer.WriteFloat((float)time); else writer.WriteFloat(0);
|
||||
writer.WriteInt(int.Parse((string)o["color"], NumberStyles.HexNumber));
|
||||
if (i < n - 1) WriteCurve(o);
|
||||
}
|
||||
}
|
||||
else if (type == "twoColor")
|
||||
{
|
||||
writer.WriteByte(SkeletonBinary.SLOT_TWO_COLOR);
|
||||
writer.WriteVarInt(frames.Count);
|
||||
for (int i = 0, n = frames.Count; i < n; i++)
|
||||
{
|
||||
JsonObject o = frames[i].AsObject();
|
||||
if (o.TryGetPropertyValue("time", out var time)) writer.WriteFloat((float)time); else writer.WriteFloat(0);
|
||||
writer.WriteInt(int.Parse((string)o["light"], NumberStyles.HexNumber));
|
||||
writer.WriteInt(int.Parse((string)o["dark"], NumberStyles.HexNumber));
|
||||
if (i < n - 1) WriteCurve(o);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void WriteBoneTimelines(JsonObject boneTimelines)
|
||||
{
|
||||
writer.WriteVarInt(boneTimelines.Count);
|
||||
foreach (var (name, _timeline) in boneTimelines)
|
||||
{
|
||||
JsonObject timeline = _timeline.AsObject();
|
||||
writer.WriteVarInt(bone2idx[name]);
|
||||
writer.WriteVarInt(timeline.Count);
|
||||
foreach (var (type, _frames) in timeline)
|
||||
{
|
||||
JsonArray frames = _frames.AsArray();
|
||||
if (type == "rotate")
|
||||
{
|
||||
writer.WriteByte(SkeletonBinary.BONE_ROTATE);
|
||||
writer.WriteVarInt(frames.Count);
|
||||
for (int i = 0, n = frames.Count; i < n; i++)
|
||||
{
|
||||
JsonObject o = frames[i].AsObject();
|
||||
if (o.TryGetPropertyValue("time", out var time)) writer.WriteFloat((float)time); else writer.WriteFloat(0);
|
||||
if (o.TryGetPropertyValue("angle", out var angle)) writer.WriteFloat((float)angle); else writer.WriteFloat(0);
|
||||
if (i < n - 1) WriteCurve(o);
|
||||
}
|
||||
}
|
||||
else if (type == "translate")
|
||||
{
|
||||
writer.WriteByte(SkeletonBinary.BONE_TRANSLATE);
|
||||
writer.WriteVarInt(frames.Count);
|
||||
for (int i = 0, n = frames.Count; i < n; i++)
|
||||
{
|
||||
JsonObject o = frames[i].AsObject();
|
||||
if (o.TryGetPropertyValue("time", out var time)) writer.WriteFloat((float)time); else writer.WriteFloat(0);
|
||||
if (o.TryGetPropertyValue("x", out var x)) writer.WriteFloat((float)x); else writer.WriteFloat(0);
|
||||
if (o.TryGetPropertyValue("y", out var y)) writer.WriteFloat((float)y); else writer.WriteFloat(0);
|
||||
if (i < n - 1) WriteCurve(o);
|
||||
}
|
||||
}
|
||||
else if (type == "scale")
|
||||
{
|
||||
writer.WriteByte(SkeletonBinary.BONE_SCALE);
|
||||
writer.WriteVarInt(frames.Count);
|
||||
for (int i = 0, n = frames.Count; i < n; i++)
|
||||
{
|
||||
JsonObject o = frames[i].AsObject();
|
||||
if (o.TryGetPropertyValue("time", out var time)) writer.WriteFloat((float)time); else writer.WriteFloat(0);
|
||||
if (o.TryGetPropertyValue("x", out var x)) writer.WriteFloat((float)x); else writer.WriteFloat(1);
|
||||
if (o.TryGetPropertyValue("y", out var y)) writer.WriteFloat((float)y); else writer.WriteFloat(1);
|
||||
if (i < n - 1) WriteCurve(o);
|
||||
}
|
||||
}
|
||||
else if (type == "shear")
|
||||
{
|
||||
writer.WriteByte(SkeletonBinary.BONE_SHEAR);
|
||||
writer.WriteVarInt(frames.Count);
|
||||
for (int i = 0, n = frames.Count; i < n; i++)
|
||||
{
|
||||
JsonObject o = frames[i].AsObject();
|
||||
if (o.TryGetPropertyValue("time", out var time)) writer.WriteFloat((float)time); else writer.WriteFloat(0);
|
||||
if (o.TryGetPropertyValue("x", out var x)) writer.WriteFloat((float)x); else writer.WriteFloat(0);
|
||||
if (o.TryGetPropertyValue("y", out var y)) writer.WriteFloat((float)y); else writer.WriteFloat(0);
|
||||
if (i < n - 1) WriteCurve(o);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void WriteIKTimelines(JsonObject ikTimelines)
|
||||
{
|
||||
writer.WriteVarInt(ikTimelines.Count);
|
||||
foreach (var (name, _frames) in ikTimelines)
|
||||
{
|
||||
JsonArray frames = _frames.AsArray();
|
||||
writer.WriteVarInt(ik2idx[name]);
|
||||
writer.WriteVarInt(frames.Count);
|
||||
for (int i = 0, n = frames.Count; i < n; i++)
|
||||
{
|
||||
JsonObject o = frames[i].AsObject();
|
||||
if (o.TryGetPropertyValue("time", out var time)) writer.WriteFloat((float)time); else writer.WriteFloat(0);
|
||||
if (o.TryGetPropertyValue("mix", out var mix)) writer.WriteFloat((float)mix); else writer.WriteFloat(1);
|
||||
if (o.TryGetPropertyValue("softness", out var softness)) writer.WriteFloat((float)softness); else writer.WriteFloat(0);
|
||||
if (o.TryGetPropertyValue("bendPositive", out var bendPositive)) writer.WriteSByte((sbyte)((bool)bendPositive ? 1 : -1)); else writer.WriteSByte(1);
|
||||
if (o.TryGetPropertyValue("compress", out var compress)) writer.WriteBoolean((bool)compress); else writer.WriteBoolean(false);
|
||||
if (o.TryGetPropertyValue("stretch", out var stretch)) writer.WriteBoolean((bool)stretch); else writer.WriteBoolean(false);
|
||||
if (i < n - 1) WriteCurve(o);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void WriteTransformTimelines(JsonObject transformTimelines)
|
||||
{
|
||||
writer.WriteVarInt(transformTimelines.Count);
|
||||
foreach (var (name, _frames) in transformTimelines)
|
||||
{
|
||||
JsonArray frames = _frames.AsArray();
|
||||
writer.WriteVarInt(transform2idx[name]);
|
||||
writer.WriteVarInt(frames.Count);
|
||||
for (int i = 0, n = frames.Count; i < n; i++)
|
||||
{
|
||||
JsonObject o = frames[i].AsObject();
|
||||
if (o.TryGetPropertyValue("time", out var time)) writer.WriteFloat((float)time); else writer.WriteFloat(0);
|
||||
if (o.TryGetPropertyValue("rotateMix", out var rotateMix)) writer.WriteFloat((float)rotateMix); else writer.WriteFloat(1);
|
||||
if (o.TryGetPropertyValue("translateMix", out var translateMix)) writer.WriteFloat((float)translateMix); else writer.WriteFloat(1);
|
||||
if (o.TryGetPropertyValue("scaleMix", out var scaleMix)) writer.WriteFloat((float)scaleMix); else writer.WriteFloat(1);
|
||||
if (o.TryGetPropertyValue("shearMix", out var shearMix)) writer.WriteFloat((float)shearMix); else writer.WriteFloat(1);
|
||||
if (i < n - 1) WriteCurve(o);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void WritePathTimelines(JsonObject pathTimelines)
|
||||
{
|
||||
writer.WriteVarInt(pathTimelines.Count);
|
||||
foreach (var (name, _timeline) in pathTimelines)
|
||||
{
|
||||
JsonObject timeline = _timeline.AsObject();
|
||||
writer.WriteVarInt(path2idx[name]);
|
||||
writer.WriteVarInt(timeline.Count);
|
||||
foreach (var (type, _frame) in timeline)
|
||||
{
|
||||
JsonArray frames = _frame.AsArray();
|
||||
if (type == "position")
|
||||
{
|
||||
writer.WriteByte(SkeletonBinary.PATH_POSITION);
|
||||
writer.WriteVarInt(frames.Count);
|
||||
for (int i = 0, n = frames.Count; i < n; i++)
|
||||
{
|
||||
JsonObject o = frames[i].AsObject();
|
||||
if (o.TryGetPropertyValue("time", out var time)) writer.WriteFloat((float)time); else writer.WriteFloat(0);
|
||||
if (o.TryGetPropertyValue("position", out var position)) writer.WriteFloat((float)position); else writer.WriteFloat(0);
|
||||
if (i < n - 1) WriteCurve(o);
|
||||
}
|
||||
}
|
||||
else if (type == "spacing")
|
||||
{
|
||||
writer.WriteByte(SkeletonBinary.PATH_SPACING);
|
||||
writer.WriteVarInt(frames.Count);
|
||||
for (int i = 0, n = frames.Count; i < n; i++)
|
||||
{
|
||||
JsonObject o = frames[i].AsObject();
|
||||
if (o.TryGetPropertyValue("time", out var time)) writer.WriteFloat((float)time); else writer.WriteFloat(0);
|
||||
if (o.TryGetPropertyValue("spacing", out var spacing)) writer.WriteFloat((float)spacing); else writer.WriteFloat(0);
|
||||
if (i < n - 1) WriteCurve(o);
|
||||
}
|
||||
}
|
||||
else if (type == "mix")
|
||||
{
|
||||
writer.WriteByte(SkeletonBinary.PATH_MIX);
|
||||
writer.WriteVarInt(frames.Count);
|
||||
for (int i = 0, n = frames.Count; i < n; i++)
|
||||
{
|
||||
JsonObject o = frames[i].AsObject();
|
||||
if (o.TryGetPropertyValue("time", out var time)) writer.WriteFloat((float)time); else writer.WriteFloat(0);
|
||||
if (o.TryGetPropertyValue("rotateMix", out var rotateMix)) writer.WriteFloat((float)rotateMix); else writer.WriteFloat(1);
|
||||
if (o.TryGetPropertyValue("translateMix", out var translateMix)) writer.WriteFloat((float)translateMix); else writer.WriteFloat(1);
|
||||
if (i < n - 1) WriteCurve(o);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void WriteDeformTimelines(JsonObject deformTimelines)
|
||||
{
|
||||
writer.WriteVarInt(deformTimelines.Count);
|
||||
foreach (var (skinName, _skinValue) in deformTimelines)
|
||||
{
|
||||
JsonObject skinValue = _skinValue.AsObject();
|
||||
writer.WriteVarInt(skin2idx[skinName]);
|
||||
writer.WriteVarInt(skinValue.Count);
|
||||
foreach (var (slotName, _slotValue) in skinValue)
|
||||
{
|
||||
JsonObject slotValue = _slotValue.AsObject();
|
||||
writer.WriteVarInt(slot2idx[slotName]);
|
||||
writer.WriteVarInt(slotValue.Count);
|
||||
foreach (var (attachmentName, _frames) in slotValue)
|
||||
{
|
||||
JsonArray frames = _frames.AsArray();
|
||||
writer.WriteStringRef(attachmentName);
|
||||
writer.WriteVarInt(frames.Count);
|
||||
for (int i = 0, n = frames.Count; i < n; i++)
|
||||
{
|
||||
JsonObject o = frames[i].AsObject();
|
||||
if (o.TryGetPropertyValue("time", out var time)) writer.WriteFloat((float)time); else writer.WriteFloat(0);
|
||||
if (o.TryGetPropertyValue("vertices", out var _vertices))
|
||||
{
|
||||
JsonArray vertices = _vertices.AsArray();
|
||||
writer.WriteVarInt(vertices.Count);
|
||||
if (vertices.Count > 0)
|
||||
{
|
||||
if (o.TryGetPropertyValue("offset", out var offset)) writer.WriteVarInt((int)offset); else writer.WriteVarInt(0);
|
||||
WriteFloatArray(vertices, vertices.Count);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
writer.WriteVarInt(0);
|
||||
}
|
||||
if (i < n - 1) WriteCurve(o);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void WriteDrawOrderTimelines(JsonArray drawOrderTimelines)
|
||||
{
|
||||
writer.WriteVarInt(drawOrderTimelines.Count);
|
||||
foreach (JsonObject data in drawOrderTimelines)
|
||||
{
|
||||
if (data.TryGetPropertyValue("time", out var time)) writer.WriteFloat((float)time); else writer.WriteFloat(0);
|
||||
if (data.TryGetPropertyValue("offsets", out var _offsets))
|
||||
{
|
||||
JsonArray offsets = _offsets.AsArray();
|
||||
writer.WriteVarInt(offsets.Count);
|
||||
foreach (JsonObject o in offsets)
|
||||
{
|
||||
writer.WriteVarInt(slot2idx[(string)o["slot"]]);
|
||||
writer.WriteVarInt((int)o["offset"]);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
writer.WriteVarInt(0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void WriteEventTimelines(JsonArray eventTimelines)
|
||||
{
|
||||
JsonObject events = root["events"].AsObject();
|
||||
|
||||
writer.WriteVarInt(eventTimelines.Count);
|
||||
foreach(JsonObject data in eventTimelines)
|
||||
{
|
||||
JsonObject eventData = events[(string)data["name"]].AsObject();
|
||||
if (data.TryGetPropertyValue("time", out var time)) writer.WriteFloat((float)time); else writer.WriteFloat(0);
|
||||
writer.WriteVarInt(event2idx[(string)data["name"]]);
|
||||
if (data.TryGetPropertyValue("int", out var @int)) writer.WriteVarInt((int)@int); else
|
||||
if (eventData.TryGetPropertyValue("int", out var @int2)) writer.WriteVarInt((int)@int2); else writer.WriteVarInt(0);
|
||||
if (data.TryGetPropertyValue("float", out var @float)) writer.WriteFloat((float)@float); else
|
||||
if (eventData.TryGetPropertyValue("float", out var @float2)) writer.WriteFloat((float)@float2); else writer.WriteFloat(0);
|
||||
if (data.TryGetPropertyValue("string", out var @string))
|
||||
{
|
||||
writer.WriteBoolean(true);
|
||||
writer.WriteString((string)@string);
|
||||
}
|
||||
else
|
||||
{
|
||||
writer.WriteBoolean(false);
|
||||
}
|
||||
|
||||
if (eventData.ContainsKey("audio"))
|
||||
{
|
||||
if (data.TryGetPropertyValue("volume", out var volume)) writer.WriteFloat((float)volume); else
|
||||
if (eventData.TryGetPropertyValue("volume", out var volume2)) writer.WriteFloat((float)volume2); else writer.WriteFloat(1);
|
||||
if (data.TryGetPropertyValue("balance", out var balance)) writer.WriteFloat((float)balance); else
|
||||
if (eventData.TryGetPropertyValue("balance", out var balance2)) writer.WriteFloat((float)balance2); else writer.WriteFloat(0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1234,7 +1616,7 @@ namespace SpineViewer.Spine.Implementations.SkeletonConverter
|
||||
public void WriteShortArray(JsonArray array)
|
||||
{
|
||||
writer.WriteVarInt(array.Count);
|
||||
foreach (uint i in array)
|
||||
foreach (int i in array)
|
||||
{
|
||||
writer.WriteByte((byte)(i >> 8));
|
||||
writer.WriteByte((byte)i);
|
||||
@@ -1267,6 +1649,29 @@ namespace SpineViewer.Spine.Implementations.SkeletonConverter
|
||||
}
|
||||
}
|
||||
|
||||
private void WriteCurve(JsonObject obj)
|
||||
{
|
||||
if (obj.TryGetPropertyValue("curve", out var curve))
|
||||
{
|
||||
if (curve.GetValueKind() == JsonValueKind.String)
|
||||
{
|
||||
writer.WriteByte(SkeletonBinary.CURVE_STEPPED);
|
||||
}
|
||||
else
|
||||
{
|
||||
writer.WriteByte(SkeletonBinary.CURVE_BEZIER);
|
||||
writer.WriteFloat((float)curve);
|
||||
if (obj.TryGetPropertyValue("c2", out var c2)) writer.WriteFloat((float)c2); else writer.WriteFloat(0);
|
||||
if (obj.TryGetPropertyValue("c3", out var c3)) writer.WriteFloat((float)c3); else writer.WriteFloat(1);
|
||||
if (obj.TryGetPropertyValue("c4", out var c4)) writer.WriteFloat((float)c4); else writer.WriteFloat(1);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
writer.WriteByte(SkeletonBinary.CURVE_LINEAR);
|
||||
}
|
||||
}
|
||||
|
||||
public override JsonObject ReadJson(string jsonPath)
|
||||
{
|
||||
// replace 3.8.75 to another version to avoid detection in official runtime
|
||||
|
||||
@@ -1,352 +0,0 @@
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using SpineRuntime36;
|
||||
using SpineViewer.Utilities;
|
||||
|
||||
namespace SpineViewer.Spine.Implementations.Spine
|
||||
{
|
||||
[SpineImplementation(SpineVersion.V36)]
|
||||
internal class Spine36 : SpineViewer.Spine.Spine
|
||||
{
|
||||
private static readonly Animation EmptyAnimation = new(EMPTY_ANIMATION, [], 0);
|
||||
|
||||
private class TextureLoader : SpineRuntime36.TextureLoader
|
||||
{
|
||||
public void Load(AtlasPage page, string path)
|
||||
{
|
||||
var texture = new SFML.Graphics.Texture(path);
|
||||
if (page.magFilter == TextureFilter.Linear)
|
||||
texture.Smooth = true;
|
||||
if (page.uWrap == TextureWrap.Repeat && page.vWrap == TextureWrap.Repeat)
|
||||
texture.Repeated = true;
|
||||
|
||||
page.rendererObject = texture;
|
||||
}
|
||||
|
||||
public void Unload(object texture)
|
||||
{
|
||||
((SFML.Graphics.Texture)texture).Dispose();
|
||||
}
|
||||
}
|
||||
private static TextureLoader textureLoader = new();
|
||||
|
||||
private Atlas atlas;
|
||||
private SkeletonBinary? skeletonBinary;
|
||||
private SkeletonJson? skeletonJson;
|
||||
private SkeletonData skeletonData;
|
||||
private AnimationStateData animationStateData;
|
||||
|
||||
private Skeleton skeleton;
|
||||
private AnimationState animationState;
|
||||
|
||||
private SkeletonClipping clipping = new();
|
||||
|
||||
public Spine36(string skelPath, string atlasPath) : base(skelPath, atlasPath)
|
||||
{
|
||||
atlas = new Atlas(AtlasPath, textureLoader);
|
||||
try
|
||||
{
|
||||
// 先尝试二进制文件
|
||||
skeletonJson = null;
|
||||
skeletonBinary = new SkeletonBinary(atlas);
|
||||
skeletonData = skeletonBinary.ReadSkeletonData(SkelPath);
|
||||
}
|
||||
catch
|
||||
{
|
||||
try
|
||||
{
|
||||
// 再尝试 Json 文件
|
||||
skeletonBinary = null;
|
||||
skeletonJson = new SkeletonJson(atlas);
|
||||
skeletonData = skeletonJson.ReadSkeletonData(SkelPath);
|
||||
}
|
||||
catch
|
||||
{
|
||||
// 都不行就报错
|
||||
throw new InvalidDataException($"Unknown skeleton file format {SkelPath}");
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var skin in skeletonData.Skins)
|
||||
skinNames.Add(skin.Name);
|
||||
|
||||
foreach (var anime in skeletonData.Animations)
|
||||
animationNames.Add(anime.Name);
|
||||
|
||||
skeleton = new Skeleton(skeletonData);
|
||||
animationStateData = new AnimationStateData(skeletonData);
|
||||
animationState = new AnimationState(animationStateData);
|
||||
}
|
||||
|
||||
protected override void Dispose(bool disposing)
|
||||
{
|
||||
base.Dispose(disposing);
|
||||
atlas.Dispose();
|
||||
}
|
||||
|
||||
public override string FileVersion { get => skeletonData.Version; }
|
||||
|
||||
protected override float scale
|
||||
{
|
||||
get
|
||||
{
|
||||
if (skeletonBinary is not null)
|
||||
return skeletonBinary.Scale;
|
||||
else if (skeletonJson is not null)
|
||||
return skeletonJson.Scale;
|
||||
else
|
||||
return 1f;
|
||||
}
|
||||
set
|
||||
{
|
||||
// 保存状态
|
||||
var pos = position;
|
||||
var fX = flipX;
|
||||
var fY = flipY;
|
||||
var animations = animationState.Tracks.Where(te => te is not null).Select(te => te.Animation.Name).ToArray();
|
||||
|
||||
if (skeletonBinary is not null)
|
||||
{
|
||||
skeletonBinary.Scale = value;
|
||||
skeletonData = skeletonBinary.ReadSkeletonData(SkelPath);
|
||||
}
|
||||
else if (skeletonJson is not null)
|
||||
{
|
||||
skeletonJson.Scale = value;
|
||||
skeletonData = skeletonJson.ReadSkeletonData(SkelPath);
|
||||
}
|
||||
|
||||
// reload skel-dependent data
|
||||
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);
|
||||
for (int i = 0; i < animations.Length; i++) setAnimation(i, animations[i]);
|
||||
}
|
||||
}
|
||||
|
||||
protected override PointF position
|
||||
{
|
||||
get => new(skeleton.X, skeleton.Y);
|
||||
set
|
||||
{
|
||||
skeleton.X = value.X;
|
||||
skeleton.Y = value.Y;
|
||||
}
|
||||
}
|
||||
|
||||
protected override bool flipX
|
||||
{
|
||||
get => skeleton.FlipX;
|
||||
set => skeleton.FlipX = value;
|
||||
}
|
||||
|
||||
protected override bool flipY
|
||||
{
|
||||
get => skeleton.FlipY;
|
||||
set => skeleton.FlipY = value;
|
||||
}
|
||||
|
||||
protected override void addSkin(string name)
|
||||
{
|
||||
if (!skinNames.Contains(name)) return;
|
||||
skeleton.SetSkin(name); // XXX: 3.7 及以下不支持 AddSkin
|
||||
skeleton.SetSlotsToSetupPose();
|
||||
}
|
||||
|
||||
protected override void clearSkin()
|
||||
{
|
||||
skeleton.SetSkin(skeletonData.DefaultSkin);
|
||||
skeleton.SetSlotsToSetupPose();
|
||||
}
|
||||
|
||||
protected override int[] getTrackIndices() => animationState.Tracks.Select((_, i) => i).Where(i => animationState.Tracks.Items[i] is not null).ToArray();
|
||||
|
||||
protected override string getAnimation(int track) => animationState.GetCurrent(track)?.Animation.Name ?? EMPTY_ANIMATION;
|
||||
|
||||
protected override void setAnimation(int track, string name)
|
||||
{
|
||||
if (name == EMPTY_ANIMATION)
|
||||
animationState.SetAnimation(track, EmptyAnimation, false);
|
||||
else if (animationNames.Contains(name))
|
||||
animationState.SetAnimation(track, name, true);
|
||||
}
|
||||
|
||||
protected override void clearTrack(int i) => animationState.ClearTrack(i);
|
||||
|
||||
public override float GetAnimationDuration(string name) { return skeletonData.FindAnimation(name)?.Duration ?? 0f; }
|
||||
|
||||
protected override RectangleF bounds
|
||||
{
|
||||
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 void update(float delta)
|
||||
{
|
||||
animationState.Update(delta);
|
||||
animationState.Apply(skeleton);
|
||||
skeleton.Update(delta);
|
||||
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)
|
||||
{
|
||||
vertexArray.Clear();
|
||||
states.Texture = null;
|
||||
states.Shader = SFMLShader.GetSpineShader(usePma);
|
||||
|
||||
// 要用 DrawOrder 而不是 Slots
|
||||
foreach (var slot in skeleton.DrawOrder)
|
||||
{
|
||||
var attachment = slot.Attachment;
|
||||
|
||||
SFML.Graphics.Texture texture;
|
||||
|
||||
float[] worldVertices = worldVerticesBuffer; // 顶点世界坐标, 连续的 [x0, y0, x1, y1, ...] 坐标值
|
||||
int worldVerticesCount; // 等于顶点数组的长度除以 2
|
||||
int[] worldTriangleIndices; // 三角形索引, 从顶点坐标数组取的时候要乘以 2, 最大值是 worldVerticesCount - 1
|
||||
int worldTriangleIndicesLength; // 三角形索引数组长度
|
||||
float[] uvs; // 纹理坐标
|
||||
float tintR = skeleton.R * slot.R;
|
||||
float tintG = skeleton.G * slot.G;
|
||||
float tintB = skeleton.B * slot.B;
|
||||
float tintA = skeleton.A * slot.A;
|
||||
|
||||
if (attachment is RegionAttachment regionAttachment)
|
||||
{
|
||||
texture = (SFML.Graphics.Texture)((AtlasRegion)regionAttachment.RendererObject).page.rendererObject;
|
||||
|
||||
regionAttachment.ComputeWorldVertices(slot.Bone, worldVertices, 0);
|
||||
worldVerticesCount = 4;
|
||||
worldTriangleIndices = [0, 1, 2, 2, 3, 0];
|
||||
worldTriangleIndicesLength = 6;
|
||||
uvs = regionAttachment.UVs;
|
||||
tintR *= regionAttachment.R;
|
||||
tintG *= regionAttachment.G;
|
||||
tintB *= regionAttachment.B;
|
||||
tintA *= regionAttachment.A;
|
||||
}
|
||||
else if (attachment is MeshAttachment meshAttachment)
|
||||
{
|
||||
texture = (SFML.Graphics.Texture)((AtlasRegion)meshAttachment.RendererObject).page.rendererObject;
|
||||
|
||||
if (meshAttachment.WorldVerticesLength > worldVertices.Length)
|
||||
worldVertices = worldVerticesBuffer = new float[meshAttachment.WorldVerticesLength * 2];
|
||||
meshAttachment.ComputeWorldVertices(slot, worldVertices);
|
||||
worldVerticesCount = meshAttachment.WorldVerticesLength / 2;
|
||||
worldTriangleIndices = meshAttachment.Triangles;
|
||||
worldTriangleIndicesLength = meshAttachment.Triangles.Length;
|
||||
uvs = meshAttachment.UVs;
|
||||
tintR *= meshAttachment.R;
|
||||
tintG *= meshAttachment.G;
|
||||
tintB *= meshAttachment.B;
|
||||
tintA *= meshAttachment.A;
|
||||
}
|
||||
else if (attachment is ClippingAttachment clippingAttachment)
|
||||
{
|
||||
clipping.ClipStart(slot, clippingAttachment);
|
||||
continue;
|
||||
}
|
||||
else
|
||||
{
|
||||
clipping.ClipEnd(slot);
|
||||
continue;
|
||||
}
|
||||
|
||||
SFML.Graphics.BlendMode blendMode = GetSFMLBlendMode(slot.Data.BlendMode);
|
||||
|
||||
states.Texture ??= texture;
|
||||
if (states.BlendMode != blendMode || states.Texture != texture)
|
||||
{
|
||||
if (vertexArray.VertexCount > 0)
|
||||
{
|
||||
// 调试纹理
|
||||
if (!isDebug || debugTexture)
|
||||
target.Draw(vertexArray, states);
|
||||
|
||||
vertexArray.Clear();
|
||||
}
|
||||
states.BlendMode = blendMode;
|
||||
states.Texture = texture;
|
||||
}
|
||||
|
||||
if (clipping.IsClipping)
|
||||
{
|
||||
// 这里必须单独记录 Count, 和 Items 的 Length 是不一致的
|
||||
clipping.ClipTriangles(worldVertices, worldVerticesCount * 2, worldTriangleIndices, worldTriangleIndicesLength, uvs);
|
||||
worldVertices = clipping.ClippedVertices.Items;
|
||||
worldVerticesCount = clipping.ClippedVertices.Count / 2;
|
||||
worldTriangleIndices = clipping.ClippedTriangles.Items;
|
||||
worldTriangleIndicesLength = clipping.ClippedTriangles.Count;
|
||||
uvs = clipping.ClippedUVs.Items;
|
||||
}
|
||||
|
||||
var textureSizeX = texture.Size.X;
|
||||
var textureSizeY = texture.Size.Y;
|
||||
|
||||
SFML.Graphics.Vertex vertex = new();
|
||||
vertex.Color.R = (byte)(tintR * 255);
|
||||
vertex.Color.G = (byte)(tintG * 255);
|
||||
vertex.Color.B = (byte)(tintB * 255);
|
||||
vertex.Color.A = (byte)(tintA * 255);
|
||||
|
||||
// 必须用 worldTriangleIndicesLength 不能直接 foreach
|
||||
for (int i = 0; i < worldTriangleIndicesLength; i++)
|
||||
{
|
||||
var index = worldTriangleIndices[i] * 2;
|
||||
vertex.Position.X = worldVertices[index];
|
||||
vertex.Position.Y = worldVertices[index + 1];
|
||||
vertex.TexCoords.X = uvs[index] * textureSizeX;
|
||||
vertex.TexCoords.Y = uvs[index + 1] * textureSizeY;
|
||||
vertexArray.Append(vertex);
|
||||
}
|
||||
|
||||
clipping.ClipEnd(slot);
|
||||
}
|
||||
clipping.ClipEnd();
|
||||
|
||||
// 调试纹理
|
||||
if (!isDebug || debugTexture)
|
||||
target.Draw(vertexArray, states);
|
||||
|
||||
// 包围盒
|
||||
if (isDebug && isSelected && debugBounds)
|
||||
{
|
||||
var b = bounds;
|
||||
boundsVertices[0] = boundsVertices[4] = new(new(b.Left, b.Top), BoundsColor);
|
||||
boundsVertices[1] = new(new(b.Right, b.Top), BoundsColor);
|
||||
boundsVertices[2] = new(new(b.Right, b.Bottom), BoundsColor);
|
||||
boundsVertices[3] = new(new(b.Left, b.Bottom), BoundsColor);
|
||||
target.Draw(boundsVertices);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,324 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using SpineRuntime37;
|
||||
using SpineViewer.Utilities;
|
||||
|
||||
namespace SpineViewer.Spine.Implementations.Spine
|
||||
{
|
||||
[SpineImplementation(SpineVersion.V37)]
|
||||
internal class Spine37 : SpineViewer.Spine.Spine
|
||||
{
|
||||
private static readonly Animation EmptyAnimation = new(EMPTY_ANIMATION, [], 0);
|
||||
|
||||
private class TextureLoader : SpineRuntime37.TextureLoader
|
||||
{
|
||||
public void Load(AtlasPage page, string path)
|
||||
{
|
||||
var texture = new SFML.Graphics.Texture(path);
|
||||
if (page.magFilter == TextureFilter.Linear)
|
||||
texture.Smooth = true;
|
||||
if (page.uWrap == TextureWrap.Repeat && page.vWrap == TextureWrap.Repeat)
|
||||
texture.Repeated = true;
|
||||
|
||||
page.rendererObject = texture;
|
||||
}
|
||||
|
||||
public void Unload(object texture)
|
||||
{
|
||||
((SFML.Graphics.Texture)texture).Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
private static TextureLoader textureLoader = new();
|
||||
|
||||
private Atlas atlas;
|
||||
private SkeletonBinary? skeletonBinary;
|
||||
private SkeletonJson? skeletonJson;
|
||||
private SkeletonData skeletonData;
|
||||
private AnimationStateData animationStateData;
|
||||
|
||||
private Skeleton skeleton;
|
||||
private AnimationState animationState;
|
||||
|
||||
private SkeletonClipping clipping = new();
|
||||
|
||||
public Spine37(string skelPath, string atlasPath) : base(skelPath, atlasPath)
|
||||
{
|
||||
atlas = new Atlas(AtlasPath, textureLoader);
|
||||
try
|
||||
{
|
||||
// 先尝试二进制文件
|
||||
skeletonJson = null;
|
||||
skeletonBinary = new SkeletonBinary(atlas);
|
||||
skeletonData = skeletonBinary.ReadSkeletonData(SkelPath);
|
||||
}
|
||||
catch
|
||||
{
|
||||
try
|
||||
{
|
||||
// 再尝试 Json 文件
|
||||
skeletonBinary = null;
|
||||
skeletonJson = new SkeletonJson(atlas);
|
||||
skeletonData = skeletonJson.ReadSkeletonData(SkelPath);
|
||||
}
|
||||
catch
|
||||
{
|
||||
// 都不行就报错
|
||||
throw new InvalidDataException($"Unknown skeleton file format {SkelPath}");
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var skin in skeletonData.Skins)
|
||||
skinNames.Add(skin.Name);
|
||||
|
||||
foreach (var anime in skeletonData.Animations)
|
||||
animationNames.Add(anime.Name);
|
||||
|
||||
skeleton = new Skeleton(skeletonData);
|
||||
animationStateData = new AnimationStateData(skeletonData);
|
||||
animationState = new AnimationState(animationStateData);
|
||||
}
|
||||
|
||||
protected override void Dispose(bool disposing)
|
||||
{
|
||||
base.Dispose(disposing);
|
||||
atlas.Dispose();
|
||||
}
|
||||
|
||||
public override string FileVersion { get => skeletonData.Version; }
|
||||
|
||||
protected override float scale
|
||||
{
|
||||
get => Math.Abs(skeleton.ScaleX);
|
||||
set
|
||||
{
|
||||
skeleton.ScaleX = Math.Sign(skeleton.ScaleX) * value;
|
||||
skeleton.ScaleY = Math.Sign(skeleton.ScaleY) * value;
|
||||
}
|
||||
}
|
||||
|
||||
protected override PointF position
|
||||
{
|
||||
get => new(skeleton.X, skeleton.Y);
|
||||
set
|
||||
{
|
||||
skeleton.X = value.X;
|
||||
skeleton.Y = value.Y;
|
||||
}
|
||||
}
|
||||
|
||||
protected override bool flipX
|
||||
{
|
||||
get => skeleton.ScaleX < 0;
|
||||
set
|
||||
{
|
||||
if (skeleton.ScaleX > 0 && value || skeleton.ScaleX < 0 && !value)
|
||||
skeleton.ScaleX *= -1;
|
||||
}
|
||||
}
|
||||
|
||||
protected override bool flipY
|
||||
{
|
||||
get => skeleton.ScaleY < 0;
|
||||
set
|
||||
{
|
||||
if (skeleton.ScaleY > 0 && value || skeleton.ScaleY < 0 && !value)
|
||||
skeleton.ScaleY *= -1;
|
||||
}
|
||||
}
|
||||
|
||||
protected override void addSkin(string name)
|
||||
{
|
||||
if (!skinNames.Contains(name)) return;
|
||||
skeleton.SetSkin(name); // XXX: 3.7 及以下不支持 AddSkin
|
||||
skeleton.SetSlotsToSetupPose();
|
||||
}
|
||||
|
||||
protected override void clearSkin()
|
||||
{
|
||||
skeleton.SetSkin(skeletonData.DefaultSkin);
|
||||
skeleton.SetSlotsToSetupPose();
|
||||
}
|
||||
|
||||
protected override int[] getTrackIndices() => animationState.Tracks.Select((_, i) => i).Where(i => animationState.Tracks.Items[i] is not null).ToArray();
|
||||
|
||||
protected override string getAnimation(int track) => animationState.GetCurrent(track)?.Animation.Name ?? EMPTY_ANIMATION;
|
||||
|
||||
protected override void setAnimation(int track, string name)
|
||||
{
|
||||
if (name == EMPTY_ANIMATION)
|
||||
animationState.SetAnimation(track, EmptyAnimation, false);
|
||||
else if (animationNames.Contains(name))
|
||||
animationState.SetAnimation(track, name, true);
|
||||
}
|
||||
|
||||
protected override void clearTrack(int i) => animationState.ClearTrack(i);
|
||||
|
||||
public override float GetAnimationDuration(string name) { return skeletonData.FindAnimation(name)?.Duration ?? 0f; }
|
||||
|
||||
protected override RectangleF bounds
|
||||
{
|
||||
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 void update(float delta)
|
||||
{
|
||||
animationState.Update(delta);
|
||||
animationState.Apply(skeleton);
|
||||
skeleton.Update(delta);
|
||||
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)
|
||||
{
|
||||
vertexArray.Clear();
|
||||
states.Texture = null;
|
||||
states.Shader = SFMLShader.GetSpineShader(usePma);
|
||||
|
||||
// 要用 DrawOrder 而不是 Slots
|
||||
foreach (var slot in skeleton.DrawOrder)
|
||||
{
|
||||
var attachment = slot.Attachment;
|
||||
|
||||
SFML.Graphics.Texture texture;
|
||||
|
||||
float[] worldVertices = worldVerticesBuffer; // 顶点世界坐标, 连续的 [x0, y0, x1, y1, ...] 坐标值
|
||||
int worldVerticesCount; // 等于顶点数组的长度除以 2
|
||||
int[] worldTriangleIndices; // 三角形索引, 从顶点坐标数组取的时候要乘以 2, 最大值是 worldVerticesCount - 1
|
||||
int worldTriangleIndicesLength; // 三角形索引数组长度
|
||||
float[] uvs; // 纹理坐标
|
||||
float tintR = skeleton.R * slot.R;
|
||||
float tintG = skeleton.G * slot.G;
|
||||
float tintB = skeleton.B * slot.B;
|
||||
float tintA = skeleton.A * slot.A;
|
||||
|
||||
if (attachment is RegionAttachment regionAttachment)
|
||||
{
|
||||
texture = (SFML.Graphics.Texture)((AtlasRegion)regionAttachment.RendererObject).page.rendererObject;
|
||||
|
||||
regionAttachment.ComputeWorldVertices(slot.Bone, worldVertices, 0);
|
||||
worldVerticesCount = 4;
|
||||
worldTriangleIndices = [0, 1, 2, 2, 3, 0];
|
||||
worldTriangleIndicesLength = 6;
|
||||
uvs = regionAttachment.UVs;
|
||||
tintR *= regionAttachment.R;
|
||||
tintG *= regionAttachment.G;
|
||||
tintB *= regionAttachment.B;
|
||||
tintA *= regionAttachment.A;
|
||||
}
|
||||
else if (attachment is MeshAttachment meshAttachment)
|
||||
{
|
||||
texture = (SFML.Graphics.Texture)((AtlasRegion)meshAttachment.RendererObject).page.rendererObject;
|
||||
|
||||
if (meshAttachment.WorldVerticesLength > worldVertices.Length)
|
||||
worldVertices = worldVerticesBuffer = new float[meshAttachment.WorldVerticesLength * 2];
|
||||
meshAttachment.ComputeWorldVertices(slot, worldVertices);
|
||||
worldVerticesCount = meshAttachment.WorldVerticesLength / 2;
|
||||
worldTriangleIndices = meshAttachment.Triangles;
|
||||
worldTriangleIndicesLength = meshAttachment.Triangles.Length;
|
||||
uvs = meshAttachment.UVs;
|
||||
tintR *= meshAttachment.R;
|
||||
tintG *= meshAttachment.G;
|
||||
tintB *= meshAttachment.B;
|
||||
tintA *= meshAttachment.A;
|
||||
}
|
||||
else if (attachment is ClippingAttachment clippingAttachment)
|
||||
{
|
||||
clipping.ClipStart(slot, clippingAttachment);
|
||||
continue;
|
||||
}
|
||||
else
|
||||
{
|
||||
clipping.ClipEnd(slot);
|
||||
continue;
|
||||
}
|
||||
|
||||
SFML.Graphics.BlendMode blendMode = GetSFMLBlendMode(slot.Data.BlendMode);
|
||||
|
||||
states.Texture ??= texture;
|
||||
if (states.BlendMode != blendMode || states.Texture != texture)
|
||||
{
|
||||
if (vertexArray.VertexCount > 0)
|
||||
{
|
||||
// 调试纹理
|
||||
if (!isDebug || debugTexture)
|
||||
target.Draw(vertexArray, states);
|
||||
|
||||
vertexArray.Clear();
|
||||
}
|
||||
states.BlendMode = blendMode;
|
||||
states.Texture = texture;
|
||||
}
|
||||
|
||||
if (clipping.IsClipping)
|
||||
{
|
||||
// 这里必须单独记录 Count, 和 Items 的 Length 是不一致的
|
||||
clipping.ClipTriangles(worldVertices, worldVerticesCount * 2, worldTriangleIndices, worldTriangleIndicesLength, uvs);
|
||||
worldVertices = clipping.ClippedVertices.Items;
|
||||
worldVerticesCount = clipping.ClippedVertices.Count / 2;
|
||||
worldTriangleIndices = clipping.ClippedTriangles.Items;
|
||||
worldTriangleIndicesLength = clipping.ClippedTriangles.Count;
|
||||
uvs = clipping.ClippedUVs.Items;
|
||||
}
|
||||
|
||||
var textureSizeX = texture.Size.X;
|
||||
var textureSizeY = texture.Size.Y;
|
||||
|
||||
SFML.Graphics.Vertex vertex = new();
|
||||
vertex.Color.R = (byte)(tintR * 255);
|
||||
vertex.Color.G = (byte)(tintG * 255);
|
||||
vertex.Color.B = (byte)(tintB * 255);
|
||||
vertex.Color.A = (byte)(tintA * 255);
|
||||
|
||||
// 必须用 worldTriangleIndicesLength 不能直接 foreach
|
||||
for (int i = 0; i < worldTriangleIndicesLength; i++)
|
||||
{
|
||||
var index = worldTriangleIndices[i] * 2;
|
||||
vertex.Position.X = worldVertices[index];
|
||||
vertex.Position.Y = worldVertices[index + 1];
|
||||
vertex.TexCoords.X = uvs[index] * textureSizeX;
|
||||
vertex.TexCoords.Y = uvs[index + 1] * textureSizeY;
|
||||
vertexArray.Append(vertex);
|
||||
}
|
||||
|
||||
clipping.ClipEnd(slot);
|
||||
}
|
||||
clipping.ClipEnd();
|
||||
|
||||
// 调试纹理
|
||||
if (!isDebug || debugTexture)
|
||||
target.Draw(vertexArray, states);
|
||||
|
||||
// 包围盒
|
||||
if (isDebug && isSelected && debugBounds)
|
||||
{
|
||||
var b = bounds;
|
||||
boundsVertices[0] = boundsVertices[4] = new(new(b.Left, b.Top), BoundsColor);
|
||||
boundsVertices[1] = new(new(b.Right, b.Top), BoundsColor);
|
||||
boundsVertices[2] = new(new(b.Right, b.Bottom), BoundsColor);
|
||||
boundsVertices[3] = new(new(b.Left, b.Bottom), BoundsColor);
|
||||
target.Draw(boundsVertices);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,332 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using SpineRuntime38;
|
||||
using SpineRuntime38.Attachments;
|
||||
using SpineViewer.Utilities;
|
||||
|
||||
namespace SpineViewer.Spine.Implementations.Spine
|
||||
{
|
||||
[SpineImplementation(SpineVersion.V38)]
|
||||
internal class Spine38 : SpineViewer.Spine.Spine
|
||||
{
|
||||
private static readonly Animation EmptyAnimation = new(EMPTY_ANIMATION, [], 0);
|
||||
|
||||
private class TextureLoader : SpineRuntime38.TextureLoader
|
||||
{
|
||||
public void Load(AtlasPage page, string path)
|
||||
{
|
||||
var texture = new SFML.Graphics.Texture(path);
|
||||
if (page.magFilter == TextureFilter.Linear)
|
||||
texture.Smooth = true;
|
||||
if (page.uWrap == TextureWrap.Repeat && page.vWrap == TextureWrap.Repeat)
|
||||
texture.Repeated = true;
|
||||
|
||||
page.rendererObject = texture;
|
||||
// 似乎是不需要设置的, 因为存在某些 png 和 atlas 大小不同的情况, 一般是有一些缩放, 如果设置了反而渲染异常
|
||||
// page.width = (int)texture.Size.X;
|
||||
// page.height = (int)texture.Size.Y;
|
||||
}
|
||||
|
||||
public void Unload(object texture)
|
||||
{
|
||||
((SFML.Graphics.Texture)texture).Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
private static TextureLoader textureLoader = new();
|
||||
|
||||
private Atlas atlas;
|
||||
private SkeletonBinary? skeletonBinary;
|
||||
private SkeletonJson? skeletonJson;
|
||||
private SkeletonData skeletonData;
|
||||
private AnimationStateData animationStateData;
|
||||
|
||||
private Skeleton skeleton;
|
||||
private AnimationState animationState;
|
||||
|
||||
private SkeletonClipping clipping = new();
|
||||
|
||||
public Spine38(string skelPath, string atlasPath) : base(skelPath, atlasPath)
|
||||
{
|
||||
atlas = new Atlas(AtlasPath, textureLoader);
|
||||
try
|
||||
{
|
||||
// 先尝试二进制文件
|
||||
skeletonJson = null;
|
||||
skeletonBinary = new SkeletonBinary(atlas);
|
||||
skeletonData = skeletonBinary.ReadSkeletonData(SkelPath);
|
||||
}
|
||||
catch
|
||||
{
|
||||
try
|
||||
{
|
||||
// 再尝试 Json 文件
|
||||
skeletonBinary = null;
|
||||
skeletonJson = new SkeletonJson(atlas);
|
||||
skeletonData = skeletonJson.ReadSkeletonData(SkelPath);
|
||||
}
|
||||
catch
|
||||
{
|
||||
// 都不行就报错
|
||||
throw new InvalidDataException($"Unknown skeleton file format {SkelPath}");
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var skin in skeletonData.Skins)
|
||||
skinNames.Add(skin.Name);
|
||||
|
||||
foreach (var anime in skeletonData.Animations)
|
||||
animationNames.Add(anime.Name);
|
||||
|
||||
skeleton = new Skeleton(skeletonData) { Skin = new(Guid.NewGuid().ToString()) }; // 挂载一个空皮肤当作容器
|
||||
animationStateData = new AnimationStateData(skeletonData);
|
||||
animationState = new AnimationState(animationStateData);
|
||||
}
|
||||
|
||||
protected override void Dispose(bool disposing)
|
||||
{
|
||||
base.Dispose(disposing);
|
||||
atlas.Dispose();
|
||||
}
|
||||
|
||||
public override string FileVersion { get => skeletonData.Version; }
|
||||
|
||||
protected override float scale
|
||||
{
|
||||
get => Math.Abs(skeleton.ScaleX);
|
||||
set
|
||||
{
|
||||
skeleton.ScaleX = Math.Sign(skeleton.ScaleX) * value;
|
||||
skeleton.ScaleY = Math.Sign(skeleton.ScaleY) * value;
|
||||
}
|
||||
}
|
||||
|
||||
protected override PointF position
|
||||
{
|
||||
get => new(skeleton.X, skeleton.Y);
|
||||
set
|
||||
{
|
||||
skeleton.X = value.X;
|
||||
skeleton.Y = value.Y;
|
||||
}
|
||||
}
|
||||
|
||||
protected override bool flipX
|
||||
{
|
||||
get => skeleton.ScaleX < 0;
|
||||
set
|
||||
{
|
||||
if (skeleton.ScaleX > 0 && value || skeleton.ScaleX < 0 && !value)
|
||||
skeleton.ScaleX *= -1;
|
||||
}
|
||||
}
|
||||
|
||||
protected override bool flipY
|
||||
{
|
||||
get => skeleton.ScaleY < 0;
|
||||
set
|
||||
{
|
||||
if (skeleton.ScaleY > 0 && value || skeleton.ScaleY < 0 && !value)
|
||||
skeleton.ScaleY *= -1;
|
||||
}
|
||||
}
|
||||
|
||||
protected override void addSkin(string name)
|
||||
{
|
||||
if (skeletonData.FindSkin(name) is Skin sk)
|
||||
{
|
||||
skeleton.Skin.AddSkin(sk);
|
||||
skeleton.SetSlotsToSetupPose();
|
||||
}
|
||||
}
|
||||
|
||||
protected override void clearSkin()
|
||||
{
|
||||
skeleton.Skin.Clear();
|
||||
skeleton.SetSlotsToSetupPose();
|
||||
}
|
||||
|
||||
protected override int[] getTrackIndices() => animationState.Tracks.Select((_, i) => i).Where(i => animationState.Tracks.Items[i] is not null).ToArray();
|
||||
|
||||
protected override string getAnimation(int track) => animationState.GetCurrent(track)?.Animation.Name ?? EMPTY_ANIMATION;
|
||||
|
||||
protected override void setAnimation(int track, string name)
|
||||
{
|
||||
if (name == EMPTY_ANIMATION)
|
||||
animationState.SetAnimation(track, EmptyAnimation, false);
|
||||
else if (animationNames.Contains(name))
|
||||
animationState.SetAnimation(track, name, true);
|
||||
}
|
||||
|
||||
protected override void clearTrack(int i) => animationState.ClearTrack(i);
|
||||
|
||||
public override float GetAnimationDuration(string name) { return skeletonData.FindAnimation(name)?.Duration ?? 0f; }
|
||||
|
||||
protected override RectangleF bounds
|
||||
{
|
||||
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 void update(float delta)
|
||||
{
|
||||
animationState.Update(delta);
|
||||
animationState.Apply(skeleton);
|
||||
skeleton.Update(delta);
|
||||
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)
|
||||
{
|
||||
vertexArray.Clear();
|
||||
states.Texture = null;
|
||||
states.Shader = SFMLShader.GetSpineShader(usePma);
|
||||
|
||||
// 要用 DrawOrder 而不是 Slots
|
||||
foreach (var slot in skeleton.DrawOrder)
|
||||
{
|
||||
var attachment = slot.Attachment;
|
||||
|
||||
SFML.Graphics.Texture texture;
|
||||
|
||||
float[] worldVertices = worldVerticesBuffer; // 顶点世界坐标, 连续的 [x0, y0, x1, y1, ...] 坐标值
|
||||
int worldVerticesCount; // 等于顶点数组的长度除以 2
|
||||
int[] worldTriangleIndices; // 三角形索引, 从顶点坐标数组取的时候要乘以 2, 最大值是 worldVerticesCount - 1
|
||||
int worldTriangleIndicesLength; // 三角形索引数组长度
|
||||
float[] uvs; // 纹理坐标
|
||||
float tintR = skeleton.R * slot.R;
|
||||
float tintG = skeleton.G * slot.G;
|
||||
float tintB = skeleton.B * slot.B;
|
||||
float tintA = skeleton.A * slot.A;
|
||||
|
||||
if (attachment is RegionAttachment regionAttachment)
|
||||
{
|
||||
texture = (SFML.Graphics.Texture)((AtlasRegion)regionAttachment.RendererObject).page.rendererObject;
|
||||
|
||||
regionAttachment.ComputeWorldVertices(slot.Bone, worldVertices, 0);
|
||||
worldVerticesCount = 4;
|
||||
worldTriangleIndices = [0, 1, 2, 2, 3, 0];
|
||||
worldTriangleIndicesLength = 6;
|
||||
uvs = regionAttachment.UVs;
|
||||
tintR *= regionAttachment.R;
|
||||
tintG *= regionAttachment.G;
|
||||
tintB *= regionAttachment.B;
|
||||
tintA *= regionAttachment.A;
|
||||
}
|
||||
else if (attachment is MeshAttachment meshAttachment)
|
||||
{
|
||||
texture = (SFML.Graphics.Texture)((AtlasRegion)meshAttachment.RendererObject).page.rendererObject;
|
||||
|
||||
if (meshAttachment.WorldVerticesLength > worldVertices.Length)
|
||||
worldVertices = worldVerticesBuffer = new float[meshAttachment.WorldVerticesLength * 2];
|
||||
meshAttachment.ComputeWorldVertices(slot, worldVertices);
|
||||
worldVerticesCount = meshAttachment.WorldVerticesLength / 2;
|
||||
worldTriangleIndices = meshAttachment.Triangles;
|
||||
worldTriangleIndicesLength = meshAttachment.Triangles.Length;
|
||||
uvs = meshAttachment.UVs;
|
||||
tintR *= meshAttachment.R;
|
||||
tintG *= meshAttachment.G;
|
||||
tintB *= meshAttachment.B;
|
||||
tintA *= meshAttachment.A;
|
||||
}
|
||||
else if (attachment is ClippingAttachment clippingAttachment)
|
||||
{
|
||||
clipping.ClipStart(slot, clippingAttachment);
|
||||
continue;
|
||||
}
|
||||
else
|
||||
{
|
||||
clipping.ClipEnd(slot);
|
||||
continue;
|
||||
}
|
||||
|
||||
SFML.Graphics.BlendMode blendMode = GetSFMLBlendMode(slot.Data.BlendMode);
|
||||
|
||||
states.Texture ??= texture;
|
||||
if (states.BlendMode != blendMode || states.Texture != texture)
|
||||
{
|
||||
if (vertexArray.VertexCount > 0)
|
||||
{
|
||||
// 调试纹理
|
||||
if (!isDebug || debugTexture)
|
||||
target.Draw(vertexArray, states);
|
||||
|
||||
vertexArray.Clear();
|
||||
}
|
||||
states.BlendMode = blendMode;
|
||||
states.Texture = texture;
|
||||
}
|
||||
|
||||
if (clipping.IsClipping)
|
||||
{
|
||||
// 这里必须单独记录 Count, 和 Items 的 Length 是不一致的
|
||||
clipping.ClipTriangles(worldVertices, worldVerticesCount * 2, worldTriangleIndices, worldTriangleIndicesLength, uvs);
|
||||
worldVertices = clipping.ClippedVertices.Items;
|
||||
worldVerticesCount = clipping.ClippedVertices.Count / 2;
|
||||
worldTriangleIndices = clipping.ClippedTriangles.Items;
|
||||
worldTriangleIndicesLength = clipping.ClippedTriangles.Count;
|
||||
uvs = clipping.ClippedUVs.Items;
|
||||
}
|
||||
|
||||
var textureSizeX = texture.Size.X;
|
||||
var textureSizeY = texture.Size.Y;
|
||||
|
||||
SFML.Graphics.Vertex vertex = new();
|
||||
vertex.Color.R = (byte)(tintR * 255);
|
||||
vertex.Color.G = (byte)(tintG * 255);
|
||||
vertex.Color.B = (byte)(tintB * 255);
|
||||
vertex.Color.A = (byte)(tintA * 255);
|
||||
|
||||
// 必须用 worldTriangleIndicesLength 不能直接 foreach
|
||||
for (int i = 0; i < worldTriangleIndicesLength; i++)
|
||||
{
|
||||
var index = worldTriangleIndices[i] * 2;
|
||||
vertex.Position.X = worldVertices[index];
|
||||
vertex.Position.Y = worldVertices[index + 1];
|
||||
vertex.TexCoords.X = uvs[index] * textureSizeX;
|
||||
vertex.TexCoords.Y = uvs[index + 1] * textureSizeY;
|
||||
vertexArray.Append(vertex);
|
||||
}
|
||||
|
||||
clipping.ClipEnd(slot);
|
||||
}
|
||||
clipping.ClipEnd();
|
||||
|
||||
// 调试纹理
|
||||
if (!isDebug || debugTexture)
|
||||
target.Draw(vertexArray, states);
|
||||
|
||||
// 调试包围盒
|
||||
if (isDebug && isSelected && debugBounds)
|
||||
{
|
||||
var b = bounds;
|
||||
boundsVertices[0] = boundsVertices[4] = new(new(b.Left, b.Top), BoundsColor);
|
||||
boundsVertices[1] = new(new(b.Right, b.Top), BoundsColor);
|
||||
boundsVertices[2] = new(new(b.Right, b.Bottom), BoundsColor);
|
||||
boundsVertices[3] = new(new(b.Left, b.Bottom), BoundsColor);
|
||||
target.Draw(boundsVertices);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,328 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using SpineRuntime40;
|
||||
using SpineViewer.Utilities;
|
||||
|
||||
namespace SpineViewer.Spine.Implementations.Spine
|
||||
{
|
||||
[SpineImplementation(SpineVersion.V40)]
|
||||
internal class Spine40 : SpineViewer.Spine.Spine
|
||||
{
|
||||
private static readonly Animation EmptyAnimation = new(EMPTY_ANIMATION, [], 0);
|
||||
|
||||
private class TextureLoader : SpineRuntime40.TextureLoader
|
||||
{
|
||||
public void Load(AtlasPage page, string path)
|
||||
{
|
||||
var texture = new SFML.Graphics.Texture(path);
|
||||
if (page.magFilter == TextureFilter.Linear)
|
||||
texture.Smooth = true;
|
||||
if (page.uWrap == TextureWrap.Repeat && page.vWrap == TextureWrap.Repeat)
|
||||
texture.Repeated = true;
|
||||
|
||||
page.rendererObject = texture;
|
||||
}
|
||||
|
||||
public void Unload(object texture)
|
||||
{
|
||||
((SFML.Graphics.Texture)texture).Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
private static TextureLoader textureLoader = new();
|
||||
|
||||
private Atlas atlas;
|
||||
private SkeletonBinary? skeletonBinary;
|
||||
private SkeletonJson? skeletonJson;
|
||||
private SkeletonData skeletonData;
|
||||
private AnimationStateData animationStateData;
|
||||
|
||||
private Skeleton skeleton;
|
||||
private AnimationState animationState;
|
||||
|
||||
private SkeletonClipping clipping = new();
|
||||
|
||||
public Spine40(string skelPath, string atlasPath) : base(skelPath, atlasPath)
|
||||
{
|
||||
atlas = new Atlas(AtlasPath, textureLoader);
|
||||
try
|
||||
{
|
||||
// 先尝试二进制文件
|
||||
skeletonJson = null;
|
||||
skeletonBinary = new SkeletonBinary(atlas);
|
||||
skeletonData = skeletonBinary.ReadSkeletonData(SkelPath);
|
||||
}
|
||||
catch
|
||||
{
|
||||
try
|
||||
{
|
||||
// 再尝试 Json 文件
|
||||
skeletonBinary = null;
|
||||
skeletonJson = new SkeletonJson(atlas);
|
||||
skeletonData = skeletonJson.ReadSkeletonData(SkelPath);
|
||||
}
|
||||
catch
|
||||
{
|
||||
// 都不行就报错
|
||||
throw new InvalidDataException($"Unknown skeleton file format {SkelPath}");
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var skin in skeletonData.Skins)
|
||||
skinNames.Add(skin.Name);
|
||||
|
||||
foreach (var anime in skeletonData.Animations)
|
||||
animationNames.Add(anime.Name);
|
||||
|
||||
skeleton = new Skeleton(skeletonData) { Skin = new(Guid.NewGuid().ToString()) }; // 挂载一个空皮肤当作容器
|
||||
animationStateData = new AnimationStateData(skeletonData);
|
||||
animationState = new AnimationState(animationStateData);
|
||||
}
|
||||
|
||||
protected override void Dispose(bool disposing)
|
||||
{
|
||||
base.Dispose(disposing);
|
||||
atlas.Dispose();
|
||||
}
|
||||
|
||||
public override string FileVersion { get => skeletonData.Version; }
|
||||
|
||||
protected override float scale
|
||||
{
|
||||
get => Math.Abs(skeleton.ScaleX);
|
||||
set
|
||||
{
|
||||
skeleton.ScaleX = Math.Sign(skeleton.ScaleX) * value;
|
||||
skeleton.ScaleY = Math.Sign(skeleton.ScaleY) * value;
|
||||
}
|
||||
}
|
||||
|
||||
protected override PointF position
|
||||
{
|
||||
get => new(skeleton.X, skeleton.Y);
|
||||
set
|
||||
{
|
||||
skeleton.X = value.X;
|
||||
skeleton.Y = value.Y;
|
||||
}
|
||||
}
|
||||
|
||||
protected override bool flipX
|
||||
{
|
||||
get => skeleton.ScaleX < 0;
|
||||
set
|
||||
{
|
||||
if (skeleton.ScaleX > 0 && value || skeleton.ScaleX < 0 && !value)
|
||||
skeleton.ScaleX *= -1;
|
||||
}
|
||||
}
|
||||
|
||||
protected override bool flipY
|
||||
{
|
||||
get => skeleton.ScaleY < 0;
|
||||
set
|
||||
{
|
||||
if (skeleton.ScaleY > 0 && value || skeleton.ScaleY < 0 && !value)
|
||||
skeleton.ScaleY *= -1;
|
||||
}
|
||||
}
|
||||
|
||||
protected override void addSkin(string name)
|
||||
{
|
||||
if (skeletonData.FindSkin(name) is Skin sk)
|
||||
{
|
||||
skeleton.Skin.AddSkin(sk);
|
||||
skeleton.SetSlotsToSetupPose();
|
||||
}
|
||||
}
|
||||
|
||||
protected override void clearSkin()
|
||||
{
|
||||
skeleton.Skin.Clear();
|
||||
skeleton.SetSlotsToSetupPose();
|
||||
}
|
||||
|
||||
protected override int[] getTrackIndices() => animationState.Tracks.Select((_, i) => i).Where(i => animationState.Tracks.Items[i] is not null).ToArray();
|
||||
|
||||
protected override string getAnimation(int track) => animationState.GetCurrent(track)?.Animation.Name ?? EMPTY_ANIMATION;
|
||||
|
||||
protected override void setAnimation(int track, string name)
|
||||
{
|
||||
if (name == EMPTY_ANIMATION)
|
||||
animationState.SetAnimation(track, EmptyAnimation, false);
|
||||
else if (animationNames.Contains(name))
|
||||
animationState.SetAnimation(track, name, true);
|
||||
}
|
||||
|
||||
protected override void clearTrack(int i) => animationState.ClearTrack(i);
|
||||
|
||||
public override float GetAnimationDuration(string name) { return skeletonData.FindAnimation(name)?.Duration ?? 0f; }
|
||||
|
||||
protected override RectangleF bounds
|
||||
{
|
||||
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 void update(float delta)
|
||||
{
|
||||
animationState.Update(delta);
|
||||
animationState.Apply(skeleton);
|
||||
skeleton.Update(delta);
|
||||
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)
|
||||
{
|
||||
vertexArray.Clear();
|
||||
states.Texture = null;
|
||||
states.Shader = SFMLShader.GetSpineShader(usePma);
|
||||
|
||||
// 要用 DrawOrder 而不是 Slots
|
||||
foreach (var slot in skeleton.DrawOrder)
|
||||
{
|
||||
var attachment = slot.Attachment;
|
||||
|
||||
SFML.Graphics.Texture texture;
|
||||
|
||||
float[] worldVertices = worldVerticesBuffer; // 顶点世界坐标, 连续的 [x0, y0, x1, y1, ...] 坐标值
|
||||
int worldVerticesCount; // 等于顶点数组的长度除以 2
|
||||
int[] worldTriangleIndices; // 三角形索引, 从顶点坐标数组取的时候要乘以 2, 最大值是 worldVerticesCount - 1
|
||||
int worldTriangleIndicesLength; // 三角形索引数组长度
|
||||
float[] uvs; // 纹理坐标
|
||||
float tintR = skeleton.R * slot.R;
|
||||
float tintG = skeleton.G * slot.G;
|
||||
float tintB = skeleton.B * slot.B;
|
||||
float tintA = skeleton.A * slot.A;
|
||||
|
||||
if (attachment is RegionAttachment regionAttachment)
|
||||
{
|
||||
texture = (SFML.Graphics.Texture)((AtlasRegion)regionAttachment.RendererObject).page.rendererObject;
|
||||
|
||||
regionAttachment.ComputeWorldVertices(slot.Bone, worldVertices, 0);
|
||||
worldVerticesCount = 4;
|
||||
worldTriangleIndices = [0, 1, 2, 2, 3, 0];
|
||||
worldTriangleIndicesLength = 6;
|
||||
uvs = regionAttachment.UVs;
|
||||
tintR *= regionAttachment.R;
|
||||
tintG *= regionAttachment.G;
|
||||
tintB *= regionAttachment.B;
|
||||
tintA *= regionAttachment.A;
|
||||
}
|
||||
else if (attachment is MeshAttachment meshAttachment)
|
||||
{
|
||||
texture = (SFML.Graphics.Texture)((AtlasRegion)meshAttachment.RendererObject).page.rendererObject;
|
||||
|
||||
if (meshAttachment.WorldVerticesLength > worldVertices.Length)
|
||||
worldVertices = worldVerticesBuffer = new float[meshAttachment.WorldVerticesLength * 2];
|
||||
meshAttachment.ComputeWorldVertices(slot, worldVertices);
|
||||
worldVerticesCount = meshAttachment.WorldVerticesLength / 2;
|
||||
worldTriangleIndices = meshAttachment.Triangles;
|
||||
worldTriangleIndicesLength = meshAttachment.Triangles.Length;
|
||||
uvs = meshAttachment.UVs;
|
||||
tintR *= meshAttachment.R;
|
||||
tintG *= meshAttachment.G;
|
||||
tintB *= meshAttachment.B;
|
||||
tintA *= meshAttachment.A;
|
||||
}
|
||||
else if (attachment is ClippingAttachment clippingAttachment)
|
||||
{
|
||||
clipping.ClipStart(slot, clippingAttachment);
|
||||
continue;
|
||||
}
|
||||
else
|
||||
{
|
||||
clipping.ClipEnd(slot);
|
||||
continue;
|
||||
}
|
||||
|
||||
SFML.Graphics.BlendMode blendMode = GetSFMLBlendMode(slot.Data.BlendMode);
|
||||
|
||||
states.Texture ??= texture;
|
||||
if (states.BlendMode != blendMode || states.Texture != texture)
|
||||
{
|
||||
if (vertexArray.VertexCount > 0)
|
||||
{
|
||||
// 调试纹理
|
||||
if (!isDebug || debugTexture)
|
||||
target.Draw(vertexArray, states);
|
||||
|
||||
vertexArray.Clear();
|
||||
}
|
||||
states.BlendMode = blendMode;
|
||||
states.Texture = texture;
|
||||
}
|
||||
|
||||
if (clipping.IsClipping)
|
||||
{
|
||||
// 这里必须单独记录 Count, 和 Items 的 Length 是不一致的
|
||||
clipping.ClipTriangles(worldVertices, worldVerticesCount * 2, worldTriangleIndices, worldTriangleIndicesLength, uvs);
|
||||
worldVertices = clipping.ClippedVertices.Items;
|
||||
worldVerticesCount = clipping.ClippedVertices.Count / 2;
|
||||
worldTriangleIndices = clipping.ClippedTriangles.Items;
|
||||
worldTriangleIndicesLength = clipping.ClippedTriangles.Count;
|
||||
uvs = clipping.ClippedUVs.Items;
|
||||
}
|
||||
|
||||
var textureSizeX = texture.Size.X;
|
||||
var textureSizeY = texture.Size.Y;
|
||||
|
||||
SFML.Graphics.Vertex vertex = new();
|
||||
vertex.Color.R = (byte)(tintR * 255);
|
||||
vertex.Color.G = (byte)(tintG * 255);
|
||||
vertex.Color.B = (byte)(tintB * 255);
|
||||
vertex.Color.A = (byte)(tintA * 255);
|
||||
|
||||
// 必须用 worldTriangleIndicesLength 不能直接 foreach
|
||||
for (int i = 0; i < worldTriangleIndicesLength; i++)
|
||||
{
|
||||
var index = worldTriangleIndices[i] * 2;
|
||||
vertex.Position.X = worldVertices[index];
|
||||
vertex.Position.Y = worldVertices[index + 1];
|
||||
vertex.TexCoords.X = uvs[index] * textureSizeX;
|
||||
vertex.TexCoords.Y = uvs[index + 1] * textureSizeY;
|
||||
vertexArray.Append(vertex);
|
||||
}
|
||||
|
||||
clipping.ClipEnd(slot);
|
||||
}
|
||||
clipping.ClipEnd();
|
||||
|
||||
// 调试纹理
|
||||
if (!isDebug || debugTexture)
|
||||
target.Draw(vertexArray, states);
|
||||
|
||||
// 包围盒
|
||||
if (isDebug && isSelected && debugBounds)
|
||||
{
|
||||
var b = bounds;
|
||||
boundsVertices[0] = boundsVertices[4] = new(new(b.Left, b.Top), BoundsColor);
|
||||
boundsVertices[1] = new(new(b.Right, b.Top), BoundsColor);
|
||||
boundsVertices[2] = new(new(b.Right, b.Bottom), BoundsColor);
|
||||
boundsVertices[3] = new(new(b.Left, b.Bottom), BoundsColor);
|
||||
target.Draw(boundsVertices);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,328 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using SpineRuntime41;
|
||||
using SpineViewer.Utilities;
|
||||
|
||||
namespace SpineViewer.Spine.Implementations.Spine
|
||||
{
|
||||
[SpineImplementation(SpineVersion.V41)]
|
||||
internal class Spine41 : SpineViewer.Spine.Spine
|
||||
{
|
||||
private static readonly Animation EmptyAnimation = new(EMPTY_ANIMATION, [], 0);
|
||||
|
||||
private class TextureLoader : SpineRuntime41.TextureLoader
|
||||
{
|
||||
public void Load(AtlasPage page, string path)
|
||||
{
|
||||
var texture = new SFML.Graphics.Texture(path);
|
||||
if (page.magFilter == TextureFilter.Linear)
|
||||
texture.Smooth = true;
|
||||
if (page.uWrap == TextureWrap.Repeat && page.vWrap == TextureWrap.Repeat)
|
||||
texture.Repeated = true;
|
||||
|
||||
page.rendererObject = texture;
|
||||
}
|
||||
|
||||
public void Unload(object texture)
|
||||
{
|
||||
((SFML.Graphics.Texture)texture).Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
private static TextureLoader textureLoader = new();
|
||||
|
||||
private Atlas atlas;
|
||||
private SkeletonBinary? skeletonBinary;
|
||||
private SkeletonJson? skeletonJson;
|
||||
private SkeletonData skeletonData;
|
||||
private AnimationStateData animationStateData;
|
||||
|
||||
private Skeleton skeleton;
|
||||
private AnimationState animationState;
|
||||
|
||||
private SkeletonClipping clipping = new();
|
||||
|
||||
public Spine41(string skelPath, string atlasPath) : base(skelPath, atlasPath)
|
||||
{
|
||||
atlas = new Atlas(AtlasPath, textureLoader);
|
||||
try
|
||||
{
|
||||
// 先尝试二进制文件
|
||||
skeletonJson = null;
|
||||
skeletonBinary = new SkeletonBinary(atlas);
|
||||
skeletonData = skeletonBinary.ReadSkeletonData(SkelPath);
|
||||
}
|
||||
catch
|
||||
{
|
||||
try
|
||||
{
|
||||
// 再尝试 Json 文件
|
||||
skeletonBinary = null;
|
||||
skeletonJson = new SkeletonJson(atlas);
|
||||
skeletonData = skeletonJson.ReadSkeletonData(SkelPath);
|
||||
}
|
||||
catch
|
||||
{
|
||||
// 都不行就报错
|
||||
throw new InvalidDataException($"Unknown skeleton file format {SkelPath}");
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var skin in skeletonData.Skins)
|
||||
skinNames.Add(skin.Name);
|
||||
|
||||
foreach (var anime in skeletonData.Animations)
|
||||
animationNames.Add(anime.Name);
|
||||
|
||||
skeleton = new Skeleton(skeletonData) { Skin = new(Guid.NewGuid().ToString()) }; // 挂载一个空皮肤当作容器
|
||||
animationStateData = new AnimationStateData(skeletonData);
|
||||
animationState = new AnimationState(animationStateData);
|
||||
}
|
||||
|
||||
protected override void Dispose(bool disposing)
|
||||
{
|
||||
base.Dispose(disposing);
|
||||
atlas.Dispose();
|
||||
}
|
||||
|
||||
public override string FileVersion { get => skeletonData.Version; }
|
||||
|
||||
protected override float scale
|
||||
{
|
||||
get => Math.Abs(skeleton.ScaleX);
|
||||
set
|
||||
{
|
||||
skeleton.ScaleX = Math.Sign(skeleton.ScaleX) * value;
|
||||
skeleton.ScaleY = Math.Sign(skeleton.ScaleY) * value;
|
||||
}
|
||||
}
|
||||
|
||||
protected override PointF position
|
||||
{
|
||||
get => new(skeleton.X, skeleton.Y);
|
||||
set
|
||||
{
|
||||
skeleton.X = value.X;
|
||||
skeleton.Y = value.Y;
|
||||
}
|
||||
}
|
||||
|
||||
protected override bool flipX
|
||||
{
|
||||
get => skeleton.ScaleX < 0;
|
||||
set
|
||||
{
|
||||
if (skeleton.ScaleX > 0 && value || skeleton.ScaleX < 0 && !value)
|
||||
skeleton.ScaleX *= -1;
|
||||
}
|
||||
}
|
||||
|
||||
protected override bool flipY
|
||||
{
|
||||
get => skeleton.ScaleY < 0;
|
||||
set
|
||||
{
|
||||
if (skeleton.ScaleY > 0 && value || skeleton.ScaleY < 0 && !value)
|
||||
skeleton.ScaleY *= -1;
|
||||
}
|
||||
}
|
||||
|
||||
protected override void addSkin(string name)
|
||||
{
|
||||
if (skeletonData.FindSkin(name) is Skin sk)
|
||||
{
|
||||
skeleton.Skin.AddSkin(sk);
|
||||
skeleton.SetSlotsToSetupPose();
|
||||
}
|
||||
}
|
||||
|
||||
protected override void clearSkin()
|
||||
{
|
||||
skeleton.Skin.Clear();
|
||||
skeleton.SetSlotsToSetupPose();
|
||||
}
|
||||
|
||||
protected override int[] getTrackIndices() => animationState.Tracks.Select((_, i) => i).Where(i => animationState.Tracks.Items[i] is not null).ToArray();
|
||||
|
||||
protected override string getAnimation(int track) => animationState.GetCurrent(track)?.Animation.Name ?? EMPTY_ANIMATION;
|
||||
|
||||
protected override void setAnimation(int track, string name)
|
||||
{
|
||||
if (name == EMPTY_ANIMATION)
|
||||
animationState.SetAnimation(track, EmptyAnimation, false);
|
||||
else if (animationNames.Contains(name))
|
||||
animationState.SetAnimation(track, name, true);
|
||||
}
|
||||
|
||||
protected override void clearTrack(int i) => animationState.ClearTrack(i);
|
||||
|
||||
public override float GetAnimationDuration(string name) { return skeletonData.FindAnimation(name)?.Duration ?? 0f; }
|
||||
|
||||
protected override RectangleF bounds
|
||||
{
|
||||
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 void update(float delta)
|
||||
{
|
||||
animationState.Update(delta);
|
||||
animationState.Apply(skeleton);
|
||||
//skeleton.Update(delta); // XXX: v4.1.x 没有 Update 方法
|
||||
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)
|
||||
{
|
||||
vertexArray.Clear();
|
||||
states.Texture = null;
|
||||
states.Shader = SFMLShader.GetSpineShader(usePma);
|
||||
|
||||
// 要用 DrawOrder 而不是 Slots
|
||||
foreach (var slot in skeleton.DrawOrder)
|
||||
{
|
||||
var attachment = slot.Attachment;
|
||||
|
||||
SFML.Graphics.Texture texture;
|
||||
|
||||
float[] worldVertices = worldVerticesBuffer; // 顶点世界坐标, 连续的 [x0, y0, x1, y1, ...] 坐标值
|
||||
int worldVerticesCount; // 等于顶点数组的长度除以 2
|
||||
int[] worldTriangleIndices; // 三角形索引, 从顶点坐标数组取的时候要乘以 2, 最大值是 worldVerticesCount - 1
|
||||
int worldTriangleIndicesLength; // 三角形索引数组长度
|
||||
float[] uvs; // 纹理坐标
|
||||
float tintR = skeleton.R * slot.R;
|
||||
float tintG = skeleton.G * slot.G;
|
||||
float tintB = skeleton.B * slot.B;
|
||||
float tintA = skeleton.A * slot.A;
|
||||
|
||||
if (attachment is RegionAttachment regionAttachment)
|
||||
{
|
||||
texture = (SFML.Graphics.Texture)((AtlasRegion)regionAttachment.Region).page.rendererObject;
|
||||
|
||||
regionAttachment.ComputeWorldVertices(slot, worldVertices, 0);
|
||||
worldVerticesCount = 4;
|
||||
worldTriangleIndices = [0, 1, 2, 2, 3, 0];
|
||||
worldTriangleIndicesLength = 6;
|
||||
uvs = regionAttachment.UVs;
|
||||
tintR *= regionAttachment.R;
|
||||
tintG *= regionAttachment.G;
|
||||
tintB *= regionAttachment.B;
|
||||
tintA *= regionAttachment.A;
|
||||
}
|
||||
else if (attachment is MeshAttachment meshAttachment)
|
||||
{
|
||||
texture = (SFML.Graphics.Texture)((AtlasRegion)meshAttachment.Region).page.rendererObject;
|
||||
|
||||
if (meshAttachment.WorldVerticesLength > worldVertices.Length)
|
||||
worldVertices = worldVerticesBuffer = new float[meshAttachment.WorldVerticesLength * 2];
|
||||
meshAttachment.ComputeWorldVertices(slot, worldVertices);
|
||||
worldVerticesCount = meshAttachment.WorldVerticesLength / 2;
|
||||
worldTriangleIndices = meshAttachment.Triangles;
|
||||
worldTriangleIndicesLength = meshAttachment.Triangles.Length;
|
||||
uvs = meshAttachment.UVs;
|
||||
tintR *= meshAttachment.R;
|
||||
tintG *= meshAttachment.G;
|
||||
tintB *= meshAttachment.B;
|
||||
tintA *= meshAttachment.A;
|
||||
}
|
||||
else if (attachment is ClippingAttachment clippingAttachment)
|
||||
{
|
||||
clipping.ClipStart(slot, clippingAttachment);
|
||||
continue;
|
||||
}
|
||||
else
|
||||
{
|
||||
clipping.ClipEnd(slot);
|
||||
continue;
|
||||
}
|
||||
|
||||
SFML.Graphics.BlendMode blendMode = GetSFMLBlendMode(slot.Data.BlendMode);
|
||||
|
||||
states.Texture ??= texture;
|
||||
if (states.BlendMode != blendMode || states.Texture != texture)
|
||||
{
|
||||
if (vertexArray.VertexCount > 0)
|
||||
{
|
||||
// 调试纹理
|
||||
if (!isDebug || debugTexture)
|
||||
target.Draw(vertexArray, states);
|
||||
|
||||
vertexArray.Clear();
|
||||
}
|
||||
states.BlendMode = blendMode;
|
||||
states.Texture = texture;
|
||||
}
|
||||
|
||||
if (clipping.IsClipping)
|
||||
{
|
||||
// 这里必须单独记录 Count, 和 Items 的 Length 是不一致的
|
||||
clipping.ClipTriangles(worldVertices, worldVerticesCount * 2, worldTriangleIndices, worldTriangleIndicesLength, uvs);
|
||||
worldVertices = clipping.ClippedVertices.Items;
|
||||
worldVerticesCount = clipping.ClippedVertices.Count / 2;
|
||||
worldTriangleIndices = clipping.ClippedTriangles.Items;
|
||||
worldTriangleIndicesLength = clipping.ClippedTriangles.Count;
|
||||
uvs = clipping.ClippedUVs.Items;
|
||||
}
|
||||
|
||||
var textureSizeX = texture.Size.X;
|
||||
var textureSizeY = texture.Size.Y;
|
||||
|
||||
SFML.Graphics.Vertex vertex = new();
|
||||
vertex.Color.R = (byte)(tintR * 255);
|
||||
vertex.Color.G = (byte)(tintG * 255);
|
||||
vertex.Color.B = (byte)(tintB * 255);
|
||||
vertex.Color.A = (byte)(tintA * 255);
|
||||
|
||||
// 必须用 worldTriangleIndicesLength 不能直接 foreach
|
||||
for (int i = 0; i < worldTriangleIndicesLength; i++)
|
||||
{
|
||||
var index = worldTriangleIndices[i] * 2;
|
||||
vertex.Position.X = worldVertices[index];
|
||||
vertex.Position.Y = worldVertices[index + 1];
|
||||
vertex.TexCoords.X = uvs[index] * textureSizeX;
|
||||
vertex.TexCoords.Y = uvs[index + 1] * textureSizeY;
|
||||
vertexArray.Append(vertex);
|
||||
}
|
||||
|
||||
clipping.ClipEnd(slot);
|
||||
}
|
||||
clipping.ClipEnd();
|
||||
|
||||
// 调试纹理
|
||||
if (!isDebug || debugTexture)
|
||||
target.Draw(vertexArray, states);
|
||||
|
||||
// 包围盒
|
||||
if (isDebug && isSelected && debugBounds)
|
||||
{
|
||||
var b = bounds;
|
||||
boundsVertices[0] = boundsVertices[4] = new(new(b.Left, b.Top), BoundsColor);
|
||||
boundsVertices[1] = new(new(b.Right, b.Top), BoundsColor);
|
||||
boundsVertices[2] = new(new(b.Right, b.Bottom), BoundsColor);
|
||||
boundsVertices[3] = new(new(b.Left, b.Bottom), BoundsColor);
|
||||
target.Draw(boundsVertices);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,328 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using SpineRuntime42;
|
||||
using SpineViewer.Utilities;
|
||||
|
||||
namespace SpineViewer.Spine.Implementations.Spine
|
||||
{
|
||||
[SpineImplementation(SpineVersion.V42)]
|
||||
internal class Spine42 : SpineViewer.Spine.Spine
|
||||
{
|
||||
private static readonly Animation EmptyAnimation = new(EMPTY_ANIMATION, [], 0);
|
||||
|
||||
private class TextureLoader : SpineRuntime42.TextureLoader
|
||||
{
|
||||
public void Load(AtlasPage page, string path)
|
||||
{
|
||||
var texture = new SFML.Graphics.Texture(path);
|
||||
if (page.magFilter == TextureFilter.Linear)
|
||||
texture.Smooth = true;
|
||||
if (page.uWrap == TextureWrap.Repeat && page.vWrap == TextureWrap.Repeat)
|
||||
texture.Repeated = true;
|
||||
|
||||
page.rendererObject = texture;
|
||||
}
|
||||
|
||||
public void Unload(object texture)
|
||||
{
|
||||
((SFML.Graphics.Texture)texture).Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
private static TextureLoader textureLoader = new();
|
||||
|
||||
private Atlas atlas;
|
||||
private SkeletonBinary? skeletonBinary;
|
||||
private SkeletonJson? skeletonJson;
|
||||
private SkeletonData skeletonData;
|
||||
private AnimationStateData animationStateData;
|
||||
|
||||
private Skeleton skeleton;
|
||||
private AnimationState animationState;
|
||||
|
||||
private SkeletonClipping clipping = new();
|
||||
|
||||
public Spine42(string skelPath, string atlasPath) : base(skelPath, atlasPath)
|
||||
{
|
||||
atlas = new Atlas(AtlasPath, textureLoader);
|
||||
try
|
||||
{
|
||||
// 先尝试二进制文件
|
||||
skeletonJson = null;
|
||||
skeletonBinary = new SkeletonBinary(atlas);
|
||||
skeletonData = skeletonBinary.ReadSkeletonData(SkelPath);
|
||||
}
|
||||
catch
|
||||
{
|
||||
try
|
||||
{
|
||||
// 再尝试 Json 文件
|
||||
skeletonBinary = null;
|
||||
skeletonJson = new SkeletonJson(atlas);
|
||||
skeletonData = skeletonJson.ReadSkeletonData(SkelPath);
|
||||
}
|
||||
catch
|
||||
{
|
||||
// 都不行就报错
|
||||
throw new InvalidDataException($"Unknown skeleton file format {SkelPath}");
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var skin in skeletonData.Skins)
|
||||
skinNames.Add(skin.Name);
|
||||
|
||||
foreach (var anime in skeletonData.Animations)
|
||||
animationNames.Add(anime.Name);
|
||||
|
||||
skeleton = new Skeleton(skeletonData) { Skin = new(Guid.NewGuid().ToString()) }; // 挂载一个空皮肤当作容器
|
||||
animationStateData = new AnimationStateData(skeletonData);
|
||||
animationState = new AnimationState(animationStateData);
|
||||
}
|
||||
|
||||
protected override void Dispose(bool disposing)
|
||||
{
|
||||
base.Dispose(disposing);
|
||||
atlas.Dispose();
|
||||
}
|
||||
|
||||
public override string FileVersion { get => skeletonData.Version; }
|
||||
|
||||
protected override float scale
|
||||
{
|
||||
get => Math.Abs(skeleton.ScaleX);
|
||||
set
|
||||
{
|
||||
skeleton.ScaleX = Math.Sign(skeleton.ScaleX) * value;
|
||||
skeleton.ScaleY = Math.Sign(skeleton.ScaleY) * value;
|
||||
}
|
||||
}
|
||||
|
||||
protected override PointF position
|
||||
{
|
||||
get => new(skeleton.X, skeleton.Y);
|
||||
set
|
||||
{
|
||||
skeleton.X = value.X;
|
||||
skeleton.Y = value.Y;
|
||||
}
|
||||
}
|
||||
|
||||
protected override bool flipX
|
||||
{
|
||||
get => skeleton.ScaleX < 0;
|
||||
set
|
||||
{
|
||||
if (skeleton.ScaleX > 0 && value || skeleton.ScaleX < 0 && !value)
|
||||
skeleton.ScaleX *= -1;
|
||||
}
|
||||
}
|
||||
|
||||
protected override bool flipY
|
||||
{
|
||||
get => skeleton.ScaleY < 0;
|
||||
set
|
||||
{
|
||||
if (skeleton.ScaleY > 0 && value || skeleton.ScaleY < 0 && !value)
|
||||
skeleton.ScaleY *= -1;
|
||||
}
|
||||
}
|
||||
|
||||
protected override void addSkin(string name)
|
||||
{
|
||||
if (skeletonData.FindSkin(name) is Skin sk)
|
||||
{
|
||||
skeleton.Skin.AddSkin(sk);
|
||||
skeleton.SetSlotsToSetupPose();
|
||||
}
|
||||
}
|
||||
|
||||
protected override void clearSkin()
|
||||
{
|
||||
skeleton.Skin.Clear();
|
||||
skeleton.SetSlotsToSetupPose();
|
||||
}
|
||||
|
||||
protected override int[] getTrackIndices() => animationState.Tracks.Select((_, i) => i).Where(i => animationState.Tracks.Items[i] is not null).ToArray();
|
||||
|
||||
protected override string getAnimation(int track) => animationState.GetCurrent(track)?.Animation.Name ?? EMPTY_ANIMATION;
|
||||
|
||||
protected override void setAnimation(int track, string name)
|
||||
{
|
||||
if (name == EMPTY_ANIMATION)
|
||||
animationState.SetAnimation(track, EmptyAnimation, false);
|
||||
else if (animationNames.Contains(name))
|
||||
animationState.SetAnimation(track, name, true);
|
||||
}
|
||||
|
||||
protected override void clearTrack(int i) => animationState.ClearTrack(i);
|
||||
|
||||
public override float GetAnimationDuration(string name) { return skeletonData.FindAnimation(name)?.Duration ?? 0f; }
|
||||
|
||||
protected override RectangleF bounds
|
||||
{
|
||||
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 void update(float delta)
|
||||
{
|
||||
animationState.Update(delta);
|
||||
animationState.Apply(skeleton);
|
||||
skeleton.Update(delta);
|
||||
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)
|
||||
{
|
||||
vertexArray.Clear();
|
||||
states.Texture = null;
|
||||
states.Shader = SFMLShader.GetSpineShader(usePma);
|
||||
|
||||
// 要用 DrawOrder 而不是 Slots
|
||||
foreach (var slot in skeleton.DrawOrder)
|
||||
{
|
||||
var attachment = slot.Attachment;
|
||||
|
||||
SFML.Graphics.Texture texture;
|
||||
|
||||
float[] worldVertices = worldVerticesBuffer; // 顶点世界坐标, 连续的 [x0, y0, x1, y1, ...] 坐标值
|
||||
int worldVerticesCount; // 等于顶点数组的长度除以 2
|
||||
int[] worldTriangleIndices; // 三角形索引, 从顶点坐标数组取的时候要乘以 2, 最大值是 worldVerticesCount - 1
|
||||
int worldTriangleIndicesLength; // 三角形索引数组长度
|
||||
float[] uvs; // 纹理坐标
|
||||
float tintR = skeleton.R * slot.R;
|
||||
float tintG = skeleton.G * slot.G;
|
||||
float tintB = skeleton.B * slot.B;
|
||||
float tintA = skeleton.A * slot.A;
|
||||
|
||||
if (attachment is RegionAttachment regionAttachment)
|
||||
{
|
||||
texture = (SFML.Graphics.Texture)((AtlasRegion)regionAttachment.Region).page.rendererObject;
|
||||
|
||||
regionAttachment.ComputeWorldVertices(slot, worldVertices, 0);
|
||||
worldVerticesCount = 4;
|
||||
worldTriangleIndices = [0, 1, 2, 2, 3, 0];
|
||||
worldTriangleIndicesLength = 6;
|
||||
uvs = regionAttachment.UVs;
|
||||
tintR *= regionAttachment.R;
|
||||
tintG *= regionAttachment.G;
|
||||
tintB *= regionAttachment.B;
|
||||
tintA *= regionAttachment.A;
|
||||
}
|
||||
else if (attachment is MeshAttachment meshAttachment)
|
||||
{
|
||||
texture = (SFML.Graphics.Texture)((AtlasRegion)meshAttachment.Region).page.rendererObject;
|
||||
|
||||
if (meshAttachment.WorldVerticesLength > worldVertices.Length)
|
||||
worldVertices = worldVerticesBuffer = new float[meshAttachment.WorldVerticesLength * 2];
|
||||
meshAttachment.ComputeWorldVertices(slot, worldVertices);
|
||||
worldVerticesCount = meshAttachment.WorldVerticesLength / 2;
|
||||
worldTriangleIndices = meshAttachment.Triangles;
|
||||
worldTriangleIndicesLength = meshAttachment.Triangles.Length;
|
||||
uvs = meshAttachment.UVs;
|
||||
tintR *= meshAttachment.R;
|
||||
tintG *= meshAttachment.G;
|
||||
tintB *= meshAttachment.B;
|
||||
tintA *= meshAttachment.A;
|
||||
}
|
||||
else if (attachment is ClippingAttachment clippingAttachment)
|
||||
{
|
||||
clipping.ClipStart(slot, clippingAttachment);
|
||||
continue;
|
||||
}
|
||||
else
|
||||
{
|
||||
clipping.ClipEnd(slot);
|
||||
continue;
|
||||
}
|
||||
|
||||
SFML.Graphics.BlendMode blendMode = GetSFMLBlendMode(slot.Data.BlendMode);
|
||||
|
||||
states.Texture ??= texture;
|
||||
if (states.BlendMode != blendMode || states.Texture != texture)
|
||||
{
|
||||
if (vertexArray.VertexCount > 0)
|
||||
{
|
||||
// 调试纹理
|
||||
if (!isDebug || debugTexture)
|
||||
target.Draw(vertexArray, states);
|
||||
|
||||
vertexArray.Clear();
|
||||
}
|
||||
states.BlendMode = blendMode;
|
||||
states.Texture = texture;
|
||||
}
|
||||
|
||||
if (clipping.IsClipping)
|
||||
{
|
||||
// 这里必须单独记录 Count, 和 Items 的 Length 是不一致的
|
||||
clipping.ClipTriangles(worldVertices, worldTriangleIndices, worldTriangleIndicesLength, uvs);
|
||||
worldVertices = clipping.ClippedVertices.Items;
|
||||
worldVerticesCount = clipping.ClippedVertices.Count / 2;
|
||||
worldTriangleIndices = clipping.ClippedTriangles.Items;
|
||||
worldTriangleIndicesLength = clipping.ClippedTriangles.Count;
|
||||
uvs = clipping.ClippedUVs.Items;
|
||||
}
|
||||
|
||||
var textureSizeX = texture.Size.X;
|
||||
var textureSizeY = texture.Size.Y;
|
||||
|
||||
SFML.Graphics.Vertex vertex = new();
|
||||
vertex.Color.R = (byte)(tintR * 255);
|
||||
vertex.Color.G = (byte)(tintG * 255);
|
||||
vertex.Color.B = (byte)(tintB * 255);
|
||||
vertex.Color.A = (byte)(tintA * 255);
|
||||
|
||||
// 必须用 worldTriangleIndicesLength 不能直接 foreach
|
||||
for (int i = 0; i < worldTriangleIndicesLength; i++)
|
||||
{
|
||||
var index = worldTriangleIndices[i] * 2;
|
||||
vertex.Position.X = worldVertices[index];
|
||||
vertex.Position.Y = worldVertices[index + 1];
|
||||
vertex.TexCoords.X = uvs[index] * textureSizeX;
|
||||
vertex.TexCoords.Y = uvs[index + 1] * textureSizeY;
|
||||
vertexArray.Append(vertex);
|
||||
}
|
||||
|
||||
clipping.ClipEnd(slot);
|
||||
}
|
||||
clipping.ClipEnd();
|
||||
|
||||
// 调试纹理
|
||||
if (!isDebug || debugTexture)
|
||||
target.Draw(vertexArray, states);
|
||||
|
||||
// 包围盒
|
||||
if (isDebug && isSelected && debugBounds)
|
||||
{
|
||||
var b = bounds;
|
||||
boundsVertices[0] = boundsVertices[4] = new(new(b.Left, b.Top), BoundsColor);
|
||||
boundsVertices[1] = new(new(b.Right, b.Top), BoundsColor);
|
||||
boundsVertices[2] = new(new(b.Right, b.Bottom), BoundsColor);
|
||||
boundsVertices[3] = new(new(b.Left, b.Bottom), BoundsColor);
|
||||
target.Draw(boundsVertices);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,20 +1,33 @@
|
||||
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.Utilities;
|
||||
using SpineViewer.Extensions;
|
||||
using SpineViewer.Utils;
|
||||
|
||||
namespace SpineViewer.Spine.Implementations.Spine
|
||||
namespace SpineViewer.Spine.Implementations.SpineObject
|
||||
{
|
||||
[SpineImplementation(SpineVersion.V21)]
|
||||
internal class Spine21 : SpineViewer.Spine.Spine
|
||||
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.Spine
|
||||
((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,7 +63,12 @@ namespace SpineViewer.Spine.Implementations.Spine
|
||||
// 2.1.x 不支持剪裁
|
||||
//private SkeletonClipping clipping = new();
|
||||
|
||||
public Spine21(string skelPath, string atlasPath) : base(skelPath, atlasPath)
|
||||
/// <summary>
|
||||
/// 所有插槽在所有皮肤中可用的附件集合
|
||||
/// </summary>
|
||||
private readonly Dictionary<string, Dictionary<string, Attachment>> slotAttachments = [];
|
||||
|
||||
public SpineObject21(string skelPath, string atlasPath) : base(skelPath, atlasPath)
|
||||
{
|
||||
atlas = new Atlas(AtlasPath, textureLoader);
|
||||
try
|
||||
@@ -74,13 +94,21 @@ namespace SpineViewer.Spine.Implementations.Spine
|
||||
}
|
||||
}
|
||||
|
||||
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).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.Spine
|
||||
}
|
||||
|
||||
// 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,31 @@ namespace SpineViewer.Spine.Implementations.Spine
|
||||
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
|
||||
// default 不需要加载
|
||||
if (name != "default" && 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 +223,7 @@ namespace SpineViewer.Spine.Implementations.Spine
|
||||
{
|
||||
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 +231,53 @@ namespace SpineViewer.Spine.Implementations.Spine
|
||||
|
||||
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,21 +288,9 @@ namespace SpineViewer.Spine.Implementations.Spine
|
||||
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)
|
||||
{
|
||||
vertexArray.Clear();
|
||||
triangleVertices.Clear();
|
||||
states.Texture = null;
|
||||
states.Shader = SFMLShader.GetSpineShader(usePma);
|
||||
|
||||
@@ -329,13 +359,10 @@ namespace SpineViewer.Spine.Implementations.Spine
|
||||
states.Texture ??= texture;
|
||||
if (states.BlendMode != blendMode || states.Texture != texture)
|
||||
{
|
||||
if (vertexArray.VertexCount > 0)
|
||||
if (triangleVertices.VertexCount > 0)
|
||||
{
|
||||
// 调试纹理
|
||||
if (!isDebug || debugTexture)
|
||||
target.Draw(vertexArray, states);
|
||||
|
||||
vertexArray.Clear();
|
||||
target.Draw(triangleVertices, states);
|
||||
triangleVertices.Clear();
|
||||
}
|
||||
states.BlendMode = blendMode;
|
||||
states.Texture = texture;
|
||||
@@ -369,26 +396,190 @@ namespace SpineViewer.Spine.Implementations.Spine
|
||||
vertex.Position.Y = worldVertices[index + 1];
|
||||
vertex.TexCoords.X = uvs[index] * textureSizeX;
|
||||
vertex.TexCoords.Y = uvs[index + 1] * textureSizeY;
|
||||
vertexArray.Append(vertex);
|
||||
triangleVertices.Append(vertex);
|
||||
}
|
||||
|
||||
//clipping.ClipEnd(slot);
|
||||
}
|
||||
//clipping.ClipEnd();
|
||||
|
||||
// 调试纹理
|
||||
if (!isDebug || debugTexture)
|
||||
target.Draw(vertexArray, states);
|
||||
target.Draw(triangleVertices, states);
|
||||
}
|
||||
|
||||
// 包围盒
|
||||
if (isDebug && isSelected && debugBounds)
|
||||
protected override void debugDraw(SFML.Graphics.RenderTarget target)
|
||||
{
|
||||
lineVertices.Clear();
|
||||
rectLineVertices.Clear();
|
||||
|
||||
if (debugRegions)
|
||||
{
|
||||
var b = bounds;
|
||||
boundsVertices[0] = boundsVertices[4] = new(new(b.Left, b.Top), BoundsColor);
|
||||
boundsVertices[1] = new(new(b.Right, b.Top), BoundsColor);
|
||||
boundsVertices[2] = new(new(b.Right, b.Bottom), BoundsColor);
|
||||
boundsVertices[3] = new(new(b.Left, b.Bottom), BoundsColor);
|
||||
target.Draw(boundsVertices);
|
||||
SFML.Graphics.Vertex vt = new() { Color = AttachmentLineColor };
|
||||
foreach (var slot in skeleton.Slots)
|
||||
{
|
||||
if (slot.Attachment is RegionAttachment regionAttachment)
|
||||
{
|
||||
regionAttachment.ComputeWorldVertices(slot.Bone, worldVerticesBuffer);
|
||||
|
||||
vt.Position.X = worldVerticesBuffer[0];
|
||||
vt.Position.Y = worldVerticesBuffer[1];
|
||||
lineVertices.Append(vt);
|
||||
|
||||
vt.Position.X = worldVerticesBuffer[2];
|
||||
vt.Position.Y = worldVerticesBuffer[3];
|
||||
lineVertices.Append(vt); lineVertices.Append(vt);
|
||||
|
||||
vt.Position.X = worldVerticesBuffer[4];
|
||||
vt.Position.Y = worldVerticesBuffer[5];
|
||||
lineVertices.Append(vt); lineVertices.Append(vt);
|
||||
|
||||
vt.Position.X = worldVerticesBuffer[6];
|
||||
vt.Position.Y = worldVerticesBuffer[7];
|
||||
lineVertices.Append(vt); lineVertices.Append(vt);
|
||||
|
||||
vt.Position.X = worldVerticesBuffer[0];
|
||||
vt.Position.Y = worldVerticesBuffer[1];
|
||||
lineVertices.Append(vt);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (debugMeshes)
|
||||
{
|
||||
SFML.Graphics.Vertex vt = new() { Color = MeshLineColor };
|
||||
foreach (var slot in skeleton.Slots)
|
||||
{
|
||||
if (slot.Attachment is MeshAttachment meshAttachment)
|
||||
{
|
||||
if (meshAttachment.Vertices.Length > worldVerticesBuffer.Length)
|
||||
worldVerticesBuffer = new float[meshAttachment.Vertices.Length * 2];
|
||||
|
||||
meshAttachment.ComputeWorldVertices(slot, worldVerticesBuffer);
|
||||
|
||||
var triangleIndices = meshAttachment.Triangles;
|
||||
for (int i = 0; i < triangleIndices.Length; i += 3)
|
||||
{
|
||||
var idx0 = triangleIndices[i] * 2;
|
||||
var idx1 = triangleIndices[i + 1] * 2;
|
||||
var idx2 = triangleIndices[i + 2] * 2;
|
||||
|
||||
vt.Position.X = worldVerticesBuffer[idx0];
|
||||
vt.Position.Y = worldVerticesBuffer[idx0 + 1];
|
||||
lineVertices.Append(vt);
|
||||
|
||||
vt.Position.X = worldVerticesBuffer[idx1];
|
||||
vt.Position.Y = worldVerticesBuffer[idx1 + 1];
|
||||
lineVertices.Append(vt); lineVertices.Append(vt);
|
||||
|
||||
vt.Position.X = worldVerticesBuffer[idx2];
|
||||
vt.Position.Y = worldVerticesBuffer[idx2 + 1];
|
||||
lineVertices.Append(vt); lineVertices.Append(vt);
|
||||
|
||||
vt.Position.X = worldVerticesBuffer[idx0];
|
||||
vt.Position.Y = worldVerticesBuffer[idx0 + 1];
|
||||
lineVertices.Append(vt);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (debugMeshHulls)
|
||||
{
|
||||
SFML.Graphics.Vertex vt = new() { Color = AttachmentLineColor };
|
||||
foreach (var slot in skeleton.Slots)
|
||||
{
|
||||
if (slot.Attachment is MeshAttachment meshAttachment)
|
||||
{
|
||||
if (meshAttachment.Vertices.Length > worldVerticesBuffer.Length)
|
||||
worldVerticesBuffer = new float[meshAttachment.Vertices.Length * 2];
|
||||
|
||||
meshAttachment.ComputeWorldVertices(slot, worldVerticesBuffer);
|
||||
|
||||
var hullLength = (meshAttachment.HullLength >> 1) << 1;
|
||||
|
||||
if (debugMeshHulls && hullLength > 2)
|
||||
{
|
||||
vt.Position.X = worldVerticesBuffer[0];
|
||||
vt.Position.Y = worldVerticesBuffer[1];
|
||||
lineVertices.Append(vt);
|
||||
|
||||
for (int i = 2; i < hullLength; i += 2)
|
||||
{
|
||||
vt.Position.X = worldVerticesBuffer[i];
|
||||
vt.Position.Y = worldVerticesBuffer[i + 1];
|
||||
lineVertices.Append(vt);
|
||||
lineVertices.Append(vt);
|
||||
}
|
||||
|
||||
vt.Position.X = worldVerticesBuffer[0];
|
||||
vt.Position.Y = worldVerticesBuffer[1];
|
||||
lineVertices.Append(vt);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (debugBoundingBoxes)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
if (debugPaths)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
if (debugClippings) { } // 没有剪裁附件
|
||||
|
||||
if (debugBounds)
|
||||
{
|
||||
var vt = new SFML.Graphics.Vertex() { Color = BoundsColor };
|
||||
var b = getCurrentBounds();
|
||||
|
||||
vt.Position.X = b.Left;
|
||||
vt.Position.Y = b.Top;
|
||||
lineVertices.Append(vt);
|
||||
|
||||
vt.Position.X = b.Right;
|
||||
vt.Position.Y = b.Top;
|
||||
lineVertices.Append(vt); lineVertices.Append(vt);
|
||||
|
||||
vt.Position.X = b.Right;
|
||||
vt.Position.Y = b.Bottom;
|
||||
lineVertices.Append(vt); lineVertices.Append(vt);
|
||||
|
||||
vt.Position.X = b.Left;
|
||||
vt.Position.Y = b.Bottom;
|
||||
lineVertices.Append(vt); lineVertices.Append(vt);
|
||||
|
||||
vt.Position.X = b.Left;
|
||||
vt.Position.Y = b.Top;
|
||||
lineVertices.Append(vt);
|
||||
}
|
||||
|
||||
// 骨骼线放最后画
|
||||
if (debugBones)
|
||||
{
|
||||
var width = scale;
|
||||
foreach (var bone in skeleton.Bones)
|
||||
{
|
||||
var boneLength = bone.Data.Length;
|
||||
var p1 = new SFML.System.Vector2f(bone.WorldX, bone.WorldY);
|
||||
var p2 = new SFML.System.Vector2f(bone.WorldX + boneLength * bone.M00, bone.WorldY + boneLength * bone.M10);
|
||||
AddRectLine(p1, p2, BoneLineColor, width);
|
||||
}
|
||||
}
|
||||
|
||||
target.Draw(lineVertices);
|
||||
target.Draw(rectLineVertices);
|
||||
|
||||
// 骨骼的点最后画, 层级处于骨骼线上面
|
||||
if (debugBones)
|
||||
{
|
||||
var radius = scale;
|
||||
foreach (var bone in skeleton.Bones)
|
||||
{
|
||||
DrawCirclePoint(target, new(bone.WorldX, bone.WorldY), BonePointColor, radius);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
587
SpineViewer/Spine/Implementations/SpineObject/SpineObject36.cs
Normal file
587
SpineViewer/Spine/Implementations/SpineObject/SpineObject36.cs
Normal file
@@ -0,0 +1,587 @@
|
||||
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
|
||||
{
|
||||
[SpineImplementation(SpineVersion.V36)]
|
||||
internal class SpineObject36 : Spine.SpineObject
|
||||
{
|
||||
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
|
||||
{
|
||||
public void Load(AtlasPage page, string path)
|
||||
{
|
||||
var texture = new SFML.Graphics.Texture(path);
|
||||
if (page.magFilter == TextureFilter.Linear)
|
||||
texture.Smooth = true;
|
||||
if (page.uWrap == TextureWrap.Repeat && page.vWrap == TextureWrap.Repeat)
|
||||
texture.Repeated = true;
|
||||
|
||||
page.rendererObject = texture;
|
||||
}
|
||||
|
||||
public void Unload(object texture)
|
||||
{
|
||||
((SFML.Graphics.Texture)texture).Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
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 readonly SkeletonClipping clipping = new();
|
||||
|
||||
/// <summary>
|
||||
/// 所有插槽在所有皮肤中可用的附件集合
|
||||
/// </summary>
|
||||
private readonly Dictionary<string, Dictionary<string, Attachment>> slotAttachments = [];
|
||||
|
||||
public SpineObject36(string skelPath, string atlasPath) : base(skelPath, atlasPath)
|
||||
{
|
||||
atlas = new Atlas(AtlasPath, textureLoader);
|
||||
try
|
||||
{
|
||||
// 先尝试二进制文件
|
||||
skeletonJson = null;
|
||||
skeletonBinary = new SkeletonBinary(atlas);
|
||||
skeletonData = skeletonBinary.ReadSkeletonData(SkelPath);
|
||||
}
|
||||
catch
|
||||
{
|
||||
try
|
||||
{
|
||||
// 再尝试 Json 文件
|
||||
skeletonBinary = null;
|
||||
skeletonJson = new SkeletonJson(atlas);
|
||||
skeletonData = skeletonJson.ReadSkeletonData(SkelPath);
|
||||
}
|
||||
catch
|
||||
{
|
||||
// 都不行就报错
|
||||
throw new InvalidDataException($"Unknown skeleton file format {SkelPath}");
|
||||
}
|
||||
}
|
||||
|
||||
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).ToImmutableArray();
|
||||
AnimationNames = [EMPTY_ANIMATION, .. skeletonData.Animations.Select(v => v.Name)];
|
||||
|
||||
skeleton = new Skeleton(skeletonData) { Skin = new(Guid.NewGuid().ToString()) }; // 挂载一个空皮肤当作容器
|
||||
animationStateData = new AnimationStateData(skeletonData);
|
||||
animationState = new AnimationState(animationStateData);
|
||||
}
|
||||
|
||||
protected override void Dispose(bool disposing)
|
||||
{
|
||||
base.Dispose(disposing);
|
||||
atlas.Dispose();
|
||||
}
|
||||
|
||||
public override string FileVersion { get => skeletonData.Version; }
|
||||
|
||||
protected override float scale
|
||||
{
|
||||
get => Math.Abs(skeleton.ScaleX);
|
||||
set
|
||||
{
|
||||
skeleton.ScaleX = Math.Sign(skeleton.ScaleX) * value;
|
||||
skeleton.ScaleY = Math.Sign(skeleton.ScaleY) * value;
|
||||
}
|
||||
}
|
||||
|
||||
protected override PointF position
|
||||
{
|
||||
get => new(skeleton.X, skeleton.Y);
|
||||
set
|
||||
{
|
||||
skeleton.X = value.X;
|
||||
skeleton.Y = value.Y;
|
||||
}
|
||||
}
|
||||
|
||||
protected override bool flipX
|
||||
{
|
||||
get => skeleton.ScaleX < 0;
|
||||
set
|
||||
{
|
||||
if (skeleton.ScaleX > 0 && value || skeleton.ScaleX < 0 && !value)
|
||||
skeleton.ScaleX *= -1;
|
||||
}
|
||||
}
|
||||
|
||||
protected override bool flipY
|
||||
{
|
||||
get => skeleton.ScaleY < 0;
|
||||
set
|
||||
{
|
||||
if (skeleton.ScaleY > 0 && value || skeleton.ScaleY < 0 && !value)
|
||||
skeleton.ScaleY *= -1;
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
{
|
||||
// default 不需要加载
|
||||
if (name != "default" && 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 clearSkins()
|
||||
{
|
||||
skeleton.Skin.Attachments.Clear();
|
||||
skeleton.SetSlotsToSetupPose();
|
||||
}
|
||||
|
||||
protected override int[] getTrackIndices() => animationState.Tracks.Select((_, i) => i).Where(i => animationState.Tracks.Items[i] is not null).ToArray();
|
||||
|
||||
protected override string getAnimation(int track) => animationState.GetCurrent(track)?.Animation.Name ?? EMPTY_ANIMATION;
|
||||
|
||||
protected override void setAnimation(int track, string name)
|
||||
{
|
||||
if (name == EMPTY_ANIMATION)
|
||||
animationState.SetAnimation(track, EmptyAnimation, false);
|
||||
else if (AnimationNames.Contains(name))
|
||||
animationState.SetAnimation(track, name, true);
|
||||
}
|
||||
|
||||
protected override void clearTrack(int i) => animationState.ClearTrack(i);
|
||||
|
||||
public override float GetAnimationDuration(string name) { return skeletonData.FindAnimation(name)?.Duration ?? 0f; }
|
||||
|
||||
protected override RectangleF getCurrentBounds()
|
||||
{
|
||||
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))
|
||||
{
|
||||
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)
|
||||
{
|
||||
animationState.Update(delta);
|
||||
animationState.Apply(skeleton);
|
||||
skeleton.Update(delta);
|
||||
skeleton.UpdateWorldTransform();
|
||||
}
|
||||
|
||||
protected override void draw(SFML.Graphics.RenderTarget target, SFML.Graphics.RenderStates states)
|
||||
{
|
||||
triangleVertices.Clear();
|
||||
states.Texture = null;
|
||||
states.Shader = SFMLShader.GetSpineShader(usePma);
|
||||
|
||||
// 要用 DrawOrder 而不是 Slots
|
||||
foreach (var slot in skeleton.DrawOrder)
|
||||
{
|
||||
var attachment = slot.Attachment;
|
||||
|
||||
SFML.Graphics.Texture texture;
|
||||
|
||||
float[] worldVertices = worldVerticesBuffer; // 顶点世界坐标, 连续的 [x0, y0, x1, y1, ...] 坐标值
|
||||
int worldVerticesCount; // 等于顶点数组的长度除以 2
|
||||
int[] worldTriangleIndices; // 三角形索引, 从顶点坐标数组取的时候要乘以 2, 最大值是 worldVerticesCount - 1
|
||||
int worldTriangleIndicesLength; // 三角形索引数组长度
|
||||
float[] uvs; // 纹理坐标
|
||||
float tintR = skeleton.R * slot.R;
|
||||
float tintG = skeleton.G * slot.G;
|
||||
float tintB = skeleton.B * slot.B;
|
||||
float tintA = skeleton.A * slot.A;
|
||||
|
||||
if (attachment is RegionAttachment regionAttachment)
|
||||
{
|
||||
texture = (SFML.Graphics.Texture)((AtlasRegion)regionAttachment.RendererObject).page.rendererObject;
|
||||
|
||||
regionAttachment.ComputeWorldVertices(slot.Bone, worldVertices, 0);
|
||||
worldVerticesCount = 4;
|
||||
worldTriangleIndices = [0, 1, 2, 2, 3, 0];
|
||||
worldTriangleIndicesLength = 6;
|
||||
uvs = regionAttachment.UVs;
|
||||
tintR *= regionAttachment.R;
|
||||
tintG *= regionAttachment.G;
|
||||
tintB *= regionAttachment.B;
|
||||
tintA *= regionAttachment.A;
|
||||
}
|
||||
else if (attachment is MeshAttachment meshAttachment)
|
||||
{
|
||||
texture = (SFML.Graphics.Texture)((AtlasRegion)meshAttachment.RendererObject).page.rendererObject;
|
||||
|
||||
if (meshAttachment.WorldVerticesLength > worldVertices.Length)
|
||||
worldVertices = worldVerticesBuffer = new float[meshAttachment.WorldVerticesLength * 2];
|
||||
meshAttachment.ComputeWorldVertices(slot, worldVertices);
|
||||
worldVerticesCount = meshAttachment.WorldVerticesLength / 2;
|
||||
worldTriangleIndices = meshAttachment.Triangles;
|
||||
worldTriangleIndicesLength = meshAttachment.Triangles.Length;
|
||||
uvs = meshAttachment.UVs;
|
||||
tintR *= meshAttachment.R;
|
||||
tintG *= meshAttachment.G;
|
||||
tintB *= meshAttachment.B;
|
||||
tintA *= meshAttachment.A;
|
||||
}
|
||||
else if (attachment is ClippingAttachment clippingAttachment)
|
||||
{
|
||||
clipping.ClipStart(slot, clippingAttachment);
|
||||
continue;
|
||||
}
|
||||
else
|
||||
{
|
||||
clipping.ClipEnd(slot);
|
||||
continue;
|
||||
}
|
||||
|
||||
SFML.Graphics.BlendMode blendMode = GetSFMLBlendMode(slot.Data.BlendMode);
|
||||
|
||||
states.Texture ??= texture;
|
||||
if (states.BlendMode != blendMode || states.Texture != texture)
|
||||
{
|
||||
if (triangleVertices.VertexCount > 0)
|
||||
{
|
||||
target.Draw(triangleVertices, states);
|
||||
triangleVertices.Clear();
|
||||
}
|
||||
states.BlendMode = blendMode;
|
||||
states.Texture = texture;
|
||||
}
|
||||
|
||||
if (clipping.IsClipping)
|
||||
{
|
||||
// 这里必须单独记录 Count, 和 Items 的 Length 是不一致的
|
||||
clipping.ClipTriangles(worldVertices, worldVerticesCount * 2, worldTriangleIndices, worldTriangleIndicesLength, uvs);
|
||||
worldVertices = clipping.ClippedVertices.Items;
|
||||
worldVerticesCount = clipping.ClippedVertices.Count / 2;
|
||||
worldTriangleIndices = clipping.ClippedTriangles.Items;
|
||||
worldTriangleIndicesLength = clipping.ClippedTriangles.Count;
|
||||
uvs = clipping.ClippedUVs.Items;
|
||||
}
|
||||
|
||||
var textureSizeX = texture.Size.X;
|
||||
var textureSizeY = texture.Size.Y;
|
||||
|
||||
SFML.Graphics.Vertex vertex = new();
|
||||
vertex.Color.R = (byte)(tintR * 255);
|
||||
vertex.Color.G = (byte)(tintG * 255);
|
||||
vertex.Color.B = (byte)(tintB * 255);
|
||||
vertex.Color.A = (byte)(tintA * 255);
|
||||
|
||||
// 必须用 worldTriangleIndicesLength 不能直接 foreach
|
||||
for (int i = 0; i < worldTriangleIndicesLength; i++)
|
||||
{
|
||||
var index = worldTriangleIndices[i] * 2;
|
||||
vertex.Position.X = worldVertices[index];
|
||||
vertex.Position.Y = worldVertices[index + 1];
|
||||
vertex.TexCoords.X = uvs[index] * textureSizeX;
|
||||
vertex.TexCoords.Y = uvs[index + 1] * textureSizeY;
|
||||
triangleVertices.Append(vertex);
|
||||
}
|
||||
|
||||
clipping.ClipEnd(slot);
|
||||
}
|
||||
clipping.ClipEnd();
|
||||
|
||||
target.Draw(triangleVertices, states);
|
||||
}
|
||||
|
||||
protected override void debugDraw(SFML.Graphics.RenderTarget target)
|
||||
{
|
||||
lineVertices.Clear();
|
||||
rectLineVertices.Clear();
|
||||
|
||||
if (debugRegions)
|
||||
{
|
||||
SFML.Graphics.Vertex vt = new() { Color = AttachmentLineColor };
|
||||
foreach (var slot in skeleton.Slots)
|
||||
{
|
||||
if (slot.Attachment is RegionAttachment regionAttachment)
|
||||
{
|
||||
regionAttachment.ComputeWorldVertices(slot.Bone, worldVerticesBuffer, 0);
|
||||
|
||||
vt.Position.X = worldVerticesBuffer[0];
|
||||
vt.Position.Y = worldVerticesBuffer[1];
|
||||
lineVertices.Append(vt);
|
||||
|
||||
vt.Position.X = worldVerticesBuffer[2];
|
||||
vt.Position.Y = worldVerticesBuffer[3];
|
||||
lineVertices.Append(vt); lineVertices.Append(vt);
|
||||
|
||||
vt.Position.X = worldVerticesBuffer[4];
|
||||
vt.Position.Y = worldVerticesBuffer[5];
|
||||
lineVertices.Append(vt); lineVertices.Append(vt);
|
||||
|
||||
vt.Position.X = worldVerticesBuffer[6];
|
||||
vt.Position.Y = worldVerticesBuffer[7];
|
||||
lineVertices.Append(vt); lineVertices.Append(vt);
|
||||
|
||||
vt.Position.X = worldVerticesBuffer[0];
|
||||
vt.Position.Y = worldVerticesBuffer[1];
|
||||
lineVertices.Append(vt);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (debugMeshes)
|
||||
{
|
||||
SFML.Graphics.Vertex vt = new() { Color = MeshLineColor };
|
||||
foreach (var slot in skeleton.Slots)
|
||||
{
|
||||
if (slot.Attachment is MeshAttachment meshAttachment)
|
||||
{
|
||||
if (meshAttachment.WorldVerticesLength > worldVerticesBuffer.Length)
|
||||
worldVerticesBuffer = new float[meshAttachment.WorldVerticesLength * 2];
|
||||
|
||||
meshAttachment.ComputeWorldVertices(slot, worldVerticesBuffer);
|
||||
|
||||
var triangleIndices = meshAttachment.Triangles;
|
||||
for (int i = 0; i < triangleIndices.Length; i += 3)
|
||||
{
|
||||
var idx0 = triangleIndices[i] * 2;
|
||||
var idx1 = triangleIndices[i + 1] * 2;
|
||||
var idx2 = triangleIndices[i + 2] * 2;
|
||||
|
||||
vt.Position.X = worldVerticesBuffer[idx0];
|
||||
vt.Position.Y = worldVerticesBuffer[idx0 + 1];
|
||||
lineVertices.Append(vt);
|
||||
|
||||
vt.Position.X = worldVerticesBuffer[idx1];
|
||||
vt.Position.Y = worldVerticesBuffer[idx1 + 1];
|
||||
lineVertices.Append(vt); lineVertices.Append(vt);
|
||||
|
||||
vt.Position.X = worldVerticesBuffer[idx2];
|
||||
vt.Position.Y = worldVerticesBuffer[idx2 + 1];
|
||||
lineVertices.Append(vt); lineVertices.Append(vt);
|
||||
|
||||
vt.Position.X = worldVerticesBuffer[idx0];
|
||||
vt.Position.Y = worldVerticesBuffer[idx0 + 1];
|
||||
lineVertices.Append(vt);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (debugMeshHulls)
|
||||
{
|
||||
SFML.Graphics.Vertex vt = new() { Color = AttachmentLineColor };
|
||||
foreach (var slot in skeleton.Slots)
|
||||
{
|
||||
if (slot.Attachment is MeshAttachment meshAttachment)
|
||||
{
|
||||
if (meshAttachment.WorldVerticesLength > worldVerticesBuffer.Length)
|
||||
worldVerticesBuffer = new float[meshAttachment.WorldVerticesLength * 2];
|
||||
|
||||
meshAttachment.ComputeWorldVertices(slot, worldVerticesBuffer);
|
||||
|
||||
var hullLength = (meshAttachment.HullLength >> 1) << 1;
|
||||
|
||||
if (debugMeshHulls && hullLength > 2)
|
||||
{
|
||||
vt.Position.X = worldVerticesBuffer[0];
|
||||
vt.Position.Y = worldVerticesBuffer[1];
|
||||
lineVertices.Append(vt);
|
||||
|
||||
for (int i = 2; i < hullLength; i += 2)
|
||||
{
|
||||
vt.Position.X = worldVerticesBuffer[i];
|
||||
vt.Position.Y = worldVerticesBuffer[i + 1];
|
||||
lineVertices.Append(vt);
|
||||
lineVertices.Append(vt);
|
||||
}
|
||||
|
||||
vt.Position.X = worldVerticesBuffer[0];
|
||||
vt.Position.Y = worldVerticesBuffer[1];
|
||||
lineVertices.Append(vt);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (debugBoundingBoxes)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
if (debugPaths)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
if (debugClippings)
|
||||
{
|
||||
SFML.Graphics.Vertex vt = new() { Color = ClippingLineColor };
|
||||
foreach (var slot in skeleton.Slots)
|
||||
{
|
||||
if (slot.Attachment is ClippingAttachment clippingAttachment)
|
||||
{
|
||||
if (clippingAttachment.WorldVerticesLength > worldVerticesBuffer.Length)
|
||||
worldVerticesBuffer = worldVerticesBuffer = new float[clippingAttachment.WorldVerticesLength * 2];
|
||||
|
||||
clippingAttachment.ComputeWorldVertices(slot, worldVerticesBuffer);
|
||||
|
||||
vt.Position.X = worldVerticesBuffer[0];
|
||||
vt.Position.Y = worldVerticesBuffer[1];
|
||||
lineVertices.Append(vt);
|
||||
|
||||
for (int i = 2; i < clippingAttachment.WorldVerticesLength; i += 2)
|
||||
{
|
||||
vt.Position.X = worldVerticesBuffer[i];
|
||||
vt.Position.Y = worldVerticesBuffer[i + 1];
|
||||
lineVertices.Append(vt);
|
||||
lineVertices.Append(vt);
|
||||
}
|
||||
|
||||
vt.Position.X = worldVerticesBuffer[0];
|
||||
vt.Position.Y = worldVerticesBuffer[1];
|
||||
lineVertices.Append(vt);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (debugBounds)
|
||||
{
|
||||
var vt = new SFML.Graphics.Vertex() { Color = BoundsColor };
|
||||
var b = getCurrentBounds();
|
||||
|
||||
vt.Position.X = b.Left;
|
||||
vt.Position.Y = b.Top;
|
||||
lineVertices.Append(vt);
|
||||
|
||||
vt.Position.X = b.Right;
|
||||
vt.Position.Y = b.Top;
|
||||
lineVertices.Append(vt); lineVertices.Append(vt);
|
||||
|
||||
vt.Position.X = b.Right;
|
||||
vt.Position.Y = b.Bottom;
|
||||
lineVertices.Append(vt); lineVertices.Append(vt);
|
||||
|
||||
vt.Position.X = b.Left;
|
||||
vt.Position.Y = b.Bottom;
|
||||
lineVertices.Append(vt); lineVertices.Append(vt);
|
||||
|
||||
vt.Position.X = b.Left;
|
||||
vt.Position.Y = b.Top;
|
||||
lineVertices.Append(vt);
|
||||
}
|
||||
|
||||
// 骨骼线放最后画
|
||||
if (debugBones)
|
||||
{
|
||||
var width = scale;
|
||||
foreach (var bone in skeleton.Bones)
|
||||
{
|
||||
var boneLength = bone.Data.Length;
|
||||
var p1 = new SFML.System.Vector2f(bone.WorldX, bone.WorldY);
|
||||
var p2 = new SFML.System.Vector2f(bone.WorldX + boneLength * bone.A, bone.WorldY + boneLength * bone.C);
|
||||
AddRectLine(p1, p2, BoneLineColor, width);
|
||||
}
|
||||
}
|
||||
|
||||
target.Draw(lineVertices);
|
||||
target.Draw(rectLineVertices);
|
||||
|
||||
// 骨骼的点最后画, 层级处于骨骼线上面
|
||||
if (debugBones)
|
||||
{
|
||||
var radius = scale;
|
||||
foreach (var bone in skeleton.Bones)
|
||||
{
|
||||
DrawCirclePoint(target, new(bone.WorldX, bone.WorldY), BonePointColor, radius);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
584
SpineViewer/Spine/Implementations/SpineObject/SpineObject37.cs
Normal file
584
SpineViewer/Spine/Implementations/SpineObject/SpineObject37.cs
Normal file
@@ -0,0 +1,584 @@
|
||||
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
|
||||
{
|
||||
[SpineImplementation(SpineVersion.V37)]
|
||||
internal class SpineObject37 : Spine.SpineObject
|
||||
{
|
||||
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
|
||||
{
|
||||
public void Load(AtlasPage page, string path)
|
||||
{
|
||||
var texture = new SFML.Graphics.Texture(path);
|
||||
if (page.magFilter == TextureFilter.Linear)
|
||||
texture.Smooth = true;
|
||||
if (page.uWrap == TextureWrap.Repeat && page.vWrap == TextureWrap.Repeat)
|
||||
texture.Repeated = true;
|
||||
|
||||
page.rendererObject = texture;
|
||||
}
|
||||
|
||||
public void Unload(object texture)
|
||||
{
|
||||
((SFML.Graphics.Texture)texture).Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
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 readonly SkeletonData skeletonData;
|
||||
private readonly AnimationStateData animationStateData;
|
||||
|
||||
private readonly Skeleton skeleton;
|
||||
private readonly AnimationState animationState;
|
||||
|
||||
private readonly SkeletonClipping clipping = new();
|
||||
|
||||
/// <summary>
|
||||
/// 所有插槽在所有皮肤中可用的附件集合
|
||||
/// </summary>
|
||||
private readonly Dictionary<string, Dictionary<string, Attachment>> slotAttachments = [];
|
||||
|
||||
public SpineObject37(string skelPath, string atlasPath) : base(skelPath, atlasPath)
|
||||
{
|
||||
atlas = new Atlas(AtlasPath, textureLoader);
|
||||
try
|
||||
{
|
||||
// 先尝试二进制文件
|
||||
skeletonJson = null;
|
||||
skeletonBinary = new SkeletonBinary(atlas);
|
||||
skeletonData = skeletonBinary.ReadSkeletonData(SkelPath);
|
||||
}
|
||||
catch
|
||||
{
|
||||
try
|
||||
{
|
||||
// 再尝试 Json 文件
|
||||
skeletonBinary = null;
|
||||
skeletonJson = new SkeletonJson(atlas);
|
||||
skeletonData = skeletonJson.ReadSkeletonData(SkelPath);
|
||||
}
|
||||
catch
|
||||
{
|
||||
// 都不行就报错
|
||||
throw new InvalidDataException($"Unknown skeleton file format {SkelPath}");
|
||||
}
|
||||
}
|
||||
|
||||
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).ToImmutableArray();
|
||||
AnimationNames = [EMPTY_ANIMATION, .. skeletonData.Animations.Select(v => v.Name)];
|
||||
|
||||
skeleton = new Skeleton(skeletonData) { Skin = new(Guid.NewGuid().ToString()) }; // 挂载一个空皮肤当作容器
|
||||
animationStateData = new AnimationStateData(skeletonData);
|
||||
animationState = new AnimationState(animationStateData);
|
||||
}
|
||||
|
||||
protected override void Dispose(bool disposing)
|
||||
{
|
||||
base.Dispose(disposing);
|
||||
atlas.Dispose();
|
||||
}
|
||||
|
||||
public override string FileVersion { get => skeletonData.Version; }
|
||||
|
||||
protected override float scale
|
||||
{
|
||||
get => Math.Abs(skeleton.ScaleX);
|
||||
set
|
||||
{
|
||||
skeleton.ScaleX = Math.Sign(skeleton.ScaleX) * value;
|
||||
skeleton.ScaleY = Math.Sign(skeleton.ScaleY) * value;
|
||||
}
|
||||
}
|
||||
|
||||
protected override PointF position
|
||||
{
|
||||
get => new(skeleton.X, skeleton.Y);
|
||||
set
|
||||
{
|
||||
skeleton.X = value.X;
|
||||
skeleton.Y = value.Y;
|
||||
}
|
||||
}
|
||||
|
||||
protected override bool flipX
|
||||
{
|
||||
get => skeleton.ScaleX < 0;
|
||||
set
|
||||
{
|
||||
if (skeleton.ScaleX > 0 && value || skeleton.ScaleX < 0 && !value)
|
||||
skeleton.ScaleX *= -1;
|
||||
}
|
||||
}
|
||||
|
||||
protected override bool flipY
|
||||
{
|
||||
get => skeleton.ScaleY < 0;
|
||||
set
|
||||
{
|
||||
if (skeleton.ScaleY > 0 && value || skeleton.ScaleY < 0 && !value)
|
||||
skeleton.ScaleY *= -1;
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
{
|
||||
// default 不需要加载
|
||||
if (name != "default" && 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 clearSkins()
|
||||
{
|
||||
skeleton.Skin.Attachments.Clear();
|
||||
skeleton.SetSlotsToSetupPose();
|
||||
}
|
||||
|
||||
protected override int[] getTrackIndices() => animationState.Tracks.Select((_, i) => i).Where(i => animationState.Tracks.Items[i] is not null).ToArray();
|
||||
|
||||
protected override string getAnimation(int track) => animationState.GetCurrent(track)?.Animation.Name ?? EMPTY_ANIMATION;
|
||||
|
||||
protected override void setAnimation(int track, string name)
|
||||
{
|
||||
if (name == EMPTY_ANIMATION)
|
||||
animationState.SetAnimation(track, EmptyAnimation, false);
|
||||
else if (AnimationNames.Contains(name))
|
||||
animationState.SetAnimation(track, name, true);
|
||||
}
|
||||
|
||||
protected override void clearTrack(int i) => animationState.ClearTrack(i);
|
||||
|
||||
public override float GetAnimationDuration(string name) { return skeletonData.FindAnimation(name)?.Duration ?? 0f; }
|
||||
|
||||
protected override RectangleF getCurrentBounds()
|
||||
{
|
||||
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))
|
||||
{
|
||||
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)
|
||||
{
|
||||
animationState.Update(delta);
|
||||
animationState.Apply(skeleton);
|
||||
skeleton.Update(delta);
|
||||
skeleton.UpdateWorldTransform();
|
||||
}
|
||||
|
||||
protected override void draw(SFML.Graphics.RenderTarget target, SFML.Graphics.RenderStates states)
|
||||
{
|
||||
triangleVertices.Clear();
|
||||
states.Texture = null;
|
||||
states.Shader = SFMLShader.GetSpineShader(usePma);
|
||||
|
||||
// 要用 DrawOrder 而不是 Slots
|
||||
foreach (var slot in skeleton.DrawOrder)
|
||||
{
|
||||
var attachment = slot.Attachment;
|
||||
|
||||
SFML.Graphics.Texture texture;
|
||||
|
||||
float[] worldVertices = worldVerticesBuffer; // 顶点世界坐标, 连续的 [x0, y0, x1, y1, ...] 坐标值
|
||||
int worldVerticesCount; // 等于顶点数组的长度除以 2
|
||||
int[] worldTriangleIndices; // 三角形索引, 从顶点坐标数组取的时候要乘以 2, 最大值是 worldVerticesCount - 1
|
||||
int worldTriangleIndicesLength; // 三角形索引数组长度
|
||||
float[] uvs; // 纹理坐标
|
||||
float tintR = skeleton.R * slot.R;
|
||||
float tintG = skeleton.G * slot.G;
|
||||
float tintB = skeleton.B * slot.B;
|
||||
float tintA = skeleton.A * slot.A;
|
||||
|
||||
if (attachment is RegionAttachment regionAttachment)
|
||||
{
|
||||
texture = (SFML.Graphics.Texture)((AtlasRegion)regionAttachment.RendererObject).page.rendererObject;
|
||||
|
||||
regionAttachment.ComputeWorldVertices(slot.Bone, worldVertices, 0);
|
||||
worldVerticesCount = 4;
|
||||
worldTriangleIndices = [0, 1, 2, 2, 3, 0];
|
||||
worldTriangleIndicesLength = 6;
|
||||
uvs = regionAttachment.UVs;
|
||||
tintR *= regionAttachment.R;
|
||||
tintG *= regionAttachment.G;
|
||||
tintB *= regionAttachment.B;
|
||||
tintA *= regionAttachment.A;
|
||||
}
|
||||
else if (attachment is MeshAttachment meshAttachment)
|
||||
{
|
||||
texture = (SFML.Graphics.Texture)((AtlasRegion)meshAttachment.RendererObject).page.rendererObject;
|
||||
|
||||
if (meshAttachment.WorldVerticesLength > worldVertices.Length)
|
||||
worldVertices = worldVerticesBuffer = new float[meshAttachment.WorldVerticesLength * 2];
|
||||
meshAttachment.ComputeWorldVertices(slot, worldVertices);
|
||||
worldVerticesCount = meshAttachment.WorldVerticesLength / 2;
|
||||
worldTriangleIndices = meshAttachment.Triangles;
|
||||
worldTriangleIndicesLength = meshAttachment.Triangles.Length;
|
||||
uvs = meshAttachment.UVs;
|
||||
tintR *= meshAttachment.R;
|
||||
tintG *= meshAttachment.G;
|
||||
tintB *= meshAttachment.B;
|
||||
tintA *= meshAttachment.A;
|
||||
}
|
||||
else if (attachment is ClippingAttachment clippingAttachment)
|
||||
{
|
||||
clipping.ClipStart(slot, clippingAttachment);
|
||||
continue;
|
||||
}
|
||||
else
|
||||
{
|
||||
clipping.ClipEnd(slot);
|
||||
continue;
|
||||
}
|
||||
|
||||
SFML.Graphics.BlendMode blendMode = GetSFMLBlendMode(slot.Data.BlendMode);
|
||||
|
||||
states.Texture ??= texture;
|
||||
if (states.BlendMode != blendMode || states.Texture != texture)
|
||||
{
|
||||
if (triangleVertices.VertexCount > 0)
|
||||
{
|
||||
target.Draw(triangleVertices, states);
|
||||
triangleVertices.Clear();
|
||||
}
|
||||
states.BlendMode = blendMode;
|
||||
states.Texture = texture;
|
||||
}
|
||||
|
||||
if (clipping.IsClipping)
|
||||
{
|
||||
// 这里必须单独记录 Count, 和 Items 的 Length 是不一致的
|
||||
clipping.ClipTriangles(worldVertices, worldVerticesCount * 2, worldTriangleIndices, worldTriangleIndicesLength, uvs);
|
||||
worldVertices = clipping.ClippedVertices.Items;
|
||||
worldVerticesCount = clipping.ClippedVertices.Count / 2;
|
||||
worldTriangleIndices = clipping.ClippedTriangles.Items;
|
||||
worldTriangleIndicesLength = clipping.ClippedTriangles.Count;
|
||||
uvs = clipping.ClippedUVs.Items;
|
||||
}
|
||||
|
||||
var textureSizeX = texture.Size.X;
|
||||
var textureSizeY = texture.Size.Y;
|
||||
|
||||
SFML.Graphics.Vertex vertex = new();
|
||||
vertex.Color.R = (byte)(tintR * 255);
|
||||
vertex.Color.G = (byte)(tintG * 255);
|
||||
vertex.Color.B = (byte)(tintB * 255);
|
||||
vertex.Color.A = (byte)(tintA * 255);
|
||||
|
||||
// 必须用 worldTriangleIndicesLength 不能直接 foreach
|
||||
for (int i = 0; i < worldTriangleIndicesLength; i++)
|
||||
{
|
||||
var index = worldTriangleIndices[i] * 2;
|
||||
vertex.Position.X = worldVertices[index];
|
||||
vertex.Position.Y = worldVertices[index + 1];
|
||||
vertex.TexCoords.X = uvs[index] * textureSizeX;
|
||||
vertex.TexCoords.Y = uvs[index + 1] * textureSizeY;
|
||||
triangleVertices.Append(vertex);
|
||||
}
|
||||
|
||||
clipping.ClipEnd(slot);
|
||||
}
|
||||
clipping.ClipEnd();
|
||||
|
||||
target.Draw(triangleVertices, states);
|
||||
}
|
||||
|
||||
protected override void debugDraw(SFML.Graphics.RenderTarget target)
|
||||
{
|
||||
lineVertices.Clear();
|
||||
rectLineVertices.Clear();
|
||||
|
||||
if (debugRegions)
|
||||
{
|
||||
SFML.Graphics.Vertex vt = new() { Color = AttachmentLineColor };
|
||||
foreach (var slot in skeleton.Slots)
|
||||
{
|
||||
if (slot.Attachment is RegionAttachment regionAttachment)
|
||||
{
|
||||
regionAttachment.ComputeWorldVertices(slot.Bone, worldVerticesBuffer, 0);
|
||||
|
||||
vt.Position.X = worldVerticesBuffer[0];
|
||||
vt.Position.Y = worldVerticesBuffer[1];
|
||||
lineVertices.Append(vt);
|
||||
|
||||
vt.Position.X = worldVerticesBuffer[2];
|
||||
vt.Position.Y = worldVerticesBuffer[3];
|
||||
lineVertices.Append(vt); lineVertices.Append(vt);
|
||||
|
||||
vt.Position.X = worldVerticesBuffer[4];
|
||||
vt.Position.Y = worldVerticesBuffer[5];
|
||||
lineVertices.Append(vt); lineVertices.Append(vt);
|
||||
|
||||
vt.Position.X = worldVerticesBuffer[6];
|
||||
vt.Position.Y = worldVerticesBuffer[7];
|
||||
lineVertices.Append(vt); lineVertices.Append(vt);
|
||||
|
||||
vt.Position.X = worldVerticesBuffer[0];
|
||||
vt.Position.Y = worldVerticesBuffer[1];
|
||||
lineVertices.Append(vt);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (debugMeshes)
|
||||
{
|
||||
SFML.Graphics.Vertex vt = new() { Color = MeshLineColor };
|
||||
foreach (var slot in skeleton.Slots)
|
||||
{
|
||||
if (slot.Attachment is MeshAttachment meshAttachment)
|
||||
{
|
||||
if (meshAttachment.WorldVerticesLength > worldVerticesBuffer.Length)
|
||||
worldVerticesBuffer = new float[meshAttachment.WorldVerticesLength * 2];
|
||||
|
||||
meshAttachment.ComputeWorldVertices(slot, worldVerticesBuffer);
|
||||
|
||||
var triangleIndices = meshAttachment.Triangles;
|
||||
for (int i = 0; i < triangleIndices.Length; i += 3)
|
||||
{
|
||||
var idx0 = triangleIndices[i] * 2;
|
||||
var idx1 = triangleIndices[i + 1] * 2;
|
||||
var idx2 = triangleIndices[i + 2] * 2;
|
||||
|
||||
vt.Position.X = worldVerticesBuffer[idx0];
|
||||
vt.Position.Y = worldVerticesBuffer[idx0 + 1];
|
||||
lineVertices.Append(vt);
|
||||
|
||||
vt.Position.X = worldVerticesBuffer[idx1];
|
||||
vt.Position.Y = worldVerticesBuffer[idx1 + 1];
|
||||
lineVertices.Append(vt); lineVertices.Append(vt);
|
||||
|
||||
vt.Position.X = worldVerticesBuffer[idx2];
|
||||
vt.Position.Y = worldVerticesBuffer[idx2 + 1];
|
||||
lineVertices.Append(vt); lineVertices.Append(vt);
|
||||
|
||||
vt.Position.X = worldVerticesBuffer[idx0];
|
||||
vt.Position.Y = worldVerticesBuffer[idx0 + 1];
|
||||
lineVertices.Append(vt);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (debugMeshHulls)
|
||||
{
|
||||
SFML.Graphics.Vertex vt = new() { Color = AttachmentLineColor };
|
||||
foreach (var slot in skeleton.Slots)
|
||||
{
|
||||
if (slot.Attachment is MeshAttachment meshAttachment)
|
||||
{
|
||||
if (meshAttachment.WorldVerticesLength > worldVerticesBuffer.Length)
|
||||
worldVerticesBuffer = new float[meshAttachment.WorldVerticesLength * 2];
|
||||
|
||||
meshAttachment.ComputeWorldVertices(slot, worldVerticesBuffer);
|
||||
|
||||
var hullLength = (meshAttachment.HullLength >> 1) << 1;
|
||||
|
||||
if (debugMeshHulls && hullLength > 2)
|
||||
{
|
||||
vt.Position.X = worldVerticesBuffer[0];
|
||||
vt.Position.Y = worldVerticesBuffer[1];
|
||||
lineVertices.Append(vt);
|
||||
|
||||
for (int i = 2; i < hullLength; i += 2)
|
||||
{
|
||||
vt.Position.X = worldVerticesBuffer[i];
|
||||
vt.Position.Y = worldVerticesBuffer[i + 1];
|
||||
lineVertices.Append(vt);
|
||||
lineVertices.Append(vt);
|
||||
}
|
||||
|
||||
vt.Position.X = worldVerticesBuffer[0];
|
||||
vt.Position.Y = worldVerticesBuffer[1];
|
||||
lineVertices.Append(vt);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (debugBoundingBoxes)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
if (debugPaths)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
if (debugClippings)
|
||||
{
|
||||
SFML.Graphics.Vertex vt = new() { Color = ClippingLineColor };
|
||||
foreach (var slot in skeleton.Slots)
|
||||
{
|
||||
if (slot.Attachment is ClippingAttachment clippingAttachment)
|
||||
{
|
||||
if (clippingAttachment.WorldVerticesLength > worldVerticesBuffer.Length)
|
||||
worldVerticesBuffer = worldVerticesBuffer = new float[clippingAttachment.WorldVerticesLength * 2];
|
||||
|
||||
clippingAttachment.ComputeWorldVertices(slot, worldVerticesBuffer);
|
||||
|
||||
vt.Position.X = worldVerticesBuffer[0];
|
||||
vt.Position.Y = worldVerticesBuffer[1];
|
||||
lineVertices.Append(vt);
|
||||
|
||||
for (int i = 2; i < clippingAttachment.WorldVerticesLength; i += 2)
|
||||
{
|
||||
vt.Position.X = worldVerticesBuffer[i];
|
||||
vt.Position.Y = worldVerticesBuffer[i + 1];
|
||||
lineVertices.Append(vt);
|
||||
lineVertices.Append(vt);
|
||||
}
|
||||
|
||||
vt.Position.X = worldVerticesBuffer[0];
|
||||
vt.Position.Y = worldVerticesBuffer[1];
|
||||
lineVertices.Append(vt);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (debugBounds)
|
||||
{
|
||||
var vt = new SFML.Graphics.Vertex() { Color = BoundsColor };
|
||||
var b = getCurrentBounds();
|
||||
|
||||
vt.Position.X = b.Left;
|
||||
vt.Position.Y = b.Top;
|
||||
lineVertices.Append(vt);
|
||||
|
||||
vt.Position.X = b.Right;
|
||||
vt.Position.Y = b.Top;
|
||||
lineVertices.Append(vt); lineVertices.Append(vt);
|
||||
|
||||
vt.Position.X = b.Right;
|
||||
vt.Position.Y = b.Bottom;
|
||||
lineVertices.Append(vt); lineVertices.Append(vt);
|
||||
|
||||
vt.Position.X = b.Left;
|
||||
vt.Position.Y = b.Bottom;
|
||||
lineVertices.Append(vt); lineVertices.Append(vt);
|
||||
|
||||
vt.Position.X = b.Left;
|
||||
vt.Position.Y = b.Top;
|
||||
lineVertices.Append(vt);
|
||||
}
|
||||
|
||||
// 骨骼线放最后画
|
||||
if (debugBones)
|
||||
{
|
||||
var width = scale;
|
||||
foreach (var bone in skeleton.Bones)
|
||||
{
|
||||
var boneLength = bone.Data.Length;
|
||||
var p1 = new SFML.System.Vector2f(bone.WorldX, bone.WorldY);
|
||||
var p2 = new SFML.System.Vector2f(bone.WorldX + boneLength * bone.A, bone.WorldY + boneLength * bone.C);
|
||||
AddRectLine(p1, p2, BoneLineColor, width);
|
||||
}
|
||||
}
|
||||
|
||||
target.Draw(lineVertices);
|
||||
target.Draw(rectLineVertices);
|
||||
|
||||
// 骨骼的点最后画, 层级处于骨骼线上面
|
||||
if (debugBones)
|
||||
{
|
||||
var radius = scale;
|
||||
foreach (var bone in skeleton.Bones)
|
||||
{
|
||||
DrawCirclePoint(target, new(bone.WorldX, bone.WorldY), BonePointColor, radius);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
587
SpineViewer/Spine/Implementations/SpineObject/SpineObject38.cs
Normal file
587
SpineViewer/Spine/Implementations/SpineObject/SpineObject38.cs
Normal file
@@ -0,0 +1,587 @@
|
||||
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
|
||||
{
|
||||
[SpineImplementation(SpineVersion.V38)]
|
||||
internal class SpineObject38 : Spine.SpineObject
|
||||
{
|
||||
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
|
||||
{
|
||||
public void Load(AtlasPage page, string path)
|
||||
{
|
||||
var texture = new SFML.Graphics.Texture(path);
|
||||
if (page.magFilter == TextureFilter.Linear)
|
||||
texture.Smooth = true;
|
||||
if (page.uWrap == TextureWrap.Repeat && page.vWrap == TextureWrap.Repeat)
|
||||
texture.Repeated = true;
|
||||
|
||||
page.rendererObject = texture;
|
||||
// 似乎是不需要设置的, 因为存在某些 png 和 atlas 大小不同的情况, 一般是有一些缩放, 如果设置了反而渲染异常
|
||||
// page.width = (int)texture.Size.X;
|
||||
// page.height = (int)texture.Size.Y;
|
||||
}
|
||||
|
||||
public void Unload(object texture)
|
||||
{
|
||||
((SFML.Graphics.Texture)texture).Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
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 readonly SkeletonData skeletonData;
|
||||
private readonly AnimationStateData animationStateData;
|
||||
|
||||
private readonly Skeleton skeleton;
|
||||
private readonly AnimationState animationState;
|
||||
|
||||
private readonly SkeletonClipping clipping = new();
|
||||
|
||||
/// <summary>
|
||||
/// 所有插槽在所有皮肤中可用的附件集合
|
||||
/// </summary>
|
||||
private readonly Dictionary<string, Dictionary<string, Attachment>> slotAttachments = [];
|
||||
|
||||
public SpineObject38(string skelPath, string atlasPath) : base(skelPath, atlasPath)
|
||||
{
|
||||
atlas = new Atlas(AtlasPath, textureLoader);
|
||||
try
|
||||
{
|
||||
// 先尝试二进制文件
|
||||
skeletonJson = null;
|
||||
skeletonBinary = new SkeletonBinary(atlas);
|
||||
skeletonData = skeletonBinary.ReadSkeletonData(SkelPath);
|
||||
}
|
||||
catch
|
||||
{
|
||||
try
|
||||
{
|
||||
// 再尝试 Json 文件
|
||||
skeletonBinary = null;
|
||||
skeletonJson = new SkeletonJson(atlas);
|
||||
skeletonData = skeletonJson.ReadSkeletonData(SkelPath);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
// 都不行就报错
|
||||
throw new InvalidDataException($"Unknown skeleton file format {SkelPath}", ex);
|
||||
}
|
||||
}
|
||||
|
||||
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).ToImmutableArray();
|
||||
AnimationNames = [EMPTY_ANIMATION, .. skeletonData.Animations.Select(v => v.Name)];
|
||||
|
||||
skeleton = new Skeleton(skeletonData) { Skin = new(Guid.NewGuid().ToString()) }; // 挂载一个空皮肤当作容器
|
||||
animationStateData = new AnimationStateData(skeletonData);
|
||||
animationState = new AnimationState(animationStateData);
|
||||
}
|
||||
|
||||
protected override void Dispose(bool disposing)
|
||||
{
|
||||
base.Dispose(disposing);
|
||||
atlas.Dispose();
|
||||
}
|
||||
|
||||
public override string FileVersion { get => skeletonData.Version; }
|
||||
|
||||
protected override float scale
|
||||
{
|
||||
get => Math.Abs(skeleton.ScaleX);
|
||||
set
|
||||
{
|
||||
skeleton.ScaleX = Math.Sign(skeleton.ScaleX) * value;
|
||||
skeleton.ScaleY = Math.Sign(skeleton.ScaleY) * value;
|
||||
}
|
||||
}
|
||||
|
||||
protected override PointF position
|
||||
{
|
||||
get => new(skeleton.X, skeleton.Y);
|
||||
set
|
||||
{
|
||||
skeleton.X = value.X;
|
||||
skeleton.Y = value.Y;
|
||||
}
|
||||
}
|
||||
|
||||
protected override bool flipX
|
||||
{
|
||||
get => skeleton.ScaleX < 0;
|
||||
set
|
||||
{
|
||||
if (skeleton.ScaleX > 0 && value || skeleton.ScaleX < 0 && !value)
|
||||
skeleton.ScaleX *= -1;
|
||||
}
|
||||
}
|
||||
|
||||
protected override bool flipY
|
||||
{
|
||||
get => skeleton.ScaleY < 0;
|
||||
set
|
||||
{
|
||||
if (skeleton.ScaleY > 0 && value || skeleton.ScaleY < 0 && !value)
|
||||
skeleton.ScaleY *= -1;
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
{
|
||||
// default 不需要加载
|
||||
if (name != "default" && skeletonData.FindSkin(name) is Skin sk)
|
||||
{
|
||||
skeleton.Skin.AddSkin(sk);
|
||||
skeleton.SetSlotsToSetupPose();
|
||||
}
|
||||
}
|
||||
|
||||
protected override void clearSkins()
|
||||
{
|
||||
skeleton.Skin.Clear();
|
||||
skeleton.SetSlotsToSetupPose();
|
||||
}
|
||||
|
||||
protected override int[] getTrackIndices() => animationState.Tracks.Select((_, i) => i).Where(i => animationState.Tracks.Items[i] is not null).ToArray();
|
||||
|
||||
protected override string getAnimation(int track) => animationState.GetCurrent(track)?.Animation.Name ?? EMPTY_ANIMATION;
|
||||
|
||||
protected override void setAnimation(int track, string name)
|
||||
{
|
||||
if (name == EMPTY_ANIMATION)
|
||||
animationState.SetAnimation(track, EmptyAnimation, false);
|
||||
else if (AnimationNames.Contains(name))
|
||||
animationState.SetAnimation(track, name, true);
|
||||
}
|
||||
|
||||
protected override void clearTrack(int i) => animationState.ClearTrack(i);
|
||||
|
||||
public override float GetAnimationDuration(string name) { return skeletonData.FindAnimation(name)?.Duration ?? 0f; }
|
||||
|
||||
protected override RectangleF getCurrentBounds()
|
||||
{
|
||||
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))
|
||||
{
|
||||
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)
|
||||
{
|
||||
animationState.Update(delta);
|
||||
animationState.Apply(skeleton);
|
||||
skeleton.Update(delta);
|
||||
skeleton.UpdateWorldTransform();
|
||||
}
|
||||
|
||||
protected override void draw(SFML.Graphics.RenderTarget target, SFML.Graphics.RenderStates states)
|
||||
{
|
||||
triangleVertices.Clear();
|
||||
states.Texture = null;
|
||||
states.Shader = SFMLShader.GetSpineShader(usePma);
|
||||
|
||||
// 要用 DrawOrder 而不是 Slots
|
||||
foreach (var slot in skeleton.DrawOrder)
|
||||
{
|
||||
var attachment = slot.Attachment;
|
||||
|
||||
SFML.Graphics.Texture texture;
|
||||
|
||||
float[] worldVertices = worldVerticesBuffer; // 顶点世界坐标, 连续的 [x0, y0, x1, y1, ...] 坐标值
|
||||
int worldVerticesCount; // 等于顶点数组的长度除以 2
|
||||
int[] worldTriangleIndices; // 三角形索引, 从顶点坐标数组取的时候要乘以 2, 最大值是 worldVerticesCount - 1
|
||||
int worldTriangleIndicesLength; // 三角形索引数组长度
|
||||
float[] uvs; // 纹理坐标
|
||||
float tintR = skeleton.R * slot.R;
|
||||
float tintG = skeleton.G * slot.G;
|
||||
float tintB = skeleton.B * slot.B;
|
||||
float tintA = skeleton.A * slot.A;
|
||||
|
||||
if (attachment is RegionAttachment regionAttachment)
|
||||
{
|
||||
texture = (SFML.Graphics.Texture)((AtlasRegion)regionAttachment.RendererObject).page.rendererObject;
|
||||
|
||||
regionAttachment.ComputeWorldVertices(slot.Bone, worldVertices, 0);
|
||||
worldVerticesCount = 4;
|
||||
worldTriangleIndices = [0, 1, 2, 2, 3, 0];
|
||||
worldTriangleIndicesLength = 6;
|
||||
uvs = regionAttachment.UVs;
|
||||
tintR *= regionAttachment.R;
|
||||
tintG *= regionAttachment.G;
|
||||
tintB *= regionAttachment.B;
|
||||
tintA *= regionAttachment.A;
|
||||
}
|
||||
else if (attachment is MeshAttachment meshAttachment)
|
||||
{
|
||||
texture = (SFML.Graphics.Texture)((AtlasRegion)meshAttachment.RendererObject).page.rendererObject;
|
||||
|
||||
if (meshAttachment.WorldVerticesLength > worldVertices.Length)
|
||||
worldVertices = worldVerticesBuffer = new float[meshAttachment.WorldVerticesLength * 2];
|
||||
meshAttachment.ComputeWorldVertices(slot, worldVertices);
|
||||
worldVerticesCount = meshAttachment.WorldVerticesLength / 2;
|
||||
worldTriangleIndices = meshAttachment.Triangles;
|
||||
worldTriangleIndicesLength = meshAttachment.Triangles.Length;
|
||||
uvs = meshAttachment.UVs;
|
||||
tintR *= meshAttachment.R;
|
||||
tintG *= meshAttachment.G;
|
||||
tintB *= meshAttachment.B;
|
||||
tintA *= meshAttachment.A;
|
||||
}
|
||||
else if (attachment is ClippingAttachment clippingAttachment)
|
||||
{
|
||||
clipping.ClipStart(slot, clippingAttachment);
|
||||
continue;
|
||||
}
|
||||
else
|
||||
{
|
||||
clipping.ClipEnd(slot);
|
||||
continue;
|
||||
}
|
||||
|
||||
SFML.Graphics.BlendMode blendMode = GetSFMLBlendMode(slot.Data.BlendMode);
|
||||
|
||||
states.Texture ??= texture;
|
||||
if (states.BlendMode != blendMode || states.Texture != texture)
|
||||
{
|
||||
if (triangleVertices.VertexCount > 0)
|
||||
{
|
||||
target.Draw(triangleVertices, states);
|
||||
triangleVertices.Clear();
|
||||
}
|
||||
states.BlendMode = blendMode;
|
||||
states.Texture = texture;
|
||||
}
|
||||
|
||||
if (clipping.IsClipping)
|
||||
{
|
||||
// 这里必须单独记录 Count, 和 Items 的 Length 是不一致的
|
||||
clipping.ClipTriangles(worldVertices, worldVerticesCount * 2, worldTriangleIndices, worldTriangleIndicesLength, uvs);
|
||||
worldVertices = clipping.ClippedVertices.Items;
|
||||
worldVerticesCount = clipping.ClippedVertices.Count / 2;
|
||||
worldTriangleIndices = clipping.ClippedTriangles.Items;
|
||||
worldTriangleIndicesLength = clipping.ClippedTriangles.Count;
|
||||
uvs = clipping.ClippedUVs.Items;
|
||||
}
|
||||
|
||||
var textureSizeX = texture.Size.X;
|
||||
var textureSizeY = texture.Size.Y;
|
||||
|
||||
SFML.Graphics.Vertex vertex = new();
|
||||
vertex.Color.R = (byte)(tintR * 255);
|
||||
vertex.Color.G = (byte)(tintG * 255);
|
||||
vertex.Color.B = (byte)(tintB * 255);
|
||||
vertex.Color.A = (byte)(tintA * 255);
|
||||
|
||||
// 必须用 worldTriangleIndicesLength 不能直接 foreach
|
||||
for (int i = 0; i < worldTriangleIndicesLength; i++)
|
||||
{
|
||||
var index = worldTriangleIndices[i] * 2;
|
||||
vertex.Position.X = worldVertices[index];
|
||||
vertex.Position.Y = worldVertices[index + 1];
|
||||
vertex.TexCoords.X = uvs[index] * textureSizeX;
|
||||
vertex.TexCoords.Y = uvs[index + 1] * textureSizeY;
|
||||
triangleVertices.Append(vertex);
|
||||
}
|
||||
|
||||
clipping.ClipEnd(slot);
|
||||
}
|
||||
clipping.ClipEnd();
|
||||
|
||||
target.Draw(triangleVertices, states);
|
||||
}
|
||||
|
||||
protected override void debugDraw(SFML.Graphics.RenderTarget target)
|
||||
{
|
||||
lineVertices.Clear();
|
||||
rectLineVertices.Clear();
|
||||
|
||||
if (debugRegions)
|
||||
{
|
||||
SFML.Graphics.Vertex vt = new() { Color = AttachmentLineColor };
|
||||
foreach (var slot in skeleton.Slots)
|
||||
{
|
||||
if (slot.Bone.Active && slot.Attachment is RegionAttachment regionAttachment)
|
||||
{
|
||||
regionAttachment.ComputeWorldVertices(slot.Bone, worldVerticesBuffer, 0);
|
||||
|
||||
vt.Position.X = worldVerticesBuffer[0];
|
||||
vt.Position.Y = worldVerticesBuffer[1];
|
||||
lineVertices.Append(vt);
|
||||
|
||||
vt.Position.X = worldVerticesBuffer[2];
|
||||
vt.Position.Y = worldVerticesBuffer[3];
|
||||
lineVertices.Append(vt); lineVertices.Append(vt);
|
||||
|
||||
vt.Position.X = worldVerticesBuffer[4];
|
||||
vt.Position.Y = worldVerticesBuffer[5];
|
||||
lineVertices.Append(vt); lineVertices.Append(vt);
|
||||
|
||||
vt.Position.X = worldVerticesBuffer[6];
|
||||
vt.Position.Y = worldVerticesBuffer[7];
|
||||
lineVertices.Append(vt); lineVertices.Append(vt);
|
||||
|
||||
vt.Position.X = worldVerticesBuffer[0];
|
||||
vt.Position.Y = worldVerticesBuffer[1];
|
||||
lineVertices.Append(vt);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (debugMeshes)
|
||||
{
|
||||
SFML.Graphics.Vertex vt = new() { Color = MeshLineColor };
|
||||
foreach (var slot in skeleton.Slots)
|
||||
{
|
||||
if (slot.Bone.Active && slot.Attachment is MeshAttachment meshAttachment)
|
||||
{
|
||||
if (meshAttachment.WorldVerticesLength > worldVerticesBuffer.Length)
|
||||
worldVerticesBuffer = new float[meshAttachment.WorldVerticesLength * 2];
|
||||
|
||||
meshAttachment.ComputeWorldVertices(slot, worldVerticesBuffer);
|
||||
|
||||
var triangleIndices = meshAttachment.Triangles;
|
||||
for (int i = 0; i < triangleIndices.Length; i += 3)
|
||||
{
|
||||
var idx0 = triangleIndices[i] * 2;
|
||||
var idx1 = triangleIndices[i + 1] * 2;
|
||||
var idx2 = triangleIndices[i + 2] * 2;
|
||||
|
||||
vt.Position.X = worldVerticesBuffer[idx0];
|
||||
vt.Position.Y = worldVerticesBuffer[idx0 + 1];
|
||||
lineVertices.Append(vt);
|
||||
|
||||
vt.Position.X = worldVerticesBuffer[idx1];
|
||||
vt.Position.Y = worldVerticesBuffer[idx1 + 1];
|
||||
lineVertices.Append(vt); lineVertices.Append(vt);
|
||||
|
||||
vt.Position.X = worldVerticesBuffer[idx2];
|
||||
vt.Position.Y = worldVerticesBuffer[idx2 + 1];
|
||||
lineVertices.Append(vt); lineVertices.Append(vt);
|
||||
|
||||
vt.Position.X = worldVerticesBuffer[idx0];
|
||||
vt.Position.Y = worldVerticesBuffer[idx0 + 1];
|
||||
lineVertices.Append(vt);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (debugMeshHulls)
|
||||
{
|
||||
SFML.Graphics.Vertex vt = new() { Color = AttachmentLineColor };
|
||||
foreach (var slot in skeleton.Slots)
|
||||
{
|
||||
if (slot.Bone.Active && slot.Attachment is MeshAttachment meshAttachment)
|
||||
{
|
||||
if (meshAttachment.WorldVerticesLength > worldVerticesBuffer.Length)
|
||||
worldVerticesBuffer = new float[meshAttachment.WorldVerticesLength * 2];
|
||||
|
||||
meshAttachment.ComputeWorldVertices(slot, worldVerticesBuffer);
|
||||
|
||||
var hullLength = (meshAttachment.HullLength >> 1) << 1;
|
||||
|
||||
if (debugMeshHulls && hullLength > 2)
|
||||
{
|
||||
vt.Position.X = worldVerticesBuffer[0];
|
||||
vt.Position.Y = worldVerticesBuffer[1];
|
||||
lineVertices.Append(vt);
|
||||
|
||||
for (int i = 2; i < hullLength; i += 2)
|
||||
{
|
||||
vt.Position.X = worldVerticesBuffer[i];
|
||||
vt.Position.Y = worldVerticesBuffer[i + 1];
|
||||
lineVertices.Append(vt);
|
||||
lineVertices.Append(vt);
|
||||
}
|
||||
|
||||
vt.Position.X = worldVerticesBuffer[0];
|
||||
vt.Position.Y = worldVerticesBuffer[1];
|
||||
lineVertices.Append(vt);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (debugBoundingBoxes)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
if (debugPaths)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
if (debugClippings)
|
||||
{
|
||||
SFML.Graphics.Vertex vt = new() { Color = ClippingLineColor };
|
||||
foreach (var slot in skeleton.Slots)
|
||||
{
|
||||
if (slot.Bone.Active && slot.Attachment is ClippingAttachment clippingAttachment)
|
||||
{
|
||||
if (clippingAttachment.WorldVerticesLength > worldVerticesBuffer.Length)
|
||||
worldVerticesBuffer = worldVerticesBuffer = new float[clippingAttachment.WorldVerticesLength * 2];
|
||||
|
||||
clippingAttachment.ComputeWorldVertices(slot, worldVerticesBuffer);
|
||||
|
||||
vt.Position.X = worldVerticesBuffer[0];
|
||||
vt.Position.Y = worldVerticesBuffer[1];
|
||||
lineVertices.Append(vt);
|
||||
|
||||
for (int i = 2; i < clippingAttachment.WorldVerticesLength; i += 2)
|
||||
{
|
||||
vt.Position.X = worldVerticesBuffer[i];
|
||||
vt.Position.Y = worldVerticesBuffer[i + 1];
|
||||
lineVertices.Append(vt);
|
||||
lineVertices.Append(vt);
|
||||
}
|
||||
|
||||
vt.Position.X = worldVerticesBuffer[0];
|
||||
vt.Position.Y = worldVerticesBuffer[1];
|
||||
lineVertices.Append(vt);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (debugBounds)
|
||||
{
|
||||
var vt = new SFML.Graphics.Vertex() { Color = BoundsColor };
|
||||
var b = getCurrentBounds();
|
||||
|
||||
vt.Position.X = b.Left;
|
||||
vt.Position.Y = b.Top;
|
||||
lineVertices.Append(vt);
|
||||
|
||||
vt.Position.X = b.Right;
|
||||
vt.Position.Y = b.Top;
|
||||
lineVertices.Append(vt); lineVertices.Append(vt);
|
||||
|
||||
vt.Position.X = b.Right;
|
||||
vt.Position.Y = b.Bottom;
|
||||
lineVertices.Append(vt); lineVertices.Append(vt);
|
||||
|
||||
vt.Position.X = b.Left;
|
||||
vt.Position.Y = b.Bottom;
|
||||
lineVertices.Append(vt); lineVertices.Append(vt);
|
||||
|
||||
vt.Position.X = b.Left;
|
||||
vt.Position.Y = b.Top;
|
||||
lineVertices.Append(vt);
|
||||
}
|
||||
|
||||
// 骨骼线放最后画
|
||||
if (debugBones)
|
||||
{
|
||||
var width = scale;
|
||||
foreach (var bone in skeleton.Bones)
|
||||
{
|
||||
if (!bone.Active) continue;
|
||||
var boneLength = bone.Data.Length;
|
||||
var p1 = new SFML.System.Vector2f(bone.WorldX, bone.WorldY);
|
||||
var p2 = new SFML.System.Vector2f(bone.WorldX + boneLength * bone.A, bone.WorldY + boneLength * bone.C);
|
||||
AddRectLine(p1, p2, BoneLineColor, width);
|
||||
}
|
||||
}
|
||||
|
||||
target.Draw(lineVertices);
|
||||
target.Draw(rectLineVertices);
|
||||
|
||||
// 骨骼的点最后画, 层级处于骨骼线上面
|
||||
if (debugBones)
|
||||
{
|
||||
var radius = scale;
|
||||
foreach (var bone in skeleton.Bones)
|
||||
{
|
||||
if (!bone.Active) continue;
|
||||
DrawCirclePoint(target, new(bone.WorldX, bone.WorldY), BonePointColor, radius);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
583
SpineViewer/Spine/Implementations/SpineObject/SpineObject40.cs
Normal file
583
SpineViewer/Spine/Implementations/SpineObject/SpineObject40.cs
Normal file
@@ -0,0 +1,583 @@
|
||||
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
|
||||
{
|
||||
[SpineImplementation(SpineVersion.V40)]
|
||||
internal class SpineObject40 : Spine.SpineObject
|
||||
{
|
||||
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
|
||||
{
|
||||
public void Load(AtlasPage page, string path)
|
||||
{
|
||||
var texture = new SFML.Graphics.Texture(path);
|
||||
if (page.magFilter == TextureFilter.Linear)
|
||||
texture.Smooth = true;
|
||||
if (page.uWrap == TextureWrap.Repeat && page.vWrap == TextureWrap.Repeat)
|
||||
texture.Repeated = true;
|
||||
|
||||
page.rendererObject = texture;
|
||||
}
|
||||
|
||||
public void Unload(object texture)
|
||||
{
|
||||
((SFML.Graphics.Texture)texture).Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
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 readonly SkeletonData skeletonData;
|
||||
private readonly AnimationStateData animationStateData;
|
||||
|
||||
private readonly Skeleton skeleton;
|
||||
private readonly AnimationState animationState;
|
||||
|
||||
private readonly SkeletonClipping clipping = new();
|
||||
|
||||
/// <summary>
|
||||
/// 所有插槽在所有皮肤中可用的附件集合
|
||||
/// </summary>
|
||||
private readonly Dictionary<string, Dictionary<string, Attachment>> slotAttachments = [];
|
||||
|
||||
public SpineObject40(string skelPath, string atlasPath) : base(skelPath, atlasPath)
|
||||
{
|
||||
atlas = new Atlas(AtlasPath, textureLoader);
|
||||
try
|
||||
{
|
||||
// 先尝试二进制文件
|
||||
skeletonJson = null;
|
||||
skeletonBinary = new SkeletonBinary(atlas);
|
||||
skeletonData = skeletonBinary.ReadSkeletonData(SkelPath);
|
||||
}
|
||||
catch
|
||||
{
|
||||
try
|
||||
{
|
||||
// 再尝试 Json 文件
|
||||
skeletonBinary = null;
|
||||
skeletonJson = new SkeletonJson(atlas);
|
||||
skeletonData = skeletonJson.ReadSkeletonData(SkelPath);
|
||||
}
|
||||
catch
|
||||
{
|
||||
// 都不行就报错
|
||||
throw new InvalidDataException($"Unknown skeleton file format {SkelPath}");
|
||||
}
|
||||
}
|
||||
|
||||
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).ToImmutableArray();
|
||||
AnimationNames = [EMPTY_ANIMATION, .. skeletonData.Animations.Select(v => v.Name)];
|
||||
|
||||
skeleton = new Skeleton(skeletonData) { Skin = new(Guid.NewGuid().ToString()) }; // 挂载一个空皮肤当作容器
|
||||
animationStateData = new AnimationStateData(skeletonData);
|
||||
animationState = new AnimationState(animationStateData);
|
||||
}
|
||||
|
||||
protected override void Dispose(bool disposing)
|
||||
{
|
||||
base.Dispose(disposing);
|
||||
atlas.Dispose();
|
||||
}
|
||||
|
||||
public override string FileVersion { get => skeletonData.Version; }
|
||||
|
||||
protected override float scale
|
||||
{
|
||||
get => Math.Abs(skeleton.ScaleX);
|
||||
set
|
||||
{
|
||||
skeleton.ScaleX = Math.Sign(skeleton.ScaleX) * value;
|
||||
skeleton.ScaleY = Math.Sign(skeleton.ScaleY) * value;
|
||||
}
|
||||
}
|
||||
|
||||
protected override PointF position
|
||||
{
|
||||
get => new(skeleton.X, skeleton.Y);
|
||||
set
|
||||
{
|
||||
skeleton.X = value.X;
|
||||
skeleton.Y = value.Y;
|
||||
}
|
||||
}
|
||||
|
||||
protected override bool flipX
|
||||
{
|
||||
get => skeleton.ScaleX < 0;
|
||||
set
|
||||
{
|
||||
if (skeleton.ScaleX > 0 && value || skeleton.ScaleX < 0 && !value)
|
||||
skeleton.ScaleX *= -1;
|
||||
}
|
||||
}
|
||||
|
||||
protected override bool flipY
|
||||
{
|
||||
get => skeleton.ScaleY < 0;
|
||||
set
|
||||
{
|
||||
if (skeleton.ScaleY > 0 && value || skeleton.ScaleY < 0 && !value)
|
||||
skeleton.ScaleY *= -1;
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
{
|
||||
// default 不需要加载
|
||||
if (name != "default" && skeletonData.FindSkin(name) is Skin sk)
|
||||
{
|
||||
skeleton.Skin.AddSkin(sk);
|
||||
skeleton.SetSlotsToSetupPose();
|
||||
}
|
||||
}
|
||||
|
||||
protected override void clearSkins()
|
||||
{
|
||||
skeleton.Skin.Clear();
|
||||
skeleton.SetSlotsToSetupPose();
|
||||
}
|
||||
|
||||
protected override int[] getTrackIndices() => animationState.Tracks.Select((_, i) => i).Where(i => animationState.Tracks.Items[i] is not null).ToArray();
|
||||
|
||||
protected override string getAnimation(int track) => animationState.GetCurrent(track)?.Animation.Name ?? EMPTY_ANIMATION;
|
||||
|
||||
protected override void setAnimation(int track, string name)
|
||||
{
|
||||
if (name == EMPTY_ANIMATION)
|
||||
animationState.SetAnimation(track, EmptyAnimation, false);
|
||||
else if (AnimationNames.Contains(name))
|
||||
animationState.SetAnimation(track, name, true);
|
||||
}
|
||||
|
||||
protected override void clearTrack(int i) => animationState.ClearTrack(i);
|
||||
|
||||
public override float GetAnimationDuration(string name) { return skeletonData.FindAnimation(name)?.Duration ?? 0f; }
|
||||
|
||||
protected override RectangleF getCurrentBounds()
|
||||
{
|
||||
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))
|
||||
{
|
||||
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)
|
||||
{
|
||||
animationState.Update(delta);
|
||||
animationState.Apply(skeleton);
|
||||
skeleton.Update(delta);
|
||||
skeleton.UpdateWorldTransform();
|
||||
}
|
||||
|
||||
protected override void draw(SFML.Graphics.RenderTarget target, SFML.Graphics.RenderStates states)
|
||||
{
|
||||
triangleVertices.Clear();
|
||||
states.Texture = null;
|
||||
states.Shader = SFMLShader.GetSpineShader(usePma);
|
||||
|
||||
// 要用 DrawOrder 而不是 Slots
|
||||
foreach (var slot in skeleton.DrawOrder)
|
||||
{
|
||||
var attachment = slot.Attachment;
|
||||
|
||||
SFML.Graphics.Texture texture;
|
||||
|
||||
float[] worldVertices = worldVerticesBuffer; // 顶点世界坐标, 连续的 [x0, y0, x1, y1, ...] 坐标值
|
||||
int worldVerticesCount; // 等于顶点数组的长度除以 2
|
||||
int[] worldTriangleIndices; // 三角形索引, 从顶点坐标数组取的时候要乘以 2, 最大值是 worldVerticesCount - 1
|
||||
int worldTriangleIndicesLength; // 三角形索引数组长度
|
||||
float[] uvs; // 纹理坐标
|
||||
float tintR = skeleton.R * slot.R;
|
||||
float tintG = skeleton.G * slot.G;
|
||||
float tintB = skeleton.B * slot.B;
|
||||
float tintA = skeleton.A * slot.A;
|
||||
|
||||
if (attachment is RegionAttachment regionAttachment)
|
||||
{
|
||||
texture = (SFML.Graphics.Texture)((AtlasRegion)regionAttachment.RendererObject).page.rendererObject;
|
||||
|
||||
regionAttachment.ComputeWorldVertices(slot.Bone, worldVertices, 0);
|
||||
worldVerticesCount = 4;
|
||||
worldTriangleIndices = [0, 1, 2, 2, 3, 0];
|
||||
worldTriangleIndicesLength = 6;
|
||||
uvs = regionAttachment.UVs;
|
||||
tintR *= regionAttachment.R;
|
||||
tintG *= regionAttachment.G;
|
||||
tintB *= regionAttachment.B;
|
||||
tintA *= regionAttachment.A;
|
||||
}
|
||||
else if (attachment is MeshAttachment meshAttachment)
|
||||
{
|
||||
texture = (SFML.Graphics.Texture)((AtlasRegion)meshAttachment.RendererObject).page.rendererObject;
|
||||
|
||||
if (meshAttachment.WorldVerticesLength > worldVertices.Length)
|
||||
worldVertices = worldVerticesBuffer = new float[meshAttachment.WorldVerticesLength * 2];
|
||||
meshAttachment.ComputeWorldVertices(slot, worldVertices);
|
||||
worldVerticesCount = meshAttachment.WorldVerticesLength / 2;
|
||||
worldTriangleIndices = meshAttachment.Triangles;
|
||||
worldTriangleIndicesLength = meshAttachment.Triangles.Length;
|
||||
uvs = meshAttachment.UVs;
|
||||
tintR *= meshAttachment.R;
|
||||
tintG *= meshAttachment.G;
|
||||
tintB *= meshAttachment.B;
|
||||
tintA *= meshAttachment.A;
|
||||
}
|
||||
else if (attachment is ClippingAttachment clippingAttachment)
|
||||
{
|
||||
clipping.ClipStart(slot, clippingAttachment);
|
||||
continue;
|
||||
}
|
||||
else
|
||||
{
|
||||
clipping.ClipEnd(slot);
|
||||
continue;
|
||||
}
|
||||
|
||||
SFML.Graphics.BlendMode blendMode = GetSFMLBlendMode(slot.Data.BlendMode);
|
||||
|
||||
states.Texture ??= texture;
|
||||
if (states.BlendMode != blendMode || states.Texture != texture)
|
||||
{
|
||||
if (triangleVertices.VertexCount > 0)
|
||||
{
|
||||
target.Draw(triangleVertices, states);
|
||||
triangleVertices.Clear();
|
||||
}
|
||||
states.BlendMode = blendMode;
|
||||
states.Texture = texture;
|
||||
}
|
||||
|
||||
if (clipping.IsClipping)
|
||||
{
|
||||
// 这里必须单独记录 Count, 和 Items 的 Length 是不一致的
|
||||
clipping.ClipTriangles(worldVertices, worldVerticesCount * 2, worldTriangleIndices, worldTriangleIndicesLength, uvs);
|
||||
worldVertices = clipping.ClippedVertices.Items;
|
||||
worldVerticesCount = clipping.ClippedVertices.Count / 2;
|
||||
worldTriangleIndices = clipping.ClippedTriangles.Items;
|
||||
worldTriangleIndicesLength = clipping.ClippedTriangles.Count;
|
||||
uvs = clipping.ClippedUVs.Items;
|
||||
}
|
||||
|
||||
var textureSizeX = texture.Size.X;
|
||||
var textureSizeY = texture.Size.Y;
|
||||
|
||||
SFML.Graphics.Vertex vertex = new();
|
||||
vertex.Color.R = (byte)(tintR * 255);
|
||||
vertex.Color.G = (byte)(tintG * 255);
|
||||
vertex.Color.B = (byte)(tintB * 255);
|
||||
vertex.Color.A = (byte)(tintA * 255);
|
||||
|
||||
// 必须用 worldTriangleIndicesLength 不能直接 foreach
|
||||
for (int i = 0; i < worldTriangleIndicesLength; i++)
|
||||
{
|
||||
var index = worldTriangleIndices[i] * 2;
|
||||
vertex.Position.X = worldVertices[index];
|
||||
vertex.Position.Y = worldVertices[index + 1];
|
||||
vertex.TexCoords.X = uvs[index] * textureSizeX;
|
||||
vertex.TexCoords.Y = uvs[index + 1] * textureSizeY;
|
||||
triangleVertices.Append(vertex);
|
||||
}
|
||||
|
||||
clipping.ClipEnd(slot);
|
||||
}
|
||||
clipping.ClipEnd();
|
||||
|
||||
target.Draw(triangleVertices, states);
|
||||
}
|
||||
|
||||
protected override void debugDraw(SFML.Graphics.RenderTarget target)
|
||||
{
|
||||
lineVertices.Clear();
|
||||
rectLineVertices.Clear();
|
||||
|
||||
if (debugRegions)
|
||||
{
|
||||
SFML.Graphics.Vertex vt = new() { Color = AttachmentLineColor };
|
||||
foreach (var slot in skeleton.Slots)
|
||||
{
|
||||
if (slot.Bone.Active && slot.Attachment is RegionAttachment regionAttachment)
|
||||
{
|
||||
regionAttachment.ComputeWorldVertices(slot.Bone, worldVerticesBuffer, 0);
|
||||
|
||||
vt.Position.X = worldVerticesBuffer[0];
|
||||
vt.Position.Y = worldVerticesBuffer[1];
|
||||
lineVertices.Append(vt);
|
||||
|
||||
vt.Position.X = worldVerticesBuffer[2];
|
||||
vt.Position.Y = worldVerticesBuffer[3];
|
||||
lineVertices.Append(vt); lineVertices.Append(vt);
|
||||
|
||||
vt.Position.X = worldVerticesBuffer[4];
|
||||
vt.Position.Y = worldVerticesBuffer[5];
|
||||
lineVertices.Append(vt); lineVertices.Append(vt);
|
||||
|
||||
vt.Position.X = worldVerticesBuffer[6];
|
||||
vt.Position.Y = worldVerticesBuffer[7];
|
||||
lineVertices.Append(vt); lineVertices.Append(vt);
|
||||
|
||||
vt.Position.X = worldVerticesBuffer[0];
|
||||
vt.Position.Y = worldVerticesBuffer[1];
|
||||
lineVertices.Append(vt);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (debugMeshes)
|
||||
{
|
||||
SFML.Graphics.Vertex vt = new() { Color = MeshLineColor };
|
||||
foreach (var slot in skeleton.Slots)
|
||||
{
|
||||
if (slot.Bone.Active && slot.Attachment is MeshAttachment meshAttachment)
|
||||
{
|
||||
if (meshAttachment.WorldVerticesLength > worldVerticesBuffer.Length)
|
||||
worldVerticesBuffer = new float[meshAttachment.WorldVerticesLength * 2];
|
||||
|
||||
meshAttachment.ComputeWorldVertices(slot, worldVerticesBuffer);
|
||||
|
||||
var triangleIndices = meshAttachment.Triangles;
|
||||
for (int i = 0; i < triangleIndices.Length; i += 3)
|
||||
{
|
||||
var idx0 = triangleIndices[i] * 2;
|
||||
var idx1 = triangleIndices[i + 1] * 2;
|
||||
var idx2 = triangleIndices[i + 2] * 2;
|
||||
|
||||
vt.Position.X = worldVerticesBuffer[idx0];
|
||||
vt.Position.Y = worldVerticesBuffer[idx0 + 1];
|
||||
lineVertices.Append(vt);
|
||||
|
||||
vt.Position.X = worldVerticesBuffer[idx1];
|
||||
vt.Position.Y = worldVerticesBuffer[idx1 + 1];
|
||||
lineVertices.Append(vt); lineVertices.Append(vt);
|
||||
|
||||
vt.Position.X = worldVerticesBuffer[idx2];
|
||||
vt.Position.Y = worldVerticesBuffer[idx2 + 1];
|
||||
lineVertices.Append(vt); lineVertices.Append(vt);
|
||||
|
||||
vt.Position.X = worldVerticesBuffer[idx0];
|
||||
vt.Position.Y = worldVerticesBuffer[idx0 + 1];
|
||||
lineVertices.Append(vt);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (debugMeshHulls)
|
||||
{
|
||||
SFML.Graphics.Vertex vt = new() { Color = AttachmentLineColor };
|
||||
foreach (var slot in skeleton.Slots)
|
||||
{
|
||||
if (slot.Bone.Active && slot.Attachment is MeshAttachment meshAttachment)
|
||||
{
|
||||
if (meshAttachment.WorldVerticesLength > worldVerticesBuffer.Length)
|
||||
worldVerticesBuffer = new float[meshAttachment.WorldVerticesLength * 2];
|
||||
|
||||
meshAttachment.ComputeWorldVertices(slot, worldVerticesBuffer);
|
||||
|
||||
var hullLength = (meshAttachment.HullLength >> 1) << 1;
|
||||
|
||||
if (debugMeshHulls && hullLength > 2)
|
||||
{
|
||||
vt.Position.X = worldVerticesBuffer[0];
|
||||
vt.Position.Y = worldVerticesBuffer[1];
|
||||
lineVertices.Append(vt);
|
||||
|
||||
for (int i = 2; i < hullLength; i += 2)
|
||||
{
|
||||
vt.Position.X = worldVerticesBuffer[i];
|
||||
vt.Position.Y = worldVerticesBuffer[i + 1];
|
||||
lineVertices.Append(vt);
|
||||
lineVertices.Append(vt);
|
||||
}
|
||||
|
||||
vt.Position.X = worldVerticesBuffer[0];
|
||||
vt.Position.Y = worldVerticesBuffer[1];
|
||||
lineVertices.Append(vt);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (debugBoundingBoxes)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
if (debugPaths)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
if (debugClippings)
|
||||
{
|
||||
SFML.Graphics.Vertex vt = new() { Color = ClippingLineColor };
|
||||
foreach (var slot in skeleton.Slots)
|
||||
{
|
||||
if (slot.Bone.Active && slot.Attachment is ClippingAttachment clippingAttachment)
|
||||
{
|
||||
if (clippingAttachment.WorldVerticesLength > worldVerticesBuffer.Length)
|
||||
worldVerticesBuffer = worldVerticesBuffer = new float[clippingAttachment.WorldVerticesLength * 2];
|
||||
|
||||
clippingAttachment.ComputeWorldVertices(slot, worldVerticesBuffer);
|
||||
|
||||
vt.Position.X = worldVerticesBuffer[0];
|
||||
vt.Position.Y = worldVerticesBuffer[1];
|
||||
lineVertices.Append(vt);
|
||||
|
||||
for (int i = 2; i < clippingAttachment.WorldVerticesLength; i += 2)
|
||||
{
|
||||
vt.Position.X = worldVerticesBuffer[i];
|
||||
vt.Position.Y = worldVerticesBuffer[i + 1];
|
||||
lineVertices.Append(vt);
|
||||
lineVertices.Append(vt);
|
||||
}
|
||||
|
||||
vt.Position.X = worldVerticesBuffer[0];
|
||||
vt.Position.Y = worldVerticesBuffer[1];
|
||||
lineVertices.Append(vt);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (debugBounds)
|
||||
{
|
||||
var vt = new SFML.Graphics.Vertex() { Color = BoundsColor };
|
||||
var b = getCurrentBounds();
|
||||
|
||||
vt.Position.X = b.Left;
|
||||
vt.Position.Y = b.Top;
|
||||
lineVertices.Append(vt);
|
||||
|
||||
vt.Position.X = b.Right;
|
||||
vt.Position.Y = b.Top;
|
||||
lineVertices.Append(vt); lineVertices.Append(vt);
|
||||
|
||||
vt.Position.X = b.Right;
|
||||
vt.Position.Y = b.Bottom;
|
||||
lineVertices.Append(vt); lineVertices.Append(vt);
|
||||
|
||||
vt.Position.X = b.Left;
|
||||
vt.Position.Y = b.Bottom;
|
||||
lineVertices.Append(vt); lineVertices.Append(vt);
|
||||
|
||||
vt.Position.X = b.Left;
|
||||
vt.Position.Y = b.Top;
|
||||
lineVertices.Append(vt);
|
||||
}
|
||||
|
||||
// 骨骼线放最后画
|
||||
if (debugBones)
|
||||
{
|
||||
var width = scale;
|
||||
foreach (var bone in skeleton.Bones)
|
||||
{
|
||||
if (!bone.Active) continue;
|
||||
var boneLength = bone.Data.Length;
|
||||
var p1 = new SFML.System.Vector2f(bone.WorldX, bone.WorldY);
|
||||
var p2 = new SFML.System.Vector2f(bone.WorldX + boneLength * bone.A, bone.WorldY + boneLength * bone.C);
|
||||
AddRectLine(p1, p2, BoneLineColor, width);
|
||||
}
|
||||
}
|
||||
|
||||
target.Draw(lineVertices);
|
||||
target.Draw(rectLineVertices);
|
||||
|
||||
// 骨骼的点最后画, 层级处于骨骼线上面
|
||||
if (debugBones)
|
||||
{
|
||||
var radius = scale;
|
||||
foreach (var bone in skeleton.Bones)
|
||||
{
|
||||
if (!bone.Active) continue;
|
||||
DrawCirclePoint(target, new(bone.WorldX, bone.WorldY), BonePointColor, radius);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
582
SpineViewer/Spine/Implementations/SpineObject/SpineObject41.cs
Normal file
582
SpineViewer/Spine/Implementations/SpineObject/SpineObject41.cs
Normal file
@@ -0,0 +1,582 @@
|
||||
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
|
||||
{
|
||||
[SpineImplementation(SpineVersion.V41)]
|
||||
internal class SpineObject41 : Spine.SpineObject
|
||||
{
|
||||
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
|
||||
{
|
||||
public void Load(AtlasPage page, string path)
|
||||
{
|
||||
var texture = new SFML.Graphics.Texture(path);
|
||||
if (page.magFilter == TextureFilter.Linear)
|
||||
texture.Smooth = true;
|
||||
if (page.uWrap == TextureWrap.Repeat && page.vWrap == TextureWrap.Repeat)
|
||||
texture.Repeated = true;
|
||||
|
||||
page.rendererObject = texture;
|
||||
}
|
||||
|
||||
public void Unload(object texture)
|
||||
{
|
||||
((SFML.Graphics.Texture)texture).Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
private 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 readonly SkeletonData skeletonData;
|
||||
private readonly AnimationStateData animationStateData;
|
||||
|
||||
private readonly Skeleton skeleton;
|
||||
private readonly AnimationState animationState;
|
||||
|
||||
private readonly SkeletonClipping clipping = new();
|
||||
|
||||
/// <summary>
|
||||
/// 所有插槽在所有皮肤中可用的附件集合
|
||||
/// </summary>
|
||||
private readonly Dictionary<string, Dictionary<string, Attachment>> slotAttachments = [];
|
||||
|
||||
public SpineObject41(string skelPath, string atlasPath) : base(skelPath, atlasPath)
|
||||
{
|
||||
atlas = new Atlas(AtlasPath, textureLoader);
|
||||
try
|
||||
{
|
||||
// 先尝试二进制文件
|
||||
skeletonJson = null;
|
||||
skeletonBinary = new SkeletonBinary(atlas);
|
||||
skeletonData = skeletonBinary.ReadSkeletonData(SkelPath);
|
||||
}
|
||||
catch
|
||||
{
|
||||
try
|
||||
{
|
||||
// 再尝试 Json 文件
|
||||
skeletonBinary = null;
|
||||
skeletonJson = new SkeletonJson(atlas);
|
||||
skeletonData = skeletonJson.ReadSkeletonData(SkelPath);
|
||||
}
|
||||
catch
|
||||
{
|
||||
// 都不行就报错
|
||||
throw new InvalidDataException($"Unknown skeleton file format {SkelPath}");
|
||||
}
|
||||
}
|
||||
|
||||
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).ToImmutableArray();
|
||||
AnimationNames = [EMPTY_ANIMATION, .. skeletonData.Animations.Select(v => v.Name)];
|
||||
|
||||
skeleton = new Skeleton(skeletonData) { Skin = new(Guid.NewGuid().ToString()) }; // 挂载一个空皮肤当作容器
|
||||
animationStateData = new AnimationStateData(skeletonData);
|
||||
animationState = new AnimationState(animationStateData);
|
||||
}
|
||||
|
||||
protected override void Dispose(bool disposing)
|
||||
{
|
||||
base.Dispose(disposing);
|
||||
atlas.Dispose();
|
||||
}
|
||||
|
||||
public override string FileVersion { get => skeletonData.Version; }
|
||||
|
||||
protected override float scale
|
||||
{
|
||||
get => Math.Abs(skeleton.ScaleX);
|
||||
set
|
||||
{
|
||||
skeleton.ScaleX = Math.Sign(skeleton.ScaleX) * value;
|
||||
skeleton.ScaleY = Math.Sign(skeleton.ScaleY) * value;
|
||||
}
|
||||
}
|
||||
|
||||
protected override PointF position
|
||||
{
|
||||
get => new(skeleton.X, skeleton.Y);
|
||||
set
|
||||
{
|
||||
skeleton.X = value.X;
|
||||
skeleton.Y = value.Y;
|
||||
}
|
||||
}
|
||||
|
||||
protected override bool flipX
|
||||
{
|
||||
get => skeleton.ScaleX < 0;
|
||||
set
|
||||
{
|
||||
if (skeleton.ScaleX > 0 && value || skeleton.ScaleX < 0 && !value)
|
||||
skeleton.ScaleX *= -1;
|
||||
}
|
||||
}
|
||||
|
||||
protected override bool flipY
|
||||
{
|
||||
get => skeleton.ScaleY < 0;
|
||||
set
|
||||
{
|
||||
if (skeleton.ScaleY > 0 && value || skeleton.ScaleY < 0 && !value)
|
||||
skeleton.ScaleY *= -1;
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
{
|
||||
// default 不需要加载
|
||||
if (name != "default" && skeletonData.FindSkin(name) is Skin sk)
|
||||
{
|
||||
skeleton.Skin.AddSkin(sk);
|
||||
skeleton.SetSlotsToSetupPose();
|
||||
}
|
||||
}
|
||||
|
||||
protected override void clearSkins()
|
||||
{
|
||||
skeleton.Skin.Clear();
|
||||
skeleton.SetSlotsToSetupPose();
|
||||
}
|
||||
|
||||
protected override int[] getTrackIndices() => animationState.Tracks.Select((_, i) => i).Where(i => animationState.Tracks.Items[i] is not null).ToArray();
|
||||
|
||||
protected override string getAnimation(int track) => animationState.GetCurrent(track)?.Animation.Name ?? EMPTY_ANIMATION;
|
||||
|
||||
protected override void setAnimation(int track, string name)
|
||||
{
|
||||
if (name == EMPTY_ANIMATION)
|
||||
animationState.SetAnimation(track, EmptyAnimation, false);
|
||||
else if (AnimationNames.Contains(name))
|
||||
animationState.SetAnimation(track, name, true);
|
||||
}
|
||||
|
||||
protected override void clearTrack(int i) => animationState.ClearTrack(i);
|
||||
|
||||
public override float GetAnimationDuration(string name) { return skeletonData.FindAnimation(name)?.Duration ?? 0f; }
|
||||
|
||||
protected override RectangleF getCurrentBounds()
|
||||
{
|
||||
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))
|
||||
{
|
||||
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)
|
||||
{
|
||||
animationState.Update(delta);
|
||||
animationState.Apply(skeleton);
|
||||
//skeleton.Update(delta); // XXX: v4.1.x 没有 Update 方法
|
||||
skeleton.UpdateWorldTransform();
|
||||
}
|
||||
|
||||
protected override void draw(SFML.Graphics.RenderTarget target, SFML.Graphics.RenderStates states)
|
||||
{
|
||||
triangleVertices.Clear();
|
||||
states.Texture = null;
|
||||
states.Shader = SFMLShader.GetSpineShader(usePma);
|
||||
|
||||
// 要用 DrawOrder 而不是 Slots
|
||||
foreach (var slot in skeleton.DrawOrder)
|
||||
{
|
||||
var attachment = slot.Attachment;
|
||||
|
||||
SFML.Graphics.Texture texture;
|
||||
|
||||
float[] worldVertices = worldVerticesBuffer; // 顶点世界坐标, 连续的 [x0, y0, x1, y1, ...] 坐标值
|
||||
int worldVerticesCount; // 等于顶点数组的长度除以 2
|
||||
int[] worldTriangleIndices; // 三角形索引, 从顶点坐标数组取的时候要乘以 2, 最大值是 worldVerticesCount - 1
|
||||
int worldTriangleIndicesLength; // 三角形索引数组长度
|
||||
float[] uvs; // 纹理坐标
|
||||
float tintR = skeleton.R * slot.R;
|
||||
float tintG = skeleton.G * slot.G;
|
||||
float tintB = skeleton.B * slot.B;
|
||||
float tintA = skeleton.A * slot.A;
|
||||
|
||||
if (attachment is RegionAttachment regionAttachment)
|
||||
{
|
||||
texture = (SFML.Graphics.Texture)((AtlasRegion)regionAttachment.Region).page.rendererObject;
|
||||
|
||||
regionAttachment.ComputeWorldVertices(slot, worldVertices, 0);
|
||||
worldVerticesCount = 4;
|
||||
worldTriangleIndices = [0, 1, 2, 2, 3, 0];
|
||||
worldTriangleIndicesLength = 6;
|
||||
uvs = regionAttachment.UVs;
|
||||
tintR *= regionAttachment.R;
|
||||
tintG *= regionAttachment.G;
|
||||
tintB *= regionAttachment.B;
|
||||
tintA *= regionAttachment.A;
|
||||
}
|
||||
else if (attachment is MeshAttachment meshAttachment)
|
||||
{
|
||||
texture = (SFML.Graphics.Texture)((AtlasRegion)meshAttachment.Region).page.rendererObject;
|
||||
|
||||
if (meshAttachment.WorldVerticesLength > worldVertices.Length)
|
||||
worldVertices = worldVerticesBuffer = new float[meshAttachment.WorldVerticesLength * 2];
|
||||
meshAttachment.ComputeWorldVertices(slot, worldVertices);
|
||||
worldVerticesCount = meshAttachment.WorldVerticesLength / 2;
|
||||
worldTriangleIndices = meshAttachment.Triangles;
|
||||
worldTriangleIndicesLength = meshAttachment.Triangles.Length;
|
||||
uvs = meshAttachment.UVs;
|
||||
tintR *= meshAttachment.R;
|
||||
tintG *= meshAttachment.G;
|
||||
tintB *= meshAttachment.B;
|
||||
tintA *= meshAttachment.A;
|
||||
}
|
||||
else if (attachment is ClippingAttachment clippingAttachment)
|
||||
{
|
||||
clipping.ClipStart(slot, clippingAttachment);
|
||||
continue;
|
||||
}
|
||||
else
|
||||
{
|
||||
clipping.ClipEnd(slot);
|
||||
continue;
|
||||
}
|
||||
|
||||
SFML.Graphics.BlendMode blendMode = GetSFMLBlendMode(slot.Data.BlendMode);
|
||||
|
||||
states.Texture ??= texture;
|
||||
if (states.BlendMode != blendMode || states.Texture != texture)
|
||||
{
|
||||
if (triangleVertices.VertexCount > 0)
|
||||
{
|
||||
target.Draw(triangleVertices, states);
|
||||
triangleVertices.Clear();
|
||||
}
|
||||
states.BlendMode = blendMode;
|
||||
states.Texture = texture;
|
||||
}
|
||||
|
||||
if (clipping.IsClipping)
|
||||
{
|
||||
// 这里必须单独记录 Count, 和 Items 的 Length 是不一致的
|
||||
clipping.ClipTriangles(worldVertices, worldVerticesCount * 2, worldTriangleIndices, worldTriangleIndicesLength, uvs);
|
||||
worldVertices = clipping.ClippedVertices.Items;
|
||||
worldVerticesCount = clipping.ClippedVertices.Count / 2;
|
||||
worldTriangleIndices = clipping.ClippedTriangles.Items;
|
||||
worldTriangleIndicesLength = clipping.ClippedTriangles.Count;
|
||||
uvs = clipping.ClippedUVs.Items;
|
||||
}
|
||||
|
||||
var textureSizeX = texture.Size.X;
|
||||
var textureSizeY = texture.Size.Y;
|
||||
|
||||
SFML.Graphics.Vertex vertex = new();
|
||||
vertex.Color.R = (byte)(tintR * 255);
|
||||
vertex.Color.G = (byte)(tintG * 255);
|
||||
vertex.Color.B = (byte)(tintB * 255);
|
||||
vertex.Color.A = (byte)(tintA * 255);
|
||||
|
||||
// 必须用 worldTriangleIndicesLength 不能直接 foreach
|
||||
for (int i = 0; i < worldTriangleIndicesLength; i++)
|
||||
{
|
||||
var index = worldTriangleIndices[i] * 2;
|
||||
vertex.Position.X = worldVertices[index];
|
||||
vertex.Position.Y = worldVertices[index + 1];
|
||||
vertex.TexCoords.X = uvs[index] * textureSizeX;
|
||||
vertex.TexCoords.Y = uvs[index + 1] * textureSizeY;
|
||||
triangleVertices.Append(vertex);
|
||||
}
|
||||
|
||||
clipping.ClipEnd(slot);
|
||||
}
|
||||
clipping.ClipEnd();
|
||||
|
||||
target.Draw(triangleVertices, states);
|
||||
}
|
||||
|
||||
protected override void debugDraw(SFML.Graphics.RenderTarget target)
|
||||
{
|
||||
lineVertices.Clear();
|
||||
rectLineVertices.Clear();
|
||||
|
||||
if (debugRegions)
|
||||
{
|
||||
SFML.Graphics.Vertex vt = new() { Color = AttachmentLineColor };
|
||||
foreach (var slot in skeleton.Slots)
|
||||
{
|
||||
if (slot.Bone.Active && slot.Attachment is RegionAttachment regionAttachment)
|
||||
{
|
||||
regionAttachment.ComputeWorldVertices(slot, worldVerticesBuffer, 0);
|
||||
|
||||
vt.Position.X = worldVerticesBuffer[0];
|
||||
vt.Position.Y = worldVerticesBuffer[1];
|
||||
lineVertices.Append(vt);
|
||||
|
||||
vt.Position.X = worldVerticesBuffer[2];
|
||||
vt.Position.Y = worldVerticesBuffer[3];
|
||||
lineVertices.Append(vt); lineVertices.Append(vt);
|
||||
|
||||
vt.Position.X = worldVerticesBuffer[4];
|
||||
vt.Position.Y = worldVerticesBuffer[5];
|
||||
lineVertices.Append(vt); lineVertices.Append(vt);
|
||||
|
||||
vt.Position.X = worldVerticesBuffer[6];
|
||||
vt.Position.Y = worldVerticesBuffer[7];
|
||||
lineVertices.Append(vt); lineVertices.Append(vt);
|
||||
|
||||
vt.Position.X = worldVerticesBuffer[0];
|
||||
vt.Position.Y = worldVerticesBuffer[1];
|
||||
lineVertices.Append(vt);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (debugMeshes)
|
||||
{
|
||||
SFML.Graphics.Vertex vt = new() { Color = MeshLineColor };
|
||||
foreach (var slot in skeleton.Slots)
|
||||
{
|
||||
if (slot.Bone.Active && slot.Attachment is MeshAttachment meshAttachment)
|
||||
{
|
||||
if (meshAttachment.WorldVerticesLength > worldVerticesBuffer.Length)
|
||||
worldVerticesBuffer = new float[meshAttachment.WorldVerticesLength * 2];
|
||||
|
||||
meshAttachment.ComputeWorldVertices(slot, worldVerticesBuffer);
|
||||
|
||||
var triangleIndices = meshAttachment.Triangles;
|
||||
for (int i = 0; i < triangleIndices.Length; i += 3)
|
||||
{
|
||||
var idx0 = triangleIndices[i] * 2;
|
||||
var idx1 = triangleIndices[i + 1] * 2;
|
||||
var idx2 = triangleIndices[i + 2] * 2;
|
||||
|
||||
vt.Position.X = worldVerticesBuffer[idx0];
|
||||
vt.Position.Y = worldVerticesBuffer[idx0 + 1];
|
||||
lineVertices.Append(vt);
|
||||
|
||||
vt.Position.X = worldVerticesBuffer[idx1];
|
||||
vt.Position.Y = worldVerticesBuffer[idx1 + 1];
|
||||
lineVertices.Append(vt); lineVertices.Append(vt);
|
||||
|
||||
vt.Position.X = worldVerticesBuffer[idx2];
|
||||
vt.Position.Y = worldVerticesBuffer[idx2 + 1];
|
||||
lineVertices.Append(vt); lineVertices.Append(vt);
|
||||
|
||||
vt.Position.X = worldVerticesBuffer[idx0];
|
||||
vt.Position.Y = worldVerticesBuffer[idx0 + 1];
|
||||
lineVertices.Append(vt);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (debugMeshHulls)
|
||||
{
|
||||
SFML.Graphics.Vertex vt = new() { Color = AttachmentLineColor };
|
||||
foreach (var slot in skeleton.Slots)
|
||||
{
|
||||
if (slot.Bone.Active && slot.Attachment is MeshAttachment meshAttachment)
|
||||
{
|
||||
if (meshAttachment.WorldVerticesLength > worldVerticesBuffer.Length)
|
||||
worldVerticesBuffer = new float[meshAttachment.WorldVerticesLength * 2];
|
||||
|
||||
meshAttachment.ComputeWorldVertices(slot, worldVerticesBuffer);
|
||||
|
||||
var hullLength = (meshAttachment.HullLength >> 1) << 1;
|
||||
|
||||
if (debugMeshHulls && hullLength > 2)
|
||||
{
|
||||
vt.Position.X = worldVerticesBuffer[0];
|
||||
vt.Position.Y = worldVerticesBuffer[1];
|
||||
lineVertices.Append(vt);
|
||||
|
||||
for (int i = 2; i < hullLength; i += 2)
|
||||
{
|
||||
vt.Position.X = worldVerticesBuffer[i];
|
||||
vt.Position.Y = worldVerticesBuffer[i + 1];
|
||||
lineVertices.Append(vt);
|
||||
lineVertices.Append(vt);
|
||||
}
|
||||
|
||||
vt.Position.X = worldVerticesBuffer[0];
|
||||
vt.Position.Y = worldVerticesBuffer[1];
|
||||
lineVertices.Append(vt);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (debugBoundingBoxes)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
if (debugPaths)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
if (debugClippings)
|
||||
{
|
||||
SFML.Graphics.Vertex vt = new() { Color = ClippingLineColor };
|
||||
foreach (var slot in skeleton.Slots)
|
||||
{
|
||||
if (slot.Bone.Active && slot.Attachment is ClippingAttachment clippingAttachment)
|
||||
{
|
||||
if (clippingAttachment.WorldVerticesLength > worldVerticesBuffer.Length)
|
||||
worldVerticesBuffer = worldVerticesBuffer = new float[clippingAttachment.WorldVerticesLength * 2];
|
||||
|
||||
clippingAttachment.ComputeWorldVertices(slot, worldVerticesBuffer);
|
||||
|
||||
vt.Position.X = worldVerticesBuffer[0];
|
||||
vt.Position.Y = worldVerticesBuffer[1];
|
||||
lineVertices.Append(vt);
|
||||
|
||||
for (int i = 2; i < clippingAttachment.WorldVerticesLength; i += 2)
|
||||
{
|
||||
vt.Position.X = worldVerticesBuffer[i];
|
||||
vt.Position.Y = worldVerticesBuffer[i + 1];
|
||||
lineVertices.Append(vt);
|
||||
lineVertices.Append(vt);
|
||||
}
|
||||
|
||||
vt.Position.X = worldVerticesBuffer[0];
|
||||
vt.Position.Y = worldVerticesBuffer[1];
|
||||
lineVertices.Append(vt);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (debugBounds)
|
||||
{
|
||||
var vt = new SFML.Graphics.Vertex() { Color = BoundsColor };
|
||||
var b = getCurrentBounds();
|
||||
|
||||
vt.Position.X = b.Left;
|
||||
vt.Position.Y = b.Top;
|
||||
lineVertices.Append(vt);
|
||||
|
||||
vt.Position.X = b.Right;
|
||||
vt.Position.Y = b.Top;
|
||||
lineVertices.Append(vt); lineVertices.Append(vt);
|
||||
|
||||
vt.Position.X = b.Right;
|
||||
vt.Position.Y = b.Bottom;
|
||||
lineVertices.Append(vt); lineVertices.Append(vt);
|
||||
|
||||
vt.Position.X = b.Left;
|
||||
vt.Position.Y = b.Bottom;
|
||||
lineVertices.Append(vt); lineVertices.Append(vt);
|
||||
|
||||
vt.Position.X = b.Left;
|
||||
vt.Position.Y = b.Top;
|
||||
lineVertices.Append(vt);
|
||||
}
|
||||
|
||||
// 骨骼线放最后画
|
||||
if (debugBones)
|
||||
{
|
||||
var width = scale;
|
||||
foreach (var bone in skeleton.Bones)
|
||||
{
|
||||
if (!bone.Active) continue;
|
||||
var boneLength = bone.Data.Length;
|
||||
var p1 = new SFML.System.Vector2f(bone.WorldX, bone.WorldY);
|
||||
var p2 = new SFML.System.Vector2f(bone.WorldX + boneLength * bone.A, bone.WorldY + boneLength * bone.C);
|
||||
AddRectLine(p1, p2, BoneLineColor, width);
|
||||
}
|
||||
}
|
||||
|
||||
target.Draw(lineVertices);
|
||||
target.Draw(rectLineVertices);
|
||||
|
||||
// 骨骼的点最后画, 层级处于骨骼线上面
|
||||
if (debugBones)
|
||||
{
|
||||
var radius = scale;
|
||||
foreach (var bone in skeleton.Bones)
|
||||
{
|
||||
if (!bone.Active) continue;
|
||||
DrawCirclePoint(target, new(bone.WorldX, bone.WorldY), BonePointColor, radius);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
583
SpineViewer/Spine/Implementations/SpineObject/SpineObject42.cs
Normal file
583
SpineViewer/Spine/Implementations/SpineObject/SpineObject42.cs
Normal file
@@ -0,0 +1,583 @@
|
||||
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
|
||||
{
|
||||
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
|
||||
{
|
||||
public void Load(AtlasPage page, string path)
|
||||
{
|
||||
var texture = new SFML.Graphics.Texture(path);
|
||||
if (page.magFilter == TextureFilter.Linear)
|
||||
texture.Smooth = true;
|
||||
if (page.uWrap == TextureWrap.Repeat && page.vWrap == TextureWrap.Repeat)
|
||||
texture.Repeated = true;
|
||||
|
||||
page.rendererObject = texture;
|
||||
}
|
||||
|
||||
public void Unload(object texture)
|
||||
{
|
||||
((SFML.Graphics.Texture)texture).Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
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 readonly SkeletonData skeletonData;
|
||||
private readonly AnimationStateData animationStateData;
|
||||
|
||||
private readonly Skeleton skeleton;
|
||||
private readonly AnimationState animationState;
|
||||
|
||||
private readonly SkeletonClipping clipping = new();
|
||||
|
||||
/// <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
|
||||
{
|
||||
// 先尝试二进制文件
|
||||
skeletonJson = null;
|
||||
skeletonBinary = new SkeletonBinary(atlas);
|
||||
skeletonData = skeletonBinary.ReadSkeletonData(SkelPath);
|
||||
}
|
||||
catch
|
||||
{
|
||||
try
|
||||
{
|
||||
// 再尝试 Json 文件
|
||||
skeletonBinary = null;
|
||||
skeletonJson = new SkeletonJson(atlas);
|
||||
skeletonData = skeletonJson.ReadSkeletonData(SkelPath);
|
||||
}
|
||||
catch
|
||||
{
|
||||
// 都不行就报错
|
||||
throw new InvalidDataException($"Unknown skeleton file format {SkelPath}");
|
||||
}
|
||||
}
|
||||
|
||||
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).ToImmutableArray();
|
||||
AnimationNames = [EMPTY_ANIMATION, .. skeletonData.Animations.Select(v => v.Name)];
|
||||
|
||||
skeleton = new Skeleton(skeletonData) { Skin = new(Guid.NewGuid().ToString()) }; // 挂载一个空皮肤当作容器
|
||||
animationStateData = new AnimationStateData(skeletonData);
|
||||
animationState = new AnimationState(animationStateData);
|
||||
}
|
||||
|
||||
protected override void Dispose(bool disposing)
|
||||
{
|
||||
base.Dispose(disposing);
|
||||
atlas.Dispose();
|
||||
}
|
||||
|
||||
public override string FileVersion { get => skeletonData.Version; }
|
||||
|
||||
protected override float scale
|
||||
{
|
||||
get => Math.Abs(skeleton.ScaleX);
|
||||
set
|
||||
{
|
||||
skeleton.ScaleX = Math.Sign(skeleton.ScaleX) * value;
|
||||
skeleton.ScaleY = Math.Sign(skeleton.ScaleY) * value;
|
||||
}
|
||||
}
|
||||
|
||||
protected override PointF position
|
||||
{
|
||||
get => new(skeleton.X, skeleton.Y);
|
||||
set
|
||||
{
|
||||
skeleton.X = value.X;
|
||||
skeleton.Y = value.Y;
|
||||
}
|
||||
}
|
||||
|
||||
protected override bool flipX
|
||||
{
|
||||
get => skeleton.ScaleX < 0;
|
||||
set
|
||||
{
|
||||
if (skeleton.ScaleX > 0 && value || skeleton.ScaleX < 0 && !value)
|
||||
skeleton.ScaleX *= -1;
|
||||
}
|
||||
}
|
||||
|
||||
protected override bool flipY
|
||||
{
|
||||
get => skeleton.ScaleY < 0;
|
||||
set
|
||||
{
|
||||
if (skeleton.ScaleY > 0 && value || skeleton.ScaleY < 0 && !value)
|
||||
skeleton.ScaleY *= -1;
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
{
|
||||
// default 不需要加载
|
||||
if (name != "default" && skeletonData.FindSkin(name) is Skin sk)
|
||||
{
|
||||
skeleton.Skin.AddSkin(sk);
|
||||
skeleton.SetSlotsToSetupPose();
|
||||
}
|
||||
}
|
||||
|
||||
protected override void clearSkins()
|
||||
{
|
||||
skeleton.Skin.Clear();
|
||||
skeleton.SetSlotsToSetupPose();
|
||||
}
|
||||
|
||||
protected override int[] getTrackIndices() => animationState.Tracks.Select((_, i) => i).Where(i => animationState.Tracks.Items[i] is not null).ToArray();
|
||||
|
||||
protected override string getAnimation(int track) => animationState.GetCurrent(track)?.Animation.Name ?? EMPTY_ANIMATION;
|
||||
|
||||
protected override void setAnimation(int track, string name)
|
||||
{
|
||||
if (name == EMPTY_ANIMATION)
|
||||
animationState.SetAnimation(track, EmptyAnimation, false);
|
||||
else if (AnimationNames.Contains(name))
|
||||
animationState.SetAnimation(track, name, true);
|
||||
}
|
||||
|
||||
protected override void clearTrack(int i) => animationState.ClearTrack(i);
|
||||
|
||||
public override float GetAnimationDuration(string name) { return skeletonData.FindAnimation(name)?.Duration ?? 0f; }
|
||||
|
||||
protected override RectangleF getCurrentBounds()
|
||||
{
|
||||
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))
|
||||
{
|
||||
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)
|
||||
{
|
||||
animationState.Update(delta);
|
||||
animationState.Apply(skeleton);
|
||||
skeleton.Update(delta);
|
||||
skeleton.UpdateWorldTransform(Skeleton.Physics.Update);
|
||||
}
|
||||
|
||||
protected override void draw(SFML.Graphics.RenderTarget target, SFML.Graphics.RenderStates states)
|
||||
{
|
||||
triangleVertices.Clear();
|
||||
states.Texture = null;
|
||||
states.Shader = SFMLShader.GetSpineShader(usePma);
|
||||
|
||||
// 要用 DrawOrder 而不是 Slots
|
||||
foreach (var slot in skeleton.DrawOrder)
|
||||
{
|
||||
var attachment = slot.Attachment;
|
||||
|
||||
SFML.Graphics.Texture texture;
|
||||
|
||||
float[] worldVertices = worldVerticesBuffer; // 顶点世界坐标, 连续的 [x0, y0, x1, y1, ...] 坐标值
|
||||
int worldVerticesCount; // 等于顶点数组的长度除以 2
|
||||
int[] worldTriangleIndices; // 三角形索引, 从顶点坐标数组取的时候要乘以 2, 最大值是 worldVerticesCount - 1
|
||||
int worldTriangleIndicesLength; // 三角形索引数组长度
|
||||
float[] uvs; // 纹理坐标
|
||||
float tintR = skeleton.R * slot.R;
|
||||
float tintG = skeleton.G * slot.G;
|
||||
float tintB = skeleton.B * slot.B;
|
||||
float tintA = skeleton.A * slot.A;
|
||||
|
||||
if (attachment is RegionAttachment regionAttachment)
|
||||
{
|
||||
texture = (SFML.Graphics.Texture)((AtlasRegion)regionAttachment.Region).page.rendererObject;
|
||||
|
||||
regionAttachment.ComputeWorldVertices(slot, worldVertices, 0);
|
||||
worldVerticesCount = 4;
|
||||
worldTriangleIndices = [0, 1, 2, 2, 3, 0];
|
||||
worldTriangleIndicesLength = 6;
|
||||
uvs = regionAttachment.UVs;
|
||||
tintR *= regionAttachment.R;
|
||||
tintG *= regionAttachment.G;
|
||||
tintB *= regionAttachment.B;
|
||||
tintA *= regionAttachment.A;
|
||||
}
|
||||
else if (attachment is MeshAttachment meshAttachment)
|
||||
{
|
||||
texture = (SFML.Graphics.Texture)((AtlasRegion)meshAttachment.Region).page.rendererObject;
|
||||
|
||||
if (meshAttachment.WorldVerticesLength > worldVertices.Length)
|
||||
worldVertices = worldVerticesBuffer = new float[meshAttachment.WorldVerticesLength * 2];
|
||||
meshAttachment.ComputeWorldVertices(slot, worldVertices);
|
||||
worldVerticesCount = meshAttachment.WorldVerticesLength / 2;
|
||||
worldTriangleIndices = meshAttachment.Triangles;
|
||||
worldTriangleIndicesLength = meshAttachment.Triangles.Length;
|
||||
uvs = meshAttachment.UVs;
|
||||
tintR *= meshAttachment.R;
|
||||
tintG *= meshAttachment.G;
|
||||
tintB *= meshAttachment.B;
|
||||
tintA *= meshAttachment.A;
|
||||
}
|
||||
else if (attachment is ClippingAttachment clippingAttachment)
|
||||
{
|
||||
clipping.ClipStart(slot, clippingAttachment);
|
||||
continue;
|
||||
}
|
||||
else
|
||||
{
|
||||
clipping.ClipEnd(slot);
|
||||
continue;
|
||||
}
|
||||
|
||||
SFML.Graphics.BlendMode blendMode = GetSFMLBlendMode(slot.Data.BlendMode);
|
||||
|
||||
states.Texture ??= texture;
|
||||
if (states.BlendMode != blendMode || states.Texture != texture)
|
||||
{
|
||||
if (triangleVertices.VertexCount > 0)
|
||||
{
|
||||
target.Draw(triangleVertices, states);
|
||||
triangleVertices.Clear();
|
||||
}
|
||||
states.BlendMode = blendMode;
|
||||
states.Texture = texture;
|
||||
}
|
||||
|
||||
if (clipping.IsClipping)
|
||||
{
|
||||
// 这里必须单独记录 Count, 和 Items 的 Length 是不一致的
|
||||
clipping.ClipTriangles(worldVertices, worldTriangleIndices, worldTriangleIndicesLength, uvs);
|
||||
worldVertices = clipping.ClippedVertices.Items;
|
||||
worldVerticesCount = clipping.ClippedVertices.Count / 2;
|
||||
worldTriangleIndices = clipping.ClippedTriangles.Items;
|
||||
worldTriangleIndicesLength = clipping.ClippedTriangles.Count;
|
||||
uvs = clipping.ClippedUVs.Items;
|
||||
}
|
||||
|
||||
var textureSizeX = texture.Size.X;
|
||||
var textureSizeY = texture.Size.Y;
|
||||
|
||||
SFML.Graphics.Vertex vertex = new();
|
||||
vertex.Color.R = (byte)(tintR * 255);
|
||||
vertex.Color.G = (byte)(tintG * 255);
|
||||
vertex.Color.B = (byte)(tintB * 255);
|
||||
vertex.Color.A = (byte)(tintA * 255);
|
||||
|
||||
// 必须用 worldTriangleIndicesLength 不能直接 foreach
|
||||
for (int i = 0; i < worldTriangleIndicesLength; i++)
|
||||
{
|
||||
var index = worldTriangleIndices[i] * 2;
|
||||
vertex.Position.X = worldVertices[index];
|
||||
vertex.Position.Y = worldVertices[index + 1];
|
||||
vertex.TexCoords.X = uvs[index] * textureSizeX;
|
||||
vertex.TexCoords.Y = uvs[index + 1] * textureSizeY;
|
||||
triangleVertices.Append(vertex);
|
||||
}
|
||||
|
||||
clipping.ClipEnd(slot);
|
||||
}
|
||||
clipping.ClipEnd();
|
||||
|
||||
target.Draw(triangleVertices, states);
|
||||
}
|
||||
|
||||
protected override void debugDraw(SFML.Graphics.RenderTarget target)
|
||||
{
|
||||
lineVertices.Clear();
|
||||
rectLineVertices.Clear();
|
||||
|
||||
if (debugRegions)
|
||||
{
|
||||
SFML.Graphics.Vertex vt = new() { Color = AttachmentLineColor };
|
||||
foreach (var slot in skeleton.Slots)
|
||||
{
|
||||
if (slot.Bone.Active && slot.Attachment is RegionAttachment regionAttachment)
|
||||
{
|
||||
regionAttachment.ComputeWorldVertices(slot, worldVerticesBuffer, 0);
|
||||
|
||||
vt.Position.X = worldVerticesBuffer[0];
|
||||
vt.Position.Y = worldVerticesBuffer[1];
|
||||
lineVertices.Append(vt);
|
||||
|
||||
vt.Position.X = worldVerticesBuffer[2];
|
||||
vt.Position.Y = worldVerticesBuffer[3];
|
||||
lineVertices.Append(vt); lineVertices.Append(vt);
|
||||
|
||||
vt.Position.X = worldVerticesBuffer[4];
|
||||
vt.Position.Y = worldVerticesBuffer[5];
|
||||
lineVertices.Append(vt); lineVertices.Append(vt);
|
||||
|
||||
vt.Position.X = worldVerticesBuffer[6];
|
||||
vt.Position.Y = worldVerticesBuffer[7];
|
||||
lineVertices.Append(vt); lineVertices.Append(vt);
|
||||
|
||||
vt.Position.X = worldVerticesBuffer[0];
|
||||
vt.Position.Y = worldVerticesBuffer[1];
|
||||
lineVertices.Append(vt);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (debugMeshes)
|
||||
{
|
||||
SFML.Graphics.Vertex vt = new() { Color = MeshLineColor };
|
||||
foreach (var slot in skeleton.Slots)
|
||||
{
|
||||
if (slot.Bone.Active && slot.Attachment is MeshAttachment meshAttachment)
|
||||
{
|
||||
if (meshAttachment.WorldVerticesLength > worldVerticesBuffer.Length)
|
||||
worldVerticesBuffer = new float[meshAttachment.WorldVerticesLength * 2];
|
||||
|
||||
meshAttachment.ComputeWorldVertices(slot, worldVerticesBuffer);
|
||||
|
||||
var triangleIndices = meshAttachment.Triangles;
|
||||
for (int i = 0; i < triangleIndices.Length; i += 3)
|
||||
{
|
||||
var idx0 = triangleIndices[i] * 2;
|
||||
var idx1 = triangleIndices[i + 1] * 2;
|
||||
var idx2 = triangleIndices[i + 2] * 2;
|
||||
|
||||
vt.Position.X = worldVerticesBuffer[idx0];
|
||||
vt.Position.Y = worldVerticesBuffer[idx0 + 1];
|
||||
lineVertices.Append(vt);
|
||||
|
||||
vt.Position.X = worldVerticesBuffer[idx1];
|
||||
vt.Position.Y = worldVerticesBuffer[idx1 + 1];
|
||||
lineVertices.Append(vt); lineVertices.Append(vt);
|
||||
|
||||
vt.Position.X = worldVerticesBuffer[idx2];
|
||||
vt.Position.Y = worldVerticesBuffer[idx2 + 1];
|
||||
lineVertices.Append(vt); lineVertices.Append(vt);
|
||||
|
||||
vt.Position.X = worldVerticesBuffer[idx0];
|
||||
vt.Position.Y = worldVerticesBuffer[idx0 + 1];
|
||||
lineVertices.Append(vt);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (debugMeshHulls)
|
||||
{
|
||||
SFML.Graphics.Vertex vt = new() { Color = AttachmentLineColor };
|
||||
foreach (var slot in skeleton.Slots)
|
||||
{
|
||||
if (slot.Bone.Active && slot.Attachment is MeshAttachment meshAttachment)
|
||||
{
|
||||
if (meshAttachment.WorldVerticesLength > worldVerticesBuffer.Length)
|
||||
worldVerticesBuffer = new float[meshAttachment.WorldVerticesLength * 2];
|
||||
|
||||
meshAttachment.ComputeWorldVertices(slot, worldVerticesBuffer);
|
||||
|
||||
var hullLength = (meshAttachment.HullLength >> 1) << 1;
|
||||
|
||||
if (debugMeshHulls && hullLength > 2)
|
||||
{
|
||||
vt.Position.X = worldVerticesBuffer[0];
|
||||
vt.Position.Y = worldVerticesBuffer[1];
|
||||
lineVertices.Append(vt);
|
||||
|
||||
for (int i = 2; i < hullLength; i += 2)
|
||||
{
|
||||
vt.Position.X = worldVerticesBuffer[i];
|
||||
vt.Position.Y = worldVerticesBuffer[i + 1];
|
||||
lineVertices.Append(vt);
|
||||
lineVertices.Append(vt);
|
||||
}
|
||||
|
||||
vt.Position.X = worldVerticesBuffer[0];
|
||||
vt.Position.Y = worldVerticesBuffer[1];
|
||||
lineVertices.Append(vt);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (debugBoundingBoxes)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
if (debugPaths)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
if (debugClippings)
|
||||
{
|
||||
SFML.Graphics.Vertex vt = new() { Color = ClippingLineColor };
|
||||
foreach (var slot in skeleton.Slots)
|
||||
{
|
||||
if (slot.Bone.Active && slot.Attachment is ClippingAttachment clippingAttachment)
|
||||
{
|
||||
if (clippingAttachment.WorldVerticesLength > worldVerticesBuffer.Length)
|
||||
worldVerticesBuffer = worldVerticesBuffer = new float[clippingAttachment.WorldVerticesLength * 2];
|
||||
|
||||
clippingAttachment.ComputeWorldVertices(slot, worldVerticesBuffer);
|
||||
|
||||
vt.Position.X = worldVerticesBuffer[0];
|
||||
vt.Position.Y = worldVerticesBuffer[1];
|
||||
lineVertices.Append(vt);
|
||||
|
||||
for (int i = 2; i < clippingAttachment.WorldVerticesLength; i += 2)
|
||||
{
|
||||
vt.Position.X = worldVerticesBuffer[i];
|
||||
vt.Position.Y = worldVerticesBuffer[i + 1];
|
||||
lineVertices.Append(vt);
|
||||
lineVertices.Append(vt);
|
||||
}
|
||||
|
||||
vt.Position.X = worldVerticesBuffer[0];
|
||||
vt.Position.Y = worldVerticesBuffer[1];
|
||||
lineVertices.Append(vt);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (debugBounds)
|
||||
{
|
||||
var vt = new SFML.Graphics.Vertex() { Color = BoundsColor };
|
||||
var b = getCurrentBounds();
|
||||
|
||||
vt.Position.X = b.Left;
|
||||
vt.Position.Y = b.Top;
|
||||
lineVertices.Append(vt);
|
||||
|
||||
vt.Position.X = b.Right;
|
||||
vt.Position.Y = b.Top;
|
||||
lineVertices.Append(vt); lineVertices.Append(vt);
|
||||
|
||||
vt.Position.X = b.Right;
|
||||
vt.Position.Y = b.Bottom;
|
||||
lineVertices.Append(vt); lineVertices.Append(vt);
|
||||
|
||||
vt.Position.X = b.Left;
|
||||
vt.Position.Y = b.Bottom;
|
||||
lineVertices.Append(vt); lineVertices.Append(vt);
|
||||
|
||||
vt.Position.X = b.Left;
|
||||
vt.Position.Y = b.Top;
|
||||
lineVertices.Append(vt);
|
||||
}
|
||||
|
||||
// 骨骼线放最后画
|
||||
if (debugBones)
|
||||
{
|
||||
var width = scale;
|
||||
foreach (var bone in skeleton.Bones)
|
||||
{
|
||||
if (!bone.Active) continue;
|
||||
var boneLength = bone.Data.Length;
|
||||
var p1 = new SFML.System.Vector2f(bone.WorldX, bone.WorldY);
|
||||
var p2 = new SFML.System.Vector2f(bone.WorldX + boneLength * bone.A, bone.WorldY + boneLength * bone.C);
|
||||
AddRectLine(p1, p2, BoneLineColor, width);
|
||||
}
|
||||
}
|
||||
|
||||
target.Draw(lineVertices);
|
||||
target.Draw(rectLineVertices);
|
||||
|
||||
// 骨骼的点最后画, 层级处于骨骼线上面
|
||||
if (debugBones)
|
||||
{
|
||||
var radius = scale;
|
||||
foreach (var bone in skeleton.Bones)
|
||||
{
|
||||
if (!bone.Active) continue;
|
||||
DrawCirclePoint(target, new(bone.WorldX, bone.WorldY), BonePointColor, radius);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -9,7 +9,7 @@ using System.Text;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Nodes;
|
||||
using System.Text.Encodings.Web;
|
||||
using SpineViewer.Utilities;
|
||||
using SpineViewer.Utils;
|
||||
|
||||
namespace SpineViewer.Spine
|
||||
{
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using SpineViewer.Exporter;
|
||||
using FFMpegCore;
|
||||
using SpineViewer.Utils;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
@@ -6,9 +7,54 @@ using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace SpineViewer.PropertyGridWrappers.Exporter
|
||||
namespace SpineViewer.Spine.SpineExporter
|
||||
{
|
||||
public class AvifExporterWrapper(AvifExporter exporter) : FFmpegVideoExporterWrapper(exporter)
|
||||
/// <summary>
|
||||
/// MP4 导出参数
|
||||
/// </summary>
|
||||
public class AvifExporter : FFmpegVideoExporter
|
||||
{
|
||||
public AvifExporter()
|
||||
{
|
||||
FPS = 24;
|
||||
}
|
||||
|
||||
public override string Format => "avif";
|
||||
|
||||
public override string Suffix => ".avif";
|
||||
|
||||
/// <summary>
|
||||
/// 编码器
|
||||
/// </summary>
|
||||
public string Codec { get; set; } = "av1_nvenc";
|
||||
|
||||
/// <summary>
|
||||
/// CRF
|
||||
/// </summary>
|
||||
public int CRF { get => crf; set => crf = Math.Clamp(value, 0, 63); }
|
||||
private int crf = 23;
|
||||
|
||||
/// <summary>
|
||||
/// 像素格式
|
||||
/// </summary>
|
||||
public string PixelFormat { get; set; } = "yuv420p";
|
||||
|
||||
/// <summary>
|
||||
/// 循环次数, 0 无限循环, 取值范围 [0, 65535]
|
||||
/// </summary>
|
||||
public int Loop { get => loop; set => loop = Math.Clamp(value, 0, 65535); }
|
||||
private int loop = 0;
|
||||
|
||||
public override string FileNameNoteSuffix => $"{Codec}_{CRF}_{PixelFormat}";
|
||||
|
||||
public override void SetOutputOptions(FFMpegArgumentOptions options)
|
||||
{
|
||||
base.SetOutputOptions(options);
|
||||
options.WithVideoCodec(Codec).ForcePixelFormat(PixelFormat).WithConstantRateFactor(CRF).WithCustomArgument($"-loop {Loop}");
|
||||
}
|
||||
}
|
||||
|
||||
public class AvifExporterProperty(AvifExporter exporter) : FFmpegVideoExporterProperty(exporter)
|
||||
{
|
||||
[Browsable(false)]
|
||||
public override AvifExporter Exporter => (AvifExporter)base.Exporter;
|
||||
@@ -1,14 +1,40 @@
|
||||
using SpineViewer.Exporter;
|
||||
using System;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace SpineViewer.PropertyGridWrappers.Exporter
|
||||
namespace SpineViewer.Spine.SpineExporter
|
||||
{
|
||||
public class CustomExporterWrapper(CustomExporter exporter) : FFmpegVideoExporterWrapper(exporter)
|
||||
/// <summary>
|
||||
/// FFmpeg 自定义视频导出参数
|
||||
/// </summary>
|
||||
public class CustomExporter : FFmpegVideoExporter
|
||||
{
|
||||
public CustomExporter()
|
||||
{
|
||||
CustomArgument = "-c:v libx264 -crf 23 -pix_fmt yuv420p"; // 提供一个示例参数
|
||||
}
|
||||
|
||||
public override string Format => CustomFormat;
|
||||
|
||||
public override string Suffix => CustomSuffix;
|
||||
|
||||
public override string FileNameNoteSuffix => string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 文件格式
|
||||
/// </summary>
|
||||
public string CustomFormat { get; set; } = "mp4";
|
||||
|
||||
/// <summary>
|
||||
/// 文件名后缀
|
||||
/// </summary>
|
||||
public string CustomSuffix { get; set; } = ".mp4";
|
||||
}
|
||||
|
||||
public class CustomExporterProperty(CustomExporter exporter) : FFmpegVideoExporterProperty(exporter)
|
||||
{
|
||||
[Browsable(false)]
|
||||
public override CustomExporter Exporter => (CustomExporter)base.Exporter;
|
||||
@@ -6,7 +6,7 @@ using System.Drawing.Imaging;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
|
||||
namespace SpineViewer.Exporter
|
||||
namespace SpineViewer.Spine.SpineExporter
|
||||
{
|
||||
/// <summary>
|
||||
/// SFML.Graphics.Image 帧对象包装类, 将接管给定的 image 对象生命周期
|
||||
410
SpineViewer/Spine/SpineExporter/Exporter.cs
Normal file
410
SpineViewer/Spine/SpineExporter/Exporter.cs
Normal file
@@ -0,0 +1,410 @@
|
||||
using NLog;
|
||||
using SpineViewer.Extensions;
|
||||
using SpineViewer.Utils;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using System.Drawing.Design;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace SpineViewer.Spine.SpineExporter
|
||||
{
|
||||
/// <summary>
|
||||
/// 导出器基类
|
||||
/// </summary>
|
||||
public abstract class Exporter : IDisposable
|
||||
{
|
||||
/// <summary>
|
||||
/// 日志器
|
||||
/// </summary>
|
||||
protected readonly Logger logger = LogManager.GetCurrentClassLogger();
|
||||
|
||||
/// <summary>
|
||||
/// 可用于文件名的时间戳字符串
|
||||
/// </summary>
|
||||
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) { PreviewerView.Dispose(); }
|
||||
|
||||
/// <summary>
|
||||
/// 输出文件夹
|
||||
/// </summary>
|
||||
public string? OutputDir { get; set; } = null;
|
||||
|
||||
/// <summary>
|
||||
/// 导出单个
|
||||
/// </summary>
|
||||
public bool IsExportSingle { get; set; } = false;
|
||||
|
||||
/// <summary>
|
||||
/// 画面分辨率
|
||||
/// </summary>
|
||||
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>
|
||||
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>
|
||||
/// 是否仅渲染选中
|
||||
/// </summary>
|
||||
public bool RenderSelectedOnly { get; set; } = false;
|
||||
|
||||
/// <summary>
|
||||
/// 背景颜色
|
||||
/// </summary>
|
||||
public SFML.Graphics.Color BackgroundColor
|
||||
{
|
||||
get => backgroundColor;
|
||||
set
|
||||
{
|
||||
backgroundColor = value;
|
||||
var bcPma = value;
|
||||
var a = bcPma.A / 255f;
|
||||
bcPma.R = (byte)(bcPma.R * a);
|
||||
bcPma.G = (byte)(bcPma.G * a);
|
||||
bcPma.B = (byte)(bcPma.B * a);
|
||||
backgroundColorPma = bcPma;
|
||||
}
|
||||
}
|
||||
private SFML.Graphics.Color backgroundColor = SFML.Graphics.Color.Transparent;
|
||||
|
||||
/// <summary>
|
||||
/// 预乘后的背景颜色
|
||||
/// </summary>
|
||||
private SFML.Graphics.Color backgroundColorPma = SFML.Graphics.Color.Transparent;
|
||||
|
||||
/// <summary>
|
||||
/// 四周边缘距离, 单位为像素
|
||||
/// </summary>
|
||||
public Padding Margin
|
||||
{
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取单个模型的单帧画面
|
||||
/// </summary>
|
||||
protected SFMLImageVideoFrame GetFrame(SpineObject spine) => GetFrame([spine]);
|
||||
|
||||
/// <summary>
|
||||
/// 获取模型列表的单帧画面
|
||||
/// </summary>
|
||||
protected SFMLImageVideoFrame GetFrame(SpineObject[] spinesToRender)
|
||||
{
|
||||
// RenderTexture 必须临时创建, 随用随取, 防止出现跨线程的情况
|
||||
using var texPma = GetRenderTexture(AutoResolution ? spinesToRender : null);
|
||||
|
||||
// 先将预乘结果准确绘制出来, 注意背景色也应当是预乘的
|
||||
texPma.Clear(backgroundColorPma);
|
||||
foreach (var spine in spinesToRender) texPma.Draw(spine);
|
||||
texPma.Display();
|
||||
|
||||
// 背景色透明度不为 1 时需要处理反预乘, 否则直接就是结果
|
||||
if (BackgroundColor.A < 255)
|
||||
{
|
||||
// 从预乘结果构造渲染对象, 并正确设置变换
|
||||
using var view = texPma.GetView();
|
||||
using var img = texPma.Texture.CopyToImage();
|
||||
using var texSprite = new SFML.Graphics.Texture(img);
|
||||
using var sp = new SFML.Graphics.Sprite(texSprite)
|
||||
{
|
||||
Origin = new(texPma.Size.X / 2f, texPma.Size.Y / 2f),
|
||||
Position = new(view.Center.X, view.Center.Y),
|
||||
Scale = new(view.Size.X / texPma.Size.X, view.Size.Y / texPma.Size.Y),
|
||||
Rotation = view.Rotation
|
||||
};
|
||||
|
||||
// 混合模式用直接覆盖的方式, 保证得到的图像区域是反预乘的颜色和透明度, 同时使用反预乘着色器
|
||||
var st = SFML.Graphics.RenderStates.Default;
|
||||
st.BlendMode = SFMLBlendMode.SourceOnly;
|
||||
st.Shader = SFMLShader.InversePma;
|
||||
|
||||
// 在最终结果上二次渲染非预乘画面
|
||||
using var tex = GetRenderTexture(AutoResolution ? spinesToRender : null);
|
||||
|
||||
// 将非预乘结果覆盖式绘制在目标对象上, 注意背景色应该用非预乘的
|
||||
tex.Clear(BackgroundColor);
|
||||
tex.Draw(sp, st);
|
||||
tex.Display();
|
||||
return new(tex.Texture.CopyToImage());
|
||||
}
|
||||
else
|
||||
{
|
||||
return new(texPma.Texture.CopyToImage());
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 每个模型在同一个画面进行导出
|
||||
/// </summary>
|
||||
protected abstract void ExportSingle(SpineObject[] spinesToRender, BackgroundWorker? worker = null);
|
||||
|
||||
/// <summary>
|
||||
/// 每个模型独立导出
|
||||
/// </summary>
|
||||
protected abstract void ExportIndividual(SpineObject[] spinesToRender, BackgroundWorker? worker = null);
|
||||
|
||||
/// <summary>
|
||||
/// 检查参数是否合法并规范化参数值, 否则返回用户错误原因
|
||||
/// </summary>
|
||||
public virtual string? Validate()
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(OutputDir) && File.Exists(OutputDir))
|
||||
return "输出文件夹无效";
|
||||
if (!string.IsNullOrWhiteSpace(OutputDir) && !Directory.Exists(OutputDir))
|
||||
return $"文件夹 {OutputDir} 不存在";
|
||||
if (IsExportSingle && string.IsNullOrWhiteSpace(OutputDir))
|
||||
return "导出单个时必须提供输出文件夹";
|
||||
|
||||
OutputDir = string.IsNullOrWhiteSpace(OutputDir) ? null : Path.GetFullPath(OutputDir);
|
||||
return null;
|
||||
}
|
||||
|
||||
private void ClearCache()
|
||||
{
|
||||
exportViewCache?.Dispose();
|
||||
exportViewCache = null;
|
||||
spineResolutionCache.Clear();
|
||||
foreach (var v in spineViewCache.Values) v.Dispose();
|
||||
spineViewCache.Clear();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 执行导出
|
||||
/// </summary>
|
||||
/// <param name="spines">要进行导出的 Spine 列表</param>
|
||||
/// <param name="worker">用来执行该函数的 worker</param>
|
||||
/// <exception cref="ArgumentException"></exception>
|
||||
public virtual void Export(SpineObject[] spines, BackgroundWorker? worker = null)
|
||||
{
|
||||
if (Validate() is string err) throw new ArgumentException(err);
|
||||
|
||||
var spinesToRender = spines.Where(sp => !RenderSelectedOnly || sp.IsSelected).Reverse().ToArray();
|
||||
if (spinesToRender.Length > 0)
|
||||
{
|
||||
ClearCache();
|
||||
|
||||
timestamp = DateTime.Now.ToString("yyMMddHHmmss"); // 刷新时间戳
|
||||
if (IsExportSingle) ExportSingle(spinesToRender, worker);
|
||||
else ExportIndividual(spinesToRender, worker);
|
||||
|
||||
ClearCache();
|
||||
}
|
||||
|
||||
logger.LogCurrentProcessMemoryUsage();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 用于在 PropertyGrid 上提供用户操作接口的包装类
|
||||
/// </summary>
|
||||
public class ExporterProperty(Exporter exporter)
|
||||
{
|
||||
[Browsable(false)]
|
||||
public virtual Exporter Exporter { get; } = exporter;
|
||||
|
||||
/// <summary>
|
||||
/// 输出文件夹
|
||||
/// </summary>
|
||||
[Editor(typeof(FolderNameEditor), typeof(UITypeEditor))]
|
||||
[Category("[0] 导出"), DisplayName("输出文件夹"), Description("逐个导出时可以留空,将逐个导出到模型自身所在目录")]
|
||||
public string? OutputDir { get => Exporter.OutputDir; set => Exporter.OutputDir = value; }
|
||||
|
||||
/// <summary>
|
||||
/// 导出单个
|
||||
/// </summary>
|
||||
[Category("[0] 导出"), DisplayName("导出单个"), Description("是否将模型在同一个画面上导出单个文件,否则逐个导出模型")]
|
||||
public bool IsExportSingle { get => Exporter.IsExportSingle; set => Exporter.IsExportSingle = value; }
|
||||
|
||||
/// <summary>
|
||||
/// 画面分辨率
|
||||
/// </summary>
|
||||
[TypeConverter(typeof(SizeConverter))]
|
||||
[Category("[0] 导出"), DisplayName("分辨率"), Description("画面的宽高像素大小,请在预览画面参数面板进行调整")]
|
||||
public Size Resolution { get => Exporter.Resolution; }
|
||||
|
||||
/// <summary>
|
||||
/// 预览画面视区
|
||||
/// </summary>
|
||||
[Category("[0] 导出"), DisplayName("预览画面视区"), Description("预览画面的视区参数,请在预览画面参数面板进行调整")]
|
||||
public SFML.Graphics.View View { get => Exporter.PreviewerView; }
|
||||
|
||||
/// <summary>
|
||||
/// 是否仅渲染选中
|
||||
/// </summary>
|
||||
[Category("[0] 导出"), DisplayName("仅渲染选中"), Description("是否仅导出选中的模型,请在预览画面参数面板进行调整")]
|
||||
public bool RenderSelectedOnly { get => Exporter.RenderSelectedOnly; }
|
||||
|
||||
/// <summary>
|
||||
/// 背景颜色
|
||||
/// </summary>
|
||||
[Editor(typeof(SFMLColorEditor), typeof(UITypeEditor))]
|
||||
[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; }
|
||||
}
|
||||
}
|
||||
@@ -8,7 +8,7 @@ using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using System.Diagnostics;
|
||||
|
||||
namespace SpineViewer.Exporter
|
||||
namespace SpineViewer.Spine.SpineExporter
|
||||
{
|
||||
/// <summary>
|
||||
/// 使用 FFmpeg 的视频导出器
|
||||
@@ -51,7 +51,7 @@ namespace SpineViewer.Exporter
|
||||
return null;
|
||||
}
|
||||
|
||||
protected override void ExportSingle(Spine.Spine[] spinesToRender, BackgroundWorker? worker = null)
|
||||
protected override void ExportSingle(SpineObject[] spinesToRender, BackgroundWorker? worker = null)
|
||||
{
|
||||
var noteSuffix = FileNameNoteSuffix;
|
||||
if (!string.IsNullOrWhiteSpace(noteSuffix)) noteSuffix = $"_{noteSuffix}";
|
||||
@@ -76,7 +76,7 @@ namespace SpineViewer.Exporter
|
||||
}
|
||||
}
|
||||
|
||||
protected override void ExportIndividual(Spine.Spine[] spinesToRender, BackgroundWorker? worker = null)
|
||||
protected override void ExportIndividual(SpineObject[] spinesToRender, BackgroundWorker? worker = null)
|
||||
{
|
||||
var noteSuffix = FileNameNoteSuffix;
|
||||
if (!string.IsNullOrWhiteSpace(noteSuffix)) noteSuffix = $"_{noteSuffix}";
|
||||
@@ -108,4 +108,28 @@ namespace SpineViewer.Exporter
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public class FFmpegVideoExporterProperty(FFmpegVideoExporter exporter) : VideoExporterProperty(exporter)
|
||||
{
|
||||
[Browsable(false)]
|
||||
public override FFmpegVideoExporter Exporter => (FFmpegVideoExporter)base.Exporter;
|
||||
|
||||
/// <summary>
|
||||
/// 文件格式
|
||||
/// </summary>
|
||||
[Category("[2] FFmpeg 基本参数"), DisplayName("文件格式"), Description("-f, 文件格式")]
|
||||
public virtual string Format => Exporter.Format;
|
||||
|
||||
/// <summary>
|
||||
/// 文件名后缀
|
||||
/// </summary>
|
||||
[Category("[2] FFmpeg 基本参数"), DisplayName("文件名后缀"), Description("文件名后缀")]
|
||||
public virtual string Suffix => Exporter.Suffix;
|
||||
|
||||
/// <summary>
|
||||
/// 文件名后缀
|
||||
/// </summary>
|
||||
[Category("[2] FFmpeg 基本参数"), DisplayName("自定义参数"), Description("使用 \"ffmpeg -h encoder=<编码器>\" 查看编码器支持的参数\n使用 \"ffmpeg -h muxer=<文件格式>\" 查看文件格式支持的参数")]
|
||||
public string CustomArgument { get => Exporter.CustomArgument; set => Exporter.CustomArgument = value; }
|
||||
}
|
||||
}
|
||||
@@ -7,7 +7,7 @@ using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace SpineViewer.Exporter
|
||||
namespace SpineViewer.Spine.SpineExporter
|
||||
{
|
||||
/// <summary>
|
||||
/// 单帧画面导出器
|
||||
@@ -43,7 +43,7 @@ namespace SpineViewer.Exporter
|
||||
}
|
||||
private SizeF dpi = new(144, 144);
|
||||
|
||||
protected override void ExportSingle(Spine.Spine[] spinesToRender, BackgroundWorker? worker = null)
|
||||
protected override void ExportSingle(SpineObject[] spinesToRender, BackgroundWorker? worker = null)
|
||||
{
|
||||
// 导出单个时必定提供输出文件夹
|
||||
var filename = $"frame_{timestamp}{ImageFormat.GetSuffix()}";
|
||||
@@ -65,7 +65,7 @@ namespace SpineViewer.Exporter
|
||||
worker?.ReportProgress(100, $"已处理 1/1");
|
||||
}
|
||||
|
||||
protected override void ExportIndividual(Spine.Spine[] spinesToRender, BackgroundWorker? worker = null)
|
||||
protected override void ExportIndividual(SpineObject[] spinesToRender, BackgroundWorker? worker = null)
|
||||
{
|
||||
int total = spinesToRender.Length;
|
||||
int success = 0;
|
||||
@@ -104,4 +104,30 @@ namespace SpineViewer.Exporter
|
||||
logger.Info("{} frames saved successfully", success);
|
||||
}
|
||||
}
|
||||
|
||||
public class FrameExporterProperty(FrameExporter exporter) : ExporterProperty(exporter)
|
||||
{
|
||||
[Browsable(false)]
|
||||
public override FrameExporter Exporter => (FrameExporter)base.Exporter;
|
||||
|
||||
/// <summary>
|
||||
/// 单帧画面格式
|
||||
/// </summary>
|
||||
[TypeConverter(typeof(ImageFormatConverter))]
|
||||
[Category("[1] 单帧画面"), DisplayName("图像格式")]
|
||||
public ImageFormat ImageFormat { get => Exporter.ImageFormat; set => Exporter.ImageFormat = value; }
|
||||
|
||||
/// <summary>
|
||||
/// 文件名后缀
|
||||
/// </summary>
|
||||
[Category("[1] 单帧画面"), DisplayName("文件名后缀"), Description("与图像格式匹配的文件名后缀")]
|
||||
public string Suffix { get => Exporter.ImageFormat.GetSuffix(); }
|
||||
|
||||
/// <summary>
|
||||
/// DPI
|
||||
/// </summary>
|
||||
[TypeConverter(typeof(SizeFConverter))]
|
||||
[Category("[1] 单帧画面"), DisplayName("DPI"), Description("导出图像的每英寸像素数,用于调整图像的物理尺寸")]
|
||||
public SizeF DPI { get => Exporter.DPI; set => Exporter.DPI = value; }
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
using SpineViewer.Spine;
|
||||
using SpineViewer.Utils;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
@@ -6,7 +7,7 @@ using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace SpineViewer.Exporter
|
||||
namespace SpineViewer.Spine.SpineExporter
|
||||
{
|
||||
/// <summary>
|
||||
/// 帧序列导出器
|
||||
@@ -18,7 +19,7 @@ namespace SpineViewer.Exporter
|
||||
/// </summary>
|
||||
public string Suffix { get; set; } = ".png";
|
||||
|
||||
protected override void ExportSingle(Spine.Spine[] spinesToRender, BackgroundWorker? worker = null)
|
||||
protected override void ExportSingle(SpineObject[] spinesToRender, BackgroundWorker? worker = null)
|
||||
{
|
||||
// 导出单个时必定提供输出文件夹,
|
||||
var saveDir = Path.Combine(OutputDir, $"frames_{timestamp}_{FPS:f0}");
|
||||
@@ -47,7 +48,7 @@ namespace SpineViewer.Exporter
|
||||
}
|
||||
}
|
||||
|
||||
protected override void ExportIndividual(Spine.Spine[] spinesToRender, BackgroundWorker? worker = null)
|
||||
protected override void ExportIndividual(SpineObject[] spinesToRender, BackgroundWorker? worker = null)
|
||||
{
|
||||
foreach (var spine in spinesToRender)
|
||||
{
|
||||
@@ -82,4 +83,17 @@ namespace SpineViewer.Exporter
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public class FrameSequenceExporterProperty(VideoExporter exporter) : VideoExporterProperty(exporter)
|
||||
{
|
||||
[Browsable(false)]
|
||||
public override FrameSequenceExporter Exporter => (FrameSequenceExporter)base.Exporter;
|
||||
|
||||
/// <summary>
|
||||
/// 文件名后缀
|
||||
/// </summary>
|
||||
[TypeConverter(typeof(StringEnumConverter)), StringEnumConverter.StandardValues(".png", ".jpg", ".tga", ".bmp")]
|
||||
[Category("[2] 帧序列参数"), DisplayName("文件名后缀"), Description("帧文件的后缀,同时决定帧图像格式")]
|
||||
public string Suffix { get => Exporter.Suffix; set => Exporter.Suffix = value; }
|
||||
}
|
||||
}
|
||||
@@ -6,7 +6,7 @@ using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace SpineViewer.Exporter
|
||||
namespace SpineViewer.Spine.SpineExporter
|
||||
{
|
||||
/// <summary>
|
||||
/// GIF 导出参数
|
||||
@@ -52,4 +52,28 @@ namespace SpineViewer.Exporter
|
||||
options.WithCustomArgument(customArgs);
|
||||
}
|
||||
}
|
||||
|
||||
class GifExporterProperty(FFmpegVideoExporter exporter) : FFmpegVideoExporterProperty(exporter)
|
||||
{
|
||||
[Browsable(false)]
|
||||
public override GifExporter Exporter => (GifExporter)base.Exporter;
|
||||
|
||||
/// <summary>
|
||||
/// 调色板最大颜色数量
|
||||
/// </summary>
|
||||
[Category("[3] 格式参数"), DisplayName("调色板最大颜色数量"), Description("设置调色板使用的最大颜色数量, 越多则色彩保留程度越高")]
|
||||
public uint MaxColors { get => Exporter.MaxColors; set => Exporter.MaxColors = value; }
|
||||
|
||||
/// <summary>
|
||||
/// 透明度阈值
|
||||
/// </summary>
|
||||
[Category("[3] 格式参数"), DisplayName("透明度阈值"), Description("小于该值的像素点会被认为是透明像素")]
|
||||
public byte AlphaThreshold { get => Exporter.AlphaThreshold; set => Exporter.AlphaThreshold = value; }
|
||||
|
||||
/// <summary>
|
||||
/// 透明度阈值
|
||||
/// </summary>
|
||||
[Category("[3] 格式参数"), DisplayName("循环次数"), Description("-loop, 循环次数, -1 不循环, 0 无限循环, 取值范围 [-1, 65535]")]
|
||||
public int Loop { get => Exporter.Loop; set => Exporter.Loop = value; }
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
using SpineViewer.Exporter;
|
||||
using FFMpegCore;
|
||||
using SpineViewer.Utils;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
@@ -6,9 +7,48 @@ using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace SpineViewer.PropertyGridWrappers.Exporter
|
||||
namespace SpineViewer.Spine.SpineExporter
|
||||
{
|
||||
public class MkvExporterWrapper(FFmpegVideoExporter exporter) : FFmpegVideoExporterWrapper(exporter)
|
||||
/// <summary>
|
||||
/// MKV 导出参数
|
||||
/// </summary>
|
||||
public class MkvExporter : FFmpegVideoExporter
|
||||
{
|
||||
public MkvExporter()
|
||||
{
|
||||
BackgroundColor = new(0, 255, 0);
|
||||
}
|
||||
|
||||
public override string Format => "matroska";
|
||||
|
||||
public override string Suffix => ".mkv";
|
||||
|
||||
/// <summary>
|
||||
/// 编码器
|
||||
/// </summary>
|
||||
public string Codec { get; set; } = "libx265";
|
||||
|
||||
/// <summary>
|
||||
/// CRF
|
||||
/// </summary>
|
||||
public int CRF { get => crf; set => crf = Math.Clamp(value, 0, 63); }
|
||||
private int crf = 23;
|
||||
|
||||
/// <summary>
|
||||
/// 像素格式
|
||||
/// </summary>
|
||||
public string PixelFormat { get; set; } = "yuv444p";
|
||||
|
||||
public override string FileNameNoteSuffix => $"{Codec}_{CRF}_{PixelFormat}";
|
||||
|
||||
public override void SetOutputOptions(FFMpegArgumentOptions options)
|
||||
{
|
||||
base.SetOutputOptions(options);
|
||||
options.WithVideoCodec(Codec).WithConstantRateFactor(CRF).ForcePixelFormat(PixelFormat);
|
||||
}
|
||||
}
|
||||
|
||||
public class MkvExporterProperty(FFmpegVideoExporter exporter) : FFmpegVideoExporterProperty(exporter)
|
||||
{
|
||||
[Browsable(false)]
|
||||
public override MkvExporter Exporter => (MkvExporter)base.Exporter;
|
||||
@@ -1,4 +1,5 @@
|
||||
using SpineViewer.Exporter;
|
||||
using FFMpegCore;
|
||||
using SpineViewer.Utils;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
@@ -6,9 +7,47 @@ using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace SpineViewer.PropertyGridWrappers.Exporter
|
||||
namespace SpineViewer.Spine.SpineExporter
|
||||
{
|
||||
public class MovExporterWrapper(FFmpegVideoExporter exporter) : FFmpegVideoExporterWrapper(exporter)
|
||||
/// <summary>
|
||||
/// MOV 导出参数
|
||||
/// </summary>
|
||||
public class MovExporter : FFmpegVideoExporter
|
||||
{
|
||||
public MovExporter()
|
||||
{
|
||||
BackgroundColor = new(0, 255, 0);
|
||||
}
|
||||
|
||||
public override string Format => "mov";
|
||||
|
||||
public override string Suffix => ".mov";
|
||||
|
||||
/// <summary>
|
||||
/// 编码器
|
||||
/// </summary>
|
||||
public string Codec { get; set; } = "prores_ks";
|
||||
|
||||
/// <summary>
|
||||
/// 预设
|
||||
/// </summary>
|
||||
public string Profile { get; set; } = "auto";
|
||||
|
||||
/// <summary>
|
||||
/// 像素格式
|
||||
/// </summary>
|
||||
public string PixelFormat { get; set; } = "yuva444p10le";
|
||||
|
||||
public override string FileNameNoteSuffix => $"{Codec}_{Profile}_{PixelFormat}";
|
||||
|
||||
public override void SetOutputOptions(FFMpegArgumentOptions options)
|
||||
{
|
||||
base.SetOutputOptions(options);
|
||||
options.WithFastStart().WithVideoCodec(Codec).WithCustomArgument($"-profile {Profile}").ForcePixelFormat(PixelFormat);
|
||||
}
|
||||
}
|
||||
|
||||
public class MovExporterProperty(FFmpegVideoExporter exporter) : FFmpegVideoExporterProperty(exporter)
|
||||
{
|
||||
[Browsable(false)]
|
||||
public override MovExporter Exporter => (MovExporter)base.Exporter;
|
||||
@@ -1,4 +1,5 @@
|
||||
using SpineViewer.Exporter;
|
||||
using FFMpegCore;
|
||||
using SpineViewer.Utils;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
@@ -6,9 +7,48 @@ using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace SpineViewer.PropertyGridWrappers.Exporter
|
||||
namespace SpineViewer.Spine.SpineExporter
|
||||
{
|
||||
public class Mp4ExporterWrapper(FFmpegVideoExporter exporter) : FFmpegVideoExporterWrapper(exporter)
|
||||
/// <summary>
|
||||
/// MP4 导出参数
|
||||
/// </summary>
|
||||
public class Mp4Exporter : FFmpegVideoExporter
|
||||
{
|
||||
public Mp4Exporter()
|
||||
{
|
||||
BackgroundColor = new(0, 255, 0);
|
||||
}
|
||||
|
||||
public override string Format => "mp4";
|
||||
|
||||
public override string Suffix => ".mp4";
|
||||
|
||||
/// <summary>
|
||||
/// 编码器
|
||||
/// </summary>
|
||||
public string Codec { get; set; } = "libx264";
|
||||
|
||||
/// <summary>
|
||||
/// CRF
|
||||
/// </summary>
|
||||
public int CRF { get => crf; set => crf = Math.Clamp(value, 0, 63); }
|
||||
private int crf = 23;
|
||||
|
||||
/// <summary>
|
||||
/// 像素格式
|
||||
/// </summary>
|
||||
public string PixelFormat { get; set; } = "yuv444p";
|
||||
|
||||
public override string FileNameNoteSuffix => $"{Codec}_{CRF}_{PixelFormat}";
|
||||
|
||||
public override void SetOutputOptions(FFMpegArgumentOptions options)
|
||||
{
|
||||
base.SetOutputOptions(options);
|
||||
options.WithFastStart().WithVideoCodec(Codec).WithConstantRateFactor(CRF).ForcePixelFormat(PixelFormat);
|
||||
}
|
||||
}
|
||||
|
||||
public class Mp4ExporterProperty(FFmpegVideoExporter exporter) : FFmpegVideoExporterProperty(exporter)
|
||||
{
|
||||
[Browsable(false)]
|
||||
public override Mp4Exporter Exporter => (Mp4Exporter)base.Exporter;
|
||||
@@ -6,7 +6,7 @@ using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace SpineViewer.Exporter
|
||||
namespace SpineViewer.Spine.SpineExporter
|
||||
{
|
||||
/// <summary>
|
||||
/// 视频导出基类
|
||||
@@ -41,7 +41,7 @@ namespace SpineViewer.Exporter
|
||||
/// <summary>
|
||||
/// 生成单个模型的帧序列
|
||||
/// </summary>
|
||||
protected IEnumerable<SFMLImageVideoFrame> GetFrames(Spine.Spine spine, BackgroundWorker? worker = null)
|
||||
protected IEnumerable<SFMLImageVideoFrame> GetFrames(SpineObject spine, BackgroundWorker? worker = null)
|
||||
{
|
||||
// 独立导出时如果 Duration 小于 0 则使用所有轨道上动画时长最大值
|
||||
var duration = Duration;
|
||||
@@ -51,7 +51,7 @@ namespace SpineViewer.Exporter
|
||||
int total = (int)(duration * FPS); // 完整帧的数量
|
||||
|
||||
float deltaFinal = duration - delta * total; // 最后一帧时长
|
||||
int final = (KeepLast && (deltaFinal > 1e-3)) ? 1 : 0;
|
||||
int final = KeepLast && deltaFinal > 1e-3 ? 1 : 0;
|
||||
|
||||
int frameCount = 1 + total + final; // 所有帧的数量 = 起始帧 + 完整帧 + 最后一帧
|
||||
|
||||
@@ -90,7 +90,7 @@ namespace SpineViewer.Exporter
|
||||
/// <summary>
|
||||
/// 生成多个模型的帧序列
|
||||
/// </summary>
|
||||
protected IEnumerable<SFMLImageVideoFrame> GetFrames(Spine.Spine[] spinesToRender, BackgroundWorker? worker = null)
|
||||
protected IEnumerable<SFMLImageVideoFrame> GetFrames(SpineObject[] spinesToRender, BackgroundWorker? worker = null)
|
||||
{
|
||||
// 导出单个时必须根据 Duration 决定导出时长
|
||||
var duration = Duration;
|
||||
@@ -99,7 +99,7 @@ namespace SpineViewer.Exporter
|
||||
int total = (int)(duration * FPS); // 完整帧的数量
|
||||
|
||||
float deltaFinal = duration - delta * total; // 最后一帧时长
|
||||
int final = (KeepLast && (deltaFinal > 1e-3)) ? 1 : 0;
|
||||
int final = KeepLast && deltaFinal > 1e-3 ? 1 : 0;
|
||||
|
||||
int frameCount = 1 + total + final; // 所有帧的数量 = 起始帧 + 完整帧 + 最后一帧
|
||||
|
||||
@@ -135,11 +135,35 @@ namespace SpineViewer.Exporter
|
||||
}
|
||||
}
|
||||
|
||||
public override void Export(Spine.Spine[] spines, BackgroundWorker? worker = null)
|
||||
public override void Export(SpineObject[] spines, BackgroundWorker? worker = null)
|
||||
{
|
||||
// 导出视频格式需要把模型时间都重置到 0
|
||||
foreach (var spine in spines) spine.ResetAnimationsTime();
|
||||
base.Export(spines, worker);
|
||||
}
|
||||
}
|
||||
|
||||
public class VideoExporterProperty(VideoExporter exporter) : ExporterProperty(exporter)
|
||||
{
|
||||
[Browsable(false)]
|
||||
public override VideoExporter Exporter => (VideoExporter)base.Exporter;
|
||||
|
||||
/// <summary>
|
||||
/// 导出时长
|
||||
/// </summary>
|
||||
[Category("[1] 视频参数"), DisplayName("时长"), Description("可以从模型列表查看动画时长, 如果小于 0, 则在逐个导出时每个模型使用各自的所有轨道动画时长最大值")]
|
||||
public float Duration { get => Exporter.Duration; set => Exporter.Duration = value; }
|
||||
|
||||
/// <summary>
|
||||
/// 帧率
|
||||
/// </summary>
|
||||
[Category("[1] 视频参数"), DisplayName("帧率"), Description("每秒画面数")]
|
||||
public float FPS { get => Exporter.FPS; set => Exporter.FPS = value; }
|
||||
|
||||
/// <summary>
|
||||
/// 保留最后一帧
|
||||
/// </summary>
|
||||
[Category("[1] 视频参数"), DisplayName("保留最后一帧"), Description("当设置保留最后一帧时, 动图会更为连贯, 但是帧数可能比预期帧数多 1")]
|
||||
public bool KeepLast { get => Exporter.KeepLast; set => Exporter.KeepLast = value; }
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
using SpineViewer.Exporter;
|
||||
using FFMpegCore;
|
||||
using SpineViewer.Utils;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
@@ -6,9 +7,49 @@ using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace SpineViewer.PropertyGridWrappers.Exporter
|
||||
namespace SpineViewer.Spine.SpineExporter
|
||||
{
|
||||
public class WebmExporterWrapper(WebmExporter exporter) : FFmpegVideoExporterWrapper(exporter)
|
||||
/// <summary>
|
||||
/// WebM 导出参数
|
||||
/// </summary>
|
||||
public class WebmExporter : FFmpegVideoExporter
|
||||
{
|
||||
public WebmExporter()
|
||||
{
|
||||
// 默认用透明黑背景
|
||||
BackgroundColor = new(0, 0, 0, 0);
|
||||
}
|
||||
|
||||
public override string Format => "webm";
|
||||
|
||||
public override string Suffix => ".webm";
|
||||
|
||||
/// <summary>
|
||||
/// 编码器
|
||||
/// </summary>
|
||||
public string Codec { get; set; } = "libvpx-vp9";
|
||||
|
||||
/// <summary>
|
||||
/// CRF
|
||||
/// </summary>
|
||||
public int CRF { get => crf; set => crf = Math.Clamp(value, 0, 63); }
|
||||
private int crf = 23;
|
||||
|
||||
/// <summary>
|
||||
/// 像素格式
|
||||
/// </summary>
|
||||
public string PixelFormat { get; set; } = "yuva420p";
|
||||
|
||||
public override string FileNameNoteSuffix => $"{Codec}_{CRF}_{PixelFormat}";
|
||||
|
||||
public override void SetOutputOptions(FFMpegArgumentOptions options)
|
||||
{
|
||||
base.SetOutputOptions(options);
|
||||
options.WithVideoCodec(Codec).WithConstantRateFactor(CRF).ForcePixelFormat(PixelFormat);
|
||||
}
|
||||
}
|
||||
|
||||
public class WebmExporterProperty(WebmExporter exporter) : FFmpegVideoExporterProperty(exporter)
|
||||
{
|
||||
[Browsable(false)]
|
||||
public override WebmExporter Exporter => (WebmExporter)base.Exporter;
|
||||
@@ -1,4 +1,5 @@
|
||||
using SpineViewer.Exporter;
|
||||
using FFMpegCore;
|
||||
using SpineViewer.Utils;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
@@ -6,9 +7,59 @@ using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace SpineViewer.PropertyGridWrappers.Exporter
|
||||
namespace SpineViewer.Spine.SpineExporter
|
||||
{
|
||||
public class WebpExporterWrapper(WebpExporter exporter) : FFmpegVideoExporterWrapper(exporter)
|
||||
/// <summary>
|
||||
/// MP4 导出参数
|
||||
/// </summary>
|
||||
public class WebpExporter : FFmpegVideoExporter
|
||||
{
|
||||
public WebpExporter()
|
||||
{
|
||||
FPS = 24;
|
||||
}
|
||||
|
||||
public override string Format => "webp";
|
||||
|
||||
public override string Suffix => ".webp";
|
||||
|
||||
/// <summary>
|
||||
/// 编码器
|
||||
/// </summary>
|
||||
public string Codec { get; set; } = "libwebp_anim";
|
||||
|
||||
/// <summary>
|
||||
/// 是否无损
|
||||
/// </summary>
|
||||
public bool Lossless { get; set; } = false;
|
||||
|
||||
/// <summary>
|
||||
/// 质量
|
||||
/// </summary>
|
||||
public int Quality { get => quality; set => quality = Math.Clamp(value, 0, 100); }
|
||||
private int quality = 75;
|
||||
|
||||
/// <summary>
|
||||
/// 像素格式
|
||||
/// </summary>
|
||||
public string PixelFormat { get; set; } = "yuva420p";
|
||||
|
||||
/// <summary>
|
||||
/// 循环次数, 0 无限循环, 取值范围 [0, 65535]
|
||||
/// </summary>
|
||||
public int Loop { get => loop; set => loop = Math.Clamp(value, 0, 65535); }
|
||||
private int loop = 0;
|
||||
|
||||
public override string FileNameNoteSuffix => $"{Codec}_{Quality}_{PixelFormat}";
|
||||
|
||||
public override void SetOutputOptions(FFMpegArgumentOptions options)
|
||||
{
|
||||
base.SetOutputOptions(options);
|
||||
options.WithVideoCodec(Codec).ForcePixelFormat(PixelFormat).WithCustomArgument($"-lossless {(Lossless ? 1 : 0)} -quality {Quality} -loop {Loop}");
|
||||
}
|
||||
}
|
||||
|
||||
public class WebpExporterProperty(WebpExporter exporter) : FFmpegVideoExporterProperty(exporter)
|
||||
{
|
||||
[Browsable(false)]
|
||||
public override WebpExporter Exporter => (WebpExporter)base.Exporter;
|
||||
@@ -5,40 +5,43 @@ using System.Drawing.Design;
|
||||
using NLog;
|
||||
using System.Xml.Linq;
|
||||
using SpineViewer.Extensions;
|
||||
using SpineViewer.Utilities;
|
||||
using SpineViewer.Utils;
|
||||
using System.Collections.Immutable;
|
||||
using System.Collections.Frozen;
|
||||
using System.Linq;
|
||||
|
||||
namespace SpineViewer.Spine
|
||||
{
|
||||
/// <summary>
|
||||
/// Spine 基类, 使用静态方法 New 来创建具体版本对象, 该类是线程安全的
|
||||
/// </summary>
|
||||
public abstract class Spine : ImplementationResolver<Spine, SpineImplementationAttribute, SpineVersion>, SFML.Graphics.Drawable, IDisposable
|
||||
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
|
||||
/// </summary>
|
||||
public static Spine New(SpineVersion version, string skelPath, string? atlasPath = null)
|
||||
public static SpineObject New(SpineVersion version, string skelPath, string? atlasPath = null)
|
||||
{
|
||||
atlasPath ??= Path.ChangeExtension(skelPath, ".atlas");
|
||||
skelPath = Path.GetFullPath(skelPath);
|
||||
atlasPath = Path.GetFullPath(atlasPath);
|
||||
|
||||
if (version == SpineVersion.Auto) version = SpineHelper.GetVersion(skelPath);
|
||||
if (version == SpineVersion.Auto) version = SpineUtils.GetVersion(skelPath);
|
||||
if (!File.Exists(atlasPath)) throw new FileNotFoundException($"atlas file {atlasPath} not found");
|
||||
return New(version, [skelPath, atlasPath]).PostInit();
|
||||
}
|
||||
@@ -48,13 +51,15 @@ namespace SpineViewer.Spine
|
||||
/// </summary>
|
||||
private readonly object _lock = new();
|
||||
|
||||
private readonly Logger logger = LogManager.GetCurrentClassLogger();
|
||||
private bool skinLoggerWarned = false;
|
||||
/// <summary>
|
||||
/// 日志器
|
||||
/// </summary>
|
||||
protected readonly Logger logger = LogManager.GetCurrentClassLogger();
|
||||
|
||||
/// <summary>
|
||||
/// 构造函数
|
||||
/// </summary>
|
||||
public Spine(string skelPath, string atlasPath)
|
||||
public SpineObject(string skelPath, string atlasPath)
|
||||
{
|
||||
Version = GetType().GetCustomAttribute<SpineImplementationAttribute>().ImplementationKey;
|
||||
AssetsDir = Directory.GetParent(skelPath).FullName;
|
||||
@@ -66,11 +71,8 @@ namespace SpineViewer.Spine
|
||||
/// <summary>
|
||||
/// 构造函数之后的初始化工作
|
||||
/// </summary>
|
||||
private Spine PostInit()
|
||||
private SpineObject PostInit()
|
||||
{
|
||||
SkinNames = skinNames.AsReadOnly();
|
||||
AnimationNames = animationNames.AsReadOnly();
|
||||
|
||||
// 必须 Update 一次否则包围盒还没有值
|
||||
update(0);
|
||||
|
||||
@@ -79,28 +81,36 @@ 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();
|
||||
// 初始化皮肤加载情况, 不需要记录 default
|
||||
foreach (var n in SkinNames.Where(v => v != "default")) skinLoadStatus[n] = false;
|
||||
|
||||
// 默认初始化10个动画空位
|
||||
for (int i = 0; i < 10; i++) setAnimation(i, AnimationNames.First());
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
~Spine() { Dispose(false); }
|
||||
~SpineObject() { Dispose(false); }
|
||||
public void Dispose() { Dispose(true); GC.SuppressFinalize(this); }
|
||||
protected virtual void Dispose(bool disposing) { Preview?.Dispose(); }
|
||||
protected virtual void Dispose(bool disposing)
|
||||
{
|
||||
Preview?.Dispose();
|
||||
triangleVertices.Dispose();
|
||||
lineVertices.Dispose();
|
||||
rectLineVertices.Dispose();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 运行时唯一 ID
|
||||
@@ -154,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>
|
||||
@@ -200,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>
|
||||
@@ -223,14 +215,14 @@ namespace SpineViewer.Spine
|
||||
protected bool isSelected = false;
|
||||
|
||||
/// <summary>
|
||||
/// 显示调试
|
||||
/// 启用渲染调试
|
||||
/// </summary>
|
||||
public bool IsDebug
|
||||
public bool EnableDebug
|
||||
{
|
||||
get { lock (_lock) return isDebug; }
|
||||
set { lock (_lock) { isDebug = value; update(0); } }
|
||||
get { lock (_lock) return enableDebug; }
|
||||
set { lock (_lock) { enableDebug = value; update(0); } }
|
||||
}
|
||||
protected bool isDebug = false;
|
||||
private bool enableDebug = false;
|
||||
|
||||
/// <summary>
|
||||
/// 显示纹理
|
||||
@@ -240,7 +232,7 @@ namespace SpineViewer.Spine
|
||||
get { lock (_lock) return debugTexture; }
|
||||
set { lock (_lock) { debugTexture = value; update(0); } }
|
||||
}
|
||||
protected bool debugTexture = true;
|
||||
private bool debugTexture = true;
|
||||
|
||||
/// <summary>
|
||||
/// 显示包围盒
|
||||
@@ -263,76 +255,137 @@ namespace SpineViewer.Spine
|
||||
protected bool debugBones = false;
|
||||
|
||||
/// <summary>
|
||||
/// 获取已加载的皮肤列表快照, 允许出现重复值
|
||||
/// 显示区域附件边框
|
||||
/// </summary>
|
||||
public string[] GetLoadedSkins() { lock (_lock) return loadedSkins.ToArray(); }
|
||||
protected readonly List<string> loadedSkins = [];
|
||||
|
||||
/// <summary>
|
||||
/// 加载指定皮肤, 添加至列表末尾, 如果不存在则忽略, 允许加载重复的值
|
||||
/// </summary>
|
||||
public void LoadSkin(string name)
|
||||
public bool DebugRegions
|
||||
{
|
||||
if (!skinNames.Contains(name)) return;
|
||||
lock (_lock)
|
||||
{
|
||||
loadedSkins.Add(name);
|
||||
reloadSkins();
|
||||
|
||||
if (!skinLoggerWarned && Version <= SpineVersion.V37 && loadedSkins.Count > 1)
|
||||
{
|
||||
logger.Warn($"Multiplt skins not supported in SpineVersion {Version.GetName()}");
|
||||
skinLoggerWarned = true;
|
||||
}
|
||||
}
|
||||
get { lock (_lock) return debugRegions; }
|
||||
set { lock (_lock) { debugRegions = value; update(0); } }
|
||||
}
|
||||
protected bool debugRegions = false;
|
||||
|
||||
/// <summary>
|
||||
/// 卸载列表指定位置皮肤, 如果超出范围则忽略
|
||||
/// 显示网格附件边框线
|
||||
/// </summary>
|
||||
public void UnloadSkin(int idx)
|
||||
public bool DebugMeshHulls
|
||||
{
|
||||
if (idx < 0 || idx >= loadedSkins.Count) return;
|
||||
get { lock (_lock) return debugMeshHulls; }
|
||||
set { lock (_lock) { debugMeshHulls = value; update(0); } }
|
||||
}
|
||||
protected bool debugMeshHulls = false;
|
||||
|
||||
/// <summary>
|
||||
/// 显示网格附件网格线
|
||||
/// </summary>
|
||||
public bool DebugMeshes
|
||||
{
|
||||
get { lock (_lock) return debugMeshes; }
|
||||
set { lock (_lock) { debugMeshes = value; update(0); } }
|
||||
}
|
||||
protected bool debugMeshes = false;
|
||||
|
||||
/// <summary>
|
||||
/// 显示碰撞盒附件边框线
|
||||
/// </summary>
|
||||
public bool DebugBoundingBoxes
|
||||
{
|
||||
get { lock (_lock) return debugBoundingBoxes; }
|
||||
set { lock (_lock) { debugBoundingBoxes = value; update(0); } }
|
||||
}
|
||||
protected bool debugBoundingBoxes = false;
|
||||
|
||||
/// <summary>
|
||||
/// 显示路径附件网格线
|
||||
/// </summary>
|
||||
public bool DebugPaths
|
||||
{
|
||||
get { lock (_lock) return debugPaths; }
|
||||
set { lock (_lock) { debugPaths = value; update(0); } }
|
||||
}
|
||||
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>
|
||||
public bool DebugClippings
|
||||
{
|
||||
get { lock (_lock) return debugClippings; }
|
||||
set { lock (_lock) { debugClippings = value; update(0); } }
|
||||
}
|
||||
protected bool debugClippings = false;
|
||||
|
||||
/// <summary>
|
||||
/// 所有插槽下可用的附件名
|
||||
/// </summary>
|
||||
public FrozenDictionary<string, ImmutableArray<string>> SlotAttachmentNames { get; protected set; }
|
||||
|
||||
/// <summary>
|
||||
/// 包含的所有皮肤名称 (不含 default 默认皮肤)
|
||||
/// </summary>
|
||||
public ImmutableArray<string> SkinNames { get; protected set; }
|
||||
|
||||
/// <summary>
|
||||
/// 包含的所有动画名称
|
||||
/// </summary>
|
||||
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 name == "default" || skinLoadStatus.TryGetValue(name, out var status) && status; }
|
||||
|
||||
/// <summary>
|
||||
/// 设置皮肤加载状态, 忽略不存在的皮肤
|
||||
/// </summary>
|
||||
public void SetSkinStatus(string name, bool status)
|
||||
{
|
||||
if (!skinLoadStatus.ContainsKey(name)) return;
|
||||
lock (_lock)
|
||||
{
|
||||
loadedSkins.RemoveAt(idx);
|
||||
skinLoadStatus[name] = status;
|
||||
reloadSkins();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 替换皮肤列表指定位置皮肤, 超出范围或者皮肤不存在则忽略
|
||||
/// </summary>
|
||||
public void ReplaceSkin(int idx, string name)
|
||||
{
|
||||
if (idx < 0 || idx >= loadedSkins.Count || !skinNames.Contains(name)) return;
|
||||
lock (_lock)
|
||||
{
|
||||
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 的轨道索引快照
|
||||
@@ -368,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>
|
||||
@@ -376,32 +441,115 @@ namespace SpineViewer.Spine
|
||||
|
||||
#region SFML.Graphics.Drawable 接口实现
|
||||
|
||||
/// <summary>
|
||||
/// 顶点坐标缓冲区
|
||||
/// </summary>
|
||||
protected float[] worldVerticesBuffer = new float[1024];
|
||||
|
||||
/// <summary>
|
||||
/// 顶点缓冲区
|
||||
/// </summary>
|
||||
protected readonly SFML.Graphics.VertexArray vertexArray = new(SFML.Graphics.PrimitiveType.Triangles);
|
||||
|
||||
/// <summary>
|
||||
/// 包围盒颜色
|
||||
/// </summary>
|
||||
protected static readonly SFML.Graphics.Color BoundsColor = new(120, 200, 0);
|
||||
|
||||
/// <summary>
|
||||
/// 包围盒顶点数组
|
||||
/// 骨骼点颜色
|
||||
/// </summary>
|
||||
protected readonly SFML.Graphics.VertexArray boundsVertices = new(SFML.Graphics.PrimitiveType.LineStrip, 5);
|
||||
protected static readonly SFML.Graphics.Color BonePointColor = new(0, 255, 0);
|
||||
|
||||
/// <summary>
|
||||
/// 骨骼线颜色
|
||||
/// </summary>
|
||||
protected static readonly SFML.Graphics.Color BoneLineColor = new(255, 0, 0);
|
||||
|
||||
/// <summary>
|
||||
/// 网格线颜色
|
||||
/// </summary>
|
||||
protected static readonly SFML.Graphics.Color MeshLineColor = new(255, 163, 0, 128);
|
||||
|
||||
/// <summary>
|
||||
/// 附件边框线颜色
|
||||
/// </summary>
|
||||
protected static readonly SFML.Graphics.Color AttachmentLineColor = new(0, 0, 255, 128);
|
||||
|
||||
/// <summary>
|
||||
/// 剪裁附件边框线颜色
|
||||
/// </summary>
|
||||
protected static readonly SFML.Graphics.Color ClippingLineColor = new(204, 0, 0);
|
||||
|
||||
/// <summary>
|
||||
/// spine 顶点坐标缓冲区
|
||||
/// </summary>
|
||||
protected float[] worldVerticesBuffer = new float[1024];
|
||||
|
||||
/// <summary>
|
||||
/// 三角形顶点缓冲区
|
||||
/// </summary>
|
||||
protected readonly SFML.Graphics.VertexArray triangleVertices = new(SFML.Graphics.PrimitiveType.Triangles);
|
||||
|
||||
/// <summary>
|
||||
/// 无面积线条缓冲区
|
||||
/// </summary>
|
||||
protected readonly SFML.Graphics.VertexArray lineVertices = new(SFML.Graphics.PrimitiveType.Lines);
|
||||
|
||||
/// <summary>
|
||||
/// 有半径圆点临时缓存对象
|
||||
/// </summary>
|
||||
private readonly SFML.Graphics.CircleShape circlePointShape = new();
|
||||
|
||||
/// <summary>
|
||||
/// 有宽度线条缓冲区, 需要通过 <see cref="AddRectLine"/> 添加顶点
|
||||
/// </summary>
|
||||
protected readonly SFML.Graphics.VertexArray rectLineVertices = new(SFML.Graphics.PrimitiveType.Quads);
|
||||
|
||||
/// <summary>
|
||||
/// 绘制有半径的实心圆点, 随模型一起缩放大小
|
||||
/// </summary>
|
||||
protected void DrawCirclePoint(SFML.Graphics.RenderTarget target, SFML.System.Vector2f p, SFML.Graphics.Color color, float radius = 1)
|
||||
{
|
||||
circlePointShape.Origin = new(radius, radius);
|
||||
circlePointShape.Position = p;
|
||||
circlePointShape.FillColor = color;
|
||||
circlePointShape.Radius = radius;
|
||||
target.Draw(circlePointShape);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 绘制有宽度的实心线, 会随模型一起缩放粗细, 顶点被存储在 <see cref="rectLineVertices"/> 数组内
|
||||
/// </summary>
|
||||
protected void AddRectLine(SFML.System.Vector2f p1, SFML.System.Vector2f p2, SFML.Graphics.Color color, float width = 1)
|
||||
{
|
||||
var dx = p2.X - p1.X;
|
||||
var dy = p2.Y - p1.Y;
|
||||
var dt = (float)Math.Sqrt(dx * dx + dy * dy);
|
||||
if (dt == 0) return;
|
||||
|
||||
var cosTheta = -dy / dt;
|
||||
var sinTheta = dx / dt;
|
||||
var halfWidth = width / 2;
|
||||
var t = new SFML.System.Vector2f(halfWidth * cosTheta, halfWidth * sinTheta);
|
||||
var v = new SFML.Graphics.Vertex() { Color = color };
|
||||
|
||||
v.Position = p1 + t; rectLineVertices.Append(v);
|
||||
v.Position = p2 + t; rectLineVertices.Append(v);
|
||||
v.Position = p2 - t; rectLineVertices.Append(v);
|
||||
v.Position = p1 - t; rectLineVertices.Append(v);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// SFML.Graphics.Drawable 接口实现
|
||||
/// <para>这个渲染实现绘制出来的像素将是预乘的, 当渲染的背景透明度是 1 时, 则等价于非预乘的结果, 即正常画面, 否则画面偏暗</para>
|
||||
/// <para>可以用于 <see cref="SFML.Graphics.RenderWindow"/> 的渲染, 因为直接在窗口上绘制时窗口始终是不透明的</para>
|
||||
/// </summary>
|
||||
public void Draw(SFML.Graphics.RenderTarget target, SFML.Graphics.RenderStates states) { lock (_lock) draw(target, states); }
|
||||
public void Draw(SFML.Graphics.RenderTarget target, SFML.Graphics.RenderStates states)
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
if (!enableDebug)
|
||||
{
|
||||
draw(target, states);
|
||||
}
|
||||
else
|
||||
{
|
||||
if (debugTexture) draw(target, states);
|
||||
if (isSelected) debugDraw(target);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 这个渲染实现绘制出来的像素将是预乘的, 当渲染的背景透明度是 1 时, 则等价于非预乘的结果, 即正常画面, 否则画面偏暗
|
||||
@@ -409,6 +557,11 @@ namespace SpineViewer.Spine
|
||||
/// </summary>
|
||||
protected abstract void draw(SFML.Graphics.RenderTarget target, SFML.Graphics.RenderStates states);
|
||||
|
||||
/// <summary>
|
||||
/// 渲染调试内容
|
||||
/// </summary>
|
||||
protected abstract void debugDraw(SFML.Graphics.RenderTarget target);
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
using SpineViewer.Utilities;
|
||||
using SpineViewer.Utils;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
@@ -12,35 +12,10 @@ using System.Threading.Tasks;
|
||||
|
||||
namespace SpineViewer.Spine
|
||||
{
|
||||
/// <summary>
|
||||
/// 支持的 Spine 版本
|
||||
/// </summary>
|
||||
public enum SpineVersion
|
||||
{
|
||||
[Description("<Auto>")] Auto = 0x0000,
|
||||
[Description("2.1.x")] V21 = 0x0201,
|
||||
[Description("3.6.x")] V36 = 0x0306,
|
||||
[Description("3.7.x")] V37 = 0x0307,
|
||||
[Description("3.8.x")] V38 = 0x0308,
|
||||
[Description("4.0.x")] V40 = 0x0400,
|
||||
[Description("4.1.x")] V41 = 0x0401,
|
||||
[Description("4.2.x")] V42 = 0x0402,
|
||||
[Description("4.3.x")] V43 = 0x0403,
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Spine 实现类标记
|
||||
/// </summary>
|
||||
[AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = false)]
|
||||
public class SpineImplementationAttribute(SpineVersion version) : Attribute, IImplementationKey<SpineVersion>
|
||||
{
|
||||
public SpineVersion ImplementationKey { get; private set; } = version;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Spine 版本静态辅助类
|
||||
/// </summary>
|
||||
public static class SpineHelper
|
||||
public static class SpineUtils
|
||||
{
|
||||
/// <summary>
|
||||
/// 版本名称
|
||||
@@ -53,7 +28,7 @@ namespace SpineViewer.Spine
|
||||
/// </summary>
|
||||
private static readonly Dictionary<SpineVersion, string> runtimes = [];
|
||||
|
||||
static SpineHelper()
|
||||
static SpineUtils()
|
||||
{
|
||||
// 初始化缓存
|
||||
foreach (var value in Enum.GetValues(typeof(SpineVersion)))
|
||||
52
SpineViewer/Spine/SpineVersion.cs
Normal file
52
SpineViewer/Spine/SpineVersion.cs
Normal file
@@ -0,0 +1,52 @@
|
||||
using SpineViewer.Utils;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.ComponentModel;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using System.Text;
|
||||
using System.Text.Json.Nodes;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace SpineViewer.Spine
|
||||
{
|
||||
/// <summary>
|
||||
/// 支持的 Spine 版本
|
||||
/// </summary>
|
||||
public enum SpineVersion
|
||||
{
|
||||
[Description("<Auto>")] Auto = 0x0000,
|
||||
[Description("2.1.x")] V21 = 0x0201,
|
||||
[Description("3.6.x")] V36 = 0x0306,
|
||||
[Description("3.7.x")] V37 = 0x0307,
|
||||
[Description("3.8.x")] V38 = 0x0308,
|
||||
[Description("4.0.x")] V40 = 0x0400,
|
||||
[Description("4.1.x")] V41 = 0x0401,
|
||||
[Description("4.2.x")] V42 = 0x0402,
|
||||
[Description("4.3.x")] V43 = 0x0403,
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Spine 实现类标记
|
||||
/// </summary>
|
||||
[AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = false)]
|
||||
public class SpineImplementationAttribute(SpineVersion version) : Attribute, IImplementationKey<SpineVersion>
|
||||
{
|
||||
public SpineVersion ImplementationKey { get; private set; } = version;
|
||||
}
|
||||
|
||||
public class SpineVersionConverter : EnumConverter
|
||||
{
|
||||
public SpineVersionConverter() : base(typeof(SpineVersion)) { }
|
||||
|
||||
public override object? ConvertTo(ITypeDescriptorContext? context, CultureInfo? culture, object? value, Type? destinationType)
|
||||
{
|
||||
if (destinationType == typeof(string) && value is SpineVersion version)
|
||||
return version.GetName();
|
||||
return base.ConvertTo(context, culture, value, destinationType);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
using SpineViewer.Spine;
|
||||
using SpineViewer.Utils;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
@@ -6,89 +7,15 @@ using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace SpineViewer.PropertyGridWrappers.Spine
|
||||
namespace SpineViewer.Spine.SpineView
|
||||
{
|
||||
/// <summary>
|
||||
/// 对轨道索引属性的包装类, 能够在面板上显示例如时长的属性, 但是处理该属性时按字符串去处理, 例如 ToString 和判断对象相等都是用动画名称实现逻辑
|
||||
/// </summary>
|
||||
/// <param name="spine"></param>
|
||||
/// <param name="i"></param>
|
||||
[TypeConverter(typeof(TrackWrapperConverter))]
|
||||
public class TrackWrapper(SpineViewer.Spine.Spine spine, int i)
|
||||
{
|
||||
private readonly SpineViewer.Spine.Spine spine = spine;
|
||||
|
||||
[Browsable(false)]
|
||||
public int Index { get; } = i;
|
||||
|
||||
[DisplayName("时长")]
|
||||
public float Duration => spine.GetAnimationDuration(spine.GetAnimation(Index));
|
||||
|
||||
/// <summary>
|
||||
/// 实现了默认的转为字符串的方式
|
||||
/// </summary>
|
||||
public override string ToString() => spine.GetAnimation(Index);
|
||||
|
||||
/// <summary>
|
||||
/// 影响了属性面板的判断, 当动画名称相同的时候认为两个对象是相同的, 这样属性面板可以在多选的时候正确显示相同取值的内容
|
||||
/// </summary>
|
||||
public override bool Equals(object? obj)
|
||||
{
|
||||
if (obj is TrackWrapper) return ToString() == obj.ToString();
|
||||
return base.Equals(obj);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 哈希码需要和 Equals 行为类似
|
||||
/// </summary>
|
||||
public override int GetHashCode() => HashCode.Combine(typeof(TrackWrapper).FullName.GetHashCode(), ToString().GetHashCode());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 用于在 PropertyGrid 上显示 Spine 动画列表的包装类
|
||||
/// </summary>
|
||||
public class SpineAnimationWrapper(SpineViewer.Spine.Spine spine) : ICustomTypeDescriptor
|
||||
public class SpineAnimationProperty(SpineObject spine) : ICustomTypeDescriptor
|
||||
{
|
||||
/// <summary>
|
||||
/// 轨道属性描述符, 实现对属性的读取和赋值
|
||||
/// </summary>
|
||||
/// <param name="i">轨道索引</param>
|
||||
private class TrackWrapperPropertyDescriptor(int i, Attribute[]? attributes) : PropertyDescriptor($"Track{i}", attributes)
|
||||
{
|
||||
private readonly int idx = i;
|
||||
|
||||
public override Type ComponentType => typeof(SpineAnimationWrapper);
|
||||
public override bool IsReadOnly => false;
|
||||
public override Type PropertyType => typeof(TrackWrapper);
|
||||
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 SpineAnimationWrapper tracks)
|
||||
return tracks.GetTrackWrapper(idx);
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 允许通过字符串赋值修改该轨道的动画, 这里决定了当其他地方的调用 (比如 Converter) 通过 value 来设置属性值的时候应该怎么处理
|
||||
/// </summary>
|
||||
public override void SetValue(object? component, object? value)
|
||||
{
|
||||
if (component is SpineAnimationWrapper tracks)
|
||||
{
|
||||
if (value is string s)
|
||||
tracks.SetTrackWrapper(idx, s);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[Browsable(false)]
|
||||
public SpineViewer.Spine.Spine Spine { get; } = spine;
|
||||
public SpineObject Spine { get; } = spine;
|
||||
|
||||
/// <summary>
|
||||
/// 全轨道动画最大时长
|
||||
@@ -97,24 +24,24 @@ namespace SpineViewer.PropertyGridWrappers.Spine
|
||||
public float AnimationTracksMaxDuration => Spine.GetTrackIndices().Select(i => Spine.GetAnimationDuration(Spine.GetAnimation(i))).Max();
|
||||
|
||||
/// <summary>
|
||||
/// TrackWrapper 属性对象缓存
|
||||
/// <see cref="TrackAnimationProperty"/> 属性对象缓存
|
||||
/// </summary>
|
||||
private readonly Dictionary<int, TrackWrapper> trackWrapperProperties = [];
|
||||
private readonly Dictionary<int, TrackAnimationProperty> trackAnimationProperties = [];
|
||||
|
||||
/// <summary>
|
||||
/// 访问 TrackWrapper 属性 <c>AnimationTracks.Track{i}</c>
|
||||
/// <c>this.Track{i}</c>
|
||||
/// </summary>
|
||||
public TrackWrapper GetTrackWrapper(int i)
|
||||
public TrackAnimationProperty GetTrackAnimation(int i)
|
||||
{
|
||||
if (!trackWrapperProperties.ContainsKey(i))
|
||||
trackWrapperProperties[i] = new TrackWrapper(Spine, i);
|
||||
return trackWrapperProperties[i];
|
||||
if (!trackAnimationProperties.ContainsKey(i))
|
||||
trackAnimationProperties[i] = new TrackAnimationProperty(Spine, i);
|
||||
return trackAnimationProperties[i];
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 设置 TrackWrapper 属性 <c>AnimationTracks.Track{i} = <paramref name="value"/></c>
|
||||
/// <c>this.Track{i} = <paramref name="value"/></c>
|
||||
/// </summary>
|
||||
public void SetTrackWrapper(int i, string value)
|
||||
public void SetTrackAnimation(int i, string value)
|
||||
{
|
||||
Spine.SetAnimation(i, value);
|
||||
TypeDescriptor.Refresh(this);
|
||||
@@ -127,11 +54,11 @@ namespace SpineViewer.PropertyGridWrappers.Spine
|
||||
|
||||
public override bool Equals(object? obj)
|
||||
{
|
||||
if (obj is SpineAnimationWrapper wrapper) return ToString() == wrapper.ToString();
|
||||
if (obj is SpineAnimationProperty prop) return ToString() == prop.ToString();
|
||||
return base.Equals(obj);
|
||||
}
|
||||
|
||||
public override int GetHashCode() => HashCode.Combine(typeof(SpineAnimationWrapper).FullName.GetHashCode(), ToString().GetHashCode());
|
||||
public override int GetHashCode() => HashCode.Combine(typeof(SpineAnimationProperty).FullName.GetHashCode(), ToString().GetHashCode());
|
||||
|
||||
#region ICustomTypeDescriptor 接口实现
|
||||
|
||||
@@ -158,13 +85,115 @@ namespace SpineViewer.PropertyGridWrappers.Spine
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 轨道属性描述符, 实现对属性的读取和赋值
|
||||
/// </summary>
|
||||
/// <param name="i">轨道索引</param>
|
||||
private class TrackWrapperPropertyDescriptor(int i, Attribute[]? attributes) : PropertyDescriptor($"Track{i}", attributes)
|
||||
{
|
||||
private readonly int idx = i;
|
||||
|
||||
public override Type ComponentType => typeof(SpineAnimationProperty);
|
||||
public override bool IsReadOnly => false;
|
||||
public override Type PropertyType => typeof(TrackAnimationProperty);
|
||||
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 SpineAnimationProperty tracks)
|
||||
return tracks.GetTrackAnimation(idx);
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 允许通过字符串赋值修改该轨道的动画, 这里决定了当其他地方的调用 (比如 Converter) 通过 value 来设置属性值的时候应该怎么处理
|
||||
/// </summary>
|
||||
public override void SetValue(object? component, object? value)
|
||||
{
|
||||
if (component is SpineAnimationProperty tracks)
|
||||
{
|
||||
if (value is string s)
|
||||
tracks.SetTrackAnimation(idx, s);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 对 <c><see cref="SpineAnimationProperty"/>.Track{i}</c> 属性的包装类
|
||||
/// </summary>
|
||||
[TypeConverter(typeof(TrackAnimationPropertyConverter))]
|
||||
public class TrackAnimationProperty(SpineObject spine, int i)
|
||||
{
|
||||
private readonly SpineObject spine = spine;
|
||||
|
||||
[Browsable(false)]
|
||||
public int Index { get; } = i;
|
||||
|
||||
[DisplayName("时长")]
|
||||
public float Duration => spine.GetAnimationDuration(spine.GetAnimation(Index));
|
||||
|
||||
/// <summary>
|
||||
/// 实现了默认的转为字符串的方式
|
||||
/// </summary>
|
||||
public override string ToString() => spine.GetAnimation(Index);
|
||||
|
||||
/// <summary>
|
||||
/// 影响了属性面板的判断, 当动画名称相同的时候认为两个对象是相同的, 这样属性面板可以在多选的时候正确显示相同取值的内容
|
||||
/// </summary>
|
||||
public override bool Equals(object? obj)
|
||||
{
|
||||
if (obj is TrackAnimationProperty) return ToString() == obj.ToString();
|
||||
return base.Equals(obj);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 哈希码需要和 Equals 行为类似
|
||||
/// </summary>
|
||||
public override int GetHashCode() => HashCode.Combine(typeof(TrackAnimationProperty).FullName.GetHashCode(), ToString().GetHashCode());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 轨道索引包装类转换器, 实现字符串和包装类的互相转换, 并且提供标准值列表对属性进行设置, 同时还提供在面板上显示包装类属性的能力
|
||||
/// </summary>
|
||||
public class TrackAnimationPropertyConverter : ExpandableObjectConverter
|
||||
{
|
||||
// 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 SpineAnimationProperty tracks)
|
||||
{
|
||||
return new StandardValuesCollection(tracks.Spine.AnimationNames);
|
||||
}
|
||||
else if (context?.Instance is object[] instances)
|
||||
{
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
using SpineViewer.Spine;
|
||||
using SpineViewer.Utils;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
@@ -6,15 +7,15 @@ using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace SpineViewer.PropertyGridWrappers.Spine
|
||||
namespace SpineViewer.Spine.SpineView
|
||||
{
|
||||
/// <summary>
|
||||
/// 用于在 PropertyGrid 上显示 Spine 基本信息的包装类
|
||||
/// </summary>
|
||||
public class SpineBaseInfoWrapper(SpineViewer.Spine.Spine spine)
|
||||
public class SpineBaseInfoProperty(SpineObject spine)
|
||||
{
|
||||
[Browsable(false)]
|
||||
public SpineViewer.Spine.Spine Spine { get; } = spine;
|
||||
public SpineObject Spine { get; } = spine;
|
||||
|
||||
/// <summary>
|
||||
/// 获取所属版本
|
||||
79
SpineViewer/Spine/SpineView/SpineDebugProperty.cs
Normal file
79
SpineViewer/Spine/SpineView/SpineDebugProperty.cs
Normal file
@@ -0,0 +1,79 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using SpineViewer.Spine;
|
||||
|
||||
namespace SpineViewer.Spine.SpineView
|
||||
{
|
||||
/// <summary>
|
||||
/// 用于在 PropertyGrid 上显示 Spine 调试属性的包装类
|
||||
/// </summary>
|
||||
public class SpineDebugProperty(SpineObject spine)
|
||||
{
|
||||
[Browsable(false)]
|
||||
public SpineObject Spine { get; } = spine;
|
||||
|
||||
/// <summary>
|
||||
/// 显示纹理
|
||||
/// </summary>
|
||||
[DisplayName("Texture")]
|
||||
public bool DebugTexture { get => Spine.DebugTexture; set => Spine.DebugTexture = value; }
|
||||
|
||||
/// <summary>
|
||||
/// 显示包围盒
|
||||
/// </summary>
|
||||
[DisplayName("Bounds")]
|
||||
public bool DebugBounds { get => Spine.DebugBounds; set => Spine.DebugBounds = value; }
|
||||
|
||||
/// <summary>
|
||||
/// 显示骨骼
|
||||
/// </summary>
|
||||
[DisplayName("Bones")]
|
||||
public bool DebugBones { get => Spine.DebugBones; set => Spine.DebugBones = value; }
|
||||
|
||||
/// <summary>
|
||||
/// 显示区域附件边框线
|
||||
/// </summary>
|
||||
[DisplayName("Regions")]
|
||||
public bool DebugRegions { get => Spine.DebugRegions; set => Spine.DebugRegions = value; }
|
||||
|
||||
/// <summary>
|
||||
/// 显示网格附件边框线
|
||||
/// </summary>
|
||||
[DisplayName("MeshHulls")]
|
||||
public bool DebugMeshHulls { get => Spine.DebugMeshHulls; set => Spine.DebugMeshHulls = value; }
|
||||
|
||||
/// <summary>
|
||||
/// 显示网格附件网格线
|
||||
/// </summary>
|
||||
[DisplayName("Meshes")]
|
||||
public bool DebugMeshes { get => Spine.DebugMeshes; set => Spine.DebugMeshes = value; }
|
||||
|
||||
///// <summary>
|
||||
///// 显示碰撞盒附件边框线
|
||||
///// </summary>
|
||||
//[DisplayName("BoudingBoxes")]
|
||||
//public bool DebugBoundingBoxes { get => Spine.DebugBoundingBoxes; set => Spine.DebugBoundingBoxes = value; }
|
||||
|
||||
///// <summary>
|
||||
///// 显示路径附件网格线
|
||||
///// </summary>
|
||||
//[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>
|
||||
[DisplayName("Clippings")]
|
||||
public bool DebugClippings { get => Spine.DebugClippings; set => Spine.DebugClippings = value; }
|
||||
}
|
||||
}
|
||||
41
SpineViewer/Spine/SpineView/SpineObjectProperty.cs
Normal file
41
SpineViewer/Spine/SpineView/SpineObjectProperty.cs
Normal file
@@ -0,0 +1,41 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using System.Drawing.Design;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using SpineViewer.Spine;
|
||||
|
||||
namespace SpineViewer.Spine.SpineView
|
||||
{
|
||||
public class SpineObjectProperty(SpineObject spine)
|
||||
{
|
||||
[Browsable(false)]
|
||||
public SpineObject Spine { get; } = spine;
|
||||
|
||||
[DisplayName("基本信息")]
|
||||
public SpineBaseInfoProperty BaseInfo { get; } = new(spine);
|
||||
|
||||
[DisplayName("渲染")]
|
||||
public SpineRenderProperty Render { get; } = new(spine);
|
||||
|
||||
[DisplayName("变换")]
|
||||
public SpineTransformProperty Transform { get; } = new(spine);
|
||||
|
||||
[TypeConverter(typeof(ExpandableObjectConverter))]
|
||||
[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);
|
||||
|
||||
[DisplayName("调试")]
|
||||
public SpineDebugProperty Debug { get; } = new(spine);
|
||||
}
|
||||
}
|
||||
@@ -4,16 +4,17 @@ using System.ComponentModel;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using SpineViewer.Spine;
|
||||
|
||||
namespace SpineViewer.PropertyGridWrappers.Spine
|
||||
namespace SpineViewer.Spine.SpineView
|
||||
{
|
||||
/// <summary>
|
||||
/// 用于在 PropertyGrid 上显示 Spine 渲染设置的包装类
|
||||
/// </summary>
|
||||
public class SpineRenderWrapper(SpineViewer.Spine.Spine spine)
|
||||
public class SpineRenderProperty(SpineObject spine)
|
||||
{
|
||||
[Browsable(false)]
|
||||
public SpineViewer.Spine.Spine Spine { get; } = spine;
|
||||
public SpineObject Spine { get; } = spine;
|
||||
|
||||
/// <summary>
|
||||
/// 是否被隐藏, 被隐藏的模型将仅仅在列表显示, 不参与其他行为
|
||||
94
SpineViewer/Spine/SpineView/SpineSkinProperty.cs
Normal file
94
SpineViewer/Spine/SpineView/SpineSkinProperty.cs
Normal file
@@ -0,0 +1,94 @@
|
||||
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>
|
||||
/// 皮肤动态类型包装类, 用于提供对 Spine 皮肤的管理能力
|
||||
/// </summary>
|
||||
/// <param name="spine">关联的 Spine 对象</param>
|
||||
public class SpineSkinProperty(SpineObject spine) : ICustomTypeDescriptor
|
||||
{
|
||||
[Browsable(false)]
|
||||
public SpineObject Spine { get; } = spine;
|
||||
|
||||
/// <summary>
|
||||
/// 在属性面板悬停可以显示已加载的皮肤列表
|
||||
/// </summary>
|
||||
public override string ToString() => $"[{string.Join(", ", Spine.SkinNames.Where(Spine.GetSkinStatus))}]";
|
||||
|
||||
public override bool Equals(object? obj)
|
||||
{
|
||||
if (obj is SpineSkinProperty prop) return ToString() == prop.ToString();
|
||||
return base.Equals(obj);
|
||||
}
|
||||
|
||||
public override int GetHashCode() => HashCode.Combine(typeof(SpineSkinProperty).FullName.GetHashCode(), ToString().GetHashCode());
|
||||
|
||||
#region ICustomTypeDescriptor 接口实现
|
||||
|
||||
// XXX: 必须实现 ICustomTypeDescriptor 接口, 不能继承 CustomTypeDescriptor, 似乎继承下来的东西会有问题, 导致某些调用不正确
|
||||
|
||||
private static readonly Dictionary<string, SkinPropertyDescriptor> 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 name in Spine.SkinNames)
|
||||
{
|
||||
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 SkinPropertyDescriptor(string name, Attribute[]? attributes) : PropertyDescriptor(name, attributes)
|
||||
{
|
||||
public override Type ComponentType => typeof(SpineSkinProperty);
|
||||
public override bool IsReadOnly => Name == "default";
|
||||
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;
|
||||
|
||||
public override object? GetValue(object? component)
|
||||
{
|
||||
if (component is SpineSkinProperty prop)
|
||||
return prop.Spine.GetSkinStatus(Name);
|
||||
return null;
|
||||
}
|
||||
|
||||
public override void SetValue(object? component, object? value)
|
||||
{
|
||||
if (component is SpineSkinProperty prop)
|
||||
{
|
||||
if (value is bool s)
|
||||
prop.Spine.SetSkinStatus(Name, s);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
170
SpineViewer/Spine/SpineView/SpineSlotProperty.cs
Normal file
170
SpineViewer/Spine/SpineView/SpineSlotProperty.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -4,16 +4,18 @@ using System.ComponentModel;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using SpineViewer.Spine;
|
||||
using SpineViewer.Utils;
|
||||
|
||||
namespace SpineViewer.PropertyGridWrappers.Spine
|
||||
namespace SpineViewer.Spine.SpineView
|
||||
{
|
||||
/// <summary>
|
||||
/// 用于在 PropertyGrid 上显示 Spine 空间变换的包装类
|
||||
/// </summary>
|
||||
public class SpineTransformWrapper(SpineViewer.Spine.Spine spine)
|
||||
public class SpineTransformProperty(SpineObject spine)
|
||||
{
|
||||
[Browsable(false)]
|
||||
public SpineViewer.Spine.Spine Spine { get; } = spine;
|
||||
public SpineObject Spine { get; } = spine;
|
||||
|
||||
/// <summary>
|
||||
/// 缩放比例
|
||||
@@ -7,7 +7,7 @@
|
||||
<TargetFramework>net8.0-windows</TargetFramework>
|
||||
<BaseOutputPath>$(SolutionDir)out</BaseOutputPath>
|
||||
<IncludeSourceRevisionInInformationalVersion>false</IncludeSourceRevisionInInformationalVersion>
|
||||
<Version>0.12.2</Version>
|
||||
<Version>0.12.6</Version>
|
||||
<OutputType>WinExe</OutputType>
|
||||
<UseWindowsForms>true</UseWindowsForms>
|
||||
<ApplicationIcon>appicon.ico</ApplicationIcon>
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user