Compare commits

..

10 Commits

Author SHA1 Message Date
ww-rm
580eaf990d 测试代码 2025-04-05 11:57:44 +08:00
ww-rm
5ab232a961 更新至v0.12.1 2025-04-05 11:56:53 +08:00
ww-rm
e596cd7ea4 update changelog 2025-04-05 11:56:13 +08:00
ww-rm
05c47a4daa 增加初始动画皮肤空位 2025-04-05 11:52:34 +08:00
ww-rm
5a8783b5f4 增加确定按钮 2025-04-05 11:18:54 +08:00
ww-rm
08bc171a72 修复分辨率调整时父容器尺寸获取错误bug 2025-04-05 10:38:17 +08:00
ww-rm
7372f5fe08 optimize 2025-04-05 10:31:30 +08:00
ww-rm
6f032bdd05 optimize 2025-04-05 10:10:04 +08:00
ww-rm
153d3603d2 optimize 2025-04-05 09:40:02 +08:00
ww-rm
95261e6907 optimize 2025-04-05 01:53:39 +08:00
21 changed files with 385 additions and 80 deletions

View File

@@ -1,5 +1,10 @@
# CHANGELOG
## v0.12.1
- 优化使用体验, 提供初始皮肤/动画空位
- 修复预览画面分辨率调整时父容器尺寸获取错误
## v0.12.0
- 支持皮肤列表 (仅 3.8.x 及以上支持)

View File

@@ -97,8 +97,8 @@ namespace SpineViewer.Controls
if (value.Width <= 0) value.Width = 100;
if (value.Height <= 0) value.Height = 100;
float parentX = Width;
float parentY = Height;
float parentX = panel.Parent.Width;
float parentY = panel.Parent.Height;
float sizeX = value.Width;
float sizeY = value.Height;

View File

@@ -33,6 +33,7 @@
flowLayoutPanel1 = new FlowLayoutPanel();
button_Add = new Button();
button_Delete = new Button();
button_Ok = new Button();
propertyGrid_AnimationTracks = new PropertyGrid();
panel.SuspendLayout();
tableLayoutPanel1.SuspendLayout();
@@ -69,6 +70,7 @@
flowLayoutPanel1.AutoSize = true;
flowLayoutPanel1.Controls.Add(button_Add);
flowLayoutPanel1.Controls.Add(button_Delete);
flowLayoutPanel1.Controls.Add(button_Ok);
flowLayoutPanel1.Dock = DockStyle.Fill;
flowLayoutPanel1.Location = new Point(3, 415);
flowLayoutPanel1.Name = "flowLayoutPanel1";
@@ -95,6 +97,16 @@
button_Delete.UseVisualStyleBackColor = true;
button_Delete.Click += button_Delete_Click;
//
// button_Ok
//
button_Ok.Location = new Point(239, 3);
button_Ok.Name = "button_Ok";
button_Ok.Size = new Size(112, 34);
button_Ok.TabIndex = 2;
button_Ok.Text = "确定";
button_Ok.UseVisualStyleBackColor = true;
button_Ok.Click += button_Ok_Click;
//
// propertyGrid_AnimationTracks
//
propertyGrid_AnimationTracks.Dock = DockStyle.Fill;
@@ -108,6 +120,7 @@
//
// AnimationTracksEditorDialog
//
AcceptButton = button_Ok;
AutoScaleDimensions = new SizeF(11F, 24F);
AutoScaleMode = AutoScaleMode.Font;
ClientSize = new Size(666, 483);
@@ -135,5 +148,6 @@
private Button button_Add;
private Button button_Delete;
private PropertyGrid propertyGrid_AnimationTracks;
private Button button_Ok;
}
}

View File

@@ -39,5 +39,10 @@ namespace SpineViewer.Dialogs
propertyGrid_AnimationTracks.Refresh();
propertyGrid_AnimationTracks.SelectedGridItem = propertyGrid_AnimationTracks.SelectedGridItem?.Parent?.GridItems?.Cast<GridItem>().Last();
}
private void button_Ok_Click(object sender, EventArgs e)
{
DialogResult = DialogResult.OK;
}
}
}

View File

@@ -33,6 +33,7 @@
flowLayoutPanel1 = new FlowLayoutPanel();
button_Add = new Button();
button_Delete = new Button();
button_Ok = new Button();
propertyGrid_SkinManager = new PropertyGrid();
panel.SuspendLayout();
tableLayoutPanel1.SuspendLayout();
@@ -69,6 +70,7 @@
flowLayoutPanel1.AutoSize = true;
flowLayoutPanel1.Controls.Add(button_Add);
flowLayoutPanel1.Controls.Add(button_Delete);
flowLayoutPanel1.Controls.Add(button_Ok);
flowLayoutPanel1.Dock = DockStyle.Fill;
flowLayoutPanel1.Location = new Point(3, 415);
flowLayoutPanel1.Name = "flowLayoutPanel1";
@@ -95,6 +97,16 @@
button_Delete.UseVisualStyleBackColor = true;
button_Delete.Click += button_Delete_Click;
//
// button_Ok
//
button_Ok.Location = new Point(239, 3);
button_Ok.Name = "button_Ok";
button_Ok.Size = new Size(112, 34);
button_Ok.TabIndex = 3;
button_Ok.Text = "确定";
button_Ok.UseVisualStyleBackColor = true;
button_Ok.Click += button_Ok_Click;
//
// propertyGrid_SkinManager
//
propertyGrid_SkinManager.Dock = DockStyle.Fill;
@@ -108,6 +120,7 @@
//
// SkinManagerEditorDialog
//
AcceptButton = button_Ok;
AutoScaleDimensions = new SizeF(11F, 24F);
AutoScaleMode = AutoScaleMode.Font;
ClientSize = new Size(666, 483);
@@ -135,5 +148,6 @@
private Button button_Add;
private Button button_Delete;
private PropertyGrid propertyGrid_SkinManager;
private Button button_Ok;
}
}

View File

@@ -41,5 +41,10 @@ namespace SpineViewer.Dialogs
if (propertyGrid_SkinManager.SelectedGridItem?.Parent?.GridItems?.Cast<GridItem>().Last() is GridItem gt)
propertyGrid_SkinManager.SelectedGridItem = gt;
}
private void button_Ok_Click(object sender, EventArgs e)
{
DialogResult = DialogResult.OK;
}
}
}

View File

@@ -14,6 +14,11 @@ namespace SpineViewer.Exporter
/// </summary>
public abstract class Exporter(ExportArgs exportArgs) : ImplementationResolver<Exporter, ExportImplementationAttribute, ExportType>
{
/// <summary>
/// 仅源像素混合模式
/// </summary>
private static readonly SFML.Graphics.BlendMode SrcOnlyBlendMode = new(SFML.Graphics.BlendMode.Factor.One, SFML.Graphics.BlendMode.Factor.Zero);
/// <summary>
/// 创建指定类型导出器
/// </summary>
@@ -83,7 +88,7 @@ namespace SpineViewer.Exporter
// 混合模式用直接覆盖的方式, 保证得到的图像区域是反预乘的颜色和透明度, 同时使用反预乘着色器
var st = SFML.Graphics.RenderStates.Default;
st.BlendMode = new(SFML.Graphics.BlendMode.Factor.One, SFML.Graphics.BlendMode.Factor.Zero); // 用源的颜色和透明度直接覆盖
st.BlendMode = SrcOnlyBlendMode; // 用源的颜色和透明度直接覆盖
st.Shader = Shader.InversePma;
// 在最终结果上二次渲染非预乘画面

50
SpineViewer/PetForm.Designer.cs generated Normal file
View File

@@ -0,0 +1,50 @@
namespace SpineViewer
{
partial class PetForm
{
/// <summary>
/// Required designer variable.
/// </summary>
private System.ComponentModel.IContainer components = null;
/// <summary>
/// Clean up any resources being used.
/// </summary>
/// <param name="disposing">true if managed resources should be disposed; otherwise, false.</param>
protected override void Dispose(bool disposing)
{
if (disposing && (components != null))
{
components.Dispose();
}
base.Dispose(disposing);
}
#region Windows Form Designer generated code
/// <summary>
/// Required method for Designer support - do not modify
/// the contents of this method with the code editor.
/// </summary>
private void InitializeComponent()
{
SuspendLayout();
//
// PetForm
//
AutoScaleMode = AutoScaleMode.None;
ClientSize = new Size(490, 456);
ControlBox = false;
MaximizeBox = false;
MinimizeBox = false;
Name = "PetForm";
ShowIcon = false;
ShowInTaskbar = false;
StartPosition = FormStartPosition.Manual;
Text = "PetForm";
ResumeLayout(false);
}
#endregion
}
}

48
SpineViewer/PetForm.cs Normal file
View File

@@ -0,0 +1,48 @@
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms;
namespace SpineViewer
{
public partial class PetForm: Form
{
public PetForm()
{
InitializeComponent();
}
protected override CreateParams CreateParams
{
get
{
//var style = Win32.GetWindowLong(hWnd, Win32.GWL_STYLE) | Win32.WS_POPUP;
//var exStyle = Win32.GetWindowLong(hWnd, Win32.GWL_EXSTYLE) | Win32.WS_EX_LAYERED | Win32.WS_EX_TOOLWINDOW | Win32.WS_EX_TOPMOST;
//Win32.SetWindowLong(hWnd, Win32.GWL_STYLE, style);
//Win32.SetWindowLong(hWnd, Win32.GWL_EXSTYLE, exStyle);
//Win32.SetLayeredWindowAttributes(hWnd, crKey, 255, Win32.LWA_COLORKEY | Win32.LWA_ALPHA);
//Win32.SetWindowPos(hWnd, Win32.HWND_TOPMOST, 0, 0, 0, 0, Win32.SWP_NOMOVE | Win32.SWP_NOSIZE);
var cp = base.CreateParams;
cp.ExStyle = Win32.WS_EX_LAYERED | Win32.WS_EX_TOPMOST;
cp.Style = Win32.WS_POPUP;
//cp.ExStyle |= Win32.WS_EX_LAYERED | Win32.WS_EX_TOOLWINDOW | Win32.WS_EX_TOPMOST;
return cp;
}
}
protected override void OnPaint(PaintEventArgs e)
{
;
}
protected override void OnPaintBackground(PaintEventArgs e)
{
;
}
}
}

120
SpineViewer/PetForm.resx Normal file
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

@@ -46,14 +46,12 @@ namespace SpineViewer.Spine
/// <summary>
/// 轨道属性描述符, 实现对属性的读取和赋值
/// </summary>
/// <param name="spine">关联的 Spine 对象</param>
/// <param name="i">轨道索引</param>
public class TrackWrapperPropertyDescriptor(Spine spine, int i) : PropertyDescriptor($"Track{i}", [new DisplayNameAttribute($"轨道 {i}")])
public class TrackWrapperPropertyDescriptor(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 Type ComponentType => typeof(AnimationTracks);
public override bool IsReadOnly => false;
public override Type PropertyType => typeof(TrackWrapper);
public override bool CanResetValue(object component) => false;
@@ -63,14 +61,23 @@ namespace SpineViewer.Spine
/// <summary>
/// 得到一个轨道包装类, 允许用户查看或者修改具体的属性值, 这个地方决定了在面板上看到的是一个对象及其属性
/// </summary>
public override object? GetValue(object? component) => new TrackWrapper(spine, idx);
public override object? GetValue(object? component)
{
if (component is AnimationTracks tracks)
return tracks.GetTrackWrapper(idx);
return null;
}
/// <summary>
/// 允许通过字符串赋值修改该轨道的动画, 这里决定了当其他地方的调用 (比如 Converter) 通过 value 来设置属性值的时候应该怎么处理
/// </summary>
public override void SetValue(object? component, object? value)
{
if (value is string s) spine.SetAnimation(idx, s);
if (component is AnimationTracks tracks)
{
if (value is string s)
tracks.Spine.SetAnimation(idx, s); // tracks.SetTrackWrapper(idx, s);
}
}
}
@@ -78,10 +85,12 @@ namespace SpineViewer.Spine
/// AnimationTracks 动态类型包装类, 用于提供对 Spine 对象多轨道动画的访问能力, 不同轨道将动态生成属性
/// </summary>
/// <param name="spine">关联的 Spine 对象</param>
public class AnimationTracksType(Spine spine) : ICustomTypeDescriptor
public class AnimationTracks(Spine spine) : ICustomTypeDescriptor
{
private readonly Dictionary<int, TrackWrapperPropertyDescriptor> pdCache = [];
private static readonly Dictionary<int, TrackWrapperPropertyDescriptor> pdCache = [];
public Spine Spine { get; } = spine;
private readonly Dictionary<int, TrackWrapper> trackWrapperProperties = [];
// XXX: 必须实现 ICustomTypeDescriptor 接口, 不能继承 CustomTypeDescriptor, 似乎继承下来的东西会有问题, 导致某些调用不正确
@@ -102,12 +111,22 @@ namespace SpineViewer.Spine
foreach (var i in Spine.GetTrackIndices())
{
if (!pdCache.ContainsKey(i))
pdCache[i] = new TrackWrapperPropertyDescriptor(Spine, i);
pdCache[i] = new TrackWrapperPropertyDescriptor(i);
props.Add(pdCache[i]);
}
return new PropertyDescriptorCollection(props.ToArray());
}
/// <summary>
/// 访问 TrackWrapper 属性 <c>AnimationTracks.Track{i}</c>
/// </summary>
public TrackWrapper GetTrackWrapper(int i)
{
if (!trackWrapperProperties.ContainsKey(i))
trackWrapperProperties[i] = new TrackWrapper(Spine, i);
return trackWrapperProperties[i];
}
/// <summary>
/// 在属性面板悬停可以按轨道顺序显示动画名称
/// </summary>
@@ -115,10 +134,10 @@ namespace SpineViewer.Spine
public override bool Equals(object? obj)
{
if (obj is AnimationTracksType tracks) return ToString() == tracks.ToString();
if (obj is AnimationTracks tracks) return ToString() == tracks.ToString();
return base.Equals(obj);
}
public override int GetHashCode() => (typeof(AnimationTracksType).FullName + ToString()).GetHashCode();
public override int GetHashCode() => (typeof(AnimationTracks).FullName + ToString()).GetHashCode();
}
}

View File

@@ -111,15 +111,14 @@ namespace SpineViewer.Spine.Implementations.Spine
var fY = flipY;
var animations = animationState.Tracks.Where(te => te is not null).Select(te => te.Animation.Name).ToArray();
var val = Math.Max(value, SCALE_MIN);
if (skeletonBinary is not null)
{
skeletonBinary.Scale = val;
skeletonBinary.Scale = value;
skeletonData = skeletonBinary.ReadSkeletonData(SkelPath);
}
else if (skeletonJson is not null)
{
skeletonJson.Scale = val;
skeletonJson.Scale = value;
skeletonData = skeletonJson.ReadSkeletonData(SkelPath);
}

View File

@@ -110,15 +110,14 @@ namespace SpineViewer.Spine.Implementations.Spine
var fY = flipY;
var animations = animationState.Tracks.Where(te => te is not null).Select(te => te.Animation.Name).ToArray();
var val = Math.Max(value, SCALE_MIN);
if (skeletonBinary is not null)
{
skeletonBinary.Scale = val;
skeletonBinary.Scale = value;
skeletonData = skeletonBinary.ReadSkeletonData(SkelPath);
}
else if (skeletonJson is not null)
{
skeletonJson.Scale = val;
skeletonJson.Scale = value;
skeletonData = skeletonJson.ReadSkeletonData(SkelPath);
}

View File

@@ -137,9 +137,11 @@ namespace SpineViewer.Spine.Implementations.Spine
protected override void addSkin(string name)
{
if (!skinNames.Contains(name)) return;
skeleton.Skin.AddSkin(skeletonData.FindSkin(name));
skeleton.SetSlotsToSetupPose();
if (skeletonData.FindSkin(name) is Skin sk)
{
skeleton.Skin.AddSkin(sk);
skeleton.SetSlotsToSetupPose();
}
}
protected override void clearSkin()

View File

@@ -133,9 +133,11 @@ namespace SpineViewer.Spine.Implementations.Spine
protected override void addSkin(string name)
{
if (!skinNames.Contains(name)) return;
skeleton.Skin.AddSkin(skeletonData.FindSkin(name));
skeleton.SetSlotsToSetupPose();
if (skeletonData.FindSkin(name) is Skin sk)
{
skeleton.Skin.AddSkin(sk);
skeleton.SetSlotsToSetupPose();
}
}
protected override void clearSkin()

View File

@@ -133,9 +133,11 @@ namespace SpineViewer.Spine.Implementations.Spine
protected override void addSkin(string name)
{
if (!skinNames.Contains(name)) return;
skeleton.Skin.AddSkin(skeletonData.FindSkin(name));
skeleton.SetSlotsToSetupPose();
if (skeletonData.FindSkin(name) is Skin sk)
{
skeleton.Skin.AddSkin(sk);
skeleton.SetSlotsToSetupPose();
}
}
protected override void clearSkin()

View File

@@ -133,9 +133,11 @@ namespace SpineViewer.Spine.Implementations.Spine
protected override void addSkin(string name)
{
if (!skinNames.Contains(name)) return;
skeleton.Skin.AddSkin(skeletonData.FindSkin(name));
skeleton.SetSlotsToSetupPose();
if (skeletonData.FindSkin(name) is Skin sk)
{
skeleton.Skin.AddSkin(sk);
skeleton.SetSlotsToSetupPose();
}
}
protected override void clearSkin()

View File

@@ -39,9 +39,8 @@ namespace SpineViewer.Spine
/// 皮肤属性描述符, 实现对属性的读取和赋值
/// </summary>
/// <param name="spine">关联的 Spine 对象</param>
public class SkinWrapperPropertyDescriptor(Spine spine, int i) : PropertyDescriptor($"Skin{i}", [new DisplayNameAttribute($"皮肤 {i}")])
public class SkinWrapperPropertyDescriptor(int i) : PropertyDescriptor($"Skin{i}", [new DisplayNameAttribute($"皮肤 {i}")])
{
private readonly Spine spine = spine;
private readonly int idx = i;
public override Type ComponentType => typeof(SkinManager);
@@ -54,14 +53,23 @@ namespace SpineViewer.Spine
/// <summary>
/// 得到一个 SkinWrapper, 允许用户查看或者修改具体的属性值, 这个地方决定了在面板上看到的是一个对象及其属性
/// </summary>
public override object? GetValue(object? component) => new SkinWrapper(spine, idx);
public override object? GetValue(object? component)
{
if (component is SkinManager manager)
return manager.GetSkinWrapper(idx);
return null;
}
/// <summary>
/// 允许通过字符串赋值修改该位置的皮肤
/// </summary>
public override void SetValue(object? component, object? value)
{
if (value is string s) spine.ReplaceSkin(idx, s);
if (component is SkinManager manager)
{
if (value is string s)
manager.Spine.ReplaceSkin(idx, s); // manager.SetSkinWrapper(idx, s);
}
}
}
@@ -71,8 +79,10 @@ namespace SpineViewer.Spine
/// <param name="spine">关联的 Spine 对象</param>
public class SkinManager(Spine spine) : ICustomTypeDescriptor
{
private readonly Dictionary<int, SkinWrapperPropertyDescriptor> pdCache = [];
private static readonly Dictionary<int, SkinWrapperPropertyDescriptor> pdCache = [];
public Spine Spine { get; } = spine;
private readonly Dictionary<int, SkinWrapper> skinWrapperProperties = [];
// XXX: 必须实现 ICustomTypeDescriptor 接口, 不能继承 CustomTypeDescriptor, 似乎继承下来的东西会有问题, 导致某些调用不正确
@@ -93,12 +103,22 @@ namespace SpineViewer.Spine
for (var i = 0; i < Spine.GetLoadedSkins().Length; i++)
{
if (!pdCache.ContainsKey(i))
pdCache[i] = new SkinWrapperPropertyDescriptor(Spine, i);
pdCache[i] = new SkinWrapperPropertyDescriptor(i);
props.Add(pdCache[i]);
}
return new PropertyDescriptorCollection(props.ToArray());
}
/// <summary>
/// 访问 SkinWrapper 属性 <c>SkinManager.Skin{i}</c>
/// </summary>
public SkinWrapper GetSkinWrapper(int i)
{
if (!skinWrapperProperties.ContainsKey(i))
skinWrapperProperties[i] = new SkinWrapper(Spine, i);
return skinWrapperProperties[i];
}
/// <summary>
/// 在属性面板悬停可以显示已加载的皮肤列表
/// </summary>

View File

@@ -4,6 +4,7 @@ using System.ComponentModel;
using System.Reflection;
using System.Drawing.Design;
using NLog;
using System.Xml.Linq;
namespace SpineViewer.Spine
{
@@ -30,11 +31,6 @@ namespace SpineViewer.Spine
/// </summary>
protected const uint PREVIEW_HEIGHT = 256;
/// <summary>
/// 缩放最小值
/// </summary>
protected const float SCALE_MIN = 0.001f;
/// <summary>
/// 创建特定版本的 Spine
/// </summary>
@@ -88,8 +84,13 @@ namespace SpineViewer.Spine
tex.Display();
Preview = tex.Texture.CopyToBitmap();
// 取最后一个作为初始, 尽可能去显示非默认的内容
setAnimation(0, AnimationNames.Last());
// 默认初始化10个空位
for (int i = 0; i < 10; i++)
{
setAnimation(i, AnimationNames.First());
loadedSkins.Add(SkinNames.First());
}
reloadSkins();
return this;
}
@@ -174,7 +175,7 @@ namespace SpineViewer.Spine
public float Scale
{
get { lock (_lock) return scale; }
set { lock (_lock) { scale = value; update(0); } }
set { lock (_lock) { scale = Math.Max(value, 0.001f); update(0); } }
}
protected abstract float scale { get; set; }
@@ -227,8 +228,7 @@ namespace SpineViewer.Spine
/// <summary>
/// 默认轨道动画名称, 如果设置的动画不存在则忽略
/// </summary>
[TypeConverter(typeof(AnimationConverter))]
[Category("[3] "), DisplayName(" 0 ")]
[Browsable(false)]
public string Track0Animation
{
get { lock (_lock) return getAnimation(0); }
@@ -236,10 +236,10 @@ namespace SpineViewer.Spine
}
/// <summary>
/// 默认轨道动画时长
/// 轨道动画最大时长
/// </summary>
[Category("[3] "), DisplayName(" 0 ")]
public float Track0AnimationDuration => GetAnimationDuration(Track0Animation);
[Category("[3] "), DisplayName("")]
public float AnimationTracksMaxDuration { get { lock (_lock) return getTrackIndices().Select(i => GetAnimationDuration(getAnimation(i))).Max(); } }
/// <summary>
/// 默认轨道动画时长
@@ -247,7 +247,7 @@ namespace SpineViewer.Spine
[Editor(typeof(AnimationTracksEditor), typeof(UITypeEditor))]
[TypeConverter(typeof(ExpandableObjectConverter))]
[Category("[3] "), DisplayName("")]
public AnimationTracksType AnimationTracks { get; private set; }
public AnimationTracks AnimationTracks { get; private set; }
/// <summary>
/// 包含的所有皮肤名称
@@ -261,24 +261,22 @@ namespace SpineViewer.Spine
/// </summary>
public string[] GetLoadedSkins() { lock (_lock) return loadedSkins.ToArray(); }
protected readonly List<string> loadedSkins = [];
/// <summary>
/// 加载指定皮肤, 添加至列表末尾, 如果不存在则忽略, 允许加载重复的值
/// </summary>
public void LoadSkin(string name)
public void LoadSkin(string name)
{
if (!skinNames.Contains(name)) return;
lock (_lock)
{
if (skinNames.Contains(name))
{
loadedSkins.Add(name);
reloadSkins();
loadedSkins.Add(name);
reloadSkins();
if (!skinLoggerWarned && Version <= SpineVersion.V37 && loadedSkins.Count > 1)
{
logger.Warn($"Multiplt skins not supported in SpineVersion {Version.GetName()}");
skinLoggerWarned = true;
}
if (!skinLoggerWarned && Version <= SpineVersion.V37 && loadedSkins.Count > 1)
{
logger.Warn($"Multiplt skins not supported in SpineVersion {Version.GetName()}");
skinLoggerWarned = true;
}
}
}
@@ -288,13 +286,11 @@ namespace SpineViewer.Spine
/// </summary>
public void UnloadSkin(int idx)
{
if (idx < 0 || idx >= loadedSkins.Count) return;
lock (_lock)
{
if (idx >= 0 && idx < loadedSkins.Count)
{
loadedSkins.RemoveAt(idx);
reloadSkins();
}
loadedSkins.RemoveAt(idx);
reloadSkins();
}
}
@@ -303,13 +299,11 @@ namespace SpineViewer.Spine
/// </summary>
public void ReplaceSkin(int idx, string name)
{
if (idx < 0 || idx >= loadedSkins.Count || !skinNames.Contains(name)) return;
lock (_lock)
{
if (idx >= 0 && idx < loadedSkins.Count && skinNames.Contains(name))
{
loadedSkins[idx] = name;
reloadSkins();
}
loadedSkins[idx] = name;
reloadSkins();
}
}
@@ -325,7 +319,7 @@ namespace SpineViewer.Spine
}
/// <summary>
/// 加载皮肤, 之后需要使用 <see cref="setSlotsToSetupPose"/> 来复位
/// 加载皮肤, 如果不存在则忽略
/// </summary>
protected abstract void addSkin(string name);

View File

@@ -89,14 +89,14 @@ namespace SpineViewer.Spine
public override StandardValuesCollection? GetStandardValues(ITypeDescriptorContext? context)
{
if (context.Instance is AnimationTracksType animTrack)
if (context.Instance is AnimationTracks tracks)
{
return new StandardValuesCollection(animTrack.Spine.AnimationNames);
return new StandardValuesCollection(tracks.Spine.AnimationNames);
}
else if (context.Instance is object[] instances && instances.All(x => x is AnimationTracksType))
else if (context.Instance is object[] instances && instances.All(x => x is AnimationTracks))
{
// XXX: 这里不知道为啥总是会得到 object[] 类型而不是具体的 AnimationTracksType[] 类型
var animTracks = instances.Cast<AnimationTracksType>().ToArray();
// XXX: 这里不知道为啥总是会得到 object[] 类型而不是具体的类型
var animTracks = instances.Cast<AnimationTracks>().ToArray();
if (animTracks.Length > 0)
{
IEnumerable<string> common = animTracks[0].Spine.AnimationNames;

View File

@@ -7,7 +7,7 @@
<TargetFramework>net8.0-windows</TargetFramework>
<BaseOutputPath>$(SolutionDir)out</BaseOutputPath>
<IncludeSourceRevisionInInformationalVersion>false</IncludeSourceRevisionInInformationalVersion>
<Version>0.12.0</Version>
<Version>0.12.1</Version>
<OutputType>WinExe</OutputType>
<UseWindowsForms>true</UseWindowsForms>
<ApplicationIcon>appicon.ico</ApplicationIcon>