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:
@@ -12,7 +12,7 @@ export function appDisplayNameForLocale(localeTag: string): string {
|
||||
return APP_DISPLAY_NAME_EN;
|
||||
}
|
||||
|
||||
/** Префикс заголовка окон: `TTRPG - Редактор`. */
|
||||
/** Префикс заголовка окон (EN): `TTRPG - Editor`. RU — полное имя «НРИ Плеер». */
|
||||
export const APP_WINDOW_BRAND = 'TTRPG';
|
||||
|
||||
export type AppWindowKind =
|
||||
@@ -27,9 +27,9 @@ export type AppWindowKind =
|
||||
| 'npcs';
|
||||
|
||||
const WINDOW_SUFFIX: Record<AppWindowKind, { ru: string; en: string }> = {
|
||||
editor: { ru: 'Редактор', en: 'Editor' },
|
||||
editor: { ru: 'Редактор компании', en: 'Campaign editor' },
|
||||
presentation: { ru: 'Презентация', en: 'Presentation' },
|
||||
control: { ru: 'Пульт', en: 'Control' },
|
||||
control: { ru: 'Пульт управления', en: 'Control deck' },
|
||||
boot: { ru: 'Загрузка', en: 'Loading' },
|
||||
sceneDescription: { ru: 'Описание сцены', en: 'Scene description' },
|
||||
materials: { ru: 'Материалы', en: 'Materials' },
|
||||
@@ -40,8 +40,10 @@ const WINDOW_SUFFIX: Record<AppWindowKind, { ru: string; en: string }> = {
|
||||
|
||||
export function windowChromeTitle(kind: AppWindowKind, localeTag: string): string {
|
||||
const tag = localeTag.trim().toLowerCase();
|
||||
const suffix = tag.startsWith('ru') ? WINDOW_SUFFIX[kind].ru : WINDOW_SUFFIX[kind].en;
|
||||
return `${APP_WINDOW_BRAND} - ${suffix}`;
|
||||
const ru = tag.startsWith('ru');
|
||||
const prefix = ru ? APP_DISPLAY_NAME_RU : APP_WINDOW_BRAND;
|
||||
const suffix = ru ? WINDOW_SUFFIX[kind].ru : WINDOW_SUFFIX[kind].en;
|
||||
return `${prefix} - ${suffix}`;
|
||||
}
|
||||
|
||||
/** Подписи фильтров в системных диалогах выбора файла (main process). */
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
/** Вписать прямоугольник целевого аспекта в бокс (как object-fit: contain). */
|
||||
export function fitAspectRect(
|
||||
boxW: number,
|
||||
boxH: number,
|
||||
aspectW: number,
|
||||
aspectH: number,
|
||||
): { x: number; y: number; w: number; h: number } {
|
||||
const bw = Math.max(1, boxW);
|
||||
const bh = Math.max(1, boxH);
|
||||
const aw = Math.max(1, aspectW);
|
||||
const ah = Math.max(1, aspectH);
|
||||
const boxAspect = bw / bh;
|
||||
const targetAspect = aw / ah;
|
||||
if (boxAspect > targetAspect) {
|
||||
const h = bh;
|
||||
const w = h * targetAspect;
|
||||
return { x: (bw - w) / 2, y: 0, w, h };
|
||||
}
|
||||
const w = bw;
|
||||
const h = w / targetAspect;
|
||||
return { x: 0, y: (bh - h) / 2, w, h };
|
||||
}
|
||||
@@ -135,6 +135,9 @@ export const ipcChannels = {
|
||||
closeNpcs: 'windows.closeNpcs',
|
||||
openSceneEditor: 'windows.openSceneEditor',
|
||||
closeSceneEditor: 'windows.closeSceneEditor',
|
||||
syncChromeTitles: 'windows.syncChromeTitles',
|
||||
getPresentationContentSize: 'windows.getPresentationContentSize',
|
||||
presentationContentSizeChanged: 'windows.presentationContentSizeChanged',
|
||||
},
|
||||
session: {
|
||||
stateChanged: 'session.stateChanged',
|
||||
@@ -257,6 +260,7 @@ export type IpcEventMap = {
|
||||
[ipcChannels.video.stateChanged]: { state: VideoPlaybackState };
|
||||
[ipcChannels.license.statusChanged]: { snapshot: LicenseSnapshot };
|
||||
[ipcChannels.windows.multiWindowStateChanged]: { open: boolean };
|
||||
[ipcChannels.windows.presentationContentSizeChanged]: { width: number; height: number };
|
||||
[ipcChannels.windows.sceneDescriptionContent]: { html: string };
|
||||
[ipcChannels.project.importZipProgress]: ZipProgressEvent;
|
||||
[ipcChannels.project.exportZipProgress]: ZipProgressEvent;
|
||||
@@ -588,6 +592,10 @@ export type IpcInvokeMap = {
|
||||
req: Record<string, never>;
|
||||
res: { open: boolean };
|
||||
};
|
||||
[ipcChannels.windows.getPresentationContentSize]: {
|
||||
req: Record<string, never>;
|
||||
res: { width: number; height: number } | { width: null; height: null };
|
||||
};
|
||||
[ipcChannels.windows.openSceneDescription]: {
|
||||
req: { html: string };
|
||||
res: { ok: true };
|
||||
@@ -632,6 +640,10 @@ export type IpcInvokeMap = {
|
||||
req: Record<string, never>;
|
||||
res: { ok: true };
|
||||
};
|
||||
[ipcChannels.windows.syncChromeTitles]: {
|
||||
req: { localeTag: string };
|
||||
res: { ok: true };
|
||||
};
|
||||
[ipcChannels.materialsOverlay.getState]: {
|
||||
req: Record<string, never>;
|
||||
res: { state: MaterialsOverlayState };
|
||||
|
||||
@@ -11,6 +11,7 @@ void test('package.json: конфиг electron-builder (mac/win/linux)', () => {
|
||||
build: {
|
||||
appId: string;
|
||||
asar: boolean;
|
||||
npmRebuild: boolean;
|
||||
asarUnpack: string[];
|
||||
extraResources: { from: string; to: string }[];
|
||||
mac: { target: unknown; artifactName?: string };
|
||||
@@ -25,6 +26,11 @@ void test('package.json: конфиг electron-builder (mac/win/linux)', () => {
|
||||
assert.ok(pkg.build);
|
||||
assert.equal(pkg.build.appId, 'com.ttrpgplayer.app');
|
||||
assert.equal(pkg.build.asar, true, 'релизный артефакт: app.asar без «голого» дерева dist в .app/.exe');
|
||||
assert.equal(
|
||||
pkg.build.npmRebuild,
|
||||
false,
|
||||
'N-API prebuilds (classic-level, sharp @img) — без node-gyp / Visual Studio на pack',
|
||||
);
|
||||
assert.ok(Array.isArray(pkg.build.asarUnpack));
|
||||
assert.ok(pkg.build.asarUnpack.some((p) => p.includes('preload')));
|
||||
assert.ok(Array.isArray(pkg.build.extraResources));
|
||||
@@ -56,6 +62,10 @@ void test('package.json: конфиг electron-builder (mac/win/linux)', () => {
|
||||
pkg.build.asarUnpack.some((p) => p.includes('@img')),
|
||||
'sharp native binaries live under node_modules/@img',
|
||||
);
|
||||
assert.ok(
|
||||
pkg.build.asarUnpack.some((p) => p.includes('classic-level')),
|
||||
'Foundry DB: classic-level stays unpacked with its prebuilds',
|
||||
);
|
||||
});
|
||||
|
||||
void test('package.json: pack:mac runs release-mac-prep before electron-builder', () => {
|
||||
|
||||
@@ -14,13 +14,14 @@ export type MaterialsOverlayLayout = {
|
||||
|
||||
export type MaterialsZoomTool = 'zoomIn' | 'zoomOut' | null;
|
||||
|
||||
/** Session-only: какой материал сейчас показан поверх сцены. */
|
||||
/** Session-only: какие материалы сейчас показаны поверх сцены. */
|
||||
export type MaterialsOverlayState = {
|
||||
revision: number;
|
||||
activeMaterialId: MaterialId | null;
|
||||
layout: MaterialsOverlayLayout;
|
||||
/** Раскладка блока описаний легенды (если материал с легендой). */
|
||||
legendLayout: MaterialsOverlayLayout;
|
||||
activeMaterialIds: MaterialId[];
|
||||
layouts: Record<string, MaterialsOverlayLayout>;
|
||||
/** Раскладка блока описаний легенды по materialId. */
|
||||
legendLayouts: Record<string, MaterialsOverlayLayout>;
|
||||
focusMaterialId: MaterialId | null;
|
||||
zoomTool: MaterialsZoomTool;
|
||||
};
|
||||
|
||||
@@ -35,10 +36,10 @@ export type MaterialsOverlayEvent =
|
||||
| { kind: 'show'; materialId: MaterialId; rotationDeg?: number }
|
||||
| { kind: 'hide' }
|
||||
| { kind: 'toggle'; materialId: MaterialId; rotationDeg?: number }
|
||||
| { kind: 'layout.set'; layout: MaterialsOverlayLayout }
|
||||
| { kind: 'legendLayout.set'; layout: MaterialsOverlayLayout }
|
||||
| { kind: 'layout.set'; materialId: MaterialId; layout: MaterialsOverlayLayout }
|
||||
| { kind: 'legendLayout.set'; materialId: MaterialId; layout: MaterialsOverlayLayout }
|
||||
| { kind: 'zoomTool.set'; tool: MaterialsZoomTool }
|
||||
| { kind: 'zoomAt'; nx: number; ny: number };
|
||||
| { kind: 'zoomAt'; nx: number; ny: number; materialId?: MaterialId };
|
||||
|
||||
/** Нормализация угла в диапазон [0, 360). */
|
||||
export function normalizeMaterialsRotation(deg: number): number {
|
||||
@@ -73,3 +74,17 @@ export function zoomMaterialsLayoutAt(
|
||||
scale: clamped.scale,
|
||||
});
|
||||
}
|
||||
|
||||
function initialLegendLayout(): MaterialsOverlayLayout {
|
||||
return clampMaterialsLayout({
|
||||
...DEFAULT_MATERIALS_OVERLAY_LAYOUT,
|
||||
cx: 0.82,
|
||||
cy: 0.5,
|
||||
scale: 0.85,
|
||||
rotationDeg: 0,
|
||||
});
|
||||
}
|
||||
|
||||
export function defaultLegendLayoutForMaterial(): MaterialsOverlayLayout {
|
||||
return initialLegendLayout();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user