318 lines
9.6 KiB
TypeScript
318 lines
9.6 KiB
TypeScript
'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
|