refactor(arch): 改进 ServiceToken 设计,统一服务获取模式 (#300)

* refactor(arch): 移除全局变量,使用 ServiceToken 模式

- 创建 PluginServiceRegistry 类,提供类型安全的服务注册/获取
- 添加 ProfilerServiceToken 和 CollisionLayerConfigToken
- 重构所有 __PROFILER_SERVICE__ 全局变量访问为 getProfilerService()
- 重构 __PHYSICS_RAPIER2D__ 全局变量访问为 CollisionLayerConfigToken
- 在 Core 类添加 pluginServices 静态属性
- 添加 getService.ts 辅助模块简化服务获取

这是 ServiceToken 模式重构的第一阶段,移除了最常用的两个全局变量。
后续可继续应用到其他模块(Camera/Audio 等)。

* refactor(arch): 改进 ServiceToken 设计,移除重复常量

- tokens.ts: 从 engine-core 导入 createServiceToken(符合规范)
- tokens.ts: Token 使用接口 IProfilerService 而非具体类
- 移除 AssetPickerDialog 和 ContentBrowser 中重复的 MANAGED_ASSET_DIRECTORIES
- 统一从 editor-core 导入 MANAGED_ASSET_DIRECTORIES

* fix(type): 修复 IProfilerService 接口与实现类型不匹配

- 将 ProfilerData 等数据类型移到 tokens.ts 以避免循环依赖
- ProfilerService 显式实现 IProfilerService 接口
- 更新使用方使用 IProfilerService 接口类型而非具体类

* refactor(type): 移除类型重导出,改进类型安全

- 删除 ProfilerService.ts 中的类型重导出,消费方直接从 tokens.ts 导入
- PanelDescriptor 接口添加 titleZh 属性,移除 App.tsx 中的 as any
- 改进 useDynamicIcon.ts 的类型安全,使用正确的 Record 类型

* refactor(arch): 为模块添加 ServiceToken 支持

- Material System: 创建 tokens.ts,定义 IMaterialManager 接口和 MaterialManagerToken
- Audio: 创建预留 tokens.ts 文件,为未来 AudioManager 服务扩展做准备
- Camera: 创建预留 tokens.ts 文件,为未来 CameraManager 服务扩展做准备

遵循"谁定义接口,谁导出 Token"原则,统一服务访问模式
This commit is contained in:
YHH
2025-12-09 11:07:44 +08:00
committed by GitHub
parent c71a47f2b0
commit 995fa2d514
31 changed files with 1024 additions and 210 deletions

View File

@@ -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;
console.log('====================================');
console.log('ESEngine Worker 物理演示');
console.log('Canvas:', canvas.width, 'x', canvas.height);
console.log('====================================');
// ============ 初始化实体 | 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: 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
});
console.log('Worker 创建成功!');
} catch (error) {
console.error('Worker 创建失败:', error.message);
}
if (worker) {
// 设置消息处理
worker.onMessage(function(res) {
console.log('\\n[4] 收到 Worker 响应!');
pendingRequest = false;
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.error('Worker error:', res.error);
return;
}
console.log('\\n========== Worker 测试成功! ==========');
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;
}
}
}
});
// 创建测试数据
console.log('\\n[3] 发送测试数据...');
workerReady = true;
console.log('Worker created successfully!');
} catch (error) {
console.error('Worker creation failed:', error.message);
workerReady = false;
}
}
var entities = [];
for (var i = 0; i < 10; i++) {
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
// ============ 发送物理更新到 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);
// 绘制地面
// Draw ground
ctx.fillStyle = '#2d3436';
ctx.fillRect(0, canvas.height - 10, canvas.width, 10);
// 绘制所有球
// Draw all balls
for (var i = 0; i < entities.length; i++) {
var e = entities[i];
// 球体渐变效果
// 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.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 = '18px Arial';
ctx.textAlign = 'center';
ctx.fillText('ESEngine Worker 测试', canvas.width / 2, 50);
ctx.font = '14px Arial';
ctx.fillStyle = '#aaaaaa';
ctx.fillText('查看控制台日志', canvas.width / 2, 80);
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);
ctx.fillStyle = worker ? '#00ff00' : '#ff0000';
ctx.fillText('Worker: ' + (worker ? '已创建' : '创建失败'), canvas.width / 2, 110);
// 提示文字
// 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');

View File

@@ -0,0 +1,6 @@
{
"generatedAt": "2025-12-08T10:33:50.647Z",
"mappings": {
"PhysicsWorkerSystem": "workers/physics-worker.js"
}
}

View File

@@ -1,2 +1,6 @@
export { AudioSourceComponent } from './AudioSourceComponent';
export { AudioPlugin } from './AudioPlugin';
// Service Tokens (reserved for future use)
// 服务令牌(预留用于未来扩展)
// export { AudioManagerToken, type IAudioManager } from './tokens';

View File

@@ -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<IAudioManager>('audioManager');

View File

@@ -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';

View File

@@ -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<ICameraManager>('cameraManager');

View File

@@ -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<IMyService>('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管理器
*

View File

@@ -0,0 +1,135 @@
/**
* 插件服务注册表
* Plugin Service Registry
*
* 基于 ServiceToken 的类型安全服务注册表。
* Type-safe service registry based on ServiceToken.
*
* 设计原则 | Design principles:
* 1. 类型安全 - 使用 ServiceToken 携带类型信息
* 2. 显式依赖 - 通过导入 token 明确表达依赖关系
* 3. 可选依赖 - get 返回 undefinedrequire 抛异常
* 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<T> {
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<T>(name: string): ServiceToken<T> {
// __phantom 仅用于类型推断,运行时不需要实际值
// __phantom is only for type inference, no actual value needed at runtime
return {
id: Symbol(name),
name
} as ServiceToken<T>;
}
// ============================================================================
// 插件服务注册表 | Plugin Service Registry
// ============================================================================
/**
* 插件服务注册表
* Plugin service registry
*
* 用于跨插件共享服务的类型安全注册表。
* Type-safe registry for sharing services between plugins.
*/
export class PluginServiceRegistry {
private _services = new Map<symbol, unknown>();
/**
* 注册服务
* Register a service
*/
register<T>(token: ServiceToken<T>, service: T): void {
this._services.set(token.id, service);
}
/**
* 获取服务(可选)
* Get a service (optional)
*/
get<T>(token: ServiceToken<T>): T | undefined {
return this._services.get(token.id) as T | undefined;
}
/**
* 获取服务(必需)
* Get a service (required)
*
* @throws 如果服务未注册 | If service is not registered
*/
require<T>(token: ServiceToken<T>): 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<T>(token: ServiceToken<T>): boolean {
return this._services.has(token.id);
}
/**
* 注销服务
* Unregister a service
*/
unregister<T>(token: ServiceToken<T>): 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();
}
}

View File

@@ -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';

View File

@@ -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: <Component key={`${panelDesc.id}-${pluginUpdateTrigger}`} projectPath={currentProjectPath} />,
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;

View File

@@ -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,20 +9,20 @@ interface AdvancedProfilerWindowProps {
onClose: () => void;
}
interface WindowWithProfiler extends Window {
__PROFILER_SERVICE__?: ProfilerService;
}
export function AdvancedProfilerWindow({ onClose }: AdvancedProfilerWindowProps) {
const [profilerService, setProfilerService] = useState<ProfilerService | null>(null);
const [profilerService, setProfilerService] = useState<IProfilerService | null>(null);
const [isConnected, setIsConnected] = useState(false);
const [isFullscreen, setIsFullscreen] = useState(false);
useEffect(() => {
const service = (window as WindowWithProfiler).__PROFILER_SERVICE__;
try {
const service = Core.pluginServices.get(ProfilerServiceToken);
if (service) {
setProfilerService(service);
}
} catch {
// Core 可能还没有初始化
}
}, []);
useEffect(() => {

View File

@@ -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;

View File

@@ -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));

View File

@@ -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');

View File

@@ -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');

View File

@@ -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);
}

View File

@@ -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;

View File

@@ -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 {

View File

@@ -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;
/**
* 获取 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 getProfilerService(): ProfilerService | undefined {
return (window as any).__PROFILER_SERVICE__;
}
export function isComponentData(value: unknown): value is ComponentData {

View File

@@ -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]);

View File

@@ -1,24 +1,53 @@
/**
* 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;
/**
* 获取 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<IProfilerService | undefined>(() => {
try {
return Core.pluginServices.get(ProfilerServiceToken);
} catch {
// Core 可能还没有初始化
// Core might not be initialized yet
return undefined;
}
export function useProfilerService(): ProfilerService | undefined {
const [service, setService] = useState<ProfilerService | undefined>(() => {
return (window as any).__PROFILER_SERVICE__;
});
useEffect(() => {
// 定期检查服务是否可用(处理服务延迟注册的情况)
// Periodically check if service is available (handles delayed service registration)
const checkService = () => {
const newService = (window as any).__PROFILER_SERVICE__;
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);
}
}
};
const interval = setInterval(checkService, 1000);

View File

@@ -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<void> {
// 从服务注册表注销
// 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[] {

View File

@@ -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;

View File

@@ -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<T>(token: ServiceToken<T>): 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);
}

View File

@@ -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<void>;
/** 手动停止服务器 | Manually stop server */
manualStopServer(): Promise<void>;
/** 订阅数据更新 | 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<IProfilerService>('profilerService');

View File

@@ -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 */

View File

@@ -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';

View File

@@ -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<void>;
// ========== Shader Management | 着色器管理 ==========
/**
* 注册着色器
* Register a shader
*/
registerShader(shader: Shader): Promise<number>;
/**
* 通过 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<number>;
// ========== Material Management | 材质管理 ==========
/**
* 注册材质
* Register a material
*/
registerMaterial(material: Material): Promise<number>;
/**
* 通过 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<number>;
/**
* 克隆材质
* Clone a material
*/
cloneMaterial(materialId: number, newName?: string): Promise<Material | null>;
// ========== 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<IMaterialManager>('materialManager');

View File

@@ -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());
}
/**

View File

@@ -34,7 +34,9 @@ export {
Physics2DSystemToken,
Physics2DWorldToken,
PhysicsConfigToken,
CollisionLayerConfigToken,
type IPhysics2DQuery,
type IPhysics2DWorld,
type ICollisionLayerConfig,
type PhysicsConfig
} from './tokens';

View File

@@ -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<Physics2DSystem>('physics
* For passing physics configuration (gravity, timestep, etc.).
*/
export const PhysicsConfigToken = createServiceToken<PhysicsConfig>('physicsConfig');
/**
* 碰撞层配置令牌
* Collision layer config token
*
* 用于获取碰撞层配置服务。
* For getting collision layer config service.
*/
export const CollisionLayerConfigToken = createServiceToken<ICollisionLayerConfig>('collisionLayerConfig');