@@ -61,6 +61,7 @@ import {
61
61
SkeletonClipping ,
62
62
SkeletonData ,
63
63
SkeletonJson ,
64
+ Skin ,
64
65
Slot ,
65
66
type TextureAtlas ,
66
67
TrackEntry ,
@@ -89,6 +90,9 @@ export interface SpineFromOptions {
89
90
* If `undefined`, use the dark tint renderer if at least one slot has tint black
90
91
*/
91
92
darkTint ?: boolean ;
93
+
94
+ /** The bounds provider to use. If undefined the bounds will be dynamic, calculated when requested and based on the current frame. */
95
+ boundsProvider ?: SpineBoundsProvider ,
92
96
} ;
93
97
94
98
const vectorAux = new Vector2 ( ) ;
@@ -97,6 +101,138 @@ Skeleton.yDown = true;
97
101
98
102
const clipper = new SkeletonClipping ( ) ;
99
103
104
+ /** A bounds provider calculates the bounding box for a skeleton, which is then assigned as the size of the SpineGameObject. */
105
+ export interface SpineBoundsProvider {
106
+ /** Returns the bounding box for the skeleton, in skeleton space. */
107
+ calculateBounds ( gameObject : Spine ) : {
108
+ x : number ;
109
+ y : number ;
110
+ width : number ;
111
+ height : number ;
112
+ } ;
113
+ }
114
+
115
+ /** A bounds provider that provides a fixed size given by the user. */
116
+ export class AABBRectangleBoundsProvider implements SpineBoundsProvider {
117
+ constructor (
118
+ private x : number ,
119
+ private y : number ,
120
+ private width : number ,
121
+ private height : number ,
122
+ ) { }
123
+ calculateBounds ( ) {
124
+ return { x : this . x , y : this . y , width : this . width , height : this . height } ;
125
+ }
126
+ }
127
+
128
+ /** A bounds provider that calculates the bounding box from the setup pose. */
129
+ export class SetupPoseBoundsProvider implements SpineBoundsProvider {
130
+ /**
131
+ * @param clipping If true, clipping attachments are used to compute the bounds. False, by default.
132
+ */
133
+ constructor (
134
+ private clipping = false ,
135
+ ) { }
136
+
137
+ calculateBounds ( gameObject : Spine ) {
138
+ if ( ! gameObject . skeleton ) return { x : 0 , y : 0 , width : 0 , height : 0 } ;
139
+ // Make a copy of animation state and skeleton as this might be called while
140
+ // the skeleton in the GameObject has already been heavily modified. We can not
141
+ // reconstruct that state.
142
+ const skeleton = new Skeleton ( gameObject . skeleton . data ) ;
143
+ skeleton . setToSetupPose ( ) ;
144
+ skeleton . updateWorldTransform ( Physics . update ) ;
145
+ const bounds = skeleton . getBoundsRect ( this . clipping ? new SkeletonClipping ( ) : undefined ) ;
146
+ return bounds . width == Number . NEGATIVE_INFINITY
147
+ ? { x : 0 , y : 0 , width : 0 , height : 0 }
148
+ : bounds ;
149
+ }
150
+ }
151
+
152
+ /** A bounds provider that calculates the bounding box by taking the maximumg bounding box for a combination of skins and specific animation. */
153
+ export class SkinsAndAnimationBoundsProvider
154
+ implements SpineBoundsProvider {
155
+ /**
156
+ * @param animation The animation to use for calculating the bounds. If null, the setup pose is used.
157
+ * @param skins The skins to use for calculating the bounds. If empty, the default skin is used.
158
+ * @param timeStep The time step to use for calculating the bounds. A smaller time step means more precision, but slower calculation.
159
+ * @param clipping If true, clipping attachments are used to compute the bounds. False, by default.
160
+ */
161
+ constructor (
162
+ private animation : string | null ,
163
+ private skins : string [ ] = [ ] ,
164
+ private timeStep : number = 0.05 ,
165
+ private clipping = false ,
166
+ ) { }
167
+
168
+ calculateBounds ( gameObject : Spine ) : {
169
+ x : number ;
170
+ y : number ;
171
+ width : number ;
172
+ height : number ;
173
+ } {
174
+ if ( ! gameObject . skeleton || ! gameObject . state )
175
+ return { x : 0 , y : 0 , width : 0 , height : 0 } ;
176
+ // Make a copy of animation state and skeleton as this might be called while
177
+ // the skeleton in the GameObject has already been heavily modified. We can not
178
+ // reconstruct that state.
179
+ const animationState = new AnimationState ( gameObject . state . data ) ;
180
+ const skeleton = new Skeleton ( gameObject . skeleton . data ) ;
181
+ const clipper = this . clipping ? new SkeletonClipping ( ) : undefined ;
182
+ const data = skeleton . data ;
183
+ if ( this . skins . length > 0 ) {
184
+ let customSkin = new Skin ( "custom-skin" ) ;
185
+ for ( const skinName of this . skins ) {
186
+ const skin = data . findSkin ( skinName ) ;
187
+ if ( skin == null ) continue ;
188
+ customSkin . addSkin ( skin ) ;
189
+ }
190
+ skeleton . setSkin ( customSkin ) ;
191
+ }
192
+ skeleton . setToSetupPose ( ) ;
193
+
194
+ const animation = this . animation != null ? data . findAnimation ( this . animation ! ) : null ;
195
+
196
+ if ( animation == null ) {
197
+ skeleton . updateWorldTransform ( Physics . update ) ;
198
+ const bounds = skeleton . getBoundsRect ( clipper ) ;
199
+ return bounds . width == Number . NEGATIVE_INFINITY
200
+ ? { x : 0 , y : 0 , width : 0 , height : 0 }
201
+ : bounds ;
202
+ } else {
203
+ let minX = Number . POSITIVE_INFINITY ,
204
+ minY = Number . POSITIVE_INFINITY ,
205
+ maxX = Number . NEGATIVE_INFINITY ,
206
+ maxY = Number . NEGATIVE_INFINITY ;
207
+ animationState . clearTracks ( ) ;
208
+ animationState . setAnimationWith ( 0 , animation , false ) ;
209
+ const steps = Math . max ( animation . duration / this . timeStep , 1.0 ) ;
210
+ for ( let i = 0 ; i < steps ; i ++ ) {
211
+ const delta = i > 0 ? this . timeStep : 0 ;
212
+ animationState . update ( delta ) ;
213
+ animationState . apply ( skeleton ) ;
214
+ skeleton . update ( delta ) ;
215
+ skeleton . updateWorldTransform ( Physics . update ) ;
216
+
217
+ const bounds = skeleton . getBoundsRect ( clipper ) ;
218
+ minX = Math . min ( minX , bounds . x ) ;
219
+ minY = Math . min ( minY , bounds . y ) ;
220
+ maxX = Math . max ( maxX , bounds . x + bounds . width ) ;
221
+ maxY = Math . max ( maxY , bounds . y + bounds . height ) ;
222
+ }
223
+ const bounds = {
224
+ x : minX ,
225
+ y : minY ,
226
+ width : maxX - minX ,
227
+ height : maxY - minY ,
228
+ } ;
229
+ return bounds . width == Number . NEGATIVE_INFINITY
230
+ ? { x : 0 , y : 0 , width : 0 , height : 0 }
231
+ : bounds ;
232
+ }
233
+ }
234
+ }
235
+
100
236
export interface SpineOptions extends ContainerOptions {
101
237
/** the {@link SkeletonData} used to instantiate the skeleton */
102
238
skeletonData : SkeletonData ;
@@ -106,6 +242,9 @@ export interface SpineOptions extends ContainerOptions {
106
242
107
243
/** See {@link SpineFromOptions.darkTint}. */
108
244
darkTint ?: boolean ;
245
+
246
+ /** See {@link SpineFromOptions.boundsProvider}. */
247
+ boundsProvider ?: SpineBoundsProvider ,
109
248
}
110
249
111
250
/**
@@ -229,6 +368,19 @@ export class Spine extends ViewContainer {
229
368
this . _autoUpdate = value ;
230
369
}
231
370
371
+ public _boundsProvider ?: SpineBoundsProvider ;
372
+ /** The bounds provider to use. If undefined the bounds will be dynamic, calculated when requested and based on the current frame. */
373
+ public get boundsProvider ( ) : SpineBoundsProvider | undefined {
374
+ return this . _boundsProvider ;
375
+ }
376
+ public set boundsProvider ( value : SpineBoundsProvider | undefined ) {
377
+ this . _boundsProvider = value ;
378
+ if ( value ) {
379
+ this . _boundsDirty = false ;
380
+ }
381
+ this . updateBounds ( ) ;
382
+ }
383
+
232
384
private hasNeverUpdated = true ;
233
385
constructor ( options : SpineOptions | SkeletonData ) {
234
386
if ( options instanceof SkeletonData ) {
@@ -255,6 +407,8 @@ export class Spine extends ViewContainer {
255
407
for ( let i = 0 ; i < slots . length ; i ++ ) {
256
408
this . attachmentCacheData [ i ] = Object . create ( null ) ;
257
409
}
410
+
411
+ this . _boundsProvider = options . boundsProvider ;
258
412
}
259
413
260
414
/** If {@link Spine.autoUpdate} is `false`, this method allows to update the AnimationState and the Skeleton with the given delta. */
@@ -357,8 +511,6 @@ export class Spine extends ViewContainer {
357
511
358
512
this . _stateChanged = true ;
359
513
360
- this . _boundsDirty = true ;
361
-
362
514
this . onViewUpdate ( ) ;
363
515
}
364
516
@@ -692,7 +844,9 @@ export class Spine extends ViewContainer {
692
844
protected onViewUpdate ( ) {
693
845
// increment from the 12th bit!
694
846
this . _didViewChangeTick ++ ;
695
- this . _boundsDirty = true ;
847
+ if ( ! this . _boundsProvider ) {
848
+ this . _boundsDirty = true ;
849
+ }
696
850
697
851
if ( this . didViewUpdate ) return ;
698
852
this . didViewUpdate = true ;
@@ -806,7 +960,18 @@ export class Spine extends ViewContainer {
806
960
807
961
skeletonBounds . update ( this . skeleton , true ) ;
808
962
809
- if ( skeletonBounds . minX === Infinity ) {
963
+ if ( this . _boundsProvider ) {
964
+ const boundsSpine = this . _boundsProvider . calculateBounds ( this ) ;
965
+
966
+ const bounds = this . _bounds ;
967
+ bounds . clear ( ) ;
968
+
969
+ bounds . x = boundsSpine . x ;
970
+ bounds . y = boundsSpine . y ;
971
+ bounds . width = boundsSpine . width ;
972
+ bounds . height = boundsSpine . height ;
973
+
974
+ } else if ( skeletonBounds . minX === Infinity ) {
810
975
if ( this . hasNeverUpdated ) {
811
976
this . _updateAndApplyState ( 0 ) ;
812
977
this . _boundsDirty = false ;
@@ -898,11 +1063,16 @@ export class Spine extends ViewContainer {
898
1063
* @param options - Options to configure the Spine game object. See {@link SpineFromOptions}
899
1064
* @returns {Spine } The Spine game object instantiated
900
1065
*/
901
- static from ( { skeleton, atlas, scale = 1 , darkTint, autoUpdate = true } : SpineFromOptions ) {
1066
+ static from ( { skeleton, atlas, scale = 1 , darkTint, autoUpdate = true , boundsProvider } : SpineFromOptions ) {
902
1067
const cacheKey = `${ skeleton } -${ atlas } -${ scale } ` ;
903
1068
904
1069
if ( Cache . has ( cacheKey ) ) {
905
- return new Spine ( Cache . get < SkeletonData > ( cacheKey ) ) ;
1070
+ return new Spine ( {
1071
+ skeletonData : Cache . get < SkeletonData > ( cacheKey ) ,
1072
+ darkTint,
1073
+ autoUpdate,
1074
+ boundsProvider,
1075
+ } ) ;
906
1076
}
907
1077
908
1078
const skeletonAsset = Assets . get < any | Uint8Array > ( skeleton ) ;
@@ -922,6 +1092,7 @@ export class Spine extends ViewContainer {
922
1092
skeletonData,
923
1093
darkTint,
924
1094
autoUpdate,
1095
+ boundsProvider,
925
1096
} ) ;
926
1097
}
927
1098
}
0 commit comments