1ab6ffd593
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>
93 lines
2.9 KiB
TypeScript
93 lines
2.9 KiB
TypeScript
import React from 'react';
|
|
|
|
import type { TokenPath } from '../../../shared/types/tokenPath';
|
|
import { tokenPathPolyline } from '../../../shared/types/tokenPath';
|
|
import type { SceneNpcToken, SceneToken } from '../../../shared/types';
|
|
import { tokenPathKey, type TokenPathSessionState } from '../../../shared/types/tokenPathSession';
|
|
|
|
import styles from './TokenPathsOverlay.module.css';
|
|
|
|
type Viewport = { x: number; y: number; w: number; h: number };
|
|
|
|
type PathItem = {
|
|
key: string;
|
|
path: TokenPath;
|
|
emphasized?: boolean;
|
|
};
|
|
|
|
type Props = {
|
|
tokens: readonly SceneToken[];
|
|
npcTokens?: readonly SceneNpcToken[];
|
|
viewport: Viewport | null;
|
|
/** control/editor: always; presentation: only presentationVisible */
|
|
mode: 'always' | 'presentation';
|
|
pathSession?: TokenPathSessionState | null;
|
|
/** Highlight path currently edited */
|
|
emphasizeKey?: string | null;
|
|
};
|
|
|
|
function toSvgPoints(path: TokenPath, viewport: Viewport): string {
|
|
const pts = tokenPathPolyline(path);
|
|
return pts
|
|
.map((p) => {
|
|
const x = viewport.x + p.nx * viewport.w;
|
|
const y = viewport.y + p.ny * viewport.h;
|
|
return `${x},${y}`;
|
|
})
|
|
.join(' ');
|
|
}
|
|
|
|
export function TokenPathsOverlay({
|
|
tokens,
|
|
npcTokens = [],
|
|
viewport,
|
|
mode,
|
|
pathSession = null,
|
|
emphasizeKey = null,
|
|
}: Props) {
|
|
if (!viewport) return null;
|
|
|
|
const items: PathItem[] = [];
|
|
for (const t of tokens) {
|
|
if (!t.path || t.path.points.length < 2) continue;
|
|
const key = tokenPathKey('token', String(t.id));
|
|
if (mode === 'presentation' && !pathSession?.presentationVisible[key]) continue;
|
|
items.push({ key, path: t.path, emphasized: emphasizeKey === key });
|
|
}
|
|
for (const t of npcTokens) {
|
|
if (!t.path || t.path.points.length < 2) continue;
|
|
const key = tokenPathKey('npcToken', String(t.id));
|
|
if (mode === 'presentation' && !pathSession?.presentationVisible[key]) continue;
|
|
items.push({ key, path: t.path, emphasized: emphasizeKey === key });
|
|
}
|
|
if (items.length === 0) return null;
|
|
|
|
return (
|
|
<svg className={styles.layer} width="100%" height="100%" aria-hidden>
|
|
{items.map((item) => {
|
|
const pts = toSvgPoints(item.path, viewport);
|
|
if (!pts) return null;
|
|
const first = item.path.points[0]!;
|
|
const fx = viewport.x + first.nx * viewport.w;
|
|
const fy = viewport.y + first.ny * viewport.h;
|
|
return (
|
|
<g key={item.key} className={item.emphasized ? styles.emphasized : undefined}>
|
|
<polyline className={styles.lineGlow} points={pts} fill="none" />
|
|
<polyline className={styles.line} points={pts} fill="none" />
|
|
<circle className={styles.start} cx={fx} cy={fy} r={4} />
|
|
{item.path.closed ? (
|
|
<circle
|
|
className={styles.closed}
|
|
cx={fx}
|
|
cy={fy}
|
|
r={7}
|
|
fill="none"
|
|
/>
|
|
) : null}
|
|
</g>
|
|
);
|
|
})}
|
|
</svg>
|
|
);
|
|
}
|