组件注册与添加

This commit is contained in:
YHH
2025-10-14 23:42:06 +08:00
parent 1cf5641c4c
commit 3a5e73266e
10 changed files with 481 additions and 4 deletions

View File

@@ -1,10 +1,13 @@
import { useState, useEffect } from 'react'; import { useState, useEffect } from 'react';
import { Core, Scene } from '@esengine/ecs-framework'; import { Core, Scene } from '@esengine/ecs-framework';
import { EditorPluginManager, UIRegistry, MessageHub, SerializerRegistry, EntityStoreService } from '@esengine/editor-core'; import { EditorPluginManager, UIRegistry, MessageHub, SerializerRegistry, EntityStoreService, ComponentRegistry } from '@esengine/editor-core';
import { SceneInspectorPlugin } from './plugins/SceneInspectorPlugin'; import { SceneInspectorPlugin } from './plugins/SceneInspectorPlugin';
import { SceneHierarchy } from './components/SceneHierarchy'; import { SceneHierarchy } from './components/SceneHierarchy';
import { EntityInspector } from './components/EntityInspector'; import { EntityInspector } from './components/EntityInspector';
import { TauriAPI } from './api/tauri'; import { TauriAPI } from './api/tauri';
import { TransformComponent } from './example-components/TransformComponent';
import { SpriteComponent } from './example-components/SpriteComponent';
import { RigidBodyComponent } from './example-components/RigidBodyComponent';
import './styles/App.css'; import './styles/App.css';
function App() { function App() {
@@ -26,11 +29,34 @@ function App() {
const messageHub = new MessageHub(); const messageHub = new MessageHub();
const serializerRegistry = new SerializerRegistry(); const serializerRegistry = new SerializerRegistry();
const entityStore = new EntityStoreService(messageHub); const entityStore = new EntityStoreService(messageHub);
const componentRegistry = new ComponentRegistry();
componentRegistry.register({
name: 'Transform',
type: TransformComponent,
category: 'Transform',
description: 'Position, rotation and scale'
});
componentRegistry.register({
name: 'Sprite',
type: SpriteComponent,
category: 'Rendering',
description: 'Sprite renderer'
});
componentRegistry.register({
name: 'RigidBody',
type: RigidBodyComponent,
category: 'Physics',
description: 'Physics body'
});
Core.services.registerInstance(UIRegistry, uiRegistry); Core.services.registerInstance(UIRegistry, uiRegistry);
Core.services.registerInstance(MessageHub, messageHub); Core.services.registerInstance(MessageHub, messageHub);
Core.services.registerInstance(SerializerRegistry, serializerRegistry); Core.services.registerInstance(SerializerRegistry, serializerRegistry);
Core.services.registerInstance(EntityStoreService, entityStore); Core.services.registerInstance(EntityStoreService, entityStore);
Core.services.registerInstance(ComponentRegistry, componentRegistry);
const pluginMgr = new EditorPluginManager(); const pluginMgr = new EditorPluginManager();
pluginMgr.initialize(coreInstance, Core.services); pluginMgr.initialize(coreInstance, Core.services);

View File

@@ -0,0 +1,175 @@
.modal-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(0, 0, 0, 0.5);
display: flex;
align-items: center;
justify-content: center;
z-index: 1000;
}
.add-component-dialog {
background-color: #2d2d2d;
border-radius: 8px;
width: 500px;
max-width: 90%;
max-height: 80vh;
display: flex;
flex-direction: column;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
}
.dialog-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 16px 20px;
border-bottom: 1px solid #404040;
}
.dialog-header h3 {
margin: 0;
font-size: 16px;
color: #fff;
}
.close-btn {
background: none;
border: none;
color: #aaa;
font-size: 24px;
cursor: pointer;
padding: 0;
width: 24px;
height: 24px;
line-height: 20px;
transition: color 0.2s;
}
.close-btn:hover {
color: #fff;
}
.dialog-content {
flex: 1;
display: flex;
flex-direction: column;
overflow: hidden;
padding: 16px 20px;
}
.component-filter {
width: 100%;
padding: 8px 12px;
background-color: #1e1e1e;
border: 1px solid #404040;
border-radius: 4px;
color: #fff;
font-size: 14px;
margin-bottom: 12px;
}
.component-filter:focus {
outline: none;
border-color: #007acc;
}
.component-list {
flex: 1;
overflow-y: auto;
min-height: 200px;
max-height: 400px;
}
.component-category {
margin-bottom: 16px;
}
.category-header {
font-size: 12px;
font-weight: 600;
color: #888;
text-transform: uppercase;
margin-bottom: 8px;
padding-bottom: 4px;
border-bottom: 1px solid #404040;
}
.component-option {
padding: 10px 12px;
cursor: pointer;
border-radius: 4px;
margin-bottom: 4px;
transition: background-color 0.2s;
}
.component-option:hover {
background-color: #3a3a3a;
}
.component-option.selected {
background-color: #094771;
border: 1px solid #007acc;
}
.component-name {
font-size: 14px;
color: #fff;
font-weight: 500;
margin-bottom: 2px;
}
.component-description {
font-size: 12px;
color: #aaa;
}
.empty-message {
color: #888;
text-align: center;
padding: 40px 20px;
font-size: 14px;
}
.dialog-footer {
display: flex;
justify-content: flex-end;
gap: 8px;
padding: 16px 20px;
border-top: 1px solid #404040;
}
.btn {
padding: 8px 16px;
border-radius: 4px;
border: none;
cursor: pointer;
font-size: 14px;
transition: all 0.2s;
}
.btn:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.btn-cancel {
background-color: #3a3a3a;
color: #fff;
}
.btn-cancel:hover:not(:disabled) {
background-color: #4a4a4a;
}
.btn-primary {
background-color: #007acc;
color: #fff;
}
.btn-primary:hover:not(:disabled) {
background-color: #0098ff;
}

View File

@@ -0,0 +1,108 @@
import { useState, useEffect } from 'react';
import { Entity } from '@esengine/ecs-framework';
import { ComponentRegistry, ComponentTypeInfo } from '@esengine/editor-core';
import './AddComponent.css';
interface AddComponentProps {
entity: Entity;
componentRegistry: ComponentRegistry;
onAdd: (componentName: string) => void;
onCancel: () => void;
}
export function AddComponent({ entity, componentRegistry, onAdd, onCancel }: AddComponentProps) {
const [components, setComponents] = useState<ComponentTypeInfo[]>([]);
const [selectedComponent, setSelectedComponent] = useState<string | null>(null);
const [filter, setFilter] = useState('');
useEffect(() => {
const allComponents = componentRegistry.getAllComponents();
const existingComponentNames = entity.components.map(c => c.constructor.name);
const availableComponents = allComponents.filter(
comp => !existingComponentNames.includes(comp.name)
);
setComponents(availableComponents);
}, [entity, componentRegistry]);
const filteredComponents = components.filter(comp =>
comp.name.toLowerCase().includes(filter.toLowerCase()) ||
comp.category?.toLowerCase().includes(filter.toLowerCase())
);
const handleAdd = () => {
if (selectedComponent) {
onAdd(selectedComponent);
}
};
const groupedComponents = filteredComponents.reduce((groups, comp) => {
const category = comp.category || 'Other';
if (!groups[category]) {
groups[category] = [];
}
groups[category].push(comp);
return groups;
}, {} as Record<string, ComponentTypeInfo[]>);
return (
<div className="modal-overlay" onClick={onCancel}>
<div className="add-component-dialog" onClick={(e) => e.stopPropagation()}>
<div className="dialog-header">
<h3>Add Component</h3>
<button className="close-btn" onClick={onCancel}>&times;</button>
</div>
<div className="dialog-content">
<input
type="text"
className="component-filter"
placeholder="Search components..."
value={filter}
onChange={(e) => setFilter(e.target.value)}
autoFocus
/>
<div className="component-list">
{Object.keys(groupedComponents).length === 0 ? (
<div className="empty-message">No available components</div>
) : (
Object.entries(groupedComponents).map(([category, comps]) => (
<div key={category} className="component-category">
<div className="category-header">{category}</div>
{comps.map(comp => (
<div
key={comp.name}
className={`component-option ${selectedComponent === comp.name ? 'selected' : ''}`}
onClick={() => setSelectedComponent(comp.name)}
onDoubleClick={handleAdd}
>
<div className="component-name">{comp.name}</div>
{comp.description && (
<div className="component-description">{comp.description}</div>
)}
</div>
))}
</div>
))
)}
</div>
</div>
<div className="dialog-footer">
<button className="btn btn-cancel" onClick={onCancel}>
Cancel
</button>
<button
className="btn btn-primary"
onClick={handleAdd}
disabled={!selectedComponent}
>
Add Component
</button>
</div>
</div>
</div>
);
}

View File

@@ -1,6 +1,7 @@
import { useState, useEffect } from 'react'; import { useState, useEffect } from 'react';
import { Entity } from '@esengine/ecs-framework'; import { Entity, Core } from '@esengine/ecs-framework';
import { EntityStoreService, MessageHub } from '@esengine/editor-core'; import { EntityStoreService, MessageHub, ComponentRegistry } from '@esengine/editor-core';
import { AddComponent } from './AddComponent';
import '../styles/EntityInspector.css'; import '../styles/EntityInspector.css';
interface EntityInspectorProps { interface EntityInspectorProps {
@@ -10,10 +11,12 @@ interface EntityInspectorProps {
export function EntityInspector({ entityStore, messageHub }: EntityInspectorProps) { export function EntityInspector({ entityStore, messageHub }: EntityInspectorProps) {
const [selectedEntity, setSelectedEntity] = useState<Entity | null>(null); const [selectedEntity, setSelectedEntity] = useState<Entity | null>(null);
const [showAddComponent, setShowAddComponent] = useState(false);
useEffect(() => { useEffect(() => {
const handleSelection = (data: { entity: Entity | null }) => { const handleSelection = (data: { entity: Entity | null }) => {
setSelectedEntity(data.entity); setSelectedEntity(data.entity);
setShowAddComponent(false);
}; };
const unsubSelect = messageHub.subscribe('entity:selected', handleSelection); const unsubSelect = messageHub.subscribe('entity:selected', handleSelection);
@@ -23,6 +26,32 @@ export function EntityInspector({ entityStore, messageHub }: EntityInspectorProp
}; };
}, [messageHub]); }, [messageHub]);
const handleAddComponent = (componentName: string) => {
if (!selectedEntity) return;
const componentRegistry = Core.services.resolve(ComponentRegistry);
if (!componentRegistry) {
console.error('ComponentRegistry not found');
return;
}
const component = componentRegistry.createInstance(componentName);
if (component) {
selectedEntity.addComponent(component);
messageHub.publish('component:added', { entity: selectedEntity, component });
setShowAddComponent(false);
}
};
const handleRemoveComponent = (index: number) => {
if (!selectedEntity) return;
const component = selectedEntity.components[index];
if (component) {
selectedEntity.removeComponent(component);
messageHub.publish('component:removed', { entity: selectedEntity, component });
}
};
if (!selectedEntity) { if (!selectedEntity) {
return ( return (
<div className="entity-inspector"> <div className="entity-inspector">
@@ -63,7 +92,16 @@ export function EntityInspector({ entityStore, messageHub }: EntityInspectorProp
</div> </div>
<div className="inspector-section"> <div className="inspector-section">
<div className="section-header">Components ({components.length})</div> <div className="section-header">
<span>Components ({components.length})</span>
<button
className="add-component-btn"
onClick={() => setShowAddComponent(true)}
title="Add Component"
>
+
</button>
</div>
<div className="section-content"> <div className="section-content">
{components.length === 0 ? ( {components.length === 0 ? (
<div className="empty-state">No components</div> <div className="empty-state">No components</div>
@@ -73,6 +111,13 @@ export function EntityInspector({ entityStore, messageHub }: EntityInspectorProp
<li key={index} className="component-item"> <li key={index} className="component-item">
<span className="component-icon">🔧</span> <span className="component-icon">🔧</span>
<span className="component-name">{component.constructor.name}</span> <span className="component-name">{component.constructor.name}</span>
<button
className="remove-component-btn"
onClick={() => handleRemoveComponent(index)}
title="Remove Component"
>
×
</button>
</li> </li>
))} ))}
</ul> </ul>
@@ -80,6 +125,15 @@ export function EntityInspector({ entityStore, messageHub }: EntityInspectorProp
</div> </div>
</div> </div>
</div> </div>
{showAddComponent && selectedEntity && (
<AddComponent
entity={selectedEntity}
componentRegistry={Core.services.resolve(ComponentRegistry)}
onAdd={handleAddComponent}
onCancel={() => setShowAddComponent(false)}
/>
)}
</div> </div>
); );
} }

View File

@@ -0,0 +1,8 @@
import { Component } from '@esengine/ecs-framework';
export class RigidBodyComponent extends Component {
public mass: number = 1;
public velocityX: number = 0;
public velocityY: number = 0;
public gravity: boolean = true;
}

View File

@@ -0,0 +1,8 @@
import { Component } from '@esengine/ecs-framework';
export class SpriteComponent extends Component {
public texture: string = '';
public width: number = 100;
public height: number = 100;
public alpha: number = 1;
}

View File

@@ -0,0 +1,9 @@
import { Component } from '@esengine/ecs-framework';
export class TransformComponent extends Component {
public x: number = 0;
public y: number = 0;
public rotation: number = 0;
public scaleX: number = 1;
public scaleY: number = 1;
}

View File

@@ -30,6 +30,9 @@
} }
.section-header { .section-header {
display: flex;
justify-content: space-between;
align-items: center;
font-size: 12px; font-size: 12px;
font-weight: 600; font-weight: 600;
color: #858585; color: #858585;
@@ -40,6 +43,26 @@
border-bottom: 1px solid #3c3c3c; border-bottom: 1px solid #3c3c3c;
} }
.add-component-btn {
background-color: #007acc;
color: #fff;
border: none;
border-radius: 3px;
width: 20px;
height: 20px;
font-size: 16px;
line-height: 16px;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
transition: background-color 0.2s;
}
.add-component-btn:hover {
background-color: #0098ff;
}
.section-content { .section-content {
padding: 8px 0; padding: 8px 0;
} }
@@ -75,6 +98,7 @@
border: 1px solid #3c3c3c; border: 1px solid #3c3c3c;
border-radius: 3px; border-radius: 3px;
font-size: 13px; font-size: 13px;
position: relative;
} }
.component-item:hover { .component-item:hover {
@@ -88,9 +112,26 @@
} }
.component-name { .component-name {
flex: 1;
color: #cccccc; color: #cccccc;
} }
.remove-component-btn {
background: none;
border: none;
color: #858585;
font-size: 18px;
line-height: 14px;
cursor: pointer;
padding: 2px 6px;
transition: color 0.2s;
margin-left: 8px;
}
.remove-component-btn:hover {
color: #ff5555;
}
.empty-state { .empty-state {
padding: 20px; padding: 20px;
text-align: center; text-align: center;

View File

@@ -0,0 +1,47 @@
import { Injectable, IService, Component } from '@esengine/ecs-framework';
export interface ComponentTypeInfo {
name: string;
type: new (...args: any[]) => Component;
category?: string;
description?: string;
}
/**
* 管理编辑器中可用的组件类型
*/
@Injectable()
export class ComponentRegistry implements IService {
private components: Map<string, ComponentTypeInfo> = new Map();
public dispose(): void {
this.components.clear();
}
public register(info: ComponentTypeInfo): void {
this.components.set(info.name, info);
}
public unregister(name: string): void {
this.components.delete(name);
}
public getComponent(name: string): ComponentTypeInfo | undefined {
return this.components.get(name);
}
public getAllComponents(): ComponentTypeInfo[] {
return Array.from(this.components.values());
}
public getComponentsByCategory(category: string): ComponentTypeInfo[] {
return this.getAllComponents().filter(c => c.category === category);
}
public createInstance(name: string, ...args: any[]): Component | null {
const info = this.components.get(name);
if (!info) return null;
return new info.type(...args);
}
}

View File

@@ -11,5 +11,6 @@ export * from './Services/UIRegistry';
export * from './Services/MessageHub'; export * from './Services/MessageHub';
export * from './Services/SerializerRegistry'; export * from './Services/SerializerRegistry';
export * from './Services/EntityStoreService'; export * from './Services/EntityStoreService';
export * from './Services/ComponentRegistry';
export * from './Types/UITypes'; export * from './Types/UITypes';