feat(时间轴): 动作片段支持拖拽调整起止时间

This commit is contained in:
2026-09-18 10:55:48 +08:00
parent efe44a35a6
commit 9d3a5b9bea
14 changed files with 366 additions and 18 deletions

View File

@@ -60,6 +60,9 @@ import {
getTrajectoryPointTimes,
getTrajectoryStartTime,
shiftTrajectoryClip,
getMotionStartTime,
getMotionEndTime,
MOTION_CLIP_MIN_DURATION,
} from "../schema/trajectoryMotion";
export type TransformMode = "translate" | "rotate" | "scale";
@@ -170,6 +173,12 @@ export interface DirectorActions {
setTrajectoryClipTrimStart: (objectId: string, startTime: number) => void;
/** 设置人物轨迹片段的结束时间(停留时长):角色走完最后一点后原地停留到这一刻,拖条右端把手时写入 */
setTrajectoryClipEnd: (objectId: string, endTime: number) => void;
/** 整体平移动作片段(拖动作条本体):片段起点与结束时间同步平移,保持动作时长不变 */
setMotionClipStart: (objectId: string, startTime: number) => void;
/** 修剪动作片段起点(拖动作条左端把手):只挪动作开始时间,结束时间不动 */
setMotionClipTrimStart: (objectId: string, startTime: number) => void;
/** 设置动作片段的结束时间(拖动作条右端把手):动作播放到这一刻停止,起点不动 */
setMotionClipEnd: (objectId: string, endTime: number) => void;
removeTrajectoryPoint: (objectId: string, pointId: string) => void;
/** 选中某个轨迹点;传 null 表示取消选中(点到轨道空白处 / 面板其它地方时清掉轨迹关键帧的选中态) */
selectTrajectoryPoint: (objectId: string | null, pointId: string | null) => void;
@@ -1903,6 +1912,62 @@ export const useDirectorStore = create<DirectorStore>((set, get) => {
}),
},
})),
setMotionClipStart: (objectId, startTime) =>
commitMutation((state) => ({
...state,
project: {
...state.project,
objects: updateObjectById(state.project.objects, objectId, (item) => {
const duration = state.project.timeline.duration;
const currentStart = getMotionStartTime(item, duration);
const currentEnd = getMotionEndTime(item, duration, currentStart);
// 片段刚性平移:动作时长(起点→终点)保持不变,起点夹在时间轴内,终点跟着走同样的量。
const minSpan = MOTION_CLIP_MIN_DURATION;
const span = Math.max(currentEnd - currentStart, minSpan);
const upper = Math.max(duration - span, 0);
const nextStart = Math.min(Math.max(startTime, 0), upper);
const delta = nextStart - currentStart;
return {
...item,
motionStartTime: nextStart,
motionEndTime: currentEnd + delta,
};
}),
},
})),
setMotionClipTrimStart: (objectId, startTime) =>
commitMutation((state) => ({
...state,
project: {
...state.project,
objects: updateObjectById(state.project.objects, objectId, (item) => {
const duration = state.project.timeline.duration;
const currentEnd = getMotionEndTime(item, duration, getMotionStartTime(item, duration));
// 起点最多挪到「结束时间再往前一个最短时长」处,不能越过终点,否则片段就空了。
const upper = Math.max(currentEnd - MOTION_CLIP_MIN_DURATION, 0);
const nextStart = Math.min(Math.max(startTime, 0), upper);
return { ...item, motionStartTime: nextStart };
}),
},
})),
setMotionClipEnd: (objectId, endTime) =>
commitMutation((state) => ({
...state,
project: {
...state.project,
objects: updateObjectById(state.project.objects, objectId, (item) => {
const duration = state.project.timeline.duration;
const currentStart = getMotionStartTime(item, duration);
// 结束时间夹在「起点 + 最短时长 ~ 时间轴末尾」之间,保证片段始终合法、不为空。
const lower = currentStart + MOTION_CLIP_MIN_DURATION;
const nextEnd = Math.min(Math.max(endTime, lower), duration);
return { ...item, motionEndTime: nextEnd };
}),
},
})),
removeTrajectoryPoint: (objectId, pointId) =>
commitMutation((state) => ({
...state,