From 37ab494e4aefe27131c59bf344d3e3831773c864 Mon Sep 17 00:00:00 2001 From: yhh <359807859@qq.com> Date: Wed, 3 Dec 2025 16:20:48 +0800 Subject: [PATCH] =?UTF-8?q?feat(modules):=20=E6=B7=BB=E5=8A=A0module.json?= =?UTF-8?q?=E9=85=8D=E7=BD=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/audio/module.json | 43 + packages/audio/src/AudioPlugin.ts | 20 +- .../src/BehaviorTreePlugin.ts | 43 +- packages/behavior-tree-editor/src/index.ts | 6 +- packages/behavior-tree/module.json | 45 + .../src/BehaviorTreeRuntimeModule.ts | 28 +- .../execution/BehaviorTreeExecutionSystem.ts | 51 +- .../src/loaders/BehaviorTreeLoader.ts | 64 +- .../blueprint-editor/src/BlueprintPlugin.ts | 26 +- packages/blueprint/module.json | 43 + packages/blueprint/package.json | 1 + packages/blueprint/src/BlueprintPlugin.ts | 60 ++ packages/blueprint/src/index.ts | 3 + packages/camera/module.json | 38 + packages/camera/src/CameraPlugin.ts | 20 +- packages/sprite-editor/package.json | 2 + .../src/SpriteComponentInspector.css | 585 +++++++++++ .../src/SpriteComponentInspector.tsx | 983 ++++++++++++++++++ packages/sprite-editor/src/index.ts | 53 +- packages/sprite/module.json | 38 + packages/sprite/src/SpriteComponent.ts | 238 ++++- packages/sprite/src/SpriteRuntimeModule.ts | 26 +- packages/sprite/src/index.ts | 1 + packages/ui-editor/src/index.ts | 20 +- packages/ui/module.json | 43 + packages/ui/src/UIRuntimeModule.ts | 23 +- 26 files changed, 2356 insertions(+), 147 deletions(-) create mode 100644 packages/audio/module.json create mode 100644 packages/behavior-tree/module.json create mode 100644 packages/blueprint/module.json create mode 100644 packages/blueprint/src/BlueprintPlugin.ts create mode 100644 packages/camera/module.json create mode 100644 packages/sprite-editor/src/SpriteComponentInspector.css create mode 100644 packages/sprite-editor/src/SpriteComponentInspector.tsx create mode 100644 packages/sprite/module.json create mode 100644 packages/ui/module.json diff --git a/packages/audio/module.json b/packages/audio/module.json new file mode 100644 index 00000000..fcba94ab --- /dev/null +++ b/packages/audio/module.json @@ -0,0 +1,43 @@ +{ + "id": "audio", + "name": "@esengine/audio", + "displayName": "Audio", + "description": "Audio playback and sound effects | 音频播放和音效", + "version": "1.0.0", + "category": "Audio", + "icon": "Volume2", + "tags": [ + "audio", + "sound", + "music" + ], + "isCore": false, + "defaultEnabled": false, + "isEngineModule": true, + "canContainContent": true, + "platforms": [ + "web", + "desktop", + "mobile" + ], + "dependencies": [ + "core", + "asset-system" + ], + "exports": { + "components": [ + "AudioSourceComponent", + "AudioListenerComponent" + ], + "systems": [ + "AudioSystem" + ], + "other": [ + "AudioClip", + "AudioMixer" + ] + }, + "requiresWasm": false, + "outputPath": "dist/index.js", + "pluginExport": "AudioPlugin" +} diff --git a/packages/audio/src/AudioPlugin.ts b/packages/audio/src/AudioPlugin.ts index 4e7ccb2e..ca897d83 100644 --- a/packages/audio/src/AudioPlugin.ts +++ b/packages/audio/src/AudioPlugin.ts @@ -1,5 +1,5 @@ import type { ComponentRegistry as ComponentRegistryType } from '@esengine/ecs-framework'; -import type { IRuntimeModule, IPlugin, PluginDescriptor } from '@esengine/engine-core'; +import type { IRuntimeModule, IPlugin, ModuleManifest } from '@esengine/engine-core'; import { AudioSourceComponent } from './AudioSourceComponent'; class AudioRuntimeModule implements IRuntimeModule { @@ -8,17 +8,21 @@ class AudioRuntimeModule implements IRuntimeModule { } } -const descriptor: PluginDescriptor = { - id: '@esengine/audio', - name: 'Audio', +const manifest: ModuleManifest = { + id: 'audio', + name: '@esengine/audio', + displayName: 'Audio', version: '1.0.0', description: '音频组件', - category: 'audio', - enabledByDefault: true, - isEnginePlugin: true + category: 'Audio', + isCore: false, + defaultEnabled: true, + isEngineModule: true, + dependencies: ['core', 'asset-system'], + exports: { components: ['AudioSourceComponent'] } }; export const AudioPlugin: IPlugin = { - descriptor, + manifest, runtimeModule: new AudioRuntimeModule() }; diff --git a/packages/behavior-tree-editor/src/BehaviorTreePlugin.ts b/packages/behavior-tree-editor/src/BehaviorTreePlugin.ts index b5cdca6f..35828750 100644 --- a/packages/behavior-tree-editor/src/BehaviorTreePlugin.ts +++ b/packages/behavior-tree-editor/src/BehaviorTreePlugin.ts @@ -1,36 +1,29 @@ /** - * Behavior Tree Plugin Descriptor - * 行为树插件描述符 + * Behavior Tree Plugin Manifest + * 行为树插件清单 */ -import type { PluginDescriptor } from '@esengine/editor-runtime'; +import type { ModuleManifest } from '@esengine/editor-runtime'; /** - * 插件描述符 + * 插件清单 */ -export const descriptor: PluginDescriptor = { +export const manifest: ModuleManifest = { id: '@esengine/behavior-tree', - name: 'Behavior Tree System', + name: '@esengine/behavior-tree', + displayName: 'Behavior Tree System', version: '1.0.0', description: 'AI 行为树系统,支持可视化编辑和运行时执行', - category: 'ai', - enabledByDefault: true, + category: 'AI', + icon: 'GitBranch', + isCore: false, + defaultEnabled: true, + isEngineModule: false, canContainContent: false, - isEnginePlugin: false, - modules: [ - { - name: 'BehaviorTreeRuntime', - type: 'runtime', - loadingPhase: 'default' - }, - { - name: 'BehaviorTreeEditor', - type: 'editor', - loadingPhase: 'default' - } - ], - dependencies: [ - { id: '@esengine/engine-core', version: '>=1.0.0', optional: true } - ], - icon: 'GitBranch' + dependencies: ['engine-core'], + exports: { + components: ['BehaviorTreeRuntimeComponent'], + systems: ['BehaviorTreeExecutionSystem'], + loaders: ['BehaviorTreeLoader'] + } }; diff --git a/packages/behavior-tree-editor/src/index.ts b/packages/behavior-tree-editor/src/index.ts index 6772feae..edb14154 100644 --- a/packages/behavior-tree-editor/src/index.ts +++ b/packages/behavior-tree-editor/src/index.ts @@ -42,8 +42,8 @@ import { useBehaviorTreeDataStore } from './stores'; import { createRootNode } from './domain/constants/RootNode'; import { PluginContext } from './PluginContext'; -// Import descriptor from local file -import { descriptor } from './BehaviorTreePlugin'; +// Import manifest from local file +import { manifest } from './BehaviorTreePlugin'; // 导入编辑器 CSS 样式(会被 vite 自动处理并注入到 DOM) // Import editor CSS styles (automatically handled and injected by vite) @@ -340,7 +340,7 @@ export class BehaviorTreeEditorModule implements IEditorModuleLoader { // Create the complete plugin with editor module export const BehaviorTreePlugin: IPluginLoader = { - descriptor, + manifest, runtimeModule: new BehaviorTreeRuntimeModule(), editorModule: new BehaviorTreeEditorModule(), }; diff --git a/packages/behavior-tree/module.json b/packages/behavior-tree/module.json new file mode 100644 index 00000000..528a7317 --- /dev/null +++ b/packages/behavior-tree/module.json @@ -0,0 +1,45 @@ +{ + "id": "behavior-tree", + "name": "@esengine/behavior-tree", + "displayName": "Behavior Tree", + "description": "AI behavior tree system | AI 行为树系统", + "version": "1.0.0", + "category": "AI", + "icon": "GitBranch", + "tags": [ + "ai", + "behavior", + "tree" + ], + "isCore": false, + "defaultEnabled": false, + "isEngineModule": true, + "canContainContent": true, + "platforms": [ + "web", + "desktop" + ], + "dependencies": [ + "core" + ], + "exports": { + "components": [ + "BehaviorTreeComponent" + ], + "systems": [ + "BehaviorTreeSystem" + ], + "other": [ + "BehaviorTree", + "BTNode", + "Selector", + "Sequence", + "Condition", + "Action" + ] + }, + "editorPackage": "@esengine/behavior-tree-editor", + "requiresWasm": false, + "outputPath": "dist/index.js", + "pluginExport": "BehaviorTreePlugin" +} diff --git a/packages/behavior-tree/src/BehaviorTreeRuntimeModule.ts b/packages/behavior-tree/src/BehaviorTreeRuntimeModule.ts index 025b8543..44348e37 100644 --- a/packages/behavior-tree/src/BehaviorTreeRuntimeModule.ts +++ b/packages/behavior-tree/src/BehaviorTreeRuntimeModule.ts @@ -1,6 +1,6 @@ import type { IScene, ServiceContainer } from '@esengine/ecs-framework'; import { ComponentRegistry, Core } from '@esengine/ecs-framework'; -import type { IRuntimeModule, IPlugin, PluginDescriptor, SystemContext } from '@esengine/engine-core'; +import type { IRuntimeModule, IPlugin, ModuleManifest, SystemContext } from '@esengine/engine-core'; import type { AssetManager } from '@esengine/asset-system'; import { BehaviorTreeRuntimeComponent } from './execution/BehaviorTreeRuntimeComponent'; @@ -39,7 +39,10 @@ class BehaviorTreeRuntimeModule implements IRuntimeModule { this._loaderRegistered = true; } - const behaviorTreeSystem = new BehaviorTreeExecutionSystem(Core); + // 使用 context 中的 services,确保与调用方使用同一个 ServiceContainer 实例 + // Use services from context to ensure same ServiceContainer instance as caller + const services = (btContext as any).services || Core.services; + const behaviorTreeSystem = new BehaviorTreeExecutionSystem(services); if (btContext.assetManager) { behaviorTreeSystem.setAssetManager(btContext.assetManager); @@ -54,18 +57,25 @@ class BehaviorTreeRuntimeModule implements IRuntimeModule { } } -const descriptor: PluginDescriptor = { - id: '@esengine/behavior-tree', - name: 'Behavior Tree', +const manifest: ModuleManifest = { + id: 'behavior-tree', + name: '@esengine/behavior-tree', + displayName: 'Behavior Tree', version: '1.0.0', description: 'AI behavior tree system', - category: 'ai', - enabledByDefault: false, - isEnginePlugin: true + category: 'AI', + icon: 'GitBranch', + isCore: false, + defaultEnabled: false, + isEngineModule: true, + canContainContent: true, + dependencies: ['core'], + exports: { components: ['BehaviorTreeComponent'] }, + editorPackage: '@esengine/behavior-tree-editor' }; export const BehaviorTreePlugin: IPlugin = { - descriptor, + manifest, runtimeModule: new BehaviorTreeRuntimeModule() }; diff --git a/packages/behavior-tree/src/execution/BehaviorTreeExecutionSystem.ts b/packages/behavior-tree/src/execution/BehaviorTreeExecutionSystem.ts index 50b89905..caffca08 100644 --- a/packages/behavior-tree/src/execution/BehaviorTreeExecutionSystem.ts +++ b/packages/behavior-tree/src/execution/BehaviorTreeExecutionSystem.ts @@ -1,4 +1,4 @@ -import { EntitySystem, Matcher, Entity, Time, Core, ECSSystem } from '@esengine/ecs-framework'; +import { EntitySystem, Matcher, Entity, Time, Core, ECSSystem, ServiceContainer } from '@esengine/ecs-framework'; import type { AssetManager } from '@esengine/asset-system'; import { BehaviorTreeRuntimeComponent } from './BehaviorTreeRuntimeComponent'; import { BehaviorTreeAssetManager } from './BehaviorTreeAssetManager'; @@ -6,6 +6,7 @@ import { NodeExecutorRegistry, NodeExecutionContext } from './NodeExecutor'; import { BehaviorTreeData, BehaviorNodeData } from './BehaviorTreeData'; import { TaskStatus } from '../Types/TaskStatus'; import { NodeMetadataRegistry } from './NodeMetadata'; +import type { IBehaviorTreeAsset } from '../loaders/BehaviorTreeLoader'; import './Executors'; /** @@ -17,14 +18,17 @@ import './Executors'; export class BehaviorTreeExecutionSystem extends EntitySystem { private btAssetManager: BehaviorTreeAssetManager | null = null; private executorRegistry: NodeExecutorRegistry; - private coreInstance: typeof Core | null = null; + private _services: ServiceContainer | null = null; /** 引用 asset-system 的 AssetManager(由 BehaviorTreeRuntimeModule 设置) */ private _assetManager: AssetManager | null = null; - constructor(coreInstance?: typeof Core) { + /** 已警告过的缺失资产,避免重复警告 */ + private _warnedMissingAssets: Set = new Set(); + + constructor(services?: ServiceContainer) { super(Matcher.empty().all(BehaviorTreeRuntimeComponent)); - this.coreInstance = coreInstance || null; + this._services = services || null; this.executorRegistry = new NodeExecutorRegistry(); this.registerBuiltInExecutors(); } @@ -121,12 +125,38 @@ export class BehaviorTreeExecutionSystem extends EntitySystem { private getBTAssetManager(): BehaviorTreeAssetManager { if (!this.btAssetManager) { - const core = this.coreInstance || Core; - this.btAssetManager = core.services.resolve(BehaviorTreeAssetManager); + // 优先使用传入的 services,否则回退到全局 Core.services + // Prefer passed services, fallback to global Core.services + const services = this._services || Core.services; + if (!services) { + throw new Error('ServiceContainer is not available. Ensure Core.create() was called.'); + } + this.btAssetManager = services.resolve(BehaviorTreeAssetManager); } return this.btAssetManager; } + /** + * 获取行为树数据 + * Get behavior tree data from AssetManager or BehaviorTreeAssetManager + * + * 优先从 AssetManager 获取(新方式),如果没有再从 BehaviorTreeAssetManager 获取(兼容旧方式) + */ + private getTreeData(assetIdOrPath: string): BehaviorTreeData | undefined { + // 1. 优先从 AssetManager 获取(如果已加载) + // First try AssetManager (preferred way) + if (this._assetManager) { + const cachedAsset = this._assetManager.getAssetByPath(assetIdOrPath); + if (cachedAsset?.data) { + return cachedAsset.data; + } + } + + // 2. 回退到 BehaviorTreeAssetManager(兼容旧方式) + // Fallback to BehaviorTreeAssetManager (legacy support) + return this.getBTAssetManager().getAsset(assetIdOrPath); + } + /** * 注册所有执行器(包括内置和插件提供的) */ @@ -158,9 +188,14 @@ export class BehaviorTreeExecutionSystem extends EntitySystem { continue; } - const treeData = this.getBTAssetManager().getAsset(runtime.treeAssetId); + const treeData = this.getTreeData(runtime.treeAssetId); if (!treeData) { - this.logger.warn(`未找到行为树资产: ${runtime.treeAssetId}`); + // 只警告一次,避免每帧重复输出 + // Only warn once to avoid repeated output every frame + if (!this._warnedMissingAssets.has(runtime.treeAssetId)) { + this._warnedMissingAssets.add(runtime.treeAssetId); + this.logger.warn(`未找到行为树资产: ${runtime.treeAssetId}`); + } continue; } diff --git a/packages/behavior-tree/src/loaders/BehaviorTreeLoader.ts b/packages/behavior-tree/src/loaders/BehaviorTreeLoader.ts index befd7610..6d22d33f 100644 --- a/packages/behavior-tree/src/loaders/BehaviorTreeLoader.ts +++ b/packages/behavior-tree/src/loaders/BehaviorTreeLoader.ts @@ -7,9 +7,9 @@ import type { IAssetLoader, - IAssetMetadata, - IAssetLoadOptions, - IAssetLoadResult + IAssetParseContext, + IAssetContent, + AssetContentType } from '@esengine/asset-system'; import { Core } from '@esengine/ecs-framework'; import { BehaviorTreeData } from '../execution/BehaviorTreeData'; @@ -34,60 +34,38 @@ export interface IBehaviorTreeAsset { export class BehaviorTreeLoader implements IAssetLoader { readonly supportedType = BehaviorTreeAssetType; readonly supportedExtensions = ['.btree']; + readonly contentType: AssetContentType = 'text'; /** - * 加载行为树资产 - * Load behavior tree asset + * 从内容解析行为树资产 + * Parse behavior tree asset from content */ - async load( - path: string, - metadata: IAssetMetadata, - _options?: IAssetLoadOptions - ): Promise> { - // 获取文件系统服务 - const IFileSystemServiceKey = Symbol.for('IFileSystemService'); - const fileSystem = Core.services.tryResolve(IFileSystemServiceKey) as IFileSystem | null; - - if (!fileSystem) { - throw new Error('FileSystem service not available'); + async parse(content: IAssetContent, context: IAssetParseContext): Promise { + if (!content.text) { + throw new Error('Behavior tree content is empty'); } - // 读取文件内容 - const content = await fileSystem.readFile(path); - // 转换为运行时数据 - const treeData = EditorToBehaviorTreeDataConverter.fromEditorJSON(content); + const treeData = EditorToBehaviorTreeDataConverter.fromEditorJSON(content.text); // 使用文件路径作为 ID - treeData.id = path; + const assetPath = context.metadata.path; + treeData.id = assetPath; - // 注册到 BehaviorTreeAssetManager(保持兼容性) + // 同时注册到 BehaviorTreeAssetManager + // Also register to BehaviorTreeAssetManager for legacy code that uses it directly + // (e.g., loadFromEditorJSON, or code that doesn't use AssetManager) const btAssetManager = Core.services.tryResolve(BehaviorTreeAssetManager); if (btAssetManager) { btAssetManager.loadAsset(treeData); } - const asset: IBehaviorTreeAsset = { - data: treeData, - path - }; - return { - asset, - handle: 0, // 由 AssetManager 分配 - metadata, - loadTime: 0 + data: treeData, + path: assetPath }; } - /** - * 检查是否可以加载 - * Check if can load this asset - */ - canLoad(path: string, _metadata: IAssetMetadata): boolean { - return path.endsWith('.btree'); - } - /** * 释放资产 * Dispose asset @@ -100,11 +78,3 @@ export class BehaviorTreeLoader implements IAssetLoader { } } } - -/** - * 文件系统接口(简化版,仅用于类型) - */ -interface IFileSystem { - readFile(path: string): Promise; - exists(path: string): Promise; -} diff --git a/packages/blueprint-editor/src/BlueprintPlugin.ts b/packages/blueprint-editor/src/BlueprintPlugin.ts index 8b7412cb..f19e38bd 100644 --- a/packages/blueprint-editor/src/BlueprintPlugin.ts +++ b/packages/blueprint-editor/src/BlueprintPlugin.ts @@ -4,7 +4,7 @@ */ import { Core, type ServiceContainer } from '@esengine/ecs-framework'; -import type { IPlugin, PluginDescriptor } from '@esengine/engine-core'; +import type { IPlugin, ModuleManifest } from '@esengine/engine-core'; import type { IEditorModuleLoader, PanelDescriptor, FileActionHandler, FileCreationTemplate } from '@esengine/editor-core'; import { MessageHub, PanelPosition } from '@esengine/editor-core'; @@ -95,19 +95,23 @@ class BlueprintEditorModuleImpl implements IEditorModuleLoader { } } -const descriptor: PluginDescriptor = { +const manifest: ModuleManifest = { id: '@esengine/blueprint', - name: 'Blueprint', + name: '@esengine/blueprint', + displayName: 'Blueprint', version: '1.0.0', description: 'Visual scripting system for ECS Framework', - category: 'scripting', - enabledByDefault: false, - isEnginePlugin: true, + category: 'Other', + isCore: false, + defaultEnabled: false, + isEngineModule: true, canContainContent: true, - modules: [ - { name: 'Runtime', type: 'runtime', loadingPhase: 'default' }, - { name: 'Editor', type: 'editor', loadingPhase: 'postDefault' } - ] + dependencies: ['engine-core'], + exports: { + components: ['BlueprintComponent'], + systems: ['BlueprintSystem'], + other: ['NodeRegistry', 'BlueprintVM'] + } }; /** @@ -115,7 +119,7 @@ const descriptor: PluginDescriptor = { * 完整的蓝图插件,包含运行时和编辑器模块 */ export const BlueprintPlugin: IPlugin = { - descriptor, + manifest, editorModule: new BlueprintEditorModuleImpl() }; diff --git a/packages/blueprint/module.json b/packages/blueprint/module.json new file mode 100644 index 00000000..6a7f8475 --- /dev/null +++ b/packages/blueprint/module.json @@ -0,0 +1,43 @@ +{ + "id": "blueprint", + "name": "@esengine/blueprint", + "displayName": "Blueprint", + "description": "Visual scripting system | 可视化脚本系统", + "version": "1.0.0", + "category": "AI", + "icon": "Workflow", + "tags": [ + "visual", + "scripting", + "blueprint", + "nodes" + ], + "isCore": false, + "defaultEnabled": false, + "isEngineModule": true, + "canContainContent": true, + "platforms": [ + "web", + "desktop" + ], + "dependencies": [ + "core" + ], + "exports": { + "components": [ + "BlueprintComponent" + ], + "systems": [ + "BlueprintSystem" + ], + "other": [ + "Blueprint", + "BlueprintNode", + "BlueprintGraph" + ] + }, + "editorPackage": "@esengine/blueprint-editor", + "requiresWasm": false, + "outputPath": "dist/index.js", + "pluginExport": "BlueprintPlugin" +} diff --git a/packages/blueprint/package.json b/packages/blueprint/package.json index 748b7d01..21210c63 100644 --- a/packages/blueprint/package.json +++ b/packages/blueprint/package.json @@ -31,6 +31,7 @@ "license": "MIT", "devDependencies": { "@esengine/ecs-framework": "workspace:*", + "@esengine/engine-core": "workspace:*", "@esengine/build-config": "workspace:*", "@types/node": "^20.19.17", "rimraf": "^5.0.0", diff --git a/packages/blueprint/src/BlueprintPlugin.ts b/packages/blueprint/src/BlueprintPlugin.ts new file mode 100644 index 00000000..e7566c3b --- /dev/null +++ b/packages/blueprint/src/BlueprintPlugin.ts @@ -0,0 +1,60 @@ +/** + * Blueprint Plugin for ES Engine. + * ES引擎的蓝图插件。 + * + * Provides visual scripting runtime support. + * 提供可视化脚本运行时支持。 + */ + +import type { IPlugin, ModuleManifest, IRuntimeModule } from '@esengine/engine-core'; + +/** + * Blueprint Runtime Module. + * 蓝图运行时模块。 + * + * Note: Blueprint uses a custom system (IBlueprintSystem) instead of EntitySystem, + * so createSystems is not implemented here. Blueprint systems should be created + * manually using createBlueprintSystem(scene). + */ +class BlueprintRuntimeModule implements IRuntimeModule { + async onInitialize(): Promise { + // Blueprint system initialization + // Blueprint uses IBlueprintSystem which is different from EntitySystem + } + + onDestroy(): void { + // Cleanup + } +} + +/** + * Plugin manifest for Blueprint. + * 蓝图的插件清单。 + */ +const manifest: ModuleManifest = { + id: 'blueprint', + name: '@esengine/blueprint', + displayName: 'Blueprint', + version: '1.0.0', + description: '可视化脚本系统', + category: 'AI', + icon: 'Workflow', + isCore: false, + defaultEnabled: false, + isEngineModule: true, + dependencies: ['core'], + exports: { + components: ['BlueprintComponent'], + systems: ['BlueprintSystem'] + }, + requiresWasm: false +}; + +/** + * Blueprint Plugin. + * 蓝图插件。 + */ +export const BlueprintPlugin: IPlugin = { + manifest, + runtimeModule: new BlueprintRuntimeModule() +}; diff --git a/packages/blueprint/src/index.ts b/packages/blueprint/src/index.ts index 5ba686b4..83ca22b0 100644 --- a/packages/blueprint/src/index.ts +++ b/packages/blueprint/src/index.ts @@ -29,3 +29,6 @@ export { triggerCustomBlueprintEvent } from './runtime/BlueprintSystem'; export { createEmptyBlueprint, validateBlueprintAsset } from './types/blueprint'; + +// Plugin +export { BlueprintPlugin } from './BlueprintPlugin'; diff --git a/packages/camera/module.json b/packages/camera/module.json new file mode 100644 index 00000000..11a9c217 --- /dev/null +++ b/packages/camera/module.json @@ -0,0 +1,38 @@ +{ + "id": "camera", + "name": "@esengine/camera", + "displayName": "Camera", + "description": "Camera and viewport management | 相机和视口管理", + "version": "1.0.0", + "category": "Rendering", + "icon": "Video", + "tags": [ + "camera", + "viewport", + "rendering" + ], + "isCore": false, + "defaultEnabled": true, + "isEngineModule": true, + "canContainContent": false, + "platforms": [ + "web", + "desktop", + "mobile" + ], + "dependencies": [ + "core", + "math" + ], + "exports": { + "components": [ + "CameraComponent" + ], + "systems": [ + "CameraSystem" + ] + }, + "requiresWasm": false, + "outputPath": "dist/index.js", + "pluginExport": "CameraPlugin" +} diff --git a/packages/camera/src/CameraPlugin.ts b/packages/camera/src/CameraPlugin.ts index 457b541b..d129c42f 100644 --- a/packages/camera/src/CameraPlugin.ts +++ b/packages/camera/src/CameraPlugin.ts @@ -1,5 +1,5 @@ import type { ComponentRegistry as ComponentRegistryType } from '@esengine/ecs-framework'; -import type { IRuntimeModule, IPlugin, PluginDescriptor } from '@esengine/engine-core'; +import type { IRuntimeModule, IPlugin, ModuleManifest } from '@esengine/engine-core'; import { CameraComponent } from './CameraComponent'; class CameraRuntimeModule implements IRuntimeModule { @@ -8,17 +8,21 @@ class CameraRuntimeModule implements IRuntimeModule { } } -const descriptor: PluginDescriptor = { - id: '@esengine/camera', - name: 'Camera', +const manifest: ModuleManifest = { + id: 'camera', + name: '@esengine/camera', + displayName: 'Camera', version: '1.0.0', description: '2D/3D 相机组件', - category: 'core', - enabledByDefault: true, - isEnginePlugin: true + category: 'Rendering', + isCore: false, + defaultEnabled: true, + isEngineModule: true, + dependencies: ['core', 'math'], + exports: { components: ['CameraComponent'] } }; export const CameraPlugin: IPlugin = { - descriptor, + manifest, runtimeModule: new CameraRuntimeModule() }; diff --git a/packages/sprite-editor/package.json b/packages/sprite-editor/package.json index 234a67b0..af10a1ff 100644 --- a/packages/sprite-editor/package.json +++ b/packages/sprite-editor/package.json @@ -26,9 +26,11 @@ "@esengine/engine-core": "workspace:*", "@esengine/sprite": "workspace:*", "@esengine/editor-core": "workspace:*", + "@esengine/material-system": "workspace:*", "@esengine/build-config": "workspace:*", "react": "^18.3.1", "@types/react": "^18.2.0", + "lucide-react": "^0.453.0", "rimraf": "^5.0.5", "tsup": "^8.0.0", "typescript": "^5.3.3" diff --git a/packages/sprite-editor/src/SpriteComponentInspector.css b/packages/sprite-editor/src/SpriteComponentInspector.css new file mode 100644 index 00000000..2f70a4c1 --- /dev/null +++ b/packages/sprite-editor/src/SpriteComponentInspector.css @@ -0,0 +1,585 @@ +/** + * Sprite Component Inspector Styles. + * 精灵组件检查器样式。 + */ + +.sprite-component-inspector { + display: flex; + flex-direction: column; + gap: 8px; +} + +/* Material Override Section */ +.material-override-section { + border: 1px solid var(--border-color, #333); + border-radius: 4px; + margin-top: 8px; + background: var(--bg-secondary, #252526); +} + +.material-override-header { + display: flex; + align-items: center; + gap: 4px; + padding: 8px 10px; + cursor: pointer; + user-select: none; +} + +.material-override-header:hover { + background: var(--bg-hover, #2a2a2a); +} + +.material-override-expand { + color: var(--text-secondary, #888); +} + +.material-override-title { + flex: 1; + font-weight: 500; + font-size: 12px; + color: var(--text-primary, #e0e0e0); +} + +.material-override-count { + padding: 2px 6px; + border-radius: 10px; + background: var(--accent-color, #0078d4); + color: white; + font-size: 10px; + font-weight: 500; +} + +.material-override-content { + padding: 8px 10px; + border-top: 1px solid var(--border-color, #333); +} + +/* Override Items */ +.material-override-item { + padding: 8px; + margin-bottom: 8px; + border-radius: 4px; + background: var(--bg-tertiary, #1e1e1e); +} + +.material-override-item:last-child { + margin-bottom: 0; +} + +.material-override-item-header { + display: flex; + align-items: center; + gap: 6px; + margin-bottom: 6px; +} + +.material-override-name { + flex: 1; + font-size: 11px; + font-weight: 500; + color: var(--text-primary, #e0e0e0); +} + +.material-override-type { + font-size: 10px; + color: var(--text-tertiary, #666); + font-style: italic; +} + +.material-override-remove { + display: flex; + align-items: center; + justify-content: center; + width: 18px; + height: 18px; + border: none; + border-radius: 3px; + background: transparent; + color: var(--text-secondary, #888); + cursor: pointer; +} + +.material-override-remove:hover { + background: var(--error-bg, #3a2020); + color: var(--error-color, #f87171); +} + +/* Override Inputs */ +.override-input { + width: 100%; + padding: 4px 8px; + border: 1px solid var(--border-color, #333); + border-radius: 3px; + background: var(--input-bg, #333); + color: var(--text-primary, #e0e0e0); + font-size: 11px; +} + +.override-input:focus { + outline: none; + border-color: var(--accent-color, #0078d4); +} + +.override-input-number { + text-align: right; +} + +/* Vector Inputs */ +.override-vector { + display: flex; + gap: 4px; +} + +.override-vector-4 { + flex-wrap: wrap; +} + +.override-vector-axis { + flex: 1; + display: flex; + align-items: center; + gap: 2px; + min-width: 50px; +} + +.override-axis-label { + width: 14px; + height: 14px; + display: flex; + align-items: center; + justify-content: center; + border-radius: 2px; + font-size: 9px; + font-weight: 600; + color: white; + flex-shrink: 0; +} + +.override-axis-x { background: #e06666; } +.override-axis-y { background: #93c47d; } +.override-axis-z { background: #6fa8dc; } +.override-axis-w { background: #b4a7d6; } + +.override-vector-axis .override-input { + flex: 1; + min-width: 0; + padding: 3px 4px; + text-align: right; +} + +/* Color Input */ +.override-color { + display: flex; + align-items: center; + gap: 6px; +} + +.override-color-preview { + width: 24px; + height: 24px; + border-radius: 4px; + border: 1px solid var(--border-color, #333); + flex-shrink: 0; +} + +.override-color-input { + width: 60px; + height: 24px; + padding: 0; + border: none; + background: transparent; + cursor: pointer; +} + +.override-color-input::-webkit-color-swatch-wrapper { + padding: 0; +} + +.override-color-input::-webkit-color-swatch { + border-radius: 3px; + border: 1px solid var(--border-color, #333); +} + +.override-alpha { + width: 50px; + text-align: right; +} + +/* Add Override */ +.material-override-add-container { + position: relative; + margin-top: 8px; +} + +.material-override-add-btn { + display: flex; + align-items: center; + gap: 4px; + width: 100%; + padding: 6px 10px; + border: 1px dashed var(--border-color, #444); + border-radius: 4px; + background: transparent; + color: var(--text-secondary, #888); + font-size: 11px; + cursor: pointer; + transition: all 0.15s ease; +} + +.material-override-add-btn:hover { + border-color: var(--accent-color, #0078d4); + color: var(--accent-color, #0078d4); + background: var(--accent-bg, rgba(0, 120, 212, 0.1)); +} + +.material-override-add-menu { + position: absolute; + left: 0; + right: 0; + top: 100%; + margin-top: 4px; + padding: 4px 0; + border-radius: 4px; + background: var(--dropdown-bg, #2d2d2d); + border: 1px solid var(--border-color, #444); + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3); + z-index: 100; + max-height: 200px; + overflow-y: auto; +} + +.material-override-add-item { + display: flex; + align-items: center; + justify-content: space-between; + width: 100%; + padding: 6px 10px; + border: none; + background: transparent; + color: var(--text-primary, #e0e0e0); + font-size: 11px; + cursor: pointer; + text-align: left; +} + +.material-override-add-item:hover { + background: var(--bg-hover, #3a3a3a); +} + +.material-override-type-hint { + font-size: 10px; + color: var(--text-tertiary, #666); + font-style: italic; +} + +/* Empty State */ +.material-override-empty { + padding: 12px; + text-align: center; + color: var(--text-secondary, #888); + font-size: 11px; + font-style: italic; +} + +.override-unsupported { + color: var(--text-tertiary, #666); + font-size: 10px; + font-style: italic; +} + +/* ============================================ + Inline Material Editor + ============================================ */ + +.inline-material-editor { + border: 1px solid var(--border-color, #333); + border-radius: 4px; + margin-top: 8px; + background: var(--bg-secondary, #252526); +} + +.inline-material-header { + display: flex; + align-items: center; + gap: 4px; + padding: 8px 10px; + cursor: pointer; + user-select: none; + border-bottom: 1px solid transparent; +} + +.inline-material-header:hover { + background: var(--bg-hover, #2a2a2a); +} + +.inline-material-expand { + color: var(--text-secondary, #888); +} + +.inline-material-title { + flex: 1; + font-weight: 500; + font-size: 12px; + color: var(--accent-color, #0078d4); +} + +.inline-material-dirty { + color: var(--warning-color, #fbbf24); + margin-left: 4px; +} + +.inline-material-actions { + display: flex; + gap: 4px; +} + +.inline-material-btn { + display: flex; + align-items: center; + justify-content: center; + width: 22px; + height: 22px; + border: none; + border-radius: 3px; + background: var(--button-bg, #333); + color: var(--text-secondary, #888); + cursor: pointer; + transition: all 0.15s ease; +} + +.inline-material-btn:hover:not(:disabled) { + background: var(--button-hover-bg, #444); + color: var(--text-primary, #e0e0e0); +} + +.inline-material-btn:disabled { + opacity: 0.4; + cursor: not-allowed; +} + +.inline-material-content { + padding: 10px; + border-top: 1px solid var(--border-color, #333); +} + +.inline-material-row { + display: flex; + align-items: center; + gap: 8px; + margin-bottom: 8px; +} + +.inline-material-row:last-child { + margin-bottom: 0; +} + +.inline-material-row label { + flex: 0 0 80px; + font-size: 11px; + color: var(--text-secondary, #888); +} + +.inline-material-row select { + flex: 1; + padding: 4px 8px; + border: 1px solid var(--border-color, #333); + border-radius: 3px; + background: var(--input-bg, #333); + color: var(--text-primary, #e0e0e0); + font-size: 11px; +} + +.inline-material-row select:focus { + outline: none; + border-color: var(--accent-color, #0078d4); +} + +/* Shader select with browse button */ +.inline-material-shader-select { + display: flex; + gap: 4px; + flex: 1; +} + +.inline-material-shader-select select { + flex: 1; +} + +.inline-material-refresh-btn { + display: flex; + align-items: center; + justify-content: center; + width: 26px; + height: 26px; + border: 1px solid var(--border-color, #333); + border-radius: 3px; + background: var(--button-bg, #333); + color: var(--text-secondary, #888); + cursor: pointer; + flex-shrink: 0; + transition: transform 0.2s ease; +} + +.inline-material-refresh-btn:hover:not(:disabled) { + background: var(--button-hover-bg, #444); + color: var(--text-primary, #e0e0e0); + border-color: var(--accent-color, #0078d4); +} + +.inline-material-refresh-btn:disabled { + opacity: 0.5; + cursor: not-allowed; +} + +.inline-material-refresh-btn.loading svg { + animation: spin 1s linear infinite; +} + +@keyframes spin { + from { transform: rotate(0deg); } + to { transform: rotate(360deg); } +} + +.inline-material-uniforms { + margin-top: 12px; + padding-top: 8px; + border-top: 1px solid var(--border-color, #333); +} + +.inline-material-uniforms-header { + font-size: 11px; + font-weight: 500; + color: var(--text-secondary, #888); + margin-bottom: 8px; +} + +.inline-material-uniform { + display: flex; + align-items: center; + gap: 8px; + margin-bottom: 6px; +} + +.inline-material-uniform:last-child { + margin-bottom: 0; +} + +.inline-material-uniform label { + flex: 0 0 80px; + font-size: 11px; + color: var(--text-secondary, #888); + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +/* ============================================ + Uniform Value Editor (shared) + ============================================ */ + +.uniform-input { + width: 100%; + padding: 4px 8px; + border: 1px solid var(--border-color, #333); + border-radius: 3px; + background: var(--input-bg, #333); + color: var(--text-primary, #e0e0e0); + font-size: 11px; +} + +.uniform-input:focus { + outline: none; + border-color: var(--accent-color, #0078d4); +} + +.uniform-input-number { + text-align: right; +} + +.uniform-vector { + display: flex; + gap: 4px; + flex: 1; +} + +.uniform-vector-4 { + flex-wrap: wrap; +} + +.uniform-vector-axis { + flex: 1; + display: flex; + align-items: center; + gap: 2px; + min-width: 45px; +} + +.uniform-axis-label { + width: 14px; + height: 14px; + display: flex; + align-items: center; + justify-content: center; + border-radius: 2px; + font-size: 9px; + font-weight: 600; + color: white; + flex-shrink: 0; +} + +.uniform-axis-x { background: #e06666; } +.uniform-axis-y { background: #93c47d; } +.uniform-axis-z { background: #6fa8dc; } +.uniform-axis-w { background: #b4a7d6; } + +.uniform-vector-axis .uniform-input { + flex: 1; + min-width: 0; + padding: 3px 4px; + text-align: right; +} + +.uniform-color { + display: flex; + align-items: center; + gap: 6px; + flex: 1; +} + +.uniform-color-preview { + width: 22px; + height: 22px; + border-radius: 3px; + border: 1px solid var(--border-color, #333); + flex-shrink: 0; +} + +.uniform-color-input { + width: 50px; + height: 22px; + padding: 0; + border: none; + background: transparent; + cursor: pointer; +} + +.uniform-color-input::-webkit-color-swatch-wrapper { + padding: 0; +} + +.uniform-color-input::-webkit-color-swatch { + border-radius: 3px; + border: 1px solid var(--border-color, #333); +} + +.uniform-alpha { + width: 45px; + text-align: right; +} + +.uniform-unsupported { + color: var(--text-tertiary, #666); + font-size: 10px; + font-style: italic; +} diff --git a/packages/sprite-editor/src/SpriteComponentInspector.tsx b/packages/sprite-editor/src/SpriteComponentInspector.tsx new file mode 100644 index 00000000..ceceb64b --- /dev/null +++ b/packages/sprite-editor/src/SpriteComponentInspector.tsx @@ -0,0 +1,983 @@ +/** + * Sprite Component Inspector. + * 精灵组件检查器。 + * + * Provides custom inspector UI for SpriteComponent with material override support. + * 为 SpriteComponent 提供带材质覆盖支持的自定义检查器 UI。 + */ + +import React, { useState, useEffect, useMemo, useCallback } from 'react'; +import { Component, Core, getComponentInstanceTypeName } from '@esengine/ecs-framework'; +import { IComponentInspector, ComponentInspectorContext, MessageHub, IFileSystemService, IFileSystem, ProjectService } from '@esengine/editor-core'; +import { SpriteComponent, MaterialOverrides, MaterialPropertyOverride } from '@esengine/sprite'; +import { getMaterialManager, Material, BlendMode, BuiltInShaders, UniformType } from '@esengine/material-system'; +import { ChevronDown, ChevronRight, X, Plus, Save, ExternalLink, RefreshCw } from 'lucide-react'; +import './SpriteComponentInspector.css'; + +/** + * Blend mode options. + * 混合模式选项。 + */ +const BLEND_MODE_OPTIONS = [ + { value: BlendMode.None, label: 'None (Opaque)' }, + { value: BlendMode.Alpha, label: 'Alpha Blend' }, + { value: BlendMode.Additive, label: 'Additive' }, + { value: BlendMode.Multiply, label: 'Multiply' }, + { value: BlendMode.Screen, label: 'Screen' }, + { value: BlendMode.PremultipliedAlpha, label: 'Premultiplied Alpha' }, +]; + +/** + * Built-in shader options. + * 内置着色器选项。 + */ +const BUILT_IN_SHADER_OPTIONS = [ + { value: BuiltInShaders.DefaultSprite, label: 'Default Sprite' }, + { value: BuiltInShaders.Grayscale, label: 'Grayscale' }, + { value: BuiltInShaders.Tint, label: 'Tint' }, + { value: BuiltInShaders.Flash, label: 'Flash' }, + { value: BuiltInShaders.Outline, label: 'Outline' }, +]; + +/** + * Shader option with path info. + * 带路径信息的着色器选项。 + */ +interface ShaderOption { + value: number; + label: string; + path?: string; +} + +/** + * Get all available shaders (built-in + custom loaded). + * 获取所有可用着色器(内置 + 自定义加载的)。 + */ +function getAvailableShaders(): ShaderOption[] { + const materialManager = getMaterialManager(); + if (!materialManager) { + return BUILT_IN_SHADER_OPTIONS; + } + + const shaderIds = materialManager.getShaderIds(); + const options: ShaderOption[] = []; + + for (const id of shaderIds) { + const shader = materialManager.getShader(id); + if (shader) { + // Check if it's a built-in shader. + // 检查是否是内置着色器。 + const builtIn = BUILT_IN_SHADER_OPTIONS.find(opt => opt.value === id); + options.push({ + value: id, + label: builtIn ? builtIn.label : shader.name + }); + } + } + + return options; +} + +/** + * Scan and load all shader files from project. + * 扫描并加载项目中所有的着色器文件。 + */ +async function scanAndLoadProjectShaders(): Promise { + const fileSystem = Core.services.tryResolve(IFileSystemService); + const projectService = Core.services.tryResolve(ProjectService); + const materialManager = getMaterialManager(); + + if (!fileSystem || !projectService || !materialManager) { + return getAvailableShaders(); + } + + const currentProject = projectService.getCurrentProject(); + if (!currentProject) { + return getAvailableShaders(); + } + + try { + // Scan for .shader files in project. + // 扫描项目中的 .shader 文件。 + const shaderFiles = await fileSystem.scanFiles(currentProject.path, '**/*.shader'); + + // Load each shader. + // 加载每个着色器。 + for (const shaderPath of shaderFiles) { + // Skip if already loaded. + // 如果已加载则跳过。 + if (materialManager.hasShaderByPath(shaderPath)) { + continue; + } + + try { + await materialManager.loadShaderFromPath(shaderPath); + } catch (error) { + console.warn('[SpriteComponentInspector] Failed to load shader:', shaderPath, error); + } + } + } catch (error) { + console.warn('[SpriteComponentInspector] Failed to scan shader files:', error); + } + + return getAvailableShaders(); +} + +/** + * Uniform type display names. + * Uniform 类型显示名称。 + */ +const UNIFORM_TYPE_LABELS: Record = { + 'float': 'Float', + 'vec2': 'Vec2', + 'vec3': 'Vec3', + 'vec4': 'Vec4', + 'color': 'Color', + 'int': 'Int', + 'mat3': 'Mat3', + 'mat4': 'Mat4', + 'sampler': 'Sampler', +}; + +/** + * Inline material editor props. + * 内联材质编辑器属性。 + */ +interface InlineMaterialEditorProps { + material: Material; + materialPath: string; + onMaterialChange: () => void; +} + +/** + * Inline material editor component. + * 内联材质编辑器组件。 + * + * Allows editing material properties directly in the sprite inspector. + * 允许直接在精灵检查器中编辑材质属性。 + */ +function InlineMaterialEditor({ material, materialPath, onMaterialChange }: InlineMaterialEditorProps) { + const [isExpanded, setIsExpanded] = useState(true); + const [isDirty, setIsDirty] = useState(false); + const [isLoadingShaders, setIsLoadingShaders] = useState(false); + const [shaderOptions, setShaderOptions] = useState(() => getAvailableShaders()); + const [localMaterial, setLocalMaterial] = useState(() => ({ + name: material.name, + shader: material.shaderId, + blendMode: material.blendMode, + uniforms: Object.fromEntries(material.getUniforms()) + })); + + // Scan and load project shaders on mount. + // 挂载时扫描并加载项目着色器。 + useEffect(() => { + let mounted = true; + setIsLoadingShaders(true); + + scanAndLoadProjectShaders().then(options => { + if (mounted) { + setShaderOptions(options); + setIsLoadingShaders(false); + } + }); + + return () => { mounted = false; }; + }, []); + + // Sync with material changes. + // 同步材质变化。 + useEffect(() => { + setLocalMaterial({ + name: material.name, + shader: material.shaderId, + blendMode: material.blendMode, + uniforms: Object.fromEntries(material.getUniforms()) + }); + setIsDirty(false); + }, [material]); + + const handleShaderChange = (shaderId: number) => { + material.shaderId = shaderId; + setLocalMaterial(prev => ({ ...prev, shader: shaderId })); + setIsDirty(true); + onMaterialChange(); + }; + + const handleRefreshShaders = async () => { + // Re-scan project shaders. + // 重新扫描项目着色器。 + setIsLoadingShaders(true); + const options = await scanAndLoadProjectShaders(); + setShaderOptions(options); + setIsLoadingShaders(false); + }; + + const handleBlendModeChange = (blendMode: BlendMode) => { + material.blendMode = blendMode; + setLocalMaterial(prev => ({ ...prev, blendMode })); + setIsDirty(true); + onMaterialChange(); + }; + + const handleUniformChange = (name: string, value: number | number[]) => { + // Get the uniform type from current material. + // 从当前材质获取 uniform 类型。 + const currentUniform = material.getUniform(name); + if (!currentUniform) return; + + // Set uniform based on type. + // 根据类型设置 uniform。 + switch (currentUniform.type) { + case UniformType.Float: + if (typeof value === 'number') { + material.setFloat(name, value); + } + break; + case UniformType.Int: + if (typeof value === 'number') { + material.setInt(name, value); + } + break; + case UniformType.Vec2: + if (Array.isArray(value) && value.length >= 2) { + material.setVec2(name, value[0], value[1]); + } + break; + case UniformType.Vec3: + if (Array.isArray(value) && value.length >= 3) { + material.setVec3(name, value[0], value[1], value[2]); + } + break; + case UniformType.Vec4: + if (Array.isArray(value) && value.length >= 4) { + material.setVec4(name, value[0], value[1], value[2], value[3]); + } + break; + case UniformType.Color: + if (Array.isArray(value) && value.length >= 4) { + material.setColor(name, value[0], value[1], value[2], value[3]); + } + break; + } + + setLocalMaterial(prev => ({ + ...prev, + uniforms: { ...prev.uniforms, [name]: { ...prev.uniforms[name], value } } + })); + setIsDirty(true); + onMaterialChange(); + }; + + const handleSave = async () => { + if (!materialPath) return; + + try { + const fileSystem = Core.services.tryResolve(IFileSystemService); + if (!fileSystem) { + console.error('[InlineMaterialEditor] FileSystem service not available'); + return; + } + + // Build material data. + // 构建材质数据。 + const materialData = { + name: material.name, + shader: material.shaderId, + blendMode: material.blendMode, + uniforms: Object.fromEntries( + Array.from(material.getUniforms().entries()).map(([k, v]) => [k, { type: v.type, value: v.value }]) + ) + }; + + await fileSystem.writeFile(materialPath, JSON.stringify(materialData, null, 2)); + setIsDirty(false); + + // Notify + const messageHub = Core.services.tryResolve(MessageHub); + if (messageHub) { + messageHub.publish('material:saved', { filePath: materialPath }); + } + } catch (error) { + console.error('[InlineMaterialEditor] Failed to save material:', error); + } + }; + + const handleOpenInEditor = () => { + const messageHub = Core.services.tryResolve(MessageHub); + if (messageHub && materialPath) { + messageHub.publish('asset:open', { filePath: materialPath, type: 'material' }); + } + }; + + const uniforms = Array.from(material.getUniforms().entries()); + + return ( +
+
setIsExpanded(!isExpanded)} + > + + {isExpanded ? : } + + + Material: {material.name} + {isDirty && *} + +
e.stopPropagation()}> + + +
+
+ + {isExpanded && ( +
+ {/* Shader */} +
+ +
+ + +
+
+ + {/* Blend Mode */} +
+ + +
+ + {/* Uniforms */} + {uniforms.length > 0 && ( +
+
Uniforms
+ {uniforms.map(([name, uniform]) => ( +
+ + handleUniformChange(name, v)} + /> +
+ ))} +
+ )} +
+ )} +
+ ); +} + +/** + * Uniform value editor component (reused for both material and overrides). + * Uniform 值编辑器组件(用于材质和覆盖)。 + */ +function UniformValueEditor({ type, value, onChange }: { + type: MaterialPropertyOverride['type']; + value: number | number[]; + onChange: (value: number | number[]) => void; +}) { + switch (type) { + case 'float': + case 'int': + return ( + { + const v = type === 'int' + ? Math.floor(parseFloat(e.target.value) || 0) + : parseFloat(e.target.value) || 0; + onChange(v); + }} + /> + ); + + case 'vec2': + return ( +
+ {['X', 'Y'].map((axis, i) => ( +
+ {axis} + { + const arr = Array.isArray(value) ? [...value] : [0, 0]; + arr[i] = parseFloat(e.target.value) || 0; + onChange(arr); + }} + /> +
+ ))} +
+ ); + + case 'vec3': + return ( +
+ {['X', 'Y', 'Z'].map((axis, i) => ( +
+ {axis} + { + const arr = Array.isArray(value) ? [...value] : [0, 0, 0]; + arr[i] = parseFloat(e.target.value) || 0; + onChange(arr); + }} + /> +
+ ))} +
+ ); + + case 'vec4': + return ( +
+ {['X', 'Y', 'Z', 'W'].map((axis, i) => ( +
+ {axis} + { + const arr = Array.isArray(value) ? [...value] : [0, 0, 0, 0]; + arr[i] = parseFloat(e.target.value) || 0; + onChange(arr); + }} + /> +
+ ))} +
+ ); + + case 'color': { + const colorArray = Array.isArray(value) ? value : [1, 1, 1, 1]; + const r = Math.round((colorArray[0] ?? 1) * 255); + const g = Math.round((colorArray[1] ?? 1) * 255); + const b = Math.round((colorArray[2] ?? 1) * 255); + const hexColor = `#${r.toString(16).padStart(2, '0')}${g.toString(16).padStart(2, '0')}${b.toString(16).padStart(2, '0')}`; + + return ( +
+
+ { + const hex = e.target.value; + const newR = parseInt(hex.slice(1, 3), 16) / 255; + const newG = parseInt(hex.slice(3, 5), 16) / 255; + const newB = parseInt(hex.slice(5, 7), 16) / 255; + onChange([newR, newG, newB, colorArray[3] ?? 1]); + }} + /> + { + const alpha = Math.max(0, Math.min(1, parseFloat(e.target.value) || 0)); + onChange([colorArray[0] ?? 1, colorArray[1] ?? 1, colorArray[2] ?? 1, alpha]); + }} + /> +
+ ); + } + + default: + return Unsupported type; + } +} + +/** + * Material override editor props. + * 材质覆盖编辑器属性。 + */ +interface MaterialOverrideEditorProps { + sprite: SpriteComponent; + material: Material | null; + onChange: (propertyName: string, value: unknown) => void; +} + +/** + * Material override editor component. + * 材质覆盖编辑器组件。 + */ +function MaterialOverrideEditor({ sprite, material, onChange }: MaterialOverrideEditorProps) { + const [isExpanded, setIsExpanded] = useState(true); + const [showAddMenu, setShowAddMenu] = useState(false); + + // Get available uniforms from material. + // 从材质获取可用的 uniforms。 + const availableUniforms = useMemo(() => { + if (!material) return []; + const uniforms = material.getUniforms(); + return Array.from(uniforms.entries()).map(([name, value]) => ({ + name, + type: value.type, + defaultValue: value.value + })); + }, [material]); + + // Get current overrides. + // 获取当前覆盖。 + const currentOverrides = sprite.materialOverrides || {}; + const overrideKeys = Object.keys(currentOverrides); + + // Get uniforms not yet overridden. + // 获取尚未覆盖的 uniforms。 + const unoverriddenUniforms = availableUniforms.filter( + u => !overrideKeys.includes(u.name) + ); + + const handleAddOverride = (uniformName: string) => { + const uniform = availableUniforms.find(u => u.name === uniformName); + if (!uniform) return; + + // Convert defaultValue to appropriate type + let value: number | number[]; + if (typeof uniform.defaultValue === 'number') { + value = uniform.defaultValue; + } else if (Array.isArray(uniform.defaultValue)) { + value = uniform.defaultValue as number[]; + } else { + value = 0; + } + + const newOverride: MaterialPropertyOverride = { + type: uniform.type as MaterialPropertyOverride['type'], + value + }; + + const newOverrides = { ...currentOverrides, [uniformName]: newOverride }; + onChange('materialOverrides', newOverrides); + setShowAddMenu(false); + }; + + const handleRemoveOverride = (uniformName: string) => { + const newOverrides = { ...currentOverrides }; + delete newOverrides[uniformName]; + onChange('materialOverrides', newOverrides); + }; + + const handleOverrideChange = (uniformName: string, value: number | number[]) => { + const current = currentOverrides[uniformName]; + if (!current) return; + + const newOverrides = { + ...currentOverrides, + [uniformName]: { ...current, value } + }; + onChange('materialOverrides', newOverrides); + }; + + if (!sprite.material) { + return null; + } + + return ( +
+
setIsExpanded(!isExpanded)} + > + + {isExpanded ? : } + + Material Overrides + {overrideKeys.length > 0 && ( + {overrideKeys.length} + )} +
+ + {isExpanded && ( +
+ {/* Existing overrides */} + {overrideKeys.map(key => { + const override = currentOverrides[key]; + if (!override) return null; + return ( +
+
+ {key} + + {UNIFORM_TYPE_LABELS[override.type] || override.type} + + +
+ handleOverrideChange(key, v)} + /> +
+ ); + })} + + {/* Add override button */} + {unoverriddenUniforms.length > 0 && ( +
+ + {showAddMenu && ( +
+ {unoverriddenUniforms.map(u => ( + + ))} +
+ )} +
+ )} + + {/* Empty state */} + {overrideKeys.length === 0 && unoverriddenUniforms.length === 0 && ( +
+ {material ? 'No parameters available' : 'Select a material first'} +
+ )} +
+ )} +
+ ); +} + +/** + * Override value editor props. + * 覆盖值编辑器属性。 + */ +interface OverrideValueEditorProps { + type: MaterialPropertyOverride['type']; + value: number | number[]; + onChange: (value: number | number[]) => void; +} + +/** + * Override value editor component. + * 覆盖值编辑器组件。 + */ +function OverrideValueEditor({ type, value, onChange }: OverrideValueEditorProps) { + switch (type) { + case 'float': + case 'int': + return ( + { + const v = type === 'int' + ? Math.floor(parseFloat(e.target.value) || 0) + : parseFloat(e.target.value) || 0; + onChange(v); + }} + /> + ); + + case 'vec2': + return ( +
+ {['X', 'Y'].map((axis, i) => ( +
+ {axis} + { + const arr = Array.isArray(value) ? [...value] : [0, 0]; + arr[i] = parseFloat(e.target.value) || 0; + onChange(arr); + }} + /> +
+ ))} +
+ ); + + case 'vec3': + return ( +
+ {['X', 'Y', 'Z'].map((axis, i) => ( +
+ {axis} + { + const arr = Array.isArray(value) ? [...value] : [0, 0, 0]; + arr[i] = parseFloat(e.target.value) || 0; + onChange(arr); + }} + /> +
+ ))} +
+ ); + + case 'vec4': + return ( +
+ {['X', 'Y', 'Z', 'W'].map((axis, i) => ( +
+ {axis} + { + const arr = Array.isArray(value) ? [...value] : [0, 0, 0, 0]; + arr[i] = parseFloat(e.target.value) || 0; + onChange(arr); + }} + /> +
+ ))} +
+ ); + + case 'color': { + const colorArray = Array.isArray(value) ? value : [1, 1, 1, 1]; + const r = Math.round((colorArray[0] ?? 1) * 255); + const g = Math.round((colorArray[1] ?? 1) * 255); + const b = Math.round((colorArray[2] ?? 1) * 255); + const hexColor = `#${r.toString(16).padStart(2, '0')}${g.toString(16).padStart(2, '0')}${b.toString(16).padStart(2, '0')}`; + + return ( +
+
+ { + const hex = e.target.value; + const newR = parseInt(hex.slice(1, 3), 16) / 255; + const newG = parseInt(hex.slice(3, 5), 16) / 255; + const newB = parseInt(hex.slice(5, 7), 16) / 255; + onChange([newR, newG, newB, colorArray[3] ?? 1]); + }} + /> + { + const alpha = Math.max(0, Math.min(1, parseFloat(e.target.value) || 0)); + onChange([colorArray[0] ?? 1, colorArray[1] ?? 1, colorArray[2] ?? 1, alpha]); + }} + /> +
+ ); + } + + default: + return Unsupported type; + } +} + +/** + * Sprite inspector content component. + * 精灵检查器内容组件。 + */ +function SpriteInspectorContent({ context }: { context: ComponentInspectorContext }) { + const sprite = context.component as SpriteComponent; + const [material, setMaterial] = useState(null); + const [, forceUpdate] = useState({}); + + // Load material when sprite.material changes. + // 当 sprite.material 变化时加载材质。 + useEffect(() => { + if (!sprite.material) { + setMaterial(null); + return; + } + + const materialManager = getMaterialManager(); + if (!materialManager) { + setMaterial(null); + return; + } + + // Try to get cached material by ID. + // 尝试通过 ID 获取缓存的材质。 + const materialId = materialManager.getMaterialIdByPath(sprite.material); + if (materialId > 0) { + const mat = materialManager.getMaterial(materialId); + setMaterial(mat || null); + return; + } + + // Load material asynchronously. + // 异步加载材质。 + materialManager.loadMaterialFromPath(sprite.material) + .then(matId => { + const mat = materialManager.getMaterial(matId); + setMaterial(mat || null); + }) + .catch(() => { + setMaterial(null); + }); + }, [sprite.material]); + + const handleChange = useCallback((propertyName: string, value: unknown) => { + (sprite as unknown as Record)[propertyName] = value; + context.onChange?.(propertyName, value); + forceUpdate({}); + + // Publish scene:modified. + // 发布 scene:modified。 + const messageHub = Core.services.tryResolve(MessageHub); + if (messageHub) { + messageHub.publish('scene:modified', {}); + } + }, [sprite, context]); + + const handleMaterialChange = useCallback(() => { + forceUpdate({}); + // Publish scene:modified for material changes. + // 发布 scene:modified 用于材质变更。 + const messageHub = Core.services.tryResolve(MessageHub); + if (messageHub) { + messageHub.publish('scene:modified', {}); + } + }, []); + + // No material selected + if (!sprite.material) { + return null; + } + + return ( +
+ {/* Inline material editor */} + {material && ( + + )} + + {/* Material override section */} + +
+ ); +} + +/** + * Sprite component inspector implementation. + * 精灵组件检查器实现。 + * + * Uses 'append' mode to show material overrides after the default PropertyInspector. + * 使用 'append' 模式在默认 PropertyInspector 后显示材质覆盖。 + */ +export class SpriteComponentInspector implements IComponentInspector { + readonly id = 'sprite-component-inspector'; + readonly name = 'Sprite Component Inspector'; + readonly priority = 100; + readonly targetComponents = ['Sprite', 'SpriteComponent']; + readonly renderMode = 'append' as const; + + canHandle(component: Component): component is SpriteComponent { + const typeName = getComponentInstanceTypeName(component); + return typeName === 'Sprite' || typeName === 'SpriteComponent'; + } + + render(context: ComponentInspectorContext): React.ReactElement { + return React.createElement(SpriteInspectorContent, { + context, + key: `sprite-${context.version}` + }); + } +} diff --git a/packages/sprite-editor/src/index.ts b/packages/sprite-editor/src/index.ts index 7ca9e4fc..dcd9fd1f 100644 --- a/packages/sprite-editor/src/index.ts +++ b/packages/sprite-editor/src/index.ts @@ -9,27 +9,43 @@ import type { Entity, ServiceContainer } from '@esengine/ecs-framework'; import { Core } from '@esengine/ecs-framework'; import type { IEditorModuleLoader, - EntityCreationTemplate + EntityCreationTemplate, + IPlugin, + ModuleManifest } from '@esengine/editor-core'; import { EntityStoreService, MessageHub, - ComponentRegistry + ComponentRegistry, + ComponentInspectorRegistry } from '@esengine/editor-core'; import { TransformComponent } from '@esengine/engine-core'; // Runtime imports from @esengine/sprite import { SpriteComponent, - SpriteAnimatorComponent + SpriteAnimatorComponent, + SpriteRuntimeModule } from '@esengine/sprite'; +// Inspector +import { SpriteComponentInspector } from './SpriteComponentInspector'; + +// Export inspector +export { SpriteComponentInspector } from './SpriteComponentInspector'; + /** * 精灵编辑器模块 * Sprite Editor Module */ export class SpriteEditorModule implements IEditorModuleLoader { async install(services: ServiceContainer): Promise { + // 注册组件检查器 | Register component inspectors + const componentInspectorRegistry = services.tryResolve(ComponentInspectorRegistry); + if (componentInspectorRegistry) { + componentInspectorRegistry.register(new SpriteComponentInspector()); + } + // 注册 Sprite 组件到编辑器组件注册表 | Register Sprite components to editor component registry const componentRegistry = services.resolve(ComponentRegistry); if (componentRegistry) { @@ -144,4 +160,35 @@ export class SpriteEditorModule implements IEditorModuleLoader { export const spriteEditorModule = new SpriteEditorModule(); +/** + * Sprite 插件清单 + * Sprite Plugin Manifest + */ +const manifest: ModuleManifest = { + id: '@esengine/sprite', + name: '@esengine/sprite', + displayName: 'Sprite', + version: '1.0.0', + description: 'Sprite and animation components for 2D rendering', + category: 'Rendering', + isCore: false, + defaultEnabled: true, + isEngineModule: true, + dependencies: ['engine-core'], + exports: { + components: ['SpriteComponent', 'SpriteAnimatorComponent'], + systems: ['SpriteRenderSystem'] + } +}; + +/** + * 完整的 Sprite 插件(运行时 + 编辑器) + * Complete Sprite Plugin (runtime + editor) + */ +export const SpritePlugin: IPlugin = { + manifest, + runtimeModule: new SpriteRuntimeModule(), + editorModule: spriteEditorModule +}; + export default spriteEditorModule; diff --git a/packages/sprite/module.json b/packages/sprite/module.json new file mode 100644 index 00000000..eac4d347 --- /dev/null +++ b/packages/sprite/module.json @@ -0,0 +1,38 @@ +{ + "id": "sprite", + "name": "@esengine/sprite", + "displayName": "Sprite 2D", + "description": "2D sprite rendering | 2D 精灵渲染", + "version": "1.0.0", + "category": "Rendering", + "icon": "Image", + "tags": [ + "2d", + "sprite", + "rendering" + ], + "isCore": false, + "defaultEnabled": true, + "isEngineModule": true, + "canContainContent": true, + "platforms": [ + "web", + "desktop" + ], + "dependencies": [ + "core", + "math" + ], + "exports": { + "components": [ + "SpriteComponent" + ], + "systems": [ + "SpriteRenderSystem" + ] + }, + "editorPackage": "@esengine/sprite-editor", + "requiresWasm": true, + "outputPath": "dist/index.js", + "pluginExport": "SpritePlugin" +} diff --git a/packages/sprite/src/SpriteComponent.ts b/packages/sprite/src/SpriteComponent.ts index e03b4e7e..59c4d5ea 100644 --- a/packages/sprite/src/SpriteComponent.ts +++ b/packages/sprite/src/SpriteComponent.ts @@ -1,12 +1,33 @@ -import { Component, ECSComponent, Serializable, Serialize, Property } from '@esengine/ecs-framework'; import type { AssetReference } from '@esengine/asset-system'; +import { Component, ECSComponent, Property, Serializable, Serialize } from '@esengine/ecs-framework'; + +/** + * Material property override value. + * 材质属性覆盖值。 + * + * Used to override specific uniform parameters on a per-instance basis + * without creating a new material instance. + * 用于在每个实例上覆盖特定的 uniform 参数,而无需创建新的材质实例。 + */ +export interface MaterialPropertyOverride { + /** Uniform type. | Uniform 类型。 */ + type: 'float' | 'vec2' | 'vec3' | 'vec4' | 'color' | 'int'; + /** Uniform value. | Uniform 值。 */ + value: number | number[]; +} + +/** + * Material property overrides map. + * 材质属性覆盖映射。 + */ +export type MaterialOverrides = Record; /** * 精灵组件 - 管理2D图像渲染 * Sprite component - manages 2D image rendering */ @ECSComponent('Sprite') -@Serializable({ version: 2, typeId: 'Sprite' }) +@Serializable({ version: 3, typeId: 'Sprite' }) export class SpriteComponent extends Component { /** 纹理路径或资源ID | Texture path or asset ID */ @Serialize() @@ -129,6 +150,55 @@ export class SpriteComponent extends Component { @Property({ type: 'integer', label: 'Sorting Order' }) public sortingOrder: number = 0; + /** + * 材质资产路径(共享材质) + * Material asset path (shared material) + * + * Multiple sprites can reference the same material file. + * 多个精灵可以引用同一个材质文件。 + */ + @Serialize() + @Property({ type: 'asset', label: 'Material', extensions: ['.mat'] }) + public material: string = ''; + + /** + * 材质属性覆盖(实例级别) + * Material property overrides (instance level) + * + * Override specific uniform parameters without creating a new material. + * 覆盖特定的 uniform 参数,无需创建新材质。 + */ + @Serialize() + public materialOverrides: MaterialOverrides = {}; + + /** + * 是否使用独立材质实例 + * Whether to use an independent material instance + * + * When true, a copy of the shared material is created for this sprite. + * Changes to this material won't affect other sprites using the same source. + * 当为 true 时,会为此精灵创建共享材质的副本。 + * 对此材质的更改不会影响使用相同源的其他精灵。 + */ + @Serialize() + @Property({ type: 'boolean', label: 'Use Instance Material' }) + public useInstanceMaterial: boolean = false; + + /** + * 运行时材质ID(缓存) + * Runtime material ID (cached) + * + * Cached material ID for rendering. Updated when material path changes. + * 用于渲染的缓存材质ID。当材质路径更改时更新。 + */ + private _materialId: number = 0; + + /** + * 独立材质实例(如果 useInstanceMaterial 为 true) + * Independent material instance (if useInstanceMaterial is true) + */ + private _instanceMaterial: unknown = null; + /** 锚点X (0-1) - 别名为originX | Anchor X (0-1) - alias for originX */ get anchorX(): number { return this.originX; @@ -229,6 +299,167 @@ export class SpriteComponent extends Component { return this.assetGuid || this.texture; } + // ============= Material Override Methods ============= + // ============= 材质覆盖方法 ============= + + /** + * 获取材质ID + * Get material ID + * + * # Returns | 返回 + * The cached material ID for rendering. + * 用于渲染的缓存材质ID。 + */ + getMaterialId(): number { + return this._materialId; + } + + /** + * 设置材质ID + * Set material ID + * + * # Arguments | 参数 + * * `id` - Material ID from MaterialManager. | 来自 MaterialManager 的材质ID。 + */ + setMaterialId(id: number): void { + this._materialId = id; + } + + /** + * 设置浮点覆盖值 + * Set float override value + * + * # Arguments | 参数 + * * `name` - Uniform name. | Uniform 名称。 + * * `value` - Float value. | 浮点值。 + */ + setOverrideFloat(name: string, value: number): this { + this.materialOverrides[name] = { type: 'float', value }; + return this; + } + + /** + * 设置 vec2 覆盖值 + * Set vec2 override value + * + * # Arguments | 参数 + * * `name` - Uniform name. | Uniform 名称。 + * * `x` - X component. | X 分量。 + * * `y` - Y component. | Y 分量。 + */ + setOverrideVec2(name: string, x: number, y: number): this { + this.materialOverrides[name] = { type: 'vec2', value: [x, y] }; + return this; + } + + /** + * 设置 vec3 覆盖值 + * Set vec3 override value + * + * # Arguments | 参数 + * * `name` - Uniform name. | Uniform 名称。 + * * `x` - X component. | X 分量。 + * * `y` - Y component. | Y 分量。 + * * `z` - Z component. | Z 分量。 + */ + setOverrideVec3(name: string, x: number, y: number, z: number): this { + this.materialOverrides[name] = { type: 'vec3', value: [x, y, z] }; + return this; + } + + /** + * 设置 vec4 覆盖值 + * Set vec4 override value + * + * # Arguments | 参数 + * * `name` - Uniform name. | Uniform 名称。 + * * `x` - X component. | X 分量。 + * * `y` - Y component. | Y 分量。 + * * `z` - Z component. | Z 分量。 + * * `w` - W component. | W 分量。 + */ + setOverrideVec4(name: string, x: number, y: number, z: number, w: number): this { + this.materialOverrides[name] = { type: 'vec4', value: [x, y, z, w] }; + return this; + } + + /** + * 设置颜色覆盖值 + * Set color override value + * + * # Arguments | 参数 + * * `name` - Uniform name. | Uniform 名称。 + * * `r` - Red component (0-1). | 红色分量 (0-1)。 + * * `g` - Green component (0-1). | 绿色分量 (0-1)。 + * * `b` - Blue component (0-1). | 蓝色分量 (0-1)。 + * * `a` - Alpha component (0-1). | 透明度分量 (0-1)。 + */ + setOverrideColor(name: string, r: number, g: number, b: number, a: number = 1.0): this { + this.materialOverrides[name] = { type: 'color', value: [r, g, b, a] }; + return this; + } + + /** + * 设置整数覆盖值 + * Set integer override value + * + * # Arguments | 参数 + * * `name` - Uniform name. | Uniform 名称。 + * * `value` - Integer value. | 整数值。 + */ + setOverrideInt(name: string, value: number): this { + this.materialOverrides[name] = { type: 'int', value: Math.floor(value) }; + return this; + } + + /** + * 获取覆盖值 + * Get override value + * + * # Arguments | 参数 + * * `name` - Uniform name. | Uniform 名称。 + * + * # Returns | 返回 + * Override value or undefined if not set. + * 覆盖值,如果未设置则返回 undefined。 + */ + getOverride(name: string): MaterialPropertyOverride | undefined { + return this.materialOverrides[name]; + } + + /** + * 移除覆盖值 + * Remove override value + * + * # Arguments | 参数 + * * `name` - Uniform name to remove. | 要移除的 Uniform 名称。 + */ + removeOverride(name: string): this { + delete this.materialOverrides[name]; + return this; + } + + /** + * 清除所有覆盖值 + * Clear all override values + */ + clearOverrides(): this { + this.materialOverrides = {}; + return this; + } + + /** + * 检查是否有覆盖值 + * Check if there are any overrides + * + * # Returns | 返回 + * True if there are any material overrides. + * 如果有任何材质覆盖则返回 true。 + */ + hasOverrides(): boolean { + return Object.keys(this.materialOverrides).length > 0; + } + /** * 组件销毁时调用 * Called when component is destroyed @@ -239,5 +470,8 @@ export class SpriteComponent extends Component { this._assetReference.release(); this._assetReference = undefined; } + // 清理材质覆盖 / Clear material overrides + this.materialOverrides = {}; + this._instanceMaterial = null; } } diff --git a/packages/sprite/src/SpriteRuntimeModule.ts b/packages/sprite/src/SpriteRuntimeModule.ts index 5b93ccab..230a9ad7 100644 --- a/packages/sprite/src/SpriteRuntimeModule.ts +++ b/packages/sprite/src/SpriteRuntimeModule.ts @@ -1,10 +1,10 @@ import type { ComponentRegistry as ComponentRegistryType, IScene } from '@esengine/ecs-framework'; -import type { IRuntimeModule, IPlugin, PluginDescriptor, SystemContext } from '@esengine/engine-core'; +import type { IRuntimeModule, IPlugin, ModuleManifest, SystemContext } from '@esengine/engine-core'; import { SpriteComponent } from './SpriteComponent'; import { SpriteAnimatorComponent } from './SpriteAnimatorComponent'; import { SpriteAnimatorSystem } from './systems/SpriteAnimatorSystem'; -export type { SystemContext, PluginDescriptor, IRuntimeModule as IRuntimeModuleLoader, IPlugin as IPluginLoader }; +export type { SystemContext, ModuleManifest, IRuntimeModule as IRuntimeModuleLoader, IPlugin as IPluginLoader }; class SpriteRuntimeModule implements IRuntimeModule { registerComponents(registry: typeof ComponentRegistryType): void { @@ -24,18 +24,26 @@ class SpriteRuntimeModule implements IRuntimeModule { } } -const descriptor: PluginDescriptor = { - id: '@esengine/sprite', - name: 'Sprite Components', +const manifest: ModuleManifest = { + id: 'sprite', + name: '@esengine/sprite', + displayName: 'Sprite 2D', version: '1.0.0', description: 'Sprite and SpriteAnimator components for 2D rendering', - category: 'rendering', - enabledByDefault: true, - isEnginePlugin: true + category: 'Rendering', + icon: 'Image', + isCore: false, + defaultEnabled: true, + isEngineModule: true, + canContainContent: true, + dependencies: ['core', 'math'], + exports: { components: ['SpriteComponent', 'SpriteAnimatorComponent'] }, + editorPackage: '@esengine/sprite-editor', + requiresWasm: true }; export const SpritePlugin: IPlugin = { - descriptor, + manifest, runtimeModule: new SpriteRuntimeModule() }; diff --git a/packages/sprite/src/index.ts b/packages/sprite/src/index.ts index 074306aa..0c232719 100644 --- a/packages/sprite/src/index.ts +++ b/packages/sprite/src/index.ts @@ -1,4 +1,5 @@ export { SpriteComponent } from './SpriteComponent'; +export type { MaterialPropertyOverride, MaterialOverrides } from './SpriteComponent'; export { SpriteAnimatorComponent } from './SpriteAnimatorComponent'; export type { AnimationFrame, AnimationClip } from './SpriteAnimatorComponent'; export { SpriteAnimatorSystem } from './systems/SpriteAnimatorSystem'; diff --git a/packages/ui-editor/src/index.ts b/packages/ui-editor/src/index.ts index d1869b85..9ebd2dc3 100644 --- a/packages/ui-editor/src/index.ts +++ b/packages/ui-editor/src/index.ts @@ -404,15 +404,23 @@ export const uiEditorModule = new UIEditorModule(); // 从 @esengine/ui 导入运行时模块 import { UIRuntimeModule } from '@esengine/ui'; -import type { IPlugin, PluginDescriptor } from '@esengine/editor-core'; +import type { IPlugin, ModuleManifest } from '@esengine/editor-core'; -const descriptor: PluginDescriptor = { +const manifest: ModuleManifest = { id: '@esengine/ui', - name: 'UI', + name: '@esengine/ui', + displayName: 'UI', version: '1.0.0', description: 'ECS-based UI system with editor support', - category: 'ui', - enabledByDefault: true + category: 'Rendering', + isCore: false, + defaultEnabled: true, + isEngineModule: true, + dependencies: ['engine-core'], + exports: { + components: ['UITransformComponent', 'UIRenderComponent', 'UITextComponent', 'UIButtonComponent'], + systems: ['UIRenderSystem', 'UILayoutSystem', 'UIInteractionSystem'] + } }; /** @@ -420,7 +428,7 @@ const descriptor: PluginDescriptor = { * Complete UI Plugin (runtime + editor) */ export const UIPlugin: IPlugin = { - descriptor, + manifest, runtimeModule: new UIRuntimeModule(), editorModule: uiEditorModule }; diff --git a/packages/ui/module.json b/packages/ui/module.json new file mode 100644 index 00000000..f3b2d60c --- /dev/null +++ b/packages/ui/module.json @@ -0,0 +1,43 @@ +{ + "id": "ui", + "name": "@esengine/ui", + "displayName": "UI System", + "description": "User interface components and layout | 用户界面组件和布局", + "version": "1.0.0", + "category": "Rendering", + "icon": "Layout", + "tags": [ + "ui", + "interface", + "layout" + ], + "isCore": false, + "defaultEnabled": false, + "isEngineModule": true, + "canContainContent": true, + "platforms": [ + "web", + "desktop", + "mobile" + ], + "dependencies": [ + "core", + "math" + ], + "exports": { + "components": [ + "UICanvasComponent", + "UITextComponent", + "UIImageComponent", + "UIButtonComponent" + ], + "systems": [ + "UIRenderSystem", + "UIEventSystem" + ] + }, + "editorPackage": "@esengine/ui-editor", + "requiresWasm": false, + "outputPath": "dist/index.js", + "pluginExport": "UIPlugin" +} diff --git a/packages/ui/src/UIRuntimeModule.ts b/packages/ui/src/UIRuntimeModule.ts index 2d8bfba5..2336bdee 100644 --- a/packages/ui/src/UIRuntimeModule.ts +++ b/packages/ui/src/UIRuntimeModule.ts @@ -1,6 +1,6 @@ import type { IScene } from '@esengine/ecs-framework'; import { ComponentRegistry } from '@esengine/ecs-framework'; -import type { IRuntimeModule, IPlugin, PluginDescriptor, SystemContext } from '@esengine/engine-core'; +import type { IRuntimeModule, IPlugin, ModuleManifest, SystemContext } from '@esengine/engine-core'; import { UITransformComponent, @@ -95,18 +95,25 @@ class UIRuntimeModule implements IRuntimeModule { } } -const descriptor: PluginDescriptor = { - id: '@esengine/ui', - name: 'UI', +const manifest: ModuleManifest = { + id: 'ui', + name: '@esengine/ui', + displayName: 'UI System', version: '1.0.0', description: 'ECS-based UI system', - category: 'ui', - enabledByDefault: true, - isEnginePlugin: true + category: 'Rendering', + icon: 'Layout', + isCore: false, + defaultEnabled: false, + isEngineModule: true, + canContainContent: true, + dependencies: ['core', 'math'], + exports: { components: ['UICanvasComponent'] }, + editorPackage: '@esengine/ui-editor' }; export const UIPlugin: IPlugin = { - descriptor, + manifest, runtimeModule: new UIRuntimeModule() };