mirror of
https://github.com/EsotericSoftware/spine-runtimes.git
synced 2026-03-26 22:49:01 +08:00
511 lines
17 KiB
Haxe
511 lines
17 KiB
Haxe
/******************************************************************************
|
|
* 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;
|
|
|
|
import spine.Rectangle;
|
|
import spine.attachments.Attachment;
|
|
import spine.attachments.ClippingAttachment;
|
|
import spine.attachments.MeshAttachment;
|
|
import spine.attachments.RegionAttachment;
|
|
|
|
/** Stores the current pose for a skeleton.
|
|
*
|
|
* @see https://esotericsoftware.com/spine-runtime-architecture#Instance-objects Instance objects in the Spine Runtimes Guide
|
|
*/
|
|
class Skeleton {
|
|
private static var quadTriangles:Array<Int> = [0, 1, 2, 2, 3, 0];
|
|
|
|
/** The skeleton's setup pose data. */
|
|
public final data:SkeletonData;
|
|
|
|
/** The skeleton's bones, sorted parent first. The root bone is always the first bone. */
|
|
public final bones:Array<Bone>;
|
|
|
|
/** The skeleton's slots. */
|
|
public final slots:Array<Slot>; // Setup pose draw order.
|
|
|
|
/** The skeleton's slots in the order they should be drawn. The returned array may be modified to change the draw order. */
|
|
public var drawOrder:Array<Slot>;
|
|
|
|
/** The skeleton's constraints. */
|
|
public final constraints:Array<Constraint<Dynamic, Dynamic, Dynamic>>;
|
|
|
|
/** The skeleton's physics constraints. */
|
|
public final physics:Array<PhysicsConstraint>;
|
|
|
|
/** The list of bones and constraints, sorted in the order they should be updated, as computed by Skeleton.updateCache(). */
|
|
public final _updateCache = new Array<Dynamic>();
|
|
|
|
private final resetCache = new Array<Posed<Dynamic, Dynamic, Dynamic>>();
|
|
|
|
/** The skeleton's current skin. */
|
|
public var skin(default, set):Skin = null;
|
|
|
|
/** The color to tint all the skeleton's attachments. */
|
|
public final color:Color;
|
|
|
|
/** Sets the skeleton X position, which is added to the root bone worldX position.
|
|
*
|
|
* Bones that do not inherit translation are still affected by this property. */
|
|
public var x:Float = 0;
|
|
|
|
/** Sets the skeleton Y position, which is added to the root bone worldY position.
|
|
*
|
|
* Bones that do not inherit translation are still affected by this property. */
|
|
public var y:Float = 0;
|
|
|
|
/** Scales the entire skeleton on the X axis.
|
|
*
|
|
* Bones that do not inherit scale are still affected by this property. */
|
|
public var scaleX:Float = 1;
|
|
|
|
/** Scales the entire skeleton on the Y axis.
|
|
*
|
|
* Bones that do not inherit scale are still affected by this property. */
|
|
public var scaleY(get, default):Float = 1;
|
|
|
|
function get_scaleY() {
|
|
return scaleY * Bone.yDir;
|
|
}
|
|
|
|
/** Returns the skeleton's time. This is used for time-based manipulations, such as spine.PhysicsConstraint.
|
|
*
|
|
* See Skeleton.update(). */
|
|
public var time:Float = 0;
|
|
|
|
public var windX:Float = 1;
|
|
public var windY:Float = 0;
|
|
public var gravityX:Float = 0;
|
|
public var gravityY:Float = 1;
|
|
|
|
public var _update = 0;
|
|
|
|
/** Creates a new skeleton with the specified skeleton data. */
|
|
public function new(data:SkeletonData) {
|
|
if (data == null)
|
|
throw new SpineException("data cannot be null.");
|
|
this.data = data;
|
|
|
|
bones = new Array<Bone>();
|
|
for (boneData in data.bones) {
|
|
var bone:Bone;
|
|
if (boneData.parent == null) {
|
|
bone = new Bone(boneData, null);
|
|
} else {
|
|
var parent:Bone = bones[boneData.parent.index];
|
|
bone = new Bone(boneData, parent);
|
|
parent.children.push(bone);
|
|
}
|
|
bones.push(bone);
|
|
}
|
|
|
|
slots = new Array<Slot>();
|
|
drawOrder = new Array<Slot>();
|
|
for (slotData in data.slots) {
|
|
var slot = new Slot(slotData, this);
|
|
slots.push(slot);
|
|
drawOrder.push(slot);
|
|
}
|
|
|
|
physics = new Array<PhysicsConstraint>();
|
|
constraints = new Array<Constraint<Dynamic, Dynamic, Dynamic>>();
|
|
for (constraintData in data.constraints) {
|
|
var constraint = constraintData.create(this);
|
|
if (Std.isOfType(constraint, PhysicsConstraint))
|
|
physics.push(cast(constraint, PhysicsConstraint));
|
|
constraints.push(constraint);
|
|
}
|
|
|
|
color = new Color(1, 1, 1, 1);
|
|
|
|
updateCache();
|
|
}
|
|
|
|
/** Caches information about bones and constraints. Must be called if the Skeleton.skin is modified or if bones,
|
|
* constraints, or weighted path attachments are added or removed. */
|
|
public function updateCache():Void {
|
|
_updateCache.resize(0);
|
|
resetCache.resize(0);
|
|
|
|
for (slot in slots)
|
|
slot.usePose();
|
|
|
|
for (bone in bones) {
|
|
bone.sorted = bone.data.skinRequired;
|
|
bone.active = !bone.sorted;
|
|
bone.usePose();
|
|
}
|
|
|
|
if (skin != null) {
|
|
var skinBones = skin.bones;
|
|
for (i in 0...skin.bones.length) {
|
|
var bone:Bone = bones[skinBones[i].index];
|
|
do {
|
|
bone.sorted = false;
|
|
bone.active = true;
|
|
bone = bone.parent;
|
|
} while (bone != null);
|
|
}
|
|
}
|
|
|
|
for (constraint in constraints)
|
|
constraint.usePose();
|
|
for (c in constraints) {
|
|
var constraint:Constraint<Dynamic, Dynamic, Dynamic> = c;
|
|
constraint.active = constraint.isSourceActive()
|
|
&& (!constraint.data.skinRequired || (skin != null && contains(skin.constraints, cast constraint.data)));
|
|
if (constraint.active)
|
|
constraint.sort(this);
|
|
}
|
|
|
|
for (bone in bones)
|
|
sortBone(bone);
|
|
|
|
var updateCache = this._updateCache;
|
|
var n = updateCache.length;
|
|
for (i in 0...n) {
|
|
var updatable = updateCache[i];
|
|
if (Std.isOfType(updatable, Bone)) {
|
|
var b:Bone = cast updatable;
|
|
updateCache[i] = b.applied;
|
|
}
|
|
}
|
|
}
|
|
|
|
private static function contains(list:Array<ConstraintData<Dynamic, Dynamic>>, element:ConstraintData<Dynamic, Dynamic>):Bool {
|
|
return list.indexOf(element) != -1;
|
|
}
|
|
|
|
public function constrained(object:Posed<Dynamic, Dynamic, Dynamic>) {
|
|
if (object.pose == object.applied) {
|
|
object.useConstrained();
|
|
resetCache.push(object);
|
|
}
|
|
}
|
|
|
|
public function sortBone(bone:Bone):Void {
|
|
if (bone.sorted || !bone.active)
|
|
return;
|
|
var parent = bone.parent;
|
|
if (parent != null)
|
|
sortBone(parent);
|
|
bone.sorted = true;
|
|
_updateCache.push(bone);
|
|
}
|
|
|
|
public function sortReset(bones:Array<Bone>):Void {
|
|
for (bone in bones) {
|
|
if (bone.active) {
|
|
if (bone.sorted)
|
|
sortReset(bone.children);
|
|
bone.sorted = false;
|
|
}
|
|
}
|
|
}
|
|
|
|
/** Updates the world transform for each bone and applies all constraints.
|
|
*
|
|
* @see https://esotericsoftware.com/spine-runtime-skeletons#World-transforms World transforms in the Spine Runtimes Guide
|
|
*/
|
|
public function updateWorldTransform(physics:Physics):Void {
|
|
_update++;
|
|
|
|
for (resetable in resetCache)
|
|
resetable.resetConstrained();
|
|
|
|
for (updatable in _updateCache)
|
|
updatable.update(this, physics);
|
|
}
|
|
|
|
/** Sets the bones, constraints, slots, and draw order to their setup pose values. */
|
|
public function setupPose():Void {
|
|
setupPoseBones();
|
|
setupPoseSlots();
|
|
}
|
|
|
|
/** Sets the bones and constraints to their setup pose values. */
|
|
public function setupPoseBones():Void {
|
|
for (bone in this.bones)
|
|
bone.setupPose();
|
|
for (constraint in this.constraints)
|
|
constraint.setupPose();
|
|
}
|
|
|
|
/** Sets the slots and draw order to their setup pose values. */
|
|
public function setupPoseSlots():Void {
|
|
var i:Int = 0;
|
|
for (slot in slots) {
|
|
drawOrder[i++] = slot;
|
|
slot.setupPose();
|
|
}
|
|
}
|
|
|
|
/** Returns the root bone, or null if the skeleton has no bones. */
|
|
public var rootBone(get, never):Bone;
|
|
|
|
private function get_rootBone():Bone {
|
|
return bones.length == 0 ? null : bones[0];
|
|
}
|
|
|
|
/** Finds a bone by comparing each bone's name. It is more efficient to cache the results of this method than to call it
|
|
* repeatedly. */
|
|
public function findBone(boneName:String):Bone {
|
|
if (boneName == null)
|
|
throw new SpineException("boneName cannot be null.");
|
|
for (bone in bones)
|
|
if (bone.data.name == boneName)
|
|
return bone;
|
|
return null;
|
|
}
|
|
|
|
/** @return -1 if the bone was not found. */
|
|
public function findBoneIndex(boneName:String):Int {
|
|
if (boneName == null)
|
|
throw new SpineException("boneName cannot be null.");
|
|
var i:Int = 0;
|
|
for (bone in bones) {
|
|
if (bone.data.name == boneName)
|
|
return i;
|
|
i++;
|
|
}
|
|
return -1;
|
|
}
|
|
|
|
/** Finds a slot by comparing each slot's name. It is more efficient to cache the results of this method than to call it
|
|
* repeatedly. */
|
|
public function findSlot(slotName:String):Slot {
|
|
if (slotName == null)
|
|
throw new SpineException("slotName cannot be null.");
|
|
for (slot in slots)
|
|
if (slot.data.name == slotName)
|
|
return slot;
|
|
return null;
|
|
}
|
|
|
|
/** The skeleton's current skin. */
|
|
public var skinName(get, set):String;
|
|
|
|
/** Sets a skin by name.
|
|
*
|
|
* See Skeleton.skin. */
|
|
private function set_skinName(skinName:String):String {
|
|
var skin:Skin = data.findSkin(skinName);
|
|
if (skin == null)
|
|
throw new SpineException("Skin not found: " + skinName);
|
|
this.skin = skin;
|
|
return skinName;
|
|
}
|
|
|
|
/** @return May be null. */
|
|
private function get_skinName():String {
|
|
return skin == null ? null : skin.name;
|
|
}
|
|
|
|
/** Sets the skin used to look up attachments before looking in the spine.SkeletonData default skin. If the
|
|
* skin is changed, Skeleton.updateCache() is called.
|
|
*
|
|
* Attachments from the new skin are attached if the corresponding attachment from the old skin was attached. If there was no
|
|
* old skin, each slot's setup mode attachment is attached from the new skin.
|
|
*
|
|
* After changing the skin, the visible attachments can be reset to those attached in the setup pose by calling
|
|
* Skeleton.setSlotsToSetupPose(). Also, often spine.AnimationState.apply() is called before the next time the
|
|
* skeleton is rendered to allow any attachment keys in the current animation(s) to hide or show attachments from the new
|
|
* skin. */
|
|
private function set_skin(newSkin:Skin):Skin {
|
|
if (newSkin == skin)
|
|
return null;
|
|
if (newSkin != null) {
|
|
if (skin != null) {
|
|
newSkin.attachAll(this, skin);
|
|
} else {
|
|
var i:Int = 0;
|
|
for (slot in slots) {
|
|
var name:String = slot.data.attachmentName;
|
|
if (name != null) {
|
|
var attachment:Attachment = newSkin.getAttachment(i, name);
|
|
if (attachment != null)
|
|
slot.pose.attachment = attachment;
|
|
}
|
|
i++;
|
|
}
|
|
}
|
|
}
|
|
skin = newSkin;
|
|
updateCache();
|
|
return skin;
|
|
}
|
|
|
|
/** Finds an attachment by looking in the Skeleton.skin and spine.SkeletonData defaultSkin using the slot name and attachment
|
|
* name.
|
|
*
|
|
* See Skeleton.getAttachmentForSlotIndex(). */
|
|
public function getAttachmentForSlotName(slotName:String, attachmentName:String):Attachment {
|
|
return getAttachmentForSlotIndex(data.findSlot(slotName).index, attachmentName);
|
|
}
|
|
|
|
/** Finds an attachment by looking in the Skeleton.skin and spine.SkeletonData defaultSkin using the slot index and
|
|
* attachment name. First the skin is checked and if the attachment was not found, the default skin is checked.
|
|
*
|
|
* @see https://esotericsoftware.com/spine-runtime-skins Runtime skins in the Spine Runtimes Guide
|
|
*/
|
|
public function getAttachmentForSlotIndex(slotIndex:Int, attachmentName:String):Attachment {
|
|
if (attachmentName == null)
|
|
throw new SpineException("attachmentName cannot be null.");
|
|
if (skin != null) {
|
|
var attachment:Attachment = skin.getAttachment(slotIndex, attachmentName);
|
|
if (attachment != null)
|
|
return attachment;
|
|
}
|
|
if (data.defaultSkin != null)
|
|
return data.defaultSkin.getAttachment(slotIndex, attachmentName);
|
|
return null;
|
|
}
|
|
|
|
/** A convenience method to set an attachment by finding the slot with Skeleton.findSlot(), finding the attachment with
|
|
* Skeleton.getAttachmentForSlotIndex(), then setting the slot's spine.Slot attachment.
|
|
* @param attachmentName May be null to clear the slot's attachment. */
|
|
public function setAttachment(slotName:String, attachmentName:String):Void {
|
|
if (slotName == null)
|
|
throw new SpineException("slotName cannot be null.");
|
|
var i:Int = 0;
|
|
for (slot in slots) {
|
|
if (slot.data.name == slotName) {
|
|
var attachment:Attachment = null;
|
|
if (attachmentName != null) {
|
|
attachment = getAttachmentForSlotIndex(i, attachmentName);
|
|
if (attachment == null) {
|
|
throw new SpineException("Attachment not found: " + attachmentName + ", for slot: " + slotName);
|
|
}
|
|
}
|
|
slot.pose.attachment = attachment;
|
|
return;
|
|
}
|
|
i++;
|
|
}
|
|
throw new SpineException("Slot not found: " + slotName);
|
|
}
|
|
|
|
public function findConstraint<T:Constraint<Dynamic, Dynamic, Dynamic>>(constraintName:String, type:Class<T>):Null<T> {
|
|
if (constraintName == null)
|
|
throw new SpineException("constraintName cannot be null.");
|
|
if (type == null)
|
|
throw new SpineException("type cannot be null.");
|
|
for (constraint in constraints)
|
|
if (Std.isOfType(constraint, type) && constraint.data.name == constraintName)
|
|
return Std.downcast(constraint, type);
|
|
return null;
|
|
}
|
|
|
|
private var _tempVertices = new Array<Float>();
|
|
private var _bounds = new Rectangle();
|
|
|
|
/** Returns the axis aligned bounding box (AABB) of the region and mesh attachments for the current pose. Optionally applies
|
|
* clipping. */
|
|
public function getBounds(clipper:SkeletonClipping = null):Rectangle {
|
|
var minX = Math.POSITIVE_INFINITY;
|
|
var minY = Math.POSITIVE_INFINITY;
|
|
var maxX = Math.NEGATIVE_INFINITY;
|
|
var maxY = Math.NEGATIVE_INFINITY;
|
|
for (slot in drawOrder) {
|
|
var verticesLength:Int = 0;
|
|
var vertices:Array<Float> = null;
|
|
var triangles:Array<Int> = null;
|
|
var attachment:Attachment = slot.pose.attachment;
|
|
if (attachment != null) {
|
|
if (Std.isOfType(attachment, RegionAttachment)) {
|
|
verticesLength = 8;
|
|
_tempVertices.resize(verticesLength);
|
|
vertices = _tempVertices;
|
|
var region:RegionAttachment = cast(attachment, RegionAttachment);
|
|
region.computeWorldVertices(slot, region.getOffsets(slot.applied), vertices, 0, 2);
|
|
triangles = Skeleton.quadTriangles;
|
|
} else if (Std.isOfType(attachment, MeshAttachment)) {
|
|
var mesh:MeshAttachment = cast(attachment, MeshAttachment);
|
|
verticesLength = mesh.worldVerticesLength;
|
|
_tempVertices.resize(verticesLength);
|
|
vertices = _tempVertices;
|
|
mesh.computeWorldVertices(this, slot, 0, verticesLength, vertices, 0, 2);
|
|
triangles = mesh.triangles;
|
|
} else if (Std.isOfType(attachment, ClippingAttachment) && clipper != null) {
|
|
clipper.clipEnd(slot);
|
|
clipper.clipStart(this, slot, cast(attachment, ClippingAttachment));
|
|
continue;
|
|
}
|
|
if (vertices != null) {
|
|
if (clipper != null && clipper.isClipping() && clipper.clipTriangles(vertices, triangles, triangles.length)) {
|
|
vertices = clipper.clippedVertices;
|
|
verticesLength = clipper.clippedVertices.length;
|
|
}
|
|
var ii:Int = 0;
|
|
var nn:Int = vertices.length;
|
|
while (ii < nn) {
|
|
var x:Float = vertices[ii], y:Float = vertices[ii + 1];
|
|
minX = Math.min(minX, x);
|
|
minY = Math.min(minY, y);
|
|
maxX = Math.max(maxX, x);
|
|
maxY = Math.max(maxY, y);
|
|
ii += 2;
|
|
}
|
|
}
|
|
if (clipper != null)
|
|
clipper.clipEnd(slot);
|
|
}
|
|
}
|
|
if (clipper != null)
|
|
clipper.clipEnd();
|
|
_bounds.x = minX;
|
|
_bounds.y = minY;
|
|
_bounds.width = maxX - minX;
|
|
_bounds.height = maxY - minY;
|
|
return _bounds;
|
|
}
|
|
|
|
/** Increments the skeleton's Skeleton.time. */
|
|
public function update(delta:Float):Void {
|
|
time += delta;
|
|
}
|
|
|
|
/** Calls spine.PhysicsConstraint.translate() for each physics constraint. */
|
|
public function physicsTranslate(x:Float, y:Float):Void {
|
|
for (physicsConstraint in physics)
|
|
physicsConstraint.translate(x, y);
|
|
}
|
|
|
|
/** Calls spine.PhysicsConstraint.rotate() for each physics constraint. */
|
|
public function physicsRotate(x:Float, y:Float, degrees:Float):Void {
|
|
for (physicsConstraint in physics)
|
|
physicsConstraint.rotate(x, y, degrees);
|
|
}
|
|
|
|
public function toString():String {
|
|
return data.name != null ? data.name : "Skeleton?";
|
|
}
|
|
}
|