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:
@@ -386,6 +386,7 @@ export async function buildProjectFromFoundryDocuments(
|
||||
previewVideoAutostart: previewAssetType === 'video',
|
||||
previewRotationDeg: 0,
|
||||
darkenScene: false,
|
||||
traps: [],
|
||||
media: { videos: [], audios: audioRefs },
|
||||
settings: {
|
||||
autoplayVideo: previewAssetType === 'video',
|
||||
|
||||
+61
-3
@@ -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() };
|
||||
});
|
||||
|
||||
@@ -14,6 +14,7 @@ function emptyState(): MaterialsOverlayState {
|
||||
revision: 1,
|
||||
activeMaterialId: null,
|
||||
layout: { ...DEFAULT_MATERIALS_OVERLAY_LAYOUT },
|
||||
legendLayout: { ...DEFAULT_MATERIALS_OVERLAY_LAYOUT, cx: 0.82, cy: 0.5, scale: 0.85 },
|
||||
zoomTool: null,
|
||||
};
|
||||
}
|
||||
@@ -25,6 +26,16 @@ function initialLayout(rotationDeg?: number): MaterialsOverlayLayout {
|
||||
});
|
||||
}
|
||||
|
||||
function initialLegendLayout(): MaterialsOverlayLayout {
|
||||
return clampMaterialsLayout({
|
||||
...DEFAULT_MATERIALS_OVERLAY_LAYOUT,
|
||||
cx: 0.82,
|
||||
cy: 0.5,
|
||||
scale: 0.85,
|
||||
rotationDeg: 0,
|
||||
});
|
||||
}
|
||||
|
||||
export class MaterialsOverlayStore {
|
||||
private state: MaterialsOverlayState = emptyState();
|
||||
|
||||
@@ -40,6 +51,7 @@ export class MaterialsOverlayStore {
|
||||
revision: this.state.revision + 1,
|
||||
activeMaterialId: null,
|
||||
layout: { ...DEFAULT_MATERIALS_OVERLAY_LAYOUT },
|
||||
legendLayout: initialLegendLayout(),
|
||||
zoomTool: null,
|
||||
};
|
||||
return this.state;
|
||||
@@ -54,6 +66,7 @@ export class MaterialsOverlayStore {
|
||||
revision: this.state.revision + 1,
|
||||
activeMaterialId: event.materialId,
|
||||
layout: initialLayout(event.rotationDeg),
|
||||
legendLayout: initialLegendLayout(),
|
||||
zoomTool: this.state.zoomTool,
|
||||
};
|
||||
return this.state;
|
||||
@@ -65,6 +78,7 @@ export class MaterialsOverlayStore {
|
||||
revision: this.state.revision + 1,
|
||||
activeMaterialId: event.materialId,
|
||||
layout: initialLayout(event.rotationDeg),
|
||||
legendLayout: initialLegendLayout(),
|
||||
zoomTool: this.state.zoomTool,
|
||||
};
|
||||
return this.state;
|
||||
@@ -81,6 +95,19 @@ export class MaterialsOverlayStore {
|
||||
};
|
||||
return this.state;
|
||||
}
|
||||
case 'legendLayout.set': {
|
||||
if (this.state.activeMaterialId === null) return this.state;
|
||||
this.state = {
|
||||
...this.state,
|
||||
revision: this.state.revision + 1,
|
||||
legendLayout: clampMaterialsLayout({
|
||||
...this.state.legendLayout,
|
||||
...event.layout,
|
||||
rotationDeg: 0,
|
||||
}),
|
||||
};
|
||||
return this.state;
|
||||
}
|
||||
case 'zoomTool.set': {
|
||||
const tool: MaterialsZoomTool = event.tool;
|
||||
this.state = {
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -0,0 +1,134 @@
|
||||
import {
|
||||
defaultTrapRuntime,
|
||||
type SceneTrapsEvent,
|
||||
type SceneTrapsState,
|
||||
type SceneTrapRuntime,
|
||||
} from '../../shared/types';
|
||||
|
||||
function emptyState(): SceneTrapsState {
|
||||
return {
|
||||
revision: 1,
|
||||
cacheKey: null,
|
||||
byId: {},
|
||||
lastActivation: null,
|
||||
};
|
||||
}
|
||||
|
||||
function cloneById(byId: Record<string, SceneTrapRuntime>): Record<string, SceneTrapRuntime> {
|
||||
const out: Record<string, SceneTrapRuntime> = {};
|
||||
for (const [k, v] of Object.entries(byId)) {
|
||||
out[k] = { ...v };
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
export class SceneTrapsStore {
|
||||
private state: SceneTrapsState = emptyState();
|
||||
/** Кэш runtime по ключу сцены/ноды на время сессии показа. */
|
||||
private cache = new Map<string, Record<string, SceneTrapRuntime>>();
|
||||
private currentKey: string | null = null;
|
||||
private activationToken = 0;
|
||||
|
||||
getState(): SceneTrapsState {
|
||||
return {
|
||||
...this.state,
|
||||
byId: cloneById(this.state.byId),
|
||||
lastActivation: this.state.lastActivation ? { ...this.state.lastActivation } : null,
|
||||
};
|
||||
}
|
||||
|
||||
resetSession(): void {
|
||||
this.cache.clear();
|
||||
this.currentKey = null;
|
||||
this.state = emptyState();
|
||||
}
|
||||
|
||||
/**
|
||||
* Переключение сцены: сохраняем runtime в кэш и подгружаем/инициализируем для новых trapIds.
|
||||
*/
|
||||
switchScene(cacheKey: string | null, trapIds: readonly string[]): SceneTrapsState {
|
||||
if (this.currentKey !== null) {
|
||||
this.cache.set(this.currentKey, cloneById(this.state.byId));
|
||||
}
|
||||
this.currentKey = cacheKey;
|
||||
const cached = cacheKey ? this.cache.get(cacheKey) : undefined;
|
||||
const byId: Record<string, SceneTrapRuntime> = {};
|
||||
for (const id of trapIds) {
|
||||
byId[id] = cached?.[id] ? { ...cached[id]! } : defaultTrapRuntime();
|
||||
}
|
||||
if (cacheKey) {
|
||||
this.cache.set(cacheKey, cloneById(byId));
|
||||
}
|
||||
this.state = {
|
||||
revision: this.state.revision + 1,
|
||||
cacheKey,
|
||||
byId,
|
||||
lastActivation: null,
|
||||
};
|
||||
return this.getState();
|
||||
}
|
||||
|
||||
dispatch(event: SceneTrapsEvent): SceneTrapsState {
|
||||
switch (event.kind) {
|
||||
case 'syncTrapIds': {
|
||||
const byId: Record<string, SceneTrapRuntime> = {};
|
||||
for (const id of event.trapIds) {
|
||||
byId[id] = this.state.byId[id] ? { ...this.state.byId[id]! } : defaultTrapRuntime();
|
||||
}
|
||||
if (this.currentKey) this.cache.set(this.currentKey, cloneById(byId));
|
||||
this.state = {
|
||||
...this.state,
|
||||
revision: this.state.revision + 1,
|
||||
byId,
|
||||
};
|
||||
return this.getState();
|
||||
}
|
||||
case 'reveal': {
|
||||
const cur = this.state.byId[event.trapId];
|
||||
if (!cur) return this.getState();
|
||||
const byId = cloneById(this.state.byId);
|
||||
byId[event.trapId] = { ...cur, revealed: true };
|
||||
if (this.currentKey) this.cache.set(this.currentKey, cloneById(byId));
|
||||
this.state = {
|
||||
...this.state,
|
||||
revision: this.state.revision + 1,
|
||||
byId,
|
||||
};
|
||||
return this.getState();
|
||||
}
|
||||
case 'activate': {
|
||||
const cur = this.state.byId[event.trapId];
|
||||
if (!cur) return this.getState();
|
||||
const byId = cloneById(this.state.byId);
|
||||
byId[event.trapId] = { status: 'active', revealed: true };
|
||||
this.activationToken += 1;
|
||||
if (this.currentKey) this.cache.set(this.currentKey, cloneById(byId));
|
||||
this.state = {
|
||||
...this.state,
|
||||
revision: this.state.revision + 1,
|
||||
byId,
|
||||
lastActivation: { trapId: event.trapId, token: this.activationToken },
|
||||
};
|
||||
return this.getState();
|
||||
}
|
||||
case 'disarm': {
|
||||
const cur = this.state.byId[event.trapId];
|
||||
if (!cur) return this.getState();
|
||||
const byId = cloneById(this.state.byId);
|
||||
byId[event.trapId] = { status: 'disarmed', revealed: true };
|
||||
if (this.currentKey) this.cache.set(this.currentKey, cloneById(byId));
|
||||
this.state = {
|
||||
...this.state,
|
||||
revision: this.state.revision + 1,
|
||||
byId,
|
||||
};
|
||||
return this.getState();
|
||||
}
|
||||
default: {
|
||||
const _x: never = event;
|
||||
void _x;
|
||||
return this.getState();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -17,6 +17,7 @@ export type WindowKind =
|
||||
| 'sceneDescription'
|
||||
| 'materials'
|
||||
| 'npcsEditor'
|
||||
| 'sceneEditor'
|
||||
| 'npcs';
|
||||
|
||||
/** Окна, которые реально слушают session.stateChanged (редактор синхронизируется через invoke). */
|
||||
@@ -26,6 +27,7 @@ export const SESSION_STATE_WINDOW_KINDS: readonly WindowKind[] = [
|
||||
'materials',
|
||||
'npcs',
|
||||
'npcsEditor',
|
||||
'sceneEditor',
|
||||
] as const;
|
||||
|
||||
const windows = new Map<WindowKind, BrowserWindow>();
|
||||
@@ -122,6 +124,8 @@ function pageNameForKind(kind: WindowKind): string {
|
||||
return 'materials.html';
|
||||
case 'npcsEditor':
|
||||
return 'npcsEditor.html';
|
||||
case 'sceneEditor':
|
||||
return 'sceneEditor.html';
|
||||
case 'npcs':
|
||||
return 'npcs.html';
|
||||
}
|
||||
@@ -192,6 +196,7 @@ function windowSizeForKind(kind: WindowKind): { width: number; height: number }
|
||||
if (kind === 'sceneDescription') return { width: 720, height: 640 };
|
||||
if (kind === 'materials') return { width: MATERIALS_WINDOW_WIDTH, height: MATERIALS_WINDOW_HEIGHT };
|
||||
if (kind === 'npcsEditor') return { width: NPCS_EDITOR_WINDOW_WIDTH, height: NPCS_EDITOR_WINDOW_HEIGHT };
|
||||
if (kind === 'sceneEditor') return { width: 1280, height: 800 };
|
||||
if (kind === 'npcs') return { width: NPCS_WINDOW_WIDTH, height: NPCS_WINDOW_HEIGHT };
|
||||
return { width: 1280, height: 800 };
|
||||
}
|
||||
@@ -229,6 +234,15 @@ function createWindow(kind: WindowKind, opts?: CreateWindowOpts): BrowserWindow
|
||||
autoHideMenuBar: true,
|
||||
}
|
||||
: {}),
|
||||
...(kind === 'sceneEditor'
|
||||
? {
|
||||
width: 1280,
|
||||
height: 800,
|
||||
minWidth: 960,
|
||||
minHeight: 600,
|
||||
autoHideMenuBar: true,
|
||||
}
|
||||
: {}),
|
||||
...(kind === 'npcs'
|
||||
? {
|
||||
width: NPCS_WINDOW_WIDTH,
|
||||
@@ -267,6 +281,7 @@ function createWindow(kind: WindowKind, opts?: CreateWindowOpts): BrowserWindow
|
||||
kind === 'sceneDescription' ||
|
||||
kind === 'materials' ||
|
||||
kind === 'npcsEditor' ||
|
||||
kind === 'sceneEditor' ||
|
||||
kind === 'npcs'
|
||||
) {
|
||||
win.setMenuBarVisibility(false);
|
||||
@@ -421,6 +436,13 @@ export function closeNpcsEditorWindow(): void {
|
||||
}
|
||||
}
|
||||
|
||||
export function closeSceneEditorWindow(): void {
|
||||
const win = windows.get('sceneEditor');
|
||||
if (win && !win.isDestroyed()) {
|
||||
win.close();
|
||||
}
|
||||
}
|
||||
|
||||
export function closeNpcsWindow(): void {
|
||||
const win = windows.get('npcs');
|
||||
if (win && !win.isDestroyed()) {
|
||||
@@ -536,6 +558,37 @@ export function openNpcsEditorWindow(): void {
|
||||
});
|
||||
}
|
||||
|
||||
/** Редактор сцены: расстановка ловушек и легенды материалов. */
|
||||
export function openSceneEditorWindow(): void {
|
||||
const existing = windows.get('sceneEditor');
|
||||
if (existing && !existing.isDestroyed()) {
|
||||
if (existing.isMinimized()) existing.restore();
|
||||
existing.show();
|
||||
existing.focus();
|
||||
existing.moveTop();
|
||||
return;
|
||||
}
|
||||
|
||||
const parent = windows.get('editor');
|
||||
const win = createWindow('sceneEditor', parent ? { parent } : undefined);
|
||||
const { width, height } = win.getBounds();
|
||||
const display = screen.getDisplayMatching(parent?.getBounds() ?? win.getBounds());
|
||||
const { x, y, width: dw, height: dh } = display.workArea;
|
||||
win.setBounds({
|
||||
x: Math.round(x + Math.max(0, (dw - width) / 2)),
|
||||
y: Math.round(y + Math.max(0, (dh - height) / 2)),
|
||||
width,
|
||||
height,
|
||||
});
|
||||
win.webContents.once('did-finish-load', () => {
|
||||
if (!win.isDestroyed()) {
|
||||
win.show();
|
||||
win.focus();
|
||||
win.moveTop();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/** Пульт НПС: список + описание выбранного; оверлей аватара на сцене. */
|
||||
export function openNpcsWindow(): void {
|
||||
const existing = windows.get('npcs');
|
||||
|
||||
Reference in New Issue
Block a user