整理结构

This commit is contained in:
ww-rm
2025-04-12 11:35:36 +08:00
parent 2ae175abd0
commit 90bfaa7b56
19 changed files with 339 additions and 381 deletions

View File

@@ -34,14 +34,14 @@ namespace SpineViewer.Controls
{ {
if (File.Exists(path)) 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)); listBox.Items.Add(Path.GetFullPath(path));
} }
else if (Directory.Exists(path)) else if (Directory.Exists(path))
{ {
foreach (var file in Directory.EnumerateFiles(path, "*.*", SearchOption.AllDirectories)) 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); listBox.Items.Add(file);
} }
} }
@@ -58,7 +58,7 @@ namespace SpineViewer.Controls
{ {
foreach (var file in Directory.EnumerateFiles(path, "*.*", SearchOption.AllDirectories)) 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); listBox.Items.Add(file);
} }
} }

View File

@@ -39,7 +39,7 @@ namespace SpineViewer.Controls
/// <summary> /// <summary>
/// 用于属性页显示模型参数的包装类 /// 用于属性页显示模型参数的包装类
/// </summary> /// </summary>
private readonly Dictionary<string, SpineWrapper> spinePropertyWrappers = []; private readonly Dictionary<string, SpineObjectProperty> spinePropertyWrappers = [];
public SpineListView() public SpineListView()
{ {
@@ -205,14 +205,14 @@ namespace SpineViewer.Controls
{ {
if (File.Exists(path)) if (File.Exists(path))
{ {
if (SpineHelper.CommonSkelSuffix.Contains(Path.GetExtension(path).ToLower())) if (SpineUtils.CommonSkelSuffix.Contains(Path.GetExtension(path).ToLower()))
validPaths.Add(path); validPaths.Add(path);
} }
else if (Directory.Exists(path)) else if (Directory.Exists(path))
{ {
foreach (var file in Directory.EnumerateFiles(path, "*.*", SearchOption.AllDirectories)) 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); validPaths.Add(file);
} }
} }

View File

@@ -22,7 +22,7 @@ namespace SpineViewer.Controls
/// <summary> /// <summary>
/// 设置选中的对象列表, 可以赋值 null 来清空选中, 行为与 PropertyGrid.SelectedObjects 类似 /// 设置选中的对象列表, 可以赋值 null 来清空选中, 行为与 PropertyGrid.SelectedObjects 类似
/// </summary> /// </summary>
public SpineWrapper[] SelectedSpines public SpineObjectProperty[] SelectedSpines
{ {
get => selectedSpines ?? []; get => selectedSpines ?? [];
set set
@@ -49,14 +49,14 @@ namespace SpineViewer.Controls
} }
} }
} }
private SpineWrapper[]? selectedSpines = null; private SpineObjectProperty[]? selectedSpines = null;
private void contextMenuStrip_Skin_Opening(object sender, CancelEventArgs e) private void contextMenuStrip_Skin_Opening(object sender, CancelEventArgs e)
{ {
if (selectedSpines?.Length == 1) if (selectedSpines?.Length == 1)
{ {
toolStripMenuItem_AddSkin.Enabled = true; toolStripMenuItem_AddSkin.Enabled = true;
toolStripMenuItem_RemoveSkin.Enabled = propertyGrid_Skin.SelectedGridItem.Value is SkinWrapper; toolStripMenuItem_RemoveSkin.Enabled = propertyGrid_Skin.SelectedGridItem.Value is SkinNameProperty;
} }
else else
{ {
@@ -70,7 +70,7 @@ namespace SpineViewer.Controls
if (selectedSpines?.Length == 1) if (selectedSpines?.Length == 1)
{ {
toolStripMenuItem_AddAnimation.Enabled = true; toolStripMenuItem_AddAnimation.Enabled = true;
toolStripMenuItem_RemoveAnimation.Enabled = propertyGrid_Animation.SelectedGridItem.Value is TrackWrapper; toolStripMenuItem_RemoveAnimation.Enabled = propertyGrid_Animation.SelectedGridItem.Value is TrackAnimationProperty;
} }
else else
{ {
@@ -99,7 +99,7 @@ namespace SpineViewer.Controls
{ {
if (selectedSpines?.Length != 1) return; if (selectedSpines?.Length != 1) return;
if (propertyGrid_Skin.SelectedGridItem.Value is SkinWrapper wrapper) if (propertyGrid_Skin.SelectedGridItem.Value is SkinNameProperty wrapper)
{ {
selectedSpines[0].Skin.Spine.UnloadSkin(wrapper.Index); selectedSpines[0].Skin.Spine.UnloadSkin(wrapper.Index);
propertyGrid_Skin.Refresh(); propertyGrid_Skin.Refresh();
@@ -119,7 +119,7 @@ namespace SpineViewer.Controls
{ {
if (selectedSpines?.Length != 1) return; 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); selectedSpines[0].Animation.Spine.ClearTrack(wrapper.Index);
propertyGrid_Animation.Refresh(); propertyGrid_Animation.Refresh();

View File

@@ -22,7 +22,7 @@ namespace SpineViewer.Dialogs
public BatchOpenSpineDialog() public BatchOpenSpineDialog()
{ {
InitializeComponent(); InitializeComponent();
comboBox_Version.DataSource = SpineHelper.Names.ToList(); comboBox_Version.DataSource = SpineUtils.Names.ToList();
comboBox_Version.DisplayMember = "Value"; comboBox_Version.DisplayMember = "Value";
comboBox_Version.ValueMember = "Key"; comboBox_Version.ValueMember = "Key";
comboBox_Version.SelectedValue = SpineVersion.Auto; comboBox_Version.SelectedValue = SpineVersion.Auto;

View File

@@ -23,13 +23,13 @@ namespace SpineViewer.Dialogs
{ {
InitializeComponent(); InitializeComponent();
comboBox_SourceVersion.DataSource = SpineHelper.Names.ToList(); comboBox_SourceVersion.DataSource = SpineUtils.Names.ToList();
comboBox_SourceVersion.DisplayMember = "Value"; comboBox_SourceVersion.DisplayMember = "Value";
comboBox_SourceVersion.ValueMember = "Key"; comboBox_SourceVersion.ValueMember = "Key";
comboBox_SourceVersion.SelectedValue = SpineVersion.Auto; comboBox_SourceVersion.SelectedValue = SpineVersion.Auto;
// 目标版本不包含自动 // 目标版本不包含自动
var versionsWithoutAuto = SpineHelper.Names.ToDictionary(); var versionsWithoutAuto = SpineUtils.Names.ToDictionary();
versionsWithoutAuto.Remove(SpineVersion.Auto); versionsWithoutAuto.Remove(SpineVersion.Auto);
comboBox_TargetVersion.DataSource = versionsWithoutAuto.ToList(); comboBox_TargetVersion.DataSource = versionsWithoutAuto.ToList();
comboBox_TargetVersion.DisplayMember = "Value"; comboBox_TargetVersion.DisplayMember = "Value";

View File

@@ -21,7 +21,7 @@ namespace SpineViewer.Dialogs
public OpenSpineDialog() public OpenSpineDialog()
{ {
InitializeComponent(); InitializeComponent();
comboBox_Version.DataSource = SpineHelper.Names.ToList(); comboBox_Version.DataSource = SpineUtils.Names.ToList();
comboBox_Version.DisplayMember = "Value"; comboBox_Version.DisplayMember = "Value";
comboBox_Version.ValueMember = "Key"; comboBox_Version.ValueMember = "Key";
comboBox_Version.SelectedValue = SpineVersion.Auto; comboBox_Version.SelectedValue = SpineVersion.Auto;

View File

@@ -379,7 +379,7 @@ namespace SpineViewer
{ {
try try
{ {
srcCvter = SkeletonConverter.New(SpineHelper.GetVersion(skelPath)); srcCvter = SkeletonConverter.New(SpineUtils.GetVersion(skelPath));
} }
catch (Exception ex) catch (Exception ex)
{ {

View File

@@ -38,7 +38,7 @@ namespace SpineViewer.Spine
skelPath = Path.GetFullPath(skelPath); skelPath = Path.GetFullPath(skelPath);
atlasPath = Path.GetFullPath(atlasPath); 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"); if (!File.Exists(atlasPath)) throw new FileNotFoundException($"atlas file {atlasPath} not found");
return New(version, [skelPath, atlasPath]).PostInit(); return New(version, [skelPath, atlasPath]).PostInit();
} }

View File

@@ -12,35 +12,10 @@ using System.Threading.Tasks;
namespace SpineViewer.Spine 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> /// <summary>
/// Spine 版本静态辅助类 /// Spine 版本静态辅助类
/// </summary> /// </summary>
public static class SpineHelper public static class SpineUtils
{ {
/// <summary> /// <summary>
/// 版本名称 /// 版本名称
@@ -53,7 +28,7 @@ namespace SpineViewer.Spine
/// </summary> /// </summary>
private static readonly Dictionary<SpineVersion, string> runtimes = []; private static readonly Dictionary<SpineVersion, string> runtimes = [];
static SpineHelper() static SpineUtils()
{ {
// 初始化缓存 // 初始化缓存
foreach (var value in Enum.GetValues(typeof(SpineVersion))) foreach (var value in Enum.GetValues(typeof(SpineVersion)))

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

View File

@@ -9,85 +9,11 @@ using System.Threading.Tasks;
namespace SpineViewer.Spine.SpineView namespace SpineViewer.Spine.SpineView
{ {
/// <summary>
/// 对轨道索引属性的包装类, 能够在面板上显示例如时长的属性, 但是处理该属性时按字符串去处理, 例如 ToString 和判断对象相等都是用动画名称实现逻辑
/// </summary>
/// <param name="spine"></param>
/// <param name="i"></param>
[TypeConverter(typeof(TrackWrapperConverter))]
public class TrackWrapper(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 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> /// <summary>
/// 用于在 PropertyGrid 上显示 Spine 动画列表的包装类 /// 用于在 PropertyGrid 上显示 Spine 动画列表的包装类
/// </summary> /// </summary>
public class SpineAnimationWrapper(SpineObject 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)] [Browsable(false)]
public SpineObject Spine { get; } = spine; public SpineObject Spine { get; } = spine;
@@ -98,24 +24,24 @@ namespace SpineViewer.Spine.SpineView
public float AnimationTracksMaxDuration => Spine.GetTrackIndices().Select(i => Spine.GetAnimationDuration(Spine.GetAnimation(i))).Max(); public float AnimationTracksMaxDuration => Spine.GetTrackIndices().Select(i => Spine.GetAnimationDuration(Spine.GetAnimation(i))).Max();
/// <summary> /// <summary>
/// TrackWrapper 属性对象缓存 /// <see cref="TrackAnimationProperty"/> 属性对象缓存
/// </summary> /// </summary>
private readonly Dictionary<int, TrackWrapper> trackWrapperProperties = []; private readonly Dictionary<int, TrackAnimationProperty> trackAnimationProperties = [];
/// <summary> /// <summary>
/// 访问 TrackWrapper 属性 <c>AnimationTracks.Track{i}</c> /// <c>this.Track{i}</c>
/// </summary> /// </summary>
public TrackWrapper GetTrackWrapper(int i) public TrackAnimationProperty GetTrackAnimation(int i)
{ {
if (!trackWrapperProperties.ContainsKey(i)) if (!trackAnimationProperties.ContainsKey(i))
trackWrapperProperties[i] = new TrackWrapper(Spine, i); trackAnimationProperties[i] = new TrackAnimationProperty(Spine, i);
return trackWrapperProperties[i]; return trackAnimationProperties[i];
} }
/// <summary> /// <summary>
/// 设置 TrackWrapper 属性 <c>AnimationTracks.Track{i} = <paramref name="value"/></c> /// <c>this.Track{i} = <paramref name="value"/></c>
/// </summary> /// </summary>
public void SetTrackWrapper(int i, string value) public void SetTrackAnimation(int i, string value)
{ {
Spine.SetAnimation(i, value); Spine.SetAnimation(i, value);
TypeDescriptor.Refresh(this); TypeDescriptor.Refresh(this);
@@ -128,11 +54,11 @@ namespace SpineViewer.Spine.SpineView
public override bool Equals(object? obj) 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); 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 #region ICustomTypeDescriptor
@@ -166,6 +92,113 @@ namespace SpineViewer.Spine.SpineView
return props; 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 #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 && instances.All(x => x is SpineAnimationProperty))
{
// XXX: 这里不知道为啥总是会得到 object[] 类型而不是具体的类型
var animTracks = instances.Cast<SpineAnimationProperty>().ToArray();
if (animTracks.Length > 0)
{
IEnumerable<string> common = animTracks[0].Spine.AnimationNames;
foreach (var t in animTracks.Skip(1))
common = common.Union(t.Spine.AnimationNames);
return new StandardValuesCollection(common.ToArray());
}
}
return base.GetStandardValues(context);
}
}
} }

View File

@@ -12,7 +12,7 @@ namespace SpineViewer.Spine.SpineView
/// <summary> /// <summary>
/// 用于在 PropertyGrid 上显示 Spine 基本信息的包装类 /// 用于在 PropertyGrid 上显示 Spine 基本信息的包装类
/// </summary> /// </summary>
public class SpineBaseInfoWrapper(SpineObject spine) public class SpineBaseInfoProperty(SpineObject spine)
{ {
[Browsable(false)] [Browsable(false)]
public SpineObject Spine { get; } = spine; public SpineObject Spine { get; } = spine;

View File

@@ -11,7 +11,7 @@ namespace SpineViewer.Spine.SpineView
/// <summary> /// <summary>
/// 用于在 PropertyGrid 上显示 Spine 调试属性的包装类 /// 用于在 PropertyGrid 上显示 Spine 调试属性的包装类
/// </summary> /// </summary>
public class SpineDebugWrapper(SpineObject spine) public class SpineDebugProperty(SpineObject spine)
{ {
[Browsable(false)] [Browsable(false)]
public SpineObject Spine { get; } = spine; public SpineObject Spine { get; } = spine;

View File

@@ -9,29 +9,29 @@ using SpineViewer.Spine;
namespace SpineViewer.Spine.SpineView namespace SpineViewer.Spine.SpineView
{ {
public class SpineWrapper(SpineObject spine) public class SpineObjectProperty(SpineObject spine)
{ {
[Browsable(false)] [Browsable(false)]
public SpineObject Spine { get; } = spine; public SpineObject Spine { get; } = spine;
[DisplayName("基本信息")] [DisplayName("基本信息")]
public SpineBaseInfoWrapper BaseInfo { get; } = new(spine); public SpineBaseInfoProperty BaseInfo { get; } = new(spine);
[DisplayName("渲染")] [DisplayName("渲染")]
public SpineRenderWrapper Render { get; } = new(spine); public SpineRenderProperty Render { get; } = new(spine);
[DisplayName("变换")] [DisplayName("变换")]
public SpineTransformWrapper Transform { get; } = new(spine); public SpineTransformProperty Transform { get; } = new(spine);
[TypeConverter(typeof(ExpandableObjectConverter))] [TypeConverter(typeof(ExpandableObjectConverter))]
[DisplayName("皮肤")] [DisplayName("皮肤")]
public SpineSkinWrapper Skin { get; } = new(spine); public SpineSkinProperty Skin { get; } = new(spine);
[TypeConverter(typeof(ExpandableObjectConverter))] [TypeConverter(typeof(ExpandableObjectConverter))]
[DisplayName("动画")] [DisplayName("动画")]
public SpineAnimationWrapper Animation { get; } = new(spine); public SpineAnimationProperty Animation { get; } = new(spine);
[DisplayName("调试")] [DisplayName("调试")]
public SpineDebugWrapper Debug { get; } = new(spine); public SpineDebugProperty Debug { get; } = new(spine);
} }
} }

View File

@@ -11,7 +11,7 @@ namespace SpineViewer.Spine.SpineView
/// <summary> /// <summary>
/// 用于在 PropertyGrid 上显示 Spine 渲染设置的包装类 /// 用于在 PropertyGrid 上显示 Spine 渲染设置的包装类
/// </summary> /// </summary>
public class SpineRenderWrapper(SpineObject spine) public class SpineRenderProperty(SpineObject spine)
{ {
[Browsable(false)] [Browsable(false)]
public SpineObject Spine { get; } = spine; public SpineObject Spine { get; } = spine;

View File

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

View File

@@ -12,7 +12,7 @@ namespace SpineViewer.Spine.SpineView
/// <summary> /// <summary>
/// 用于在 PropertyGrid 上显示 Spine 空间变换的包装类 /// 用于在 PropertyGrid 上显示 Spine 空间变换的包装类
/// </summary> /// </summary>
public class SpineTransformWrapper(SpineObject spine) public class SpineTransformProperty(SpineObject spine)
{ {
[Browsable(false)] [Browsable(false)]
public SpineObject Spine { get; } = spine; public SpineObject Spine { get; } = spine;

View File

@@ -100,140 +100,6 @@ namespace SpineViewer.Utils
} }
} }
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 SpineObject obj)
{
return new StandardValuesCollection(obj.SkinNames);
}
else if (context?.Instance is SpineObject[] 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 SpineObject obj)
{
return new StandardValuesCollection(obj.AnimationNames);
}
else if (context?.Instance is SpineObject[] 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 public class SFMLColorConverter : ExpandableObjectConverter
{ {
private class SFMLColorPropertyDescriptor : SimplePropertyDescriptor private class SFMLColorPropertyDescriptor : SimplePropertyDescriptor

View File

@@ -36,33 +36,33 @@ namespace SpineViewer.Utils
} }
} }
/// <summary> ///// <summary>
/// skel 文件路径编辑器 ///// skel 文件路径编辑器
/// </summary> ///// </summary>
public class SkelFileNameEditor : FileNameEditor //public class SkelFileNameEditor : FileNameEditor
{ //{
protected override void InitializeDialog(OpenFileDialog openFileDialog) // protected override void InitializeDialog(OpenFileDialog openFileDialog)
{ // {
base.InitializeDialog(openFileDialog); // base.InitializeDialog(openFileDialog);
openFileDialog.Title = "选择 skel 文件"; // openFileDialog.Title = "选择 skel 文件";
openFileDialog.AddExtension = false; // openFileDialog.AddExtension = false;
openFileDialog.Filter = "skel 文件 (*.skel; *.json)|*.skel;*.json|二进制文件 (*.skel)|*.skel|文本文件 (*.json)|*.json|所有文件 (*.*)|*.*"; // openFileDialog.Filter = "skel 文件 (*.skel; *.json)|*.skel;*.json|二进制文件 (*.skel)|*.skel|文本文件 (*.json)|*.json|所有文件 (*.*)|*.*";
} // }
} //}
/// <summary> ///// <summary>
/// atlas 文件路径编辑器 ///// atlas 文件路径编辑器
/// </summary> ///// </summary>
public class AtlasFileNameEditor : FileNameEditor //public class AtlasFileNameEditor : FileNameEditor
{ //{
protected override void InitializeDialog(OpenFileDialog openFileDialog) // protected override void InitializeDialog(OpenFileDialog openFileDialog)
{ // {
base.InitializeDialog(openFileDialog); // base.InitializeDialog(openFileDialog);
openFileDialog.Title = "选择 atlas 文件"; // openFileDialog.Title = "选择 atlas 文件";
openFileDialog.AddExtension = false; // openFileDialog.AddExtension = false;
openFileDialog.Filter = "atlas 文件 (*.atlas)|*.atlas|所有文件 (*.*)|*.*"; // openFileDialog.Filter = "atlas 文件 (*.atlas)|*.atlas|所有文件 (*.*)|*.*";
} // }
} //}
class SFMLColorEditor : UITypeEditor class SFMLColorEditor : UITypeEditor
{ {