diff --git a/examples/wechat-worker-demo/deploy.js b/examples/wechat-worker-demo/deploy.js index 692680eb..f36cf5a3 100644 --- a/examples/wechat-worker-demo/deploy.js +++ b/examples/wechat-worker-demo/deploy.js @@ -55,105 +55,283 @@ for (const file of filesToCopy) { } } -// 创建 game.js +// 创建 game.js - 完整物理球可视化演示 +// Create game.js - Full physics ball visualization demo const gameJs = `/** - * ESEngine Worker System 微信小游戏测试 - * ESEngine Worker System WeChat Mini Game Test + * ESEngine Worker System 微信小游戏物理演示 + * ESEngine Worker System WeChat Mini Game Physics Demo + * + * 演示 Worker 线程处理物理计算,主线程渲染 + * Demonstrates Worker thread physics + main thread rendering */ -console.log('===================================='); -console.log('ESEngine Worker 微信小游戏测试'); -console.log('===================================='); +// ============ 配置 | Configuration ============ +var CONFIG = { + BALL_COUNT: 20, // 球数量 | Number of balls + GRAVITY: 400, // 重力 | Gravity + GROUND_FRICTION: 0.98, // 地面摩擦 | Ground friction + BALL_BOUNCE: 0.85, // 弹性系数 | Bounce factor + MIN_RADIUS: 8, // 最小半径 | Min radius + MAX_RADIUS: 20, // 最大半径 | Max radius + COLORS: ['#FF6B6B', '#4ECDC4', '#45B7D1', '#96CEB4', '#FFEAA7', '#DDA0DD', '#98D8C8', '#F7DC6F'] +}; -// 检查 Worker API -console.log('\\n[1] 检查环境...'); -console.log('wx.createWorker:', typeof wx.createWorker); - -// 创建 Worker -console.log('\\n[2] 创建 Worker...'); +// ============ 全局状态 | Global State ============ +var canvas = wx.createCanvas(); +var ctx = canvas.getContext('2d'); var worker = null; +var entities = []; +var lastTime = Date.now(); +var frameCount = 0; +var fps = 0; +var workerReady = false; +var pendingRequest = false; -try { - worker = wx.createWorker('workers/physics-worker.js', { - useExperimentalWorker: true - }); - console.log('Worker 创建成功!'); -} catch (error) { - console.error('Worker 创建失败:', error.message); -} +console.log('===================================='); +console.log('ESEngine Worker 物理演示'); +console.log('Canvas:', canvas.width, 'x', canvas.height); +console.log('===================================='); -if (worker) { - // 设置消息处理 - worker.onMessage(function(res) { - console.log('\\n[4] 收到 Worker 响应!'); - - if (res.error) { - console.error('Worker 错误:', res.error); - } else if (res.result) { - console.log('Worker 处理成功!'); - console.log('实体数量:', res.result.length); - - // 显示前 3 个实体 - for (var i = 0; i < Math.min(3, res.result.length); i++) { - var e = res.result[i]; - console.log(' 实体 ' + e.id + ': (' + e.x.toFixed(1) + ', ' + e.y.toFixed(1) + ')'); - } - - console.log('\\n========== Worker 测试成功! =========='); - } - }); - - // 创建测试数据 - console.log('\\n[3] 发送测试数据...'); - - var entities = []; - for (var i = 0; i < 10; i++) { +// ============ 初始化实体 | Initialize Entities ============ +function initEntities() { + entities = []; + for (var i = 0; i < CONFIG.BALL_COUNT; i++) { + var radius = CONFIG.MIN_RADIUS + Math.random() * (CONFIG.MAX_RADIUS - CONFIG.MIN_RADIUS); entities.push({ id: i + 1, - x: Math.random() * 300 + 37, - y: Math.random() * 400 + 100, - dx: (Math.random() - 0.5) * 100, - dy: (Math.random() - 0.5) * 50, - mass: 1 + Math.random(), - bounce: 0.8, - friction: 0.98, - radius: 5 + Math.random() * 5 + x: radius + Math.random() * (canvas.width - radius * 2), + y: radius + Math.random() * (canvas.height * 0.5), // 上半部分生成 + dx: (Math.random() - 0.5) * 200, + dy: Math.random() * 100, + mass: radius * 0.1, + bounce: CONFIG.BALL_BOUNCE, + friction: CONFIG.GROUND_FRICTION, + radius: radius, + color: CONFIG.COLORS[i % CONFIG.COLORS.length] + }); + } + console.log('Created', entities.length, 'balls'); +} + +// ============ 创建 Worker | Create Worker ============ +function createWorker() { + try { + worker = wx.createWorker('workers/physics-worker.js', { + useExperimentalWorker: true + }); + + worker.onMessage(function(res) { + pendingRequest = false; + + if (res.error) { + console.error('Worker error:', res.error); + return; + } + + if (res.result && Array.isArray(res.result)) { + // 更新实体位置(保留颜色等渲染属性) + // Update entity positions (keep rendering properties like color) + for (var i = 0; i < res.result.length; i++) { + var updated = res.result[i]; + var entity = entities[i]; + if (entity && updated) { + entity.x = updated.x; + entity.y = updated.y; + entity.dx = updated.dx; + entity.dy = updated.dy; + } + } + } + }); + + workerReady = true; + console.log('Worker created successfully!'); + } catch (error) { + console.error('Worker creation failed:', error.message); + workerReady = false; + } +} + +// ============ 发送物理更新到 Worker | Send Physics Update to Worker ============ +function sendToWorker(deltaTime) { + if (!worker || !workerReady || pendingRequest) return; + + // 准备发送数据(只发送物理相关属性) + // Prepare data (only physics-related properties) + var physicsData = []; + for (var i = 0; i < entities.length; i++) { + var e = entities[i]; + physicsData.push({ + id: e.id, + x: e.x, + y: e.y, + dx: e.dx, + dy: e.dy, + mass: e.mass, + bounce: e.bounce, + friction: e.friction, + radius: e.radius }); } + pendingRequest = true; worker.postMessage({ id: Date.now(), - entities: entities, - deltaTime: 0.016, + entities: physicsData, + deltaTime: deltaTime, systemConfig: { - gravity: 200, - canvasWidth: 375, - canvasHeight: 667, - groundFriction: 0.98 + gravity: CONFIG.GRAVITY, + canvasWidth: canvas.width, + canvasHeight: canvas.height, + groundFriction: CONFIG.GROUND_FRICTION } }); - - console.log('已发送 ' + entities.length + ' 个实体'); } -// 创建 Canvas 显示 -var canvas = wx.createCanvas(); -var ctx = canvas.getContext('2d'); +// ============ 渲染 | Render ============ +function render() { + // 清屏 - 深色背景 + // Clear screen - dark background + ctx.fillStyle = '#1a1a2e'; + ctx.fillRect(0, 0, canvas.width, canvas.height); -ctx.fillStyle = '#1a1a2e'; -ctx.fillRect(0, 0, canvas.width, canvas.height); + // 绘制地面 + // Draw ground + ctx.fillStyle = '#2d3436'; + ctx.fillRect(0, canvas.height - 10, canvas.width, 10); -ctx.fillStyle = '#ffffff'; -ctx.font = '18px Arial'; -ctx.textAlign = 'center'; -ctx.fillText('ESEngine Worker 测试', canvas.width / 2, 50); + // 绘制所有球 + // Draw all balls + for (var i = 0; i < entities.length; i++) { + var e = entities[i]; -ctx.font = '14px Arial'; -ctx.fillStyle = '#aaaaaa'; -ctx.fillText('查看控制台日志', canvas.width / 2, 80); + // 球体渐变效果 + // Ball gradient effect + var gradient = ctx.createRadialGradient( + e.x - e.radius * 0.3, e.y - e.radius * 0.3, 0, + e.x, e.y, e.radius + ); + gradient.addColorStop(0, '#ffffff'); + gradient.addColorStop(0.3, e.color); + gradient.addColorStop(1, shadeColor(e.color, -30)); -ctx.fillStyle = worker ? '#00ff00' : '#ff0000'; -ctx.fillText('Worker: ' + (worker ? '已创建' : '创建失败'), canvas.width / 2, 110); + ctx.beginPath(); + ctx.arc(e.x, e.y, e.radius, 0, Math.PI * 2); + ctx.fillStyle = gradient; + ctx.fill(); + + // 球体边框 + // Ball border + ctx.strokeStyle = shadeColor(e.color, -50); + ctx.lineWidth = 1; + ctx.stroke(); + } + + // 绘制 UI + // Draw UI + ctx.fillStyle = '#ffffff'; + ctx.font = '14px Arial'; + ctx.textAlign = 'left'; + ctx.fillText('ESEngine Worker Physics Demo', 10, 25); + ctx.fillText('FPS: ' + fps + ' | Balls: ' + entities.length, 10, 45); + ctx.fillText('Worker: ' + (workerReady ? 'Active' : 'Failed'), 10, 65); + + // 提示文字 + // Hint text + ctx.textAlign = 'center'; + ctx.fillStyle = '#888888'; + ctx.font = '12px Arial'; + ctx.fillText('Physics calculated in Worker thread', canvas.width / 2, canvas.height - 20); +} + +// 颜色加深/减淡工具函数 +// Color shade utility function +function shadeColor(color, percent) { + var num = parseInt(color.replace('#', ''), 16); + var amt = Math.round(2.55 * percent); + var R = (num >> 16) + amt; + var G = (num >> 8 & 0x00FF) + amt; + var B = (num & 0x0000FF) + amt; + R = Math.max(0, Math.min(255, R)); + G = Math.max(0, Math.min(255, G)); + B = Math.max(0, Math.min(255, B)); + return '#' + (0x1000000 + R * 0x10000 + G * 0x100 + B).toString(16).slice(1); +} + +// ============ 游戏循环 | Game Loop ============ +function gameLoop() { + var now = Date.now(); + var deltaTime = (now - lastTime) / 1000; + lastTime = now; + + // 限制 deltaTime 防止跳帧 + // Clamp deltaTime to prevent frame skip + if (deltaTime > 0.1) deltaTime = 0.1; + + // FPS 计算 + // FPS calculation + frameCount++; + if (frameCount >= 30) { + fps = Math.round(30 / ((now - (lastTime - deltaTime * 1000 * 30)) / 1000)); + frameCount = 0; + } + + // 发送物理计算到 Worker + // Send physics calculation to Worker + sendToWorker(deltaTime); + + // 渲染 + // Render + render(); + + // 下一帧 + // Next frame + requestAnimationFrame(gameLoop); +} + +// ============ 启动 | Start ============ +initEntities(); +createWorker(); + +// 简单的 FPS 计算变量 +var fpsLastTime = Date.now(); +var fpsFrameCount = 0; + +// 覆盖 FPS 计算逻辑 +setInterval(function() { + var now = Date.now(); + fps = Math.round(fpsFrameCount * 1000 / (now - fpsLastTime)); + fpsLastTime = now; + fpsFrameCount = 0; +}, 1000); + +// 修改 gameLoop 中的帧计数 +var originalGameLoop = gameLoop; +gameLoop = function() { + fpsFrameCount++; + + var now = Date.now(); + var deltaTime = (now - lastTime) / 1000; + lastTime = now; + + if (deltaTime > 0.1) deltaTime = 0.1; + + sendToWorker(deltaTime); + render(); + requestAnimationFrame(gameLoop); +}; + +// 开始游戏循环 +// Start game loop +console.log('Starting game loop...'); +requestAnimationFrame(gameLoop); + +// 触摸重置 +// Touch to reset +wx.onTouchStart(function(e) { + console.log('Touch detected - resetting balls'); + initEntities(); +}); `; const gameJsPath = path.join(WECHAT_PROJECT, 'game.js'); diff --git a/examples/wechat-worker-demo/worker-mapping.json b/examples/wechat-worker-demo/worker-mapping.json new file mode 100644 index 00000000..c28c6c1c --- /dev/null +++ b/examples/wechat-worker-demo/worker-mapping.json @@ -0,0 +1,6 @@ +{ + "generatedAt": "2025-12-08T10:33:50.647Z", + "mappings": { + "PhysicsWorkerSystem": "workers/physics-worker.js" + } +} \ No newline at end of file diff --git a/packages/audio/src/index.ts b/packages/audio/src/index.ts index 66bf81d0..c953b858 100644 --- a/packages/audio/src/index.ts +++ b/packages/audio/src/index.ts @@ -1,2 +1,6 @@ export { AudioSourceComponent } from './AudioSourceComponent'; export { AudioPlugin } from './AudioPlugin'; + +// Service Tokens (reserved for future use) +// 服务令牌(预留用于未来扩展) +// export { AudioManagerToken, type IAudioManager } from './tokens'; diff --git a/packages/audio/src/tokens.ts b/packages/audio/src/tokens.ts new file mode 100644 index 00000000..3a87216a --- /dev/null +++ b/packages/audio/src/tokens.ts @@ -0,0 +1,31 @@ +/** + * Audio Module Service Tokens + * 音频模块服务令牌 + * + * 遵循"谁定义接口,谁导出 Token"原则。 + * Following "who defines interface, who exports Token" principle. + * + * 当前模块仅提供组件,暂无服务定义。 + * 此文件预留用于未来可能添加的 AudioManager 服务。 + * + * Currently this module only provides components, no services defined yet. + * This file is reserved for potential future AudioManager service. + */ + +// import { createServiceToken } from '@esengine/engine-core'; + +// ============================================================================ +// Reserved for future service tokens +// 预留用于未来的服务令牌 +// ============================================================================ + +// export interface IAudioManager { +// // 播放音效 | Play sound effect +// playSound(path: string): void; +// // 播放背景音乐 | Play background music +// playMusic(path: string): void; +// // 停止所有音频 | Stop all audio +// stopAll(): void; +// } + +// export const AudioManagerToken = createServiceToken('audioManager'); diff --git a/packages/camera/src/index.ts b/packages/camera/src/index.ts index 2fa92d1c..382385b0 100644 --- a/packages/camera/src/index.ts +++ b/packages/camera/src/index.ts @@ -1,2 +1,6 @@ export { CameraComponent, ECameraProjection, CameraProjection } from './CameraComponent'; export { CameraPlugin } from './CameraPlugin'; + +// Service Tokens (reserved for future use) +// 服务令牌(预留用于未来扩展) +// export { CameraManagerToken, type ICameraManager } from './tokens'; diff --git a/packages/camera/src/tokens.ts b/packages/camera/src/tokens.ts new file mode 100644 index 00000000..dcf11feb --- /dev/null +++ b/packages/camera/src/tokens.ts @@ -0,0 +1,31 @@ +/** + * Camera Module Service Tokens + * 相机模块服务令牌 + * + * 遵循"谁定义接口,谁导出 Token"原则。 + * Following "who defines interface, who exports Token" principle. + * + * 当前模块仅提供组件,暂无服务定义。 + * 此文件预留用于未来可能添加的 CameraManager 服务。 + * + * Currently this module only provides components, no services defined yet. + * This file is reserved for potential future CameraManager service. + */ + +// import { createServiceToken } from '@esengine/engine-core'; + +// ============================================================================ +// Reserved for future service tokens +// 预留用于未来的服务令牌 +// ============================================================================ + +// export interface ICameraManager { +// // 获取主相机 | Get main camera +// getMainCamera(): CameraComponent | null; +// // 设置主相机 | Set main camera +// setMainCamera(camera: CameraComponent): void; +// // 屏幕坐标转世界坐标 | Screen to world coordinates +// screenToWorld(screenX: number, screenY: number): { x: number; y: number }; +// } + +// export const CameraManagerToken = createServiceToken('cameraManager'); diff --git a/packages/core/src/Core.ts b/packages/core/src/Core.ts index 8c6dd263..31d720e2 100644 --- a/packages/core/src/Core.ts +++ b/packages/core/src/Core.ts @@ -11,6 +11,7 @@ import { SceneManager } from './ECS/SceneManager'; import { IScene } from './ECS/IScene'; import { ServiceContainer } from './Core/ServiceContainer'; import { PluginManager } from './Core/PluginManager'; +import { PluginServiceRegistry } from './Core/PluginServiceRegistry'; import { IPlugin } from './Core/Plugin'; import { WorldManager } from './ECS/WorldManager'; import { DebugConfigService } from './Utils/Debug/DebugConfigService'; @@ -109,6 +110,14 @@ export class Core { */ private _pluginManager: PluginManager; + /** + * 插件服务注册表 + * + * 基于 ServiceToken 的类型安全服务注册表。 + * Type-safe service registry based on ServiceToken. + */ + private _pluginServiceRegistry: PluginServiceRegistry; + /** * Core配置 */ @@ -168,6 +177,11 @@ export class Core { this._pluginManager.initialize(this, this._serviceContainer); this._serviceContainer.registerInstance(PluginManager, this._pluginManager); + // 初始化插件服务注册表 + // Initialize plugin service registry + this._pluginServiceRegistry = new PluginServiceRegistry(); + this._serviceContainer.registerInstance(PluginServiceRegistry, this._pluginServiceRegistry); + this.debug = this._config.debug ?? true; // 初始化调试管理器 @@ -220,6 +234,39 @@ export class Core { return this._instance._serviceContainer; } + /** + * 获取插件服务注册表 + * + * 用于基于 ServiceToken 的类型安全服务注册和获取。 + * For type-safe service registration and retrieval based on ServiceToken. + * + * @returns PluginServiceRegistry 实例 + * @throws 如果 Core 实例未创建 + * + * @example + * ```typescript + * import { createServiceToken } from '@esengine/ecs-framework'; + * + * // 定义服务令牌 + * const MyServiceToken = createServiceToken('myService'); + * + * // 注册服务 + * Core.pluginServices.register(MyServiceToken, myServiceInstance); + * + * // 获取服务(可选) + * const service = Core.pluginServices.get(MyServiceToken); + * + * // 获取服务(必需,不存在则抛异常) + * const service = Core.pluginServices.require(MyServiceToken); + * ``` + */ + public static get pluginServices(): PluginServiceRegistry { + if (!this._instance) { + throw new Error('Core实例未创建,请先调用Core.create()'); + } + return this._instance._pluginServiceRegistry; + } + /** * 获取World管理器 * diff --git a/packages/core/src/Core/PluginServiceRegistry.ts b/packages/core/src/Core/PluginServiceRegistry.ts new file mode 100644 index 00000000..7856d654 --- /dev/null +++ b/packages/core/src/Core/PluginServiceRegistry.ts @@ -0,0 +1,135 @@ +/** + * 插件服务注册表 + * Plugin Service Registry + * + * 基于 ServiceToken 的类型安全服务注册表。 + * Type-safe service registry based on ServiceToken. + * + * 设计原则 | Design principles: + * 1. 类型安全 - 使用 ServiceToken 携带类型信息 + * 2. 显式依赖 - 通过导入 token 明确表达依赖关系 + * 3. 可选依赖 - get 返回 undefined,require 抛异常 + * 4. 单一职责 - 只负责服务注册和查询,不涉及生命周期管理 + * 5. 谁定义接口,谁导出 Token - 各模块定义自己的接口和 Token + */ + +// ============================================================================ +// 服务令牌 | Service Token +// ============================================================================ + +/** + * 服务令牌接口 + * Service token interface + * + * 用于类型安全的服务注册和获取。 + * For type-safe service registration and retrieval. + * + * 注意:__phantom 是必需属性,确保 TypeScript 在跨包类型解析时保留泛型类型信息。 + * Note: __phantom is a required property to ensure TypeScript preserves generic + * type information across packages. + */ +export interface ServiceToken { + readonly id: symbol; + readonly name: string; + /** + * Phantom type 标记(强制类型推断) + * Phantom type marker (enforces type inference) + */ + readonly __phantom: T; +} + +/** + * 创建服务令牌 + * Create a service token + * + * @param name 令牌名称 | Token name + * @returns 服务令牌 | Service token + */ +export function createServiceToken(name: string): ServiceToken { + // __phantom 仅用于类型推断,运行时不需要实际值 + // __phantom is only for type inference, no actual value needed at runtime + return { + id: Symbol(name), + name + } as ServiceToken; +} + +// ============================================================================ +// 插件服务注册表 | Plugin Service Registry +// ============================================================================ + +/** + * 插件服务注册表 + * Plugin service registry + * + * 用于跨插件共享服务的类型安全注册表。 + * Type-safe registry for sharing services between plugins. + */ +export class PluginServiceRegistry { + private _services = new Map(); + + /** + * 注册服务 + * Register a service + */ + register(token: ServiceToken, service: T): void { + this._services.set(token.id, service); + } + + /** + * 获取服务(可选) + * Get a service (optional) + */ + get(token: ServiceToken): T | undefined { + return this._services.get(token.id) as T | undefined; + } + + /** + * 获取服务(必需) + * Get a service (required) + * + * @throws 如果服务未注册 | If service is not registered + */ + require(token: ServiceToken): T { + const service = this._services.get(token.id); + if (service === undefined) { + throw new Error(`Service not found: ${token.name}`); + } + return service as T; + } + + /** + * 检查服务是否已注册 + * Check if a service is registered + */ + has(token: ServiceToken): boolean { + return this._services.has(token.id); + } + + /** + * 注销服务 + * Unregister a service + */ + unregister(token: ServiceToken): boolean { + return this._services.delete(token.id); + } + + /** + * 清空所有服务 + * Clear all services + */ + clear(): void { + this._services.clear(); + } + + /** + * 释放资源 + * Dispose resources + * + * 实现 IService 接口,在服务容器清理时调用。 + * Implements IService interface, called when service container is cleaned up. + */ + dispose(): void { + this.clear(); + } +} diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index b90f4c43..ac0c90c3 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -8,6 +8,11 @@ export { Core } from './Core'; export { ServiceContainer, ServiceLifetime } from './Core/ServiceContainer'; export type { IService, ServiceType, ServiceIdentifier } from './Core/ServiceContainer'; +// 插件服务注册表(基于 ServiceToken 的类型安全服务管理) +// Plugin Service Registry (type-safe service management based on ServiceToken) +export { PluginServiceRegistry, createServiceToken } from './Core/PluginServiceRegistry'; +export type { ServiceToken } from './Core/PluginServiceRegistry'; + // 插件系统 export { PluginManager } from './Core/PluginManager'; export { PluginState } from './Core/Plugin'; diff --git a/packages/editor-app/src/App.tsx b/packages/editor-app/src/App.tsx index 4655a6cc..637ab2b1 100644 --- a/packages/editor-app/src/App.tsx +++ b/packages/editor-app/src/App.tsx @@ -3,6 +3,7 @@ import * as ReactDOM from 'react-dom'; import * as ReactJSXRuntime from 'react/jsx-runtime'; import { Core, createLogger, Scene } from '@esengine/ecs-framework'; import * as ECSFramework from '@esengine/ecs-framework'; +import { getProfilerService } from './services/getService'; // 将 React 暴露到全局,供动态加载的插件使用 // editor-runtime.js 将 React 设为 external,需要从全局获取 @@ -207,14 +208,15 @@ function App() { }, [messageHub, showToast]); // 监听远程连接状态 + // Monitor remote connection status useEffect(() => { const checkConnection = () => { - const profilerService = (window as any).__PROFILER_SERVICE__; - const connected = profilerService && profilerService.isConnected(); + const profilerService = getProfilerService(); + const connected = !!(profilerService && profilerService.isConnected()); setIsRemoteConnected((prevConnected) => { if (connected !== prevConnected) { - // 状态发生变化 + // 状态发生变化 | State has changed if (connected) { setStatus(t('header.status.remoteConnected')); } else { @@ -246,7 +248,8 @@ function App() { initRef.current = true; try { - (window as any).__ECS_FRAMEWORK__ = ECSFramework; + // ECS Framework 已通过 PluginSDKRegistry 暴露到全局 + // ECS Framework is exposed globally via PluginSDKRegistry const editorScene = new Scene(); Core.setScene(editorScene); @@ -775,7 +778,7 @@ function App() { const Component = panelDesc.component; return { id: panelDesc.id, - title: (panelDesc as any).titleZh && locale === 'zh' ? (panelDesc as any).titleZh : panelDesc.title, + title: panelDesc.titleZh && locale === 'zh' ? panelDesc.titleZh : panelDesc.title, content: , closable: panelDesc.closable ?? true }; @@ -791,7 +794,7 @@ function App() { const panelDesc = uiRegistry.getPanel(panelId)!; // 优先使用动态标题,否则使用默认标题 const customTitle = dynamicPanelTitles.get(panelId); - const defaultTitle = (panelDesc as any).titleZh && locale === 'zh' ? (panelDesc as any).titleZh : panelDesc.title; + const defaultTitle = panelDesc.titleZh && locale === 'zh' ? panelDesc.titleZh : panelDesc.title; // 支持 component 或 render 两种方式 let content: React.ReactNode; diff --git a/packages/editor-app/src/components/AdvancedProfilerWindow.tsx b/packages/editor-app/src/components/AdvancedProfilerWindow.tsx index e25d8f74..1323f498 100644 --- a/packages/editor-app/src/components/AdvancedProfilerWindow.tsx +++ b/packages/editor-app/src/components/AdvancedProfilerWindow.tsx @@ -1,6 +1,7 @@ import { useState, useEffect, useCallback } from 'react'; import { X, BarChart3, Maximize2, Minimize2 } from 'lucide-react'; -import { ProfilerService } from '../services/ProfilerService'; +import { Core } from '@esengine/ecs-framework'; +import { ProfilerServiceToken, type IProfilerService } from '../services/tokens'; import { AdvancedProfiler } from './AdvancedProfiler'; import '../styles/ProfilerWindow.css'; @@ -8,19 +9,19 @@ interface AdvancedProfilerWindowProps { onClose: () => void; } -interface WindowWithProfiler extends Window { - __PROFILER_SERVICE__?: ProfilerService; -} - export function AdvancedProfilerWindow({ onClose }: AdvancedProfilerWindowProps) { - const [profilerService, setProfilerService] = useState(null); + const [profilerService, setProfilerService] = useState(null); const [isConnected, setIsConnected] = useState(false); const [isFullscreen, setIsFullscreen] = useState(false); useEffect(() => { - const service = (window as WindowWithProfiler).__PROFILER_SERVICE__; - if (service) { - setProfilerService(service); + try { + const service = Core.pluginServices.get(ProfilerServiceToken); + if (service) { + setProfilerService(service); + } + } catch { + // Core 可能还没有初始化 } }, []); diff --git a/packages/editor-app/src/components/ContentBrowser.tsx b/packages/editor-app/src/components/ContentBrowser.tsx index 6270c593..62d500b2 100644 --- a/packages/editor-app/src/components/ContentBrowser.tsx +++ b/packages/editor-app/src/components/ContentBrowser.tsx @@ -40,22 +40,13 @@ import { AlertTriangle } from 'lucide-react'; import { Core } from '@esengine/ecs-framework'; -import { MessageHub, FileActionRegistry, AssetRegistryService, type FileCreationTemplate } from '@esengine/editor-core'; +import { MessageHub, FileActionRegistry, AssetRegistryService, MANAGED_ASSET_DIRECTORIES, type FileCreationTemplate } from '@esengine/editor-core'; import { TauriAPI, DirectoryEntry } from '../api/tauri'; import { SettingsService } from '../services/SettingsService'; import { ContextMenu, ContextMenuItem } from './ContextMenu'; import { PromptDialog } from './PromptDialog'; import '../styles/ContentBrowser.css'; -/** - * Directories managed by asset registry (GUID system) - * 被资产注册表(GUID 系统)管理的目录 - * - * Note: This is duplicated from AssetRegistryService to avoid build dependency issues. - * Keep in sync with MANAGED_ASSET_DIRECTORIES in AssetRegistryService.ts - */ -const MANAGED_ASSET_DIRECTORIES = ['assets', 'scripts', 'scenes'] as const; - interface AssetItem { name: string; path: string; diff --git a/packages/editor-app/src/components/PortManager.tsx b/packages/editor-app/src/components/PortManager.tsx index 0228f2b3..600fd517 100644 --- a/packages/editor-app/src/components/PortManager.tsx +++ b/packages/editor-app/src/components/PortManager.tsx @@ -2,7 +2,7 @@ import { useState, useEffect } from 'react'; import { invoke } from '@tauri-apps/api/core'; import { X, Server, WifiOff, Wifi } from 'lucide-react'; import { SettingsService } from '../services/SettingsService'; -import { ProfilerService } from '../services/ProfilerService'; +import { getProfilerService } from '../services/getService'; import '../styles/PortManager.css'; interface PortManagerProps { @@ -58,7 +58,7 @@ export function PortManager({ onClose }: PortManagerProps) { const handleStopServer = async () => { setIsStopping(true); try { - const profilerService = (window as any).__PROFILER_SERVICE__ as ProfilerService | undefined; + const profilerService = getProfilerService(); if (profilerService) { await profilerService.manualStopServer(); setIsServerRunning(false); @@ -73,7 +73,7 @@ export function PortManager({ onClose }: PortManagerProps) { const handleStartServer = async () => { setIsStarting(true); try { - const profilerService = (window as any).__PROFILER_SERVICE__ as ProfilerService | undefined; + const profilerService = getProfilerService(); if (profilerService) { await profilerService.manualStartServer(); await new Promise((resolve) => setTimeout(resolve, 500)); diff --git a/packages/editor-app/src/components/ProfilerDockPanel.tsx b/packages/editor-app/src/components/ProfilerDockPanel.tsx index 1d06a930..c038401e 100644 --- a/packages/editor-app/src/components/ProfilerDockPanel.tsx +++ b/packages/editor-app/src/components/ProfilerDockPanel.tsx @@ -1,9 +1,10 @@ import { useState, useEffect } from 'react'; import { Activity, Cpu, Layers, Package, Wifi, WifiOff, Maximize2, Pause, Play, BarChart3 } from 'lucide-react'; -import { ProfilerService, ProfilerData } from '../services/ProfilerService'; +import type { ProfilerData } from '../services/tokens'; import { SettingsService } from '../services/SettingsService'; import { Core } from '@esengine/ecs-framework'; import { MessageHub } from '@esengine/editor-core'; +import { getProfilerService } from '../services/getService'; import '../styles/ProfilerDockPanel.css'; export function ProfilerDockPanel() { @@ -32,7 +33,7 @@ export function ProfilerDockPanel() { }, []); useEffect(() => { - const profilerService = (window as any).__PROFILER_SERVICE__ as ProfilerService | undefined; + const profilerService = getProfilerService(); if (!profilerService) { console.warn('[ProfilerDockPanel] ProfilerService not available - plugin may be disabled'); diff --git a/packages/editor-app/src/components/ProfilerWindow.tsx b/packages/editor-app/src/components/ProfilerWindow.tsx index 74d3aa4f..3fdd00db 100644 --- a/packages/editor-app/src/components/ProfilerWindow.tsx +++ b/packages/editor-app/src/components/ProfilerWindow.tsx @@ -3,6 +3,7 @@ import { Core } from '@esengine/ecs-framework'; import { Activity, BarChart3, Clock, Cpu, RefreshCw, Pause, Play, X, Wifi, WifiOff, Server, Search, Table2, TreePine } from 'lucide-react'; import { ProfilerService } from '../services/ProfilerService'; import { SettingsService } from '../services/SettingsService'; +import { getProfilerService } from '../services/getService'; import '../styles/ProfilerWindow.css'; interface SystemPerformanceData { @@ -59,7 +60,7 @@ export function ProfilerWindow({ onClose }: ProfilerWindowProps) { // Check ProfilerService connection status useEffect(() => { - const profilerService = (window as any).__PROFILER_SERVICE__ as ProfilerService | undefined; + const profilerService = getProfilerService(); if (!profilerService) { return; @@ -186,7 +187,7 @@ export function ProfilerWindow({ onClose }: ProfilerWindowProps) { useEffect(() => { if (dataSource !== 'remote') return; - const profilerService = (window as any).__PROFILER_SERVICE__ as ProfilerService | undefined; + const profilerService = getProfilerService(); if (!profilerService) { console.warn('[ProfilerWindow] ProfilerService not available'); diff --git a/packages/editor-app/src/components/SceneHierarchy.tsx b/packages/editor-app/src/components/SceneHierarchy.tsx index d4fd8393..c6549eaa 100644 --- a/packages/editor-app/src/components/SceneHierarchy.tsx +++ b/packages/editor-app/src/components/SceneHierarchy.tsx @@ -8,7 +8,8 @@ import { Eye, Star, Lock, Settings, Filter, Folder, Sun, Cloud, Mountain, Flag, SquareStack, FolderPlus } from 'lucide-react'; -import { ProfilerService, RemoteEntity } from '../services/ProfilerService'; +import type { RemoteEntity } from '../services/tokens'; +import { getProfilerService } from '../services/getService'; import { confirm } from '@tauri-apps/plugin-dialog'; import { CreateEntityCommand, DeleteEntityCommand, ReparentEntityCommand, DropPosition } from '../application/commands/entity'; import '../styles/SceneHierarchy.css'; @@ -264,7 +265,7 @@ export function SceneHierarchy({ entityStore, messageHub, commandManager, isProf // Subscribe to remote entity data from ProfilerService useEffect(() => { - const profilerService = (window as any).__PROFILER_SERVICE__ as ProfilerService | undefined; + const profilerService = getProfilerService(); if (!profilerService) { return; @@ -444,7 +445,7 @@ export function SceneHierarchy({ entityStore, messageHub, commandManager, isProf const handleRemoteEntityClick = (entity: RemoteEntity) => { setSelectedIds(new Set([entity.id])); - const profilerService = (window as any).__PROFILER_SERVICE__ as ProfilerService | undefined; + const profilerService = getProfilerService(); if (profilerService) { profilerService.requestEntityDetails(entity.id); } diff --git a/packages/editor-app/src/components/dialogs/AssetPickerDialog.tsx b/packages/editor-app/src/components/dialogs/AssetPickerDialog.tsx index 3d286a33..c0c40f2b 100644 --- a/packages/editor-app/src/components/dialogs/AssetPickerDialog.tsx +++ b/packages/editor-app/src/components/dialogs/AssetPickerDialog.tsx @@ -1,18 +1,10 @@ import React, { useState, useEffect, useMemo, useCallback } from 'react'; import { X, Search, Folder, FolderOpen, File, Image, FileText, Music, Video, Database, AlertTriangle } from 'lucide-react'; import { Core } from '@esengine/ecs-framework'; -import { ProjectService, AssetRegistryService } from '@esengine/editor-core'; +import { ProjectService, AssetRegistryService, MANAGED_ASSET_DIRECTORIES } from '@esengine/editor-core'; import { TauriFileSystemService } from '../../services/TauriFileSystemService'; import './AssetPickerDialog.css'; -/** - * Directories managed by asset registry (GUID system) - * Only files in these directories can be selected - * - * Note: Keep in sync with MANAGED_ASSET_DIRECTORIES in AssetRegistryService.ts - */ -const MANAGED_ASSET_DIRECTORIES = ['assets', 'scripts', 'scenes'] as const; - interface AssetPickerDialogProps { isOpen: boolean; onClose: () => void; diff --git a/packages/editor-app/src/components/inspectors/fields/CollisionLayerField.tsx b/packages/editor-app/src/components/inspectors/fields/CollisionLayerField.tsx index 92d5e525..c0fa4813 100644 --- a/packages/editor-app/src/components/inspectors/fields/CollisionLayerField.tsx +++ b/packages/editor-app/src/components/inspectors/fields/CollisionLayerField.tsx @@ -4,18 +4,15 @@ */ import React, { useState, useEffect, useRef, useCallback } from 'react'; - -/** - * 碰撞层配置接口(用于获取自定义层名称) - */ -interface CollisionLayerConfigAPI { - getLayers(): Array<{ name: string }>; - addListener(callback: () => void): void; - removeListener(callback: () => void): void; -} +import { Core } from '@esengine/ecs-framework'; +import { + CollisionLayerConfigToken, + type ICollisionLayerConfig +} from '@esengine/physics-rapier2d'; /** * 默认层名称(当 CollisionLayerConfig 不可用时使用) + * Default layer names (used when CollisionLayerConfig is unavailable) */ const DEFAULT_LAYER_NAMES = [ 'Default', 'Player', 'Enemy', 'Projectile', @@ -24,25 +21,18 @@ const DEFAULT_LAYER_NAMES = [ 'Layer12', 'Layer13', 'Layer14', 'Layer15', ]; -let cachedConfig: CollisionLayerConfigAPI | null = null; - /** * 尝试获取 CollisionLayerConfig 实例 + * Try to get CollisionLayerConfig instance */ -function getCollisionConfig(): CollisionLayerConfigAPI | null { - if (cachedConfig) return cachedConfig; - +function getCollisionConfig(): ICollisionLayerConfig | undefined { try { - // 动态导入以避免循环依赖 - const physicsModule = (window as any).__PHYSICS_RAPIER2D__; - if (physicsModule?.CollisionLayerConfig) { - cachedConfig = physicsModule.CollisionLayerConfig.getInstance(); - return cachedConfig; - } + return Core.pluginServices.get(CollisionLayerConfigToken); } catch { - // 忽略错误 + // Core 可能还没有初始化 + // Core might not be initialized yet + return undefined; } - return null; } interface CollisionLayerFieldProps { diff --git a/packages/editor-app/src/components/inspectors/utils.ts b/packages/editor-app/src/components/inspectors/utils.ts index 17b53a0c..9e7801f0 100644 --- a/packages/editor-app/src/components/inspectors/utils.ts +++ b/packages/editor-app/src/components/inspectors/utils.ts @@ -1,4 +1,6 @@ +import { Core } from '@esengine/ecs-framework'; import { ComponentData } from './types'; +import { ProfilerServiceToken, type IProfilerService } from '../../services/tokens'; export function formatNumber(value: number, decimalPlaces: number): string { if (decimalPlaces < 0) { @@ -10,13 +12,21 @@ export function formatNumber(value: number, decimalPlaces: number): string { return value.toFixed(decimalPlaces); } -export interface ProfilerService { - requestEntityDetails(entityId: number): void; - subscribe(callback: () => void): () => void; -} - -export function getProfilerService(): ProfilerService | undefined { - return (window as any).__PROFILER_SERVICE__; +/** + * 获取 ProfilerService 实例 + * Get ProfilerService instance + * + * 使用 ServiceToken 从 Core.pluginServices 获取服务。 + * Uses ServiceToken to get service from Core.pluginServices. + */ +export function getProfilerService(): IProfilerService | undefined { + try { + return Core.pluginServices.get(ProfilerServiceToken); + } catch { + // Core 可能还没有初始化 + // Core might not be initialized yet + return undefined; + } } export function isComponentData(value: unknown): value is ComponentData { diff --git a/packages/editor-app/src/hooks/useDynamicIcon.ts b/packages/editor-app/src/hooks/useDynamicIcon.ts index 6c86e689..2f4b044d 100644 --- a/packages/editor-app/src/hooks/useDynamicIcon.ts +++ b/packages/editor-app/src/hooks/useDynamicIcon.ts @@ -1,14 +1,22 @@ import { useMemo } from 'react'; import * as LucideIcons from 'lucide-react'; -type LucideIconName = keyof typeof LucideIcons; - +/** + * 动态获取 Lucide 图标组件 + * Dynamically get Lucide icon component by name + * + * @param iconName - 图标名称(如 'Package', 'Settings') + * @param fallback - 找不到时的回退组件 + * @returns Lucide 图标组件 + */ export function useDynamicIcon(iconName?: string, fallback?: React.ComponentType) { return useMemo(() => { if (!iconName) { return fallback || LucideIcons.Package; } + // 动态图标查找需要使用 any,因为 lucide-react 的类型定义不支持动态索引 + // Dynamic icon lookup requires any, as lucide-react types don't support dynamic indexing const IconComponent = (LucideIcons as any)[iconName]; return IconComponent || fallback || LucideIcons.Package; }, [iconName, fallback]); diff --git a/packages/editor-app/src/hooks/useProfilerService.ts b/packages/editor-app/src/hooks/useProfilerService.ts index bca466bf..8d48ef0b 100644 --- a/packages/editor-app/src/hooks/useProfilerService.ts +++ b/packages/editor-app/src/hooks/useProfilerService.ts @@ -1,23 +1,52 @@ +/** + * ProfilerService Hook + * + * 通过 ServiceToken 获取 ProfilerService 实例。 + * Get ProfilerService instance via ServiceToken. + */ + import { useEffect, useState } from 'react'; +import { Core } from '@esengine/ecs-framework'; +import { ProfilerServiceToken, type IProfilerService } from '../services/tokens'; -export interface ProfilerService { - connect(port: number): void; - disconnect(): void; - isConnected(): boolean; - requestEntityList(): void; - requestEntityDetails(entityId: number): void; -} - -export function useProfilerService(): ProfilerService | undefined { - const [service, setService] = useState(() => { - return (window as any).__PROFILER_SERVICE__; +/** + * 获取 ProfilerService 实例的 Hook + * Hook to get ProfilerService instance + * + * 使用 ServiceToken 从 Core.pluginServices 获取服务, + * 提供类型安全的服务访问。 + * + * Uses ServiceToken to get service from Core.pluginServices, + * providing type-safe service access. + * + * @returns ProfilerService 实例,如果未注册则返回 undefined + */ +export function useProfilerService(): IProfilerService | undefined { + const [service, setService] = useState(() => { + try { + return Core.pluginServices.get(ProfilerServiceToken); + } catch { + // Core 可能还没有初始化 + // Core might not be initialized yet + return undefined; + } }); useEffect(() => { + // 定期检查服务是否可用(处理服务延迟注册的情况) + // Periodically check if service is available (handles delayed service registration) const checkService = () => { - const newService = (window as any).__PROFILER_SERVICE__; - if (newService !== service) { - setService(newService); + try { + const newService = Core.pluginServices.get(ProfilerServiceToken); + if (newService !== service) { + setService(newService); + } + } catch { + // Core 可能还没有初始化 + // Core might not be initialized yet + if (service !== undefined) { + setService(undefined); + } } }; diff --git a/packages/editor-app/src/plugins/builtin/ProfilerPlugin.tsx b/packages/editor-app/src/plugins/builtin/ProfilerPlugin.tsx index f411dfd5..208c0404 100644 --- a/packages/editor-app/src/plugins/builtin/ProfilerPlugin.tsx +++ b/packages/editor-app/src/plugins/builtin/ProfilerPlugin.tsx @@ -4,6 +4,7 @@ */ import type { ServiceContainer } from '@esengine/ecs-framework'; +import { Core } from '@esengine/ecs-framework'; import type { IPlugin, IEditorModuleLoader, @@ -12,6 +13,7 @@ import type { } from '@esengine/editor-core'; import { MessageHub, SettingsRegistry } from '@esengine/editor-core'; import { ProfilerService } from '../../services/ProfilerService'; +import { ProfilerServiceToken } from '../../services/tokens'; /** * Profiler 编辑器模块 @@ -87,15 +89,21 @@ class ProfilerEditorModule implements IEditorModuleLoader { }); this.profilerService = new ProfilerService(); - (window as any).__PROFILER_SERVICE__ = this.profilerService; + + // 使用 ServiceToken 注册服务(类型安全) + // Register service using ServiceToken (type-safe) + Core.pluginServices.register(ProfilerServiceToken, this.profilerService); } async uninstall(): Promise { + // 从服务注册表注销 + // Unregister from service registry + Core.pluginServices.unregister(ProfilerServiceToken); + if (this.profilerService) { this.profilerService.destroy(); this.profilerService = null; } - delete (window as any).__PROFILER_SERVICE__; } getMenuItems(): MenuItemDescriptor[] { diff --git a/packages/editor-app/src/services/ProfilerService.ts b/packages/editor-app/src/services/ProfilerService.ts index 0d47d73f..0505ec1c 100644 --- a/packages/editor-app/src/services/ProfilerService.ts +++ b/packages/editor-app/src/services/ProfilerService.ts @@ -1,29 +1,13 @@ import { invoke } from '@tauri-apps/api/core'; import { SettingsService } from './SettingsService'; import { LogLevel } from '@esengine/ecs-framework'; - -export interface SystemPerformanceData { - name: string; - executionTime: number; - entityCount: number; - averageTime: number; - percentage: number; -} - -export interface RemoteEntity { - id: number; - name: string; - enabled: boolean; - active: boolean; - activeInHierarchy: boolean; - componentCount: number; - componentTypes: string[]; - parentId: number | null; - childIds: number[]; - depth: number; - tag: number; - updateOrder: number; -} +import type { + IProfilerService, + ProfilerData, + SystemPerformanceData, + RemoteEntity, + AdvancedProfilerDataPayload +} from './tokens'; export interface RemoteComponentDetail { typeName: string; @@ -45,29 +29,10 @@ export interface RemoteEntityDetails { parentName: string | null; } -export interface ProfilerData { - totalFrameTime: number; - systems: SystemPerformanceData[]; - entityCount: number; - componentCount: number; - fps: number; - entities?: RemoteEntity[]; -} - type ProfilerDataListener = (data: ProfilerData) => void; - -/** - * 高级性能数据结构(用于高级性能分析器) - */ -export interface AdvancedProfilerDataPayload { - advancedProfiler?: any; - performance?: any; - systems?: any; -} - type AdvancedProfilerDataListener = (data: AdvancedProfilerDataPayload) => void; -export class ProfilerService { +export class ProfilerService implements IProfilerService { private ws: WebSocket | null = null; private isServerRunning = false; private wsPort: number; diff --git a/packages/editor-app/src/services/getService.ts b/packages/editor-app/src/services/getService.ts new file mode 100644 index 00000000..5181ac68 --- /dev/null +++ b/packages/editor-app/src/services/getService.ts @@ -0,0 +1,36 @@ +/** + * 服务获取辅助函数 + * Service getter helper functions + * + * 提供类型安全的服务获取,避免直接访问全局变量。 + * Provides type-safe service access, avoiding direct global variable access. + */ + +import { Core } from '@esengine/ecs-framework'; +import type { ServiceToken } from '@esengine/engine-core'; +import { ProfilerServiceToken, type IProfilerService } from './tokens'; + +/** + * 安全获取插件服务 + * Safely get plugin service + * + * 在 Core 未初始化时返回 undefined 而非抛出异常。 + * Returns undefined instead of throwing when Core is not initialized. + */ +export function getPluginService(token: ServiceToken): T | undefined { + try { + return Core.pluginServices.get(token); + } catch { + // Core 可能还没有初始化 + // Core might not be initialized yet + return undefined; + } +} + +/** + * 获取 ProfilerService 实例 + * Get ProfilerService instance + */ +export function getProfilerService(): IProfilerService | undefined { + return getPluginService(ProfilerServiceToken); +} diff --git a/packages/editor-app/src/services/tokens.ts b/packages/editor-app/src/services/tokens.ts new file mode 100644 index 00000000..232072a6 --- /dev/null +++ b/packages/editor-app/src/services/tokens.ts @@ -0,0 +1,114 @@ +/** + * 编辑器服务令牌 + * Editor Service Tokens + * + * 遵循"谁定义接口,谁导出 Token"原则。 + * 这些服务定义在 editor-app 中,所以 Token 也在这里定义。 + * + * Following "who defines interface, who exports Token" principle. + * These services are defined in editor-app, so Tokens are also defined here. + */ + +import { createServiceToken } from '@esengine/engine-core'; + +// ============================================================================ +// Profiler Data Types (定义在这里以避免循环依赖) +// ============================================================================ + +export interface SystemPerformanceData { + name: string; + executionTime: number; + entityCount: number; + averageTime: number; + percentage: number; +} + +export interface RemoteEntity { + id: number; + name: string; + enabled: boolean; + active: boolean; + activeInHierarchy: boolean; + componentCount: number; + componentTypes: string[]; + parentId: number | null; + childIds: number[]; + depth: number; + tag: number; + updateOrder: number; +} + +export interface ProfilerData { + totalFrameTime: number; + systems: SystemPerformanceData[]; + entityCount: number; + componentCount: number; + fps: number; + entities?: RemoteEntity[]; +} + +/** + * 高级性能数据结构(用于高级性能分析器) + * Advanced profiler data structure + */ +export interface AdvancedProfilerDataPayload { + advancedProfiler?: any; + performance?: any; + systems?: any; +} + +// ============================================================================ +// Profiler Service Token +// ============================================================================ + +/** + * ProfilerService 接口(用于类型检查) + * ProfilerService interface (for type checking) + * + * 提供远程性能分析功能,包括: + * - WebSocket 连接管理 + * - 性能数据收集和分发 + * - 远程日志接收 + * + * Provides remote profiling capabilities including: + * - WebSocket connection management + * - Performance data collection and distribution + * - Remote log reception + */ +export interface IProfilerService { + /** 检查是否已连接 | Check if connected */ + isConnected(): boolean; + + /** 检查服务器是否运行 | Check if server is running */ + isServerActive(): boolean; + + /** 手动启动服务器 | Manually start server */ + manualStartServer(): Promise; + + /** 手动停止服务器 | Manually stop server */ + manualStopServer(): Promise; + + /** 订阅数据更新 | Subscribe to data updates */ + subscribe(callback: (data: ProfilerData) => void): () => void; + + /** 订阅高级数据更新 | Subscribe to advanced data updates */ + subscribeAdvanced(callback: (data: AdvancedProfilerDataPayload) => void): () => void; + + /** 请求实体详情 | Request entity details */ + requestEntityDetails(entityId: number): void; + + /** 请求高级性能分析数据 | Request advanced profiler data */ + requestAdvancedProfilerData(): void; + + /** 设置选中的函数 | Set selected function */ + setProfilerSelectedFunction(functionName: string | null): void; + + /** 销毁服务 | Destroy service */ + destroy(): void; +} + +/** + * ProfilerService 的服务令牌 + * Service token for ProfilerService + */ +export const ProfilerServiceToken = createServiceToken('profilerService'); diff --git a/packages/editor-core/src/Plugin/EditorModule.ts b/packages/editor-core/src/Plugin/EditorModule.ts index e46dc192..5ffd9ae1 100644 --- a/packages/editor-core/src/Plugin/EditorModule.ts +++ b/packages/editor-core/src/Plugin/EditorModule.ts @@ -47,6 +47,8 @@ export interface PanelDescriptor { id: string; /** 面板标题 | Panel title */ title: string; + /** 面板中文标题 | Panel title in Chinese */ + titleZh?: string; /** 面板图标 | Panel icon */ icon?: string; /** 面板位置 | Panel position */ diff --git a/packages/material-system/src/index.ts b/packages/material-system/src/index.ts index 1097e11a..e93c2a3d 100644 --- a/packages/material-system/src/index.ts +++ b/packages/material-system/src/index.ts @@ -54,3 +54,8 @@ export type { IShaderAssetData, ShaderFileFormat } from './loaders/ShaderLoader' // 运行时模块。 export { MaterialRuntimeModule, materialRuntimeModule, MaterialSystemPlugin } from './MaterialSystemPlugin'; export type { IMaterialRuntimeModule } from './MaterialSystemPlugin'; + +// Service Tokens. +// 服务令牌。 +export { MaterialManagerToken } from './tokens'; +export type { IMaterialManager } from './tokens'; diff --git a/packages/material-system/src/tokens.ts b/packages/material-system/src/tokens.ts new file mode 100644 index 00000000..ede50414 --- /dev/null +++ b/packages/material-system/src/tokens.ts @@ -0,0 +1,174 @@ +/** + * Material System Service Tokens + * 材质系统服务令牌 + * + * 遵循"谁定义接口,谁导出 Token"原则。 + * Following "who defines interface, who exports Token" principle. + */ + +import { createServiceToken } from '@esengine/engine-core'; +import type { Material } from './Material'; +import type { Shader } from './Shader'; +import type { IEngineBridge } from './MaterialManager'; +import type { IAssetManager } from '@esengine/asset-system'; + +// ============================================================================ +// Material Manager Interface +// ============================================================================ + +/** + * MaterialManager 接口 + * MaterialManager interface + * + * 提供材质和着色器管理功能。 + * Provides material and shader management functionality. + */ +export interface IMaterialManager { + // ========== Initialization | 初始化 ========== + + /** + * 设置引擎桥接 + * Set engine bridge for Rust communication + */ + setEngineBridge(bridge: IEngineBridge): void; + + /** + * 设置资产管理器 + * Set asset manager for loading assets + */ + setAssetManager(assetManager: IAssetManager): void; + + /** + * 初始化内置材质 + * Initialize built-in materials + */ + initializeBuiltInMaterials(): Promise; + + // ========== Shader Management | 着色器管理 ========== + + /** + * 注册着色器 + * Register a shader + */ + registerShader(shader: Shader): Promise; + + /** + * 通过 ID 获取着色器 + * Get shader by ID + */ + getShader(id: number): Shader | undefined; + + /** + * 通过名称获取着色器 + * Get shader by name + */ + getShaderByName(name: string): Shader | undefined; + + /** + * 移除着色器 + * Remove a shader + */ + removeShader(id: number): boolean; + + /** + * 从路径加载着色器 + * Load shader from path + */ + loadShaderByPath(path: string): Promise; + + // ========== Material Management | 材质管理 ========== + + /** + * 注册材质 + * Register a material + */ + registerMaterial(material: Material): Promise; + + /** + * 通过 ID 获取材质 + * Get material by ID + */ + getMaterial(id: number): Material | undefined; + + /** + * 通过名称获取材质 + * Get material by name + */ + getMaterialByName(name: string): Material | undefined; + + /** + * 移除材质 + * Remove a material + */ + removeMaterial(id: number): boolean; + + /** + * 从路径加载材质 + * Load material from path + */ + loadMaterialByPath(path: string): Promise; + + /** + * 克隆材质 + * Clone a material + */ + cloneMaterial(materialId: number, newName?: string): Promise; + + // ========== Built-in Materials | 内置材质 ========== + + /** + * 获取默认材质 ID + * Get default material ID + */ + getDefaultMaterialId(): number; + + /** + * 获取灰度材质 ID + * Get grayscale material ID + */ + getGrayscaleMaterialId(): number; + + /** + * 获取着色材质 ID + * Get tint material ID + */ + getTintMaterialId(): number; + + /** + * 获取闪烁材质 ID + * Get flash material ID + */ + getFlashMaterialId(): number; + + /** + * 获取轮廓材质 ID + * Get outline material ID + */ + getOutlineMaterialId(): number; + + // ========== Uniform Management | Uniform 管理 ========== + + /** + * 设置材质 uniform 值 + * Set material uniform value + */ + setMaterialUniform(materialId: number, name: string, value: any): boolean; + + // ========== Lifecycle | 生命周期 ========== + + /** + * 销毁管理器,释放所有资源 + * Destroy manager and release all resources + */ + destroy(): void; +} + +// ============================================================================ +// Service Token +// ============================================================================ + +/** + * MaterialManager 服务令牌 + * MaterialManager service token + */ +export const MaterialManagerToken = createServiceToken('materialManager'); diff --git a/packages/physics-rapier2d/src/PhysicsRuntimeModule.ts b/packages/physics-rapier2d/src/PhysicsRuntimeModule.ts index 4950ec3e..693570c6 100644 --- a/packages/physics-rapier2d/src/PhysicsRuntimeModule.ts +++ b/packages/physics-rapier2d/src/PhysicsRuntimeModule.ts @@ -22,9 +22,11 @@ import { Physics2DSystemToken, Physics2DWorldToken, PhysicsConfigToken, + CollisionLayerConfigToken, type IPhysics2DQuery, type PhysicsConfig } from './tokens'; +import { CollisionLayerConfig } from './services/CollisionLayerConfig'; // 注册 Rapier2D 加载器 import './loaders'; @@ -35,6 +37,7 @@ export { Physics2DSystemToken, Physics2DWorldToken, PhysicsConfigToken, + CollisionLayerConfigToken, type IPhysics2DQuery, type PhysicsConfig } from './tokens'; @@ -144,6 +147,7 @@ class PhysicsRuntimeModule implements IRuntimeModule { context.services.register(Physics2DSystemToken, physicsSystem); context.services.register(Physics2DWorldToken, physicsSystem.world); context.services.register(Physics2DQueryToken, physicsSystem); + context.services.register(CollisionLayerConfigToken, CollisionLayerConfig.getInstance()); } /** diff --git a/packages/physics-rapier2d/src/index.ts b/packages/physics-rapier2d/src/index.ts index d8e9aa91..231d759e 100644 --- a/packages/physics-rapier2d/src/index.ts +++ b/packages/physics-rapier2d/src/index.ts @@ -34,7 +34,9 @@ export { Physics2DSystemToken, Physics2DWorldToken, PhysicsConfigToken, + CollisionLayerConfigToken, type IPhysics2DQuery, type IPhysics2DWorld, + type ICollisionLayerConfig, type PhysicsConfig } from './tokens'; diff --git a/packages/physics-rapier2d/src/tokens.ts b/packages/physics-rapier2d/src/tokens.ts index 84a71f0c..a8f04150 100644 --- a/packages/physics-rapier2d/src/tokens.ts +++ b/packages/physics-rapier2d/src/tokens.ts @@ -136,6 +136,33 @@ export interface PhysicsConfig { timestep?: number; } +/** + * 碰撞层配置接口 + * Collision layer config interface + * + * 跨模块共享的碰撞层配置契约。 + * Cross-module shared collision layer config contract. + */ +export interface ICollisionLayerConfig { + /** + * 获取所有层定义 + * Get all layer definitions + */ + getLayers(): ReadonlyArray<{ name: string }>; + + /** + * 添加监听器 + * Add listener + */ + addListener(callback: () => void): void; + + /** + * 移除监听器 + * Remove listener + */ + removeListener(callback: () => void): void; +} + // ============================================================================ // 服务令牌 | Service Tokens // ============================================================================ @@ -175,3 +202,12 @@ export const Physics2DSystemToken = createServiceToken('physics * For passing physics configuration (gravity, timestep, etc.). */ export const PhysicsConfigToken = createServiceToken('physicsConfig'); + +/** + * 碰撞层配置令牌 + * Collision layer config token + * + * 用于获取碰撞层配置服务。 + * For getting collision layer config service. + */ +export const CollisionLayerConfigToken = createServiceToken('collisionLayerConfig');