112 lines
3.9 KiB
TypeScript
112 lines
3.9 KiB
TypeScript
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>
|
||
);
|
||
}
|