feat(materials): add campaign materials overlay for sessions

Let GMs manage and show images over the scene from the editor and control panel, with zoom tools, help docs, and full ru/en i18n.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Ivan Fontosh
2026-07-17 11:05:38 +08:00
parent 195d4be086
commit 61875be857
39 changed files with 2521 additions and 22 deletions
@@ -61,12 +61,13 @@ function minimalProject(overrides: Partial<Project> = {}): Project {
updatedAt: '2020-01-01T00:00:00.000Z',
createdWithAppVersion: '1',
appVersion: '1',
schemaVersion: 5,
schemaVersion: 7,
},
scenes: {},
sceneListOrder: [],
assets: {},
campaignAudios: [],
materials: [],
currentSceneId: null,
currentGraphNodeId: null,
sceneGraphNodes: [],
+28 -1
View File
@@ -14,7 +14,7 @@ import type {
SceneId,
} from '../types';
import type { AssetId, ProjectId } from '../types/ids';
import { asAssetId, asGraphNodeId, asProjectId, asSceneId } from '../types/ids';
import { asAssetId, asGraphNodeId, asMaterialId, asProjectId, asSceneId } from '../types/ids';
export type StorylineKind = 'main' | 'side';
@@ -273,6 +273,7 @@ export function buildPartialExportProject(
...draft,
assets,
campaignAudios: source.campaignAudios.map((a) => ({ ...a })),
materials: (source.materials ?? []).map((m) => ({ ...m })),
currentSceneId: mainStart?.sceneId ?? firstSide?.sceneId ?? null,
currentGraphNodeId: mainStart?.id ?? firstSide?.id ?? null,
};
@@ -287,6 +288,7 @@ function collectReferencedAssetIdsForProject(p: Project): Set<AssetId> {
for (const au of sc.media.audios) refs.add(au.assetId);
}
for (const au of p.campaignAudios) refs.add(au.assetId);
for (const m of p.materials ?? []) refs.add(m.assetId);
return refs;
}
@@ -373,6 +375,7 @@ export function mergeStorylinesIntoProject(
for (const au of sc.media.audios) neededAssetIds.add(au.assetId);
}
for (const au of source.campaignAudios) neededAssetIds.add(au.assetId);
for (const m of source.materials ?? []) neededAssetIds.add(m.assetId);
const assetMap = new Map<AssetId, AssetId>();
const targetSha = new Map<string, AssetId>();
@@ -483,11 +486,35 @@ export function mergeStorylinesIntoProject(
}
}
const materials = [...(target.materials ?? [])];
const materialNameKeys = new Set(materials.map((m) => m.name.trim().toLowerCase()));
const materialAssetIds = new Set(materials.map((m) => m.assetId));
for (const m of source.materials ?? []) {
const mapped = assetMap.get(m.assetId) ?? m.assetId;
if (materialAssetIds.has(mapped)) continue;
let name = m.name.trim();
const baseKey = name.toLowerCase();
if (materialNameKeys.has(baseKey)) {
let n = 2;
while (materialNameKeys.has(`${baseKey} (${String(n)})`)) n += 1;
name = `${name} (${String(n)})`;
}
materials.push({
id: asMaterialId(`mat_${generateId()}`),
name,
assetId: mapped,
rotationDeg: m.rotationDeg ?? 0,
});
materialNameKeys.add(name.toLowerCase());
materialAssetIds.add(mapped);
}
let merged: Project = {
...target,
scenes,
assets,
campaignAudios,
materials,
sceneGraphNodes: [...target.sceneGraphNodes, ...newGraphNodes],
sceneGraphEdges: [...target.sceneGraphEdges, ...newEdges],
};