feat(materials): add campaign materials overlay for sessions
Let GMs manage and show images over the scene from the editor and control panel, with zoom tools, help docs, and full ru/en i18n. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -1,6 +1,14 @@
|
||||
import type { AssetId, GraphNodeId, ProjectId, SceneId } from './ids';
|
||||
import type { AssetId, GraphNodeId, MaterialId, ProjectId, SceneId } from './ids';
|
||||
|
||||
export const PROJECT_SCHEMA_VERSION = 5 as const;
|
||||
export const PROJECT_SCHEMA_VERSION = 7 as const;
|
||||
|
||||
/** Материал кампании: изображение, показываемое поверх сцены во время игры. */
|
||||
export type ProjectMaterial = {
|
||||
id: MaterialId;
|
||||
name: string;
|
||||
assetId: AssetId;
|
||||
rotationDeg: 0 | 90 | 180 | 270;
|
||||
};
|
||||
|
||||
export type IsoDateTimeString = string;
|
||||
|
||||
@@ -135,6 +143,8 @@ export type Project = {
|
||||
assets: Record<AssetId, MediaAsset>;
|
||||
/** Аудио кампании: играет в пульте на протяжении всей презентации, если в сцене нет своей музыки. */
|
||||
campaignAudios: SceneAudioRef[];
|
||||
/** Материалы кампании: изображения для показа поверх сцены (порядок = порядок в списке). */
|
||||
materials: ProjectMaterial[];
|
||||
currentSceneId: SceneId | null;
|
||||
/** Текущая нода графа (важно, когда одна сцена имеет несколько нод). */
|
||||
currentGraphNodeId: GraphNodeId | null;
|
||||
|
||||
@@ -4,6 +4,7 @@ export type ProjectId = Brand<string, 'ProjectId'>;
|
||||
export type SceneId = Brand<string, 'SceneId'>;
|
||||
export type AssetId = Brand<string, 'AssetId'>;
|
||||
export type GraphNodeId = Brand<string, 'GraphNodeId'>;
|
||||
export type MaterialId = Brand<string, 'MaterialId'>;
|
||||
|
||||
export function asProjectId(value: string): ProjectId {
|
||||
return value as ProjectId;
|
||||
@@ -20,3 +21,7 @@ export function asAssetId(value: string): AssetId {
|
||||
export function asGraphNodeId(value: string): GraphNodeId {
|
||||
return value as GraphNodeId;
|
||||
}
|
||||
|
||||
export function asMaterialId(value: string): MaterialId {
|
||||
return value as MaterialId;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
export * from './domain';
|
||||
export * from './effects';
|
||||
export * from './ids';
|
||||
export * from './materials';
|
||||
export * from './sceneDarkness';
|
||||
export * from './videoPlayback';
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
import type { MaterialId } from './ids';
|
||||
|
||||
/** Нормированная раскладка материала в области показа (0..1). */
|
||||
export type MaterialsOverlayLayout = {
|
||||
/** Центр по X относительно вьюпорта. */
|
||||
cx: number;
|
||||
/** Центр по Y относительно вьюпорта. */
|
||||
cy: number;
|
||||
/** Масштаб относительно «базового contain» (1 = по умолчанию). */
|
||||
scale: number;
|
||||
};
|
||||
|
||||
export type MaterialsZoomTool = 'zoomIn' | 'zoomOut' | null;
|
||||
|
||||
/** Session-only: какой материал сейчас показан поверх сцены. */
|
||||
export type MaterialsOverlayState = {
|
||||
revision: number;
|
||||
activeMaterialId: MaterialId | null;
|
||||
layout: MaterialsOverlayLayout;
|
||||
zoomTool: MaterialsZoomTool;
|
||||
};
|
||||
|
||||
export const DEFAULT_MATERIALS_OVERLAY_LAYOUT: MaterialsOverlayLayout = {
|
||||
cx: 0.5,
|
||||
cy: 0.5,
|
||||
scale: 1,
|
||||
};
|
||||
|
||||
export type MaterialsOverlayEvent =
|
||||
| { kind: 'show'; materialId: MaterialId }
|
||||
| { kind: 'hide' }
|
||||
| { kind: 'toggle'; materialId: MaterialId }
|
||||
| { kind: 'layout.set'; layout: MaterialsOverlayLayout }
|
||||
| { kind: 'zoomTool.set'; tool: MaterialsZoomTool }
|
||||
| { kind: 'zoomAt'; nx: number; ny: number };
|
||||
|
||||
export function clampMaterialsLayout(layout: MaterialsOverlayLayout): MaterialsOverlayLayout {
|
||||
return {
|
||||
cx: Math.min(1.2, Math.max(-0.2, layout.cx)),
|
||||
cy: Math.min(1.2, Math.max(-0.2, layout.cy)),
|
||||
scale: Math.min(8, Math.max(0.15, layout.scale)),
|
||||
};
|
||||
}
|
||||
|
||||
/** Zoom around normalized point so that point stays fixed. */
|
||||
export function zoomMaterialsLayoutAt(
|
||||
layout: MaterialsOverlayLayout,
|
||||
nx: number,
|
||||
ny: number,
|
||||
factor: number,
|
||||
): MaterialsOverlayLayout {
|
||||
const nextScale = layout.scale * factor;
|
||||
const clamped = clampMaterialsLayout({ ...layout, scale: nextScale });
|
||||
const ratio = clamped.scale / layout.scale;
|
||||
return clampMaterialsLayout({
|
||||
...layout,
|
||||
cx: nx - (nx - layout.cx) * ratio,
|
||||
cy: ny - (ny - layout.cy) * ratio,
|
||||
scale: clamped.scale,
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user