import React, { useRef, useEffect, useState, useCallback } from "react"; import { useWebSocket } from "../actions/websocket.jsx"; import { message } from "antd"; // import { useTargetStorage } from "../hooks/useTargetStorage.js"; const CameraView = ({ selectedTargetId, onClearSelection, onRectangleClick, selectedTemplate, targets = [], targetsLoading = false, onRefreshTargets, }) => { const imgRef = useRef(null); const canvasRef = useRef(null); const videoInnerRef = useRef(null); const containerRef = useRef(null); // 缩放和平移相关状态 const [scale, setScale] = useState(1.0); const [translateX, setTranslateX] = useState(0); const [translateY, setTranslateY] = useState(0); const [videoNaturalWidth, setVideoNaturalWidth] = useState(0); const [videoNaturalHeight, setVideoNaturalHeight] = useState(0); const [mousePos, setMousePos] = useState({ x: 0, y: 0 }); // 交互状态 const [drawing, setDrawing] = useState(false); const [dragging, setDragging] = useState(false); const [startPos, setStartPos] = useState({ x: 0, y: 0 }); const [lastMousePos, setLastMousePos] = useState({ x: 0, y: 0 }); const [mouseDownTime, setMouseDownTime] = useState(0); // 记录鼠标按下的时间 const [mouseDownPos, setMouseDownPos] = useState({ x: 0, y: 0 }); // 记录鼠标按下的位置 // 矩形列表管理 - 最多5个矩形 const [rectangles, setRectangles] = useState([]); const [selectedRectIndex, setSelectedRectIndex] = useState(-1); const [isDraggingRect, setIsDraggingRect] = useState(false); const [dragStartPos, setDragStartPos] = useState({ x: 0, y: 0 }); const [dragRectOffset, setDragRectOffset] = useState({ x: 0, y: 0 }); // 鼠标在矩形内的偏移 const [currentDrawingRect, setCurrentDrawingRect] = useState(null); const [hoveredRectIndex, setHoveredRectIndex] = useState(-1); const [isSaving, setIsSaving] = useState(false); // 保存状态管理 const [streamError, setStreamError] = useState(false); // 视频流断开状态 // 使用WebSocket连接 const { isConnected, sendMessage } = useWebSocket(); // 从 props 接收标靶数据,而不是调用 Hook // const { targets, loading, refreshTargets } = useTargetStorage(); const minScale = 0.2; const maxScale = 4.0; const scaleStep = 0.1; const maxRectangles = 10; let streamUrl = `http://${window.location.hostname}:2240/video_flow`; // 摄像头流地址 if (window.env && window.env.FS_FLAG === "localdev") { streamUrl = `http://10.8.30.179:2240/video_flow`; //开发用 } // 应用变换 const applyTransform = () => { if (videoInnerRef.current) { videoInnerRef.current.style.transform = `translate(${translateX}px, ${translateY}px) scale(${scale})`; videoInnerRef.current.style.transformOrigin = "0 0"; } // 重新绘制所有矩形 redrawAllRectangles(); }; // 调整画布大小 const resizeCanvas = () => { if (canvasRef.current && containerRef.current) { const container = containerRef.current; const rect = container.getBoundingClientRect(); const canvas = canvasRef.current; canvas.width = rect.width; canvas.height = rect.height; canvas.style.width = rect.width + "px"; canvas.style.height = rect.height + "px"; // console.log("resizeCanvas: 画布大小已调整", { width: rect.width, height: rect.height }); // 延迟调用applyTransform,确保画布已经完全设置好 setTimeout(() => { applyTransform(); }, 10); } }; // 图片加载完成 const handleImageLoad = () => { setStreamError(false); // 恢复正常 if (imgRef.current) { const img = imgRef.current; if (img.naturalWidth && img.naturalHeight) { setVideoNaturalWidth(img.naturalWidth); setVideoNaturalHeight(img.naturalHeight); // console.log(`handleImageLoad: 视频原始分辨率: ${img.naturalWidth}x${img.naturalHeight}`); } resizeCanvas(); // 视频加载完成后,确保重新绘制矩形框 setTimeout(() => { // console.log("handleImageLoad: 延迟重绘矩形框"); redrawAllRectangles(); }, 200); } }; // 图片加载失败 const handleImageError = () => { setStreamError(true); // 自动重连,3秒后尝试刷新图片src setTimeout(() => { setStreamError(false); if (imgRef.current) { imgRef.current.src = streamUrl + "?t=" + Date.now(); } }, 3000); }; // 鼠标按下 const handleMouseDown = (e) => { const container = containerRef.current; const rect = container.getBoundingClientRect(); const mouseX = e.clientX - rect.left; const mouseY = e.clientY - rect.top; // 记录鼠标按下的时间和位置 setMouseDownTime(Date.now()); setMouseDownPos({ x: mouseX, y: mouseY }); if (e.altKey && e.button === 0) { // Alt + 左键拖拽进行平移 setDragging(true); setLastMousePos({ x: mouseX, y: mouseY }); e.preventDefault(); } else if (!e.altKey && e.button === 0) { // 检查是否点击在已存在的矩形上 const rectIndex = findRectangleAtPoint(mouseX, mouseY); if (rectIndex !== -1) { // 点击在已存在的矩形上,开始拖拽 setSelectedRectIndex(rectIndex); setIsDraggingRect(true); setDragStartPos({ x: mouseX, y: mouseY }); // 计算鼠标在矩形内的相对位置 const rect = rectangles[rectIndex]; const rectScreenPos = videoToScreenCoordinates( rect.x, rect.y, rect.width, rect.height ); if (rectScreenPos) { setDragRectOffset({ x: mouseX - rectScreenPos.x, y: mouseY - rectScreenPos.y, }); } redrawAllRectangles(); } else { // 点击在空白区域,开始绘制新矩形(如果没有达到上限) if (rectangles.length < maxRectangles) { setSelectedRectIndex(-1); setStartPos({ x: mouseX, y: mouseY }); setDrawing(true); setCurrentDrawingRect(null); } else { message.warning(`已达到标靶数量上限(${maxRectangles}个)!`); } } e.preventDefault(); } }; // 鼠标移动 const handleMouseMove = (e) => { const container = containerRef.current; const rect = container.getBoundingClientRect(); const mouseX = e.clientX - rect.left; const mouseY = e.clientY - rect.top; // 更新鼠标位置显示 if (!drawing && !dragging && !isDraggingRect) { updateMousePosition(mouseX, mouseY); // 检测鼠标是否悬停在矩形上 const rectIndex = findRectangleAtPoint(mouseX, mouseY); setHoveredRectIndex(rectIndex); } if (dragging) { // 拖拽平移 const deltaX = mouseX - lastMousePos.x; const deltaY = mouseY - lastMousePos.y; setTranslateX((prev) => prev + deltaX); setTranslateY((prev) => prev + deltaY); setLastMousePos({ x: mouseX, y: mouseY }); } else if (drawing) { // 绘制新矩形预览 drawNewRectanglePreview(startPos.x, startPos.y, mouseX, mouseY); } else if (isDraggingRect && selectedRectIndex !== -1) { // 拖拽已存在的矩形 dragExistingRectangle(mouseX, mouseY); } }; // 鼠标松开 const handleMouseUp = (e) => { const currentTime = Date.now(); const timeDiff = currentTime - mouseDownTime; const container = containerRef.current; const rect = container.getBoundingClientRect(); const mouseX = e.clientX - rect.left; const mouseY = e.clientY - rect.top; // 计算鼠标移动距离 const mouseMoveDistance = Math.sqrt( Math.pow(mouseX - mouseDownPos.x, 2) + Math.pow(mouseY - mouseDownPos.y, 2) ); // 检查是否是单击操作:时间短且移动距离小 const isClick = timeDiff < 300 && mouseMoveDistance < 5; if (dragging) { setDragging(false); } else if (drawing) { setDrawing(false); // 完成新矩形的绘制 finishDrawingRectangle(startPos.x, startPos.y, mouseX, mouseY); setCurrentDrawingRect(null); } else if (isDraggingRect) { if (isClick && onRectangleClick) { // 找到被点击的矩形 const rectIndex = findRectangleAtPoint(mouseX, mouseY); if (rectIndex !== -1) { const clickedRectangle = rectangles[rectIndex]; // 通过矩形ID找到对应的标靶数据 const targetData = targets.find( (target) => target.id === clickedRectangle.id ); if (targetData) { // console.log("点击矩形框,弹出标靶详情:", targetData); onRectangleClick(targetData); } } } setIsDraggingRect(false); setSelectedRectIndex(-1); // 取消选中状态 // 只有在非单击情况下才清除选中高亮状态 if (onClearSelection && !isClick) { onClearSelection(); } } }; // 绘制新矩形预览 const drawNewRectanglePreview = (startX, startY, endX, endY) => { const x = Math.min(startX, endX); const y = Math.min(startY, endY); const w = Math.abs(endX - startX); const h = Math.abs(endY - startY); if (w < 5 || h < 5) return; // 太小的矩形不显示 // 转换为视频坐标系的矩形 const videoRect = screenToVideoCoordinates(x, y, w, h); if (videoRect) { setCurrentDrawingRect(videoRect); redrawAllRectangles(); } }; // 完成新矩形的绘制 const finishDrawingRectangle = (startX, startY, endX, endY) => { const x = Math.min(startX, endX); const y = Math.min(startY, endY); const w = Math.abs(endX - startX); const h = Math.abs(endY - startY); if (w < 10 || h < 10) return; // 太小的矩形不保存 const videoRect = screenToVideoCoordinates(x, y, w, h); if (videoRect && rectangles.length < maxRectangles) { const nextIndex = Number(rectangles[rectangles.length - 1]?.id) ? Number(rectangles[rectangles.length - 1]?.id) - 100 + 1 : 1; const fixedId = `10${nextIndex}`; // 根据选中的模板预设参数 const templateParams = selectedTemplate ? { // 从模板中获取参数 name: `T_${nextIndex}` || selectedTemplate.name, radius: selectedTemplate.physicalRadius || 40.0, isReferencePoint: selectedTemplate.isBaseline || false, gradientThreshold: selectedTemplate.gradientThresholdValue, anchorThreshold: selectedTemplate.anchorThresholdValue, gaussianBlurThreshold: selectedTemplate.gaussianBlur || 3, binaryThreshold: selectedTemplate.binaryThreshold || 120, hasAdvancedConfig: false, } : { // 默认参数 name: `T_${nextIndex}`, radius: 40.0, isReferencePoint: false, gradientThreshold: 100, anchorThreshold: 80, gaussianBlurThreshold: 3, binaryThreshold: 120, hasAdvancedConfig: false, }; const newRect = { id: fixedId, key: fixedId, ...videoRect, ...templateParams, // 保存矩形区域信息用于服务端 rectangleArea: { x: videoRect.x, y: videoRect.y, w: videoRect.width, h: videoRect.height, width: videoRect.width, height: videoRect.height, }, }; setRectangles((prev) => [...prev, newRect]); // console.log("新建矩形:", newRect); if (selectedTemplate) { // console.log("使用模板参数:", selectedTemplate); } else { // console.log("使用默认参数"); } // logRectangleData(); } }; // 拖拽已存在的矩形 const dragExistingRectangle = (mouseX, mouseY) => { if (selectedRectIndex === -1) return; // 计算矩形应该在的屏幕位置(考虑鼠标在矩形内的偏移) const targetScreenX = mouseX - dragRectOffset.x; const targetScreenY = mouseY - dragRectOffset.y; // 转换为视频坐标 const rect = rectangles[selectedRectIndex]; const videoCoords = screenToVideoCoordinates( targetScreenX, targetScreenY, 0, 0 ); if (videoCoords) { setRectangles((prev) => { const newRects = [...prev]; // 计算新位置,确保不超出视频边界 const newX = Math.max( 0, Math.min(videoNaturalWidth - rect.width, videoCoords.x) ); const newY = Math.max( 0, Math.min(videoNaturalHeight - rect.height, videoCoords.y) ); newRects[selectedRectIndex] = { ...rect, x: newX, y: newY, }; return newRects; }); } }; // 屏幕坐标转换为视频坐标 const screenToVideoCoordinates = (screenX, screenY, screenW, screenH) => { // 转换为视频内容坐标系(考虑当前的变换) const videoX = (screenX - translateX) / scale; const videoY = (screenY - translateY) / scale; const videoW = screenW / scale; const videoH = screenH / scale; // 计算视频显示区域 const canvas = canvasRef.current; if (!canvas) return null; const displayW = canvas.width; const displayH = canvas.height; const videoAR = videoNaturalWidth / videoNaturalHeight; const displayAR = displayW / displayH; let drawW, drawH, offsetX, offsetY; if (videoAR > displayAR) { drawW = displayW; drawH = displayW / videoAR; offsetX = 0; offsetY = (displayH - drawH) / 2; } else { drawH = displayH; drawW = displayH * videoAR; offsetX = (displayW - drawW) / 2; offsetY = 0; } // 转换为基于原始视频分辨率的绝对坐标 const videoScaleX = videoNaturalWidth / drawW; const videoScaleY = videoNaturalHeight / drawH; const absX = Math.round((videoX - offsetX) * videoScaleX); const absY = Math.round((videoY - offsetY) * videoScaleY); const absW = Math.round(videoW * videoScaleX); const absH = Math.round(videoH * videoScaleY); // 限制在视频范围内 const finalRect = { x: Math.max(0, Math.min(videoNaturalWidth - 1, absX)), y: Math.max(0, Math.min(videoNaturalHeight - 1, absY)), width: Math.max(1, Math.min(videoNaturalWidth - absX, absW)), height: Math.max(1, Math.min(videoNaturalHeight - absY, absH)), }; return finalRect; }; // 视频坐标转换为屏幕坐标 const videoToScreenCoordinates = (videoX, videoY, videoW, videoH) => { const canvas = canvasRef.current; if (!canvas) return null; const displayW = canvas.width; const displayH = canvas.height; const videoAR = videoNaturalWidth / videoNaturalHeight; const displayAR = displayW / displayH; let drawW, drawH, offsetX, offsetY; if (videoAR > displayAR) { drawW = displayW; drawH = displayW / videoAR; offsetX = 0; offsetY = (displayH - drawH) / 2; } else { drawH = displayH; drawW = displayH * videoAR; offsetX = (displayW - drawW) / 2; offsetY = 0; } // 将视频坐标转换为显示坐标 const displayScaleX = drawW / videoNaturalWidth; const displayScaleY = drawH / videoNaturalHeight; const displayX = offsetX + videoX * displayScaleX; const displayY = offsetY + videoY * displayScaleY; const displayWidth = videoW * displayScaleX; const displayHeight = videoH * displayScaleY; // 应用当前的缩放和平移变换 const screenX = displayX * scale + translateX; const screenY = displayY * scale + translateY; const screenW = displayWidth * scale; const screenH = displayHeight * scale; return { x: screenX, y: screenY, width: screenW, height: screenH, }; }; // 输出所有矩形数据到控制台 const logRectangleData = () => { console.log( "当前矩形列表:", rectangles.map((rect, index) => ({ index, id: rect.id, leftTop: { x: rect.x, y: rect.y }, rightBottom: { x: rect.x + rect.width, y: rect.y + rect.height }, width: rect.width, height: rect.height, })) ); }; // 检查点击是否在矩形内 const findRectangleAtPoint = (mouseX, mouseY) => { if (!videoNaturalWidth || !videoNaturalHeight) return -1; const canvas = canvasRef.current; if (!canvas) return -1; // 计算视频显示区域 const displayW = canvas.width; const displayH = canvas.height; const videoAR = videoNaturalWidth / videoNaturalHeight; const displayAR = displayW / displayH; let drawW, drawH, offsetX, offsetY; if (videoAR > displayAR) { drawW = displayW; drawH = displayW / videoAR; offsetX = 0; offsetY = (displayH - drawH) / 2; } else { drawH = displayH; drawW = displayH * videoAR; offsetX = (displayW - drawW) / 2; offsetY = 0; } // 从后往前检查(优先选择最上层的矩形) for (let i = rectangles.length - 1; i >= 0; i--) { const rect = rectangles[i]; // 将绝对坐标转换为显示坐标 const displayScaleX = drawW / videoNaturalWidth; const displayScaleY = drawH / videoNaturalHeight; const displayX = offsetX + rect.x * displayScaleX; const displayY = offsetY + rect.y * displayScaleY; const displayW_rect = rect.width * displayScaleX; const displayH_rect = rect.height * displayScaleY; // 转换鼠标坐标到视频显示坐标系 const videoMouseX = (mouseX - translateX) / scale; const videoMouseY = (mouseY - translateY) / scale; // 检查是否在矩形内 if ( videoMouseX >= displayX && videoMouseX <= displayX + displayW_rect && videoMouseY >= displayY && videoMouseY <= displayY + displayH_rect ) { return i; } } return -1; }; // 重新绘制所有矩形 const redrawAllRectangles = () => { const canvas = canvasRef.current; if (!canvas) { console.log("redrawAllRectangles: canvas不存在"); return; } const ctx = canvas.getContext("2d"); ctx.clearRect(0, 0, canvas.width, canvas.height); // 如果视频没有加载完成,直接返回 if ( !videoNaturalWidth || !videoNaturalHeight || videoNaturalWidth < 100 || videoNaturalHeight < 100 ) { // 清空画布,防止残影 const ctx = canvas.getContext("2d"); ctx.clearRect(0, 0, canvas.width, canvas.height); return; } // 如果没有矩形框数据且没有正在绘制的预览矩形,也记录一下 if ((!rectangles || rectangles.length === 0) && !currentDrawingRect) { // console.log("redrawAllRectangles: 没有矩形框数据且没有预览矩形"); return; } // 计算视频显示区域 const displayW = canvas.width; const displayH = canvas.height; const videoAR = videoNaturalWidth / videoNaturalHeight; const displayAR = displayW / displayH; let drawW, drawH, offsetX, offsetY; if (videoAR > displayAR) { drawW = displayW; drawH = displayW / videoAR; offsetX = 0; offsetY = (displayH - drawH) / 2; } else { drawH = displayH; drawW = displayH * videoAR; offsetX = (displayW - drawW) / 2; offsetY = 0; } ctx.save(); ctx.translate(translateX, translateY); ctx.scale(scale, scale); // 绘制所有已保存的矩形 if (rectangles && rectangles.length > 0) { rectangles.forEach((rect, index) => { // 将绝对坐标转换为显示坐标(在变换空间中) const displayScaleX = drawW / videoNaturalWidth; const displayScaleY = drawH / videoNaturalHeight; const displayX = offsetX + rect.x * displayScaleX; const displayY = offsetY + rect.y * displayScaleY; const displayW_rect = rect.width * displayScaleX; const displayH_rect = rect.height * displayScaleY; // 检查是否为选中的标靶 const isSelected = selectedTargetId && rect.id && rect.id.toString() === selectedTargetId.toString(); // 设置矩形样式 - 根据不同状态设置不同颜色和样式 if (index === selectedRectIndex && isDraggingRect) { ctx.strokeStyle = "lime"; // 拖拽时显示为绿色 ctx.lineWidth = 2 / scale; } else if (isSelected) { ctx.strokeStyle = "lime"; // 选中状态显示为lime绿色 ctx.lineWidth = 2 / scale; // 选中时线条更粗 ctx.setLineDash([]); // 实线 } else { ctx.strokeStyle = "blue"; // 正常状态为蓝色 ctx.lineWidth = 1 / scale; ctx.setLineDash([]); // 实线 } ctx.strokeRect(displayX, displayY, displayW_rect, displayH_rect); // 显示矩形序号 ctx.fillStyle = index === selectedRectIndex && isDraggingRect ? "lime" : isSelected ? "lime" : "blue"; ctx.font = `${12 / scale}px Arial`; // ctx.fillText(`#${index + 1}`, displayX + 5 / scale, displayY + 15 / scale); }); } // 绘制当前正在绘制的矩形预览 if (currentDrawingRect) { const displayScaleX = drawW / videoNaturalWidth; const displayScaleY = drawH / videoNaturalHeight; const displayX = offsetX + currentDrawingRect.x * displayScaleX; const displayY = offsetY + currentDrawingRect.y * displayScaleY; const displayW_rect = currentDrawingRect.width * displayScaleX; const displayH_rect = currentDrawingRect.height * displayScaleY; ctx.strokeStyle = "cyan"; ctx.lineWidth = 2 / scale; ctx.setLineDash([6, 4]); // 预览时使用虚线 ctx.strokeRect(displayX, displayY, displayW_rect, displayH_rect); } ctx.restore(); }; // 更新鼠标位置显示 const updateMousePosition = (mouseX, mouseY) => { // 计算鼠标在原始视频坐标系中的位置 const videoX = (mouseX - translateX) / scale; const videoY = (mouseY - translateY) / scale; // 转换为绝对像素坐标 const canvas = canvasRef.current; if (!canvas) return; const displayW = canvas.width; const displayH = canvas.height; const videoAR = videoNaturalWidth / videoNaturalHeight; const displayAR = displayW / displayH; let drawW, drawH, offsetX, offsetY; if (videoAR > displayAR) { drawW = displayW; drawH = displayW / videoAR; offsetX = 0; offsetY = (displayH - drawH) / 2; } else { drawH = displayH; drawW = displayH * videoAR; offsetX = (displayW - drawW) / 2; offsetY = 0; } const videoScaleX = videoNaturalWidth / drawW; const videoScaleY = videoNaturalHeight / drawH; const absMouseX = Math.round((videoX - offsetX) * videoScaleX); const absMouseY = Math.round((videoY - offsetY) * videoScaleY); // 检查是否在视频区域内 if ( absMouseX >= 0 && absMouseX < videoNaturalWidth && absMouseY >= 0 && absMouseY < videoNaturalHeight ) { setMousePos({ x: absMouseX, y: absMouseY }); } }; // 重置视图 const handleReset = () => { setScale(1.0); setTranslateX(0); setTranslateY(0); // 不清除选择框,保留 selectionRect 和 rectInfo }; // 保存矩形信息 const handleSave = useCallback(async () => { // 防止重复保存 if (isSaving) { // console.log("正在保存中,忽略重复请求"); return; } // console.log("=== 保存矩形信息 ==="); // console.log("矩形总数:", rectangles.length); if (rectangles.length === 0) { // console.log("没有创建任何矩形"); return; } setIsSaving(true); try { // 组装指定格式的数据 const targets = {}; // console.log(rectangles, "当前矩形数据"); rectangles.forEach((rect) => { // 使用标靶的原始ID,而不是数组索引 const targetKey = rect.id ? rect.id.toString() : Date.now().toString(); targets[targetKey] = { info: { rectangle_area: { x: rect.x, y: rect.y, w: rect.width, h: rect.height, }, threshold: { binary: rect.binaryThreshold || 120, gauss: rect.gaussianBlurThreshold || 1, gradient: rect.gradientThreshold, anchor: rect.anchorThreshold, }, radius: rect.radius || 40.0, id: rect.id || targetKey, // 使用原始ID desc: rect.name || `target_${targetKey}`, base: rect.isReferencePoint || false, }, perspective: [ [1.0, 0, 0], [0, 1.0, 0], [0, 0, 1.0], ], }; }); const outputData = { _from: "setup", cmd: "setPoints", values: { targets: targets, }, }; // console.log("格式化数据:"); // 手动序列化以保持浮点数格式 const jsonString = JSON.stringify(outputData, null, 2).replace( /"perspective":\s*\[\s*\[\s*1,\s*0,\s*0\s*\],\s*\[\s*0,\s*1,\s*0\s*\],\s*\[\s*0,\s*0,\s*1\s*\]\s*\]/g, '"perspective": [\n [1.0, 0, 0],\n [0, 1.0, 0],\n [0, 0, 1.0]\n ]' ); if (isConnected) { const success = sendMessage(jsonString); if (success) { // console.log("数据已发送到服务器"); // 保存成功后刷新标靶列表数据 setTimeout(() => { if (onRefreshTargets) { onRefreshTargets(); } }, 500); // 延迟500ms确保服务器处理完成 // 清除选中状态 if (onClearSelection) { onClearSelection(); } } else { console.error("发送数据失败"); } } else { console.error("WebSocket未连接,无法发送数据"); } } catch (error) { console.error("保存过程中出现错误:", error); } finally { // 延迟重置保存状态,确保不会立即被重复触发 setTimeout(() => { setIsSaving(false); }, 1000); } }, [ rectangles, isConnected, sendMessage, onRefreshTargets, onClearSelection, isSaving, ]); // 全局滚轮事件处理,防止alt+滚轮触发页面滚动 useEffect(() => { const handleGlobalWheel = (e) => { if (e.altKey) { const container = containerRef.current; // 检查事件目标是否在摄像头容器内 if (container && container.contains(e.target)) { // 如果在容器内,不拦截,让容器处理器处理 return; } // 只有在容器外才拦截 e.preventDefault(); e.stopPropagation(); e.stopImmediatePropagation(); return false; } }; // 在document上添加监听器 document.addEventListener("wheel", handleGlobalWheel, { passive: false }); return () => { document.removeEventListener("wheel", handleGlobalWheel); }; }, []); // 应用变换效果 useEffect(() => { applyTransform(); }, [scale, translateX, translateY]); // 监听视频尺寸变化,确保重新绘制 useEffect(() => { if (videoNaturalWidth && videoNaturalHeight) { // console.log("视频尺寸已更新,重新绘制矩形框"); redrawAllRectangles(); } }, [videoNaturalWidth, videoNaturalHeight]); // 监听矩形列表变化,自动重绘 useEffect(() => { // console.log("矩形列表发生变化,重新绘制:", rectangles); // 使用requestAnimationFrame来优化重绘性能 const rafId = requestAnimationFrame(() => { redrawAllRectangles(); }); return () => cancelAnimationFrame(rafId); }, [rectangles]); // 从标靶数据初始化矩形框 useEffect(() => { // console.log("targets 或 loading 状态变化:", { // targets, // targetsLoading, // targetsLength: targets?.length, // }); if ( !targetsLoading && targets && targets.length > 0 && videoNaturalWidth > 0 && videoNaturalHeight > 0 ) { // console.log("从标靶数据初始化矩形框:", targets); const initialRectangles = targets .map((target) => { const rectangleArea = target.rectangleArea; // console.log("处理标靶:", target.id, "矩形区域:", rectangleArea); if ( rectangleArea && rectangleArea.x !== undefined && rectangleArea.y !== undefined ) { return { id: target.id, key: target.key, x: rectangleArea.x, y: rectangleArea.y, width: rectangleArea.w || rectangleArea.width || 100, height: rectangleArea.h || rectangleArea.height || 100, // 保留所有标靶属性,确保拖拽后不会丢失 name: target.name, radius: target.radius, isReferencePoint: target.isReferencePoint, gradientThreshold: target.gradientThreshold, anchorThreshold: target.anchorThreshold, gaussianBlurThreshold: target.gaussianBlurThreshold, binaryThreshold: target.binaryThreshold, hasAdvancedConfig: target.hasAdvancedConfig, }; } return null; }) .filter((rect) => rect !== null); // 过滤掉空值 // console.log("生成的矩形框数据:", initialRectangles); // console.log("当前矩形框数据:", rectangles); // console.log( // "标靶数据详情:", // targets.map((t) => ({ id: t.id, rectangleArea: t.rectangleArea })) // ); // 总是根据targets数据更新rectangles,确保数据同步 // console.log("强制更新矩形框数据以确保与targets同步"); setRectangles(initialRectangles); // console.log("已从标靶数据强制更新矩形框:", initialRectangles); // 矩形框更新后,延迟一下确保重绘 const timeoutId = setTimeout(() => { redrawAllRectangles(); }, 100); return () => clearTimeout(timeoutId); } else if (!targetsLoading && (!targets || targets.length === 0)) { // 如果没有标靶数据,清空矩形框 // console.log( // "标靶数据为空,准备清空矩形框,当前矩形框数量:", // rectangles.length // ); setRectangles([]); // console.log("标靶数据为空,已清空矩形框"); } }, [targets, targetsLoading, videoNaturalWidth, videoNaturalHeight]); // 专门监听rectangles变化并重绘 useEffect(() => { // console.log("rectangles状态变化:", rectangles); // 矩形状态改变时立即重绘 if (videoNaturalWidth && videoNaturalHeight) { // 如果矩形数组为空,强制清除画布 if (rectangles.length === 0) { const canvas = canvasRef.current; if (canvas) { const ctx = canvas.getContext("2d"); ctx.clearRect(0, 0, canvas.width, canvas.height); // console.log("强制清除画布,因为矩形数组为空"); } } else { redrawAllRectangles(); } } }, [rectangles, videoNaturalWidth, videoNaturalHeight]); // 监听拖拽状态变化,自动重绘 useEffect(() => { redrawAllRectangles(); }, [isDraggingRect, selectedRectIndex]); // 确保在视频尺寸和矩形框数据都准备好时重绘 useEffect(() => { if (videoNaturalWidth && videoNaturalHeight && rectangles.length > 0) { // 立即尝试重绘一次 redrawAllRectangles(); } }, [videoNaturalWidth, videoNaturalHeight, rectangles.length]); // 监听选中标靶变化,重新绘制高亮效果 useEffect(() => { // console.log("选中标靶ID变化:", selectedTargetId); // 只有在有矩形框的时候才重绘,使用requestAnimationFrame优化性能 if (rectangles.length > 0) { const rafId = requestAnimationFrame(() => { redrawAllRectangles(); }); return () => cancelAnimationFrame(rafId); } }, [selectedTargetId]); // 容器滚轮事件处理 useEffect(() => { const container = containerRef.current; if (!container) return; const handleContainerWheel = (e) => { if (e.altKey) { e.preventDefault(); e.stopPropagation(); // 执行缩放逻辑 const rect = container.getBoundingClientRect(); const mouseX = e.clientX - rect.left; const mouseY = e.clientY - rect.top; const beforeX = (mouseX - translateX) / scale; const beforeY = (mouseY - translateY) / scale; const delta = e.deltaY > 0 ? -scaleStep : scaleStep; const newScale = Math.max( minScale, Math.min(maxScale, scale + delta) ); const newTranslateX = mouseX - beforeX * newScale; const newTranslateY = mouseY - beforeY * newScale; setScale(newScale); setTranslateX(newTranslateX); setTranslateY(newTranslateY); } }; // 使用原生addEventListener,更好的控制 container.addEventListener("wheel", handleContainerWheel, { passive: false, }); return () => { container.removeEventListener("wheel", handleContainerWheel); }; }, [scale, translateX, translateY, minScale, maxScale, scaleStep]); // 窗口大小变化时调整画布 useEffect(() => { const handleResize = () => { setTimeout(resizeCanvas, 100); }; window.addEventListener("resize", handleResize); return () => window.removeEventListener("resize", handleResize); }, []); return (
{/* 视频容器 */}
{/* 视频内容 */}
MJPEG 视频流 {/* 视频流断开提示 */} {streamError && (
视频流断开,正在尝试重连...
)}
{/* 绘制画布 */}
{/* 重置按钮 */} {/* 保存按钮 */} {/* 信息显示 */}
摄像头分辨率: {videoNaturalWidth} × {videoNaturalHeight}
缩放: {Math.round(scale * 100)}%
鼠标: ({mousePos.x}, {mousePos.y})
标靶数量: {rectangles.length}/{maxRectangles}
{/* 操作说明 */}
Alt + 滚轮: 缩放
Alt + 拖拽: 平移
左键拖拽空白: 创建矩形
左键拖拽矩形: 移动矩形
); }; export default CameraView;