DNDGamePlayer: Electron редактор сцен, презентация, упаковка electron-builder
Made-with: Cursor
This commit is contained in:
@@ -0,0 +1,121 @@
|
||||
import type { AssetId, GraphNodeId, ProjectId, SceneId } from './ids';
|
||||
|
||||
export const PROJECT_SCHEMA_VERSION = 4 as const;
|
||||
|
||||
export type IsoDateTimeString = string;
|
||||
|
||||
export type MediaAssetType = 'image' | 'video' | 'audio';
|
||||
|
||||
export type MediaAssetBase = {
|
||||
id: AssetId;
|
||||
type: MediaAssetType;
|
||||
mime: string;
|
||||
originalName: string;
|
||||
relPath: string;
|
||||
sha256: string;
|
||||
sizeBytes: number;
|
||||
createdAt: IsoDateTimeString;
|
||||
};
|
||||
|
||||
export type ImageAsset = MediaAssetBase & {
|
||||
type: 'image';
|
||||
widthPx?: number;
|
||||
heightPx?: number;
|
||||
};
|
||||
|
||||
export type VideoAsset = MediaAssetBase & {
|
||||
type: 'video';
|
||||
durationMs?: number;
|
||||
widthPx?: number;
|
||||
heightPx?: number;
|
||||
};
|
||||
|
||||
export type AudioAsset = MediaAssetBase & {
|
||||
type: 'audio';
|
||||
durationMs?: number;
|
||||
};
|
||||
|
||||
export type MediaAsset = ImageAsset | VideoAsset | AudioAsset;
|
||||
|
||||
/** Только видео и аудио сцены (изображения — только превью, отдельное поле). */
|
||||
export type SceneAudioRef = {
|
||||
assetId: AssetId;
|
||||
autoplay: boolean;
|
||||
loop: boolean;
|
||||
};
|
||||
|
||||
export type SceneMediaRefs = {
|
||||
videos: AssetId[];
|
||||
audios: SceneAudioRef[];
|
||||
};
|
||||
|
||||
export type SceneSettings = {
|
||||
autoplayVideo: boolean;
|
||||
autoplayAudio: boolean;
|
||||
loopVideo: boolean;
|
||||
loopAudio: boolean;
|
||||
};
|
||||
|
||||
export type SceneLayout = {
|
||||
x: number;
|
||||
y: number;
|
||||
};
|
||||
|
||||
/** Узел на визуальном графе (одна сцена может иметь несколько узлов). */
|
||||
export type SceneGraphNode = {
|
||||
id: GraphNodeId;
|
||||
sceneId: SceneId;
|
||||
x: number;
|
||||
y: number;
|
||||
/** Ровно один узел в проекте может быть начальной сценой для входа в граф. */
|
||||
isStartScene: boolean;
|
||||
};
|
||||
|
||||
export type SceneGraphEdge = {
|
||||
id: string;
|
||||
sourceGraphNodeId: GraphNodeId;
|
||||
targetGraphNodeId: GraphNodeId;
|
||||
};
|
||||
|
||||
export type Scene = {
|
||||
id: SceneId;
|
||||
title: string;
|
||||
description: string;
|
||||
/** Превью ассет (изображение или видео). */
|
||||
previewAssetId: AssetId | null;
|
||||
previewAssetType: 'image' | 'video' | null;
|
||||
/** Для видео-превью: автозапуск (в редакторе/списках/на графе). */
|
||||
previewVideoAutostart: boolean;
|
||||
/** Поворот превью в градусах (0/90/180/270). */
|
||||
previewRotationDeg: 0 | 90 | 180 | 270;
|
||||
media: SceneMediaRefs;
|
||||
settings: SceneSettings;
|
||||
connections: SceneId[];
|
||||
layout: SceneLayout;
|
||||
};
|
||||
|
||||
export type ProjectMeta = {
|
||||
name: string;
|
||||
/** Имя файла проекта без суффикса `.dnd.zip` (то, что пользователь редактирует). */
|
||||
fileBaseName: string;
|
||||
createdAt: IsoDateTimeString;
|
||||
updatedAt: IsoDateTimeString;
|
||||
/** Версия приложения, с которой проект был создан (не меняется при сохранениях). */
|
||||
createdWithAppVersion: string;
|
||||
/** Версия приложения при последнем сохранении. */
|
||||
appVersion: string;
|
||||
schemaVersion: typeof PROJECT_SCHEMA_VERSION;
|
||||
};
|
||||
|
||||
export type Project = {
|
||||
id: ProjectId;
|
||||
meta: ProjectMeta;
|
||||
scenes: Record<SceneId, Scene>;
|
||||
assets: Record<AssetId, MediaAsset>;
|
||||
currentSceneId: SceneId | null;
|
||||
/** Текущая нода графа (важно, когда одна сцена имеет несколько нод). */
|
||||
currentGraphNodeId: GraphNodeId | null;
|
||||
/** Позиции карточек на графе; логические связи сцен по-прежнему в `Scene.connections`. */
|
||||
sceneGraphNodes: SceneGraphNode[];
|
||||
sceneGraphEdges: SceneGraphEdge[];
|
||||
};
|
||||
@@ -0,0 +1,102 @@
|
||||
export type EffectToolType = 'fog' | 'fire' | 'rain' | 'lightning' | 'freeze' | 'eraser';
|
||||
|
||||
export type EffectInstanceType = 'fog' | 'fire' | 'rain' | 'lightning' | 'freeze' | 'scorch' | 'ice';
|
||||
|
||||
/** Нормализованные координаты (0..1) относительно области предпросмотра/презентации. */
|
||||
export type NPoint = { x: number; y: number; tMs: number; pressure?: number };
|
||||
|
||||
export type EffectInstanceBase = {
|
||||
id: string;
|
||||
type: EffectInstanceType;
|
||||
/** Для детерминизма процедурных эффектов. */
|
||||
seed: number;
|
||||
/** Время создания по часам main-процесса. */
|
||||
createdAtMs: number;
|
||||
};
|
||||
|
||||
export type FogInstance = EffectInstanceBase & {
|
||||
type: 'fog';
|
||||
points: NPoint[];
|
||||
radiusN: number;
|
||||
opacity: number;
|
||||
lifetimeMs: number | null;
|
||||
};
|
||||
|
||||
export type FireInstance = EffectInstanceBase & {
|
||||
type: 'fire';
|
||||
points: NPoint[];
|
||||
radiusN: number;
|
||||
opacity: number;
|
||||
lifetimeMs: number | null;
|
||||
};
|
||||
|
||||
export type RainInstance = EffectInstanceBase & {
|
||||
type: 'rain';
|
||||
points: NPoint[];
|
||||
radiusN: number;
|
||||
opacity: number;
|
||||
lifetimeMs: number | null;
|
||||
};
|
||||
|
||||
export type LightningInstance = EffectInstanceBase & {
|
||||
type: 'lightning';
|
||||
start: { x: number; y: number };
|
||||
end: { x: number; y: number };
|
||||
widthN: number;
|
||||
intensity: number;
|
||||
lifetimeMs: number;
|
||||
};
|
||||
|
||||
export type FreezeInstance = EffectInstanceBase & {
|
||||
type: 'freeze';
|
||||
at: { x: number; y: number };
|
||||
intensity: number;
|
||||
lifetimeMs: number;
|
||||
};
|
||||
|
||||
export type ScorchInstance = EffectInstanceBase & {
|
||||
/** Внутренний инстанс: след после молнии. */
|
||||
type: 'scorch';
|
||||
at: { x: number; y: number };
|
||||
radiusN: number;
|
||||
opacity: number;
|
||||
lifetimeMs: number;
|
||||
};
|
||||
|
||||
export type IceInstance = EffectInstanceBase & {
|
||||
/** Внутренний инстанс: ледяной след после заморозки. */
|
||||
type: 'ice';
|
||||
at: { x: number; y: number };
|
||||
radiusN: number;
|
||||
opacity: number;
|
||||
lifetimeMs: number;
|
||||
};
|
||||
|
||||
export type EffectInstance =
|
||||
| FogInstance
|
||||
| FireInstance
|
||||
| RainInstance
|
||||
| LightningInstance
|
||||
| FreezeInstance
|
||||
| ScorchInstance
|
||||
| IceInstance;
|
||||
|
||||
export type EffectToolState = {
|
||||
tool: EffectToolType;
|
||||
radiusN: number;
|
||||
intensity: number;
|
||||
};
|
||||
|
||||
export type EffectsState = {
|
||||
revision: number;
|
||||
/** Текущее время по часам main-процесса (для синхронной анимации между окнами). */
|
||||
serverNowMs: number;
|
||||
tool: EffectToolState;
|
||||
instances: EffectInstance[];
|
||||
};
|
||||
|
||||
export type EffectsEvent =
|
||||
| { kind: 'tool.set'; tool: EffectToolState }
|
||||
| { kind: 'instances.clear' }
|
||||
| { kind: 'instance.add'; instance: EffectInstance }
|
||||
| { kind: 'instance.remove'; id: string };
|
||||
@@ -0,0 +1,22 @@
|
||||
export type Brand<T, B extends string> = T & { readonly __brand: B };
|
||||
|
||||
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 function asProjectId(value: string): ProjectId {
|
||||
return value as ProjectId;
|
||||
}
|
||||
|
||||
export function asSceneId(value: string): SceneId {
|
||||
return value as SceneId;
|
||||
}
|
||||
|
||||
export function asAssetId(value: string): AssetId {
|
||||
return value as AssetId;
|
||||
}
|
||||
|
||||
export function asGraphNodeId(value: string): GraphNodeId {
|
||||
return value as GraphNodeId;
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
export * from './domain';
|
||||
export * from './effects';
|
||||
export * from './ids';
|
||||
export * from './videoPlayback';
|
||||
@@ -0,0 +1,21 @@
|
||||
import type { AssetId } from './ids';
|
||||
|
||||
export type VideoPlaybackState = {
|
||||
revision: number;
|
||||
serverNowMs: number;
|
||||
/** Какая именно видео-дорожка сейчас синхронизируется (previewAssetId). */
|
||||
targetAssetId: AssetId | null;
|
||||
playing: boolean;
|
||||
playbackRate: number;
|
||||
/** Опорная точка: при `anchorServerMs` видео было на `anchorVideoTimeSec`. */
|
||||
anchorServerMs: number;
|
||||
anchorVideoTimeSec: number;
|
||||
};
|
||||
|
||||
export type VideoPlaybackEvent =
|
||||
| { kind: 'target.set'; assetId: AssetId | null; autostart?: boolean }
|
||||
| { kind: 'play' }
|
||||
| { kind: 'pause' }
|
||||
| { kind: 'stop' }
|
||||
| { kind: 'seek'; timeSec: number }
|
||||
| { kind: 'rate.set'; rate: number };
|
||||
Reference in New Issue
Block a user