@flighthq/skeleton2d 0.2.1-next.840.857425c
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/applyAnimationClipToSkeleton2D.d.ts +3 -0
- package/dist/applyAnimationClipToSkeleton2D.d.ts.map +1 -0
- package/dist/applyAnimationClipToSkeleton2D.js +45 -0
- package/dist/applyAnimationClipToSkeleton2D.js.map +1 -0
- package/dist/deformMeshAttachment2D.d.ts +3 -0
- package/dist/deformMeshAttachment2D.d.ts.map +1 -0
- package/dist/deformMeshAttachment2D.js +59 -0
- package/dist/deformMeshAttachment2D.js.map +1 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +5 -0
- package/dist/index.js.map +1 -0
- package/dist/regionAttachment2D.d.ts +3 -0
- package/dist/regionAttachment2D.d.ts.map +1 -0
- package/dist/regionAttachment2D.js +42 -0
- package/dist/regionAttachment2D.js.map +1 -0
- package/dist/skeleton2d.d.ts +12 -0
- package/dist/skeleton2d.d.ts.map +1 -0
- package/dist/skeleton2d.js +253 -0
- package/dist/skeleton2d.js.map +1 -0
- package/package.json +46 -0
- package/src/applyAnimationClipToSkeleton2D.test.ts +74 -0
- package/src/deformMeshAttachment2D.test.ts +81 -0
- package/src/regionAttachment2D.test.ts +71 -0
- package/src/skeleton2d.test.ts +234 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"applyAnimationClipToSkeleton2D.d.ts","sourceRoot":"","sources":["../src/applyAnimationClipToSkeleton2D.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,aAAa,EAAE,UAAU,EAA6B,MAAM,iBAAiB,CAAC;AAU5F,wBAAgB,8BAA8B,CAC5C,IAAI,EAAE,QAAQ,CAAC,aAAa,CAAC,EAC7B,QAAQ,EAAE,UAAU,EACpB,IAAI,EAAE,MAAM,GACX,IAAI,CA+BN"}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { sampleAnimationTrack } from '@flighthq/animation';
|
|
2
|
+
import { Skeleton2DAnimationPath } from '@flighthq/types';
|
|
3
|
+
// Poses a Skeleton2D from an AnimationClip at `time`: for each channel whose `targetRef` is a
|
|
4
|
+
// Skeleton2DAnimationTarget, samples the channel's track and writes the value into the target bone's local
|
|
5
|
+
// setup-transform fields. The 2D-skeletal analogue of @flighthq/scene3d's `applyAnimationClipToScene3D`, over
|
|
6
|
+
// `Bone2D` instead of a `Node3D`. It mutates only the bones' local pose — the caller runs
|
|
7
|
+
// `computeSkeleton2DWorldTransforms` (then the deform) afterward, so a whole clip applies before one world
|
|
8
|
+
// propagation. Module scratch, allocation-free. A channel whose target is foreign (not a
|
|
9
|
+
// Skeleton2DAnimationTarget) or out of range is skipped (sentinel guard, no throw).
|
|
10
|
+
export function applyAnimationClipToSkeleton2D(clip, skeleton, time) {
|
|
11
|
+
const channels = clip.channels;
|
|
12
|
+
const bones = skeleton.bones;
|
|
13
|
+
for (let i = 0; i < channels.length; i++) {
|
|
14
|
+
const channel = channels[i];
|
|
15
|
+
const target = channel.targetRef;
|
|
16
|
+
if (target === null || typeof target !== 'object' || typeof target.boneIndex !== 'number')
|
|
17
|
+
continue;
|
|
18
|
+
const boneIndex = target.boneIndex;
|
|
19
|
+
if (boneIndex < 0 || boneIndex >= bones.length)
|
|
20
|
+
continue;
|
|
21
|
+
sampleAnimationTrack(_scratch, channel.track, time);
|
|
22
|
+
const bone = bones[boneIndex];
|
|
23
|
+
switch (target.path) {
|
|
24
|
+
case Skeleton2DAnimationPath.Translation:
|
|
25
|
+
bone.x = _scratch[0];
|
|
26
|
+
bone.y = _scratch[1];
|
|
27
|
+
break;
|
|
28
|
+
case Skeleton2DAnimationPath.Rotation:
|
|
29
|
+
bone.rotation = _scratch[0];
|
|
30
|
+
break;
|
|
31
|
+
case Skeleton2DAnimationPath.Scale:
|
|
32
|
+
bone.scaleX = _scratch[0];
|
|
33
|
+
bone.scaleY = _scratch[1];
|
|
34
|
+
break;
|
|
35
|
+
case Skeleton2DAnimationPath.Shear:
|
|
36
|
+
bone.shearX = _scratch[0];
|
|
37
|
+
bone.shearY = _scratch[1];
|
|
38
|
+
break;
|
|
39
|
+
default:
|
|
40
|
+
break;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
const _scratch = [0, 0, 0, 0];
|
|
45
|
+
//# sourceMappingURL=applyAnimationClipToSkeleton2D.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"applyAnimationClipToSkeleton2D.js","sourceRoot":"","sources":["../src/applyAnimationClipToSkeleton2D.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,oBAAoB,EAAE,MAAM,qBAAqB,CAAC;AAE3D,OAAO,EAAE,uBAAuB,EAAE,MAAM,iBAAiB,CAAC;AAE1D,8FAA8F;AAC9F,2GAA2G;AAC3G,8GAA8G;AAC9G,0FAA0F;AAC1F,2GAA2G;AAC3G,yFAAyF;AACzF,oFAAoF;AACpF,MAAM,UAAU,8BAA8B,CAC5C,IAA6B,EAC7B,QAAoB,EACpB,IAAY;IAEZ,MAAM,QAAQ,GAAG,IAAI,CAAC,QAAQ,CAAC;IAC/B,MAAM,KAAK,GAAG,QAAQ,CAAC,KAAK,CAAC;IAC7B,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,QAAQ,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE,CAAC;QACzC,MAAM,OAAO,GAAG,QAAQ,CAAC,CAAC,CAAC,CAAC;QAC5B,MAAM,MAAM,GAAG,OAAO,CAAC,SAA6C,CAAC;QACrE,IAAI,MAAM,KAAK,IAAI,IAAI,OAAO,MAAM,KAAK,QAAQ,IAAI,OAAO,MAAM,CAAC,SAAS,KAAK,QAAQ;YAAE,SAAS;QACpG,MAAM,SAAS,GAAG,MAAM,CAAC,SAAS,CAAC;QACnC,IAAI,SAAS,GAAG,CAAC,IAAI,SAAS,IAAI,KAAK,CAAC,MAAM;YAAE,SAAS;QACzD,oBAAoB,CAAC,QAAQ,EAAE,OAAO,CAAC,KAAK,EAAE,IAAI,CAAC,CAAC;QACpD,MAAM,IAAI,GAAG,KAAK,CAAC,SAAS,CAAC,CAAC;QAC9B,QAAQ,MAAM,CAAC,IAAI,EAAE,CAAC;YACpB,KAAK,uBAAuB,CAAC,WAAW;gBACtC,IAAI,CAAC,CAAC,GAAG,QAAQ,CAAC,CAAC,CAAC,CAAC;gBACrB,IAAI,CAAC,CAAC,GAAG,QAAQ,CAAC,CAAC,CAAC,CAAC;gBACrB,MAAM;YACR,KAAK,uBAAuB,CAAC,QAAQ;gBACnC,IAAI,CAAC,QAAQ,GAAG,QAAQ,CAAC,CAAC,CAAC,CAAC;gBAC5B,MAAM;YACR,KAAK,uBAAuB,CAAC,KAAK;gBAChC,IAAI,CAAC,MAAM,GAAG,QAAQ,CAAC,CAAC,CAAC,CAAC;gBAC1B,IAAI,CAAC,MAAM,GAAG,QAAQ,CAAC,CAAC,CAAC,CAAC;gBAC1B,MAAM;YACR,KAAK,uBAAuB,CAAC,KAAK;gBAChC,IAAI,CAAC,MAAM,GAAG,QAAQ,CAAC,CAAC,CAAC,CAAC;gBAC1B,IAAI,CAAC,MAAM,GAAG,QAAQ,CAAC,CAAC,CAAC,CAAC;gBAC1B,MAAM;YACR;gBACE,MAAM;QACV,CAAC;IACH,CAAC;AACH,CAAC;AAED,MAAM,QAAQ,GAAG,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,CAAC"}
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import type { MeshAttachment2D, Skeleton2D } from '@flighthq/types';
|
|
2
|
+
export declare function deformSkeleton2DMeshAttachment(out: Float32Array, attachment: Readonly<MeshAttachment2D>, skeleton: Readonly<Skeleton2D>, boneIndex: number): void;
|
|
3
|
+
//# sourceMappingURL=deformMeshAttachment2D.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"deformMeshAttachment2D.d.ts","sourceRoot":"","sources":["../src/deformMeshAttachment2D.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAC;AAgBpE,wBAAgB,8BAA8B,CAC5C,GAAG,EAAE,YAAY,EACjB,UAAU,EAAE,QAAQ,CAAC,gBAAgB,CAAC,EACtC,QAAQ,EAAE,QAAQ,CAAC,UAAU,CAAC,EAC9B,SAAS,EAAE,MAAM,GAChB,IAAI,CA4CN"}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
// 6 floats per bone in the flat world-transform buffer (a, b, c, d, tx, ty).
|
|
2
|
+
const MATRIX_STRIDE = 6;
|
|
3
|
+
// Deforms a MeshAttachment2D's vertices into flat interleaved world positions `[x0, y0, x1, y1, …]` in
|
|
4
|
+
// `out` (length ≥ 2 × attachment.vertexCount), for the display layer to draw. Requires
|
|
5
|
+
// `computeSkeleton2DWorldTransforms` to have filled `skeleton.worldMatrices`. Out-parameter,
|
|
6
|
+
// allocation-free.
|
|
7
|
+
//
|
|
8
|
+
// WEIGHTED (attachment.skin non-null): each vertex is Σ weight · (boneWorld · (localX, localY)) over its
|
|
9
|
+
// Skin2D influences — the offsets are already in each bone's local space (they bake the bind pose), so
|
|
10
|
+
// this reads bone WORLD matrices directly, no palette. `boneIndex` is ignored.
|
|
11
|
+
// RIGID (attachment.skin null): the mesh's setup-pose local `vertices` are transformed by bone
|
|
12
|
+
// `boneIndex`'s (the slot's bone) world matrix. Alias-safe — each vertex is read into locals before its
|
|
13
|
+
// output is written, so `out` may be the same buffer as `attachment.vertices`.
|
|
14
|
+
export function deformSkeleton2DMeshAttachment(out, attachment, skeleton, boneIndex) {
|
|
15
|
+
const world = skeleton.worldMatrices;
|
|
16
|
+
const skin = attachment.skin;
|
|
17
|
+
if (skin !== null && skin !== undefined) {
|
|
18
|
+
const counts = skin.influenceCounts;
|
|
19
|
+
const inf = skin.influences;
|
|
20
|
+
let vi = 0; // influence-stream cursor (stride 4: boneIndex, localX, localY, weight)
|
|
21
|
+
let oi = 0; // output cursor (stride 2)
|
|
22
|
+
for (let v = 0; v < counts.length; v++) {
|
|
23
|
+
let wx = 0;
|
|
24
|
+
let wy = 0;
|
|
25
|
+
const n = counts[v];
|
|
26
|
+
for (let k = 0; k < n; k++) {
|
|
27
|
+
const b = inf[vi] * MATRIX_STRIDE;
|
|
28
|
+
const lx = inf[vi + 1];
|
|
29
|
+
const ly = inf[vi + 2];
|
|
30
|
+
const weight = inf[vi + 3];
|
|
31
|
+
wx += weight * (world[b] * lx + world[b + 2] * ly + world[b + 4]);
|
|
32
|
+
wy += weight * (world[b + 1] * lx + world[b + 3] * ly + world[b + 5]);
|
|
33
|
+
vi += 4;
|
|
34
|
+
}
|
|
35
|
+
out[oi] = wx;
|
|
36
|
+
out[oi + 1] = wy;
|
|
37
|
+
oi += 2;
|
|
38
|
+
}
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
// Rigid mesh: the whole attachment follows one bone's world transform.
|
|
42
|
+
const vertices = attachment.vertices;
|
|
43
|
+
if (vertices === null || vertices === undefined)
|
|
44
|
+
return;
|
|
45
|
+
const b = boneIndex * MATRIX_STRIDE;
|
|
46
|
+
const a = world[b];
|
|
47
|
+
const bb = world[b + 1];
|
|
48
|
+
const c = world[b + 2];
|
|
49
|
+
const d = world[b + 3];
|
|
50
|
+
const tx = world[b + 4];
|
|
51
|
+
const ty = world[b + 5];
|
|
52
|
+
for (let i = 0; i < vertices.length; i += 2) {
|
|
53
|
+
const vx = vertices[i];
|
|
54
|
+
const vy = vertices[i + 1];
|
|
55
|
+
out[i] = a * vx + c * vy + tx;
|
|
56
|
+
out[i + 1] = bb * vx + d * vy + ty;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
//# sourceMappingURL=deformMeshAttachment2D.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"deformMeshAttachment2D.js","sourceRoot":"","sources":["../src/deformMeshAttachment2D.ts"],"names":[],"mappings":"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"}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,cAAc,CAAC;AAC7B,cAAc,0BAA0B,CAAC;AACzC,cAAc,sBAAsB,CAAC;AACrC,cAAc,kCAAkC,CAAC"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,cAAc,CAAC;AAC7B,cAAc,0BAA0B,CAAC;AACzC,cAAc,sBAAsB,CAAC;AACrC,cAAc,kCAAkC,CAAC"}
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import type { RegionAttachment2D, Skeleton2D } from '@flighthq/types';
|
|
2
|
+
export declare function computeSkeleton2DRegionAttachmentVertices(out: Float32Array, attachment: Readonly<RegionAttachment2D>, skeleton: Readonly<Skeleton2D>, boneIndex: number): void;
|
|
3
|
+
//# sourceMappingURL=regionAttachment2D.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"regionAttachment2D.d.ts","sourceRoot":"","sources":["../src/regionAttachment2D.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAc,kBAAkB,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAC;AAUlF,wBAAgB,yCAAyC,CACvD,GAAG,EAAE,YAAY,EACjB,UAAU,EAAE,QAAQ,CAAC,kBAAkB,CAAC,EACxC,QAAQ,EAAE,QAAQ,CAAC,UAAU,CAAC,EAC9B,SAAS,EAAE,MAAM,GAChB,IAAI,CAkCN"}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { matrixTransformPointXY, multiplyMatrix, setTransformMatrix } from '@flighthq/geometry';
|
|
2
|
+
import { DEG_TO_RAD } from '@flighthq/math';
|
|
3
|
+
// 6 floats per bone in the flat world-transform buffer (a, b, c, d, tx, ty).
|
|
4
|
+
const MATRIX_STRIDE = 6;
|
|
5
|
+
// Writes a RegionAttachment2D's four world corner positions into `out` as flat interleaved pairs — order
|
|
6
|
+
// bottom-left, top-left, top-right, bottom-right (`[blx, bly, tlx, tly, trx, try, brx, bry]`, 8 floats).
|
|
7
|
+
// The region's local rect (centered at its `x`/`y` offset, sized `width`×`height`, rotated/scaled by its
|
|
8
|
+
// local transform) is transformed by the slot bone (`boneIndex`) world matrix. Requires
|
|
9
|
+
// `computeSkeleton2DWorldTransforms` to have run. Out-parameter, allocation-free (module scratch).
|
|
10
|
+
export function computeSkeleton2DRegionAttachmentVertices(out, attachment, skeleton, boneIndex) {
|
|
11
|
+
// Region local matrix (offset + rotation° + scale; regions carry no shear).
|
|
12
|
+
setTransformMatrix(_local, attachment.scaleX, attachment.scaleY, attachment.rotation * DEG_TO_RAD, attachment.x, attachment.y);
|
|
13
|
+
const b = boneIndex * MATRIX_STRIDE;
|
|
14
|
+
const world = skeleton.worldMatrices;
|
|
15
|
+
_bone.a = world[b];
|
|
16
|
+
_bone.b = world[b + 1];
|
|
17
|
+
_bone.c = world[b + 2];
|
|
18
|
+
_bone.d = world[b + 3];
|
|
19
|
+
_bone.tx = world[b + 4];
|
|
20
|
+
_bone.ty = world[b + 5];
|
|
21
|
+
// Combined = boneWorld × regionLocal: maps a corner in the region's own space to world.
|
|
22
|
+
multiplyMatrix(_combined, _bone, _local);
|
|
23
|
+
const hw = attachment.width / 2;
|
|
24
|
+
const hh = attachment.height / 2;
|
|
25
|
+
matrixTransformPointXY(_corner, _combined, -hw, -hh);
|
|
26
|
+
out[0] = _corner.x;
|
|
27
|
+
out[1] = _corner.y;
|
|
28
|
+
matrixTransformPointXY(_corner, _combined, -hw, hh);
|
|
29
|
+
out[2] = _corner.x;
|
|
30
|
+
out[3] = _corner.y;
|
|
31
|
+
matrixTransformPointXY(_corner, _combined, hw, hh);
|
|
32
|
+
out[4] = _corner.x;
|
|
33
|
+
out[5] = _corner.y;
|
|
34
|
+
matrixTransformPointXY(_corner, _combined, hw, -hh);
|
|
35
|
+
out[6] = _corner.x;
|
|
36
|
+
out[7] = _corner.y;
|
|
37
|
+
}
|
|
38
|
+
const _local = { a: 1, b: 0, c: 0, d: 1, tx: 0, ty: 0 };
|
|
39
|
+
const _bone = { a: 1, b: 0, c: 0, d: 1, tx: 0, ty: 0 };
|
|
40
|
+
const _combined = { a: 1, b: 0, c: 0, d: 1, tx: 0, ty: 0 };
|
|
41
|
+
const _corner = { x: 0, y: 0 };
|
|
42
|
+
//# sourceMappingURL=regionAttachment2D.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"regionAttachment2D.js","sourceRoot":"","sources":["../src/regionAttachment2D.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { Bone2D, MatrixLike, Skeleton2D } from '@flighthq/types';
|
|
2
|
+
export declare function cloneSkeleton2D(skeleton: Readonly<Skeleton2D>): Skeleton2D;
|
|
3
|
+
export declare function computeSkeleton2DBoneMatrices(skeleton: Readonly<Skeleton2D>): void;
|
|
4
|
+
export declare function computeSkeleton2DWorldTransforms(skeleton: Readonly<Skeleton2D>): void;
|
|
5
|
+
export declare function createSkeleton2D(bones: Bone2D[], slots?: Skeleton2D['slots']): Skeleton2D;
|
|
6
|
+
export declare function disposeSkeleton2D(skeleton: Skeleton2D): void;
|
|
7
|
+
export declare function equalsSkeleton2D(a: Readonly<Skeleton2D>, b: Readonly<Skeleton2D>): boolean;
|
|
8
|
+
export declare function getSkeleton2DBoneIndexByName(skeleton: Readonly<Skeleton2D>, name: string): number;
|
|
9
|
+
export declare function getSkeleton2DBoneWorldMatrix(out: MatrixLike, skeleton: Readonly<Skeleton2D>, boneIndex: number): boolean;
|
|
10
|
+
export declare function setSkeleton2DBindPose(skeleton: Readonly<Skeleton2D>): void;
|
|
11
|
+
export declare function validateSkeleton2D(skeleton: Readonly<Skeleton2D>): string | null;
|
|
12
|
+
//# sourceMappingURL=skeleton2d.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"skeleton2d.d.ts","sourceRoot":"","sources":["../src/skeleton2d.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,MAAM,EAAE,UAAU,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAC;AAStE,wBAAgB,eAAe,CAAC,QAAQ,EAAE,QAAQ,CAAC,UAAU,CAAC,GAAG,UAAU,CAS1E;AAMD,wBAAgB,6BAA6B,CAAC,QAAQ,EAAE,QAAQ,CAAC,UAAU,CAAC,GAAG,IAAI,CAYlF;AAOD,wBAAgB,gCAAgC,CAAC,QAAQ,EAAE,QAAQ,CAAC,UAAU,CAAC,GAAG,IAAI,CAqFrF;AAOD,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,MAAM,EAAE,EAAE,KAAK,GAAE,UAAU,CAAC,OAAO,CAAQ,GAAG,UAAU,CAS/F;AAID,wBAAgB,iBAAiB,CAAC,QAAQ,EAAE,UAAU,GAAG,IAAI,CAG5D;AAED,wBAAgB,gBAAgB,CAAC,CAAC,EAAE,QAAQ,CAAC,UAAU,CAAC,EAAE,CAAC,EAAE,QAAQ,CAAC,UAAU,CAAC,GAAG,OAAO,CAsB1F;AAGD,wBAAgB,4BAA4B,CAAC,QAAQ,EAAE,QAAQ,CAAC,UAAU,CAAC,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,CAMjG;AAID,wBAAgB,4BAA4B,CAC1C,GAAG,EAAE,UAAU,EACf,QAAQ,EAAE,QAAQ,CAAC,UAAU,CAAC,EAC9B,SAAS,EAAE,MAAM,GAChB,OAAO,CAIT;AAMD,wBAAgB,qBAAqB,CAAC,QAAQ,EAAE,QAAQ,CAAC,UAAU,CAAC,GAAG,IAAI,CAU1E;AAID,wBAAgB,kBAAkB,CAAC,QAAQ,EAAE,QAAQ,CAAC,UAAU,CAAC,GAAG,MAAM,GAAG,IAAI,CAiBhF"}
|
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
import { createEntity } from '@flighthq/entity';
|
|
2
|
+
import { inverseMatrix, multiplyMatrix } from '@flighthq/geometry';
|
|
3
|
+
import { DEG_TO_RAD } from '@flighthq/math';
|
|
4
|
+
import { TransformMode2D } from '@flighthq/types';
|
|
5
|
+
// 6 floats per bone in the flat 2×3 affine buffers (a, b, c, d, tx, ty), matching the Matrix field order.
|
|
6
|
+
const MATRIX_STRIDE = 6;
|
|
7
|
+
// Deep-copies the bone array (each Bone2D cloned) and the transform buffers, producing an independently
|
|
8
|
+
// posable skeleton. Slots and their attachments are SHARED (attachments are immutable setup data); use a
|
|
9
|
+
// skin/slot editing layer for per-instance attachment swaps.
|
|
10
|
+
export function cloneSkeleton2D(skeleton) {
|
|
11
|
+
return createEntity({
|
|
12
|
+
boneMatrices: skeleton.boneMatrices.slice(),
|
|
13
|
+
bones: skeleton.bones.map((bone) => ({ ...bone })),
|
|
14
|
+
inverseBindMatrices: skeleton.inverseBindMatrices.slice(),
|
|
15
|
+
slots: skeleton.slots === null || skeleton.slots === undefined ? skeleton.slots : skeleton.slots.map((s) => ({ ...s })),
|
|
16
|
+
worldMatrices: skeleton.worldMatrices.slice(),
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
// Fills the skin palette `boneMatrices[i] = worldMatrices[i] × inverseBindMatrices[i]` — the matrix that
|
|
20
|
+
// takes a bind-pose (setup) mesh vertex to its posed position. Call after `computeSkeleton2DWorldTransforms`
|
|
21
|
+
// and once `setSkeleton2DBindPose` has captured the inverse-bind. Out-parameter (writes `boneMatrices`),
|
|
22
|
+
// allocation-free (module scratch).
|
|
23
|
+
export function computeSkeleton2DBoneMatrices(skeleton) {
|
|
24
|
+
const world = skeleton.worldMatrices;
|
|
25
|
+
const invBind = skeleton.inverseBindMatrices;
|
|
26
|
+
const out = skeleton.boneMatrices;
|
|
27
|
+
const count = skeleton.bones.length;
|
|
28
|
+
for (let i = 0; i < count; i++) {
|
|
29
|
+
const o = i * MATRIX_STRIDE;
|
|
30
|
+
readMatrix(_scratchA, world, o);
|
|
31
|
+
readMatrix(_scratchB, invBind, o);
|
|
32
|
+
multiplyMatrix(_scratchC, _scratchA, _scratchB);
|
|
33
|
+
writeMatrix(out, o, _scratchC);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
// Propagates each bone's world transform from its local setup transform (x/y/rotation/scale/shear) and its
|
|
37
|
+
// parent's world transform, honoring the bone's inherit mode. One linear pass over the parent-before-child
|
|
38
|
+
// ordered array (`worldMatrices[i]` reads `worldMatrices[parentIndex]`, already written). Out-parameter,
|
|
39
|
+
// allocation-free. Angles are converted degrees→radians at this seam. Position always inherits fully (the
|
|
40
|
+
// local origin is placed by the parent); the inherit mode only changes the linear (rotation/scale) part.
|
|
41
|
+
export function computeSkeleton2DWorldTransforms(skeleton) {
|
|
42
|
+
const bones = skeleton.bones;
|
|
43
|
+
const world = skeleton.worldMatrices;
|
|
44
|
+
const count = bones.length;
|
|
45
|
+
for (let i = 0; i < count; i++) {
|
|
46
|
+
const bone = bones[i];
|
|
47
|
+
// Local linear part (Spine bone matrix, transposed b↔c into Flight's x'=a·x+c·y convention):
|
|
48
|
+
// a=cos(rot+shearX)·scaleX, b=sin(rot+shearX)·scaleX, c=cos(rot+90°+shearY)·scaleY, d=sin(…)·scaleY.
|
|
49
|
+
const rotX = (bone.rotation + bone.shearX) * DEG_TO_RAD;
|
|
50
|
+
const rotY = (bone.rotation + 90 + bone.shearY) * DEG_TO_RAD;
|
|
51
|
+
const la = Math.cos(rotX) * bone.scaleX;
|
|
52
|
+
const lb = Math.sin(rotX) * bone.scaleX;
|
|
53
|
+
const lc = Math.cos(rotY) * bone.scaleY;
|
|
54
|
+
const ld = Math.sin(rotY) * bone.scaleY;
|
|
55
|
+
const o = i * MATRIX_STRIDE;
|
|
56
|
+
if (bone.parentIndex < 0) {
|
|
57
|
+
world[o] = la;
|
|
58
|
+
world[o + 1] = lb;
|
|
59
|
+
world[o + 2] = lc;
|
|
60
|
+
world[o + 3] = ld;
|
|
61
|
+
world[o + 4] = bone.x;
|
|
62
|
+
world[o + 5] = bone.y;
|
|
63
|
+
continue;
|
|
64
|
+
}
|
|
65
|
+
const p = bone.parentIndex * MATRIX_STRIDE;
|
|
66
|
+
const pa = world[p];
|
|
67
|
+
const pb = world[p + 1];
|
|
68
|
+
const pc = world[p + 2];
|
|
69
|
+
const pd = world[p + 3];
|
|
70
|
+
// Local origin placed by the parent — the same for every inherit mode.
|
|
71
|
+
world[o + 4] = pa * bone.x + pc * bone.y + world[p + 4];
|
|
72
|
+
world[o + 5] = pb * bone.x + pd * bone.y + world[p + 5];
|
|
73
|
+
switch (bone.transformMode) {
|
|
74
|
+
case TransformMode2D.OnlyTranslation:
|
|
75
|
+
// Rotation/scale come from the bone's own local transform; only position inherits (set above).
|
|
76
|
+
world[o] = la;
|
|
77
|
+
world[o + 1] = lb;
|
|
78
|
+
world[o + 2] = lc;
|
|
79
|
+
world[o + 3] = ld;
|
|
80
|
+
break;
|
|
81
|
+
case TransformMode2D.NoRotationOrReflection: {
|
|
82
|
+
// Inherit the parent's SCALE (its column lengths) but strip rotation + reflection: compose the
|
|
83
|
+
// local transform with a diagonal, axis-aligned scale-only parent.
|
|
84
|
+
const psx = Math.hypot(pa, pb);
|
|
85
|
+
const psy = Math.hypot(pc, pd);
|
|
86
|
+
world[o] = psx * la;
|
|
87
|
+
world[o + 1] = psy * lb;
|
|
88
|
+
world[o + 2] = psx * lc;
|
|
89
|
+
world[o + 3] = psy * ld;
|
|
90
|
+
break;
|
|
91
|
+
}
|
|
92
|
+
case TransformMode2D.NoScale:
|
|
93
|
+
case TransformMode2D.NoScaleOrReflection: {
|
|
94
|
+
// Inherit the parent's ORIENTATION but not its scale: normalize the parent's columns to unit
|
|
95
|
+
// length, then compose with the local transform. NoScale keeps the parent's reflection (its
|
|
96
|
+
// normalized columns as-is); NoScaleOrReflection strips it, forcing the y-axis to the +90°
|
|
97
|
+
// rotation of the x-axis (det = +1) so the child never flips under a negatively-scaled parent.
|
|
98
|
+
const psx = Math.hypot(pa, pb) || 1;
|
|
99
|
+
const nax = pa / psx;
|
|
100
|
+
const nay = pb / psx;
|
|
101
|
+
let ncx;
|
|
102
|
+
let ncy;
|
|
103
|
+
if (bone.transformMode === TransformMode2D.NoScaleOrReflection) {
|
|
104
|
+
ncx = -nay;
|
|
105
|
+
ncy = nax;
|
|
106
|
+
}
|
|
107
|
+
else {
|
|
108
|
+
const psy = Math.hypot(pc, pd) || 1;
|
|
109
|
+
ncx = pc / psy;
|
|
110
|
+
ncy = pd / psy;
|
|
111
|
+
}
|
|
112
|
+
world[o] = nax * la + ncx * lb;
|
|
113
|
+
world[o + 1] = nay * la + ncy * lb;
|
|
114
|
+
world[o + 2] = nax * lc + ncx * ld;
|
|
115
|
+
world[o + 3] = nay * lc + ncy * ld;
|
|
116
|
+
break;
|
|
117
|
+
}
|
|
118
|
+
default:
|
|
119
|
+
// Normal: full inherit — world linear part = parent × local.
|
|
120
|
+
world[o] = pa * la + pc * lb;
|
|
121
|
+
world[o + 1] = pb * la + pd * lb;
|
|
122
|
+
world[o + 2] = pa * lc + pc * ld;
|
|
123
|
+
world[o + 3] = pb * lc + pd * ld;
|
|
124
|
+
break;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
// Allocates a Skeleton2D from a parent-before-child ordered bone array. Sizes the flat world/inverse-bind/
|
|
129
|
+
// palette buffers to the bone count. `computeSkeleton2DWorldTransforms` fills `worldMatrices`;
|
|
130
|
+
// `setSkeleton2DBindPose` captures `inverseBindMatrices`; `computeSkeleton2DBoneMatrices` fills the palette.
|
|
131
|
+
// Pass `slots` for a drawable skeleton (null for a pure bone rig). Bones must be topologically ordered
|
|
132
|
+
// (each bone's `parentIndex` < its own index); `validateSkeleton2D` checks this.
|
|
133
|
+
export function createSkeleton2D(bones, slots = null) {
|
|
134
|
+
const count = bones.length;
|
|
135
|
+
return createEntity({
|
|
136
|
+
boneMatrices: new Float32Array(count * MATRIX_STRIDE),
|
|
137
|
+
bones,
|
|
138
|
+
inverseBindMatrices: new Float32Array(count * MATRIX_STRIDE),
|
|
139
|
+
slots,
|
|
140
|
+
worldMatrices: new Float32Array(count * MATRIX_STRIDE),
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
// Detaches bones/slots so the skeleton is GC-eligible. `dispose*` (not `destroy*`): a Skeleton2D owns no
|
|
144
|
+
// GPU or native resource — its buffers are plain GC memory — so this only clears references.
|
|
145
|
+
export function disposeSkeleton2D(skeleton) {
|
|
146
|
+
skeleton.bones = [];
|
|
147
|
+
skeleton.slots = null;
|
|
148
|
+
}
|
|
149
|
+
export function equalsSkeleton2D(a, b) {
|
|
150
|
+
if (a === b)
|
|
151
|
+
return true;
|
|
152
|
+
if (a.bones.length !== b.bones.length)
|
|
153
|
+
return false;
|
|
154
|
+
for (let i = 0; i < a.bones.length; i++) {
|
|
155
|
+
const x = a.bones[i];
|
|
156
|
+
const y = b.bones[i];
|
|
157
|
+
if (x.parentIndex !== y.parentIndex ||
|
|
158
|
+
x.x !== y.x ||
|
|
159
|
+
x.y !== y.y ||
|
|
160
|
+
x.rotation !== y.rotation ||
|
|
161
|
+
x.scaleX !== y.scaleX ||
|
|
162
|
+
x.scaleY !== y.scaleY ||
|
|
163
|
+
x.shearX !== y.shearX ||
|
|
164
|
+
x.shearY !== y.shearY ||
|
|
165
|
+
x.length !== y.length ||
|
|
166
|
+
x.transformMode !== y.transformMode) {
|
|
167
|
+
return false;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
return true;
|
|
171
|
+
}
|
|
172
|
+
// Returns the index of the bone named `name`, or the sentinel -1 when no bone has that name.
|
|
173
|
+
export function getSkeleton2DBoneIndexByName(skeleton, name) {
|
|
174
|
+
const bones = skeleton.bones;
|
|
175
|
+
for (let i = 0; i < bones.length; i++) {
|
|
176
|
+
if (bones[i].name === name)
|
|
177
|
+
return i;
|
|
178
|
+
}
|
|
179
|
+
return -1;
|
|
180
|
+
}
|
|
181
|
+
// Writes bone `boneIndex`'s world transform into `out`, returning true; returns the sentinel false (leaving
|
|
182
|
+
// `out` untouched) when `boneIndex` is out of range. Requires `computeSkeleton2DWorldTransforms` to have run.
|
|
183
|
+
export function getSkeleton2DBoneWorldMatrix(out, skeleton, boneIndex) {
|
|
184
|
+
if (boneIndex < 0 || boneIndex >= skeleton.bones.length)
|
|
185
|
+
return false;
|
|
186
|
+
readMatrix(out, skeleton.worldMatrices, boneIndex * MATRIX_STRIDE);
|
|
187
|
+
return true;
|
|
188
|
+
}
|
|
189
|
+
// Captures the current world transforms as the bind (setup) pose: `inverseBindMatrices[i] =
|
|
190
|
+
// inverse(worldMatrices[i])`. Call after posing the bones to setup and running
|
|
191
|
+
// `computeSkeleton2DWorldTransforms`. A non-invertible (degenerate, zero-scale) bone leaves its
|
|
192
|
+
// inverse-bind at identity (`inverseMatrix` returns false) rather than producing NaNs.
|
|
193
|
+
export function setSkeleton2DBindPose(skeleton) {
|
|
194
|
+
const world = skeleton.worldMatrices;
|
|
195
|
+
const out = skeleton.inverseBindMatrices;
|
|
196
|
+
const count = skeleton.bones.length;
|
|
197
|
+
for (let i = 0; i < count; i++) {
|
|
198
|
+
const o = i * MATRIX_STRIDE;
|
|
199
|
+
readMatrix(_scratchA, world, o);
|
|
200
|
+
if (!inverseMatrix(_scratchB, _scratchA))
|
|
201
|
+
setMatrixIdentityLocal(_scratchB);
|
|
202
|
+
writeMatrix(out, o, _scratchB);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
// Checks the parent-before-child ordering invariant (each bone's parentIndex is -1 or < its own index) and
|
|
206
|
+
// buffer sizing. Returns null when valid, or a message describing the first violation (sentinel over throw).
|
|
207
|
+
export function validateSkeleton2D(skeleton) {
|
|
208
|
+
const count = skeleton.bones.length;
|
|
209
|
+
const expected = count * MATRIX_STRIDE;
|
|
210
|
+
if (skeleton.worldMatrices.length !== expected)
|
|
211
|
+
return `worldMatrices length ${skeleton.worldMatrices.length} != ${expected}`;
|
|
212
|
+
if (skeleton.inverseBindMatrices.length !== expected) {
|
|
213
|
+
return `inverseBindMatrices length ${skeleton.inverseBindMatrices.length} != ${expected}`;
|
|
214
|
+
}
|
|
215
|
+
if (skeleton.boneMatrices.length !== expected)
|
|
216
|
+
return `boneMatrices length ${skeleton.boneMatrices.length} != ${expected}`;
|
|
217
|
+
for (let i = 0; i < count; i++) {
|
|
218
|
+
const parentIndex = skeleton.bones[i].parentIndex;
|
|
219
|
+
if (parentIndex >= i)
|
|
220
|
+
return `bone ${i} parentIndex ${parentIndex} is not < its own index (bones must be parent-before-child ordered)`;
|
|
221
|
+
if (parentIndex < -1)
|
|
222
|
+
return `bone ${i} parentIndex ${parentIndex} < -1`;
|
|
223
|
+
}
|
|
224
|
+
return null;
|
|
225
|
+
}
|
|
226
|
+
function readMatrix(out, buffer, offset) {
|
|
227
|
+
out.a = buffer[offset];
|
|
228
|
+
out.b = buffer[offset + 1];
|
|
229
|
+
out.c = buffer[offset + 2];
|
|
230
|
+
out.d = buffer[offset + 3];
|
|
231
|
+
out.tx = buffer[offset + 4];
|
|
232
|
+
out.ty = buffer[offset + 5];
|
|
233
|
+
}
|
|
234
|
+
function setMatrixIdentityLocal(out) {
|
|
235
|
+
out.a = 1;
|
|
236
|
+
out.b = 0;
|
|
237
|
+
out.c = 0;
|
|
238
|
+
out.d = 1;
|
|
239
|
+
out.tx = 0;
|
|
240
|
+
out.ty = 0;
|
|
241
|
+
}
|
|
242
|
+
function writeMatrix(buffer, offset, source) {
|
|
243
|
+
buffer[offset] = source.a;
|
|
244
|
+
buffer[offset + 1] = source.b;
|
|
245
|
+
buffer[offset + 2] = source.c;
|
|
246
|
+
buffer[offset + 3] = source.d;
|
|
247
|
+
buffer[offset + 4] = source.tx;
|
|
248
|
+
buffer[offset + 5] = source.ty;
|
|
249
|
+
}
|
|
250
|
+
const _scratchA = { a: 1, b: 0, c: 0, d: 1, tx: 0, ty: 0 };
|
|
251
|
+
const _scratchB = { a: 1, b: 0, c: 0, d: 1, tx: 0, ty: 0 };
|
|
252
|
+
const _scratchC = { a: 1, b: 0, c: 0, d: 1, tx: 0, ty: 0 };
|
|
253
|
+
//# sourceMappingURL=skeleton2d.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"skeleton2d.js","sourceRoot":"","sources":["../src/skeleton2d.ts"],"names":[],"mappings":"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"}
|
package/package.json
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@flighthq/skeleton2d",
|
|
3
|
+
"version": "0.2.1-next.840.857425c",
|
|
4
|
+
"repository": {
|
|
5
|
+
"type": "git",
|
|
6
|
+
"url": "https://github.com/flighthq/flight.git",
|
|
7
|
+
"directory": "packages/skeleton2d"
|
|
8
|
+
},
|
|
9
|
+
"type": "module",
|
|
10
|
+
"main": "dist/index.js",
|
|
11
|
+
"types": "dist/index.d.ts",
|
|
12
|
+
"exports": {
|
|
13
|
+
".": {
|
|
14
|
+
"types": "./dist/index.d.ts",
|
|
15
|
+
"default": "./dist/index.js"
|
|
16
|
+
}
|
|
17
|
+
},
|
|
18
|
+
"files": [
|
|
19
|
+
"dist",
|
|
20
|
+
"src/**/*.test.ts",
|
|
21
|
+
"!dist/**/*.test.js",
|
|
22
|
+
"!dist/**/*.test.d.ts",
|
|
23
|
+
"!dist/**/*.test.js.map",
|
|
24
|
+
"!dist/**/*.test.d.ts.map"
|
|
25
|
+
],
|
|
26
|
+
"scripts": {
|
|
27
|
+
"build": "tsc -b",
|
|
28
|
+
"clean": "tsc -b --clean",
|
|
29
|
+
"test": "vitest run --config vitest.config.ts",
|
|
30
|
+
"test:watch": "vitest --watch --config vitest.config.ts",
|
|
31
|
+
"prepack": "npm run clean && npm run clean:dist && npm run build",
|
|
32
|
+
"clean:dist": "tsx ../../scripts/clean-package-dist.ts"
|
|
33
|
+
},
|
|
34
|
+
"dependencies": {
|
|
35
|
+
"@flighthq/animation": "0.2.1-next.840.857425c",
|
|
36
|
+
"@flighthq/entity": "0.2.1-next.840.857425c",
|
|
37
|
+
"@flighthq/geometry": "0.2.1-next.840.857425c",
|
|
38
|
+
"@flighthq/math": "0.2.1-next.840.857425c",
|
|
39
|
+
"@flighthq/types": "0.2.1-next.840.857425c"
|
|
40
|
+
},
|
|
41
|
+
"devDependencies": {
|
|
42
|
+
"typescript": "^5.3.0"
|
|
43
|
+
},
|
|
44
|
+
"description": "2D skeleton: bone hierarchy with 2D affine transforms, world propagation, inverse-bind, and weighted CPU mesh deformation (Spine/DragonBones model)",
|
|
45
|
+
"sideEffects": false
|
|
46
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { createAnimationChannel, createAnimationClip, createAnimationTrack } from '@flighthq/animation';
|
|
2
|
+
import type { Bone2D } from '@flighthq/types';
|
|
3
|
+
import { AnimationInterpolationLinear, Skeleton2DAnimationPath, TransformMode2D } from '@flighthq/types';
|
|
4
|
+
import { describe, expect, it } from 'vitest';
|
|
5
|
+
|
|
6
|
+
import { applyAnimationClipToSkeleton2D } from './applyAnimationClipToSkeleton2D';
|
|
7
|
+
import { createSkeleton2D } from './skeleton2d';
|
|
8
|
+
|
|
9
|
+
function makeBone(overrides: Partial<Bone2D> = {}): Bone2D {
|
|
10
|
+
return {
|
|
11
|
+
length: 0,
|
|
12
|
+
name: null,
|
|
13
|
+
parentIndex: -1,
|
|
14
|
+
rotation: 0,
|
|
15
|
+
scaleX: 1,
|
|
16
|
+
scaleY: 1,
|
|
17
|
+
shearX: 0,
|
|
18
|
+
shearY: 0,
|
|
19
|
+
transformMode: TransformMode2D.Normal,
|
|
20
|
+
x: 0,
|
|
21
|
+
y: 0,
|
|
22
|
+
...overrides,
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function scalarTrack(times: number[], values: number[], components: number) {
|
|
27
|
+
return createAnimationTrack({ times, values, components, interpolation: AnimationInterpolationLinear });
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
describe('applyAnimationClipToSkeleton2D', () => {
|
|
31
|
+
it('samples a rotation track into the target bone at the given time', () => {
|
|
32
|
+
const s = createSkeleton2D([makeBone()]);
|
|
33
|
+
const clip = createAnimationClip([
|
|
34
|
+
createAnimationChannel(scalarTrack([0, 1], [0, 90], 1), { boneIndex: 0, path: Skeleton2DAnimationPath.Rotation }),
|
|
35
|
+
]);
|
|
36
|
+
applyAnimationClipToSkeleton2D(clip, s, 0.5);
|
|
37
|
+
expect(s.bones[0].rotation).toBeCloseTo(45, 5); // linear 0→90 at t=0.5
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it('drives translation, scale, and shear across their two-component paths', () => {
|
|
41
|
+
const s = createSkeleton2D([makeBone()]);
|
|
42
|
+
const clip = createAnimationClip([
|
|
43
|
+
createAnimationChannel(scalarTrack([0, 1], [0, 0, 10, 20], 2), {
|
|
44
|
+
boneIndex: 0,
|
|
45
|
+
path: Skeleton2DAnimationPath.Translation,
|
|
46
|
+
}),
|
|
47
|
+
createAnimationChannel(scalarTrack([0, 1], [1, 1, 3, 4], 2), {
|
|
48
|
+
boneIndex: 0,
|
|
49
|
+
path: Skeleton2DAnimationPath.Scale,
|
|
50
|
+
}),
|
|
51
|
+
createAnimationChannel(scalarTrack([0, 1], [0, 0, 5, 6], 2), {
|
|
52
|
+
boneIndex: 0,
|
|
53
|
+
path: Skeleton2DAnimationPath.Shear,
|
|
54
|
+
}),
|
|
55
|
+
]);
|
|
56
|
+
applyAnimationClipToSkeleton2D(clip, s, 1);
|
|
57
|
+
expect(s.bones[0].x).toBeCloseTo(10, 5);
|
|
58
|
+
expect(s.bones[0].y).toBeCloseTo(20, 5);
|
|
59
|
+
expect(s.bones[0].scaleX).toBeCloseTo(3, 5);
|
|
60
|
+
expect(s.bones[0].scaleY).toBeCloseTo(4, 5);
|
|
61
|
+
expect(s.bones[0].shearX).toBeCloseTo(5, 5);
|
|
62
|
+
expect(s.bones[0].shearY).toBeCloseTo(6, 5);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it('skips a channel with a foreign target or an out-of-range bone (no throw, no mutation)', () => {
|
|
66
|
+
const s = createSkeleton2D([makeBone({ rotation: 7 })]);
|
|
67
|
+
const clip = createAnimationClip([
|
|
68
|
+
createAnimationChannel(scalarTrack([0, 1], [0, 90], 1), { someOtherTarget: true }),
|
|
69
|
+
createAnimationChannel(scalarTrack([0, 1], [0, 90], 1), { boneIndex: 9, path: Skeleton2DAnimationPath.Rotation }),
|
|
70
|
+
]);
|
|
71
|
+
applyAnimationClipToSkeleton2D(clip, s, 1);
|
|
72
|
+
expect(s.bones[0].rotation).toBe(7); // untouched
|
|
73
|
+
});
|
|
74
|
+
});
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import type { Bone2D, MeshAttachment2D, Skin2D } from '@flighthq/types';
|
|
2
|
+
import { MeshAttachment2DKind, TransformMode2D } from '@flighthq/types';
|
|
3
|
+
import { describe, expect, it } from 'vitest';
|
|
4
|
+
|
|
5
|
+
import { deformSkeleton2DMeshAttachment } from './deformMeshAttachment2D';
|
|
6
|
+
import { computeSkeleton2DWorldTransforms, createSkeleton2D } from './skeleton2d';
|
|
7
|
+
|
|
8
|
+
function makeBone(overrides: Partial<Bone2D> = {}): Bone2D {
|
|
9
|
+
return {
|
|
10
|
+
length: 0,
|
|
11
|
+
name: null,
|
|
12
|
+
parentIndex: -1,
|
|
13
|
+
rotation: 0,
|
|
14
|
+
scaleX: 1,
|
|
15
|
+
scaleY: 1,
|
|
16
|
+
shearX: 0,
|
|
17
|
+
shearY: 0,
|
|
18
|
+
transformMode: TransformMode2D.Normal,
|
|
19
|
+
x: 0,
|
|
20
|
+
y: 0,
|
|
21
|
+
...overrides,
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function weightedMesh(skin: Skin2D, vertexCount: number): MeshAttachment2D {
|
|
26
|
+
return {
|
|
27
|
+
kind: MeshAttachment2DKind,
|
|
28
|
+
skin,
|
|
29
|
+
triangles: new Uint16Array(),
|
|
30
|
+
uvs: new Float32Array(vertexCount * 2),
|
|
31
|
+
vertexCount,
|
|
32
|
+
vertices: null,
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
describe('deformSkeleton2DMeshAttachment', () => {
|
|
37
|
+
it('deforms a single-bone weighted vertex through the bone world transform', () => {
|
|
38
|
+
// One bone rotated 90° at (5,0). One vertex, one influence: offset (1,0) in the bone, weight 1.
|
|
39
|
+
const s = createSkeleton2D([makeBone({ x: 5, rotation: 90 })]);
|
|
40
|
+
computeSkeleton2DWorldTransforms(s);
|
|
41
|
+
const mesh = weightedMesh({ influenceCounts: new Uint16Array([1]), influences: new Float32Array([0, 1, 0, 1]) }, 1);
|
|
42
|
+
const out = new Float32Array(2);
|
|
43
|
+
deformSkeleton2DMeshAttachment(out, mesh, s, 0);
|
|
44
|
+
// (1,0) rotated 90° = (0,1), + translation (5,0) → (5,1).
|
|
45
|
+
expect(out[0]).toBeCloseTo(5, 5);
|
|
46
|
+
expect(out[1]).toBeCloseTo(1, 5);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it('blends two bones by weight (0.5/0.5 → midpoint of their translations)', () => {
|
|
50
|
+
const s = createSkeleton2D([makeBone({ x: 0 }), makeBone({ x: 10 })]);
|
|
51
|
+
computeSkeleton2DWorldTransforms(s);
|
|
52
|
+
// Vertex at each bone's origin (0,0), weighted 0.5 to bone 0 and 0.5 to bone 1.
|
|
53
|
+
const skin: Skin2D = {
|
|
54
|
+
influenceCounts: new Uint16Array([2]),
|
|
55
|
+
influences: new Float32Array([0, 0, 0, 0.5, 1, 0, 0, 0.5]),
|
|
56
|
+
};
|
|
57
|
+
const out = new Float32Array(2);
|
|
58
|
+
deformSkeleton2DMeshAttachment(out, weightedMesh(skin, 1), s, 0);
|
|
59
|
+
expect(out[0]).toBeCloseTo(5, 5); // midpoint of x=0 and x=10
|
|
60
|
+
expect(out[1]).toBeCloseTo(0, 5);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it('transforms a rigid mesh by the slot bone world matrix (alias-safe in place)', () => {
|
|
64
|
+
const s = createSkeleton2D([makeBone({ x: 5, rotation: 90 })]);
|
|
65
|
+
computeSkeleton2DWorldTransforms(s);
|
|
66
|
+
const verts = new Float32Array([2, 0]); // one vertex, local to the bone
|
|
67
|
+
const mesh: MeshAttachment2D = {
|
|
68
|
+
kind: MeshAttachment2DKind,
|
|
69
|
+
skin: null,
|
|
70
|
+
triangles: new Uint16Array(),
|
|
71
|
+
uvs: new Float32Array(2),
|
|
72
|
+
vertexCount: 1,
|
|
73
|
+
vertices: verts,
|
|
74
|
+
};
|
|
75
|
+
// Deform in place (out === vertices) to exercise alias safety.
|
|
76
|
+
deformSkeleton2DMeshAttachment(verts, mesh, s, 0);
|
|
77
|
+
// (2,0) rotated 90° = (0,2), + (5,0) → (5,2).
|
|
78
|
+
expect(verts[0]).toBeCloseTo(5, 5);
|
|
79
|
+
expect(verts[1]).toBeCloseTo(2, 5);
|
|
80
|
+
});
|
|
81
|
+
});
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import type { Bone2D, RegionAttachment2D } from '@flighthq/types';
|
|
2
|
+
import { RegionAttachment2DKind, TransformMode2D } from '@flighthq/types';
|
|
3
|
+
import { describe, expect, it } from 'vitest';
|
|
4
|
+
|
|
5
|
+
import { computeSkeleton2DRegionAttachmentVertices } from './regionAttachment2D';
|
|
6
|
+
import { computeSkeleton2DWorldTransforms, createSkeleton2D } from './skeleton2d';
|
|
7
|
+
|
|
8
|
+
function makeBone(overrides: Partial<Bone2D> = {}): Bone2D {
|
|
9
|
+
return {
|
|
10
|
+
length: 0,
|
|
11
|
+
name: null,
|
|
12
|
+
parentIndex: -1,
|
|
13
|
+
rotation: 0,
|
|
14
|
+
scaleX: 1,
|
|
15
|
+
scaleY: 1,
|
|
16
|
+
shearX: 0,
|
|
17
|
+
shearY: 0,
|
|
18
|
+
transformMode: TransformMode2D.Normal,
|
|
19
|
+
x: 0,
|
|
20
|
+
y: 0,
|
|
21
|
+
...overrides,
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function region(overrides: Partial<RegionAttachment2D> = {}): RegionAttachment2D {
|
|
26
|
+
return {
|
|
27
|
+
kind: RegionAttachment2DKind,
|
|
28
|
+
height: 2,
|
|
29
|
+
rotation: 0,
|
|
30
|
+
scaleX: 1,
|
|
31
|
+
scaleY: 1,
|
|
32
|
+
width: 4,
|
|
33
|
+
x: 0,
|
|
34
|
+
y: 0,
|
|
35
|
+
...overrides,
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
describe('computeSkeleton2DRegionAttachmentVertices', () => {
|
|
40
|
+
it('offsets a 4×2 region rect by the bone translation (BL, TL, TR, BR order)', () => {
|
|
41
|
+
const s = createSkeleton2D([makeBone({ x: 10, y: 5 })]);
|
|
42
|
+
computeSkeleton2DWorldTransforms(s);
|
|
43
|
+
const out = new Float32Array(8);
|
|
44
|
+
computeSkeleton2DRegionAttachmentVertices(out, region(), s, 0);
|
|
45
|
+
// Corners (±2, ±1) shifted by (10,5): BL(8,4) TL(8,6) TR(12,6) BR(12,4).
|
|
46
|
+
expect(Array.from(out)).toEqual([8, 4, 8, 6, 12, 6, 12, 4]);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it('rotates the region rect through a 90° bone', () => {
|
|
50
|
+
const s = createSkeleton2D([makeBone({ rotation: 90 })]);
|
|
51
|
+
computeSkeleton2DWorldTransforms(s);
|
|
52
|
+
const out = new Float32Array(8);
|
|
53
|
+
computeSkeleton2DRegionAttachmentVertices(out, region(), s, 0);
|
|
54
|
+
// BL local (-2,-1) rotated 90° CCW → (1,-2).
|
|
55
|
+
expect(out[0]).toBeCloseTo(1, 5);
|
|
56
|
+
expect(out[1]).toBeCloseTo(-2, 5);
|
|
57
|
+
// TR local (2,1) → (-1,2).
|
|
58
|
+
expect(out[4]).toBeCloseTo(-1, 5);
|
|
59
|
+
expect(out[5]).toBeCloseTo(2, 5);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it('applies the region local offset and scale on top of the bone', () => {
|
|
63
|
+
const s = createSkeleton2D([makeBone()]);
|
|
64
|
+
computeSkeleton2DWorldTransforms(s);
|
|
65
|
+
const out = new Float32Array(8);
|
|
66
|
+
// Region offset to (3,0), doubled in x: half-width 2·2 = 4, so BL at (3-4, 0-1) = (-1,-1).
|
|
67
|
+
computeSkeleton2DRegionAttachmentVertices(out, region({ x: 3, scaleX: 2 }), s, 0);
|
|
68
|
+
expect(out[0]).toBeCloseTo(-1, 5);
|
|
69
|
+
expect(out[1]).toBeCloseTo(-1, 5);
|
|
70
|
+
});
|
|
71
|
+
});
|
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
import { createMatrix } from '@flighthq/geometry';
|
|
2
|
+
import type { Bone2D } from '@flighthq/types';
|
|
3
|
+
import { TransformMode2D } from '@flighthq/types';
|
|
4
|
+
import { describe, expect, it } from 'vitest';
|
|
5
|
+
|
|
6
|
+
import {
|
|
7
|
+
cloneSkeleton2D,
|
|
8
|
+
computeSkeleton2DBoneMatrices,
|
|
9
|
+
computeSkeleton2DWorldTransforms,
|
|
10
|
+
createSkeleton2D,
|
|
11
|
+
disposeSkeleton2D,
|
|
12
|
+
equalsSkeleton2D,
|
|
13
|
+
getSkeleton2DBoneIndexByName,
|
|
14
|
+
getSkeleton2DBoneWorldMatrix,
|
|
15
|
+
setSkeleton2DBindPose,
|
|
16
|
+
validateSkeleton2D,
|
|
17
|
+
} from './skeleton2d';
|
|
18
|
+
|
|
19
|
+
function makeBone(overrides: Partial<Bone2D> = {}): Bone2D {
|
|
20
|
+
return {
|
|
21
|
+
length: 0,
|
|
22
|
+
name: null,
|
|
23
|
+
parentIndex: -1,
|
|
24
|
+
rotation: 0,
|
|
25
|
+
scaleX: 1,
|
|
26
|
+
scaleY: 1,
|
|
27
|
+
shearX: 0,
|
|
28
|
+
shearY: 0,
|
|
29
|
+
transformMode: TransformMode2D.Normal,
|
|
30
|
+
x: 0,
|
|
31
|
+
y: 0,
|
|
32
|
+
...overrides,
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
describe('cloneSkeleton2D', () => {
|
|
37
|
+
it('deep-copies bones so the clone poses independently', () => {
|
|
38
|
+
const s = createSkeleton2D([makeBone({ name: 'root', rotation: 30 })]);
|
|
39
|
+
const c = cloneSkeleton2D(s);
|
|
40
|
+
c.bones[0].rotation = 90;
|
|
41
|
+
expect(s.bones[0].rotation).toBe(30);
|
|
42
|
+
expect(c.bones[0].rotation).toBe(90);
|
|
43
|
+
// Buffers are distinct instances.
|
|
44
|
+
expect(c.worldMatrices).not.toBe(s.worldMatrices);
|
|
45
|
+
});
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
describe('computeSkeleton2DBoneMatrices', () => {
|
|
49
|
+
it('yields identity palettes at the captured bind pose and non-identity once posed', () => {
|
|
50
|
+
const s = createSkeleton2D([makeBone({ x: 10, rotation: 45 }), makeBone({ parentIndex: 0, x: 5 })]);
|
|
51
|
+
computeSkeleton2DWorldTransforms(s);
|
|
52
|
+
setSkeleton2DBindPose(s);
|
|
53
|
+
computeSkeleton2DBoneMatrices(s);
|
|
54
|
+
// At bind pose, palette = world × inverse(world) = identity for every bone.
|
|
55
|
+
for (let i = 0; i < s.bones.length; i++) {
|
|
56
|
+
const o = i * 6;
|
|
57
|
+
expect(s.boneMatrices[o]).toBeCloseTo(1, 4);
|
|
58
|
+
expect(s.boneMatrices[o + 1]).toBeCloseTo(0, 4);
|
|
59
|
+
expect(s.boneMatrices[o + 2]).toBeCloseTo(0, 4);
|
|
60
|
+
expect(s.boneMatrices[o + 3]).toBeCloseTo(1, 4);
|
|
61
|
+
expect(s.boneMatrices[o + 4]).toBeCloseTo(0, 4);
|
|
62
|
+
expect(s.boneMatrices[o + 5]).toBeCloseTo(0, 4);
|
|
63
|
+
}
|
|
64
|
+
// Repose the root; the palette leaves identity.
|
|
65
|
+
s.bones[0].rotation = 90;
|
|
66
|
+
computeSkeleton2DWorldTransforms(s);
|
|
67
|
+
computeSkeleton2DBoneMatrices(s);
|
|
68
|
+
const changed = Math.abs(s.boneMatrices[0] - 1) > 1e-3 || Math.abs(s.boneMatrices[1]) > 1e-3;
|
|
69
|
+
expect(changed).toBe(true);
|
|
70
|
+
});
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
describe('computeSkeleton2DWorldTransforms', () => {
|
|
74
|
+
it('rotates a root 90° into an (a=0,b=1,c=-1,d=0) world matrix at its translation', () => {
|
|
75
|
+
const s = createSkeleton2D([makeBone({ x: 10, y: 0, rotation: 90 })]);
|
|
76
|
+
computeSkeleton2DWorldTransforms(s);
|
|
77
|
+
expect(s.worldMatrices[0]).toBeCloseTo(0, 5); // a
|
|
78
|
+
expect(s.worldMatrices[1]).toBeCloseTo(1, 5); // b
|
|
79
|
+
expect(s.worldMatrices[2]).toBeCloseTo(-1, 5); // c
|
|
80
|
+
expect(s.worldMatrices[3]).toBeCloseTo(0, 5); // d
|
|
81
|
+
expect(s.worldMatrices[4]).toBeCloseTo(10, 5); // tx
|
|
82
|
+
expect(s.worldMatrices[5]).toBeCloseTo(0, 5); // ty
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it('places a child at parent × local (2 units along a 90°-rotated parent maps to +y)', () => {
|
|
86
|
+
const s = createSkeleton2D([makeBone({ x: 10, y: 0, rotation: 90 }), makeBone({ parentIndex: 0, x: 2, y: 0 })]);
|
|
87
|
+
computeSkeleton2DWorldTransforms(s);
|
|
88
|
+
const out = createMatrix();
|
|
89
|
+
getSkeleton2DBoneWorldMatrix(out, s, 1);
|
|
90
|
+
expect(out.tx).toBeCloseTo(10, 5);
|
|
91
|
+
expect(out.ty).toBeCloseTo(2, 5);
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it('OnlyTranslation inherits the parent position but not its rotation', () => {
|
|
95
|
+
const s = createSkeleton2D([
|
|
96
|
+
makeBone({ rotation: 90 }),
|
|
97
|
+
makeBone({ parentIndex: 0, x: 3, y: 0, transformMode: TransformMode2D.OnlyTranslation }),
|
|
98
|
+
]);
|
|
99
|
+
computeSkeleton2DWorldTransforms(s);
|
|
100
|
+
const out = createMatrix();
|
|
101
|
+
getSkeleton2DBoneWorldMatrix(out, s, 1);
|
|
102
|
+
// Linear part is the child's own (identity), not the parent's 90° rotation.
|
|
103
|
+
expect(out.a).toBeCloseTo(1, 5);
|
|
104
|
+
expect(out.b).toBeCloseTo(0, 5);
|
|
105
|
+
expect(out.c).toBeCloseTo(0, 5);
|
|
106
|
+
expect(out.d).toBeCloseTo(1, 5);
|
|
107
|
+
// Position still follows the parent (3 along parent +x → +y in world).
|
|
108
|
+
expect(out.tx).toBeCloseTo(0, 5);
|
|
109
|
+
expect(out.ty).toBeCloseTo(3, 5);
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
it('NoScale strips the parent scale but keeps its rotation', () => {
|
|
113
|
+
const s = createSkeleton2D([
|
|
114
|
+
makeBone({ scaleX: 3, scaleY: 3 }),
|
|
115
|
+
makeBone({ parentIndex: 0, transformMode: TransformMode2D.NoScale }),
|
|
116
|
+
]);
|
|
117
|
+
computeSkeleton2DWorldTransforms(s);
|
|
118
|
+
const out = createMatrix();
|
|
119
|
+
getSkeleton2DBoneWorldMatrix(out, s, 1);
|
|
120
|
+
// Parent's 3× scale removed → the child's world linear part is unit (identity here, no rotation).
|
|
121
|
+
expect(out.a).toBeCloseTo(1, 5);
|
|
122
|
+
expect(out.b).toBeCloseTo(0, 5);
|
|
123
|
+
expect(out.c).toBeCloseTo(0, 5);
|
|
124
|
+
expect(out.d).toBeCloseTo(1, 5);
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
it('NoRotationOrReflection keeps the parent scale but strips its rotation', () => {
|
|
128
|
+
const s = createSkeleton2D([
|
|
129
|
+
makeBone({ rotation: 90, scaleX: 2, scaleY: 2 }),
|
|
130
|
+
makeBone({ parentIndex: 0, transformMode: TransformMode2D.NoRotationOrReflection }),
|
|
131
|
+
]);
|
|
132
|
+
computeSkeleton2DWorldTransforms(s);
|
|
133
|
+
const out = createMatrix();
|
|
134
|
+
getSkeleton2DBoneWorldMatrix(out, s, 1);
|
|
135
|
+
// Parent's 90° rotation removed, its 2× scale kept → axis-aligned 2× world.
|
|
136
|
+
expect(out.a).toBeCloseTo(2, 5);
|
|
137
|
+
expect(out.b).toBeCloseTo(0, 5);
|
|
138
|
+
expect(out.c).toBeCloseTo(0, 5);
|
|
139
|
+
expect(out.d).toBeCloseTo(2, 5);
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
it('NoScaleOrReflection never flips the child under a reflected parent', () => {
|
|
143
|
+
const s = createSkeleton2D([
|
|
144
|
+
makeBone({ scaleX: -2, scaleY: 2 }), // reflected (negative X scale)
|
|
145
|
+
makeBone({ parentIndex: 0, transformMode: TransformMode2D.NoScaleOrReflection }),
|
|
146
|
+
]);
|
|
147
|
+
computeSkeleton2DWorldTransforms(s);
|
|
148
|
+
const out = createMatrix();
|
|
149
|
+
getSkeleton2DBoneWorldMatrix(out, s, 1);
|
|
150
|
+
// Determinant stays positive (no reflection) and unit magnitude (no scale).
|
|
151
|
+
const det = out.a * out.d - out.c * out.b;
|
|
152
|
+
expect(det).toBeCloseTo(1, 5);
|
|
153
|
+
});
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
describe('createSkeleton2D', () => {
|
|
157
|
+
it('sizes the flat buffers to 6 floats per bone', () => {
|
|
158
|
+
const s = createSkeleton2D([makeBone(), makeBone({ parentIndex: 0 })]);
|
|
159
|
+
expect(s.worldMatrices.length).toBe(12);
|
|
160
|
+
expect(s.inverseBindMatrices.length).toBe(12);
|
|
161
|
+
expect(s.boneMatrices.length).toBe(12);
|
|
162
|
+
expect(s.slots).toBeNull();
|
|
163
|
+
});
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
describe('disposeSkeleton2D', () => {
|
|
167
|
+
it('clears bones and slots for GC', () => {
|
|
168
|
+
const s = createSkeleton2D([makeBone()], []);
|
|
169
|
+
disposeSkeleton2D(s);
|
|
170
|
+
expect(s.bones.length).toBe(0);
|
|
171
|
+
expect(s.slots).toBeNull();
|
|
172
|
+
});
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
describe('equalsSkeleton2D', () => {
|
|
176
|
+
it('is true for a fresh clone and false after a bone edit', () => {
|
|
177
|
+
const s = createSkeleton2D([makeBone({ rotation: 15 })]);
|
|
178
|
+
const c = cloneSkeleton2D(s);
|
|
179
|
+
expect(equalsSkeleton2D(s, c)).toBe(true);
|
|
180
|
+
c.bones[0].rotation = 16;
|
|
181
|
+
expect(equalsSkeleton2D(s, c)).toBe(false);
|
|
182
|
+
});
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
describe('getSkeleton2DBoneIndexByName', () => {
|
|
186
|
+
it('finds a named bone and returns -1 for a miss', () => {
|
|
187
|
+
const s = createSkeleton2D([makeBone({ name: 'root' }), makeBone({ parentIndex: 0, name: 'arm' })]);
|
|
188
|
+
expect(getSkeleton2DBoneIndexByName(s, 'arm')).toBe(1);
|
|
189
|
+
expect(getSkeleton2DBoneIndexByName(s, 'leg')).toBe(-1);
|
|
190
|
+
});
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
describe('getSkeleton2DBoneWorldMatrix', () => {
|
|
194
|
+
it('writes the world matrix in range and returns false out of range', () => {
|
|
195
|
+
const s = createSkeleton2D([makeBone({ x: 7 })]);
|
|
196
|
+
computeSkeleton2DWorldTransforms(s);
|
|
197
|
+
const out = createMatrix();
|
|
198
|
+
expect(getSkeleton2DBoneWorldMatrix(out, s, 0)).toBe(true);
|
|
199
|
+
expect(out.tx).toBeCloseTo(7, 5);
|
|
200
|
+
expect(getSkeleton2DBoneWorldMatrix(out, s, 5)).toBe(false);
|
|
201
|
+
expect(getSkeleton2DBoneWorldMatrix(out, s, -1)).toBe(false);
|
|
202
|
+
});
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
describe('setSkeleton2DBindPose', () => {
|
|
206
|
+
it('captures the inverse of the current world so the palette is identity at bind', () => {
|
|
207
|
+
const s = createSkeleton2D([makeBone({ x: 4, rotation: 60, scaleX: 2 })]);
|
|
208
|
+
computeSkeleton2DWorldTransforms(s);
|
|
209
|
+
setSkeleton2DBindPose(s);
|
|
210
|
+
computeSkeleton2DBoneMatrices(s);
|
|
211
|
+
expect(s.boneMatrices[0]).toBeCloseTo(1, 4);
|
|
212
|
+
expect(s.boneMatrices[3]).toBeCloseTo(1, 4);
|
|
213
|
+
expect(s.boneMatrices[4]).toBeCloseTo(0, 4);
|
|
214
|
+
expect(s.boneMatrices[5]).toBeCloseTo(0, 4);
|
|
215
|
+
});
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
describe('validateSkeleton2D', () => {
|
|
219
|
+
it('returns null for a valid parent-before-child skeleton', () => {
|
|
220
|
+
const s = createSkeleton2D([makeBone(), makeBone({ parentIndex: 0 })]);
|
|
221
|
+
expect(validateSkeleton2D(s)).toBeNull();
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
it('reports a child whose parentIndex is not before it', () => {
|
|
225
|
+
const s = createSkeleton2D([makeBone({ parentIndex: 1 }), makeBone({ parentIndex: 0 })]);
|
|
226
|
+
expect(validateSkeleton2D(s)).toContain('parent-before-child');
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
it('reports a mis-sized buffer', () => {
|
|
230
|
+
const s = createSkeleton2D([makeBone()]);
|
|
231
|
+
s.worldMatrices = new Float32Array(3);
|
|
232
|
+
expect(validateSkeleton2D(s)).toContain('worldMatrices');
|
|
233
|
+
});
|
|
234
|
+
});
|