From 5990697d6e5b7c9d840cb2bf8af277da99dc9434 Mon Sep 17 00:00:00 2001 From: Davide Tantillo Date: Wed, 8 Oct 2025 12:42:51 +0200 Subject: [PATCH] [haxe][flixel] SkeletonSprite extends FlxTypedGroup rather than FlxObject. Replace direct bounds calculation with BoundsProvider interface for better performance and correctness. This makes it easier to get the correct bounds. BREAKING CHANGES: - SkeletonSprite extends FlxTypedGroup rather than FlxObject. This was necessary because the FlxObject bounding/hitbox is always connected to its position and size and cannot be offset. - Removed getAnimationBounds() method. Replace it with the appropriate BoundsProvider implementation based on your use case, or create your own. - Removed setBoundingBox(). Use BoundsProvider features. - 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: - Uses BoundsProvider as starling. - SkeletonSprite constructor now accepts a third optional parameter for BoundsProvider. SetupPoseBoundsProvider is used by default. - Added calculateBounds() to recalculate bounds on demand. --- spine-haxe/example/assets/quad.atlas | 5 + spine-haxe/example/assets/quad.json | 44 +++ spine-haxe/example/assets/quad.png | Bin 0 -> 1537 bytes .../src/flixelExamples/BasicExample.hx | 3 +- ...undExample.hx => BoundsProviderExample.hx} | 33 +- .../src/flixelExamples/CloudPotExample.hx | 2 +- .../src/flixelExamples/ControlBonesExample.hx | 7 +- .../src/flixelExamples/EventsExample.hx | 3 - .../example/src/flixelExamples/FlixelState.hx | 79 +++- .../src/flixelExamples/MixAndMatchExample.hx | 6 +- .../src/flixelExamples/SequenceExample.hx | 1 - .../example/src/flixelExamples/TankExample.hx | 1 - .../example/src/flixelExamples/VineExample.hx | 1 - spine-haxe/project.xml | 4 +- .../BoundsProvider/SetupPoseBoundsProvider.hx | 8 +- .../SkinsAndAnimationBoundsProvider.hx | 8 +- .../spine-haxe/spine/flixel/SkeletonSprite.hx | 354 ++++++++++++------ .../example/control-bones-example.html | 6 +- 18 files changed, 408 insertions(+), 157 deletions(-) create mode 100644 spine-haxe/example/assets/quad.atlas create mode 100644 spine-haxe/example/assets/quad.json create mode 100644 spine-haxe/example/assets/quad.png rename spine-haxe/example/src/flixelExamples/{AnimationBoundExample.hx => BoundsProviderExample.hx} (74%) diff --git a/spine-haxe/example/assets/quad.atlas b/spine-haxe/example/assets/quad.atlas new file mode 100644 index 000000000..6ac9eebdd --- /dev/null +++ b/spine-haxe/example/assets/quad.atlas @@ -0,0 +1,5 @@ +quad.png +size:100,100 +filter:Linear,Linear +image +bounds:0,0,100,100 \ No newline at end of file diff --git a/spine-haxe/example/assets/quad.json b/spine-haxe/example/assets/quad.json new file mode 100644 index 000000000..ce8808c57 --- /dev/null +++ b/spine-haxe/example/assets/quad.json @@ -0,0 +1,44 @@ +{ + "bones": [ + { "name": "root" }, + { + "name": "pivot", + "parent": "root", + "scaleX": 1, + "scaleY": 1, + "rotation": 0, + "x": 20, + "y": -20 + }, + { + "name": "pivot2", + "parent": "pivot", + "scaleX": 1, + "scaleY": 1, + "rotation": 0, + "x": 20, + "y": -20 + }, + { + "name": "replaceMe", + "parent": "pivot2", + "scaleX": 1, + "scaleY": 1, + "rotation": 0, + "x": 20, + "y": -20 + } + ], + "slots": [ + { "name": "replaceMe", "bone": "replaceMe", "attachment": "image" } + ], + "skins": [ + { + "name": "default", + "attachments": { + "replaceMe": { "image": { "width": 100, "height": 100 } } + } + } + ], + "animations": { "animation": {} } +} diff --git a/spine-haxe/example/assets/quad.png b/spine-haxe/example/assets/quad.png new file mode 100644 index 0000000000000000000000000000000000000000..94f8545bcd67f4551cc9ba8d5281ce9c1472c3f6 GIT binary patch literal 1537 zcmV+c2LAbpP)B`L~=tfZ8MQdSnohD2TodB6Gfy*1O^xte?Dcjw;o zJ$H`!_Ukw1p81~dXU^lE%l7;G3nJ)*jdcPd1PJL6AjCp|5DV@>Y&M%CSm)yNAkG+8 zLE#qUFAOW7a0%iLgLf1GLM*rg(SpG<3RfVWVDO5<4TxINt%AY@h}L3T0flQ2z3-(M zcYHp^1Q0XxB98CeCHcwN|({K<9o#ZKBLqS+~j>o*2fUqt&<=hPe z;TeNv9}EHEb*iG!53hq*1%p+8yb5A93>N(|1%!1$dFq?jK&*^mW*@x*;tLpN^;I{B zRo4wx{nZ8HE;&zvb&)Ccg#-vo7;1g^ZxC;H#3~?q`{C0dM)ZLhh!GvK0*LOM%r1hv zV)$YX0)$u&0)$u&0)$u&0)$u&0>t1Petv%7`}PjM6=A0Hp^_Vy-8Q!NJT9L~?rp{uJ4va_?HxVRXqtE(X@ zDhmAk{2cB0`};#uQW8u|Oh|YhgCM}(-X65Kw?ln>JuELT!~6R?xni)+0qZn6Itl>+ z0e@ca8i9d<(9qBTH#aw)f#A@qtgMt|v7yAoMA+KeB2^3uIedM6K}AJ{CJcMv$f>HT zg4^3$?I5S8rw|((D{Ew6V1QIHDCB@31qB6eN5o&F2kY6{*(qJG3FPA90-~d%WsTI< z*6P|ZSP=xLtj5Mh#dVR9kzBF=c5RA!-=;`T5cvzG_Zt zAYz2*8zH!mmzS5r_4T#3oCJt5Ab4L=SXcX2hY#X-T}dVb9Z+)^!4>QVsLN}=H}+4!$>{P zOsocCuaoN?JTB@(LqnR6pkyGKnVD|qMSI|xOi4+J>P?O>f%NwF!o$M@l$Dh!+6)g5 zhoz+@O3wW$3=$F&q%USoO^x&i4bJbG zpPyIk#PEzu?c~Ge`T`2$t^hv(hZqh|oHOl##|C(kiKoLXia`Mc18ao$85 FlxG.switchState(() -> new AnimationBoundExample())); + var button = new FlxButton(0, 0, "Next scene", () -> FlxG.switchState(() -> new BoundsProviderExample())); button.setPosition(FlxG.width * .75, FlxG.height / 10); add(button); diff --git a/spine-haxe/example/src/flixelExamples/ControlBonesExample.hx b/spine-haxe/example/src/flixelExamples/ControlBonesExample.hx index 49c7c13d8..67cd1c547 100644 --- a/spine-haxe/example/src/flixelExamples/ControlBonesExample.hx +++ b/spine-haxe/example/src/flixelExamples/ControlBonesExample.hx @@ -65,8 +65,7 @@ class ControlBonesExample extends FlxState { var skeletonSprite = new SkeletonSprite(data, animationStateData); skeletonSprite.scaleX = .5; skeletonSprite.scaleY = .5; - var animation = skeletonSprite.state.setAnimationByName(0, "idle", true).animation; - skeletonSprite.setBoundingBox(animation); + skeletonSprite.state.setAnimationByName(0, "idle", true); skeletonSprite.screenCenter(); add(skeletonSprite); @@ -77,6 +76,8 @@ class ControlBonesExample extends FlxState { "front-leg-ik-target", ]; + // we need to update, to ensure scale is applied before getting bone values + skeletonSprite.update(0); var radius = 6; for (boneName in controlBoneNames) { var bone = skeletonSprite.skeleton.findBone(boneName); @@ -84,7 +85,7 @@ class ControlBonesExample extends FlxState { skeletonSprite.skeletonToHaxeWorldCoordinates(point); var control = new FlxSprite(); control.makeGraphic(radius * 2, radius * 2, FlxColor.TRANSPARENT, true); - FlxSpriteUtil.drawCircle(control, radius, radius, radius, 0xffff00ff); + FlxSpriteUtil.drawCircle(control, -1, -1, -1, 0xffff00ff); control.setPosition(point[0] - radius, point[1] - radius); controlBones.push(bone); controls.push(control); diff --git a/spine-haxe/example/src/flixelExamples/EventsExample.hx b/spine-haxe/example/src/flixelExamples/EventsExample.hx index 861ca315c..09a1ed18a 100644 --- a/spine-haxe/example/src/flixelExamples/EventsExample.hx +++ b/spine-haxe/example/src/flixelExamples/EventsExample.hx @@ -68,9 +68,6 @@ class EventsExample extends FlxState { skeletonSprite.state.setAnimationByName(0, "walk", true); var trackEntry = skeletonSprite.state.addAnimationByName(0, "run", true, 3); - skeletonSprite.setBoundingBox(trackEntry.animation); - - skeletonSprite.setBoundingBox(); skeletonSprite.screenCenter(); skeletonSprite.skeleton.setupPoseBones(); add(skeletonSprite); diff --git a/spine-haxe/example/src/flixelExamples/FlixelState.hx b/spine-haxe/example/src/flixelExamples/FlixelState.hx index eb3efe8ad..6e26439b3 100644 --- a/spine-haxe/example/src/flixelExamples/FlixelState.hx +++ b/spine-haxe/example/src/flixelExamples/FlixelState.hx @@ -30,6 +30,11 @@ package flixelExamples; import flixel.ui.FlxButton; +import flixel.math.FlxPoint; +import flixel.util.FlxColor; +import flixel.util.FlxSpriteUtil; +import spine.boundsprovider.SetupPoseBoundsProvider; +import spine.boundsprovider.SkinsAndAnimationBoundsProvider; import flixel.group.FlxSpriteGroup; import flixel.FlxSprite; import flixel.graphics.FlxGraphic; @@ -56,6 +61,11 @@ class FlixelState extends FlxState { var scale = 4; var speed:Float; + var skeletonOrigin:FlxSprite; + var gameObjectOrigin:FlxSprite; + var radius = 3; + var rootPoint = [.0, .0]; + override public function create():Void { FlxG.cameras.bgColor = 0xffa1b2b0; @@ -97,12 +107,37 @@ class FlixelState extends FlxState { // loading spineboy var atlas = new TextureAtlas(Assets.getText("assets/spineboy.atlas"), new FlixelTextureLoader("assets/spineboy.atlas")); - var skeletondata = SkeletonData.from(Assets.getText("assets/spineboy-pro.json"), atlas, 1 / scale); + var skeletondata = SkeletonData.from(Assets.getText("assets/spineboy-pro.json"), atlas, 1); + + // var atlas = new TextureAtlas(Assets.getText("assets/quad.atlas"), new FlixelTextureLoader("assets/quad.atlas")); + // var skeletondata = SkeletonData.from(Assets.getText("assets/quad.json"), atlas, 2); + var animationStateData = new AnimationStateData(skeletondata); - spineSprite = new SkeletonSprite(skeletondata, animationStateData); + // spineSprite = new SkeletonSprite(skeletondata, animationStateData, new SetupPoseBoundsProvider()); + + spineSprite = new SkeletonSprite(skeletondata, animationStateData, new SkinsAndAnimationBoundsProvider("walk")); + + spineSprite.scale = new FlxPoint(1 / scale, 1 / scale); // positioning spineboy - spineSprite.setPosition(.5 * FlxG.width, .5 * FlxG.height); + spineSprite.screenCenter(); + spineSprite.y += spineSprite.height / 2; + + var control3 = new FlxSprite(); + control3.makeGraphic(radius * 2, radius * 2, FlxColor.TRANSPARENT, true); + FlxSpriteUtil.drawCircle(control3, radius, radius, radius, 0xff5500ff); + add(control3); + control3.setPosition(FlxG.width / 2, FlxG.height / 2); + + skeletonOrigin = new FlxSprite(); + skeletonOrigin.makeGraphic(radius * 2, radius * 2, FlxColor.TRANSPARENT, true); + FlxSpriteUtil.drawCircle(skeletonOrigin, radius, radius, radius, 0xff5500ff); + add(skeletonOrigin); + + gameObjectOrigin = new FlxSprite(); + gameObjectOrigin.makeGraphic(radius * 2, radius * 2, FlxColor.TRANSPARENT, true); + FlxSpriteUtil.drawCircle(gameObjectOrigin, radius, radius, radius, 0xffff9100); + add(gameObjectOrigin); // setting mix times animationStateData.defaultMix = 0.5; @@ -119,7 +154,7 @@ class FlixelState extends FlxState { // setting idle animation spineSprite.state.setAnimationByName(0, "idle", true); - // setting y offset function to move object body while jumping + // // setting y offset function to move object body while jumping var hip = spineSprite.skeleton.findBone("hip"); var initialY = 0.; var initialOffsetY = 0.; @@ -137,13 +172,19 @@ class FlixelState extends FlxState { } }); var diff = .0; + var tmpPoint = [.0, .0]; spineSprite.afterUpdateWorldTransforms = spineSprite -> { if (jumping) { - diff -= hip.pose.y; - spineSprite.offsetY -= diff; - spineSprite.y += diff; + tmpPoint[1] = hip.applied.worldY; + spineSprite.skeletonToHaxeWorldCoordinates(tmpPoint); + diff -= (tmpPoint[1]); + spineSprite.offsetY += diff; + spineSprite.y -= diff; } - diff = hip.pose.y; + + tmpPoint[1] = hip.applied.worldY; + spineSprite.skeletonToHaxeWorldCoordinates(tmpPoint); + diff = tmpPoint[1]; } // adding spineboy to the stage @@ -152,7 +193,7 @@ class FlixelState extends FlxState { // FlxG.debugger.visible = !FlxG.debugger.visible; // debug ui // FlxG.debugger.visible = true; - // FlxG.debugger.drawDebug = true; + FlxG.debugger.drawDebug = true; // FlxG.log.redirectTraces = true; // FlxG.debugger.track(spineSprite); @@ -160,12 +201,14 @@ class FlixelState extends FlxState { // FlxG.watch.add(spineSprite, "offsetY"); // FlxG.watch.add(spineSprite, "y"); // FlxG.watch.add(this, "jumping"); + super.create(); } var justSetIdle = true; override public function update(elapsed:Float):Void { + // spineSprite.calculateBounds(); if (FlxG.overlap(spineSprite, group)) { myText.text = "Overlapping"; } else { @@ -184,6 +227,17 @@ class FlixelState extends FlxState { FlxG.debugger.visible = !FlxG.debugger.visible; } + if (FlxG.keys.anyPressed([UP, DOWN])) { + if (FlxG.keys.anyPressed([UP])) { + if (spineSprite.flipY == true) + spineSprite.flipY = false; + } + if (FlxG.keys.anyPressed([DOWN])) { + if (spineSprite.flipY == false) + spineSprite.flipY = true; + } + } + if (FlxG.keys.anyPressed([RIGHT, LEFT])) { justSetIdle = false; var flipped = false; @@ -217,6 +271,13 @@ class FlixelState extends FlxState { spineSprite.state.setAnimationByName(0, "idle", true); } + var rootBone = spineSprite.skeleton.findBone("root"); + rootPoint[0] = rootBone.applied.worldX; + rootPoint[1] = rootBone.applied.worldY; + spineSprite.skeletonToHaxeWorldCoordinates(rootPoint); + skeletonOrigin.setPosition(rootPoint[0] - radius, rootPoint[1] - radius); + gameObjectOrigin.setPosition(spineSprite.x - radius, spineSprite.y - radius); + super.update(elapsed); } } diff --git a/spine-haxe/example/src/flixelExamples/MixAndMatchExample.hx b/spine-haxe/example/src/flixelExamples/MixAndMatchExample.hx index 6024ef0a2..48c148761 100644 --- a/spine-haxe/example/src/flixelExamples/MixAndMatchExample.hx +++ b/spine-haxe/example/src/flixelExamples/MixAndMatchExample.hx @@ -29,6 +29,7 @@ package flixelExamples; +import spine.boundsprovider.SkinsAndAnimationBoundsProvider; import spine.Skin; import flixel.ui.FlxButton; import flixel.FlxG; @@ -59,7 +60,7 @@ class MixAndMatchExample extends FlxState { var animationStateData = new AnimationStateData(data); animationStateData.defaultMix = 0.25; - skeletonSprite = new SkeletonSprite(data, animationStateData); + skeletonSprite = new SkeletonSprite(data, animationStateData, new SkinsAndAnimationBoundsProvider("dance", ["full-skins/boy"])); var customSkin = new Skin("custom"); var skinBase = data.findSkin("skin-base"); customSkin.addSkin(skinBase); @@ -74,8 +75,7 @@ class MixAndMatchExample extends FlxState { skeletonSprite.skeleton.skin = customSkin; skeletonSprite.state.update(0); - var animation = skeletonSprite.state.setAnimationByName(0, "dance", true).animation; - skeletonSprite.setBoundingBox(animation); + skeletonSprite.state.setAnimationByName(0, "dance", true); skeletonSprite.screenCenter(); add(skeletonSprite); diff --git a/spine-haxe/example/src/flixelExamples/SequenceExample.hx b/spine-haxe/example/src/flixelExamples/SequenceExample.hx index 63b22766c..48fecf4d0 100644 --- a/spine-haxe/example/src/flixelExamples/SequenceExample.hx +++ b/spine-haxe/example/src/flixelExamples/SequenceExample.hx @@ -59,7 +59,6 @@ class SequenceExample extends FlxState { skeletonSprite = new SkeletonSprite(skeletondata, animationStateData); var animation = skeletonSprite.state.setAnimationByName(0, "flying", true).animation; - skeletonSprite.setBoundingBox(animation); skeletonSprite.screenCenter(); add(skeletonSprite); super.create(); diff --git a/spine-haxe/example/src/flixelExamples/TankExample.hx b/spine-haxe/example/src/flixelExamples/TankExample.hx index 39c57890a..447e4a6c1 100644 --- a/spine-haxe/example/src/flixelExamples/TankExample.hx +++ b/spine-haxe/example/src/flixelExamples/TankExample.hx @@ -57,7 +57,6 @@ class TankExample extends FlxState { var skeletonSprite = new SkeletonSprite(data, animationStateData); var animation = skeletonSprite.state.setAnimationByName(0, "drive", true).animation; - skeletonSprite.setBoundingBox(animation); skeletonSprite.screenCenter(); add(skeletonSprite); diff --git a/spine-haxe/example/src/flixelExamples/VineExample.hx b/spine-haxe/example/src/flixelExamples/VineExample.hx index 1c7431397..9f8d632aa 100644 --- a/spine-haxe/example/src/flixelExamples/VineExample.hx +++ b/spine-haxe/example/src/flixelExamples/VineExample.hx @@ -57,7 +57,6 @@ class VineExample extends FlxState { var skeletonSprite = new SkeletonSprite(data, animationStateData); var animation = skeletonSprite.state.setAnimationByName(0, "grow", true).animation; - skeletonSprite.setBoundingBox(animation); skeletonSprite.screenCenter(); add(skeletonSprite); diff --git a/spine-haxe/project.xml b/spine-haxe/project.xml index 6b4423862..d3fe1547e 100644 --- a/spine-haxe/project.xml +++ b/spine-haxe/project.xml @@ -2,9 +2,9 @@ - + - + diff --git a/spine-haxe/spine-haxe/spine/BoundsProvider/SetupPoseBoundsProvider.hx b/spine-haxe/spine-haxe/spine/BoundsProvider/SetupPoseBoundsProvider.hx index 22e40bf00..71156cfeb 100644 --- a/spine-haxe/spine-haxe/spine/BoundsProvider/SetupPoseBoundsProvider.hx +++ b/spine-haxe/spine-haxe/spine/BoundsProvider/SetupPoseBoundsProvider.hx @@ -45,8 +45,8 @@ class SetupPoseBoundsProvider extends BoundsProvider { } public function calculateBounds(gameObject:BoundsGameObject, out:BoundsRectangle):BoundsRectangle { - var skeleton = gameObject.skeleton; - if (skeleton == null) { + var prevSkeleton = gameObject.skeleton; + if (prevSkeleton == null) { zeroRectangle(out); return out; } @@ -54,7 +54,9 @@ class SetupPoseBoundsProvider extends BoundsProvider { // 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); + var skeleton = new Skeleton(prevSkeleton.data); + skeleton.scaleX = prevSkeleton.scaleX; + skeleton.scaleY = prevSkeleton.scaleY * Bone.yDir; skeleton.setupPose(); skeleton.updateWorldTransform(Physics.update); var newBounds = skeleton.getBounds(clipping ? new SkeletonClipping() : null); diff --git a/spine-haxe/spine-haxe/spine/BoundsProvider/SkinsAndAnimationBoundsProvider.hx b/spine-haxe/spine-haxe/spine/BoundsProvider/SkinsAndAnimationBoundsProvider.hx index 6f083e226..b300b240d 100644 --- a/spine-haxe/spine-haxe/spine/BoundsProvider/SkinsAndAnimationBoundsProvider.hx +++ b/spine-haxe/spine-haxe/spine/BoundsProvider/SkinsAndAnimationBoundsProvider.hx @@ -56,9 +56,9 @@ class SkinsAndAnimationBoundsProvider extends BoundsProvider { } public function calculateBounds(gameObject:BoundsGameObject, out:BoundsRectangle):BoundsRectangle { - var skeleton = gameObject.skeleton; + var prevSkeleton = gameObject.skeleton; var state = gameObject.state; - if (skeleton == null || state == null) { + if (prevSkeleton == null || state == null) { zeroRectangle(out); return out; } @@ -67,7 +67,9 @@ class SkinsAndAnimationBoundsProvider extends BoundsProvider { // 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 skeleton = new Skeleton(prevSkeleton.data); + skeleton.scaleX = prevSkeleton.scaleX; + skeleton.scaleY = prevSkeleton.scaleY * Bone.yDir; var clipper = clipping ? new SkeletonClipping() : null; var data = skeleton.data; if (skins.length > 0) { diff --git a/spine-haxe/spine-haxe/spine/flixel/SkeletonSprite.hx b/spine-haxe/spine-haxe/spine/flixel/SkeletonSprite.hx index e75fe107e..d2abe1e7c 100644 --- a/spine-haxe/spine-haxe/spine/flixel/SkeletonSprite.hx +++ b/spine-haxe/spine-haxe/spine/flixel/SkeletonSprite.hx @@ -29,42 +29,35 @@ package spine.flixel; +import flixel.util.FlxDirectionFlags; +import flixel.math.FlxRect; +import flixel.FlxCamera; +import flixel.FlxBasic; +import flixel.util.FlxAxes; +import flixel.group.FlxGroup.FlxTypedGroup; +import spine.boundsprovider.SetupPoseBoundsProvider; +import spine.boundsprovider.BoundsProvider; import openfl.geom.Point; import flixel.math.FlxPoint; import flixel.math.FlxMatrix; -import spine.animation.MixDirection; -import spine.animation.MixBlend; -import spine.animation.Animation; import spine.TextureRegion; -import haxe.extern.EitherType; -import spine.attachments.Attachment; -import flixel.util.typeLimit.OneOfTwo; -import flixel.FlxCamera; -import flixel.math.FlxRect; import flixel.FlxG; import flixel.FlxObject; -import flixel.FlxSprite; -import flixel.FlxStrip; -import flixel.group.FlxSpriteGroup; -import flixel.graphics.FlxGraphic; import flixel.util.FlxColor; import openfl.Vector; -import openfl.display.BlendMode; import spine.Bone; -import spine.Rectangle; import spine.Skeleton; import spine.SkeletonData; import spine.Slot; import spine.animation.AnimationState; import spine.animation.AnimationStateData; -import spine.atlas.TextureAtlasRegion; import spine.attachments.MeshAttachment; import spine.attachments.RegionAttachment; import spine.attachments.ClippingAttachment; import spine.flixel.SkeletonMesh; /** A FlxObject that draws a skeleton. The animation state and skeleton must be updated each frame. */ -class SkeletonSprite extends FlxObject { +class SkeletonSprite extends FlxTypedGroup { public var skeleton(default, null):Skeleton; public var state(default, null):AnimationState; public var stateData(default, null):AnimationStateData; @@ -81,6 +74,18 @@ class SkeletonSprite extends FlxObject { public var flipY(default, set):Bool = false; public var antialiasing:Bool = true; + public var boundsProvider:BoundsProvider; + + public var angle(default, set) = 0.; + public var x(default, set) = 0.; + public var y(default, set) = 0.; + public var width(get, set):Float; + public var height(get, set):Float; + public var boundsX(get, never):Float; + public var boundsY(get, never):Float; + + @:isVar + public var scale(never, set):FlxPoint; @:isVar public var scaleX(get, set):Float = 1; @:isVar @@ -92,103 +97,50 @@ class SkeletonSprite extends FlxObject { private var _tempMatrix = new FlxMatrix(); private var _tempPoint = new Point(); + private var _tempPointFlip = [.0, .0]; + private var __bounds = new openfl.geom.Rectangle(); + private var __objectBounds = new FlxObject(); private static var QUAD_INDICES:Array = [0, 1, 2, 2, 3, 0]; /** Creates an uninitialized SkeletonSprite. The renderer, skeleton, and animation state must be set before use. */ - public function new(skeletonData:SkeletonData, animationStateData:AnimationStateData = null) { - super(0, 0); + public function new(skeletonData:SkeletonData, animationStateData:AnimationStateData = null, ?boundsProvider:BoundsProvider) { + super(1); Bone.yDown = true; skeleton = new Skeleton(skeletonData); skeleton.updateWorldTransform(Physics.update); state = new AnimationState(animationStateData != null ? animationStateData : new AnimationStateData(skeletonData)); - setBoundingBox(); + // setBoundingBox(); + this.boundsProvider = boundsProvider ?? new SetupPoseBoundsProvider(); + this.calculateBounds(); + add(__objectBounds); } - public function setBoundingBox(?animation:Animation, ?clip:Bool = true) { - var bounds = animation == null ? skeleton.getBounds() : getAnimationBounds(animation, clip); - if (bounds.width > 0 && bounds.height > 0) { - width = bounds.width; - height = bounds.height; - offsetX = -bounds.x; - offsetY = -bounds.y; - } + // TODO: this changes the scale + // public function setSize(width:Float, height:Float):Void { + // this.width = width; + // this.height = height; + // } + // ============================================================ + // DEBUG METHODS (if FLX_DEBUG) + // ============================================================ + #if FLX_DEBUG + public function drawDebug():Void { + __objectBounds.drawDebug(); } - 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 function drawDebugOnCamera(camera:FlxCamera):Void { + __objectBounds.drawDebugOnCamera(camera); } + #end - override public function destroy():Void { - state.clearListeners(); - state = null; - skeleton = null; - - _tempVertices = null; - _quadTriangles = null; - _tempMatrix = null; - _tempPoint = null; - - if (_meshes != null) { - for (mesh in _meshes) - mesh.destroy(); - _meshes = null; - } - - super.destroy(); - } - - override public function update(elapsed:Float):Void { - super.update(elapsed); - state.update(elapsed); - state.apply(skeleton); - this.beforeUpdateWorldTransforms(this); - skeleton.update(elapsed); - skeleton.updateWorldTransform(Physics.update); - this.afterUpdateWorldTransforms(this); - } - - override public function draw():Void { - if (alpha == 0) - return; - - renderMeshes(); - - #if FLX_DEBUG - if (FlxG.debugger.drawDebug) - drawDebug(); - #end + // ============================================================ + // SKELETON SPRITE METHODS + // ============================================================ + public function calculateBounds() { + this.boundsProvider.calculateBounds(this, __bounds); + __objectBounds.setPosition(x + __bounds.x, y + __bounds.y); + __objectBounds.setSize(__bounds.width, __bounds.height); } function renderMeshes():Void { @@ -320,7 +272,7 @@ class SkeletonSprite extends FlxObject { private function getTransformMatrix():FlxMatrix { _tempMatrix.identity(); // scale is connected to the skeleton scale - no need to rescale - _tempMatrix.scale(1, 1); + // _tempMatrix.scale(1, 1); _tempMatrix.rotate(angle * Math.PI / 180); _tempMatrix.translate(x + offsetX, y + offsetY); return _tempMatrix; @@ -375,18 +327,24 @@ class SkeletonSprite extends FlxObject { } function set_flipX(value:Bool):Bool { - if (value != flipX) + if (value != flipX) { skeleton.scaleX = -skeleton.scaleX; + this.calculateBounds(); + } return flipX = value; } function set_flipY(value:Bool):Bool { - if (value != flipY) - skeleton.scaleY = -skeleton.scaleY; + if (value != flipY) { + skeleton.scaleY = -skeleton.scaleY * Bone.yDir; + this.calculateBounds(); + } return flipY = value; } function set_scale(value:FlxPoint):FlxPoint { + scaleX = value.x; + scaleY = value.y; return value; } @@ -395,15 +353,199 @@ class SkeletonSprite extends FlxObject { } function set_scaleX(value:Float):Float { - return skeleton.scaleX = value; + skeleton.scaleX = value; + this.calculateBounds(); + return value; } function get_scaleY():Float { - return skeleton.scaleY; + return skeleton.scaleY * Bone.yDir; } function set_scaleY(value:Float):Float { - return skeleton.scaleY = value; + skeleton.scaleY = value; + this.calculateBounds(); + return value; + } + + function set_angle(value:Float):Float { + __objectBounds.angle = value; + return angle = value; + } + + function set_x(value:Float):Float { + __objectBounds.x = __bounds.x + value; + return x = value; + } + + function set_y(value:Float):Float { + __objectBounds.y = __bounds.y + value; + return y = value; + } + + function get_height():Float { + return __bounds.height; + } + + function get_width():Float { + return __bounds.width; + } + + function set_width(value:Float):Float { + var scale = value / __bounds.width; + scaleX *= scale; + return __bounds.width; + } + + function set_height(value:Float):Float { + var scale = value / __bounds.height; + scaleY *= scale; + return __bounds.height; + } + + function get_boundsX():Float { + return __objectBounds.x; + } + + function get_boundsY():Float { + return __objectBounds.y; + } + + // ============================================================ + // OVERRIDE METHODS FROM FlxBasic + // ============================================================ + + override public function update(elapsed:Float):Void { + super.update(elapsed); + state.update(elapsed); + state.apply(skeleton); + this.beforeUpdateWorldTransforms(this); + skeleton.update(elapsed); + skeleton.updateWorldTransform(Physics.update); + this.afterUpdateWorldTransforms(this); + } + + override public function draw():Void { + if (alpha == 0) + return; + + renderMeshes(); + + #if FLX_DEBUG + if (FlxG.debugger.drawDebug) + __objectBounds.drawDebug(); + #end + } + + override public function destroy():Void { + state.clearListeners(); + state = null; + skeleton = null; + + _tempVertices = null; + _quadTriangles = null; + _tempMatrix = null; + _tempPoint = null; + + if (_meshes != null) { + for (mesh in _meshes) + mesh.destroy(); + _meshes = null; + } + + super.destroy(); + } + + // ============================================================ + // OVERLAP/COLLISION METHODS + // ============================================================ + + public function overlaps(objectOrGroup:FlxBasic, inScreenSpace:Bool = false, ?camera:FlxCamera):Bool { + return __objectBounds.overlaps(objectOrGroup, inScreenSpace, camera); + } + + public function overlapsAt(x:Float, y:Float, objectOrGroup:FlxBasic, inScreenSpace = false, ?camera:FlxCamera):Bool { + return __objectBounds.overlapsAt(x, y, objectOrGroup, inScreenSpace, camera); + } + + public function overlapsPoint(point:FlxPoint, inScreenSpace = false, ?camera:FlxCamera):Bool { + return __objectBounds.overlapsPoint(point, inScreenSpace, camera); + } + + // ============================================================ + // BOUNDS/POSITION METHODS + // ============================================================ + + public function inWorldBounds():Bool { + return __objectBounds.inWorldBounds(); + } + + public function getScreenPosition(?result:FlxPoint, ?camera:FlxCamera):FlxPoint { + return __objectBounds.getScreenPosition(result, camera); + } + + public function getPosition(?result:FlxPoint):FlxPoint { + return __objectBounds.getPosition(result); + } + + public function getMidpoint(?point:FlxPoint):FlxPoint { + return __objectBounds.getMidpoint(point); + } + + public function getHitbox(?rect:FlxRect):FlxRect { + return __objectBounds.getHitbox(rect); + } + + public function getRotatedBounds(?newRect:FlxRect):FlxRect { + return __objectBounds.getRotatedBounds(newRect); + } + + // ============================================================ + // STATE METHODS + // ============================================================ + + public function reset(x:Float, y:Float):Void { + __objectBounds.reset(x, y); + } + + public function isOnScreen(?camera:FlxCamera):Bool { + return __objectBounds.isOnScreen(camera); + } + + public function isPixelPerfectRender(?camera:FlxCamera):Bool { + return __objectBounds.isPixelPerfectRender(camera); + } + + public function isTouching(direction:FlxDirectionFlags):Bool { + return __objectBounds.isTouching(direction); + } + + public function justTouched(direction:FlxDirectionFlags):Bool { + return __objectBounds.justTouched(direction); + } + + // ============================================================ + // UTILITY METHODS + // ============================================================ + + public inline function screenCenter(axes:FlxAxes = XY):SkeletonSprite { + if (axes.x) + x = (FlxG.width - __bounds.width) / 2 - __bounds.x; + + if (axes.y) + y = (FlxG.height - __bounds.height) / 2 - __bounds.y; + + return this; + } + + public function setPosition(x = 0.0, y = 0.0):Void { + this.x = x; + this.y = y; + } + + public function setSize(width:Float, height:Float):Void { + this.width = width; + this.height = height; } } diff --git a/spine-ts/spine-pixi-v8/example/control-bones-example.html b/spine-ts/spine-pixi-v8/example/control-bones-example.html index 4b8eb6873..9477c1a69 100644 --- a/spine-ts/spine-pixi-v8/example/control-bones-example.html +++ b/spine-ts/spine-pixi-v8/example/control-bones-example.html @@ -46,9 +46,13 @@ // Create the spine display object const stretchyman = spine.Spine.from({skeleton: "stretchymanData", atlas: "stretchymanAtlas", - scale: 0.75, + scale: 1, + // scale: 0.75, }); + stretchyman.skeleton.scaleX = .5; + stretchyman.skeleton.scaleY = 1; + // Set the default mix time to use when transitioning // from one animation to the next. stretchyman.state.data.defaultMix = 0.2;