import { useEffect, useRef, useState, type RefObject } from "react"; import { Stage, Layer, Image, Shape } from "react-konva"; import type { KonvaEventObject } from "konva/lib/Node"; import { useCreateVideoSnapshot } from "../../hooks/useGetvideoSnapshots"; import { useCameraFeedContext } from "../../../../app/context/CameraFeedContext"; import { useCameraFeedSocket } from "../../../../app/context/WebSocketContext"; import { ReadyState } from "react-use-websocket"; import { toast } from "sonner"; import type { CameraID } from "../../../../app/config/cameraConfig"; const BACKEND_WIDTH = 640; const BACKEND_HEIGHT = 360; const BACKEND_CELL_SIZE = 16; const rows = 22.5; const cols = 40; const gap = 0; const VideoFeedGridPainter = () => { const { state } = useCameraFeedContext(); const cameraFeedID = state.cameraFeedID; const paintedCells = state?.paintedCells?.[cameraFeedID]; const regions = state.regionsByCamera[cameraFeedID]; const selectedRegionIndex = state.selectedRegionIndex; const mode = state.modeByCamera[cameraFeedID]; const { latestBitmapRef, isloading } = useCreateVideoSnapshot(); const [stageSize, setStageSize] = useState({ width: BACKEND_WIDTH, height: BACKEND_HEIGHT }); const isDrawingRef = useRef(false); const [scale, setScale] = useState(1); const [position, setPosition] = useState({ x: 0, y: 0 }); // eslint-disable-next-line @typescript-eslint/no-explicit-any const stageRef = useRef(null); // eslint-disable-next-line @typescript-eslint/no-explicit-any const paintLayerRef = useRef(null); const currentScale = stageSize.width / BACKEND_WIDTH; const size = BACKEND_CELL_SIZE * currentScale; const cameraSocket = useCameraFeedSocket(); const getCurrentSocket = () => { switch (cameraFeedID) { case "A": return cameraSocket; case "B": return cameraSocket; case "C": return cameraSocket; } }; const handleZoomClick = (e: KonvaEventObject, cameraFeedID: CameraID) => { if (mode !== "zoom") return; const socket = getCurrentSocket(); const stage = e.target.getStage(); const coords = stage?.getPointerPosition(); if (!coords || !socket) return; const newX = coords.x / stageSize.width; const newY = coords.y / stageSize.height; // Check if WebSocket is connected if (socket.readyState !== ReadyState.OPEN) { toast.error(`Camera ${cameraFeedID} WebSocket is not connected`); return; } try { socket.send(`ZOOM=${newX.toFixed(2)},${newY.toFixed(2)}`); } catch (error) { console.error("WebSocket send error:", error); toast.error(`Failed to send command to Camera ${cameraFeedID}`); } }; const draw = (bmp: RefObject): ImageBitmap | null => { if (!bmp || !bmp.current) { return null; } const image = bmp.current; return image; }; const image = draw(latestBitmapRef); const paintCell = (x: number, y: number) => { if (mode === "controller" || mode === "zoom" || mode === "magnify") return; const col = Math.floor(x / (size + gap)); const row = Math.floor(y / (size + gap)); if (row < 0 || row >= rows || col < 0 || col >= cols) return; const activeRegion = regions[selectedRegionIndex]; if (!activeRegion) return; const key = `${row}-${col}`; const currentColour = regions[selectedRegionIndex].brushColour; const map = paintedCells; const existing = map.get(key); if (mode === "eraser") { if (map.has(key)) { map.delete(key); paintLayerRef.current?.batchDraw(); } return; } if (existing && existing.colour === currentColour) return; map.set(key, { colour: currentColour, region: activeRegion }); paintLayerRef.current?.batchDraw(); }; const handleStageMouseDown = (e: KonvaEventObject) => { if (!regions[selectedRegionIndex] || mode === "magnify" || mode === "zoom") return; isDrawingRef.current = true; const pos = e.target.getStage()?.getPointerPosition(); if (pos) paintCell(pos.x, pos.y); }; const handleStageMouseMove = (e: KonvaEventObject) => { if (!isDrawingRef.current || mode === "magnify") return; if (!regions[selectedRegionIndex]) return; const pos = e.target.getStage()?.getPointerPosition(); if (pos) paintCell(pos.x, pos.y); }; const handleStageMouseUp = () => { isDrawingRef.current = false; }; const handleMouseEnter = () => { if (mode !== "magnify") return; setScale(2); }; const handleMouseLeave = () => { document.body.style.cursor = "default"; setScale(1); setPosition({ x: 0, y: 0 }); }; const handleMouseMove = (e: KonvaEventObject) => { if (scale === 1) return; const stage = e.target.getStage(); if (!stage) return; const pointerPosition = stage.getPointerPosition(); if (!pointerPosition) return; const newX = stageSize.width / 2 - pointerPosition.x * scale; const newY = stageSize.height / 2 - pointerPosition.y * scale; const maxX = 0; const minX = stageSize.width - stageSize.width * scale; const maxY = 0; const minY = stageSize.height - stageSize.height * scale; setPosition({ x: Math.max(minX, Math.min(maxX, newX)), y: Math.max(minY, Math.min(maxY, newY)), }); }; useEffect(() => { const handleResize = () => { const width = window.innerWidth; const aspectRatio = BACKEND_WIDTH / BACKEND_HEIGHT; if (width < 768) { const newWidth = width * 0.8; const newHeight = newWidth / aspectRatio; setStageSize({ width: newWidth, height: newHeight }); } else { const newWidth = width * 0.6; const newHeight = newWidth / aspectRatio; setStageSize({ width: newWidth, height: newHeight }); } }; handleResize(); window.addEventListener("resize", handleResize); return () => window.removeEventListener("resize", handleResize); }, []); if (image === null || isloading) return Loading Video feed…; return (
handleZoomClick(e, cameraFeedID)} cornerRadius={10} /> {mode === "painter" || mode === "eraser" ? ( { const cells = paintedCells; if (!cells || cells.size === 0 || !paintLayerRef.current) return; cells?.forEach((cell, key) => { const [rowStr, colStr] = key.split("-"); const row = Number(rowStr); const col = Number(colStr); const x = col * (size + gap); const y = row * (size + gap); ctx.beginPath(); ctx.rect(x, y, size, size); ctx.fillStyle = cell.colour; ctx.fill(); }); ctx.fillStrokeShape(shape); }} width={stageSize.width} height={stageSize.height} /> ) : null}
); }; export default VideoFeedGridPainter;