From 4550a6146aab1418e15b0433c53599ebd7db6ccc Mon Sep 17 00:00:00 2001 From: YHH <359807859@qq.com> Date: Wed, 15 Oct 2025 00:15:12 +0800 Subject: [PATCH] =?UTF-8?q?=E7=BB=84=E4=BB=B6=E5=B1=9E=E6=80=A7=E7=BC=96?= =?UTF-8?q?=E8=BE=91=E5=99=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/editor-app/src/App.tsx | 36 +++- .../src/components/EntityInspector.tsx | 70 ++++++-- .../src/components/PropertyInspector.tsx | 156 ++++++++++++++++++ .../editor-app/src/styles/EntityInspector.css | 44 ++++- .../src/styles/PropertyInspector.css | 80 +++++++++ .../src/Services/PropertyMetadata.ts | 87 ++++++++++ packages/editor-core/src/index.ts | 1 + 7 files changed, 452 insertions(+), 22 deletions(-) create mode 100644 packages/editor-app/src/components/PropertyInspector.tsx create mode 100644 packages/editor-app/src/styles/PropertyInspector.css create mode 100644 packages/editor-core/src/Services/PropertyMetadata.ts diff --git a/packages/editor-app/src/App.tsx b/packages/editor-app/src/App.tsx index 51822372..6ede39c2 100644 --- a/packages/editor-app/src/App.tsx +++ b/packages/editor-app/src/App.tsx @@ -1,6 +1,6 @@ import { useState, useEffect } from 'react'; import { Core, Scene } from '@esengine/ecs-framework'; -import { EditorPluginManager, UIRegistry, MessageHub, SerializerRegistry, EntityStoreService, ComponentRegistry, LocaleService } from '@esengine/editor-core'; +import { EditorPluginManager, UIRegistry, MessageHub, SerializerRegistry, EntityStoreService, ComponentRegistry, LocaleService, PropertyMetadataService } from '@esengine/editor-core'; import { SceneInspectorPlugin } from './plugins/SceneInspectorPlugin'; import { SceneHierarchy } from './components/SceneHierarchy'; import { EntityInspector } from './components/EntityInspector'; @@ -13,13 +13,43 @@ import { en, zh } from './locales'; import './styles/App.css'; // 在 App 组件外部初始化 Core 和基础服务 -Core.create({ debug: true }); +const coreInstance = Core.create({ debug: true }); const localeService = new LocaleService(); localeService.registerTranslations('en', en); localeService.registerTranslations('zh', zh); Core.services.registerInstance(LocaleService, localeService); +const propertyMetadata = new PropertyMetadataService(); +Core.services.registerInstance(PropertyMetadataService, propertyMetadata); + +propertyMetadata.register(TransformComponent, { + properties: { + x: { type: 'number', label: 'X Position' }, + y: { type: 'number', label: 'Y Position' }, + rotation: { type: 'number', label: 'Rotation', min: 0, max: 360 }, + scaleX: { type: 'number', label: 'Scale X', min: 0, step: 0.1 }, + scaleY: { type: 'number', label: 'Scale Y', min: 0, step: 0.1 } + } +}); + +propertyMetadata.register(SpriteComponent, { + properties: { + texturePath: { type: 'string', label: 'Texture Path' }, + color: { type: 'color', label: 'Tint Color' }, + visible: { type: 'boolean', label: 'Visible' } + } +}); + +propertyMetadata.register(RigidBodyComponent, { + properties: { + mass: { type: 'number', label: 'Mass', min: 0, step: 0.1 }, + friction: { type: 'number', label: 'Friction', min: 0, max: 1, step: 0.01 }, + restitution: { type: 'number', label: 'Restitution', min: 0, max: 1, step: 0.01 }, + isDynamic: { type: 'boolean', label: 'Dynamic' } + } +}); + function App() { const [initialized, setInitialized] = useState(false); const [pluginManager, setPluginManager] = useState(null); @@ -68,7 +98,7 @@ function App() { Core.services.registerInstance(ComponentRegistry, componentRegistry); const pluginMgr = new EditorPluginManager(); - pluginMgr.initialize(Core, Core.services); + pluginMgr.initialize(coreInstance, Core.services); await pluginMgr.installEditor(new SceneInspectorPlugin()); diff --git a/packages/editor-app/src/components/EntityInspector.tsx b/packages/editor-app/src/components/EntityInspector.tsx index 50c879f0..8c7cb2de 100644 --- a/packages/editor-app/src/components/EntityInspector.tsx +++ b/packages/editor-app/src/components/EntityInspector.tsx @@ -2,6 +2,7 @@ import { useState, useEffect } from 'react'; import { Entity, Core } from '@esengine/ecs-framework'; import { EntityStoreService, MessageHub, ComponentRegistry } from '@esengine/editor-core'; import { AddComponent } from './AddComponent'; +import { PropertyInspector } from './PropertyInspector'; import '../styles/EntityInspector.css'; interface EntityInspectorProps { @@ -12,6 +13,7 @@ interface EntityInspectorProps { export function EntityInspector({ entityStore, messageHub }: EntityInspectorProps) { const [selectedEntity, setSelectedEntity] = useState(null); const [showAddComponent, setShowAddComponent] = useState(false); + const [expandedComponents, setExpandedComponents] = useState>(new Set()); useEffect(() => { const handleSelection = (data: { entity: Entity | null }) => { @@ -52,6 +54,28 @@ export function EntityInspector({ entityStore, messageHub }: EntityInspectorProp } }; + 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 + }); + }; + if (!selectedEntity) { return (
@@ -107,19 +131,39 @@ export function EntityInspector({ entityStore, messageHub }: EntityInspectorProp
No components
) : (
    - {components.map((component, index) => ( -
  • - 🔧 - {component.constructor.name} - -
  • - ))} + {components.map((component, index) => { + const isExpanded = expandedComponents.has(index); + return ( +
  • +
    + + 🔧 + {component.constructor.name} + +
    + {isExpanded && ( +
    + handlePropertyChange(component, propertyName, value)} + /> +
    + )} +
  • + ); + })}
)}
diff --git a/packages/editor-app/src/components/PropertyInspector.tsx b/packages/editor-app/src/components/PropertyInspector.tsx new file mode 100644 index 00000000..6f44f48d --- /dev/null +++ b/packages/editor-app/src/components/PropertyInspector.tsx @@ -0,0 +1,156 @@ +import { useState, useEffect } from 'react'; +import { Component, Core } from '@esengine/ecs-framework'; +import { PropertyMetadataService, PropertyMetadata } from '@esengine/editor-core'; +import '../styles/PropertyInspector.css'; + +interface PropertyInspectorProps { + component: Component; + onChange?: (propertyName: string, value: any) => void; +} + +export function PropertyInspector({ component, onChange }: PropertyInspectorProps) { + const [properties, setProperties] = useState>({}); + const [values, setValues] = useState>({}); + + useEffect(() => { + const propertyMetadataService = Core.services.resolve(PropertyMetadataService); + if (!propertyMetadataService) return; + + const metadata = propertyMetadataService.getEditableProperties(component); + setProperties(metadata); + + const componentAsAny = component as any; + const currentValues: Record = {}; + for (const key in metadata) { + currentValues[key] = componentAsAny[key]; + } + setValues(currentValues); + }, [component]); + + const handleChange = (propertyName: string, value: any) => { + const componentAsAny = component as any; + componentAsAny[propertyName] = value; + + setValues(prev => ({ + ...prev, + [propertyName]: value + })); + + if (onChange) { + onChange(propertyName, value); + } + }; + + const renderProperty = (propertyName: string, metadata: PropertyMetadata) => { + const value = values[propertyName]; + const label = metadata.label || propertyName; + + switch (metadata.type) { + case 'number': + return ( +
+ + handleChange(propertyName, parseFloat(e.target.value) || 0)} + /> +
+ ); + + case 'string': + return ( +
+ + handleChange(propertyName, e.target.value)} + /> +
+ ); + + case 'boolean': + return ( +
+ + handleChange(propertyName, e.target.checked)} + /> +
+ ); + + case 'color': + return ( +
+ + handleChange(propertyName, e.target.value)} + /> +
+ ); + + case 'vector2': + case 'vector3': + return ( +
+ +
+ handleChange(propertyName, { ...value, x: parseFloat(e.target.value) || 0 })} + /> + handleChange(propertyName, { ...value, y: parseFloat(e.target.value) || 0 })} + /> + {metadata.type === 'vector3' && ( + handleChange(propertyName, { ...value, z: parseFloat(e.target.value) || 0 })} + /> + )} +
+
+ ); + + default: + return null; + } + }; + + return ( +
+ {Object.entries(properties).map(([propertyName, metadata]) => + renderProperty(propertyName, metadata) + )} +
+ ); +} diff --git a/packages/editor-app/src/styles/EntityInspector.css b/packages/editor-app/src/styles/EntityInspector.css index 62935e03..1179c313 100644 --- a/packages/editor-app/src/styles/EntityInspector.css +++ b/packages/editor-app/src/styles/EntityInspector.css @@ -90,22 +90,48 @@ } .component-item { - display: flex; - align-items: center; - padding: 8px; - margin-bottom: 4px; + margin-bottom: 8px; background-color: #252526; border: 1px solid #3c3c3c; border-radius: 3px; font-size: 13px; - position: relative; + overflow: hidden; } .component-item:hover { - background-color: #2a2d2e; border-color: #505050; } +.component-header { + display: flex; + align-items: center; + padding: 8px; + cursor: pointer; + transition: background-color 0.2s; +} + +.component-header:hover { + background-color: #2a2d2e; +} + +.component-expand-btn { + background: none; + border: none; + color: #858585; + font-size: 10px; + cursor: pointer; + padding: 4px; + margin-right: 4px; + transition: color 0.2s; + display: flex; + align-items: center; + justify-content: center; +} + +.component-expand-btn:hover { + color: #cccccc; +} + .component-icon { margin-right: 8px; font-size: 14px; @@ -132,6 +158,12 @@ color: #ff5555; } +.component-properties { + border-top: 1px solid #3c3c3c; + background-color: #1e1e1e; + padding: 4px; +} + .empty-state { padding: 20px; text-align: center; diff --git a/packages/editor-app/src/styles/PropertyInspector.css b/packages/editor-app/src/styles/PropertyInspector.css new file mode 100644 index 00000000..892ea7d9 --- /dev/null +++ b/packages/editor-app/src/styles/PropertyInspector.css @@ -0,0 +1,80 @@ +.property-inspector { + padding: 8px; +} + +.property-field { + display: flex; + flex-direction: column; + margin-bottom: 12px; +} + +.property-field-checkbox { + flex-direction: row; + align-items: center; + gap: 8px; +} + +.property-label { + font-size: 12px; + font-weight: 500; + color: #e0e0e0; + margin-bottom: 4px; +} + +.property-field-checkbox .property-label { + margin-bottom: 0; + flex: 1; +} + +.property-input { + background: #2a2a2a; + border: 1px solid #444; + border-radius: 4px; + padding: 6px 8px; + color: #e0e0e0; + font-size: 13px; + font-family: inherit; +} + +.property-input:focus { + outline: none; + border-color: #4a9eff; + background: #333; +} + +.property-input:disabled { + opacity: 0.5; + cursor: not-allowed; +} + +.property-checkbox { + width: 18px; + height: 18px; + cursor: pointer; +} + +.property-checkbox:disabled { + opacity: 0.5; + cursor: not-allowed; +} + +.property-color { + height: 36px; + padding: 2px; + cursor: pointer; +} + +.property-vector { + display: flex; + gap: 6px; +} + +.property-vector-input { + flex: 1; + min-width: 0; +} + +input[type="number"]::-webkit-inner-spin-button, +input[type="number"]::-webkit-outer-spin-button { + opacity: 1; +} diff --git a/packages/editor-core/src/Services/PropertyMetadata.ts b/packages/editor-core/src/Services/PropertyMetadata.ts new file mode 100644 index 00000000..c8e2b44e --- /dev/null +++ b/packages/editor-core/src/Services/PropertyMetadata.ts @@ -0,0 +1,87 @@ +import type { IService } from '@esengine/ecs-framework'; +import { Injectable, Component } from '@esengine/ecs-framework'; +import { createLogger } from '@esengine/ecs-framework'; + +const logger = createLogger('PropertyMetadata'); + +export type PropertyType = 'number' | 'string' | 'boolean' | 'color' | 'vector2' | 'vector3'; + +export interface PropertyMetadata { + type: PropertyType; + label?: string; + min?: number; + max?: number; + step?: number; + options?: Array<{ label: string; value: any }>; + readOnly?: boolean; +} + +export interface ComponentMetadata { + properties: Record; +} + +/** + * 组件属性元数据服务 + * + * 管理组件属性的元数据信息,用于动态生成属性编辑器 + */ +@Injectable() +export class PropertyMetadataService implements IService { + private metadata: Map Component, ComponentMetadata> = new Map(); + + /** + * 注册组件元数据 + */ + public register(componentType: new (...args: any[]) => Component, metadata: ComponentMetadata): void { + this.metadata.set(componentType, metadata); + logger.debug(`Registered metadata for component: ${componentType.name}`); + } + + /** + * 获取组件元数据 + */ + public getMetadata(componentType: new (...args: any[]) => Component): ComponentMetadata | undefined { + return this.metadata.get(componentType); + } + + /** + * 获取组件的所有可编辑属性 + */ + public getEditableProperties(component: Component): Record { + const metadata = this.metadata.get(component.constructor as new (...args: any[]) => Component); + if (!metadata) { + return this.inferProperties(component); + } + return metadata.properties; + } + + /** + * 推断组件属性(当没有明确元数据时) + */ + private inferProperties(component: Component): Record { + const properties: Record = {}; + const componentAsAny = component as any; + + for (const key in component) { + if (component.hasOwnProperty(key)) { + const value = componentAsAny[key]; + const type = typeof value; + + if (type === 'number') { + properties[key] = { type: 'number' }; + } else if (type === 'string') { + properties[key] = { type: 'string' }; + } else if (type === 'boolean') { + properties[key] = { type: 'boolean' }; + } + } + } + + return properties; + } + + public dispose(): void { + this.metadata.clear(); + logger.info('PropertyMetadataService disposed'); + } +} diff --git a/packages/editor-core/src/index.ts b/packages/editor-core/src/index.ts index 60c1d5e9..12bac4a8 100644 --- a/packages/editor-core/src/index.ts +++ b/packages/editor-core/src/index.ts @@ -13,5 +13,6 @@ export * from './Services/SerializerRegistry'; export * from './Services/EntityStoreService'; export * from './Services/ComponentRegistry'; export * from './Services/LocaleService'; +export * from './Services/PropertyMetadata'; export * from './Types/UITypes';