增加多轨道动画编辑
This commit is contained in:
139
SpineViewer/Dialogs/AnimationTracksEditorDialog.Designer.cs
generated
Normal file
139
SpineViewer/Dialogs/AnimationTracksEditorDialog.Designer.cs
generated
Normal file
@@ -0,0 +1,139 @@
|
||||
namespace SpineViewer.Dialogs
|
||||
{
|
||||
partial class AnimationTracksEditorDialog
|
||||
{
|
||||
/// <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()
|
||||
{
|
||||
panel = new Panel();
|
||||
tableLayoutPanel1 = new TableLayoutPanel();
|
||||
flowLayoutPanel1 = new FlowLayoutPanel();
|
||||
button_Add = new Button();
|
||||
button_Delete = new Button();
|
||||
propertyGrid_AnimationTracks = new PropertyGrid();
|
||||
panel.SuspendLayout();
|
||||
tableLayoutPanel1.SuspendLayout();
|
||||
flowLayoutPanel1.SuspendLayout();
|
||||
SuspendLayout();
|
||||
//
|
||||
// panel
|
||||
//
|
||||
panel.Controls.Add(tableLayoutPanel1);
|
||||
panel.Dock = DockStyle.Fill;
|
||||
panel.Location = new Point(0, 0);
|
||||
panel.Name = "panel";
|
||||
panel.Padding = new Padding(50, 15, 50, 10);
|
||||
panel.Size = new Size(666, 483);
|
||||
panel.TabIndex = 0;
|
||||
//
|
||||
// tableLayoutPanel1
|
||||
//
|
||||
tableLayoutPanel1.ColumnCount = 1;
|
||||
tableLayoutPanel1.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 100F));
|
||||
tableLayoutPanel1.Controls.Add(flowLayoutPanel1, 0, 1);
|
||||
tableLayoutPanel1.Controls.Add(propertyGrid_AnimationTracks, 0, 0);
|
||||
tableLayoutPanel1.Dock = DockStyle.Fill;
|
||||
tableLayoutPanel1.Location = new Point(50, 15);
|
||||
tableLayoutPanel1.Name = "tableLayoutPanel1";
|
||||
tableLayoutPanel1.RowCount = 2;
|
||||
tableLayoutPanel1.RowStyles.Add(new RowStyle(SizeType.Percent, 100F));
|
||||
tableLayoutPanel1.RowStyles.Add(new RowStyle());
|
||||
tableLayoutPanel1.Size = new Size(566, 458);
|
||||
tableLayoutPanel1.TabIndex = 0;
|
||||
//
|
||||
// flowLayoutPanel1
|
||||
//
|
||||
flowLayoutPanel1.AutoSize = true;
|
||||
flowLayoutPanel1.Controls.Add(button_Add);
|
||||
flowLayoutPanel1.Controls.Add(button_Delete);
|
||||
flowLayoutPanel1.Dock = DockStyle.Fill;
|
||||
flowLayoutPanel1.Location = new Point(3, 415);
|
||||
flowLayoutPanel1.Name = "flowLayoutPanel1";
|
||||
flowLayoutPanel1.Size = new Size(560, 40);
|
||||
flowLayoutPanel1.TabIndex = 2;
|
||||
//
|
||||
// button_Add
|
||||
//
|
||||
button_Add.Location = new Point(3, 3);
|
||||
button_Add.Name = "button_Add";
|
||||
button_Add.Size = new Size(112, 34);
|
||||
button_Add.TabIndex = 0;
|
||||
button_Add.Text = "添加";
|
||||
button_Add.UseVisualStyleBackColor = true;
|
||||
button_Add.Click += button_Add_Click;
|
||||
//
|
||||
// button_Delete
|
||||
//
|
||||
button_Delete.Location = new Point(121, 3);
|
||||
button_Delete.Name = "button_Delete";
|
||||
button_Delete.Size = new Size(112, 34);
|
||||
button_Delete.TabIndex = 1;
|
||||
button_Delete.Text = "删除";
|
||||
button_Delete.UseVisualStyleBackColor = true;
|
||||
button_Delete.Click += button_Delete_Click;
|
||||
//
|
||||
// propertyGrid_AnimationTracks
|
||||
//
|
||||
propertyGrid_AnimationTracks.Dock = DockStyle.Fill;
|
||||
propertyGrid_AnimationTracks.HelpVisible = false;
|
||||
propertyGrid_AnimationTracks.Location = new Point(3, 3);
|
||||
propertyGrid_AnimationTracks.Name = "propertyGrid_AnimationTracks";
|
||||
propertyGrid_AnimationTracks.PropertySort = PropertySort.NoSort;
|
||||
propertyGrid_AnimationTracks.Size = new Size(560, 406);
|
||||
propertyGrid_AnimationTracks.TabIndex = 1;
|
||||
propertyGrid_AnimationTracks.ToolbarVisible = false;
|
||||
//
|
||||
// AnimationTracksEditorDialog
|
||||
//
|
||||
AutoScaleDimensions = new SizeF(11F, 24F);
|
||||
AutoScaleMode = AutoScaleMode.Font;
|
||||
ClientSize = new Size(666, 483);
|
||||
Controls.Add(panel);
|
||||
FormBorderStyle = FormBorderStyle.FixedDialog;
|
||||
MaximizeBox = false;
|
||||
MinimizeBox = false;
|
||||
Name = "AnimationTracksEditorDialog";
|
||||
ShowIcon = false;
|
||||
ShowInTaskbar = false;
|
||||
StartPosition = FormStartPosition.CenterScreen;
|
||||
Text = "多轨道动画实时编辑器";
|
||||
panel.ResumeLayout(false);
|
||||
tableLayoutPanel1.ResumeLayout(false);
|
||||
tableLayoutPanel1.PerformLayout();
|
||||
flowLayoutPanel1.ResumeLayout(false);
|
||||
ResumeLayout(false);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
private Panel panel;
|
||||
private TableLayoutPanel tableLayoutPanel1;
|
||||
private FlowLayoutPanel flowLayoutPanel1;
|
||||
private Button button_Add;
|
||||
private Button button_Delete;
|
||||
private PropertyGrid propertyGrid_AnimationTracks;
|
||||
}
|
||||
}
|
||||
43
SpineViewer/Dialogs/AnimationTracksEditorDialog.cs
Normal file
43
SpineViewer/Dialogs/AnimationTracksEditorDialog.cs
Normal file
@@ -0,0 +1,43 @@
|
||||
using SpineViewer.Spine;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using System.Data;
|
||||
using System.Drawing;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows.Forms;
|
||||
|
||||
namespace SpineViewer.Dialogs
|
||||
{
|
||||
public partial class AnimationTracksEditorDialog : Form
|
||||
{
|
||||
private readonly Spine.Spine spine;
|
||||
public AnimationTracksEditorDialog(Spine.Spine spine)
|
||||
{
|
||||
InitializeComponent();
|
||||
this.spine = spine;
|
||||
propertyGrid_AnimationTracks.SelectedObject = spine.AnimationTracks;
|
||||
}
|
||||
|
||||
private void button_Add_Click(object sender, EventArgs e)
|
||||
{
|
||||
spine.SetAnimation(spine.GetTrackIndices().Max() + 1, spine.AnimationNames[0]);
|
||||
propertyGrid_AnimationTracks.Refresh();
|
||||
}
|
||||
|
||||
private void button_Delete_Click(object sender, EventArgs e)
|
||||
{
|
||||
if (propertyGrid_AnimationTracks.SelectedGridItem?.Value is TrackWrapper tr)
|
||||
{
|
||||
if (tr.Index == 0)
|
||||
MessageBox.Info("必须保留轨道 0");
|
||||
else
|
||||
spine.ClearTrack(tr.Index);
|
||||
}
|
||||
propertyGrid_AnimationTracks.Refresh();
|
||||
propertyGrid_AnimationTracks.SelectedGridItem = propertyGrid_AnimationTracks.SelectedGridItem.Parent.GridItems.Cast<GridItem>().Last();
|
||||
}
|
||||
}
|
||||
}
|
||||
120
SpineViewer/Dialogs/AnimationTracksEditorDialog.resx
Normal file
120
SpineViewer/Dialogs/AnimationTracksEditorDialog.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>
|
||||
124
SpineViewer/Spine/AnimationTracks.cs
Normal file
124
SpineViewer/Spine/AnimationTracks.cs
Normal file
@@ -0,0 +1,124 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace SpineViewer.Spine
|
||||
{
|
||||
/// <summary>
|
||||
/// 对轨道索引的包装类, 能够在面板上显示例如时长的属性, 但是处理该属性时按字符串去处理, 例如 ToString 和判断对象相等都是用动画名称实现逻辑
|
||||
/// </summary>
|
||||
/// <param name="spine"></param>
|
||||
/// <param name="i"></param>
|
||||
[TypeConverter(typeof(TrackWrapperConverter))]
|
||||
public class TrackWrapper(Spine spine, int i)
|
||||
{
|
||||
private readonly 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 tr) return ToString() == obj.ToString();
|
||||
return base.Equals(obj);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 哈希码需要和 Equals 行为类似
|
||||
/// </summary>
|
||||
public override int GetHashCode() => ToString().GetHashCode();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 轨道属性描述符, 实现对属性的读取和赋值
|
||||
/// </summary>
|
||||
/// <param name="spine">关联的 Spine 对象</param>
|
||||
/// <param name="i">轨道索引</param>
|
||||
public class TrackWrapperPropertyDescriptor(Spine spine, int i) : PropertyDescriptor($"Track{i}", [new DisplayNameAttribute($"轨道 {i}")])
|
||||
{
|
||||
private readonly Spine spine = spine;
|
||||
private readonly int idx = i;
|
||||
|
||||
public override Type ComponentType => typeof(AnimationTracksType);
|
||||
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) => new TrackWrapper(spine, idx);
|
||||
|
||||
/// <summary>
|
||||
/// 允许通过字符串赋值修改该轨道的动画, 这里决定了当其他地方的调用 (比如 Converter) 通过 value 来设置属性值的时候应该怎么处理
|
||||
/// </summary>
|
||||
public override void SetValue(object? component, object? value)
|
||||
{
|
||||
if (value is string s) spine.SetAnimation(idx, s);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// AnimationTracks 动态类型包装类, 用于提供对 Spine 对象多轨道动画的访问能力, 不同轨道将动态生成属性
|
||||
/// </summary>
|
||||
/// <param name="spine">关联的 Spine 对象</param>
|
||||
public class AnimationTracksType(Spine spine) : ICustomTypeDescriptor
|
||||
{
|
||||
private readonly Dictionary<int, TrackWrapperPropertyDescriptor> pdCache = [];
|
||||
public Spine Spine { get; } = spine;
|
||||
|
||||
// XXX: 必须实现 ICustomTypeDescriptor 接口, 不能继承 CustomTypeDescriptor, 似乎继承下来的东西会有问题, 导致某些调用不正确
|
||||
|
||||
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 List<TrackWrapperPropertyDescriptor>();
|
||||
foreach (var i in Spine.GetTrackIndices())
|
||||
{
|
||||
if (!pdCache.ContainsKey(i))
|
||||
pdCache[i] = new TrackWrapperPropertyDescriptor(Spine, i);
|
||||
props.Add(pdCache[i]);
|
||||
}
|
||||
return new PropertyDescriptorCollection(props.ToArray());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 在属性面板悬停可以按轨道顺序显示动画名称
|
||||
/// </summary>
|
||||
public override string ToString() => $"[{string.Join(", ", Spine.GetTrackIndices().Select(Spine.GetAnimation))}]";
|
||||
|
||||
public override bool Equals(object? obj)
|
||||
{
|
||||
if (obj is AnimationTracksType tracks) return ToString() == tracks.ToString();
|
||||
return base.Equals(obj);
|
||||
}
|
||||
|
||||
public override int GetHashCode() => ToString().GetHashCode();
|
||||
}
|
||||
}
|
||||
@@ -171,7 +171,7 @@ namespace SpineViewer.Spine.Implementations.Spine
|
||||
}
|
||||
}
|
||||
|
||||
protected override int[] trackIndices => animationState.Tracks.Select((_, i) => i).Where(i => animationState.Tracks[i] is not null).ToArray();
|
||||
protected override int[] getTrackIndices() => animationState.Tracks.Select((_, i) => i).Where(i => animationState.Tracks[i] is not null).ToArray();
|
||||
|
||||
protected override string getAnimation(int track) => animationState.GetCurrent(track)?.Animation.Name ?? EMPTY_ANIMATION;
|
||||
|
||||
|
||||
@@ -170,7 +170,7 @@ namespace SpineViewer.Spine.Implementations.Spine
|
||||
}
|
||||
}
|
||||
|
||||
protected override int[] trackIndices => animationState.Tracks.Select((_, i) => i).Where(i => animationState.Tracks.Items[i] is not null).ToArray();
|
||||
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;
|
||||
|
||||
|
||||
@@ -140,7 +140,7 @@ namespace SpineViewer.Spine.Implementations.Spine
|
||||
}
|
||||
}
|
||||
|
||||
protected override int[] trackIndices => animationState.Tracks.Select((_, i) => i).Where(i => animationState.Tracks.Items[i] is not null).ToArray();
|
||||
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;
|
||||
|
||||
|
||||
@@ -146,7 +146,7 @@ namespace SpineViewer.Spine.Implementations.Spine
|
||||
}
|
||||
}
|
||||
|
||||
protected override int[] trackIndices => animationState.Tracks.Select((_, i) => i).Where(i => animationState.Tracks.Items[i] is not null).ToArray();
|
||||
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;
|
||||
|
||||
|
||||
@@ -142,7 +142,7 @@ namespace SpineViewer.Spine.Implementations.Spine
|
||||
}
|
||||
}
|
||||
|
||||
protected override int[] trackIndices => animationState.Tracks.Select((_, i) => i).Where(i => animationState.Tracks.Items[i] is not null).ToArray();
|
||||
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;
|
||||
|
||||
|
||||
@@ -142,7 +142,7 @@ namespace SpineViewer.Spine.Implementations.Spine
|
||||
}
|
||||
}
|
||||
|
||||
protected override int[] trackIndices => animationState.Tracks.Select((_, i) => i).Where(i => animationState.Tracks.Items[i] is not null).ToArray();
|
||||
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;
|
||||
|
||||
|
||||
@@ -142,7 +142,7 @@ namespace SpineViewer.Spine.Implementations.Spine
|
||||
}
|
||||
}
|
||||
|
||||
protected override int[] trackIndices => animationState.Tracks.Select((_, i) => i).Where(i => animationState.Tracks.Items[i] is not null).ToArray();
|
||||
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;
|
||||
|
||||
|
||||
@@ -235,13 +235,6 @@ namespace SpineViewer.Spine
|
||||
|
||||
#region 属性 | [3] 动画
|
||||
|
||||
/// <summary>
|
||||
/// 包含的所有皮肤名称
|
||||
/// </summary>
|
||||
[Browsable(false)]
|
||||
public ReadOnlyCollection<string> SkinNames { get; private set; }
|
||||
protected List<string> skinNames = [];
|
||||
|
||||
/// <summary>
|
||||
/// 使用的皮肤名称, 如果设置的皮肤不存在则忽略
|
||||
/// </summary>
|
||||
@@ -254,13 +247,6 @@ namespace SpineViewer.Spine
|
||||
}
|
||||
protected abstract string skin { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 包含的所有动画名称
|
||||
/// </summary>
|
||||
[Browsable(false)]
|
||||
public ReadOnlyCollection<string> AnimationNames { get; private set; }
|
||||
protected List<string> animationNames = [EMPTY_ANIMATION];
|
||||
|
||||
/// <summary>
|
||||
/// 默认轨道动画名称, 如果设置的动画不存在则忽略
|
||||
/// </summary>
|
||||
@@ -276,34 +262,58 @@ namespace SpineViewer.Spine
|
||||
/// 默认轨道动画时长
|
||||
/// </summary>
|
||||
[Category("[3] 动画"), DisplayName("轨道 0 动画时长")]
|
||||
public float Track0AnimationDuration { get => GetAnimationDuration(Track0Animation); } // TODO: 动画时长变成伪属性在面板显示
|
||||
public float Track0AnimationDuration => GetAnimationDuration(Track0Animation);
|
||||
|
||||
/// <summary>
|
||||
/// 默认轨道动画时长
|
||||
/// </summary>
|
||||
[Editor(typeof(CollectionEditor), typeof(UITypeEditor))]
|
||||
[Category("[3] 动画"), DisplayName("多轨道动画")]
|
||||
public AnimationTrackDict AnimationTracks { get; private set; }
|
||||
[Editor(typeof(AnimationTracksEditor), typeof(UITypeEditor))]
|
||||
[TypeConverter(typeof(ExpandableObjectConverter))]
|
||||
[Category("[3] 动画"), DisplayName("多轨道动画管理")]
|
||||
public AnimationTracksType AnimationTracks { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// 包含的所有皮肤名称
|
||||
/// </summary>
|
||||
[Browsable(false)]
|
||||
public ReadOnlyCollection<string> SkinNames { get; private set; }
|
||||
protected List<string> skinNames = [];
|
||||
|
||||
/// <summary>
|
||||
/// 包含的所有动画名称
|
||||
/// </summary>
|
||||
[Browsable(false)]
|
||||
public ReadOnlyCollection<string> AnimationNames { get; private set; }
|
||||
protected List<string> animationNames = [EMPTY_ANIMATION];
|
||||
|
||||
/// <summary>
|
||||
/// 获取所有非 null 的轨道索引
|
||||
/// </summary>
|
||||
protected abstract int[] trackIndices { get; }
|
||||
public int[] GetTrackIndices() { lock (_lock) return getTrackIndices(); }
|
||||
protected abstract int[] getTrackIndices();
|
||||
|
||||
/// <summary>
|
||||
/// 获取指定轨道的当前动画, 如果没有, 应当返回空动画名称
|
||||
/// </summary>
|
||||
public string GetAnimation(int track) { lock (_lock) return getAnimation(track); }
|
||||
protected abstract string getAnimation(int track);
|
||||
|
||||
/// <summary>
|
||||
/// 设置某个轨道动画
|
||||
/// </summary>
|
||||
public void SetAnimation(int track, string name) { lock (_lock) setAnimation(track, name); }
|
||||
protected abstract void setAnimation(int track, string name);
|
||||
|
||||
/// <summary>
|
||||
/// 清除某个轨道, 与设置空动画不同, 是彻底删除轨道内的东西
|
||||
/// </summary>
|
||||
protected abstract void clearTrack(int i);
|
||||
public void ClearTrack(int i) { lock (_lock) clearTrack(i); }
|
||||
protected abstract void clearTrack(int i); // XXX: 清除轨道之后被加载的附件还是会保留, 不会自动卸下, 除非使用 SetSlotsToSetupPose
|
||||
|
||||
/// <summary>
|
||||
/// 获取动画时长, 如果动画不存在则返回 0
|
||||
/// </summary>
|
||||
public abstract float GetAnimationDuration(string name);
|
||||
|
||||
#endregion
|
||||
|
||||
@@ -384,11 +394,6 @@ namespace SpineViewer.Spine
|
||||
[Browsable(false)]
|
||||
public Image Preview { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// 获取动画时长, 如果动画不存在则返回 0
|
||||
/// </summary>
|
||||
public abstract float GetAnimationDuration(string name);
|
||||
|
||||
/// <summary>
|
||||
/// 更新内部状态
|
||||
/// </summary>
|
||||
@@ -425,128 +430,5 @@ namespace SpineViewer.Spine
|
||||
|
||||
#endregion
|
||||
|
||||
/// <summary>
|
||||
/// 多轨动画管理集合
|
||||
/// </summary>
|
||||
/// <param name="spine"></param>
|
||||
public class AnimationTrackDict(Spine spine) : IDictionary<int, string>
|
||||
{
|
||||
private readonly Spine sp = spine;
|
||||
|
||||
public string this[int key]
|
||||
{
|
||||
get
|
||||
{
|
||||
lock (sp._lock)
|
||||
{
|
||||
if (!sp.trackIndices.Contains(key))
|
||||
throw new KeyNotFoundException($"Track {key} not found.");
|
||||
return sp.getAnimation(key);
|
||||
}
|
||||
}
|
||||
set
|
||||
{
|
||||
lock (sp._lock) sp.setAnimation(key, value);
|
||||
}
|
||||
}
|
||||
|
||||
public ICollection<int> Keys
|
||||
{
|
||||
get { lock (sp._lock) return sp.trackIndices; }
|
||||
}
|
||||
|
||||
public ICollection<string> Values
|
||||
{
|
||||
get { lock (sp._lock) return sp.trackIndices.Select(sp.getAnimation).ToArray(); }
|
||||
}
|
||||
|
||||
public int Count
|
||||
{
|
||||
get { lock (sp._lock) return sp.trackIndices.Length; }
|
||||
}
|
||||
|
||||
public bool IsReadOnly => false;
|
||||
|
||||
public void Add(int key, string value)
|
||||
{
|
||||
lock (sp._lock) sp.setAnimation(key, value);
|
||||
}
|
||||
|
||||
public void Add(KeyValuePair<int, string> item)
|
||||
{
|
||||
lock (sp._lock) sp.setAnimation(item.Key, item.Value);
|
||||
}
|
||||
|
||||
public void Clear()
|
||||
{
|
||||
lock (sp._lock) foreach (var i in sp.trackIndices) sp.setAnimation(i, EMPTY_ANIMATION);
|
||||
}
|
||||
|
||||
public bool Contains(KeyValuePair<int, string> item)
|
||||
{
|
||||
lock (sp._lock) return sp.trackIndices.Contains(item.Key) && sp.getAnimation(item.Key) == item.Value;
|
||||
}
|
||||
|
||||
public bool ContainsKey(int key)
|
||||
{
|
||||
lock (sp._lock) return sp.trackIndices.Contains(key);
|
||||
}
|
||||
|
||||
public void CopyTo(KeyValuePair<int, string>[] array, int arrayIndex)
|
||||
{
|
||||
lock (sp._lock) foreach (var i in sp.trackIndices) array[arrayIndex++] = new KeyValuePair<int, string>(i, sp.getAnimation(i));
|
||||
}
|
||||
|
||||
public IEnumerator<KeyValuePair<int, string>> GetEnumerator()
|
||||
{
|
||||
List<KeyValuePair<int, string>> cache;
|
||||
lock (sp._lock)
|
||||
{
|
||||
cache = sp.trackIndices.Select(i => new KeyValuePair<int, string>(i, sp.getAnimation(i))).ToList();
|
||||
}
|
||||
foreach (var item in cache)
|
||||
{
|
||||
yield return item;
|
||||
}
|
||||
}
|
||||
|
||||
public bool Remove(int key)
|
||||
{
|
||||
lock (sp._lock)
|
||||
{
|
||||
sp.setAnimation(key, EMPTY_ANIMATION);
|
||||
sp.clearTrack(key);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
public bool Remove(KeyValuePair<int, string> item)
|
||||
{
|
||||
lock (sp._lock)
|
||||
{
|
||||
sp.setAnimation(item.Key, EMPTY_ANIMATION);
|
||||
sp.clearTrack(item.Key);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
public bool TryGetValue(int key, [MaybeNullWhen(false)] out string value)
|
||||
{
|
||||
value = null;
|
||||
lock (sp._lock)
|
||||
{
|
||||
if (!sp.trackIndices.Contains(key)) return false;
|
||||
value = sp.getAnimation(key);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
lock (sp._lock) return string.Join(", ", sp.trackIndices.Select(sp.getAnimation));
|
||||
}
|
||||
|
||||
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -21,6 +21,32 @@ namespace SpineViewer.Spine
|
||||
}
|
||||
}
|
||||
|
||||
public class SkinConverter : 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 Spine obj)
|
||||
{
|
||||
return new StandardValuesCollection(obj.SkinNames);
|
||||
}
|
||||
else if (context?.Instance is 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 AnimationConverter : StringConverter
|
||||
{
|
||||
public override bool GetStandardValuesSupported(ITypeDescriptorContext? context) => true;
|
||||
@@ -47,25 +73,34 @@ namespace SpineViewer.Spine
|
||||
}
|
||||
}
|
||||
|
||||
public class SkinConverter : StringConverter
|
||||
/// <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 Spine obj)
|
||||
if (context.Instance is AnimationTracksType animTrack)
|
||||
{
|
||||
return new StandardValuesCollection(obj.SkinNames);
|
||||
return new StandardValuesCollection(animTrack.Spine.AnimationNames);
|
||||
}
|
||||
else if (context?.Instance is Spine[] spines)
|
||||
else if (context.Instance is object[] instances && instances.All(x => x is AnimationTracksType))
|
||||
{
|
||||
if (spines.Length > 0)
|
||||
// XXX: 这里不知道为啥总是会得到 object[] 类型而不是具体的 AnimationTracksType[] 类型
|
||||
var animTracks = instances.Cast<AnimationTracksType>().ToArray();
|
||||
if (animTracks.Length > 0)
|
||||
{
|
||||
IEnumerable<string> common = spines[0].SkinNames;
|
||||
foreach (var spine in spines.Skip(1))
|
||||
common = common.Union(spine.SkinNames);
|
||||
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());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using System;
|
||||
using SpineViewer.Dialogs;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using System.Drawing.Design;
|
||||
@@ -36,4 +37,28 @@ namespace SpineViewer.Spine
|
||||
openFileDialog.Filter = "atlas 文件 (*.atlas)|*.atlas|所有文件 (*.*)|*.*";
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 多轨道动画编辑器
|
||||
/// </summary>
|
||||
public class AnimationTracksEditor : UITypeEditor
|
||||
{
|
||||
public override UITypeEditorEditStyle GetEditStyle(ITypeDescriptorContext? context) => UITypeEditorEditStyle.Modal;
|
||||
|
||||
public override object? EditValue(ITypeDescriptorContext? context, IServiceProvider provider, object? value)
|
||||
{
|
||||
if (provider == null || context == null || context.Instance is not Spine)
|
||||
return value;
|
||||
|
||||
IWindowsFormsEditorService editorService = provider.GetService(typeof(IWindowsFormsEditorService)) as IWindowsFormsEditorService;
|
||||
if (editorService == null)
|
||||
return value;
|
||||
|
||||
using (var dialog = new AnimationTracksEditorDialog((Spine)context.Instance))
|
||||
editorService.ShowDialog(dialog);
|
||||
|
||||
TypeDescriptor.Refresh(context.Instance);
|
||||
return value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user