249 lines
7.8 KiB
TypeScript
249 lines
7.8 KiB
TypeScript
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 ? <p>Whoopsie Daisy!</p> : 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<number | undefined>(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 (
|
||
<ErrorBoundary>
|
||
{applicationRef?.app && (
|
||
<pixiGraphics
|
||
draw={(g) => {
|
||
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}
|
||
/>
|
||
)}
|
||
<pixiContainer
|
||
x={position.x}
|
||
y={position.y}
|
||
scale={scale}
|
||
rotation={rotation}
|
||
>
|
||
{children}
|
||
</pixiContainer>
|
||
{/* Show center of the screen.
|
||
<pixiGraphics
|
||
eventMode="none"
|
||
draw={(g) => {
|
||
g.clear();
|
||
const center = screenCenter ?? {x: 0, y: 0};
|
||
g.circle(center.x, center.y, 1);
|
||
g.fill("#fff");
|
||
}}
|
||
/> */}
|
||
</ErrorBoundary>
|
||
);
|
||
}
|