feat(tokens): app-local non-player tokens with session moves and UI polish
Add token library/placements, keep play-time moves for the session, lock presentation interactions, and fix export/import modal layout plus freeform trap label. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -15,11 +15,13 @@ import {
|
||||
} from '../../shared/graph/sceneListOrder';
|
||||
import {
|
||||
buildPartialExportProject,
|
||||
collectTokenIdsFromProject,
|
||||
computeGraphImportOffsetX,
|
||||
listExportableStorylines,
|
||||
listImportableStorylines,
|
||||
mergeStorylinesIntoProject,
|
||||
newExportBundleProjectId,
|
||||
remapProjectSceneTokenIds,
|
||||
type NpcImportResolution,
|
||||
type SceneImportResolution,
|
||||
type StorylineImportMergeReport,
|
||||
@@ -51,6 +53,7 @@ import type {
|
||||
} from '../../shared/types';
|
||||
import { PROJECT_SCHEMA_VERSION } from '../../shared/types';
|
||||
import { normalizeMaterialLegend } from '../../shared/types/materialLegend';
|
||||
import { normalizeSceneToken, type SceneToken } from '../../shared/types/appTokens';
|
||||
import { DEFAULT_SCENE_GRID, normalizeSceneGrid } from '../../shared/types/sceneGrid';
|
||||
import { normalizeSceneTrap } from '../../shared/types/sceneTraps';
|
||||
import type { AssetId, GraphNodeId, MaterialId, NpcGroupId, NpcId, NpcRelationId } from '../../shared/types/ids';
|
||||
@@ -617,12 +620,14 @@ export class ZipProjectStore {
|
||||
previewRotationDeg: 0,
|
||||
darkenScene: false,
|
||||
traps: [],
|
||||
tokens: [],
|
||||
grid: { ...DEFAULT_SCENE_GRID },
|
||||
} satisfies Scene);
|
||||
|
||||
const next: Scene = {
|
||||
...base,
|
||||
traps: base.traps ?? [],
|
||||
tokens: base.tokens ?? [],
|
||||
grid: base.grid ?? { ...DEFAULT_SCENE_GRID },
|
||||
...(patch.title !== undefined ? { title: patch.title } : null),
|
||||
...(patch.description !== undefined ? { description: patch.description } : null),
|
||||
@@ -643,6 +648,13 @@ export class ZipProjectStore {
|
||||
.filter((t): t is SceneTrap => Boolean(t)),
|
||||
}
|
||||
: null),
|
||||
...(patch.tokens !== undefined
|
||||
? {
|
||||
tokens: patch.tokens
|
||||
.map((t) => normalizeSceneToken(t))
|
||||
.filter((t): t is SceneToken => Boolean(t)),
|
||||
}
|
||||
: null),
|
||||
...(patch.grid !== undefined ? { grid: normalizeSceneGrid(patch.grid) } : null),
|
||||
...(patch.settings ? { settings: { ...base.settings, ...patch.settings } } : null),
|
||||
...(patch.media ? { media: { ...base.media, ...patch.media } } : null),
|
||||
@@ -1992,6 +2004,7 @@ export class ZipProjectStore {
|
||||
destinationPath: string,
|
||||
labels: StorylineLabels,
|
||||
onProgress?: (p: { stage: 'zip' | 'done'; percent: number; detail?: string }) => void,
|
||||
packAppTokens?: (tokenIds: string[], exportRoot: string) => Promise<void>,
|
||||
): Promise<void> {
|
||||
if (selections.length === 0) throw new Error('Не выбрана ни одна сюжетная линия');
|
||||
await this.ensureRoots();
|
||||
@@ -2016,10 +2029,14 @@ export class ZipProjectStore {
|
||||
await fs.mkdir(path.dirname(destAbs), { recursive: true });
|
||||
await fs.copyFile(srcAbs, destAbs);
|
||||
if (onProgress) {
|
||||
const pct = Math.round(((i + 1) / Math.max(1, assetIds.length)) * 80);
|
||||
const pct = Math.round(((i + 1) / Math.max(1, assetIds.length)) * 75);
|
||||
onProgress({ stage: 'zip', percent: pct, detail: 'Сборка архива…' });
|
||||
}
|
||||
}
|
||||
if (packAppTokens) {
|
||||
const tokenIds = collectTokenIdsFromProject(partial);
|
||||
await packAppTokens(tokenIds, exportCache);
|
||||
}
|
||||
const now = new Date().toISOString();
|
||||
const toWrite: Project = {
|
||||
...partial,
|
||||
@@ -2116,12 +2133,18 @@ export class ZipProjectStore {
|
||||
sceneResolutions: SceneImportResolution[],
|
||||
onProgress?: (p: { stage: 'copy' | 'done'; percent: number; detail?: string }) => void,
|
||||
npcResolutions?: NpcImportResolution[],
|
||||
importAppTokens?: (sourceCacheDir: string) => Promise<Map<string, string>>,
|
||||
): Promise<{ project: Project; report: StorylineImportMergeReport }> {
|
||||
if (!this.openProject) throw new Error('Нет открытого проекта');
|
||||
let sourceForMerge = source;
|
||||
if (importAppTokens) {
|
||||
const remap = await importAppTokens(sourceCache);
|
||||
sourceForMerge = remapProjectSceneTokenIds(source, remap);
|
||||
}
|
||||
const offsetX = computeGraphImportOffsetX(this.openProject.project);
|
||||
const { project: merged, report, assetCopies } = mergeStorylinesIntoProject(
|
||||
this.openProject.project,
|
||||
source,
|
||||
sourceForMerge,
|
||||
selections,
|
||||
sceneResolutions,
|
||||
{
|
||||
@@ -2162,6 +2185,7 @@ export class ZipProjectStore {
|
||||
sceneResolutions: SceneImportResolution[],
|
||||
onProgress?: (p: { stage: 'copy' | 'done'; percent: number; detail?: string }) => void,
|
||||
npcResolutions?: NpcImportResolution[],
|
||||
importAppTokens?: (sourceCacheDir: string) => Promise<Map<string, string>>,
|
||||
): Promise<{ project: Project; report: StorylineImportMergeReport }> {
|
||||
this.assertStorylineMergeAllowed(selections);
|
||||
const snap = await this.loadProjectSnapshot(sourceProjectId);
|
||||
@@ -2173,6 +2197,7 @@ export class ZipProjectStore {
|
||||
sceneResolutions,
|
||||
onProgress,
|
||||
npcResolutions,
|
||||
importAppTokens,
|
||||
);
|
||||
} finally {
|
||||
if (snap.ownsCache) {
|
||||
@@ -2187,6 +2212,7 @@ export class ZipProjectStore {
|
||||
sceneResolutions: SceneImportResolution[],
|
||||
onProgress?: (p: { stage: 'copy' | 'done'; percent: number; detail?: string }) => void,
|
||||
npcResolutions?: NpcImportResolution[],
|
||||
importAppTokens?: (sourceCacheDir: string) => Promise<Map<string, string>>,
|
||||
): Promise<{ project: Project; report: StorylineImportMergeReport }> {
|
||||
this.assertStorylineMergeAllowed(selections);
|
||||
const source = await this.readExternalProjectForImport(sourcePath);
|
||||
@@ -2201,6 +2227,7 @@ export class ZipProjectStore {
|
||||
sceneResolutions,
|
||||
onProgress,
|
||||
npcResolutions,
|
||||
importAppTokens,
|
||||
);
|
||||
} finally {
|
||||
await fs.rm(sourceCache, { recursive: true, force: true }).catch(() => undefined);
|
||||
@@ -2329,6 +2356,10 @@ function normalizeScene(s: Scene): Scene {
|
||||
const traps = (Array.isArray(rawTraps) ? rawTraps : [])
|
||||
.map((t) => normalizeSceneTrap(t))
|
||||
.filter((t): t is SceneTrap => Boolean(t));
|
||||
const rawTokens = (s as unknown as { tokens?: unknown[] }).tokens;
|
||||
const tokens = (Array.isArray(rawTokens) ? rawTokens : [])
|
||||
.map((t) => normalizeSceneToken(t))
|
||||
.filter((t): t is SceneToken => Boolean(t));
|
||||
const grid = normalizeSceneGrid((s as unknown as { grid?: unknown }).grid);
|
||||
|
||||
const rawAudios = Array.isArray(raw.audios) ? raw.audios : [];
|
||||
@@ -2359,6 +2390,7 @@ function normalizeScene(s: Scene): Scene {
|
||||
previewRotationDeg,
|
||||
darkenScene,
|
||||
traps,
|
||||
tokens,
|
||||
grid,
|
||||
layout: layoutIn ?? { x: 0, y: 0 },
|
||||
media: {
|
||||
|
||||
Reference in New Issue
Block a user