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:
Ivan Fontosh
2026-07-24 16:29:43 +08:00
parent d3b1c4660d
commit 02d73ddf81
39 changed files with 2563 additions and 36 deletions
+61 -3
View File
@@ -21,6 +21,7 @@ import type { Project } from '../shared/types';
import { EffectsStore } from './effects/effectsStore';
import { SceneDarknessStore } from './effects/sceneDarknessStore';
import { SceneTrapsStore } from './sceneTraps/sceneTrapsStore';
import { installIpcRouter, registerHandler, setLicenseAssert } from './ipc/router';
import { LicenseService } from './license/licenseService';
import { MaterialsOverlayStore } from './materials/materialsOverlayStore';
@@ -50,10 +51,12 @@ import {
openMaterialsWindow,
openMultiWindow,
openNpcsEditorWindow,
openSceneEditorWindow,
openNpcsWindow,
openSceneDescriptionWindow,
closeMaterialsWindow,
closeNpcsEditorWindow,
closeSceneEditorWindow,
closeNpcsWindow,
sendToAppWindows,
togglePresentationFullscreen,
@@ -147,6 +150,7 @@ function installAppMenuForSession(): void {
const effectsStore = new EffectsStore();
const sceneDarknessStore = new SceneDarknessStore();
const sceneTrapsStore = new SceneTrapsStore();
const sceneViewStore = new SceneViewStore();
const videoStore = new VideoPlaybackStore();
const materialsOverlayStore = new MaterialsOverlayStore();
@@ -212,6 +216,20 @@ function syncSceneDarknessForProject(project: Project): void {
sceneDarknessStore.switchScene(cacheKey, enabled);
}
function emitSceneTrapsState(): void {
const state = sceneTrapsStore.getState();
for (const win of BrowserWindow.getAllWindows()) {
win.webContents.send(ipcChannels.sceneTraps.stateChanged, { state });
}
}
function syncSceneTrapsForProject(project: Project): void {
const cacheKey = project.currentGraphNodeId ?? project.currentSceneId ?? null;
const scene = project.currentSceneId ? project.scenes[project.currentSceneId] : undefined;
const trapIds = (scene?.traps ?? []).map((t) => t.id);
sceneTrapsStore.switchScene(cacheKey, trapIds);
}
function emitVideoState(): void {
const state = videoStore.getState();
for (const win of BrowserWindow.getAllWindows()) {
@@ -361,8 +379,12 @@ async function main() {
sceneDarknessStore.resetSession();
openMultiWindow();
const project = projectStore.getOpenProject();
if (project) syncSceneDarknessForProject(project);
if (project) {
syncSceneDarknessForProject(project);
syncSceneTrapsForProject(project);
}
emitSceneDarknessState();
emitSceneTrapsState();
return { ok: true };
});
registerHandler(ipcChannels.windows.closeMultiWindow, () => {
@@ -403,6 +425,14 @@ async function main() {
closeNpcsEditorWindow();
return { ok: true };
});
registerHandler(ipcChannels.windows.openSceneEditor, () => {
openSceneEditorWindow();
return { ok: true };
});
registerHandler(ipcChannels.windows.closeSceneEditor, () => {
closeSceneEditorWindow();
return { ok: true };
});
registerHandler(ipcChannels.windows.openNpcs, () => {
openNpcsWindow();
return { ok: true };
@@ -458,11 +488,13 @@ async function main() {
materialsOverlayStore.clear();
npcsOverlayStore.clear();
sceneDarknessStore.resetSession();
sceneTrapsStore.resetSession();
sceneViewStore.reset();
emitEffectsState();
emitMaterialsOverlayState();
emitNpcsOverlayState();
emitSceneDarknessState();
emitSceneTrapsState();
emitSceneViewState();
emitSessionState();
return { ok: true };
@@ -481,11 +513,15 @@ async function main() {
npcsOverlayStore.clear();
sceneViewStore.reset();
const project = projectStore.getOpenProject();
if (project) syncSceneDarknessForProject(project);
if (project) {
syncSceneDarknessForProject(project);
syncSceneTrapsForProject(project);
}
emitEffectsState();
emitMaterialsOverlayState();
emitNpcsOverlayState();
emitSceneDarknessState();
emitSceneTrapsState();
emitSceneViewState();
emitSessionState();
return { currentSceneId: projectStore.getOpenProject()?.currentSceneId ?? null };
@@ -504,11 +540,15 @@ async function main() {
npcsOverlayStore.clear();
sceneViewStore.reset();
const project = projectStore.getOpenProject();
if (project) syncSceneDarknessForProject(project);
if (project) {
syncSceneDarknessForProject(project);
syncSceneTrapsForProject(project);
}
emitEffectsState();
emitMaterialsOverlayState();
emitNpcsOverlayState();
emitSceneDarknessState();
emitSceneTrapsState();
emitSceneViewState();
emitSessionState();
const p = projectStore.getOpenProject();
@@ -524,6 +564,10 @@ async function main() {
syncSceneDarknessForProject(project);
emitSceneDarknessState();
}
if (project && project.currentSceneId === sceneId && patch.traps !== undefined) {
syncSceneTrapsForProject(project);
emitSceneTrapsState();
}
emitSessionState();
return { scene: next };
});
@@ -627,6 +671,11 @@ async function main() {
emitSessionState();
return { project };
});
registerHandler(ipcChannels.project.setMaterialLegend, async ({ materialId, legend }) => {
const project = await projectStore.setMaterialLegend(materialId, legend);
emitSessionState();
return { project };
});
registerHandler(ipcChannels.project.pickMaterialImage, async () => {
const { canceled, filePaths } = await dialog.showOpenDialog({
properties: ['openFile'],
@@ -1099,6 +1148,15 @@ async function main() {
return { ok: true };
});
registerHandler(ipcChannels.sceneTraps.getState, () => {
return { state: sceneTrapsStore.getState() };
});
registerHandler(ipcChannels.sceneTraps.dispatch, ({ event }) => {
sceneTrapsStore.dispatch(event);
emitSceneTrapsState();
return { ok: true };
});
registerHandler(ipcChannels.sceneView.getState, () => {
return { state: sceneViewStore.getState() };
});