Преглед на файлове

Merge branch '4.1-beta' of https://github.com/esotericsoftware/spine-runtimes into 4.1-beta

Mario Zechner преди 3 години
родител
ревизия
f5c5b09a2e
променени са 29 файла, в които са добавени 671 реда и са изтрити 527 реда
  1. 4 8
      spine-libgdx/spine-libgdx-tests/src/com/esotericsoftware/spine/AnimationStateTests.java
  2. 4 7
      spine-libgdx/spine-libgdx-tests/src/com/esotericsoftware/spine/BonePlotting.java
  3. 3 1
      spine-libgdx/spine-libgdx-tests/src/com/esotericsoftware/spine/Box2DExample.java
  4. 0 1
      spine-libgdx/spine-libgdx-tests/src/com/esotericsoftware/spine/MixTest.java
  5. 0 1
      spine-libgdx/spine-libgdx-tests/src/com/esotericsoftware/spine/NormalMapTest.java
  6. 0 1
      spine-libgdx/spine-libgdx-tests/src/com/esotericsoftware/spine/VertexEffectTest.java
  7. 95 5
      spine-libgdx/spine-libgdx/src/com/esotericsoftware/spine/Animation.java
  8. 8 4
      spine-libgdx/spine-libgdx/src/com/esotericsoftware/spine/AnimationState.java
  9. 2 21
      spine-libgdx/spine-libgdx/src/com/esotericsoftware/spine/Skeleton.java
  10. 87 73
      spine-libgdx/spine-libgdx/src/com/esotericsoftware/spine/SkeletonBinary.java
  11. 89 74
      spine-libgdx/spine-libgdx/src/com/esotericsoftware/spine/SkeletonJson.java
  12. 3 3
      spine-libgdx/spine-libgdx/src/com/esotericsoftware/spine/SkeletonRenderer.java
  13. 1 1
      spine-libgdx/spine-libgdx/src/com/esotericsoftware/spine/SkeletonRendererDebug.java
  14. 16 14
      spine-libgdx/spine-libgdx/src/com/esotericsoftware/spine/Slot.java
  15. 28 19
      spine-libgdx/spine-libgdx/src/com/esotericsoftware/spine/attachments/AtlasAttachmentLoader.java
  16. 6 1
      spine-libgdx/spine-libgdx/src/com/esotericsoftware/spine/attachments/Attachment.java
  17. 2 5
      spine-libgdx/spine-libgdx/src/com/esotericsoftware/spine/attachments/AttachmentLoader.java
  18. 8 5
      spine-libgdx/spine-libgdx/src/com/esotericsoftware/spine/attachments/BoundingBoxAttachment.java
  19. 9 6
      spine-libgdx/spine-libgdx/src/com/esotericsoftware/spine/attachments/ClippingAttachment.java
  20. 11 6
      spine-libgdx/spine-libgdx/src/com/esotericsoftware/spine/attachments/HasTextureRegion.java
  21. 56 33
      spine-libgdx/spine-libgdx/src/com/esotericsoftware/spine/attachments/MeshAttachment.java
  22. 14 9
      spine-libgdx/spine-libgdx/src/com/esotericsoftware/spine/attachments/PathAttachment.java
  23. 11 7
      spine-libgdx/spine-libgdx/src/com/esotericsoftware/spine/attachments/PointAttachment.java
  24. 51 34
      spine-libgdx/spine-libgdx/src/com/esotericsoftware/spine/attachments/RegionAttachment.java
  25. 122 0
      spine-libgdx/spine-libgdx/src/com/esotericsoftware/spine/attachments/Sequence.java
  26. 0 151
      spine-libgdx/spine-libgdx/src/com/esotericsoftware/spine/attachments/SequenceAttachment.java
  27. 9 6
      spine-libgdx/spine-libgdx/src/com/esotericsoftware/spine/attachments/SkeletonAttachment.java
  28. 32 30
      spine-libgdx/spine-libgdx/src/com/esotericsoftware/spine/attachments/VertexAttachment.java
  29. 0 1
      spine-libgdx/spine-skeletonviewer/src/com/esotericsoftware/spine/SkeletonViewer.java

+ 4 - 8
spine-libgdx/spine-libgdx-tests/src/com/esotericsoftware/spine/AnimationStateTests.java

@@ -36,6 +36,7 @@ import com.badlogic.gdx.Files.FileType;
 import com.badlogic.gdx.backends.lwjgl.LwjglFileHandle;
 import com.badlogic.gdx.math.MathUtils;
 import com.badlogic.gdx.utils.Array;
+import com.badlogic.gdx.utils.Null;
 import com.badlogic.gdx.utils.Pool;
 
 import com.esotericsoftware.spine.AnimationState.AnimationStateListener;
@@ -47,19 +48,15 @@ import com.esotericsoftware.spine.attachments.MeshAttachment;
 import com.esotericsoftware.spine.attachments.PathAttachment;
 import com.esotericsoftware.spine.attachments.PointAttachment;
 import com.esotericsoftware.spine.attachments.RegionAttachment;
-import com.esotericsoftware.spine.attachments.SequenceAttachment;
+import com.esotericsoftware.spine.attachments.Sequence;
 
 public class AnimationStateTests {
 	final SkeletonJson json = new SkeletonJson(new AttachmentLoader() {
-		public RegionAttachment newRegionAttachment (Skin skin, String name, String path) {
+		public RegionAttachment newRegionAttachment (Skin skin, String name, String path, @Null Sequence sequence) {
 			return null;
 		}
 
-		public MeshAttachment newMeshAttachment (Skin skin, String name, String path) {
-			return null;
-		}
-
-		public SequenceAttachment newSequenceAttachment (Skin skin, String name, String path, int frameCount) {
+		public MeshAttachment newMeshAttachment (Skin skin, String name, String path, @Null Sequence sequence) {
 			return null;
 		}
 
@@ -893,7 +890,6 @@ public class AnimationStateTests {
 		state.apply(skeleton);
 		while (time < endTime) {
 			time += incr;
-			skeleton.update(incr);
 			state.update(incr);
 
 			// Reduce float discrepancies for tests.

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

@@ -30,6 +30,7 @@
 package com.esotericsoftware.spine;
 
 import com.badlogic.gdx.files.FileHandle;
+import com.badlogic.gdx.utils.Null;
 
 import com.esotericsoftware.spine.Animation.MixBlend;
 import com.esotericsoftware.spine.Animation.MixDirection;
@@ -40,21 +41,17 @@ import com.esotericsoftware.spine.attachments.MeshAttachment;
 import com.esotericsoftware.spine.attachments.PathAttachment;
 import com.esotericsoftware.spine.attachments.PointAttachment;
 import com.esotericsoftware.spine.attachments.RegionAttachment;
-import com.esotericsoftware.spine.attachments.SequenceAttachment;
+import com.esotericsoftware.spine.attachments.Sequence;
 
 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 RegionAttachment newRegionAttachment (Skin skin, String name, String path) {
+			public RegionAttachment newRegionAttachment (Skin skin, String name, String path, @Null Sequence sequence) {
 				return null;
 			}
 
-			public MeshAttachment newMeshAttachment (Skin skin, String name, String path) {
-				return null;
-			}
-
-			public SequenceAttachment newSequenceAttachment (Skin skin, String name, String path, int frameCount) {
+			public MeshAttachment newMeshAttachment (Skin skin, String name, String path, @Null Sequence sequence) {
 				return null;
 			}
 

+ 3 - 1
spine-libgdx/spine-libgdx-tests/src/com/esotericsoftware/spine/Box2DExample.java

@@ -49,12 +49,14 @@ import com.badlogic.gdx.physics.box2d.FixtureDef;
 import com.badlogic.gdx.physics.box2d.PolygonShape;
 import com.badlogic.gdx.physics.box2d.World;
 import com.badlogic.gdx.utils.Array;
+import com.badlogic.gdx.utils.Null;
 import com.badlogic.gdx.utils.ScreenUtils;
 
 import com.esotericsoftware.spine.Animation.MixBlend;
 import com.esotericsoftware.spine.Animation.MixDirection;
 import com.esotericsoftware.spine.attachments.AtlasAttachmentLoader;
 import com.esotericsoftware.spine.attachments.RegionAttachment;
+import com.esotericsoftware.spine.attachments.Sequence;
 
 public class Box2DExample extends ApplicationAdapter {
 	SpriteBatch batch;
@@ -85,7 +87,7 @@ public class Box2DExample extends ApplicationAdapter {
 		// This loader creates Box2dAttachments instead of RegionAttachments for an easy way to keep
 		// track of the Box2D body for each attachment.
 		AtlasAttachmentLoader atlasLoader = new AtlasAttachmentLoader(atlas) {
-			public RegionAttachment newRegionAttachment (Skin skin, String name, String path) {
+			public RegionAttachment newRegionAttachment (Skin skin, String name, String path, @Null Sequence sequence) {
 				Box2dAttachment attachment = new Box2dAttachment(name);
 				AtlasRegion region = atlas.findRegion(attachment.getName());
 				if (region == null) throw new RuntimeException("Region not found in atlas: " + attachment);

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

@@ -125,7 +125,6 @@ public class MixTest extends ApplicationAdapter {
 		}
 
 		skeleton.updateWorldTransform();
-		skeleton.update(Gdx.graphics.getDeltaTime());
 
 		batch.begin();
 		renderer.draw(batch, skeleton);

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

@@ -134,7 +134,6 @@ public class NormalMapTest extends ApplicationAdapter {
 		time += Gdx.graphics.getDeltaTime();
 		if (animation != null) animation.apply(skeleton, lastTime, time, true, null, 1, MixBlend.first, MixDirection.in);
 		skeleton.updateWorldTransform();
-		skeleton.update(Gdx.graphics.getDeltaTime());
 
 		lightPosition.x = Gdx.input.getX();
 		lightPosition.y = (Gdx.graphics.getHeight() - 1 - Gdx.input.getY());

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

@@ -85,7 +85,6 @@ public class VertexEffectTest extends ApplicationAdapter {
 	public void render () {
 		// Update the skeleton and animation time.
 		float delta = Gdx.graphics.getDeltaTime();
-		skeleton.update(delta);
 		state.update(delta);
 
 		swirlTime += delta;

+ 95 - 5
spine-libgdx/spine-libgdx/src/com/esotericsoftware/spine/Animation.java

@@ -40,6 +40,9 @@ import com.badlogic.gdx.utils.Null;
 import com.badlogic.gdx.utils.ObjectSet;
 
 import com.esotericsoftware.spine.attachments.Attachment;
+import com.esotericsoftware.spine.attachments.HasTextureRegion;
+import com.esotericsoftware.spine.attachments.Sequence;
+import com.esotericsoftware.spine.attachments.Sequence.SequenceMode;
 import com.esotericsoftware.spine.attachments.VertexAttachment;
 
 /** Stores a list of timelines to animate a skeleton's pose over time. */
@@ -175,7 +178,8 @@ public class Animation {
 		attachment, deform, //
 		event, drawOrder, //
 		ikConstraint, transformConstraint, //
-		pathConstraintPosition, pathConstraintSpacing, pathConstraintMix
+		pathConstraintPosition, pathConstraintSpacing, pathConstraintMix, //
+		sequence
 	}
 
 	/** The base class for all timelines. */
@@ -1646,7 +1650,7 @@ public class Animation {
 
 		/** The attachment that will be deformed.
 		 * <p>
-		 * See {@link VertexAttachment#getDeformAttachment()}. */
+		 * See {@link VertexAttachment#getTimelineAttachment()}. */
 		public VertexAttachment getAttachment () {
 			return attachment;
 		}
@@ -1724,9 +1728,9 @@ public class Animation {
 			if (!slot.bone.active) return;
 			Attachment slotAttachment = slot.attachment;
 			if (!(slotAttachment instanceof VertexAttachment)
-				|| ((VertexAttachment)slotAttachment).getDeformAttachment() != attachment) return;
+				|| ((VertexAttachment)slotAttachment).getTimelineAttachment() != attachment) return;
 
-			FloatArray deformArray = slot.getDeform();
+			FloatArray deformArray = slot.deform;
 			if (deformArray.size == 0) blend = setup;
 
 			float[][] vertices = this.vertices;
@@ -1734,7 +1738,6 @@ public class Animation {
 
 			float[] frames = this.frames;
 			if (time < frames[0]) { // Time is before first frame.
-				VertexAttachment vertexAttachment = (VertexAttachment)slotAttachment;
 				switch (blend) {
 				case setup:
 					deformArray.clear();
@@ -1745,6 +1748,7 @@ public class Animation {
 						return;
 					}
 					float[] deform = deformArray.setSize(vertexCount);
+					VertexAttachment vertexAttachment = (VertexAttachment)slotAttachment;
 					if (vertexAttachment.getBones() == null) {
 						// Unweighted vertex positions.
 						float[] setupVertices = vertexAttachment.getVertices();
@@ -2419,4 +2423,90 @@ public class Animation {
 			}
 		}
 	}
+
+	/** Changes a slot's {@link Slot#getSequenceIndex()} for an attachment's {@link Sequence}. */
+	static public class SequenceTimeline extends Timeline implements SlotTimeline {
+		static public final int ENTRIES = 3;
+		static private final int MODE = 1, DELAY = 2;
+
+		final int slotIndex;
+		final HasTextureRegion attachment;
+
+		public <T extends Attachment & HasTextureRegion> SequenceTimeline (int frameCount, int slotIndex, Attachment attachment) {
+			super(frameCount,
+				Property.sequence.ordinal() + "|" + slotIndex + "|" + ((HasTextureRegion)attachment).getSequence().getId());
+			this.slotIndex = slotIndex;
+			this.attachment = (HasTextureRegion)attachment;
+		}
+
+		public int getFrameEntries () {
+			return ENTRIES;
+		}
+
+		public int getSlotIndex () {
+			return slotIndex;
+		}
+
+		public Attachment getAttachment () {
+			return (Attachment)attachment;
+		}
+
+		/** Sets the time, mode, index, and frame time for the specified frame.
+		 * @param frame Between 0 and <code>frameCount</code>, inclusive.
+		 * @param time Seconds between frames. */
+		public void setFrame (int frame, float time, SequenceMode mode, int index, float delay) {
+			frame *= ENTRIES;
+			frames[frame] = time;
+			frames[frame + MODE] = mode.ordinal() | (index << 4);
+			frames[frame + DELAY] = delay;
+		}
+
+		public void apply (Skeleton skeleton, float lastTime, float time, @Null Array<Event> events, float alpha, MixBlend blend,
+			MixDirection direction) {
+
+			Slot slot = skeleton.slots.get(slotIndex);
+			if (!slot.bone.active) return;
+			Attachment slotAttachment = slot.attachment;
+			if (slotAttachment != attachment) {
+				if (!(slotAttachment instanceof VertexAttachment)
+					|| ((VertexAttachment)slotAttachment).getTimelineAttachment() != attachment) return;
+			}
+
+			float[] frames = this.frames;
+			if (time < frames[0]) { // Time is before first frame.
+				if (blend == setup || blend == first) slot.setSequenceIndex(-1);
+				return;
+			}
+
+			int i = search(frames, time, ENTRIES);
+			float before = frames[i];
+			int modeAndIndex = (int)frames[i + MODE];
+			float delay = frames[i + DELAY];
+
+			int index = modeAndIndex >> 4, count = attachment.getSequence().getRegions().length;
+			SequenceMode mode = SequenceMode.values[modeAndIndex & 0xf];
+			if (mode != SequenceMode.stop) {
+				index += (time - before) / delay + 0.00001f;
+				switch (mode) {
+				case once:
+					index = Math.min(count - 1, index);
+					break;
+				case loop:
+					index %= count;
+					break;
+				case pingpong:
+					int n = (count << 1) - 2;
+					index %= n;
+					if (index >= count) index = n - index;
+					break;
+				case onceReverse:
+					index = Math.max(count - 1 - index, 0);
+					break;
+				case loopReverse:
+					index = count - 1 - (index % count);
+				}
+			}
+			slot.setSequenceIndex(index);
+		}
+	}
 }

+ 8 - 4
spine-libgdx/spine-libgdx/src/com/esotericsoftware/spine/AnimationState.java

@@ -1012,16 +1012,20 @@ public class AnimationState {
 			nextAnimationLast = animationLast;
 		}
 
-		/** Uses {@link #getTrackTime()} to compute the <code>animationTime</code>, which is between {@link #getAnimationStart()}
-		 * and {@link #getAnimationEnd()}. When the <code>trackTime</code> is 0, the <code>animationTime</code> is equal to the
-		 * <code>animationStart</code> time. */
+		/** Uses {@link #getTrackTime()} to compute the <code>animationTime</code>. When the <code>trackTime</code> is 0, the
+		 * <code>animationTime</code> is equal to the <code>animationStart</code> time.
+		 * <p>
+		 * The <code>animationTime</code> is between {@link #getAnimationStart()} and {@link #getAnimationEnd()}, except if this
+		 * track entry is non-looping and {@link #getAnimationEnd()} is >= to the animation {@link Animation#duration}, then
+		 * <code>animationTime</code> continues to increase past {@link #getAnimationEnd()}. */
 		public float getAnimationTime () {
 			if (loop) {
 				float duration = animationEnd - animationStart;
 				if (duration == 0) return animationStart;
 				return (trackTime % duration) + animationStart;
 			}
-			return Math.min(trackTime + animationStart, animationEnd);
+			float animationTime = trackTime + animationStart;
+			return animationEnd >= animation.duration ? animationTime : Math.min(animationTime, animationEnd);
 		}
 
 		/** Multiplier for the delta time when this track entry is updated, causing time for this animation to pass slower or

+ 2 - 21
spine-libgdx/spine-libgdx/src/com/esotericsoftware/spine/Skeleton.java

@@ -42,7 +42,6 @@ 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.SequenceAttachment;
 
 /** Stores the current pose for a skeleton.
  * <p>
@@ -60,7 +59,6 @@ public class Skeleton {
 	final Array<Updatable> updateCache = new Array();
 	@Null Skin skin;
 	final Color color;
-	float time;
 	float scaleX = 1, scaleY = 1;
 	float x, y;
 
@@ -158,7 +156,6 @@ public class Skeleton {
 
 		skin = skeleton.skin;
 		color = new Color(skeleton.color);
-		time = skeleton.time;
 		scaleX = skeleton.scaleX;
 		scaleY = skeleton.scaleY;
 
@@ -722,11 +719,11 @@ public class Skeleton {
 			int verticesLength = 0;
 			float[] vertices = null;
 			Attachment attachment = slot.attachment;
-			if (attachment instanceof SequenceAttachment) attachment = ((SequenceAttachment)attachment).updateAttachment(slot);
 			if (attachment instanceof RegionAttachment) {
+				RegionAttachment region = (RegionAttachment)attachment;
 				verticesLength = 8;
 				vertices = temp.setSize(8);
-				((RegionAttachment)attachment).computeWorldVertices(slot.getBone(), vertices, 0, 2);
+				region.computeWorldVertices(slot, vertices, 0, 2);
 			} else if (attachment instanceof MeshAttachment) {
 				MeshAttachment mesh = (MeshAttachment)attachment;
 				verticesLength = mesh.getWorldVerticesLength();
@@ -812,22 +809,6 @@ public class Skeleton {
 		this.y = y;
 	}
 
-	/** Returns the skeleton's time. This can be used for tracking, such as with Slot {@link Slot#getAttachmentTime()}.
-	 * <p>
-	 * See {@link #update(float)}. */
-	public float getTime () {
-		return time;
-	}
-
-	public void setTime (float time) {
-		this.time = time;
-	}
-
-	/** Increments the skeleton's {@link #time}. */
-	public void update (float delta) {
-		time += delta;
-	}
-
 	public String toString () {
 		return data.name != null ? data.name : super.toString();
 	}

+ 87 - 73
spine-libgdx/spine-libgdx/src/com/esotericsoftware/spine/SkeletonBinary.java

@@ -63,6 +63,7 @@ import com.esotericsoftware.spine.Animation.RotateTimeline;
 import com.esotericsoftware.spine.Animation.ScaleTimeline;
 import com.esotericsoftware.spine.Animation.ScaleXTimeline;
 import com.esotericsoftware.spine.Animation.ScaleYTimeline;
+import com.esotericsoftware.spine.Animation.SequenceTimeline;
 import com.esotericsoftware.spine.Animation.ShearTimeline;
 import com.esotericsoftware.spine.Animation.ShearXTimeline;
 import com.esotericsoftware.spine.Animation.ShearYTimeline;
@@ -85,9 +86,8 @@ import com.esotericsoftware.spine.attachments.MeshAttachment;
 import com.esotericsoftware.spine.attachments.PathAttachment;
 import com.esotericsoftware.spine.attachments.PointAttachment;
 import com.esotericsoftware.spine.attachments.RegionAttachment;
-import com.esotericsoftware.spine.attachments.SequenceAttachment;
-import com.esotericsoftware.spine.attachments.SequenceAttachment.SequenceMode;
-import com.esotericsoftware.spine.attachments.HasTextureRegion;
+import com.esotericsoftware.spine.attachments.Sequence;
+import com.esotericsoftware.spine.attachments.Sequence.SequenceMode;
 import com.esotericsoftware.spine.attachments.VertexAttachment;
 
 /** Loads skeleton data in the Spine binary format.
@@ -114,6 +114,9 @@ public class SkeletonBinary extends SkeletonLoader {
 	static public final int SLOT_RGB2 = 4;
 	static public final int SLOT_ALPHA = 5;
 
+	static public final int ATTACHMENT_DEFORM = 0;
+	static public final int ATTACHMENT_SEQUENCE = 1;
+
 	static public final int PATH_POSITION = 0;
 	static public final int PATH_SPACING = 1;
 	static public final int PATH_MIX = 2;
@@ -303,9 +306,9 @@ public class SkeletonBinary extends SkeletonLoader {
 				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);
-				linkedMesh.mesh.setDeformAttachment(linkedMesh.inheritDeform ? (VertexAttachment)parent : linkedMesh.mesh);
+				linkedMesh.mesh.setTimelineAttachment(linkedMesh.inheritTimelines ? (VertexAttachment)parent : linkedMesh.mesh);
 				linkedMesh.mesh.setParentMesh((MeshAttachment)parent);
-				linkedMesh.mesh.updateRegion();
+				if (linkedMesh.mesh.getSequence() == null) linkedMesh.mesh.updateRegion();
 			}
 			linkedMeshes.clear();
 
@@ -398,9 +401,10 @@ public class SkeletonBinary extends SkeletonLoader {
 			float width = input.readFloat();
 			float height = input.readFloat();
 			int color = input.readInt();
+			Sequence sequence = readSequence(input);
 
 			if (path == null) path = name;
-			RegionAttachment region = attachmentLoader.newRegionAttachment(skin, name, path);
+			RegionAttachment region = attachmentLoader.newRegionAttachment(skin, name, path, sequence);
 			if (region == null) return null;
 			region.setPath(path);
 			region.setX(x * scale);
@@ -411,7 +415,8 @@ public class SkeletonBinary extends SkeletonLoader {
 			region.setWidth(width * scale);
 			region.setHeight(height * scale);
 			Color.rgba8888ToColor(region.getColor(), color);
-			region.updateRegion();
+			region.setSequence(sequence);
+			if (sequence == null) region.updateRegion();
 			return region;
 		}
 		case boundingbox: {
@@ -435,6 +440,7 @@ public class SkeletonBinary extends SkeletonLoader {
 			short[] triangles = readShortArray(input);
 			Vertices vertices = readVertices(input, vertexCount);
 			int hullLength = input.readInt(true);
+			Sequence sequence = readSequence(input);
 			short[] edges = null;
 			float width = 0, height = 0;
 			if (nonessential) {
@@ -444,7 +450,7 @@ public class SkeletonBinary extends SkeletonLoader {
 			}
 
 			if (path == null) path = name;
-			MeshAttachment mesh = attachmentLoader.newMeshAttachment(skin, name, path);
+			MeshAttachment mesh = attachmentLoader.newMeshAttachment(skin, name, path, sequence);
 			if (mesh == null) return null;
 			mesh.setPath(path);
 			Color.rgba8888ToColor(mesh.getColor(), color);
@@ -453,8 +459,9 @@ public class SkeletonBinary extends SkeletonLoader {
 			mesh.setWorldVerticesLength(vertexCount << 1);
 			mesh.setTriangles(triangles);
 			mesh.setRegionUVs(uvs);
-			mesh.updateRegion();
+			if (sequence == null) mesh.updateRegion();
 			mesh.setHullLength(hullLength << 1);
+			mesh.setSequence(sequence);
 			if (nonessential) {
 				mesh.setEdges(edges);
 				mesh.setWidth(width * scale);
@@ -467,7 +474,8 @@ public class SkeletonBinary extends SkeletonLoader {
 			int color = input.readInt();
 			String skinName = input.readStringRef();
 			String parent = input.readStringRef();
-			boolean inheritDeform = input.readBoolean();
+			boolean inheritTimelines = input.readBoolean();
+			Sequence sequence = readSequence(input);
 			float width = 0, height = 0;
 			if (nonessential) {
 				width = input.readFloat();
@@ -475,15 +483,16 @@ public class SkeletonBinary extends SkeletonLoader {
 			}
 
 			if (path == null) path = name;
-			MeshAttachment mesh = attachmentLoader.newMeshAttachment(skin, name, path);
+			MeshAttachment mesh = attachmentLoader.newMeshAttachment(skin, name, path, sequence);
 			if (mesh == null) return null;
 			mesh.setPath(path);
 			Color.rgba8888ToColor(mesh.getColor(), color);
+			mesh.setSequence(sequence);
 			if (nonessential) {
 				mesh.setWidth(width * scale);
 				mesh.setHeight(height * scale);
 			}
-			linkedMeshes.add(new LinkedMesh(mesh, skinName, slotIndex, parent, inheritDeform));
+			linkedMeshes.add(new LinkedMesh(mesh, skinName, slotIndex, parent, inheritTimelines));
 			return mesh;
 		}
 		case path: {
@@ -521,7 +530,7 @@ public class SkeletonBinary extends SkeletonLoader {
 			if (nonessential) Color.rgba8888ToColor(point.getColor(), color);
 			return point;
 		}
-		case clipping: {
+		case clipping:
 			int endSlotIndex = input.readInt(true);
 			int vertexCount = input.readInt(true);
 			Vertices vertices = readVertices(input, vertexCount);
@@ -536,28 +545,18 @@ public class SkeletonBinary extends SkeletonLoader {
 			if (nonessential) Color.rgba8888ToColor(clip.getColor(), color);
 			return clip;
 		}
-		case sequence:
-			Attachment attachment = readAttachment(input, skeletonData, skin, slotIndex, attachmentName, nonessential);
-			int frameCount = input.readInt(true);
-			float frameTime = input.readFloat();
-			SequenceMode mode = SequenceMode.values[input.readInt(true)];
-
-			if (attachment == null) return null;
-			String path = ((HasTextureRegion)attachment).getPath();
-
-			SequenceAttachment sequence = attachmentLoader.newSequenceAttachment(skin, name, path, frameCount);
-			if (sequence == null) return null;
-
-			sequence.setAttachment(attachment);
-			sequence.setPath(path);
-			sequence.setFrameCount(frameCount);
-			sequence.setFrameTime(frameTime);
-			sequence.setMode(mode);
-			return sequence;
-		}
 		return null;
 	}
 
+	private Sequence readSequence (SkeletonInput input) throws IOException {
+		if (!input.readBoolean()) return null;
+		Sequence sequence = new Sequence(input.readInt(true));
+		sequence.setStart(input.readInt(true));
+		sequence.setDigits(input.readInt(true));
+		sequence.setSetupIndex(input.readInt(true));
+		return sequence;
+	}
+
 	private Vertices readVertices (SkeletonInput input, int vertexCount) throws IOException {
 		float scale = this.scale;
 		int verticesLength = vertexCount << 1;
@@ -767,7 +766,6 @@ public class SkeletonBinary extends SkeletonLoader {
 						a = a2;
 					}
 					timelines.add(timeline);
-					break;
 				}
 			}
 		}
@@ -913,57 +911,73 @@ public class SkeletonBinary extends SkeletonLoader {
 			}
 		}
 
-		// Deform timelines.
+		// Attachment 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++) {
 					String attachmentName = input.readStringRef();
-					VertexAttachment attachment = (VertexAttachment)skin.getAttachment(slotIndex, attachmentName);
-					if (attachment == null) throw new SerializationException("Vertex attachment not found: " + attachmentName);
-					boolean weighted = attachment.getBones() != null;
-					float[] vertices = attachment.getVertices();
-					int deformLength = weighted ? (vertices.length / 3) << 1 : vertices.length;
-
-					int frameCount = input.readInt(true), frameLast = frameCount - 1;
-					DeformTimeline timeline = new DeformTimeline(frameCount, input.readInt(true), slotIndex, attachment);
-
-					float time = input.readFloat();
-					for (int frame = 0, bezier = 0;; frame++) {
-						float[] deform;
-						int end = input.readInt(true);
-						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++)
-									deform[v] = input.readFloat();
-							} else {
-								for (int v = start; v < end; v++)
-									deform[v] = input.readFloat() * scale;
+					Attachment attachment = skin.getAttachment(slotIndex, attachmentName);
+					if (attachment == null) throw new SerializationException("Timeline attachment not found: " + attachmentName);
+
+					int timelineType = input.readByte(), frameCount = input.readInt(true), frameLast = frameCount - 1;
+					switch (timelineType) {
+					case ATTACHMENT_DEFORM: {
+						VertexAttachment vertexAttachment = (VertexAttachment)attachment;
+						boolean weighted = vertexAttachment.getBones() != null;
+						float[] vertices = vertexAttachment.getVertices();
+						int deformLength = weighted ? (vertices.length / 3) << 1 : vertices.length;
+
+						DeformTimeline timeline = new DeformTimeline(frameCount, input.readInt(true), slotIndex, vertexAttachment);
+
+						float time = input.readFloat();
+						for (int frame = 0, bezier = 0;; frame++) {
+							float[] deform;
+							int end = input.readInt(true);
+							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++)
+										deform[v] = input.readFloat();
+								} else {
+									for (int v = start; v < end; v++)
+										deform[v] = input.readFloat() * scale;
+								}
+								if (!weighted) {
+									for (int v = 0, vn = deform.length; v < vn; v++)
+										deform[v] += vertices[v];
+								}
 							}
-							if (!weighted) {
-								for (int v = 0, vn = deform.length; v < vn; v++)
-									deform[v] += vertices[v];
+							timeline.setFrame(frame, time, deform);
+							if (frame == frameLast) break;
+							float time2 = input.readFloat();
+							switch (input.readByte()) {
+							case CURVE_STEPPED:
+								timeline.setStepped(frame);
+								break;
+							case CURVE_BEZIER:
+								setBezier(input, timeline, bezier++, frame, 0, time, time2, 0, 1, 1);
 							}
+							time = time2;
 						}
-						timeline.setFrame(frame, time, deform);
-						if (frame == frameLast) break;
-						float time2 = input.readFloat();
-						switch (input.readByte()) {
-						case CURVE_STEPPED:
-							timeline.setStepped(frame);
-							break;
-						case CURVE_BEZIER:
-							setBezier(input, timeline, bezier++, frame, 0, time, time2, 0, 1, 1);
+						timelines.add(timeline);
+						break;
+					}
+					case ATTACHMENT_SEQUENCE:
+						SequenceTimeline timeline = new SequenceTimeline(frameCount, slotIndex, attachment);
+						for (int frame = 0; frame < frameCount; frame++) {
+							float time = input.readFloat();
+							int modeAndIndex = input.readInt();
+							timeline.setFrame(frame, time, SequenceMode.values[modeAndIndex & 0xf], modeAndIndex >> 4,
+								input.readFloat());
 						}
-						time = time2;
+						timelines.add(timeline);
 					}
-					timelines.add(timeline);
 				}
 			}
 		}

+ 89 - 74
spine-libgdx/spine-libgdx/src/com/esotericsoftware/spine/SkeletonJson.java

@@ -41,6 +41,7 @@ import com.badlogic.gdx.utils.FloatArray;
 import com.badlogic.gdx.utils.IntArray;
 import com.badlogic.gdx.utils.JsonReader;
 import com.badlogic.gdx.utils.JsonValue;
+import com.badlogic.gdx.utils.Null;
 import com.badlogic.gdx.utils.SerializationException;
 
 import com.esotericsoftware.spine.Animation.AlphaTimeline;
@@ -63,6 +64,7 @@ import com.esotericsoftware.spine.Animation.RotateTimeline;
 import com.esotericsoftware.spine.Animation.ScaleTimeline;
 import com.esotericsoftware.spine.Animation.ScaleXTimeline;
 import com.esotericsoftware.spine.Animation.ScaleYTimeline;
+import com.esotericsoftware.spine.Animation.SequenceTimeline;
 import com.esotericsoftware.spine.Animation.ShearTimeline;
 import com.esotericsoftware.spine.Animation.ShearXTimeline;
 import com.esotericsoftware.spine.Animation.ShearYTimeline;
@@ -84,9 +86,8 @@ import com.esotericsoftware.spine.attachments.MeshAttachment;
 import com.esotericsoftware.spine.attachments.PathAttachment;
 import com.esotericsoftware.spine.attachments.PointAttachment;
 import com.esotericsoftware.spine.attachments.RegionAttachment;
-import com.esotericsoftware.spine.attachments.SequenceAttachment;
-import com.esotericsoftware.spine.attachments.SequenceAttachment.SequenceMode;
-import com.esotericsoftware.spine.attachments.HasTextureRegion;
+import com.esotericsoftware.spine.attachments.Sequence;
+import com.esotericsoftware.spine.attachments.Sequence.SequenceMode;
 import com.esotericsoftware.spine.attachments.VertexAttachment;
 
 /** Loads skeleton data in the Spine JSON format.
@@ -324,9 +325,9 @@ public class SkeletonJson extends SkeletonLoader {
 			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);
-			linkedMesh.mesh.setDeformAttachment(linkedMesh.inheritDeform ? (VertexAttachment)parent : linkedMesh.mesh);
+			linkedMesh.mesh.setTimelineAttachment(linkedMesh.inheritTimelines ? (VertexAttachment)parent : linkedMesh.mesh);
 			linkedMesh.mesh.setParentMesh((MeshAttachment)parent);
-			linkedMesh.mesh.updateRegion();
+			if (linkedMesh.mesh.getRegion() != null) linkedMesh.mesh.updateRegion();
 		}
 		linkedMeshes.clear();
 
@@ -369,7 +370,8 @@ public class SkeletonJson extends SkeletonLoader {
 		switch (AttachmentType.valueOf(map.getString("type", AttachmentType.region.name()))) {
 		case region: {
 			String path = map.getString("path", name);
-			RegionAttachment region = attachmentLoader.newRegionAttachment(skin, name, path);
+			Sequence sequence = readSequence(map.get("sequence"));
+			RegionAttachment region = attachmentLoader.newRegionAttachment(skin, name, path, sequence);
 			if (region == null) return null;
 			region.setPath(path);
 			region.setX(map.getFloat("x", 0) * scale);
@@ -379,11 +381,12 @@ public class SkeletonJson extends SkeletonLoader {
 			region.setRotation(map.getFloat("rotation", 0));
 			region.setWidth(map.getFloat("width") * scale);
 			region.setHeight(map.getFloat("height") * scale);
+			region.setSequence(sequence);
 
 			String color = map.getString("color", null);
 			if (color != null) Color.valueOf(color, region.getColor());
 
-			region.updateRegion();
+			if (region.getRegion() != null) region.updateRegion();
 			return region;
 		}
 		case boundingbox: {
@@ -398,7 +401,8 @@ public class SkeletonJson extends SkeletonLoader {
 		case mesh:
 		case linkedmesh: {
 			String path = map.getString("path", name);
-			MeshAttachment mesh = attachmentLoader.newMeshAttachment(skin, name, path);
+			Sequence sequence = readSequence(map.get("sequence"));
+			MeshAttachment mesh = attachmentLoader.newMeshAttachment(skin, name, path, sequence);
 			if (mesh == null) return null;
 			mesh.setPath(path);
 
@@ -407,11 +411,12 @@ public class SkeletonJson extends SkeletonLoader {
 
 			mesh.setWidth(map.getFloat("width", 0) * scale);
 			mesh.setHeight(map.getFloat("height", 0) * scale);
+			mesh.setSequence(sequence);
 
 			String parent = map.getString("parent", null);
 			if (parent != null) {
 				linkedMeshes
-					.add(new LinkedMesh(mesh, map.getString("skin", null), slotIndex, parent, map.getBoolean("deform", true)));
+					.add(new LinkedMesh(mesh, map.getString("skin", null), slotIndex, parent, map.getBoolean("timelines", true)));
 				return mesh;
 			}
 
@@ -419,7 +424,7 @@ public class SkeletonJson extends SkeletonLoader {
 			readVertices(map, mesh, uvs.length);
 			mesh.setTriangles(map.require("triangles").asShortArray());
 			mesh.setRegionUVs(uvs);
-			mesh.updateRegion();
+			if (mesh.getRegion() != null) mesh.updateRegion();
 
 			if (map.has("hull")) mesh.setHullLength(map.require("hull").asInt() << 1);
 			if (map.has("edges")) mesh.setEdges(map.require("edges").asShortArray());
@@ -455,7 +460,7 @@ public class SkeletonJson extends SkeletonLoader {
 			if (color != null) Color.valueOf(color, point.getColor());
 			return point;
 		}
-		case clipping: {
+		case clipping:
 			ClippingAttachment clip = attachmentLoader.newClippingAttachment(skin, name);
 			if (clip == null) return null;
 
@@ -472,23 +477,18 @@ public class SkeletonJson extends SkeletonLoader {
 			if (color != null) Color.valueOf(color, clip.getColor());
 			return clip;
 		}
-		case sequence:
-			Attachment attachment = readAttachment(map.getChild("attachment"), skin, slotIndex, name, skeletonData);
-			if (attachment == null) return null;
-			String path = ((HasTextureRegion)attachment).getPath();
-			int frameCount = map.getInt("count");
-			SequenceAttachment sequence = attachmentLoader.newSequenceAttachment(skin, name, path, frameCount);
-			if (sequence == null) return null;
-			sequence.setAttachment(attachment);
-			sequence.setPath(path);
-			sequence.setFrameCount(frameCount);
-			sequence.setFrameTime(map.getInt("time"));
-			sequence.setMode(SequenceMode.valueOf(map.getString("mode", SequenceMode.forward.name())));
-			return sequence;
-		}
 		return null;
 	}
 
+	private Sequence readSequence (@Null JsonValue map) {
+		if (map == null) return null;
+		Sequence sequence = new Sequence(map.getInt("count"));
+		sequence.setStart(map.getInt("start", 1));
+		sequence.setDigits(map.getInt("digits", 0));
+		sequence.setSetupIndex(map.getInt("setup", 0));
+		return sequence;
+	}
+
 	private void readVertices (JsonValue map, VertexAttachment attachment, int verticesLength) {
 		attachment.setWorldVerticesLength(verticesLength);
 		float[] vertices = map.require("vertices").asFloatArray();
@@ -533,7 +533,7 @@ public class SkeletonJson extends SkeletonLoader {
 				if (timelineName.equals("attachment")) {
 					AttachmentTimeline timeline = new AttachmentTimeline(frames, slot.index);
 					for (int frame = 0; keyMap != null; keyMap = keyMap.next, frame++)
-						timeline.setFrame(frame, keyMap.getFloat("time", 0), keyMap.getString("name"));
+						timeline.setFrame(frame, keyMap.getFloat("time", 0), keyMap.getString("name", null));
 					timelines.add(timeline);
 
 				} else if (timelineName.equals("rgba")) {
@@ -877,57 +877,72 @@ public class SkeletonJson extends SkeletonLoader {
 			}
 		}
 
-		// 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) {
+		// Attachment timelines.
+		for (JsonValue attachmentsMap = map.getChild("attachments"); attachmentsMap != null; attachmentsMap = attachmentsMap.next) {
+			Skin skin = skeletonData.findSkin(attachmentsMap.name);
+			if (skin == null) throw new SerializationException("Skin not found: " + attachmentsMap.name);
+			for (JsonValue slotMap = attachmentsMap.child; slotMap != null; slotMap = slotMap.next) {
 				SlotData slot = skeletonData.findSlot(slotMap.name);
 				if (slot == null) throw new SerializationException("Slot not found: " + slotMap.name);
-				for (JsonValue timelineMap = slotMap.child; timelineMap != null; timelineMap = timelineMap.next) {
-					JsonValue keyMap = timelineMap.child;
-					if (keyMap == null) continue;
-
-					VertexAttachment attachment = (VertexAttachment)skin.getAttachment(slot.index, 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) << 1 : vertices.length;
-
-					DeformTimeline timeline = new DeformTimeline(timelineMap.size, timelineMap.size, slot.index, attachment);
-					float time = keyMap.getFloat("time", 0);
-					for (int frame = 0, bezier = 0;; frame++) {
-						float[] deform;
-						JsonValue verticesValue = keyMap.get("vertices");
-						if (verticesValue == null)
-							deform = weighted ? new float[deformLength] : vertices;
-						else {
-							deform = new float[deformLength];
-							int start = keyMap.getInt("offset", 0);
-							arraycopy(verticesValue.asFloatArray(), 0, deform, start, verticesValue.size);
-							if (scale != 1) {
-								for (int i = start, n = i + verticesValue.size; i < n; i++)
-									deform[i] *= scale;
+				for (JsonValue attachmentMap = slotMap.child; attachmentMap != null; attachmentMap = attachmentMap.next) {
+					Attachment attachment = skin.getAttachment(slot.index, attachmentMap.name);
+					if (attachment == null) throw new SerializationException("Timeline attachment not found: " + attachmentMap.name);
+					for (JsonValue timelineMap = attachmentMap.child; timelineMap != null; timelineMap = timelineMap.next) {
+						JsonValue keyMap = timelineMap.child;
+						int frames = timelineMap.size;
+						String timelineName = timelineMap.name;
+						if (timelineName.equals("deform")) {
+							VertexAttachment vertexAttachment = (VertexAttachment)attachment;
+							boolean weighted = vertexAttachment.getBones() != null;
+							float[] vertices = vertexAttachment.getVertices();
+							int deformLength = weighted ? (vertices.length / 3) << 1 : vertices.length;
+
+							DeformTimeline timeline = new DeformTimeline(frames, frames, slot.index, vertexAttachment);
+							float time = keyMap.getFloat("time", 0);
+							for (int frame = 0, bezier = 0;; frame++) {
+								float[] deform;
+								JsonValue verticesValue = keyMap.get("vertices");
+								if (verticesValue == null)
+									deform = weighted ? new float[deformLength] : vertices;
+								else {
+									deform = new float[deformLength];
+									int start = keyMap.getInt("offset", 0);
+									arraycopy(verticesValue.asFloatArray(), 0, deform, start, verticesValue.size);
+									if (scale != 1) {
+										for (int i = start, n = i + verticesValue.size; i < n; i++)
+											deform[i] *= scale;
+									}
+									if (!weighted) {
+										for (int i = 0; i < deformLength; i++)
+											deform[i] += vertices[i];
+									}
+								}
+
+								timeline.setFrame(frame, time, deform);
+								JsonValue nextMap = keyMap.next;
+								if (nextMap == null) {
+									timeline.shrink(bezier);
+									break;
+								}
+								float time2 = nextMap.getFloat("time", 0);
+								JsonValue curve = keyMap.get("curve");
+								if (curve != null) bezier = readCurve(curve, timeline, bezier, frame, 0, time, time2, 0, 1, 1);
+								time = time2;
+								keyMap = nextMap;
 							}
-							if (!weighted) {
-								for (int i = 0; i < deformLength; i++)
-									deform[i] += vertices[i];
+							timelines.add(timeline);
+						} else if (timelineName.equals("sequence")) {
+							SequenceTimeline timeline = new SequenceTimeline(frames, slot.index, attachment);
+							float lastDelay = 0;
+							for (int frame = 0; keyMap != null; keyMap = keyMap.next, frame++) {
+								float delay = keyMap.getFloat("delay", lastDelay);
+								timeline.setFrame(frame, keyMap.getFloat("time", 0),
+									SequenceMode.valueOf(keyMap.getString("mode", "stop")), keyMap.getInt("index", 0), delay);
+								lastDelay = delay;
 							}
+							timelines.add(timeline);
 						}
-
-						timeline.setFrame(frame, time, deform);
-						JsonValue nextMap = keyMap.next;
-						if (nextMap == null) {
-							timeline.shrink(bezier);
-							break;
-						}
-						float time2 = nextMap.getFloat("time", 0);
-						JsonValue curve = keyMap.get("curve");
-						if (curve != null) bezier = readCurve(curve, timeline, bezier, frame, 0, time, time2, 0, 1, 1);
-						time = time2;
-						keyMap = nextMap;
 					}
-					timelines.add(timeline);
 				}
 			}
 		}
@@ -1068,14 +1083,14 @@ public class SkeletonJson extends SkeletonLoader {
 		String parent, skin;
 		int slotIndex;
 		MeshAttachment mesh;
-		boolean inheritDeform;
+		boolean inheritTimelines;
 
-		public LinkedMesh (MeshAttachment mesh, String skin, int slotIndex, String parent, boolean inheritDeform) {
+		public LinkedMesh (MeshAttachment mesh, String skin, int slotIndex, String parent, boolean inheritTimelines) {
 			this.mesh = mesh;
 			this.skin = skin;
 			this.slotIndex = slotIndex;
 			this.parent = parent;
-			this.inheritDeform = inheritDeform;
+			this.inheritTimelines = inheritTimelines;
 		}
 	}
 }

+ 3 - 3
spine-libgdx/spine-libgdx/src/com/esotericsoftware/spine/SkeletonRenderer.java

@@ -98,7 +98,7 @@ public class SkeletonRenderer {
 			Attachment attachment = slot.attachment;
 			if (attachment instanceof RegionAttachment) {
 				RegionAttachment region = (RegionAttachment)attachment;
-				region.computeWorldVertices(slot.getBone(), vertices, 0, 5);
+				region.computeWorldVertices(slot, vertices, 0, 5);
 				Color color = region.getColor(), slotColor = slot.getColor();
 				float alpha = a * slotColor.a * color.a * 255;
 				float multiplier = pmaColors ? alpha : 255;
@@ -183,7 +183,7 @@ public class SkeletonRenderer {
 				RegionAttachment region = (RegionAttachment)attachment;
 				verticesLength = vertexSize << 2;
 				vertices = this.vertices.items;
-				region.computeWorldVertices(slot.getBone(), vertices, 0, vertexSize);
+				region.computeWorldVertices(slot, vertices, 0, vertexSize);
 				triangles = quadTriangles;
 				texture = region.getRegion().getTexture();
 				uvs = region.getUVs();
@@ -309,7 +309,7 @@ public class SkeletonRenderer {
 				RegionAttachment region = (RegionAttachment)attachment;
 				verticesLength = vertexSize << 2;
 				vertices = this.vertices.items;
-				region.computeWorldVertices(slot.getBone(), vertices, 0, vertexSize);
+				region.computeWorldVertices(slot, vertices, 0, vertexSize);
 				triangles = quadTriangles;
 				texture = region.getRegion().getTexture();
 				uvs = region.getUVs();

+ 1 - 1
spine-libgdx/spine-libgdx/src/com/esotericsoftware/spine/SkeletonRendererDebug.java

@@ -127,7 +127,7 @@ public class SkeletonRendererDebug {
 				if (attachment instanceof RegionAttachment) {
 					RegionAttachment region = (RegionAttachment)attachment;
 					float[] vertices = this.vertices.items;
-					region.computeWorldVertices(slot.getBone(), vertices, 0, 2);
+					region.computeWorldVertices(slot, vertices, 0, 2);
 					shapes.line(vertices[0], vertices[1], vertices[2], vertices[3]);
 					shapes.line(vertices[2], vertices[3], vertices[4], vertices[5]);
 					shapes.line(vertices[4], vertices[5], vertices[6], vertices[7]);

+ 16 - 14
spine-libgdx/spine-libgdx/src/com/esotericsoftware/spine/Slot.java

@@ -35,6 +35,7 @@ import com.badlogic.gdx.utils.Null;
 
 import com.esotericsoftware.spine.Animation.DeformTimeline;
 import com.esotericsoftware.spine.attachments.Attachment;
+import com.esotericsoftware.spine.attachments.Sequence;
 import com.esotericsoftware.spine.attachments.VertexAttachment;
 
 /** Stores a slot's current pose. Slots organize attachments for {@link Skeleton#drawOrder} purposes and provide a place to store
@@ -46,8 +47,8 @@ public class Slot {
 	final Color color = new Color();
 	@Null final Color darkColor;
 	@Null Attachment attachment;
-	private float attachmentTime;
-	private FloatArray deform = new FloatArray();
+	int sequenceIndex;
+	FloatArray deform = new FloatArray();
 
 	int attachmentState;
 
@@ -69,7 +70,7 @@ public class Slot {
 		color.set(slot.color);
 		darkColor = slot.darkColor == null ? null : new Color(slot.darkColor);
 		attachment = slot.attachment;
-		attachmentTime = slot.attachmentTime;
+		sequenceIndex = slot.sequenceIndex;
 		deform.addAll(slot.deform);
 	}
 
@@ -105,27 +106,28 @@ public class Slot {
 		return attachment;
 	}
 
-	/** Sets the slot's attachment and, if the attachment changed, resets {@link #attachmentTime} and clears the {@link #deform}.
-	 * The deform is not cleared if the old attachment has the same {@link VertexAttachment#getDeformAttachment()} as the specified
-	 * attachment. */
+	/** Sets the slot's attachment and, if the attachment changed, resets {@link #sequenceIndex} and clears the {@link #deform}.
+	 * The deform is not cleared if the old attachment has the same {@link VertexAttachment#getTimelineAttachment()} as the
+	 * specified attachment. */
 	public void setAttachment (@Null Attachment attachment) {
 		if (this.attachment == attachment) return;
 		if (!(attachment instanceof VertexAttachment) || !(this.attachment instanceof VertexAttachment)
-			|| ((VertexAttachment)attachment).getDeformAttachment() != ((VertexAttachment)this.attachment).getDeformAttachment()) {
+			|| ((VertexAttachment)attachment).getTimelineAttachment() != ((VertexAttachment)this.attachment)
+				.getTimelineAttachment()) {
 			deform.clear();
 		}
 		this.attachment = attachment;
-		attachmentTime = bone.skeleton.time;
+		sequenceIndex = -1;
 	}
 
-	/** The time that has elapsed since the last time the attachment was set or cleared. Relies on Skeleton
-	 * {@link Skeleton#time}. */
-	public float getAttachmentTime () {
-		return bone.skeleton.time - attachmentTime;
+	/** The index of the texture region to display when the slot's attachment has a {@link Sequence}. -1 represents the
+	 * {@link Sequence#getSetupIndex()}. */
+	public int getSequenceIndex () {
+		return sequenceIndex;
 	}
 
-	public void setAttachmentTime (float time) {
-		attachmentTime = bone.skeleton.time - time;
+	public void setSequenceIndex (int sequenceIndex) {
+		this.sequenceIndex = sequenceIndex;
 	}
 
 	/** Values to deform the slot's attachment. For an unweighted mesh, the entries are local positions for each vertex. For a

+ 28 - 19
spine-libgdx/spine-libgdx/src/com/esotericsoftware/spine/attachments/AtlasAttachmentLoader.java

@@ -31,6 +31,8 @@ package com.esotericsoftware.spine.attachments;
 
 import com.badlogic.gdx.graphics.g2d.TextureAtlas;
 import com.badlogic.gdx.graphics.g2d.TextureAtlas.AtlasRegion;
+import com.badlogic.gdx.graphics.g2d.TextureRegion;
+import com.badlogic.gdx.utils.Null;
 
 import com.esotericsoftware.spine.Skin;
 
@@ -47,32 +49,39 @@ public class AtlasAttachmentLoader implements AttachmentLoader {
 		this.atlas = atlas;
 	}
 
-	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 + ")");
-		RegionAttachment attachment = new RegionAttachment(name);
-		attachment.setRegion(region);
-		return attachment;
+	private void loadSequence (String name, String basePath, Sequence sequence) {
+		TextureRegion[] regions = sequence.getRegions();
+		for (int i = 0, n = regions.length; i < n; i++) {
+			String path = sequence.getPath(basePath, i);
+			regions[i] = atlas.findRegion(path);
+			if (regions[i] == null) throw new RuntimeException("Region not found in atlas: " + path + " (sequence: " + name + ")");
+		}
 	}
 
-	public MeshAttachment newMeshAttachment (Skin skin, String name, String path) {
-		AtlasRegion region = atlas.findRegion(path);
-		if (region == null) throw new RuntimeException("Region not found in atlas: " + path + " (mesh attachment: " + name + ")");
-		MeshAttachment attachment = new MeshAttachment(name);
-		attachment.setRegion(region);
+	public RegionAttachment newRegionAttachment (Skin skin, String name, String path, @Null Sequence sequence) {
+		RegionAttachment attachment = new RegionAttachment(name);
+		if (sequence != null)
+			loadSequence(name, path, sequence);
+		else {
+			AtlasRegion region = atlas.findRegion(path);
+			if (region == null)
+				throw new RuntimeException("Region not found in atlas: " + path + " (region attachment: " + name + ")");
+			attachment.setRegion(region);
+		}
 		return attachment;
 	}
 
-	public SequenceAttachment newSequenceAttachment (Skin skin, String name, String path, int frameCount) {
-		AtlasRegion[] regions = new AtlasRegion[frameCount];
-		for (int i = 0; i < frameCount; i++) {
-			AtlasRegion region = atlas.findRegion(path + frameCount); // BOZO - Zero pad?
+	public MeshAttachment newMeshAttachment (Skin skin, String name, String path, @Null Sequence sequence) {
+		MeshAttachment attachment = new MeshAttachment(name);
+		if (sequence != null)
+			loadSequence(name, path, sequence);
+		else {
+			AtlasRegion region = atlas.findRegion(path);
 			if (region == null)
-				throw new RuntimeException("Region not found in atlas: " + path + frameCount + " (sequence: " + name + ")");
+				throw new RuntimeException("Region not found in atlas: " + path + " (mesh attachment: " + name + ")");
+			attachment.setRegion(region);
 		}
-		SequenceAttachment sequence = new SequenceAttachment(name);
-		sequence.setRegions(regions);
-		return sequence;
+		return attachment;
 	}
 
 	public BoundingBoxAttachment newBoundingBoxAttachment (Skin skin, String name) {

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

@@ -31,13 +31,18 @@ package com.esotericsoftware.spine.attachments;
 
 /** The base class for all attachments. */
 abstract public class Attachment {
-	String name;
+	final String name;
 
 	public Attachment (String name) {
 		if (name == null) throw new IllegalArgumentException("name cannot be null.");
 		this.name = name;
 	}
 
+	/** Copy constructor. */
+	protected Attachment (Attachment other) {
+		name = other.name;
+	}
+
 	/** The attachment's name. */
 	public String getName () {
 		return name;

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

@@ -39,13 +39,10 @@ import com.esotericsoftware.spine.Skin;
  * Runtimes Guide. */
 public interface AttachmentLoader {
 	/** @return May be null to not load the attachment. */
-	public @Null RegionAttachment newRegionAttachment (Skin skin, String name, String path);
+	public @Null RegionAttachment newRegionAttachment (Skin skin, String name, String path, @Null Sequence sequence);
 
 	/** @return May be null to not load the attachment. In that case null should also be returned for child meshes. */
-	public @Null MeshAttachment newMeshAttachment (Skin skin, String name, String path);
-
-	/** @return May be null to not load the attachment. */
-	public @Null SequenceAttachment newSequenceAttachment (Skin skin, String name, String path, int frameCount);
+	public @Null MeshAttachment newMeshAttachment (Skin skin, String name, String path, @Null Sequence sequence);
 
 	/** @return May be null to not load the attachment. */
 	public @Null BoundingBoxAttachment newBoundingBoxAttachment (Skin skin, String name);

+ 8 - 5
spine-libgdx/spine-libgdx/src/com/esotericsoftware/spine/attachments/BoundingBoxAttachment.java

@@ -46,16 +46,19 @@ public class BoundingBoxAttachment extends VertexAttachment {
 		super(name);
 	}
 
+	/** Copy constructor. */
+	protected BoundingBoxAttachment (BoundingBoxAttachment other) {
+		super(other);
+		color.set(other.color);
+	}
+
 	/** The color of the bounding box as it was in Spine, or a default color if nonessential data was not exported. Bounding boxes
 	 * are not usually rendered at runtime. */
 	public Color getColor () {
 		return color;
 	}
 
-	public Attachment copy () {
-		BoundingBoxAttachment copy = new BoundingBoxAttachment(name);
-		copyTo(copy);
-		copy.color.set(color);
-		return copy;
+	public BoundingBoxAttachment copy () {
+		return new BoundingBoxAttachment(name);
 	}
 }

+ 9 - 6
spine-libgdx/spine-libgdx/src/com/esotericsoftware/spine/attachments/ClippingAttachment.java

@@ -45,6 +45,13 @@ public class ClippingAttachment extends VertexAttachment {
 		super(name);
 	}
 
+	/** Copy constructor. */
+	protected ClippingAttachment (ClippingAttachment other) {
+		super(other);
+		endSlot = other.endSlot;
+		color.set(other.color);
+	}
+
 	/** Clipping is performed between the clipping attachment's slot and the end slot. If null clipping is done until the end of
 	 * the skeleton's rendering. */
 	public @Null SlotData getEndSlot () {
@@ -61,11 +68,7 @@ public class ClippingAttachment extends VertexAttachment {
 		return color;
 	}
 
-	public Attachment copy () {
-		ClippingAttachment copy = new ClippingAttachment(name);
-		copyTo(copy);
-		copy.endSlot = endSlot;
-		copy.color.set(color);
-		return copy;
+	public ClippingAttachment copy () {
+		return new ClippingAttachment(name);
 	}
 }

+ 11 - 6
spine-libgdx/spine-libgdx/src/com/esotericsoftware/spine/attachments/HasTextureRegion.java

@@ -3,6 +3,7 @@ package com.esotericsoftware.spine.attachments;
 
 import com.badlogic.gdx.graphics.Color;
 import com.badlogic.gdx.graphics.g2d.TextureRegion;
+import com.badlogic.gdx.utils.Null;
 
 public interface HasTextureRegion {
 	/** The name used to find the {@link #getRegion()}. */
@@ -10,16 +11,20 @@ public interface HasTextureRegion {
 
 	public void setPath (String path);
 
-	/** Sets the region used to draw the attachment. If the region or its properties are changed, {@link #updateRegion()} must be
-	 * called. */
-	public void setRegion (TextureRegion region);
-
 	public TextureRegion getRegion ();
 
-	/** Updates any values the attachment calculates using the {@link #getRegion()}. Must be called after changing the region or
-	 * the region's properties. */
+	/** Sets the region used to draw the attachment. After setting the region or if the region's properties are changed,
+	 * {@link #updateRegion()} must be called. */
+	public void setRegion (TextureRegion region);
+
+	/** Updates any values the attachment calculates using the {@link #getRegion()}. Must be called after setting the
+	 * {@link #getRegion()} or if the region's properties are changed. */
 	public void updateRegion ();
 
 	/** The color to tint the attachment. */
 	public Color getColor ();
+
+	public @Null Sequence getSequence ();
+
+	public void setSequence (@Null Sequence sequence);
 }

+ 56 - 33
spine-libgdx/spine-libgdx/src/com/esotericsoftware/spine/attachments/MeshAttachment.java

@@ -36,6 +36,8 @@ import com.badlogic.gdx.graphics.g2d.TextureAtlas.AtlasRegion;
 import com.badlogic.gdx.graphics.g2d.TextureRegion;
 import com.badlogic.gdx.utils.Null;
 
+import com.esotericsoftware.spine.Slot;
+
 /** An attachment that displays a textured mesh. A mesh has hull vertices and internal vertices within the hull. Holes are not
  * supported. Each vertex has UVs (texture coordinates) and triangles are used to map an image on to the mesh.
  * <p>
@@ -48,6 +50,7 @@ public class MeshAttachment extends VertexAttachment implements HasTextureRegion
 	private final Color color = new Color(1, 1, 1, 1);
 	private int hullLength;
 	private @Null MeshAttachment parentMesh;
+	private @Null Sequence sequence;
 
 	// Nonessential.
 	private @Null short[] edges;
@@ -57,18 +60,47 @@ public class MeshAttachment extends VertexAttachment implements HasTextureRegion
 		super(name);
 	}
 
+	/** Copy constructor. Use {@link #newLinkedMesh()} if the other mesh is a linked mesh. */
+	protected MeshAttachment (MeshAttachment other) {
+		super(other);
+
+		if (parentMesh != null) throw new IllegalArgumentException("Use newLinkedMesh to copy a linked mesh.");
+
+		region = other.region;
+		path = other.path;
+		color.set(other.color);
+
+		regionUVs = new float[other.regionUVs.length];
+		arraycopy(other.regionUVs, 0, regionUVs, 0, regionUVs.length);
+
+		uvs = new float[other.uvs.length];
+		arraycopy(other.uvs, 0, uvs, 0, uvs.length);
+
+		triangles = new short[other.triangles.length];
+		arraycopy(other.triangles, 0, triangles, 0, triangles.length);
+
+		hullLength = other.hullLength;
+
+		// Nonessential.
+		if (other.edges != null) {
+			edges = new short[other.edges.length];
+			arraycopy(other.edges, 0, edges, 0, edges.length);
+		}
+		width = other.width;
+		height = other.height;
+	}
+
 	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);
+	public @Null TextureRegion getRegion () {
 		return region;
 	}
 
-	/** Calculates {@link #uvs} using {@link #regionUVs} and the {@link #region}. Must be called after changing the region or the
-	 * region's properties. */
+	/** Calculates {@link #uvs} using the {@link #regionUVs} and {@link #region}. Must be called if the {@link #region},
+	 * {@link #regionUVs}, or the region's properties are changed. */
 	public void updateRegion () {
 		float[] regionUVs = this.regionUVs;
 		if (this.uvs == null || this.uvs.length != regionUVs.length) this.uvs = new float[regionUVs.length];
@@ -131,6 +163,12 @@ public class MeshAttachment extends VertexAttachment implements HasTextureRegion
 		}
 	}
 
+	/** If the attachment has a {@link #sequence}, the {@link #region} may be changed. */
+	public void computeWorldVertices (Slot slot, int start, int count, float[] worldVertices, int offset, int stride) {
+		if (sequence != null) sequence.apply(slot, this);
+		super.computeWorldVertices(slot, start, count, worldVertices, offset, stride);
+	}
+
 	/** Triplets of vertex indices which describe the mesh's triangulation. */
 	public short[] getTriangles () {
 		return triangles;
@@ -210,6 +248,14 @@ public class MeshAttachment extends VertexAttachment implements HasTextureRegion
 		this.height = height;
 	}
 
+	public @Null Sequence getSequence () {
+		return sequence;
+	}
+
+	public void setSequence (@Null Sequence sequence) {
+		this.sequence = sequence;
+	}
+
 	/** The parent mesh if this is a linked mesh, else null. A linked mesh shares the {@link #bones}, {@link #vertices},
 	 * {@link #regionUVs}, {@link #triangles}, {@link #hullLength}, {@link #edges}, {@link #width}, and {@link #height} with the
 	 * parent mesh, but may have a different {@link #name} or {@link #path} (and therefore a different texture). */
@@ -232,42 +278,19 @@ public class MeshAttachment extends VertexAttachment implements HasTextureRegion
 		}
 	}
 
-	public Attachment copy () {
-		if (parentMesh != null) return newLinkedMesh();
-
-		MeshAttachment copy = new MeshAttachment(name);
-		copy.region = region;
-		copy.path = path;
-		copy.color.set(color);
-
-		copyTo(copy);
-		copy.regionUVs = new float[regionUVs.length];
-		arraycopy(regionUVs, 0, copy.regionUVs, 0, regionUVs.length);
-		copy.uvs = new float[uvs.length];
-		arraycopy(uvs, 0, copy.uvs, 0, uvs.length);
-		copy.triangles = new short[triangles.length];
-		arraycopy(triangles, 0, copy.triangles, 0, triangles.length);
-		copy.hullLength = hullLength;
-
-		// Nonessential.
-		if (edges != null) {
-			copy.edges = new short[edges.length];
-			arraycopy(edges, 0, copy.edges, 0, edges.length);
-		}
-		copy.width = width;
-		copy.height = height;
-		return copy;
-	}
-
 	/** Returns a new mesh with the {@link #parentMesh} set to this mesh's parent mesh, if any, else to this mesh. */
 	public MeshAttachment newLinkedMesh () {
 		MeshAttachment mesh = new MeshAttachment(name);
+		mesh.timelineAttachment = timelineAttachment;
 		mesh.region = region;
 		mesh.path = path;
 		mesh.color.set(color);
-		mesh.deformAttachment = deformAttachment;
 		mesh.setParentMesh(parentMesh != null ? parentMesh : this);
-		mesh.updateRegion();
+		if (mesh.getRegion() != null) mesh.updateRegion();
 		return mesh;
 	}
+
+	public MeshAttachment copy () {
+		return parentMesh != null ? newLinkedMesh() : new MeshAttachment(this);
+	}
 }

+ 14 - 9
spine-libgdx/spine-libgdx/src/com/esotericsoftware/spine/attachments/PathAttachment.java

@@ -49,6 +49,18 @@ public class PathAttachment extends VertexAttachment {
 		super(name);
 	}
 
+	/** Copy constructor. */
+	protected PathAttachment (PathAttachment other) {
+		super(other);
+
+		lengths = new float[other.lengths.length];
+		arraycopy(other.lengths, 0, lengths, 0, lengths.length);
+
+		closed = other.closed;
+		constantSpeed = other.constantSpeed;
+		color.set(other.color);
+	}
+
 	/** If true, the start and end knots are connected. */
 	public boolean getClosed () {
 		return closed;
@@ -83,14 +95,7 @@ public class PathAttachment extends VertexAttachment {
 		return color;
 	}
 
-	public Attachment copy () {
-		PathAttachment copy = new PathAttachment(name);
-		copyTo(copy);
-		copy.lengths = new float[lengths.length];
-		arraycopy(lengths, 0, copy.lengths, 0, lengths.length);
-		copy.closed = closed;
-		copy.constantSpeed = constantSpeed;
-		copy.color.set(color);
-		return copy;
+	public PathAttachment copy () {
+		return new PathAttachment(this);
 	}
 }

+ 11 - 7
spine-libgdx/spine-libgdx/src/com/esotericsoftware/spine/attachments/PointAttachment.java

@@ -51,6 +51,15 @@ public class PointAttachment extends Attachment {
 		super(name);
 	}
 
+	/** Copy constructor. */
+	protected PointAttachment (PointAttachment other) {
+		super(other);
+		x = other.x;
+		y = other.y;
+		rotation = other.rotation;
+		color.set(other.color);
+	}
+
 	public float getX () {
 		return x;
 	}
@@ -94,12 +103,7 @@ public class PointAttachment extends Attachment {
 		return (float)Math.atan2(y, x) * radDeg;
 	}
 
-	public Attachment copy () {
-		PointAttachment copy = new PointAttachment(name);
-		copy.x = x;
-		copy.y = y;
-		copy.rotation = rotation;
-		copy.color.set(color);
-		return copy;
+	public PointAttachment copy () {
+		return new PointAttachment(this);
 	}
 }

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

@@ -34,21 +34,19 @@ import static com.esotericsoftware.spine.utils.SpineUtils.*;
 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.Null;
 
 import com.esotericsoftware.spine.Bone;
+import com.esotericsoftware.spine.Slot;
 
 /** An attachment that displays a textured quadrilateral.
  * <p>
  * See <a href="http://esotericsoftware.com/spine-regions">Region attachments</a> in the Spine User Guide. */
 public class RegionAttachment extends Attachment implements HasTextureRegion {
-	static public final int BLX = 0;
-	static public final int BLY = 1;
-	static public final int ULX = 2;
-	static public final int ULY = 3;
-	static public final int URX = 4;
-	static public final int URY = 5;
-	static public final int BRX = 6;
-	static public final int BRY = 7;
+	static public final int BLX = 0, BLY = 1;
+	static public final int ULX = 2, ULY = 3;
+	static public final int URX = 4, URY = 5;
+	static public final int BRX = 6, BRY = 7;
 
 	private TextureRegion region;
 	private String path;
@@ -56,13 +54,31 @@ public class RegionAttachment extends Attachment implements HasTextureRegion {
 	private final float[] uvs = new float[8];
 	private final float[] offset = new float[8];
 	private final Color color = new Color(1, 1, 1, 1);
+	private @Null Sequence sequence;
 
 	public RegionAttachment (String name) {
 		super(name);
 	}
 
-	/** Calculates the {@link #offset} using the {@link #region}. Must be called after changing the region or the region's
-	 * properties. */
+	/** Copy constructor. */
+	protected RegionAttachment (RegionAttachment other) {
+		super(other);
+		region = other.region;
+		path = other.path;
+		x = other.x;
+		y = other.y;
+		scaleX = other.scaleX;
+		scaleY = other.scaleY;
+		rotation = other.rotation;
+		width = other.width;
+		height = other.height;
+		arraycopy(other.uvs, 0, uvs, 0, 8);
+		arraycopy(other.offset, 0, offset, 0, 8);
+		color.set(other.color);
+	}
+
+	/** Calculates the {@link #offset} and {@link #uvs} using the {@link #region} and the attachment's transform. Must be called if
+	 * the {@link #region}, transform, or the region's properties are changed. */
 	public void updateRegion () {
 		float width = getWidth();
 		float height = getHeight();
@@ -70,11 +86,13 @@ public class RegionAttachment extends Attachment implements HasTextureRegion {
 		float localY2 = height / 2;
 		float localX = -localX2;
 		float localY = -localY2;
+		boolean rotated = false;
 		if (region instanceof AtlasRegion) {
 			AtlasRegion region = (AtlasRegion)this.region;
 			localX += region.offsetX / region.originalWidth * width;
 			localY += region.offsetY / region.originalHeight * height;
 			if (region.degrees == 90) {
+				rotated = true;
 				localX2 -= (region.originalWidth - region.offsetX - region.packedHeight) / region.originalWidth * width;
 				localY2 -= (region.originalHeight - region.offsetY - region.packedWidth) / region.originalHeight * height;
 			} else {
@@ -110,13 +128,9 @@ public class RegionAttachment extends Attachment implements HasTextureRegion {
 		offset[URY] = localY2Cos + localX2Sin;
 		offset[BRX] = localX2Cos - localYSin;
 		offset[BRY] = localYCos + localX2Sin;
-	}
 
-	public void setRegion (TextureRegion region) {
-		if (region == null) throw new IllegalArgumentException("region cannot be null.");
-		this.region = region;
 		float[] uvs = this.uvs;
-		if (region instanceof AtlasRegion && ((AtlasRegion)region).degrees == 90) {
+		if (rotated) {
 			uvs[URX] = region.getU();
 			uvs[URY] = region.getV2();
 			uvs[BRX] = region.getU();
@@ -137,20 +151,28 @@ public class RegionAttachment extends Attachment implements HasTextureRegion {
 		}
 	}
 
-	public TextureRegion getRegion () {
-		if (region == null) throw new IllegalStateException("Region has not been set: " + name);
+	public void setRegion (TextureRegion region) {
+		if (region == null) throw new IllegalArgumentException("region cannot be null.");
+		this.region = region;
+	}
+
+	public @Null TextureRegion getRegion () {
 		return region;
 	}
 
-	/** Transforms the attachment's four vertices to world coordinates.
+	/** Transforms the attachment's four vertices to world coordinates. If the attachment has a {@link #sequence}, the
+	 * {@link #region} may be changed.
 	 * <p>
 	 * See <a href="http://esotericsoftware.com/spine-runtime-skeletons#World-transforms">World transforms</a> in the Spine
 	 * Runtimes Guide.
 	 * @param worldVertices The output world vertices. Must have a length >= <code>offset</code> + 8.
 	 * @param offset The <code>worldVertices</code> index to begin writing values.
 	 * @param stride The number of <code>worldVertices</code> entries between the value pairs written. */
-	public void computeWorldVertices (Bone bone, float[] worldVertices, int offset, int stride) {
+	public void computeWorldVertices (Slot slot, float[] worldVertices, int offset, int stride) {
+		if (sequence != null) sequence.apply(slot, this);
+
 		float[] vertexOffset = this.offset;
+		Bone bone = slot.getBone();
 		float x = bone.getWorldX(), y = bone.getWorldY();
 		float a = bone.getA(), b = bone.getB(), c = bone.getC(), d = bone.getD();
 		float offsetX, offsetY;
@@ -265,20 +287,15 @@ public class RegionAttachment extends Attachment implements HasTextureRegion {
 		this.path = path;
 	}
 
-	public Attachment copy () {
-		RegionAttachment copy = new RegionAttachment(name);
-		copy.region = region;
-		copy.path = path;
-		copy.x = x;
-		copy.y = y;
-		copy.scaleX = scaleX;
-		copy.scaleY = scaleY;
-		copy.rotation = rotation;
-		copy.width = width;
-		copy.height = height;
-		arraycopy(uvs, 0, copy.uvs, 0, 8);
-		arraycopy(offset, 0, copy.offset, 0, 8);
-		copy.color.set(color);
-		return copy;
+	public @Null Sequence getSequence () {
+		return sequence;
+	}
+
+	public void setSequence (@Null Sequence sequence) {
+		this.sequence = sequence;
+	}
+
+	public RegionAttachment copy () {
+		return new RegionAttachment(this);
 	}
 }

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

@@ -0,0 +1,122 @@
+/******************************************************************************
+ * Spine Runtimes License Agreement
+ * Last updated September 24, 2021. Replaces all prior versions.
+ *
+ * Copyright (c) 2013-2021, Esoteric Software LLC
+ *
+ * Integration of the Spine Runtimes into software or otherwise creating
+ * derivative works of the Spine Runtimes is permitted under the terms and
+ * conditions of Section 2 of the Spine Editor License Agreement:
+ * http://esotericsoftware.com/spine-editor-license
+ *
+ * Otherwise, it is permitted to integrate the Spine Runtimes into software
+ * or otherwise create derivative works of the Spine Runtimes (collectively,
+ * "Products"), provided that each user of the Products must obtain their own
+ * Spine Editor license and redistribution of the Products in any form must
+ * include this license and copyright notice.
+ *
+ * THE SPINE RUNTIMES ARE PROVIDED BY ESOTERIC SOFTWARE LLC "AS IS" AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL ESOTERIC SOFTWARE LLC BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES,
+ * BUSINESS INTERRUPTION, OR LOSS OF USE, DATA, OR PROFITS) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
+ * THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ *****************************************************************************/
+
+package com.esotericsoftware.spine.attachments;
+
+import static com.esotericsoftware.spine.utils.SpineUtils.*;
+
+import com.badlogic.gdx.graphics.g2d.TextureRegion;
+
+import com.esotericsoftware.spine.Slot;
+
+public class Sequence {
+	static private int nextID;
+
+	private final int id = nextID();
+	private final TextureRegion[] regions;
+	private int start, digits, setupIndex;
+
+	public Sequence (int count) {
+		regions = new TextureRegion[count];
+	}
+
+	/** Copy constructor. */
+	protected Sequence (Sequence other) {
+		regions = new TextureRegion[other.regions.length];
+		arraycopy(other.regions, 0, regions, 0, regions.length);
+
+		start = other.start;
+		digits = other.digits;
+		setupIndex = other.setupIndex;
+	}
+
+	public <T extends Attachment & HasTextureRegion> void apply (Slot slot, T attachment) {
+		int index = slot.getSequenceIndex();
+		if (index == -1) index = setupIndex;
+		if (index >= regions.length) index = regions.length - 1;
+		TextureRegion region = regions[index];
+		if (attachment.getRegion() != region) {
+			attachment.setRegion(region);
+			attachment.updateRegion();
+		}
+	}
+
+	public String getPath (String basePath, int index) {
+		StringBuilder buffer = new StringBuilder(basePath.length() + digits);
+		buffer.append(basePath);
+		buffer.append(start + index);
+		while (buffer.length() < digits)
+			buffer.append('0');
+		return buffer.toString();
+	}
+
+	public int getStart () {
+		return start;
+	}
+
+	public void setStart (int start) {
+		this.start = start;
+	}
+
+	public int getDigits () {
+		return digits;
+	}
+
+	public void setDigits (int digits) {
+		this.digits = digits;
+	}
+
+	/** The index of the region to show for the setup pose. */
+	public int getSetupIndex () {
+		return setupIndex;
+	}
+
+	public void setSetupIndex (int index) {
+		this.setupIndex = index;
+	}
+
+	public TextureRegion[] getRegions () {
+		return regions;
+	}
+
+	/** Returns a unique ID for this attachment. */
+	public int getId () {
+		return id;
+	}
+
+	static private synchronized int nextID () {
+		return nextID++;
+	}
+
+	static public enum SequenceMode {
+		stop, once, loop, pingpong, onceReverse, loopReverse, pingpongReverse;
+
+		static public final SequenceMode[] values = values();
+	}
+}

+ 0 - 151
spine-libgdx/spine-libgdx/src/com/esotericsoftware/spine/attachments/SequenceAttachment.java

@@ -1,151 +0,0 @@
-/******************************************************************************
- * Spine Runtimes License Agreement
- * Last updated September 24, 2021. Replaces all prior versions.
- *
- * Copyright (c) 2013-2021, Esoteric Software LLC
- *
- * Integration of the Spine Runtimes into software or otherwise creating
- * derivative works of the Spine Runtimes is permitted under the terms and
- * conditions of Section 2 of the Spine Editor License Agreement:
- * http://esotericsoftware.com/spine-editor-license
- *
- * Otherwise, it is permitted to integrate the Spine Runtimes into software
- * or otherwise create derivative works of the Spine Runtimes (collectively,
- * "Products"), provided that each user of the Products must obtain their own
- * Spine Editor license and redistribution of the Products in any form must
- * include this license and copyright notice.
- *
- * THE SPINE RUNTIMES ARE PROVIDED BY ESOTERIC SOFTWARE LLC "AS IS" AND ANY
- * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
- * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
- * DISCLAIMED. IN NO EVENT SHALL ESOTERIC SOFTWARE LLC BE LIABLE FOR ANY
- * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
- * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES,
- * BUSINESS INTERRUPTION, OR LOSS OF USE, DATA, OR PROFITS) HOWEVER CAUSED AND
- * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
- * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
- * THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
- *****************************************************************************/
-
-package com.esotericsoftware.spine.attachments;
-
-import com.badlogic.gdx.graphics.g2d.TextureRegion;
-import com.badlogic.gdx.math.MathUtils;
-
-import com.esotericsoftware.spine.Slot;
-
-/** An attachment that applies a sequence of texture atlas regions to a region or mesh attachment.
- * <p>
- * See <a href="http://esotericsoftware.com/spine-sequences">Sequence attachments</a> in the Spine User Guide. */
-public class SequenceAttachment<T extends Attachment & HasTextureRegion> extends Attachment {
-	private T attachment;
-	private String path;
-	private int frameCount;
-	private float frameTime;
-	private SequenceMode mode;
-	private TextureRegion[] regions;
-
-	public SequenceAttachment (String name) {
-		super(name);
-	}
-
-	/** Updates the {@link #attachment} with the {@link #regions region} for the slot's {@link Slot#getAttachmentTime()} and
-	 * returns it. */
-	public T updateAttachment (Slot slot) {
-		int index = (int)(slot.getAttachmentTime() / frameTime);
-		switch (mode) {
-		case forward:
-			index = Math.min(frameCount - 1, index);
-			break;
-		case backward:
-			index = Math.max(frameCount - index - 1, 0);
-			break;
-		case forwardLoop:
-			index = index % frameCount;
-			break;
-		case backwardLoop:
-			index = frameCount - (index % frameCount) - 1;
-			break;
-		case pingPong:
-			index = index % (frameCount << 1);
-			if (index >= frameCount) index = frameCount - 1 - (index - frameCount);
-			break;
-		case random:
-			index = MathUtils.random(frameCount - 1);
-		}
-		attachment.setRegion(regions[index]);
-		attachment.updateRegion();
-		return attachment;
-	}
-
-	public void setAttachment (T attachment) {
-		this.attachment = attachment;
-	}
-
-	public T getAttachment () {
-		return attachment;
-	}
-
-	/** The prefix used to find the {@link #regions} for this attachment. */
-	public String getPath () {
-		return path;
-	}
-
-	public void setPath (String path) {
-		this.path = path;
-	}
-
-	public SequenceMode getMode () {
-		return mode;
-	}
-
-	public void setMode (SequenceMode mode) {
-		if (mode == null) throw new IllegalArgumentException("mode cannot be null.");
-		this.mode = mode;
-	}
-
-	public int getFrameCount () {
-		return frameCount;
-	}
-
-	public void setFrameCount (int frameCount) {
-		this.frameCount = frameCount;
-	}
-
-	/** The time in seconds each frame is shown. */
-	public float getFrameTime () {
-		return frameTime;
-	}
-
-	public void setFrameTime (float frameTime) {
-		this.frameTime = frameTime;
-	}
-
-	public TextureRegion[] getRegions () {
-		if (regions == null) throw new IllegalStateException("Regions have not been set: " + name);
-		return regions;
-	}
-
-	public void setRegions (TextureRegion[] regions) {
-		if (regions == null) throw new IllegalArgumentException("regions cannot be null.");
-		this.regions = regions;
-	}
-
-	public Attachment copy () {
-		SequenceAttachment copy = new SequenceAttachment(name);
-		copy.attachment = attachment.copy();
-		copy.path = path;
-		copy.frameCount = frameCount;
-		copy.frameTime = frameTime;
-		copy.frameTime = frameTime;
-		copy.mode = mode;
-		copy.regions = regions;
-		return copy;
-	}
-
-	static public enum SequenceMode {
-		forward, backward, forwardLoop, backwardLoop, pingPong, random;
-
-		static public final SequenceMode[] values = values();
-	}
-}

+ 9 - 6
spine-libgdx/spine-libgdx/src/com/esotericsoftware/spine/attachments/SkeletonAttachment.java

@@ -41,8 +41,13 @@ public class SkeletonAttachment extends Attachment {
 		super(name);
 	}
 
-	/** @return May return null. */
-	public Skeleton getSkeleton () {
+	/** Copy constructor. */
+	protected SkeletonAttachment (SkeletonAttachment other) {
+		super(other);
+		skeleton = other.skeleton;
+	}
+
+	public @Null Skeleton getSkeleton () {
 		return skeleton;
 	}
 
@@ -50,9 +55,7 @@ public class SkeletonAttachment extends Attachment {
 		this.skeleton = skeleton;
 	}
 
-	public Attachment copy () {
-		SkeletonAttachment copy = new SkeletonAttachment(name);
-		copy.skeleton = skeleton;
-		return copy;
+	public SkeletonAttachment copy () {
+		return new SkeletonAttachment(this);
 	}
 }

+ 32 - 30
spine-libgdx/spine-libgdx/src/com/esotericsoftware/spine/attachments/VertexAttachment.java

@@ -44,15 +44,35 @@ abstract public class VertexAttachment extends Attachment {
 	static private int nextID;
 
 	private final int id = nextID();
+	@Null Attachment timelineAttachment = this;
 	@Null int[] bones;
 	float[] vertices;
 	int worldVerticesLength;
-	@Null VertexAttachment deformAttachment = this;
 
 	public VertexAttachment (String name) {
 		super(name);
 	}
 
+	/** Copy constructor. */
+	public VertexAttachment (VertexAttachment other) {
+		super(other);
+		timelineAttachment = other.timelineAttachment;
+
+		if (other.bones != null) {
+			bones = new int[other.bones.length];
+			arraycopy(other.bones, 0, bones, 0, bones.length);
+		} else
+			bones = null;
+
+		if (other.vertices != null) {
+			vertices = new float[other.vertices.length];
+			arraycopy(other.vertices, 0, vertices, 0, vertices.length);
+		} else
+			vertices = null;
+
+		worldVerticesLength = other.worldVerticesLength;
+	}
+
 	/** Transforms the attachment's local {@link #getVertices()} to world coordinates. If the slot's {@link Slot#getDeform()} is
 	 * not empty, it is used to deform the vertices.
 	 * <p>
@@ -120,17 +140,6 @@ abstract public class VertexAttachment extends Attachment {
 		}
 	}
 
-	/** Deform keys for the deform attachment are also applied to this attachment.
-	 * @return May be null if no deform keys should be applied. */
-	public @Null VertexAttachment getDeformAttachment () {
-		return deformAttachment;
-	}
-
-	/** @param deformAttachment May be null if no deform keys should be applied. */
-	public void setDeformAttachment (@Null VertexAttachment deformAttachment) {
-		this.deformAttachment = deformAttachment;
-	}
-
 	/** The bones which affect the {@link #getVertices()}. The array entries are, for each vertex, the number of bones affecting
 	 * the vertex followed by that many bone indices, which is the index of the bone in {@link Skeleton#getBones()}. Will be null
 	 * if this attachment has no weights. */
@@ -164,27 +173,20 @@ abstract public class VertexAttachment extends Attachment {
 		this.worldVerticesLength = worldVerticesLength;
 	}
 
-	/** Returns a unique ID for this attachment. */
-	public int getId () {
-		return id;
+	/** Timelines for the timeline attachment are also applied to this attachment.
+	 * @return May be null if no attachment-specific timelines should be applied. */
+	public @Null Attachment getTimelineAttachment () {
+		return timelineAttachment;
 	}
 
-	/** Does not copy id (generated) or name (set on construction). */
-	void copyTo (VertexAttachment attachment) {
-		if (bones != null) {
-			attachment.bones = new int[bones.length];
-			arraycopy(bones, 0, attachment.bones, 0, bones.length);
-		} else
-			attachment.bones = null;
-
-		if (vertices != null) {
-			attachment.vertices = new float[vertices.length];
-			arraycopy(vertices, 0, attachment.vertices, 0, vertices.length);
-		} else
-			attachment.vertices = null;
+	/** @param timelineAttachment May be null if no attachment-specific timelines should be applied. */
+	public void setTimelineAttachment (Attachment timelineAttachment) {
+		this.timelineAttachment = timelineAttachment;
+	}
 
-		attachment.worldVerticesLength = worldVerticesLength;
-		attachment.deformAttachment = deformAttachment;
+	/** Returns a unique ID for this attachment. */
+	public int getId () {
+		return id;
 	}
 
 	static private synchronized int nextID () {

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

@@ -258,7 +258,6 @@ public class SkeletonViewer extends ApplicationAdapter {
 				skeleton.setSlotsToSetupPose();
 
 			delta = Math.min(delta, 0.032f) * ui.speedSlider.getValue();
-			skeleton.update(delta);
 			state.update(delta);
 			state.apply(skeleton);
 			skeleton.updateWorldTransform();