- 新增 GLB/GLTF 模型加载与渲染,支持内嵌动画播放 - 通过 IndexedDB 持久化大体积模型二进制,规避 localStorage 限制 - 新增动作片段序列,时间轴可添加、编辑与编排多段动作 - 优化机位视角相机同步,修复角度修改不生效问题 - 扩展骨骼识别,兼容 Mixamo 前缀及 UE4 骨架
64 lines
2.4 KiB
TypeScript
64 lines
2.4 KiB
TypeScript
import { describe, expect, it } from "vitest";
|
|
import { getActiveMotionClipAtTime, resolveMotionClipEnd } from "./motion";
|
|
import type { MotionClip } from "../../schema/directorProject";
|
|
|
|
function clip(partial: Partial<MotionClip> & Pick<MotionClip, "id" | "start" | "end">): MotionClip {
|
|
return {
|
|
kind: "walk",
|
|
speed: 1,
|
|
amplitude: 1,
|
|
enabled: true,
|
|
...partial,
|
|
};
|
|
}
|
|
|
|
describe("getActiveMotionClipAtTime", () => {
|
|
it("returns null when there are no clips", () => {
|
|
expect(getActiveMotionClipAtTime(undefined, 1, 10)).toBeNull();
|
|
expect(getActiveMotionClipAtTime([], 1, 10)).toBeNull();
|
|
});
|
|
|
|
it("returns the clip covering the current time", () => {
|
|
const a = clip({ id: "a", start: 0, end: 2 });
|
|
const b = clip({ id: "b", kind: "run", start: 2, end: 5 });
|
|
expect(getActiveMotionClipAtTime([a, b], 1, 5)?.id).toBe("a");
|
|
expect(getActiveMotionClipAtTime([a, b], 3, 5)?.id).toBe("b");
|
|
});
|
|
|
|
it("later clips win on overlap (last covering segment wins)", () => {
|
|
const a = clip({ id: "a", start: 0, end: 5 });
|
|
const b = clip({ id: "b", start: 2, end: 6 });
|
|
expect(getActiveMotionClipAtTime([a, b], 3, 6)?.id).toBe("b");
|
|
});
|
|
|
|
it("treats an open-ended clip (Infinity) as lasting to the timeline end", () => {
|
|
const a = clip({ id: "a", start: 0, end: Number.POSITIVE_INFINITY });
|
|
expect(getActiveMotionClipAtTime([a], 999, 10)?.id).toBe("a");
|
|
});
|
|
|
|
it("returns null for disabled / none clips (character stays static)", () => {
|
|
const disabled = clip({ id: "a", start: 0, end: 5, enabled: false });
|
|
const none = clip({ id: "b", start: 0, end: 5, kind: "none" });
|
|
expect(getActiveMotionClipAtTime([disabled], 1, 5)).toBeNull();
|
|
expect(getActiveMotionClipAtTime([none], 1, 5)).toBeNull();
|
|
});
|
|
|
|
it("returns null outside any clip", () => {
|
|
const a = clip({ id: "a", start: 1, end: 4 });
|
|
expect(getActiveMotionClipAtTime([a], 0, 5)).toBeNull();
|
|
expect(getActiveMotionClipAtTime([a], 5, 5)).toBeNull();
|
|
});
|
|
});
|
|
|
|
describe("resolveMotionClipEnd", () => {
|
|
it("keeps a finite end within bounds", () => {
|
|
const a = clip({ id: "a", start: 1, end: 4 });
|
|
expect(resolveMotionClipEnd(a, 10)).toBe(4);
|
|
});
|
|
|
|
it("resolves an open-ended end to the timeline duration", () => {
|
|
const a = clip({ id: "a", start: 1, end: Number.POSITIVE_INFINITY });
|
|
expect(resolveMotionClipEnd(a, 10)).toBe(10);
|
|
});
|
|
});
|