feat: 增加轨迹

This commit is contained in:
tongtong committed 2026-09-17 09:09:06 +08:00
1 parent b9fd289caf
commit e74cf6d28b
42 files changed
+4010 -570

No files matched your search

+95 -9
View File
@@ -12,6 +12,7 @@ import {
Boxes,
Camera,
ChevronRight,
Circle,
Expand,
Grid2X2,
Grid3X3,
@@ -21,7 +22,9 @@ import {
Plus,
Ratio,
Rotate3D,
Route,
Scale3D,
Square,
Trash2,
UserPlus,
Video,
@@ -29,8 +32,8 @@ import {
type LucideIcon,
} from "lucide-react";
import { requestViewportCapture } from "../io/captureBridge";
import { importPanoramaFile } from "../runtime/panoramaCommand";
import { readLocalModelFile } from "../loaders/localModelImport";
import { readPanoramaFile } from "../loaders/panoramaImport";
import {
getModelLibraryItems,
MODEL_LIBRARY_CATEGORIES,
@@ -54,6 +57,11 @@ type ToolbarAction = {
label: string;
icon: LucideIcon;
mode?: TransformMode;
/** 非变换模式的自定义切换态(如「录制」) */
pressed?: boolean;
disabled?: boolean;
title?: string;
className?: string;
onClick: () => void;
};
@@ -136,6 +144,37 @@ export function ViewportToolbar({
const setTransformMode = useDirectorStore((state) => state.setTransformMode);
const setViewportAspectRatio = useDirectorStore((state) => state.setViewportAspectRatio);
const toggleViewportPanelsCollapsed = useDirectorStore((state) => state.toggleViewportPanelsCollapsed);
const project = useDirectorStore((state) => state.project);
const selectedObjectId = useDirectorStore((state) => state.selectedObjectId);
const timelineVisible = useDirectorStore((state) => state.timelineVisible);
const toggleTimelineVisible = useDirectorStore((state) => state.toggleTimelineVisible);
const trajectoryPlacementObjectId = useDirectorStore((state) => state.trajectoryPlacementObjectId);
const startTrajectoryPlacement = useDirectorStore((state) => state.startTrajectoryPlacement);
const stopTrajectoryPlacement = useDirectorStore((state) => state.stopTrajectoryPlacement);
// 轨迹打点目标:只有角色带运动轨迹(画布打点模式只接受角色)
const selectedObject = project.objects.find((item) => item.id === selectedObjectId) ?? null;
const selectedCharacter = selectedObject?.kind === "character" ? selectedObject : null;
const canPlaceTrajectory = Boolean(selectedCharacter);
const trajectoryOwnerName = selectedCharacter?.name ?? "";
const isPlacingTrajectory =
Boolean(selectedCharacter) && trajectoryPlacementObjectId === selectedCharacter?.id;
// 打点模式下按 Esc 退出
useEffect(() => {
if (!trajectoryPlacementObjectId) return undefined;
function handleKeyDown(event: KeyboardEvent) {
if (event.key !== "Escape") return;
stopTrajectoryPlacement();
}
window.addEventListener("keydown", handleKeyDown);
return () => {
window.removeEventListener("keydown", handleKeyDown);
};
}, [stopTrajectoryPlacement, trajectoryPlacementObjectId]);
useEffect(() => {
if (!characterMenuOpen && !crowdPanelOpen && !modelLibraryOpen && !aspectRatioPanelOpen) return;
@@ -297,10 +336,8 @@ export function ViewportToolbar({
if (!file) return;
try {
const result = await readPanoramaFile(file);
addImportedAsset({ kind: "panorama", ...result });
} catch {
// The toolbar keeps file actions quiet; detailed import feedback lives in the side panel.
// 与场景面板「未连接全景图」占位卡共用同一实现
await importPanoramaFile(file);
} finally {
input.value = "";
}
@@ -439,6 +476,23 @@ export function ViewportToolbar({
setAspectRatioPanelOpen(false);
}
/** 添加轨迹:进入 / 退出「画布打点」模式(Esc 也能退出) */
function toggleTrajectoryPlacement() {
if (isPlacingTrajectory) {
stopTrajectoryPlacement();
return;
}
if (selectedCharacter) {
startTrajectoryPlacement(selectedCharacter.id);
}
}
/** 录制:显示 / 隐藏悬浮时间轴(录制面板) */
function toggleRecording() {
toggleTimelineVisible();
}
const actions: ToolbarAction[] = [
{ label: "移动", icon: Move3D, mode: "translate", onClick: () => selectTransformMode("translate") },
{ label: "旋转", icon: Rotate3D, mode: "rotate", onClick: () => selectTransformMode("rotate") },
@@ -453,23 +507,55 @@ export function ViewportToolbar({
},
{ label: "模型库", icon: Boxes, onClick: toggleModelLibrary },
{ label: "添加机位", icon: Video, onClick: addCameraFromViewport },
{ label: "选择画幅比例", icon: Ratio, onClick: toggleAspectRatioPanel },
{ label: "当前视角截图", icon: Camera, onClick: () => void handleCapture("current") },
{ label: "选择画幅比例", icon: Ratio, onClick: toggleAspectRatioPanel },
{ label: "四方位截图", icon: Grid2X2, onClick: () => void handleCapture("four") },
{ label: "十二方位截图", icon: Grid3X3, onClick: () => void handleCapture("twelve") },
{ label: "全屏", icon: Expand, onClick: toggleViewportPanelsCollapsed },
// 打点 / 录制放在最右侧
{
label: "添加轨迹",
icon: Route,
disabled: !canPlaceTrajectory,
pressed: isPlacingTrajectory,
title: canPlaceTrajectory
? isPlacingTrajectory
? `退出打点(Esc 也可):此时画布不响应选取,双击地面给「${trajectoryOwnerName}」再落一个点`
: `进入打点:之后双击画布即可给「${trajectoryOwnerName}」落一个贴地点的轨迹点`
: "请先选中一个角色,再为它添加轨迹",
onClick: toggleTrajectoryPlacement,
},
{
label: "录制",
icon: timelineVisible ? Square : Circle,
pressed: timelineVisible,
className: timelineVisible ? "is-recording" : undefined,
title: timelineVisible ? "隐藏时间轴 · 录制中按 K 打关键帧" : "显示时间轴(录制)",
onClick: toggleRecording,
},
];
function renderActionButton(action: ToolbarAction) {
const Icon = action.icon;
const active = action.mode ? transformMode === action.mode : false;
const active = action.mode ? transformMode === action.mode : Boolean(action.pressed);
const isToggle = Boolean(action.mode) || action.pressed !== undefined;
const buttonClassName = [
"ui-icon-button",
"viewport-toolbar-button",
active ? "is-active" : "",
action.className ?? "",
]
.filter(Boolean)
.join(" ");
return (
<button
key={action.label}
aria-label={action.label}
aria-pressed={action.mode ? active : undefined}
className={`ui-icon-button viewport-toolbar-button${active ? " is-active" : ""}`}
aria-pressed={isToggle ? active : undefined}
className={buttonClassName}
disabled={action.disabled}
title={action.title}
type="button"
onClick={action.onClick}
>