@safe-engine/pixi 8.3.7 → 8.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (139) hide show
  1. package/.github/workflows/npm-publish.yml +35 -0
  2. package/README.md +70 -5
  3. package/dist/app.d.ts +2 -5
  4. package/dist/app.d.ts.map +1 -1
  5. package/dist/app.js +44 -27
  6. package/dist/base/gworld.d.ts +2 -0
  7. package/dist/base/gworld.d.ts.map +1 -1
  8. package/dist/box2d-wasm/ContactListener.d.ts +12 -0
  9. package/dist/box2d-wasm/ContactListener.d.ts.map +1 -0
  10. package/dist/box2d-wasm/ContactListener.js +64 -0
  11. package/dist/box2d-wasm/PhysicsComponent.d.ts +50 -0
  12. package/dist/box2d-wasm/PhysicsComponent.d.ts.map +1 -0
  13. package/dist/box2d-wasm/PhysicsComponent.js +17 -0
  14. package/dist/box2d-wasm/PhysicsSprite.d.ts +11 -0
  15. package/dist/box2d-wasm/PhysicsSprite.d.ts.map +1 -0
  16. package/dist/box2d-wasm/PhysicsSprite.js +30 -0
  17. package/dist/box2d-wasm/PhysicsSystem.d.ts +17 -0
  18. package/dist/box2d-wasm/PhysicsSystem.d.ts.map +1 -0
  19. package/dist/box2d-wasm/PhysicsSystem.js +145 -0
  20. package/dist/box2d-wasm/debugDraw.d.ts +67 -0
  21. package/dist/box2d-wasm/debugDraw.d.ts.map +1 -0
  22. package/dist/box2d-wasm/debugDraw.js +224 -0
  23. package/dist/box2d-wasm/index.d.ts +6 -0
  24. package/dist/box2d-wasm/index.d.ts.map +1 -0
  25. package/dist/box2d-wasm/index.js +17 -0
  26. package/dist/collider/CollideComponent.d.ts.map +1 -1
  27. package/dist/collider/CollideComponent.js +12 -13
  28. package/dist/collider/CollideSystem.d.ts.map +1 -1
  29. package/dist/collider/CollideSystem.js +1 -2
  30. package/dist/collider/helper/utils.d.ts +2 -0
  31. package/dist/collider/helper/utils.d.ts.map +1 -1
  32. package/dist/collider/helper/utils.js +22 -0
  33. package/dist/collider/index.d.ts +1 -0
  34. package/dist/collider/index.d.ts.map +1 -1
  35. package/dist/collider/index.js +12 -0
  36. package/dist/components/NodeComp.d.ts +6 -4
  37. package/dist/components/NodeComp.d.ts.map +1 -1
  38. package/dist/components/NodeComp.js +13 -13
  39. package/dist/components/Scene.d.ts.map +1 -1
  40. package/dist/components/Scene.js +3 -4
  41. package/dist/core/director.d.ts.map +1 -1
  42. package/dist/core/director.js +4 -3
  43. package/dist/dragonbones/DragonBonesSystem.d.ts +2 -1
  44. package/dist/dragonbones/DragonBonesSystem.d.ts.map +1 -1
  45. package/dist/dragonbones/DragonBonesSystem.js +2 -0
  46. package/dist/dragonbones/index.d.ts +1 -0
  47. package/dist/dragonbones/index.d.ts.map +1 -1
  48. package/dist/dragonbones/index.js +7 -0
  49. package/dist/gui/GUIComponent.d.ts +0 -9
  50. package/dist/gui/GUIComponent.d.ts.map +1 -1
  51. package/dist/gui/GUIComponent.js +0 -37
  52. package/dist/gui/GUISystem.d.ts +1 -0
  53. package/dist/gui/GUISystem.d.ts.map +1 -1
  54. package/dist/gui/GUISystem.js +2 -15
  55. package/dist/index.d.ts +1 -0
  56. package/dist/index.d.ts.map +1 -1
  57. package/dist/index.js +1 -0
  58. package/dist/planck/PhysicsComponent.d.ts +7 -7
  59. package/dist/planck/PhysicsComponent.js +4 -4
  60. package/dist/planck/PhysicsSystem.js +12 -12
  61. package/dist/richtext/RichTextComp.d.ts +17 -0
  62. package/dist/richtext/RichTextComp.d.ts.map +1 -0
  63. package/dist/richtext/RichTextComp.js +38 -0
  64. package/dist/richtext/RichTextSystem.d.ts +7 -0
  65. package/dist/richtext/RichTextSystem.d.ts.map +1 -0
  66. package/dist/richtext/RichTextSystem.js +25 -0
  67. package/dist/richtext/html-text-parser.d.ts +20 -0
  68. package/dist/richtext/html-text-parser.d.ts.map +1 -0
  69. package/dist/richtext/html-text-parser.js +64 -0
  70. package/dist/richtext/index.d.ts +3 -0
  71. package/dist/richtext/index.d.ts.map +1 -0
  72. package/dist/richtext/index.js +7 -0
  73. package/dist/spine/index.d.ts +1 -0
  74. package/dist/spine/index.d.ts.map +1 -1
  75. package/dist/spine/index.js +7 -0
  76. package/package.json +9 -11
  77. package/src/@types/index.d.ts +2 -0
  78. package/src/@types/safex.d.ts +15 -0
  79. package/src/app.ts +85 -0
  80. package/src/base/EnhancedComponent.ts +37 -0
  81. package/src/base/gworld.ts +19 -0
  82. package/src/base/index.ts +3 -0
  83. package/src/base/utils.ts +23 -0
  84. package/src/box2d-wasm/ContactListener.ts +66 -0
  85. package/src/box2d-wasm/PhysicsComponent.ts +83 -0
  86. package/src/box2d-wasm/PhysicsSprite.ts +42 -0
  87. package/src/box2d-wasm/PhysicsSystem.ts +145 -0
  88. package/src/box2d-wasm/debugDraw.ts +257 -0
  89. package/src/box2d-wasm/index.ts +19 -0
  90. package/src/collider/CollideComponent.ts +257 -0
  91. package/src/collider/CollideSystem.ts +166 -0
  92. package/src/collider/helper/Intersection.ts +139 -0
  93. package/src/collider/helper/utils.ts +37 -0
  94. package/src/collider/index.ts +16 -0
  95. package/src/components/BaseComponent.ts +17 -0
  96. package/src/components/NodeComp.ts +434 -0
  97. package/src/components/Scene.ts +17 -0
  98. package/src/core/Color.ts +7 -0
  99. package/src/core/LoadingBar.ts +63 -0
  100. package/src/core/NodePool.ts +28 -0
  101. package/src/core/Size.ts +21 -0
  102. package/src/core/director.ts +11 -0
  103. package/src/core/math.ts +13 -0
  104. package/src/dragonbones/DragonBonesComponent.ts +32 -0
  105. package/src/dragonbones/DragonBonesSystem.ts +35 -0
  106. package/src/dragonbones/index.ts +11 -0
  107. package/src/gui/GUIComponent.ts +159 -0
  108. package/src/gui/GUISystem.ts +116 -0
  109. package/src/helper/utils.ts +50 -0
  110. package/src/index.ts +23 -0
  111. package/src/norender/NoRenderComponent.ts +60 -0
  112. package/src/norender/NoRenderSystem.ts +66 -0
  113. package/src/planck/PhysicsComponent.ts +83 -0
  114. package/src/planck/PhysicsSprite.ts +43 -0
  115. package/src/planck/PhysicsSystem.ts +201 -0
  116. package/src/planck/index.ts +3 -0
  117. package/src/render/RenderComponent.ts +138 -0
  118. package/src/render/RenderSystem.ts +67 -0
  119. package/src/richtext/RichTextComp.ts +46 -0
  120. package/src/richtext/RichTextSystem.ts +26 -0
  121. package/src/richtext/html-text-parser.ts +87 -0
  122. package/src/richtext/index.ts +8 -0
  123. package/src/spine/SpineComponent.ts +18 -0
  124. package/src/spine/SpineSystem.ts +30 -0
  125. package/src/spine/index.ts +11 -0
  126. package/src/spine/lib/BatchableSpineSlot.ts +138 -0
  127. package/src/spine/lib/Spine.ts +910 -0
  128. package/src/spine/lib/SpineDebugRenderer.ts +615 -0
  129. package/src/spine/lib/SpinePipe.ts +203 -0
  130. package/src/spine/lib/SpineTexture.ts +143 -0
  131. package/src/spine/lib/assets/atlasLoader.ts +158 -0
  132. package/src/spine/lib/assets/skeletonLoader.ts +81 -0
  133. package/src/spine/lib/darktint/DarkTintBatchGeometry.ts +92 -0
  134. package/src/spine/lib/darktint/DarkTintBatcher.ts +186 -0
  135. package/src/spine/lib/darktint/DarkTintShader.ts +74 -0
  136. package/src/spine/lib/darktint/darkTintBit.ts +77 -0
  137. package/src/spine/lib/index.ts +43 -0
  138. package/src/spine/lib/require-shim.ts +43 -0
  139. package/tsconfig.json +18 -0
@@ -0,0 +1,910 @@
1
+ /** ****************************************************************************
2
+ * Spine Runtimes License Agreement
3
+ * Last updated July 28, 2023. Replaces all prior versions.
4
+ *
5
+ * Copyright (c) 2013-2023, Esoteric Software LLC
6
+ *
7
+ * Integration of the Spine Runtimes into software or otherwise creating
8
+ * derivative works of the Spine Runtimes is permitted under the terms and
9
+ * conditions of Section 2 of the Spine Editor License Agreement:
10
+ * http://esotericsoftware.com/spine-editor-license
11
+ *
12
+ * Otherwise, it is permitted to integrate the Spine Runtimes into software or
13
+ * otherwise create derivative works of the Spine Runtimes (collectively,
14
+ * "Products"), provided that each user of the Products must obtain their own
15
+ * Spine Editor license and redistribution of the Products in any form must
16
+ * include this license and copyright notice.
17
+ *
18
+ * THE SPINE RUNTIMES ARE PROVIDED BY ESOTERIC SOFTWARE LLC "AS IS" AND ANY
19
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
20
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
21
+ * DISCLAIMED. IN NO EVENT SHALL ESOTERIC SOFTWARE LLC BE LIABLE FOR ANY
22
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
23
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES,
24
+ * BUSINESS INTERRUPTION, OR LOSS OF USE, DATA, OR PROFITS) HOWEVER CAUSED AND
25
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
26
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THE
27
+ * SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
28
+ *****************************************************************************/
29
+
30
+ import {
31
+ AnimationState,
32
+ AnimationStateData,
33
+ AtlasAttachmentLoader,
34
+ Attachment,
35
+ Bone,
36
+ ClippingAttachment,
37
+ Color,
38
+ MeshAttachment,
39
+ Physics,
40
+ Pool,
41
+ RegionAttachment,
42
+ Skeleton,
43
+ SkeletonBinary,
44
+ SkeletonBounds,
45
+ SkeletonClipping,
46
+ SkeletonData,
47
+ SkeletonJson,
48
+ Slot,
49
+ type TextureAtlas,
50
+ TrackEntry,
51
+ Vector2,
52
+ } from '@esotericsoftware/spine-core';
53
+ import {
54
+ Assets,
55
+ Bounds,
56
+ Cache,
57
+ Container,
58
+ ContainerOptions,
59
+ DEG_TO_RAD,
60
+ DestroyOptions,
61
+ fastCopy,
62
+ Graphics,
63
+ PointData,
64
+ Texture,
65
+ Ticker,
66
+ ViewContainer,
67
+ } from 'pixi.js';
68
+ import { ISpineDebugRenderer } from './SpineDebugRenderer.js';
69
+
70
+ /**
71
+ * Options to create a {@link Spine} using {@link Spine.from}.
72
+ */
73
+ export interface SpineFromOptions {
74
+ /** the asset name for the skeleton `.skel` or `.json` file previously loaded into the Assets */
75
+ skeleton: string;
76
+
77
+ /** the asset name for the atlas file previously loaded into the Assets */
78
+ atlas: string;
79
+
80
+ /** The value passed to the skeleton reader. If omitted, 1 is passed. See {@link SkeletonBinary.scale} for details. */
81
+ scale?: number;
82
+
83
+ /** Set the {@link Spine.autoUpdate} value. If omitted, it is set to `true`. */
84
+ autoUpdate?: boolean;
85
+
86
+ /**
87
+ * If `true`, use the dark tint renderer to render the skeleton
88
+ * If `false`, use the default pixi renderer to render the skeleton
89
+ * If `undefined`, use the dark tint renderer if at least one slot has tint black
90
+ */
91
+ darkTint?: boolean;
92
+ };
93
+
94
+ const vectorAux = new Vector2();
95
+
96
+ Skeleton.yDown = true;
97
+
98
+ const clipper = new SkeletonClipping();
99
+
100
+ export interface SpineOptions extends ContainerOptions {
101
+ /** the {@link SkeletonData} used to instantiate the skeleton */
102
+ skeletonData: SkeletonData;
103
+
104
+ /** See {@link SpineFromOptions.autoUpdate}. */
105
+ autoUpdate?: boolean;
106
+
107
+ /** See {@link SpineFromOptions.darkTint}. */
108
+ darkTint?: boolean;
109
+ }
110
+
111
+ /**
112
+ * AnimationStateListener {@link https://en.esotericsoftware.com/spine-api-reference#AnimationStateListener events} exposed for Pixi.
113
+ */
114
+ export interface SpineEvents {
115
+ complete: [trackEntry: TrackEntry];
116
+ dispose: [trackEntry: TrackEntry];
117
+ end: [trackEntry: TrackEntry];
118
+ event: [trackEntry: TrackEntry, event: Event];
119
+ interrupt: [trackEntry: TrackEntry];
120
+ start: [trackEntry: TrackEntry];
121
+ }
122
+
123
+ export interface AttachmentCacheData {
124
+ id: string;
125
+ clipped: boolean;
126
+ vertices: Float32Array;
127
+ uvs: Float32Array;
128
+ indices: number[];
129
+ color: Color;
130
+ darkColor: Color;
131
+ darkTint: boolean;
132
+ skipRender: boolean;
133
+ texture: Texture;
134
+ clippedData?: {
135
+ vertices: Float32Array;
136
+ uvs: Float32Array;
137
+ indices: Uint16Array;
138
+ vertexCount: number;
139
+ indicesCount: number;
140
+ };
141
+ }
142
+
143
+ interface SlotsToClipping {
144
+ slot: Slot,
145
+ mask?: Graphics,
146
+ maskComputed?: boolean,
147
+ vertices: Array<number>,
148
+ };
149
+
150
+ const maskPool = new Pool<Graphics>(() => new Graphics);
151
+
152
+ /**
153
+ * The class to instantiate a {@link Spine} game object in Pixi.
154
+ * The static method {@link Spine.from} should be used to instantiate a Spine game object.
155
+ */
156
+ export class Spine extends ViewContainer {
157
+ // Pixi properties
158
+ public batched = true;
159
+ public buildId = 0;
160
+ public override readonly renderPipeId = 'spine';
161
+ public _didSpineUpdate = false;
162
+
163
+ public beforeUpdateWorldTransforms: (object: Spine) => void = () => { /** */ };
164
+ public afterUpdateWorldTransforms: (object: Spine) => void = () => { /** */ };
165
+
166
+ // Spine properties
167
+ /** The skeleton for this Spine game object. */
168
+ public skeleton: Skeleton;
169
+ /** The animation state for this Spine game object. */
170
+ public state: AnimationState;
171
+ public skeletonBounds?: SkeletonBounds;
172
+
173
+ private darkTint = false;
174
+ private _debug?: ISpineDebugRenderer | undefined = undefined;
175
+
176
+ readonly _slotsObject: Record<string, { slot: Slot, container: Container } | null> = Object.create(null);
177
+ private clippingSlotToPixiMasks: Record<string, SlotsToClipping> = Object.create(null);
178
+
179
+ private getSlotFromRef(slotRef: number | string | Slot): Slot {
180
+ let slot: Slot | null;
181
+
182
+ if (typeof slotRef === 'number') slot = this.skeleton.slots[slotRef];
183
+ else if (typeof slotRef === 'string') slot = this.skeleton.findSlot(slotRef);
184
+ else slot = slotRef;
185
+
186
+ if (!slot) throw new Error(`No slot found with the given slot reference: ${slotRef}`);
187
+
188
+ return slot;
189
+ }
190
+
191
+ public spineAttachmentsDirty = true;
192
+ public spineTexturesDirty = true;
193
+
194
+ private _lastAttachments: Attachment[] = [];
195
+
196
+ private _stateChanged = true;
197
+ private attachmentCacheData: Record<string, AttachmentCacheData>[] = [];
198
+
199
+ public get debug(): ISpineDebugRenderer | undefined {
200
+ return this._debug;
201
+ }
202
+
203
+ /** Pass a {@link SpineDebugRenderer} or create your own {@link ISpineDebugRenderer} to render bones, meshes, ...
204
+ * @example spineGO.debug = new SpineDebugRenderer();
205
+ */
206
+ public set debug(value: ISpineDebugRenderer | undefined) {
207
+ if (this._debug) {
208
+ this._debug.unregisterSpine(this);
209
+ }
210
+ if (value) {
211
+ value.registerSpine(this);
212
+ }
213
+ this._debug = value;
214
+ }
215
+
216
+ private _autoUpdate = true;
217
+
218
+ public get autoUpdate(): boolean {
219
+ return this._autoUpdate;
220
+ }
221
+ /** When `true`, the Spine AnimationState and the Skeleton will be automatically updated using the {@link Ticker.shared} instance. */
222
+ public set autoUpdate(value: boolean) {
223
+ if (value) {
224
+ Ticker.shared.add(this.internalUpdate, this);
225
+ } else {
226
+ Ticker.shared.remove(this.internalUpdate, this);
227
+ }
228
+
229
+ this._autoUpdate = value;
230
+ }
231
+
232
+ private hasNeverUpdated = true;
233
+ constructor(options: SpineOptions | SkeletonData) {
234
+ if (options instanceof SkeletonData) {
235
+ options = {
236
+ skeletonData: options,
237
+ };
238
+ }
239
+
240
+ super(options);
241
+
242
+ const skeletonData = options instanceof SkeletonData ? options : options.skeletonData;
243
+
244
+ this.skeleton = new Skeleton(skeletonData);
245
+ this.state = new AnimationState(new AnimationStateData(skeletonData));
246
+ this.autoUpdate = options?.autoUpdate ?? true;
247
+
248
+ // dark tint can be enabled by options, otherwise is enable if at least one slot has tint black
249
+ this.darkTint = options?.darkTint === undefined
250
+ ? this.skeleton.slots.some(slot => !!slot.data.darkColor)
251
+ : options?.darkTint;
252
+
253
+ const slots = this.skeleton.slots;
254
+
255
+ for (let i = 0; i < slots.length; i++) {
256
+ this.attachmentCacheData[i] = Object.create(null);
257
+ }
258
+ }
259
+
260
+ /** If {@link Spine.autoUpdate} is `false`, this method allows to update the AnimationState and the Skeleton with the given delta. */
261
+ public update(dt: number): void {
262
+ this.internalUpdate(0, dt);
263
+ }
264
+
265
+ protected internalUpdate(_deltaFrame: any, deltaSeconds?: number): void {
266
+ // Because reasons, pixi uses deltaFrames at 60fps.
267
+ // We ignore the default deltaFrames and use the deltaSeconds from pixi ticker.
268
+ this._updateAndApplyState(deltaSeconds ?? Ticker.shared.deltaMS / 1000);
269
+ }
270
+
271
+ override get bounds() {
272
+ if (this._boundsDirty) {
273
+ this.updateBounds();
274
+ }
275
+
276
+ return this._bounds;
277
+ }
278
+
279
+ /**
280
+ * Set the position of the bone given in input through a {@link IPointData}.
281
+ * @param bone: the bone name or the bone instance to set the position
282
+ * @param outPos: the new position of the bone.
283
+ * @throws {Error}: if the given bone is not found in the skeleton, an error is thrown
284
+ */
285
+ public setBonePosition(bone: string | Bone, position: PointData): void {
286
+ const boneAux = bone;
287
+
288
+ if (typeof bone === 'string') {
289
+ bone = this.skeleton.findBone(bone) as Bone;
290
+ }
291
+
292
+ if (!bone) throw Error(`Cant set bone position, bone ${String(boneAux)} not found`);
293
+ vectorAux.set(position.x, position.y);
294
+
295
+ if (bone.parent) {
296
+ const aux = bone.parent.worldToLocal(vectorAux);
297
+
298
+ bone.x = aux.x;
299
+ bone.y = -aux.y;
300
+ }
301
+ else {
302
+ bone.x = vectorAux.x;
303
+ bone.y = vectorAux.y;
304
+ }
305
+ }
306
+
307
+ /**
308
+ * Return the position of the bone given in input into an {@link IPointData}.
309
+ * @param bone: the bone name or the bone instance to get the position from
310
+ * @param outPos: an optional {@link IPointData} to use to return the bone position, rathern than instantiating a new object.
311
+ * @returns {IPointData | undefined}: the position of the bone, or undefined if no matching bone is found in the skeleton
312
+ */
313
+ public getBonePosition(bone: string | Bone, outPos?: PointData): PointData | undefined {
314
+ const boneAux = bone;
315
+
316
+ if (typeof bone === 'string') {
317
+ bone = this.skeleton.findBone(bone) as Bone;
318
+ }
319
+
320
+ if (!bone) {
321
+ console.error(`Cant set bone position! Bone ${String(boneAux)} not found`);
322
+
323
+ return outPos;
324
+ }
325
+
326
+ if (!outPos) {
327
+ outPos = { x: 0, y: 0 };
328
+ }
329
+
330
+ outPos.x = bone.worldX;
331
+ outPos.y = bone.worldY;
332
+
333
+ return outPos;
334
+ }
335
+
336
+ /**
337
+ * Advance the state and skeleton by the given time, then update slot objects too.
338
+ * The container transform is not updated.
339
+ *
340
+ * @param time the time at which to set the state
341
+ */
342
+ private _updateAndApplyState(time: number) {
343
+ this.hasNeverUpdated = false;
344
+
345
+ this.state.update(time);
346
+ this.skeleton.update(time);
347
+
348
+ const { skeleton } = this;
349
+
350
+ this.state.apply(skeleton);
351
+
352
+ this.beforeUpdateWorldTransforms(this);
353
+ skeleton.updateWorldTransform(Physics.update);
354
+ this.afterUpdateWorldTransforms(this);
355
+
356
+ this.updateSlotObjects();
357
+
358
+ this._stateChanged = true;
359
+
360
+ this._boundsDirty = true;
361
+
362
+ this.onViewUpdate();
363
+ }
364
+
365
+ /**
366
+ * - validates the attachments - to flag if the attachments have changed this state
367
+ * - transforms the attachments - to update the vertices of the attachments based on the new positions
368
+ * @internal
369
+ */
370
+ _validateAndTransformAttachments() {
371
+ if (!this._stateChanged) return;
372
+ this._stateChanged = false;
373
+
374
+ this.validateAttachments();
375
+
376
+ this.transformAttachments();
377
+ }
378
+
379
+ private validateAttachments() {
380
+
381
+ const currentDrawOrder = this.skeleton.drawOrder;
382
+
383
+ const lastAttachments = this._lastAttachments;
384
+
385
+ let index = 0;
386
+
387
+ let spineAttachmentsDirty = false;
388
+
389
+ for (let i = 0; i < currentDrawOrder.length; i++) {
390
+ const slot = currentDrawOrder[i];
391
+ const attachment = slot.getAttachment();
392
+
393
+ if (attachment) {
394
+ if (attachment !== lastAttachments[index]) {
395
+ spineAttachmentsDirty = true;
396
+ lastAttachments[index] = attachment;
397
+ }
398
+
399
+ index++;
400
+ }
401
+ }
402
+
403
+ if (index !== lastAttachments.length) {
404
+ spineAttachmentsDirty = true;
405
+ lastAttachments.length = index;
406
+ }
407
+
408
+ this.spineAttachmentsDirty ||= spineAttachmentsDirty;
409
+ }
410
+
411
+ private updateAndSetPixiMask(slot: Slot, last: boolean) {
412
+ // assign/create the currentClippingSlot
413
+ const attachment = slot.attachment;
414
+ if (attachment && attachment instanceof ClippingAttachment) {
415
+ const clip = (this.clippingSlotToPixiMasks[slot.data.name] ||= { slot, vertices: new Array<number>() });
416
+ clip.maskComputed = false;
417
+ this.currentClippingSlot = this.clippingSlotToPixiMasks[slot.data.name];
418
+ return;
419
+ }
420
+
421
+ // assign the currentClippingSlot mask to the slot object
422
+ let currentClippingSlot = this.currentClippingSlot;
423
+ let slotObject = this._slotsObject[slot.data.name];
424
+ if (currentClippingSlot && slotObject) {
425
+ let slotClipping = currentClippingSlot.slot;
426
+ let clippingAttachment = slotClipping.attachment as ClippingAttachment;
427
+
428
+ // create the pixi mask, only the first time and if the clipped slot is the first one clipped by this currentClippingSlot
429
+ let mask = currentClippingSlot.mask as Graphics;
430
+ if (!mask) {
431
+ mask = maskPool.obtain();
432
+ currentClippingSlot.mask = mask;
433
+ this.addChild(mask);
434
+ }
435
+
436
+ // compute the pixi mask polygon, if the clipped slot is the first one clipped by this currentClippingSlot
437
+ if (!currentClippingSlot.maskComputed) {
438
+ currentClippingSlot.maskComputed = true;
439
+ const worldVerticesLength = clippingAttachment.worldVerticesLength;
440
+ const vertices = currentClippingSlot.vertices;
441
+ clippingAttachment.computeWorldVertices(slotClipping, 0, worldVerticesLength, vertices, 0, 2);
442
+ mask.clear().poly(vertices).stroke({ width: 0 }).fill({ alpha: .25 });
443
+ }
444
+ slotObject.container.mask = mask;
445
+ } else if (slotObject?.container.mask) {
446
+ // remove the mask, if slot object has a mask, but currentClippingSlot is undefined
447
+ slotObject.container.mask = null;
448
+ }
449
+
450
+ // if current slot is the ending one of the currentClippingSlot mask, set currentClippingSlot to undefined
451
+ if (currentClippingSlot && (currentClippingSlot.slot.attachment as ClippingAttachment).endSlot == slot.data) {
452
+ this.currentClippingSlot = undefined;
453
+ }
454
+
455
+ // clean up unused masks
456
+ if (last) {
457
+ for (const key in this.clippingSlotToPixiMasks) {
458
+ const clippingSlotToPixiMask = this.clippingSlotToPixiMasks[key];
459
+ if ((!(clippingSlotToPixiMask.slot.attachment instanceof ClippingAttachment) || !clippingSlotToPixiMask.maskComputed) && clippingSlotToPixiMask.mask) {
460
+ this.removeChild(clippingSlotToPixiMask.mask);
461
+ maskPool.free(clippingSlotToPixiMask.mask);
462
+ clippingSlotToPixiMask.mask = undefined;
463
+ }
464
+ }
465
+ }
466
+ }
467
+
468
+ private currentClippingSlot: SlotsToClipping | undefined;
469
+ private transformAttachments() {
470
+ const currentDrawOrder = this.skeleton.drawOrder;
471
+
472
+ for (let i = 0; i < currentDrawOrder.length; i++) {
473
+ const slot = currentDrawOrder[i];
474
+
475
+ this.updateAndSetPixiMask(slot, i === currentDrawOrder.length - 1);
476
+
477
+ const attachment = slot.getAttachment();
478
+
479
+ if (attachment) {
480
+ if (attachment instanceof MeshAttachment || attachment instanceof RegionAttachment) {
481
+ const cacheData = this._getCachedData(slot, attachment);
482
+
483
+ if (attachment instanceof RegionAttachment) {
484
+ attachment.computeWorldVertices(slot, cacheData.vertices, 0, 2);
485
+ }
486
+ else {
487
+ attachment.computeWorldVertices(
488
+ slot,
489
+ 0,
490
+ attachment.worldVerticesLength,
491
+ cacheData.vertices,
492
+ 0,
493
+ 2,
494
+ );
495
+ }
496
+
497
+ // sequences uvs are known only after computeWorldVertices is invoked
498
+ if (cacheData.uvs.length < attachment.uvs.length) {
499
+ cacheData.uvs = new Float32Array(attachment.uvs.length);
500
+ }
501
+
502
+ // need to copy because attachments uvs are shared among skeletons using the same atlas
503
+ fastCopy((attachment.uvs as Float32Array).buffer, cacheData.uvs.buffer);
504
+
505
+ const skeleton = slot.bone.skeleton;
506
+ const skeletonColor = skeleton.color;
507
+ const slotColor = slot.color;
508
+
509
+ const attachmentColor = attachment.color;
510
+
511
+ cacheData.color.set(
512
+ skeletonColor.r * slotColor.r * attachmentColor.r,
513
+ skeletonColor.g * slotColor.g * attachmentColor.g,
514
+ skeletonColor.b * slotColor.b * attachmentColor.b,
515
+ skeletonColor.a * slotColor.a * attachmentColor.a,
516
+ );
517
+
518
+ if (slot.darkColor) {
519
+ cacheData.darkColor.setFromColor(slot.darkColor);
520
+ }
521
+
522
+ cacheData.skipRender = cacheData.clipped = false;
523
+
524
+ const texture = attachment.region?.texture.texture || Texture.EMPTY;
525
+
526
+ if (cacheData.texture !== texture) {
527
+ cacheData.texture = texture;
528
+ this.spineTexturesDirty = true;
529
+ }
530
+
531
+ if (clipper.isClipping()) {
532
+ this.updateClippingData(cacheData);
533
+ }
534
+ }
535
+ else if (attachment instanceof ClippingAttachment) {
536
+ clipper.clipStart(slot, attachment);
537
+ continue;
538
+ }
539
+ }
540
+ clipper.clipEndWithSlot(slot);
541
+ }
542
+ clipper.clipEnd();
543
+ }
544
+
545
+ private updateClippingData(cacheData: AttachmentCacheData) {
546
+ cacheData.clipped = true;
547
+
548
+ clipper.clipTrianglesUnpacked(
549
+ cacheData.vertices,
550
+ cacheData.indices,
551
+ cacheData.indices.length,
552
+ cacheData.uvs,
553
+ );
554
+
555
+ const { clippedVertices, clippedUVs, clippedTriangles } = clipper;
556
+
557
+ const verticesCount = clippedVertices.length / 2;
558
+ const indicesCount = clippedTriangles.length;
559
+
560
+ if (!cacheData.clippedData) {
561
+ cacheData.clippedData = {
562
+ vertices: new Float32Array(verticesCount * 2),
563
+ uvs: new Float32Array(verticesCount * 2),
564
+ vertexCount: verticesCount,
565
+ indices: new Uint16Array(indicesCount),
566
+ indicesCount,
567
+ };
568
+
569
+ this.spineAttachmentsDirty = true;
570
+ }
571
+
572
+ const clippedData = cacheData.clippedData;
573
+
574
+ const sizeChange = clippedData.vertexCount !== verticesCount || indicesCount !== clippedData.indicesCount;
575
+
576
+ cacheData.skipRender = verticesCount === 0;
577
+
578
+ if (sizeChange) {
579
+ this.spineAttachmentsDirty = true;
580
+
581
+ if (clippedData.vertexCount < verticesCount) {
582
+ // buffer reuse!
583
+ clippedData.vertices = new Float32Array(verticesCount * 2);
584
+ clippedData.uvs = new Float32Array(verticesCount * 2);
585
+ }
586
+
587
+ if (clippedData.indices.length < indicesCount) {
588
+ clippedData.indices = new Uint16Array(indicesCount);
589
+ }
590
+ }
591
+
592
+ const { vertices, uvs, indices } = clippedData;
593
+
594
+ for (let i = 0; i < verticesCount; i++) {
595
+ vertices[i * 2] = clippedVertices[i * 2];
596
+ vertices[(i * 2) + 1] = clippedVertices[(i * 2) + 1];
597
+
598
+ uvs[i * 2] = clippedUVs[(i * 2)];
599
+ uvs[(i * 2) + 1] = clippedUVs[(i * 2) + 1];
600
+ }
601
+
602
+ clippedData.vertexCount = verticesCount;
603
+
604
+ for (let i = 0; i < indicesCount; i++) {
605
+ if (indices[i] !== clippedTriangles[i]) {
606
+ this.spineAttachmentsDirty = true;
607
+ indices[i] = clippedTriangles[i];
608
+ }
609
+ }
610
+
611
+ clippedData.indicesCount = indicesCount;
612
+ }
613
+
614
+ /**
615
+ * ensure that attached containers map correctly to their slots
616
+ * along with their position, rotation, scale, and visibility.
617
+ */
618
+ private updateSlotObjects() {
619
+ for (const i in this._slotsObject) {
620
+ const slotAttachment = this._slotsObject[i];
621
+
622
+ if (!slotAttachment) continue;
623
+
624
+ this.updateSlotObject(slotAttachment);
625
+ }
626
+ }
627
+
628
+ private updateSlotObject(slotAttachment: { slot: Slot, container: Container }) {
629
+ const { slot, container } = slotAttachment;
630
+
631
+ container.visible = this.skeleton.drawOrder.includes(slot);
632
+
633
+ if (container.visible) {
634
+ const bone = slot.bone;
635
+
636
+ container.position.set(bone.worldX, bone.worldY);
637
+
638
+ container.scale.x = bone.getWorldScaleX();
639
+ container.scale.y = bone.getWorldScaleY();
640
+
641
+ container.rotation = bone.getWorldRotationX() * DEG_TO_RAD;
642
+
643
+ container.alpha = this.skeleton.color.a * slot.color.a;
644
+ }
645
+ }
646
+
647
+ /** @internal */
648
+ _getCachedData(slot: Slot, attachment: RegionAttachment | MeshAttachment): AttachmentCacheData {
649
+ return this.attachmentCacheData[slot.data.index][attachment.name] || this.initCachedData(slot, attachment);
650
+ }
651
+
652
+ private initCachedData(slot: Slot, attachment: RegionAttachment | MeshAttachment): AttachmentCacheData {
653
+ let vertices: Float32Array;
654
+
655
+ if (attachment instanceof RegionAttachment) {
656
+ vertices = new Float32Array(8);
657
+
658
+ this.attachmentCacheData[slot.data.index][attachment.name] = {
659
+ id: `${slot.data.index}-${attachment.name}`,
660
+ vertices,
661
+ clipped: false,
662
+ indices: [0, 1, 2, 0, 2, 3],
663
+ uvs: new Float32Array(attachment.uvs.length),
664
+ color: new Color(1, 1, 1, 1),
665
+ darkColor: new Color(0, 0, 0, 0),
666
+ darkTint: this.darkTint,
667
+ skipRender: false,
668
+ texture: attachment.region?.texture.texture,
669
+ };
670
+ }
671
+ else {
672
+ vertices = new Float32Array(attachment.worldVerticesLength);
673
+
674
+ this.attachmentCacheData[slot.data.index][attachment.name] = {
675
+ id: `${slot.data.index}-${attachment.name}`,
676
+ vertices,
677
+ clipped: false,
678
+ indices: attachment.triangles,
679
+ uvs: new Float32Array(attachment.uvs.length),
680
+ color: new Color(1, 1, 1, 1),
681
+ darkColor: new Color(0, 0, 0, 0),
682
+ darkTint: this.darkTint,
683
+ skipRender: false,
684
+ texture: attachment.region?.texture.texture,
685
+ };
686
+ }
687
+
688
+ return this.attachmentCacheData[slot.data.index][attachment.name];
689
+ }
690
+
691
+ protected onViewUpdate() {
692
+ // increment from the 12th bit!
693
+ this._didViewChangeTick++;
694
+ this._boundsDirty = true;
695
+
696
+ if (this.didViewUpdate) return;
697
+ this.didViewUpdate = true;
698
+
699
+ const renderGroup = this.renderGroup || this.parentRenderGroup;
700
+
701
+ if (renderGroup) {
702
+ renderGroup.onChildViewUpdate(this);
703
+ }
704
+
705
+ this.debug?.renderDebug(this);
706
+ }
707
+
708
+ /**
709
+ * Attaches a PixiJS container to a specified slot. This will map the world transform of the slots bone
710
+ * to the attached container. A container can only be attached to one slot at a time.
711
+ *
712
+ * @param container - The container to attach to the slot
713
+ * @param slotRef - The slot id or slot to attach to
714
+ */
715
+ public addSlotObject(slot: number | string | Slot, container: Container) {
716
+ slot = this.getSlotFromRef(slot);
717
+
718
+ // need to check in on the container too...
719
+ for (const i in this._slotsObject) {
720
+ if (this._slotsObject[i]?.container === container) {
721
+ this.removeSlotObject(this._slotsObject[i].slot);
722
+ }
723
+ }
724
+
725
+ this.removeSlotObject(slot);
726
+
727
+ container.includeInBuild = false;
728
+
729
+ // TODO only add once??
730
+ this.addChild(container);
731
+
732
+ const slotObject = { container, slot };
733
+ this._slotsObject[slot.data.name] = slotObject;
734
+
735
+ this.updateSlotObject(slotObject);
736
+ }
737
+
738
+ /**
739
+ * Removes a PixiJS container from the slot it is attached to.
740
+ *
741
+ * @param container - The container to detach from the slot
742
+ * @param slotOrContainer - The container, slot id or slot to detach from
743
+ */
744
+ public removeSlotObject(slotOrContainer: number | string | Slot | Container) {
745
+ let containerToRemove: Container | undefined;
746
+
747
+ if (slotOrContainer instanceof Container) {
748
+ for (const i in this._slotsObject) {
749
+ if (this._slotsObject[i]?.container === slotOrContainer) {
750
+ this._slotsObject[i] = null;
751
+
752
+ containerToRemove = slotOrContainer;
753
+ break;
754
+ }
755
+ }
756
+ }
757
+ else {
758
+ const slot = this.getSlotFromRef(slotOrContainer);
759
+
760
+ containerToRemove = this._slotsObject[slot.data.name]?.container;
761
+ this._slotsObject[slot.data.name] = null;
762
+ }
763
+
764
+ if (containerToRemove) {
765
+ this.removeChild(containerToRemove);
766
+
767
+ containerToRemove.includeInBuild = true;
768
+ }
769
+ }
770
+
771
+ /**
772
+ * Returns a container attached to a slot, or undefined if no container is attached.
773
+ *
774
+ * @param slotRef - The slot id or slot to get the attachment from
775
+ * @returns - The container attached to the slot
776
+ */
777
+ public getSlotObject(slot: number | string | Slot) {
778
+ slot = this.getSlotFromRef(slot);
779
+
780
+ return this._slotsObject[slot.data.name]?.container;
781
+ }
782
+
783
+ protected updateBounds() {
784
+ this._boundsDirty = false;
785
+
786
+ this.skeletonBounds ||= new SkeletonBounds();
787
+
788
+ const skeletonBounds = this.skeletonBounds;
789
+
790
+ skeletonBounds.update(this.skeleton, true);
791
+
792
+ if (skeletonBounds.minX === Infinity) {
793
+ if (this.hasNeverUpdated) {
794
+ this._updateAndApplyState(0);
795
+ this._boundsDirty = false;
796
+ }
797
+ this._validateAndTransformAttachments();
798
+
799
+ const drawOrder = this.skeleton.drawOrder;
800
+ const bounds = this._bounds;
801
+
802
+ bounds.clear();
803
+
804
+ for (let i = 0; i < drawOrder.length; i++) {
805
+ const slot = drawOrder[i];
806
+
807
+ const attachment = slot.getAttachment();
808
+
809
+ if (attachment && (attachment instanceof RegionAttachment || attachment instanceof MeshAttachment)) {
810
+ const cacheData = this._getCachedData(slot, attachment);
811
+
812
+ bounds.addVertexData(cacheData.vertices, 0, cacheData.vertices.length);
813
+ }
814
+ }
815
+ }
816
+ else {
817
+ this._bounds.minX = skeletonBounds.minX;
818
+ this._bounds.minY = skeletonBounds.minY;
819
+ this._bounds.maxX = skeletonBounds.maxX;
820
+ this._bounds.maxY = skeletonBounds.maxY;
821
+ }
822
+ }
823
+
824
+ /** @internal */
825
+ addBounds(bounds: Bounds) {
826
+ bounds.addBounds(this.bounds);
827
+ }
828
+
829
+ /**
830
+ * Destroys this sprite renderable and optionally its texture.
831
+ * @param options - Options parameter. A boolean will act as if all options
832
+ * have been set to that value
833
+ * @param {boolean} [options.texture=false] - Should it destroy the current texture of the renderable as well
834
+ * @param {boolean} [options.textureSource=false] - Should it destroy the textureSource of the renderable as well
835
+ */
836
+ public override destroy(options: DestroyOptions = false) {
837
+ super.destroy(options);
838
+
839
+ Ticker.shared.remove(this.internalUpdate, this);
840
+ this.state.clearListeners();
841
+ this.debug = undefined;
842
+ this.skeleton = null as any;
843
+ this.state = null as any;
844
+ (this._slotsObject as any) = null;
845
+ this._lastAttachments.length = 0;
846
+ this.attachmentCacheData = null as any;
847
+ }
848
+
849
+ /** Converts a point from the skeleton coordinate system to the Pixi world coordinate system. */
850
+ public skeletonToPixiWorldCoordinates(point: { x: number; y: number }) {
851
+ this.worldTransform.apply(point, point);
852
+ }
853
+
854
+ /** Converts a point from the Pixi world coordinate system to the skeleton coordinate system. */
855
+ public pixiWorldCoordinatesToSkeleton(point: { x: number; y: number }) {
856
+ this.worldTransform.applyInverse(point, point);
857
+ }
858
+
859
+ /** Converts a point from the Pixi world coordinate system to the bone's local coordinate system. */
860
+ public pixiWorldCoordinatesToBone(point: { x: number; y: number }, bone: Bone) {
861
+ this.pixiWorldCoordinatesToSkeleton(point);
862
+ if (bone.parent) {
863
+ bone.parent.worldToLocal(point as Vector2);
864
+ }
865
+ else {
866
+ bone.worldToLocal(point as Vector2);
867
+ }
868
+ }
869
+
870
+ /**
871
+ * Use this method to instantiate a Spine game object.
872
+ * Before instantiating a Spine game object, the skeleton (`.skel` or `.json`) and the atlas text files must be loaded into the Assets. For example:
873
+ * ```
874
+ * PIXI.Assets.add("sackData", "./assets/sack-pro.skel");
875
+ * PIXI.Assets.add("sackAtlas", "./assets/sack-pma.atlas");
876
+ * await PIXI.Assets.load(["sackData", "sackAtlas"]);
877
+ * ```
878
+ * Once a Spine game object is created, its skeleton data is cached into {@link Cache} using the key:
879
+ * `${skeletonAssetName}-${atlasAssetName}-${options?.scale ?? 1}`
880
+ *
881
+ * @param options - Options to configure the Spine game object. See {@link SpineFromOptions}
882
+ * @returns {Spine} The Spine game object instantiated
883
+ */
884
+ static from({ skeleton, atlas, scale = 1, darkTint, autoUpdate = true }: SpineFromOptions) {
885
+ const cacheKey = `${skeleton}-${atlas}-${scale}`;
886
+
887
+ if (Cache.has(cacheKey)) {
888
+ return new Spine(Cache.get<SkeletonData>(cacheKey));
889
+ }
890
+
891
+ const skeletonAsset = Assets.get<any | Uint8Array>(skeleton);
892
+
893
+ const atlasAsset = Assets.get<TextureAtlas>(atlas);
894
+ const attachmentLoader = new AtlasAttachmentLoader(atlasAsset);
895
+ const parser = skeletonAsset instanceof Uint8Array
896
+ ? new SkeletonBinary(attachmentLoader)
897
+ : new SkeletonJson(attachmentLoader);
898
+
899
+ parser.scale = scale;
900
+ const skeletonData = parser.readSkeletonData(skeletonAsset);
901
+
902
+ Cache.set(cacheKey, skeletonData);
903
+
904
+ return new Spine({
905
+ skeletonData,
906
+ darkTint,
907
+ autoUpdate,
908
+ });
909
+ }
910
+ }