233 lines
6.3 KiB
TypeScript
233 lines
6.3 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);
|
|
|
|
const lastOriginalRotation = useRef<number | undefined>(undefined);
|
|
|
|
useEffect(() => {
|
|
if (!applicationRef?.app?.canvas) return;
|
|
|
|
const canvas = applicationRef.app.canvas;
|
|
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, 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();
|
|
};
|
|
|
|
useEffect(() => {
|
|
const newRotation = originalRouteData?.rotate ?? 0;
|
|
|
|
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
|
|
);
|
|
|
|
const rotationDiff = currentAngle - startAngle;
|
|
|
|
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);
|
|
|
|
setTimeout(() => {
|
|
setIsUserInteracting(false);
|
|
}, 100);
|
|
e.stopPropagation();
|
|
};
|
|
|
|
const handleWheel = (e: FederatedWheelEvent) => {
|
|
e.stopPropagation();
|
|
setIsUserInteracting(true);
|
|
|
|
const mouseX = e.globalX - position.x;
|
|
const mouseY = e.globalY - position.y;
|
|
|
|
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;
|
|
const newScale = Math.max(scaleMin, Math.min(scaleMax, scale * zoomFactor));
|
|
const actualZoomFactor = newScale / scale;
|
|
|
|
if (scale === newScale) {
|
|
setTimeout(() => {
|
|
setIsUserInteracting(false);
|
|
}, 100);
|
|
return;
|
|
}
|
|
|
|
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>
|
|
);
|
|
}
|