增加多轨道动画编辑

This commit is contained in:
ww-rm
2025-04-02 23:59:18 +08:00
parent 8b7866d37f
commit 53d987476e
14 changed files with 533 additions and 165 deletions

View 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;
}
}

View 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();
}
}
}

View 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>

View 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();
}
}

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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();
}
}
}

View File

@@ -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());
}
}

View File

@@ -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;
}
}
}