import { createAnimationChannel, createAnimationClip, createAnimationTrack } from '@flighthq/animation/contract'; import { copyQuaternion, copyVector3, createQuaternion, createVector3 } from '@flighthq/geometry/contract'; import { createBoxMeshGeometry } from '@flighthq/mesh/contract'; import type { MeshMorph } from '@flighthq/types/contract'; import { Scene3DAnimationPathRotation, Scene3DAnimationPathScale, Scene3DAnimationPathTranslation, Scene3DAnimationPathWeights, } from '@flighthq/types/contract'; import { createMesh } from './mesh'; import { applyAnimationClipToScene3D } from './sceneAnimation'; import { createNode3D } from './sceneNode'; describe('applyAnimationClipToScene3D', () => { it('drives a node translation from a Vector3 channel', () => { const node = createNode3D(); const track = createAnimationTrack({ components: 3, times: [0, 1], values: [0, 0, 0, 10, 20, 30] }); const clip = createAnimationClip([createAnimationChannel(track, { node, path: Scene3DAnimationPathTranslation })]); applyAnimationClipToScene3D(clip, 0.5); const out = createVector3(); copyVector3(out, node.position); expect(out.x).toBeCloseTo(5); expect(out.y).toBeCloseTo(10); expect(out.z).toBeCloseTo(15); }); it('drives a node rotation from a quaternion channel', () => { const node = createNode3D(); const s = Math.sin(Math.PI / 4); const c = Math.cos(Math.PI / 4); const track = createAnimationTrack({ components: 4, times: [0, 1], values: [0, 0, 0, 1, 0, 0, s, c], }); const clip = createAnimationClip([createAnimationChannel(track, { node, path: Scene3DAnimationPathRotation })]); applyAnimationClipToScene3D(clip, 1); const out = createQuaternion(); copyQuaternion(out, node.rotation); expect(out.x).toBeCloseTo(0); expect(out.y).toBeCloseTo(0); expect(out.z).toBeCloseTo(s); expect(out.w).toBeCloseTo(c); }); it('drives a node scale', () => { const node = createNode3D(); const track = createAnimationTrack({ components: 3, times: [0, 1], values: [1, 1, 1, 3, 3, 3] }); const clip = createAnimationClip([createAnimationChannel(track, { node, path: Scene3DAnimationPathScale })]); applyAnimationClipToScene3D(clip, 1); const out = createVector3(); copyVector3(out, node.scale); expect(out.x).toBeCloseTo(3); expect(out.y).toBeCloseTo(3); expect(out.z).toBeCloseTo(3); }); it('drives a mesh morph weight array from a Weights channel', () => { const morph: MeshMorph = { targets: [ { normalDeltas: null, positionDeltas: new Float32Array(0), tangentDeltas: null }, { normalDeltas: null, positionDeltas: new Float32Array(0), tangentDeltas: null }, ], weights: new Float32Array([0, 0]), }; const mesh = createMesh(createBoxMeshGeometry(), []); mesh.morph = morph; // A 2-wide weights track: at t=0.5 it interpolates halfway between [0,0] and [1,0.5]. const track = createAnimationTrack({ components: 2, times: [0, 1], values: [0, 0, 1, 0.5] }); const clip = createAnimationClip([ createAnimationChannel(track, { node: mesh, path: Scene3DAnimationPathWeights }), ]); applyAnimationClipToScene3D(clip, 0.5); expect(morph.weights[0]).toBeCloseTo(0.5); expect(morph.weights[1]).toBeCloseTo(0.25); }); it('skips a Weights channel whose target mesh carries no morph', () => { const mesh = createMesh(createBoxMeshGeometry(), []); const track = createAnimationTrack({ components: 1, times: [0, 1], values: [0, 1] }); const clip = createAnimationClip([ createAnimationChannel(track, { node: mesh, path: Scene3DAnimationPathWeights }), ]); // No morph on the mesh — the channel is skipped without throwing. expect(() => applyAnimationClipToScene3D(clip, 1)).not.toThrow(); }); it('skips channels whose targetRef is not a scene target', () => { const node = createNode3D(); const track = createAnimationTrack({ components: 3, times: [0, 1], values: [0, 0, 0, 9, 9, 9] }); const clip = createAnimationClip([createAnimationChannel(track, null)]); applyAnimationClipToScene3D(clip, 1); const out = createVector3(); copyVector3(out, node.position); expect(out.x).toBe(0); }); });