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:
Ivan Fontosh
2026-08-14 08:48:41 +08:00
parent 46bec1a86a
commit 1ab6ffd593
32 changed files with 2933 additions and 28 deletions
+24
View File
@@ -24,8 +24,11 @@ import { useScenePlayerTokensSession } from './playerToken/useScenePlayerTokensS
import { SceneOverlayHost } from './sceneOverlay/SceneOverlayHost';
import { useSceneViewState } from './sceneView/useSceneViewState';
import { SceneTokensOverlay } from './tokens/SceneTokensOverlay';
import { TokenPathsOverlay } from './tokens/TokenPathsOverlay';
import { useAppTokens } from './tokens/useAppTokens';
import { useSceneTokensSession } from './tokens/useSceneTokensSession';
import { useTokenPathLivePoses } from './tokens/useTokenPathLivePoses';
import { useTokenPathSession } from './tokens/useTokenPathSession';
import { SceneTrapsOverlay } from './traps/SceneTrapsOverlay';
import { useSceneTrapsState } from './traps/useSceneTrapsState';
import styles from './PresentationView.module.css';
@@ -61,6 +64,7 @@ export function PresentationView({
const [sceneTokensSession] = useSceneTokensSession();
const [sceneNpcTokensSession] = useSceneNpcTokensSession();
const [scenePlayerTokensSession] = useScenePlayerTokensSession();
const [tokenPathSession, tokenPathApi] = useTokenPathSession();
const [vp] = useVideoPlaybackState();
const videoElRef = useRef<HTMLVideoElement | null>(null);
const [contentRect, setContentRect] = React.useState<{ x: number; y: number; w: number; h: number } | null>(
@@ -69,6 +73,15 @@ export function PresentationView({
const scene =
session?.project && session.currentSceneId ? session.project.scenes[session.currentSceneId] : undefined;
const project = session?.project;
const pathLivePoses = useTokenPathLivePoses({
tokens: scene?.tokens ?? [],
npcTokens: scene?.npcTokens ?? [],
pathSession: tokenPathSession,
enabled: Boolean(scene),
onMarkDone: (kind, placementId) => {
void tokenPathApi.dispatch({ kind: 'markDone', target: { kind, placementId } });
},
});
const activeMaterialItems =
project && (materialsOverlay?.activeMaterialIds?.length ?? 0) > 0
? (materialsOverlay?.activeMaterialIds ?? [])
@@ -195,12 +208,22 @@ export function PresentationView({
<SceneGridOverlay grid={scene.grid} viewport={contentRect} />
) : null}
<div className={styles.vignette} />
{(scene?.previewAssetType === 'image' || scene?.previewAssetType === 'video') && contentRect ? (
<TokenPathsOverlay
tokens={scene.tokens ?? []}
npcTokens={USERS_BRANCH_FEATURES_ENABLED ? (scene.npcTokens ?? []) : []}
viewport={contentRect}
mode="presentation"
pathSession={tokenPathSession}
/>
) : null}
{(scene?.previewAssetType === 'image' || scene?.previewAssetType === 'video') && contentRect ? (
<SceneTokensOverlay
placements={scene.tokens ?? []}
library={appTokens}
session={sceneTokensSession}
viewport={contentRect}
poseOverrides={pathLivePoses.tokenPoses}
/>
) : null}
{USERS_BRANCH_FEATURES_ENABLED &&
@@ -212,6 +235,7 @@ export function PresentationView({
session={sceneNpcTokensSession}
viewport={contentRect}
grid={scene.grid}
poseOverrides={pathLivePoses.npcPoses}
/>
) : null}
{USERS_BRANCH_FEATURES_ENABLED &&