feat(editor): DnD сцен в список, порядок списка и правки импорта

Можно перетащить изображения/видео в колонку сцен: создаются новые сцены
с превью и названием из файла, с полноэкранным прогрессом и отчётом о
пропущенных файлах.

Добавлен ручной порядок сцен в списке (sceneListOrder) с сохранением в
проект; перетаскивание карточек отделено от импорта файлов. Во время
сортировки снова работает скролл колесом.

Исправлен залипающий disabled у «+ Новая сцена» после создания сцены.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Ivan Fontosh
2026-07-12 10:32:50 +08:00
parent b7e6ff6915
commit 35a6e979eb
18 changed files with 789 additions and 73 deletions
+34 -1
View File
@@ -8,6 +8,11 @@ import { ZipFile } from 'yazl';
import { isSceneGraphEdgeRejected } from '../../shared/graph/sceneGraphEdgeRules';
import { canSetSideStoryStart, getSideStoryComponentNodeIds } from '../../shared/graph/sceneGraphLineage';
import {
prependSceneListOrder,
reconcileSceneListOrder,
removeFromSceneListOrder,
} from '../../shared/graph/sceneListOrder';
import {
buildPartialExportProject,
computeGraphImportOffsetX,
@@ -217,6 +222,7 @@ export class ZipProjectStore {
schemaVersion: PROJECT_SCHEMA_VERSION,
},
scenes: {},
sceneListOrder: [],
assets: {},
campaignAudios: [],
currentSceneId: null,
@@ -591,7 +597,14 @@ export class ZipProjectStore {
...(patch.layout ? { layout: { ...base.layout, ...patch.layout } } : null),
};
await this.updateProject((p) => ({ ...p, scenes: { ...p.scenes, [sceneId]: next } }));
await this.updateProject((p) => {
const scenes = { ...p.scenes, [sceneId]: next };
const sceneListOrder =
existing != null
? reconcileSceneListOrder(scenes, p.sceneListOrder)
: prependSceneListOrder(reconcileSceneListOrder(p.scenes, p.sceneListOrder), sceneId);
return { ...p, scenes, sceneListOrder };
});
return next;
}
@@ -638,6 +651,10 @@ export class ZipProjectStore {
return {
...withGraph,
scenes: nextScenes,
sceneListOrder: removeFromSceneListOrder(
reconcileSceneListOrder(withGraph.scenes, p.sceneListOrder),
sceneId,
),
currentSceneId,
};
});
@@ -646,6 +663,18 @@ export class ZipProjectStore {
return latest;
}
async setSceneListOrder(sceneListOrder: SceneId[]): Promise<Project> {
const open = this.openProject;
if (!open) throw new Error('No open project');
await this.updateProject((p) => ({
...p,
sceneListOrder: reconcileSceneListOrder(p.scenes, sceneListOrder),
}));
const latest = this.getOpenProject();
if (!latest) throw new Error('No open project');
return latest;
}
async updateSceneGraphNodePosition(nodeId: GraphNodeId, x: number, y: number): Promise<Project> {
const open = this.openProject;
if (!open) throw new Error('No open project');
@@ -1711,6 +1740,10 @@ function normalizeProject(p: Project): Project {
sceneGraphNodes,
sceneGraphEdges,
currentGraphNodeId,
sceneListOrder: reconcileSceneListOrder(
scenes,
(p as { sceneListOrder?: SceneId[] }).sceneListOrder,
),
};
}