feat(tokens): app-local non-player tokens with session moves and UI polish

Add token library/placements, keep play-time moves for the session, lock presentation interactions, and fix export/import modal layout plus freeform trap label.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Ivan Fontosh
2026-07-27 11:04:12 +08:00
parent bdeb64e356
commit f270812219
44 changed files with 2617 additions and 341 deletions
+51
View File
@@ -17,16 +17,21 @@ import type {
NpcsOverlayState,
Project,
ProjectId,
AppToken,
Scene,
SceneDarknessEvent,
SceneDarknessState,
SceneGrid,
SceneId,
SceneToken,
SceneTokensSessionEvent,
SceneTokensSessionState,
SceneTrap,
SceneTrapsEvent,
SceneTrapsState,
SceneViewEvent,
SceneViewState,
TokenId,
VideoPlaybackEvent,
VideoPlaybackState,
} from '../types';
@@ -164,6 +169,19 @@ export const ipcChannels = {
dispatch: 'sceneView.dispatch',
stateChanged: 'sceneView.stateChanged',
},
tokens: {
list: 'tokens.list',
upsert: 'tokens.upsert',
delete: 'tokens.delete',
pickImage: 'tokens.pickImage',
imageUrl: 'tokens.imageUrl',
stateChanged: 'tokens.stateChanged',
},
sceneTokensSession: {
getState: 'sceneTokensSession.getState',
dispatch: 'sceneTokensSession.dispatch',
stateChanged: 'sceneTokensSession.stateChanged',
},
video: {
getState: 'video.getState',
dispatch: 'video.dispatch',
@@ -232,6 +250,8 @@ export type IpcEventMap = {
[ipcChannels.sceneDarkness.stateChanged]: { state: SceneDarknessState };
[ipcChannels.sceneTraps.stateChanged]: { state: SceneTrapsState };
[ipcChannels.sceneView.stateChanged]: { state: SceneViewState };
[ipcChannels.tokens.stateChanged]: { tokens: AppToken[] };
[ipcChannels.sceneTokensSession.stateChanged]: { state: SceneTokensSessionState };
[ipcChannels.video.stateChanged]: { state: VideoPlaybackState };
[ipcChannels.license.statusChanged]: { snapshot: LicenseSnapshot };
[ipcChannels.windows.multiWindowStateChanged]: { open: boolean };
@@ -651,6 +671,34 @@ export type IpcInvokeMap = {
req: { event: SceneViewEvent };
res: { ok: true };
};
[ipcChannels.tokens.list]: {
req: Record<string, never>;
res: { tokens: AppToken[] };
};
[ipcChannels.tokens.upsert]: {
req: { id?: TokenId | null; name: string; filePath?: string | null };
res: { token: AppToken };
};
[ipcChannels.tokens.delete]: {
req: { id: TokenId };
res: { ok: true };
};
[ipcChannels.tokens.pickImage]: {
req: Record<string, never>;
res: { canceled: true } | { canceled: false; filePath: string; previewDataUrl: string };
};
[ipcChannels.tokens.imageUrl]: {
req: { id: TokenId };
res: { url: string | null };
};
[ipcChannels.sceneTokensSession.getState]: {
req: Record<string, never>;
res: { state: SceneTokensSessionState };
};
[ipcChannels.sceneTokensSession.dispatch]: {
req: { event: SceneTokensSessionEvent };
res: { ok: true };
};
[ipcChannels.video.getState]: {
req: Record<string, never>;
res: { state: VideoPlaybackState };
@@ -690,6 +738,8 @@ export type LegacyIpcEventMap = {
[ipcChannels.sceneDarkness.stateChanged]: { state: SceneDarknessState };
[ipcChannels.sceneTraps.stateChanged]: { state: SceneTrapsState };
[ipcChannels.sceneView.stateChanged]: { state: SceneViewState };
[ipcChannels.tokens.stateChanged]: { tokens: AppToken[] };
[ipcChannels.sceneTokensSession.stateChanged]: { state: SceneTokensSessionState };
[ipcChannels.video.stateChanged]: { state: VideoPlaybackState };
[ipcChannels.license.statusChanged]: Record<string, never>;
};
@@ -704,6 +754,7 @@ export type ScenePatch = {
previewRotationDeg?: 0 | 90 | 180 | 270;
darkenScene?: boolean;
traps?: SceneTrap[];
tokens?: SceneToken[];
grid?: SceneGrid;
settings?: Partial<Scene['settings']>;
media?: Partial<Scene['media']>;