From 0e30172dfd738f047415949a59e565c2b0cba4f0 Mon Sep 17 00:00:00 2001 From: ww-rm Date: Tue, 4 Mar 2025 19:29:42 +0800 Subject: [PATCH] =?UTF-8?q?=E5=A2=9E=E5=8A=A0spine3.7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- SpineRuntimes/SpineRuntime37/Animation.cs | 1775 +++++++++++++++++ .../SpineRuntime37/AnimationState.cs | 1259 ++++++++++++ .../SpineRuntime37/AnimationStateData.cs | 114 ++ SpineRuntimes/SpineRuntime37/Atlas.cs | 319 +++ .../Attachments/AtlasAttachmentLoader.cs | 108 + .../SpineRuntime37/Attachments/Attachment.cs | 49 + .../Attachments/AttachmentLoader.cs | 48 + .../Attachments/AttachmentType.cs | 34 + .../Attachments/BoundingBoxAttachment.cs | 39 + .../Attachments/ClippingAttachment.cs | 41 + .../Attachments/MeshAttachment.cs | 133 ++ .../Attachments/PathAttachment.cs | 47 + .../Attachments/PointAttachment.cs | 59 + .../Attachments/RegionAttachment.cs | 182 ++ .../Attachments/VertexAttachment.cs | 129 ++ SpineRuntimes/SpineRuntime37/BlendMode.cs | 34 + SpineRuntimes/SpineRuntime37/Bone.cs | 361 ++++ SpineRuntimes/SpineRuntime37/BoneData.cs | 99 + SpineRuntimes/SpineRuntime37/Event.cs | 64 + SpineRuntimes/SpineRuntime37/EventData.cs | 56 + SpineRuntimes/SpineRuntime37/ExposedList.cs | 624 ++++++ SpineRuntimes/SpineRuntime37/IConstraint.cs | 39 + SpineRuntimes/SpineRuntime37/IUpdatable.cs | 34 + SpineRuntimes/SpineRuntime37/IkConstraint.cs | 302 +++ .../SpineRuntime37/IkConstraintData.cs | 111 ++ SpineRuntimes/SpineRuntime37/Json.cs | 534 +++++ SpineRuntimes/SpineRuntime37/MathUtils.cs | 145 ++ .../SpineRuntime37/PathConstraint.cs | 469 +++++ .../SpineRuntime37/PathConstraintData.cs | 78 + SpineRuntimes/SpineRuntime37/Skeleton.cs | 599 ++++++ .../SpineRuntime37/SkeletonBinary.cs | 910 +++++++++ .../SpineRuntime37/SkeletonBounds.cs | 233 +++ .../SpineRuntime37/SkeletonClipping.cs | 296 +++ SpineRuntimes/SpineRuntime37/SkeletonData.cs | 228 +++ SpineRuntimes/SpineRuntime37/SkeletonJson.cs | 881 ++++++++ SpineRuntimes/SpineRuntime37/Skin.cs | 130 ++ SpineRuntimes/SpineRuntime37/Slot.cs | 181 ++ SpineRuntimes/SpineRuntime37/SlotData.cs | 73 + .../SpineRuntime37/SpineRuntime37.csproj | 14 + .../SpineRuntime37/TransformConstraint.cs | 311 +++ .../SpineRuntime37/TransformConstraintData.cs | 70 + SpineRuntimes/SpineRuntime37/Triangulator.cs | 277 +++ SpineViewer.sln | 7 + SpineViewer/Spine/Implementations/Spine37.cs | 338 ++++ SpineViewer/Spine/Version.cs | 2 +- SpineViewer/SpineViewer.csproj | 1 + 46 files changed, 11836 insertions(+), 1 deletion(-) create mode 100644 SpineRuntimes/SpineRuntime37/Animation.cs create mode 100644 SpineRuntimes/SpineRuntime37/AnimationState.cs create mode 100644 SpineRuntimes/SpineRuntime37/AnimationStateData.cs create mode 100644 SpineRuntimes/SpineRuntime37/Atlas.cs create mode 100644 SpineRuntimes/SpineRuntime37/Attachments/AtlasAttachmentLoader.cs create mode 100644 SpineRuntimes/SpineRuntime37/Attachments/Attachment.cs create mode 100644 SpineRuntimes/SpineRuntime37/Attachments/AttachmentLoader.cs create mode 100644 SpineRuntimes/SpineRuntime37/Attachments/AttachmentType.cs create mode 100644 SpineRuntimes/SpineRuntime37/Attachments/BoundingBoxAttachment.cs create mode 100644 SpineRuntimes/SpineRuntime37/Attachments/ClippingAttachment.cs create mode 100644 SpineRuntimes/SpineRuntime37/Attachments/MeshAttachment.cs create mode 100644 SpineRuntimes/SpineRuntime37/Attachments/PathAttachment.cs create mode 100644 SpineRuntimes/SpineRuntime37/Attachments/PointAttachment.cs create mode 100644 SpineRuntimes/SpineRuntime37/Attachments/RegionAttachment.cs create mode 100644 SpineRuntimes/SpineRuntime37/Attachments/VertexAttachment.cs create mode 100644 SpineRuntimes/SpineRuntime37/BlendMode.cs create mode 100644 SpineRuntimes/SpineRuntime37/Bone.cs create mode 100644 SpineRuntimes/SpineRuntime37/BoneData.cs create mode 100644 SpineRuntimes/SpineRuntime37/Event.cs create mode 100644 SpineRuntimes/SpineRuntime37/EventData.cs create mode 100644 SpineRuntimes/SpineRuntime37/ExposedList.cs create mode 100644 SpineRuntimes/SpineRuntime37/IConstraint.cs create mode 100644 SpineRuntimes/SpineRuntime37/IUpdatable.cs create mode 100644 SpineRuntimes/SpineRuntime37/IkConstraint.cs create mode 100644 SpineRuntimes/SpineRuntime37/IkConstraintData.cs create mode 100644 SpineRuntimes/SpineRuntime37/Json.cs create mode 100644 SpineRuntimes/SpineRuntime37/MathUtils.cs create mode 100644 SpineRuntimes/SpineRuntime37/PathConstraint.cs create mode 100644 SpineRuntimes/SpineRuntime37/PathConstraintData.cs create mode 100644 SpineRuntimes/SpineRuntime37/Skeleton.cs create mode 100644 SpineRuntimes/SpineRuntime37/SkeletonBinary.cs create mode 100644 SpineRuntimes/SpineRuntime37/SkeletonBounds.cs create mode 100644 SpineRuntimes/SpineRuntime37/SkeletonClipping.cs create mode 100644 SpineRuntimes/SpineRuntime37/SkeletonData.cs create mode 100644 SpineRuntimes/SpineRuntime37/SkeletonJson.cs create mode 100644 SpineRuntimes/SpineRuntime37/Skin.cs create mode 100644 SpineRuntimes/SpineRuntime37/Slot.cs create mode 100644 SpineRuntimes/SpineRuntime37/SlotData.cs create mode 100644 SpineRuntimes/SpineRuntime37/SpineRuntime37.csproj create mode 100644 SpineRuntimes/SpineRuntime37/TransformConstraint.cs create mode 100644 SpineRuntimes/SpineRuntime37/TransformConstraintData.cs create mode 100644 SpineRuntimes/SpineRuntime37/Triangulator.cs create mode 100644 SpineViewer/Spine/Implementations/Spine37.cs diff --git a/SpineRuntimes/SpineRuntime37/Animation.cs b/SpineRuntimes/SpineRuntime37/Animation.cs new file mode 100644 index 0000000..bcaebbb --- /dev/null +++ b/SpineRuntimes/SpineRuntime37/Animation.cs @@ -0,0 +1,1775 @@ +/****************************************************************************** + * Spine Runtimes License Agreement + * Last updated May 1, 2019. Replaces all prior versions. + * + * Copyright (c) 2013-2019, Esoteric Software LLC + * + * Integration of the Spine Runtimes into software or otherwise creating + * derivative works of the Spine Runtimes is permitted under the terms and + * conditions of Section 2 of the Spine Editor License Agreement: + * http://esotericsoftware.com/spine-editor-license + * + * Otherwise, it is permitted to integrate the Spine Runtimes into software + * or otherwise create derivative works of the Spine Runtimes (collectively, + * "Products"), provided that each user of the Products must obtain their own + * Spine Editor license and redistribution of the Products in any form must + * include this license and copyright notice. + * + * THIS SOFTWARE IS PROVIDED BY ESOTERIC SOFTWARE LLC "AS IS" AND ANY EXPRESS + * OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES + * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN + * NO EVENT SHALL ESOTERIC SOFTWARE LLC BE LIABLE FOR ANY DIRECT, INDIRECT, + * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, + * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES, BUSINESS + * INTERRUPTION, OR LOSS OF USE, DATA, OR PROFITS) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, + * EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + *****************************************************************************/ + +using System; +using System.Collections.Generic; + +namespace SpineRuntime37 { + + /// + /// A simple container for a list of timelines and a name. + public class Animation { + internal String name; + internal ExposedList timelines; + internal float duration; + + public Animation (string name, ExposedList timelines, float duration) { + if (name == null) throw new ArgumentNullException("name", "name cannot be null."); + if (timelines == null) throw new ArgumentNullException("timelines", "timelines cannot be null."); + this.name = name; + this.timelines = timelines; + this.duration = duration; + } + + public ExposedList Timelines { get { return timelines; } set { timelines = value; } } + + /// The duration of the animation in seconds, which is the highest time of all keys in the timeline. + public float Duration { get { return duration; } set { duration = value; } } + + /// The animation's name, which is unique within the skeleton. + public string Name { get { return name; } } + + /// Applies all the animation's timelines to the specified skeleton. + /// + public void Apply (Skeleton skeleton, float lastTime, float time, bool loop, ExposedList events, float alpha, MixBlend blend, + MixDirection direction) { + if (skeleton == null) throw new ArgumentNullException("skeleton", "skeleton cannot be null."); + + if (loop && duration != 0) { + time %= duration; + if (lastTime > 0) lastTime %= duration; + } + + ExposedList timelines = this.timelines; + for (int i = 0, n = timelines.Count; i < n; i++) + timelines.Items[i].Apply(skeleton, lastTime, time, events, alpha, blend, direction); + } + + override public string ToString () { + return name; + } + + /// After the first and before the last entry. + /// Index of first value greater than the target. + internal static int BinarySearch (float[] values, float target, int step) { + int low = 0; + int high = values.Length / step - 2; + if (high == 0) return step; + int current = (int)((uint)high >> 1); + while (true) { + if (values[(current + 1) * step] <= target) + low = current + 1; + else + high = current; + if (low == high) return (low + 1) * step; + current = (int)((uint)(low + high) >> 1); + } + } + + /// After the first and before the last entry. + internal static int BinarySearch (float[] values, float target) { + int low = 0; + int high = values.Length - 2; + if (high == 0) return 1; + int current = (int)((uint)high >> 1); + while (true) { + if (values[current + 1] <= target) + low = current + 1; + else + high = current; + if (low == high) return (low + 1); + current = (int)((uint)(low + high) >> 1); + } + } + + internal static int LinearSearch (float[] values, float target, int step) { + for (int i = 0, last = values.Length - step; i <= last; i += step) + if (values[i] > target) return i; + return -1; + } + } + + /// + /// The interface for all timelines. + public interface Timeline { + /// Applies this timeline to the skeleton. + /// The skeleton the timeline is being applied to. This provides access to the bones, slots, and other + /// skeleton components the timeline may change. + /// The time this timeline was last applied. Timelines such as trigger only at specific + /// times rather than every frame. In that case, the timeline triggers everything between lastTime + /// (exclusive) and time (inclusive). + /// The time within the animation. Most timelines find the key before and the key after this time so they can + /// interpolate between the keys. + /// If any events are fired, they are added to this list. Can be null to ignore firing events or if the + /// timeline does not fire events. + /// 0 applies the current or setup value (depending on blend). 1 applies the timeline value. + /// Between 0 and 1 applies a value between the current or setup value and the timeline value. By adjusting + /// alpha over time, an animation can be mixed in or out. alpha can also be useful to + /// apply animations on top of each other (layered). + /// Controls how mixing is applied when alpha < 1. + /// Indicates whether the timeline is mixing in or out. Used by timelines which perform instant transitions, + /// such as or . + void Apply (Skeleton skeleton, float lastTime, float time, ExposedList events, float alpha, MixBlend blend, MixDirection direction); + /// Uniquely encodes both the type of this timeline and the skeleton property that it affects. + int PropertyId { get; } + } + + /// + /// Controls how a timeline is mixed with the setup or current pose. + /// + public enum MixBlend { + + /// Transitions from the setup value to the timeline value (the current value is not used). Before the first key, the setup + /// value is set. + Setup, + + /// + /// + /// Transitions from the current value to the timeline value. Before the first key, transitions from the current value to + /// the setup value. Timelines which perform instant transitions, such as or + /// , use the setup value before the first key. + /// + /// First is intended for the first animations applied, not for animations layered on top of those. + /// + First, + + /// + /// + /// Transitions from the current value to the timeline value. No change is made before the first key (the current value is + /// kept until the first key). + /// + /// Replace is intended for animations layered on top of others, not for the first animations applied. + /// + Replace, + + /// + /// + /// Transitions from the current value to the current value plus the timeline value. No change is made before the first key + /// (the current value is kept until the first key). + /// + /// Add is intended for animations layered on top of others, not for the first animations applied. + /// + Add + } + + /// + /// Indicates whether a timeline's alpha is mixing out over time toward 0 (the setup or current pose value) or + /// mixing in toward 1 (the timeline's value). + /// + public enum MixDirection { + In, + Out + } + + internal enum TimelineType { + Rotate = 0, Translate, Scale, Shear, // + Attachment, Color, Deform, // + Event, DrawOrder, // + IkConstraint, TransformConstraint, // + PathConstraintPosition, PathConstraintSpacing, PathConstraintMix, // + TwoColor + } + + /// An interface for timelines which change the property of a bone. + public interface IBoneTimeline { + /// The index of the bone in that will be changed. + int BoneIndex { get; } + } + + /// An interface for timelines which change the property of a slot. + public interface ISlotTimeline { + /// The index of the slot in that will be changed. + int SlotIndex { get; } + } + + /// The base class for timelines that use interpolation between key frame values. + abstract public class CurveTimeline : Timeline { + protected const float LINEAR = 0, STEPPED = 1, BEZIER = 2; + protected const int BEZIER_SIZE = 10 * 2 - 1; + + internal float[] curves; // type, x, y, ... + /// The number of key frames for this timeline. + public int FrameCount { get { return curves.Length / BEZIER_SIZE + 1; } } + + public CurveTimeline (int frameCount) { + if (frameCount <= 0) throw new ArgumentOutOfRangeException("frameCount must be > 0: "); + curves = new float[(frameCount - 1) * BEZIER_SIZE]; + } + + abstract public void Apply (Skeleton skeleton, float lastTime, float time, ExposedList firedEvents, float alpha, MixBlend blend, MixDirection direction); + + abstract public int PropertyId { get; } + + /// Sets the specified key frame to linear interpolation. + public void SetLinear (int frameIndex) { + curves[frameIndex * BEZIER_SIZE] = LINEAR; + } + + /// Sets the specified key frame to stepped interpolation. + public void SetStepped (int frameIndex) { + curves[frameIndex * BEZIER_SIZE] = STEPPED; + } + + /// Returns the interpolation type for the specified key frame. + /// Linear is 0, stepped is 1, Bezier is 2. + public float GetCurveType (int frameIndex) { + int index = frameIndex * BEZIER_SIZE; + if (index == curves.Length) return LINEAR; + float type = curves[index]; + if (type == LINEAR) return LINEAR; + if (type == STEPPED) return STEPPED; + return BEZIER; + } + + /// Sets the specified key frame to Bezier interpolation. cx1 and cx2 are from 0 to 1, + /// representing the percent of time between the two key frames. cy1 and cy2 are the percent of the + /// difference between the key frame's values. + public void SetCurve (int frameIndex, float cx1, float cy1, float cx2, float cy2) { + float tmpx = (-cx1 * 2 + cx2) * 0.03f, tmpy = (-cy1 * 2 + cy2) * 0.03f; + float dddfx = ((cx1 - cx2) * 3 + 1) * 0.006f, dddfy = ((cy1 - cy2) * 3 + 1) * 0.006f; + float ddfx = tmpx * 2 + dddfx, ddfy = tmpy * 2 + dddfy; + float dfx = cx1 * 0.3f + tmpx + dddfx * 0.16666667f, dfy = cy1 * 0.3f + tmpy + dddfy * 0.16666667f; + + int i = frameIndex * BEZIER_SIZE; + float[] curves = this.curves; + curves[i++] = BEZIER; + + float x = dfx, y = dfy; + for (int n = i + BEZIER_SIZE - 1; i < n; i += 2) { + curves[i] = x; + curves[i + 1] = y; + dfx += ddfx; + dfy += ddfy; + ddfx += dddfx; + ddfy += dddfy; + x += dfx; + y += dfy; + } + } + + /// Returns the interpolated percentage for the specified key frame and linear percentage. + public float GetCurvePercent (int frameIndex, float percent) { + percent = MathUtils.Clamp (percent, 0, 1); + float[] curves = this.curves; + int i = frameIndex * BEZIER_SIZE; + float type = curves[i]; + if (type == LINEAR) return percent; + if (type == STEPPED) return 0; + i++; + float x = 0; + for (int start = i, n = i + BEZIER_SIZE - 1; i < n; i += 2) { + x = curves[i]; + if (x >= percent) { + if (i == start) return curves[i + 1] * percent / x; // First point is 0,0. + float prevX = curves[i - 2], prevY = curves[i - 1]; + return prevY + (curves[i + 1] - prevY) * (percent - prevX) / (x - prevX); + } + } + float y = curves[i - 1]; + return y + (1 - y) * (percent - x) / (1 - x); // Last point is 1,1. + } + } + + /// Changes a bone's local . + public class RotateTimeline : CurveTimeline, IBoneTimeline { + public const int ENTRIES = 2; + internal const int PREV_TIME = -2, PREV_ROTATION = -1; + internal const int ROTATION = 1; + + internal int boneIndex; + internal float[] frames; // time, degrees, ... + + public RotateTimeline (int frameCount) + : base(frameCount) { + frames = new float[frameCount << 1]; + } + + override public int PropertyId { + get { return ((int)TimelineType.Rotate << 24) + boneIndex; } + } + /// The index of the bone in that will be changed. + public int BoneIndex { + set { + if (value < 0) throw new ArgumentOutOfRangeException("index must be >= 0."); + this.boneIndex = value; + } + get { + return boneIndex; + } + } + /// The time in seconds and rotation in degrees for each key frame. + public float[] Frames { get { return frames; } set { frames = value; } } + + /// Sets the time in seconds and the rotation in degrees for the specified key frame. + public void SetFrame (int frameIndex, float time, float degrees) { + frameIndex <<= 1; + frames[frameIndex] = time; + frames[frameIndex + ROTATION] = degrees; + } + + override public void Apply (Skeleton skeleton, float lastTime, float time, ExposedList firedEvents, float alpha, MixBlend blend, + MixDirection direction) { + Bone bone = skeleton.bones.Items[boneIndex]; + + float[] frames = this.frames; + if (time < frames[0]) { // Time is before first frame. + switch (blend) { + case MixBlend.Setup: + bone.rotation = bone.data.rotation; + return; + case MixBlend.First: + float r = bone.data.rotation - bone.rotation; + bone.rotation += (r - (16384 - (int)(16384.499999999996 - r / 360)) * 360) * alpha; + return; + } + return; + } + + if (time >= frames[frames.Length - ENTRIES]) { // Time is after last frame. + float r = frames[frames.Length + PREV_ROTATION]; + switch (blend) { + case MixBlend.Setup: + bone.rotation = bone.data.rotation + r * alpha; + break; + case MixBlend.First: + case MixBlend.Replace: + r += bone.data.rotation - bone.rotation; + r -= (16384 - (int)(16384.499999999996 - r / 360)) * 360; + goto case MixBlend.Add; // Fall through. + + case MixBlend.Add: + bone.rotation += r * alpha; + break; + } + return; + } + + // Interpolate between the previous frame and the current frame. + int frame = Animation.BinarySearch(frames, time, ENTRIES); + float prevRotation = frames[frame + PREV_ROTATION]; + float frameTime = frames[frame]; + float percent = GetCurvePercent((frame >> 1) - 1, 1 - (time - frameTime) / (frames[frame + PREV_TIME] - frameTime)); + // scope for 'r' to prevent compile error. + { + float r = frames[frame + ROTATION] - prevRotation; + r = prevRotation + (r - (16384 - (int)(16384.499999999996 - r / 360)) * 360) * percent; + switch (blend) { + case MixBlend.Setup: + bone.rotation = bone.data.rotation + (r - (16384 - (int)(16384.499999999996 - r / 360)) * 360) * alpha; + break; + case MixBlend.First: + case MixBlend.Replace: + r += bone.data.rotation - bone.rotation; + goto case MixBlend.Add; // Fall through. + case MixBlend.Add: + bone.rotation += (r - (16384 - (int)(16384.499999999996 - r / 360)) * 360) * alpha; + break; + } + } + } + } + + /// Changes a bone's local and . + public class TranslateTimeline : CurveTimeline, IBoneTimeline { + public const int ENTRIES = 3; + protected const int PREV_TIME = -3, PREV_X = -2, PREV_Y = -1; + protected const int X = 1, Y = 2; + + internal int boneIndex; + internal float[] frames; // time, x, y, ... + + public TranslateTimeline (int frameCount) + : base(frameCount) { + frames = new float[frameCount * ENTRIES]; + } + + override public int PropertyId { + get { return ((int)TimelineType.Translate << 24) + boneIndex; } + } + + /// The index of the bone in that will be changed. + public int BoneIndex { + set { + if (value < 0) throw new ArgumentOutOfRangeException("index must be >= 0."); + this.boneIndex = value; + } + get { + return boneIndex; + } + } + /// The time in seconds, x, and y values for each key frame. + public float[] Frames { get { return frames; } set { frames = value; } } + + + /// Sets the time in seconds, x, and y values for the specified key frame. + public void SetFrame (int frameIndex, float time, float x, float y) { + frameIndex *= ENTRIES; + frames[frameIndex] = time; + frames[frameIndex + X] = x; + frames[frameIndex + Y] = y; + } + + override public void Apply (Skeleton skeleton, float lastTime, float time, ExposedList firedEvents, float alpha, MixBlend blend, + MixDirection direction) { + Bone bone = skeleton.bones.Items[boneIndex]; + + float[] frames = this.frames; + if (time < frames[0]) { // Time is before first frame. + switch (blend) { + case MixBlend.Setup: + bone.x = bone.data.x; + bone.y = bone.data.y; + return; + case MixBlend.First: + bone.x += (bone.data.x - bone.x) * alpha; + bone.y += (bone.data.y - bone.y) * alpha; + return; + } + return; + } + + float x, y; + if (time >= frames[frames.Length - ENTRIES]) { // Time is after last frame. + x = frames[frames.Length + PREV_X]; + y = frames[frames.Length + PREV_Y]; + } else { + // Interpolate between the previous frame and the current frame. + int frame = Animation.BinarySearch(frames, time, ENTRIES); + x = frames[frame + PREV_X]; + y = frames[frame + PREV_Y]; + float frameTime = frames[frame]; + float percent = GetCurvePercent(frame / ENTRIES - 1, + 1 - (time - frameTime) / (frames[frame + PREV_TIME] - frameTime)); + + x += (frames[frame + X] - x) * percent; + y += (frames[frame + Y] - y) * percent; + } + switch (blend) { + case MixBlend.Setup: + bone.x = bone.data.x + x * alpha; + bone.y = bone.data.y + y * alpha; + break; + case MixBlend.First: + case MixBlend.Replace: + bone.x += (bone.data.x + x - bone.x) * alpha; + bone.y += (bone.data.y + y - bone.y) * alpha; + break; + case MixBlend.Add: + bone.x += x * alpha; + bone.y += y * alpha; + break; + } + } + } + + /// Changes a bone's local and . + public class ScaleTimeline : TranslateTimeline, IBoneTimeline { + public ScaleTimeline (int frameCount) + : base(frameCount) { + } + + override public int PropertyId { + get { return ((int)TimelineType.Scale << 24) + boneIndex; } + } + + override public void Apply (Skeleton skeleton, float lastTime, float time, ExposedList firedEvents, float alpha, MixBlend blend, + MixDirection direction) { + Bone bone = skeleton.bones.Items[boneIndex]; + + float[] frames = this.frames; + if (time < frames[0]) { // Time is before first frame. + switch (blend) { + case MixBlend.Setup: + bone.scaleX = bone.data.scaleX; + bone.scaleY = bone.data.scaleY; + return; + case MixBlend.First: + bone.scaleX += (bone.data.scaleX - bone.scaleX) * alpha; + bone.scaleY += (bone.data.scaleY - bone.scaleY) * alpha; + return; + } + return; + } + + float x, y; + if (time >= frames[frames.Length - ENTRIES]) { // Time is after last frame. + x = frames[frames.Length + PREV_X] * bone.data.scaleX; + y = frames[frames.Length + PREV_Y] * bone.data.scaleY; + } else { + // Interpolate between the previous frame and the current frame. + int frame = Animation.BinarySearch(frames, time, ENTRIES); + x = frames[frame + PREV_X]; + y = frames[frame + PREV_Y]; + float frameTime = frames[frame]; + float percent = GetCurvePercent(frame / ENTRIES - 1, + 1 - (time - frameTime) / (frames[frame + PREV_TIME] - frameTime)); + + x = (x + (frames[frame + X] - x) * percent) * bone.data.scaleX; + y = (y + (frames[frame + Y] - y) * percent) * bone.data.scaleY; + } + if (alpha == 1) { + if (blend == MixBlend.Add) { + bone.scaleX += x - bone.data.scaleX; + bone.scaleY += y - bone.data.scaleY; + } else { + bone.scaleX = x; + bone.scaleY = y; + } + } else { + // Mixing out uses sign of setup or current pose, else use sign of key. + float bx, by; + if (direction == MixDirection.Out) { + switch (blend) { + case MixBlend.Setup: + bx = bone.data.scaleX; + by = bone.data.scaleY; + bone.scaleX = bx + (Math.Abs(x) * Math.Sign(bx) - bx) * alpha; + bone.scaleY = by + (Math.Abs(y) * Math.Sign(by) - by) * alpha; + break; + case MixBlend.First: + case MixBlend.Replace: + bx = bone.scaleX; + by = bone.scaleY; + bone.scaleX = bx + (Math.Abs(x) * Math.Sign(bx) - bx) * alpha; + bone.scaleY = by + (Math.Abs(y) * Math.Sign(by) - by) * alpha; + break; + case MixBlend.Add: + bx = bone.scaleX; + by = bone.scaleY; + bone.scaleX = bx + (Math.Abs(x) * Math.Sign(bx) - bone.data.scaleX) * alpha; + bone.scaleY = by + (Math.Abs(y) * Math.Sign(by) - bone.data.scaleY) * alpha; + break; + } + } else { + switch (blend) { + case MixBlend.Setup: + bx = Math.Abs(bone.data.scaleX) * Math.Sign(x); + by = Math.Abs(bone.data.scaleY) * Math.Sign(y); + bone.scaleX = bx + (x - bx) * alpha; + bone.scaleY = by + (y - by) * alpha; + break; + case MixBlend.First: + case MixBlend.Replace: + bx = Math.Abs(bone.scaleX) * Math.Sign(x); + by = Math.Abs(bone.scaleY) * Math.Sign(y); + bone.scaleX = bx + (x - bx) * alpha; + bone.scaleY = by + (y - by) * alpha; + break; + case MixBlend.Add: + bx = Math.Sign(x); + by = Math.Sign(y); + bone.scaleX = Math.Abs(bone.scaleX) * bx + (x - Math.Abs(bone.data.scaleX) * bx) * alpha; + bone.scaleY = Math.Abs(bone.scaleY) * by + (y - Math.Abs(bone.data.scaleY) * by) * alpha; + break; + } + } + } + } + } + + /// Changes a bone's local and . + public class ShearTimeline : TranslateTimeline, IBoneTimeline { + public ShearTimeline (int frameCount) + : base(frameCount) { + } + + override public int PropertyId { + get { return ((int)TimelineType.Shear << 24) + boneIndex; } + } + + override public void Apply (Skeleton skeleton, float lastTime, float time, ExposedList firedEvents, float alpha, MixBlend blend, + MixDirection direction) { + Bone bone = skeleton.bones.Items[boneIndex]; + float[] frames = this.frames; + if (time < frames[0]) { // Time is before first frame. + switch (blend) { + case MixBlend.Setup: + bone.shearX = bone.data.shearX; + bone.shearY = bone.data.shearY; + return; + case MixBlend.First: + bone.shearX += (bone.data.shearX - bone.shearX) * alpha; + bone.shearY += (bone.data.shearY - bone.shearY) * alpha; + return; + } + return; + } + + float x, y; + if (time >= frames[frames.Length - ENTRIES]) { // Time is after last frame. + x = frames[frames.Length + PREV_X]; + y = frames[frames.Length + PREV_Y]; + } else { + // Interpolate between the previous frame and the current frame. + int frame = Animation.BinarySearch(frames, time, ENTRIES); + x = frames[frame + PREV_X]; + y = frames[frame + PREV_Y]; + float frameTime = frames[frame]; + float percent = GetCurvePercent(frame / ENTRIES - 1, + 1 - (time - frameTime) / (frames[frame + PREV_TIME] - frameTime)); + + x = x + (frames[frame + X] - x) * percent; + y = y + (frames[frame + Y] - y) * percent; + } + switch (blend) { + case MixBlend.Setup: + bone.shearX = bone.data.shearX + x * alpha; + bone.shearY = bone.data.shearY + y * alpha; + break; + case MixBlend.First: + case MixBlend.Replace: + bone.shearX += (bone.data.shearX + x - bone.shearX) * alpha; + bone.shearY += (bone.data.shearY + y - bone.shearY) * alpha; + break; + case MixBlend.Add: + bone.shearX += x * alpha; + bone.shearY += y * alpha; + break; + } + } + } + + /// Changes a slot's . + public class ColorTimeline : CurveTimeline, ISlotTimeline { + public const int ENTRIES = 5; + protected const int PREV_TIME = -5, PREV_R = -4, PREV_G = -3, PREV_B = -2, PREV_A = -1; + protected const int R = 1, G = 2, B = 3, A = 4; + + internal int slotIndex; + internal float[] frames; // time, r, g, b, a, ... + + public ColorTimeline (int frameCount) + : base(frameCount) { + frames = new float[frameCount * ENTRIES]; + } + + override public int PropertyId { + get { return ((int)TimelineType.Color << 24) + slotIndex; } + } + + /// The index of the slot in that will be changed. + public int SlotIndex { + set { + if (value < 0) throw new ArgumentOutOfRangeException("index must be >= 0."); + this.slotIndex = value; + } + get { + return slotIndex; + } + } + /// The time in seconds, red, green, blue, and alpha values for each key frame. + public float[] Frames { get { return frames; } set { frames = value; } } + + /// Sets the time in seconds, red, green, blue, and alpha for the specified key frame. + public void SetFrame (int frameIndex, float time, float r, float g, float b, float a) { + frameIndex *= ENTRIES; + frames[frameIndex] = time; + frames[frameIndex + R] = r; + frames[frameIndex + G] = g; + frames[frameIndex + B] = b; + frames[frameIndex + A] = a; + } + + override public void Apply (Skeleton skeleton, float lastTime, float time, ExposedList firedEvents, float alpha, MixBlend blend, + MixDirection direction) { + Slot slot = skeleton.slots.Items[slotIndex]; + float[] frames = this.frames; + if (time < frames[0]) { // Time is before first frame. + var slotData = slot.data; + switch (blend) { + case MixBlend.Setup: + slot.r = slotData.r; + slot.g = slotData.g; + slot.b = slotData.b; + slot.a = slotData.a; + return; + case MixBlend.First: + slot.r += (slotData.r - slot.r) * alpha; + slot.g += (slotData.g - slot.g) * alpha; + slot.b += (slotData.b - slot.b) * alpha; + slot.a += (slotData.a - slot.a) * alpha; + return; + } + return; + } + + float r, g, b, a; + if (time >= frames[frames.Length - ENTRIES]) { // Time is after last frame. + int i = frames.Length; + r = frames[i + PREV_R]; + g = frames[i + PREV_G]; + b = frames[i + PREV_B]; + a = frames[i + PREV_A]; + } else { + // Interpolate between the previous frame and the current frame. + int frame = Animation.BinarySearch(frames, time, ENTRIES); + r = frames[frame + PREV_R]; + g = frames[frame + PREV_G]; + b = frames[frame + PREV_B]; + a = frames[frame + PREV_A]; + float frameTime = frames[frame]; + float percent = GetCurvePercent(frame / ENTRIES - 1, + 1 - (time - frameTime) / (frames[frame + PREV_TIME] - frameTime)); + + r += (frames[frame + R] - r) * percent; + g += (frames[frame + G] - g) * percent; + b += (frames[frame + B] - b) * percent; + a += (frames[frame + A] - a) * percent; + } + if (alpha == 1) { + slot.r = r; + slot.g = g; + slot.b = b; + slot.a = a; + } else { + float br, bg, bb, ba; + if (blend == MixBlend.Setup) { + br = slot.data.r; + bg = slot.data.g; + bb = slot.data.b; + ba = slot.data.a; + } else { + br = slot.r; + bg = slot.g; + bb = slot.b; + ba = slot.a; + } + slot.r = br + ((r - br) * alpha); + slot.g = bg + ((g - bg) * alpha); + slot.b = bb + ((b - bb) * alpha); + slot.a = ba + ((a - ba) * alpha); + } + } + } + + /// Changes a slot's and for two color tinting. + public class TwoColorTimeline : CurveTimeline, ISlotTimeline { + public const int ENTRIES = 8; + protected const int PREV_TIME = -8, PREV_R = -7, PREV_G = -6, PREV_B = -5, PREV_A = -4; + protected const int PREV_R2 = -3, PREV_G2 = -2, PREV_B2 = -1; + protected const int R = 1, G = 2, B = 3, A = 4, R2 = 5, G2 = 6, B2 = 7; + + internal int slotIndex; + internal float[] frames; // time, r, g, b, a, r2, g2, b2, ... + + public TwoColorTimeline (int frameCount) : + base(frameCount) { + frames = new float[frameCount * ENTRIES]; + } + + override public int PropertyId { + get { return ((int)TimelineType.TwoColor << 24) + slotIndex; } + } + + /// The index of the slot in that will be changed. + public int SlotIndex { + set { + if (value < 0) throw new ArgumentOutOfRangeException("index must be >= 0."); + this.slotIndex = value; + } + get { + return slotIndex; + } + } + /// The time in seconds, red, green, blue, and alpha values for each key frame. + public float[] Frames { get { return frames; } } + + /// Sets the time in seconds, light, and dark colors for the specified key frame.. + public void SetFrame (int frameIndex, float time, float r, float g, float b, float a, float r2, float g2, float b2) { + frameIndex *= ENTRIES; + frames[frameIndex] = time; + frames[frameIndex + R] = r; + frames[frameIndex + G] = g; + frames[frameIndex + B] = b; + frames[frameIndex + A] = a; + frames[frameIndex + R2] = r2; + frames[frameIndex + G2] = g2; + frames[frameIndex + B2] = b2; + } + + override public void Apply (Skeleton skeleton, float lastTime, float time, ExposedList firedEvents, float alpha, MixBlend blend, + MixDirection direction) { + Slot slot = skeleton.slots.Items[slotIndex]; + float[] frames = this.frames; + if (time < frames[0]) { // Time is before first frame. + var slotData = slot.data; + switch (blend) { + case MixBlend.Setup: + // slot.color.set(slot.data.color); + // slot.darkColor.set(slot.data.darkColor); + slot.r = slotData.r; + slot.g = slotData.g; + slot.b = slotData.b; + slot.a = slotData.a; + slot.r2 = slotData.r2; + slot.g2 = slotData.g2; + slot.b2 = slotData.b2; + return; + case MixBlend.First: + slot.r += (slot.r - slotData.r) * alpha; + slot.g += (slot.g - slotData.g) * alpha; + slot.b += (slot.b - slotData.b) * alpha; + slot.a += (slot.a - slotData.a) * alpha; + slot.r2 += (slot.r2 - slotData.r2) * alpha; + slot.g2 += (slot.g2 - slotData.g2) * alpha; + slot.b2 += (slot.b2 - slotData.b2) * alpha; + return; + } + return; + } + + float r, g, b, a, r2, g2, b2; + if (time >= frames[frames.Length - ENTRIES]) { // Time is after last frame. + int i = frames.Length; + r = frames[i + PREV_R]; + g = frames[i + PREV_G]; + b = frames[i + PREV_B]; + a = frames[i + PREV_A]; + r2 = frames[i + PREV_R2]; + g2 = frames[i + PREV_G2]; + b2 = frames[i + PREV_B2]; + } else { + // Interpolate between the previous frame and the current frame. + int frame = Animation.BinarySearch(frames, time, ENTRIES); + r = frames[frame + PREV_R]; + g = frames[frame + PREV_G]; + b = frames[frame + PREV_B]; + a = frames[frame + PREV_A]; + r2 = frames[frame + PREV_R2]; + g2 = frames[frame + PREV_G2]; + b2 = frames[frame + PREV_B2]; + float frameTime = frames[frame]; + float percent = GetCurvePercent(frame / ENTRIES - 1, + 1 - (time - frameTime) / (frames[frame + PREV_TIME] - frameTime)); + + r += (frames[frame + R] - r) * percent; + g += (frames[frame + G] - g) * percent; + b += (frames[frame + B] - b) * percent; + a += (frames[frame + A] - a) * percent; + r2 += (frames[frame + R2] - r2) * percent; + g2 += (frames[frame + G2] - g2) * percent; + b2 += (frames[frame + B2] - b2) * percent; + } + if (alpha == 1) { + slot.r = r; + slot.g = g; + slot.b = b; + slot.a = a; + slot.r2 = r2; + slot.g2 = g2; + slot.b2 = b2; + } else { + float br, bg, bb, ba, br2, bg2, bb2; + if (blend == MixBlend.Setup) { + br = slot.data.r; + bg = slot.data.g; + bb = slot.data.b; + ba = slot.data.a; + br2 = slot.data.r2; + bg2 = slot.data.g2; + bb2 = slot.data.b2; + } else { + br = slot.r; + bg = slot.g; + bb = slot.b; + ba = slot.a; + br2 = slot.r2; + bg2 = slot.g2; + bb2 = slot.b2; + } + slot.r = br + ((r - br) * alpha); + slot.g = bg + ((g - bg) * alpha); + slot.b = bb + ((b - bb) * alpha); + slot.a = ba + ((a - ba) * alpha); + slot.r2 = br2 + ((r2 - br2) * alpha); + slot.g2 = bg2 + ((g2 - bg2) * alpha); + slot.b2 = bb2 + ((b2 - bb2) * alpha); + } + } + + } + + /// Changes a slot's . + public class AttachmentTimeline : Timeline, ISlotTimeline { + internal int slotIndex; + internal float[] frames; // time, ... + internal string[] attachmentNames; + + public AttachmentTimeline (int frameCount) { + frames = new float[frameCount]; + attachmentNames = new String[frameCount]; + } + + public int PropertyId { + get { return ((int)TimelineType.Attachment << 24) + slotIndex; } + } + + /// The number of key frames for this timeline. + public int FrameCount { get { return frames.Length; } } + + /// The index of the slot in that will be changed. + public int SlotIndex { + set { + if (value < 0) throw new ArgumentOutOfRangeException("index must be >= 0."); + this.slotIndex = value; + } + get { + return slotIndex; + } + } + + /// The time in seconds for each key frame. + public float[] Frames { get { return frames; } set { frames = value; } } + + /// The attachment name for each key frame. May contain null values to clear the attachment. + public string[] AttachmentNames { get { return attachmentNames; } set { attachmentNames = value; } } + + /// Sets the time in seconds and the attachment name for the specified key frame. + public void SetFrame (int frameIndex, float time, String attachmentName) { + frames[frameIndex] = time; + attachmentNames[frameIndex] = attachmentName; + } + + public void Apply (Skeleton skeleton, float lastTime, float time, ExposedList firedEvents, float alpha, MixBlend blend, + MixDirection direction) { + string attachmentName; + Slot slot = skeleton.slots.Items[slotIndex]; + if (direction == MixDirection.Out && blend == MixBlend.Setup) { + attachmentName = slot.data.attachmentName; + slot.Attachment = attachmentName == null ? null : skeleton.GetAttachment(slotIndex, attachmentName); + return; + } + + float[] frames = this.frames; + if (time < frames[0]) { // Time is before first frame. + if (blend == MixBlend.Setup || blend == MixBlend.First) { + attachmentName = slot.data.attachmentName; + slot.Attachment = attachmentName == null ? null : skeleton.GetAttachment(slotIndex, attachmentName); + } + return; + } + + int frameIndex; + if (time >= frames[frames.Length - 1]) // Time is after last frame. + frameIndex = frames.Length - 1; + else + frameIndex = Animation.BinarySearch(frames, time) - 1; + + attachmentName = attachmentNames[frameIndex]; + slot.Attachment = attachmentName == null ? null : skeleton.GetAttachment(slotIndex, attachmentName); + } + } + + /// Changes a slot's to deform a . + public class DeformTimeline : CurveTimeline, ISlotTimeline { + internal int slotIndex; + internal VertexAttachment attachment; + internal float[] frames; // time, ... + internal float[][] frameVertices; + + public DeformTimeline (int frameCount) + : base(frameCount) { + frames = new float[frameCount]; + frameVertices = new float[frameCount][]; + } + + override public int PropertyId { + get { return ((int)TimelineType.Deform << 27) + attachment.id + slotIndex; } + } + + /// The index of the slot in that will be changed. + public int SlotIndex { + set { + if (value < 0) throw new ArgumentOutOfRangeException("index must be >= 0."); + this.slotIndex = value; + } + get { + return slotIndex; + } + } + /// The attachment that will be deformed. + public VertexAttachment Attachment { get { return attachment; } set { attachment = value; } } + + /// The time in seconds for each key frame. + public float[] Frames { get { return frames; } set { frames = value; } } + + /// The vertices for each key frame. + public float[][] Vertices { get { return frameVertices; } set { frameVertices = value; } } + + + /// Sets the time in seconds and the vertices for the specified key frame. + /// Vertex positions for an unweighted VertexAttachment, or deform offsets if it has weights. + public void SetFrame (int frameIndex, float time, float[] vertices) { + frames[frameIndex] = time; + frameVertices[frameIndex] = vertices; + } + + override public void Apply (Skeleton skeleton, float lastTime, float time, ExposedList firedEvents, float alpha, MixBlend blend, + MixDirection direction) { + Slot slot = skeleton.slots.Items[slotIndex]; + VertexAttachment vertexAttachment = slot.attachment as VertexAttachment; + if (vertexAttachment == null || !vertexAttachment.ApplyDeform(attachment)) return; + + var verticesArray = slot.attachmentVertices; + if (verticesArray.Count == 0) blend = MixBlend.Setup; + + float[][] frameVertices = this.frameVertices; + int vertexCount = frameVertices[0].Length; + float[] frames = this.frames; + float[] vertices; + + if (time < frames[0]) { // Time is before first frame. + + switch (blend) { + case MixBlend.Setup: + verticesArray.Clear(); + return; + case MixBlend.Replace: + if (alpha == 1) { + verticesArray.Clear(); + return; + } + + // verticesArray.SetSize(vertexCount) // Ensure size and preemptively set count. + if (verticesArray.Capacity < vertexCount) verticesArray.Capacity = vertexCount; + verticesArray.Count = vertexCount; + vertices = verticesArray.Items; + + if (vertexAttachment.bones == null) { + // Unweighted vertex positions. + float[] setupVertices = vertexAttachment.vertices; + for (int i = 0; i < vertexCount; i++) + vertices[i] += (setupVertices[i] - vertices[i]) * alpha; + } else { + // Weighted deform offsets. + alpha = 1 - alpha; + for (int i = 0; i < vertexCount; i++) + vertices[i] *= alpha; + } + return; + default: + return; + } + + } + + // verticesArray.SetSize(vertexCount) // Ensure size and preemptively set count. + if (verticesArray.Capacity < vertexCount) verticesArray.Capacity = vertexCount; + verticesArray.Count = vertexCount; + vertices = verticesArray.Items; + + if (time >= frames[frames.Length - 1]) { // Time is after last frame. + + float[] lastVertices = frameVertices[frames.Length - 1]; + if (alpha == 1) { + if (blend == MixBlend.Add) { + if (vertexAttachment.bones == null) { + // Unweighted vertex positions, no alpha. + float[] setupVertices = vertexAttachment.vertices; + for (int i = 0; i < vertexCount; i++) + vertices[i] += lastVertices[i] - setupVertices[i]; + } else { + // Weighted deform offsets, no alpha. + for (int i = 0; i < vertexCount; i++) + vertices[i] += lastVertices[i]; + } + } else { + // Vertex positions or deform offsets, no alpha. + Array.Copy(lastVertices, 0, vertices, 0, vertexCount); + } + } else { + switch (blend) { + case MixBlend.Setup: { + if (vertexAttachment.bones == null) { + // Unweighted vertex positions, with alpha. + float[] setupVertices = vertexAttachment.vertices; + for (int i = 0; i < vertexCount; i++) { + float setup = setupVertices[i]; + vertices[i] = setup + (lastVertices[i] - setup) * alpha; + } + } else { + // Weighted deform offsets, with alpha. + for (int i = 0; i < vertexCount; i++) + vertices[i] = lastVertices[i] * alpha; + } + break; + } + case MixBlend.First: + case MixBlend.Replace: + // Vertex positions or deform offsets, with alpha. + for (int i = 0; i < vertexCount; i++) + vertices[i] += (lastVertices[i] - vertices[i]) * alpha; + break; + case MixBlend.Add: + if (vertexAttachment.bones == null) { + // Unweighted vertex positions, no alpha. + float[] setupVertices = vertexAttachment.vertices; + for (int i = 0; i < vertexCount; i++) + vertices[i] += (lastVertices[i] - setupVertices[i]) * alpha; + } else { + // Weighted deform offsets, alpha. + for (int i = 0; i < vertexCount; i++) + vertices[i] += lastVertices[i] * alpha; + } + break; + } + } + return; + } + + // Interpolate between the previous frame and the current frame. + int frame = Animation.BinarySearch(frames, time); + float[] prevVertices = frameVertices[frame - 1]; + float[] nextVertices = frameVertices[frame]; + float frameTime = frames[frame]; + float percent = GetCurvePercent(frame - 1, 1 - (time - frameTime) / (frames[frame - 1] - frameTime)); + + if (alpha == 1) { + if (blend == MixBlend.Add) { + if (vertexAttachment.bones == null) { + // Unweighted vertex positions, no alpha. + float[] setupVertices = vertexAttachment.vertices; + for (int i = 0; i < vertexCount; i++) { + float prev = prevVertices[i]; + vertices[i] += prev + (nextVertices[i] - prev) * percent - setupVertices[i]; + } + } else { + // Weighted deform offsets, no alpha. + for (int i = 0; i < vertexCount; i++) { + float prev = prevVertices[i]; + vertices[i] += prev + (nextVertices[i] - prev) * percent; + } + } + } else { + // Vertex positions or deform offsets, no alpha. + for (int i = 0; i < vertexCount; i++) { + float prev = prevVertices[i]; + vertices[i] = prev + (nextVertices[i] - prev) * percent; + } + } + } else { + switch (blend) { + case MixBlend.Setup: { + if (vertexAttachment.bones == null) { + // Unweighted vertex positions, with alpha. + float[] setupVertices = vertexAttachment.vertices; + for (int i = 0; i < vertexCount; i++) { + float prev = prevVertices[i], setup = setupVertices[i]; + vertices[i] = setup + (prev + (nextVertices[i] - prev) * percent - setup) * alpha; + } + } else { + // Weighted deform offsets, with alpha. + for (int i = 0; i < vertexCount; i++) { + float prev = prevVertices[i]; + vertices[i] = (prev + (nextVertices[i] - prev) * percent) * alpha; + } + } + break; + } + case MixBlend.First: + case MixBlend.Replace: { + // Vertex positions or deform offsets, with alpha. + for (int i = 0; i < vertexCount; i++) { + float prev = prevVertices[i]; + vertices[i] += (prev + (nextVertices[i] - prev) * percent - vertices[i]) * alpha; + } + break; + } + case MixBlend.Add: { + if (vertexAttachment.bones == null) { + // Unweighted vertex positions, with alpha. + float[] setupVertices = vertexAttachment.vertices; + for (int i = 0; i < vertexCount; i++) { + float prev = prevVertices[i]; + vertices[i] += (prev + (nextVertices[i] - prev) * percent - setupVertices[i]) * alpha; + } + } else { + // Weighted deform offsets, with alpha. + for (int i = 0; i < vertexCount; i++) { + float prev = prevVertices[i]; + vertices[i] += (prev + (nextVertices[i] - prev) * percent) * alpha; + } + } + break; + } + } + } + } + } + + /// Fires an when specific animation times are reached. + public class EventTimeline : Timeline { + internal float[] frames; // time, ... + private Event[] events; + + public EventTimeline (int frameCount) { + frames = new float[frameCount]; + events = new Event[frameCount]; + } + + public int PropertyId { + get { return ((int)TimelineType.Event << 24); } + } + + /// The number of key frames for this timeline. + public int FrameCount { get { return frames.Length; } } + + /// The time in seconds for each key frame. + public float[] Frames { get { return frames; } set { frames = value; } } + + /// The event for each key frame. + public Event[] Events { get { return events; } set { events = value; } } + + /// Sets the time in seconds and the event for the specified key frame. + public void SetFrame (int frameIndex, Event e) { + frames[frameIndex] = e.Time; + events[frameIndex] = e; + } + + /// Fires events for frames > lastTime and <= time. + public void Apply (Skeleton skeleton, float lastTime, float time, ExposedList firedEvents, float alpha, MixBlend blend, + MixDirection direction) { + if (firedEvents == null) return; + float[] frames = this.frames; + int frameCount = frames.Length; + + if (lastTime > time) { // Fire events after last time for looped animations. + Apply(skeleton, lastTime, int.MaxValue, firedEvents, alpha, blend, direction); + lastTime = -1f; + } else if (lastTime >= frames[frameCount - 1]) // Last time is after last frame. + return; + if (time < frames[0]) return; // Time is before first frame. + + int frame; + if (lastTime < frames[0]) + frame = 0; + else { + frame = Animation.BinarySearch(frames, lastTime); + float frameTime = frames[frame]; + while (frame > 0) { // Fire multiple events with the same frame. + if (frames[frame - 1] != frameTime) break; + frame--; + } + } + for (; frame < frameCount && time >= frames[frame]; frame++) + firedEvents.Add(events[frame]); + } + } + + /// Changes a skeleton's . + public class DrawOrderTimeline : Timeline { + internal float[] frames; // time, ... + private int[][] drawOrders; + + public DrawOrderTimeline (int frameCount) { + frames = new float[frameCount]; + drawOrders = new int[frameCount][]; + } + + public int PropertyId { + get { return ((int)TimelineType.DrawOrder << 24); } + } + + /// The number of key frames for this timeline. + public int FrameCount { get { return frames.Length; } } + + /// The time in seconds for each key frame. + public float[] Frames { get { return frames; } set { frames = value; } } // time, ... + + /// The draw order for each key frame. + /// . + public int[][] DrawOrders { get { return drawOrders; } set { drawOrders = value; } } + + /// Sets the time in seconds and the draw order for the specified key frame. + /// For each slot in the index of the new draw order. May be null to use setup pose + /// draw order.. + public void SetFrame (int frameIndex, float time, int[] drawOrder) { + frames[frameIndex] = time; + drawOrders[frameIndex] = drawOrder; + } + + public void Apply (Skeleton skeleton, float lastTime, float time, ExposedList firedEvents, float alpha, MixBlend blend, + MixDirection direction) { + ExposedList drawOrder = skeleton.drawOrder; + ExposedList slots = skeleton.slots; + if (direction == MixDirection.Out && blend == MixBlend.Setup) { + Array.Copy(slots.Items, 0, drawOrder.Items, 0, slots.Count); + return; + } + + float[] frames = this.frames; + if (time < frames[0]) { // Time is before first frame. + if (blend == MixBlend.Setup || blend == MixBlend.First) Array.Copy(slots.Items, 0, drawOrder.Items, 0, slots.Count); + return; + } + + int frame; + if (time >= frames[frames.Length - 1]) // Time is after last frame. + frame = frames.Length - 1; + else + frame = Animation.BinarySearch(frames, time) - 1; + + int[] drawOrderToSetupIndex = drawOrders[frame]; + if (drawOrderToSetupIndex == null) { + Array.Copy(slots.Items, 0, drawOrder.Items, 0, slots.Count); + } else { + var drawOrderItems = drawOrder.Items; + var slotsItems = slots.Items; + for (int i = 0, n = drawOrderToSetupIndex.Length; i < n; i++) + drawOrderItems[i] = slotsItems[drawOrderToSetupIndex[i]]; + } + } + } + + /// Changes an IK constraint's , , + /// , and . + public class IkConstraintTimeline : CurveTimeline { + public const int ENTRIES = 5; + private const int PREV_TIME = -5, PREV_MIX = -4, PREV_BEND_DIRECTION = -3, PREV_COMPRESS = -2, PREV_STRETCH = -1; + private const int MIX = 1, BEND_DIRECTION = 2, COMPRESS = 3, STRETCH = 4; + + internal int ikConstraintIndex; + internal float[] frames; // time, mix, bendDirection, compress, stretch, ... + + public IkConstraintTimeline (int frameCount) + : base(frameCount) { + frames = new float[frameCount * ENTRIES]; + } + + override public int PropertyId { + get { return ((int)TimelineType.IkConstraint << 24) + ikConstraintIndex; } + } + + /// The index of the IK constraint slot in that will be changed. + public int IkConstraintIndex { + set { + if (value < 0) throw new ArgumentOutOfRangeException("index must be >= 0."); + this.ikConstraintIndex = value; + } + get { + return ikConstraintIndex; + } + } + + /// The time in seconds, mix, bend direction, compress, and stretch for each key frame. + public float[] Frames { get { return frames; } set { frames = value; } } + + /// Sets the time in seconds, mix, bend direction, compress, and stretch for the specified key frame. + public void SetFrame (int frameIndex, float time, float mix, int bendDirection, bool compress, bool stretch) { + frameIndex *= ENTRIES; + frames[frameIndex] = time; + frames[frameIndex + MIX] = mix; + frames[frameIndex + BEND_DIRECTION] = bendDirection; + frames[frameIndex + COMPRESS] = compress ? 1 : 0; + frames[frameIndex + STRETCH] = stretch ? 1 : 0; + } + + override public void Apply (Skeleton skeleton, float lastTime, float time, ExposedList firedEvents, float alpha, MixBlend blend, + MixDirection direction) { + IkConstraint constraint = skeleton.ikConstraints.Items[ikConstraintIndex]; + float[] frames = this.frames; + if (time < frames[0]) { // Time is before first frame. + switch (blend) { + case MixBlend.Setup: + constraint.mix = constraint.data.mix; + constraint.bendDirection = constraint.data.bendDirection; + constraint.compress = constraint.data.compress; + constraint.stretch = constraint.data.stretch; + return; + case MixBlend.First: + constraint.mix += (constraint.data.mix - constraint.mix) * alpha; + constraint.bendDirection = constraint.data.bendDirection; + constraint.compress = constraint.data.compress; + constraint.stretch = constraint.data.stretch; + return; + } + return; + } + + if (time >= frames[frames.Length - ENTRIES]) { // Time is after last frame. + if (blend == MixBlend.Setup) { + constraint.mix = constraint.data.mix + (frames[frames.Length + PREV_MIX] - constraint.data.mix) * alpha; + if (direction == MixDirection.Out) { + constraint.bendDirection = constraint.data.bendDirection; + constraint.compress = constraint.data.compress; + constraint.stretch = constraint.data.stretch; + } else { + constraint.bendDirection = (int)frames[frames.Length + PREV_BEND_DIRECTION]; + constraint.compress = frames[frames.Length + PREV_COMPRESS] != 0; + constraint.stretch = frames[frames.Length + PREV_STRETCH] != 0; + } + } else { + constraint.mix += (frames[frames.Length + PREV_MIX] - constraint.mix) * alpha; + if (direction == MixDirection.In) { + constraint.bendDirection = (int)frames[frames.Length + PREV_BEND_DIRECTION]; + constraint.compress = frames[frames.Length + PREV_COMPRESS] != 0; + constraint.stretch = frames[frames.Length + PREV_STRETCH] != 0; + } + } + return; + } + + // Interpolate between the previous frame and the current frame. + int frame = Animation.BinarySearch(frames, time, ENTRIES); + float mix = frames[frame + PREV_MIX]; + float frameTime = frames[frame]; + float percent = GetCurvePercent(frame / ENTRIES - 1, 1 - (time - frameTime) / (frames[frame + PREV_TIME] - frameTime)); + + if (blend == MixBlend.Setup) { + constraint.mix = constraint.data.mix + (mix + (frames[frame + MIX] - mix) * percent - constraint.data.mix) * alpha; + if (direction == MixDirection.Out) { + constraint.bendDirection = constraint.data.bendDirection; + constraint.compress = constraint.data.compress; + constraint.stretch = constraint.data.stretch; + } else { + constraint.bendDirection = (int)frames[frame + PREV_BEND_DIRECTION]; + constraint.compress = frames[frame + PREV_COMPRESS] != 0; + constraint.stretch = frames[frame + PREV_STRETCH] != 0; + } + } else { + constraint.mix += (mix + (frames[frame + MIX] - mix) * percent - constraint.mix) * alpha; + if (direction == MixDirection.In) { + constraint.bendDirection = (int)frames[frame + PREV_BEND_DIRECTION]; + constraint.compress = frames[frame + PREV_COMPRESS] != 0; + constraint.stretch = frames[frame + PREV_STRETCH] != 0; + } + } + } + } + + /// Changes a transform constraint's mixes. + public class TransformConstraintTimeline : CurveTimeline { + public const int ENTRIES = 5; + private const int PREV_TIME = -5, PREV_ROTATE = -4, PREV_TRANSLATE = -3, PREV_SCALE = -2, PREV_SHEAR = -1; + private const int ROTATE = 1, TRANSLATE = 2, SCALE = 3, SHEAR = 4; + + internal int transformConstraintIndex; + internal float[] frames; // time, rotate mix, translate mix, scale mix, shear mix, ... + + public TransformConstraintTimeline (int frameCount) + : base(frameCount) { + frames = new float[frameCount * ENTRIES]; + } + + override public int PropertyId { + get { return ((int)TimelineType.TransformConstraint << 24) + transformConstraintIndex; } + } + + /// The index of the transform constraint slot in that will be changed. + public int TransformConstraintIndex { + set { + if (value < 0) throw new ArgumentOutOfRangeException("index must be >= 0."); + this.transformConstraintIndex = value; + } + get { + return transformConstraintIndex; + } + } + + /// The time in seconds, rotate mix, translate mix, scale mix, and shear mix for each key frame. + public float[] Frames { get { return frames; } set { frames = value; } } // time, rotate mix, translate mix, scale mix, shear mix, ... + + /// The time in seconds, rotate mix, translate mix, scale mix, and shear mix for the specified key frame. + public void SetFrame (int frameIndex, float time, float rotateMix, float translateMix, float scaleMix, float shearMix) { + frameIndex *= ENTRIES; + frames[frameIndex] = time; + frames[frameIndex + ROTATE] = rotateMix; + frames[frameIndex + TRANSLATE] = translateMix; + frames[frameIndex + SCALE] = scaleMix; + frames[frameIndex + SHEAR] = shearMix; + } + + override public void Apply (Skeleton skeleton, float lastTime, float time, ExposedList firedEvents, float alpha, MixBlend blend, + MixDirection direction) { + TransformConstraint constraint = skeleton.transformConstraints.Items[transformConstraintIndex]; + float[] frames = this.frames; + if (time < frames[0]) { // Time is before first frame. + TransformConstraintData data = constraint.data; + switch (blend) { + case MixBlend.Setup: + constraint.rotateMix = data.rotateMix; + constraint.translateMix = data.translateMix; + constraint.scaleMix = data.scaleMix; + constraint.shearMix = data.shearMix; + return; + case MixBlend.First: + constraint.rotateMix += (data.rotateMix - constraint.rotateMix) * alpha; + constraint.translateMix += (data.translateMix - constraint.translateMix) * alpha; + constraint.scaleMix += (data.scaleMix - constraint.scaleMix) * alpha; + constraint.shearMix += (data.shearMix - constraint.shearMix) * alpha; + return; + } + return; + } + + float rotate, translate, scale, shear; + if (time >= frames[frames.Length - ENTRIES]) { // Time is after last frame. + int i = frames.Length; + rotate = frames[i + PREV_ROTATE]; + translate = frames[i + PREV_TRANSLATE]; + scale = frames[i + PREV_SCALE]; + shear = frames[i + PREV_SHEAR]; + } else { + // Interpolate between the previous frame and the current frame. + int frame = Animation.BinarySearch(frames, time, ENTRIES); + rotate = frames[frame + PREV_ROTATE]; + translate = frames[frame + PREV_TRANSLATE]; + scale = frames[frame + PREV_SCALE]; + shear = frames[frame + PREV_SHEAR]; + float frameTime = frames[frame]; + float percent = GetCurvePercent(frame / ENTRIES - 1, + 1 - (time - frameTime) / (frames[frame + PREV_TIME] - frameTime)); + + rotate += (frames[frame + ROTATE] - rotate) * percent; + translate += (frames[frame + TRANSLATE] - translate) * percent; + scale += (frames[frame + SCALE] - scale) * percent; + shear += (frames[frame + SHEAR] - shear) * percent; + } + if (blend == MixBlend.Setup) { + TransformConstraintData data = constraint.data; + constraint.rotateMix = data.rotateMix + (rotate - data.rotateMix) * alpha; + constraint.translateMix = data.translateMix + (translate - data.translateMix) * alpha; + constraint.scaleMix = data.scaleMix + (scale - data.scaleMix) * alpha; + constraint.shearMix = data.shearMix + (shear - data.shearMix) * alpha; + } else { + constraint.rotateMix += (rotate - constraint.rotateMix) * alpha; + constraint.translateMix += (translate - constraint.translateMix) * alpha; + constraint.scaleMix += (scale - constraint.scaleMix) * alpha; + constraint.shearMix += (shear - constraint.shearMix) * alpha; + } + } + } + + /// Changes a path constraint's . + public class PathConstraintPositionTimeline : CurveTimeline { + public const int ENTRIES = 2; + protected const int PREV_TIME = -2, PREV_VALUE = -1; + protected const int VALUE = 1; + + internal int pathConstraintIndex; + internal float[] frames; // time, position, ... + + public PathConstraintPositionTimeline (int frameCount) + : base(frameCount) { + frames = new float[frameCount * ENTRIES]; + } + + override public int PropertyId { + get { return ((int)TimelineType.PathConstraintPosition << 24) + pathConstraintIndex; } + } + + /// The index of the path constraint slot in that will be changed. + public int PathConstraintIndex { + set { + if (value < 0) throw new ArgumentOutOfRangeException("index must be >= 0."); + this.pathConstraintIndex = value; + } + get { + return pathConstraintIndex; + } + } + + /// The time in seconds and path constraint position for each key frame. + public float[] Frames { get { return frames; } set { frames = value; } } // time, position, ... + + /// Sets the time in seconds and path constraint position for the specified key frame. + public void SetFrame (int frameIndex, float time, float position) { + frameIndex *= ENTRIES; + frames[frameIndex] = time; + frames[frameIndex + VALUE] = position; + } + + override public void Apply (Skeleton skeleton, float lastTime, float time, ExposedList firedEvents, float alpha, MixBlend blend, + MixDirection direction) { + PathConstraint constraint = skeleton.pathConstraints.Items[pathConstraintIndex]; + float[] frames = this.frames; + if (time < frames[0]) { // Time is before first frame. + switch (blend) { + case MixBlend.Setup: + constraint.position = constraint.data.position; + return; + case MixBlend.First: + constraint.position += (constraint.data.position - constraint.position) * alpha; + return; + } + return; + } + + float position; + if (time >= frames[frames.Length - ENTRIES]) // Time is after last frame. + position = frames[frames.Length + PREV_VALUE]; + else { + // Interpolate between the previous frame and the current frame. + int frame = Animation.BinarySearch(frames, time, ENTRIES); + position = frames[frame + PREV_VALUE]; + float frameTime = frames[frame]; + float percent = GetCurvePercent(frame / ENTRIES - 1, + 1 - (time - frameTime) / (frames[frame + PREV_TIME] - frameTime)); + + position += (frames[frame + VALUE] - position) * percent; + } + if (blend == MixBlend.Setup) + constraint.position = constraint.data.position + (position - constraint.data.position) * alpha; + else + constraint.position += (position - constraint.position) * alpha; + } + } + + /// Changes a path constraint's . + public class PathConstraintSpacingTimeline : PathConstraintPositionTimeline { + public PathConstraintSpacingTimeline (int frameCount) + : base(frameCount) { + } + + override public int PropertyId { + get { return ((int)TimelineType.PathConstraintSpacing << 24) + pathConstraintIndex; } + } + + override public void Apply (Skeleton skeleton, float lastTime, float time, ExposedList events, float alpha, MixBlend blend, + MixDirection direction) { + PathConstraint constraint = skeleton.pathConstraints.Items[pathConstraintIndex]; + float[] frames = this.frames; + if (time < frames[0]) { // Time is before first frame. + switch (blend) { + case MixBlend.Setup: + constraint.spacing = constraint.data.spacing; + return; + case MixBlend.First: + constraint.spacing += (constraint.data.spacing - constraint.spacing) * alpha; + return; + } + return; + } + + float spacing; + if (time >= frames[frames.Length - ENTRIES]) // Time is after last frame. + spacing = frames[frames.Length + PREV_VALUE]; + else { + // Interpolate between the previous frame and the current frame. + int frame = Animation.BinarySearch(frames, time, ENTRIES); + spacing = frames[frame + PREV_VALUE]; + float frameTime = frames[frame]; + float percent = GetCurvePercent(frame / ENTRIES - 1, + 1 - (time - frameTime) / (frames[frame + PREV_TIME] - frameTime)); + + spacing += (frames[frame + VALUE] - spacing) * percent; + } + + if (blend == MixBlend.Setup) + constraint.spacing = constraint.data.spacing + (spacing - constraint.data.spacing) * alpha; + else + constraint.spacing += (spacing - constraint.spacing) * alpha; + } + } + + /// Changes a path constraint's mixes. + public class PathConstraintMixTimeline : CurveTimeline { + public const int ENTRIES = 3; + private const int PREV_TIME = -3, PREV_ROTATE = -2, PREV_TRANSLATE = -1; + private const int ROTATE = 1, TRANSLATE = 2; + + internal int pathConstraintIndex; + internal float[] frames; // time, rotate mix, translate mix, ... + + public PathConstraintMixTimeline (int frameCount) + : base(frameCount) { + frames = new float[frameCount * ENTRIES]; + } + + override public int PropertyId { + get { return ((int)TimelineType.PathConstraintMix << 24) + pathConstraintIndex; } + } + + /// The index of the path constraint slot in that will be changed. + public int PathConstraintIndex { + set { + if (value < 0) throw new ArgumentOutOfRangeException("index must be >= 0."); + this.pathConstraintIndex = value; + } + get { + return pathConstraintIndex; + } + } + + /// The time in seconds, rotate mix, and translate mix for each key frame. + public float[] Frames { get { return frames; } set { frames = value; } } // time, rotate mix, translate mix, ... + + /// The time in seconds, rotate mix, and translate mix for the specified key frame. + public void SetFrame (int frameIndex, float time, float rotateMix, float translateMix) { + frameIndex *= ENTRIES; + frames[frameIndex] = time; + frames[frameIndex + ROTATE] = rotateMix; + frames[frameIndex + TRANSLATE] = translateMix; + } + + override public void Apply (Skeleton skeleton, float lastTime, float time, ExposedList firedEvents, float alpha, MixBlend blend, + MixDirection direction) { + PathConstraint constraint = skeleton.pathConstraints.Items[pathConstraintIndex]; + float[] frames = this.frames; + if (time < frames[0]) { // Time is before first frame. + switch (blend) { + case MixBlend.Setup: + constraint.rotateMix = constraint.data.rotateMix; + constraint.translateMix = constraint.data.translateMix; + return; + case MixBlend.First: + constraint.rotateMix += (constraint.data.rotateMix - constraint.rotateMix) * alpha; + constraint.translateMix += (constraint.data.translateMix - constraint.translateMix) * alpha; + return; + } + return; + } + + float rotate, translate; + if (time >= frames[frames.Length - ENTRIES]) { // Time is after last frame. + rotate = frames[frames.Length + PREV_ROTATE]; + translate = frames[frames.Length + PREV_TRANSLATE]; + } else { + // Interpolate between the previous frame and the current frame. + int frame = Animation.BinarySearch(frames, time, ENTRIES); + rotate = frames[frame + PREV_ROTATE]; + translate = frames[frame + PREV_TRANSLATE]; + float frameTime = frames[frame]; + float percent = GetCurvePercent(frame / ENTRIES - 1, + 1 - (time - frameTime) / (frames[frame + PREV_TIME] - frameTime)); + + rotate += (frames[frame + ROTATE] - rotate) * percent; + translate += (frames[frame + TRANSLATE] - translate) * percent; + } + + if (blend == MixBlend.Setup) { + constraint.rotateMix = constraint.data.rotateMix + (rotate - constraint.data.rotateMix) * alpha; + constraint.translateMix = constraint.data.translateMix + (translate - constraint.data.translateMix) * alpha; + } else { + constraint.rotateMix += (rotate - constraint.rotateMix) * alpha; + constraint.translateMix += (translate - constraint.translateMix) * alpha; + } + } + } +} diff --git a/SpineRuntimes/SpineRuntime37/AnimationState.cs b/SpineRuntimes/SpineRuntime37/AnimationState.cs new file mode 100644 index 0000000..10e114c --- /dev/null +++ b/SpineRuntimes/SpineRuntime37/AnimationState.cs @@ -0,0 +1,1259 @@ +/****************************************************************************** + * Spine Runtimes License Agreement + * Last updated May 1, 2019. Replaces all prior versions. + * + * Copyright (c) 2013-2019, Esoteric Software LLC + * + * Integration of the Spine Runtimes into software or otherwise creating + * derivative works of the Spine Runtimes is permitted under the terms and + * conditions of Section 2 of the Spine Editor License Agreement: + * http://esotericsoftware.com/spine-editor-license + * + * Otherwise, it is permitted to integrate the Spine Runtimes into software + * or otherwise create derivative works of the Spine Runtimes (collectively, + * "Products"), provided that each user of the Products must obtain their own + * Spine Editor license and redistribution of the Products in any form must + * include this license and copyright notice. + * + * THIS SOFTWARE IS PROVIDED BY ESOTERIC SOFTWARE LLC "AS IS" AND ANY EXPRESS + * OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES + * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN + * NO EVENT SHALL ESOTERIC SOFTWARE LLC BE LIABLE FOR ANY DIRECT, INDIRECT, + * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, + * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES, BUSINESS + * INTERRUPTION, OR LOSS OF USE, DATA, OR PROFITS) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, + * EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + *****************************************************************************/ + +using System; +using System.Collections.Generic; + +namespace SpineRuntime37 { + + /// + /// + /// Applies animations over time, queues animations for later playback, mixes (crossfading) between animations, and applies + /// multiple animations on top of each other (layering). + /// + /// See Applying Animations in the Spine Runtimes Guide. + /// + public class AnimationState { + static readonly Animation EmptyAnimation = new Animation("", new ExposedList(), 0); + + /// 1) A previously applied timeline has set this property. + /// Result: Mix from the current pose to the timeline pose. + internal const int Subsequent = 0; + /// 1) This is the first timeline to set this property. + /// 2) The next track entry applied after this one does not have a timeline to set this property. + /// Result: Mix from the setup pose to the timeline pose. + internal const int First = 1; + /// 1) This is the first timeline to set this property. + /// 2) The next track entry to be applied does have a timeline to set this property. + /// 3) The next track entry after that one does not have a timeline to set this property. + /// Result: Mix from the setup pose to the timeline pose, but do not mix out. This avoids "dipping" when crossfading animations + /// that key the same property. A subsequent timeline will set this property using a mix. + internal const int Hold = 2; + /// 1) This is the first timeline to set this property. + /// 2) The next track entry to be applied does have a timeline to set this property. + /// 3) The next track entry after that one does have a timeline to set this property. + /// 4) timelineHoldMix stores the first subsequent track entry that does not have a timeline to set this property. + /// Result: The same as HOLD except the mix percentage from the timelineHoldMix track entry is used. This handles when more than + /// 2 track entries in a row have a timeline that sets the same property. + /// Eg, A -> B -> C -> D where A, B, and C have a timeline setting same property, but D does not. When A is applied, to avoid + /// "dipping" A is not mixed out, however D (the first entry that doesn't set the property) mixing in is used to mix out A + /// (which affects B and C). Without using D to mix out, A would be applied fully until mixing completes, then snap into + /// place. + internal const int HoldMix = 3; + + protected AnimationStateData data; + private readonly ExposedList tracks = new ExposedList(); + private readonly ExposedList events = new ExposedList(); + + // difference to libgdx reference: delegates are used for event callbacks instead of 'Array listeners'. + internal void OnStart (TrackEntry entry) { if (Start != null) Start(entry); } + internal void OnInterrupt (TrackEntry entry) { if (Interrupt != null) Interrupt(entry); } + internal void OnEnd (TrackEntry entry) { if (End != null) End(entry); } + internal void OnDispose (TrackEntry entry) { if (Dispose != null) Dispose(entry); } + internal void OnComplete (TrackEntry entry) { if (Complete != null) Complete(entry); } + internal void OnEvent (TrackEntry entry, Event e) { if (Event != null) Event(entry, e); } + + public delegate void TrackEntryDelegate(TrackEntry trackEntry); + public event TrackEntryDelegate Start, Interrupt, End, Dispose, Complete; + + public delegate void TrackEntryEventDelegate(TrackEntry trackEntry, Event e); + public event TrackEntryEventDelegate Event; + + private readonly EventQueue queue; // Initialized by constructor. + private readonly HashSet propertyIDs = new HashSet(); + private bool animationsChanged; + private float timeScale = 1; + + private readonly Pool trackEntryPool = new Pool(); + + public AnimationState(AnimationStateData data) { + if (data == null) throw new ArgumentNullException("data", "data cannot be null."); + this.data = data; + this.queue = new EventQueue( + this, + delegate { this.animationsChanged = true; }, + trackEntryPool + ); + } + + /// + /// Increments the track entry , setting queued animations as current if needed. + /// delta time + public void Update (float delta) { + delta *= timeScale; + var tracksItems = tracks.Items; + for (int i = 0, n = tracks.Count; i < n; i++) { + TrackEntry current = tracksItems[i]; + if (current == null) continue; + + current.animationLast = current.nextAnimationLast; + current.trackLast = current.nextTrackLast; + + float currentDelta = delta * current.timeScale; + + if (current.delay > 0) { + current.delay -= currentDelta; + if (current.delay > 0) continue; + currentDelta = -current.delay; + current.delay = 0; + } + + TrackEntry next = current.next; + if (next != null) { + // When the next entry's delay is passed, change to the next entry, preserving leftover time. + float nextTime = current.trackLast - next.delay; + if (nextTime >= 0) { + next.delay = 0; + next.trackTime = current.timeScale == 0 ? 0 : (nextTime / current.timeScale + delta) * next.timeScale; + current.trackTime += currentDelta; + SetCurrent(i, next, true); + while (next.mixingFrom != null) { + next.mixTime += delta; + next = next.mixingFrom; + } + continue; + } + } else if (current.trackLast >= current.trackEnd && current.mixingFrom == null) { + // Clear the track when there is no next entry, the track end time is reached, and there is no mixingFrom. + tracksItems[i] = null; + queue.End(current); + DisposeNext(current); + continue; + } + if (current.mixingFrom != null && UpdateMixingFrom(current, delta)) { + // End mixing from entries once all have completed. + TrackEntry from = current.mixingFrom; + current.mixingFrom = null; + if (from != null) from.mixingTo = null; + while (from != null) { + queue.End(from); + from = from.mixingFrom; + } + } + + current.trackTime += currentDelta; + } + + queue.Drain(); + } + + /// Returns true when all mixing from entries are complete. + private bool UpdateMixingFrom (TrackEntry to, float delta) { + TrackEntry from = to.mixingFrom; + if (from == null) return true; + + bool finished = UpdateMixingFrom(from, delta); + + from.animationLast = from.nextAnimationLast; + from.trackLast = from.nextTrackLast; + + // Require mixTime > 0 to ensure the mixing from entry was applied at least once. + if (to.mixTime > 0 && to.mixTime >= to.mixDuration) { + // Require totalAlpha == 0 to ensure mixing is complete, unless mixDuration == 0 (the transition is a single frame). + if (from.totalAlpha == 0 || to.mixDuration == 0) { + to.mixingFrom = from.mixingFrom; + if (from.mixingFrom != null) from.mixingFrom.mixingTo = to; + to.interruptAlpha = from.interruptAlpha; + queue.End(from); + } + return finished; + } + + from.trackTime += delta * from.timeScale; + to.mixTime += delta; + return false; + } + + /// + /// Poses the skeleton using the track entry animations. There are no side effects other than invoking listeners, so the + /// animation state can be applied to multiple skeletons to pose them identically. + /// True if any animations were applied. + public bool Apply (Skeleton skeleton) { + if (skeleton == null) throw new ArgumentNullException("skeleton", "skeleton cannot be null."); + if (animationsChanged) AnimationsChanged(); + + var events = this.events; + bool applied = false; + var tracksItems = tracks.Items; + for (int i = 0, n = tracks.Count; i < n; i++) { + TrackEntry current = tracksItems[i]; + if (current == null || current.delay > 0) continue; + applied = true; + + // Track 0 animations aren't for layering, so do not show the previously applied animations before the first key. + MixBlend blend = i == 0 ? MixBlend.First : current.mixBlend; + + // Apply mixing from entries first. + float mix = current.alpha; + if (current.mixingFrom != null) + mix *= ApplyMixingFrom(current, skeleton, blend); + else if (current.trackTime >= current.trackEnd && current.next == null) // + mix = 0; // Set to setup pose the last time the entry will be applied. + + // Apply current entry. + float animationLast = current.animationLast, animationTime = current.AnimationTime; + int timelineCount = current.animation.timelines.Count; + var timelines = current.animation.timelines; + var timelinesItems = timelines.Items; + if ((i == 0 && mix == 1) || blend == MixBlend.Add) { + for (int ii = 0; ii < timelineCount; ii++) + timelinesItems[ii].Apply(skeleton, animationLast, animationTime, events, mix, blend, MixDirection.In); + } else { + var timelineMode = current.timelineMode.Items; + + bool firstFrame = current.timelinesRotation.Count == 0; + if (firstFrame) current.timelinesRotation.EnsureCapacity(timelines.Count << 1); + var timelinesRotation = current.timelinesRotation.Items; + + for (int ii = 0; ii < timelineCount; ii++) { + Timeline timeline = timelinesItems[ii]; + MixBlend timelineBlend = timelineMode[ii] == AnimationState.Subsequent ? blend : MixBlend.Setup; + var rotateTimeline = timeline as RotateTimeline; + if (rotateTimeline != null) + ApplyRotateTimeline(rotateTimeline, skeleton, animationTime, mix, timelineBlend, timelinesRotation, ii << 1, + firstFrame); + else + timeline.Apply(skeleton, animationLast, animationTime, events, mix, timelineBlend, MixDirection.In); + } + } + QueueEvents(current, animationTime); + events.Clear(false); + current.nextAnimationLast = animationTime; + current.nextTrackLast = current.trackTime; + } + + queue.Drain(); + return applied; + } + + private float ApplyMixingFrom (TrackEntry to, Skeleton skeleton, MixBlend blend) { + TrackEntry from = to.mixingFrom; + if (from.mixingFrom != null) ApplyMixingFrom(from, skeleton, blend); + + float mix; + if (to.mixDuration == 0) { // Single frame mix to undo mixingFrom changes. + mix = 1; + if (blend == MixBlend.First) blend = MixBlend.Setup; // Tracks > 0 are transparent and can't reset to setup pose. + } else { + mix = to.mixTime / to.mixDuration; + if (mix > 1) mix = 1; + if (blend != MixBlend.First) blend = from.mixBlend; // Track 0 ignores track mix blend. + } + + var eventBuffer = mix < from.eventThreshold ? this.events : null; + bool attachments = mix < from.attachmentThreshold, drawOrder = mix < from.drawOrderThreshold; + float animationLast = from.animationLast, animationTime = from.AnimationTime; + var timelines = from.animation.timelines; + int timelineCount = timelines.Count; + var timelinesItems = timelines.Items; + float alphaHold = from.alpha * to.interruptAlpha, alphaMix = alphaHold * (1 - mix); + + if (blend == MixBlend.Add) { + for (int i = 0; i < timelineCount; i++) + timelinesItems[i].Apply(skeleton, animationLast, animationTime, eventBuffer, alphaMix, blend, MixDirection.Out); + } else { + var timelineMode = from.timelineMode.Items; + var timelineHoldMix = from.timelineHoldMix.Items; + + bool firstFrame = from.timelinesRotation.Count == 0; + if (firstFrame) from.timelinesRotation.Resize(timelines.Count << 1); // from.timelinesRotation.setSize + var timelinesRotation = from.timelinesRotation.Items; + + from.totalAlpha = 0; + for (int i = 0; i < timelineCount; i++) { + Timeline timeline = timelinesItems[i]; + MixDirection direction = MixDirection.Out; + MixBlend timelineBlend; + float alpha; + switch (timelineMode[i]) { + case AnimationState.Subsequent: + if (!attachments && timeline is AttachmentTimeline) continue; + if (!drawOrder && timeline is DrawOrderTimeline) continue; + timelineBlend = blend; + alpha = alphaMix; + break; + case AnimationState.First: + timelineBlend = MixBlend.Setup; + alpha = alphaMix; + break; + case AnimationState.Hold: + timelineBlend = MixBlend.Setup; + alpha = alphaHold; + break; + default: + timelineBlend = MixBlend.Setup; + TrackEntry holdMix = timelineHoldMix[i]; + alpha = alphaHold * Math.Max(0, 1 - holdMix.mixTime / holdMix.mixDuration); + break; + } + from.totalAlpha += alpha; + + var rotateTimeline = timeline as RotateTimeline; + if (rotateTimeline != null) { + ApplyRotateTimeline(rotateTimeline, skeleton, animationTime, alpha, timelineBlend, timelinesRotation, i << 1, + firstFrame); + } else { + if (timelineBlend == MixBlend.Setup) { + if (timeline is AttachmentTimeline) { + if (attachments) direction = MixDirection.In; + } else if (timeline is DrawOrderTimeline) { + if (drawOrder) { + direction = MixDirection.In; + } + } + } + + timeline.Apply(skeleton, animationLast, animationTime, eventBuffer, alpha, timelineBlend, direction); + } + } + } + + if (to.mixDuration > 0) QueueEvents(from, animationTime); + this.events.Clear(false); + from.nextAnimationLast = animationTime; + from.nextTrackLast = from.trackTime; + + return mix; + } + + static private void ApplyRotateTimeline (RotateTimeline rotateTimeline, Skeleton skeleton, float time, float alpha, MixBlend blend, + float[] timelinesRotation, int i, bool firstFrame) { + + if (firstFrame) timelinesRotation[i] = 0; + + if (alpha == 1) { + rotateTimeline.Apply(skeleton, 0, time, null, 1, blend, MixDirection.In); + return; + } + + Bone bone = skeleton.bones.Items[rotateTimeline.boneIndex]; + float[] frames = rotateTimeline.frames; + float r1, r2; + if (time < frames[0]) { // Time is before first frame. + switch (blend) { + case MixBlend.Setup: + bone.rotation = bone.data.rotation; + return; + default: + return; + case MixBlend.First: + r1 = bone.rotation; + r2 = bone.data.rotation; + break; + } + } else { + r1 = blend == MixBlend.Setup ? bone.data.rotation : bone.rotation; + if (time >= frames[frames.Length - RotateTimeline.ENTRIES]) // Time is after last frame. + r2 = bone.data.rotation + frames[frames.Length + RotateTimeline.PREV_ROTATION]; + else { + // Interpolate between the previous frame and the current frame. + int frame = Animation.BinarySearch(frames, time, RotateTimeline.ENTRIES); + float prevRotation = frames[frame + RotateTimeline.PREV_ROTATION]; + float frameTime = frames[frame]; + float percent = rotateTimeline.GetCurvePercent((frame >> 1) - 1, + 1 - (time - frameTime) / (frames[frame + RotateTimeline.PREV_TIME] - frameTime)); + + r2 = frames[frame + RotateTimeline.ROTATION] - prevRotation; + r2 -= (16384 - (int)(16384.499999999996 - r2 / 360)) * 360; + r2 = prevRotation + r2 * percent + bone.data.rotation; + r2 -= (16384 - (int)(16384.499999999996 - r2 / 360)) * 360; + } + } + + // Mix between rotations using the direction of the shortest route on the first frame. + float total, diff = r2 - r1; + diff -= (16384 - (int)(16384.499999999996 - diff / 360)) * 360; + if (diff == 0) { + total = timelinesRotation[i]; + } else { + float lastTotal, lastDiff; + if (firstFrame) { + lastTotal = 0; + lastDiff = diff; + } else { + lastTotal = timelinesRotation[i]; // Angle and direction of mix, including loops. + lastDiff = timelinesRotation[i + 1]; // Difference between bones. + } + bool current = diff > 0, dir = lastTotal >= 0; + // Detect cross at 0 (not 180). + if (Math.Sign(lastDiff) != Math.Sign(diff) && Math.Abs(lastDiff) <= 90) { + // A cross after a 360 rotation is a loop. + if (Math.Abs(lastTotal) > 180) lastTotal += 360 * Math.Sign(lastTotal); + dir = current; + } + total = diff + lastTotal - lastTotal % 360; // Store loops as part of lastTotal. + if (dir != current) total += 360 * Math.Sign(lastTotal); + timelinesRotation[i] = total; + } + timelinesRotation[i + 1] = diff; + r1 += total * alpha; + bone.rotation = r1 - (16384 - (int)(16384.499999999996 - r1 / 360)) * 360; + } + + private void QueueEvents (TrackEntry entry, float animationTime) { + float animationStart = entry.animationStart, animationEnd = entry.animationEnd; + float duration = animationEnd - animationStart; + float trackLastWrapped = entry.trackLast % duration; + + // Queue events before complete. + var events = this.events; + var eventsItems = events.Items; + int i = 0, n = events.Count; + for (; i < n; i++) { + Event e = eventsItems[i]; + if (e.time < trackLastWrapped) break; + if (e.time > animationEnd) continue; // Discard events outside animation start/end. + queue.Event(entry, e); + } + + // Queue complete if completed a loop iteration or the animation. + bool complete = false; + if (entry.loop) + complete = duration == 0 || (trackLastWrapped > entry.trackTime % duration); + else + complete = animationTime >= animationEnd && entry.animationLast < animationEnd; + if (complete) queue.Complete(entry); + + // Queue events after complete. + for (; i < n; i++) { + Event e = eventsItems[i]; + if (e.time < animationStart) continue; // Discard events outside animation start/end. + queue.Event(entry, eventsItems[i]); + } + } + + /// + /// Removes all animations from all tracks, leaving skeletons in their current pose. + /// + /// It may be desired to use to mix the skeletons back to the setup pose, + /// rather than leaving them in their current pose. + /// + public void ClearTracks () { + bool oldDrainDisabled = queue.drainDisabled; + queue.drainDisabled = true; + for (int i = 0, n = tracks.Count; i < n; i++) { + ClearTrack(i); + } + tracks.Clear(); + queue.drainDisabled = oldDrainDisabled; + queue.Drain(); + } + + /// + /// Removes all animations from the track, leaving skeletons in their current pose. + /// + /// It may be desired to use to mix the skeletons back to the setup pose, + /// rather than leaving them in their current pose. + /// + public void ClearTrack (int trackIndex) { + if (trackIndex >= tracks.Count) return; + TrackEntry current = tracks.Items[trackIndex]; + if (current == null) return; + + queue.End(current); + + DisposeNext(current); + + TrackEntry entry = current; + while (true) { + TrackEntry from = entry.mixingFrom; + if (from == null) break; + queue.End(from); + entry.mixingFrom = null; + entry.mixingTo = null; + entry = from; + } + + tracks.Items[current.trackIndex] = null; + + queue.Drain(); + } + + /// Sets the active TrackEntry for a given track number. + private void SetCurrent (int index, TrackEntry current, bool interrupt) { + TrackEntry from = ExpandToIndex(index); + tracks.Items[index] = current; + + if (from != null) { + if (interrupt) queue.Interrupt(from); + current.mixingFrom = from; + from.mixingTo = current; + current.mixTime = 0; + + // Store the interrupted mix percentage. + if (from.mixingFrom != null && from.mixDuration > 0) + current.interruptAlpha *= Math.Min(1, from.mixTime / from.mixDuration); + + from.timelinesRotation.Clear(); // Reset rotation for mixing out, in case entry was mixed in. + } + + queue.Start(current); // triggers AnimationsChanged + } + + + /// Sets an animation by name. + public TrackEntry SetAnimation (int trackIndex, string animationName, bool loop) { + Animation animation = data.skeletonData.FindAnimation(animationName); + if (animation == null) throw new ArgumentException("Animation not found: " + animationName, "animationName"); + return SetAnimation(trackIndex, animation, loop); + } + + /// Sets the current animation for a track, discarding any queued animations. If the formerly current track entry was never + /// applied to a skeleton, it is replaced (not mixed from). + /// If true, the animation will repeat. If false it will not, instead its last frame is applied if played beyond its + /// duration. In either case determines when the track is cleared. + /// A track entry to allow further customization of animation playback. References to the track entry must not be kept + /// after the event occurs. + public TrackEntry SetAnimation (int trackIndex, Animation animation, bool loop) { + if (animation == null) throw new ArgumentNullException("animation", "animation cannot be null."); + bool interrupt = true; + TrackEntry current = ExpandToIndex(trackIndex); + if (current != null) { + if (current.nextTrackLast == -1) { + // Don't mix from an entry that was never applied. + tracks.Items[trackIndex] = current.mixingFrom; + queue.Interrupt(current); + queue.End(current); + DisposeNext(current); + current = current.mixingFrom; + interrupt = false; // mixingFrom is current again, but don't interrupt it twice. + } else { + DisposeNext(current); + } + } + TrackEntry entry = NewTrackEntry(trackIndex, animation, loop, current); + SetCurrent(trackIndex, entry, interrupt); + queue.Drain(); + return entry; + } + + /// Queues an animation by name. + /// + public TrackEntry AddAnimation (int trackIndex, string animationName, bool loop, float delay) { + Animation animation = data.skeletonData.FindAnimation(animationName); + if (animation == null) throw new ArgumentException("Animation not found: " + animationName, "animationName"); + return AddAnimation(trackIndex, animation, loop, delay); + } + + /// Adds an animation to be played after the current or last queued animation for a track. If the track is empty, it is + /// equivalent to calling . + /// + /// If > 0, sets . If <= 0, the delay set is the duration of the previous track entry + /// minus any mix duration (from the {@link AnimationStateData}) plus the specified Delay (ie the mix + /// ends at (Delay = 0) or before (Delay < 0) the previous track entry duration). If the + /// previous entry is looping, its next loop completion is used instead of its duration. + /// + /// A track entry to allow further customization of animation playback. References to the track entry must not be kept + /// after the event occurs. + public TrackEntry AddAnimation (int trackIndex, Animation animation, bool loop, float delay) { + if (animation == null) throw new ArgumentNullException("animation", "animation cannot be null."); + + TrackEntry last = ExpandToIndex(trackIndex); + if (last != null) { + while (last.next != null) + last = last.next; + } + + TrackEntry entry = NewTrackEntry(trackIndex, animation, loop, last); + + if (last == null) { + SetCurrent(trackIndex, entry, true); + queue.Drain(); + } else { + last.next = entry; + if (delay <= 0) { + float duration = last.animationEnd - last.animationStart; + if (duration != 0) { + if (last.loop) { + delay += duration * (1 + (int)(last.trackTime / duration)); // Completion of next loop. + } else { + delay += Math.Max(duration, last.trackTime); // After duration, else next update. + } + delay -= data.GetMix(last.animation, animation); + } else + delay = last.trackTime; // Next update. + } + } + + entry.delay = delay; + return entry; + } + + /// + /// Sets an empty animation for a track, discarding any queued animations, and sets the track entry's + /// . An empty animation has no timelines and serves as a placeholder for mixing in or out. + /// + /// Mixing out is done by setting an empty animation with a mix duration using either , + /// , or . Mixing to an empty animation causes + /// the previous animation to be applied less and less over the mix duration. Properties keyed in the previous animation + /// transition to the value from lower tracks or to the setup pose value if no lower tracks key the property. A mix duration of + /// 0 still mixes out over one frame. + /// + /// Mixing in is done by first setting an empty animation, then adding an animation using + /// and on the returned track entry, set the + /// . Mixing from an empty animation causes the new animation to be applied more and + /// more over the mix duration. Properties keyed in the new animation transition from the value from lower tracks or from the + /// setup pose value if no lower tracks key the property to the value keyed in the new animation. + /// + public TrackEntry SetEmptyAnimation (int trackIndex, float mixDuration) { + TrackEntry entry = SetAnimation(trackIndex, AnimationState.EmptyAnimation, false); + entry.mixDuration = mixDuration; + entry.trackEnd = mixDuration; + return entry; + } + + /// + /// Adds an empty animation to be played after the current or last queued animation for a track, and sets the track entry's + /// . If the track is empty, it is equivalent to calling + /// . + /// + /// Track number. + /// Mix duration. + /// If > 0, sets . If <= 0, the delay set is the duration of the previous track entry + /// minus any mix duration plus the specified Delay (ie the mix ends at (Delay = 0) or + /// before (Delay < 0) the previous track entry duration). If the previous entry is looping, its next + /// loop completion is used instead of its duration. + /// A track entry to allow further customization of animation playback. References to the track entry must not be kept + /// after the event occurs. + /// + 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.trackEnd = 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) { + bool oldDrainDisabled = queue.drainDisabled; + queue.drainDisabled = true; + for (int i = 0, n = tracks.Count; i < n; i++) { + TrackEntry current = tracks.Items[i]; + if (current != null) SetEmptyAnimation(current.trackIndex, mixDuration); + } + queue.drainDisabled = oldDrainDisabled; + queue.Drain(); + } + + private TrackEntry ExpandToIndex (int index) { + if (index < tracks.Count) return tracks.Items[index]; + tracks.Resize(index + 1); + return null; + } + + /// Object-pooling version of new TrackEntry. Obtain an unused TrackEntry from the pool and clear/initialize its values. + /// May be null. + private TrackEntry NewTrackEntry (int trackIndex, Animation animation, bool loop, TrackEntry last) { + TrackEntry entry = trackEntryPool.Obtain(); // Pooling + entry.trackIndex = trackIndex; + entry.animation = animation; + entry.loop = loop; + entry.holdPrevious = false; + + entry.eventThreshold = 0; + entry.attachmentThreshold = 0; + entry.drawOrderThreshold = 0; + + entry.animationStart = 0; + entry.animationEnd = animation.Duration; + entry.animationLast = -1; + entry.nextAnimationLast = -1; + + entry.delay = 0; + entry.trackTime = 0; + entry.trackLast = -1; + entry.nextTrackLast = -1; // nextTrackLast == -1 signifies a TrackEntry that wasn't applied yet. + entry.trackEnd = float.MaxValue; // loop ? float.MaxValue : animation.Duration; + entry.timeScale = 1; + + entry.alpha = 1; + entry.interruptAlpha = 1; + entry.mixTime = 0; + entry.mixDuration = (last == null) ? 0 : data.GetMix(last.animation, animation); + return entry; + } + + /// Dispose all track entries queued after the given TrackEntry. + private void DisposeNext (TrackEntry entry) { + TrackEntry next = entry.next; + while (next != null) { + queue.Dispose(next); + next = next.next; + } + entry.next = null; + } + + private void AnimationsChanged () { + animationsChanged = false; + + propertyIDs.Clear(); + + var tracksItems = tracks.Items; + for (int i = 0, n = tracks.Count; i < n; i++) { + TrackEntry entry = tracksItems[i]; + if (entry == null) continue; + // Move to last entry, then iterate in reverse (the order animations are applied). + while (entry.mixingFrom != null) + entry = entry.mixingFrom; + + do { + if (entry.mixingTo == null || entry.mixBlend != MixBlend.Add) SetTimelineModes(entry); + entry = entry.mixingTo; + } while (entry != null); + + } + } + + private void SetTimelineModes (TrackEntry entry) { + TrackEntry to = entry.mixingTo; + var timelines = entry.animation.timelines.Items; + int timelinesCount = entry.animation.timelines.Count; + var timelineMode = entry.timelineMode.Resize(timelinesCount).Items; //timelineMode.setSize(timelinesCount); + entry.timelineHoldMix.Clear(); + var timelineHoldMix = entry.timelineHoldMix.Resize(timelinesCount).Items; //timelineHoldMix.setSize(timelinesCount); + var propertyIDs = this.propertyIDs; + + if (to != null && to.holdPrevious) { + for (int i = 0; i < timelinesCount; i++) { + propertyIDs.Add(timelines[i].PropertyId); + timelineMode[i] = AnimationState.Hold; + } + return; + } + + // outer: + for (int i = 0; i < timelinesCount; i++) { + int id = timelines[i].PropertyId; + if (!propertyIDs.Add(id)) + timelineMode[i] = AnimationState.Subsequent; + else if (to == null || !HasTimeline(to, id)) + timelineMode[i] = AnimationState.First; + else { + for (TrackEntry next = to.mixingTo; next != null; next = next.mixingTo) { + if (HasTimeline(next, id)) continue; + if (next.mixDuration > 0) { + timelineMode[i] = AnimationState.HoldMix; + timelineHoldMix[i] = next; + goto continue_outer; // continue outer; + } + break; + } + timelineMode[i] = AnimationState.Hold; + } + continue_outer: {} + } + } + + static bool HasTimeline (TrackEntry entry, int id) { + var timelines = entry.animation.timelines.Items; + for (int i = 0, n = entry.animation.timelines.Count; i < n; i++) + if (timelines[i].PropertyId == id) return true; + return false; + } + + /// The track entry for the animation currently playing on the track, or null if no animation is currently playing. + public TrackEntry GetCurrent (int trackIndex) { + if (trackIndex >= tracks.Count) return null; + return tracks.Items[trackIndex]; + } + + /// Discards all listener notifications that have not yet been delivered. This can be useful to call from an + /// AnimationState event subscriber when it is known that further notifications that may have been already queued for delivery + /// are not wanted because new animations are being set. + public void ClearListenerNotifications () { + queue.Clear(); + } + + /// + /// Multiplier for the delta time when the animation state is updated, causing time for all animations and mixes to play slower + /// or faster. Defaults to 1. + /// + /// See TrackEntry for affecting a single animation. + /// + public float TimeScale { get { return timeScale; } set { timeScale = value; } } + + /// The AnimationStateData to look up mix durations. + public AnimationStateData Data { + get { + return data; + } + set { + if (data == null) throw new ArgumentNullException("data", "data cannot be null."); + this.data = value; + } + } + + /// A list of tracks that have animations, which may contain nulls. + public ExposedList Tracks { get { return tracks; } } + + override public string ToString () { + var buffer = new System.Text.StringBuilder(); + for (int i = 0, n = tracks.Count; i < n; i++) { + TrackEntry entry = tracks.Items[i]; + if (entry == null) continue; + if (buffer.Length > 0) buffer.Append(", "); + buffer.Append(entry.ToString()); + } + if (buffer.Length == 0) return ""; + return buffer.ToString(); + } + } + + /// + /// + /// Stores settings and other state for the playback of an animation on an track. + /// + /// References to a track entry must not be kept after the event occurs. + /// + public class TrackEntry : Pool.IPoolable { + internal Animation animation; + + internal TrackEntry next, mixingFrom, mixingTo; + // difference to libgdx reference: delegates are used for event callbacks instead of 'AnimationStateListener listener'. + public event AnimationState.TrackEntryDelegate Start, Interrupt, End, Dispose, Complete; + public event AnimationState.TrackEntryEventDelegate Event; + internal void OnStart () { if (Start != null) Start(this); } + internal void OnInterrupt () { if (Interrupt != null) Interrupt(this); } + internal void OnEnd () { if (End != null) End(this); } + internal void OnDispose () { if (Dispose != null) Dispose(this); } + internal void OnComplete () { if (Complete != null) Complete(this); } + internal void OnEvent (Event e) { if (Event != null) Event(this, e); } + + internal int trackIndex; + + internal bool loop, holdPrevious; + internal float eventThreshold, attachmentThreshold, drawOrderThreshold; + internal float animationStart, animationEnd, animationLast, nextAnimationLast; + internal float delay, trackTime, trackLast, nextTrackLast, trackEnd, timeScale = 1f; + internal float alpha, mixTime, mixDuration, interruptAlpha, totalAlpha; + internal MixBlend mixBlend = MixBlend.Replace; + internal readonly ExposedList timelineMode = new ExposedList(); + internal readonly ExposedList timelineHoldMix = new ExposedList(); + internal readonly ExposedList timelinesRotation = new ExposedList(); + + // IPoolable.Reset() + public void Reset () { + next = null; + mixingFrom = null; + mixingTo = null; + animation = null; + // replaces 'listener = null;' since delegates are used for event callbacks + Start = null; + Interrupt = null; + End = null; + Dispose = null; + Complete = null; + Event = null; + timelineMode.Clear(); + timelineHoldMix.Clear(); + timelinesRotation.Clear(); + } + + /// The index of the track where this entry is either current or queued. + /// + public int TrackIndex { get { return trackIndex; } } + + /// The animation to apply for this track entry. + public Animation Animation { get { return animation; } } + + /// + /// If true, the animation will repeat. If false it will not, instead its last frame is applied if played beyond its + /// duration. + public bool Loop { get { return loop; } set { loop = value; } } + + /// + /// + /// Seconds to postpone playing the animation. When this track entry is the current track entry, Delay + /// postpones incrementing the . When this track entry is queued, Delay is the time from + /// the start of the previous animation to when this track entry will become the current track entry (ie when the previous + /// track entry >= this track entry's Delay). + /// + /// affects the delay. + /// + public float Delay { get { return delay; } set { delay = value; } } + + /// + /// Current time in seconds this track entry has been the current track entry. The track time determines + /// . The track time can be set to start the animation at a time other than 0, without affecting + /// looping. + public float TrackTime { get { return trackTime; } set { trackTime = value; } } + + /// + /// + /// The track time in seconds when this animation will be removed from the track. Defaults to the highest possible float + /// value, meaning the animation will be applied until a new animation is set or the track is cleared. If the track end time + /// is reached, no other animations are queued for playback, and mixing from any previous animations is complete, then the + /// properties keyed by the animation are set to the setup pose and the track is cleared. + /// + /// It may be desired to use rather than have the animation + /// abruptly cease being applied. + /// + public float TrackEnd { get { return trackEnd; } set { trackEnd = value; } } + + /// + /// + /// Seconds when this animation starts, both initially and after looping. Defaults to 0. + /// + /// When changing the AnimationStart time, it often makes sense to set to the same + /// value to prevent timeline keys before the start time from triggering. + /// + public float AnimationStart { get { return animationStart; } set { animationStart = value; } } + + /// + /// Seconds for the last frame of this animation. Non-looping animations won't play past this time. Looping animations will + /// loop back to at this time. Defaults to the animation . + /// + public float AnimationEnd { get { return animationEnd; } set { animationEnd = value; } } + + /// + /// The time in seconds this animation was last applied. Some timelines use this for one-time triggers. Eg, when this + /// animation is applied, event timelines will fire all events between the AnimationLast time (exclusive) and + /// AnimationTime (inclusive). Defaults to -1 to ensure triggers on frame 0 happen the first time this animation + /// is applied. + public float AnimationLast { + get { return animationLast; } + set { + animationLast = value; + nextAnimationLast = value; + } + } + + /// + /// Uses to compute the AnimationTime, which is between + /// and . When the TrackTime is 0, the AnimationTime is equal to the + /// AnimationStart time. + /// + public float AnimationTime { + get { + if (loop) { + float duration = animationEnd - animationStart; + if (duration == 0) return animationStart; + return (trackTime % duration) + animationStart; + } + return Math.Min(trackTime + animationStart, animationEnd); + } + } + + /// + /// + /// Multiplier for the delta time when this track entry is updated, causing time for this animation to pass slower or + /// faster. Defaults to 1. + /// + /// is not affected by track entry time scale, so may need to be adjusted to + /// match the animation speed. + /// + /// When using with a Delay <= 0, note the + /// { is set using the mix duration from the , assuming time scale to be 1. If + /// the time scale is not 1, the delay may need to be adjusted. + /// + /// See AnimationState for affecting all animations. + /// + public float TimeScale { get { return timeScale; } set { timeScale = value; } } + + /// + /// + /// Values < 1 mix this animation with the skeleton's current pose (usually the pose resulting from lower tracks). Defaults + /// to 1, which overwrites the skeleton's current pose with this animation. + /// + /// Typically track 0 is used to completely pose the skeleton, then alpha is used on higher tracks. It doesn't make sense to + /// use alpha on track 0 if the skeleton pose is from the last frame render. + /// + public float Alpha { get { return alpha; } set { alpha = value; } } + + /// + /// When the mix percentage ( / ) is less than the + /// EventThreshold, event timelines are applied while this animation is being mixed out. Defaults to 0, so event + /// timelines are not applied while this animation is being mixed out. + /// + public float EventThreshold { get { return eventThreshold; } set { eventThreshold = value; } } + + /// + /// When the mix percentage ( / ) is less than the + /// AttachmentThreshold, attachment timelines are applied while this animation is being mixed out. Defaults to + /// 0, so attachment timelines are not applied while this animation is being mixed out. + /// + public float AttachmentThreshold { get { return attachmentThreshold; } set { attachmentThreshold = value; } } + + /// + /// When the mix percentage ( / ) is less than the + /// DrawOrderThreshold, draw order timelines are applied while this animation is being mixed out. Defaults to 0, + /// so draw order timelines are not applied while this animation is being mixed out. + /// + public float DrawOrderThreshold { get { return drawOrderThreshold; } set { drawOrderThreshold = value; } } + + /// + /// The animation queued to start after this animation, or null. Next makes up a linked list. + public TrackEntry Next { get { return next; } } + + /// + /// Returns true if at least one loop has been completed. + /// + public bool IsComplete { + get { return trackTime >= animationEnd - animationStart; } + } + + /// + /// Seconds from 0 to the when mixing from the previous animation to this animation. May be + /// slightly more than MixDuration when the mix is complete. + public float MixTime { get { return mixTime; } set { mixTime = value; } } + + /// + /// + /// Seconds for mixing from the previous animation to this animation. Defaults to the value provided by AnimationStateData + /// based on the animation before this animation (if any). + /// + /// The MixDuration can be set manually rather than use the value from + /// . In that case, the MixDuration can be set for a new + /// track entry only before is first called. + /// + /// When using with a Delay <= 0, note the + /// is set using the mix duration from the , not a mix duration set + /// afterward. + /// + public float MixDuration { get { return mixDuration; } set { mixDuration = value; } } + + /// + /// + /// Controls how properties keyed in the animation are mixed with lower tracks. Defaults to , which + /// replaces the values from the lower tracks with the animation values. adds the animation values to + /// the values from the lower tracks. + /// + /// The MixBlend can be set for a new track entry only before is first + /// called. + /// + public MixBlend MixBlend { get { return mixBlend; } set { mixBlend = value; } } + + /// + /// The track entry for the previous animation when mixing from the previous animation to this animation, or null if no + /// mixing is currently occuring. When mixing from multiple animations, MixingFrom makes up a linked list. + public TrackEntry MixingFrom { get { return mixingFrom; } } + + /// + /// The track entry for the next animation when mixing from this animation to the next animation, or null if no mixing is + /// currently occuring. When mixing to multiple animations, MixingTo makes up a linked list. + public TrackEntry MixingTo { get { return mixingTo; } } + + /// + /// + /// If true, when mixing from the previous animation to this animation, the previous animation is applied as normal instead + /// of being mixed out. + /// + /// When mixing between animations that key the same property, if a lower track also keys that property then the value will + /// briefly dip toward the lower track value during the mix. This happens because the first animation mixes from 100% to 0% + /// while the second animation mixes from 0% to 100%. Setting HoldPrevious to true applies the first animation + /// at 100% during the mix so the lower track value is overwritten. Such dipping does not occur on the lowest track which + /// keys the property, only when a higher track also keys the property. + /// + /// Snapping will occur if HoldPrevious is true and this animation does not key all the same properties as the + /// previous animation. + /// + public bool HoldPrevious { get { return holdPrevious; } set { holdPrevious = value; } } + + /// + /// + /// Resets the rotation directions for mixing this entry's rotate timelines. This can be useful to avoid bones rotating the + /// long way around when using and starting animations on other tracks. + /// + /// Mixing with involves finding a rotation between two others, which has two possible solutions: + /// the short way or the long way around. The two rotations likely change over time, so which direction is the short or long + /// way also changes. If the short way was always chosen, bones would flip to the other side when that direction became the + /// long way. TrackEntry chooses the short way the first time it is applied and remembers that direction. + /// + public void ResetRotationDirections () { + timelinesRotation.Clear(); + } + + override public string ToString () { + return animation == null ? "" : animation.name; + } + } + + class EventQueue { + private readonly List eventQueueEntries = new List(); + internal bool drainDisabled; + + private readonly AnimationState state; + private readonly Pool trackEntryPool; + internal event Action AnimationsChanged; + + internal EventQueue (AnimationState state, Action HandleAnimationsChanged, Pool trackEntryPool) { + this.state = state; + this.AnimationsChanged += HandleAnimationsChanged; + this.trackEntryPool = trackEntryPool; + } + + struct EventQueueEntry { + public EventType type; + public TrackEntry entry; + public Event e; + + public EventQueueEntry (EventType eventType, TrackEntry trackEntry, Event e = null) { + this.type = eventType; + this.entry = trackEntry; + this.e = e; + } + } + + enum EventType { + Start, Interrupt, End, Dispose, Complete, Event + } + + internal void Start (TrackEntry entry) { + eventQueueEntries.Add(new EventQueueEntry(EventType.Start, entry)); + if (AnimationsChanged != null) AnimationsChanged(); + } + + internal void Interrupt (TrackEntry entry) { + eventQueueEntries.Add(new EventQueueEntry(EventType.Interrupt, entry)); + } + + internal void End (TrackEntry entry) { + eventQueueEntries.Add(new EventQueueEntry(EventType.End, entry)); + if (AnimationsChanged != null) AnimationsChanged(); + } + + internal void Dispose (TrackEntry entry) { + eventQueueEntries.Add(new EventQueueEntry(EventType.Dispose, entry)); + } + + internal void Complete (TrackEntry entry) { + eventQueueEntries.Add(new EventQueueEntry(EventType.Complete, entry)); + } + + internal void Event (TrackEntry entry, Event e) { + eventQueueEntries.Add(new EventQueueEntry(EventType.Event, entry, e)); + } + + /// Raises all events in the queue and drains the queue. + internal void Drain () { + if (drainDisabled) return; + drainDisabled = true; + + var entries = this.eventQueueEntries; + AnimationState state = this.state; + + // Don't cache entries.Count so callbacks can queue their own events (eg, call SetAnimation in AnimationState_Complete). + for (int i = 0; i < entries.Count; i++) { + var queueEntry = entries[i]; + TrackEntry trackEntry = queueEntry.entry; + + switch (queueEntry.type) { + case EventType.Start: + trackEntry.OnStart(); + state.OnStart(trackEntry); + break; + case EventType.Interrupt: + trackEntry.OnInterrupt(); + state.OnInterrupt(trackEntry); + break; + case EventType.End: + trackEntry.OnEnd(); + state.OnEnd(trackEntry); + goto case EventType.Dispose; // Fall through. (C#) + case EventType.Dispose: + trackEntry.OnDispose(); + state.OnDispose(trackEntry); + trackEntryPool.Free(trackEntry); // Pooling + break; + case EventType.Complete: + trackEntry.OnComplete(); + state.OnComplete(trackEntry); + break; + case EventType.Event: + trackEntry.OnEvent(queueEntry.e); + state.OnEvent(trackEntry, queueEntry.e); + break; + } + } + eventQueueEntries.Clear(); + + drainDisabled = false; + } + + internal void Clear () { + eventQueueEntries.Clear(); + } + } + + 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/SpineRuntime37/AnimationStateData.cs b/SpineRuntimes/SpineRuntime37/AnimationStateData.cs new file mode 100644 index 0000000..b1a4a85 --- /dev/null +++ b/SpineRuntimes/SpineRuntime37/AnimationStateData.cs @@ -0,0 +1,114 @@ +/****************************************************************************** + * Spine Runtimes License Agreement + * Last updated May 1, 2019. Replaces all prior versions. + * + * Copyright (c) 2013-2019, Esoteric Software LLC + * + * Integration of the Spine Runtimes into software or otherwise creating + * derivative works of the Spine Runtimes is permitted under the terms and + * conditions of Section 2 of the Spine Editor License Agreement: + * http://esotericsoftware.com/spine-editor-license + * + * Otherwise, it is permitted to integrate the Spine Runtimes into software + * or otherwise create derivative works of the Spine Runtimes (collectively, + * "Products"), provided that each user of the Products must obtain their own + * Spine Editor license and redistribution of the Products in any form must + * include this license and copyright notice. + * + * THIS SOFTWARE IS PROVIDED BY ESOTERIC SOFTWARE LLC "AS IS" AND ANY EXPRESS + * OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES + * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN + * NO EVENT SHALL ESOTERIC SOFTWARE LLC BE LIABLE FOR ANY DIRECT, INDIRECT, + * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, + * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES, BUSINESS + * INTERRUPTION, OR LOSS OF USE, DATA, OR PROFITS) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, + * EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + *****************************************************************************/ + +using System; +using System.Collections.Generic; + +namespace SpineRuntime37 { + + /// Stores mix (crossfade) durations to be applied when AnimationState animations are changed. + public class AnimationStateData { + internal SkeletonData skeletonData; + readonly Dictionary animationToMixTime = new Dictionary(AnimationPairComparer.Instance); + internal float defaultMix; + + /// The SkeletonData to look up animations when they are specified by name. + public SkeletonData SkeletonData { get { return skeletonData; } } + + /// + /// The mix duration to use when no mix duration has been specifically defined between two animations. + public float DefaultMix { get { return defaultMix; } set { defaultMix = value; } } + + public AnimationStateData (SkeletonData skeletonData) { + if (skeletonData == null) throw new ArgumentException("skeletonData cannot be null.", "skeletonData"); + this.skeletonData = skeletonData; + } + + /// Sets a mix duration by animation names. + public void SetMix (string fromName, string toName, float duration) { + Animation from = skeletonData.FindAnimation(fromName); + if (from == null) throw new ArgumentException("Animation not found: " + fromName, "fromName"); + Animation to = skeletonData.FindAnimation(toName); + if (to == null) throw new ArgumentException("Animation not found: " + toName, "toName"); + SetMix(from, to, duration); + } + + /// Sets a mix duration when changing from the specified animation to the other. + /// See TrackEntry.MixDuration. + public void SetMix (Animation from, Animation to, float duration) { + if (from == null) throw new ArgumentNullException("from", "from cannot be null."); + if (to == null) throw new ArgumentNullException("to", "to cannot be null."); + AnimationPair key = new AnimationPair(from, to); + animationToMixTime.Remove(key); + animationToMixTime.Add(key, duration); + } + + /// + /// The mix duration to use when changing from the specified animation to the other, + /// or the DefaultMix if no mix duration has been set. + /// + public float GetMix (Animation from, Animation to) { + if (from == null) throw new ArgumentNullException("from", "from cannot be null."); + if (to == null) throw new ArgumentNullException("to", "to cannot be null."); + AnimationPair key = new AnimationPair(from, to); + float duration; + if (animationToMixTime.TryGetValue(key, out duration)) return duration; + return defaultMix; + } + + public struct AnimationPair { + public readonly Animation a1; + public readonly Animation a2; + + public AnimationPair (Animation a1, Animation a2) { + this.a1 = a1; + this.a2 = a2; + } + + public override string ToString () { + return a1.name + "->" + a2.name; + } + } + + // Avoids boxing in the dictionary. + public class AnimationPairComparer : IEqualityComparer { + public static readonly AnimationPairComparer Instance = new AnimationPairComparer(); + + bool IEqualityComparer.Equals (AnimationPair x, AnimationPair y) { + return ReferenceEquals(x.a1, y.a1) && ReferenceEquals(x.a2, y.a2); + } + + int IEqualityComparer.GetHashCode (AnimationPair obj) { + // from Tuple.CombineHashCodes // return (((h1 << 5) + h1) ^ h2); + int h1 = obj.a1.GetHashCode(); + return (((h1 << 5) + h1) ^ obj.a2.GetHashCode()); + } + } + } +} diff --git a/SpineRuntimes/SpineRuntime37/Atlas.cs b/SpineRuntimes/SpineRuntime37/Atlas.cs new file mode 100644 index 0000000..feabfee --- /dev/null +++ b/SpineRuntimes/SpineRuntime37/Atlas.cs @@ -0,0 +1,319 @@ +/****************************************************************************** + * Spine Runtimes License Agreement + * Last updated May 1, 2019. Replaces all prior versions. + * + * Copyright (c) 2013-2019, Esoteric Software LLC + * + * Integration of the Spine Runtimes into software or otherwise creating + * derivative works of the Spine Runtimes is permitted under the terms and + * conditions of Section 2 of the Spine Editor License Agreement: + * http://esotericsoftware.com/spine-editor-license + * + * Otherwise, it is permitted to integrate the Spine Runtimes into software + * or otherwise create derivative works of the Spine Runtimes (collectively, + * "Products"), provided that each user of the Products must obtain their own + * Spine Editor license and redistribution of the Products in any form must + * include this license and copyright notice. + * + * THIS SOFTWARE IS PROVIDED BY ESOTERIC SOFTWARE LLC "AS IS" AND ANY EXPRESS + * OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES + * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN + * NO EVENT SHALL ESOTERIC SOFTWARE LLC BE LIABLE FOR ANY DIRECT, INDIRECT, + * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, + * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES, BUSINESS + * INTERRUPTION, OR LOSS OF USE, DATA, OR PROFITS) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, + * EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + *****************************************************************************/ + +#if (UNITY_5 || UNITY_5_3_OR_NEWER || UNITY_WSA || UNITY_WP8 || UNITY_WP8_1) +#define IS_UNITY +#endif + +using System; +using System.Collections.Generic; +using System.Globalization; +using System.IO; +using System.Reflection; + +#if WINDOWS_STOREAPP +using System.Threading.Tasks; +using Windows.Storage; +#endif + +namespace SpineRuntime37 { + public class Atlas : IEnumerable { + readonly List pages = new List(); + List regions = new List(); + TextureLoader textureLoader; + + #region IEnumerable implementation + public IEnumerator GetEnumerator () { + return regions.GetEnumerator(); + } + + System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator () { + return regions.GetEnumerator(); + } + #endregion + + #if !(IS_UNITY) + #if WINDOWS_STOREAPP + private async Task ReadFile(string path, TextureLoader textureLoader) { + var folder = Windows.ApplicationModel.Package.Current.InstalledLocation; + var file = await folder.GetFileAsync(path).AsTask().ConfigureAwait(false); + using (var reader = new StreamReader(await file.OpenStreamForReadAsync().ConfigureAwait(false))) { + try { + Load(reader, Path.GetDirectoryName(path), textureLoader); + } catch (Exception ex) { + throw new Exception("Error reading atlas file: " + path, ex); + } + } + } + + public Atlas(string path, TextureLoader textureLoader) { + this.ReadFile(path, textureLoader).Wait(); + } + #else + + public Atlas (string path, TextureLoader textureLoader) { + + #if WINDOWS_PHONE + Stream stream = Microsoft.Xna.Framework.TitleContainer.OpenStream(path); + using (StreamReader reader = new StreamReader(stream)) { + #else + using (StreamReader reader = new StreamReader(path)) { + #endif // WINDOWS_PHONE + + try { + Load(reader, Path.GetDirectoryName(path), textureLoader); + } catch (Exception ex) { + throw new Exception("Error reading atlas file: " + path, ex); + } + + } + } + #endif // WINDOWS_STOREAPP + + #endif + + public Atlas (TextReader reader, string dir, TextureLoader textureLoader) { + Load(reader, dir, textureLoader); + } + + public Atlas (List pages, List regions) { + this.pages = pages; + this.regions = regions; + this.textureLoader = null; + } + + private void Load (TextReader reader, string imagesDir, TextureLoader textureLoader) { + if (textureLoader == null) throw new ArgumentNullException("textureLoader", "textureLoader cannot be null."); + this.textureLoader = textureLoader; + + string[] tuple = new string[4]; + AtlasPage page = null; + while (true) { + string line = reader.ReadLine(); + if (line == null) break; + if (line.Trim().Length == 0) + page = null; + else if (page == null) { + page = new AtlasPage(); + page.name = line; + + if (ReadTuple(reader, tuple) == 2) { // size is only optional for an atlas packed with an old TexturePacker. + page.width = int.Parse(tuple[0], CultureInfo.InvariantCulture); + page.height = int.Parse(tuple[1], CultureInfo.InvariantCulture); + ReadTuple(reader, tuple); + } + page.format = (Format)Enum.Parse(typeof(Format), tuple[0], false); + + ReadTuple(reader, tuple); + page.minFilter = (TextureFilter)Enum.Parse(typeof(TextureFilter), tuple[0], false); + page.magFilter = (TextureFilter)Enum.Parse(typeof(TextureFilter), tuple[1], false); + + string direction = ReadValue(reader); + page.uWrap = TextureWrap.ClampToEdge; + page.vWrap = TextureWrap.ClampToEdge; + if (direction == "x") + page.uWrap = TextureWrap.Repeat; + else if (direction == "y") + page.vWrap = TextureWrap.Repeat; + else if (direction == "xy") + page.uWrap = page.vWrap = TextureWrap.Repeat; + + textureLoader.Load(page, Path.Combine(imagesDir, line)); + + pages.Add(page); + + } else { + AtlasRegion region = new AtlasRegion(); + region.name = line; + region.page = page; + + region.rotate = Boolean.Parse(ReadValue(reader)); + + ReadTuple(reader, tuple); + int x = int.Parse(tuple[0], CultureInfo.InvariantCulture); + int y = int.Parse(tuple[1], CultureInfo.InvariantCulture); + + ReadTuple(reader, tuple); + int width = int.Parse(tuple[0], CultureInfo.InvariantCulture); + int height = int.Parse(tuple[1], CultureInfo.InvariantCulture); + + region.u = x / (float)page.width; + region.v = y / (float)page.height; + if (region.rotate) { + region.u2 = (x + height) / (float)page.width; + region.v2 = (y + width) / (float)page.height; + } else { + region.u2 = (x + width) / (float)page.width; + region.v2 = (y + height) / (float)page.height; + } + region.x = x; + region.y = y; + region.width = Math.Abs(width); + region.height = Math.Abs(height); + + if (ReadTuple(reader, tuple) == 4) { // split is optional + region.splits = new [] {int.Parse(tuple[0], CultureInfo.InvariantCulture), + int.Parse(tuple[1], CultureInfo.InvariantCulture), + int.Parse(tuple[2], CultureInfo.InvariantCulture), + int.Parse(tuple[3], CultureInfo.InvariantCulture)}; + + if (ReadTuple(reader, tuple) == 4) { // pad is optional, but only present with splits + region.pads = new [] {int.Parse(tuple[0], CultureInfo.InvariantCulture), + int.Parse(tuple[1], CultureInfo.InvariantCulture), + int.Parse(tuple[2], CultureInfo.InvariantCulture), + int.Parse(tuple[3], CultureInfo.InvariantCulture)}; + + ReadTuple(reader, tuple); + } + } + + region.originalWidth = int.Parse(tuple[0], CultureInfo.InvariantCulture); + region.originalHeight = int.Parse(tuple[1], CultureInfo.InvariantCulture); + + ReadTuple(reader, tuple); + region.offsetX = int.Parse(tuple[0], CultureInfo.InvariantCulture); + region.offsetY = int.Parse(tuple[1], CultureInfo.InvariantCulture); + + region.index = int.Parse(ReadValue(reader), CultureInfo.InvariantCulture); + + regions.Add(region); + } + } + } + + static string ReadValue (TextReader reader) { + string line = reader.ReadLine(); + int colon = line.IndexOf(':'); + if (colon == -1) throw new Exception("Invalid line: " + line); + return line.Substring(colon + 1).Trim(); + } + + /// Returns the number of tuple values read (1, 2 or 4). + static int ReadTuple (TextReader reader, string[] tuple) { + string line = reader.ReadLine(); + int colon = line.IndexOf(':'); + if (colon == -1) throw new Exception("Invalid line: " + line); + int i = 0, lastMatch = colon + 1; + for (; i < 3; i++) { + int comma = line.IndexOf(',', lastMatch); + if (comma == -1) break; + tuple[i] = line.Substring(lastMatch, comma - lastMatch).Trim(); + lastMatch = comma + 1; + } + tuple[i] = line.Substring(lastMatch).Trim(); + return i + 1; + } + + public void FlipV () { + for (int i = 0, n = regions.Count; i < n; i++) { + AtlasRegion region = regions[i]; + region.v = 1 - region.v; + region.v2 = 1 - region.v2; + } + } + + /// Returns the first region found with the specified name. This method uses string comparison to find the region, so the result + /// should be cached rather than calling this method multiple times. + /// The region, or null. + public AtlasRegion FindRegion (string name) { + for (int i = 0, n = regions.Count; i < n; i++) + if (regions[i].name == name) return regions[i]; + return null; + } + + public void Dispose () { + if (textureLoader == null) return; + for (int i = 0, n = pages.Count; i < n; i++) + textureLoader.Unload(pages[i].rendererObject); + } + } + + public enum Format { + Alpha, + Intensity, + LuminanceAlpha, + RGB565, + RGBA4444, + RGB888, + RGBA8888 + } + + public enum TextureFilter { + Nearest, + Linear, + MipMap, + MipMapNearestNearest, + MipMapLinearNearest, + MipMapNearestLinear, + MipMapLinearLinear + } + + public enum TextureWrap { + MirroredRepeat, + ClampToEdge, + Repeat + } + + public class AtlasPage { + public string name; + public Format format; + public TextureFilter minFilter; + public TextureFilter magFilter; + public TextureWrap uWrap; + public TextureWrap vWrap; + public object rendererObject; + public int width, height; + + public AtlasPage Clone () { + return MemberwiseClone() as AtlasPage; + } + } + + public class AtlasRegion { + public AtlasPage page; + public string name; + public int x, y, width, height; + public float u, v, u2, v2; + public float offsetX, offsetY; + public int originalWidth, originalHeight; + public int index; + public bool rotate; + public int[] splits; + public int[] pads; + + public AtlasRegion Clone () { + return MemberwiseClone() as AtlasRegion; + } + } + + public interface TextureLoader { + void Load (AtlasPage page, string path); + void Unload (Object texture); + } +} diff --git a/SpineRuntimes/SpineRuntime37/Attachments/AtlasAttachmentLoader.cs b/SpineRuntimes/SpineRuntime37/Attachments/AtlasAttachmentLoader.cs new file mode 100644 index 0000000..346adf6 --- /dev/null +++ b/SpineRuntimes/SpineRuntime37/Attachments/AtlasAttachmentLoader.cs @@ -0,0 +1,108 @@ +/****************************************************************************** + * Spine Runtimes License Agreement + * Last updated May 1, 2019. Replaces all prior versions. + * + * Copyright (c) 2013-2019, Esoteric Software LLC + * + * Integration of the Spine Runtimes into software or otherwise creating + * derivative works of the Spine Runtimes is permitted under the terms and + * conditions of Section 2 of the Spine Editor License Agreement: + * http://esotericsoftware.com/spine-editor-license + * + * Otherwise, it is permitted to integrate the Spine Runtimes into software + * or otherwise create derivative works of the Spine Runtimes (collectively, + * "Products"), provided that each user of the Products must obtain their own + * Spine Editor license and redistribution of the Products in any form must + * include this license and copyright notice. + * + * THIS SOFTWARE IS PROVIDED BY ESOTERIC SOFTWARE LLC "AS IS" AND ANY EXPRESS + * OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES + * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN + * NO EVENT SHALL ESOTERIC SOFTWARE LLC BE LIABLE FOR ANY DIRECT, INDIRECT, + * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, + * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES, BUSINESS + * INTERRUPTION, OR LOSS OF USE, DATA, OR PROFITS) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, + * EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + *****************************************************************************/ + +using System; + +namespace SpineRuntime37 { + + /// + /// An AttachmentLoader that configures attachments using texture regions from an Atlas. + /// See Loading Skeleton Data in the Spine Runtimes Guide. + /// + public class AtlasAttachmentLoader : AttachmentLoader { + private Atlas[] atlasArray; + + public AtlasAttachmentLoader (params Atlas[] atlasArray) { + if (atlasArray == null) throw new ArgumentNullException("atlas array cannot be null."); + this.atlasArray = atlasArray; + } + + public RegionAttachment NewRegionAttachment (Skin skin, string name, string path) { + AtlasRegion region = FindRegion(path); + if (region == null) throw new ArgumentException(string.Format("Region not found in atlas: {0} (region attachment: {1})", path, name)); + RegionAttachment attachment = new RegionAttachment(name); + attachment.RendererObject = region; + attachment.SetUVs(region.u, region.v, region.u2, region.v2, region.rotate); + attachment.regionOffsetX = region.offsetX; + attachment.regionOffsetY = region.offsetY; + attachment.regionWidth = region.width; + attachment.regionHeight = region.height; + attachment.regionOriginalWidth = region.originalWidth; + attachment.regionOriginalHeight = region.originalHeight; + return attachment; + } + + public MeshAttachment NewMeshAttachment (Skin skin, string name, string path) { + AtlasRegion region = FindRegion(path); + if (region == null) throw new ArgumentException(string.Format("Region not found in atlas: {0} (region attachment: {1})", path, name)); + MeshAttachment attachment = new MeshAttachment(name); + attachment.RendererObject = region; + attachment.RegionU = region.u; + attachment.RegionV = region.v; + attachment.RegionU2 = region.u2; + attachment.RegionV2 = region.v2; + attachment.RegionRotate = region.rotate; + attachment.regionOffsetX = region.offsetX; + attachment.regionOffsetY = region.offsetY; + attachment.regionWidth = region.width; + attachment.regionHeight = region.height; + attachment.regionOriginalWidth = region.originalWidth; + attachment.regionOriginalHeight = region.originalHeight; + return attachment; + } + + public BoundingBoxAttachment NewBoundingBoxAttachment (Skin skin, string name) { + return new BoundingBoxAttachment(name); + } + + public PathAttachment NewPathAttachment (Skin skin, string name) { + return new PathAttachment(name); + } + + public PointAttachment NewPointAttachment (Skin skin, string name) { + return new PointAttachment(name); + } + + public ClippingAttachment NewClippingAttachment(Skin skin, string name) { + return new ClippingAttachment(name); + } + + public AtlasRegion FindRegion (string name) { + AtlasRegion region; + + for (int i = 0; i < atlasArray.Length; i++) { + region = atlasArray[i].FindRegion(name); + if (region != null) + return region; + } + + return null; + } + } +} diff --git a/SpineRuntimes/SpineRuntime37/Attachments/Attachment.cs b/SpineRuntimes/SpineRuntime37/Attachments/Attachment.cs new file mode 100644 index 0000000..a2d8bef --- /dev/null +++ b/SpineRuntimes/SpineRuntime37/Attachments/Attachment.cs @@ -0,0 +1,49 @@ +/****************************************************************************** + * Spine Runtimes License Agreement + * Last updated May 1, 2019. Replaces all prior versions. + * + * Copyright (c) 2013-2019, Esoteric Software LLC + * + * Integration of the Spine Runtimes into software or otherwise creating + * derivative works of the Spine Runtimes is permitted under the terms and + * conditions of Section 2 of the Spine Editor License Agreement: + * http://esotericsoftware.com/spine-editor-license + * + * Otherwise, it is permitted to integrate the Spine Runtimes into software + * or otherwise create derivative works of the Spine Runtimes (collectively, + * "Products"), provided that each user of the Products must obtain their own + * Spine Editor license and redistribution of the Products in any form must + * include this license and copyright notice. + * + * THIS SOFTWARE IS PROVIDED BY ESOTERIC SOFTWARE LLC "AS IS" AND ANY EXPRESS + * OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES + * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN + * NO EVENT SHALL ESOTERIC SOFTWARE LLC BE LIABLE FOR ANY DIRECT, INDIRECT, + * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, + * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES, BUSINESS + * INTERRUPTION, OR LOSS OF USE, DATA, OR PROFITS) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, + * EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + *****************************************************************************/ + +using System; + +namespace SpineRuntime37 { + abstract public class Attachment { + public string Name { get; private set; } + + protected Attachment (string name) { + if (name == null) throw new ArgumentNullException("name", "name cannot be null"); + Name = name; + } + + override public string ToString () { + return Name; + } + } + + public interface IHasRendererObject { + object RendererObject { get; set; } + } +} diff --git a/SpineRuntimes/SpineRuntime37/Attachments/AttachmentLoader.cs b/SpineRuntimes/SpineRuntime37/Attachments/AttachmentLoader.cs new file mode 100644 index 0000000..d1531f1 --- /dev/null +++ b/SpineRuntimes/SpineRuntime37/Attachments/AttachmentLoader.cs @@ -0,0 +1,48 @@ +/****************************************************************************** + * Spine Runtimes License Agreement + * Last updated May 1, 2019. Replaces all prior versions. + * + * Copyright (c) 2013-2019, Esoteric Software LLC + * + * Integration of the Spine Runtimes into software or otherwise creating + * derivative works of the Spine Runtimes is permitted under the terms and + * conditions of Section 2 of the Spine Editor License Agreement: + * http://esotericsoftware.com/spine-editor-license + * + * Otherwise, it is permitted to integrate the Spine Runtimes into software + * or otherwise create derivative works of the Spine Runtimes (collectively, + * "Products"), provided that each user of the Products must obtain their own + * Spine Editor license and redistribution of the Products in any form must + * include this license and copyright notice. + * + * THIS SOFTWARE IS PROVIDED BY ESOTERIC SOFTWARE LLC "AS IS" AND ANY EXPRESS + * OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES + * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN + * NO EVENT SHALL ESOTERIC SOFTWARE LLC BE LIABLE FOR ANY DIRECT, INDIRECT, + * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, + * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES, BUSINESS + * INTERRUPTION, OR LOSS OF USE, DATA, OR PROFITS) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, + * EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + *****************************************************************************/ + +namespace SpineRuntime37 { + public interface AttachmentLoader { + /// May be null to not load any attachment. + RegionAttachment NewRegionAttachment (Skin skin, string name, string path); + + /// May be null to not load any attachment. + MeshAttachment NewMeshAttachment (Skin skin, string name, string path); + + /// May be null to not load any attachment. + BoundingBoxAttachment NewBoundingBoxAttachment (Skin skin, string name); + + /// May be null to not load any attachment + PathAttachment NewPathAttachment (Skin skin, string name); + + PointAttachment NewPointAttachment (Skin skin, string name); + + ClippingAttachment NewClippingAttachment (Skin skin, string name); + } +} diff --git a/SpineRuntimes/SpineRuntime37/Attachments/AttachmentType.cs b/SpineRuntimes/SpineRuntime37/Attachments/AttachmentType.cs new file mode 100644 index 0000000..7801d80 --- /dev/null +++ b/SpineRuntimes/SpineRuntime37/Attachments/AttachmentType.cs @@ -0,0 +1,34 @@ +/****************************************************************************** + * Spine Runtimes License Agreement + * Last updated May 1, 2019. Replaces all prior versions. + * + * Copyright (c) 2013-2019, Esoteric Software LLC + * + * Integration of the Spine Runtimes into software or otherwise creating + * derivative works of the Spine Runtimes is permitted under the terms and + * conditions of Section 2 of the Spine Editor License Agreement: + * http://esotericsoftware.com/spine-editor-license + * + * Otherwise, it is permitted to integrate the Spine Runtimes into software + * or otherwise create derivative works of the Spine Runtimes (collectively, + * "Products"), provided that each user of the Products must obtain their own + * Spine Editor license and redistribution of the Products in any form must + * include this license and copyright notice. + * + * THIS SOFTWARE IS PROVIDED BY ESOTERIC SOFTWARE LLC "AS IS" AND ANY EXPRESS + * OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES + * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN + * NO EVENT SHALL ESOTERIC SOFTWARE LLC BE LIABLE FOR ANY DIRECT, INDIRECT, + * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, + * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES, BUSINESS + * INTERRUPTION, OR LOSS OF USE, DATA, OR PROFITS) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, + * EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + *****************************************************************************/ + +namespace SpineRuntime37 { + public enum AttachmentType { + Region, Boundingbox, Mesh, Linkedmesh, Path, Point, Clipping + } +} diff --git a/SpineRuntimes/SpineRuntime37/Attachments/BoundingBoxAttachment.cs b/SpineRuntimes/SpineRuntime37/Attachments/BoundingBoxAttachment.cs new file mode 100644 index 0000000..af28366 --- /dev/null +++ b/SpineRuntimes/SpineRuntime37/Attachments/BoundingBoxAttachment.cs @@ -0,0 +1,39 @@ +/****************************************************************************** + * Spine Runtimes License Agreement + * Last updated May 1, 2019. Replaces all prior versions. + * + * Copyright (c) 2013-2019, Esoteric Software LLC + * + * Integration of the Spine Runtimes into software or otherwise creating + * derivative works of the Spine Runtimes is permitted under the terms and + * conditions of Section 2 of the Spine Editor License Agreement: + * http://esotericsoftware.com/spine-editor-license + * + * Otherwise, it is permitted to integrate the Spine Runtimes into software + * or otherwise create derivative works of the Spine Runtimes (collectively, + * "Products"), provided that each user of the Products must obtain their own + * Spine Editor license and redistribution of the Products in any form must + * include this license and copyright notice. + * + * THIS SOFTWARE IS PROVIDED BY ESOTERIC SOFTWARE LLC "AS IS" AND ANY EXPRESS + * OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES + * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN + * NO EVENT SHALL ESOTERIC SOFTWARE LLC BE LIABLE FOR ANY DIRECT, INDIRECT, + * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, + * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES, BUSINESS + * INTERRUPTION, OR LOSS OF USE, DATA, OR PROFITS) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, + * EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + *****************************************************************************/ + +using System; + +namespace SpineRuntime37 { + /// Attachment that has a polygon for bounds checking. + public class BoundingBoxAttachment : VertexAttachment { + public BoundingBoxAttachment (string name) + : base(name) { + } + } +} diff --git a/SpineRuntimes/SpineRuntime37/Attachments/ClippingAttachment.cs b/SpineRuntimes/SpineRuntime37/Attachments/ClippingAttachment.cs new file mode 100644 index 0000000..5f27dc8 --- /dev/null +++ b/SpineRuntimes/SpineRuntime37/Attachments/ClippingAttachment.cs @@ -0,0 +1,41 @@ +/****************************************************************************** + * Spine Runtimes License Agreement + * Last updated May 1, 2019. Replaces all prior versions. + * + * Copyright (c) 2013-2019, Esoteric Software LLC + * + * Integration of the Spine Runtimes into software or otherwise creating + * derivative works of the Spine Runtimes is permitted under the terms and + * conditions of Section 2 of the Spine Editor License Agreement: + * http://esotericsoftware.com/spine-editor-license + * + * Otherwise, it is permitted to integrate the Spine Runtimes into software + * or otherwise create derivative works of the Spine Runtimes (collectively, + * "Products"), provided that each user of the Products must obtain their own + * Spine Editor license and redistribution of the Products in any form must + * include this license and copyright notice. + * + * THIS SOFTWARE IS PROVIDED BY ESOTERIC SOFTWARE LLC "AS IS" AND ANY EXPRESS + * OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES + * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN + * NO EVENT SHALL ESOTERIC SOFTWARE LLC BE LIABLE FOR ANY DIRECT, INDIRECT, + * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, + * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES, BUSINESS + * INTERRUPTION, OR LOSS OF USE, DATA, OR PROFITS) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, + * EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + *****************************************************************************/ + +using System; + +namespace SpineRuntime37 { + public class ClippingAttachment : VertexAttachment { + internal SlotData endSlot; + + public SlotData EndSlot { get { return endSlot; } set { endSlot = value; } } + + public ClippingAttachment(string name) : base(name) { + } + } +} diff --git a/SpineRuntimes/SpineRuntime37/Attachments/MeshAttachment.cs b/SpineRuntimes/SpineRuntime37/Attachments/MeshAttachment.cs new file mode 100644 index 0000000..a60c9d2 --- /dev/null +++ b/SpineRuntimes/SpineRuntime37/Attachments/MeshAttachment.cs @@ -0,0 +1,133 @@ +/****************************************************************************** + * Spine Runtimes License Agreement + * Last updated May 1, 2019. Replaces all prior versions. + * + * Copyright (c) 2013-2019, Esoteric Software LLC + * + * Integration of the Spine Runtimes into software or otherwise creating + * derivative works of the Spine Runtimes is permitted under the terms and + * conditions of Section 2 of the Spine Editor License Agreement: + * http://esotericsoftware.com/spine-editor-license + * + * Otherwise, it is permitted to integrate the Spine Runtimes into software + * or otherwise create derivative works of the Spine Runtimes (collectively, + * "Products"), provided that each user of the Products must obtain their own + * Spine Editor license and redistribution of the Products in any form must + * include this license and copyright notice. + * + * THIS SOFTWARE IS PROVIDED BY ESOTERIC SOFTWARE LLC "AS IS" AND ANY EXPRESS + * OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES + * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN + * NO EVENT SHALL ESOTERIC SOFTWARE LLC BE LIABLE FOR ANY DIRECT, INDIRECT, + * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, + * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES, BUSINESS + * INTERRUPTION, OR LOSS OF USE, DATA, OR PROFITS) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, + * EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + *****************************************************************************/ + +using System; + +namespace SpineRuntime37 { + /// Attachment that displays a texture region using a mesh. + public class MeshAttachment : VertexAttachment, IHasRendererObject { + internal float regionOffsetX, regionOffsetY, regionWidth, regionHeight, regionOriginalWidth, regionOriginalHeight; + private MeshAttachment parentMesh; + internal float[] uvs, regionUVs; + internal int[] triangles; + internal float r = 1, g = 1, b = 1, a = 1; + internal int hulllength; + internal bool inheritDeform; + + public int HullLength { get { return hulllength; } set { hulllength = value; } } + public float[] RegionUVs { get { return regionUVs; } set { regionUVs = value; } } + /// The UV pair for each vertex, normalized within the entire texture. + public float[] UVs { get { return uvs; } set { uvs = value; } } + public int[] Triangles { get { return triangles; } set { triangles = value; } } + + public float R { get { return r; } set { r = value; } } + public float G { get { return g; } set { g = value; } } + public float B { get { return b; } set { b = value; } } + public float A { get { return a; } set { a = value; } } + + public string Path { get; set; } + public object RendererObject { get; set; } + public float RegionU { get; set; } + public float RegionV { get; set; } + public float RegionU2 { get; set; } + public float RegionV2 { get; set; } + public bool RegionRotate { get; set; } + public float RegionOffsetX { get { return regionOffsetX; } set { regionOffsetX = value; } } + public float RegionOffsetY { get { return regionOffsetY; } set { regionOffsetY = value; } } // Pixels stripped from the bottom left, unrotated. + public float RegionWidth { get { return regionWidth; } set { regionWidth = value; } } + public float RegionHeight { get { return regionHeight; } set { regionHeight = value; } } // Unrotated, stripped size. + public float RegionOriginalWidth { get { return regionOriginalWidth; } set { regionOriginalWidth = value; } } + public float RegionOriginalHeight { get { return regionOriginalHeight; } set { regionOriginalHeight = value; } } // Unrotated, unstripped size. + + public bool InheritDeform { get { return inheritDeform; } set { inheritDeform = value; } } + + public MeshAttachment ParentMesh { + get { return parentMesh; } + set { + parentMesh = value; + if (value != null) { + bones = value.bones; + vertices = value.vertices; + worldVerticesLength = value.worldVerticesLength; + regionUVs = value.regionUVs; + triangles = value.triangles; + HullLength = value.HullLength; + Edges = value.Edges; + Width = value.Width; + Height = value.Height; + } + } + } + + // Nonessential. + public int[] Edges { get; set; } + public float Width { get; set; } + public float Height { get; set; } + + public MeshAttachment (string name) + : base(name) { + } + + public void UpdateUVs () { + float[] regionUVs = this.regionUVs; + if (this.uvs == null || this.uvs.Length != regionUVs.Length) this.uvs = new float[regionUVs.Length]; + float[] uvs = this.uvs; + + if (RegionRotate) { + float textureHeight = this.regionWidth / (RegionV2 - RegionV); + float textureWidth = this.regionHeight / (RegionU2 - RegionU); + float u = RegionU - (RegionOriginalHeight - RegionOffsetY - RegionHeight) / textureWidth; + float v = RegionV - (RegionOriginalWidth - RegionOffsetX - RegionWidth) / textureHeight; + float width = RegionOriginalHeight / textureWidth; + float height = RegionOriginalWidth / textureHeight; + + for (int i = 0, n = uvs.Length; i < n; i += 2) { + uvs[i] = u + regionUVs[i + 1] * width; + uvs[i + 1] = v + height - regionUVs[i] * height; + } + } else { + float textureWidth = this.regionWidth / (RegionU2 - RegionU); + float textureHeight = this.regionHeight / (RegionV2 - RegionV); + float u = RegionU - RegionOffsetX / textureWidth; + float v = RegionV - (RegionOriginalHeight - RegionOffsetY - RegionHeight) / textureHeight; + float width = RegionOriginalWidth / textureWidth; + float height = RegionOriginalHeight / textureHeight; + + for (int i = 0, n = uvs.Length; i < n; i += 2) { + uvs[i] = u + regionUVs[i] * width; + uvs[i + 1] = v + regionUVs[i + 1] * height; + } + } + } + + override public bool ApplyDeform (VertexAttachment sourceAttachment) { + return this == sourceAttachment || (inheritDeform && parentMesh == sourceAttachment); + } + } +} diff --git a/SpineRuntimes/SpineRuntime37/Attachments/PathAttachment.cs b/SpineRuntimes/SpineRuntime37/Attachments/PathAttachment.cs new file mode 100644 index 0000000..bd488ff --- /dev/null +++ b/SpineRuntimes/SpineRuntime37/Attachments/PathAttachment.cs @@ -0,0 +1,47 @@ +/****************************************************************************** + * Spine Runtimes License Agreement + * Last updated May 1, 2019. Replaces all prior versions. + * + * Copyright (c) 2013-2019, Esoteric Software LLC + * + * Integration of the Spine Runtimes into software or otherwise creating + * derivative works of the Spine Runtimes is permitted under the terms and + * conditions of Section 2 of the Spine Editor License Agreement: + * http://esotericsoftware.com/spine-editor-license + * + * Otherwise, it is permitted to integrate the Spine Runtimes into software + * or otherwise create derivative works of the Spine Runtimes (collectively, + * "Products"), provided that each user of the Products must obtain their own + * Spine Editor license and redistribution of the Products in any form must + * include this license and copyright notice. + * + * THIS SOFTWARE IS PROVIDED BY ESOTERIC SOFTWARE LLC "AS IS" AND ANY EXPRESS + * OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES + * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN + * NO EVENT SHALL ESOTERIC SOFTWARE LLC BE LIABLE FOR ANY DIRECT, INDIRECT, + * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, + * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES, BUSINESS + * INTERRUPTION, OR LOSS OF USE, DATA, OR PROFITS) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, + * EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + *****************************************************************************/ + +using System; +using System.Collections.Generic; + +namespace SpineRuntime37 { + public class PathAttachment : VertexAttachment { + internal float[] lengths; + internal bool closed, constantSpeed; + + /// The length in the setup pose from the start of the path to the end of each curve. + public float[] Lengths { get { return lengths; } set { lengths = value; } } + public bool Closed { get { return closed; } set { closed = value; } } + public bool ConstantSpeed { get { return constantSpeed; } set { constantSpeed = value; } } + + public PathAttachment (String name) + : base(name) { + } + } +} diff --git a/SpineRuntimes/SpineRuntime37/Attachments/PointAttachment.cs b/SpineRuntimes/SpineRuntime37/Attachments/PointAttachment.cs new file mode 100644 index 0000000..c8587e1 --- /dev/null +++ b/SpineRuntimes/SpineRuntime37/Attachments/PointAttachment.cs @@ -0,0 +1,59 @@ +/****************************************************************************** + * Spine Runtimes License Agreement + * Last updated May 1, 2019. Replaces all prior versions. + * + * Copyright (c) 2013-2019, Esoteric Software LLC + * + * Integration of the Spine Runtimes into software or otherwise creating + * derivative works of the Spine Runtimes is permitted under the terms and + * conditions of Section 2 of the Spine Editor License Agreement: + * http://esotericsoftware.com/spine-editor-license + * + * Otherwise, it is permitted to integrate the Spine Runtimes into software + * or otherwise create derivative works of the Spine Runtimes (collectively, + * "Products"), provided that each user of the Products must obtain their own + * Spine Editor license and redistribution of the Products in any form must + * include this license and copyright notice. + * + * THIS SOFTWARE IS PROVIDED BY ESOTERIC SOFTWARE LLC "AS IS" AND ANY EXPRESS + * OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES + * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN + * NO EVENT SHALL ESOTERIC SOFTWARE LLC BE LIABLE FOR ANY DIRECT, INDIRECT, + * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, + * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES, BUSINESS + * INTERRUPTION, OR LOSS OF USE, DATA, OR PROFITS) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, + * EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + *****************************************************************************/ + +namespace SpineRuntime37 { + /// + /// An attachment which is a single point and a rotation. This can be used to spawn projectiles, particles, etc. A bone can be + /// used in similar ways, but a PointAttachment is slightly less expensive to compute and can be hidden, shown, and placed in a + /// skin. + ///

+ /// See Point Attachments in the Spine User Guide. + ///

+ public class PointAttachment : Attachment { + internal float x, y, rotation; + public float X { get { return x; } set { x = value; } } + public float Y { get { return y; } set { y = value; } } + public float Rotation { get { return rotation; } set { rotation = value; } } + + public PointAttachment (string name) + : base(name) { + } + + public void ComputeWorldPosition (Bone bone, out float ox, out float oy) { + bone.LocalToWorld(this.x, this.y, out ox, out oy); + } + + public float ComputeWorldRotation (Bone bone) { + float cos = MathUtils.CosDeg(rotation), sin = MathUtils.SinDeg(rotation); + float ix = cos * bone.a + sin * bone.b; + float iy = cos * bone.c + sin * bone.d; + return MathUtils.Atan2(iy, ix) * MathUtils.RadDeg; + } + } +} diff --git a/SpineRuntimes/SpineRuntime37/Attachments/RegionAttachment.cs b/SpineRuntimes/SpineRuntime37/Attachments/RegionAttachment.cs new file mode 100644 index 0000000..e119fa7 --- /dev/null +++ b/SpineRuntimes/SpineRuntime37/Attachments/RegionAttachment.cs @@ -0,0 +1,182 @@ +/****************************************************************************** + * Spine Runtimes License Agreement + * Last updated May 1, 2019. Replaces all prior versions. + * + * Copyright (c) 2013-2019, Esoteric Software LLC + * + * Integration of the Spine Runtimes into software or otherwise creating + * derivative works of the Spine Runtimes is permitted under the terms and + * conditions of Section 2 of the Spine Editor License Agreement: + * http://esotericsoftware.com/spine-editor-license + * + * Otherwise, it is permitted to integrate the Spine Runtimes into software + * or otherwise create derivative works of the Spine Runtimes (collectively, + * "Products"), provided that each user of the Products must obtain their own + * Spine Editor license and redistribution of the Products in any form must + * include this license and copyright notice. + * + * THIS SOFTWARE IS PROVIDED BY ESOTERIC SOFTWARE LLC "AS IS" AND ANY EXPRESS + * OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES + * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN + * NO EVENT SHALL ESOTERIC SOFTWARE LLC BE LIABLE FOR ANY DIRECT, INDIRECT, + * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, + * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES, BUSINESS + * INTERRUPTION, OR LOSS OF USE, DATA, OR PROFITS) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, + * EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + *****************************************************************************/ + +using System; + +namespace SpineRuntime37 { + /// Attachment that displays a texture region. + public class RegionAttachment : Attachment, IHasRendererObject { + public const int BLX = 0; + public const int BLY = 1; + public const int ULX = 2; + public const int ULY = 3; + public const int URX = 4; + public const int URY = 5; + public const int BRX = 6; + public const int BRY = 7; + + internal float x, y, rotation, scaleX = 1, scaleY = 1, width, height; + internal float regionOffsetX, regionOffsetY, regionWidth, regionHeight, regionOriginalWidth, regionOriginalHeight; + internal float[] offset = new float[8], uvs = new float[8]; + internal float r = 1, g = 1, b = 1, a = 1; + + public float X { get { return x; } set { x = value; } } + public float Y { get { return y; } set { y = value; } } + public float Rotation { get { return rotation; } set { rotation = value; } } + public float ScaleX { get { return scaleX; } set { scaleX = value; } } + public float ScaleY { get { return scaleY; } set { scaleY = value; } } + public float Width { get { return width; } set { width = value; } } + public float Height { get { return height; } set { height = value; } } + + public float R { get { return r; } set { r = value; } } + public float G { get { return g; } set { g = value; } } + public float B { get { return b; } set { b = value; } } + public float A { get { return a; } set { a = value; } } + + public string Path { get; set; } + public object RendererObject { get; set; } + public float RegionOffsetX { get { return regionOffsetX; } set { regionOffsetX = value; } } + public float RegionOffsetY { get { return regionOffsetY; } set { regionOffsetY = value; } } // Pixels stripped from the bottom left, unrotated. + public float RegionWidth { get { return regionWidth; } set { regionWidth = value; } } + public float RegionHeight { get { return regionHeight; } set { regionHeight = value; } } // Unrotated, stripped size. + public float RegionOriginalWidth { get { return regionOriginalWidth; } set { regionOriginalWidth = value; } } + public float RegionOriginalHeight { get { return regionOriginalHeight; } set { regionOriginalHeight = value; } } // Unrotated, unstripped size. + + public float[] Offset { get { return offset; } } + public float[] UVs { get { return uvs; } } + + public RegionAttachment (string name) + : base(name) { + } + + public void UpdateOffset () { + float width = this.width; + float height = this.height; + float localX2 = width * 0.5f; + float localY2 = height * 0.5f; + float localX = -localX2; + float localY = -localY2; + if (regionOriginalWidth != 0) { // if (region != null) + localX += regionOffsetX / regionOriginalWidth * width; + localY += regionOffsetY / regionOriginalHeight * height; + localX2 -= (regionOriginalWidth - regionOffsetX - regionWidth) / regionOriginalWidth * width; + localY2 -= (regionOriginalHeight - regionOffsetY - regionHeight) / regionOriginalHeight * height; + } + float scaleX = this.scaleX; + float scaleY = this.scaleY; + localX *= scaleX; + localY *= scaleY; + localX2 *= scaleX; + localY2 *= scaleY; + float rotation = this.rotation; + float cos = MathUtils.CosDeg(rotation); + float sin = MathUtils.SinDeg(rotation); + float x = this.x; + float y = this.y; + float localXCos = localX * cos + x; + float localXSin = localX * sin; + float localYCos = localY * cos + y; + float localYSin = localY * sin; + float localX2Cos = localX2 * cos + x; + float localX2Sin = localX2 * sin; + float localY2Cos = localY2 * cos + y; + float localY2Sin = localY2 * sin; + float[] offset = this.offset; + offset[BLX] = localXCos - localYSin; + offset[BLY] = localYCos + localXSin; + offset[ULX] = localXCos - localY2Sin; + offset[ULY] = localY2Cos + localXSin; + offset[URX] = localX2Cos - localY2Sin; + offset[URY] = localY2Cos + localX2Sin; + offset[BRX] = localX2Cos - localYSin; + offset[BRY] = localYCos + localX2Sin; + } + + public void SetUVs (float u, float v, float u2, float v2, bool rotate) { + float[] uvs = this.uvs; + // UV values differ from RegionAttachment.java + if (rotate) { + uvs[URX] = u; + uvs[URY] = v2; + uvs[BRX] = u; + uvs[BRY] = v; + uvs[BLX] = u2; + uvs[BLY] = v; + uvs[ULX] = u2; + uvs[ULY] = v2; + } else { + uvs[ULX] = u; + uvs[ULY] = v2; + uvs[URX] = u; + uvs[URY] = v; + uvs[BRX] = u2; + uvs[BRY] = v; + uvs[BLX] = u2; + uvs[BLY] = v2; + } + } + + /// Transforms the attachment's four vertices to world coordinates. + /// The parent bone. + /// The output world vertices. Must have a length greater than or equal to offset + 8. + /// The worldVertices index to begin writing values. + /// The number of worldVertices entries between the value pairs written. + public void ComputeWorldVertices (Bone bone, float[] worldVertices, int offset, int stride = 2) { + float[] vertexOffset = this.offset; + float bwx = bone.worldX, bwy = bone.worldY; + float a = bone.a, b = bone.b, c = bone.c, d = bone.d; + float offsetX, offsetY; + + // Vertex order is different from RegionAttachment.java + offsetX = vertexOffset[BRX]; // 0 + offsetY = vertexOffset[BRY]; // 1 + worldVertices[offset] = offsetX * a + offsetY * b + bwx; // bl + worldVertices[offset + 1] = offsetX * c + offsetY * d + bwy; + offset += stride; + + offsetX = vertexOffset[BLX]; // 2 + offsetY = vertexOffset[BLY]; // 3 + worldVertices[offset] = offsetX * a + offsetY * b + bwx; // ul + worldVertices[offset + 1] = offsetX * c + offsetY * d + bwy; + offset += stride; + + offsetX = vertexOffset[ULX]; // 4 + offsetY = vertexOffset[ULY]; // 5 + worldVertices[offset] = offsetX * a + offsetY * b + bwx; // ur + worldVertices[offset + 1] = offsetX * c + offsetY * d + bwy; + offset += stride; + + offsetX = vertexOffset[URX]; // 6 + offsetY = vertexOffset[URY]; // 7 + worldVertices[offset] = offsetX * a + offsetY * b + bwx; // br + worldVertices[offset + 1] = offsetX * c + offsetY * d + bwy; + //offset += stride; + } + } +} diff --git a/SpineRuntimes/SpineRuntime37/Attachments/VertexAttachment.cs b/SpineRuntimes/SpineRuntime37/Attachments/VertexAttachment.cs new file mode 100644 index 0000000..d595089 --- /dev/null +++ b/SpineRuntimes/SpineRuntime37/Attachments/VertexAttachment.cs @@ -0,0 +1,129 @@ +/****************************************************************************** + * Spine Runtimes License Agreement + * Last updated May 1, 2019. Replaces all prior versions. + * + * Copyright (c) 2013-2019, Esoteric Software LLC + * + * Integration of the Spine Runtimes into software or otherwise creating + * derivative works of the Spine Runtimes is permitted under the terms and + * conditions of Section 2 of the Spine Editor License Agreement: + * http://esotericsoftware.com/spine-editor-license + * + * Otherwise, it is permitted to integrate the Spine Runtimes into software + * or otherwise create derivative works of the Spine Runtimes (collectively, + * "Products"), provided that each user of the Products must obtain their own + * Spine Editor license and redistribution of the Products in any form must + * include this license and copyright notice. + * + * THIS SOFTWARE IS PROVIDED BY ESOTERIC SOFTWARE LLC "AS IS" AND ANY EXPRESS + * OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES + * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN + * NO EVENT SHALL ESOTERIC SOFTWARE LLC BE LIABLE FOR ANY DIRECT, INDIRECT, + * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, + * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES, BUSINESS + * INTERRUPTION, OR LOSS OF USE, DATA, OR PROFITS) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, + * EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + *****************************************************************************/ + +using System; + +namespace SpineRuntime37 { + /// >An attachment with vertices that are transformed by one or more bones and can be deformed by a slot's vertices. + public class VertexAttachment : Attachment { + static int nextID = 0; + static readonly Object nextIdLock = new Object(); + + internal readonly int id; + internal int[] bones; + internal float[] vertices; + internal int worldVerticesLength; + + /// Gets a unique ID for this attachment. + public int Id { get { return id; } } + public int[] Bones { get { return bones; } set { bones = value; } } + public float[] Vertices { get { return vertices; } set { vertices = value; } } + public int WorldVerticesLength { get { return worldVerticesLength; } set { worldVerticesLength = value; } } + + public VertexAttachment (string name) + : base(name) { + + lock (VertexAttachment.nextIdLock) { + id = (VertexAttachment.nextID++ & 65535) << 11; + } + } + + public void ComputeWorldVertices (Slot slot, float[] worldVertices) { + ComputeWorldVertices(slot, 0, worldVerticesLength, worldVertices, 0); + } + + /// Transforms local vertices to world coordinates. + /// The index of the first value to transform. Each vertex has 2 values, x and y. + /// The number of world vertex values to output. Must be less than or equal to - start. + /// The output world vertices. Must have a length greater than or equal to + . + /// The index to begin writing values. + /// The number of entries between the value pairs written. + public void ComputeWorldVertices (Slot slot, int start, int count, float[] worldVertices, int offset, int stride = 2) { + count = offset + (count >> 1) * stride; + Skeleton skeleton = slot.bone.skeleton; + var deformArray = slot.attachmentVertices; + float[] vertices = this.vertices; + int[] bones = this.bones; + if (bones == null) { + if (deformArray.Count > 0) vertices = deformArray.Items; + Bone bone = slot.bone; + float x = bone.worldX, y = bone.worldY; + float a = bone.a, b = bone.b, c = bone.c, d = bone.d; + for (int vv = start, w = offset; w < count; vv += 2, w += stride) { + float vx = vertices[vv], vy = vertices[vv + 1]; + worldVertices[w] = vx * a + vy * b + x; + worldVertices[w + 1] = vx * c + vy * d + y; + } + return; + } + int v = 0, skip = 0; + for (int i = 0; i < start; i += 2) { + int n = bones[v]; + v += n + 1; + skip += n; + } + var skeletonBones = skeleton.bones.Items; + if (deformArray.Count == 0) { + for (int w = offset, b = skip * 3; w < count; w += stride) { + float wx = 0, wy = 0; + int n = bones[v++]; + n += v; + for (; v < n; v++, b += 3) { + Bone bone = skeletonBones[bones[v]]; + float vx = vertices[b], vy = vertices[b + 1], weight = vertices[b + 2]; + wx += (vx * bone.a + vy * bone.b + bone.worldX) * weight; + wy += (vx * bone.c + vy * bone.d + bone.worldY) * weight; + } + worldVertices[w] = wx; + worldVertices[w + 1] = wy; + } + } else { + float[] deform = deformArray.Items; + for (int w = offset, b = skip * 3, f = skip << 1; w < count; w += stride) { + float wx = 0, wy = 0; + int n = bones[v++]; + n += v; + for (; v < n; v++, b += 3, f += 2) { + Bone bone = skeletonBones[bones[v]]; + float vx = vertices[b] + deform[f], vy = vertices[b + 1] + deform[f + 1], weight = vertices[b + 2]; + wx += (vx * bone.a + vy * bone.b + bone.worldX) * weight; + wy += (vx * bone.c + vy * bone.d + bone.worldY) * weight; + } + worldVertices[w] = wx; + worldVertices[w + 1] = wy; + } + } + } + + /// Returns true if a deform originally applied to the specified attachment should be applied to this attachment. + virtual public bool ApplyDeform (VertexAttachment sourceAttachment) { + return this == sourceAttachment; + } + } +} diff --git a/SpineRuntimes/SpineRuntime37/BlendMode.cs b/SpineRuntimes/SpineRuntime37/BlendMode.cs new file mode 100644 index 0000000..1f882bf --- /dev/null +++ b/SpineRuntimes/SpineRuntime37/BlendMode.cs @@ -0,0 +1,34 @@ +/****************************************************************************** + * Spine Runtimes License Agreement + * Last updated May 1, 2019. Replaces all prior versions. + * + * Copyright (c) 2013-2019, Esoteric Software LLC + * + * Integration of the Spine Runtimes into software or otherwise creating + * derivative works of the Spine Runtimes is permitted under the terms and + * conditions of Section 2 of the Spine Editor License Agreement: + * http://esotericsoftware.com/spine-editor-license + * + * Otherwise, it is permitted to integrate the Spine Runtimes into software + * or otherwise create derivative works of the Spine Runtimes (collectively, + * "Products"), provided that each user of the Products must obtain their own + * Spine Editor license and redistribution of the Products in any form must + * include this license and copyright notice. + * + * THIS SOFTWARE IS PROVIDED BY ESOTERIC SOFTWARE LLC "AS IS" AND ANY EXPRESS + * OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES + * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN + * NO EVENT SHALL ESOTERIC SOFTWARE LLC BE LIABLE FOR ANY DIRECT, INDIRECT, + * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, + * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES, BUSINESS + * INTERRUPTION, OR LOSS OF USE, DATA, OR PROFITS) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, + * EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + *****************************************************************************/ + +namespace SpineRuntime37 { + public enum BlendMode { + Normal, Additive, Multiply, Screen + } +} diff --git a/SpineRuntimes/SpineRuntime37/Bone.cs b/SpineRuntimes/SpineRuntime37/Bone.cs new file mode 100644 index 0000000..bcbea54 --- /dev/null +++ b/SpineRuntimes/SpineRuntime37/Bone.cs @@ -0,0 +1,361 @@ +/****************************************************************************** + * Spine Runtimes License Agreement + * Last updated May 1, 2019. Replaces all prior versions. + * + * Copyright (c) 2013-2019, Esoteric Software LLC + * + * Integration of the Spine Runtimes into software or otherwise creating + * derivative works of the Spine Runtimes is permitted under the terms and + * conditions of Section 2 of the Spine Editor License Agreement: + * http://esotericsoftware.com/spine-editor-license + * + * Otherwise, it is permitted to integrate the Spine Runtimes into software + * or otherwise create derivative works of the Spine Runtimes (collectively, + * "Products"), provided that each user of the Products must obtain their own + * Spine Editor license and redistribution of the Products in any form must + * include this license and copyright notice. + * + * THIS SOFTWARE IS PROVIDED BY ESOTERIC SOFTWARE LLC "AS IS" AND ANY EXPRESS + * OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES + * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN + * NO EVENT SHALL ESOTERIC SOFTWARE LLC BE LIABLE FOR ANY DIRECT, INDIRECT, + * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, + * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES, BUSINESS + * INTERRUPTION, OR LOSS OF USE, DATA, OR PROFITS) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, + * EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + *****************************************************************************/ + +using System; + +namespace SpineRuntime37 { + /// + /// Stores a bone's current pose. + /// + /// A bone has a local transform which is used to compute its world transform. A bone also has an applied transform, which is a + /// local transform that can be applied to compute the world transform. The local transform and applied transform may differ if a + /// constraint or application code modifies the world transform after it was computed from the local transform. + /// + /// + public class Bone : IUpdatable { + static public bool yDown; + + internal BoneData data; + internal Skeleton skeleton; + internal Bone parent; + internal ExposedList children = new ExposedList(); + internal float x, y, rotation, scaleX, scaleY, shearX, shearY; + internal float ax, ay, arotation, ascaleX, ascaleY, ashearX, ashearY; + internal bool appliedValid; + + internal float a, b, worldX; + internal float c, d, worldY; + + internal bool sorted; + + public BoneData Data { get { return data; } } + public Skeleton Skeleton { get { return skeleton; } } + public Bone Parent { get { return parent; } } + public ExposedList Children { get { return children; } } + /// The local X translation. + public float X { get { return x; } set { x = value; } } + /// The local Y translation. + public float Y { get { return y; } set { y = value; } } + /// The local rotation. + public float Rotation { get { return rotation; } set { rotation = value; } } + + /// The local scaleX. + public float ScaleX { get { return scaleX; } set { scaleX = value; } } + + /// The local scaleY. + public float ScaleY { get { return scaleY; } set { scaleY = value; } } + + /// The local shearX. + public float ShearX { get { return shearX; } set { shearX = value; } } + + /// The local shearY. + public float ShearY { get { return shearY; } set { shearY = value; } } + + /// The rotation, as calculated by any constraints. + public float AppliedRotation { get { return arotation; } set { arotation = value; } } + + /// The applied local x translation. + public float AX { get { return ax; } set { ax = value; } } + + /// The applied local y translation. + public float AY { get { return ay; } set { ay = value; } } + + /// The applied local scaleX. + public float AScaleX { get { return ascaleX; } set { ascaleX = value; } } + + /// The applied local scaleY. + public float AScaleY { get { return ascaleY; } set { ascaleY = value; } } + + /// The applied local shearX. + public float AShearX { get { return ashearX; } set { ashearX = value; } } + + /// The applied local shearY. + public float AShearY { get { return ashearY; } set { ashearY = value; } } + + public float A { get { return a; } } + public float B { get { return b; } } + public float C { get { return c; } } + public float D { get { return d; } } + + public float WorldX { get { return worldX; } } + public float WorldY { get { return worldY; } } + public float WorldRotationX { get { return MathUtils.Atan2(c, a) * MathUtils.RadDeg; } } + public float WorldRotationY { get { return MathUtils.Atan2(d, b) * MathUtils.RadDeg; } } + + /// Returns the magnitide (always positive) of the world scale X. + public float WorldScaleX { get { return (float)Math.Sqrt(a * a + c * c); } } + /// Returns the magnitide (always positive) of the world scale Y. + public float WorldScaleY { get { return (float)Math.Sqrt(b * b + d * d); } } + + /// May be null. + public Bone (BoneData data, Skeleton skeleton, Bone parent) { + if (data == null) throw new ArgumentNullException("data", "data cannot be null."); + if (skeleton == null) throw new ArgumentNullException("skeleton", "skeleton cannot be null."); + this.data = data; + this.skeleton = skeleton; + this.parent = parent; + SetToSetupPose(); + } + + /// Same as . This method exists for Bone to implement . + public void Update () { + UpdateWorldTransform(x, y, rotation, scaleX, scaleY, shearX, shearY); + } + + /// Computes the world transform using the parent bone and this bone's local transform. + public void UpdateWorldTransform () { + UpdateWorldTransform(x, y, rotation, scaleX, scaleY, shearX, shearY); + } + + /// Computes the world transform using the parent bone and the specified local transform. + public void UpdateWorldTransform (float x, float y, float rotation, float scaleX, float scaleY, float shearX, float shearY) { + ax = x; + ay = y; + arotation = rotation; + ascaleX = scaleX; + ascaleY = scaleY; + ashearX = shearX; + ashearY = shearY; + appliedValid = true; + Skeleton skeleton = this.skeleton; + + Bone parent = this.parent; + if (parent == null) { // Root bone. + 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; + + switch (data.transformMode) { + case TransformMode.Normal: { + 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; + a = pa * la + pb * lc; + b = pa * lb + pb * ld; + c = pc * la + pd * lc; + d = pc * lb + pd * ld; + return; + } + case TransformMode.OnlyTranslation: { + float rotationY = rotation + 90 + shearY; + a = MathUtils.CosDeg(rotation + shearX) * scaleX; + b = MathUtils.CosDeg(rotationY) * scaleY; + c = MathUtils.SinDeg(rotation + shearX) * scaleX; + d = MathUtils.SinDeg(rotationY) * scaleY; + break; + } + case TransformMode.NoRotationOrReflection: { + float s = pa * pa + pc * pc, prx; + if (s > 0.0001f) { + s = Math.Abs(pa * pd - pb * pc) / s; + pb = pc * s; + pd = pa * s; + prx = MathUtils.Atan2(pc, pa) * MathUtils.RadDeg; + } else { + pa = 0; + pc = 0; + prx = 90 - MathUtils.Atan2(pd, pb) * MathUtils.RadDeg; + } + float rx = rotation + shearX - prx; + float ry = rotation + shearY - prx + 90; + float la = MathUtils.CosDeg(rx) * scaleX; + float lb = MathUtils.CosDeg(ry) * scaleY; + float lc = MathUtils.SinDeg(rx) * scaleX; + float ld = MathUtils.SinDeg(ry) * scaleY; + a = pa * la - pb * lc; + b = pa * lb - pb * ld; + c = pc * la + pd * lc; + d = pc * lb + pd * ld; + break; + } + case TransformMode.NoScale: + case TransformMode.NoScaleOrReflection: { + float cos = MathUtils.CosDeg(rotation), sin = MathUtils.SinDeg(rotation); + 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; + float la = MathUtils.CosDeg(shearX) * scaleX; + float lb = MathUtils.CosDeg(90 + shearY) * scaleY; + float lc = MathUtils.SinDeg(shearX) * scaleX; + float ld = MathUtils.SinDeg(90 + shearY) * scaleY; + a = za * la + zb * lc; + b = za * lb + zb * ld; + c = zc * la + zd * lc; + d = zc * lb + zd * ld; + break; + } + } + + a *= skeleton.scaleX; + b *= skeleton.scaleX; + c *= skeleton.scaleY; + d *= skeleton.scaleY; + } + + public void SetToSetupPose () { + BoneData data = this.data; + x = data.x; + y = data.y; + rotation = data.rotation; + scaleX = data.scaleX; + scaleY = data.scaleY; + shearX = data.shearX; + shearY = data.shearY; + } + + /// + /// Computes the individual applied transform values from the world transform. This can be useful to perform processing using + /// the applied transform after the world transform has been modified directly (eg, by a constraint).. + /// + /// Some information is ambiguous in the world transform, such as -1,-1 scale versus 180 rotation. + /// + internal void UpdateAppliedTransform () { + appliedValid = true; + Bone parent = this.parent; + if (parent == null) { + ax = worldX; + ay = worldY; + arotation = MathUtils.Atan2(c, a) * MathUtils.RadDeg; + ascaleX = (float)Math.Sqrt(a * a + c * c); + ascaleY = (float)Math.Sqrt(b * b + d * d); + ashearX = 0; + ashearY = MathUtils.Atan2(a * b + c * d, a * d - b * c) * MathUtils.RadDeg; + return; + } + float pa = parent.a, pb = parent.b, pc = parent.c, pd = parent.d; + float pid = 1 / (pa * pd - pb * pc); + float dx = worldX - parent.worldX, dy = worldY - parent.worldY; + ax = (dx * pd * pid - dy * pb * pid); + ay = (dy * pa * pid - dx * pc * pid); + float ia = pid * pd; + float id = pid * pa; + float ib = pid * pb; + float ic = pid * pc; + float ra = ia * a - ib * c; + float rb = ia * b - ib * d; + float rc = id * c - ic * a; + float rd = id * d - ic * b; + ashearX = 0; + ascaleX = (float)Math.Sqrt(ra * ra + rc * rc); + if (ascaleX > 0.0001f) { + float det = ra * rd - rb * rc; + ascaleY = det / ascaleX; + ashearY = MathUtils.Atan2(ra * rb + rc * rd, det) * MathUtils.RadDeg; + arotation = MathUtils.Atan2(rc, ra) * MathUtils.RadDeg; + } else { + ascaleX = 0; + ascaleY = (float)Math.Sqrt(rb * rb + rd * rd); + ashearY = 0; + arotation = 90 - MathUtils.Atan2(rd, rb) * MathUtils.RadDeg; + } + } + + public void WorldToLocal (float worldX, float worldY, out float localX, out float localY) { + float a = this.a, b = this.b, c = this.c, d = this.d; + float invDet = 1 / (a * d - b * c); + float x = worldX - this.worldX, y = worldY - this.worldY; + localX = (x * d * invDet - y * b * invDet); + localY = (y * a * invDet - x * c * invDet); + } + + public void LocalToWorld (float localX, float localY, out float worldX, out float worldY) { + worldX = localX * a + localY * b + this.worldX; + worldY = localX * c + localY * d + this.worldY; + } + + public float WorldToLocalRotationX { + get { + Bone parent = this.parent; + if (parent == null) return arotation; + float pa = parent.a, pb = parent.b, pc = parent.c, pd = parent.d, a = this.a, c = this.c; + return MathUtils.Atan2(pa * c - pc * a, pd * a - pb * c) * MathUtils.RadDeg; + } + } + + public float WorldToLocalRotationY { + get { + Bone parent = this.parent; + if (parent == null) return arotation; + float pa = parent.a, pb = parent.b, pc = parent.c, pd = parent.d, b = this.b, d = this.d; + return MathUtils.Atan2(pa * d - pc * b, pd * b - pb * d) * MathUtils.RadDeg; + } + } + + public float WorldToLocalRotation (float worldRotation) { + float sin = MathUtils.SinDeg(worldRotation), cos = MathUtils.CosDeg(worldRotation); + return MathUtils.Atan2(a * sin - c * cos, d * cos - b * sin) * MathUtils.RadDeg + rotation - shearX; + } + + public float LocalToWorldRotation (float localRotation) { + localRotation -= rotation - shearX; + float sin = MathUtils.SinDeg(localRotation), cos = MathUtils.CosDeg(localRotation); + return MathUtils.Atan2(cos * c + sin * d, cos * a + sin * b) * MathUtils.RadDeg; + } + + /// + /// Rotates the world transform the specified amount and sets isAppliedValid to false. + /// + /// Degrees. + public void RotateWorld (float degrees) { + float a = this.a, b = this.b, c = this.c, d = this.d; + float cos = MathUtils.CosDeg(degrees), sin = MathUtils.SinDeg(degrees); + this.a = cos * a - sin * c; + this.b = cos * b - sin * d; + this.c = sin * a + cos * c; + this.d = sin * b + cos * d; + appliedValid = false; + } + + override public string ToString () { + return data.name; + } + } +} diff --git a/SpineRuntimes/SpineRuntime37/BoneData.cs b/SpineRuntimes/SpineRuntime37/BoneData.cs new file mode 100644 index 0000000..6735afe --- /dev/null +++ b/SpineRuntimes/SpineRuntime37/BoneData.cs @@ -0,0 +1,99 @@ +/****************************************************************************** + * Spine Runtimes License Agreement + * Last updated May 1, 2019. Replaces all prior versions. + * + * Copyright (c) 2013-2019, Esoteric Software LLC + * + * Integration of the Spine Runtimes into software or otherwise creating + * derivative works of the Spine Runtimes is permitted under the terms and + * conditions of Section 2 of the Spine Editor License Agreement: + * http://esotericsoftware.com/spine-editor-license + * + * Otherwise, it is permitted to integrate the Spine Runtimes into software + * or otherwise create derivative works of the Spine Runtimes (collectively, + * "Products"), provided that each user of the Products must obtain their own + * Spine Editor license and redistribution of the Products in any form must + * include this license and copyright notice. + * + * THIS SOFTWARE IS PROVIDED BY ESOTERIC SOFTWARE LLC "AS IS" AND ANY EXPRESS + * OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES + * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN + * NO EVENT SHALL ESOTERIC SOFTWARE LLC BE LIABLE FOR ANY DIRECT, INDIRECT, + * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, + * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES, BUSINESS + * INTERRUPTION, OR LOSS OF USE, DATA, OR PROFITS) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, + * EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + *****************************************************************************/ + +using System; + +namespace SpineRuntime37 { + public class BoneData { + internal int index; + internal string name; + internal BoneData parent; + internal float length; + internal float x, y, rotation, scaleX = 1, scaleY = 1, shearX, shearY; + internal TransformMode transformMode = TransformMode.Normal; + + /// The index of the bone in Skeleton.Bones + public int Index { get { return index; } } + + /// The name of the bone, which is unique within the skeleton. + public string Name { get { return name; } } + + /// May be null. + public BoneData Parent { get { return parent; } } + + public float Length { get { return length; } set { length = value; } } + + /// Local X translation. + public float X { get { return x; } set { x = value; } } + + /// Local Y translation. + public float Y { get { return y; } set { y = value; } } + + /// Local rotation. + public float Rotation { get { return rotation; } set { rotation = value; } } + + /// Local scaleX. + public float ScaleX { get { return scaleX; } set { scaleX = value; } } + + /// Local scaleY. + public float ScaleY { get { return scaleY; } set { scaleY = value; } } + + /// Local shearX. + public float ShearX { get { return shearX; } set { shearX = value; } } + + /// Local shearY. + public float ShearY { get { return shearY; } set { shearY = value; } } + + /// The transform mode for how parent world transforms affect this bone. + public TransformMode TransformMode { get { return transformMode; } set { transformMode = value; } } + + /// May be null. + public BoneData (int index, string name, BoneData parent) { + if (index < 0) throw new ArgumentException("index must be >= 0", "index"); + if (name == null) throw new ArgumentNullException("name", "name cannot be null."); + this.index = index; + this.name = name; + this.parent = parent; + } + + override public string ToString () { + return name; + } + } + + [Flags] + public enum TransformMode { + //0000 0 Flip Scale Rotation + Normal = 0, // 0000 + OnlyTranslation = 7, // 0111 + NoRotationOrReflection = 1, // 0001 + NoScale = 2, // 0010 + NoScaleOrReflection = 6, // 0110 + } +} diff --git a/SpineRuntimes/SpineRuntime37/Event.cs b/SpineRuntimes/SpineRuntime37/Event.cs new file mode 100644 index 0000000..fe9dc13 --- /dev/null +++ b/SpineRuntimes/SpineRuntime37/Event.cs @@ -0,0 +1,64 @@ +/****************************************************************************** + * Spine Runtimes License Agreement + * Last updated May 1, 2019. Replaces all prior versions. + * + * Copyright (c) 2013-2019, Esoteric Software LLC + * + * Integration of the Spine Runtimes into software or otherwise creating + * derivative works of the Spine Runtimes is permitted under the terms and + * conditions of Section 2 of the Spine Editor License Agreement: + * http://esotericsoftware.com/spine-editor-license + * + * Otherwise, it is permitted to integrate the Spine Runtimes into software + * or otherwise create derivative works of the Spine Runtimes (collectively, + * "Products"), provided that each user of the Products must obtain their own + * Spine Editor license and redistribution of the Products in any form must + * include this license and copyright notice. + * + * THIS SOFTWARE IS PROVIDED BY ESOTERIC SOFTWARE LLC "AS IS" AND ANY EXPRESS + * OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES + * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN + * NO EVENT SHALL ESOTERIC SOFTWARE LLC BE LIABLE FOR ANY DIRECT, INDIRECT, + * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, + * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES, BUSINESS + * INTERRUPTION, OR LOSS OF USE, DATA, OR PROFITS) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, + * EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + *****************************************************************************/ + +using System; + +namespace SpineRuntime37 { + /// Stores the current pose values for an Event. + public class Event { + internal readonly EventData data; + internal readonly float time; + internal int intValue; + internal float floatValue; + internal string stringValue; + internal float volume; + internal float balance; + + public EventData Data { get { return data; } } + /// The animation time this event was keyed. + public float Time { get { return time; } } + + public int Int { get { return intValue; } set { intValue = value; } } + public float Float { get { return floatValue; } set { floatValue = value; } } + public string String { get { return stringValue; } set { stringValue = value; } } + + public float Volume { get { return volume; } set { volume = value; } } + public float Balance { get { return balance; } set { balance = value; } } + + public Event (float time, EventData data) { + if (data == null) throw new ArgumentNullException("data", "data cannot be null."); + this.time = time; + this.data = data; + } + + override public string ToString () { + return this.data.Name; + } + } +} diff --git a/SpineRuntimes/SpineRuntime37/EventData.cs b/SpineRuntimes/SpineRuntime37/EventData.cs new file mode 100644 index 0000000..4eb9454 --- /dev/null +++ b/SpineRuntimes/SpineRuntime37/EventData.cs @@ -0,0 +1,56 @@ +/****************************************************************************** + * Spine Runtimes License Agreement + * Last updated May 1, 2019. Replaces all prior versions. + * + * Copyright (c) 2013-2019, Esoteric Software LLC + * + * Integration of the Spine Runtimes into software or otherwise creating + * derivative works of the Spine Runtimes is permitted under the terms and + * conditions of Section 2 of the Spine Editor License Agreement: + * http://esotericsoftware.com/spine-editor-license + * + * Otherwise, it is permitted to integrate the Spine Runtimes into software + * or otherwise create derivative works of the Spine Runtimes (collectively, + * "Products"), provided that each user of the Products must obtain their own + * Spine Editor license and redistribution of the Products in any form must + * include this license and copyright notice. + * + * THIS SOFTWARE IS PROVIDED BY ESOTERIC SOFTWARE LLC "AS IS" AND ANY EXPRESS + * OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES + * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN + * NO EVENT SHALL ESOTERIC SOFTWARE LLC BE LIABLE FOR ANY DIRECT, INDIRECT, + * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, + * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES, BUSINESS + * INTERRUPTION, OR LOSS OF USE, DATA, OR PROFITS) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, + * EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + *****************************************************************************/ + +using System; + +namespace SpineRuntime37 { + /// Stores the setup pose values for an Event. + public class EventData { + internal string name; + + /// The name of the event, which is unique within the skeleton. + public string Name { get { return name; } } + public int Int { get; set; } + public float Float { get; set; } + public string @String { get; set; } + + public string AudioPath { get; set; } + public float Volume { get; set; } + public float Balance { get; set; } + + public EventData (string name) { + if (name == null) throw new ArgumentNullException("name", "name cannot be null."); + this.name = name; + } + + override public string ToString () { + return Name; + } + } +} diff --git a/SpineRuntimes/SpineRuntime37/ExposedList.cs b/SpineRuntimes/SpineRuntime37/ExposedList.cs new file mode 100644 index 0000000..cd3a542 --- /dev/null +++ b/SpineRuntimes/SpineRuntime37/ExposedList.cs @@ -0,0 +1,624 @@ +// +// System.Collections.Generic.List +// +// Authors: +// Ben Maurer (bmaurer@ximian.com) +// Martin Baulig (martin@ximian.com) +// Carlos Alberto Cortez (calberto.cortez@gmail.com) +// David Waite (mass@akuma.org) +// +// Copyright (C) 2004-2005 Novell, Inc (http://www.novell.com) +// Copyright (C) 2005 David Waite +// +// Permission is hereby granted, free of charge, to any person obtaining +// a copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to +// permit persons to whom the Software is furnished to do so, subject to +// the following conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +// + +using System; +using System.Collections; +using System.Collections.Generic; +using System.Diagnostics; + +namespace SpineRuntime37 { + [DebuggerDisplay("Count={Count}")] + public class ExposedList : IEnumerable { + public T[] Items; + public int Count; + private const int DefaultCapacity = 4; + private static readonly T[] EmptyArray = new T[0]; + private int version; + + public ExposedList () { + Items = EmptyArray; + } + + public ExposedList (IEnumerable collection) { + CheckCollection(collection); + + // initialize to needed size (if determinable) + ICollection c = collection as ICollection; + if (c == null) { + Items = EmptyArray; + AddEnumerable(collection); + } else { + Items = new T[c.Count]; + AddCollection(c); + } + } + + public ExposedList (int capacity) { + if (capacity < 0) + throw new ArgumentOutOfRangeException("capacity"); + Items = new T[capacity]; + } + + internal ExposedList (T[] data, int size) { + Items = data; + Count = size; + } + + public void Add (T item) { + // If we check to see if we need to grow before trying to grow + // we can speed things up by 25% + if (Count == Items.Length) + GrowIfNeeded(1); + Items[Count++] = item; + version++; + } + + public void GrowIfNeeded (int addedCount) { + int minimumSize = Count + addedCount; + if (minimumSize > Items.Length) + Capacity = Math.Max(Math.Max(Capacity * 2, DefaultCapacity), minimumSize); + } + + public ExposedList Resize (int newSize) { + int itemsLength = Items.Length; + var oldItems = Items; + if (newSize > itemsLength) { + Array.Resize(ref Items, newSize); +// var newItems = new T[newSize]; +// Array.Copy(oldItems, newItems, Count); +// Items = newItems; + } else if (newSize < itemsLength) { + // Allow nulling of T reference type to allow GC. + for (int i = newSize; i < itemsLength; i++) + oldItems[i] = default(T); + } + Count = newSize; + return this; + } + + public void EnsureCapacity (int min) { + if (Items.Length < min) { + int newCapacity = Items.Length == 0 ? DefaultCapacity : Items.Length * 2; + //if ((uint)newCapacity > Array.MaxArrayLength) newCapacity = Array.MaxArrayLength; + if (newCapacity < min) newCapacity = min; + Capacity = newCapacity; + } + } + + private void CheckRange (int index, int count) { + if (index < 0) + throw new ArgumentOutOfRangeException("index"); + + if (count < 0) + throw new ArgumentOutOfRangeException("count"); + + if ((uint)index + (uint)count > (uint)Count) + throw new ArgumentException("index and count exceed length of list"); + } + + private void AddCollection (ICollection collection) { + int collectionCount = collection.Count; + if (collectionCount == 0) + return; + + GrowIfNeeded(collectionCount); + collection.CopyTo(Items, Count); + Count += collectionCount; + } + + private void AddEnumerable (IEnumerable enumerable) { + foreach (T t in enumerable) { + Add(t); + } + } + + public void AddRange (IEnumerable collection) { + CheckCollection(collection); + + ICollection c = collection as ICollection; + if (c != null) + AddCollection(c); + else + AddEnumerable(collection); + version++; + } + + public int BinarySearch (T item) { + return Array.BinarySearch(Items, 0, Count, item); + } + + public int BinarySearch (T item, IComparer comparer) { + return Array.BinarySearch(Items, 0, Count, item, comparer); + } + + public int BinarySearch (int index, int count, T item, IComparer comparer) { + CheckRange(index, count); + return Array.BinarySearch(Items, index, count, item, comparer); + } + + public void Clear (bool clearArray = true) { + if (clearArray) + Array.Clear(Items, 0, Items.Length); + + Count = 0; + version++; + } + + public bool Contains (T item) { + return Array.IndexOf(Items, item, 0, Count) != -1; + } + + public ExposedList ConvertAll (Converter converter) { + if (converter == null) + throw new ArgumentNullException("converter"); + ExposedList u = new ExposedList(Count); + for (int i = 0; i < Count; i++) + u.Items[i] = converter(Items[i]); + + u.Count = Count; + return u; + } + + public void CopyTo (T[] array) { + Array.Copy(Items, 0, array, 0, Count); + } + + public void CopyTo (T[] array, int arrayIndex) { + Array.Copy(Items, 0, array, arrayIndex, Count); + } + + public void CopyTo (int index, T[] array, int arrayIndex, int count) { + CheckRange(index, count); + Array.Copy(Items, index, array, arrayIndex, count); + } + + + + public bool Exists (Predicate match) { + CheckMatch(match); + return GetIndex(0, Count, match) != -1; + } + + public T Find (Predicate match) { + CheckMatch(match); + int i = GetIndex(0, Count, match); + return (i != -1) ? Items[i] : default(T); + } + + private static void CheckMatch (Predicate match) { + if (match == null) + throw new ArgumentNullException("match"); + } + + public ExposedList FindAll (Predicate match) { + CheckMatch(match); + return FindAllList(match); + } + + private ExposedList FindAllList (Predicate match) { + ExposedList results = new ExposedList(); + for (int i = 0; i < Count; i++) + if (match(Items[i])) + results.Add(Items[i]); + + return results; + } + + public int FindIndex (Predicate match) { + CheckMatch(match); + return GetIndex(0, Count, match); + } + + public int FindIndex (int startIndex, Predicate match) { + CheckMatch(match); + CheckIndex(startIndex); + return GetIndex(startIndex, Count - startIndex, match); + } + + public int FindIndex (int startIndex, int count, Predicate match) { + CheckMatch(match); + CheckRange(startIndex, count); + return GetIndex(startIndex, count, match); + } + + private int GetIndex (int startIndex, int count, Predicate match) { + int end = startIndex + count; + for (int i = startIndex; i < end; i++) + if (match(Items[i])) + return i; + + return -1; + } + + public T FindLast (Predicate match) { + CheckMatch(match); + int i = GetLastIndex(0, Count, match); + return i == -1 ? default(T) : Items[i]; + } + + public int FindLastIndex (Predicate match) { + CheckMatch(match); + return GetLastIndex(0, Count, match); + } + + public int FindLastIndex (int startIndex, Predicate match) { + CheckMatch(match); + CheckIndex(startIndex); + return GetLastIndex(0, startIndex + 1, match); + } + + public int FindLastIndex (int startIndex, int count, Predicate match) { + CheckMatch(match); + int start = startIndex - count + 1; + CheckRange(start, count); + return GetLastIndex(start, count, match); + } + + private int GetLastIndex (int startIndex, int count, Predicate match) { + // unlike FindLastIndex, takes regular params for search range + for (int i = startIndex + count; i != startIndex; ) + if (match(Items[--i])) + return i; + return -1; + } + + public void ForEach (Action action) { + if (action == null) + throw new ArgumentNullException("action"); + for (int i = 0; i < Count; i++) + action(Items[i]); + } + + public Enumerator GetEnumerator () { + return new Enumerator(this); + } + + public ExposedList GetRange (int index, int count) { + CheckRange(index, count); + T[] tmpArray = new T[count]; + Array.Copy(Items, index, tmpArray, 0, count); + return new ExposedList(tmpArray, count); + } + + public int IndexOf (T item) { + return Array.IndexOf(Items, item, 0, Count); + } + + public int IndexOf (T item, int index) { + CheckIndex(index); + return Array.IndexOf(Items, item, index, Count - index); + } + + public int IndexOf (T item, int index, int count) { + if (index < 0) + throw new ArgumentOutOfRangeException("index"); + + if (count < 0) + throw new ArgumentOutOfRangeException("count"); + + if ((uint)index + (uint)count > (uint)Count) + throw new ArgumentOutOfRangeException("index and count exceed length of list"); + + return Array.IndexOf(Items, item, index, count); + } + + private void Shift (int start, int delta) { + if (delta < 0) + start -= delta; + + if (start < Count) + Array.Copy(Items, start, Items, start + delta, Count - start); + + Count += delta; + + if (delta < 0) + Array.Clear(Items, Count, -delta); + } + + private void CheckIndex (int index) { + if (index < 0 || (uint)index > (uint)Count) + throw new ArgumentOutOfRangeException("index"); + } + + public void Insert (int index, T item) { + CheckIndex(index); + if (Count == Items.Length) + GrowIfNeeded(1); + Shift(index, 1); + Items[index] = item; + version++; + } + + private void CheckCollection (IEnumerable collection) { + if (collection == null) + throw new ArgumentNullException("collection"); + } + + public void InsertRange (int index, IEnumerable collection) { + CheckCollection(collection); + CheckIndex(index); + if (collection == this) { + T[] buffer = new T[Count]; + CopyTo(buffer, 0); + GrowIfNeeded(Count); + Shift(index, buffer.Length); + Array.Copy(buffer, 0, Items, index, buffer.Length); + } else { + ICollection c = collection as ICollection; + if (c != null) + InsertCollection(index, c); + else + InsertEnumeration(index, collection); + } + version++; + } + + private void InsertCollection (int index, ICollection collection) { + int collectionCount = collection.Count; + GrowIfNeeded(collectionCount); + + Shift(index, collectionCount); + collection.CopyTo(Items, index); + } + + private void InsertEnumeration (int index, IEnumerable enumerable) { + foreach (T t in enumerable) + Insert(index++, t); + } + + public int LastIndexOf (T item) { + return Array.LastIndexOf(Items, item, Count - 1, Count); + } + + public int LastIndexOf (T item, int index) { + CheckIndex(index); + return Array.LastIndexOf(Items, item, index, index + 1); + } + + public int LastIndexOf (T item, int index, int count) { + if (index < 0) + throw new ArgumentOutOfRangeException("index", index, "index is negative"); + + if (count < 0) + throw new ArgumentOutOfRangeException("count", count, "count is negative"); + + if (index - count + 1 < 0) + throw new ArgumentOutOfRangeException("count", count, "count is too large"); + + return Array.LastIndexOf(Items, item, index, count); + } + + public bool Remove (T item) { + int loc = IndexOf(item); + if (loc != -1) + RemoveAt(loc); + + return loc != -1; + } + + public int RemoveAll (Predicate match) { + CheckMatch(match); + int i = 0; + int j = 0; + + // Find the first item to remove + for (i = 0; i < Count; i++) + if (match(Items[i])) + break; + + if (i == Count) + return 0; + + version++; + + // Remove any additional items + for (j = i + 1; j < Count; j++) { + if (!match(Items[j])) + Items[i++] = Items[j]; + } + if (j - i > 0) + Array.Clear(Items, i, j - i); + + Count = i; + return (j - i); + } + + public void RemoveAt (int index) { + if (index < 0 || (uint)index >= (uint)Count) + throw new ArgumentOutOfRangeException("index"); + Shift(index, -1); + Array.Clear(Items, Count, 1); + version++; + } + + // Spine Added Method + // Based on Stack.Pop(); https://referencesource.microsoft.com/#mscorlib/system/collections/stack.cs + /// Pops the last item of the list. If the list is empty, Pop throws an InvalidOperationException. + public T Pop () { + if (Count == 0) + throw new InvalidOperationException("List is empty. Nothing to pop."); + + int i = Count - 1; + T item = Items[i]; + Items[i] = default(T); + Count--; + version++; + return item; + } + + public void RemoveRange (int index, int count) { + CheckRange(index, count); + if (count > 0) { + Shift(index, -count); + Array.Clear(Items, Count, count); + version++; + } + } + + public void Reverse () { + Array.Reverse(Items, 0, Count); + version++; + } + + public void Reverse (int index, int count) { + CheckRange(index, count); + Array.Reverse(Items, index, count); + version++; + } + + public void Sort () { + Array.Sort(Items, 0, Count, Comparer.Default); + version++; + } + + public void Sort (IComparer comparer) { + Array.Sort(Items, 0, Count, comparer); + version++; + } + + public void Sort (Comparison comparison) { + Array.Sort(Items, comparison); + version++; + } + + public void Sort (int index, int count, IComparer comparer) { + CheckRange(index, count); + Array.Sort(Items, index, count, comparer); + version++; + } + + public T[] ToArray () { + T[] t = new T[Count]; + Array.Copy(Items, t, Count); + + return t; + } + + public void TrimExcess () { + Capacity = Count; + } + + public bool TrueForAll (Predicate match) { + CheckMatch(match); + + for (int i = 0; i < Count; i++) + if (!match(Items[i])) + return false; + + return true; + } + + public int Capacity { + get { + return Items.Length; + } + set { + if ((uint)value < (uint)Count) + throw new ArgumentOutOfRangeException(); + + Array.Resize(ref Items, value); + } + } + + #region Interface implementations. + + IEnumerator IEnumerable.GetEnumerator () { + return GetEnumerator(); + } + + IEnumerator IEnumerable.GetEnumerator () { + return GetEnumerator(); + } + + #endregion + + public struct Enumerator : IEnumerator, IDisposable { + private ExposedList l; + private int next; + private int ver; + private T current; + + internal Enumerator (ExposedList l) + : this() { + this.l = l; + ver = l.version; + } + + public void Dispose () { + l = null; + } + + private void VerifyState () { + if (l == null) + throw new ObjectDisposedException(GetType().FullName); + if (ver != l.version) + throw new InvalidOperationException( + "Collection was modified; enumeration operation may not execute."); + } + + public bool MoveNext () { + VerifyState(); + + if (next < 0) + return false; + + if (next < l.Count) { + current = l.Items[next++]; + return true; + } + + next = -1; + return false; + } + + public T Current { + get { + return current; + } + } + + void IEnumerator.Reset () { + VerifyState(); + next = 0; + } + + object IEnumerator.Current { + get { + VerifyState(); + if (next <= 0) + throw new InvalidOperationException(); + return current; + } + } + } + } +} diff --git a/SpineRuntimes/SpineRuntime37/IConstraint.cs b/SpineRuntimes/SpineRuntime37/IConstraint.cs new file mode 100644 index 0000000..e65319c --- /dev/null +++ b/SpineRuntimes/SpineRuntime37/IConstraint.cs @@ -0,0 +1,39 @@ +/****************************************************************************** + * Spine Runtimes License Agreement + * Last updated May 1, 2019. Replaces all prior versions. + * + * Copyright (c) 2013-2019, Esoteric Software LLC + * + * Integration of the Spine Runtimes into software or otherwise creating + * derivative works of the Spine Runtimes is permitted under the terms and + * conditions of Section 2 of the Spine Editor License Agreement: + * http://esotericsoftware.com/spine-editor-license + * + * Otherwise, it is permitted to integrate the Spine Runtimes into software + * or otherwise create derivative works of the Spine Runtimes (collectively, + * "Products"), provided that each user of the Products must obtain their own + * Spine Editor license and redistribution of the Products in any form must + * include this license and copyright notice. + * + * THIS SOFTWARE IS PROVIDED BY ESOTERIC SOFTWARE LLC "AS IS" AND ANY EXPRESS + * OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES + * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN + * NO EVENT SHALL ESOTERIC SOFTWARE LLC BE LIABLE FOR ANY DIRECT, INDIRECT, + * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, + * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES, BUSINESS + * INTERRUPTION, OR LOSS OF USE, DATA, OR PROFITS) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, + * EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + *****************************************************************************/ + +namespace SpineRuntime37 { + + /// The interface for all constraints. + public interface IConstraint : IUpdatable { + /// The ordinal for the order a skeleton's constraints will be applied. + int Order { get; } + + } + +} diff --git a/SpineRuntimes/SpineRuntime37/IUpdatable.cs b/SpineRuntimes/SpineRuntime37/IUpdatable.cs new file mode 100644 index 0000000..4b3d8bb --- /dev/null +++ b/SpineRuntimes/SpineRuntime37/IUpdatable.cs @@ -0,0 +1,34 @@ +/****************************************************************************** + * Spine Runtimes License Agreement + * Last updated May 1, 2019. Replaces all prior versions. + * + * Copyright (c) 2013-2019, Esoteric Software LLC + * + * Integration of the Spine Runtimes into software or otherwise creating + * derivative works of the Spine Runtimes is permitted under the terms and + * conditions of Section 2 of the Spine Editor License Agreement: + * http://esotericsoftware.com/spine-editor-license + * + * Otherwise, it is permitted to integrate the Spine Runtimes into software + * or otherwise create derivative works of the Spine Runtimes (collectively, + * "Products"), provided that each user of the Products must obtain their own + * Spine Editor license and redistribution of the Products in any form must + * include this license and copyright notice. + * + * THIS SOFTWARE IS PROVIDED BY ESOTERIC SOFTWARE LLC "AS IS" AND ANY EXPRESS + * OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES + * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN + * NO EVENT SHALL ESOTERIC SOFTWARE LLC BE LIABLE FOR ANY DIRECT, INDIRECT, + * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, + * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES, BUSINESS + * INTERRUPTION, OR LOSS OF USE, DATA, OR PROFITS) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, + * EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + *****************************************************************************/ + +namespace SpineRuntime37 { + public interface IUpdatable { + void Update (); + } +} diff --git a/SpineRuntimes/SpineRuntime37/IkConstraint.cs b/SpineRuntimes/SpineRuntime37/IkConstraint.cs new file mode 100644 index 0000000..f2fabe9 --- /dev/null +++ b/SpineRuntimes/SpineRuntime37/IkConstraint.cs @@ -0,0 +1,302 @@ +/****************************************************************************** + * Spine Runtimes License Agreement + * Last updated May 1, 2019. Replaces all prior versions. + * + * Copyright (c) 2013-2019, Esoteric Software LLC + * + * Integration of the Spine Runtimes into software or otherwise creating + * derivative works of the Spine Runtimes is permitted under the terms and + * conditions of Section 2 of the Spine Editor License Agreement: + * http://esotericsoftware.com/spine-editor-license + * + * Otherwise, it is permitted to integrate the Spine Runtimes into software + * or otherwise create derivative works of the Spine Runtimes (collectively, + * "Products"), provided that each user of the Products must obtain their own + * Spine Editor license and redistribution of the Products in any form must + * include this license and copyright notice. + * + * THIS SOFTWARE IS PROVIDED BY ESOTERIC SOFTWARE LLC "AS IS" AND ANY EXPRESS + * OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES + * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN + * NO EVENT SHALL ESOTERIC SOFTWARE LLC BE LIABLE FOR ANY DIRECT, INDIRECT, + * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, + * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES, BUSINESS + * INTERRUPTION, OR LOSS OF USE, DATA, OR PROFITS) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, + * EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + *****************************************************************************/ + +using System; + +namespace SpineRuntime37 { + /// + /// + /// Stores the current pose for an IK constraint. An IK constraint adjusts the rotation of 1 or 2 constrained bones so the tip of + /// the last bone is as close to the target bone as possible. + /// + /// See IK constraints in the Spine User Guide. + /// + public class IkConstraint : IConstraint { + internal IkConstraintData data; + internal ExposedList bones = new ExposedList(); + internal Bone target; + internal int bendDirection; + internal bool compress, stretch; + internal float mix = 1; + + public IkConstraint (IkConstraintData data, Skeleton skeleton) { + if (data == null) throw new ArgumentNullException("data", "data cannot be null."); + if (skeleton == null) throw new ArgumentNullException("skeleton", "skeleton cannot be null."); + this.data = data; + mix = data.mix; + bendDirection = data.bendDirection; + compress = data.compress; + stretch = data.stretch; + + bones = new ExposedList(data.bones.Count); + foreach (BoneData boneData in data.bones) + bones.Add(skeleton.FindBone(boneData.name)); + target = skeleton.FindBone(data.target.name); + } + + /// Copy constructor. + public IkConstraint (IkConstraint constraint, Skeleton skeleton) { + if (constraint == null) throw new ArgumentNullException("constraint cannot be null."); + if (skeleton == null) throw new ArgumentNullException("skeleton cannot be null."); + data = constraint.data; + bones = new ExposedList(constraint.Bones.Count); + foreach (Bone bone in constraint.Bones) + bones.Add(skeleton.Bones.Items[bone.data.index]); + target = skeleton.Bones.Items[constraint.target.data.index]; + mix = constraint.mix; + bendDirection = constraint.bendDirection; + compress = constraint.compress; + stretch = constraint.stretch; + } + + /// Applies the constraint to the constrained bones. + public void Apply () { + Update(); + } + + public void Update () { + Bone target = this.target; + ExposedList bones = this.bones; + switch (bones.Count) { + case 1: + Apply(bones.Items[0], target.worldX, target.worldY, compress, stretch, data.uniform, mix); + break; + case 2: + Apply(bones.Items[0], bones.Items[1], target.worldX, target.worldY, bendDirection, stretch, mix); + break; + } + } + + + public int Order { + get { return data.order; } + } + + /// The bones that will be modified by this IK constraint. + public ExposedList Bones { + get { return bones; } + } + + /// The bone that is the IK target. + public Bone Target { + get { return target; } + set { target = value; } + } + + /// A percentage (0-1) that controls the mix between the constrained and unconstrained rotations. + public float Mix { + get { return mix; } + set { mix = value; } + } + + /// Controls the bend direction of the IK bones, either 1 or -1. + public int BendDirection { + get { return bendDirection; } + set { bendDirection = value; } + } + + /// + /// When true and only a single bone is being constrained, if the target is too close, the bone is scaled to reach it. + public bool Compress { + get { return compress; } + set { compress = value; } + } + + /// + /// When true, if the target is out of range, the parent bone is scaled to reach it. If more than one bone is being constrained + /// and the parent bone has local nonuniform scale, stretch is not applied. + public bool Stretch { + get { return stretch; } + set { stretch = value; } + } + + /// The IK constraint's setup pose data. + public IkConstraintData Data { + get { return data; } + } + + override public string ToString () { + return data.name; + } + + /// Applies 1 bone IK. The target is specified in the world coordinate system. + static public void Apply (Bone bone, float targetX, float targetY, bool compress, bool stretch, bool uniform, + float alpha) { + if (!bone.appliedValid) bone.UpdateAppliedTransform(); + Bone p = bone.parent; + float id = 1 / (p.a * p.d - p.b * p.c); + float x = targetX - p.worldX, y = targetY - p.worldY; + float tx = (x * p.d - y * p.b) * id - bone.ax, ty = (y * p.a - x * p.c) * id - bone.ay; + float rotationIK = (float)Math.Atan2(ty, tx) * MathUtils.RadDeg - bone.ashearX - bone.arotation; + if (bone.ascaleX < 0) rotationIK += 180; + if (rotationIK > 180) + rotationIK -= 360; + else if (rotationIK < -180) // + rotationIK += 360; + float sx = bone.ascaleX, sy = bone.ascaleY; + if (compress || stretch) { + float b = bone.data.length * sx, dd = (float)Math.Sqrt(tx * tx + ty * ty); + if ((compress && dd < b) || (stretch && dd > b) && b > 0.0001f) { + float s = (dd / b - 1) * alpha + 1; + sx *= s; + if (uniform) sy *= s; + } + } + bone.UpdateWorldTransform(bone.ax, bone.ay, bone.arotation + rotationIK * alpha, sx, sy, bone.ashearX, bone.ashearY); + } + + /// Applies 2 bone IK. The target is specified in the world coordinate system. + /// A direct descendant of the parent bone. + static public void Apply (Bone parent, Bone child, float targetX, float targetY, int bendDir, bool stretch, float alpha) { + if (alpha == 0) { + child.UpdateWorldTransform(); + return; + } + if (!parent.appliedValid) parent.UpdateAppliedTransform(); + if (!child.appliedValid) child.UpdateAppliedTransform(); + float px = parent.ax, py = parent.ay, psx = parent.ascaleX, sx = psx, psy = parent.ascaleY, csx = child.ascaleX; + int os1, os2, s2; + if (psx < 0) { + psx = -psx; + os1 = 180; + s2 = -1; + } else { + os1 = 0; + s2 = 1; + } + if (psy < 0) { + psy = -psy; + s2 = -s2; + } + if (csx < 0) { + csx = -csx; + os2 = 180; + } else + os2 = 0; + float cx = child.ax, cy, cwx, cwy, a = parent.a, b = parent.b, c = parent.c, d = parent.d; + bool u = Math.Abs(psx - psy) <= 0.0001f; + if (!u) { + cy = 0; + cwx = a * cx + parent.worldX; + cwy = c * cx + parent.worldY; + } else { + cy = child.ay; + cwx = a * cx + b * cy + parent.worldX; + cwy = c * cx + d * cy + parent.worldY; + } + Bone pp = parent.parent; + a = pp.a; + b = pp.b; + c = pp.c; + d = pp.d; + float id = 1 / (a * d - b * c), x = targetX - pp.worldX, y = targetY - pp.worldY; + float tx = (x * d - y * b) * id - px, ty = (y * a - x * c) * id - py, dd = tx * tx + ty * ty; + x = cwx - pp.worldX; + y = cwy - pp.worldY; + float dx = (x * d - y * b) * id - px, dy = (y * a - x * c) * id - py; + float l1 = (float)Math.Sqrt(dx * dx + dy * dy), l2 = child.data.length * csx, a1, a2; + if (u) { + l2 *= psx; + float cos = (dd - l1 * l1 - l2 * l2) / (2 * l1 * l2); + if (cos < -1) + cos = -1; + else if (cos > 1) { + cos = 1; + if (stretch && l1 + l2 > 0.0001f) sx *= ((float)Math.Sqrt(dd) / (l1 + l2) - 1) * alpha + 1; + } + a2 = (float)Math.Acos(cos) * bendDir; + a = l1 + l2 * cos; + b = l2 * (float)Math.Sin(a2); + a1 = (float)Math.Atan2(ty * a - tx * b, tx * a + ty * b); + } else { + a = psx * l2; + b = psy * l2; + float aa = a * a, bb = b * b, ta = (float)Math.Atan2(ty, tx); + c = bb * l1 * l1 + aa * dd - aa * bb; + float c1 = -2 * bb * l1, c2 = bb - aa; + d = c1 * c1 - 4 * c2 * c; + if (d >= 0) { + float q = (float)Math.Sqrt(d); + if (c1 < 0) q = -q; + q = -(c1 + q) / 2; + float r0 = q / c2, r1 = c / q; + float r = Math.Abs(r0) < Math.Abs(r1) ? r0 : r1; + if (r * r <= dd) { + y = (float)Math.Sqrt(dd - r * r) * bendDir; + a1 = ta - (float)Math.Atan2(y, r); + a2 = (float)Math.Atan2(y / psy, (r - l1) / psx); + goto break_outer; // break outer; + } + } + float minAngle = MathUtils.PI, minX = l1 - a, minDist = minX * minX, minY = 0; + float maxAngle = 0, maxX = l1 + a, maxDist = maxX * maxX, maxY = 0; + c = -a * l1 / (aa - bb); + if (c >= -1 && c <= 1) { + c = (float)Math.Acos(c); + x = a * (float)Math.Cos(c) + l1; + y = b * (float)Math.Sin(c); + d = x * x + y * y; + if (d < minDist) { + minAngle = c; + minDist = d; + minX = x; + minY = y; + } + if (d > maxDist) { + maxAngle = c; + maxDist = d; + maxX = x; + maxY = y; + } + } + if (dd <= (minDist + maxDist) / 2) { + a1 = ta - (float)Math.Atan2(minY * bendDir, minX); + a2 = minAngle * bendDir; + } else { + a1 = ta - (float)Math.Atan2(maxY * bendDir, maxX); + a2 = maxAngle * bendDir; + } + } + break_outer: + float os = (float)Math.Atan2(cy, cx) * s2; + float rotation = parent.arotation; + a1 = (a1 - os) * MathUtils.RadDeg + os1 - rotation; + if (a1 > 180) + a1 -= 360; + else if (a1 < -180) a1 += 360; + parent.UpdateWorldTransform(px, py, rotation + a1 * alpha, sx, parent.ascaleY, 0, 0); + rotation = child.arotation; + a2 = ((a2 + os) * MathUtils.RadDeg - child.ashearX) * s2 + os2 - rotation; + if (a2 > 180) + a2 -= 360; + else if (a2 < -180) a2 += 360; + child.UpdateWorldTransform(cx, cy, rotation + a2 * alpha, child.ascaleX, child.ascaleY, child.ashearX, child.ashearY); + } + } +} diff --git a/SpineRuntimes/SpineRuntime37/IkConstraintData.cs b/SpineRuntimes/SpineRuntime37/IkConstraintData.cs new file mode 100644 index 0000000..b1a7a0c --- /dev/null +++ b/SpineRuntimes/SpineRuntime37/IkConstraintData.cs @@ -0,0 +1,111 @@ +/****************************************************************************** + * Spine Runtimes License Agreement + * Last updated May 1, 2019. Replaces all prior versions. + * + * Copyright (c) 2013-2019, Esoteric Software LLC + * + * Integration of the Spine Runtimes into software or otherwise creating + * derivative works of the Spine Runtimes is permitted under the terms and + * conditions of Section 2 of the Spine Editor License Agreement: + * http://esotericsoftware.com/spine-editor-license + * + * Otherwise, it is permitted to integrate the Spine Runtimes into software + * or otherwise create derivative works of the Spine Runtimes (collectively, + * "Products"), provided that each user of the Products must obtain their own + * Spine Editor license and redistribution of the Products in any form must + * include this license and copyright notice. + * + * THIS SOFTWARE IS PROVIDED BY ESOTERIC SOFTWARE LLC "AS IS" AND ANY EXPRESS + * OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES + * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN + * NO EVENT SHALL ESOTERIC SOFTWARE LLC BE LIABLE FOR ANY DIRECT, INDIRECT, + * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, + * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES, BUSINESS + * INTERRUPTION, OR LOSS OF USE, DATA, OR PROFITS) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, + * EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + *****************************************************************************/ + +using System; +using System.Collections.Generic; + +namespace SpineRuntime37 { + /// Stores the setup pose for an IkConstraint. + public class IkConstraintData { + internal string name; + internal int order; + internal List bones = new List(); + internal BoneData target; + internal int bendDirection = 1; + internal bool compress, stretch, uniform; + internal float mix = 1; + + /// The IK constraint's name, which is unique within the skeleton. + public string Name { + get { return name; } + } + + public int Order { + get { return order; } + set { order = value; } + } + + /// The bones that are constrained by this IK Constraint. + public List Bones { + get { return bones; } + } + + /// The bone that is the IK target. + public BoneData Target { + get { return target; } + set { target = value; } + } + + /// + /// A percentage (0-1) that controls the mix between the constraint and unconstrained rotations. + public float Mix { + get { return mix; } + set { mix = value; } + } + + /// Controls the bend direction of the IK bones, either 1 or -1. + public int BendDirection { + get { return bendDirection; } + set { bendDirection = value; } + } + + /// + /// When true, and only a single bone is being constrained, + /// if the target is too close, the bone is scaled to reach it. + public bool Compress { + get { return compress; } + set { compress = value; } + } + + /// + /// When true, if the target is out of range, the parent bone is scaled on the X axis to reach it. + /// If the bone has local nonuniform scale, stretching is not applied. + public bool Stretch { + get { return stretch; } + set { stretch = value; } + } + + /// + /// When true, only a single bone is being constrained and Compress or Stretch is used, + /// the bone is scaled both on the X and Y axes. + public bool Uniform { + get { return uniform; } + set { uniform = value; } + } + + public IkConstraintData (string name) { + if (name == null) throw new ArgumentNullException("name", "name cannot be null."); + this.name = name; + } + + override public string ToString () { + return name; + } + } +} diff --git a/SpineRuntimes/SpineRuntime37/Json.cs b/SpineRuntimes/SpineRuntime37/Json.cs new file mode 100644 index 0000000..ab8f8b6 --- /dev/null +++ b/SpineRuntimes/SpineRuntime37/Json.cs @@ -0,0 +1,534 @@ +/****************************************************************************** + * Spine Runtimes License Agreement + * Last updated May 1, 2019. Replaces all prior versions. + * + * Copyright (c) 2013-2019, Esoteric Software LLC + * + * Integration of the Spine Runtimes into software or otherwise creating + * derivative works of the Spine Runtimes is permitted under the terms and + * conditions of Section 2 of the Spine Editor License Agreement: + * http://esotericsoftware.com/spine-editor-license + * + * Otherwise, it is permitted to integrate the Spine Runtimes into software + * or otherwise create derivative works of the Spine Runtimes (collectively, + * "Products"), provided that each user of the Products must obtain their own + * Spine Editor license and redistribution of the Products in any form must + * include this license and copyright notice. + * + * THIS SOFTWARE IS PROVIDED BY ESOTERIC SOFTWARE LLC "AS IS" AND ANY EXPRESS + * OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES + * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN + * NO EVENT SHALL ESOTERIC SOFTWARE LLC BE LIABLE FOR ANY DIRECT, INDIRECT, + * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, + * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES, BUSINESS + * INTERRUPTION, OR LOSS OF USE, DATA, OR PROFITS) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, + * EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + *****************************************************************************/ + +using System; +using System.IO; +using System.Text; +using System.Collections; +using System.Globalization; +using System.Collections.Generic; + +namespace SpineRuntime37 { + public static class Json { + public static object Deserialize (TextReader text) { + var parser = new SharpJson.JsonDecoder(); + parser.parseNumbersAsFloat = true; + return parser.Decode(text.ReadToEnd()); + } + } +} + +/** + * + * Copyright (c) 2016 Adriano Tinoco d'Oliveira Rezende + * + * Based on the JSON parser by Patrick van Bergen + * http://techblog.procurios.nl/k/news/view/14605/14863/how-do-i-write-my-own-parser-(for-json).html + * + * Changes made: + * + * - Optimized parser speed (deserialize roughly near 3x faster than original) + * - Added support to handle lexer/parser error messages with line numbers + * - Added more fine grained control over type conversions during the parsing + * - Refactory API (Separate Lexer code from Parser code and the Encoder from Decoder) + * + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of this software + * and associated documentation files (the "Software"), to deal in the Software without restriction, + * including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * The above copyright notice and this permission notice shall be included in all copies or substantial + * portions of the Software. + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT + * LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. + * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, + * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE + * OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + * + */ +namespace SharpJson +{ + class Lexer + { + public enum Token { + None, + Null, + True, + False, + Colon, + Comma, + String, + Number, + CurlyOpen, + CurlyClose, + SquaredOpen, + SquaredClose, + }; + + public bool hasError { + get { + return !success; + } + } + + public int lineNumber { + get; + private set; + } + + public bool parseNumbersAsFloat { + get; + set; + } + + char[] json; + int index = 0; + bool success = true; + char[] stringBuffer = new char[4096]; + + public Lexer(string text) + { + Reset(); + + json = text.ToCharArray(); + parseNumbersAsFloat = false; + } + + public void Reset() + { + index = 0; + lineNumber = 1; + success = true; + } + + public string ParseString() + { + int idx = 0; + StringBuilder builder = null; + + SkipWhiteSpaces(); + + // " + char c = json[index++]; + + bool failed = false; + bool complete = false; + + while (!complete && !failed) { + if (index == json.Length) + break; + + c = json[index++]; + if (c == '"') { + complete = true; + break; + } else if (c == '\\') { + if (index == json.Length) + break; + + c = json[index++]; + + switch (c) { + case '"': + stringBuffer[idx++] = '"'; + break; + case '\\': + stringBuffer[idx++] = '\\'; + break; + case '/': + stringBuffer[idx++] = '/'; + break; + case 'b': + stringBuffer[idx++] = '\b'; + break; + case'f': + stringBuffer[idx++] = '\f'; + break; + case 'n': + stringBuffer[idx++] = '\n'; + break; + case 'r': + stringBuffer[idx++] = '\r'; + break; + case 't': + stringBuffer[idx++] = '\t'; + break; + case 'u': + int remainingLength = json.Length - index; + if (remainingLength >= 4) { + var hex = new string(json, index, 4); + + // XXX: handle UTF + stringBuffer[idx++] = (char) Convert.ToInt32(hex, 16); + + // skip 4 chars + index += 4; + } else { + failed = true; + } + break; + } + } else { + stringBuffer[idx++] = c; + } + + if (idx >= stringBuffer.Length) { + if (builder == null) + builder = new StringBuilder(); + + builder.Append(stringBuffer, 0, idx); + idx = 0; + } + } + + if (!complete) { + success = false; + return null; + } + + if (builder != null) + return builder.ToString (); + else + return new string (stringBuffer, 0, idx); + } + + string GetNumberString() + { + SkipWhiteSpaces(); + + int lastIndex = GetLastIndexOfNumber(index); + int charLength = (lastIndex - index) + 1; + + var result = new string (json, index, charLength); + + index = lastIndex + 1; + + return result; + } + + public float ParseFloatNumber() + { + float number; + var str = GetNumberString (); + + if (!float.TryParse (str, NumberStyles.Float, CultureInfo.InvariantCulture, out number)) + return 0; + + return number; + } + + public double ParseDoubleNumber() + { + double number; + var str = GetNumberString (); + + if (!double.TryParse(str, NumberStyles.Any, CultureInfo.InvariantCulture, out number)) + return 0; + + return number; + } + + int GetLastIndexOfNumber(int index) + { + int lastIndex; + + for (lastIndex = index; lastIndex < json.Length; lastIndex++) { + char ch = json[lastIndex]; + + if ((ch < '0' || ch > '9') && ch != '+' && ch != '-' + && ch != '.' && ch != 'e' && ch != 'E') + break; + } + + return lastIndex - 1; + } + + void SkipWhiteSpaces() + { + for (; index < json.Length; index++) { + char ch = json[index]; + + if (ch == '\n') + lineNumber++; + + if (!char.IsWhiteSpace(json[index])) + break; + } + } + + public Token LookAhead() + { + SkipWhiteSpaces(); + + int savedIndex = index; + return NextToken(json, ref savedIndex); + } + + public Token NextToken() + { + SkipWhiteSpaces(); + return NextToken(json, ref index); + } + + static Token NextToken(char[] json, ref int index) + { + if (index == json.Length) + return Token.None; + + char c = json[index++]; + + switch (c) { + case '{': + return Token.CurlyOpen; + case '}': + return Token.CurlyClose; + case '[': + return Token.SquaredOpen; + case ']': + return Token.SquaredClose; + case ',': + return Token.Comma; + case '"': + return Token.String; + case '0': case '1': case '2': case '3': case '4': + case '5': case '6': case '7': case '8': case '9': + case '-': + return Token.Number; + case ':': + return Token.Colon; + } + + index--; + + int remainingLength = json.Length - index; + + // false + if (remainingLength >= 5) { + if (json[index] == 'f' && + json[index + 1] == 'a' && + json[index + 2] == 'l' && + json[index + 3] == 's' && + json[index + 4] == 'e') { + index += 5; + return Token.False; + } + } + + // true + if (remainingLength >= 4) { + if (json[index] == 't' && + json[index + 1] == 'r' && + json[index + 2] == 'u' && + json[index + 3] == 'e') { + index += 4; + return Token.True; + } + } + + // null + if (remainingLength >= 4) { + if (json[index] == 'n' && + json[index + 1] == 'u' && + json[index + 2] == 'l' && + json[index + 3] == 'l') { + index += 4; + return Token.Null; + } + } + + return Token.None; + } + } + + public class JsonDecoder + { + public string errorMessage { + get; + private set; + } + + public bool parseNumbersAsFloat { + get; + set; + } + + Lexer lexer; + + public JsonDecoder() + { + errorMessage = null; + parseNumbersAsFloat = false; + } + + public object Decode(string text) + { + errorMessage = null; + + lexer = new Lexer(text); + lexer.parseNumbersAsFloat = parseNumbersAsFloat; + + return ParseValue(); + } + + public static object DecodeText(string text) + { + var builder = new JsonDecoder(); + return builder.Decode(text); + } + + IDictionary ParseObject() + { + var table = new Dictionary(); + + // { + lexer.NextToken(); + + while (true) { + var token = lexer.LookAhead(); + + switch (token) { + case Lexer.Token.None: + TriggerError("Invalid token"); + return null; + case Lexer.Token.Comma: + lexer.NextToken(); + break; + case Lexer.Token.CurlyClose: + lexer.NextToken(); + return table; + default: + // name + string name = EvalLexer(lexer.ParseString()); + + if (errorMessage != null) + return null; + + // : + token = lexer.NextToken(); + + if (token != Lexer.Token.Colon) { + TriggerError("Invalid token; expected ':'"); + return null; + } + + // value + object value = ParseValue(); + + if (errorMessage != null) + return null; + + table[name] = value; + break; + } + } + + //return null; // Unreachable code + } + + IList ParseArray() + { + var array = new List(); + + // [ + lexer.NextToken(); + + while (true) { + var token = lexer.LookAhead(); + + switch (token) { + case Lexer.Token.None: + TriggerError("Invalid token"); + return null; + case Lexer.Token.Comma: + lexer.NextToken(); + break; + case Lexer.Token.SquaredClose: + lexer.NextToken(); + return array; + default: + object value = ParseValue(); + + if (errorMessage != null) + return null; + + array.Add(value); + break; + } + } + + //return null; // Unreachable code + } + + object ParseValue() + { + switch (lexer.LookAhead()) { + case Lexer.Token.String: + return EvalLexer(lexer.ParseString()); + case Lexer.Token.Number: + if (parseNumbersAsFloat) + return EvalLexer(lexer.ParseFloatNumber()); + else + return EvalLexer(lexer.ParseDoubleNumber()); + case Lexer.Token.CurlyOpen: + return ParseObject(); + case Lexer.Token.SquaredOpen: + return ParseArray(); + case Lexer.Token.True: + lexer.NextToken(); + return true; + case Lexer.Token.False: + lexer.NextToken(); + return false; + case Lexer.Token.Null: + lexer.NextToken(); + return null; + case Lexer.Token.None: + break; + } + + TriggerError("Unable to parse value"); + return null; + } + + void TriggerError(string message) + { + errorMessage = string.Format("Error: '{0}' at line {1}", + message, lexer.lineNumber); + } + + T EvalLexer(T value) + { + if (lexer.hasError) + TriggerError("Lexical error ocurred"); + + return value; + } + } +} diff --git a/SpineRuntimes/SpineRuntime37/MathUtils.cs b/SpineRuntimes/SpineRuntime37/MathUtils.cs new file mode 100644 index 0000000..566296e --- /dev/null +++ b/SpineRuntimes/SpineRuntime37/MathUtils.cs @@ -0,0 +1,145 @@ +/****************************************************************************** + * Spine Runtimes License Agreement + * Last updated May 1, 2019. Replaces all prior versions. + * + * Copyright (c) 2013-2019, Esoteric Software LLC + * + * Integration of the Spine Runtimes into software or otherwise creating + * derivative works of the Spine Runtimes is permitted under the terms and + * conditions of Section 2 of the Spine Editor License Agreement: + * http://esotericsoftware.com/spine-editor-license + * + * Otherwise, it is permitted to integrate the Spine Runtimes into software + * or otherwise create derivative works of the Spine Runtimes (collectively, + * "Products"), provided that each user of the Products must obtain their own + * Spine Editor license and redistribution of the Products in any form must + * include this license and copyright notice. + * + * THIS SOFTWARE IS PROVIDED BY ESOTERIC SOFTWARE LLC "AS IS" AND ANY EXPRESS + * OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES + * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN + * NO EVENT SHALL ESOTERIC SOFTWARE LLC BE LIABLE FOR ANY DIRECT, INDIRECT, + * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, + * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES, BUSINESS + * INTERRUPTION, OR LOSS OF USE, DATA, OR PROFITS) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, + * EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + *****************************************************************************/ + +using System; + +namespace SpineRuntime37 { + public static class MathUtils { + public const float PI = 3.1415927f; + public const float PI2 = PI * 2; + public const float RadDeg = 180f / PI; + public const float DegRad = PI / 180; + + const int SIN_BITS = 14; // 16KB. Adjust for accuracy. + const int SIN_MASK = ~(-1 << SIN_BITS); + const int SIN_COUNT = SIN_MASK + 1; + const float RadFull = PI * 2; + const float DegFull = 360; + const float RadToIndex = SIN_COUNT / RadFull; + const float DegToIndex = SIN_COUNT / DegFull; + static float[] sin = new float[SIN_COUNT]; + + static Random random = new Random(); + + static MathUtils () { + for (int i = 0; i < SIN_COUNT; i++) + sin[i] = (float)Math.Sin((i + 0.5f) / SIN_COUNT * RadFull); + for (int i = 0; i < 360; i += 90) + sin[(int)(i * DegToIndex) & SIN_MASK] = (float)Math.Sin(i * DegRad); + } + + /// Returns the sine in radians from a lookup table. + static public float Sin (float radians) { + return sin[(int)(radians * RadToIndex) & SIN_MASK]; + } + + /// Returns the cosine in radians from a lookup table. + static public float Cos (float radians) { + return sin[(int)((radians + PI / 2) * RadToIndex) & SIN_MASK]; + } + + /// Returns the sine in radians from a lookup table. + static public float SinDeg (float degrees) { + return sin[(int)(degrees * DegToIndex) & SIN_MASK]; + } + + /// Returns the cosine in radians from a lookup table. + static public float CosDeg (float degrees) { + return sin[(int)((degrees + 90) * DegToIndex) & SIN_MASK]; + } + + /// Returns atan2 in radians, faster but less accurate than Math.Atan2. Average error of 0.00231 radians (0.1323 + /// degrees), largest error of 0.00488 radians (0.2796 degrees). + static public float Atan2 (float y, float x) { + if (x == 0f) { + if (y > 0f) return PI / 2; + if (y == 0f) return 0f; + return -PI / 2; + } + float atan, z = y / x; + if (Math.Abs(z) < 1f) { + atan = z / (1f + 0.28f * z * z); + if (x < 0f) return atan + (y < 0f ? -PI : PI); + return atan; + } + atan = PI / 2 - z / (z * z + 0.28f); + return y < 0f ? atan - PI : atan; + } + + static public float Clamp (float value, float min, float max) { + if (value < min) return min; + if (value > max) return max; + return value; + } + + static public float RandomTriangle(float min, float max) { + return RandomTriangle(min, max, (min + max) * 0.5f); + } + + static public float RandomTriangle(float min, float max, float mode) { + float u = (float)random.NextDouble(); + float d = max - min; + if (u <= (mode - min) / d) return min + (float)Math.Sqrt(u * d * (mode - min)); + return max - (float)Math.Sqrt((1 - u) * d * (max - mode)); + } + } + + public abstract class IInterpolation { + public static IInterpolation Pow2 = new Pow(2); + public static IInterpolation Pow2Out = new PowOut(2); + + protected abstract float Apply(float a); + + public float Apply(float start, float end, float a) { + return start + (end - start) * Apply(a); + } + } + + public class Pow: IInterpolation { + public float Power { get; set; } + + public Pow(float power) { + Power = power; + } + + protected override float Apply(float a) { + if (a <= 0.5f) return (float)Math.Pow(a * 2, Power) / 2; + return (float)Math.Pow((a - 1) * 2, Power) / (Power % 2 == 0 ? -2 : 2) + 1; + } + } + + public class PowOut : Pow { + public PowOut(float power) : base(power) { + } + + protected override float Apply(float a) { + return (float)Math.Pow(a - 1, Power) * (Power % 2 == 0 ? -1 : 1) + 1; + } + } +} diff --git a/SpineRuntimes/SpineRuntime37/PathConstraint.cs b/SpineRuntimes/SpineRuntime37/PathConstraint.cs new file mode 100644 index 0000000..0ad366c --- /dev/null +++ b/SpineRuntimes/SpineRuntime37/PathConstraint.cs @@ -0,0 +1,469 @@ +/****************************************************************************** + * Spine Runtimes License Agreement + * Last updated May 1, 2019. Replaces all prior versions. + * + * Copyright (c) 2013-2019, Esoteric Software LLC + * + * Integration of the Spine Runtimes into software or otherwise creating + * derivative works of the Spine Runtimes is permitted under the terms and + * conditions of Section 2 of the Spine Editor License Agreement: + * http://esotericsoftware.com/spine-editor-license + * + * Otherwise, it is permitted to integrate the Spine Runtimes into software + * or otherwise create derivative works of the Spine Runtimes (collectively, + * "Products"), provided that each user of the Products must obtain their own + * Spine Editor license and redistribution of the Products in any form must + * include this license and copyright notice. + * + * THIS SOFTWARE IS PROVIDED BY ESOTERIC SOFTWARE LLC "AS IS" AND ANY EXPRESS + * OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES + * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN + * NO EVENT SHALL ESOTERIC SOFTWARE LLC BE LIABLE FOR ANY DIRECT, INDIRECT, + * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, + * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES, BUSINESS + * INTERRUPTION, OR LOSS OF USE, DATA, OR PROFITS) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, + * EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + *****************************************************************************/ + +using System; + +namespace SpineRuntime37 { + + /// + /// + /// Stores the current pose for a path constraint. A path constraint adjusts the rotation, translation, and scale of the + /// constrained bones so they follow a {@link PathAttachment}. + /// + /// See Path constraints in the Spine User Guide. + /// + public class PathConstraint : IConstraint { + const int NONE = -1, BEFORE = -2, AFTER = -3; + const float Epsilon = 0.00001f; + + internal PathConstraintData data; + internal ExposedList bones; + internal Slot target; + internal float position, spacing, rotateMix, translateMix; + + internal ExposedList spaces = new ExposedList(), positions = new ExposedList(); + internal ExposedList world = new ExposedList(), curves = new ExposedList(), lengths = new ExposedList(); + internal float[] segments = new float[10]; + + public PathConstraint (PathConstraintData data, Skeleton skeleton) { + if (data == null) throw new ArgumentNullException("data", "data cannot be null."); + if (skeleton == null) throw new ArgumentNullException("skeleton", "skeleton cannot be null."); + this.data = data; + bones = new ExposedList(data.Bones.Count); + foreach (BoneData boneData in data.bones) + bones.Add(skeleton.FindBone(boneData.name)); + target = skeleton.FindSlot(data.target.name); + position = data.position; + spacing = data.spacing; + rotateMix = data.rotateMix; + translateMix = data.translateMix; + } + + /// Copy constructor. + public PathConstraint (PathConstraint constraint, Skeleton skeleton) { + if (constraint == null) throw new ArgumentNullException("constraint cannot be null."); + if (skeleton == null) throw new ArgumentNullException("skeleton cannot be null."); + data = constraint.data; + bones = new ExposedList(constraint.Bones.Count); + foreach (Bone bone in constraint.Bones) + bones.Add(skeleton.Bones.Items[bone.data.index]); + target = skeleton.slots.Items[constraint.target.data.index]; + position = constraint.position; + spacing = constraint.spacing; + rotateMix = constraint.rotateMix; + translateMix = constraint.translateMix; + } + + /// Applies the constraint to the constrained bones. + public void Apply () { + Update(); + } + + public void Update () { + PathAttachment attachment = target.Attachment as PathAttachment; + if (attachment == null) return; + + float rotateMix = this.rotateMix, translateMix = this.translateMix; + bool translate = translateMix > 0, rotate = rotateMix > 0; + if (!translate && !rotate) return; + + PathConstraintData data = this.data; + bool percentSpacing = data.spacingMode == SpacingMode.Percent; + RotateMode rotateMode = data.rotateMode; + bool tangents = rotateMode == RotateMode.Tangent, scale = rotateMode == RotateMode.ChainScale; + int boneCount = this.bones.Count, spacesCount = tangents ? boneCount : boneCount + 1; + Bone[] bonesItems = this.bones.Items; + ExposedList spaces = this.spaces.Resize(spacesCount), lengths = null; + float spacing = this.spacing; + if (scale || !percentSpacing) { + if (scale) lengths = this.lengths.Resize(boneCount); + bool lengthSpacing = data.spacingMode == SpacingMode.Length; + for (int i = 0, n = spacesCount - 1; i < n;) { + Bone bone = bonesItems[i]; + float setupLength = bone.data.length; + if (setupLength < PathConstraint.Epsilon) { + if (scale) lengths.Items[i] = 0; + spaces.Items[++i] = 0; + } else if (percentSpacing) { + if (scale) { + float x = setupLength * bone.a, y = setupLength * bone.c; + float length = (float)Math.Sqrt(x * x + y * y); + lengths.Items[i] = length; + } + spaces.Items[++i] = spacing; + } else { + float x = setupLength * bone.a, y = setupLength * bone.c; + float length = (float)Math.Sqrt(x * x + y * y); + if (scale) lengths.Items[i] = length; + spaces.Items[++i] = (lengthSpacing ? setupLength + spacing : spacing) * length / setupLength; + } + } + } else { + for (int i = 1; i < spacesCount; i++) + spaces.Items[i] = spacing; + } + + float[] positions = ComputeWorldPositions(attachment, spacesCount, tangents, + data.positionMode == PositionMode.Percent, percentSpacing); + float boneX = positions[0], boneY = positions[1], offsetRotation = data.offsetRotation; + bool tip; + if (offsetRotation == 0) { + tip = rotateMode == RotateMode.Chain; + } else { + tip = false; + Bone p = target.bone; + offsetRotation *= p.a * p.d - p.b * p.c > 0 ? MathUtils.DegRad : -MathUtils.DegRad; + } + for (int i = 0, p = 3; i < boneCount; i++, p += 3) { + Bone bone = bonesItems[i]; + bone.worldX += (boneX - bone.worldX) * translateMix; + bone.worldY += (boneY - bone.worldY) * translateMix; + float x = positions[p], y = positions[p + 1], dx = x - boneX, dy = y - boneY; + if (scale) { + float length = lengths.Items[i]; + if (length >= PathConstraint.Epsilon) { + float s = ((float)Math.Sqrt(dx * dx + dy * dy) / length - 1) * rotateMix + 1; + bone.a *= s; + bone.c *= s; + } + } + boneX = x; + boneY = y; + if (rotate) { + float a = bone.a, b = bone.b, c = bone.c, d = bone.d, r, cos, sin; + if (tangents) + r = positions[p - 1]; + else if (spaces.Items[i + 1] < PathConstraint.Epsilon) + r = positions[p + 2]; + else + r = MathUtils.Atan2(dy, dx); + r -= MathUtils.Atan2(c, a); + if (tip) { + cos = MathUtils.Cos(r); + sin = MathUtils.Sin(r); + float length = bone.data.length; + boneX += (length * (cos * a - sin * c) - dx) * rotateMix; + boneY += (length * (sin * a + cos * c) - dy) * rotateMix; + } else + r += offsetRotation; + if (r > MathUtils.PI) + r -= MathUtils.PI2; + else if (r < -MathUtils.PI) // + r += MathUtils.PI2; + r *= rotateMix; + cos = MathUtils.Cos(r); + sin = MathUtils.Sin(r); + bone.a = cos * a - sin * c; + bone.b = cos * b - sin * d; + bone.c = sin * a + cos * c; + bone.d = sin * b + cos * d; + } + bone.appliedValid = false; + } + } + + float[] ComputeWorldPositions (PathAttachment path, int spacesCount, bool tangents, bool percentPosition, + bool percentSpacing) { + + Slot target = this.target; + float position = this.position; + float[] spacesItems = this.spaces.Items, output = this.positions.Resize(spacesCount * 3 + 2).Items, world; + bool closed = path.Closed; + int verticesLength = path.WorldVerticesLength, curveCount = verticesLength / 6, prevCurve = NONE; + float pathLength = 0; + + if (!path.ConstantSpeed) { + float[] lengths = path.Lengths; + curveCount -= closed ? 1 : 2; + pathLength = lengths[curveCount]; + if (percentPosition) position *= pathLength; + if (percentSpacing) { + for (int i = 1; i < spacesCount; i++) + spacesItems[i] *= pathLength; + } + world = this.world.Resize(8).Items; + for (int i = 0, o = 0, curve = 0; i < spacesCount; i++, o += 3) { + float space = spacesItems[i]; + position += space; + float p = position; + + if (closed) { + p %= pathLength; + if (p < 0) p += pathLength; + curve = 0; + } else if (p < 0) { + if (prevCurve != BEFORE) { + prevCurve = BEFORE; + path.ComputeWorldVertices(target, 2, 4, world, 0, 2); + } + AddBeforePosition(p, world, 0, output, o); + continue; + } else if (p > pathLength) { + if (prevCurve != AFTER) { + prevCurve = AFTER; + path.ComputeWorldVertices(target, verticesLength - 6, 4, world, 0, 2); + } + AddAfterPosition(p - pathLength, world, 0, output, o); + continue; + } + + // Determine curve containing position. + for (;; curve++) { + float length = lengths[curve]; + if (p > length) continue; + if (curve == 0) + p /= length; + else { + float prev = lengths[curve - 1]; + p = (p - prev) / (length - prev); + } + break; + } + if (curve != prevCurve) { + prevCurve = curve; + if (closed && curve == curveCount) { + path.ComputeWorldVertices(target, verticesLength - 4, 4, world, 0, 2); + path.ComputeWorldVertices(target, 0, 4, world, 4, 2); + } else + path.ComputeWorldVertices(target, curve * 6 + 2, 8, world, 0, 2); + } + AddCurvePosition(p, world[0], world[1], world[2], world[3], world[4], world[5], world[6], world[7], output, o, + tangents || (i > 0 && space < PathConstraint.Epsilon)); + } + return output; + } + + // World vertices. + if (closed) { + verticesLength += 2; + world = this.world.Resize(verticesLength).Items; + path.ComputeWorldVertices(target, 2, verticesLength - 4, world, 0, 2); + path.ComputeWorldVertices(target, 0, 2, world, verticesLength - 4, 2); + world[verticesLength - 2] = world[0]; + world[verticesLength - 1] = world[1]; + } else { + curveCount--; + verticesLength -= 4; + world = this.world.Resize(verticesLength).Items; + path.ComputeWorldVertices(target, 2, verticesLength, world, 0, 2); + } + + // Curve lengths. + float[] curves = this.curves.Resize(curveCount).Items; + pathLength = 0; + float x1 = world[0], y1 = world[1], cx1 = 0, cy1 = 0, cx2 = 0, cy2 = 0, x2 = 0, y2 = 0; + float tmpx, tmpy, dddfx, dddfy, ddfx, ddfy, dfx, dfy; + for (int i = 0, w = 2; i < curveCount; i++, w += 6) { + cx1 = world[w]; + cy1 = world[w + 1]; + cx2 = world[w + 2]; + cy2 = world[w + 3]; + x2 = world[w + 4]; + y2 = world[w + 5]; + tmpx = (x1 - cx1 * 2 + cx2) * 0.1875f; + tmpy = (y1 - cy1 * 2 + cy2) * 0.1875f; + dddfx = ((cx1 - cx2) * 3 - x1 + x2) * 0.09375f; + dddfy = ((cy1 - cy2) * 3 - y1 + y2) * 0.09375f; + ddfx = tmpx * 2 + dddfx; + ddfy = tmpy * 2 + dddfy; + dfx = (cx1 - x1) * 0.75f + tmpx + dddfx * 0.16666667f; + dfy = (cy1 - y1) * 0.75f + tmpy + dddfy * 0.16666667f; + pathLength += (float)Math.Sqrt(dfx * dfx + dfy * dfy); + dfx += ddfx; + dfy += ddfy; + ddfx += dddfx; + ddfy += dddfy; + pathLength += (float)Math.Sqrt(dfx * dfx + dfy * dfy); + dfx += ddfx; + dfy += ddfy; + pathLength += (float)Math.Sqrt(dfx * dfx + dfy * dfy); + dfx += ddfx + dddfx; + dfy += ddfy + dddfy; + pathLength += (float)Math.Sqrt(dfx * dfx + dfy * dfy); + curves[i] = pathLength; + x1 = x2; + y1 = y2; + } + if (percentPosition) + position *= pathLength; + else + position *= pathLength / path.lengths[curveCount - 1]; + + if (percentSpacing) { + for (int i = 1; i < spacesCount; i++) + spacesItems[i] *= pathLength; + } + + float[] segments = this.segments; + float curveLength = 0; + for (int i = 0, o = 0, curve = 0, segment = 0; i < spacesCount; i++, o += 3) { + float space = spacesItems[i]; + position += space; + float p = position; + + if (closed) { + p %= pathLength; + if (p < 0) p += pathLength; + curve = 0; + } else if (p < 0) { + AddBeforePosition(p, world, 0, output, o); + continue; + } else if (p > pathLength) { + AddAfterPosition(p - pathLength, world, verticesLength - 4, output, o); + continue; + } + + // Determine curve containing position. + for (;; curve++) { + float length = curves[curve]; + if (p > length) continue; + if (curve == 0) + p /= length; + else { + float prev = curves[curve - 1]; + p = (p - prev) / (length - prev); + } + break; + } + + // Curve segment lengths. + if (curve != prevCurve) { + prevCurve = curve; + int ii = curve * 6; + x1 = world[ii]; + y1 = world[ii + 1]; + cx1 = world[ii + 2]; + cy1 = world[ii + 3]; + cx2 = world[ii + 4]; + cy2 = world[ii + 5]; + x2 = world[ii + 6]; + y2 = world[ii + 7]; + tmpx = (x1 - cx1 * 2 + cx2) * 0.03f; + tmpy = (y1 - cy1 * 2 + cy2) * 0.03f; + dddfx = ((cx1 - cx2) * 3 - x1 + x2) * 0.006f; + dddfy = ((cy1 - cy2) * 3 - y1 + y2) * 0.006f; + ddfx = tmpx * 2 + dddfx; + ddfy = tmpy * 2 + dddfy; + dfx = (cx1 - x1) * 0.3f + tmpx + dddfx * 0.16666667f; + dfy = (cy1 - y1) * 0.3f + tmpy + dddfy * 0.16666667f; + curveLength = (float)Math.Sqrt(dfx * dfx + dfy * dfy); + segments[0] = curveLength; + for (ii = 1; ii < 8; ii++) { + dfx += ddfx; + dfy += ddfy; + ddfx += dddfx; + ddfy += dddfy; + curveLength += (float)Math.Sqrt(dfx * dfx + dfy * dfy); + segments[ii] = curveLength; + } + dfx += ddfx; + dfy += ddfy; + curveLength += (float)Math.Sqrt(dfx * dfx + dfy * dfy); + segments[8] = curveLength; + dfx += ddfx + dddfx; + dfy += ddfy + dddfy; + curveLength += (float)Math.Sqrt(dfx * dfx + dfy * dfy); + segments[9] = curveLength; + segment = 0; + } + + // Weight by segment length. + p *= curveLength; + for (;; segment++) { + float length = segments[segment]; + if (p > length) continue; + if (segment == 0) + p /= length; + else { + float prev = segments[segment - 1]; + p = segment + (p - prev) / (length - prev); + } + break; + } + AddCurvePosition(p * 0.1f, x1, y1, cx1, cy1, cx2, cy2, x2, y2, output, o, tangents || (i > 0 && space < PathConstraint.Epsilon)); + } + return output; + } + + static void AddBeforePosition (float p, float[] temp, int i, float[] output, int o) { + float x1 = temp[i], y1 = temp[i + 1], dx = temp[i + 2] - x1, dy = temp[i + 3] - y1, r = MathUtils.Atan2(dy, dx); + output[o] = x1 + p * MathUtils.Cos(r); + output[o + 1] = y1 + p * MathUtils.Sin(r); + output[o + 2] = r; + } + + static void AddAfterPosition (float p, float[] temp, int i, float[] output, int o) { + float x1 = temp[i + 2], y1 = temp[i + 3], dx = x1 - temp[i], dy = y1 - temp[i + 1], r = MathUtils.Atan2(dy, dx); + output[o] = x1 + p * MathUtils.Cos(r); + output[o + 1] = y1 + p * MathUtils.Sin(r); + output[o + 2] = r; + } + + static void AddCurvePosition (float p, float x1, float y1, float cx1, float cy1, float cx2, float cy2, float x2, float y2, + float[] output, int o, bool tangents) { + if (p < PathConstraint.Epsilon || float.IsNaN(p)) { + output[o] = x1; + output[o + 1] = y1; + output[o + 2] = (float)Math.Atan2(cy1 - y1, cx1 - x1); + return; + } + float tt = p * p, ttt = tt * p, u = 1 - p, uu = u * u, uuu = uu * u; + float ut = u * p, ut3 = ut * 3, uut3 = u * ut3, utt3 = ut3 * p; + float x = x1 * uuu + cx1 * uut3 + cx2 * utt3 + x2 * ttt, y = y1 * uuu + cy1 * uut3 + cy2 * utt3 + y2 * ttt; + output[o] = x; + output[o + 1] = y; + if (tangents) { + if (p < 0.001f) + output[o + 2] = (float)Math.Atan2(cy1 - y1, cx1 - x1); + else + output[o + 2] = (float)Math.Atan2(y - (y1 * uu + cy1 * ut * 2 + cy2 * tt), x - (x1 * uu + cx1 * ut * 2 + cx2 * tt)); + } + } + + public int Order { get { return data.order; } } + /// The position along the path. + public float Position { get { return position; } set { position = value; } } + /// The spacing between bones. + public float Spacing { get { return spacing; } set { spacing = value; } } + /// A percentage (0-1) that controls the mix between the constrained and unconstrained rotations. + public float RotateMix { get { return rotateMix; } set { rotateMix = value; } } + /// A percentage (0-1) that controls the mix between the constrained and unconstrained translations. + public float TranslateMix { get { return translateMix; } set { translateMix = value; } } + /// The bones that will be modified by this path constraint. + public ExposedList Bones { get { return bones; } } + /// The slot whose path attachment will be used to constrained the bones. + public Slot Target { get { return target; } set { target = value; } } + /// The path constraint's setup pose data. + public PathConstraintData Data { get { return data; } } + + override public string ToString () { + return data.name; + } + } +} diff --git a/SpineRuntimes/SpineRuntime37/PathConstraintData.cs b/SpineRuntimes/SpineRuntime37/PathConstraintData.cs new file mode 100644 index 0000000..b32aca4 --- /dev/null +++ b/SpineRuntimes/SpineRuntime37/PathConstraintData.cs @@ -0,0 +1,78 @@ +/****************************************************************************** + * Spine Runtimes License Agreement + * Last updated May 1, 2019. Replaces all prior versions. + * + * Copyright (c) 2013-2019, Esoteric Software LLC + * + * Integration of the Spine Runtimes into software or otherwise creating + * derivative works of the Spine Runtimes is permitted under the terms and + * conditions of Section 2 of the Spine Editor License Agreement: + * http://esotericsoftware.com/spine-editor-license + * + * Otherwise, it is permitted to integrate the Spine Runtimes into software + * or otherwise create derivative works of the Spine Runtimes (collectively, + * "Products"), provided that each user of the Products must obtain their own + * Spine Editor license and redistribution of the Products in any form must + * include this license and copyright notice. + * + * THIS SOFTWARE IS PROVIDED BY ESOTERIC SOFTWARE LLC "AS IS" AND ANY EXPRESS + * OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES + * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN + * NO EVENT SHALL ESOTERIC SOFTWARE LLC BE LIABLE FOR ANY DIRECT, INDIRECT, + * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, + * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES, BUSINESS + * INTERRUPTION, OR LOSS OF USE, DATA, OR PROFITS) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, + * EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + *****************************************************************************/ + +using System; + +namespace SpineRuntime37 { + public class PathConstraintData { + internal string name; + internal int order; + internal ExposedList bones = new ExposedList(); + internal SlotData target; + internal PositionMode positionMode; + internal SpacingMode spacingMode; + internal RotateMode rotateMode; + internal float offsetRotation; + internal float position, spacing, rotateMix, translateMix; + + public string Name { get { return name; } } + public int Order { get { return order; } set { order = value; } } + public ExposedList Bones { get { return bones; } } + public SlotData Target { get { return target; } set { target = value; } } + public PositionMode PositionMode { get { return positionMode; } set { positionMode = value; } } + public SpacingMode SpacingMode { get { return spacingMode; } set { spacingMode = value; } } + public RotateMode RotateMode { get { return rotateMode; } set { rotateMode = value; } } + public float OffsetRotation { get { return offsetRotation; } set { offsetRotation = value; } } + public float Position { get { return position; } set { position = value; } } + public float Spacing { get { return spacing; } set { spacing = value; } } + public float RotateMix { get { return rotateMix; } set { rotateMix = value; } } + public float TranslateMix { get { return translateMix; } set { translateMix = value; } } + + public PathConstraintData (String name) { + if (name == null) throw new ArgumentNullException("name", "name cannot be null."); + this.name = name; + } + + public override string ToString () { + return name; + } + } + + public enum PositionMode { + Fixed, Percent + } + + public enum SpacingMode { + Length, Fixed, Percent + } + + public enum RotateMode { + Tangent, Chain, ChainScale + } +} diff --git a/SpineRuntimes/SpineRuntime37/Skeleton.cs b/SpineRuntimes/SpineRuntime37/Skeleton.cs new file mode 100644 index 0000000..db7a403 --- /dev/null +++ b/SpineRuntimes/SpineRuntime37/Skeleton.cs @@ -0,0 +1,599 @@ +/****************************************************************************** + * Spine Runtimes License Agreement + * Last updated May 1, 2019. Replaces all prior versions. + * + * Copyright (c) 2013-2019, Esoteric Software LLC + * + * Integration of the Spine Runtimes into software or otherwise creating + * derivative works of the Spine Runtimes is permitted under the terms and + * conditions of Section 2 of the Spine Editor License Agreement: + * http://esotericsoftware.com/spine-editor-license + * + * Otherwise, it is permitted to integrate the Spine Runtimes into software + * or otherwise create derivative works of the Spine Runtimes (collectively, + * "Products"), provided that each user of the Products must obtain their own + * Spine Editor license and redistribution of the Products in any form must + * include this license and copyright notice. + * + * THIS SOFTWARE IS PROVIDED BY ESOTERIC SOFTWARE LLC "AS IS" AND ANY EXPRESS + * OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES + * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN + * NO EVENT SHALL ESOTERIC SOFTWARE LLC BE LIABLE FOR ANY DIRECT, INDIRECT, + * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, + * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES, BUSINESS + * INTERRUPTION, OR LOSS OF USE, DATA, OR PROFITS) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, + * EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + *****************************************************************************/ + +using System; +using System.Collections.Generic; + +namespace SpineRuntime37 { + public class Skeleton { + internal SkeletonData data; + internal ExposedList bones; + internal ExposedList slots; + internal ExposedList drawOrder; + internal ExposedList ikConstraints; + internal ExposedList transformConstraints; + internal ExposedList pathConstraints; + internal ExposedList updateCache = new ExposedList(); + internal ExposedList updateCacheReset = new ExposedList(); + internal Skin skin; + internal float r = 1, g = 1, b = 1, a = 1; + internal float time; + internal float scaleX = 1, scaleY = 1; + internal float x, y; + + public SkeletonData Data { get { return data; } } + public ExposedList Bones { get { return bones; } } + public ExposedList UpdateCacheList { get { return updateCache; } } + public ExposedList Slots { get { return slots; } } + public ExposedList DrawOrder { get { return drawOrder; } } + public ExposedList IkConstraints { get { return ikConstraints; } } + public ExposedList PathConstraints { get { return pathConstraints; } } + public ExposedList TransformConstraints { get { return transformConstraints; } } + public Skin Skin { get { return skin; } set { skin = value; } } + public float R { get { return r; } set { r = value; } } + public float G { get { return g; } set { g = value; } } + public float B { get { return b; } set { b = value; } } + public float A { get { return a; } set { a = value; } } + 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 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]; } + } + + public Skeleton (SkeletonData data) { + if (data == null) throw new ArgumentNullException("data", "data cannot be null."); + this.data = data; + + bones = new ExposedList(data.bones.Count); + foreach (BoneData boneData in data.bones) { + Bone bone; + if (boneData.parent == null) { + bone = new Bone(boneData, this, null); + } else { + Bone parent = bones.Items[boneData.parent.index]; + bone = new Bone(boneData, this, parent); + parent.children.Add(bone); + } + bones.Add(bone); + } + + slots = new ExposedList(data.slots.Count); + drawOrder = new ExposedList(data.slots.Count); + foreach (SlotData slotData in data.slots) { + Bone bone = bones.Items[slotData.boneData.index]; + Slot slot = new Slot(slotData, bone); + slots.Add(slot); + drawOrder.Add(slot); + } + + ikConstraints = new ExposedList(data.ikConstraints.Count); + foreach (IkConstraintData ikConstraintData in data.ikConstraints) + ikConstraints.Add(new IkConstraint(ikConstraintData, this)); + + transformConstraints = new ExposedList(data.transformConstraints.Count); + foreach (TransformConstraintData transformConstraintData in data.transformConstraints) + transformConstraints.Add(new TransformConstraint(transformConstraintData, this)); + + pathConstraints = new ExposedList (data.pathConstraints.Count); + foreach (PathConstraintData pathConstraintData in data.pathConstraints) + pathConstraints.Add(new PathConstraint(pathConstraintData, this)); + + UpdateCache(); + UpdateWorldTransform(); + } + + /// Caches information about bones and constraints. Must be called if bones, constraints or weighted path attachments are added + /// or removed. + public void UpdateCache () { + var updateCache = this.updateCache; + updateCache.Clear(); + this.updateCacheReset.Clear(); + + var bones = this.bones; + for (int i = 0, n = bones.Count; i < n; i++) + bones.Items[i].sorted = false; + + var ikConstraints = this.ikConstraints; + var transformConstraints = this.transformConstraints; + var pathConstraints = this.pathConstraints; + int ikCount = ikConstraints.Count, transformCount = transformConstraints.Count, pathCount = pathConstraints.Count; + int constraintCount = ikCount + transformCount + pathCount; + //outer: + for (int i = 0; i < constraintCount; i++) { + for (int ii = 0; ii < ikCount; ii++) { + IkConstraint constraint = ikConstraints.Items[ii]; + if (constraint.data.order == i) { + SortIkConstraint(constraint); + goto continue_outer; //continue outer; + } + } + for (int ii = 0; ii < transformCount; ii++) { + TransformConstraint constraint = transformConstraints.Items[ii]; + if (constraint.data.order == i) { + SortTransformConstraint(constraint); + goto continue_outer; //continue outer; + } + } + for (int ii = 0; ii < pathCount; ii++) { + PathConstraint constraint = pathConstraints.Items[ii]; + if (constraint.data.order == i) { + SortPathConstraint(constraint); + goto continue_outer; //continue outer; + } + } + continue_outer: {} + } + + for (int i = 0, n = bones.Count; i < n; i++) + SortBone(bones.Items[i]); + } + + private void SortIkConstraint (IkConstraint constraint) { + Bone target = constraint.target; + SortBone(target); + + var constrained = constraint.bones; + Bone parent = constrained.Items[0]; + SortBone(parent); + + if (constrained.Count > 1) { + Bone child = constrained.Items[constrained.Count - 1]; + if (!updateCache.Contains(child)) + updateCacheReset.Add(child); + } + + updateCache.Add(constraint); + + SortReset(parent.children); + constrained.Items[constrained.Count - 1].sorted = true; + } + + private void SortPathConstraint (PathConstraint constraint) { + Slot slot = constraint.target; + int slotIndex = slot.data.index; + Bone slotBone = slot.bone; + if (skin != null) SortPathConstraintAttachment(skin, slotIndex, slotBone); + if (data.defaultSkin != null && data.defaultSkin != skin) + SortPathConstraintAttachment(data.defaultSkin, slotIndex, slotBone); + for (int ii = 0, nn = data.skins.Count; ii < nn; ii++) + SortPathConstraintAttachment(data.skins.Items[ii], slotIndex, slotBone); + + Attachment attachment = slot.attachment; + if (attachment is PathAttachment) SortPathConstraintAttachment(attachment, slotBone); + + var constrained = constraint.bones; + int boneCount = constrained.Count; + for (int i = 0; i < boneCount; i++) + SortBone(constrained.Items[i]); + + updateCache.Add(constraint); + + for (int i = 0; i < boneCount; i++) + SortReset(constrained.Items[i].children); + for (int i = 0; i < boneCount; i++) + constrained.Items[i].sorted = true; + } + + private void SortTransformConstraint (TransformConstraint constraint) { + SortBone(constraint.target); + + var constrained = constraint.bones; + int boneCount = constrained.Count; + if (constraint.data.local) { + for (int i = 0; i < boneCount; i++) { + Bone child = constrained.Items[i]; + SortBone(child.parent); + if (!updateCache.Contains(child)) updateCacheReset.Add(child); + } + } else { + for (int i = 0; i < boneCount; i++) + SortBone(constrained.Items[i]); + } + + updateCache.Add(constraint); + + for (int i = 0; i < boneCount; i++) + SortReset(constrained.Items[i].children); + for (int i = 0; i < boneCount; i++) + constrained.Items[i].sorted = true; + } + + private void SortPathConstraintAttachment (Skin skin, int slotIndex, Bone slotBone) { + foreach (var entry in skin.Attachments) + if (entry.Key.slotIndex == slotIndex) SortPathConstraintAttachment(entry.Value, slotBone); + } + + private void SortPathConstraintAttachment (Attachment attachment, Bone slotBone) { + if (!(attachment is PathAttachment)) return; + int[] pathBones = ((PathAttachment)attachment).bones; + if (pathBones == null) + SortBone(slotBone); + else { + var bones = this.bones; + for (int i = 0, n = pathBones.Length; i < n;) { + int nn = pathBones[i++]; + nn += i; + while (i < nn) + SortBone(bones.Items[pathBones[i++]]); + } + } + } + + private void SortBone (Bone bone) { + if (bone.sorted) return; + Bone parent = bone.parent; + if (parent != null) SortBone(parent); + bone.sorted = true; + updateCache.Add(bone); + } + + private static void SortReset (ExposedList bones) { + var bonesItems = bones.Items; + for (int i = 0, n = bones.Count; i < n; i++) { + Bone bone = bonesItems[i]; + if (bone.sorted) SortReset(bone.children); + bone.sorted = false; + } + } + + /// Updates the world transform for each bone and applies constraints. + public void UpdateWorldTransform () { + var updateCacheReset = this.updateCacheReset; + var updateCacheResetItems = updateCacheReset.Items; + for (int i = 0, n = updateCacheReset.Count; i < n; i++) { + Bone bone = updateCacheResetItems[i]; + bone.ax = bone.x; + bone.ay = bone.y; + bone.arotation = bone.rotation; + bone.ascaleX = bone.scaleX; + bone.ascaleY = bone.scaleY; + bone.ashearX = bone.shearX; + bone.ashearY = bone.shearY; + bone.appliedValid = true; + } + var updateItems = this.updateCache.Items; + for (int i = 0, n = updateCache.Count; i < n; i++) + updateItems[i].Update(); + } + + /// + /// Updates the world transform for each bone and applies all constraints. The root bone will be temporarily parented to the specified bone. + /// + public void UpdateWorldTransform (Bone parent) { + // This partial update avoids computing the world transform for constrained bones when 1) the bone is not updated + // before the constraint, 2) the constraint only needs to access the applied local transform, and 3) the constraint calls + // updateWorldTransform. + var updateCacheReset = this.updateCacheReset; + var updateCacheResetItems = updateCacheReset.Items; + for (int i = 0, n = updateCacheReset.Count; i < n; i++) { + Bone bone = updateCacheResetItems[i]; + bone.ax = bone.x; + bone.ay = bone.y; + bone.arotation = bone.rotation; + bone.ascaleX = bone.scaleX; + bone.ascaleY = bone.scaleY; + bone.ashearX = bone.shearX; + bone.ashearY = bone.shearY; + bone.appliedValid = true; + } + + // Apply the parent bone transform to the root bone. The root bone + // always inherits scale, rotation and reflection. + Bone rootBone = this.RootBone; + float pa = parent.a, pb = parent.b, pc = parent.c, pd = parent.d; + rootBone.worldX = pa * x + pb * y + parent.worldX; + rootBone.worldY = pc * x + pd * y + parent.worldY; + + float rotationY = rootBone.rotation + 90 + rootBone.shearY; + float la = MathUtils.CosDeg(rootBone.rotation + rootBone.shearX) * rootBone.scaleX; + float lb = MathUtils.CosDeg(rotationY) * rootBone.scaleY; + float lc = MathUtils.SinDeg(rootBone.rotation + rootBone.shearX) * rootBone.scaleX; + float ld = MathUtils.SinDeg(rotationY) * rootBone.scaleY; + rootBone.a = (pa * la + pb * lc) * scaleX; + rootBone.b = (pa * lb + pb * ld) * scaleX; + rootBone.c = (pc * la + pd * lc) * scaleY; + rootBone.d = (pc * lb + pd * ld) * scaleY; + + // Update everything except root bone. + var updateCache = this.updateCache; + var updateCacheItems = updateCache.Items; + for (int i = 0, n = updateCache.Count; i < n; i++) { + var updatable = updateCacheItems[i]; + if (updatable != rootBone) + updatable.Update(); + } + } + + /// Sets the bones, constraints, and slots to their setup pose values. + public void SetToSetupPose () { + SetBonesToSetupPose(); + SetSlotsToSetupPose(); + } + + /// Sets the bones and constraints to their setup pose values. + public void SetBonesToSetupPose () { + var bonesItems = this.bones.Items; + for (int i = 0, n = bones.Count; i < n; i++) + bonesItems[i].SetToSetupPose(); + + var ikConstraintsItems = this.ikConstraints.Items; + for (int i = 0, n = ikConstraints.Count; i < n; i++) { + IkConstraint constraint = ikConstraintsItems[i]; + constraint.mix = constraint.data.mix; + constraint.bendDirection = constraint.data.bendDirection; + constraint.compress = constraint.data.compress; + constraint.stretch = constraint.data.stretch; + } + + var transformConstraintsItems = this.transformConstraints.Items; + for (int i = 0, n = transformConstraints.Count; i < n; i++) { + TransformConstraint constraint = transformConstraintsItems[i]; + TransformConstraintData constraintData = constraint.data; + constraint.rotateMix = constraintData.rotateMix; + constraint.translateMix = constraintData.translateMix; + constraint.scaleMix = constraintData.scaleMix; + constraint.shearMix = constraintData.shearMix; + } + + var pathConstraintItems = this.pathConstraints.Items; + for (int i = 0, n = pathConstraints.Count; i < n; i++) { + PathConstraint constraint = pathConstraintItems[i]; + PathConstraintData constraintData = constraint.data; + constraint.position = constraintData.position; + constraint.spacing = constraintData.spacing; + constraint.rotateMix = constraintData.rotateMix; + constraint.translateMix = constraintData.translateMix; + } + } + + public void SetSlotsToSetupPose () { + var slots = this.slots; + var slotsItems = slots.Items; + drawOrder.Clear(); + for (int i = 0, n = slots.Count; i < n; i++) + drawOrder.Add(slotsItems[i]); + + for (int i = 0, n = slots.Count; i < n; i++) + slotsItems[i].SetToSetupPose(); + } + + /// May be null. + public Bone FindBone (string boneName) { + if (boneName == null) throw new ArgumentNullException("boneName", "boneName cannot be null."); + var bones = this.bones; + var bonesItems = bones.Items; + for (int i = 0, n = bones.Count; i < n; i++) { + Bone bone = bonesItems[i]; + if (bone.data.name == boneName) return bone; + } + return null; + } + + /// -1 if the bone was not found. + public int FindBoneIndex (string boneName) { + if (boneName == null) throw new ArgumentNullException("boneName", "boneName cannot be null."); + var bones = this.bones; + var bonesItems = bones.Items; + for (int i = 0, n = bones.Count; i < n; i++) + if (bonesItems[i].data.name == boneName) return i; + return -1; + } + + /// May be null. + public Slot FindSlot (string slotName) { + if (slotName == null) throw new ArgumentNullException("slotName", "slotName cannot be null."); + var slots = this.slots; + var slotsItems = slots.Items; + for (int i = 0, n = slots.Count; i < n; i++) { + Slot slot = slotsItems[i]; + if (slot.data.name == slotName) return slot; + } + return null; + } + + /// -1 if the bone was not found. + public int FindSlotIndex (string slotName) { + if (slotName == null) throw new ArgumentNullException("slotName", "slotName cannot be null."); + var slots = this.slots; + var slotsItems = slots.Items; + for (int i = 0, n = slots.Count; i < n; i++) + if (slotsItems[i].data.name.Equals(slotName)) return i; + return -1; + } + + /// Sets a skin by name (see SetSkin). + public void SetSkin (string skinName) { + Skin foundSkin = data.FindSkin(skinName); + if (foundSkin == null) throw new ArgumentException("Skin not found: " + skinName, "skinName"); + SetSkin(foundSkin); + } + + /// + /// Attachments from the new skin are attached if the corresponding attachment from the old skin was attached. + /// If there was no old skin, each slot's setup mode attachment is attached from the new skin. + /// After changing the skin, the visible attachments can be reset to those attached in the setup pose by calling + /// . + /// Also, often is called before the next time the + /// skeleton is rendered to allow any attachment keys in the current animation(s) to hide or show attachments from the new skin. + /// + /// May be null. + public void SetSkin (Skin newSkin) { + if (newSkin != null) { + if (skin != null) + newSkin.AttachAll(this, skin); + else { + ExposedList slots = this.slots; + for (int i = 0, n = slots.Count; i < n; i++) { + Slot slot = slots.Items[i]; + string name = slot.data.attachmentName; + if (name != null) { + Attachment attachment = newSkin.GetAttachment(i, name); + if (attachment != null) slot.Attachment = attachment; + } + } + } + } + skin = newSkin; + } + + /// Finds an attachment by looking in the {@link #skin} and {@link SkeletonData#defaultSkin} using the slot name and attachment name. + /// May be null. + public Attachment GetAttachment (string slotName, string attachmentName) { + return GetAttachment(data.FindSlotIndex(slotName), attachmentName); + } + + /// Finds an attachment by looking in the skin and skeletonData.defaultSkin using the slot index and attachment name.First the skin is checked and if the attachment was not found, the default skin is checked. + /// May be null. + public Attachment GetAttachment (int slotIndex, string attachmentName) { + if (attachmentName == null) throw new ArgumentNullException("attachmentName", "attachmentName cannot be null."); + if (skin != null) { + Attachment attachment = skin.GetAttachment(slotIndex, attachmentName); + if (attachment != null) return attachment; + } + return data.defaultSkin != null ? data.defaultSkin.GetAttachment(slotIndex, attachmentName) : null; + } + + /// A convenience method to set an attachment by finding the slot with FindSlot, finding the attachment with GetAttachment, then setting the slot's slot.Attachment. + /// May be null. + public void SetAttachment (string slotName, string attachmentName) { + if (slotName == null) throw new ArgumentNullException("slotName", "slotName cannot be null."); + ExposedList slots = this.slots; + for (int i = 0, n = slots.Count; i < n; i++) { + Slot slot = slots.Items[i]; + if (slot.data.name == slotName) { + Attachment attachment = null; + if (attachmentName != null) { + attachment = GetAttachment(i, attachmentName); + if (attachment == null) throw new Exception("Attachment not found: " + attachmentName + ", for slot: " + slotName); + } + slot.Attachment = attachment; + return; + } + } + throw new Exception("Slot not found: " + slotName); + } + + /// May be null. + public IkConstraint FindIkConstraint (string constraintName) { + if (constraintName == null) throw new ArgumentNullException("constraintName", "constraintName cannot be null."); + ExposedList ikConstraints = this.ikConstraints; + for (int i = 0, n = ikConstraints.Count; i < n; i++) { + IkConstraint ikConstraint = ikConstraints.Items[i]; + if (ikConstraint.data.name == constraintName) return ikConstraint; + } + return null; + } + + /// May be null. + public TransformConstraint FindTransformConstraint (string constraintName) { + if (constraintName == null) throw new ArgumentNullException("constraintName", "constraintName cannot be null."); + ExposedList transformConstraints = this.transformConstraints; + for (int i = 0, n = transformConstraints.Count; i < n; i++) { + TransformConstraint transformConstraint = transformConstraints.Items[i]; + if (transformConstraint.data.name == constraintName) return transformConstraint; + } + return null; + } + + /// May be null. + public PathConstraint FindPathConstraint (string constraintName) { + if (constraintName == null) throw new ArgumentNullException("constraintName", "constraintName cannot be null."); + ExposedList pathConstraints = this.pathConstraints; + for (int i = 0, n = pathConstraints.Count; i < n; i++) { + PathConstraint constraint = pathConstraints.Items[i]; + if (constraint.data.name.Equals(constraintName)) return constraint; + } + return null; + } + + public void Update (float delta) { + time += delta; + } + + /// Returns the axis aligned bounding box (AABB) of the region and mesh attachments for the current pose. + /// The horizontal distance between the skeleton origin and the left side of the AABB. + /// The vertical distance between the skeleton origin and the bottom side of the AABB. + /// The width of the AABB + /// The height of the AABB. + /// Reference to hold a float[]. May be a null reference. This method will assign it a new float[] with the appropriate size as needed. + public void GetBounds (out float x, out float y, out float width, out float height, ref float[] vertexBuffer) { + float[] temp = vertexBuffer; + temp = temp ?? new float[8]; + var drawOrderItems = this.drawOrder.Items; + float minX = int.MaxValue, minY = int.MaxValue, maxX = int.MinValue, maxY = int.MinValue; + for (int i = 0, n = this.drawOrder.Count; i < n; i++) { + Slot slot = drawOrderItems[i]; + int verticesLength = 0; + float[] vertices = null; + Attachment attachment = slot.attachment; + var regionAttachment = attachment as RegionAttachment; + if (regionAttachment != null) { + verticesLength = 8; + vertices = temp; + if (vertices.Length < 8) vertices = temp = new float[8]; + regionAttachment.ComputeWorldVertices(slot.bone, temp, 0); + } else { + var meshAttachment = attachment as MeshAttachment; + if (meshAttachment != null) { + MeshAttachment mesh = meshAttachment; + verticesLength = mesh.WorldVerticesLength; + vertices = temp; + if (vertices.Length < verticesLength) vertices = temp = new float[verticesLength]; + mesh.ComputeWorldVertices(slot, 0, verticesLength, temp, 0); + } + } + + 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; + vertexBuffer = temp; + } + } +} diff --git a/SpineRuntimes/SpineRuntime37/SkeletonBinary.cs b/SpineRuntimes/SpineRuntime37/SkeletonBinary.cs new file mode 100644 index 0000000..39534b0 --- /dev/null +++ b/SpineRuntimes/SpineRuntime37/SkeletonBinary.cs @@ -0,0 +1,910 @@ +/****************************************************************************** + * Spine Runtimes License Agreement + * Last updated May 1, 2019. Replaces all prior versions. + * + * Copyright (c) 2013-2019, Esoteric Software LLC + * + * Integration of the Spine Runtimes into software or otherwise creating + * derivative works of the Spine Runtimes is permitted under the terms and + * conditions of Section 2 of the Spine Editor License Agreement: + * http://esotericsoftware.com/spine-editor-license + * + * Otherwise, it is permitted to integrate the Spine Runtimes into software + * or otherwise create derivative works of the Spine Runtimes (collectively, + * "Products"), provided that each user of the Products must obtain their own + * Spine Editor license and redistribution of the Products in any form must + * include this license and copyright notice. + * + * THIS SOFTWARE IS PROVIDED BY ESOTERIC SOFTWARE LLC "AS IS" AND ANY EXPRESS + * OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES + * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN + * NO EVENT SHALL ESOTERIC SOFTWARE LLC BE LIABLE FOR ANY DIRECT, INDIRECT, + * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, + * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES, BUSINESS + * INTERRUPTION, OR LOSS OF USE, DATA, OR PROFITS) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, + * EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + *****************************************************************************/ + +#if (UNITY_5 || UNITY_5_3_OR_NEWER || UNITY_WSA || UNITY_WP8 || UNITY_WP8_1) +#define IS_UNITY +#endif + +using System; +using System.IO; +using System.Collections.Generic; + +#if WINDOWS_STOREAPP +using System.Threading.Tasks; +using Windows.Storage; +#endif + +namespace SpineRuntime37 { + public class SkeletonBinary { + public const int BONE_ROTATE = 0; + public const int BONE_TRANSLATE = 1; + public const int BONE_SCALE = 2; + public const int BONE_SHEAR = 3; + + public const int SLOT_ATTACHMENT = 0; + public const int SLOT_COLOR = 1; + public const int SLOT_TWO_COLOR = 2; + + public const int PATH_POSITION = 0; + public const int PATH_SPACING = 1; + public const int PATH_MIX = 2; + + public const int CURVE_LINEAR = 0; + public const int CURVE_STEPPED = 1; + public const int CURVE_BEZIER = 2; + + public float Scale { get; set; } + + private AttachmentLoader attachmentLoader; + private byte[] buffer = new byte[32]; + private List linkedMeshes = new List(); + + public SkeletonBinary (params Atlas[] atlasArray) + : this(new AtlasAttachmentLoader(atlasArray)) { + } + + public SkeletonBinary (AttachmentLoader attachmentLoader) { + if (attachmentLoader == null) throw new ArgumentNullException("attachmentLoader"); + this.attachmentLoader = attachmentLoader; + Scale = 1; + } + + #if !ISUNITY && WINDOWS_STOREAPP + private async Task ReadFile(string path) { + var folder = Windows.ApplicationModel.Package.Current.InstalledLocation; + using (var input = new BufferedStream(await folder.GetFileAsync(path).AsTask().ConfigureAwait(false))) { + SkeletonData skeletonData = ReadSkeletonData(input); + skeletonData.Name = Path.GetFileNameWithoutExtension(path); + return skeletonData; + } + } + + public SkeletonData ReadSkeletonData (String path) { + return this.ReadFile(path).Result; + } + #else + public SkeletonData ReadSkeletonData (String path) { + #if WINDOWS_PHONE + using (var input = new BufferedStream(Microsoft.Xna.Framework.TitleContainer.OpenStream(path))) { + #else + using (var input = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.Read)) { + #endif + SkeletonData skeletonData = ReadSkeletonData(input); + skeletonData.name = Path.GetFileNameWithoutExtension(path); + return skeletonData; + } + } + #endif // WINDOWS_STOREAPP + + public static readonly TransformMode[] TransformModeValues = { + TransformMode.Normal, + TransformMode.OnlyTranslation, + TransformMode.NoRotationOrReflection, + TransformMode.NoScale, + TransformMode.NoScaleOrReflection + }; + + /// Returns the version string of binary skeleton data. + public static string GetVersionString (Stream input) { + if (input == null) throw new ArgumentNullException("input"); + + try { + // Hash. + int byteCount = ReadVarint(input, true); + if (byteCount > 1) input.Position += byteCount - 1; + + // Version. + byteCount = ReadVarint(input, true); + if (byteCount > 1) { + byteCount--; + var buffer = new byte[byteCount]; + ReadFully(input, buffer, 0, byteCount); + return System.Text.Encoding.UTF8.GetString(buffer, 0, byteCount); + } + + throw new ArgumentException("Stream does not contain a valid binary Skeleton Data.", "input"); + } catch (Exception e) { + throw new ArgumentException("Stream does not contain a valid binary Skeleton Data.\n" + e, "input"); + } + } + + public SkeletonData ReadSkeletonData (Stream input) { + if (input == null) throw new ArgumentNullException("input"); + float scale = Scale; + + var skeletonData = new SkeletonData(); + skeletonData.hash = ReadString(input); + if (skeletonData.hash.Length == 0) skeletonData.hash = null; + skeletonData.version = ReadString(input); + if (skeletonData.version.Length == 0) skeletonData.version = null; + skeletonData.width = ReadFloat(input); + skeletonData.height = ReadFloat(input); + + bool nonessential = ReadBoolean(input); + + if (nonessential) { + skeletonData.fps = ReadFloat(input); + + skeletonData.imagesPath = ReadString(input); + if (string.IsNullOrEmpty(skeletonData.imagesPath)) skeletonData.imagesPath = null; + + skeletonData.audioPath = ReadString(input); + if (string.IsNullOrEmpty(skeletonData.audioPath)) skeletonData.audioPath = null; + } + + // Bones. + for (int i = 0, n = ReadVarint(input, true); i < n; i++) { + String name = ReadString(input); + BoneData parent = i == 0 ? null : skeletonData.bones.Items[ReadVarint(input, true)]; + BoneData data = new BoneData(i, name, parent); + data.rotation = ReadFloat(input); + data.x = ReadFloat(input) * scale; + data.y = ReadFloat(input) * scale; + data.scaleX = ReadFloat(input); + data.scaleY = ReadFloat(input); + data.shearX = ReadFloat(input); + data.shearY = ReadFloat(input); + data.length = ReadFloat(input) * scale; + data.transformMode = TransformModeValues[ReadVarint(input, true)]; + if (nonessential) ReadInt(input); // Skip bone color. + skeletonData.bones.Add(data); + } + + // Slots. + for (int i = 0, n = ReadVarint(input, true); i < n; i++) { + String slotName = ReadString(input); + BoneData boneData = skeletonData.bones.Items[ReadVarint(input, true)]; + SlotData slotData = new SlotData(i, slotName, boneData); + int color = ReadInt(input); + slotData.r = ((color & 0xff000000) >> 24) / 255f; + slotData.g = ((color & 0x00ff0000) >> 16) / 255f; + slotData.b = ((color & 0x0000ff00) >> 8) / 255f; + slotData.a = ((color & 0x000000ff)) / 255f; + + int darkColor = ReadInt(input); // 0x00rrggbb + if (darkColor != -1) { + slotData.hasSecondColor = true; + slotData.r2 = ((darkColor & 0x00ff0000) >> 16) / 255f; + slotData.g2 = ((darkColor & 0x0000ff00) >> 8) / 255f; + slotData.b2 = ((darkColor & 0x000000ff)) / 255f; + } + + slotData.attachmentName = ReadString(input); + slotData.blendMode = (BlendMode)ReadVarint(input, true); + skeletonData.slots.Add(slotData); + } + + // IK constraints. + for (int i = 0, n = ReadVarint(input, true); i < n; i++) { + IkConstraintData data = new IkConstraintData(ReadString(input)); + data.order = ReadVarint(input, true); + for (int ii = 0, nn = ReadVarint(input, true); ii < nn; ii++) + data.bones.Add(skeletonData.bones.Items[ReadVarint(input, true)]); + data.target = skeletonData.bones.Items[ReadVarint(input, true)]; + data.mix = ReadFloat(input); + data.bendDirection = ReadSByte(input); + data.compress = ReadBoolean(input); + data.stretch = ReadBoolean(input); + data.uniform = ReadBoolean(input); + skeletonData.ikConstraints.Add(data); + } + + // Transform constraints. + for (int i = 0, n = ReadVarint(input, true); i < n; i++) { + TransformConstraintData data = new TransformConstraintData(ReadString(input)); + data.order = ReadVarint(input, true); + for (int ii = 0, nn = ReadVarint(input, true); ii < nn; ii++) + data.bones.Add(skeletonData.bones.Items[ReadVarint(input, true)]); + data.target = skeletonData.bones.Items[ReadVarint(input, true)]; + data.local = ReadBoolean(input); + data.relative = ReadBoolean(input); + data.offsetRotation = ReadFloat(input); + data.offsetX = ReadFloat(input) * scale; + data.offsetY = ReadFloat(input) * scale; + data.offsetScaleX = ReadFloat(input); + data.offsetScaleY = ReadFloat(input); + data.offsetShearY = ReadFloat(input); + data.rotateMix = ReadFloat(input); + data.translateMix = ReadFloat(input); + data.scaleMix = ReadFloat(input); + data.shearMix = ReadFloat(input); + skeletonData.transformConstraints.Add(data); + } + + // Path constraints + for (int i = 0, n = ReadVarint(input, true); i < n; i++) { + PathConstraintData data = new PathConstraintData(ReadString(input)); + data.order = ReadVarint(input, true); + for (int ii = 0, nn = ReadVarint(input, true); ii < nn; ii++) + data.bones.Add(skeletonData.bones.Items[ReadVarint(input, true)]); + data.target = skeletonData.slots.Items[ReadVarint(input, true)]; + data.positionMode = (PositionMode)Enum.GetValues(typeof(PositionMode)).GetValue(ReadVarint(input, true)); + data.spacingMode = (SpacingMode)Enum.GetValues(typeof(SpacingMode)).GetValue(ReadVarint(input, true)); + data.rotateMode = (RotateMode)Enum.GetValues(typeof(RotateMode)).GetValue(ReadVarint(input, true)); + data.offsetRotation = ReadFloat(input); + data.position = ReadFloat(input); + if (data.positionMode == PositionMode.Fixed) data.position *= scale; + data.spacing = ReadFloat(input); + if (data.spacingMode == SpacingMode.Length || data.spacingMode == SpacingMode.Fixed) data.spacing *= scale; + data.rotateMix = ReadFloat(input); + data.translateMix = ReadFloat(input); + skeletonData.pathConstraints.Add(data); + } + + // Default skin. + Skin defaultSkin = ReadSkin(input, skeletonData, "default", nonessential); + if (defaultSkin != null) { + skeletonData.defaultSkin = defaultSkin; + skeletonData.skins.Add(defaultSkin); + } + + // Skins. + for (int i = 0, n = ReadVarint(input, true); i < n; i++) + skeletonData.skins.Add(ReadSkin(input, skeletonData, ReadString(input), nonessential)); + + // Linked meshes. + for (int i = 0, n = linkedMeshes.Count; i < n; i++) { + SkeletonJson.LinkedMesh linkedMesh = linkedMeshes[i]; + Skin skin = linkedMesh.skin == null ? skeletonData.DefaultSkin : skeletonData.FindSkin(linkedMesh.skin); + if (skin == null) throw new Exception("Skin not found: " + linkedMesh.skin); + Attachment parent = skin.GetAttachment(linkedMesh.slotIndex, linkedMesh.parent); + if (parent == null) throw new Exception("Parent mesh not found: " + linkedMesh.parent); + linkedMesh.mesh.ParentMesh = (MeshAttachment)parent; + linkedMesh.mesh.UpdateUVs(); + } + linkedMeshes.Clear(); + + // Events. + for (int i = 0, n = ReadVarint(input, true); i < n; i++) { + EventData data = new EventData(ReadString(input)); + data.Int = ReadVarint(input, false); + data.Float = ReadFloat(input); + data.String = ReadString(input); + data.AudioPath = ReadString(input); + if (data.AudioPath != null) { + data.Volume = ReadFloat(input); + data.Balance = ReadFloat(input); + } + skeletonData.events.Add(data); + } + + // Animations. + for (int i = 0, n = ReadVarint(input, true); i < n; i++) + ReadAnimation(ReadString(input), input, skeletonData); + + skeletonData.bones.TrimExcess(); + skeletonData.slots.TrimExcess(); + skeletonData.skins.TrimExcess(); + skeletonData.events.TrimExcess(); + skeletonData.animations.TrimExcess(); + skeletonData.ikConstraints.TrimExcess(); + skeletonData.pathConstraints.TrimExcess(); + return skeletonData; + } + + + /// May be null. + private Skin ReadSkin (Stream input, SkeletonData skeletonData, String skinName, bool nonessential) { + int slotCount = ReadVarint(input, true); + if (slotCount == 0) return null; + Skin skin = new Skin(skinName); + for (int i = 0; i < slotCount; i++) { + int slotIndex = ReadVarint(input, true); + for (int ii = 0, nn = ReadVarint(input, true); ii < nn; ii++) { + String name = ReadString(input); + Attachment attachment = ReadAttachment(input, skeletonData, skin, slotIndex, name, nonessential); + if (attachment != null) skin.AddAttachment(slotIndex, name, attachment); + } + } + return skin; + } + + private Attachment ReadAttachment (Stream input, SkeletonData skeletonData, Skin skin, int slotIndex, String attachmentName, bool nonessential) { + float scale = Scale; + + String name = ReadString(input); + if (name == null) name = attachmentName; + + AttachmentType type = (AttachmentType)input.ReadByte(); + switch (type) { + case AttachmentType.Region: { + String path = ReadString(input); + float rotation = ReadFloat(input); + float x = ReadFloat(input); + float y = ReadFloat(input); + float scaleX = ReadFloat(input); + float scaleY = ReadFloat(input); + float width = ReadFloat(input); + float height = ReadFloat(input); + int color = ReadInt(input); + + if (path == null) path = name; + RegionAttachment region = attachmentLoader.NewRegionAttachment(skin, name, path); + if (region == null) return null; + region.Path = path; + region.x = x * scale; + region.y = y * scale; + region.scaleX = scaleX; + region.scaleY = scaleY; + region.rotation = rotation; + region.width = width * scale; + region.height = height * scale; + region.r = ((color & 0xff000000) >> 24) / 255f; + region.g = ((color & 0x00ff0000) >> 16) / 255f; + region.b = ((color & 0x0000ff00) >> 8) / 255f; + region.a = ((color & 0x000000ff)) / 255f; + region.UpdateOffset(); + return region; + } + case AttachmentType.Boundingbox: { + int vertexCount = ReadVarint(input, true); + Vertices vertices = ReadVertices(input, vertexCount); + if (nonessential) ReadInt(input); //int color = nonessential ? ReadInt(input) : 0; // Avoid unused local warning. + + BoundingBoxAttachment box = attachmentLoader.NewBoundingBoxAttachment(skin, name); + if (box == null) return null; + box.worldVerticesLength = vertexCount << 1; + box.vertices = vertices.vertices; + box.bones = vertices.bones; + return box; + } + case AttachmentType.Mesh: { + String path = ReadString(input); + int color = ReadInt(input); + int vertexCount = ReadVarint(input, true); + float[] uvs = ReadFloatArray(input, vertexCount << 1, 1); + int[] triangles = ReadShortArray(input); + Vertices vertices = ReadVertices(input, vertexCount); + int hullLength = ReadVarint(input, true); + int[] edges = null; + float width = 0, height = 0; + if (nonessential) { + edges = ReadShortArray(input); + width = ReadFloat(input); + height = ReadFloat(input); + } + + if (path == null) path = name; + MeshAttachment mesh = attachmentLoader.NewMeshAttachment(skin, name, path); + if (mesh == null) return null; + mesh.Path = path; + mesh.r = ((color & 0xff000000) >> 24) / 255f; + mesh.g = ((color & 0x00ff0000) >> 16) / 255f; + mesh.b = ((color & 0x0000ff00) >> 8) / 255f; + mesh.a = ((color & 0x000000ff)) / 255f; + mesh.bones = vertices.bones; + mesh.vertices = vertices.vertices; + mesh.WorldVerticesLength = vertexCount << 1; + mesh.triangles = triangles; + mesh.regionUVs = uvs; + mesh.UpdateUVs(); + mesh.HullLength = hullLength << 1; + if (nonessential) { + mesh.Edges = edges; + mesh.Width = width * scale; + mesh.Height = height * scale; + } + return mesh; + } + case AttachmentType.Linkedmesh: { + String path = ReadString(input); + int color = ReadInt(input); + String skinName = ReadString(input); + String parent = ReadString(input); + bool inheritDeform = ReadBoolean(input); + float width = 0, height = 0; + if (nonessential) { + width = ReadFloat(input); + height = ReadFloat(input); + } + + if (path == null) path = name; + MeshAttachment mesh = attachmentLoader.NewMeshAttachment(skin, name, path); + if (mesh == null) return null; + mesh.Path = path; + mesh.r = ((color & 0xff000000) >> 24) / 255f; + mesh.g = ((color & 0x00ff0000) >> 16) / 255f; + mesh.b = ((color & 0x0000ff00) >> 8) / 255f; + mesh.a = ((color & 0x000000ff)) / 255f; + mesh.inheritDeform = inheritDeform; + if (nonessential) { + mesh.Width = width * scale; + mesh.Height = height * scale; + } + linkedMeshes.Add(new SkeletonJson.LinkedMesh(mesh, skinName, slotIndex, parent)); + return mesh; + } + case AttachmentType.Path: { + bool closed = ReadBoolean(input); + bool constantSpeed = ReadBoolean(input); + int vertexCount = ReadVarint(input, true); + Vertices vertices = ReadVertices(input, vertexCount); + float[] lengths = new float[vertexCount / 3]; + for (int i = 0, n = lengths.Length; i < n; i++) + lengths[i] = ReadFloat(input) * scale; + if (nonessential) ReadInt(input); //int color = nonessential ? ReadInt(input) : 0; + + PathAttachment path = attachmentLoader.NewPathAttachment(skin, name); + if (path == null) return null; + path.closed = closed; + path.constantSpeed = constantSpeed; + path.worldVerticesLength = vertexCount << 1; + path.vertices = vertices.vertices; + path.bones = vertices.bones; + path.lengths = lengths; + return path; + } + case AttachmentType.Point: { + float rotation = ReadFloat(input); + float x = ReadFloat(input); + float y = ReadFloat(input); + if (nonessential) ReadInt(input); //int color = nonessential ? ReadInt(input) : 0; + + PointAttachment point = attachmentLoader.NewPointAttachment(skin, name); + if (point == null) return null; + point.x = x * scale; + point.y = y * scale; + point.rotation = rotation; + //if (nonessential) point.color = color; + return point; + } + case AttachmentType.Clipping: { + int endSlotIndex = ReadVarint(input, true); + int vertexCount = ReadVarint(input, true); + Vertices vertices = ReadVertices(input, vertexCount); + if (nonessential) ReadInt(input); + + ClippingAttachment clip = attachmentLoader.NewClippingAttachment(skin, name); + if (clip == null) return null; + clip.EndSlot = skeletonData.slots.Items[endSlotIndex]; + clip.worldVerticesLength = vertexCount << 1; + clip.vertices = vertices.vertices; + clip.bones = vertices.bones; + return clip; + } + } + return null; + } + + private Vertices ReadVertices (Stream input, int vertexCount) { + float scale = Scale; + int verticesLength = vertexCount << 1; + Vertices vertices = new Vertices(); + if(!ReadBoolean(input)) { + vertices.vertices = ReadFloatArray(input, verticesLength, scale); + return vertices; + } + var weights = new ExposedList(verticesLength * 3 * 3); + var bonesArray = new ExposedList(verticesLength * 3); + for (int i = 0; i < vertexCount; i++) { + int boneCount = ReadVarint(input, true); + bonesArray.Add(boneCount); + for (int ii = 0; ii < boneCount; ii++) { + bonesArray.Add(ReadVarint(input, true)); + weights.Add(ReadFloat(input) * scale); + weights.Add(ReadFloat(input) * scale); + weights.Add(ReadFloat(input)); + } + } + + vertices.vertices = weights.ToArray(); + vertices.bones = bonesArray.ToArray(); + return vertices; + } + + private float[] ReadFloatArray (Stream input, int n, float scale) { + float[] array = new float[n]; + if (scale == 1) { + for (int i = 0; i < n; i++) + array[i] = ReadFloat(input); + } else { + for (int i = 0; i < n; i++) + array[i] = ReadFloat(input) * scale; + } + return array; + } + + private int[] ReadShortArray (Stream input) { + int n = ReadVarint(input, true); + int[] array = new int[n]; + for (int i = 0; i < n; i++) + array[i] = (input.ReadByte() << 8) | input.ReadByte(); + return array; + } + + private void ReadAnimation (String name, Stream input, SkeletonData skeletonData) { + var timelines = new ExposedList(); + float scale = Scale; + float duration = 0; + + // Slot timelines. + for (int i = 0, n = ReadVarint(input, true); i < n; i++) { + int slotIndex = ReadVarint(input, true); + for (int ii = 0, nn = ReadVarint(input, true); ii < nn; ii++) { + int timelineType = input.ReadByte(); + int frameCount = ReadVarint(input, true); + switch (timelineType) { + case SLOT_ATTACHMENT: { + AttachmentTimeline timeline = new AttachmentTimeline(frameCount); + timeline.slotIndex = slotIndex; + for (int frameIndex = 0; frameIndex < frameCount; frameIndex++) + timeline.SetFrame(frameIndex, ReadFloat(input), ReadString(input)); + timelines.Add(timeline); + duration = Math.Max(duration, timeline.frames[frameCount - 1]); + break; + } + case SLOT_COLOR: { + ColorTimeline timeline = new ColorTimeline(frameCount); + timeline.slotIndex = slotIndex; + for (int frameIndex = 0; frameIndex < frameCount; frameIndex++) { + float time = ReadFloat(input); + int color = ReadInt(input); + float r = ((color & 0xff000000) >> 24) / 255f; + float g = ((color & 0x00ff0000) >> 16) / 255f; + float b = ((color & 0x0000ff00) >> 8) / 255f; + float a = ((color & 0x000000ff)) / 255f; + timeline.SetFrame(frameIndex, time, r, g, b, a); + if (frameIndex < frameCount - 1) ReadCurve(input, frameIndex, timeline); + } + timelines.Add(timeline); + duration = Math.Max(duration, timeline.frames[(timeline.FrameCount - 1) * ColorTimeline.ENTRIES]); + break; + } + case SLOT_TWO_COLOR: { + TwoColorTimeline timeline = new TwoColorTimeline(frameCount); + timeline.slotIndex = slotIndex; + for (int frameIndex = 0; frameIndex < frameCount; frameIndex++) { + float time = ReadFloat(input); + int color = ReadInt(input); + float r = ((color & 0xff000000) >> 24) / 255f; + float g = ((color & 0x00ff0000) >> 16) / 255f; + float b = ((color & 0x0000ff00) >> 8) / 255f; + float a = ((color & 0x000000ff)) / 255f; + int color2 = ReadInt(input); // 0x00rrggbb + float r2 = ((color2 & 0x00ff0000) >> 16) / 255f; + float g2 = ((color2 & 0x0000ff00) >> 8) / 255f; + float b2 = ((color2 & 0x000000ff)) / 255f; + + timeline.SetFrame(frameIndex, time, r, g, b, a, r2, g2, b2); + if (frameIndex < frameCount - 1) ReadCurve(input, frameIndex, timeline); + } + timelines.Add(timeline); + duration = Math.Max(duration, timeline.frames[(timeline.FrameCount - 1) * TwoColorTimeline.ENTRIES]); + break; + } + } + } + } + + // Bone timelines. + for (int i = 0, n = ReadVarint(input, true); i < n; i++) { + int boneIndex = ReadVarint(input, true); + for (int ii = 0, nn = ReadVarint(input, true); ii < nn; ii++) { + int timelineType = input.ReadByte(); + int frameCount = ReadVarint(input, true); + switch (timelineType) { + case BONE_ROTATE: { + RotateTimeline timeline = new RotateTimeline(frameCount); + timeline.boneIndex = boneIndex; + for (int frameIndex = 0; frameIndex < frameCount; frameIndex++) { + timeline.SetFrame(frameIndex, ReadFloat(input), ReadFloat(input)); + if (frameIndex < frameCount - 1) ReadCurve(input, frameIndex, timeline); + } + timelines.Add(timeline); + duration = Math.Max(duration, timeline.frames[(frameCount - 1) * RotateTimeline.ENTRIES]); + break; + } + case BONE_TRANSLATE: + case BONE_SCALE: + case BONE_SHEAR: { + TranslateTimeline timeline; + float timelineScale = 1; + if (timelineType == BONE_SCALE) + timeline = new ScaleTimeline(frameCount); + else if (timelineType == BONE_SHEAR) + timeline = new ShearTimeline(frameCount); + else { + timeline = new TranslateTimeline(frameCount); + timelineScale = scale; + } + timeline.boneIndex = boneIndex; + for (int frameIndex = 0; frameIndex < frameCount; frameIndex++) { + timeline.SetFrame(frameIndex, ReadFloat(input), ReadFloat(input) * timelineScale, ReadFloat(input) + * timelineScale); + if (frameIndex < frameCount - 1) ReadCurve(input, frameIndex, timeline); + } + timelines.Add(timeline); + duration = Math.Max(duration, timeline.frames[(frameCount - 1) * TranslateTimeline.ENTRIES]); + break; + } + } + } + } + + // IK timelines. + for (int i = 0, n = ReadVarint(input, true); i < n; i++) { + int index = ReadVarint(input, true); + int frameCount = ReadVarint(input, true); + IkConstraintTimeline timeline = new IkConstraintTimeline(frameCount) { + ikConstraintIndex = index + }; + for (int frameIndex = 0; frameIndex < frameCount; frameIndex++) { + timeline.SetFrame(frameIndex, ReadFloat(input), ReadFloat(input), ReadSByte(input), ReadBoolean(input), ReadBoolean(input)); + if (frameIndex < frameCount - 1) ReadCurve(input, frameIndex, timeline); + } + timelines.Add(timeline); + duration = Math.Max(duration, timeline.frames[(frameCount - 1) * IkConstraintTimeline.ENTRIES]); + } + + // Transform constraint timelines. + for (int i = 0, n = ReadVarint(input, true); i < n; i++) { + int index = ReadVarint(input, true); + int frameCount = ReadVarint(input, true); + TransformConstraintTimeline timeline = new TransformConstraintTimeline(frameCount); + timeline.transformConstraintIndex = index; + for (int frameIndex = 0; frameIndex < frameCount; frameIndex++) { + timeline.SetFrame(frameIndex, ReadFloat(input), ReadFloat(input), ReadFloat(input), ReadFloat(input), ReadFloat(input)); + if (frameIndex < frameCount - 1) ReadCurve(input, frameIndex, timeline); + } + timelines.Add(timeline); + duration = Math.Max(duration, timeline.frames[(frameCount - 1) * TransformConstraintTimeline.ENTRIES]); + } + + // Path constraint timelines. + for (int i = 0, n = ReadVarint(input, true); i < n; i++) { + int index = ReadVarint(input, true); + PathConstraintData data = skeletonData.pathConstraints.Items[index]; + for (int ii = 0, nn = ReadVarint(input, true); ii < nn; ii++) { + int timelineType = ReadSByte(input); + int frameCount = ReadVarint(input, true); + switch(timelineType) { + case PATH_POSITION: + case PATH_SPACING: { + PathConstraintPositionTimeline timeline; + float timelineScale = 1; + if (timelineType == PATH_SPACING) { + timeline = new PathConstraintSpacingTimeline(frameCount); + if (data.spacingMode == SpacingMode.Length || data.spacingMode == SpacingMode.Fixed) timelineScale = scale; + } else { + timeline = new PathConstraintPositionTimeline(frameCount); + if (data.positionMode == PositionMode.Fixed) timelineScale = scale; + } + timeline.pathConstraintIndex = index; + for (int frameIndex = 0; frameIndex < frameCount; frameIndex++) { + timeline.SetFrame(frameIndex, ReadFloat(input), ReadFloat(input) * timelineScale); + if (frameIndex < frameCount - 1) ReadCurve(input, frameIndex, timeline); + } + timelines.Add(timeline); + duration = Math.Max(duration, timeline.frames[(frameCount - 1) * PathConstraintPositionTimeline.ENTRIES]); + break; + } + case PATH_MIX: { + PathConstraintMixTimeline timeline = new PathConstraintMixTimeline(frameCount); + timeline.pathConstraintIndex = index; + for (int frameIndex = 0; frameIndex < frameCount; frameIndex++) { + timeline.SetFrame(frameIndex, ReadFloat(input), ReadFloat(input), ReadFloat(input)); + if (frameIndex < frameCount - 1) ReadCurve(input, frameIndex, timeline); + } + timelines.Add(timeline); + duration = Math.Max(duration, timeline.frames[(frameCount - 1) * PathConstraintMixTimeline.ENTRIES]); + break; + } + } + } + } + + // Deform timelines. + for (int i = 0, n = ReadVarint(input, true); i < n; i++) { + Skin skin = skeletonData.skins.Items[ReadVarint(input, true)]; + for (int ii = 0, nn = ReadVarint(input, true); ii < nn; ii++) { + int slotIndex = ReadVarint(input, true); + for (int iii = 0, nnn = ReadVarint(input, true); iii < nnn; iii++) { + VertexAttachment attachment = (VertexAttachment)skin.GetAttachment(slotIndex, ReadString(input)); + bool weighted = attachment.bones != null; + float[] vertices = attachment.vertices; + int deformLength = weighted ? vertices.Length / 3 * 2 : vertices.Length; + + int frameCount = ReadVarint(input, true); + DeformTimeline timeline = new DeformTimeline(frameCount); + timeline.slotIndex = slotIndex; + timeline.attachment = attachment; + + for (int frameIndex = 0; frameIndex < frameCount; frameIndex++) { + float time = ReadFloat(input); + float[] deform; + int end = ReadVarint(input, true); + if (end == 0) + deform = weighted ? new float[deformLength] : vertices; + else { + deform = new float[deformLength]; + int start = ReadVarint(input, true); + end += start; + if (scale == 1) { + for (int v = start; v < end; v++) + deform[v] = ReadFloat(input); + } else { + for (int v = start; v < end; v++) + deform[v] = ReadFloat(input) * scale; + } + if (!weighted) { + for (int v = 0, vn = deform.Length; v < vn; v++) + deform[v] += vertices[v]; + } + } + + timeline.SetFrame(frameIndex, time, deform); + if (frameIndex < frameCount - 1) ReadCurve(input, frameIndex, timeline); + } + timelines.Add(timeline); + duration = Math.Max(duration, timeline.frames[frameCount - 1]); + } + } + } + + // Draw order timeline. + int drawOrderCount = ReadVarint(input, true); + if (drawOrderCount > 0) { + DrawOrderTimeline timeline = new DrawOrderTimeline(drawOrderCount); + int slotCount = skeletonData.slots.Count; + for (int i = 0; i < drawOrderCount; i++) { + float time = ReadFloat(input); + int offsetCount = ReadVarint(input, true); + int[] drawOrder = new int[slotCount]; + for (int ii = slotCount - 1; ii >= 0; ii--) + drawOrder[ii] = -1; + int[] unchanged = new int[slotCount - offsetCount]; + int originalIndex = 0, unchangedIndex = 0; + for (int ii = 0; ii < offsetCount; ii++) { + int slotIndex = ReadVarint(input, true); + // Collect unchanged items. + while (originalIndex != slotIndex) + unchanged[unchangedIndex++] = originalIndex++; + // Set changed items. + drawOrder[originalIndex + ReadVarint(input, true)] = originalIndex++; + } + // Collect remaining unchanged items. + while (originalIndex < slotCount) + unchanged[unchangedIndex++] = originalIndex++; + // Fill in unchanged items. + for (int ii = slotCount - 1; ii >= 0; ii--) + if (drawOrder[ii] == -1) drawOrder[ii] = unchanged[--unchangedIndex]; + timeline.SetFrame(i, time, drawOrder); + } + timelines.Add(timeline); + duration = Math.Max(duration, timeline.frames[drawOrderCount - 1]); + } + + // Event timeline. + int eventCount = ReadVarint(input, true); + if (eventCount > 0) { + EventTimeline timeline = new EventTimeline(eventCount); + for (int i = 0; i < eventCount; i++) { + float time = ReadFloat(input); + EventData eventData = skeletonData.events.Items[ReadVarint(input, true)]; + Event e = new Event(time, eventData) { + Int = ReadVarint(input, false), + Float = ReadFloat(input), + String = ReadBoolean(input) ? ReadString(input) : eventData.String + }; + if (e.data.AudioPath != null) { + e.volume = ReadFloat(input); + e.balance = ReadFloat(input); + } + timeline.SetFrame(i, e); + } + timelines.Add(timeline); + duration = Math.Max(duration, timeline.frames[eventCount - 1]); + } + + timelines.TrimExcess(); + skeletonData.animations.Add(new Animation(name, timelines, duration)); + } + + private void ReadCurve (Stream input, int frameIndex, CurveTimeline timeline) { + switch (input.ReadByte()) { + case CURVE_STEPPED: + timeline.SetStepped(frameIndex); + break; + case CURVE_BEZIER: + timeline.SetCurve(frameIndex, ReadFloat(input), ReadFloat(input), ReadFloat(input), ReadFloat(input)); + break; + } + } + + private static sbyte ReadSByte (Stream input) { + int value = input.ReadByte(); + if (value == -1) throw new EndOfStreamException(); + return (sbyte)value; + } + + private static bool ReadBoolean (Stream input) { + return input.ReadByte() != 0; + } + + private float ReadFloat (Stream input) { + buffer[3] = (byte)input.ReadByte(); + buffer[2] = (byte)input.ReadByte(); + buffer[1] = (byte)input.ReadByte(); + buffer[0] = (byte)input.ReadByte(); + return BitConverter.ToSingle(buffer, 0); + } + + private static int ReadInt (Stream input) { + return (input.ReadByte() << 24) + (input.ReadByte() << 16) + (input.ReadByte() << 8) + input.ReadByte(); + } + + private static int ReadVarint (Stream input, bool optimizePositive) { + int b = input.ReadByte(); + int result = b & 0x7F; + if ((b & 0x80) != 0) { + b = input.ReadByte(); + result |= (b & 0x7F) << 7; + if ((b & 0x80) != 0) { + b = input.ReadByte(); + result |= (b & 0x7F) << 14; + if ((b & 0x80) != 0) { + b = input.ReadByte(); + result |= (b & 0x7F) << 21; + if ((b & 0x80) != 0) result |= (input.ReadByte() & 0x7F) << 28; + } + } + } + return optimizePositive ? result : ((result >> 1) ^ -(result & 1)); + } + + private string ReadString (Stream input) { + int byteCount = ReadVarint(input, true); + switch (byteCount) { + case 0: + return null; + case 1: + return ""; + } + byteCount--; + byte[] buffer = this.buffer; + if (buffer.Length < byteCount) buffer = new byte[byteCount]; + ReadFully(input, buffer, 0, byteCount); + return System.Text.Encoding.UTF8.GetString(buffer, 0, byteCount); + } + + private static void ReadFully (Stream input, byte[] buffer, int offset, int length) { + while (length > 0) { + int count = input.Read(buffer, offset, length); + if (count <= 0) throw new EndOfStreamException(); + offset += count; + length -= count; + } + } + + internal class Vertices { + public int[] bones; + public float[] vertices; + } + } +} diff --git a/SpineRuntimes/SpineRuntime37/SkeletonBounds.cs b/SpineRuntimes/SpineRuntime37/SkeletonBounds.cs new file mode 100644 index 0000000..7407765 --- /dev/null +++ b/SpineRuntimes/SpineRuntime37/SkeletonBounds.cs @@ -0,0 +1,233 @@ +/****************************************************************************** + * Spine Runtimes License Agreement + * Last updated May 1, 2019. Replaces all prior versions. + * + * Copyright (c) 2013-2019, Esoteric Software LLC + * + * Integration of the Spine Runtimes into software or otherwise creating + * derivative works of the Spine Runtimes is permitted under the terms and + * conditions of Section 2 of the Spine Editor License Agreement: + * http://esotericsoftware.com/spine-editor-license + * + * Otherwise, it is permitted to integrate the Spine Runtimes into software + * or otherwise create derivative works of the Spine Runtimes (collectively, + * "Products"), provided that each user of the Products must obtain their own + * Spine Editor license and redistribution of the Products in any form must + * include this license and copyright notice. + * + * THIS SOFTWARE IS PROVIDED BY ESOTERIC SOFTWARE LLC "AS IS" AND ANY EXPRESS + * OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES + * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN + * NO EVENT SHALL ESOTERIC SOFTWARE LLC BE LIABLE FOR ANY DIRECT, INDIRECT, + * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, + * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES, BUSINESS + * INTERRUPTION, OR LOSS OF USE, DATA, OR PROFITS) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, + * EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + *****************************************************************************/ + +using System; + +namespace SpineRuntime37 { + + /// + /// Collects each BoundingBoxAttachment that is visible and computes the world vertices for its polygon. + /// The polygon vertices are provided along with convenience methods for doing hit detection. + /// + public class SkeletonBounds { + private ExposedList polygonPool = new ExposedList(); + private float minX, minY, maxX, maxY; + + public ExposedList BoundingBoxes { get; private set; } + public ExposedList Polygons { get; private set; } + public float MinX { get { return minX; } set { minX = value; } } + public float MinY { get { return minY; } set { minY = value; } } + public float MaxX { get { return maxX; } set { maxX = value; } } + public float MaxY { get { return maxY; } set { maxY = value; } } + public float Width { get { return maxX - minX; } } + public float Height { get { return maxY - minY; } } + + public SkeletonBounds () { + BoundingBoxes = new ExposedList(); + Polygons = new ExposedList(); + } + + /// + /// Clears any previous polygons, finds all visible bounding box attachments, + /// and computes the world vertices for each bounding box's polygon. + /// The skeleton. + /// + /// If true, the axis aligned bounding box containing all the polygons is computed. + /// If false, the SkeletonBounds AABB methods will always return true. + /// + public void Update (Skeleton skeleton, bool updateAabb) { + ExposedList boundingBoxes = BoundingBoxes; + ExposedList polygons = Polygons; + ExposedList slots = skeleton.slots; + int slotCount = slots.Count; + + boundingBoxes.Clear(); + for (int i = 0, n = polygons.Count; i < n; i++) + polygonPool.Add(polygons.Items[i]); + polygons.Clear(); + + for (int i = 0; i < slotCount; i++) { + Slot slot = slots.Items[i]; + BoundingBoxAttachment boundingBox = slot.attachment as BoundingBoxAttachment; + if (boundingBox == null) continue; + boundingBoxes.Add(boundingBox); + + Polygon polygon = null; + int poolCount = polygonPool.Count; + if (poolCount > 0) { + polygon = polygonPool.Items[poolCount - 1]; + polygonPool.RemoveAt(poolCount - 1); + } else + polygon = new Polygon(); + polygons.Add(polygon); + + int count = boundingBox.worldVerticesLength; + polygon.Count = count; + if (polygon.Vertices.Length < count) polygon.Vertices = new float[count]; + boundingBox.ComputeWorldVertices(slot, polygon.Vertices); + } + + if (updateAabb) { + AabbCompute(); + } else { + minX = int.MinValue; + minY = int.MinValue; + maxX = int.MaxValue; + maxY = int.MaxValue; + } + } + + private void AabbCompute () { + float minX = int.MaxValue, minY = int.MaxValue, maxX = int.MinValue, maxY = int.MinValue; + ExposedList polygons = Polygons; + for (int i = 0, n = polygons.Count; i < n; i++) { + Polygon polygon = polygons.Items[i]; + float[] vertices = polygon.Vertices; + for (int ii = 0, nn = polygon.Count; ii < nn; ii += 2) { + float x = vertices[ii]; + float y = vertices[ii + 1]; + minX = Math.Min(minX, x); + minY = Math.Min(minY, y); + maxX = Math.Max(maxX, x); + maxY = Math.Max(maxY, y); + } + } + this.minX = minX; + this.minY = minY; + this.maxX = maxX; + this.maxY = maxY; + } + + + /// Returns true if the axis aligned bounding box contains the point. + public bool AabbContainsPoint (float x, float y) { + return x >= minX && x <= maxX && y >= minY && y <= maxY; + } + + /// Returns true if the axis aligned bounding box intersects the line segment. + public bool AabbIntersectsSegment (float x1, float y1, float x2, float y2) { + float minX = this.minX; + float minY = this.minY; + float maxX = this.maxX; + float maxY = this.maxY; + if ((x1 <= minX && x2 <= minX) || (y1 <= minY && y2 <= minY) || (x1 >= maxX && x2 >= maxX) || (y1 >= maxY && y2 >= maxY)) + return false; + float m = (y2 - y1) / (x2 - x1); + float y = m * (minX - x1) + y1; + if (y > minY && y < maxY) return true; + y = m * (maxX - x1) + y1; + if (y > minY && y < maxY) return true; + float x = (minY - y1) / m + x1; + if (x > minX && x < maxX) return true; + x = (maxY - y1) / m + x1; + if (x > minX && x < maxX) return true; + return false; + } + + /// Returns true if the axis aligned bounding box intersects the axis aligned bounding box of the specified bounds. + public bool AabbIntersectsSkeleton (SkeletonBounds bounds) { + return minX < bounds.maxX && maxX > bounds.minX && minY < bounds.maxY && maxY > bounds.minY; + } + + /// Returns true if the polygon contains the point. + public bool ContainsPoint (Polygon polygon, float x, float y) { + float[] vertices = polygon.Vertices; + int nn = polygon.Count; + + int prevIndex = nn - 2; + bool inside = false; + for (int ii = 0; ii < nn; ii += 2) { + float vertexY = vertices[ii + 1]; + float prevY = vertices[prevIndex + 1]; + if ((vertexY < y && prevY >= y) || (prevY < y && vertexY >= y)) { + float vertexX = vertices[ii]; + if (vertexX + (y - vertexY) / (prevY - vertexY) * (vertices[prevIndex] - vertexX) < x) inside = !inside; + } + prevIndex = ii; + } + return inside; + } + + /// Returns the first bounding box attachment that contains the point, or null. When doing many checks, it is usually more + /// efficient to only call this method if {@link #aabbContainsPoint(float, float)} returns true. + public BoundingBoxAttachment ContainsPoint (float x, float y) { + ExposedList polygons = Polygons; + for (int i = 0, n = polygons.Count; i < n; i++) + if (ContainsPoint(polygons.Items[i], x, y)) return BoundingBoxes.Items[i]; + return null; + } + + /// Returns the first bounding box attachment that contains the line segment, or null. When doing many checks, it is usually + /// more efficient to only call this method if {@link #aabbIntersectsSegment(float, float, float, float)} returns true. + public BoundingBoxAttachment IntersectsSegment (float x1, float y1, float x2, float y2) { + ExposedList polygons = Polygons; + for (int i = 0, n = polygons.Count; i < n; i++) + if (IntersectsSegment(polygons.Items[i], x1, y1, x2, y2)) return BoundingBoxes.Items[i]; + return null; + } + + /// Returns true if the polygon contains the line segment. + public bool IntersectsSegment (Polygon polygon, float x1, float y1, float x2, float y2) { + float[] vertices = polygon.Vertices; + int nn = polygon.Count; + + float width12 = x1 - x2, height12 = y1 - y2; + float det1 = x1 * y2 - y1 * x2; + float x3 = vertices[nn - 2], y3 = vertices[nn - 1]; + for (int ii = 0; ii < nn; ii += 2) { + float x4 = vertices[ii], y4 = vertices[ii + 1]; + float det2 = x3 * y4 - y3 * x4; + float width34 = x3 - x4, height34 = y3 - y4; + float det3 = width12 * height34 - height12 * width34; + float x = (det1 * width34 - width12 * det2) / det3; + if (((x >= x3 && x <= x4) || (x >= x4 && x <= x3)) && ((x >= x1 && x <= x2) || (x >= x2 && x <= x1))) { + float y = (det1 * height34 - height12 * det2) / det3; + if (((y >= y3 && y <= y4) || (y >= y4 && y <= y3)) && ((y >= y1 && y <= y2) || (y >= y2 && y <= y1))) return true; + } + x3 = x4; + y3 = y4; + } + return false; + } + + public Polygon GetPolygon (BoundingBoxAttachment attachment) { + int index = BoundingBoxes.IndexOf(attachment); + return index == -1 ? null : Polygons.Items[index]; + } + } + + public class Polygon { + public float[] Vertices { get; set; } + public int Count { get; set; } + + public Polygon () { + Vertices = new float[16]; + } + } +} diff --git a/SpineRuntimes/SpineRuntime37/SkeletonClipping.cs b/SpineRuntimes/SpineRuntime37/SkeletonClipping.cs new file mode 100644 index 0000000..66292a9 --- /dev/null +++ b/SpineRuntimes/SpineRuntime37/SkeletonClipping.cs @@ -0,0 +1,296 @@ +/****************************************************************************** + * Spine Runtimes License Agreement + * Last updated May 1, 2019. Replaces all prior versions. + * + * Copyright (c) 2013-2019, Esoteric Software LLC + * + * Integration of the Spine Runtimes into software or otherwise creating + * derivative works of the Spine Runtimes is permitted under the terms and + * conditions of Section 2 of the Spine Editor License Agreement: + * http://esotericsoftware.com/spine-editor-license + * + * Otherwise, it is permitted to integrate the Spine Runtimes into software + * or otherwise create derivative works of the Spine Runtimes (collectively, + * "Products"), provided that each user of the Products must obtain their own + * Spine Editor license and redistribution of the Products in any form must + * include this license and copyright notice. + * + * THIS SOFTWARE IS PROVIDED BY ESOTERIC SOFTWARE LLC "AS IS" AND ANY EXPRESS + * OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES + * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN + * NO EVENT SHALL ESOTERIC SOFTWARE LLC BE LIABLE FOR ANY DIRECT, INDIRECT, + * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, + * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES, BUSINESS + * INTERRUPTION, OR LOSS OF USE, DATA, OR PROFITS) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, + * EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + *****************************************************************************/ + +using System; + +namespace SpineRuntime37 { + public class SkeletonClipping { + internal readonly Triangulator triangulator = new Triangulator(); + internal readonly ExposedList clippingPolygon = new ExposedList(); + internal readonly ExposedList clipOutput = new ExposedList(128); + internal readonly ExposedList clippedVertices = new ExposedList(128); + internal readonly ExposedList clippedTriangles = new ExposedList(128); + internal readonly ExposedList clippedUVs = new ExposedList(128); + internal readonly ExposedList scratch = new ExposedList(); + + internal ClippingAttachment clipAttachment; + internal ExposedList> clippingPolygons; + + public ExposedList ClippedVertices { get { return clippedVertices; } } + public ExposedList ClippedTriangles { get { return clippedTriangles; } } + public ExposedList ClippedUVs { get { return clippedUVs; } } + + public bool IsClipping { get { return clipAttachment != null; } } + + public int ClipStart (Slot slot, ClippingAttachment clip) { + if (clipAttachment != null) return 0; + clipAttachment = clip; + + int n = clip.worldVerticesLength; + float[] vertices = clippingPolygon.Resize(n).Items; + clip.ComputeWorldVertices(slot, 0, n, vertices, 0, 2); + MakeClockwise(clippingPolygon); + clippingPolygons = triangulator.Decompose(clippingPolygon, triangulator.Triangulate(clippingPolygon)); + foreach (var polygon in clippingPolygons) { + MakeClockwise(polygon); + polygon.Add(polygon.Items[0]); + polygon.Add(polygon.Items[1]); + } + return clippingPolygons.Count; + } + + public void ClipEnd (Slot slot) { + if (clipAttachment != null && clipAttachment.endSlot == slot.data) ClipEnd(); + } + + public void ClipEnd () { + if (clipAttachment == null) return; + clipAttachment = null; + clippingPolygons = null; + clippedVertices.Clear(); + clippedTriangles.Clear(); + clippingPolygon.Clear(); + } + + public void ClipTriangles (float[] vertices, int verticesLength, int[] triangles, int trianglesLength, float[] uvs) { + ExposedList clipOutput = this.clipOutput, clippedVertices = this.clippedVertices; + var clippedTriangles = this.clippedTriangles; + var polygons = clippingPolygons.Items; + int polygonsCount = clippingPolygons.Count; + + int index = 0; + clippedVertices.Clear(); + clippedUVs.Clear(); + clippedTriangles.Clear(); + //outer: + for (int i = 0; i < trianglesLength; i += 3) { + int vertexOffset = triangles[i] << 1; + float x1 = vertices[vertexOffset], y1 = vertices[vertexOffset + 1]; + float u1 = uvs[vertexOffset], v1 = uvs[vertexOffset + 1]; + + vertexOffset = triangles[i + 1] << 1; + float x2 = vertices[vertexOffset], y2 = vertices[vertexOffset + 1]; + float u2 = uvs[vertexOffset], v2 = uvs[vertexOffset + 1]; + + vertexOffset = triangles[i + 2] << 1; + float x3 = vertices[vertexOffset], y3 = vertices[vertexOffset + 1]; + float u3 = uvs[vertexOffset], v3 = uvs[vertexOffset + 1]; + + for (int p = 0; p < polygonsCount; p++) { + int s = clippedVertices.Count; + if (Clip(x1, y1, x2, y2, x3, y3, polygons[p], clipOutput)) { + int clipOutputLength = clipOutput.Count; + if (clipOutputLength == 0) continue; + float d0 = y2 - y3, d1 = x3 - x2, d2 = x1 - x3, d4 = y3 - y1; + float d = 1 / (d0 * d2 + d1 * (y1 - y3)); + + int clipOutputCount = clipOutputLength >> 1; + float[] clipOutputItems = clipOutput.Items; + float[] clippedVerticesItems = clippedVertices.Resize(s + clipOutputCount * 2).Items; + float[] clippedUVsItems = clippedUVs.Resize(s + clipOutputCount * 2).Items; + for (int ii = 0; ii < clipOutputLength; ii += 2) { + float x = clipOutputItems[ii], y = clipOutputItems[ii + 1]; + clippedVerticesItems[s] = x; + clippedVerticesItems[s + 1] = y; + float c0 = x - x3, c1 = y - y3; + float a = (d0 * c0 + d1 * c1) * d; + float b = (d4 * c0 + d2 * c1) * d; + float c = 1 - a - b; + clippedUVsItems[s] = u1 * a + u2 * b + u3 * c; + clippedUVsItems[s + 1] = v1 * a + v2 * b + v3 * c; + s += 2; + } + + s = clippedTriangles.Count; + int[] clippedTrianglesItems = clippedTriangles.Resize(s + 3 * (clipOutputCount - 2)).Items; + clipOutputCount--; + for (int ii = 1; ii < clipOutputCount; ii++) { + clippedTrianglesItems[s] = index; + clippedTrianglesItems[s + 1] = index + ii; + clippedTrianglesItems[s + 2] = index + ii + 1; + s += 3; + } + index += clipOutputCount + 1; + } + else { + float[] clippedVerticesItems = clippedVertices.Resize(s + 3 * 2).Items; + float[] clippedUVsItems = clippedUVs.Resize(s + 3 * 2).Items; + clippedVerticesItems[s] = x1; + clippedVerticesItems[s + 1] = y1; + clippedVerticesItems[s + 2] = x2; + clippedVerticesItems[s + 3] = y2; + clippedVerticesItems[s + 4] = x3; + clippedVerticesItems[s + 5] = y3; + + clippedUVsItems[s] = u1; + clippedUVsItems[s + 1] = v1; + clippedUVsItems[s + 2] = u2; + clippedUVsItems[s + 3] = v2; + clippedUVsItems[s + 4] = u3; + clippedUVsItems[s + 5] = v3; + + s = clippedTriangles.Count; + int[] clippedTrianglesItems = clippedTriangles.Resize(s + 3).Items; + clippedTrianglesItems[s] = index; + clippedTrianglesItems[s + 1] = index + 1; + clippedTrianglesItems[s + 2] = index + 2; + index += 3; + break; //continue outer; + } + } + } + + } + + /** Clips the input triangle against the convex, clockwise clipping area. If the triangle lies entirely within the clipping + * area, false is returned. The clipping area must duplicate the first vertex at the end of the vertices list. */ + internal bool Clip (float x1, float y1, float x2, float y2, float x3, float y3, ExposedList clippingArea, ExposedList output) { + var originalOutput = output; + var clipped = false; + + // Avoid copy at the end. + ExposedList input = null; + if (clippingArea.Count % 4 >= 2) { + input = output; + output = scratch; + } else { + input = scratch; + } + + input.Clear(); + input.Add(x1); + input.Add(y1); + input.Add(x2); + input.Add(y2); + input.Add(x3); + input.Add(y3); + input.Add(x1); + input.Add(y1); + output.Clear(); + + float[] clippingVertices = clippingArea.Items; + int clippingVerticesLast = clippingArea.Count - 4; + for (int i = 0; ; i += 2) { + float edgeX = clippingVertices[i], edgeY = clippingVertices[i + 1]; + float edgeX2 = clippingVertices[i + 2], edgeY2 = clippingVertices[i + 3]; + float deltaX = edgeX - edgeX2, deltaY = edgeY - edgeY2; + + float[] inputVertices = input.Items; + int inputVerticesLength = input.Count - 2, outputStart = output.Count; + for (int ii = 0; ii < inputVerticesLength; ii += 2) { + float inputX = inputVertices[ii], inputY = inputVertices[ii + 1]; + float inputX2 = inputVertices[ii + 2], inputY2 = inputVertices[ii + 3]; + bool side2 = deltaX * (inputY2 - edgeY2) - deltaY * (inputX2 - edgeX2) > 0; + if (deltaX * (inputY - edgeY2) - deltaY * (inputX - edgeX2) > 0) { + if (side2) { // v1 inside, v2 inside + output.Add(inputX2); + output.Add(inputY2); + continue; + } + // v1 inside, v2 outside + float c0 = inputY2 - inputY, c2 = inputX2 - inputX; + float s = c0 * (edgeX2 - edgeX) - c2 * (edgeY2 - edgeY); + if (Math.Abs(s) > 0.000001f) { + float ua = (c2 * (edgeY - inputY) - c0 * (edgeX - inputX)) / s; + output.Add(edgeX + (edgeX2 - edgeX) * ua); + output.Add(edgeY + (edgeY2 - edgeY) * ua); + } else { + output.Add(edgeX); + output.Add(edgeY); + } + } + else if (side2) { // v1 outside, v2 inside + float c0 = inputY2 - inputY, c2 = inputX2 - inputX; + float s = c0 * (edgeX2 - edgeX) - c2 * (edgeY2 - edgeY); + if (Math.Abs(s) > 0.000001f) { + float ua = (c2 * (edgeY - inputY) - c0 * (edgeX - inputX)) / s; + output.Add(edgeX + (edgeX2 - edgeX) * ua); + output.Add(edgeY + (edgeY2 - edgeY) * ua); + } else { + output.Add(edgeX); + output.Add(edgeY); + } + output.Add(inputX2); + output.Add(inputY2); + } + clipped = true; + } + + if (outputStart == output.Count) { // All edges outside. + originalOutput.Clear(); + return true; + } + + output.Add(output.Items[0]); + output.Add(output.Items[1]); + + if (i == clippingVerticesLast) break; + var temp = output; + output = input; + output.Clear(); + input = temp; + } + + if (originalOutput != output) { + originalOutput.Clear(); + for (int i = 0, n = output.Count - 2; i < n; i++) { + originalOutput.Add(output.Items[i]); + } + } else { + originalOutput.Resize(originalOutput.Count - 2); + } + + return clipped; + } + + public static void MakeClockwise (ExposedList polygon) { + float[] vertices = polygon.Items; + int verticeslength = polygon.Count; + + float area = vertices[verticeslength - 2] * vertices[1] - vertices[0] * vertices[verticeslength - 1], p1x, p1y, p2x, p2y; + for (int i = 0, n = verticeslength - 3; i < n; i += 2) { + p1x = vertices[i]; + p1y = vertices[i + 1]; + p2x = vertices[i + 2]; + p2y = vertices[i + 3]; + area += p1x * p2y - p2x * p1y; + } + if (area < 0) return; + + for (int i = 0, lastX = verticeslength - 2, n = verticeslength >> 1; i < n; i += 2) { + float x = vertices[i], y = vertices[i + 1]; + int other = lastX - i; + vertices[i] = vertices[other]; + vertices[i + 1] = vertices[other + 1]; + vertices[other] = x; + vertices[other + 1] = y; + } + } + } +} diff --git a/SpineRuntimes/SpineRuntime37/SkeletonData.cs b/SpineRuntimes/SpineRuntime37/SkeletonData.cs new file mode 100644 index 0000000..7a1f335 --- /dev/null +++ b/SpineRuntimes/SpineRuntime37/SkeletonData.cs @@ -0,0 +1,228 @@ +/****************************************************************************** + * Spine Runtimes License Agreement + * Last updated May 1, 2019. Replaces all prior versions. + * + * Copyright (c) 2013-2019, Esoteric Software LLC + * + * Integration of the Spine Runtimes into software or otherwise creating + * derivative works of the Spine Runtimes is permitted under the terms and + * conditions of Section 2 of the Spine Editor License Agreement: + * http://esotericsoftware.com/spine-editor-license + * + * Otherwise, it is permitted to integrate the Spine Runtimes into software + * or otherwise create derivative works of the Spine Runtimes (collectively, + * "Products"), provided that each user of the Products must obtain their own + * Spine Editor license and redistribution of the Products in any form must + * include this license and copyright notice. + * + * THIS SOFTWARE IS PROVIDED BY ESOTERIC SOFTWARE LLC "AS IS" AND ANY EXPRESS + * OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES + * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN + * NO EVENT SHALL ESOTERIC SOFTWARE LLC BE LIABLE FOR ANY DIRECT, INDIRECT, + * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, + * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES, BUSINESS + * INTERRUPTION, OR LOSS OF USE, DATA, OR PROFITS) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, + * EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + *****************************************************************************/ + +using System; + +namespace SpineRuntime37 { + + /// Stores the setup pose and all of the stateless data for a skeleton. + public class SkeletonData { + internal string name; + internal ExposedList bones = new ExposedList(); // Ordered parents first + internal ExposedList slots = new ExposedList(); // Setup pose draw order. + internal ExposedList skins = new ExposedList(); + internal Skin defaultSkin; + internal ExposedList events = new ExposedList(); + internal ExposedList animations = new ExposedList(); + internal ExposedList ikConstraints = new ExposedList(); + internal ExposedList transformConstraints = new ExposedList(); + internal ExposedList pathConstraints = new ExposedList(); + internal float width, height; + internal string version, hash; + + // Nonessential. + internal float fps; + internal string imagesPath, audioPath; + + public string Name { get { return name; } set { name = value; } } + + /// The skeleton's bones, sorted parent first. The root bone is always the first bone. + public ExposedList Bones { get { return bones; } } + + public ExposedList Slots { get { return slots; } } + + /// All skins, including the default skin. + public ExposedList Skins { get { return skins; } set { skins = value; } } + + /// + /// The skeleton's default skin. + /// By default this skin contains all attachments that were not in a skin in Spine. + /// + /// May be null. + public Skin DefaultSkin { get { return defaultSkin; } set { defaultSkin = value; } } + + public ExposedList Events { get { return events; } set { events = value; } } + public ExposedList Animations { get { return animations; } set { animations = value; } } + public ExposedList IkConstraints { get { return ikConstraints; } set { ikConstraints = value; } } + public ExposedList TransformConstraints { get { return transformConstraints; } set { transformConstraints = value; } } + public ExposedList PathConstraints { get { return pathConstraints; } set { pathConstraints = value; } } + + public float Width { get { return width; } set { width = value; } } + public float Height { get { return height; } set { height = value; } } + /// The Spine version used to export this data, or null. + public string Version { get { return version; } set { version = value; } } + public string Hash { get { return hash; } set { hash = value; } } + + /// The path to the images directory as defined in Spine. Available only when nonessential data was exported. May be null + public string ImagesPath { get { return imagesPath; } set { imagesPath = value; } } + + /// The path to the audio directory defined in Spine. Available only when nonessential data was exported. May be null. + public string AudioPath { get { return audioPath; } set { audioPath = value; } } + + /// + /// The dopesheet FPS in Spine. Available only when nonessential data was exported. + public float Fps { get { return fps; } set { fps = value; } } + + // --- Bones. + + /// + /// Finds a bone by comparing each bone's name. + /// It is more efficient to cache the results of this method than to call it multiple times. + /// May be null. + public BoneData FindBone (string boneName) { + if (boneName == null) throw new ArgumentNullException("boneName", "boneName cannot be null."); + var bones = this.bones; + var bonesItems = bones.Items; + for (int i = 0, n = bones.Count; i < n; i++) { + BoneData bone = bonesItems[i]; + if (bone.name == boneName) return bone; + } + return null; + } + + /// -1 if the bone was not found. + public int FindBoneIndex (string boneName) { + if (boneName == null) throw new ArgumentNullException("boneName", "boneName cannot be null."); + var bones = this.bones; + var bonesItems = bones.Items; + for (int i = 0, n = bones.Count; i < n; i++) + if (bonesItems[i].name == boneName) return i; + return -1; + } + + // --- Slots. + + /// May be null. + public SlotData FindSlot (string slotName) { + if (slotName == null) throw new ArgumentNullException("slotName", "slotName cannot be null."); + ExposedList slots = this.slots; + for (int i = 0, n = slots.Count; i < n; i++) { + SlotData slot = slots.Items[i]; + if (slot.name == slotName) return slot; + } + return null; + } + + /// -1 if the slot was not found. + public int FindSlotIndex (string slotName) { + if (slotName == null) throw new ArgumentNullException("slotName", "slotName cannot be null."); + ExposedList slots = this.slots; + for (int i = 0, n = slots.Count; i < n; i++) + if (slots.Items[i].name == slotName) return i; + return -1; + } + + // --- Skins. + + /// May be null. + public Skin FindSkin (string skinName) { + if (skinName == null) throw new ArgumentNullException("skinName", "skinName cannot be null."); + foreach (Skin skin in skins) + if (skin.name == skinName) return skin; + return null; + } + + // --- Events. + + /// May be null. + public EventData FindEvent (string eventDataName) { + if (eventDataName == null) throw new ArgumentNullException("eventDataName", "eventDataName cannot be null."); + foreach (EventData eventData in events) + if (eventData.name == eventDataName) return eventData; + return null; + } + + // --- Animations. + + /// May be null. + public Animation FindAnimation (string animationName) { + if (animationName == null) throw new ArgumentNullException("animationName", "animationName cannot be null."); + ExposedList animations = this.animations; + for (int i = 0, n = animations.Count; i < n; i++) { + Animation animation = animations.Items[i]; + if (animation.name == animationName) return animation; + } + return null; + } + + // --- IK constraints. + + /// May be null. + public IkConstraintData FindIkConstraint (string constraintName) { + if (constraintName == null) throw new ArgumentNullException("constraintName", "constraintName cannot be null."); + ExposedList ikConstraints = this.ikConstraints; + for (int i = 0, n = ikConstraints.Count; i < n; i++) { + IkConstraintData ikConstraint = ikConstraints.Items[i]; + if (ikConstraint.name == constraintName) return ikConstraint; + } + return null; + } + + // --- Transform constraints. + + /// May be null. + public TransformConstraintData FindTransformConstraint (string constraintName) { + if (constraintName == null) throw new ArgumentNullException("constraintName", "constraintName cannot be null."); + ExposedList transformConstraints = this.transformConstraints; + for (int i = 0, n = transformConstraints.Count; i < n; i++) { + TransformConstraintData transformConstraint = transformConstraints.Items[i]; + if (transformConstraint.name == constraintName) return transformConstraint; + } + return null; + } + + // --- Path constraints. + + /// May be null. + public PathConstraintData FindPathConstraint (string constraintName) { + if (constraintName == null) throw new ArgumentNullException("constraintName", "constraintName cannot be null."); + ExposedList pathConstraints = this.pathConstraints; + for (int i = 0, n = pathConstraints.Count; i < n; i++) { + PathConstraintData constraint = pathConstraints.Items[i]; + if (constraint.name.Equals(constraintName)) return constraint; + } + return null; + } + + /// -1 if the path constraint was not found. + public int FindPathConstraintIndex (string pathConstraintName) { + if (pathConstraintName == null) throw new ArgumentNullException("pathConstraintName", "pathConstraintName cannot be null."); + ExposedList pathConstraints = this.pathConstraints; + for (int i = 0, n = pathConstraints.Count; i < n; i++) + if (pathConstraints.Items[i].name.Equals(pathConstraintName)) return i; + return -1; + } + + // --- + + override public string ToString () { + return name ?? base.ToString(); + } + } +} diff --git a/SpineRuntimes/SpineRuntime37/SkeletonJson.cs b/SpineRuntimes/SpineRuntime37/SkeletonJson.cs new file mode 100644 index 0000000..a49aac3 --- /dev/null +++ b/SpineRuntimes/SpineRuntime37/SkeletonJson.cs @@ -0,0 +1,881 @@ +/****************************************************************************** + * Spine Runtimes License Agreement + * Last updated May 1, 2019. Replaces all prior versions. + * + * Copyright (c) 2013-2019, Esoteric Software LLC + * + * Integration of the Spine Runtimes into software or otherwise creating + * derivative works of the Spine Runtimes is permitted under the terms and + * conditions of Section 2 of the Spine Editor License Agreement: + * http://esotericsoftware.com/spine-editor-license + * + * Otherwise, it is permitted to integrate the Spine Runtimes into software + * or otherwise create derivative works of the Spine Runtimes (collectively, + * "Products"), provided that each user of the Products must obtain their own + * Spine Editor license and redistribution of the Products in any form must + * include this license and copyright notice. + * + * THIS SOFTWARE IS PROVIDED BY ESOTERIC SOFTWARE LLC "AS IS" AND ANY EXPRESS + * OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES + * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN + * NO EVENT SHALL ESOTERIC SOFTWARE LLC BE LIABLE FOR ANY DIRECT, INDIRECT, + * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, + * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES, BUSINESS + * INTERRUPTION, OR LOSS OF USE, DATA, OR PROFITS) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, + * EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + *****************************************************************************/ + +#if (UNITY_5 || UNITY_5_3_OR_NEWER || UNITY_WSA || UNITY_WP8 || UNITY_WP8_1) +#define IS_UNITY +#endif + +using System; +using System.IO; +using System.Collections.Generic; + +#if WINDOWS_STOREAPP +using System.Threading.Tasks; +using Windows.Storage; +#endif + +namespace SpineRuntime37 { + public class SkeletonJson { + public float Scale { get; set; } + + private AttachmentLoader attachmentLoader; + private List linkedMeshes = new List(); + + public SkeletonJson (params Atlas[] atlasArray) + : this(new AtlasAttachmentLoader(atlasArray)) { + } + + public SkeletonJson (AttachmentLoader attachmentLoader) { + if (attachmentLoader == null) throw new ArgumentNullException("attachmentLoader", "attachmentLoader cannot be null."); + this.attachmentLoader = attachmentLoader; + Scale = 1; + } + + #if !IS_UNITY && WINDOWS_STOREAPP + private async Task ReadFile(string path) { + var folder = Windows.ApplicationModel.Package.Current.InstalledLocation; + var file = await folder.GetFileAsync(path).AsTask().ConfigureAwait(false); + using (var reader = new StreamReader(await file.OpenStreamForReadAsync().ConfigureAwait(false))) { + SkeletonData skeletonData = ReadSkeletonData(reader); + skeletonData.Name = Path.GetFileNameWithoutExtension(path); + return skeletonData; + } + } + + public SkeletonData ReadSkeletonData (string path) { + return this.ReadFile(path).Result; + } + #else + public SkeletonData ReadSkeletonData (string path) { + #if WINDOWS_PHONE + using (var reader = new StreamReader(Microsoft.Xna.Framework.TitleContainer.OpenStream(path))) { + #else + using (var reader = new StreamReader(new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.Read))) { + #endif + SkeletonData skeletonData = ReadSkeletonData(reader); + skeletonData.name = Path.GetFileNameWithoutExtension(path); + return skeletonData; + } + } + #endif + + public SkeletonData ReadSkeletonData (TextReader reader) { + if (reader == null) throw new ArgumentNullException("reader", "reader cannot be null."); + + float scale = this.Scale; + var skeletonData = new SkeletonData(); + + var root = Json.Deserialize(reader) as Dictionary; + if (root == null) throw new Exception("Invalid JSON."); + + // Skeleton. + if (root.ContainsKey("skeleton")) { + var skeletonMap = (Dictionary)root["skeleton"]; + skeletonData.hash = (string)skeletonMap["hash"]; + skeletonData.version = (string)skeletonMap["spine"]; + skeletonData.width = GetFloat(skeletonMap, "width", 0); + skeletonData.height = GetFloat(skeletonMap, "height", 0); + skeletonData.fps = GetFloat(skeletonMap, "fps", 0); + skeletonData.imagesPath = GetString(skeletonMap, "images", null); + skeletonData.audioPath = GetString(skeletonMap, "audio", null); + } + + // Bones. + foreach (Dictionary boneMap in (List)root["bones"]) { + BoneData parent = null; + if (boneMap.ContainsKey("parent")) { + parent = skeletonData.FindBone((string)boneMap["parent"]); + if (parent == null) + throw new Exception("Parent bone not found: " + boneMap["parent"]); + } + var data = new BoneData(skeletonData.Bones.Count, (string)boneMap["name"], parent); + data.length = GetFloat(boneMap, "length", 0) * scale; + data.x = GetFloat(boneMap, "x", 0) * scale; + data.y = GetFloat(boneMap, "y", 0) * scale; + data.rotation = GetFloat(boneMap, "rotation", 0); + data.scaleX = GetFloat(boneMap, "scaleX", 1); + data.scaleY = GetFloat(boneMap, "scaleY", 1); + data.shearX = GetFloat(boneMap, "shearX", 0); + data.shearY = GetFloat(boneMap, "shearY", 0); + + string tm = GetString(boneMap, "transform", TransformMode.Normal.ToString()); + data.transformMode = (TransformMode)Enum.Parse(typeof(TransformMode), tm, true); + + skeletonData.bones.Add(data); + } + + // Slots. + if (root.ContainsKey("slots")) { + foreach (Dictionary slotMap in (List)root["slots"]) { + var slotName = (string)slotMap["name"]; + var boneName = (string)slotMap["bone"]; + BoneData boneData = skeletonData.FindBone(boneName); + if (boneData == null) throw new Exception("Slot bone not found: " + boneName); + var data = new SlotData(skeletonData.Slots.Count, slotName, boneData); + + if (slotMap.ContainsKey("color")) { + string color = (string)slotMap["color"]; + data.r = ToColor(color, 0); + data.g = ToColor(color, 1); + data.b = ToColor(color, 2); + data.a = ToColor(color, 3); + } + + if (slotMap.ContainsKey("dark")) { + var color2 = (string)slotMap["dark"]; + data.r2 = ToColor(color2, 0, 6); // expectedLength = 6. ie. "RRGGBB" + data.g2 = ToColor(color2, 1, 6); + data.b2 = ToColor(color2, 2, 6); + data.hasSecondColor = true; + } + + data.attachmentName = GetString(slotMap, "attachment", null); + if (slotMap.ContainsKey("blend")) + data.blendMode = (BlendMode)Enum.Parse(typeof(BlendMode), (string)slotMap["blend"], true); + else + data.blendMode = BlendMode.Normal; + skeletonData.slots.Add(data); + } + } + + // IK constraints. + if (root.ContainsKey("ik")) { + foreach (Dictionary constraintMap in (List)root["ik"]) { + IkConstraintData data = new IkConstraintData((string)constraintMap["name"]); + data.order = GetInt(constraintMap, "order", 0); + + foreach (string boneName in (List)constraintMap["bones"]) { + BoneData bone = skeletonData.FindBone(boneName); + if (bone == null) throw new Exception("IK constraint bone not found: " + boneName); + data.bones.Add(bone); + } + + string targetName = (string)constraintMap["target"]; + data.target = skeletonData.FindBone(targetName); + if (data.target == null) throw new Exception("Target bone not found: " + targetName); + data.mix = GetFloat(constraintMap, "mix", 1); + data.bendDirection = GetBoolean(constraintMap, "bendPositive", true) ? 1 : -1; + data.compress = GetBoolean(constraintMap, "compress", false); + data.stretch = GetBoolean(constraintMap, "stretch", false); + data.uniform = GetBoolean(constraintMap, "uniform", false); + + skeletonData.ikConstraints.Add(data); + } + } + + // Transform constraints. + if (root.ContainsKey("transform")) { + foreach (Dictionary constraintMap in (List)root["transform"]) { + TransformConstraintData data = new TransformConstraintData((string)constraintMap["name"]); + data.order = GetInt(constraintMap, "order", 0); + + foreach (string boneName in (List)constraintMap["bones"]) { + BoneData bone = skeletonData.FindBone(boneName); + if (bone == null) throw new Exception("Transform constraint bone not found: " + boneName); + data.bones.Add(bone); + } + + string targetName = (string)constraintMap["target"]; + data.target = skeletonData.FindBone(targetName); + if (data.target == null) throw new Exception("Target bone not found: " + targetName); + + data.local = GetBoolean(constraintMap, "local", false); + data.relative = GetBoolean(constraintMap, "relative", false); + + data.offsetRotation = GetFloat(constraintMap, "rotation", 0); + data.offsetX = GetFloat(constraintMap, "x", 0) * scale; + data.offsetY = GetFloat(constraintMap, "y", 0) * scale; + data.offsetScaleX = GetFloat(constraintMap, "scaleX", 0); + data.offsetScaleY = GetFloat(constraintMap, "scaleY", 0); + data.offsetShearY = GetFloat(constraintMap, "shearY", 0); + + data.rotateMix = GetFloat(constraintMap, "rotateMix", 1); + data.translateMix = GetFloat(constraintMap, "translateMix", 1); + data.scaleMix = GetFloat(constraintMap, "scaleMix", 1); + data.shearMix = GetFloat(constraintMap, "shearMix", 1); + + skeletonData.transformConstraints.Add(data); + } + } + + // Path constraints. + if(root.ContainsKey("path")) { + foreach (Dictionary constraintMap in (List)root["path"]) { + PathConstraintData data = new PathConstraintData((string)constraintMap["name"]); + data.order = GetInt(constraintMap, "order", 0); + + foreach (string boneName in (List)constraintMap["bones"]) { + BoneData bone = skeletonData.FindBone(boneName); + if (bone == null) throw new Exception("Path bone not found: " + boneName); + data.bones.Add(bone); + } + + string targetName = (string)constraintMap["target"]; + data.target = skeletonData.FindSlot(targetName); + if (data.target == null) throw new Exception("Target slot not found: " + targetName); + + data.positionMode = (PositionMode)Enum.Parse(typeof(PositionMode), GetString(constraintMap, "positionMode", "percent"), true); + data.spacingMode = (SpacingMode)Enum.Parse(typeof(SpacingMode), GetString(constraintMap, "spacingMode", "length"), true); + data.rotateMode = (RotateMode)Enum.Parse(typeof(RotateMode), GetString(constraintMap, "rotateMode", "tangent"), true); + data.offsetRotation = GetFloat(constraintMap, "rotation", 0); + data.position = GetFloat(constraintMap, "position", 0); + if (data.positionMode == PositionMode.Fixed) data.position *= scale; + data.spacing = GetFloat(constraintMap, "spacing", 0); + if (data.spacingMode == SpacingMode.Length || data.spacingMode == SpacingMode.Fixed) data.spacing *= scale; + data.rotateMix = GetFloat(constraintMap, "rotateMix", 1); + data.translateMix = GetFloat(constraintMap, "translateMix", 1); + + skeletonData.pathConstraints.Add(data); + } + } + + // Skins. + if (root.ContainsKey("skins")) { + foreach (KeyValuePair skinMap in (Dictionary)root["skins"]) { + var skin = new Skin(skinMap.Key); + foreach (KeyValuePair slotEntry in (Dictionary)skinMap.Value) { + int slotIndex = skeletonData.FindSlotIndex(slotEntry.Key); + foreach (KeyValuePair entry in ((Dictionary)slotEntry.Value)) { + try { + Attachment attachment = ReadAttachment((Dictionary)entry.Value, skin, slotIndex, entry.Key, skeletonData); + if (attachment != null) skin.AddAttachment(slotIndex, entry.Key, attachment); + } catch (Exception e) { + throw new Exception("Error reading attachment: " + entry.Key + ", skin: " + skin, e); + } + } + } + skeletonData.skins.Add(skin); + if (skin.name == "default") skeletonData.defaultSkin = skin; + } + } + + // Linked meshes. + for (int i = 0, n = linkedMeshes.Count; i < n; i++) { + LinkedMesh linkedMesh = linkedMeshes[i]; + Skin skin = linkedMesh.skin == null ? skeletonData.defaultSkin : skeletonData.FindSkin(linkedMesh.skin); + if (skin == null) throw new Exception("Slot not found: " + linkedMesh.skin); + Attachment parent = skin.GetAttachment(linkedMesh.slotIndex, linkedMesh.parent); + if (parent == null) throw new Exception("Parent mesh not found: " + linkedMesh.parent); + linkedMesh.mesh.ParentMesh = (MeshAttachment)parent; + linkedMesh.mesh.UpdateUVs(); + } + linkedMeshes.Clear(); + + // Events. + if (root.ContainsKey("events")) { + foreach (KeyValuePair entry in (Dictionary)root["events"]) { + var entryMap = (Dictionary)entry.Value; + var data = new EventData(entry.Key); + data.Int = GetInt(entryMap, "int", 0); + data.Float = GetFloat(entryMap, "float", 0); + data.String = GetString(entryMap, "string", string.Empty); + data.AudioPath = GetString(entryMap, "audio", null); + if (data.AudioPath != null) { + data.Volume = GetFloat(entryMap, "volume", 1); + data.Balance = GetFloat(entryMap, "balance", 0); + } + skeletonData.events.Add(data); + } + } + + // Animations. + if (root.ContainsKey("animations")) { + foreach (KeyValuePair entry in (Dictionary)root["animations"]) { + try { + ReadAnimation((Dictionary)entry.Value, entry.Key, skeletonData); + } catch (Exception e) { + throw new Exception("Error reading animation: " + entry.Key, e); + } + } + } + + skeletonData.bones.TrimExcess(); + skeletonData.slots.TrimExcess(); + skeletonData.skins.TrimExcess(); + skeletonData.events.TrimExcess(); + skeletonData.animations.TrimExcess(); + skeletonData.ikConstraints.TrimExcess(); + return skeletonData; + } + + private Attachment ReadAttachment (Dictionary map, Skin skin, int slotIndex, string name, SkeletonData skeletonData) { + float scale = this.Scale; + name = GetString(map, "name", name); + + var typeName = GetString(map, "type", "region"); + if (typeName == "skinnedmesh") typeName = "weightedmesh"; + if (typeName == "weightedmesh") typeName = "mesh"; + if (typeName == "weightedlinkedmesh") typeName = "linkedmesh"; + var type = (AttachmentType)Enum.Parse(typeof(AttachmentType), typeName, true); + + string path = GetString(map, "path", name); + + switch (type) { + case AttachmentType.Region: + RegionAttachment region = attachmentLoader.NewRegionAttachment(skin, name, path); + if (region == null) return null; + region.Path = path; + region.x = GetFloat(map, "x", 0) * scale; + region.y = GetFloat(map, "y", 0) * scale; + region.scaleX = GetFloat(map, "scaleX", 1); + region.scaleY = GetFloat(map, "scaleY", 1); + region.rotation = GetFloat(map, "rotation", 0); + region.width = GetFloat(map, "width", 32) * scale; + region.height = GetFloat(map, "height", 32) * scale; + + if (map.ContainsKey("color")) { + var color = (string)map["color"]; + region.r = ToColor(color, 0); + region.g = ToColor(color, 1); + region.b = ToColor(color, 2); + region.a = ToColor(color, 3); + } + + region.UpdateOffset(); + return region; + case AttachmentType.Boundingbox: + BoundingBoxAttachment box = attachmentLoader.NewBoundingBoxAttachment(skin, name); + if (box == null) return null; + ReadVertices(map, box, GetInt(map, "vertexCount", 0) << 1); + return box; + case AttachmentType.Mesh: + case AttachmentType.Linkedmesh: { + MeshAttachment mesh = attachmentLoader.NewMeshAttachment(skin, name, path); + if (mesh == null) return null; + mesh.Path = path; + + if (map.ContainsKey("color")) { + var color = (string)map["color"]; + mesh.r = ToColor(color, 0); + mesh.g = ToColor(color, 1); + mesh.b = ToColor(color, 2); + mesh.a = ToColor(color, 3); + } + + mesh.Width = GetFloat(map, "width", 0) * scale; + mesh.Height = GetFloat(map, "height", 0) * scale; + + string parent = GetString(map, "parent", null); + if (parent != null) { + mesh.InheritDeform = GetBoolean(map, "deform", true); + linkedMeshes.Add(new LinkedMesh(mesh, GetString(map, "skin", null), slotIndex, parent)); + return mesh; + } + + float[] uvs = GetFloatArray(map, "uvs", 1); + ReadVertices(map, mesh, uvs.Length); + mesh.triangles = GetIntArray(map, "triangles"); + mesh.regionUVs = uvs; + mesh.UpdateUVs(); + + if (map.ContainsKey("hull")) mesh.HullLength = GetInt(map, "hull", 0) * 2; + if (map.ContainsKey("edges")) mesh.Edges = GetIntArray(map, "edges"); + return mesh; + } + case AttachmentType.Path: { + PathAttachment pathAttachment = attachmentLoader.NewPathAttachment(skin, name); + if (pathAttachment == null) return null; + pathAttachment.closed = GetBoolean(map, "closed", false); + pathAttachment.constantSpeed = GetBoolean(map, "constantSpeed", true); + + int vertexCount = GetInt(map, "vertexCount", 0); + ReadVertices(map, pathAttachment, vertexCount << 1); + + // potential BOZO see Java impl + pathAttachment.lengths = GetFloatArray(map, "lengths", scale); + return pathAttachment; + } + case AttachmentType.Point: { + PointAttachment point = attachmentLoader.NewPointAttachment(skin, name); + if (point == null) return null; + point.x = GetFloat(map, "x", 0) * scale; + point.y = GetFloat(map, "y", 0) * scale; + point.rotation = GetFloat(map, "rotation", 0); + + //string color = GetString(map, "color", null); + //if (color != null) point.color = color; + return point; + } + case AttachmentType.Clipping: { + ClippingAttachment clip = attachmentLoader.NewClippingAttachment(skin, name); + if (clip == null) return null; + + string end = GetString(map, "end", null); + if (end != null) { + SlotData slot = skeletonData.FindSlot(end); + if (slot == null) throw new Exception("Clipping end slot not found: " + end); + clip.EndSlot = slot; + } + + ReadVertices(map, clip, GetInt(map, "vertexCount", 0) << 1); + + //string color = GetString(map, "color", null); + // if (color != null) clip.color = color; + return clip; + } + } + return null; + } + + private void ReadVertices (Dictionary map, VertexAttachment attachment, int verticesLength) { + attachment.WorldVerticesLength = verticesLength; + float[] vertices = GetFloatArray(map, "vertices", 1); + float scale = Scale; + if (verticesLength == vertices.Length) { + if (scale != 1) { + for (int i = 0; i < vertices.Length; i++) { + vertices[i] *= scale; + } + } + attachment.vertices = vertices; + return; + } + ExposedList weights = new ExposedList(verticesLength * 3 * 3); + ExposedList bones = new ExposedList(verticesLength * 3); + for (int i = 0, n = vertices.Length; i < n;) { + int boneCount = (int)vertices[i++]; + bones.Add(boneCount); + for (int nn = i + boneCount * 4; i < nn; i += 4) { + bones.Add((int)vertices[i]); + weights.Add(vertices[i + 1] * this.Scale); + weights.Add(vertices[i + 2] * this.Scale); + weights.Add(vertices[i + 3]); + } + } + attachment.bones = bones.ToArray(); + attachment.vertices = weights.ToArray(); + } + + private void ReadAnimation (Dictionary map, string name, SkeletonData skeletonData) { + var scale = this.Scale; + var timelines = new ExposedList(); + float duration = 0; + + // Slot timelines. + if (map.ContainsKey("slots")) { + foreach (KeyValuePair entry in (Dictionary)map["slots"]) { + string slotName = entry.Key; + int slotIndex = skeletonData.FindSlotIndex(slotName); + var timelineMap = (Dictionary)entry.Value; + foreach (KeyValuePair timelineEntry in timelineMap) { + var values = (List)timelineEntry.Value; + var timelineName = (string)timelineEntry.Key; + if (timelineName == "attachment") { + var timeline = new AttachmentTimeline(values.Count); + timeline.slotIndex = slotIndex; + + int frameIndex = 0; + foreach (Dictionary valueMap in values) { + float time = (float)valueMap["time"]; + timeline.SetFrame(frameIndex++, time, (string)valueMap["name"]); + } + timelines.Add(timeline); + duration = Math.Max(duration, timeline.frames[timeline.FrameCount - 1]); + + } else if (timelineName == "color") { + var timeline = new ColorTimeline(values.Count); + timeline.slotIndex = slotIndex; + + int frameIndex = 0; + foreach (Dictionary valueMap in values) { + float time = (float)valueMap["time"]; + string c = (string)valueMap["color"]; + timeline.SetFrame(frameIndex, time, ToColor(c, 0), ToColor(c, 1), ToColor(c, 2), ToColor(c, 3)); + ReadCurve(valueMap, timeline, frameIndex); + frameIndex++; + } + timelines.Add(timeline); + duration = Math.Max(duration, timeline.frames[(timeline.FrameCount - 1) * ColorTimeline.ENTRIES]); + + } else if (timelineName == "twoColor") { + var timeline = new TwoColorTimeline(values.Count); + timeline.slotIndex = slotIndex; + + int frameIndex = 0; + foreach (Dictionary valueMap in values) { + float time = (float)valueMap["time"]; + string light = (string)valueMap["light"]; + string dark = (string)valueMap["dark"]; + timeline.SetFrame(frameIndex, time, ToColor(light, 0), ToColor(light, 1), ToColor(light, 2), ToColor(light, 3), + ToColor(dark, 0, 6), ToColor(dark, 1, 6), ToColor(dark, 2, 6)); + ReadCurve(valueMap, timeline, frameIndex); + frameIndex++; + } + timelines.Add(timeline); + duration = Math.Max(duration, timeline.frames[(timeline.FrameCount - 1) * TwoColorTimeline.ENTRIES]); + + } else + throw new Exception("Invalid timeline type for a slot: " + timelineName + " (" + slotName + ")"); + } + } + } + + // Bone timelines. + if (map.ContainsKey("bones")) { + foreach (KeyValuePair entry in (Dictionary)map["bones"]) { + string boneName = entry.Key; + int boneIndex = skeletonData.FindBoneIndex(boneName); + if (boneIndex == -1) throw new Exception("Bone not found: " + boneName); + var timelineMap = (Dictionary)entry.Value; + foreach (KeyValuePair timelineEntry in timelineMap) { + var values = (List)timelineEntry.Value; + var timelineName = (string)timelineEntry.Key; + if (timelineName == "rotate") { + var timeline = new RotateTimeline(values.Count); + timeline.boneIndex = boneIndex; + + int frameIndex = 0; + foreach (Dictionary valueMap in values) { + timeline.SetFrame(frameIndex, (float)valueMap["time"], (float)valueMap["angle"]); + ReadCurve(valueMap, timeline, frameIndex); + frameIndex++; + } + timelines.Add(timeline); + duration = Math.Max(duration, timeline.frames[(timeline.FrameCount - 1) * RotateTimeline.ENTRIES]); + + } else if (timelineName == "translate" || timelineName == "scale" || timelineName == "shear") { + TranslateTimeline timeline; + float timelineScale = 1; + if (timelineName == "scale") + timeline = new ScaleTimeline(values.Count); + else if (timelineName == "shear") + timeline = new ShearTimeline(values.Count); + else { + timeline = new TranslateTimeline(values.Count); + timelineScale = scale; + } + timeline.boneIndex = boneIndex; + + int frameIndex = 0; + foreach (Dictionary valueMap in values) { + float time = (float)valueMap["time"]; + float x = GetFloat(valueMap, "x", 0); + float y = GetFloat(valueMap, "y", 0); + timeline.SetFrame(frameIndex, time, x * timelineScale, y * timelineScale); + ReadCurve(valueMap, timeline, frameIndex); + frameIndex++; + } + timelines.Add(timeline); + duration = Math.Max(duration, timeline.frames[(timeline.FrameCount - 1) * TranslateTimeline.ENTRIES]); + + } else + throw new Exception("Invalid timeline type for a bone: " + timelineName + " (" + boneName + ")"); + } + } + } + + // IK constraint timelines. + if (map.ContainsKey("ik")) { + foreach (KeyValuePair constraintMap in (Dictionary)map["ik"]) { + IkConstraintData constraint = skeletonData.FindIkConstraint(constraintMap.Key); + var values = (List)constraintMap.Value; + var timeline = new IkConstraintTimeline(values.Count); + timeline.ikConstraintIndex = skeletonData.ikConstraints.IndexOf(constraint); + int frameIndex = 0; + foreach (Dictionary valueMap in values) { + timeline.SetFrame( + frameIndex, + (float)valueMap["time"], + GetFloat(valueMap, "mix", 1), + GetBoolean(valueMap, "bendPositive", true) ? 1 : -1, + GetBoolean(valueMap, "compress", true), + GetBoolean(valueMap, "stretch", false) + ); + ReadCurve(valueMap, timeline, frameIndex); + frameIndex++; + } + timelines.Add(timeline); + duration = Math.Max(duration, timeline.frames[(timeline.FrameCount - 1) * IkConstraintTimeline.ENTRIES]); + } + } + + // Transform constraint timelines. + if (map.ContainsKey("transform")) { + foreach (KeyValuePair constraintMap in (Dictionary)map["transform"]) { + TransformConstraintData constraint = skeletonData.FindTransformConstraint(constraintMap.Key); + var values = (List)constraintMap.Value; + var timeline = new TransformConstraintTimeline(values.Count); + timeline.transformConstraintIndex = skeletonData.transformConstraints.IndexOf(constraint); + int frameIndex = 0; + foreach (Dictionary valueMap in values) { + float time = (float)valueMap["time"]; + float rotateMix = GetFloat(valueMap, "rotateMix", 1); + float translateMix = GetFloat(valueMap, "translateMix", 1); + float scaleMix = GetFloat(valueMap, "scaleMix", 1); + float shearMix = GetFloat(valueMap, "shearMix", 1); + timeline.SetFrame(frameIndex, time, rotateMix, translateMix, scaleMix, shearMix); + ReadCurve(valueMap, timeline, frameIndex); + frameIndex++; + } + timelines.Add(timeline); + duration = Math.Max(duration, timeline.frames[(timeline.FrameCount - 1) * TransformConstraintTimeline.ENTRIES]); + } + } + + // Path constraint timelines. + if (map.ContainsKey("paths")) { + foreach (KeyValuePair constraintMap in (Dictionary)map["paths"]) { + int index = skeletonData.FindPathConstraintIndex(constraintMap.Key); + if (index == -1) throw new Exception("Path constraint not found: " + constraintMap.Key); + PathConstraintData data = skeletonData.pathConstraints.Items[index]; + var timelineMap = (Dictionary)constraintMap.Value; + foreach (KeyValuePair timelineEntry in timelineMap) { + var values = (List)timelineEntry.Value; + var timelineName = (string)timelineEntry.Key; + if (timelineName == "position" || timelineName == "spacing") { + PathConstraintPositionTimeline timeline; + float timelineScale = 1; + if (timelineName == "spacing") { + timeline = new PathConstraintSpacingTimeline(values.Count); + if (data.spacingMode == SpacingMode.Length || data.spacingMode == SpacingMode.Fixed) timelineScale = scale; + } + else { + timeline = new PathConstraintPositionTimeline(values.Count); + if (data.positionMode == PositionMode.Fixed) timelineScale = scale; + } + timeline.pathConstraintIndex = index; + int frameIndex = 0; + foreach (Dictionary valueMap in values) { + timeline.SetFrame(frameIndex, (float)valueMap["time"], GetFloat(valueMap, timelineName, 0) * timelineScale); + ReadCurve(valueMap, timeline, frameIndex); + frameIndex++; + } + timelines.Add(timeline); + duration = Math.Max(duration, timeline.frames[(timeline.FrameCount - 1) * PathConstraintPositionTimeline.ENTRIES]); + } + else if (timelineName == "mix") { + PathConstraintMixTimeline timeline = new PathConstraintMixTimeline(values.Count); + timeline.pathConstraintIndex = index; + int frameIndex = 0; + foreach (Dictionary valueMap in values) { + timeline.SetFrame(frameIndex, (float)valueMap["time"], GetFloat(valueMap, "rotateMix", 1), GetFloat(valueMap, "translateMix", 1)); + ReadCurve(valueMap, timeline, frameIndex); + frameIndex++; + } + timelines.Add(timeline); + duration = Math.Max(duration, timeline.frames[(timeline.FrameCount - 1) * PathConstraintMixTimeline.ENTRIES]); + } + } + } + } + + // Deform timelines. + if (map.ContainsKey("deform")) { + foreach (KeyValuePair deformMap in (Dictionary)map["deform"]) { + Skin skin = skeletonData.FindSkin(deformMap.Key); + foreach (KeyValuePair slotMap in (Dictionary)deformMap.Value) { + int slotIndex = skeletonData.FindSlotIndex(slotMap.Key); + if (slotIndex == -1) throw new Exception("Slot not found: " + slotMap.Key); + foreach (KeyValuePair timelineMap in (Dictionary)slotMap.Value) { + var values = (List)timelineMap.Value; + VertexAttachment attachment = (VertexAttachment)skin.GetAttachment(slotIndex, timelineMap.Key); + if (attachment == null) throw new Exception("Deform attachment not found: " + timelineMap.Key); + bool weighted = attachment.bones != null; + float[] vertices = attachment.vertices; + int deformLength = weighted ? vertices.Length / 3 * 2 : vertices.Length; + + var timeline = new DeformTimeline(values.Count); + timeline.slotIndex = slotIndex; + timeline.attachment = attachment; + + int frameIndex = 0; + foreach (Dictionary valueMap in values) { + float[] deform; + if (!valueMap.ContainsKey("vertices")) { + deform = weighted ? new float[deformLength] : vertices; + } else { + deform = new float[deformLength]; + int start = GetInt(valueMap, "offset", 0); + float[] verticesValue = GetFloatArray(valueMap, "vertices", 1); + Array.Copy(verticesValue, 0, deform, start, verticesValue.Length); + if (scale != 1) { + for (int i = start, n = i + verticesValue.Length; i < n; i++) + deform[i] *= scale; + } + + if (!weighted) { + for (int i = 0; i < deformLength; i++) + deform[i] += vertices[i]; + } + } + + timeline.SetFrame(frameIndex, (float)valueMap["time"], deform); + ReadCurve(valueMap, timeline, frameIndex); + frameIndex++; + } + timelines.Add(timeline); + duration = Math.Max(duration, timeline.frames[timeline.FrameCount - 1]); + } + } + } + } + + // Draw order timeline. + if (map.ContainsKey("drawOrder") || map.ContainsKey("draworder")) { + var values = (List)map[map.ContainsKey("drawOrder") ? "drawOrder" : "draworder"]; + var timeline = new DrawOrderTimeline(values.Count); + int slotCount = skeletonData.slots.Count; + int frameIndex = 0; + foreach (Dictionary drawOrderMap in values) { + int[] drawOrder = null; + if (drawOrderMap.ContainsKey("offsets")) { + drawOrder = new int[slotCount]; + for (int i = slotCount - 1; i >= 0; i--) + drawOrder[i] = -1; + var offsets = (List)drawOrderMap["offsets"]; + int[] unchanged = new int[slotCount - offsets.Count]; + int originalIndex = 0, unchangedIndex = 0; + foreach (Dictionary offsetMap in offsets) { + int slotIndex = skeletonData.FindSlotIndex((string)offsetMap["slot"]); + if (slotIndex == -1) throw new Exception("Slot not found: " + offsetMap["slot"]); + // Collect unchanged items. + while (originalIndex != slotIndex) + unchanged[unchangedIndex++] = originalIndex++; + // Set changed items. + int index = originalIndex + (int)(float)offsetMap["offset"]; + drawOrder[index] = originalIndex++; + } + // Collect remaining unchanged items. + while (originalIndex < slotCount) + unchanged[unchangedIndex++] = originalIndex++; + // Fill in unchanged items. + for (int i = slotCount - 1; i >= 0; i--) + if (drawOrder[i] == -1) drawOrder[i] = unchanged[--unchangedIndex]; + } + timeline.SetFrame(frameIndex++, (float)drawOrderMap["time"], drawOrder); + } + timelines.Add(timeline); + duration = Math.Max(duration, timeline.frames[timeline.FrameCount - 1]); + } + + // Event timeline. + if (map.ContainsKey("events")) { + var eventsMap = (List)map["events"]; + var timeline = new EventTimeline(eventsMap.Count); + int frameIndex = 0; + foreach (Dictionary eventMap in eventsMap) { + EventData eventData = skeletonData.FindEvent((string)eventMap["name"]); + if (eventData == null) throw new Exception("Event not found: " + eventMap["name"]); + var e = new Event((float)eventMap["time"], eventData) { + intValue = GetInt(eventMap, "int", eventData.Int), + floatValue = GetFloat(eventMap, "float", eventData.Float), + stringValue = GetString(eventMap, "string", eventData.String) + }; + if (e.data.AudioPath != null) { + e.volume = GetFloat(eventMap, "volume", eventData.Volume); + e.balance = GetFloat(eventMap, "balance", eventData.Balance); + } + timeline.SetFrame(frameIndex++, e); + } + timelines.Add(timeline); + duration = Math.Max(duration, timeline.frames[timeline.FrameCount - 1]); + } + + timelines.TrimExcess(); + skeletonData.animations.Add(new Animation(name, timelines, duration)); + } + + static void ReadCurve (Dictionary valueMap, CurveTimeline timeline, int frameIndex) { + if (!valueMap.ContainsKey("curve")) + return; + Object curveObject = valueMap["curve"]; + if (curveObject.Equals("stepped")) + timeline.SetStepped(frameIndex); + else { + var curve = curveObject as List; + if (curve != null) + timeline.SetCurve(frameIndex, (float)curve[0], (float)curve[1], (float)curve[2], (float)curve[3]); + } + } + + internal class LinkedMesh { + internal string parent, skin; + internal int slotIndex; + internal MeshAttachment mesh; + + public LinkedMesh (MeshAttachment mesh, string skin, int slotIndex, string parent) { + this.mesh = mesh; + this.skin = skin; + this.slotIndex = slotIndex; + this.parent = parent; + } + } + + static float[] GetFloatArray(Dictionary map, string name, float scale) { + var list = (List)map[name]; + var values = new float[list.Count]; + if (scale == 1) { + for (int i = 0, n = list.Count; i < n; i++) + values[i] = (float)list[i]; + } else { + for (int i = 0, n = list.Count; i < n; i++) + values[i] = (float)list[i] * scale; + } + return values; + } + + static int[] GetIntArray(Dictionary map, string name) { + var list = (List)map[name]; + var values = new int[list.Count]; + for (int i = 0, n = list.Count; i < n; i++) + values[i] = (int)(float)list[i]; + return values; + } + + static float GetFloat(Dictionary map, string name, float defaultValue) { + if (!map.ContainsKey(name)) + return defaultValue; + return (float)map[name]; + } + + static int GetInt(Dictionary map, string name, int defaultValue) { + if (!map.ContainsKey(name)) + return defaultValue; + return (int)(float)map[name]; + } + + static bool GetBoolean(Dictionary map, string name, bool defaultValue) { + if (!map.ContainsKey(name)) + return defaultValue; + return (bool)map[name]; + } + + static string GetString(Dictionary map, string name, string defaultValue) { + if (!map.ContainsKey(name)) + return defaultValue; + return (string)map[name]; + } + + static float ToColor(string hexString, int colorIndex, int expectedLength = 8) { + if (hexString.Length != expectedLength) + throw new ArgumentException("Color hexidecimal length must be " + expectedLength + ", recieved: " + hexString, "hexString"); + return Convert.ToInt32(hexString.Substring(colorIndex * 2, 2), 16) / (float)255; + } + } +} diff --git a/SpineRuntimes/SpineRuntime37/Skin.cs b/SpineRuntimes/SpineRuntime37/Skin.cs new file mode 100644 index 0000000..49370cf --- /dev/null +++ b/SpineRuntimes/SpineRuntime37/Skin.cs @@ -0,0 +1,130 @@ +/****************************************************************************** + * Spine Runtimes License Agreement + * Last updated May 1, 2019. Replaces all prior versions. + * + * Copyright (c) 2013-2019, Esoteric Software LLC + * + * Integration of the Spine Runtimes into software or otherwise creating + * derivative works of the Spine Runtimes is permitted under the terms and + * conditions of Section 2 of the Spine Editor License Agreement: + * http://esotericsoftware.com/spine-editor-license + * + * Otherwise, it is permitted to integrate the Spine Runtimes into software + * or otherwise create derivative works of the Spine Runtimes (collectively, + * "Products"), provided that each user of the Products must obtain their own + * Spine Editor license and redistribution of the Products in any form must + * include this license and copyright notice. + * + * THIS SOFTWARE IS PROVIDED BY ESOTERIC SOFTWARE LLC "AS IS" AND ANY EXPRESS + * OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES + * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN + * NO EVENT SHALL ESOTERIC SOFTWARE LLC BE LIABLE FOR ANY DIRECT, INDIRECT, + * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, + * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES, BUSINESS + * INTERRUPTION, OR LOSS OF USE, DATA, OR PROFITS) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, + * EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + *****************************************************************************/ + +using System; +using System.Collections.Generic; + +namespace SpineRuntime37 { + /// Stores attachments by slot index and attachment name. + /// See SkeletonData , Skeleton , and + /// Runtime skins in the Spine Runtimes Guide. + /// + public class Skin { + internal string name; + private Dictionary attachments = + new Dictionary(AttachmentKeyTupleComparer.Instance); + + public string Name { get { return name; } } + public Dictionary Attachments { get { return attachments; } } + + public Skin (string name) { + if (name == null) throw new ArgumentNullException("name", "name cannot be null."); + this.name = name; + } + + /// Adds an attachment to the skin for the specified slot index and name. If the name already exists for the slot, the previous value is replaced. + public void AddAttachment (int slotIndex, string name, Attachment attachment) { + if (attachment == null) throw new ArgumentNullException("attachment", "attachment cannot be null."); + attachments[new AttachmentKeyTuple(slotIndex, name)] = attachment; + } + + /// Returns the attachment for the specified slot index and name, or null. + /// May be null. + public Attachment GetAttachment (int slotIndex, string name) { + Attachment attachment; + attachments.TryGetValue(new AttachmentKeyTuple(slotIndex, name), out attachment); + return attachment; + } + + /// Removes the attachment in the skin for the specified slot index and name, if any. + public void RemoveAttachment (int slotIndex, string name) { + if (slotIndex < 0) throw new ArgumentOutOfRangeException("slotIndex", "slotIndex must be >= 0"); + attachments.Remove(new AttachmentKeyTuple(slotIndex, name)); + } + + /// Finds the skin keys for a given slot. The results are added to the passed List(names). + /// The target slotIndex. To find the slot index, use or + /// Found skin key names will be added to this list. + public void FindNamesForSlot (int slotIndex, List names) { + if (names == null) throw new ArgumentNullException("names", "names cannot be null."); + foreach (AttachmentKeyTuple key in attachments.Keys) + if (key.slotIndex == slotIndex) names.Add(key.name); + } + + /// Finds the attachments for a given slot. The results are added to the passed List(Attachment). + /// The target slotIndex. To find the slot index, use or + /// Found Attachments will be added to this list. + public void FindAttachmentsForSlot (int slotIndex, List attachments) { + if (attachments == null) throw new ArgumentNullException("attachments", "attachments cannot be null."); + foreach (KeyValuePair entry in this.attachments) + if (entry.Key.slotIndex == slotIndex) attachments.Add(entry.Value); + } + + override public string ToString () { + return name; + } + + /// Attach all attachments from this skin if the corresponding attachment from the old skin is currently attached. + internal void AttachAll (Skeleton skeleton, Skin oldSkin) { + foreach (KeyValuePair entry in oldSkin.attachments) { + int slotIndex = entry.Key.slotIndex; + Slot slot = skeleton.slots.Items[slotIndex]; + if (slot.Attachment == entry.Value) { + Attachment attachment = GetAttachment(slotIndex, entry.Key.name); + if (attachment != null) slot.Attachment = attachment; + } + } + } + + public struct AttachmentKeyTuple { + public readonly int slotIndex; + public readonly string name; + internal readonly int nameHashCode; + + public AttachmentKeyTuple (int slotIndex, string name) { + this.slotIndex = slotIndex; + this.name = name; + nameHashCode = this.name.GetHashCode(); + } + } + + // Avoids boxing in the dictionary. + class AttachmentKeyTupleComparer : IEqualityComparer { + internal static readonly AttachmentKeyTupleComparer Instance = new AttachmentKeyTupleComparer(); + + bool IEqualityComparer.Equals (AttachmentKeyTuple o1, AttachmentKeyTuple o2) { + return o1.slotIndex == o2.slotIndex && o1.nameHashCode == o2.nameHashCode && string.Equals(o1.name, o2.name, StringComparison.Ordinal); + } + + int IEqualityComparer.GetHashCode (AttachmentKeyTuple o) { + return o.slotIndex; + } + } + } +} diff --git a/SpineRuntimes/SpineRuntime37/Slot.cs b/SpineRuntimes/SpineRuntime37/Slot.cs new file mode 100644 index 0000000..95244cf --- /dev/null +++ b/SpineRuntimes/SpineRuntime37/Slot.cs @@ -0,0 +1,181 @@ +/****************************************************************************** + * Spine Runtimes License Agreement + * Last updated May 1, 2019. Replaces all prior versions. + * + * Copyright (c) 2013-2019, Esoteric Software LLC + * + * Integration of the Spine Runtimes into software or otherwise creating + * derivative works of the Spine Runtimes is permitted under the terms and + * conditions of Section 2 of the Spine Editor License Agreement: + * http://esotericsoftware.com/spine-editor-license + * + * Otherwise, it is permitted to integrate the Spine Runtimes into software + * or otherwise create derivative works of the Spine Runtimes (collectively, + * "Products"), provided that each user of the Products must obtain their own + * Spine Editor license and redistribution of the Products in any form must + * include this license and copyright notice. + * + * THIS SOFTWARE IS PROVIDED BY ESOTERIC SOFTWARE LLC "AS IS" AND ANY EXPRESS + * OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES + * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN + * NO EVENT SHALL ESOTERIC SOFTWARE LLC BE LIABLE FOR ANY DIRECT, INDIRECT, + * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, + * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES, BUSINESS + * INTERRUPTION, OR LOSS OF USE, DATA, OR PROFITS) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, + * EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + *****************************************************************************/ + +using System; + +namespace SpineRuntime37 { + + /// + /// Stores a slot's current pose. Slots organize attachments for {@link Skeleton#drawOrder} purposes and provide a place to store + /// state for an attachment.State cannot be stored in an attachment itself because attachments are stateless and may be shared + /// across multiple skeletons. + /// + public class Slot { + internal SlotData data; + internal Bone bone; + internal float r, g, b, a; + internal float r2, g2, b2; + internal bool hasSecondColor; + internal Attachment attachment; + internal float attachmentTime; + internal ExposedList attachmentVertices = new ExposedList(); + + public Slot (SlotData data, Bone bone) { + if (data == null) throw new ArgumentNullException("data", "data cannot be null."); + if (bone == null) throw new ArgumentNullException("bone", "bone cannot be null."); + this.data = data; + this.bone = bone; + + // darkColor = data.darkColor == null ? null : new Color(); + if (data.hasSecondColor) { + r2 = g2 = b2 = 0; + } + + SetToSetupPose(); + } + + /// Copy constructor. + public Slot(Slot slot, Bone bone) { + if (slot == null) throw new ArgumentNullException("slot", "slot cannot be null."); + if (bone == null) throw new ArgumentNullException("bone", "bone cannot be null."); + data = slot.data; + this.bone = bone; + r = slot.r; + g = slot.g; + b = slot.b; + a = slot.a; + + // darkColor = slot.darkColor == null ? null : new Color(slot.darkColor); + if (slot.hasSecondColor) { + r2 = slot.r2; + g2 = slot.g2; + b2 = slot.b2; + } else { + r2 = g2 = b2 = 0; + } + hasSecondColor = slot.hasSecondColor; + + attachment = slot.attachment; + attachmentTime = slot.attachmentTime; + } + + /// The slot's setup pose data. + public SlotData Data { get { return data; } } + /// The bone this slot belongs to. + public Bone Bone { get { return bone; } } + /// The skeleton this slot belongs to. + public Skeleton Skeleton { get { return bone.skeleton; } } + /// The color used to tint the slot's attachment. If is set, this is used as the light color for two + /// color tinting. + public float R { get { return r; } set { r = value; } } + /// The color used to tint the slot's attachment. If is set, this is used as the light color for two + /// color tinting. + public float G { get { return g; } set { g = value; } } + /// The color used to tint the slot's attachment. If is set, this is used as the light color for two + /// color tinting. + public float B { get { return b; } set { b = value; } } + /// The color used to tint the slot's attachment. If is set, this is used as the light color for two + /// color tinting. + public float A { get { return a; } set { a = value; } } + + /// The dark color used to tint the slot's attachment for two color tinting, ignored if two color tinting is not used. + /// + public float R2 { get { return r2; } set { r2 = value; } } + /// The dark color used to tint the slot's attachment for two color tinting, ignored if two color tinting is not used. + /// + public float G2 { get { return g2; } set { g2 = value; } } + /// The dark color used to tint the slot's attachment for two color tinting, ignored if two color tinting is not used. + /// + public float B2 { get { return b2; } set { b2 = value; } } + /// Whether R2 G2 B2 are used to tint the slot's attachment for two color tinting. False if two color tinting is not used. + public bool HasSecondColor { get { return data.hasSecondColor; } set { data.hasSecondColor = value; } } + + public Attachment Attachment { + /// The current attachment for the slot, or null if the slot has no attachment. + get { return attachment; } + /// + /// Sets the slot's attachment and, if the attachment changed, resets and clears + /// . + /// May be null. + set { + if (attachment == value) return; + attachment = value; + attachmentTime = bone.skeleton.time; + attachmentVertices.Clear(false); + } + } + + /// The time that has elapsed since the last time the attachment was set or cleared. Relies on Skeleton + /// + public float AttachmentTime { + get { return bone.skeleton.time - attachmentTime; } + set { attachmentTime = bone.skeleton.time - value; } + } + + /// Vertices to deform the slot's attachment. For an unweighted mesh, the entries are local positions for each vertex. For a + /// weighted mesh, the entries are an offset for each vertex which will be added to the mesh's local vertex positions. + /// + /// See and . + public ExposedList AttachmentVertices { + get { + return attachmentVertices; + } + set { + if (attachmentVertices == null) throw new ArgumentNullException("attachmentVertices", "attachmentVertices cannot be null."); + attachmentVertices = value; + } + } + + /// Sets this slot to the setup pose. + public void SetToSetupPose () { + r = data.r; + g = data.g; + b = data.b; + a = data.a; + + // if (darkColor != null) darkColor.set(data.darkColor); + if (HasSecondColor) { + r2 = data.r2; + g2 = data.g2; + b2 = data.b2; + } + + if (data.attachmentName == null) + Attachment = null; + else { + attachment = null; + Attachment = bone.skeleton.GetAttachment(data.index, data.attachmentName); + } + } + + override public string ToString () { + return data.name; + } + } +} diff --git a/SpineRuntimes/SpineRuntime37/SlotData.cs b/SpineRuntimes/SpineRuntime37/SlotData.cs new file mode 100644 index 0000000..b93ece9 --- /dev/null +++ b/SpineRuntimes/SpineRuntime37/SlotData.cs @@ -0,0 +1,73 @@ +/****************************************************************************** + * Spine Runtimes License Agreement + * Last updated May 1, 2019. Replaces all prior versions. + * + * Copyright (c) 2013-2019, Esoteric Software LLC + * + * Integration of the Spine Runtimes into software or otherwise creating + * derivative works of the Spine Runtimes is permitted under the terms and + * conditions of Section 2 of the Spine Editor License Agreement: + * http://esotericsoftware.com/spine-editor-license + * + * Otherwise, it is permitted to integrate the Spine Runtimes into software + * or otherwise create derivative works of the Spine Runtimes (collectively, + * "Products"), provided that each user of the Products must obtain their own + * Spine Editor license and redistribution of the Products in any form must + * include this license and copyright notice. + * + * THIS SOFTWARE IS PROVIDED BY ESOTERIC SOFTWARE LLC "AS IS" AND ANY EXPRESS + * OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES + * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN + * NO EVENT SHALL ESOTERIC SOFTWARE LLC BE LIABLE FOR ANY DIRECT, INDIRECT, + * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, + * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES, BUSINESS + * INTERRUPTION, OR LOSS OF USE, DATA, OR PROFITS) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, + * EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + *****************************************************************************/ + +using System; + +namespace SpineRuntime37 { + public class SlotData { + internal int index; + internal string name; + internal BoneData boneData; + internal float r = 1, g = 1, b = 1, a = 1; + internal float r2 = 0, g2 = 0, b2 = 0; + internal bool hasSecondColor = false; + internal string attachmentName; + internal BlendMode blendMode; + + public int Index { get { return index; } } + public string Name { get { return name; } } + public BoneData BoneData { get { return boneData; } } + public float R { get { return r; } set { r = value; } } + public float G { get { return g; } set { g = value; } } + public float B { get { return b; } set { b = value; } } + public float A { get { return a; } set { a = value; } } + + public float R2 { get { return r2; } set { r2 = value; } } + public float G2 { get { return g2; } set { g2 = value; } } + public float B2 { get { return b2; } set { b2 = value; } } + public bool HasSecondColor { get { return hasSecondColor; } set { hasSecondColor = value; } } + + /// May be null. + public String AttachmentName { get { return attachmentName; } set { attachmentName = value; } } + public BlendMode BlendMode { get { return blendMode; } set { blendMode = value; } } + + public SlotData (int index, String name, BoneData boneData) { + if (index < 0) throw new ArgumentException ("index must be >= 0.", "index"); + if (name == null) throw new ArgumentNullException("name", "name cannot be null."); + if (boneData == null) throw new ArgumentNullException("boneData", "boneData cannot be null."); + this.index = index; + this.name = name; + this.boneData = boneData; + } + + override public string ToString () { + return name; + } + } +} diff --git a/SpineRuntimes/SpineRuntime37/SpineRuntime37.csproj b/SpineRuntimes/SpineRuntime37/SpineRuntime37.csproj new file mode 100644 index 0000000..f7fc3ba --- /dev/null +++ b/SpineRuntimes/SpineRuntime37/SpineRuntime37.csproj @@ -0,0 +1,14 @@ + + + + enable + enable + x64 + net8.0-windows + win-x64 + $(SolutionDir)out + false + 3.7.94 + + + diff --git a/SpineRuntimes/SpineRuntime37/TransformConstraint.cs b/SpineRuntimes/SpineRuntime37/TransformConstraint.cs new file mode 100644 index 0000000..14f0d03 --- /dev/null +++ b/SpineRuntimes/SpineRuntime37/TransformConstraint.cs @@ -0,0 +1,311 @@ +/****************************************************************************** + * Spine Runtimes License Agreement + * Last updated May 1, 2019. Replaces all prior versions. + * + * Copyright (c) 2013-2019, Esoteric Software LLC + * + * Integration of the Spine Runtimes into software or otherwise creating + * derivative works of the Spine Runtimes is permitted under the terms and + * conditions of Section 2 of the Spine Editor License Agreement: + * http://esotericsoftware.com/spine-editor-license + * + * Otherwise, it is permitted to integrate the Spine Runtimes into software + * or otherwise create derivative works of the Spine Runtimes (collectively, + * "Products"), provided that each user of the Products must obtain their own + * Spine Editor license and redistribution of the Products in any form must + * include this license and copyright notice. + * + * THIS SOFTWARE IS PROVIDED BY ESOTERIC SOFTWARE LLC "AS IS" AND ANY EXPRESS + * OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES + * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN + * NO EVENT SHALL ESOTERIC SOFTWARE LLC BE LIABLE FOR ANY DIRECT, INDIRECT, + * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, + * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES, BUSINESS + * INTERRUPTION, OR LOSS OF USE, DATA, OR PROFITS) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, + * EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + *****************************************************************************/ + +using System; + +namespace SpineRuntime37 { + /// + /// + /// Stores the current pose for a transform constraint. A transform constraint adjusts the world transform of the constrained + /// bones to match that of the target bone. + /// + /// See Transform constraints in the Spine User Guide. + /// + public class TransformConstraint : IConstraint { + internal TransformConstraintData data; + internal ExposedList bones; + internal Bone target; + internal float rotateMix, translateMix, scaleMix, shearMix; + + public TransformConstraint (TransformConstraintData data, Skeleton skeleton) { + if (data == null) throw new ArgumentNullException("data", "data cannot be null."); + if (skeleton == null) throw new ArgumentNullException("skeleton", "skeleton cannot be null."); + this.data = data; + rotateMix = data.rotateMix; + translateMix = data.translateMix; + scaleMix = data.scaleMix; + shearMix = data.shearMix; + + bones = new ExposedList(); + foreach (BoneData boneData in data.bones) + bones.Add (skeleton.FindBone(boneData.name)); + + target = skeleton.FindBone(data.target.name); + } + + /// Copy constructor. + public TransformConstraint (TransformConstraint constraint, Skeleton skeleton) { + if (constraint == null) throw new ArgumentNullException("constraint cannot be null."); + if (skeleton == null) throw new ArgumentNullException("skeleton cannot be null."); + data = constraint.data; + bones = new ExposedList(constraint.Bones.Count); + foreach (Bone bone in constraint.Bones) + bones.Add(skeleton.Bones.Items[bone.data.index]); + target = skeleton.Bones.Items[constraint.target.data.index]; + rotateMix = constraint.rotateMix; + translateMix = constraint.translateMix; + scaleMix = constraint.scaleMix; + shearMix = constraint.shearMix; + } + + /// Applies the constraint to the constrained bones. + public void Apply () { + Update(); + } + + public void Update () { + if (data.local) { + if (data.relative) + ApplyRelativeLocal(); + else + ApplyAbsoluteLocal(); + } else { + if (data.relative) + ApplyRelativeWorld(); + else + ApplyAbsoluteWorld(); + } + } + + void ApplyAbsoluteWorld () { + float rotateMix = this.rotateMix, translateMix = this.translateMix, scaleMix = this.scaleMix, shearMix = this.shearMix; + Bone target = this.target; + float ta = target.a, tb = target.b, tc = target.c, td = target.d; + float degRadReflect = ta * td - tb * tc > 0 ? MathUtils.DegRad : -MathUtils.DegRad; + float offsetRotation = data.offsetRotation * degRadReflect, offsetShearY = data.offsetShearY * degRadReflect; + var bones = this.bones; + for (int i = 0, n = bones.Count; i < n; i++) { + Bone bone = bones.Items[i]; + bool modified = false; + + if (rotateMix != 0) { + float a = bone.a, b = bone.b, c = bone.c, d = bone.d; + float r = MathUtils.Atan2(tc, ta) - MathUtils.Atan2(c, a) + offsetRotation; + if (r > MathUtils.PI) + r -= MathUtils.PI2; + else if (r < -MathUtils.PI) r += MathUtils.PI2; + r *= rotateMix; + float cos = MathUtils.Cos(r), sin = MathUtils.Sin(r); + bone.a = cos * a - sin * c; + bone.b = cos * b - sin * d; + bone.c = sin * a + cos * c; + bone.d = sin * b + cos * d; + modified = true; + } + + if (translateMix != 0) { + float tx, ty; //Vector2 temp = this.temp; + target.LocalToWorld(data.offsetX, data.offsetY, out tx, out ty); //target.localToWorld(temp.set(data.offsetX, data.offsetY)); + bone.worldX += (tx - bone.worldX) * translateMix; + bone.worldY += (ty - bone.worldY) * translateMix; + modified = true; + } + + if (scaleMix > 0) { + float s = (float)Math.Sqrt(bone.a * bone.a + bone.c * bone.c); + if (s != 0) s = (s + ((float)Math.Sqrt(ta * ta + tc * tc) - s + data.offsetScaleX) * scaleMix) / s; + bone.a *= s; + bone.c *= s; + s = (float)Math.Sqrt(bone.b * bone.b + bone.d * bone.d); + if (s != 0) s = (s + ((float)Math.Sqrt(tb * tb + td * td) - s + data.offsetScaleY) * scaleMix) / s; + bone.b *= s; + bone.d *= s; + modified = true; + } + + if (shearMix > 0) { + float b = bone.b, d = bone.d; + float by = MathUtils.Atan2(d, b); + float r = MathUtils.Atan2(td, tb) - MathUtils.Atan2(tc, ta) - (by - MathUtils.Atan2(bone.c, bone.a)); + if (r > MathUtils.PI) + r -= MathUtils.PI2; + else if (r < -MathUtils.PI) r += MathUtils.PI2; + r = by + (r + offsetShearY) * shearMix; + float s = (float)Math.Sqrt(b * b + d * d); + bone.b = MathUtils.Cos(r) * s; + bone.d = MathUtils.Sin(r) * s; + modified = true; + } + + if (modified) bone.appliedValid = false; + } + } + + void ApplyRelativeWorld () { + float rotateMix = this.rotateMix, translateMix = this.translateMix, scaleMix = this.scaleMix, shearMix = this.shearMix; + Bone target = this.target; + float ta = target.a, tb = target.b, tc = target.c, td = target.d; + float degRadReflect = ta * td - tb * tc > 0 ? MathUtils.DegRad : -MathUtils.DegRad; + float offsetRotation = data.offsetRotation * degRadReflect, offsetShearY = data.offsetShearY * degRadReflect; + var bones = this.bones; + for (int i = 0, n = bones.Count; i < n; i++) { + Bone bone = bones.Items[i]; + bool modified = false; + + if (rotateMix != 0) { + float a = bone.a, b = bone.b, c = bone.c, d = bone.d; + float r = MathUtils.Atan2(tc, ta) + offsetRotation; + if (r > MathUtils.PI) + r -= MathUtils.PI2; + else if (r < -MathUtils.PI) r += MathUtils.PI2; + r *= rotateMix; + float cos = MathUtils.Cos(r), sin = MathUtils.Sin(r); + bone.a = cos * a - sin * c; + bone.b = cos * b - sin * d; + bone.c = sin * a + cos * c; + bone.d = sin * b + cos * d; + modified = true; + } + + if (translateMix != 0) { + float tx, ty; //Vector2 temp = this.temp; + target.LocalToWorld(data.offsetX, data.offsetY, out tx, out ty); //target.localToWorld(temp.set(data.offsetX, data.offsetY)); + bone.worldX += tx * translateMix; + bone.worldY += ty * translateMix; + modified = true; + } + + if (scaleMix > 0) { + float s = ((float)Math.Sqrt(ta * ta + tc * tc) - 1 + data.offsetScaleX) * scaleMix + 1; + bone.a *= s; + bone.c *= s; + s = ((float)Math.Sqrt(tb * tb + td * td) - 1 + data.offsetScaleY) * scaleMix + 1; + bone.b *= s; + bone.d *= s; + modified = true; + } + + if (shearMix > 0) { + float r = MathUtils.Atan2(td, tb) - MathUtils.Atan2(tc, ta); + if (r > MathUtils.PI) + r -= MathUtils.PI2; + else if (r < -MathUtils.PI) r += MathUtils.PI2; + float b = bone.b, d = bone.d; + r = MathUtils.Atan2(d, b) + (r - MathUtils.PI / 2 + offsetShearY) * shearMix; + float s = (float)Math.Sqrt(b * b + d * d); + bone.b = MathUtils.Cos(r) * s; + bone.d = MathUtils.Sin(r) * s; + modified = true; + } + + if (modified) bone.appliedValid = false; + } + } + + void ApplyAbsoluteLocal () { + float rotateMix = this.rotateMix, translateMix = this.translateMix, scaleMix = this.scaleMix, shearMix = this.shearMix; + Bone target = this.target; + if (!target.appliedValid) target.UpdateAppliedTransform(); + var bonesItems = this.bones.Items; + for (int i = 0, n = this.bones.Count; i < n; i++) { + Bone bone = bonesItems[i]; + if (!bone.appliedValid) bone.UpdateAppliedTransform(); + + float rotation = bone.arotation; + if (rotateMix != 0) { + float r = target.arotation - rotation + data.offsetRotation; + r -= (16384 - (int)(16384.499999999996 - r / 360)) * 360; + rotation += r * rotateMix; + } + + float x = bone.ax, y = bone.ay; + if (translateMix != 0) { + x += (target.ax - x + data.offsetX) * translateMix; + y += (target.ay - y + data.offsetY) * translateMix; + } + + float scaleX = bone.ascaleX, scaleY = bone.ascaleY; + if (scaleMix != 0) { + if (scaleX != 0) scaleX = (scaleX + (target.ascaleX - scaleX + data.offsetScaleX) * scaleMix) / scaleX; + if (scaleY != 0) scaleY = (scaleY + (target.ascaleY - scaleY + data.offsetScaleY) * scaleMix) / scaleY; + } + + float shearY = bone.ashearY; + if (shearMix != 0) { + float r = target.ashearY - shearY + data.offsetShearY; + r -= (16384 - (int)(16384.499999999996 - r / 360)) * 360; + shearY += r * shearMix; + } + + bone.UpdateWorldTransform(x, y, rotation, scaleX, scaleY, bone.ashearX, shearY); + } + } + + void ApplyRelativeLocal () { + float rotateMix = this.rotateMix, translateMix = this.translateMix, scaleMix = this.scaleMix, shearMix = this.shearMix; + Bone target = this.target; + if (!target.appliedValid) target.UpdateAppliedTransform(); + var bonesItems = this.bones.Items; + for (int i = 0, n = this.bones.Count; i < n; i++) { + Bone bone = bonesItems[i]; + if (!bone.appliedValid) bone.UpdateAppliedTransform(); + + float rotation = bone.arotation; + if (rotateMix != 0) rotation += (target.arotation + data.offsetRotation) * rotateMix; + + float x = bone.ax, y = bone.ay; + if (translateMix != 0) { + x += (target.ax + data.offsetX) * translateMix; + y += (target.ay + data.offsetY) * translateMix; + } + + float scaleX = bone.ascaleX, scaleY = bone.ascaleY; + if (scaleMix != 0) { + scaleX *= ((target.ascaleX - 1 + data.offsetScaleX) * scaleMix) + 1; + scaleY *= ((target.ascaleY - 1 + data.offsetScaleY) * scaleMix) + 1; + } + + float shearY = bone.ashearY; + if (shearMix != 0) shearY += (target.ashearY + data.offsetShearY) * shearMix; + + bone.UpdateWorldTransform(x, y, rotation, scaleX, scaleY, bone.ashearX, shearY); + } + } + + public int Order { get { return data.order; } } + /// The bones that will be modified by this transform constraint. + public ExposedList Bones { get { return bones; } } + /// The target bone whose world transform will be copied to the constrained bones. + public Bone Target { get { return target; } set { target = value; } } + /// A percentage (0-1) that controls the mix between the constrained and unconstrained rotations. + public float RotateMix { get { return rotateMix; } set { rotateMix = value; } } + /// A percentage (0-1) that controls the mix between the constrained and unconstrained translations. + public float TranslateMix { get { return translateMix; } set { translateMix = value; } } + /// A percentage (0-1) that controls the mix between the constrained and unconstrained scales. + public float ScaleMix { get { return scaleMix; } set { scaleMix = value; } } + /// A percentage (0-1) that controls the mix between the constrained and unconstrained scales. + public float ShearMix { get { return shearMix; } set { shearMix = value; } } + /// The transform constraint's setup pose data. + public TransformConstraintData Data { get { return data; } } + + override public string ToString () { + return data.name; + } + } +} diff --git a/SpineRuntimes/SpineRuntime37/TransformConstraintData.cs b/SpineRuntimes/SpineRuntime37/TransformConstraintData.cs new file mode 100644 index 0000000..8d4c6da --- /dev/null +++ b/SpineRuntimes/SpineRuntime37/TransformConstraintData.cs @@ -0,0 +1,70 @@ +/****************************************************************************** + * Spine Runtimes License Agreement + * Last updated May 1, 2019. Replaces all prior versions. + * + * Copyright (c) 2013-2019, Esoteric Software LLC + * + * Integration of the Spine Runtimes into software or otherwise creating + * derivative works of the Spine Runtimes is permitted under the terms and + * conditions of Section 2 of the Spine Editor License Agreement: + * http://esotericsoftware.com/spine-editor-license + * + * Otherwise, it is permitted to integrate the Spine Runtimes into software + * or otherwise create derivative works of the Spine Runtimes (collectively, + * "Products"), provided that each user of the Products must obtain their own + * Spine Editor license and redistribution of the Products in any form must + * include this license and copyright notice. + * + * THIS SOFTWARE IS PROVIDED BY ESOTERIC SOFTWARE LLC "AS IS" AND ANY EXPRESS + * OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES + * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN + * NO EVENT SHALL ESOTERIC SOFTWARE LLC BE LIABLE FOR ANY DIRECT, INDIRECT, + * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, + * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES, BUSINESS + * INTERRUPTION, OR LOSS OF USE, DATA, OR PROFITS) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, + * EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + *****************************************************************************/ + +using System; + +namespace SpineRuntime37 { + public class TransformConstraintData { + internal string name; + internal int order; + internal ExposedList bones = new ExposedList(); + internal BoneData target; + internal float rotateMix, translateMix, scaleMix, shearMix; + internal float offsetRotation, offsetX, offsetY, offsetScaleX, offsetScaleY, offsetShearY; + internal bool relative, local; + + public string Name { get { return name; } } + public int Order { get { return order; } set { order = value; } } + public ExposedList Bones { get { return bones; } } + public BoneData Target { get { return target; } set { target = value; } } + public float RotateMix { get { return rotateMix; } set { rotateMix = value; } } + public float TranslateMix { get { return translateMix; } set { translateMix = value; } } + public float ScaleMix { get { return scaleMix; } set { scaleMix = value; } } + public float ShearMix { get { return shearMix; } set { shearMix = value; } } + + public float OffsetRotation { get { return offsetRotation; } set { offsetRotation = value; } } + public float OffsetX { get { return offsetX; } set { offsetX = value; } } + public float OffsetY { get { return offsetY; } set { offsetY = value; } } + public float OffsetScaleX { get { return offsetScaleX; } set { offsetScaleX = value; } } + public float OffsetScaleY { get { return offsetScaleY; } set { offsetScaleY = value; } } + public float OffsetShearY { get { return offsetShearY; } set { offsetShearY = value; } } + + public bool Relative { get { return relative; } set { relative = value; } } + public bool Local { get { return local; } set { local = value; } } + + public TransformConstraintData (string name) { + if (name == null) throw new ArgumentNullException("name", "name cannot be null."); + this.name = name; + } + + override public string ToString () { + return name; + } + } +} diff --git a/SpineRuntimes/SpineRuntime37/Triangulator.cs b/SpineRuntimes/SpineRuntime37/Triangulator.cs new file mode 100644 index 0000000..bc4dd3a --- /dev/null +++ b/SpineRuntimes/SpineRuntime37/Triangulator.cs @@ -0,0 +1,277 @@ +/****************************************************************************** + * Spine Runtimes License Agreement + * Last updated May 1, 2019. Replaces all prior versions. + * + * Copyright (c) 2013-2019, Esoteric Software LLC + * + * Integration of the Spine Runtimes into software or otherwise creating + * derivative works of the Spine Runtimes is permitted under the terms and + * conditions of Section 2 of the Spine Editor License Agreement: + * http://esotericsoftware.com/spine-editor-license + * + * Otherwise, it is permitted to integrate the Spine Runtimes into software + * or otherwise create derivative works of the Spine Runtimes (collectively, + * "Products"), provided that each user of the Products must obtain their own + * Spine Editor license and redistribution of the Products in any form must + * include this license and copyright notice. + * + * THIS SOFTWARE IS PROVIDED BY ESOTERIC SOFTWARE LLC "AS IS" AND ANY EXPRESS + * OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES + * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN + * NO EVENT SHALL ESOTERIC SOFTWARE LLC BE LIABLE FOR ANY DIRECT, INDIRECT, + * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, + * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES, BUSINESS + * INTERRUPTION, OR LOSS OF USE, DATA, OR PROFITS) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, + * EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + *****************************************************************************/ + +using System; + +namespace SpineRuntime37 { + public class Triangulator { + private readonly ExposedList> convexPolygons = new ExposedList>(); + private readonly ExposedList> convexPolygonsIndices = new ExposedList>(); + + private readonly ExposedList indicesArray = new ExposedList(); + private readonly ExposedList isConcaveArray = new ExposedList(); + private readonly ExposedList triangles = new ExposedList(); + + private readonly Pool> polygonPool = new Pool>(); + private readonly Pool> polygonIndicesPool = new Pool>(); + + public ExposedList Triangulate (ExposedList verticesArray) { + var vertices = verticesArray.Items; + int vertexCount = verticesArray.Count >> 1; + + var indicesArray = this.indicesArray; + indicesArray.Clear(); + int[] indices = indicesArray.Resize(vertexCount).Items; + for (int i = 0; i < vertexCount; i++) + indices[i] = i; + + var isConcaveArray = this.isConcaveArray; + bool[] isConcave = isConcaveArray.Resize(vertexCount).Items; + for (int i = 0, n = vertexCount; i < n; ++i) + isConcave[i] = IsConcave(i, vertexCount, vertices, indices); + + var triangles = this.triangles; + triangles.Clear(); + triangles.EnsureCapacity(Math.Max(0, vertexCount - 2) << 2); + + while (vertexCount > 3) { + // Find ear tip. + int previous = vertexCount - 1, i = 0, next = 1; + + // outer: + while (true) { + if (!isConcave[i]) { + int p1 = indices[previous] << 1, p2 = indices[i] << 1, p3 = indices[next] << 1; + float p1x = vertices[p1], p1y = vertices[p1 + 1]; + float p2x = vertices[p2], p2y = vertices[p2 + 1]; + float p3x = vertices[p3], p3y = vertices[p3 + 1]; + for (int ii = (next + 1) % vertexCount; ii != previous; ii = (ii + 1) % vertexCount) { + if (!isConcave[ii]) continue; + int v = indices[ii] << 1; + float vx = vertices[v], vy = vertices[v + 1]; + if (PositiveArea(p3x, p3y, p1x, p1y, vx, vy)) { + if (PositiveArea(p1x, p1y, p2x, p2y, vx, vy)) { + if (PositiveArea(p2x, p2y, p3x, p3y, vx, vy)) goto break_outer; // break outer; + } + } + } + break; + } + break_outer: + + if (next == 0) { + do { + if (!isConcave[i]) break; + i--; + } while (i > 0); + break; + } + + previous = i; + i = next; + next = (next + 1) % vertexCount; + } + + // Cut ear tip. + triangles.Add(indices[(vertexCount + i - 1) % vertexCount]); + triangles.Add(indices[i]); + triangles.Add(indices[(i + 1) % vertexCount]); + indicesArray.RemoveAt(i); + isConcaveArray.RemoveAt(i); + vertexCount--; + + int previousIndex = (vertexCount + i - 1) % vertexCount; + int nextIndex = i == vertexCount ? 0 : i; + isConcave[previousIndex] = IsConcave(previousIndex, vertexCount, vertices, indices); + isConcave[nextIndex] = IsConcave(nextIndex, vertexCount, vertices, indices); + } + + if (vertexCount == 3) { + triangles.Add(indices[2]); + triangles.Add(indices[0]); + triangles.Add(indices[1]); + } + + return triangles; + } + + public ExposedList> Decompose (ExposedList verticesArray, ExposedList triangles) { + var vertices = verticesArray.Items; + var convexPolygons = this.convexPolygons; + for (int i = 0, n = convexPolygons.Count; i < n; i++) { + polygonPool.Free(convexPolygons.Items[i]); + } + convexPolygons.Clear(); + + var convexPolygonsIndices = this.convexPolygonsIndices; + for (int i = 0, n = convexPolygonsIndices.Count; i < n; i++) { + polygonIndicesPool.Free(convexPolygonsIndices.Items[i]); + } + convexPolygonsIndices.Clear(); + + var polygonIndices = polygonIndicesPool.Obtain(); + polygonIndices.Clear(); + + var polygon = polygonPool.Obtain(); + polygon.Clear(); + + // Merge subsequent triangles if they form a triangle fan. + int fanBaseIndex = -1, lastWinding = 0; + int[] trianglesItems = triangles.Items; + for (int i = 0, n = triangles.Count; i < n; i += 3) { + int t1 = trianglesItems[i] << 1, t2 = trianglesItems[i + 1] << 1, t3 = trianglesItems[i + 2] << 1; + float x1 = vertices[t1], y1 = vertices[t1 + 1]; + float x2 = vertices[t2], y2 = vertices[t2 + 1]; + float x3 = vertices[t3], y3 = vertices[t3 + 1]; + + // If the base of the last triangle is the same as this triangle, check if they form a convex polygon (triangle fan). + var merged = false; + if (fanBaseIndex == t1) { + int o = polygon.Count - 4; + float[] p = polygon.Items; + int winding1 = Winding(p[o], p[o + 1], p[o + 2], p[o + 3], x3, y3); + int winding2 = Winding(x3, y3, p[0], p[1], p[2], p[3]); + if (winding1 == lastWinding && winding2 == lastWinding) { + polygon.Add(x3); + polygon.Add(y3); + polygonIndices.Add(t3); + merged = true; + } + } + + // Otherwise make this triangle the new base. + if (!merged) { + if (polygon.Count > 0) { + convexPolygons.Add(polygon); + convexPolygonsIndices.Add(polygonIndices); + } else { + polygonPool.Free(polygon); + polygonIndicesPool.Free(polygonIndices); + } + polygon = polygonPool.Obtain(); + polygon.Clear(); + polygon.Add(x1); + polygon.Add(y1); + polygon.Add(x2); + polygon.Add(y2); + polygon.Add(x3); + polygon.Add(y3); + polygonIndices = polygonIndicesPool.Obtain(); + polygonIndices.Clear(); + polygonIndices.Add(t1); + polygonIndices.Add(t2); + polygonIndices.Add(t3); + lastWinding = Winding(x1, y1, x2, y2, x3, y3); + fanBaseIndex = t1; + } + } + + if (polygon.Count > 0) { + convexPolygons.Add(polygon); + convexPolygonsIndices.Add(polygonIndices); + } + + // Go through the list of polygons and try to merge the remaining triangles with the found triangle fans. + for (int i = 0, n = convexPolygons.Count; i < n; i++) { + polygonIndices = convexPolygonsIndices.Items[i]; + if (polygonIndices.Count == 0) continue; + int firstIndex = polygonIndices.Items[0]; + int lastIndex = polygonIndices.Items[polygonIndices.Count - 1]; + + polygon = convexPolygons.Items[i]; + int o = polygon.Count - 4; + float[] p = polygon.Items; + float prevPrevX = p[o], prevPrevY = p[o + 1]; + float prevX = p[o + 2], prevY = p[o + 3]; + float firstX = p[0], firstY = p[1]; + float secondX = p[2], secondY = p[3]; + int winding = Winding(prevPrevX, prevPrevY, prevX, prevY, firstX, firstY); + + for (int ii = 0; ii < n; ii++) { + if (ii == i) continue; + var otherIndices = convexPolygonsIndices.Items[ii]; + if (otherIndices.Count != 3) continue; + int otherFirstIndex = otherIndices.Items[0]; + int otherSecondIndex = otherIndices.Items[1]; + int otherLastIndex = otherIndices.Items[2]; + + var otherPoly = convexPolygons.Items[ii]; + float x3 = otherPoly.Items[otherPoly.Count - 2], y3 = otherPoly.Items[otherPoly.Count - 1]; + + if (otherFirstIndex != firstIndex || otherSecondIndex != lastIndex) continue; + int winding1 = Winding(prevPrevX, prevPrevY, prevX, prevY, x3, y3); + int winding2 = Winding(x3, y3, firstX, firstY, secondX, secondY); + if (winding1 == winding && winding2 == winding) { + otherPoly.Clear(); + otherIndices.Clear(); + polygon.Add(x3); + polygon.Add(y3); + polygonIndices.Add(otherLastIndex); + prevPrevX = prevX; + prevPrevY = prevY; + prevX = x3; + prevY = y3; + ii = 0; + } + } + } + + // Remove empty polygons that resulted from the merge step above. + for (int i = convexPolygons.Count - 1; i >= 0; i--) { + polygon = convexPolygons.Items[i]; + if (polygon.Count == 0) { + convexPolygons.RemoveAt(i); + polygonPool.Free(polygon); + polygonIndices = convexPolygonsIndices.Items[i]; + convexPolygonsIndices.RemoveAt(i); + polygonIndicesPool.Free(polygonIndices); + } + } + + return convexPolygons; + } + + static private bool IsConcave (int index, int vertexCount, float[] vertices, int[] indices) { + int previous = indices[(vertexCount + index - 1) % vertexCount] << 1; + int current = indices[index] << 1; + int next = indices[(index + 1) % vertexCount] << 1; + return !PositiveArea(vertices[previous], vertices[previous + 1], vertices[current], vertices[current + 1], vertices[next], + vertices[next + 1]); + } + + static private bool PositiveArea (float p1x, float p1y, float p2x, float p2y, float p3x, float p3y) { + return p1x * (p3y - p2y) + p2x * (p1y - p3y) + p3x * (p2y - p1y) >= 0; + } + + static private int Winding (float p1x, float p1y, float p2x, float p2y, float p3x, float p3y) { + float px = p2x - p1x, py = p2y - p1y; + return p3x * py - p3y * px + px * p1y - p1x * py >= 0 ? 1 : -1; + } + } +} diff --git a/SpineViewer.sln b/SpineViewer.sln index 864681c..ad792d4 100644 --- a/SpineViewer.sln +++ b/SpineViewer.sln @@ -18,6 +18,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution README.md = README.md EndProjectSection EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SpineRuntime37", "SpineRuntimes\SpineRuntime37\SpineRuntime37.csproj", "{58954AAB-423F-178B-7B9C-61C54ECEC69C}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|x64 = Debug|x64 @@ -36,6 +38,10 @@ Global {ECF7297E-031B-4E37-8033-7C2345DB8766}.Debug|x64.Build.0 = Debug|x64 {ECF7297E-031B-4E37-8033-7C2345DB8766}.Release|x64.ActiveCfg = Release|x64 {ECF7297E-031B-4E37-8033-7C2345DB8766}.Release|x64.Build.0 = Release|x64 + {58954AAB-423F-178B-7B9C-61C54ECEC69C}.Debug|x64.ActiveCfg = Debug|x64 + {58954AAB-423F-178B-7B9C-61C54ECEC69C}.Debug|x64.Build.0 = Debug|x64 + {58954AAB-423F-178B-7B9C-61C54ECEC69C}.Release|x64.ActiveCfg = Release|x64 + {58954AAB-423F-178B-7B9C-61C54ECEC69C}.Release|x64.Build.0 = Release|x64 EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -43,6 +49,7 @@ Global GlobalSection(NestedProjects) = preSolution {CA964DA9-3649-44BC-84F7-B1108A652905} = {EA2E1399-02BC-43BC-AD9F-42E23E9C3DA8} {ECF7297E-031B-4E37-8033-7C2345DB8766} = {EA2E1399-02BC-43BC-AD9F-42E23E9C3DA8} + {58954AAB-423F-178B-7B9C-61C54ECEC69C} = {EA2E1399-02BC-43BC-AD9F-42E23E9C3DA8} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {91F0EFD1-4B07-4C3C-82D8-90432349D3A5} diff --git a/SpineViewer/Spine/Implementations/Spine37.cs b/SpineViewer/Spine/Implementations/Spine37.cs new file mode 100644 index 0000000..686516a --- /dev/null +++ b/SpineViewer/Spine/Implementations/Spine37.cs @@ -0,0 +1,338 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using SpineRuntime37; + +namespace SpineViewer.Spine.Implementations +{ + [SpineImplementation(Version.V37)] + internal class Spine37 : Spine + { + private class TextureLoader : SpineRuntime37.TextureLoader + { + public void Load(AtlasPage page, string path) + { + var texture = new SFML.Graphics.Texture(path); + if (page.magFilter == TextureFilter.Linear) + texture.Smooth = true; + if (page.uWrap == TextureWrap.Repeat && page.vWrap == TextureWrap.Repeat) + texture.Repeated = true; + + page.rendererObject = texture; + page.width = (int)texture.Size.X; + page.height = (int)texture.Size.Y; + } + + public void Unload(object texture) + { + ((SFML.Graphics.Texture)texture).Dispose(); + } + } + + private static TextureLoader textureLoader = new(); + + private Atlas atlas; + private SkeletonBinary? skeletonBinary; + private SkeletonJson? skeletonJson; + private SkeletonData skeletonData; + private AnimationStateData animationStateData; + + private Skeleton skeleton; + private AnimationState animationState; + + private SkeletonClipping clipping = new(); + + public Spine37(string skelPath, string? atlasPath = null) : base(skelPath, atlasPath) + { + atlas = new Atlas(AtlasPath, textureLoader); + try + { + // 先尝试二进制文件 + skeletonJson = null; + skeletonBinary = new SkeletonBinary(atlas); + skeletonData = skeletonBinary.ReadSkeletonData(SkelPath); + } + catch + { + try + { + // 再尝试 Json 文件 + skeletonBinary = null; + skeletonJson = new SkeletonJson(atlas); + skeletonData = skeletonJson.ReadSkeletonData(SkelPath); + } + catch + { + // 都不行就报错 + throw new ArgumentException($"Unknown skeleton file format {SkelPath}"); + } + } + + animationStateData = new AnimationStateData(skeletonData); + skeleton = new Skeleton(skeletonData); + animationState = new AnimationState(animationStateData); + + foreach (var anime in skeletonData.Animations) + animationNames.Add(anime.Name); + + CurrentAnimation = DefaultAnimationName; + } + + protected override void Dispose(bool disposing) + { + base.Dispose(disposing); + atlas.Dispose(); + } + + public override float Scale + { + get + { + if (skeletonBinary is not null) + return skeletonBinary.Scale; + else if (skeletonJson is not null) + return skeletonJson.Scale; + else + return 1f; + } + set + { + // 保存状态 + var position = Position; + var flipX = FlipX; + var flipY = FlipY; + var savedTrack0 = animationState.GetCurrent(0); + + var val = Math.Max(value, SCALE_MIN); + if (skeletonBinary is not null) + { + skeletonBinary.Scale = val; + skeletonData = skeletonBinary.ReadSkeletonData(SkelPath); + } + else if (skeletonJson is not null) + { + skeletonJson.Scale = val; + skeletonData = skeletonJson.ReadSkeletonData(SkelPath); + } + + // reload skel-dependent data + animationStateData = new AnimationStateData(skeletonData) { DefaultMix = animationStateData.DefaultMix }; + skeleton = new Skeleton(skeletonData); + animationState = new AnimationState(animationStateData); + + // 恢复状态 + Position = position; + FlipX = flipX; + FlipY = flipY; + + // 恢复原本 Track0 上所有动画 + if (savedTrack0 is not null) + { + var entry = animationState.SetAnimation(0, savedTrack0.Animation.Name, true); + entry.TrackTime = savedTrack0.TrackTime; + var savedEntry = savedTrack0.Next; + while (savedEntry is not null) + { + entry = animationState.AddAnimation(0, savedEntry.Animation.Name, true, 0); + entry.TrackTime = savedEntry.TrackTime; + savedEntry = savedEntry.Next; + } + } + } + } + + public override PointF Position + { + get => new(skeleton.X, skeleton.Y); + set + { + skeleton.X = value.X; + skeleton.Y = value.Y; + } + } + + public override bool FlipX + { + get => skeleton.ScaleX < 0; + set + { + if (skeleton.ScaleX > 0 && value || skeleton.ScaleX < 0 && !value) + skeleton.ScaleX *= -1; + } + } + + public override bool FlipY + { + get => skeleton.ScaleY < 0; + set + { + if (skeleton.ScaleY > 0 && value || skeleton.ScaleY < 0 && !value) + skeleton.ScaleY *= -1; + } + } + + public override string CurrentAnimation + { + get => animationState.GetCurrent(0)?.Animation.Name ?? DefaultAnimationName; + set { if (animationNames.Contains(value)) { animationState.SetAnimation(0, value, true); Update(0); } } + } + + public override RectangleF Bounds + { + get + { + float[] _ = []; + skeleton.GetBounds(out var x, out var y, out var w, out var h, ref _); + return new RectangleF(x, y, w, h); + } + } + + public override float GetAnimationDuration(string name) { return skeletonData.FindAnimation(name)?.Duration ?? 0f; } + + public override void Update(float delta) + { + skeleton.Update(delta); + animationState.Update(delta); + animationState.Apply(skeleton); + skeleton.UpdateWorldTransform(); + } + + private SFML.Graphics.BlendMode GetSFMLBlendMode(SpineRuntime37.BlendMode spineBlendMode) + { + return spineBlendMode switch + { + SpineRuntime37.BlendMode.Normal => BlendMode.Normal, + SpineRuntime37.BlendMode.Additive => BlendMode.Additive, + SpineRuntime37.BlendMode.Multiply => BlendMode.Multiply, + SpineRuntime37.BlendMode.Screen => BlendMode.Screen, + _ => throw new NotImplementedException($"{spineBlendMode}"), + }; + } + + public override void Draw(SFML.Graphics.RenderTarget target, SFML.Graphics.RenderStates states) + { + vertexArray.Clear(); + states.Texture = null; + + // 要用 DrawOrder 而不是 Slots + foreach (var slot in skeleton.DrawOrder) + { + var attachment = slot.Attachment; + + SFML.Graphics.Texture texture; + + float[] worldVertices = worldVerticesBuffer; // 顶点世界坐标, 连续的 [x0, y0, x1, y1, ...] 坐标值 + int worldVerticesCount; // 等于顶点数组的长度除以 2 + int[] worldTriangleIndices; // 三角形索引, 从顶点坐标数组取的时候要乘以 2, 最大值是 worldVerticesCount - 1 + int worldTriangleIndicesLength; // 三角形索引数组长度 + float[] uvs; // 纹理坐标 + float tintR = skeleton.R * slot.R; + float tintG = skeleton.G * slot.G; + float tintB = skeleton.B * slot.B; + float tintA = skeleton.A * slot.A; + + if (attachment is RegionAttachment regionAttachment) + { + texture = (SFML.Graphics.Texture)((AtlasRegion)regionAttachment.RendererObject).page.rendererObject; + + regionAttachment.ComputeWorldVertices(slot.Bone, worldVertices, 0); + worldVerticesCount = 4; + worldTriangleIndices = [0, 1, 2, 2, 3, 0]; + worldTriangleIndicesLength = 6; + uvs = regionAttachment.UVs; + tintR *= regionAttachment.R; + tintG *= regionAttachment.G; + tintB *= regionAttachment.B; + tintA *= regionAttachment.A; + } + else if (attachment is MeshAttachment meshAttachment) + { + texture = (SFML.Graphics.Texture)((AtlasRegion)meshAttachment.RendererObject).page.rendererObject; + + if (meshAttachment.WorldVerticesLength > worldVertices.Length) + worldVertices = worldVerticesBuffer = new float[meshAttachment.WorldVerticesLength * 2]; + meshAttachment.ComputeWorldVertices(slot, worldVertices); + worldVerticesCount = meshAttachment.WorldVerticesLength / 2; + worldTriangleIndices = meshAttachment.Triangles; + worldTriangleIndicesLength = meshAttachment.Triangles.Length; + uvs = meshAttachment.UVs; + tintR *= meshAttachment.R; + tintG *= meshAttachment.G; + tintB *= meshAttachment.B; + tintA *= meshAttachment.A; + } + else if (attachment is ClippingAttachment clippingAttachment) + { + clipping.ClipStart(slot, clippingAttachment); + continue; + } + else + { + clipping.ClipEnd(slot); + continue; + } + + SFML.Graphics.BlendMode blendMode = GetSFMLBlendMode(slot.Data.BlendMode); + + states.Texture ??= texture; + if (states.BlendMode != blendMode || states.Texture != texture) + { + if (vertexArray.VertexCount > 0) + { + // XXX: 实测不用设置 sampler2D 的值也正确 + if (UsePremultipliedAlpha && (states.BlendMode == BlendMode.Normal || states.BlendMode == BlendMode.Additive)) + states.Shader = FragmentShader; + else + states.Shader = null; + target.Draw(vertexArray, states); + vertexArray.Clear(); + } + states.BlendMode = blendMode; + states.Texture = texture; + } + + if (clipping.IsClipping) + { + // 这里必须单独记录 Count, 和 Items 的 Length 是不一致的 + clipping.ClipTriangles(worldVertices, worldVerticesCount * 2, worldTriangleIndices, worldTriangleIndicesLength, uvs); + worldVertices = clipping.ClippedVertices.Items; + worldVerticesCount = clipping.ClippedVertices.Count / 2; + worldTriangleIndices = clipping.ClippedTriangles.Items; + worldTriangleIndicesLength = clipping.ClippedTriangles.Count; + uvs = clipping.ClippedUVs.Items; + } + + var textureSizeX = texture.Size.X; + var textureSizeY = texture.Size.Y; + + SFML.Graphics.Vertex vertex = new(); + vertex.Color.R = (byte)(tintR * 255); + vertex.Color.G = (byte)(tintG * 255); + vertex.Color.B = (byte)(tintB * 255); + vertex.Color.A = (byte)(tintA * 255); + + // 必须用 worldTriangleIndicesLength 不能直接 foreach + for (int i = 0; i < worldTriangleIndicesLength; i++) + { + var index = worldTriangleIndices[i] * 2; + vertex.Position.X = worldVertices[index]; + vertex.Position.Y = worldVertices[index + 1]; + vertex.TexCoords.X = uvs[index] * textureSizeX; + vertex.TexCoords.Y = uvs[index + 1] * textureSizeY; + vertexArray.Append(vertex); + } + + clipping.ClipEnd(slot); + } + + if (UsePremultipliedAlpha && (states.BlendMode == BlendMode.Normal || states.BlendMode == BlendMode.Additive)) + states.Shader = FragmentShader; + else + states.Shader = null; + target.Draw(vertexArray, states); + clipping.ClipEnd(); + } + } +} diff --git a/SpineViewer/Spine/Version.cs b/SpineViewer/Spine/Version.cs index 92d4b29..0c4fc16 100644 --- a/SpineViewer/Spine/Version.cs +++ b/SpineViewer/Spine/Version.cs @@ -41,7 +41,7 @@ namespace SpineViewer.Spine public enum Version { [Description("v3.6.53")] V36 = 0x0306, - [Description("v3.7.x")] V37 = 0x0307, + [Description("v3.7.94")] V37 = 0x0307, [Description("v3.8.99")] V38 = 0x0308, [Description("v4.0.x")] V40 = 0x0400, [Description("v4.1.x")] V41 = 0x0401, diff --git a/SpineViewer/SpineViewer.csproj b/SpineViewer/SpineViewer.csproj index 7440ad6..e63b7c0 100644 --- a/SpineViewer/SpineViewer.csproj +++ b/SpineViewer/SpineViewer.csproj @@ -26,6 +26,7 @@ +