Browse Source

[ts] Port latest physics changes.

Mario Zechner 1 year ago
parent
commit
be767b21ea

+ 2 - 2
spine-ts/spine-core/src/IkConstraintData.ts

@@ -47,7 +47,7 @@ export class IkConstraintData extends ConstraintData {
 	}
 
 	/** Controls the bend direction of the IK bones, either 1 or -1. */
-	bendDirection = 1;
+	bendDirection = 0;
 
 	/** When true and only a single bone is being constrained, if the target is too close, the bone is scaled to reach it. */
 	compress = false;
@@ -61,7 +61,7 @@ export class IkConstraintData extends ConstraintData {
 	uniform = false;
 
 	/** A percentage (0-1) that controls the mix between the constrained and unconstrained rotations. */
-	mix = 1;
+	mix = 0;
 
 	/** For two bone IK, the distance from the maximum reach of the bones that rotation will slow. */
 	softness = 0;

+ 40 - 29
spine-ts/spine-core/src/PhysicsConstraint.ts

@@ -134,7 +134,8 @@ export class PhysicsConstraint implements Updatable {
 				this.reset();
 			// Fall through.
 			case Physics.update:
-				this.remaining += Math.max(this.skeleton.time - this.lastTime, 0);
+				const delta = Math.max(this.skeleton.time - this.lastTime, 0);
+				this.remaining += delta;
 				this.lastTime = this.skeleton.time;
 
 				const bx = bone.worldX, by = bone.worldY;
@@ -143,41 +144,52 @@ export class PhysicsConstraint implements Updatable {
 					this.ux = bx;
 					this.uy = by;
 				} else {
-					let remaining = this.remaining, i = this.inertia, step = this.data.step;
+					let a = this.remaining, i = this.inertia, q = this.data.limit * delta, t = this.data.step, f = this.skeleton.data.referenceScale, d = -1;
 					if (x || y) {
 						if (x) {
-							this.xOffset += (this.ux - bx) * i;
+							const u = (this.ux - bx) * i;
+							this.xOffset += u > q ? q : u < -q ? -q : u;
 							this.ux = bx;
 						}
 						if (y) {
-							this.yOffset += (this.uy - by) * i;
+							const u = (this.uy - by) * i;
+							this.yOffset += u > q ? q : u < -q ? -q : u;
 							this.uy = by;
 						}
-						if (remaining >= step) {
-							const m = this.massInverse * step, e = this.strength, w = this.wind * 100, g = this.gravity * -100;
-							const d = Math.pow(this.damping, 60 * step);
+						if (a >= t) {
+							d = Math.pow(this.damping, 60 * t);
+							const m = this.massInverse * t, e = this.strength, w = this.wind * f, g = this.gravity * f;
 							do {
 								if (x) {
 									this.xVelocity += (w - this.xOffset * e) * m;
-									this.xOffset += this.xVelocity * step;
+									this.xOffset += this.xVelocity * t;
 									this.xVelocity *= d;
 								}
 								if (y) {
-									this.yVelocity += (g - this.yOffset * e) * m;
-									this.yOffset += this.yVelocity * step;
+									this.yVelocity -= (g + this.yOffset * e) * m;
+									this.yOffset += this.yVelocity * t;
 									this.yVelocity *= d;
 								}
-								remaining -= step;
-							} while (remaining >= step);
+								a -= t;
+							} while (a >= t);
 						}
 						if (x) bone.worldX += this.xOffset * mix * this.data.x;
 						if (y) bone.worldY += this.yOffset * mix * this.data.y;
 					}
 					if (rotateOrShearX || scaleX) {
 						let ca = Math.atan2(bone.c, bone.a), c = 0, s = 0, mr = 0;
+						let dx = this.cx - bone.worldX, dy = this.cy - bone.worldY;
+						if (dx > q)
+							dx = q;
+						else if (dx < -q) //
+							dx = -q;
+						if (dy > q)
+							dy = q;
+						else if (dy < -q) //
+							dy = -q;
 						if (rotateOrShearX) {
 							mr = (this.data.rotate + this.data.shearX) * mix;
-							let dx = this.cx - bone.worldX, dy = this.cy - bone.worldY, r = Math.atan2(dy + this.ty, dx + this.tx) - ca - this.rotateOffset * mr;
+							let r = Math.atan2(dy + this.ty, dx + this.tx) - ca - this.rotateOffset * mr;
 							this.rotateOffset += (r - Math.ceil(r * MathUtils.invPI2 - 0.5) * MathUtils.PI2) * i;
 							r = this.rotateOffset * mr + ca;
 							c = Math.cos(r);
@@ -190,33 +202,33 @@ export class PhysicsConstraint implements Updatable {
 							c = Math.cos(ca);
 							s = Math.sin(ca);
 							const r = l * bone.getWorldScaleX();
-							if (r > 0) this.scaleOffset += ((this.cx - bone.worldX) * c + (this.cy - bone.worldY) * s) * i / r;
+							if (r > 0) this.scaleOffset += (dx * c + dy * s) * i / r;
 						}
-						remaining = this.remaining;
-						if (remaining >= step) {
-							const m = this.massInverse * step, e = this.strength, w = this.wind, g = this.gravity;
-							const d = Math.pow(this.damping, 60 * step);
+						a = this.remaining;
+						if (a >= t) {
+							if (d == -1) d = Math.pow(this.damping, 60 * t);
+							const m = this.massInverse * t, e = this.strength, w = this.wind, g = this.gravity, h = l / f;
 							while (true) {
-								remaining -= step;
+								a -= t;
 								if (scaleX) {
 									this.scaleVelocity += (w * c - g * s - this.scaleOffset * e) * m;
-									this.scaleOffset += this.scaleVelocity * step;
+									this.scaleOffset += this.scaleVelocity * t;
 									this.scaleVelocity *= d;
 								}
 								if (rotateOrShearX) {
-									this.rotateVelocity += (-0.01 * l * (w * s + g * c) - this.rotateOffset * e) * m;
-									this.rotateOffset += this.rotateVelocity * step;
+									this.rotateVelocity -= ((w * s + g * c) * h + this.rotateOffset * e) * m;
+									this.rotateOffset += this.rotateVelocity * t;
 									this.rotateVelocity *= d;
-									if (remaining < step) break;
+									if (a < t) break;
 									const r = this.rotateOffset * mr + ca;
 									c = Math.cos(r);
 									s = Math.sin(r);
-								} else if (remaining < step) //
+								} else if (a < t) //
 									break;
 							}
 						}
 					}
-					this.remaining = remaining;
+					this.remaining = a;
 				}
 				this.cx = bone.worldX;
 				this.cy = bone.worldY;
@@ -268,6 +280,8 @@ export class PhysicsConstraint implements Updatable {
 		bone.updateAppliedTransform();
 	}
 
+	/** Translates the physics constraint so next {@link #update(Physics)} forces are applied as if the bone moved an additional
+	 * amount in world space. */
 	translate (x: number, y: number) {
 		this.ux -= x;
 		this.uy -= y;
@@ -278,10 +292,7 @@ export class PhysicsConstraint implements Updatable {
 	/** Rotates the physics constraint so next {@link #update(Physics)} forces are applied as if the bone rotated around the
 	 * specified point in world space. */
 	rotate (x: number, y: number, degrees: number) {
-		let r = degrees * MathUtils.degRad, cos = Math.cos(r), sin = Math.sin(r);
-		r = this.tx * cos - this.ty * sin;
-		this.ty = this.tx * sin + this.ty * cos;
-		this.tx = r;
+		const r = degrees * MathUtils.degRad, cos = Math.cos(r), sin = Math.sin(r);
 		const dx = this.cx - x, dy = this.cy - y;
 		this.translate(dx * cos - dy * sin - dx, dx * sin + dy * cos - dy);
 	}

+ 1 - 0
spine-ts/spine-core/src/PhysicsConstraintData.ts

@@ -48,6 +48,7 @@ export class PhysicsConstraintData extends ConstraintData {
 	rotate = 0;
 	scaleX = 0;
 	shearX = 0;
+	limit = 0;
 	step = 0;
 	inertia = 0;
 	strength = 0;

+ 1 - 1
spine-ts/spine-core/src/Skeleton.ts

@@ -54,7 +54,7 @@ export class Skeleton {
 	/** The skeleton's bones, sorted parent first. The root bone is always the first bone. */
 	bones: Array<Bone>;
 
-	/** The skeleton's slots. */
+	/** The skeleton's slots in the setup pose draw order. */
 	slots: Array<Slot>;
 
 	/** The skeleton's slots in the order they should be drawn. The returned array may be modified to change the draw order. */

+ 54 - 39
spine-ts/spine-core/src/SkeletonBinary.ts

@@ -80,11 +80,11 @@ export class SkeletonBinary {
 		skeletonData.y = input.readFloat();
 		skeletonData.width = input.readFloat();
 		skeletonData.height = input.readFloat();
+		skeletonData.referenceScale = input.readFloat() * scale;
 
 		let nonessential = input.readBoolean();
 		if (nonessential) {
 			skeletonData.fps = input.readFloat();
-
 			skeletonData.imagesPath = input.readString();
 			skeletonData.audioPath = input.readString();
 		}
@@ -128,6 +128,14 @@ export class SkeletonBinary {
 		for (let i = 0; i < n; i++) {
 			let slotName = input.readString();
 			if (!slotName) throw new Error("Slot name must not be null.");
+			let path: string | null = null;
+			if (nonessential) {
+				const slash = slotName!.lastIndexOf('/');
+				if (slash != -1) {
+					path = slotName.substring(0, slash);
+					slotName = slotName.substring(slash + 1);
+				}
+			}
 			let boneData = skeletonData.bones[input.readInt(true)];
 			let data = new SlotData(i, slotName, boneData);
 			Color.rgba8888ToColor(data.color, input.readInt32());
@@ -137,7 +145,10 @@ export class SkeletonBinary {
 
 			data.attachmentName = input.readStringRef();
 			data.blendMode = input.readInt(true);
-			if (nonessential) data.visible = input.readBoolean();
+			if (nonessential) {
+				data.visible = input.readBoolean();
+				data.path = path;
+			}
 			skeletonData.slots.push(data);
 		}
 
@@ -152,14 +163,14 @@ export class SkeletonBinary {
 			for (let ii = 0; ii < nn; ii++)
 				data.bones.push(skeletonData.bones[input.readInt(true)]);
 			data.target = skeletonData.bones[input.readInt(true)];
-			data.mix = input.readFloat();
-			data.softness = input.readFloat() * scale;
 			let flags = input.readByte();
 			data.skinRequired = (flags & 1) != 0;
 			data.bendDirection = (flags & 2) != 0 ? 1 : -1;
 			data.compress = (flags & 4) != 0;
 			data.stretch = (flags & 8) != 0;
 			data.uniform = (flags & 16) != 0;
+			if ((flags & 32) != 0) data.mix = (flags & 64) != 0 ? input.readFloat() : 1;
+			if ((flags & 128) != 0) data.softness = input.readFloat() * scale;
 			skeletonData.ikConstraints.push(data);
 		}
 
@@ -174,22 +185,23 @@ export class SkeletonBinary {
 			for (let ii = 0; ii < nn; ii++)
 				data.bones.push(skeletonData.bones[input.readInt(true)]);
 			data.target = skeletonData.bones[input.readInt(true)];
-			const flags = input.readByte();
+			let flags = input.readByte();
 			data.skinRequired = (flags & 1) != 0;
 			data.local = (flags & 2) != 0;
 			data.relative = (flags & 4) != 0;
-			data.offsetRotation = input.readFloat();
-			data.offsetX = input.readFloat() * scale;
-			data.offsetY = input.readFloat() * scale;
-			data.offsetScaleX = input.readFloat();
-			data.offsetScaleY = input.readFloat();
-			data.offsetShearY = input.readFloat();
-			data.mixRotate = input.readFloat();
-			data.mixX = input.readFloat();
-			data.mixY = input.readFloat();
-			data.mixScaleX = input.readFloat();
-			data.mixScaleY = input.readFloat();
-			data.mixShearY = input.readFloat();
+			if ((flags & 8) != 0) data.offsetRotation = input.readFloat();
+			if ((flags & 16) != 0) data.offsetX = input.readFloat() * scale;
+			if ((flags & 32) != 0) data.offsetY = input.readFloat() * scale;
+			if ((flags & 64) != 0) data.offsetScaleX = input.readFloat();
+			if ((flags & 128) != 0) data.offsetScaleY = input.readFloat();
+			flags = input.readByte();
+			if ((flags & 1) != 0) data.offsetShearY = input.readFloat();
+			if ((flags & 2) != 0) data.mixRotate = input.readFloat();
+			if ((flags & 4) != 0) data.mixX = input.readFloat();
+			if ((flags & 8) != 0) data.mixY = input.readFloat();
+			if ((flags & 16) != 0) data.mixScaleX = input.readFloat();
+			if ((flags & 32) != 0) data.mixScaleY = input.readFloat();
+			if ((flags & 64) != 0) data.mixShearY = input.readFloat();
 			skeletonData.transformConstraints.push(data);
 		}
 
@@ -205,10 +217,11 @@ export class SkeletonBinary {
 			for (let ii = 0; ii < nn; ii++)
 				data.bones.push(skeletonData.bones[input.readInt(true)]);
 			data.target = skeletonData.slots[input.readInt(true)];
-			data.positionMode = input.readInt(true);
-			data.spacingMode = input.readInt(true);
-			data.rotateMode = input.readInt(true);
-			data.offsetRotation = input.readFloat();
+			const flags = input.readByte();
+			data.positionMode = flags & 1;
+			data.spacingMode = (flags >> 1) & 3;
+			data.rotateMode = (flags >> 3) & 3;
+			if ((flags & 128) != 0) data.offsetRotation = input.readFloat();
 			data.position = input.readFloat();
 			if (data.positionMode == PositionMode.Fixed) data.position *= scale;
 			data.spacing = input.readFloat();
@@ -234,14 +247,14 @@ export class SkeletonBinary {
 			if ((flags & 8) != 0) data.rotate = input.readFloat();
 			if ((flags & 16) != 0) data.scaleX = input.readFloat();
 			if ((flags & 32) != 0) data.shearX = input.readFloat();
+			data.limit = ((flags & 64) != 0 ? input.readFloat() : 5000) * scale;
 			data.step = 1 / input.readByte();
 			data.inertia = input.readFloat();
 			data.strength = input.readFloat();
 			data.damping = input.readFloat();
-			data.massInverse = input.readFloat();
-			data.wind = input.readFloat() * scale;
-			data.gravity = input.readFloat() * scale;
-			data.mix = input.readFloat();
+			data.massInverse = (flags & 128) != 0 ? input.readFloat() : 1;
+			data.wind = input.readFloat();
+			data.gravity = input.readFloat();
 			flags = input.readByte();
 			if ((flags & 1) != 0) data.inertiaGlobal = true;
 			if ((flags & 2) != 0) data.strengthGlobal = true;
@@ -250,6 +263,7 @@ export class SkeletonBinary {
 			if ((flags & 16) != 0) data.windGlobal = true;
 			if ((flags & 32) != 0) data.gravityGlobal = true;
 			if ((flags & 64) != 0) data.mixGlobal = true;
+			data.mix = (flags & 128) != 0 ? input.readFloat() : 1;
 			skeletonData.physicsConstraints.push(data);
 		}
 
@@ -365,7 +379,7 @@ export class SkeletonBinary {
 				let path = (flags & 16) != 0 ? input.readStringRef() : null;
 				const color = (flags & 32) != 0 ? input.readInt32() : 0xffffffff;
 				const sequence = (flags & 64) != 0 ? this.readSequence(input) : null;
-				let rotation = input.readFloat();
+				let rotation = (flags & 128) != 0 ? input.readFloat() : 0;
 				let x = input.readFloat();
 				let y = input.readFloat();
 				let scaleX = input.readFloat();
@@ -827,19 +841,20 @@ export class SkeletonBinary {
 		for (let i = 0, n = input.readInt(true); i < n; i++) {
 			let index = input.readInt(true), frameCount = input.readInt(true), frameLast = frameCount - 1;
 			let timeline = new IkConstraintTimeline(frameCount, input.readInt(true), index);
-			let time = input.readFloat(), mix = input.readFloat(), softness = input.readFloat() * scale;
+			let flags = input.readByte();
+			let time = input.readFloat(), mix = (flags & 1) != 0 ? ((flags & 2) != 0 ? input.readFloat() : 1) : 0;
+			let softness = (flags & 4) != 0 ? input.readFloat() * scale : 0;
 			for (let frame = 0, bezier = 0; ; frame++) {
-				const flags = input.readByte();
-				timeline.setFrame(frame, time, mix, softness, input.readByte(), (flags & 1) != 0, (flags & 2) != 0);
+				timeline.setFrame(frame, time, mix, softness, (flags & 8) != 0 ? 1 : -1, (flags & 16) != 0, (flags & 32) != 0);
 				if (frame == frameLast) break;
-				let time2 = input.readFloat(), mix2 = input.readFloat(), softness2 = input.readFloat() * scale;
-				switch (input.readByte()) {
-					case CURVE_STEPPED:
-						timeline.setStepped(frame);
-						break;
-					case CURVE_BEZIER:
-						setBezier(input, timeline, bezier++, frame, 0, time, time2, mix, mix2, 1);
-						setBezier(input, timeline, bezier++, frame, 1, time, time2, softness, softness2, scale);
+				flags = input.readByte();
+				const time2 = input.readFloat(), mix2 = (flags & 1) != 0 ? ((flags & 2) != 0 ? input.readFloat() : 1) : 0;
+				const softness2 = (flags & 4) != 0 ? input.readFloat() * scale : 0;
+				if ((flags & 64) != 0) {
+					timeline.setStepped(frame);
+				} else if ((flags & 128) != 0) {
+					setBezier(input, timeline, bezier++, frame, 0, time, time2, mix, mix2, 1);
+					setBezier(input, timeline, bezier++, frame, 1, time, time2, softness, softness2, scale);
 				}
 				time = time2;
 				mix = mix2;
@@ -953,10 +968,10 @@ export class SkeletonBinary {
 						timelines.push(readTimeline1(input, new PhysicsConstraintMassTimeline(frameCount, bezierCount, index), 1));
 						break;
 					case PHYSICS_WIND:
-						timelines.push(readTimeline1(input, new PhysicsConstraintWindTimeline(frameCount, bezierCount, index), scale));
+						timelines.push(readTimeline1(input, new PhysicsConstraintWindTimeline(frameCount, bezierCount, index), 1));
 						break;
 					case PHYSICS_GRAVITY:
-						timelines.push(readTimeline1(input, new PhysicsConstraintGravityTimeline(frameCount, bezierCount, index), scale));
+						timelines.push(readTimeline1(input, new PhysicsConstraintGravityTimeline(frameCount, bezierCount, index), 1));
 						break;
 					case PHYSICS_MIX:
 						timelines.push(readTimeline1(input, new PhysicsConstraintMixTimeline(frameCount, bezierCount, index), 1));

+ 6 - 1
spine-ts/spine-core/src/SkeletonData.ts

@@ -49,8 +49,9 @@ export class SkeletonData {
 	/** The skeleton's bones, sorted parent first. The root bone is always the first bone. */
 	bones = new Array<BoneData>(); // Ordered parents first.
 
-	/** The skeleton's slots. */
+	/** The skeleton's slots in the setup pose draw order. */
 	slots = new Array<SlotData>(); // Setup pose draw order.
+
 	skins = new Array<Skin>();
 
 	/** The skeleton's default skin. By default this skin contains all attachments that were not in a skin in Spine.
@@ -89,6 +90,10 @@ export class SkeletonData {
 	/** The height of the skeleton's axis aligned bounding box in the setup pose. */
 	height: number = 0;
 
+	/** Baseline scale factor for applying distance-dependent effects on non-scalable properties, such as angle or scale. Default
+	 * is 100. */
+	referenceScale = 100;
+
 	/** The Spine version used to export the skeleton data, or null. */
 	version: string | null = null;
 

+ 20 - 13
spine-ts/spine-core/src/SkeletonJson.ts

@@ -79,8 +79,10 @@ export class SkeletonJson {
 			skeletonData.y = skeletonMap.y;
 			skeletonData.width = skeletonMap.width;
 			skeletonData.height = skeletonMap.height;
+			skeletonData.referenceScale = getValue(skeletonMap, "referenceScale", 100) * scale;
 			skeletonData.fps = skeletonMap.fps;
-			skeletonData.imagesPath = skeletonMap.images;
+			skeletonData.imagesPath = skeletonMap.images ?? null;
+			skeletonData.audioPath = skeletonMap.audio ?? null;
 		}
 
 		// Bones
@@ -114,9 +116,16 @@ export class SkeletonJson {
 		if (root.slots) {
 			for (let i = 0; i < root.slots.length; i++) {
 				let slotMap = root.slots[i];
+				let path: string | null = null;
+				let slotName = slotMap.name;
+				const slash = slotName.lastIndexOf('/');
+				if (slash != -1) {
+					path = slotName.substring(0, slash);
+					slotName = slotName.substring(slash + 1);
+				}
 				let boneData = skeletonData.findBone(slotMap.bone);
-				if (!boneData) throw new Error(`Couldn't find bone ${slotMap.bone} for slot ${slotMap.name}`);
-				let data = new SlotData(skeletonData.slots.length, slotMap.name, boneData);
+				if (!boneData) throw new Error(`Couldn't find bone ${slotMap.bone} for slot ${slotName}`);
+				let data = new SlotData(skeletonData.slots.length, slotName, boneData);
 
 				let color: string = getValue(slotMap, "color", null);
 				if (color) data.color.setFromString(color);
@@ -126,6 +135,8 @@ export class SkeletonJson {
 
 				data.attachmentName = getValue(slotMap, "attachment", null);
 				data.blendMode = Utils.enumValue(BlendMode, getValue(slotMap, "blend", "normal"));
+				data.visible = getValue(slotMap, "visible", true);
+				data.path = path;
 				skeletonData.slots.push(data);
 			}
 		}
@@ -253,13 +264,14 @@ export class SkeletonJson {
 				data.rotate = getValue(constraintMap, "rotate", 0);
 				data.scaleX = getValue(constraintMap, "scaleX", 0);
 				data.shearX = getValue(constraintMap, "shearX", 0);
+				data.limit = getValue(constraintMap, "limit", 5000) * scale;
 				data.step = 1 / getValue(constraintMap, "fps", 60);
 				data.inertia = getValue(constraintMap, "inertia", 1);
 				data.strength = getValue(constraintMap, "strength", 100);
 				data.damping = getValue(constraintMap, "damping", 1);
 				data.massInverse = 1 / getValue(constraintMap, "mass", 1);
-				data.wind = getValue(constraintMap, "wind", 0) * scale;
-				data.gravity = getValue(constraintMap, "gravity", 0) * scale;
+				data.wind = getValue(constraintMap, "wind", 0);
+				data.gravity = getValue(constraintMap, "gravity", 0);
 				data.mix = getValue(constraintMap, "mix", 1);
 				data.inertiaGlobal = getValue(constraintMap, "inertiaGlobal", false);
 				data.strengthGlobal = getValue(constraintMap, "strengthGlobal", false);
@@ -911,7 +923,6 @@ export class SkeletonJson {
 					}
 
 					let timeline;
-					let timelineScale = 1;
 					if (timelineName == "inertia")
 						timeline = new PhysicsConstraintInertiaTimeline(frames, frames, constraintIndex);
 					else if (timelineName == "strength")
@@ -920,19 +931,15 @@ export class SkeletonJson {
 						timeline = new PhysicsConstraintDampingTimeline(frames, frames, constraintIndex);
 					else if (timelineName == "mass")
 						timeline = new PhysicsConstraintMassTimeline(frames, frames, constraintIndex);
-					else if (timelineName == "wind") {
+					else if (timelineName == "wind")
 						timeline = new PhysicsConstraintWindTimeline(frames, frames, constraintIndex);
-						timelineScale = scale;
-					}
-					else if (timelineName == "gravity") {
+					else if (timelineName == "gravity")
 						timeline = new PhysicsConstraintGravityTimeline(frames, frames, constraintIndex);
-						timelineScale = scale;
-					}
 					else if (timelineName == "mix") //
 						timeline = new PhysicsConstraintMixTimeline(frames, frames, constraintIndex);
 					else
 						continue;
-					timelines.push(readTimeline1(timelineMap, timeline, 0, timelineScale));
+					timelines.push(readTimeline1(timelineMap, timeline, 0, 1));
 				}
 			}
 		}

+ 4 - 0
spine-ts/spine-core/src/SlotData.ts

@@ -58,6 +58,10 @@ export class SlotData {
 	/** False if the slot was hidden in Spine and nonessential data was exported. Does not affect runtime rendering. */
 	visible = true;
 
+	/** The folders for this slot in the draw order, delimited by <code>/</code>, or null if nonessential data was not exported. */
+	path: string | null = null;
+
+
 	constructor (index: number, name: string, boneData: BoneData) {
 		if (index < 0) throw new Error("index must be >= 0.");
 		if (!name) throw new Error("name cannot be null.");