调试实体和组件属性
This commit is contained in:
@@ -206,7 +206,8 @@ export class DebugManager implements IService, IUpdatable {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const expandedData = this.entityCollector.expandLazyObject(entityId, componentIndex, propertyPath);
|
const scene = this.sceneManager.currentScene;
|
||||||
|
const expandedData = this.entityCollector.expandLazyObject(entityId, componentIndex, propertyPath, scene);
|
||||||
|
|
||||||
this.webSocketManager.send({
|
this.webSocketManager.send({
|
||||||
type: 'expand_lazy_object_response',
|
type: 'expand_lazy_object_response',
|
||||||
@@ -238,7 +239,8 @@ export class DebugManager implements IService, IUpdatable {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const properties = this.entityCollector.getComponentProperties(entityId, componentIndex);
|
const scene = this.sceneManager.currentScene;
|
||||||
|
const properties = this.entityCollector.getComponentProperties(entityId, componentIndex, scene);
|
||||||
|
|
||||||
this.webSocketManager.send({
|
this.webSocketManager.send({
|
||||||
type: 'get_component_properties_response',
|
type: 'get_component_properties_response',
|
||||||
@@ -261,7 +263,8 @@ export class DebugManager implements IService, IUpdatable {
|
|||||||
try {
|
try {
|
||||||
const { requestId } = message;
|
const { requestId } = message;
|
||||||
|
|
||||||
const rawEntityList = this.entityCollector.getRawEntityList();
|
const scene = this.sceneManager.currentScene;
|
||||||
|
const rawEntityList = this.entityCollector.getRawEntityList(scene);
|
||||||
|
|
||||||
this.webSocketManager.send({
|
this.webSocketManager.send({
|
||||||
type: 'get_raw_entity_list_response',
|
type: 'get_raw_entity_list_response',
|
||||||
@@ -293,7 +296,8 @@ export class DebugManager implements IService, IUpdatable {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const entityDetails = this.entityCollector.getEntityDetails(entityId);
|
const scene = this.sceneManager.currentScene;
|
||||||
|
const entityDetails = this.entityCollector.getEntityDetails(entityId, scene);
|
||||||
|
|
||||||
this.webSocketManager.send({
|
this.webSocketManager.send({
|
||||||
type: 'get_entity_details_response',
|
type: 'get_entity_details_response',
|
||||||
|
|||||||
@@ -263,8 +263,7 @@ export class EntityDataCollector {
|
|||||||
componentCount: entity.components?.length || 0,
|
componentCount: entity.components?.length || 0,
|
||||||
memory: 0
|
memory: 0
|
||||||
}))
|
}))
|
||||||
.sort((a: any, b: any) => b.componentCount - a.componentCount)
|
.sort((a: any, b: any) => b.componentCount - a.componentCount);
|
||||||
.slice(0, 10);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -303,7 +302,7 @@ export class EntityDataCollector {
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (archetype.entities) {
|
if (archetype.entities) {
|
||||||
archetype.entities.slice(0, 5).forEach((entity: any) => {
|
archetype.entities.forEach((entity: any) => {
|
||||||
topEntities.push({
|
topEntities.push({
|
||||||
id: entity.id.toString(),
|
id: entity.id.toString(),
|
||||||
name: entity.name || `Entity_${entity.id}`,
|
name: entity.name || `Entity_${entity.id}`,
|
||||||
@@ -352,7 +351,7 @@ export class EntityDataCollector {
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (archetype.entities) {
|
if (archetype.entities) {
|
||||||
archetype.entities.slice(0, 5).forEach((entity: any) => {
|
archetype.entities.forEach((entity: any) => {
|
||||||
topEntities.push({
|
topEntities.push({
|
||||||
id: entity.id.toString(),
|
id: entity.id.toString(),
|
||||||
name: entity.name || `Entity_${entity.id}`,
|
name: entity.name || `Entity_${entity.id}`,
|
||||||
|
|||||||
@@ -131,10 +131,10 @@ async fn handle_connection(
|
|||||||
while let Some(msg) = ws_receiver.next().await {
|
while let Some(msg) = ws_receiver.next().await {
|
||||||
match msg {
|
match msg {
|
||||||
Ok(Message::Text(text)) => {
|
Ok(Message::Text(text)) => {
|
||||||
// Parse incoming debug data from game client
|
// Parse incoming messages
|
||||||
if let Ok(json_value) = serde_json::from_str::<serde_json::Value>(&text) {
|
if let Ok(json_value) = serde_json::from_str::<serde_json::Value>(&text) {
|
||||||
if json_value.get("type").and_then(|t| t.as_str()) == Some("debug_data") {
|
if json_value.get("type").and_then(|t| t.as_str()) == Some("debug_data") {
|
||||||
// Broadcast to frontend (ProfilerWindow)
|
// Broadcast debug data from game client to all clients (including frontend)
|
||||||
tx.send(text).ok();
|
tx.send(text).ok();
|
||||||
} else if json_value.get("type").and_then(|t| t.as_str()) == Some("ping") {
|
} else if json_value.get("type").and_then(|t| t.as_str()) == Some("ping") {
|
||||||
// Respond to ping
|
// Respond to ping
|
||||||
@@ -145,6 +145,10 @@ async fn handle_connection(
|
|||||||
})
|
})
|
||||||
.to_string(),
|
.to_string(),
|
||||||
);
|
);
|
||||||
|
} else {
|
||||||
|
// Forward all other messages (like get_raw_entity_list, get_entity_details, etc.)
|
||||||
|
// to all connected clients (this enables frontend -> game client communication)
|
||||||
|
tx.send(text).ok();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,19 +13,42 @@ interface EntityInspectorProps {
|
|||||||
|
|
||||||
export function EntityInspector({ entityStore: _entityStore, messageHub }: EntityInspectorProps) {
|
export function EntityInspector({ entityStore: _entityStore, messageHub }: EntityInspectorProps) {
|
||||||
const [selectedEntity, setSelectedEntity] = useState<Entity | null>(null);
|
const [selectedEntity, setSelectedEntity] = useState<Entity | null>(null);
|
||||||
|
const [remoteEntity, setRemoteEntity] = useState<any | null>(null);
|
||||||
|
const [remoteEntityDetails, setRemoteEntityDetails] = useState<any | null>(null);
|
||||||
const [showAddComponent, setShowAddComponent] = useState(false);
|
const [showAddComponent, setShowAddComponent] = useState(false);
|
||||||
const [expandedComponents, setExpandedComponents] = useState<Set<number>>(new Set());
|
const [expandedComponents, setExpandedComponents] = useState<Set<number>>(new Set());
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const handleSelection = (data: { entity: Entity | null }) => {
|
const handleSelection = (data: { entity: Entity | null }) => {
|
||||||
setSelectedEntity(data.entity);
|
setSelectedEntity(data.entity);
|
||||||
|
setRemoteEntity(null);
|
||||||
|
setRemoteEntityDetails(null);
|
||||||
setShowAddComponent(false);
|
setShowAddComponent(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleRemoteSelection = (data: { entity: any }) => {
|
||||||
|
setRemoteEntity(data.entity);
|
||||||
|
setRemoteEntityDetails(null);
|
||||||
|
setSelectedEntity(null);
|
||||||
|
setShowAddComponent(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleEntityDetails = (event: Event) => {
|
||||||
|
const customEvent = event as CustomEvent;
|
||||||
|
const details = customEvent.detail;
|
||||||
|
console.log('[EntityInspector] Received entity details:', details);
|
||||||
|
setRemoteEntityDetails(details);
|
||||||
|
};
|
||||||
|
|
||||||
const unsubSelect = messageHub.subscribe('entity:selected', handleSelection);
|
const unsubSelect = messageHub.subscribe('entity:selected', handleSelection);
|
||||||
|
const unsubRemoteSelect = messageHub.subscribe('remote-entity:selected', handleRemoteSelection);
|
||||||
|
|
||||||
|
window.addEventListener('profiler:entity-details', handleEntityDetails);
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
unsubSelect();
|
unsubSelect();
|
||||||
|
unsubRemoteSelect();
|
||||||
|
window.removeEventListener('profiler:entity-details', handleEntityDetails);
|
||||||
};
|
};
|
||||||
}, [messageHub]);
|
}, [messageHub]);
|
||||||
|
|
||||||
@@ -82,7 +105,215 @@ export function EntityInspector({ entityStore: _entityStore, messageHub }: Entit
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
if (!selectedEntity) {
|
const renderRemoteProperty = (key: string, value: any) => {
|
||||||
|
if (value === null || value === undefined) {
|
||||||
|
return (
|
||||||
|
<div key={key} className="property-field">
|
||||||
|
<label className="property-label">{key}</label>
|
||||||
|
<span className="property-value-text">null</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Array.isArray(value)) {
|
||||||
|
return (
|
||||||
|
<div key={key} className="property-field">
|
||||||
|
<label className="property-label">{key}</label>
|
||||||
|
<div style={{ flex: 1, display: 'flex', flexWrap: 'wrap', gap: '4px' }}>
|
||||||
|
{value.length === 0 ? (
|
||||||
|
<span className="property-value-text" style={{ opacity: 0.5 }}>Empty Array</span>
|
||||||
|
) : (
|
||||||
|
value.map((item, index) => (
|
||||||
|
<span
|
||||||
|
key={index}
|
||||||
|
style={{
|
||||||
|
padding: '2px 6px',
|
||||||
|
background: 'var(--color-bg-inset)',
|
||||||
|
border: '1px solid var(--color-border-default)',
|
||||||
|
borderRadius: '3px',
|
||||||
|
fontSize: '10px',
|
||||||
|
color: 'var(--color-text-primary)',
|
||||||
|
fontFamily: 'var(--font-family-mono)'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{typeof item === 'object' ? JSON.stringify(item) : String(item)}
|
||||||
|
</span>
|
||||||
|
))
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const valueType = typeof value;
|
||||||
|
|
||||||
|
if (valueType === 'boolean') {
|
||||||
|
return (
|
||||||
|
<div key={key} className="property-field property-field-boolean">
|
||||||
|
<label className="property-label">{key}</label>
|
||||||
|
<div className={`property-toggle ${value ? 'property-toggle-on' : 'property-toggle-off'} property-toggle-readonly`}>
|
||||||
|
<span className="property-toggle-thumb" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (valueType === 'number') {
|
||||||
|
return (
|
||||||
|
<div key={key} className="property-field">
|
||||||
|
<label className="property-label">{key}</label>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
className="property-input property-input-number"
|
||||||
|
value={value}
|
||||||
|
disabled
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (valueType === 'string') {
|
||||||
|
return (
|
||||||
|
<div key={key} className="property-field">
|
||||||
|
<label className="property-label">{key}</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
className="property-input property-input-text"
|
||||||
|
value={value}
|
||||||
|
disabled
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (valueType === 'object' && value.minX !== undefined && value.maxX !== undefined && value.minY !== undefined && value.maxY !== undefined) {
|
||||||
|
return (
|
||||||
|
<div key={key} className="property-field" style={{ flexDirection: 'column', alignItems: 'stretch' }}>
|
||||||
|
<label className="property-label" style={{ flex: 'none', marginBottom: '4px' }}>{key}</label>
|
||||||
|
<div style={{ display: 'flex', flexDirection: 'column', gap: '4px' }}>
|
||||||
|
<div className="property-vector-compact">
|
||||||
|
<div className="property-vector-axis-compact">
|
||||||
|
<span className="property-vector-axis-label property-vector-axis-x">X</span>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
className="property-input property-input-number-compact"
|
||||||
|
value={value.minX}
|
||||||
|
disabled
|
||||||
|
placeholder="Min"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="property-vector-axis-compact">
|
||||||
|
<span className="property-vector-axis-label property-vector-axis-x">X</span>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
className="property-input property-input-number-compact"
|
||||||
|
value={value.maxX}
|
||||||
|
disabled
|
||||||
|
placeholder="Max"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="property-vector-compact">
|
||||||
|
<div className="property-vector-axis-compact">
|
||||||
|
<span className="property-vector-axis-label property-vector-axis-y">Y</span>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
className="property-input property-input-number-compact"
|
||||||
|
value={value.minY}
|
||||||
|
disabled
|
||||||
|
placeholder="Min"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="property-vector-axis-compact">
|
||||||
|
<span className="property-vector-axis-label property-vector-axis-y">Y</span>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
className="property-input property-input-number-compact"
|
||||||
|
value={value.maxY}
|
||||||
|
disabled
|
||||||
|
placeholder="Max"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (valueType === 'object' && value.x !== undefined && value.y !== undefined) {
|
||||||
|
if (value.z !== undefined) {
|
||||||
|
return (
|
||||||
|
<div key={key} className="property-field">
|
||||||
|
<label className="property-label">{key}</label>
|
||||||
|
<div className="property-vector-compact">
|
||||||
|
<div className="property-vector-axis-compact">
|
||||||
|
<span className="property-vector-axis-label property-vector-axis-x">X</span>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
className="property-input property-input-number-compact"
|
||||||
|
value={value.x}
|
||||||
|
disabled
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="property-vector-axis-compact">
|
||||||
|
<span className="property-vector-axis-label property-vector-axis-y">Y</span>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
className="property-input property-input-number-compact"
|
||||||
|
value={value.y}
|
||||||
|
disabled
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="property-vector-axis-compact">
|
||||||
|
<span className="property-vector-axis-label property-vector-axis-z">Z</span>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
className="property-input property-input-number-compact"
|
||||||
|
value={value.z}
|
||||||
|
disabled
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return (
|
||||||
|
<div key={key} className="property-field">
|
||||||
|
<label className="property-label">{key}</label>
|
||||||
|
<div className="property-vector-compact">
|
||||||
|
<div className="property-vector-axis-compact">
|
||||||
|
<span className="property-vector-axis-label property-vector-axis-x">X</span>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
className="property-input property-input-number-compact"
|
||||||
|
value={value.x}
|
||||||
|
disabled
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="property-vector-axis-compact">
|
||||||
|
<span className="property-vector-axis-label property-vector-axis-y">Y</span>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
className="property-input property-input-number-compact"
|
||||||
|
value={value.y}
|
||||||
|
disabled
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div key={key} className="property-field">
|
||||||
|
<label className="property-label">{key}</label>
|
||||||
|
<span className="property-value-text">{JSON.stringify(value)}</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!selectedEntity && !remoteEntity) {
|
||||||
return (
|
return (
|
||||||
<div className="entity-inspector">
|
<div className="entity-inspector">
|
||||||
<div className="inspector-header">
|
<div className="inspector-header">
|
||||||
@@ -100,7 +331,102 @@ export function EntityInspector({ entityStore: _entityStore, messageHub }: Entit
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const components = selectedEntity.components;
|
// 显示远程实体
|
||||||
|
if (remoteEntity) {
|
||||||
|
const displayData = remoteEntityDetails || remoteEntity;
|
||||||
|
const hasDetailedComponents = remoteEntityDetails && remoteEntityDetails.components && remoteEntityDetails.components.length > 0;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="entity-inspector">
|
||||||
|
<div className="inspector-header">
|
||||||
|
<FileSearch size={16} className="inspector-header-icon" />
|
||||||
|
<h3>Inspector</h3>
|
||||||
|
</div>
|
||||||
|
<div className="inspector-content scrollable">
|
||||||
|
<div className="inspector-section">
|
||||||
|
<div className="section-header">
|
||||||
|
<Settings size={12} className="section-icon" />
|
||||||
|
<span>Entity Info (Remote)</span>
|
||||||
|
</div>
|
||||||
|
<div className="section-content">
|
||||||
|
<div className="info-row">
|
||||||
|
<span className="info-label">ID:</span>
|
||||||
|
<span className="info-value">{displayData.id}</span>
|
||||||
|
</div>
|
||||||
|
<div className="info-row">
|
||||||
|
<span className="info-label">Name:</span>
|
||||||
|
<span className="info-value">{displayData.name}</span>
|
||||||
|
</div>
|
||||||
|
<div className="info-row">
|
||||||
|
<span className="info-label">Enabled:</span>
|
||||||
|
<span className="info-value">{displayData.enabled ? 'Yes' : 'No'}</span>
|
||||||
|
</div>
|
||||||
|
{displayData.scene && (
|
||||||
|
<div className="info-row">
|
||||||
|
<span className="info-label">Scene:</span>
|
||||||
|
<span className="info-value">{displayData.scene}</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="inspector-section">
|
||||||
|
<div className="section-header">
|
||||||
|
<Settings size={12} className="section-icon" />
|
||||||
|
<span>Components ({displayData.componentCount})</span>
|
||||||
|
</div>
|
||||||
|
<div className="section-content">
|
||||||
|
{hasDetailedComponents ? (
|
||||||
|
<ul className="component-list">
|
||||||
|
{remoteEntityDetails!.components.map((component: any, index: number) => {
|
||||||
|
const isExpanded = expandedComponents.has(index);
|
||||||
|
return (
|
||||||
|
<li key={index} className={`component-item ${isExpanded ? 'expanded' : ''}`}>
|
||||||
|
<div className="component-header" onClick={() => toggleComponentExpanded(index)}>
|
||||||
|
<button
|
||||||
|
className="component-expand-btn"
|
||||||
|
title={isExpanded ? 'Collapse' : 'Expand'}
|
||||||
|
>
|
||||||
|
{isExpanded ? <ChevronDown size={14} /> : <ChevronRight size={14} />}
|
||||||
|
</button>
|
||||||
|
<Settings size={14} className="component-icon" />
|
||||||
|
<span className="component-name">{component.typeName}</span>
|
||||||
|
</div>
|
||||||
|
{isExpanded && (
|
||||||
|
<div className="component-properties animate-slideDown">
|
||||||
|
<div className="property-inspector">
|
||||||
|
{Object.entries(component.properties).map(([key, value]) =>
|
||||||
|
renderRemoteProperty(key, value)
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</li>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</ul>
|
||||||
|
) : displayData.componentTypes && displayData.componentTypes.length > 0 ? (
|
||||||
|
<ul className="component-list">
|
||||||
|
{displayData.componentTypes.map((componentType: string, index: number) => (
|
||||||
|
<li key={index} className="component-item">
|
||||||
|
<div className="component-header">
|
||||||
|
<Settings size={14} className="component-icon" />
|
||||||
|
<span className="component-name">{componentType}</span>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
) : (
|
||||||
|
<div className="empty-state-small">No components</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const components = selectedEntity!.components;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="entity-inspector">
|
<div className="entity-inspector">
|
||||||
@@ -121,11 +447,11 @@ export function EntityInspector({ entityStore: _entityStore, messageHub }: Entit
|
|||||||
</div>
|
</div>
|
||||||
<div className="info-row">
|
<div className="info-row">
|
||||||
<span className="info-label">Name:</span>
|
<span className="info-label">Name:</span>
|
||||||
<span className="info-value">Entity {selectedEntity.id}</span>
|
<span className="info-value">Entity {selectedEntity!.id}</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="info-row">
|
<div className="info-row">
|
||||||
<span className="info-label">Enabled:</span>
|
<span className="info-label">Enabled:</span>
|
||||||
<span className="info-value">{selectedEntity.enabled ? 'Yes' : 'No'}</span>
|
<span className="info-value">{selectedEntity!.enabled ? 'Yes' : 'No'}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -74,6 +74,27 @@ export function SceneHierarchy({ entityStore, messageHub }: SceneHierarchyProps)
|
|||||||
entityStore.selectEntity(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
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
// Determine which entities to display
|
// Determine which entities to display
|
||||||
const displayEntities = isRemoteConnected ? remoteEntities : entities;
|
const displayEntities = isRemoteConnected ? remoteEntities : entities;
|
||||||
const showRemoteIndicator = isRemoteConnected && remoteEntities.length > 0;
|
const showRemoteIndicator = isRemoteConnected && remoteEntities.length > 0;
|
||||||
@@ -105,13 +126,14 @@ export function SceneHierarchy({ entityStore, messageHub }: SceneHierarchyProps)
|
|||||||
{remoteEntities.map(entity => (
|
{remoteEntities.map(entity => (
|
||||||
<li
|
<li
|
||||||
key={entity.id}
|
key={entity.id}
|
||||||
className={`entity-item remote-entity ${!entity.enabled ? 'disabled' : ''}`}
|
className={`entity-item remote-entity ${selectedId === entity.id ? 'selected' : ''} ${!entity.enabled ? 'disabled' : ''}`}
|
||||||
title={`${entity.name} - ${entity.components.join(', ')}`}
|
title={`${entity.name} - ${entity.componentTypes.join(', ')}`}
|
||||||
|
onClick={() => handleRemoteEntityClick(entity)}
|
||||||
>
|
>
|
||||||
<Box size={14} className="entity-icon" />
|
<Box size={14} className="entity-icon" />
|
||||||
<span className="entity-name">{entity.name}</span>
|
<span className="entity-name">{entity.name}</span>
|
||||||
{entity.components.length > 0 && (
|
{entity.componentCount > 0 && (
|
||||||
<span className="component-count">{entity.components.length}</span>
|
<span className="component-count">{entity.componentCount}</span>
|
||||||
)}
|
)}
|
||||||
</li>
|
</li>
|
||||||
))}
|
))}
|
||||||
|
|||||||
@@ -12,7 +12,35 @@ export interface RemoteEntity {
|
|||||||
id: number;
|
id: number;
|
||||||
name: string;
|
name: string;
|
||||||
enabled: boolean;
|
enabled: boolean;
|
||||||
components: string[];
|
active: boolean;
|
||||||
|
activeInHierarchy: boolean;
|
||||||
|
componentCount: number;
|
||||||
|
componentTypes: string[];
|
||||||
|
parentId: number | null;
|
||||||
|
childIds: number[];
|
||||||
|
depth: number;
|
||||||
|
tag: number;
|
||||||
|
updateOrder: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface RemoteComponentDetail {
|
||||||
|
typeName: string;
|
||||||
|
properties: Record<string, any>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface RemoteEntityDetails {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
enabled: boolean;
|
||||||
|
active: boolean;
|
||||||
|
activeInHierarchy: boolean;
|
||||||
|
scene: string;
|
||||||
|
sceneName: string;
|
||||||
|
sceneType: string;
|
||||||
|
componentCount: number;
|
||||||
|
componentTypes: string[];
|
||||||
|
components: RemoteComponentDetail[];
|
||||||
|
parentName: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ProfilerData {
|
export interface ProfilerData {
|
||||||
@@ -140,6 +168,10 @@ export class ProfilerService {
|
|||||||
const message = JSON.parse(event.data);
|
const message = JSON.parse(event.data);
|
||||||
if (message.type === 'debug_data' && message.data) {
|
if (message.type === 'debug_data' && message.data) {
|
||||||
this.handleDebugData(message.data);
|
this.handleDebugData(message.data);
|
||||||
|
} else if (message.type === 'get_raw_entity_list_response' && message.data) {
|
||||||
|
this.handleRawEntityListResponse(message.data);
|
||||||
|
} else if (message.type === 'get_entity_details_response' && message.data) {
|
||||||
|
this.handleEntityDetailsResponse(message.data);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('[ProfilerService] Failed to parse message:', error);
|
console.error('[ProfilerService] Failed to parse message:', error);
|
||||||
@@ -152,6 +184,25 @@ export class ProfilerService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public requestEntityDetails(entityId: number): void {
|
||||||
|
if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
|
||||||
|
console.warn('[ProfilerService] Cannot request entity details: WebSocket not connected');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const request = {
|
||||||
|
type: 'get_entity_details',
|
||||||
|
requestId: `entity_details_${entityId}_${Date.now()}`,
|
||||||
|
entityId
|
||||||
|
};
|
||||||
|
console.log('[ProfilerService] Requesting entity details:', request);
|
||||||
|
this.ws.send(JSON.stringify(request));
|
||||||
|
} catch (error) {
|
||||||
|
console.error('[ProfilerService] Failed to request entity details:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private handleDebugData(debugData: any): void {
|
private handleDebugData(debugData: any): void {
|
||||||
const performance = debugData.performance;
|
const performance = debugData.performance;
|
||||||
if (!performance) return;
|
if (!performance) return;
|
||||||
@@ -179,49 +230,98 @@ export class ProfilerService {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const entityCount = debugData.entities?.totalCount || 0;
|
const entityCount = debugData.entities?.totalEntities || debugData.entities?.totalCount || 0;
|
||||||
const componentTypes = debugData.components?.types || [];
|
const componentTypes = debugData.components?.types || [];
|
||||||
const componentCount = componentTypes.length;
|
const componentCount = componentTypes.length;
|
||||||
|
|
||||||
// 解析实体列表
|
|
||||||
console.log('[ProfilerService] debugData.entities:', debugData.entities);
|
|
||||||
let entities: RemoteEntity[] = [];
|
|
||||||
|
|
||||||
// 尝试从 topEntitiesByComponents 获取实体列表
|
|
||||||
if (debugData.entities?.topEntitiesByComponents && Array.isArray(debugData.entities.topEntitiesByComponents)) {
|
|
||||||
console.log('[ProfilerService] Found topEntitiesByComponents, length:', debugData.entities.topEntitiesByComponents.length);
|
|
||||||
entities = debugData.entities.topEntitiesByComponents.map((e: any) => ({
|
|
||||||
id: parseInt(e.id) || 0,
|
|
||||||
name: e.name || `Entity ${e.id}`,
|
|
||||||
enabled: true, // topEntitiesByComponents doesn't have enabled flag, assume true
|
|
||||||
components: [] // componentCount is provided but not component names
|
|
||||||
}));
|
|
||||||
console.log('[ProfilerService] Parsed entities from topEntitiesByComponents:', entities.length);
|
|
||||||
}
|
|
||||||
// 尝试从 entities 获取实体列表(旧格式兼容)
|
|
||||||
else if (debugData.entities?.entities && Array.isArray(debugData.entities.entities)) {
|
|
||||||
console.log('[ProfilerService] Found entities array, length:', debugData.entities.entities.length);
|
|
||||||
entities = debugData.entities.entities.map((e: any) => ({
|
|
||||||
id: e.id,
|
|
||||||
name: e.name || `Entity ${e.id}`,
|
|
||||||
enabled: e.enabled !== false,
|
|
||||||
components: e.components || []
|
|
||||||
}));
|
|
||||||
console.log('[ProfilerService] Parsed entities:', entities.length);
|
|
||||||
} else {
|
|
||||||
console.log('[ProfilerService] No entities array found');
|
|
||||||
}
|
|
||||||
|
|
||||||
this.currentData = {
|
this.currentData = {
|
||||||
totalFrameTime,
|
totalFrameTime,
|
||||||
systems,
|
systems,
|
||||||
entityCount,
|
entityCount,
|
||||||
componentCount,
|
componentCount,
|
||||||
fps,
|
fps,
|
||||||
entities
|
entities: []
|
||||||
};
|
};
|
||||||
|
|
||||||
this.notifyListeners(this.currentData);
|
this.notifyListeners(this.currentData);
|
||||||
|
|
||||||
|
// 请求完整的实体列表
|
||||||
|
this.requestRawEntityList();
|
||||||
|
}
|
||||||
|
|
||||||
|
private requestRawEntityList(): void {
|
||||||
|
if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
|
||||||
|
console.warn('[ProfilerService] Cannot request entity list: WebSocket not connected');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const request = {
|
||||||
|
type: 'get_raw_entity_list',
|
||||||
|
requestId: `entity_list_${Date.now()}`
|
||||||
|
};
|
||||||
|
console.log('[ProfilerService] Requesting entity list:', request);
|
||||||
|
this.ws.send(JSON.stringify(request));
|
||||||
|
} catch (error) {
|
||||||
|
console.error('[ProfilerService] Failed to request entity list:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private handleRawEntityListResponse(data: any): void {
|
||||||
|
if (!data || !Array.isArray(data)) {
|
||||||
|
console.warn('[ProfilerService] Invalid raw entity list response:', data);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('[ProfilerService] Received raw entity list, count:', data.length);
|
||||||
|
|
||||||
|
const entities: RemoteEntity[] = data.map((e: any) => ({
|
||||||
|
id: e.id,
|
||||||
|
name: e.name || `Entity ${e.id}`,
|
||||||
|
enabled: e.enabled !== false,
|
||||||
|
active: e.active !== false,
|
||||||
|
activeInHierarchy: e.activeInHierarchy !== false,
|
||||||
|
componentCount: e.componentCount || 0,
|
||||||
|
componentTypes: e.componentTypes || [],
|
||||||
|
parentId: e.parentId || null,
|
||||||
|
childIds: e.childIds || [],
|
||||||
|
depth: e.depth || 0,
|
||||||
|
tag: e.tag || 0,
|
||||||
|
updateOrder: e.updateOrder || 0
|
||||||
|
}));
|
||||||
|
|
||||||
|
if (this.currentData) {
|
||||||
|
this.currentData.entities = entities;
|
||||||
|
this.notifyListeners(this.currentData);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private handleEntityDetailsResponse(data: any): void {
|
||||||
|
if (!data) {
|
||||||
|
console.warn('[ProfilerService] Invalid entity details response:', data);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('[ProfilerService] Received entity details:', data);
|
||||||
|
|
||||||
|
const entityDetails: RemoteEntityDetails = {
|
||||||
|
id: data.id,
|
||||||
|
name: data.name || `Entity ${data.id}`,
|
||||||
|
enabled: data.enabled !== false,
|
||||||
|
active: data.active !== false,
|
||||||
|
activeInHierarchy: data.activeInHierarchy !== false,
|
||||||
|
scene: data.scene || '',
|
||||||
|
sceneName: data.sceneName || '',
|
||||||
|
sceneType: data.sceneType || '',
|
||||||
|
componentCount: data.componentCount || 0,
|
||||||
|
componentTypes: data.componentTypes || [],
|
||||||
|
components: data.components || [],
|
||||||
|
parentName: data.parentName || null
|
||||||
|
};
|
||||||
|
|
||||||
|
window.dispatchEvent(new CustomEvent('profiler:entity-details', {
|
||||||
|
detail: entityDetails
|
||||||
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
private createEmptyData(): ProfilerData {
|
private createEmptyData(): ProfilerData {
|
||||||
|
|||||||
@@ -39,6 +39,26 @@
|
|||||||
min-height: 0;
|
min-height: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.inspector-content::-webkit-scrollbar {
|
||||||
|
width: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.inspector-content::-webkit-scrollbar-track {
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.inspector-content::-webkit-scrollbar-thumb {
|
||||||
|
background: rgba(121, 121, 121, 0.4);
|
||||||
|
border-radius: 8px;
|
||||||
|
border: 3px solid transparent;
|
||||||
|
background-clip: padding-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.inspector-content::-webkit-scrollbar-thumb:hover {
|
||||||
|
background: rgba(100, 100, 100, 0.7);
|
||||||
|
background-clip: padding-box;
|
||||||
|
}
|
||||||
|
|
||||||
.inspector-section {
|
.inspector-section {
|
||||||
margin-bottom: var(--spacing-lg);
|
margin-bottom: var(--spacing-lg);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user