import { AnyNodeId, useScene } from "@pascal-app/core"; import { ChevronDown, ChevronRight } from "lucide-react"; import { forwardRef, useEffect, useRef } from "react"; import { cn } from "@/lib/utils"; import { motion, AnimatePresence } from "motion/react"; export function handleTreeSelection( e: React.MouseEvent, nodeId: string, selectedIds: string[], setSelection: (s: any) => void ) { if (e.metaKey || e.ctrlKey) { if (selectedIds.includes(nodeId)) { setSelection({ selectedIds: selectedIds.filter((id) => id !== nodeId) }); } else { setSelection({ selectedIds: [...selectedIds, nodeId] }); } return true; } if (e.shiftKey && selectedIds.length > 0) { const lastSelectedId = selectedIds[selectedIds.length - 1]; if (lastSelectedId) { const nodes = Array.from(document.querySelectorAll('[data-treenode-id]')); const nodeIds = nodes.map(n => n.getAttribute('data-treenode-id') as string); const startIndex = nodeIds.indexOf(lastSelectedId); const endIndex = nodeIds.indexOf(nodeId); if (startIndex !== -1 && endIndex !== -1) { const start = Math.min(startIndex, endIndex); const end = Math.max(startIndex, endIndex); const range = nodeIds.slice(start, end + 1); // We can keep the previous selections that were outside the range if we want, // but standard file system shift-click replaces the selection with the range. setSelection({ selectedIds: range }); return true; } } // Fallback: if range selection fails (e.g. node not visible in tree), just add to selection if (!selectedIds.includes(nodeId)) { setSelection({ selectedIds: [...selectedIds, nodeId] }); return true; } } setSelection({ selectedIds: [nodeId] }); return false; } import { BuildingTreeNode } from "./building-tree-node"; import { CeilingTreeNode } from "./ceiling-tree-node"; import { DoorTreeNode } from "./door-tree-node"; import { ItemTreeNode } from "./item-tree-node"; import { LevelTreeNode } from "./level-tree-node"; import { RoofTreeNode } from "./roof-tree-node"; import { SlabTreeNode } from "./slab-tree-node"; import { WallTreeNode } from "./wall-tree-node"; import { WindowTreeNode } from "./window-tree-node"; import { ZoneTreeNode } from "./zone-tree-node"; interface TreeNodeProps { nodeId: AnyNodeId; depth?: number; isLast?: boolean; } export function TreeNode({ nodeId, depth = 0, isLast }: TreeNodeProps) { const node = useScene((state) => state.nodes[nodeId]); if (!node) return null; switch (node.type) { case "building": return ; case "ceiling": return ; case "level": return ; case "slab": return ; case "wall": return ; case "roof": return ; case "item": return ; case "door": return ; case "window": return ; case "zone": return ; default: return null; } } interface TreeNodeWrapperProps { nodeId?: string; icon: React.ReactNode; label: React.ReactNode; depth: number; hasChildren: boolean; expanded: boolean; onToggle: () => void; onClick: (e: React.MouseEvent) => void; onDoubleClick?: () => void; onMouseEnter?: () => void; onMouseLeave?: () => void; actions?: React.ReactNode; children?: React.ReactNode; isSelected?: boolean; isHovered?: boolean; isVisible?: boolean; isLast?: boolean; } export const TreeNodeWrapper = forwardRef( function TreeNodeWrapper( { nodeId, icon, label, depth, hasChildren, expanded, onToggle, onClick, onDoubleClick, onMouseEnter, onMouseLeave, actions, children, isSelected, isHovered, isVisible = true, isLast, }, ref ) { const rowRef = useRef(null); useEffect(() => { if (isSelected && rowRef.current) { rowRef.current.scrollIntoView({ behavior: 'smooth', block: 'nearest' }); } }, [isSelected]); return (
{/* Vertical tree line */}
{/* Horizontal branch line */}
{/* Line down to children */} {hasChildren && expanded && (
)}
{icon}
{label}
{actions && (
{actions}
)}
{expanded && children && ( {children} )}
); } );