136 lines
		
	
	
		
			3.8 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
	
	
			
		
		
	
	
			136 lines
		
	
	
		
			3.8 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
	
	
| import { useEffect, useState } from "react";
 | |
| import { useTransform } from "./TransformContext";
 | |
| import { SightData } from "./types";
 | |
| import { Assets, FederatedMouseEvent, Texture } from "pixi.js";
 | |
| 
 | |
| import { SIGHT_SIZE, UP_SCALE } from "./Constants";
 | |
| import { coordinatesToLocal, localToCoordinates } from "./utils";
 | |
| import { useMapData } from "./MapDataContext";
 | |
| 
 | |
| interface SightProps {
 | |
|   sight: SightData;
 | |
|   id: number;
 | |
| }
 | |
| 
 | |
| export const Sight = ({ sight, id }: Readonly<SightProps>) => {
 | |
|   const { rotation, scale } = useTransform();
 | |
|   const { setSightCoordinates, setSelectedSight } = useMapData();
 | |
| 
 | |
|   const [position, setPosition] = useState(
 | |
|     coordinatesToLocal(sight.latitude, sight.longitude)
 | |
|   );
 | |
|   const [isDragging, setIsDragging] = useState(false);
 | |
|   const [isPointerDown, setIsPointerDown] = useState(false);
 | |
|   const [startPosition, setStartPosition] = useState({ x: 0, y: 0 });
 | |
|   const [startMousePosition, setStartMousePosition] = useState({ x: 0, y: 0 });
 | |
| 
 | |
|   const handlePointerDown = (e: FederatedMouseEvent) => {
 | |
|     setIsPointerDown(true);
 | |
|     setIsDragging(false);
 | |
|     setStartPosition({
 | |
|       x: position.x,
 | |
|       y: position.y,
 | |
|     });
 | |
|     setStartMousePosition({
 | |
|       x: e.globalX,
 | |
|       y: e.globalY,
 | |
|     });
 | |
| 
 | |
|     e.stopPropagation();
 | |
|   };
 | |
|   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) > 2 || Math.abs(dy) > 2) {
 | |
|         setIsDragging(true);
 | |
|       } else {
 | |
|         return;
 | |
|       }
 | |
|     }
 | |
| 
 | |
|     const dx = (e.globalX - startMousePosition.x) / scale / UP_SCALE;
 | |
|     const dy = (e.globalY - startMousePosition.y) / scale / UP_SCALE;
 | |
|     const cos = Math.cos(rotation);
 | |
|     const sin = Math.sin(rotation);
 | |
|     const newPosition = {
 | |
|       x: startPosition.x + dx * cos + dy * sin,
 | |
|       y: startPosition.y - dx * sin + dy * cos,
 | |
|     };
 | |
|     setPosition(newPosition);
 | |
|     const coordinates = localToCoordinates(newPosition.x, newPosition.y);
 | |
|     setSightCoordinates(sight.id, coordinates.latitude, coordinates.longitude);
 | |
|     e.stopPropagation();
 | |
|   };
 | |
| 
 | |
|   const handlePointerUp = (e: FederatedMouseEvent) => {
 | |
|     setIsPointerDown(false);
 | |
| 
 | |
|     // Если не было перетаскивания, то это клик
 | |
|     if (!isDragging) {
 | |
|       setSelectedSight(sight);
 | |
|     }
 | |
| 
 | |
|     setIsDragging(false);
 | |
|     e.stopPropagation();
 | |
|   };
 | |
| 
 | |
|   const [texture, setTexture] = useState(Texture.EMPTY);
 | |
|   useEffect(() => {
 | |
|     Assets.load("/sight_icon.svg").then(setTexture);
 | |
|   }, []);
 | |
| 
 | |
|   useEffect(() => {}, [id, sight.latitude, sight.longitude]);
 | |
| 
 | |
|   if (!sight) {
 | |
|     console.error("sight is null");
 | |
|     return null;
 | |
|   }
 | |
| 
 | |
|   // Компенсируем масштаб для сохранения постоянного размера
 | |
|   const compensatedSize = SIGHT_SIZE / scale;
 | |
|   const compensatedFontSize = 24 / scale;
 | |
| 
 | |
|   return (
 | |
|     <pixiContainer
 | |
|       rotation={-rotation}
 | |
|       eventMode="static"
 | |
|       interactive
 | |
|       onPointerDown={handlePointerDown}
 | |
|       onGlobalPointerMove={handlePointerMove}
 | |
|       onPointerUp={handlePointerUp}
 | |
|       onPointerUpOutside={handlePointerUp}
 | |
|       x={position.x * UP_SCALE - SIGHT_SIZE / 2}
 | |
|       y={position.y * UP_SCALE - SIGHT_SIZE / 2}
 | |
|     >
 | |
|       <pixiSprite
 | |
|         texture={texture}
 | |
|         width={compensatedSize}
 | |
|         height={compensatedSize}
 | |
|       />
 | |
|       <pixiGraphics
 | |
|         draw={(g) => {
 | |
|           g.clear();
 | |
|           g.circle(0, 0, 20 / scale);
 | |
|           g.fill({ color: "#000" });
 | |
|         }}
 | |
|         x={compensatedSize}
 | |
|         y={0}
 | |
|       />
 | |
|       <pixiText
 | |
|         text={`${id + 1}`}
 | |
|         x={compensatedSize + 1 / scale}
 | |
|         y={0}
 | |
|         anchor={0.5}
 | |
|         style={{
 | |
|           fontSize: compensatedFontSize,
 | |
|           fontWeight: "bold",
 | |
|           fill: "#ffffff",
 | |
|         }}
 | |
|       />
 | |
|     </pixiContainer>
 | |
|   );
 | |
| };
 |