Files
aerbim-ht-monitor/frontend/components/model/Canvas2DPlan.tsx

318 lines
9.6 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
'use client'
import React, { useEffect, useRef, useState } from 'react'
import { AbstractMesh, Vector3 } from '@babylonjs/core'
interface Canvas2DPlanProps {
meshes: AbstractMesh[]
sensorStatusMap: Record<string, string>
onClose: () => void
onSensorClick?: (sensorId: string) => void
}
interface Sensor2D {
id: string
x: number
y: number
status: string
}
const Canvas2DPlan: React.FC<Canvas2DPlanProps> = ({
meshes,
sensorStatusMap,
onClose,
onSensorClick,
}) => {
const canvasRef = useRef<HTMLCanvasElement>(null)
const [sensors, setSensors] = useState<Sensor2D[]>([])
const [hoveredSensor, setHoveredSensor] = useState<string | null>(null)
const [scale, setScale] = useState(10)
const [offset, setOffset] = useState({ x: 0, y: 0 })
const [isDragging, setIsDragging] = useState(false)
const [dragStart, setDragStart] = useState({ x: 0, y: 0 })
// Извлечение датчиков из mesh'ей
useEffect(() => {
const extractedSensors: Sensor2D[] = []
console.log('[Canvas2DPlan] Extracting sensors from meshes:', meshes.length)
console.log('[Canvas2DPlan] sensorStatusMap:', sensorStatusMap)
let meshesWithMetadata = 0
let meshesWithSensorID = 0
let meshesInStatusMap = 0
meshes.forEach((mesh, index) => {
if (mesh.metadata) {
meshesWithMetadata++
if (index < 3) {
console.log(`[Canvas2DPlan] Sample mesh[${index}] metadata:`, mesh.metadata)
}
}
const sensorId = mesh.metadata?.Sensor_ID
if (sensorId) {
meshesWithSensorID++
if (index < 3) {
console.log(`[Canvas2DPlan] Sample mesh[${index}] Sensor_ID:`, sensorId, 'in map?', !!sensorStatusMap[sensorId])
}
}
if (sensorId && sensorStatusMap[sensorId]) {
meshesInStatusMap++
const position = mesh.getAbsolutePosition()
extractedSensors.push({
id: sensorId,
x: position.x,
y: position.z, // Используем Z как Y для вида сверху
status: sensorStatusMap[sensorId],
})
}
})
console.log('[Canvas2DPlan] Meshes with metadata:', meshesWithMetadata)
console.log('[Canvas2DPlan] Meshes with Sensor_ID:', meshesWithSensorID)
console.log('[Canvas2DPlan] Meshes in statusMap:', meshesInStatusMap)
console.log('[Canvas2DPlan] Extracted sensors:', extractedSensors.length, extractedSensors)
setSensors(extractedSensors)
// Автоматическое центрирование
if (extractedSensors.length > 0 && canvasRef.current) {
const minX = Math.min(...extractedSensors.map((s) => s.x))
const maxX = Math.max(...extractedSensors.map((s) => s.x))
const minY = Math.min(...extractedSensors.map((s) => s.y))
const maxY = Math.max(...extractedSensors.map((s) => s.y))
const centerX = (minX + maxX) / 2
const centerY = (minY + maxY) / 2
const canvas = canvasRef.current
setOffset({
x: canvas.width / 2 - centerX * scale,
y: canvas.height / 2 - centerY * scale,
})
}
}, [meshes, sensorStatusMap, scale])
// Рендеринг canvas
useEffect(() => {
const canvas = canvasRef.current
if (!canvas) return
const ctx = canvas.getContext('2d')
if (!ctx) return
// Очистка
ctx.clearRect(0, 0, canvas.width, canvas.height)
// Фон
ctx.fillStyle = '#0e111a'
ctx.fillRect(0, 0, canvas.width, canvas.height)
// Сетка
ctx.strokeStyle = '#1a1d2e'
ctx.lineWidth = 1
const gridSize = 50
for (let x = 0; x < canvas.width; x += gridSize) {
ctx.beginPath()
ctx.moveTo(x, 0)
ctx.lineTo(x, canvas.height)
ctx.stroke()
}
for (let y = 0; y < canvas.height; y += gridSize) {
ctx.beginPath()
ctx.moveTo(0, y)
ctx.lineTo(canvas.width, y)
ctx.stroke()
}
// Рисуем датчики
sensors.forEach((sensor) => {
const x = sensor.x * scale + offset.x
const y = sensor.y * scale + offset.y
// Определяем цвет по статусу
let color = '#6b7280' // gray
if (sensor.status === 'critical') color = '#ef4444' // red
else if (sensor.status === 'warning') color = '#f59e0b' // amber
else if (sensor.status === 'normal') color = '#10b981' // green
// Внешний круг (подсветка при hover)
if (hoveredSensor === sensor.id) {
ctx.fillStyle = color + '40'
ctx.beginPath()
ctx.arc(x, y, 20, 0, Math.PI * 2)
ctx.fill()
}
// Основной круг датчика
ctx.fillStyle = color
ctx.beginPath()
ctx.arc(x, y, 8, 0, Math.PI * 2)
ctx.fill()
// Обводка
ctx.strokeStyle = '#ffffff'
ctx.lineWidth = 2
ctx.stroke()
// Подпись
ctx.fillStyle = '#ffffff'
ctx.font = '12px Inter, sans-serif'
ctx.textAlign = 'center'
ctx.fillText(sensor.id, x, y - 15)
})
// Легенда
const legendX = 20
const legendY = canvas.height - 80
ctx.fillStyle = '#161824cc'
ctx.fillRect(legendX - 10, legendY - 10, 180, 70)
const statuses = [
{ label: 'Критический', color: '#ef4444' },
{ label: 'Предупреждение', color: '#f59e0b' },
{ label: 'Нормальный', color: '#10b981' },
]
statuses.forEach((status, index) => {
const y = legendY + index * 20
ctx.fillStyle = status.color
ctx.beginPath()
ctx.arc(legendX, y, 6, 0, Math.PI * 2)
ctx.fill()
ctx.fillStyle = '#ffffff'
ctx.font = '12px Inter, sans-serif'
ctx.textAlign = 'left'
ctx.fillText(status.label, legendX + 15, y + 4)
})
}, [sensors, scale, offset, hoveredSensor])
// Обработка клика
const handleCanvasClick = (e: React.MouseEvent<HTMLCanvasElement>) => {
const canvas = canvasRef.current
if (!canvas) return
const rect = canvas.getBoundingClientRect()
const clickX = e.clientX - rect.left
const clickY = e.clientY - rect.top
// Проверяем клик по датчику
for (const sensor of sensors) {
const x = sensor.x * scale + offset.x
const y = sensor.y * scale + offset.y
const distance = Math.sqrt((clickX - x) ** 2 + (clickY - y) ** 2)
if (distance <= 10) {
onSensorClick?.(sensor.id)
return
}
}
}
// Обработка hover
const handleCanvasMove = (e: React.MouseEvent<HTMLCanvasElement>) => {
if (isDragging) {
const dx = e.clientX - dragStart.x
const dy = e.clientY - dragStart.y
setOffset((prev) => ({ x: prev.x + dx, y: prev.y + dy }))
setDragStart({ x: e.clientX, y: e.clientY })
return
}
const canvas = canvasRef.current
if (!canvas) return
const rect = canvas.getBoundingClientRect()
const mouseX = e.clientX - rect.left
const mouseY = e.clientY - rect.top
let foundSensor: string | null = null
for (const sensor of sensors) {
const x = sensor.x * scale + offset.x
const y = sensor.y * scale + offset.y
const distance = Math.sqrt((mouseX - x) ** 2 + (mouseY - y) ** 2)
if (distance <= 10) {
foundSensor = sensor.id
break
}
}
setHoveredSensor(foundSensor)
}
// Обработка zoom
const handleWheel = (e: React.WheelEvent<HTMLCanvasElement>) => {
e.preventDefault()
const delta = e.deltaY > 0 ? 0.9 : 1.1
setScale((prev) => Math.max(1, Math.min(50, prev * delta)))
}
// Обработка drag
const handleMouseDown = (e: React.MouseEvent<HTMLCanvasElement>) => {
setIsDragging(true)
setDragStart({ x: e.clientX, y: e.clientY })
}
const handleMouseUp = () => {
setIsDragging(false)
}
return (
<div className="fixed inset-0 z-[100] bg-black/80 flex items-center justify-center p-4">
<div className="relative bg-[#161824] rounded-lg shadow-2xl border border-white/10 p-6 w-full h-full max-w-[1400px] max-h-[900px] flex flex-col">
{/* Заголовок */}
<div className="flex items-center justify-between mb-4">
<h2 className="text-xl font-semibold text-white">2D План-схема</h2>
<button
onClick={onClose}
className="text-gray-400 hover:text-white transition-colors"
>
<svg
className="w-6 h-6"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M6 18L18 6M6 6l12 12"
/>
</svg>
</button>
</div>
{/* Canvas */}
<div className="flex-1 min-h-0">
<canvas
ref={canvasRef}
width={1200}
height={700}
onClick={handleCanvasClick}
onMouseMove={handleCanvasMove}
onMouseDown={handleMouseDown}
onMouseUp={handleMouseUp}
onMouseLeave={handleMouseUp}
onWheel={handleWheel}
className="border border-white/10 rounded cursor-move w-full h-full"
style={{ cursor: isDragging ? 'grabbing' : 'grab', maxHeight: '700px' }}
/>
</div>
{/* Подсказка */}
<div className="mt-4 text-sm text-gray-400 text-center">
<p>Колесико мыши - масштаб | Перетаскивание - перемещение | Клик по датчику - подробности</p>
</div>
</div>
</div>
)
}
export default Canvas2DPlan