Files
ttstd-3d-director/src/App.tsx

112 lines
3.9 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import "./styles/index.css";
import { useEffect } from "react";
import { DirectorDeskShell } from "./app/layout/DirectorDeskShell";
import { DirectorCanvas } from "./editor/canvas/DirectorCanvas";
import { initDirectorDeskHostBridge } from "./editor/io/hostBridge";
import { addKeyframeAtPlayhead } from "./editor/runtime/keyframeCommand";
import { useDirectorStore } from "./editor/store/directorStore";
function isEditableShortcutTarget(target: EventTarget | null) {
if (!(target instanceof HTMLElement)) return false;
return target.isContentEditable || ["INPUT", "TEXTAREA", "SELECT"].includes(target.tagName);
}
export default function App() {
const viewMode = useDirectorStore((state) => state.viewMode);
const setViewMode = useDirectorStore((state) => state.setViewMode);
const hydratePersistedModelBinaries = useDirectorStore((state) => state.hydratePersistedModelBinaries);
useEffect(() => {
initDirectorDeskHostBridge();
window.parent?.postMessage({ type: "storyai:director-desk-ready" }, window.location.origin);
}, []);
useEffect(() => {
// 启动后把持久化的 GLB/GLTF 二进制从 IndexedDB 还原成 objectURL(localStorage 不存大文件)。
void hydratePersistedModelBinaries();
}, [hydratePersistedModelBinaries]);
useEffect(() => {
function handleKeyDown(event: KeyboardEvent) {
if (event.defaultPrevented || isEditableShortcutTarget(event.target)) return;
// 录制时(悬浮时间轴 = 录制面板打开)按 K:在当前时间指针处打一个关键帧。
// 只在录制面板打开时生效,避免平时打字/其它操作误触;长按不重复打点。
if (event.key.toLowerCase() === "k" && !event.metaKey && !event.ctrlKey && !event.altKey) {
if (event.repeat || !useDirectorStore.getState().timelineVisible) return;
if (addKeyframeAtPlayhead()) {
event.preventDefault();
}
return;
}
if (!event.metaKey && !event.ctrlKey) return;
const key = event.key.toLowerCase();
if (key === "c") {
event.preventDefault();
useDirectorStore.getState().copySelectedObjects();
return;
}
if (key === "v") {
event.preventDefault();
useDirectorStore.getState().pasteClipboardObjects();
return;
}
if (key === "z" && !event.shiftKey) {
event.preventDefault();
useDirectorStore.getState().undo();
return;
}
// 重做:Ctrl/Cmd+Shift+Z 或 Ctrl+Y(Mac 上常见为 Cmd+Shift+Z)
if ((key === "z" && event.shiftKey) || key === "y") {
event.preventDefault();
useDirectorStore.getState().redo();
}
}
window.addEventListener("keydown", handleKeyDown);
return () => {
window.removeEventListener("keydown", handleKeyDown);
};
}, []);
return (
<div className="app-shell">
<header className="top-bar">
<div className="top-bar-left">
<h1 className="top-bar-title">3D导演台</h1>
</div>
<div className="top-bar-center">
<div className="mode-toggle ui-segmented" role="group" aria-label="视角切换">
<button
className={`mode-toggle-button ui-segmented-item ${viewMode === "director" ? "ui-segmented-item-active" : ""}`}
aria-pressed={viewMode === "director"}
type="button"
onClick={() => setViewMode("director")}
>
导演视角
</button>
<button
className={`mode-toggle-button ui-segmented-item ${viewMode === "camera" ? "ui-segmented-item-active" : ""}`}
aria-pressed={viewMode === "camera"}
type="button"
onClick={() => setViewMode("camera")}
>
机位视角
</button>
</div>
</div>
</header>
<DirectorDeskShell>
<DirectorCanvas />
</DirectorDeskShell>
</div>
);
}