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 (