import { FederatedMouseEvent, FederatedWheelEvent } from "pixi.js"; import { Component, ReactNode, useEffect, useState, useRef } from "react"; import { useTransform } from "./TransformContext"; import { useMapData } from "./MapDataContext"; import { SCALE_FACTOR } from "./Constants"; import { useApplication } from "@pixi/react"; class ErrorBoundary extends Component< { children: ReactNode }, { hasError: boolean } > { state = { hasError: false }; static getDerivedStateFromError() { return { hasError: true }; } componentDidCatch(error: Error, info: React.ErrorInfo) { console.error("Error caught:", error, info); } render() { return this.state.hasError ?

Whoopsie Daisy!

: this.props.children; } } export function InfiniteCanvas({ children, }: Readonly<{ children?: ReactNode }>) { const { position, setPosition, scale, setScale, rotation, setRotation, setScreenCenter, screenCenter, } = useTransform(); const { routeData, originalRouteData, setSelectedSight } = useMapData(); const applicationRef = useApplication(); const [isDragging, setIsDragging] = useState(false); const [startMousePosition, setStartMousePosition] = useState({ x: 0, y: 0 }); const [startRotation, setStartRotation] = useState(0); const [startPosition, setStartPosition] = useState({ x: 0, y: 0 }); const [isPointerDown, setIsPointerDown] = useState(false); // Флаг для предотвращения конфликта между пользовательским вводом и данными маршрута const [isUserInteracting, setIsUserInteracting] = useState(false); // Реф для отслеживания последнего значения originalRouteData?.rotate const lastOriginalRotation = useRef(undefined); useEffect(() => { const canvas = applicationRef?.app?.canvas; if (!canvas) return; const canvasRect = canvas.getBoundingClientRect(); const canvasLeft = canvasRect.left; const canvasTop = canvasRect.top; const centerX = window.innerWidth / 2 - canvasLeft; const centerY = window.innerHeight / 2 - canvasTop; setScreenCenter({ x: centerX, y: centerY }); }, [applicationRef?.app?.canvas, setScreenCenter]); const handlePointerDown = (e: FederatedMouseEvent) => { setIsPointerDown(true); setIsDragging(false); setIsUserInteracting(true); // Устанавливаем флаг взаимодействия пользователя setStartPosition({ x: position.x, y: position.y, }); setStartMousePosition({ x: e.globalX, y: e.globalY, }); setStartRotation(rotation); e.stopPropagation(); }; // Устанавливаем rotation только при изменении originalRouteData и отсутствии взаимодействия пользователя useEffect(() => { const newRotation = originalRouteData?.rotate ?? 0; // Обновляем rotation только если: // 1. Пользователь не взаимодействует с канвасом // 2. Значение действительно изменилось if (!isUserInteracting && lastOriginalRotation.current !== newRotation) { setRotation((newRotation * Math.PI) / 180); lastOriginalRotation.current = newRotation; } }, [originalRouteData?.rotate, isUserInteracting, setRotation]); const handlePointerMove = (e: FederatedMouseEvent) => { if (!isPointerDown) return; // Проверяем, началось ли перетаскивание if (!isDragging) { const dx = e.globalX - startMousePosition.x; const dy = e.globalY - startMousePosition.y; if (Math.abs(dx) > 5 || Math.abs(dy) > 5) { setIsDragging(true); } else { return; } } if (e.shiftKey) { const center = screenCenter ?? { x: 0, y: 0 }; const startAngle = Math.atan2( startMousePosition.y - center.y, startMousePosition.x - center.x ); const currentAngle = Math.atan2( e.globalY - center.y, e.globalX - center.x ); // Calculate rotation difference in radians const rotationDiff = currentAngle - startAngle; // Update rotation setRotation(startRotation + rotationDiff); const cosDelta = Math.cos(rotationDiff); const sinDelta = Math.sin(rotationDiff); setPosition({ x: center.x * (1 - cosDelta) + startPosition.x * cosDelta + (center.y - startPosition.y) * sinDelta, y: center.y * (1 - cosDelta) + startPosition.y * cosDelta + (startPosition.x - center.x) * sinDelta, }); } else { setRotation(startRotation); setPosition({ x: startPosition.x - startMousePosition.x + e.globalX, y: startPosition.y - startMousePosition.y + e.globalY, }); } e.stopPropagation(); }; const handlePointerUp = (e: FederatedMouseEvent) => { // Если не было перетаскивания, то это простой клик - закрываем виджет if (!isDragging) { setSelectedSight(undefined); } setIsPointerDown(false); setIsDragging(false); // Сбрасываем флаг взаимодействия через небольшую задержку // чтобы избежать немедленного срабатывания useEffect setTimeout(() => { setIsUserInteracting(false); }, 100); e.stopPropagation(); }; const handleWheel = (e: FederatedWheelEvent) => { e.stopPropagation(); setIsUserInteracting(true); // Устанавливаем флаг при зуме // Get mouse position relative to canvas const mouseX = e.globalX - position.x; const mouseY = e.globalY - position.y; // Calculate new scale const scaleMin = (routeData?.scale_min ?? 10) / SCALE_FACTOR; const scaleMax = (routeData?.scale_max ?? 20) / SCALE_FACTOR; const zoomFactor = e.deltaY > 0 ? 0.9 : 1.1; // Zoom out/in const newScale = Math.max(scaleMin, Math.min(scaleMax, scale * zoomFactor)); const actualZoomFactor = newScale / scale; if (scale === newScale) { // Сбрасываем флаг, если зум не изменился setTimeout(() => { setIsUserInteracting(false); }, 100); return; } // Update position to zoom towards mouse cursor setPosition({ x: position.x + mouseX * (1 - actualZoomFactor), y: position.y + mouseY * (1 - actualZoomFactor), }); setScale(newScale); // Сбрасываем флаг взаимодействия через задержку setTimeout(() => { setIsUserInteracting(false); }, 100); }; useEffect(() => { applicationRef?.app.render(); }, [position, scale, rotation]); return ( {applicationRef?.app && ( { const canvas = applicationRef.app.canvas; g.clear(); g.rect(0, 0, canvas?.width ?? 0, canvas?.height ?? 0); g.fill("#111"); }} eventMode={"static"} interactive onPointerDown={handlePointerDown} onGlobalPointerMove={handlePointerMove} onPointerUp={handlePointerUp} onPointerUpOutside={handlePointerUp} onWheel={handleWheel} /> )} {children} {/* Show center of the screen. { g.clear(); const center = screenCenter ?? {x: 0, y: 0}; g.circle(center.x, center.y, 1); g.fill("#fff"); }} /> */} ); }