diff --git a/examples/assets/spine_logo.png b/examples/assets/spine_logo.png
new file mode 100644
index 0000000..40e65c5
Binary files /dev/null and b/examples/assets/spine_logo.png differ
diff --git a/examples/events-example.html b/examples/events-example.html
index 1eca2a5..6c37961 100644
--- a/examples/events-example.html
+++ b/examples/events-example.html
@@ -2,7 +2,7 @@
spine-pixi
-
+
@@ -35,8 +35,8 @@
document.body.appendChild(app.view);
// Pre-load the skeleton data and atlas. You can also load .json skeleton data.
- PIXI.Assets.add("spineboyData", "./assets/spineboy-pro.skel");
- PIXI.Assets.add("spineboyAtlas", "./assets/spineboy-pma.atlas");
+ PIXI.Assets.add({alias: "spineboyData", src: "./assets/spineboy-pro.skel" });
+ PIXI.Assets.add({alias: "spineboyAtlas", src: "./assets/spineboy-pma.atlas" });
await PIXI.Assets.load(["spineboyData", "spineboyAtlas"]);
// Create the Spine display object
diff --git a/examples/index.html b/examples/index.html
index da69aff..f1b9866 100644
--- a/examples/index.html
+++ b/examples/index.html
@@ -2,7 +2,7 @@
spine-pixi
-
+
diff --git a/examples/manual-loading.html b/examples/manual-loading.html
index 1792aa3..7acbd87 100644
--- a/examples/manual-loading.html
+++ b/examples/manual-loading.html
@@ -2,7 +2,7 @@
spine-pixi
-
+
@@ -24,8 +24,8 @@
document.body.appendChild(app.view);
// Pre-load the skeleton data and atlas. You can also load .json skeleton data.
- PIXI.Assets.add("spineboyData", "./assets/spineboy-pro.skel");
- PIXI.Assets.add("spineboyAtlas", "./assets/spineboy-pma.atlas");
+ PIXI.Assets.add({alias: "spineboyData", src: "./assets/spineboy-pro.skel" });
+ PIXI.Assets.add({alias: "spineboyAtlas", src: "./assets/spineboy-pma.atlas" });
await PIXI.Assets.load(["spineboyData", "spineboyAtlas"]);
// Manually load the data and create a Spine display object from it using
diff --git a/examples/mix-and-match-example.html b/examples/mix-and-match-example.html
index 30a636a..ac69f38 100644
--- a/examples/mix-and-match-example.html
+++ b/examples/mix-and-match-example.html
@@ -2,7 +2,7 @@
spine-pixi
-
+
@@ -24,8 +24,8 @@
document.body.appendChild(app.view);
// Pre-load the skeleton data and atlas. You can also load .json skeleton data.
- PIXI.Assets.add("mixAndMatchData", "./assets/mix-and-match-pro.skel");
- PIXI.Assets.add("mixAndMatchAtlas", "./assets/mix-and-match-pma.atlas");
+ PIXI.Assets.add({alias: "mixAndMatchData", src: "./assets/mix-and-match-pro.skel" });
+ PIXI.Assets.add({alias: "mixAndMatchAtlas", src: "./assets/mix-and-match-pma.atlas" });
await PIXI.Assets.load(["mixAndMatchData", "mixAndMatchAtlas"]);
// Create the Spine display object
diff --git a/examples/mouse-following.html b/examples/mouse-following.html
index 9038b26..b157ba7 100644
--- a/examples/mouse-following.html
+++ b/examples/mouse-following.html
@@ -2,7 +2,7 @@
Spine Pixi Example
-
+
@@ -24,8 +24,8 @@
document.body.appendChild(app.view);
// Pre-load the skeleton data and atlas. You can also load .json skeleton data.
- PIXI.Assets.add("spineboyData", "./assets/spineboy-pro.skel");
- PIXI.Assets.add("spineboyAtlas", "./assets/spineboy-pma.atlas");
+ PIXI.Assets.add({alias: "spineboyData", src: "./assets/spineboy-pro.skel" });
+ PIXI.Assets.add({alias: "spineboyAtlas", src: "./assets/spineboy-pma.atlas" });
await PIXI.Assets.load(["spineboyData", "spineboyAtlas"]);
// Create the spine display object
@@ -47,6 +47,7 @@
// Add the display object to the stage.
app.stage.addChild(spineboy);
+ app.stage.hitArea = new PIXI.Rectangle(0, 0, app.view.width, app.view.height);
// Make the stage interactive and register pointer events
app.stage.eventMode = "dynamic";
@@ -57,7 +58,7 @@
setBonePosition(e);
});
- app.stage.on("pointermove", (e) => {
+ app.stage.on("globalpointermove", (e) => {
if (isDragging) setBonePosition(e);
});
diff --git a/examples/simple-input.html b/examples/simple-input.html
index 8220bf1..5c13edd 100644
--- a/examples/simple-input.html
+++ b/examples/simple-input.html
@@ -2,7 +2,7 @@
spine-pixi
-
+
@@ -24,8 +24,8 @@
document.body.appendChild(app.view);
// Pre-load the skeleton data and atlas. You can also load .json skeleton data.
- PIXI.Assets.add("spineboyData", "./assets/spineboy-pro.skel");
- PIXI.Assets.add("spineboyAtlas", "./assets/spineboy-pma.atlas");
+ PIXI.Assets.add({alias: "spineboyData", src: "./assets/spineboy-pro.skel" });
+ PIXI.Assets.add({alias: "spineboyAtlas", src: "./assets/spineboy-pma.atlas" });
await PIXI.Assets.load(["spineboyData", "spineboyAtlas"]);
// Create the spine display object
@@ -35,7 +35,7 @@
// Set the default animation and the
// default mix for transitioning between animations.
- spineboy.state.setAnimation(0, "run", true);
+ spineboy.state.setAnimation(0, "hoverboard", true);
spineboy.state.data.defaultMix = 0.2;
// Center the spine object on screen.
@@ -43,20 +43,56 @@
spineboy.y = window.innerHeight / 2 + spineboy.getBounds().height / 2;
// Make it so that you can interact with Spineboy.
- // Also, handle the case that you click or tap on the screen.
- // The callback function definition can be seen below.
+ // Handle the case that you click/tap the screen.
spineboy.eventMode = 'static';
spineboy.on('pointerdown', onClick);
-
+
// Add the display object to the stage.
app.stage.addChild(spineboy);
+
+ // Add variables for movement, speed.
+ let moveLeft = false;
+ let moveRight = false;
+ const speed = 5;
+
+ // Handle the case that the keyboard keys specified below are pressed.
+ function onKeyDown(key) {
+ if (key.code === "ArrowLeft" || key.code === "KeyA") {
+ moveLeft = true;
+ spineboy.skeleton.scaleX = -1;
+ } else if (key.code === "ArrowRight" || key.code === "KeyD") {
+ moveRight = true;
+ spineboy.skeleton.scaleX = 1;
+ }
+ }
+
+ // Handle when the keys are released, if they were pressed.
+ function onKeyUp(key) {
+ if (key.code === "ArrowLeft" || key.code === "KeyA") {
+ moveLeft = false;
+ } else if (key.code === "ArrowRight" || key.code === "KeyD") {
+ moveRight = false;
+ }
+ }
- // This callback function handles what happens
- // when you click or tap on the screen.
+ // Handle if you click/tap the screen.
function onClick() {
- spineboy.state.addAnimation(0, "jump", false, 0);
- spineboy.state.addAnimation(0, "idle", true, 0);
+ spineboy.state.setAnimation(1, "shoot", false, 0);
}
+
+ // Add event listeners so that the window will correctly handle input.
+ window.addEventListener("keydown", onKeyDown);
+ window.addEventListener("keyup", onKeyUp);
+
+ // Update the application to move Spineboy if input is detected.
+ app.ticker.add(() => {
+ if (moveLeft) {
+ spineboy.x -= speed;
+ }
+ if (moveRight) {
+ spineboy.x += speed;
+ }
+ });
})();
diff --git a/examples/slot-objects.html b/examples/slot-objects.html
new file mode 100644
index 0000000..3f09381
--- /dev/null
+++ b/examples/slot-objects.html
@@ -0,0 +1,125 @@
+
+
+
+ spine-pixi
+
+
+
+
+
+
+
+
+
+
diff --git a/src/BatchableClippedSpineSlot.ts b/src/BatchableClippedSpineSlot.ts
deleted file mode 100644
index 9cae5da..0000000
--- a/src/BatchableClippedSpineSlot.ts
+++ /dev/null
@@ -1,123 +0,0 @@
-/** ****************************************************************************
- * Spine Runtimes License Agreement
- * Last updated July 28, 2023. Replaces all prior versions.
- *
- * Copyright (c) 2013-2023, 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.
- *****************************************************************************/
-
-import { Spine } from './Spine';
-
-import type { Batch, BatchableObject, Batcher, IndexBufferArray, Texture } from 'pixi.js';
-import type { SkeletonClipping, Slot } from '@esotericsoftware/spine-core';
-
-export class BatchableClippedSpineSlot implements BatchableObject
-{
- indexStart: number;
- textureId: number;
- texture: Texture;
- location: number;
- batcher: Batcher;
- batch: Batch;
- renderable: Spine;
-
- slot:Slot;
- indexSize: number;
- vertexSize: number;
- clippedVertices: number[] = [];
- clippedTriangles: number[] = [];
-
- roundPixels: 0 | 1;
-
- get blendMode() { return this.renderable.groupBlendMode; }
-
- reset()
- {
- this.renderable = null as any;
- this.texture = null as any;
- this.batcher = null as any;
- this.batch = null as any;
- }
-
- setClipper(clipper:SkeletonClipping)
- {
- // copy clipped verts and triangles
- copyArray(clipper.clippedVertices, this.clippedVertices);
- copyArray(clipper.clippedTriangles, this.clippedTriangles);
-
- this.vertexSize = (clipper.clippedVertices.length / 8);
- this.indexSize = clipper.clippedTriangles.length;
- }
-
- packIndex(indexBuffer: IndexBufferArray, index: number, indicesOffset: number)
- {
- const indices = this.clippedTriangles;
-
- for (let i = 0; i < indices.length; i++)
- {
- indexBuffer[index++] = indices[i] + indicesOffset;
- }
- }
-
- packAttributes(
- float32View: Float32Array,
- uint32View: Uint32Array,
- index: number,
- textureId: number
- )
- {
- const clippedVertices = this.clippedVertices;
- const vertexSize = this.vertexSize;
-
- const abgr = this.renderable.groupColor;
-
- const textureIdAndRound = (textureId << 16) | (this.roundPixels & 0xFFFF);
-
- for (let i = 0; i < vertexSize; i++)
- {
- const localIndex = i * 8;
-
- // position
- float32View[index++] = clippedVertices[localIndex];
- float32View[index++] = clippedVertices[localIndex + 1] * -1;
-
- // uv
- float32View[index++] = clippedVertices[localIndex + 6];
- float32View[index++] = clippedVertices[localIndex + 7];
- // color
- uint32View[index++] = abgr;
-
- // texture id
- float32View[index++] = textureIdAndRound;
- }
- }
-}
-
-function copyArray(a:number[], b:number[])
-{
- for (let i = 0; i < a.length; i++)
- {
- b[i] = a[i];
- }
-}
diff --git a/src/BatchableSpineSlot.ts b/src/BatchableSpineSlot.ts
index 3ca8c9a..25ff043 100644
--- a/src/BatchableSpineSlot.ts
+++ b/src/BatchableSpineSlot.ts
@@ -27,12 +27,9 @@
* SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*****************************************************************************/
-import { Spine } from './Spine';
-import { MeshAttachment, RegionAttachment, Slot } from '@esotericsoftware/spine-core';
+import { AttachmentCacheData, Spine } from './Spine';
-import type { Batch, BatchableObject, Batcher, IndexBufferArray, Texture } from 'pixi.js';
-
-const QUAD_TRIANGLES = [0, 1, 2, 2, 3, 0];
+import type { Batch, BatchableObject, Batcher, BLEND_MODES, IndexBufferArray, Texture } from 'pixi.js';
export class BatchableSpineSlot implements BatchableObject
{
@@ -44,43 +41,55 @@ export class BatchableSpineSlot implements BatchableObject
batch: Batch;
renderable: Spine;
- slot:Slot;
+ vertices: Float32Array;
+ indices: number[] | Uint16Array;
+ uvs: Float32Array;
+
indexSize: number;
vertexSize: number;
roundPixels: 0 | 1;
-
- get blendMode() { return this.renderable.groupBlendMode; }
-
- reset()
- {
- this.renderable = null as any;
- this.texture = null as any;
- this.batcher = null as any;
- this.batch = null as any;
- }
-
- setSlot(slot:Slot)
+ data: AttachmentCacheData;
+ blendMode: BLEND_MODES;
+
+ setData(
+ renderable:Spine,
+ data:AttachmentCacheData,
+ texture:Texture,
+ blendMode:BLEND_MODES,
+ roundPixels: 0 | 1)
{
- this.slot = slot;
-
- const attachment = slot.getAttachment();
+ this.renderable = renderable;
+ this.data = data;
- if (attachment instanceof RegionAttachment)
+ if (data.clipped)
{
- this.vertexSize = 4;
- this.indexSize = 6;
+ const clippedData = data.clippedData;
+
+ this.indexSize = clippedData.indicesCount;
+ this.vertexSize = clippedData.vertexCount;
+ this.vertices = clippedData.vertices;
+ this.indices = clippedData.indices;
+ this.uvs = clippedData.uvs;
}
- else if (attachment instanceof MeshAttachment)
+ else
{
- this.vertexSize = attachment.worldVerticesLength / 2;
- this.indexSize = attachment.triangles.length;
+ this.indexSize = data.indices.length;
+ this.vertexSize = data.vertices.length / 2;
+ this.vertices = data.vertices;
+ this.indices = data.indices;
+ this.uvs = data.uvs;
}
+
+ this.texture = texture;
+ this.roundPixels = roundPixels;
+
+ this.blendMode = blendMode;
}
packIndex(indexBuffer: IndexBufferArray, index: number, indicesOffset: number)
{
- const indices = (this.slot.getAttachment() as MeshAttachment).triangles ?? QUAD_TRIANGLES;
+ const indices = this.indices;
for (let i = 0; i < indices.length; i++)
{
@@ -95,25 +104,13 @@ export class BatchableSpineSlot implements BatchableObject
textureId: number
)
{
- const slot = this.slot;
- const attachment = slot.getAttachment() as MeshAttachment | RegionAttachment;
+ const { uvs, vertices, vertexSize } = this;
- if (attachment instanceof MeshAttachment)
- {
- attachment.computeWorldVertices(slot, 0, attachment.worldVerticesLength, float32View, index, 6);
- }
- else if (attachment instanceof RegionAttachment)
- {
- attachment.computeWorldVertices(slot, float32View, index, 6);
- }
-
- const vertexSize = this.vertexSize;
+ const slotColor = this.data.color;
- const parentColor:number = this.renderable.groupColor; // BGR
+ const parentColor:number = this.renderable.groupColor;
const parentAlpha:number = this.renderable.groupAlpha;
- const slotColor: {r: number, g:number, b: number, a: number} = slot.color;
-
let abgr:number;
const mixedA = (slotColor.a * parentAlpha) * 255;
@@ -135,8 +132,6 @@ export class BatchableSpineSlot implements BatchableObject
abgr = ((mixedA) << 24) | ((slotColor.b * 255) << 16) | ((slotColor.g * 255) << 8) | (slotColor.r * 255);
}
- const uvs = attachment.uvs;
-
const matrix = this.renderable.groupTransform;
const a = matrix.a;
@@ -150,10 +145,8 @@ export class BatchableSpineSlot implements BatchableObject
for (let i = 0; i < vertexSize; i++)
{
- // index++;
- // float32View[index++] *= -1;
- const x = float32View[index];
- const y = -float32View[index + 1];
+ const x = vertices[i * 2];
+ const y = vertices[(i * 2) + 1];
float32View[index++] = (a * x) + (c * y) + tx;
float32View[index++] = (b * x) + (d * y) + ty;
diff --git a/src/Spine.ts b/src/Spine.ts
index a85223c..0f459b9 100644
--- a/src/Spine.ts
+++ b/src/Spine.ts
@@ -35,24 +35,31 @@ import {
ContainerOptions,
DEG_TO_RAD,
DestroyOptions,
- PointData, Ticker,
- View
+ PointData,
+ Ticker,
+ View,
} from 'pixi.js';
-import { getSkeletonBounds } from './getSkeletonBounds';
import { ISpineDebugRenderer } from './SpineDebugRenderer';
import {
AnimationState,
AnimationStateData,
AtlasAttachmentLoader,
+ Attachment,
Bone,
+ ClippingAttachment,
+ Color,
+ MeshAttachment,
+ RegionAttachment,
Skeleton,
SkeletonBinary,
SkeletonBounds,
+ SkeletonClipping,
SkeletonData,
SkeletonJson,
+ Slot,
type TextureAtlas,
TrackEntry,
- Vector2
+ Vector2,
} from '@esotericsoftware/spine-core';
export type SpineFromOptions = {
@@ -62,6 +69,12 @@ export type SpineFromOptions = {
};
const vectorAux = new Vector2();
+const lightColor = new Color();
+const darkColor = new Color();
+
+Skeleton.yDown = true;
+
+const clipper = new SkeletonClipping();
export interface SpineOptions extends ContainerOptions
{
@@ -79,6 +92,23 @@ export interface SpineEvents
start: [trackEntry: TrackEntry];
}
+export interface AttachmentCacheData
+{
+ id: string;
+ clipped: boolean;
+ vertices: Float32Array;
+ uvs: Float32Array;
+ indices: number[];
+ color: { r: number; g: number; b: number; a: number };
+ clippedData?: {
+ vertices: Float32Array;
+ uvs: Float32Array;
+ indices: Uint16Array;
+ vertexCount: number;
+ indicesCount: number;
+ };
+}
+
export class Spine extends Container implements View
{
// Pixi properties
@@ -88,7 +118,7 @@ export class Spine extends Container implements View
public _didSpineUpdate = false;
public _boundsDirty = true;
public _roundPixels: 0 | 1;
- private _bounds:Bounds = new Bounds();
+ private _bounds: Bounds = new Bounds();
// Spine properties
public skeleton: Skeleton;
@@ -96,12 +126,32 @@ export class Spine extends Container implements View
public skeletonBounds: SkeletonBounds;
private _debug?: ISpineDebugRenderer | undefined = undefined;
- private readonly _mappings:{bone:Bone, container:Container}[] = [];
+ readonly _slotsObject: Record = Object.create(null);
+
+ 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;
+ }
+
+ public spineAttachmentsDirty: boolean;
+ private _lastAttachments: Attachment[];
+
+ private _stateChanged: boolean;
+ private attachmentCacheData: Record = {};
public get debug(): ISpineDebugRenderer | undefined
{
return this._debug;
}
+
public set debug(value: ISpineDebugRenderer | undefined)
{
if (this._debug)
@@ -114,12 +164,15 @@ export class Spine extends Container implements View
}
this._debug = value;
}
+
private autoUpdateWarned = false;
private _autoUpdate = true;
+
public get autoUpdate(): boolean
{
return this._autoUpdate;
}
+
public set autoUpdate(value: boolean)
{
if (value)
@@ -131,15 +184,16 @@ export class Spine extends Container implements View
{
Ticker.shared.remove(this.internalUpdate, this);
}
+
this._autoUpdate = value;
}
- constructor(options:SpineOptions | SkeletonData)
+ constructor(options: SpineOptions | SkeletonData)
{
if (options instanceof SkeletonData)
{
options = {
- skeletonData: options
+ skeletonData: options,
};
}
@@ -150,16 +204,21 @@ export class Spine extends Container implements View
this.skeleton = new Skeleton(skeletonData);
this.state = new AnimationState(new AnimationStateData(skeletonData));
this.autoUpdate = options?.autoUpdate ?? true;
+
+ this._updateState(0);
}
public update(dt: number): void
{
if (this.autoUpdate && !this.autoUpdateWarned)
{
- // eslint-disable-next-line max-len
- console.warn('You are calling update on a Spine instance that has autoUpdate set to true. This is probably not what you want.');
+ console.warn(
+ // eslint-disable-next-line max-len
+ 'You are calling update on a Spine instance that has autoUpdate set to true. This is probably not what you want.',
+ );
this.autoUpdateWarned = true;
}
+
this.internalUpdate(0, dt);
}
@@ -167,7 +226,7 @@ export class Spine extends Container implements View
{
// Because reasons, pixi uses deltaFrames at 60fps.
// We ignore the default deltaFrames and use the deltaSeconds from pixi ticker.
- this.updateState(deltaSeconds ?? Ticker.shared.deltaMS / 1000);
+ this._updateState(deltaSeconds ?? Ticker.shared.deltaMS / 1000);
}
get bounds()
@@ -202,7 +261,7 @@ export class Spine extends Container implements View
else
{
bone.x = vectorAux.x;
- bone.y = -vectorAux.y;
+ bone.y = vectorAux.y;
}
}
@@ -228,45 +287,304 @@ export class Spine extends Container implements View
}
outPos.x = bone.worldX;
- outPos.y = -bone.worldY;
+ outPos.y = bone.worldY;
return outPos;
}
- updateState(dt:number)
+ /**
+ * Will update the state based on the specified time, this will not apply the state to the skeleton
+ * as this is differed until the `applyState` method is called.
+ *
+ * @param time the time at which to set the state
+ * @internal
+ */
+ _updateState(time: number)
{
- this.state.update(dt);
+ this.state.update(time);
+
+ this._stateChanged = true;
+
this._boundsDirty = true;
- // update the mappings..
+ this.onViewUpdate();
+ }
+
+ /**
+ * Applies the state to this spine instance.
+ * - updates the state to the skeleton
+ * - updates its world transform (spine world transform)
+ * - validates the attachments - to flag if the attachments have changed this state
+ * - transforms the attachments - to update the vertices of the attachments based on the new positions
+ * - update the slot attachments - to update the position, rotation, scale, and visibility of the attached containers
+ * @internal
+ */
+ _applyState()
+ {
+ if (!this._stateChanged) return;
+ this._stateChanged = false;
+
+ const { skeleton } = this;
+
+ this.state.apply(skeleton);
+
+ skeleton.updateWorldTransform();
+
+ this.validateAttachments();
+
+ this.transformAttachments();
+
+ this.updateSlotObjects();
+ }
+
+ private validateAttachments()
+ {
+ const currentDrawOrder = this.skeleton.drawOrder;
+
+ const lastAttachments = (this._lastAttachments ||= []);
+
+ let index = 0;
- this._mappings.forEach((mapping) =>
+ let spineAttachmentsDirty = false;
+
+ for (let i = 0; i < currentDrawOrder.length; i++)
+ {
+ const slot = currentDrawOrder[i];
+ const attachment = slot.getAttachment();
+
+ if (attachment)
+ {
+ if (attachment !== lastAttachments[index])
+ {
+ spineAttachmentsDirty = true;
+ lastAttachments[index] = attachment;
+ }
+
+ index++;
+ }
+ }
+
+ if (index !== lastAttachments.length)
+ {
+ spineAttachmentsDirty = true;
+ lastAttachments.length = index;
+ }
+
+ this.spineAttachmentsDirty = spineAttachmentsDirty;
+ }
+
+ private transformAttachments()
+ {
+ const currentDrawOrder = this.skeleton.drawOrder;
+
+ for (let i = 0; i < currentDrawOrder.length; i++)
+ {
+ const slot = currentDrawOrder[i];
+
+ const attachment = slot.getAttachment();
+
+ if (attachment)
+ {
+ if (attachment instanceof MeshAttachment || attachment instanceof RegionAttachment)
+ {
+ const cacheData = this._getCachedData(slot, attachment);
+
+ if (attachment instanceof RegionAttachment)
+ {
+ attachment.computeWorldVertices(slot, cacheData.vertices, 0, 2);
+ }
+ else
+ {
+ attachment.computeWorldVertices(
+ slot,
+ 0,
+ attachment.worldVerticesLength,
+ cacheData.vertices,
+ 0,
+ 2,
+ );
+ }
+
+ cacheData.clipped = false;
+
+ if (clipper.isClipping())
+ {
+ this.updateClippingData(cacheData);
+ }
+ }
+ else if (attachment instanceof ClippingAttachment)
+ {
+ clipper.clipStart(slot, attachment);
+ }
+ else
+ {
+ clipper.clipEndWithSlot(slot);
+ }
+ }
+ }
+
+ clipper.clipEnd();
+ }
+
+ private updateClippingData(cacheData: AttachmentCacheData)
+ {
+ cacheData.clipped = true;
+
+ clipper.clipTriangles(
+ cacheData.vertices,
+ cacheData.vertices.length,
+ cacheData.indices,
+ cacheData.indices.length,
+ cacheData.uvs,
+ lightColor,
+ darkColor,
+ false,
+ );
+
+ const { clippedVertices, clippedTriangles } = clipper;
+
+ const verticesCount = clippedVertices.length / 8;
+ const indicesCount = clippedTriangles.length;
+
+ if (!cacheData.clippedData)
{
- const { bone, container } = mapping;
+ cacheData.clippedData = {
+ vertices: new Float32Array(verticesCount * 2),
+ uvs: new Float32Array(verticesCount * 2),
+ vertexCount: verticesCount,
+ indices: new Uint16Array(indicesCount),
+ indicesCount,
+ };
+
+ this.spineAttachmentsDirty = true;
+ }
- container.position.set(bone.worldX, -bone.worldY);
+ const clippedData = cacheData.clippedData;
+
+ const sizeChange = clippedData.vertexCount !== verticesCount || indicesCount !== clippedData.indicesCount;
+
+ if (sizeChange)
+ {
+ this.spineAttachmentsDirty = true;
+
+ if (clippedData.vertexCount < verticesCount)
+ {
+ // buffer reuse!
+ clippedData.vertices = new Float32Array(verticesCount * 2);
+ clippedData.uvs = new Float32Array(verticesCount * 2);
+ }
+
+ if (clippedData.indices.length < indicesCount)
+ {
+ clippedData.indices = new Uint16Array(indicesCount);
+ }
+ }
+
+ const { vertices, uvs, indices } = clippedData;
+
+ for (let i = 0; i < verticesCount; i++)
+ {
+ vertices[i * 2] = clippedVertices[i * 8];
+ vertices[(i * 2) + 1] = clippedVertices[(i * 8) + 1];
+
+ uvs[i * 2] = clippedVertices[(i * 8) + 6];
+ uvs[(i * 2) + 1] = clippedVertices[(i * 8) + 7];
+ }
+
+ clippedData.vertexCount = verticesCount;
+
+ for (let i = 0; i < indices.length; i++)
+ {
+ indices[i] = clippedTriangles[i];
+ }
+
+ clippedData.indicesCount = indicesCount;
+ }
+
+ /**
+ * ensure that attached containers map correctly to their slots
+ * along with their position, rotation, scale, and visibility.
+ */
+ private updateSlotObjects()
+ {
+ for (const i in this._slotsObject)
+ {
+ const slotAttachment = this._slotsObject[i];
+
+ if (!slotAttachment) continue;
+
+ this.updateSlotObject(slotAttachment);
+ }
+ }
+
+ private updateSlotObject(slotAttachment: {slot:Slot, container:Container})
+ {
+ const { slot, container } = slotAttachment;
+
+ container.visible = this.skeleton.drawOrder.includes(slot);
+
+ if (container.visible)
+ {
+ const bone = slot.bone;
+
+ container.position.set(bone.worldX, bone.worldY);
container.scale.x = bone.getWorldScaleX();
container.scale.y = bone.getWorldScaleY();
- const rotationX = bone.getWorldRotationX() * DEG_TO_RAD;
- const rotationY = bone.getWorldRotationY() * DEG_TO_RAD;
+ container.rotation = bone.getWorldRotationX() * DEG_TO_RAD;
+ }
+ }
- container.rotation = -Math.atan2(
- Math.sin(rotationX) + Math.sin(rotationY),
- Math.cos(rotationX) + Math.cos(rotationY)
- );
- });
- this.onViewUpdate();
+ /** @internal */
+ _getCachedData(slot: Slot, attachment: RegionAttachment | MeshAttachment): AttachmentCacheData
+ {
+ const key = `${slot.data.index}-${attachment.name}`;
+
+ return this.attachmentCacheData[key] || this.initCachedData(slot, attachment);
+ }
+
+ private initCachedData(slot: Slot, attachment: RegionAttachment | MeshAttachment): AttachmentCacheData
+ {
+ const key = `${slot.data.index}-${attachment.name}`;
+
+ let vertices: Float32Array;
+
+ if (attachment instanceof RegionAttachment)
+ {
+ vertices = new Float32Array(8);
+
+ this.attachmentCacheData[key] = {
+ id: key,
+ vertices,
+ clipped: false,
+ indices: [0, 1, 2, 0, 2, 3],
+ uvs: attachment.uvs as Float32Array,
+ color: slot.color,
+ };
+ }
+ else
+ {
+ vertices = new Float32Array(attachment.worldVerticesLength);
+
+ this.attachmentCacheData[key] = {
+ id: key,
+ vertices,
+ clipped: false,
+ indices: attachment.triangles,
+ uvs: attachment.uvs as Float32Array,
+ color: slot.color,
+ };
+ }
+
+ return this.attachmentCacheData[key];
}
onViewUpdate()
{
// increment from the 12th bit!
this._didChangeId += 1 << 12;
- this._didSpineUpdate = true;
- this._didSpineUpdate = true;
this._boundsDirty = true;
if (this.didViewUpdate) return;
@@ -283,68 +601,92 @@ export class Spine extends Container implements View
}
/**
- * Attaches a PixiJS container to a specified bone. This will map the world transform of the bone
- * to the attached container. A container can only be attached to one bone at a time.
+ * Attaches a PixiJS container to a specified slot. This will map the world transform of the slots bone
+ * to the attached container. A container can only be attached to one slot at a time.
*
- * @param container - The container to attach to the bone
- * @param bone - The bone id or bone to attach to
+ * @param container - The container to attach to the slot
+ * @param slotRef - The slot id or slot to attach to
*/
- attachToBone(container:Container, bone:string | Bone)
+ addSlotObject(slot: number | string | Slot, container: Container)
{
- this.detachFromBone(container, bone);
+ slot = this.getSlotFromRef(slot);
- if (typeof bone === 'string')
+ // need to check in on the container too...
+ for (const i in this._slotsObject)
{
- bone = this.skeleton.findBone(bone) as Bone;
+ if (this._slotsObject[i]?.container === container)
+ {
+ this.removeSlotObject(this._slotsObject[i].slot);
+ }
}
- if (!bone)
- {
- throw new Error(`Bone ${bone} not found`);
- }
+ this.removeSlotObject(slot);
+
+ container.includeInBuild = false;
// TODO only add once??
this.addChild(container);
- // TODO search for copies... - one container - to one bone!
- this._mappings.push({
- bone,
- container
- });
+ this._slotsObject[slot.data.name] = {
+ container,
+ slot
+ };
+
+ this.updateSlotObject(this._slotsObject[slot.data.name]);
}
/**
- * Removes a PixiJS container from the bone it is attached to.
+ * Removes a PixiJS container from the slot it is attached to.
*
- * @param container - The container to detach from the bone
- * @param bone - The bone id or bone to detach from
+ * @param container - The container to detach from the slot
+ * @param slotOrContainer - The container, slot id or slot to detach from
*/
- detachFromBone(container:Container, bone:string | Bone)
+ removeSlotObject(slotOrContainer: number | string | Slot | Container)
{
- if (typeof bone === 'string')
- {
- bone = this.skeleton.findBone(bone) as Bone;
- }
+ let containerToRemove: Container | undefined;
- if (!bone)
+ if (slotOrContainer instanceof Container)
{
- throw new Error(`Bone ${bone} not found`);
+ for (const i in this._slotsObject)
+ {
+ if (this._slotsObject[i]?.container === slotOrContainer)
+ {
+ this._slotsObject[i] = null;
+
+ containerToRemove = slotOrContainer;
+ break;
+ }
+ }
}
+ else
+ {
+ const slot = this.getSlotFromRef(slotOrContainer);
- this.removeChild(container);
+ containerToRemove = this._slotsObject[slot.data.name]?.container;
+ this._slotsObject[slot.data.name] = null;
+ }
- for (let i = 0; i < this._mappings.length; i++)
+ if (containerToRemove)
{
- const mapping = this._mappings[i];
+ this.removeChild(containerToRemove);
- if (mapping.bone === bone && mapping.container === container)
- {
- this._mappings.splice(i, 1);
- break;
- }
+ containerToRemove.includeInBuild = true;
}
}
+ /**
+ * Returns a container attached to a slot, or undefined if no container is attached.
+ *
+ * @param slotRef - The slot id or slot to get the attachment from
+ * @returns - The container attached to the slot
+ */
+ getSlotObject(slot: number | string | Slot)
+ {
+ slot = this.getSlotFromRef(slot);
+
+ return this._slotsObject[slot.data.name].container;
+ }
+
updateBounds()
{
this._boundsDirty = false;
@@ -357,10 +699,26 @@ export class Spine extends Container implements View
if (skeletonBounds.minX === Infinity)
{
- this.state.apply(this.skeleton);
+ this._applyState();
+
+ const drawOrder = this.skeleton.drawOrder;
+ const bounds = this._bounds;
+
+ bounds.clear();
+
+ for (let i = 0; i < drawOrder.length; i++)
+ {
+ const slot = drawOrder[i];
+
+ const attachment = slot.getAttachment();
+
+ if (attachment && (attachment instanceof RegionAttachment || attachment instanceof MeshAttachment))
+ {
+ const cacheData = this._getCachedData(slot, attachment);
- // now region bounding attachments..
- getSkeletonBounds(this.skeleton, this._bounds);
+ bounds.addVertexData(cacheData.vertices, 0, cacheData.vertices.length);
+ }
+ }
}
else
{
@@ -401,12 +759,15 @@ export class Spine extends Container implements View
public override destroy(options: DestroyOptions = false)
{
super.destroy(options);
+
Ticker.shared.remove(this.internalUpdate, this);
this.state.clearListeners();
this.debug = undefined;
this.skeleton = null as any;
this.state = null as any;
- (this._mappings as any) = null;
+ (this._slotsObject as any) = null;
+ this._lastAttachments = null;
+ this.attachmentCacheData = null as any;
}
/** Whether or not to round the x/y position of the sprite. */
@@ -420,7 +781,7 @@ export class Spine extends Container implements View
this._roundPixels = value ? 1 : 0;
}
- static from({ skeleton, atlas, scale = 1 }:SpineFromOptions)
+ static from({ skeleton, atlas, scale = 1 }: SpineFromOptions)
{
const cacheKey = `${skeleton}-${atlas}`;
@@ -434,7 +795,10 @@ export class Spine extends Container implements View
const atlasAsset = Assets.get(atlas);
const attachmentLoader = new AtlasAttachmentLoader(atlasAsset);
// eslint-disable-next-line max-len
- const parser = skeletonAsset instanceof Uint8Array ? new SkeletonBinary(attachmentLoader) : new SkeletonJson(attachmentLoader);
+ const parser
+ = skeletonAsset instanceof Uint8Array
+ ? new SkeletonBinary(attachmentLoader)
+ : new SkeletonJson(attachmentLoader);
// TODO scale?
parser.scale = scale;
@@ -443,7 +807,7 @@ export class Spine extends Container implements View
Cache.set(cacheKey, skeletonData);
return new Spine({
- skeletonData
+ skeletonData,
});
}
}
diff --git a/src/SpinePipe.ts b/src/SpinePipe.ts
index f2443fc..f87a6bd 100644
--- a/src/SpinePipe.ts
+++ b/src/SpinePipe.ts
@@ -27,18 +27,26 @@
* SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*****************************************************************************/
-import { BigPool, extensions, ExtensionType, type Renderer, type RenderPipe, Texture } from 'pixi.js';
-import { BatchableClippedSpineSlot } from './BatchableClippedSpineSlot';
+import {
+ collectAllRenderables,
+ extensions, ExtensionType,
+ InstructionSet,
+ type Renderer,
+ type RenderPipe,
+ Texture
+} from 'pixi.js';
import { BatchableSpineSlot } from './BatchableSpineSlot';
import { Spine } from './Spine';
-import { ClippingAttachment, Color, MeshAttachment, RegionAttachment, SkeletonClipping } from '@esotericsoftware/spine-core';
+import { MeshAttachment, RegionAttachment, SkeletonClipping } from '@esotericsoftware/spine-core';
-import type { Bone } from '@esotericsoftware/spine-core';
+const clipper = new SkeletonClipping();
-const QUAD_TRIANGLES = [0, 1, 2, 2, 3, 0];
-const QUAD_VERTS = new Float32Array(8);
-const lightColor = new Color();
-const darkColor = new Color();
+const spineBlendModeMap = {
+ 0: 'normal',
+ 1: 'add',
+ 2: 'multiply',
+ 3: 'screen'
+};
// eslint-disable-next-line max-len
export class SpinePipe implements RenderPipe
@@ -55,153 +63,107 @@ export class SpinePipe implements RenderPipe
renderer: Renderer;
- private readonly activeBatchableSpineSlots: (BatchableSpineSlot | BatchableClippedSpineSlot)[] = [];
+ private gpuSpineData:Record = {};
constructor(renderer: Renderer)
{
this.renderer = renderer;
-
- renderer.runners.prerender.add({
- prerender: () =>
- {
- this.buildStart();
- }
- });
}
- validateRenderable(_renderable: Spine): boolean
+ validateRenderable(spine: Spine): boolean
{
- return true;
- }
+ spine._applyState();
+ // loop through and see if the mesh lengths have changed..
- buildStart()
- {
- this._returnActiveBatches();
+ return spine.spineAttachmentsDirty;
}
- addRenderable(spine: Spine)
+ addRenderable(spine: Spine, instructionSet:InstructionSet)
{
- const batcher = this.renderer.renderPipes.batch;
+ const gpuSpine = this.gpuSpineData[spine.uid] ||= { slotBatches: {} };
- const rootBone = spine.skeleton.getRootBone() as Bone;
-
- rootBone.x = 0;
- rootBone.y = 0;
- rootBone.scaleX = 1;
- rootBone.scaleY = 1;
- rootBone.rotation = 0;
-
- spine.state.apply(spine.skeleton);
- spine.skeleton.updateWorldTransform();
+ const batcher = this.renderer.renderPipes.batch;
const drawOrder = spine.skeleton.drawOrder;
- const activeBatchableSpineSlot = this.activeBatchableSpineSlots;
+ const roundPixels = (this.renderer._roundPixels | spine._roundPixels) as 0 | 1;
- const clipper = new SkeletonClipping();
+ spine._applyState();
for (let i = 0, n = drawOrder.length; i < n; i++)
{
const slot = drawOrder[i];
const attachment = slot.getAttachment();
+ const blendMode = spineBlendModeMap[slot.data.blendMode];
if (attachment instanceof RegionAttachment || attachment instanceof MeshAttachment)
{
- if (clipper?.isClipping())
- {
- if (attachment instanceof RegionAttachment)
- {
- const temp = QUAD_VERTS;
-
- attachment.computeWorldVertices(slot, temp, 0, 2);
-
- // TODO this function could be optimised.. no need to write colors for us!
- clipper.clipTriangles(
- QUAD_VERTS,
- QUAD_VERTS.length,
- QUAD_TRIANGLES,
- QUAD_TRIANGLES.length,
- attachment.uvs,
- lightColor,
- darkColor,
- false // useDarkColor
- );
-
- // unwind it!
- if (clipper.clippedVertices.length > 0)
- {
- const batchableSpineSlot = BigPool.get(BatchableClippedSpineSlot);
-
- activeBatchableSpineSlot.push(batchableSpineSlot);
-
- batchableSpineSlot.texture = (attachment.region?.texture.texture) || Texture.WHITE;
- batchableSpineSlot.roundPixels = (this.renderer._roundPixels | spine._roundPixels) as 0 | 1;
-
- batchableSpineSlot.setClipper(clipper);
- batchableSpineSlot.renderable = spine;
-
- batcher.addToBatch(batchableSpineSlot);
- }
- }
- }
- else
- {
- const batchableSpineSlot = BigPool.get(BatchableSpineSlot);
-
- activeBatchableSpineSlot.push(batchableSpineSlot);
-
- batchableSpineSlot.renderable = spine;
+ const cacheData = spine._getCachedData(slot, attachment);
+ const batchableSpineSlot = gpuSpine.slotBatches[cacheData.id] ||= new BatchableSpineSlot();
- batchableSpineSlot.setSlot(slot);
-
- batchableSpineSlot.texture = (attachment.region?.texture.texture) || Texture.EMPTY;
- batchableSpineSlot.roundPixels = (this.renderer._roundPixels | spine._roundPixels) as 0 | 1;
+ if (!cacheData.clipped || (cacheData.clipped && cacheData.clippedData.vertices.length > 0))
+ {
+ batchableSpineSlot.setData(
+ spine,
+ cacheData,
+ (attachment.region?.texture.texture) || Texture.EMPTY,
+ blendMode,
+ roundPixels
+ );
batcher.addToBatch(batchableSpineSlot);
}
}
- else if (attachment instanceof ClippingAttachment)
- {
- clipper.clipStart(slot, attachment);
- }
- else
+
+ const containerAttachment = spine._slotsObject[slot.data.name];
+
+ if (containerAttachment)
{
- clipper.clipEndWithSlot(slot);
+ const container = containerAttachment.container;
+
+ container.includeInBuild = true;
+ collectAllRenderables(container, instructionSet, this.renderer.renderPipes);
+ container.includeInBuild = false;
}
}
clipper.clipEnd();
}
- updateRenderable(_renderable: Spine)
+ updateRenderable(spine: Spine)
{
- // this does not happen.. yet!
// we assume that spine will always change its verts size..
+ const gpuSpine = this.gpuSpineData[spine.uid];
+
+ spine._applyState();
+
+ const drawOrder = spine.skeleton.drawOrder;
+
+ for (let i = 0, n = drawOrder.length; i < n; i++)
+ {
+ const slot = drawOrder[i];
+ const attachment = slot.getAttachment();
+
+ if (attachment instanceof RegionAttachment || attachment instanceof MeshAttachment)
+ {
+ const batchableSpineSlot = gpuSpine.slotBatches[spine._getCachedData(slot, attachment).id];
+
+ batchableSpineSlot.batcher.updateElement(batchableSpineSlot);
+ }
+ }
}
- destroyRenderable(_renderable: Spine)
+ destroyRenderable(spine: Spine)
{
- this._returnActiveBatches();
+ // TODO remove the renderable from the batcher
+ this.gpuSpineData[spine.uid] = null as any;
}
destroy()
{
- this._returnActiveBatches();
+ this.gpuSpineData = null as any;
this.renderer = null as any;
}
-
- private _returnActiveBatches()
- {
- const activeBatchableSpineSlots = this.activeBatchableSpineSlots;
-
- for (let i = 0; i < activeBatchableSpineSlots.length; i++)
- {
- BigPool.return(activeBatchableSpineSlots[i]);
- }
-
- // TODO this can be optimised
- activeBatchableSpineSlots.length = 0;
- }
}
extensions.add(SpinePipe);
diff --git a/src/getSkeletonBounds.ts b/src/getSkeletonBounds.ts
deleted file mode 100644
index d8f9e64..0000000
--- a/src/getSkeletonBounds.ts
+++ /dev/null
@@ -1,84 +0,0 @@
-/** ****************************************************************************
- * Spine Runtimes License Agreement
- * Last updated July 28, 2023. Replaces all prior versions.
- *
- * Copyright (c) 2013-2023, 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.
- *****************************************************************************/
-
-import {
- type Bone,
- ClippingAttachment,
- MeshAttachment,
- RegionAttachment,
- type Skeleton
-} from '@esotericsoftware/spine-core';
-
-import type { Bounds } from 'pixi.js';
-
-const QUAD_VERTS = new Float32Array(8);
-const tempVerts:number[] = [];
-
-export function getSkeletonBounds(skeleton:Skeleton, out:Bounds)
-{
- out.clear();
-
- const rootBone = skeleton.getRootBone() as Bone;
-
- rootBone.x = 0;
- rootBone.y = 0;
- rootBone.scaleX = 1;
- rootBone.scaleY = -1;
- rootBone.rotation = 0;
-
- skeleton.updateWorldTransform();
-
- const drawOrder = skeleton.drawOrder;
-
- for (let i = 0, n = drawOrder.length; i < n; i++)
- {
- const slot = drawOrder[i];
- const attachment = slot.getAttachment();
-
- if (attachment instanceof RegionAttachment)
- {
- const temp = QUAD_VERTS;
-
- attachment.computeWorldVertices(slot, temp, 0, 2);
-
- // TODO this can be skipped if matrix is local??
- out.addVertexData(temp, 0, 8);
- }
- else if (attachment instanceof MeshAttachment)
- {
- attachment.computeWorldVertices(slot, 0, attachment.worldVerticesLength, tempVerts, 0, 2);
-
- out.addVertexData(tempVerts as any as Float32Array, 0, attachment.worldVerticesLength);
- }
- else if (attachment instanceof ClippingAttachment)
- {
- console.warn('[Pixi Spine] ClippingAttachment bounds is not supported yet');
- }
- }
-}