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

@@ -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,19 +9,19 @@ 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__;
if (service) {
setProfilerService(service);
try {
const service = Core.pluginServices.get(ProfilerServiceToken);
if (service) {
setProfilerService(service);
}
} catch {
// Core 可能还没有初始化
}
}, []);

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;
}
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 {

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,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<ProfilerService | undefined>(() => {
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<IProfilerService | undefined>(() => {
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);
}
}
};

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