import { useEffect, useRef } from 'react'; interface SkinViewerProps { width?: number; height?: number; skinUrl?: string; capeUrl?: string; walkingSpeed?: number; autoRotate?: boolean; } const DEFAULT_SKIN = 'https://static.planetminecraft.com/files/resource_media/skin/original-steve-15053860.png'; export default function SkinViewer({ width = 300, height = 400, skinUrl, capeUrl, walkingSpeed = 0.5, autoRotate = true, }: SkinViewerProps) { const canvasRef = useRef(null); const viewerRef = useRef(null); const animRef = useRef(null); // 1) Инициализируем viewer ОДИН РАЗ useEffect(() => { let disposed = false; const init = async () => { if (!canvasRef.current || viewerRef.current) return; try { const skinview3d = await import('skinview3d'); if (disposed) return; const viewer = new skinview3d.SkinViewer({ canvas: canvasRef.current, width, height, }); // базовая настройка viewer.autoRotate = autoRotate; // анимация ходьбы const walking = new skinview3d.WalkingAnimation(); walking.speed = walkingSpeed; viewer.animation = walking; viewerRef.current = viewer; animRef.current = walking; // выставляем ресурсы сразу const finalSkin = skinUrl?.trim() ? skinUrl : DEFAULT_SKIN; await viewer.loadSkin(finalSkin); if (capeUrl?.trim()) { await viewer.loadCape(capeUrl); } else { viewer.cape = null; } } catch (e) { console.error('Ошибка при инициализации skinview3d:', e); } }; init(); return () => { disposed = true; if (viewerRef.current) { viewerRef.current.dispose(); viewerRef.current = null; animRef.current = null; } }; // ⚠️ пустой deps — создаём один раз // eslint-disable-next-line react-hooks/exhaustive-deps }, []); // 2) Обновляем размеры (не пересоздаём viewer) useEffect(() => { const viewer = viewerRef.current; if (!viewer) return; viewer.width = width; viewer.height = height; }, [width, height]); // 3) Обновляем автоповорот useEffect(() => { const viewer = viewerRef.current; if (!viewer) return; viewer.autoRotate = autoRotate; }, [autoRotate]); // 4) Обновляем скорость анимации useEffect(() => { const walking = animRef.current; if (!walking) return; walking.speed = walkingSpeed; }, [walkingSpeed]); // 5) Обновляем скин useEffect(() => { const viewer = viewerRef.current; if (!viewer) return; const finalSkin = skinUrl?.trim() ? skinUrl : DEFAULT_SKIN; // защита от кеша: добавим “bust” только если URL уже имеет query — не обязательно, но помогает const url = finalSkin.includes('?') ? `${finalSkin}&t=${Date.now()}` : `${finalSkin}?t=${Date.now()}`; viewer.loadSkin(url).catch((e: any) => console.error('loadSkin error:', e)); }, [skinUrl]); // 6) Обновляем плащ useEffect(() => { const viewer = viewerRef.current; if (!viewer) return; if (capeUrl?.trim()) { const url = capeUrl.includes('?') ? `${capeUrl}&t=${Date.now()}` : `${capeUrl}?t=${Date.now()}`; viewer.loadCape(url).catch((e: any) => console.error('loadCape error:', e)); } else { viewer.cape = null; } }, [capeUrl]); return ; }