New api and zone management; highligh occlusion and highlighAll functionality; improved search in reports and alerts history + autofill; refactored alert panel

This commit is contained in:
iv_vuytsik
2025-12-25 03:10:21 +03:00
parent 31030f2997
commit ce7e39debf
36 changed files with 1562 additions and 472 deletions

View File

@@ -39,8 +39,12 @@ export interface ModelViewerProps {
}
}) => void
onError?: (error: string) => void
activeMenu?: string | null
focusSensorId?: string | null
renderOverlay?: (params: { anchor: { left: number; top: number } | null; info?: { name?: string; sensorId?: string } | null }) => React.ReactNode
isSensorSelectionEnabled?: boolean
onSensorPick?: (sensorId: string | null) => void
highlightAllSensors?: boolean
}
const ModelViewer: React.FC<ModelViewerProps> = ({
@@ -50,6 +54,9 @@ const ModelViewer: React.FC<ModelViewerProps> = ({
onError,
focusSensorId,
renderOverlay,
isSensorSelectionEnabled,
onSensorPick,
highlightAllSensors,
}) => {
const canvasRef = useRef<HTMLCanvasElement>(null)
const engineRef = useRef<Nullable<Engine>>(null)
@@ -61,6 +68,7 @@ const ModelViewer: React.FC<ModelViewerProps> = ({
const isDisposedRef = useRef(false)
const importedMeshesRef = useRef<AbstractMesh[]>([])
const highlightLayerRef = useRef<HighlightLayer | null>(null)
const highlightedMeshesRef = useRef<AbstractMesh[]>([])
const chosenMeshRef = useRef<AbstractMesh | null>(null)
const [overlayPos, setOverlayPos] = useState<{ left: number; top: number } | null>(null)
const [overlayData, setOverlayData] = useState<{ name?: string; sensorId?: string } | null>(null)
@@ -214,7 +222,7 @@ const ModelViewer: React.FC<ModelViewerProps> = ({
if (!canvasRef.current || isInitializedRef.current) return
const canvas = canvasRef.current
const engine = new Engine(canvas, true)
const engine = new Engine(canvas, true, { stencil: true })
engineRef.current = engine
engine.runRenderLoop(() => {
@@ -419,29 +427,40 @@ 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()
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()
chosenMeshRef.current = null
setOverlayPos(null)
setOverlayData(null)
return
}
}
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()
chosenMeshRef.current = null
setOverlayPos(null)
setOverlayData(null)
return
}
}
const sensorMeshes = allMeshes.filter((m: any) => {
try {
@@ -516,15 +535,26 @@ const ModelViewer: React.FC<ModelViewerProps> = ({
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))
}
}
@@ -534,18 +564,144 @@ const ModelViewer: React.FC<ModelViewerProps> = ({
setOverlayData({ name: chosen.name, sensorId })
} catch (error) {
console.error('[ModelViewer] Error focusing on sensor mesh:', error)
for (const m of highlightedMeshesRef.current) { m.renderingGroupId = 0 }
highlightedMeshesRef.current = []
highlightLayerRef.current?.removeAllMeshes()
chosenMeshRef.current = null
setOverlayPos(null)
setOverlayData(null)
}
} else {
for (const m of highlightedMeshesRef.current) { m.renderingGroupId = 0 }
highlightedMeshesRef.current = []
highlightLayerRef.current?.removeAllMeshes()
chosenMeshRef.current = null
setOverlayPos(null)
setOverlayData(null)
}
} else {
highlightLayerRef.current?.removeAllMeshes()
chosenMeshRef.current = null
setOverlayPos(null)
setOverlayData(null)
}
}, [focusSensorId, modelReady])
// 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
if (!scene || !modelReady || !isSensorSelectionEnabled) return
const pickObserver = scene.onPointerObservable.add((pointerInfo: PointerInfo) => {
if (pointerInfo.type !== PointerEventTypes.POINTERPICK) return
const pick = pointerInfo.pickInfo
if (!pick || !pick.hit) {
onSensorPick?.(null)
return
}
const pickedMesh = pick.pickedMesh
const sensorId = getSensorIdFromMesh(pickedMesh)
if (sensorId) {
onSensorPick?.(sensorId)
} else {
onSensorPick?.(null)
}
})
return () => {
scene.onPointerObservable.remove(pickObserver)
}
}, [modelReady, isSensorSelectionEnabled, onSensorPick, getSensorIdFromMesh])
// Расчет позиции оверлея
const computeOverlayPosition = React.useCallback((mesh: AbstractMesh | null) => {