import React, { useEffect, useRef, useState } from 'react'; import type { AppToken, SceneToken, SceneTokensSessionState } from '../../../shared/types'; import { useTokenImageUrl } from './useTokenImageUrl'; import styles from './SceneTokensOverlay.module.css'; type Viewport = { x: number; y: number; w: number; h: number }; type Props = { placements: readonly SceneToken[]; library: readonly AppToken[]; session: SceneTokensSessionState | null; viewport: Viewport | null; editable?: boolean; onMove?: (placementId: string, nx: number, ny: number) => void; }; function TokenSprite({ placement, nx, ny, viewport, editable, onMove, }: { placement: SceneToken; nx: number; ny: number; viewport: Viewport; editable: boolean; onMove?: (placementId: string, nx: number, ny: number) => void; }) { const url = useTokenImageUrl(placement.tokenId); const dragRef = useRef<{ startNx: number; startNy: number; pointerNx: number; pointerNy: number; pointerId: number; lastNx: number; lastNy: number; } | null>(null); const [localPos, setLocalPos] = useState<{ nx: number; ny: number } | null>(null); const frameRef = useRef(0); useEffect(() => { if (dragRef.current) return; setLocalPos(null); }, [nx, ny]); useEffect(() => { return () => { if (frameRef.current) cancelAnimationFrame(frameRef.current); }; }, []); const posNx = localPos?.nx ?? nx; const posNy = localPos?.ny ?? ny; const minDim = Math.min(viewport.w, viewport.h); const sizePx = Math.max(16, placement.sizeN * minDim); const left = viewport.x + posNx * viewport.w; const top = viewport.y + posNy * viewport.h; const hostToNorm = (clientX: number, clientY: number, host: HTMLElement) => { const r = host.getBoundingClientRect(); const w = Math.max(1e-6, viewport.w); const h = Math.max(1e-6, viewport.h); return { x: Math.max(0, Math.min(1, (clientX - (r.left + viewport.x)) / w)), y: Math.max(0, Math.min(1, (clientY - (r.top + viewport.y)) / h)), }; }; const endDrag = (el: HTMLDivElement, pointerId: number) => { const d = dragRef.current; if (!d || d.pointerId !== pointerId) return; dragRef.current = null; if (frameRef.current) { cancelAnimationFrame(frameRef.current); frameRef.current = 0; } // Финальный commit в session store — один раз на отпускание. onMove?.(String(placement.id), d.lastNx, d.lastNy); setLocalPos({ nx: d.lastNx, ny: d.lastNy }); try { if (el.hasPointerCapture(pointerId)) el.releasePointerCapture(pointerId); } catch { /* ignore */ } }; return (
{ if (e.button !== 0) return; e.stopPropagation(); e.preventDefault(); const host = (e.currentTarget.parentElement as HTMLElement | null) ?? e.currentTarget; const p = hostToNorm(e.clientX, e.clientY, host); dragRef.current = { startNx: posNx, startNy: posNy, pointerNx: p.x, pointerNy: p.y, pointerId: e.pointerId, lastNx: posNx, lastNy: posNy, }; setLocalPos({ nx: posNx, ny: posNy }); (e.currentTarget as HTMLDivElement).setPointerCapture(e.pointerId); } : undefined } onPointerMove={ editable && onMove ? (e) => { const d = dragRef.current; if (!d || d.pointerId !== e.pointerId) return; const host = (e.currentTarget.parentElement as HTMLElement | null) ?? e.currentTarget; const p = hostToNorm(e.clientX, e.clientY, host); const nextNx = Math.max(0, Math.min(1, d.startNx + (p.x - d.pointerNx))); const nextNy = Math.max(0, Math.min(1, d.startNy + (p.y - d.pointerNy))); d.lastNx = nextNx; d.lastNy = nextNy; if (frameRef.current) return; frameRef.current = requestAnimationFrame(() => { frameRef.current = 0; const cur = dragRef.current; if (!cur) return; setLocalPos({ nx: cur.lastNx, ny: cur.lastNy }); }); } : undefined } onPointerUp={(e) => { endDrag(e.currentTarget as HTMLDivElement, e.pointerId); }} onPointerCancel={(e) => { endDrag(e.currentTarget as HTMLDivElement, e.pointerId); }} > {url ? : null}
); } export function SceneTokensOverlay({ placements, library, session, viewport, editable = false, onMove, }: Props) { if (!viewport || placements.length === 0) return null; const known = new Set(library.map((t) => t.id)); return (
{placements .filter((p) => known.has(p.tokenId)) .map((placement) => { const key = String(placement.id); const override = session?.byPlacementId[key] ?? session?.byPlacementId[placement.id]; const nx = override?.nx ?? placement.nx; const ny = override?.ny ?? placement.ny; return ( ); })}
); }