feat: multi-material overlays, presentation guides, and release prebuilds

Align control/presentation with presentation screen rect and darkness z-order; sync window titles and session window cleanup. Pack Win/Mac/Linux with npmRebuild disabled and release-native-prep for classic-level and sharp.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Ivan Fontosh
2026-08-03 11:28:18 +08:00
parent 61446dacfc
commit 2979d06f1c
41 changed files with 1060 additions and 252 deletions
+112 -10
View File
@@ -2,7 +2,7 @@ import path from 'node:path';
import { app, BrowserWindow, screen } from 'electron';
import { windowChromeTitle } from '../../shared/appBranding';
import { windowChromeTitle, type AppWindowKind } from '../../shared/appBranding';
import { ipcChannels } from '../../shared/ipc/contracts';
import { safeConsoleError } from '../safeConsole';
@@ -32,7 +32,16 @@ export const SESSION_STATE_WINDOW_KINDS: readonly WindowKind[] = [
const windows = new Map<WindowKind, BrowserWindow>();
/** Язык заголовков окон (из редактора); иначе `app.getLocale()`. */
let chromeLocaleTagOverride: string | null = null;
function resolveChromeLocaleTag(): string {
return chromeLocaleTagOverride ?? app.getLocale();
}
let appQuitting = false;
/** Защита от каскада close(control) ↔ close(presentation). */
let closingPlaySession = false;
let pendingSceneDescriptionHtml = '';
/** Окно материалов — только колонка списка. */
@@ -60,6 +69,35 @@ function broadcastMultiWindowStateChanged(open: boolean): void {
}
}
export function getPresentationContentSize(): { width: number; height: number } | null {
const pres = windows.get('presentation');
if (!pres || pres.isDestroyed()) return null;
const [width, height] = pres.getContentSize();
if (width <= 0 || height <= 0) return null;
return { width, height };
}
function broadcastPresentationContentSize(): void {
const size = getPresentationContentSize();
if (!size) return;
for (const w of BrowserWindow.getAllWindows()) {
if (w.isDestroyed() || w.webContents.isDestroyed()) continue;
try {
w.webContents.send(ipcChannels.windows.presentationContentSizeChanged, size);
} catch {
/* ignore */
}
}
}
function bindPresentationContentSizeTracking(win: BrowserWindow): void {
const emit = () => broadcastPresentationContentSize();
win.on('resize', emit);
win.on('enter-full-screen', emit);
win.on('leave-full-screen', emit);
win.webContents.once('did-finish-load', emit);
}
function sendSceneDescriptionContent(win: BrowserWindow, html: string): void {
if (win.isDestroyed() || win.webContents.isDestroyed()) return;
try {
@@ -91,6 +129,51 @@ export function sendToAppWindows(
}
}
function applyWindowChromeTitle(win: BrowserWindow, kind: WindowKind): void {
if (win.isDestroyed()) return;
win.setTitle(windowChromeTitle(kind as AppWindowKind, resolveChromeLocaleTag()));
}
export function syncAllWindowChromeTitles(localeTag: string): void {
chromeLocaleTagOverride = localeTag.trim() || null;
for (const [kind, win] of windows.entries()) {
applyWindowChromeTitle(win, kind);
}
}
function bindWindowChromeTitle(win: BrowserWindow, kind: WindowKind): void {
const apply = () => applyWindowChromeTitle(win, kind);
apply();
win.webContents.on('page-title-updated', (event) => {
event.preventDefault();
apply();
});
win.webContents.on('did-finish-load', () => {
apply();
});
}
/** Закрыть окна сессии, кроме редактора и его дочерних окон. */
function closePlaySessionAuxiliaryWindows(): void {
closeSceneDescriptionWindow();
closeMaterialsWindow();
closeNpcsWindow();
}
function closePresentationWindow(): void {
const pres = windows.get('presentation');
if (pres && !pres.isDestroyed()) {
pres.close();
}
}
function closeControlWindow(): void {
const ctrl = windows.get('control');
if (ctrl && !ctrl.isDestroyed()) {
ctrl.close();
}
}
function quitAppFromEditorClose(): void {
markAppQuitting();
app.quit();
@@ -272,7 +355,7 @@ function createWindow(kind: WindowKind, opts?: CreateWindowOpts): BrowserWindow
}
}
win.setTitle(windowChromeTitle(kind, app.getLocale()));
bindWindowChromeTitle(win, kind);
if (
kind === 'sceneDescription' ||
kind === 'materials' ||
@@ -307,14 +390,30 @@ function createWindow(kind: WindowKind, opts?: CreateWindowOpts): BrowserWindow
quitAppFromEditorClose();
});
}
if (kind === 'control') {
win.on('close', () => {
if (appQuitting || closingPlaySession) return;
closingPlaySession = true;
closePlaySessionAuxiliaryWindows();
closePresentationWindow();
});
}
if (kind === 'presentation') {
bindPresentationContentSizeTracking(win);
win.on('close', () => {
if (appQuitting || closingPlaySession) return;
closingPlaySession = true;
closePlaySessionAuxiliaryWindows();
closeControlWindow();
});
}
win.on('closed', () => windows.delete(kind));
win.on('closed', () => {
if (kind !== 'presentation' && kind !== 'control') return;
const open = windows.has('presentation') || windows.has('control');
if (!open) {
closeSceneDescriptionWindow();
closeMaterialsWindow();
closeNpcsWindow();
closingPlaySession = false;
closePlaySessionAuxiliaryWindows();
}
broadcastMultiWindowStateChanged(open);
});
@@ -395,16 +494,19 @@ export function openMultiWindow() {
createWindow('control', process.platform === 'darwin' ? undefined : { parent: presentation });
}
broadcastMultiWindowStateChanged(true);
broadcastPresentationContentSize();
}
export function closeMultiWindow(): void {
closeSceneDescriptionWindow();
closeMaterialsWindow();
closeNpcsWindow();
closingPlaySession = true;
closePlaySessionAuxiliaryWindows();
const pres = windows.get('presentation');
const ctrl = windows.get('control');
if (pres) pres.close();
if (ctrl) ctrl.close();
if (pres && !pres.isDestroyed()) pres.close();
if (ctrl && !ctrl.isDestroyed()) ctrl.close();
if (!windows.has('presentation') && !windows.has('control')) {
closingPlaySession = false;
}
}
export function isMultiWindowOpen(): boolean {