mirror of
https://github.com/EsotericSoftware/spine-runtimes.git
synced 2025-12-21 17:56:04 +08:00
[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:
parent
cf0cb56609
commit
196df9c386
@ -29,10 +29,11 @@
|
||||
|
||||
package starlingExamples;
|
||||
|
||||
import starling.filters.BlurFilter;
|
||||
import spine.boundsprovider.SkinsAndAnimationBoundsProvider;
|
||||
import starlingExamples.Scene.SceneManager;
|
||||
import openfl.utils.Assets;
|
||||
import spine.SkeletonData;
|
||||
import spine.Physics;
|
||||
import spine.animation.AnimationStateData;
|
||||
import spine.atlas.TextureAtlas;
|
||||
import spine.starling.SkeletonSprite;
|
||||
@ -42,55 +43,56 @@ import starling.events.TouchEvent;
|
||||
import starling.events.TouchPhase;
|
||||
import starling.display.Quad;
|
||||
|
||||
class AnimationBoundExample extends Scene {
|
||||
class BoundsProviderExample extends Scene {
|
||||
var loadBinary = false;
|
||||
var skeletonSpriteClipping:SkeletonSprite;
|
||||
var skeletonSpriteNoClipping:SkeletonSprite;
|
||||
var quad:Quad;
|
||||
var quadNoClipping:Quad;
|
||||
private var movement = new openfl.geom.Point();
|
||||
|
||||
public function load():Void {
|
||||
background.color = 0x333333;
|
||||
var scale = .2;
|
||||
var scale = .4;
|
||||
|
||||
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 animationStateDataClipping = new AnimationStateData(skeletondata);
|
||||
animationStateDataClipping.defaultMix = 0.25;
|
||||
|
||||
skeletonSpriteClipping = new SkeletonSprite(skeletondata, animationStateDataClipping);
|
||||
skeletonSpriteClipping.skeleton.updateWorldTransform(Physics.update);
|
||||
var skeletondata = SkeletonData.from(Assets.getText("assets/spineboy-pro.json"), atlas, .5);
|
||||
|
||||
var stateDataClipping = new AnimationStateData(skeletondata);
|
||||
skeletonSpriteClipping = new SkeletonSprite(skeletondata, stateDataClipping, new SkinsAndAnimationBoundsProvider("portal", null, null, false));
|
||||
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.state.setAnimationByName(0, "portal", true);
|
||||
skeletonSpriteClipping.filter = new BlurFilter();
|
||||
|
||||
var animationClipping = skeletonSpriteClipping.state.setAnimationByName(0, "portal", true).animation;
|
||||
var animationBoundClipping = skeletonSpriteClipping.getAnimationBounds(animationClipping, true);
|
||||
var quad:Quad = new Quad(animationBoundClipping.width * scale, animationBoundClipping.height * scale, 0xc70000);
|
||||
quad.x = skeletonSpriteClipping.x + animationBoundClipping.x * scale;
|
||||
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;
|
||||
|
||||
var bounds = skeletonSpriteClipping.bounds;
|
||||
quad = new Quad(bounds.width, bounds.height, 0xc70000);
|
||||
quad.x = bounds.x;
|
||||
quad.y = bounds.y;
|
||||
addChild(quad);
|
||||
addChild(quadNoClipping);
|
||||
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);
|
||||
addText("Animation bound without clipping", 75, 350);
|
||||
addText("Animation bound with clipping", 370, 350);
|
||||
addText("Red area is the animation bound", 240, 400);
|
||||
|
||||
addText("Bounds with clipping", 40, 350);
|
||||
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(skeletonSpriteNoClipping);
|
||||
@ -98,9 +100,33 @@ class AnimationBoundExample extends Scene {
|
||||
}
|
||||
|
||||
public function onTouch(e:TouchEvent) {
|
||||
var touch = e.getTouch(this);
|
||||
if (touch != null && touch.phase == TouchPhase.ENDED) {
|
||||
var skeletonTouch = e.getTouch(skeletonSpriteClipping);
|
||||
var skeletonTouch2 = e.getTouch(skeletonSpriteNoClipping);
|
||||
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());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -29,7 +29,6 @@
|
||||
|
||||
package starlingExamples;
|
||||
|
||||
import spine.BlendMode;
|
||||
import starlingExamples.Scene.SceneManager;
|
||||
import openfl.utils.Assets;
|
||||
import spine.SkeletonData;
|
||||
@ -73,7 +72,7 @@ class CloudPotExample extends Scene {
|
||||
public function onTouch(e:TouchEvent) {
|
||||
var touch = e.getTouch(this);
|
||||
if (touch != null && touch.phase == TouchPhase.ENDED) {
|
||||
SceneManager.getInstance().switchScene(new AnimationBoundExample());
|
||||
SceneManager.getInstance().switchScene(new BoundsProviderExample());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
58
spine-haxe/spine-haxe/spine/BoundsProvider/BoundsProvider.hx
Normal file
58
spine-haxe/spine-haxe/spine/BoundsProvider/BoundsProvider.hx
Normal 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;
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
@ -29,7 +29,8 @@
|
||||
|
||||
package spine.starling;
|
||||
|
||||
import spine.animation.Animation;
|
||||
import spine.boundsprovider.BoundsProvider;
|
||||
import spine.boundsprovider.SetupPoseBoundsProvider;
|
||||
import starling.animation.IAnimatable;
|
||||
import openfl.geom.Matrix;
|
||||
import openfl.geom.Point;
|
||||
@ -42,8 +43,6 @@ import spine.SkeletonData;
|
||||
import spine.Slot;
|
||||
import spine.animation.AnimationState;
|
||||
import spine.animation.AnimationStateData;
|
||||
import spine.animation.MixBlend;
|
||||
import spine.animation.MixDirection;
|
||||
import spine.attachments.ClippingAttachment;
|
||||
import spine.attachments.MeshAttachment;
|
||||
import spine.attachments.RegionAttachment;
|
||||
@ -55,7 +54,6 @@ import starling.rendering.VertexData;
|
||||
import starling.textures.Texture;
|
||||
import starling.utils.Color;
|
||||
import starling.utils.MatrixUtil;
|
||||
import starling.utils.Max;
|
||||
|
||||
/** A starling display object that draws a skeleton. */
|
||||
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 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";
|
||||
|
||||
@ -80,12 +82,14 @@ class SkeletonSprite extends DisplayObject implements IAnimatable {
|
||||
public var afterUpdateWorldTransforms:SkeletonSprite->Void = function(_) {};
|
||||
|
||||
/** 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();
|
||||
Bone.yDown = true;
|
||||
_skeleton = new Skeleton(skeletonData);
|
||||
_skeleton.updateWorldTransform(Physics.update);
|
||||
_state = new AnimationState(animationStateData != null ? animationStateData : new AnimationStateData(skeletonData));
|
||||
skeleton = new Skeleton(skeletonData);
|
||||
skeleton.updateWorldTransform(Physics.update);
|
||||
state = new AnimationState(animationStateData != null ? animationStateData : new AnimationStateData(skeletonData));
|
||||
this.boundsProvider = boundsProvider ?? new SetupPoseBoundsProvider();
|
||||
this.calculateBounds();
|
||||
}
|
||||
|
||||
override public function render(painter:Painter):Void {
|
||||
@ -238,137 +242,37 @@ class SkeletonSprite extends DisplayObject implements IAnimatable {
|
||||
}
|
||||
|
||||
override public function hitTest(localPoint:Point):DisplayObject {
|
||||
if (!this.visible || !this.touchable)
|
||||
if (!visible || !touchable)
|
||||
return null;
|
||||
|
||||
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) {
|
||||
else if (__bounds.containsPoint(localPoint))
|
||||
return this;
|
||||
}
|
||||
|
||||
else
|
||||
return null;
|
||||
}
|
||||
|
||||
override public function getBounds(targetSpace:DisplayObject, resultRect:OpenFlRectangle = null):OpenFlRectangle {
|
||||
if (resultRect == null) {
|
||||
resultRect = new OpenFlRectangle();
|
||||
public function calculateBounds() {
|
||||
this.boundsProvider.calculateBounds(this, __bounds);
|
||||
}
|
||||
|
||||
override public function getBounds(targetSpace:DisplayObject, out:OpenFlRectangle = null):OpenFlRectangle {
|
||||
if (out == null)
|
||||
out = new OpenFlRectangle();
|
||||
|
||||
if (targetSpace == this) {
|
||||
resultRect.setTo(0, 0, 0, 0);
|
||||
out.setTo(0, 0, __bounds.width, __bounds.height);
|
||||
} 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 {
|
||||
getTransformationMatrix(targetSpace, _tempMatrix);
|
||||
MatrixUtil.transformCoords(_tempMatrix, 0, 0, _tempPoint);
|
||||
resultRect.setTo(_tempPoint.x, _tempPoint.y, 0, 0);
|
||||
}
|
||||
return resultRect;
|
||||
out.setTo(__bounds.x, __bounds.y, __bounds.width, __bounds.height);
|
||||
MatrixUtil.transformCoords(_tempMatrix, out.x, out.y, _tempPoint);
|
||||
out.setTo(_tempPoint.x, _tempPoint.y, out.width * scaleX, out.height * scaleY);
|
||||
}
|
||||
|
||||
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();
|
||||
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;
|
||||
return out;
|
||||
}
|
||||
|
||||
public var smoothing(get, set):String;
|
||||
@ -383,8 +287,8 @@ class SkeletonSprite extends DisplayObject implements IAnimatable {
|
||||
}
|
||||
|
||||
public function advanceTime(time:Float):Void {
|
||||
_state.update(time);
|
||||
_state.apply(skeleton);
|
||||
state.update(time);
|
||||
state.apply(skeleton);
|
||||
this.beforeUpdateWorldTransforms(this);
|
||||
skeleton.update(time);
|
||||
skeleton.updateWorldTransform(Physics.update);
|
||||
@ -431,15 +335,14 @@ class SkeletonSprite extends DisplayObject implements IAnimatable {
|
||||
}
|
||||
|
||||
override public function dispose():Void {
|
||||
if (_state != null) {
|
||||
_state.clearListeners();
|
||||
_state = null;
|
||||
if (state != null) {
|
||||
state.clearListeners();
|
||||
state = null;
|
||||
}
|
||||
if (_skeleton != null)
|
||||
_skeleton = null;
|
||||
if (skeleton != null)
|
||||
skeleton = null;
|
||||
dispatchEventWith(starling.events.Event.REMOVE_FROM_JUGGLER);
|
||||
removeFromParent();
|
||||
|
||||
// this will remove also all starling event listeners
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user