From 3a5e73266ed556ea829dc40bdfc14948429b931b Mon Sep 17 00:00:00 2001 From: YHH <359807859@qq.com> Date: Tue, 14 Oct 2025 23:42:06 +0800 Subject: [PATCH] =?UTF-8?q?=E7=BB=84=E4=BB=B6=E6=B3=A8=E5=86=8C=E4=B8=8E?= =?UTF-8?q?=E6=B7=BB=E5=8A=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/editor-app/src/App.tsx | 28 ++- .../src/components/AddComponent.css | 175 ++++++++++++++++++ .../src/components/AddComponent.tsx | 108 +++++++++++ .../src/components/EntityInspector.tsx | 60 +++++- .../example-components/RigidBodyComponent.ts | 8 + .../src/example-components/SpriteComponent.ts | 8 + .../example-components/TransformComponent.ts | 9 + .../editor-app/src/styles/EntityInspector.css | 41 ++++ .../src/Services/ComponentRegistry.ts | 47 +++++ packages/editor-core/src/index.ts | 1 + 10 files changed, 481 insertions(+), 4 deletions(-) create mode 100644 packages/editor-app/src/components/AddComponent.css create mode 100644 packages/editor-app/src/components/AddComponent.tsx create mode 100644 packages/editor-app/src/example-components/RigidBodyComponent.ts create mode 100644 packages/editor-app/src/example-components/SpriteComponent.ts create mode 100644 packages/editor-app/src/example-components/TransformComponent.ts create mode 100644 packages/editor-core/src/Services/ComponentRegistry.ts diff --git a/packages/editor-app/src/App.tsx b/packages/editor-app/src/App.tsx index 4f5981cb..cca2e7aa 100644 --- a/packages/editor-app/src/App.tsx +++ b/packages/editor-app/src/App.tsx @@ -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); diff --git a/packages/editor-app/src/components/AddComponent.css b/packages/editor-app/src/components/AddComponent.css new file mode 100644 index 00000000..c0985b9d --- /dev/null +++ b/packages/editor-app/src/components/AddComponent.css @@ -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; +} diff --git a/packages/editor-app/src/components/AddComponent.tsx b/packages/editor-app/src/components/AddComponent.tsx new file mode 100644 index 00000000..8e0ab29e --- /dev/null +++ b/packages/editor-app/src/components/AddComponent.tsx @@ -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([]); + const [selectedComponent, setSelectedComponent] = useState(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); + + return ( +
+
e.stopPropagation()}> +
+

Add Component

+ +
+ +
+ setFilter(e.target.value)} + autoFocus + /> + +
+ {Object.keys(groupedComponents).length === 0 ? ( +
No available components
+ ) : ( + Object.entries(groupedComponents).map(([category, comps]) => ( +
+
{category}
+ {comps.map(comp => ( +
setSelectedComponent(comp.name)} + onDoubleClick={handleAdd} + > +
{comp.name}
+ {comp.description && ( +
{comp.description}
+ )} +
+ ))} +
+ )) + )} +
+
+ +
+ + +
+
+
+ ); +} diff --git a/packages/editor-app/src/components/EntityInspector.tsx b/packages/editor-app/src/components/EntityInspector.tsx index 43a26333..50c879f0 100644 --- a/packages/editor-app/src/components/EntityInspector.tsx +++ b/packages/editor-app/src/components/EntityInspector.tsx @@ -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(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 (
@@ -63,7 +92,16 @@ export function EntityInspector({ entityStore, messageHub }: EntityInspectorProp
-
Components ({components.length})
+
+ Components ({components.length}) + +
{components.length === 0 ? (
No components
@@ -73,6 +111,13 @@ export function EntityInspector({ entityStore, messageHub }: EntityInspectorProp
  • 🔧 {component.constructor.name} +
  • ))} @@ -80,6 +125,15 @@ export function EntityInspector({ entityStore, messageHub }: EntityInspectorProp
    + + {showAddComponent && selectedEntity && ( + setShowAddComponent(false)} + /> + )} ); } diff --git a/packages/editor-app/src/example-components/RigidBodyComponent.ts b/packages/editor-app/src/example-components/RigidBodyComponent.ts new file mode 100644 index 00000000..eb06622e --- /dev/null +++ b/packages/editor-app/src/example-components/RigidBodyComponent.ts @@ -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; +} diff --git a/packages/editor-app/src/example-components/SpriteComponent.ts b/packages/editor-app/src/example-components/SpriteComponent.ts new file mode 100644 index 00000000..b7acd478 --- /dev/null +++ b/packages/editor-app/src/example-components/SpriteComponent.ts @@ -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; +} diff --git a/packages/editor-app/src/example-components/TransformComponent.ts b/packages/editor-app/src/example-components/TransformComponent.ts new file mode 100644 index 00000000..835651b0 --- /dev/null +++ b/packages/editor-app/src/example-components/TransformComponent.ts @@ -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; +} diff --git a/packages/editor-app/src/styles/EntityInspector.css b/packages/editor-app/src/styles/EntityInspector.css index 69d30f08..62935e03 100644 --- a/packages/editor-app/src/styles/EntityInspector.css +++ b/packages/editor-app/src/styles/EntityInspector.css @@ -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; diff --git a/packages/editor-core/src/Services/ComponentRegistry.ts b/packages/editor-core/src/Services/ComponentRegistry.ts new file mode 100644 index 00000000..a239d8f3 --- /dev/null +++ b/packages/editor-core/src/Services/ComponentRegistry.ts @@ -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 = 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); + } +} diff --git a/packages/editor-core/src/index.ts b/packages/editor-core/src/index.ts index 77987c2b..aaa2f2be 100644 --- a/packages/editor-core/src/index.ts +++ b/packages/editor-core/src/index.ts @@ -11,5 +11,6 @@ export * from './Services/UIRegistry'; export * from './Services/MessageHub'; export * from './Services/SerializerRegistry'; export * from './Services/EntityStoreService'; +export * from './Services/ComponentRegistry'; export * from './Types/UITypes';