/* 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 { ChevronRight, Layers, Eye, EyeOff, FileBox, X, } from 'lucide-react'; import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip'; import { cn } from '@/lib/utils'; import type { TreeNode } from './types'; import { isSpatialContainer } from './types'; import { IFC_ICON_CODEPOINTS, IFC_ICON_DEFAULT } from './ifc-icons'; /** * Resolve the Material Symbols code point for a given IFC type string. * Falls back to the generic product icon for unmapped classes. */ function getIfcIconCodepoint(ifcType: string | undefined): string { if (!ifcType) return IFC_ICON_DEFAULT; return IFC_ICON_CODEPOINTS[ifcType] ?? IFC_ICON_DEFAULT; } /** Lucide fallback icons for non-IFC node types */ const NODE_TYPE_ICONS: Record = { 'unified-storey': Layers, 'model-header': FileBox, }; export interface HierarchyNodeProps { node: TreeNode; virtualRow: { size: number; start: number }; isSelected: boolean; nodeHidden: boolean; isMultiModel: boolean; modelsCount: number; modelVisible?: boolean; onNodeClick: (node: TreeNode, e: React.MouseEvent) => void; onToggleExpand: (nodeId: string) => void; onVisibilityToggle: (node: TreeNode) => void; onModelVisibilityToggle: (modelId: string, e: React.MouseEvent) => void; onRemoveModel: (modelId: string, e: React.MouseEvent) => void; onModelHeaderClick: (modelId: string, nodeId: string, hasChildren: boolean) => void; } export function HierarchyNode({ node, virtualRow, isSelected, nodeHidden, isMultiModel, modelsCount, modelVisible, onNodeClick, onToggleExpand, onVisibilityToggle, onModelVisibilityToggle, onRemoveModel, onModelHeaderClick, }: HierarchyNodeProps) { const resolvedType = node.ifcType || node.type; // Use Lucide icon for non-IFC structural nodes, Material Symbols for IFC classes const LucideIcon = NODE_TYPE_ICONS[node.type]; const iconCodepoint = getIfcIconCodepoint(resolvedType); // Model header nodes (for visibility control and expansion) if (node.type === 'model-header' && node.id.startsWith('model-')) { const modelId = node.modelIds[0]; return (
onModelHeaderClick(modelId, node.id, node.hasChildren)} > {/* Expand/collapse chevron */} {node.hasChildren ? ( ) : (
)} {node.name} {node.elementCount !== undefined && ( {node.elementCount.toLocaleString()} )}

{modelVisible ? 'Hide model' : 'Show model'}

{modelsCount > 1 && (

Remove model

)}
); } // Regular node rendering (spatial hierarchy nodes and elements) return (
{ if ((e.target as HTMLElement).closest('button') === null) { onNodeClick(node, e); } }} onMouseDown={(e) => { if ((e.target as HTMLElement).closest('button') === null) { e.preventDefault(); } }} > {/* Expand/Collapse */} {node.hasChildren ? ( ) : (
)} {/* Visibility Toggle - hide for spatial containers (Project/Site/Building) in multi-model mode */} {!(isMultiModel && isSpatialContainer(node.type)) && (

{node.isVisible ? 'Hide' : 'Show'}

)} {/* Type Icon */} {LucideIcon ? ( ) : ( )}

{resolvedType}

{/* Name */} {node.name} {node.ifcType && node.type === 'element' && ( {node.ifcType} )} {/* Storey Elevation */} {node.storeyElevation !== undefined && ( {node.storeyElevation >= 0 ? '+' : ''}{node.storeyElevation.toFixed(2)}m

Elevation: {node.storeyElevation >= 0 ? '+' : ''}{node.storeyElevation.toFixed(2)}m

)} {/* Element Count */} {node.elementCount !== undefined && ( {node.elementCount.toLocaleString()}

{node.elementCount.toLocaleString()} {node.elementCount === 1 ? 'element' : 'elements'}

)}
); } export interface SectionHeaderProps { icon: React.ElementType; title: string; count?: number; } export function SectionHeader({ icon: IconComponent, title, count }: SectionHeaderProps) { return (
{title} {count !== undefined && ( {count} )}
); }