import { useState, useEffect } from 'react'; import { Entity, Core } from '@esengine/ecs-framework'; import { EntityStoreService, MessageHub, SceneManagerService, CommandManager } from '@esengine/editor-core'; import { useLocale } from '../hooks/useLocale'; import { Box, Layers, Wifi, Search, Plus, Trash2, Monitor, Globe, Image, Camera, Film } from 'lucide-react'; import { ProfilerService, RemoteEntity } from '../services/ProfilerService'; import { confirm } from '@tauri-apps/plugin-dialog'; import { CreateEntityCommand, CreateSpriteEntityCommand, CreateAnimatedSpriteEntityCommand, CreateCameraEntityCommand, DeleteEntityCommand } from '../application/commands/entity'; import '../styles/SceneHierarchy.css'; type ViewMode = 'local' | 'remote'; interface SceneHierarchyProps { entityStore: EntityStoreService; messageHub: MessageHub; commandManager: CommandManager; } export function SceneHierarchy({ entityStore, messageHub, commandManager }: SceneHierarchyProps) { const [entities, setEntities] = useState([]); const [remoteEntities, setRemoteEntities] = useState([]); const [isRemoteConnected, setIsRemoteConnected] = useState(false); const [viewMode, setViewMode] = useState('local'); const [selectedId, setSelectedId] = useState(null); const [searchQuery, setSearchQuery] = useState(''); const [sceneName, setSceneName] = useState('Untitled'); const [remoteSceneName, setRemoteSceneName] = useState(null); const [sceneFilePath, setSceneFilePath] = useState(null); const [isSceneModified, setIsSceneModified] = useState(false); const [contextMenu, setContextMenu] = useState<{ x: number; y: number; entityId: number | null } | null>(null); const { t, locale } = useLocale(); const isShowingRemote = viewMode === 'remote' && isRemoteConnected; // 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 initiallyConnected = profilerService.isConnected(); setIsRemoteConnected(initiallyConnected); 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; }); // 请求第一个实体的详情以获取场景名称 if (!remoteSceneName && data.entities.length > 0 && data.entities[0]) { profilerService.requestEntityDetails(data.entities[0].id); } } else if (!connected) { setRemoteEntities([]); setRemoteSceneName(null); } }); return () => unsubscribe(); }, [remoteSceneName]); // Listen for entity details to get remote scene name useEffect(() => { const handleEntityDetails = ((event: CustomEvent) => { const details = event.detail; if (details && details.sceneName) { setRemoteSceneName(details.sceneName); } }) as EventListener; window.addEventListener('profiler:entity-details', handleEntityDetails); return () => window.removeEventListener('profiler:entity-details', handleEntityDetails); }, []); 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 entityCount = entityStore.getAllEntities().length; const entityName = `Entity ${entityCount + 1}`; const command = new CreateEntityCommand( entityStore, messageHub, entityName ); commandManager.execute(command); }; const handleCreateSpriteEntity = () => { // Count only Sprite entities for naming const spriteCount = entityStore.getAllEntities().filter((e) => e.name.startsWith('Sprite ')).length; const entityName = `Sprite ${spriteCount + 1}`; const command = new CreateSpriteEntityCommand( entityStore, messageHub, entityName ); commandManager.execute(command); }; const handleCreateAnimatedSpriteEntity = () => { const animCount = entityStore.getAllEntities().filter((e) => e.name.startsWith('AnimatedSprite ')).length; const entityName = `AnimatedSprite ${animCount + 1}`; const command = new CreateAnimatedSpriteEntityCommand( entityStore, messageHub, entityName ); commandManager.execute(command); }; const handleCreateCameraEntity = () => { const entityCount = entityStore.getAllEntities().length; const entityName = `Camera ${entityCount + 1}`; const command = new CreateCameraEntityCommand( entityStore, messageHub, entityName ); commandManager.execute(command); }; 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}"?`, { title: locale === 'zh' ? '删除实体' : 'Delete Entity', kind: 'warning' } ); if (confirmed) { const command = new DeleteEntityCommand( entityStore, messageHub, entity ); commandManager.execute(command); } }; const handleContextMenu = (e: React.MouseEvent, entityId: number | null) => { e.preventDefault(); setContextMenu({ x: e.clientX, y: e.clientY, entityId }); }; const closeContextMenu = () => { setContextMenu(null); }; // Close context menu on click outside useEffect(() => { const handleClick = () => closeContextMenu(); if (contextMenu) { window.addEventListener('click', handleClick); return () => window.removeEventListener('click', handleClick); } }, [contextMenu]); // Listen for Delete key useEffect(() => { const handleKeyDown = (e: KeyboardEvent) => { if (e.key === 'Delete' && selectedId && !isShowingRemote) { handleDeleteEntity(); } }; window.addEventListener('keydown', handleKeyDown); return () => window.removeEventListener('keydown', handleKeyDown); }, [selectedId, isShowingRemote]); // 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 = isShowingRemote ? filterRemoteEntities(remoteEntities) : filterLocalEntities(entities); const showRemoteIndicator = isShowingRemote && remoteEntities.length > 0; const displaySceneName = isShowingRemote && remoteSceneName ? remoteSceneName : sceneName; return (

{t('hierarchy.title')}

{displaySceneName}{!isRemoteConnected && isSceneModified ? '*' : ''}
{isRemoteConnected && (
)} {showRemoteIndicator && (
)}
setSearchQuery(e.target.value)} />
{!isShowingRemote && (
)}
!isShowingRemote && handleContextMenu(e, null)}> {displayEntities.length === 0 ? (
{t('hierarchy.empty')}
{isShowingRemote ? 'No entities in remote game' : 'Create an entity to get started'}
) : isShowingRemote ? (
    {(displayEntities as RemoteEntity[]).map((entity) => (
  • handleRemoteEntityClick(entity)} > {entity.name} {entity.tag !== 0 && ( #{entity.tag} )} {entity.componentCount > 0 && ( {entity.componentCount} )}
  • ))}
) : (
    {entities.map((entity) => (
  • handleEntityClick(entity)} onContextMenu={(e) => { e.stopPropagation(); handleEntityClick(entity); handleContextMenu(e, entity.id); }} > {entity.name || `Entity ${entity.id}`}
  • ))}
)}
{contextMenu && !isShowingRemote && (
{contextMenu.entityId && ( <>
)}
)}
); }