import { describe, expect, it } from "vitest"; import { AnimationClip, BoxGeometry, Group, Mesh, MeshStandardMaterial, Quaternion, QuaternionKeyframeTrack, VectorKeyframeTrack } from "three"; import { CHARACTERS, createCharacterModule, retargetClips, teleportTargets } from "../src/characters/index.js"; const rest = (q) => new Map([["arm", { position: { x: 0, y: 1, z: 0 }, quaternion: q }]]); describe("retargetClips", () => { it("wirft konstante Positionsspuren weg", () => { const clip = new AnimationClip("a", 1, [ new VectorKeyframeTrack("arm.position", [0, 1], [0, 1, 0, 0, 1, 0]), new QuaternionKeyframeTrack("arm.quaternion", [0], [0, 0, 0, 1]), ]); const out = retargetClips([clip], rest(new Quaternion()), rest(new Quaternion())); expect(out[0].tracks.map((t) => t.name)).toEqual(["arm.quaternion"]); }); it("überträgt Rotationen relativ zur Ruhepose", () => { const src = new Quaternion().setFromAxisAngle({ x: 0, y: 1, z: 0 }, 0.3); const dst = new Quaternion().setFromAxisAngle({ x: 0, y: 1, z: 0 }, 0.9); const clip = new AnimationClip("a", 1, [ new QuaternionKeyframeTrack("arm.quaternion", [0], [src.x, src.y, src.z, src.w]), ]); const out = retargetClips([clip], rest(src), rest(dst)); const v = out[0].tracks[0].values; expect(new Quaternion(v[0], v[1], v[2], v[3]).angleTo(dst)).toBeLessThan(1e-6); }); // Fix-Runde 1, Ruling 67: ual.glb hängt die Arme, der Quaternius-Rig steht in T (70° an // upperarm_l, s. task-11-report.md) — die Ruhepose-Differenz ist damit selbst schon fast // eine ganze Pose, nicht die paar Grad Bauartunterschied, für die "Delta" gedacht ist. Im // absoluten Modus wird die animierte Rotation unverändert übernommen (q_neu = q_anim), // ohne jede Ruhepose-Referenz — das Gegenstück zu "relativ zur Ruhepose" oben. it("übernimmt Rotationen im absoluten Modus unverändert (Ruling 67)", () => { const src = new Quaternion().setFromAxisAngle({ x: 0, y: 1, z: 0 }, 0.3); const dst = new Quaternion().setFromAxisAngle({ x: 0, y: 1, z: 0 }, 0.9); const clip = new AnimationClip("a", 1, [ new QuaternionKeyframeTrack("arm.quaternion", [0], [src.x, src.y, src.z, src.w]), ]); const out = retargetClips([clip], rest(src), rest(dst), { rotation: "absolute" }); const v = out[0].tracks[0].values; expect(new Quaternion(v[0], v[1], v[2], v[3]).angleTo(src)).toBeLessThan(1e-6); }); }); // Fix-Runde 1, Ruling 67: durchprobiert statt vermutet — Delta hielt Superhero Male/Female // bei Idle_Loop UND Walk_Loop in praktisch voller T-Pose (Screenshots r67-03..r67-04 im // Bericht), Absolute zeigte für beide einen sauberen Gang ohne Schulterverdrehung // (r67-05..r67-10), und für Mannequin sind beide Verfahren ohnehin identisch (sourceRest === // targetRest, da ual.glb sich selbst liefert). Ein Verfahren trägt für alle drei — dieser Test // hält das Messergebnis fest, damit ein künftiger Umbau nicht versehentlich zu Delta zurückfällt. describe("CHARACTERS", () => { it("verwendet absolute Rotation für alle drei Figuren (gemessen, nicht vermutet — Ruling 67)", () => { for (const c of CHARACTERS) expect(c.retarget).toBe("absolute"); }); }); describe("teleportTargets", () => { it("sortiert Wahrzeichen nach Höhe", () => { const targets = teleportTargets([ { name: "klein", xyz: [0, 0, 0], height: 30 }, { name: "groß", xyz: [10, 20, 0], height: 200 }, ]); expect(targets[0].name).toBe("groß"); expect(targets[0].z).toBe(-20); }); }); describe("createCharacterModule", () => { // Befund (Schatten-Spec): enableModelShadows lief nur in applyCharacter — das Startmodell // (ual.glb, Mannequin) warf bis zum ersten Figurenwechsel keinen Schatten. it("schaltet Schatten schon für das Startmodell ein", () => { const body = new Mesh(new BoxGeometry(), new MeshStandardMaterial()); const playerModel = new Group().add(body); createCharacterModule({ playerModel, playerModelConfig: { animations: [] } }); expect(body.castShadow).toBe(true); expect(body.receiveShadow).toBe(true); }); });