From 7f24ed23af02d8577e738957e17bf4027b1ce211 Mon Sep 17 00:00:00 2001 From: Davide Tantillo Date: Mon, 16 Mar 2026 11:05:36 +0100 Subject: [PATCH] [ts] Port of cf45806b: Added DrawOrderFolderTimeline. --- spine-ts/spine-core/src/Animation.ts | 83 +++++++++++++++++- spine-ts/spine-core/src/AnimationState.ts | 5 +- spine-ts/spine-core/src/SkeletonBinary.ts | 67 ++++++++------ spine-ts/spine-core/src/SkeletonJson.ts | 101 +++++++++++++++------- 4 files changed, 194 insertions(+), 62 deletions(-) diff --git a/spine-ts/spine-core/src/Animation.ts b/spine-ts/spine-core/src/Animation.ts index 1f7d8c85d..4b60cf9e3 100644 --- a/spine-ts/spine-core/src/Animation.ts +++ b/spine-ts/spine-core/src/Animation.ts @@ -1792,7 +1792,7 @@ export class DrawOrderTimeline extends Timeline { static propertyIds = [`${Property.drawOrder}`]; /** The draw order for each key frame. See {@link #setFrame(int, float, int[])}. */ - drawOrders: Array | null>; + private readonly drawOrders: Array | null>; constructor (frameCount: number) { super(frameCount, ...DrawOrderTimeline.propertyIds); @@ -1804,7 +1804,7 @@ export class DrawOrderTimeline extends Timeline { } /** Sets the time in seconds and the draw order for the specified key frame. - * @param drawOrder For each slot in {@link Skeleton#slots}, the index of the new draw order. May be null to use setup pose + * @param drawOrder Ordered {@link Skeleton#slots} indices, or null to use setup pose * draw order. */ setFrame (frame: number, time: number, drawOrder: Array | null) { this.frames[frame] = time; @@ -1837,6 +1837,85 @@ export class DrawOrderTimeline extends Timeline { } } +/** Changes a subset of a skeleton's {@link Skeleton#getDrawOrder()}. */ +export class DrawOrderFolderTimeline extends Timeline { + private readonly slots: number[]; + private readonly inFolder: boolean[]; + private readonly drawOrders: Array | null>; + + /** @param slots {@link Skeleton#slots} indices controlled by this timeline, in setup order. + * @param slotCount The maximum number of slots in the skeleton. */ + constructor (frameCount: number, slots: number[], slotCount: number) { + super(frameCount, ...DrawOrderTimeline.propertyIds); + this.slots = slots; + this.drawOrders = new Array(frameCount); + this.inFolder = new Array(slotCount); + for (const i of slots) + this.inFolder[i] = true; + } + + getFrameCount (): number { + return this.frames.length; + } + + /** The {@link Skeleton#getSlots()} indices that this timeline affects, in setup order. */ + getSlots (): number[] { + return this.slots; + } + + /** The draw order for each frame. See {@link #setFrame(int, float, int[])}. */ + getDrawOrders (): Array | null> { + return this.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. */ + setFrame (frame: number, time: number, drawOrder: Array | null): void { + this.frames[frame] = time; + this.drawOrders[frame] = drawOrder; + } + + apply (skeleton: Skeleton, lastTime: number, time: number, events: Array, alpha: number, blend: MixBlend, + direction: MixDirection, appliedPose: boolean): void { + + if (direction === MixDirection.out) { + if (blend === MixBlend.setup) this.setup(skeleton); + } else if (time < this.frames[0]) { + if (blend === MixBlend.setup || blend === MixBlend.first) this.setup(skeleton); + } else { + const order = this.drawOrders[Timeline.search(this.frames, time)]; + if (!order) + this.setup(skeleton); + else + this.apply1(skeleton, order); + } + } + + private setup (skeleton: Skeleton): void { + const { inFolder, slots } = this; + const { drawOrder, slots: allSlots } = skeleton; + for (let i = 0, found = 0, done = slots.length; ; i++) { + if (inFolder[drawOrder[i].data.index]) { + drawOrder[i] = allSlots[slots[found]]; + if (++found === done) break; + } + } + } + + private apply1 (skeleton: Skeleton, order: number[]): void { + const { inFolder, slots } = this; + const { drawOrder, slots: allSlots } = skeleton; + for (let i = 0, found = 0, done = slots.length; ; i++) { + if (inFolder[drawOrder[i].data.index]) { + drawOrder[i] = allSlots[slots[order[found]]]; + if (++found === done) break; + } + } + } +} + export interface ConstraintTimeline { /** The index of the constraint in {@link Skeleton.constraints} that will be changed when this timeline is applied, or * -1 if a specific constraint will not be changed. */ diff --git a/spine-ts/spine-core/src/AnimationState.ts b/spine-ts/spine-core/src/AnimationState.ts index 3aa80d85b..b19eea2f1 100644 --- a/spine-ts/spine-core/src/AnimationState.ts +++ b/spine-ts/spine-core/src/AnimationState.ts @@ -29,7 +29,7 @@ /** biome-ignore-all lint/style/noNonNullAssertion: reference runtime expects some nullable to not be null */ -import { Animation, AttachmentTimeline, DrawOrderTimeline, EventTimeline, MixBlend, MixDirection, RotateTimeline, Timeline } from "./Animation.js"; +import { Animation, AttachmentTimeline, DrawOrderFolderTimeline, DrawOrderTimeline, EventTimeline, MixBlend, MixDirection, RotateTimeline, Timeline } from "./Animation.js"; import type { AnimationStateData } from "./AnimationStateData.js"; import type { Event } from "./Event.js"; import type { Skeleton } from "./Skeleton.js"; @@ -788,7 +788,8 @@ export class AnimationState { if (!propertyIDs.addAll(ids)) timelineMode[i] = SUBSEQUENT; else if (!to || 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 (let next = to.mixingTo; next; next = next!.mixingTo) { diff --git a/spine-ts/spine-core/src/SkeletonBinary.ts b/spine-ts/spine-core/src/SkeletonBinary.ts index 427832ac3..f621dd536 100644 --- a/spine-ts/spine-core/src/SkeletonBinary.ts +++ b/spine-ts/spine-core/src/SkeletonBinary.ts @@ -27,7 +27,7 @@ * THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. *****************************************************************************/ -import { AlphaTimeline, Animation, AttachmentTimeline, type BoneTimeline2, type CurveTimeline, CurveTimeline1, DeformTimeline, DrawOrderTimeline, EventTimeline, IkConstraintTimeline, InheritTimeline, PathConstraintMixTimeline, PathConstraintPositionTimeline, PathConstraintSpacingTimeline, PhysicsConstraintDampingTimeline, PhysicsConstraintGravityTimeline, PhysicsConstraintInertiaTimeline, PhysicsConstraintMassTimeline, PhysicsConstraintMixTimeline, PhysicsConstraintResetTimeline, PhysicsConstraintStrengthTimeline, PhysicsConstraintWindTimeline, RGB2Timeline, RGBA2Timeline, RGBATimeline, RGBTimeline, RotateTimeline, ScaleTimeline, ScaleXTimeline, ScaleYTimeline, SequenceTimeline, ShearTimeline, ShearXTimeline, ShearYTimeline, SliderMixTimeline, SliderTimeline, type Timeline, TransformConstraintTimeline, TranslateTimeline, TranslateXTimeline, TranslateYTimeline } from "./Animation.js"; +import { AlphaTimeline, Animation, AttachmentTimeline, type BoneTimeline2, type CurveTimeline, CurveTimeline1, DeformTimeline, DrawOrderFolderTimeline, DrawOrderTimeline, EventTimeline, IkConstraintTimeline, InheritTimeline, PathConstraintMixTimeline, PathConstraintPositionTimeline, PathConstraintSpacingTimeline, PhysicsConstraintDampingTimeline, PhysicsConstraintGravityTimeline, PhysicsConstraintInertiaTimeline, PhysicsConstraintMassTimeline, PhysicsConstraintMixTimeline, PhysicsConstraintResetTimeline, PhysicsConstraintStrengthTimeline, PhysicsConstraintWindTimeline, RGB2Timeline, RGBA2Timeline, RGBATimeline, RGBTimeline, RotateTimeline, ScaleTimeline, ScaleXTimeline, ScaleYTimeline, SequenceTimeline, ShearTimeline, ShearXTimeline, ShearYTimeline, SliderMixTimeline, SliderTimeline, type Timeline, TransformConstraintTimeline, TranslateTimeline, TranslateXTimeline, TranslateYTimeline } from "./Animation.js"; import type { Attachment, VertexAttachment } from "./attachments/Attachment.js"; import type { AttachmentLoader } from "./attachments/AttachmentLoader.js"; import type { HasSequence } from "./attachments/HasSequence.js"; @@ -1130,34 +1130,26 @@ export class SkeletonBinary { } // Draw order timeline. + const slotCount = skeletonData.slots.length; const drawOrderCount = input.readInt(true); if (drawOrderCount > 0) { const timeline = new DrawOrderTimeline(drawOrderCount); - const slotCount = skeletonData.slots.length; - for (let i = 0; i < drawOrderCount; i++) { - const time = input.readFloat(); - const offsetCount = input.readInt(true); - const drawOrder = Utils.newArray(slotCount, 0); - for (let ii = slotCount - 1; ii >= 0; ii--) - drawOrder[ii] = -1; - const unchanged = Utils.newArray(slotCount - offsetCount, 0); - let originalIndex = 0, unchangedIndex = 0; - for (let ii = 0; ii < offsetCount; ii++) { - const 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 (let ii = slotCount - 1; ii >= 0; ii--) - if (drawOrder[ii] === -1) drawOrder[ii] = unchanged[--unchangedIndex]; - timeline.setFrame(i, time, drawOrder); - } + for (let i = 0; i < drawOrderCount; i++) + timeline.setFrame(i, input.readFloat(), readDrawOrder(input, slotCount)); + timelines.push(timeline); + } + + // Draw order folder timelines. + const folderCount = input.readInt(true); + for (let i = 0; i < folderCount; i++) { + const folderSlotCount = input.readInt(true); + const folderSlots = new Array(folderSlotCount); + for (let ii = 0; ii < folderSlotCount; ii++) + folderSlots[ii] = input.readInt(true); + const keyCount = input.readInt(true); + const timeline = new DrawOrderFolderTimeline(keyCount, folderSlots, slotCount); + for (let ii = 0; ii < keyCount; ii++) + timeline.setFrame(ii, input.readFloat(), readDrawOrder(input, folderSlotCount)); timelines.push(timeline); } @@ -1351,6 +1343,29 @@ function readTimeline2 (input: BinaryInput, timelines: Array, timeline timelines.push(timeline); } +function readDrawOrder (input: BinaryInput, slotCount: number): number[] | null { + const changeCount = input.readInt(true); + if (changeCount === 0) return null; + const drawOrder = new Array(slotCount).fill(-1); + const unchanged = new Array(slotCount - changeCount); + let originalIndex = 0, unchangedIndex = 0; + for (let i = 0; i < changeCount; i++) { + const 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 (let i = slotCount - 1; i >= 0; i--) + if (drawOrder[i] === -1) drawOrder[i] = unchanged[--unchangedIndex]; + return drawOrder; +} + function setBezier (input: BinaryInput, timeline: CurveTimeline, bezier: number, frame: number, value: number, time1: number, time2: number, value1: number, value2: number, scale: number) { timeline.setBezier(bezier, frame, value, time1, value1, input.readFloat(), input.readFloat() * scale, input.readFloat(), input.readFloat() * scale, time2, value2); diff --git a/spine-ts/spine-core/src/SkeletonJson.ts b/spine-ts/spine-core/src/SkeletonJson.ts index 7d9e9a6bf..c293bc6e8 100644 --- a/spine-ts/spine-core/src/SkeletonJson.ts +++ b/spine-ts/spine-core/src/SkeletonJson.ts @@ -27,7 +27,7 @@ * THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. *****************************************************************************/ -import { AlphaTimeline, Animation, AttachmentTimeline, type BoneTimeline2, type CurveTimeline, type CurveTimeline1, DeformTimeline, DrawOrderTimeline, EventTimeline, IkConstraintTimeline, InheritTimeline, PathConstraintMixTimeline, PathConstraintPositionTimeline, PathConstraintSpacingTimeline, PhysicsConstraintDampingTimeline, PhysicsConstraintGravityTimeline, PhysicsConstraintInertiaTimeline, PhysicsConstraintMassTimeline, PhysicsConstraintMixTimeline, PhysicsConstraintResetTimeline, PhysicsConstraintStrengthTimeline, PhysicsConstraintWindTimeline, RGB2Timeline, RGBA2Timeline, RGBATimeline, RGBTimeline, RotateTimeline, ScaleTimeline, ScaleXTimeline, ScaleYTimeline, SequenceTimeline, ShearTimeline, ShearXTimeline, ShearYTimeline, SliderMixTimeline, SliderTimeline, type Timeline, TransformConstraintTimeline, TranslateTimeline, TranslateXTimeline, TranslateYTimeline } from "./Animation.js"; +import { AlphaTimeline, Animation, AttachmentTimeline, type BoneTimeline2, type CurveTimeline, type CurveTimeline1, DeformTimeline, DrawOrderFolderTimeline, DrawOrderTimeline, EventTimeline, IkConstraintTimeline, InheritTimeline, PathConstraintMixTimeline, PathConstraintPositionTimeline, PathConstraintSpacingTimeline, PhysicsConstraintDampingTimeline, PhysicsConstraintGravityTimeline, PhysicsConstraintInertiaTimeline, PhysicsConstraintMassTimeline, PhysicsConstraintMixTimeline, PhysicsConstraintResetTimeline, PhysicsConstraintStrengthTimeline, PhysicsConstraintWindTimeline, RGB2Timeline, RGBA2Timeline, RGBATimeline, RGBTimeline, RotateTimeline, ScaleTimeline, ScaleXTimeline, ScaleYTimeline, SequenceTimeline, ShearTimeline, ShearXTimeline, ShearYTimeline, SliderMixTimeline, SliderTimeline, type Timeline, TransformConstraintTimeline, TranslateTimeline, TranslateXTimeline, TranslateYTimeline } from "./Animation.js"; import type { Attachment, VertexAttachment } from "./attachments/Attachment.js"; import type { AttachmentLoader } from "./attachments/AttachmentLoader.js"; import type { HasSequence } from "./attachments/HasSequence.js"; @@ -419,7 +419,7 @@ export class SkeletonJson { for (const slotName in skinMap.attachments) { const slot = skeletonData.findSlot(slotName); - if (!slot) throw new Error(`Couldn't find slot ${slotName} for skin ${skinMap.name}.`); + if (!slot) throw new Error(`Couldn't find skin slot ${slotName} for skin ${skinMap.name}.`); const slotMap = skinMap.attachments[slotName]; for (const entryName in slotMap) { const attachment = this.readAttachment(slotMap[entryName], skin, slot.index, entryName, skeletonData); @@ -1101,7 +1101,7 @@ export class SkeletonJson { for (const slotMapName in attachmentsMap) { const slotMap = attachmentsMap[slotMapName]; const slot = skeletonData.findSlot(slotMapName); - if (!slot) throw new Error(`Slot not found: ${slotMapName}`); + if (!slot) throw new Error(`Attachment slot not found: ${slotMapName}`); const slotIndex = slot.index; for (const attachmentMapName in slotMap) { const attachmentMap = slotMap[attachmentMapName]; @@ -1172,42 +1172,39 @@ export class SkeletonJson { } } - // Draw order timelines. + // Draw order timeline. if (map.drawOrder) { const timeline = new DrawOrderTimeline(map.drawOrder.length); const slotCount = skeletonData.slots.length; - let frame = 0; - for (let i = 0; i < map.drawOrder.length; i++, frame++) { - const drawOrderMap = map.drawOrder[i]; - let drawOrder: Array | null = null; - const offsets = getValue(drawOrderMap, "offsets", null); - if (offsets) { - drawOrder = Utils.newArray(slotCount, -1); - const unchanged = Utils.newArray(slotCount - offsets.length, 0); - let originalIndex = 0, unchangedIndex = 0; - for (let ii = 0; ii < offsets.length; ii++) { - const offsetMap = offsets[ii]; - const slot = skeletonData.findSlot(offsetMap.slot); - if (!slot) throw new Error(`Slot not found: ${slot}`); - const slotIndex = slot.index; - // Collect unchanged items. - while (originalIndex !== slotIndex) - unchanged[unchangedIndex++] = originalIndex++; - // Set changed items. - drawOrder[originalIndex + offsetMap.offset] = originalIndex++; - } - // Collect remaining unchanged items. - while (originalIndex < slotCount) - unchanged[unchangedIndex++] = originalIndex++; - // Fill in unchanged items. - for (let ii = slotCount - 1; ii >= 0; ii--) - if (drawOrder[ii] === -1) drawOrder[ii] = unchanged[--unchangedIndex]; - } - timeline.setFrame(frame, getValue(drawOrderMap, "time", 0), drawOrder); + let frame = 0 + for (const drawOrderMap of (map.drawOrder as DrawOrderKeysType[])) { + timeline.setFrame(frame++, getValue(drawOrderMap, "time", 0), readDrawOrder(skeletonData, drawOrderMap, slotCount, null)); } timelines.push(timeline); } + // Draw order folder timelines. + if (map.drawOrderFolder) { + for (const timelineMap of map.drawOrderFolder) { + const slotEntries = getValue(timelineMap, "slots", []) as string[]; + const folderSlots = new Array(slotEntries.length); + let ii = 0; + for (const slotEntry of slotEntries) { + const slot = skeletonData.findSlot(slotEntry); + if (!slot) throw new Error(`Draw order folder slot not found: ${slotEntry}`); + folderSlots[ii++] = slot.index; + } + + const drawOrderFolderEntries = getValue(timelineMap, "keys", []) as DrawOrderKeysType[]; + const timeline = new DrawOrderFolderTimeline(drawOrderFolderEntries.length, folderSlots, skeletonData.slots.length); + let frame = 0; + for (const drawOrderFolderMap of drawOrderFolderEntries) { + timeline.setFrame(frame++, getValue(drawOrderFolderMap, "time", 0), readDrawOrder(skeletonData, drawOrderFolderMap, folderSlots.length, folderSlots)); + } + timelines.push(timeline); + } + } + // Event timelines. if (map.events) { const timeline = new EventTimeline(map.events.length); @@ -1307,6 +1304,46 @@ function readTimeline2 (timelines: Array, keys: Timeline2KeysType[], t } } +type DrawOrderKeysType = { offsets?: { slot: string, offset: number }[] }; + +/** @param folderSlots Slot names are resolved to positions within this array. If null, slot indices are used as positions. */ +function readDrawOrder (skeletonData: SkeletonData, keys: DrawOrderKeysType, slotCount: number, folderSlots: number[] | null): number[] | null { + const changes = keys.offsets; + if (!changes) return null; // Setup draw order. + const drawOrder = new Array(slotCount).fill(-1); + const unchanged = new Array(slotCount - changes.length); + let originalIndex = 0, unchangedIndex = 0; + for (const offsetMap of changes) { + const slot = skeletonData.findSlot(offsetMap.slot); + if (slot == null) throw new Error(`Draw order slot not found: ${offsetMap.slot}`); + let index = 0; + if (!folderSlots) + index = slot.index; + else { + index = -1; + for (let i = 0; i < slotCount; i++) { + if (folderSlots[i] === slot.index) { + index = i; + break; + } + } + if (index === -1) throw new Error(`Slot not in folder: ${offsetMap.slot}`); + } + // Collect unchanged items. + while (originalIndex !== index) + unchanged[unchangedIndex++] = originalIndex++; + // Set changed items. + drawOrder[originalIndex + offsetMap.offset] = originalIndex++; + } + // Collect remaining unchanged items. + while (originalIndex < slotCount) + unchanged[unchangedIndex++] = originalIndex++; + // Fill in unchanged items. + for (let i = slotCount - 1; i >= 0; i--) + if (drawOrder[i] === -1) drawOrder[i] = unchanged[--unchangedIndex]; + return drawOrder; +} + function readCurve (curve: [number, number, number, number] | "stepped", timeline: CurveTimeline, bezier: number, frame: number, value: number, time1: number, time2: number, value1: number, value2: number, scale: number) { if (curve === "stepped") {