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:
@@ -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) => {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import React, { useState } from 'react';
|
||||
import Image from 'next/image';
|
||||
import useNavigationStore from '@/app/store/navigationStore';
|
||||
import type { Zone } from '@/app/types';
|
||||
|
||||
interface ToolbarButton {
|
||||
icon: string;
|
||||
@@ -32,7 +33,7 @@ const SceneToolbar: React.FC<SceneToolbarProps> = ({
|
||||
navMenuActive = false,
|
||||
}) => {
|
||||
const [isZoomOpen, setIsZoomOpen] = useState(false);
|
||||
const { PREFERRED_MODEL, showMonitoring, openMonitoring, closeMonitoring } = useNavigationStore();
|
||||
const { showMonitoring, openMonitoring, closeMonitoring, currentZones, loadZones, currentObject } = useNavigationStore();
|
||||
|
||||
const handleToggleNavMenu = () => {
|
||||
if (showMonitoring) {
|
||||
@@ -43,25 +44,46 @@ const SceneToolbar: React.FC<SceneToolbarProps> = ({
|
||||
};
|
||||
|
||||
const handleHomeClick = async () => {
|
||||
if (onSelectModel) {
|
||||
try {
|
||||
const res = await fetch('/api/big-models/list');
|
||||
if (!res.ok) {
|
||||
throw new Error('Failed to fetch models list');
|
||||
}
|
||||
const data = await res.json();
|
||||
const items: { name: string; path: string }[] = Array.isArray(data?.models) ? data.models : [];
|
||||
const preferredModelName = PREFERRED_MODEL.split('/').pop()?.split('.').slice(0, -1).join('.') || '';
|
||||
const preferredModel = items.find(model => (model.path.split('/').pop()?.split('.').slice(0, -1).join('.') || '') === preferredModelName);
|
||||
if (!onSelectModel) return;
|
||||
|
||||
if (preferredModel) {
|
||||
onSelectModel(preferredModel.path);
|
||||
} else {
|
||||
console.error('Preferred model not found in the list');
|
||||
try {
|
||||
let zones: Zone[] = Array.isArray(currentZones) ? currentZones : [];
|
||||
|
||||
// Если зоны ещё не загружены, откройте Monitoring и загрузите зоны для текущего объекта
|
||||
if ((!zones || zones.length === 0) && currentObject?.id) {
|
||||
if (!showMonitoring) {
|
||||
openMonitoring();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error fetching models list:', error);
|
||||
await loadZones(currentObject.id);
|
||||
zones = useNavigationStore.getState().currentZones || [];
|
||||
}
|
||||
|
||||
if (!Array.isArray(zones) || zones.length === 0) {
|
||||
console.warn('No zones available to select a model from.');
|
||||
return;
|
||||
}
|
||||
|
||||
const sorted = zones.slice().sort((a: Zone, b: Zone) => {
|
||||
const oa = typeof a.order === 'number' ? a.order : 0;
|
||||
const ob = typeof b.order === 'number' ? b.order : 0;
|
||||
if (oa !== ob) return oa - ob;
|
||||
return (a.name || '').localeCompare(b.name || '');
|
||||
});
|
||||
|
||||
const top = sorted[0];
|
||||
let chosenPath: string | null = top?.model_path && String(top.model_path).trim() ? top.model_path! : null;
|
||||
if (!chosenPath) {
|
||||
const nextWithModel = sorted.find((z) => z.model_path && String(z.model_path).trim());
|
||||
chosenPath = nextWithModel?.model_path ?? null;
|
||||
}
|
||||
|
||||
if (chosenPath) {
|
||||
onSelectModel(chosenPath);
|
||||
} else {
|
||||
console.warn('No zone has a valid model_path to open.');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error selecting top zone model:', error);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user