2025-10-14 23:31:09 +08:00
|
|
|
import { useState, useEffect } from 'react';
|
|
|
|
|
import { Entity } from '@esengine/ecs-framework';
|
|
|
|
|
import { EntityStoreService, MessageHub } from '@esengine/editor-core';
|
2025-10-14 23:56:54 +08:00
|
|
|
import { useLocale } from '../hooks/useLocale';
|
2025-10-15 23:24:13 +08:00
|
|
|
import { Box, Layers, Wifi } from 'lucide-react';
|
|
|
|
|
import { ProfilerService, RemoteEntity } from '../services/ProfilerService';
|
2025-10-14 23:31:09 +08:00
|
|
|
import '../styles/SceneHierarchy.css';
|
|
|
|
|
|
|
|
|
|
interface SceneHierarchyProps {
|
|
|
|
|
entityStore: EntityStoreService;
|
|
|
|
|
messageHub: MessageHub;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function SceneHierarchy({ entityStore, messageHub }: SceneHierarchyProps) {
|
|
|
|
|
const [entities, setEntities] = useState<Entity[]>([]);
|
2025-10-15 23:24:13 +08:00
|
|
|
const [remoteEntities, setRemoteEntities] = useState<RemoteEntity[]>([]);
|
|
|
|
|
const [isRemoteConnected, setIsRemoteConnected] = useState(false);
|
2025-10-14 23:31:09 +08:00
|
|
|
const [selectedId, setSelectedId] = useState<number | null>(null);
|
2025-10-14 23:56:54 +08:00
|
|
|
const { t } = useLocale();
|
2025-10-14 23:31:09 +08:00
|
|
|
|
2025-10-15 23:24:13 +08:00
|
|
|
// Subscribe to local entity changes
|
2025-10-14 23:31:09 +08:00
|
|
|
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]);
|
|
|
|
|
|
2025-10-15 23:24:13 +08:00
|
|
|
// 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) {
|
2025-10-16 17:10:22 +08:00
|
|
|
// 只在实体列表发生实质性变化时才更新
|
|
|
|
|
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) {
|
2025-10-15 23:24:13 +08:00
|
|
|
setRemoteEntities([]);
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
return () => unsubscribe();
|
|
|
|
|
}, []);
|
|
|
|
|
|
2025-10-14 23:31:09 +08:00
|
|
|
const handleEntityClick = (entity: Entity) => {
|
|
|
|
|
entityStore.selectEntity(entity);
|
|
|
|
|
};
|
|
|
|
|
|
2025-10-16 11:55:41 +08:00
|
|
|
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
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
};
|
|
|
|
|
|
2025-10-15 23:24:13 +08:00
|
|
|
// Determine which entities to display
|
|
|
|
|
const displayEntities = isRemoteConnected ? remoteEntities : entities;
|
|
|
|
|
const showRemoteIndicator = isRemoteConnected && remoteEntities.length > 0;
|
|
|
|
|
|
2025-10-14 23:31:09 +08:00
|
|
|
return (
|
|
|
|
|
<div className="scene-hierarchy">
|
|
|
|
|
<div className="hierarchy-header">
|
2025-10-15 17:15:05 +08:00
|
|
|
<Layers size={16} className="hierarchy-header-icon" />
|
2025-10-14 23:56:54 +08:00
|
|
|
<h3>{t('hierarchy.title')}</h3>
|
2025-10-15 23:24:13 +08:00
|
|
|
{showRemoteIndicator && (
|
|
|
|
|
<div className="remote-indicator" title="Showing remote entities">
|
|
|
|
|
<Wifi size={12} />
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
2025-10-14 23:31:09 +08:00
|
|
|
</div>
|
2025-10-15 17:15:05 +08:00
|
|
|
<div className="hierarchy-content scrollable">
|
2025-10-15 23:24:13 +08:00
|
|
|
{displayEntities.length === 0 ? (
|
2025-10-15 17:15:05 +08:00
|
|
|
<div className="empty-state">
|
|
|
|
|
<Box size={48} strokeWidth={1.5} className="empty-icon" />
|
|
|
|
|
<div className="empty-title">{t('hierarchy.empty')}</div>
|
2025-10-15 23:24:13 +08:00
|
|
|
<div className="empty-hint">
|
|
|
|
|
{isRemoteConnected
|
|
|
|
|
? 'No entities in remote game'
|
|
|
|
|
: 'Create an entity to get started'}
|
|
|
|
|
</div>
|
2025-10-15 17:15:05 +08:00
|
|
|
</div>
|
2025-10-15 23:24:13 +08:00
|
|
|
) : isRemoteConnected ? (
|
|
|
|
|
<ul className="entity-list">
|
|
|
|
|
{remoteEntities.map(entity => (
|
|
|
|
|
<li
|
|
|
|
|
key={entity.id}
|
2025-10-16 11:55:41 +08:00
|
|
|
className={`entity-item remote-entity ${selectedId === entity.id ? 'selected' : ''} ${!entity.enabled ? 'disabled' : ''}`}
|
|
|
|
|
title={`${entity.name} - ${entity.componentTypes.join(', ')}`}
|
|
|
|
|
onClick={() => handleRemoteEntityClick(entity)}
|
2025-10-15 23:24:13 +08:00
|
|
|
>
|
|
|
|
|
<Box size={14} className="entity-icon" />
|
|
|
|
|
<span className="entity-name">{entity.name}</span>
|
2025-10-16 11:55:41 +08:00
|
|
|
{entity.componentCount > 0 && (
|
|
|
|
|
<span className="component-count">{entity.componentCount}</span>
|
2025-10-15 23:24:13 +08:00
|
|
|
)}
|
|
|
|
|
</li>
|
|
|
|
|
))}
|
|
|
|
|
</ul>
|
2025-10-14 23:31:09 +08:00
|
|
|
) : (
|
|
|
|
|
<ul className="entity-list">
|
|
|
|
|
{entities.map(entity => (
|
|
|
|
|
<li
|
|
|
|
|
key={entity.id}
|
|
|
|
|
className={`entity-item ${selectedId === entity.id ? 'selected' : ''}`}
|
|
|
|
|
onClick={() => handleEntityClick(entity)}
|
|
|
|
|
>
|
2025-10-15 17:15:05 +08:00
|
|
|
<Box size={14} className="entity-icon" />
|
2025-10-14 23:31:09 +08:00
|
|
|
<span className="entity-name">Entity {entity.id}</span>
|
|
|
|
|
</li>
|
|
|
|
|
))}
|
|
|
|
|
</ul>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|