Overhaul of the highlight system

This commit is contained in:
iv_vuytsik
2026-01-21 03:16:52 +03:00
parent ce7e39debf
commit 87a1a628d3
13 changed files with 481 additions and 259 deletions

View File

@@ -1,4 +1,5 @@
import React, { useState, useMemo } from 'react'
import * as statusColors from '../../lib/statusColors'
interface AlertItem {
id: number
@@ -31,10 +32,14 @@ const AlertsList: React.FC<AlertsListProps> = ({ alerts, onAcknowledgeToggle, in
const getStatusColor = (type: string) => {
switch (type) {
case 'critical': return '#b3261e'
case 'warning': return '#fd7c22'
case 'info': return '#00ff00'
default: return '#666'
case 'critical':
return statusColors.STATUS_COLOR_CRITICAL
case 'warning':
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">
<span
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' ? 'Средний' : 'Низкий'}
</span>
@@ -142,4 +154,4 @@ const AlertsList: React.FC<AlertsListProps> = ({ alerts, onAcknowledgeToggle, in
)
}
export default AlertsList
export default AlertsList

View File

@@ -1,7 +1,8 @@
'use client'
import React, { useState, useEffect } from 'react'
import * as statusColors from '../../lib/statusColors'
interface Detector {
detector_id: number
name: string
@@ -30,14 +31,14 @@ interface RawDetector {
priority: string
}>
}
interface DetectorListProps {
objectId?: string
selectedDetectors: number[]
onDetectorSelect: (detectorId: number, selected: boolean) => void
initialSearchTerm?: string
}
const DetectorList: React.FC<DetectorListProps> = ({ objectId, selectedDetectors, onDetectorSelect, initialSearchTerm = '' }) => {
const [detectors, setDetectors] = useState<Detector[]>([])
const [searchTerm, setSearchTerm] = useState<string>(initialSearchTerm)
@@ -150,12 +151,15 @@ const DetectorList: React.FC<DetectorListProps> = ({ objectId, selectedDetectors
<td className="py-3">
<div className="flex items-center gap-2">
<div
className={`w-3 h-3 rounded-full`}
className="w-3 h-3 rounded-full"
style={{ backgroundColor: detector.status }}
></div>
<span className="text-sm text-gray-300">
{detector.status === '#b3261e' ? 'Критическое' :
detector.status === '#fd7c22' ? 'Предупреждение' : 'Норма'}
{detector.status === statusColors.STATUS_COLOR_CRITICAL
? 'Критическое'
: detector.status === statusColors.STATUS_COLOR_WARNING
? 'Предупреждение'
: 'Норма'}
</span>
</div>
</td>
@@ -187,15 +191,15 @@ const DetectorList: React.FC<DetectorListProps> = ({ objectId, selectedDetectors
<div className="text-sm text-gray-400">Всего</div>
</div>
<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>
<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>
<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>
</div>
@@ -209,4 +213,4 @@ const DetectorList: React.FC<DetectorListProps> = ({ objectId, selectedDetectors
)
}
export default DetectorList
export default DetectorList

View File

@@ -12,9 +12,7 @@ import {
Color4,
AbstractMesh,
Nullable,
HighlightLayer,
Mesh,
InstancedMesh,
HighlightLayer,
Animation,
CubicEase,
EasingFunction,
@@ -24,9 +22,19 @@ import {
Matrix,
} from '@babylonjs/core'
import '@babylonjs/loaders'
import SceneToolbar from './SceneToolbar';
import LoadingSpinner from '../ui/LoadingSpinner'
import {
getSensorIdFromMesh,
collectSensorMeshes,
applyHighlightToMeshes,
statusToColor3,
} from './sensorHighlight'
import {
computeSensorOverlayCircles,
hexWithAlpha,
} from './sensorHighlightOverlay'
export interface ModelViewerProps {
modelPath: string
@@ -45,6 +53,7 @@ export interface ModelViewerProps {
isSensorSelectionEnabled?: boolean
onSensorPick?: (sensorId: string | null) => void
highlightAllSensors?: boolean
sensorStatusMap?: Record<string, string>
}
const ModelViewer: React.FC<ModelViewerProps> = ({
@@ -57,6 +66,7 @@ const ModelViewer: React.FC<ModelViewerProps> = ({
isSensorSelectionEnabled,
onSensorPick,
highlightAllSensors,
sensorStatusMap,
}) => {
const canvasRef = useRef<HTMLCanvasElement>(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 [modelReady, setModelReady] = 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);
@@ -222,7 +236,41 @@ const ModelViewer: React.FC<ModelViewerProps> = ({
if (!canvasRef.current || isInitializedRef.current) return
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
engine.runRenderLoop(() => {
@@ -260,7 +308,14 @@ const ModelViewer: React.FC<ModelViewerProps> = ({
fillLight.intensity = 0.3
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
const handleResize = () => {
@@ -285,7 +340,7 @@ const ModelViewer: React.FC<ModelViewerProps> = ({
}
sceneRef.current = null
}
}, [])
}, [onError])
useEffect(() => {
if (!isInitializedRef.current || isDisposedRef.current) {
@@ -427,19 +482,30 @@ const ModelViewer: React.FC<ModelViewerProps> = ({
}, [modelPath, onError, onModelLoaded])
useEffect(() => {
console.log('[ModelViewer] highlightAllSensors effect triggered:', { highlightAllSensors, modelReady, sceneReady: !!sceneRef.current })
if (!sceneRef.current || isDisposedRef.current || !modelReady) return
// Если включено выделение всех сенсоров - выделяем их все
if (highlightAllSensors) {
console.log('[ModelViewer] Calling highlightAllSensorMeshes()')
highlightAllSensorMeshes()
const allMeshes = importedMeshesRef.current || []
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
}
const sensorId = (focusSensorId ?? '').trim()
if (!sensorId) {
console.log('[ModelViewer] Focus cleared (no Sensor_ID provided)')
for (const m of highlightedMeshesRef.current) { m.renderingGroupId = 0 }
highlightedMeshesRef.current = []
highlightLayerRef.current?.removeAllMeshes()
@@ -452,7 +518,6 @@ const ModelViewer: React.FC<ModelViewerProps> = ({
const allMeshes = importedMeshesRef.current || []
if (allMeshes.length === 0) {
console.warn('[ModelViewer] No meshes available for sensor matching')
for (const m of highlightedMeshesRef.current) { m.renderingGroupId = 0 }
highlightedMeshesRef.current = []
highlightLayerRef.current?.removeAllMeshes()
@@ -462,44 +527,8 @@ const ModelViewer: React.FC<ModelViewerProps> = ({
return
}
const sensorMeshes = allMeshes.filter((m: any) => {
try {
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
}
})
const sensorMeshes = collectSensorMeshes(allMeshes)
const chosen = sensorMeshes.find(m => getSensorIdFromMesh(m) === sensorId)
console.log('[ModelViewer] Sensor focus', {
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('camRadius', camera, 'radius', frameRate, totalFrames, camera.radius, targetRadius, Animation.ANIMATIONLOOPMODE_CONSTANT, ease)
const hl = highlightLayerRef.current
if (hl) {
// Переключаем группу рендеринга для предыдущего выделенного меша
for (const m of highlightedMeshesRef.current) { m.renderingGroupId = 0 }
highlightedMeshesRef.current = []
hl.removeAllMeshes()
if (chosen instanceof Mesh) {
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))
}
}
}
}
applyHighlightToMeshes(
highlightLayerRef.current,
highlightedMeshesRef,
[chosen],
mesh => {
const sid = getSensorIdFromMesh(mesh)
const status = sid ? sensorStatusMap?.[sid] : undefined
return statusToColor3(status ?? null)
},
)
chosenMeshRef.current = chosen
setOverlayData({ name: chosen.name, sensorId })
} catch (error) {
console.error('[ModelViewer] Error focusing on sensor mesh:', error)
} catch {
for (const m of highlightedMeshesRef.current) { m.renderingGroupId = 0 }
highlightedMeshesRef.current = []
highlightLayerRef.current?.removeAllMeshes()
@@ -582,99 +593,6 @@ const ModelViewer: React.FC<ModelViewerProps> = ({
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [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(() => {
const scene = sceneRef.current
@@ -701,7 +619,7 @@ const ModelViewer: React.FC<ModelViewerProps> = ({
return () => {
scene.onPointerObservable.remove(pickObserver)
}
}, [modelReady, isSensorSelectionEnabled, onSensorPick, getSensorIdFromMesh])
}, [modelReady, isSensorSelectionEnabled, onSensorPick])
// Расчет позиции оверлея
const computeOverlayPosition = React.useCallback((mesh: AbstractMesh | null) => {
@@ -733,6 +651,41 @@ const ModelViewer: React.FC<ModelViewerProps> = ({
setOverlayPos(pos)
}, [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(() => {
if (!sceneRef.current || !chosenMeshRef.current || !overlayData) return
@@ -767,10 +720,24 @@ const ModelViewer: React.FC<ModelViewerProps> = ({
<canvas
ref={canvasRef}
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">
<LoadingSpinner
progress={loadingProgress}
@@ -778,8 +745,7 @@ const ModelViewer: React.FC<ModelViewerProps> = ({
strokeWidth={8}
/>
</div>
)}
{!modelReady && !isLoading && (
) : !modelReady ? (
<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-gray-400 text-lg font-semibold mb-4">
@@ -790,7 +756,7 @@ const ModelViewer: React.FC<ModelViewerProps> = ({
</div>
</div>
</div>
)}
) : null}
<SceneToolbar
onZoomIn={handleZoomIn}
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({ anchor: overlayPos, info: overlayData })
: null
@@ -809,4 +796,4 @@ const ModelViewer: React.FC<ModelViewerProps> = ({
)
}
export default ModelViewer
export default ModelViewer

View 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)
}
})
}

View 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
}

View File

@@ -4,6 +4,7 @@ import React from 'react'
import { useRouter } from 'next/navigation'
import useNavigationStore from '@/app/store/navigationStore'
import AreaChart from '../dashboard/AreaChart'
import * as statusColors from '../../lib/statusColors'
interface AlertType {
id: number
@@ -18,7 +19,7 @@ interface AlertType {
acknowledged: boolean
priority: string
}
interface AlertMenuProps {
alert: AlertType
isOpen: boolean
@@ -27,7 +28,7 @@ interface AlertMenuProps {
compact?: boolean
anchor?: { left: number; top: number } | null
}
const AlertMenu: React.FC<AlertMenuProps> = ({ alert, isOpen, onClose, getStatusText, compact = false, anchor = null }) => {
const router = useRouter()
const { setSelectedDetector, currentObject } = useNavigationStore()
@@ -56,17 +57,19 @@ const AlertMenu: React.FC<AlertMenuProps> = ({ alert, isOpen, onClose, getStatus
}
const getStatusColorCircle = (status: string) => {
// Use hex colors from Alerts submenu system
if (status === '#b3261e') return 'bg-red-500'
if (status === '#fd7c22') return 'bg-orange-500'
if (status === '#00ff00') return 'bg-green-500'
if (status === statusColors.STATUS_COLOR_CRITICAL) return 'bg-red-500'
if (status === statusColors.STATUS_COLOR_WARNING) return 'bg-orange-500'
if (status === statusColors.STATUS_COLOR_NORMAL) return 'bg-green-500'
// Fallback for text-based status
switch (status?.toLowerCase()) {
case 'critical': return 'bg-red-500'
case 'warning': return 'bg-orange-500'
case 'normal': return 'bg-green-500'
default: return 'bg-gray-500'
case 'critical':
return 'bg-red-500'
case 'warning':
return 'bg-orange-500'
case 'normal':
return 'bg-green-500'
default:
return 'bg-gray-500'
}
}
@@ -339,4 +342,4 @@ const AlertMenu: React.FC<AlertMenuProps> = ({ alert, isOpen, onClose, getStatus
)
}
export default AlertMenu
export default AlertMenu

View File

@@ -1,11 +1,12 @@
'use client'
import React, { useState } from 'react'
import * as statusColors from '../../lib/statusColors'
interface DetectorsDataType {
detectors: Record<string, DetectorType>
}
interface FloorNavigationProps {
objectId?: string
detectorsData: DetectorsDataType
@@ -13,7 +14,7 @@ interface FloorNavigationProps {
onClose?: () => void
is3DReady?: boolean
}
interface DetectorType {
detector_id: number
name: string
@@ -34,7 +35,7 @@ interface DetectorType {
priority: string
}>
}
const FloorNavigation: React.FC<FloorNavigationProps> = (props) => {
const { objectId, detectorsData, onDetectorMenuClick, onClose, is3DReady = true } = props
const [expandedFloors, setExpandedFloors] = useState<Set<number>>(new Set())
@@ -81,19 +82,27 @@ const FloorNavigation: React.FC<FloorNavigationProps> = (props) => {
const getStatusColor = (status: string) => {
switch (status) {
case '#b3261e': return 'bg-red-500'
case '#fd7c22': return 'bg-orange-500'
case '#00ff00': return 'bg-green-500'
default: return 'bg-gray-500'
case statusColors.STATUS_COLOR_CRITICAL:
return 'bg-red-500'
case statusColors.STATUS_COLOR_WARNING:
return 'bg-orange-500'
case statusColors.STATUS_COLOR_NORMAL:
return 'bg-green-500'
default:
return 'bg-gray-500'
}
}
const getStatusText = (status: string) => {
switch (status) {
case '#b3261e': return 'Критический'
case '#fd7c22': return 'Предупреждение'
case '#00ff00': return 'Норма'
default: return 'Неизвестно'
case statusColors.STATUS_COLOR_CRITICAL:
return 'Критический'
case statusColors.STATUS_COLOR_WARNING:
return 'Предупреждение'
case statusColors.STATUS_COLOR_NORMAL:
return 'Норма'
default:
return 'Неизвестно'
}
}
@@ -223,4 +232,4 @@ const FloorNavigation: React.FC<FloorNavigationProps> = (props) => {
)
}
export default FloorNavigation
export default FloorNavigation

View File

@@ -1,6 +1,7 @@
'use client'
import React, { useState } from 'react'
import * as statusColors from '../../lib/statusColors'
interface DetectorsDataType {
detectors: Record<string, DetectorType>
@@ -58,19 +59,27 @@ const ListOfDetectors: React.FC<ListOfDetectorsProps> = ({ objectId, detectorsDa
const getStatusColor = (status: string) => {
switch (status) {
case '#b3261e': return 'bg-red-500'
case '#fd7c22': return 'bg-orange-500'
case '#00ff00': return 'bg-green-500'
default: return 'bg-gray-500'
case statusColors.STATUS_COLOR_CRITICAL:
return 'bg-red-500'
case statusColors.STATUS_COLOR_WARNING:
return 'bg-orange-500'
case statusColors.STATUS_COLOR_NORMAL:
return 'bg-green-500'
default:
return 'bg-gray-500'
}
}
const getStatusText = (status: string) => {
switch (status) {
case '#b3261e': return 'Критический'
case '#fd7c22': return 'Предупреждение'
case '#00ff00': return 'Норма'
default: return 'Неизвестно'
case statusColors.STATUS_COLOR_CRITICAL:
return 'Критический'
case statusColors.STATUS_COLOR_WARNING:
return 'Предупреждение'
case statusColors.STATUS_COLOR_NORMAL:
return 'Норма'
default:
return 'Неизвестно'
}
}
@@ -168,4 +177,4 @@ const ListOfDetectors: React.FC<ListOfDetectorsProps> = ({ objectId, detectorsDa
)
}
export default ListOfDetectors
export default ListOfDetectors

View File

@@ -1,6 +1,7 @@
'use client'
import React from 'react'
import * as statusColors from '../../lib/statusColors'
interface DetectorInfoType {
detector_id: number
@@ -52,9 +53,9 @@ const NotificationDetectorInfo: React.FC<NotificationDetectorInfoProps> = ({ det
}
const getStatusColor = (status: string) => {
if (status === '#b3261e') return 'text-red-400'
if (status === '#fd7c22') return 'text-orange-400'
if (status === '#4caf50') return 'text-green-400'
if (status === statusColors.STATUS_COLOR_CRITICAL) return 'text-red-400'
if (status === statusColors.STATUS_COLOR_WARNING) return 'text-orange-400'
if (status === statusColors.STATUS_COLOR_NORMAL || status === '#4caf50') return 'text-green-400'
return 'text-gray-400'
}
@@ -147,9 +148,13 @@ const NotificationDetectorInfo: React.FC<NotificationDetectorInfoProps> = ({ det
style={{ backgroundColor: detectorInfo.status }}
></div>
<span className={`text-sm font-medium ${getStatusColor(detectorInfo.status)}`}>
{detectorInfo.status === '#b3261e' ? 'Критический' :
detectorInfo.status === '#fd7c22' ? 'Предупреждение' :
detectorInfo.status === '#4caf50' ? 'Нормальный' : 'Неизвестно'}
{detectorInfo.status === statusColors.STATUS_COLOR_CRITICAL
? 'Критический'
: detectorInfo.status === statusColors.STATUS_COLOR_WARNING
? 'Предупреждение'
: detectorInfo.status === statusColors.STATUS_COLOR_NORMAL || detectorInfo.status === '#4caf50'
? 'Нормальный'
: 'Неизвестно'}
</span>
</div>
</div>
@@ -181,4 +186,4 @@ const NotificationDetectorInfo: React.FC<NotificationDetectorInfoProps> = ({ det
)
}
export default NotificationDetectorInfo
export default NotificationDetectorInfo

View File

@@ -1,6 +1,7 @@
'use client';
import React, { useState, useMemo } from 'react';
import * as statusColors from '../../lib/statusColors';
interface NotificationType {
id: number;
@@ -78,19 +79,27 @@ const ReportsList: React.FC<ReportsListProps> = ({ detectorsData, initialSearchT
const getStatusColor = (type: string) => {
switch (type) {
case 'critical': return '#b3261e';
case 'warning': return '#fd7c22';
case 'info': return '#00ff00';
default: return '#666';
case 'critical':
return statusColors.STATUS_COLOR_CRITICAL;
case 'warning':
return statusColors.STATUS_COLOR_WARNING;
case 'info':
return statusColors.STATUS_COLOR_NORMAL;
default:
return statusColors.STATUS_COLOR_UNKNOWN;
}
};
const getPriorityColor = (priority: string) => {
switch (priority) {
case 'high': return '#b3261e';
case 'medium': return '#fd7c22';
case 'low': return '#00ff00';
default: return '#666';
case 'high':
return statusColors.STATUS_COLOR_CRITICAL;
case 'medium':
return statusColors.STATUS_COLOR_WARNING;
case 'low':
return statusColors.STATUS_COLOR_NORMAL;
default:
return statusColors.STATUS_COLOR_UNKNOWN;
}
};
@@ -278,4 +287,4 @@ const ReportsList: React.FC<ReportsListProps> = ({ detectorsData, initialSearchT
);
};
export default ReportsList;
export default ReportsList;