Mario Zechner c79031cc75 Remove npx install prompts and rely on auto-download
- Use npx -y for tsx to avoid install prompts
- Remove tsx from devDependencies since we use npx
- Remove npm install checks from format-ts.sh
2025-07-16 05:05:33 +02:00

599 lines
23 KiB
C#

/******************************************************************************
* 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.
*****************************************************************************/
#if UNITY_5_3_OR_NEWER
#define IS_UNITY
#endif
using System;
namespace Spine {
#if IS_UNITY
using Color32F = UnityEngine.Color;
#endif
public class Skeleton {
static private readonly int[] quadTriangles = { 0, 1, 2, 2, 3, 0 };
internal SkeletonData data;
internal ExposedList<Bone> bones;
internal ExposedList<Slot> slots;
internal ExposedList<Slot> drawOrder;
internal ExposedList<IConstraint> constraints;
internal ExposedList<PhysicsConstraint> physics;
internal ExposedList<object> updateCache = new ExposedList<object>();
internal ExposedList<IPosedInternal> resetCache = new ExposedList<IPosedInternal>(16);
internal Skin skin;
// Color is a struct, set to protected to prevent
// Color32F color = slot.color; color.a = 0.5;
// modifying just a copy of the struct instead of the original
// object as in reference implementation.
protected Color32F color;
internal float x, y, scaleX = 1, time, windX = 1, windY = 0, gravityX = 0, gravityY = 1;
/// <summary>Private to enforce usage of ScaleY getter taking Bone.yDown into account.</summary>
private float scaleY = 1;
internal int update;
public Skeleton (SkeletonData data) {
if (data == null) throw new ArgumentNullException("data", "data cannot be null.");
this.data = data;
bones = new ExposedList<Bone>(data.bones.Count);
Bone[] bonesItems = this.bones.Items;
foreach (BoneData boneData in data.bones) {
Bone bone;
if (boneData.parent == null) {
bone = new Bone(boneData, null);
} else {
Bone parent = bonesItems[boneData.parent.index];
bone = new Bone(boneData, parent);
parent.children.Add(bone);
}
this.bones.Add(bone);
}
slots = new ExposedList<Slot>(data.slots.Count);
drawOrder = new ExposedList<Slot>(data.slots.Count);
foreach (SlotData slotData in data.slots) {
Bone bone = bonesItems[slotData.boneData.index];
Slot slot = new Slot(slotData, this);
slots.Add(slot);
drawOrder.Add(slot);
}
physics = new ExposedList<PhysicsConstraint>(8);
constraints = new ExposedList<IConstraint>(data.constraints.Count);
foreach (IConstraintData constraintData in data.constraints) {
IConstraint constraint = constraintData.Create(this);
PhysicsConstraint physicsConstraint = constraint as PhysicsConstraint;
if (physicsConstraint != null) physics.Add(physicsConstraint);
constraints.Add(constraint);
}
physics.TrimExcess();
color = new Color32F(1, 1, 1, 1);
UpdateCache();
}
/// <summary>Copy constructor.</summary>
public Skeleton (Skeleton skeleton) {
if (skeleton == null) throw new ArgumentNullException("skeleton", "skeleton cannot be null.");
data = skeleton.data;
bones = new ExposedList<Bone>(skeleton.bones.Count);
foreach (Bone bone in skeleton.bones) {
Bone newBone;
if (bone.parent == null)
newBone = new Bone(bone, null);
else {
Bone parent = bones.Items[bone.parent.data.index];
newBone = new Bone(bone, parent);
parent.children.Add(newBone);
}
bones.Add(newBone);
}
slots = new ExposedList<Slot>(skeleton.slots.Count);
Bone[] bonesItems = bones.Items;
foreach (Slot slot in skeleton.slots)
slots.Add(new Slot(slot, bonesItems[slot.bone.data.index], this));
drawOrder = new ExposedList<Slot>(slots.Count);
Slot[] slotsItems = slots.Items;
foreach (Slot slot in skeleton.drawOrder)
drawOrder.Add(slotsItems[slot.data.index]);
physics = new ExposedList<PhysicsConstraint>(skeleton.physics.Count);
constraints = new ExposedList<IConstraint>(skeleton.constraints.Count);
foreach (IConstraint other in skeleton.constraints) {
IConstraint constraint = other.Copy(this);
PhysicsConstraint physicsConstraint = constraint as PhysicsConstraint;
if (physicsConstraint != null) physics.Add(physicsConstraint);
constraints.Add(constraint);
}
skin = skeleton.skin;
color = skeleton.color;
x = skeleton.x;
y = skeleton.y;
scaleX = skeleton.scaleX;
scaleY = skeleton.scaleY;
time = skeleton.time;
UpdateCache();
}
/// <summary>Caches information about bones and constraints. Must be called if the <see cref="Skin"/> is modified or if bones, constraints, or
/// constraints, or weighted path attachments are added or removed.</summary>
public void UpdateCache () {
updateCache.Clear();
resetCache.Clear();
Slot[] slots = this.slots.Items;
for (int i = 0, n = this.slots.Count; i < n; i++) {
((IPosedInternal)slots[i]).UsePose();
}
int boneCount = this.bones.Count;
Bone[] bones = this.bones.Items;
for (int i = 0; i < boneCount; i++) {
Bone bone = bones[i];
bone.sorted = bone.data.skinRequired;
bone.active = !bone.sorted;
((IPosedInternal)bone).UsePose();
}
if (skin != null) {
BoneData[] skinBones = skin.bones.Items;
for (int i = 0, n = skin.bones.Count; i < n; i++) {
Bone bone = bones[skinBones[i].index];
do {
bone.sorted = false;
bone.active = true;
bone = bone.parent;
} while (bone != null);
}
}
IConstraint[] constraints = this.constraints.Items;
{ // scope added to prevent compile error of n already being declared in enclosing scope
int n = this.constraints.Count;
for (int i = 0; i < n; i++) {
((IPosedInternal)constraints[i]).UsePose();
}
for (int i = 0; i < n; i++) {
IConstraint constraint = constraints[i];
constraint.Active = constraint.IsSourceActive
&& (!constraint.IData.SkinRequired || (skin != null && skin.constraints.Contains(constraint.IData)));
if (constraint.Active) constraint.Sort(this);
}
for (int i = 0; i < boneCount; i++)
SortBone(bones[i]);
object[] updateCache = this.updateCache.Items;
n = this.updateCache.Count;
for (int i = 0; i < n; i++) {
Bone bone = updateCache[i] as Bone;
if (bone != null) updateCache[i] = bone.applied;
}
}
}
internal void Constrained (IPosedInternal obj) {
if (obj.PoseEqualsApplied) { // if (obj.pose == obj.applied) {
obj.UseConstrained();
resetCache.Add(obj);
}
}
internal void SortBone (Bone bone) {
if (bone.sorted || !bone.active) return;
Bone parent = bone.parent;
if (parent != null) SortBone(parent);
bone.sorted = true;
updateCache.Add(bone);
}
internal void SortReset (ExposedList<Bone> bones) {
Bone[] items = bones.Items;
for (int i = 0, n = bones.Count; i < n; i++) {
Bone bone = items[i];
if (bone.active) {
if (bone.sorted) SortReset(bone.children);
bone.sorted = false;
}
}
}
/// <summary>
/// Updates the world transform for each bone and applies all constraints.
/// <para>
/// See <a href="http://esotericsoftware.com/spine-runtime-skeletons#World-transforms">World transforms</a> in the Spine
/// Runtimes Guide.</para>
/// </summary>
public void UpdateWorldTransform (Physics physics) {
update++;
IPosedInternal[] resetCache = this.resetCache.Items;
for (int i = 0, n = this.resetCache.Count; i < n; i++) {
resetCache[i].ResetConstrained();
}
object[] updateCache = this.updateCache.Items;
for (int i = 0, n = this.updateCache.Count; i < n; i++)
((IUpdate)updateCache[i]).Update(this, physics);
}
/// <summary>Sets the bones, constraints, slots, and draw order to their setup pose values.</summary>
public void SetupPose () {
SetupPoseBones();
SetupPoseSlots();
}
/// <summary>Sets the bones and constraints to their setup pose values.</summary>
public void SetupPoseBones () {
Bone[] bones = this.bones.Items;
for (int i = 0, n = this.bones.Count; i < n; i++)
bones[i].SetupPose();
IConstraint[] constraints = this.constraints.Items;
for (int i = 0, n = this.constraints.Count; i < n; i++)
constraints[i].SetupPose();
}
/// <summary>Sets the slots and draw order to their setup pose values.</summary>
public void SetupPoseSlots () {
Slot[] slots = this.slots.Items;
int n = this.slots.Count;
Array.Copy(slots, 0, drawOrder.Items, 0, n);
for (int i = 0; i < n; i++)
slots[i].SetupPose();
}
/// <summary>The skeleton's setup pose data.</summary>
public SkeletonData Data { get { return data; } }
/// <summary>The skeleton's bones, sorted parent first. The root bone is always the first bone.</summary>
public ExposedList<Bone> Bones { get { return bones; } }
/// <summary>
/// The list of bones and constraints, sorted in the order they should be updated, as computed by <see cref="UpdateCache()"/>.
/// </summary>
public ExposedList<object> UpdateCacheList { get { return updateCache; } }
/// <summary>Returns the root bone, or null if the skeleton has no bones.</summary>
public Bone RootBone {
get { return bones.Count == 0 ? null : bones.Items[0]; }
}
/// <summary>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.</summary>
/// <returns>May be null.</returns>
public Bone FindBone (string boneName) {
if (boneName == null) throw new ArgumentNullException("boneName", "boneName cannot be null.");
Bone[] bones = this.bones.Items;
for (int i = 0, n = this.bones.Count; i < n; i++) {
Bone bone = bones[i];
if (bone.data.name == boneName) return bone;
}
return null;
}
/// <summary>The skeleton's slots.</summary>
public ExposedList<Slot> Slots { get { return slots; } }
/// <summary>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.</summary>
/// <returns>May be null.</returns>
public Slot FindSlot (string slotName) {
if (slotName == null) throw new ArgumentNullException("slotName", "slotName cannot be null.");
Slot[] slots = this.slots.Items;
for (int i = 0, n = this.slots.Count; i < n; i++) {
Slot slot = slots[i];
if (slot.data.name == slotName) return slot;
}
return null;
}
/// <summary>
/// The skeleton's slots in the order they should be drawn. The returned array may be modified to change the draw order.
/// </summary>
public ExposedList<Slot> DrawOrder {
get { return drawOrder; }
set {
if (value == null) throw new ArgumentNullException("drawOrder ", "drawOrder cannot be null.");
this.drawOrder = value;
}
}
/// <summary>The skeleton's current skin. May be null. See <see cref="SetSkin(Spine.Skin)"/></summary>
public Skin Skin {
/// <summary>The skeleton's current skin. May be null.</summary>
get { return skin; }
/// <summary>Sets a skin, <see cref="SetSkin(Skin)"/>.</summary>
set { SetSkin(value); }
}
/// <summary>Sets a skin by name (see <see cref="SetSkin(Spine.Skin)"/>).</summary>
public void SetSkin (string skinName) {
Skin foundSkin = data.FindSkin(skinName);
if (foundSkin == null) throw new ArgumentException("Skin not found: " + skinName, "skinName");
SetSkin(foundSkin);
}
/// <summary>
/// <para>Sets the skin used to look up attachments before looking in the <see cref="SkeletonData.DefaultSkin"/>. If the
/// skin is changed, <see cref="UpdateCache()"/> is called.
/// </para>
/// <para>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.
/// </para>
/// <para>After changing the skin, the visible attachments can be reset to those attached in the setup pose by calling
/// <see cref="Skeleton.SetupPoseSlots()"/>. Also, often <see cref="AnimationState.Apply(Skeleton)"/> 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.</para>
/// </summary>
/// <param name="newSkin">May be null.</param>
public void SetSkin (Skin newSkin) {
if (newSkin == skin) return;
if (newSkin != null) {
if (skin != null)
newSkin.AttachAll(this, skin);
else {
Slot[] slots = this.slots.Items;
for (int i = 0, n = this.slots.Count; i < n; i++) {
Slot slot = slots[i];
string name = slot.data.attachmentName;
if (name != null) {
Attachment attachment = newSkin.GetAttachment(i, name);
if (attachment != null) slot.pose.Attachment = attachment;
}
}
}
}
skin = newSkin;
UpdateCache();
}
/// <summary>Finds an attachment by looking in the <see cref="Skeleton.Skin"/> and <see cref="SkeletonData.DefaultSkin"/> using the slot name and attachment
/// name.</summary>
/// <returns>May be null.</returns>
/// <seealso cref="GetAttachment(int, string)"/>
public Attachment GetAttachment (string slotName, string attachmentName) {
SlotData slot = data.FindSlot(slotName);
if (slot == null) throw new ArgumentException("Slot not found: " + slotName, "slotName");
return GetAttachment(slot.index, attachmentName);
}
/// <summary>Finds an attachment by looking in the skin and 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.</summary>
/// <para>
/// See <a href="http://esotericsoftware.com/spine-runtime-skins">Runtime skins</a> in the Spine Runtimes Guide.</para>
/// <returns>May be null.</returns>
public Attachment GetAttachment (int slotIndex, string attachmentName) {
if (attachmentName == null) throw new ArgumentNullException("attachmentName", "attachmentName cannot be null.");
if (skin != null) {
Attachment attachment = skin.GetAttachment(slotIndex, attachmentName);
if (attachment != null) return attachment;
}
if (data.defaultSkin != null) return data.defaultSkin.GetAttachment(slotIndex, attachmentName);
return null;
}
/// <summary>A convenience method to set an attachment by finding the slot with <see cref="FindSlot(string)"/>, finding the attachment with
/// <see cref="GetAttachment(int, string)"/>, then setting the slot's <see cref="SlotPose.Attachment"/>.</summary>
/// <param name="attachmentName">May be null to clear the slot's attachment.</param>
public void SetAttachment (string slotName, string attachmentName) {
if (slotName == null) throw new ArgumentNullException("slotName", "slotName cannot be null.");
Slot slot = FindSlot(slotName);
if (slot == null) throw new ArgumentException("Slot not found: " + slotName, "slotName");
Attachment attachment = null;
if (attachmentName != null) {
attachment = GetAttachment(slot.data.index, attachmentName);
if (attachment == null)
throw new ArgumentException("Attachment not found: " + attachmentName + ", for slot: " + slotName, "attachmentName");
}
slot.pose.Attachment = attachment;
}
/// <summary>The skeleton's constraints.</summary>
public ExposedList<IConstraint> Constraints { get { return constraints; } }
/// <summary>The skeleton's physics constraints.</summary>
public ExposedList<PhysicsConstraint> PhysicsConstraints { get { return physics; } }
/// <returns>May be null.</returns>
public T FindConstraint<T> (string constraintName) where T : class, IConstraint {
if (constraintName == null) throw new ArgumentNullException("constraintName", "constraintName cannot be null.");
IConstraint[] constraints = this.constraints.Items;
for (int i = 0, n = this.constraints.Count; i < n; i++) {
IConstraint constraint = constraints[i];
if (constraint is T && constraint.IData.Name == constraintName) return (T)constraint;
}
return null;
}
/// <summary>Returns the axis aligned bounding box (AABB) of the region and mesh attachments for the current pose.</summary>
/// <param name="x">The horizontal distance between the skeleton origin and the left side of the AABB.</param>
/// <param name="y">The vertical distance between the skeleton origin and the bottom side of the AABB.</param>
/// <param name="width">The width of the AABB</param>
/// <param name="height">The height of the AABB.</param>
/// <param name="vertexBuffer">Reference to hold a float[]. May be a null reference. This method will assign it a new float[] with the appropriate size as needed.</param>
public void GetBounds (out float x, out float y, out float width, out float height, ref float[] vertexBuffer,
SkeletonClipping clipper = null) {
float[] temp = vertexBuffer;
temp = temp ?? new float[8];
Slot[] drawOrder = this.drawOrder.Items;
float minX = int.MaxValue, minY = int.MaxValue, maxX = int.MinValue, maxY = int.MinValue;
for (int i = 0, n = this.drawOrder.Count; i < n; i++) {
Slot slot = drawOrder[i];
if (!slot.bone.active) continue;
int verticesLength = 0;
float[] vertices = null;
int[] triangles = null;
Attachment attachment = slot.pose.attachment;
RegionAttachment region = attachment as RegionAttachment;
if (region != null) {
verticesLength = 8;
vertices = temp;
if (vertices.Length < 8) vertices = temp = new float[8];
region.ComputeWorldVertices(slot, vertices, 0, 2);
triangles = quadTriangles;
} else {
MeshAttachment mesh = attachment as MeshAttachment;
if (mesh != null) {
verticesLength = mesh.WorldVerticesLength;
vertices = temp;
if (vertices.Length < verticesLength) vertices = temp = new float[verticesLength];
mesh.ComputeWorldVertices(this, slot, 0, verticesLength, temp, 0, 2);
triangles = mesh.Triangles;
} else if (clipper != null) {
ClippingAttachment clip = attachment as ClippingAttachment;
if (clip != null) {
clipper.ClipEnd(slot);
clipper.ClipStart(this, slot, clip);
continue;
}
}
}
if (vertices != null) {
if (clipper != null && clipper.IsClipping && clipper.ClipTriangles(vertices, triangles, triangles.Length)) {
vertices = clipper.ClippedVertices.Items;
verticesLength = clipper.ClippedVertices.Count;
}
for (int ii = 0; ii < verticesLength; ii += 2) {
float vx = vertices[ii], vy = vertices[ii + 1];
minX = Math.Min(minX, vx);
minY = Math.Min(minY, vy);
maxX = Math.Max(maxX, vx);
maxY = Math.Max(maxY, vy);
}
}
if (clipper != null) clipper.ClipEnd(slot);
}
if (clipper != null) clipper.ClipEnd();
x = minX;
y = minY;
width = maxX - minX;
height = maxY - minY;
vertexBuffer = temp;
}
/// <returns>A copy of the color to tint all the skeleton's attachments.</returns>
public Color32F GetColor () {
return color;
}
/// <summary>Sets the color to tint all the skeleton's attachments.</summary>
public void SetColor (Color32F color) {
this.color = color;
}
/// <summary>
/// A convenience method for setting the skeleton color. The color can also be set by
/// <see cref="SetColor(Color32F)"/>
/// </summary>
public void SetColor (float r, float g, float b, float a) {
color = new Color32F(r, g, b, a);
}
/// <summary><para> Scales the entire skeleton on the X axis.</para>
/// <para>
/// Bones that do not inherit scale are still affected by this property.</para></summary>
public float ScaleX { get { return scaleX; } set { scaleX = value; } }
/// <summary><para> Scales the entire skeleton on the Y axis.</para>
/// <para>
/// Bones that do not inherit scale are still affected by this property.</para></summary>
public float ScaleY { get { return scaleY * (Bone.yDown ? -1 : 1); } set { scaleY = value; } }
/// <summary>
/// Scales the entire skeleton on the X and Y axes.
/// <para>
/// Bones that do not inherit scale are still affected by this property.
/// </para></summary>
public void SetScale (float scaleX, float scaleY) {
this.scaleX = scaleX;
this.scaleY = scaleY;
}
/// <summary><para>The skeleton X position, which is added to the root bone worldX position.</para>
/// <para>
/// Bones that do not inherit translation are still affected by this property.</para></summary>
public float X { get { return x; } set { x = value; } }
/// <summary><para>The skeleton Y position, which is added to the root bone worldY position.</para>
/// <para>
/// Bones that do not inherit translation are still affected by this property.</para></summary>
public float Y { get { return y; } set { y = value; } }
/// <summary>
/// Sets the skeleton X and Y position, which is added to the root bone worldX and worldY position.
/// <para>
/// Bones that do not inherit translation are still affected by this property.</para></summary>
public void SetPosition (float x, float y) {
this.x = x;
this.y = y;
}
public float WindX { get { return windX; } set { windX = value; } }
public float WindY { get { return windY; } set { windY = value; } }
public float GravityX { get { return gravityX; } set { gravityX = value; } }
public float GravityY { get { return gravityY; } set { gravityY = value; } }
/// <summary>
/// Calls <see cref="PhysicsConstraint.Translate(float, float)"/> for each physics constraint.
/// </summary>
public void PhysicsTranslate (float x, float y) {
PhysicsConstraint[] physicsConstraints = this.physics.Items;
for (int i = 0, n = this.physics.Count; i < n; i++)
physicsConstraints[i].Translate(x, y);
}
/// <summary>
/// Calls <see cref="PhysicsConstraint.Rotate(float, float, float)"/> for each physics constraint.
/// </summary>
public void PhysicsRotate (float x, float y, float degrees) {
PhysicsConstraint[] physicsConstraints = this.physics.Items;
for (int i = 0, n = this.physics.Count; i < n; i++)
physicsConstraints[i].Rotate(x, y, degrees);
}
/// <summary>Returns the skeleton's time. This is used for time-based manipulations, such as <see cref="PhysicsConstraint"/>.</summary>
/// <seealso cref="Update(float)"/>
public float Time { get { return time; } set { time = value; } }
/// <summary>Increments the skeleton's <see cref="time"/>.</summary>
public void Update (float delta) {
time += delta;
}
override public string ToString () {
return data.name;
}
}
}