import { useState, useEffect } from 'react'; import { Entity, Core } from '@esengine/ecs-framework'; import { EntityStoreService, MessageHub, SceneManagerService } from '@esengine/editor-core'; import { useLocale } from '../hooks/useLocale'; import { Box, Layers, Wifi, Search, Plus, Trash2 } from 'lucide-react'; import { ProfilerService, RemoteEntity } from '../services/ProfilerService'; import { confirm } from '@tauri-apps/plugin-dialog'; import '../styles/SceneHierarchy.css'; interface SceneHierarchyProps { entityStore: EntityStoreService; messageHub: MessageHub; } export function SceneHierarchy({ entityStore, messageHub }: SceneHierarchyProps) { const [entities, setEntities] = useState([]); const [remoteEntities, setRemoteEntities] = useState([]); const [isRemoteConnected, setIsRemoteConnected] = useState(false); const [selectedId, setSelectedId] = useState(null); const [searchQuery, setSearchQuery] = useState(''); const [sceneName, setSceneName] = useState('Untitled'); const [sceneFilePath, setSceneFilePath] = useState(null); const [isSceneModified, setIsSceneModified] = useState(false); const { t, locale } = useLocale(); // Subscribe to scene changes useEffect(() => { const sceneManager = Core.services.resolve(SceneManagerService); const updateSceneInfo = () => { if (sceneManager) { const state = sceneManager.getSceneState(); setSceneName(state.sceneName); setIsSceneModified(state.isModified); } }; updateSceneInfo(); const unsubLoaded = messageHub.subscribe('scene:loaded', (data: any) => { if (data.sceneName) { setSceneName(data.sceneName); setSceneFilePath(data.path || null); setIsSceneModified(data.isModified || false); } else { updateSceneInfo(); } }); const unsubNew = messageHub.subscribe('scene:new', () => { updateSceneInfo(); }); const unsubSaved = messageHub.subscribe('scene:saved', () => { updateSceneInfo(); }); const unsubModified = messageHub.subscribe('scene:modified', () => { updateSceneInfo(); }); return () => { unsubLoaded(); unsubNew(); unsubSaved(); unsubModified(); }; }, [messageHub]); // Subscribe to local entity changes useEffect(() => { const updateEntities = () => { setEntities(entityStore.getRootEntities()); }; const handleSelection = (data: { entity: Entity | null }) => { setSelectedId(data.entity?.id ?? null); }; updateEntities(); const unsubAdd = messageHub.subscribe('entity:added', updateEntities); const unsubRemove = messageHub.subscribe('entity:removed', updateEntities); const unsubClear = messageHub.subscribe('entities:cleared', updateEntities); const unsubSelect = messageHub.subscribe('entity:selected', handleSelection); return () => { unsubAdd(); unsubRemove(); unsubClear(); unsubSelect(); }; }, [entityStore, messageHub]); // Subscribe to remote entity data from ProfilerService useEffect(() => { const profilerService = (window as any).__PROFILER_SERVICE__ as ProfilerService | undefined; if (!profilerService) { return; } const unsubscribe = profilerService.subscribe((data) => { const connected = profilerService.isConnected(); setIsRemoteConnected(connected); if (connected && data.entities && data.entities.length > 0) { // 只在实体列表发生实质性变化时才更新 setRemoteEntities(prev => { if (prev.length !== data.entities!.length) { return data.entities!; } // 检查实体ID和名称是否变化 const hasChanged = data.entities!.some((entity, index) => { const prevEntity = prev[index]; return !prevEntity || prevEntity.id !== entity.id || prevEntity.name !== entity.name || prevEntity.componentCount !== entity.componentCount; }); return hasChanged ? data.entities! : prev; }); } else if (!connected) { setRemoteEntities([]); } }); return () => unsubscribe(); }, []); const handleEntityClick = (entity: Entity) => { entityStore.selectEntity(entity); }; const handleRemoteEntityClick = (entity: RemoteEntity) => { setSelectedId(entity.id); // 请求完整的实体详情(包含组件属性) const profilerService = (window as any).__PROFILER_SERVICE__ as ProfilerService | undefined; if (profilerService) { profilerService.requestEntityDetails(entity.id); } // 先发布基本信息,详细信息稍后通过 ProfilerService 异步返回 messageHub.publish('remote-entity:selected', { entity: { id: entity.id, name: entity.name, enabled: entity.enabled, componentCount: entity.componentCount, componentTypes: entity.componentTypes } }); }; const handleSceneNameClick = () => { if (sceneFilePath) { messageHub.publish('asset:reveal', { path: sceneFilePath }); } }; const handleCreateEntity = () => { const scene = Core.scene; if (!scene) return; const entityCount = entityStore.getAllEntities().length; const entityName = `Entity ${entityCount + 1}`; const entity = scene.createEntity(entityName); entityStore.addEntity(entity); entityStore.selectEntity(entity); }; const handleDeleteEntity = async () => { if (!selectedId) return; const entity = entityStore.getEntity(selectedId); if (!entity) return; const confirmed = await confirm( locale === 'zh' ? `确定要删除实体 "${entity.name}" 吗?此操作无法撤销。` : `Are you sure you want to delete entity "${entity.name}"? This action cannot be undone.`, { title: locale === 'zh' ? '删除实体' : 'Delete Entity', kind: 'warning' } ); if (confirmed) { entity.destroy(); entityStore.removeEntity(entity); } }; // Listen for Delete key useEffect(() => { const handleKeyDown = (e: KeyboardEvent) => { if (e.key === 'Delete' && selectedId && !isRemoteConnected) { handleDeleteEntity(); } }; window.addEventListener('keydown', handleKeyDown); return () => window.removeEventListener('keydown', handleKeyDown); }, [selectedId, isRemoteConnected]); // Filter entities based on search query const filterRemoteEntities = (entityList: RemoteEntity[]): RemoteEntity[] => { if (!searchQuery.trim()) return entityList; const query = searchQuery.toLowerCase(); return entityList.filter(entity => { const name = entity.name; const id = entity.id.toString(); // Search by name or ID if (name.toLowerCase().includes(query) || id.includes(query)) { return true; } // Search by component types if (Array.isArray(entity.componentTypes)) { return entity.componentTypes.some(type => type.toLowerCase().includes(query) ); } return false; }); }; const filterLocalEntities = (entityList: Entity[]): Entity[] => { if (!searchQuery.trim()) return entityList; const query = searchQuery.toLowerCase(); return entityList.filter(entity => { const id = entity.id.toString(); return id.includes(query); }); }; // Determine which entities to display const displayEntities = isRemoteConnected ? filterRemoteEntities(remoteEntities) : filterLocalEntities(entities); const showRemoteIndicator = isRemoteConnected && remoteEntities.length > 0; return (

{t('hierarchy.title')}

{sceneName}{isSceneModified ? '*' : ''}
{showRemoteIndicator && (
)}
setSearchQuery(e.target.value)} />
{!isRemoteConnected && (
)}
{displayEntities.length === 0 ? (
{t('hierarchy.empty')}
{isRemoteConnected ? 'No entities in remote game' : 'Create an entity to get started'}
) : isRemoteConnected ? (
    {(displayEntities as RemoteEntity[]).map(entity => (
  • handleRemoteEntityClick(entity)} > {entity.name} {entity.tag !== 0 && ( #{entity.tag} )} {entity.componentCount > 0 && ( {entity.componentCount} )}
  • ))}
) : (
    {entities.map(entity => (
  • handleEntityClick(entity)} > Entity {entity.id}
  • ))}
)}
); }