组件注册与添加
This commit is contained in:
@@ -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);
|
||||
|
||||
175
packages/editor-app/src/components/AddComponent.css
Normal file
175
packages/editor-app/src/components/AddComponent.css
Normal 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;
|
||||
}
|
||||
108
packages/editor-app/src/components/AddComponent.tsx
Normal file
108
packages/editor-app/src/components/AddComponent.tsx
Normal 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}>×</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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user