[haxe][starling] Replace bounds calculation with BoundsProvider system. Close #2915.

Replace direct bounds calculation with BoundsProvider interface for better
performance and correctness. This enables filters to work properly and makes it easier to get the correct bounds (SkeletonSprite.bounds).

BREAKING CHANGES:
- Removed getAnimationBounds() method. Replace it with the appropriate BoundsProvider implementation based on your use case, or create your own.
- hitTest() now uses the assigned BoundsProvider instead of direct calculation. For accurate hit testing, use CurrentPoseBoundsProvider and call calculateBounds() each frame or on click.

New features:
- Add BoundsProvider abstract class and AABBRectangleBoundsProvider, CurrentPoseBoundsProvider, SetupPoseBoundsProvider, SkinsAndAnimationBoundsProvider implementations
- SkeletonSprite constructor now accepts a third optional parameter for BoundsProvider. SetupPoseBoundsProvider is used by default.
- Added calculateBounds() to recalculate bounds on demand (useful with CurrentPoseBoundsProvider)
This commit is contained in:
Davide Tantillo 2025-10-03 11:45:07 +02:00
parent cf0cb56609
commit 196df9c386
8 changed files with 484 additions and 179 deletions

View File

@ -29,10 +29,11 @@
package starlingExamples; package starlingExamples;
import starling.filters.BlurFilter;
import spine.boundsprovider.SkinsAndAnimationBoundsProvider;
import starlingExamples.Scene.SceneManager; import starlingExamples.Scene.SceneManager;
import openfl.utils.Assets; import openfl.utils.Assets;
import spine.SkeletonData; import spine.SkeletonData;
import spine.Physics;
import spine.animation.AnimationStateData; import spine.animation.AnimationStateData;
import spine.atlas.TextureAtlas; import spine.atlas.TextureAtlas;
import spine.starling.SkeletonSprite; import spine.starling.SkeletonSprite;
@ -42,55 +43,56 @@ import starling.events.TouchEvent;
import starling.events.TouchPhase; import starling.events.TouchPhase;
import starling.display.Quad; import starling.display.Quad;
class AnimationBoundExample extends Scene { class BoundsProviderExample extends Scene {
var loadBinary = false; var loadBinary = false;
var skeletonSpriteClipping:SkeletonSprite; var skeletonSpriteClipping:SkeletonSprite;
var skeletonSpriteNoClipping:SkeletonSprite; var skeletonSpriteNoClipping:SkeletonSprite;
var quad:Quad;
var quadNoClipping:Quad;
private var movement = new openfl.geom.Point();
public function load():Void { public function load():Void {
background.color = 0x333333; background.color = 0x333333;
var scale = .2; var scale = .4;
var atlas = new TextureAtlas(Assets.getText("assets/spineboy.atlas"), new StarlingTextureLoader("assets/spineboy.atlas")); var atlas = new TextureAtlas(Assets.getText("assets/spineboy.atlas"), new StarlingTextureLoader("assets/spineboy.atlas"));
var skeletondata = SkeletonData.from(Assets.getText("assets/spineboy-pro.json"), atlas); var skeletondata = SkeletonData.from(Assets.getText("assets/spineboy-pro.json"), atlas, .5);
var animationStateDataClipping = new AnimationStateData(skeletondata);
animationStateDataClipping.defaultMix = 0.25;
skeletonSpriteClipping = new SkeletonSprite(skeletondata, animationStateDataClipping);
skeletonSpriteClipping.skeleton.updateWorldTransform(Physics.update);
var stateDataClipping = new AnimationStateData(skeletondata);
skeletonSpriteClipping = new SkeletonSprite(skeletondata, stateDataClipping, new SkinsAndAnimationBoundsProvider("portal", null, null, false));
skeletonSpriteClipping.scale = scale; skeletonSpriteClipping.scale = scale;
skeletonSpriteClipping.x = Starling.current.stage.stageWidth / 3 * 2; skeletonSpriteClipping.x = Starling.current.stage.stageWidth / 4 * 3;
skeletonSpriteClipping.y = Starling.current.stage.stageHeight / 2; skeletonSpriteClipping.y = Starling.current.stage.stageHeight / 2;
skeletonSpriteClipping.state.setAnimationByName(0, "portal", true);
skeletonSpriteClipping.filter = new BlurFilter();
var animationClipping = skeletonSpriteClipping.state.setAnimationByName(0, "portal", true).animation; var bounds = skeletonSpriteClipping.bounds;
var animationBoundClipping = skeletonSpriteClipping.getAnimationBounds(animationClipping, true); quad = new Quad(bounds.width, bounds.height, 0xc70000);
var quad:Quad = new Quad(animationBoundClipping.width * scale, animationBoundClipping.height * scale, 0xc70000); quad.x = bounds.x;
quad.x = skeletonSpriteClipping.x + animationBoundClipping.x * scale; quad.y = bounds.y;
quad.y = skeletonSpriteClipping.y + animationBoundClipping.y * scale;
var animationStateDataNoClipping = new AnimationStateData(skeletondata);
animationStateDataNoClipping.defaultMix = 0.25;
skeletonSpriteNoClipping = new SkeletonSprite(skeletondata, animationStateDataNoClipping);
skeletonSpriteNoClipping.skeleton.updateWorldTransform(Physics.update);
skeletonSpriteNoClipping.scale = scale;
skeletonSpriteNoClipping.x = Starling.current.stage.stageWidth / 3;
skeletonSpriteNoClipping.y = Starling.current.stage.stageHeight / 2;
var animationNoClipping = skeletonSpriteNoClipping.state.setAnimationByName(0, "portal", true).animation;
var animationBoundNoClipping = skeletonSpriteNoClipping.getAnimationBounds(animationNoClipping, false);
var quadNoClipping:Quad = new Quad(animationBoundNoClipping.width * scale, animationBoundNoClipping.height * scale, 0xc70000);
quadNoClipping.x = skeletonSpriteNoClipping.x + animationBoundNoClipping.x * scale;
quadNoClipping.y = skeletonSpriteNoClipping.y + animationBoundNoClipping.y * scale;
addChild(quad); addChild(quad);
addChild(quadNoClipping);
addChild(skeletonSpriteClipping); addChild(skeletonSpriteClipping);
var stateDataNoClipping = new AnimationStateData(skeletondata);
skeletonSpriteNoClipping = new SkeletonSprite(skeletondata, stateDataNoClipping, new SkinsAndAnimationBoundsProvider("portal", null, null, true));
skeletonSpriteNoClipping.scale = scale;
skeletonSpriteNoClipping.x = Starling.current.stage.stageWidth / 4;
skeletonSpriteNoClipping.y = Starling.current.stage.stageHeight / 2;
skeletonSpriteNoClipping.state.setAnimationByName(0, "portal", true);
skeletonSpriteNoClipping.filter = new BlurFilter();
bounds = skeletonSpriteNoClipping.bounds;
quadNoClipping = new Quad(bounds.width, bounds.height, 0xc70000);
quadNoClipping.x = bounds.x;
quadNoClipping.y = bounds.y;
addChild(quadNoClipping);
addChild(skeletonSpriteNoClipping); addChild(skeletonSpriteNoClipping);
addText("Animation bound without clipping", 75, 350);
addText("Animation bound with clipping", 370, 350); addText("Bounds with clipping", 40, 350);
addText("Red area is the animation bound", 240, 400); addText("Bounds without clipping", 400, 350);
addText("Bounds created with SkinsAndAnimationBoundsProvider", 240, 400);
addText("The blur filter shows also the correcntess of the bounds.", 240, 450);
addText("You can move the elements around to see the bounds is always correct.", 240, 500);
juggler.add(skeletonSpriteClipping); juggler.add(skeletonSpriteClipping);
juggler.add(skeletonSpriteNoClipping); juggler.add(skeletonSpriteNoClipping);
@ -98,9 +100,33 @@ class AnimationBoundExample extends Scene {
} }
public function onTouch(e:TouchEvent) { public function onTouch(e:TouchEvent) {
var touch = e.getTouch(this); var skeletonTouch = e.getTouch(skeletonSpriteClipping);
if (touch != null && touch.phase == TouchPhase.ENDED) { var skeletonTouch2 = e.getTouch(skeletonSpriteNoClipping);
SceneManager.getInstance().switchScene(new ControlBonesExample()); if (skeletonTouch != null) {
if (skeletonTouch.phase == TouchPhase.MOVED) {
skeletonTouch.getMovement(this, movement);
skeletonSpriteClipping.x += movement.x;
skeletonSpriteClipping.y += movement.y;
var sBounds = skeletonSpriteClipping.bounds;
quad.x = sBounds.x;
quad.y = sBounds.y;
}
} else if (skeletonTouch2 != null) {
if (skeletonTouch2.phase == TouchPhase.MOVED) {
skeletonTouch2.getMovement(this, movement);
skeletonSpriteNoClipping.x += movement.x;
skeletonSpriteNoClipping.y += movement.y;
var sBounds = skeletonSpriteNoClipping.bounds;
quadNoClipping.x = sBounds.x;
quadNoClipping.y = sBounds.y;
}
} else {
var sceneTouch = e.getTouch(this);
if (sceneTouch != null && sceneTouch.phase == TouchPhase.ENDED) {
SceneManager.getInstance().switchScene(new ControlBonesExample());
}
} }
} }
} }

View File

@ -29,7 +29,6 @@
package starlingExamples; package starlingExamples;
import spine.BlendMode;
import starlingExamples.Scene.SceneManager; import starlingExamples.Scene.SceneManager;
import openfl.utils.Assets; import openfl.utils.Assets;
import spine.SkeletonData; import spine.SkeletonData;
@ -73,7 +72,7 @@ class CloudPotExample extends Scene {
public function onTouch(e:TouchEvent) { public function onTouch(e:TouchEvent) {
var touch = e.getTouch(this); var touch = e.getTouch(this);
if (touch != null && touch.phase == TouchPhase.ENDED) { if (touch != null && touch.phase == TouchPhase.ENDED) {
SceneManager.getInstance().switchScene(new AnimationBoundExample()); SceneManager.getInstance().switchScene(new BoundsProviderExample());
} }
} }
} }

View File

@ -0,0 +1,57 @@
/******************************************************************************
* Spine Runtimes License Agreement
* Last updated April 5, 2025. Replaces all prior versions.
*
* Copyright (c) 2013-2025, 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.
*****************************************************************************/
package spine.boundsprovider;
import spine.animation.AnimationState;
import spine.boundsprovider.BoundsProvider.BoundsGameObject;
import spine.boundsprovider.BoundsProvider.BoundsRectangle;
/** A bounds provider that provides a fixed size given by the user. */
class AABBRectangleBoundsProvider extends BoundsProvider {
private var x:Float;
private var y:Float;
private var width:Float;
private var height:Float;
public function new(x:Float, y:Float, width:Float, height:Float) {
this.x = x;
this.y = y;
this.width = width;
this.height = height;
}
public function calculateBounds(gameObject:BoundsGameObject, out:BoundsRectangle):BoundsRectangle {
out.x = x;
out.y = y;
out.width = width;
out.height = height;
return out;
}
}

View File

@ -0,0 +1,58 @@
/******************************************************************************
* Spine Runtimes License Agreement
* Last updated April 5, 2025. Replaces all prior versions.
*
* Copyright (c) 2013-2025, 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.
*****************************************************************************/
package spine.boundsprovider;
import spine.animation.AnimationState;
/** A bounds provider calculates the bounding box for a skeleton, which is then assigned as the size of the SpineGameObject. */
abstract class BoundsProvider {
/** Returns the bounding box for the skeleton, in skeleton space. */
abstract public function calculateBounds(gameObject:BoundsGameObject, out:BoundsRectangle):BoundsRectangle;
private function zeroRectangle(rectangle:BoundsRectangle):BoundsRectangle {
rectangle.x = 0;
rectangle.y = 0;
rectangle.width = 0;
rectangle.height = 0;
return rectangle;
}
}
typedef BoundsGameObject = {
var skeleton(default, null):Skeleton;
var state(default, null):AnimationState;
}
typedef BoundsRectangle = {
var x:Float;
var y:Float;
var width:Float;
var height:Float;
}

View File

@ -0,0 +1,66 @@
/******************************************************************************
* Spine Runtimes License Agreement
* Last updated April 5, 2025. Replaces all prior versions.
*
* Copyright (c) 2013-2025, 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.
*****************************************************************************/
package spine.boundsprovider;
import spine.animation.AnimationState;
import spine.boundsprovider.BoundsProvider.BoundsGameObject;
import spine.boundsprovider.BoundsProvider.BoundsRectangle;
/** A bounds provider that calculates the bounding box from the current pose it is invoked. */
class CurrentPoseBoundsProvider extends BoundsProvider {
private var clipping:Bool;
/**
* @param clipping If true, clipping attachments are used to compute the bounds. False, by default.
*/
public function new(clipping = false) {
this.clipping = clipping;
}
public function calculateBounds(gameObject:BoundsGameObject, out:BoundsRectangle):BoundsRectangle {
var skeleton = gameObject.skeleton;
if (skeleton == null) {
zeroRectangle(out);
return out;
}
var newBounds = skeleton.getBounds(clipping ? new SkeletonClipping() : null);
if (newBounds.width == Math.NEGATIVE_INFINITY) {
zeroRectangle(out);
return out;
}
out.x = newBounds.x;
out.y = newBounds.y;
out.width = newBounds.width;
out.height = newBounds.height;
return out;
}
}

View File

@ -0,0 +1,71 @@
/******************************************************************************
* Spine Runtimes License Agreement
* Last updated April 5, 2025. Replaces all prior versions.
*
* Copyright (c) 2013-2025, 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.
*****************************************************************************/
package spine.boundsprovider;
import spine.animation.AnimationState;
import spine.boundsprovider.BoundsProvider.BoundsGameObject;
import spine.boundsprovider.BoundsProvider.BoundsRectangle;
/** A bounds provider that calculates the bounding box from the setup pose. */
class SetupPoseBoundsProvider extends BoundsProvider {
private var clipping:Bool;
/**
* @param clipping If true, clipping attachments are used to compute the bounds. False, by default.
*/
public function new(clipping = false) {
this.clipping = clipping;
}
public function calculateBounds(gameObject:BoundsGameObject, out:BoundsRectangle):BoundsRectangle {
var skeleton = gameObject.skeleton;
if (skeleton == null) {
zeroRectangle(out);
return out;
}
// Make a copy of skeleton as this might be called while
// the skeleton in the GameObject has already been heavily modified. We can not
// reconstruct that state.
var skeleton = new Skeleton(skeleton.data);
skeleton.setupPose();
skeleton.updateWorldTransform(Physics.update);
var newBounds = skeleton.getBounds(clipping ? new SkeletonClipping() : null);
if (newBounds.width == Math.NEGATIVE_INFINITY) {
zeroRectangle(out);
return out;
}
out.x = newBounds.x;
out.y = newBounds.y;
out.width = newBounds.width;
out.height = newBounds.height;
return out;
}
}

View File

@ -0,0 +1,125 @@
/******************************************************************************
* Spine Runtimes License Agreement
* Last updated April 5, 2025. Replaces all prior versions.
*
* Copyright (c) 2013-2025, 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.
*****************************************************************************/
package spine.boundsprovider;
import spine.animation.AnimationState;
import spine.boundsprovider.BoundsProvider.BoundsGameObject;
import spine.boundsprovider.BoundsProvider.BoundsRectangle;
/** A bounds provider that calculates the bounding box by taking the maximumg bounding box for a combination of skins and specific animation. */
class SkinsAndAnimationBoundsProvider extends BoundsProvider {
private var animation:String;
private var skins:Array<String>;
private var timeStep:Float;
private var clipping:Bool;
/**
* @param animation The animation to use for calculating the bounds. If null, the setup pose is used.
* @param skins The skins to use for calculating the bounds. If empty, the default skin is used.
* @param timeStep The time step to use for calculating the bounds. A smaller time step means more precision, but slower calculation.
* @param clipping If true, clipping attachments are used to compute the bounds. False, by default.
*/
public function new(?animation:String, ?skins:Array<String>, timeStep:Float = 0.05, clipping = false) {
if (skins == null)
skins = [];
this.animation = animation;
this.skins = skins;
this.timeStep = timeStep;
this.clipping = clipping;
}
public function calculateBounds(gameObject:BoundsGameObject, out:BoundsRectangle):BoundsRectangle {
var skeleton = gameObject.skeleton;
var state = gameObject.state;
if (skeleton == null || state == null) {
zeroRectangle(out);
return out;
}
// Make a copy of animation state and skeleton as this might be called while
// the skeleton in the GameObject has already been heavily modified. We can not
// reconstruct that state.
var animationState = new AnimationState(state.data);
var skeleton = new Skeleton(skeleton.data);
var clipper = clipping ? new SkeletonClipping() : null;
var data = skeleton.data;
if (skins.length > 0) {
var customSkin = new Skin("custom-skin");
for (skinName in skins) {
var skin = data.findSkin(skinName);
if (skin == null)
continue;
customSkin.addSkin(skin);
}
skeleton.skin = customSkin;
}
skeleton.setupPose();
var animation = this.animation != null ? data.findAnimation(this.animation) : null;
if (animation == null) {
skeleton.updateWorldTransform(Physics.update);
var newBounds = skeleton.getBounds(clipper);
out.x = newBounds.x;
out.y = newBounds.y;
out.width = newBounds.width;
out.height = newBounds.height;
return out;
}
var minX = Math.POSITIVE_INFINITY,
minY = Math.POSITIVE_INFINITY,
maxX = Math.NEGATIVE_INFINITY,
maxY = Math.NEGATIVE_INFINITY;
animationState.clearTracks();
animationState.setAnimation(0, animation, false);
var steps = Math.max(animation.duration / this.timeStep, 1.0);
var i = 0.0;
while (i < steps) {
var delta = i > 0 ? this.timeStep : 0;
animationState.update(delta);
animationState.apply(skeleton);
skeleton.update(delta);
skeleton.updateWorldTransform(Physics.update);
var bounds = skeleton.getBounds(clipper);
minX = Math.min(minX, bounds.x);
minY = Math.min(minY, bounds.y);
maxX = Math.max(maxX, bounds.x + bounds.width);
maxY = Math.max(maxY, bounds.y + bounds.height);
i++;
}
out.x = minX;
out.y = minY;
out.width = maxX - minX;
out.height = maxY - minY;
return out;
}
}

View File

@ -29,7 +29,8 @@
package spine.starling; package spine.starling;
import spine.animation.Animation; import spine.boundsprovider.BoundsProvider;
import spine.boundsprovider.SetupPoseBoundsProvider;
import starling.animation.IAnimatable; import starling.animation.IAnimatable;
import openfl.geom.Matrix; import openfl.geom.Matrix;
import openfl.geom.Point; import openfl.geom.Point;
@ -42,8 +43,6 @@ import spine.SkeletonData;
import spine.Slot; import spine.Slot;
import spine.animation.AnimationState; import spine.animation.AnimationState;
import spine.animation.AnimationStateData; import spine.animation.AnimationStateData;
import spine.animation.MixBlend;
import spine.animation.MixDirection;
import spine.attachments.ClippingAttachment; import spine.attachments.ClippingAttachment;
import spine.attachments.MeshAttachment; import spine.attachments.MeshAttachment;
import spine.attachments.RegionAttachment; import spine.attachments.RegionAttachment;
@ -55,7 +54,6 @@ import starling.rendering.VertexData;
import starling.textures.Texture; import starling.textures.Texture;
import starling.utils.Color; import starling.utils.Color;
import starling.utils.MatrixUtil; import starling.utils.MatrixUtil;
import starling.utils.Max;
/** A starling display object that draws a skeleton. */ /** A starling display object that draws a skeleton. */
class SkeletonSprite extends DisplayObject implements IAnimatable { class SkeletonSprite extends DisplayObject implements IAnimatable {
@ -64,9 +62,13 @@ class SkeletonSprite extends DisplayObject implements IAnimatable {
static private var _tempVertices:Array<Float> = new Array<Float>(); static private var _tempVertices:Array<Float> = new Array<Float>();
static private var blendModes:Array<String> = [BlendMode.NORMAL, BlendMode.ADD, BlendMode.MULTIPLY, BlendMode.SCREEN]; static private var blendModes:Array<String> = [BlendMode.NORMAL, BlendMode.ADD, BlendMode.MULTIPLY, BlendMode.SCREEN];
private var _skeleton:Skeleton; public var skeleton(default, null):Skeleton;
public var state(default, null):AnimationState;
public var _state:AnimationState; public var boundsProvider:BoundsProvider;
private var __bounds = new OpenFlRectangle();
private var _boundsPoint = [.0, .0];
private var _smoothing:String = "bilinear"; private var _smoothing:String = "bilinear";
@ -80,12 +82,14 @@ class SkeletonSprite extends DisplayObject implements IAnimatable {
public var afterUpdateWorldTransforms:SkeletonSprite->Void = function(_) {}; public var afterUpdateWorldTransforms:SkeletonSprite->Void = function(_) {};
/** Creates an uninitialized SkeletonSprite. The skeleton and animation state must be set before use. */ /** Creates an uninitialized SkeletonSprite. The skeleton and animation state must be set before use. */
public function new(skeletonData:SkeletonData, animationStateData:AnimationStateData = null) { public function new(skeletonData:SkeletonData, animationStateData:AnimationStateData = null, ?boundsProvider:BoundsProvider) {
super(); super();
Bone.yDown = true; Bone.yDown = true;
_skeleton = new Skeleton(skeletonData); skeleton = new Skeleton(skeletonData);
_skeleton.updateWorldTransform(Physics.update); skeleton.updateWorldTransform(Physics.update);
_state = new AnimationState(animationStateData != null ? animationStateData : new AnimationStateData(skeletonData)); state = new AnimationState(animationStateData != null ? animationStateData : new AnimationStateData(skeletonData));
this.boundsProvider = boundsProvider ?? new SetupPoseBoundsProvider();
this.calculateBounds();
} }
override public function render(painter:Painter):Void { override public function render(painter:Painter):Void {
@ -238,137 +242,37 @@ class SkeletonSprite extends DisplayObject implements IAnimatable {
} }
override public function hitTest(localPoint:Point):DisplayObject { override public function hitTest(localPoint:Point):DisplayObject {
if (!this.visible || !this.touchable) if (!visible || !touchable)
return null; return null;
else if (__bounds.containsPoint(localPoint))
var minX:Float = Max.MAX_VALUE;
var minY:Float = Max.MAX_VALUE;
var maxX:Float = -Max.MAX_VALUE;
var maxY:Float = -Max.MAX_VALUE;
var slots:Array<Slot> = skeleton.slots;
var worldVertices:Array<Float> = _tempVertices;
var empty:Bool = true;
for (i in 0...slots.length) {
var slot:Slot = slots[i];
var pose = slot.applied;
var attachment = pose.attachment;
if (attachment == null)
continue;
var verticesLength:Int;
if (Std.isOfType(attachment, RegionAttachment)) {
var region:RegionAttachment = cast(attachment, RegionAttachment);
verticesLength = 8;
region.computeWorldVertices(slot, worldVertices, 0, 2);
} else if (Std.isOfType(attachment, MeshAttachment)) {
var mesh:MeshAttachment = cast(attachment, MeshAttachment);
verticesLength = mesh.worldVerticesLength;
if (worldVertices.length < verticesLength)
worldVertices.resize(verticesLength);
mesh.computeWorldVertices(skeleton, slot, 0, verticesLength, worldVertices, 0, 2);
} else {
continue;
}
if (verticesLength != 0) {
empty = false;
}
var ii:Int = 0;
while (ii < verticesLength) {
var x:Float = worldVertices[ii],
y:Float = worldVertices[ii + 1];
minX = minX < x ? minX : x;
minY = minY < y ? minY : y;
maxX = maxX > x ? maxX : x;
maxY = maxY > y ? maxY : y;
ii += 2;
}
}
if (empty) {
return null;
}
var temp:Float;
if (maxX < minX) {
temp = maxX;
maxX = minX;
minX = temp;
}
if (maxY < minY) {
temp = maxY;
maxY = minY;
minY = temp;
}
if (localPoint.x >= minX && localPoint.x < maxX && localPoint.y >= minY && localPoint.y < maxY) {
return this; return this;
} else
return null;
return null;
} }
override public function getBounds(targetSpace:DisplayObject, resultRect:OpenFlRectangle = null):OpenFlRectangle { public function calculateBounds() {
if (resultRect == null) { this.boundsProvider.calculateBounds(this, __bounds);
resultRect = new OpenFlRectangle(); }
}
override public function getBounds(targetSpace:DisplayObject, out:OpenFlRectangle = null):OpenFlRectangle {
if (out == null)
out = new OpenFlRectangle();
if (targetSpace == this) { if (targetSpace == this) {
resultRect.setTo(0, 0, 0, 0); out.setTo(0, 0, __bounds.width, __bounds.height);
} else if (targetSpace == parent) { } else if (targetSpace == parent) {
resultRect.setTo(x, y, 0, 0); _boundsPoint[0] = __bounds.x;
_boundsPoint[1] = __bounds.y;
skeletonToHaxeWorldCoordinates(_boundsPoint);
out.setTo(_boundsPoint[0], _boundsPoint[1], __bounds.width * scaleX, __bounds.height * scaleX);
} else { } else {
getTransformationMatrix(targetSpace, _tempMatrix); getTransformationMatrix(targetSpace, _tempMatrix);
MatrixUtil.transformCoords(_tempMatrix, 0, 0, _tempPoint); out.setTo(__bounds.x, __bounds.y, __bounds.width, __bounds.height);
resultRect.setTo(_tempPoint.x, _tempPoint.y, 0, 0); MatrixUtil.transformCoords(_tempMatrix, out.x, out.y, _tempPoint);
} out.setTo(_tempPoint.x, _tempPoint.y, out.width * scaleX, out.height * scaleY);
return resultRect;
}
public function getAnimationBounds(animation:Animation, clip:Bool = true):Rectangle {
var clipper = clip ? SkeletonSprite.clipper : null;
_skeleton.setupPose();
var steps = 100, time = 0.;
var stepTime = animation.duration != 0 ? animation.duration / steps : 0;
var minX = 100000000.,
maxX = -100000000.,
minY = 100000000.,
maxY = -100000000.;
for (i in 0...steps) {
animation.apply(_skeleton, time, time, false, [], 1, MixBlend.setup, MixDirection.mixIn, false);
_skeleton.updateWorldTransform(Physics.update);
var boundsSkel = _skeleton.getBounds(clipper);
if (!Math.isNaN(boundsSkel.x) && !Math.isNaN(boundsSkel.y) && !Math.isNaN(boundsSkel.width) && !Math.isNaN(boundsSkel.height)) {
minX = Math.min(boundsSkel.x, minX);
minY = Math.min(boundsSkel.y, minY);
maxX = Math.max(boundsSkel.x + boundsSkel.width, maxX);
maxY = Math.max(boundsSkel.y + boundsSkel.height, maxY);
} else
throw new SpineException("Animation bounds are invalid: " + animation.name);
time += stepTime;
} }
var bounds = new Rectangle(); return out;
bounds.x = minX;
bounds.y = minY;
bounds.width = maxX - minX;
bounds.height = maxY - minY;
return bounds;
}
public var skeleton(get, never):Skeleton;
private function get_skeleton():Skeleton {
return _skeleton;
}
public var state(get, never):AnimationState;
private function get_state():AnimationState {
return _state;
} }
public var smoothing(get, set):String; public var smoothing(get, set):String;
@ -383,8 +287,8 @@ class SkeletonSprite extends DisplayObject implements IAnimatable {
} }
public function advanceTime(time:Float):Void { public function advanceTime(time:Float):Void {
_state.update(time); state.update(time);
_state.apply(skeleton); state.apply(skeleton);
this.beforeUpdateWorldTransforms(this); this.beforeUpdateWorldTransforms(this);
skeleton.update(time); skeleton.update(time);
skeleton.updateWorldTransform(Physics.update); skeleton.updateWorldTransform(Physics.update);
@ -431,15 +335,14 @@ class SkeletonSprite extends DisplayObject implements IAnimatable {
} }
override public function dispose():Void { override public function dispose():Void {
if (_state != null) { if (state != null) {
_state.clearListeners(); state.clearListeners();
_state = null; state = null;
} }
if (_skeleton != null) if (skeleton != null)
_skeleton = null; skeleton = null;
dispatchEventWith(starling.events.Event.REMOVE_FROM_JUGGLER); dispatchEventWith(starling.events.Event.REMOVE_FROM_JUGGLER);
removeFromParent(); removeFromParent();
// this will remove also all starling event listeners // this will remove also all starling event listeners
super.dispose(); super.dispose();
} }