anycoder-810a4242 / components /Viewer3D.jsx
BerserkerZT's picture
Upload components/Viewer3D.jsx with huggingface_hub
586fa1f verified
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>
);
}