2025-10-14 23:31:09 +08:00
|
|
|
import { useState, useEffect } from 'react';
|
2025-10-14 23:42:06 +08:00
|
|
|
import { Entity, Core } from '@esengine/ecs-framework';
|
|
|
|
|
import { EntityStoreService, MessageHub, ComponentRegistry } from '@esengine/editor-core';
|
|
|
|
|
import { AddComponent } from './AddComponent';
|
2025-10-15 00:15:12 +08:00
|
|
|
import { PropertyInspector } from './PropertyInspector';
|
2025-10-15 17:15:05 +08:00
|
|
|
import { FileSearch, Plus, ChevronDown, ChevronRight, X, Settings } from 'lucide-react';
|
2025-10-14 23:31:09 +08:00
|
|
|
import '../styles/EntityInspector.css';
|
|
|
|
|
|
|
|
|
|
interface EntityInspectorProps {
|
|
|
|
|
entityStore: EntityStoreService;
|
|
|
|
|
messageHub: MessageHub;
|
|
|
|
|
}
|
|
|
|
|
|
2025-10-15 09:34:44 +08:00
|
|
|
export function EntityInspector({ entityStore: _entityStore, messageHub }: EntityInspectorProps) {
|
2025-10-14 23:31:09 +08:00
|
|
|
const [selectedEntity, setSelectedEntity] = useState<Entity | null>(null);
|
2025-10-14 23:42:06 +08:00
|
|
|
const [showAddComponent, setShowAddComponent] = useState(false);
|
2025-10-15 00:15:12 +08:00
|
|
|
const [expandedComponents, setExpandedComponents] = useState<Set<number>>(new Set());
|
2025-10-14 23:31:09 +08:00
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
const handleSelection = (data: { entity: Entity | null }) => {
|
|
|
|
|
setSelectedEntity(data.entity);
|
2025-10-14 23:42:06 +08:00
|
|
|
setShowAddComponent(false);
|
2025-10-14 23:31:09 +08:00
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const unsubSelect = messageHub.subscribe('entity:selected', handleSelection);
|
|
|
|
|
|
|
|
|
|
return () => {
|
|
|
|
|
unsubSelect();
|
|
|
|
|
};
|
|
|
|
|
}, [messageHub]);
|
|
|
|
|
|
2025-10-14 23:42:06 +08:00
|
|
|
const handleAddComponent = (componentName: string) => {
|
|
|
|
|
if (!selectedEntity) return;
|
|
|
|
|
|
|
|
|
|
const componentRegistry = Core.services.resolve(ComponentRegistry);
|
|
|
|
|
if (!componentRegistry) {
|
|
|
|
|
console.error('ComponentRegistry not found');
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2025-10-15 17:15:05 +08:00
|
|
|
console.log('Attempting to create component:', componentName);
|
2025-10-14 23:42:06 +08:00
|
|
|
const component = componentRegistry.createInstance(componentName);
|
2025-10-15 17:15:05 +08:00
|
|
|
console.log('Created component:', component);
|
|
|
|
|
|
2025-10-14 23:42:06 +08:00
|
|
|
if (component) {
|
|
|
|
|
selectedEntity.addComponent(component);
|
|
|
|
|
messageHub.publish('component:added', { entity: selectedEntity, component });
|
|
|
|
|
setShowAddComponent(false);
|
2025-10-15 17:15:05 +08:00
|
|
|
} else {
|
|
|
|
|
console.error('Failed to create component instance for:', componentName);
|
2025-10-14 23:42:06 +08:00
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const handleRemoveComponent = (index: number) => {
|
|
|
|
|
if (!selectedEntity) return;
|
|
|
|
|
const component = selectedEntity.components[index];
|
|
|
|
|
if (component) {
|
|
|
|
|
selectedEntity.removeComponent(component);
|
|
|
|
|
messageHub.publish('component:removed', { entity: selectedEntity, component });
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
2025-10-15 00:15:12 +08:00
|
|
|
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 (!selectedEntity) return;
|
|
|
|
|
messageHub.publish('component:property:changed', {
|
|
|
|
|
entity: selectedEntity,
|
|
|
|
|
component,
|
|
|
|
|
propertyName,
|
|
|
|
|
value
|
|
|
|
|
});
|
|
|
|
|
};
|
|
|
|
|
|
2025-10-14 23:31:09 +08:00
|
|
|
if (!selectedEntity) {
|
|
|
|
|
return (
|
|
|
|
|
<div className="entity-inspector">
|
|
|
|
|
<div className="inspector-header">
|
2025-10-15 17:15:05 +08:00
|
|
|
<FileSearch size={16} className="inspector-header-icon" />
|
2025-10-14 23:31:09 +08:00
|
|
|
<h3>Inspector</h3>
|
|
|
|
|
</div>
|
|
|
|
|
<div className="inspector-content">
|
2025-10-15 17:15:05 +08:00
|
|
|
<div className="empty-state">
|
|
|
|
|
<FileSearch size={48} strokeWidth={1.5} className="empty-icon" />
|
|
|
|
|
<div className="empty-title">No entity selected</div>
|
|
|
|
|
<div className="empty-hint">Select an entity from the hierarchy</div>
|
|
|
|
|
</div>
|
2025-10-14 23:31:09 +08:00
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const components = selectedEntity.components;
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<div className="entity-inspector">
|
|
|
|
|
<div className="inspector-header">
|
2025-10-15 17:15:05 +08:00
|
|
|
<FileSearch size={16} className="inspector-header-icon" />
|
2025-10-14 23:31:09 +08:00
|
|
|
<h3>Inspector</h3>
|
|
|
|
|
</div>
|
2025-10-15 17:15:05 +08:00
|
|
|
<div className="inspector-content scrollable">
|
2025-10-14 23:31:09 +08:00
|
|
|
<div className="inspector-section">
|
2025-10-15 17:15:05 +08:00
|
|
|
<div className="section-header">
|
|
|
|
|
<Settings size={12} className="section-icon" />
|
|
|
|
|
<span>Entity Info</span>
|
|
|
|
|
</div>
|
2025-10-14 23:31:09 +08:00
|
|
|
<div className="section-content">
|
|
|
|
|
<div className="info-row">
|
|
|
|
|
<span className="info-label">ID:</span>
|
|
|
|
|
<span className="info-value">{selectedEntity.id}</span>
|
|
|
|
|
</div>
|
|
|
|
|
<div className="info-row">
|
|
|
|
|
<span className="info-label">Name:</span>
|
|
|
|
|
<span className="info-value">Entity {selectedEntity.id}</span>
|
|
|
|
|
</div>
|
|
|
|
|
<div className="info-row">
|
|
|
|
|
<span className="info-label">Enabled:</span>
|
|
|
|
|
<span className="info-value">{selectedEntity.enabled ? 'Yes' : 'No'}</span>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div className="inspector-section">
|
2025-10-14 23:42:06 +08:00
|
|
|
<div className="section-header">
|
2025-10-15 17:15:05 +08:00
|
|
|
<Settings size={12} className="section-icon" />
|
2025-10-14 23:42:06 +08:00
|
|
|
<span>Components ({components.length})</span>
|
|
|
|
|
<button
|
|
|
|
|
className="add-component-btn"
|
|
|
|
|
onClick={() => setShowAddComponent(true)}
|
|
|
|
|
title="Add Component"
|
|
|
|
|
>
|
2025-10-15 17:15:05 +08:00
|
|
|
<Plus size={12} />
|
2025-10-14 23:42:06 +08:00
|
|
|
</button>
|
|
|
|
|
</div>
|
2025-10-14 23:31:09 +08:00
|
|
|
<div className="section-content">
|
|
|
|
|
{components.length === 0 ? (
|
2025-10-15 17:15:05 +08:00
|
|
|
<div className="empty-state-small">No components</div>
|
2025-10-14 23:31:09 +08:00
|
|
|
) : (
|
|
|
|
|
<ul className="component-list">
|
2025-10-15 00:15:12 +08:00
|
|
|
{components.map((component, index) => {
|
|
|
|
|
const isExpanded = expandedComponents.has(index);
|
|
|
|
|
return (
|
2025-10-15 17:15:05 +08:00
|
|
|
<li key={index} className={`component-item ${isExpanded ? 'expanded' : ''}`}>
|
|
|
|
|
<div className="component-header" onClick={() => toggleComponentExpanded(index)}>
|
2025-10-15 00:15:12 +08:00
|
|
|
<button
|
|
|
|
|
className="component-expand-btn"
|
|
|
|
|
title={isExpanded ? 'Collapse' : 'Expand'}
|
|
|
|
|
>
|
2025-10-15 17:15:05 +08:00
|
|
|
{isExpanded ? <ChevronDown size={14} /> : <ChevronRight size={14} />}
|
2025-10-15 00:15:12 +08:00
|
|
|
</button>
|
2025-10-15 17:15:05 +08:00
|
|
|
<Settings size={14} className="component-icon" />
|
2025-10-15 00:15:12 +08:00
|
|
|
<span className="component-name">{component.constructor.name}</span>
|
|
|
|
|
<button
|
|
|
|
|
className="remove-component-btn"
|
2025-10-15 17:15:05 +08:00
|
|
|
onClick={(e) => {
|
|
|
|
|
e.stopPropagation();
|
|
|
|
|
handleRemoveComponent(index);
|
|
|
|
|
}}
|
2025-10-15 00:15:12 +08:00
|
|
|
title="Remove Component"
|
|
|
|
|
>
|
2025-10-15 17:15:05 +08:00
|
|
|
<X size={14} />
|
2025-10-15 00:15:12 +08:00
|
|
|
</button>
|
|
|
|
|
</div>
|
|
|
|
|
{isExpanded && (
|
2025-10-15 17:15:05 +08:00
|
|
|
<div className="component-properties animate-slideDown">
|
2025-10-15 00:15:12 +08:00
|
|
|
<PropertyInspector
|
|
|
|
|
component={component}
|
|
|
|
|
onChange={(propertyName, value) => handlePropertyChange(component, propertyName, value)}
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
</li>
|
|
|
|
|
);
|
|
|
|
|
})}
|
2025-10-14 23:31:09 +08:00
|
|
|
</ul>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
2025-10-14 23:42:06 +08:00
|
|
|
|
|
|
|
|
{showAddComponent && selectedEntity && (
|
|
|
|
|
<AddComponent
|
|
|
|
|
entity={selectedEntity}
|
|
|
|
|
componentRegistry={Core.services.resolve(ComponentRegistry)}
|
|
|
|
|
onAdd={handleAddComponent}
|
|
|
|
|
onCancel={() => setShowAddComponent(false)}
|
|
|
|
|
/>
|
|
|
|
|
)}
|
2025-10-14 23:31:09 +08:00
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|