import { describe, expect, it } from "vitest"; import { AnimationClip, Bone, BoxGeometry, Group, Mesh, MeshBasicMaterial, Object3D, QuaternionKeyframeTrack, VectorKeyframeTrack } from "three"; import { applyGrip, createDummy, fingerPoseClip } from "../src/showroom/dummies.js"; const source = new AnimationClip("Pistol_Idle_Loop", 2, [ new QuaternionKeyframeTrack("index_01_r.quaternion", [0, 1], [0, 0, 0.3826834, 0.9238795, 0, 0, 0, 1]), new QuaternionKeyframeTrack("thumb_03_l.quaternion", [0, 2], [0.1, 0, 0, 0.9949874, 0, 0, 0, 1]), new QuaternionKeyframeTrack("index_04_leaf_r.quaternion", [0, 1], [0, 0, 0, 1, 0, 0, 0, 1]), new QuaternionKeyframeTrack("hand_r.quaternion", [0, 1], [0, 0, 0, 1, 0, 0, 0, 1]), new VectorKeyframeTrack("index_01_r.position", [0, 1], [0, 0, 0, 1, 1, 1]), ]); describe("fingerPoseClip", () => { // Bruch: Hand oder Blattknochen mitgenommen — die Fingerhaltung würde die Mocap-Bewegung überschreiben. it("behält nur die Drehungen der Fingerknochen", () => { expect(fingerPoseClip(source).tracks.map((track) => track.name).sort()) .toEqual(["index_01_r.quaternion", "thumb_03_l.quaternion"]); }); it("hält den ersten Keyframe über die ganze Dauer", () => { const clip = fingerPoseClip(source, "Finger_Waffe"); const track = clip.tracks.find((entry) => entry.name === "index_01_r.quaternion"); expect(clip.name).toBe("Finger_Waffe"); expect(clip.duration).toBe(1); expect([...track.times]).toEqual([0, 1]); const expected = Float32Array.from([0, 0, 0.3826834, 0.9238795, 0, 0, 0.3826834, 0.9238795]); expect([...track.values]).toEqual([...expected]); }); it("meldet einen Clip ohne Fingerspuren", () => { const empty = new AnimationClip("Leer", 1, [ new QuaternionKeyframeTrack("hand_r.quaternion", [0, 1], [0, 0, 0, 1, 0, 0, 0, 1]), ]); expect(() => fingerPoseClip(empty)).toThrow(/keine Fingerspuren/); }); }); describe("applyGrip", () => { it("setzt Position in Metern und Drehung in Grad", () => { const object = new Object3D(); applyGrip(object, { position: [0.01, -0.02, 0.03], rotation: [90, 0, -45] }); expect(object.position.toArray()).toEqual([0.01, -0.02, 0.03]); expect(object.rotation.x).toBeCloseTo(Math.PI / 2, 12); expect(object.rotation.z).toBeCloseTo(-Math.PI / 4, 12); expect(object.rotation.order).toBe("XYZ"); }); }); describe("createDummy", () => { const WEAPON_GRIP = { position: [0, 0, 0], rotation: [0, 0, 0] }; function template() { const root = new Group(); const pelvis = new Bone(); pelvis.name = "pelvis"; const right = new Bone(); right.name = "hand_r"; const left = new Bone(); left.name = "hand_l"; // Ziele der Fingerspuren aus `fingers` unten (index_01_r, thumb_03_l) — ohne sie meldet three.js beim // Binden für jeden Testlauf "No target node found for track". const indexR = new Bone(); indexR.name = "index_01_r"; const thumbL = new Bone(); thumbL.name = "thumb_03_l"; root.add(pelvis); pelvis.add(right); pelvis.add(left); right.add(indexR); left.add(thumbL); return root; } const clip = new AnimationClip("take", 1, [ new QuaternionKeyframeTrack("pelvis.quaternion", [0, 1], [0, 0, 0, 1, 0, 0, 0, 1]), ]); const fingers = fingerPoseClip(source, "Finger_Waffe"); const weapon = new Group(); weapon.add(new Mesh(new BoxGeometry(0.2, 0.05, 0.02), new MeshBasicMaterial())); // Bruch: Waffe an der falschen Hand oder ohne Griff — sie steckte im Unterarm. it("hängt eine eigene Kopie der Waffe mit Griff an die gewählte Hand", () => { const dummy = createDummy({ template: template(), clip, fingers, weapon, hand: "l", grip: { position: [0.1, 0, 0], rotation: [0, 90, 0] }, }); expect(dummy.weapon.parent.name).toBe("hand_l"); expect(dummy.weapon).not.toBe(weapon); expect(dummy.weapon.position.x).toBeCloseTo(0.1, 12); expect(dummy.weapon.rotation.y).toBeCloseTo(Math.PI / 2, 12); }); it("spielt Clip und Fingerhaltung zugleich, lässt leere Hände leer und gibt den Clip zurück", () => { const dummy = createDummy({ template: template(), clip, fingers }); expect(dummy.weapon).toBeNull(); expect(dummy.clip).toBe(clip); expect(dummy.mixer.existingAction(clip).isRunning()).toBe(true); expect(dummy.mixer.existingAction(fingers).isRunning()).toBe(true); }); it("meldet eine fehlende Hand", () => { const root = new Group(); const pelvis = new Bone(); pelvis.name = "pelvis"; const indexR = new Bone(); indexR.name = "index_01_r"; const thumbL = new Bone(); thumbL.name = "thumb_03_l"; root.add(pelvis); pelvis.add(indexR); pelvis.add(thumbL); expect(() => createDummy({ template: root, clip, fingers, weapon, hand: "r", grip: WEAPON_GRIP })) .toThrow(/Dummy ohne Knochen hand_r/); }); // Bruch: Waffe ohne Griff — applyGrip bekäme kein Objekt und bräche mit einem rohen TypeError ab. it("meldet eine Waffe ohne Griff", () => { expect(() => createDummy({ template: template(), clip, fingers, weapon, hand: "r", grip: null })) .toThrow(/Waffe ohne Griff/); }); });