feat: 增加轨迹

This commit is contained in:
2026-09-17 09:09:06 +08:00
parent b9fd289caf
commit e74cf6d28b
42 changed files with 4010 additions and 570 deletions

View File

@@ -2,6 +2,7 @@ import { act, render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { vi } from "vitest";
import { createInitialDirectorState, useDirectorStore } from "./editor/store/directorStore";
import { usePlaybackStore } from "./editor/runtime/playbackStore";
vi.mock("./editor/canvas/DirectorCanvas", () => ({
DirectorCanvas: () => <div data-testid="mock-director-canvas" />,
@@ -24,7 +25,9 @@ it("renders the director desk header and view mode switch", () => {
expect(screen.getByRole("button", { name: "机位视角" })).toBeInTheDocument();
expect(container.querySelector(".top-bar-center .mode-toggle")).toBeInTheDocument();
expect(screen.queryByLabelText("帮助")).not.toBeInTheDocument();
expect(screen.getByLabelText("关闭")).toBeInTheDocument();
// 右上角的「关闭」按钮已去掉
expect(screen.queryByLabelText("关闭")).not.toBeInTheDocument();
expect(container.querySelector(".top-bar-actions")).not.toBeInTheDocument();
});
it("notifies the host canvas when the director desk app is ready", () => {
@@ -40,22 +43,6 @@ it("notifies the host canvas when the director desk app is ready", () => {
postMessage.mockRestore();
});
it("notifies the host canvas when the director desk close button is clicked", async () => {
const user = userEvent.setup();
const postMessage = vi.spyOn(window.parent, "postMessage").mockImplementation(() => undefined);
render(<App />);
await user.click(screen.getByRole("button", { name: "关闭" }));
expect(postMessage).toHaveBeenCalledWith(
{ type: "storyai:director-desk-close" },
window.location.origin
);
postMessage.mockRestore();
});
it("uses a full-width director desk frame instead of floating card columns", () => {
const { container } = render(<App />);
const shell = container.querySelector(".director-shell.director-shell-fullbleed");
@@ -85,6 +72,24 @@ it("collapses both side panels from the fullscreen toolbar action", async () =>
expect(screen.getByLabelText("属性")).toHaveAttribute("aria-hidden", "true");
});
it("shows the floating timeline by default and hides it from the record action", () => {
const { container } = render(<App />);
expect(container.querySelector(".timeline-panel")).toBeInTheDocument();
act(() => {
useDirectorStore.getState().toggleTimelineVisible();
});
expect(container.querySelector(".timeline-panel")).not.toBeInTheDocument();
act(() => {
useDirectorStore.getState().toggleTimelineVisible();
});
expect(container.querySelector(".timeline-panel")).toBeInTheDocument();
});
it("switches from director mode to camera mode", async () => {
const user = userEvent.setup();
render(<App />);
@@ -101,6 +106,68 @@ it("switches from director mode to camera mode", async () => {
expect(cameraButton).toHaveAttribute("aria-pressed", "true");
});
it("adds a keyframe at the playhead when K is pressed while the recording panel is open", async () => {
const user = userEvent.setup();
render(<App />);
act(() => {
useDirectorStore.getState().selectObject("char_default_a");
usePlaybackStore.getState().setPlayhead(1.5);
});
await user.keyboard("k");
const track = useDirectorStore
.getState()
.project.timeline.tracks.find((item) => item.objectId === "char_default_a");
expect(track?.keyframes).toHaveLength(1);
expect(track?.keyframes[0]?.time).toBe(1.5);
});
it("records a camera keyframe with K when the selected target is a camera object", async () => {
const user = userEvent.setup();
render(<App />);
act(() => {
useDirectorStore.getState().selectObject("cam_object_1");
usePlaybackStore.getState().setPlayhead(2);
});
await user.keyboard("k");
const track = useDirectorStore
.getState()
.project.timeline.cameraTracks.find((item) => item.cameraId === "cam_1");
expect(track?.keyframes).toHaveLength(1);
expect(track?.keyframes[0]?.time).toBe(2);
});
it("ignores K while the recording panel is hidden and while typing in a field", async () => {
const user = userEvent.setup();
render(<App />);
act(() => {
useDirectorStore.getState().selectObject("char_default_a");
// 录制面板隐藏 → K 不再是快捷键
useDirectorStore.getState().toggleTimelineVisible();
});
await user.keyboard("k");
expect(useDirectorStore.getState().project.timeline.tracks).toHaveLength(0);
act(() => {
useDirectorStore.getState().toggleTimelineVisible();
});
// 在输入框里敲 k 只应该输入文字,不能打点
await user.click(screen.getByLabelText("搜索场景内容"));
await user.keyboard("k");
expect(useDirectorStore.getState().project.timeline.tracks).toHaveLength(0);
});
it("supports Cmd/Ctrl+C and Cmd/Ctrl+V to duplicate the selected object", async () => {
const user = userEvent.setup();
render(<App />);