组件注册与添加

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 { 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 { SceneHierarchy } from './components/SceneHierarchy';
import { EntityInspector } from './components/EntityInspector';
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';
function App() {
@@ -26,11 +29,34 @@ function App() {
const messageHub = new MessageHub();
const serializerRegistry = new SerializerRegistry();
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(MessageHub, messageHub);
Core.services.registerInstance(SerializerRegistry, serializerRegistry);
Core.services.registerInstance(EntityStoreService, entityStore);
Core.services.registerInstance(ComponentRegistry, componentRegistry);
const pluginMgr = new EditorPluginManager();
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 { Entity } from '@esengine/ecs-framework';
import { EntityStoreService, MessageHub } from '@esengine/editor-core';
import { Entity, Core } from '@esengine/ecs-framework';
import { EntityStoreService, MessageHub, ComponentRegistry } from '@esengine/editor-core';
import { AddComponent } from './AddComponent';
import '../styles/EntityInspector.css';
interface EntityInspectorProps {
@@ -10,10 +11,12 @@ interface EntityInspectorProps {
export function EntityInspector({ entityStore, messageHub }: EntityInspectorProps) {
const [selectedEntity, setSelectedEntity] = useState<Entity | null>(null);
const [showAddComponent, setShowAddComponent] = useState(false);
useEffect(() => {
const handleSelection = (data: { entity: Entity | null }) => {
setSelectedEntity(data.entity);
setShowAddComponent(false);
};
const unsubSelect = messageHub.subscribe('entity:selected', handleSelection);
@@ -23,6 +26,32 @@ export function EntityInspector({ entityStore, messageHub }: EntityInspectorProp
};
}, [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) {
return (
<div className="entity-inspector">
@@ -63,7 +92,16 @@ export function EntityInspector({ entityStore, messageHub }: EntityInspectorProp
</div>
<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">
{components.length === 0 ? (
<div className="empty-state">No components</div>
@@ -73,6 +111,13 @@ export function EntityInspector({ entityStore, messageHub }: EntityInspectorProp
<li key={index} className="component-item">
<span className="component-icon">🔧</span>
<span className="component-name">{component.constructor.name}</span>
<button
className="remove-component-btn"
onClick={() => handleRemoveComponent(index)}
title="Remove Component"
>
×
</button>
</li>
))}
</ul>
@@ -80,6 +125,15 @@ export function EntityInspector({ entityStore, messageHub }: EntityInspectorProp
</div>
</div>
</div>
{showAddComponent && selectedEntity && (
<AddComponent
entity={selectedEntity}
componentRegistry={Core.services.resolve(ComponentRegistry)}
onAdd={handleAddComponent}
onCancel={() => setShowAddComponent(false)}
/>
)}
</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 {
display: flex;
justify-content: space-between;
align-items: center;
font-size: 12px;
font-weight: 600;
color: #858585;
@@ -40,6 +43,26 @@
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 {
padding: 8px 0;
}
@@ -75,6 +98,7 @@
border: 1px solid #3c3c3c;
border-radius: 3px;
font-size: 13px;
position: relative;
}
.component-item:hover {
@@ -88,9 +112,26 @@
}
.component-name {
flex: 1;
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 {
padding: 20px;
text-align: center;