diff --git a/spine-libgdx/spine-libgdx-tests/src/com/esotericsoftware/spine/utils/SkeletonSerializer.java b/spine-libgdx/spine-libgdx-tests/src/com/esotericsoftware/spine/utils/SkeletonSerializer.java index 5d944e778..bfa76abcc 100644 --- a/spine-libgdx/spine-libgdx-tests/src/com/esotericsoftware/spine/utils/SkeletonSerializer.java +++ b/spine-libgdx/spine-libgdx-tests/src/com/esotericsoftware/spine/utils/SkeletonSerializer.java @@ -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 visitedObjects = new HashMap<>(); + private final Map visitedObjects = new HashMap(); private int nextId = 1; private JsonWriter json; @@ -3271,7 +3310,7 @@ public class SkeletonSerializer { json.writeValue("Skin"); json.writeName("attachments"); - Array sortedAttachments = new Array<>(obj.getAttachments()); + var sortedAttachments = new Array(obj.getAttachments()); sortedAttachments.sort( (a, b) -> Integer.compare(a.getSlotIndex(), b.getSlotIndex())); json.writeArrayStart(); for (SkinEntry item : sortedAttachments) { diff --git a/spine-libgdx/spine-libgdx/src/com/esotericsoftware/spine/Animation.java b/spine-libgdx/spine-libgdx/src/com/esotericsoftware/spine/Animation.java index ab7a9b198..8f3794d8b 100644 --- a/spine-libgdx/spine-libgdx/src/com/esotericsoftware/spine/Animation.java +++ b/spine-libgdx/spine-libgdx/src/com/esotericsoftware/spine/Animation.java @@ -1880,8 +1880,7 @@ public class Animation { /** Sets the time and draw order for the specified frame. * @param frame Between 0 and frameCount, 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 frameCount, 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 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. */ diff --git a/spine-libgdx/spine-libgdx/src/com/esotericsoftware/spine/AnimationState.java b/spine-libgdx/spine-libgdx/src/com/esotericsoftware/spine/AnimationState.java index 63a8728f1..5b5cfb415 100644 --- a/spine-libgdx/spine-libgdx/src/com/esotericsoftware/spine/AnimationState.java +++ b/spine-libgdx/spine-libgdx/src/com/esotericsoftware/spine/AnimationState.java @@ -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) { diff --git a/spine-libgdx/spine-libgdx/src/com/esotericsoftware/spine/SkeletonBinary.java b/spine-libgdx/spine-libgdx/src/com/esotericsoftware/spine/SkeletonBinary.java index a8279d4bf..03785e87a 100644 --- a/spine-libgdx/spine-libgdx/src/com/esotericsoftware/spine/SkeletonBinary.java +++ b/spine-libgdx/spine-libgdx/src/com/esotericsoftware/spine/SkeletonBinary.java @@ -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(), diff --git a/spine-libgdx/spine-libgdx/src/com/esotericsoftware/spine/SkeletonJson.java b/spine-libgdx/spine-libgdx/src/com/esotericsoftware/spine/SkeletonJson.java index 2981712c2..815c7dd3c 100644 --- a/spine-libgdx/spine-libgdx/src/com/esotericsoftware/spine/SkeletonJson.java +++ b/spine-libgdx/spine-libgdx/src/com/esotericsoftware/spine/SkeletonJson.java @@ -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()) {