feat: 增加轨迹
This commit is contained in:
@@ -10,6 +10,8 @@ import type {
|
||||
DirectorCameraShot,
|
||||
DirectorObject,
|
||||
DirectorProject,
|
||||
DirectorTrajectoryPoint,
|
||||
DirectorTrajectoryPointRef,
|
||||
DirectorTransform,
|
||||
GeometryPrimitiveType,
|
||||
MotionKind,
|
||||
@@ -77,11 +79,17 @@ export interface DirectorUiState {
|
||||
selectedObjectId: string | null;
|
||||
selectedObjectIds: string[];
|
||||
selectedCrowdId: string | null;
|
||||
/** 当前选中的轨迹点(决定右侧显示「轨迹点」卡片) */
|
||||
selectedTrajectoryPoint: DirectorTrajectoryPointRef | null;
|
||||
/** 轨迹打点模式的目标角色 id;非空时画布不可选取其它对象,双击地面即落一个轨迹点 */
|
||||
trajectoryPlacementObjectId: string | null;
|
||||
directorInspectorMode: "auto" | "scene";
|
||||
transformMode: TransformMode;
|
||||
viewportAspectRatio: ViewportAspectRatio;
|
||||
viewportRuleOfThirdsEnabled: boolean;
|
||||
viewportPanelsCollapsed: boolean;
|
||||
/** 悬浮时间轴(录制面板)是否显示,由视口快捷工具条的「录制」按钮切换 */
|
||||
timelineVisible: boolean;
|
||||
}
|
||||
|
||||
export interface DirectorState extends DirectorUiState {
|
||||
@@ -109,10 +117,25 @@ export interface DirectorActions {
|
||||
setViewportRuleOfThirdsEnabled: (enabled: boolean) => void;
|
||||
toggleViewportPanelsCollapsed: () => void;
|
||||
setViewportPanelsCollapsed: (collapsed: boolean) => void;
|
||||
toggleTimelineVisible: () => void;
|
||||
setTimelineVisible: (visible: boolean) => void;
|
||||
selectObject: (id: string | null) => void;
|
||||
selectCrowd: (crowdId: string | null) => void;
|
||||
toggleObjectSelection: (id: string) => void;
|
||||
openSceneInspector: () => void;
|
||||
// 角色运动轨迹(平面轨迹点)
|
||||
/** 进入轨迹打点模式:只接受角色,进入后画布不可选取其它对象,双击地面即落一个点 */
|
||||
startTrajectoryPlacement: (objectId: string) => void;
|
||||
stopTrajectoryPlacement: () => void;
|
||||
/** 在场景坐标系里落一个轨迹点(平面:y 固定 0),返回新点 id;目标不是角色时返回 null */
|
||||
addTrajectoryPoint: (objectId: string, position: [number, number, number]) => string | null;
|
||||
updateTrajectoryPoint: (
|
||||
objectId: string,
|
||||
pointId: string,
|
||||
patch: Partial<Pick<DirectorTrajectoryPoint, "position" | "rotation">>
|
||||
) => void;
|
||||
removeTrajectoryPoint: (objectId: string, pointId: string) => void;
|
||||
selectTrajectoryPoint: (objectId: string, pointId: string) => void;
|
||||
updateScene: (patch: Partial<SceneSettings>) => void;
|
||||
removePanoramaAsset: () => void;
|
||||
removeImportedAsset: (assetId: string) => void;
|
||||
@@ -132,6 +155,10 @@ export interface DirectorActions {
|
||||
addGeometryPrimitive: (geometryType: GeometryPrimitiveType) => void;
|
||||
addCameraShot: (snapshot?: CameraShotSnapshot) => string;
|
||||
deleteSelectedObject: () => void;
|
||||
/** 按 id 删除单个对象(场景树每行的删除按钮) */
|
||||
deleteObject: (id: string) => void;
|
||||
/** 按 crowdId 删除整组群众(场景树群众行的删除按钮) */
|
||||
deleteCrowd: (crowdId: string) => void;
|
||||
toggleObjectVisible: (id: string) => void;
|
||||
toggleObjectLocked: (id: string) => void;
|
||||
applyPosePreset: (id: string, presetId: PosePresetId) => void;
|
||||
@@ -222,11 +249,14 @@ const DEFAULT_UI_STATE: DirectorUiState = {
|
||||
selectedObjectId: null,
|
||||
selectedObjectIds: [],
|
||||
selectedCrowdId: null,
|
||||
selectedTrajectoryPoint: null,
|
||||
trajectoryPlacementObjectId: null,
|
||||
directorInspectorMode: "auto",
|
||||
transformMode: "translate",
|
||||
viewportAspectRatio: "auto",
|
||||
viewportRuleOfThirdsEnabled: false,
|
||||
viewportPanelsCollapsed: false,
|
||||
timelineVisible: true,
|
||||
};
|
||||
|
||||
function normalizeDirectorScenePersistenceScopeId(scopeId: string | null | undefined) {
|
||||
@@ -276,6 +306,11 @@ function formatSceneItemName(prefix: "角色" | "机位", index: number) {
|
||||
return `${prefix}${String(index).padStart(2, "0")}`;
|
||||
}
|
||||
|
||||
function createTrajectoryPointId(): string {
|
||||
return `tp_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 8)}`;
|
||||
}
|
||||
|
||||
/** 画布上单击得到的是精确浮点世界坐标,直接整段存进轨迹点,不取整也不收敛 —— 鼠标点到哪里就是哪里(含小数坐标) */
|
||||
function getNextSequentialId(existingIds: string[], prefix: string, minimumIndex = 1) {
|
||||
let maxIndex = minimumIndex - 1;
|
||||
|
||||
@@ -347,6 +382,16 @@ function persistLocalModelAsset(asset: DirectorAssetRef) {
|
||||
writePersistedLocalModelAssets([...persistedAssets, asset]);
|
||||
}
|
||||
|
||||
function readPersistedTrajectoryPointRef(value: unknown): DirectorTrajectoryPointRef | null {
|
||||
if (!value || typeof value !== "object") return null;
|
||||
|
||||
const ref = value as Partial<DirectorTrajectoryPointRef>;
|
||||
|
||||
return typeof ref.objectId === "string" && typeof ref.pointId === "string"
|
||||
? { objectId: ref.objectId, pointId: ref.pointId }
|
||||
: null;
|
||||
}
|
||||
|
||||
function removePersistedLocalModelAsset(assetId: string) {
|
||||
writePersistedLocalModelAssets(readPersistedLocalModelAssets().filter((asset) => asset.id !== assetId));
|
||||
}
|
||||
@@ -415,17 +460,40 @@ function migrateDirectorProject(project: DirectorProject): DirectorProject {
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* blob: URL 只对创建它的那次页面会话有效,刷新后浏览器已回收。持久化读回来时若全景图还是
|
||||
* 这种失效地址,说明是上一版本(或上一次会话)留下的,加载必然失败 —— 直接剔除并清空
|
||||
* panoramaAssetId,回落到纯色天空,而不是弹「全景图加载失败」。
|
||||
* 新导入的全景图已统一存成 data URL(见 panoramaImport.ts),不会再走到这里。
|
||||
*/
|
||||
function dropExpiredPanoramaAsset(project: DirectorProject): DirectorProject {
|
||||
const panoramaAssetId = project.panoramaAssetId;
|
||||
if (!panoramaAssetId) return project;
|
||||
|
||||
const panoramaAsset = project.assets.find((asset) => asset.id === panoramaAssetId);
|
||||
if (!panoramaAsset || !panoramaAsset.url.startsWith("blob:")) return project;
|
||||
|
||||
return {
|
||||
...project,
|
||||
assets: project.assets.filter((asset) => asset.id !== panoramaAssetId),
|
||||
panoramaAssetId: null,
|
||||
};
|
||||
}
|
||||
|
||||
function extractPersistedDirectorState(state: DirectorRuntimeState): DirectorState {
|
||||
return cloneJsonValue({
|
||||
viewMode: state.viewMode,
|
||||
selectedObjectId: state.selectedObjectId,
|
||||
selectedObjectIds: state.selectedObjectIds,
|
||||
selectedCrowdId: state.selectedCrowdId,
|
||||
selectedTrajectoryPoint: state.selectedTrajectoryPoint,
|
||||
trajectoryPlacementObjectId: state.trajectoryPlacementObjectId,
|
||||
directorInspectorMode: state.directorInspectorMode,
|
||||
transformMode: state.transformMode,
|
||||
viewportAspectRatio: state.viewportAspectRatio,
|
||||
viewportRuleOfThirdsEnabled: state.viewportRuleOfThirdsEnabled,
|
||||
viewportPanelsCollapsed: state.viewportPanelsCollapsed,
|
||||
timelineVisible: state.timelineVisible,
|
||||
project: state.project,
|
||||
});
|
||||
}
|
||||
@@ -444,7 +512,9 @@ function writePersistedDirectorState(state: DirectorState) {
|
||||
function createStateFromPersistedProject(project: DirectorProject, options: DirectorStateOptions = {}): DirectorState {
|
||||
return {
|
||||
...DEFAULT_UI_STATE,
|
||||
project: withPersistedLocalAssets(migrateDirectorProject(cloneJsonValue(project)), options.includePersistedLocalAssets),
|
||||
project: dropExpiredPanoramaAsset(
|
||||
withPersistedLocalAssets(migrateDirectorProject(cloneJsonValue(project)), options.includePersistedLocalAssets)
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -474,12 +544,16 @@ function readPersistedDirectorState(options: DirectorStateOptions = {}): Directo
|
||||
? state.selectedObjectIds.filter((item): item is string => typeof item === "string")
|
||||
: [],
|
||||
selectedCrowdId: typeof state.selectedCrowdId === "string" ? state.selectedCrowdId : null,
|
||||
selectedTrajectoryPoint: readPersistedTrajectoryPointRef(state.selectedTrajectoryPoint),
|
||||
trajectoryPlacementObjectId:
|
||||
typeof state.trajectoryPlacementObjectId === "string" ? state.trajectoryPlacementObjectId : null,
|
||||
directorInspectorMode: state.directorInspectorMode === "scene" ? "scene" : "auto",
|
||||
transformMode:
|
||||
state.transformMode === "rotate" || state.transformMode === "scale" ? state.transformMode : "translate",
|
||||
viewportAspectRatio: state.viewportAspectRatio ?? "auto",
|
||||
viewportRuleOfThirdsEnabled: Boolean(state.viewportRuleOfThirdsEnabled),
|
||||
viewportPanelsCollapsed: Boolean(state.viewportPanelsCollapsed),
|
||||
timelineVisible: state.timelineVisible === false ? false : true,
|
||||
project: withPersistedLocalAssets(
|
||||
migrateDirectorProject(cloneJsonValue(state.project)),
|
||||
options.includePersistedLocalAssets
|
||||
@@ -938,6 +1012,107 @@ function getOrderedSelectedObjectIds(state: DirectorState) {
|
||||
return state.selectedObjectId ? [state.selectedObjectId] : [];
|
||||
}
|
||||
|
||||
/**
|
||||
* 按 id 删除一组对象,并做级联清理:
|
||||
* - 摄像机对象连带删掉它背后的机位(cameras)
|
||||
* - 原本注视被删对象的机位退回「手动坐标」注视
|
||||
* - 只被这些对象引用的模型素材一并回收(本地导入的素材保留)
|
||||
* - 时间轴上对应的物体轨道 / 相机轨道一起删掉
|
||||
* 没被删掉的选中项继续保持选中(删别人不会丢掉当前选中)。
|
||||
*/
|
||||
function deleteObjectsByIds(state: DirectorRuntimeState, objectIds: string[]): DirectorRuntimeState {
|
||||
if (!objectIds.length) return state;
|
||||
|
||||
const objectIdSet = new Set(objectIds);
|
||||
const targetObjects = state.project.objects.filter((item) => objectIdSet.has(item.id));
|
||||
|
||||
if (!targetObjects.length) {
|
||||
return {
|
||||
...state,
|
||||
selectedObjectId: null,
|
||||
selectedObjectIds: [],
|
||||
};
|
||||
}
|
||||
|
||||
const linkedCameraIds = new Set(
|
||||
targetObjects.filter((item) => item.kind === "camera" && item.linkedCameraId).map((item) => item.linkedCameraId)
|
||||
);
|
||||
const nextCameras = linkedCameraIds.size
|
||||
? state.project.cameras.filter((camera) => !linkedCameraIds.has(camera.id))
|
||||
: state.project.cameras;
|
||||
const nextFocusedCameras = nextCameras.map((camera) =>
|
||||
camera.targetObjectId && objectIdSet.has(camera.targetObjectId)
|
||||
? {
|
||||
...camera,
|
||||
targetMode: "manual" as const,
|
||||
targetObjectId: null,
|
||||
}
|
||||
: camera
|
||||
);
|
||||
const nextActiveCameraId =
|
||||
state.project.activeCameraId && linkedCameraIds.has(state.project.activeCameraId)
|
||||
? nextFocusedCameras[0]?.id ?? null
|
||||
: state.project.activeCameraId;
|
||||
const nextObjects = state.project.objects.filter((item) => !objectIdSet.has(item.id));
|
||||
const assetsById = new Map(state.project.assets.map((item) => [item.id, item]));
|
||||
const remainingAssetRefIds = new Set(
|
||||
nextObjects.map((item) => item.assetRefId).filter((assetRefId): assetRefId is string => Boolean(assetRefId))
|
||||
);
|
||||
const removedAssetRefIds = new Set(
|
||||
targetObjects
|
||||
.map((item) => item.assetRefId)
|
||||
.filter((assetRefId): assetRefId is string => {
|
||||
if (typeof assetRefId !== "string" || remainingAssetRefIds.has(assetRefId)) return false;
|
||||
return assetsById.get(assetRefId)?.assetSource !== "local";
|
||||
})
|
||||
);
|
||||
|
||||
// 同步清理被删对象的关键帧轨道:物体轨道按 objectId、相机轨道按 linkedCameraId
|
||||
const nextTracks = state.project.timeline.tracks.filter((track) => !objectIdSet.has(track.objectId));
|
||||
const nextCameraTracks = state.project.timeline.cameraTracks.filter(
|
||||
(track) => !linkedCameraIds.has(track.cameraId)
|
||||
);
|
||||
const timelineUnchanged =
|
||||
nextTracks.length === state.project.timeline.tracks.length &&
|
||||
nextCameraTracks.length === state.project.timeline.cameraTracks.length;
|
||||
const remainingSelectedObjectIds = getOrderedSelectedObjectIds(state).filter((id) => !objectIdSet.has(id));
|
||||
const remainingSelectedCrowdId =
|
||||
state.selectedCrowdId && nextObjects.some((item) => item.crowdId === state.selectedCrowdId)
|
||||
? state.selectedCrowdId
|
||||
: null;
|
||||
|
||||
return {
|
||||
...state,
|
||||
selectedObjectId: remainingSelectedObjectIds[remainingSelectedObjectIds.length - 1] ?? null,
|
||||
selectedObjectIds: remainingSelectedObjectIds,
|
||||
selectedCrowdId: remainingSelectedCrowdId,
|
||||
// 所属角色被删了,它的轨迹点选中态 / 打点模式也要一并清掉
|
||||
selectedTrajectoryPoint:
|
||||
state.selectedTrajectoryPoint && objectIdSet.has(state.selectedTrajectoryPoint.objectId)
|
||||
? null
|
||||
: state.selectedTrajectoryPoint,
|
||||
trajectoryPlacementObjectId:
|
||||
state.trajectoryPlacementObjectId && objectIdSet.has(state.trajectoryPlacementObjectId)
|
||||
? null
|
||||
: state.trajectoryPlacementObjectId,
|
||||
directorInspectorMode: "auto",
|
||||
project: {
|
||||
...state.project,
|
||||
assets: state.project.assets.filter((item) => !removedAssetRefIds.has(item.id)),
|
||||
objects: nextObjects,
|
||||
cameras: nextFocusedCameras,
|
||||
activeCameraId: nextActiveCameraId,
|
||||
timeline: timelineUnchanged
|
||||
? state.project.timeline
|
||||
: {
|
||||
...state.project.timeline,
|
||||
tracks: nextTracks,
|
||||
cameraTracks: nextCameraTracks,
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function createObjectIdForDuplicate(existingObjects: DirectorObject[], source: DirectorObject) {
|
||||
if (source.kind === "camera") {
|
||||
return getNextSequentialId(
|
||||
@@ -1259,6 +1434,16 @@ export const useDirectorStore = create<DirectorStore>((set, get) => {
|
||||
...state,
|
||||
viewportPanelsCollapsed: collapsed,
|
||||
})),
|
||||
toggleTimelineVisible: () =>
|
||||
commitUiMutation((state) => ({
|
||||
...state,
|
||||
timelineVisible: !state.timelineVisible,
|
||||
})),
|
||||
setTimelineVisible: (visible) =>
|
||||
commitUiMutation((state) => ({
|
||||
...state,
|
||||
timelineVisible: visible,
|
||||
})),
|
||||
setViewMode: (mode) =>
|
||||
commitUiMutation((state) => ({
|
||||
...state,
|
||||
@@ -1280,6 +1465,8 @@ export const useDirectorStore = create<DirectorStore>((set, get) => {
|
||||
selectedObjectId: id,
|
||||
selectedObjectIds: id ? [id] : [],
|
||||
selectedCrowdId: null,
|
||||
// 改选其它对象就退出「轨迹点」卡片
|
||||
selectedTrajectoryPoint: null,
|
||||
directorInspectorMode: "auto",
|
||||
project: {
|
||||
...state.project,
|
||||
@@ -1309,6 +1496,7 @@ export const useDirectorStore = create<DirectorStore>((set, get) => {
|
||||
selectedCrowdId: crowdId,
|
||||
selectedObjectId: crowdMemberIds[crowdMemberIds.length - 1] ?? null,
|
||||
selectedObjectIds: crowdMemberIds,
|
||||
selectedTrajectoryPoint: null,
|
||||
directorInspectorMode: "auto",
|
||||
};
|
||||
}),
|
||||
@@ -1328,6 +1516,7 @@ export const useDirectorStore = create<DirectorStore>((set, get) => {
|
||||
selectedObjectId: nextSelectedObjectId,
|
||||
selectedObjectIds: nextSelectedObjectIds,
|
||||
selectedCrowdId: null,
|
||||
selectedTrajectoryPoint: null,
|
||||
directorInspectorMode: "auto",
|
||||
project: {
|
||||
...state.project,
|
||||
@@ -1345,7 +1534,108 @@ export const useDirectorStore = create<DirectorStore>((set, get) => {
|
||||
selectedObjectId: null,
|
||||
selectedObjectIds: [],
|
||||
selectedCrowdId: null,
|
||||
selectedTrajectoryPoint: null,
|
||||
})),
|
||||
startTrajectoryPlacement: (objectId) =>
|
||||
commitUiMutation((state) => {
|
||||
const target = state.project.objects.find((item) => item.id === objectId);
|
||||
if (!target || target.kind !== "character") return state;
|
||||
|
||||
return {
|
||||
...state,
|
||||
selectedObjectId: objectId,
|
||||
selectedObjectIds: [objectId],
|
||||
selectedCrowdId: null,
|
||||
selectedTrajectoryPoint: null,
|
||||
directorInspectorMode: "auto",
|
||||
trajectoryPlacementObjectId: objectId,
|
||||
};
|
||||
}),
|
||||
stopTrajectoryPlacement: () =>
|
||||
commitUiMutation((state) => ({ ...state, trajectoryPlacementObjectId: null })),
|
||||
addTrajectoryPoint: (objectId, position) => {
|
||||
// 只有角色能挂运动轨迹,先在外面判定,目标不合法时直接返回 null
|
||||
const target = get().project.objects.find((item) => item.id === objectId);
|
||||
if (!target || target.kind !== "character") return null;
|
||||
|
||||
const pointId = createTrajectoryPointId();
|
||||
|
||||
commitMutation((state) => {
|
||||
const target = state.project.objects.find((item) => item.id === objectId);
|
||||
if (!target || target.kind !== "character") return state;
|
||||
|
||||
const nextPoint: DirectorTrajectoryPoint = {
|
||||
id: pointId,
|
||||
// 只取 x / z:新建的轨迹点永远落在地面(y = 0),所以轨迹默认是平面的。
|
||||
// 坐标不取整:鼠标在地面点到的精确浮点位置就是落点(用户要求非整数坐标也能放)。
|
||||
position: [position[0], 0, position[2]],
|
||||
rotation: [0, 0, 0],
|
||||
};
|
||||
|
||||
return {
|
||||
...state,
|
||||
selectedTrajectoryPoint: { objectId, pointId },
|
||||
project: {
|
||||
...state.project,
|
||||
objects: updateObjectById(state.project.objects, objectId, (item) => ({
|
||||
...item,
|
||||
trajectoryPoints: [...(item.trajectoryPoints ?? []), nextPoint],
|
||||
})),
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
return pointId;
|
||||
},
|
||||
updateTrajectoryPoint: (objectId, pointId, patch) =>
|
||||
commitMutation((state) => ({
|
||||
...state,
|
||||
project: {
|
||||
...state.project,
|
||||
objects: updateObjectById(state.project.objects, objectId, (item) => ({
|
||||
...item,
|
||||
trajectoryPoints: (item.trajectoryPoints ?? []).map((point) =>
|
||||
point.id === pointId
|
||||
? {
|
||||
...point,
|
||||
// 直接采用传入的精确值(含小数),不再收敛
|
||||
position: patch.position ? patch.position : point.position,
|
||||
rotation: patch.rotation ? patch.rotation : point.rotation,
|
||||
}
|
||||
: point
|
||||
),
|
||||
})),
|
||||
},
|
||||
})),
|
||||
removeTrajectoryPoint: (objectId, pointId) =>
|
||||
commitMutation((state) => ({
|
||||
...state,
|
||||
selectedTrajectoryPoint:
|
||||
state.selectedTrajectoryPoint?.objectId === objectId && state.selectedTrajectoryPoint.pointId === pointId
|
||||
? null
|
||||
: state.selectedTrajectoryPoint,
|
||||
project: {
|
||||
...state.project,
|
||||
objects: updateObjectById(state.project.objects, objectId, (item) => ({
|
||||
...item,
|
||||
trajectoryPoints: (item.trajectoryPoints ?? []).filter((point) => point.id !== pointId),
|
||||
})),
|
||||
},
|
||||
})),
|
||||
selectTrajectoryPoint: (objectId, pointId) =>
|
||||
commitUiMutation((state) => {
|
||||
const target = state.project.objects.find((item) => item.id === objectId);
|
||||
if (!target?.trajectoryPoints?.some((point) => point.id === pointId)) return state;
|
||||
|
||||
return {
|
||||
...state,
|
||||
selectedObjectId: objectId,
|
||||
selectedObjectIds: [objectId],
|
||||
selectedCrowdId: null,
|
||||
directorInspectorMode: "auto",
|
||||
selectedTrajectoryPoint: { objectId, pointId },
|
||||
};
|
||||
}),
|
||||
updateScene: (patch) =>
|
||||
commitMutation((state) => ({
|
||||
...state,
|
||||
@@ -1846,90 +2136,12 @@ export const useDirectorStore = create<DirectorStore>((set, get) => {
|
||||
return nextCameraId;
|
||||
},
|
||||
deleteSelectedObject: () =>
|
||||
commitMutation((state) => {
|
||||
const selectedObjectIds = getOrderedSelectedObjectIds(state);
|
||||
if (!selectedObjectIds.length) return state;
|
||||
|
||||
const selectedObjects = state.project.objects.filter((item) => selectedObjectIds.includes(item.id));
|
||||
if (!selectedObjects.length) {
|
||||
return {
|
||||
...state,
|
||||
selectedObjectId: null,
|
||||
selectedObjectIds: [],
|
||||
};
|
||||
}
|
||||
|
||||
const linkedCameraIds = new Set(
|
||||
selectedObjects
|
||||
.filter((item) => item.kind === "camera" && item.linkedCameraId)
|
||||
.map((item) => item.linkedCameraId)
|
||||
);
|
||||
const nextCameras = linkedCameraIds.size
|
||||
? state.project.cameras.filter((camera) => !linkedCameraIds.has(camera.id))
|
||||
: state.project.cameras;
|
||||
const selectedObjectIdSet = new Set(selectedObjectIds);
|
||||
const nextFocusedCameras = nextCameras.map((camera) =>
|
||||
camera.targetObjectId && selectedObjectIdSet.has(camera.targetObjectId)
|
||||
? {
|
||||
...camera,
|
||||
targetMode: "manual" as const,
|
||||
targetObjectId: null,
|
||||
}
|
||||
: camera
|
||||
);
|
||||
const nextActiveCameraId =
|
||||
state.project.activeCameraId && linkedCameraIds.has(state.project.activeCameraId)
|
||||
? nextFocusedCameras[0]?.id ?? null
|
||||
: state.project.activeCameraId;
|
||||
const nextObjects = state.project.objects.filter((item) => !selectedObjectIds.includes(item.id));
|
||||
const assetsById = new Map(state.project.assets.map((item) => [item.id, item]));
|
||||
const remainingAssetRefIds = new Set(
|
||||
nextObjects.map((item) => item.assetRefId).filter((assetRefId): assetRefId is string => Boolean(assetRefId))
|
||||
);
|
||||
const removedAssetRefIds = new Set(
|
||||
selectedObjects
|
||||
.map((item) => item.assetRefId)
|
||||
.filter(
|
||||
(assetRefId): assetRefId is string => {
|
||||
if (typeof assetRefId !== "string" || remainingAssetRefIds.has(assetRefId)) return false;
|
||||
return assetsById.get(assetRefId)?.assetSource !== "local";
|
||||
}
|
||||
)
|
||||
);
|
||||
|
||||
// 同步清理被删对象的关键帧轨道:物体轨道按 objectId、相机轨道按 linkedCameraId
|
||||
const nextTracks = state.project.timeline.tracks.filter(
|
||||
(track) => !selectedObjectIdSet.has(track.objectId)
|
||||
);
|
||||
const nextCameraTracks = state.project.timeline.cameraTracks.filter(
|
||||
(track) => !linkedCameraIds.has(track.cameraId)
|
||||
);
|
||||
const timelineUnchanged =
|
||||
nextTracks.length === state.project.timeline.tracks.length &&
|
||||
nextCameraTracks.length === state.project.timeline.cameraTracks.length;
|
||||
|
||||
return {
|
||||
...state,
|
||||
selectedObjectId: null,
|
||||
selectedObjectIds: [],
|
||||
selectedCrowdId: null,
|
||||
directorInspectorMode: "auto",
|
||||
project: {
|
||||
...state.project,
|
||||
assets: state.project.assets.filter((item) => !removedAssetRefIds.has(item.id)),
|
||||
objects: nextObjects,
|
||||
cameras: nextFocusedCameras,
|
||||
activeCameraId: nextActiveCameraId,
|
||||
timeline: timelineUnchanged
|
||||
? state.project.timeline
|
||||
: {
|
||||
...state.project.timeline,
|
||||
tracks: nextTracks,
|
||||
cameraTracks: nextCameraTracks,
|
||||
},
|
||||
},
|
||||
};
|
||||
}),
|
||||
commitMutation((state) => deleteObjectsByIds(state, getOrderedSelectedObjectIds(state))),
|
||||
/** 删除单个对象(角色 / 几何体 / 模型 / 摄像机对象),供场景树每行的删除按钮使用 */
|
||||
deleteObject: (id) => commitMutation((state) => deleteObjectsByIds(state, [id])),
|
||||
/** 删除整个群众组 */
|
||||
deleteCrowd: (crowdId) =>
|
||||
commitMutation((state) => deleteObjectsByIds(state, getCrowdMemberIds(state.project.objects, crowdId))),
|
||||
toggleObjectVisible: (id) =>
|
||||
commitMutation((state) => ({
|
||||
...state,
|
||||
|
||||
Reference in New Issue
Block a user