Selaa lähdekoodia

Merge remote-tracking branch 'origin/dev'

NathanSweet 9 vuotta sitten
vanhempi
commit
db1afae89a
37 muutettua tiedostoa jossa 2255 lisäystä ja 1252 poistoa
  1. 1 1
      spine-c/src/spine/SkeletonJson.c
  2. 5 5
      spine-libgdx/spine-libgdx-tests/src/com/esotericsoftware/spine/AnimationStateTest.java
  3. 2 2
      spine-libgdx/spine-libgdx-tests/src/com/esotericsoftware/spine/AttachmentTimelineTests.java
  4. 6 6
      spine-libgdx/spine-libgdx-tests/src/com/esotericsoftware/spine/BonePlotting.java
  5. 288 156
      spine-libgdx/spine-libgdx/src/com/esotericsoftware/spine/Animation.java
  6. 1 0
      spine-libgdx/spine-libgdx/src/com/esotericsoftware/spine/AnimationStateData.java
  7. 92 23
      spine-libgdx/spine-libgdx/src/com/esotericsoftware/spine/Bone.java
  8. 23 15
      spine-libgdx/spine-libgdx/src/com/esotericsoftware/spine/BoneData.java
  9. 1 0
      spine-libgdx/spine-libgdx/src/com/esotericsoftware/spine/Event.java
  10. 60 44
      spine-libgdx/spine-libgdx/src/com/esotericsoftware/spine/IkConstraint.java
  11. 2 0
      spine-libgdx/spine-libgdx/src/com/esotericsoftware/spine/IkConstraintData.java
  12. 436 0
      spine-libgdx/spine-libgdx/src/com/esotericsoftware/spine/PathConstraint.java
  13. 122 0
      spine-libgdx/spine-libgdx/src/com/esotericsoftware/spine/PathConstraintData.java
  14. 186 38
      spine-libgdx/spine-libgdx/src/com/esotericsoftware/spine/Skeleton.java
  15. 237 183
      spine-libgdx/spine-libgdx/src/com/esotericsoftware/spine/SkeletonBinary.java
  16. 3 5
      spine-libgdx/spine-libgdx/src/com/esotericsoftware/spine/SkeletonBounds.java
  17. 28 1
      spine-libgdx/spine-libgdx/src/com/esotericsoftware/spine/SkeletonData.java
  18. 263 197
      spine-libgdx/spine-libgdx/src/com/esotericsoftware/spine/SkeletonJson.java
  19. 1 8
      spine-libgdx/spine-libgdx/src/com/esotericsoftware/spine/SkeletonMeshRenderer.java
  20. 2 7
      spine-libgdx/spine-libgdx/src/com/esotericsoftware/spine/SkeletonRenderer.java
  21. 60 27
      spine-libgdx/spine-libgdx/src/com/esotericsoftware/spine/SkeletonRendererDebug.java
  22. 3 12
      spine-libgdx/spine-libgdx/src/com/esotericsoftware/spine/Slot.java
  23. 8 6
      spine-libgdx/spine-libgdx/src/com/esotericsoftware/spine/SlotData.java
  24. 71 123
      spine-libgdx/spine-libgdx/src/com/esotericsoftware/spine/TransformConstraint.java
  25. 8 7
      spine-libgdx/spine-libgdx/src/com/esotericsoftware/spine/TransformConstraintData.java
  26. 5 11
      spine-libgdx/spine-libgdx/src/com/esotericsoftware/spine/attachments/AtlasAttachmentLoader.java
  27. 2 2
      spine-libgdx/spine-libgdx/src/com/esotericsoftware/spine/attachments/AttachmentLoader.java
  28. 1 1
      spine-libgdx/spine-libgdx/src/com/esotericsoftware/spine/attachments/AttachmentType.java
  29. 9 24
      spine-libgdx/spine-libgdx/src/com/esotericsoftware/spine/attachments/BoundingBoxAttachment.java
  30. 0 6
      spine-libgdx/spine-libgdx/src/com/esotericsoftware/spine/attachments/FfdAttachment.java
  31. 72 44
      spine-libgdx/spine-libgdx/src/com/esotericsoftware/spine/attachments/MeshAttachment.java
  32. 84 0
      spine-libgdx/spine-libgdx/src/com/esotericsoftware/spine/attachments/PathAttachment.java
  33. 12 12
      spine-libgdx/spine-libgdx/src/com/esotericsoftware/spine/attachments/RegionAttachment.java
  34. 1 1
      spine-libgdx/spine-libgdx/src/com/esotericsoftware/spine/attachments/RegionSequenceAttachment.java
  35. 146 0
      spine-libgdx/spine-libgdx/src/com/esotericsoftware/spine/attachments/VertexAttachment.java
  36. 0 274
      spine-libgdx/spine-libgdx/src/com/esotericsoftware/spine/attachments/WeightedMeshAttachment.java
  37. 14 11
      spine-libgdx/spine-skeletonviewer/src/com/esotericsoftware/spine/SkeletonViewer.java

+ 1 - 1
spine-c/src/spine/SkeletonJson.c

@@ -126,7 +126,7 @@ static void _spSkeletonJson_addLinkedMesh (spSkeletonJson* self, spAttachment* m
 		internal->linkedMeshCapacity *= 2;
 		if (internal->linkedMeshCapacity < 8) internal->linkedMeshCapacity = 8;
 		linkedMeshes = MALLOC(_spLinkedMesh, internal->linkedMeshCapacity);
-		memcpy(linkedMeshes, internal->linkedMeshes, internal->linkedMeshCount);
+		memcpy(linkedMeshes, internal->linkedMeshes, sizeof(_spLinkedMesh) * internal->linkedMeshCount);
 		FREE(internal->linkedMeshes);
 		internal->linkedMeshes = linkedMeshes;
 	}

+ 5 - 5
spine-libgdx/spine-libgdx-tests/src/com/esotericsoftware/spine/AnimationStateTest.java

@@ -37,13 +37,13 @@ import com.badlogic.gdx.utils.Array;
 import com.esotericsoftware.spine.AnimationState.AnimationStateListener;
 import com.esotericsoftware.spine.attachments.AttachmentLoader;
 import com.esotericsoftware.spine.attachments.BoundingBoxAttachment;
-import com.esotericsoftware.spine.attachments.MeshAttachment;
 import com.esotericsoftware.spine.attachments.RegionAttachment;
-import com.esotericsoftware.spine.attachments.WeightedMeshAttachment;
+import com.esotericsoftware.spine.attachments.MeshAttachment;
+import com.esotericsoftware.spine.attachments.PathAttachment;
 
 public class AnimationStateTest {
 	final SkeletonJson json = new SkeletonJson(new AttachmentLoader() {
-		public WeightedMeshAttachment newWeightedMeshAttachment (Skin skin, String name, String path) {
+		public MeshAttachment newMeshAttachment (Skin skin, String name, String path) {
 			return null;
 		}
 
@@ -51,11 +51,11 @@ public class AnimationStateTest {
 			return null;
 		}
 
-		public MeshAttachment newMeshAttachment (Skin skin, String name, String path) {
+		public BoundingBoxAttachment newBoundingBoxAttachment (Skin skin, String name) {
 			return null;
 		}
 
-		public BoundingBoxAttachment newBoundingBoxAttachment (Skin skin, String name) {
+		public PathAttachment newPathAttachment (Skin skin, String name) {
 			return null;
 		}
 	});

+ 2 - 2
spine-libgdx/spine-libgdx-tests/src/com/esotericsoftware/spine/AttachmentTimelineTests.java

@@ -46,10 +46,10 @@ public class AttachmentTimelineTests {
 	public AttachmentTimelineTests () {
 		skeletonData = new SkeletonData();
 
-		BoneData boneData = new BoneData("bone", null);
+		BoneData boneData = new BoneData(0, "bone", null);
 		skeletonData.getBones().add(boneData);
 
-		skeletonData.getSlots().add(new SlotData("slot", boneData));
+		skeletonData.getSlots().add(new SlotData(0, "slot", boneData));
 
 		Attachment attachment1 = new Attachment("attachment1") {};
 		Attachment attachment2 = new Attachment("attachment2") {};

+ 6 - 6
spine-libgdx/spine-libgdx-tests/src/com/esotericsoftware/spine/BonePlotting.java

@@ -34,18 +34,14 @@ package com.esotericsoftware.spine;
 import com.badlogic.gdx.files.FileHandle;
 import com.esotericsoftware.spine.attachments.AttachmentLoader;
 import com.esotericsoftware.spine.attachments.BoundingBoxAttachment;
-import com.esotericsoftware.spine.attachments.MeshAttachment;
 import com.esotericsoftware.spine.attachments.RegionAttachment;
-import com.esotericsoftware.spine.attachments.WeightedMeshAttachment;
+import com.esotericsoftware.spine.attachments.MeshAttachment;
+import com.esotericsoftware.spine.attachments.PathAttachment;
 
 public class BonePlotting {
 	static public void main (String[] args) throws Exception {
 		// This example shows how to load skeleton data and plot a bone transform for each animation.
 		SkeletonJson json = new SkeletonJson(new AttachmentLoader() {
-			public WeightedMeshAttachment newWeightedMeshAttachment (Skin skin, String name, String path) {
-				return null;
-			}
-
 			public RegionAttachment newRegionAttachment (Skin skin, String name, String path) {
 				return null;
 			}
@@ -57,6 +53,10 @@ public class BonePlotting {
 			public BoundingBoxAttachment newBoundingBoxAttachment (Skin skin, String name) {
 				return null;
 			}
+
+			public PathAttachment newPathAttachment (Skin skin, String name) {
+				return null;
+			}
 		});
 		SkeletonData skeletonData = json.readSkeletonData(new FileHandle("assets/spineboy/spineboy.json"));
 		Skeleton skeleton = new Skeleton(skeletonData);

+ 288 - 156
spine-libgdx/spine-libgdx/src/com/esotericsoftware/spine/Animation.java

@@ -36,7 +36,7 @@ import com.badlogic.gdx.math.MathUtils;
 import com.badlogic.gdx.utils.Array;
 import com.badlogic.gdx.utils.FloatArray;
 import com.esotericsoftware.spine.attachments.Attachment;
-import com.esotericsoftware.spine.attachments.FfdAttachment;
+import com.esotericsoftware.spine.attachments.VertexAttachment;
 
 public class Animation {
 	final String name;
@@ -66,7 +66,7 @@ public class Animation {
 
 	/** Poses the skeleton at the specified time for this animation.
 	 * @param lastTime The last time the animation was applied.
-	 * @param events Any triggered events are added. */
+	 * @param events Any triggered events are added. May be null. */
 	public void apply (Skeleton skeleton, float lastTime, float time, boolean loop, Array<Event> events) {
 		if (skeleton == null) throw new IllegalArgumentException("skeleton cannot be null.");
 
@@ -82,7 +82,7 @@ public class Animation {
 
 	/** Poses the skeleton at the specified time for this animation mixed with the current pose.
 	 * @param lastTime The last time the animation was applied.
-	 * @param events Any triggered events are added.
+	 * @param events Any triggered events are added. May be null.
 	 * @param alpha The amount of this animation that affects the current pose. */
 	public void mix (Skeleton skeleton, float lastTime, float time, boolean loop, Array<Event> events, float alpha) {
 		if (skeleton == null) throw new IllegalArgumentException("skeleton cannot be null.");
@@ -154,7 +154,7 @@ public class Animation {
 	/** Base class for frames that use an interpolation bezier curve. */
 	abstract static public class CurveTimeline implements Timeline {
 		static public final float LINEAR = 0, STEPPED = 1, BEZIER = 2;
-		static private final int BEZIER_SEGMENTS = 10, BEZIER_SIZE = BEZIER_SEGMENTS * 2 - 1;
+		static private final int BEZIER_SIZE = 10 * 2 - 1;
 
 		private final float[] curves; // type, x, y, ...
 
@@ -188,12 +188,10 @@ public class Animation {
 		 * 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 subdiv1 = 1f / BEZIER_SEGMENTS, subdiv2 = subdiv1 * subdiv1, subdiv3 = subdiv2 * subdiv1;
-			float pre1 = 3 * subdiv1, pre2 = 3 * subdiv2, pre4 = 6 * subdiv2, pre5 = 6 * subdiv3;
-			float tmp1x = -cx1 * 2 + cx2, tmp1y = -cy1 * 2 + cy2, tmp2x = (cx1 - cx2) * 3 + 1, tmp2y = (cy1 - cy2) * 3 + 1;
-			float dfx = cx1 * pre1 + tmp1x * pre2 + tmp2x * subdiv3, dfy = cy1 * pre1 + tmp1y * pre2 + tmp2y * subdiv3;
-			float ddfx = tmp1x * pre4 + tmp2x * pre5, ddfy = tmp1y * pre4 + tmp2y * pre5;
-			float dddfx = tmp2x * pre5, dddfy = tmp2y * pre5;
+			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;
@@ -213,6 +211,7 @@ public class Animation {
 		}
 
 		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];
@@ -240,19 +239,21 @@ public class Animation {
 	}
 
 	static public class RotateTimeline extends CurveTimeline {
-		static final int PREV_TIME = -2;
-		static final int VALUE = 1;
+		static public final int ENTRIES = 2;
+		static private final int PREV_TIME = -2, PREV_ROTATION = -1;
+		static private final int ROTATION = 1;
 
 		int boneIndex;
-		final float[] frames; // time, angle, ...
+		final float[] frames; // time, degrees, ...
 
 		public RotateTimeline (int frameCount) {
 			super(frameCount);
 			frames = new float[frameCount << 1];
 		}
 
-		public void setBoneIndex (int boneIndex) {
-			this.boneIndex = boneIndex;
+		public void setBoneIndex (int index) {
+			if (index < 0) throw new IllegalArgumentException("index must be >= 0.");
+			this.boneIndex = index;
 		}
 
 		public int getBoneIndex () {
@@ -264,10 +265,10 @@ public class Animation {
 		}
 
 		/** Sets the time and angle of the specified keyframe. */
-		public void setFrame (int frameIndex, float time, float angle) {
-			frameIndex *= 2;
+		public void setFrame (int frameIndex, float time, float degrees) {
+			frameIndex <<= 1;
 			frames[frameIndex] = time;
-			frames[frameIndex + 1] = angle;
+			frames[frameIndex + ROTATION] = degrees;
 		}
 
 		public void apply (Skeleton skeleton, float lastTime, float time, Array<Event> events, float alpha) {
@@ -276,8 +277,8 @@ public class Animation {
 
 			Bone bone = skeleton.bones.get(boneIndex);
 
-			if (time >= frames[frames.length - 2]) { // Time is after last frame.
-				float amount = bone.data.rotation + frames[frames.length - 1] - bone.rotation;
+			if (time >= frames[frames.length - ENTRIES]) { // Time is after last frame.
+				float amount = bone.data.rotation + frames[frames.length + PREV_ROTATION] - bone.rotation;
 				while (amount > 180)
 					amount -= 360;
 				while (amount < -180)
@@ -287,18 +288,17 @@ public class Animation {
 			}
 
 			// Interpolate between the previous frame and the current frame.
-			int frame = binarySearch(frames, time, 2);
-			float prevFrameValue = frames[frame - 1];
+			int frame = binarySearch(frames, time, ENTRIES);
+			float prevRotation = frames[frame + PREV_ROTATION];
 			float frameTime = frames[frame];
-			float percent = MathUtils.clamp(1 - (time - frameTime) / (frames[frame + PREV_TIME] - frameTime), 0, 1);
-			percent = getCurvePercent((frame >> 1) - 1, percent);
+			float percent = getCurvePercent((frame >> 1) - 1, 1 - (time - frameTime) / (frames[frame + PREV_TIME] - frameTime));
 
-			float amount = frames[frame + VALUE] - prevFrameValue;
+			float amount = frames[frame + ROTATION] - prevRotation;
 			while (amount > 180)
 				amount -= 360;
 			while (amount < -180)
 				amount += 360;
-			amount = bone.data.rotation + (prevFrameValue + amount * percent) - bone.rotation;
+			amount = bone.data.rotation + (prevRotation + amount * percent) - bone.rotation;
 			while (amount > 180)
 				amount -= 360;
 			while (amount < -180)
@@ -308,20 +308,21 @@ public class Animation {
 	}
 
 	static public class TranslateTimeline extends CurveTimeline {
-		static final int PREV_TIME = -3;
-		static final int X = 1;
-		static final int Y = 2;
+		static public final int ENTRIES = 3;
+		static final int PREV_TIME = -3, PREV_X = -2, PREV_Y = -1;
+		static final int X = 1, Y = 2;
 
 		int boneIndex;
 		final float[] frames; // time, x, y, ...
 
 		public TranslateTimeline (int frameCount) {
 			super(frameCount);
-			frames = new float[frameCount * 3];
+			frames = new float[frameCount * ENTRIES];
 		}
 
-		public void setBoneIndex (int boneIndex) {
-			this.boneIndex = boneIndex;
+		public void setBoneIndex (int index) {
+			if (index < 0) throw new IllegalArgumentException("index must be >= 0.");
+			this.boneIndex = index;
 		}
 
 		public int getBoneIndex () {
@@ -334,10 +335,10 @@ public class Animation {
 
 		/** Sets the time and value of the specified keyframe. */
 		public void setFrame (int frameIndex, float time, float x, float y) {
-			frameIndex *= 3;
+			frameIndex *= ENTRIES;
 			frames[frameIndex] = time;
-			frames[frameIndex + 1] = x;
-			frames[frameIndex + 2] = y;
+			frames[frameIndex + X] = x;
+			frames[frameIndex + Y] = y;
 		}
 
 		public void apply (Skeleton skeleton, float lastTime, float time, Array<Event> events, float alpha) {
@@ -346,22 +347,21 @@ public class Animation {
 
 			Bone bone = skeleton.bones.get(boneIndex);
 
-			if (time >= frames[frames.length - 3]) { // Time is after last frame.
-				bone.x += (bone.data.x + frames[frames.length - 2] - bone.x) * alpha;
-				bone.y += (bone.data.y + frames[frames.length - 1] - bone.y) * alpha;
+			if (time >= frames[frames.length - ENTRIES]) { // Time is after last frame.
+				bone.x += (bone.data.x + frames[frames.length + PREV_X] - bone.x) * alpha;
+				bone.y += (bone.data.y + frames[frames.length + PREV_Y] - bone.y) * alpha;
 				return;
 			}
 
 			// Interpolate between the previous frame and the current frame.
-			int frame = binarySearch(frames, time, 3);
-			float prevFrameX = frames[frame - 2];
-			float prevFrameY = frames[frame - 1];
+			int frame = binarySearch(frames, time, ENTRIES);
+			float prevX = frames[frame + PREV_X];
+			float prevY = frames[frame + PREV_Y];
 			float frameTime = frames[frame];
-			float percent = MathUtils.clamp(1 - (time - frameTime) / (frames[frame + PREV_TIME] - frameTime), 0, 1);
-			percent = getCurvePercent(frame / 3 - 1, percent);
+			float percent = getCurvePercent(frame / ENTRIES - 1, 1 - (time - frameTime) / (frames[frame + PREV_TIME] - frameTime));
 
-			bone.x += (bone.data.x + prevFrameX + (frames[frame + X] - prevFrameX) * percent - bone.x) * alpha;
-			bone.y += (bone.data.y + prevFrameY + (frames[frame + Y] - prevFrameY) * percent - bone.y) * alpha;
+			bone.x += (bone.data.x + prevX + (frames[frame + X] - prevX) * percent - bone.x) * alpha;
+			bone.y += (bone.data.y + prevY + (frames[frame + Y] - prevY) * percent - bone.y) * alpha;
 		}
 	}
 
@@ -375,22 +375,21 @@ public class Animation {
 			if (time < frames[0]) return; // Time is before first frame.
 
 			Bone bone = skeleton.bones.get(boneIndex);
-			if (time >= frames[frames.length - 3]) { // Time is after last frame.
-				bone.scaleX += (bone.data.scaleX * frames[frames.length - 2] - bone.scaleX) * alpha;
-				bone.scaleY += (bone.data.scaleY * frames[frames.length - 1] - bone.scaleY) * alpha;
+			if (time >= frames[frames.length - ENTRIES]) { // Time is after last frame.
+				bone.scaleX += (bone.data.scaleX * frames[frames.length + PREV_X] - bone.scaleX) * alpha;
+				bone.scaleY += (bone.data.scaleY * frames[frames.length + PREV_Y] - bone.scaleY) * alpha;
 				return;
 			}
 
 			// Interpolate between the previous frame and the current frame.
-			int frame = binarySearch(frames, time, 3);
-			float prevFrameX = frames[frame - 2];
-			float prevFrameY = frames[frame - 1];
+			int frame = binarySearch(frames, time, ENTRIES);
+			float prevX = frames[frame + PREV_X];
+			float prevY = frames[frame + PREV_Y];
 			float frameTime = frames[frame];
-			float percent = MathUtils.clamp(1 - (time - frameTime) / (frames[frame + PREV_TIME] - frameTime), 0, 1);
-			percent = getCurvePercent(frame / 3 - 1, percent);
+			float percent = getCurvePercent(frame / ENTRIES - 1, 1 - (time - frameTime) / (frames[frame + PREV_TIME] - frameTime));
 
-			bone.scaleX += (bone.data.scaleX * (prevFrameX + (frames[frame + X] - prevFrameX) * percent) - bone.scaleX) * alpha;
-			bone.scaleY += (bone.data.scaleY * (prevFrameY + (frames[frame + Y] - prevFrameY) * percent) - bone.scaleY) * alpha;
+			bone.scaleX += (bone.data.scaleX * (prevX + (frames[frame + X] - prevX) * percent) - bone.scaleX) * alpha;
+			bone.scaleY += (bone.data.scaleY * (prevY + (frames[frame + Y] - prevY) * percent) - bone.scaleY) * alpha;
 		}
 	}
 
@@ -404,42 +403,40 @@ public class Animation {
 			if (time < frames[0]) return; // Time is before first frame.
 
 			Bone bone = skeleton.bones.get(boneIndex);
-			if (time >= frames[frames.length - 3]) { // Time is after last frame.
-				bone.shearX += (bone.data.shearX + frames[frames.length - 2] - bone.shearX) * alpha;
-				bone.shearY += (bone.data.shearY + frames[frames.length - 1] - bone.shearY) * alpha;
+			if (time >= frames[frames.length - ENTRIES]) { // Time is after last frame.
+				bone.shearX += (bone.data.shearX + frames[frames.length + PREV_X] - bone.shearX) * alpha;
+				bone.shearY += (bone.data.shearY + frames[frames.length + PREV_Y] - bone.shearY) * alpha;
 				return;
 			}
 
 			// Interpolate between the previous frame and the current frame.
-			int frame = binarySearch(frames, time, 3);
-			float prevFrameX = frames[frame - 2];
-			float prevFrameY = frames[frame - 1];
+			int frame = binarySearch(frames, time, ENTRIES);
+			float prevX = frames[frame + PREV_X];
+			float prevY = frames[frame + PREV_Y];
 			float frameTime = frames[frame];
-			float percent = MathUtils.clamp(1 - (time - frameTime) / (frames[frame + PREV_TIME] - frameTime), 0, 1);
-			percent = getCurvePercent(frame / 3 - 1, percent);
+			float percent = getCurvePercent(frame / ENTRIES - 1, 1 - (time - frameTime) / (frames[frame + PREV_TIME] - frameTime));
 
-			bone.shearX += (bone.data.shearX + (prevFrameX + (frames[frame + X] - prevFrameX) * percent) - bone.shearX) * alpha;
-			bone.shearY += (bone.data.shearY + (prevFrameY + (frames[frame + Y] - prevFrameY) * percent) - bone.shearY) * alpha;
+			bone.shearX += (bone.data.shearX + (prevX + (frames[frame + X] - prevX) * percent) - bone.shearX) * alpha;
+			bone.shearY += (bone.data.shearY + (prevY + (frames[frame + Y] - prevY) * percent) - bone.shearY) * alpha;
 		}
 	}
 
 	static public class ColorTimeline extends CurveTimeline {
-		static private final int PREV_TIME = -5;
-		static private final int R = 1;
-		static private final int G = 2;
-		static private final int B = 3;
-		static private final int A = 4;
+		static public final int ENTRIES = 5;
+		static private final int PREV_TIME = -5, PREV_R = -4, PREV_G = -3, PREV_B = -2, PREV_A = -1;
+		static private final int R = 1, G = 2, B = 3, A = 4;
 
 		int slotIndex;
 		private final float[] frames; // time, r, g, b, a, ...
 
 		public ColorTimeline (int frameCount) {
 			super(frameCount);
-			frames = new float[frameCount * 5];
+			frames = new float[frameCount * ENTRIES];
 		}
 
-		public void setSlotIndex (int slotIndex) {
-			this.slotIndex = slotIndex;
+		public void setSlotIndex (int index) {
+			if (index < 0) throw new IllegalArgumentException("index must be >= 0.");
+			this.slotIndex = index;
 		}
 
 		public int getSlotIndex () {
@@ -452,12 +449,12 @@ public class Animation {
 
 		/** 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 *= 5;
+			frameIndex *= ENTRIES;
 			frames[frameIndex] = time;
-			frames[frameIndex + 1] = r;
-			frames[frameIndex + 2] = g;
-			frames[frameIndex + 3] = b;
-			frames[frameIndex + 4] = a;
+			frames[frameIndex + R] = r;
+			frames[frameIndex + G] = g;
+			frames[frameIndex + B] = b;
+			frames[frameIndex + A] = a;
 		}
 
 		public void apply (Skeleton skeleton, float lastTime, float time, Array<Event> events, float alpha) {
@@ -465,23 +462,23 @@ public class Animation {
 			if (time < frames[0]) return; // Time is before first frame.
 
 			float r, g, b, a;
-			if (time >= frames[frames.length - 5]) { // Time is after last frame.
-				int i = frames.length - 1;
-				r = frames[i - 3];
-				g = frames[i - 2];
-				b = frames[i - 1];
-				a = frames[i];
+			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 = binarySearch(frames, time, 5);
+				int frame = 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 = MathUtils.clamp(1 - (time - frameTime) / (frames[frame + PREV_TIME] - frameTime), 0, 1);
-				percent = getCurvePercent(frame / 5 - 1, percent);
+				float percent = getCurvePercent(frame / ENTRIES - 1,
+					1 - (time - frameTime) / (frames[frame + PREV_TIME] - frameTime));
 
-				r = frames[frame - 4];
-				g = frames[frame - 3];
-				b = frames[frame - 2];
-				a = frames[frame - 1];
 				r += (frames[frame + R] - r) * percent;
 				g += (frames[frame + G] - g) * percent;
 				b += (frames[frame + B] - b) * percent;
@@ -509,12 +506,13 @@ public class Animation {
 			return frames.length;
 		}
 
-		public int getSlotIndex () {
-			return slotIndex;
+		public void setSlotIndex (int index) {
+			if (index < 0) throw new IllegalArgumentException("index must be >= 0.");
+			this.slotIndex = index;
 		}
 
-		public void setSlotIndex (int slotIndex) {
-			this.slotIndex = slotIndex;
+		public int getSlotIndex () {
+			return slotIndex;
 		}
 
 		public float[] getFrames () {
@@ -654,27 +652,28 @@ public class Animation {
 		}
 	}
 
-	static public class FfdTimeline extends CurveTimeline {
+	static public class DeformTimeline extends CurveTimeline {
 		private final float[] frames; // time, ...
 		private final float[][] frameVertices;
 		int slotIndex;
-		Attachment attachment;
+		VertexAttachment attachment;
 
-		public FfdTimeline (int frameCount) {
+		public DeformTimeline (int frameCount) {
 			super(frameCount);
 			frames = new float[frameCount];
 			frameVertices = new float[frameCount][];
 		}
 
-		public void setSlotIndex (int slotIndex) {
-			this.slotIndex = slotIndex;
+		public void setSlotIndex (int index) {
+			if (index < 0) throw new IllegalArgumentException("index must be >= 0.");
+			this.slotIndex = index;
 		}
 
 		public int getSlotIndex () {
 			return slotIndex;
 		}
 
-		public void setAttachment (Attachment attachment) {
+		public void setAttachment (VertexAttachment attachment) {
 			this.attachment = attachment;
 		}
 
@@ -699,7 +698,7 @@ public class Animation {
 		public void apply (Skeleton skeleton, float lastTime, float time, Array<Event> firedEvents, float alpha) {
 			Slot slot = skeleton.slots.get(slotIndex);
 			Attachment slotAttachment = slot.getAttachment();
-			if (!(slotAttachment instanceof FfdAttachment) || !((FfdAttachment)slotAttachment).applyFFD(attachment)) return;
+			if (!(slotAttachment instanceof VertexAttachment) || !((VertexAttachment)slotAttachment).applyDeform(attachment)) return;
 
 			float[] frames = this.frames;
 			if (time < frames[0]) return; // Time is before first frame.
@@ -709,10 +708,7 @@ public class Animation {
 
 			FloatArray verticesArray = slot.getAttachmentVertices();
 			if (verticesArray.size != vertexCount) alpha = 1; // Don't mix from uninitialized slot vertices.
-			verticesArray.size = 0;
-			verticesArray.ensureCapacity(vertexCount);
-			verticesArray.size = vertexCount;
-			float[] vertices = verticesArray.items;
+			float[] vertices = verticesArray.setSize(vertexCount);
 
 			if (time >= frames[frames.length - 1]) { // Time is after last frame.
 				float[] lastVertices = frameVertices[frames.length - 1];
@@ -726,12 +722,11 @@ public class Animation {
 
 			// Interpolate between the previous frame and the current frame.
 			int frame = binarySearch(frames, time);
-			float frameTime = frames[frame];
-			float percent = MathUtils.clamp(1 - (time - frameTime) / (frames[frame - 1] - frameTime), 0, 1);
-			percent = getCurvePercent(frame - 1, percent);
-
 			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) {
 				for (int i = 0; i < vertexCount; i++) {
 					float prev = prevVertices[i];
@@ -747,21 +742,21 @@ public class Animation {
 	}
 
 	static public class IkConstraintTimeline extends CurveTimeline {
-		static private final int PREV_TIME = -3;
-		static private final int PREV_MIX = -2;
-		static private final int PREV_BEND_DIRECTION = -1;
-		static private final int MIX = 1;
+		static public final int ENTRIES = 3;
+		static private final int PREV_TIME = -3, PREV_MIX = -2, PREV_BEND_DIRECTION = -1;
+		static private final int MIX = 1, BEND_DIRECTION = 2;
 
 		int ikConstraintIndex;
 		private final float[] frames; // time, mix, bendDirection, ...
 
 		public IkConstraintTimeline (int frameCount) {
 			super(frameCount);
-			frames = new float[frameCount * 3];
+			frames = new float[frameCount * ENTRIES];
 		}
 
-		public void setIkConstraintIndex (int ikConstraint) {
-			this.ikConstraintIndex = ikConstraint;
+		public void setIkConstraintIndex (int index) {
+			if (index < 0) throw new IllegalArgumentException("index must be >= 0.");
+			this.ikConstraintIndex = index;
 		}
 
 		public int getIkConstraintIndex () {
@@ -774,10 +769,10 @@ public class Animation {
 
 		/** Sets the time, mix and bend direction of the specified keyframe. */
 		public void setFrame (int frameIndex, float time, float mix, int bendDirection) {
-			frameIndex *= 3;
+			frameIndex *= ENTRIES;
 			frames[frameIndex] = time;
-			frames[frameIndex + 1] = mix;
-			frames[frameIndex + 2] = bendDirection;
+			frames[frameIndex + MIX] = mix;
+			frames[frameIndex + BEND_DIRECTION] = bendDirection;
 		}
 
 		public void apply (Skeleton skeleton, float lastTime, float time, Array<Event> events, float alpha) {
@@ -786,45 +781,39 @@ public class Animation {
 
 			IkConstraint constraint = skeleton.ikConstraints.get(ikConstraintIndex);
 
-			if (time >= frames[frames.length - 3]) { // Time is after last frame.
+			if (time >= frames[frames.length - ENTRIES]) { // Time is after last frame.
 				constraint.mix += (frames[frames.length + PREV_MIX] - constraint.mix) * alpha;
 				constraint.bendDirection = (int)frames[frames.length + PREV_BEND_DIRECTION];
 				return;
 			}
 
 			// Interpolate between the previous frame and the current frame.
-			int frame = binarySearch(frames, time, 3);
+			int frame = binarySearch(frames, time, ENTRIES);
+			float mix = frames[frame + PREV_MIX];
 			float frameTime = frames[frame];
-			float percent = MathUtils.clamp(1 - (time - frameTime) / (frames[frame + PREV_TIME] - frameTime), 0, 1);
-			percent = getCurvePercent(frame / 3 - 1, percent);
+			float percent = getCurvePercent(frame / ENTRIES - 1, 1 - (time - frameTime) / (frames[frame + PREV_TIME] - frameTime));
 
-			float mix = frames[frame + PREV_MIX];
 			constraint.mix += (mix + (frames[frame + MIX] - mix) * percent - constraint.mix) * alpha;
 			constraint.bendDirection = (int)frames[frame + PREV_BEND_DIRECTION];
 		}
 	}
 
 	static public class TransformConstraintTimeline extends CurveTimeline {
-		static private final int PREV_TIME = -5;
-		static private final int PREV_ROTATE_MIX = -4;
-		static private final int PREV_TRANSLATE_MIX = -3;
-		static private final int PREV_SCALE_MIX = -2;
-		static private final int PREV_SHEAR_MIX = -1;
-		static private final int ROTATE_MIX = 1;
-		static private final int TRANSLATE_MIX = 2;
-		static private final int SCALE_MIX = 3;
-		static private final int SHEAR_MIX = 4;
+		static public final int ENTRIES = 5;
+		static private final int PREV_TIME = -5, PREV_ROTATE = -4, PREV_TRANSLATE = -3, PREV_SCALE = -2, PREV_SHEAR = -1;
+		static private final int ROTATE = 1, TRANSLATE = 2, SCALE = 3, SHEAR = 4;
 
 		int transformConstraintIndex;
 		private final float[] frames; // time, rotate mix, translate mix, scale mix, shear mix, ...
 
 		public TransformConstraintTimeline (int frameCount) {
 			super(frameCount);
-			frames = new float[frameCount * 5];
+			frames = new float[frameCount * ENTRIES];
 		}
 
-		public void setTransformConstraintIndex (int ikConstraint) {
-			this.transformConstraintIndex = ikConstraint;
+		public void setTransformConstraintIndex (int index) {
+			if (index < 0) throw new IllegalArgumentException("index must be >= 0.");
+			this.transformConstraintIndex = index;
 		}
 
 		public int getTransformConstraintIndex () {
@@ -837,7 +826,7 @@ public class Animation {
 
 		/** Sets the time and mixes of the specified keyframe. */
 		public void setFrame (int frameIndex, float time, float rotateMix, float translateMix, float scaleMix, float shearMix) {
-			frameIndex *= 5;
+			frameIndex *= ENTRIES;
 			frames[frameIndex] = time;
 			frames[frameIndex + 1] = rotateMix;
 			frames[frameIndex + 2] = translateMix;
@@ -851,30 +840,173 @@ public class Animation {
 
 			TransformConstraint constraint = skeleton.transformConstraints.get(transformConstraintIndex);
 
-			if (time >= frames[frames.length - 5]) { // Time is after last frame.
-				int i = frames.length - 1;
-				constraint.rotateMix += (frames[i - 3] - constraint.rotateMix) * alpha;
-				constraint.translateMix += (frames[i - 2] - constraint.translateMix) * alpha;
-				constraint.scaleMix += (frames[i - 1] - constraint.scaleMix) * alpha;
-				constraint.shearMix += (frames[i] - constraint.shearMix) * alpha;
+			if (time >= frames[frames.length - ENTRIES]) { // Time is after last frame.
+				int i = frames.length;
+				constraint.rotateMix += (frames[i + PREV_ROTATE] - constraint.rotateMix) * alpha;
+				constraint.translateMix += (frames[i + PREV_TRANSLATE] - constraint.translateMix) * alpha;
+				constraint.scaleMix += (frames[i + PREV_SCALE] - constraint.scaleMix) * alpha;
+				constraint.shearMix += (frames[i + PREV_SHEAR] - constraint.shearMix) * alpha;
+				return;
+			}
+
+			// Interpolate between the previous frame and the current frame.
+			int frame = binarySearch(frames, time, ENTRIES);
+			float frameTime = frames[frame];
+			float percent = getCurvePercent(frame / ENTRIES - 1, 1 - (time - frameTime) / (frames[frame + PREV_TIME] - frameTime));
+
+			float rotate = frames[frame + PREV_ROTATE];
+			float translate = frames[frame + PREV_TRANSLATE];
+			float scale = frames[frame + PREV_SCALE];
+			float shear = frames[frame + PREV_SHEAR];
+			constraint.rotateMix += (rotate + (frames[frame + ROTATE] - rotate) * percent - constraint.rotateMix) * alpha;
+			constraint.translateMix += (translate + (frames[frame + TRANSLATE] - translate) * percent - constraint.translateMix)
+				* alpha;
+			constraint.scaleMix += (scale + (frames[frame + SCALE] - scale) * percent - constraint.scaleMix) * alpha;
+			constraint.shearMix += (shear + (frames[frame + SHEAR] - shear) * percent - constraint.shearMix) * alpha;
+		}
+	}
+
+	static public class PathConstraintPositionTimeline extends CurveTimeline {
+		static public final int ENTRIES = 2;
+		static final int PREV_TIME = -2, PREV_VALUE = -1;
+		static final int VALUE = 1;
+
+		int pathConstraintIndex;
+
+		final float[] frames; // time, position, ...
+
+		public PathConstraintPositionTimeline (int frameCount) {
+			super(frameCount);
+			frames = new float[frameCount * ENTRIES];
+		}
+
+		public void setPathConstraintIndex (int index) {
+			if (index < 0) throw new IllegalArgumentException("index must be >= 0.");
+			this.pathConstraintIndex = index;
+		}
+
+		public int getPathConstraintIndex () {
+			return pathConstraintIndex;
+		}
+
+		public float[] getFrames () {
+			return frames;
+		}
+
+		/** 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;
+		}
+
+		public void apply (Skeleton skeleton, float lastTime, float time, Array<Event> events, float alpha) {
+			float[] frames = this.frames;
+			if (time < frames[0]) return; // Time is before first frame.
+
+			PathConstraint constraint = skeleton.pathConstraints.get(pathConstraintIndex);
+
+			if (time >= frames[frames.length - ENTRIES]) { // Time is after last frame.
+				int i = frames.length;
+				constraint.position += (frames[i + PREV_VALUE] - constraint.position) * alpha;
+				return;
+			}
+
+			// Interpolate between the previous frame and the current frame.
+			int frame = binarySearch(frames, time, ENTRIES);
+			float position = frames[frame + PREV_VALUE];
+			float frameTime = frames[frame];
+			float percent = getCurvePercent(frame / ENTRIES - 1, 1 - (time - frameTime) / (frames[frame + PREV_TIME] - frameTime));
+
+			constraint.position += (position + (frames[frame + VALUE] - position) * percent - constraint.position) * alpha;
+		}
+	}
+
+	static public class PathConstraintSpacingTimeline extends PathConstraintPositionTimeline {
+		public PathConstraintSpacingTimeline (int frameCount) {
+			super(frameCount);
+		}
+
+		public void apply (Skeleton skeleton, float lastTime, float time, Array<Event> events, float alpha) {
+			float[] frames = this.frames;
+			if (time < frames[0]) return; // Time is before first frame.
+
+			PathConstraint constraint = skeleton.pathConstraints.get(pathConstraintIndex);
+
+			if (time >= frames[frames.length - ENTRIES]) { // Time is after last frame.
+				int i = frames.length;
+				constraint.spacing += (frames[i + PREV_VALUE] - constraint.spacing) * alpha;
 				return;
 			}
 
 			// Interpolate between the previous frame and the current frame.
-			int frame = binarySearch(frames, time, 5);
+			int frame = binarySearch(frames, time, ENTRIES);
+			float spacing = frames[frame + PREV_VALUE];
 			float frameTime = frames[frame];
-			float percent = MathUtils.clamp(1 - (time - frameTime) / (frames[frame + PREV_TIME] - frameTime), 0, 1);
-			percent = getCurvePercent(frame / 5 - 1, percent);
-
-			float rotate = frames[frame + PREV_ROTATE_MIX];
-			float translate = frames[frame + PREV_TRANSLATE_MIX];
-			float scale = frames[frame + PREV_SCALE_MIX];
-			float shear = frames[frame + PREV_SHEAR_MIX];
-			constraint.rotateMix += (rotate + (frames[frame + ROTATE_MIX] - rotate) * percent - constraint.rotateMix) * alpha;
-			constraint.translateMix += (translate + (frames[frame + TRANSLATE_MIX] - translate) * percent - constraint.translateMix)
+			float percent = getCurvePercent(frame / ENTRIES - 1, 1 - (time - frameTime) / (frames[frame + PREV_TIME] - frameTime));
+
+			constraint.spacing += (spacing + (frames[frame + VALUE] - spacing) * percent - constraint.spacing) * alpha;
+		}
+	}
+
+	static public class PathConstraintMixTimeline extends CurveTimeline {
+		static public final int ENTRIES = 3;
+		static private final int PREV_TIME = -3, PREV_ROTATE = -2, PREV_TRANSLATE = -1;
+		static private final int ROTATE = 1, TRANSLATE = 2;
+
+		int pathConstraintIndex;
+
+		private final float[] frames; // time, rotate mix, translate mix, ...
+
+		public PathConstraintMixTimeline (int frameCount) {
+			super(frameCount);
+			frames = new float[frameCount * ENTRIES];
+		}
+
+		public void setPathConstraintIndex (int index) {
+			if (index < 0) throw new IllegalArgumentException("index must be >= 0.");
+			this.pathConstraintIndex = index;
+		}
+
+		public int getPathConstraintIndex () {
+			return pathConstraintIndex;
+		}
+
+		public float[] getFrames () {
+			return frames;
+		}
+
+		/** 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;
+		}
+
+		public void apply (Skeleton skeleton, float lastTime, float time, Array<Event> events, float alpha) {
+			float[] frames = this.frames;
+			if (time < frames[0]) return; // Time is before first frame.
+
+			PathConstraint constraint = skeleton.pathConstraints.get(pathConstraintIndex);
+
+			if (time >= frames[frames.length - ENTRIES]) { // Time is after last frame.
+				int i = frames.length;
+				constraint.rotateMix += (frames[i + PREV_ROTATE] - constraint.rotateMix) * alpha;
+				constraint.translateMix += (frames[i + PREV_TRANSLATE] - constraint.translateMix) * alpha;
+				return;
+			}
+
+			// Interpolate between the previous frame and the current frame.
+			int frame = binarySearch(frames, time, ENTRIES);
+			float rotate = frames[frame + PREV_ROTATE];
+			float translate = frames[frame + PREV_TRANSLATE];
+			float frameTime = frames[frame];
+			float percent = getCurvePercent(frame / ENTRIES - 1, 1 - (time - frameTime) / (frames[frame + PREV_TIME] - frameTime));
+
+			constraint.rotateMix += (rotate + (frames[frame + ROTATE] - rotate) * percent - constraint.rotateMix) * alpha;
+			constraint.translateMix += (translate + (frames[frame + TRANSLATE] - translate) * percent - constraint.translateMix)
 				* alpha;
-			constraint.scaleMix += (scale + (frames[frame + SCALE_MIX] - scale) * percent - constraint.scaleMix) * alpha;
-			constraint.shearMix += (shear + (frames[frame + SHEAR_MIX] - shear) * percent - constraint.shearMix) * alpha;
 		}
 	}
 }

+ 1 - 0
spine-libgdx/spine-libgdx/src/com/esotericsoftware/spine/AnimationStateData.java

@@ -41,6 +41,7 @@ public class AnimationStateData {
 	float defaultMix;
 
 	public AnimationStateData (SkeletonData skeletonData) {
+		if (skeletonData == null) throw new IllegalArgumentException("skeletonData cannot be null.");
 		this.skeletonData = skeletonData;
 	}
 

+ 92 - 23
spine-libgdx/spine-libgdx/src/com/esotericsoftware/spine/Bone.java

@@ -36,23 +36,21 @@ import static com.badlogic.gdx.math.Matrix3.*;
 
 import com.badlogic.gdx.math.Matrix3;
 import com.badlogic.gdx.math.Vector2;
+import com.badlogic.gdx.utils.Array;
 
 public class Bone implements Updatable {
 	final BoneData data;
 	final Skeleton skeleton;
 	final Bone parent;
+	final Array<Bone> children = new Array();
 	float x, y, rotation, scaleX, scaleY, shearX, shearY;
-	float appliedRotation, appliedScaleX, appliedScaleY;
+	float appliedRotation;
 
 	float a, b, worldX;
 	float c, d, worldY;
 	float worldSignX, worldSignY;
 
-	Bone (BoneData data) {
-		this.data = data;
-		parent = null;
-		skeleton = null;
-	}
+	boolean sorted;
 
 	/** @param parent May be null. */
 	public Bone (BoneData data, Skeleton skeleton, Bone parent) {
@@ -68,6 +66,7 @@ public class Bone implements Updatable {
 	 * @param parent May be null. */
 	public Bone (Bone bone, Skeleton skeleton, Bone parent) {
 		if (bone == null) throw new IllegalArgumentException("bone cannot be null.");
+		if (skeleton == null) throw new IllegalArgumentException("skeleton cannot be null.");
 		this.skeleton = skeleton;
 		this.parent = parent;
 		data = bone.data;
@@ -85,16 +84,14 @@ public class Bone implements Updatable {
 		updateWorldTransform(x, y, rotation, scaleX, scaleY, shearX, shearY);
 	}
 
-	/** Computes the world SRT using the parent bone and this bone's local SRT. */
+	/** 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 SRT using the parent bone and the specified local SRT. */
+	/** 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) {
 		appliedRotation = rotation;
-		appliedScaleX = scaleX;
-		appliedScaleY = scaleY;
 
 		float rotationY = rotation + 90 + shearY;
 		float la = cosDeg(rotation + shearX) * scaleX, lb = cosDeg(rotationY) * scaleY;
@@ -144,10 +141,10 @@ public class Bone implements Updatable {
 				do {
 					float cos = cosDeg(parent.appliedRotation), sin = sinDeg(parent.appliedRotation);
 					float temp = pa * cos + pb * sin;
-					pb = pa * -sin + pb * cos;
+					pb = pb * cos - pa * sin;
 					pa = temp;
 					temp = pc * cos + pd * sin;
-					pd = pc * -sin + pd * cos;
+					pd = pd * cos - pc * sin;
 					pc = temp;
 
 					if (!parent.data.inheritRotation) break;
@@ -163,24 +160,22 @@ public class Bone implements Updatable {
 				pc = 0;
 				pd = 1;
 				do {
-					float r = parent.appliedRotation, cos = cosDeg(r), sin = sinDeg(r);
-					float psx = parent.appliedScaleX, psy = parent.appliedScaleY;
-					float za = cos * psx, zb = -sin * psy, zc = sin * psx, zd = cos * psy;
+					float cos = cosDeg(parent.appliedRotation), sin = sinDeg(parent.appliedRotation);
+					float psx = parent.scaleX, psy = parent.scaleY;
+					float za = cos * psx, zb = sin * psy, zc = sin * psx, zd = cos * psy;
 					float temp = pa * za + pb * zc;
-					pb = pa * zb + pb * zd;
+					pb = pb * zd - pa * zb;
 					pa = temp;
 					temp = pc * za + pd * zc;
-					pd = pc * zb + pd * zd;
+					pd = pd * zd - pc * zb;
 					pc = temp;
 
-					if (psx < 0) r = -r;
-					cos = cosDeg(-r);
-					sin = sinDeg(-r);
+					if (psx >= 0) sin = -sin;
 					temp = pa * cos + pb * sin;
-					pb = pa * -sin + pb * cos;
+					pb = pb * cos - pa * sin;
 					pa = temp;
 					temp = pc * cos + pd * sin;
-					pd = pc * -sin + pd * cos;
+					pd = pd * cos - pc * sin;
 					pc = temp;
 
 					if (!parent.data.inheritScale) break;
@@ -230,6 +225,10 @@ public class Bone implements Updatable {
 		return parent;
 	}
 
+	public Array<Bone> getChildren () {
+		return children;
+	}
+
 	public float getX () {
 		return x;
 	}
@@ -349,6 +348,76 @@ public class Bone implements Updatable {
 		return (float)Math.sqrt(c * c + d * d) * worldSignY;
 	}
 
+	public float worldToLocalRotationX () {
+		Bone parent = this.parent;
+		if (parent == null) return rotation;
+		float pa = parent.a, pb = parent.b, pc = parent.c, pd = parent.d, a = this.a, c = this.c;
+		return atan2(pa * c - pc * a, pd * a - pb * c) * radDeg;
+	}
+
+	public float worldToLocalRotationY () {
+		Bone parent = this.parent;
+		if (parent == null) return rotation;
+		float pa = parent.a, pb = parent.b, pc = parent.c, pd = parent.d, b = this.b, d = this.d;
+		return atan2(pa * d - pc * b, pd * b - pb * d) * radDeg;
+	}
+
+	public void rotateWorld (float degrees) {
+		float a = this.a, b = this.b, c = this.c, d = this.d;
+		float cos = cosDeg(degrees), sin = 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;
+	}
+
+	/** Computes the local transform from the world transform. This can be useful to perform processing on the local transform
+	 * after the world transform has been modified directly (eg, by a constraint).
+	 * <p>
+	 * Some redundant information is lost by the world transform, such as -1,-1 scale versus 180 rotation. The computed local
+	 * transform values may differ from the original values but are functionally the same. */
+	public void updateLocalTransform () {
+		Bone parent = this.parent;
+		if (parent == null) {
+			x = worldX;
+			y = worldY;
+			rotation = atan2(c, a) * radDeg;
+			scaleX = (float)Math.sqrt(a * a + c * c);
+			scaleY = (float)Math.sqrt(b * b + d * d);
+			float det = a * d - b * c;
+			shearX = 0;
+			shearY = atan2(a * b + c * d, det) * 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;
+		x = (dx * pd * pid - dy * pb * pid);
+		y = (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;
+		shearX = 0;
+		scaleX = (float)Math.sqrt(ra * ra + rc * rc);
+		if (scaleX > 0.0001f) {
+			float det = ra * rd - rb * rc;
+			scaleY = det / scaleX;
+			shearY = atan2(ra * rb + rc * rd, det) * radDeg;
+			rotation = atan2(rc, ra) * radDeg;
+		} else {
+			scaleX = 0;
+			scaleY = (float)Math.sqrt(rb * rb + rd * rd);
+			shearY = 0;
+			rotation = 90 - atan2(rd, rb) * radDeg;
+		}
+		appliedRotation = rotation;
+	}
+
 	public Matrix3 getWorldTransform (Matrix3 worldTransform) {
 		if (worldTransform == null) throw new IllegalArgumentException("worldTransform cannot be null.");
 		float[] val = worldTransform.val;
@@ -365,9 +434,9 @@ public class Bone implements Updatable {
 	}
 
 	public Vector2 worldToLocal (Vector2 world) {
-		float x = world.x - worldX, y = world.y - worldY;
 		float a = this.a, b = this.b, c = this.c, d = this.d;
 		float invDet = 1 / (a * d - b * c);
+		float x = world.x - worldX, y = world.y - worldY;
 		world.x = (x * d * invDet - y * b * invDet);
 		world.y = (y * a * invDet - x * c * invDet);
 		return world;

+ 23 - 15
spine-libgdx/spine-libgdx/src/com/esotericsoftware/spine/BoneData.java

@@ -34,18 +34,21 @@ package com.esotericsoftware.spine;
 import com.badlogic.gdx.graphics.Color;
 
 public class BoneData {
-	final BoneData parent;
+	final int index;
 	final String name;
+	final BoneData parent;
 	float length;
 	float x, y, rotation, scaleX = 1, scaleY = 1, shearX, shearY;
-	boolean inheritScale = true, inheritRotation = true;
+	boolean inheritRotation = true, inheritScale = true;
 
 	// Nonessential.
 	final Color color = new Color(0.61f, 0.61f, 0.61f, 1);
 
 	/** @param parent May be null. */
-	public BoneData (String name, BoneData parent) {
+	public BoneData (int index, String name, BoneData parent) {
+		if (index < 0) throw new IllegalArgumentException("index must be >= 0.");
 		if (name == null) throw new IllegalArgumentException("name cannot be null.");
+		this.index = index;
 		this.name = name;
 		this.parent = parent;
 	}
@@ -54,8 +57,9 @@ public class BoneData {
 	 * @param parent May be null. */
 	public BoneData (BoneData bone, BoneData parent) {
 		if (bone == null) throw new IllegalArgumentException("bone cannot be null.");
-		this.parent = parent;
+		index = bone.index;
 		name = bone.name;
+		this.parent = parent;
 		length = bone.length;
 		x = bone.x;
 		y = bone.y;
@@ -66,15 +70,19 @@ public class BoneData {
 		shearY = bone.shearY;
 	}
 
-	/** @return May be null. */
-	public BoneData getParent () {
-		return parent;
+	public int getIndex () {
+		return index;
 	}
 
 	public String getName () {
 		return name;
 	}
 
+	/** @return May be null. */
+	public BoneData getParent () {
+		return parent;
+	}
+
 	public float getLength () {
 		return length;
 	}
@@ -149,14 +157,6 @@ public class BoneData {
 		this.shearY = shearY;
 	}
 
-	public boolean getInheritScale () {
-		return inheritScale;
-	}
-
-	public void setInheritScale (boolean inheritScale) {
-		this.inheritScale = inheritScale;
-	}
-
 	public boolean getInheritRotation () {
 		return inheritRotation;
 	}
@@ -165,6 +165,14 @@ public class BoneData {
 		this.inheritRotation = inheritRotation;
 	}
 
+	public boolean getInheritScale () {
+		return inheritScale;
+	}
+
+	public void setInheritScale (boolean inheritScale) {
+		this.inheritScale = inheritScale;
+	}
+
 	public Color getColor () {
 		return color;
 	}

+ 1 - 0
spine-libgdx/spine-libgdx/src/com/esotericsoftware/spine/Event.java

@@ -39,6 +39,7 @@ public class Event {
 	final float time;
 
 	public Event (float time, EventData data) {
+		if (data == null) throw new IllegalArgumentException("data cannot be null.");
 		this.time = time;
 		this.data = data;
 	}

+ 60 - 44
spine-libgdx/spine-libgdx/src/com/esotericsoftware/spine/IkConstraint.java

@@ -42,28 +42,32 @@ public class IkConstraint implements Updatable {
 	float mix = 1;
 	int bendDirection;
 
+	int level;
+
 	public IkConstraint (IkConstraintData data, Skeleton skeleton) {
+		if (data == null) throw new IllegalArgumentException("data cannot be null.");
+		if (skeleton == null) throw new IllegalArgumentException("skeleton cannot be null.");
 		this.data = data;
 		mix = data.mix;
 		bendDirection = data.bendDirection;
 
 		bones = new Array(data.bones.size);
-		if (skeleton != null) {
-			for (BoneData boneData : data.bones)
-				bones.add(skeleton.findBone(boneData.name));
-			target = skeleton.findBone(data.target.name);
-		}
+		for (BoneData boneData : data.bones)
+			bones.add(skeleton.findBone(boneData.name));
+		target = skeleton.findBone(data.target.name);
 	}
 
 	/** Copy constructor. */
-	public IkConstraint (IkConstraint ikConstraint, Skeleton skeleton) {
-		data = ikConstraint.data;
-		bones = new Array(ikConstraint.bones.size);
-		for (Bone bone : ikConstraint.bones)
-			bones.add(skeleton.bones.get(bone.skeleton.bones.indexOf(bone, true)));
-		target = skeleton.bones.get(ikConstraint.target.skeleton.bones.indexOf(ikConstraint.target, true));
-		mix = ikConstraint.mix;
-		bendDirection = ikConstraint.bendDirection;
+	public IkConstraint (IkConstraint constraint, Skeleton skeleton) {
+		if (constraint == null) throw new IllegalArgumentException("constraint cannot be null.");
+		if (skeleton == null) throw new IllegalArgumentException("skeleton cannot be null.");
+		data = constraint.data;
+		bones = new Array(constraint.bones.size);
+		for (Bone bone : constraint.bones)
+			bones.add(skeleton.bones.get(bone.data.index));
+		target = skeleton.bones.get(constraint.target.data.index);
+		mix = constraint.mix;
+		bendDirection = constraint.bendDirection;
 	}
 
 	public void apply () {
@@ -131,16 +135,19 @@ public class IkConstraint implements Updatable {
 		if (rotationIK > 180)
 			rotationIK -= 360;
 		else if (rotationIK < -180) rotationIK += 360;
-		bone.updateWorldTransform(bone.x, bone.y, bone.rotation + (rotationIK - bone.rotation) * alpha, bone.appliedScaleX,
-			bone.appliedScaleY, bone.shearX, bone.shearY);
+		bone.updateWorldTransform(bone.x, bone.y, bone.rotation + (rotationIK - bone.rotation) * alpha, bone.scaleX, bone.scaleY,
+			bone.shearX, bone.shearY);
 	}
 
 	/** 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.
 	 * @param child 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) return;
-		float px = parent.x, py = parent.y, psx = parent.appliedScaleX, psy = parent.appliedScaleY;
+		if (alpha == 0) {
+			child.updateWorldTransform();
+			return;
+		}
+		float px = parent.x, py = parent.y, psx = parent.scaleX, psy = parent.scaleY, csx = child.scaleX;
 		int os1, os2, s2;
 		if (psx < 0) {
 			psx = -psx;
@@ -154,25 +161,32 @@ public class IkConstraint implements Updatable {
 			psy = -psy;
 			s2 = -s2;
 		}
-		float cx = child.x, cy = child.y, csx = child.appliedScaleX;
-		boolean u = Math.abs(psx - psy) <= 0.0001f;
-		if (!u && cy != 0) {
-			child.worldX = parent.a * cx + parent.worldX;
-			child.worldY = parent.c * cx + parent.worldY;
-			cy = 0;
-		}
 		if (csx < 0) {
 			csx = -csx;
 			os2 = 180;
 		} else
 			os2 = 0;
+		float cx = child.x, cy, cwx, cwy, a = parent.a, b = parent.b, c = parent.c, d = parent.d;
+		boolean u = Math.abs(psx - psy) <= 0.0001f;
+		if (!u) {
+			cy = 0;
+			cwx = a * cx + parent.worldX;
+			cwy = c * cx + parent.worldY;
+		} else {
+			cy = child.y;
+			cwx = a * cx + b * cy + parent.worldX;
+			cwy = c * cx + d * cy + parent.worldY;
+		}
 		Bone pp = parent.parent;
-		float ppa = pp.a, ppb = pp.b, ppc = pp.c, ppd = pp.d, id = 1 / (ppa * ppd - ppb * ppc);
-		float x = targetX - pp.worldX, y = targetY - pp.worldY;
-		float tx = (x * ppd - y * ppb) * id - px, ty = (y * ppa - x * ppc) * id - py;
-		x = child.worldX - pp.worldX;
-		y = child.worldY - pp.worldY;
-		float dx = (x * ppd - y * ppb) * id - px, dy = (y * ppa - x * ppc) * id - py;
+		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;
 		outer:
 		if (u) {
@@ -182,18 +196,21 @@ public class IkConstraint implements Updatable {
 				cos = -1;
 			else if (cos > 1) cos = 1;
 			a2 = (float)Math.acos(cos) * bendDir;
-			float a = l1 + l2 * cos, o = l2 * sin(a2);
-			a1 = atan2(ty * a - tx * o, tx * a + ty * o);
+			a = l1 + l2 * cos;
+			b = l2 * sin(a2);
+			a1 = atan2(ty * a - tx * b, tx * a + ty * b);
 		} else {
-			float a = psx * l2, b = psy * l2, ta = atan2(ty, tx);
-			float aa = a * a, bb = b * b, ll = l1 * l1, dd = tx * tx + ty * ty;
-			float c0 = bb * ll + aa * dd - aa * bb, c1 = -2 * bb * l1, c2 = bb - aa;
-			float d = c1 * c1 - 4 * c2 * c0;
+			a = psx * l2;
+			b = psy * l2;
+			float aa = a * a, bb = b * b, dd = tx * tx + ty * ty, ta = 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 = c0 / q;
+				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;
@@ -243,18 +260,17 @@ public class IkConstraint implements Updatable {
 			}
 		}
 		float os = atan2(cy, cx) * s2;
-		a1 = (a1 - os) * radDeg + os1;
-		a2 = ((a2 + os) * radDeg - child.shearX) * s2 + os2;
+		float rotation = parent.rotation;
+		a1 = (a1 - os) * radDeg + os1 - rotation;
 		if (a1 > 180)
 			a1 -= 360;
 		else if (a1 < -180) a1 += 360;
+		parent.updateWorldTransform(px, py, rotation + a1 * alpha, parent.scaleX, parent.scaleY, 0, 0);
+		rotation = child.rotation;
+		a2 = ((a2 + os) * radDeg - child.shearX) * s2 + os2 - rotation;
 		if (a2 > 180)
 			a2 -= 360;
 		else if (a2 < -180) a2 += 360;
-		float rotation = parent.rotation;
-		parent.updateWorldTransform(px, py, rotation + (a1 - rotation) * alpha, parent.appliedScaleX, parent.appliedScaleY, 0, 0);
-		rotation = child.rotation;
-		child.updateWorldTransform(cx, cy, rotation + (a2 - rotation) * alpha, child.appliedScaleX, child.appliedScaleY,
-			child.shearX, child.shearY);
+		child.updateWorldTransform(cx, cy, rotation + a2 * alpha, child.scaleX, child.scaleY, child.shearX, child.shearY);
 	}
 }

+ 2 - 0
spine-libgdx/spine-libgdx/src/com/esotericsoftware/spine/IkConstraintData.java

@@ -41,6 +41,7 @@ public class IkConstraintData {
 	float mix = 1;
 
 	public IkConstraintData (String name) {
+		if (name == null) throw new IllegalArgumentException("name cannot be null.");
 		this.name = name;
 	}
 
@@ -57,6 +58,7 @@ public class IkConstraintData {
 	}
 
 	public void setTarget (BoneData target) {
+		if (target == null) throw new IllegalArgumentException("target cannot be null.");
 		this.target = target;
 	}
 

+ 436 - 0
spine-libgdx/spine-libgdx/src/com/esotericsoftware/spine/PathConstraint.java

@@ -0,0 +1,436 @@
+
+package com.esotericsoftware.spine;
+
+import static com.badlogic.gdx.math.MathUtils.*;
+
+import com.badlogic.gdx.utils.Array;
+import com.badlogic.gdx.utils.FloatArray;
+import com.esotericsoftware.spine.PathConstraintData.PositionMode;
+import com.esotericsoftware.spine.PathConstraintData.RotateMode;
+import com.esotericsoftware.spine.PathConstraintData.SpacingMode;
+import com.esotericsoftware.spine.attachments.Attachment;
+import com.esotericsoftware.spine.attachments.PathAttachment;
+
+public class PathConstraint implements Updatable {
+	static private final int NONE = -1, BEFORE = -2, AFTER = -3;
+
+	final PathConstraintData data;
+	final Array<Bone> bones;
+	Slot target;
+	float position, spacing, rotateMix, translateMix;
+
+	private final FloatArray spaces = new FloatArray(), positions = new FloatArray();
+	private final FloatArray world = new FloatArray(), curves = new FloatArray(), lengths = new FloatArray();
+	private final float[] segments = new float[10];
+
+	public PathConstraint (PathConstraintData data, Skeleton skeleton) {
+		if (data == null) throw new IllegalArgumentException("data cannot be null.");
+		if (skeleton == null) throw new IllegalArgumentException("skeleton cannot be null.");
+		this.data = data;
+		bones = new Array(data.bones.size);
+		for (BoneData boneData : data.bones)
+			bones.add(skeleton.findBone(boneData.name));
+		target = skeleton.findSlot(data.target.name);
+		position = data.position;
+		spacing = data.spacing;
+		rotateMix = data.rotateMix;
+		translateMix = data.translateMix;
+	}
+
+	/** Copy constructor. */
+	public PathConstraint (PathConstraint constraint, Skeleton skeleton) {
+		if (constraint == null) throw new IllegalArgumentException("constraint cannot be null.");
+		if (skeleton == null) throw new IllegalArgumentException("skeleton cannot be null.");
+		data = constraint.data;
+		bones = new Array(constraint.bones.size);
+		for (Bone bone : constraint.bones)
+			bones.add(skeleton.bones.get(bone.data.index));
+		target = skeleton.slots.get(constraint.target.data.index);
+		position = constraint.position;
+		spacing = constraint.spacing;
+		rotateMix = constraint.rotateMix;
+		translateMix = constraint.translateMix;
+	}
+
+	public void apply () {
+		update();
+	}
+
+	@SuppressWarnings("null")
+	public void update () {
+		Attachment attachment = target.getAttachment();
+		if (!(attachment instanceof PathAttachment)) return;
+
+		float rotateMix = this.rotateMix, translateMix = this.translateMix;
+		boolean translate = translateMix > 0, rotate = rotateMix > 0;
+		if (!translate && !rotate) return;
+
+		PathConstraintData data = this.data;
+		SpacingMode spacingMode = data.spacingMode;
+		boolean lengthSpacing = spacingMode == SpacingMode.length;
+		RotateMode rotateMode = data.rotateMode;
+		boolean tangents = rotateMode == RotateMode.tangent, scale = rotateMode == RotateMode.chainScale;
+		int boneCount = this.bones.size, spacesCount = tangents ? boneCount : boneCount + 1;
+		Object[] bones = this.bones.items;
+		float[] spaces = this.spaces.setSize(spacesCount), lengths = null;
+		float spacing = this.spacing;
+		if (scale || lengthSpacing) {
+			if (scale) lengths = this.lengths.setSize(boneCount);
+			for (int i = 0, n = spacesCount - 1; i < n;) {
+				Bone bone = (Bone)bones[i];
+				float length = bone.data.length, x = length * bone.a, y = length * bone.c;
+				length = (float)Math.sqrt(x * x + y * y);
+				if (scale) lengths[i] = length;
+				spaces[++i] = lengthSpacing ? Math.max(0, length + spacing) : spacing;
+			}
+		} else {
+			for (int i = 1; i < spacesCount; i++)
+				spaces[i] = spacing;
+		}
+
+		float[] positions = computeWorldPositions((PathAttachment)attachment, spacesCount, tangents,
+			data.positionMode == PositionMode.percent, spacingMode == SpacingMode.percent);
+		Skeleton skeleton = target.getSkeleton();
+		float skeletonX = skeleton.x, skeletonY = skeleton.y;
+		float boneX = positions[0], boneY = positions[1], offsetRotation = data.offsetRotation;
+		boolean tip = rotateMode == RotateMode.chain && offsetRotation == 0;
+		for (int i = 0, p = 3; i < boneCount; i++, p += 3) {
+			Bone bone = (Bone)bones[i];
+			bone.worldX += (boneX - skeletonX - bone.worldX) * translateMix;
+			bone.worldY += (boneY - skeletonY - bone.worldY) * translateMix;
+			float x = positions[p], y = positions[p + 1], dx = x - boneX, dy = y - boneY;
+			if (scale) {
+				float length = lengths[i];
+				if (length != 0) {
+					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[i + 1] == 0)
+					r = positions[p + 2];
+				else
+					r = atan2(dy, dx);
+				r -= atan2(c, a) - offsetRotation * degRad;
+				if (tip) {
+					cos = cos(r);
+					sin = sin(r);
+					float length = bone.data.length;
+					boneX += (length * (cos * a - sin * c) - dx) * rotateMix;
+					boneY += (length * (sin * a + cos * c) - dy) * rotateMix;
+				}
+				if (r > PI)
+					r -= PI2;
+				else if (r < -PI) //
+					r += PI2;
+				r *= rotateMix;
+				cos = cos(r);
+				sin = 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;
+			}
+		}
+	}
+
+	private float[] computeWorldPositions (PathAttachment path, int spacesCount, boolean tangents, boolean percentPosition,
+		boolean percentSpacing) {
+		Slot target = this.target;
+		float position = this.position;
+		float[] spaces = this.spaces.items, out = this.positions.setSize(spacesCount * 3 + 2), world;
+		boolean closed = path.getClosed();
+		int verticesLength = path.getWorldVerticesLength(), curveCount = verticesLength / 6, prevCurve = NONE;
+
+		if (!path.getConstantSpeed()) {
+			float[] lengths = path.getLengths();
+			curveCount -= closed ? 1 : 2;
+			float pathLength = lengths[curveCount];
+			if (percentPosition) position *= pathLength;
+			if (percentSpacing) {
+				for (int i = 0; i < spacesCount; i++)
+					spaces[i] *= pathLength;
+			}
+			world = this.world.setSize(8);
+			for (int i = 0, o = 0, curve = 0; i < spacesCount; i++, o += 3) {
+				float space = spaces[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, out, o);
+					continue;
+				} else if (p > pathLength) {
+					if (prevCurve != AFTER) {
+						prevCurve = AFTER;
+						path.computeWorldVertices(target, verticesLength - 6, 4, world, 0);
+					}
+					addAfterPosition(p - pathLength, world, 0, out, 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], out, o,
+					tangents || (i > 0 && space == 0));
+			}
+			return out;
+		}
+
+		// World vertices.
+		if (closed) {
+			verticesLength += 2;
+			world = this.world.setSize(verticesLength);
+			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.setSize(verticesLength);
+			path.computeWorldVertices(target, 2, verticesLength, world, 0);
+		}
+
+		// Curve lengths.
+		float[] curves = this.curves.setSize(curveCount);
+		float 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++)
+				spaces[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 = spaces[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, out, o);
+				continue;
+			} else if (p > pathLength) {
+				addAfterPosition(p - pathLength, world, verticesLength - 4, out, 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, out, o, tangents || (i > 0 && space == 0));
+		}
+		return out;
+	}
+
+	private void addBeforePosition (float p, float[] temp, int i, float[] out, int o) {
+		float x1 = temp[i], y1 = temp[i + 1], dx = temp[i + 2] - x1, dy = temp[i + 3] - y1, r = atan2(dy, dx);
+		out[o] = x1 + p * cos(r);
+		out[o + 1] = y1 + p * sin(r);
+		out[o + 2] = r;
+	}
+
+	private void addAfterPosition (float p, float[] temp, int i, float[] out, int o) {
+		float x1 = temp[i + 2], y1 = temp[i + 3], dx = x1 - temp[i], dy = y1 - temp[i + 1], r = atan2(dy, dx);
+		out[o] = x1 + p * cos(r);
+		out[o + 1] = y1 + p * sin(r);
+		out[o + 2] = r;
+	}
+
+	private void addCurvePosition (float p, float x1, float y1, float cx1, float cy1, float cx2, float cy2, float x2, float y2,
+		float[] out, int o, boolean tangents) {
+		if (p == 0) p = 0.0001f;
+		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;
+		out[o] = x;
+		out[o + 1] = y;
+		if (tangents) out[o + 2] = atan2(y - (y1 * uu + cy1 * ut * 2 + cy2 * tt), x - (x1 * uu + cx1 * ut * 2 + cx2 * tt));
+	}
+
+	public float getPosition () {
+		return position;
+	}
+
+	public void setPosition (float position) {
+		this.position = position;
+	}
+
+	public float getSpacing () {
+		return spacing;
+	}
+
+	public void setSpacing (float spacing) {
+		this.spacing = spacing;
+	}
+
+	public float getRotateMix () {
+		return rotateMix;
+	}
+
+	public void setRotateMix (float rotateMix) {
+		this.rotateMix = rotateMix;
+	}
+
+	public float getTranslateMix () {
+		return translateMix;
+	}
+
+	public void setTranslateMix (float translateMix) {
+		this.translateMix = translateMix;
+	}
+
+	public Array<Bone> getBones () {
+		return bones;
+	}
+
+	public Slot getTarget () {
+		return target;
+	}
+
+	public void setTarget (Slot target) {
+		this.target = target;
+	}
+
+	public PathConstraintData getData () {
+		return data;
+	}
+
+	public String toString () {
+		return data.name;
+	}
+}

+ 122 - 0
spine-libgdx/spine-libgdx/src/com/esotericsoftware/spine/PathConstraintData.java

@@ -0,0 +1,122 @@
+
+package com.esotericsoftware.spine;
+
+import com.badlogic.gdx.utils.Array;
+
+public class PathConstraintData {
+	final String name;
+	final Array<BoneData> bones = new Array();
+	SlotData target;
+	PositionMode positionMode;
+	SpacingMode spacingMode;
+	RotateMode rotateMode;
+	float offsetRotation;
+	float position, spacing, rotateMix, translateMix;
+
+	public PathConstraintData (String name) {
+		if (name == null) throw new IllegalArgumentException("name cannot be null.");
+		this.name = name;
+	}
+
+	public Array<BoneData> getBones () {
+		return bones;
+	}
+
+	public SlotData getTarget () {
+		return target;
+	}
+
+	public void setTarget (SlotData target) {
+		this.target = target;
+	}
+
+	public PositionMode getPositionMode () {
+		return positionMode;
+	}
+
+	public void setPositionMode (PositionMode positionMode) {
+		this.positionMode = positionMode;
+	}
+
+	public SpacingMode getSpacingMode () {
+		return spacingMode;
+	}
+
+	public void setSpacingMode (SpacingMode spacingMode) {
+		this.spacingMode = spacingMode;
+	}
+
+	public RotateMode getRotateMode () {
+		return rotateMode;
+	}
+
+	public void setRotateMode (RotateMode rotateMode) {
+		this.rotateMode = rotateMode;
+	}
+
+	public float getOffsetRotation () {
+		return offsetRotation;
+	}
+
+	public void setOffsetRotation (float offsetRotation) {
+		this.offsetRotation = offsetRotation;
+	}
+
+	public float getPosition () {
+		return position;
+	}
+
+	public void setPosition (float position) {
+		this.position = position;
+	}
+
+	public float getSpacing () {
+		return spacing;
+	}
+
+	public void setSpacing (float spacing) {
+		this.spacing = spacing;
+	}
+
+	public float getRotateMix () {
+		return rotateMix;
+	}
+
+	public void setRotateMix (float rotateMix) {
+		this.rotateMix = rotateMix;
+	}
+
+	public float getTranslateMix () {
+		return translateMix;
+	}
+
+	public void setTranslateMix (float translateMix) {
+		this.translateMix = translateMix;
+	}
+
+	public String getName () {
+		return name;
+	}
+
+	public String toString () {
+		return name;
+	}
+
+	static public enum PositionMode {
+		fixed, percent;
+
+		static public final PositionMode[] values = PositionMode.values();
+	}
+
+	static public enum SpacingMode {
+		length, fixed, percent;
+
+		static public final SpacingMode[] values = SpacingMode.values();
+	}
+
+	static public enum RotateMode {
+		tangent, chain, chainScale;
+
+		static public final RotateMode[] values = RotateMode.values();
+	}
+}

+ 186 - 38
spine-libgdx/spine-libgdx/src/com/esotericsoftware/spine/Skeleton.java

@@ -34,19 +34,22 @@ package com.esotericsoftware.spine;
 import com.badlogic.gdx.graphics.Color;
 import com.badlogic.gdx.math.Vector2;
 import com.badlogic.gdx.utils.Array;
+import com.badlogic.gdx.utils.ObjectMap.Entry;
+import com.esotericsoftware.spine.Skin.Key;
 import com.esotericsoftware.spine.attachments.Attachment;
 import com.esotericsoftware.spine.attachments.MeshAttachment;
+import com.esotericsoftware.spine.attachments.PathAttachment;
 import com.esotericsoftware.spine.attachments.RegionAttachment;
-import com.esotericsoftware.spine.attachments.WeightedMeshAttachment;
 
 public class Skeleton {
 	final SkeletonData data;
 	final Array<Bone> bones;
 	final Array<Slot> slots;
 	Array<Slot> drawOrder;
-	final Array<IkConstraint> ikConstraints;
+	final Array<IkConstraint> ikConstraints, ikConstraintsSorted;
 	final Array<TransformConstraint> transformConstraints;
-	private final Array<Updatable> updateCache = new Array();
+	final Array<PathConstraint> pathConstraints;
+	final Array<Updatable> updateCache = new Array();
 	Skin skin;
 	final Color color;
 	float time;
@@ -59,20 +62,28 @@ public class Skeleton {
 
 		bones = new Array(data.bones.size);
 		for (BoneData boneData : data.bones) {
-			Bone parent = boneData.parent == null ? null : bones.get(data.bones.indexOf(boneData.parent, true));
-			bones.add(new Bone(boneData, this, parent));
+			Bone bone;
+			if (boneData.parent == null)
+				bone = new Bone(boneData, this, null);
+			else {
+				Bone parent = bones.get(boneData.parent.index);
+				bone = new Bone(boneData, this, parent);
+				parent.children.add(bone);
+			}
+			bones.add(bone);
 		}
 
 		slots = new Array(data.slots.size);
 		drawOrder = new Array(data.slots.size);
 		for (SlotData slotData : data.slots) {
-			Bone bone = bones.get(data.bones.indexOf(slotData.boneData, true));
+			Bone bone = bones.get(slotData.boneData.index);
 			Slot slot = new Slot(slotData, bone);
 			slots.add(slot);
 			drawOrder.add(slot);
 		}
 
 		ikConstraints = new Array(data.ikConstraints.size);
+		ikConstraintsSorted = new Array(ikConstraints.size);
 		for (IkConstraintData ikConstraintData : data.ikConstraints)
 			ikConstraints.add(new IkConstraint(ikConstraintData, this));
 
@@ -80,6 +91,10 @@ public class Skeleton {
 		for (TransformConstraintData transformConstraintData : data.transformConstraints)
 			transformConstraints.add(new TransformConstraint(transformConstraintData, this));
 
+		pathConstraints = new Array(data.pathConstraints.size);
+		for (PathConstraintData pathConstraintData : data.pathConstraints)
+			pathConstraints.add(new PathConstraint(pathConstraintData, this));
+
 		color = new Color(1, 1, 1, 1);
 
 		updateCache();
@@ -92,21 +107,22 @@ public class Skeleton {
 
 		bones = new Array(skeleton.bones.size);
 		for (Bone bone : skeleton.bones) {
-			Bone parent = bone.parent == null ? null : bones.get(skeleton.bones.indexOf(bone.parent, true));
+			Bone parent = bone.parent == null ? null : bones.get(bone.parent.data.index);
 			bones.add(new Bone(bone, this, parent));
 		}
 
 		slots = new Array(skeleton.slots.size);
 		for (Slot slot : skeleton.slots) {
-			Bone bone = bones.get(skeleton.bones.indexOf(slot.bone, true));
+			Bone bone = bones.get(slot.bone.data.index);
 			slots.add(new Slot(slot, bone));
 		}
 
 		drawOrder = new Array(slots.size);
 		for (Slot slot : skeleton.drawOrder)
-			drawOrder.add(slots.get(skeleton.slots.indexOf(slot, true)));
+			drawOrder.add(slots.get(slot.data.index));
 
 		ikConstraints = new Array(skeleton.ikConstraints.size);
+		ikConstraintsSorted = new Array(ikConstraints.size);
 		for (IkConstraint ikConstraint : skeleton.ikConstraints)
 			ikConstraints.add(new IkConstraint(ikConstraint, this));
 
@@ -114,6 +130,10 @@ public class Skeleton {
 		for (TransformConstraint transformConstraint : skeleton.transformConstraints)
 			transformConstraints.add(new TransformConstraint(transformConstraint, this));
 
+		pathConstraints = new Array(skeleton.pathConstraints.size);
+		for (PathConstraint pathConstraint : skeleton.pathConstraints)
+			pathConstraints.add(new PathConstraint(pathConstraint, this));
+
 		skin = skeleton.skin;
 		color = new Color(skeleton.color);
 		time = skeleton.time;
@@ -123,36 +143,135 @@ public class Skeleton {
 		updateCache();
 	}
 
-	/** Caches information about bones and constraints. Must be called if bones or constraints are added or removed. */
+	/** Caches information about bones and constraints. Must be called if bones, constraints, or weighted path attachments are
+	 * added or removed. */
 	public void updateCache () {
-		Array<Bone> bones = this.bones;
 		Array<Updatable> updateCache = this.updateCache;
-		Array<IkConstraint> ikConstraints = this.ikConstraints;
-		Array<TransformConstraint> transformConstraints = this.transformConstraints;
-		int ikConstraintsCount = ikConstraints.size;
-		int transformConstraintsCount = transformConstraints.size;
 		updateCache.clear();
 
-		for (int i = 0, n = bones.size; i < n; i++) {
-			Bone bone = bones.get(i);
-			updateCache.add(bone);
-			for (int ii = 0; ii < ikConstraintsCount; ii++) {
-				IkConstraint ikConstraint = ikConstraints.get(ii);
-				if (bone == ikConstraint.bones.peek()) {
-					updateCache.add(ikConstraint);
-					break;
-				}
+		Array<Bone> bones = this.bones;
+		for (int i = 0, n = bones.size; i < n; i++)
+			bones.get(i).sorted = false;
+
+		// IK first, lowest hierarchy depth first.
+		Array<IkConstraint> ikConstraints = this.ikConstraintsSorted;
+		ikConstraints.clear();
+		ikConstraints.addAll(this.ikConstraints);
+		int ikCount = ikConstraints.size;
+		for (int i = 0, level, n = ikCount; i < n; i++) {
+			IkConstraint ik = ikConstraints.get(i);
+			Bone bone = ik.bones.first().parent;
+			for (level = 0; bone != null; level++)
+				bone = bone.parent;
+			ik.level = level;
+		}
+		for (int i = 1, ii; i < ikCount; i++) {
+			IkConstraint ik = ikConstraints.get(i);
+			int level = ik.level;
+			for (ii = i - 1; ii >= 0; ii--) {
+				IkConstraint other = ikConstraints.get(ii);
+				if (other.level < level) break;
+				ikConstraints.set(ii + 1, other);
 			}
+			ikConstraints.set(ii + 1, ik);
 		}
+		for (int i = 0, n = ikConstraints.size; i < n; i++) {
+			IkConstraint constraint = ikConstraints.get(i);
+			Bone target = constraint.target;
+			sortBone(target);
 
-		for (int i = 0; i < transformConstraintsCount; i++) {
-			TransformConstraint transformConstraint = transformConstraints.get(i);
-			for (int ii = updateCache.size - 1; ii >= 0; ii--) {
-				if (updateCache.get(ii) == transformConstraint.bone) {
-					updateCache.insert(ii + 1, transformConstraint);
-					break;
-				}
-			}
+			Array<Bone> constrained = constraint.bones;
+			Bone parent = constrained.first();
+			sortBone(parent);
+
+			updateCache.add(constraint);
+
+			sortReset(parent.children);
+			constrained.peek().sorted = true;
+		}
+
+		Array<PathConstraint> pathConstraints = this.pathConstraints;
+		for (int i = 0, n = pathConstraints.size; i < n; i++) {
+			PathConstraint constraint = pathConstraints.get(i);
+
+			Slot slot = constraint.target;
+			int slotIndex = slot.getData().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.size; ii < nn; ii++)
+				sortPathConstraintAttachment(data.skins.get(ii), slotIndex, slotBone);
+
+			Attachment attachment = slot.getAttachment();
+			if (attachment instanceof PathAttachment) sortPathConstraintAttachment(attachment, slotBone);
+
+			Array<Bone> constrained = constraint.bones;
+			int boneCount = constrained.size;
+			for (int ii = 0; ii < boneCount; ii++)
+				sortBone(constrained.get(ii));
+
+			updateCache.add(constraint);
+
+			for (int ii = 0; ii < boneCount; ii++)
+				sortReset(constrained.get(ii).children);
+			for (int ii = 0; ii < boneCount; ii++)
+				constrained.get(ii).sorted = true;
+		}
+
+		Array<TransformConstraint> transformConstraints = this.transformConstraints;
+		for (int i = 0, n = transformConstraints.size; i < n; i++) {
+			TransformConstraint constraint = transformConstraints.get(i);
+
+			sortBone(constraint.target);
+
+			Array<Bone> constrained = constraint.bones;
+			int boneCount = constrained.size;
+			for (int ii = 0; ii < boneCount; ii++)
+				sortBone(constrained.get(ii));
+
+			updateCache.add(constraint);
+
+			for (int ii = 0; ii < boneCount; ii++)
+				sortReset(constrained.get(ii).children);
+			for (int ii = 0; ii < boneCount; ii++)
+				constrained.get(ii).sorted = true;
+		}
+
+		for (int i = 0, n = bones.size; i < n; i++)
+			sortBone(bones.get(i));
+	}
+
+	private void sortPathConstraintAttachment (Skin skin, int slotIndex, Bone slotBone) {
+		for (Entry<Key, Attachment> entry : skin.attachments.entries())
+			if (entry.key.slotIndex == slotIndex) sortPathConstraintAttachment(entry.value, slotBone);
+	}
+
+	private void sortPathConstraintAttachment (Attachment attachment, Bone slotBone) {
+		if (!(attachment instanceof PathAttachment)) return;
+		int[] pathBones = ((PathAttachment)attachment).getBones();
+		if (pathBones == null)
+			sortBone(slotBone);
+		else {
+			Array<Bone> bones = this.bones;
+			for (int boneIndex : pathBones)
+				sortBone(bones.get(boneIndex));
+		}
+	}
+
+	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 void sortReset (Array<Bone> bones) {
+		for (int i = 0, n = bones.size; i < n; i++) {
+			Bone bone = bones.get(i);
+			if (bone.sorted) sortReset(bone.children);
+			bone.sorted = false;
 		}
 	}
 
@@ -191,13 +310,23 @@ public class Skeleton {
 			constraint.scaleMix = data.scaleMix;
 			constraint.shearMix = data.shearMix;
 		}
+
+		Array<PathConstraint> pathConstraints = this.pathConstraints;
+		for (int i = 0, n = pathConstraints.size; i < n; i++) {
+			PathConstraint constraint = pathConstraints.get(i);
+			PathConstraintData data = constraint.data;
+			constraint.position = data.position;
+			constraint.spacing = data.spacing;
+			constraint.rotateMix = data.rotateMix;
+			constraint.translateMix = data.translateMix;
+		}
 	}
 
 	public void setSlotsToSetupPose () {
 		Array<Slot> slots = this.slots;
 		System.arraycopy(slots.items, 0, drawOrder.items, 0, slots.size);
 		for (int i = 0, n = slots.size; i < n; i++)
-			slots.get(i).setToSetupPose(i);
+			slots.get(i).setToSetupPose();
 	}
 
 	public SkeletonData getData () {
@@ -208,6 +337,10 @@ public class Skeleton {
 		return bones;
 	}
 
+	public Array<Updatable> getUpdateCache () {
+		return updateCache;
+	}
+
 	/** @return May return null. */
 	public Bone getRootBone () {
 		if (bones.size == 0) return null;
@@ -265,6 +398,7 @@ public class Skeleton {
 
 	/** Sets the slots and the order they will be drawn. */
 	public void setDrawOrder (Array<Slot> drawOrder) {
+		if (drawOrder == null) throw new IllegalArgumentException("drawOrder cannot be null.");
 		this.drawOrder = drawOrder;
 	}
 
@@ -370,10 +504,27 @@ public class Skeleton {
 		return null;
 	}
 
-	/** Returns the axis aligned bounding box (AABB) of the region, mesh, and skinned mesh attachments for the current pose.
+	public Array<PathConstraint> getPathConstraints () {
+		return pathConstraints;
+	}
+
+	/** @return May be null. */
+	public PathConstraint findPathConstraint (String constraintName) {
+		if (constraintName == null) throw new IllegalArgumentException("constraintName cannot be null.");
+		Array<PathConstraint> pathConstraints = this.pathConstraints;
+		for (int i = 0, n = pathConstraints.size; i < n; i++) {
+			PathConstraint constraint = pathConstraints.get(i);
+			if (constraint.data.name.equals(constraintName)) return constraint;
+		}
+		return null;
+	}
+
+	/** Returns the axis aligned bounding box (AABB) of the region and mesh attachments for the current pose.
 	 * @param offset The distance from the skeleton origin to the bottom left corner of the AABB.
 	 * @param size The width and height of the AABB. */
 	public void getBounds (Vector2 offset, Vector2 size) {
+		if (offset == null) throw new IllegalArgumentException("offset cannot be null.");
+		if (size == null) throw new IllegalArgumentException("size cannot be null.");
 		Array<Slot> drawOrder = this.drawOrder;
 		float minX = Integer.MAX_VALUE, minY = Integer.MAX_VALUE, maxX = Integer.MIN_VALUE, maxY = Integer.MIN_VALUE;
 		for (int i = 0, n = drawOrder.size; i < n; i++) {
@@ -382,12 +533,8 @@ public class Skeleton {
 			Attachment attachment = slot.attachment;
 			if (attachment instanceof RegionAttachment) {
 				vertices = ((RegionAttachment)attachment).updateWorldVertices(slot, false);
-
 			} else if (attachment instanceof MeshAttachment) {
 				vertices = ((MeshAttachment)attachment).updateWorldVertices(slot, true);
-
-			} else if (attachment instanceof WeightedMeshAttachment) {
-				vertices = ((WeightedMeshAttachment)attachment).updateWorldVertices(slot, true);
 			}
 			if (vertices != null) {
 				for (int ii = 0, nn = vertices.length; ii < nn; ii += 5) {
@@ -409,6 +556,7 @@ public class Skeleton {
 
 	/** A convenience method for setting the skeleton color. The color can also be set by modifying {@link #getColor()}. */
 	public void setColor (Color color) {
+		if (color == null) throw new IllegalArgumentException("color cannot be null.");
 		this.color.set(color);
 	}
 

+ 237 - 183
spine-libgdx/spine-libgdx/src/com/esotericsoftware/spine/SkeletonBinary.java

@@ -31,6 +31,7 @@
 
 package com.esotericsoftware.spine;
 
+import java.io.EOFException;
 import java.io.IOException;
 
 import com.badlogic.gdx.files.FileHandle;
@@ -44,16 +45,22 @@ import com.badlogic.gdx.utils.SerializationException;
 import com.esotericsoftware.spine.Animation.AttachmentTimeline;
 import com.esotericsoftware.spine.Animation.ColorTimeline;
 import com.esotericsoftware.spine.Animation.CurveTimeline;
+import com.esotericsoftware.spine.Animation.DeformTimeline;
 import com.esotericsoftware.spine.Animation.DrawOrderTimeline;
 import com.esotericsoftware.spine.Animation.EventTimeline;
-import com.esotericsoftware.spine.Animation.FfdTimeline;
 import com.esotericsoftware.spine.Animation.IkConstraintTimeline;
+import com.esotericsoftware.spine.Animation.PathConstraintMixTimeline;
+import com.esotericsoftware.spine.Animation.PathConstraintPositionTimeline;
+import com.esotericsoftware.spine.Animation.PathConstraintSpacingTimeline;
 import com.esotericsoftware.spine.Animation.RotateTimeline;
 import com.esotericsoftware.spine.Animation.ScaleTimeline;
 import com.esotericsoftware.spine.Animation.ShearTimeline;
 import com.esotericsoftware.spine.Animation.Timeline;
 import com.esotericsoftware.spine.Animation.TransformConstraintTimeline;
 import com.esotericsoftware.spine.Animation.TranslateTimeline;
+import com.esotericsoftware.spine.PathConstraintData.PositionMode;
+import com.esotericsoftware.spine.PathConstraintData.RotateMode;
+import com.esotericsoftware.spine.PathConstraintData.SpacingMode;
 import com.esotericsoftware.spine.SkeletonJson.LinkedMesh;
 import com.esotericsoftware.spine.attachments.AtlasAttachmentLoader;
 import com.esotericsoftware.spine.attachments.Attachment;
@@ -61,16 +68,22 @@ import com.esotericsoftware.spine.attachments.AttachmentLoader;
 import com.esotericsoftware.spine.attachments.AttachmentType;
 import com.esotericsoftware.spine.attachments.BoundingBoxAttachment;
 import com.esotericsoftware.spine.attachments.MeshAttachment;
+import com.esotericsoftware.spine.attachments.PathAttachment;
 import com.esotericsoftware.spine.attachments.RegionAttachment;
-import com.esotericsoftware.spine.attachments.WeightedMeshAttachment;
+import com.esotericsoftware.spine.attachments.VertexAttachment;
 
 public class SkeletonBinary {
-	static public final int TIMELINE_ROTATE = 0;
-	static public final int TIMELINE_TRANSLATE = 1;
-	static public final int TIMELINE_SCALE = 2;
-	static public final int TIMELINE_SHEAR = 3;
-	static public final int TIMELINE_ATTACHMENT = 4;
-	static public final int TIMELINE_COLOR = 5;
+	static public final int BONE_ROTATE = 0;
+	static public final int BONE_TRANSLATE = 1;
+	static public final int BONE_SCALE = 2;
+	static public final int BONE_SHEAR = 3;
+
+	static public final int SLOT_ATTACHMENT = 0;
+	static public final int SLOT_COLOR = 1;
+
+	static public final int PATH_POSITION = 0;
+	static public final int PATH_SPACING = 1;
+	static public final int PATH_MIX = 2;
 
 	static public final int CURVE_LINEAR = 0;
 	static public final int CURVE_STEPPED = 1;
@@ -87,6 +100,7 @@ public class SkeletonBinary {
 	}
 
 	public SkeletonBinary (AttachmentLoader attachmentLoader) {
+		if (attachmentLoader == null) throw new IllegalArgumentException("attachmentLoader cannot be null.");
 		this.attachmentLoader = attachmentLoader;
 	}
 
@@ -125,6 +139,8 @@ public class SkeletonBinary {
 				for (int i = 0; i < byteCount;) {
 					int b = read();
 					switch (b >> 4) {
+					case -1:
+						throw new EOFException();
 					case 12:
 					case 13:
 						chars[charCount++] = (char)((b & 0x1F) << 6 | read() & 0x3F);
@@ -161,59 +177,79 @@ public class SkeletonBinary {
 			for (int i = 0, n = input.readInt(true); i < n; i++) {
 				String name = input.readString();
 				BoneData parent = i == 0 ? null : skeletonData.bones.get(input.readInt(true));
-				BoneData boneData = new BoneData(name, parent);
-				boneData.rotation = input.readFloat();
-				boneData.x = input.readFloat() * scale;
-				boneData.y = input.readFloat() * scale;
-				boneData.scaleX = input.readFloat();
-				boneData.scaleY = input.readFloat();
-				boneData.shearX = input.readFloat();
-				boneData.shearY = input.readFloat();
-				boneData.length = input.readFloat() * scale;
-				boneData.inheritRotation = input.readBoolean();
-				boneData.inheritScale = input.readBoolean();
-				if (nonessential) Color.rgba8888ToColor(boneData.color, input.readInt());
-				skeletonData.bones.add(boneData);
+				BoneData data = new BoneData(i, name, parent);
+				data.rotation = input.readFloat();
+				data.x = input.readFloat() * scale;
+				data.y = input.readFloat() * scale;
+				data.scaleX = input.readFloat();
+				data.scaleY = input.readFloat();
+				data.shearX = input.readFloat();
+				data.shearY = input.readFloat();
+				data.length = input.readFloat() * scale;
+				data.inheritRotation = input.readBoolean();
+				data.inheritScale = input.readBoolean();
+				if (nonessential) Color.rgba8888ToColor(data.color, input.readInt());
+				skeletonData.bones.add(data);
+			}
+
+			// Slots.
+			for (int i = 0, n = input.readInt(true); i < n; i++) {
+				String slotName = input.readString();
+				BoneData boneData = skeletonData.bones.get(input.readInt(true));
+				SlotData data = new SlotData(i, slotName, boneData);
+				Color.rgba8888ToColor(data.color, input.readInt());
+				data.attachmentName = input.readString();
+				data.blendMode = BlendMode.values[input.readInt(true)];
+				skeletonData.slots.add(data);
 			}
 
 			// IK constraints.
 			for (int i = 0, n = input.readInt(true); i < n; i++) {
-				IkConstraintData ikConstraintData = new IkConstraintData(input.readString());
+				IkConstraintData data = new IkConstraintData(input.readString());
 				for (int ii = 0, nn = input.readInt(true); ii < nn; ii++)
-					ikConstraintData.bones.add(skeletonData.bones.get(input.readInt(true)));
-				ikConstraintData.target = skeletonData.bones.get(input.readInt(true));
-				ikConstraintData.mix = input.readFloat();
-				ikConstraintData.bendDirection = input.readByte();
-				skeletonData.ikConstraints.add(ikConstraintData);
+					data.bones.add(skeletonData.bones.get(input.readInt(true)));
+				data.target = skeletonData.bones.get(input.readInt(true));
+				data.mix = input.readFloat();
+				data.bendDirection = input.readByte();
+				skeletonData.ikConstraints.add(data);
 			}
 
 			// Transform constraints.
 			for (int i = 0, n = input.readInt(true); i < n; i++) {
-				TransformConstraintData transformConstraintData = new TransformConstraintData(input.readString());
-				transformConstraintData.bone = skeletonData.bones.get(input.readInt(true));
-				transformConstraintData.target = skeletonData.bones.get(input.readInt(true));
-				transformConstraintData.offsetRotation = input.readFloat();
-				transformConstraintData.offsetX = input.readFloat() * scale;
-				transformConstraintData.offsetY = input.readFloat() * scale;
-				transformConstraintData.offsetScaleX = input.readFloat();
-				transformConstraintData.offsetScaleY = input.readFloat();
-				transformConstraintData.offsetShearY = input.readFloat();
-				transformConstraintData.rotateMix = input.readFloat();
-				transformConstraintData.translateMix = input.readFloat();
-				transformConstraintData.scaleMix = input.readFloat();
-				transformConstraintData.shearMix = input.readFloat();
-				skeletonData.transformConstraints.add(transformConstraintData);
+				TransformConstraintData data = new TransformConstraintData(input.readString());
+				for (int ii = 0, nn = input.readInt(true); ii < nn; ii++)
+					data.bones.add(skeletonData.bones.get(input.readInt(true)));
+				data.target = skeletonData.bones.get(input.readInt(true));
+				data.offsetRotation = input.readFloat();
+				data.offsetX = input.readFloat() * scale;
+				data.offsetY = input.readFloat() * scale;
+				data.offsetScaleX = input.readFloat();
+				data.offsetScaleY = input.readFloat();
+				data.offsetShearY = input.readFloat();
+				data.rotateMix = input.readFloat();
+				data.translateMix = input.readFloat();
+				data.scaleMix = input.readFloat();
+				data.shearMix = input.readFloat();
+				skeletonData.transformConstraints.add(data);
 			}
 
-			// Slots.
+			// Path constraints.
 			for (int i = 0, n = input.readInt(true); i < n; i++) {
-				String slotName = input.readString();
-				BoneData boneData = skeletonData.bones.get(input.readInt(true));
-				SlotData slotData = new SlotData(slotName, boneData);
-				Color.rgba8888ToColor(slotData.color, input.readInt());
-				slotData.attachmentName = input.readString();
-				slotData.blendMode = BlendMode.values[input.readInt(true)];
-				skeletonData.slots.add(slotData);
+				PathConstraintData data = new PathConstraintData(input.readString());
+				for (int ii = 0, nn = input.readInt(true); ii < nn; ii++)
+					data.bones.add(skeletonData.bones.get(input.readInt(true)));
+				data.target = skeletonData.slots.get(input.readInt(true));
+				data.positionMode = PositionMode.values[input.readInt(true)];
+				data.spacingMode = SpacingMode.values[input.readInt(true)];
+				data.rotateMode = RotateMode.values[input.readInt(true)];
+				data.offsetRotation = input.readFloat();
+				data.position = input.readFloat();
+				if (data.positionMode == PositionMode.fixed) data.position *= scale;
+				data.spacing = input.readFloat();
+				if (data.spacingMode == SpacingMode.length || data.spacingMode == SpacingMode.fixed) data.spacing *= scale;
+				data.rotateMix = input.readFloat();
+				data.translateMix = input.readFloat();
+				skeletonData.pathConstraints.add(data);
 			}
 
 			// Default skin.
@@ -234,25 +270,18 @@ public class SkeletonBinary {
 				if (skin == null) throw new SerializationException("Skin not found: " + linkedMesh.skin);
 				Attachment parent = skin.getAttachment(linkedMesh.slotIndex, linkedMesh.parent);
 				if (parent == null) throw new SerializationException("Parent mesh not found: " + linkedMesh.parent);
-				if (linkedMesh.mesh instanceof MeshAttachment) {
-					MeshAttachment mesh = (MeshAttachment)linkedMesh.mesh;
-					mesh.setParentMesh((MeshAttachment)parent);
-					mesh.updateUVs();
-				} else {
-					WeightedMeshAttachment mesh = (WeightedMeshAttachment)linkedMesh.mesh;
-					mesh.setParentMesh((WeightedMeshAttachment)parent);
-					mesh.updateUVs();
-				}
+				linkedMesh.mesh.setParentMesh((MeshAttachment)parent);
+				linkedMesh.mesh.updateUVs();
 			}
 			linkedMeshes.clear();
 
 			// Events.
 			for (int i = 0, n = input.readInt(true); i < n; i++) {
-				EventData eventData = new EventData(input.readString());
-				eventData.intValue = input.readInt(false);
-				eventData.floatValue = input.readFloat();
-				eventData.stringValue = input.readString();
-				skeletonData.events.add(eventData);
+				EventData data = new EventData(input.readString());
+				data.intValue = input.readInt(false);
+				data.floatValue = input.readFloat();
+				data.stringValue = input.readString();
+				skeletonData.events.add(data);
 			}
 
 			// Animations.
@@ -328,21 +357,26 @@ public class SkeletonBinary {
 			return region;
 		}
 		case boundingbox: {
-			float[] vertices = readFloatArray(input, input.readInt(true) * 2, scale);
+			int vertexCount = input.readInt(true);
+			Vertices vertices = readVertices(input, vertexCount);
+			int color = nonessential ? input.readInt() : 0;
+
 			BoundingBoxAttachment box = attachmentLoader.newBoundingBoxAttachment(skin, name);
 			if (box == null) return null;
-			box.setVertices(vertices);
+			box.setWorldVerticesLength(vertexCount << 1);
+			box.setVertices(vertices.vertices);
+			box.setBones(vertices.bones);
+			if (nonessential) Color.rgba8888ToColor(box.getColor(), color);
 			return box;
 		}
 		case mesh: {
 			String path = input.readString();
 			int color = input.readInt();
-			int hullLength = 0;
-			int verticesLength = input.readInt(true) * 2;
-			float[] uvs = readFloatArray(input, verticesLength, 1);
+			int vertexCount = input.readInt(true);
+			float[] uvs = readFloatArray(input, vertexCount << 1, 1);
 			short[] triangles = readShortArray(input);
-			float[] vertices = readFloatArray(input, verticesLength, scale);
-			hullLength = input.readInt(true);
+			Vertices vertices = readVertices(input, vertexCount);
+			int hullLength = input.readInt(true);
 			short[] edges = null;
 			float width = 0, height = 0;
 			if (nonessential) {
@@ -356,11 +390,13 @@ public class SkeletonBinary {
 			if (mesh == null) return null;
 			mesh.setPath(path);
 			Color.rgba8888ToColor(mesh.getColor(), color);
-			mesh.setVertices(vertices);
+			mesh.setBones(vertices.bones);
+			mesh.setVertices(vertices.vertices);
+			mesh.setWorldVerticesLength(vertexCount << 1);
 			mesh.setTriangles(triangles);
 			mesh.setRegionUVs(uvs);
 			mesh.updateUVs();
-			mesh.setHullLength(hullLength * 2);
+			mesh.setHullLength(hullLength << 1);
 			if (nonessential) {
 				mesh.setEdges(edges);
 				mesh.setWidth(width * scale);
@@ -373,7 +409,7 @@ public class SkeletonBinary {
 			int color = input.readInt();
 			String skinName = input.readString();
 			String parent = input.readString();
-			boolean inheritFFD = input.readBoolean();
+			boolean inheritDeform = input.readBoolean();
 			float width = 0, height = 0;
 			if (nonessential) {
 				width = input.readFloat();
@@ -385,7 +421,7 @@ public class SkeletonBinary {
 			if (mesh == null) return null;
 			mesh.setPath(path);
 			Color.rgba8888ToColor(mesh.getColor(), color);
-			mesh.setInheritFFD(inheritFFD);
+			mesh.setInheritDeform(inheritDeform);
 			if (nonessential) {
 				mesh.setWidth(width * scale);
 				mesh.setHeight(height * scale);
@@ -393,78 +429,53 @@ public class SkeletonBinary {
 			linkedMeshes.add(new LinkedMesh(mesh, skinName, slotIndex, parent));
 			return mesh;
 		}
-		case weightedmesh: {
-			String path = input.readString();
-			int color = input.readInt();
+		case path: {
+			boolean closed = input.readBoolean();
+			boolean constantSpeed = input.readBoolean();
 			int vertexCount = input.readInt(true);
-			float[] uvs = readFloatArray(input, vertexCount * 2, 1);
-			short[] triangles = readShortArray(input);
-			FloatArray weights = new FloatArray(uvs.length * 3 * 3);
-			IntArray bones = new IntArray(uvs.length * 3);
-			for (int i = 0; i < vertexCount; i++) {
-				int boneCount = (int)input.readFloat();
-				bones.add(boneCount);
-				for (int ii = 0; ii < boneCount; ii++) {
-					bones.add((int)input.readFloat());
-					weights.add(input.readFloat() * scale);
-					weights.add(input.readFloat() * scale);
-					weights.add(input.readFloat());
-				}
-			}
-			int hullLength = input.readInt(true);
-			short[] edges = null;
-			float width = 0, height = 0;
-			if (nonessential) {
-				edges = readShortArray(input);
-				width = input.readFloat();
-				height = input.readFloat();
-			}
-
-			if (path == null) path = name;
-			WeightedMeshAttachment mesh = attachmentLoader.newWeightedMeshAttachment(skin, name, path);
-			if (mesh == null) return null;
-			mesh.setPath(path);
-			Color.rgba8888ToColor(mesh.getColor(), color);
-			mesh.setBones(bones.toArray());
-			mesh.setWeights(weights.toArray());
-			mesh.setTriangles(triangles);
-			mesh.setRegionUVs(uvs);
-			mesh.updateUVs();
-			mesh.setHullLength(hullLength * 2);
-			if (nonessential) {
-				mesh.setEdges(edges);
-				mesh.setWidth(width * scale);
-				mesh.setHeight(height * scale);
-			}
-			return mesh;
+			Vertices vertices = readVertices(input, vertexCount);
+			float[] lengths = new float[vertexCount / 3];
+			for (int i = 0, n = lengths.length; i < n; i++)
+				lengths[i] = input.readFloat() * scale;
+			int color = nonessential ? input.readInt() : 0;
+
+			PathAttachment path = attachmentLoader.newPathAttachment(skin, name);
+			if (path == null) return null;
+			path.setClosed(closed);
+			path.setConstantSpeed(constantSpeed);
+			path.setWorldVerticesLength(vertexCount << 1);
+			path.setVertices(vertices.vertices);
+			path.setBones(vertices.bones);
+			path.setLengths(lengths);
+			if (nonessential) Color.rgba8888ToColor(path.getColor(), color);
+			return path;
 		}
-		case weightedlinkedmesh: {
-			String path = input.readString();
-			int color = input.readInt();
-			String skinName = input.readString();
-			String parent = input.readString();
-			boolean inheritFFD = input.readBoolean();
-			float width = 0, height = 0;
-			if (nonessential) {
-				width = input.readFloat();
-				height = input.readFloat();
-			}
+		}
+		return null;
+	}
 
-			if (path == null) path = name;
-			WeightedMeshAttachment mesh = attachmentLoader.newWeightedMeshAttachment(skin, name, path);
-			if (mesh == null) return null;
-			mesh.setPath(path);
-			Color.rgba8888ToColor(mesh.getColor(), color);
-			mesh.setInheritFFD(inheritFFD);
-			if (nonessential) {
-				mesh.setWidth(width * scale);
-				mesh.setHeight(height * scale);
-			}
-			linkedMeshes.add(new LinkedMesh(mesh, skinName, slotIndex, parent));
-			return mesh;
+	private Vertices readVertices (DataInput input, int vertexCount) throws IOException {
+		int verticesLength = vertexCount << 1;
+		Vertices vertices = new Vertices();
+		if (!input.readBoolean()) {
+			vertices.vertices = readFloatArray(input, verticesLength, scale);
+			return vertices;
 		}
+		FloatArray weights = new FloatArray(verticesLength * 3 * 3);
+		IntArray bonesArray = new IntArray(verticesLength * 3);
+		for (int i = 0; i < vertexCount; i++) {
+			int boneCount = input.readInt(true);
+			bonesArray.add(boneCount);
+			for (int ii = 0; ii < boneCount; ii++) {
+				bonesArray.add(input.readInt(true));
+				weights.add(input.readFloat() * scale);
+				weights.add(input.readFloat() * scale);
+				weights.add(input.readFloat());
+			}
 		}
-		return null;
+		vertices.vertices = weights.toArray();
+		vertices.bones = bonesArray.toArray();
+		return vertices;
 	}
 
 	private float[] readFloatArray (DataInput input, int n, float scale) throws IOException {
@@ -500,7 +511,7 @@ public class SkeletonBinary {
 					int timelineType = input.readByte();
 					int frameCount = input.readInt(true);
 					switch (timelineType) {
-					case TIMELINE_COLOR: {
+					case SLOT_COLOR: {
 						ColorTimeline timeline = new ColorTimeline(frameCount);
 						timeline.slotIndex = slotIndex;
 						for (int frameIndex = 0; frameIndex < frameCount; frameIndex++) {
@@ -510,10 +521,10 @@ public class SkeletonBinary {
 							if (frameIndex < frameCount - 1) readCurve(input, frameIndex, timeline);
 						}
 						timelines.add(timeline);
-						duration = Math.max(duration, timeline.getFrames()[frameCount * 5 - 5]);
+						duration = Math.max(duration, timeline.getFrames()[(frameCount - 1) * ColorTimeline.ENTRIES]);
 						break;
 					}
-					case TIMELINE_ATTACHMENT:
+					case SLOT_ATTACHMENT:
 						AttachmentTimeline timeline = new AttachmentTimeline(frameCount);
 						timeline.slotIndex = slotIndex;
 						for (int frameIndex = 0; frameIndex < frameCount; frameIndex++)
@@ -532,7 +543,7 @@ public class SkeletonBinary {
 					int timelineType = input.readByte();
 					int frameCount = input.readInt(true);
 					switch (timelineType) {
-					case TIMELINE_ROTATE: {
+					case BONE_ROTATE: {
 						RotateTimeline timeline = new RotateTimeline(frameCount);
 						timeline.boneIndex = boneIndex;
 						for (int frameIndex = 0; frameIndex < frameCount; frameIndex++) {
@@ -540,17 +551,17 @@ public class SkeletonBinary {
 							if (frameIndex < frameCount - 1) readCurve(input, frameIndex, timeline);
 						}
 						timelines.add(timeline);
-						duration = Math.max(duration, timeline.getFrames()[frameCount * 2 - 2]);
+						duration = Math.max(duration, timeline.getFrames()[(frameCount - 1) * RotateTimeline.ENTRIES]);
 						break;
 					}
-					case TIMELINE_TRANSLATE:
-					case TIMELINE_SCALE:
-					case TIMELINE_SHEAR: {
+					case BONE_TRANSLATE:
+					case BONE_SCALE:
+					case BONE_SHEAR: {
 						TranslateTimeline timeline;
 						float timelineScale = 1;
-						if (timelineType == TIMELINE_SCALE)
+						if (timelineType == BONE_SCALE)
 							timeline = new ScaleTimeline(frameCount);
-						else if (timelineType == TIMELINE_SHEAR)
+						else if (timelineType == BONE_SHEAR)
 							timeline = new ShearTimeline(frameCount);
 						else {
 							timeline = new TranslateTimeline(frameCount);
@@ -563,7 +574,7 @@ public class SkeletonBinary {
 							if (frameIndex < frameCount - 1) readCurve(input, frameIndex, timeline);
 						}
 						timelines.add(timeline);
-						duration = Math.max(duration, timeline.getFrames()[frameCount * 3 - 3]);
+						duration = Math.max(duration, timeline.getFrames()[(frameCount - 1) * TranslateTimeline.ENTRIES]);
 						break;
 					}
 					}
@@ -572,79 +583,116 @@ public class SkeletonBinary {
 
 			// IK constraint timelines.
 			for (int i = 0, n = input.readInt(true); i < n; i++) {
-				IkConstraintData constraint = skeletonData.ikConstraints.get(input.readInt(true));
+				int index = input.readInt(true);
 				int frameCount = input.readInt(true);
 				IkConstraintTimeline timeline = new IkConstraintTimeline(frameCount);
-				timeline.ikConstraintIndex = skeletonData.getIkConstraints().indexOf(constraint, true);
+				timeline.ikConstraintIndex = index;
 				for (int frameIndex = 0; frameIndex < frameCount; frameIndex++) {
 					timeline.setFrame(frameIndex, input.readFloat(), input.readFloat(), input.readByte());
 					if (frameIndex < frameCount - 1) readCurve(input, frameIndex, timeline);
 				}
 				timelines.add(timeline);
-				duration = Math.max(duration, timeline.getFrames()[frameCount * 3 - 3]);
+				duration = Math.max(duration, timeline.getFrames()[(frameCount - 1) * IkConstraintTimeline.ENTRIES]);
 			}
 
 			// Transform constraint timelines.
 			for (int i = 0, n = input.readInt(true); i < n; i++) {
-				TransformConstraintData constraint = skeletonData.transformConstraints.get(input.readInt(true));
+				int index = input.readInt(true);
 				int frameCount = input.readInt(true);
 				TransformConstraintTimeline timeline = new TransformConstraintTimeline(frameCount);
-				timeline.transformConstraintIndex = skeletonData.getTransformConstraints().indexOf(constraint, true);
+				timeline.transformConstraintIndex = index;
 				for (int frameIndex = 0; frameIndex < frameCount; frameIndex++) {
 					timeline.setFrame(frameIndex, input.readFloat(), input.readFloat(), input.readFloat(), input.readFloat(),
 						input.readFloat());
 					if (frameIndex < frameCount - 1) readCurve(input, frameIndex, timeline);
 				}
 				timelines.add(timeline);
-				duration = Math.max(duration, timeline.getFrames()[frameCount * 5 - 5]);
+				duration = Math.max(duration, timeline.getFrames()[(frameCount - 1) * TransformConstraintTimeline.ENTRIES]);
 			}
 
-			// FFD timelines.
+			// Path constraint timelines.
+			for (int i = 0, n = input.readInt(true); i < n; i++) {
+				int index = input.readInt(true);
+				PathConstraintData data = skeletonData.getPathConstraints().get(index);
+				for (int ii = 0, nn = input.readInt(true); ii < nn; ii++) {
+					int timelineType = input.readByte();
+					int frameCount = input.readInt(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, input.readFloat(), input.readFloat() * timelineScale);
+							if (frameIndex < frameCount - 1) readCurve(input, frameIndex, timeline);
+						}
+						timelines.add(timeline);
+						duration = Math.max(duration, timeline.getFrames()[(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, input.readFloat(), input.readFloat(), input.readFloat());
+							if (frameIndex < frameCount - 1) readCurve(input, frameIndex, timeline);
+						}
+						timelines.add(timeline);
+						duration = Math.max(duration, timeline.getFrames()[(frameCount - 1) * PathConstraintMixTimeline.ENTRIES]);
+						break;
+					}
+					}
+				}
+			}
+
+			// Deform timelines.
 			for (int i = 0, n = input.readInt(true); i < n; i++) {
 				Skin skin = skeletonData.skins.get(input.readInt(true));
 				for (int ii = 0, nn = input.readInt(true); ii < nn; ii++) {
 					int slotIndex = input.readInt(true);
 					for (int iii = 0, nnn = input.readInt(true); iii < nnn; iii++) {
-						Attachment attachment = skin.getAttachment(slotIndex, input.readString());
+						VertexAttachment attachment = (VertexAttachment)skin.getAttachment(slotIndex, input.readString());
+						boolean weighted = attachment.getBones() != null;
+						float[] vertices = attachment.getVertices();
+						int deformLength = weighted ? vertices.length / 3 * 2 : vertices.length;
+
 						int frameCount = input.readInt(true);
-						FfdTimeline timeline = new FfdTimeline(frameCount);
+						DeformTimeline timeline = new DeformTimeline(frameCount);
 						timeline.slotIndex = slotIndex;
 						timeline.attachment = attachment;
+
 						for (int frameIndex = 0; frameIndex < frameCount; frameIndex++) {
 							float time = input.readFloat();
-
-							float[] vertices;
-							int vertexCount;
-							if (attachment instanceof MeshAttachment)
-								vertexCount = ((MeshAttachment)attachment).getVertices().length;
-							else
-								vertexCount = ((WeightedMeshAttachment)attachment).getWeights().length / 3 * 2;
-
+							float[] deform;
 							int end = input.readInt(true);
-							if (end == 0) {
-								if (attachment instanceof MeshAttachment)
-									vertices = ((MeshAttachment)attachment).getVertices();
-								else
-									vertices = new float[vertexCount];
-							} else {
-								vertices = new float[vertexCount];
+							if (end == 0)
+								deform = weighted ? new float[deformLength] : vertices;
+							else {
+								deform = new float[deformLength];
 								int start = input.readInt(true);
 								end += start;
 								if (scale == 1) {
 									for (int v = start; v < end; v++)
-										vertices[v] = input.readFloat();
+										deform[v] = input.readFloat();
 								} else {
 									for (int v = start; v < end; v++)
-										vertices[v] = input.readFloat() * scale;
+										deform[v] = input.readFloat() * scale;
 								}
-								if (attachment instanceof MeshAttachment) {
-									float[] meshVertices = ((MeshAttachment)attachment).getVertices();
-									for (int v = 0, vn = vertices.length; v < vn; v++)
-										vertices[v] += meshVertices[v];
+								if (!weighted) {
+									for (int v = 0, vn = deform.length; v < vn; v++)
+										deform[v] += vertices[v];
 								}
 							}
 
-							timeline.setFrame(frameIndex, time, vertices);
+							timeline.setFrame(frameIndex, time, deform);
 							if (frameIndex < frameCount - 1) readCurve(input, frameIndex, timeline);
 						}
 						timelines.add(timeline);
@@ -708,6 +756,7 @@ public class SkeletonBinary {
 
 		timelines.shrink();
 		skeletonData.animations.add(new Animation(name, timelines, duration));
+
 	}
 
 	private void readCurve (DataInput input, int frameIndex, CurveTimeline timeline) throws IOException {
@@ -724,4 +773,9 @@ public class SkeletonBinary {
 	void setCurve (CurveTimeline timeline, int frameIndex, float cx1, float cy1, float cx2, float cy2) {
 		timeline.setCurve(frameIndex, cx1, cy1, cx2, cy2);
 	}
+
+	static class Vertices {
+		int[] bones;
+		float[] vertices;
+	}
 }

+ 3 - 5
spine-libgdx/spine-libgdx/src/com/esotericsoftware/spine/SkeletonBounds.java

@@ -49,6 +49,7 @@ public class SkeletonBounds {
 	};
 
 	public void update (Skeleton skeleton, boolean updateAabb) {
+		if (skeleton == null) throw new IllegalArgumentException("skeleton cannot be null.");
 		Array<BoundingBoxAttachment> boundingBoxes = this.boundingBoxes;
 		Array<FloatArray> polygons = this.polygons;
 		Array<Slot> slots = skeleton.slots;
@@ -67,11 +68,7 @@ public class SkeletonBounds {
 
 				FloatArray polygon = polygonPool.obtain();
 				polygons.add(polygon);
-				int vertexCount = boundingBox.getVertices().length;
-				polygon.ensureCapacity(vertexCount);
-				polygon.size = vertexCount;
-
-				boundingBox.computeWorldVertices(slot.bone, polygon.items);
+				boundingBox.computeWorldVertices(slot, polygon.setSize(boundingBox.getWorldVerticesLength()));
 			}
 		}
 
@@ -225,6 +222,7 @@ public class SkeletonBounds {
 
 	/** Returns the polygon for the specified bounding box, or null. */
 	public FloatArray getPolygon (BoundingBoxAttachment boundingBox) {
+		if (boundingBox == null) throw new IllegalArgumentException("boundingBox cannot be null.");
 		int index = boundingBoxes.indexOf(boundingBox, true);
 		return index == -1 ? null : polygons.get(index);
 	}

+ 28 - 1
spine-libgdx/spine-libgdx/src/com/esotericsoftware/spine/SkeletonData.java

@@ -43,6 +43,7 @@ public class SkeletonData {
 	final Array<Animation> animations = new Array();
 	final Array<IkConstraintData> ikConstraints = new Array();
 	final Array<TransformConstraintData> transformConstraints = new Array();
+	final Array<PathConstraintData> pathConstraints = new Array();
 	float width, height;
 	String version, hash, imagesPath;
 
@@ -89,7 +90,7 @@ public class SkeletonData {
 		return null;
 	}
 
-	/** @return -1 if the bone was not found. */
+	/** @return -1 if the slot was not found. */
 	public int findSlotIndex (String slotName) {
 		if (slotName == null) throw new IllegalArgumentException("slotName cannot be null.");
 		Array<SlotData> slots = this.slots;
@@ -188,6 +189,32 @@ public class SkeletonData {
 		return null;
 	}
 
+	// --- Path constraints
+
+	public Array<PathConstraintData> getPathConstraints () {
+		return pathConstraints;
+	}
+
+	/** @return May be null. */
+	public PathConstraintData findPathConstraint (String constraintName) {
+		if (constraintName == null) throw new IllegalArgumentException("constraintName cannot be null.");
+		Array<PathConstraintData> pathConstraints = this.pathConstraints;
+		for (int i = 0, n = pathConstraints.size; i < n; i++) {
+			PathConstraintData constraint = pathConstraints.get(i);
+			if (constraint.name.equals(constraintName)) return constraint;
+		}
+		return null;
+	}
+
+	/** @return -1 if the path constraint was not found. */
+	public int findPathConstraintIndex (String pathConstraintName) {
+		if (pathConstraintName == null) throw new IllegalArgumentException("pathConstraintName cannot be null.");
+		Array<PathConstraintData> pathConstraints = this.pathConstraints;
+		for (int i = 0, n = pathConstraints.size; i < n; i++)
+			if (pathConstraints.get(i).name.equals(pathConstraintName)) return i;
+		return -1;
+	}
+
 	// ---
 
 	/** @return May be null. */

+ 263 - 197
spine-libgdx/spine-libgdx/src/com/esotericsoftware/spine/SkeletonJson.java

@@ -43,24 +43,31 @@ import com.badlogic.gdx.utils.SerializationException;
 import com.esotericsoftware.spine.Animation.AttachmentTimeline;
 import com.esotericsoftware.spine.Animation.ColorTimeline;
 import com.esotericsoftware.spine.Animation.CurveTimeline;
+import com.esotericsoftware.spine.Animation.DeformTimeline;
 import com.esotericsoftware.spine.Animation.DrawOrderTimeline;
 import com.esotericsoftware.spine.Animation.EventTimeline;
-import com.esotericsoftware.spine.Animation.FfdTimeline;
 import com.esotericsoftware.spine.Animation.IkConstraintTimeline;
+import com.esotericsoftware.spine.Animation.PathConstraintMixTimeline;
+import com.esotericsoftware.spine.Animation.PathConstraintPositionTimeline;
+import com.esotericsoftware.spine.Animation.PathConstraintSpacingTimeline;
 import com.esotericsoftware.spine.Animation.RotateTimeline;
 import com.esotericsoftware.spine.Animation.ScaleTimeline;
 import com.esotericsoftware.spine.Animation.ShearTimeline;
 import com.esotericsoftware.spine.Animation.Timeline;
 import com.esotericsoftware.spine.Animation.TransformConstraintTimeline;
 import com.esotericsoftware.spine.Animation.TranslateTimeline;
+import com.esotericsoftware.spine.PathConstraintData.PositionMode;
+import com.esotericsoftware.spine.PathConstraintData.RotateMode;
+import com.esotericsoftware.spine.PathConstraintData.SpacingMode;
 import com.esotericsoftware.spine.attachments.AtlasAttachmentLoader;
 import com.esotericsoftware.spine.attachments.Attachment;
 import com.esotericsoftware.spine.attachments.AttachmentLoader;
 import com.esotericsoftware.spine.attachments.AttachmentType;
 import com.esotericsoftware.spine.attachments.BoundingBoxAttachment;
 import com.esotericsoftware.spine.attachments.MeshAttachment;
+import com.esotericsoftware.spine.attachments.PathAttachment;
 import com.esotericsoftware.spine.attachments.RegionAttachment;
-import com.esotericsoftware.spine.attachments.WeightedMeshAttachment;
+import com.esotericsoftware.spine.attachments.VertexAttachment;
 
 public class SkeletonJson {
 	private final AttachmentLoader attachmentLoader;
@@ -72,6 +79,7 @@ public class SkeletonJson {
 	}
 
 	public SkeletonJson (AttachmentLoader attachmentLoader) {
+		if (attachmentLoader == null) throw new IllegalArgumentException("attachmentLoader cannot be null.");
 		this.attachmentLoader = attachmentLoader;
 	}
 
@@ -112,86 +120,118 @@ public class SkeletonJson {
 				parent = skeletonData.findBone(parentName);
 				if (parent == null) throw new SerializationException("Parent bone not found: " + parentName);
 			}
-			BoneData boneData = new BoneData(boneMap.getString("name"), parent);
-			boneData.length = boneMap.getFloat("length", 0) * scale;
-			boneData.x = boneMap.getFloat("x", 0) * scale;
-			boneData.y = boneMap.getFloat("y", 0) * scale;
-			boneData.rotation = boneMap.getFloat("rotation", 0);
-			boneData.scaleX = boneMap.getFloat("scaleX", 1);
-			boneData.scaleY = boneMap.getFloat("scaleY", 1);
-			boneData.shearX = boneMap.getFloat("shearX", 0);
-			boneData.shearY = boneMap.getFloat("shearY", 0);
-			boneData.inheritScale = boneMap.getBoolean("inheritScale", true);
-			boneData.inheritRotation = boneMap.getBoolean("inheritRotation", true);
+			BoneData data = new BoneData(skeletonData.bones.size, boneMap.getString("name"), parent);
+			data.length = boneMap.getFloat("length", 0) * scale;
+			data.x = boneMap.getFloat("x", 0) * scale;
+			data.y = boneMap.getFloat("y", 0) * scale;
+			data.rotation = boneMap.getFloat("rotation", 0);
+			data.scaleX = boneMap.getFloat("scaleX", 1);
+			data.scaleY = boneMap.getFloat("scaleY", 1);
+			data.shearX = boneMap.getFloat("shearX", 0);
+			data.shearY = boneMap.getFloat("shearY", 0);
+			data.inheritRotation = boneMap.getBoolean("inheritRotation", true);
+			data.inheritScale = boneMap.getBoolean("inheritScale", true);
 
 			String color = boneMap.getString("color", null);
-			if (color != null) boneData.getColor().set(Color.valueOf(color));
+			if (color != null) data.getColor().set(Color.valueOf(color));
 
-			skeletonData.bones.add(boneData);
+			skeletonData.bones.add(data);
+		}
+
+		// Slots.
+		for (JsonValue slotMap = root.getChild("slots"); slotMap != null; slotMap = slotMap.next) {
+			String slotName = slotMap.getString("name");
+			String boneName = slotMap.getString("bone");
+			BoneData boneData = skeletonData.findBone(boneName);
+			if (boneData == null) throw new SerializationException("Slot bone not found: " + boneName);
+			SlotData data = new SlotData(skeletonData.slots.size, slotName, boneData);
+
+			String color = slotMap.getString("color", null);
+			if (color != null) data.getColor().set(Color.valueOf(color));
+
+			data.attachmentName = slotMap.getString("attachment", null);
+			data.blendMode = BlendMode.valueOf(slotMap.getString("blend", BlendMode.normal.name()));
+			skeletonData.slots.add(data);
 		}
 
 		// IK constraints.
-		for (JsonValue ikMap = root.getChild("ik"); ikMap != null; ikMap = ikMap.next) {
-			IkConstraintData ikConstraintData = new IkConstraintData(ikMap.getString("name"));
+		for (JsonValue constraintMap = root.getChild("ik"); constraintMap != null; constraintMap = constraintMap.next) {
+			IkConstraintData data = new IkConstraintData(constraintMap.getString("name"));
 
-			for (JsonValue boneMap = ikMap.getChild("bones"); boneMap != null; boneMap = boneMap.next) {
+			for (JsonValue boneMap = constraintMap.getChild("bones"); boneMap != null; boneMap = boneMap.next) {
 				String boneName = boneMap.asString();
 				BoneData bone = skeletonData.findBone(boneName);
 				if (bone == null) throw new SerializationException("IK bone not found: " + boneName);
-				ikConstraintData.bones.add(bone);
+				data.bones.add(bone);
 			}
 
-			String targetName = ikMap.getString("target");
-			ikConstraintData.target = skeletonData.findBone(targetName);
-			if (ikConstraintData.target == null) throw new SerializationException("Target bone not found: " + targetName);
+			String targetName = constraintMap.getString("target");
+			data.target = skeletonData.findBone(targetName);
+			if (data.target == null) throw new SerializationException("Target bone not found: " + targetName);
 
-			ikConstraintData.bendDirection = ikMap.getBoolean("bendPositive", true) ? 1 : -1;
-			ikConstraintData.mix = ikMap.getFloat("mix", 1);
+			data.bendDirection = constraintMap.getBoolean("bendPositive", true) ? 1 : -1;
+			data.mix = constraintMap.getFloat("mix", 1);
 
-			skeletonData.ikConstraints.add(ikConstraintData);
+			skeletonData.ikConstraints.add(data);
 		}
 
 		// Transform constraints.
-		for (JsonValue transformMap = root.getChild("transform"); transformMap != null; transformMap = transformMap.next) {
-			TransformConstraintData transformConstraintData = new TransformConstraintData(transformMap.getString("name"));
-
-			String boneName = transformMap.getString("bone");
-			transformConstraintData.bone = skeletonData.findBone(boneName);
-			if (transformConstraintData.bone == null) throw new SerializationException("Bone not found: " + boneName);
-
-			String targetName = transformMap.getString("target");
-			transformConstraintData.target = skeletonData.findBone(targetName);
-			if (transformConstraintData.target == null) throw new SerializationException("Target bone not found: " + targetName);
-
-			transformConstraintData.offsetRotation = transformMap.getFloat("rotation", 0);
-			transformConstraintData.offsetX = transformMap.getFloat("x", 0) * scale;
-			transformConstraintData.offsetY = transformMap.getFloat("y", 0) * scale;
-			transformConstraintData.offsetScaleX = transformMap.getFloat("scaleX", 0) * scale;
-			transformConstraintData.offsetScaleY = transformMap.getFloat("scaleY", 0) * scale;
-			transformConstraintData.offsetShearY = transformMap.getFloat("shearY", 0) * scale;
-
-			transformConstraintData.rotateMix = transformMap.getFloat("rotateMix", 1);
-			transformConstraintData.translateMix = transformMap.getFloat("translateMix", 1);
-			transformConstraintData.scaleMix = transformMap.getFloat("scaleMix", 1);
-			transformConstraintData.shearMix = transformMap.getFloat("shearMix", 1);
-
-			skeletonData.transformConstraints.add(transformConstraintData);
+		for (JsonValue constraintMap = root.getChild("transform"); constraintMap != null; constraintMap = constraintMap.next) {
+			TransformConstraintData data = new TransformConstraintData(constraintMap.getString("name"));
+
+			for (JsonValue boneMap = constraintMap.getChild("bones"); boneMap != null; boneMap = boneMap.next) {
+				String boneName = boneMap.asString();
+				BoneData bone = skeletonData.findBone(boneName);
+				if (bone == null) throw new SerializationException("Path bone not found: " + boneName);
+				data.bones.add(bone);
+			}
+
+			String targetName = constraintMap.getString("target");
+			data.target = skeletonData.findBone(targetName);
+			if (data.target == null) throw new SerializationException("Target bone not found: " + targetName);
+
+			data.offsetRotation = constraintMap.getFloat("rotation", 0);
+			data.offsetX = constraintMap.getFloat("x", 0) * scale;
+			data.offsetY = constraintMap.getFloat("y", 0) * scale;
+			data.offsetScaleX = constraintMap.getFloat("scaleX", 0) * scale;
+			data.offsetScaleY = constraintMap.getFloat("scaleY", 0) * scale;
+			data.offsetShearY = constraintMap.getFloat("shearY", 0) * scale;
+
+			data.rotateMix = constraintMap.getFloat("rotateMix", 1);
+			data.translateMix = constraintMap.getFloat("translateMix", 1);
+			data.scaleMix = constraintMap.getFloat("scaleMix", 1);
+			data.shearMix = constraintMap.getFloat("shearMix", 1);
+
+			skeletonData.transformConstraints.add(data);
 		}
 
-		// Slots.
-		for (JsonValue slotMap = root.getChild("slots"); slotMap != null; slotMap = slotMap.next) {
-			String slotName = slotMap.getString("name");
-			String boneName = slotMap.getString("bone");
-			BoneData boneData = skeletonData.findBone(boneName);
-			if (boneData == null) throw new SerializationException("Slot bone not found: " + boneName);
-			SlotData slotData = new SlotData(slotName, boneData);
+		// Path constraints.
+		for (JsonValue constraintMap = root.getChild("path"); constraintMap != null; constraintMap = constraintMap.next) {
+			PathConstraintData data = new PathConstraintData(constraintMap.getString("name"));
 
-			String color = slotMap.getString("color", null);
-			if (color != null) slotData.getColor().set(Color.valueOf(color));
+			for (JsonValue boneMap = constraintMap.getChild("bones"); boneMap != null; boneMap = boneMap.next) {
+				String boneName = boneMap.asString();
+				BoneData bone = skeletonData.findBone(boneName);
+				if (bone == null) throw new SerializationException("Path bone not found: " + boneName);
+				data.bones.add(bone);
+			}
 
-			slotData.attachmentName = slotMap.getString("attachment", null);
-			slotData.blendMode = BlendMode.valueOf(slotMap.getString("blend", BlendMode.normal.name()));
-			skeletonData.slots.add(slotData);
+			String targetName = constraintMap.getString("target");
+			data.target = skeletonData.findSlot(targetName);
+			if (data.target == null) throw new SerializationException("Target slot not found: " + targetName);
+
+			data.positionMode = PositionMode.valueOf(constraintMap.getString("positionMode", "percent"));
+			data.spacingMode = SpacingMode.valueOf(constraintMap.getString("spacingMode", "length"));
+			data.rotateMode = RotateMode.valueOf(constraintMap.getString("rotateMode", "tangent"));
+			data.offsetRotation = constraintMap.getFloat("rotation", 0);
+			data.position = constraintMap.getFloat("position", 0);
+			if (data.positionMode == PositionMode.fixed) data.position *= scale;
+			data.spacing = constraintMap.getFloat("spacing", 0);
+			if (data.spacingMode == SpacingMode.length || data.spacingMode == SpacingMode.fixed) data.spacing *= scale;
+			data.rotateMix = constraintMap.getFloat("rotateMix", 1);
+			data.translateMix = constraintMap.getFloat("translateMix", 1);
+
+			skeletonData.pathConstraints.add(data);
 		}
 
 		// Skins.
@@ -201,7 +241,7 @@ public class SkeletonJson {
 				int slotIndex = skeletonData.findSlotIndex(slotEntry.name);
 				if (slotIndex == -1) throw new SerializationException("Slot not found: " + slotEntry.name);
 				for (JsonValue entry = slotEntry.child; entry != null; entry = entry.next) {
-					Attachment attachment = readAttachment(skin, slotIndex, entry.name, entry);
+					Attachment attachment = readAttachment(entry, skin, slotIndex, entry.name);
 					if (attachment != null) skin.addAttachment(slotIndex, entry.name, attachment);
 				}
 			}
@@ -216,30 +256,23 @@ public class SkeletonJson {
 			if (skin == null) throw new SerializationException("Skin not found: " + linkedMesh.skin);
 			Attachment parent = skin.getAttachment(linkedMesh.slotIndex, linkedMesh.parent);
 			if (parent == null) throw new SerializationException("Parent mesh not found: " + linkedMesh.parent);
-			if (linkedMesh.mesh instanceof MeshAttachment) {
-				MeshAttachment mesh = (MeshAttachment)linkedMesh.mesh;
-				mesh.setParentMesh((MeshAttachment)parent);
-				mesh.updateUVs();
-			} else {
-				WeightedMeshAttachment mesh = (WeightedMeshAttachment)linkedMesh.mesh;
-				mesh.setParentMesh((WeightedMeshAttachment)parent);
-				mesh.updateUVs();
-			}
+			linkedMesh.mesh.setParentMesh((MeshAttachment)parent);
+			linkedMesh.mesh.updateUVs();
 		}
 		linkedMeshes.clear();
 
 		// Events.
 		for (JsonValue eventMap = root.getChild("events"); eventMap != null; eventMap = eventMap.next) {
-			EventData eventData = new EventData(eventMap.name);
-			eventData.intValue = eventMap.getInt("int", 0);
-			eventData.floatValue = eventMap.getFloat("float", 0f);
-			eventData.stringValue = eventMap.getString("string", null);
-			skeletonData.events.add(eventData);
+			EventData data = new EventData(eventMap.name);
+			data.intValue = eventMap.getInt("int", 0);
+			data.floatValue = eventMap.getFloat("float", 0f);
+			data.stringValue = eventMap.getString("string", null);
+			skeletonData.events.add(data);
 		}
 
 		// Animations.
 		for (JsonValue animationMap = root.getChild("animations"); animationMap != null; animationMap = animationMap.next)
-			readAnimation(animationMap.name, animationMap, skeletonData);
+			readAnimation(animationMap, animationMap.name, skeletonData);
 
 		skeletonData.bones.shrink();
 		skeletonData.slots.shrink();
@@ -250,15 +283,20 @@ public class SkeletonJson {
 		return skeletonData;
 	}
 
-	private Attachment readAttachment (Skin skin, int slotIndex, String name, JsonValue map) {
+	private Attachment readAttachment (JsonValue map, Skin skin, int slotIndex, String name) {
 		float scale = this.scale;
 		name = map.getString("name", name);
-		String path = map.getString("path", name);
 
 		String type = map.getString("type", AttachmentType.region.name());
+
+		// BOZO - Warning: These types are deprecated and will be removed in the near future.
 		if (type.equals("skinnedmesh")) type = "weightedmesh";
+		if (type.equals("weightedmesh")) type = "mesh";
+		if (type.equals("weightedlinkedmesh")) type = "linkedmesh";
+
 		switch (AttachmentType.valueOf(type)) {
 		case region: {
+			String path = map.getString("path", name);
 			RegionAttachment region = attachmentLoader.newRegionAttachment(skin, name, path);
 			if (region == null) return null;
 			region.setPath(path);
@@ -279,16 +317,15 @@ public class SkeletonJson {
 		case boundingbox: {
 			BoundingBoxAttachment box = attachmentLoader.newBoundingBoxAttachment(skin, name);
 			if (box == null) return null;
-			float[] vertices = map.require("vertices").asFloatArray();
-			if (scale != 1) {
-				for (int i = 0, n = vertices.length; i < n; i++)
-					vertices[i] *= scale;
-			}
-			box.setVertices(vertices);
+			readVertices(map, box, map.getInt("vertexCount") << 1);
+
+			String color = map.getString("color", null);
+			if (color != null) box.getColor().set(Color.valueOf(color));
 			return box;
 		}
 		case mesh:
 		case linkedmesh: {
+			String path = map.getString("path", name);
 			MeshAttachment mesh = attachmentLoader.newMeshAttachment(skin, name, path);
 			if (mesh == null) return null;
 			mesh.setPath(path);
@@ -300,81 +337,73 @@ public class SkeletonJson {
 			mesh.setHeight(map.getFloat("height", 0) * scale);
 
 			String parent = map.getString("parent", null);
-			if (parent == null) {
-				float[] vertices = map.require("vertices").asFloatArray();
-				if (scale != 1) {
-					for (int i = 0, n = vertices.length; i < n; i++)
-						vertices[i] *= scale;
-				}
-				mesh.setVertices(vertices);
-				mesh.setTriangles(map.require("triangles").asShortArray());
-				mesh.setRegionUVs(map.require("uvs").asFloatArray());
-				mesh.updateUVs();
-
-				if (map.has("hull")) mesh.setHullLength(map.require("hull").asInt() * 2);
-				if (map.has("edges")) mesh.setEdges(map.require("edges").asShortArray());
-			} else {
-				mesh.setInheritFFD(map.getBoolean("ffd", true));
+			if (parent != null) {
+				mesh.setInheritDeform(map.getBoolean("deform", true));
 				linkedMeshes.add(new LinkedMesh(mesh, map.getString("skin", null), slotIndex, parent));
+				return mesh;
 			}
+
+			float[] uvs = map.require("uvs").asFloatArray();
+			readVertices(map, mesh, uvs.length);
+			mesh.setTriangles(map.require("triangles").asShortArray());
+			mesh.setRegionUVs(uvs);
+			mesh.updateUVs();
+
+			if (map.has("hull")) mesh.setHullLength(map.require("hull").asInt() * 2);
+			if (map.has("edges")) mesh.setEdges(map.require("edges").asShortArray());
 			return mesh;
 		}
-		case weightedmesh:
-		case weightedlinkedmesh: {
-			WeightedMeshAttachment mesh = attachmentLoader.newWeightedMeshAttachment(skin, name, path);
-			if (mesh == null) return null;
-			mesh.setPath(path);
+		case path: {
+			PathAttachment path = attachmentLoader.newPathAttachment(skin, name);
+			if (path == null) return null;
+			path.setClosed(map.getBoolean("closed", false));
+			path.setConstantSpeed(map.getBoolean("constantSpeed", true));
 
-			String color = map.getString("color", null);
-			if (color != null) mesh.getColor().set(Color.valueOf(color));
+			int vertexCount = map.getInt("vertexCount");
+			readVertices(map, path, vertexCount << 1);
 
-			mesh.setWidth(map.getFloat("width", 0) * scale);
-			mesh.setHeight(map.getFloat("height", 0) * scale);
+			float[] lengths = new float[vertexCount / 3];
+			int i = 0;
+			for (JsonValue curves = map.require("lengths").child; curves != null; curves = curves.next)
+				lengths[i++] = curves.asFloat() * scale;
+			path.setLengths(lengths);
 
-			String parent = map.getString("parent", null);
-			if (parent == null) {
-				float[] uvs = map.require("uvs").asFloatArray();
-				float[] vertices = map.require("vertices").asFloatArray();
-				FloatArray weights = new FloatArray(uvs.length * 3 * 3);
-				IntArray bones = new IntArray(uvs.length * 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] * scale);
-						weights.add(vertices[i + 2] * scale);
-						weights.add(vertices[i + 3]);
-					}
-				}
-				mesh.setBones(bones.toArray());
-				mesh.setWeights(weights.toArray());
-				mesh.setTriangles(map.require("triangles").asShortArray());
-				mesh.setRegionUVs(uvs);
-				mesh.updateUVs();
-
-				if (map.has("hull")) mesh.setHullLength(map.require("hull").asInt() * 2);
-				if (map.has("edges")) mesh.setEdges(map.require("edges").asShortArray());
-			} else {
-				mesh.setInheritFFD(map.getBoolean("ffd", true));
-				linkedMeshes.add(new LinkedMesh(mesh, map.getString("skin", null), slotIndex, parent));
-			}
-			return mesh;
+			String color = map.getString("color", null);
+			if (color != null) path.getColor().set(Color.valueOf(color));
+			return path;
 		}
 		}
-
-		// RegionSequenceAttachment regionSequenceAttachment = (RegionSequenceAttachment)attachment;
-		//
-		// float fps = map.getFloat("fps");
-		// regionSequenceAttachment.setFrameTime(fps);
-		//
-		// String modeString = map.getString("mode");
-		// regionSequenceAttachment.setMode(modeString == null ? Mode.forward : Mode.valueOf(modeString));
-
 		return null;
 	}
 
-	private void readAnimation (String name, JsonValue map, SkeletonData skeletonData) {
+	private void readVertices (JsonValue map, VertexAttachment attachment, int verticesLength) {
+		attachment.setWorldVerticesLength(verticesLength);
+		float[] vertices = map.require("vertices").asFloatArray();
+		if (verticesLength == vertices.length) {
+			if (scale != 1) {
+				for (int i = 0, n = vertices.length; i < n; i++)
+					vertices[i] *= scale;
+			}
+			attachment.setVertices(vertices);
+			return;
+		}
+		FloatArray weights = new FloatArray(verticesLength * 3 * 3);
+		IntArray bones = new IntArray(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] * scale);
+				weights.add(vertices[i + 2] * scale);
+				weights.add(vertices[i + 3]);
+			}
+		}
+		attachment.setBones(bones.toArray());
+		attachment.setVertices(weights.toArray());
+	}
+
+	private void readAnimation (JsonValue map, String name, SkeletonData skeletonData) {
 		float scale = this.scale;
 		Array<Timeline> timelines = new Array();
 		float duration = 0;
@@ -383,7 +412,6 @@ public class SkeletonJson {
 		for (JsonValue slotMap = map.getChild("slots"); slotMap != null; slotMap = slotMap.next) {
 			int slotIndex = skeletonData.findSlotIndex(slotMap.name);
 			if (slotIndex == -1) throw new SerializationException("Slot not found: " + slotMap.name);
-
 			for (JsonValue timelineMap = slotMap.child; timelineMap != null; timelineMap = timelineMap.next) {
 				String timelineName = timelineMap.name;
 				if (timelineName.equals("color")) {
@@ -394,11 +422,11 @@ public class SkeletonJson {
 					for (JsonValue valueMap = timelineMap.child; valueMap != null; valueMap = valueMap.next) {
 						Color color = Color.valueOf(valueMap.getString("color"));
 						timeline.setFrame(frameIndex, valueMap.getFloat("time"), color.r, color.g, color.b, color.a);
-						readCurve(timeline, frameIndex, valueMap);
+						readCurve(valueMap, timeline, frameIndex);
 						frameIndex++;
 					}
 					timelines.add(timeline);
-					duration = Math.max(duration, timeline.getFrames()[timeline.getFrameCount() * 5 - 5]);
+					duration = Math.max(duration, timeline.getFrames()[(timeline.getFrameCount() - 1) * ColorTimeline.ENTRIES]);
 
 				} else if (timelineName.equals("attachment")) {
 					AttachmentTimeline timeline = new AttachmentTimeline(timelineMap.size);
@@ -418,7 +446,6 @@ public class SkeletonJson {
 		for (JsonValue boneMap = map.getChild("bones"); boneMap != null; boneMap = boneMap.next) {
 			int boneIndex = skeletonData.findBoneIndex(boneMap.name);
 			if (boneIndex == -1) throw new SerializationException("Bone not found: " + boneMap.name);
-
 			for (JsonValue timelineMap = boneMap.child; timelineMap != null; timelineMap = timelineMap.next) {
 				String timelineName = timelineMap.name;
 				if (timelineName.equals("rotate")) {
@@ -428,11 +455,11 @@ public class SkeletonJson {
 					int frameIndex = 0;
 					for (JsonValue valueMap = timelineMap.child; valueMap != null; valueMap = valueMap.next) {
 						timeline.setFrame(frameIndex, valueMap.getFloat("time"), valueMap.getFloat("angle"));
-						readCurve(timeline, frameIndex, valueMap);
+						readCurve(valueMap, timeline, frameIndex);
 						frameIndex++;
 					}
 					timelines.add(timeline);
-					duration = Math.max(duration, timeline.getFrames()[timeline.getFrameCount() * 2 - 2]);
+					duration = Math.max(duration, timeline.getFrames()[(timeline.getFrameCount() - 1) * RotateTimeline.ENTRIES]);
 
 				} else if (timelineName.equals("translate") || timelineName.equals("scale") || timelineName.equals("shear")) {
 					TranslateTimeline timeline;
@@ -451,11 +478,11 @@ public class SkeletonJson {
 					for (JsonValue valueMap = timelineMap.child; valueMap != null; valueMap = valueMap.next) {
 						float x = valueMap.getFloat("x", 0), y = valueMap.getFloat("y", 0);
 						timeline.setFrame(frameIndex, valueMap.getFloat("time"), x * timelineScale, y * timelineScale);
-						readCurve(timeline, frameIndex, valueMap);
+						readCurve(valueMap, timeline, frameIndex);
 						frameIndex++;
 					}
 					timelines.add(timeline);
-					duration = Math.max(duration, timeline.getFrames()[timeline.getFrameCount() * 3 - 3]);
+					duration = Math.max(duration, timeline.getFrames()[(timeline.getFrameCount() - 1) * TranslateTimeline.ENTRIES]);
 
 				} else
 					throw new RuntimeException("Invalid timeline type for a bone: " + timelineName + " (" + boneMap.name + ")");
@@ -470,12 +497,12 @@ public class SkeletonJson {
 			int frameIndex = 0;
 			for (JsonValue valueMap = constraintMap.child; valueMap != null; valueMap = valueMap.next) {
 				timeline.setFrame(frameIndex, valueMap.getFloat("time"), valueMap.getFloat("mix", 1),
-					valueMap.getBoolean("bendPositive") ? 1 : -1);
-				readCurve(timeline, frameIndex, valueMap);
+					valueMap.getBoolean("bendPositive", false) ? 1 : -1);
+				readCurve(valueMap, timeline, frameIndex);
 				frameIndex++;
 			}
 			timelines.add(timeline);
-			duration = Math.max(duration, timeline.getFrames()[timeline.getFrameCount() * 3 - 3]);
+			duration = Math.max(duration, timeline.getFrames()[(timeline.getFrameCount() - 1) * IkConstraintTimeline.ENTRIES]);
 		}
 
 		// Transform constraint timelines.
@@ -487,59 +514,98 @@ public class SkeletonJson {
 			for (JsonValue valueMap = constraintMap.child; valueMap != null; valueMap = valueMap.next) {
 				timeline.setFrame(frameIndex, valueMap.getFloat("time"), valueMap.getFloat("rotateMix", 1),
 					valueMap.getFloat("translateMix", 1), valueMap.getFloat("scaleMix", 1), valueMap.getFloat("shearMix", 1));
-				readCurve(timeline, frameIndex, valueMap);
+				readCurve(valueMap, timeline, frameIndex);
 				frameIndex++;
 			}
 			timelines.add(timeline);
-			duration = Math.max(duration, timeline.getFrames()[timeline.getFrameCount() * 5 - 5]);
+			duration = Math.max(duration,
+				timeline.getFrames()[(timeline.getFrameCount() - 1) * TransformConstraintTimeline.ENTRIES]);
 		}
 
-		// FFD timelines.
-		for (JsonValue ffdMap = map.getChild("ffd"); ffdMap != null; ffdMap = ffdMap.next) {
-			Skin skin = skeletonData.findSkin(ffdMap.name);
-			if (skin == null) throw new SerializationException("Skin not found: " + ffdMap.name);
-			for (JsonValue slotMap = ffdMap.child; slotMap != null; slotMap = slotMap.next) {
+		// Path constraint timelines.
+		for (JsonValue constraintMap = map.getChild("paths"); constraintMap != null; constraintMap = constraintMap.next) {
+			int index = skeletonData.findPathConstraintIndex(constraintMap.name);
+			if (index == -1) throw new SerializationException("Path constraint not found: " + constraintMap.name);
+			PathConstraintData data = skeletonData.getPathConstraints().get(index);
+			for (JsonValue timelineMap = constraintMap.child; timelineMap != null; timelineMap = timelineMap.next) {
+				String timelineName = timelineMap.name;
+				if (timelineName.equals("position") || timelineName.equals("spacing")) {
+					PathConstraintPositionTimeline timeline;
+					float timelineScale = 1;
+					if (timelineName.equals("spacing")) {
+						timeline = new PathConstraintSpacingTimeline(timelineMap.size);
+						if (data.spacingMode == SpacingMode.length || data.spacingMode == SpacingMode.fixed) timelineScale = scale;
+					} else {
+						timeline = new PathConstraintPositionTimeline(timelineMap.size);
+						if (data.positionMode == PositionMode.fixed) timelineScale = scale;
+					}
+					timeline.pathConstraintIndex = index;
+					int frameIndex = 0;
+					for (JsonValue valueMap = timelineMap.child; valueMap != null; valueMap = valueMap.next) {
+						timeline.setFrame(frameIndex, valueMap.getFloat("time"), valueMap.getFloat(timelineName, 0) * timelineScale);
+						readCurve(valueMap, timeline, frameIndex);
+						frameIndex++;
+					}
+					timelines.add(timeline);
+					duration = Math.max(duration,
+						timeline.getFrames()[(timeline.getFrameCount() - 1) * PathConstraintPositionTimeline.ENTRIES]);
+				} else if (timelineName.equals("mix")) {
+					PathConstraintMixTimeline timeline = new PathConstraintMixTimeline(timelineMap.size);
+					timeline.pathConstraintIndex = index;
+					int frameIndex = 0;
+					for (JsonValue valueMap = timelineMap.child; valueMap != null; valueMap = valueMap.next) {
+						timeline.setFrame(frameIndex, valueMap.getFloat("time"), valueMap.getFloat("rotateMix", 1),
+							valueMap.getFloat("translateMix", 1));
+						readCurve(valueMap, timeline, frameIndex);
+						frameIndex++;
+					}
+					timelines.add(timeline);
+					duration = Math.max(duration,
+						timeline.getFrames()[(timeline.getFrameCount() - 1) * PathConstraintMixTimeline.ENTRIES]);
+				}
+			}
+		}
+
+		// Deform timelines.
+		for (JsonValue deformMap = map.getChild("deform"); deformMap != null; deformMap = deformMap.next) {
+			Skin skin = skeletonData.findSkin(deformMap.name);
+			if (skin == null) throw new SerializationException("Skin not found: " + deformMap.name);
+			for (JsonValue slotMap = deformMap.child; slotMap != null; slotMap = slotMap.next) {
 				int slotIndex = skeletonData.findSlotIndex(slotMap.name);
 				if (slotIndex == -1) throw new SerializationException("Slot not found: " + slotMap.name);
-				for (JsonValue meshMap = slotMap.child; meshMap != null; meshMap = meshMap.next) {
-					FfdTimeline timeline = new FfdTimeline(meshMap.size);
-					Attachment attachment = skin.getAttachment(slotIndex, meshMap.name);
-					if (attachment == null) throw new SerializationException("FFD attachment not found: " + meshMap.name);
+				for (JsonValue timelineMap = slotMap.child; timelineMap != null; timelineMap = timelineMap.next) {
+					VertexAttachment attachment = (VertexAttachment)skin.getAttachment(slotIndex, timelineMap.name);
+					if (attachment == null) throw new SerializationException("Deform attachment not found: " + timelineMap.name);
+					boolean weighted = attachment.getBones() != null;
+					float[] vertices = attachment.getVertices();
+					int deformLength = weighted ? vertices.length / 3 * 2 : vertices.length;
+
+					DeformTimeline timeline = new DeformTimeline(timelineMap.size);
 					timeline.slotIndex = slotIndex;
 					timeline.attachment = attachment;
 
-					int vertexCount;
-					if (attachment instanceof MeshAttachment)
-						vertexCount = ((MeshAttachment)attachment).getVertices().length;
-					else
-						vertexCount = ((WeightedMeshAttachment)attachment).getWeights().length / 3 * 2;
-
 					int frameIndex = 0;
-					for (JsonValue valueMap = meshMap.child; valueMap != null; valueMap = valueMap.next) {
-						float[] vertices;
+					for (JsonValue valueMap = timelineMap.child; valueMap != null; valueMap = valueMap.next) {
+						float[] deform;
 						JsonValue verticesValue = valueMap.get("vertices");
-						if (verticesValue == null) {
-							if (attachment instanceof MeshAttachment)
-								vertices = ((MeshAttachment)attachment).getVertices();
-							else
-								vertices = new float[vertexCount];
-						} else {
-							vertices = new float[vertexCount];
+						if (verticesValue == null)
+							deform = weighted ? new float[deformLength] : vertices;
+						else {
+							deform = new float[deformLength];
 							int start = valueMap.getInt("offset", 0);
-							System.arraycopy(verticesValue.asFloatArray(), 0, vertices, start, verticesValue.size);
+							System.arraycopy(verticesValue.asFloatArray(), 0, deform, start, verticesValue.size);
 							if (scale != 1) {
 								for (int i = start, n = i + verticesValue.size; i < n; i++)
-									vertices[i] *= scale;
+									deform[i] *= scale;
 							}
-							if (attachment instanceof MeshAttachment) {
-								float[] meshVertices = ((MeshAttachment)attachment).getVertices();
-								for (int i = 0; i < vertexCount; i++)
-									vertices[i] += meshVertices[i];
+							if (!weighted) {
+								for (int i = 0; i < deformLength; i++)
+									deform[i] += vertices[i];
 							}
 						}
 
-						timeline.setFrame(frameIndex, valueMap.getFloat("time"), vertices);
-						readCurve(timeline, frameIndex, valueMap);
+						timeline.setFrame(frameIndex, valueMap.getFloat("time"), deform);
+						readCurve(valueMap, timeline, frameIndex);
 						frameIndex++;
 					}
 					timelines.add(timeline);
@@ -608,8 +674,8 @@ public class SkeletonJson {
 		skeletonData.animations.add(new Animation(name, timelines, duration));
 	}
 
-	void readCurve (CurveTimeline timeline, int frameIndex, JsonValue valueMap) {
-		JsonValue curve = valueMap.get("curve");
+	void readCurve (JsonValue map, CurveTimeline timeline, int frameIndex) {
+		JsonValue curve = map.get("curve");
 		if (curve == null) return;
 		if (curve.isString() && curve.asString().equals("stepped"))
 			timeline.setStepped(frameIndex);
@@ -621,9 +687,9 @@ public class SkeletonJson {
 	static class LinkedMesh {
 		String parent, skin;
 		int slotIndex;
-		Attachment mesh;
+		MeshAttachment mesh;
 
-		public LinkedMesh (Attachment mesh, String skin, int slotIndex, String parent) {
+		public LinkedMesh (MeshAttachment mesh, String skin, int slotIndex, String parent) {
 			this.mesh = mesh;
 			this.skin = skin;
 			this.slotIndex = slotIndex;

+ 1 - 8
spine-libgdx/spine-libgdx/src/com/esotericsoftware/spine/SkeletonMeshRenderer.java

@@ -35,10 +35,9 @@ import com.badlogic.gdx.graphics.Texture;
 import com.badlogic.gdx.graphics.g2d.PolygonSpriteBatch;
 import com.badlogic.gdx.utils.Array;
 import com.esotericsoftware.spine.attachments.Attachment;
-import com.esotericsoftware.spine.attachments.MeshAttachment;
 import com.esotericsoftware.spine.attachments.RegionAttachment;
 import com.esotericsoftware.spine.attachments.SkeletonAttachment;
-import com.esotericsoftware.spine.attachments.WeightedMeshAttachment;
+import com.esotericsoftware.spine.attachments.MeshAttachment;
 
 public class SkeletonMeshRenderer extends SkeletonRenderer<PolygonSpriteBatch> {
 	static private final short[] quadTriangles = {0, 1, 2, 2, 3, 0};
@@ -67,12 +66,6 @@ public class SkeletonMeshRenderer extends SkeletonRenderer<PolygonSpriteBatch> {
 				triangles = mesh.getTriangles();
 				texture = mesh.getRegion().getTexture();
 
-			} else if (attachment instanceof WeightedMeshAttachment) {
-				WeightedMeshAttachment mesh = (WeightedMeshAttachment)attachment;
-				vertices = mesh.updateWorldVertices(slot, premultipliedAlpha);
-				triangles = mesh.getTriangles();
-				texture = mesh.getRegion().getTexture();
-
 			} else if (attachment instanceof SkeletonAttachment) {
 				Skeleton attachmentSkeleton = ((SkeletonAttachment)attachment).getSkeleton();
 				if (attachmentSkeleton == null) continue;

+ 2 - 7
spine-libgdx/spine-libgdx/src/com/esotericsoftware/spine/SkeletonRenderer.java

@@ -34,18 +34,13 @@ package com.esotericsoftware.spine;
 import com.badlogic.gdx.graphics.g2d.Batch;
 import com.badlogic.gdx.utils.Array;
 import com.esotericsoftware.spine.attachments.Attachment;
-import com.esotericsoftware.spine.attachments.MeshAttachment;
 import com.esotericsoftware.spine.attachments.RegionAttachment;
 import com.esotericsoftware.spine.attachments.SkeletonAttachment;
-import com.esotericsoftware.spine.attachments.WeightedMeshAttachment;
+import com.esotericsoftware.spine.attachments.MeshAttachment;
 
 public class SkeletonRenderer<T extends Batch> {
 	boolean premultipliedAlpha;
 
-	public SkeletonRenderer () {
-		super();
-	}
-
 	public void draw (T batch, Skeleton skeleton) {
 		boolean premultipliedAlpha = this.premultipliedAlpha;
 		BlendMode blendMode = null;
@@ -64,7 +59,7 @@ public class SkeletonRenderer<T extends Batch> {
 				}
 				batch.draw(regionAttachment.getRegion().getTexture(), vertices, 0, 20);
 
-			} else if (attachment instanceof MeshAttachment || attachment instanceof WeightedMeshAttachment) {
+			} else if (attachment instanceof MeshAttachment) {
 				throw new RuntimeException("SkeletonMeshRenderer is required to render meshes.");
 
 			} else if (attachment instanceof SkeletonAttachment) {

+ 60 - 27
spine-libgdx/spine-libgdx/src/com/esotericsoftware/spine/SkeletonRendererDebug.java

@@ -31,11 +31,6 @@
 
 package com.esotericsoftware.spine;
 
-import com.esotericsoftware.spine.attachments.Attachment;
-import com.esotericsoftware.spine.attachments.MeshAttachment;
-import com.esotericsoftware.spine.attachments.RegionAttachment;
-import com.esotericsoftware.spine.attachments.WeightedMeshAttachment;
-
 import static com.badlogic.gdx.graphics.g2d.Batch.*;
 
 import com.badlogic.gdx.Gdx;
@@ -45,19 +40,24 @@ import com.badlogic.gdx.graphics.glutils.ShapeRenderer;
 import com.badlogic.gdx.graphics.glutils.ShapeRenderer.ShapeType;
 import com.badlogic.gdx.utils.Array;
 import com.badlogic.gdx.utils.FloatArray;
+import com.esotericsoftware.spine.attachments.Attachment;
+import com.esotericsoftware.spine.attachments.BoundingBoxAttachment;
+import com.esotericsoftware.spine.attachments.MeshAttachment;
+import com.esotericsoftware.spine.attachments.PathAttachment;
+import com.esotericsoftware.spine.attachments.RegionAttachment;
 
 public class SkeletonRendererDebug {
 	static private final Color boneLineColor = Color.RED;
 	static private final Color boneOriginColor = Color.GREEN;
 	static private final Color attachmentLineColor = new Color(0, 0, 1, 0.5f);
 	static private final Color triangleLineColor = new Color(1, 0.64f, 0, 0.5f);
-	static private final Color boundingBoxColor = new Color(0, 1, 0, 0.8f);
 	static private final Color aabbColor = new Color(0, 1, 0, 0.5f);
 
 	private final ShapeRenderer shapes;
 	private boolean drawBones = true, drawRegionAttachments = true, drawBoundingBoxes = true;
-	private boolean drawMeshHull = true, drawMeshTriangles = true;
+	private boolean drawMeshHull = true, drawMeshTriangles = true, drawPaths = true;
 	private final SkeletonBounds bounds = new SkeletonBounds();
+	private final FloatArray temp = new FloatArray();
 	private float scale = 1;
 	private float boneWidth = 2;
 	private boolean premultipliedAlpha;
@@ -119,23 +119,12 @@ public class SkeletonRendererDebug {
 			for (int i = 0, n = slots.size; i < n; i++) {
 				Slot slot = slots.get(i);
 				Attachment attachment = slot.attachment;
-				float[] vertices = null;
-				short[] triangles = null;
-				int hullLength = 0;
-				if (attachment instanceof MeshAttachment) {
-					MeshAttachment mesh = (MeshAttachment)attachment;
-					mesh.updateWorldVertices(slot, false);
-					vertices = mesh.getWorldVertices();
-					triangles = mesh.getTriangles();
-					hullLength = mesh.getHullLength();
-				} else if (attachment instanceof WeightedMeshAttachment) {
-					WeightedMeshAttachment mesh = (WeightedMeshAttachment)attachment;
-					mesh.updateWorldVertices(slot, false);
-					vertices = mesh.getWorldVertices();
-					triangles = mesh.getTriangles();
-					hullLength = mesh.getHullLength();
-				}
-				if (vertices == null || triangles == null) continue;
+				if (!(attachment instanceof MeshAttachment)) continue;
+				MeshAttachment mesh = (MeshAttachment)attachment;
+				mesh.updateWorldVertices(slot, false);
+				float[] vertices = mesh.getWorldVertices();
+				short[] triangles = mesh.getTriangles();
+				int hullLength = mesh.getHullLength();
 				if (drawMeshTriangles) {
 					shapes.setColor(triangleLineColor);
 					for (int ii = 0, nn = triangles.length; ii < nn; ii += 3) {
@@ -143,12 +132,12 @@ public class SkeletonRendererDebug {
 						shapes.triangle(vertices[v1], vertices[v1 + 1], //
 							vertices[v2], vertices[v2 + 1], //
 							vertices[v3], vertices[v3 + 1] //
-							);
+						);
 					}
 				}
 				if (drawMeshHull && hullLength > 0) {
 					shapes.setColor(attachmentLineColor);
-					hullLength = hullLength / 2 * 5;
+					hullLength = (hullLength >> 1) * 5;
 					float lastX = vertices[hullLength - 5], lastY = vertices[hullLength - 4];
 					for (int ii = 0, nn = hullLength; ii < nn; ii += 5) {
 						float x = vertices[ii], y = vertices[ii + 1];
@@ -165,14 +154,53 @@ public class SkeletonRendererDebug {
 			bounds.update(skeleton, true);
 			shapes.setColor(aabbColor);
 			shapes.rect(bounds.getMinX(), bounds.getMinY(), bounds.getWidth(), bounds.getHeight());
-			shapes.setColor(boundingBoxColor);
 			Array<FloatArray> polygons = bounds.getPolygons();
+			Array<BoundingBoxAttachment> boxes = bounds.getBoundingBoxes();
 			for (int i = 0, n = polygons.size; i < n; i++) {
 				FloatArray polygon = polygons.get(i);
+				shapes.setColor(boxes.get(i).getColor());
 				shapes.polygon(polygon.items, 0, polygon.size);
 			}
 		}
 
+		if (drawPaths) {
+			Array<Slot> slots = skeleton.getSlots();
+			for (int i = 0, n = slots.size; i < n; i++) {
+				Slot slot = slots.get(i);
+				Attachment attachment = slot.attachment;
+				if (!(attachment instanceof PathAttachment)) continue;
+				PathAttachment path = (PathAttachment)attachment;
+				int nn = path.getWorldVerticesLength();
+				float[] world = temp.setSize(nn);
+				path.computeWorldVertices(slot, world);
+				Color color = path.getColor();
+				float x1 = world[2], y1 = world[3], x2 = 0, y2 = 0;
+				if (path.getClosed()) {
+					shapes.setColor(color);
+					float cx1 = world[0], cy1 = world[1], cx2 = world[nn - 2], cy2 = world[nn - 1];
+					x2 = world[nn - 4];
+					y2 = world[nn - 3];
+					shapes.curve(x1, y1, cx1, cy1, cx2, cy2, x2, y2, 32);
+					shapes.setColor(Color.LIGHT_GRAY);
+					shapes.line(x1, y1, cx1, cy1);
+					shapes.line(x2, y2, cx2, cy2);
+				}
+				nn -= 4;
+				for (int ii = 4; ii < nn; ii += 6) {
+					float cx1 = world[ii], cy1 = world[ii + 1], cx2 = world[ii + 2], cy2 = world[ii + 3];
+					x2 = world[ii + 4];
+					y2 = world[ii + 5];
+					shapes.setColor(color);
+					shapes.curve(x1, y1, cx1, cy1, cx2, cy2, x2, y2, 32);
+					shapes.setColor(Color.LIGHT_GRAY);
+					shapes.line(x1, y1, cx1, cy1);
+					shapes.line(x2, y2, cx2, cy2);
+					x1 = x2;
+					y1 = y2;
+				}
+			}
+		}
+
 		shapes.end();
 		shapes.begin(ShapeType.Filled);
 
@@ -186,6 +214,7 @@ public class SkeletonRendererDebug {
 		}
 
 		shapes.end();
+
 	}
 
 	public ShapeRenderer getShapeRenderer () {
@@ -216,6 +245,10 @@ public class SkeletonRendererDebug {
 		this.drawMeshTriangles = meshTriangles;
 	}
 
+	public void setPaths (boolean paths) {
+		this.drawPaths = paths;
+	}
+
 	public void setPremultipliedAlpha (boolean premultipliedAlpha) {
 		this.premultipliedAlpha = premultipliedAlpha;
 	}

+ 3 - 12
spine-libgdx/spine-libgdx/src/com/esotericsoftware/spine/Slot.java

@@ -44,12 +44,6 @@ public class Slot {
 	private float attachmentTime;
 	private FloatArray attachmentVertices = new FloatArray();
 
-	Slot (SlotData data) {
-		this.data = data;
-		bone = null;
-		color = new Color(1, 1, 1, 1);
-	}
-
 	public Slot (SlotData data, Bone bone) {
 		if (data == null) throw new IllegalArgumentException("data cannot be null.");
 		if (bone == null) throw new IllegalArgumentException("bone cannot be null.");
@@ -110,6 +104,7 @@ public class Slot {
 	}
 
 	public void setAttachmentVertices (FloatArray attachmentVertices) {
+		if (attachmentVertices == null) throw new IllegalArgumentException("attachmentVertices cannot be null.");
 		this.attachmentVertices = attachmentVertices;
 	}
 
@@ -117,20 +112,16 @@ public class Slot {
 		return attachmentVertices;
 	}
 
-	void setToSetupPose (int slotIndex) {
+	public void setToSetupPose () {
 		color.set(data.color);
 		if (data.attachmentName == null)
 			setAttachment(null);
 		else {
 			attachment = null;
-			setAttachment(bone.skeleton.getAttachment(slotIndex, data.attachmentName));
+			setAttachment(bone.skeleton.getAttachment(data.index, data.attachmentName));
 		}
 	}
 
-	public void setToSetupPose () {
-		setToSetupPose(bone.skeleton.data.slots.indexOf(data, true));
-	}
-
 	public String toString () {
 		return data.name;
 	}

+ 8 - 6
spine-libgdx/spine-libgdx/src/com/esotericsoftware/spine/SlotData.java

@@ -34,24 +34,26 @@ package com.esotericsoftware.spine;
 import com.badlogic.gdx.graphics.Color;
 
 public class SlotData {
+	final int index;
 	final String name;
 	final BoneData boneData;
 	final Color color = new Color(1, 1, 1, 1);
 	String attachmentName;
 	BlendMode blendMode;
 
-	SlotData () {
-		name = null;
-		boneData = null;
-	}
-
-	public SlotData (String name, BoneData boneData) {
+	public SlotData (int index, String name, BoneData boneData) {
+		if (index < 0) throw new IllegalArgumentException("index must be >= 0.");
 		if (name == null) throw new IllegalArgumentException("name cannot be null.");
 		if (boneData == null) throw new IllegalArgumentException("boneData cannot be null.");
+		this.index = index;
 		this.name = name;
 		this.boneData = boneData;
 	}
 
+	public int getIndex () {
+		return index;
+	}
+
 	public String getName () {
 		return name;
 	}

+ 71 - 123
spine-libgdx/spine-libgdx/src/com/esotericsoftware/spine/TransformConstraint.java

@@ -4,46 +4,42 @@ package com.esotericsoftware.spine;
 import static com.badlogic.gdx.math.MathUtils.*;
 
 import com.badlogic.gdx.math.Vector2;
+import com.badlogic.gdx.utils.Array;
 
 public class TransformConstraint implements Updatable {
 	final TransformConstraintData data;
-	Bone bone, target;
+	final Array<Bone> bones;
+	Bone target;
 	float rotateMix, translateMix, scaleMix, shearMix;
-	float offsetRotation, offsetX, offsetY, offsetScaleX, offsetScaleY, offsetShearY;
 	final Vector2 temp = new Vector2();
 
 	public TransformConstraint (TransformConstraintData data, Skeleton skeleton) {
+		if (data == null) throw new IllegalArgumentException("data cannot be null.");
+		if (skeleton == null) throw new IllegalArgumentException("skeleton cannot be null.");
 		this.data = data;
-		translateMix = data.translateMix;
 		rotateMix = data.rotateMix;
+		translateMix = data.translateMix;
 		scaleMix = data.scaleMix;
 		shearMix = data.shearMix;
-		offsetX = data.offsetX;
-		offsetY = data.offsetY;
-		offsetScaleX = data.offsetScaleX;
-		offsetScaleY = data.offsetScaleY;
-		offsetShearY = data.offsetShearY;
-
-		if (skeleton != null) {
-			bone = skeleton.findBone(data.bone.name);
-			target = skeleton.findBone(data.target.name);
-		}
+		bones = new Array(data.bones.size);
+		for (BoneData boneData : data.bones)
+			bones.add(skeleton.findBone(boneData.name));
+		target = skeleton.findBone(data.target.name);
 	}
 
 	/** Copy constructor. */
 	public TransformConstraint (TransformConstraint constraint, Skeleton skeleton) {
+		if (constraint == null) throw new IllegalArgumentException("constraint cannot be null.");
+		if (skeleton == null) throw new IllegalArgumentException("skeleton cannot be null.");
 		data = constraint.data;
-		bone = skeleton.bones.get(constraint.bone.skeleton.bones.indexOf(constraint.bone, true));
-		target = skeleton.bones.get(constraint.target.skeleton.bones.indexOf(constraint.target, true));
-		translateMix = constraint.translateMix;
+		bones = new Array(constraint.bones.size);
+		for (Bone bone : constraint.bones)
+			bones.add(skeleton.bones.get(bone.data.index));
+		target = skeleton.bones.get(constraint.target.data.index);
 		rotateMix = constraint.rotateMix;
+		translateMix = constraint.translateMix;
 		scaleMix = constraint.scaleMix;
 		shearMix = constraint.shearMix;
-		offsetX = constraint.offsetX;
-		offsetY = constraint.offsetY;
-		offsetScaleX = constraint.offsetScaleX;
-		offsetScaleY = constraint.offsetScaleY;
-		offsetShearY = constraint.offsetShearY;
 	}
 
 	public void apply () {
@@ -51,64 +47,64 @@ public class TransformConstraint implements Updatable {
 	}
 
 	public void update () {
-		Bone bone = this.bone;
+		float rotateMix = this.rotateMix, translateMix = this.translateMix, scaleMix = this.scaleMix, shearMix = this.shearMix;
 		Bone target = this.target;
-
-		if (rotateMix > 0) {
-			float a = bone.a, b = bone.b, c = bone.c, d = bone.d;
-			float r = atan2(target.c, target.a) - atan2(c, a) + offsetRotation * degRad;
-			if (r > PI)
-				r -= PI2;
-			else if (r < -PI) r += PI2;
-			r *= rotateMix;
-			float cos = cos(r), sin = 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;
-		}
-
-		if (scaleMix > 0) {
-			float bs = (float)Math.sqrt(bone.a * bone.a + bone.c * bone.c);
-			float ts = (float)Math.sqrt(target.a * target.a + target.c * target.c);
-			float s = bs > 0.00001f ? (bs + (ts - bs + offsetScaleX) * scaleMix) / bs : 0;
-			bone.a *= s;
-			bone.c *= s;
-			bs = (float)Math.sqrt(bone.b * bone.b + bone.d * bone.d);
-			ts = (float)Math.sqrt(target.b * target.b + target.d * target.d);
-			s = bs > 0.00001f ? (bs + (ts - bs + offsetScaleY) * scaleMix) / bs : 0;
-			bone.b *= s;
-			bone.d *= s;
-		}
-
-		if (shearMix > 0) {
-			float b = bone.b, d = bone.d;
-			float by = atan2(d, b);
-			float r = atan2(target.d, target.b) - atan2(target.c, target.a) - (by - atan2(bone.c, bone.a));
-			if (r > PI)
-				r -= PI2;
-			else if (r < -PI) r += PI2;
-			r = by + (r + offsetShearY * degRad) * shearMix;
-			float s = (float)Math.sqrt(b * b + d * d);
-			bone.b = cos(r) * s;
-			bone.d = sin(r) * s;
-		}
-
-		float translateMix = this.translateMix;
-		if (translateMix > 0) {
-			Vector2 temp = this.temp;
-			target.localToWorld(temp.set(offsetX, offsetY));
-			bone.worldX += (temp.x - bone.worldX) * translateMix;
-			bone.worldY += (temp.y - bone.worldY) * translateMix;
+		float ta = target.a, tb = target.b, tc = target.c, td = target.d;
+		Array<Bone> bones = this.bones;
+		for (int i = 0, n = bones.size; i < n; i++) {
+			Bone bone = bones.get(i);
+
+			if (rotateMix > 0) {
+				float a = bone.a, b = bone.b, c = bone.c, d = bone.d;
+				float r = atan2(tc, ta) - atan2(c, a) + data.offsetRotation * degRad;
+				if (r > PI)
+					r -= PI2;
+				else if (r < -PI) r += PI2;
+				r *= rotateMix;
+				float cos = cos(r), sin = 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;
+			}
+
+			if (translateMix > 0) {
+				Vector2 temp = this.temp;
+				target.localToWorld(temp.set(data.offsetX, data.offsetY));
+				bone.worldX += (temp.x - bone.worldX) * translateMix;
+				bone.worldY += (temp.y - bone.worldY) * translateMix;
+			}
+
+			if (scaleMix > 0) {
+				float bs = (float)Math.sqrt(bone.a * bone.a + bone.c * bone.c);
+				float ts = (float)Math.sqrt(ta * ta + tc * tc);
+				float s = bs > 0.00001f ? (bs + (ts - bs + data.offsetScaleX) * scaleMix) / bs : 0;
+				bone.a *= s;
+				bone.c *= s;
+				bs = (float)Math.sqrt(bone.b * bone.b + bone.d * bone.d);
+				ts = (float)Math.sqrt(tb * tb + td * td);
+				s = bs > 0.00001f ? (bs + (ts - bs + data.offsetScaleY) * scaleMix) / bs : 0;
+				bone.b *= s;
+				bone.d *= s;
+			}
+
+			if (shearMix > 0) {
+				float b = bone.b, d = bone.d;
+				float by = atan2(d, b);
+				float r = atan2(td, tb) - atan2(tc, ta) - (by - atan2(bone.c, bone.a));
+				if (r > PI)
+					r -= PI2;
+				else if (r < -PI) r += PI2;
+				r = by + (r + data.offsetShearY * degRad) * shearMix;
+				float s = (float)Math.sqrt(b * b + d * d);
+				bone.b = cos(r) * s;
+				bone.d = sin(r) * s;
+			}
 		}
 	}
 
-	public Bone getBone () {
-		return bone;
-	}
-
-	public void setBone (Bone bone) {
-		this.bone = bone;
+	public Array<Bone> getBones () {
+		return bones;
 	}
 
 	public Bone getTarget () {
@@ -151,54 +147,6 @@ public class TransformConstraint implements Updatable {
 		this.shearMix = shearMix;
 	}
 
-	public float getOffsetRotation () {
-		return offsetRotation;
-	}
-
-	public void setOffsetRotation (float offsetRotation) {
-		this.offsetRotation = offsetRotation;
-	}
-
-	public float getOffsetX () {
-		return offsetX;
-	}
-
-	public void setOffsetX (float offsetX) {
-		this.offsetX = offsetX;
-	}
-
-	public float getOffsetY () {
-		return offsetY;
-	}
-
-	public void setOffsetY (float offsetY) {
-		this.offsetY = offsetY;
-	}
-
-	public float getOffsetScaleX () {
-		return offsetScaleX;
-	}
-
-	public void setOffsetScaleX (float offsetScaleX) {
-		this.offsetScaleX = offsetScaleX;
-	}
-
-	public float getOffsetScaleY () {
-		return offsetScaleY;
-	}
-
-	public void setOffsetScaleY (float offsetScaleY) {
-		this.offsetScaleY = offsetScaleY;
-	}
-
-	public float getOffsetShearY () {
-		return offsetShearY;
-	}
-
-	public void setOffsetShearY (float offsetShearY) {
-		this.offsetShearY = offsetShearY;
-	}
-
 	public TransformConstraintData getData () {
 		return data;
 	}

+ 8 - 7
spine-libgdx/spine-libgdx/src/com/esotericsoftware/spine/TransformConstraintData.java

@@ -1,13 +1,17 @@
 
 package com.esotericsoftware.spine;
 
+import com.badlogic.gdx.utils.Array;
+
 public class TransformConstraintData {
 	final String name;
-	BoneData bone, target;
+	final Array<BoneData> bones = new Array();
+	BoneData target;
 	float rotateMix, translateMix, scaleMix, shearMix;
 	float offsetRotation, offsetX, offsetY, offsetScaleX, offsetScaleY, offsetShearY;
 
 	public TransformConstraintData (String name) {
+		if (name == null) throw new IllegalArgumentException("name cannot be null.");
 		this.name = name;
 	}
 
@@ -15,12 +19,8 @@ public class TransformConstraintData {
 		return name;
 	}
 
-	public BoneData getBone () {
-		return bone;
-	}
-
-	public void setBone (BoneData bone) {
-		this.bone = bone;
+	public Array<BoneData> getBones () {
+		return bones;
 	}
 
 	public BoneData getTarget () {
@@ -28,6 +28,7 @@ public class TransformConstraintData {
 	}
 
 	public void setTarget (BoneData target) {
+		if (target == null) throw new IllegalArgumentException("target cannot be null.");
 		this.target = target;
 	}
 

+ 5 - 11
spine-libgdx/spine-libgdx/src/com/esotericsoftware/spine/attachments/AtlasAttachmentLoader.java

@@ -46,8 +46,7 @@ public class AtlasAttachmentLoader implements AttachmentLoader {
 
 	public RegionAttachment newRegionAttachment (Skin skin, String name, String path) {
 		AtlasRegion region = atlas.findRegion(path);
-		if (region == null)
-			throw new RuntimeException("Region not found in atlas: " + path + " (region attachment: " + name + ")");
+		if (region == null) throw new RuntimeException("Region not found in atlas: " + path + " (region attachment: " + name + ")");
 		RegionAttachment attachment = new RegionAttachment(name);
 		attachment.setRegion(region);
 		return attachment;
@@ -61,16 +60,11 @@ public class AtlasAttachmentLoader implements AttachmentLoader {
 		return attachment;
 	}
 
-	public WeightedMeshAttachment newWeightedMeshAttachment (Skin skin, String name, String path) {
-		AtlasRegion region = atlas.findRegion(path);
-		if (region == null)
-			throw new RuntimeException("Region not found in atlas: " + path + " (weighted mesh attachment: " + name + ")");
-		WeightedMeshAttachment attachment = new WeightedMeshAttachment(name);
-		attachment.setRegion(region);
-		return attachment;
-	}
-
 	public BoundingBoxAttachment newBoundingBoxAttachment (Skin skin, String name) {
 		return new BoundingBoxAttachment(name);
 	}
+
+	public PathAttachment newPathAttachment (Skin skin, String name) {
+		return new PathAttachment(name);
+	}
 }

+ 2 - 2
spine-libgdx/spine-libgdx/src/com/esotericsoftware/spine/attachments/AttachmentLoader.java

@@ -41,8 +41,8 @@ public interface AttachmentLoader {
 	public MeshAttachment newMeshAttachment (Skin skin, String name, String path);
 
 	/** @return May be null to not load any attachment. */
-	public WeightedMeshAttachment newWeightedMeshAttachment (Skin skin, String name, String path);
+	public BoundingBoxAttachment newBoundingBoxAttachment (Skin skin, String name);
 
 	/** @return May be null to not load any attachment. */
-	public BoundingBoxAttachment newBoundingBoxAttachment (Skin skin, String name);
+	public PathAttachment newPathAttachment (Skin skin, String name);
 }

+ 1 - 1
spine-libgdx/spine-libgdx/src/com/esotericsoftware/spine/attachments/AttachmentType.java

@@ -32,7 +32,7 @@
 package com.esotericsoftware.spine.attachments;
 
 public enum AttachmentType {
-	region, boundingbox, mesh, weightedmesh, linkedmesh, weightedlinkedmesh;
+	region, boundingbox, mesh, linkedmesh, path;
 
 	static public AttachmentType[] values = values();
 }

+ 9 - 24
spine-libgdx/spine-libgdx/src/com/esotericsoftware/spine/attachments/BoundingBoxAttachment.java

@@ -31,37 +31,22 @@
 
 package com.esotericsoftware.spine.attachments;
 
-import com.esotericsoftware.spine.Bone;
-import com.esotericsoftware.spine.Skeleton;
+import com.badlogic.gdx.graphics.Color;
+import com.esotericsoftware.spine.Slot;
 
-public class BoundingBoxAttachment extends Attachment {
-	private float[] vertices;
+public class BoundingBoxAttachment extends VertexAttachment {
+	// Nonessential.
+	final Color color = new Color(0.38f, 0.94f, 0, 1);
 
 	public BoundingBoxAttachment (String name) {
 		super(name);
 	}
 
-	public void computeWorldVertices (Bone bone, float[] worldVertices) {
-		Skeleton skeleton = bone.getSkeleton();
-		float x = skeleton.getX() + bone.getWorldX(), y = skeleton.getY() + bone.getWorldY();
-		float m00 = bone.getA();
-		float m01 = bone.getB();
-		float m10 = bone.getC();
-		float m11 = bone.getD();
-		float[] vertices = this.vertices;
-		for (int i = 0, n = vertices.length; i < n; i += 2) {
-			float px = vertices[i];
-			float py = vertices[i + 1];
-			worldVertices[i] = px * m00 + py * m01 + x;
-			worldVertices[i + 1] = px * m10 + py * m11 + y;
-		}
+	public void computeWorldVertices (Slot slot, float[] worldVertices) {
+		computeWorldVertices(slot, 0, worldVerticesLength, worldVertices, 0);
 	}
 
-	public float[] getVertices () {
-		return vertices;
-	}
-
-	public void setVertices (float[] vertices) {
-		this.vertices = vertices;
+	public Color getColor () {
+		return color;
 	}
 }

+ 0 - 6
spine-libgdx/spine-libgdx/src/com/esotericsoftware/spine/attachments/FfdAttachment.java

@@ -1,6 +0,0 @@
-
-package com.esotericsoftware.spine.attachments;
-
-public interface FfdAttachment {
-	public boolean applyFFD (Attachment sourceAttachment);
-}

+ 72 - 44
spine-libgdx/spine-libgdx/src/com/esotericsoftware/spine/attachments/MeshAttachment.java

@@ -41,16 +41,15 @@ import com.esotericsoftware.spine.Skeleton;
 import com.esotericsoftware.spine.Slot;
 
 /** Attachment that displays a texture region. */
-public class MeshAttachment extends Attachment implements FfdAttachment {
+public class MeshAttachment extends VertexAttachment {
 	private TextureRegion region;
 	private String path;
-	private float[] vertices, regionUVs;
+	private float[] regionUVs, worldVertices;
 	private short[] triangles;
-	private float[] worldVertices;
 	private final Color color = new Color(1, 1, 1, 1);
 	private int hullLength;
 	private MeshAttachment parentMesh;
-	private boolean inheritFFD;
+	private boolean inheritDeform;
 
 	// Nonessential.
 	private short[] edges;
@@ -71,8 +70,9 @@ public class MeshAttachment extends Attachment implements FfdAttachment {
 	}
 
 	public void updateUVs () {
-		int verticesLength = vertices.length;
-		int worldVerticesLength = verticesLength / 2 * 5;
+		float[] regionUVs = this.regionUVs;
+		int verticesLength = regionUVs.length;
+		int worldVerticesLength = (verticesLength >> 1) * 5;
 		if (worldVertices == null || worldVertices.length != worldVerticesLength) worldVertices = new float[worldVerticesLength];
 
 		float u, v, width, height;
@@ -85,7 +85,6 @@ public class MeshAttachment extends Attachment implements FfdAttachment {
 			width = region.getU2() - u;
 			height = region.getV2() - v;
 		}
-		float[] regionUVs = this.regionUVs;
 		if (region instanceof AtlasRegion && ((AtlasRegion)region).rotate) {
 			for (int i = 0, w = 3; i < verticesLength; i += 2, w += 5) {
 				worldVertices[w] = u + regionUVs[i + 1] * width;
@@ -102,54 +101,81 @@ public class MeshAttachment extends Attachment implements FfdAttachment {
 	/** @return The updated world vertices. */
 	public float[] updateWorldVertices (Slot slot, boolean premultipliedAlpha) {
 		Skeleton skeleton = slot.getSkeleton();
-		Color skeletonColor = skeleton.getColor();
-		Color slotColor = slot.getColor();
-		Color meshColor = color;
-		float a = skeletonColor.a * slotColor.a * meshColor.a * 255;
-		float multiplier = premultipliedAlpha ? a : 255;
+		Color skeletonColor = skeleton.getColor(), slotColor = slot.getColor(), meshColor = color;
+		float alpha = skeletonColor.a * slotColor.a * meshColor.a * 255;
+		float multiplier = premultipliedAlpha ? alpha : 255;
 		float color = NumberUtils.intToFloatColor( //
-			((int)a << 24) //
+			((int)alpha << 24) //
 				| ((int)(skeletonColor.b * slotColor.b * meshColor.b * multiplier) << 16) //
 				| ((int)(skeletonColor.g * slotColor.g * meshColor.g * multiplier) << 8) //
 				| (int)(skeletonColor.r * slotColor.r * meshColor.r * multiplier));
 
-		float[] worldVertices = this.worldVertices;
-		FloatArray slotVertices = slot.getAttachmentVertices();
-		float[] vertices = this.vertices;
-		if (slotVertices.size == vertices.length) vertices = slotVertices.items;
-		Bone bone = slot.getBone();
-		float x = skeleton.getX() + bone.getWorldX(), y = skeleton.getY() + bone.getWorldY();
-		float m00 = bone.getA(), m01 = bone.getB(), m10 = bone.getC(), m11 = bone.getD();
-		for (int v = 0, w = 0, n = worldVertices.length; w < n; v += 2, w += 5) {
-			float vx = vertices[v];
-			float vy = vertices[v + 1];
-			worldVertices[w] = vx * m00 + vy * m01 + x;
-			worldVertices[w + 1] = vx * m10 + vy * m11 + y;
-			worldVertices[w + 2] = color;
+		float x = skeleton.getX(), y = skeleton.getY();
+		FloatArray deformArray = slot.getAttachmentVertices();
+		float[] vertices = this.vertices, worldVertices = this.worldVertices;
+		int[] bones = this.bones;
+		if (bones == null) {
+			int verticesLength = vertices.length;
+			if (deformArray.size > 0) vertices = deformArray.items;
+			Bone bone = slot.getBone();
+			x += bone.getWorldX();
+			y += bone.getWorldY();
+			float a = bone.getA(), b = bone.getB(), c = bone.getC(), d = bone.getD();
+			for (int v = 0, w = 0; v < verticesLength; v += 2, w += 5) {
+				float vx = vertices[v], vy = vertices[v + 1];
+				worldVertices[w] = vx * a + vy * b + x;
+				worldVertices[w + 1] = vx * c + vy * d + y;
+				worldVertices[w + 2] = color;
+			}
+			return worldVertices;
+		}
+		Object[] skeletonBones = skeleton.getBones().items;
+		if (deformArray.size == 0) {
+			for (int w = 0, v = 0, b = 0, n = bones.length; v < n; w += 5) {
+				float wx = x, wy = y;
+				int nn = bones[v++] + v;
+				for (; v < nn; v++, b += 3) {
+					Bone bone = (Bone)skeletonBones[bones[v]];
+					float vx = vertices[b], vy = vertices[b + 1], weight = vertices[b + 2];
+					wx += (vx * bone.getA() + vy * bone.getB() + bone.getWorldX()) * weight;
+					wy += (vx * bone.getC() + vy * bone.getD() + bone.getWorldY()) * weight;
+				}
+				worldVertices[w] = wx;
+				worldVertices[w + 1] = wy;
+				worldVertices[w + 2] = color;
+			}
+		} else {
+			float[] deform = deformArray.items;
+			for (int w = 0, v = 0, b = 0, f = 0, n = bones.length; v < n; w += 5) {
+				float wx = x, wy = y;
+				int nn = bones[v++] + v;
+				for (; v < nn; v++, b += 3, f += 2) {
+					Bone 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.getA() + vy * bone.getB() + bone.getWorldX()) * weight;
+					wy += (vx * bone.getC() + vy * bone.getD() + bone.getWorldY()) * weight;
+				}
+				worldVertices[w] = wx;
+				worldVertices[w + 1] = wy;
+				worldVertices[w + 2] = color;
+			}
 		}
 		return worldVertices;
 	}
 
-	public boolean applyFFD (Attachment sourceAttachment) {
-		return this == sourceAttachment || (inheritFFD && parentMesh == sourceAttachment);
+	public boolean applyDeform (VertexAttachment sourceAttachment) {
+		return this == sourceAttachment || (inheritDeform && parentMesh == sourceAttachment);
 	}
 
 	public float[] getWorldVertices () {
 		return worldVertices;
 	}
 
-	public float[] getVertices () {
-		return vertices;
-	}
-
-	public void setVertices (float[] vertices) {
-		this.vertices = vertices;
-	}
-
 	public short[] getTriangles () {
 		return triangles;
 	}
 
+	/** Vertex number triplets which describe the mesh's triangulation. */
 	public void setTriangles (short[] triangles) {
 		this.triangles = triangles;
 	}
@@ -158,6 +184,7 @@ public class MeshAttachment extends Attachment implements FfdAttachment {
 		return regionUVs;
 	}
 
+	/** Sets the texture coordinates for the region. The values are u,v pairs for each vertex. */
 	public void setRegionUVs (float[] regionUVs) {
 		this.regionUVs = regionUVs;
 	}
@@ -182,14 +209,14 @@ public class MeshAttachment extends Attachment implements FfdAttachment {
 		this.hullLength = hullLength;
 	}
 
-	public short[] getEdges () {
-		return edges;
-	}
-
 	public void setEdges (short[] edges) {
 		this.edges = edges;
 	}
 
+	public short[] getEdges () {
+		return edges;
+	}
+
 	public float getWidth () {
 		return width;
 	}
@@ -215,6 +242,7 @@ public class MeshAttachment extends Attachment implements FfdAttachment {
 	public void setParentMesh (MeshAttachment parentMesh) {
 		this.parentMesh = parentMesh;
 		if (parentMesh != null) {
+			bones = parentMesh.bones;
 			vertices = parentMesh.vertices;
 			regionUVs = parentMesh.regionUVs;
 			triangles = parentMesh.triangles;
@@ -225,11 +253,11 @@ public class MeshAttachment extends Attachment implements FfdAttachment {
 		}
 	}
 
-	public boolean getInheritFFD () {
-		return inheritFFD;
+	public boolean getInheritDeform () {
+		return inheritDeform;
 	}
 
-	public void setInheritFFD (boolean inheritFFD) {
-		this.inheritFFD = inheritFFD;
+	public void setInheritDeform (boolean inheritDeform) {
+		this.inheritDeform = inheritDeform;
 	}
 }

+ 84 - 0
spine-libgdx/spine-libgdx/src/com/esotericsoftware/spine/attachments/PathAttachment.java

@@ -0,0 +1,84 @@
+/******************************************************************************
+ * Spine Runtimes Software License
+ * Version 2.3
+ * 
+ * Copyright (c) 2013-2015, 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 (the "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 otherwise create derivative works, improvements of the
+ * Software or develop new applications using the Software 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; LOSS OF USE, DATA, OR PROFITS;
+ * OR BUSINESS INTERRUPTION) 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.
+ *****************************************************************************/
+
+package com.esotericsoftware.spine.attachments;
+
+import com.badlogic.gdx.graphics.Color;
+import com.esotericsoftware.spine.Slot;
+
+public class PathAttachment extends VertexAttachment {
+	float[] lengths;
+	boolean closed, constantSpeed;
+
+	// Nonessential.
+	final Color color = new Color(1, 0.5f, 0, 1);
+
+	public PathAttachment (String name) {
+		super(name);
+	}
+
+	public void computeWorldVertices (Slot slot, float[] worldVertices) {
+		super.computeWorldVertices(slot, worldVertices);
+	}
+
+	public void computeWorldVertices (Slot slot, int start, int count, float[] worldVertices, int offset) {
+		super.computeWorldVertices(slot, start, count, worldVertices, offset);
+	}
+
+	public boolean getClosed () {
+		return closed;
+	}
+
+	public void setClosed (boolean closed) {
+		this.closed = closed;
+	}
+
+	public boolean getConstantSpeed () {
+		return constantSpeed;
+	}
+
+	public void setConstantSpeed (boolean constantSpeed) {
+		this.constantSpeed = constantSpeed;
+	}
+
+	/** Returns the length in the setup pose from the start of the path to the end of each curve. */
+	public float[] getLengths () {
+		return lengths;
+	}
+
+	public void setLengths (float[] lengths) {
+		this.lengths = lengths;
+	}
+
+	public Color getColor () {
+		return color;
+	}
+}

+ 12 - 12
spine-libgdx/spine-libgdx/src/com/esotericsoftware/spine/attachments/RegionAttachment.java

@@ -152,10 +152,10 @@ public class RegionAttachment extends Attachment {
 		Color skeletonColor = skeleton.getColor();
 		Color slotColor = slot.getColor();
 		Color regionColor = color;
-		float a = skeletonColor.a * slotColor.a * regionColor.a * 255;
-		float multiplier = premultipliedAlpha ? a : 255;
+		float alpha = skeletonColor.a * slotColor.a * regionColor.a * 255;
+		float multiplier = premultipliedAlpha ? alpha : 255;
 		float color = NumberUtils.intToFloatColor( //
-			((int)a << 24) //
+			((int)alpha << 24) //
 				| ((int)(skeletonColor.b * slotColor.b * regionColor.b * multiplier) << 16) //
 				| ((int)(skeletonColor.g * slotColor.g * regionColor.g * multiplier) << 8) //
 				| (int)(skeletonColor.r * slotColor.r * regionColor.r * multiplier));
@@ -164,31 +164,31 @@ public class RegionAttachment extends Attachment {
 		float[] offset = this.offset;
 		Bone bone = slot.getBone();
 		float x = skeleton.getX() + bone.getWorldX(), y = skeleton.getY() + bone.getWorldY();
-		float m00 = bone.getA(), m01 = bone.getB(), m10 = bone.getC(), m11 = bone.getD();
+		float a = bone.getA(), b = bone.getB(), c = bone.getC(), d = bone.getD();
 		float offsetX, offsetY;
 
 		offsetX = offset[BRX];
 		offsetY = offset[BRY];
-		vertices[X1] = offsetX * m00 + offsetY * m01 + x; // br
-		vertices[Y1] = offsetX * m10 + offsetY * m11 + y;
+		vertices[X1] = offsetX * a + offsetY * b + x; // br
+		vertices[Y1] = offsetX * c + offsetY * d + y;
 		vertices[C1] = color;
 
 		offsetX = offset[BLX];
 		offsetY = offset[BLY];
-		vertices[X2] = offsetX * m00 + offsetY * m01 + x; // bl
-		vertices[Y2] = offsetX * m10 + offsetY * m11 + y;
+		vertices[X2] = offsetX * a + offsetY * b + x; // bl
+		vertices[Y2] = offsetX * c + offsetY * d + y;
 		vertices[C2] = color;
 
 		offsetX = offset[ULX];
 		offsetY = offset[ULY];
-		vertices[X3] = offsetX * m00 + offsetY * m01 + x; // ul
-		vertices[Y3] = offsetX * m10 + offsetY * m11 + y;
+		vertices[X3] = offsetX * a + offsetY * b + x; // ul
+		vertices[Y3] = offsetX * c + offsetY * d + y;
 		vertices[C3] = color;
 
 		offsetX = offset[URX];
 		offsetY = offset[URY];
-		vertices[X4] = offsetX * m00 + offsetY * m01 + x; // ur
-		vertices[Y4] = offsetX * m10 + offsetY * m11 + y;
+		vertices[X4] = offsetX * a + offsetY * b + x; // ur
+		vertices[Y4] = offsetX * c + offsetY * d + y;
 		vertices[C4] = color;
 		return vertices;
 	}

+ 1 - 1
spine-libgdx/spine-libgdx/src/com/esotericsoftware/spine/attachments/RegionSequenceAttachment.java

@@ -58,7 +58,7 @@ public class RegionSequenceAttachment extends RegionAttachment {
 			frameIndex = frameIndex % regions.length;
 			break;
 		case pingPong:
-			frameIndex = frameIndex % (regions.length * 2);
+			frameIndex = frameIndex % (regions.length << 1);
 			if (frameIndex >= regions.length) frameIndex = regions.length - 1 - (frameIndex - regions.length);
 			break;
 		case random:

+ 146 - 0
spine-libgdx/spine-libgdx/src/com/esotericsoftware/spine/attachments/VertexAttachment.java

@@ -0,0 +1,146 @@
+/******************************************************************************
+ * Spine Runtimes Software License
+ * Version 2.3
+ * 
+ * Copyright (c) 2013-2015, 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 (the "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 otherwise create derivative works, improvements of the
+ * Software or develop new applications using the Software 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; LOSS OF USE, DATA, OR PROFITS;
+ * OR BUSINESS INTERRUPTION) 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.
+ *****************************************************************************/
+
+package com.esotericsoftware.spine.attachments;
+
+import com.badlogic.gdx.utils.FloatArray;
+import com.esotericsoftware.spine.Bone;
+import com.esotericsoftware.spine.Skeleton;
+import com.esotericsoftware.spine.Slot;
+
+/** An attachment with vertices that are transformed by one or more bones and can be deformed by a slot's vertices. */
+public class VertexAttachment extends Attachment {
+	int[] bones;
+	float[] vertices;
+	int worldVerticesLength;
+
+	public VertexAttachment (String name) {
+		super(name);
+	}
+
+	protected void computeWorldVertices (Slot slot, float[] worldVertices) {
+		computeWorldVertices(slot, 0, worldVerticesLength, worldVertices, 0);
+	}
+
+	/** Transforms local vertices to world coordinates.
+	 * @param start The index of the first local vertex value to transform. Each vertex has 2 values, x and y.
+	 * @param count The number of world vertex values to output. Must be <= {@link #getWorldVerticesLength()} - start.
+	 * @param worldVertices The output world vertices. Must have a length >= offset + count.
+	 * @param offset The worldVertices index to begin writing values. */
+	protected void computeWorldVertices (Slot slot, int start, int count, float[] worldVertices, int offset) {
+		count += offset;
+		Skeleton skeleton = slot.getSkeleton();
+		float x = skeleton.getX(), y = skeleton.getY();
+		FloatArray deformArray = slot.getAttachmentVertices();
+		float[] vertices = this.vertices;
+		int[] bones = this.bones;
+		if (bones == null) {
+			if (deformArray.size > 0) vertices = deformArray.items;
+			Bone bone = slot.getBone();
+			x += bone.getWorldX();
+			y += bone.getWorldY();
+			float a = bone.getA(), b = bone.getB(), c = bone.getC(), d = bone.getD();
+			for (int v = start, w = offset; w < count; v += 2, w += 2) {
+				float vx = vertices[v], vy = vertices[v + 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;
+		}
+		Object[] skeletonBones = skeleton.getBones().items;
+		if (deformArray.size == 0) {
+			for (int w = offset, b = skip * 3; w < count; w += 2) {
+				float wx = x, wy = y;
+				for (int n = bones[v++] + v; v < n; v++, b += 3) {
+					Bone bone = (Bone)skeletonBones[bones[v]];
+					float vx = vertices[b], vy = vertices[b + 1], weight = vertices[b + 2];
+					wx += (vx * bone.getA() + vy * bone.getB() + bone.getWorldX()) * weight;
+					wy += (vx * bone.getC() + vy * bone.getD() + bone.getWorldY()) * 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 += 2) {
+				float wx = x, wy = y;
+				for (int n = bones[v++] + v; v < n; v++, b += 3, f += 2) {
+					Bone 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.getA() + vy * bone.getB() + bone.getWorldX()) * weight;
+					wy += (vx * bone.getC() + vy * bone.getD() + bone.getWorldY()) * 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. */
+	public boolean applyDeform (VertexAttachment sourceAttachment) {
+		return this == sourceAttachment;
+	}
+
+	/** @return May be null if this attachment has no weights. */
+	public int[] getBones () {
+		return bones;
+	}
+
+	/** For each vertex, the number of bones affecting the vertex followed by that many bone indices. Ie: count, boneIndex, ...
+	 * @param bones May be null if this attachment has no weights. */
+	public void setBones (int[] bones) {
+		this.bones = bones;
+	}
+
+	public float[] getVertices () {
+		return vertices;
+	}
+
+	/** Sets the vertex position in the bone's coordinate system. For a non-weighted attachment, the values are x,y entries for
+	 * each vertex. For a weighted attachment, the values are x,y,weight entries for each bone affecting each vertex. */
+	public void setVertices (float[] vertices) {
+		this.vertices = vertices;
+	}
+
+	public int getWorldVerticesLength () {
+		return worldVerticesLength;
+	}
+
+	public void setWorldVerticesLength (int worldVerticesLength) {
+		this.worldVerticesLength = worldVerticesLength;
+	}
+}

+ 0 - 274
spine-libgdx/spine-libgdx/src/com/esotericsoftware/spine/attachments/WeightedMeshAttachment.java

@@ -1,274 +0,0 @@
-/******************************************************************************
- * Spine Runtimes Software License
- * Version 2.3
- * 
- * Copyright (c) 2013-2015, 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 (the "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 otherwise create derivative works, improvements of the
- * Software or develop new applications using the Software 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; LOSS OF USE, DATA, OR PROFITS;
- * OR BUSINESS INTERRUPTION) 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.
- *****************************************************************************/
-
-package com.esotericsoftware.spine.attachments;
-
-import com.esotericsoftware.spine.Bone;
-import com.esotericsoftware.spine.Skeleton;
-import com.esotericsoftware.spine.Slot;
-
-import com.badlogic.gdx.graphics.Color;
-import com.badlogic.gdx.graphics.g2d.TextureAtlas.AtlasRegion;
-import com.badlogic.gdx.graphics.g2d.TextureRegion;
-import com.badlogic.gdx.utils.FloatArray;
-import com.badlogic.gdx.utils.NumberUtils;
-
-/** Attachment that displays a texture region. */
-public class WeightedMeshAttachment extends Attachment implements FfdAttachment {
-	private TextureRegion region;
-	private String path;
-	private int[] bones;
-	private float[] weights, regionUVs;
-	private short[] triangles;
-	private float[] worldVertices;
-	private final Color color = new Color(1, 1, 1, 1);
-	private int hullLength;
-	private WeightedMeshAttachment parentMesh;
-	private boolean inheritFFD;
-
-	// Nonessential.
-	private short[] edges;
-	private float width, height;
-
-	public WeightedMeshAttachment (String name) {
-		super(name);
-	}
-
-	public void setRegion (TextureRegion region) {
-		if (region == null) throw new IllegalArgumentException("region cannot be null.");
-		this.region = region;
-	}
-
-	public TextureRegion getRegion () {
-		if (region == null) throw new IllegalStateException("Region has not been set: " + this);
-		return region;
-	}
-
-	public void updateUVs () {
-		float[] regionUVs = this.regionUVs;
-		int verticesLength = regionUVs.length;
-		int worldVerticesLength = verticesLength / 2 * 5;
-		if (worldVertices == null || worldVertices.length != worldVerticesLength) worldVertices = new float[worldVerticesLength];
-
-		float u, v, width, height;
-		if (region == null) {
-			u = v = 0;
-			width = height = 1;
-		} else {
-			u = region.getU();
-			v = region.getV();
-			width = region.getU2() - u;
-			height = region.getV2() - v;
-		}
-		if (region instanceof AtlasRegion && ((AtlasRegion)region).rotate) {
-			for (int i = 0, w = 3; i < verticesLength; i += 2, w += 5) {
-				worldVertices[w] = u + regionUVs[i + 1] * width;
-				worldVertices[w + 1] = v + height - regionUVs[i] * height;
-			}
-		} else {
-			for (int i = 0, w = 3; i < verticesLength; i += 2, w += 5) {
-				worldVertices[w] = u + regionUVs[i] * width;
-				worldVertices[w + 1] = v + regionUVs[i + 1] * height;
-			}
-		}
-	}
-
-	/** @return The updated world vertices. */
-	public float[] updateWorldVertices (Slot slot, boolean premultipliedAlpha) {
-		Skeleton skeleton = slot.getSkeleton();
-		Color skeletonColor = skeleton.getColor();
-		Color meshColor = slot.getColor();
-		Color regionColor = color;
-		float a = skeletonColor.a * meshColor.a * regionColor.a * 255;
-		float multiplier = premultipliedAlpha ? a : 255;
-		float color = NumberUtils.intToFloatColor( //
-			((int)a << 24) //
-				| ((int)(skeletonColor.b * meshColor.b * regionColor.b * multiplier) << 16) //
-				| ((int)(skeletonColor.g * meshColor.g * regionColor.g * multiplier) << 8) //
-				| (int)(skeletonColor.r * meshColor.r * regionColor.r * multiplier));
-
-		float[] worldVertices = this.worldVertices;
-		float x = skeleton.getX(), y = skeleton.getY();
-		Object[] skeletonBones = skeleton.getBones().items;
-		float[] weights = this.weights;
-		int[] bones = this.bones;
-
-		FloatArray ffdArray = slot.getAttachmentVertices();
-		if (ffdArray.size == 0) {
-			for (int w = 0, v = 0, b = 0, n = bones.length; v < n; w += 5) {
-				float wx = 0, wy = 0;
-				int nn = bones[v++] + v;
-				for (; v < nn; v++, b += 3) {
-					Bone bone = (Bone)skeletonBones[bones[v]];
-					float vx = weights[b], vy = weights[b + 1], weight = weights[b + 2];
-					wx += (vx * bone.getA() + vy * bone.getB() + bone.getWorldX()) * weight;
-					wy += (vx * bone.getC() + vy * bone.getD() + bone.getWorldY()) * weight;
-				}
-				worldVertices[w] = wx + x;
-				worldVertices[w + 1] = wy + y;
-				worldVertices[w + 2] = color;
-			}
-		} else {
-			float[] ffd = ffdArray.items;
-			for (int w = 0, v = 0, b = 0, f = 0, n = bones.length; v < n; w += 5) {
-				float wx = 0, wy = 0;
-				int nn = bones[v++] + v;
-				for (; v < nn; v++, b += 3, f += 2) {
-					Bone bone = (Bone)skeletonBones[bones[v]];
-					float vx = weights[b] + ffd[f], vy = weights[b + 1] + ffd[f + 1], weight = weights[b + 2];
-					wx += (vx * bone.getA() + vy * bone.getB() + bone.getWorldX()) * weight;
-					wy += (vx * bone.getC() + vy * bone.getD() + bone.getWorldY()) * weight;
-				}
-				worldVertices[w] = wx + x;
-				worldVertices[w + 1] = wy + y;
-				worldVertices[w + 2] = color;
-			}
-		}
-		return worldVertices;
-	}
-
-	public boolean applyFFD (Attachment sourceAttachment) {
-		return this == sourceAttachment || (inheritFFD && parentMesh == sourceAttachment);
-	}
-
-	public float[] getWorldVertices () {
-		return worldVertices;
-	}
-
-	public int[] getBones () {
-		return bones;
-	}
-
-	/** For each vertex, the number of bones affecting the vertex followed by that many bone indices. Ie: count, boneIndex, ... */
-	public void setBones (int[] bones) {
-		this.bones = bones;
-	}
-
-	public float[] getWeights () {
-		return weights;
-	}
-
-	/** For each bone affecting the vertex, the vertex position in the bone's coordinate system and the weight for the bone's
-	 * influence. Ie: x, y, weight, ... */
-	public void setWeights (float[] weights) {
-		this.weights = weights;
-	}
-
-	public short[] getTriangles () {
-		return triangles;
-	}
-
-	/** Vertex number triplets which describe the mesh's triangulation. */
-	public void setTriangles (short[] triangles) {
-		this.triangles = triangles;
-	}
-
-	public float[] getRegionUVs () {
-		return regionUVs;
-	}
-
-	/** For each vertex, a texure coordinate pair. Ie: u, v, ... */
-	public void setRegionUVs (float[] regionUVs) {
-		this.regionUVs = regionUVs;
-	}
-
-	public Color getColor () {
-		return color;
-	}
-
-	public String getPath () {
-		return path;
-	}
-
-	public void setPath (String path) {
-		this.path = path;
-	}
-
-	public int getHullLength () {
-		return hullLength;
-	}
-
-	public void setHullLength (int hullLength) {
-		this.hullLength = hullLength;
-	}
-
-	public void setEdges (short[] edges) {
-		this.edges = edges;
-	}
-
-	public short[] getEdges () {
-		return edges;
-	}
-
-	public float getWidth () {
-		return width;
-	}
-
-	public void setWidth (float width) {
-		this.width = width;
-	}
-
-	public float getHeight () {
-		return height;
-	}
-
-	public void setHeight (float height) {
-		this.height = height;
-	}
-
-	/** Returns the source mesh if this is a linked mesh, else returns null. */
-	public WeightedMeshAttachment getParentMesh () {
-		return parentMesh;
-	}
-
-	/** @param parentMesh May be null. */
-	public void setParentMesh (WeightedMeshAttachment parentMesh) {
-		this.parentMesh = parentMesh;
-		if (parentMesh != null) {
-			bones = parentMesh.bones;
-			weights = parentMesh.weights;
-			regionUVs = parentMesh.regionUVs;
-			triangles = parentMesh.triangles;
-			hullLength = parentMesh.hullLength;
-			edges = parentMesh.edges;
-			width = parentMesh.width;
-			height = parentMesh.height;
-		}
-	}
-
-	public boolean getInheritFFD () {
-		return inheritFFD;
-	}
-
-	public void setInheritFFD (boolean inheritFFD) {
-		this.inheritFFD = inheritFFD;
-	}
-}

+ 14 - 11
spine-libgdx/spine-skeletonviewer/src/com/esotericsoftware/spine/SkeletonViewer.java

@@ -246,6 +246,7 @@ public class SkeletonViewer extends ApplicationAdapter {
 			debugRenderer.setBoundingBoxes(ui.debugBoundingBoxesCheckbox.isChecked());
 			debugRenderer.setMeshHull(ui.debugMeshHullCheckbox.isChecked());
 			debugRenderer.setMeshTriangles(ui.debugMeshTrianglesCheckbox.isChecked());
+			debugRenderer.setPaths(ui.debugPathsCheckbox.isChecked());
 			debugRenderer.draw(skeleton);
 		}
 
@@ -285,19 +286,20 @@ public class SkeletonViewer extends ApplicationAdapter {
 		TextButton openButton = new TextButton("Open", skin);
 		List<String> animationList = new List(skin);
 		List<String> skinList = new List(skin);
-		CheckBox loopCheckbox = new CheckBox(" Loop", skin);
-		CheckBox premultipliedCheckbox = new CheckBox(" Premultiplied", skin);
+		CheckBox loopCheckbox = new CheckBox("Loop", skin);
+		CheckBox premultipliedCheckbox = new CheckBox("Premultiplied", skin);
 		Slider mixSlider = new Slider(0f, 2, 0.01f, false, skin);
 		Label mixLabel = new Label("0.3", skin);
 		Slider speedSlider = new Slider(0.1f, 3, 0.01f, false, skin);
 		Label speedLabel = new Label("1.0", skin);
-		CheckBox flipXCheckbox = new CheckBox(" X", skin);
-		CheckBox flipYCheckbox = new CheckBox(" Y", skin);
-		CheckBox debugBonesCheckbox = new CheckBox(" Bones", skin);
-		CheckBox debugRegionsCheckbox = new CheckBox(" Regions", skin);
-		CheckBox debugBoundingBoxesCheckbox = new CheckBox(" Bounds", skin);
-		CheckBox debugMeshHullCheckbox = new CheckBox(" Mesh Hull", skin);
-		CheckBox debugMeshTrianglesCheckbox = new CheckBox(" Mesh Triangles", skin);
+		CheckBox flipXCheckbox = new CheckBox("X", skin);
+		CheckBox flipYCheckbox = new CheckBox("Y", skin);
+		CheckBox debugBonesCheckbox = new CheckBox("Bones", skin);
+		CheckBox debugRegionsCheckbox = new CheckBox("Regions", skin);
+		CheckBox debugBoundingBoxesCheckbox = new CheckBox("Bounds", skin);
+		CheckBox debugMeshHullCheckbox = new CheckBox("Mesh hull", skin);
+		CheckBox debugMeshTrianglesCheckbox = new CheckBox("Triangles", skin);
+		CheckBox debugPathsCheckbox = new CheckBox("Paths", skin);
 		Slider scaleSlider = new Slider(0.1f, 3, 0.01f, false, skin);
 		Label scaleLabel = new Label("1.0", skin);
 		TextButton pauseButton = new TextButton("Pause", skin, "toggle");
@@ -330,6 +332,7 @@ public class SkeletonViewer extends ApplicationAdapter {
 			window.setX(-3);
 			window.setY(-2);
 
+			window.getTitleLabel().setColor(new Color(0.76f, 1, 1, 1));
 			window.getTitleTable().add(openButton).space(3);
 			window.getTitleTable().add(minimizeButton).width(20);
 
@@ -356,12 +359,12 @@ public class SkeletonViewer extends ApplicationAdapter {
 			root.add("Debug:");
 			root.add(table(debugBonesCheckbox, debugRegionsCheckbox, debugBoundingBoxesCheckbox)).row();
 			root.add();
-			root.add(table(debugMeshHullCheckbox, debugMeshTrianglesCheckbox)).row();
+			root.add(table(debugMeshHullCheckbox, debugMeshTrianglesCheckbox, debugPathsCheckbox)).row();
 			root.add("Alpha:");
 			root.add(premultipliedCheckbox).row();
 			root.add("Skin:");
 			root.add(skinScroll).expand().fill().minHeight(75).row();
-			root.add("Setup Pose:");
+			root.add("Setup pose:");
 			root.add(table(bonesSetupPoseButton, slotsSetupPoseButton, setupPoseButton)).row();
 			root.add("Animation:");
 			root.add(animationScroll).expand().fill().minHeight(75).row();