import { useState, useEffect, useMemo } from 'react'; import { Entity } from '@esengine/ecs-framework'; import { EntityStoreService, MessageHub } from '@esengine/editor-core'; import { PropertyInspector } from './PropertyInspector'; import { BehaviorTreeNodeProperties } from './BehaviorTreeNodeProperties'; import { FileSearch, ChevronDown, ChevronRight, X, Settings, Box, AlertTriangle, Copy, File as FileIcon, Folder, Clock, HardDrive } from 'lucide-react'; import { BehaviorTreeNode, useBehaviorTreeStore } from '../stores/behaviorTreeStore'; import { ICON_MAP } from '../presentation/config/editorConstants'; import { useNodeOperations } from '../presentation/hooks/useNodeOperations'; import { useCommandHistory } from '../presentation/hooks/useCommandHistory'; import { NodeFactory } from '../infrastructure/factories/NodeFactory'; import { BehaviorTreeValidator } from '../infrastructure/validation/BehaviorTreeValidator'; import { TauriAPI } from '../api/tauri'; import '../styles/EntityInspector.css'; interface InspectorProps { entityStore: EntityStoreService; messageHub: MessageHub; projectPath?: string | null; isExecuting?: boolean; executionMode?: 'idle' | 'running' | 'paused' | 'step'; } interface AssetFileInfo { name: string; path: string; extension?: string; size?: number; modified?: number; isDirectory: boolean; } type InspectorTarget = | { type: 'entity'; data: Entity } | { type: 'remote-entity'; data: any; details?: any } | { type: 'behavior-tree-node'; data: BehaviorTreeNode } | { type: 'asset-file'; data: AssetFileInfo; content?: string } | null; export function Inspector({ entityStore: _entityStore, messageHub, projectPath, isExecuting, executionMode }: InspectorProps) { const [target, setTarget] = useState(null); const [expandedComponents, setExpandedComponents] = useState>(new Set()); const [componentVersion, setComponentVersion] = useState(0); // 行为树节点操作相关 const nodeFactory = useMemo(() => new NodeFactory(), []); const validator = useMemo(() => new BehaviorTreeValidator(), []); const { commandManager } = useCommandHistory(); const nodeOperations = useNodeOperations(nodeFactory, validator, commandManager); const { nodes, connections, isExecuting: storeIsExecuting } = useBehaviorTreeStore(); // 优先使用传入的 isExecuting,否则使用 store 中的 const isRunning = isExecuting ?? storeIsExecuting; // 当节点数据更新时,同步更新 target 中的节点 useEffect(() => { if (target?.type === 'behavior-tree-node') { const updatedNode = nodes.find(n => n.id === target.data.id); if (updatedNode) { const currentDataStr = JSON.stringify(target.data.data); const updatedDataStr = JSON.stringify(updatedNode.data); if (currentDataStr !== updatedDataStr) { setTarget({ type: 'behavior-tree-node', data: updatedNode }); } } } }, [nodes]); useEffect(() => { const handleEntitySelection = (data: { entity: Entity | null }) => { if (data.entity) { setTarget({ type: 'entity', data: data.entity }); } else { setTarget(null); } setComponentVersion(0); }; const handleRemoteEntitySelection = (data: { entity: any }) => { setTarget({ type: 'remote-entity', data: data.entity }); }; const handleEntityDetails = (event: Event) => { const customEvent = event as CustomEvent; const details = customEvent.detail; if (target?.type === 'remote-entity') { setTarget({ ...target, details }); } }; const handleBehaviorTreeNodeSelection = (data: { node: BehaviorTreeNode }) => { setTarget({ type: 'behavior-tree-node', data: data.node }); }; const handleAssetFileSelection = async (data: { fileInfo: AssetFileInfo }) => { const fileInfo = data.fileInfo; if (fileInfo.isDirectory) { setTarget({ type: 'asset-file', data: fileInfo }); return; } const textExtensions = ['txt', 'json', 'md', 'ts', 'tsx', 'js', 'jsx', 'css', 'html', 'xml', 'yaml', 'yml', 'toml', 'ini', 'cfg', 'conf', 'log', 'btree', 'ecs']; const isTextFile = fileInfo.extension && textExtensions.includes(fileInfo.extension.toLowerCase()); if (isTextFile) { try { const content = await TauriAPI.readFileContent(fileInfo.path); setTarget({ type: 'asset-file', data: fileInfo, content }); } catch (error) { console.error('Failed to read file content:', error); setTarget({ type: 'asset-file', data: fileInfo }); } } else { setTarget({ type: 'asset-file', data: fileInfo }); } }; const handleComponentChange = () => { setComponentVersion((prev) => prev + 1); }; const unsubEntitySelect = messageHub.subscribe('entity:selected', handleEntitySelection); const unsubRemoteSelect = messageHub.subscribe('remote-entity:selected', handleRemoteEntitySelection); const unsubNodeSelect = messageHub.subscribe('behavior-tree:node-selected', handleBehaviorTreeNodeSelection); const unsubAssetFileSelect = messageHub.subscribe('asset-file:selected', handleAssetFileSelection); const unsubComponentAdded = messageHub.subscribe('component:added', handleComponentChange); const unsubComponentRemoved = messageHub.subscribe('component:removed', handleComponentChange); window.addEventListener('profiler:entity-details', handleEntityDetails); return () => { unsubEntitySelect(); unsubRemoteSelect(); unsubNodeSelect(); unsubAssetFileSelect(); unsubComponentAdded(); unsubComponentRemoved(); window.removeEventListener('profiler:entity-details', handleEntityDetails); }; }, [messageHub, target]); const handleRemoveComponent = (index: number) => { if (target?.type !== 'entity') return; const entity = target.data; const component = entity.components[index]; if (component) { entity.removeComponent(component); messageHub.publish('component:removed', { entity, component }); } }; const toggleComponentExpanded = (index: number) => { setExpandedComponents((prev) => { const newSet = new Set(prev); if (newSet.has(index)) { newSet.delete(index); } else { newSet.add(index); } return newSet; }); }; const handlePropertyChange = (component: any, propertyName: string, value: any) => { if (target?.type !== 'entity') return; const entity = target.data; messageHub.publish('component:property:changed', { entity, component, propertyName, value }); }; const handleNodePropertyChange = (propertyName: string, value: any) => { if (target?.type !== 'behavior-tree-node') return; const node = target.data; nodeOperations.updateNodeData(node.id, { ...node.data, [propertyName]: value }); }; const handleCopyNodeInfo = () => { if (target?.type !== 'behavior-tree-node') return; const node = target.data; const childrenInfo = node.children.map((childId, index) => { const childNode = nodes.find(n => n.id === childId); return ` ${index + 1}. ${childNode?.template.displayName || '未知'} (ID: ${childId})`; }).join('\n'); const incomingConnections = connections.filter(conn => conn.to === node.id); const outgoingConnections = connections.filter(conn => conn.from === node.id); const connectionInfo = [ incomingConnections.length > 0 ? `输入连接: ${incomingConnections.length}个` : '', ...incomingConnections.map(conn => { const fromNode = nodes.find(n => n.id === conn.from); return ` 来自: ${fromNode?.template.displayName || '未知'} (${conn.from})`; }), outgoingConnections.length > 0 ? `输出连接: ${outgoingConnections.length}个` : '', ...outgoingConnections.map(conn => { const toNode = nodes.find(n => n.id === conn.to); return ` 到: ${toNode?.template.displayName || '未知'} (${conn.to})`; }) ].filter(Boolean).join('\n'); const nodeInfo = ` 节点信息 ======== 名称: ${node.template.displayName} 类型: ${node.template.type} 分类: ${node.template.category} 类名: ${node.template.className || '无'} 节点ID: ${node.id} 子节点 (${node.children.length}个): ${childrenInfo || ' 无'} 连接信息: ${connectionInfo || ' 无连接'} 属性数据: ${JSON.stringify(node.data, null, 2)} `.trim(); navigator.clipboard.writeText(nodeInfo).then(() => { messageHub.publish('notification:show', { type: 'success', message: '节点信息已复制到剪贴板' }); }).catch(() => { const textarea = document.createElement('textarea'); textarea.value = nodeInfo; textarea.style.position = 'fixed'; textarea.style.opacity = '0'; document.body.appendChild(textarea); textarea.select(); document.execCommand('copy'); document.body.removeChild(textarea); messageHub.publish('notification:show', { type: 'success', message: '节点信息已复制到剪贴板' }); }); }; const renderRemoteProperty = (key: string, value: any) => { if (value === null || value === undefined) { return (
null
); } if (typeof value === 'object' && !Array.isArray(value)) { return (
{Object.entries(value).map(([subKey, subValue]) => (
{subKey}: {String(subValue)}
))}
); } return (
{String(value)}
); }; const formatFileSize = (bytes?: number): string => { if (!bytes) return '0 B'; const units = ['B', 'KB', 'MB', 'GB']; let size = bytes; let unitIndex = 0; while (size >= 1024 && unitIndex < units.length - 1) { size /= 1024; unitIndex++; } return `${size.toFixed(2)} ${units[unitIndex]}`; }; const formatDate = (timestamp?: number): string => { if (!timestamp) return '未知'; const date = new Date(timestamp * 1000); return date.toLocaleString('zh-CN', { year: 'numeric', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit' }); }; const renderAssetFile = (fileInfo: AssetFileInfo, content?: string) => { const IconComponent = fileInfo.isDirectory ? Folder : FileIcon; const iconColor = fileInfo.isDirectory ? '#dcb67a' : '#90caf9'; return (
{fileInfo.name}
文件信息
{fileInfo.isDirectory ? '文件夹' : fileInfo.extension ? `.${fileInfo.extension}` : '文件'}
{fileInfo.size !== undefined && !fileInfo.isDirectory && (
{formatFileSize(fileInfo.size)}
)} {fileInfo.modified !== undefined && (
{formatDate(fileInfo.modified)}
)}
{fileInfo.path}
{content && (
文件预览
{content}
)} {!content && !fileInfo.isDirectory && (
此文件类型不支持预览
)}
); }; const renderBehaviorTreeNode = (node: BehaviorTreeNode) => { const IconComponent = node.template.icon ? (ICON_MAP as any)[node.template.icon] : Box; return (
{IconComponent && } {node.template.displayName || '未命名节点'}
{isRunning && (
运行时模式:属性修改将在停止后还原
)}
基本信息
{node.template.type}
{node.template.category}
{node.template.description && (
{node.template.description}
)} {node.template.className && (
{node.template.className}
)}
{node.template.properties && node.template.properties.length > 0 && (
属性
)} {node.children.length > 0 && (
子节点 ({node.children.length})
{node.children.map((childId, index) => { const childNode = nodes.find(n => n.id === childId); const ChildIcon = childNode?.template.icon ? (ICON_MAP as any)[childNode.template.icon] : Box; return (
{index + 1}. {childNode && ChildIcon && ( )} {childNode?.template.displayName || childId}
); })}
)}
调试信息
{node.id}
({node.position.x.toFixed(0)}, {node.position.y.toFixed(0)})
); }; if (!target) { return (
未选择对象
选择实体或节点以查看详细信息
); } if (target.type === 'behavior-tree-node') { return renderBehaviorTreeNode(target.data); } if (target.type === 'asset-file') { return renderAssetFile(target.data, target.content); } if (target.type === 'remote-entity') { const entity = target.data; const details = (target as any).details; return (
运行时实体 #{entity.id}
基本信息
{entity.id}
{entity.enabled ? 'true' : 'false'}
{entity.name && (
{entity.name}
)}
{details && (
组件详情
{Object.entries(details).map(([key, value]) => renderRemoteProperty(key, value))}
)}
); } if (target.type === 'entity') { const entity = target.data; return (
{entity.name || `Entity #${entity.id}`}
基本信息
{entity.id}
{entity.enabled ? 'true' : 'false'}
{entity.components.length > 0 && (
组件
{entity.components.map((component: any, index: number) => { const isExpanded = expandedComponents.has(index); const componentName = component.constructor?.name || 'Component'; return (
toggleComponentExpanded(index)}> {isExpanded ? : } {componentName}
{isExpanded && (
handlePropertyChange(component, propName, value)} />
)}
); })}
)}
); } return null; }