|
|
@ -16,8 +16,8 @@ const CameraView = ({ |
|
|
|
const [scale, setScale] = useState(1.0); |
|
|
|
const [translateX, setTranslateX] = useState(0); |
|
|
|
const [translateY, setTranslateY] = useState(0); |
|
|
|
const [videoNaturalWidth, setVideoNaturalWidth] = useState(1920); |
|
|
|
const [videoNaturalHeight, setVideoNaturalHeight] = useState(1080); |
|
|
|
const [videoNaturalWidth, setVideoNaturalWidth] = useState(0); |
|
|
|
const [videoNaturalHeight, setVideoNaturalHeight] = useState(0); |
|
|
|
const [mousePos, setMousePos] = useState({ x: 0, y: 0 }); |
|
|
|
|
|
|
|
// 交互状态 |
|
|
@ -37,6 +37,7 @@ const CameraView = ({ |
|
|
|
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(); |
|
|
@ -76,10 +77,7 @@ const CameraView = ({ |
|
|
|
canvas.style.width = rect.width + "px"; |
|
|
|
canvas.style.height = rect.height + "px"; |
|
|
|
|
|
|
|
console.log("resizeCanvas: 画布大小已调整", { |
|
|
|
width: rect.width, |
|
|
|
height: rect.height, |
|
|
|
}); |
|
|
|
// console.log("resizeCanvas: 画布大小已调整", { width: rect.width, height: rect.height }); |
|
|
|
|
|
|
|
// 延迟调用applyTransform,确保画布已经完全设置好 |
|
|
|
setTimeout(() => { |
|
|
@ -90,24 +88,35 @@ const CameraView = ({ |
|
|
|
|
|
|
|
// 图片加载完成 |
|
|
|
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}` |
|
|
|
); |
|
|
|
// console.log(`handleImageLoad: 视频原始分辨率: ${img.naturalWidth}x${img.naturalHeight}`); |
|
|
|
} |
|
|
|
resizeCanvas(); |
|
|
|
// 视频加载完成后,确保重新绘制矩形框 |
|
|
|
setTimeout(() => { |
|
|
|
console.log("handleImageLoad: 延迟重绘矩形框"); |
|
|
|
// 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; |
|
|
@ -229,7 +238,7 @@ const CameraView = ({ |
|
|
|
(target) => target.id === clickedRectangle.id |
|
|
|
); |
|
|
|
if (targetData) { |
|
|
|
console.log("点击矩形框,弹出标靶详情:", targetData); |
|
|
|
// console.log("点击矩形框,弹出标靶详情:", targetData); |
|
|
|
onRectangleClick(targetData); |
|
|
|
} |
|
|
|
} |
|
|
@ -277,7 +286,8 @@ const CameraView = ({ |
|
|
|
const templateParams = selectedTemplate |
|
|
|
? { |
|
|
|
// 从模板中获取参数 |
|
|
|
name: selectedTemplate.name || `target${rectangles.length + 1}`, |
|
|
|
name: |
|
|
|
selectedTemplate.name || `target${rectangles.length + 1}`, |
|
|
|
radius: selectedTemplate.physicalRadius || 40.0, |
|
|
|
isReferencePoint: selectedTemplate.isBaseline || false, |
|
|
|
gradientThreshold: |
|
|
@ -317,13 +327,13 @@ const CameraView = ({ |
|
|
|
|
|
|
|
setRectangles((prev) => [...prev, newRect]); |
|
|
|
|
|
|
|
console.log("新建矩形:", newRect); |
|
|
|
// console.log("新建矩形:", newRect); |
|
|
|
if (selectedTemplate) { |
|
|
|
console.log("使用模板参数:", selectedTemplate); |
|
|
|
// console.log("使用模板参数:", selectedTemplate); |
|
|
|
} else { |
|
|
|
console.log("使用默认参数"); |
|
|
|
// console.log("使用默认参数"); |
|
|
|
} |
|
|
|
logRectangleData(); |
|
|
|
// logRectangleData(); |
|
|
|
} |
|
|
|
}; // 拖拽已存在的矩形 |
|
|
|
const dragExistingRectangle = (mouseX, mouseY) => { |
|
|
@ -547,11 +557,15 @@ const CameraView = ({ |
|
|
|
ctx.clearRect(0, 0, canvas.width, canvas.height); |
|
|
|
|
|
|
|
// 如果视频没有加载完成,直接返回 |
|
|
|
if (!videoNaturalWidth || !videoNaturalHeight) { |
|
|
|
console.log("redrawAllRectangles: 视频尺寸未准备好", { |
|
|
|
videoNaturalWidth, |
|
|
|
videoNaturalHeight, |
|
|
|
}); |
|
|
|
if ( |
|
|
|
!videoNaturalWidth || |
|
|
|
!videoNaturalHeight || |
|
|
|
videoNaturalWidth < 100 || |
|
|
|
videoNaturalHeight < 100 |
|
|
|
) { |
|
|
|
// 清空画布,防止残影 |
|
|
|
const ctx = canvas.getContext("2d"); |
|
|
|
ctx.clearRect(0, 0, canvas.width, canvas.height); |
|
|
|
return; |
|
|
|
} |
|
|
|
|
|
|
@ -703,15 +717,15 @@ const CameraView = ({ |
|
|
|
const handleSave = useCallback(async () => { |
|
|
|
// 防止重复保存 |
|
|
|
if (isSaving) { |
|
|
|
console.log("正在保存中,忽略重复请求"); |
|
|
|
// console.log("正在保存中,忽略重复请求"); |
|
|
|
return; |
|
|
|
} |
|
|
|
|
|
|
|
console.log("=== 保存矩形信息 ==="); |
|
|
|
console.log("矩形总数:", rectangles.length); |
|
|
|
// console.log("=== 保存矩形信息 ==="); |
|
|
|
// console.log("矩形总数:", rectangles.length); |
|
|
|
|
|
|
|
if (rectangles.length === 0) { |
|
|
|
console.log("没有创建任何矩形"); |
|
|
|
// console.log("没有创建任何矩形"); |
|
|
|
return; |
|
|
|
} |
|
|
|
|
|
|
@ -720,7 +734,7 @@ const CameraView = ({ |
|
|
|
try { |
|
|
|
// 组装指定格式的数据 |
|
|
|
const targets = {}; |
|
|
|
console.log(rectangles, "当前矩形数据"); |
|
|
|
// console.log(rectangles, "当前矩形数据"); |
|
|
|
|
|
|
|
rectangles.forEach((rect, index) => { |
|
|
|
targets[index.toString()] = { |
|
|
@ -758,7 +772,7 @@ const CameraView = ({ |
|
|
|
}, |
|
|
|
}; |
|
|
|
|
|
|
|
console.log("格式化数据:"); |
|
|
|
// console.log("格式化数据:"); |
|
|
|
|
|
|
|
// 手动序列化以保持浮点数格式 |
|
|
|
const jsonString = JSON.stringify(outputData, null, 2).replace( |
|
|
@ -770,7 +784,7 @@ const CameraView = ({ |
|
|
|
const success = sendMessage(jsonString); |
|
|
|
|
|
|
|
if (success) { |
|
|
|
console.log("数据已发送到服务器"); |
|
|
|
// console.log("数据已发送到服务器"); |
|
|
|
|
|
|
|
// 保存成功后刷新标靶列表数据 |
|
|
|
setTimeout(() => { |
|
|
@ -795,7 +809,14 @@ const CameraView = ({ |
|
|
|
setIsSaving(false); |
|
|
|
}, 1000); |
|
|
|
} |
|
|
|
}, [rectangles, isConnected, sendMessage, refreshTargets, onClearSelection, isSaving]); |
|
|
|
}, [ |
|
|
|
rectangles, |
|
|
|
isConnected, |
|
|
|
sendMessage, |
|
|
|
refreshTargets, |
|
|
|
onClearSelection, |
|
|
|
isSaving, |
|
|
|
]); |
|
|
|
|
|
|
|
// 全局滚轮事件处理,防止alt+滚轮触发页面滚动 |
|
|
|
useEffect(() => { |
|
|
@ -832,14 +853,14 @@ const CameraView = ({ |
|
|
|
// 监听视频尺寸变化,确保重新绘制 |
|
|
|
useEffect(() => { |
|
|
|
if (videoNaturalWidth && videoNaturalHeight) { |
|
|
|
console.log("视频尺寸已更新,重新绘制矩形框"); |
|
|
|
// console.log("视频尺寸已更新,重新绘制矩形框"); |
|
|
|
redrawAllRectangles(); |
|
|
|
} |
|
|
|
}, [videoNaturalWidth, videoNaturalHeight]); |
|
|
|
|
|
|
|
// 监听矩形列表变化,自动重绘 |
|
|
|
useEffect(() => { |
|
|
|
console.log("矩形列表发生变化,重新绘制:", rectangles); |
|
|
|
// console.log("矩形列表发生变化,重新绘制:", rectangles); |
|
|
|
// 使用requestAnimationFrame来优化重绘性能 |
|
|
|
const rafId = requestAnimationFrame(() => { |
|
|
|
redrawAllRectangles(); |
|
|
@ -848,27 +869,27 @@ const CameraView = ({ |
|
|
|
return () => cancelAnimationFrame(rafId); |
|
|
|
}, [rectangles]); |
|
|
|
|
|
|
|
// 强制刷新矩形框的函数 |
|
|
|
const forceRefreshRectangles = useCallback(() => { |
|
|
|
console.log("强制刷新矩形框"); |
|
|
|
redrawAllRectangles(); |
|
|
|
}, []); |
|
|
|
|
|
|
|
// 从标靶数据初始化矩形框 |
|
|
|
useEffect(() => { |
|
|
|
console.log("targets 或 loading 状态变化:", { |
|
|
|
targets, |
|
|
|
loading, |
|
|
|
targetsLength: targets?.length, |
|
|
|
}); |
|
|
|
// console.log("targets 或 loading 状态变化:", { |
|
|
|
// targets, |
|
|
|
// loading, |
|
|
|
// targetsLength: targets?.length, |
|
|
|
// }); |
|
|
|
|
|
|
|
if (!loading && targets && targets.length > 0) { |
|
|
|
console.log("从标靶数据初始化矩形框:", targets); |
|
|
|
if ( |
|
|
|
!loading && |
|
|
|
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); |
|
|
|
// console.log("处理标靶:", target.id, "矩形区域:", rectangleArea); |
|
|
|
if ( |
|
|
|
rectangleArea && |
|
|
|
rectangleArea.x !== undefined && |
|
|
@ -896,17 +917,17 @@ const CameraView = ({ |
|
|
|
}) |
|
|
|
.filter((rect) => rect !== null); // 过滤掉空值 |
|
|
|
|
|
|
|
console.log("生成的矩形框数据:", initialRectangles); |
|
|
|
console.log("当前矩形框数据:", rectangles); |
|
|
|
console.log( |
|
|
|
"标靶数据详情:", |
|
|
|
targets.map((t) => ({ id: t.id, rectangleArea: t.rectangleArea })) |
|
|
|
); |
|
|
|
// console.log("生成的矩形框数据:", initialRectangles); |
|
|
|
// console.log("当前矩形框数据:", rectangles); |
|
|
|
// console.log( |
|
|
|
// "标靶数据详情:", |
|
|
|
// targets.map((t) => ({ id: t.id, rectangleArea: t.rectangleArea })) |
|
|
|
// ); |
|
|
|
|
|
|
|
// 总是根据targets数据更新rectangles,确保数据同步 |
|
|
|
console.log("强制更新矩形框数据以确保与targets同步"); |
|
|
|
// console.log("强制更新矩形框数据以确保与targets同步"); |
|
|
|
setRectangles(initialRectangles); |
|
|
|
console.log("已从标靶数据强制更新矩形框:", initialRectangles); |
|
|
|
// console.log("已从标靶数据强制更新矩形框:", initialRectangles); |
|
|
|
|
|
|
|
// 矩形框更新后,延迟一下确保重绘 |
|
|
|
const timeoutId = setTimeout(() => { |
|
|
@ -915,18 +936,18 @@ const CameraView = ({ |
|
|
|
return () => clearTimeout(timeoutId); |
|
|
|
} else if (!loading && (!targets || targets.length === 0)) { |
|
|
|
// 如果没有标靶数据,清空矩形框 |
|
|
|
console.log( |
|
|
|
"标靶数据为空,准备清空矩形框,当前矩形框数量:", |
|
|
|
rectangles.length |
|
|
|
); |
|
|
|
// console.log( |
|
|
|
// "标靶数据为空,准备清空矩形框,当前矩形框数量:", |
|
|
|
// rectangles.length |
|
|
|
// ); |
|
|
|
setRectangles([]); |
|
|
|
console.log("标靶数据为空,已清空矩形框"); |
|
|
|
// console.log("标靶数据为空,已清空矩形框"); |
|
|
|
} |
|
|
|
}, [targets, loading]); // 只依赖targets和loading状态 |
|
|
|
}, [targets, loading, videoNaturalWidth, videoNaturalHeight]); // 只依赖targets和loading状态 |
|
|
|
|
|
|
|
// 专门监听rectangles变化并重绘 |
|
|
|
useEffect(() => { |
|
|
|
console.log("rectangles状态变化:", rectangles); |
|
|
|
// console.log("rectangles状态变化:", rectangles); |
|
|
|
// 矩形状态改变时立即重绘 |
|
|
|
if (videoNaturalWidth && videoNaturalHeight) { |
|
|
|
// 如果矩形数组为空,强制清除画布 |
|
|
@ -935,7 +956,7 @@ const CameraView = ({ |
|
|
|
if (canvas) { |
|
|
|
const ctx = canvas.getContext("2d"); |
|
|
|
ctx.clearRect(0, 0, canvas.width, canvas.height); |
|
|
|
console.log("强制清除画布,因为矩形数组为空"); |
|
|
|
// console.log("强制清除画布,因为矩形数组为空"); |
|
|
|
} |
|
|
|
} else { |
|
|
|
redrawAllRectangles(); |
|
|
@ -958,7 +979,7 @@ const CameraView = ({ |
|
|
|
|
|
|
|
// 监听选中标靶变化,重新绘制高亮效果 |
|
|
|
useEffect(() => { |
|
|
|
console.log("选中标靶ID变化:", selectedTargetId); |
|
|
|
// console.log("选中标靶ID变化:", selectedTargetId); |
|
|
|
// 只有在有矩形框的时候才重绘,使用requestAnimationFrame优化性能 |
|
|
|
if (rectangles.length > 0) { |
|
|
|
const rafId = requestAnimationFrame(() => { |
|
|
@ -1068,7 +1089,7 @@ const CameraView = ({ |
|
|
|
src={streamUrl} |
|
|
|
alt="MJPEG 视频流" |
|
|
|
onLoad={handleImageLoad} |
|
|
|
onError={() => console.error("视频流加载失败")} |
|
|
|
onError={handleImageError} |
|
|
|
style={{ |
|
|
|
position: "absolute", |
|
|
|
left: 0, |
|
|
@ -1080,6 +1101,27 @@ const CameraView = ({ |
|
|
|
pointerEvents: "none", |
|
|
|
}} |
|
|
|
/> |
|
|
|
{/* 视频流断开提示 */} |
|
|
|
{streamError && ( |
|
|
|
<div |
|
|
|
style={{ |
|
|
|
position: "absolute", |
|
|
|
left: 0, |
|
|
|
top: 0, |
|
|
|
width: "100%", |
|
|
|
height: "100%", |
|
|
|
background: "rgba(0,0,0,0.7)", |
|
|
|
color: "#fff", |
|
|
|
display: "flex", |
|
|
|
alignItems: "center", |
|
|
|
justifyContent: "center", |
|
|
|
fontSize: 24, |
|
|
|
zIndex: 100, |
|
|
|
}} |
|
|
|
> |
|
|
|
视频流断开,正在尝试重连... |
|
|
|
</div> |
|
|
|
)} |
|
|
|
</div> |
|
|
|
|
|
|
|
{/* 绘制画布 */} |
|
|
@ -1131,7 +1173,9 @@ const CameraView = ({ |
|
|
|
position: "absolute", |
|
|
|
top: "10px", |
|
|
|
right: "10px", |
|
|
|
backgroundColor: isSaving ? "rgba(128, 128, 128, 0.7)" : "rgba(0, 100, 0, 0.7)", |
|
|
|
backgroundColor: isSaving |
|
|
|
? "rgba(128, 128, 128, 0.7)" |
|
|
|
: "rgba(0, 100, 0, 0.7)", |
|
|
|
color: "white", |
|
|
|
border: "1px solid rgba(255, 255, 255, 0.3)", |
|
|
|
borderRadius: "4px", |
|
|
@ -1172,7 +1216,7 @@ const CameraView = ({ |
|
|
|
}} |
|
|
|
> |
|
|
|
<div> |
|
|
|
分辨率: {videoNaturalWidth} × {videoNaturalHeight} |
|
|
|
摄像头分辨率: {videoNaturalWidth} × {videoNaturalHeight} |
|
|
|
</div> |
|
|
|
<div>缩放: {Math.round(scale * 100)}%</div> |
|
|
|
<div> |
|
|
|