From 196df9c386c99a8b1601719d1f48e56ad28e17fd Mon Sep 17 00:00:00 2001 From: Davide Tantillo Date: Fri, 3 Oct 2025 11:45:07 +0200 Subject: [PATCH] [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) --- ...undExample.hx => BoundsProviderExample.hx} | 102 ++++++---- .../src/starlingExamples/CloudPotExample.hx | 3 +- .../AABBRectangleBoundsProvider.hx | 57 ++++++ .../spine/BoundsProvider/BoundsProvider.hx | 58 ++++++ .../CurrentPoseBoundsProvider.hx | 66 +++++++ .../BoundsProvider/SetupPoseBoundsProvider.hx | 71 +++++++ .../SkinsAndAnimationBoundsProvider.hx | 125 ++++++++++++ .../spine/starling/SkeletonSprite.hx | 181 ++++-------------- 8 files changed, 484 insertions(+), 179 deletions(-) rename spine-haxe/example/src/starlingExamples/{AnimationBoundExample.hx => BoundsProviderExample.hx} (54%) create mode 100644 spine-haxe/spine-haxe/spine/BoundsProvider/AABBRectangleBoundsProvider.hx create mode 100644 spine-haxe/spine-haxe/spine/BoundsProvider/BoundsProvider.hx create mode 100644 spine-haxe/spine-haxe/spine/BoundsProvider/CurrentPoseBoundsProvider.hx create mode 100644 spine-haxe/spine-haxe/spine/BoundsProvider/SetupPoseBoundsProvider.hx create mode 100644 spine-haxe/spine-haxe/spine/BoundsProvider/SkinsAndAnimationBoundsProvider.hx diff --git a/spine-haxe/example/src/starlingExamples/AnimationBoundExample.hx b/spine-haxe/example/src/starlingExamples/BoundsProviderExample.hx similarity index 54% rename from spine-haxe/example/src/starlingExamples/AnimationBoundExample.hx rename to spine-haxe/example/src/starlingExamples/BoundsProviderExample.hx index 43a043187..87e8365ff 100644 --- a/spine-haxe/example/src/starlingExamples/AnimationBoundExample.hx +++ b/spine-haxe/example/src/starlingExamples/BoundsProviderExample.hx @@ -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) { - SceneManager.getInstance().switchScene(new ControlBonesExample()); + 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()); + } } } } diff --git a/spine-haxe/example/src/starlingExamples/CloudPotExample.hx b/spine-haxe/example/src/starlingExamples/CloudPotExample.hx index 5d5fdb2e5..26166afea 100644 --- a/spine-haxe/example/src/starlingExamples/CloudPotExample.hx +++ b/spine-haxe/example/src/starlingExamples/CloudPotExample.hx @@ -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()); } } } diff --git a/spine-haxe/spine-haxe/spine/BoundsProvider/AABBRectangleBoundsProvider.hx b/spine-haxe/spine-haxe/spine/BoundsProvider/AABBRectangleBoundsProvider.hx new file mode 100644 index 000000000..d9d4cb297 --- /dev/null +++ b/spine-haxe/spine-haxe/spine/BoundsProvider/AABBRectangleBoundsProvider.hx @@ -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; + } +} diff --git a/spine-haxe/spine-haxe/spine/BoundsProvider/BoundsProvider.hx b/spine-haxe/spine-haxe/spine/BoundsProvider/BoundsProvider.hx new file mode 100644 index 000000000..e5d8b9088 --- /dev/null +++ b/spine-haxe/spine-haxe/spine/BoundsProvider/BoundsProvider.hx @@ -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; +} diff --git a/spine-haxe/spine-haxe/spine/BoundsProvider/CurrentPoseBoundsProvider.hx b/spine-haxe/spine-haxe/spine/BoundsProvider/CurrentPoseBoundsProvider.hx new file mode 100644 index 000000000..3bb010a7b --- /dev/null +++ b/spine-haxe/spine-haxe/spine/BoundsProvider/CurrentPoseBoundsProvider.hx @@ -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; + } +} diff --git a/spine-haxe/spine-haxe/spine/BoundsProvider/SetupPoseBoundsProvider.hx b/spine-haxe/spine-haxe/spine/BoundsProvider/SetupPoseBoundsProvider.hx new file mode 100644 index 000000000..22e40bf00 --- /dev/null +++ b/spine-haxe/spine-haxe/spine/BoundsProvider/SetupPoseBoundsProvider.hx @@ -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; + } +} diff --git a/spine-haxe/spine-haxe/spine/BoundsProvider/SkinsAndAnimationBoundsProvider.hx b/spine-haxe/spine-haxe/spine/BoundsProvider/SkinsAndAnimationBoundsProvider.hx new file mode 100644 index 000000000..6f083e226 --- /dev/null +++ b/spine-haxe/spine-haxe/spine/BoundsProvider/SkinsAndAnimationBoundsProvider.hx @@ -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; + 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, 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; + } +} diff --git a/spine-haxe/spine-haxe/spine/starling/SkeletonSprite.hx b/spine-haxe/spine-haxe/spine/starling/SkeletonSprite.hx index f0e9b3f70..09473a119 100644 --- a/spine-haxe/spine-haxe/spine/starling/SkeletonSprite.hx +++ b/spine-haxe/spine-haxe/spine/starling/SkeletonSprite.hx @@ -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 = new Array(); static private var blendModes:Array = [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 = skeleton.slots; - var worldVertices:Array = _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; - } - - return null; + 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; - } - - 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; + 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); } - 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(); }