Spaces:
Build error
Build error
| import { useRef, useEffect, useState, useCallback } from 'react'; | |
| import { Canvas, useFrame, useThree } from '@react-three/fiber'; | |
| import { OrbitControls, Grid, Box, Stats } from '@react-three/drei'; | |
| import * as THREE from 'three'; | |
| import { useAWPStore } from '../store/awpStore'; | |
| // Mock IFC geometry loader - in production, use web-ifc-three | |
| function IFCModel({ modelData, onElementClick }) { | |
| const meshRef = useRef(); | |
| const [hovered, setHovered] = useState(null); | |
| // Generate mock geometry based on model metadata | |
| const geometries = useCallback(() => { | |
| if (!modelData) return []; | |
| // Mock: Generate boxes representing plant components | |
| const components = []; | |
| const colors = { | |
| installed: 0x22c55e, | |
| ready: 0xeab308, | |
| constrained: 0xef4444, | |
| pending: 0x6b7280, | |
| }; | |
| for (let i = 0; i < 50; i++) { | |
| const status = ['installed', 'ready', 'constrained', 'pending'][Math.floor(Math.random() * 4)]; | |
| components.push({ | |
| id: `element-${i}`, | |
| position: [ | |
| (Math.random() - 0.5) * 100, | |
| (Math.random() - 0.5) * 50, | |
| (Math.random() - 0.5) * 100, | |
| ], | |
| scale: [ | |
| 2 + Math.random() * 8, | |
| 2 + Math.random() * 8, | |
| 2 + Math.random() * 8, | |
| ], | |
| color: colors[status], | |
| status, | |
| }); | |
| } | |
| return components; | |
| }, [modelData]); | |
| const elements = geometries(); | |
| return ( | |
| <group ref={meshRef}> | |
| {elements.map((el) => ( | |
| <Box | |
| key={el.id} | |
| position={el.position} | |
| args={el.scale} | |
| onClick={() => onElementClick(el.id)} | |
| onPointerOver={() => setHovered(el.id)} | |
| onPointerOut={() => setHovered(null)} | |
| > | |
| <meshStandardMaterial | |
| color={hovered === el.id ? 0x06b6d4 : el.color} | |
| transparent | |
| opacity={hovered === el.id ? 0.9 : 0.7} | |
| emissive={hovered === el.id ? 0x06b6d4 : 0x000000} | |
| emissiveIntensity={hovered === el.id ? 0.3 : 0} | |
| /> | |
| <lineSegments> | |
| <edgesGeometry args={[new THREE.BoxGeometry(...el.scale)]} /> | |
| <lineBasicMaterial color={0x353545} /> | |
| </lineSegments> | |
| </Box> | |
| ))} | |
| </group> | |
| ); | |
| } | |
| function Scene({ modelData, onElementClick }) { | |
| const { camera } = useThree(); | |
| useEffect(() => { | |
| camera.position.set(50, 50, 50); | |
| camera.lookAt(0, 0, 0); | |
| }, [camera]); | |
| return ( | |
| <> | |
| <ambientLight intensity={0.4} /> | |
| <directionalLight position={[100, 100, 50]} intensity={0.8} /> | |
| <pointLight position={[-100, -100, -100]} intensity={0.3} color={0x06b6d4} /> | |
| <Grid | |
| position={[0, -25, 0]} | |
| args={[200, 200]} | |
| cellSize={10} | |
| cellThickness={0.5} | |
| cellColor={0x353545} | |
| sectionSize={50} | |
| sectionThickness={1} | |
| sectionColor={0x454555} | |
| fadeDistance={200} | |
| fadeStrength={1} | |
| infiniteGrid | |
| /> | |
| <IFCModel modelData={modelData} onElementClick={onElementClick} /> | |
| <OrbitControls | |
| enablePan | |
| enableZoom | |
| enableRotate | |
| minDistance={10} | |
| maxDistance={200} | |
| /> | |
| </> | |
| ); | |
| } | |
| function FPSCounter() { | |
| const [fps, setFps] = useState(0); | |
| const frameCount = useRef(0); | |
| const lastTime = useRef(performance.now()); | |
| const { updateFPS } = useAWPStore(); | |
| useFrame(() => { | |
| frameCount.current++; | |
| const now = performance.now(); | |
| const delta = now - lastTime.current; | |
| if (delta >= 1000) { | |
| const currentFps = Math.round((frameCount.current * 1000) / delta); | |
| setFps(currentFps); | |
| updateFPS(currentFps); | |
| frameCount.current = 0; | |
| lastTime.current = now; | |
| } | |
| }); | |
| return ( | |
| <div className="absolute top-4 right-4 glass px-3 py-1.5 rounded-lg"> | |
| <span className={`text-sm font-mono ${fps >= 30 ? 'text-status-installed' : fps >= 15 ? 'text-status-ready' : 'text-status-constrained'}`}> | |
| {fps} FPS | |
| </span> | |
| </div> | |
| ); | |
| } | |
| export function Viewer3D() { | |
| const { ifcModel, selectElement, addLog } = useAWPStore(); | |
| const [viewMode, setViewMode] = useState('3d'); // '3d', 'top', 'side' | |
| const handleElementClick = useCallback((elementId) => { | |
| selectElement(elementId); | |
| addLog('RENDERER', 'DEBUG', `Element selected: ${elementId}`); | |
| }, [selectElement, addLog]); | |
| return ( | |
| <div className="h-full bg-industrial-900 relative"> | |
| {/* View Controls */} | |
| <div className="absolute top-4 left-4 flex gap-2 z-10"> | |
| {['3d', 'top', 'side'].map((mode) => ( | |
| <button | |
| key={mode} | |
| onClick={() => setViewMode(mode)} | |
| className={`px-3 py-1.5 rounded text-xs font-medium uppercase transition-colors ${ | |
| viewMode === mode | |
| ? 'bg-accent-cyan text-industrial-900' | |
| : 'bg-industrial-700 text-industrial-300 hover:bg-industrial-600' | |
| }`} | |
| > | |
| {mode} | |
| </button> | |
| ))} | |
| </div> | |
| {/* Legend */} | |
| <div className="absolute bottom-4 left-4 glass px-4 py-3 rounded-lg z-10"> | |
| <h4 className="text-xs font-semibold text-industrial-300 mb-2 uppercase tracking-wider">Status Legend</h4> | |
| <div className="space-y-1.5"> | |
| {[ | |
| { color: 'bg-status-installed', label: 'Installed' }, | |
| { color: 'bg-status-ready', label: 'Ready/Material Available' }, | |
| { color: 'bg-status-constrained', label: 'Constrained/Delayed' }, | |
| { color: 'bg-industrial-500', label: 'Pending' }, | |
| ].map(({ color, label }) => ( | |
| <div key={label} className="flex items-center gap-2"> | |
| <span className={`w-3 h-3 rounded-sm ${color}`} /> | |
| <span className="text-xs text-industrial-300">{label}</span> | |
| </div> | |
| ))} | |
| </div> | |
| </div> | |
| {/* 3D Canvas */} | |
| <Canvas | |
| camera={{ position: [50, 50, 50], fov: 50 }} | |
| gl={{ | |
| antialias: true, | |
| alpha: true, | |
| powerPreference: 'high-performance', | |
| onCreated={({ gl }) => { | |
| gl.setClearColor(0x0a0a0f); | |
| > | |
| <Scene modelData={ifcModel} onElementClick={handleElementClick} /> | |
| <FPSCounter /> | |
| </Canvas> | |
| {/* Empty State */} | |
| {!ifcModel && ( | |
| <div className="absolute inset-0 flex items-center justify-center bg-industrial-900/90"> | |
| <div className="text-center"> | |
| <div className="w-16 h-16 mx-auto mb-4 rounded-lg bg-industrial-700 flex items-center justify-center"> | |
| <span className="text-2xl">📐</span> | |
| </div> | |
| <h3 className="text-lg font-semibold text-white mb-2">No Model Loaded</h3> | |
| <p className="text-sm text-industrial-400 max-w-xs"> | |
| Import an IFC file from the WBS panel to begin 3D visualization | |
| </p> | |
| </div> | |
| </div> | |
| )} | |
| </div> | |
| ); | |
| } |