/* 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/. */ /** * Model metadata panel - displays file info, schema version, entity counts, * coordinate system info, and project information. */ import { useMemo } from 'react'; import { Layers, FileText, Tag, FileBox, Clock, HardDrive, Hash, Database, Building2, Ruler, } from 'lucide-react'; import { ScrollArea } from '@/components/ui/scroll-area'; import { PropertySetCard } from './PropertySetCard'; import { GeoreferencingPanel } from './GeoreferencingPanel'; import type { PropertySet } from './encodingUtils'; import type { FederatedModel } from '@/store/types'; import { extractGeoreferencingOnDemand, extractLengthUnitScale, type IfcDataStore } from '@ifc-lite/parser'; /** Model metadata panel - displays file info, schema version, entity counts, etc. */ export function ModelMetadataPanel({ model }: { model: FederatedModel }) { const dataStore = model.ifcDataStore; // Format file size const formatFileSize = (bytes: number): string => { if (bytes < 1024) return `${bytes} B`; if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`; return `${(bytes / (1024 * 1024)).toFixed(2)} MB`; }; // Format date const formatDate = (timestamp: number): string => { return new Date(timestamp).toLocaleString(); }; // Get IfcProject data if available const projectData = useMemo(() => { if (!dataStore?.spatialHierarchy?.project) return null; const project = dataStore.spatialHierarchy.project; const projectId = project.expressId; // Get project entity attributes const name = dataStore.entities.getName(projectId); const globalId = dataStore.entities.getGlobalId(projectId); const description = dataStore.entities.getDescription(projectId); // Get project properties const properties: PropertySet[] = []; if (dataStore.properties) { for (const pset of dataStore.properties.getForEntity(projectId)) { properties.push({ name: pset.name, properties: pset.properties.map(p => ({ name: p.name, value: p.value })), }); } } return { name, globalId, description, properties }; }, [dataStore]); // Count storeys and elements const stats = useMemo(() => { if (!dataStore?.spatialHierarchy) { return { storeys: 0, elementsWithGeometry: 0 }; } const storeys = dataStore.spatialHierarchy.byStorey.size; let elementsWithGeometry = 0; for (const elements of dataStore.spatialHierarchy.byStorey.values()) { elementsWithGeometry += (elements as number[]).length; } return { storeys, elementsWithGeometry }; }, [dataStore]); // Extract georeferencing info const georef = useMemo(() => { if (!dataStore) return null; const info = extractGeoreferencingOnDemand(dataStore as IfcDataStore); return info?.hasGeoreference ? info : null; }, [dataStore]); // Extract length unit scale const unitInfo = useMemo(() => { if (!dataStore?.source?.length || !dataStore?.entityIndex) return null; const scale = extractLengthUnitScale(dataStore.source, dataStore.entityIndex); let unitName = 'Meters'; if (Math.abs(scale - 0.001) < 0.0001) unitName = 'Millimeters'; else if (Math.abs(scale - 0.01) < 0.001) unitName = 'Centimeters'; else if (Math.abs(scale - 0.0254) < 0.001) unitName = 'Inches'; else if (Math.abs(scale - 0.3048) < 0.01) unitName = 'Feet'; return { scale, unitName }; }, [dataStore]); return (
{/* Header */}

{model.name}

IFC Model

{/* Schema badge */}
{model.schemaVersion}
{/* `min-h-0` is required: without it `flex-1` falls back to min-height:auto and the ScrollArea grows past the panel's height instead of constraining the inner viewport, so the map (and any tall content underneath) overflowed past the right panel's clip box. */} {/* File Information */}

File Information

File Size {formatFileSize(model.fileSize)}
Loaded At {formatDate(model.loadedAt)}
{dataStore && dataStore.parseTime != null && (
Parse Time {dataStore.parseTime.toFixed(0)} ms
)}
{/* Length Unit */} {unitInfo && (
Length Unit {unitInfo.unitName} ({unitInfo.scale})
)} {/* IfcProject Data — placed near the top so the model's name, description, and project-level psets are the first thing users see after file info. Previously the section was at the bottom of the panel (below the map), which buried critical project identity below scrollable georeferencing content. */} {projectData && (

Project Information

{projectData.name && (
Name {projectData.name}
)} {projectData.description && (
Description {projectData.description}
)} {projectData.globalId && (
GlobalId {projectData.globalId}
)}
{/* Project Properties */} {projectData.properties.length > 0 && (
{projectData.properties.map((pset) => ( ))}
)}
)} {/* Entity Statistics */}

Statistics

Total Entities {dataStore?.entityCount?.toLocaleString() ?? 'N/A'}
Building Storeys {stats.storeys}
Elements with Geometry {stats.elementsWithGeometry.toLocaleString()}
Max Express ID {model.maxExpressId.toLocaleString()}
{/* Georeferencing — kept at the bottom because it embeds a tall location map; placing it earlier would push the statistics + project metadata below the fold. */}
); }