From 04736e9935c980f2cfc6e0e80880db295dae2e25 Mon Sep 17 00:00:00 2001 From: Davide Tantillo Date: Wed, 5 Jun 2024 14:43:56 +0200 Subject: [PATCH] [ts][pixi] Add feature to attach pixi objects to slots --- .../spine-pixi/example/assets/spine_logo.png | Bin 0 -> 3495 bytes spine-ts/spine-pixi/example/slot-objects.html | 125 ++++++++++++++++++ spine-ts/spine-pixi/src/Spine.ts | 87 +++++++++++- 3 files changed, 209 insertions(+), 3 deletions(-) create mode 100644 spine-ts/spine-pixi/example/assets/spine_logo.png create mode 100644 spine-ts/spine-pixi/example/slot-objects.html diff --git a/spine-ts/spine-pixi/example/assets/spine_logo.png b/spine-ts/spine-pixi/example/assets/spine_logo.png new file mode 100644 index 0000000000000000000000000000000000000000..40e65c5c91884ba3af3a5d70cb8eb265c6b3baeb GIT binary patch literal 3495 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 + + + spine-pixi + + + + + + + + + + \ No newline at end of file diff --git a/spine-ts/spine-pixi/src/Spine.ts b/spine-ts/spine-pixi/src/Spine.ts index d32515b918..e32d2516e3 100644 --- a/spine-ts/spine-pixi/src/Spine.ts +++ b/spine-ts/spine-pixi/src/Spine.ts @@ -34,6 +34,7 @@ import { AtlasAttachmentLoader, ClippingAttachment, Color, + MathUtils, MeshAttachment, Physics, RegionAttachment, @@ -224,8 +225,78 @@ export class Spine extends Container { } } - private verticesCache: NumberArrayLike = Utils.newFloatArray(1024); + private slotsObject = new Map(); + private getSlotFromRef(slotRef: number | string | Slot): Slot { + let slot: Slot | null; + if (typeof slotRef === 'number') slot = this.skeleton.slots[slotRef]; + else if (typeof slotRef === 'string') slot = this.skeleton.findSlot(slotRef); + else slot = slotRef; + + if (!slot) throw new Error(`No slot found with the given slot reference: ${slotRef}`); + return slot; + } + /** + * Add a pixi DisplayObject as a child of the Spine object. + * The DisplayObject will be rendered coherently with the draw order of the slot. + * If an attachment is active on the slot, the pixi DisplayObject will be rendered on top of it. + * If the DisplayObject is already attached to the given slot, nothing will happen. + * If the DisplayObject is already attached to another slot, it will be removed from that slot + * before adding it to the given one. + * If another DisplayObject is already attached to this slot, the old one will be removed from this + * slot before adding it to the current one. + * @param slotRef - The slot index, or the slot name, or the Slot where the pixi object will be added to. + * @param pixiObject - The pixi DisplayObject to add. + */ + addSlotObject(slotRef: number | string | Slot, pixiObject: DisplayObject): void { + let slot = this.getSlotFromRef(slotRef); + let oldPixiObject = this.slotsObject.get(slot); + + // search if the pixiObject was already in another slotObject + if (!oldPixiObject) { + for (const [slot, oldPixiObjectAnotherSlot] of this.slotsObject) { + if (oldPixiObjectAnotherSlot === pixiObject) { + this.removeSlotObject(slot, pixiObject); + break; + } + } + } + + if (oldPixiObject === pixiObject) return; + if (oldPixiObject) this.removeChild(oldPixiObject); + + this.slotsObject.set(slot, pixiObject); + this.addChild(pixiObject); + } + /** + * Return the DisplayObject connected to the given slot, if any. + * Otherwise return undefined + * @param pixiObject - The slot index, or the slot name, or the Slot to get the DisplayObject from. + * @returns a DisplayObject if any, undefined otherwise. + */ + getSlotObject(slotRef: number | string | Slot): DisplayObject | undefined { + return this.slotsObject.get(this.getSlotFromRef(slotRef)); + } + /** + * Remove a slot object from the given slot. + * If `pixiObject` is passed and attached to the given slot, remove it from the slot. + * If `pixiObject` is not passed and the given slot has an attached DisplayObject, remove it from the slot. + * @param slotRef - The slot index, or the slot name, or the Slot where the pixi object will be remove from. + * @param pixiObject - Optional, The pixi DisplayObject to remove. + */ + removeSlotObject(slotRef: number | string | Slot, pixiObject?: DisplayObject): void { + let slot = this.getSlotFromRef(slotRef); + let slotObject = this.slotsObject.get(slot); + if (!slotObject) return; + + // if pixiObject is passed, remove only if it is equal to the given one + if (pixiObject && pixiObject !== slotObject) return; + + this.removeChild(slotObject); + this.slotsObject.delete(slot); + } + + private verticesCache: NumberArrayLike = Utils.newFloatArray(1024); private renderMeshes (): void { this.resetMeshes(); @@ -233,8 +304,18 @@ export class Spine extends Container { let uvs: NumberArrayLike | null = null; const drawOrder = this.skeleton.drawOrder; - for (let i = 0, n = drawOrder.length; i < n; i++) { + for (let i = 0, n = drawOrder.length, slotObjectsCounter = 0; i < n; i++) { const slot = drawOrder[i]; + + // render pixi object on the current slot on top of the slot attachment + let pixiObject = this.slotsObject.get(slot); + let zIndex = i + slotObjectsCounter; + if (pixiObject) { + pixiObject.setTransform(slot.bone.worldX, slot.bone.worldY, slot.bone.scaleX, slot.bone.scaleY, slot.bone.getWorldRotationX() * MathUtils.degRad); + pixiObject.zIndex = zIndex + 1; + slotObjectsCounter++; + } + const useDarkColor = slot.darkColor != null; const vertexSize = Spine.clipper.isClipping() ? 2 : useDarkColor ? Spine.DARK_VERTEX_SIZE : Spine.VERTEX_SIZE; if (!slot.bone.active) { @@ -331,7 +412,7 @@ export class Spine extends Container { } const mesh = this.getMeshForSlot(slot); - mesh.zIndex = i; + mesh.zIndex = zIndex; mesh.updateFromSpineData(texture, slot.data.blendMode, slot.data.name, finalVertices, finalVerticesLength, finalIndices, finalIndicesLength, useDarkColor); }