+ where P : IPose {
+
+ public Constraint (D data, P pose, P constrained)
+ : base(data, pose, constrained) {
+ }
+
+ public D Data { get { return data; } }
+ public IConstraintData IData { get { return data; } }
+ abstract public IConstraint Copy (Skeleton skeleton);
+ abstract public void Sort (Skeleton skeleton);
+ public virtual bool IsSourceActive { get { return true; } }
+ abstract public void Update (Skeleton skeleton, Physics physics);
+ }
+}
diff --git a/spine-csharp/src/Constraint.cs.meta b/spine-csharp/src/Constraint.cs.meta
new file mode 100644
index 000000000..94386f870
--- /dev/null
+++ b/spine-csharp/src/Constraint.cs.meta
@@ -0,0 +1,2 @@
+fileFormatVersion: 2
+guid: 66115e19768098240ace7f8ad9cb4d4c
\ No newline at end of file
diff --git a/spine-csharp/src/ConstraintData.cs b/spine-csharp/src/ConstraintData.cs
index 242804c6a..16c42fe58 100644
--- a/spine-csharp/src/ConstraintData.cs
+++ b/spine-csharp/src/ConstraintData.cs
@@ -31,31 +31,19 @@ using System;
using System.Collections.Generic;
namespace Spine {
- /// The base class for all constraint datas.
- public abstract class ConstraintData {
- internal readonly string name;
- internal int order;
- internal bool skinRequired;
- public ConstraintData (string name) {
- if (name == null) throw new ArgumentNullException("name", "name cannot be null.");
- this.name = name;
+ public interface IConstraintData : IPosedData {
+ public string Name { get; }
+ public IConstraint Create (Skeleton skeleton);
+ }
+
+ public abstract class ConstraintData : PosedData, IConstraintData
+ where T : IConstraint
+ where P : IPose
{
+ public ConstraintData (string name, P setup)
+ : base(name, setup) {
}
- /// The constraint's name, which is unique across all constraints in the skeleton of the same type.
- public string Name { get { return name; } }
-
- /// The ordinal of this constraint for the order a skeleton's constraints will be applied by
- /// .
- public int Order { get { return order; } set { order = value; } }
-
- /// When true, only updates this constraint if the
- /// contains this constraint.
- ///
- public bool SkinRequired { get { return skinRequired; } set { skinRequired = value; } }
-
- override public string ToString () {
- return name;
- }
+ abstract public IConstraint Create (Skeleton skeleton);
}
}
diff --git a/spine-csharp/src/Event.cs b/spine-csharp/src/Event.cs
index 15f804fa1..f204103f0 100644
--- a/spine-csharp/src/Event.cs
+++ b/spine-csharp/src/Event.cs
@@ -40,6 +40,7 @@ namespace Spine {
internal float volume;
internal float balance;
+ /// The event's setup pose data.
public EventData Data { get { return data; } }
/// The animation time this event was keyed.
public float Time { get { return time; } }
diff --git a/spine-csharp/src/IPose.cs b/spine-csharp/src/IPose.cs
new file mode 100644
index 000000000..6a652e47e
--- /dev/null
+++ b/spine-csharp/src/IPose.cs
@@ -0,0 +1,36 @@
+/******************************************************************************
+ * Spine Runtimes License Agreement
+ * Last updated April 5, 2025. Replaces all prior versions.
+ *
+ * Copyright (c) 2013-2025, Esoteric Software LLC
+ *
+ * Integration of the Spine Runtimes into software or otherwise creating
+ * derivative works of the Spine Runtimes is permitted under the terms and
+ * conditions of Section 2 of the Spine Editor License Agreement:
+ * http://esotericsoftware.com/spine-editor-license
+ *
+ * Otherwise, it is permitted to integrate the Spine Runtimes into software
+ * or otherwise create derivative works of the Spine Runtimes (collectively,
+ * "Products"), provided that each user of the Products must obtain their own
+ * Spine Editor license and redistribution of the Products in any form must
+ * include this license and copyright notice.
+ *
+ * THE SPINE RUNTIMES ARE PROVIDED BY ESOTERIC SOFTWARE LLC "AS IS" AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL ESOTERIC SOFTWARE LLC BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES,
+ * BUSINESS INTERRUPTION, OR LOSS OF USE, DATA, OR PROFITS) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
+ * THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ *****************************************************************************/
+
+using System;
+
+namespace Spine {
+ public interface IPose
{
+ public void Set (P pose);
+ }
+}
diff --git a/spine-csharp/src/IPose.cs.meta b/spine-csharp/src/IPose.cs.meta
new file mode 100644
index 000000000..24e0d14d2
--- /dev/null
+++ b/spine-csharp/src/IPose.cs.meta
@@ -0,0 +1,2 @@
+fileFormatVersion: 2
+guid: 114be1b77921d6645ac5974d5b33f2eb
\ No newline at end of file
diff --git a/spine-csharp/src/IUpdatable.cs b/spine-csharp/src/IUpdate.cs
similarity index 77%
rename from spine-csharp/src/IUpdatable.cs
rename to spine-csharp/src/IUpdate.cs
index 866f7974e..bc82cca37 100644
--- a/spine-csharp/src/IUpdatable.cs
+++ b/spine-csharp/src/IUpdate.cs
@@ -28,20 +28,10 @@
*****************************************************************************/
namespace Spine {
- using Physics = Skeleton.Physics;
/// The interface for items updated by .
- public interface IUpdatable {
+ public interface IUpdate {
/// Determines how physics and other non-deterministic updates are applied.
- void Update (Physics physics);
-
- /// Returns false when this item won't be updated by
- /// because a skin is required and the
- /// active skin does not contain this item.
- ///
- ///
- ///
- ///
- bool Active { get; }
+ void Update (Skeleton skeleton, Physics physics);
}
}
diff --git a/spine-csharp/src/IUpdatable.cs.meta b/spine-csharp/src/IUpdate.cs.meta
similarity index 100%
rename from spine-csharp/src/IUpdatable.cs.meta
rename to spine-csharp/src/IUpdate.cs.meta
diff --git a/spine-csharp/src/IkConstraint.cs b/spine-csharp/src/IkConstraint.cs
index cbc9af4bf..68bbb8a41 100644
--- a/spine-csharp/src/IkConstraint.cs
+++ b/spine-csharp/src/IkConstraint.cs
@@ -30,7 +30,6 @@
using System;
namespace Spine {
- using Physics = Skeleton.Physics;
///
///
@@ -39,69 +38,59 @@ namespace Spine {
///
/// See IK constraints in the Spine User Guide.
///
- public class IkConstraint : IUpdatable {
- internal readonly IkConstraintData data;
- internal readonly ExposedList bones = new ExposedList();
+ public class IkConstraint : Constraint {
+ internal readonly ExposedList bones;
internal Bone target;
- internal int bendDirection;
- internal bool compress, stretch;
- internal float mix = 1, softness;
- internal bool active;
+ public IkConstraint (IkConstraintData data, Skeleton skeleton)
+ : base(data, new IkConstraintPose(), new IkConstraintPose()) {
+ if (skeleton == null) throw new ArgumentNullException("skeleton", "skeleton cannot be null.");
- public IkConstraint (IkConstraintData data, Skeleton skeleton) {
- if (data == null) throw new ArgumentNullException("data", "data cannot be null.");
- this.data = data;
-
- bones = new ExposedList(data.bones.Count);
+ bones = new ExposedList(data.bones.Count);
foreach (BoneData boneData in data.bones)
- bones.Add(skeleton.bones.Items[boneData.index]);
+ bones.Add(skeleton.bones.Items[boneData.index].constrained);
target = skeleton.bones.Items[data.target.index];
-
- mix = data.mix;
- softness = data.softness;
- bendDirection = data.bendDirection;
- compress = data.compress;
- stretch = data.stretch;
}
- /// Copy constructor.
- public IkConstraint (IkConstraint constraint, Skeleton skeleton)
- : this(constraint.data, skeleton) {
-
- mix = constraint.mix;
- softness = constraint.softness;
- bendDirection = constraint.bendDirection;
- compress = constraint.compress;
- stretch = constraint.stretch;
+ override public IConstraint Copy (Skeleton skeleton) {
+ var copy = new IkConstraint(data, skeleton);
+ copy.pose.Set(pose);
+ return copy;
}
- 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;
+ /// Applies the constraint to the constrained bones.
+ override public void Update (Skeleton skeleton, Physics physics) {
+ IkConstraintPose p = applied;
+ if (p.mix == 0) return;
+ BonePose target = this.target.applied;
+ BonePose[] bones = this.bones.Items;
switch (this.bones.Count) {
case 1:
- Apply(bones[0], target.worldX, target.worldY, compress, stretch, data.uniform, mix);
+ Apply(skeleton, bones[0], target.worldX, target.worldY, p.compress, p.stretch, data.uniform, p.mix);
break;
case 2:
- Apply(bones[0], bones[1], target.worldX, target.worldY, bendDirection, stretch, data.uniform, softness, mix);
+ Apply(skeleton, bones[0], bones[1], target.worldX, target.worldY, p.bendDirection, p.stretch, data.uniform,
+ p.softness, p.mix);
break;
}
}
+ override public void Sort (Skeleton skeleton) {
+ skeleton.SortBone(target);
+ Bone parent = bones.Items[0].bone;
+ skeleton.SortBone(parent);
+ skeleton.updateCache.Add(this);
+ parent.sorted = false;
+ skeleton.SortReset(parent.children);
+ skeleton.Constrained(parent);
+ if (bones.Count > 1) skeleton.Constrained(bones.Items[1].bone);
+ }
+
+ override public bool IsSourceActive { get { return target.active; } }
+
/// The bones that will be modified by this IK constraint.
- public ExposedList Bones {
+ public ExposedList Bones {
get { return bones; }
}
@@ -111,79 +100,26 @@ namespace Spine {
set { target = value; }
}
- /// A percentage (0-1) that controls the mix between the constrained and unconstrained rotation.
- ///
- /// For two bone IK: if the parent bone has local nonuniform scale, the child bone's local Y translation is set to 0.
- ///
- public float Mix {
- get { return mix; }
- set { mix = value; }
- }
-
- /// For two bone IK, the target bone's distance from the maximum reach of the bones where rotation begins to slow. The bones
- /// will not straighten completely until the target is this far out of range.
- public float Softness {
- get { return softness; }
- set { softness = value; }
- }
-
- /// For two bone IK, controls the bend direction of the IK bones, either 1 or -1.
- public int BendDirection {
- get { return bendDirection; }
- set { bendDirection = value; }
- }
-
- /// For one bone IK, when true and the target is too close, the bone is scaled to reach it.
- public bool Compress {
- get { return compress; }
- set { compress = value; }
- }
-
- /// When true and the target is out of range, the parent bone is scaled to reach it.
- ///
- /// For two bone IK: 1) the child bone's local Y translation is set to 0,
- /// 2) stretch is not applied if is > 0,
- /// and 3) if the parent bone has local nonuniform scale, stretch is not applied.
- ///
- public bool Stretch {
- get { return stretch; }
- set { stretch = value; }
- }
-
- public bool Active {
- get { return active; }
- }
-
- /// The IK constraint's setup pose data.
- public IkConstraintData Data {
- get { return data; }
- }
-
- override public string ToString () {
- return data.name;
- }
-
/// Applies 1 bone IK. The target is specified in the world coordinate system.
- static public void Apply (Bone bone, float targetX, float targetY, bool compress, bool stretch, bool uniform,
- float alpha) {
+ static public void Apply (Skeleton skeleton, BonePose bone, float targetX, float targetY, bool compress, bool stretch,
+ bool uniform, float mix) {
if (bone == null) throw new ArgumentNullException("bone", "bone cannot be null.");
- Bone p = bone.parent;
+ bone.ModifyLocal(skeleton);
+ BonePose p = bone.bone.parent.applied;
float pa = p.a, pb = p.b, pc = p.c, pd = p.d;
- float rotationIK = -bone.ashearX - bone.arotation;
- float tx = 0, ty = 0;
-
+ float rotationIK = -bone.shearX - bone.rotation, tx, ty;
switch (bone.inherit) {
case Inherit.OnlyTranslation:
- tx = (targetX - bone.worldX) * Math.Sign(bone.skeleton.ScaleX);
- ty = (targetY - bone.worldY) * Math.Sign(bone.skeleton.ScaleY);
+ tx = (targetX - bone.worldX) * Math.Sign(skeleton.ScaleX);
+ ty = (targetY - bone.worldY) * Math.Sign(skeleton.ScaleY);
break;
case Inherit.NoRotationOrReflection: {
float s = Math.Abs(pa * pd - pb * pc) / Math.Max(0.0001f, pa * pa + pc * pc);
- float sa = pa / bone.skeleton.scaleX;
- float sc = pc / bone.skeleton.ScaleY;
- pb = -sc * s * bone.skeleton.scaleX;
- pd = sa * s * bone.skeleton.ScaleY;
+ float sa = pa / skeleton.scaleX;
+ float sc = pc / skeleton.ScaleY;
+ pb = -sc * s * skeleton.scaleX;
+ pd = sa * s * skeleton.ScaleY;
rotationIK += MathUtils.Atan2Deg(sc, sa);
goto default; // Fall through.
}
@@ -194,21 +130,20 @@ namespace Spine {
tx = 0;
ty = 0;
} else {
- tx = (x * pd - y * pb) / d - bone.ax;
- ty = (y * pa - x * pc) / d - bone.ay;
+ tx = (x * pd - y * pb) / d - bone.x;
+ ty = (y * pa - x * pc) / d - bone.y;
}
break;
}
}
rotationIK += MathUtils.Atan2Deg(ty, tx);
- if (bone.ascaleX < 0) rotationIK += 180;
+ if (bone.scaleX < 0) rotationIK += 180;
if (rotationIK > 180)
rotationIK -= 360;
else if (rotationIK < -180) //
rotationIK += 360;
-
- float sx = bone.ascaleX, sy = bone.ascaleY;
+ bone.rotation += rotationIK * mix;
if (compress || stretch) {
switch (bone.inherit) {
case Inherit.NoScale:
@@ -217,27 +152,28 @@ namespace Spine {
ty = targetY - bone.worldY;
break;
}
- float b = bone.data.length * sx;
+ float b = bone.bone.data.length * bone.scaleX;
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;
+ float s = ((float)Math.Sqrt(dd) / b - 1) * mix + 1;
+ bone.scaleX *= s;
+ if (uniform) bone.scaleY *= s;
}
}
}
- bone.UpdateWorldTransform(bone.ax, bone.ay, bone.arotation + rotationIK * alpha, sx, sy, bone.ashearX, bone.ashearY);
}
/// Applies 2 bone IK. The target is specified in the world coordinate system.
/// A direct descendant of the parent bone.
- static public void Apply (Bone parent, Bone child, float targetX, float targetY, int bendDir, bool stretch, bool uniform,
- float softness, float alpha) {
+ static public void Apply (Skeleton skeleton, BonePose parent, BonePose child, float targetX, float targetY, int bendDir,
+ bool stretch, bool uniform, float softness, float mix) {
if (parent == null) throw new ArgumentNullException("parent", "parent cannot be null.");
if (child == null) throw new ArgumentNullException("child", "child cannot be null.");
if (parent.inherit != Inherit.Normal || child.inherit != Inherit.Normal) return;
- float px = parent.ax, py = parent.ay, psx = parent.ascaleX, psy = parent.ascaleY, sx = psx, sy = psy, csx = child.ascaleX;
+ parent.ModifyLocal(skeleton);
+ child.ModifyLocal(skeleton);
+ float px = parent.x, py = parent.y, psx = parent.scaleX, psy = parent.scaleY, csx = child.scaleX;
int os1, os2, s2;
if (psx < 0) {
psx = -psx;
@@ -256,18 +192,17 @@ namespace Spine {
os2 = 180;
} else
os2 = 0;
- float cx = child.ax, cy, cwx, cwy, a = parent.a, b = parent.b, c = parent.c, d = parent.d;
+ float cwx, cwy, a = parent.a, b = parent.b, c = parent.c, d = parent.d;
bool u = Math.Abs(psx - psy) <= 0.0001f;
if (!u || stretch) {
- cy = 0;
- cwx = a * cx + parent.worldX;
- cwy = c * cx + parent.worldY;
+ child.y = 0;
+ cwx = a * child.x + parent.worldX;
+ cwy = c * child.x + parent.worldY;
} else {
- cy = child.ay;
- cwx = a * cx + b * cy + parent.worldX;
- cwy = c * cx + d * cy + parent.worldY;
+ cwx = a * child.x + b * child.y + parent.worldX;
+ cwy = c * child.x + d * child.y + parent.worldY;
}
- Bone pp = parent.parent;
+ BonePose pp = parent.bone.parent.applied;
a = pp.a;
b = pp.b;
c = pp.c;
@@ -275,10 +210,10 @@ namespace Spine {
float id = a * d - b * c, x = cwx - pp.worldX, y = cwy - pp.worldY;
id = Math.Abs(id) <= 0.0001f ? 0 : 1 / id;
float dx = (x * d - y * b) * id - px, dy = (y * a - x * c) * id - py;
- float l1 = (float)Math.Sqrt(dx * dx + dy * dy), l2 = child.data.length * csx, a1, a2;
+ float l1 = (float)Math.Sqrt(dx * dx + dy * dy), l2 = child.bone.data.length * csx, a1, a2;
if (l1 < 0.0001f) {
- Apply(parent, targetX, targetY, false, stretch, false, alpha);
- child.UpdateWorldTransform(cx, cy, 0, child.ascaleX, child.ascaleY, child.ashearX, child.ashearY);
+ Apply(skeleton, parent, targetX, targetY, false, stretch, false, mix);
+ child.rotation = 0;
return;
}
x = targetX - pp.worldX;
@@ -306,9 +241,9 @@ namespace Spine {
cos = 1;
a2 = 0;
if (stretch) {
- a = ((float)Math.Sqrt(dd) / (l1 + l2) - 1) * alpha + 1;
- sx *= a;
- if (uniform) sy *= a;
+ a = ((float)Math.Sqrt(dd) / (l1 + l2) - 1) * mix + 1;
+ parent.scaleX *= a;
+ if (uniform) parent.scaleY *= a;
}
} else
a2 = (float)Math.Acos(cos) * bendDir;
@@ -366,21 +301,19 @@ namespace Spine {
}
}
break_outer:
- float os = (float)Math.Atan2(cy, cx) * s2;
- float rotation = parent.arotation;
- a1 = (a1 - os) * MathUtils.RadDeg + os1 - rotation;
+ float os = (float)Math.Atan2(child.y, child.x) * s2;
+ a1 = (a1 - os) * MathUtils.RadDeg + os1 - parent.rotation;
if (a1 > 180)
a1 -= 360;
else if (a1 < -180)
a1 += 360;
- parent.UpdateWorldTransform(px, py, rotation + a1 * alpha, sx, sy, 0, 0);
- rotation = child.arotation;
- a2 = ((a2 + os) * MathUtils.RadDeg - child.ashearX) * s2 + os2 - rotation;
+ parent.rotation += a1 * mix;
+ a2 = ((a2 + os) * MathUtils.RadDeg - child.shearX) * s2 + os2 - child.rotation;
if (a2 > 180)
a2 -= 360;
else if (a2 < -180)
a2 += 360;
- child.UpdateWorldTransform(cx, cy, rotation + a2 * alpha, child.ascaleX, child.ascaleY, child.ashearX, child.ashearY);
+ child.rotation += a2 * mix;
}
}
}
diff --git a/spine-csharp/src/IkConstraintData.cs b/spine-csharp/src/IkConstraintData.cs
index c0f5be11d..5488d4082 100644
--- a/spine-csharp/src/IkConstraintData.cs
+++ b/spine-csharp/src/IkConstraintData.cs
@@ -32,14 +32,17 @@ using System.Collections.Generic;
namespace Spine {
/// Stores the setup pose for an IkConstraint.
- public class IkConstraintData : ConstraintData {
- internal ExposedList bones = new ExposedList();
+ public class IkConstraintData : ConstraintData {
+ internal ExposedList bones = new ExposedList(2);
internal BoneData target;
- internal int bendDirection;
- internal bool compress, stretch, uniform;
- internal float mix, softness;
+ internal bool uniform;
- public IkConstraintData (string name) : base(name) {
+ public IkConstraintData (string name)
+ : base(name, new IkConstraintPose()) {
+ }
+
+ override public IConstraint Create (Skeleton skeleton) {
+ return new IkConstraint(this, skeleton);
}
/// The bones that are constrained by this IK Constraint.
@@ -54,46 +57,8 @@ namespace Spine {
}
///
- /// A percentage (0-1) that controls the mix between the constrained and unconstrained rotation.
- ///
- /// For two bone IK: if the parent bone has local nonuniform scale, the child bone's local Y translation is set to 0.
- ///
- public float Mix {
- get { return mix; }
- set { mix = value; }
- }
-
- /// For two bone IK, the target bone's distance from the maximum reach of the bones where rotation begins to slow. The bones
- /// will not straighten completely until the target is this far out of range.
- public float Softness {
- get { return softness; }
- set { softness = value; }
- }
-
- /// For two bone IK, controls the bend direction of the IK bones, either 1 or -1.
- public int BendDirection {
- get { return bendDirection; }
- set { bendDirection = value; }
- }
-
- /// For one bone IK, when true and the target is too close, the bone is scaled to reach it.
- public bool Compress {
- get { return compress; }
- set { compress = value; }
- }
-
- /// When true and the target is out of range, the parent bone is scaled to reach it.
- ///
- /// For two bone IK: 1) the child bone's local Y translation is set to 0,
- /// 2) stretch is not applied if is > 0,
- /// and 3) if the parent bone has local nonuniform scale, stretch is not applied.
- public bool Stretch {
- get { return stretch; }
- set { stretch = value; }
- }
-
- ///
- /// When true and or is used, the bone is scaled on both the X and Y axes.
+ /// When true and or is used, the bone is scaled
+ /// on both the X and Y axes.
///
public bool Uniform {
get { return uniform; }
diff --git a/spine-csharp/src/IkConstraintPose.cs b/spine-csharp/src/IkConstraintPose.cs
new file mode 100644
index 000000000..dba54d8d5
--- /dev/null
+++ b/spine-csharp/src/IkConstraintPose.cs
@@ -0,0 +1,87 @@
+/******************************************************************************
+ * Spine Runtimes License Agreement
+ * Last updated April 5, 2025. Replaces all prior versions.
+ *
+ * Copyright (c) 2013-2025, Esoteric Software LLC
+ *
+ * Integration of the Spine Runtimes into software or otherwise creating
+ * derivative works of the Spine Runtimes is permitted under the terms and
+ * conditions of Section 2 of the Spine Editor License Agreement:
+ * http://esotericsoftware.com/spine-editor-license
+ *
+ * Otherwise, it is permitted to integrate the Spine Runtimes into software
+ * or otherwise create derivative works of the Spine Runtimes (collectively,
+ * "Products"), provided that each user of the Products must obtain their own
+ * Spine Editor license and redistribution of the Products in any form must
+ * include this license and copyright notice.
+ *
+ * THE SPINE RUNTIMES ARE PROVIDED BY ESOTERIC SOFTWARE LLC "AS IS" AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL ESOTERIC SOFTWARE LLC BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES,
+ * BUSINESS INTERRUPTION, OR LOSS OF USE, DATA, OR PROFITS) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
+ * THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ *****************************************************************************/
+
+using System;
+
+namespace Spine {
+
+ /// Stores the current pose for an IK constraint.
+ public class IkConstraintPose : IPose {
+ internal int bendDirection;
+ internal bool compress, stretch;
+ internal float mix = 1, softness;
+
+ public void Set (IkConstraintPose pose) {
+ mix = pose.mix;
+ softness = pose.softness;
+ bendDirection = pose.bendDirection;
+ compress = pose.compress;
+ stretch = pose.stretch;
+ }
+
+ /// A percentage (0-1) that controls the mix between the constrained and unconstrained rotation.
+ ///
+ /// For two bone IK: if the parent bone has local nonuniform scale, the child bone's local Y translation is set to 0.
+ ///
+ public float Mix {
+ get { return mix; }
+ set { mix = value; }
+ }
+
+ /// For two bone IK, the target bone's distance from the maximum reach of the bones where rotation begins to slow. The bones
+ /// will not straighten completely until the target is this far out of range.
+ public float Softness {
+ get { return softness; }
+ set { softness = value; }
+ }
+
+ /// For two bone IK, controls the bend direction of the IK bones, either 1 or -1.
+ public int BendDirection {
+ get { return bendDirection; }
+ set { bendDirection = value; }
+ }
+
+ /// For one bone IK, when true and the target is too close, the bone is scaled to reach it.
+ public bool Compress {
+ get { return compress; }
+ set { compress = value; }
+ }
+
+ /// When true and the target is out of range, the parent bone is scaled to reach it.
+ ///
+ /// For two bone IK: 1) the child bone's local Y translation is set to 0,
+ /// 2) stretch is not applied if is > 0,
+ /// and 3) if the parent bone has local nonuniform scale, stretch is not applied.
+ ///
+ public bool Stretch {
+ get { return stretch; }
+ set { stretch = value; }
+ }
+ }
+}
diff --git a/spine-csharp/src/IkConstraintPose.cs.meta b/spine-csharp/src/IkConstraintPose.cs.meta
new file mode 100644
index 000000000..328d89d0c
--- /dev/null
+++ b/spine-csharp/src/IkConstraintPose.cs.meta
@@ -0,0 +1,2 @@
+fileFormatVersion: 2
+guid: f7b8113cda0e502458c2084ec31e0cca
\ No newline at end of file
diff --git a/spine-csharp/src/MathUtils.cs b/spine-csharp/src/MathUtils.cs
index 0c875d3ea..3eb9214d8 100644
--- a/spine-csharp/src/MathUtils.cs
+++ b/spine-csharp/src/MathUtils.cs
@@ -137,6 +137,12 @@ namespace Spine {
return value;
}
+ static public float Clamp01 (float value) {
+ if (value < 0) return 0;
+ if (value > 1) return 1;
+ return value;
+ }
+
static public float RandomTriangle (float min, float max) {
return RandomTriangle(min, max, (min + max) * 0.5f);
}
diff --git a/spine-csharp/src/PathConstraint.cs b/spine-csharp/src/PathConstraint.cs
index 1f21c3fbb..b13f0390b 100644
--- a/spine-csharp/src/PathConstraint.cs
+++ b/spine-csharp/src/PathConstraint.cs
@@ -30,7 +30,6 @@
using System;
namespace Spine {
- using Physics = Skeleton.Physics;
///
///
@@ -39,48 +38,32 @@ namespace Spine {
///
/// See Path constraints in the Spine User Guide.
///
- public class PathConstraint : IUpdatable {
+ public class PathConstraint : Constraint {
const int NONE = -1, BEFORE = -2, AFTER = -3;
const float Epsilon = 0.00001f;
- internal readonly PathConstraintData data;
- internal readonly ExposedList bones;
+ internal readonly ExposedList bones;
internal Slot slot;
- internal float position, spacing, mixRotate, mixX, mixY;
-
- internal bool active;
internal readonly ExposedList spaces = new ExposedList(), positions = new ExposedList();
internal readonly ExposedList world = new ExposedList(), curves = new ExposedList(), lengths = new ExposedList();
internal readonly float[] segments = new float[10];
- public PathConstraint (PathConstraintData data, Skeleton skeleton) {
- if (data == null) throw new ArgumentNullException("data", "data cannot be null.");
+ public PathConstraint (PathConstraintData data, Skeleton skeleton)
+ : base(data, new PathConstraintPose (), new PathConstraintPose ()) {
if (skeleton == null) throw new ArgumentNullException("skeleton", "skeleton cannot be null.");
- this.data = data;
- bones = new ExposedList(data.Bones.Count);
+ bones = new ExposedList(data.Bones.Count);
foreach (BoneData boneData in data.bones)
- bones.Add(skeleton.bones.Items[boneData.index]);
+ bones.Add(skeleton.bones.Items[boneData.index].constrained);
slot = skeleton.slots.Items[data.slot.index];
-
- position = data.position;
- spacing = data.spacing;
- mixRotate = data.mixRotate;
- mixX = data.mixX;
- mixY = data.mixY;
}
- /// Copy constructor.
- public PathConstraint (PathConstraint constraint, Skeleton skeleton)
- : this(constraint.data, skeleton) {
-
- position = constraint.position;
- spacing = constraint.spacing;
- mixRotate = constraint.mixRotate;
- mixX = constraint.mixX;
- mixY = constraint.mixY;
+ override public IConstraint Copy (Skeleton skeleton) {
+ var copy = new PathConstraint(data, skeleton);
+ copy.pose.Set(pose);
+ return copy;
}
public static void ArraysFill (float[] a, int fromIndex, int toIndex, float val) {
@@ -88,34 +71,26 @@ namespace Spine {
a[i] = val;
}
- public void SetToSetupPose () {
- PathConstraintData data = this.data;
- position = data.position;
- spacing = data.spacing;
- mixRotate = data.mixRotate;
- mixX = data.mixX;
- mixY = data.mixY;
- }
+ override public void Update (Skeleton skeleton, Physics physics) {
+ PathAttachment pathAttachment = slot.applied.Attachment as PathAttachment;
+ if (pathAttachment == null) return;
- public void Update (Physics physics) {
- PathAttachment attachment = slot.Attachment as PathAttachment;
- if (attachment == null) return;
-
- float mixRotate = this.mixRotate, mixX = this.mixX, mixY = this.mixY;
+ PathConstraintPose p = applied;
+ float mixRotate = p.mixRotate, mixX = p.mixX, mixY = p.mixY;
if (mixRotate == 0 && mixX == 0 && mixY == 0) return;
PathConstraintData data = this.data;
bool tangents = data.rotateMode == RotateMode.Tangent, scale = data.rotateMode == RotateMode.ChainScale;
int boneCount = this.bones.Count, spacesCount = tangents ? boneCount : boneCount + 1;
- Bone[] bonesItems = this.bones.Items;
+ BonePose[] bonesItems = this.bones.Items;
float[] spaces = this.spaces.Resize(spacesCount).Items, lengths = scale ? this.lengths.Resize(boneCount).Items : null;
- float spacing = this.spacing;
+ float spacing = p.spacing;
switch (data.spacingMode) {
case SpacingMode.Percent:
if (scale) {
for (int i = 0, n = spacesCount - 1; i < n; i++) {
- Bone bone = bonesItems[i];
- float setupLength = bone.data.length;
+ BonePose bone = bonesItems[i];
+ float setupLength = bone.bone.data.length;
float x = setupLength * bone.a, y = setupLength * bone.c;
lengths[i] = (float)Math.Sqrt(x * x + y * y);
}
@@ -125,9 +100,9 @@ namespace Spine {
case SpacingMode.Proportional: {
float sum = 0;
for (int i = 0, n = spacesCount - 1; i < n;) {
- Bone bone = bonesItems[i];
- float setupLength = bone.data.length;
- if (setupLength < PathConstraint.Epsilon) {
+ BonePose bone = bonesItems[i];
+ float setupLength = bone.bone.data.length;
+ if (setupLength < Epsilon) {
if (scale) lengths[i] = 0;
spaces[++i] = spacing;
} else {
@@ -148,8 +123,8 @@ namespace Spine {
default: {
bool lengthSpacing = data.spacingMode == SpacingMode.Length;
for (int i = 0, n = spacesCount - 1; i < n;) {
- Bone bone = bonesItems[i];
- float setupLength = bone.data.length;
+ BonePose bone = bonesItems[i];
+ float setupLength = bone.bone.data.length;
if (setupLength < PathConstraint.Epsilon) {
if (scale) lengths[i] = 0;
spaces[++i] = spacing;
@@ -157,31 +132,31 @@ namespace Spine {
float x = setupLength * bone.a, y = setupLength * bone.c;
float length = (float)Math.Sqrt(x * x + y * y);
if (scale) lengths[i] = length;
- spaces[++i] = (lengthSpacing ? setupLength + spacing : spacing) * length / setupLength;
+ spaces[++i] = (lengthSpacing ? Math.Max(0, setupLength + spacing) : spacing) * length / setupLength;
}
}
break;
}
}
- float[] positions = ComputeWorldPositions(attachment, spacesCount, tangents);
+ float[] positions = ComputeWorldPositions(skeleton, pathAttachment, spacesCount, tangents);
float boneX = positions[0], boneY = positions[1], offsetRotation = data.offsetRotation;
bool tip;
if (offsetRotation == 0) {
tip = data.rotateMode == RotateMode.Chain;
} else {
tip = false;
- Bone p = slot.bone;
- offsetRotation *= p.a * p.d - p.b * p.c > 0 ? MathUtils.DegRad : -MathUtils.DegRad;
+ BonePose bone = slot.bone.applied;
+ offsetRotation *= bone.a * bone.d - bone.b * bone.c > 0 ? MathUtils.DegRad : -MathUtils.DegRad;
}
- for (int i = 0, p = 3; i < boneCount; i++, p += 3) {
- Bone bone = bonesItems[i];
+ for (int i = 0, ip = 3, u = skeleton.update; i < boneCount; i++, ip += 3) {
+ BonePose bone = bonesItems[i];
bone.worldX += (boneX - bone.worldX) * mixX;
bone.worldY += (boneY - bone.worldY) * mixY;
- float x = positions[p], y = positions[p + 1], dx = x - boneX, dy = y - boneY;
+ float x = positions[ip], y = positions[ip + 1], dx = x - boneX, dy = y - boneY;
if (scale) {
float length = lengths[i];
- if (length >= PathConstraint.Epsilon) {
+ if (length >= Epsilon) {
float s = ((float)Math.Sqrt(dx * dx + dy * dy) / length - 1) * mixRotate + 1;
bone.a *= s;
bone.c *= s;
@@ -192,16 +167,16 @@ namespace Spine {
if (mixRotate > 0) {
float a = bone.a, b = bone.b, c = bone.c, d = bone.d, r, cos, sin;
if (tangents)
- r = positions[p - 1];
- else if (spaces[i + 1] < PathConstraint.Epsilon)
- r = positions[p + 2];
+ r = positions[ip - 1];
+ else if (spaces[i + 1] < Epsilon)
+ r = positions[ip + 2];
else
r = MathUtils.Atan2(dy, dx);
r -= MathUtils.Atan2(c, a);
if (tip) {
cos = MathUtils.Cos(r);
sin = MathUtils.Sin(r);
- float length = bone.data.length;
+ float length = bone.bone.data.length;
boneX += (length * (cos * a - sin * c) - dx) * mixRotate;
boneY += (length * (sin * a + cos * c) - dy) * mixRotate;
} else
@@ -218,13 +193,13 @@ namespace Spine {
bone.c = sin * a + cos * c;
bone.d = sin * b + cos * d;
}
- bone.UpdateAppliedTransform();
+ bone.ModifyWorld(u);
}
}
- float[] ComputeWorldPositions (PathAttachment path, int spacesCount, bool tangents) {
- Slot target = this.slot;
- float position = this.position;
+ float[] ComputeWorldPositions (Skeleton skeleton, PathAttachment path, int spacesCount, bool tangents) {
+ Slot slot = this.slot;
+ float position = applied.position;
float[] spaces = this.spaces.Items, output = this.positions.Resize(spacesCount * 3 + 2).Items, world;
bool closed = path.Closed;
int verticesLength = path.WorldVerticesLength, curveCount = verticesLength / 6, prevCurve = NONE;
@@ -262,14 +237,14 @@ namespace Spine {
} else if (p < 0) {
if (prevCurve != BEFORE) {
prevCurve = BEFORE;
- path.ComputeWorldVertices(target, 2, 4, world, 0, 2);
+ path.ComputeWorldVertices(skeleton, slot, 2, 4, world, 0, 2);
}
AddBeforePosition(p, world, 0, output, o);
continue;
} else if (p > pathLength) {
if (prevCurve != AFTER) {
prevCurve = AFTER;
- path.ComputeWorldVertices(target, verticesLength - 6, 4, world, 0, 2);
+ path.ComputeWorldVertices(skeleton, slot, verticesLength - 6, 4, world, 0, 2);
}
AddAfterPosition(p - pathLength, world, 0, output, o);
continue;
@@ -290,13 +265,13 @@ namespace Spine {
if (curve != prevCurve) {
prevCurve = curve;
if (closed && curve == curveCount) {
- path.ComputeWorldVertices(target, verticesLength - 4, 4, world, 0, 2);
- path.ComputeWorldVertices(target, 0, 4, world, 4, 2);
+ path.ComputeWorldVertices(skeleton, slot, verticesLength - 4, 4, world, 0, 2);
+ path.ComputeWorldVertices(skeleton, slot, 0, 4, world, 4, 2);
} else
- path.ComputeWorldVertices(target, curve * 6 + 2, 8, world, 0, 2);
+ path.ComputeWorldVertices(skeleton, slot, curve * 6 + 2, 8, world, 0, 2);
}
AddCurvePosition(p, world[0], world[1], world[2], world[3], world[4], world[5], world[6], world[7], output, o,
- tangents || (i > 0 && space < PathConstraint.Epsilon));
+ tangents || (i > 0 && space < Epsilon));
}
return output;
}
@@ -305,15 +280,15 @@ namespace Spine {
if (closed) {
verticesLength += 2;
world = this.world.Resize(verticesLength).Items;
- path.ComputeWorldVertices(target, 2, verticesLength - 4, world, 0, 2);
- path.ComputeWorldVertices(target, 0, 2, world, verticesLength - 4, 2);
+ path.ComputeWorldVertices(skeleton, slot, 2, verticesLength - 4, world, 0, 2);
+ path.ComputeWorldVertices(skeleton, slot, 0, 2, world, verticesLength - 4, 2);
world[verticesLength - 2] = world[0];
world[verticesLength - 1] = world[1];
} else {
curveCount--;
verticesLength -= 4;
world = this.world.Resize(verticesLength).Items;
- path.ComputeWorldVertices(target, 2, verticesLength, world, 0, 2);
+ path.ComputeWorldVertices(skeleton, slot, 2, verticesLength, world, 0, 2);
}
// Curve lengths.
@@ -378,6 +353,7 @@ namespace Spine {
p %= pathLength;
if (p < 0) p += pathLength;
curve = 0;
+ segment = 0;
} else if (p < 0) {
AddBeforePosition(p, world, 0, output, o);
continue;
@@ -493,26 +469,53 @@ namespace Spine {
}
}
- /// The position along the path.
- public float Position { get { return position; } set { position = value; } }
- /// The spacing between bones.
- public float Spacing { get { return spacing; } set { spacing = value; } }
- /// A percentage (0-1) that controls the mix between the constrained and unconstrained rotations.
- public float MixRotate { get { return mixRotate; } set { mixRotate = value; } }
- /// A percentage (0-1) that controls the mix between the constrained and unconstrained translation X.
- public float MixX { get { return mixX; } set { mixX = value; } }
- /// A percentage (0-1) that controls the mix between the constrained and unconstrained translation Y.
- public float MixY { get { return mixY; } set { mixY = value; } }
- /// The bones that will be modified by this path constraint.
- public ExposedList Bones { get { return bones; } }
- /// The slot whose path attachment will be used to constrained the bones.
- public Slot Target { get { return slot; } set { slot = value; } }
- public bool Active { get { return active; } }
- /// The path constraint's setup pose data.
- public PathConstraintData Data { get { return data; } }
-
- override public string ToString () {
- return data.name;
+ override public void Sort (Skeleton skeleton) {
+ int slotIndex = slot.Data.index;
+ Bone slotBone = slot.bone;
+ if (skeleton.skin != null) SortPathSlot(skeleton, skeleton.skin, slotIndex, slotBone);
+ if (skeleton.data.defaultSkin != null && skeleton.data.defaultSkin != skeleton.skin)
+ SortPathSlot(skeleton, skeleton.data.defaultSkin, slotIndex, slotBone);
+ SortPath(skeleton, slot.pose.attachment, slotBone);
+ BonePose[] bones = this.bones.Items;
+ int boneCount = this.bones.Count;
+ for (int i = 0; i < boneCount; i++) {
+ Bone bone = bones[i].bone;
+ skeleton.SortBone(bone);
+ skeleton.Constrained(bone);
+ }
+ skeleton.updateCache.Add(this);
+ for (int i = 0; i < boneCount; i++)
+ skeleton.SortReset(bones[i].bone.children);
+ for (int i = 0; i < boneCount; i++)
+ bones[i].bone.sorted = true;
}
+
+ private void SortPathSlot (Skeleton skeleton, Skin skin, int slotIndex, Bone slotBone) {
+ foreach (Skin.SkinEntry entry in skin.Attachments)
+ if (entry.SlotIndex == slotIndex) SortPath(skeleton, entry.Attachment, slotBone);
+ }
+
+ private void SortPath (Skeleton skeleton, Attachment attachment, Bone slotBone) {
+ if (!(attachment is PathAttachment)) return;
+ int[] pathBones = ((PathAttachment)attachment).bones;
+ if (pathBones == null)
+ skeleton.SortBone(slotBone);
+ else {
+ Bone[] bones = skeleton.bones.Items;
+ for (int i = 0, n = pathBones.Length; i < n;) {
+ int nn = pathBones[i++];
+ nn += i;
+ while (i < nn)
+ skeleton.SortBone(bones[pathBones[i++]]);
+ }
+ }
+ }
+
+ override public bool IsSourceActive { get { return slot.bone.active; } }
+
+ /// The bones that will be modified by this path constraint.
+ public ExposedList Bones { get { return bones; } }
+ /// The slot whose path attachment will be used to constrained the bones.
+ public Slot Slot { get { return slot; } set { slot = value; } }
}
}
diff --git a/spine-csharp/src/PathConstraintData.cs b/spine-csharp/src/PathConstraintData.cs
index 5025d53d8..2dd130cea 100644
--- a/spine-csharp/src/PathConstraintData.cs
+++ b/spine-csharp/src/PathConstraintData.cs
@@ -30,16 +30,20 @@
using System;
namespace Spine {
- public class PathConstraintData : ConstraintData {
+ public class PathConstraintData : ConstraintData {
internal ExposedList bones = new ExposedList();
internal SlotData slot;
internal PositionMode positionMode;
internal SpacingMode spacingMode;
internal RotateMode rotateMode;
internal float offsetRotation;
- internal float position, spacing, mixRotate, mixX, mixY;
- public PathConstraintData (string name) : base(name) {
+ public PathConstraintData (string name)
+ : base(name, new PathConstraintPose()) {
+ }
+
+ override public IConstraint Create (Skeleton skeleton) {
+ return new PathConstraint(this, skeleton);
}
public ExposedList Bones { get { return bones; } }
@@ -48,14 +52,6 @@ namespace Spine {
public SpacingMode SpacingMode { get { return spacingMode; } set { spacingMode = value; } }
public RotateMode RotateMode { get { return rotateMode; } set { rotateMode = value; } }
public float OffsetRotation { get { return offsetRotation; } set { offsetRotation = value; } }
- public float Position { get { return position; } set { position = value; } }
- public float Spacing { get { return spacing; } set { spacing = value; } }
- /// A percentage (0-1) that controls the mix between the constrained and unconstrained rotation.
- public float RotateMix { get { return mixRotate; } set { mixRotate = value; } }
- /// A percentage (0-1) that controls the mix between the constrained and unconstrained translation X.
- public float MixX { get { return mixX; } set { mixX = value; } }
- /// A percentage (0-1) that controls the mix between the constrained and unconstrained translation Y.
- public float MixY { get { return mixY; } set { mixY = value; } }
}
public enum PositionMode {
diff --git a/spine-csharp/src/PathConstraintPose.cs b/spine-csharp/src/PathConstraintPose.cs
new file mode 100644
index 000000000..4fec4f98c
--- /dev/null
+++ b/spine-csharp/src/PathConstraintPose.cs
@@ -0,0 +1,59 @@
+/******************************************************************************
+ * Spine Runtimes License Agreement
+ * Last updated April 5, 2025. Replaces all prior versions.
+ *
+ * Copyright (c) 2013-2025, Esoteric Software LLC
+ *
+ * Integration of the Spine Runtimes into software or otherwise creating
+ * derivative works of the Spine Runtimes is permitted under the terms and
+ * conditions of Section 2 of the Spine Editor License Agreement:
+ * http://esotericsoftware.com/spine-editor-license
+ *
+ * Otherwise, it is permitted to integrate the Spine Runtimes into software
+ * or otherwise create derivative works of the Spine Runtimes (collectively,
+ * "Products"), provided that each user of the Products must obtain their own
+ * Spine Editor license and redistribution of the Products in any form must
+ * include this license and copyright notice.
+ *
+ * THE SPINE RUNTIMES ARE PROVIDED BY ESOTERIC SOFTWARE LLC "AS IS" AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL ESOTERIC SOFTWARE LLC BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES,
+ * BUSINESS INTERRUPTION, OR LOSS OF USE, DATA, OR PROFITS) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
+ * THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ *****************************************************************************/
+
+using System;
+
+namespace Spine {
+
+ ///
+ /// Stores a pose for a path constraint.
+ ///
+ public class PathConstraintPose : IPose {
+ internal float position, spacing, mixRotate, mixX, mixY;
+
+ public void Set (PathConstraintPose pose) {
+ position = pose.position;
+ spacing = pose.spacing;
+ mixRotate = pose.mixRotate;
+ mixX = pose.mixX;
+ mixY = pose.mixY;
+ }
+
+ /// The position along the path.
+ public float Position { get { return position; } set { position = value; } }
+ /// The spacing between bones.
+ public float Spacing { get { return spacing; } set { spacing = value; } }
+ /// A percentage (0-1) that controls the mix between the constrained and unconstrained rotations.
+ public float MixRotate { get { return mixRotate; } set { mixRotate = value; } }
+ /// A percentage (0-1) that controls the mix between the constrained and unconstrained translation X.
+ public float MixX { get { return mixX; } set { mixX = value; } }
+ /// A percentage (0-1) that controls the mix between the constrained and unconstrained translation Y.
+ public float MixY { get { return mixY; } set { mixY = value; } }
+ }
+}
diff --git a/spine-csharp/src/PathConstraintPose.cs.meta b/spine-csharp/src/PathConstraintPose.cs.meta
new file mode 100644
index 000000000..3750d32d5
--- /dev/null
+++ b/spine-csharp/src/PathConstraintPose.cs.meta
@@ -0,0 +1,2 @@
+fileFormatVersion: 2
+guid: 40244ea3e8f247f4fac4363b219d8418
\ No newline at end of file
diff --git a/spine-csharp/src/Physics.cs b/spine-csharp/src/Physics.cs
new file mode 100644
index 000000000..ef9884193
--- /dev/null
+++ b/spine-csharp/src/Physics.cs
@@ -0,0 +1,46 @@
+/******************************************************************************
+ * Spine Runtimes License Agreement
+ * Last updated April 5, 2025. Replaces all prior versions.
+ *
+ * Copyright (c) 2013-2025, Esoteric Software LLC
+ *
+ * Integration of the Spine Runtimes into software or otherwise creating
+ * derivative works of the Spine Runtimes is permitted under the terms and
+ * conditions of Section 2 of the Spine Editor License Agreement:
+ * http://esotericsoftware.com/spine-editor-license
+ *
+ * Otherwise, it is permitted to integrate the Spine Runtimes into software
+ * or otherwise create derivative works of the Spine Runtimes (collectively,
+ * "Products"), provided that each user of the Products must obtain their own
+ * Spine Editor license and redistribution of the Products in any form must
+ * include this license and copyright notice.
+ *
+ * THE SPINE RUNTIMES ARE PROVIDED BY ESOTERIC SOFTWARE LLC "AS IS" AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL ESOTERIC SOFTWARE LLC BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES,
+ * BUSINESS INTERRUPTION, OR LOSS OF USE, DATA, OR PROFITS) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
+ * THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ *****************************************************************************/
+
+namespace Spine {
+
+ /// Determines how physics and other non-deterministic updates are applied.
+ public enum Physics {
+ /// Physics are not updated or applied.
+ None,
+
+ /// Physics are reset to the current pose.
+ Reset,
+
+ /// Physics are updated and the pose from physics is applied.
+ Update,
+
+ /// Physics are not updated but the pose from physics is applied.
+ Pose
+ }
+}
diff --git a/spine-csharp/src/Physics.cs.meta b/spine-csharp/src/Physics.cs.meta
new file mode 100644
index 000000000..db447bd88
--- /dev/null
+++ b/spine-csharp/src/Physics.cs.meta
@@ -0,0 +1,2 @@
+fileFormatVersion: 2
+guid: 2dab58587078e30468faff31f1ad4fa9
\ No newline at end of file
diff --git a/spine-csharp/src/PhysicsConstraint.cs b/spine-csharp/src/PhysicsConstraint.cs
index 328a08adc..a5dfc6e0f 100644
--- a/spine-csharp/src/PhysicsConstraint.cs
+++ b/spine-csharp/src/PhysicsConstraint.cs
@@ -30,17 +30,14 @@
using System;
namespace Spine {
- using Physics = Skeleton.Physics;
///
/// Stores the current pose for a physics constraint. A physics constraint applies physics to bones.
///
/// See Physics constraints in the Spine User Guide.
///
- public class PhysicsConstraint : IUpdatable {
- internal readonly PhysicsConstraintData data;
- internal Bone bone;
- internal float inertia, strength, damping, massInverse, wind, gravity, mix;
+ public class PhysicsConstraint : Constraint {
+ internal BonePose bone;
bool reset = true;
float ux, uy, cx, cy, tx, ty;
@@ -49,42 +46,22 @@ namespace Spine {
float rotateOffset, rotateLag, rotateVelocity;
float scaleOffset, scaleLag, 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.");
+ public PhysicsConstraint (PhysicsConstraintData data, Skeleton skeleton)
+ : base(data, new PhysicsConstraintPose(), new PhysicsConstraintPose()) {
if (skeleton == null) throw new ArgumentNullException("skeleton", "skeleton cannot be null.");
- this.data = data;
- this.skeleton = skeleton;
- bone = skeleton.bones.Items[data.bone.index];
-
- inertia = data.inertia;
- strength = data.strength;
- damping = data.damping;
- massInverse = data.massInverse;
- wind = data.wind;
- gravity = data.gravity;
- mix = data.mix;
+ bone = skeleton.bones.Items[data.bone.index].constrained;
}
- /// Copy constructor.
- public PhysicsConstraint (PhysicsConstraint constraint, Skeleton skeleton)
- : this(constraint.data, skeleton) {
-
- inertia = constraint.inertia;
- strength = constraint.strength;
- damping = constraint.damping;
- massInverse = constraint.massInverse;
- wind = constraint.wind;
- gravity = constraint.gravity;
- mix = constraint.mix;
+ override public IConstraint Copy (Skeleton skeleton) {
+ var copy = new PhysicsConstraint(data, skeleton);
+ copy.pose.Set(pose);
+ return copy;
}
- public void Reset () {
+ public void Reset (Skeleton skeleton) {
remaining = 0;
lastTime = skeleton.time;
reset = true;
@@ -102,20 +79,9 @@ namespace Spine {
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;
- }
-
///
- /// Translates the physics constraint so next forces are applied as if the bone moved an additional
- /// amount in world space.
+ /// Translates the physics constraint so next forces are applied as if the bone moved an
+ /// additional amount in world space.
///
public void Translate (float x, float y) {
ux -= x;
@@ -125,8 +91,8 @@ namespace Spine {
}
///
- /// Rotates the physics constraint so next forces are applied as if the bone rotated around the
- /// specified point in world space.
+ /// Rotates the physics constraint so next forces are applied as if the bone rotated around
+ /// the specified point in world space.
///
public void Rotate (float x, float y, float degrees) {
float r = degrees * MathUtils.DegRad, cos = (float)Math.Cos(r), sin = (float)Math.Sin(r);
@@ -135,23 +101,23 @@ namespace Spine {
}
/// Applies the constraint to the constrained bones.
- public void Update (Physics physics) {
- float mix = this.mix;
+ override public void Update (Skeleton skeleton, Physics physics) {
+ PhysicsConstraintPose p = applied;
+ float mix = p.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, t = data.step, z = 0;
+ BonePose bone = this.bone;
+ float l = bone.bone.data.length, t = data.step, z = 0;
switch (physics) {
case Physics.None:
return;
case Physics.Reset:
- Reset();
+ Reset(skeleton);
goto case Physics.Update; // Fall through.
case Physics.Update:
- Skeleton skeleton = this.skeleton;
- float delta = Math.Max(skeleton.time - lastTime, 0);
+ float delta = Math.Max(skeleton.time - lastTime, 0), aa = remaining;
remaining += delta;
lastTime = skeleton.time;
@@ -161,8 +127,8 @@ namespace Spine {
ux = bx;
uy = by;
} else {
- float a = remaining, i = inertia, f = skeleton.data.referenceScale, d = -1, qx = data.limit * delta,
- qy = qx * Math.Abs(skeleton.ScaleY);
+ float a = remaining, i = p.inertia, f = skeleton.data.referenceScale, d = -1, m = 0, e = 0, ax = 0, ay = 0,
+ qx = data.limit * delta, qy = qx * Math.Abs(skeleton.ScaleY);
qx *= Math.Abs(skeleton.ScaleX);
if (x || y) {
@@ -177,17 +143,21 @@ namespace Spine {
uy = by;
}
if (a >= t) {
- d = (float)Math.Pow(damping, 60 * t);
- float m = massInverse * t, e = strength, w = wind * f * skeleton.ScaleX,
- g = gravity * f * skeleton.ScaleY, xs = xOffset, ys = yOffset;
+ float xs = xOffset, ys = yOffset;
+ d = (float)Math.Pow(p.damping, 60 * t);
+ m = t * p.massInverse;
+ e = p.strength;
+ float w = f * p.wind, g = f * p.gravity;
+ ax = (w * skeleton.windX + g * skeleton.gravityX) * skeleton.scaleX;
+ ay = (w * skeleton.windY + g * skeleton.gravityY) * skeleton.ScaleY;
do {
if (x) {
- xVelocity += (w - xOffset * e) * m;
+ xVelocity += (ax - xOffset * e) * m;
xOffset += xVelocity * t;
xVelocity *= d;
}
if (y) {
- yVelocity -= (g + yOffset * e) * m;
+ yVelocity -= (ay + yOffset * e) * m;
yOffset += yVelocity * t;
yVelocity *= d;
}
@@ -210,12 +180,12 @@ namespace Spine {
dy = qy;
else if (dy < -qy)
dy = -qy;
- a = remaining;
if (rotateOrShearX) {
mr = (data.rotate + data.shearX) * mix;
- float rz = rotateLag * Math.Max(0, 1 - a / t), r = (float)Math.Atan2(dy + ty, dx + tx) - ca - (rotateOffset - rz) * mr;
+ z = rotateLag * Math.Max(0, 1 - aa / t);
+ float r = (float)Math.Atan2(dy + ty, dx + tx) - ca - (rotateOffset - z) * mr;
rotateOffset += (r - (float)Math.Ceiling(r * MathUtils.InvPI2 - 0.5f) * MathUtils.PI2) * i;
- r = (rotateOffset - rz) * mr + ca;
+ r = (rotateOffset - z) * mr + ca;
c = (float)Math.Cos(r);
s = (float)Math.Sin(r);
if (scaleX) {
@@ -225,23 +195,30 @@ namespace Spine {
} else {
c = (float)Math.Cos(ca);
s = (float)Math.Sin(ca);
- float r = l * bone.WorldScaleX;
+ float r = l * bone.WorldScaleX - scaleLag * Math.Max(0, 1 - aa / t);
if (r > 0) scaleOffset += (dx * c + dy * s) * i / r;
}
a = remaining;
if (a >= t) {
- if (d == -1) d = (float)Math.Pow(damping, 60 * t);
- float m = massInverse * t, e = strength, w = wind, g = (Bone.yDown ? -gravity : gravity), h = l / f,
- rs = rotateOffset, ss = scaleOffset;
+ if (d == -1) {
+ d = (float)Math.Pow(p.damping, 60 * t);
+ m = t * p.massInverse;
+ e = p.strength;
+ float w = f * p.wind, g = f * p.gravity;
+ ax = (w * skeleton.windX + g * skeleton.gravityX) * skeleton.scaleX;
+ ay = (w * skeleton.windY + g * skeleton.gravityY) * skeleton.ScaleY;
+ }
+ float rs = rotateOffset, ss = scaleOffset, h = l / f;
+ if (Spine.Bone.yDown) ay = -ay;
while (true) {
a -= t;
if (scaleX) {
- scaleVelocity += (w * c - g * s - scaleOffset * e) * m;
+ scaleVelocity += (ax * c - ay * s - scaleOffset * e) * m;
scaleOffset += scaleVelocity * t;
scaleVelocity *= d;
}
if (rotateOrShearX) {
- rotateVelocity -= ((w * s + g * c) * h + rotateOffset * e) * m;
+ rotateVelocity -= ((ax * s + ay * c) * h + rotateOffset * e) * m;
rotateOffset += rotateVelocity * t;
rotateVelocity *= d;
if (a < t) break;
@@ -307,32 +284,20 @@ namespace Spine {
tx = l * bone.a;
ty = l * bone.c;
}
- bone.UpdateAppliedTransform();
+ bone.ModifyWorld(skeleton.update);
}
+ override public void Sort (Skeleton skeleton) {
+ Bone bone = this.bone.bone;
+ skeleton.SortBone(bone);
+ skeleton.updateCache.Add(this);
+ skeleton.SortReset(bone.children);
+ skeleton.Constrained(bone);
+ }
+
+ override public bool IsSourceActive { get { return bone.bone.active; } }
+
/// The bone constrained by this physics constraint.
- 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; } }
- /// A percentage (0-1) that controls the mix between the constrained and unconstrained poses.
- public float Mix { get { return mix; } set { mix = value; } }
- public bool Active { get { return active; } }
-
-
- /// The physics constraint's setup pose data.
- public PhysicsConstraintData getData () {
- return data;
- }
-
- /// The physics constraint's setup pose data.
- public PhysicsConstraintData Data { get { return data; } }
-
- override public string ToString () {
- return data.name;
- }
+ public BonePose Bone { get { return bone; } set { bone = value; } }
}
}
diff --git a/spine-csharp/src/PhysicsConstraintData.cs b/spine-csharp/src/PhysicsConstraintData.cs
index 875a782ae..dbedd20a5 100644
--- a/spine-csharp/src/PhysicsConstraintData.cs
+++ b/spine-csharp/src/PhysicsConstraintData.cs
@@ -33,13 +33,17 @@ namespace Spine {
///
/// See Physics constraints in the Spine User Guide.
///