Overhaul of the highlight system
This commit is contained in:
@@ -14,6 +14,7 @@ import Notifications from '../../../components/notifications/Notifications'
|
|||||||
import NotificationDetectorInfo from '../../../components/notifications/NotificationDetectorInfo'
|
import NotificationDetectorInfo from '../../../components/notifications/NotificationDetectorInfo'
|
||||||
import dynamic from 'next/dynamic'
|
import dynamic from 'next/dynamic'
|
||||||
import type { ModelViewerProps } from '../../../components/model/ModelViewer'
|
import type { ModelViewerProps } from '../../../components/model/ModelViewer'
|
||||||
|
import * as statusColors from '../../../lib/statusColors'
|
||||||
|
|
||||||
const ModelViewer = dynamic<ModelViewerProps>(() => import('../../../components/model/ModelViewer'), {
|
const ModelViewer = dynamic<ModelViewerProps>(() => import('../../../components/model/ModelViewer'), {
|
||||||
ssr: false,
|
ssr: false,
|
||||||
@@ -109,6 +110,15 @@ const NavigationPage: React.FC = () => {
|
|||||||
const [isModelReady, setIsModelReady] = useState(false)
|
const [isModelReady, setIsModelReady] = useState(false)
|
||||||
const [focusedSensorId, setFocusedSensorId] = useState<string | null>(null)
|
const [focusedSensorId, setFocusedSensorId] = useState<string | null>(null)
|
||||||
const [highlightAllSensors, setHighlightAllSensors] = useState(false)
|
const [highlightAllSensors, setHighlightAllSensors] = useState(false)
|
||||||
|
const sensorStatusMap = React.useMemo(() => {
|
||||||
|
const map: Record<string, string> = {}
|
||||||
|
Object.values(detectorsData.detectors).forEach(d => {
|
||||||
|
if (d.serial_number && d.status) {
|
||||||
|
map[String(d.serial_number).trim()] = d.status
|
||||||
|
}
|
||||||
|
})
|
||||||
|
return map
|
||||||
|
}, [detectorsData])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (selectedDetector === null && selectedAlert === null) {
|
if (selectedDetector === null && selectedAlert === null) {
|
||||||
@@ -353,13 +363,13 @@ const NavigationPage: React.FC = () => {
|
|||||||
const getStatusText = (status: string) => {
|
const getStatusText = (status: string) => {
|
||||||
const s = (status || '').toLowerCase()
|
const s = (status || '').toLowerCase()
|
||||||
switch (s) {
|
switch (s) {
|
||||||
case '#b3261e':
|
case statusColors.STATUS_COLOR_CRITICAL:
|
||||||
case 'critical':
|
case 'critical':
|
||||||
return 'Критический'
|
return 'Критический'
|
||||||
case '#fd7c22':
|
case statusColors.STATUS_COLOR_WARNING:
|
||||||
case 'warning':
|
case 'warning':
|
||||||
return 'Предупреждение'
|
return 'Предупреждение'
|
||||||
case '#00ff00':
|
case statusColors.STATUS_COLOR_NORMAL:
|
||||||
case 'normal':
|
case 'normal':
|
||||||
return 'Норма'
|
return 'Норма'
|
||||||
default:
|
default:
|
||||||
@@ -546,6 +556,7 @@ const NavigationPage: React.FC = () => {
|
|||||||
activeMenu={showSensors ? 'sensors' : showFloorNavigation ? 'floor' : showListOfDetectors ? 'detectors' : null}
|
activeMenu={showSensors ? 'sensors' : showFloorNavigation ? 'floor' : showListOfDetectors ? 'detectors' : null}
|
||||||
focusSensorId={focusedSensorId}
|
focusSensorId={focusedSensorId}
|
||||||
highlightAllSensors={highlightAllSensors}
|
highlightAllSensors={highlightAllSensors}
|
||||||
|
sensorStatusMap={sensorStatusMap}
|
||||||
isSensorSelectionEnabled={showSensors || showFloorNavigation || showListOfDetectors}
|
isSensorSelectionEnabled={showSensors || showFloorNavigation || showListOfDetectors}
|
||||||
onSensorPick={handleSensorSelection}
|
onSensorPick={handleSensorSelection}
|
||||||
renderOverlay={({ anchor }) => (
|
renderOverlay={({ anchor }) => (
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { NextResponse } from 'next/server'
|
import { NextResponse } from 'next/server'
|
||||||
import { getServerSession } from 'next-auth'
|
import { getServerSession } from 'next-auth'
|
||||||
import { authOptions } from '@/lib/auth'
|
import { authOptions } from '@/lib/auth'
|
||||||
|
import * as statusColors from '@/lib/statusColors'
|
||||||
|
|
||||||
export async function GET() {
|
export async function GET() {
|
||||||
try {
|
try {
|
||||||
@@ -50,15 +51,15 @@ export async function GET() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const statusToColor: Record<string, string> = {
|
const statusToColor: Record<string, string> = {
|
||||||
critical: '#b3261e',
|
critical: statusColors.STATUS_COLOR_CRITICAL,
|
||||||
warning: '#fd7c22',
|
warning: statusColors.STATUS_COLOR_WARNING,
|
||||||
normal: '#00ff00',
|
normal: statusColors.STATUS_COLOR_NORMAL,
|
||||||
}
|
}
|
||||||
|
|
||||||
const transformedDetectors: Record<string, any> = {}
|
const transformedDetectors: Record<string, any> = {}
|
||||||
const detectorsObj = detectorsPayload?.detectors ?? {}
|
const detectorsObj = detectorsPayload?.detectors ?? {}
|
||||||
for (const [key, sensor] of Object.entries<any>(detectorsObj)) {
|
for (const [key, sensor] of Object.entries<any>(detectorsObj)) {
|
||||||
const color = statusToColor[sensor.status] ?? '#00ff00'
|
const color = statusToColor[sensor.status] ?? statusColors.STATUS_COLOR_NORMAL
|
||||||
const objectId = titleToIdMap[sensor.object] || sensor.object
|
const objectId = titleToIdMap[sensor.object] || sensor.object
|
||||||
transformedDetectors[key] = {
|
transformedDetectors[key] = {
|
||||||
...sensor,
|
...sensor,
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import React, { useState, useMemo } from 'react'
|
import React, { useState, useMemo } from 'react'
|
||||||
|
import * as statusColors from '../../lib/statusColors'
|
||||||
|
|
||||||
interface AlertItem {
|
interface AlertItem {
|
||||||
id: number
|
id: number
|
||||||
@@ -31,10 +32,14 @@ const AlertsList: React.FC<AlertsListProps> = ({ alerts, onAcknowledgeToggle, in
|
|||||||
|
|
||||||
const getStatusColor = (type: string) => {
|
const getStatusColor = (type: string) => {
|
||||||
switch (type) {
|
switch (type) {
|
||||||
case 'critical': return '#b3261e'
|
case 'critical':
|
||||||
case 'warning': return '#fd7c22'
|
return statusColors.STATUS_COLOR_CRITICAL
|
||||||
case 'info': return '#00ff00'
|
case 'warning':
|
||||||
default: return '#666'
|
return statusColors.STATUS_COLOR_WARNING
|
||||||
|
case 'info':
|
||||||
|
return statusColors.STATUS_COLOR_NORMAL
|
||||||
|
default:
|
||||||
|
return statusColors.STATUS_COLOR_UNKNOWN
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -104,7 +109,14 @@ const AlertsList: React.FC<AlertsListProps> = ({ alerts, onAcknowledgeToggle, in
|
|||||||
<td className="py-4">
|
<td className="py-4">
|
||||||
<span
|
<span
|
||||||
className="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium text-white"
|
className="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium text-white"
|
||||||
style={{ backgroundColor: item.priority === 'high' ? '#b3261e' : item.priority === 'medium' ? '#fd7c22' : '#00ff00' }}
|
style={{
|
||||||
|
backgroundColor:
|
||||||
|
item.priority === 'high'
|
||||||
|
? statusColors.STATUS_COLOR_CRITICAL
|
||||||
|
: item.priority === 'medium'
|
||||||
|
? statusColors.STATUS_COLOR_WARNING
|
||||||
|
: statusColors.STATUS_COLOR_NORMAL,
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
{item.priority === 'high' ? 'Высокий' : item.priority === 'medium' ? 'Средний' : 'Низкий'}
|
{item.priority === 'high' ? 'Высокий' : item.priority === 'medium' ? 'Средний' : 'Низкий'}
|
||||||
</span>
|
</span>
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
'use client'
|
'use client'
|
||||||
|
|
||||||
import React, { useState, useEffect } from 'react'
|
import React, { useState, useEffect } from 'react'
|
||||||
|
import * as statusColors from '../../lib/statusColors'
|
||||||
|
|
||||||
interface Detector {
|
interface Detector {
|
||||||
detector_id: number
|
detector_id: number
|
||||||
@@ -150,12 +151,15 @@ const DetectorList: React.FC<DetectorListProps> = ({ objectId, selectedDetectors
|
|||||||
<td className="py-3">
|
<td className="py-3">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<div
|
<div
|
||||||
className={`w-3 h-3 rounded-full`}
|
className="w-3 h-3 rounded-full"
|
||||||
style={{ backgroundColor: detector.status }}
|
style={{ backgroundColor: detector.status }}
|
||||||
></div>
|
></div>
|
||||||
<span className="text-sm text-gray-300">
|
<span className="text-sm text-gray-300">
|
||||||
{detector.status === '#b3261e' ? 'Критическое' :
|
{detector.status === statusColors.STATUS_COLOR_CRITICAL
|
||||||
detector.status === '#fd7c22' ? 'Предупреждение' : 'Норма'}
|
? 'Критическое'
|
||||||
|
: detector.status === statusColors.STATUS_COLOR_WARNING
|
||||||
|
? 'Предупреждение'
|
||||||
|
: 'Норма'}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
@@ -187,15 +191,15 @@ const DetectorList: React.FC<DetectorListProps> = ({ objectId, selectedDetectors
|
|||||||
<div className="text-sm text-gray-400">Всего</div>
|
<div className="text-sm text-gray-400">Всего</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="bg-[#161824] p-4 rounded-lg">
|
<div className="bg-[#161824] p-4 rounded-lg">
|
||||||
<div className="text-2xl font-bold text-green-500">{filteredDetectors.filter(d => d.status === '#00ff00').length}</div>
|
<div className="text-2xl font-bold text-green-500">{filteredDetectors.filter(d => d.status === statusColors.STATUS_COLOR_NORMAL).length}</div>
|
||||||
<div className="text-sm text-gray-400">Норма</div>
|
<div className="text-sm text-gray-400">Норма</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="bg-[#161824] p-4 rounded-lg">
|
<div className="bg-[#161824] p-4 rounded-lg">
|
||||||
<div className="text-2xl font-bold text-orange-500">{filteredDetectors.filter(d => d.status === '#fd7c22').length}</div>
|
<div className="text-2xl font-bold text-orange-500">{filteredDetectors.filter(d => d.status === statusColors.STATUS_COLOR_WARNING).length}</div>
|
||||||
<div className="text-sm text-gray-400">Предупреждения</div>
|
<div className="text-sm text-gray-400">Предупреждения</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="bg-[#161824] p-4 rounded-lg">
|
<div className="bg-[#161824] p-4 rounded-lg">
|
||||||
<div className="text-2xl font-bold text-red-500">{filteredDetectors.filter(d => d.status === '#b3261e').length}</div>
|
<div className="text-2xl font-bold text-red-500">{filteredDetectors.filter(d => d.status === statusColors.STATUS_COLOR_CRITICAL).length}</div>
|
||||||
<div className="text-sm text-gray-400">Критические</div>
|
<div className="text-sm text-gray-400">Критические</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -13,8 +13,6 @@ import {
|
|||||||
AbstractMesh,
|
AbstractMesh,
|
||||||
Nullable,
|
Nullable,
|
||||||
HighlightLayer,
|
HighlightLayer,
|
||||||
Mesh,
|
|
||||||
InstancedMesh,
|
|
||||||
Animation,
|
Animation,
|
||||||
CubicEase,
|
CubicEase,
|
||||||
EasingFunction,
|
EasingFunction,
|
||||||
@@ -27,6 +25,16 @@ import '@babylonjs/loaders'
|
|||||||
|
|
||||||
import SceneToolbar from './SceneToolbar';
|
import SceneToolbar from './SceneToolbar';
|
||||||
import LoadingSpinner from '../ui/LoadingSpinner'
|
import LoadingSpinner from '../ui/LoadingSpinner'
|
||||||
|
import {
|
||||||
|
getSensorIdFromMesh,
|
||||||
|
collectSensorMeshes,
|
||||||
|
applyHighlightToMeshes,
|
||||||
|
statusToColor3,
|
||||||
|
} from './sensorHighlight'
|
||||||
|
import {
|
||||||
|
computeSensorOverlayCircles,
|
||||||
|
hexWithAlpha,
|
||||||
|
} from './sensorHighlightOverlay'
|
||||||
|
|
||||||
export interface ModelViewerProps {
|
export interface ModelViewerProps {
|
||||||
modelPath: string
|
modelPath: string
|
||||||
@@ -45,6 +53,7 @@ export interface ModelViewerProps {
|
|||||||
isSensorSelectionEnabled?: boolean
|
isSensorSelectionEnabled?: boolean
|
||||||
onSensorPick?: (sensorId: string | null) => void
|
onSensorPick?: (sensorId: string | null) => void
|
||||||
highlightAllSensors?: boolean
|
highlightAllSensors?: boolean
|
||||||
|
sensorStatusMap?: Record<string, string>
|
||||||
}
|
}
|
||||||
|
|
||||||
const ModelViewer: React.FC<ModelViewerProps> = ({
|
const ModelViewer: React.FC<ModelViewerProps> = ({
|
||||||
@@ -57,6 +66,7 @@ const ModelViewer: React.FC<ModelViewerProps> = ({
|
|||||||
isSensorSelectionEnabled,
|
isSensorSelectionEnabled,
|
||||||
onSensorPick,
|
onSensorPick,
|
||||||
highlightAllSensors,
|
highlightAllSensors,
|
||||||
|
sensorStatusMap,
|
||||||
}) => {
|
}) => {
|
||||||
const canvasRef = useRef<HTMLCanvasElement>(null)
|
const canvasRef = useRef<HTMLCanvasElement>(null)
|
||||||
const engineRef = useRef<Nullable<Engine>>(null)
|
const engineRef = useRef<Nullable<Engine>>(null)
|
||||||
@@ -74,6 +84,10 @@ const ModelViewer: React.FC<ModelViewerProps> = ({
|
|||||||
const [overlayData, setOverlayData] = useState<{ name?: string; sensorId?: string } | null>(null)
|
const [overlayData, setOverlayData] = useState<{ name?: string; sensorId?: string } | null>(null)
|
||||||
const [modelReady, setModelReady] = useState(false)
|
const [modelReady, setModelReady] = useState(false)
|
||||||
const [panActive, setPanActive] = useState(false);
|
const [panActive, setPanActive] = useState(false);
|
||||||
|
const [webglError, setWebglError] = useState<string | null>(null)
|
||||||
|
const [allSensorsOverlayCircles, setAllSensorsOverlayCircles] = useState<
|
||||||
|
{ sensorId: string; left: number; top: number; colorHex: string }[]
|
||||||
|
>([])
|
||||||
|
|
||||||
const handlePan = () => setPanActive(!panActive);
|
const handlePan = () => setPanActive(!panActive);
|
||||||
|
|
||||||
@@ -222,7 +236,41 @@ const ModelViewer: React.FC<ModelViewerProps> = ({
|
|||||||
if (!canvasRef.current || isInitializedRef.current) return
|
if (!canvasRef.current || isInitializedRef.current) return
|
||||||
|
|
||||||
const canvas = canvasRef.current
|
const canvas = canvasRef.current
|
||||||
const engine = new Engine(canvas, true, { stencil: true })
|
setWebglError(null)
|
||||||
|
|
||||||
|
let hasWebGL = false
|
||||||
|
try {
|
||||||
|
const testCanvas = document.createElement('canvas')
|
||||||
|
const gl =
|
||||||
|
testCanvas.getContext('webgl2') ||
|
||||||
|
testCanvas.getContext('webgl') ||
|
||||||
|
testCanvas.getContext('experimental-webgl')
|
||||||
|
hasWebGL = !!gl
|
||||||
|
} catch {
|
||||||
|
hasWebGL = false
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!hasWebGL) {
|
||||||
|
const message = 'WebGL не поддерживается в текущем окружении'
|
||||||
|
setWebglError(message)
|
||||||
|
onError?.(message)
|
||||||
|
setIsLoading(false)
|
||||||
|
setModelReady(false)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let engine: Engine
|
||||||
|
try {
|
||||||
|
engine = new Engine(canvas, true, { stencil: true })
|
||||||
|
} catch (error) {
|
||||||
|
const errorMessage = error instanceof Error ? error.message : String(error)
|
||||||
|
const message = `WebGL недоступен: ${errorMessage}`
|
||||||
|
setWebglError(message)
|
||||||
|
onError?.(message)
|
||||||
|
setIsLoading(false)
|
||||||
|
setModelReady(false)
|
||||||
|
return
|
||||||
|
}
|
||||||
engineRef.current = engine
|
engineRef.current = engine
|
||||||
|
|
||||||
engine.runRenderLoop(() => {
|
engine.runRenderLoop(() => {
|
||||||
@@ -260,7 +308,14 @@ const ModelViewer: React.FC<ModelViewerProps> = ({
|
|||||||
fillLight.intensity = 0.3
|
fillLight.intensity = 0.3
|
||||||
fillLight.diffuse = new Color3(0.8, 0.8, 1)
|
fillLight.diffuse = new Color3(0.8, 0.8, 1)
|
||||||
|
|
||||||
const hl = new HighlightLayer('highlight-layer', scene)
|
const hl = new HighlightLayer('highlight-layer', scene, {
|
||||||
|
mainTextureRatio: 1,
|
||||||
|
blurTextureSizeRatio: 1,
|
||||||
|
})
|
||||||
|
hl.innerGlow = false
|
||||||
|
hl.outerGlow = true
|
||||||
|
hl.blurHorizontalSize = 2
|
||||||
|
hl.blurVerticalSize = 2
|
||||||
highlightLayerRef.current = hl
|
highlightLayerRef.current = hl
|
||||||
|
|
||||||
const handleResize = () => {
|
const handleResize = () => {
|
||||||
@@ -285,7 +340,7 @@ const ModelViewer: React.FC<ModelViewerProps> = ({
|
|||||||
}
|
}
|
||||||
sceneRef.current = null
|
sceneRef.current = null
|
||||||
}
|
}
|
||||||
}, [])
|
}, [onError])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!isInitializedRef.current || isDisposedRef.current) {
|
if (!isInitializedRef.current || isDisposedRef.current) {
|
||||||
@@ -427,19 +482,30 @@ const ModelViewer: React.FC<ModelViewerProps> = ({
|
|||||||
}, [modelPath, onError, onModelLoaded])
|
}, [modelPath, onError, onModelLoaded])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
console.log('[ModelViewer] highlightAllSensors effect triggered:', { highlightAllSensors, modelReady, sceneReady: !!sceneRef.current })
|
|
||||||
if (!sceneRef.current || isDisposedRef.current || !modelReady) return
|
if (!sceneRef.current || isDisposedRef.current || !modelReady) return
|
||||||
|
|
||||||
// Если включено выделение всех сенсоров - выделяем их все
|
|
||||||
if (highlightAllSensors) {
|
if (highlightAllSensors) {
|
||||||
console.log('[ModelViewer] Calling highlightAllSensorMeshes()')
|
const allMeshes = importedMeshesRef.current || []
|
||||||
highlightAllSensorMeshes()
|
const sensorMeshes = collectSensorMeshes(allMeshes)
|
||||||
|
applyHighlightToMeshes(
|
||||||
|
highlightLayerRef.current,
|
||||||
|
highlightedMeshesRef,
|
||||||
|
sensorMeshes,
|
||||||
|
mesh => {
|
||||||
|
const sid = getSensorIdFromMesh(mesh)
|
||||||
|
const status = sid ? sensorStatusMap?.[sid] : undefined
|
||||||
|
return statusToColor3(status ?? null)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
chosenMeshRef.current = null
|
||||||
|
setOverlayPos(null)
|
||||||
|
setOverlayData(null)
|
||||||
|
setAllSensorsOverlayCircles([])
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const sensorId = (focusSensorId ?? '').trim()
|
const sensorId = (focusSensorId ?? '').trim()
|
||||||
if (!sensorId) {
|
if (!sensorId) {
|
||||||
console.log('[ModelViewer] Focus cleared (no Sensor_ID provided)')
|
|
||||||
for (const m of highlightedMeshesRef.current) { m.renderingGroupId = 0 }
|
for (const m of highlightedMeshesRef.current) { m.renderingGroupId = 0 }
|
||||||
highlightedMeshesRef.current = []
|
highlightedMeshesRef.current = []
|
||||||
highlightLayerRef.current?.removeAllMeshes()
|
highlightLayerRef.current?.removeAllMeshes()
|
||||||
@@ -452,7 +518,6 @@ const ModelViewer: React.FC<ModelViewerProps> = ({
|
|||||||
const allMeshes = importedMeshesRef.current || []
|
const allMeshes = importedMeshesRef.current || []
|
||||||
|
|
||||||
if (allMeshes.length === 0) {
|
if (allMeshes.length === 0) {
|
||||||
console.warn('[ModelViewer] No meshes available for sensor matching')
|
|
||||||
for (const m of highlightedMeshesRef.current) { m.renderingGroupId = 0 }
|
for (const m of highlightedMeshesRef.current) { m.renderingGroupId = 0 }
|
||||||
highlightedMeshesRef.current = []
|
highlightedMeshesRef.current = []
|
||||||
highlightLayerRef.current?.removeAllMeshes()
|
highlightLayerRef.current?.removeAllMeshes()
|
||||||
@@ -462,44 +527,8 @@ const ModelViewer: React.FC<ModelViewerProps> = ({
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const sensorMeshes = allMeshes.filter((m: any) => {
|
const sensorMeshes = collectSensorMeshes(allMeshes)
|
||||||
try {
|
const chosen = sensorMeshes.find(m => getSensorIdFromMesh(m) === sensorId)
|
||||||
return ((m.id ?? '').includes('IfcSensor') || (m.name ?? '').includes('IfcSensor'))
|
|
||||||
} catch (error) {
|
|
||||||
console.warn('[ModelViewer] Error filtering sensor mesh:', error)
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
const chosen = sensorMeshes.find((m: any) => {
|
|
||||||
try {
|
|
||||||
const meta: any = (m as any)?.metadata
|
|
||||||
const extras: any = meta?.gltf?.extras ?? meta?.extras ?? (m as any)?.extras
|
|
||||||
|
|
||||||
const sid = extras?.Sensor_ID ?? extras?.sensor_id ?? extras?.SERIAL_NUMBER ?? extras?.serial_number ?? extras?.detector_id
|
|
||||||
if (sid != null) {
|
|
||||||
return String(sid).trim() === sensorId
|
|
||||||
}
|
|
||||||
|
|
||||||
const monitoringSensorInstance = extras?.bonsaiPset_ARBM_PSet_MonitoringSensor_Instance
|
|
||||||
if (monitoringSensorInstance && typeof monitoringSensorInstance === 'string') {
|
|
||||||
try {
|
|
||||||
const parsedInstance = JSON.parse(monitoringSensorInstance)
|
|
||||||
const instanceSensorId = parsedInstance?.Sensor_ID
|
|
||||||
if (instanceSensorId != null) {
|
|
||||||
return String(instanceSensorId).trim() === sensorId
|
|
||||||
}
|
|
||||||
} catch (parseError) {
|
|
||||||
console.warn('[ModelViewer] Error parsing MonitoringSensor_Instance JSON:', parseError)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return false
|
|
||||||
} catch (error) {
|
|
||||||
console.warn('[ModelViewer] Error matching sensor mesh:', error)
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
console.log('[ModelViewer] Sensor focus', {
|
console.log('[ModelViewer] Sensor focus', {
|
||||||
requested: sensorId,
|
requested: sensorId,
|
||||||
@@ -533,37 +562,19 @@ const ModelViewer: React.FC<ModelViewerProps> = ({
|
|||||||
Animation.CreateAndStartAnimation('camTarget', camera, 'target', frameRate, totalFrames, camera.target.clone(), center.clone(), Animation.ANIMATIONLOOPMODE_CONSTANT, ease)
|
Animation.CreateAndStartAnimation('camTarget', camera, 'target', frameRate, totalFrames, camera.target.clone(), center.clone(), Animation.ANIMATIONLOOPMODE_CONSTANT, ease)
|
||||||
Animation.CreateAndStartAnimation('camRadius', camera, 'radius', frameRate, totalFrames, camera.radius, targetRadius, Animation.ANIMATIONLOOPMODE_CONSTANT, ease)
|
Animation.CreateAndStartAnimation('camRadius', camera, 'radius', frameRate, totalFrames, camera.radius, targetRadius, Animation.ANIMATIONLOOPMODE_CONSTANT, ease)
|
||||||
|
|
||||||
const hl = highlightLayerRef.current
|
applyHighlightToMeshes(
|
||||||
if (hl) {
|
highlightLayerRef.current,
|
||||||
// Переключаем группу рендеринга для предыдущего выделенного меша
|
highlightedMeshesRef,
|
||||||
for (const m of highlightedMeshesRef.current) { m.renderingGroupId = 0 }
|
[chosen],
|
||||||
highlightedMeshesRef.current = []
|
mesh => {
|
||||||
|
const sid = getSensorIdFromMesh(mesh)
|
||||||
hl.removeAllMeshes()
|
const status = sid ? sensorStatusMap?.[sid] : undefined
|
||||||
if (chosen instanceof Mesh) {
|
return statusToColor3(status ?? null)
|
||||||
chosen.renderingGroupId = 1
|
},
|
||||||
highlightedMeshesRef.current.push(chosen)
|
)
|
||||||
hl.addMesh(chosen, new Color3(1, 1, 0))
|
|
||||||
} else if (chosen instanceof InstancedMesh) {
|
|
||||||
// Сохраняем исходный меш для инстанса
|
|
||||||
chosen.sourceMesh.renderingGroupId = 1
|
|
||||||
highlightedMeshesRef.current.push(chosen.sourceMesh)
|
|
||||||
hl.addMesh(chosen.sourceMesh, new Color3(1, 1, 0))
|
|
||||||
} else {
|
|
||||||
const children = typeof (chosen as any)?.getChildMeshes === 'function' ? (chosen as any).getChildMeshes() : []
|
|
||||||
for (const cm of children) {
|
|
||||||
if (cm instanceof Mesh) {
|
|
||||||
cm.renderingGroupId = 1
|
|
||||||
highlightedMeshesRef.current.push(cm)
|
|
||||||
hl.addMesh(cm, new Color3(1, 1, 0))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
chosenMeshRef.current = chosen
|
chosenMeshRef.current = chosen
|
||||||
setOverlayData({ name: chosen.name, sensorId })
|
setOverlayData({ name: chosen.name, sensorId })
|
||||||
} catch (error) {
|
} catch {
|
||||||
console.error('[ModelViewer] Error focusing on sensor mesh:', error)
|
|
||||||
for (const m of highlightedMeshesRef.current) { m.renderingGroupId = 0 }
|
for (const m of highlightedMeshesRef.current) { m.renderingGroupId = 0 }
|
||||||
highlightedMeshesRef.current = []
|
highlightedMeshesRef.current = []
|
||||||
highlightLayerRef.current?.removeAllMeshes()
|
highlightLayerRef.current?.removeAllMeshes()
|
||||||
@@ -582,99 +593,6 @@ const ModelViewer: React.FC<ModelViewerProps> = ({
|
|||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [focusSensorId, modelReady, highlightAllSensors])
|
}, [focusSensorId, modelReady, highlightAllSensors])
|
||||||
|
|
||||||
// Помощь: извлечь Sensor_ID из метаданных меша (совпадающая логика с фокусом)
|
|
||||||
const getSensorIdFromMesh = React.useCallback((m: AbstractMesh | null): string | null => {
|
|
||||||
if (!m) return null
|
|
||||||
try {
|
|
||||||
const meta: any = (m as any)?.metadata
|
|
||||||
const extras: any = meta?.gltf?.extras ?? meta?.extras ?? (m as any)?.extras
|
|
||||||
const sid = extras?.Sensor_ID ?? extras?.sensor_id ?? extras?.SERIAL_NUMBER ?? extras?.serial_number ?? extras?.detector_id
|
|
||||||
if (sid != null) return String(sid).trim()
|
|
||||||
const monitoringSensorInstance = extras?.bonsaiPset_ARBM_PSet_MonitoringSensor_Instance
|
|
||||||
if (monitoringSensorInstance && typeof monitoringSensorInstance === 'string') {
|
|
||||||
try {
|
|
||||||
const parsedInstance = JSON.parse(monitoringSensorInstance)
|
|
||||||
const instanceSensorId = parsedInstance?.Sensor_ID
|
|
||||||
if (instanceSensorId != null) return String(instanceSensorId).trim()
|
|
||||||
} catch (parseError) {
|
|
||||||
console.warn('[ModelViewer] Error parsing MonitoringSensor_Instance JSON in pick:', parseError)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
}
|
|
||||||
return null
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
// Функция для выделения всех сенсоров на модели
|
|
||||||
const highlightAllSensorMeshes = React.useCallback(() => {
|
|
||||||
console.log('[ModelViewer] highlightAllSensorMeshes called')
|
|
||||||
const scene = sceneRef.current
|
|
||||||
if (!scene || !highlightLayerRef.current) {
|
|
||||||
console.log('[ModelViewer] Cannot highlight - scene or highlightLayer not ready:', {
|
|
||||||
sceneReady: !!scene,
|
|
||||||
highlightLayerReady: !!highlightLayerRef.current
|
|
||||||
})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const allMeshes = importedMeshesRef.current || []
|
|
||||||
|
|
||||||
// Use the same logic as getSensorIdFromMesh to identify sensor meshes
|
|
||||||
const sensorMeshes = allMeshes.filter((m: any) => {
|
|
||||||
try {
|
|
||||||
const sensorId = getSensorIdFromMesh(m)
|
|
||||||
if (sensorId) {
|
|
||||||
console.log(`[ModelViewer] Found sensor mesh: ${m.name} (id: ${m.id}, sensorId: ${sensorId})`)
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
} catch (error) {
|
|
||||||
console.warn('[ModelViewer] Error filtering sensor mesh:', error)
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
console.log(`[ModelViewer] Found ${sensorMeshes.length} sensor meshes out of ${allMeshes.length} total meshes`)
|
|
||||||
|
|
||||||
if (sensorMeshes.length === 0) {
|
|
||||||
console.log('[ModelViewer] No sensor meshes found to highlight')
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Clear previous highlights
|
|
||||||
for (const m of highlightedMeshesRef.current) { m.renderingGroupId = 0 }
|
|
||||||
highlightedMeshesRef.current = []
|
|
||||||
highlightLayerRef.current?.removeAllMeshes()
|
|
||||||
|
|
||||||
// Highlight all sensor meshes
|
|
||||||
sensorMeshes.forEach((mesh: any) => {
|
|
||||||
try {
|
|
||||||
if (mesh instanceof Mesh) {
|
|
||||||
mesh.renderingGroupId = 1
|
|
||||||
highlightedMeshesRef.current.push(mesh)
|
|
||||||
highlightLayerRef.current?.addMesh(mesh, new Color3(1, 1, 0))
|
|
||||||
} else if (mesh instanceof InstancedMesh) {
|
|
||||||
mesh.sourceMesh.renderingGroupId = 1
|
|
||||||
highlightedMeshesRef.current.push(mesh.sourceMesh)
|
|
||||||
highlightLayerRef.current?.addMesh(mesh.sourceMesh, new Color3(1, 1, 0))
|
|
||||||
} else {
|
|
||||||
const children = typeof mesh.getChildMeshes === 'function' ? mesh.getChildMeshes() : []
|
|
||||||
for (const cm of children) {
|
|
||||||
if (cm instanceof Mesh) {
|
|
||||||
cm.renderingGroupId = 1
|
|
||||||
highlightedMeshesRef.current.push(cm)
|
|
||||||
highlightLayerRef.current?.addMesh(cm, new Color3(1, 1, 0))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.warn('[ModelViewer] Error highlighting sensor mesh:', error)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
console.log(`[ModelViewer] Successfully highlighted ${highlightedMeshesRef.current.length} sensor meshes`)
|
|
||||||
}, [getSensorIdFromMesh])
|
|
||||||
|
|
||||||
// Включение выбора на основе взаимодействия с моделью только при готовности модели и включении выбора сенсоров
|
// Включение выбора на основе взаимодействия с моделью только при готовности модели и включении выбора сенсоров
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const scene = sceneRef.current
|
const scene = sceneRef.current
|
||||||
@@ -701,7 +619,7 @@ const ModelViewer: React.FC<ModelViewerProps> = ({
|
|||||||
return () => {
|
return () => {
|
||||||
scene.onPointerObservable.remove(pickObserver)
|
scene.onPointerObservable.remove(pickObserver)
|
||||||
}
|
}
|
||||||
}, [modelReady, isSensorSelectionEnabled, onSensorPick, getSensorIdFromMesh])
|
}, [modelReady, isSensorSelectionEnabled, onSensorPick])
|
||||||
|
|
||||||
// Расчет позиции оверлея
|
// Расчет позиции оверлея
|
||||||
const computeOverlayPosition = React.useCallback((mesh: AbstractMesh | null) => {
|
const computeOverlayPosition = React.useCallback((mesh: AbstractMesh | null) => {
|
||||||
@@ -733,6 +651,41 @@ const ModelViewer: React.FC<ModelViewerProps> = ({
|
|||||||
setOverlayPos(pos)
|
setOverlayPos(pos)
|
||||||
}, [overlayData, computeOverlayPosition])
|
}, [overlayData, computeOverlayPosition])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const scene = sceneRef.current
|
||||||
|
const engine = engineRef.current
|
||||||
|
if (!scene || !engine || !modelReady) {
|
||||||
|
setAllSensorsOverlayCircles([])
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (!highlightAllSensors || focusSensorId || !sensorStatusMap) {
|
||||||
|
setAllSensorsOverlayCircles([])
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const allMeshes = importedMeshesRef.current || []
|
||||||
|
const sensorMeshes = collectSensorMeshes(allMeshes)
|
||||||
|
if (sensorMeshes.length === 0) {
|
||||||
|
setAllSensorsOverlayCircles([])
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const engineTyped = engine as Engine
|
||||||
|
const updateCircles = () => {
|
||||||
|
const circles = computeSensorOverlayCircles({
|
||||||
|
scene,
|
||||||
|
engine: engineTyped,
|
||||||
|
meshes: sensorMeshes,
|
||||||
|
sensorStatusMap,
|
||||||
|
})
|
||||||
|
setAllSensorsOverlayCircles(circles)
|
||||||
|
}
|
||||||
|
updateCircles()
|
||||||
|
const observer = scene.onBeforeRenderObservable.add(updateCircles)
|
||||||
|
return () => {
|
||||||
|
scene.onBeforeRenderObservable.remove(observer)
|
||||||
|
setAllSensorsOverlayCircles([])
|
||||||
|
}
|
||||||
|
}, [highlightAllSensors, focusSensorId, modelReady, sensorStatusMap])
|
||||||
|
|
||||||
// Позиция оверлея при движущейся камере
|
// Позиция оверлея при движущейся камере
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!sceneRef.current || !chosenMeshRef.current || !overlayData) return
|
if (!sceneRef.current || !chosenMeshRef.current || !overlayData) return
|
||||||
@@ -767,10 +720,24 @@ const ModelViewer: React.FC<ModelViewerProps> = ({
|
|||||||
<canvas
|
<canvas
|
||||||
ref={canvasRef}
|
ref={canvasRef}
|
||||||
className={`w-full h-full outline-none block transition-opacity duration-500 ${
|
className={`w-full h-full outline-none block transition-opacity duration-500 ${
|
||||||
showModel ? 'opacity-100' : 'opacity-0'
|
showModel && !webglError ? 'opacity-100' : 'opacity-0'
|
||||||
}`}
|
}`}
|
||||||
/>
|
/>
|
||||||
{isLoading && (
|
{webglError ? (
|
||||||
|
<div className="absolute inset-0 bg-gray-900 flex items-center justify-center z-50">
|
||||||
|
<div className="text-center p-8 bg-[#161824] rounded-lg border border-gray-700 max-w-md shadow-xl">
|
||||||
|
<div className="text-red-400 text-lg font-semibold mb-2">
|
||||||
|
3D просмотр недоступен
|
||||||
|
</div>
|
||||||
|
<div className="text-gray-300 mb-4">
|
||||||
|
{webglError}
|
||||||
|
</div>
|
||||||
|
<div className="text-sm text-gray-400">
|
||||||
|
Включите аппаратное ускорение в браузере или откройте страницу в другом браузере/устройстве
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : isLoading ? (
|
||||||
<div className="absolute inset-0 bg-gray-900 flex items-center justify-center z-50">
|
<div className="absolute inset-0 bg-gray-900 flex items-center justify-center z-50">
|
||||||
<LoadingSpinner
|
<LoadingSpinner
|
||||||
progress={loadingProgress}
|
progress={loadingProgress}
|
||||||
@@ -778,8 +745,7 @@ const ModelViewer: React.FC<ModelViewerProps> = ({
|
|||||||
strokeWidth={8}
|
strokeWidth={8}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
) : !modelReady ? (
|
||||||
{!modelReady && !isLoading && (
|
|
||||||
<div className="absolute inset-0 bg-gray-900 flex items-center justify-center z-40">
|
<div className="absolute inset-0 bg-gray-900 flex items-center justify-center z-40">
|
||||||
<div className="text-center p-8 bg-[#161824] rounded-lg border border-gray-700 max-w-md">
|
<div className="text-center p-8 bg-[#161824] rounded-lg border border-gray-700 max-w-md">
|
||||||
<div className="text-gray-400 text-lg font-semibold mb-4">
|
<div className="text-gray-400 text-lg font-semibold mb-4">
|
||||||
@@ -790,7 +756,7 @@ const ModelViewer: React.FC<ModelViewerProps> = ({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
) : null}
|
||||||
<SceneToolbar
|
<SceneToolbar
|
||||||
onZoomIn={handleZoomIn}
|
onZoomIn={handleZoomIn}
|
||||||
onZoomOut={handleZoomOut}
|
onZoomOut={handleZoomOut}
|
||||||
@@ -801,6 +767,27 @@ const ModelViewer: React.FC<ModelViewerProps> = ({
|
|||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
{allSensorsOverlayCircles.map(circle => {
|
||||||
|
const size = 36
|
||||||
|
const radius = size / 2
|
||||||
|
const fill = hexWithAlpha(circle.colorHex, 0.2)
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={`${circle.sensorId}-${Math.round(circle.left)}-${Math.round(circle.top)}`}
|
||||||
|
style={{
|
||||||
|
position: 'absolute',
|
||||||
|
left: circle.left - radius,
|
||||||
|
top: circle.top - radius,
|
||||||
|
width: size,
|
||||||
|
height: size,
|
||||||
|
borderRadius: '9999px',
|
||||||
|
border: `2px solid ${circle.colorHex}`,
|
||||||
|
backgroundColor: fill,
|
||||||
|
pointerEvents: 'none',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
})}
|
||||||
{renderOverlay && overlayPos && overlayData
|
{renderOverlay && overlayPos && overlayData
|
||||||
? renderOverlay({ anchor: overlayPos, info: overlayData })
|
? renderOverlay({ anchor: overlayPos, info: overlayData })
|
||||||
: null
|
: null
|
||||||
|
|||||||
96
frontend/components/model/sensorHighlight.ts
Normal file
96
frontend/components/model/sensorHighlight.ts
Normal file
@@ -0,0 +1,96 @@
|
|||||||
|
import {
|
||||||
|
AbstractMesh,
|
||||||
|
HighlightLayer,
|
||||||
|
Mesh,
|
||||||
|
InstancedMesh,
|
||||||
|
Color3,
|
||||||
|
} from '@babylonjs/core'
|
||||||
|
import * as statusColors from '../../lib/statusColors'
|
||||||
|
|
||||||
|
export const SENSOR_HIGHLIGHT_COLOR = new Color3(1, 1, 0)
|
||||||
|
|
||||||
|
const CRITICAL_COLOR3 = Color3.FromHexString(statusColors.STATUS_COLOR_CRITICAL)
|
||||||
|
const WARNING_COLOR3 = Color3.FromHexString(statusColors.STATUS_COLOR_WARNING)
|
||||||
|
const NORMAL_COLOR3 = Color3.FromHexString(statusColors.STATUS_COLOR_NORMAL)
|
||||||
|
|
||||||
|
export const statusToColor3 = (status?: string | null): Color3 => {
|
||||||
|
if (!status) return SENSOR_HIGHLIGHT_COLOR
|
||||||
|
const lower = status.toLowerCase()
|
||||||
|
if (lower === 'critical') {
|
||||||
|
return CRITICAL_COLOR3
|
||||||
|
}
|
||||||
|
if (lower === 'warning') {
|
||||||
|
return WARNING_COLOR3
|
||||||
|
}
|
||||||
|
if (lower === 'info' || lower === 'normal' || lower === 'ok') {
|
||||||
|
return NORMAL_COLOR3
|
||||||
|
}
|
||||||
|
if (status === statusColors.STATUS_COLOR_CRITICAL) return CRITICAL_COLOR3
|
||||||
|
if (status === statusColors.STATUS_COLOR_WARNING) return WARNING_COLOR3
|
||||||
|
if (status === statusColors.STATUS_COLOR_NORMAL) return NORMAL_COLOR3
|
||||||
|
return SENSOR_HIGHLIGHT_COLOR
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getSensorIdFromMesh = (m: AbstractMesh | null): string | null => {
|
||||||
|
if (!m) return null
|
||||||
|
try {
|
||||||
|
const meta: any = (m as any)?.metadata
|
||||||
|
const extras: any = meta?.gltf?.extras ?? meta?.extras ?? (m as any)?.extras
|
||||||
|
const sid =
|
||||||
|
extras?.Sensor_ID ??
|
||||||
|
extras?.sensor_id ??
|
||||||
|
extras?.SERIAL_NUMBER ??
|
||||||
|
extras?.serial_number ??
|
||||||
|
extras?.detector_id
|
||||||
|
if (sid != null) return String(sid).trim()
|
||||||
|
const monitoringSensorInstance = extras?.bonsaiPset_ARBM_PSet_MonitoringSensor_Instance
|
||||||
|
if (monitoringSensorInstance && typeof monitoringSensorInstance === 'string') {
|
||||||
|
try {
|
||||||
|
const parsedInstance = JSON.parse(monitoringSensorInstance)
|
||||||
|
const instanceSensorId = parsedInstance?.Sensor_ID
|
||||||
|
if (instanceSensorId != null) return String(instanceSensorId).trim()
|
||||||
|
} catch {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
export const collectSensorMeshes = (meshes: AbstractMesh[]): AbstractMesh[] => {
|
||||||
|
const result: AbstractMesh[] = []
|
||||||
|
for (const m of meshes) {
|
||||||
|
const sid = getSensorIdFromMesh(m)
|
||||||
|
if (sid) result.push(m)
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
export const applyHighlightToMeshes = (
|
||||||
|
layer: HighlightLayer | null,
|
||||||
|
highlightedRef: { current: AbstractMesh[] },
|
||||||
|
meshesToHighlight: AbstractMesh[],
|
||||||
|
getColor?: (mesh: AbstractMesh) => Color3 | null,
|
||||||
|
) => {
|
||||||
|
if (!layer) return
|
||||||
|
for (const m of highlightedRef.current) {
|
||||||
|
m.renderingGroupId = 0
|
||||||
|
}
|
||||||
|
highlightedRef.current = []
|
||||||
|
layer.removeAllMeshes()
|
||||||
|
|
||||||
|
meshesToHighlight.forEach(mesh => {
|
||||||
|
const color = getColor ? getColor(mesh) ?? SENSOR_HIGHLIGHT_COLOR : SENSOR_HIGHLIGHT_COLOR
|
||||||
|
if (mesh instanceof Mesh) {
|
||||||
|
mesh.renderingGroupId = 1
|
||||||
|
highlightedRef.current.push(mesh)
|
||||||
|
layer.addMesh(mesh, color)
|
||||||
|
} else if (mesh instanceof InstancedMesh) {
|
||||||
|
mesh.sourceMesh.renderingGroupId = 1
|
||||||
|
highlightedRef.current.push(mesh.sourceMesh)
|
||||||
|
layer.addMesh(mesh.sourceMesh, color)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
72
frontend/components/model/sensorHighlightOverlay.ts
Normal file
72
frontend/components/model/sensorHighlightOverlay.ts
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
import { AbstractMesh, Matrix, Vector3, Scene, Engine } from '@babylonjs/core'
|
||||||
|
import * as statusColors from '../../lib/statusColors'
|
||||||
|
import { getSensorIdFromMesh } from './sensorHighlight'
|
||||||
|
|
||||||
|
export interface SensorOverlayCircle {
|
||||||
|
sensorId: string
|
||||||
|
left: number
|
||||||
|
top: number
|
||||||
|
colorHex: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const parseHexColor = (hex: string) => {
|
||||||
|
let clean = hex.trim().replace('#', '')
|
||||||
|
if (clean.length === 3) {
|
||||||
|
clean = clean
|
||||||
|
.split('')
|
||||||
|
.map(c => c + c)
|
||||||
|
.join('')
|
||||||
|
}
|
||||||
|
const num = parseInt(clean, 16)
|
||||||
|
const r = (num >> 16) & 255
|
||||||
|
const g = (num >> 8) & 255
|
||||||
|
const b = num & 255
|
||||||
|
return { r, g, b }
|
||||||
|
}
|
||||||
|
|
||||||
|
export const hexWithAlpha = (hex: string, alpha: number) => {
|
||||||
|
const { r, g, b } = parseHexColor(hex)
|
||||||
|
return `rgba(${r}, ${g}, ${b}, ${alpha})`
|
||||||
|
}
|
||||||
|
|
||||||
|
export const computeSensorOverlayCircles = (params: {
|
||||||
|
scene: Scene
|
||||||
|
engine: Engine
|
||||||
|
meshes: AbstractMesh[]
|
||||||
|
sensorStatusMap?: Record<string, string>
|
||||||
|
}): SensorOverlayCircle[] => {
|
||||||
|
const { scene, engine, meshes, sensorStatusMap } = params
|
||||||
|
const camera = scene.activeCamera
|
||||||
|
if (!camera) return []
|
||||||
|
const viewport = camera.viewport.toGlobal(engine.getRenderWidth(), engine.getRenderHeight())
|
||||||
|
const result: SensorOverlayCircle[] = []
|
||||||
|
|
||||||
|
for (const mesh of meshes) {
|
||||||
|
const sensorId = getSensorIdFromMesh(mesh)
|
||||||
|
if (!sensorId) continue
|
||||||
|
|
||||||
|
const bbox =
|
||||||
|
typeof mesh.getHierarchyBoundingVectors === 'function'
|
||||||
|
? mesh.getHierarchyBoundingVectors()
|
||||||
|
: {
|
||||||
|
min: mesh.getBoundingInfo().boundingBox.minimumWorld,
|
||||||
|
max: mesh.getBoundingInfo().boundingBox.maximumWorld,
|
||||||
|
}
|
||||||
|
const center = bbox.min.add(bbox.max).scale(0.5)
|
||||||
|
|
||||||
|
const projected = Vector3.Project(center, Matrix.Identity(), scene.getTransformMatrix(), viewport)
|
||||||
|
if (!projected) continue
|
||||||
|
|
||||||
|
const statusColor = sensorStatusMap?.[sensorId] || statusColors.STATUS_COLOR_NORMAL
|
||||||
|
|
||||||
|
result.push({
|
||||||
|
sensorId,
|
||||||
|
left: projected.x,
|
||||||
|
top: projected.y,
|
||||||
|
colorHex: statusColor,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
@@ -4,6 +4,7 @@ import React from 'react'
|
|||||||
import { useRouter } from 'next/navigation'
|
import { useRouter } from 'next/navigation'
|
||||||
import useNavigationStore from '@/app/store/navigationStore'
|
import useNavigationStore from '@/app/store/navigationStore'
|
||||||
import AreaChart from '../dashboard/AreaChart'
|
import AreaChart from '../dashboard/AreaChart'
|
||||||
|
import * as statusColors from '../../lib/statusColors'
|
||||||
|
|
||||||
interface AlertType {
|
interface AlertType {
|
||||||
id: number
|
id: number
|
||||||
@@ -56,17 +57,19 @@ const AlertMenu: React.FC<AlertMenuProps> = ({ alert, isOpen, onClose, getStatus
|
|||||||
}
|
}
|
||||||
|
|
||||||
const getStatusColorCircle = (status: string) => {
|
const getStatusColorCircle = (status: string) => {
|
||||||
// Use hex colors from Alerts submenu system
|
if (status === statusColors.STATUS_COLOR_CRITICAL) return 'bg-red-500'
|
||||||
if (status === '#b3261e') return 'bg-red-500'
|
if (status === statusColors.STATUS_COLOR_WARNING) return 'bg-orange-500'
|
||||||
if (status === '#fd7c22') return 'bg-orange-500'
|
if (status === statusColors.STATUS_COLOR_NORMAL) return 'bg-green-500'
|
||||||
if (status === '#00ff00') return 'bg-green-500'
|
|
||||||
|
|
||||||
// Fallback for text-based status
|
|
||||||
switch (status?.toLowerCase()) {
|
switch (status?.toLowerCase()) {
|
||||||
case 'critical': return 'bg-red-500'
|
case 'critical':
|
||||||
case 'warning': return 'bg-orange-500'
|
return 'bg-red-500'
|
||||||
case 'normal': return 'bg-green-500'
|
case 'warning':
|
||||||
default: return 'bg-gray-500'
|
return 'bg-orange-500'
|
||||||
|
case 'normal':
|
||||||
|
return 'bg-green-500'
|
||||||
|
default:
|
||||||
|
return 'bg-gray-500'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
'use client'
|
'use client'
|
||||||
|
|
||||||
import React, { useState } from 'react'
|
import React, { useState } from 'react'
|
||||||
|
import * as statusColors from '../../lib/statusColors'
|
||||||
|
|
||||||
interface DetectorsDataType {
|
interface DetectorsDataType {
|
||||||
detectors: Record<string, DetectorType>
|
detectors: Record<string, DetectorType>
|
||||||
@@ -81,19 +82,27 @@ const FloorNavigation: React.FC<FloorNavigationProps> = (props) => {
|
|||||||
|
|
||||||
const getStatusColor = (status: string) => {
|
const getStatusColor = (status: string) => {
|
||||||
switch (status) {
|
switch (status) {
|
||||||
case '#b3261e': return 'bg-red-500'
|
case statusColors.STATUS_COLOR_CRITICAL:
|
||||||
case '#fd7c22': return 'bg-orange-500'
|
return 'bg-red-500'
|
||||||
case '#00ff00': return 'bg-green-500'
|
case statusColors.STATUS_COLOR_WARNING:
|
||||||
default: return 'bg-gray-500'
|
return 'bg-orange-500'
|
||||||
|
case statusColors.STATUS_COLOR_NORMAL:
|
||||||
|
return 'bg-green-500'
|
||||||
|
default:
|
||||||
|
return 'bg-gray-500'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const getStatusText = (status: string) => {
|
const getStatusText = (status: string) => {
|
||||||
switch (status) {
|
switch (status) {
|
||||||
case '#b3261e': return 'Критический'
|
case statusColors.STATUS_COLOR_CRITICAL:
|
||||||
case '#fd7c22': return 'Предупреждение'
|
return 'Критический'
|
||||||
case '#00ff00': return 'Норма'
|
case statusColors.STATUS_COLOR_WARNING:
|
||||||
default: return 'Неизвестно'
|
return 'Предупреждение'
|
||||||
|
case statusColors.STATUS_COLOR_NORMAL:
|
||||||
|
return 'Норма'
|
||||||
|
default:
|
||||||
|
return 'Неизвестно'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
'use client'
|
'use client'
|
||||||
|
|
||||||
import React, { useState } from 'react'
|
import React, { useState } from 'react'
|
||||||
|
import * as statusColors from '../../lib/statusColors'
|
||||||
|
|
||||||
interface DetectorsDataType {
|
interface DetectorsDataType {
|
||||||
detectors: Record<string, DetectorType>
|
detectors: Record<string, DetectorType>
|
||||||
@@ -58,19 +59,27 @@ const ListOfDetectors: React.FC<ListOfDetectorsProps> = ({ objectId, detectorsDa
|
|||||||
|
|
||||||
const getStatusColor = (status: string) => {
|
const getStatusColor = (status: string) => {
|
||||||
switch (status) {
|
switch (status) {
|
||||||
case '#b3261e': return 'bg-red-500'
|
case statusColors.STATUS_COLOR_CRITICAL:
|
||||||
case '#fd7c22': return 'bg-orange-500'
|
return 'bg-red-500'
|
||||||
case '#00ff00': return 'bg-green-500'
|
case statusColors.STATUS_COLOR_WARNING:
|
||||||
default: return 'bg-gray-500'
|
return 'bg-orange-500'
|
||||||
|
case statusColors.STATUS_COLOR_NORMAL:
|
||||||
|
return 'bg-green-500'
|
||||||
|
default:
|
||||||
|
return 'bg-gray-500'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const getStatusText = (status: string) => {
|
const getStatusText = (status: string) => {
|
||||||
switch (status) {
|
switch (status) {
|
||||||
case '#b3261e': return 'Критический'
|
case statusColors.STATUS_COLOR_CRITICAL:
|
||||||
case '#fd7c22': return 'Предупреждение'
|
return 'Критический'
|
||||||
case '#00ff00': return 'Норма'
|
case statusColors.STATUS_COLOR_WARNING:
|
||||||
default: return 'Неизвестно'
|
return 'Предупреждение'
|
||||||
|
case statusColors.STATUS_COLOR_NORMAL:
|
||||||
|
return 'Норма'
|
||||||
|
default:
|
||||||
|
return 'Неизвестно'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
'use client'
|
'use client'
|
||||||
|
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
|
import * as statusColors from '../../lib/statusColors'
|
||||||
|
|
||||||
interface DetectorInfoType {
|
interface DetectorInfoType {
|
||||||
detector_id: number
|
detector_id: number
|
||||||
@@ -52,9 +53,9 @@ const NotificationDetectorInfo: React.FC<NotificationDetectorInfoProps> = ({ det
|
|||||||
}
|
}
|
||||||
|
|
||||||
const getStatusColor = (status: string) => {
|
const getStatusColor = (status: string) => {
|
||||||
if (status === '#b3261e') return 'text-red-400'
|
if (status === statusColors.STATUS_COLOR_CRITICAL) return 'text-red-400'
|
||||||
if (status === '#fd7c22') return 'text-orange-400'
|
if (status === statusColors.STATUS_COLOR_WARNING) return 'text-orange-400'
|
||||||
if (status === '#4caf50') return 'text-green-400'
|
if (status === statusColors.STATUS_COLOR_NORMAL || status === '#4caf50') return 'text-green-400'
|
||||||
return 'text-gray-400'
|
return 'text-gray-400'
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -147,9 +148,13 @@ const NotificationDetectorInfo: React.FC<NotificationDetectorInfoProps> = ({ det
|
|||||||
style={{ backgroundColor: detectorInfo.status }}
|
style={{ backgroundColor: detectorInfo.status }}
|
||||||
></div>
|
></div>
|
||||||
<span className={`text-sm font-medium ${getStatusColor(detectorInfo.status)}`}>
|
<span className={`text-sm font-medium ${getStatusColor(detectorInfo.status)}`}>
|
||||||
{detectorInfo.status === '#b3261e' ? 'Критический' :
|
{detectorInfo.status === statusColors.STATUS_COLOR_CRITICAL
|
||||||
detectorInfo.status === '#fd7c22' ? 'Предупреждение' :
|
? 'Критический'
|
||||||
detectorInfo.status === '#4caf50' ? 'Нормальный' : 'Неизвестно'}
|
: detectorInfo.status === statusColors.STATUS_COLOR_WARNING
|
||||||
|
? 'Предупреждение'
|
||||||
|
: detectorInfo.status === statusColors.STATUS_COLOR_NORMAL || detectorInfo.status === '#4caf50'
|
||||||
|
? 'Нормальный'
|
||||||
|
: 'Неизвестно'}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import React, { useState, useMemo } from 'react';
|
import React, { useState, useMemo } from 'react';
|
||||||
|
import * as statusColors from '../../lib/statusColors';
|
||||||
|
|
||||||
interface NotificationType {
|
interface NotificationType {
|
||||||
id: number;
|
id: number;
|
||||||
@@ -78,19 +79,27 @@ const ReportsList: React.FC<ReportsListProps> = ({ detectorsData, initialSearchT
|
|||||||
|
|
||||||
const getStatusColor = (type: string) => {
|
const getStatusColor = (type: string) => {
|
||||||
switch (type) {
|
switch (type) {
|
||||||
case 'critical': return '#b3261e';
|
case 'critical':
|
||||||
case 'warning': return '#fd7c22';
|
return statusColors.STATUS_COLOR_CRITICAL;
|
||||||
case 'info': return '#00ff00';
|
case 'warning':
|
||||||
default: return '#666';
|
return statusColors.STATUS_COLOR_WARNING;
|
||||||
|
case 'info':
|
||||||
|
return statusColors.STATUS_COLOR_NORMAL;
|
||||||
|
default:
|
||||||
|
return statusColors.STATUS_COLOR_UNKNOWN;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const getPriorityColor = (priority: string) => {
|
const getPriorityColor = (priority: string) => {
|
||||||
switch (priority) {
|
switch (priority) {
|
||||||
case 'high': return '#b3261e';
|
case 'high':
|
||||||
case 'medium': return '#fd7c22';
|
return statusColors.STATUS_COLOR_CRITICAL;
|
||||||
case 'low': return '#00ff00';
|
case 'medium':
|
||||||
default: return '#666';
|
return statusColors.STATUS_COLOR_WARNING;
|
||||||
|
case 'low':
|
||||||
|
return statusColors.STATUS_COLOR_NORMAL;
|
||||||
|
default:
|
||||||
|
return statusColors.STATUS_COLOR_UNKNOWN;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
4
frontend/lib/statusColors.ts
Normal file
4
frontend/lib/statusColors.ts
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
export const STATUS_COLOR_CRITICAL = '#b3261e'
|
||||||
|
export const STATUS_COLOR_WARNING = '#fd7c22'
|
||||||
|
export const STATUS_COLOR_NORMAL = '#00ff00'
|
||||||
|
export const STATUS_COLOR_UNKNOWN = '#666666'
|
||||||
Reference in New Issue
Block a user