Files
aerbim-ht-monitor/frontend/components/model/ModelViewer.tsx
2025-09-25 08:31:31 +03:00

223 lines
6.5 KiB
TypeScript

'use client'
import React, { useEffect, useRef, useState } from 'react'
import {
Engine,
Scene,
Vector3,
HemisphericLight,
ArcRotateCamera,
Color3,
Color4,
AbstractMesh,
Nullable,
ImportMeshAsync
} from '@babylonjs/core'
import '@babylonjs/loaders'
import LoadingSpinner from '../ui/LoadingSpinner'
interface ModelViewerProps {
modelPath: string
onModelLoaded?: (modelData: {
meshes: AbstractMesh[]
boundingBox: {
min: { x: number; y: number; z: number }
max: { x: number; y: number; z: number }
}
}) => void
onError?: (error: string) => void
}
const ModelViewer: React.FC<ModelViewerProps> = ({
modelPath,
onModelLoaded,
onError
}) => {
const canvasRef = useRef<HTMLCanvasElement>(null)
const engineRef = useRef<Nullable<Engine>>(null)
const sceneRef = useRef<Nullable<Scene>>(null)
const [isLoading, setIsLoading] = useState(false)
const [loadingProgress, setLoadingProgress] = useState(0)
const [showModel, setShowModel] = useState(false)
const isInitializedRef = useRef(false)
const isDisposedRef = useRef(false)
useEffect(() => {
isDisposedRef.current = false
isInitializedRef.current = false
return () => {
isDisposedRef.current = true
}
}, [])
useEffect(() => {
if (!canvasRef.current || isInitializedRef.current) return
const canvas = canvasRef.current
const engine = new Engine(canvas, true)
engineRef.current = engine
engine.runRenderLoop(() => {
if (!isDisposedRef.current && sceneRef.current) {
sceneRef.current.render()
}
})
const scene = new Scene(engine)
sceneRef.current = scene
scene.clearColor = new Color4(0.1, 0.1, 0.15, 1)
const camera = new ArcRotateCamera('camera', 0, Math.PI / 3, 20, Vector3.Zero(), scene)
camera.attachControl(canvas, true)
camera.lowerRadiusLimit = 2
camera.upperRadiusLimit = 200
camera.wheelDeltaPercentage = 0.01
camera.panningSensibility = 50
camera.angularSensibilityX = 1000
camera.angularSensibilityY = 1000
const ambientLight = new HemisphericLight('ambientLight', new Vector3(0, 1, 0), scene)
ambientLight.intensity = 0.4
ambientLight.diffuse = new Color3(0.7, 0.7, 0.8)
ambientLight.specular = new Color3(0.2, 0.2, 0.3)
ambientLight.groundColor = new Color3(0.3, 0.3, 0.4)
const keyLight = new HemisphericLight('keyLight', new Vector3(1, 1, 0), scene)
keyLight.intensity = 0.6
keyLight.diffuse = new Color3(1, 1, 0.9)
keyLight.specular = new Color3(1, 1, 0.9)
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 handleResize = () => {
if (!isDisposedRef.current) {
engine.resize()
}
}
window.addEventListener('resize', handleResize)
isInitializedRef.current = true
return () => {
isDisposedRef.current = true
isInitializedRef.current = false
window.removeEventListener('resize', handleResize)
if (engineRef.current) {
engineRef.current.dispose()
engineRef.current = null
}
sceneRef.current = null
}
}, [])
useEffect(() => {
if (!isInitializedRef.current || !modelPath || isDisposedRef.current) {
return
}
const loadModel = async () => {
if (!sceneRef.current || isDisposedRef.current) {
return
}
const oldMeshes = sceneRef.current.meshes.slice();
oldMeshes.forEach(m => m.dispose());
setIsLoading(true)
setLoadingProgress(0)
setShowModel(false)
console.log('Loading GLTF model:', modelPath)
// UI элемент загрузчика (есть эффект замедленности)
const progressInterval = setInterval(() => {
setLoadingProgress(prev => {
if (prev >= 90) {
clearInterval(progressInterval)
return 90
}
return prev + Math.random() * 15
})
}, 100)
try {
const result = await ImportMeshAsync(modelPath, sceneRef.current)
clearInterval(progressInterval)
setLoadingProgress(100)
console.log('GLTF Model loaded successfully!')
console.log('\n=== Complete Model Object ===')
console.log(result)
console.log('\n=== Structure Overview ===')
console.log('Meshes:', result.meshes?.length || 0)
console.log('Transform Nodes:', result.transformNodes?.length || 0)
if (result.meshes.length > 0) {
const boundingBox = result.meshes[0].getHierarchyBoundingVectors()
const size = boundingBox.max.subtract(boundingBox.min)
const maxDimension = Math.max(size.x, size.y, size.z)
const camera = sceneRef.current!.activeCamera as ArcRotateCamera
camera.radius = maxDimension * 2
camera.target = result.meshes[0].position
onModelLoaded?.({
meshes: result.meshes,
boundingBox: {
min: boundingBox.min,
max: boundingBox.max
}
})
// Плавное появление модели
setTimeout(() => {
if (!isDisposedRef.current) {
setShowModel(true)
setIsLoading(false)
}
}, 500)
} else {
console.warn('No meshes found in model')
onError?.('No geometry found in model')
setIsLoading(false)
}
} catch (error) {
clearInterval(progressInterval)
console.error('Error loading GLTF model:', error)
onError?.(`Failed to load model: ${error}`)
setIsLoading(false)
}
}
// Загрузка модлеи начинается после появления спиннера
requestIdleCallback(() => loadModel(), { timeout: 50 })
}, [modelPath, onError, onModelLoaded])
return (
<div className="w-full h-screen relative bg-gray-900 overflow-hidden">
<canvas
ref={canvasRef}
className={`w-full h-full outline-none block transition-opacity duration-500 ${
showModel ? 'opacity-100' : 'opacity-0'
}`}
/>
{isLoading && (
<div className="absolute inset-0 bg-gray-900 flex items-center justify-center z-50">
<LoadingSpinner
progress={loadingProgress}
size={120}
strokeWidth={8}
/>
</div>
)}
</div>
)
}
export default ModelViewer