/* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ import { useMemo, useState, useEffect } from 'react'; import { Boxes, Triangle, CheckCircle2, AlertCircle, Loader2 } from 'lucide-react'; import { Separator } from '@/components/ui/separator'; import { formatNumber, formatBytes } from '@/lib/utils'; import { useViewerStore } from '@/store'; import { useIfc } from '@/hooks/useIfc'; import { useWebGPU } from '@/hooks/useWebGPU'; import { FlavorIndicator } from '@/components/extensions/FlavorIndicator'; import { FlavorDialog } from '@/components/extensions/FlavorDialog'; export function StatusBar() { const { loading, geometryResult, ifcDataStore } = useIfc(); const progress = useViewerStore((s) => s.progress); const error = useViewerStore((s) => s.error); const selectedStoreys = useViewerStore((s) => s.selectedStoreys); const activeStreamCanceller = useViewerStore((s) => s.activeStreamCanceller); const webgpu = useWebGPU(); const [fps, setFps] = useState(60); const [memory, setMemory] = useState(0); const [flavorDialogOpen, setFlavorDialogOpen] = useState(false); /** Deep-link from Command Palette → "Manage flavors…". */ const flavorDialogRequested = useViewerStore((s) => s.flavorDialogRequested); const setFlavorDialogRequested = useViewerStore((s) => s.setFlavorDialogRequested); useEffect(() => { if (flavorDialogRequested) { setFlavorDialogOpen(true); setFlavorDialogRequested(false); } }, [flavorDialogRequested, setFlavorDialogRequested]); // FPS counter (simplified) useEffect(() => { let frameCount = 0; let lastTime = performance.now(); let animationId: number; const measureFps = () => { frameCount++; const currentTime = performance.now(); if (currentTime - lastTime >= 1000) { setFps(frameCount); frameCount = 0; lastTime = currentTime; } animationId = requestAnimationFrame(measureFps); }; animationId = requestAnimationFrame(measureFps); return () => cancelAnimationFrame(animationId); }, []); // Memory usage (if available) useEffect(() => { const updateMemory = () => { if ((performance as any).memory) { setMemory((performance as any).memory.usedJSHeapSize); } }; updateMemory(); const interval = setInterval(updateMemory, 2000); return () => clearInterval(interval); }, []); const stats = useMemo(() => { if (!geometryResult) { return { elements: 0, triangles: 0 }; } // Count actual entities: for color-merged meshes, count unique entity IDs let elements = 0; const meshes = geometryResult.meshes; if (meshes) { for (let i = 0; i < meshes.length; i++) { const m = meshes[i] as { entityIds?: Uint32Array }; if (m.entityIds && m.entityIds.length > 0) { // Count unique entity IDs in this merged mesh const seen = new Set(); for (let j = 0; j < m.entityIds.length; j++) seen.add(m.entityIds[j]); elements += seen.size; } else { elements += 1; } } } return { elements, triangles: geometryResult.totalTriangles ?? 0, }; }, [geometryResult]); const visibleElements = useMemo(() => { if (selectedStoreys.size === 0 || !ifcDataStore?.spatialHierarchy) { return stats.elements; } // Count elements from all selected storeys let count = 0; for (const storeyId of selectedStoreys) { const storeyElements = ifcDataStore.spatialHierarchy.byStorey.get(storeyId); if (storeyElements) { count += storeyElements.length; } } return count || stats.elements; }, [selectedStoreys, ifcDataStore, stats.elements]); return (
{/* Left: Status */}
{loading ? ( {progress?.phase || 'Loading...'} ) : error ? ( {error} ) : ( Ready )} {/* Cancel button — only visible while a long-running stream (LAS/LAZ/PLY/PCD/E57) is in flight. The loader hooks register/clear the canceller around `await ingest.done`. */} {activeStreamCanceller && ( )}
{/* Center: Model Stats */}
{formatNumber(visibleElements)} {selectedStoreys.size > 0 && stats.elements !== visibleElements && ( / {formatNumber(stats.elements)} )} {' '}elements
{formatNumber(stats.triangles)} tris
{/* Right: Performance */}
{fps} FPS {memory > 0 && ( <> {formatBytes(memory)} )}
{webgpu.checking ? ( ) : webgpu.supported ? ( ) : ( )} {webgpu.checking ? 'Checking...' : webgpu.supported ? 'WebGPU' : 'No WebGPU'}
setFlavorDialogOpen(true)} /> v{__APP_VERSION__} ifclite.dev →
setFlavorDialogOpen(false)} />
); }