Linked backend data to detectors' meshes.

This commit is contained in:
iv_vuytsik
2025-10-20 11:02:34 +03:00
parent aeda917001
commit 66e2bab683
15 changed files with 546 additions and 156 deletions

View File

@@ -11,14 +11,22 @@ import {
Color4,
AbstractMesh,
Nullable,
ImportMeshAsync
ImportMeshAsync,
HighlightLayer,
Mesh,
InstancedMesh,
Animation,
CubicEase,
EasingFunction,
Matrix,
Viewport
} from '@babylonjs/core'
import '@babylonjs/loaders'
import LoadingSpinner from '../ui/LoadingSpinner'
interface ModelViewerProps {
export interface ModelViewerProps {
modelPath: string
onModelLoaded?: (modelData: {
meshes: AbstractMesh[]
@@ -27,13 +35,17 @@ interface ModelViewerProps {
max: { x: number; y: number; z: number }
}
}) => void
onError?: (error: string) => void
onError?: (error: string) => void
focusSensorId?: string | null
renderOverlay?: (params: { anchor: { left: number; top: number } | null; info?: { name?: string; sensorId?: string } | null }) => React.ReactNode
}
const ModelViewer: React.FC<ModelViewerProps> = ({
modelPath,
onModelLoaded,
onError
onError,
focusSensorId,
renderOverlay,
}) => {
const canvasRef = useRef<HTMLCanvasElement>(null)
const engineRef = useRef<Nullable<Engine>>(null)
@@ -43,7 +55,13 @@ const ModelViewer: React.FC<ModelViewerProps> = ({
const [showModel, setShowModel] = useState(false)
const isInitializedRef = useRef(false)
const isDisposedRef = useRef(false)
const importedMeshesRef = useRef<AbstractMesh[]>([])
const highlightLayerRef = useRef<HighlightLayer | null>(null)
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)
const [modelReady, setModelReady] = useState(false)
useEffect(() => {
isDisposedRef.current = false
@@ -94,6 +112,9 @@ const ModelViewer: React.FC<ModelViewerProps> = ({
const fillLight = new HemisphericLight('fillLight', new Vector3(-1, 0.5, -1), scene)
fillLight.intensity = 0.3
fillLight.diffuse = new Color3(0.8, 0.8, 1)
const hl = new HighlightLayer('highlight-layer', scene)
highlightLayerRef.current = hl
const handleResize = () => {
if (!isDisposedRef.current) {
@@ -108,6 +129,9 @@ const ModelViewer: React.FC<ModelViewerProps> = ({
isDisposedRef.current = true
isInitializedRef.current = false
window.removeEventListener('resize', handleResize)
highlightLayerRef.current?.dispose()
highlightLayerRef.current = null
if (engineRef.current) {
engineRef.current.dispose()
engineRef.current = null
@@ -147,37 +171,14 @@ const ModelViewer: React.FC<ModelViewerProps> = ({
try {
const result = await ImportMeshAsync(modelPath, sceneRef.current)
importedMeshesRef.current = result.meshes
clearInterval(progressInterval)
setLoadingProgress(100)
console.log('GLTF Model loaded successfully!')
console.log('\n=== IfcSensor Meshes ===')
const sensorMeshes = (result.meshes || []).filter(m => (m.id ?? '').includes('IfcSensor'))
console.log('IfcSensor Mesh count:', sensorMeshes.length)
sensorMeshes.forEach(m => {
const meta: any = (m as any).metadata
const extras = meta?.extras ?? meta?.gltf?.extras
console.group(`IfcSensor Mesh: ${m.id || m.name}`)
console.log('id:', m.id)
console.log('name:', m.name)
console.log('uniqueId:', m.uniqueId)
console.log('class:', typeof (m as any).getClassName === 'function' ? (m as any).getClassName() : 'Mesh')
console.log('material:', m.material?.name)
console.log('parent:', m.parent?.name)
console.log('metadata:', meta)
if (extras) console.log('extras:', extras)
const bi = m.getBoundingInfo?.()
const bb = bi?.boundingBox
if (bb) {
console.log('bounding.center:', bb.center)
console.log('bounding.extendSize:', bb.extendSize)
}
const verts = (m as any).getTotalVertices?.()
if (typeof verts === 'number') console.log('vertices:', verts)
console.groupEnd()
})
console.log('[ModelViewer] ImportMeshAsync result:', result)
if (result.meshes.length > 0) {
const boundingBox = result.meshes[0].getHierarchyBoundingVectors()
@@ -187,7 +188,10 @@ const ModelViewer: React.FC<ModelViewerProps> = ({
const camera = sceneRef.current!.activeCamera as ArcRotateCamera
camera.radius = maxDimension * 2
camera.target = result.meshes[0].position
importedMeshesRef.current = result.meshes
setModelReady(true)
onModelLoaded?.({
meshes: result.meshes,
boundingBox: {
@@ -220,6 +224,113 @@ const ModelViewer: React.FC<ModelViewerProps> = ({
requestIdleCallback(() => loadModel(), { timeout: 50 })
}, [modelPath, onError, onModelLoaded])
useEffect(() => {
if (!sceneRef.current || isDisposedRef.current || !modelReady) return
const sensorId = (focusSensorId ?? '').trim()
if (!sensorId) {
console.log('[ModelViewer] Focus cleared (no Sensor_ID provided)')
highlightLayerRef.current?.removeAllMeshes()
chosenMeshRef.current = null
setOverlayPos(null)
setOverlayData(null)
return
}
const allMeshes = importedMeshesRef.current || []
const sensorMeshes = allMeshes.filter((m: any) => ((m.id ?? '').includes('IfcSensor') || (m.name ?? '').includes('IfcSensor')))
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
if (sid == null) return false
return String(sid).trim() === sensorId
} catch {
return false
}
})
console.log('[ModelViewer] Sensor focus', {
requested: sensorId,
totalImportedMeshes: allMeshes.length,
totalSensorMeshes: sensorMeshes.length,
chosen: chosen ? { id: chosen.id, name: chosen.name, uniqueId: chosen.uniqueId, parent: chosen.parent?.name } : null,
source: 'result.meshes',
})
const scene = sceneRef.current!
if (chosen) {
const camera = scene.activeCamera as ArcRotateCamera
const bbox = (typeof chosen.getHierarchyBoundingVectors === 'function')
? chosen.getHierarchyBoundingVectors()
: { min: chosen.getBoundingInfo().boundingBox.minimumWorld, max: chosen.getBoundingInfo().boundingBox.maximumWorld }
const center = bbox.min.add(bbox.max).scale(0.5)
const size = bbox.max.subtract(bbox.min)
const maxDimension = Math.max(size.x, size.y, size.z)
const targetRadius = Math.max(camera.lowerRadiusLimit ?? 2, maxDimension * 1.5)
scene.stopAnimation(camera)
const ease = new CubicEase()
ease.setEasingMode(EasingFunction.EASINGMODE_EASEINOUT)
const frameRate = 60
const durationMs = 600
const totalFrames = Math.round((durationMs / 1000) * frameRate)
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) {
hl.removeAllMeshes()
if (chosen instanceof Mesh) {
hl.addMesh(chosen, new Color3(1, 1, 0))
} else if (chosen instanceof InstancedMesh) {
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) {
hl.addMesh(cm, new Color3(1, 1, 0))
}
}
}
}
chosenMeshRef.current = chosen
setOverlayData({ name: chosen.name, sensorId })
} else {
highlightLayerRef.current?.removeAllMeshes()
chosenMeshRef.current = null
setOverlayPos(null)
setOverlayData(null)
}
}, [focusSensorId, modelReady])
useEffect(() => {
const scene = sceneRef.current
if (!scene || isDisposedRef.current) return
const observer = scene.onAfterRenderObservable.add(() => {
const chosen = chosenMeshRef.current
if (!chosen) return
const engine = scene.getEngine()
const cam = scene.activeCamera
if (!cam) return
const center = chosen.getBoundingInfo().boundingBox.centerWorld
const world = Matrix.IdentityReadOnly
const transform = scene.getTransformMatrix()
const viewport = new Viewport(0, 0, engine.getRenderWidth(), engine.getRenderHeight())
const projected = Vector3.Project(center, world, transform, viewport)
setOverlayPos({ left: projected.x, top: projected.y })
})
return () => {
scene.onAfterRenderObservable.remove(observer)
}
}, [])
return (
<div className="w-full h-screen relative bg-gray-900 overflow-hidden">
<canvas
@@ -237,6 +348,16 @@ const ModelViewer: React.FC<ModelViewerProps> = ({
/>
</div>
)}
{renderOverlay
? renderOverlay({ anchor: overlayPos, info: overlayData })
: (overlayData && overlayPos && (
<div className="absolute z-40 pointer-events-none" style={{ left: overlayPos.left, top: overlayPos.top }}>
<div className="rounded bg-black/70 text-white text-xs px-3 py-2 shadow-lg">
<div className="font-semibold truncate max-w-[200px]">{overlayData.name || 'Sensor'}</div>
{overlayData.sensorId && <div className="opacity-80">ID: {overlayData.sensorId}</div>}
</div>
</div>
))}
</div>
)
}