feat(tokens): animated paths for scene and NPC tokens
Add path editor window, session playback on control/presentation, and RMB controls. Fix live pose clock so motion no longer freezes after ~250ms. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -59,17 +59,20 @@ import {
|
||||
focusEditorWindow,
|
||||
getPresentationContentSize,
|
||||
getSceneDescriptionContent,
|
||||
getTokenPathEditorTarget,
|
||||
isMultiWindowOpen,
|
||||
markAppQuitting,
|
||||
openMaterialsWindow,
|
||||
openMultiWindow,
|
||||
openNpcsEditorWindow,
|
||||
openSceneEditorWindow,
|
||||
openTokenPathEditorWindow,
|
||||
openNpcsWindow,
|
||||
openSceneDescriptionWindow,
|
||||
closeMaterialsWindow,
|
||||
closeNpcsEditorWindow,
|
||||
closeSceneEditorWindow,
|
||||
closeTokenPathEditorWindow,
|
||||
closeNpcsWindow,
|
||||
sendToAppWindows,
|
||||
syncAllWindowChromeTitles,
|
||||
@@ -77,6 +80,9 @@ import {
|
||||
waitForEditorWindowReady,
|
||||
warmNpcsEditorWindow,
|
||||
} from './windows/createWindows';
|
||||
import { TokenPathSessionStore } from './tokens/tokenPathSessionStore';
|
||||
import { tokenPathTotalLength } from '../shared/types/tokenPath';
|
||||
import type { TokenPathTargetKind } from '../shared/types/tokenPathSession';
|
||||
|
||||
function emitZipProgress(evt: {
|
||||
kind: 'import' | 'export';
|
||||
@@ -160,12 +166,59 @@ const videoStore = new VideoPlaybackStore();
|
||||
const materialsOverlayStore = new MaterialsOverlayStore();
|
||||
const npcsOverlayStore = new NpcsOverlayStore();
|
||||
const sceneTokensSessionStore = new SceneTokensSessionStore();
|
||||
const tokenPathSessionStore = new TokenPathSessionStore();
|
||||
const sceneNpcTokensSessionStore = new SceneNpcTokensSessionStore();
|
||||
const scenePlayerTokensSessionStore = new ScenePlayerTokensSessionStore();
|
||||
const tokenGridSnapSessionStore = new TokenGridSnapSessionStore();
|
||||
let tokensStore: TokensStore | null = null;
|
||||
let playersStore: PlayersStore | null = null;
|
||||
|
||||
function emitTokenPathSessionState(): void {
|
||||
const state = tokenPathSessionStore.getState();
|
||||
for (const win of BrowserWindow.getAllWindows()) {
|
||||
win.webContents.send(ipcChannels.tokenPathSession.stateChanged, { state });
|
||||
}
|
||||
}
|
||||
|
||||
function seedTokenPathPlaybackForProject(project: Project | null): void {
|
||||
tokenPathSessionStore.reset();
|
||||
if (!project?.currentSceneId) {
|
||||
emitTokenPathSessionState();
|
||||
return;
|
||||
}
|
||||
const scene = project.scenes[project.currentSceneId];
|
||||
if (!scene) {
|
||||
emitTokenPathSessionState();
|
||||
return;
|
||||
}
|
||||
const now = Date.now();
|
||||
const seedOne = (kind: TokenPathTargetKind, placementId: string, path: NonNullable<(typeof scene.tokens)[number]['path']>) => {
|
||||
const pathLength = tokenPathTotalLength(path);
|
||||
if (pathLength <= 1e-9) return;
|
||||
tokenPathSessionStore.dispatch({
|
||||
kind: 'seedPlayback',
|
||||
entry: {
|
||||
kind,
|
||||
placementId,
|
||||
phase: path.startMode === 'delayed' ? 'delay' : 'moving',
|
||||
baseDist: 0,
|
||||
direction: 1,
|
||||
rejoinDist: null,
|
||||
durationSec: path.durationSec,
|
||||
pathLength,
|
||||
segmentStartedAtMs: now,
|
||||
},
|
||||
});
|
||||
};
|
||||
for (const t of scene.tokens ?? []) {
|
||||
if (t.path && t.path.points.length >= 2) seedOne('token', String(t.id), t.path);
|
||||
}
|
||||
for (const t of scene.npcTokens ?? []) {
|
||||
if (t.path && t.path.points.length >= 2) seedOne('npcToken', String(t.id), t.path);
|
||||
}
|
||||
emitTokenPathSessionState();
|
||||
}
|
||||
|
||||
function emitEffectsState(): void {
|
||||
const state = effectsStore.getState();
|
||||
for (const win of BrowserWindow.getAllWindows()) {
|
||||
@@ -446,6 +499,7 @@ async function main() {
|
||||
sceneDarknessStore.resetSession();
|
||||
sceneTrapsStore.resetSession();
|
||||
sceneTokensSessionStore.reset();
|
||||
tokenPathSessionStore.reset();
|
||||
sceneNpcTokensSessionStore.reset();
|
||||
scenePlayerTokensSessionStore.reset();
|
||||
tokenGridSnapSessionStore.reset();
|
||||
@@ -461,6 +515,9 @@ async function main() {
|
||||
if (project) {
|
||||
syncSceneDarknessForProject(project);
|
||||
syncSceneTrapsForProject(project);
|
||||
seedTokenPathPlaybackForProject(project);
|
||||
} else {
|
||||
emitTokenPathSessionState();
|
||||
}
|
||||
emitSceneDarknessState();
|
||||
emitSceneTrapsState();
|
||||
@@ -527,6 +584,15 @@ async function main() {
|
||||
closeSceneEditorWindow();
|
||||
return { ok: true };
|
||||
});
|
||||
registerHandler(ipcChannels.windows.openTokenPathEditor, ({ kind, placementId }) => {
|
||||
openTokenPathEditorWindow(kind, placementId);
|
||||
return { ok: true };
|
||||
});
|
||||
registerHandler(ipcChannels.windows.closeTokenPathEditor, () => {
|
||||
closeTokenPathEditorWindow();
|
||||
return { ok: true };
|
||||
});
|
||||
registerHandler(ipcChannels.windows.getTokenPathEditorTarget, () => getTokenPathEditorTarget());
|
||||
registerHandler(ipcChannels.windows.openNpcs, (req) => {
|
||||
openNpcsWindow();
|
||||
const npcId = req?.npcId ? asNpcId(String(req.npcId)) : null;
|
||||
@@ -643,6 +709,7 @@ async function main() {
|
||||
emitSceneViewState();
|
||||
emitSceneTokensSessionState();
|
||||
emitScenePlayerTokensSessionState();
|
||||
seedTokenPathPlaybackForProject(project);
|
||||
emitSessionState();
|
||||
return { currentSceneId: projectStore.getOpenProject()?.currentSceneId ?? null };
|
||||
});
|
||||
@@ -674,6 +741,7 @@ async function main() {
|
||||
emitSceneViewState();
|
||||
emitSceneTokensSessionState();
|
||||
emitScenePlayerTokensSessionState();
|
||||
seedTokenPathPlaybackForProject(project);
|
||||
emitSessionState();
|
||||
const p = projectStore.getOpenProject();
|
||||
return {
|
||||
@@ -1411,6 +1479,14 @@ async function main() {
|
||||
emitSceneTokensSessionState();
|
||||
return { ok: true };
|
||||
});
|
||||
registerHandler(ipcChannels.tokenPathSession.getState, () => {
|
||||
return { state: tokenPathSessionStore.getState() };
|
||||
});
|
||||
registerHandler(ipcChannels.tokenPathSession.dispatch, ({ event }) => {
|
||||
tokenPathSessionStore.dispatch(event);
|
||||
emitTokenPathSessionState();
|
||||
return { ok: true };
|
||||
});
|
||||
|
||||
registerHandler(ipcChannels.tokenGridSnap.getState, () => tokenGridSnapSessionStore.getState());
|
||||
registerHandler(ipcChannels.tokenGridSnap.setEnabled, ({ enabled }) => {
|
||||
|
||||
Reference in New Issue
Block a user