Explorar o código

spine-as3 updated to v3.1.

NathanSweet %!s(int64=9) %!d(string=hai) anos
pai
achega
1ccfaab4c9

+ 1 - 1
spine-as3/README.md

@@ -10,7 +10,7 @@ The Spine Runtimes are developed with the intent to be used with data exported f
 
 ## Spine version
 
-spine-as3 works with data exported from the latest version of Spine, except linked meshes are [not yet supported](https://trello.com/c/bERJAFEq/73-update-runtimes-to-support-v3-1-linked-meshes).
+spine-as3 works with data exported from the latest version of Spine.
 
 spine-as3 supports all Spine features, including meshes. If using the `spine.flash` classes for rendering, meshes are not supported.
 

BIN=BIN
spine-as3/spine-as3-example/lib/spine-as3.swc


+ 64 - 70
spine-as3/spine-as3/src/spine/Bone.as

@@ -119,72 +119,71 @@ public class Bone implements Updatable {
 			_b = pa * lb + pb * ld;
 			_c = pc * la + pd * lc;
 			_d = pc * lb + pd * ld;
-		} else if (data.inheritRotation) { // No scale inheritance.
-			pa = 1;
-			pb = 0;
-			pc = 0;
-			pd = 1;
-			do {
-				radians = parent.appliedRotation * MathUtils.degRad;
-				cos = Math.cos(radians);
-				sin = Math.sin(radians);
-				var temp1:Number = pa * cos + pb * sin;
-				pb = pa * -sin + pb * cos;
-				pa = temp1;
-				temp1 = pc * cos + pd * sin;
-				pd = pc * -sin + pd * cos;
-				pc = temp1;
-
-				if (!parent.data.inheritRotation) break;
-				parent = parent.parent;
-			} while (parent != null);
-			_a = pa * la + pb * lc;
-			_b = pa * lb + pb * ld;
-			_c = pc * la + pd * lc;
-			_d = pc * lb + pd * ld;
-			if (_skeleton.flipX) {
-				_a = -_a;
-				_b = -_b;
-			}
-			if (_skeleton.flipY != yDown) {
-				_c = -_c;
-				_d = -_d;
+		} else {
+			if (data.inheritRotation) { // No scale inheritance.
+				pa = 1;
+				pb = 0;
+				pc = 0;
+				pd = 1;
+				do {
+					radians = parent.appliedRotation * MathUtils.degRad;
+					cos = Math.cos(radians);
+					sin = Math.sin(radians);
+					var temp1:Number = pa * cos + pb * sin;
+					pb = pa * -sin + pb * cos;
+					pa = temp1;
+					temp1 = pc * cos + pd * sin;
+					pd = pc * -sin + pd * cos;
+					pc = temp1;
+	
+					if (!parent.data.inheritRotation) break;
+					parent = parent.parent;
+				} while (parent != null);
+				_a = pa * la + pb * lc;
+				_b = pa * lb + pb * ld;
+				_c = pc * la + pd * lc;
+				_d = pc * lb + pd * ld;
+			} else if (data.inheritScale) { // No rotation inheritance.
+				pa = 1;
+				pb = 0;
+				pc = 0;
+				pd = 1;
+				do {
+					radians = parent.rotation * MathUtils.degRad;
+					cos = Math.cos(radians);
+					sin = Math.sin(radians);
+					var psx:Number = parent.appliedScaleX, psy:Number = parent.appliedScaleY;
+					var za:Number = cos * psx, zb:Number = -sin * psy, zc:Number = sin * psx, zd:Number = cos * psy;
+					var temp2:Number = pa * za + pb * zc;
+					pb = pa * zb + pb * zd;
+					pa = temp2;
+					temp2 = pc * za + pd * zc;
+					pd = pc * zb + pd * zd;
+					pc = temp2;
+	
+					if (psx < 0) radians = -radians;
+					cos = Math.cos(-radians);
+					sin = Math.sin(-radians);
+					temp2 = pa * cos + pb * sin;
+					pb = pa * -sin + pb * cos;
+					pa = temp2;
+					temp2 = pc * cos + pd * sin;
+					pd = pc * -sin + pd * cos;
+					pc = temp2;
+	
+					if (!parent.data.inheritScale) break;
+					parent = parent.parent;
+				} while (parent != null);
+				_a = pa * la + pb * lc;
+				_b = pa * lb + pb * ld;
+				_c = pc * la + pd * lc;
+				_d = pc * lb + pd * ld;
+			} else {
+				_a = la;
+				_b = lb;
+				_c = lc;
+				_d = ld;
 			}
-		} else if (data.inheritScale) { // No rotation inheritance.
-			pa = 1;
-			pb = 0;
-			pc = 0;
-			pd = 1;
-			do {
-				radians = parent.rotation * MathUtils.degRad;
-				cos = Math.cos(radians);
-				sin = Math.sin(radians);
-				var psx:Number = parent.appliedScaleX, psy:Number = parent.appliedScaleY;
-				var za:Number = cos * psx, zb:Number = -sin * psy, zc:Number = sin * psx, zd:Number = cos * psy;
-				var temp2:Number = pa * za + pb * zc;
-				pb = pa * zb + pb * zd;
-				pa = temp2;
-				temp2 = pc * za + pd * zc;
-				pd = pc * zb + pd * zd;
-				pc = temp2;
-
-				if (psx < 0) radians = -radians;
-				cos = Math.cos(-radians);
-				sin = Math.sin(-radians);
-				temp2 = pa * cos + pb * sin;
-				pb = pa * -sin + pb * cos;
-				pa = temp2;
-				temp2 = pc * cos + pd * sin;
-				pd = pc * -sin + pd * cos;
-				pc = temp2;
-
-				if (!parent.data.inheritScale) break;
-				parent = parent.parent;
-			} while (parent != null);
-			_a = pa * la + pb * lc;
-			_b = pa * lb + pb * ld;
-			_c = pc * la + pd * lc;
-			_d = pc * lb + pd * ld;
 			if (_skeleton.flipX) {
 				_a = -_a;
 				_b = -_b;
@@ -193,11 +192,6 @@ public class Bone implements Updatable {
 				_c = -_c;
 				_d = -_d;
 			}
-		} else {
-			_a = la;
-			_b = lb;
-			_c = lc;
-			_d = ld;
 		}
 	}
 

+ 23 - 18
spine-as3/spine-as3/src/spine/IkConstraint.as

@@ -84,7 +84,7 @@ public class IkConstraint implements Updatable {
 			rotationIK = 360 - rotationIK;
 		if (rotationIK > 180) rotationIK -= 360;
 		else if (rotationIK < -180) rotationIK += 360;
-		bone.updateWorldTransformWith(bone.x, bone.y, rotation + (rotationIK - rotation) * alpha, bone.scaleX, bone.scaleY);
+		bone.updateWorldTransformWith(bone.x, bone.y, rotation + (rotationIK - rotation) * alpha, bone.appliedScaleX, bone.appliedScaleY);
 	}
 
 	/** Adjusts the parent and child bone rotations so the tip of the child is as close to the target position as possible. The
@@ -92,26 +92,32 @@ public class IkConstraint implements Updatable {
 	 * @param child Any descendant bone of the parent. */
 	static public function apply2 (parent:Bone, child:Bone, targetX:Number, targetY:Number, bendDir:int, alpha:Number) : void {
 		if (alpha == 0) return;
-		var px:Number = parent.x, py:Number = parent.y, psx:Number = parent.scaleX, psy:Number = parent.scaleY;
-		var csx:Number = child.scaleX, cy:Number = child.y;
-		var offset1:int, offset2:int, sign2:int;
+		var px:Number = parent.x, py:Number = parent.y, psx:Number = parent.appliedScaleX, psy:Number = parent.appliedScaleY;
+		var o1:int, o2:int, s2:int;
 		if (psx < 0) {
 			psx = -psx;
-			offset1 = 180;
-			sign2 = -1;
+			o1 = 180;
+			s2 = -1;
 		} else {
-			offset1 = 0;
-			sign2 = 1;
+			o1 = 0;
+			s2 = 1;
 		}
 		if (psy < 0) {
 			psy = -psy;
-			sign2 = -sign2;
+			s2 = -s2;
+		}
+		var cx:Number = child.x, cy:Number = child.y, csx:Number = child.appliedScaleX;
+		var u:Boolean = Math.abs(psx - psy) <= 0.0001;
+		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;
-			offset2 = 180;
+			o2 = 180;
 		} else
-			offset2 = 0;
+			o2 = 0;
 		var pp:Bone = parent.parent;
 		var tx:Number, ty:Number, dx:Number, dy:Number;
 		if (!pp) {
@@ -132,7 +138,7 @@ public class IkConstraint implements Updatable {
 		}
 		var l1:Number = Math.sqrt(dx * dx + dy * dy), l2:Number = child.data.length * csx, a1:Number, a2:Number;
 		outer:
-		if (Math.abs(psx - psy) <= 0.0001) {
+		if (u) {
 			l2 *= psx;
 			var cos:Number = (tx * tx + ty * ty - l1 * l1 - l2 * l2) / (2 * l1 * l2);
 			if (cos < -1) cos = -1;
@@ -141,7 +147,6 @@ public class IkConstraint implements Updatable {
 			var ad:Number = l1 + l2 * cos, o:Number = l2 * Math.sin(a2);
 			a1 = Math.atan2(ty * ad - tx * o, tx * ad + ty * o);
 		} else {
-			cy = 0;
 			var a:Number = psx * l2, b:Number = psy * l2, ta:Number = Math.atan2(ty, tx);
 			var aa:Number = a * a, bb:Number = b * b, ll:Number = l1 * l1, dd:Number = tx * tx + ty * ty;
 			var c0:Number = bb * ll + aa * dd - aa * bb, c1:Number = -2 * bb * l1, c2:Number = bb - aa;
@@ -198,17 +203,17 @@ public class IkConstraint implements Updatable {
 				a2 = maxAngle * bendDir;
 			}
 		}
-		var offset:Number = Math.atan2(cy, child.x) * sign2;
-		a1 = (a1 - offset) * MathUtils.radDeg + offset1;
-		a2 = (a2 + offset) * MathUtils.radDeg * sign2 + offset2;
+		var os:Number = Math.atan2(cy, cx) * s2;
+		a1 = (a1 - os) * MathUtils.radDeg + o1;
+		a2 = (a2 + os) * MathUtils.radDeg * s2 + o2;
 		if (a1 > 180) a1 -= 360;
 		else if (a1 < -180) a1 += 360;
 		if (a2 > 180) a2 -= 360;
 		else if (a2 < -180) a2 += 360;
 		var rotation:Number = parent.rotation;
-		parent.updateWorldTransformWith(parent.x, parent.y, rotation + (a1 - rotation) * alpha, parent.scaleX, parent.scaleY);
+		parent.updateWorldTransformWith(px, py, rotation + (a1 - rotation) * alpha, parent.appliedScaleX, parent.appliedScaleY);
 		rotation = child.rotation;
-		child.updateWorldTransformWith(child.x, cy, rotation + (a2 - rotation) * alpha, child.scaleX, child.scaleY);
+		child.updateWorldTransformWith(cx, cy, rotation + (a2 - rotation) * alpha, child.appliedScaleX, child.appliedScaleY);
 	}
 }
 

+ 515 - 459
spine-as3/spine-as3/src/spine/SkeletonJson.as

@@ -29,235 +29,279 @@
  * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
  *****************************************************************************/
 package spine {
-	import flash.utils.ByteArray;
-
-	import spine.animation.Animation;
-	import spine.animation.AttachmentTimeline;
-	import spine.animation.ColorTimeline;
-	import spine.animation.CurveTimeline;
-	import spine.animation.DrawOrderTimeline;
-	import spine.animation.EventTimeline;
-	import spine.animation.FfdTimeline;
-	import spine.animation.IkConstraintTimeline;
-	import spine.animation.RotateTimeline;
-	import spine.animation.ScaleTimeline;
-	import spine.animation.Timeline;
-	import spine.animation.TranslateTimeline;
-	import spine.attachments.Attachment;
-	import spine.attachments.AttachmentLoader;
-	import spine.attachments.AttachmentType;
-	import spine.attachments.BoundingBoxAttachment;
-	import spine.attachments.MeshAttachment;
-	import spine.attachments.RegionAttachment;
-	import spine.attachments.WeightedMeshAttachment;
-
-	public class SkeletonJson {
-		public var attachmentLoader:AttachmentLoader;
-		public var scale:Number = 1;
-
-		public function SkeletonJson(attachmentLoader:AttachmentLoader = null) {
-			this.attachmentLoader = attachmentLoader;
+import flash.utils.ByteArray;
+
+import spine.animation.Animation;
+import spine.animation.AttachmentTimeline;
+import spine.animation.ColorTimeline;
+import spine.animation.CurveTimeline;
+import spine.animation.DrawOrderTimeline;
+import spine.animation.EventTimeline;
+import spine.animation.FfdTimeline;
+import spine.animation.IkConstraintTimeline;
+import spine.animation.RotateTimeline;
+import spine.animation.ScaleTimeline;
+import spine.animation.Timeline;
+import spine.animation.TranslateTimeline;
+import spine.attachments.Attachment;
+import spine.attachments.AttachmentLoader;
+import spine.attachments.AttachmentType;
+import spine.attachments.BoundingBoxAttachment;
+import spine.attachments.MeshAttachment;
+import spine.attachments.RegionAttachment;
+import spine.attachments.WeightedMeshAttachment;
+
+public class SkeletonJson {
+	public var attachmentLoader:AttachmentLoader;
+	public var scale:Number = 1;
+	private var linkedMeshes:Vector.<LinkedMesh> = new Vector.<LinkedMesh>();
+
+	public function SkeletonJson (attachmentLoader:AttachmentLoader = null) {
+		this.attachmentLoader = attachmentLoader;
+	}
+
+	/** @param object A String or ByteArray. */
+	public function readSkeletonData (object:*, name:String = null) : SkeletonData {
+		if (object == null) throw new ArgumentError("object cannot be null.");
+
+		var root:Object;
+		if (object is String)
+			root = JSON.parse(String(object));
+		else if (object is ByteArray)
+			root = JSON.parse(ByteArray(object).readUTFBytes(ByteArray(object).length));
+		else if (object is Object)
+			root = object;
+		else
+			throw new ArgumentError("object must be a String, ByteArray or Object.");
+
+		var skeletonData:SkeletonData = new SkeletonData();
+		skeletonData.name = name;
+
+		// Skeleton.
+		var skeletonMap:Object = root["skeleton"];
+		if (skeletonMap) {
+			skeletonData.hash = skeletonMap["hash"];
+			skeletonData.version = skeletonMap["spine"];
+			skeletonData.width = skeletonMap["width"] || 0;
+			skeletonData.height = skeletonMap["height"] || 0;
 		}
 
-		/** @param object A String or ByteArray. */
-		public function readSkeletonData(object:*, name:String = null) : SkeletonData {
-			if (object == null) throw new ArgumentError("object cannot be null.");
-
-			var root:Object;
-			if (object is String)
-				root = JSON.parse(String(object));
-			else if (object is ByteArray)
-				root = JSON.parse(ByteArray(object).readUTFBytes(ByteArray(object).length));
-			else if (object is Object)
-				root = object;
-			else
-				throw new ArgumentError("object must be a String, ByteArray or Object.");
-
-			var skeletonData:SkeletonData = new SkeletonData();
-			skeletonData.name = name;
-
-			// Skeleton.
-			var skeletonMap:Object = root["skeleton"];
-			if (skeletonMap) {
-				skeletonData.hash = skeletonMap["hash"];
-				skeletonData.version = skeletonMap["spine"];
-				skeletonData.width = skeletonMap["width"] || 0;
-				skeletonData.height = skeletonMap["height"] || 0;
+		// Bones.
+		var boneData:BoneData;
+		for each (var boneMap:Object in root["bones"]) {
+			var parent:BoneData = null;
+			var parentName:String = boneMap["parent"];
+			if (parentName) {
+				parent = skeletonData.findBone(parentName);
+				if (!parent) throw new Error("Parent bone not found: " + parentName);
 			}
+			boneData = new BoneData(boneMap["name"], parent);
+			boneData.length = Number(boneMap["length"] || 0) * scale;
+			boneData.x = Number(boneMap["x"] || 0) * scale;
+			boneData.y = Number(boneMap["y"] || 0) * scale;
+			boneData.rotation = (boneMap["rotation"] || 0);
+			boneData.scaleX = boneMap.hasOwnProperty("scaleX") ? boneMap["scaleX"] : 1;
+			boneData.scaleY = boneMap.hasOwnProperty("scaleY") ? boneMap["scaleY"] : 1;
+			boneData.inheritScale = boneMap.hasOwnProperty("inheritScale") ? boneMap["inheritScale"] : true;
+			boneData.inheritRotation = boneMap.hasOwnProperty("inheritRotation") ? boneMap["inheritRotation"] : true;
+			skeletonData.bones[skeletonData.bones.length] = boneData;
+		}
 
-			// Bones.
-			var boneData:BoneData;
-			for each (var boneMap:Object in root["bones"]) {
-				var parent:BoneData = null;
-				var parentName:String = boneMap["parent"];
-				if (parentName) {
-					parent = skeletonData.findBone(parentName);
-					if (!parent) throw new Error("Parent bone not found: " + parentName);
-				}
-				boneData = new BoneData(boneMap["name"], parent);
-				boneData.length = Number(boneMap["length"] || 0) * scale;
-				boneData.x = Number(boneMap["x"] || 0) * scale;
-				boneData.y = Number(boneMap["y"] || 0) * scale;
-				boneData.rotation = (boneMap["rotation"] || 0);
-				boneData.scaleX = boneMap.hasOwnProperty("scaleX") ? boneMap["scaleX"] : 1;
-				boneData.scaleY = boneMap.hasOwnProperty("scaleY") ? boneMap["scaleY"] : 1;
-				boneData.inheritScale = boneMap.hasOwnProperty("inheritScale") ? boneMap["inheritScale"] : true;
-				boneData.inheritRotation = boneMap.hasOwnProperty("inheritRotation") ? boneMap["inheritRotation"] : true;
-				skeletonData.bones[skeletonData.bones.length] = boneData;
-			}
+		// IK constraints.
+		for each (var ikMap:Object in root["ik"]) {
+			var ikConstraintData:IkConstraintData = new IkConstraintData(ikMap["name"]);
 
-			// IK constraints.
-			for each (var ikMap:Object in root["ik"]) {
-				var ikConstraintData:IkConstraintData = new IkConstraintData(ikMap["name"]);
+			for each (var boneName:String in ikMap["bones"]) {
+				var bone:BoneData = skeletonData.findBone(boneName);
+				if (!bone) throw new Error("IK bone not found: " + boneName);
+				ikConstraintData.bones[ikConstraintData.bones.length] = bone;
+			}
 
-				for each (var boneName:String in ikMap["bones"]) {
-					var bone:BoneData = skeletonData.findBone(boneName);
-					if (!bone) throw new Error("IK bone not found: " + boneName);
-					ikConstraintData.bones[ikConstraintData.bones.length] = bone;
-				}
+			ikConstraintData.target = skeletonData.findBone(ikMap["target"]);
+			if (!ikConstraintData.target) throw new Error("Target bone not found: " + ikMap["target"]);
 
-				ikConstraintData.target = skeletonData.findBone(ikMap["target"]);
-				if (!ikConstraintData.target) throw new Error("Target bone not found: " + ikMap["target"]);
+			ikConstraintData.bendDirection = (!ikMap.hasOwnProperty("bendPositive") || ikMap["bendPositive"]) ? 1 : -1;
+			ikConstraintData.mix = ikMap.hasOwnProperty("mix") ? ikMap["mix"] : 1;
 
-				ikConstraintData.bendDirection = (!ikMap.hasOwnProperty("bendPositive") || ikMap["bendPositive"]) ? 1 : -1;
-				ikConstraintData.mix = ikMap.hasOwnProperty("mix") ? ikMap["mix"] : 1;
+			skeletonData.ikConstraints[skeletonData.ikConstraints.length] = ikConstraintData;
+		}
 
-				skeletonData.ikConstraints[skeletonData.ikConstraints.length] = ikConstraintData;
-			}
+		// Transform constraints.
+		for each (var transformMap:Object in root["transform"]) {
+			var transformConstraintData:TransformConstraintData = new TransformConstraintData(transformMap["name"]);
 
-			// Transform constraints.
-			for each (var transformMap:Object in root["transform"]) {
-				var transformConstraintData:TransformConstraintData = new TransformConstraintData(transformMap["name"]);
+			transformConstraintData.bone = skeletonData.findBone(transformMap["bone"]);
+			if (!transformConstraintData.bone) throw new Error("Bone not found: " + transformMap["bone"]);
 
-				transformConstraintData.bone = skeletonData.findBone(transformMap["bone"]);
-				if (!transformConstraintData.bone) throw new Error("Bone not found: " + transformMap["bone"]);
+			transformConstraintData.target = skeletonData.findBone(transformMap["target"]);
+			if (!transformConstraintData.target) throw new Error("Target bone not found: " + transformMap["target"]);
 
-				transformConstraintData.target = skeletonData.findBone(transformMap["target"]);
-				if (!transformConstraintData.target) throw new Error("Target bone not found: " + transformMap["target"]);
+			transformConstraintData.translateMix = transformMap.hasOwnProperty("translateMix") ? transformMap["translateMix"] : 1;
+			transformConstraintData.x = Number(boneMap["x"] || 0) * scale;
+			transformConstraintData.y = Number(boneMap["y"] || 0) * scale;
 
-				transformConstraintData.translateMix = transformMap.hasOwnProperty("translateMix") ? transformMap["translateMix"] : 1;
-				transformConstraintData.x = Number(boneMap["x"] || 0) * scale;
-				transformConstraintData.y = Number(boneMap["y"] || 0) * scale;
+			skeletonData.transformConstraints[skeletonData.transformConstraints.length] = transformConstraintData;
+		}
 
-				skeletonData.transformConstraints[skeletonData.transformConstraints.length] = transformConstraintData;
+		// Slots.
+		for each (var slotMap:Object in root["slots"]) {
+			boneName = slotMap["bone"];
+			boneData = skeletonData.findBone(boneName);
+			if (!boneData) throw new Error("Slot bone not found: " + boneName);
+			var slotData:SlotData = new SlotData(slotMap["name"], boneData);
+
+			var color:String = slotMap["color"];
+			if (color) {
+				slotData.r = toColor(color, 0);
+				slotData.g = toColor(color, 1);
+				slotData.b = toColor(color, 2);
+				slotData.a = toColor(color, 3);
 			}
 
-			// Slots.
-			for each (var slotMap:Object in root["slots"]) {
-				boneName = slotMap["bone"];
-				boneData = skeletonData.findBone(boneName);
-				if (!boneData) throw new Error("Slot bone not found: " + boneName);
-				var slotData:SlotData = new SlotData(slotMap["name"], boneData);
-
-				var color:String = slotMap["color"];
-				if (color) {
-					slotData.r = toColor(color, 0);
-					slotData.g = toColor(color, 1);
-					slotData.b = toColor(color, 2);
-					slotData.a = toColor(color, 3);
-				}
-
-				slotData.attachmentName = slotMap["attachment"];
-				slotData.blendMode = BlendMode[slotMap["blend"] || "normal"];
+			slotData.attachmentName = slotMap["attachment"];
+			slotData.blendMode = BlendMode[slotMap["blend"] || "normal"];
 
-				skeletonData.slots[skeletonData.slots.length] = slotData;
-			}
+			skeletonData.slots[skeletonData.slots.length] = slotData;
+		}
 
-			// Skins.
-			var skins:Object = root["skins"];
-			for (var skinName:String in skins) {
-				var skinMap:Object = skins[skinName];
-				var skin:Skin = new Skin(skinName);
-				for (var slotName:String in skinMap) {
-					var slotIndex:int = skeletonData.findSlotIndex(slotName);
-					var slotEntry:Object = skinMap[slotName];
-					for (var attachmentName:String in slotEntry) {
-						var attachment:Attachment = readAttachment(skin, attachmentName, slotEntry[attachmentName]);
-						if (attachment != null)
-							skin.addAttachment(slotIndex, attachmentName, attachment);
-					}
+		// Skins.
+		var skins:Object = root["skins"];
+		for (var skinName:String in skins) {
+			var skinMap:Object = skins[skinName];
+			var skin:Skin = new Skin(skinName);
+			for (var slotName:String in skinMap) {
+				var slotIndex:int = skeletonData.findSlotIndex(slotName);
+				var slotEntry:Object = skinMap[slotName];
+				for (var attachmentName:String in slotEntry) {
+					var attachment:Attachment = readAttachment(skin, slotIndex, attachmentName, slotEntry[attachmentName]);
+					if (attachment != null)
+						skin.addAttachment(slotIndex, attachmentName, attachment);
 				}
-				skeletonData.skins[skeletonData.skins.length] = skin;
-				if (skin.name == "default")
-					skeletonData.defaultSkin = skin;
 			}
+			skeletonData.skins[skeletonData.skins.length] = skin;
+			if (skin.name == "default")
+				skeletonData.defaultSkin = skin;
+		}
 
-			// Events.
-			var events:Object = root["events"];
-			if (events) {
-				for (var eventName:String in events) {
-					var eventMap:Object = events[eventName];
-					var eventData:EventData = new EventData(eventName);
-					eventData.intValue = eventMap["int"] || 0;
-					eventData.floatValue = eventMap["float"] || 0;
-					eventData.stringValue = eventMap["string"] || null;
-					skeletonData.events[skeletonData.events.length] = eventData;
-				}
+		// Linked meshes.
+		for each (var linkedMesh:LinkedMesh in linkedMeshes) {
+			var parentSkin:Skin = !linkedMesh.skin ? skeletonData.defaultSkin : skeletonData.findSkin(linkedMesh.skin);
+			if (!parentSkin) throw new Error("Skin not found: " + linkedMesh.skin);
+			var parentMesh:Attachment = parentSkin.getAttachment(linkedMesh.slotIndex, linkedMesh.parent);
+			if (!parentMesh) throw new Error("Parent mesh not found: " + linkedMesh.parent);
+			if (linkedMesh.mesh is MeshAttachment) {
+				var mesh:MeshAttachment = MeshAttachment(linkedMesh.mesh);
+				mesh.parentMesh = MeshAttachment(parentMesh);
+				mesh.updateUVs();
+			} else {
+				var weightedMesh:WeightedMeshAttachment = WeightedMeshAttachment(linkedMesh.mesh);
+				weightedMesh.parentMesh = WeightedMeshAttachment(parentMesh);
+				weightedMesh.updateUVs();
+			}
+		}
+		linkedMeshes.length = 0;
+
+		// Events.
+		var events:Object = root["events"];
+		if (events) {
+			for (var eventName:String in events) {
+				var eventMap:Object = events[eventName];
+				var eventData:EventData = new EventData(eventName);
+				eventData.intValue = eventMap["int"] || 0;
+				eventData.floatValue = eventMap["float"] || 0;
+				eventData.stringValue = eventMap["string"] || null;
+				skeletonData.events[skeletonData.events.length] = eventData;
 			}
+		}
 
-			// Animations.
-			var animations:Object = root["animations"];
-			for (var animationName:String in animations)
-				readAnimation(animationName, animations[animationName], skeletonData);
+		// Animations.
+		var animations:Object = root["animations"];
+		for (var animationName:String in animations)
+			readAnimation(animationName, animations[animationName], skeletonData);
 
-			return skeletonData;
-		}
+		return skeletonData;
+	}
 
-		private function readAttachment(skin:Skin, name:String, map:Object) : Attachment {
-			name = map["name"] || name;
-
-			var typeName:String = map["type"] || "region";
-			if (typeName == "skinnedmesh") typeName = "weightedmesh";
-			var type:AttachmentType = AttachmentType[typeName];
-			var path:String = map["path"] || name;
-
-			var scale:Number = this.scale;
-			var color:String, vertices:Vector.<Number>;
-			switch (type) {
-				case AttachmentType.region:
-					var region:RegionAttachment = attachmentLoader.newRegionAttachment(skin, name, path);
-					if (!region) return null;
-					region.path = path;
-					region.x = Number(map["x"] || 0) * scale;
-					region.y = Number(map["y"] || 0) * scale;
-					region.scaleX = map.hasOwnProperty("scaleX") ? map["scaleX"] : 1;
-					region.scaleY = map.hasOwnProperty("scaleY") ? map["scaleY"] : 1;
-					region.rotation = map["rotation"] || 0;
-					region.width = Number(map["width"] || 0) * scale;
-					region.height = Number(map["height"] || 0) * scale;
-					color = map["color"];
-					if (color) {
-						region.r = toColor(color, 0);
-						region.g = toColor(color, 1);
-						region.b = toColor(color, 2);
-						region.a = toColor(color, 3);
-					}
-					region.updateOffset();
-					return region;
-				case AttachmentType.mesh:
-					var mesh:MeshAttachment = attachmentLoader.newMeshAttachment(skin, name, path);
-					if (!mesh) return null;
-					mesh.path = path;
+	private function readAttachment (skin:Skin, slotIndex:int, name:String, map:Object) : Attachment {
+		name = map["name"] || name;
+
+		var typeName:String = map["type"] || "region";
+		if (typeName == "skinnedmesh") typeName = "weightedmesh";
+		var type:AttachmentType = AttachmentType[typeName];
+		var path:String = map["path"] || name;
+
+		var scale:Number = this.scale;
+		var color:String, vertices:Vector.<Number>;
+		switch (type) {
+			case AttachmentType.region:
+				var region:RegionAttachment = attachmentLoader.newRegionAttachment(skin, name, path);
+				if (!region) return null;
+				region.path = path;
+				region.x = Number(map["x"] || 0) * scale;
+				region.y = Number(map["y"] || 0) * scale;
+				region.scaleX = map.hasOwnProperty("scaleX") ? map["scaleX"] : 1;
+				region.scaleY = map.hasOwnProperty("scaleY") ? map["scaleY"] : 1;
+				region.rotation = map["rotation"] || 0;
+				region.width = Number(map["width"] || 0) * scale;
+				region.height = Number(map["height"] || 0) * scale;
+				color = map["color"];
+				if (color) {
+					region.r = toColor(color, 0);
+					region.g = toColor(color, 1);
+					region.b = toColor(color, 2);
+					region.a = toColor(color, 3);
+				}
+				region.updateOffset();
+				return region;
+			case AttachmentType.mesh:
+			case AttachmentType.linkedmesh:
+				var mesh:MeshAttachment = attachmentLoader.newMeshAttachment(skin, name, path);
+				if (!mesh) return null;
+				mesh.path = path;
+
+				color = map["color"];
+				if (color) {
+					mesh.r = toColor(color, 0);
+					mesh.g = toColor(color, 1);
+					mesh.b = toColor(color, 2);
+					mesh.a = toColor(color, 3);
+				}
+
+				mesh.width = Number(map["width"] || 0) * scale;
+				mesh.height = Number(map["height"] || 0) * scale;
+
+				if (!map["parent"]) {
 					mesh.vertices = getFloatArray(map, "vertices", scale);
 					mesh.triangles = getUintArray(map, "triangles");
 					mesh.regionUVs = getFloatArray(map, "uvs", 1);
 					mesh.updateUVs();
-					color = map["color"];
-					if (color) {
-						mesh.r = toColor(color, 0);
-						mesh.g = toColor(color, 1);
-						mesh.b = toColor(color, 2);
-						mesh.a = toColor(color, 3);
-					}
+
 					mesh.hullLength = int(map["hull"] || 0) * 2;
 					if (map["edges"]) mesh.edges = getIntArray(map, "edges");
-					mesh.width = Number(map["width"] || 0) * scale;
-					mesh.height = Number(map["height"] || 0) * scale;
-					return mesh;
-				case AttachmentType.weightedmesh:
-					var weightedMesh:WeightedMeshAttachment = attachmentLoader.newWeightedMeshAttachment(skin, name, path);
-					if (!weightedMesh) return null;
-					weightedMesh.path = path;
+				} else {
+					mesh.inheritFFD = map.hasOwnProperty("ffd") ? map["ffd"] : true;
+					linkedMeshes[linkedMeshes.length] = new LinkedMesh(mesh, map["skin"], slotIndex, map["parent"]);
+				}
+				return mesh;
+			case AttachmentType.weightedmesh:
+			case AttachmentType.weightedlinkedmesh:
+				var weightedMesh:WeightedMeshAttachment = attachmentLoader.newWeightedMeshAttachment(skin, name, path);
+				if (!weightedMesh) return null;
+
+				weightedMesh.path = path;
+
+				color = map["color"];
+				if (color) {
+					weightedMesh.r = toColor(color, 0);
+					weightedMesh.g = toColor(color, 1);
+					weightedMesh.b = toColor(color, 2);
+					weightedMesh.a = toColor(color, 3);
+				}
+
+				weightedMesh.width = Number(map["width"] || 0) * scale;
+				weightedMesh.height = Number(map["height"] || 0) * scale;
+
+				if (!map["parent"]) {
 					var uvs:Vector.<Number> = getFloatArray(map, "uvs", 1);
 					vertices = getFloatArray(map, "vertices", 1);
 					var weights:Vector.<Number> = new Vector.<Number>();
@@ -278,297 +322,309 @@ package spine {
 					weightedMesh.triangles = getUintArray(map, "triangles");
 					weightedMesh.regionUVs = uvs;
 					weightedMesh.updateUVs();
-					color = map["color"];
-					if (color) {
-						weightedMesh.r = toColor(color, 0);
-						weightedMesh.g = toColor(color, 1);
-						weightedMesh.b = toColor(color, 2);
-						weightedMesh.a = toColor(color, 3);
-					}
+
 					weightedMesh.hullLength = int(map["hull"] || 0) * 2;
 					if (map["edges"]) weightedMesh.edges = getIntArray(map, "edges");
-					weightedMesh.width = Number(map["width"] || 0) * scale;
-					weightedMesh.height = Number(map["height"] || 0) * scale;
-					return weightedMesh;
-				case AttachmentType.boundingbox:
-					var box:BoundingBoxAttachment = attachmentLoader.newBoundingBoxAttachment(skin, name);
-					vertices = box.vertices;
-					for each (var point:Number in map["vertices"])
-						vertices[vertices.length] = point * scale;
-					return box;
-			}
-
-			return null;
+				} else {
+					weightedMesh.inheritFFD = map.hasOwnProperty("ffd") ? map["ffd"] : true;
+					linkedMeshes[linkedMeshes.length] = new LinkedMesh(weightedMesh, map["skin"], slotIndex, map["parent"]);
+				}
+				return weightedMesh;
+			case AttachmentType.boundingbox:
+				var box:BoundingBoxAttachment = attachmentLoader.newBoundingBoxAttachment(skin, name);
+				vertices = box.vertices;
+				for each (var point:Number in map["vertices"])
+					vertices[vertices.length] = point * scale;
+				return box;
 		}
 
-		private function readAnimation(name:String, map:Object, skeletonData:SkeletonData) : void {
-			var timelines:Vector.<Timeline> = new Vector.<Timeline>();
-			var duration:Number = 0;
-
-			var slotMap:Object, slotIndex:int, slotName:String;
-			var values:Array, valueMap:Object, frameIndex:int;
-			var i:int;
-			var timelineName:String;
-
-			var slots:Object = map["slots"];
-			for (slotName in slots) {
-				slotMap = slots[slotName];
-				slotIndex = skeletonData.findSlotIndex(slotName);
+		return null;
+	}
 
-				for (timelineName in slotMap) {
-					values = slotMap[timelineName];
-					if (timelineName == "color") {
-						var colorTimeline:ColorTimeline = new ColorTimeline(values.length);
-						colorTimeline.slotIndex = slotIndex;
-
-						frameIndex = 0;
-						for each (valueMap in values) {
-							var color:String = valueMap["color"];
-							var r:Number = toColor(color, 0);
-							var g:Number = toColor(color, 1);
-							var b:Number = toColor(color, 2);
-							var a:Number = toColor(color, 3);
-							colorTimeline.setFrame(frameIndex, valueMap["time"], r, g, b, a);
-							readCurve(colorTimeline, frameIndex, valueMap);
-							frameIndex++;
-						}
-						timelines[timelines.length] = colorTimeline;
-						duration = Math.max(duration, colorTimeline.frames[colorTimeline.frameCount * 5 - 5]);
-					} else if (timelineName == "attachment") {
-						var attachmentTimeline:AttachmentTimeline = new AttachmentTimeline(values.length);
-						attachmentTimeline.slotIndex = slotIndex;
-
-						frameIndex = 0;
-						for each (valueMap in values)
-							attachmentTimeline.setFrame(frameIndex++, valueMap["time"], valueMap["name"]);
-						timelines[timelines.length] = attachmentTimeline;
-						duration = Math.max(duration, attachmentTimeline.frames[attachmentTimeline.frameCount - 1]);
-					} else
-						throw new Error("Invalid timeline type for a slot: " + timelineName + " (" + slotName + ")");
-				}
+	private function readAnimation (name:String, map:Object, skeletonData:SkeletonData) : void {
+		var timelines:Vector.<Timeline> = new Vector.<Timeline>();
+		var duration:Number = 0;
+
+		var slotMap:Object, slotIndex:int, slotName:String;
+		var values:Array, valueMap:Object, frameIndex:int;
+		var i:int;
+		var timelineName:String;
+
+		var slots:Object = map["slots"];
+		for (slotName in slots) {
+			slotMap = slots[slotName];
+			slotIndex = skeletonData.findSlotIndex(slotName);
+
+			for (timelineName in slotMap) {
+				values = slotMap[timelineName];
+				if (timelineName == "color") {
+					var colorTimeline:ColorTimeline = new ColorTimeline(values.length);
+					colorTimeline.slotIndex = slotIndex;
+
+					frameIndex = 0;
+					for each (valueMap in values) {
+						var color:String = valueMap["color"];
+						var r:Number = toColor(color, 0);
+						var g:Number = toColor(color, 1);
+						var b:Number = toColor(color, 2);
+						var a:Number = toColor(color, 3);
+						colorTimeline.setFrame(frameIndex, valueMap["time"], r, g, b, a);
+						readCurve(colorTimeline, frameIndex, valueMap);
+						frameIndex++;
+					}
+					timelines[timelines.length] = colorTimeline;
+					duration = Math.max(duration, colorTimeline.frames[colorTimeline.frameCount * 5 - 5]);
+				} else if (timelineName == "attachment") {
+					var attachmentTimeline:AttachmentTimeline = new AttachmentTimeline(values.length);
+					attachmentTimeline.slotIndex = slotIndex;
+
+					frameIndex = 0;
+					for each (valueMap in values)
+						attachmentTimeline.setFrame(frameIndex++, valueMap["time"], valueMap["name"]);
+					timelines[timelines.length] = attachmentTimeline;
+					duration = Math.max(duration, attachmentTimeline.frames[attachmentTimeline.frameCount - 1]);
+				} else
+					throw new Error("Invalid timeline type for a slot: " + timelineName + " (" + slotName + ")");
 			}
+		}
 
-			var bones:Object = map["bones"];
-			for (var boneName:String in bones) {
-				var boneIndex:int = skeletonData.findBoneIndex(boneName);
-				if (boneIndex == -1) throw new Error("Bone not found: " + boneName);
-				var boneMap:Object = bones[boneName];
-
-				for (timelineName in boneMap) {
-					values = boneMap[timelineName];
-					if (timelineName == "rotate") {
-						var rotateTimeline:RotateTimeline = new RotateTimeline(values.length);
-						rotateTimeline.boneIndex = boneIndex;
-
-						frameIndex = 0;
-						for each (valueMap in values) {
-							rotateTimeline.setFrame(frameIndex, valueMap["time"], valueMap["angle"]);
-							readCurve(rotateTimeline, frameIndex, valueMap);
-							frameIndex++;
-						}
-						timelines[timelines.length] = rotateTimeline;
-						duration = Math.max(duration, rotateTimeline.frames[rotateTimeline.frameCount * 2 - 2]);
-					} else if (timelineName == "translate" || timelineName == "scale") {
-						var timeline:TranslateTimeline;
-						var timelineScale:Number = 1;
-						if (timelineName == "scale")
-							timeline = new ScaleTimeline(values.length);
-						else {
-							timeline = new TranslateTimeline(values.length);
-							timelineScale = scale;
-						}
-						timeline.boneIndex = boneIndex;
-
-						frameIndex = 0;
-						for each (valueMap in values) {
-							var x:Number = Number(valueMap["x"] || 0) * timelineScale;
-							var y:Number = Number(valueMap["y"] || 0) * timelineScale;
-							timeline.setFrame(frameIndex, valueMap["time"], x, y);
-							readCurve(timeline, frameIndex, valueMap);
-							frameIndex++;
-						}
-						timelines[timelines.length] = timeline;
-						duration = Math.max(duration, timeline.frames[timeline.frameCount * 3 - 3]);
-					} else
-						throw new Error("Invalid timeline type for a bone: " + timelineName + " (" + boneName + ")");
-				}
+		var bones:Object = map["bones"];
+		for (var boneName:String in bones) {
+			var boneIndex:int = skeletonData.findBoneIndex(boneName);
+			if (boneIndex == -1) throw new Error("Bone not found: " + boneName);
+			var boneMap:Object = bones[boneName];
+
+			for (timelineName in boneMap) {
+				values = boneMap[timelineName];
+				if (timelineName == "rotate") {
+					var rotateTimeline:RotateTimeline = new RotateTimeline(values.length);
+					rotateTimeline.boneIndex = boneIndex;
+
+					frameIndex = 0;
+					for each (valueMap in values) {
+						rotateTimeline.setFrame(frameIndex, valueMap["time"], valueMap["angle"]);
+						readCurve(rotateTimeline, frameIndex, valueMap);
+						frameIndex++;
+					}
+					timelines[timelines.length] = rotateTimeline;
+					duration = Math.max(duration, rotateTimeline.frames[rotateTimeline.frameCount * 2 - 2]);
+				} else if (timelineName == "translate" || timelineName == "scale") {
+					var timeline:TranslateTimeline;
+					var timelineScale:Number = 1;
+					if (timelineName == "scale")
+						timeline = new ScaleTimeline(values.length);
+					else {
+						timeline = new TranslateTimeline(values.length);
+						timelineScale = scale;
+					}
+					timeline.boneIndex = boneIndex;
+
+					frameIndex = 0;
+					for each (valueMap in values) {
+						var x:Number = Number(valueMap["x"] || 0) * timelineScale;
+						var y:Number = Number(valueMap["y"] || 0) * timelineScale;
+						timeline.setFrame(frameIndex, valueMap["time"], x, y);
+						readCurve(timeline, frameIndex, valueMap);
+						frameIndex++;
+					}
+					timelines[timelines.length] = timeline;
+					duration = Math.max(duration, timeline.frames[timeline.frameCount * 3 - 3]);
+				} else
+					throw new Error("Invalid timeline type for a bone: " + timelineName + " (" + boneName + ")");
 			}
+		}
 
-			var ikMap:Object = map["ik"];
-			for (var ikConstraintName:String in ikMap) {
-				var ikConstraint:IkConstraintData = skeletonData.findIkConstraint(ikConstraintName);
-				values = ikMap[ikConstraintName];
-				var ikTimeline:IkConstraintTimeline = new IkConstraintTimeline(values.length);
-				ikTimeline.ikConstraintIndex = skeletonData.ikConstraints.indexOf(ikConstraint);
-				frameIndex = 0;
-				for each (valueMap in values) {
-					var mix:Number = valueMap.hasOwnProperty("mix") ? valueMap["mix"] : 1;
-					var bendDirection:int = (!valueMap.hasOwnProperty("bendPositive") || valueMap["bendPositive"]) ? 1 : -1;
-					ikTimeline.setFrame(frameIndex, valueMap["time"], mix, bendDirection);
-					readCurve(ikTimeline, frameIndex, valueMap);
-					frameIndex++;
-				}
-				timelines[timelines.length] = ikTimeline;
-				duration = Math.max(duration, ikTimeline.frames[ikTimeline.frameCount * 3 - 3]);
+		var ikMap:Object = map["ik"];
+		for (var ikConstraintName:String in ikMap) {
+			var ikConstraint:IkConstraintData = skeletonData.findIkConstraint(ikConstraintName);
+			values = ikMap[ikConstraintName];
+			var ikTimeline:IkConstraintTimeline = new IkConstraintTimeline(values.length);
+			ikTimeline.ikConstraintIndex = skeletonData.ikConstraints.indexOf(ikConstraint);
+			frameIndex = 0;
+			for each (valueMap in values) {
+				var mix:Number = valueMap.hasOwnProperty("mix") ? valueMap["mix"] : 1;
+				var bendDirection:int = (!valueMap.hasOwnProperty("bendPositive") || valueMap["bendPositive"]) ? 1 : -1;
+				ikTimeline.setFrame(frameIndex, valueMap["time"], mix, bendDirection);
+				readCurve(ikTimeline, frameIndex, valueMap);
+				frameIndex++;
 			}
+			timelines[timelines.length] = ikTimeline;
+			duration = Math.max(duration, ikTimeline.frames[ikTimeline.frameCount * 3 - 3]);
+		}
 
-			var ffd:Object = map["ffd"];
-			for (var skinName:String in ffd) {
-				var skin:Skin = skeletonData.findSkin(skinName);
-				slotMap = ffd[skinName];
-				for (slotName in slotMap) {
-					slotIndex = skeletonData.findSlotIndex(slotName);
-					var meshMap:Object = slotMap[slotName];
-					for (var meshName:String in meshMap) {
-						values = meshMap[meshName];
-						var ffdTimeline:FfdTimeline = new FfdTimeline(values.length);
-						var attachment:Attachment = skin.getAttachment(slotIndex, meshName);
-						if (!attachment) throw new Error("FFD attachment not found: " + meshName);
-						ffdTimeline.slotIndex = slotIndex;
-						ffdTimeline.attachment = attachment;
-
-						var vertexCount:int;
-						if (attachment is MeshAttachment)
-							vertexCount = (attachment as MeshAttachment).vertices.length;
-						else
-							vertexCount = (attachment as WeightedMeshAttachment).weights.length / 3 * 2;
-
-						frameIndex = 0;
-						for each (valueMap in values) {
-							var vertices:Vector.<Number>;
-							if (!valueMap["vertices"]) {
-								if (attachment is MeshAttachment)
-									vertices = (attachment as MeshAttachment).vertices;
-								else
-									vertices = new Vector.<Number>(vertexCount, true);
-							} else {
-								var verticesValue:Array = valueMap["vertices"];
+		var ffd:Object = map["ffd"];
+		for (var skinName:String in ffd) {
+			var skin:Skin = skeletonData.findSkin(skinName);
+			slotMap = ffd[skinName];
+			for (slotName in slotMap) {
+				slotIndex = skeletonData.findSlotIndex(slotName);
+				var meshMap:Object = slotMap[slotName];
+				for (var meshName:String in meshMap) {
+					values = meshMap[meshName];
+					var ffdTimeline:FfdTimeline = new FfdTimeline(values.length);
+					var attachment:Attachment = skin.getAttachment(slotIndex, meshName);
+					if (!attachment) throw new Error("FFD attachment not found: " + meshName);
+					ffdTimeline.slotIndex = slotIndex;
+					ffdTimeline.attachment = attachment;
+
+					var vertexCount:int;
+					if (attachment is MeshAttachment)
+						vertexCount = (attachment as MeshAttachment).vertices.length;
+					else
+						vertexCount = (attachment as WeightedMeshAttachment).weights.length / 3 * 2;
+
+					frameIndex = 0;
+					for each (valueMap in values) {
+						var vertices:Vector.<Number>;
+						if (!valueMap["vertices"]) {
+							if (attachment is MeshAttachment)
+								vertices = (attachment as MeshAttachment).vertices;
+							else
 								vertices = new Vector.<Number>(vertexCount, true);
-								var start:int = valueMap["offset"] || 0;
-								var n:int = verticesValue.length;
-								if (scale == 1) {
-									for (i = 0; i < n; i++)
-										vertices[i + start] = verticesValue[i];
-								} else {
-									for (i = 0; i < n; i++)
-										vertices[i + start] = verticesValue[i] * scale;
-								}
-								if (attachment is MeshAttachment) {
-									var meshVertices:Vector.<Number> = (attachment as MeshAttachment).vertices;
-									for (i = 0; i < vertexCount; i++)
-										vertices[i] += meshVertices[i];
-								}
+						} else {
+							var verticesValue:Array = valueMap["vertices"];
+							vertices = new Vector.<Number>(vertexCount, true);
+							var start:int = valueMap["offset"] || 0;
+							var n:int = verticesValue.length;
+							if (scale == 1) {
+								for (i = 0; i < n; i++)
+									vertices[i + start] = verticesValue[i];
+							} else {
+								for (i = 0; i < n; i++)
+									vertices[i + start] = verticesValue[i] * scale;
+							}
+							if (attachment is MeshAttachment) {
+								var meshVertices:Vector.<Number> = (attachment as MeshAttachment).vertices;
+								for (i = 0; i < vertexCount; i++)
+									vertices[i] += meshVertices[i];
 							}
-
-							ffdTimeline.setFrame(frameIndex, valueMap["time"], vertices);
-							readCurve(ffdTimeline, frameIndex, valueMap);
-							frameIndex++;
 						}
-						timelines[timelines.length] = ffdTimeline;
-						duration = Math.max(duration, ffdTimeline.frames[ffdTimeline.frameCount - 1]);
+
+						ffdTimeline.setFrame(frameIndex, valueMap["time"], vertices);
+						readCurve(ffdTimeline, frameIndex, valueMap);
+						frameIndex++;
 					}
+					timelines[timelines.length] = ffdTimeline;
+					duration = Math.max(duration, ffdTimeline.frames[ffdTimeline.frameCount - 1]);
 				}
 			}
+		}
 
-			var drawOrderValues:Array = map["drawOrder"];
-			if (!drawOrderValues) drawOrderValues = map["draworder"];
-			if (drawOrderValues) {
-				var drawOrderTimeline:DrawOrderTimeline = new DrawOrderTimeline(drawOrderValues.length);
-				var slotCount:int = skeletonData.slots.length;
-				frameIndex = 0;
-				for each (var drawOrderMap:Object in drawOrderValues) {
-					var drawOrder:Vector.<int> = null;
-					if (drawOrderMap["offsets"]) {
-						drawOrder = new Vector.<int>(slotCount);
-						for (i = slotCount - 1; i >= 0; i--)
-							drawOrder[i] = -1;
-						var offsets:Array = drawOrderMap["offsets"];
-						var unchanged:Vector.<int> = new Vector.<int>(slotCount - offsets.length);
-						var originalIndex:int = 0, unchangedIndex:int = 0;
-						for each (var offsetMap:Object in offsets) {
-							slotIndex = skeletonData.findSlotIndex(offsetMap["slot"]);
-							if (slotIndex == -1) throw new Error("Slot not found: " + offsetMap["slot"]);
-							// Collect unchanged items.
-							while (originalIndex != slotIndex)
-								unchanged[unchangedIndex++] = originalIndex++;
-							// Set changed items.
-							drawOrder[originalIndex + offsetMap["offset"]] = originalIndex++;
-						}
-						// Collect remaining unchanged items.
-						while (originalIndex < slotCount)
+		var drawOrderValues:Array = map["drawOrder"];
+		if (!drawOrderValues) drawOrderValues = map["draworder"];
+		if (drawOrderValues) {
+			var drawOrderTimeline:DrawOrderTimeline = new DrawOrderTimeline(drawOrderValues.length);
+			var slotCount:int = skeletonData.slots.length;
+			frameIndex = 0;
+			for each (var drawOrderMap:Object in drawOrderValues) {
+				var drawOrder:Vector.<int> = null;
+				if (drawOrderMap["offsets"]) {
+					drawOrder = new Vector.<int>(slotCount);
+					for (i = slotCount - 1; i >= 0; i--)
+						drawOrder[i] = -1;
+					var offsets:Array = drawOrderMap["offsets"];
+					var unchanged:Vector.<int> = new Vector.<int>(slotCount - offsets.length);
+					var originalIndex:int = 0, unchangedIndex:int = 0;
+					for each (var offsetMap:Object in offsets) {
+						slotIndex = skeletonData.findSlotIndex(offsetMap["slot"]);
+						if (slotIndex == -1) throw new Error("Slot not found: " + offsetMap["slot"]);
+						// Collect unchanged items.
+						while (originalIndex != slotIndex)
 							unchanged[unchangedIndex++] = originalIndex++;
-						// Fill in unchanged items.
-						for (i = slotCount - 1; i >= 0; i--)
-							if (drawOrder[i] == -1) drawOrder[i] = unchanged[--unchangedIndex];
+						// Set changed items.
+						drawOrder[originalIndex + offsetMap["offset"]] = originalIndex++;
 					}
-					drawOrderTimeline.setFrame(frameIndex++, drawOrderMap["time"], drawOrder);
+					// Collect remaining unchanged items.
+					while (originalIndex < slotCount)
+						unchanged[unchangedIndex++] = originalIndex++;
+					// Fill in unchanged items.
+					for (i = slotCount - 1; i >= 0; i--)
+						if (drawOrder[i] == -1) drawOrder[i] = unchanged[--unchangedIndex];
 				}
-				timelines[timelines.length] = drawOrderTimeline;
-				duration = Math.max(duration, drawOrderTimeline.frames[drawOrderTimeline.frameCount - 1]);
+				drawOrderTimeline.setFrame(frameIndex++, drawOrderMap["time"], drawOrder);
 			}
+			timelines[timelines.length] = drawOrderTimeline;
+			duration = Math.max(duration, drawOrderTimeline.frames[drawOrderTimeline.frameCount - 1]);
+		}
 
-			var eventsMap:Array = map["events"];
-			if (eventsMap) {
-				var eventTimeline:EventTimeline = new EventTimeline(eventsMap.length);
-				frameIndex = 0;
-				for each (var eventMap:Object in eventsMap) {
-					var eventData:EventData = skeletonData.findEvent(eventMap["name"]);
-					if (!eventData) throw new Error("Event not found: " + eventMap["name"]);
-					var event:Event = new Event(eventMap["time"], eventData);
-					event.intValue = eventMap.hasOwnProperty("int") ? eventMap["int"] : eventData.intValue;
-					event.floatValue = eventMap.hasOwnProperty("float") ? eventMap["float"] : eventData.floatValue;
-					event.stringValue = eventMap.hasOwnProperty("string") ? eventMap["string"] : eventData.stringValue;
-					eventTimeline.setFrame(frameIndex++, event);
-				}
-				timelines[timelines.length] = eventTimeline;
-				duration = Math.max(duration, eventTimeline.frames[eventTimeline.frameCount - 1]);
+		var eventsMap:Array = map["events"];
+		if (eventsMap) {
+			var eventTimeline:EventTimeline = new EventTimeline(eventsMap.length);
+			frameIndex = 0;
+			for each (var eventMap:Object in eventsMap) {
+				var eventData:EventData = skeletonData.findEvent(eventMap["name"]);
+				if (!eventData) throw new Error("Event not found: " + eventMap["name"]);
+				var event:Event = new Event(eventMap["time"], eventData);
+				event.intValue = eventMap.hasOwnProperty("int") ? eventMap["int"] : eventData.intValue;
+				event.floatValue = eventMap.hasOwnProperty("float") ? eventMap["float"] : eventData.floatValue;
+				event.stringValue = eventMap.hasOwnProperty("string") ? eventMap["string"] : eventData.stringValue;
+				eventTimeline.setFrame(frameIndex++, event);
 			}
-
-			skeletonData.animations[skeletonData.animations.length] = new Animation(name, timelines, duration);
+			timelines[timelines.length] = eventTimeline;
+			duration = Math.max(duration, eventTimeline.frames[eventTimeline.frameCount - 1]);
 		}
 
-		static private function readCurve(timeline:CurveTimeline, frameIndex:int, valueMap:Object) : void {
-			var curve:Object = valueMap["curve"];
-			if (!curve) return;
-			if (curve == "stepped")
-				timeline.setStepped(frameIndex);
-			else if (curve is Array)
-				timeline.setCurve(frameIndex, curve[0], curve[1], curve[2], curve[3]);
-		}
+		skeletonData.animations[skeletonData.animations.length] = new Animation(name, timelines, duration);
+	}
 
-		static private function toColor(hexString:String, colorIndex:int) : Number {
-			if (hexString.length != 8) throw new ArgumentError("Color hexidecimal length must be 8, recieved: " + hexString);
-			return parseInt(hexString.substring(colorIndex * 2, colorIndex * 2 + 2), 16) / 255;
-		}
+	static private function readCurve (timeline:CurveTimeline, frameIndex:int, valueMap:Object) : void {
+		var curve:Object = valueMap["curve"];
+		if (!curve) return;
+		if (curve == "stepped")
+			timeline.setStepped(frameIndex);
+		else if (curve is Array)
+			timeline.setCurve(frameIndex, curve[0], curve[1], curve[2], curve[3]);
+	}
 
-		static private function getFloatArray(map:Object, name:String, scale:Number) : Vector.<Number> {
-			var list:Array = map[name];
-			var values:Vector.<Number> = new Vector.<Number>(list.length, true);
-			var i:int = 0, n:int = list.length;
-			if (scale == 1) {
-				for (; i < n; i++)
-					values[i] = list[i];
-			} else {
-				for (; i < n; i++)
-					values[i] = list[i] * scale;
-			}
-			return values;
-		}
+	static private function toColor (hexString:String, colorIndex:int) : Number {
+		if (hexString.length != 8) throw new ArgumentError("Color hexidecimal length must be 8, recieved: " + hexString);
+		return parseInt(hexString.substring(colorIndex * 2, colorIndex * 2 + 2), 16) / 255;
+	}
 
-		static private function getIntArray(map:Object, name:String) : Vector.<int> {
-			var list:Array = map[name];
-			var values:Vector.<int> = new Vector.<int>(list.length, true);
-			for (var i:int = 0, n:int = list.length; i < n; i++)
-				values[i] = int(list[i]);
-			return values;
+	static private function getFloatArray (map:Object, name:String, scale:Number) : Vector.<Number> {
+		var list:Array = map[name];
+		var values:Vector.<Number> = new Vector.<Number>(list.length, true);
+		var i:int = 0, n:int = list.length;
+		if (scale == 1) {
+			for (; i < n; i++)
+				values[i] = list[i];
+		} else {
+			for (; i < n; i++)
+				values[i] = list[i] * scale;
 		}
+		return values;
+	}
 
-		static private function getUintArray(map:Object, name:String) : Vector.<uint> {
-			var list:Array = map[name];
-			var values:Vector.<uint> = new Vector.<uint>(list.length, true);
-			for (var i:int = 0, n:int = list.length; i < n; i++)
-				values[i] = int(list[i]);
-			return values;
-		}
+	static private function getIntArray (map:Object, name:String) : Vector.<int> {
+		var list:Array = map[name];
+		var values:Vector.<int> = new Vector.<int>(list.length, true);
+		for (var i:int = 0, n:int = list.length; i < n; i++)
+			values[i] = int(list[i]);
+		return values;
+	}
+
+	static private function getUintArray (map:Object, name:String) : Vector.<uint> {
+		var list:Array = map[name];
+		var values:Vector.<uint> = new Vector.<uint>(list.length, true);
+		for (var i:int = 0, n:int = list.length; i < n; i++)
+			values[i] = int(list[i]);
+		return values;
+	}
+}
+
+}
+
+import spine.attachments.Attachment;
+
+internal class LinkedMesh {
+	internal var parent:String, skin:String;
+	internal var slotIndex:int;
+	internal var mesh:Attachment;
+
+	public function LinkedMesh (mesh:Attachment, skin:String, slotIndex:int, parent:String) {
+		this.mesh = mesh;
+		this.skin = skin;
+		this.slotIndex = slotIndex;
+		this.parent = parent;
 	}
 }

+ 3 - 1
spine-as3/spine-as3/src/spine/animation/FfdTimeline.as

@@ -30,6 +30,7 @@
  *****************************************************************************/
 
 package spine.animation {
+import spine.attachments.FfdAttachment;
 import spine.Event;
 import spine.Skeleton;
 import spine.Slot;
@@ -55,7 +56,8 @@ public class FfdTimeline extends CurveTimeline {
 
 	override public function apply (skeleton:Skeleton, lastTime:Number, time:Number, firedEvents:Vector.<Event>, alpha:Number) : void {
 		var slot:Slot = skeleton.slots[slotIndex];
-		if (slot.attachment != attachment) return;
+		var slotAttachment:FfdAttachment = slot.attachment as FfdAttachment;
+		if (!slotAttachment || !slotAttachment.applyFFD(attachment)) return;
 
 		var frames:Vector.<Number> = this.frames;
 		if (time < frames[0]) return; // Time is before first frame.

+ 2 - 0
spine-as3/spine-as3/src/spine/attachments/AttachmentType.as

@@ -37,6 +37,8 @@ public class AttachmentType {
 	public static const boundingbox:AttachmentType = new AttachmentType(2, "boundingbox");
 	public static const mesh:AttachmentType = new AttachmentType(3, "mesh");
 	public static const weightedmesh:AttachmentType = new AttachmentType(4, "weightedmesh");
+	public static const linkedmesh:AttachmentType = new AttachmentType(3, "linkedmesh");
+	public static const weightedlinkedmesh:AttachmentType = new AttachmentType(4, "weightedlinkedmesh");
 
 	public var ordinal:int;
 	public var name:String;

+ 38 - 0
spine-as3/spine-as3/src/spine/attachments/FfdAttachment.as

@@ -0,0 +1,38 @@
+/******************************************************************************
+ * 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 spine.attachments {
+
+public interface FfdAttachment {
+	function applyFFD (sourceAttachment:Attachment) : Boolean;
+}
+
+}

+ 24 - 1
spine-as3/spine-as3/src/spine/attachments/MeshAttachment.as

@@ -33,7 +33,7 @@ package spine.attachments {
 import spine.Slot;
 import spine.Bone;
 
-public dynamic class MeshAttachment extends Attachment {
+public dynamic class MeshAttachment extends Attachment implements FfdAttachment {
 	public var vertices:Vector.<Number>;
 	public var uvs:Vector.<Number>;
 	public var regionUVs:Vector.<Number>;
@@ -43,6 +43,8 @@ public dynamic class MeshAttachment extends Attachment {
 	public var g:Number = 1;
 	public var b:Number = 1;
 	public var a:Number = 1;
+	private var _parentMesh:MeshAttachment;
+	public var inheritFFD:Boolean;
 
 	public var path:String;
 	public var rendererObject:Object;
@@ -102,6 +104,27 @@ public dynamic class MeshAttachment extends Attachment {
 			worldVertices[int(ii + 1)] = vx * m10 + vy * m11 + y;
 		}
 	}
+
+	public function applyFFD (sourceAttachment:Attachment) : Boolean {
+		return this == sourceAttachment || (inheritFFD && _parentMesh == sourceAttachment);
+	}
+
+	public function get parentMesh () : MeshAttachment {
+		return _parentMesh;
+	}
+
+	public function set parentMesh (parentMesh:MeshAttachment) : void {
+		_parentMesh = parentMesh;
+		if (parentMesh != null) {
+			vertices = parentMesh.vertices;
+			regionUVs = parentMesh.regionUVs;
+			triangles = parentMesh.triangles;
+			hullLength = parentMesh.hullLength;
+			edges = parentMesh.edges;
+			width = parentMesh.width;
+			height = parentMesh.height;
+		}
+	}
 }
 
 }

+ 25 - 1
spine-as3/spine-as3/src/spine/attachments/WeightedMeshAttachment.as

@@ -33,7 +33,7 @@ package spine.attachments {
 import spine.Slot;
 import spine.Bone;
 
-public dynamic class WeightedMeshAttachment extends Attachment {
+public dynamic class WeightedMeshAttachment extends Attachment implements FfdAttachment {
 	public var bones:Vector.<int>;
 	public var weights:Vector.<Number>;
 	public var uvs:Vector.<Number>;
@@ -44,6 +44,8 @@ public dynamic class WeightedMeshAttachment extends Attachment {
 	public var g:Number = 1;
 	public var b:Number = 1;
 	public var a:Number = 1;
+	private var _parentMesh:WeightedMeshAttachment;
+	public var inheritFFD:Boolean;
 
 	public var path:String;
 	public var rendererObject:Object;
@@ -127,6 +129,28 @@ public dynamic class WeightedMeshAttachment extends Attachment {
 			}
 		}
 	}
+
+	public function applyFFD (sourceAttachment:Attachment) : Boolean {
+		return this == sourceAttachment || (inheritFFD && _parentMesh == sourceAttachment);
+	}
+
+	public function get parentMesh () : WeightedMeshAttachment {
+		return _parentMesh;
+	}
+
+	public function set parentMesh (parentMesh:WeightedMeshAttachment) : void {
+		_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;
+		}
+	}
 }
 
 }

+ 1 - 1
spine-as3/spine-as3/src/spine/flash/SkeletonSprite.as

@@ -78,7 +78,7 @@ public class SkeletonSprite extends Sprite {
 		for (var i:int = 0, n:int = drawOrder.length; i < n; i++) {
 			var slot:Slot = drawOrder[i];
 			var regionAttachment:RegionAttachment = slot.attachment as RegionAttachment;
-			if (regionAttachment != null) {
+			if (regionAttachment) {
 				var wrapper:Sprite = regionAttachment["wrapper"];
 				var region:AtlasRegion = AtlasRegion(regionAttachment.rendererObject);
 				if (!wrapper) {

+ 1 - 1
spine-starling/README.md

@@ -10,7 +10,7 @@ The Spine Runtimes are developed with the intent to be used with data exported f
 
 ## Spine version
 
-spine-starling works with data exported from the latest version of Spine, except linked meshes are [not yet supported](https://trello.com/c/bERJAFEq/73-update-runtimes-to-support-v3-1-linked-meshes).
+spine-starling works with data exported from the latest version of Spine.
 
 spine-starling supports all Spine features.
 

BIN=BIN
spine-starling/spine-starling-example/lib/spine-as3.swc


BIN=BIN
spine-starling/spine-starling-example/lib/spine-starling.swc


BIN=BIN
spine-starling/spine-starling/lib/spine-as3.swc