feat: scene traps, material legends, and scene editor window
Add trap overlays with session state, material legend editor/panel, and a dedicated scene editor window wired through IPC and project persistence. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -34,6 +34,7 @@ import {
|
||||
stripProjectZipExtension,
|
||||
} from '../../shared/project/projectZipExtension';
|
||||
import type {
|
||||
MaterialLegend,
|
||||
MediaAsset,
|
||||
MediaAssetType,
|
||||
NpcBinding,
|
||||
@@ -46,8 +47,11 @@ import type {
|
||||
SceneGraphEdge,
|
||||
SceneGraphNode,
|
||||
SceneId,
|
||||
SceneTrap,
|
||||
} from '../../shared/types';
|
||||
import { PROJECT_SCHEMA_VERSION } from '../../shared/types';
|
||||
import { normalizeMaterialLegend } from '../../shared/types/materialLegend';
|
||||
import { normalizeSceneTrap } from '../../shared/types/sceneTraps';
|
||||
import type { AssetId, GraphNodeId, MaterialId, NpcGroupId, NpcId, NpcRelationId } from '../../shared/types/ids';
|
||||
import {
|
||||
asAssetId,
|
||||
@@ -611,10 +615,12 @@ export class ZipProjectStore {
|
||||
previewVideoAutostart: false,
|
||||
previewRotationDeg: 0,
|
||||
darkenScene: false,
|
||||
traps: [],
|
||||
} satisfies Scene);
|
||||
|
||||
const next: Scene = {
|
||||
...base,
|
||||
traps: base.traps ?? [],
|
||||
...(patch.title !== undefined ? { title: patch.title } : null),
|
||||
...(patch.description !== undefined ? { description: patch.description } : null),
|
||||
...(patch.previewAssetId !== undefined ? { previewAssetId: patch.previewAssetId } : null),
|
||||
@@ -627,6 +633,13 @@ export class ZipProjectStore {
|
||||
: null),
|
||||
...(patch.previewRotationDeg !== undefined ? { previewRotationDeg: patch.previewRotationDeg } : null),
|
||||
...(patch.darkenScene !== undefined ? { darkenScene: patch.darkenScene } : null),
|
||||
...(patch.traps !== undefined
|
||||
? {
|
||||
traps: patch.traps
|
||||
.map((t) => normalizeSceneTrap(t))
|
||||
.filter((t): t is SceneTrap => Boolean(t)),
|
||||
}
|
||||
: null),
|
||||
...(patch.settings ? { settings: { ...base.settings, ...patch.settings } } : null),
|
||||
...(patch.media ? { media: { ...base.media, ...patch.media } } : null),
|
||||
...(patch.layout ? { layout: { ...base.layout, ...patch.layout } } : null),
|
||||
@@ -1163,6 +1176,7 @@ export class ZipProjectStore {
|
||||
name,
|
||||
assetId,
|
||||
rotationDeg: prev.rotationDeg ?? 0,
|
||||
...(prev.legend ? { legend: prev.legend } : {}),
|
||||
};
|
||||
} else {
|
||||
if (!nextAssetId) throw new Error('Material image is required');
|
||||
@@ -1198,6 +1212,30 @@ export class ZipProjectStore {
|
||||
return latest;
|
||||
}
|
||||
|
||||
async setMaterialLegend(materialId: MaterialId, legend: MaterialLegend | null): Promise<Project> {
|
||||
const open = this.openProject;
|
||||
if (!open) throw new Error('No open project');
|
||||
const normalized = legend ? normalizeMaterialLegend(legend) : undefined;
|
||||
await this.updateProject((p) => {
|
||||
const materials = (p.materials ?? []).map((m) => {
|
||||
if (m.id !== materialId) return m;
|
||||
if (
|
||||
!normalized ||
|
||||
(!normalized.enabled && normalized.items.length === 0 && normalized.markers.length === 0)
|
||||
) {
|
||||
const { legend: _drop, ...rest } = m;
|
||||
void _drop;
|
||||
return rest;
|
||||
}
|
||||
return { ...m, legend: normalized };
|
||||
});
|
||||
return { ...p, materials };
|
||||
});
|
||||
const latest = this.getOpenProject();
|
||||
if (!latest) throw new Error('No open project');
|
||||
return latest;
|
||||
}
|
||||
|
||||
async deleteMaterial(materialId: MaterialId): Promise<Project> {
|
||||
const open = this.openProject;
|
||||
if (!open) throw new Error('No open project');
|
||||
@@ -2270,6 +2308,10 @@ function normalizeScene(s: Scene): Scene {
|
||||
const previewThumbAssetId =
|
||||
(s as unknown as { previewThumbAssetId?: AssetId | null }).previewThumbAssetId ?? null;
|
||||
const darkenScene = Boolean((s as unknown as { darkenScene?: boolean }).darkenScene);
|
||||
const rawTraps = (s as unknown as { traps?: unknown[] }).traps;
|
||||
const traps = (Array.isArray(rawTraps) ? rawTraps : [])
|
||||
.map((t) => normalizeSceneTrap(t))
|
||||
.filter((t): t is SceneTrap => Boolean(t));
|
||||
|
||||
const rawAudios = Array.isArray(raw.audios) ? raw.audios : [];
|
||||
const audios = rawAudios
|
||||
@@ -2298,6 +2340,7 @@ function normalizeScene(s: Scene): Scene {
|
||||
previewVideoAutostart,
|
||||
previewRotationDeg,
|
||||
darkenScene,
|
||||
traps,
|
||||
layout: layoutIn ?? { x: 0, y: 0 },
|
||||
media: {
|
||||
videos: raw.videos ?? [],
|
||||
@@ -2342,17 +2385,37 @@ function normalizeProject(p: Project): Project {
|
||||
const materials = (Array.isArray(rawMaterials) ? rawMaterials : [])
|
||||
.map((m) => {
|
||||
if (!m || typeof m !== 'object') return null;
|
||||
const obj = m as { id?: string; name?: string; assetId?: AssetId; rotationDeg?: number };
|
||||
const obj = m as {
|
||||
id?: string;
|
||||
name?: string;
|
||||
assetId?: AssetId;
|
||||
rotationDeg?: number;
|
||||
legend?: unknown;
|
||||
};
|
||||
if (!obj.id || !obj.assetId || typeof obj.name !== 'string') return null;
|
||||
const name = obj.name.trim();
|
||||
if (!name) return null;
|
||||
const rot = obj.rotationDeg;
|
||||
const rotationDeg: 0 | 90 | 180 | 270 = rot === 90 || rot === 180 || rot === 270 ? rot : 0;
|
||||
return { id: asMaterialId(String(obj.id)), name, assetId: obj.assetId, rotationDeg };
|
||||
const legend = normalizeMaterialLegend(obj.legend);
|
||||
return {
|
||||
id: asMaterialId(String(obj.id)),
|
||||
name,
|
||||
assetId: obj.assetId,
|
||||
rotationDeg,
|
||||
...(legend ? { legend } : {}),
|
||||
};
|
||||
})
|
||||
.filter(
|
||||
(x): x is { id: MaterialId; name: string; assetId: AssetId; rotationDeg: 0 | 90 | 180 | 270 } =>
|
||||
Boolean(x),
|
||||
(
|
||||
x,
|
||||
): x is {
|
||||
id: MaterialId;
|
||||
name: string;
|
||||
assetId: AssetId;
|
||||
rotationDeg: 0 | 90 | 180 | 270;
|
||||
legend?: MaterialLegend;
|
||||
} => Boolean(x),
|
||||
);
|
||||
const npcGroups = normalizeNpcGroups((p as unknown as { npcGroups?: unknown }).npcGroups);
|
||||
const groupIdSet = new Set(npcGroups.map((g) => g.id));
|
||||
|
||||
Reference in New Issue
Block a user