feat(timeline): 支持角色轨迹关键帧与轨道拖拽排序
This commit is contained in:
@@ -90,6 +90,11 @@ export interface DirectorUiState {
|
||||
viewportPanelsCollapsed: boolean;
|
||||
/** 悬浮时间轴(录制面板)是否显示,由视口快捷工具条的「录制」按钮切换 */
|
||||
timelineVisible: boolean;
|
||||
/** 用户自定义的时间轴轨道顺序(轨道 key 列表)。
|
||||
* 只记录「被调整过」的轨道;未出现在列表里的新轨道按默认顺序排在后面。 */
|
||||
timelineTrackOrder: string[];
|
||||
/** 已折叠的时间轴轨道 key(角色轨道折叠后只留抬头行) */
|
||||
timelineCollapsedTracks: string[];
|
||||
}
|
||||
|
||||
export interface DirectorState extends DirectorUiState {
|
||||
@@ -119,6 +124,10 @@ export interface DirectorActions {
|
||||
setViewportPanelsCollapsed: (collapsed: boolean) => void;
|
||||
toggleTimelineVisible: () => void;
|
||||
setTimelineVisible: (visible: boolean) => void;
|
||||
/** 覆盖时间轴轨道顺序(传入解析后的完整顺序) */
|
||||
setTimelineTrackOrder: (order: string[]) => void;
|
||||
/** 折叠 / 展开某个时间轴轨道 */
|
||||
toggleTimelineTrackCollapsed: (trackKey: string) => void;
|
||||
selectObject: (id: string | null) => void;
|
||||
selectCrowd: (crowdId: string | null) => void;
|
||||
toggleObjectSelection: (id: string) => void;
|
||||
@@ -257,6 +266,8 @@ const DEFAULT_UI_STATE: DirectorUiState = {
|
||||
viewportRuleOfThirdsEnabled: false,
|
||||
viewportPanelsCollapsed: false,
|
||||
timelineVisible: true,
|
||||
timelineTrackOrder: [],
|
||||
timelineCollapsedTracks: [],
|
||||
};
|
||||
|
||||
function normalizeDirectorScenePersistenceScopeId(scopeId: string | null | undefined) {
|
||||
@@ -392,6 +403,11 @@ function readPersistedTrajectoryPointRef(value: unknown): DirectorTrajectoryPoin
|
||||
: null;
|
||||
}
|
||||
|
||||
/** 读取持久化里的字符串数组(时间轴轨道顺序 / 折叠状态),非法值一律回落到空数组 */
|
||||
function readPersistedStringArray(value: unknown): string[] {
|
||||
return Array.isArray(value) ? value.filter((item): item is string => typeof item === "string") : [];
|
||||
}
|
||||
|
||||
function removePersistedLocalModelAsset(assetId: string) {
|
||||
writePersistedLocalModelAssets(readPersistedLocalModelAssets().filter((asset) => asset.id !== assetId));
|
||||
}
|
||||
@@ -494,6 +510,8 @@ function extractPersistedDirectorState(state: DirectorRuntimeState): DirectorSta
|
||||
viewportRuleOfThirdsEnabled: state.viewportRuleOfThirdsEnabled,
|
||||
viewportPanelsCollapsed: state.viewportPanelsCollapsed,
|
||||
timelineVisible: state.timelineVisible,
|
||||
timelineTrackOrder: state.timelineTrackOrder,
|
||||
timelineCollapsedTracks: state.timelineCollapsedTracks,
|
||||
project: state.project,
|
||||
});
|
||||
}
|
||||
@@ -554,6 +572,8 @@ function readPersistedDirectorState(options: DirectorStateOptions = {}): Directo
|
||||
viewportRuleOfThirdsEnabled: Boolean(state.viewportRuleOfThirdsEnabled),
|
||||
viewportPanelsCollapsed: Boolean(state.viewportPanelsCollapsed),
|
||||
timelineVisible: state.timelineVisible === false ? false : true,
|
||||
timelineTrackOrder: readPersistedStringArray(state.timelineTrackOrder),
|
||||
timelineCollapsedTracks: readPersistedStringArray(state.timelineCollapsedTracks),
|
||||
project: withPersistedLocalAssets(
|
||||
migrateDirectorProject(cloneJsonValue(state.project)),
|
||||
options.includePersistedLocalAssets
|
||||
@@ -1317,6 +1337,17 @@ export const useDirectorStore = create<DirectorStore>((set, get) => {
|
||||
createInitialDirectorState({ includePersistedLocalAssets: true, includePersistedScene: true })
|
||||
);
|
||||
|
||||
/** 拖拽(撤销批次)期间的持久化防抖间隔:连续拖拽时完全不序列化,停手 150ms 后落盘一次 */
|
||||
const UNDO_BATCH_PERSIST_DEBOUNCE_MS = 150;
|
||||
let undoBatchPersistTimer: ReturnType<typeof setTimeout> | null = null;
|
||||
|
||||
function cancelPendingUndoBatchPersist() {
|
||||
if (undoBatchPersistTimer === null) return;
|
||||
|
||||
clearTimeout(undoBatchPersistTimer);
|
||||
undoBatchPersistTimer = null;
|
||||
}
|
||||
|
||||
function commitMutation(
|
||||
updater: (state: DirectorRuntimeState) => DirectorRuntimeState,
|
||||
options: { trackUndo?: boolean; persist?: boolean } = {}
|
||||
@@ -1325,10 +1356,14 @@ export const useDirectorStore = create<DirectorStore>((set, get) => {
|
||||
|
||||
set((state) => {
|
||||
const currentState = state as DirectorRuntimeState;
|
||||
const previousSnapshot = createUndoStackEntry(currentState);
|
||||
// 撤销批次内(gizmo / 轨迹点拖拽中)不会再有新的撤销快照 —— 批次开始时的快照已经够回退。
|
||||
// 因此这里跳过「整工程深克隆 + 全量 JSON 比较」:拖拽时每次指针移动都做一遍的话,
|
||||
// 工程里只要带了 base64 全景图就会直接卡死(实测单次写入 39ms @2MB 资源)。
|
||||
const isInsideUndoBatch = trackUndo && currentState.undoBatchDepth > 0;
|
||||
const previousSnapshot = isInsideUndoBatch ? null : createUndoStackEntry(currentState);
|
||||
const nextState = updater(currentState);
|
||||
const nextSnapshot = extractPersistedDirectorState(nextState);
|
||||
const didChange = !isSameDirectorState(previousSnapshot, nextSnapshot);
|
||||
const didChange =
|
||||
previousSnapshot === null || !isSameDirectorState(previousSnapshot, extractPersistedDirectorState(nextState));
|
||||
|
||||
if (!didChange) {
|
||||
return {
|
||||
@@ -1340,27 +1375,37 @@ export const useDirectorStore = create<DirectorStore>((set, get) => {
|
||||
};
|
||||
}
|
||||
|
||||
const shouldCaptureUndoBatchSnapshot =
|
||||
trackUndo && currentState.undoBatchDepth > 0 && currentState.undoBatchSnapshot === null;
|
||||
const shouldCaptureUndoBatchSnapshot = isInsideUndoBatch && currentState.undoBatchSnapshot === null;
|
||||
const nextUndoStack =
|
||||
trackUndo && currentState.undoBatchDepth === 0
|
||||
previousSnapshot && currentState.undoBatchDepth === 0
|
||||
? trimUndoStack([...currentState.undoStack, previousSnapshot])
|
||||
: nextState.undoStack;
|
||||
const runtimeState: DirectorRuntimeState = {
|
||||
...nextState,
|
||||
undoStack: nextUndoStack,
|
||||
undoBatchSnapshot: shouldCaptureUndoBatchSnapshot ? previousSnapshot : nextState.undoBatchSnapshot,
|
||||
undoBatchHasTrackedChanges:
|
||||
trackUndo && currentState.undoBatchDepth > 0 ? true : nextState.undoBatchHasTrackedChanges,
|
||||
undoBatchSnapshot: shouldCaptureUndoBatchSnapshot
|
||||
? createUndoStackEntry(currentState)
|
||||
: nextState.undoBatchSnapshot,
|
||||
undoBatchHasTrackedChanges: isInsideUndoBatch ? true : nextState.undoBatchHasTrackedChanges,
|
||||
};
|
||||
|
||||
if (persist) {
|
||||
writePersistedDirectorState(extractPersistedDirectorState(runtimeState));
|
||||
if (isInsideUndoBatch) {
|
||||
// 拖拽中不落盘:整工程序列化 + 同步写 localStorage 会直接卡住指针跟随,
|
||||
// 改成停手 150ms 后写一次,endUndoBatch 时还会立刻补写最终结果。
|
||||
cancelPendingUndoBatchPersist();
|
||||
undoBatchPersistTimer = setTimeout(() => {
|
||||
undoBatchPersistTimer = null;
|
||||
writePersistedDirectorState(extractPersistedDirectorState(get() as DirectorRuntimeState));
|
||||
}, UNDO_BATCH_PERSIST_DEBOUNCE_MS);
|
||||
} else {
|
||||
writePersistedDirectorState(extractPersistedDirectorState(runtimeState));
|
||||
}
|
||||
}
|
||||
|
||||
return runtimeState;
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function commitUiMutation(updater: (state: DirectorRuntimeState) => DirectorRuntimeState) {
|
||||
commitMutation(updater, { trackUndo: false, persist: true });
|
||||
@@ -1381,6 +1426,8 @@ export const useDirectorStore = create<DirectorStore>((set, get) => {
|
||||
});
|
||||
},
|
||||
endUndoBatch: () => {
|
||||
const depthBefore = (get() as DirectorRuntimeState).undoBatchDepth;
|
||||
|
||||
set((state) => {
|
||||
const currentState = state as DirectorRuntimeState;
|
||||
if (currentState.undoBatchDepth === 0) return currentState;
|
||||
@@ -1409,6 +1456,12 @@ export const useDirectorStore = create<DirectorStore>((set, get) => {
|
||||
undoBatchHasTrackedChanges: false,
|
||||
};
|
||||
});
|
||||
|
||||
// 批次内的落盘是防抖的,收尾时立刻补写一次,保证刷新页面拿到的是拖拽后的最终结果
|
||||
if (depthBefore > 0) {
|
||||
cancelPendingUndoBatchPersist();
|
||||
writePersistedDirectorState(extractPersistedDirectorState(get() as DirectorRuntimeState));
|
||||
}
|
||||
},
|
||||
setTransformMode: (mode) =>
|
||||
commitUiMutation((state) => ({
|
||||
@@ -1445,6 +1498,18 @@ export const useDirectorStore = create<DirectorStore>((set, get) => {
|
||||
...state,
|
||||
timelineVisible: visible,
|
||||
})),
|
||||
setTimelineTrackOrder: (order) =>
|
||||
commitUiMutation((state) => ({
|
||||
...state,
|
||||
timelineTrackOrder: order.filter((key): key is string => typeof key === "string"),
|
||||
})),
|
||||
toggleTimelineTrackCollapsed: (trackKey) =>
|
||||
commitUiMutation((state) => ({
|
||||
...state,
|
||||
timelineCollapsedTracks: state.timelineCollapsedTracks.includes(trackKey)
|
||||
? state.timelineCollapsedTracks.filter((key) => key !== trackKey)
|
||||
: [...state.timelineCollapsedTracks, trackKey],
|
||||
})),
|
||||
setViewMode: (mode) =>
|
||||
commitUiMutation((state) => ({
|
||||
...state,
|
||||
|
||||
Reference in New Issue
Block a user