feat: 优化时间轴

This commit is contained in:
2026-09-18 00:52:28 +08:00
parent e66e3374bf
commit 8645ea3007
19 changed files with 1539 additions and 392 deletions

View File

@@ -53,7 +53,12 @@ import {
type CameraKeyframeValue,
type Timeline,
} from "../schema/timeline";
import { getTrajectoryStartTime, shiftTrajectoryClip } from "../schema/trajectoryMotion";
import {
TRAJECTORY_POINT_MIN_GAP,
getTrajectoryPointTimes,
getTrajectoryStartTime,
shiftTrajectoryClip,
} from "../schema/trajectoryMotion";
export type TransformMode = "translate" | "rotate" | "scale";
@@ -159,8 +164,13 @@ export interface DirectorActions {
setTrajectoryPointTimes: (objectId: string, times: number[]) => void;
/** 整体平移人物轨迹片段(拖轨迹条本体):片段起点 + 所有轨迹点时间同步平移 */
setTrajectoryClipStart: (objectId: string, startTime: number) => void;
/** 修剪人物轨迹片段起点(拖轨迹条左端把手):只挪片段起点,轨迹点与停留结束时间都不动 */
setTrajectoryClipTrimStart: (objectId: string, startTime: number) => void;
/** 设置人物轨迹片段的结束时间(停留时长):角色走完最后一点后原地停留到这一刻,拖条右端把手时写入 */
setTrajectoryClipEnd: (objectId: string, endTime: number) => void;
removeTrajectoryPoint: (objectId: string, pointId: string) => void;
selectTrajectoryPoint: (objectId: string, pointId: string) => void;
/** 选中某个轨迹点;传 null 表示取消选中(点到轨道空白处 / 面板其它地方时清掉轨迹关键帧的选中态) */
selectTrajectoryPoint: (objectId: string | null, pointId: string | null) => void;
updateScene: (patch: Partial<SceneSettings>) => void;
removePanoramaAsset: () => void;
removeImportedAsset: (assetId: string) => void;
@@ -1763,21 +1773,75 @@ export const useDirectorStore = create<DirectorStore>((set, get) => {
// 片段刚性平移:起点夹在时间轴内,所有轨迹点跟着走同样的量。
// 点的时间在这里落成显式值,避免默认推导把片段「压缩」而不是「平移」。
const currentStart = getTrajectoryStartTime(item, state.project.timeline.duration);
const { startTime: nextStart, times } = shiftTrajectoryClip(
points,
state.project.timeline.duration,
getTrajectoryStartTime(item, state.project.timeline.duration),
currentStart,
startTime
);
const delta = nextStart - currentStart;
// 停留结束时间(trajectoryEndTime)同样随片段刚性平移,保持与轨迹点的相对关系
const nextEndTime =
typeof item.trajectoryEndTime === "number"
? Math.min(Math.max(item.trajectoryEndTime + delta, 0), state.project.timeline.duration)
: item.trajectoryEndTime;
return {
...item,
trajectoryStartTime: nextStart,
trajectoryEndTime: nextEndTime,
trajectoryPoints: points.map((point, index) => ({ ...point, time: times[index] })),
};
}),
},
})),
setTrajectoryClipTrimStart: (objectId, startTime) =>
commitMutation((state) => ({
...state,
project: {
...state.project,
objects: updateObjectById(state.project.objects, objectId, (item) => {
const points = item.trajectoryPoints ?? [];
if (points.length === 0) return item;
const duration = state.project.timeline.duration;
const currentStart = getTrajectoryStartTime(item, duration);
const times = getTrajectoryPointTimes(points, duration, currentStart);
const firstPointTime = times[0] ?? 0;
// 起点最多挪到「第一个轨迹点再往前一个最小间隔」处 —— 不能越过第一个点,
// 否则那一列点就落在片段起点之前,和「起点之前原地等待」的语义冲突。
const upper = Math.max(firstPointTime - TRAJECTORY_POINT_MIN_GAP, 0);
const nextStart = Math.min(Math.max(startTime, 0), upper);
return {
...item,
trajectoryStartTime: nextStart,
// 把点的时间落成显式值:这些点不该跟着起点走,否则就退化成整段平移而不是修剪起点。
// (没有显式 time 的点会按「起点 → 时间轴末尾」重新推导,改起点会连点一起拖走。)
trajectoryPoints: points.map((point, index) => ({ ...point, time: times[index] })),
};
}),
},
})),
setTrajectoryClipEnd: (objectId, endTime) =>
commitMutation((state) => ({
...state,
project: {
...state.project,
objects: updateObjectById(state.project.objects, objectId, (item) => {
const points = item.trajectoryPoints ?? [];
if (points.length === 0) return item;
const duration = state.project.timeline.duration;
const times = getTrajectoryPointTimes(points, duration, getTrajectoryStartTime(item, duration));
const lastPointTime = times.length > 0 ? times[times.length - 1] : 0;
const nextEnd = Math.min(Math.max(endTime, lastPointTime), duration);
return { ...item, trajectoryEndTime: nextEnd };
}),
},
})),
removeTrajectoryPoint: (objectId, pointId) =>
commitMutation((state) => ({
...state,
@@ -1795,6 +1859,11 @@ export const useDirectorStore = create<DirectorStore>((set, get) => {
})),
selectTrajectoryPoint: (objectId, pointId) =>
commitUiMutation((state) => {
// 取消选中:只清掉「轨迹点」这一层,不动角色本身的选择,避免点一下空白就把对象也取消选中
if (!objectId || !pointId) {
return state.selectedTrajectoryPoint ? { ...state, selectedTrajectoryPoint: null } : state;
}
const target = state.project.objects.find((item) => item.id === objectId);
if (!target?.trajectoryPoints?.some((point) => point.id === pointId)) return state;