Bladeren bron

[csharp] Ported 4.2-beta physics.

Harald Csaszar 1 jaar geleden
bovenliggende
commit
fe53638f69

+ 16 - 0
CHANGELOG.md

@@ -34,6 +34,22 @@
   * `SkeletonGraphic` now supports automatic scaling based on its `RectTransform` bounds. Automatic scaling can be enabled by setting the added `Layout Scale Mode` Inspector property to either `Width Controls Height`, `Height Controls Width`, `FitInParent` or `EnvelopeParent`. It is set to `None` by default to keep previous behaviour and avoid breaking existing projects. To modify the reference layout bounds, hit the additional `Edit Layout Bounds` toggle button to switch into edit mode, adjust the bounds or hit `Match RectTransform with Mesh`, and hit the button again when done adjusting. The skeleton will now be scaled accordingly to fit the reference layout bounds to the object's `RectTransform`.
   * Added previously missing unlit URP 2D shader variant, available under `Universal Render Pipeline/2D/Spine/Skeleton`.
   * Added support for light cookies at `Universal Render Pipeline/Spine/Sprite` shader.
+  * Timeline extension package: An additional Spine preferences parameter `Timeline` - `Default Mix Duration` has been added, setting newly added `SpineAnimationStateClip` clips accordingly, defaults to false. This Spine preferences parameter can be enabled to default to the previous behaviour before this update.
+  * Tint Black: Added support for [Tint Black](http://en.esotericsoftware.com/spine-slots#Tint-black) functionality at all Spine URP shaders (2D and 3D shaders) and at all standard pipeline `Spine/Sprite` shaders. This feature can be enabled via the `Tint Black` material parameter in the Inspector. Note: The URP Sprite shaders provided in the Spine URP Shaders extension UPM package require the latest version of the spine-unity runtime (package version 4.1.12, 2023-05-31 or newer) to display the added material parameters in the Inspector GUI.
+  * Added `SkeletonGraphic.MeshScale` property to allow access to calculated mesh scale. `MeshScale` is based on (1) Canvas pixels per unit, and (2) `RectTransform` bounds when using `Layout Scale Mode` other than `None` at `SkeletonGraphic` which scales the skeleton mesh to fit the parent `RectTransform` bounds accordingly.
+  * Added `updateSeparatorPartScale` property to `SkeletonGraphic` to let render separator parts follow the scale (lossy scale) of the `SkeletonGraphic` GameObject. Defaults to `false` to maintain existing behaviour.
+  * Added experimental `EditorSkeletonPlayer` component to allow Editor playback of the initial animation set at `SkeletonAnimation` or `SkeletonGraphic` components. Add this component to your skeleton GameObject to enable the in-editor animation preview. Allows configurations for continuous playback when selected, deselected, and alternative single-frame preview by setting `Fixed Track Time` to any value other than 0. Limitations: At skeletons with variable material count the Inspector preview may be too unresponsive. It is then recommended to disable the `EditorSkeletonPlayer` component (at the top of the Inspector) to make it responsive again, then you can disable `Play When Selected` and re-enable the component to preview playback only when deselected.
+  * Added example component `RenderCombinedMesh` to render a combined mesh of multiple meshes or submeshes. This is required by `OutlineOnly` shaders to render a combined outline when using `SkeletonRenderSeparator` or multiple atlas pages which would normally lead to outlines around individual parts. To add a combined outline to your SkeletenRenderer:
+    1) Add a child GameObject and move it a bit back (e.g. position Z = 0.01).
+    2) Add a `RenderCombinedMesh` component, provided in the `Spine Examples/Scripts/Sample Components` directory.
+    3) Copy the original material, add *_Outline* to its name and set the shader to your outline-only shader like `Universal Render Pipeline/Spine/Outline/Skeleton-OutlineOnly` or `Spine/Outline/OutlineOnly-ZWrite`.
+    4) Assign this *_Outline* material at the new child GameObject's `MeshRenderer` component.
+    If you are using `SkeletonRenderSeparator` and need to enable and disable the `SkeletonRenderSeparator` component at runtime, you can increase the `RenderCombinedMesh` `Reference Renderers` array by one and assign the `SkeletonRenderer` itself at the last entry after the parts renderers. Disabled `MeshRenderer` components will be skipped when combining the final mesh, so the combined mesh is automatically filled from the desired active renderers.
+  * Timeline extension package: Added static `EditorEvent` callback to allow editor scripts to react to animation events outside of play-mode. Register to the events via `Spine.Unity.Playables.SpineAnimationStateMixerBehaviour.EditorEvent += YourCallback;`.
+  * URP Shaders: Added `Depth Write` property to shaders `Universal Render Pipeline/Spine/Skeleton` and `Universal Render Pipeline/Spine/Skeleton Lit`. Defaults to false to maintain existing behaviour.
+  * Added `Animation Update` mode (called `UpdateTiming` in code) `In Late Update` for `SkeletonAnimation`, `SkeletonMecanim` and `SkeletonGraphic`. This allows you to update the `SkeletonMecanim` skeleton in the same frame that the Mecanim Animator updated its state, which happens between `Update` and `LateUpdate`.
+  * URP Shaders: Added URP "Blend Mode" shader variants for both URP 3D and URP 2D renderers. They are listed under shader name "Universal Render Pipeline/Spine/Blend Modes/" and "Universal Render Pipeline/2D/Spine/Blend Modes/" respectively.
+  * URP Shaders: Added support for [Tint Black](http://en.esotericsoftware.com/spine-slots#Tint-black) functionality at "Blend Modes" Spine URP shaders (2D and 3D shaders).
 
 * **Breaking changes**
   * Changed `SpineShaderWithOutlineGUI` outline related methods from `private` to `protected virtual` to allow for custom shader GUI subclasses to switch to different outline shaders.

File diff suppressed because it is too large
+ 416 - 331
spine-csharp/src/Animation.cs


+ 28 - 9
spine-csharp/src/AnimationState.cs

@@ -246,6 +246,7 @@ namespace Spine {
 					mix *= ApplyMixingFrom(current, skeleton, blend);
 				else if (current.trackTime >= current.trackEnd && current.next == null) //
 					mix = 0; // Set to setup pose the last time the entry will be applied.
+				bool attachments = mix < current.attachmentThreshold;
 
 				// Apply current entry.
 				float animationLast = current.animationLast, animationTime = current.AnimationTime, applyTime = animationTime;
@@ -258,10 +259,11 @@ namespace Spine {
 				int timelineCount = current.animation.timelines.Count;
 				Timeline[] timelines = current.animation.timelines.Items;
 				if ((i == 0 && mix == 1) || blend == MixBlend.Add) {
+					if (i == 0) attachments = true;
 					for (int ii = 0; ii < timelineCount; ii++) {
 						Timeline timeline = timelines[ii];
 						if (timeline is AttachmentTimeline)
-							ApplyAttachmentTimeline((AttachmentTimeline)timeline, skeleton, applyTime, blend, true);
+							ApplyAttachmentTimeline((AttachmentTimeline)timeline, skeleton, applyTime, blend, attachments);
 						else
 							timeline.Apply(skeleton, animationLast, applyTime, applyEvents, mix, blend, MixDirection.In);
 					}
@@ -281,7 +283,7 @@ namespace Spine {
 							ApplyRotateTimeline(rotateTimeline, skeleton, applyTime, mix, timelineBlend, timelinesRotation,
 												ii << 1, firstFrame);
 						else if (timeline is AttachmentTimeline)
-							ApplyAttachmentTimeline((AttachmentTimeline)timeline, skeleton, applyTime, blend, true);
+							ApplyAttachmentTimeline((AttachmentTimeline)timeline, skeleton, applyTime, blend, attachments);
 						else
 							timeline.Apply(skeleton, animationLast, applyTime, applyEvents, mix, timelineBlend, MixDirection.In);
 					}
@@ -544,7 +546,7 @@ namespace Spine {
 
 			// Mix between rotations using the direction of the shortest route on the first frame.
 			float total, diff = r2 - r1;
-			diff -= (16384 - (int)(16384.499999999996 - diff / 360)) * 360;
+			diff -= (float)Math.Ceiling(diff / 360 - 0.5f) * 360;
 			if (diff == 0) {
 				total = timelinesRotation[i];
 			} else {
@@ -942,7 +944,7 @@ namespace Spine {
 		/// </summary>
 		public float TimeScale { get { return timeScale; } set { timeScale = value; } }
 
-		/// <summary>The AnimationStateData to look up mix durations.</summary>
+		/// <summary>The <see cref="AnimationStateData"/> to look up mix durations.</summary>
 		public AnimationStateData Data {
 			get {
 				return data;
@@ -1037,7 +1039,7 @@ namespace Spine {
 		/// duration.</summary>
 		public bool Loop { get { return loop; } set { loop = value; } }
 
-		///<summary>
+		/// <summary>
 		/// <para>
 		/// Seconds to postpone playing the animation. When this track entry is the current track entry, <code>Delay</code>
 		/// postpones incrementing the <see cref="TrackEntry.TrackTime"/>. When this track entry is queued, <code>Delay</code> is the time from
@@ -1096,7 +1098,7 @@ namespace Spine {
 		/// <summary>
 		/// Seconds for the last frame of this animation. Non-looping animations won't play past this time. Looping animations will
 		/// loop back to <see cref="TrackEntry.AnimationStart"/> at this time. Defaults to the animation <see cref="Animation.Duration"/>.
-		///</summary>
+		/// </summary>
 		public float AnimationEnd { get { return animationEnd; } set { animationEnd = value; } }
 
 		/// <summary>
@@ -1173,7 +1175,7 @@ namespace Spine {
 		/// When the mix percentage (<see cref="TrackEntry.MixTime"/> / <see cref="TrackEntry.MixDuration"/>) is less than the
 		/// <code>AttachmentThreshold</code>, attachment timelines are applied while this animation is being mixed out. Defaults to
 		/// 0, so attachment timelines are not applied while this animation is being mixed out.
-		///</summary>
+		/// </summary>
 		public float AttachmentThreshold { get { return attachmentThreshold; } set { attachmentThreshold = value; } }
 
 		/// <summary>
@@ -1194,6 +1196,12 @@ namespace Spine {
 		/// The animation queued to play before this animation, or null. <code>previous</code> makes up a doubly linked list.</summary>
 		public TrackEntry Previous { get { return previous; } }
 
+		/// <summary>Returns true if this track entry has been applied at least once.</summary>
+		/// <seealso cref="AnimationState.Apply(Skeleton)"/>
+		public bool WasApplied {
+			get { return nextTrackLast != -1; }
+		}
+
 		/// <summary>
 		/// Returns true if at least one loop has been completed.</summary>
 		/// <seealso cref="TrackEntry.Complete"/>
@@ -1222,6 +1230,17 @@ namespace Spine {
 		/// </para></summary>
 		public float MixDuration { get { return mixDuration; } set { mixDuration = value; } }
 
+		/// <summary>Sets both <see cref="MixDuration"/> and <see cref="Delay"/>.</summary>
+		/// <param name="delay">If > 0, sets <see cref="TrackEntry.Delay"/>. If <= 0, the delay set is the duration of the previous track
+		///		entry minus the specified mix duration plus the specified<code> delay</code> (ie the mix ends at
+		///		(<code>delay</code> = 0) or before (<code>delay</code> < 0) the previous track entry duration). If the previous
+		///		entry is looping, its next loop completion is used instead of its duration.</param>
+		public void SetMixDuration (float mixDuration, float delay) {
+			this.mixDuration = mixDuration;
+			if (previous != null && delay <= 0) delay += previous.TrackComplete - mixDuration;
+			this.delay = delay;
+		}
+
 		/// <summary>
 		/// <para>
 		/// Controls how properties keyed in the animation are mixed with lower tracks. Defaults to <see cref="MixBlend.Replace"/>.
@@ -1235,12 +1254,12 @@ namespace Spine {
 
 		/// <summary>
 		/// The track entry for the previous animation when mixing from the previous animation to this animation, or null if no
-		/// mixing is currently occuring. When mixing from multiple animations, <code>MixingFrom</code> makes up a linked list.</summary>
+		/// mixing is currently occurring. When mixing from multiple animations, <code>MixingFrom</code> makes up a linked list.</summary>
 		public TrackEntry MixingFrom { get { return mixingFrom; } }
 
 		/// <summary>
 		/// The track entry for the next animation when mixing from this animation to the next animation, or null if no mixing is
-		/// currently occuring. When mixing to multiple animations, <code>MixingTo</code> makes up a linked list.</summary>
+		/// currently occurring. When mixing to multiple animations, <code>MixingTo</code> makes up a linked list.</summary>
 		public TrackEntry MixingTo { get { return mixingTo; } }
 
 		/// <summary>

+ 1 - 1
spine-csharp/src/Attachments/Attachment.cs

@@ -50,7 +50,7 @@ namespace Spine {
 			return Name;
 		}
 
-		///<summary>Returns a copy of the attachment.</summary>
+		/// <summary>Returns a copy of the attachment.</summary>
 		public abstract Attachment Copy ();
 	}
 }

+ 1 - 1
spine-csharp/src/Attachments/MeshAttachment.cs

@@ -197,7 +197,7 @@ namespace Spine {
 			base.ComputeWorldVertices(slot, start, count, worldVertices, offset, stride);
 		}
 
-		///<summary>Returns a new mesh with this mesh set as the <see cref="ParentMesh"/>.
+		/// <summary>Returns a new mesh with this mesh set as the <see cref="ParentMesh"/>.
 		public MeshAttachment NewLinkedMesh () {
 			MeshAttachment mesh = new MeshAttachment(Name);
 

+ 7 - 5
spine-csharp/src/Attachments/PointAttachment.cs

@@ -27,6 +27,8 @@
  * SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
  *****************************************************************************/
 
+using System;
+
 namespace Spine {
 	/// <summary>
 	/// An attachment which is a single point and a rotation. This can be used to spawn projectiles, particles, etc. A bone can be
@@ -45,7 +47,7 @@ namespace Spine {
 			: base(name) {
 		}
 
-		/** Copy constructor. */
+		/// <summary>Copy constructor.</summary>
 		protected PointAttachment (PointAttachment other)
 			: base(other) {
 			x = other.x;
@@ -58,10 +60,10 @@ namespace Spine {
 		}
 
 		public float ComputeWorldRotation (Bone bone) {
-			float cos = MathUtils.CosDeg(rotation), sin = MathUtils.SinDeg(rotation);
-			float ix = cos * bone.a + sin * bone.b;
-			float iy = cos * bone.c + sin * bone.d;
-			return MathUtils.Atan2(iy, ix) * MathUtils.RadDeg;
+			float r = rotation * MathUtils.DegRad, cos = (float)Math.Cos(r), sin = (float)Math.Sin(r);
+			float x = cos * bone.a + sin * bone.b;
+			float y = cos * bone.c + sin * bone.d;
+			return MathUtils.Atan2Deg(y, x);
 		}
 
 		public override Attachment Copy () {

+ 4 - 9
spine-csharp/src/Attachments/RegionAttachment.cs

@@ -106,8 +106,7 @@ namespace Spine {
 				return;
 			}
 
-			float width = Width;
-			float height = Height;
+			float width = Width, height = Height;
 			float localX2 = width / 2;
 			float localY2 = height / 2;
 			float localX = -localX2;
@@ -126,17 +125,13 @@ namespace Spine {
 					localY2 -= (region.originalHeight - region.offsetY - region.packedHeight) / region.originalHeight * height;
 				}
 			}
-			float scaleX = ScaleX;
-			float scaleY = ScaleY;
+			float scaleX = ScaleX, scaleY = ScaleY;
 			localX *= scaleX;
 			localY *= scaleY;
 			localX2 *= scaleX;
 			localY2 *= scaleY;
-			float rotation = Rotation;
-			float cos = MathUtils.CosDeg(this.rotation);
-			float sin = MathUtils.SinDeg(this.rotation);
-			float x = X;
-			float y = Y;
+			float r = Rotation * MathUtils.DegRad, cos = (float)Math.Cos(r), sin = (float)Math.Sin(r);
+			float x = X, y = Y;
 			float localXCos = localX * cos + x;
 			float localXSin = localX * sin;
 			float localYCos = localY * cos + y;

+ 1 - 1
spine-csharp/src/Attachments/VertexAttachment.cs

@@ -47,7 +47,7 @@ namespace Spine {
 		public int[] Bones { get { return bones; } set { bones = value; } }
 		public float[] Vertices { get { return vertices; } set { vertices = value; } }
 		public int WorldVerticesLength { get { return worldVerticesLength; } set { worldVerticesLength = value; } }
-		///<summary>Timelines for the timeline attachment are also applied to this attachment.
+		/// <summary>Timelines for the timeline attachment are also applied to this attachment.
 		/// May be null if no attachment-specific timelines should be applied.</summary>
 		public VertexAttachment TimelineAttachment { get { return timelineAttachment; } set { timelineAttachment = value; } }
 

+ 91 - 73
spine-csharp/src/Bone.cs

@@ -30,6 +30,8 @@
 using System;
 
 namespace Spine {
+	using Physics = Skeleton.Physics;
+
 	/// <summary>
 	/// Stores a bone's current pose.
 	/// <para>
@@ -57,8 +59,6 @@ namespace Spine {
 		public Skeleton Skeleton { get { return skeleton; } }
 		public Bone Parent { get { return parent; } }
 		public ExposedList<Bone> Children { get { return children; } }
-		/// <summary>Returns false when the bone has not been computed because <see cref="BoneData.SkinRequired"/> is true and the
-		/// <see cref="Skeleton.Skin">active skin</see> does not <see cref="Skin.Bones">contain</see> this bone.</summary>
 		public bool Active { get { return active; } }
 		/// <summary>The local X translation.</summary>
 		public float X { get { return x; } set { x = value; } }
@@ -113,8 +113,10 @@ namespace Spine {
 		public float WorldX { get { return worldX; } set { worldX = value; } }
 		/// <summary>The world Y position. If changed, <see cref="UpdateAppliedTransform()"/> should be called.</summary>
 		public float WorldY { get { return worldY; } set { worldY = value; } }
-		public float WorldRotationX { get { return MathUtils.Atan2(c, a) * MathUtils.RadDeg; } }
-		public float WorldRotationY { get { return MathUtils.Atan2(d, b) * MathUtils.RadDeg; } }
+		/// <summary>The world rotation for the X axis, calculated using <see cref="a"/> and <see cref="c"/>.</summary>
+		public float WorldRotationX { get { return MathUtils.Atan2Deg(c, a); } }
+		/// <summary>The world rotation for the Y axis, calculated using <see cref="b"/> and <see cref="d"/>.</summary>
+		public float WorldRotationY { get { return MathUtils.Atan2Deg(d, b); } }
 
 		/// <summary>Returns the magnitide (always positive) of the world scale X.</summary>
 		public float WorldScaleX { get { return (float)Math.Sqrt(a * a + c * c); } }
@@ -148,7 +150,7 @@ namespace Spine {
 		}
 
 		/// <summary>Computes the world transform using the parent bone and this bone's local applied transform.</summary>
-		public void Update () {
+		public void Update (Physics physics) {
 			UpdateWorldTransform(ax, ay, arotation, ascaleX, ascaleY, ashearX, ashearY);
 		}
 
@@ -173,11 +175,14 @@ namespace Spine {
 
 			Bone parent = this.parent;
 			if (parent == null) { // Root bone.
-				float rotationY = rotation + 90 + shearY, sx = skeleton.scaleX, sy = skeleton.scaleY;
-				a = MathUtils.CosDeg(rotation + shearX) * scaleX * sx;
-				b = MathUtils.CosDeg(rotationY) * scaleY * sx;
-				c = MathUtils.SinDeg(rotation + shearX) * scaleX * sy;
-				d = MathUtils.SinDeg(rotationY) * scaleY * sy;
+				Skeleton skeleton = this.skeleton;
+				float sx = skeleton.scaleX, sy = skeleton.scaleY;
+				float rx = (rotation + shearX) * MathUtils.DegRad;
+				float ry = (rotation + 90 + shearY) * MathUtils.DegRad;
+				a = (float)Math.Cos(rx) * scaleX * sx;
+				b = (float)Math.Cos(ry) * scaleY * sx;
+				c = (float)Math.Sin(rx) * scaleX * sy;
+				d = (float)Math.Sin(ry) * scaleY * sy;
 				worldX = x * sx + skeleton.x;
 				worldY = y * sy + skeleton.y;
 				return;
@@ -189,11 +194,12 @@ namespace Spine {
 
 			switch (data.transformMode) {
 			case TransformMode.Normal: {
-				float rotationY = rotation + 90 + shearY;
-				float la = MathUtils.CosDeg(rotation + shearX) * scaleX;
-				float lb = MathUtils.CosDeg(rotationY) * scaleY;
-				float lc = MathUtils.SinDeg(rotation + shearX) * scaleX;
-				float ld = MathUtils.SinDeg(rotationY) * scaleY;
+				float rx = (rotation + shearX) * MathUtils.DegRad;
+				float ry = (rotation + 90 + shearY) * MathUtils.DegRad;
+				float la = (float)Math.Cos(rx) * scaleX;
+				float lb = (float)Math.Cos(ry) * scaleY;
+				float lc = (float)Math.Sin(rx) * scaleX;
+				float ld = (float)Math.Sin(ry) * scaleY;
 				a = pa * la + pb * lc;
 				b = pa * lb + pb * ld;
 				c = pc * la + pd * lc;
@@ -201,11 +207,12 @@ namespace Spine {
 				return;
 			}
 			case TransformMode.OnlyTranslation: {
-				float rotationY = rotation + 90 + shearY;
-				a = MathUtils.CosDeg(rotation + shearX) * scaleX;
-				b = MathUtils.CosDeg(rotationY) * scaleY;
-				c = MathUtils.SinDeg(rotation + shearX) * scaleX;
-				d = MathUtils.SinDeg(rotationY) * scaleY;
+				float rx = (rotation + shearX) * MathUtils.DegRad;
+				float ry = (rotation + 90 + shearY) * MathUtils.DegRad;
+				a = (float)Math.Cos(rx) * scaleX;
+				b = (float)Math.Cos(ry) * scaleY;
+				c = (float)Math.Sin(rx) * scaleX;
+				d = (float)Math.Sin(ry) * scaleY;
 				break;
 			}
 			case TransformMode.NoRotationOrReflection: {
@@ -216,18 +223,18 @@ namespace Spine {
 					pc /= skeleton.scaleY;
 					pb = pc * s;
 					pd = pa * s;
-					prx = MathUtils.Atan2(pc, pa) * MathUtils.RadDeg;
+					prx = MathUtils.Atan2Deg(pc, pa);
 				} else {
 					pa = 0;
 					pc = 0;
-					prx = 90 - MathUtils.Atan2(pd, pb) * MathUtils.RadDeg;
+					prx = 90 - MathUtils.Atan2Deg(pd, pb);
 				}
-				float rx = rotation + shearX - prx;
-				float ry = rotation + shearY - prx + 90;
-				float la = MathUtils.CosDeg(rx) * scaleX;
-				float lb = MathUtils.CosDeg(ry) * scaleY;
-				float lc = MathUtils.SinDeg(rx) * scaleX;
-				float ld = MathUtils.SinDeg(ry) * scaleY;
+				float rx = (rotation + shearX - prx) * MathUtils.DegRad;
+				float ry = (rotation + shearY - prx + 90) * MathUtils.DegRad;
+				float la = (float)Math.Cos(rx) * scaleX;
+				float lb = (float)Math.Cos(ry) * scaleY;
+				float lc = (float)Math.Sin(rx) * scaleX;
+				float ld = (float)Math.Sin(ry) * scaleY;
 				a = pa * la - pb * lc;
 				b = pa * lb - pb * ld;
 				c = pc * la + pd * lc;
@@ -236,7 +243,8 @@ namespace Spine {
 			}
 			case TransformMode.NoScale:
 			case TransformMode.NoScaleOrReflection: {
-				float cos = MathUtils.CosDeg(rotation), sin = MathUtils.SinDeg(rotation);
+				rotation *= MathUtils.DegRad;
+				float cos = (float)Math.Cos(rotation), sin = (float)Math.Sin(rotation);
 				float za = (pa * cos + pb * sin) / skeleton.scaleX;
 				float zc = (pc * cos + pd * sin) / skeleton.scaleY;
 				float s = (float)Math.Sqrt(za * za + zc * zc);
@@ -246,14 +254,15 @@ namespace Spine {
 				s = (float)Math.Sqrt(za * za + zc * zc);
 				if (data.transformMode == TransformMode.NoScale
 					&& (pa * pd - pb * pc < 0) != (skeleton.scaleX < 0 != skeleton.scaleY < 0)) s = -s;
-
-				float r = MathUtils.PI / 2 + MathUtils.Atan2(zc, za);
-				float zb = MathUtils.Cos(r) * s;
-				float zd = MathUtils.Sin(r) * s;
-				float la = MathUtils.CosDeg(shearX) * scaleX;
-				float lb = MathUtils.CosDeg(90 + shearY) * scaleY;
-				float lc = MathUtils.SinDeg(shearX) * scaleX;
-				float ld = MathUtils.SinDeg(90 + shearY) * scaleY;
+				rotation = MathUtils.PI / 2 + MathUtils.Atan2(zc, za);
+				float zb = (float)Math.Cos(rotation) * s;
+				float zd = (float)Math.Sin(rotation) * s;
+				shearX *= MathUtils.DegRad;
+				shearY = (90 + shearY) * MathUtils.DegRad;
+				float la = (float)Math.Cos(shearX) * scaleX;
+				float lb = (float)Math.Cos(shearY) * scaleY;
+				float lc = (float)Math.Sin(shearX) * scaleX;
+				float ld = (float)Math.Sin(shearY) * scaleY;
 				a = za * la + zb * lc;
 				b = za * lb + zb * ld;
 				c = zc * la + zd * lc;
@@ -261,13 +270,13 @@ namespace Spine {
 				break;
 			}
 			}
-
 			a *= skeleton.scaleX;
 			b *= skeleton.scaleX;
 			c *= skeleton.scaleY;
 			d *= skeleton.scaleY;
 		}
 
+		/// <summary>Sets this bone's local transform to the setup pose.</summary>
 		public void SetToSetupPose () {
 			BoneData data = this.data;
 			x = data.x;
@@ -294,18 +303,19 @@ namespace Spine {
 			if (parent == null) {
 				ax = worldX - skeleton.x;
 				ay = worldY - skeleton.y;
-				arotation = MathUtils.Atan2(c, a) * MathUtils.RadDeg;
+				float a = this.a, b = this.b, c = this.c, d = this.d;
+				arotation = MathUtils.Atan2Deg(c, a);
 				ascaleX = (float)Math.Sqrt(a * a + c * c);
 				ascaleY = (float)Math.Sqrt(b * b + d * d);
 				ashearX = 0;
-				ashearY = MathUtils.Atan2(a * b + c * d, a * d - b * c) * MathUtils.RadDeg;
+				ashearY = MathUtils.Atan2Deg(a * b + c * d, a * d - b * c);
 				return;
 			}
+
 			float pa = parent.a, pb = parent.b, pc = parent.c, pd = parent.d;
 			float pid = 1 / (pa * pd - pb * pc);
 			float ia = pd * pid, ib = pb * pid, ic = pc * pid, id = pa * pid;
 			float dx = worldX - parent.worldX, dy = worldY - parent.worldY;
-
 			ax = (dx * ia - dy * ib);
 			ay = (dy * id - dx * ic);
 
@@ -330,7 +340,7 @@ namespace Spine {
 				}
 				case TransformMode.NoScale:
 				case TransformMode.NoScaleOrReflection: {
-					float cos = MathUtils.CosDeg(rotation), sin = MathUtils.SinDeg(rotation);
+					float r = rotation * MathUtils.DegRad, cos = (float)Math.Cos(r), sin = (float)Math.Sin(r);
 					pa = (pa * cos + pb * sin) / skeleton.scaleX;
 					pc = (pc * cos + pd * sin) / skeleton.scaleY;
 					float s = (float)Math.Sqrt(pa * pa + pc * pc);
@@ -339,9 +349,9 @@ namespace Spine {
 					pc *= s;
 					s = (float)Math.Sqrt(pa * pa + pc * pc);
 					if (data.transformMode == TransformMode.NoScale && pid < 0 != (skeleton.scaleX < 0 != skeleton.scaleY < 0)) s = -s;
-					float r = MathUtils.PI / 2 + MathUtils.Atan2(pc, pa);
-					pb = MathUtils.Cos(r) * s;
-					pd = MathUtils.Sin(r) * s;
+					r = MathUtils.PI / 2 + MathUtils.Atan2(pc, pa);
+					pb = (float)Math.Cos(r) * s;
+					pd = (float)Math.Sin(r) * s;
 					pid = 1 / (pa * pd - pb * pc);
 					ia = pd * pid;
 					ib = pb * pid;
@@ -361,16 +371,17 @@ namespace Spine {
 			if (ascaleX > 0.0001f) {
 				float det = ra * rd - rb * rc;
 				ascaleY = det / ascaleX;
-				ashearY = -MathUtils.Atan2(ra * rb + rc * rd, det) * MathUtils.RadDeg;
-				arotation = MathUtils.Atan2(rc, ra) * MathUtils.RadDeg;
+				ashearY = -MathUtils.Atan2Deg(ra * rb + rc * rd, det);
+				arotation = MathUtils.Atan2Deg(rc, ra);
 			} else {
 				ascaleX = 0;
 				ascaleY = (float)Math.Sqrt(rb * rb + rd * rd);
 				ashearY = 0;
-				arotation = 90 - MathUtils.Atan2(rd, rb) * MathUtils.RadDeg;
+				arotation = 90 - MathUtils.Atan2Deg(rd, rb);
 			}
 		}
 
+		/// <summary>Transforms a point from world coordinates to the bone's local coordinates.</summary>
 		public void WorldToLocal (float worldX, float worldY, out float localX, out float localY) {
 			float a = this.a, b = this.b, c = this.c, d = this.d;
 			float det = a * d - b * c;
@@ -379,53 +390,60 @@ namespace Spine {
 			localY = (y * a - x * c) / det;
 		}
 
+		/// <summary>Transforms a point from the bone's local coordinates to world coordinates.</summary>
 		public void LocalToWorld (float localX, float localY, out float worldX, out float worldY) {
 			worldX = localX * a + localY * b + this.worldX;
 			worldY = localX * c + localY * d + this.worldY;
 		}
 
-		public float WorldToLocalRotationX {
-			get {
-				Bone parent = this.parent;
-				if (parent == null) return arotation;
-				float pa = parent.a, pb = parent.b, pc = parent.c, pd = parent.d, a = this.a, c = this.c;
-				return MathUtils.Atan2(pa * c - pc * a, pd * a - pb * c) * MathUtils.RadDeg;
+		/// <summary>Transforms a point from world coordinates to the parent bone's local coordinates.</summary>
+		public void WorldToParent (float worldX, float worldY, out float parentX, out float parentY) {
+			if (parent == null) {
+				parentX = worldX;
+				parentY = worldY;
+			} else {
+				parent.WorldToLocal(worldX, worldY, out parentX, out parentY);
 			}
 		}
 
-		public float WorldToLocalRotationY {
-			get {
-				Bone parent = this.parent;
-				if (parent == null) return arotation;
-				float pa = parent.a, pb = parent.b, pc = parent.c, pd = parent.d, b = this.b, d = this.d;
-				return MathUtils.Atan2(pa * d - pc * b, pd * b - pb * d) * MathUtils.RadDeg;
+		/// <summary>Transforms a point from the parent bone's coordinates to world coordinates.</summary>
+		public void ParentToWorld (float parentX, float parentY, out float worldX, out float worldY) {
+			if (parent == null) {
+				worldX = parentX;
+				worldY = parentY;
+			} else {
+				parent.LocalToWorld(parentX, parentY, out worldX, out worldY);
 			}
 		}
 
+		/// <summary>Transforms a world rotation to a local rotation.</summary>
 		public float WorldToLocalRotation (float worldRotation) {
-			float sin = MathUtils.SinDeg(worldRotation), cos = MathUtils.CosDeg(worldRotation);
-			return MathUtils.Atan2(a * sin - c * cos, d * cos - b * sin) * MathUtils.RadDeg + rotation - shearX;
+			worldRotation *= MathUtils.DegRad;
+			float sin = (float)Math.Sin(worldRotation), cos = (float)Math.Cos(worldRotation);
+			return MathUtils.Atan2Deg(a * sin - c * cos, d * cos - b * sin) + rotation - shearX;
 		}
 
+		/// <summary>Transforms a local rotation to a world rotation.</summary>
 		public float LocalToWorldRotation (float localRotation) {
-			localRotation -= rotation - shearX;
-			float sin = MathUtils.SinDeg(localRotation), cos = MathUtils.CosDeg(localRotation);
-			return MathUtils.Atan2(cos * c + sin * d, cos * a + sin * b) * MathUtils.RadDeg;
+			localRotation = (localRotation - rotation - shearX) * MathUtils.DegRad;
+			float sin = (float)Math.Sin(localRotation), cos = (float)Math.Cos(localRotation);
+			return MathUtils.Atan2Deg(cos * c + sin * d, cos * a + sin * b);
 		}
 
 		/// <summary>
 		/// Rotates the world transform the specified amount.
 		/// <para>
-		/// After changes are made to the world transform, <see cref="UpdateAppliedTransform()"/> should be called and <see cref="Update()"/> will
-		/// need to be called on any child bones, recursively.
+		/// After changes are made to the world transform, <see cref="UpdateAppliedTransform()"/> should be called and
+		/// <see cref="Update(Skeleton.Physics)"/> will need to be called on any child bones, recursively.
 		/// </para></summary>
 		public void RotateWorld (float degrees) {
-			float a = this.a, b = this.b, c = this.c, d = this.d;
-			float cos = MathUtils.CosDeg(degrees), sin = MathUtils.SinDeg(degrees);
-			this.a = cos * a - sin * c;
-			this.b = cos * b - sin * d;
-			this.c = sin * a + cos * c;
-			this.d = sin * b + cos * d;
+			degrees *= MathUtils.DegRad;
+			float sin = (float)Math.Sin(degrees), cos = (float)Math.Cos(degrees);
+			float ra = a, rb = b;
+			a = cos * ra - sin * c;
+			b = cos * rb - sin * d;
+			c = sin * ra + cos * c;
+			d = sin * rb + cos * d;
 		}
 
 		override public string ToString () {

+ 3 - 3
spine-csharp/src/BoneData.cs

@@ -56,7 +56,7 @@ namespace Spine {
 		/// <summary>Local Y translation.</summary>
 		public float Y { get { return y; } set { y = value; } }
 
-		/// <summary>Local rotation.</summary>
+		/// <summary>Local rotation in degrees, counter clockwise.</summary>
 		public float Rotation { get { return rotation; } set { rotation = value; } }
 
 		/// <summary>Local scaleX.</summary>
@@ -74,8 +74,8 @@ namespace Spine {
 		/// <summary>The transform mode for how parent world transforms affect this bone.</summary>
 		public TransformMode TransformMode { get { return transformMode; } set { transformMode = value; } }
 
-		///<summary>When true, <see cref="Skeleton.UpdateWorldTransform()"/> only updates this bone if the <see cref="Skeleton.Skin"/> contains this
-		/// bone.</summary>
+		/// <summary>When true, <see cref="Skeleton.UpdateWorldTransform(Skeleton.Physics)"/> only updates this bone if the <see cref="Skeleton.Skin"/> contains
+		/// this bone.</summary>
 		/// <seealso cref="Skin.Bones"/>
 		public bool SkinRequired { get { return skinRequired; } set { skinRequired = value; } }
 

+ 5 - 5
spine-csharp/src/ConstraintData.cs

@@ -45,13 +45,13 @@ namespace Spine {
 		/// <summary> The constraint's name, which is unique across all constraints in the skeleton of the same type.</summary>
 		public string Name { get { return name; } }
 
-		///<summary>The ordinal of this constraint for the order a skeleton's constraints will be applied by
-		/// <see cref="Skeleton.UpdateWorldTransform()"/>.</summary>
+		/// <summary>The ordinal of this constraint for the order a skeleton's constraints will be applied by
+		/// <see cref="Skeleton.UpdateWorldTransform(Skeleton.Physics)"/>.</summary>
 		public int Order { get { return order; } set { order = value; } }
 
-		///<summary>When true, <see cref="Skeleton.UpdateWorldTransform()"/> only updates this constraint if the <see cref="Skeleton.Skin"/> contains
-		/// this constraint.</summary>
-		///<seealso cref="Skin.Constraints"/>
+		/// <summary>When true, <see cref="Skeleton.UpdateWorldTransform(Skeleton.Physics)"/> only updates this constraint if the <see cref="Skeleton.Skin"/>
+		/// contains this constraint.</summary>
+		/// <seealso cref="Skin.Constraints"/>
 		public bool SkinRequired { get { return skinRequired; } set { skinRequired = value; } }
 
 		override public string ToString () {

+ 9 - 4
spine-csharp/src/IUpdatable.cs

@@ -28,15 +28,20 @@
  *****************************************************************************/
 
 namespace Spine {
+	using Physics = Skeleton.Physics;
 
-	///<summary>The interface for items updated by <see cref="Skeleton.UpdateWorldTransform()"/>.</summary>
+	/// <summary>The interface for items updated by <see cref="Skeleton.UpdateWorldTransform(Physics)"/>.</summary>
 	public interface IUpdatable {
-		void Update ();
+		/// <param name="physics">Determines how physics and other non-deterministic updates are applied.</param>
+		void Update (Physics physics);
 
-		///<summary>Returns false when this item has not been updated because a skin is required and the <see cref="Skeleton.Skin">active
-		/// skin</see> does not contain this item.</summary>
+		/// <summary>Returns false when this item won't be updated by
+		/// <see cref="Skeleton.UpdateWorldTransform(Skeleton.Physics)"/> because a skin is required and the
+		/// <see cref="Skeleton.Skin">active skin</see> does not contain this item.</summary>
 		/// <seealso cref="Skin.Bones"/>
 		/// <seealso cref="Skin.Constraints"/>
+		/// <seealso cref="BoneData.SkinRequired"/>
+		/// <seealso cref="ConstraintData.SkinRequired"/>
 		bool Active { get; }
 	}
 }

+ 27 - 15
spine-csharp/src/IkConstraint.cs

@@ -30,6 +30,8 @@
 using System;
 
 namespace Spine {
+	using Physics = Skeleton.Physics;
+
 	/// <summary>
 	/// <para>
 	/// Stores the current pose for an IK constraint. An IK constraint adjusts the rotation of 1 or 2 constrained bones so the tip of
@@ -49,7 +51,6 @@ namespace Spine {
 
 		public IkConstraint (IkConstraintData data, Skeleton skeleton) {
 			if (data == null) throw new ArgumentNullException("data", "data cannot be null.");
-			if (skeleton == null) throw new ArgumentNullException("skeleton", "skeleton cannot be null.");
 			this.data = data;
 			mix = data.mix;
 			softness = data.softness;
@@ -60,18 +61,17 @@ namespace Spine {
 			bones = new ExposedList<Bone>(data.bones.Count);
 			foreach (BoneData boneData in data.bones)
 				bones.Add(skeleton.bones.Items[boneData.index]);
+
 			target = skeleton.bones.Items[data.target.index];
 		}
 
 		/// <summary>Copy constructor.</summary>
-		public IkConstraint (IkConstraint constraint, Skeleton skeleton) {
-			if (constraint == null) throw new ArgumentNullException("constraint cannot be null.");
-			if (skeleton == null) throw new ArgumentNullException("skeleton cannot be null.");
+		public IkConstraint (IkConstraint constraint) {
+			if (constraint == null) throw new ArgumentNullException("constraint", "constraint cannot be null.");
 			data = constraint.data;
 			bones = new ExposedList<Bone>(constraint.Bones.Count);
-			foreach (Bone bone in constraint.Bones)
-				bones.Add(skeleton.Bones.Items[bone.data.index]);
-			target = skeleton.Bones.Items[constraint.target.data.index];
+			bones.AddRange(constraint.Bones);
+			target = constraint.target;
 			mix = constraint.mix;
 			softness = constraint.softness;
 			bendDirection = constraint.bendDirection;
@@ -79,7 +79,16 @@ namespace Spine {
 			stretch = constraint.stretch;
 		}
 
-		public void Update () {
+		public void SetToSetupPose () {
+			IkConstraintData data = this.data;
+			mix = data.mix;
+			softness = data.softness;
+			bendDirection = data.bendDirection;
+			compress = data.compress;
+			stretch = data.stretch;
+		}
+
+		public void Update (Physics physics) {
 			if (mix == 0) return;
 			Bone target = this.target;
 			Bone[] bones = this.bones.Items;
@@ -177,7 +186,7 @@ namespace Spine {
 				float sc = pc / bone.skeleton.scaleY;
 				pb = -sc * s * bone.skeleton.scaleX;
 				pd = sa * s * bone.skeleton.scaleY;
-				rotationIK += (float)Math.Atan2(sc, sa) * MathUtils.RadDeg;
+				rotationIK += MathUtils.Atan2Deg(sc, sa);
 				goto default; // Fall through.
 			}
 			default: {
@@ -194,7 +203,7 @@ namespace Spine {
 			}
 			}
 
-			rotationIK += (float)Math.Atan2(ty, tx) * MathUtils.RadDeg;
+			rotationIK += MathUtils.Atan2Deg(ty, tx);
 			if (bone.ascaleX < 0) rotationIK += 180;
 			if (rotationIK > 180)
 				rotationIK -= 360;
@@ -210,11 +219,14 @@ namespace Spine {
 					ty = targetY - bone.worldY;
 					break;
 				}
-				float b = bone.data.length * sx, dd = (float)Math.Sqrt(tx * tx + ty * ty);
-				if ((compress && dd < b) || (stretch && dd > b) && b > 0.0001f) {
-					float s = (dd / b - 1) * alpha + 1;
-					sx *= s;
-					if (uniform) sy *= s;
+				float b = bone.data.length * sx;
+				if (b > 0.0001f) {
+					float dd = tx * tx + ty * ty;
+					if ((compress && dd < b * b) || (stretch && dd > b * b)) {
+						float s = ((float)Math.Sqrt(dd) / b - 1) * alpha + 1;
+						sx *= s;
+						if (uniform) sy *= s;
+					}
 				}
 			}
 			bone.UpdateWorldTransform(bone.ax, bone.ay, bone.arotation + rotationIK * alpha, sx, sy, bone.ashearX, bone.ashearY);

+ 7 - 0
spine-csharp/src/MathUtils.cs

@@ -35,6 +35,7 @@ namespace Spine {
 	public static class MathUtils {
 		public const float PI = 3.1415927f;
 		public const float PI2 = PI * 2;
+		public const float InvPI2 = 1 / PI2;
 		public const float RadDeg = 180f / PI;
 		public const float DegRad = PI / 180;
 
@@ -115,6 +116,12 @@ namespace Spine {
 			return (float)Math.Cos(degrees * DegRad);
 		}
 
+
+		static public float Atan2Deg (float y, float x) {
+			return (float)Math.Atan2(y, x) * RadDeg;
+		}
+
+
 		/// <summary>Returns the atan2 using Math.Atan2.</summary>
 		static public float Atan2 (float y, float x) {
 			return (float)Math.Atan2(y, x);

+ 19 - 13
spine-csharp/src/PathConstraint.cs

@@ -30,6 +30,7 @@
 using System;
 
 namespace Spine {
+	using Physics = Skeleton.Physics;
 
 	/// <summary>
 	/// <para>
@@ -57,9 +58,11 @@ namespace Spine {
 			if (data == null) throw new ArgumentNullException("data", "data cannot be null.");
 			if (skeleton == null) throw new ArgumentNullException("skeleton", "skeleton cannot be null.");
 			this.data = data;
+
 			bones = new ExposedList<Bone>(data.Bones.Count);
 			foreach (BoneData boneData in data.bones)
 				bones.Add(skeleton.bones.Items[boneData.index]);
+
 			target = skeleton.slots.Items[data.target.index];
 			position = data.position;
 			spacing = data.spacing;
@@ -69,14 +72,12 @@ namespace Spine {
 		}
 
 		/// <summary>Copy constructor.</summary>
-		public PathConstraint (PathConstraint constraint, Skeleton skeleton) {
+		public PathConstraint (PathConstraint constraint) {
 			if (constraint == null) throw new ArgumentNullException("constraint cannot be null.");
-			if (skeleton == null) throw new ArgumentNullException("skeleton cannot be null.");
 			data = constraint.data;
-			bones = new ExposedList<Bone>(constraint.bones.Count);
-			foreach (Bone bone in constraint.bones)
-				bones.Add(skeleton.bones.Items[bone.data.index]);
-			target = skeleton.slots.Items[constraint.target.data.index];
+			bones = new ExposedList<Bone>(constraint.Bones.Count);
+			bones.AddRange(constraint.Bones);
+			target = constraint.target;
 			position = constraint.position;
 			spacing = constraint.spacing;
 			mixRotate = constraint.mixRotate;
@@ -89,7 +90,16 @@ namespace Spine {
 				a[i] = val;
 		}
 
-		public void Update () {
+		public void SetToSetupPose () {
+			PathConstraintData data = this.data;
+			position = data.position;
+			spacing = data.spacing;
+			mixRotate = data.mixRotate;
+			mixX = data.mixX;
+			mixY = data.mixY;
+		}
+
+		public void Update (Physics physics) {
 			PathAttachment attachment = target.Attachment as PathAttachment;
 			if (attachment == null) return;
 
@@ -108,12 +118,8 @@ namespace Spine {
 					for (int i = 0, n = spacesCount - 1; i < n; i++) {
 						Bone bone = bonesItems[i];
 						float setupLength = bone.data.length;
-						if (setupLength < PathConstraint.Epsilon)
-							lengths[i] = 0;
-						else {
-							float x = setupLength * bone.a, y = setupLength * bone.c;
-							lengths[i] = (float)Math.Sqrt(x * x + y * y);
-						}
+						float x = setupLength * bone.a, y = setupLength * bone.c;
+						lengths[i] = (float)Math.Sqrt(x * x + y * y);
 					}
 				}
 				ArraysFill(spaces, 1, spacesCount, spacing);

+ 225 - 44
spine-csharp/src/PhysicsConstraint.cs

@@ -1,16 +1,17 @@
+
 /******************************************************************************
  * Spine Runtimes License Agreement
- * Last updated September 24, 2021. Replaces all prior versions.
+ * Last updated July 28, 2023. Replaces all prior versions.
  *
- * Copyright (c) 2013-2021, Esoteric Software LLC
+ * Copyright (c) 2013-2023, 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,
+ * 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.
@@ -23,13 +24,15 @@
  * (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.
+ * (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.
  *****************************************************************************/
 
 using System;
 
 namespace Spine {
+	using Physics = Skeleton.Physics;
+
 	/// <summary>
 	/// Stores the current pose for a physics constraint. A physics constraint applies physics to bones.
 	/// <para>
@@ -37,66 +40,244 @@ namespace Spine {
 	/// </summary>
 	public class PhysicsConstraint : IUpdatable {
 		internal readonly PhysicsConstraintData data;
-		internal readonly ExposedList<Bone> bones;
-		// BOZO! - stiffness -> strength. stiffness, damping, rope, stretch -> move to spring.
-		internal float mix, friction, gravity, wind, stiffness, damping;
-		internal bool rope, stretch;
+		public Bone bone;
+		internal float inertia, strength, damping, massInverse, wind, gravity, mix;
+
+		bool reset = true;
+		float ux, uy, cx, cy, tx, ty;
+		float xOffset, xVelocity;
+		float yOffset, yVelocity;
+		float rotateOffset, rotateVelocity;
+		float scaleOffset, scaleVelocity;
 
 		internal bool active;
 
+		readonly Skeleton skeleton;
+		float remaining, lastTime;
+
 		public PhysicsConstraint (PhysicsConstraintData data, Skeleton skeleton) {
 			if (data == null) throw new ArgumentNullException("data", "data cannot be null.");
 			if (skeleton == null) throw new ArgumentNullException("skeleton", "skeleton cannot be null.");
 			this.data = data;
-			mix = data.mix;
-			friction = data.friction;
-			gravity = data.gravity;
-			wind = data.wind;
-			stiffness = data.stiffness;
+			this.skeleton = skeleton;
+			bone = skeleton.bones.Items[data.bone.index];
+			inertia = data.inertia;
+			strength = data.strength;
 			damping = data.damping;
-			rope = data.rope;
-			stretch = data.stretch;
-
-			bones = new ExposedList<Bone>(data.Bones.Count);
-			foreach (BoneData boneData in data.bones)
-				bones.Add(skeleton.bones.Items[boneData.index]);
+			massInverse = data.massInverse;
+			wind = data.wind;
+			gravity = data.gravity;
+			mix = data.mix;
 		}
 
-		/// <summary>Copy constructor.</summary>
-		public PhysicsConstraint (PhysicsConstraint constraint, Skeleton skeleton) {
+		/** Copy constructor. */
+		public PhysicsConstraint (PhysicsConstraint constraint) {
 			if (constraint == null) throw new ArgumentNullException("constraint", "constraint cannot be null.");
-			if (skeleton == null) throw new ArgumentNullException("skeleton", "skeleton cannot be null.");
 			data = constraint.data;
-			bones = new ExposedList<Bone>(constraint.bones.Count);
-			foreach (Bone bone in constraint.bones)
-				bones.Add(skeleton.bones.Items[bone.data.index]);
-			mix = constraint.mix;
-			friction = constraint.friction;
-			gravity = constraint.gravity;
-			wind = constraint.wind;
-			stiffness = constraint.stiffness;
+			skeleton = constraint.skeleton;
+			bone = constraint.bone;
+			inertia = constraint.inertia;
+			strength = constraint.strength;
 			damping = constraint.damping;
-			rope = constraint.rope;
-			stretch = constraint.stretch;
+			massInverse = constraint.massInverse;
+			wind = constraint.wind;
+			gravity = constraint.gravity;
+			mix = constraint.mix;
+		}
+
+		public void Reset () {
+			remaining = 0;
+			lastTime = skeleton.time;
+			reset = true;
+			xOffset = 0;
+			xVelocity = 0;
+			yOffset = 0;
+			yVelocity = 0;
+			rotateOffset = 0;
+			rotateVelocity = 0;
+			scaleOffset = 0;
+			scaleVelocity = 0;
+		}
+
+		public void SetToSetupPose () {
+			PhysicsConstraintData data = this.data;
+			inertia = data.inertia;
+			strength = data.strength;
+			damping = data.damping;
+			massInverse = data.massInverse;
+			wind = data.wind;
+			gravity = data.gravity;
+			mix = data.mix;
 		}
 
 		/// <summary>Applies the constraint to the constrained bones.</summary>
-		public void Update () {
+		public void Update (Physics physics) {
+			float mix = this.mix;
+			if (mix == 0) return;
+
+			bool x = data.x > 0, y = data.y > 0, rotateOrShearX = data.rotate > 0 || data.shearX > 0, scaleX = data.scaleX > 0;
+			Bone bone = this.bone;
+			float l = bone.data.length;
+
+			switch (physics) {
+			case Physics.None:
+				return;
+			case Physics.Reset:
+				Reset();
+				goto case Physics.Update; // Fall through.
+			case Physics.Update:
+				remaining += Math.Max(skeleton.time - lastTime, 0);
+				lastTime = skeleton.time;
 
+				float bx = bone.worldX, by = bone.worldY;
+				if (reset) {
+					reset = false;
+					ux = bx;
+					uy = by;
+				} else {
+					float remaining = this.remaining, i = inertia, step = data.step;
+					if (x || y) {
+						if (x) {
+							xOffset += (ux - bx) * i;
+							ux = bx;
+						}
+						if (y) {
+							yOffset += (uy - by) * i;
+							uy = by;
+						}
+						if (remaining >= step) {
+							float m = massInverse * step, e = strength, w = wind * 100, g = gravity * -100;
+							float d = (float)Math.Pow(damping, 60 * step);
+							do {
+								if (x) {
+									xVelocity += (w - xOffset * e) * m;
+									xOffset += xVelocity * step;
+									xVelocity *= d;
+								}
+								if (y) {
+									yVelocity += (g - yOffset * e) * m;
+									yOffset += yVelocity * step;
+									yVelocity *= d;
+								}
+								remaining -= step;
+							} while (remaining >= step);
+						}
+						if (x) bone.worldX += xOffset * mix * data.x;
+						if (y) bone.worldY += yOffset * mix * data.y;
+					}
+					if (rotateOrShearX || scaleX) {
+						float ca = (float)Math.Atan2(bone.c, bone.a), c, s;
+						if (rotateOrShearX) {
+							float dx = cx - bone.worldX, dy = cy - bone.worldY, r = (float)Math.Atan2(dy + ty, dx + tx) - ca - rotateOffset * mix;
+							rotateOffset += (r - (float)Math.Ceiling(r * MathUtils.InvPI2 - 0.5f) * MathUtils.PI2) * i;
+							r = rotateOffset * mix + ca;
+							c = (float)Math.Cos(r);
+							s = (float)Math.Sin(r);
+							if (scaleX) {
+								r = l * bone.WorldScaleX;
+								if (r > 0) scaleOffset += (dx * c + dy * s) * i / r;
+							}
+						} else {
+							c = (float)Math.Cos(ca);
+							s = (float)Math.Sin(ca);
+							float r = l * bone.WorldScaleX;
+							if (r > 0) scaleOffset += ((cx - bone.worldX) * c + (cy - bone.worldY) * s) * i / r;
+						}
+						remaining = this.remaining;
+						if (remaining >= step) {
+							float m = massInverse * step, e = strength, w = wind, g = gravity;
+							float d = (float)Math.Pow(damping, 60 * step);
+							while (true) {
+								remaining -= step;
+								if (scaleX) {
+									scaleVelocity += (w * c - g * s - scaleOffset * e) * m;
+									scaleOffset += scaleVelocity * step;
+									scaleVelocity *= d;
+								}
+								if (rotateOrShearX) {
+									rotateVelocity += (-0.01f * l * (w * s + g * c) - rotateOffset * e) * m;
+									rotateOffset += rotateVelocity * step;
+									rotateVelocity *= d;
+									if (remaining < step) break;
+									float r = rotateOffset * mix + ca;
+									c = (float)Math.Cos(r);
+									s = (float)Math.Sin(r);
+								} else if (remaining < step) //
+									break;
+							}
+						}
+					}
+					this.remaining = remaining;
+				}
+				cx = bone.worldX;
+				cy = bone.worldY;
+				break;
+			case Physics.Pose:
+				if (x) bone.worldX += xOffset * mix * data.x;
+				if (y) bone.worldY += yOffset * mix * data.y;
+				break;
+			}
+
+			if (rotateOrShearX) {
+				float o = rotateOffset * mix, s, c, a;
+				if (data.shearX > 0) {
+					float r = 0;
+					if (data.rotate > 0) {
+						r = o * data.rotate;
+						s = (float)Math.Sin(r);
+						c = (float)Math.Cos(r);
+						a = bone.b;
+						bone.b = c * a - s * bone.d;
+						bone.d = s * a + c * bone.d;
+					}
+					r += o * data.shearX;
+					s = (float)Math.Sin(r);
+					c = (float)Math.Cos(r);
+					a = bone.a;
+					bone.a = c * a - s * bone.c;
+					bone.c = s * a + c * bone.c;
+				} else {
+					o *= data.rotate;
+					s = (float)Math.Sin(o);
+					c = (float)Math.Cos(o);
+					a = bone.a;
+					bone.a = c * a - s * bone.c;
+					bone.c = s * a + c * bone.c;
+					a = bone.b;
+					bone.b = c * a - s * bone.d;
+					bone.d = s * a + c * bone.d;
+				}
+			}
+			if (scaleX) {
+				float s = 1 + scaleOffset * mix * data.scaleX;
+				bone.a *= s;
+				bone.c *= s;
+			}
+			if (physics != Physics.Pose) {
+				tx = l * bone.a;
+				ty = l * bone.c;
+			}
+			bone.UpdateAppliedTransform();
 		}
 
-		/// <summary>The bones that will be modified by this physics constraint.</summary>
-		public ExposedList<Bone> Bones { get { return bones; } }
+		/// <summary>The bone constrained by this physics constraint.</summary>
+		public Bone Bone { get {return bone;} set { bone = value; } }
+		public float Inertia { get { return inertia; } set { inertia = value; } }
+		public float Strength { get { return strength; } set { strength = value; } }
+		public float Damping { get { return damping; } set { damping = value; } }
+		public float MassInverse { get { return massInverse; } set { massInverse = value; } }
+		public float Wind { get { return wind; } set { wind = value; } }
+		public float Gravity { get { return gravity; } set { gravity = value; } }
 		/// <summary>A percentage (0-1) that controls the mix between the constrained and unconstrained poses.</summary>
 		public float Mix { get { return mix; } set { mix = value; } }
-		public float Friction { get { return friction; } set { friction = value; } }
-		public float Gravity { get { return gravity; } set { gravity = value; } }
-		public float Wind { get { return wind; } set { wind = value; } }
-		public float Stiffness { get { return stiffness; } set { stiffness = value; } }
-		public float Damping { get { return damping; } set { damping = value; } }
-		public bool Rope { get { return rope; } set { rope = value; } }
-		public bool Stretch { get { return stretch; } set { stretch = value; } }
 		public bool Active { get { return active; } }
+
+
+		/** The physics constraint's setup pose data. */
+		public PhysicsConstraintData getData () {
+			return data;
+		}
+
 		/// <summary>The physics constraint's setup pose data.</summary>
 		public PhysicsConstraintData Data { get { return data; } }
 

+ 31 - 20
spine-csharp/src/PhysicsConstraintData.cs

@@ -1,16 +1,16 @@
 /******************************************************************************
  * Spine Runtimes License Agreement
- * Last updated September 24, 2021. Replaces all prior versions.
+ * Last updated July 28, 2023. Replaces all prior versions.
  *
- * Copyright (c) 2013-2021, Esoteric Software LLC
+ * Copyright (c) 2013-2023, 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,
+ * 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.
@@ -23,12 +23,10 @@
  * (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.
+ * (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.
  *****************************************************************************/
 
-using System;
-
 namespace Spine {
 	/// <summary>
 	/// Stores the setup pose for a <see cref="PhysicsConstraint"/>.
@@ -36,24 +34,37 @@ namespace Spine {
 	/// See <a href="http://esotericsoftware.com/spine-physics-constraints">Physics constraints</a> in the Spine User Guide.</para>
 	/// </summary>
 	public class PhysicsConstraintData : ConstraintData {
-		internal ExposedList<BoneData> bones = new ExposedList<BoneData>();
-		internal float mix, friction, gravity, wind, stiffness, damping;
-		internal bool rope, stretch;
+		internal BoneData bone;
+		internal float x, y, rotate, scaleX, shearX;
+		internal float step, inertia, strength, damping, massInverse, wind, gravity, mix;
+		internal bool inertiaGlobal, strengthGlobal, dampingGlobal, massGlobal, windGlobal, gravityGlobal, mixGlobal;
 
 		public PhysicsConstraintData (string name) : base(name) {
 		}
 
-		/// <summary>The bones that are constrained by this physics constraint.</summary>
-		public ExposedList<BoneData> Bones { get { return bones; } }
+		/// <summary>The bone constrained by this physics constraint.</summary>
+		public BoneData Bone { get { return bone; } }
 
+		public float Step { get { return step; } set { step = value; } }
+		public float X { get { return x; } set { x = value; } }
+		public float Y { get { return y; } set { y = value; } }
+		public float Rotate { get { return rotate; } set { rotate = value; } }
+		public float ScaleX { get { return scaleX; } set { scaleX = value; } }
+		public float ShearX { get { return shearX; } set { shearX = value; } }
+		public float Inertia { get { return inertia; } set { inertia = value; } }
+		public float Strength { get { return strength; } set { strength = value; } }
+		public float Damping { get { return damping; } set { damping = value; } }
+		public float MassInverse { get { return massInverse; } set { massInverse = value; } }
+		public float Wind { get { return wind; } set { wind = value; } }
+		public float Gravity { get { return gravity; } set { gravity = value; } }
 		/// <summary>A percentage (0-1) that controls the mix between the constrained and unconstrained poses.</summary>
 		public float Mix { get { return mix; } set { mix = value; } }
-		public float Friction { get { return friction; } set { friction = value; } }
-		public float Gravity { get { return gravity; } set { gravity = value; } }
-		public float Wind { get { return wind; } set { wind = value; } }
-		public float Stiffness { get { return stiffness; } set { stiffness = value; } }
-		public float Damping { get { return damping; } set { damping = value; } }
-		public bool Rope { get { return rope; } set { rope = value; } }
-		public bool Stretch { get { return stretch; } set { stretch = value; } }
+		public bool InertiaGlobal { get { return inertiaGlobal; } set { inertiaGlobal = value; } }
+		public bool StrengthGlobal { get { return strengthGlobal; } set { strengthGlobal = value; } }
+		public bool DampingGlobal { get { return dampingGlobal; } set { dampingGlobal = value; } }
+		public bool MassGlobal { get { return massGlobal; } set { massGlobal = value; } }
+		public bool WindGlobal { get { return windGlobal; } set { windGlobal = value; } }
+		public bool GravityGlobal { get { return gravityGlobal; } set { gravityGlobal = value; } }
+		public bool MixGlobal { get { return mixGlobal; } set { mixGlobal = value; } }
 	}
 }

+ 61 - 62
spine-csharp/src/Skeleton.cs

@@ -42,8 +42,9 @@ namespace Spine {
 		internal ExposedList<IUpdatable> updateCache = new ExposedList<IUpdatable>();
 		internal Skin skin;
 		internal float r = 1, g = 1, b = 1, a = 1;
-		internal float scaleX = 1, scaleY = 1;
 		internal float x, y;
+		internal float scaleX = 1, scaleY = 1;
+		internal float time;
 
 		/// <summary>The skeleton's setup pose data.</summary>
 		public SkeletonData Data { get { return data; } }
@@ -99,6 +100,9 @@ namespace Spine {
 
 		[Obsolete("Use ScaleY instead. FlipY is when ScaleY is negative.")]
 		public bool FlipY { get { return scaleY < 0; } set { scaleY = value ? -1f : 1f; } }
+		/// <summary>Returns the skeleton's time. This is used for time-based manipulations, such as <see cref="PhysicsConstraint"/>.</summary>
+		/// <seealso cref="Update(float)"/>
+		public float Time { get { return time; } set { time = value; } }
 
 		/// <summary>Returns the root bone, or null if the skeleton has no bones.</summary>
 		public Bone RootBone {
@@ -183,27 +187,30 @@ namespace Spine {
 
 			ikConstraints = new ExposedList<IkConstraint>(skeleton.ikConstraints.Count);
 			foreach (IkConstraint ikConstraint in skeleton.ikConstraints)
-				ikConstraints.Add(new IkConstraint(ikConstraint, this));
+				ikConstraints.Add(new IkConstraint(ikConstraint));
 
 			transformConstraints = new ExposedList<TransformConstraint>(skeleton.transformConstraints.Count);
 			foreach (TransformConstraint transformConstraint in skeleton.transformConstraints)
-				transformConstraints.Add(new TransformConstraint(transformConstraint, this));
+				transformConstraints.Add(new TransformConstraint(transformConstraint));
 
 			pathConstraints = new ExposedList<PathConstraint>(skeleton.pathConstraints.Count);
 			foreach (PathConstraint pathConstraint in skeleton.pathConstraints)
-				pathConstraints.Add(new PathConstraint(pathConstraint, this));
+				pathConstraints.Add(new PathConstraint(pathConstraint));
 
 			physicsConstraints = new ExposedList<PhysicsConstraint>(skeleton.physicsConstraints.Count);
 			foreach (PhysicsConstraint physicsConstraint in skeleton.physicsConstraints)
-				physicsConstraints.Add(new PhysicsConstraint(physicsConstraint, this));
+				physicsConstraints.Add(new PhysicsConstraint(physicsConstraint));
 
 			skin = skeleton.skin;
 			r = skeleton.r;
 			g = skeleton.g;
 			b = skeleton.b;
 			a = skeleton.a;
+			x = skeleton.x;
+			y = skeleton.y;
 			scaleX = skeleton.scaleX;
 			scaleY = skeleton.scaleY;
+			time = skeleton.time;
 
 			UpdateCache();
 		}
@@ -383,17 +390,16 @@ namespace Spine {
 			constraint.active = !constraint.data.skinRequired || (skin != null && skin.constraints.Contains(constraint.data));
 			if (!constraint.active) return;
 
-			Object[] constrained = constraint.bones.Items;
-			int boneCount = constraint.bones.Count;
-			for (int i = 0; i < boneCount; i++)
-				SortBone((Bone)constrained[i]);
+			Bone bone = constraint.bone;
+			constraint.active = bone.active;
+			if (!constraint.active) return;
+
+			SortBone(bone);
 
 			updateCache.Add(constraint);
 
-			for (int i = 0; i < boneCount; i++)
-				SortReset(((Bone)constrained[i]).children);
-			for (int i = 0; i < boneCount; i++)
-				((Bone)constrained[i]).sorted = true;
+			SortReset(bone.children);
+			bone.sorted = true;
 		}
 
 		private void SortBone (Bone bone) {
@@ -420,7 +426,7 @@ namespace Spine {
 		/// See <a href="http://esotericsoftware.com/spine-runtime-skeletons#World-transforms">World transforms</a> in the Spine
 		/// Runtimes Guide.</para>
 		/// </summary>
-		public void UpdateWorldTransform () {
+		public void UpdateWorldTransform (Physics physics) {
 			Bone[] bones = this.bones.Items;
 			for (int i = 0, n = this.bones.Count; i < n; i++) {
 				Bone bone = bones[i];
@@ -435,14 +441,14 @@ namespace Spine {
 
 			IUpdatable[] updateCache = this.updateCache.Items;
 			for (int i = 0, n = this.updateCache.Count; i < n; i++)
-				updateCache[i].Update();
+				updateCache[i].Update(physics);
 		}
 
 		/// <summary>
 		/// Temporarily sets the root bone as a child of the specified bone, then updates the world transform for each bone and applies
 		/// all constraints.
 		/// </summary>
-		public void UpdateWorldTransform (Bone parent) {
+		public void UpdateWorldTransform (Physics physics, Bone parent) {
 			if (parent == null) throw new ArgumentNullException("parent", "parent cannot be null.");
 
 			// Apply the parent bone transform to the root bone. The root bone always inherits scale, rotation and reflection.
@@ -451,11 +457,12 @@ namespace Spine {
 			rootBone.worldX = pa * x + pb * y + parent.worldX;
 			rootBone.worldY = pc * x + pd * y + parent.worldY;
 
-			float rotationY = rootBone.rotation + 90 + rootBone.shearY;
-			float la = MathUtils.CosDeg(rootBone.rotation + rootBone.shearX) * rootBone.scaleX;
-			float lb = MathUtils.CosDeg(rotationY) * rootBone.scaleY;
-			float lc = MathUtils.SinDeg(rootBone.rotation + rootBone.shearX) * rootBone.scaleX;
-			float ld = MathUtils.SinDeg(rotationY) * rootBone.scaleY;
+			float rx = (rootBone.rotation + rootBone.shearX) * MathUtils.DegRad;
+			float ry = (rootBone.rotation + 90 + rootBone.shearY) * MathUtils.DegRad;
+			float la = (float)Math.Cos(rx) * rootBone.scaleX;
+			float lb = (float)Math.Cos(ry) * rootBone.scaleY;
+			float lc = (float)Math.Sin(rx) * rootBone.scaleX;
+			float ld = (float)Math.Sin(ry) * rootBone.scaleY;
 			rootBone.a = (pa * la + pb * lc) * scaleX;
 			rootBone.b = (pa * lb + pb * ld) * scaleX;
 			rootBone.c = (pc * la + pd * lc) * scaleY;
@@ -465,10 +472,15 @@ namespace Spine {
 			IUpdatable[] updateCache = this.updateCache.Items;
 			for (int i = 0, n = this.updateCache.Count; i < n; i++) {
 				IUpdatable updatable = updateCache[i];
-				if (updatable != rootBone) updatable.Update();
+				if (updatable != rootBone) updatable.Update(physics);
 			}
 		}
 
+		/// <summary>Increments the skeleton's <see cref="time"/>.</summary>
+		public void Update (float delta) {
+			time += delta;
+		}
+
 		/// <summary>Sets the bones, constraints, and slots to their setup pose values.</summary>
 		public void SetToSetupPose () {
 			SetBonesToSetupPose();
@@ -482,52 +494,20 @@ namespace Spine {
 				bones[i].SetToSetupPose();
 
 			IkConstraint[] ikConstraints = this.ikConstraints.Items;
-			for (int i = 0, n = this.ikConstraints.Count; i < n; i++) {
-				IkConstraint constraint = ikConstraints[i];
-				IkConstraintData data = constraint.data;
-				constraint.mix = data.mix;
-				constraint.softness = data.softness;
-				constraint.bendDirection = data.bendDirection;
-				constraint.compress = data.compress;
-				constraint.stretch = data.stretch;
-			}
+			for (int i = 0, n = this.ikConstraints.Count; i < n; i++)
+				ikConstraints[i].SetToSetupPose();
 
 			TransformConstraint[] transformConstraints = this.transformConstraints.Items;
-			for (int i = 0, n = this.transformConstraints.Count; i < n; i++) {
-				TransformConstraint constraint = transformConstraints[i];
-				TransformConstraintData data = constraint.data;
-				constraint.mixRotate = data.mixRotate;
-				constraint.mixX = data.mixX;
-				constraint.mixY = data.mixY;
-				constraint.mixScaleX = data.mixScaleX;
-				constraint.mixScaleY = data.mixScaleY;
-				constraint.mixShearY = data.mixShearY;
-			}
+			for (int i = 0, n = this.transformConstraints.Count; i < n; i++)
+				transformConstraints[i].SetToSetupPose();
 
 			PathConstraint[] pathConstraints = this.pathConstraints.Items;
-			for (int i = 0, n = this.pathConstraints.Count; i < n; i++) {
-				PathConstraint constraint = pathConstraints[i];
-				PathConstraintData data = constraint.data;
-				constraint.position = data.position;
-				constraint.spacing = data.spacing;
-				constraint.mixRotate = data.mixRotate;
-				constraint.mixX = data.mixX;
-				constraint.mixY = data.mixY;
-			}
+			for (int i = 0, n = this.pathConstraints.Count; i < n; i++)
+				pathConstraints[i].SetToSetupPose();
 
 			PhysicsConstraint[] physicsConstraints = this.physicsConstraints.Items;
-			for (int i = 0, n = this.physicsConstraints.Count; i < n; i++) {
-				PhysicsConstraint constraint = physicsConstraints[i];
-				PhysicsConstraintData data = constraint.data;
-				constraint.mix = data.mix;
-				constraint.friction = data.friction;
-				constraint.gravity = data.gravity;
-				constraint.wind = data.wind;
-				constraint.stiffness = data.stiffness;
-				constraint.damping = data.damping;
-				constraint.rope = data.rope;
-				constraint.stretch = data.stretch;
-			}
+			for (int i = 0, n = this.physicsConstraints.Count; i < n; i++)
+				physicsConstraints[i].SetToSetupPose();
 		}
 
 		public void SetSlotsToSetupPose () {
@@ -743,5 +723,24 @@ namespace Spine {
 			height = maxY - minY;
 			vertexBuffer = temp;
 		}
+
+		override public string ToString () {
+			return data.name;
+		}
+
+		/// <summary>Determines how physics and other non-deterministic updates are applied.</summary>
+		public enum Physics {
+			/// <summary>Physics are not updated or applied.</summary>
+			None,
+
+			/// <summary>Physics are reset to the current pose.</summary>
+			Reset,
+
+			/// <summary>Physics are updated and the pose from physics is applied.</summary>
+			Update,
+
+			/// <summary>Physics are not updated but the pose from physics is applied.</summary>
+			Pose
+		}
 	}
 }

+ 191 - 87
spine-csharp/src/SkeletonBinary.cs

@@ -68,10 +68,21 @@ namespace Spine {
 		public const int PATH_SPACING = 1;
 		public const int PATH_MIX = 2;
 
+		public const int PHYSICS_INERTIA = 0;
+		public const int PHYSICS_STRENGTH = 1;
+		public const int PHYSICS_DAMPING = 2;
+		public const int PHYSICS_MASS = 4;
+		public const int PHYSICS_WIND = 5;
+		public const int PHYSICS_GRAVITY = 6;
+		public const int PHYSICS_MIX = 7;
+		public const int PHYSICS_RESET = 8;
+
 		public const int CURVE_LINEAR = 0;
 		public const int CURVE_STEPPED = 1;
 		public const int CURVE_BEZIER = 2;
 
+		private readonly List<LinkedMesh> linkedMeshes = new List<LinkedMesh>();
+
 		public SkeletonBinary (AttachmentLoader attachmentLoader)
 			: base(attachmentLoader) {
 		}
@@ -177,7 +188,11 @@ namespace Spine {
 				data.Length = input.ReadFloat() * scale;
 				data.transformMode = TransformModeValues[input.ReadInt(true)];
 				data.skinRequired = input.ReadBoolean();
-				if (nonessential) input.ReadInt(); // Skip bone color.
+				if (nonessential) { // discard non-essential data
+					input.ReadInt(); // Color.rgba8888ToColor(data.color, input.readInt());
+					input.ReadString(); // data.icon = input.readString();
+					input.ReadBoolean(); // data.visible = input.readBoolean();
+				}
 				bones[i] = data;
 			}
 
@@ -203,6 +218,7 @@ namespace Spine {
 
 				slotData.attachmentName = input.ReadStringRef();
 				slotData.blendMode = (BlendMode)input.ReadInt(true);
+				if (nonessential) input.ReadBoolean(); // if (nonessential) data.visible = input.readBoolean();
 				slots[i] = slotData;
 			}
 
@@ -211,17 +227,18 @@ namespace Spine {
 			for (int i = 0, nn; i < n; i++) {
 				IkConstraintData data = new IkConstraintData(input.ReadString());
 				data.order = input.ReadInt(true);
-				data.skinRequired = input.ReadBoolean();
 				BoneData[] constraintBones = data.bones.Resize(nn = input.ReadInt(true)).Items;
 				for (int ii = 0; ii < nn; ii++)
 					constraintBones[ii] = bones[input.ReadInt(true)];
 				data.target = bones[input.ReadInt(true)];
 				data.mix = input.ReadFloat();
 				data.softness = input.ReadFloat() * scale;
-				data.bendDirection = input.ReadSByte();
-				data.compress = input.ReadBoolean();
-				data.stretch = input.ReadBoolean();
-				data.uniform = input.ReadBoolean();
+				int flags = input.Read();
+				data.skinRequired = (flags & 1) != 0;
+				data.bendDirection = (flags & 2) != 0 ? 1 : -1;
+				data.compress = (flags & 4) != 0;
+				data.stretch = (flags & 8) != 0;
+				data.uniform = (flags & 16) != 0;
 				o[i] = data;
 			}
 
@@ -230,13 +247,14 @@ namespace Spine {
 			for (int i = 0, nn; i < n; i++) {
 				TransformConstraintData data = new TransformConstraintData(input.ReadString());
 				data.order = input.ReadInt(true);
-				data.skinRequired = input.ReadBoolean();
 				BoneData[] constraintBones = data.bones.Resize(nn = input.ReadInt(true)).Items;
 				for (int ii = 0; ii < nn; ii++)
 					constraintBones[ii] = bones[input.ReadInt(true)];
 				data.target = bones[input.ReadInt(true)];
-				data.local = input.ReadBoolean();
-				data.relative = input.ReadBoolean();
+				int flags = input.Read();
+				data.skinRequired = (flags & 1) != 0;
+				data.local = (flags & 2) != 0;
+				data.relative = (flags & 4) != 0;
 				data.offsetRotation = input.ReadFloat();
 				data.offsetX = input.ReadFloat() * scale;
 				data.offsetY = input.ReadFloat() * scale;
@@ -258,7 +276,7 @@ namespace Spine {
 				PathConstraintData data = new PathConstraintData(input.ReadString());
 				data.order = input.ReadInt(true);
 				data.skinRequired = input.ReadBoolean();
-				Object[] constraintBones = data.bones.Resize(nn = input.ReadInt(true)).Items;
+				BoneData[] constraintBones = data.bones.Resize(nn = input.ReadInt(true)).Items;
 				for (int ii = 0; ii < nn; ii++)
 					constraintBones[ii] = bones[input.ReadInt(true)];
 				data.target = slots[input.ReadInt(true)];
@@ -276,6 +294,38 @@ namespace Spine {
 				o[i] = data;
 			}
 
+			// Physics constraints.
+			o = skeletonData.physicsConstraints.Resize(n = input.ReadInt(true)).Items;
+			for (int i = 0; i < n; i++) {
+				PhysicsConstraintData data = new PhysicsConstraintData(input.ReadString());
+				data.order = input.ReadInt(true);
+				data.bone = bones[input.ReadInt(true)];
+				int flags = input.Read();
+				data.skinRequired = (flags & 1) != 0;
+				if ((flags & 2) != 0) data.x = input.ReadFloat();
+				if ((flags & 4) != 0) data.y = input.ReadFloat();
+				if ((flags & 8) != 0) data.rotate = input.ReadFloat();
+				if ((flags & 16) != 0) data.scaleX = input.ReadFloat();
+				if ((flags & 32) != 0) data.shearX = input.ReadFloat();
+				data.step = 1f / input.ReadByte();
+				data.inertia = input.ReadFloat();
+				data.strength = input.ReadFloat();
+				data.damping = input.ReadFloat();
+				data.massInverse = input.ReadFloat();
+				data.wind = input.ReadFloat();
+				data.gravity = input.ReadFloat();
+				data.mix = input.ReadFloat();
+				flags = input.Read();
+				if ((flags & 1) != 0) data.inertiaGlobal = true;
+				if ((flags & 2) != 0) data.strengthGlobal = true;
+				if ((flags & 4) != 0) data.dampingGlobal = true;
+				if ((flags & 8) != 0) data.massGlobal = true;
+				if ((flags & 16) != 0) data.windGlobal = true;
+				if ((flags & 32) != 0) data.gravityGlobal = true;
+				if ((flags & 64) != 0) data.mixGlobal = true;
+				o[i] = data;
+			}
+
 			// Default skin.
 			Skin defaultSkin = ReadSkin(input, skeletonData, true, nonessential);
 			if (defaultSkin != null) {
@@ -295,8 +345,7 @@ namespace Spine {
 			n = linkedMeshes.Count;
 			for (int i = 0; i < n; i++) {
 				LinkedMesh linkedMesh = linkedMeshes[i];
-				Skin skin = linkedMesh.skin == null ? skeletonData.DefaultSkin : skeletonData.FindSkin(linkedMesh.skin);
-				if (skin == null) throw new Exception("Skin not found: " + linkedMesh.skin);
+				Skin skin = skeletonData.skins.Items[linkedMesh.skinIndex];
 				Attachment parent = skin.GetAttachment(linkedMesh.slotIndex, linkedMesh.parent);
 				if (parent == null) throw new Exception("Parent mesh not found: " + linkedMesh.parent);
 				linkedMesh.mesh.TimelineAttachment = linkedMesh.inheritTimelines ? (VertexAttachment)parent : linkedMesh.mesh;
@@ -308,7 +357,7 @@ namespace Spine {
 			// Events.
 			o = skeletonData.events.Resize(n = input.ReadInt(true)).Items;
 			for (int i = 0; i < n; i++) {
-				EventData data = new EventData(input.ReadStringRef());
+				EventData data = new EventData(input.ReadString());
 				data.Int = input.ReadInt(false);
 				data.Float = input.ReadFloat();
 				data.String = input.ReadString();
@@ -339,7 +388,10 @@ namespace Spine {
 				if (slotCount == 0) return null;
 				skin = new Skin("default");
 			} else {
-				skin = new Skin(input.ReadStringRef());
+				skin = new Skin(input.ReadString());
+
+				if (nonessential) input.ReadInt(); // discard, Color.rgba8888ToColor(skin.color, input.readInt());
+
 				Object[] bones = skin.bones.Resize(input.ReadInt(true)).Items;
 				BoneData[] bonesItems = skeletonData.bones.Items;
 				for (int i = 0, n = skin.bones.Count; i < n; i++)
@@ -354,6 +406,9 @@ namespace Spine {
 				PathConstraintData[] pathConstraintsItems = skeletonData.pathConstraints.Items;
 				for (int i = 0, n = input.ReadInt(true); i < n; i++)
 					skin.constraints.Add(pathConstraintsItems[input.ReadInt(true)]);
+				PhysicsConstraintData[] physicsConstraintsItems = skeletonData.physicsConstraints.Items;
+				for (int i = 0, n = input.ReadInt(true); i < n; i++)
+					skin.constraints.Add(physicsConstraintsItems[input.ReadInt(true)]);
 				skin.constraints.TrimExcess();
 
 				slotCount = input.ReadInt(true);
@@ -373,12 +428,14 @@ namespace Spine {
 			String attachmentName, bool nonessential) {
 			float scale = this.scale;
 
-			String name = input.ReadStringRef();
-			if (name == null) name = attachmentName;
+			int flags = input.ReadByte();
+			string name = (flags & 8) != 0 ? input.ReadStringRef() : attachmentName;
 
-			switch ((AttachmentType)input.ReadByte()) {
+			switch ((AttachmentType)(flags & 0b111)) {
 			case AttachmentType.Region: {
-				String path = input.ReadStringRef();
+				string path = (flags & 16) != 0 ? input.ReadStringRef() : null;
+				uint color = (flags & 32) != 0 ? (uint)input.ReadInt() : 0xffffffff;
+				Sequence sequence = (flags & 64) != 0 ? ReadSequence(input) : null;
 				float rotation = input.ReadFloat();
 				float x = input.ReadFloat();
 				float y = input.ReadFloat();
@@ -386,8 +443,6 @@ namespace Spine {
 				float scaleY = input.ReadFloat();
 				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, sequence);
@@ -409,36 +464,34 @@ namespace Spine {
 				return region;
 			}
 			case AttachmentType.Boundingbox: {
-				int vertexCount = input.ReadInt(true);
-				Vertices vertices = ReadVertices(input, vertexCount);
-				if (nonessential) input.ReadInt(); //int color = nonessential ? input.ReadInt() : 0; // Avoid unused local warning.
+				Vertices vertices = ReadVertices(input, (flags & 16) != 0);
+				if (nonessential) input.ReadInt(); // discard, int color = nonessential ? input.readInt() : 0;
 
 				BoundingBoxAttachment box = attachmentLoader.NewBoundingBoxAttachment(skin, name);
 				if (box == null) return null;
-				box.worldVerticesLength = vertexCount << 1;
+				box.worldVerticesLength = vertices.length;
 				box.vertices = vertices.vertices;
 				box.bones = vertices.bones;
 				// skipped porting: if (nonessential) Color.rgba8888ToColor(box.getColor(), color);
 				return box;
 			}
 			case AttachmentType.Mesh: {
-				String path = input.ReadStringRef();
-				int color = input.ReadInt();
-				int vertexCount = input.ReadInt(true);
-				float[] uvs = ReadFloatArray(input, vertexCount << 1, 1);
-				int[] triangles = ReadShortArray(input);
-				Vertices vertices = ReadVertices(input, vertexCount);
+				string path = (flags & 16) != 0 ? input.ReadStringRef() : name;
+				uint color = (flags & 32) != 0 ? (uint)input.ReadInt() : 0xffffffff;
+				Sequence sequence = (flags & 64) != 0 ? ReadSequence(input) : null;
 				int hullLength = input.ReadInt(true);
-				Sequence sequence = ReadSequence(input);
+				Vertices vertices = ReadVertices(input, (flags & 128) != 0);
+				float[] uvs = ReadFloatArray(input, vertices.length, 1);
+				int[] triangles = ReadShortArray(input, (vertices.length - hullLength - 2) * 3);
+
 				int[] edges = null;
 				float width = 0, height = 0;
 				if (nonessential) {
-					edges = ReadShortArray(input);
+					edges = ReadShortArray(input, input.ReadInt(true));
 					width = input.ReadFloat();
 					height = input.ReadFloat();
 				}
 
-				if (path == null) path = name;
 				MeshAttachment mesh = attachmentLoader.NewMeshAttachment(skin, name, path, sequence);
 				if (mesh == null) return null;
 				mesh.Path = path;
@@ -448,7 +501,7 @@ namespace Spine {
 				mesh.a = ((color & 0x000000ff)) / 255f;
 				mesh.bones = vertices.bones;
 				mesh.vertices = vertices.vertices;
-				mesh.WorldVerticesLength = vertexCount << 1;
+				mesh.WorldVerticesLength = vertices.length;
 				mesh.triangles = triangles;
 				mesh.regionUVs = uvs;
 				if (sequence == null) mesh.UpdateRegion();
@@ -462,19 +515,18 @@ namespace Spine {
 				return mesh;
 			}
 			case AttachmentType.Linkedmesh: {
-				String path = input.ReadStringRef();
-				int color = input.ReadInt();
-				String skinName = input.ReadStringRef();
-				String parent = input.ReadStringRef();
-				bool inheritTimelines = input.ReadBoolean();
-				Sequence sequence = ReadSequence(input);
+				String path = (flags & 16) != 0 ? input.ReadStringRef() : name;
+				uint color = (flags & 32) != 0 ? (uint)input.ReadInt() : 0xffffffff;
+				Sequence sequence = (flags & 64) != 0 ? ReadSequence(input) : null;
+				bool inheritTimelines = (flags & 128) != 0;
+				int skinIndex = input.ReadInt(true);
+				string parent = input.ReadStringRef();
 				float width = 0, height = 0;
 				if (nonessential) {
 					width = input.ReadFloat();
 					height = input.ReadFloat();
 				}
 
-				if (path == null) path = name;
 				MeshAttachment mesh = attachmentLoader.NewMeshAttachment(skin, name, path, sequence);
 				if (mesh == null) return null;
 				mesh.Path = path;
@@ -487,15 +539,14 @@ namespace Spine {
 					mesh.Width = width * scale;
 					mesh.Height = height * scale;
 				}
-				linkedMeshes.Add(new SkeletonJson.LinkedMesh(mesh, skinName, slotIndex, parent, inheritTimelines));
+				linkedMeshes.Add(new LinkedMesh(mesh, skinIndex, slotIndex, parent, inheritTimelines));
 				return mesh;
 			}
 			case AttachmentType.Path: {
-				bool closed = input.ReadBoolean();
-				bool constantSpeed = input.ReadBoolean();
-				int vertexCount = input.ReadInt(true);
-				Vertices vertices = ReadVertices(input, vertexCount);
-				float[] lengths = new float[vertexCount / 3];
+				bool closed = (flags & 16) != 0;
+				bool constantSpeed = (flags & 32) != 0;
+				Vertices vertices = ReadVertices(input, (flags & 64) != 0);
+				float[] lengths = new float[vertices.length / 6];
 				for (int i = 0, n = lengths.Length; i < n; i++)
 					lengths[i] = input.ReadFloat() * scale;
 				if (nonessential) input.ReadInt(); //int color = nonessential ? input.ReadInt() : 0;
@@ -504,7 +555,7 @@ namespace Spine {
 				if (path == null) return null;
 				path.closed = closed;
 				path.constantSpeed = constantSpeed;
-				path.worldVerticesLength = vertexCount << 1;
+				path.worldVerticesLength = vertices.length;
 				path.vertices = vertices.vertices;
 				path.bones = vertices.bones;
 				path.lengths = lengths;
@@ -527,14 +578,13 @@ namespace Spine {
 			}
 			case AttachmentType.Clipping: {
 				int endSlotIndex = input.ReadInt(true);
-				int vertexCount = input.ReadInt(true);
-				Vertices vertices = ReadVertices(input, vertexCount);
+				Vertices vertices = ReadVertices(input, (flags & 16) != 0);
 				if (nonessential) input.ReadInt();
 
 				ClippingAttachment clip = attachmentLoader.NewClippingAttachment(skin, name);
 				if (clip == null) return null;
 				clip.EndSlot = skeletonData.slots.Items[endSlotIndex];
-				clip.worldVerticesLength = vertexCount << 1;
+				clip.worldVerticesLength = vertices.length;
 				clip.vertices = vertices.vertices;
 				clip.bones = vertices.bones;
 				// skipped porting: if (nonessential) Color.rgba8888ToColor(clip.getColor(), color);
@@ -545,7 +595,6 @@ namespace Spine {
 		}
 
 		private Sequence ReadSequence (SkeletonInput input) {
-			if (!input.ReadBoolean()) return null;
 			Sequence sequence = new Sequence(input.ReadInt(true));
 			sequence.Start = input.ReadInt(true);
 			sequence.Digits = input.ReadInt(true);
@@ -553,16 +602,17 @@ namespace Spine {
 			return sequence;
 		}
 
-		private Vertices ReadVertices (SkeletonInput input, int vertexCount) {
+		private Vertices ReadVertices (SkeletonInput input, bool weighted) {
 			float scale = this.scale;
-			int verticesLength = vertexCount << 1;
+			int vertexCount = input.ReadInt(true);
 			Vertices vertices = new Vertices();
-			if (!input.ReadBoolean()) {
-				vertices.vertices = ReadFloatArray(input, verticesLength, scale);
+			vertices.length = vertexCount << 1;
+			if (!weighted) {
+				vertices.vertices = ReadFloatArray(input, vertices.length, scale);
 				return vertices;
 			}
-			ExposedList<float> weights = new ExposedList<float>(verticesLength * 3 * 3);
-			ExposedList<int> bonesArray = new ExposedList<int>(verticesLength * 3);
+			ExposedList<float> weights = new ExposedList<float>(vertices.length * 3 * 3);
+			ExposedList<int> bonesArray = new ExposedList<int>(vertices.length * 3);
 			for (int i = 0; i < vertexCount; i++) {
 				int boneCount = input.ReadInt(true);
 				bonesArray.Add(boneCount);
@@ -591,11 +641,10 @@ namespace Spine {
 			return array;
 		}
 
-		private int[] ReadShortArray (SkeletonInput input) {
-			int n = input.ReadInt(true);
+		private int[] ReadShortArray (SkeletonInput input, int n) {
 			int[] array = new int[n];
 			for (int i = 0; i < n; i++)
-				array[i] = (input.ReadByte() << 8) | input.ReadByte();
+				array[i] = input.ReadInt(true);
 			return array;
 		}
 
@@ -783,34 +832,34 @@ namespace Spine {
 					int type = input.ReadByte(), frameCount = input.ReadInt(true), bezierCount = input.ReadInt(true);
 					switch (type) {
 					case BONE_ROTATE:
-						timelines.Add(ReadTimeline(input, new RotateTimeline(frameCount, bezierCount, boneIndex), 1));
+						ReadTimeline(input, timelines, new RotateTimeline(frameCount, bezierCount, boneIndex), 1);
 						break;
 					case BONE_TRANSLATE:
-						timelines.Add(ReadTimeline(input, new TranslateTimeline(frameCount, bezierCount, boneIndex), scale));
+						ReadTimeline(input, timelines, new TranslateTimeline(frameCount, bezierCount, boneIndex), scale);
 						break;
 					case BONE_TRANSLATEX:
-						timelines.Add(ReadTimeline(input, new TranslateXTimeline(frameCount, bezierCount, boneIndex), scale));
+						ReadTimeline(input, timelines, new TranslateXTimeline(frameCount, bezierCount, boneIndex), scale);
 						break;
 					case BONE_TRANSLATEY:
-						timelines.Add(ReadTimeline(input, new TranslateYTimeline(frameCount, bezierCount, boneIndex), scale));
+						ReadTimeline(input, timelines, new TranslateYTimeline(frameCount, bezierCount, boneIndex), scale);
 						break;
 					case BONE_SCALE:
-						timelines.Add(ReadTimeline(input, new ScaleTimeline(frameCount, bezierCount, boneIndex), 1));
+						ReadTimeline(input, timelines, new ScaleTimeline(frameCount, bezierCount, boneIndex), 1);
 						break;
 					case BONE_SCALEX:
-						timelines.Add(ReadTimeline(input, new ScaleXTimeline(frameCount, bezierCount, boneIndex), 1));
+						ReadTimeline(input, timelines, new ScaleXTimeline(frameCount, bezierCount, boneIndex), 1);
 						break;
 					case BONE_SCALEY:
-						timelines.Add(ReadTimeline(input, new ScaleYTimeline(frameCount, bezierCount, boneIndex), 1));
+						ReadTimeline(input, timelines, new ScaleYTimeline(frameCount, bezierCount, boneIndex), 1);
 						break;
 					case BONE_SHEAR:
-						timelines.Add(ReadTimeline(input, new ShearTimeline(frameCount, bezierCount, boneIndex), 1));
+						ReadTimeline(input, timelines, new ShearTimeline(frameCount, bezierCount, boneIndex), 1);
 						break;
 					case BONE_SHEARX:
-						timelines.Add(ReadTimeline(input, new ShearXTimeline(frameCount, bezierCount, boneIndex), 1));
+						ReadTimeline(input, timelines, new ShearXTimeline(frameCount, bezierCount, boneIndex), 1);
 						break;
 					case BONE_SHEARY:
-						timelines.Add(ReadTimeline(input, new ShearYTimeline(frameCount, bezierCount, boneIndex), 1));
+						ReadTimeline(input, timelines, new ShearYTimeline(frameCount, bezierCount, boneIndex), 1);
 						break;
 					}
 				}
@@ -822,7 +871,8 @@ namespace Spine {
 				IkConstraintTimeline timeline = new IkConstraintTimeline(frameCount, input.ReadInt(true), index);
 				float time = input.ReadFloat(), mix = input.ReadFloat(), softness = input.ReadFloat() * scale;
 				for (int frame = 0, bezier = 0; ; frame++) {
-					timeline.SetFrame(frame, time, mix, softness, input.ReadSByte(), input.ReadBoolean(), input.ReadBoolean());
+					int flags = input.Read();
+					timeline.SetFrame(frame, time, mix, softness, input.ReadByte(), (flags & 1) != 0, (flags & 2) != 0);
 					if (frame == frameLast) break;
 					float time2 = input.ReadFloat(), mix2 = input.ReadFloat(), softness2 = input.ReadFloat() * scale;
 					switch (input.ReadByte()) {
@@ -881,20 +931,18 @@ namespace Spine {
 				int index = input.ReadInt(true);
 				PathConstraintData data = skeletonData.pathConstraints.Items[index];
 				for (int ii = 0, nn = input.ReadInt(true); ii < nn; ii++) {
-					switch (input.ReadByte()) {
+					int type = input.ReadByte(), frameCount = input.ReadInt(true), bezierCount = input.ReadInt(true);
+					switch (type) {
 					case PATH_POSITION:
-						timelines
-							.Add(ReadTimeline(input, new PathConstraintPositionTimeline(input.ReadInt(true), input.ReadInt(true), index),
-								data.positionMode == PositionMode.Fixed ? scale : 1));
+						ReadTimeline(input, timelines, new PathConstraintPositionTimeline(frameCount, bezierCount, index),
+							data.positionMode == PositionMode.Fixed ? scale : 1);
 						break;
 					case PATH_SPACING:
-						timelines
-							.Add(ReadTimeline(input, new PathConstraintSpacingTimeline(input.ReadInt(true), input.ReadInt(true), index),
-								data.spacingMode == SpacingMode.Length || data.spacingMode == SpacingMode.Fixed ? scale : 1));
+						ReadTimeline(input, timelines, new PathConstraintSpacingTimeline(frameCount, bezierCount, index),
+							data.spacingMode == SpacingMode.Length || data.spacingMode == SpacingMode.Fixed ? scale : 1);
 						break;
 					case PATH_MIX:
-						PathConstraintMixTimeline timeline = new PathConstraintMixTimeline(input.ReadInt(true), input.ReadInt(true),
-							index);
+						PathConstraintMixTimeline timeline = new PathConstraintMixTimeline(frameCount, bezierCount, index);
 						float time = input.ReadFloat(), mixRotate = input.ReadFloat(), mixX = input.ReadFloat(), mixY = input.ReadFloat();
 						for (int frame = 0, bezier = 0, frameLast = timeline.FrameCount - 1; ; frame++) {
 							timeline.SetFrame(frame, time, mixRotate, mixX, mixY);
@@ -922,6 +970,45 @@ namespace Spine {
 				}
 			}
 
+			// Physics timelines.
+			for (int i = 0, n = input.ReadInt(true); i < n; i++) {
+				int index = input.ReadInt(true) - 1;
+				for (int ii = 0, nn = input.ReadInt(true); ii < nn; ii++) {
+					int type = input.ReadByte(), frameCount = input.ReadInt(true);
+					if (type == PHYSICS_RESET) {
+						PhysicsConstraintResetTimeline timeline = new PhysicsConstraintResetTimeline(frameCount, index);
+						for (int frame = 0; frame < frameCount; frame++)
+							timeline.SetFrame(frame, input.ReadFloat());
+						timelines.Add(timeline);
+						continue;
+					}
+					int bezierCount = input.ReadInt(true);
+					switch (type) {
+					case PHYSICS_INERTIA:
+						ReadTimeline(input, timelines, new PhysicsConstraintInertiaTimeline(frameCount, bezierCount, index), 1);
+						break;
+					case PHYSICS_STRENGTH:
+						ReadTimeline(input, timelines, new PhysicsConstraintStrengthTimeline(frameCount, bezierCount, index), 1);
+						break;
+					case PHYSICS_DAMPING:
+						ReadTimeline(input, timelines, new PhysicsConstraintDampingTimeline(frameCount, bezierCount, index), 1);
+						break;
+					case PHYSICS_MASS:
+						ReadTimeline(input, timelines, new PhysicsConstraintMassTimeline(frameCount, bezierCount, index), 1);
+						break;
+					case PHYSICS_WIND:
+						ReadTimeline(input, timelines, new PhysicsConstraintWindTimeline(frameCount, bezierCount, index), 1);
+						break;
+					case PHYSICS_GRAVITY:
+						ReadTimeline(input, timelines, new PhysicsConstraintGravityTimeline(frameCount, bezierCount, index), 1);
+						break;
+					case PHYSICS_MIX:
+						ReadTimeline(input, timelines, new PhysicsConstraintMixTimeline(frameCount, bezierCount, index), 1);
+						break;
+					}
+				}
+			}
+
 			// Attachment timelines.
 			for (int i = 0, n = input.ReadInt(true); i < n; i++) {
 				Skin skin = skeletonData.skins.Items[input.ReadInt(true)];
@@ -1038,7 +1125,8 @@ namespace Spine {
 					Event e = new Event(time, eventData);
 					e.intValue = input.ReadInt(false);
 					e.floatValue = input.ReadFloat();
-					e.stringValue = input.ReadBoolean() ? input.ReadString() : eventData.String;
+					e.stringValue = input.ReadString();
+					if (e.stringValue == null) e.stringValue = eventData.String;
 					if (e.Data.AudioPath != null) {
 						e.volume = input.ReadFloat();
 						e.balance = input.ReadFloat();
@@ -1056,7 +1144,7 @@ namespace Spine {
 		}
 
 		/// <exception cref="IOException">Throws IOException when a read operation fails.</exception>
-		private Timeline ReadTimeline (SkeletonInput input, CurveTimeline1 timeline, float scale) {
+		private void ReadTimeline (SkeletonInput input, ExposedList<Timeline> timelines, CurveTimeline1 timeline, float scale) {
 			float time = input.ReadFloat(), value = input.ReadFloat() * scale;
 			for (int frame = 0, bezier = 0, frameLast = timeline.FrameCount - 1; ; frame++) {
 				timeline.SetFrame(frame, time, value);
@@ -1073,11 +1161,11 @@ namespace Spine {
 				time = time2;
 				value = value2;
 			}
-			return timeline;
+			timelines.Add(timeline);
 		}
 
 		/// <exception cref="IOException">Throws IOException when a read operation fails.</exception>
-		private Timeline ReadTimeline (SkeletonInput input, CurveTimeline2 timeline, float scale) {
+		private void ReadTimeline (SkeletonInput input, ExposedList<Timeline> timelines, CurveTimeline2 timeline, float scale) {
 			float time = input.ReadFloat(), value1 = input.ReadFloat() * scale, value2 = input.ReadFloat() * scale;
 			for (int frame = 0, bezier = 0, frameLast = timeline.FrameCount - 1; ; frame++) {
 				timeline.SetFrame(frame, time, value1, value2);
@@ -1096,7 +1184,7 @@ namespace Spine {
 				value1 = nvalue1;
 				value2 = nvalue2;
 			}
-			return timeline;
+			timelines.Add(timeline);
 		}
 
 		/// <exception cref="IOException">Throws IOException when a read operation fails.</exception>
@@ -1107,6 +1195,7 @@ namespace Spine {
 		}
 
 		internal class Vertices {
+			public int length;
 			public int[] bones;
 			public float[] vertices;
 		}
@@ -1202,7 +1291,7 @@ namespace Spine {
 				return System.Text.Encoding.UTF8.GetString(buffer, 0, byteCount);
 			}
 
-			///<return>May be null.</return>
+			/// <return>May be null.</return>
 			public String ReadStringRef () {
 				int index = ReadInt(true);
 				return index == 0 ? null : strings[index - 1];
@@ -1257,5 +1346,20 @@ namespace Spine {
 				throw new ArgumentException("Stream does not contain valid binary Skeleton Data.");
 			}
 		}
+
+		private class LinkedMesh {
+			internal string parent;
+			internal int skinIndex, slotIndex;
+			internal MeshAttachment mesh;
+			internal bool inheritTimelines;
+
+			public LinkedMesh (MeshAttachment mesh, int skinIndex, int slotIndex, string parent, bool inheritTimelines) {
+				this.mesh = mesh;
+				this.skinIndex = skinIndex;
+				this.slotIndex = slotIndex;
+				this.parent = parent;
+				this.inheritTimelines = inheritTimelines;
+			}
+		}
 	}
 }

+ 5 - 5
spine-csharp/src/SkeletonData.cs

@@ -51,8 +51,8 @@ namespace Spine {
 		internal float fps;
 		internal string imagesPath, audioPath;
 
-		///<summary>The skeleton's name, which by default is the name of the skeleton data file when possible, or null when a name hasn't been
-		///set.</summary>
+		/// <summary>The skeleton's name, which by default is the name of the skeleton data file when possible, or null when a name hasn't been
+		/// set.</summary>
 		public string Name { get { return name; } set { name = value; } }
 
 		/// <summary>The skeleton's bones, sorted parent first. The root bone is always the first bone.</summary>
@@ -90,8 +90,8 @@ namespace Spine {
 		/// <summary>The Spine version used to export this data, or null.</summary>
 		public string Version { get { return version; } set { version = value; } }
 
-		///<summary>The skeleton data hash. This value will change if any of the skeleton data has changed.
-		///May be null.</summary>
+		/// <summary>The skeleton data hash. This value will change if any of the skeleton data has changed.
+		/// May be null.</summary>
 		public string Hash { get { return hash; } set { hash = value; } }
 
 		public string ImagesPath { get { return imagesPath; } set { imagesPath = value; } }
@@ -217,7 +217,7 @@ namespace Spine {
 		/// <returns>May be null.</returns>
 		public PhysicsConstraintData FindPhysicsConstraint (String constraintName) {
 			if (constraintName == null) throw new ArgumentNullException("constraintName", "constraintName cannot be null.");
-			Object[] physicsConstraints = this.physicsConstraints.Items;
+			PhysicsConstraintData[] physicsConstraints = this.physicsConstraints.Items;
 			for (int i = 0, n = this.physicsConstraints.Count; i < n; i++) {
 				PhysicsConstraintData constraint = (PhysicsConstraintData)physicsConstraints[i];
 				if (constraint.name.Equals(constraintName)) return constraint;

+ 121 - 13
spine-csharp/src/SkeletonJson.cs

@@ -52,6 +52,7 @@ namespace Spine {
 	/// Runtimes Guide.</para>
 	/// </summary>
 	public class SkeletonJson : SkeletonLoader {
+		private readonly List<LinkedMesh> linkedMeshes = new List<LinkedMesh>();
 
 		public SkeletonJson (AttachmentLoader attachmentLoader)
 			: base(attachmentLoader) {
@@ -277,6 +278,42 @@ namespace Spine {
 				}
 			}
 
+			// Physics constraints.
+			if (root.ContainsKey("physics")) {
+				foreach (Dictionary<string, Object> constraintMap in (List<Object>)root["physics"]) {
+					PhysicsConstraintData data = new PhysicsConstraintData((string)constraintMap["name"]);
+					data.order = GetInt(constraintMap, "order", 0);
+					data.skinRequired = GetBoolean(constraintMap, "skin", false);
+
+					string boneName = (string)constraintMap["bone"];
+					data.bone = skeletonData.FindBone(boneName);
+					if (data.bone == null) throw new Exception("Physics bone not found: " + boneName);
+
+					data.x = GetFloat(constraintMap, "x", 0);
+					data.y = GetFloat(constraintMap, "y", 0);
+					data.rotate = GetFloat(constraintMap, "rotate", 0);
+					data.scaleX = GetFloat(constraintMap, "scaleX", 0);
+					data.shearX = GetFloat(constraintMap, "shearX", 0);
+					data.step = 1f / GetInt(constraintMap, "fps", 60);
+					data.inertia = GetFloat(constraintMap, "inertia", 1);
+					data.strength = GetFloat(constraintMap, "strength", 100);
+					data.damping = GetFloat(constraintMap, "damping", 1);
+					data.massInverse = 1f / GetFloat(constraintMap, "mass", 1);
+					data.wind = GetFloat(constraintMap, "wind", 0);
+					data.gravity = GetFloat(constraintMap, "gravity", 0);
+					data.mix = GetFloat(constraintMap, "mix", 1);
+					data.inertiaGlobal = GetBoolean(constraintMap, "inertiaGlobal", false);
+					data.strengthGlobal = GetBoolean(constraintMap, "strengthGlobal", false);
+					data.dampingGlobal = GetBoolean(constraintMap, "dampingGlobal", false);
+					data.massGlobal = GetBoolean(constraintMap, "massGlobal", false);
+					data.windGlobal = GetBoolean(constraintMap, "windGlobal", false);
+					data.gravityGlobal = GetBoolean(constraintMap, "gravityGlobal", false);
+					data.mixGlobal = GetBoolean(constraintMap, "mixGlobal", false);
+
+					skeletonData.physicsConstraints.Add(data);
+				}
+			}
+
 			// Skins.
 			if (root.ContainsKey("skins")) {
 				foreach (Dictionary<string, object> skinMap in (List<object>)root["skins"]) {
@@ -310,6 +347,13 @@ namespace Spine {
 							skin.constraints.Add(constraint);
 						}
 					}
+					if (skinMap.ContainsKey("physics")) {
+						foreach (string entryName in (List<Object>)skinMap["physics"]) {
+							PhysicsConstraintData constraint = skeletonData.FindPhysicsConstraint(entryName);
+							if (constraint == null) throw new Exception("Skin physics constraint not found: " + entryName);
+							skin.constraints.Add(constraint);
+						}
+					}
 					skin.constraints.TrimExcess();
 					if (skinMap.ContainsKey("attachments")) {
 						foreach (KeyValuePair<string, Object> slotEntry in (Dictionary<string, Object>)skinMap["attachments"]) {
@@ -964,6 +1008,55 @@ namespace Spine {
 				}
 			}
 
+			// Physics constraint timelines.
+			if (map.ContainsKey("physics")) {
+				foreach (KeyValuePair<string, Object> constraintMap in (Dictionary<string, Object>)map["physics"]) {
+					int index = -1;
+					if (!string.IsNullOrEmpty(constraintMap.Key)) {
+						PhysicsConstraintData constraint = skeletonData.FindPhysicsConstraint(constraintMap.Key);
+						if (constraint == null) throw new Exception("Physics constraint not found: " + constraintMap.Key);
+						index = skeletonData.physicsConstraints.IndexOf(constraint);
+					}
+					Dictionary<string, object> timelineMap = (Dictionary<string, Object>)constraintMap.Value;
+					foreach (KeyValuePair<string, Object> timelineEntry in timelineMap) {
+						List<object> values = (List<Object>)timelineEntry.Value;
+						List<object>.Enumerator keyMapEnumerator = values.GetEnumerator();
+						if (!keyMapEnumerator.MoveNext()) continue;
+
+						int frames = values.Count;
+						string timelineName = (string)timelineEntry.Key;
+						if (timelineName == "reset") {
+							PhysicsConstraintResetTimeline timeline1 = new PhysicsConstraintResetTimeline(frames, index);
+							int frame = 0;
+							foreach (Dictionary<string, Object> keyMap in values) {
+								timeline1.SetFrame(frame++, GetFloat(keyMap, "time", 0));
+							}
+							timelines.Add(timeline1);
+							continue;
+						}
+
+						CurveTimeline1 timeline;
+						if (timelineName == "inertia")
+							timeline = new PhysicsConstraintInertiaTimeline(frames, frames, index);
+						else if (timelineName == "strength")
+							timeline = new PhysicsConstraintStrengthTimeline(frames, frames, index);
+						else if (timelineName == "damping")
+							timeline = new PhysicsConstraintDampingTimeline(frames, frames, index);
+						else if (timelineName == "mass")
+							timeline = new PhysicsConstraintMassTimeline(frames, frames, index);
+						else if (timelineName == "wind")
+							timeline = new PhysicsConstraintWindTimeline(frames, frames, index);
+						else if (timelineName == "gravity")
+							timeline = new PhysicsConstraintGravityTimeline(frames, frames, index);
+						else if (timelineName == "mix") //
+							timeline = new PhysicsConstraintMixTimeline(frames, frames, index);
+						else
+							continue;
+						timelines.Add(ReadTimeline(ref keyMapEnumerator, timeline, 0, 1));
+					}
+				}
+			}
+
 			// Attachment timelines.
 			if (map.ContainsKey("attachments")) {
 				foreach (KeyValuePair<string, Object> attachmentsMap in (Dictionary<string, Object>)map["attachments"]) {
@@ -1051,13 +1144,13 @@ namespace Spine {
 				DrawOrderTimeline timeline = new DrawOrderTimeline(values.Count);
 				int slotCount = skeletonData.slots.Count;
 				int frame = 0;
-				foreach (Dictionary<string, Object> drawOrderMap in values) {
+				foreach (Dictionary<string, Object> keyMap in values) {
 					int[] drawOrder = null;
-					if (drawOrderMap.ContainsKey("offsets")) {
+					if (keyMap.ContainsKey("offsets")) {
 						drawOrder = new int[slotCount];
 						for (int i = slotCount - 1; i >= 0; i--)
 							drawOrder[i] = -1;
-						List<object> offsets = (List<Object>)drawOrderMap["offsets"];
+						List<object> offsets = (List<Object>)keyMap["offsets"];
 						int[] unchanged = new int[slotCount - offsets.Count];
 						int originalIndex = 0, unchangedIndex = 0;
 						foreach (Dictionary<string, Object> offsetMap in offsets) {
@@ -1076,7 +1169,7 @@ namespace Spine {
 						for (int i = slotCount - 1; i >= 0; i--)
 							if (drawOrder[i] == -1) drawOrder[i] = unchanged[--unchangedIndex];
 					}
-					timeline.SetFrame(frame, GetFloat(drawOrderMap, "time", 0), drawOrder);
+					timeline.SetFrame(frame, GetFloat(keyMap, "time", 0), drawOrder);
 					++frame;
 				}
 				timelines.Add(timeline);
@@ -1087,17 +1180,17 @@ namespace Spine {
 				List<object> eventsMap = (List<Object>)map["events"];
 				EventTimeline timeline = new EventTimeline(eventsMap.Count);
 				int frame = 0;
-				foreach (Dictionary<string, Object> eventMap in eventsMap) {
-					EventData eventData = skeletonData.FindEvent((string)eventMap["name"]);
-					if (eventData == null) throw new Exception("Event not found: " + eventMap["name"]);
-					Event e = new Event(GetFloat(eventMap, "time", 0), eventData) {
-						intValue = GetInt(eventMap, "int", eventData.Int),
-						floatValue = GetFloat(eventMap, "float", eventData.Float),
-						stringValue = GetString(eventMap, "string", eventData.String)
+				foreach (Dictionary<string, Object> keyMap in eventsMap) {
+					EventData eventData = skeletonData.FindEvent((string)keyMap["name"]);
+					if (eventData == null) throw new Exception("Event not found: " + keyMap["name"]);
+					Event e = new Event(GetFloat(keyMap, "time", 0), eventData) {
+						intValue = GetInt(keyMap, "int", eventData.Int),
+						floatValue = GetFloat(keyMap, "float", eventData.Float),
+						stringValue = GetString(keyMap, "string", eventData.String)
 					};
 					if (e.data.AudioPath != null) {
-						e.volume = GetFloat(eventMap, "volume", eventData.Volume);
-						e.balance = GetFloat(eventMap, "balance", eventData.Balance);
+						e.volume = GetFloat(keyMap, "volume", eventData.Volume);
+						e.balance = GetFloat(keyMap, "balance", eventData.Balance);
 					}
 					timeline.SetFrame(frame, e);
 					++frame;
@@ -1236,5 +1329,20 @@ namespace Spine {
 				throw new ArgumentException("Color hexidecimal length must be " + expectedLength + ", recieved: " + hexString, "hexString");
 			return Convert.ToInt32(hexString.Substring(colorIndex * 2, 2), 16) / (float)255;
 		}
+
+		private class LinkedMesh {
+			internal string parent, skin;
+			internal int slotIndex;
+			internal MeshAttachment mesh;
+			internal bool inheritTimelines;
+
+			public LinkedMesh (MeshAttachment mesh, string skin, int slotIndex, string parent, bool inheritTimelines) {
+				this.mesh = mesh;
+				this.skin = skin;
+				this.slotIndex = slotIndex;
+				this.parent = parent;
+				this.inheritTimelines = inheritTimelines;
+			}
+		}
 	}
 }

+ 0 - 17
spine-csharp/src/SkeletonLoader.cs

@@ -42,7 +42,6 @@ namespace Spine {
 	public abstract class SkeletonLoader {
 		protected readonly AttachmentLoader attachmentLoader;
 		protected float scale = 1;
-		protected readonly List<LinkedMesh> linkedMeshes = new List<LinkedMesh>();
 
 		/// <summary>Creates a skeleton loader that loads attachments using an <see cref="AtlasAttachmentLoader"/> with the specified atlas.
 		/// </summary>
@@ -72,21 +71,5 @@ namespace Spine {
 		}
 
 		public abstract SkeletonData ReadSkeletonData (string path);
-
-		protected class LinkedMesh {
-			internal string parent, skin;
-			internal int slotIndex;
-			internal MeshAttachment mesh;
-			internal bool inheritTimelines;
-
-			public LinkedMesh (MeshAttachment mesh, string skin, int slotIndex, string parent, bool inheritTimelines) {
-				this.mesh = mesh;
-				this.skin = skin;
-				this.slotIndex = slotIndex;
-				this.parent = parent;
-				this.inheritTimelines = inheritTimelines;
-			}
-		}
-
 	}
 }

+ 4 - 4
spine-csharp/src/Skin.cs

@@ -45,7 +45,7 @@ namespace Spine {
 		internal readonly ExposedList<ConstraintData> constraints = new ExposedList<ConstraintData>();
 
 		public string Name { get { return name; } }
-		///<summary>Returns all attachments contained in this skin.</summary>
+		/// <summary>Returns all attachments contained in this skin.</summary>
 		public ICollection<SkinEntry> Attachments { get { return attachments.Values; } }
 		public ExposedList<BoneData> Bones { get { return bones; } }
 		public ExposedList<ConstraintData> Constraints { get { return constraints; } }
@@ -62,7 +62,7 @@ namespace Spine {
 			attachments[new SkinKey(slotIndex, name)] = new SkinEntry(slotIndex, name, attachment);
 		}
 
-		///<summary>Adds all attachments, bones, and constraints from the specified skin to this skin.</summary>
+		/// <summary>Adds all attachments, bones, and constraints from the specified skin to this skin.</summary>
 		public void AddSkin (Skin skin) {
 			foreach (BoneData data in skin.bones)
 				if (!bones.Contains(data)) bones.Add(data);
@@ -76,7 +76,7 @@ namespace Spine {
 			}
 		}
 
-		///<summary>Adds all attachments from the specified skin to this skin. Attachments are deep copied.</summary>
+		/// <summary>Adds all attachments from the specified skin to this skin. Attachments are deep copied.</summary>
 		public void CopySkin (Skin skin) {
 			foreach (BoneData data in skin.bones)
 				if (!bones.Contains(data)) bones.Add(data);
@@ -118,7 +118,7 @@ namespace Spine {
 			}
 		}
 
-		///<summary>Clears all attachments, bones, and constraints.</summary>
+		/// <summary>Clears all attachments, bones, and constraints.</summary>
 		public void Clear () {
 			attachments.Clear();
 			bones.Clear();

+ 19 - 8
spine-csharp/src/TransformConstraint.cs

@@ -30,6 +30,8 @@
 using System;
 
 namespace Spine {
+	using Physics = Skeleton.Physics;
+
 	/// <summary>
 	/// <para>
 	/// Stores the current pose for a transform constraint. A transform constraint adjusts the world transform of the constrained
@@ -55,6 +57,7 @@ namespace Spine {
 			mixScaleX = data.mixScaleX;
 			mixScaleY = data.mixScaleY;
 			mixShearY = data.mixShearY;
+
 			bones = new ExposedList<Bone>();
 			foreach (BoneData boneData in data.bones)
 				bones.Add(skeleton.bones.Items[boneData.index]);
@@ -63,14 +66,12 @@ namespace Spine {
 		}
 
 		/// <summary>Copy constructor.</summary>
-		public TransformConstraint (TransformConstraint constraint, Skeleton skeleton) {
+		public TransformConstraint (TransformConstraint constraint) {
 			if (constraint == null) throw new ArgumentNullException("constraint cannot be null.");
-			if (skeleton == null) throw new ArgumentNullException("skeleton cannot be null.");
 			data = constraint.data;
 			bones = new ExposedList<Bone>(constraint.Bones.Count);
-			foreach (Bone bone in constraint.Bones)
-				bones.Add(skeleton.Bones.Items[bone.data.index]);
-			target = skeleton.Bones.Items[constraint.target.data.index];
+			bones.AddRange(constraint.Bones);
+			target = constraint.target;
 			mixRotate = constraint.mixRotate;
 			mixX = constraint.mixX;
 			mixY = constraint.mixY;
@@ -79,7 +80,17 @@ namespace Spine {
 			mixShearY = constraint.mixShearY;
 		}
 
-		public void Update () {
+		public void SetToSetupPose () {
+			TransformConstraintData data = this.data;
+			mixRotate = data.mixRotate;
+			mixX = data.mixX;
+			mixY = data.mixY;
+			mixScaleX = data.mixScaleX;
+			mixScaleY = data.mixScaleY;
+			mixShearY = data.mixShearY;
+		}
+
+		public void Update (Physics physics) {
 			if (mixRotate == 0 && mixX == 0 && mixY == 0 && mixScaleX == 0 && mixScaleY == 0 && mixShearY == 0) return;
 			if (data.local) {
 				if (data.relative)
@@ -238,7 +249,7 @@ namespace Spine {
 				float rotation = bone.arotation;
 				if (mixRotate != 0) {
 					float r = target.arotation - rotation + data.offsetRotation;
-					r -= (16384 - (int)(16384.499999999996 - r / 360)) * 360;
+					r -= (float)Math.Ceiling(r / 360 - 0.5f) * 360;
 					rotation += r * mixRotate;
 				}
 
@@ -255,7 +266,7 @@ namespace Spine {
 				float shearY = bone.ashearY;
 				if (mixShearY != 0) {
 					float r = target.ashearY - shearY + data.offsetShearY;
-					r -= (16384 - (int)(16384.499999999996 - r / 360)) * 360;
+					r -= (float)Math.Ceiling(r / 360 - 0.5f) * 360;
 					shearY += r * mixShearY;
 				}
 

+ 1 - 1
spine-csharp/src/package.json

@@ -2,7 +2,7 @@
 	"name": "com.esotericsoftware.spine.spine-csharp",
 	"displayName": "spine-csharp Runtime",
 	"description": "This plugin provides the spine-csharp core runtime.",
-	"version": "4.2.1",
+	"version": "4.2.2",
 	"unity": "2018.3",
 	"author": {
 		"name": "Esoteric Software",

+ 4 - 0
spine-unity/Assets/Spine Examples/Scripts/Sample Components/Sample VertexEffects/TwoByTwoTransformEffectExample.cs

@@ -98,7 +98,11 @@ public class TwoByTwoTransformEffectExampleEditor : UnityEditor.Editor {
 		Color originalColor = UnityEditor.Handles.color;
 		UnityEditor.Handles.color = color;
 		UnityEditor.Handles.DrawLine(transform.position, transform.TransformPoint(v));
+#if UNITY_2021_3_OR_NEWER
+		v = transform.InverseTransformPoint(UnityEditor.Handles.FreeMoveHandle(transform.TransformPoint(v), 0.3f, Vector3.zero, UnityEditor.Handles.CubeHandleCap));
+#else
 		v = transform.InverseTransformPoint(UnityEditor.Handles.FreeMoveHandle(transform.TransformPoint(v), Quaternion.identity, 0.3f, Vector3.zero, UnityEditor.Handles.CubeHandleCap));
+#endif
 		UnityEditor.Handles.color = originalColor;
 	}
 }

Some files were not shown because too many files changed in this diff