[libgdx] Added Posed and other base classes. Renamed BoneLocal/Pose. Removed skeleton fields, prefer passing.

This commit is contained in:
Nathan Sweet 2025-04-16 23:09:38 -04:00
parent a8c081dbb1
commit fff1606b6d
47 changed files with 793 additions and 1169 deletions

View File

@ -75,7 +75,7 @@ public class BonePlotting {
SkeletonData skeletonData = json.readSkeletonData(new FileHandle("assets/spineboy/spineboy-ess.json"));
Skeleton skeleton = new Skeleton(skeletonData);
BoneApplied bone = skeleton.findBone("gun-tip").getAppliedPose();
BonePose bone = skeleton.findBone("gun-tip").getAppliedPose();
// Pose the skeleton at regular intervals throughout each animation.
float fps = 1 / 15f;

View File

@ -161,7 +161,7 @@ public class Box2DExample extends ApplicationAdapter {
if (!(slot.getAppliedPose().getAttachment() instanceof Box2dAttachment)) continue;
Box2dAttachment attachment = (Box2dAttachment)slot.getAppliedPose().getAttachment();
if (attachment.body == null) continue;
BoneApplied bone = slot.getBone().getAppliedPose();
BonePose bone = slot.getBone().getAppliedPose();
float x = bone.getWorldX();
float y = bone.getWorldY();
float rotation = bone.getWorldRotationX();

View File

@ -47,7 +47,7 @@ public class SkeletonAttachmentTest extends ApplicationAdapter {
Skeleton spineboy, goblin;
AnimationState spineboyState, goblinState;
BoneApplied attachmentBone;
BonePose attachmentBone;
public void create () {
camera = new OrthographicCamera();

View File

@ -544,7 +544,7 @@ public class Animation {
}
}
/** Changes a bone's local {@link BonePose#getRotation()}. */
/** Changes a bone's local {@link BoneLocal#getRotation()}. */
static public class RotateTimeline extends CurveTimeline1 implements BoneTimeline {
final int boneIndex;
@ -562,13 +562,13 @@ public class Animation {
Bone bone = skeleton.bones.get(boneIndex);
if (bone.active) {
BonePose pose = appliedPose ? bone.applied : bone.pose;
BoneLocal pose = appliedPose ? bone.applied : bone.pose;
pose.rotation = getRelativeValue(time, alpha, blend, pose.rotation, bone.data.setup.rotation);
}
}
}
/** Changes a bone's local {@link BonePose#getX()} and {@link BonePose#getY()}. */
/** Changes a bone's local {@link BoneLocal#getX()} and {@link BoneLocal#getY()}. */
static public class TranslateTimeline extends CurveTimeline2 implements BoneTimeline {
final int boneIndex;
@ -588,7 +588,7 @@ public class Animation {
Bone bone = skeleton.bones.get(boneIndex);
if (!bone.active) return;
BonePose pose = appliedPose ? bone.applied : bone.pose, setup = bone.data.setup;
BoneLocal pose = appliedPose ? bone.applied : bone.pose, setup = bone.data.setup;
float[] frames = this.frames;
if (time < frames[0]) {
@ -641,7 +641,7 @@ public class Animation {
}
}
/** Changes a bone's local {@link BonePose#getX()}. */
/** Changes a bone's local {@link BoneLocal#getX()}. */
static public class TranslateXTimeline extends CurveTimeline1 implements BoneTimeline {
final int boneIndex;
@ -659,13 +659,13 @@ public class Animation {
Bone bone = skeleton.bones.get(boneIndex);
if (bone.active) {
BonePose pose = appliedPose ? bone.applied : bone.pose;
BoneLocal pose = appliedPose ? bone.applied : bone.pose;
pose.x = getRelativeValue(time, alpha, blend, pose.x, bone.data.setup.x);
}
}
}
/** Changes a bone's local {@link BonePose#getY()}. */
/** Changes a bone's local {@link BoneLocal#getY()}. */
static public class TranslateYTimeline extends CurveTimeline1 implements BoneTimeline {
final int boneIndex;
@ -683,13 +683,13 @@ public class Animation {
Bone bone = skeleton.bones.get(boneIndex);
if (bone.active) {
BonePose pose = appliedPose ? bone.applied : bone.pose;
BoneLocal pose = appliedPose ? bone.applied : bone.pose;
pose.y = getRelativeValue(time, alpha, blend, pose.y, bone.data.setup.y);
}
}
}
/** Changes a bone's local {@link BonePose#getScaleX()} and {@link BonePose#getScaleY()}. */
/** Changes a bone's local {@link BoneLocal#getScaleX()} and {@link BoneLocal#getScaleY()}. */
static public class ScaleTimeline extends CurveTimeline2 implements BoneTimeline {
final int boneIndex;
@ -709,7 +709,7 @@ public class Animation {
Bone bone = skeleton.bones.get(boneIndex);
if (!bone.active) return;
BonePose pose = appliedPose ? bone.applied : bone.pose, setup = bone.data.setup;
BoneLocal pose = appliedPose ? bone.applied : bone.pose, setup = bone.data.setup;
float[] frames = this.frames;
if (time < frames[0]) {
@ -801,7 +801,7 @@ public class Animation {
}
}
/** Changes a bone's local {@link BonePose#getScaleX()}. */
/** Changes a bone's local {@link BoneLocal#getScaleX()}. */
static public class ScaleXTimeline extends CurveTimeline1 implements BoneTimeline {
final int boneIndex;
@ -819,13 +819,13 @@ public class Animation {
Bone bone = skeleton.bones.get(boneIndex);
if (bone.active) {
BonePose pose = appliedPose ? bone.applied : bone.pose;
BoneLocal pose = appliedPose ? bone.applied : bone.pose;
pose.scaleX = getScaleValue(time, alpha, blend, direction, pose.scaleX, bone.data.setup.scaleX);
}
}
}
/** Changes a bone's local {@link BonePose#getScaleY()}. */
/** Changes a bone's local {@link BoneLocal#getScaleY()}. */
static public class ScaleYTimeline extends CurveTimeline1 implements BoneTimeline {
final int boneIndex;
@ -843,13 +843,13 @@ public class Animation {
Bone bone = skeleton.bones.get(boneIndex);
if (bone.active) {
BonePose pose = appliedPose ? bone.applied : bone.pose;
BoneLocal pose = appliedPose ? bone.applied : bone.pose;
pose.scaleY = getScaleValue(time, alpha, blend, direction, pose.scaleY, bone.data.setup.scaleY);
}
}
}
/** Changes a bone's local {@link BonePose#getShearX()} and {@link BonePose#getShearY()}. */
/** Changes a bone's local {@link BoneLocal#getShearX()} and {@link BoneLocal#getShearY()}. */
static public class ShearTimeline extends CurveTimeline2 implements BoneTimeline {
final int boneIndex;
@ -869,7 +869,7 @@ public class Animation {
Bone bone = skeleton.bones.get(boneIndex);
if (!bone.active) return;
BonePose pose = appliedPose ? bone.applied : bone.pose, setup = bone.data.setup;
BoneLocal pose = appliedPose ? bone.applied : bone.pose, setup = bone.data.setup;
float[] frames = this.frames;
if (time < frames[0]) {
@ -922,7 +922,7 @@ public class Animation {
}
}
/** Changes a bone's local {@link BonePose#getShearX()}. */
/** Changes a bone's local {@link BoneLocal#getShearX()}. */
static public class ShearXTimeline extends CurveTimeline1 implements BoneTimeline {
final int boneIndex;
@ -940,13 +940,13 @@ public class Animation {
Bone bone = skeleton.bones.get(boneIndex);
if (bone.active) {
BonePose pose = appliedPose ? bone.applied : bone.pose;
BoneLocal pose = appliedPose ? bone.applied : bone.pose;
pose.shearX = getRelativeValue(time, alpha, blend, pose.shearX, bone.data.setup.shearX);
}
}
}
/** Changes a bone's local {@link BonePose#getShearY()}. */
/** Changes a bone's local {@link BoneLocal#getShearY()}. */
static public class ShearYTimeline extends CurveTimeline1 implements BoneTimeline {
final int boneIndex;
@ -964,13 +964,13 @@ public class Animation {
Bone bone = skeleton.bones.get(boneIndex);
if (bone.active) {
BonePose pose = appliedPose ? bone.applied : bone.pose;
BoneLocal pose = appliedPose ? bone.applied : bone.pose;
pose.shearY = getRelativeValue(time, alpha, blend, pose.shearY, bone.data.setup.shearY);
}
}
}
/** Changes a bone's {@link BonePose#getInherit()}. */
/** Changes a bone's {@link BoneLocal#getInherit()}. */
static public class InheritTimeline extends Timeline implements BoneTimeline {
static public final int ENTRIES = 2;
static private final int INHERIT = 1;
@ -1004,7 +1004,7 @@ public class Animation {
Bone bone = skeleton.bones.get(boneIndex);
if (!bone.active) return;
BonePose pose = appliedPose ? bone.applied : bone.pose;
BoneLocal pose = appliedPose ? bone.applied : bone.pose;
if (direction == out) {
if (blend == setup) pose.inherit = bone.data.setup.inherit;
@ -2572,12 +2572,12 @@ public class Animation {
if (lastTime < frames[0] || time >= frames[search(frames, lastTime) + 1]) {
if (constraint != null)
constraint.reset();
constraint.reset(skeleton);
else {
Object[] constraints = skeleton.physicsConstraints.items;
for (int i = 0, n = skeleton.physicsConstraints.size; i < n; i++) {
constraint = (PhysicsConstraint)constraints[i];
if (constraint.active) constraint.reset();
if (constraint.active) constraint.reset(skeleton);
}
}
}

View File

@ -417,7 +417,7 @@ public class AnimationState {
Bone bone = skeleton.bones.get(timeline.boneIndex);
if (!bone.active) return;
BonePose pose = bone.pose, setup = bone.data.setup;
BoneLocal pose = bone.pose, setup = bone.data.setup;
float[] frames = timeline.frames;
float r1, r2;
if (time < frames[0]) { // Time is before first frame.

View File

@ -38,67 +38,24 @@ import com.badlogic.gdx.utils.Null;
* A bone has a local transform which is used to compute its world transform. A bone also has an applied transform, which is a
* local transform that can be applied to compute the world transform. The local transform and applied transform may differ if a
* constraint or application code modifies the world transform after it was computed from the local transform. */
public class Bone implements Constrained {
final BoneData data;
final Skeleton skeleton;
public class Bone extends PosedActive<BoneData, BoneLocal, BonePose> {
@Null final Bone parent;
final Array<Bone> children = new Array();
final BoneApplied pose = new BoneApplied(this), constrained = new BoneApplied(this);
BoneApplied applied = pose;
boolean sorted, active;
boolean sorted;
public Bone (BoneData data, Skeleton skeleton, @Null Bone parent) {
if (data == null) throw new IllegalArgumentException("data cannot be null.");
if (skeleton == null) throw new IllegalArgumentException("skeleton cannot be null.");
this.data = data;
this.skeleton = skeleton;
public Bone (BoneData data, @Null Bone parent) {
super(data, new BonePose(), new BonePose());
this.parent = parent;
setupPose();
applied.bone = this;
constrained.bone = this;
}
/** Copy constructor. Does not copy the {@link #getChildren()} bones. */
public Bone (Bone bone, Skeleton skeleton, @Null Bone parent) {
this.data = bone.data;
this.skeleton = skeleton;
this.parent = parent;
public Bone (Bone bone, @Null Bone parent) {
this(bone.data, parent);
pose.set(bone.pose);
}
/** Sets this bone's local transform to the setup pose. */
public void setupPose () {
pose.set(data.setup);
}
/** The bone's setup pose data. */
public BoneData getData () {
return data;
}
public BonePose getPose () {
return pose;
}
public BoneApplied getAppliedPose () {
return applied;
}
public BoneApplied getConstrainedPose () {
return constrained;
}
public void setConstrained (boolean constrained) {
applied = constrained ? this.constrained : pose;
}
public void resetAppliedPose () {
applied.set(pose);
}
/** The skeleton this bone belongs to. */
public Skeleton getSkeleton () {
return skeleton;
}
/** The parent bone, or null if this is the root bone. */
public @Null Bone getParent () {
return parent;
@ -108,18 +65,4 @@ public class Bone implements Constrained {
public Array<Bone> getChildren () {
return children;
}
/** Returns false when this bone won't be updated by {@link Skeleton#updateWorldTransform(com.esotericsoftware.spine.Physics)}
* because a skin is required and the {@link Skeleton#getSkin() active skin} does not contain this item.
* @see Skin#getBones()
* @see Skin#getConstraints()
* @see BoneData#getSkinRequired()
* @see Skeleton#updateCache() */
public boolean isActive () {
return active;
}
public String toString () {
return data.name;
}
}

View File

@ -1,376 +0,0 @@
package com.esotericsoftware.spine;
import static com.badlogic.gdx.math.Matrix3.*;
import static com.esotericsoftware.spine.utils.SpineUtils.*;
import com.badlogic.gdx.math.Matrix3;
import com.badlogic.gdx.math.Vector2;
import com.esotericsoftware.spine.BoneData.Inherit;
/** The applied pose for a bone. This is the {@link Bone} pose with constraints applied and the world transform computed by
* {@link Skeleton#updateWorldTransform(Physics)}. */
public class BoneApplied extends BonePose implements Update {
final Bone bone;
float a, b, worldX;
float c, d, worldY;
BoneApplied (Bone bone) {
this.bone = bone;
}
/** Computes the world transform using the parent bone and this bone's local applied transform. */
public void updateWorldTransform () {
update(null);
}
/** Computes the world transform using the parent bone and this bone's local transform.
* <p>
* See {@link #updateWorldTransform(float, float, float, float, float, float, float)}. */
/** Computes the world transform using the parent bone and the specified local transform. The applied transform is set to the
* specified local transform. Child bones are not updated.
* <p>
* See <a href="https://esotericsoftware.com/spine-runtime-skeletons#World-transforms">World transforms</a> in the Spine
* Runtimes Guide. */
public void update (Physics physics) {
if (bone.parent == null) { // Root bone.
Skeleton skeleton = bone.skeleton;
float sx = skeleton.scaleX, sy = skeleton.scaleY;
float rx = (rotation + shearX) * degRad;
float ry = (rotation + 90 + shearY) * degRad;
a = cos(rx) * scaleX * sx;
b = cos(ry) * scaleY * sx;
c = sin(rx) * scaleX * sy;
d = sin(ry) * scaleY * sy;
worldX = x * sx + skeleton.x;
worldY = y * sy + skeleton.y;
return;
}
BoneApplied parent = bone.parent.applied;
float pa = parent.a, pb = parent.b, pc = parent.c, pd = parent.d;
worldX = pa * x + pb * y + parent.worldX;
worldY = pc * x + pd * y + parent.worldY;
switch (inherit) {
case normal -> {
float rx = (rotation + shearX) * degRad;
float ry = (rotation + 90 + shearY) * degRad;
float la = cos(rx) * scaleX;
float lb = cos(ry) * scaleY;
float lc = sin(rx) * scaleX;
float ld = sin(ry) * scaleY;
a = pa * la + pb * lc;
b = pa * lb + pb * ld;
c = pc * la + pd * lc;
d = pc * lb + pd * ld;
return;
}
case onlyTranslation -> {
float rx = (rotation + shearX) * degRad;
float ry = (rotation + 90 + shearY) * degRad;
a = cos(rx) * scaleX;
b = cos(ry) * scaleY;
c = sin(rx) * scaleX;
d = sin(ry) * scaleY;
}
case noRotationOrReflection -> {
float sx = 1 / bone.skeleton.scaleX, sy = 1 / bone.skeleton.scaleY;
pa *= sx;
pc *= sy;
float s = pa * pa + pc * pc, prx;
if (s > 0.0001f) {
s = Math.abs(pa * pd * sy - pb * sx * pc) / s;
pb = pc * s;
pd = pa * s;
prx = atan2Deg(pc, pa);
} else {
pa = 0;
pc = 0;
prx = 90 - atan2Deg(pd, pb);
}
float rx = (rotation + shearX - prx) * degRad;
float ry = (rotation + shearY - prx + 90) * degRad;
float la = cos(rx) * scaleX;
float lb = cos(ry) * scaleY;
float lc = sin(rx) * scaleX;
float ld = sin(ry) * scaleY;
a = pa * la - pb * lc;
b = pa * lb - pb * ld;
c = pc * la + pd * lc;
d = pc * lb + pd * ld;
}
case noScale, noScaleOrReflection -> {
Skeleton skeleton = bone.skeleton;
rotation *= degRad;
float cos = cos(rotation), sin = 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);
if (s > 0.00001f) s = 1 / s;
za *= s;
zc *= s;
s = (float)Math.sqrt(za * za + zc * zc);
if (inherit == Inherit.noScale && (pa * pd - pb * pc < 0) != (skeleton.scaleX < 0 != skeleton.scaleY < 0)) s = -s;
rotation = PI / 2 + atan2(zc, za);
float zb = cos(rotation) * s;
float zd = sin(rotation) * s;
shearX *= degRad;
shearY = (90 + shearY) * degRad;
float la = cos(shearX) * scaleX;
float lb = cos(shearY) * scaleY;
float lc = sin(shearX) * scaleX;
float ld = sin(shearY) * scaleY;
a = za * la + zb * lc;
b = za * lb + zb * ld;
c = zc * la + zd * lc;
d = zc * lb + zd * ld;
}
}
Skeleton skeleton = bone.skeleton;
a *= skeleton.scaleX;
b *= skeleton.scaleX;
c *= skeleton.scaleY;
d *= skeleton.scaleY;
}
/** Computes the local transform values from the world transform.
* <p>
* If the world transform is modified (by a constraint, {@link #rotateWorld(float)}, etc) then this method should be called so
* the local transform matches the world transform. The local transform may be needed by other code (eg to apply another
* constraint).
* <p>
* Some information is ambiguous in the world transform, such as -1,-1 scale versus 180 rotation. The local transform after
* calling this method is equivalent to the local transform used to compute the world transform, but may not be identical. */
public void updateLocalTransform () {
BoneApplied parent = bone.parent.applied;
if (parent == null) {
x = worldX - bone.skeleton.x;
y = worldY - bone.skeleton.y;
float a = this.a, b = this.b, c = this.c, d = this.d;
rotation = atan2Deg(c, a);
scaleX = (float)Math.sqrt(a * a + c * c);
scaleY = (float)Math.sqrt(b * b + d * d);
shearX = 0;
shearY = 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;
x = (dx * ia - dy * ib);
y = (dy * id - dx * ic);
float ra, rb, rc, rd;
if (inherit == Inherit.onlyTranslation) {
ra = a;
rb = b;
rc = c;
rd = d;
} else {
Skeleton skeleton = bone.skeleton;
switch (inherit) {
case noRotationOrReflection -> {
float s = Math.abs(pa * pd - pb * pc) / (pa * pa + pc * pc);
pb = -pc * skeleton.scaleX * s / skeleton.scaleY;
pd = pa * skeleton.scaleY * s / skeleton.scaleX;
pid = 1 / (pa * pd - pb * pc);
ia = pd * pid;
ib = pb * pid;
}
case noScale, noScaleOrReflection -> {
float r = rotation * degRad, cos = cos(r), sin = 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);
if (s > 0.00001f) s = 1 / s;
pa *= s;
pc *= s;
s = (float)Math.sqrt(pa * pa + pc * pc);
if (inherit == Inherit.noScale && pid < 0 != (skeleton.scaleX < 0 != skeleton.scaleY < 0)) s = -s;
r = PI / 2 + atan2(pc, pa);
pb = cos(r) * s;
pd = sin(r) * s;
pid = 1 / (pa * pd - pb * pc);
ia = pd * pid;
ib = pb * pid;
ic = pc * pid;
id = pa * pid;
}
}
ra = ia * a - ib * c;
rb = ia * b - ib * d;
rc = id * c - ic * a;
rd = id * d - ic * b;
}
shearX = 0;
scaleX = (float)Math.sqrt(ra * ra + rc * rc);
if (scaleX > 0.0001f) {
float det = ra * rd - rb * rc;
scaleY = det / scaleX;
shearY = -atan2Deg(ra * rb + rc * rd, det);
rotation = atan2Deg(rc, ra);
} else {
scaleX = 0;
scaleY = (float)Math.sqrt(rb * rb + rd * rd);
shearY = 0;
rotation = 90 - atan2Deg(rd, rb);
}
}
/** Part of the world transform matrix for the X axis. If changed, {@link #updateLocalTransform()} should be called. */
public float getA () {
return a;
}
public void setA (float a) {
this.a = a;
}
/** Part of the world transform matrix for the Y axis. If changed, {@link #updateLocalTransform()} should be called. */
public float getB () {
return b;
}
public void setB (float b) {
this.b = b;
}
/** Part of the world transform matrix for the X axis. If changed, {@link #updateLocalTransform()} should be called. */
public float getC () {
return c;
}
public void setC (float c) {
this.c = c;
}
/** Part of the world transform matrix for the Y axis. If changed, {@link #updateLocalTransform()} should be called. */
public float getD () {
return d;
}
public void setD (float d) {
this.d = d;
}
/** The world X position. If changed, {@link #updateLocalTransform()} should be called. */
public float getWorldX () {
return worldX;
}
public void setWorldX (float worldX) {
this.worldX = worldX;
}
/** The world Y position. If changed, {@link #updateLocalTransform()} should be called. */
public float getWorldY () {
return worldY;
}
public void setWorldY (float worldY) {
this.worldY = worldY;
}
/** The world rotation for the X axis, calculated using {@link #a} and {@link #c}. */
public float getWorldRotationX () {
return atan2Deg(c, a);
}
/** The world rotation for the Y axis, calculated using {@link #b} and {@link #d}. */
public float getWorldRotationY () {
return atan2Deg(d, b);
}
/** The magnitude (always positive) of the world scale X, calculated using {@link #a} and {@link #c}. */
public float getWorldScaleX () {
return (float)Math.sqrt(a * a + c * c);
}
/** The magnitude (always positive) of the world scale Y, calculated using {@link #b} and {@link #d}. */
public float getWorldScaleY () {
return (float)Math.sqrt(b * b + d * d);
}
public Matrix3 getWorldTransform (Matrix3 worldTransform) {
if (worldTransform == null) throw new IllegalArgumentException("worldTransform cannot be null.");
float[] val = worldTransform.val;
val[M00] = a;
val[M01] = b;
val[M10] = c;
val[M11] = d;
val[M02] = worldX;
val[M12] = worldY;
val[M20] = 0;
val[M21] = 0;
val[M22] = 1;
return worldTransform;
}
/** Transforms a point from world coordinates to the bone's local coordinates. */
public Vector2 worldToLocal (Vector2 world) {
if (world == null) throw new IllegalArgumentException("world cannot be null.");
float det = a * d - b * c;
float x = world.x - worldX, y = world.y - worldY;
world.x = (x * d - y * b) / det;
world.y = (y * a - x * c) / det;
return world;
}
/** Transforms a point from the bone's local coordinates to world coordinates. */
public Vector2 localToWorld (Vector2 local) {
if (local == null) throw new IllegalArgumentException("local cannot be null.");
float x = local.x, y = local.y;
local.x = x * a + y * b + worldX;
local.y = x * c + y * d + worldY;
return local;
}
/** Transforms a point from world coordinates to the parent bone's local coordinates. */
public Vector2 worldToParent (Vector2 world) {
if (world == null) throw new IllegalArgumentException("world cannot be null.");
return bone.parent == null ? world : bone.parent.applied.worldToLocal(world);
}
/** Transforms a point from the parent bone's coordinates to world coordinates. */
public Vector2 parentToWorld (Vector2 world) {
if (world == null) throw new IllegalArgumentException("world cannot be null.");
return bone.parent == null ? world : bone.parent.applied.localToWorld(world);
}
/** Transforms a world rotation to a local rotation. */
public float worldToLocalRotation (float worldRotation) {
worldRotation *= degRad;
float sin = sin(worldRotation), cos = cos(worldRotation);
return atan2Deg(a * sin - c * cos, d * cos - b * sin) + rotation - shearX;
}
/** Transforms a local rotation to a world rotation. */
public float localToWorldRotation (float localRotation) {
localRotation = (localRotation - rotation - shearX) * degRad;
float sin = sin(localRotation), cos = cos(localRotation);
return atan2Deg(cos * c + sin * d, cos * a + sin * b);
}
/** Rotates the world transform the specified amount.
* <p>
* After changes are made to the world transform, {@link #updateLocalTransform()} should be called and {@link #update(Physics)}
* will need to be called on any child bones, recursively. */
public void rotateWorld (float degrees) {
degrees *= degRad;
float sin = sin(degrees), cos = 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;
}
public String toString () {
return bone.data.name;
}
}

View File

@ -33,13 +33,10 @@ import com.badlogic.gdx.graphics.Color;
import com.badlogic.gdx.utils.Null;
/** The setup pose for a bone. */
public class BoneData {
public class BoneData extends PosedData<BoneLocal> {
final int index;
final String name;
@Null final BoneData parent;
final BonePose setup = new BonePose();
float length;
boolean skinRequired;
// Nonessential.
final Color color = new Color(0.61f, 0.61f, 0.61f, 1); // 9b9b9bff
@ -47,18 +44,16 @@ public class BoneData {
boolean visible;
public BoneData (int index, String name, @Null BoneData parent) {
super(name, new BoneLocal());
if (index < 0) throw new IllegalArgumentException("index must be >= 0.");
if (name == null) throw new IllegalArgumentException("name cannot be null.");
this.index = index;
this.name = name;
this.parent = parent;
}
/** Copy constructor. */
public BoneData (BoneData data, @Null BoneData parent) {
index = data.index;
name = data.name;
this.parent = parent;
this(data.index, data.name, parent);
length = data.length;
setup.set(data.setup);
}
@ -68,19 +63,10 @@ public class BoneData {
return index;
}
/** The name of the bone, which is unique across all bones in the skeleton. */
public String getName () {
return name;
}
public @Null BoneData getParent () {
return parent;
}
public BonePose getSetupPose () {
return setup;
}
/** The bone's length. */
public float getLength () {
return length;
@ -90,18 +76,6 @@ public class BoneData {
this.length = length;
}
/** When true, {@link Skeleton#updateWorldTransform(Physics)} only updates this bone if the {@link Skeleton#getSkin()} contains
* this bone.
* <p>
* See {@link Skin#getBones()}. */
public boolean getSkinRequired () {
return skinRequired;
}
public void setSkinRequired (boolean skinRequired) {
this.skinRequired = skinRequired;
}
/** The color of the bone as it was in Spine, or a default color if nonessential data was not exported. Bones are not usually
* rendered at runtime. */
public Color getColor () {
@ -126,10 +100,6 @@ public class BoneData {
this.visible = visible;
}
public String toString () {
return name;
}
/** Determines how a bone inherits world transforms from parent bones. */
static public enum Inherit {
normal, onlyTranslation, noRotationOrReflection, noScale, noScaleOrReflection;

View File

@ -0,0 +1,141 @@
/******************************************************************************
* 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.
*****************************************************************************/
package com.esotericsoftware.spine;
import com.esotericsoftware.spine.BoneData.Inherit;
/** Stores a bone's local pose. */
public class BoneLocal implements Pose<BoneLocal> {
float x, y, rotation, scaleX, scaleY, shearX, shearY;
Inherit inherit;
BoneLocal () {
}
public void set (BoneLocal pose) {
if (pose == null) throw new IllegalArgumentException("pose cannot be null.");
x = pose.x;
y = pose.y;
rotation = pose.rotation;
scaleX = pose.scaleX;
scaleY = pose.scaleY;
shearX = pose.shearX;
shearY = pose.shearY;
inherit = pose.inherit;
}
/** The local x translation. */
public float getX () {
return x;
}
public void setX (float x) {
this.x = x;
}
/** The local y translation. */
public float getY () {
return y;
}
public void setY (float y) {
this.y = y;
}
public void setPosition (float x, float y) {
this.x = x;
this.y = y;
}
/** The local rotation in degrees, counter clockwise. */
public float getRotation () {
return rotation;
}
public void setRotation (float rotation) {
this.rotation = rotation;
}
/** The local scaleX. */
public float getScaleX () {
return scaleX;
}
public void setScaleX (float scaleX) {
this.scaleX = scaleX;
}
/** The local scaleY. */
public float getScaleY () {
return scaleY;
}
public void setScaleY (float scaleY) {
this.scaleY = scaleY;
}
public void setScale (float scaleX, float scaleY) {
this.scaleX = scaleX;
this.scaleY = scaleY;
}
public void setScale (float scale) {
scaleX = scale;
scaleY = scale;
}
/** The local shearX. */
public float getShearX () {
return shearX;
}
public void setShearX (float shearX) {
this.shearX = shearX;
}
/** The local shearY. */
public float getShearY () {
return shearY;
}
public void setShearY (float shearY) {
this.shearY = shearY;
}
/** Determines how parent world transforms affect this bone. */
public Inherit getInherit () {
return inherit;
}
public void setInherit (Inherit inherit) {
if (inherit == null) throw new IllegalArgumentException("inherit cannot be null.");
this.inherit = inherit;
}
}

View File

@ -1,141 +1,371 @@
/******************************************************************************
* 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.
*****************************************************************************/
package com.esotericsoftware.spine;
import static com.badlogic.gdx.math.Matrix3.*;
import static com.esotericsoftware.spine.utils.SpineUtils.*;
import com.badlogic.gdx.math.Matrix3;
import com.badlogic.gdx.math.Vector2;
import com.esotericsoftware.spine.BoneData.Inherit;
/** Stores a bone's local pose. */
public class BonePose {
float x, y, rotation, scaleX, scaleY, shearX, shearY;
Inherit inherit;
/** The applied pose for a bone. This is the {@link Bone} pose with constraints applied and the world transform computed by
* {@link Skeleton#updateWorldTransform(Physics)}. */
public class BonePose extends BoneLocal implements Update {
Bone bone;
float a, b, worldX;
float c, d, worldY;
BonePose () {
}
public void set (BonePose pose) {
if (pose == null) throw new IllegalArgumentException("pose cannot be null.");
x = pose.x;
y = pose.y;
rotation = pose.rotation;
scaleX = pose.scaleX;
scaleY = pose.scaleY;
shearX = pose.shearX;
shearY = pose.shearY;
inherit = pose.inherit;
/** Computes the world transform using the parent bone and this bone's local applied transform. */
public void updateWorldTransform (Skeleton skeleton) {
update(skeleton, null);
}
/** The local x translation. */
public float getX () {
return x;
/** Computes the world transform using the parent bone and this bone's local transform.
* <p>
* See {@link #updateWorldTransform(float, float, float, float, float, float, float)}. */
/** Computes the world transform using the parent bone and the specified local transform. The applied transform is set to the
* specified local transform. Child bones are not updated.
* <p>
* See <a href="https://esotericsoftware.com/spine-runtime-skeletons#World-transforms">World transforms</a> in the Spine
* Runtimes Guide. */
public void update (Skeleton skeleton, Physics physics) {
if (bone.parent == null) { // Root bone.
float sx = skeleton.scaleX, sy = skeleton.scaleY;
float rx = (rotation + shearX) * degRad;
float ry = (rotation + 90 + shearY) * degRad;
a = cos(rx) * scaleX * sx;
b = cos(ry) * scaleY * sx;
c = sin(rx) * scaleX * sy;
d = sin(ry) * scaleY * sy;
worldX = x * sx + skeleton.x;
worldY = y * sy + skeleton.y;
return;
}
BonePose parent = bone.parent.applied;
float pa = parent.a, pb = parent.b, pc = parent.c, pd = parent.d;
worldX = pa * x + pb * y + parent.worldX;
worldY = pc * x + pd * y + parent.worldY;
switch (inherit) {
case normal -> {
float rx = (rotation + shearX) * degRad;
float ry = (rotation + 90 + shearY) * degRad;
float la = cos(rx) * scaleX;
float lb = cos(ry) * scaleY;
float lc = sin(rx) * scaleX;
float ld = sin(ry) * scaleY;
a = pa * la + pb * lc;
b = pa * lb + pb * ld;
c = pc * la + pd * lc;
d = pc * lb + pd * ld;
return;
}
case onlyTranslation -> {
float rx = (rotation + shearX) * degRad;
float ry = (rotation + 90 + shearY) * degRad;
a = cos(rx) * scaleX;
b = cos(ry) * scaleY;
c = sin(rx) * scaleX;
d = sin(ry) * scaleY;
}
case noRotationOrReflection -> {
float sx = 1 / skeleton.scaleX, sy = 1 / skeleton.scaleY;
pa *= sx;
pc *= sy;
float s = pa * pa + pc * pc, prx;
if (s > 0.0001f) {
s = Math.abs(pa * pd * sy - pb * sx * pc) / s;
pb = pc * s;
pd = pa * s;
prx = atan2Deg(pc, pa);
} else {
pa = 0;
pc = 0;
prx = 90 - atan2Deg(pd, pb);
}
float rx = (rotation + shearX - prx) * degRad;
float ry = (rotation + shearY - prx + 90) * degRad;
float la = cos(rx) * scaleX;
float lb = cos(ry) * scaleY;
float lc = sin(rx) * scaleX;
float ld = sin(ry) * scaleY;
a = pa * la - pb * lc;
b = pa * lb - pb * ld;
c = pc * la + pd * lc;
d = pc * lb + pd * ld;
}
case noScale, noScaleOrReflection -> {
rotation *= degRad;
float cos = cos(rotation), sin = 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);
if (s > 0.00001f) s = 1 / s;
za *= s;
zc *= s;
s = (float)Math.sqrt(za * za + zc * zc);
if (inherit == Inherit.noScale && (pa * pd - pb * pc < 0) != (skeleton.scaleX < 0 != skeleton.scaleY < 0)) s = -s;
rotation = PI / 2 + atan2(zc, za);
float zb = cos(rotation) * s;
float zd = sin(rotation) * s;
shearX *= degRad;
shearY = (90 + shearY) * degRad;
float la = cos(shearX) * scaleX;
float lb = cos(shearY) * scaleY;
float lc = sin(shearX) * scaleX;
float ld = sin(shearY) * scaleY;
a = za * la + zb * lc;
b = za * lb + zb * ld;
c = zc * la + zd * lc;
d = zc * lb + zd * ld;
}
}
a *= skeleton.scaleX;
b *= skeleton.scaleX;
c *= skeleton.scaleY;
d *= skeleton.scaleY;
}
public void setX (float x) {
this.x = x;
/** Computes the local transform values from the world transform.
* <p>
* If the world transform is modified (by a constraint, {@link #rotateWorld(float)}, etc) then this method should be called so
* the local transform matches the world transform. The local transform may be needed by other code (eg to apply another
* constraint).
* <p>
* Some information is ambiguous in the world transform, such as -1,-1 scale versus 180 rotation. The local transform after
* calling this method is equivalent to the local transform used to compute the world transform, but may not be identical. */
public void updateLocalTransform (Skeleton skeleton) {
if (bone.parent == null) {
x = worldX - skeleton.x;
y = worldY - skeleton.y;
float a = this.a, b = this.b, c = this.c, d = this.d;
rotation = atan2Deg(c, a);
scaleX = (float)Math.sqrt(a * a + c * c);
scaleY = (float)Math.sqrt(b * b + d * d);
shearX = 0;
shearY = atan2Deg(a * b + c * d, a * d - b * c);
return;
}
BonePose parent = bone.parent.applied;
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;
x = (dx * ia - dy * ib);
y = (dy * id - dx * ic);
float ra, rb, rc, rd;
if (inherit == Inherit.onlyTranslation) {
ra = a;
rb = b;
rc = c;
rd = d;
} else {
switch (inherit) {
case noRotationOrReflection -> {
float s = Math.abs(pa * pd - pb * pc) / (pa * pa + pc * pc);
pb = -pc * skeleton.scaleX * s / skeleton.scaleY;
pd = pa * skeleton.scaleY * s / skeleton.scaleX;
pid = 1 / (pa * pd - pb * pc);
ia = pd * pid;
ib = pb * pid;
}
case noScale, noScaleOrReflection -> {
float r = rotation * degRad, cos = cos(r), sin = 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);
if (s > 0.00001f) s = 1 / s;
pa *= s;
pc *= s;
s = (float)Math.sqrt(pa * pa + pc * pc);
if (inherit == Inherit.noScale && pid < 0 != (skeleton.scaleX < 0 != skeleton.scaleY < 0)) s = -s;
r = PI / 2 + atan2(pc, pa);
pb = cos(r) * s;
pd = sin(r) * s;
pid = 1 / (pa * pd - pb * pc);
ia = pd * pid;
ib = pb * pid;
ic = pc * pid;
id = pa * pid;
}
}
ra = ia * a - ib * c;
rb = ia * b - ib * d;
rc = id * c - ic * a;
rd = id * d - ic * b;
}
shearX = 0;
scaleX = (float)Math.sqrt(ra * ra + rc * rc);
if (scaleX > 0.0001f) {
float det = ra * rd - rb * rc;
scaleY = det / scaleX;
shearY = -atan2Deg(ra * rb + rc * rd, det);
rotation = atan2Deg(rc, ra);
} else {
scaleX = 0;
scaleY = (float)Math.sqrt(rb * rb + rd * rd);
shearY = 0;
rotation = 90 - atan2Deg(rd, rb);
}
}
/** The local y translation. */
public float getY () {
return y;
/** Part of the world transform matrix for the X axis. If changed, {@link #updateLocalTransform(Skeleton)} should be called. */
public float getA () {
return a;
}
public void setY (float y) {
this.y = y;
public void setA (float a) {
this.a = a;
}
public void setPosition (float x, float y) {
this.x = x;
this.y = y;
/** Part of the world transform matrix for the Y axis. If changed, {@link #updateLocalTransform(Skeleton)} should be called. */
public float getB () {
return b;
}
/** The local rotation in degrees, counter clockwise. */
public float getRotation () {
return rotation;
public void setB (float b) {
this.b = b;
}
public void setRotation (float rotation) {
this.rotation = rotation;
/** Part of the world transform matrix for the X axis. If changed, {@link #updateLocalTransform(Skeleton)} should be called. */
public float getC () {
return c;
}
/** The local scaleX. */
public float getScaleX () {
return scaleX;
public void setC (float c) {
this.c = c;
}
public void setScaleX (float scaleX) {
this.scaleX = scaleX;
/** Part of the world transform matrix for the Y axis. If changed, {@link #updateLocalTransform(Skeleton)} should be called. */
public float getD () {
return d;
}
/** The local scaleY. */
public float getScaleY () {
return scaleY;
public void setD (float d) {
this.d = d;
}
public void setScaleY (float scaleY) {
this.scaleY = scaleY;
/** The world X position. If changed, {@link #updateLocalTransform(Skeleton)} should be called. */
public float getWorldX () {
return worldX;
}
public void setScale (float scaleX, float scaleY) {
this.scaleX = scaleX;
this.scaleY = scaleY;
public void setWorldX (float worldX) {
this.worldX = worldX;
}
public void setScale (float scale) {
scaleX = scale;
scaleY = scale;
/** The world Y position. If changed, {@link #updateLocalTransform(Skeleton)} should be called. */
public float getWorldY () {
return worldY;
}
/** The local shearX. */
public float getShearX () {
return shearX;
public void setWorldY (float worldY) {
this.worldY = worldY;
}
public void setShearX (float shearX) {
this.shearX = shearX;
/** The world rotation for the X axis, calculated using {@link #a} and {@link #c}. */
public float getWorldRotationX () {
return atan2Deg(c, a);
}
/** The local shearY. */
public float getShearY () {
return shearY;
/** The world rotation for the Y axis, calculated using {@link #b} and {@link #d}. */
public float getWorldRotationY () {
return atan2Deg(d, b);
}
public void setShearY (float shearY) {
this.shearY = shearY;
/** The magnitude (always positive) of the world scale X, calculated using {@link #a} and {@link #c}. */
public float getWorldScaleX () {
return (float)Math.sqrt(a * a + c * c);
}
/** Determines how parent world transforms affect this bone. */
public Inherit getInherit () {
return inherit;
/** The magnitude (always positive) of the world scale Y, calculated using {@link #b} and {@link #d}. */
public float getWorldScaleY () {
return (float)Math.sqrt(b * b + d * d);
}
public void setInherit (Inherit inherit) {
if (inherit == null) throw new IllegalArgumentException("inherit cannot be null.");
this.inherit = inherit;
public Matrix3 getWorldTransform (Matrix3 worldTransform) {
if (worldTransform == null) throw new IllegalArgumentException("worldTransform cannot be null.");
float[] val = worldTransform.val;
val[M00] = a;
val[M01] = b;
val[M10] = c;
val[M11] = d;
val[M02] = worldX;
val[M12] = worldY;
val[M20] = 0;
val[M21] = 0;
val[M22] = 1;
return worldTransform;
}
/** Transforms a point from world coordinates to the bone's local coordinates. */
public Vector2 worldToLocal (Vector2 world) {
if (world == null) throw new IllegalArgumentException("world cannot be null.");
float det = a * d - b * c;
float x = world.x - worldX, y = world.y - worldY;
world.x = (x * d - y * b) / det;
world.y = (y * a - x * c) / det;
return world;
}
/** Transforms a point from the bone's local coordinates to world coordinates. */
public Vector2 localToWorld (Vector2 local) {
if (local == null) throw new IllegalArgumentException("local cannot be null.");
float x = local.x, y = local.y;
local.x = x * a + y * b + worldX;
local.y = x * c + y * d + worldY;
return local;
}
/** Transforms a point from world coordinates to the parent bone's local coordinates. */
public Vector2 worldToParent (Vector2 world) {
if (world == null) throw new IllegalArgumentException("world cannot be null.");
return bone.parent == null ? world : bone.parent.applied.worldToLocal(world);
}
/** Transforms a point from the parent bone's coordinates to world coordinates. */
public Vector2 parentToWorld (Vector2 world) {
if (world == null) throw new IllegalArgumentException("world cannot be null.");
return bone.parent == null ? world : bone.parent.applied.localToWorld(world);
}
/** Transforms a world rotation to a local rotation. */
public float worldToLocalRotation (float worldRotation) {
worldRotation *= degRad;
float sin = sin(worldRotation), cos = cos(worldRotation);
return atan2Deg(a * sin - c * cos, d * cos - b * sin) + rotation - shearX;
}
/** Transforms a local rotation to a world rotation. */
public float localToWorldRotation (float localRotation) {
localRotation = (localRotation - rotation - shearX) * degRad;
float sin = sin(localRotation), cos = cos(localRotation);
return atan2Deg(cos * c + sin * d, cos * a + sin * b);
}
/** Rotates the world transform the specified amount.
* <p>
* After changes are made to the world transform, {@link #updateLocalTransform(Skeleton)} should be called and
* {@link #update(Skeleton, Physics)} will need to be called on any child bones, recursively. */
public void rotateWorld (float degrees) {
degrees *= degRad;
float sin = sin(degrees), cos = 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;
}
public String toString () {
return bone.data.name;
}
}

View File

@ -1,36 +0,0 @@
/******************************************************************************
* 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.
*****************************************************************************/
package com.esotericsoftware.spine;
public interface Constrained {
public void setConstrained (boolean constrained);
public void resetAppliedPose ();
}

View File

@ -0,0 +1,10 @@
package com.esotericsoftware.spine;
abstract public class Constraint<D extends PosedData<P>, P extends Pose> extends PosedActive<D, P, P> implements Update {
public Constraint (D data, P pose, P constrained) {
super(data, pose, constrained);
}
abstract public void sort ();
}

View File

@ -39,26 +39,19 @@ import com.esotericsoftware.spine.BoneData.Inherit;
* the last bone is as close to the target bone as possible.
* <p>
* See <a href="https://esotericsoftware.com/spine-ik-constraints">IK constraints</a> in the Spine User Guide. */
public class IkConstraint implements Constrained, Update {
final IkConstraintData data;
final Array<BoneApplied> bones;
public class IkConstraint extends Constraint<IkConstraintData, IkConstraintPose> {
final Array<BonePose> bones;
Bone target;
final IkConstraintPose pose = new IkConstraintPose(), constrained = new IkConstraintPose();
IkConstraintPose applied = pose;
boolean active;
public IkConstraint (IkConstraintData data, Skeleton skeleton) {
if (data == null) throw new IllegalArgumentException("data cannot be null.");
super(data, new IkConstraintPose(), new IkConstraintPose());
if (skeleton == null) throw new IllegalArgumentException("skeleton cannot be null.");
this.data = data;
bones = new Array(data.bones.size);
for (BoneData boneData : data.bones)
bones.add(skeleton.bones.get(boneData.index).constrained);
target = skeleton.bones.get(data.target.index);
setupPose();
}
/** Copy constructor. */
@ -67,26 +60,25 @@ public class IkConstraint implements Constrained, Update {
pose.set(constraint.pose);
}
public void setupPose () {
pose.set(data.setup);
}
/** Applies the constraint to the constrained bones. */
public void update (Physics physics) {
public void update (Skeleton skeleton, Physics physics) {
IkConstraintPose a = applied;
if (a.mix == 0) return;
BoneApplied target = this.target.applied;
BonePose target = this.target.applied;
Object[] bones = this.bones.items;
switch (this.bones.size) {
case 1 -> apply((BoneApplied)bones[0], target.worldX, target.worldY, a.compress, a.stretch, data.uniform, a.mix);
case 2 -> //
apply((BoneApplied)bones[0], (BoneApplied)bones[1], target.worldX, target.worldY, a.bendDirection, a.stretch,
data.uniform, a.softness, a.mix);
case 1 -> apply(skeleton, (BonePose)bones[0], target.worldX, target.worldY, a.compress, a.stretch, data.uniform, a.mix);
case 2 -> apply(skeleton, (BonePose)bones[0], (BonePose)bones[1], target.worldX, target.worldY, a.bendDirection, a.stretch,
data.uniform, a.softness, a.mix);
}
}
public void sort () {
// BOZO
}
/** The 1 or 2 bones that will be modified by this IK constraint. */
public Array<BoneApplied> getBones () {
public Array<BonePose> getBones () {
return bones;
}
@ -100,61 +92,20 @@ public class IkConstraint implements Constrained, Update {
this.target = target;
}
public IkConstraintPose getPose () {
return pose;
}
public IkConstraintPose getAppliedPose () {
return applied;
}
public IkConstraintPose getConstrainedPose () {
return constrained;
}
public void setConstrained (boolean constrained) {
applied = constrained ? this.constrained : pose;
}
public void resetAppliedPose () {
applied.set(pose);
}
/** Returns false when this constraint won't be updated by
* {@link Skeleton#updateWorldTransform(com.esotericsoftware.spine.Physics)} because a skin is required and the
* {@link Skeleton#getSkin() active skin} does not contain this item.
* @see Skin#getBones()
* @see Skin#getConstraints()
* @see ConstraintData#getSkinRequired()
* @see Skeleton#updateCache() */
public boolean isActive () {
return active;
}
/** The IK constraint's setup pose data. */
public IkConstraintData getData () {
return data;
}
public String toString () {
return data.name;
}
/** Applies 1 bone IK. The target is specified in the world coordinate system. */
static public void apply (BoneApplied bone, float targetX, float targetY, boolean compress, boolean stretch, boolean uniform,
float alpha) {
static public void apply (Skeleton skeleton, BonePose bone, float targetX, float targetY, boolean compress, boolean stretch,
boolean uniform, float alpha) {
if (bone == null) throw new IllegalArgumentException("bone cannot be null.");
BoneApplied p = bone.bone.parent.applied;
BonePose p = bone.bone.parent.applied;
float pa = p.a, pb = p.b, pc = p.c, pd = p.d;
float rotationIK = -bone.shearX - bone.rotation, tx, ty;
switch (bone.inherit) {
case onlyTranslation:
tx = (targetX - bone.worldX) * Math.signum(bone.bone.skeleton.scaleX);
ty = (targetY - bone.worldY) * Math.signum(bone.bone.skeleton.scaleY);
tx = (targetX - bone.worldX) * Math.signum(skeleton.scaleX);
ty = (targetY - bone.worldY) * Math.signum(skeleton.scaleY);
break;
case noRotationOrReflection:
float s = Math.abs(pa * pd - pb * pc) / Math.max(0.0001f, pa * pa + pc * pc);
Skeleton skeleton = bone.bone.skeleton;
float sa = pa / skeleton.scaleX;
float sc = pc / skeleton.scaleY;
pb = -sc * s * skeleton.scaleX;
@ -196,13 +147,13 @@ public class IkConstraint implements Constrained, Update {
}
}
}
bone.updateWorldTransform();
bone.updateWorldTransform(skeleton);
}
/** Applies 2 bone IK. The target is specified in the world coordinate system.
* @param child A direct descendant of the parent bone. */
static public void apply (BoneApplied parent, BoneApplied child, float targetX, float targetY, int bendDir, boolean stretch,
boolean uniform, float softness, float alpha) {
static public void apply (Skeleton skeleton, BonePose parent, BonePose child, float targetX, float targetY, int bendDir,
boolean stretch, boolean uniform, float softness, float alpha) {
if (parent == null) throw new IllegalArgumentException("parent cannot be null.");
if (child == null) throw new IllegalArgumentException("child cannot be null.");
if (parent.inherit != Inherit.normal || child.inherit != Inherit.normal) return;
@ -235,7 +186,7 @@ public class IkConstraint implements Constrained, Update {
cwx = a * child.x + b * child.y + parent.worldX;
cwy = c * child.x + d * child.y + parent.worldY;
}
BoneApplied pp = parent.bone.parent.applied;
BonePose pp = parent.bone.parent.applied;
a = pp.a;
b = pp.b;
c = pp.c;
@ -245,9 +196,9 @@ public class IkConstraint implements Constrained, Update {
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.bone.data.length * csx, a1, a2;
if (l1 < 0.0001f) {
apply(parent, targetX, targetY, false, stretch, false, alpha);
apply(skeleton, parent, targetX, targetY, false, stretch, false, alpha);
child.rotation = 0;
child.updateWorldTransform();
child.updateWorldTransform(skeleton);
return;
}
x = targetX - pp.worldX;
@ -342,13 +293,13 @@ public class IkConstraint implements Constrained, Update {
else if (a1 < -180) //
a1 += 360;
parent.rotation += a1 * alpha;
parent.updateWorldTransform();
parent.updateWorldTransform(skeleton);
a2 = ((a2 + os) * radDeg - child.shearX) * s2 + os2 - child.rotation;
if (a2 > 180)
a2 -= 360;
else if (a2 < -180) //
a2 += 360;
child.rotation += a2 * alpha;
child.updateWorldTransform();
child.updateWorldTransform(skeleton);
}
}

View File

@ -34,18 +34,13 @@ import com.badlogic.gdx.utils.Array;
/** Stores the setup pose for an {@link IkConstraint}.
* <p>
* See <a href="https://esotericsoftware.com/spine-ik-constraints">IK constraints</a> in the Spine User Guide. */
public class IkConstraintData extends ConstraintData {
final IkConstraintPose setup = new IkConstraintPose();
public class IkConstraintData extends PosedData<IkConstraintPose> {
final Array<BoneData> bones = new Array();
BoneData target;
boolean uniform;
public IkConstraintData (String name) {
super(name);
}
public IkConstraintPose getSetupPose () {
return setup;
super(name, new IkConstraintPose());
}
/** The bones that are constrained by this IK constraint. */

View File

@ -30,7 +30,7 @@
package com.esotericsoftware.spine;
/** Stores the current pose for an IK constraint. */
public class IkConstraintPose {
public class IkConstraintPose implements Pose<IkConstraintPose> {
int bendDirection;
boolean compress, stretch;
float mix, softness;

View File

@ -45,39 +45,26 @@ import com.esotericsoftware.spine.attachments.PathAttachment;
* constrained bones so they follow a {@link PathAttachment}.
* <p>
* See <a href="https://esotericsoftware.com/spine-path-constraints">Path constraints</a> in the Spine User Guide. */
public class PathConstraint implements Constrained, Update {
public class PathConstraint extends Constraint<PathConstraintData, PathConstraintPose> {
static final int NONE = -1, BEFORE = -2, AFTER = -3;
static final float epsilon = 0.00001f;
final PathConstraintData data;
final Array<BoneApplied> bones;
final Array<BonePose> bones;
Slot slot;
final PathConstraintPose pose = new PathConstraintPose(), constrained = new PathConstraintPose();
PathConstraintPose applied = pose;
boolean active;
private final FloatArray spaces = new FloatArray(), positions = new FloatArray();
private final FloatArray world = new FloatArray(), curves = new FloatArray(), lengths = new FloatArray();
private final float[] segments = new float[10];
public PathConstraint (PathConstraintData data, Array<BoneApplied> bones, Slot slot) {
this.data = data;
this.bones = bones;
this.slot = slot;
}
public PathConstraint (PathConstraintData data, Skeleton skeleton) {
if (data == null) throw new IllegalArgumentException("data cannot be null.");
super(data, new PathConstraintPose(), new PathConstraintPose());
if (skeleton == null) throw new IllegalArgumentException("skeleton cannot be null.");
this.data = data;
bones = new Array(data.bones.size);
for (BoneData boneData : data.bones)
bones.add(skeleton.bones.get(boneData.index).constrained);
slot = skeleton.slots.get(data.slot.index);
setupPose();
}
/** Copy constructor. */
@ -86,12 +73,8 @@ public class PathConstraint implements Constrained, Update {
pose.set(constraint.pose);
}
public void setupPose () {
pose.set(data.setup);
}
/** Applies the constraint to the constrained bones. */
public void update (Physics physics) {
public void update (Skeleton skeleton, Physics physics) {
if (!(slot.applied.attachment instanceof PathAttachment pathAttachment)) return;
PathConstraintPose pose = applied;
@ -109,7 +92,7 @@ public class PathConstraint implements Constrained, Update {
case percent -> {
if (scale) {
for (int i = 0, n = spacesCount - 1; i < n; i++) {
var bone = (BoneApplied)bones[i];
var bone = (BonePose)bones[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);
@ -120,7 +103,7 @@ public class PathConstraint implements Constrained, Update {
case proportional -> {
float sum = 0;
for (int i = 0, n = spacesCount - 1; i < n;) {
var bone = (BoneApplied)bones[i];
var bone = (BonePose)bones[i];
float setupLength = bone.bone.data.length;
if (setupLength < epsilon) {
if (scale) lengths[i] = 0;
@ -142,7 +125,7 @@ public class PathConstraint implements Constrained, Update {
default -> {
boolean lengthSpacing = data.spacingMode == SpacingMode.length;
for (int i = 0, n = spacesCount - 1; i < n;) {
var bone = (BoneApplied)bones[i];
var bone = (BonePose)bones[i];
float setupLength = bone.bone.data.length;
if (setupLength < epsilon) {
if (scale) lengths[i] = 0;
@ -157,18 +140,18 @@ public class PathConstraint implements Constrained, Update {
}
}
float[] positions = computeWorldPositions(pathAttachment, spacesCount, tangents);
float[] positions = computeWorldPositions(skeleton, pathAttachment, spacesCount, tangents);
float boneX = positions[0], boneY = positions[1], offsetRotation = data.offsetRotation;
boolean tip;
if (offsetRotation == 0)
tip = data.rotateMode == RotateMode.chain;
else {
tip = false;
BoneApplied p = slot.bone.applied;
BonePose p = slot.bone.applied;
offsetRotation *= p.a * p.d - p.b * p.c > 0 ? degRad : -degRad;
}
for (int i = 0, p = 3; i < boneCount; i++, p += 3) {
var bone = (BoneApplied)bones[i];
var bone = (BonePose)bones[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;
@ -211,11 +194,11 @@ public class PathConstraint implements Constrained, Update {
bone.c = sin * a + cos * c;
bone.d = sin * b + cos * d;
}
bone.updateLocalTransform();
bone.updateLocalTransform(skeleton);
}
}
float[] computeWorldPositions (PathAttachment path, int spacesCount, boolean tangents) {
float[] computeWorldPositions (Skeleton skeleton, PathAttachment path, int spacesCount, boolean tangents) {
Slot slot = this.slot;
float position = applied.position;
float[] spaces = this.spaces.items, out = this.positions.setSize(spacesCount * 3 + 2), world;
@ -248,14 +231,14 @@ public class PathConstraint implements Constrained, Update {
} else if (p < 0) {
if (prevCurve != BEFORE) {
prevCurve = BEFORE;
path.computeWorldVertices(slot, 2, 4, world, 0, 2);
path.computeWorldVertices(skeleton, slot, 2, 4, world, 0, 2);
}
addBeforePosition(p, world, 0, out, o);
continue;
} else if (p > pathLength) {
if (prevCurve != AFTER) {
prevCurve = AFTER;
path.computeWorldVertices(slot, verticesLength - 6, 4, world, 0, 2);
path.computeWorldVertices(skeleton, slot, verticesLength - 6, 4, world, 0, 2);
}
addAfterPosition(p - pathLength, world, 0, out, o);
continue;
@ -276,10 +259,10 @@ public class PathConstraint implements Constrained, Update {
if (curve != prevCurve) {
prevCurve = curve;
if (closed && curve == curveCount) {
path.computeWorldVertices(slot, verticesLength - 4, 4, world, 0, 2);
path.computeWorldVertices(slot, 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(slot, 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], out, o,
tangents || (i > 0 && space < epsilon));
@ -291,15 +274,15 @@ public class PathConstraint implements Constrained, Update {
if (closed) {
verticesLength += 2;
world = this.world.setSize(verticesLength);
path.computeWorldVertices(slot, 2, verticesLength - 4, world, 0, 2);
path.computeWorldVertices(slot, 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.setSize(verticesLength);
path.computeWorldVertices(slot, 2, verticesLength, world, 0, 2);
path.computeWorldVertices(skeleton, slot, 2, verticesLength, world, 0, 2);
}
// Curve lengths.
@ -473,8 +456,11 @@ public class PathConstraint implements Constrained, Update {
}
}
public void sort () {
}
/** The bones that will be modified by this path constraint. */
public Array<BoneApplied> getBones () {
public Array<BonePose> getBones () {
return bones;
}
@ -487,44 +473,4 @@ public class PathConstraint implements Constrained, Update {
if (slot == null) throw new IllegalArgumentException("slot cannot be null.");
this.slot = slot;
}
public PathConstraintPose getPose () {
return pose;
}
public PathConstraintPose getAppliedPose () {
return applied;
}
public PathConstraintPose getConstrainedPose () {
return constrained;
}
public void setConstrained (boolean constrained) {
applied = constrained ? this.constrained : pose;
}
public void resetAppliedPose () {
applied.set(pose);
}
/** Returns false when this constraint won't be updated by
* {@link Skeleton#updateWorldTransform(com.esotericsoftware.spine.Physics)} because a skin is required and the
* {@link Skeleton#getSkin() active skin} does not contain this item.
* @see Skin#getBones()
* @see Skin#getConstraints()
* @see ConstraintData#getSkinRequired()
* @see Skeleton#updateCache() */
public boolean isActive () {
return active;
}
/** The path constraint's setup pose data. */
public PathConstraintData getData () {
return data;
}
public String toString () {
return data.name;
}
}

View File

@ -34,8 +34,7 @@ import com.badlogic.gdx.utils.Array;
/** Stores the setup pose for a {@link PathConstraint}.
* <p>
* See <a href="https://esotericsoftware.com/spine-path-constraints">Path constraints</a> in the Spine User Guide. */
public class PathConstraintData extends ConstraintData {
final PathConstraintPose setup = new PathConstraintPose();
public class PathConstraintData extends PosedData<PathConstraintPose> {
final Array<BoneData> bones = new Array();
SlotData slot;
PositionMode positionMode;
@ -44,11 +43,7 @@ public class PathConstraintData extends ConstraintData {
float offsetRotation;
public PathConstraintData (String name) {
super(name);
}
public PathConstraintPose getSetupPose () {
return setup;
super(name, new PathConstraintPose());
}
/** The bones that will be modified by this path constraint. */

View File

@ -30,7 +30,7 @@
package com.esotericsoftware.spine;
/** Stores a pose for a path constraint. */
public class PathConstraintPose {
public class PathConstraintPose implements Pose<PathConstraintPose> {
float position, spacing, mixRotate, mixX, mixY;
public void set (PathConstraintPose pose) {

View File

@ -34,13 +34,8 @@ import static com.esotericsoftware.spine.utils.SpineUtils.*;
/** Stores the current pose for a physics constraint. A physics constraint applies physics to bones.
* <p>
* See <a href="https://esotericsoftware.com/spine-physics-constraints">Physics constraints</a> in the Spine User Guide. */
public class PhysicsConstraint implements Constrained, Update {
final PhysicsConstraintData data;
final Skeleton skeleton;
BoneApplied bone;
final PhysicsConstraintPose pose = new PhysicsConstraintPose(), constrained = new PhysicsConstraintPose();
PhysicsConstraintPose applied = pose;
boolean active;
public class PhysicsConstraint extends Constraint<PhysicsConstraintData, PhysicsConstraintPose> {
BonePose bone;
boolean reset = true;
float ux, uy, cx, cy, tx, ty;
@ -51,14 +46,10 @@ public class PhysicsConstraint implements Constrained, Update {
float remaining, lastTime;
public PhysicsConstraint (PhysicsConstraintData data, Skeleton skeleton) {
if (data == null) throw new IllegalArgumentException("data cannot be null.");
super(data, new PhysicsConstraintPose(), new PhysicsConstraintPose());
if (skeleton == null) throw new IllegalArgumentException("skeleton cannot be null.");
this.data = data;
this.skeleton = skeleton;
bone = skeleton.bones.get(data.bone.index).constrained;
setupPose();
}
/** Copy constructor. */
@ -67,7 +58,7 @@ public class PhysicsConstraint implements Constrained, Update {
pose.set(constraint.pose);
}
public void reset () {
public void reset (Skeleton skeleton) {
remaining = 0;
lastTime = skeleton.time;
reset = true;
@ -81,12 +72,8 @@ public class PhysicsConstraint implements Constrained, Update {
scaleVelocity = 0;
}
public void setupPose () {
pose.set(data.setup);
}
/** Translates the physics constraint so next {@link #update(Physics)} forces are applied as if the bone moved an additional
* amount in world space. */
/** Translates the physics constraint so next {@link #update(Skeleton, Physics)} forces are applied as if the bone moved an
* additional amount in world space. */
public void translate (float x, float y) {
ux -= x;
uy -= y;
@ -94,8 +81,8 @@ public class PhysicsConstraint implements Constrained, Update {
cy -= y;
}
/** Rotates the physics constraint so next {@link #update(Physics)} forces are applied as if the bone rotated around the
* specified point in world space. */
/** Rotates the physics constraint so next {@link #update(Skeleton, Physics)} 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 * degRad, cos = cos(r), sin = sin(r);
float dx = cx - x, dy = cy - y;
@ -103,23 +90,22 @@ public class PhysicsConstraint implements Constrained, Update {
}
/** Applies the constraint to the constrained bones. */
public void update (Physics physics) {
public void update (Skeleton skeleton, Physics physics) {
PhysicsConstraintPose pose = applied;
float mix = pose.mix;
if (mix == 0) return;
boolean x = data.x > 0, y = data.y > 0, rotateOrShearX = data.rotate > 0 || data.shearX > 0, scaleX = data.scaleX > 0;
BoneApplied bone = this.bone;
BonePose bone = this.bone;
float l = bone.bone.data.length;
switch (physics) {
case none:
return;
case reset:
reset();
reset(skeleton);
// Fall through.
case update:
Skeleton skeleton = this.skeleton;
float delta = Math.max(skeleton.time - lastTime, 0);
remaining += delta;
lastTime = skeleton.time;
@ -266,59 +252,18 @@ public class PhysicsConstraint implements Constrained, Update {
tx = l * bone.a;
ty = l * bone.c;
}
bone.updateLocalTransform();
bone.updateLocalTransform(skeleton);
}
/** The physics constraint's setup pose data. */
public PhysicsConstraintData getData () {
return data;
}
public Skeleton getSkeleton () {
return skeleton;
public void sort () {
}
/** The bone constrained by this physics constraint. */
public BoneApplied getBone () {
public BonePose getBone () {
return bone;
}
public void setBone (BoneApplied bone) {
public void setBone (BonePose bone) {
this.bone = bone;
}
public PhysicsConstraintPose getPose () {
return pose;
}
public PhysicsConstraintPose getAppliedPose () {
return applied;
}
public PhysicsConstraintPose getConstrainedPose () {
return constrained;
}
public void setConstrained (boolean constrained) {
applied = constrained ? this.constrained : pose;
}
public void resetAppliedPose () {
applied.set(pose);
}
/** Returns false when this constraint won't be updated by
* {@link Skeleton#updateWorldTransform(com.esotericsoftware.spine.Physics)} because a skin is required and the
* {@link Skeleton#getSkin() active skin} does not contain this item.
* @see Skin#getBones()
* @see Skin#getConstraints()
* @see ConstraintData#getSkinRequired()
* @see Skeleton#updateCache() */
public boolean isActive () {
return active;
}
public String toString () {
return data.name;
}
}

View File

@ -32,18 +32,13 @@ package com.esotericsoftware.spine;
/** Stores the setup pose for a {@link PhysicsConstraint}.
* <p>
* See <a href="https://esotericsoftware.com/spine-physics-constraints">Physics constraints</a> in the Spine User Guide. */
public class PhysicsConstraintData extends ConstraintData {
final PhysicsConstraintPose setup = new PhysicsConstraintPose();
public class PhysicsConstraintData extends PosedData<PhysicsConstraintPose> {
BoneData bone;
float x, y, rotate, scaleX, shearX, limit, step;
boolean inertiaGlobal, strengthGlobal, dampingGlobal, massGlobal, windGlobal, gravityGlobal, mixGlobal;
public PhysicsConstraintData (String name) {
super(name);
}
public PhysicsConstraintPose getSetupPose () {
return setup;
super(name, new PhysicsConstraintPose());
}
/** The bone constrained by this physics constraint. */

View File

@ -30,7 +30,7 @@
package com.esotericsoftware.spine;
/** Stores a pose for a physics constraint. */
public class PhysicsConstraintPose {
public class PhysicsConstraintPose implements Pose<PhysicsConstraintPose> {
float inertia, strength, damping, massInverse, wind, gravity, mix;
public void set (PhysicsConstraintPose pose) {

View File

@ -0,0 +1,6 @@
package com.esotericsoftware.spine;
public interface Pose<P> {
public void set (P pose);
}

View File

@ -0,0 +1,51 @@
package com.esotericsoftware.spine;
abstract public class Posed<D extends PosedData<P>, P extends Pose, A extends P> {
final D data;
final P pose;
final A constrained;
A applied;
public Posed (D data, P pose, A constrained) {
if (data == null) throw new IllegalArgumentException("data cannot be null.");
this.data = data;
this.pose = pose;
this.constrained = constrained;
applied = (A)pose;
setupPose();
}
public void setupPose () {
pose.set(data.setup);
}
/** The constraint's setup pose data. */
public D getData () {
return data;
}
public P getPose () {
return pose;
}
public A getAppliedPose () {
return applied;
}
public A getConstrainedPose () {
return constrained;
}
public void setConstrained (boolean constrained) {
applied = constrained ? this.constrained : (A)pose;
}
public void resetAppliedPose () {
applied.set(pose);
}
public String toString () {
return data.name;
}
}

View File

@ -0,0 +1,21 @@
package com.esotericsoftware.spine;
abstract public class PosedActive<D extends PosedData<P>, P extends Pose, A extends P> extends Posed<D, P, A> {
boolean active;
public PosedActive (D data, P pose, A constrained) {
super(data, pose, constrained);
}
/** Returns false when this constraint won't be updated by
* {@link Skeleton#updateWorldTransform(com.esotericsoftware.spine.Physics)} because a skin is required and the
* {@link Skeleton#getSkin() active skin} does not contain this item.
* @see Skin#getBones()
* @see Skin#getConstraints()
* @see PosedData#getSkinRequired()
* @see Skeleton#updateCache() */
public boolean isActive () {
return active;
}
}

View File

@ -29,15 +29,26 @@
package com.esotericsoftware.spine;
/** The base class for all constraint datas. */
abstract public class ConstraintData {
/** The base class for all constrained datas. */
abstract public class PosedData<P extends Pose> {
final String name;
int order;
final P setup;
boolean skinRequired;
public ConstraintData (String name) {
int order; // BOZO - Remove order.
public int getOrder () {
return order;
}
public void setOrder (int order) {
this.order = order;
}
public PosedData (String name, P setup) {
if (name == null) throw new IllegalArgumentException("name cannot be null.");
this.name = name;
this.setup = setup;
}
/** The constraint's name, which is unique across all constraints in the skeleton of the same type. */
@ -45,14 +56,8 @@ abstract public class ConstraintData {
return name;
}
/** The ordinal of this constraint for the order a skeleton's constraints will be applied by
* {@link Skeleton#updateWorldTransform(Physics)}. */
public int getOrder () {
return order;
}
public void setOrder (int order) {
this.order = order;
public P getSetupPose () {
return setup;
}
/** When true, {@link Skeleton#updateWorldTransform(Physics)} only updates this constraint if the {@link Skeleton#getSkin()}

View File

@ -64,7 +64,7 @@ public class Skeleton {
final Array<PathConstraint> pathConstraints;
final Array<PhysicsConstraint> physicsConstraints;
final Array updateCache = new Array();
final Array<Constrained> resetCache = new Array();
final Array<Posed> resetCache = new Array();
@Null Skin skin;
final Color color;
float x, y, scaleX = 1, scaleY = 1, time;
@ -78,10 +78,10 @@ public class Skeleton {
for (BoneData boneData : data.bones) {
Bone bone;
if (boneData.parent == null)
bone = new Bone(boneData, this, null);
bone = new Bone(boneData, null);
else {
var parent = (Bone)bones[boneData.parent.index];
bone = new Bone(boneData, this, parent);
bone = new Bone(boneData, parent);
parent.children.add(bone);
}
this.bones.add(bone);
@ -97,7 +97,7 @@ public class Skeleton {
sliders = new Array(data.sliders.size);
for (SliderData constraint : data.sliders)
sliders.add(new Slider(constraint, this));
sliders.add(new Slider(constraint));
ikConstraints = new Array(data.ikConstraints.size);
for (IkConstraintData constraint : data.ikConstraints)
@ -129,10 +129,10 @@ public class Skeleton {
for (Bone bone : skeleton.bones) {
Bone newBone;
if (bone.parent == null)
newBone = new Bone(bone, this, null);
newBone = new Bone(bone, null);
else {
Bone parent = bones.get(bone.parent.data.index);
newBone = new Bone(bone, this, parent);
newBone = new Bone(bone, parent);
parent.children.add(newBone);
}
bones.add(newBone);
@ -150,7 +150,7 @@ public class Skeleton {
sliders = new Array(skeleton.sliders.size);
for (Slider constraint : skeleton.sliders)
sliders.add(new Slider(constraint, skeleton));
sliders.add(new Slider(constraint));
ikConstraints = new Array(skeleton.ikConstraints.size);
for (IkConstraint constraint : skeleton.ikConstraints)
@ -276,7 +276,7 @@ public class Skeleton {
sortBone(constraint.target);
Array<BoneApplied> constrained = constraint.bones;
Array<BonePose> constrained = constraint.bones;
Bone parent = constrained.first().bone;
sortBone(parent);
resetCache(parent);
@ -306,14 +306,14 @@ public class Skeleton {
int boneCount = constraint.bones.size;
if (constraint.data.localSource) {
for (int i = 0; i < boneCount; i++) {
Bone child = ((BoneApplied)constrained[i]).bone;
Bone child = ((BonePose)constrained[i]).bone;
resetCache(child);
sortBone(child.parent);
sortBone(child);
}
} else {
for (int i = 0; i < boneCount; i++) {
Bone bone = ((BoneApplied)constrained[i]).bone;
Bone bone = ((BonePose)constrained[i]).bone;
resetCache(bone);
sortBone(bone);
}
@ -322,9 +322,9 @@ public class Skeleton {
updateCache.add(constraint);
for (int i = 0; i < boneCount; i++)
sortReset(((BoneApplied)constrained[i]).bone.children);
sortReset(((BonePose)constrained[i]).bone.children);
for (int i = 0; i < boneCount; i++)
((BoneApplied)constrained[i]).bone.sorted = true;
((BonePose)constrained[i]).bone.sorted = true;
}
private void sortPathConstraint (PathConstraint constraint) {
@ -344,7 +344,7 @@ public class Skeleton {
Object[] constrained = constraint.bones.items;
int boneCount = constraint.bones.size;
for (int i = 0; i < boneCount; i++) {
Bone bone = ((BoneApplied)constrained[i]).bone;
Bone bone = ((BonePose)constrained[i]).bone;
resetCache(bone);
sortBone(bone);
}
@ -352,9 +352,9 @@ public class Skeleton {
updateCache.add(constraint);
for (int i = 0; i < boneCount; i++)
sortReset(((BoneApplied)constrained[i]).bone.children);
sortReset(((BonePose)constrained[i]).bone.children);
for (int i = 0; i < boneCount; i++)
((BoneApplied)constrained[i]).bone.sorted = true;
((BonePose)constrained[i]).bone.sorted = true;
}
private void sortPathConstraintAttachment (Skin skin, int slotIndex, Bone slotBone) {
@ -423,7 +423,7 @@ public class Skeleton {
if (timelines[i] instanceof BoneTimeline boneTimeline) sortBone((Bone)bones[boneTimeline.getBoneIndex()]);
}
private void resetCache (Constrained object) {
private void resetCache (Posed object) {
if (!resetCache.contains(object, true)) {
resetCache.add(object);
object.setConstrained(true);
@ -455,11 +455,11 @@ public class Skeleton {
public void updateWorldTransform (Physics physics) {
Object[] resetCache = this.resetCache.items;
for (int i = 0, n = this.resetCache.size; i < n; i++)
((Constrained)resetCache[i]).resetAppliedPose();
((Posed)resetCache[i]).resetAppliedPose();
Object[] updateCache = this.updateCache.items;
for (int i = 0, n = this.updateCache.size; i < n; i++)
((Update)updateCache[i]).update(physics);
((Update)updateCache[i]).update(this, physics);
}
/** Temporarily sets the root bone as a child of the specified bone, then updates the world transform for each bone and applies
@ -467,15 +467,15 @@ public class Skeleton {
* <p>
* See <a href="https://esotericsoftware.com/spine-runtime-skeletons#World-transforms">World transforms</a> in the Spine
* Runtimes Guide. */
public void updateWorldTransform (Physics physics, BoneApplied parent) {
public void updateWorldTransform (Physics physics, BonePose parent) {
if (parent == null) throw new IllegalArgumentException("parent cannot be null.");
Object[] resetCache = this.resetCache.items;
for (int i = 0, n = this.resetCache.size; i < n; i++)
((Constrained)resetCache[i]).resetAppliedPose();
((Posed)resetCache[i]).resetAppliedPose();
// Apply the parent bone transform to the root bone. The root bone always inherits scale, rotation and reflection.
BoneApplied rootBone = getRootBone().applied;
BonePose rootBone = getRootBone().applied;
float pa = parent.a, pb = parent.b, pc = parent.c, pd = parent.d;
rootBone.worldX = pa * x + pb * y + parent.worldX;
rootBone.worldY = pc * x + pd * y + parent.worldY;
@ -495,7 +495,7 @@ public class Skeleton {
Object[] updateCache = this.updateCache.items;
for (int i = 0, n = this.updateCache.size; i < n; i++) {
var updatable = (Update)updateCache[i];
if (updatable != rootBone) updatable.update(physics);
if (updatable != rootBone) updatable.update(this, physics);
}
}
@ -804,10 +804,10 @@ public class Skeleton {
} else if (attachment instanceof MeshAttachment mesh) {
verticesLength = mesh.getWorldVerticesLength();
vertices = temp.setSize(verticesLength);
mesh.computeWorldVertices(slot, 0, verticesLength, vertices, 0, 2);
mesh.computeWorldVertices(this, slot, 0, verticesLength, vertices, 0, 2);
triangles = mesh.getTriangles();
} else if (attachment instanceof ClippingAttachment clip && clipper != null) {
clipper.clipStart(slot, clip);
clipper.clipStart(this, slot, clip);
continue;
}
if (vertices != null) {

View File

@ -217,7 +217,7 @@ public class SkeletonBinary extends SkeletonLoader {
String name = input.readString();
BoneData parent = i == 0 ? null : (BoneData)bones[input.readInt(true)];
var data = new BoneData(i, name, parent);
BonePose setup = data.setup;
BoneLocal setup = data.setup;
setup.rotation = input.readFloat();
setup.x = input.readFloat() * scale;
setup.y = input.readFloat() * scale;
@ -493,16 +493,16 @@ public class SkeletonBinary extends SkeletonLoader {
items = skeletonData.ikConstraints.items;
for (int i = 0, n = input.readInt(true); i < n; i++)
skin.constraints.add((ConstraintData)items[input.readInt(true)]);
skin.constraints.add((PosedData)items[input.readInt(true)]);
items = skeletonData.transformConstraints.items;
for (int i = 0, n = input.readInt(true); i < n; i++)
skin.constraints.add((ConstraintData)items[input.readInt(true)]);
skin.constraints.add((PosedData)items[input.readInt(true)]);
items = skeletonData.pathConstraints.items;
for (int i = 0, n = input.readInt(true); i < n; i++)
skin.constraints.add((ConstraintData)items[input.readInt(true)]);
skin.constraints.add((PosedData)items[input.readInt(true)]);
items = skeletonData.physicsConstraints.items;
for (int i = 0, n = input.readInt(true); i < n; i++)
skin.constraints.add((ConstraintData)items[input.readInt(true)]);
skin.constraints.add((PosedData)items[input.readInt(true)]);
skin.constraints.shrink();
slotCount = input.readInt(true);

View File

@ -73,7 +73,7 @@ public class SkeletonBounds {
FloatArray polygon = polygonPool.obtain();
polygons.add(polygon);
boundingBox.computeWorldVertices(slot, 0, boundingBox.getWorldVerticesLength(),
boundingBox.computeWorldVertices(skeleton, slot, 0, boundingBox.getWorldVerticesLength(),
polygon.setSize(boundingBox.getWorldVerticesLength()), 0, 2);
}
}

View File

@ -174,7 +174,7 @@ public class SkeletonJson extends SkeletonLoader {
}
var data = new BoneData(skeletonData.bones.size, boneMap.getString("name"), parent);
data.length = boneMap.getFloat("length", 0) * scale;
BonePose setup = data.setup;
BoneLocal setup = data.setup;
setup.x = boneMap.getFloat("x", 0) * scale;
setup.y = boneMap.getFloat("y", 0) * scale;
setup.rotation = boneMap.getFloat("rotation", 0);

View File

@ -165,14 +165,14 @@ public class SkeletonRenderer {
int count = mesh.getWorldVerticesLength();
verticesLength = (count >> 1) * 5;
vertices = this.vertices.setSize(verticesLength);
mesh.computeWorldVertices(slot, 0, count, vertices, 0, 5);
mesh.computeWorldVertices(skeleton, slot, 0, count, vertices, 0, 5);
triangles = mesh.getTriangles();
texture = mesh.getRegion().getTexture();
uvs = mesh.getUVs();
color = mesh.getColor();
} else if (attachment instanceof ClippingAttachment clip) {
clipper.clipStart(slot, clip);
clipper.clipStart(skeleton, slot, clip);
continue;
} else if (attachment instanceof SkeletonAttachment skeletonAttachment) {
@ -260,14 +260,14 @@ public class SkeletonRenderer {
int count = mesh.getWorldVerticesLength();
verticesLength = count * 3;
vertices = this.vertices.setSize(verticesLength);
mesh.computeWorldVertices(slot, 0, count, vertices, 0, 6);
mesh.computeWorldVertices(skeleton, slot, 0, count, vertices, 0, 6);
triangles = mesh.getTriangles();
texture = mesh.getRegion().getTexture();
uvs = mesh.getUVs();
color = mesh.getColor();
} else if (attachment instanceof ClippingAttachment clip) {
clipper.clipStart(slot, clip);
clipper.clipStart(skeleton, slot, clip);
continue;
} else if (attachment instanceof SkeletonAttachment skeletonAttachment) {

View File

@ -95,7 +95,7 @@ public class SkeletonRendererDebug {
shapes.setColor(boneOriginColor);
} else
shapes.setColor(boneLineColor);
BoneApplied applied = bone.applied;
BonePose applied = bone.applied;
float x = length * applied.a + applied.worldX;
float y = length * applied.c + applied.worldY;
shapes.rectLine(applied.worldX, applied.worldY, x, y, width * scale);
@ -140,7 +140,7 @@ public class SkeletonRendererDebug {
if (!slot.bone.active) continue;
if (!(slot.pose.attachment instanceof MeshAttachment mesh)) continue;
float[] vertices = this.vertices.setSize(mesh.getWorldVerticesLength());
mesh.computeWorldVertices(slot, 0, mesh.getWorldVerticesLength(), vertices, 0, 2);
mesh.computeWorldVertices(skeleton, slot, 0, mesh.getWorldVerticesLength(), vertices, 0, 2);
short[] triangles = mesh.getTriangles();
int hullLength = mesh.getHullLength();
if (drawMeshTriangles) {
@ -187,7 +187,7 @@ public class SkeletonRendererDebug {
if (!(slot.pose.attachment instanceof ClippingAttachment clip)) continue;
int nn = clip.getWorldVerticesLength();
float[] vertices = this.vertices.setSize(nn);
clip.computeWorldVertices(slot, 0, nn, vertices, 0, 2);
clip.computeWorldVertices(skeleton, slot, 0, nn, vertices, 0, 2);
shapes.setColor(clip.getColor());
for (int ii = 2; ii < nn; ii += 2)
shapes.line(vertices[ii - 2], vertices[ii - 1], vertices[ii], vertices[ii + 1]);
@ -202,7 +202,7 @@ public class SkeletonRendererDebug {
if (!(slot.pose.attachment instanceof PathAttachment path)) continue;
int nn = path.getWorldVerticesLength();
float[] vertices = this.vertices.setSize(nn);
path.computeWorldVertices(slot, 0, nn, vertices, 0, 2);
path.computeWorldVertices(skeleton, slot, 0, nn, vertices, 0, 2);
Color color = path.getColor();
float x1 = vertices[2], y1 = vertices[3], x2 = 0, y2 = 0;
if (path.getClosed()) {

View File

@ -45,7 +45,7 @@ public class Skin {
final String name;
final OrderedSet<SkinEntry> attachments = new OrderedSet();
final Array<BoneData> bones = new Array(0);
final Array<ConstraintData> constraints = new Array(0);
final Array<PosedData> constraints = new Array(0);
private final SkinEntry lookup = new SkinEntry(0, "", null);
// Nonessential.
@ -71,7 +71,7 @@ public class Skin {
for (BoneData data : skin.bones)
if (!bones.contains(data, true)) bones.add(data);
for (ConstraintData data : skin.constraints)
for (PosedData data : skin.constraints)
if (!constraints.contains(data, true)) constraints.add(data);
for (SkinEntry entry : skin.attachments.orderedItems())
@ -86,7 +86,7 @@ public class Skin {
for (BoneData data : skin.bones)
if (!bones.contains(data, true)) bones.add(data);
for (ConstraintData data : skin.constraints)
for (PosedData data : skin.constraints)
if (!constraints.contains(data, true)) constraints.add(data);
for (SkinEntry entry : skin.attachments.orderedItems()) {
@ -134,7 +134,7 @@ public class Skin {
return bones;
}
public Array<ConstraintData> getConstraints () {
public Array<PosedData> getConstraints () {
return constraints;
}

View File

@ -35,70 +35,22 @@ import com.esotericsoftware.spine.Animation.MixDirection;
/** Stores the setup pose for a {@link PhysicsConstraint}.
* <p>
* See <a href="https://esotericsoftware.com/spine-physics-constraints">Physics constraints</a> in the Spine User Guide. */
public class Slider implements Constrained, Update {
final SliderData data;
final Skeleton skeleton;
final SliderPose pose = new SliderPose(), constrained = new SliderPose();
SliderPose applied = pose;
boolean active;
public Slider (SliderData data, Skeleton skeleton) {
if (data == null) throw new IllegalArgumentException("data cannot be null.");
if (skeleton == null) throw new IllegalArgumentException("skeleton cannot be null.");
this.data = data;
this.skeleton = skeleton;
setupPose();
public class Slider extends Constraint<SliderData, SliderPose> {
public Slider (SliderData data) {
super(data, new SliderPose(), new SliderPose());
}
/** Copy constructor. */
public Slider (Slider slider, Skeleton skeleton) {
this(slider.data, skeleton);
public Slider (Slider slider) {
this(slider.data);
pose.set(slider.pose);
}
public void update (Physics physics) {
public void update (Skeleton skeleton, Physics physics) {
SliderPose pose = applied;
data.animation.apply(skeleton, pose.time, pose.time, false, null, pose.mix, MixBlend.replace, MixDirection.in, true);
}
public void setupPose () {
pose.set(data.setup);
}
public SliderPose getPose () {
return pose;
}
public SliderPose getAppliedPose () {
return applied;
}
public SliderPose getConstrainedPose () {
return constrained;
}
public void setConstrained (boolean constrained) {
applied = constrained ? this.constrained : pose;
}
public void resetAppliedPose () {
applied.set(pose);
}
/** Returns false when this constraint won't be updated by
* {@link Skeleton#updateWorldTransform(com.esotericsoftware.spine.Physics)} because a skin is required and the
* {@link Skeleton#getSkin() active skin} does not contain this item.
* @see Skin#getBones()
* @see Skin#getConstraints()
* @see ConstraintData#getSkinRequired()
* @see Skeleton#updateCache() */
public boolean isActive () {
return active;
}
public String toString () {
return data.name;
public void sort () {
}
}

View File

@ -32,16 +32,11 @@ package com.esotericsoftware.spine;
/** Stores the setup pose for a {@link PhysicsConstraint}.
* <p>
* See <a href="https://esotericsoftware.com/spine-physics-constraints">Physics constraints</a> in the Spine User Guide. */
public class SliderData extends ConstraintData {
final SliderPose setup = new SliderPose();
public class SliderData extends PosedData<SliderPose> {
Animation animation;
public SliderData (String name) {
super(name);
}
public SliderPose getSetupPose () {
return setup;
super(name, new SliderPose());
}
public Animation getAnimation () {

View File

@ -30,7 +30,7 @@
package com.esotericsoftware.spine;
/** Stores a pose for a slider. */
public class SliderPose {
public class SliderPose implements Pose<SliderPose> {
float time, mix;
public void set (SliderPose pose) {

View File

@ -34,17 +34,13 @@ import com.badlogic.gdx.graphics.Color;
/** Stores a slot's current pose. Slots organize attachments for {@link Skeleton#drawOrder} purposes and provide a place to store
* state for an attachment. State cannot be stored in an attachment itself because attachments are stateless and may be shared
* across multiple skeletons. */
public class Slot implements Constrained {
final SlotData data;
public class Slot extends Posed<SlotData, SlotPose, SlotPose> {
final Bone bone;
final SlotPose pose = new SlotPose(), constrained = new SlotPose();
SlotPose applied = pose;
int attachmentState;
public Slot (SlotData data, Skeleton skeleton) {
if (data == null) throw new IllegalArgumentException("slot cannot be null.");
super(data, new SlotPose(), new SlotPose());
if (skeleton == null) throw new IllegalArgumentException("skeleton cannot be null.");
this.data = data;
bone = skeleton.bones.get(data.boneData.index);
if (data.setup.darkColor != null) {
pose.darkColor = new Color();
@ -55,9 +51,7 @@ public class Slot implements Constrained {
/** Copy constructor. */
public Slot (Slot slot, Bone bone) {
if (slot == null) throw new IllegalArgumentException("slot cannot be null.");
if (bone == null) throw new IllegalArgumentException("bone cannot be null.");
data = slot.data;
super(slot.data, new SlotPose(), new SlotPose());
this.bone = bone;
if (data.setup.darkColor != null) {
pose.darkColor = new Color();
@ -66,48 +60,8 @@ public class Slot implements Constrained {
pose.set(slot.pose);
}
/** Sets this slot to the setup pose. */
public void setupPose () {
pose.set(data.setup);
if (data.attachmentName != null) pose.setAttachment(bone.skeleton.getAttachment(data.index, data.attachmentName));
}
/** The slot's setup pose data. */
public SlotData getData () {
return data;
}
public SlotPose getPose () {
return pose;
}
public SlotPose getAppliedPose () {
return applied;
}
public SlotPose getConstrainedPose () {
return constrained;
}
public void setConstrained (boolean constrained) {
applied = constrained ? this.constrained : pose;
}
public void resetAppliedPose () {
applied.set(pose);
}
/** The bone this slot belongs to. */
public Bone getBone () {
return bone;
}
/** The skeleton this slot belongs to. */
public Skeleton getSkeleton () {
return bone.skeleton;
}
public String toString () {
return data.name;
}
}

View File

@ -32,11 +32,9 @@ package com.esotericsoftware.spine;
import com.badlogic.gdx.utils.Null;
/** Stores the setup pose for a {@link Slot}. */
public class SlotData {
public class SlotData extends PosedData<SlotPose> {
final int index;
final String name;
final BoneData boneData;
final SlotPose setup = new SlotPose();
@Null String attachmentName;
BlendMode blendMode;
@ -44,11 +42,10 @@ public class SlotData {
boolean visible = true;
public SlotData (int index, String name, BoneData boneData) {
super(name, new SlotPose());
if (index < 0) throw new IllegalArgumentException("index must be >= 0.");
if (name == null) throw new IllegalArgumentException("name cannot be null.");
if (boneData == null) throw new IllegalArgumentException("boneData cannot be null.");
this.index = index;
this.name = name;
this.boneData = boneData;
}
@ -57,20 +54,11 @@ public class SlotData {
return index;
}
/** The name of the slot, which is unique across all slots in the skeleton. */
public String getName () {
return name;
}
/** The bone this slot belongs to. */
public BoneData getBoneData () {
return boneData;
}
public SlotPose getSetupPose () {
return setup;
}
public void setAttachmentName (@Null String attachmentName) {
this.attachmentName = attachmentName;
}
@ -98,8 +86,4 @@ public class SlotData {
public void setVisible (boolean visible) {
this.visible = visible;
}
public String toString () {
return name;
}
}

View File

@ -41,7 +41,7 @@ import com.esotericsoftware.spine.attachments.VertexAttachment;
/** Stores a slot's pose. Slots organize attachments for {@link Skeleton#drawOrder} purposes and provide a place to store state
* for an attachment. State cannot be stored in an attachment itself because attachments are stateless and may be shared across
* multiple skeletons. */
public class SlotPose {
public class SlotPose implements Pose<SlotPose> {
final Color color = new Color();
@Null Color darkColor;
@Null Attachment attachment; // Not used in setup pose.
@ -104,7 +104,8 @@ public class SlotPose {
/** Values to deform the slot's attachment. For an unweighted mesh, the entries are local positions for each vertex. For a
* weighted mesh, the entries are an offset for each vertex which will be added to the mesh's local vertex positions.
* <p>
* See {@link VertexAttachment#computeWorldVertices(Slot, int, int, float[], int, int)} and {@link DeformTimeline}. */
* See {@link VertexAttachment#computeWorldVertices(Skeleton, Slot, int, int, float[], int, int)} and
* {@link DeformTimeline}. */
public FloatArray getDeform () {
return deform;
}

View File

@ -40,32 +40,19 @@ import com.esotericsoftware.spine.TransformConstraintData.ToProperty;
* bones to match that of the source bone.
* <p>
* See <a href="https://esotericsoftware.com/spine-transform-constraints">Transform constraints</a> in the Spine User Guide. */
public class TransformConstraint implements Constrained, Update {
final TransformConstraintData data;
final Array<BoneApplied> bones;
public class TransformConstraint extends Constraint<TransformConstraintData, TransformConstraintPose> {
final Array<BonePose> bones;
Bone source;
final TransformConstraintPose pose = new TransformConstraintPose(), constrained = new TransformConstraintPose();
TransformConstraintPose applied = pose;
boolean active;
public TransformConstraint (TransformConstraintData data, Array<BoneApplied> bones, Bone source) {
this.data = data;
this.bones = bones;
this.source = source;
}
public TransformConstraint (TransformConstraintData data, Skeleton skeleton) {
if (data == null) throw new IllegalArgumentException("data cannot be null.");
super(data, new TransformConstraintPose(), new TransformConstraintPose());
if (skeleton == null) throw new IllegalArgumentException("skeleton cannot be null.");
this.data = data;
bones = new Array(data.bones.size);
for (BoneData boneData : data.bones)
bones.add(skeleton.bones.get(boneData.index).constrained);
source = skeleton.bones.get(data.source.index);
setupPose();
}
/** Copy constructor. */
@ -74,24 +61,20 @@ public class TransformConstraint implements Constrained, Update {
pose.set(constraint.pose);
}
public void setupPose () {
pose.set(data.setup);
}
/** Applies the constraint to the constrained bones. */
public void update (Physics physics) {
public void update (Skeleton skeleton, Physics physics) {
TransformConstraintPose pose = applied;
if (pose.mixRotate == 0 && pose.mixX == 0 && pose.mixY == 0 && pose.mixScaleX == 0 && pose.mixScaleY == 0
&& pose.mixShearY == 0) return;
TransformConstraintData data = this.data;
boolean localFrom = data.localSource, localTarget = data.localTarget, additive = data.additive, clamp = data.clamp;
BoneApplied source = this.source.applied;
BonePose source = this.source.applied;
Object[] fromItems = data.properties.items;
int fn = data.properties.size;
Object[] bones = this.bones.items;
for (int i = 0, n = this.bones.size; i < n; i++) {
var bone = (BoneApplied)bones[i];
var bone = (BonePose)bones[i];
if (bone.bone.applied != bone.bone.constrained) System.out.println();
for (int f = 0; f < fn; f++) {
var from = (FromProperty)fromItems[f];
@ -112,14 +95,17 @@ public class TransformConstraint implements Constrained, Update {
}
}
if (localTarget)
bone.update(null);
bone.update(skeleton, null);
else
bone.updateLocalTransform();
bone.updateLocalTransform(skeleton);
}
}
public void sort () {
}
/** The bones that will be modified by this transform constraint. */
public Array<BoneApplied> getBones () {
public Array<BonePose> getBones () {
return bones;
}
@ -132,44 +118,4 @@ public class TransformConstraint implements Constrained, Update {
if (source == null) throw new IllegalArgumentException("source cannot be null.");
this.source = source;
}
public TransformConstraintPose getPose () {
return pose;
}
public TransformConstraintPose getAppliedPose () {
return applied;
}
public TransformConstraintPose getConstrainedPose () {
return constrained;
}
public void setConstrained (boolean constrained) {
applied = constrained ? this.constrained : pose;
}
public void resetAppliedPose () {
applied.set(pose);
}
/** Returns false when this constraint won't be updated by
* {@link Skeleton#updateWorldTransform(com.esotericsoftware.spine.Physics)} because a skin is required and the
* {@link Skeleton#getSkin() active skin} does not contain this item.
* @see Skin#getBones()
* @see Skin#getConstraints()
* @see ConstraintData#getSkinRequired()
* @see Skeleton#updateCache() */
public boolean isActive () {
return active;
}
/** The transform constraint's setup pose data. */
public TransformConstraintData getData () {
return data;
}
public String toString () {
return data.name;
}
}

View File

@ -36,8 +36,7 @@ import com.badlogic.gdx.utils.Array;
/** Stores the setup pose for a {@link TransformConstraint}.
* <p>
* See <a href="https://esotericsoftware.com/spine-transform-constraints">Transform constraints</a> in the Spine User Guide. */
public class TransformConstraintData extends ConstraintData {
final TransformConstraintPose setup = new TransformConstraintPose();
public class TransformConstraintData extends PosedData<TransformConstraintPose> {
final Array<BoneData> bones = new Array();
BoneData source;
float offsetRotation, offsetX, offsetY, offsetScaleX, offsetScaleY, offsetShearY;
@ -45,11 +44,7 @@ public class TransformConstraintData extends ConstraintData {
final Array<FromProperty> properties = new Array();
public TransformConstraintData (String name) {
super(name);
}
public TransformConstraintPose getSetupPose () {
return setup;
super(name, new TransformConstraintPose());
}
/** The bones that will be modified by this transform constraint. */
@ -171,7 +166,7 @@ public class TransformConstraintData extends ConstraintData {
public final Array<ToProperty> to = new Array();
/** Reads this property from the specified bone. */
abstract public float value (TransformConstraintData data, BoneApplied source, boolean local);
abstract public float value (TransformConstraintData data, BonePose source, boolean local);
}
/** Constrained property for a {@link TransformConstraint}. */
@ -189,11 +184,11 @@ public class TransformConstraintData extends ConstraintData {
abstract public float mix (TransformConstraintPose pose);
/** Applies the value to this property. */
abstract public void apply (TransformConstraintPose pose, BoneApplied bone, float value, boolean local, boolean additive);
abstract public void apply (TransformConstraintPose pose, BonePose bone, float value, boolean local, boolean additive);
}
static public class FromRotate extends FromProperty {
public float value (TransformConstraintData data, BoneApplied source, boolean local) {
public float value (TransformConstraintData data, BonePose source, boolean local) {
if (local) return source.rotation + data.offsetRotation;
float value = atan2(source.c, source.a) * radDeg
+ (source.a * source.d - source.b * source.c > 0 ? data.offsetRotation : -data.offsetRotation);
@ -207,7 +202,7 @@ public class TransformConstraintData extends ConstraintData {
return pose.mixRotate;
}
public void apply (TransformConstraintPose pose, BoneApplied bone, float value, boolean local, boolean additive) {
public void apply (TransformConstraintPose pose, BonePose bone, float value, boolean local, boolean additive) {
if (local) {
if (!additive) value -= bone.rotation;
bone.rotation += value * pose.mixRotate;
@ -230,7 +225,7 @@ public class TransformConstraintData extends ConstraintData {
}
static public class FromX extends FromProperty {
public float value (TransformConstraintData data, BoneApplied source, boolean local) {
public float value (TransformConstraintData data, BonePose source, boolean local) {
return local ? source.x + data.offsetX : data.offsetX * source.a + data.offsetY * source.b + source.worldX;
}
}
@ -240,7 +235,7 @@ public class TransformConstraintData extends ConstraintData {
return pose.mixX;
}
public void apply (TransformConstraintPose pose, BoneApplied bone, float value, boolean local, boolean additive) {
public void apply (TransformConstraintPose pose, BonePose bone, float value, boolean local, boolean additive) {
if (local) {
if (!additive) value -= bone.x;
bone.x += value * pose.mixX;
@ -252,7 +247,7 @@ public class TransformConstraintData extends ConstraintData {
}
static public class FromY extends FromProperty {
public float value (TransformConstraintData data, BoneApplied source, boolean local) {
public float value (TransformConstraintData data, BonePose source, boolean local) {
return local ? source.y + data.offsetY : data.offsetX * source.c + data.offsetY * source.d + source.worldY;
}
}
@ -262,7 +257,7 @@ public class TransformConstraintData extends ConstraintData {
return pose.mixY;
}
public void apply (TransformConstraintPose pose, BoneApplied bone, float value, boolean local, boolean additive) {
public void apply (TransformConstraintPose pose, BonePose bone, float value, boolean local, boolean additive) {
if (local) {
if (!additive) value -= bone.y;
bone.y += value * pose.mixY;
@ -274,7 +269,7 @@ public class TransformConstraintData extends ConstraintData {
}
static public class FromScaleX extends FromProperty {
public float value (TransformConstraintData data, BoneApplied source, boolean local) {
public float value (TransformConstraintData data, BonePose source, boolean local) {
return (local ? source.scaleX : (float)Math.sqrt(source.a * source.a + source.c * source.c)) + data.offsetScaleX;
}
}
@ -284,7 +279,7 @@ public class TransformConstraintData extends ConstraintData {
return pose.mixScaleX;
}
public void apply (TransformConstraintPose pose, BoneApplied bone, float value, boolean local, boolean additive) {
public void apply (TransformConstraintPose pose, BonePose bone, float value, boolean local, boolean additive) {
if (local) {
if (additive)
bone.scaleX *= 1 + ((value - 1) * pose.mixScaleX);
@ -305,7 +300,7 @@ public class TransformConstraintData extends ConstraintData {
}
static public class FromScaleY extends FromProperty {
public float value (TransformConstraintData data, BoneApplied source, boolean local) {
public float value (TransformConstraintData data, BonePose source, boolean local) {
return (local ? source.scaleY : (float)Math.sqrt(source.b * source.b + source.d * source.d)) + data.offsetScaleY;
}
}
@ -315,7 +310,7 @@ public class TransformConstraintData extends ConstraintData {
return pose.mixScaleY;
}
public void apply (TransformConstraintPose pose, BoneApplied bone, float value, boolean local, boolean additive) {
public void apply (TransformConstraintPose pose, BonePose bone, float value, boolean local, boolean additive) {
if (local) {
if (additive)
bone.scaleY *= 1 + ((value - 1) * pose.mixScaleY);
@ -336,7 +331,7 @@ public class TransformConstraintData extends ConstraintData {
}
static public class FromShearY extends FromProperty {
public float value (TransformConstraintData data, BoneApplied source, boolean local) {
public float value (TransformConstraintData data, BonePose source, boolean local) {
return (local ? source.shearY : (atan2(source.d, source.b) - atan2(source.c, source.a)) * radDeg - 90)
+ data.offsetShearY;
}
@ -347,7 +342,7 @@ public class TransformConstraintData extends ConstraintData {
return pose.mixShearY;
}
public void apply (TransformConstraintPose pose, BoneApplied bone, float value, boolean local, boolean additive) {
public void apply (TransformConstraintPose pose, BonePose bone, float value, boolean local, boolean additive) {
if (local) {
if (!additive) value -= bone.shearY;
bone.shearY += value * pose.mixShearY;

View File

@ -30,7 +30,7 @@
package com.esotericsoftware.spine;
/** Stores a pose for a transform constraint. */
public class TransformConstraintPose {
public class TransformConstraintPose implements Pose<TransformConstraintPose> {
float mixRotate, mixX, mixY, mixScaleX, mixScaleY, mixShearY;
public void set (TransformConstraintPose pose) {

View File

@ -32,5 +32,5 @@ package com.esotericsoftware.spine;
/** The interface for items updated by {@link Skeleton#updateWorldTransform(Physics)}. */
public interface Update {
/** @param physics Determines how physics and other non-deterministic updates are applied. */
public void update (Physics physics);
public void update (Skeleton skeleton, Physics physics);
}

View File

@ -36,6 +36,7 @@ import com.badlogic.gdx.graphics.g2d.TextureAtlas.AtlasRegion;
import com.badlogic.gdx.graphics.g2d.TextureRegion;
import com.badlogic.gdx.utils.Null;
import com.esotericsoftware.spine.Skeleton;
import com.esotericsoftware.spine.Slot;
/** An attachment that displays a textured mesh. A mesh has hull vertices and internal vertices within the hull. Holes are not
@ -164,9 +165,10 @@ public class MeshAttachment extends VertexAttachment implements HasTextureRegion
}
/** If the attachment has a {@link #sequence}, the region may be changed. */
public void computeWorldVertices (Slot slot, int start, int count, float[] worldVertices, int offset, int stride) {
public void computeWorldVertices (Skeleton skeleton, Slot slot, int start, int count, float[] worldVertices, int offset,
int stride) {
if (sequence != null) sequence.apply(slot.getAppliedPose(), this);
super.computeWorldVertices(slot, start, count, worldVertices, offset, stride);
super.computeWorldVertices(skeleton, slot, start, count, worldVertices, offset, stride);
}
/** Triplets of vertex indices which describe the mesh's triangulation. */

View File

@ -34,7 +34,7 @@ import static com.esotericsoftware.spine.utils.SpineUtils.*;
import com.badlogic.gdx.graphics.Color;
import com.badlogic.gdx.math.Vector2;
import com.esotericsoftware.spine.BoneApplied;
import com.esotericsoftware.spine.BonePose;
/** An attachment which is a single point and a rotation. This can be used to spawn projectiles, particles, etc. A bone can be
* used in similar ways, but a PointAttachment is slightly less expensive to compute and can be hidden, shown, and placed in a
@ -90,13 +90,13 @@ public class PointAttachment extends Attachment {
return color;
}
public Vector2 computeWorldPosition (BoneApplied bone, Vector2 point) {
public Vector2 computeWorldPosition (BonePose bone, Vector2 point) {
point.x = x * bone.getA() + y * bone.getB() + bone.getWorldX();
point.y = x * bone.getC() + y * bone.getD() + bone.getWorldY();
return point;
}
public float computeWorldRotation (BoneApplied bone) {
public float computeWorldRotation (BonePose bone) {
float r = rotation * degRad, cos = cos(r), sin = sin(r);
float x = cos * bone.getA() + sin * bone.getB();
float y = cos * bone.getC() + sin * bone.getD();

View File

@ -36,7 +36,7 @@ import com.badlogic.gdx.graphics.g2d.TextureAtlas.AtlasRegion;
import com.badlogic.gdx.graphics.g2d.TextureRegion;
import com.badlogic.gdx.utils.Null;
import com.esotericsoftware.spine.BoneApplied;
import com.esotericsoftware.spine.BonePose;
import com.esotericsoftware.spine.Slot;
/** An attachment that displays a textured quadrilateral.
@ -179,7 +179,7 @@ public class RegionAttachment extends Attachment implements HasTextureRegion {
if (sequence != null) sequence.apply(slot.getAppliedPose(), this);
float[] vertexOffset = this.offset;
BoneApplied bone = slot.getBone().getAppliedPose();
BonePose bone = slot.getBone().getAppliedPose();
float x = bone.getWorldX(), y = bone.getWorldY();
float a = bone.getA(), b = bone.getB(), c = bone.getC(), d = bone.getD();
float offsetX, offsetY;

View File

@ -35,7 +35,7 @@ import com.badlogic.gdx.utils.FloatArray;
import com.badlogic.gdx.utils.Null;
import com.esotericsoftware.spine.Bone;
import com.esotericsoftware.spine.BoneApplied;
import com.esotericsoftware.spine.BonePose;
import com.esotericsoftware.spine.Skeleton;
import com.esotericsoftware.spine.Slot;
import com.esotericsoftware.spine.SlotPose;
@ -86,14 +86,16 @@ abstract public class VertexAttachment extends Attachment {
* <code>stride</code> / 2.
* @param offset The <code>worldVertices</code> index to begin writing values.
* @param stride The number of <code>worldVertices</code> entries between the value pairs written. */
public void computeWorldVertices (Slot slot, int start, int count, float[] worldVertices, int offset, int stride) {
public void computeWorldVertices (Skeleton skeleton, Slot slot, int start, int count, float[] worldVertices, int offset,
int stride) {
count = offset + (count >> 1) * stride;
FloatArray deformArray = slot.getAppliedPose().getDeform();
float[] vertices = this.vertices;
int[] bones = this.bones;
if (bones == null) {
if (deformArray.size > 0) vertices = deformArray.items;
BoneApplied bone = slot.getBone().getAppliedPose();
BonePose bone = slot.getBone().getAppliedPose();
float x = bone.getWorldX(), y = bone.getWorldY();
float a = bone.getA(), b = bone.getB(), c = bone.getC(), d = bone.getD();
for (int v = start, w = offset; w < count; v += 2, w += stride) {
@ -109,14 +111,14 @@ abstract public class VertexAttachment extends Attachment {
v += n + 1;
skip += n;
}
Object[] skeletonBones = slot.getSkeleton().getBones().items;
Object[] skeletonBones = skeleton.getBones().items;
if (deformArray.size == 0) {
for (int w = offset, b = skip * 3; w < count; w += stride) {
float wx = 0, wy = 0;
int n = bones[v++];
n += v;
for (; v < n; v++, b += 3) {
BoneApplied bone = ((Bone)skeletonBones[bones[v]]).getAppliedPose();
BonePose bone = ((Bone)skeletonBones[bones[v]]).getAppliedPose();
float vx = vertices[b], vy = vertices[b + 1], weight = vertices[b + 2];
wx += (vx * bone.getA() + vy * bone.getB() + bone.getWorldX()) * weight;
wy += (vx * bone.getC() + vy * bone.getD() + bone.getWorldY()) * weight;
@ -131,7 +133,7 @@ abstract public class VertexAttachment extends Attachment {
int n = bones[v++];
n += v;
for (; v < n; v++, b += 3, f += 2) {
BoneApplied bone = ((Bone)skeletonBones[bones[v]]).getAppliedPose();
BonePose bone = ((Bone)skeletonBones[bones[v]]).getAppliedPose();
float vx = vertices[b] + deform[f], vy = vertices[b + 1] + deform[f + 1], weight = vertices[b + 2];
wx += (vx * bone.getA() + vy * bone.getB() + bone.getWorldX()) * weight;
wy += (vx * bone.getC() + vy * bone.getD() + bone.getWorldY()) * weight;
@ -166,7 +168,7 @@ abstract public class VertexAttachment extends Attachment {
}
/** The maximum number of world vertex values that can be output by
* {@link #computeWorldVertices(Slot, int, int, float[], int, int)} using the <code>count</code> parameter. */
* {@link #computeWorldVertices(Skeleton, Slot, int, int, float[], int, int)} using the <code>count</code> parameter. */
public int getWorldVerticesLength () {
return worldVerticesLength;
}

View File

@ -33,6 +33,7 @@ import com.badlogic.gdx.utils.Array;
import com.badlogic.gdx.utils.FloatArray;
import com.badlogic.gdx.utils.ShortArray;
import com.esotericsoftware.spine.Skeleton;
import com.esotericsoftware.spine.Slot;
import com.esotericsoftware.spine.attachments.ClippingAttachment;
@ -48,14 +49,14 @@ public class SkeletonClipping {
private ClippingAttachment clipAttachment;
private Array<FloatArray> clippingPolygons;
public void clipStart (Slot slot, ClippingAttachment clip) {
public void clipStart (Skeleton skeleton, Slot slot, ClippingAttachment clip) {
if (clipAttachment != null) return;
int n = clip.getWorldVerticesLength();
if (n < 6) return;
clipAttachment = clip;
float[] vertices = clippingPolygon.setSize(n);
clip.computeWorldVertices(slot, 0, n, vertices, 0, 2);
clip.computeWorldVertices(skeleton, slot, 0, n, vertices, 0, 2);
makeClockwise(clippingPolygon);
ShortArray triangles = triangulator.triangulate(clippingPolygon);
clippingPolygons = triangulator.decompose(clippingPolygon, triangles);