[libgdx] Added DrawOrderFolderTimeline.

This commit is contained in:
Nathan Sweet 2026-03-13 20:20:59 -04:00
parent 516518b2d9
commit cf45806bdd
5 changed files with 250 additions and 73 deletions

View File

@ -1,26 +1,65 @@
package com.esotericsoftware.spine.utils;
import com.esotericsoftware.spine.*;
import com.esotericsoftware.spine.Animation.*;
import com.esotericsoftware.spine.AnimationState.*;
import com.esotericsoftware.spine.BoneData.Inherit;
import com.esotericsoftware.spine.Skin.SkinEntry;
import com.esotericsoftware.spine.PathConstraintData.*;
import com.esotericsoftware.spine.TransformConstraintData.*;
import com.esotericsoftware.spine.attachments.*;
import java.util.HashMap;
import java.util.Map;
import com.badlogic.gdx.graphics.Color;
import com.badlogic.gdx.graphics.g2d.TextureRegion;
import com.badlogic.gdx.utils.Array;
import com.badlogic.gdx.utils.IntArray;
import com.badlogic.gdx.utils.FloatArray;
import com.badlogic.gdx.utils.IntArray;
import java.util.Locale;
import java.util.Map;
import java.util.HashMap;
import com.esotericsoftware.spine.Animation;
import com.esotericsoftware.spine.Animation.Timeline;
import com.esotericsoftware.spine.AnimationState;
import com.esotericsoftware.spine.AnimationState.TrackEntry;
import com.esotericsoftware.spine.AnimationStateData;
import com.esotericsoftware.spine.Bone;
import com.esotericsoftware.spine.BoneData;
import com.esotericsoftware.spine.BoneLocal;
import com.esotericsoftware.spine.BonePose;
import com.esotericsoftware.spine.Constraint;
import com.esotericsoftware.spine.ConstraintData;
import com.esotericsoftware.spine.Event;
import com.esotericsoftware.spine.EventData;
import com.esotericsoftware.spine.IkConstraint;
import com.esotericsoftware.spine.IkConstraintData;
import com.esotericsoftware.spine.IkConstraintPose;
import com.esotericsoftware.spine.PathConstraint;
import com.esotericsoftware.spine.PathConstraintData;
import com.esotericsoftware.spine.PathConstraintPose;
import com.esotericsoftware.spine.PhysicsConstraint;
import com.esotericsoftware.spine.PhysicsConstraintData;
import com.esotericsoftware.spine.PhysicsConstraintPose;
import com.esotericsoftware.spine.Skeleton;
import com.esotericsoftware.spine.SkeletonData;
import com.esotericsoftware.spine.Skin;
import com.esotericsoftware.spine.Skin.SkinEntry;
import com.esotericsoftware.spine.Slider;
import com.esotericsoftware.spine.SliderData;
import com.esotericsoftware.spine.SliderPose;
import com.esotericsoftware.spine.Slot;
import com.esotericsoftware.spine.SlotData;
import com.esotericsoftware.spine.SlotPose;
import com.esotericsoftware.spine.TransformConstraint;
import com.esotericsoftware.spine.TransformConstraintData;
import com.esotericsoftware.spine.TransformConstraintData.FromProperty;
import com.esotericsoftware.spine.TransformConstraintData.ToProperty;
import com.esotericsoftware.spine.TransformConstraintPose;
import com.esotericsoftware.spine.Update;
import com.esotericsoftware.spine.attachments.Attachment;
import com.esotericsoftware.spine.attachments.BoundingBoxAttachment;
import com.esotericsoftware.spine.attachments.ClippingAttachment;
import com.esotericsoftware.spine.attachments.MeshAttachment;
import com.esotericsoftware.spine.attachments.PathAttachment;
import com.esotericsoftware.spine.attachments.PointAttachment;
import com.esotericsoftware.spine.attachments.RegionAttachment;
import com.esotericsoftware.spine.attachments.Sequence;
import com.esotericsoftware.spine.attachments.VertexAttachment;
public class SkeletonSerializer {
private final Map<Object, String> visitedObjects = new HashMap<>();
private final Map<Object, String> visitedObjects = new HashMap();
private int nextId = 1;
private JsonWriter json;
@ -3271,7 +3310,7 @@ public class SkeletonSerializer {
json.writeValue("Skin");
json.writeName("attachments");
Array<SkinEntry> sortedAttachments = new Array<>(obj.getAttachments());
var sortedAttachments = new Array<SkinEntry>(obj.getAttachments());
sortedAttachments.sort( (a, b) -> Integer.compare(a.getSlotIndex(), b.getSlotIndex()));
json.writeArrayStart();
for (SkinEntry item : sortedAttachments) {

View File

@ -1880,8 +1880,7 @@ public class Animation {
/** Sets the time and draw order for the specified frame.
* @param frame Between 0 and <code>frameCount</code>, inclusive.
* @param time The frame time in seconds.
* @param drawOrder For each slot in {@link Skeleton#slots}, the index of the slot in the new draw order. May be null to use
* setup pose draw order. */
* @param drawOrder Ordered {@link Skeleton#slots} indices, or null to use setup pose order. */
public void setFrame (int frame, float time, @Null int[] drawOrder) {
frames[frame] = time;
drawOrders[frame] = drawOrder;
@ -1913,6 +1912,87 @@ public class Animation {
}
}
/** Changes a subset of a skeleton's {@link Skeleton#getDrawOrder()}. */
static public class DrawOrderFolderTimeline extends Timeline {
private final int[] slots;
private final boolean[] inFolder;
private final int[][] drawOrders;
/** @param slots {@link Skeleton#getSlots()} indices controlled by this timeline, in setup order.
* @param slotCount The maximum number of slots in the skeleton. */
public DrawOrderFolderTimeline (int frameCount, int[] slots, int slotCount) {
super(frameCount, DrawOrderTimeline.propertyIds);
this.slots = slots;
drawOrders = new int[frameCount][];
inFolder = new boolean[slotCount];
for (int i : slots)
inFolder[i] = true;
}
public int getFrameCount () {
return frames.length;
}
/** The {@link Skeleton#getSlots()} indices that this timeline affects, in setup order. */
public int[] getSlots () {
return slots;
}
/** The draw order for each frame. See {@link #setFrame(int, float, int[])}. */
public int[][] getDrawOrders () {
return drawOrders;
}
/** Sets the time and draw order for the specified frame.
* @param frame Between 0 and <code>frameCount</code>, inclusive.
* @param time The frame time in seconds.
* @param drawOrder Ordered {@link #getSlots()} indices, or null to use setup pose order. */
public void setFrame (int frame, float time, @Null int[] drawOrder) {
frames[frame] = time;
drawOrders[frame] = drawOrder;
}
public void apply (Skeleton skeleton, float lastTime, float time, @Null Array<Event> events, float alpha, MixBlend blend,
MixDirection direction, boolean appliedPose) {
if (direction == out) {
if (blend == setup) setup(skeleton);
} else if (time < frames[0]) {
if (blend == setup || blend == first) setup(skeleton);
} else {
int[] order = drawOrders[search(frames, time)];
if (order == null)
setup(skeleton);
else
apply(skeleton, order);
}
}
private void setup (Skeleton skeleton) {
boolean[] inFolder = this.inFolder;
Slot[] drawOrder = skeleton.drawOrder.items, allSlots = skeleton.slots.items;
int[] slots = this.slots;
for (int i = 0, found = 0, done = slots.length;; i++) {
if (inFolder[drawOrder[i].data.index]) {
drawOrder[i] = allSlots[slots[found]];
if (++found == done) break;
}
}
}
private void apply (Skeleton skeleton, int[] order) {
boolean[] inFolder = this.inFolder;
Slot[] drawOrder = skeleton.drawOrder.items, allSlots = skeleton.slots.items;
int[] slots = this.slots;
for (int i = 0, found = 0, done = slots.length;; i++) {
if (inFolder[drawOrder[i].data.index]) {
drawOrder[i] = allSlots[slots[order[found]]];
if (++found == done) break;
}
}
}
}
static public interface ConstraintTimeline {
/** The index of the constraint in {@link Skeleton#getConstraints()} that will be changed when this timeline is applied, or
* -1 if a specific constraint will not be changed. */

View File

@ -39,6 +39,7 @@ import com.badlogic.gdx.utils.Pool.Poolable;
import com.badlogic.gdx.utils.SnapshotArray;
import com.esotericsoftware.spine.Animation.AttachmentTimeline;
import com.esotericsoftware.spine.Animation.DrawOrderFolderTimeline;
import com.esotericsoftware.spine.Animation.DrawOrderTimeline;
import com.esotericsoftware.spine.Animation.EventTimeline;
import com.esotericsoftware.spine.Animation.MixBlend;
@ -807,7 +808,8 @@ public class AnimationState {
if (!propertyIds.addAll(ids))
timelineMode[i] = SUBSEQUENT;
else if (to == null || timeline instanceof AttachmentTimeline || timeline instanceof DrawOrderTimeline
|| timeline instanceof EventTimeline || !to.animation.hasTimeline(ids)) {
|| timeline instanceof DrawOrderFolderTimeline || timeline instanceof EventTimeline
|| !to.animation.hasTimeline(ids)) {
timelineMode[i] = FIRST;
} else {
for (TrackEntry next = to.mixingTo; next != null; next = next.mixingTo) {

View File

@ -32,6 +32,7 @@ package com.esotericsoftware.spine;
import java.io.EOFException;
import java.io.IOException;
import java.io.InputStream;
import java.util.Arrays;
import com.badlogic.gdx.files.FileHandle;
import com.badlogic.gdx.graphics.Color;
@ -47,6 +48,7 @@ import com.esotericsoftware.spine.Animation.BoneTimeline2;
import com.esotericsoftware.spine.Animation.CurveTimeline;
import com.esotericsoftware.spine.Animation.CurveTimeline1;
import com.esotericsoftware.spine.Animation.DeformTimeline;
import com.esotericsoftware.spine.Animation.DrawOrderFolderTimeline;
import com.esotericsoftware.spine.Animation.DrawOrderTimeline;
import com.esotericsoftware.spine.Animation.EventTimeline;
import com.esotericsoftware.spine.Animation.IkConstraintTimeline;
@ -1160,34 +1162,26 @@ public class SkeletonBinary extends SkeletonLoader {
}
// Draw order timeline.
int slotCount = skeletonData.slots.size;
int drawOrderCount = input.readInt(true);
if (drawOrderCount > 0) {
var timeline = new DrawOrderTimeline(drawOrderCount);
int slotCount = skeletonData.slots.size;
for (int i = 0; i < drawOrderCount; i++) {
float time = input.readFloat();
int offsetCount = input.readInt(true);
var drawOrder = new int[slotCount];
for (int ii = slotCount - 1; ii >= 0; ii--)
drawOrder[ii] = -1;
var unchanged = new int[slotCount - offsetCount];
int originalIndex = 0, unchangedIndex = 0;
for (int ii = 0; ii < offsetCount; ii++) {
int slotIndex = input.readInt(true);
// Collect unchanged items.
while (originalIndex != slotIndex)
unchanged[unchangedIndex++] = originalIndex++;
// Set changed items.
drawOrder[originalIndex + input.readInt(true)] = originalIndex++;
}
// Collect remaining unchanged items.
while (originalIndex < slotCount)
unchanged[unchangedIndex++] = originalIndex++;
// Fill in unchanged items.
for (int ii = slotCount - 1; ii >= 0; ii--)
if (drawOrder[ii] == -1) drawOrder[ii] = unchanged[--unchangedIndex];
timeline.setFrame(i, time, drawOrder);
}
for (int i = 0; i < drawOrderCount; i++)
timeline.setFrame(i, input.readFloat(), readDrawOrder(input, slotCount));
timelines.add(timeline);
}
// Draw order folder timelines.
int folderCount = input.readInt(true);
for (int i = 0; i < folderCount; i++) {
int folderSlotCount = input.readInt(true);
var folderSlots = new int[folderSlotCount];
for (int ii = 0; ii < folderSlotCount; ii++)
folderSlots[ii] = input.readInt(true);
int keyCount = input.readInt(true);
var timeline = new DrawOrderFolderTimeline(keyCount, folderSlots, slotCount);
for (int ii = 0; ii < keyCount; ii++)
timeline.setFrame(ii, input.readFloat(), readDrawOrder(input, folderSlotCount));
timelines.add(timeline);
}
@ -1257,6 +1251,30 @@ public class SkeletonBinary extends SkeletonLoader {
timelines.add(timeline);
}
private @Null int[] readDrawOrder (SkeletonInput input, int slotCount) throws IOException {
int changeCount = input.readInt(true);
if (changeCount == 0) return null;
var drawOrder = new int[slotCount];
Arrays.fill(drawOrder, -1);
var unchanged = new int[slotCount - changeCount];
int originalIndex = 0, unchangedIndex = 0;
for (int i = 0; i < changeCount; i++) {
int slotIndex = input.readInt(true);
// Collect unchanged items.
while (originalIndex != slotIndex)
unchanged[unchangedIndex++] = originalIndex++;
// Set changed items.
drawOrder[originalIndex + input.readInt(true)] = originalIndex++;
}
// Collect remaining unchanged items.
while (originalIndex < slotCount)
unchanged[unchangedIndex++] = originalIndex++;
// Fill in unchanged items.
for (int i = slotCount - 1; i >= 0; i--)
if (drawOrder[i] == -1) drawOrder[i] = unchanged[--unchangedIndex];
return drawOrder;
}
void setBezier (SkeletonInput input, CurveTimeline timeline, int bezier, int frame, int value, float time1, float time2,
float value1, float value2, float scale) throws IOException {
timeline.setBezier(bezier, frame, value, time1, value1, input.readFloat(), input.readFloat() * scale, input.readFloat(),

View File

@ -32,6 +32,7 @@ package com.esotericsoftware.spine;
import static com.esotericsoftware.spine.utils.SpineUtils.*;
import java.io.InputStream;
import java.util.Arrays;
import com.badlogic.gdx.files.FileHandle;
import com.badlogic.gdx.graphics.Color;
@ -50,6 +51,7 @@ import com.esotericsoftware.spine.Animation.BoneTimeline2;
import com.esotericsoftware.spine.Animation.CurveTimeline;
import com.esotericsoftware.spine.Animation.CurveTimeline1;
import com.esotericsoftware.spine.Animation.DeformTimeline;
import com.esotericsoftware.spine.Animation.DrawOrderFolderTimeline;
import com.esotericsoftware.spine.Animation.DrawOrderTimeline;
import com.esotericsoftware.spine.Animation.EventTimeline;
import com.esotericsoftware.spine.Animation.IkConstraintTimeline;
@ -464,7 +466,7 @@ public class SkeletonJson extends SkeletonLoader {
skin.constraints.shrink();
for (JsonValue slotEntry = skinMap.getChild("attachments"); slotEntry != null; slotEntry = slotEntry.next) {
SlotData slot = skeletonData.findSlot(slotEntry.name);
if (slot == null) throw new SerializationException("Slot not found: " + slotEntry.name);
if (slot == null) throw new SerializationException("Skin slot not found: " + slotEntry.name);
for (JsonValue entry = slotEntry.child; entry != null; entry = entry.next) {
try {
Attachment attachment = readAttachment(entry, skin, slot.index, entry.name, skeletonData);
@ -1136,7 +1138,7 @@ public class SkeletonJson extends SkeletonLoader {
if (skin == null) throw new SerializationException("Skin not found: " + attachmentsMap.name);
for (JsonValue slotMap = attachmentsMap.child; slotMap != null; slotMap = slotMap.next) {
SlotData slot = skeletonData.findSlot(slotMap.name);
if (slot == null) throw new SerializationException("Slot not found: " + slotMap.name);
if (slot == null) throw new SerializationException("Attachment slot not found: " + slotMap.name);
for (JsonValue attachmentMap = slotMap.child; attachmentMap != null; attachmentMap = attachmentMap.next) {
Attachment attachment = skin.getAttachment(slot.index, attachmentMap.name);
if (attachment == null) throw new SerializationException("Timeline attachment not found: " + attachmentMap.name);
@ -1206,38 +1208,35 @@ public class SkeletonJson extends SkeletonLoader {
JsonValue drawOrderMap = map.get("drawOrder");
if (drawOrderMap != null) {
var timeline = new DrawOrderTimeline(drawOrderMap.size);
int slotCount = skeletonData.slots.size;
int frame = 0;
for (JsonValue keyMap = drawOrderMap.child; keyMap != null; keyMap = keyMap.next, frame++) {
int[] drawOrder = null;
JsonValue offsets = keyMap.get("offsets");
if (offsets != null) {
drawOrder = new int[slotCount];
for (int i = slotCount - 1; i >= 0; i--)
drawOrder[i] = -1;
var unchanged = new int[slotCount - offsets.size];
int originalIndex = 0, unchangedIndex = 0;
for (JsonValue offsetMap = offsets.child; offsetMap != null; offsetMap = offsetMap.next) {
SlotData slot = skeletonData.findSlot(offsetMap.getString("slot"));
if (slot == null) throw new SerializationException("Slot not found: " + offsetMap.getString("slot"));
// Collect unchanged items.
while (originalIndex != slot.index)
unchanged[unchangedIndex++] = originalIndex++;
// Set changed items.
drawOrder[originalIndex + offsetMap.getInt("offset")] = originalIndex++;
}
// Collect remaining unchanged items.
while (originalIndex < slotCount)
unchanged[unchangedIndex++] = originalIndex++;
// Fill in unchanged items.
for (int i = slotCount - 1; i >= 0; i--)
if (drawOrder[i] == -1) drawOrder[i] = unchanged[--unchangedIndex];
}
timeline.setFrame(frame, keyMap.getFloat("time", 0), drawOrder);
}
int slotCount = skeletonData.slots.size, frame = 0;
for (JsonValue keyMap = drawOrderMap.child; keyMap != null; keyMap = keyMap.next, frame++)
timeline.setFrame(frame, keyMap.getFloat("time", 0), readDrawOrder(skeletonData, keyMap, slotCount, null));
timelines.add(timeline);
}
// Draw order folder timelines.
JsonValue drawOrderFoldersMap = map.get("drawOrderFolder");
if (drawOrderFoldersMap != null) {
for (JsonValue timelineMap = drawOrderFoldersMap.child; timelineMap != null; timelineMap = timelineMap.next) {
JsonValue slotEntry = timelineMap.get("slots");
var folderSlots = new int[slotEntry.size];
int ii = 0;
for (slotEntry = slotEntry.child; slotEntry != null; slotEntry = slotEntry.next, ii++) {
SlotData slot = skeletonData.findSlot(slotEntry.asString());
if (slot == null) throw new SerializationException("Draw order folder slot not found: " + slotEntry.asString());
folderSlots[ii] = slot.index;
}
JsonValue keyMap = timelineMap.get("keys");
var timeline = new DrawOrderFolderTimeline(keyMap.size, folderSlots, skeletonData.slots.size);
int frame = 0;
for (keyMap = keyMap.child; keyMap != null; keyMap = keyMap.next, frame++)
timeline.setFrame(frame, keyMap.getFloat("time", 0),
readDrawOrder(skeletonData, keyMap, folderSlots.length, folderSlots));
timelines.add(timeline);
}
}
// Event timeline.
JsonValue eventsMap = map.get("events");
if (eventsMap != null) {
@ -1314,6 +1313,45 @@ public class SkeletonJson extends SkeletonLoader {
}
}
/** @param folderSlots Slot names are resolved to positions within this array. If null, slot indices are used as positions. */
private @Null int[] readDrawOrder (SkeletonData skeletonData, JsonValue keyMap, int slotCount, @Null int[] folderSlots) {
JsonValue changes = keyMap.get("offsets");
if (changes == null) return null; // Setup draw order.
var drawOrder = new int[slotCount];
Arrays.fill(drawOrder, -1);
var unchanged = new int[slotCount - changes.size];
int originalIndex = 0, unchangedIndex = 0;
for (JsonValue offsetMap = changes.child; offsetMap != null; offsetMap = offsetMap.next) {
SlotData slot = skeletonData.findSlot(offsetMap.getString("slot"));
if (slot == null) throw new SerializationException("Draw order slot not found: " + offsetMap.getString("slot"));
int index;
if (folderSlots == null)
index = slot.index;
else {
index = -1;
for (int i = 0; i < slotCount; i++) {
if (folderSlots[i] == slot.index) {
index = i;
break;
}
}
if (index == -1) throw new SerializationException("Slot not in folder: " + offsetMap.getString("slot"));
}
// Collect unchanged items.
while (originalIndex != index)
unchanged[unchangedIndex++] = originalIndex++;
// Set changed items.
drawOrder[originalIndex + offsetMap.getInt("offset")] = originalIndex++;
}
// Collect remaining unchanged items.
while (originalIndex < slotCount)
unchanged[unchangedIndex++] = originalIndex++;
// Fill in unchanged items.
for (int i = slotCount - 1; i >= 0; i--)
if (drawOrder[i] == -1) drawOrder[i] = unchanged[--unchangedIndex];
return drawOrder;
}
int readCurve (JsonValue curve, CurveTimeline timeline, int bezier, int frame, int value, float time1, float time2,
float value1, float value2, float scale) {
if (curve.isString()) {