commit cda14d0b89fa86d37f64b28d09ff7246154870ee Author: ww-rm Date: Wed Feb 26 14:24:53 2025 +0800 添加v36运行时 diff --git a/SpineRuntimes/SpineRuntime36/SpineRuntime36.csproj b/SpineRuntimes/SpineRuntime36/SpineRuntime36.csproj new file mode 100644 index 0000000..8e39dc5 --- /dev/null +++ b/SpineRuntimes/SpineRuntime36/SpineRuntime36.csproj @@ -0,0 +1,13 @@ + + + + net8.0-windows + x64 + win-x64 + enable + enable + 3.6.53 + $(SolutionDir)out + + + diff --git a/SpineRuntimes/SpineRuntime36/src/Animation.cs b/SpineRuntimes/SpineRuntime36/src/Animation.cs new file mode 100644 index 0000000..653a0ed --- /dev/null +++ b/SpineRuntimes/SpineRuntime36/src/Animation.cs @@ -0,0 +1,1395 @@ +/****************************************************************************** + * Spine Runtimes Software License v2.5 + * + * Copyright (c) 2013-2016, Esoteric Software + * All rights reserved. + * + * You are granted a perpetual, non-exclusive, non-sublicensable, and + * non-transferable license to use, install, execute, and perform the Spine + * Runtimes software and derivative works solely for personal or internal + * use. Without the written permission of Esoteric Software (see Section 2 of + * the Spine Software License Agreement), you may not (a) modify, translate, + * adapt, or develop new applications using the Spine Runtimes or otherwise + * create derivative works or improvements of the Spine Runtimes or (b) remove, + * delete, alter, or obscure any trademarks or any copyright, trademark, patent, + * or other intellectual property or proprietary rights notices on or in the + * Software, including any copy thereof. Redistributions in binary or source + * form must include this license and terms. + * + * THIS SOFTWARE IS PROVIDED BY ESOTERIC SOFTWARE "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 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 SpineRuntime36 { + public class Animation { + internal ExposedList timelines; + internal float duration; + internal String name; + + public string Name { get { return name; } } + public ExposedList Timelines { get { return timelines; } set { timelines = value; } } + public float Duration { get { return duration; } set { duration = value; } } + + 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; + } + + /// 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, MixPose pose, 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, pose, direction); + } + + /// After the first and before the last entry. + 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; + } + } + + public interface Timeline { + /// Sets the value(s) for the specified time. + /// The skeleton the timeline is being applied to. This provides access to the bones, slots, and other skeleton components the timeline may change. + /// lastTime The time this timeline was last applied. Timelines such as EventTimeline 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. May be null. + /// alpha 0 applies the current or setup pose value (depending on pose parameter). 1 applies the timeline + /// value. Between 0 and 1 applies a value between the current or setup pose 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 is than 1. + /// Indicates whether the timeline is mixing in or out. Used by timelines which perform instant transitions such as DrawOrderTimeline and AttachmentTimeline. + void Apply (Skeleton skeleton, float lastTime, float time, ExposedList events, float alpha, MixPose pose, MixDirection direction); + int PropertyId { get; } + } + + /// + /// Controls how a timeline is mixed with the setup or current pose. + /// + public enum MixPose { + /// The timeline value is mixed with the setup pose (the current pose is not used). + Setup, + /// The timeline value is mixed with the current pose. The setup pose is used as the timeline value before the first key, + /// except for timelines which perform instant transitions, such as DrawOrderTimeline or AttachmentTimeline. + Current, + /// The timeline value is mixed with the current pose. No change is made before the first key (the current pose is kept until the first key). + CurrentLayered + } + + /// + /// Indicates whether a timeline's alpha is mixing out over time toward 0 (the setup or current pose) or mixing in toward 1 (the timeline's pose). + /// + public enum MixDirection { + In, + Out + } + + internal enum TimelineType { + Rotate = 0, Translate, Scale, Shear, // + Attachment, Color, Deform, // + Event, DrawOrder, // + IkConstraint, TransformConstraint, // + PathConstraintPosition, PathConstraintSpacing, PathConstraintMix, // + TwoColor + } + + /// Base class for frames that use an interpolation bezier curve. + 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, ... + public int FrameCount { get { return curves.Length / BEZIER_SIZE + 1; } } + + public CurveTimeline (int frameCount) { + if (frameCount <= 0) throw new ArgumentException("frameCount must be > 0: " + frameCount, "frameCount"); + curves = new float[(frameCount - 1) * BEZIER_SIZE]; + } + + abstract public void Apply (Skeleton skeleton, float lastTime, float time, ExposedList firedEvents, float alpha, MixPose pose, MixDirection direction); + + abstract public int PropertyId { get; } + + public void SetLinear (int frameIndex) { + curves[frameIndex * BEZIER_SIZE] = LINEAR; + } + + public void SetStepped (int frameIndex) { + curves[frameIndex * BEZIER_SIZE] = STEPPED; + } + + /// Sets the control handle positions for an interpolation bezier curve used to transition from this keyframe to the next. + /// cx1 and cx2 are from 0 to 1, representing the percent of time between the two keyframes. cy1 and cy2 are the percent of + /// the difference between the keyframe'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; + } + } + + 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) { + float prevX, prevY; + if (i == start) { + prevX = 0; + prevY = 0; + } else { + 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. + } + public float GetCurveType (int frameIndex) { + return curves[frameIndex * BEZIER_SIZE]; + } + } + + public class RotateTimeline : CurveTimeline { + 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; + + public int BoneIndex { get { return boneIndex; } set { boneIndex = value; } } + public float[] Frames { get { return frames; } set { frames = value; } } // time, angle, ... + + override public int PropertyId { + get { return ((int)TimelineType.Rotate << 24) + boneIndex; } + } + + public RotateTimeline (int frameCount) + : base(frameCount) { + frames = new float[frameCount << 1]; + } + + /// Sets the time and value of the specified keyframe. + 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, MixPose pose, MixDirection direction) { + Bone bone = skeleton.bones.Items[boneIndex]; + + float[] frames = this.frames; + if (time < frames[0]) { + switch (pose) { + case MixPose.Setup: + bone.rotation = bone.data.rotation; + return; + case MixPose.Current: + float rr = bone.data.rotation - bone.rotation; + rr -= (16384 - (int)(16384.499999999996 - rr / 360)) * 360; + bone.rotation += rr * alpha; + return; + } + return; + } + + if (time >= frames[frames.Length - ENTRIES]) { // Time is after last frame. + if (pose == MixPose.Setup) { + bone.rotation = bone.data.rotation + frames[frames.Length + PREV_ROTATION] * alpha; + } else { + float rr = bone.data.rotation + frames[frames.Length + PREV_ROTATION] - bone.rotation; + rr -= (16384 - (int)(16384.499999999996 - rr / 360)) * 360; // Wrap within -180 and 180. + bone.rotation += rr * alpha; + } + 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)); + + float r = frames[frame + ROTATION] - prevRotation; + r -= (16384 - (int)(16384.499999999996 - r / 360)) * 360; + r = prevRotation + r * percent; + if (pose == MixPose.Setup) { + r -= (16384 - (int)(16384.499999999996 - r / 360)) * 360; + bone.rotation = bone.data.rotation + r * alpha; + } else { + r = bone.data.rotation + r - bone.rotation; + r -= (16384 - (int)(16384.499999999996 - r / 360)) * 360; + bone.rotation += r * alpha; + } + } + } + + public class TranslateTimeline : CurveTimeline { + 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; + + public int BoneIndex { get { return boneIndex; } set { boneIndex = value; } } + public float[] Frames { get { return frames; } set { frames = value; } } // time, value, value, ... + + override public int PropertyId { + get { return ((int)TimelineType.Translate << 24) + boneIndex; } + } + + public TranslateTimeline (int frameCount) + : base(frameCount) { + frames = new float[frameCount * ENTRIES]; + } + + /// Sets the time and value of the specified keyframe. + 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, MixPose pose, MixDirection direction) { + Bone bone = skeleton.bones.Items[boneIndex]; + + float[] frames = this.frames; + if (time < frames[0]) { + switch (pose) { + case MixPose.Setup: + bone.x = bone.data.x; + bone.y = bone.data.y; + return; + case MixPose.Current: + 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; + } + if (pose == MixPose.Setup) { + bone.x = bone.data.x + x * alpha; + bone.y = bone.data.y + y * alpha; + } else { + bone.x += (bone.data.x + x - bone.x) * alpha; + bone.y += (bone.data.y + y - bone.y) * alpha; + } + } + } + + public class ScaleTimeline : TranslateTimeline { + override public int PropertyId { + get { return ((int)TimelineType.Scale << 24) + boneIndex; } + } + + public ScaleTimeline (int frameCount) + : base(frameCount) { + } + + override public void Apply (Skeleton skeleton, float lastTime, float time, ExposedList firedEvents, float alpha, MixPose pose, MixDirection direction) { + Bone bone = skeleton.bones.Items[boneIndex]; + + float[] frames = this.frames; + if (time < frames[0]) { + switch (pose) { + case MixPose.Setup: + bone.scaleX = bone.data.scaleX; + bone.scaleY = bone.data.scaleY; + return; + case MixPose.Current: + 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) { + bone.scaleX = x; + bone.scaleY = y; + } else { + float bx, by; + if (pose == MixPose.Setup) { + bx = bone.data.scaleX; + by = bone.data.scaleY; + } else { + bx = bone.scaleX; + by = bone.scaleY; + } + // Mixing out uses sign of setup or current pose, else use sign of key. + if (direction == MixDirection.Out) { + x = (x >= 0 ? x : -x) * (bx >= 0 ? 1 : -1); + y = (y >= 0 ? y : -y) * (by >= 0 ? 1 : -1); + } else { + bx = (bx >= 0 ? bx : -bx) * (x >= 0 ? 1 : -1); + by = (by >= 0 ? by : -by) * (y >= 0 ? 1 : -1); + } + bone.scaleX = bx + (x - bx) * alpha; + bone.scaleY = by + (y - by) * alpha; + } + } + } + + public class ShearTimeline : TranslateTimeline { + override public int PropertyId { + get { return ((int)TimelineType.Shear << 24) + boneIndex; } + } + + public ShearTimeline (int frameCount) + : base(frameCount) { + } + + override public void Apply (Skeleton skeleton, float lastTime, float time, ExposedList firedEvents, float alpha, MixPose pose, MixDirection direction) { + Bone bone = skeleton.bones.Items[boneIndex]; + float[] frames = this.frames; + if (time < frames[0]) { + switch (pose) { + case MixPose.Setup: + bone.shearX = bone.data.shearX; + bone.shearY = bone.data.shearY; + return; + case MixPose.Current: + 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; + } + if (pose == MixPose.Setup) { + bone.shearX = bone.data.shearX + x * alpha; + bone.shearY = bone.data.shearY + y * alpha; + } else { + bone.shearX += (bone.data.shearX + x - bone.shearX) * alpha; + bone.shearY += (bone.data.shearY + y - bone.shearY) * alpha; + } + } + } + + public class ColorTimeline : CurveTimeline { + 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; + + public int SlotIndex { get { return slotIndex; } set { slotIndex = value; } } + public float[] Frames { get { return frames; } set { frames = value; } } // time, r, g, b, a, ... + + override public int PropertyId { + get { return ((int)TimelineType.Color << 24) + slotIndex; } + } + + public ColorTimeline (int frameCount) + : base(frameCount) { + frames = new float[frameCount * ENTRIES]; + } + + /// Sets the time and value of the specified keyframe. + 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, MixPose pose, MixDirection direction) { + Slot slot = skeleton.slots.Items[slotIndex]; + float[] frames = this.frames; + if (time < frames[0]) { + var slotData = slot.data; + switch (pose) { + case MixPose.Setup: + slot.r = slotData.r; + slot.g = slotData.g; + slot.b = slotData.b; + slot.a = slotData.a; + return; + case MixPose.Current: + 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; + 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 (pose == MixPose.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); + } + } + } + + public class TwoColorTimeline : CurveTimeline { + 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 int SlotIndex { + get { return slotIndex; } + set { + if (value < 0) + throw new ArgumentOutOfRangeException("index must be >= 0."); + slotIndex = value; + } + } + + public float[] Frames { get { return frames; } } + + override public int PropertyId { + get { return ((int)TimelineType.TwoColor << 24) + slotIndex; } + } + + public TwoColorTimeline (int frameCount) : + base(frameCount) { + frames = new float[frameCount * ENTRIES]; + } + + /// Sets the time and value of the specified keyframe. + 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, MixPose pose, 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 (pose) { + case MixPose.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 MixPose.Current: + 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 (pose == MixPose.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); + } + } + + } + + public class AttachmentTimeline : Timeline { + internal int slotIndex; + internal float[] frames; + internal string[] attachmentNames; + + public int SlotIndex { get { return slotIndex; } set { slotIndex = value; } } + public float[] Frames { get { return frames; } set { frames = value; } } // time, ... + public string[] AttachmentNames { get { return attachmentNames; } set { attachmentNames = value; } } + public int FrameCount { get { return frames.Length; } } + + public int PropertyId { + get { return ((int)TimelineType.Attachment << 24) + slotIndex; } + } + + public AttachmentTimeline (int frameCount) { + frames = new float[frameCount]; + attachmentNames = new String[frameCount]; + } + + /// Sets the time and value of the specified keyframe. + 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, MixPose pose, MixDirection direction) { + string attachmentName; + Slot slot = skeleton.slots.Items[slotIndex]; + if (direction == MixDirection.Out && pose == MixPose.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 (pose == MixPose.Setup) { + 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) - 1; + + attachmentName = attachmentNames[frameIndex]; + slot.Attachment = attachmentName == null ? null : skeleton.GetAttachment(slotIndex, attachmentName); + } + } + + public class DeformTimeline : CurveTimeline { + internal int slotIndex; + internal float[] frames; + internal float[][] frameVertices; + internal VertexAttachment attachment; + + public int SlotIndex { get { return slotIndex; } set { slotIndex = value; } } + public float[] Frames { get { return frames; } set { frames = value; } } // time, ... + public float[][] Vertices { get { return frameVertices; } set { frameVertices = value; } } + public VertexAttachment Attachment { get { return attachment; } set { attachment = value; } } + + override public int PropertyId { + get { return ((int)TimelineType.Deform << 24) + attachment.id + slotIndex; } + } + + public DeformTimeline (int frameCount) + : base(frameCount) { + frames = new float[frameCount]; + frameVertices = new float[frameCount][]; + } + + /// Sets the time and value of the specified keyframe. + 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, MixPose pose, 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) alpha = 1; + + float[][] frameVertices = this.frameVertices; + int vertexCount = frameVertices[0].Length; + float[] frames = this.frames; + float[] vertices; + + if (time < frames[0]) { + + switch (pose) { + case MixPose.Setup: + verticesArray.Clear(); + return; + case MixPose.Current: + 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) { + // Vertex positions or deform offsets, no alpha. + Array.Copy(lastVertices, 0, vertices, 0, vertexCount); + } else if (pose == MixPose.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; + } + } else { + // Vertex positions or deform offsets, with alpha. + for (int i = 0; i < vertexCount; i++) + vertices[i] += (lastVertices[i] - vertices[i]) * alpha; + } + 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) { + // 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 if (pose == MixPose.Setup) { + if (vertexAttachment.bones == null) { + // Unweighted vertex positions, with alpha. + var 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; + } + } + } else { + // 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; + } + } + } + } + + public class EventTimeline : Timeline { + internal float[] frames; + private Event[] events; + + public float[] Frames { get { return frames; } set { frames = value; } } // time, ... + public Event[] Events { get { return events; } set { events = value; } } + public int FrameCount { get { return frames.Length; } } + + public int PropertyId { + get { return ((int)TimelineType.Event << 24); } + } + + public EventTimeline (int frameCount) { + frames = new float[frameCount]; + events = new Event[frameCount]; + } + + /// Sets the time and value of the specified keyframe. + 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, MixPose pose, 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, pose, 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]); + } + } + + public class DrawOrderTimeline : Timeline { + internal float[] frames; + private int[][] drawOrders; + + public float[] Frames { get { return frames; } set { frames = value; } } // time, ... + public int[][] DrawOrders { get { return drawOrders; } set { drawOrders = value; } } + public int FrameCount { get { return frames.Length; } } + + public int PropertyId { + get { return ((int)TimelineType.DrawOrder << 24); } + } + + public DrawOrderTimeline (int frameCount) { + frames = new float[frameCount]; + drawOrders = new int[frameCount][]; + } + + /// Sets the time and value of the specified keyframe. + /// May be null to use bind 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, MixPose pose, MixDirection direction) { + ExposedList drawOrder = skeleton.drawOrder; + ExposedList slots = skeleton.slots; + if (direction == MixDirection.Out && pose == MixPose.Setup) { + Array.Copy(slots.Items, 0, drawOrder.Items, 0, slots.Count); + return; + } + + float[] frames = this.frames; + if (time < frames[0]) { + if (pose == MixPose.Setup) 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) { + drawOrder.Clear(); + for (int i = 0, n = slots.Count; i < n; i++) + drawOrder.Add(slots.Items[i]); + } else { + var drawOrderItems = drawOrder.Items; + var slotsItems = slots.Items; + for (int i = 0, n = drawOrderToSetupIndex.Length; i < n; i++) + drawOrderItems[i] = slotsItems[drawOrderToSetupIndex[i]]; + } + } + } + + public class IkConstraintTimeline : CurveTimeline { + public const int ENTRIES = 3; + private const int PREV_TIME = -3, PREV_MIX = -2, PREV_BEND_DIRECTION = -1; + private const int MIX = 1, BEND_DIRECTION = 2; + + internal int ikConstraintIndex; + internal float[] frames; + + public int IkConstraintIndex { get { return ikConstraintIndex; } set { ikConstraintIndex = value; } } + public float[] Frames { get { return frames; } set { frames = value; } } // time, mix, bendDirection, ... + + override public int PropertyId { + get { return ((int)TimelineType.IkConstraint << 24) + ikConstraintIndex; } + } + + public IkConstraintTimeline (int frameCount) + : base(frameCount) { + frames = new float[frameCount * ENTRIES]; + } + + /// Sets the time, mix and bend direction of the specified keyframe. + public void SetFrame (int frameIndex, float time, float mix, int bendDirection) { + frameIndex *= ENTRIES; + frames[frameIndex] = time; + frames[frameIndex + MIX] = mix; + frames[frameIndex + BEND_DIRECTION] = bendDirection; + } + + override public void Apply (Skeleton skeleton, float lastTime, float time, ExposedList firedEvents, float alpha, MixPose pose, MixDirection direction) { + IkConstraint constraint = skeleton.ikConstraints.Items[ikConstraintIndex]; + float[] frames = this.frames; + if (time < frames[0]) { + switch (pose) { + case MixPose.Setup: + constraint.mix = constraint.data.mix; + constraint.bendDirection = constraint.data.bendDirection; + return; + case MixPose.Current: + constraint.mix += (constraint.data.mix - constraint.mix) * alpha; + constraint.bendDirection = constraint.data.bendDirection; + return; + } + return; + } + + if (time >= frames[frames.Length - ENTRIES]) { // Time is after last frame. + if (pose == MixPose.Setup) { + constraint.mix = constraint.data.mix + (frames[frames.Length + PREV_MIX] - constraint.data.mix) * alpha; + constraint.bendDirection = direction == MixDirection.Out ? constraint.data.bendDirection + : (int)frames[frames.Length + PREV_BEND_DIRECTION]; + } else { + constraint.mix += (frames[frames.Length + PREV_MIX] - constraint.mix) * alpha; + if (direction == MixDirection.In) constraint.bendDirection = (int)frames[frames.Length + PREV_BEND_DIRECTION]; + } + 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 (pose == MixPose.Setup) { + constraint.mix = constraint.data.mix + (mix + (frames[frame + MIX] - mix) * percent - constraint.data.mix) * alpha; + constraint.bendDirection = direction == MixDirection.Out ? constraint.data.bendDirection : (int)frames[frame + PREV_BEND_DIRECTION]; + } else { + constraint.mix += (mix + (frames[frame + MIX] - mix) * percent - constraint.mix) * alpha; + if (direction == MixDirection.In) constraint.bendDirection = (int)frames[frame + PREV_BEND_DIRECTION]; + } + } + } + + 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; + + public int TransformConstraintIndex { get { return transformConstraintIndex; } set { transformConstraintIndex = value; } } + public float[] Frames { get { return frames; } set { frames = value; } } // time, rotate mix, translate mix, scale mix, shear mix, ... + + override public int PropertyId { + get { return ((int)TimelineType.TransformConstraint << 24) + transformConstraintIndex; } + } + + public TransformConstraintTimeline (int frameCount) + : base(frameCount) { + frames = new float[frameCount * ENTRIES]; + } + + 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, MixPose pose, MixDirection direction) { + TransformConstraint constraint = skeleton.transformConstraints.Items[transformConstraintIndex]; + float[] frames = this.frames; + if (time < frames[0]) { + var data = constraint.data; + switch (pose) { + case MixPose.Setup: + constraint.rotateMix = data.rotateMix; + constraint.translateMix = data.translateMix; + constraint.scaleMix = data.scaleMix; + constraint.shearMix = data.shearMix; + return; + case MixPose.Current: + 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 (pose == MixPose.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; + } + } + } + + 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; + + override public int PropertyId { + get { return ((int)TimelineType.PathConstraintPosition << 24) + pathConstraintIndex; } + } + + public PathConstraintPositionTimeline (int frameCount) + : base(frameCount) { + frames = new float[frameCount * ENTRIES]; + } + + public int PathConstraintIndex { get { return pathConstraintIndex; } set { pathConstraintIndex = value; } } + public float[] Frames { get { return frames; } set { frames = value; } } // time, position, ... + + /// Sets the time and value of the specified keyframe. + public void SetFrame (int frameIndex, float time, float value) { + frameIndex *= ENTRIES; + frames[frameIndex] = time; + frames[frameIndex + VALUE] = value; + } + + override public void Apply (Skeleton skeleton, float lastTime, float time, ExposedList firedEvents, float alpha, MixPose pose, MixDirection direction) { + PathConstraint constraint = skeleton.pathConstraints.Items[pathConstraintIndex]; + float[] frames = this.frames; + if (time < frames[0]) { + switch (pose) { + case MixPose.Setup: + constraint.position = constraint.data.position; + return; + case MixPose.Current: + 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 (pose == MixPose.Setup) + constraint.position = constraint.data.position + (position - constraint.data.position) * alpha; + else + constraint.position += (position - constraint.position) * alpha; + } + } + + public class PathConstraintSpacingTimeline : PathConstraintPositionTimeline { + override public int PropertyId { + get { return ((int)TimelineType.PathConstraintSpacing << 24) + pathConstraintIndex; } + } + + public PathConstraintSpacingTimeline (int frameCount) + : base(frameCount) { + } + + override public void Apply (Skeleton skeleton, float lastTime, float time, ExposedList firedEvents, float alpha, MixPose pose, MixDirection direction) { + PathConstraint constraint = skeleton.pathConstraints.Items[pathConstraintIndex]; + float[] frames = this.frames; + if (time < frames[0]) { + switch (pose) { + case MixPose.Setup: + constraint.spacing = constraint.data.spacing; + return; + case MixPose.Current: + 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 (pose == MixPose.Setup) + constraint.spacing = constraint.data.spacing + (spacing - constraint.data.spacing) * alpha; + else + constraint.spacing += (spacing - constraint.spacing) * alpha; + } + } + + 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; + + public int PathConstraintIndex { get { return pathConstraintIndex; } set { pathConstraintIndex = value; } } + public float[] Frames { get { return frames; } set { frames = value; } } // time, rotate mix, translate mix, ... + + override public int PropertyId { + get { return ((int)TimelineType.PathConstraintMix << 24) + pathConstraintIndex; } + } + + public PathConstraintMixTimeline (int frameCount) + : base(frameCount) { + frames = new float[frameCount * ENTRIES]; + } + + /// Sets the time and mixes of the specified keyframe. + 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, MixPose pose, MixDirection direction) { + PathConstraint constraint = skeleton.pathConstraints.Items[pathConstraintIndex]; + float[] frames = this.frames; + if (time < frames[0]) { + switch (pose) { + case MixPose.Setup: + constraint.rotateMix = constraint.data.rotateMix; + constraint.translateMix = constraint.data.translateMix; + return; + case MixPose.Current: + 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 (pose == MixPose.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/SpineRuntime36/src/AnimationState.cs b/SpineRuntimes/SpineRuntime36/src/AnimationState.cs new file mode 100644 index 0000000..1e7a3d4 --- /dev/null +++ b/SpineRuntimes/SpineRuntime36/src/AnimationState.cs @@ -0,0 +1,1066 @@ +/****************************************************************************** + * Spine Runtimes Software License v2.5 + * + * Copyright (c) 2013-2016, Esoteric Software + * All rights reserved. + * + * You are granted a perpetual, non-exclusive, non-sublicensable, and + * non-transferable license to use, install, execute, and perform the Spine + * Runtimes software and derivative works solely for personal or internal + * use. Without the written permission of Esoteric Software (see Section 2 of + * the Spine Software License Agreement), you may not (a) modify, translate, + * adapt, or develop new applications using the Spine Runtimes or otherwise + * create derivative works or improvements of the Spine Runtimes or (b) remove, + * delete, alter, or obscure any trademarks or any copyright, trademark, patent, + * or other intellectual property or proprietary rights notices on or in the + * Software, including any copy thereof. Redistributions in binary or source + * form must include this license and terms. + * + * THIS SOFTWARE IS PROVIDED BY ESOTERIC SOFTWARE "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 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 SpineRuntime36 { + public class AnimationState { + static readonly Animation EmptyAnimation = new Animation("", new ExposedList(), 0); + internal const int Subsequent = 0, First = 1, Dip = 2, DipMix = 3; + + private AnimationStateData data; + + Pool trackEntryPool = new Pool(); + private readonly ExposedList tracks = new ExposedList(); + private readonly ExposedList events = new ExposedList(); + private readonly EventQueue queue; // Initialized by constructor. + + private readonly HashSet propertyIDs = new HashSet(); + private readonly ExposedList mixingTo = new ExposedList(); + private bool animationsChanged; + + private float timeScale = 1; + + public AnimationStateData Data { get { return data; } } + /// A list of tracks that have animations, which may contain nulls. + public ExposedList Tracks { get { return tracks; } } + public float TimeScale { get { return timeScale; } set { timeScale = value; } } + + 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; + + 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 times, 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 = nextTime + (delta * next.timeScale); + current.trackTime += currentDelta; + SetCurrent(i, next, true); + while (next.mixingFrom != null) { + next.mixTime += currentDelta; + 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. + var from = current.mixingFrom; + current.mixingFrom = 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 || to.timeScale == 0)) { + // 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; + to.interruptAlpha = from.interruptAlpha; + queue.End(from); + } + return finished; + } + + from.trackTime += delta * from.timeScale; + to.mixTime += delta * to.timeScale; + 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. + 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, m = tracks.Count; i < m; i++) { + TrackEntry current = tracksItems[i]; + if (current == null || current.delay > 0) continue; + applied = true; + MixPose currentPose = i == 0 ? MixPose.Current : MixPose.CurrentLayered; + + // Apply mixing from entries first. + float mix = current.alpha; + if (current.mixingFrom != null) + mix *= ApplyMixingFrom(current, skeleton, currentPose); + 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 (mix == 1) { + for (int ii = 0; ii < timelineCount; ii++) + timelinesItems[ii].Apply(skeleton, animationLast, animationTime, events, 1, MixPose.Setup, MixDirection.In); + } else { + var timelineData = current.timelineData.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]; + MixPose pose = timelineData[ii] >= AnimationState.First ? MixPose.Setup : currentPose; + var rotateTimeline = timeline as RotateTimeline; + if (rotateTimeline != null) + ApplyRotateTimeline(rotateTimeline, skeleton, animationTime, mix, pose, timelinesRotation, ii << 1, firstFrame); + else + timeline.Apply(skeleton, animationLast, animationTime, events, mix, pose, 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, MixPose currentPose) { + TrackEntry from = to.mixingFrom; + if (from.mixingFrom != null) ApplyMixingFrom(from, skeleton, currentPose); + + float mix; + if (to.mixDuration == 0) { // Single frame mix to undo mixingFrom changes. + mix = 1; + currentPose = MixPose.Setup; + } else { + mix = to.mixTime / to.mixDuration; + if (mix > 1) mix = 1; + } + + 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; + var timelineData = from.timelineData.Items; + var timelineDipMix = from.timelineDipMix.Items; + + bool firstFrame = from.timelinesRotation.Count == 0; + if (firstFrame) from.timelinesRotation.Resize(timelines.Count << 1); // from.timelinesRotation.setSize + var timelinesRotation = from.timelinesRotation.Items; + + MixPose pose; + float alphaDip = from.alpha * to.interruptAlpha, alphaMix = alphaDip * (1 - mix), alpha; + from.totalAlpha = 0; + for (int i = 0; i < timelineCount; i++) { + Timeline timeline = timelinesItems[i]; + switch (timelineData[i]) { + case Subsequent: + if (!attachments && timeline is AttachmentTimeline) continue; + if (!drawOrder && timeline is DrawOrderTimeline) continue; + pose = currentPose; + alpha = alphaMix; + break; + case First: + pose = MixPose.Setup; + alpha = alphaMix; + break; + case Dip: + pose = MixPose.Setup; + alpha = alphaDip; + break; + default: + pose = MixPose.Setup; + TrackEntry dipMix = timelineDipMix[i]; + alpha = alphaDip * Math.Max(0, 1 - dipMix.mixTime / dipMix.mixDuration); + break; + } + from.totalAlpha += alpha; + var rotateTimeline = timeline as RotateTimeline; + if (rotateTimeline != null) { + ApplyRotateTimeline(rotateTimeline, skeleton, animationTime, alpha, pose, timelinesRotation, i << 1, firstFrame); + } else { + timeline.Apply(skeleton, animationLast, animationTime, eventBuffer, alpha, pose, MixDirection.Out); + } + } + + 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, MixPose pose, + float[] timelinesRotation, int i, bool firstFrame) { + + if (firstFrame) timelinesRotation[i] = 0; + + if (alpha == 1) { + rotateTimeline.Apply(skeleton, 0, time, null, 1, pose, MixDirection.In); + return; + } + + Bone bone = skeleton.bones.Items[rotateTimeline.boneIndex]; + float[] frames = rotateTimeline.frames; + if (time < frames[0]) { + if (pose == MixPose.Setup) bone.rotation = bone.data.rotation; + return; + } + + float r2; + 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 while detecting crosses. + float r1 = pose == MixPose.Setup ? bone.data.rotation : bone.rotation; + float total, diff = r2 - r1; + if (diff == 0) { + total = timelinesRotation[i]; + } else { + diff -= (16384 - (int)(16384.499999999996 - diff / 360)) * 360; + 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++) { + var 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 previous pose. + /// It may be desired to use to mix the skeletons back to the setup pose, + /// rather than leaving them in their previous 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 tracks, leaving skeletons in their previous pose. + /// It may be desired to use to mix the skeletons back to the setup pose, + /// rather than leaving them in their previous 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 = 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; + current.mixTime = 0; + + // Store 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 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 . + 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; + } 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 delay seconds after the current or last queued animation + /// for a track. If the track is empty, it is equivalent to calling . + /// + /// Seconds to begin this animation after the start of the previous animation. May be <= 0 to use the animation + /// duration of the previous track minus any mix duration plus the negative delay. + /// + /// A track entry to allow further customization of animation playback. References to the track entry must not be kept + /// after + 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)); + } else { + delay += duration; + } + delay -= data.GetMix(last.animation, animation); + } else + delay = 0; + } + } + + entry.delay = delay; + return entry; + } + + /// + /// Sets an empty animation for a track, discarding any queued animations, and mixes to it over the specified mix duration. + public TrackEntry SetEmptyAnimation (int trackIndex, float mixDuration) { + TrackEntry entry = SetAnimation(trackIndex, AnimationState.EmptyAnimation, false); + entry.mixDuration = mixDuration; + entry.trackEnd = mixDuration; + return entry; + } + + /// + /// Adds an empty animation to be played after the current or last queued animation for a track, and mixes to it over the + /// specified mix duration. + /// + /// A track entry to allow further customization of animation playback. References to the track entry must not be kept after . + /// + /// Track number. + /// Mix duration. + /// Seconds to begin this animation after the start of the previous animation. May be <= 0 to use the animation + /// duration of the previous track minus any mix duration plus the negative delay. + public TrackEntry AddEmptyAnimation (int trackIndex, float mixDuration, float delay) { + if (delay <= 0) delay -= mixDuration; + TrackEntry entry = AddAnimation(trackIndex, AnimationState.EmptyAnimation, false, delay); + entry.mixDuration = mixDuration; + entry.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(i, mixDuration); + } + queue.drainDisabled = oldDrainDisabled; + queue.Drain(); + } + + private TrackEntry ExpandToIndex (int index) { + if (index < tracks.Count) return tracks.Items[index]; + while (index >= tracks.Count) + tracks.Add(null); + 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.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; + + var propertyIDs = this.propertyIDs; + propertyIDs.Clear(); + var mixingTo = this.mixingTo; + + var tracksItems = tracks.Items; + for (int i = 0, n = tracks.Count; i < n; i++) { + var entry = tracksItems[i]; + if (entry != null) entry.SetTimelineData(null, mixingTo, propertyIDs); + } + } + + /// The track entry for the animation currently playing on the track, or null if no animation is currently playing. + public TrackEntry GetCurrent (int trackIndex) { + return (trackIndex >= tracks.Count) ? null : tracks.Items[trackIndex]; + } + + 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()); + } + return buffer.Length == 0 ? "" : buffer.ToString(); + } + + 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); } + } + + /// State for the playback of an animation. + public class TrackEntry : Pool.IPoolable { + internal Animation animation; + + internal TrackEntry next, mixingFrom; + internal int trackIndex; + + internal bool loop; + 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 readonly ExposedList timelineData = new ExposedList(); + internal readonly ExposedList timelineDipMix = new ExposedList(); + internal readonly ExposedList timelinesRotation = new ExposedList(); + + // IPoolable.Reset() + public void Reset () { + next = null; + mixingFrom = null; + animation = null; + timelineData.Clear(); + timelineDipMix.Clear(); + timelinesRotation.Clear(); + + Start = null; + Interrupt = null; + End = null; + Dispose = null; + Complete = null; + Event = null; + } + + /// Sets the timeline data. + /// May be null. + internal TrackEntry SetTimelineData (TrackEntry to, ExposedList mixingToArray, HashSet propertyIDs) { + if (to != null) mixingToArray.Add(to); + var lastEntry = mixingFrom != null ? mixingFrom.SetTimelineData(this, mixingToArray, propertyIDs) : this; + if (to != null) mixingToArray.Pop(); + + var mixingTo = mixingToArray.Items; + int mixingToLast = mixingToArray.Count - 1; + var timelines = animation.timelines.Items; + int timelinesCount = animation.timelines.Count; + var timelineDataItems = timelineData.Resize(timelinesCount).Items; // timelineData.setSize(timelinesCount); + timelineDipMix.Clear(); + var timelineDipMixItems = timelineDipMix.Resize(timelinesCount).Items; //timelineDipMix.setSize(timelinesCount); + + // outer: + for (int i = 0; i < timelinesCount; i++) { + int id = timelines[i].PropertyId; + if (!propertyIDs.Add(id)) { + timelineDataItems[i] = AnimationState.Subsequent; + } else if (to == null || !to.HasTimeline(id)) { + timelineDataItems[i] = AnimationState.First; + } else { + for (int ii = mixingToLast; ii >= 0; ii--) { + var entry = mixingTo[ii]; + if (!entry.HasTimeline(id)) { + if (entry.mixDuration > 0) { + timelineDataItems[i] = AnimationState.DipMix; + timelineDipMixItems[i] = entry; + goto continue_outer; // continue outer; + } + break; + } + } + timelineDataItems[i] = AnimationState.Dip; + } + continue_outer: {} + } + return lastEntry; + } + + bool HasTimeline (int id) { + var timelines = animation.timelines.Items; + for (int i = 0, n = animation.timelines.Count; i < n; i++) + if (timelines[i].PropertyId == id) return true; + return false; + } + + /// 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 a track entry is the current track entry, delay postpones incrementing + /// the track time. When a track entry is queued, delay is the time from the start of the previous animation to when the + /// track entry will become the current track entry. + 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 animation duration for + /// non-looping animations and to for looping animations. If the track end time is reached and no + /// other animations are queued for playback, and mixing from any previous animations is complete, properties keyed by the animation, + /// are set to the setup pose and the track is cleared. + /// + /// It may be desired to use to mix the properties back to the + /// setup pose over time, rather than have it happen instantly. + /// + 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 animation start 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 duration. + 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 animation last time (exclusive) and animation time + /// (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 animation time between . and + /// . When the track time is 0, the animation time is equal to the animation start 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 the animation state is updated, causing time for this animation to play slower or + /// faster. Defaults to 1. + /// + public float TimeScale { get { return timeScale; } set { timeScale = value; } } + + /// + /// Values less than 1 mix this animation with the last skeleton pose. Defaults to 1, which overwrites the last skeleton pose with + /// this animation. + /// + /// Typically track 0 is used to completely pose the skeleton, then alpha can be 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 (mix time / mix duration) is less than the event threshold, event timelines for the animation + /// being mixed out will be applied. Defaults to 0, so event timelines are not applied for an animation being mixed out. + public float EventThreshold { get { return eventThreshold; } set { eventThreshold = value; } } + + /// + /// When the mix percentage (mix time / mix duration) is less than the attachment threshold, attachment timelines for the + /// animation being mixed out will be applied. Defaults to 0, so attachment timelines are not applied for an animation being + /// mixed out. + public float AttachmentThreshold { get { return attachmentThreshold; } set { attachmentThreshold = value; } } + + /// + /// When the mix percentage (mix time / mix duration) is less than the draw order threshold, draw order timelines for the + /// animation being mixed out will be applied. Defaults to 0, so draw order timelines are not applied for an animation being + /// mixed out. + /// + public float DrawOrderThreshold { get { return drawOrderThreshold; } set { drawOrderThreshold = value; } } + + /// + /// The animation queued to start after this animation, or null. + 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 mix duration when mixing from the previous animation to this animation. May be slightly more than + /// 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 + /// based on the animation before this animation (if any). + /// + /// The mix duration can be set manually rather than use the value from AnimationStateData.GetMix. + /// In that case, the mixDuration must be set before is next called. + /// + /// When using with a + /// delay less than or equal to 0, note the is set using the mix duration from the + /// + /// + /// + public float MixDuration { get { return mixDuration; } set { mixDuration = value; } } + + /// + /// 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; } } + + 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); } + + /// + /// 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 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/SpineRuntime36/src/AnimationStateData.cs b/SpineRuntimes/SpineRuntime36/src/AnimationStateData.cs new file mode 100644 index 0000000..a4b57a6 --- /dev/null +++ b/SpineRuntimes/SpineRuntime36/src/AnimationStateData.cs @@ -0,0 +1,115 @@ +/****************************************************************************** + * Spine Runtimes Software License v2.5 + * + * Copyright (c) 2013-2016, Esoteric Software + * All rights reserved. + * + * You are granted a perpetual, non-exclusive, non-sublicensable, and + * non-transferable license to use, install, execute, and perform the Spine + * Runtimes software and derivative works solely for personal or internal + * use. Without the written permission of Esoteric Software (see Section 2 of + * the Spine Software License Agreement), you may not (a) modify, translate, + * adapt, or develop new applications using the Spine Runtimes or otherwise + * create derivative works or improvements of the Spine Runtimes or (b) remove, + * delete, alter, or obscure any trademarks or any copyright, trademark, patent, + * or other intellectual property or proprietary rights notices on or in the + * Software, including any copy thereof. Redistributions in binary or source + * form must include this license and terms. + * + * THIS SOFTWARE IS PROVIDED BY ESOTERIC SOFTWARE "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 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 SpineRuntime36 { + + /// 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/SpineRuntime36/src/Atlas.cs b/SpineRuntimes/SpineRuntime36/src/Atlas.cs new file mode 100644 index 0000000..ce09f12 --- /dev/null +++ b/SpineRuntimes/SpineRuntime36/src/Atlas.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. + *****************************************************************************/ + +#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 SpineRuntime36 { + 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 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 interface TextureLoader { + void Load (AtlasPage page, string path); + void Unload (Object texture); + } +} diff --git a/SpineRuntimes/SpineRuntime36/src/Attachments/AtlasAttachmentLoader.cs b/SpineRuntimes/SpineRuntime36/src/Attachments/AtlasAttachmentLoader.cs new file mode 100644 index 0000000..9eda5f5 --- /dev/null +++ b/SpineRuntimes/SpineRuntime36/src/Attachments/AtlasAttachmentLoader.cs @@ -0,0 +1,109 @@ +/****************************************************************************** + * Spine Runtimes Software License v2.5 + * + * Copyright (c) 2013-2016, Esoteric Software + * All rights reserved. + * + * You are granted a perpetual, non-exclusive, non-sublicensable, and + * non-transferable license to use, install, execute, and perform the Spine + * Runtimes software and derivative works solely for personal or internal + * use. Without the written permission of Esoteric Software (see Section 2 of + * the Spine Software License Agreement), you may not (a) modify, translate, + * adapt, or develop new applications using the Spine Runtimes or otherwise + * create derivative works or improvements of the Spine Runtimes or (b) remove, + * delete, alter, or obscure any trademarks or any copyright, trademark, patent, + * or other intellectual property or proprietary rights notices on or in the + * Software, including any copy thereof. Redistributions in binary or source + * form must include this license and terms. + * + * THIS SOFTWARE IS PROVIDED BY ESOTERIC SOFTWARE "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 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 SpineRuntime36 { + + /// + /// 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/SpineRuntime36/src/Attachments/Attachment.cs b/SpineRuntimes/SpineRuntime36/src/Attachments/Attachment.cs new file mode 100644 index 0000000..eed5bf1 --- /dev/null +++ b/SpineRuntimes/SpineRuntime36/src/Attachments/Attachment.cs @@ -0,0 +1,50 @@ +/****************************************************************************** + * Spine Runtimes Software License v2.5 + * + * Copyright (c) 2013-2016, Esoteric Software + * All rights reserved. + * + * You are granted a perpetual, non-exclusive, non-sublicensable, and + * non-transferable license to use, install, execute, and perform the Spine + * Runtimes software and derivative works solely for personal or internal + * use. Without the written permission of Esoteric Software (see Section 2 of + * the Spine Software License Agreement), you may not (a) modify, translate, + * adapt, or develop new applications using the Spine Runtimes or otherwise + * create derivative works or improvements of the Spine Runtimes or (b) remove, + * delete, alter, or obscure any trademarks or any copyright, trademark, patent, + * or other intellectual property or proprietary rights notices on or in the + * Software, including any copy thereof. Redistributions in binary or source + * form must include this license and terms. + * + * THIS SOFTWARE IS PROVIDED BY ESOTERIC SOFTWARE "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 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 SpineRuntime36 { + 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; } + } +} diff --git a/SpineRuntimes/SpineRuntime36/src/Attachments/AttachmentLoader.cs b/SpineRuntimes/SpineRuntime36/src/Attachments/AttachmentLoader.cs new file mode 100644 index 0000000..081cbb0 --- /dev/null +++ b/SpineRuntimes/SpineRuntime36/src/Attachments/AttachmentLoader.cs @@ -0,0 +1,49 @@ +/****************************************************************************** + * Spine Runtimes Software License v2.5 + * + * Copyright (c) 2013-2016, Esoteric Software + * All rights reserved. + * + * You are granted a perpetual, non-exclusive, non-sublicensable, and + * non-transferable license to use, install, execute, and perform the Spine + * Runtimes software and derivative works solely for personal or internal + * use. Without the written permission of Esoteric Software (see Section 2 of + * the Spine Software License Agreement), you may not (a) modify, translate, + * adapt, or develop new applications using the Spine Runtimes or otherwise + * create derivative works or improvements of the Spine Runtimes or (b) remove, + * delete, alter, or obscure any trademarks or any copyright, trademark, patent, + * or other intellectual property or proprietary rights notices on or in the + * Software, including any copy thereof. Redistributions in binary or source + * form must include this license and terms. + * + * THIS SOFTWARE IS PROVIDED BY ESOTERIC SOFTWARE "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 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 SpineRuntime36 { + 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/SpineRuntime36/src/Attachments/AttachmentType.cs b/SpineRuntimes/SpineRuntime36/src/Attachments/AttachmentType.cs new file mode 100644 index 0000000..1cb75ae --- /dev/null +++ b/SpineRuntimes/SpineRuntime36/src/Attachments/AttachmentType.cs @@ -0,0 +1,35 @@ +/****************************************************************************** + * Spine Runtimes Software License v2.5 + * + * Copyright (c) 2013-2016, Esoteric Software + * All rights reserved. + * + * You are granted a perpetual, non-exclusive, non-sublicensable, and + * non-transferable license to use, install, execute, and perform the Spine + * Runtimes software and derivative works solely for personal or internal + * use. Without the written permission of Esoteric Software (see Section 2 of + * the Spine Software License Agreement), you may not (a) modify, translate, + * adapt, or develop new applications using the Spine Runtimes or otherwise + * create derivative works or improvements of the Spine Runtimes or (b) remove, + * delete, alter, or obscure any trademarks or any copyright, trademark, patent, + * or other intellectual property or proprietary rights notices on or in the + * Software, including any copy thereof. Redistributions in binary or source + * form must include this license and terms. + * + * THIS SOFTWARE IS PROVIDED BY ESOTERIC SOFTWARE "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 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 SpineRuntime36 { + public enum AttachmentType { + Region, Boundingbox, Mesh, Linkedmesh, Path, Point, Clipping + } +} diff --git a/SpineRuntimes/SpineRuntime36/src/Attachments/BoundingBoxAttachment.cs b/SpineRuntimes/SpineRuntime36/src/Attachments/BoundingBoxAttachment.cs new file mode 100644 index 0000000..6c2e735 --- /dev/null +++ b/SpineRuntimes/SpineRuntime36/src/Attachments/BoundingBoxAttachment.cs @@ -0,0 +1,40 @@ +/****************************************************************************** + * Spine Runtimes Software License v2.5 + * + * Copyright (c) 2013-2016, Esoteric Software + * All rights reserved. + * + * You are granted a perpetual, non-exclusive, non-sublicensable, and + * non-transferable license to use, install, execute, and perform the Spine + * Runtimes software and derivative works solely for personal or internal + * use. Without the written permission of Esoteric Software (see Section 2 of + * the Spine Software License Agreement), you may not (a) modify, translate, + * adapt, or develop new applications using the Spine Runtimes or otherwise + * create derivative works or improvements of the Spine Runtimes or (b) remove, + * delete, alter, or obscure any trademarks or any copyright, trademark, patent, + * or other intellectual property or proprietary rights notices on or in the + * Software, including any copy thereof. Redistributions in binary or source + * form must include this license and terms. + * + * THIS SOFTWARE IS PROVIDED BY ESOTERIC SOFTWARE "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 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 SpineRuntime36 { + /// Attachment that has a polygon for bounds checking. + public class BoundingBoxAttachment : VertexAttachment { + public BoundingBoxAttachment (string name) + : base(name) { + } + } +} diff --git a/SpineRuntimes/SpineRuntime36/src/Attachments/ClippingAttachment.cs b/SpineRuntimes/SpineRuntime36/src/Attachments/ClippingAttachment.cs new file mode 100644 index 0000000..7c305be --- /dev/null +++ b/SpineRuntimes/SpineRuntime36/src/Attachments/ClippingAttachment.cs @@ -0,0 +1,42 @@ +/****************************************************************************** + * Spine Runtimes Software License v2.5 + * + * Copyright (c) 2013-2016, Esoteric Software + * All rights reserved. + * + * You are granted a perpetual, non-exclusive, non-sublicensable, and + * non-transferable license to use, install, execute, and perform the Spine + * Runtimes software and derivative works solely for personal or internal + * use. Without the written permission of Esoteric Software (see Section 2 of + * the Spine Software License Agreement), you may not (a) modify, translate, + * adapt, or develop new applications using the Spine Runtimes or otherwise + * create derivative works or improvements of the Spine Runtimes or (b) remove, + * delete, alter, or obscure any trademarks or any copyright, trademark, patent, + * or other intellectual property or proprietary rights notices on or in the + * Software, including any copy thereof. Redistributions in binary or source + * form must include this license and terms. + * + * THIS SOFTWARE IS PROVIDED BY ESOTERIC SOFTWARE "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 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 SpineRuntime36 { + 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/SpineRuntime36/src/Attachments/MeshAttachment.cs b/SpineRuntimes/SpineRuntime36/src/Attachments/MeshAttachment.cs new file mode 100644 index 0000000..af386f6 --- /dev/null +++ b/SpineRuntimes/SpineRuntime36/src/Attachments/MeshAttachment.cs @@ -0,0 +1,120 @@ +/****************************************************************************** + * Spine Runtimes Software License v2.5 + * + * Copyright (c) 2013-2016, Esoteric Software + * All rights reserved. + * + * You are granted a perpetual, non-exclusive, non-sublicensable, and + * non-transferable license to use, install, execute, and perform the Spine + * Runtimes software and derivative works solely for personal or internal + * use. Without the written permission of Esoteric Software (see Section 2 of + * the Spine Software License Agreement), you may not (a) modify, translate, + * adapt, or develop new applications using the Spine Runtimes or otherwise + * create derivative works or improvements of the Spine Runtimes or (b) remove, + * delete, alter, or obscure any trademarks or any copyright, trademark, patent, + * or other intellectual property or proprietary rights notices on or in the + * Software, including any copy thereof. Redistributions in binary or source + * form must include this license and terms. + * + * THIS SOFTWARE IS PROVIDED BY ESOTERIC SOFTWARE "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 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 SpineRuntime36 { + /// 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 u = RegionU, v = RegionV, width = RegionU2 - RegionU, height = RegionV2 - RegionV; + 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) { + 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 { + 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/SpineRuntime36/src/Attachments/PathAttachment.cs b/SpineRuntimes/SpineRuntime36/src/Attachments/PathAttachment.cs new file mode 100644 index 0000000..dd811cc --- /dev/null +++ b/SpineRuntimes/SpineRuntime36/src/Attachments/PathAttachment.cs @@ -0,0 +1,48 @@ +/****************************************************************************** + * Spine Runtimes Software License v2.5 + * + * Copyright (c) 2013-2016, Esoteric Software + * All rights reserved. + * + * You are granted a perpetual, non-exclusive, non-sublicensable, and + * non-transferable license to use, install, execute, and perform the Spine + * Runtimes software and derivative works solely for personal or internal + * use. Without the written permission of Esoteric Software (see Section 2 of + * the Spine Software License Agreement), you may not (a) modify, translate, + * adapt, or develop new applications using the Spine Runtimes or otherwise + * create derivative works or improvements of the Spine Runtimes or (b) remove, + * delete, alter, or obscure any trademarks or any copyright, trademark, patent, + * or other intellectual property or proprietary rights notices on or in the + * Software, including any copy thereof. Redistributions in binary or source + * form must include this license and terms. + * + * THIS SOFTWARE IS PROVIDED BY ESOTERIC SOFTWARE "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 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 SpineRuntime36 { + 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/SpineRuntime36/src/Attachments/PointAttachment.cs b/SpineRuntimes/SpineRuntime36/src/Attachments/PointAttachment.cs new file mode 100644 index 0000000..e667b2c --- /dev/null +++ b/SpineRuntimes/SpineRuntime36/src/Attachments/PointAttachment.cs @@ -0,0 +1,61 @@ +/****************************************************************************** + * Spine Runtimes Software License v2.5 + * + * Copyright (c) 2013-2016, Esoteric Software + * All rights reserved. + * + * You are granted a perpetual, non-exclusive, non-sublicensable, and + * non-transferable license to use, install, execute, and perform the Spine + * Runtimes software and derivative works solely for personal or internal + * use. Without the written permission of Esoteric Software (see Section 2 of + * the Spine Software License Agreement), you may not (a) modify, translate, + * adapt, or develop new applications using the Spine Runtimes or otherwise + * create derivative works or improvements of the Spine Runtimes or (b) remove, + * delete, alter, or obscure any trademarks or any copyright, trademark, patent, + * or other intellectual property or proprietary rights notices on or in the + * Software, including any copy thereof. Redistributions in binary or source + * form must include this license and terms. + * + * THIS SOFTWARE IS PROVIDED BY ESOTERIC SOFTWARE "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 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 SpineRuntime36 { + /// + /// 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/SpineRuntime36/src/Attachments/RegionAttachment.cs b/SpineRuntimes/SpineRuntime36/src/Attachments/RegionAttachment.cs new file mode 100644 index 0000000..2b5fb52 --- /dev/null +++ b/SpineRuntimes/SpineRuntime36/src/Attachments/RegionAttachment.cs @@ -0,0 +1,183 @@ +/****************************************************************************** + * Spine Runtimes Software License v2.5 + * + * Copyright (c) 2013-2016, Esoteric Software + * All rights reserved. + * + * You are granted a perpetual, non-exclusive, non-sublicensable, and + * non-transferable license to use, install, execute, and perform the Spine + * Runtimes software and derivative works solely for personal or internal + * use. Without the written permission of Esoteric Software (see Section 2 of + * the Spine Software License Agreement), you may not (a) modify, translate, + * adapt, or develop new applications using the Spine Runtimes or otherwise + * create derivative works or improvements of the Spine Runtimes or (b) remove, + * delete, alter, or obscure any trademarks or any copyright, trademark, patent, + * or other intellectual property or proprietary rights notices on or in the + * Software, including any copy thereof. Redistributions in binary or source + * form must include this license and terms. + * + * THIS SOFTWARE IS PROVIDED BY ESOTERIC SOFTWARE "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 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 SpineRuntime36 { + /// 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/SpineRuntime36/src/Attachments/VertexAttachment.cs b/SpineRuntimes/SpineRuntime36/src/Attachments/VertexAttachment.cs new file mode 100644 index 0000000..3df433a --- /dev/null +++ b/SpineRuntimes/SpineRuntime36/src/Attachments/VertexAttachment.cs @@ -0,0 +1,130 @@ +/****************************************************************************** + * Spine Runtimes Software License v2.5 + * + * Copyright (c) 2013-2016, Esoteric Software + * All rights reserved. + * + * You are granted a perpetual, non-exclusive, non-sublicensable, and + * non-transferable license to use, install, execute, and perform the Spine + * Runtimes software and derivative works solely for personal or internal + * use. Without the written permission of Esoteric Software (see Section 2 of + * the Spine Software License Agreement), you may not (a) modify, translate, + * adapt, or develop new applications using the Spine Runtimes or otherwise + * create derivative works or improvements of the Spine Runtimes or (b) remove, + * delete, alter, or obscure any trademarks or any copyright, trademark, patent, + * or other intellectual property or proprietary rights notices on or in the + * Software, including any copy thereof. Redistributions in binary or source + * form must include this license and terms. + * + * THIS SOFTWARE IS PROVIDED BY ESOTERIC SOFTWARE "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 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 SpineRuntime36 { + /// >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/SpineRuntime36/src/BlendMode.cs b/SpineRuntimes/SpineRuntime36/src/BlendMode.cs new file mode 100644 index 0000000..b53be3d --- /dev/null +++ b/SpineRuntimes/SpineRuntime36/src/BlendMode.cs @@ -0,0 +1,35 @@ +/****************************************************************************** + * Spine Runtimes Software License v2.5 + * + * Copyright (c) 2013-2016, Esoteric Software + * All rights reserved. + * + * You are granted a perpetual, non-exclusive, non-sublicensable, and + * non-transferable license to use, install, execute, and perform the Spine + * Runtimes software and derivative works solely for personal or internal + * use. Without the written permission of Esoteric Software (see Section 2 of + * the Spine Software License Agreement), you may not (a) modify, translate, + * adapt, or develop new applications using the Spine Runtimes or otherwise + * create derivative works or improvements of the Spine Runtimes or (b) remove, + * delete, alter, or obscure any trademarks or any copyright, trademark, patent, + * or other intellectual property or proprietary rights notices on or in the + * Software, including any copy thereof. Redistributions in binary or source + * form must include this license and terms. + * + * THIS SOFTWARE IS PROVIDED BY ESOTERIC SOFTWARE "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 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 SpineRuntime36 { + public enum BlendMode { + Normal, Additive, Multiply, Screen + } +} diff --git a/SpineRuntimes/SpineRuntime36/src/Bone.cs b/SpineRuntimes/SpineRuntime36/src/Bone.cs new file mode 100644 index 0000000..d21cce8 --- /dev/null +++ b/SpineRuntimes/SpineRuntime36/src/Bone.cs @@ -0,0 +1,384 @@ +/****************************************************************************** + * Spine Runtimes Software License v2.5 + * + * Copyright (c) 2013-2016, Esoteric Software + * All rights reserved. + * + * You are granted a perpetual, non-exclusive, non-sublicensable, and + * non-transferable license to use, install, execute, and perform the Spine + * Runtimes software and derivative works solely for personal or internal + * use. Without the written permission of Esoteric Software (see Section 2 of + * the Spine Software License Agreement), you may not (a) modify, translate, + * adapt, or develop new applications using the Spine Runtimes or otherwise + * create derivative works or improvements of the Spine Runtimes or (b) remove, + * delete, alter, or obscure any trademarks or any copyright, trademark, patent, + * or other intellectual property or proprietary rights notices on or in the + * Software, including any copy thereof. Redistributions in binary or source + * form must include this license and terms. + * + * THIS SOFTWARE IS PROVIDED BY ESOTERIC SOFTWARE "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 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 SpineRuntime36 { + /// + /// 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 float worldSignX, worldSignY; +// public float WorldSignX { get { return worldSignX; } } +// public float WorldSignY { get { return worldSignY; } } + + 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; + float la = MathUtils.CosDeg(rotation + shearX) * scaleX; + float lb = MathUtils.CosDeg(rotationY) * scaleY; + float lc = MathUtils.SinDeg(rotation + shearX) * scaleX; + float ld = MathUtils.SinDeg(rotationY) * scaleY; + if (skeleton.flipX) { + x = -x; + la = -la; + lb = -lb; + } + if (skeleton.flipY != yDown) { + y = -y; + lc = -lc; + ld = -ld; + } + a = la; + b = lb; + c = lc; + d = ld; + worldX = x + skeleton.x; + worldY = y + skeleton.y; + 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; + float zc = pc * cos + pd * sin; + 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); + 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; + if (data.transformMode != TransformMode.NoScaleOrReflection? pa * pd - pb* pc< 0 : skeleton.flipX != skeleton.flipY) { + zb = -zb; + zd = -zd; + } + a = za * la + zb * lc; + b = za * lb + zb * ld; + c = zc * la + zd * lc; + d = zc * lb + zd * ld; + return; + } + } + + if (skeleton.flipX) { + a = -a; + b = -b; + } + if (skeleton.flipY != Bone.yDown) { + c = -c; + d = -d; + } + } + + 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; + } + + public float LocalToWorldRotation (float localRotation) { + 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/SpineRuntime36/src/BoneData.cs b/SpineRuntimes/SpineRuntime36/src/BoneData.cs new file mode 100644 index 0000000..4ec7691 --- /dev/null +++ b/SpineRuntimes/SpineRuntime36/src/BoneData.cs @@ -0,0 +1,100 @@ +/****************************************************************************** + * Spine Runtimes Software License v2.5 + * + * Copyright (c) 2013-2016, Esoteric Software + * All rights reserved. + * + * You are granted a perpetual, non-exclusive, non-sublicensable, and + * non-transferable license to use, install, execute, and perform the Spine + * Runtimes software and derivative works solely for personal or internal + * use. Without the written permission of Esoteric Software (see Section 2 of + * the Spine Software License Agreement), you may not (a) modify, translate, + * adapt, or develop new applications using the Spine Runtimes or otherwise + * create derivative works or improvements of the Spine Runtimes or (b) remove, + * delete, alter, or obscure any trademarks or any copyright, trademark, patent, + * or other intellectual property or proprietary rights notices on or in the + * Software, including any copy thereof. Redistributions in binary or source + * form must include this license and terms. + * + * THIS SOFTWARE IS PROVIDED BY ESOTERIC SOFTWARE "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 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 SpineRuntime36 { + 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/SpineRuntime36/src/Event.cs b/SpineRuntimes/SpineRuntime36/src/Event.cs new file mode 100644 index 0000000..e6913fb --- /dev/null +++ b/SpineRuntimes/SpineRuntime36/src/Event.cs @@ -0,0 +1,60 @@ +/****************************************************************************** + * Spine Runtimes Software License v2.5 + * + * Copyright (c) 2013-2016, Esoteric Software + * All rights reserved. + * + * You are granted a perpetual, non-exclusive, non-sublicensable, and + * non-transferable license to use, install, execute, and perform the Spine + * Runtimes software and derivative works solely for personal or internal + * use. Without the written permission of Esoteric Software (see Section 2 of + * the Spine Software License Agreement), you may not (a) modify, translate, + * adapt, or develop new applications using the Spine Runtimes or otherwise + * create derivative works or improvements of the Spine Runtimes or (b) remove, + * delete, alter, or obscure any trademarks or any copyright, trademark, patent, + * or other intellectual property or proprietary rights notices on or in the + * Software, including any copy thereof. Redistributions in binary or source + * form must include this license and terms. + * + * THIS SOFTWARE IS PROVIDED BY ESOTERIC SOFTWARE "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 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 SpineRuntime36 { + /// 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; + + 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 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/SpineRuntime36/src/EventData.cs b/SpineRuntimes/SpineRuntime36/src/EventData.cs new file mode 100644 index 0000000..b761031 --- /dev/null +++ b/SpineRuntimes/SpineRuntime36/src/EventData.cs @@ -0,0 +1,53 @@ +/****************************************************************************** + * Spine Runtimes Software License v2.5 + * + * Copyright (c) 2013-2016, Esoteric Software + * All rights reserved. + * + * You are granted a perpetual, non-exclusive, non-sublicensable, and + * non-transferable license to use, install, execute, and perform the Spine + * Runtimes software and derivative works solely for personal or internal + * use. Without the written permission of Esoteric Software (see Section 2 of + * the Spine Software License Agreement), you may not (a) modify, translate, + * adapt, or develop new applications using the Spine Runtimes or otherwise + * create derivative works or improvements of the Spine Runtimes or (b) remove, + * delete, alter, or obscure any trademarks or any copyright, trademark, patent, + * or other intellectual property or proprietary rights notices on or in the + * Software, including any copy thereof. Redistributions in binary or source + * form must include this license and terms. + * + * THIS SOFTWARE IS PROVIDED BY ESOTERIC SOFTWARE "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 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 SpineRuntime36 { + /// 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 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/SpineRuntime36/src/ExposedList.cs b/SpineRuntimes/SpineRuntime36/src/ExposedList.cs new file mode 100644 index 0000000..cc321e2 --- /dev/null +++ b/SpineRuntimes/SpineRuntime36/src/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 SpineRuntime36 { + [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 newCount) { + int minimumSize = Count + newCount; + 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/SpineRuntime36/src/IConstraint.cs b/SpineRuntimes/SpineRuntime36/src/IConstraint.cs new file mode 100644 index 0000000..e4cdca8 --- /dev/null +++ b/SpineRuntimes/SpineRuntime36/src/IConstraint.cs @@ -0,0 +1,40 @@ +/****************************************************************************** + * Spine Runtimes Software License v2.5 + * + * Copyright (c) 2013-2016, Esoteric Software + * All rights reserved. + * + * You are granted a perpetual, non-exclusive, non-sublicensable, and + * non-transferable license to use, install, execute, and perform the Spine + * Runtimes software and derivative works solely for personal or internal + * use. Without the written permission of Esoteric Software (see Section 2 of + * the Spine Software License Agreement), you may not (a) modify, translate, + * adapt, or develop new applications using the Spine Runtimes or otherwise + * create derivative works or improvements of the Spine Runtimes or (b) remove, + * delete, alter, or obscure any trademarks or any copyright, trademark, patent, + * or other intellectual property or proprietary rights notices on or in the + * Software, including any copy thereof. Redistributions in binary or source + * form must include this license and terms. + * + * THIS SOFTWARE IS PROVIDED BY ESOTERIC SOFTWARE "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 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 SpineRuntime36 { + + /// The interface for all constraints. + public interface IConstraint : IUpdatable { + /// The ordinal for the order a skeleton's constraints will be applied. + int Order { get; } + + } + +} \ No newline at end of file diff --git a/SpineRuntimes/SpineRuntime36/src/IUpdatable.cs b/SpineRuntimes/SpineRuntime36/src/IUpdatable.cs new file mode 100644 index 0000000..1cb6596 --- /dev/null +++ b/SpineRuntimes/SpineRuntime36/src/IUpdatable.cs @@ -0,0 +1,35 @@ +/****************************************************************************** + * Spine Runtimes Software License v2.5 + * + * Copyright (c) 2013-2016, Esoteric Software + * All rights reserved. + * + * You are granted a perpetual, non-exclusive, non-sublicensable, and + * non-transferable license to use, install, execute, and perform the Spine + * Runtimes software and derivative works solely for personal or internal + * use. Without the written permission of Esoteric Software (see Section 2 of + * the Spine Software License Agreement), you may not (a) modify, translate, + * adapt, or develop new applications using the Spine Runtimes or otherwise + * create derivative works or improvements of the Spine Runtimes or (b) remove, + * delete, alter, or obscure any trademarks or any copyright, trademark, patent, + * or other intellectual property or proprietary rights notices on or in the + * Software, including any copy thereof. Redistributions in binary or source + * form must include this license and terms. + * + * THIS SOFTWARE IS PROVIDED BY ESOTERIC SOFTWARE "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 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 SpineRuntime36 { + public interface IUpdatable { + void Update (); + } +} diff --git a/SpineRuntimes/SpineRuntime36/src/IkConstraint.cs b/SpineRuntimes/SpineRuntime36/src/IkConstraint.cs new file mode 100644 index 0000000..7f0b6e4 --- /dev/null +++ b/SpineRuntimes/SpineRuntime36/src/IkConstraint.cs @@ -0,0 +1,227 @@ +/****************************************************************************** + * Spine Runtimes Software License v2.5 + * + * Copyright (c) 2013-2016, Esoteric Software + * All rights reserved. + * + * You are granted a perpetual, non-exclusive, non-sublicensable, and + * non-transferable license to use, install, execute, and perform the Spine + * Runtimes software and derivative works solely for personal or internal + * use. Without the written permission of Esoteric Software (see Section 2 of + * the Spine Software License Agreement), you may not (a) modify, translate, + * adapt, or develop new applications using the Spine Runtimes or otherwise + * create derivative works or improvements of the Spine Runtimes or (b) remove, + * delete, alter, or obscure any trademarks or any copyright, trademark, patent, + * or other intellectual property or proprietary rights notices on or in the + * Software, including any copy thereof. Redistributions in binary or source + * form must include this license and terms. + * + * THIS SOFTWARE IS PROVIDED BY ESOTERIC SOFTWARE "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 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 SpineRuntime36 { + public class IkConstraint : IConstraint { + internal IkConstraintData data; + internal ExposedList bones = new ExposedList(); + internal Bone target; + internal float mix; + internal int bendDirection; + + public IkConstraintData Data { get { return data; } } + public int Order { get { return data.order; } } + public ExposedList Bones { get { return bones; } } + public Bone Target { get { return target; } set { target = value; } } + public int BendDirection { get { return bendDirection; } set { bendDirection = value; } } + public float Mix { get { return mix; } set { mix = value; } } + + 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; + + bones = new ExposedList(data.bones.Count); + foreach (BoneData boneData in data.bones) + bones.Add(skeleton.FindBone(boneData.name)); + target = skeleton.FindBone(data.target.name); + } + + /// 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, mix); + break; + case 2: + Apply(bones.Items[0], bones.Items[1], target.worldX, target.worldY, bendDirection, mix); + break; + } + } + + override public string ToString () { + return data.name; + } + + /// Adjusts the bone rotation so the tip is as close to the target position as possible. The target is specified + /// in the world coordinate system. + static public void Apply (Bone bone, float targetX, float targetY, 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; + bone.UpdateWorldTransform(bone.ax, bone.ay, bone.arotation + rotationIK * alpha, bone.ascaleX, bone.ascaleY, bone.ashearX, + bone.ashearY); + } + + /// Adjusts the parent and child bone rotations so the tip of the child is as close to the target position as + /// possible. 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, float alpha) { + if (alpha == 0) { + child.UpdateWorldTransform (); + return; + } + //float px = parent.x, py = parent.y, psx = parent.scaleX, psy = parent.scaleY, csx = child.scaleX; + if (!parent.appliedValid) parent.UpdateAppliedTransform(); + if (!child.appliedValid) child.UpdateAppliedTransform(); + float px = parent.ax, py = parent.ay, psx = parent.ascaleX, 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; + 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 = (tx * tx + ty * ty - l1 * l1 - l2 * l2) / (2 * l1 * l2); + if (cos < -1) + cos = -1; + else if (cos > 1) cos = 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, dd = tx * tx + ty * ty, 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, parent.scaleX, 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/SpineRuntime36/src/IkConstraintData.cs b/SpineRuntimes/SpineRuntime36/src/IkConstraintData.cs new file mode 100644 index 0000000..f75c8b7 --- /dev/null +++ b/SpineRuntimes/SpineRuntime36/src/IkConstraintData.cs @@ -0,0 +1,82 @@ +/****************************************************************************** + * Spine Runtimes Software License v2.5 + * + * Copyright (c) 2013-2016, Esoteric Software + * All rights reserved. + * + * You are granted a perpetual, non-exclusive, non-sublicensable, and + * non-transferable license to use, install, execute, and perform the Spine + * Runtimes software and derivative works solely for personal or internal + * use. Without the written permission of Esoteric Software (see Section 2 of + * the Spine Software License Agreement), you may not (a) modify, translate, + * adapt, or develop new applications using the Spine Runtimes or otherwise + * create derivative works or improvements of the Spine Runtimes or (b) remove, + * delete, alter, or obscure any trademarks or any copyright, trademark, patent, + * or other intellectual property or proprietary rights notices on or in the + * Software, including any copy thereof. Redistributions in binary or source + * form must include this license and terms. + * + * THIS SOFTWARE IS PROVIDED BY ESOTERIC SOFTWARE "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 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 SpineRuntime36 { + /// 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 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; } + } + + /// Controls the bend direction of the IK bones, either 1 or -1. + public int BendDirection { + get { return bendDirection; } + set { bendDirection = value; } + } + + public float Mix { get { return mix; } set { mix = 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/SpineRuntime36/src/Json.cs b/SpineRuntimes/SpineRuntime36/src/Json.cs new file mode 100644 index 0000000..b56f1c5 --- /dev/null +++ b/SpineRuntimes/SpineRuntime36/src/Json.cs @@ -0,0 +1,535 @@ +/****************************************************************************** + * Spine Runtimes Software License v2.5 + * + * Copyright (c) 2013-2016, Esoteric Software + * All rights reserved. + * + * You are granted a perpetual, non-exclusive, non-sublicensable, and + * non-transferable license to use, install, execute, and perform the Spine + * Runtimes software and derivative works solely for personal or internal + * use. Without the written permission of Esoteric Software (see Section 2 of + * the Spine Software License Agreement), you may not (a) modify, translate, + * adapt, or develop new applications using the Spine Runtimes or otherwise + * create derivative works or improvements of the Spine Runtimes or (b) remove, + * delete, alter, or obscure any trademarks or any copyright, trademark, patent, + * or other intellectual property or proprietary rights notices on or in the + * Software, including any copy thereof. Redistributions in binary or source + * form must include this license and terms. + * + * THIS SOFTWARE IS PROVIDED BY ESOTERIC SOFTWARE "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 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 SpineRuntime36 { + 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/SpineRuntime36/src/MathUtils.cs b/SpineRuntimes/SpineRuntime36/src/MathUtils.cs new file mode 100644 index 0000000..8f6ed2d --- /dev/null +++ b/SpineRuntimes/SpineRuntime36/src/MathUtils.cs @@ -0,0 +1,100 @@ +/****************************************************************************** + * Spine Runtimes Software License v2.5 + * + * Copyright (c) 2013-2016, Esoteric Software + * All rights reserved. + * + * You are granted a perpetual, non-exclusive, non-sublicensable, and + * non-transferable license to use, install, execute, and perform the Spine + * Runtimes software and derivative works solely for personal or internal + * use. Without the written permission of Esoteric Software (see Section 2 of + * the Spine Software License Agreement), you may not (a) modify, translate, + * adapt, or develop new applications using the Spine Runtimes or otherwise + * create derivative works or improvements of the Spine Runtimes or (b) remove, + * delete, alter, or obscure any trademarks or any copyright, trademark, patent, + * or other intellectual property or proprietary rights notices on or in the + * Software, including any copy thereof. Redistributions in binary or source + * form must include this license and terms. + * + * THIS SOFTWARE IS PROVIDED BY ESOTERIC SOFTWARE "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 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 SpineRuntime36 { + 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 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; + } + } +} diff --git a/SpineRuntimes/SpineRuntime36/src/PathConstraint.cs b/SpineRuntimes/SpineRuntime36/src/PathConstraint.cs new file mode 100644 index 0000000..36ab3e8 --- /dev/null +++ b/SpineRuntimes/SpineRuntime36/src/PathConstraint.cs @@ -0,0 +1,417 @@ +/****************************************************************************** + * Spine Runtimes Software License v2.5 + * + * Copyright (c) 2013-2016, Esoteric Software + * All rights reserved. + * + * You are granted a perpetual, non-exclusive, non-sublicensable, and + * non-transferable license to use, install, execute, and perform the Spine + * Runtimes software and derivative works solely for personal or internal + * use. Without the written permission of Esoteric Software (see Section 2 of + * the Spine Software License Agreement), you may not (a) modify, translate, + * adapt, or develop new applications using the Spine Runtimes or otherwise + * create derivative works or improvements of the Spine Runtimes or (b) remove, + * delete, alter, or obscure any trademarks or any copyright, trademark, patent, + * or other intellectual property or proprietary rights notices on or in the + * Software, including any copy thereof. Redistributions in binary or source + * form must include this license and terms. + * + * THIS SOFTWARE IS PROVIDED BY ESOTERIC SOFTWARE "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 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 SpineRuntime36 { + 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 int Order { get { return data.order; } } + 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 ExposedList Bones { get { return bones; } } + public Slot Target { get { return target; } set { target = value; } } + public PathConstraintData Data { get { return data; } } + + 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; + } + + /// 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; + SpacingMode spacingMode = data.spacingMode; + bool lengthSpacing = spacingMode == SpacingMode.Length; + 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 || lengthSpacing) { + if (scale) lengths = this.lengths.Resize(boneCount); + 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 { + 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, spacingMode == SpacingMode.Percent); + 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; + if (!path.ConstantSpeed) { + float[] lengths = path.Lengths; + curveCount -= closed ? 1 : 2; + pathLength = lengths[curveCount]; + if (percentPosition) position *= pathLength; + if (percentSpacing) { + for (int i = 0; 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); + } + AddBeforePosition(p, world, 0, output, o); + continue; + } else if (p > pathLength) { + if (prevCurve != AFTER) { + prevCurve = AFTER; + path.ComputeWorldVertices(target, verticesLength - 6, 4, world, 0); + } + 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); + path.ComputeWorldVertices(target, 0, 4, world, 4); + } else + path.ComputeWorldVertices(target, curve * 6 + 2, 8, world, 0); + } + 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); + path.ComputeWorldVertices(target, 0, 2, world, verticesLength - 4); + 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); + } + + // 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; + if (percentSpacing) { + for (int i = 0; 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)) p = PathConstraint.Epsilon; + 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) + output[o + 2] = (float)Math.Atan2(y - (y1 * uu + cy1 * ut * 2 + cy2 * tt), x - (x1 * uu + cx1 * ut * 2 + cx2 * tt)); + } + } +} diff --git a/SpineRuntimes/SpineRuntime36/src/PathConstraintData.cs b/SpineRuntimes/SpineRuntime36/src/PathConstraintData.cs new file mode 100644 index 0000000..e27940f --- /dev/null +++ b/SpineRuntimes/SpineRuntime36/src/PathConstraintData.cs @@ -0,0 +1,79 @@ +/****************************************************************************** + * Spine Runtimes Software License v2.5 + * + * Copyright (c) 2013-2016, Esoteric Software + * All rights reserved. + * + * You are granted a perpetual, non-exclusive, non-sublicensable, and + * non-transferable license to use, install, execute, and perform the Spine + * Runtimes software and derivative works solely for personal or internal + * use. Without the written permission of Esoteric Software (see Section 2 of + * the Spine Software License Agreement), you may not (a) modify, translate, + * adapt, or develop new applications using the Spine Runtimes or otherwise + * create derivative works or improvements of the Spine Runtimes or (b) remove, + * delete, alter, or obscure any trademarks or any copyright, trademark, patent, + * or other intellectual property or proprietary rights notices on or in the + * Software, including any copy thereof. Redistributions in binary or source + * form must include this license and terms. + * + * THIS SOFTWARE IS PROVIDED BY ESOTERIC SOFTWARE "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 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 SpineRuntime36 { + 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/SpineRuntime36/src/Skeleton.cs b/SpineRuntimes/SpineRuntime36/src/Skeleton.cs new file mode 100644 index 0000000..638a0c8 --- /dev/null +++ b/SpineRuntimes/SpineRuntime36/src/Skeleton.cs @@ -0,0 +1,541 @@ +/****************************************************************************** + * Spine Runtimes Software License v2.5 + * + * Copyright (c) 2013-2016, Esoteric Software + * All rights reserved. + * + * You are granted a perpetual, non-exclusive, non-sublicensable, and + * non-transferable license to use, install, execute, and perform the Spine + * Runtimes software and derivative works solely for personal or internal + * use. Without the written permission of Esoteric Software (see Section 2 of + * the Spine Software License Agreement), you may not (a) modify, translate, + * adapt, or develop new applications using the Spine Runtimes or otherwise + * create derivative works or improvements of the Spine Runtimes or (b) remove, + * delete, alter, or obscure any trademarks or any copyright, trademark, patent, + * or other intellectual property or proprietary rights notices on or in the + * Software, including any copy thereof. Redistributions in binary or source + * form must include this license and terms. + * + * THIS SOFTWARE IS PROVIDED BY ESOTERIC SOFTWARE "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 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 SpineRuntime36 { + 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 bool flipX, flipY; + 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 bool FlipX { get { return flipX; } set { flipX = value; } } + public bool FlipY { get { return flipY; } set { flipY = value; } } + + 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 () { + ExposedList updateCache = this.updateCache; + updateCache.Clear(); + this.updateCacheReset.Clear(); + + ExposedList bones = this.bones; + for (int i = 0, n = bones.Count; i < n; i++) + bones.Items[i].sorted = false; + + ExposedList 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(); + } + + /// 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.bendDirection = constraint.data.bendDirection; + constraint.mix = constraint.data.mix; + } + + 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; + } + + /// May be null. + public Attachment GetAttachment (string slotName, string attachmentName) { + return GetAttachment(data.FindSlotIndex(slotName), attachmentName); + } + + /// 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; + } + + /// 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/SpineRuntime36/src/SkeletonBinary.cs b/SpineRuntimes/SpineRuntime36/src/SkeletonBinary.cs new file mode 100644 index 0000000..a780da7 --- /dev/null +++ b/SpineRuntimes/SpineRuntime36/src/SkeletonBinary.cs @@ -0,0 +1,893 @@ +/****************************************************************************** + * Spine Runtimes Software License v2.5 + * + * Copyright (c) 2013-2016, Esoteric Software + * All rights reserved. + * + * You are granted a perpetual, non-exclusive, non-sublicensable, and + * non-transferable license to use, install, execute, and perform the Spine + * Runtimes software and derivative works solely for personal or internal + * use. Without the written permission of Esoteric Software (see Section 2 of + * the Spine Software License Agreement), you may not (a) modify, translate, + * adapt, or develop new applications using the Spine Runtimes or otherwise + * create derivative works or improvements of the Spine Runtimes or (b) remove, + * delete, alter, or obscure any trademarks or any copyright, trademark, patent, + * or other intellectual property or proprietary rights notices on or in the + * Software, including any copy thereof. Redistributions in binary or source + * form must include this license and terms. + * + * THIS SOFTWARE IS PROVIDED BY ESOTERIC SOFTWARE "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 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 SpineRuntime36 { + 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 (skeletonData.imagesPath.Length == 0) skeletonData.imagesPath = 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); + 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); + 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); + timeline.ikConstraintIndex = index; + for (int frameIndex = 0; frameIndex < frameCount; frameIndex++) { + timeline.SetFrame(frameIndex, ReadFloat(input), ReadFloat(input), ReadSByte(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); + e.Int = ReadVarint(input, false); + e.Float = ReadFloat(input); + e.String = ReadBoolean(input) ? ReadString(input) : eventData.String; + 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/SpineRuntime36/src/SkeletonBounds.cs b/SpineRuntimes/SpineRuntime36/src/SkeletonBounds.cs new file mode 100644 index 0000000..9547722 --- /dev/null +++ b/SpineRuntimes/SpineRuntime36/src/SkeletonBounds.cs @@ -0,0 +1,234 @@ +/****************************************************************************** + * Spine Runtimes Software License v2.5 + * + * Copyright (c) 2013-2016, Esoteric Software + * All rights reserved. + * + * You are granted a perpetual, non-exclusive, non-sublicensable, and + * non-transferable license to use, install, execute, and perform the Spine + * Runtimes software and derivative works solely for personal or internal + * use. Without the written permission of Esoteric Software (see Section 2 of + * the Spine Software License Agreement), you may not (a) modify, translate, + * adapt, or develop new applications using the Spine Runtimes or otherwise + * create derivative works or improvements of the Spine Runtimes or (b) remove, + * delete, alter, or obscure any trademarks or any copyright, trademark, patent, + * or other intellectual property or proprietary rights notices on or in the + * Software, including any copy thereof. Redistributions in binary or source + * form must include this license and terms. + * + * THIS SOFTWARE IS PROVIDED BY ESOTERIC SOFTWARE "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 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 SpineRuntime36 { + + /// + /// 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/SpineRuntime36/src/SkeletonClipping.cs b/SpineRuntimes/SpineRuntime36/src/SkeletonClipping.cs new file mode 100644 index 0000000..4c0d561 --- /dev/null +++ b/SpineRuntimes/SpineRuntime36/src/SkeletonClipping.cs @@ -0,0 +1,285 @@ +/****************************************************************************** + * Spine Runtimes Software License v2.5 + * + * Copyright (c) 2013-2016, Esoteric Software + * All rights reserved. + * + * You are granted a perpetual, non-exclusive, non-sublicensable, and + * non-transferable license to use, install, execute, and perform the Spine + * Runtimes software and derivative works solely for personal or internal + * use. Without the written permission of Esoteric Software (see Section 2 of + * the Spine Software License Agreement), you may not (a) modify, translate, + * adapt, or develop new applications using the Spine Runtimes or otherwise + * create derivative works or improvements of the Spine Runtimes or (b) remove, + * delete, alter, or obscure any trademarks or any copyright, trademark, patent, + * or other intellectual property or proprietary rights notices on or in the + * Software, including any copy thereof. Redistributions in binary or source + * form must include this license and terms. + * + * THIS SOFTWARE IS PROVIDED BY ESOTERIC SOFTWARE "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 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 SpineRuntime36 { + 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 ua = (c2 * (edgeY - inputY) - c0 * (edgeX - inputX)) / (c0 * (edgeX2 - edgeX) - c2 * (edgeY2 - edgeY)); + output.Add(edgeX + (edgeX2 - edgeX) * ua); + output.Add(edgeY + (edgeY2 - edgeY) * ua); + } + else if (side2) { // v1 outside, v2 inside + float c0 = inputY2 - inputY, c2 = inputX2 - inputX; + float ua = (c2 * (edgeY - inputY) - c0 * (edgeX - inputX)) / (c0 * (edgeX2 - edgeX) - c2 * (edgeY2 - edgeY)); + output.Add(edgeX + (edgeX2 - edgeX) * ua); + output.Add(edgeY + (edgeY2 - edgeY) * ua); + 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; + } + + 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/SpineRuntime36/src/SkeletonData.cs b/SpineRuntimes/SpineRuntime36/src/SkeletonData.cs new file mode 100644 index 0000000..cb5c376 --- /dev/null +++ b/SpineRuntimes/SpineRuntime36/src/SkeletonData.cs @@ -0,0 +1,224 @@ +/****************************************************************************** + * Spine Runtimes Software License v2.5 + * + * Copyright (c) 2013-2016, Esoteric Software + * All rights reserved. + * + * You are granted a perpetual, non-exclusive, non-sublicensable, and + * non-transferable license to use, install, execute, and perform the Spine + * Runtimes software and derivative works solely for personal or internal + * use. Without the written permission of Esoteric Software (see Section 2 of + * the Spine Software License Agreement), you may not (a) modify, translate, + * adapt, or develop new applications using the Spine Runtimes or otherwise + * create derivative works or improvements of the Spine Runtimes or (b) remove, + * delete, alter, or obscure any trademarks or any copyright, trademark, patent, + * or other intellectual property or proprietary rights notices on or in the + * Software, including any copy thereof. Redistributions in binary or source + * form must include this license and terms. + * + * THIS SOFTWARE IS PROVIDED BY ESOTERIC SOFTWARE "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 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 SpineRuntime36 { + + /// 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; + + 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; } } + public string ImagesPath { get { return imagesPath; } set { imagesPath = 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/SpineRuntime36/src/SkeletonJson.cs b/SpineRuntimes/SpineRuntime36/src/SkeletonJson.cs new file mode 100644 index 0000000..f8ee2c9 --- /dev/null +++ b/SpineRuntimes/SpineRuntime36/src/SkeletonJson.cs @@ -0,0 +1,865 @@ +/****************************************************************************** + * Spine Runtimes Software License v2.5 + * + * Copyright (c) 2013-2016, Esoteric Software + * All rights reserved. + * + * You are granted a perpetual, non-exclusive, non-sublicensable, and + * non-transferable license to use, install, execute, and perform the Spine + * Runtimes software and derivative works solely for personal or internal + * use. Without the written permission of Esoteric Software (see Section 2 of + * the Spine Software License Agreement), you may not (a) modify, translate, + * adapt, or develop new applications using the Spine Runtimes or otherwise + * create derivative works or improvements of the Spine Runtimes or (b) remove, + * delete, alter, or obscure any trademarks or any copyright, trademark, patent, + * or other intellectual property or proprietary rights notices on or in the + * Software, including any copy thereof. Redistributions in binary or source + * form must include this license and terms. + * + * THIS SOFTWARE IS PROVIDED BY ESOTERIC SOFTWARE "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 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 SpineRuntime36 { + 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); + } + + // 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.bendDirection = GetBoolean(constraintMap, "bendPositive", true) ? 1 : -1; + data.mix = GetFloat(constraintMap, "mix", 1); + + 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); + 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) { + float time = (float)valueMap["time"]; + float mix = GetFloat(valueMap, "mix", 1); + bool bendPositive = GetBoolean(valueMap, "bendPositive", true); + timeline.SetFrame(frameIndex, time, mix, bendPositive ? 1 : -1); + 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); + e.Int = GetInt(eventMap, "int", eventData.Int); + e.Float = GetFloat(eventMap, "float", eventData.Float); + e.String = GetString(eventMap, "string", eventData.String); + 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/SpineRuntime36/src/Skin.cs b/SpineRuntimes/SpineRuntime36/src/Skin.cs new file mode 100644 index 0000000..5188205 --- /dev/null +++ b/SpineRuntimes/SpineRuntime36/src/Skin.cs @@ -0,0 +1,125 @@ +/****************************************************************************** + * Spine Runtimes Software License v2.5 + * + * Copyright (c) 2013-2016, Esoteric Software + * All rights reserved. + * + * You are granted a perpetual, non-exclusive, non-sublicensable, and + * non-transferable license to use, install, execute, and perform the Spine + * Runtimes software and derivative works solely for personal or internal + * use. Without the written permission of Esoteric Software (see Section 2 of + * the Spine Software License Agreement), you may not (a) modify, translate, + * adapt, or develop new applications using the Spine Runtimes or otherwise + * create derivative works or improvements of the Spine Runtimes or (b) remove, + * delete, alter, or obscure any trademarks or any copyright, trademark, patent, + * or other intellectual property or proprietary rights notices on or in the + * Software, including any copy thereof. Redistributions in binary or source + * form must include this license and terms. + * + * THIS SOFTWARE IS PROVIDED BY ESOTERIC SOFTWARE "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 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 SpineRuntime36 { + /// 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; + } + + /// 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/SpineRuntime36/src/Slot.cs b/SpineRuntimes/SpineRuntime36/src/Slot.cs new file mode 100644 index 0000000..ca7f42b --- /dev/null +++ b/SpineRuntimes/SpineRuntime36/src/Slot.cs @@ -0,0 +1,100 @@ +/****************************************************************************** + * Spine Runtimes Software License v2.5 + * + * Copyright (c) 2013-2016, Esoteric Software + * All rights reserved. + * + * You are granted a perpetual, non-exclusive, non-sublicensable, and + * non-transferable license to use, install, execute, and perform the Spine + * Runtimes software and derivative works solely for personal or internal + * use. Without the written permission of Esoteric Software (see Section 2 of + * the Spine Software License Agreement), you may not (a) modify, translate, + * adapt, or develop new applications using the Spine Runtimes or otherwise + * create derivative works or improvements of the Spine Runtimes or (b) remove, + * delete, alter, or obscure any trademarks or any copyright, trademark, patent, + * or other intellectual property or proprietary rights notices on or in the + * Software, including any copy thereof. Redistributions in binary or source + * form must include this license and terms. + * + * THIS SOFTWARE IS PROVIDED BY ESOTERIC SOFTWARE "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 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 SpineRuntime36 { + public class Slot { + internal SlotData data; + internal Bone bone; + internal float r, g, b, a; + internal float r2, g2, b2; + internal bool hasSecondColor = false; + internal Attachment attachment; + internal float attachmentTime; + internal ExposedList attachmentVertices = new ExposedList(); + + public SlotData Data { get { return data; } } + public Bone Bone { get { return bone; } } + public Skeleton Skeleton { get { return bone.skeleton; } } + 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 data.hasSecondColor; } set { data.hasSecondColor = value; } } + + /// May be null. + public Attachment Attachment { + get { return attachment; } + set { + if (attachment == value) return; + attachment = value; + attachmentTime = bone.skeleton.time; + attachmentVertices.Clear(false); + } + } + + public float AttachmentTime { + get { return bone.skeleton.time - attachmentTime; } + set { attachmentTime = bone.skeleton.time - value; } + } + + public ExposedList AttachmentVertices { get { return attachmentVertices; } set { attachmentVertices = value; } } + + 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; + SetToSetupPose(); + } + + public void SetToSetupPose () { + r = data.r; + g = data.g; + b = data.b; + a = data.a; + 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/SpineRuntime36/src/SlotData.cs b/SpineRuntimes/SpineRuntime36/src/SlotData.cs new file mode 100644 index 0000000..b32a997 --- /dev/null +++ b/SpineRuntimes/SpineRuntime36/src/SlotData.cs @@ -0,0 +1,74 @@ +/****************************************************************************** + * Spine Runtimes Software License v2.5 + * + * Copyright (c) 2013-2016, Esoteric Software + * All rights reserved. + * + * You are granted a perpetual, non-exclusive, non-sublicensable, and + * non-transferable license to use, install, execute, and perform the Spine + * Runtimes software and derivative works solely for personal or internal + * use. Without the written permission of Esoteric Software (see Section 2 of + * the Spine Software License Agreement), you may not (a) modify, translate, + * adapt, or develop new applications using the Spine Runtimes or otherwise + * create derivative works or improvements of the Spine Runtimes or (b) remove, + * delete, alter, or obscure any trademarks or any copyright, trademark, patent, + * or other intellectual property or proprietary rights notices on or in the + * Software, including any copy thereof. Redistributions in binary or source + * form must include this license and terms. + * + * THIS SOFTWARE IS PROVIDED BY ESOTERIC SOFTWARE "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 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 SpineRuntime36 { + 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/SpineRuntime36/src/TransformConstraint.cs b/SpineRuntimes/SpineRuntime36/src/TransformConstraint.cs new file mode 100644 index 0000000..68b621f --- /dev/null +++ b/SpineRuntimes/SpineRuntime36/src/TransformConstraint.cs @@ -0,0 +1,284 @@ +/****************************************************************************** + * Spine Runtimes Software License v2.5 + * + * Copyright (c) 2013-2016, Esoteric Software + * All rights reserved. + * + * You are granted a perpetual, non-exclusive, non-sublicensable, and + * non-transferable license to use, install, execute, and perform the Spine + * Runtimes software and derivative works solely for personal or internal + * use. Without the written permission of Esoteric Software (see Section 2 of + * the Spine Software License Agreement), you may not (a) modify, translate, + * adapt, or develop new applications using the Spine Runtimes or otherwise + * create derivative works or improvements of the Spine Runtimes or (b) remove, + * delete, alter, or obscure any trademarks or any copyright, trademark, patent, + * or other intellectual property or proprietary rights notices on or in the + * Software, including any copy thereof. Redistributions in binary or source + * form must include this license and terms. + * + * THIS SOFTWARE IS PROVIDED BY ESOTERIC SOFTWARE "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 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 SpineRuntime36 { + public class TransformConstraint : IConstraint { + internal TransformConstraintData data; + internal ExposedList bones; + internal Bone target; + internal float rotateMix, translateMix, scaleMix, shearMix; + + public TransformConstraintData Data { get { return data; } } + public int Order { get { return data.order; } } + public ExposedList Bones { get { return bones; } } + public Bone 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 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); + } + + 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); + //float ts = (float)Math.sqrt(ta * ta + tc * tc); + if (s > 0.00001f) 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); + //ts = (float)Math.Sqrt(tb * tb + td * td); + if (s > 0.00001f) 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.00001f) scaleX = (scaleX + (target.ascaleX - scaleX + data.offsetScaleX) * scaleMix) / scaleX; + if (scaleY > 0.00001f) 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; + bone.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) { + if (scaleX > 0.00001f) scaleX *= ((target.ascaleX - 1 + data.offsetScaleX) * scaleMix) + 1; + if (scaleY > 0.00001f) 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); + } + } + + override public string ToString () { + return data.name; + } + } +} diff --git a/SpineRuntimes/SpineRuntime36/src/TransformConstraintData.cs b/SpineRuntimes/SpineRuntime36/src/TransformConstraintData.cs new file mode 100644 index 0000000..5c94e47 --- /dev/null +++ b/SpineRuntimes/SpineRuntime36/src/TransformConstraintData.cs @@ -0,0 +1,71 @@ +/****************************************************************************** + * Spine Runtimes Software License v2.5 + * + * Copyright (c) 2013-2016, Esoteric Software + * All rights reserved. + * + * You are granted a perpetual, non-exclusive, non-sublicensable, and + * non-transferable license to use, install, execute, and perform the Spine + * Runtimes software and derivative works solely for personal or internal + * use. Without the written permission of Esoteric Software (see Section 2 of + * the Spine Software License Agreement), you may not (a) modify, translate, + * adapt, or develop new applications using the Spine Runtimes or otherwise + * create derivative works or improvements of the Spine Runtimes or (b) remove, + * delete, alter, or obscure any trademarks or any copyright, trademark, patent, + * or other intellectual property or proprietary rights notices on or in the + * Software, including any copy thereof. Redistributions in binary or source + * form must include this license and terms. + * + * THIS SOFTWARE IS PROVIDED BY ESOTERIC SOFTWARE "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 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 SpineRuntime36 { + 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/SpineRuntime36/src/Triangulator.cs b/SpineRuntimes/SpineRuntime36/src/Triangulator.cs new file mode 100644 index 0000000..0668db3 --- /dev/null +++ b/SpineRuntimes/SpineRuntime36/src/Triangulator.cs @@ -0,0 +1,278 @@ +/****************************************************************************** + * Spine Runtimes Software License v2.5 + * + * Copyright (c) 2013-2016, Esoteric Software + * All rights reserved. + * + * You are granted a perpetual, non-exclusive, non-sublicensable, and + * non-transferable license to use, install, execute, and perform the Spine + * Runtimes software and derivative works solely for personal or internal + * use. Without the written permission of Esoteric Software (see Section 2 of + * the Spine Software License Agreement), you may not (a) modify, translate, + * adapt, or develop new applications using the Spine Runtimes or otherwise + * create derivative works or improvements of the Spine Runtimes or (b) remove, + * delete, alter, or obscure any trademarks or any copyright, trademark, patent, + * or other intellectual property or proprietary rights notices on or in the + * Software, including any copy thereof. Redistributions in binary or source + * form must include this license and terms. + * + * THIS SOFTWARE IS PROVIDED BY ESOTERIC SOFTWARE "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 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 SpineRuntime36 { + internal 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; + } + } +}