feat: 增加轨迹
This commit is contained in:
1 parent
b9fd289caf
commit
e74cf6d28b
42 files changed
+4010
-570
No files matched your search
@@ -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}
|
||||
>
|
||||
|
||||
Reference in new issue
Block a user