diff --git a/Spine/Implementations/SpineWrappers/V34/Animation34.cs b/Spine/Implementations/SpineWrappers/V34/Animation34.cs new file mode 100644 index 0000000..99e97ab --- /dev/null +++ b/Spine/Implementations/SpineWrappers/V34/Animation34.cs @@ -0,0 +1,23 @@ +using Spine.SpineWrappers; +using SpineRuntime34; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Spine.Implementations.SpineWrappers.V34 +{ + internal sealed class Animation34(Animation innerObject) : IAnimation + { + private readonly Animation _o = innerObject; + + public Animation InnerObject => _o; + + public string Name => _o.Name; + + public float Duration => _o.Duration; + + public override string ToString() => _o.ToString(); + } +} diff --git a/Spine/Implementations/SpineWrappers/V34/AnimationState34.cs b/Spine/Implementations/SpineWrappers/V34/AnimationState34.cs new file mode 100644 index 0000000..b9f86c6 --- /dev/null +++ b/Spine/Implementations/SpineWrappers/V34/AnimationState34.cs @@ -0,0 +1,180 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Spine.SpineWrappers; +using SpineRuntime34; + +namespace Spine.Implementations.SpineWrappers.V34 +{ + internal sealed class AnimationState34(AnimationState innerObject, SpineObjectData34 data) : IAnimationState + { + private readonly AnimationState _o = innerObject; + private readonly SpineObjectData34 _data = data; + + private readonly Dictionary _trackEntryPool = []; + + private readonly Dictionary _eventMapping = []; + private readonly Dictionary _eventCount = []; + + public AnimationState InnerObject => _o; + +#pragma warning disable CS0067 + + // NOTE: 3.4 以下没有这两个事件 + public event IAnimationState.TrackEntryDelegate? Interrupt; + public event IAnimationState.TrackEntryDelegate? Dispose; + +#pragma warning restore CS0067 + + + public event IAnimationState.TrackEntryDelegate? Start + { + add + { + if (value is null) return; + if (!_eventMapping.TryGetValue(value, out var f)) + { + _eventMapping[value] = f = (TrackEntry t) => value(GetTrackEntry(t)); + _eventCount[value] = 0; + } + _o.Start += f; + _eventCount[value]++; + } + remove + { + if (value is null) return; + if (_eventMapping.TryGetValue(value, out var f)) + { + _o.Start -= f; + _eventCount[value]--; + if (_eventCount[value] <= 0) + { + _eventMapping.Remove(value); + _eventCount.Remove(value); + } + } + } + } + + public event IAnimationState.TrackEntryDelegate? End + { + add + { + if (value is null) return; + if (!_eventMapping.TryGetValue(value, out var f)) + { + _eventMapping[value] = f = (TrackEntry t) => value(GetTrackEntry(t)); + _eventCount[value] = 0; + } + _o.End += f; + _eventCount[value]++; + } + remove + { + if (value is null) return; + if (_eventMapping.TryGetValue(value, out var f)) + { + _o.End -= f; + _eventCount[value]--; + if (_eventCount[value] <= 0) + { + _eventMapping.Remove(value); + _eventCount.Remove(value); + } + } + } + } + + public event IAnimationState.TrackEntryDelegate? Complete + { + add + { + if (value is null) return; + if (!_eventMapping.TryGetValue(value, out var f)) + { + _eventMapping[value] = f = (TrackEntry t) => value(GetTrackEntry(t)); + _eventCount[value] = 0; + } + _o.Complete += f; + _eventCount[value]++; + } + remove + { + if (value is null) return; + if (_eventMapping.TryGetValue(value, out var f)) + { + _o.Complete -= f; + _eventCount[value]--; + if (_eventCount[value] <= 0) + { + _eventMapping.Remove(value); + _eventCount.Remove(value); + } + } + } + } + + public float TimeScale { get => _o.TimeScale; set => _o.TimeScale = value; } + + public void Update(float delta) => _o.Update(delta); + + public void Apply(ISkeleton skeleton) + { + if (skeleton is Skeleton34 skel) + { + _o.Apply(skel.InnerObject); + return; + } + throw new ArgumentException($"Received {skeleton.GetType().Name}", nameof(skeleton)); + } + + /// + /// 获取 对象, 不存在则创建 + /// + public ITrackEntry GetTrackEntry(TrackEntry trackEntry) + { + if (!_trackEntryPool.TryGetValue(trackEntry, out var tr)) + _trackEntryPool[trackEntry] = tr = new(trackEntry, this, _data); + return tr; + } + + public IEnumerable IterTracks() => _o.Tracks.Select(t => t is null ? null : GetTrackEntry(t)); + + public ITrackEntry? GetCurrent(int index) { var t = _o.GetCurrent(index); return t is null ? null : GetTrackEntry(t); } + + public void ClearTrack(int index) => _o.ClearTrack(index); + + public void ClearTracks() => _o.ClearTracks(); + + public ITrackEntry SetAnimation(int trackIndex, string animationName, bool loop) + => GetTrackEntry(_o.SetAnimation(trackIndex, animationName, loop)); + + public ITrackEntry SetAnimation(int trackIndex, IAnimation animation, bool loop) + { + if (animation is Animation34 anime) + return GetTrackEntry(_o.SetAnimation(trackIndex, anime.InnerObject, loop)); + throw new ArgumentException($"Received {animation.GetType().Name}", nameof(animation)); + } + + public ITrackEntry SetEmptyAnimation(int trackIndex, float mixDuration) => GetTrackEntry(_o.SetEmptyAnimation(trackIndex, mixDuration)); + + public void SetEmptyAnimations(float mixDuration) => _o.SetEmptyAnimations(mixDuration); + + public ITrackEntry AddAnimation(int trackIndex, string animationName, bool loop, float delay) + => GetTrackEntry(_o.AddAnimation(trackIndex, animationName, loop, delay)); + + public ITrackEntry AddAnimation(int trackIndex, IAnimation animation, bool loop, float delay) + { + if (animation is Animation34 anime) + return GetTrackEntry(_o.AddAnimation(trackIndex, anime.InnerObject, loop, delay)); + throw new ArgumentException($"Received {animation.GetType().Name}", nameof(animation)); + } + + public ITrackEntry AddEmptyAnimation(int trackIndex, float mixDuration, float delay) + => GetTrackEntry(_o.AddEmptyAnimation(trackIndex, mixDuration, delay)); + + public override string ToString() => _o.ToString(); + } +} diff --git a/Spine/Implementations/SpineWrappers/V34/Attachments/Attachment34.cs b/Spine/Implementations/SpineWrappers/V34/Attachments/Attachment34.cs new file mode 100644 index 0000000..a5664df --- /dev/null +++ b/Spine/Implementations/SpineWrappers/V34/Attachments/Attachment34.cs @@ -0,0 +1,24 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Spine.SpineWrappers; +using Spine.SpineWrappers.Attachments; +using SpineRuntime34; + +namespace Spine.Implementations.SpineWrappers.V34.Attachments +{ + internal abstract class Attachment34(Attachment innerObject) : IAttachment + { + private readonly Attachment _o = innerObject; + + public virtual Attachment InnerObject => _o; + + public string Name => _o.Name; + + public abstract int ComputeWorldVertices(ISlot slot, ref float[] worldVertices); + + public override string ToString() => _o.ToString(); + } +} diff --git a/Spine/Implementations/SpineWrappers/V34/Attachments/BoundingBoxAttachment34.cs b/Spine/Implementations/SpineWrappers/V34/Attachments/BoundingBoxAttachment34.cs new file mode 100644 index 0000000..74aeea5 --- /dev/null +++ b/Spine/Implementations/SpineWrappers/V34/Attachments/BoundingBoxAttachment34.cs @@ -0,0 +1,31 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Spine.SpineWrappers.Attachments; +using SpineRuntime34; + +namespace Spine.Implementations.SpineWrappers.V34.Attachments +{ + internal sealed class BoundingBoxAttachment34(BoundingBoxAttachment innerObject) : + Attachment34(innerObject), + IBoundingBoxAttachment + { + private readonly BoundingBoxAttachment _o = innerObject; + + public override BoundingBoxAttachment InnerObject => _o; + + public override int ComputeWorldVertices(Spine.SpineWrappers.ISlot slot, ref float[] worldVertices) + { + if (slot is Slot34 st) + { + var length = _o.WorldVerticesLength; + if (worldVertices.Length < length) worldVertices = new float[length]; + _o.ComputeWorldVertices(st.InnerObject, worldVertices); + return length; + } + throw new ArgumentException($"Invalid slot type. Expected {nameof(Slot34)}, but received {slot.GetType().Name}", nameof(slot)); + } + } +} diff --git a/Spine/Implementations/SpineWrappers/V34/Attachments/MeshAttachment34.cs b/Spine/Implementations/SpineWrappers/V34/Attachments/MeshAttachment34.cs new file mode 100644 index 0000000..a16733b --- /dev/null +++ b/Spine/Implementations/SpineWrappers/V34/Attachments/MeshAttachment34.cs @@ -0,0 +1,44 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Spine.SpineWrappers.Attachments; +using SpineRuntime34; + +namespace Spine.Implementations.SpineWrappers.V34.Attachments +{ + internal sealed class MeshAttachment34(MeshAttachment innerObject) : + Attachment34(innerObject), + IMeshAttachment + { + private readonly MeshAttachment _o = innerObject; + + public override MeshAttachment InnerObject => _o; + + public override int ComputeWorldVertices(Spine.SpineWrappers.ISlot slot, ref float[] worldVertices) + { + if (slot is Slot34 st) + { + var length = _o.WorldVerticesLength; + if (worldVertices.Length < length) worldVertices = new float[length]; + _o.ComputeWorldVertices(st.InnerObject, worldVertices); + return length; + } + throw new ArgumentException($"Invalid slot type. Expected {nameof(Slot34)}, but received {slot.GetType().Name}", nameof(slot)); + } + + public float R { get => _o.R; set => _o.R = value; } + public float G { get => _o.G; set => _o.G = value; } + public float B { get => _o.B; set => _o.B = value; } + public float A { get => _o.A; set => _o.A = value; } + + public SFML.Graphics.Texture RendererObject => (SFML.Graphics.Texture)((AtlasRegion)_o.RendererObject).page.rendererObject; + + public float[] UVs => _o.UVs; + + public int[] Triangles => _o.Triangles; + + public int HullLength => _o.HullLength; + } +} diff --git a/Spine/Implementations/SpineWrappers/V34/Attachments/PathAttachment34.cs b/Spine/Implementations/SpineWrappers/V34/Attachments/PathAttachment34.cs new file mode 100644 index 0000000..df3fd69 --- /dev/null +++ b/Spine/Implementations/SpineWrappers/V34/Attachments/PathAttachment34.cs @@ -0,0 +1,31 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Spine.SpineWrappers.Attachments; +using SpineRuntime34; + +namespace Spine.Implementations.SpineWrappers.V34.Attachments +{ + internal sealed class PathAttachment34(PathAttachment innerObject) : + Attachment34(innerObject), + IPathAttachment + { + private readonly PathAttachment _o = innerObject; + + public override PathAttachment InnerObject => _o; + + public override int ComputeWorldVertices(Spine.SpineWrappers.ISlot slot, ref float[] worldVertices) + { + if (slot is Slot34 st) + { + var length = _o.WorldVerticesLength; + if (worldVertices.Length < length) worldVertices = new float[length]; + _o.ComputeWorldVertices(st.InnerObject, worldVertices); + return length; + } + throw new ArgumentException($"Invalid slot type. Expected {nameof(Slot34)}, but received {slot.GetType().Name}", nameof(slot)); + } + } +} \ No newline at end of file diff --git a/Spine/Implementations/SpineWrappers/V34/Attachments/RegionAttachment34.cs b/Spine/Implementations/SpineWrappers/V34/Attachments/RegionAttachment34.cs new file mode 100644 index 0000000..5f1a6f0 --- /dev/null +++ b/Spine/Implementations/SpineWrappers/V34/Attachments/RegionAttachment34.cs @@ -0,0 +1,39 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Spine.SpineWrappers.Attachments; +using SpineRuntime34; + +namespace Spine.Implementations.SpineWrappers.V34.Attachments +{ + internal sealed class RegionAttachment34(RegionAttachment innerObject) : + Attachment34(innerObject), + IRegionAttachment + { + private readonly RegionAttachment _o = innerObject; + + public override RegionAttachment InnerObject => _o; + + public override int ComputeWorldVertices(Spine.SpineWrappers.ISlot slot, ref float[] worldVertices) + { + if (slot is Slot34 st) + { + if (worldVertices.Length < 8) worldVertices = new float[8]; + _o.ComputeWorldVertices(st.InnerObject.Bone, worldVertices); + return 8; + } + throw new ArgumentException($"Invalid slot type. Expected {nameof(Slot34)}, but received {slot.GetType().Name}", nameof(slot)); + } + + public float R { get => _o.R; set => _o.R = value; } + public float G { get => _o.G; set => _o.G = value; } + public float B { get => _o.B; set => _o.B = value; } + public float A { get => _o.A; set => _o.A = value; } + + public SFML.Graphics.Texture RendererObject => (SFML.Graphics.Texture)((AtlasRegion)_o.RendererObject).page.rendererObject; + + public float[] UVs => _o.UVs; + } +} diff --git a/Spine/Implementations/SpineWrappers/V34/Bone34.cs b/Spine/Implementations/SpineWrappers/V34/Bone34.cs new file mode 100644 index 0000000..dff74e7 --- /dev/null +++ b/Spine/Implementations/SpineWrappers/V34/Bone34.cs @@ -0,0 +1,33 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Spine.SpineWrappers; +using SpineRuntime34; + +namespace Spine.Implementations.SpineWrappers.V34 +{ + internal sealed class Bone34(Bone innerObject, Bone34? parent = null) : IBone + { + private readonly Bone _o = innerObject; + private readonly Bone34? _parent = parent; + + public Bone InnerObject => _o; + + public string Name => _o.Data.Name; + public int Index => _o.Data.Index; + + public IBone? Parent => _parent; + public bool Active => true; // NOTE: 3.7 及以下没有 Active 属性, 此处总是返回 true + public float Length => _o.Data.Length; + public float WorldX => _o.WorldX; + public float WorldY => _o.WorldY; + public float A => _o.A; + public float B => _o.B; + public float C => _o.C; + public float D => _o.D; + + public override string ToString() => _o.ToString(); + } +} diff --git a/Spine/Implementations/SpineWrappers/V34/Skeleton34.cs b/Spine/Implementations/SpineWrappers/V34/Skeleton34.cs new file mode 100644 index 0000000..fee3d5e --- /dev/null +++ b/Spine/Implementations/SpineWrappers/V34/Skeleton34.cs @@ -0,0 +1,106 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using System.Collections.Frozen; +using System.Collections.Immutable; +using Spine.SpineWrappers; +using SpineRuntime34; + +namespace Spine.Implementations.SpineWrappers.V34 +{ + internal sealed class Skeleton34 : ISkeleton + { + private readonly Skeleton _o; + private readonly SpineObjectData34 _data; + + private readonly ImmutableArray _bones; + private readonly FrozenDictionary _bonesByName; + private readonly ImmutableArray _slots; + private readonly FrozenDictionary _slotsByName; + + private Skin34? _skin; + + public Skeleton34(Skeleton innerObject, SpineObjectData34 data) + { + _o = innerObject; + _data = data; + + List bones = []; + Dictionary bonesByName = []; + foreach (var b in _o.Bones) + { + var bone = new Bone34(b, b.Parent is null ? null : bones[b.Parent.Data.Index]); + bones.Add(bone); + bonesByName[bone.Name] = bone; + } + _bones = bones.Cast().ToImmutableArray(); + _bonesByName = bonesByName.ToFrozenDictionary(); + + List slots = []; + Dictionary slotsByName = []; + foreach (var s in _o.Slots) + { + var slot = new Slot34(s, _data, bones[s.Bone.Data.Index]); + slots.Add(slot); + slotsByName[slot.Name] = slot; + } + _slots = slots.Cast().ToImmutableArray(); + _slotsByName = slotsByName.ToFrozenDictionary(); + } + + public Skeleton InnerObject => _o; + + public float R { get => _o.R; set => _o.R = value; } + public float G { get => _o.G; set => _o.G = value; } + public float B { get => _o.B; set => _o.B = value; } + public float A { get => _o.A; set => _o.A = value; } + public float X { get => _o.X; set => _o.X = value; } + public float Y { get => _o.Y; set => _o.Y = value; } + public float ScaleX { get => _o.ScaleX; set => _o.ScaleX = value; } + public float ScaleY { get => _o.ScaleY; set => _o.ScaleY = value; } + + public ImmutableArray Bones => _bones; + public FrozenDictionary BonesByName => _bonesByName; + public ImmutableArray Slots => _slots; + public FrozenDictionary SlotsByName => _slotsByName; + + public ISkin? Skin + { + get => _skin; + set + { + if (value is null) + { + _o.Skin = null; + _skin = null; + return; + } + if (value is Skin34 sk) + { + _o.Skin = sk.InnerObject; + _skin = sk; + return; + } + throw new ArgumentException($"Received {value.GetType().Name}", nameof(value)); + } + } + + public IEnumerable IterDrawOrder() => _o.DrawOrder.Select(s => _slots[s.Data.Index]); + public void UpdateCache() => _o.UpdateCache(); + public void UpdateWorldTransform(ISkeleton.Physics physics) => _o.UpdateWorldTransform(); + public void SetToSetupPose() => _o.SetToSetupPose(); + public void SetBonesToSetupPose() => _o.SetBonesToSetupPose(); + public void SetSlotsToSetupPose() => _o.SetSlotsToSetupPose(); + public void Update(float delta) => _o.Update(delta); + + public void GetBounds(out float x, out float y, out float w, out float h) + { + float[] _ = []; + _o.GetBounds(out x, out y, out w, out h); + } + + public override string ToString() => _o.ToString(); + } +} diff --git a/Spine/Implementations/SpineWrappers/V34/SkeletonClipping34.cs b/Spine/Implementations/SpineWrappers/V34/SkeletonClipping34.cs new file mode 100644 index 0000000..7b5cc8f --- /dev/null +++ b/Spine/Implementations/SpineWrappers/V34/SkeletonClipping34.cs @@ -0,0 +1,42 @@ +using Spine.SpineWrappers; +using Spine.SpineWrappers.Attachments; +using Spine.Utils; +using SpineRuntime34; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Spine.Implementations.SpineWrappers.V34 +{ + internal sealed class SkeletonClipping34 : ISkeletonClipping + { + public bool IsClipping => false; + + public float[] ClippedVertices { get; private set; } = []; + + public int ClippedVerticesLength { get; private set; } = 0; + + public int[] ClippedTriangles { get; private set; } = []; + + public int ClippedTrianglesLength { get; private set; } = 0; + + public float[] ClippedUVs { get; private set; } = []; + + public void ClipEnd(ISlot slot) { } + + public void ClipEnd() { } + + public void ClipStart(ISlot slot, IClippingAttachment clippingAttachment) { } + + public void ClipTriangles(float[] vertices, int verticesLength, int[] triangles, int trianglesLength, float[] uvs) + { + ClippedVertices = vertices.ToArray(); + ClippedVerticesLength = verticesLength; + ClippedTriangles = triangles.ToArray(); + ClippedTrianglesLength = trianglesLength; + ClippedUVs = uvs.ToArray(); + } + } +} diff --git a/Spine/Implementations/SpineWrappers/V34/Skin34.cs b/Spine/Implementations/SpineWrappers/V34/Skin34.cs new file mode 100644 index 0000000..dd06db8 --- /dev/null +++ b/Spine/Implementations/SpineWrappers/V34/Skin34.cs @@ -0,0 +1,45 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Spine.SpineWrappers; +using SpineRuntime34; + +namespace Spine.Implementations.SpineWrappers.V34 +{ + internal sealed class Skin34 : ISkin + { + private readonly Skin _o; + + /// + /// 使用指定名字创建空皮肤 + /// + public Skin34(string name) => _o = new(name); + + /// + /// 包装已有皮肤对象 + /// + public Skin34(Skin innerObject) => _o = innerObject; + + public Skin InnerObject => _o; + + public string Name => _o.Name; + + public void AddSkin(ISkin skin) + { + if (skin is Skin34 sk) + { + // NOTE: 3.7 及以下不支持 AddSkin + foreach (var (k, v) in sk._o.Attachments) + _o.AddAttachment(k.slotIndex, k.name, v); + return; + } + throw new ArgumentException($"Received {skin.GetType().Name}", nameof(skin)); + } + + public void Clear() => _o.Attachments.Clear(); + + public override string ToString() => _o.ToString(); + } +} diff --git a/Spine/Implementations/SpineWrappers/V34/Slot34.cs b/Spine/Implementations/SpineWrappers/V34/Slot34.cs new file mode 100644 index 0000000..bcb406f --- /dev/null +++ b/Spine/Implementations/SpineWrappers/V34/Slot34.cs @@ -0,0 +1,78 @@ +using System; +using System.Collections.Frozen; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Spine.Utils; +using Spine.SpineWrappers; +using SpineRuntime34; + +namespace Spine.Implementations.SpineWrappers.V34 +{ + internal sealed class Slot34 : ISlot + { + private readonly Slot _o; + private readonly SpineObjectData34 _data; + + private readonly Bone34 _bone; + private readonly SFML.Graphics.BlendMode _blendMode; + + public Slot34(Slot innerObject, SpineObjectData34 data, Bone34 bone) + { + _o = innerObject; + _data = data; + + _bone = bone; + _blendMode = _o.Data.BlendMode switch + { + BlendMode.normal => SFMLBlendMode.NormalPma, + BlendMode.additive => SFMLBlendMode.AdditivePma, + BlendMode.multiply => SFMLBlendMode.MultiplyPma, + BlendMode.screen => SFMLBlendMode.ScreenPma, + _ => throw new NotImplementedException($"{_o.Data.BlendMode}"), + }; + } + + public Slot InnerObject => _o; + + public string Name => _o.Data.Name; + public int Index => _o.Data.Index; + public SFML.Graphics.BlendMode Blend => _blendMode; + + public float R { get => _o.R; set => _o.R = value; } + public float G { get => _o.G; set => _o.G = value; } + public float B { get => _o.B; set => _o.B = value; } + public float A { get => _o.A; set => _o.A = value; } + public IBone Bone => _bone; + + public Spine.SpineWrappers.Attachments.IAttachment? Attachment + { + get + { + if (_o.Attachment is Attachment att) + { + return _data.SlotAttachments[Name][att.Name]; + } + return null; + } + + set + { + if (value is null) + { + _o.Attachment = null; + return; + } + if (value is Attachments.Attachment34 att) + { + _o.Attachment = att.InnerObject; + return; + } + throw new ArgumentException($"Received {value.GetType().Name}", nameof(value)); + } + } + + public override string ToString() => _o.ToString(); + } +} diff --git a/Spine/Implementations/SpineWrappers/V34/SpineObjectData34.cs b/Spine/Implementations/SpineWrappers/V34/SpineObjectData34.cs new file mode 100644 index 0000000..8d19c1d --- /dev/null +++ b/Spine/Implementations/SpineWrappers/V34/SpineObjectData34.cs @@ -0,0 +1,136 @@ +using System; +using System.Collections.Generic; +using System.Collections.Frozen; +using System.Collections.Immutable; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Spine.Utils; +using Spine.SpineWrappers; +using Spine.SpineWrappers.Attachments; +using SpineRuntime34; +using Spine.Implementations.SpineWrappers.V34.Attachments; + +namespace Spine.Implementations.SpineWrappers.V34 +{ + [SpineImplementation(3, 4)] + internal sealed class SpineObjectData34 : SpineObjectData + { + private readonly Atlas _atlas; + private readonly SkeletonData _skeletonData; + private readonly AnimationStateData _animationStateData; + + private readonly ImmutableArray _skins; + private readonly FrozenDictionary _skinsByName; + private readonly FrozenDictionary> _slotAttachments; + private readonly ImmutableArray _animations; + private readonly FrozenDictionary _animationsByName; + + public SpineObjectData34(string skelPath, string atlasPath, Spine.SpineWrappers.TextureLoader textureLoader) + : base(skelPath, atlasPath, textureLoader) + { + // 加载 atlas + try { _atlas = new Atlas(atlasPath, textureLoader); } + catch (Exception ex) { throw new InvalidDataException($"Failed to load atlas '{atlasPath}'", ex); } + + try + { + if (Utf8Validator.IsUtf8(skelPath)) + { + try + { + _skeletonData = new SkeletonJson(_atlas).ReadSkeletonData(skelPath); + } + catch + { + _skeletonData = new SkeletonBinary(_atlas).ReadSkeletonData(skelPath); + } + } + else + { + try + { + _skeletonData = new SkeletonBinary(_atlas).ReadSkeletonData(skelPath); + } + catch + { + _skeletonData = new SkeletonJson(_atlas).ReadSkeletonData(skelPath); + } + } + } + catch (Exception ex) + { + _atlas.Dispose(); + throw new InvalidDataException($"Failed to load skeleton file {skelPath}", ex); + } + + // 加载动画数据 + _animationStateData = new AnimationStateData(_skeletonData); + + // 整理皮肤和附件 + Dictionary> slotAttachments = []; + List skins = []; + Dictionary skinsByName = []; + foreach (var s in _skeletonData.Skins) + { + var skin = new Skin34(s); + skins.Add(skin); + skinsByName[s.Name] = skin; + foreach (var (k, att) in s.Attachments) + { + var slotName = _skeletonData.Slots.Items[k.slotIndex].Name; + if (!slotAttachments.TryGetValue(slotName, out var attachments)) + slotAttachments[slotName] = attachments = []; + + attachments[att.Name] = att switch + { + RegionAttachment regionAtt => new RegionAttachment34(regionAtt), + MeshAttachment meshAtt => new MeshAttachment34(meshAtt), + BoundingBoxAttachment bbAtt => new BoundingBoxAttachment34(bbAtt), + PathAttachment pathAtt => new PathAttachment34(pathAtt), + _ => throw new InvalidOperationException($"Unrecognized attachment type {att.GetType().FullName}") + }; + } + } + _slotAttachments = slotAttachments.ToFrozenDictionary(it => it.Key, it => it.Value.ToFrozenDictionary()); + _skins = skins.ToImmutableArray(); + _skinsByName = skinsByName.ToFrozenDictionary(); + + // 整理所有动画数据 + List animations = []; + Dictionary animationsByName = []; + foreach (var a in _skeletonData.Animations) + { + var anime = new Animation34(a); + animations.Add(anime); + animationsByName[anime.Name] = anime; + } + _animations = animations.ToImmutableArray(); + _animationsByName = animationsByName.ToFrozenDictionary(); + } + + public override string SkeletonVersion => _skeletonData.Version; + + public override ImmutableArray Skins => _skins; + + public override FrozenDictionary SkinsByName => _skinsByName; + + public override FrozenDictionary> SlotAttachments => _slotAttachments; + + public override float DefaultMix { get => _animationStateData.DefaultMix; set => _animationStateData.DefaultMix = value; } + + public override ImmutableArray Animations => _animations; + + public override FrozenDictionary AnimationsByName => _animationsByName; + + protected override void DisposeAtlas() => _atlas.Dispose(); + + public override ISkeleton CreateSkeleton() => new Skeleton34(new(_skeletonData), this); + + public override IAnimationState CreateAnimationState() => new AnimationState34(new(_animationStateData), this); + + public override ISkeletonClipping CreateSkeletonClipping() => new SkeletonClipping34(); + + public override ISkin CreateSkin(string name) => new Skin34(name); + } +} diff --git a/Spine/Implementations/SpineWrappers/V34/TrackEntry34.cs b/Spine/Implementations/SpineWrappers/V34/TrackEntry34.cs new file mode 100644 index 0000000..7bc9482 --- /dev/null +++ b/Spine/Implementations/SpineWrappers/V34/TrackEntry34.cs @@ -0,0 +1,135 @@ +using Spine.SpineWrappers; +using SpineRuntime34; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Spine.Implementations.SpineWrappers.V34 +{ + internal sealed class TrackEntry34(TrackEntry innerObject, AnimationState34 animationState, SpineObjectData34 data): ITrackEntry + { + private readonly TrackEntry _o = innerObject; + private readonly AnimationState34 _animationState = animationState; + private readonly SpineObjectData34 _data = data; + + private readonly Dictionary _eventMapping = []; + private readonly Dictionary _eventCount = []; + + public TrackEntry InnerObject => _o; + +#pragma warning disable CS0067 + + // 3.4 及以下没有这两个事件 + public event IAnimationState.TrackEntryDelegate? Interrupt; + public event IAnimationState.TrackEntryDelegate? Dispose; + +#pragma warning restore CS0067 + + public event IAnimationState.TrackEntryDelegate? Start + { + add + { + if (value is null) return; + if (!_eventMapping.TryGetValue(value, out var f)) + { + _eventMapping[value] = f = (TrackEntry t) => value(_animationState.GetTrackEntry(t)); + _eventCount[value] = 0; + } + _o.Start += f; + _eventCount[value]++; + } + remove + { + if (value is null) return; + if (_eventMapping.TryGetValue(value, out var f)) + { + _o.Start -= f; + _eventCount[value]--; + if (_eventCount[value] <= 0) + { + _eventMapping.Remove(value); + _eventCount.Remove(value); + } + } + } + } + + public event IAnimationState.TrackEntryDelegate? End + { + add + { + if (value is null) return; + if (!_eventMapping.TryGetValue(value, out var f)) + { + _eventMapping[value] = f = (TrackEntry t) => value(_animationState.GetTrackEntry(t)); + _eventCount[value] = 0; + } + _o.End += f; + _eventCount[value]++; + } + remove + { + if (value is null) return; + if (_eventMapping.TryGetValue(value, out var f)) + { + _o.End -= f; + _eventCount[value]--; + if (_eventCount[value] <= 0) + { + _eventMapping.Remove(value); + _eventCount.Remove(value); + } + } + } + } + + public event IAnimationState.TrackEntryDelegate? Complete + { + add + { + if (value is null) return; + if (!_eventMapping.TryGetValue(value, out var f)) + { + _eventMapping[value] = f = (TrackEntry t) => value(_animationState.GetTrackEntry(t)); + _eventCount[value] = 0; + } + _o.Complete += f; + _eventCount[value]++; + } + remove + { + if (value is null) return; + if (_eventMapping.TryGetValue(value, out var f)) + { + _o.Complete -= f; + _eventCount[value]--; + if (_eventCount[value] <= 0) + { + _eventMapping.Remove(value); + _eventCount.Remove(value); + } + } + } + } + + public int TrackIndex { get => _o.TrackIndex; } + + public IAnimation Animation { get => _data.AnimationsByName[_o.Animation.Name]; } + + public ITrackEntry? Next { get { var t = _o.Next; return t is null ? null : _animationState.GetTrackEntry(t); } } + + public bool Loop { get => _o.Loop; set => _o.Loop = value; } + + public float TrackTime { get => _o.Time; set => _o.Time = value; } + + public float TimeScale { get => _o.TimeScale; set => _o.TimeScale = value; } + + public float Alpha { get => _o.Mix; set => _o.Mix = value; } + + public float MixDuration { get => _o.MixDuration; set => _o.MixDuration = value; } + + public override string ToString() => _o.ToString(); + } +} diff --git a/Spine/Implementations/SpineWrappers/V35/Animation35.cs b/Spine/Implementations/SpineWrappers/V35/Animation35.cs new file mode 100644 index 0000000..5c21bd6 --- /dev/null +++ b/Spine/Implementations/SpineWrappers/V35/Animation35.cs @@ -0,0 +1,23 @@ +using Spine.SpineWrappers; +using SpineRuntime35; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Spine.Implementations.SpineWrappers.V35 +{ + internal sealed class Animation35(Animation innerObject) : IAnimation + { + private readonly Animation _o = innerObject; + + public Animation InnerObject => _o; + + public string Name => _o.Name; + + public float Duration => _o.Duration; + + public override string ToString() => _o.ToString(); + } +} diff --git a/Spine/Implementations/SpineWrappers/V35/AnimationState35.cs b/Spine/Implementations/SpineWrappers/V35/AnimationState35.cs new file mode 100644 index 0000000..7673533 --- /dev/null +++ b/Spine/Implementations/SpineWrappers/V35/AnimationState35.cs @@ -0,0 +1,229 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Spine.SpineWrappers; +using SpineRuntime35; + +namespace Spine.Implementations.SpineWrappers.V35 +{ + internal sealed class AnimationState35(AnimationState innerObject, SpineObjectData35 data) : IAnimationState + { + private readonly AnimationState _o = innerObject; + private readonly SpineObjectData35 _data = data; + + private readonly Dictionary _trackEntryPool = []; + + private readonly Dictionary _eventMapping = []; + private readonly Dictionary _eventCount = []; + + public AnimationState InnerObject => _o; + + public event IAnimationState.TrackEntryDelegate? Start + { + add + { + if (value is null) return; + if (!_eventMapping.TryGetValue(value, out var f)) + { + _eventMapping[value] = f = (TrackEntry t) => value(GetTrackEntry(t)); + _eventCount[value] = 0; + } + _o.Start += f; + _eventCount[value]++; + } + remove + { + if (value is null) return; + if (_eventMapping.TryGetValue(value, out var f)) + { + _o.Start -= f; + _eventCount[value]--; + if (_eventCount[value] <= 0) + { + _eventMapping.Remove(value); + _eventCount.Remove(value); + } + } + } + } + + public event IAnimationState.TrackEntryDelegate? Interrupt + { + add + { + if (value is null) return; + if (!_eventMapping.TryGetValue(value, out var f)) + { + _eventMapping[value] = f = (TrackEntry t) => value(GetTrackEntry(t)); + _eventCount[value] = 0; + } + _o.Interrupt += f; + _eventCount[value]++; + } + remove + { + if (value is null) return; + if (_eventMapping.TryGetValue(value, out var f)) + { + _o.Interrupt -= f; + _eventCount[value]--; + if (_eventCount[value] <= 0) + { + _eventMapping.Remove(value); + _eventCount.Remove(value); + } + } + } + } + + public event IAnimationState.TrackEntryDelegate? End + { + add + { + if (value is null) return; + if (!_eventMapping.TryGetValue(value, out var f)) + { + _eventMapping[value] = f = (TrackEntry t) => value(GetTrackEntry(t)); + _eventCount[value] = 0; + } + _o.End += f; + _eventCount[value]++; + } + remove + { + if (value is null) return; + if (_eventMapping.TryGetValue(value, out var f)) + { + _o.End -= f; + _eventCount[value]--; + if (_eventCount[value] <= 0) + { + _eventMapping.Remove(value); + _eventCount.Remove(value); + } + } + } + } + + public event IAnimationState.TrackEntryDelegate? Complete + { + add + { + if (value is null) return; + if (!_eventMapping.TryGetValue(value, out var f)) + { + _eventMapping[value] = f = (TrackEntry t) => value(GetTrackEntry(t)); + _eventCount[value] = 0; + } + _o.Complete += f; + _eventCount[value]++; + } + remove + { + if (value is null) return; + if (_eventMapping.TryGetValue(value, out var f)) + { + _o.Complete -= f; + _eventCount[value]--; + if (_eventCount[value] <= 0) + { + _eventMapping.Remove(value); + _eventCount.Remove(value); + } + } + } + } + + public event IAnimationState.TrackEntryDelegate? Dispose + { + add + { + if (value is null) return; + if (!_eventMapping.TryGetValue(value, out var f)) + { + _eventMapping[value] = f = (TrackEntry t) => value(GetTrackEntry(t)); + _eventCount[value] = 0; + } + _o.Dispose += f; + _eventCount[value]++; + } + remove + { + if (value is null) return; + if (_eventMapping.TryGetValue(value, out var f)) + { + _o.Dispose -= f; + _eventCount[value]--; + if (_eventCount[value] <= 0) + { + _eventMapping.Remove(value); + _eventCount.Remove(value); + } + } + } + } + + public float TimeScale { get => _o.TimeScale; set => _o.TimeScale = value; } + + public void Update(float delta) => _o.Update(delta); + + public void Apply(ISkeleton skeleton) + { + if (skeleton is Skeleton35 skel) + { + _o.Apply(skel.InnerObject); + return; + } + throw new ArgumentException($"Received {skeleton.GetType().Name}", nameof(skeleton)); + } + + /// + /// 获取 对象, 不存在则创建 + /// + public ITrackEntry GetTrackEntry(TrackEntry trackEntry) + { + if (!_trackEntryPool.TryGetValue(trackEntry, out var tr)) + _trackEntryPool[trackEntry] = tr = new(trackEntry, this, _data); + return tr; + } + + public IEnumerable IterTracks() => _o.Tracks.Select(t => t is null ? null : GetTrackEntry(t)); + + public ITrackEntry? GetCurrent(int index) { var t = _o.GetCurrent(index); return t is null ? null : GetTrackEntry(t); } + + public void ClearTrack(int index) => _o.ClearTrack(index); + + public void ClearTracks() => _o.ClearTracks(); + + public ITrackEntry SetAnimation(int trackIndex, string animationName, bool loop) + => GetTrackEntry(_o.SetAnimation(trackIndex, animationName, loop)); + + public ITrackEntry SetAnimation(int trackIndex, IAnimation animation, bool loop) + { + if (animation is Animation35 anime) + return GetTrackEntry(_o.SetAnimation(trackIndex, anime.InnerObject, loop)); + throw new ArgumentException($"Received {animation.GetType().Name}", nameof(animation)); + } + + public ITrackEntry SetEmptyAnimation(int trackIndex, float mixDuration) => GetTrackEntry(_o.SetEmptyAnimation(trackIndex, mixDuration)); + + public void SetEmptyAnimations(float mixDuration) => _o.SetEmptyAnimations(mixDuration); + + public ITrackEntry AddAnimation(int trackIndex, string animationName, bool loop, float delay) + => GetTrackEntry(_o.AddAnimation(trackIndex, animationName, loop, delay)); + + public ITrackEntry AddAnimation(int trackIndex, IAnimation animation, bool loop, float delay) + { + if (animation is Animation35 anime) + return GetTrackEntry(_o.AddAnimation(trackIndex, anime.InnerObject, loop, delay)); + throw new ArgumentException($"Received {animation.GetType().Name}", nameof(animation)); + } + + public ITrackEntry AddEmptyAnimation(int trackIndex, float mixDuration, float delay) + => GetTrackEntry(_o.AddEmptyAnimation(trackIndex, mixDuration, delay)); + + public override string ToString() => _o.ToString(); + } +} diff --git a/Spine/Implementations/SpineWrappers/V35/Attachments/Attachment35.cs b/Spine/Implementations/SpineWrappers/V35/Attachments/Attachment35.cs new file mode 100644 index 0000000..10bc162 --- /dev/null +++ b/Spine/Implementations/SpineWrappers/V35/Attachments/Attachment35.cs @@ -0,0 +1,24 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Spine.SpineWrappers; +using Spine.SpineWrappers.Attachments; +using SpineRuntime35; + +namespace Spine.Implementations.SpineWrappers.V35.Attachments +{ + internal abstract class Attachment35(Attachment innerObject) : IAttachment + { + private readonly Attachment _o = innerObject; + + public virtual Attachment InnerObject => _o; + + public string Name => _o.Name; + + public abstract int ComputeWorldVertices(ISlot slot, ref float[] worldVertices); + + public override string ToString() => _o.ToString(); + } +} diff --git a/Spine/Implementations/SpineWrappers/V35/Attachments/BoundingBoxAttachment35.cs b/Spine/Implementations/SpineWrappers/V35/Attachments/BoundingBoxAttachment35.cs new file mode 100644 index 0000000..d7db614 --- /dev/null +++ b/Spine/Implementations/SpineWrappers/V35/Attachments/BoundingBoxAttachment35.cs @@ -0,0 +1,31 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Spine.SpineWrappers.Attachments; +using SpineRuntime35; + +namespace Spine.Implementations.SpineWrappers.V35.Attachments +{ + internal sealed class BoundingBoxAttachment35(BoundingBoxAttachment innerObject) : + Attachment35(innerObject), + IBoundingBoxAttachment + { + private readonly BoundingBoxAttachment _o = innerObject; + + public override BoundingBoxAttachment InnerObject => _o; + + public override int ComputeWorldVertices(Spine.SpineWrappers.ISlot slot, ref float[] worldVertices) + { + if (slot is Slot35 st) + { + var length = _o.WorldVerticesLength; + if (worldVertices.Length < length) worldVertices = new float[length]; + _o.ComputeWorldVertices(st.InnerObject, worldVertices); + return length; + } + throw new ArgumentException($"Invalid slot type. Expected {nameof(Slot35)}, but received {slot.GetType().Name}", nameof(slot)); + } + } +} diff --git a/Spine/Implementations/SpineWrappers/V35/Attachments/ClippingAttachment35.cs b/Spine/Implementations/SpineWrappers/V35/Attachments/ClippingAttachment35.cs new file mode 100644 index 0000000..5fb91ba --- /dev/null +++ b/Spine/Implementations/SpineWrappers/V35/Attachments/ClippingAttachment35.cs @@ -0,0 +1,31 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Spine.SpineWrappers.Attachments; +using SpineRuntime35; + +namespace Spine.Implementations.SpineWrappers.V35.Attachments +{ + internal sealed class ClippingAttachment35(ClippingAttachment innerObject) : + Attachment35(innerObject), + IClippingAttachment + { + private readonly ClippingAttachment _o = innerObject; + + public override ClippingAttachment InnerObject => _o; + + public override int ComputeWorldVertices(Spine.SpineWrappers.ISlot slot, ref float[] worldVertices) + { + if (slot is Slot35 st) + { + var length = _o.WorldVerticesLength; + if (worldVertices.Length < length) worldVertices = new float[length]; + _o.ComputeWorldVertices(st.InnerObject, worldVertices); + return length; + } + throw new ArgumentException($"Invalid slot type. Expected {nameof(Slot35)}, but received {slot.GetType().Name}", nameof(slot)); + } + } +} diff --git a/Spine/Implementations/SpineWrappers/V35/Attachments/MeshAttachment35.cs b/Spine/Implementations/SpineWrappers/V35/Attachments/MeshAttachment35.cs new file mode 100644 index 0000000..363576c --- /dev/null +++ b/Spine/Implementations/SpineWrappers/V35/Attachments/MeshAttachment35.cs @@ -0,0 +1,44 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Spine.SpineWrappers.Attachments; +using SpineRuntime35; + +namespace Spine.Implementations.SpineWrappers.V35.Attachments +{ + internal sealed class MeshAttachment35(MeshAttachment innerObject) : + Attachment35(innerObject), + IMeshAttachment + { + private readonly MeshAttachment _o = innerObject; + + public override MeshAttachment InnerObject => _o; + + public override int ComputeWorldVertices(Spine.SpineWrappers.ISlot slot, ref float[] worldVertices) + { + if (slot is Slot35 st) + { + var length = _o.WorldVerticesLength; + if (worldVertices.Length < length) worldVertices = new float[length]; + _o.ComputeWorldVertices(st.InnerObject, worldVertices); + return length; + } + throw new ArgumentException($"Invalid slot type. Expected {nameof(Slot35)}, but received {slot.GetType().Name}", nameof(slot)); + } + + public float R { get => _o.R; set => _o.R = value; } + public float G { get => _o.G; set => _o.G = value; } + public float B { get => _o.B; set => _o.B = value; } + public float A { get => _o.A; set => _o.A = value; } + + public SFML.Graphics.Texture RendererObject => (SFML.Graphics.Texture)((AtlasRegion)_o.RendererObject).page.rendererObject; + + public float[] UVs => _o.UVs; + + public int[] Triangles => _o.Triangles; + + public int HullLength => _o.HullLength; + } +} diff --git a/Spine/Implementations/SpineWrappers/V35/Attachments/PathAttachment35.cs b/Spine/Implementations/SpineWrappers/V35/Attachments/PathAttachment35.cs new file mode 100644 index 0000000..87fc52d --- /dev/null +++ b/Spine/Implementations/SpineWrappers/V35/Attachments/PathAttachment35.cs @@ -0,0 +1,31 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Spine.SpineWrappers.Attachments; +using SpineRuntime35; + +namespace Spine.Implementations.SpineWrappers.V35.Attachments +{ + internal sealed class PathAttachment35(PathAttachment innerObject) : + Attachment35(innerObject), + IPathAttachment + { + private readonly PathAttachment _o = innerObject; + + public override PathAttachment InnerObject => _o; + + public override int ComputeWorldVertices(Spine.SpineWrappers.ISlot slot, ref float[] worldVertices) + { + if (slot is Slot35 st) + { + var length = _o.WorldVerticesLength; + if (worldVertices.Length < length) worldVertices = new float[length]; + _o.ComputeWorldVertices(st.InnerObject, worldVertices); + return length; + } + throw new ArgumentException($"Invalid slot type. Expected {nameof(Slot35)}, but received {slot.GetType().Name}", nameof(slot)); + } + } +} \ No newline at end of file diff --git a/Spine/Implementations/SpineWrappers/V35/Attachments/PointAttachment35.cs b/Spine/Implementations/SpineWrappers/V35/Attachments/PointAttachment35.cs new file mode 100644 index 0000000..5993724 --- /dev/null +++ b/Spine/Implementations/SpineWrappers/V35/Attachments/PointAttachment35.cs @@ -0,0 +1,30 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Spine.SpineWrappers.Attachments; +using SpineRuntime35; + +namespace Spine.Implementations.SpineWrappers.V35.Attachments +{ + internal sealed class PointAttachment35(PointAttachment innerObject) : + Attachment35(innerObject), + IPointAttachment + { + private readonly PointAttachment _o = innerObject; + + public override PointAttachment InnerObject => _o; + + public override int ComputeWorldVertices(Spine.SpineWrappers.ISlot slot, ref float[] worldVertices) + { + if (slot is Slot35 st) + { + if (worldVertices.Length < 2) worldVertices = new float[2]; + _o.ComputeWorldPosition(st.InnerObject.Bone, out worldVertices[0], out worldVertices[1]); + return 2; + } + throw new ArgumentException($"Invalid slot type. Expected {nameof(Slot35)}, but received {slot.GetType().Name}", nameof(slot)); + } + } +} \ No newline at end of file diff --git a/Spine/Implementations/SpineWrappers/V35/Attachments/RegionAttachment35.cs b/Spine/Implementations/SpineWrappers/V35/Attachments/RegionAttachment35.cs new file mode 100644 index 0000000..b4d820a --- /dev/null +++ b/Spine/Implementations/SpineWrappers/V35/Attachments/RegionAttachment35.cs @@ -0,0 +1,39 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Spine.SpineWrappers.Attachments; +using SpineRuntime35; + +namespace Spine.Implementations.SpineWrappers.V35.Attachments +{ + internal sealed class RegionAttachment35(RegionAttachment innerObject) : + Attachment35(innerObject), + IRegionAttachment + { + private readonly RegionAttachment _o = innerObject; + + public override RegionAttachment InnerObject => _o; + + public override int ComputeWorldVertices(Spine.SpineWrappers.ISlot slot, ref float[] worldVertices) + { + if (slot is Slot35 st) + { + if (worldVertices.Length < 8) worldVertices = new float[8]; + _o.ComputeWorldVertices(st.InnerObject.Bone, worldVertices, 0); + return 8; + } + throw new ArgumentException($"Invalid slot type. Expected {nameof(Slot35)}, but received {slot.GetType().Name}", nameof(slot)); + } + + public float R { get => _o.R; set => _o.R = value; } + public float G { get => _o.G; set => _o.G = value; } + public float B { get => _o.B; set => _o.B = value; } + public float A { get => _o.A; set => _o.A = value; } + + public SFML.Graphics.Texture RendererObject => (SFML.Graphics.Texture)((AtlasRegion)_o.RendererObject).page.rendererObject; + + public float[] UVs => _o.UVs; + } +} diff --git a/Spine/Implementations/SpineWrappers/V35/Bone35.cs b/Spine/Implementations/SpineWrappers/V35/Bone35.cs new file mode 100644 index 0000000..c6f7f3e --- /dev/null +++ b/Spine/Implementations/SpineWrappers/V35/Bone35.cs @@ -0,0 +1,33 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Spine.SpineWrappers; +using SpineRuntime35; + +namespace Spine.Implementations.SpineWrappers.V35 +{ + internal sealed class Bone35(Bone innerObject, Bone35? parent = null) : IBone + { + private readonly Bone _o = innerObject; + private readonly Bone35? _parent = parent; + + public Bone InnerObject => _o; + + public string Name => _o.Data.Name; + public int Index => _o.Data.Index; + + public IBone? Parent => _parent; + public bool Active => true; // NOTE: 3.7 及以下没有 Active 属性, 此处总是返回 true + public float Length => _o.Data.Length; + public float WorldX => _o.WorldX; + public float WorldY => _o.WorldY; + public float A => _o.A; + public float B => _o.B; + public float C => _o.C; + public float D => _o.D; + + public override string ToString() => _o.ToString(); + } +} diff --git a/Spine/Implementations/SpineWrappers/V35/Skeleton35.cs b/Spine/Implementations/SpineWrappers/V35/Skeleton35.cs new file mode 100644 index 0000000..53a77c6 --- /dev/null +++ b/Spine/Implementations/SpineWrappers/V35/Skeleton35.cs @@ -0,0 +1,106 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using System.Collections.Frozen; +using System.Collections.Immutable; +using Spine.SpineWrappers; +using SpineRuntime35; + +namespace Spine.Implementations.SpineWrappers.V35 +{ + internal sealed class Skeleton35 : ISkeleton + { + private readonly Skeleton _o; + private readonly SpineObjectData35 _data; + + private readonly ImmutableArray _bones; + private readonly FrozenDictionary _bonesByName; + private readonly ImmutableArray _slots; + private readonly FrozenDictionary _slotsByName; + + private Skin35? _skin; + + public Skeleton35(Skeleton innerObject, SpineObjectData35 data) + { + _o = innerObject; + _data = data; + + List bones = []; + Dictionary bonesByName = []; + foreach (var b in _o.Bones) + { + var bone = new Bone35(b, b.Parent is null ? null : bones[b.Parent.Data.Index]); + bones.Add(bone); + bonesByName[bone.Name] = bone; + } + _bones = bones.Cast().ToImmutableArray(); + _bonesByName = bonesByName.ToFrozenDictionary(); + + List slots = []; + Dictionary slotsByName = []; + foreach (var s in _o.Slots) + { + var slot = new Slot35(s, _data, bones[s.Bone.Data.Index]); + slots.Add(slot); + slotsByName[slot.Name] = slot; + } + _slots = slots.Cast().ToImmutableArray(); + _slotsByName = slotsByName.ToFrozenDictionary(); + } + + public Skeleton InnerObject => _o; + + public float R { get => _o.R; set => _o.R = value; } + public float G { get => _o.G; set => _o.G = value; } + public float B { get => _o.B; set => _o.B = value; } + public float A { get => _o.A; set => _o.A = value; } + public float X { get => _o.X; set => _o.X = value; } + public float Y { get => _o.Y; set => _o.Y = value; } + public float ScaleX { get => _o.ScaleX; set => _o.ScaleX = value; } + public float ScaleY { get => _o.ScaleY; set => _o.ScaleY = value; } + + public ImmutableArray Bones => _bones; + public FrozenDictionary BonesByName => _bonesByName; + public ImmutableArray Slots => _slots; + public FrozenDictionary SlotsByName => _slotsByName; + + public ISkin? Skin + { + get => _skin; + set + { + if (value is null) + { + _o.Skin = null; + _skin = null; + return; + } + if (value is Skin35 sk) + { + _o.Skin = sk.InnerObject; + _skin = sk; + return; + } + throw new ArgumentException($"Received {value.GetType().Name}", nameof(value)); + } + } + + public IEnumerable IterDrawOrder() => _o.DrawOrder.Select(s => _slots[s.Data.Index]); + public void UpdateCache() => _o.UpdateCache(); + public void UpdateWorldTransform(ISkeleton.Physics physics) => _o.UpdateWorldTransform(); + public void SetToSetupPose() => _o.SetToSetupPose(); + public void SetBonesToSetupPose() => _o.SetBonesToSetupPose(); + public void SetSlotsToSetupPose() => _o.SetSlotsToSetupPose(); + public void Update(float delta) => _o.Update(delta); + + public void GetBounds(out float x, out float y, out float w, out float h) + { + float[] _ = []; + _o.GetBounds(out x, out y, out w, out h, ref _); + } + + public override string ToString() => _o.ToString(); + } +} diff --git a/Spine/Implementations/SpineWrappers/V35/SkeletonClipping35.cs b/Spine/Implementations/SpineWrappers/V35/SkeletonClipping35.cs new file mode 100644 index 0000000..e55d619 --- /dev/null +++ b/Spine/Implementations/SpineWrappers/V35/SkeletonClipping35.cs @@ -0,0 +1,56 @@ +using Spine.SpineWrappers; +using Spine.SpineWrappers.Attachments; +using Spine.Utils; +using SpineRuntime35; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Spine.Implementations.SpineWrappers.V35 +{ + internal sealed class SkeletonClipping35 : ISkeletonClipping + { + private readonly SkeletonClipping _o = new(); + + public bool IsClipping => _o.IsClipping(); + + public float[] ClippedVertices => _o.ClippedVertices.Items; + + public int ClippedVerticesLength => _o.ClippedVertices.Count; + + public int[] ClippedTriangles => _o.ClippedTriangles.Items; + + public int ClippedTrianglesLength => _o.ClippedTriangles.Count; + + public float[] ClippedUVs => _o.ClippedUVs.Items; + + public void ClipTriangles(float[] vertices, int verticesLength, int[] triangles, int trianglesLength, float[] uvs) + => _o.ClipTriangles(vertices, verticesLength, triangles, trianglesLength, uvs); + + public void ClipStart(ISlot slot, IClippingAttachment clippingAttachment) + { + if (slot is Slot35 st && clippingAttachment is Attachments.ClippingAttachment35 att) + { + _o.ClipStart(st.InnerObject, att.InnerObject); + return; + } + throw new ArgumentException($"Received {slot.GetType().Name} {clippingAttachment.GetType().Name}"); + } + + public void ClipEnd(ISlot slot) + { + if (slot is Slot35 st) + { + _o.ClipEnd(st.InnerObject); + return; + } + throw new ArgumentException($"Received {slot.GetType().Name}", nameof(slot)); + } + + public void ClipEnd() => _o.ClipEnd(); + + public override string ToString() => _o.ToString(); + } +} diff --git a/Spine/Implementations/SpineWrappers/V35/Skin35.cs b/Spine/Implementations/SpineWrappers/V35/Skin35.cs new file mode 100644 index 0000000..46af8ef --- /dev/null +++ b/Spine/Implementations/SpineWrappers/V35/Skin35.cs @@ -0,0 +1,45 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Spine.SpineWrappers; +using SpineRuntime35; + +namespace Spine.Implementations.SpineWrappers.V35 +{ + internal sealed class Skin35 : ISkin + { + private readonly Skin _o; + + /// + /// 使用指定名字创建空皮肤 + /// + public Skin35(string name) => _o = new(name); + + /// + /// 包装已有皮肤对象 + /// + public Skin35(Skin innerObject) => _o = innerObject; + + public Skin InnerObject => _o; + + public string Name => _o.Name; + + public void AddSkin(ISkin skin) + { + if (skin is Skin35 sk) + { + // NOTE: 3.7 及以下不支持 AddSkin + foreach (var (k, v) in sk._o.Attachments) + _o.AddAttachment(k.slotIndex, k.name, v); + return; + } + throw new ArgumentException($"Received {skin.GetType().Name}", nameof(skin)); + } + + public void Clear() => _o.Attachments.Clear(); + + public override string ToString() => _o.ToString(); + } +} diff --git a/Spine/Implementations/SpineWrappers/V35/Slot35.cs b/Spine/Implementations/SpineWrappers/V35/Slot35.cs new file mode 100644 index 0000000..538b1d2 --- /dev/null +++ b/Spine/Implementations/SpineWrappers/V35/Slot35.cs @@ -0,0 +1,78 @@ +using System; +using System.Collections.Frozen; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Spine.Utils; +using Spine.SpineWrappers; +using SpineRuntime35; + +namespace Spine.Implementations.SpineWrappers.V35 +{ + internal sealed class Slot35 : ISlot + { + private readonly Slot _o; + private readonly SpineObjectData35 _data; + + private readonly Bone35 _bone; + private readonly SFML.Graphics.BlendMode _blendMode; + + public Slot35(Slot innerObject, SpineObjectData35 data, Bone35 bone) + { + _o = innerObject; + _data = data; + + _bone = bone; + _blendMode = _o.Data.BlendMode switch + { + BlendMode.Normal => SFMLBlendMode.NormalPma, + BlendMode.Additive => SFMLBlendMode.AdditivePma, + BlendMode.Multiply => SFMLBlendMode.MultiplyPma, + BlendMode.Screen => SFMLBlendMode.ScreenPma, + _ => throw new NotImplementedException($"{_o.Data.BlendMode}"), + }; + } + + public Slot InnerObject => _o; + + public string Name => _o.Data.Name; + public int Index => _o.Data.Index; + public SFML.Graphics.BlendMode Blend => _blendMode; + + public float R { get => _o.R; set => _o.R = value; } + public float G { get => _o.G; set => _o.G = value; } + public float B { get => _o.B; set => _o.B = value; } + public float A { get => _o.A; set => _o.A = value; } + public IBone Bone => _bone; + + public Spine.SpineWrappers.Attachments.IAttachment? Attachment + { + get + { + if (_o.Attachment is Attachment att) + { + return _data.SlotAttachments[Name][att.Name]; + } + return null; + } + + set + { + if (value is null) + { + _o.Attachment = null; + return; + } + if (value is Attachments.Attachment35 att) + { + _o.Attachment = att.InnerObject; + return; + } + throw new ArgumentException($"Received {value.GetType().Name}", nameof(value)); + } + } + + public override string ToString() => _o.ToString(); + } +} diff --git a/Spine/Implementations/SpineWrappers/V35/SpineObjectData35.cs b/Spine/Implementations/SpineWrappers/V35/SpineObjectData35.cs new file mode 100644 index 0000000..011dda8 --- /dev/null +++ b/Spine/Implementations/SpineWrappers/V35/SpineObjectData35.cs @@ -0,0 +1,138 @@ +using System; +using System.Collections.Generic; +using System.Collections.Frozen; +using System.Collections.Immutable; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Spine.Utils; +using Spine.SpineWrappers; +using Spine.SpineWrappers.Attachments; +using SpineRuntime35; +using Spine.Implementations.SpineWrappers.V35.Attachments; + +namespace Spine.Implementations.SpineWrappers.V35 +{ + [SpineImplementation(3, 5)] + internal sealed class SpineObjectData35 : SpineObjectData + { + private readonly Atlas _atlas; + private readonly SkeletonData _skeletonData; + private readonly AnimationStateData _animationStateData; + + private readonly ImmutableArray _skins; + private readonly FrozenDictionary _skinsByName; + private readonly FrozenDictionary> _slotAttachments; + private readonly ImmutableArray _animations; + private readonly FrozenDictionary _animationsByName; + + public SpineObjectData35(string skelPath, string atlasPath, Spine.SpineWrappers.TextureLoader textureLoader) + : base(skelPath, atlasPath, textureLoader) + { + // 加载 atlas + try { _atlas = new Atlas(atlasPath, textureLoader); } + catch (Exception ex) { throw new InvalidDataException($"Failed to load atlas '{atlasPath}'", ex); } + + try + { + if (Utf8Validator.IsUtf8(skelPath)) + { + try + { + _skeletonData = new SkeletonJson(_atlas).ReadSkeletonData(skelPath); + } + catch + { + _skeletonData = new SkeletonBinary(_atlas).ReadSkeletonData(skelPath); + } + } + else + { + try + { + _skeletonData = new SkeletonBinary(_atlas).ReadSkeletonData(skelPath); + } + catch + { + _skeletonData = new SkeletonJson(_atlas).ReadSkeletonData(skelPath); + } + } + } + catch (Exception ex) + { + _atlas.Dispose(); + throw new InvalidDataException($"Failed to load skeleton file {skelPath}", ex); + } + + // 加载动画数据 + _animationStateData = new AnimationStateData(_skeletonData); + + // 整理皮肤和附件 + Dictionary> slotAttachments = []; + List skins = []; + Dictionary skinsByName = []; + foreach (var s in _skeletonData.Skins) + { + var skin = new Skin35(s); + skins.Add(skin); + skinsByName[s.Name] = skin; + foreach (var (k, att) in s.Attachments) + { + var slotName = _skeletonData.Slots.Items[k.slotIndex].Name; + if (!slotAttachments.TryGetValue(slotName, out var attachments)) + slotAttachments[slotName] = attachments = []; + + attachments[att.Name] = att switch + { + RegionAttachment regionAtt => new RegionAttachment35(regionAtt), + MeshAttachment meshAtt => new MeshAttachment35(meshAtt), + ClippingAttachment clipAtt => new ClippingAttachment35(clipAtt), + BoundingBoxAttachment bbAtt => new BoundingBoxAttachment35(bbAtt), + PathAttachment pathAtt => new PathAttachment35(pathAtt), + PointAttachment ptAtt => new PointAttachment35(ptAtt), + _ => throw new InvalidOperationException($"Unrecognized attachment type {att.GetType().FullName}") + }; + } + } + _slotAttachments = slotAttachments.ToFrozenDictionary(it => it.Key, it => it.Value.ToFrozenDictionary()); + _skins = skins.ToImmutableArray(); + _skinsByName = skinsByName.ToFrozenDictionary(); + + // 整理所有动画数据 + List animations = []; + Dictionary animationsByName = []; + foreach (var a in _skeletonData.Animations) + { + var anime = new Animation35(a); + animations.Add(anime); + animationsByName[anime.Name] = anime; + } + _animations = animations.ToImmutableArray(); + _animationsByName = animationsByName.ToFrozenDictionary(); + } + + public override string SkeletonVersion => _skeletonData.Version; + + public override ImmutableArray Skins => _skins; + + public override FrozenDictionary SkinsByName => _skinsByName; + + public override FrozenDictionary> SlotAttachments => _slotAttachments; + + public override float DefaultMix { get => _animationStateData.DefaultMix; set => _animationStateData.DefaultMix = value; } + + public override ImmutableArray Animations => _animations; + + public override FrozenDictionary AnimationsByName => _animationsByName; + + protected override void DisposeAtlas() => _atlas.Dispose(); + + public override ISkeleton CreateSkeleton() => new Skeleton35(new(_skeletonData), this); + + public override IAnimationState CreateAnimationState() => new AnimationState35(new(_animationStateData), this); + + public override ISkeletonClipping CreateSkeletonClipping() => new SkeletonClipping35(); + + public override ISkin CreateSkin(string name) => new Skin35(name); + } +} diff --git a/Spine/Implementations/SpineWrappers/V35/TrackEntry35.cs b/Spine/Implementations/SpineWrappers/V35/TrackEntry35.cs new file mode 100644 index 0000000..37de769 --- /dev/null +++ b/Spine/Implementations/SpineWrappers/V35/TrackEntry35.cs @@ -0,0 +1,185 @@ +using Spine.SpineWrappers; +using SpineRuntime35; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Spine.Implementations.SpineWrappers.V35 +{ + internal sealed class TrackEntry35(TrackEntry innerObject, AnimationState35 animationState, SpineObjectData35 data): ITrackEntry + { + private readonly TrackEntry _o = innerObject; + private readonly AnimationState35 _animationState = animationState; + private readonly SpineObjectData35 _data = data; + + private readonly Dictionary _eventMapping = []; + private readonly Dictionary _eventCount = []; + + public TrackEntry InnerObject => _o; + + public event IAnimationState.TrackEntryDelegate? Start + { + add + { + if (value is null) return; + if (!_eventMapping.TryGetValue(value, out var f)) + { + _eventMapping[value] = f = (TrackEntry t) => value(_animationState.GetTrackEntry(t)); + _eventCount[value] = 0; + } + _o.Start += f; + _eventCount[value]++; + } + remove + { + if (value is null) return; + if (_eventMapping.TryGetValue(value, out var f)) + { + _o.Start -= f; + _eventCount[value]--; + if (_eventCount[value] <= 0) + { + _eventMapping.Remove(value); + _eventCount.Remove(value); + } + } + } + } + + public event IAnimationState.TrackEntryDelegate? Interrupt + { + add + { + if (value is null) return; + if (!_eventMapping.TryGetValue(value, out var f)) + { + _eventMapping[value] = f = (TrackEntry t) => value(_animationState.GetTrackEntry(t)); + _eventCount[value] = 0; + } + _o.Interrupt += f; + _eventCount[value]++; + } + remove + { + if (value is null) return; + if (_eventMapping.TryGetValue(value, out var f)) + { + _o.Interrupt -= f; + _eventCount[value]--; + if (_eventCount[value] <= 0) + { + _eventMapping.Remove(value); + _eventCount.Remove(value); + } + } + } + } + + public event IAnimationState.TrackEntryDelegate? End + { + add + { + if (value is null) return; + if (!_eventMapping.TryGetValue(value, out var f)) + { + _eventMapping[value] = f = (TrackEntry t) => value(_animationState.GetTrackEntry(t)); + _eventCount[value] = 0; + } + _o.End += f; + _eventCount[value]++; + } + remove + { + if (value is null) return; + if (_eventMapping.TryGetValue(value, out var f)) + { + _o.End -= f; + _eventCount[value]--; + if (_eventCount[value] <= 0) + { + _eventMapping.Remove(value); + _eventCount.Remove(value); + } + } + } + } + + public event IAnimationState.TrackEntryDelegate? Complete + { + add + { + if (value is null) return; + if (!_eventMapping.TryGetValue(value, out var f)) + { + _eventMapping[value] = f = (TrackEntry t) => value(_animationState.GetTrackEntry(t)); + _eventCount[value] = 0; + } + _o.Complete += f; + _eventCount[value]++; + } + remove + { + if (value is null) return; + if (_eventMapping.TryGetValue(value, out var f)) + { + _o.Complete -= f; + _eventCount[value]--; + if (_eventCount[value] <= 0) + { + _eventMapping.Remove(value); + _eventCount.Remove(value); + } + } + } + } + + public event IAnimationState.TrackEntryDelegate? Dispose + { + add + { + if (value is null) return; + if (!_eventMapping.TryGetValue(value, out var f)) + { + _eventMapping[value] = f = (TrackEntry t) => value(_animationState.GetTrackEntry(t)); + _eventCount[value] = 0; + } + _o.Dispose += f; + _eventCount[value]++; + } + remove + { + if (value is null) return; + if (_eventMapping.TryGetValue(value, out var f)) + { + _o.Dispose -= f; + _eventCount[value]--; + if (_eventCount[value] <= 0) + { + _eventMapping.Remove(value); + _eventCount.Remove(value); + } + } + } + } + + public int TrackIndex { get => _o.TrackIndex; } + + public IAnimation Animation { get => _data.AnimationsByName[_o.Animation.Name]; } + + public ITrackEntry? Next { get { var t = _o.Next; return t is null ? null : _animationState.GetTrackEntry(t); } } + + public bool Loop { get => _o.Loop; set => _o.Loop = value; } + + public float TrackTime { get => _o.TrackTime; set => _o.TrackTime = value; } + + public float TimeScale { get => _o.TimeScale; set => _o.TimeScale = value; } + + public float Alpha { get => _o.Alpha; set => _o.Alpha = value; } + + public float MixDuration { get => _o.MixDuration; set => _o.MixDuration = value; } + + public override string ToString() => _o.ToString(); + } +} diff --git a/Spine/Spine.csproj b/Spine/Spine.csproj index 717e511..ca4af78 100644 --- a/Spine/Spine.csproj +++ b/Spine/Spine.csproj @@ -23,6 +23,8 @@ + + diff --git a/Spine/SpineVersion.cs b/Spine/SpineVersion.cs index 867e78e..a5b42cb 100644 --- a/Spine/SpineVersion.cs +++ b/Spine/SpineVersion.cs @@ -19,6 +19,8 @@ namespace Spine public sealed class SpineVersion : IEquatable, IComparable { public static readonly SpineVersion V21 = new(typeof(SpineRuntime21.Skeleton)); + public static readonly SpineVersion V34 = new(typeof(SpineRuntime34.Skeleton)); + public static readonly SpineVersion V35 = new(typeof(SpineRuntime35.Skeleton)); public static readonly SpineVersion V36 = new(typeof(SpineRuntime36.Skeleton)); public static readonly SpineVersion V37 = new(typeof(SpineRuntime37.Skeleton)); public static readonly SpineVersion V38 = new(typeof(SpineRuntime38.Skeleton)); diff --git a/Spine/SpineWrappers/TextureLoader.cs b/Spine/SpineWrappers/TextureLoader.cs index 34c23c7..8d86128 100644 --- a/Spine/SpineWrappers/TextureLoader.cs +++ b/Spine/SpineWrappers/TextureLoader.cs @@ -11,6 +11,8 @@ namespace Spine.SpineWrappers /// public class TextureLoader : SpineRuntime21.TextureLoader, + SpineRuntime34.TextureLoader, + SpineRuntime35.TextureLoader, SpineRuntime36.TextureLoader, SpineRuntime37.TextureLoader, SpineRuntime38.TextureLoader, @@ -106,6 +108,76 @@ namespace Spine.SpineWrappers page.rendererObject = texture; } + public virtual void Load(SpineRuntime34.AtlasPage page, string path) + { + var texture = ReadTexture(path); + + if (page.magFilter == SpineRuntime34.TextureFilter.Linear) + { + texture.Smooth = true; + } + if (page.uWrap == SpineRuntime34.TextureWrap.Repeat && page.vWrap == SpineRuntime34.TextureWrap.Repeat) + { + texture.Repeated = true; + } + switch (page.minFilter) + { + case SpineRuntime34.TextureFilter.Linear: + texture.Smooth = true; + break; + case SpineRuntime34.TextureFilter.MipMap: + case SpineRuntime34.TextureFilter.MipMapNearestNearest: + texture.GenerateMipmap(); + break; + case SpineRuntime34.TextureFilter.MipMapLinearNearest: + case SpineRuntime34.TextureFilter.MipMapNearestLinear: + case SpineRuntime34.TextureFilter.MipMapLinearLinear: + texture.Smooth = true; + texture.GenerateMipmap(); + break; + } + + if (ForceNearest) texture.Smooth = false; + if (ForceMipmap) texture.GenerateMipmap(); + + page.rendererObject = texture; + } + + public virtual void Load(SpineRuntime35.AtlasPage page, string path) + { + var texture = ReadTexture(path); + + if (page.magFilter == SpineRuntime35.TextureFilter.Linear) + { + texture.Smooth = true; + } + if (page.uWrap == SpineRuntime35.TextureWrap.Repeat && page.vWrap == SpineRuntime35.TextureWrap.Repeat) + { + texture.Repeated = true; + } + switch (page.minFilter) + { + case SpineRuntime35.TextureFilter.Linear: + texture.Smooth = true; + break; + case SpineRuntime35.TextureFilter.MipMap: + case SpineRuntime35.TextureFilter.MipMapNearestNearest: + texture.GenerateMipmap(); + break; + case SpineRuntime35.TextureFilter.MipMapLinearNearest: + case SpineRuntime35.TextureFilter.MipMapNearestLinear: + case SpineRuntime35.TextureFilter.MipMapLinearLinear: + texture.Smooth = true; + texture.GenerateMipmap(); + break; + } + + if (ForceNearest) texture.Smooth = false; + if (ForceMipmap) texture.GenerateMipmap(); + + page.rendererObject = texture; + } + public virtual void Load(SpineRuntime36.AtlasPage page, string path) { var texture = ReadTexture(path); diff --git a/SpineRuntimes/SpineRuntime21/AnimationState.cs b/SpineRuntimes/SpineRuntime21/AnimationState.cs index 52692b2..d4a9cca 100644 --- a/SpineRuntimes/SpineRuntime21/AnimationState.cs +++ b/SpineRuntimes/SpineRuntime21/AnimationState.cs @@ -38,7 +38,7 @@ namespace SpineRuntime21 { static readonly Animation EmptyAnimation = new Animation("", new List(), 0); private AnimationStateData data; - Pool trackEntryPool = new Pool(); + private readonly Pool trackEntryPool = new Pool(); private List tracks = new List(); private List events = new List(); private float timeScale = 1; diff --git a/SpineRuntimes/SpineRuntime34/AnimationState.cs b/SpineRuntimes/SpineRuntime34/AnimationState.cs index dca423f..a3dbf8b 100644 --- a/SpineRuntimes/SpineRuntime34/AnimationState.cs +++ b/SpineRuntimes/SpineRuntime34/AnimationState.cs @@ -34,26 +34,25 @@ using System.Text; namespace SpineRuntime34 { public class AnimationState { - private AnimationStateData data; + static readonly Animation EmptyAnimation = new Animation("", new ExposedList(), 0); + private AnimationStateData data; private ExposedList tracks = new ExposedList(); private ExposedList events = new ExposedList(); private float timeScale = 1; public AnimationStateData Data { get { return data; } } - /// A list of tracks that have animations, which may contain nulls. - public ExposedList Tracks { get { return tracks; } } + private readonly Pool trackEntryPool = new Pool(); + + /// A list of tracks that have animations, which may contain nulls. + public ExposedList Tracks { get { return tracks; } } public float TimeScale { get { return timeScale; } set { timeScale = value; } } - public delegate void StartEndDelegate (AnimationState state, int trackIndex); - public event StartEndDelegate Start; - public event StartEndDelegate End; + public delegate void TrackEntryDelegate(TrackEntry trackEntry); + public event TrackEntryDelegate Start, End, Complete; - public delegate void EventDelegate (AnimationState state, int trackIndex, Event e); + public delegate void EventDelegate (AnimationState state, int trackIndex, Event e); public event EventDelegate Event; - public delegate void CompleteDelegate (AnimationState state, int trackIndex, int loopCount); - public event CompleteDelegate Complete; - public AnimationState (AnimationStateData data) { if (data == null) throw new ArgumentNullException("data", "data cannot be null."); this.data = data; @@ -78,8 +77,8 @@ namespace SpineRuntime34 { // Check if completed the animation or a loop iteration. if (current.loop ? (current.lastTime % endTime > time % endTime) : (current.lastTime < endTime && time >= endTime)) { int count = (int)(time / endTime); - current.OnComplete(this, i, count); - if (Complete != null) Complete(this, i, count); + current.OnComplete(); + if (Complete != null) Complete(current); } TrackEntry next = current.next; @@ -149,11 +148,18 @@ namespace SpineRuntime34 { TrackEntry current = tracks.Items[trackIndex]; if (current == null) return; - current.OnEnd(this, trackIndex); - if (End != null) End(this, trackIndex); + current.OnEnd(); + if (End != null) End(current); tracks.Items[trackIndex] = null; - } + + while (current is not null) + { + var tmp = current.next; + trackEntryPool.Free(current); + current = tmp; + } + } private TrackEntry ExpandToIndex (int index) { if (index < tracks.Count) return tracks.Items[index]; @@ -168,8 +174,8 @@ namespace SpineRuntime34 { TrackEntry previous = current.previous; current.previous = null; - current.OnEnd(this, index); - if (End != null) End(this, index); + current.OnEnd(); + if (End != null) End(current); entry.mixDuration = data.GetMix(current.animation, entry.animation); if (entry.mixDuration > 0) { @@ -184,8 +190,15 @@ namespace SpineRuntime34 { tracks.Items[index] = entry; - entry.OnStart(this, index); - if (Start != null) Start(this, index); + while (current is not null) + { + var tmp = current.next; + trackEntryPool.Free(current); + current = tmp; + } + + entry.OnStart(); + if (Start != null) Start(entry); } /// @@ -198,8 +211,9 @@ namespace SpineRuntime34 { /// Set the current animation. Any queued animations are cleared. public TrackEntry SetAnimation (int trackIndex, Animation animation, bool loop) { if (animation == null) throw new ArgumentNullException("animation", "animation cannot be null."); - TrackEntry entry = new TrackEntry(); - entry.animation = animation; + TrackEntry entry = trackEntryPool.Obtain(); + entry.trackIndex = trackIndex; + entry.animation = animation; entry.loop = loop; entry.time = 0; entry.endTime = animation.Duration; @@ -218,8 +232,10 @@ namespace SpineRuntime34 { /// May be <= 0 to use duration of previous animation minus any mix duration plus the negative delay. public TrackEntry AddAnimation (int trackIndex, Animation animation, bool loop, float delay) { if (animation == null) throw new ArgumentNullException("animation", "animation cannot be null."); - TrackEntry entry = new TrackEntry(); - entry.animation = animation; + TrackEntry entry = trackEntryPool.Obtain(); + entry.trackIndex = trackIndex; + entry.animation = animation; + entry.animation = animation; entry.loop = loop; entry.time = 0; entry.endTime = animation.Duration; @@ -243,8 +259,48 @@ namespace SpineRuntime34 { return entry; } - /// May be null. - public TrackEntry GetCurrent (int trackIndex) { + /// + /// Sets an empty animation for a track, discarding any queued animations, and mixes to it over the specified mix duration. + public TrackEntry SetEmptyAnimation(int trackIndex, float mixDuration) + { + TrackEntry entry = SetAnimation(trackIndex, AnimationState.EmptyAnimation, false); + entry.mixDuration = mixDuration; + entry.endTime = mixDuration; + return entry; + } + + /// + /// Adds an empty animation to be played after the current or last queued animation for a track, and mixes to it over the + /// specified mix duration. + /// + /// A track entry to allow further customization of animation playback. References to the track entry must not be kept after . + /// + /// Track number. + /// Mix duration. + /// Seconds to begin this animation after the start of the previous animation. May be <= 0 to use the animation + /// duration of the previous track minus any mix duration plus the negative delay. + public TrackEntry AddEmptyAnimation(int trackIndex, float mixDuration, float delay) + { + if (delay <= 0) delay -= mixDuration; + TrackEntry entry = AddAnimation(trackIndex, AnimationState.EmptyAnimation, false, delay); + entry.mixDuration = mixDuration; + entry.endTime = mixDuration; + return entry; + } + + /// + /// Sets an empty animation for every track, discarding any queued animations, and mixes to it over the specified mix duration. + public void SetEmptyAnimations(float mixDuration) + { + for (int i = 0, n = tracks.Count; i < n; i++) + { + TrackEntry current = tracks.Items[i]; + if (current != null) SetEmptyAnimation(i, mixDuration); + } + } + + /// May be null. + public TrackEntry GetCurrent (int trackIndex) { if (trackIndex >= tracks.Count) return null; return tracks.Items[trackIndex]; } @@ -262,15 +318,17 @@ namespace SpineRuntime34 { } } - public class TrackEntry { + public class TrackEntry : Pool.IPoolable { internal TrackEntry next, previous; - internal Animation animation; + internal int trackIndex; + internal Animation animation; internal bool loop; internal float delay, time, lastTime = -1, endTime, timeScale = 1; internal float mixTime, mixDuration, mix = 1; - public Animation Animation { get { return animation; } } - public float Delay { get { return delay; } set { delay = value; } } + public int TrackIndex { get { return trackIndex; } } + public Animation Animation { get { return animation; } } + public float Delay { get { return delay; } set { delay = value; } } public float Time { get { return time; } set { time = value; } } public float LastTime { get { return lastTime; } set { lastTime = value; } } public float EndTime { get { return endTime; } set { endTime = value; } } @@ -278,29 +336,111 @@ namespace SpineRuntime34 { public float Mix { get { return mix; } set { mix = value; } } public bool Loop { get { return loop; } set { loop = value; } } - public event AnimationState.StartEndDelegate Start; - public event AnimationState.StartEndDelegate End; - public event AnimationState.EventDelegate Event; - public event AnimationState.CompleteDelegate Complete; + /// + /// Seconds for mixing from the previous animation to this animation. Defaults to the value provided by + /// based on the animation before this animation (if any). + /// + /// The mix duration can be set manually rather than use the value from AnimationStateData.GetMix. + /// In that case, the mixDuration must be set before is next called. + /// + /// When using with a + /// delay less than or equal to 0, note the is set using the mix duration from the + /// + /// + /// + public float MixDuration { get { return mixDuration; } set { mixDuration = value; } } - internal void OnStart (AnimationState state, int index) { - if (Start != null) Start(state, index); - } + /// + /// The animation queued to start after this animation, or null. + public TrackEntry Next { get { return next; } } - internal void OnEnd (AnimationState state, int index) { - if (End != null) End(state, index); - } + public event AnimationState.TrackEntryDelegate Start, End, Complete; + public event AnimationState.EventDelegate Event; - internal void OnEvent (AnimationState state, int index, Event e) { - if (Event != null) Event(state, index, e); - } + // IPoolable.Reset() + public void Reset() + { + next = null; + previous = null; + animation = null; - internal void OnComplete (AnimationState state, int index, int loopCount) { - if (Complete != null) Complete(state, index, loopCount); - } + Start = null; + End = null; + Complete = null; + Event = null; + } - override public String ToString () { + internal void OnStart() { if (Start != null) Start(this); } + internal void OnEnd() { if (End != null) End(this); } + internal void OnComplete() { if (Complete != null) Complete(this); } + + internal void OnEvent(AnimationState state, int index, Event e) + { + if (Event != null) Event(state, index, e); + } + + override public String ToString () { return animation == null ? "" : animation.name; } } + + public class Pool where T : class, new() + { + public readonly int max; + readonly Stack freeObjects; + + public int Count { get { return freeObjects.Count; } } + public int Peak { get; private set; } + + public Pool(int initialCapacity = 16, int max = int.MaxValue) + { + freeObjects = new Stack(initialCapacity); + this.max = max; + } + + public T Obtain() + { + return freeObjects.Count == 0 ? new T() : freeObjects.Pop(); + } + + public void Free(T obj) + { + if (obj == null) throw new ArgumentNullException("obj", "obj cannot be null"); + if (freeObjects.Count < max) + { + freeObjects.Push(obj); + Peak = Math.Max(Peak, freeObjects.Count); + } + Reset(obj); + } + + // protected void FreeAll (List objects) { + // if (objects == null) throw new ArgumentNullException("objects", "objects cannot be null."); + // var freeObjects = this.freeObjects; + // int max = this.max; + // for (int i = 0; i < objects.Count; i++) { + // T obj = objects[i]; + // if (obj == null) continue; + // if (freeObjects.Count < max) freeObjects.Push(obj); + // Reset(obj); + // } + // Peak = Math.Max(Peak, freeObjects.Count); + // } + + public void Clear() + { + freeObjects.Clear(); + } + + protected void Reset(T obj) + { + var poolable = obj as IPoolable; + if (poolable != null) poolable.Reset(); + } + + public interface IPoolable + { + void Reset(); + } + } } diff --git a/SpineRuntimes/SpineRuntime34/Bone.cs b/SpineRuntimes/SpineRuntime34/Bone.cs index 77a2686..dc2379b 100644 --- a/SpineRuntimes/SpineRuntime34/Bone.cs +++ b/SpineRuntimes/SpineRuntime34/Bone.cs @@ -105,22 +105,13 @@ namespace SpineRuntime34 { Bone parent = this.parent; if (parent == null) { // Root bone. Skeleton skeleton = this.skeleton; - if (skeleton.flipX) { - x = -x; - la = -la; - lb = -lb; - } - if (skeleton.flipY != yDown) { - y = -y; - lc = -lc; - ld = -ld; - } - a = la; - b = lb; - c = lc; - d = ld; - worldX = x; - worldY = y; + float sx = skeleton.scaleX, sy = skeleton.scaleY; + a = la * sx; + b = lb * sx; + c = lc * sy; + d = ld * sy; + worldX = x * sx; + worldY = y * sy; worldSignX = Math.Sign(scaleX); worldSignY = Math.Sign(scaleY); return; @@ -196,15 +187,11 @@ namespace SpineRuntime34 { c = lc; d = ld; } - if (skeleton.flipX) { - a = -a; - b = -b; - } - if (skeleton.flipY != yDown) { - c = -c; - d = -d; - } - } + a *= skeleton.scaleX; + b *= skeleton.scaleX; + c *= skeleton.scaleY; + d *= skeleton.scaleY; + } } public void SetToSetupPose () { diff --git a/SpineRuntimes/SpineRuntime34/Skeleton.cs b/SpineRuntimes/SpineRuntime34/Skeleton.cs index 11b5fd3..6cae9a4 100644 --- a/SpineRuntimes/SpineRuntime34/Skeleton.cs +++ b/SpineRuntimes/SpineRuntime34/Skeleton.cs @@ -44,8 +44,8 @@ namespace SpineRuntime34 { internal Skin skin; internal float r = 1, g = 1, b = 1, a = 1; internal float time; - internal bool flipX, flipY; - internal float x, y; + internal float scaleX = 1, scaleY = 1; + internal float x, y; public SkeletonData Data { get { return data; } } public ExposedList Bones { get { return bones; } } @@ -63,10 +63,16 @@ namespace SpineRuntime34 { public float Time { get { return time; } set { time = value; } } public float X { get { return x; } set { x = value; } } public float Y { get { return y; } set { y = value; } } - public bool FlipX { get { return flipX; } set { flipX = value; } } - public bool FlipY { get { return flipY; } set { flipY = value; } } + public float ScaleX { get { return scaleX; } set { scaleX = value; } } + public float ScaleY { get { return scaleY; } set { scaleY = value; } } - public Bone RootBone { + [Obsolete("Use ScaleX instead. FlipX is when ScaleX is negative.")] + public bool FlipX { get { return scaleX < 0; } set { scaleX = value ? -1f : 1f; } } + + [Obsolete("Use ScaleY instead. FlipY is when ScaleY is negative.")] + public bool FlipY { get { return scaleY < 0; } set { scaleY = value ? -1f : 1f; } } + + public Bone RootBone { get { return bones.Count == 0 ? null : bones.Items[0]; } } @@ -453,5 +459,50 @@ namespace SpineRuntime34 { public void Update (float delta) { time += delta; } - } + + public void GetBounds(out float x, out float y, out float width, out float height) + { + float[] temp = new float[8]; + float minX = int.MaxValue, minY = int.MaxValue, maxX = int.MinValue, maxY = int.MinValue; + for (int i = 0, n = drawOrder.Count; i < n; i++) + { + Slot slot = drawOrder.Items[i]; + int verticesLength = 0; + float[] vertices = null; + Attachment attachment = slot.Attachment; + if (attachment is RegionAttachment regionAttachment) + { + verticesLength = 8; + vertices = temp; + if (vertices.Length < 8) vertices = temp = new float[8]; + regionAttachment.ComputeWorldVertices(slot.Bone, temp); + } + else if (attachment is MeshAttachment meshAttachment) + { + + MeshAttachment mesh = meshAttachment; + verticesLength = mesh.Vertices.Length; + vertices = temp; + if (vertices.Length < verticesLength) vertices = temp = new float[verticesLength]; + mesh.ComputeWorldVertices(slot, temp); + } + + if (vertices != null) + { + for (int ii = 0; ii < verticesLength; ii += 2) + { + float vx = vertices[ii], vy = vertices[ii + 1]; + minX = Math.Min(minX, vx); + minY = Math.Min(minY, vy); + maxX = Math.Max(maxX, vx); + maxY = Math.Max(maxY, vy); + } + } + } + x = minX; + y = minY; + width = maxX - minX; + height = maxY - minY; + } + } } diff --git a/SpineRuntimes/SpineRuntime35/AnimationState.cs b/SpineRuntimes/SpineRuntime35/AnimationState.cs index 88ea5ae..03ab6c7 100644 --- a/SpineRuntimes/SpineRuntime35/AnimationState.cs +++ b/SpineRuntimes/SpineRuntime35/AnimationState.cs @@ -47,9 +47,9 @@ namespace SpineRuntime35 { private float timeScale = 1; - Pool trackEntryPool = new Pool(); + private readonly Pool trackEntryPool = new Pool(); - public AnimationStateData Data { get { return data; } } + public AnimationStateData Data { get { return data; } } /// A list of tracks that have animations, which may contain nulls. public ExposedList Tracks { get { return tracks; } } public float TimeScale { get { return timeScale; } set { timeScale = value; } } diff --git a/SpineRuntimes/SpineRuntime35/Bone.cs b/SpineRuntimes/SpineRuntime35/Bone.cs index 25d7fec..37e0f16 100644 --- a/SpineRuntimes/SpineRuntime35/Bone.cs +++ b/SpineRuntimes/SpineRuntime35/Bone.cs @@ -152,37 +152,19 @@ namespace SpineRuntime35 { Bone parent = this.parent; if (parent == null) { // Root bone. - float rotationY = rotation + 90 + shearY; - float la = MathUtils.CosDeg(rotation + shearX) * scaleX; - float lb = MathUtils.CosDeg(rotationY) * scaleY; - float lc = MathUtils.SinDeg(rotation + shearX) * scaleX; - float ld = MathUtils.SinDeg(rotationY) * scaleY; - if (skeleton.flipX) { - x = -x; - la = -la; - lb = -lb; - } - if (skeleton.flipY != yDown) { - y = -y; - lc = -lc; - ld = -ld; - } - a = la; - b = lb; - c = lc; - d = ld; - worldX = x + skeleton.x; - worldY = y + skeleton.y; -// worldSignX = Math.Sign(scaleX); -// worldSignY = Math.Sign(scaleY); + float rotationY = rotation + 90 + shearY, sx = skeleton.scaleX, sy = skeleton.scaleY; + a = MathUtils.CosDeg(rotation + shearX) * scaleX * sx; + b = MathUtils.CosDeg(rotationY) * scaleY * sx; + c = MathUtils.SinDeg(rotation + shearX) * scaleX * sy; + d = MathUtils.SinDeg(rotationY) * scaleY * sy; + worldX = x * sx + skeleton.x; + worldY = y * sy + skeleton.y; return; } float pa = parent.a, pb = parent.b, pc = parent.c, pd = parent.d; worldX = pa * x + pb * y + parent.worldX; worldY = pc * x + pd * y + parent.worldY; -// worldSignX = parent.worldSignX * Math.Sign(scaleX); -// worldSignY = parent.worldSignY * Math.Sign(scaleY); switch (data.transformMode) { case TransformMode.Normal: { @@ -232,13 +214,16 @@ namespace SpineRuntime35 { case TransformMode.NoScale: case TransformMode.NoScaleOrReflection: { float cos = MathUtils.CosDeg(rotation), sin = MathUtils.SinDeg(rotation); - float za = pa * cos + pb * sin; - float zc = pc * cos + pd * sin; + float za = (pa * cos + pb * sin) / skeleton.scaleX; + float zc = (pc * cos + pd * sin) / skeleton.scaleY; float s = (float)Math.Sqrt(za * za + zc * zc); if (s > 0.00001f) s = 1 / s; za *= s; zc *= s; s = (float)Math.Sqrt(za * za + zc * zc); + if (data.transformMode == TransformMode.NoScale + && (pa * pd - pb * pc < 0) != (skeleton.scaleX < 0 != skeleton.scaleY < 0)) s = -s; + float r = MathUtils.PI / 2 + MathUtils.Atan2(zc, za); float zb = MathUtils.Cos(r) * s; float zd = MathUtils.Sin(r) * s; @@ -250,23 +235,15 @@ namespace SpineRuntime35 { b = za * lb + zb * ld; c = zc * la + zd * lc; d = zc * lb + zd * ld; - if (data.transformMode != TransformMode.NoScaleOrReflection ? pa * pd - pb * pc < 0 : skeleton.flipX != skeleton.flipY) { - b = -b; - d = -d; - } - return; + break; } } - if (skeleton.flipX) { - a = -a; - b = -b; - } - if (skeleton.flipY != Bone.yDown) { - c = -c; - d = -d; - } - } + a *= skeleton.scaleX; + b *= skeleton.scaleX; + c *= skeleton.scaleY; + d *= skeleton.scaleY; + } public void SetToSetupPose () { BoneData data = this.data; diff --git a/SpineRuntimes/SpineRuntime35/Skeleton.cs b/SpineRuntimes/SpineRuntime35/Skeleton.cs index 9aef239..284c315 100644 --- a/SpineRuntimes/SpineRuntime35/Skeleton.cs +++ b/SpineRuntimes/SpineRuntime35/Skeleton.cs @@ -45,8 +45,8 @@ namespace SpineRuntime35 { internal Skin skin; internal float r = 1, g = 1, b = 1, a = 1; internal float time; - internal bool flipX, flipY; - internal float x, y; + internal float scaleX = 1, scaleY = 1; + internal float x, y; public SkeletonData Data { get { return data; } } public ExposedList Bones { get { return bones; } } @@ -64,8 +64,14 @@ namespace SpineRuntime35 { public float Time { get { return time; } set { time = value; } } public float X { get { return x; } set { x = value; } } public float Y { get { return y; } set { y = value; } } - public bool FlipX { get { return flipX; } set { flipX = value; } } - public bool FlipY { get { return flipY; } set { flipY = value; } } + public float ScaleX { get { return scaleX; } set { scaleX = value; } } + public float ScaleY { get { return scaleY; } set { scaleY = value; } } + + [Obsolete("Use ScaleX instead. FlipX is when ScaleX is negative.")] + public bool FlipX { get { return scaleX < 0; } set { scaleX = value ? -1f : 1f; } } + + [Obsolete("Use ScaleY instead. FlipY is when ScaleY is negative.")] + public bool FlipY { get { return scaleY < 0; } set { scaleY = value ? -1f : 1f; } } public Bone RootBone { get { return bones.Count == 0 ? null : bones.Items[0]; } diff --git a/SpineRuntimes/SpineRuntime36/AnimationState.cs b/SpineRuntimes/SpineRuntime36/AnimationState.cs index 1e7a3d4..1e96d01 100644 --- a/SpineRuntimes/SpineRuntime36/AnimationState.cs +++ b/SpineRuntimes/SpineRuntime36/AnimationState.cs @@ -38,8 +38,8 @@ namespace SpineRuntime36 { private AnimationStateData data; - Pool trackEntryPool = new Pool(); - private readonly ExposedList tracks = new ExposedList(); + private readonly Pool trackEntryPool = new Pool(); + private readonly ExposedList tracks = new ExposedList(); private readonly ExposedList events = new ExposedList(); private readonly EventQueue queue; // Initialized by constructor.