refactor: 代码规范化与依赖清理 (#317)

* refactor(deps): 统一编辑器包依赖配置 & 优化分层架构

- 将 ecs-engine-bindgen 提升为 Layer 1 核心包
- 统一 9 个编辑器包的依赖声明模式
- 清理废弃的包目录 (ui, ui-editor, network-*)

* refactor(tokens): 修复 PrefabService 令牌冲突 & 补充 module.json

- 将 editor-core 的 PrefabServiceToken 改名为 EditorPrefabServiceToken
  避免与 asset-system 的 PrefabServiceToken 冲突 (Symbol.for 冲突)
- 为 mesh-3d 添加 module.json
- 为 world-streaming 添加 module.json

* refactor(editor-core): 整理导出结构 & 添加 blueprint tokens.ts

- 按功能分组整理 editor-core 的 65 行导出
- 添加清晰的分组注释 (中英双语)
- 为 blueprint 添加占位符 tokens.ts

* chore(editor): 为 14 个编辑器插件包添加 module.json

统一编辑器包的模块配置,包含:
- isEditorPlugin 标识
- runtimeModule 关联
- exports 导出清单 (inspectors, panels, gizmos)

* refactor(core): 改进类型安全 - 减少 as any 使用

- 添加 GlobalTypes.ts 定义小游戏平台和 Chrome API 类型
- SoAStorage 使用 IComponentTypeMetadata 替代 as any
- PlatformDetector 使用类型安全的平台检测
- 添加 ISoAStorageStats/ISoAFieldStats 接口

* feat(editor): 添加 EditorServicesContext 解决 prop drilling

- 新增 contexts/EditorServicesContext.tsx 提供统一服务访问
- App.tsx 包裹 EditorServicesProvider
- 提供 useEditorServices/useMessageHub 等便捷 hooks
- SceneHierarchy 添加迁移注释,后续可移除 props

* docs(editor): 澄清 inspector 目录架构关系

- inspector/ 标记为内部实现,添加 @deprecated 警告
- inspectors/ 标记为公共 API 入口点
- 添加架构说明文档

* refactor(editor): 添加全局类型声明消除 window as any

- 创建 editor-app/src/global.d.ts 声明 Window 接口扩展
- 创建 editor-core/src/global.d.ts 声明 Window 接口扩展
- 更新 App.tsx 使用类型安全的 window 属性访问
- 更新 PluginLoader.ts 使用 window.__ESENGINE_PLUGINS__
- 更新 PluginSDKRegistry.ts 使用 window.__ESENGINE_SDK__
- 更新 UserCodeService.ts 使用类型安全的全局变量访问

* refactor(editor): 提取项目和场景操作到独立 hooks

- 创建 useProjectActions hook 封装项目操作
- 创建 useSceneActions hook 封装场景操作
- 为渐进式重构 App.tsx 做准备

* refactor(editor): 清理冗余代码和未使用文件

删除的目录和文件:
- application/state/ - 重复的状态管理(与 stores/ 重复)
- 8 个孤立 CSS 文件(对应组件不存在)
- AssetBrowser.tsx - 仅为 ContentBrowser 的向后兼容包装
- AssetPicker.tsx - 未被使用
- AssetPickerDialog.tsx (顶级) - 已被 dialogs/ 版本取代
- EntityInspector.tsx (顶级) - 已被 inspectors/views/ 版本取代

修复:
- 移除 App.tsx 中未使用的导入
- 更新 application/index.ts 移除已删除模块
- 修复 useProjectActions.ts 的 MutableRefObject 类型

* refactor(editor): 统一 inspectors 模块导出结构

- 在 inspectors/index.ts 重新导出 PropertyInspector
- 创建 inspectors/fields/index.ts barrel export
- 导出 views、fields、common 子模块
- 更新 EntityInspector 使用统一入口导入

* refactor(editor): 删除废弃的 Profiler 组件

删除未使用的组件(共 1059 行):
- ProfilerPanel.tsx (229 行)
- ProfilerWindow.tsx (589 行)
- ProfilerDockPanel.tsx (241 行)
- ProfilerPanel.css
- ProfilerDockPanel.css

保留:AdvancedProfiler + AdvancedProfilerWindow(正在使用)

* refactor(runtime-core): 统一依赖处理与插件状态管理

- 新增 DependencyUtils 统一拓扑排序和依赖验证
- 新增 PluginState 定义插件生命周期状态机
- 合并 UnifiedPluginLoader 到 PluginLoader
- 清理 index.ts 移除不必要的 Token re-exports
- 新增 RuntimeMode/UserCodeRealm/ImportMapGenerator

* refactor(editor-core): 使用统一的 ImportMapGenerator

- WebBuildPipeline 使用 runtime-core 的 generateImportMap
- UserCodeService 添加 ImportMap 相关接口

* feat(compiler): 增强 esbuild 查找策略

- 支持本地 node_modules、pnpm exec、npx、全局多种来源
- EngineService 使用 RuntimeMode

* refactor(runtime-core): 简化 GameRuntime 代码

- 合并 _disableGameLogicSystems/_enableGameLogicSystems 为 _setGameLogicSystemsEnabled
- 精简本地 Token 定义的注释

* refactor(editor-core): 引入 BaseRegistry 基类消除代码重复

- 新增 BaseRegistry 和 PrioritizedRegistry 基类
- 重构 CompilerRegistry, InspectorRegistry, FieldEditorRegistry
- 统一注册表的日志记录和错误处理

* refactor(editor-core): 扩展 BaseRegistry 重构

- ComponentInspectorRegistry 继承 PrioritizedRegistry
- EditorComponentRegistry 继承 BaseRegistry
- EntityCreationRegistry 继承 BaseRegistry
- PropertyRendererRegistry 继承 PrioritizedRegistry
- 导出 BaseRegistry 基类供外部使用
- 统一双语注释格式

* refactor(editor-core): 代码优雅性优化

CommandManager:
- 提取 tryMergeWithLast() 和 pushToUndoStack() 消除重复代码
- 统一双语注释格式

FileActionRegistry:
- 提取 normalizeExtension() 消除扩展名规范化重复
- 统一私有属性命名风格(_前缀)
- 使用 createRegistryToken 统一 Token 创建

BaseRegistry:
- 添加 IOrdered 接口
- 添加 sortByOrder() 排序辅助方法

EntityCreationRegistry:
- 使用 sortByOrder() 简化排序逻辑

* refactor(editor-core): 统一日志系统 & 代码规范优化

- GizmoRegistry: 使用 createLogger 替代 console.warn
- VirtualNodeRegistry: 使用 createLogger 替代 console.warn
- WindowRegistry: 使用 logger、添加 _ 前缀、导出 IWindowRegistry token
- EditorViewportService: 使用 createLogger 替代 console.warn
- ComponentActionRegistry: 使用 logger、添加 _ 前缀、返回值改进
- SettingsRegistry: 使用 logger、提取 ensureCategory/ensureSection 方法
- 添加 WindowRegistry 到主导出

* refactor(editor-core): ModuleRegistry 使用 logger 替代 console

* refactor(editor-core): SerializerRegistry/UIRegistry 添加 token 和 _ 前缀

* refactor(editor-core): UIRegistry 代码优雅性 & Token 命名统一

- UIRegistry: 提取 _sortByOrder 消除 6 处重复排序逻辑
- UIRegistry: 添加分节注释和双语文档
- FieldEditorRegistry: Token 重命名为 FieldEditorRegistryToken
- PropertyRendererRegistry: Token 重命名为 PropertyRendererRegistryToken

* refactor(core): 统一日志系统 - console 替换为 logger

- ComponentSerializer: 使用 logger 替代 console.warn
- ComponentRegistry: console.warn → logger.warn (已有 logger)
- SceneSerializer: 添加 logger,替换 console.warn/error
- SystemScheduler: 添加 logger,替换 console.warn
- VersionMigration: 添加 logger,替换所有 console.warn
- RuntimeModeService: console.error → logger.error
- Core.ts: _logger 改为 readonly,双语错误消息
- SceneSerializer 修复:使用 getComponentTypeName 替代 constructor.name

* fix(core): 修复 constructor.name 压缩后失效问题

- Scene.ts: 使用 system.systemName 替代 system.constructor.name
- CommandBuffer.ts: 使用 getComponentTypeName() 替代 constructor.name

* refactor(editor-core): 代码规范优化 - 私有方法命名 & 日志统一

- BuildService: console → logger
- FileActionRegistry: 添加 logger, 私有方法 _ 前缀
- SettingsRegistry: 私有方法 _ 前缀 (ensureCategory → _ensureCategory)

* refactor(core): Scene.ts 私有变量命名规范化

- logger → _logger (遵循私有变量 _ 前缀规范)

* refactor(editor-core): 服务类私有成员命名规范化

- CommandManager: 私有变量/方法添加 _ 前缀
  - undoStack/redoStack/config/isExecuting
  - tryMergeWithLast/pushToUndoStack
- LocaleService: 私有变量/方法添加 _ 前缀
  - currentLocale/translations/changeListeners
  - deepMerge/getNestedValue/loadSavedLocale/saveLocale

* refactor(core): 私有成员命名规范化 & 单例模式优化

- Component.ts: _idGenerator 私有静态变量规范化
- PlatformManager.ts: _instance, _adapter, _logger 规范化
- AutoProfiler.ts: _instance, _config 及所有私有方法规范化
- ProfilerSDK.ts: _instance, _config 及所有私有方法规范化
- ComponentPoolManager: _instance, _pools, _usageTracker 规范化
- GlobalEventBus: _instance 规范化
- 添加中英双语 JSDoc 注释

* refactor(editor-app,behavior-tree-editor): 私有成员 & 单例模式命名规范化

editor-app:
- EngineService: private static instance → _instance
- EditorEngineSync: 所有私有成员添加 _ 前缀
- RuntimeResolver: 所有私有成员和方法添加 _ 前缀
- SettingsService: 所有私有成员和方法添加 _ 前缀

behavior-tree-editor:
- GlobalBlackboardService: 所有私有成员和方法添加 _ 前缀
- NotificationService: private static instance → _instance
- NodeRegistryService: 所有私有成员和方法添加 _ 前缀
- TreeStateAdapter: private static instance → _instance

* fix(editor-runtime): 添加 editor-core 到 external 避免传递依赖问题

将 @esengine/editor-core 添加到 vite external 配置,
避免 editor-core → runtime-core → ecs-engine-bindgen 的传递依赖
被错误地打包进 editor-runtime.js,导致 CI 构建失败。

* fix(core): 修复空接口 lint 错误

将 IByteDanceMiniGameAPI、IAlipayMiniGameAPI、IBaiduMiniGameAPI 从空接口改为类型别名,修复 no-empty-object-type 规则报错
This commit is contained in:
YHH
2025-12-24 20:57:08 +08:00
committed by GitHub
parent 58f70a5783
commit dbc6793dc4
133 changed files with 6880 additions and 9141 deletions
+14
View File
@@ -0,0 +1,14 @@
{
"id": "asset-system-editor",
"name": "@esengine/asset-system-editor",
"displayName": "Asset System Editor",
"description": "Asset packing and bundling tools | 资产打包工具",
"version": "1.0.0",
"category": "Editor",
"icon": "Package",
"isEditorPlugin": true,
"runtimeModule": "@esengine/asset-system",
"exports": {
"services": ["AssetPacker", "AssetBundler"]
}
}
+15
View File
@@ -0,0 +1,15 @@
{
"id": "behavior-tree-editor",
"name": "@esengine/behavior-tree-editor",
"displayName": "Behavior Tree Editor",
"description": "Visual behavior tree editor | 可视化行为树编辑器",
"version": "1.0.0",
"category": "Editor",
"icon": "GitBranch",
"isEditorPlugin": true,
"runtimeModule": "@esengine/behavior-tree",
"exports": {
"inspectors": ["BehaviorTreeComponentInspector"],
"panels": ["BehaviorTreeEditorPanel"]
}
}
@@ -24,6 +24,9 @@
"dependencies": {
"@esengine/behavior-tree": "workspace:*"
},
"peerDependencies": {
"@esengine/editor-core": "workspace:*"
},
"devDependencies": {
"@esengine/ecs-framework": "workspace:*",
"@esengine/engine-core": "workspace:*",
@@ -24,64 +24,64 @@ export interface GlobalBlackboardVariable {
* 管理跨行为树共享的全局变量
*/
export class GlobalBlackboardService {
private static instance: GlobalBlackboardService;
private variables: Map<string, GlobalBlackboardVariable> = new Map();
private changeCallbacks: Array<() => void> = [];
private projectPath: string | null = null;
private static _instance: GlobalBlackboardService;
private _variables: Map<string, GlobalBlackboardVariable> = new Map();
private _changeCallbacks: Array<() => void> = [];
private _projectPath: string | null = null;
private constructor() {}
static getInstance(): GlobalBlackboardService {
if (!this.instance) {
this.instance = new GlobalBlackboardService();
if (!this._instance) {
this._instance = new GlobalBlackboardService();
}
return this.instance;
return this._instance;
}
/**
* 设置项目路径
*/
setProjectPath(path: string | null): void {
this.projectPath = path;
this._projectPath = path;
}
/**
* 获取项目路径
*/
getProjectPath(): string | null {
return this.projectPath;
return this._projectPath;
}
/**
* 添加全局变量
*/
addVariable(variable: GlobalBlackboardVariable): void {
if (this.variables.has(variable.key)) {
if (this._variables.has(variable.key)) {
throw new Error(`全局变量 "${variable.key}" 已存在`);
}
this.variables.set(variable.key, variable);
this.notifyChange();
this._variables.set(variable.key, variable);
this._notifyChange();
}
/**
* 更新全局变量
*/
updateVariable(key: string, updates: Partial<Omit<GlobalBlackboardVariable, 'key'>>): void {
const variable = this.variables.get(key);
const variable = this._variables.get(key);
if (!variable) {
throw new Error(`全局变量 "${key}" 不存在`);
}
this.variables.set(key, { ...variable, ...updates });
this.notifyChange();
this._variables.set(key, { ...variable, ...updates });
this._notifyChange();
}
/**
* 删除全局变量
*/
deleteVariable(key: string): boolean {
const result = this.variables.delete(key);
const result = this._variables.delete(key);
if (result) {
this.notifyChange();
this._notifyChange();
}
return result;
}
@@ -90,36 +90,36 @@ export class GlobalBlackboardService {
* 重命名全局变量
*/
renameVariable(oldKey: string, newKey: string): void {
if (!this.variables.has(oldKey)) {
if (!this._variables.has(oldKey)) {
throw new Error(`全局变量 "${oldKey}" 不存在`);
}
if (this.variables.has(newKey)) {
if (this._variables.has(newKey)) {
throw new Error(`全局变量 "${newKey}" 已存在`);
}
const variable = this.variables.get(oldKey)!;
this.variables.delete(oldKey);
this.variables.set(newKey, { ...variable, key: newKey });
this.notifyChange();
const variable = this._variables.get(oldKey)!;
this._variables.delete(oldKey);
this._variables.set(newKey, { ...variable, key: newKey });
this._notifyChange();
}
/**
* 获取全局变量
*/
getVariable(key: string): GlobalBlackboardVariable | undefined {
return this.variables.get(key);
return this._variables.get(key);
}
/**
* 获取所有全局变量
*/
getAllVariables(): GlobalBlackboardVariable[] {
return Array.from(this.variables.values());
return Array.from(this._variables.values());
}
getVariablesMap(): Record<string, GlobalBlackboardValue> {
const map: Record<string, GlobalBlackboardValue> = {};
for (const [, variable] of this.variables) {
for (const [, variable] of this._variables) {
map[variable.key] = variable.defaultValue;
}
return map;
@@ -129,15 +129,15 @@ export class GlobalBlackboardService {
* 检查变量是否存在
*/
hasVariable(key: string): boolean {
return this.variables.has(key);
return this._variables.has(key);
}
/**
* 清空所有变量
*/
clear(): void {
this.variables.clear();
this.notifyChange();
this._variables.clear();
this._notifyChange();
}
/**
@@ -146,7 +146,7 @@ export class GlobalBlackboardService {
toConfig(): GlobalBlackboardConfig {
const variables: BlackboardVariable[] = [];
for (const variable of this.variables.values()) {
for (const variable of this._variables.values()) {
variables.push({
name: variable.key,
type: variable.type,
@@ -162,11 +162,11 @@ export class GlobalBlackboardService {
* 从配置导入
*/
fromConfig(config: GlobalBlackboardConfig): void {
this.variables.clear();
this._variables.clear();
if (config.variables && Array.isArray(config.variables)) {
for (const variable of config.variables) {
this.variables.set(variable.name, {
this._variables.set(variable.name, {
key: variable.name,
type: variable.type,
defaultValue: variable.value as GlobalBlackboardValue,
@@ -175,7 +175,7 @@ export class GlobalBlackboardService {
}
}
this.notifyChange();
this._notifyChange();
}
/**
@@ -202,17 +202,17 @@ export class GlobalBlackboardService {
* 监听变化
*/
onChange(callback: () => void): () => void {
this.changeCallbacks.push(callback);
this._changeCallbacks.push(callback);
return () => {
const index = this.changeCallbacks.indexOf(callback);
const index = this._changeCallbacks.indexOf(callback);
if (index > -1) {
this.changeCallbacks.splice(index, 1);
this._changeCallbacks.splice(index, 1);
}
};
}
private notifyChange(): void {
this.changeCallbacks.forEach((cb) => {
private _notifyChange(): void {
this._changeCallbacks.forEach((cb) => {
try {
cb();
} catch (error) {
@@ -536,15 +536,15 @@ export const useBehaviorTreeDataStore = create<BehaviorTreeDataState>((set, get)
* 将 Zustand Store 适配为 ITreeState 接口
*/
export class TreeStateAdapter implements ITreeState {
private static instance: TreeStateAdapter | null = null;
private static _instance: TreeStateAdapter | null = null;
private constructor() {}
static getInstance(): TreeStateAdapter {
if (!TreeStateAdapter.instance) {
TreeStateAdapter.instance = new TreeStateAdapter();
if (!TreeStateAdapter._instance) {
TreeStateAdapter._instance = new TreeStateAdapter();
}
return TreeStateAdapter.instance;
return TreeStateAdapter._instance;
}
getTree(): BehaviorTree {
@@ -36,63 +36,63 @@ export interface NodePropertyConfig {
* 提供编辑器级别的节点注册和管理功能
*/
export class NodeRegistryService {
private static instance: NodeRegistryService;
private customTemplates: Map<string, NodeTemplate> = new Map();
private registrationCallbacks: Array<(template: NodeTemplate) => void> = [];
private static _instance: NodeRegistryService;
private _customTemplates: Map<string, NodeTemplate> = new Map();
private _registrationCallbacks: Array<(template: NodeTemplate) => void> = [];
private constructor() {}
static getInstance(): NodeRegistryService {
if (!this.instance) {
this.instance = new NodeRegistryService();
if (!this._instance) {
this._instance = new NodeRegistryService();
}
return this.instance;
return this._instance;
}
/**
* 注册自定义节点类型
*/
registerNode(config: NodeRegistrationConfig): void {
const nodeType = this.mapStringToNodeType(config.type);
const nodeType = this._mapStringToNodeType(config.type);
const metadata: NodeMetadata = {
implementationType: config.implementationType,
nodeType: nodeType,
displayName: config.displayName,
description: config.description || '',
category: config.category || this.getDefaultCategory(config.type),
configSchema: this.convertPropertiesToSchema(config.properties || []),
childrenConstraints: this.getChildrenConstraints(config)
category: config.category || this._getDefaultCategory(config.type),
configSchema: this._convertPropertiesToSchema(config.properties || []),
childrenConstraints: this._getChildrenConstraints(config)
};
class DummyExecutor {}
NodeMetadataRegistry.register(DummyExecutor, metadata);
const template = this.createTemplate(config, metadata);
this.customTemplates.set(config.implementationType, template);
const template = this._createTemplate(config, metadata);
this._customTemplates.set(config.implementationType, template);
this.registrationCallbacks.forEach((cb) => cb(template));
this._registrationCallbacks.forEach((cb) => cb(template));
}
/**
* 注销节点类型
*/
unregisterNode(implementationType: string): boolean {
return this.customTemplates.delete(implementationType);
return this._customTemplates.delete(implementationType);
}
/**
* 获取所有自定义模板
*/
getCustomTemplates(): NodeTemplate[] {
return Array.from(this.customTemplates.values());
return Array.from(this._customTemplates.values());
}
/**
* 检查节点类型是否已注册
*/
hasNode(implementationType: string): boolean {
return this.customTemplates.has(implementationType) ||
return this._customTemplates.has(implementationType) ||
NodeMetadataRegistry.getMetadata(implementationType) !== undefined;
}
@@ -100,16 +100,16 @@ export class NodeRegistryService {
* 监听节点注册事件
*/
onNodeRegistered(callback: (template: NodeTemplate) => void): () => void {
this.registrationCallbacks.push(callback);
this._registrationCallbacks.push(callback);
return () => {
const index = this.registrationCallbacks.indexOf(callback);
const index = this._registrationCallbacks.indexOf(callback);
if (index > -1) {
this.registrationCallbacks.splice(index, 1);
this._registrationCallbacks.splice(index, 1);
}
};
}
private mapStringToNodeType(type: string): NodeType {
private _mapStringToNodeType(type: string): NodeType {
switch (type) {
case 'composite': return NodeType.Composite;
case 'decorator': return NodeType.Decorator;
@@ -119,7 +119,7 @@ export class NodeRegistryService {
}
}
private getDefaultCategory(type: string): string {
private _getDefaultCategory(type: string): string {
switch (type) {
case 'composite': return '组合';
case 'decorator': return '装饰器';
@@ -129,12 +129,12 @@ export class NodeRegistryService {
}
}
private convertPropertiesToSchema(properties: NodePropertyConfig[]): Record<string, any> {
private _convertPropertiesToSchema(properties: NodePropertyConfig[]): Record<string, any> {
const schema: Record<string, any> = {};
for (const prop of properties) {
schema[prop.name] = {
type: this.mapPropertyType(prop.type),
type: this._mapPropertyType(prop.type),
default: prop.defaultValue,
description: prop.description,
min: prop.min,
@@ -146,7 +146,7 @@ export class NodeRegistryService {
return schema;
}
private mapPropertyType(type: string): string {
private _mapPropertyType(type: string): string {
switch (type) {
case 'string':
case 'code':
@@ -162,7 +162,7 @@ export class NodeRegistryService {
}
}
private getChildrenConstraints(config: NodeRegistrationConfig): { min?: number; max?: number } | undefined {
private _getChildrenConstraints(config: NodeRegistrationConfig): { min?: number; max?: number } | undefined {
if (config.minChildren !== undefined || config.maxChildren !== undefined) {
return {
min: config.minChildren,
@@ -183,7 +183,7 @@ export class NodeRegistryService {
}
}
private createTemplate(config: NodeRegistrationConfig, metadata: NodeMetadata): NodeTemplate {
private _createTemplate(config: NodeRegistrationConfig, metadata: NodeMetadata): NodeTemplate {
const defaultConfig: any = {
nodeType: config.type
};
@@ -212,10 +212,10 @@ export class NodeRegistryService {
const template: NodeTemplate = {
type: metadata.nodeType,
displayName: config.displayName,
category: config.category || this.getDefaultCategory(config.type),
category: config.category || this._getDefaultCategory(config.type),
description: config.description || '',
icon: config.icon || this.getDefaultIcon(config.type),
color: config.color || this.getDefaultColor(config.type),
icon: config.icon || this._getDefaultIcon(config.type),
color: config.color || this._getDefaultColor(config.type),
className: config.implementationType,
defaultConfig,
properties: (config.properties || []).map((p) => ({
@@ -242,7 +242,7 @@ export class NodeRegistryService {
return template;
}
private getDefaultIcon(type: string): string {
private _getDefaultIcon(type: string): string {
switch (type) {
case 'composite': return 'GitBranch';
case 'decorator': return 'Settings';
@@ -252,7 +252,7 @@ export class NodeRegistryService {
}
}
private getDefaultColor(type: string): string {
private _getDefaultColor(type: string): string {
switch (type) {
case 'composite': return '#1976d2';
case 'decorator': return '#fb8c00';
@@ -4,33 +4,33 @@ import type { MessageHub } from '@esengine/editor-runtime';
const logger = createLogger('NotificationService');
export class NotificationService {
private static instance: NotificationService;
private messageHub: MessageHub | null = null;
private static _instance: NotificationService;
private _messageHub: MessageHub | null = null;
private constructor() {
// 延迟获取 MessageHub,因为初始化时可能还不可用
}
private getMessageHub(): MessageHub | null {
if (!this.messageHub && PluginAPI.isAvailable) {
private _getMessageHub(): MessageHub | null {
if (!this._messageHub && PluginAPI.isAvailable) {
try {
this.messageHub = PluginAPI.messageHub;
this._messageHub = PluginAPI.messageHub;
} catch (error) {
logger.warn('MessageHub not available');
}
}
return this.messageHub;
return this._messageHub;
}
public static getInstance(): NotificationService {
if (!NotificationService.instance) {
NotificationService.instance = new NotificationService();
if (!NotificationService._instance) {
NotificationService._instance = new NotificationService();
}
return NotificationService.instance;
return NotificationService._instance;
}
public showToast(message: string, type: 'success' | 'error' | 'warning' | 'info' = 'info'): void {
const hub = this.getMessageHub();
const hub = this._getMessageHub();
if (!hub) {
logger.info(`[Toast ${type}] ${message}`);
return;
+15
View File
@@ -0,0 +1,15 @@
{
"id": "blueprint-editor",
"name": "@esengine/blueprint-editor",
"displayName": "Blueprint Editor",
"description": "Visual scripting editor | 可视化脚本编辑器",
"version": "1.0.0",
"category": "Editor",
"icon": "Workflow",
"isEditorPlugin": true,
"runtimeModule": "@esengine/blueprint",
"exports": {
"inspectors": ["BlueprintComponentInspector"],
"panels": ["BlueprintEditorPanel"]
}
}
+3
View File
@@ -24,6 +24,9 @@
"dependencies": {
"@esengine/blueprint": "workspace:*"
},
"peerDependencies": {
"@esengine/editor-core": "workspace:*"
},
"devDependencies": {
"@esengine/ecs-framework": "workspace:*",
"@esengine/engine-core": "workspace:*",
+22
View File
@@ -0,0 +1,22 @@
/**
* Blueprint 模块服务令牌
* Blueprint module service tokens
*
* 定义 blueprint 模块导出的服务令牌。
* Defines service tokens exported by blueprint module.
*
* 注意:当前 Blueprint 模块主要通过组件和系统工作,
* 暂时没有需要通过服务令牌暴露的全局服务。
* 未来可能添加 BlueprintDebuggerToken 等。
*
* Note: Blueprint module currently works mainly through components and systems,
* no global services need to be exposed via service tokens yet.
* May add BlueprintDebuggerToken etc. in the future.
*/
// 当前无服务令牌
// No service tokens currently
// 预留导出位置,便于将来扩展
// Reserved export location for future extension
export {};
+15
View File
@@ -0,0 +1,15 @@
{
"id": "camera-editor",
"name": "@esengine/camera-editor",
"displayName": "Camera Editor",
"description": "Editor support for camera system | 相机系统编辑器支持",
"version": "1.0.0",
"category": "Editor",
"icon": "Camera",
"isEditorPlugin": true,
"runtimeModule": "@esengine/camera",
"exports": {
"inspectors": ["CameraComponentInspector"],
"gizmos": ["CameraGizmo"]
}
}
+6 -1
View File
@@ -21,10 +21,15 @@
"type-check": "tsc --noEmit",
"clean": "rimraf dist"
},
"dependencies": {
"@esengine/camera": "workspace:*"
},
"peerDependencies": {
"@esengine/editor-core": "workspace:*"
},
"devDependencies": {
"@esengine/ecs-framework": "workspace:*",
"@esengine/engine-core": "workspace:*",
"@esengine/camera": "workspace:*",
"@esengine/editor-core": "workspace:*",
"@esengine/build-config": "workspace:*",
"react": "^18.3.1",
+4 -4
View File
@@ -73,7 +73,7 @@ export class Core {
* @zh Core专用日志器
* @en Core logger
*/
private static _logger = createLogger('Core');
private static readonly _logger = createLogger('Core');
/**
* @zh 调试模式标志,在调试模式下会启用额外的性能监控和错误检查
@@ -204,7 +204,7 @@ export class Core {
*/
public static get services(): ServiceContainer {
if (!this._instance) {
throw new Error('Core实例未创建,请先调用Core.create()');
throw new Error('Core instance not created, call Core.create() first | Core实例未创建,请先调用Core.create()');
}
return this._instance._serviceContainer;
}
@@ -238,7 +238,7 @@ export class Core {
*/
public static get pluginServices(): PluginServiceRegistry {
if (!this._instance) {
throw new Error('Core实例未创建,请先调用Core.create()');
throw new Error('Core instance not created, call Core.create() first | Core实例未创建,请先调用Core.create()');
}
return this._instance._pluginServiceRegistry;
}
@@ -264,7 +264,7 @@ export class Core {
*/
public static get worldManager(): WorldManager {
if (!this._instance) {
throw new Error('Core实例未创建,请先调用Core.create()');
throw new Error('Core instance not created, call Core.create() first | Core实例未创建,请先调用Core.create()');
}
return this._instance._worldManager;
}
+6 -3
View File
@@ -30,6 +30,7 @@
*/
import { createServiceToken } from './PluginServiceRegistry';
import { createLogger } from '../Utils/Logger';
// ============================================================================
// 接口定义 | Interface Definitions
@@ -236,16 +237,18 @@ export class RuntimeModeService implements IRuntimeMode {
}
}
private static readonly _logger = createLogger('RuntimeModeService');
/**
* 通知模式变化
* Notify mode change
* @zh 通知模式变化
* @en Notify mode change
*/
private _notifyChange(): void {
for (const callback of this._callbacks) {
try {
callback(this);
} catch (error) {
console.error('[RuntimeModeService] Callback error:', error);
RuntimeModeService._logger.error('Callback error:', error);
}
}
}
+2 -2
View File
@@ -41,7 +41,7 @@ export abstract class Component implements IComponent {
* @zh 组件ID生成器,用于为每个组件分配唯一的ID
* @en Component ID generator, used to assign unique IDs to each component
*/
private static idGenerator: number = 0;
private static _idGenerator: number = 0;
/**
* @zh 组件唯一标识符,在整个游戏生命周期中唯一
@@ -81,7 +81,7 @@ export abstract class Component implements IComponent {
* @en Create component instance, automatically assigns unique ID
*/
constructor() {
this.id = Component.idGenerator++;
this.id = Component._idGenerator++;
}
/**
+5 -2
View File
@@ -1,6 +1,7 @@
import { Entity } from '../Entity';
import { Component } from '../Component';
import { ComponentType, GlobalComponentRegistry } from './ComponentStorage';
import { getComponentTypeName } from '../Decorators';
import { IScene } from '../IScene';
import { createLogger } from '../../Utils/Logger';
@@ -239,7 +240,8 @@ export class CommandBuffer {
pending.adds.set(typeId, component);
if (this._debug) {
logger.debug(`CommandBuffer: 延迟添加组件 ${component.constructor.name} 到实体 ${entity.name}`);
const typeName = getComponentTypeName(component.constructor as ComponentType);
logger.debug(`CommandBuffer: 延迟添加组件 ${typeName} 到实体 ${entity.name}`);
}
} else {
// 旧模式
@@ -435,9 +437,10 @@ export class CommandBuffer {
entity.addComponent(component);
commandCount++;
} catch (error) {
const typeName = getComponentTypeName(component.constructor as ComponentType);
logger.error(`CommandBuffer: 添加组件失败`, {
entity: entity.name,
component: component.constructor.name,
component: typeName,
error
});
}
+57 -44
View File
@@ -142,24 +142,25 @@ interface ComponentUsageTracker {
* 全局组件池管理器
*/
export class ComponentPoolManager {
private static instance: ComponentPoolManager;
private pools = new Map<string, ComponentPool<Component>>();
private usageTracker = new Map<string, ComponentUsageTracker>();
private static _instance: ComponentPoolManager;
private _pools = new Map<string, ComponentPool<Component>>();
private _usageTracker = new Map<string, ComponentUsageTracker>();
private autoCleanupInterval = 60000;
private lastCleanupTime = 0;
private _autoCleanupInterval = 60000;
private _lastCleanupTime = 0;
private constructor() {}
static getInstance(): ComponentPoolManager {
if (!ComponentPoolManager.instance) {
ComponentPoolManager.instance = new ComponentPoolManager();
if (!ComponentPoolManager._instance) {
ComponentPoolManager._instance = new ComponentPoolManager();
}
return ComponentPoolManager.instance;
return ComponentPoolManager._instance;
}
/**
* 注册组件池
* @zh 注册组件池
* @en Register component pool
*/
registerPool<T extends Component>(
componentName: string,
@@ -168,9 +169,9 @@ export class ComponentPoolManager {
maxSize?: number,
minSize?: number
): void {
this.pools.set(componentName, new ComponentPool(createFn, resetFn, maxSize, minSize) as unknown as ComponentPool<Component>);
this._pools.set(componentName, new ComponentPool(createFn, resetFn, maxSize, minSize) as unknown as ComponentPool<Component>);
this.usageTracker.set(componentName, {
this._usageTracker.set(componentName, {
createCount: 0,
releaseCount: 0,
lastAccessTime: Date.now()
@@ -178,23 +179,25 @@ export class ComponentPoolManager {
}
/**
* 获取组件实例
* @zh 获取组件实例
* @en Acquire component instance
*/
acquireComponent<T extends Component>(componentName: string): T | null {
const pool = this.pools.get(componentName);
const pool = this._pools.get(componentName);
this.trackUsage(componentName, 'create');
this._trackUsage(componentName, 'create');
return pool ? (pool.acquire() as T) : null;
}
/**
* 释放组件实例
* @zh 释放组件实例
* @en Release component instance
*/
releaseComponent<T extends Component>(componentName: string, component: T): void {
const pool = this.pools.get(componentName);
const pool = this._pools.get(componentName);
this.trackUsage(componentName, 'release');
this._trackUsage(componentName, 'release');
if (pool) {
pool.release(component);
@@ -202,10 +205,11 @@ export class ComponentPoolManager {
}
/**
* 追踪使用情况
* @zh 追踪使用情况
* @en Track usage
*/
private trackUsage(componentName: string, action: 'create' | 'release'): void {
let tracker = this.usageTracker.get(componentName);
private _trackUsage(componentName: string, action: 'create' | 'release'): void {
let tracker = this._usageTracker.get(componentName);
if (!tracker) {
tracker = {
@@ -213,7 +217,7 @@ export class ComponentPoolManager {
releaseCount: 0,
lastAccessTime: Date.now()
};
this.usageTracker.set(componentName, tracker);
this._usageTracker.set(componentName, tracker);
}
if (action === 'create') {
@@ -226,66 +230,72 @@ export class ComponentPoolManager {
}
/**
* 自动清理(定期调用)
* @zh 自动清理(定期调用)
* @en Auto cleanup (called periodically)
*/
public update(): void {
const now = Date.now();
if (now - this.lastCleanupTime < this.autoCleanupInterval) {
if (now - this._lastCleanupTime < this._autoCleanupInterval) {
return;
}
for (const [name, tracker] of this.usageTracker.entries()) {
for (const [name, tracker] of this._usageTracker.entries()) {
const inactive = now - tracker.lastAccessTime > 120000;
if (inactive) {
const pool = this.pools.get(name);
const pool = this._pools.get(name);
if (pool) {
pool.shrink();
}
}
}
this.lastCleanupTime = now;
this._lastCleanupTime = now;
}
/**
* 获取热点组件列表
* @zh 获取热点组件列表
* @en Get hot components list
*/
public getHotComponents(threshold: number = 100): string[] {
return Array.from(this.usageTracker.entries())
return Array.from(this._usageTracker.entries())
.filter(([_, tracker]) => tracker.createCount > threshold)
.map(([name]) => name);
}
/**
* 预热所有池
* @zh 预热所有池
* @en Prewarm all pools
*/
prewarmAll(count: number = 100): void {
for (const pool of this.pools.values()) {
for (const pool of this._pools.values()) {
pool.prewarm(count);
}
}
/**
* 清空所有池
* @zh 清空所有池
* @en Clear all pools
*/
clearAll(): void {
for (const pool of this.pools.values()) {
for (const pool of this._pools.values()) {
pool.clear();
}
}
/**
* 重置管理器
* @zh 重置管理器
* @en Reset manager
*/
reset(): void {
this.pools.clear();
this.usageTracker.clear();
this._pools.clear();
this._usageTracker.clear();
}
/**
* 获取全局统计信息
* @zh 获取全局统计信息
* @en Get global stats
*/
getGlobalStats(): Array<{
componentName: string;
@@ -298,11 +308,11 @@ export class ComponentPoolManager {
usage: ComponentUsageTracker | undefined;
}> = [];
for (const [name, pool] of this.pools.entries()) {
for (const [name, pool] of this._pools.entries()) {
stats.push({
componentName: name,
poolStats: pool.getStats(),
usage: this.usageTracker.get(name)
usage: this._usageTracker.get(name)
});
}
@@ -310,11 +320,12 @@ export class ComponentPoolManager {
}
/**
* 获取池统计信息
* @zh 获取池统计信息
* @en Get pool stats
*/
getPoolStats(): Map<string, { available: number; maxSize: number }> {
const stats = new Map();
for (const [name, pool] of this.pools) {
for (const [name, pool] of this._pools) {
stats.set(name, {
available: pool.getAvailableCount(),
maxSize: pool.getMaxSize()
@@ -324,11 +335,12 @@ export class ComponentPoolManager {
}
/**
* 获取池利用率信息
* @zh 获取池利用率信息
* @en Get pool utilization info
*/
getPoolUtilization(): Map<string, { used: number; total: number; utilization: number }> {
const utilization = new Map();
for (const [name, pool] of this.pools) {
for (const [name, pool] of this._pools) {
const available = pool.getAvailableCount();
const maxSize = pool.getMaxSize();
const used = maxSize - available;
@@ -344,10 +356,11 @@ export class ComponentPoolManager {
}
/**
* 获取指定组件的池利用率
* @zh 获取指定组件的池利用率
* @en Get component pool utilization
*/
getComponentUtilization(componentName: string): number {
const pool = this.pools.get(componentName);
const pool = this._pools.get(componentName);
if (!pool) return 0;
const available = pool.getAvailableCount();
@@ -53,10 +53,11 @@ export class ComponentRegistry implements IComponentRegistry {
// 检查是否使用了 @ECSComponent 装饰器
if (!hasECSComponentDecorator(componentType) && !this._warnedComponents.has(componentType)) {
this._warnedComponents.add(componentType);
console.warn(
`[ComponentRegistry] Component "${typeName}" is missing @ECSComponent decorator. ` +
logger.warn(
`Component "${typeName}" is missing @ECSComponent decorator. ` +
`This may cause issues with serialization and code minification. ` +
`Please add: @ECSComponent('${typeName}')`
`Please add: @ECSComponent('${typeName}') | ` +
`组件 "${typeName}" 缺少 @ECSComponent 装饰器,可能导致序列化和代码压缩问题`
);
}
+16 -12
View File
@@ -443,29 +443,33 @@ export class EventBus implements IEventBus {
* 提供全局访问的事件总线
*/
export class GlobalEventBus {
private static instance: EventBus;
private static _instance: EventBus;
/**
* 获取全局事件总线实例
* @param debugMode 是否启用调试模式
* @zh 获取全局事件总线实例
* @en Get global event bus instance
*
* @param debugMode - @zh 是否启用调试模式 @en Whether to enable debug mode
*/
public static getInstance(debugMode: boolean = false): EventBus {
if (!this.instance) {
this.instance = new EventBus(debugMode);
if (!this._instance) {
this._instance = new EventBus(debugMode);
}
return this.instance;
return this._instance;
}
/**
* 重置全局事件总线实例
* @param debugMode 是否启用调试模式
* @zh 重置全局事件总线实例
* @en Reset global event bus instance
*
* @param debugMode - @zh 是否启用调试模式 @en Whether to enable debug mode
*/
public static reset(debugMode: boolean = false): EventBus {
if (this.instance) {
this.instance.clear();
if (this._instance) {
this._instance.clear();
}
this.instance = new EventBus(debugMode);
return this.instance;
this._instance = new EventBus(debugMode);
return this._instance;
}
}
+150 -145
View File
@@ -6,6 +6,7 @@ import {
TypedArrayTypeName
} from './SoATypeRegistry';
import { SoASerializer } from './SoASerializer';
import type { IComponentTypeMetadata, ComponentTypeWithMetadata } from '../../Types';
// 重新导出类型,保持向后兼容
export type { SupportedTypedArray, TypedArrayTypeName } from './SoATypeRegistry';
@@ -13,170 +14,174 @@ export { SoATypeRegistry } from './SoATypeRegistry';
export { SoASerializer } from './SoASerializer';
/**
* 启用SoA优化装饰器
* 默认关闭SoA,只有在大规模批量操作场景下才建议开启
* @zh SoA 字段统计信息
* @en SoA field statistics
*/
export interface ISoAFieldStats {
size: number;
capacity: number;
type: string;
memory: number;
}
/**
* @zh SoA 存储统计信息
* @en SoA storage statistics
*/
export interface ISoAStorageStats {
size: number;
capacity: number;
totalSlots: number;
usedSlots: number;
freeSlots: number;
fragmentation: number;
memoryUsage: number;
fieldStats: Map<string, ISoAFieldStats>;
}
/**
* @zh 启用 SoA 优化装饰器 - 默认关闭,只在大规模批量操作场景下建议开启
* @en Enable SoA optimization decorator - disabled by default, recommended only for large-scale batch operations
*/
export function EnableSoA<T extends ComponentType>(target: T): T {
(target as any).__enableSoA = true;
(target as ComponentType & IComponentTypeMetadata).__enableSoA = true;
return target;
}
/**
* 64位浮点数装饰器
* 标记字段使用Float64Array存储(更高精度但更多内存)
* @zh 组件字段元数据键(仅 Set<string> 类型的字段)
* @en Component field metadata keys (only Set<string> type fields)
*/
export function Float64(target: any, propertyKey: string | symbol): void {
const key = String(propertyKey);
if (!target.constructor.__float64Fields) {
target.constructor.__float64Fields = new Set();
}
target.constructor.__float64Fields.add(key);
type ComponentFieldMetadataKey = Exclude<keyof IComponentTypeMetadata, '__enableSoA'>;
/**
* @zh 装饰器目标原型接口
* @en Decorator target prototype interface
*/
interface IDecoratorTarget {
constructor: IComponentTypeMetadata & Function;
}
/**
* 32位浮点数装饰器
* 标记字段使用Float32Array存储(默认类型,平衡性能和精度)
* @zh 辅助函数:获取或创建字段集合
* @en Helper function: get or create field set
*/
export function Float32(target: any, propertyKey: string | symbol): void {
const key = String(propertyKey);
if (!target.constructor.__float32Fields) {
target.constructor.__float32Fields = new Set();
function getOrCreateFieldSet(
target: IDecoratorTarget,
fieldName: ComponentFieldMetadataKey
): Set<string> {
const ctor = target.constructor as IComponentTypeMetadata;
let fieldSet = ctor[fieldName];
if (!fieldSet) {
fieldSet = new Set<string>();
ctor[fieldName] = fieldSet;
}
target.constructor.__float32Fields.add(key);
return fieldSet;
}
/**
* 32位整数装饰器
* 标记字段使用Int32Array存储(适用于整数值)
* @zh 64位浮点数装饰器 - 标记字段使用 Float64Array 存储(更高精度但更多内存)
* @en Float64 decorator - marks field to use Float64Array storage (higher precision but more memory)
*/
export function Int32(target: any, propertyKey: string | symbol): void {
const key = String(propertyKey);
if (!target.constructor.__int32Fields) {
target.constructor.__int32Fields = new Set();
}
target.constructor.__int32Fields.add(key);
export function Float64(target: object, propertyKey: string | symbol): void {
getOrCreateFieldSet(target as IDecoratorTarget, '__float64Fields').add(String(propertyKey));
}
/**
* 32位无符号整数装饰器
* 标记字段使用Uint32Array存储(适用于无符号整数,如ID、标志位等)
* @zh 32位浮点数装饰器 - 标记字段使用 Float32Array 存储(默认类型,平衡性能和精度)
* @en Float32 decorator - marks field to use Float32Array storage (default, balanced performance and precision)
*/
export function Uint32(target: any, propertyKey: string | symbol): void {
const key = String(propertyKey);
if (!target.constructor.__uint32Fields) {
target.constructor.__uint32Fields = new Set();
}
target.constructor.__uint32Fields.add(key);
export function Float32(target: object, propertyKey: string | symbol): void {
getOrCreateFieldSet(target as IDecoratorTarget, '__float32Fields').add(String(propertyKey));
}
/**
* 16位整数装饰器
* 标记字段使用Int16Array存储(适用于小范围整数)
* @zh 32位整数装饰器 - 标记字段使用 Int32Array 存储(适用于整数值)
* @en Int32 decorator - marks field to use Int32Array storage (for integer values)
*/
export function Int16(target: any, propertyKey: string | symbol): void {
const key = String(propertyKey);
if (!target.constructor.__int16Fields) {
target.constructor.__int16Fields = new Set();
}
target.constructor.__int16Fields.add(key);
export function Int32(target: object, propertyKey: string | symbol): void {
getOrCreateFieldSet(target as IDecoratorTarget, '__int32Fields').add(String(propertyKey));
}
/**
* 16位无符号整数装饰器
* 标记字段使用Uint16Array存储(适用于小范围无符号整数)
* @zh 32位无符号整数装饰器 - 标记字段使用 Uint32Array 存储
* @en Uint32 decorator - marks field to use Uint32Array storage
*/
export function Uint16(target: any, propertyKey: string | symbol): void {
const key = String(propertyKey);
if (!target.constructor.__uint16Fields) {
target.constructor.__uint16Fields = new Set();
}
target.constructor.__uint16Fields.add(key);
export function Uint32(target: object, propertyKey: string | symbol): void {
getOrCreateFieldSet(target as IDecoratorTarget, '__uint32Fields').add(String(propertyKey));
}
/**
* 8位整数装饰器
* 标记字段使用Int8Array存储(适用于很小的整数值)
* @zh 16位整数装饰器 - 标记字段使用 Int16Array 存储
* @en Int16 decorator - marks field to use Int16Array storage
*/
export function Int8(target: any, propertyKey: string | symbol): void {
const key = String(propertyKey);
if (!target.constructor.__int8Fields) {
target.constructor.__int8Fields = new Set();
}
target.constructor.__int8Fields.add(key);
export function Int16(target: object, propertyKey: string | symbol): void {
getOrCreateFieldSet(target as IDecoratorTarget, '__int16Fields').add(String(propertyKey));
}
/**
* 8位无符号整数装饰器
* 标记字段使用Uint8Array存储(适用于字节值、布尔标志等)
* @zh 16位无符号整数装饰器 - 标记字段使用 Uint16Array 存储
* @en Uint16 decorator - marks field to use Uint16Array storage
*/
export function Uint8(target: any, propertyKey: string | symbol): void {
const key = String(propertyKey);
if (!target.constructor.__uint8Fields) {
target.constructor.__uint8Fields = new Set();
}
target.constructor.__uint8Fields.add(key);
export function Uint16(target: object, propertyKey: string | symbol): void {
getOrCreateFieldSet(target as IDecoratorTarget, '__uint16Fields').add(String(propertyKey));
}
/**
* 8位夹紧整数装饰器
* 标记字段使用Uint8ClampedArray存储(适用于颜色值等需要夹紧的数据)
* @zh 8位整数装饰器 - 标记字段使用 Int8Array 存储
* @en Int8 decorator - marks field to use Int8Array storage
*/
export function Uint8Clamped(target: any, propertyKey: string | symbol): void {
const key = String(propertyKey);
if (!target.constructor.__uint8ClampedFields) {
target.constructor.__uint8ClampedFields = new Set();
}
target.constructor.__uint8ClampedFields.add(key);
}
/**
* 序列化Map装饰器
* 标记Map字段需要序列化/反序列化存储
*/
export function SerializeMap(target: any, propertyKey: string | symbol): void {
const key = String(propertyKey);
if (!target.constructor.__serializeMapFields) {
target.constructor.__serializeMapFields = new Set();
}
target.constructor.__serializeMapFields.add(key);
export function Int8(target: object, propertyKey: string | symbol): void {
getOrCreateFieldSet(target as IDecoratorTarget, '__int8Fields').add(String(propertyKey));
}
/**
* 序列化Set装饰器
* 标记Set字段需要序列化/反序列化存储
* @zh 8位无符号整数装饰器 - 标记字段使用 Uint8Array 存储
* @en Uint8 decorator - marks field to use Uint8Array storage
*/
export function SerializeSet(target: any, propertyKey: string | symbol): void {
const key = String(propertyKey);
if (!target.constructor.__serializeSetFields) {
target.constructor.__serializeSetFields = new Set();
}
target.constructor.__serializeSetFields.add(key);
export function Uint8(target: object, propertyKey: string | symbol): void {
getOrCreateFieldSet(target as IDecoratorTarget, '__uint8Fields').add(String(propertyKey));
}
/**
* 序列化Array装饰器
* 标记Array字段需要序列化/反序列化存储
* @zh 8位夹紧整数装饰器 - 标记字段使用 Uint8ClampedArray 存储(适用于颜色值)
* @en Uint8Clamped decorator - marks field to use Uint8ClampedArray storage (for color values)
*/
export function SerializeArray(target: any, propertyKey: string | symbol): void {
const key = String(propertyKey);
if (!target.constructor.__serializeArrayFields) {
target.constructor.__serializeArrayFields = new Set();
}
target.constructor.__serializeArrayFields.add(key);
export function Uint8Clamped(target: object, propertyKey: string | symbol): void {
getOrCreateFieldSet(target as IDecoratorTarget, '__uint8ClampedFields').add(String(propertyKey));
}
/**
* 深拷贝装饰器
* 标记字段需要深拷贝处理(适用于嵌套对象)
* @zh 序列化 Map 装饰器 - 标记 Map 字段需要序列化/反序列化存储
* @en SerializeMap decorator - marks Map field for serialization/deserialization
*/
export function DeepCopy(target: any, propertyKey: string | symbol): void {
const key = String(propertyKey);
if (!target.constructor.__deepCopyFields) {
target.constructor.__deepCopyFields = new Set();
export function SerializeMap(target: object, propertyKey: string | symbol): void {
getOrCreateFieldSet(target as IDecoratorTarget, '__serializeMapFields').add(String(propertyKey));
}
target.constructor.__deepCopyFields.add(key);
/**
* @zh 序列化 Set 装饰器 - 标记 Set 字段需要序列化/反序列化存储
* @en SerializeSet decorator - marks Set field for serialization/deserialization
*/
export function SerializeSet(target: object, propertyKey: string | symbol): void {
getOrCreateFieldSet(target as IDecoratorTarget, '__serializeSetFields').add(String(propertyKey));
}
/**
* @zh 序列化 Array 装饰器 - 标记 Array 字段需要序列化/反序列化存储
* @en SerializeArray decorator - marks Array field for serialization/deserialization
*/
export function SerializeArray(target: object, propertyKey: string | symbol): void {
getOrCreateFieldSet(target as IDecoratorTarget, '__serializeArrayFields').add(String(propertyKey));
}
/**
* @zh 深拷贝装饰器 - 标记字段需要深拷贝处理(适用于嵌套对象)
* @en DeepCopy decorator - marks field for deep copy handling (for nested objects)
*/
export function DeepCopy(target: object, propertyKey: string | symbol): void {
getOrCreateFieldSet(target as IDecoratorTarget, '__deepCopyFields').add(String(propertyKey));
}
@@ -186,8 +191,8 @@ export function DeepCopy(target: any, propertyKey: string | symbol): void {
*/
export class SoAStorage<T extends Component> {
private fields = new Map<string, SupportedTypedArray>();
private stringFields = new Map<string, string[]>();
private serializedFields = new Map<string, string[]>();
private stringFields = new Map<string, Array<string | undefined>>();
private serializedFields = new Map<string, Array<string | undefined>>();
private complexFields = new Map<number, Map<string, unknown>>();
private entityToIndex = new Map<number, number>();
private indexToEntity: number[] = [];
@@ -318,30 +323,29 @@ export class SoAStorage<T extends Component> {
private updateComponentAtIndex(index: number, component: T): void {
const entityId = this.indexToEntity[index]!;
const complexFieldMap = new Map<string, any>();
const highPrecisionFields = (this.type as any).__highPrecisionFields || new Set();
const serializeMapFields = (this.type as any).__serializeMapFields || new Set();
const serializeSetFields = (this.type as any).__serializeSetFields || new Set();
const serializeArrayFields = (this.type as any).__serializeArrayFields || new Set();
const deepCopyFields = (this.type as any).__deepCopyFields || new Set();
const complexFieldMap = new Map<string, unknown>();
const typeWithMeta = this.type as ComponentTypeWithMetadata<T>;
const highPrecisionFields = typeWithMeta.__highPrecisionFields || new Set<string>();
const serializeMapFields = typeWithMeta.__serializeMapFields || new Set<string>();
const serializeSetFields = typeWithMeta.__serializeSetFields || new Set<string>();
const serializeArrayFields = typeWithMeta.__serializeArrayFields || new Set<string>();
const deepCopyFields = typeWithMeta.__deepCopyFields || new Set<string>();
// 处理所有字段
const componentRecord = component as Record<string, unknown>;
for (const key in component) {
if (component.hasOwnProperty(key) && key !== 'id') {
const value = (component as any)[key];
if (Object.prototype.hasOwnProperty.call(component, key) && key !== 'id') {
const value = componentRecord[key];
const type = typeof value;
if (type === 'number') {
const numValue = value as number;
if (highPrecisionFields.has(key) || !this.fields.has(key)) {
// 标记为高精度或未在TypedArray中的数值作为复杂对象存储
complexFieldMap.set(key, value);
complexFieldMap.set(key, numValue);
} else {
// 存储到TypedArray
const array = this.fields.get(key)!;
array[index] = value;
array[index] = numValue;
}
} else if (type === 'boolean' && this.fields.has(key)) {
// 布尔值存储到TypedArray
const array = this.fields.get(key)!;
array[index] = value ? 1 : 0;
} else if (this.stringFields.has(key)) {
@@ -526,7 +530,8 @@ export class SoAStorage<T extends Component> {
}
/**
* 获取组件的快照副本(用于序列化等需要独立副本的场景)
* @zh 获取组件的快照副本(用于序列化等需要独立副本的场景)
* @en Get a snapshot copy of the component (for serialization scenarios)
*/
public getComponentSnapshot(entityId: number): T | null {
const index = this.entityToIndex.get(entityId);
@@ -534,32 +539,26 @@ export class SoAStorage<T extends Component> {
return null;
}
// 需要 any 因为要动态写入泛型 T 的属性
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const component = new this.type() as any;
const component = new this.type();
const componentRecord = component as unknown as Record<string, unknown>;
// 恢复数值字段
for (const [fieldName, array] of this.fields.entries()) {
const value = array[index];
const fieldType = this.getFieldType(fieldName);
if (fieldType === 'boolean') {
component[fieldName] = value === 1;
} else {
component[fieldName] = value;
}
componentRecord[fieldName] = fieldType === 'boolean' ? value === 1 : value;
}
// 恢复字符串字段
for (const [fieldName, stringArray] of this.stringFields.entries()) {
component[fieldName] = stringArray[index];
componentRecord[fieldName] = stringArray[index];
}
// 恢复序列化字段
for (const [fieldName, serializedArray] of this.serializedFields.entries()) {
const serialized = serializedArray[index];
if (serialized) {
component[fieldName] = SoASerializer.deserialize(serialized, fieldName, {
componentRecord[fieldName] = SoASerializer.deserialize(serialized, fieldName, {
isMap: this.serializeMapFields.has(fieldName),
isSet: this.serializeSetFields.has(fieldName),
isArray: this.serializeArrayFields.has(fieldName)
@@ -571,11 +570,11 @@ export class SoAStorage<T extends Component> {
const complexFieldMap = this.complexFields.get(entityId);
if (complexFieldMap) {
for (const [fieldName, value] of complexFieldMap.entries()) {
component[fieldName] = value;
componentRecord[fieldName] = value;
}
}
return component as T;
return component;
}
private getFieldType(fieldName: string): string {
@@ -673,14 +672,14 @@ export class SoAStorage<T extends Component> {
// 重置字符串字段数组
for (const stringArray of this.stringFields.values()) {
for (let i = 0; i < stringArray.length; i++) {
stringArray[i] = undefined as any;
stringArray[i] = undefined;
}
}
// 重置序列化字段数组
for (const serializedArray of this.serializedFields.values()) {
for (let i = 0; i < serializedArray.length; i++) {
serializedArray[i] = undefined as any;
serializedArray[i] = undefined;
}
}
}
@@ -740,9 +739,13 @@ export class SoAStorage<T extends Component> {
this._size = activeEntries.length;
}
public getStats(): any {
/**
* @zh 获取 SoA 存储统计信息
* @en Get SoA storage statistics
*/
public getStats(): ISoAStorageStats {
let totalMemory = 0;
const fieldStats = new Map<string, any>();
const fieldStats = new Map<string, ISoAFieldStats>();
for (const [fieldName, array] of this.fields.entries()) {
const typeName = SoATypeRegistry.getTypeName(array);
@@ -761,7 +764,9 @@ export class SoAStorage<T extends Component> {
return {
size: this._size,
capacity: this._capacity,
usedSlots: this._size, // 兼容原测试
totalSlots: this._capacity,
usedSlots: this._size,
freeSlots: this._capacity - this._size,
fragmentation: this.freeIndices.length / this._capacity,
memoryUsage: totalMemory,
fieldStats: fieldStats
@@ -27,8 +27,11 @@
*/
import { SystemDependencyGraph, CycleDependencyError, type SystemDependencyInfo } from './SystemDependencyGraph';
import { createLogger } from '../../Utils/Logger';
import type { EntitySystem } from '../Systems/EntitySystem';
const logger = createLogger('SystemScheduler');
export { CycleDependencyError };
/**
@@ -293,7 +296,7 @@ export class SystemScheduler {
throw error;
}
// 其他错误回退到 updateOrder 排序
console.warn('[SystemScheduler] 拓扑排序失败,回退到 updateOrder 排序', error);
logger.warn('Topological sort failed, falling back to updateOrder | 拓扑排序失败,回退到 updateOrder 排序', error);
return this.fallbackSort(systems);
}
}
+15 -14
View File
@@ -154,7 +154,7 @@ export class Scene implements IScene {
/**
* 日志记录器
*/
private readonly logger: ReturnType<typeof createLogger>;
private readonly _logger: ReturnType<typeof createLogger>;
/**
* 性能监控器缓存
@@ -298,12 +298,12 @@ export class Scene implements IScene {
return this._systemScheduler.getAllSortedSystems(systems);
} catch (error) {
if (error instanceof CycleDependencyError) {
this.logger.error(
this._logger.error(
`[Scene] 系统存在循环依赖,回退到 updateOrder 排序 | Cycle dependency detected, falling back to updateOrder sort`,
error.involvedNodes
);
} else {
this.logger.error(`[Scene] 系统排序失败 | System sorting failed`, error);
this._logger.error(`[Scene] 系统排序失败 | System sorting failed`, error);
}
return this._sortSystemsByUpdateOrder(systems);
}
@@ -395,7 +395,7 @@ export class Scene implements IScene {
this.referenceTracker = new ReferenceTracker();
this.handleManager = new EntityHandleManager();
this._services = new ServiceContainer();
this.logger = createLogger('Scene');
this._logger = createLogger('Scene');
this._maxErrorCount = config?.maxSystemErrorCount ?? 10;
if (config?.name) {
@@ -467,7 +467,7 @@ export class Scene implements IScene {
try {
callback();
} catch (error) {
this.logger.error('Error executing deferred component callback:', error);
this._logger.error('Error executing deferred component callback:', error);
}
}
this._deferredComponentCallbacks = [];
@@ -580,7 +580,7 @@ export class Scene implements IScene {
try {
system.flushCommands();
} catch (error) {
this.logger.error(`Error flushing commands for system ${system.systemName}:`, error);
this._logger.error(`Error flushing commands for system ${system.systemName}:`, error);
}
}
} finally {
@@ -601,15 +601,16 @@ export class Scene implements IScene {
const errorCount = (this._systemErrorCount.get(system) || 0) + 1;
this._systemErrorCount.set(system, errorCount);
this.logger.error(
`Error in system ${system.constructor.name}.${phase}() [${errorCount}/${this._maxErrorCount}]:`,
const name = system.systemName;
this._logger.error(
`Error in system ${name}.${phase}() [${errorCount}/${this._maxErrorCount}]:`,
error
);
if (errorCount >= this._maxErrorCount) {
system.enabled = false;
this.logger.error(
`System ${system.constructor.name} has been disabled due to excessive errors (${errorCount} errors)`
this._logger.error(
`System ${name} has been disabled due to excessive errors (${errorCount} errors)`
);
}
}
@@ -1086,7 +1087,7 @@ export class Scene implements IScene {
if (this._services.isRegistered(constructor)) {
const existingSystem = this._services.resolve(constructor) as T;
this.logger.debug(`System ${constructor.name} already registered, returning existing instance`);
this._logger.debug(`System ${constructor.name} already registered, returning existing instance`);
return existingSystem;
}
@@ -1102,10 +1103,10 @@ export class Scene implements IScene {
if (this._services.isRegistered(constructor)) {
const existingSystem = this._services.resolve(constructor);
if (existingSystem === system) {
this.logger.debug(`System ${constructor.name} instance already registered, returning it`);
this._logger.debug(`System ${constructor.name} instance already registered, returning it`);
return system;
} else {
this.logger.warn(
this._logger.warn(
`Attempting to register a different instance of ${constructor.name}, ` +
'but type is already registered. Returning existing instance.'
);
@@ -1138,7 +1139,7 @@ export class Scene implements IScene {
system.initialize();
this.logger.debug(`System ${constructor.name} registered and initialized`);
this._logger.debug(`System ${constructor.name} registered and initialized`);
return system;
}
@@ -9,9 +9,12 @@ import { ComponentType } from '../Core/ComponentStorage';
import { getComponentTypeName, isEntityRefProperty } from '../Decorators';
import { getSerializationMetadata } from './SerializationDecorators';
import { ValueSerializer, SerializableValue } from './ValueSerializer';
import { createLogger } from '../../Utils/Logger';
import type { Entity } from '../Entity';
import type { SerializationContext, SerializedEntityRef } from './SerializationContext';
const logger = createLogger('ComponentSerializer');
export type { SerializableValue } from './ValueSerializer';
export type SerializedComponent = {
@@ -57,13 +60,13 @@ export class ComponentSerializer {
): Component | null {
const componentClass = componentRegistry.get(serializedData.type);
if (!componentClass) {
console.warn(`Component type not found: ${serializedData.type}`);
logger.warn(`Component type not found: ${serializedData.type} | 未找到组件类型: ${serializedData.type}`);
return null;
}
const metadata = getSerializationMetadata(componentClass);
if (!metadata) {
console.warn(`Component ${serializedData.type} is not serializable`);
logger.warn(`Component ${serializedData.type} is not serializable | 组件 ${serializedData.type} 不可序列化`);
return null;
}
@@ -15,6 +15,9 @@ import { HierarchySystem } from '../Systems/HierarchySystem';
import { HierarchyComponent } from '../Components/HierarchyComponent';
import { SerializationContext } from './SerializationContext';
import { ValueSerializer, SerializableValue } from './ValueSerializer';
import { createLogger } from '../../Utils/Logger';
const logger = createLogger('SceneSerializer');
/**
* 场景序列化格式
@@ -309,9 +312,10 @@ export class SceneSerializer {
const unresolvedCount = context.getUnresolvedCount();
if (unresolvedCount > 0) {
console.warn(
`[SceneSerializer] ${unresolvedCount} EntityRef(s) could not be resolved. ` +
`Resolved: ${resolvedCount}, Total pending: ${context.getPendingCount()}`
logger.warn(
`${unresolvedCount} EntityRef(s) could not be resolved. ` +
`Resolved: ${resolvedCount}, Total pending: ${context.getPendingCount()} | ` +
`${unresolvedCount} 个实体引用无法解析`
);
}
@@ -330,7 +334,7 @@ export class SceneSerializer {
// 如果有异步的 onDeserialized,在后台执行
if (deserializedPromises.length > 0) {
Promise.all(deserializedPromises).catch(error => {
console.error('Error in onDeserialized:', error);
logger.error('Error in onDeserialized | onDeserialized 执行错误:', error);
});
}
}
@@ -349,7 +353,8 @@ export class SceneSerializer {
promises.push(result);
}
} catch (error) {
console.error(`Error calling onDeserialized on component ${component.constructor.name}:`, error);
const typeName = getComponentTypeName(component.constructor as ComponentType);
logger.error(`Error calling onDeserialized on component ${typeName} | 调用组件 ${typeName} 的 onDeserialized 时出错:`, error);
}
}
}
@@ -6,6 +6,9 @@
import { SerializedComponent } from './ComponentSerializer';
import { SerializedScene } from './SceneSerializer';
import { createLogger } from '../../Utils/Logger';
const logger = createLogger('VersionMigration');
/**
* 组件迁移函数
@@ -123,7 +126,7 @@ export class VersionMigrationManager {
const migrations = this.componentMigrations.get(component.type);
if (!migrations) {
console.warn(`No migration path found for component ${component.type}`);
logger.warn(`No migration path found for component ${component.type} | 未找到组件 ${component.type} 的迁移路径`);
return component;
}
@@ -135,8 +138,9 @@ export class VersionMigrationManager {
const migration = migrations.get(version);
if (!migration) {
console.warn(
`Missing migration from version ${version} to ${version + 1} for ${component.type}`
logger.warn(
`Missing migration from version ${version} to ${version + 1} for ${component.type} | ` +
`缺少组件 ${component.type} 从版本 ${version}${version + 1} 的迁移`
);
break;
}
@@ -171,7 +175,7 @@ export class VersionMigrationManager {
const migration = this.sceneMigrations.get(version);
if (!migration) {
console.warn(`Missing scene migration from version ${version} to ${version + 1}`);
logger.warn(`Missing scene migration from version ${version} to ${version + 1} | 缺少场景从版本 ${version}${version + 1} 的迁移`);
break;
}
+29 -34
View File
@@ -1,10 +1,12 @@
import type { PlatformDetectionResult } from './IPlatformAdapter';
import { getGlobalWithMiniGame, type IGlobalThisWithMiniGame } from '../Types';
/**
* 平台检测器
* 自动检测当前运行环境并返回对应的平台信息
* @zh 平台检测器 - 自动检测当前运行环境并返回对应的平台信息
* @en Platform Detector - Automatically detect the current runtime environment
*/
export class PlatformDetector {
private static readonly miniGameGlobals: IGlobalThisWithMiniGame = getGlobalWithMiniGame();
/**
* 检测当前平台
*/
@@ -104,26 +106,24 @@ export class PlatformDetector {
}
/**
* 检测是否为微信小游戏环境
* @zh 检测是否为微信小游戏环境
* @en Check if running in WeChat Mini Game environment
*/
private static isWeChatMiniGame(): boolean {
// 检查wx全局对象
if (typeof (globalThis as any).wx !== 'undefined') {
const wx = (globalThis as any).wx;
// 检查微信小游戏特有的API
const wx = this.miniGameGlobals.wx;
if (wx) {
return !!(wx.getSystemInfo && wx.createCanvas && wx.createImage);
}
return false;
}
/**
* 检测是否为字节跳动小游戏环境
* @zh 检测是否为字节跳动小游戏环境
* @en Check if running in ByteDance Mini Game environment
*/
private static isByteDanceMiniGame(): boolean {
// 检查tt全局对象
if (typeof (globalThis as any).tt !== 'undefined') {
const tt = (globalThis as any).tt;
// 检查字节跳动小游戏特有的API
const tt = this.miniGameGlobals.tt;
if (tt) {
return !!(tt.getSystemInfo && tt.createCanvas && tt.createImage);
}
return false;
@@ -152,26 +152,24 @@ export class PlatformDetector {
}
/**
* 检测是否为支付宝小游戏环境
* @zh 检测是否为支付宝小游戏环境
* @en Check if running in Alipay Mini Game environment
*/
private static isAlipayMiniGame(): boolean {
// 检查my全局对象
if (typeof (globalThis as any).my !== 'undefined') {
const my = (globalThis as any).my;
// 检查支付宝小游戏特有的API
const my = this.miniGameGlobals.my;
if (my) {
return !!(my.getSystemInfo && my.createCanvas);
}
return false;
}
/**
* 检测是否为百度小游戏环境
* @zh 检测是否为百度小游戏环境
* @en Check if running in Baidu Mini Game environment
*/
private static isBaiduMiniGame(): boolean {
// 检查swan全局对象
if (typeof (globalThis as any).swan !== 'undefined') {
const swan = (globalThis as any).swan;
// 检查百度小游戏特有的API
const swan = this.miniGameGlobals.swan;
if (swan) {
return !!(swan.getSystemInfo && swan.createCanvas);
}
return false;
@@ -229,27 +227,26 @@ export class PlatformDetector {
}
/**
* 获取详细的环境信息(用于调试)
* @zh 获取详细的环境信息(用于调试)
* @en Get detailed environment information for debugging
*/
public static getDetailedInfo(): Record<string, any> {
const info: Record<string, any> = {};
public static getDetailedInfo(): Record<string, unknown> {
const info: Record<string, unknown> = {};
const globals = this.miniGameGlobals;
// 基础检测
info['userAgent'] = typeof navigator !== 'undefined' ? navigator.userAgent : 'unknown';
info['platform'] = typeof navigator !== 'undefined' ? navigator.platform : 'unknown';
// 全局对象检测
info['globalObjects'] = {
window: typeof window !== 'undefined',
document: typeof document !== 'undefined',
navigator: typeof navigator !== 'undefined',
wx: typeof (globalThis as any).wx !== 'undefined',
tt: typeof (globalThis as any).tt !== 'undefined',
my: typeof (globalThis as any).my !== 'undefined',
swan: typeof (globalThis as any).swan !== 'undefined'
wx: globals.wx !== undefined,
tt: globals.tt !== undefined,
my: globals.my !== undefined,
swan: globals.swan !== undefined
};
// Worker相关检测
info['workerSupport'] = {
Worker: typeof Worker !== 'undefined',
SharedWorker: typeof SharedWorker !== 'undefined',
@@ -258,13 +255,11 @@ export class PlatformDetector {
crossOriginIsolated: typeof self !== 'undefined' ? self.crossOriginIsolated : false
};
// 性能相关检测
info['performance'] = {
performanceNow: typeof performance !== 'undefined' && typeof performance.now === 'function',
hardwareConcurrency: typeof navigator !== 'undefined' ? navigator.hardwareConcurrency : undefined
};
// 其他API检测
info['apiSupport'] = {
Blob: typeof Blob !== 'undefined',
URL: typeof URL !== 'undefined',
+47 -39
View File
@@ -6,40 +6,43 @@ import { createLogger, type ILogger } from '../Utils/Logger';
* 用户需要手动注册平台适配器
*/
export class PlatformManager {
private static instance: PlatformManager;
private adapter: IPlatformAdapter | null = null;
private readonly logger: ILogger;
private static _instance: PlatformManager;
private _adapter: IPlatformAdapter | null = null;
private readonly _logger: ILogger;
private constructor() {
this.logger = createLogger('PlatformManager');
this._logger = createLogger('PlatformManager');
}
/**
* 获取单例实例
* @zh 获取单例实例
* @en Get singleton instance
*/
public static getInstance(): PlatformManager {
if (!PlatformManager.instance) {
PlatformManager.instance = new PlatformManager();
if (!PlatformManager._instance) {
PlatformManager._instance = new PlatformManager();
}
return PlatformManager.instance;
return PlatformManager._instance;
}
/**
* 获取当前平台适配器
* @zh 获取当前平台适配器
* @en Get current platform adapter
*/
public getAdapter(): IPlatformAdapter {
if (!this.adapter) {
if (!this._adapter) {
throw new Error('平台适配器未注册,请调用 registerAdapter() 注册适配器');
}
return this.adapter;
return this._adapter;
}
/**
* 注册平台适配器
* @zh 注册平台适配器
* @en Register platform adapter
*/
public registerAdapter(adapter: IPlatformAdapter): void {
this.adapter = adapter;
this.logger.info(`平台适配器已注册: ${adapter.name}`, {
this._adapter = adapter;
this._logger.info(`平台适配器已注册: ${adapter.name}`, {
name: adapter.name,
version: adapter.version,
supportsWorker: adapter.isWorkerSupported(),
@@ -49,37 +52,40 @@ export class PlatformManager {
}
/**
* 检查是否已注册适配器
* @zh 检查是否已注册适配器
* @en Check if adapter is registered
*/
public hasAdapter(): boolean {
return this.adapter !== null;
return this._adapter !== null;
}
/**
* 获取平台适配器信息(用于调试)
* @zh 获取平台适配器信息(用于调试)
* @en Get platform adapter info (for debugging)
*/
public getAdapterInfo(): any {
return this.adapter ? {
name: this.adapter.name,
version: this.adapter.version,
config: this.adapter.getPlatformConfig()
return this._adapter ? {
name: this._adapter.name,
version: this._adapter.version,
config: this._adapter.getPlatformConfig()
} : null;
}
/**
* 检查当前平台是否支持特定功能
* @zh 检查当前平台是否支持特定功能
* @en Check if current platform supports specific feature
*/
public supportsFeature(feature: 'worker' | 'shared-array-buffer' | 'transferable-objects' | 'module-worker'): boolean {
if (!this.adapter) return false;
if (!this._adapter) return false;
const config = this.adapter.getPlatformConfig();
const config = this._adapter.getPlatformConfig();
switch (feature) {
case 'worker':
return this.adapter.isWorkerSupported();
return this._adapter.isWorkerSupported();
case 'shared-array-buffer':
return this.adapter.isSharedArrayBufferSupported();
return this._adapter.isSharedArrayBufferSupported();
case 'transferable-objects':
return config.supportsTransferableObjects;
case 'module-worker':
@@ -90,8 +96,11 @@ export class PlatformManager {
}
/**
* 获取基础的Worker配置信息(不做自动决策)
* 用户应该根据自己的业务需求来配置Worker参数
* @zh 获取基础的Worker配置信息(不做自动决策)
* @en Get basic Worker configuration (no auto-decision)
*
* @zh 用户应该根据自己的业务需求来配置Worker参数
* @en Users should configure Worker parameters based on their business requirements
*/
public getBasicWorkerConfig(): {
platformSupportsWorker: boolean;
@@ -99,7 +108,7 @@ export class PlatformManager {
platformMaxWorkerCount: number;
platformLimitations: any;
} {
if (!this.adapter) {
if (!this._adapter) {
return {
platformSupportsWorker: false,
platformSupportsSharedArrayBuffer: false,
@@ -108,30 +117,29 @@ export class PlatformManager {
};
}
const config = this.adapter.getPlatformConfig();
const config = this._adapter.getPlatformConfig();
return {
platformSupportsWorker: this.adapter.isWorkerSupported(),
platformSupportsSharedArrayBuffer: this.adapter.isSharedArrayBufferSupported(),
platformSupportsWorker: this._adapter.isWorkerSupported(),
platformSupportsSharedArrayBuffer: this._adapter.isSharedArrayBufferSupported(),
platformMaxWorkerCount: config.maxWorkerCount,
platformLimitations: config.limitations || {}
};
}
/**
* 异步获取完整的平台配置信息(包含性能信息)
* @zh 异步获取完整的平台配置信息(包含性能信息)
* @en Async get full platform configuration (includes performance info)
*/
public async getFullPlatformConfig(): Promise<any> {
if (!this.adapter) {
if (!this._adapter) {
throw new Error('平台适配器未注册');
}
// 如果适配器支持异步获取配置,使用异步方法
if (typeof this.adapter.getPlatformConfigAsync === 'function') {
return await this.adapter.getPlatformConfigAsync();
if (typeof this._adapter.getPlatformConfigAsync === 'function') {
return await this._adapter.getPlatformConfigAsync();
}
// 否则返回同步配置
return this.adapter.getPlatformConfig();
return this._adapter.getPlatformConfig();
}
}
+145
View File
@@ -0,0 +1,145 @@
/**
* @zh 全局类型声明 - 用于减少 as any 的使用
* @en Global type declarations - to reduce as any usage
*/
// ============================================================================
// 小游戏平台 API 接口 | Mini-Game Platform API Interfaces
// ============================================================================
/**
* @zh 小游戏平台基础 API 接口
* @en Base interface for mini-game platform APIs
*/
export interface IMiniGamePlatformAPI {
getSystemInfo?: (options?: { success?: (res: unknown) => void }) => void;
getSystemInfoSync?: () => Record<string, unknown>;
createCanvas?: () => HTMLCanvasElement;
createImage?: () => HTMLImageElement;
}
/**
* @zh 微信小游戏 API
* @en WeChat Mini Game API
*/
export interface IWeChatMiniGameAPI extends IMiniGamePlatformAPI {
env?: {
USER_DATA_PATH?: string;
};
getFileSystemManager?: () => unknown;
createInnerAudioContext?: () => unknown;
}
/**
* @zh 字节跳动小游戏 API
* @en ByteDance Mini Game API
*/
export type IByteDanceMiniGameAPI = IMiniGamePlatformAPI;
/**
* @zh 支付宝小游戏 API
* @en Alipay Mini Game API
*/
export type IAlipayMiniGameAPI = IMiniGamePlatformAPI;
/**
* @zh 百度小游戏 API
* @en Baidu Mini Game API
*/
export type IBaiduMiniGameAPI = IMiniGamePlatformAPI;
/**
* @zh 扩展的 globalThis 类型,包含小游戏平台
* @en Extended globalThis type with mini-game platforms
*/
export interface IGlobalThisWithMiniGame {
wx?: IWeChatMiniGameAPI;
tt?: IByteDanceMiniGameAPI;
my?: IAlipayMiniGameAPI;
swan?: IBaiduMiniGameAPI;
}
// ============================================================================
// Chrome 性能 API | Chrome Performance API
// ============================================================================
/**
* @zh Chrome 内存信息接口
* @en Chrome memory info interface
*/
export interface IChromeMemoryInfo {
jsHeapSizeLimit: number;
totalJSHeapSize: number;
usedJSHeapSize: number;
}
/**
* @zh 扩展的 Performance 接口,包含 Chrome 特有 API
* @en Extended Performance interface with Chrome-specific APIs
*/
export interface IPerformanceWithMemory extends Performance {
memory?: IChromeMemoryInfo;
measureUserAgentSpecificMemory?: () => Promise<{
bytes: number;
breakdown: Array<{
bytes: number;
types: string[];
attribution: Array<{ scope: string; container?: unknown }>;
}>;
}>;
}
// ============================================================================
// 组件元数据接口 | Component Metadata Interface
// ============================================================================
/**
* @zh SoA 组件类型元数据接口
* @en SoA component type metadata interface
*
* @zh 用于 SoA 存储装饰器附加的元数据
* @en Used for metadata attached by SoA storage decorators
*/
export interface IComponentTypeMetadata {
__enableSoA?: boolean;
__float64Fields?: Set<string>;
__float32Fields?: Set<string>;
__int32Fields?: Set<string>;
__uint32Fields?: Set<string>;
__int16Fields?: Set<string>;
__uint16Fields?: Set<string>;
__int8Fields?: Set<string>;
__uint8Fields?: Set<string>;
__uint8ClampedFields?: Set<string>;
__highPrecisionFields?: Set<string>;
__serializeMapFields?: Set<string>;
__serializeSetFields?: Set<string>;
__serializeArrayFields?: Set<string>;
__deepCopyFields?: Set<string>;
}
/**
* @zh 带元数据的组件构造函数类型
* @en Component constructor type with metadata
*/
export type ComponentTypeWithMetadata<T> = (new () => T) & IComponentTypeMetadata;
// ============================================================================
// 类型守卫辅助函数 | Type Guard Helper Functions
// ============================================================================
/**
* @zh 获取全局小游戏平台对象
* @en Get global mini-game platform objects
*/
export function getGlobalWithMiniGame(): IGlobalThisWithMiniGame {
return globalThis as unknown as IGlobalThisWithMiniGame;
}
/**
* @zh 获取带内存 API 的 performance 对象
* @en Get performance object with memory API
*/
export function getPerformanceWithMemory(): IPerformanceWithMemory {
return performance as IPerformanceWithMemory;
}
+1
View File
@@ -7,6 +7,7 @@ import type { IWorldManagerConfig } from '../ECS';
// 导出TypeScript类型增强工具
export * from './TypeHelpers';
export * from './IUpdatable';
export * from './GlobalTypes';
/**
* 组件接口
@@ -68,33 +68,35 @@ interface WrapInfo {
* 自动性能分析器
*/
export class AutoProfiler {
private static instance: AutoProfiler | null = null;
private config: AutoProfilerConfig;
private static _instance: AutoProfiler | null = null;
private _config: AutoProfilerConfig;
private wrappedObjects: WeakMap<object, Map<string, WrapInfo>> = new WeakMap();
private samplingProfiler: SamplingProfiler | null = null;
private registeredClasses: Map<string, { constructor: Function; category: ProfileCategory }> = new Map();
private constructor(config?: Partial<AutoProfilerConfig>) {
this.config = { ...DEFAULT_CONFIG, ...config };
this._config = { ...DEFAULT_CONFIG, ...config };
}
/**
* 获取单例实例
* @zh 获取单例实例
* @en Get singleton instance
*/
public static getInstance(config?: Partial<AutoProfilerConfig>): AutoProfiler {
if (!AutoProfiler.instance) {
AutoProfiler.instance = new AutoProfiler(config);
if (!AutoProfiler._instance) {
AutoProfiler._instance = new AutoProfiler(config);
}
return AutoProfiler.instance;
return AutoProfiler._instance;
}
/**
* 重置实例
* @zh 重置实例
* @en Reset instance
*/
public static resetInstance(): void {
if (AutoProfiler.instance) {
AutoProfiler.instance.dispose();
AutoProfiler.instance = null;
if (AutoProfiler._instance) {
AutoProfiler._instance.dispose();
AutoProfiler._instance = null;
}
}
@@ -154,17 +156,19 @@ export class AutoProfiler {
}
/**
* 设置启用状态
* @zh 设置启用状态
* @en Set enabled state
*/
public setEnabled(enabled: boolean): void {
this.config.enabled = enabled;
this._config.enabled = enabled;
if (!enabled && this.samplingProfiler) {
this.samplingProfiler.stop();
}
}
/**
* 注册类以进行自动分析
* @zh 注册类以进行自动分析
* @en Register class for automatic profiling
*/
public registerClass<T extends new (...args: any[]) => any>(
constructor: T,
@@ -177,11 +181,10 @@ export class AutoProfiler {
// eslint-disable-next-line @typescript-eslint/no-this-alias -- Required for Proxy construct handler
const self = this;
// 创建代理类
const ProxiedClass = new Proxy(constructor, {
construct(target, args, newTarget) {
const instance = Reflect.construct(target, args, newTarget);
if (self.config.enabled) {
if (self._config.enabled) {
self.wrapInstance(instance, name, category);
}
return instance;
@@ -192,14 +195,15 @@ export class AutoProfiler {
}
/**
* 包装对象实例的所有方法
* @zh 包装对象实例的所有方法
* @en Wrap all methods of an object instance
*/
public wrapInstance<T extends object>(
instance: T,
className: string,
category: ProfileCategory = ProfileCategory.Custom
): T {
if (!this.config.enabled) {
if (!this._config.enabled) {
return instance;
}
@@ -211,21 +215,20 @@ export class AutoProfiler {
const wrapInfoMap = new Map<string, WrapInfo>();
this.wrappedObjects.set(instance, wrapInfoMap);
// 获取所有方法(包括原型链上的)
const methodNames = this.getAllMethodNames(instance);
const methodNames = this._getAllMethodNames(instance);
for (const methodName of methodNames) {
if (this.shouldExcludeMethod(methodName)) {
if (this._shouldExcludeMethod(methodName)) {
continue;
}
const descriptor = this.getPropertyDescriptor(instance, methodName);
const descriptor = this._getPropertyDescriptor(instance, methodName);
if (!descriptor || typeof descriptor.value !== 'function') {
continue;
}
const original = descriptor.value as Function;
const wrapped = this.createWrappedMethod(original, className, methodName, category);
const wrapped = this._createWrappedMethod(original, className, methodName, category);
wrapInfoMap.set(methodName, {
className,
@@ -245,14 +248,15 @@ export class AutoProfiler {
}
/**
* 包装单个函数
* @zh 包装单个函数
* @en Wrap a single function
*/
public wrapFunction<T extends (...args: any[]) => any>(
fn: T,
name: string,
category: ProfileCategory = ProfileCategory.Custom
): T {
if (!this.config.enabled) return fn;
if (!this._config.enabled) return fn;
// eslint-disable-next-line @typescript-eslint/no-this-alias -- Required for wrapped function closure
const self = this;
@@ -262,24 +266,20 @@ export class AutoProfiler {
try {
const result = fn.apply(this, args);
// 处理 Promise
if (self.config.trackAsync && result instanceof Promise) {
if (self._config.trackAsync && result instanceof Promise) {
return result.finally(() => {
ProfilerSDK.endSample(handle);
});
}
// 同步函数,立即结束采样
ProfilerSDK.endSample(handle);
return result;
} catch (error) {
// 发生错误时也要结束采样
ProfilerSDK.endSample(handle);
throw error;
}
} as T;
// 保留原函数的属性
Object.defineProperty(wrapped, 'name', { value: fn.name || name });
Object.defineProperty(wrapped, 'length', { value: fn.length });
@@ -287,11 +287,12 @@ export class AutoProfiler {
}
/**
* 启动采样分析器
* @zh 启动采样分析器
* @en Start sampling profiler
*/
public startSampling(): void {
if (!this.samplingProfiler) {
this.samplingProfiler = new SamplingProfiler(this.config);
this.samplingProfiler = new SamplingProfiler(this._config);
}
this.samplingProfiler.start();
}
@@ -318,9 +319,10 @@ export class AutoProfiler {
}
/**
* 创建包装后的方法
* @zh 创建包装后的方法
* @en Create wrapped method
*/
private createWrappedMethod(
private _createWrappedMethod(
original: Function,
className: string,
methodName: string,
@@ -329,10 +331,10 @@ export class AutoProfiler {
// eslint-disable-next-line @typescript-eslint/no-this-alias -- Required for wrapped method closure
const self = this;
const fullName = `${className}.${methodName}`;
const minDuration = this.config.minDuration;
const minDuration = this._config.minDuration;
return function(this: any, ...args: any[]): any {
if (!self.config.enabled || !ProfilerSDK.isEnabled()) {
if (!self._config.enabled || !ProfilerSDK.isEnabled()) {
return original.apply(this, args);
}
@@ -342,8 +344,7 @@ export class AutoProfiler {
try {
const result = original.apply(this, args);
// 处理异步方法
if (self.config.trackAsync && result instanceof Promise) {
if (self._config.trackAsync && result instanceof Promise) {
return result.then(
(value) => {
const duration = performance.now() - startTime;
@@ -359,14 +360,12 @@ export class AutoProfiler {
);
}
// 同步方法,检查最小耗时后结束采样
const duration = performance.now() - startTime;
if (duration >= minDuration) {
ProfilerSDK.endSample(handle);
}
return result;
} catch (error) {
// 发生错误时也要结束采样
ProfilerSDK.endSample(handle);
throw error;
}
@@ -374,9 +373,10 @@ export class AutoProfiler {
}
/**
* 获取对象的所有方法名
* @zh 获取对象的所有方法名
* @en Get all method names of an object
*/
private getAllMethodNames(obj: object): string[] {
private _getAllMethodNames(obj: object): string[] {
const methods = new Set<string>();
let current = obj;
@@ -393,9 +393,10 @@ export class AutoProfiler {
}
/**
* 获取属性描述符
* @zh 获取属性描述符
* @en Get property descriptor
*/
private getPropertyDescriptor(obj: object, name: string): PropertyDescriptor | undefined {
private _getPropertyDescriptor(obj: object, name: string): PropertyDescriptor | undefined {
let current = obj;
while (current && current !== Object.prototype) {
const descriptor = Object.getOwnPropertyDescriptor(current, name);
@@ -406,16 +407,15 @@ export class AutoProfiler {
}
/**
* 判断是否应该排除该方法
* @zh 判断是否应该排除该方法
* @en Check if method should be excluded
*/
private shouldExcludeMethod(methodName: string): boolean {
// 排除构造函数和内置方法
private _shouldExcludeMethod(methodName: string): boolean {
if (methodName === 'constructor' || methodName.startsWith('__')) {
return true;
}
// 检查排除模式
for (const pattern of this.config.excludePatterns) {
for (const pattern of this._config.excludePatterns) {
if (pattern.test(methodName)) {
return true;
}
+68 -56
View File
@@ -32,9 +32,9 @@ function generateId(): string {
* 性能分析器 SDK
*/
export class ProfilerSDK {
private static instance: ProfilerSDK | null = null;
private static _instance: ProfilerSDK | null = null;
private config: ProfilerConfig;
private _config: ProfilerConfig;
private currentFrame: ProfileFrame | null = null;
private frameHistory: ProfileFrame[] = [];
private frameNumber = 0;
@@ -48,29 +48,31 @@ export class ProfilerSDK {
private performanceObserver: PerformanceObserver | null = null;
private constructor(config?: Partial<ProfilerConfig>) {
this.config = { ...DEFAULT_PROFILER_CONFIG, ...config };
if (this.config.detectLongTasks) {
this.setupLongTaskObserver();
this._config = { ...DEFAULT_PROFILER_CONFIG, ...config };
if (this._config.detectLongTasks) {
this._setupLongTaskObserver();
}
}
/**
* 获取单例实例
* @zh 获取单例实例
* @en Get singleton instance
*/
public static getInstance(config?: Partial<ProfilerConfig>): ProfilerSDK {
if (!ProfilerSDK.instance) {
ProfilerSDK.instance = new ProfilerSDK(config);
if (!ProfilerSDK._instance) {
ProfilerSDK._instance = new ProfilerSDK(config);
}
return ProfilerSDK.instance;
return ProfilerSDK._instance;
}
/**
* 重置实例(测试用)
* @zh 重置实例(测试用)
* @en Reset instance (for testing)
*/
public static resetInstance(): void {
if (ProfilerSDK.instance) {
ProfilerSDK.instance.dispose();
ProfilerSDK.instance = null;
if (ProfilerSDK._instance) {
ProfilerSDK._instance.dispose();
ProfilerSDK._instance = null;
}
}
@@ -152,10 +154,11 @@ export class ProfilerSDK {
}
/**
* 检查是否启用
* @zh 检查是否启用
* @en Check if enabled
*/
public static isEnabled(): boolean {
return ProfilerSDK.getInstance().config.enabled;
return ProfilerSDK.getInstance()._config.enabled;
}
/**
@@ -187,10 +190,11 @@ export class ProfilerSDK {
}
/**
* 开始采样
* @zh 开始采样
* @en Begin sample
*/
public beginSample(name: string, category: ProfileCategory = ProfileCategory.Custom): SampleHandle | null {
if (!this.config.enabled || !this.config.enabledCategories.has(category)) {
if (!this._config.enabled || !this._config.enabledCategories.has(category)) {
return null;
}
@@ -198,7 +202,7 @@ export class ProfilerSDK {
? this.sampleStack[this.sampleStack.length - 1]
: undefined;
if (parentHandle && this.sampleStack.length >= this.config.maxSampleDepth) {
if (parentHandle && this.sampleStack.length >= this._config.maxSampleDepth) {
return null;
}
@@ -218,10 +222,11 @@ export class ProfilerSDK {
}
/**
* 结束采样
* @zh 结束采样
* @en End sample
*/
public endSample(handle: SampleHandle): void {
if (!this.config.enabled || !this.activeSamples.has(handle.id)) {
if (!this._config.enabled || !this.activeSamples.has(handle.id)) {
return;
}
@@ -249,7 +254,7 @@ export class ProfilerSDK {
this.currentFrame.samples.push(sample);
}
this.updateCallGraph(handle.name, handle.category, duration, parentHandle);
this._updateCallGraph(handle.name, handle.category, duration, parentHandle);
this.activeSamples.delete(handle.id);
const stackIndex = this.sampleStack.indexOf(handle);
@@ -291,10 +296,11 @@ export class ProfilerSDK {
}
/**
* 开始帧
* @zh 开始帧
* @en Begin frame
*/
public beginFrame(): void {
if (!this.config.enabled) return;
if (!this._config.enabled) return;
this.frameNumber++;
this.currentFrame = {
@@ -305,28 +311,29 @@ export class ProfilerSDK {
samples: [],
sampleStats: [],
counters: new Map(this.counters),
memory: this.captureMemory(),
memory: this._captureMemory(),
categoryStats: new Map()
};
this.resetFrameCounters();
this._resetFrameCounters();
}
/**
* 结束帧
* @zh 结束帧
* @en End frame
*/
public endFrame(): void {
if (!this.config.enabled || !this.currentFrame) return;
if (!this._config.enabled || !this.currentFrame) return;
this.currentFrame.endTime = performance.now();
this.currentFrame.duration = this.currentFrame.endTime - this.currentFrame.startTime;
this.calculateSampleStats();
this.calculateCategoryStats();
this._calculateSampleStats();
this._calculateCategoryStats();
this.frameHistory.push(this.currentFrame);
while (this.frameHistory.length > this.config.maxFrameHistory) {
while (this.frameHistory.length > this._config.maxFrameHistory) {
this.frameHistory.shift();
}
@@ -335,14 +342,15 @@ export class ProfilerSDK {
}
/**
* 递增计数器
* @zh 递增计数器
* @en Increment counter
*/
public incrementCounter(
name: string,
value: number = 1,
category: ProfileCategory = ProfileCategory.Custom
): void {
if (!this.config.enabled) return;
if (!this._config.enabled) return;
let counter = this.counters.get(name);
if (!counter) {
@@ -365,14 +373,15 @@ export class ProfilerSDK {
}
/**
* 设置仪表值
* @zh 设置仪表值
* @en Set gauge value
*/
public setGauge(
name: string,
value: number,
category: ProfileCategory = ProfileCategory.Custom
): void {
if (!this.config.enabled) return;
if (!this._config.enabled) return;
let counter = this.counters.get(name);
if (!counter) {
@@ -395,12 +404,13 @@ export class ProfilerSDK {
}
/**
* 设置启用状态
* @zh 设置启用状态
* @en Set enabled state
*/
public setEnabled(enabled: boolean): void {
this.config.enabled = enabled;
if (enabled && this.config.detectLongTasks && !this.performanceObserver) {
this.setupLongTaskObserver();
this._config.enabled = enabled;
if (enabled && this._config.detectLongTasks && !this.performanceObserver) {
this._setupLongTaskObserver();
}
}
@@ -428,21 +438,20 @@ export class ProfilerSDK {
: this.frameHistory;
if (frames.length === 0) {
return this.createEmptyReport();
return this._createEmptyReport();
}
const frameTimes = frames.map((f) => f.duration);
const sortedTimes = [...frameTimes].sort((a, b) => a - b);
const aggregatedStats = this.aggregateSampleStats(frames);
const aggregatedStats = this._aggregateSampleStats(frames);
const hotspots = aggregatedStats
.sort((a, b) => b.inclusiveTime - a.inclusiveTime)
.slice(0, 20);
const categoryBreakdown = this.aggregateCategoryStats(frames);
const categoryBreakdown = this._aggregateCategoryStats(frames);
// 根据帧历史重新计算 callGraph(不使用全局累积的数据)
const callGraph = this.buildCallGraphFromFrames(frames);
const callGraph = this._buildCallGraphFromFrames(frames);
const firstFrame = frames[0];
const lastFrame = frames[frames.length - 1];
@@ -465,10 +474,13 @@ export class ProfilerSDK {
}
/**
* 从帧历史构建调用图
* 注意:totalTime 存储的是平均耗时(总耗时/调用次数),而不是累计总耗时
* @zh 从帧历史构建调用图
* @en Build call graph from frame history
*
* @zh 注意:totalTime 存储的是平均耗时(总耗时/调用次数),而不是累计总耗时
* @en Note: totalTime stores average time (total/count), not cumulative total
*/
private buildCallGraphFromFrames(frames: ProfileFrame[]): Map<string, CallGraphNode> {
private _buildCallGraphFromFrames(frames: ProfileFrame[]): Map<string, CallGraphNode> {
// 临时存储累计数据
const tempData = new Map<string, {
category: ProfileCategory;
@@ -597,7 +609,7 @@ export class ProfilerSDK {
this.reset();
}
private captureMemory(): MemorySnapshot {
private _captureMemory(): MemorySnapshot {
const now = performance.now();
let usedHeapSize = 0;
let totalHeapSize = 0;
@@ -632,7 +644,7 @@ export class ProfilerSDK {
};
}
private resetFrameCounters(): void {
private _resetFrameCounters(): void {
for (const counter of this.counters.values()) {
if (counter.type === 'counter') {
counter.value = 0;
@@ -640,7 +652,7 @@ export class ProfilerSDK {
}
}
private calculateSampleStats(): void {
private _calculateSampleStats(): void {
if (!this.currentFrame) return;
const sampleMap = new Map<string, ProfileSampleStats>();
@@ -701,7 +713,7 @@ export class ProfilerSDK {
.sort((a, b) => b.inclusiveTime - a.inclusiveTime);
}
private calculateCategoryStats(): void {
private _calculateCategoryStats(): void {
if (!this.currentFrame) return;
const categoryMap = new Map<ProfileCategory, { totalTime: number; sampleCount: number }>();
@@ -727,7 +739,7 @@ export class ProfilerSDK {
}
}
private updateCallGraph(
private _updateCallGraph(
name: string,
category: ProfileCategory,
duration: number,
@@ -779,7 +791,7 @@ export class ProfilerSDK {
}
}
private aggregateSampleStats(frames: ProfileFrame[]): ProfileSampleStats[] {
private _aggregateSampleStats(frames: ProfileFrame[]): ProfileSampleStats[] {
const aggregated = new Map<string, ProfileSampleStats>();
for (const frame of frames) {
@@ -810,7 +822,7 @@ export class ProfilerSDK {
return Array.from(aggregated.values());
}
private aggregateCategoryStats(frames: ProfileFrame[]): Map<ProfileCategory, {
private _aggregateCategoryStats(frames: ProfileFrame[]): Map<ProfileCategory, {
totalTime: number;
averageTime: number;
percentOfTotal: number;
@@ -843,13 +855,13 @@ export class ProfilerSDK {
return result;
}
private setupLongTaskObserver(): void {
private _setupLongTaskObserver(): void {
if (typeof PerformanceObserver === 'undefined') return;
try {
this.performanceObserver = new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
if (entry.duration > this.config.longTaskThreshold) {
if (entry.duration > this._config.longTaskThreshold) {
this.longTasks.push({
startTime: entry.startTime,
duration: entry.duration,
@@ -869,7 +881,7 @@ export class ProfilerSDK {
}
}
private createEmptyReport(): ProfileReport {
private _createEmptyReport(): ProfileReport {
return {
startTime: 0,
endTime: 0,
+15 -5
View File
@@ -34,17 +34,27 @@
],
"author": "ESEngine Team",
"license": "MIT",
"dependencies": {
"@esengine/ecs-framework": "workspace:*",
"@esengine/ecs-framework-math": "workspace:*",
"@esengine/engine-core": "workspace:*",
"@esengine/asset-system": "workspace:*",
"@esengine/material-system": "workspace:*"
},
"peerDependencies": {
"@esengine/sprite": "workspace:*",
"@esengine/camera": "workspace:*"
},
"peerDependenciesMeta": {
"@esengine/sprite": { "optional": true },
"@esengine/camera": { "optional": true }
},
"optionalDependencies": {
"es-engine": "file:../engine/pkg"
},
"devDependencies": {
"@esengine/ecs-framework": "workspace:*",
"@esengine/ecs-framework-math": "workspace:*",
"@esengine/engine-core": "workspace:*",
"@esengine/sprite": "workspace:*",
"@esengine/camera": "workspace:*",
"@esengine/asset-system": "workspace:*",
"@esengine/material-system": "workspace:*",
"tsup": "^8.5.1",
"typescript": "^5.8.0",
"rimraf": "^5.0.0"
+1
View File
@@ -1104,6 +1104,7 @@ dependencies = [
"notify-debouncer-mini",
"once_cell",
"qrcode",
"regex",
"serde",
"serde_json",
"tauri",
+1
View File
@@ -36,6 +36,7 @@ qrcode = "0.14"
image = "0.25"
notify = "7.0"
notify-debouncer-mini = "0.5"
regex = "1"
[profile.dev]
incremental = true
@@ -171,33 +171,148 @@ pub async fn check_environment() -> Result<EnvironmentCheckResult, String> {
/// Check esbuild availability and get its status.
/// 检查 esbuild 可用性并获取其状态。
///
/// Only checks for globally installed esbuild (via npm -g).
/// 只检测通过 npm 全局安装的 esbuild
/// Checks multiple sources: bundled, local node_modules, pnpm, npx, global.
/// 检查多个来源:内置、本地 node_modules、pnpm、npx、全局
fn check_esbuild_status() -> ToolStatus {
let global_esbuild = if cfg!(windows) { "esbuild.cmd" } else { "esbuild" };
match get_esbuild_version(global_esbuild) {
Ok(version) => {
// Try to find esbuild from any available source
// 尝试从任何可用来源查找 esbuild
match find_esbuild_with_source(None) {
Ok(info) => {
let display_path = if info.prefix_args.is_empty() {
info.cmd.clone()
} else {
format!("{} {}", info.cmd, info.prefix_args.join(" "))
};
ToolStatus {
available: true,
version: Some(version),
path: Some(global_esbuild.to_string()),
source: Some("global".to_string()),
version: Some(info.version),
path: Some(display_path),
source: Some(info.source),
error: None,
}
}
Err(_) => {
Err(e) => {
ToolStatus {
available: false,
version: None,
path: None,
source: None,
error: Some("esbuild not installed globally. Please install: npm install -g esbuild | 未全局安装 esbuild,请安装: npm install -g esbuild".to_string()),
error: Some(e),
}
}
}
}
/// Esbuild execution info.
/// Esbuild 执行信息。
#[derive(Debug, Clone)]
struct EsbuildInfo {
/// Command to run (e.g., "esbuild.cmd", "pnpm.cmd")
/// 要运行的命令
cmd: String,
/// Prefix args before esbuild args (e.g., ["exec", "esbuild"] for pnpm)
/// esbuild 参数前的前缀参数
prefix_args: Vec<String>,
/// Source of esbuild: "local", "pnpm", "npx", "global"
/// esbuild 来源
source: String,
/// Version string
/// 版本字符串
version: String,
}
/// Find esbuild with source information.
/// 查找 esbuild 并返回来源信息。
///
/// Returns EsbuildInfo on success.
/// 成功时返回 EsbuildInfo。
fn find_esbuild_with_source(project_root: Option<&str>) -> Result<EsbuildInfo, String> {
// 1. Check local node_modules/.bin/esbuild (project-specific)
// 1. 检查本地 node_modules/.bin/esbuild(项目特定)
if let Some(root) = project_root {
let local_esbuild = if cfg!(windows) {
Path::new(root).join("node_modules").join(".bin").join("esbuild.cmd")
} else {
Path::new(root).join("node_modules").join(".bin").join("esbuild")
};
if local_esbuild.exists() {
let path_str = local_esbuild.to_string_lossy().to_string();
if let Ok(version) = get_esbuild_version(&path_str) {
println!("[Compiler] Found local esbuild: {}", path_str);
return Ok(EsbuildInfo {
cmd: path_str,
prefix_args: vec![],
source: "local".to_string(),
version,
});
}
}
}
// 2. Try pnpm exec esbuild (works with pnpm workspaces)
// 2. 尝试 pnpm exec esbuild(支持 pnpm 工作区)
if let Ok(version) = try_package_manager_esbuild("pnpm", &["exec", "esbuild", "--version"], project_root) {
let cmd = if cfg!(windows) { "pnpm.cmd" } else { "pnpm" };
println!("[Compiler] Found esbuild via pnpm");
return Ok(EsbuildInfo {
cmd: cmd.to_string(),
prefix_args: vec!["exec".to_string(), "esbuild".to_string()],
source: "pnpm".to_string(),
version,
});
}
// 3. Try npx esbuild (works with npm projects)
// 3. 尝试 npx esbuild(支持 npm 项目)
if let Ok(version) = try_package_manager_esbuild("npx", &["esbuild", "--version"], project_root) {
let cmd = if cfg!(windows) { "npx.cmd" } else { "npx" };
println!("[Compiler] Found esbuild via npx");
return Ok(EsbuildInfo {
cmd: cmd.to_string(),
prefix_args: vec!["esbuild".to_string()],
source: "npx".to_string(),
version,
});
}
// 4. Fall back to global esbuild
// 4. 回退到全局 esbuild
let global_esbuild = if cfg!(windows) { "esbuild.cmd" } else { "esbuild" };
if let Ok(version) = get_esbuild_version(global_esbuild) {
println!("[Compiler] Found global esbuild");
return Ok(EsbuildInfo {
cmd: global_esbuild.to_string(),
prefix_args: vec![],
source: "global".to_string(),
version,
});
}
Err("esbuild not found. Install locally (pnpm add -D esbuild) or globally (npm i -g esbuild) | 未找到 esbuild,请本地安装 (pnpm add -D esbuild) 或全局安装 (npm i -g esbuild)".to_string())
}
/// Try to get esbuild version via package manager (pnpm/npx).
/// 尝试通过包管理器获取 esbuild 版本。
fn try_package_manager_esbuild(cmd: &str, args: &[&str], project_root: Option<&str>) -> Result<String, String> {
let cmd_name = if cfg!(windows) { format!("{}.cmd", cmd) } else { cmd.to_string() };
let mut command = Command::new(&cmd_name);
command.args(args);
if let Some(root) = project_root {
command.current_dir(root);
}
match command.output() {
Ok(output) if output.status.success() => {
let version = String::from_utf8_lossy(&output.stdout).trim().to_string();
Ok(version)
}
_ => Err(format!("{} esbuild not available", cmd))
}
}
/// Get esbuild version.
/// 获取 esbuild 版本。
fn get_esbuild_version(esbuild_path: &str) -> Result<String, String> {
@@ -224,11 +339,11 @@ fn get_esbuild_version(esbuild_path: &str) -> Result<String, String> {
/// Compilation result | 编译结果
#[command]
pub async fn compile_typescript(options: CompileOptions) -> Result<CompileResult, String> {
// Check if esbuild is available | 查 esbuild 是否可用
let esbuild_path = find_esbuild(&options.project_root)?;
// Find esbuild with execution info | 查 esbuild 并获取执行信息
let esbuild_info = find_esbuild_with_source(Some(&options.project_root))?;
// Build esbuild arguments | 构建 esbuild 参数
let mut args = vec![
let mut esbuild_args = vec![
options.entry_path.clone(),
"--bundle".to_string(),
format!("--outfile={}", options.output_path),
@@ -239,38 +354,42 @@ pub async fn compile_typescript(options: CompileOptions) -> Result<CompileResult
// Add source map option | 添加 source map 选项
if options.source_map {
args.push("--sourcemap".to_string());
esbuild_args.push("--sourcemap".to_string());
}
// Add minify option | 添加压缩选项
if options.minify {
args.push("--minify".to_string());
esbuild_args.push("--minify".to_string());
}
// Add global name for IIFE format | 添加 IIFE 格式的全局名称
if let Some(ref global_name) = options.global_name {
args.push(format!("--global-name={}", global_name));
esbuild_args.push(format!("--global-name={}", global_name));
}
// Add external dependencies | 添加外部依赖
for external in &options.external {
args.push(format!("--external:{}", external));
esbuild_args.push(format!("--external:{}", external));
}
// Add module aliases | 添加模块别名
if let Some(ref aliases) = options.alias {
for (from, to) in aliases {
args.push(format!("--alias:{}={}", from, to));
esbuild_args.push(format!("--alias:{}={}", from, to));
}
}
// Combine prefix args with esbuild args | 合并前缀参数和 esbuild 参数
let mut all_args: Vec<String> = esbuild_info.prefix_args.clone();
all_args.extend(esbuild_args);
// Build full command string for error reporting | 构建完整命令字符串用于错误报告
let cmd_str = format!("{} {}", esbuild_path, args.join(" "));
println!("[Compiler] Running: {}", cmd_str);
let cmd_str = format!("{} {}", esbuild_info.cmd, all_args.join(" "));
println!("[Compiler] Running: {} (source: {})", cmd_str, esbuild_info.source);
// Run esbuild | 运行 esbuild
let output = Command::new(&esbuild_path)
.args(&args)
let output = Command::new(&esbuild_info.cmd)
.args(&all_args)
.current_dir(&options.project_root)
.output()
.map_err(|e| format!("Failed to run esbuild | 运行 esbuild 失败: {}", e))?;
@@ -324,6 +443,229 @@ pub async fn compile_typescript(options: CompileOptions) -> Result<CompileResult
}
}
/// Type check options.
/// 类型检查选项。
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct TypeCheckOptions {
/// Project root directory | 项目根目录
pub project_root: String,
/// TypeScript config path (optional) | TypeScript 配置路径(可选)
pub tsconfig_path: Option<String>,
/// Files to check (optional, if not set checks whole project)
/// 要检查的文件(可选,如果未设置则检查整个项目)
pub files: Option<Vec<String>>,
}
/// Type check result.
/// 类型检查结果。
#[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct TypeCheckResult {
/// Whether type check passed | 类型检查是否通过
pub success: bool,
/// Type errors | 类型错误
pub errors: Vec<CompileError>,
/// Duration in milliseconds | 耗时(毫秒)
pub duration_ms: u64,
}
/// Check TypeScript types using tsc.
/// 使用 tsc 检查 TypeScript 类型。
///
/// Runs tsc --noEmit to check types without generating output files.
/// 运行 tsc --noEmit 检查类型但不生成输出文件。
#[command]
pub async fn check_types(options: TypeCheckOptions) -> Result<TypeCheckResult, String> {
let start = std::time::Instant::now();
// Find tsc executable | 查找 tsc 可执行文件
let tsc_info = find_tsc_with_source(Some(&options.project_root))?;
// Build tsc arguments | 构建 tsc 参数
let mut tsc_args: Vec<String> = tsc_info.prefix_args.clone();
tsc_args.push("--noEmit".to_string());
tsc_args.push("--pretty".to_string());
tsc_args.push("false".to_string());
// Use project tsconfig if specified | 使用项目 tsconfig(如果指定)
if let Some(ref tsconfig) = options.tsconfig_path {
tsc_args.push("--project".to_string());
tsc_args.push(tsconfig.clone());
}
// Add specific files if provided | 添加特定文件(如果提供)
if let Some(ref files) = options.files {
for file in files {
tsc_args.push(file.clone());
}
}
let cmd_str = format!("{} {}", tsc_info.cmd, tsc_args.join(" "));
println!("[TypeCheck] Running: {} (source: {})", cmd_str, tsc_info.source);
// Run tsc | 运行 tsc
let output = Command::new(&tsc_info.cmd)
.args(&tsc_args)
.current_dir(&options.project_root)
.output()
.map_err(|e| format!("Failed to run tsc | 运行 tsc 失败: {}", e))?;
let duration_ms = start.elapsed().as_millis() as u64;
if output.status.success() {
println!("[TypeCheck] Type check passed in {}ms", duration_ms);
Ok(TypeCheckResult {
success: true,
errors: vec![],
duration_ms,
})
} else {
let stdout = String::from_utf8_lossy(&output.stdout);
let stderr = String::from_utf8_lossy(&output.stderr);
println!("[TypeCheck] Type check failed in {}ms", duration_ms);
// Parse tsc output | 解析 tsc 输出
let errors = parse_tsc_errors(&stdout, &stderr);
Ok(TypeCheckResult {
success: false,
errors,
duration_ms,
})
}
}
/// TSC execution info (similar to EsbuildInfo).
/// TSC 执行信息。
#[derive(Debug, Clone)]
struct TscInfo {
cmd: String,
prefix_args: Vec<String>,
source: String,
}
/// Find tsc executable.
/// 查找 tsc 可执行文件。
fn find_tsc_with_source(project_root: Option<&str>) -> Result<TscInfo, String> {
// 1. Check local node_modules/.bin/tsc
if let Some(root) = project_root {
let local_tsc = if cfg!(windows) {
Path::new(root).join("node_modules").join(".bin").join("tsc.cmd")
} else {
Path::new(root).join("node_modules").join(".bin").join("tsc")
};
if local_tsc.exists() {
let path_str = local_tsc.to_string_lossy().to_string();
println!("[TypeCheck] Found local tsc: {}", path_str);
return Ok(TscInfo {
cmd: path_str,
prefix_args: vec![],
source: "local".to_string(),
});
}
}
// 2. Try pnpm exec tsc
if try_tsc_via_package_manager("pnpm", &["exec", "tsc", "--version"], project_root).is_ok() {
let cmd = if cfg!(windows) { "pnpm.cmd" } else { "pnpm" };
println!("[TypeCheck] Found tsc via pnpm");
return Ok(TscInfo {
cmd: cmd.to_string(),
prefix_args: vec!["exec".to_string(), "tsc".to_string()],
source: "pnpm".to_string(),
});
}
// 3. Try npx tsc
if try_tsc_via_package_manager("npx", &["tsc", "--version"], project_root).is_ok() {
let cmd = if cfg!(windows) { "npx.cmd" } else { "npx" };
println!("[TypeCheck] Found tsc via npx");
return Ok(TscInfo {
cmd: cmd.to_string(),
prefix_args: vec!["tsc".to_string()],
source: "npx".to_string(),
});
}
// 4. Try global tsc
let global_tsc = if cfg!(windows) { "tsc.cmd" } else { "tsc" };
if Command::new(global_tsc).arg("--version").output().map(|o| o.status.success()).unwrap_or(false) {
println!("[TypeCheck] Found global tsc");
return Ok(TscInfo {
cmd: global_tsc.to_string(),
prefix_args: vec![],
source: "global".to_string(),
});
}
Err("TypeScript not found. Install locally (pnpm add -D typescript) or globally (npm i -g typescript) | 未找到 TypeScript,请本地安装 (pnpm add -D typescript) 或全局安装 (npm i -g typescript)".to_string())
}
/// Try to run tsc via package manager.
/// 尝试通过包管理器运行 tsc。
fn try_tsc_via_package_manager(cmd: &str, args: &[&str], project_root: Option<&str>) -> Result<(), String> {
let cmd_name = if cfg!(windows) { format!("{}.cmd", cmd) } else { cmd.to_string() };
let mut command = Command::new(&cmd_name);
command.args(args);
if let Some(root) = project_root {
command.current_dir(root);
}
match command.output() {
Ok(output) if output.status.success() => Ok(()),
_ => Err(format!("{} tsc not available", cmd))
}
}
/// Parse tsc error output.
/// 解析 tsc 错误输出。
fn parse_tsc_errors(stdout: &str, stderr: &str) -> Vec<CompileError> {
let mut errors = Vec::new();
let combined = format!("{}\n{}", stdout, stderr);
// tsc output format: file(line,col): error TSxxxx: message
// tsc 输出格式: 文件(行,列): error TSxxxx: 消息
let re = regex::Regex::new(r"(.+?)\((\d+),(\d+)\):\s*error\s+TS\d+:\s*(.+)").ok();
for line in combined.lines() {
if let Some(ref regex) = re {
if let Some(caps) = regex.captures(line) {
errors.push(CompileError {
file: Some(caps.get(1).map_or("", |m| m.as_str()).to_string()),
line: caps.get(2).and_then(|m| m.as_str().parse().ok()),
column: caps.get(3).and_then(|m| m.as_str().parse().ok()),
message: caps.get(4).map_or("", |m| m.as_str()).to_string(),
});
}
} else if line.contains("error TS") {
// Fallback: just capture the error line
errors.push(CompileError {
message: line.to_string(),
file: None,
line: None,
column: None,
});
}
}
// If no errors parsed but output is not empty, add raw output
if errors.is_empty() && !combined.trim().is_empty() {
errors.push(CompileError {
message: combined.trim().to_string(),
file: None,
line: None,
column: None,
});
}
errors
}
/// Watch for file changes in scripts directory.
/// 监视脚本目录中的文件变更。
///
@@ -695,26 +1037,13 @@ pub async fn stop_watch_scripts(
Ok(())
}
/// Find esbuild executable path.
/// 查找 esbuild 可执行文件路径。
/// Find esbuild executable path (deprecated, use find_esbuild_with_source).
/// 查找 esbuild 可执行文件路径(已弃用,使用 find_esbuild_with_source
///
/// Only uses globally installed esbuild (npm -g).
/// 只使用全局安装的 esbuild (npm -g)
fn find_esbuild(_project_root: &str) -> Result<String, String> {
let global_esbuild = if cfg!(windows) { "esbuild.cmd" } else { "esbuild" };
// Check if global esbuild exists | 检查全局 esbuild 是否存在
let check = Command::new(global_esbuild)
.arg("--version")
.output();
match check {
Ok(output) if output.status.success() => {
println!("[Compiler] Using global esbuild");
Ok(global_esbuild.to_string())
},
_ => Err("esbuild not installed globally. Please install: npm install -g esbuild | 未全局安装 esbuild,请安装: npm install -g esbuild".to_string())
}
/// Kept for backward compatibility - returns just the command string.
/// 保留用于向后兼容 - 仅返回命令字符串
fn find_esbuild(project_root: &str) -> Result<EsbuildInfo, String> {
find_esbuild_with_source(Some(project_root))
}
/// Parse esbuild error output.
@@ -94,6 +94,7 @@ fn main() {
commands::generate_qrcode,
// User code compilation | 用户代码编译
commands::compile_typescript,
commands::check_types,
commands::watch_scripts,
commands::watch_assets,
commands::stop_watch_scripts,
+30 -5
View File
@@ -1,4 +1,4 @@
import React, { useState, useEffect, useRef, useCallback } from 'react';
import React, { useState, useEffect, useRef, useCallback, useMemo } from 'react';
import * as ReactDOM from 'react-dom';
import * as ReactJSXRuntime from 'react/jsx-runtime';
import { Core, createLogger, Scene } from '@esengine/ecs-framework';
@@ -7,9 +7,9 @@ import { getProfilerService } from './services/getService';
// 将 React 暴露到全局,供动态加载的插件使用
// editor-runtime.js 将 React 设为 external,需要从全局获取
(window as any).React = React;
(window as any).ReactDOM = ReactDOM;
(window as any).ReactJSXRuntime = ReactJSXRuntime;
window.React = React;
window.ReactDOM = ReactDOM;
window.ReactJSXRuntime = ReactJSXRuntime;
import {
PluginManager,
UIRegistry,
@@ -37,7 +37,6 @@ import { ProjectCreationWizard } from './components/ProjectCreationWizard';
import { SceneHierarchy } from './components/SceneHierarchy';
import { ContentBrowser } from './components/ContentBrowser';
import { Inspector } from './components/inspectors/Inspector';
import { AssetBrowser } from './components/AssetBrowser';
import { Viewport } from './components/Viewport';
import { AdvancedProfilerWindow } from './components/AdvancedProfilerWindow';
import { RenderDebugPanel } from './components/debug/RenderDebugPanel';
@@ -66,6 +65,7 @@ import { CompilerConfigDialog } from './components/CompilerConfigDialog';
import { checkForUpdatesOnStartup } from './utils/updater';
import { useLocale } from './hooks/useLocale';
import { useStoreSubscriptions } from './hooks/useStoreSubscriptions';
import { EditorServicesProvider, type EditorServices } from './contexts';
import { en, zh, es } from './locales';
import type { Locale } from '@esengine/editor-core';
import { UserCodeService } from '@esengine/editor-core';
@@ -163,6 +163,29 @@ function App() {
const [commandManager] = useState(() => new CommandManager());
const { t, locale, changeLocale } = useLocale();
// 编辑器服务对象(用于 Context 传递)| Editor services object (for Context)
const editorServices = useMemo<EditorServices>(() => ({
entityStore: entityStoreRef.current,
messageHub: messageHubRef.current,
commandManager,
sceneManager: sceneManagerRef.current,
projectService: projectServiceRef.current,
pluginManager: pluginManagerRef.current,
inspectorRegistry: inspectorRegistryRef.current,
uiRegistry: uiRegistryRef.current,
settingsRegistry: settingsRegistryRef.current,
buildService: buildServiceRef.current,
logService: logServiceRef.current,
notification: notificationRef.current,
dialog: dialogRef.current,
projectPath: currentProjectPath,
}), [
commandManager,
currentProjectPath,
// 注意: refs 不会变化,但为了初始化后更新需要依赖 initialized
initialized,
]);
// Play 模式状态(用于层级面板实时同步)
// Play mode state (for hierarchy panel real-time sync)
const [isPlaying, setIsPlaying] = useState(false);
@@ -1322,6 +1345,7 @@ function App() {
const projectName = currentProjectPath ? currentProjectPath.split(/[\\/]/).pop() : 'Untitled';
return (
<EditorServicesProvider services={editorServices}>
<div className="editor-container">
{!isEditorFullscreen && (
<>
@@ -1511,6 +1535,7 @@ function App() {
/>
)}
</div>
</EditorServicesProvider>
);
}
@@ -1,2 +1 @@
export * from './commands';
export * from './state';
@@ -1,88 +0,0 @@
import { create } from 'zustand';
/**
* 编辑器交互状态
* 管理编辑器的交互状态(连接、框选、菜单等)
*/
interface EditorState {
/**
* 正在连接的源节点ID
*/
connectingFrom: string | null;
/**
* 正在连接的源属性
*/
connectingFromProperty: string | null;
/**
* 连接目标位置(鼠标位置)
*/
connectingToPos: { x: number; y: number } | null;
/**
* 是否正在框选
*/
isBoxSelecting: boolean;
/**
* 框选起始位置
*/
boxSelectStart: { x: number; y: number } | null;
/**
* 框选结束位置
*/
boxSelectEnd: { x: number; y: number } | null;
// Actions
setConnectingFrom: (nodeId: string | null) => void;
setConnectingFromProperty: (propertyName: string | null) => void;
setConnectingToPos: (pos: { x: number; y: number } | null) => void;
clearConnecting: () => void;
setIsBoxSelecting: (isSelecting: boolean) => void;
setBoxSelectStart: (pos: { x: number; y: number } | null) => void;
setBoxSelectEnd: (pos: { x: number; y: number } | null) => void;
clearBoxSelect: () => void;
}
/**
* Editor Store
*/
export const useEditorStore = create<EditorState>((set) => ({
connectingFrom: null,
connectingFromProperty: null,
connectingToPos: null,
isBoxSelecting: false,
boxSelectStart: null,
boxSelectEnd: null,
setConnectingFrom: (nodeId: string | null) => set({ connectingFrom: nodeId }),
setConnectingFromProperty: (propertyName: string | null) =>
set({ connectingFromProperty: propertyName }),
setConnectingToPos: (pos: { x: number; y: number } | null) => set({ connectingToPos: pos }),
clearConnecting: () =>
set({
connectingFrom: null,
connectingFromProperty: null,
connectingToPos: null
}),
setIsBoxSelecting: (isSelecting: boolean) => set({ isBoxSelecting: isSelecting }),
setBoxSelectStart: (pos: { x: number; y: number } | null) => set({ boxSelectStart: pos }),
setBoxSelectEnd: (pos: { x: number; y: number } | null) => set({ boxSelectEnd: pos }),
clearBoxSelect: () =>
set({
isBoxSelecting: false,
boxSelectStart: null,
boxSelectEnd: null
})
}));
@@ -1,131 +0,0 @@
import { create } from 'zustand';
/**
* UI 状态
* 管理UI相关的状态(选中、拖拽、画布)
*/
interface UIState {
/**
* 选中的节点ID列表
*/
selectedNodeIds: string[];
/**
* 正在拖拽的节点ID
*/
draggingNodeId: string | null;
/**
* 拖拽起始位置映射
*/
dragStartPositions: Map<string, { x: number; y: number }>;
/**
* 是否正在拖拽节点
*/
isDraggingNode: boolean;
/**
* 拖拽偏移量
*/
dragDelta: { dx: number; dy: number };
/**
* 画布偏移
*/
canvasOffset: { x: number; y: number };
/**
* 画布缩放
*/
canvasScale: number;
/**
* 是否正在平移画布
*/
isPanning: boolean;
/**
* 平移起始位置
*/
panStart: { x: number; y: number };
// Actions
setSelectedNodeIds: (nodeIds: string[]) => void;
toggleNodeSelection: (nodeId: string) => void;
clearSelection: () => void;
startDragging: (nodeId: string, startPositions: Map<string, { x: number; y: number }>) => void;
stopDragging: () => void;
setIsDraggingNode: (isDragging: boolean) => void;
setDragDelta: (delta: { dx: number; dy: number }) => void;
setCanvasOffset: (offset: { x: number; y: number }) => void;
setCanvasScale: (scale: number) => void;
setIsPanning: (isPanning: boolean) => void;
setPanStart: (panStart: { x: number; y: number }) => void;
resetView: () => void;
}
/**
* UI Store
*/
export const useUIStore = create<UIState>((set, get) => ({
selectedNodeIds: [],
draggingNodeId: null,
dragStartPositions: new Map(),
isDraggingNode: false,
dragDelta: { dx: 0, dy: 0 },
canvasOffset: { x: 0, y: 0 },
canvasScale: 1,
isPanning: false,
panStart: { x: 0, y: 0 },
setSelectedNodeIds: (nodeIds: string[]) => set({ selectedNodeIds: nodeIds }),
toggleNodeSelection: (nodeId: string) => {
const { selectedNodeIds } = get();
if (selectedNodeIds.includes(nodeId)) {
set({ selectedNodeIds: selectedNodeIds.filter((id) => id !== nodeId) });
} else {
set({ selectedNodeIds: [...selectedNodeIds, nodeId] });
}
},
clearSelection: () => set({ selectedNodeIds: [] }),
startDragging: (nodeId: string, startPositions: Map<string, { x: number; y: number }>) =>
set({
draggingNodeId: nodeId,
dragStartPositions: startPositions,
isDraggingNode: true
}),
stopDragging: () =>
set({
draggingNodeId: null,
dragStartPositions: new Map(),
isDraggingNode: false,
dragDelta: { dx: 0, dy: 0 }
}),
setIsDraggingNode: (isDragging: boolean) => set({ isDraggingNode: isDragging }),
setDragDelta: (delta: { dx: number; dy: number }) => set({ dragDelta: delta }),
setCanvasOffset: (offset: { x: number; y: number }) => set({ canvasOffset: offset }),
setCanvasScale: (scale: number) => set({ canvasScale: scale }),
setIsPanning: (isPanning: boolean) => set({ isPanning }),
setPanStart: (panStart: { x: number; y: number }) => set({ panStart }),
resetView: () =>
set({
canvasOffset: { x: 0, y: 0 },
canvasScale: 1,
isPanning: false
})
}));
@@ -1,2 +0,0 @@
export { useUIStore } from './UIStore';
export { useEditorStore } from './EditorStore';
@@ -1,22 +0,0 @@
/**
* Asset Browser - 资产浏览器
* 包装 ContentBrowser 组件,保持向后兼容
*/
import { ContentBrowser } from './ContentBrowser';
interface AssetBrowserProps {
projectPath: string | null;
locale: string;
onOpenScene?: (scenePath: string) => void;
}
export function AssetBrowser({ projectPath, locale, onOpenScene }: AssetBrowserProps) {
return (
<ContentBrowser
projectPath={projectPath}
locale={locale}
onOpenScene={onOpenScene}
/>
);
}
@@ -1,141 +0,0 @@
import { useState, useEffect } from 'react';
import { RefreshCw, Folder } from 'lucide-react';
import { TauriAPI } from '../api/tauri';
interface AssetPickerProps {
value: string;
onChange: (value: string) => void;
projectPath: string | null;
filter?: 'btree' | 'ecs';
label?: string;
}
/**
* 资产选择器组件
* 用于选择项目中的资产文件
*/
export function AssetPicker({ value, onChange, projectPath, filter = 'btree', label }: AssetPickerProps) {
const [assets, setAssets] = useState<string[]>([]);
const [loading, setLoading] = useState(false);
useEffect(() => {
if (projectPath) {
loadAssets();
}
}, [projectPath]);
const loadAssets = async () => {
if (!projectPath) return;
setLoading(true);
try {
if (filter === 'btree') {
const btrees = await TauriAPI.scanBehaviorTrees(projectPath);
setAssets(btrees);
}
} catch (error) {
console.error('Failed to load assets:', error);
setAssets([]);
} finally {
setLoading(false);
}
};
const handleBrowse = async () => {
try {
if (filter === 'btree') {
const path = await TauriAPI.openBehaviorTreeDialog();
if (path && projectPath) {
const behaviorsPath = `${projectPath}\\.ecs\\behaviors\\`.replace(/\\/g, '\\\\');
const relativePath = path.replace(behaviorsPath, '')
.replace(/\\/g, '/')
.replace('.btree', '');
onChange(relativePath);
await loadAssets();
}
}
} catch (error) {
console.error('Failed to browse asset:', error);
}
};
return (
<div style={{ display: 'flex', flexDirection: 'column', gap: '4px' }}>
{label && (
<label style={{ fontSize: '11px', color: '#aaa', fontWeight: '500' }}>
{label}
</label>
)}
<div style={{ display: 'flex', gap: '4px', alignItems: 'center' }}>
<select
value={value}
onChange={(e) => onChange(e.target.value)}
disabled={loading || !projectPath}
style={{
flex: 1,
padding: '4px 8px',
backgroundColor: '#1e1e1e',
border: '1px solid #3e3e42',
borderRadius: '3px',
color: '#cccccc',
fontSize: '12px',
cursor: loading || !projectPath ? 'not-allowed' : 'pointer'
}}
>
<option value="">{loading ? '加载中...' : '选择资产...'}</option>
{assets.map((asset) => (
<option key={asset} value={asset}>
{asset}
</option>
))}
</select>
<button
onClick={loadAssets}
disabled={loading || !projectPath}
style={{
padding: '4px 8px',
backgroundColor: '#0e639c',
border: 'none',
borderRadius: '3px',
color: '#fff',
cursor: loading || !projectPath ? 'not-allowed' : 'pointer',
display: 'flex',
alignItems: 'center',
opacity: loading || !projectPath ? 0.5 : 1
}}
title="刷新资产列表"
>
<RefreshCw size={14} />
</button>
<button
onClick={handleBrowse}
disabled={loading || !projectPath}
style={{
padding: '4px 8px',
backgroundColor: '#0e639c',
border: 'none',
borderRadius: '3px',
color: '#fff',
cursor: loading || !projectPath ? 'not-allowed' : 'pointer',
display: 'flex',
alignItems: 'center',
opacity: loading || !projectPath ? 0.5 : 1
}}
title="浏览文件..."
>
<Folder size={14} />
</button>
</div>
{!projectPath && (
<div style={{ fontSize: '10px', color: '#ff6b6b', marginTop: '2px' }}>
</div>
)}
{value && assets.length > 0 && !assets.includes(value) && (
<div style={{ fontSize: '10px', color: '#ffa726', marginTop: '2px' }}>
警告: 资产 "{value}"
</div>
)}
</div>
);
}
@@ -1,316 +0,0 @@
import { useState, useEffect } from 'react';
import { X, Folder, Search, ArrowLeft, Grid, List, FileCode } from 'lucide-react';
import { TauriAPI, DirectoryEntry } from '../api/tauri';
import { useLocale } from '../hooks/useLocale';
import '../styles/AssetPickerDialog.css';
interface AssetPickerDialogProps {
projectPath: string;
fileExtension: string;
onSelect: (assetId: string) => void;
onClose: () => void;
locale: string;
/** 资产基础路径(相对于项目根目录),用于计算 assetId */
assetBasePath?: string;
}
interface AssetItem {
name: string;
path: string;
isDir: boolean;
extension?: string;
size?: number;
modified?: number;
}
type ViewMode = 'list' | 'grid';
export function AssetPickerDialog({ projectPath, fileExtension, onSelect, onClose, locale, assetBasePath }: AssetPickerDialogProps) {
const { t, locale: currentLocale } = useLocale();
// 计算实际的资产目录路径
const actualAssetPath = assetBasePath
? `${projectPath}/${assetBasePath}`.replace(/\\/g, '/').replace(/\/+/g, '/')
: projectPath;
const [currentPath, setCurrentPath] = useState(actualAssetPath);
const [assets, setAssets] = useState<AssetItem[]>([]);
const [selectedPath, setSelectedPath] = useState<string | null>(null);
const [loading, setLoading] = useState(false);
const [searchQuery, setSearchQuery] = useState('');
const [viewMode, setViewMode] = useState<ViewMode>('list');
useEffect(() => {
loadAssets(currentPath);
}, [currentPath]);
const loadAssets = async (path: string) => {
setLoading(true);
try {
const entries = await TauriAPI.listDirectory(path);
const assetItems: AssetItem[] = entries
.map((entry: DirectoryEntry) => {
const extension = entry.is_dir ? undefined :
(entry.name.includes('.') ? entry.name.split('.').pop() : undefined);
return {
name: entry.name,
path: entry.path,
isDir: entry.is_dir,
extension,
size: entry.size,
modified: entry.modified
};
})
.filter((item) => item.isDir || item.extension === fileExtension)
.sort((a, b) => {
if (a.isDir === b.isDir) return a.name.localeCompare(b.name);
return a.isDir ? -1 : 1;
});
setAssets(assetItems);
} catch (error) {
console.error('Failed to load assets:', error);
setAssets([]);
} finally {
setLoading(false);
}
};
// 过滤搜索结果
const filteredAssets = assets.filter((item) =>
item.name.toLowerCase().includes(searchQuery.toLowerCase())
);
// 格式化文件大小
const formatFileSize = (bytes?: number): string => {
if (!bytes) return '';
if (bytes < 1024) return `${bytes} B`;
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
};
// 格式化修改时间
const formatDate = (timestamp?: number): string => {
if (!timestamp) return '';
const date = new Date(timestamp * 1000);
const localeMap: Record<string, string> = { zh: 'zh-CN', en: 'en-US', es: 'es-ES' };
return date.toLocaleDateString(localeMap[currentLocale] || 'en-US', {
year: 'numeric',
month: 'short',
day: 'numeric'
});
};
// 返回上级目录
const handleGoBack = () => {
const parentPath = currentPath.split(/[/\\]/).slice(0, -1).join('/');
const minPath = actualAssetPath.replace(/[/\\]$/, '');
if (parentPath && parentPath !== minPath) {
setCurrentPath(parentPath);
} else if (currentPath !== actualAssetPath) {
setCurrentPath(actualAssetPath);
}
};
// 只能返回到资产基础目录,不能再往上
const canGoBack = currentPath !== actualAssetPath;
const handleItemClick = (item: AssetItem) => {
if (item.isDir) {
setCurrentPath(item.path);
} else {
setSelectedPath(item.path);
}
};
const handleItemDoubleClick = (item: AssetItem) => {
if (!item.isDir) {
const assetId = calculateAssetId(item.path);
onSelect(assetId);
}
};
const handleSelect = () => {
if (selectedPath) {
const assetId = calculateAssetId(selectedPath);
onSelect(assetId);
}
};
/**
* 计算资产ID
* 将绝对路径转换为相对于资产基础目录的assetId(不含扩展名)
*/
const calculateAssetId = (absolutePath: string): string => {
const normalized = absolutePath.replace(/\\/g, '/');
const baseNormalized = actualAssetPath.replace(/\\/g, '/');
// 获取相对于资产基础目录的路径
let relativePath = normalized;
if (normalized.startsWith(baseNormalized)) {
relativePath = normalized.substring(baseNormalized.length);
}
// 移除开头的斜杠
relativePath = relativePath.replace(/^\/+/, '');
// 移除文件扩展名
const assetId = relativePath.replace(new RegExp(`\\.${fileExtension}$`), '');
return assetId;
};
const getBreadcrumbs = () => {
const basePathNormalized = actualAssetPath.replace(/\\/g, '/');
const currentPathNormalized = currentPath.replace(/\\/g, '/');
const relative = currentPathNormalized.replace(basePathNormalized, '');
const parts = relative.split('/').filter((p) => p);
// 根路径名称(显示"行为树"或"Assets"
const rootName = assetBasePath
? assetBasePath.split('/').pop() || 'Assets'
: 'Content';
const crumbs = [{ name: rootName, path: actualAssetPath }];
let accPath = actualAssetPath;
for (const part of parts) {
accPath = `${accPath}/${part}`;
crumbs.push({ name: part, path: accPath });
}
return crumbs;
};
const breadcrumbs = getBreadcrumbs();
return (
<div className="asset-picker-overlay" onClick={onClose}>
<div className="asset-picker-dialog" onClick={(e) => e.stopPropagation()}>
<div className="asset-picker-header">
<h3>{t('assetPicker.title')}</h3>
<button className="asset-picker-close" onClick={onClose}>
<X size={18} />
</button>
</div>
<div className="asset-picker-toolbar">
<button
className="toolbar-button"
onClick={handleGoBack}
disabled={!canGoBack}
title={t('assetPicker.back')}
>
<ArrowLeft size={16} />
</button>
<div className="asset-picker-breadcrumb">
{breadcrumbs.map((crumb, index) => (
<span key={crumb.path}>
<span
className="breadcrumb-item"
onClick={() => setCurrentPath(crumb.path)}
>
{crumb.name}
</span>
{index < breadcrumbs.length - 1 && <span className="breadcrumb-separator"> / </span>}
</span>
))}
</div>
<div className="view-mode-buttons">
<button
className={`toolbar-button ${viewMode === 'list' ? 'active' : ''}`}
onClick={() => setViewMode('list')}
title={t('assetPicker.listView')}
>
<List size={16} />
</button>
<button
className={`toolbar-button ${viewMode === 'grid' ? 'active' : ''}`}
onClick={() => setViewMode('grid')}
title={t('assetPicker.gridView')}
>
<Grid size={16} />
</button>
</div>
</div>
<div className="asset-picker-search">
<Search size={16} className="search-icon" />
<input
type="text"
placeholder={t('assetPicker.search')}
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
className="search-input"
/>
{searchQuery && (
<button
className="search-clear"
onClick={() => setSearchQuery('')}
>
<X size={14} />
</button>
)}
</div>
<div className="asset-picker-content">
{loading ? (
<div className="asset-picker-loading">{t('assetPicker.loading')}</div>
) : filteredAssets.length === 0 ? (
<div className="asset-picker-empty">{t('assetPicker.empty')}</div>
) : (
<div className={`asset-picker-list ${viewMode}`}>
{filteredAssets.map((item, index) => (
<div
key={index}
className={`asset-picker-item ${selectedPath === item.path ? 'selected' : ''}`}
onClick={() => handleItemClick(item)}
onDoubleClick={() => handleItemDoubleClick(item)}
>
<div className="asset-icon">
{item.isDir ? (
<Folder size={viewMode === 'grid' ? 32 : 18} style={{ color: '#ffa726' }} />
) : (
<FileCode size={viewMode === 'grid' ? 32 : 18} style={{ color: '#66bb6a' }} />
)}
</div>
<div className="asset-info">
<span className="asset-name">{item.name}</span>
{viewMode === 'list' && !item.isDir && (
<div className="asset-meta">
{item.size && <span className="asset-size">{formatFileSize(item.size)}</span>}
{item.modified && <span className="asset-date">{formatDate(item.modified)}</span>}
</div>
)}
</div>
</div>
))}
</div>
)}
</div>
<div className="asset-picker-footer">
<div className="footer-info">
{t('assetPicker.itemCount', { count: filteredAssets.length })}
</div>
<div className="footer-buttons">
<button className="asset-picker-cancel" onClick={onClose}>
{t('assetPicker.cancel')}
</button>
<button
className="asset-picker-select"
onClick={handleSelect}
disabled={!selectedPath}
>
{t('assetPicker.select')}
</button>
</div>
</div>
</div>
</div>
);
}
@@ -1,541 +0,0 @@
import { useState, useEffect } from 'react';
import { Entity } from '@esengine/ecs-framework';
import { EntityStoreService, MessageHub } from '@esengine/editor-core';
import { PropertyInspector } from './PropertyInspector';
import { FileSearch, ChevronDown, ChevronRight, X, Settings } from 'lucide-react';
import '../styles/EntityInspector.css';
interface EntityInspectorProps {
entityStore: EntityStoreService;
messageHub: MessageHub;
}
export function EntityInspector({ entityStore: _entityStore, messageHub }: EntityInspectorProps) {
const [selectedEntity, setSelectedEntity] = useState<Entity | null>(null);
const [remoteEntity, setRemoteEntity] = useState<any | null>(null);
const [remoteEntityDetails, setRemoteEntityDetails] = useState<any | null>(null);
const [expandedComponents, setExpandedComponents] = useState<Set<number>>(new Set());
const [componentVersion, setComponentVersion] = useState(0);
useEffect(() => {
const handleSelection = (data: { entity: Entity | null }) => {
setSelectedEntity((prev) => {
// Only reset version when selecting a different entity
// 只在选择不同实体时重置版本
if (prev?.id !== data.entity?.id) {
setComponentVersion(0);
} else {
// Same entity re-selected, trigger refresh
// 同一实体重新选择,触发刷新
setComponentVersion((v) => v + 1);
}
return data.entity;
});
setRemoteEntity(null);
setRemoteEntityDetails(null);
};
const handleRemoteSelection = (data: { entity: any }) => {
setRemoteEntity(data.entity);
setRemoteEntityDetails(null);
setSelectedEntity(null);
};
const handleEntityDetails = (event: Event) => {
const customEvent = event as CustomEvent;
const details = customEvent.detail;
setRemoteEntityDetails(details);
};
const handleComponentChange = () => {
setComponentVersion((prev) => prev + 1);
};
const unsubSelect = messageHub.subscribe('entity:selected', handleSelection);
const unsubRemoteSelect = messageHub.subscribe('remote-entity:selected', handleRemoteSelection);
const unsubComponentAdded = messageHub.subscribe('component:added', handleComponentChange);
const unsubComponentRemoved = messageHub.subscribe('component:removed', handleComponentChange);
const unsubPropertyChanged = messageHub.subscribe('component:property:changed', handleComponentChange);
window.addEventListener('profiler:entity-details', handleEntityDetails);
return () => {
unsubSelect();
unsubRemoteSelect();
unsubComponentAdded();
unsubComponentRemoved();
unsubPropertyChanged();
window.removeEventListener('profiler:entity-details', handleEntityDetails);
};
}, [messageHub]);
const handleRemoveComponent = (index: number) => {
if (!selectedEntity) return;
const component = selectedEntity.components[index];
if (component) {
selectedEntity.removeComponent(component);
messageHub.publish('component:removed', { entity: selectedEntity, component });
}
};
const toggleComponentExpanded = (index: number) => {
setExpandedComponents((prev) => {
const newSet = new Set(prev);
if (newSet.has(index)) {
newSet.delete(index);
} else {
newSet.add(index);
}
return newSet;
});
};
const handlePropertyChange = (component: any, propertyName: string, value: any) => {
if (!selectedEntity) {
return;
}
// Actually update the component property
// 实际更新组件属性
component[propertyName] = value;
messageHub.publish('component:property:changed', {
entity: selectedEntity,
component,
propertyName,
value
});
// Also publish scene:modified so other panels can react
messageHub.publish('scene:modified', {});
};
const renderRemoteProperty = (key: string, value: any) => {
if (value === null || value === undefined) {
return (
<div key={key} className="property-field">
<label className="property-label">{key}</label>
<span className="property-value-text">null</span>
</div>
);
}
if (Array.isArray(value)) {
return (
<div key={key} className="property-field">
<label className="property-label">{key}</label>
<div style={{ flex: 1, display: 'flex', flexWrap: 'wrap', gap: '4px' }}>
{value.length === 0 ? (
<span className="property-value-text" style={{ opacity: 0.5 }}>Empty Array</span>
) : (
value.map((item, index) => (
<span
key={index}
style={{
padding: '2px 6px',
background: 'var(--color-bg-inset)',
border: '1px solid var(--color-border-default)',
borderRadius: '3px',
fontSize: '10px',
color: 'var(--color-text-primary)',
fontFamily: 'var(--font-family-mono)'
}}
>
{typeof item === 'object' ? JSON.stringify(item) : String(item)}
</span>
))
)}
</div>
</div>
);
}
const valueType = typeof value;
if (valueType === 'boolean') {
return (
<div key={key} className="property-field property-field-boolean">
<label className="property-label">{key}</label>
<div className={`property-toggle ${value ? 'property-toggle-on' : 'property-toggle-off'} property-toggle-readonly`}>
<span className="property-toggle-thumb" />
</div>
</div>
);
}
if (valueType === 'number') {
return (
<div key={key} className="property-field">
<label className="property-label">{key}</label>
<input
type="number"
className="property-input property-input-number"
value={value}
disabled
/>
</div>
);
}
if (valueType === 'string') {
return (
<div key={key} className="property-field">
<label className="property-label">{key}</label>
<input
type="text"
className="property-input property-input-text"
value={value}
disabled
/>
</div>
);
}
if (valueType === 'object' && value.r !== undefined && value.g !== undefined && value.b !== undefined) {
const r = Math.round(value.r * 255);
const g = Math.round(value.g * 255);
const b = Math.round(value.b * 255);
const a = value.a !== undefined ? value.a : 1;
const hexColor = `#${r.toString(16).padStart(2, '0')}${g.toString(16).padStart(2, '0')}${b.toString(16).padStart(2, '0')}`;
return (
<div key={key} className="property-field">
<label className="property-label">{key}</label>
<div className="property-color-wrapper">
<div className="property-color-preview" style={{ backgroundColor: hexColor, opacity: a }} />
<input
type="text"
className="property-input property-input-color-text"
value={`${hexColor.toUpperCase()} (${a.toFixed(2)})`}
disabled
style={{ flex: 1 }}
/>
</div>
</div>
);
}
if (valueType === 'object' && value.minX !== undefined && value.maxX !== undefined && value.minY !== undefined && value.maxY !== undefined) {
return (
<div key={key} className="property-field" style={{ flexDirection: 'column', alignItems: 'stretch' }}>
<label className="property-label" style={{ flex: 'none', marginBottom: '4px' }}>{key}</label>
<div style={{ display: 'flex', flexDirection: 'column', gap: '4px' }}>
<div className="property-vector-compact">
<div className="property-vector-axis-compact">
<span className="property-vector-axis-label property-vector-axis-x">X</span>
<input
type="number"
className="property-input property-input-number-compact"
value={value.minX}
disabled
placeholder="Min"
/>
</div>
<div className="property-vector-axis-compact">
<span className="property-vector-axis-label property-vector-axis-x">X</span>
<input
type="number"
className="property-input property-input-number-compact"
value={value.maxX}
disabled
placeholder="Max"
/>
</div>
</div>
<div className="property-vector-compact">
<div className="property-vector-axis-compact">
<span className="property-vector-axis-label property-vector-axis-y">Y</span>
<input
type="number"
className="property-input property-input-number-compact"
value={value.minY}
disabled
placeholder="Min"
/>
</div>
<div className="property-vector-axis-compact">
<span className="property-vector-axis-label property-vector-axis-y">Y</span>
<input
type="number"
className="property-input property-input-number-compact"
value={value.maxY}
disabled
placeholder="Max"
/>
</div>
</div>
</div>
</div>
);
}
if (valueType === 'object' && value.x !== undefined && value.y !== undefined) {
if (value.z !== undefined) {
return (
<div key={key} className="property-field">
<label className="property-label">{key}</label>
<div className="property-vector-compact">
<div className="property-vector-axis-compact">
<span className="property-vector-axis-label property-vector-axis-x">X</span>
<input
type="number"
className="property-input property-input-number-compact"
value={value.x}
disabled
/>
</div>
<div className="property-vector-axis-compact">
<span className="property-vector-axis-label property-vector-axis-y">Y</span>
<input
type="number"
className="property-input property-input-number-compact"
value={value.y}
disabled
/>
</div>
<div className="property-vector-axis-compact">
<span className="property-vector-axis-label property-vector-axis-z">Z</span>
<input
type="number"
className="property-input property-input-number-compact"
value={value.z}
disabled
/>
</div>
</div>
</div>
);
} else {
return (
<div key={key} className="property-field">
<label className="property-label">{key}</label>
<div className="property-vector-compact">
<div className="property-vector-axis-compact">
<span className="property-vector-axis-label property-vector-axis-x">X</span>
<input
type="number"
className="property-input property-input-number-compact"
value={value.x}
disabled
/>
</div>
<div className="property-vector-axis-compact">
<span className="property-vector-axis-label property-vector-axis-y">Y</span>
<input
type="number"
className="property-input property-input-number-compact"
value={value.y}
disabled
/>
</div>
</div>
</div>
);
}
}
return (
<div key={key} className="property-field">
<label className="property-label">{key}</label>
<span className="property-value-text">{JSON.stringify(value)}</span>
</div>
);
};
if (!selectedEntity && !remoteEntity) {
return (
<div className="entity-inspector">
<div className="inspector-header">
<FileSearch size={16} className="inspector-header-icon" />
<h3>Inspector</h3>
</div>
<div className="inspector-content">
<div className="empty-state">
<FileSearch size={48} strokeWidth={1.5} className="empty-icon" />
<div className="empty-title">No entity selected</div>
<div className="empty-hint">Select an entity from the hierarchy</div>
</div>
</div>
</div>
);
}
// 显示远程实体
if (remoteEntity) {
const displayData = remoteEntityDetails || remoteEntity;
const hasDetailedComponents = remoteEntityDetails && remoteEntityDetails.components && remoteEntityDetails.components.length > 0;
return (
<div className="entity-inspector">
<div className="inspector-header">
<FileSearch size={16} className="inspector-header-icon" />
<h3>Inspector</h3>
</div>
<div className="inspector-content scrollable">
<div className="inspector-section">
<div className="section-header">
<Settings size={12} className="section-icon" />
<span>Entity Info (Remote)</span>
</div>
<div className="section-content">
<div className="info-row">
<span className="info-label">ID:</span>
<span className="info-value">{displayData.id}</span>
</div>
<div className="info-row">
<span className="info-label">Name:</span>
<span className="info-value">{displayData.name}</span>
</div>
<div className="info-row">
<span className="info-label">Enabled:</span>
<span className="info-value">{displayData.enabled ? 'Yes' : 'No'}</span>
</div>
{displayData.scene && (
<div className="info-row">
<span className="info-label">Scene:</span>
<span className="info-value">{displayData.scene}</span>
</div>
)}
</div>
</div>
<div className="inspector-section">
<div className="section-header">
<Settings size={12} className="section-icon" />
<span>Components ({displayData.componentCount})</span>
</div>
<div className="section-content">
{hasDetailedComponents ? (
<ul className="component-list">
{remoteEntityDetails!.components.map((component: any, index: number) => {
const isExpanded = expandedComponents.has(index);
return (
<li key={index} className={`component-item ${isExpanded ? 'expanded' : ''}`}>
<div className="component-header" onClick={() => toggleComponentExpanded(index)}>
<button
className="component-expand-btn"
title={isExpanded ? 'Collapse' : 'Expand'}
>
{isExpanded ? <ChevronDown size={14} /> : <ChevronRight size={14} />}
</button>
<Settings size={14} className="component-icon" />
<span className="component-name">{component.typeName}</span>
</div>
{isExpanded && (
<div className="component-properties animate-slideDown">
<div className="property-inspector">
{Object.entries(component.properties).map(([key, value]) =>
renderRemoteProperty(key, value)
)}
</div>
</div>
)}
</li>
);
})}
</ul>
) : displayData.componentTypes && displayData.componentTypes.length > 0 ? (
<ul className="component-list">
{displayData.componentTypes.map((componentType: string, index: number) => (
<li key={index} className="component-item">
<div className="component-header">
<Settings size={14} className="component-icon" />
<span className="component-name">{componentType}</span>
</div>
</li>
))}
</ul>
) : (
<div className="empty-state-small">No components</div>
)}
</div>
</div>
</div>
</div>
);
}
const components = selectedEntity!.components;
return (
<div className="entity-inspector">
<div className="inspector-header">
<FileSearch size={16} className="inspector-header-icon" />
<h3>Inspector</h3>
</div>
<div className="inspector-content scrollable">
<div className="inspector-section">
<div className="section-header">
<Settings size={12} className="section-icon" />
<span>Entity Info</span>
</div>
<div className="section-content">
<div className="info-row">
<span className="info-label">ID:</span>
<span className="info-value">{selectedEntity!.id}</span>
</div>
<div className="info-row">
<span className="info-label">Name:</span>
<span className="info-value">Entity {selectedEntity!.id}</span>
</div>
<div className="info-row">
<span className="info-label">Enabled:</span>
<span className="info-value">{selectedEntity!.enabled ? 'Yes' : 'No'}</span>
</div>
</div>
</div>
<div className="inspector-section">
<div className="section-header">
<Settings size={12} className="section-icon" />
<span>Components ({components.length})</span>
</div>
<div className="section-content">
{components.length === 0 ? (
<div className="empty-state-small">No components</div>
) : (
<ul className="component-list" key={componentVersion}>
{components.map((component, index) => {
const isExpanded = expandedComponents.has(index);
return (
<li key={index} className={`component-item ${isExpanded ? 'expanded' : ''}`}>
<div className="component-header" onClick={() => toggleComponentExpanded(index)}>
<button
className="component-expand-btn"
title={isExpanded ? 'Collapse' : 'Expand'}
>
{isExpanded ? <ChevronDown size={14} /> : <ChevronRight size={14} />}
</button>
<Settings size={14} className="component-icon" />
<span className="component-name">{component.constructor.name}</span>
<button
className="remove-component-btn"
onClick={(e) => {
e.stopPropagation();
handleRemoveComponent(index);
}}
title="Remove Component"
>
<X size={14} />
</button>
</div>
{isExpanded && (
<div className="component-properties animate-slideDown">
<PropertyInspector
key={`${index}-${componentVersion}`}
component={component}
onChange={(propertyName, value) => handlePropertyChange(component, propertyName, value)}
/>
</div>
)}
</li>
);
})}
</ul>
)}
</div>
</div>
</div>
</div>
);
}
@@ -1,241 +0,0 @@
import { useState, useEffect } from 'react';
import { Activity, Cpu, Layers, Package, Wifi, WifiOff, Maximize2, Pause, Play, BarChart3 } from 'lucide-react';
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() {
const [profilerData, setProfilerData] = useState<ProfilerData | null>(null);
const [isConnected, setIsConnected] = useState(false);
const [isServerRunning, setIsServerRunning] = useState(false);
const [port, setPort] = useState('8080');
const [isPaused, setIsPaused] = useState(false);
useEffect(() => {
const settings = SettingsService.getInstance();
setPort(settings.get('profiler.port', '8080'));
const handleSettingsChange = ((event: CustomEvent) => {
const newPort = event.detail['profiler.port'];
if (newPort) {
setPort(newPort);
}
}) as EventListener;
window.addEventListener('settings:changed', handleSettingsChange);
return () => {
window.removeEventListener('settings:changed', handleSettingsChange);
};
}, []);
useEffect(() => {
const profilerService = getProfilerService();
if (!profilerService) {
console.warn('[ProfilerDockPanel] ProfilerService not available - plugin may be disabled');
setIsServerRunning(false);
setIsConnected(false);
return;
}
// 订阅数据更新
const unsubscribe = profilerService.subscribe((data: ProfilerData) => {
if (!isPaused) {
setProfilerData(data);
}
});
// 定期检查连接状态
const checkStatus = () => {
setIsConnected(profilerService.isConnected());
setIsServerRunning(profilerService.isServerActive());
};
checkStatus();
const interval = setInterval(checkStatus, 1000);
return () => {
unsubscribe();
clearInterval(interval);
};
}, [isPaused]);
const fps = profilerData?.fps || 0;
const totalFrameTime = profilerData?.totalFrameTime || 0;
const systems = (profilerData?.systems || []).slice(0, 5); // Only show top 5 systems in dock panel
const entityCount = profilerData?.entityCount || 0;
const componentCount = profilerData?.componentCount || 0;
const targetFrameTime = 16.67;
const handleOpenDetails = () => {
const messageHub = Core.services.resolve(MessageHub);
if (messageHub) {
messageHub.publish('ui:openWindow', { windowId: 'profiler' });
}
};
const handleOpenAdvancedProfiler = () => {
const messageHub = Core.services.resolve(MessageHub);
if (messageHub) {
messageHub.publish('ui:openWindow', { windowId: 'advancedProfiler' });
}
};
const handleTogglePause = () => {
setIsPaused(!isPaused);
};
return (
<div className="profiler-dock-panel">
<div className="profiler-dock-header">
<h3>Performance Monitor</h3>
<div className="profiler-dock-header-actions">
{isConnected && (
<>
<button
className="profiler-dock-pause-btn"
onClick={handleTogglePause}
title={isPaused ? 'Resume data updates' : 'Pause data updates'}
>
{isPaused ? <Play size={14} /> : <Pause size={14} />}
</button>
<button
className="profiler-dock-details-btn"
onClick={handleOpenAdvancedProfiler}
title="Open advanced profiler"
>
<BarChart3 size={14} />
</button>
<button
className="profiler-dock-details-btn"
onClick={handleOpenDetails}
title="Open detailed profiler"
>
<Maximize2 size={14} />
</button>
</>
)}
<div className="profiler-dock-status">
{isConnected ? (
<>
<Wifi size={12} />
<span className="status-text connected">Connected</span>
</>
) : isServerRunning ? (
<>
<WifiOff size={12} />
<span className="status-text waiting">Waiting...</span>
</>
) : (
<>
<WifiOff size={12} />
<span className="status-text disconnected">Server Off</span>
</>
)}
</div>
</div>
</div>
{!isServerRunning ? (
<div className="profiler-dock-empty">
<Cpu size={32} />
<p>Profiler server not running</p>
<p className="hint">Open Profiler window and connect to start monitoring</p>
</div>
) : !isConnected ? (
<div className="profiler-dock-empty">
<Activity size={32} />
<p>Waiting for game connection...</p>
<p className="hint">Connect to: <code>ws://localhost:{port}</code></p>
</div>
) : (
<div className="profiler-dock-content">
<div className="profiler-dock-stats">
<div className="stat-card">
<div className="stat-icon">
<Activity size={16} />
</div>
<div className="stat-info">
<div className="stat-label">FPS</div>
<div className={`stat-value ${fps < 55 ? 'warning' : ''}`}>{fps}</div>
</div>
</div>
<div className="stat-card">
<div className="stat-icon">
<Cpu size={16} />
</div>
<div className="stat-info">
<div className="stat-label">Frame Time</div>
<div className={`stat-value ${totalFrameTime > targetFrameTime ? 'warning' : ''}`}>
{totalFrameTime.toFixed(1)}ms
</div>
</div>
</div>
<div className="stat-card">
<div className="stat-icon">
<Layers size={16} />
</div>
<div className="stat-info">
<div className="stat-label">Entities</div>
<div className="stat-value">{entityCount}</div>
</div>
</div>
<div className="stat-card">
<div className="stat-icon">
<Package size={16} />
</div>
<div className="stat-info">
<div className="stat-label">Components</div>
<div className="stat-value">{componentCount}</div>
</div>
</div>
</div>
{systems.length > 0 && (
<div className="profiler-dock-systems">
<h4>Top Systems</h4>
<div className="systems-list">
{systems.map((system) => (
<div key={system.name} className="system-item">
<div className="system-item-header">
<span className="system-item-name">{system.name}</span>
<span className="system-item-time">
{system.executionTime.toFixed(2)}ms
</span>
</div>
<div className="system-item-bar">
<div
className="system-item-bar-fill"
style={{
width: `${Math.min(system.percentage, 100)}%`,
backgroundColor: system.executionTime > targetFrameTime
? 'var(--color-danger)'
: system.executionTime > targetFrameTime * 0.5
? 'var(--color-warning)'
: 'var(--color-success)'
}}
/>
</div>
<div className="system-item-footer">
<span className="system-item-percentage">{system.percentage.toFixed(1)}%</span>
{system.entityCount > 0 && (
<span className="system-item-entities">{system.entityCount} entities</span>
)}
</div>
</div>
))}
</div>
</div>
)}
</div>
)}
</div>
);
}
@@ -1,229 +0,0 @@
import { useState, useEffect, useRef } from 'react';
import { Core } from '@esengine/ecs-framework';
import { Activity, BarChart3, Clock, Cpu, RefreshCw, Pause, Play } from 'lucide-react';
import '../styles/ProfilerPanel.css';
interface SystemPerformanceData {
name: string;
executionTime: number;
entityCount: number;
averageTime: number;
minTime: number;
maxTime: number;
percentage: number;
}
export function ProfilerPanel() {
const [systems, setSystems] = useState<SystemPerformanceData[]>([]);
const [totalFrameTime, setTotalFrameTime] = useState(0);
const [isPaused, setIsPaused] = useState(false);
const [sortBy, setSortBy] = useState<'time' | 'average' | 'name'>('time');
const animationRef = useRef<number>();
useEffect(() => {
const updateProfilerData = () => {
if (isPaused) {
animationRef.current = requestAnimationFrame(updateProfilerData);
return;
}
const performanceMonitor = Core.performanceMonitor;
if (!performanceMonitor?.isEnabled) {
animationRef.current = requestAnimationFrame(updateProfilerData);
return;
}
const systemDataMap = performanceMonitor.getAllSystemData();
const systemStatsMap = performanceMonitor.getAllSystemStats();
const systemsData: SystemPerformanceData[] = [];
let total = 0;
for (const [name, data] of systemDataMap.entries()) {
const stats = systemStatsMap.get(name);
if (stats) {
systemsData.push({
name,
executionTime: data.executionTime,
entityCount: data.entityCount,
averageTime: stats.averageTime,
minTime: stats.minTime,
maxTime: stats.maxTime,
percentage: 0
});
total += data.executionTime;
}
}
// Calculate percentages
systemsData.forEach((system) => {
system.percentage = total > 0 ? (system.executionTime / total) * 100 : 0;
});
// Sort systems
systemsData.sort((a, b) => {
switch (sortBy) {
case 'time':
return b.executionTime - a.executionTime;
case 'average':
return b.averageTime - a.averageTime;
case 'name':
return a.name.localeCompare(b.name);
default:
return 0;
}
});
setSystems(systemsData);
setTotalFrameTime(total);
animationRef.current = requestAnimationFrame(updateProfilerData);
};
animationRef.current = requestAnimationFrame(updateProfilerData);
return () => {
if (animationRef.current) {
cancelAnimationFrame(animationRef.current);
}
};
}, [isPaused, sortBy]);
const handleReset = () => {
Core.performanceMonitor?.reset();
};
const fps = totalFrameTime > 0 ? Math.round(1000 / totalFrameTime) : 0;
const targetFrameTime = 16.67; // 60 FPS
const isOverBudget = totalFrameTime > targetFrameTime;
return (
<div className="profiler-panel">
<div className="profiler-toolbar">
<div className="profiler-toolbar-left">
<div className="profiler-stats-summary">
<div className="summary-item">
<Clock size={14} />
<span className="summary-label">Frame:</span>
<span className={`summary-value ${isOverBudget ? 'over-budget' : ''}`}>
{totalFrameTime.toFixed(2)}ms
</span>
</div>
<div className="summary-item">
<Activity size={14} />
<span className="summary-label">FPS:</span>
<span className={`summary-value ${fps < 55 ? 'low-fps' : ''}`}>{fps}</span>
</div>
<div className="summary-item">
<BarChart3 size={14} />
<span className="summary-label">Systems:</span>
<span className="summary-value">{systems.length}</span>
</div>
</div>
</div>
<div className="profiler-toolbar-right">
<select
className="profiler-sort"
value={sortBy}
onChange={(e) => setSortBy(e.target.value as any)}
>
<option value="time">Sort by Time</option>
<option value="average">Sort by Average</option>
<option value="name">Sort by Name</option>
</select>
<button
className="profiler-btn"
onClick={() => setIsPaused(!isPaused)}
title={isPaused ? 'Resume' : 'Pause'}
>
{isPaused ? <Play size={14} /> : <Pause size={14} />}
</button>
<button
className="profiler-btn"
onClick={handleReset}
title="Reset Statistics"
>
<RefreshCw size={14} />
</button>
</div>
</div>
<div className="profiler-content">
{systems.length === 0 ? (
<div className="profiler-empty">
<Cpu size={48} />
<p>No performance data available</p>
<p className="profiler-empty-hint">
Make sure Core debug mode is enabled and systems are running
</p>
</div>
) : (
<div className="profiler-systems">
{systems.map((system, index) => (
<div key={system.name} className="system-row">
<div className="system-header">
<div className="system-info">
<span className="system-rank">#{index + 1}</span>
<span className="system-name">{system.name}</span>
{system.entityCount > 0 && (
<span className="system-entities">
({system.entityCount} entities)
</span>
)}
</div>
<div className="system-metrics">
<span className="metric-time">{system.executionTime.toFixed(2)}ms</span>
<span className="metric-percentage">{system.percentage.toFixed(1)}%</span>
</div>
</div>
<div className="system-bar">
<div
className="system-bar-fill"
style={{
width: `${Math.min(system.percentage, 100)}%`,
backgroundColor: system.executionTime > targetFrameTime
? 'var(--color-danger)'
: system.executionTime > targetFrameTime * 0.5
? 'var(--color-warning)'
: 'var(--color-success)'
}}
/>
</div>
<div className="system-stats">
<div className="stat-item">
<span className="stat-label">Avg:</span>
<span className="stat-value">{system.averageTime.toFixed(2)}ms</span>
</div>
<div className="stat-item">
<span className="stat-label">Min:</span>
<span className="stat-value">{system.minTime.toFixed(2)}ms</span>
</div>
<div className="stat-item">
<span className="stat-label">Max:</span>
<span className="stat-value">{system.maxTime.toFixed(2)}ms</span>
</div>
</div>
</div>
))}
</div>
)}
</div>
<div className="profiler-footer">
<div className="profiler-legend">
<div className="legend-item">
<div className="legend-color" style={{ background: 'var(--color-success)' }} />
<span>Good (&lt;8ms)</span>
</div>
<div className="legend-item">
<div className="legend-color" style={{ background: 'var(--color-warning)' }} />
<span>Warning (8-16ms)</span>
</div>
<div className="legend-item">
<div className="legend-color" style={{ background: 'var(--color-danger)' }} />
<span>Critical (&gt;16ms)</span>
</div>
</div>
</div>
</div>
);
}
@@ -1,589 +0,0 @@
import { useState, useEffect, useRef } from 'react';
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 {
name: string;
executionTime: number;
entityCount: number;
averageTime: number;
minTime: number;
maxTime: number;
percentage: number;
level: number;
children?: SystemPerformanceData[];
isExpanded?: boolean;
}
interface ProfilerWindowProps {
onClose: () => void;
}
type DataSource = 'local' | 'remote';
export function ProfilerWindow({ onClose }: ProfilerWindowProps) {
const [systems, setSystems] = useState<SystemPerformanceData[]>([]);
const [totalFrameTime, setTotalFrameTime] = useState(0);
const [isPaused, setIsPaused] = useState(false);
const [sortBy] = useState<'time' | 'average' | 'name'>('time');
const [dataSource, setDataSource] = useState<DataSource>('local');
const [viewMode, setViewMode] = useState<'tree' | 'table'>('table');
const [searchQuery, setSearchQuery] = useState('');
const [isConnected, setIsConnected] = useState(false);
const [isServerRunning, setIsServerRunning] = useState(false);
const [port, setPort] = useState('8080');
const animationRef = useRef<number>();
const frameTimesRef = useRef<number[]>([]);
const lastFpsRef = useRef<number>(0);
useEffect(() => {
const settings = SettingsService.getInstance();
setPort(settings.get('profiler.port', '8080'));
const handleSettingsChange = ((event: CustomEvent) => {
const newPort = event.detail['profiler.port'];
if (newPort) {
setPort(newPort);
}
}) as EventListener;
window.addEventListener('settings:changed', handleSettingsChange);
return () => {
window.removeEventListener('settings:changed', handleSettingsChange);
};
}, []);
// Check ProfilerService connection status
useEffect(() => {
const profilerService = getProfilerService();
if (!profilerService) {
return;
}
const checkStatus = () => {
setIsConnected(profilerService.isConnected());
setIsServerRunning(profilerService.isServerActive());
};
checkStatus();
const interval = setInterval(checkStatus, 1000);
return () => clearInterval(interval);
}, []);
const buildSystemTree = (flatSystems: Map<string, any>, statsMap: Map<string, any>): SystemPerformanceData[] => {
const coreUpdate = flatSystems.get('Core.update');
const servicesUpdate = flatSystems.get('Services.update');
if (!coreUpdate) return [];
const coreStats = statsMap.get('Core.update');
const coreNode: SystemPerformanceData = {
name: 'Core.update',
executionTime: coreUpdate.executionTime,
entityCount: 0,
averageTime: coreStats?.averageTime || 0,
minTime: coreStats?.minTime || 0,
maxTime: coreStats?.maxTime || 0,
percentage: 100,
level: 0,
children: [],
isExpanded: true
};
if (servicesUpdate) {
const servicesStats = statsMap.get('Services.update');
coreNode.children!.push({
name: 'Services.update',
executionTime: servicesUpdate.executionTime,
entityCount: 0,
averageTime: servicesStats?.averageTime || 0,
minTime: servicesStats?.minTime || 0,
maxTime: servicesStats?.maxTime || 0,
percentage: coreUpdate.executionTime > 0
? (servicesUpdate.executionTime / coreUpdate.executionTime) * 100
: 0,
level: 1,
isExpanded: false
});
}
const sceneSystems: SystemPerformanceData[] = [];
for (const [name, data] of flatSystems.entries()) {
if (name !== 'Core.update' && name !== 'Services.update') {
const stats = statsMap.get(name);
if (stats) {
sceneSystems.push({
name,
executionTime: data.executionTime,
entityCount: data.entityCount,
averageTime: stats.averageTime,
minTime: stats.minTime,
maxTime: stats.maxTime,
percentage: 0,
level: 1,
isExpanded: false
});
}
}
}
sceneSystems.forEach((system) => {
system.percentage = coreUpdate.executionTime > 0
? (system.executionTime / coreUpdate.executionTime) * 100
: 0;
});
sceneSystems.sort((a, b) => b.executionTime - a.executionTime);
coreNode.children!.push(...sceneSystems);
return [coreNode];
};
// Subscribe to local performance data
useEffect(() => {
if (dataSource !== 'local') return;
const updateProfilerData = () => {
if (isPaused) {
animationRef.current = requestAnimationFrame(updateProfilerData);
return;
}
const performanceMonitor = Core.performanceMonitor;
if (!performanceMonitor?.isEnabled) {
animationRef.current = requestAnimationFrame(updateProfilerData);
return;
}
const systemDataMap = performanceMonitor.getAllSystemData();
const systemStatsMap = performanceMonitor.getAllSystemStats();
const tree = buildSystemTree(systemDataMap, systemStatsMap);
const coreData = systemDataMap.get('Core.update');
setSystems(tree);
setTotalFrameTime(coreData?.executionTime || 0);
animationRef.current = requestAnimationFrame(updateProfilerData);
};
animationRef.current = requestAnimationFrame(updateProfilerData);
return () => {
if (animationRef.current) {
cancelAnimationFrame(animationRef.current);
}
};
}, [isPaused, sortBy, dataSource]);
// Subscribe to remote performance data from ProfilerService
useEffect(() => {
if (dataSource !== 'remote') return;
const profilerService = getProfilerService();
if (!profilerService) {
console.warn('[ProfilerWindow] ProfilerService not available');
return;
}
const unsubscribe = profilerService.subscribe((data) => {
if (isPaused) return;
handleRemoteDebugData({
performance: {
frameTime: data.totalFrameTime,
systemPerformance: data.systems.map((sys) => ({
systemName: sys.name,
lastExecutionTime: sys.executionTime,
averageTime: sys.averageTime,
minTime: 0,
maxTime: 0,
entityCount: sys.entityCount,
percentage: sys.percentage
}))
}
});
});
return () => unsubscribe();
}, [dataSource, isPaused]);
const handleReset = () => {
if (dataSource === 'local') {
Core.performanceMonitor?.reset();
} else {
// Reset remote data
setSystems([]);
setTotalFrameTime(0);
}
};
const handleRemoteDebugData = (debugData: any) => {
if (isPaused) return;
const performance = debugData.performance;
if (!performance) return;
if (!performance.systemPerformance || !Array.isArray(performance.systemPerformance)) {
return;
}
const flatSystemsMap = new Map();
const statsMap = new Map();
for (const system of performance.systemPerformance) {
flatSystemsMap.set(system.systemName, {
executionTime: system.lastExecutionTime || system.averageTime || 0,
entityCount: system.entityCount || 0
});
statsMap.set(system.systemName, {
averageTime: system.averageTime || 0,
minTime: system.minTime || 0,
maxTime: system.maxTime || 0
});
}
const tree = buildSystemTree(flatSystemsMap, statsMap);
setSystems(tree);
setTotalFrameTime(performance.frameTime || 0);
};
const handleDataSourceChange = (newSource: DataSource) => {
if (newSource === 'remote' && dataSource === 'local') {
// Switching to remote
if (animationRef.current) {
cancelAnimationFrame(animationRef.current);
}
}
setDataSource(newSource);
setSystems([]);
setTotalFrameTime(0);
};
const toggleExpand = (systemName: string) => {
const toggleNode = (nodes: SystemPerformanceData[]): SystemPerformanceData[] => {
return nodes.map((node) => {
if (node.name === systemName) {
return { ...node, isExpanded: !node.isExpanded };
}
if (node.children) {
return { ...node, children: toggleNode(node.children) };
}
return node;
});
};
setSystems(toggleNode(systems));
};
const flattenTree = (nodes: SystemPerformanceData[]): SystemPerformanceData[] => {
const result: SystemPerformanceData[] = [];
for (const node of nodes) {
result.push(node);
if (node.isExpanded && node.children) {
result.push(...flattenTree(node.children));
}
}
return result;
};
// Calculate FPS using rolling average for stability
// 使用滑动平均计算 FPS 以保持稳定
const calculateFps = () => {
// Add any positive frame time
// 添加任何正数的帧时间
if (totalFrameTime > 0) {
frameTimesRef.current.push(totalFrameTime);
// Keep last 60 samples
if (frameTimesRef.current.length > 60) {
frameTimesRef.current.shift();
}
}
if (frameTimesRef.current.length > 0) {
const avgFrameTime = frameTimesRef.current.reduce((a, b) => a + b, 0) / frameTimesRef.current.length;
// Cap FPS between 0-999, and ensure avgFrameTime is reasonable
if (avgFrameTime > 0.01) {
lastFpsRef.current = Math.min(999, Math.round(1000 / avgFrameTime));
}
}
return lastFpsRef.current;
};
const fps = calculateFps();
const targetFrameTime = 16.67;
const isOverBudget = totalFrameTime > targetFrameTime;
let displaySystems = viewMode === 'tree' ? flattenTree(systems) : systems;
// Apply search filter
if (searchQuery.trim()) {
const query = searchQuery.toLowerCase();
if (viewMode === 'tree') {
displaySystems = displaySystems.filter((sys) =>
sys.name.toLowerCase().includes(query)
);
} else {
// For table view, flatten and filter
const flatList: SystemPerformanceData[] = [];
const flatten = (nodes: SystemPerformanceData[]) => {
for (const node of nodes) {
flatList.push(node);
if (node.children) flatten(node.children);
}
};
flatten(systems);
displaySystems = flatList.filter((sys) =>
sys.name.toLowerCase().includes(query)
);
}
} else if (viewMode === 'table') {
// For table view without search, flatten all
const flatList: SystemPerformanceData[] = [];
const flatten = (nodes: SystemPerformanceData[]) => {
for (const node of nodes) {
flatList.push(node);
if (node.children) flatten(node.children);
}
};
flatten(systems);
displaySystems = flatList;
}
return (
<div className="profiler-window-overlay" onClick={onClose}>
<div className="profiler-window" onClick={(e) => e.stopPropagation()}>
<div className="profiler-window-header">
<div className="profiler-window-title">
<BarChart3 size={20} />
<h2>Performance Profiler</h2>
{isPaused && (
<span className="paused-indicator">PAUSED</span>
)}
</div>
<button className="profiler-window-close" onClick={onClose} title="Close">
<X size={20} />
</button>
</div>
<div className="profiler-window-toolbar">
<div className="profiler-toolbar-left">
<div className="profiler-mode-switch">
<button
className={`mode-btn ${dataSource === 'local' ? 'active' : ''}`}
onClick={() => handleDataSourceChange('local')}
title="Local Core Instance"
>
<Cpu size={14} />
<span>Local</span>
</button>
<button
className={`mode-btn ${dataSource === 'remote' ? 'active' : ''}`}
onClick={() => handleDataSourceChange('remote')}
title="Remote Game Connection"
>
<Server size={14} />
<span>Remote</span>
</button>
</div>
{dataSource === 'remote' && (
<div className="profiler-connection">
<div className="connection-port-display">
<Server size={14} />
<span>ws://localhost:{port}</span>
</div>
{isConnected ? (
<div className="connection-status-indicator connected">
<Wifi size={14} />
<span>Connected</span>
</div>
) : isServerRunning ? (
<div className="connection-status-indicator waiting">
<WifiOff size={14} />
<span>Waiting for game...</span>
</div>
) : (
<div className="connection-status-indicator disconnected">
<WifiOff size={14} />
<span>Server Off</span>
</div>
)}
</div>
)}
{dataSource === 'local' && (
<div className="profiler-stats-summary">
<div className="summary-item">
<Clock size={14} />
<span className="summary-label">Frame:</span>
<span className={`summary-value ${isOverBudget ? 'over-budget' : ''}`}>
{totalFrameTime.toFixed(2)}ms
</span>
</div>
<div className="summary-item">
<Activity size={14} />
<span className="summary-label">FPS:</span>
<span className={`summary-value ${fps < 55 ? 'low-fps' : ''}`}>{fps}</span>
</div>
<div className="summary-item">
<BarChart3 size={14} />
<span className="summary-label">Systems:</span>
<span className="summary-value">{systems.length}</span>
</div>
</div>
)}
</div>
<div className="profiler-toolbar-right">
<div className="profiler-search">
<Search size={14} />
<input
type="text"
placeholder="Search systems..."
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
className="search-input"
/>
</div>
<div className="view-mode-switch">
<button
className={`view-mode-btn ${viewMode === 'table' ? 'active' : ''}`}
onClick={() => setViewMode('table')}
title="Table View"
>
<Table2 size={14} />
</button>
<button
className={`view-mode-btn ${viewMode === 'tree' ? 'active' : ''}`}
onClick={() => setViewMode('tree')}
title="Tree View"
>
<TreePine size={14} />
</button>
</div>
<button
className={`profiler-btn ${isPaused ? 'paused' : ''}`}
onClick={() => setIsPaused(!isPaused)}
title={isPaused ? 'Resume' : 'Pause'}
>
{isPaused ? <Play size={14} /> : <Pause size={14} />}
</button>
<button
className="profiler-btn"
onClick={handleReset}
title="Reset Statistics"
>
<RefreshCw size={14} />
</button>
</div>
</div>
<div className="profiler-window-content">
{displaySystems.length === 0 ? (
<div className="profiler-empty">
<Cpu size={48} />
<p>No performance data available</p>
<p className="profiler-empty-hint">
{searchQuery ? 'No systems match your search' : 'Make sure Core debug mode is enabled and systems are running'}
</p>
</div>
) : viewMode === 'table' ? (
<table className="profiler-table">
<thead>
<tr>
<th className="col-name">System Name</th>
<th className="col-time">Current</th>
<th className="col-time">Average</th>
<th className="col-time">Min</th>
<th className="col-time">Max</th>
<th className="col-percent">%</th>
<th className="col-entities">Entities</th>
</tr>
</thead>
<tbody>
{displaySystems.map((system) => (
<tr key={system.name} className={`level-${system.level}`}>
<td className="col-name">
<span className="system-name-cell" style={{ paddingLeft: `${system.level * 16}px` }}>
{system.name}
</span>
</td>
<td className="col-time">
<span className={`time-value ${system.executionTime > targetFrameTime ? 'critical' : system.executionTime > targetFrameTime * 0.5 ? 'warning' : ''}`}>
{system.executionTime.toFixed(2)}ms
</span>
</td>
<td className="col-time">{system.averageTime.toFixed(2)}ms</td>
<td className="col-time">{system.minTime.toFixed(2)}ms</td>
<td className="col-time">{system.maxTime.toFixed(2)}ms</td>
<td className="col-percent">{system.percentage.toFixed(1)}%</td>
<td className="col-entities">{system.entityCount || '-'}</td>
</tr>
))}
</tbody>
</table>
) : (
<div className="profiler-tree">
{displaySystems.map((system) => (
<div key={system.name} className={`tree-row level-${system.level}`}>
<div className="tree-row-header">
<div className="tree-row-left">
{system.children && system.children.length > 0 && (
<button
className="expand-btn"
onClick={() => toggleExpand(system.name)}
>
{system.isExpanded ? '▼' : '▶'}
</button>
)}
<span className="system-name">{system.name}</span>
{system.entityCount > 0 && (
<span className="system-entities">({system.entityCount})</span>
)}
</div>
<div className="tree-row-right">
<span className={`time-value ${system.executionTime > targetFrameTime ? 'critical' : system.executionTime > targetFrameTime * 0.5 ? 'warning' : ''}`}>
{system.executionTime.toFixed(2)}ms
</span>
<span className="percentage-badge">{system.percentage.toFixed(1)}%</span>
</div>
</div>
<div className="tree-row-stats">
<span>Avg: {system.averageTime.toFixed(2)}ms</span>
<span>Min: {system.minTime.toFixed(2)}ms</span>
<span>Max: {system.maxTime.toFixed(2)}ms</span>
</div>
</div>
))}
</div>
)}
</div>
<div className="profiler-window-footer">
<div className="profiler-legend">
<div className="legend-item">
<div className="legend-color" style={{ background: 'var(--color-success)' }} />
<span>Good (&lt;8ms)</span>
</div>
<div className="legend-item">
<div className="legend-color" style={{ background: 'var(--color-warning)' }} />
<span>Warning (8-16ms)</span>
</div>
<div className="legend-item">
<div className="legend-color" style={{ background: 'var(--color-danger)' }} />
<span>Critical (&gt;16ms)</span>
</div>
</div>
</div>
</div>
</div>
);
}
@@ -96,6 +96,13 @@ type ViewMode = 'local' | 'remote';
type SortColumn = 'name' | 'type';
type SortDirection = 'asc' | 'desc';
/**
* @zh SceneHierarchy Props
* @en SceneHierarchy Props
*
* @zh props useEditorServices() Context
* @en Note: These props will be removed in future versions, use useEditorServices() Context instead
*/
interface SceneHierarchyProps {
entityStore: EntityStoreService;
messageHub: MessageHub;
@@ -155,7 +162,12 @@ function isEntityVisible(entity: Entity): boolean {
return entity.enabled;
}
export function SceneHierarchy({ entityStore, messageHub, commandManager, isProfilerMode = false }: SceneHierarchyProps) {
export function SceneHierarchy({
entityStore,
messageHub,
commandManager,
isProfilerMode = false
}: SceneHierarchyProps) {
// ===== 从 HierarchyStore 获取状态 | Get state from HierarchyStore =====
const {
sceneInfo,
@@ -1,6 +1,22 @@
/**
* Inspector Components
* Inspector
* @zh Inspector - inspectors/ 使
* @en Inspector Internal Components - Used by inspectors/ directory
*
* @zh 使 inspectors/
* @en Note: This is internal implementation, external code should use inspectors/ directory
*
* @zh
* - inspectors/Inspector.tsx -
* - inspector/EntityInspectorPanel.tsx -
* - inspector/ComponentPropertyEditor.tsx -
*
* @en Architecture:
* - inspectors/Inspector.tsx - Entry component, routes to different inspector types
* - inspector/EntityInspectorPanel.tsx - Core entity inspector implementation
* - inspector/ComponentPropertyEditor.tsx - Component property editor
*
* @deprecated 使 '@/components/inspectors'
* @deprecated External code should import from '@/components/inspectors'
*/
// 主组件 | Main components
@@ -0,0 +1,4 @@
export { AssetField } from './AssetField';
export { CollisionLayerField } from './CollisionLayerField';
export { EntityRefField } from './EntityRefField';
export { TransformRow, RotationRow, MobilityRow, TransformSection } from './TransformField';
@@ -1,2 +1,26 @@
/**
* @zh - API
* @en Inspector Module - Public API
*
* @zh
* @en This is the main entry point for inspectors, all external code should import from here
*
* @example
* ```tsx
* import { Inspector, PropertyInspector } from '@/components/inspectors';
* ```
*/
// 主入口组件 | Main entry component
export { Inspector } from './Inspector';
// 属性检查器 | Property Inspector
export { PropertyInspector } from '../PropertyInspector';
// 类型 | Types
export type { InspectorProps, InspectorTarget, AssetFileInfo } from './types';
// 子组件 (按需导入) | Sub-components (import as needed)
export * from './views';
export * from './fields';
export * from './common';
@@ -2,7 +2,7 @@ import { useState, useRef, useEffect, useMemo, useCallback } from 'react';
import { Settings, ChevronDown, ChevronRight, X, Plus, Box, Search, Lock, Unlock } from 'lucide-react';
import { Entity, Component, Core, getComponentDependencies, getComponentTypeName, getComponentInstanceTypeName, isComponentInstanceHiddenInInspector, PrefabInstanceComponent } from '@esengine/ecs-framework';
import { MessageHub, CommandManager, EditorComponentRegistry, ComponentActionRegistry, ComponentInspectorRegistry, PrefabService } from '@esengine/editor-core';
import { PropertyInspector } from '../../PropertyInspector';
import { PropertyInspector } from '..';
import { NotificationService } from '../../../services/NotificationService';
import { RemoveComponentCommand, UpdateComponentCommand, AddComponentCommand } from '../../../application/commands/component';
import { PrefabInstanceInfo } from '../common/PrefabInstanceInfo';
@@ -0,0 +1,178 @@
/**
* @zh - prop drilling
* @en Editor Services Context - Solves prop drilling issue
*
* @zh 访
* @en Provides unified service access, avoiding passing service instances through component tree
*/
import React, { createContext, useContext, useMemo } from 'react';
import type {
EntityStoreService,
MessageHub,
CommandManager,
InspectorRegistry,
SceneManagerService,
ProjectService,
PluginManager,
UIRegistry,
SettingsRegistry,
BuildService,
LogService,
EntityCreationRegistry,
AssetRegistryService,
} from '@esengine/editor-core';
import type { IDialogExtended } from '../services/TauriDialogService';
import type { INotification } from '@esengine/editor-core';
/**
* @zh
* @en Editor core services interface
*/
export interface EditorServices {
// 核心服务 | Core services
entityStore: EntityStoreService | null;
messageHub: MessageHub | null;
commandManager: CommandManager;
// 场景与项目 | Scene & Project
sceneManager: SceneManagerService | null;
projectService: ProjectService | null;
// 插件与注册表 | Plugin & Registries
pluginManager: PluginManager | null;
inspectorRegistry: InspectorRegistry | null;
uiRegistry: UIRegistry | null;
settingsRegistry: SettingsRegistry | null;
entityCreationRegistry?: EntityCreationRegistry | null;
assetRegistry?: AssetRegistryService | null;
// 构建与日志 | Build & Logging
buildService: BuildService | null;
logService: LogService | null;
// UI 服务 | UI Services
notification: INotification | null;
dialog: IDialogExtended | null;
// 项目路径 | Project path
projectPath: string | null;
}
/**
* @zh
* @en Editor services context
*/
const EditorServicesContext = createContext<EditorServices | null>(null);
/**
* @zh Props
* @en Editor services provider props
*/
export interface EditorServicesProviderProps {
children: React.ReactNode;
services: EditorServices;
}
/**
* @zh
* @en Editor services provider component
*
* @example
* ```tsx
* <EditorServicesProvider services={services}>
* <SceneHierarchy />
* <Inspector />
* </EditorServicesProvider>
* ```
*/
export function EditorServicesProvider({ children, services }: EditorServicesProviderProps) {
const value = useMemo(() => services, [
services.entityStore,
services.messageHub,
services.commandManager,
services.sceneManager,
services.projectService,
services.pluginManager,
services.inspectorRegistry,
services.projectPath,
]);
return (
<EditorServicesContext.Provider value={value}>
{children}
</EditorServicesContext.Provider>
);
}
/**
* @zh Hook
* @en Hook to get editor services
*
* @zh EditorServicesProvider 使
* @en Must be used within EditorServicesProvider
*
* @example
* ```tsx
* function MyComponent() {
* const { entityStore, messageHub, commandManager } = useEditorServices();
* // 使用服务...
* }
* ```
*/
export function useEditorServices(): EditorServices {
const context = useContext(EditorServicesContext);
if (!context) {
throw new Error(
'useEditorServices must be used within EditorServicesProvider. ' +
'Make sure your component is wrapped in <EditorServicesProvider>.'
);
}
return context;
}
/**
* @zh Hook
* @en Optional editor services hook (does not throw)
*
* @zh Provider 使 null
* @en Returns null when used outside Provider
*/
export function useEditorServicesOptional(): EditorServices | null {
return useContext(EditorServicesContext);
}
/**
* @zh 便 Hooks
* @en Convenience hooks for specific services
*/
export function useEntityStore(): EntityStoreService | null {
return useEditorServices().entityStore;
}
export function useMessageHub(): MessageHub | null {
return useEditorServices().messageHub;
}
export function useCommandManager(): CommandManager {
return useEditorServices().commandManager;
}
export function useSceneManager(): SceneManagerService | null {
return useEditorServices().sceneManager;
}
export function useProjectService(): ProjectService | null {
return useEditorServices().projectService;
}
export function useInspectorRegistry(): InspectorRegistry | null {
return useEditorServices().inspectorRegistry;
}
export function useProjectPath(): string | null {
return useEditorServices().projectPath;
}
export { EditorServicesContext };
+20
View File
@@ -0,0 +1,20 @@
/**
* @zh
* @en Context module exports
*/
export {
EditorServicesContext,
EditorServicesProvider,
useEditorServices,
useEditorServicesOptional,
useEntityStore,
useMessageHub,
useCommandManager,
useSceneManager,
useProjectService,
useInspectorRegistry,
useProjectPath,
type EditorServices,
type EditorServicesProviderProps,
} from './EditorServicesContext';
+60
View File
@@ -0,0 +1,60 @@
/**
* @zh
* @en Global type declarations
*
* @zh Window
* @en Extend Window interface to support editor runtime global variables
*/
import type * as React from 'react';
import type * as ReactDOM from 'react-dom';
import type * as ReactJSXRuntime from 'react/jsx-runtime';
import type { IRuntimePlugin } from '@esengine/editor-core';
/**
* @zh SDK
* @en SDK global object structure
*/
interface ESEngineSDK {
Core: typeof import('@esengine/ecs-framework').Core;
Scene: typeof import('@esengine/ecs-framework').Scene;
Entity: typeof import('@esengine/ecs-framework').Entity;
Component: typeof import('@esengine/ecs-framework').Component;
System: typeof import('@esengine/ecs-framework').System;
[key: string]: unknown;
}
/**
* @zh
* @en Plugin container structure
*/
interface PluginContainer {
[pluginName: string]: IRuntimePlugin | undefined;
}
/**
* @zh
* @en User code exports structure
*/
interface UserExports {
[name: string]: unknown;
}
declare global {
interface Window {
// React 全局变量 - 供动态加载的插件使用
// React globals - for dynamically loaded plugins
React: typeof React;
ReactDOM: typeof ReactDOM;
ReactJSXRuntime: typeof ReactJSXRuntime;
// ESEngine 全局变量(与 EditorConfig.globals 对应)
// ESEngine globals (matching EditorConfig.globals)
__ESENGINE_SDK__: ESEngineSDK | undefined;
__ESENGINE_PLUGINS__: PluginContainer | undefined;
__USER_RUNTIME_EXPORTS__: UserExports | undefined;
__USER_EDITOR_EXPORTS__: UserExports | undefined;
}
}
export {};
@@ -0,0 +1,314 @@
/**
* @zh Hook
* @en Project Actions Hook
*
* @zh
* @en Encapsulates project-related operations (open, create, close project)
*/
import { useCallback } from 'react';
import { Core } from '@esengine/ecs-framework';
import {
ProjectService,
PluginManager,
SceneManagerService,
UserCodeService
} from '@esengine/editor-core';
import { useEditorStore, useDialogStore } from '../stores';
import { TauriAPI } from '../api/tauri';
import { SettingsService } from '../services/SettingsService';
import { EngineService } from '../services/EngineService';
import { PluginLoader } from '../services/PluginLoader';
import { useLocale } from './useLocale';
interface UseProjectActionsParams {
pluginLoader: PluginLoader;
pluginManagerRef: React.RefObject<PluginManager | null>;
projectServiceRef: React.MutableRefObject<ProjectService | null>;
showToast: (message: string, type: 'success' | 'error' | 'warning' | 'info') => void;
}
export function useProjectActions({
pluginLoader,
pluginManagerRef,
projectServiceRef,
showToast,
}: UseProjectActionsParams) {
const { t } = useLocale();
const {
setProjectLoaded,
setCurrentProjectPath,
setAvailableScenes,
setIsLoading,
setStatus,
setShowProjectWizard,
} = useEditorStore();
const { setErrorDialog, setConfirmDialog } = useDialogStore();
/**
* @zh
* @en Open recent project
*/
const handleOpenRecentProject = useCallback(async (projectPath: string) => {
try {
setIsLoading(true, t('loading.step1'));
const projectService = Core.services.resolve(ProjectService);
if (!projectService) {
console.error('Required services not available');
setIsLoading(false);
return;
}
projectServiceRef.current = projectService;
await projectService.openProject(projectPath);
await TauriAPI.setProjectBasePath(projectPath);
try {
await TauriAPI.updateProjectTsconfig(projectPath);
} catch (e) {
console.warn('[useProjectActions] Failed to update project tsconfig:', e);
}
const settings = SettingsService.getInstance();
settings.addRecentProject(projectPath);
setCurrentProjectPath(projectPath);
try {
const sceneFiles = await TauriAPI.scanDirectory(`${projectPath}/scenes`, '*.ecs');
const sceneNames = sceneFiles.map(f => `scenes/${f.split(/[\\/]/).pop()}`);
setAvailableScenes(sceneNames);
} catch (e) {
console.warn('[useProjectActions] Failed to scan scenes:', e);
}
setProjectLoaded(true);
setIsLoading(true, t('loading.step2'));
const engineService = EngineService.getInstance();
const engineReady = await engineService.waitForInitialization(30000);
if (!engineReady) {
throw new Error(t('loading.engineTimeoutError'));
}
if (pluginManagerRef.current) {
const pluginSettings = projectService.getPluginSettings();
if (pluginSettings && pluginSettings.enabledPlugins.length > 0) {
await pluginManagerRef.current.loadConfig({ enabledPlugins: pluginSettings.enabledPlugins });
}
}
await engineService.initializeModuleSystems();
const uiResolution = projectService.getUIDesignResolution();
engineService.setUICanvasSize(uiResolution.width, uiResolution.height);
setStatus(t('header.status.projectOpened'));
setIsLoading(true, t('loading.step3'));
const userCodeService = Core.services.tryResolve(UserCodeService);
if (userCodeService) {
await userCodeService.waitForReady();
}
const sceneManagerService = Core.services.resolve(SceneManagerService);
if (sceneManagerService) {
await sceneManagerService.newScene();
}
if (pluginManagerRef.current) {
setIsLoading(true, t('loading.loadingPlugins'));
await pluginLoader.loadProjectPlugins(projectPath, pluginManagerRef.current);
}
setIsLoading(false);
} catch (error) {
console.error('Failed to open project:', error);
setStatus(t('header.status.failed'));
setIsLoading(false);
const errorMessage = error instanceof Error ? error.message : String(error);
setErrorDialog({
title: t('project.openFailed'),
message: `${t('project.openFailed')}:\n${errorMessage}`
});
}
}, [t, pluginLoader, pluginManagerRef, projectServiceRef, setProjectLoaded, setCurrentProjectPath, setAvailableScenes, setIsLoading, setStatus, setErrorDialog]);
/**
* @zh
* @en Open project dialog
*/
const handleOpenProject = useCallback(async () => {
try {
const projectPath = await TauriAPI.openProjectDialog();
if (!projectPath) return;
await handleOpenRecentProject(projectPath);
} catch (error) {
console.error('Failed to open project dialog:', error);
}
}, [handleOpenRecentProject]);
/**
* @zh
* @en Show create project wizard
*/
const handleCreateProject = useCallback(() => {
setShowProjectWizard(true);
}, [setShowProjectWizard]);
/**
* @zh
* @en Create project from wizard
*/
const handleCreateProjectFromWizard = useCallback(async (
projectName: string,
projectPath: string,
_templateId: string
) => {
const sep = projectPath.includes('/') ? '/' : '\\';
const fullProjectPath = `${projectPath}${sep}${projectName}`;
try {
setIsLoading(true, t('project.creating'));
const projectService = Core.services.resolve(ProjectService);
if (!projectService) {
console.error('ProjectService not available');
setIsLoading(false);
setErrorDialog({
title: t('project.createFailed'),
message: t('project.serviceUnavailable')
});
return;
}
await projectService.createProject(fullProjectPath);
setIsLoading(true, t('project.createdOpening'));
await handleOpenRecentProject(fullProjectPath);
} catch (error) {
console.error('Failed to create project:', error);
setIsLoading(false);
const errorMessage = error instanceof Error ? error.message : String(error);
if (errorMessage.includes('already exists')) {
setConfirmDialog({
title: t('project.alreadyExists'),
message: t('project.existsQuestion'),
confirmText: t('project.open'),
cancelText: t('common.cancel'),
onConfirm: () => {
setConfirmDialog(null);
setIsLoading(true, t('project.opening'));
handleOpenRecentProject(fullProjectPath).catch((err) => {
console.error('Failed to open project:', err);
setIsLoading(false);
setErrorDialog({
title: t('project.openFailed'),
message: `${t('project.openFailed')}:\n${err instanceof Error ? err.message : String(err)}`
});
});
}
});
} else {
setStatus(t('project.createFailed'));
setErrorDialog({
title: t('project.createFailed'),
message: `${t('project.createFailed')}:\n${errorMessage}`
});
}
}
}, [t, handleOpenRecentProject, setIsLoading, setStatus, setErrorDialog, setConfirmDialog]);
/**
* @zh
* @en Browse project path
*/
const handleBrowseProjectPath = useCallback(async (): Promise<string | null> => {
try {
const path = await TauriAPI.openProjectDialog();
return path || null;
} catch (error) {
console.error('Failed to browse path:', error);
return null;
}
}, []);
/**
* @zh
* @en Close project
*/
const handleCloseProject = useCallback(async () => {
if (pluginManagerRef.current) {
await pluginLoader.unloadProjectPlugins(pluginManagerRef.current);
}
const scene = Core.scene;
if (scene) {
scene.end();
}
const engineService = EngineService.getInstance();
engineService.clearModuleSystems();
const projectService = Core.services.tryResolve(ProjectService);
if (projectService) {
await projectService.closeProject();
}
setProjectLoaded(false);
setCurrentProjectPath(null);
setStatus(t('header.status.ready'));
}, [t, pluginLoader, pluginManagerRef, setProjectLoaded, setCurrentProjectPath, setStatus]);
/**
* @zh
* @en Delete project
*/
const handleDeleteProject = useCallback(async (projectPath: string) => {
console.log('[useProjectActions] handleDeleteProject called with path:', projectPath);
try {
console.log('[useProjectActions] Calling TauriAPI.deleteFolder...');
await TauriAPI.deleteFolder(projectPath);
console.log('[useProjectActions] deleteFolder succeeded');
const settings = SettingsService.getInstance();
settings.removeRecentProject(projectPath);
setStatus(t('header.status.ready'));
} catch (error) {
console.error('[useProjectActions] Failed to delete project:', error);
setErrorDialog({
title: t('project.deleteFailed'),
message: `${t('project.deleteFailed')}:\n${error instanceof Error ? error.message : String(error)}`
});
}
}, [t, setStatus, setErrorDialog]);
/**
* @zh
* @en Remove from recent projects
*/
const handleRemoveRecentProject = useCallback((projectPath: string) => {
const settings = SettingsService.getInstance();
settings.removeRecentProject(projectPath);
setStatus(t('header.status.ready'));
}, [t, setStatus]);
return {
handleOpenProject,
handleOpenRecentProject,
handleCreateProject,
handleCreateProjectFromWizard,
handleBrowseProjectPath,
handleCloseProject,
handleDeleteProject,
handleRemoveRecentProject,
};
}
@@ -0,0 +1,187 @@
/**
* @zh Hook
* @en Scene Actions Hook
*
* @zh
* @en Encapsulates scene-related operations (new, open, save scene)
*/
import { useCallback } from 'react';
import { Core } from '@esengine/ecs-framework';
import { SceneManagerService, UserCodeService } from '@esengine/editor-core';
import { useEditorStore, useDialogStore } from '../stores';
import { useLocale } from './useLocale';
interface UseSceneActionsParams {
sceneManagerRef: React.RefObject<SceneManagerService | null>;
showToast: (message: string, type: 'success' | 'error' | 'warning' | 'info') => void;
}
export function useSceneActions({
sceneManagerRef,
showToast,
}: UseSceneActionsParams) {
const { t } = useLocale();
const { setStatus } = useEditorStore();
const { setErrorDialog } = useDialogStore();
/**
* @zh
* @en Create new scene
*/
const handleNewScene = useCallback(async () => {
const sceneManager = sceneManagerRef.current;
if (!sceneManager) {
console.error('SceneManagerService not available');
return;
}
try {
await sceneManager.newScene();
setStatus(t('scene.newCreated'));
} catch (error) {
console.error('Failed to create new scene:', error);
setStatus(t('scene.createFailed'));
}
}, [t, sceneManagerRef, setStatus]);
/**
* @zh
* @en Open scene (via dialog)
*/
const handleOpenScene = useCallback(async () => {
const sceneManager = sceneManagerRef.current;
if (!sceneManager) {
console.error('SceneManagerService not available');
return;
}
try {
const userCodeService = Core.services.tryResolve(UserCodeService);
if (userCodeService) {
await userCodeService.waitForReady();
}
await sceneManager.openScene();
const sceneState = sceneManager.getSceneState();
setStatus(t('scene.openedSuccess', { name: sceneState.sceneName }));
} catch (error) {
console.error('Failed to open scene:', error);
setStatus(t('scene.openFailed'));
}
}, [t, sceneManagerRef, setStatus]);
/**
* @zh
* @en Open scene by path
*/
const handleOpenSceneByPath = useCallback(async (scenePath: string) => {
console.log('[useSceneActions] handleOpenSceneByPath called:', scenePath);
const sceneManager = sceneManagerRef.current;
if (!sceneManager) {
console.error('SceneManagerService not available');
return;
}
try {
const userCodeService = Core.services.tryResolve(UserCodeService);
if (userCodeService) {
console.log('[useSceneActions] Waiting for user code service...');
await userCodeService.waitForReady();
console.log('[useSceneActions] User code service ready');
}
console.log('[useSceneActions] Calling sceneManager.openScene...');
await sceneManager.openScene(scenePath);
console.log('[useSceneActions] Scene opened successfully');
const sceneState = sceneManager.getSceneState();
setStatus(t('scene.openedSuccess', { name: sceneState.sceneName }));
} catch (error) {
console.error('Failed to open scene:', error);
setStatus(t('scene.openFailed'));
setErrorDialog({
title: t('scene.openFailed'),
message: `${t('scene.openFailed')}:\n${error instanceof Error ? error.message : String(error)}`
});
}
}, [t, sceneManagerRef, setStatus, setErrorDialog]);
/**
* @zh
* @en Save scene
*/
const handleSaveScene = useCallback(async () => {
const sceneManager = sceneManagerRef.current;
if (!sceneManager) {
console.error('SceneManagerService not available');
return;
}
try {
await sceneManager.saveScene();
const sceneState = sceneManager.getSceneState();
setStatus(t('scene.savedSuccess', { name: sceneState.sceneName }));
} catch (error) {
console.error('Failed to save scene:', error);
setStatus(t('scene.saveFailed'));
}
}, [t, sceneManagerRef, setStatus]);
/**
* @zh
* @en Save scene as
*/
const handleSaveSceneAs = useCallback(async () => {
const sceneManager = sceneManagerRef.current;
if (!sceneManager) {
console.error('SceneManagerService not available');
return;
}
try {
await sceneManager.saveSceneAs();
const sceneState = sceneManager.getSceneState();
setStatus(t('scene.savedSuccess', { name: sceneState.sceneName }));
} catch (error) {
console.error('Failed to save scene as:', error);
setStatus(t('scene.saveAsFailed'));
}
}, [t, sceneManagerRef, setStatus]);
/**
* @zh
* @en Save prefab or scene (for shortcut)
*/
const handleSave = useCallback(async () => {
const sceneManager = sceneManagerRef.current;
if (!sceneManager) return;
try {
if (sceneManager.isPrefabEditMode()) {
await sceneManager.savePrefab();
const prefabState = sceneManager.getPrefabEditModeState();
showToast(t('editMode.prefab.savedSuccess', { name: prefabState?.prefabName ?? 'Prefab' }), 'success');
} else {
await sceneManager.saveScene();
const sceneState = sceneManager.getSceneState();
showToast(t('scene.savedSuccess', { name: sceneState.sceneName }), 'success');
}
} catch (error) {
console.error('Failed to save:', error);
if (sceneManager.isPrefabEditMode()) {
showToast(t('editMode.prefab.saveFailed'), 'error');
} else {
showToast(t('scene.saveFailed'), 'error');
}
}
}, [t, sceneManagerRef, showToast]);
return {
handleNewScene,
handleOpenScene,
handleOpenSceneByPath,
handleSaveScene,
handleSaveSceneAs,
handleSave,
};
}
@@ -13,22 +13,22 @@ import { SpriteComponent, SpriteAnimatorComponent } from '@esengine/sprite';
import { EngineService } from './EngineService';
export class EditorEngineSync {
private static instance: EditorEngineSync | null = null;
private static _instance: EditorEngineSync | null = null;
private engineService: EngineService;
private messageHub: MessageHub | null = null;
private entityStore: EntityStoreService | null = null;
private _engineService: EngineService;
private _messageHub: MessageHub | null = null;
private _entityStore: EntityStoreService | null = null;
// Track synced entities: editor entity id -> engine entity id
private syncedEntities: Map<number, Entity> = new Map();
private _syncedEntities: Map<number, Entity> = new Map();
// Subscription IDs
private subscriptions: Array<() => void> = [];
private _subscriptions: Array<() => void> = [];
private initialized = false;
private _initialized = false;
private constructor() {
this.engineService = EngineService.getInstance();
this._engineService = EngineService.getInstance();
}
/**
@@ -36,10 +36,10 @@ export class EditorEngineSync {
*
*/
static getInstance(): EditorEngineSync {
if (!EditorEngineSync.instance) {
EditorEngineSync.instance = new EditorEngineSync();
if (!EditorEngineSync._instance) {
EditorEngineSync._instance = new EditorEngineSync();
}
return EditorEngineSync.instance;
return EditorEngineSync._instance;
}
/**
@@ -47,75 +47,75 @@ export class EditorEngineSync {
*
*/
initialize(messageHub: MessageHub, entityStore: EntityStoreService): void {
if (this.initialized) {
if (this._initialized) {
return;
}
this.messageHub = messageHub;
this.entityStore = entityStore;
this._messageHub = messageHub;
this._entityStore = entityStore;
// Subscribe to entity events
this.subscribeToEvents();
this._subscribeToEvents();
// Sync existing entities
this.syncAllEntities();
this._syncAllEntities();
this.initialized = true;
this._initialized = true;
}
/**
* Subscribe to MessageHub events.
* MessageHub事件
*/
private subscribeToEvents(): void {
if (!this.messageHub) return;
private _subscribeToEvents(): void {
if (!this._messageHub) return;
// Entity added
const unsubAdd = this.messageHub.subscribe('entity:added', (data: { entity: Entity }) => {
this.syncEntity(data.entity);
const unsubAdd = this._messageHub.subscribe('entity:added', (data: { entity: Entity }) => {
this._syncEntity(data.entity);
});
this.subscriptions.push(unsubAdd);
this._subscriptions.push(unsubAdd);
// Entity removed
const unsubRemove = this.messageHub.subscribe('entity:removed', (data: { entity: Entity }) => {
this.removeEntityFromEngine(data.entity);
const unsubRemove = this._messageHub.subscribe('entity:removed', (data: { entity: Entity }) => {
this._removeEntityFromEngine(data.entity);
});
this.subscriptions.push(unsubRemove);
this._subscriptions.push(unsubRemove);
// Component property changed - need to re-sync entity
const unsubComponent = this.messageHub.subscribe('component:property:changed', (data: { entity: Entity; component: Component; propertyName: string; value: any }) => {
this.updateEntityInEngine(data.entity, data.component, data.propertyName, data.value);
const unsubComponent = this._messageHub.subscribe('component:property:changed', (data: { entity: Entity; component: Component; propertyName: string; value: any }) => {
this._updateEntityInEngine(data.entity, data.component, data.propertyName, data.value);
});
this.subscriptions.push(unsubComponent);
this._subscriptions.push(unsubComponent);
// Component added - sync entity if it has sprite
const unsubComponentAdded = this.messageHub.subscribe('component:added', (data: { entity: Entity; component: Component }) => {
this.syncEntity(data.entity);
const unsubComponentAdded = this._messageHub.subscribe('component:added', (data: { entity: Entity; component: Component }) => {
this._syncEntity(data.entity);
});
this.subscriptions.push(unsubComponentAdded);
this._subscriptions.push(unsubComponentAdded);
// Entities cleared
const unsubClear = this.messageHub.subscribe('entities:cleared', () => {
this.clearAllFromEngine();
const unsubClear = this._messageHub.subscribe('entities:cleared', () => {
this._clearAllFromEngine();
});
this.subscriptions.push(unsubClear);
this._subscriptions.push(unsubClear);
// Entity selected - update gizmo display
const unsubSelected = this.messageHub.subscribe('entity:selected', (data: { entity: Entity | null }) => {
this.updateSelectedEntity(data.entity);
const unsubSelected = this._messageHub.subscribe('entity:selected', (data: { entity: Entity | null }) => {
this._updateSelectedEntity(data.entity);
});
this.subscriptions.push(unsubSelected);
this._subscriptions.push(unsubSelected);
}
/**
* Update selected entity for gizmo display.
* Gizmo显示
*/
private updateSelectedEntity(entity: Entity | null): void {
private _updateSelectedEntity(entity: Entity | null): void {
if (entity) {
this.engineService.setSelectedEntityIds([entity.id]);
this._engineService.setSelectedEntityIds([entity.id]);
} else {
this.engineService.setSelectedEntityIds([]);
this._engineService.setSelectedEntityIds([]);
}
}
@@ -123,12 +123,12 @@ export class EditorEngineSync {
* Sync all existing entities.
*
*/
private syncAllEntities(): void {
if (!this.entityStore) return;
private _syncAllEntities(): void {
if (!this._entityStore) return;
const entities = this.entityStore.getAllEntities();
const entities = this._entityStore.getAllEntities();
for (const entity of entities) {
this.syncEntity(entity);
this._syncEntity(entity);
}
}
@@ -140,7 +140,7 @@ export class EditorEngineSync {
* via Rust engine's path-based texture loading.
* EngineRenderSystem通过Rust引擎的路径加载自动处理
*/
private syncEntity(entity: Entity): void {
private _syncEntity(entity: Entity): void {
// Check if entity has sprite component
const spriteComponent = entity.getComponent(SpriteComponent);
if (!spriteComponent) {
@@ -151,7 +151,7 @@ export class EditorEngineSync {
// 预加载动画纹理并设置第一帧
const animator = entity.getComponent(SpriteAnimatorComponent);
if (animator && animator.clips) {
const bridge = this.engineService.getBridge();
const bridge = this._engineService.getBridge();
if (bridge) {
for (const clip of animator.clips) {
for (const frame of clip.frames) {
@@ -177,40 +177,40 @@ export class EditorEngineSync {
}
// Track synced entity
this.syncedEntities.set(entity.id, entity);
this._syncedEntities.set(entity.id, entity);
}
/**
* Remove entity from tracking.
*
*/
private removeEntityFromEngine(entity: Entity): void {
private _removeEntityFromEngine(entity: Entity): void {
if (!entity) {
return;
}
// Just remove from tracking, entity destruction is handled by the command
this.syncedEntities.delete(entity.id);
this._syncedEntities.delete(entity.id);
}
/**
* Update entity in engine when component changes.
*
*/
private updateEntityInEngine(entity: Entity, component: Component, propertyName: string, value: any): void {
const engineEntity = this.syncedEntities.get(entity.id);
private _updateEntityInEngine(entity: Entity, component: Component, propertyName: string, value: any): void {
const engineEntity = this._syncedEntities.get(entity.id);
if (!engineEntity) {
// Entity not synced yet, try to sync it
this.syncEntity(entity);
this._syncEntity(entity);
return;
}
// Update based on component type
if (component instanceof TransformComponent) {
this.updateTransform(engineEntity, component);
this._updateTransform(engineEntity, component);
} else if (component instanceof SpriteComponent) {
this.updateSprite(engineEntity, component, propertyName, value);
this._updateSprite(engineEntity, component, propertyName, value);
} else if (component instanceof SpriteAnimatorComponent) {
this.updateAnimator(engineEntity, component, propertyName);
this._updateAnimator(engineEntity, component, propertyName);
}
}
@@ -218,10 +218,10 @@ export class EditorEngineSync {
* Update animator - preload textures and set initial frame.
* -
*/
private updateAnimator(entity: Entity, animator: SpriteAnimatorComponent, propertyName: string): void {
private _updateAnimator(entity: Entity, animator: SpriteAnimatorComponent, propertyName: string): void {
// In editor mode, only preload textures and show first frame (no animation playback)
// 编辑模式下只预加载纹理并显示第一帧(不播放动画)
const bridge = this.engineService.getBridge();
const bridge = this._engineService.getBridge();
const sprite = entity.getComponent(SpriteComponent);
if (bridge && animator.clips) {
@@ -252,7 +252,7 @@ export class EditorEngineSync {
* Update transform in engine entity.
*
*/
private updateTransform(engineEntity: Entity, transform: TransformComponent): void {
private _updateTransform(engineEntity: Entity, transform: TransformComponent): void {
// Get engine transform component (same type as editor)
const engineTransform = engineEntity.getComponent(TransformComponent);
if (engineTransform) {
@@ -281,11 +281,11 @@ export class EditorEngineSync {
* Preloads textures when textureGuid changes to ensure they're available for rendering.
* textureGuid
*/
private updateSprite(entity: Entity, sprite: SpriteComponent, property: string, value: any): void {
private _updateSprite(entity: Entity, sprite: SpriteComponent, property: string, value: any): void {
// When textureGuid changes, trigger texture preload
// 当 textureGuid 变更时,触发纹理预加载
if (property === 'textureGuid' && value) {
const bridge = this.engineService.getBridge();
const bridge = this._engineService.getBridge();
if (bridge) {
// Preload the texture so it's ready for the next render frame
// 预加载纹理以便下一渲染帧时可用
@@ -298,9 +298,9 @@ export class EditorEngineSync {
* Clear all synced entities from tracking.
*
*/
private clearAllFromEngine(): void {
private _clearAllFromEngine(): void {
// Just clear tracking, entity destruction is handled elsewhere
this.syncedEntities.clear();
this._syncedEntities.clear();
}
/**
@@ -308,7 +308,7 @@ export class EditorEngineSync {
*
*/
isInitialized(): boolean {
return this.initialized;
return this._initialized;
}
/**
@@ -316,7 +316,7 @@ export class EditorEngineSync {
*
*/
getSyncedCount(): number {
return this.syncedEntities.size;
return this._syncedEntities.size;
}
/**
@@ -325,15 +325,15 @@ export class EditorEngineSync {
*/
dispose(): void {
// Unsubscribe from all events
for (const unsub of this.subscriptions) {
for (const unsub of this._subscriptions) {
unsub();
}
this.subscriptions = [];
this._subscriptions = [];
// Clear synced entities
this.syncedEntities.clear();
this._syncedEntities.clear();
this.initialized = false;
this._initialized = false;
}
}
@@ -50,6 +50,7 @@ import {
GameRuntime,
createGameRuntime,
EditorPlatformAdapter,
RuntimeMode,
type GameRuntimeConfig
} from '@esengine/runtime-core';
import { BehaviorTreeSystemToken } from '@esengine/behavior-tree';
@@ -72,7 +73,7 @@ const logger = createLogger('EngineService');
* Internally uses GameRuntime, maintains original API compatibility externally
*/
export class EngineService {
private static instance: EngineService | null = null;
private static _instance: EngineService | null = null;
private _runtime: GameRuntime | null = null;
private _initialized = false;
@@ -102,10 +103,10 @@ export class EngineService {
*
*/
static getInstance(): EngineService {
if (!EngineService.instance) {
EngineService.instance = new EngineService();
if (!EngineService._instance) {
EngineService._instance = new EngineService();
}
return EngineService.instance;
return EngineService._instance;
}
/**
@@ -81,8 +81,8 @@ export class PluginLoader {
*
*/
private initPluginContainer(): void {
if (!(window as any)[PLUGINS_GLOBAL_NAME]) {
(window as any)[PLUGINS_GLOBAL_NAME] = {};
if (!window.__ESENGINE_PLUGINS__) {
window.__ESENGINE_PLUGINS__ = {};
}
}
@@ -167,7 +167,7 @@ export class PluginLoader {
): Promise<IRuntimePlugin | null> {
const pluginKey = this.sanitizePluginKey(pluginName);
const pluginsContainer = (window as any)[PLUGINS_GLOBAL_NAME] as Record<string, any>;
const pluginsContainer = window.__ESENGINE_PLUGINS__ ?? {};
try {
// 插件代码是 IIFE 格式,会自动导出到全局插件容器
@@ -338,7 +338,7 @@ export class PluginLoader {
*
*/
async unloadProjectPlugins(_pluginManager: PluginManager): Promise<void> {
const pluginsContainer = (window as any)[PLUGINS_GLOBAL_NAME] as Record<string, any> | undefined;
const pluginsContainer = window.__ESENGINE_PLUGINS__;
for (const pluginName of this.loadedPlugins.keys()) {
// 清理全局容器中的插件
@@ -101,12 +101,11 @@ export class PluginSDKRegistry {
// 设置全局对象
// Set global object
const sdkGlobalName = EditorConfig.globals.sdk;
(window as any)[sdkGlobalName] = sdkGlobal;
window.__ESENGINE_SDK__ = sdkGlobal;
this.initialized = true;
console.log(`[PluginSDKRegistry] Initialized SDK at window.${sdkGlobalName}`);
console.log(`[PluginSDKRegistry] Initialized SDK at window.__ESENGINE_SDK__`);
}
/**
@@ -45,18 +45,18 @@ export interface ModuleManifest {
}
export class RuntimeResolver {
private static instance: RuntimeResolver;
private baseDir: string = '';
private engineModulesPath: string = '';
private initialized: boolean = false;
private static _instance: RuntimeResolver;
private _baseDir: string = '';
private _engineModulesPath: string = '';
private _initialized: boolean = false;
private constructor() {}
static getInstance(): RuntimeResolver {
if (!RuntimeResolver.instance) {
RuntimeResolver.instance = new RuntimeResolver();
if (!RuntimeResolver._instance) {
RuntimeResolver._instance = new RuntimeResolver();
}
return RuntimeResolver.instance;
return RuntimeResolver._instance;
}
/**
@@ -64,23 +64,23 @@ export class RuntimeResolver {
* Initialize the runtime resolver
*/
async initialize(): Promise<void> {
if (this.initialized) return;
if (this._initialized) return;
// 查找工作区根目录 | Find workspace root
const currentDir = await TauriAPI.getCurrentDir();
this.baseDir = await this.findWorkspaceRoot(currentDir);
this._baseDir = await this._findWorkspaceRoot(currentDir);
// 查找引擎模块路径 | Find engine modules path
this.engineModulesPath = await this.findEngineModulesPath();
this._engineModulesPath = await this._findEngineModulesPath();
this.initialized = true;
this._initialized = true;
}
/**
*
* Find workspace root by looking for workspace markers
*/
private async findWorkspaceRoot(startPath: string): Promise<string> {
private async _findWorkspaceRoot(startPath: string): Promise<string> {
let currentPath = startPath;
for (let i = 0; i < 5; i++) {
@@ -122,7 +122,7 @@ export class RuntimeResolver {
* 使
* Use environment variables and standard paths, avoid hardcoding
*/
private getInstalledEnginePaths(): string[] {
private _getInstalledEnginePaths(): string[] {
const paths: string[] = [];
// 1. 使用环境变量(如果设置) | Use environment variable if set
@@ -147,16 +147,16 @@ export class RuntimeResolver {
* Find engine modules path (where compiled modules with module.json are)
* module.json
*/
private async findEngineModulesPath(): Promise<string> {
private async _findEngineModulesPath(): Promise<string> {
// Try installed editor locations first (production mode)
for (const installedPath of this.getInstalledEnginePaths()) {
for (const installedPath of this._getInstalledEnginePaths()) {
if (await TauriAPI.pathExists(`${installedPath}/index.json`)) {
return installedPath;
}
}
// Try workspace packages directory (dev mode)
const workspacePath = `${this.baseDir}\\packages`;
const workspacePath = `${this._baseDir}\\packages`;
if (await TauriAPI.pathExists(`${workspacePath}\\core\\module.json`)) {
return workspacePath;
}
@@ -172,14 +172,14 @@ export class RuntimeResolver {
* packages module.json
*/
async getAvailableModules(): Promise<ModuleManifest[]> {
if (!this.initialized) {
if (!this._initialized) {
await this.initialize();
}
const modules: ModuleManifest[] = [];
// Try to read index.json if it exists (installed editor)
const indexPath = `${this.engineModulesPath}\\index.json`;
const indexPath = `${this._engineModulesPath}\\index.json`;
if (await TauriAPI.pathExists(indexPath)) {
try {
const indexContent = await TauriAPI.readFileContent(indexPath);
@@ -191,11 +191,11 @@ export class RuntimeResolver {
}
// Scan packages directory for module.json files
const packageEntries = await TauriAPI.listDirectory(this.engineModulesPath);
const packageEntries = await TauriAPI.listDirectory(this._engineModulesPath);
for (const entry of packageEntries) {
if (!entry.is_dir) continue;
const manifestPath = `${this.engineModulesPath}\\${entry.name}\\module.json`;
const manifestPath = `${this._engineModulesPath}\\${entry.name}\\module.json`;
if (await TauriAPI.pathExists(manifestPath)) {
try {
const content = await TauriAPI.readFileContent(manifestPath);
@@ -210,14 +210,14 @@ export class RuntimeResolver {
}
// Sort by dependencies
return this.sortModulesByDependencies(modules);
return this._sortModulesByDependencies(modules);
}
/**
* Sort modules by dependencies (topological sort)
*
*/
private sortModulesByDependencies(modules: ModuleManifest[]): ModuleManifest[] {
private _sortModulesByDependencies(modules: ModuleManifest[]): ModuleManifest[] {
const sorted: ModuleManifest[] = [];
const visited = new Set<string>();
const moduleMap = new Map(modules.map(m => [m.id, m]));
@@ -246,7 +246,7 @@ export class RuntimeResolver {
* libs/{moduleId}/{moduleId}.js
*/
async prepareRuntimeFiles(targetDir: string): Promise<{ modules: ModuleManifest[], importMap: Record<string, string> }> {
if (!this.initialized) {
if (!this._initialized) {
await this.initialize();
}
@@ -267,7 +267,7 @@ export class RuntimeResolver {
// Copy each module's dist files
const missingModules: string[] = [];
for (const module of modules) {
const moduleDistDir = `${this.engineModulesPath}\\${module.id}\\dist`;
const moduleDistDir = `${this._engineModulesPath}\\${module.id}\\dist`;
const moduleSrcFile = `${moduleDistDir}\\index.mjs`;
// Check for index.mjs or index.js
@@ -287,7 +287,7 @@ export class RuntimeResolver {
// Copy all chunk files (code splitting creates chunk-*.js files)
// 复制所有 chunk 文件(代码分割会创建 chunk-*.js 文件)
await this.copyChunkFiles(moduleDistDir, dstModuleDir);
await this._copyChunkFiles(moduleDistDir, dstModuleDir);
// Add to import map using module.name from module.json
// 使用 module.json 中的 module.name 作为 import map 的 key
@@ -310,13 +310,13 @@ export class RuntimeResolver {
}
// Copy external dependencies (e.g., rapier2d)
await this.copyExternalDependencies(modules, libsDir, importMap);
await this._copyExternalDependencies(modules, libsDir, importMap);
// Copy engine WASM files to libs/es-engine/
await this.copyEngineWasm(libsDir);
await this._copyEngineWasm(libsDir);
// Copy module-specific WASM files
await this.copyModuleWasm(modules, targetDir);
await this._copyModuleWasm(modules, targetDir);
console.log(`[RuntimeResolver] Prepared ${copiedModules.length} modules for browser preview`);
@@ -327,7 +327,7 @@ export class RuntimeResolver {
* Copy chunk files from dist directory (for code-split modules)
* dist chunk
*/
private async copyChunkFiles(srcDir: string, dstDir: string): Promise<void> {
private async _copyChunkFiles(srcDir: string, dstDir: string): Promise<void> {
try {
const entries = await TauriAPI.listDirectory(srcDir);
for (const entry of entries) {
@@ -347,7 +347,7 @@ export class RuntimeResolver {
* Copy external dependencies like rapier2d
* rapier2d
*/
private async copyExternalDependencies(
private async _copyExternalDependencies(
modules: ModuleManifest[],
libsDir: string,
importMap: Record<string, string>
@@ -363,7 +363,7 @@ export class RuntimeResolver {
for (const dep of externalDeps) {
const depId = dep.startsWith('@esengine/') ? dep.slice(10) : dep.replace(/^@[^/]+\//, '');
const srcDistDir = `${this.engineModulesPath}\\${depId}\\dist`;
const srcDistDir = `${this._engineModulesPath}\\${depId}\\dist`;
let srcFile = `${srcDistDir}\\index.mjs`;
if (!await TauriAPI.pathExists(srcFile)) {
srcFile = `${srcDistDir}\\index.js`;
@@ -379,7 +379,7 @@ export class RuntimeResolver {
await TauriAPI.copyFile(srcFile, dstFile);
// Copy chunk files for external dependencies too
await this.copyChunkFiles(srcDistDir, dstModuleDir);
await this._copyChunkFiles(srcDistDir, dstModuleDir);
importMap[dep] = `./libs/${depId}/${depId}.js`;
console.log(`[RuntimeResolver] Copied external dependency: ${depId}`);
@@ -391,17 +391,17 @@ export class RuntimeResolver {
* WASM
* Get search paths for engine WASM files
*/
private getEngineWasmSearchPaths(): string[] {
private _getEngineWasmSearchPaths(): string[] {
const paths: string[] = [];
// 1. 开发模式:工作区内的 engine 包 | Dev mode: engine package in workspace
paths.push(`${this.baseDir}\\packages\\${ENGINE_WASM_CONFIG.packageName}\\pkg`);
paths.push(`${this._baseDir}\\packages\\${ENGINE_WASM_CONFIG.packageName}\\pkg`);
// 2. 相对于引擎模块路径 | Relative to engine modules path
paths.push(`${this.engineModulesPath}\\..\\..\\${ENGINE_WASM_CONFIG.packageName}\\pkg`);
paths.push(`${this._engineModulesPath}\\..\\..\\${ENGINE_WASM_CONFIG.packageName}\\pkg`);
// 3. 生产模式:安装目录中的 wasm 文件夹 | Production mode: wasm folder in install dir
for (const installedPath of this.getInstalledEnginePaths()) {
for (const installedPath of this._getInstalledEnginePaths()) {
// 将 /engine 替换为 /wasm | Replace /engine with /wasm
const wasmPath = installedPath.replace(/[/\\]engine$/, '/wasm');
paths.push(wasmPath);
@@ -414,14 +414,14 @@ export class RuntimeResolver {
* Copy engine WASM files
* WASM
*/
private async copyEngineWasm(libsDir: string): Promise<void> {
private async _copyEngineWasm(libsDir: string): Promise<void> {
const esEngineDir = `${libsDir}\\${ENGINE_WASM_CONFIG.dirName}`;
if (!await TauriAPI.pathExists(esEngineDir)) {
await TauriAPI.createDirectory(esEngineDir);
}
// Try different locations for engine WASM
const wasmSearchPaths = this.getEngineWasmSearchPaths();
const wasmSearchPaths = this._getEngineWasmSearchPaths();
for (const searchPath of wasmSearchPaths) {
if (await TauriAPI.pathExists(searchPath)) {
@@ -444,7 +444,7 @@ export class RuntimeResolver {
* Copy module-specific WASM files (e.g., physics)
* WASM
*/
private async copyModuleWasm(modules: ModuleManifest[], targetDir: string): Promise<void> {
private async _copyModuleWasm(modules: ModuleManifest[], targetDir: string): Promise<void> {
for (const module of modules) {
if (!module.requiresWasm || !module.wasmPaths?.length) continue;
@@ -463,16 +463,16 @@ export class RuntimeResolver {
// Build search paths - check module's own pkg, external deps, and common locations
const searchPaths: string[] = [
`${this.engineModulesPath}\\${module.id}\\pkg\\${wasmFileName}`,
`${this.baseDir}\\packages\\${module.id}\\pkg\\${wasmFileName}`,
`${this._engineModulesPath}\\${module.id}\\pkg\\${wasmFileName}`,
`${this._baseDir}\\packages\\${module.id}\\pkg\\${wasmFileName}`,
];
// Check external dependencies for WASM (e.g., physics-rapier2d uses rapier2d's WASM)
if (module.externalDependencies) {
for (const dep of module.externalDependencies) {
const depId = dep.startsWith('@esengine/') ? dep.slice(10) : dep.replace(/^@[^/]+\//, '');
searchPaths.push(`${this.engineModulesPath}\\${depId}\\pkg\\${wasmFileName}`);
searchPaths.push(`${this.baseDir}\\packages\\${depId}\\pkg\\${wasmFileName}`);
searchPaths.push(`${this._engineModulesPath}\\${depId}\\pkg\\${wasmFileName}`);
searchPaths.push(`${this._baseDir}\\packages\\${depId}\\pkg\\${wasmFileName}`);
}
}
@@ -501,7 +501,7 @@ export class RuntimeResolver {
*
*/
getBaseDir(): string {
return this.baseDir;
return this._baseDir;
}
/**
@@ -509,6 +509,6 @@ export class RuntimeResolver {
*
*/
getEngineModulesPath(): string {
return this.engineModulesPath;
return this._engineModulesPath;
}
}
@@ -1,68 +1,68 @@
export class SettingsService {
private static instance: SettingsService;
private settings: Map<string, any> = new Map();
private storageKey = 'editor-settings';
private static _instance: SettingsService;
private _settings: Map<string, any> = new Map();
private _storageKey = 'editor-settings';
private constructor() {
this.loadSettings();
this._loadSettings();
}
public static getInstance(): SettingsService {
if (!SettingsService.instance) {
SettingsService.instance = new SettingsService();
if (!SettingsService._instance) {
SettingsService._instance = new SettingsService();
}
return SettingsService.instance;
return SettingsService._instance;
}
private loadSettings(): void {
private _loadSettings(): void {
try {
const stored = localStorage.getItem(this.storageKey);
const stored = localStorage.getItem(this._storageKey);
if (stored) {
const data = JSON.parse(stored);
this.settings = new Map(Object.entries(data));
this._settings = new Map(Object.entries(data));
}
} catch (error) {
console.error('[SettingsService] Failed to load settings:', error);
}
}
private saveSettings(): void {
private _saveSettings(): void {
try {
const data = Object.fromEntries(this.settings);
localStorage.setItem(this.storageKey, JSON.stringify(data));
const data = Object.fromEntries(this._settings);
localStorage.setItem(this._storageKey, JSON.stringify(data));
} catch (error) {
console.error('[SettingsService] Failed to save settings:', error);
}
}
public get<T>(key: string, defaultValue: T): T {
if (this.settings.has(key)) {
return this.settings.get(key) as T;
if (this._settings.has(key)) {
return this._settings.get(key) as T;
}
return defaultValue;
}
public set<T>(key: string, value: T): void {
this.settings.set(key, value);
this.saveSettings();
this._settings.set(key, value);
this._saveSettings();
}
public has(key: string): boolean {
return this.settings.has(key);
return this._settings.has(key);
}
public delete(key: string): void {
this.settings.delete(key);
this.saveSettings();
this._settings.delete(key);
this._saveSettings();
}
public clear(): void {
this.settings.clear();
this.saveSettings();
this._settings.clear();
this._saveSettings();
}
public getAll(): Record<string, any> {
return Object.fromEntries(this.settings);
return Object.fromEntries(this._settings);
}
public getRecentProjects(): string[] {
-235
View File
@@ -1,235 +0,0 @@
.game-view {
position: relative;
width: 100%;
height: 100%;
background: #000;
display: flex;
flex-direction: column;
overflow: hidden;
}
.game-view-toolbar {
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 8px;
background: var(--color-bg-elevated);
border-bottom: 1px solid var(--color-border-default);
flex-shrink: 0;
gap: 8px;
height: 26px;
z-index: var(--z-index-above);
}
.game-view-toolbar-left {
display: flex;
align-items: center;
gap: 4px;
}
.game-view-toolbar-right {
display: flex;
align-items: center;
gap: 4px;
}
.game-view-btn {
display: inline-flex;
align-items: center;
justify-content: center;
gap: 2px;
min-width: 32px;
height: 32px;
background: transparent;
border: 1px solid transparent;
border-radius: var(--radius-sm);
color: var(--color-text-secondary);
cursor: pointer;
transition: all var(--transition-fast);
padding: 0 6px;
}
.game-view-btn:hover:not(:disabled) {
background: var(--color-bg-hover);
color: var(--color-text-primary);
border-color: var(--color-border-hover);
}
.game-view-btn.active {
background: var(--color-primary);
color: var(--color-text-inverse);
border-color: var(--color-primary);
}
.game-view-btn:disabled {
opacity: 0.4;
cursor: not-allowed;
}
.game-view-btn:active:not(:disabled) {
transform: scale(0.95);
}
.game-view-divider {
width: 1px;
height: 24px;
background: var(--color-border-default);
margin: 0 4px;
}
.game-view-dropdown {
position: relative;
}
.game-view-dropdown-menu {
position: absolute;
top: 100%;
left: 0;
margin-top: 4px;
background: var(--color-bg-elevated);
border: 1px solid var(--color-border-default);
border-radius: var(--radius-sm);
box-shadow: var(--shadow-lg);
z-index: var(--z-index-dropdown);
min-width: 160px;
padding: 4px;
animation: dropdownFadeIn 0.15s ease-out;
}
@keyframes dropdownFadeIn {
from {
opacity: 0;
transform: translateY(-4px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.game-view-dropdown-menu button {
display: flex;
align-items: center;
gap: 8px;
width: 100%;
padding: 8px 12px;
background: transparent;
border: none;
border-radius: var(--radius-xs);
color: var(--color-text-primary);
font-size: 13px;
cursor: pointer;
text-align: left;
}
.game-view-dropdown-menu button:hover {
background: var(--color-bg-hover);
}
.game-view-canvas {
flex: 1;
width: 100%;
height: 100%;
display: block;
background: #000;
user-select: none;
}
.game-view-overlay {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
display: flex;
align-items: center;
justify-content: center;
background: rgba(0, 0, 0, 0.6);
pointer-events: none;
}
.game-view-overlay-content {
display: flex;
flex-direction: column;
align-items: center;
gap: 12px;
color: var(--color-text-secondary);
}
.game-view-overlay-content svg {
opacity: 0.5;
}
.game-view-overlay-content span {
font-size: 14px;
}
.game-view-stats {
position: absolute;
bottom: 12px;
right: 12px;
display: flex;
flex-direction: column;
gap: 4px;
background: rgba(0, 0, 0, 0.8);
backdrop-filter: blur(8px);
border: 1px solid var(--color-border-subtle);
border-radius: var(--radius-sm);
padding: 8px 12px;
font-family: var(--font-family-mono);
font-size: 11px;
pointer-events: none;
z-index: var(--z-index-above);
animation: fadeIn 0.2s ease-out;
}
@keyframes fadeIn {
from {
opacity: 0;
transform: translateY(4px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.game-view-stat {
display: flex;
align-items: center;
gap: 8px;
white-space: nowrap;
}
.game-view-stat-label {
color: var(--color-text-secondary);
font-weight: 500;
}
.game-view-stat-value {
color: var(--color-primary);
font-weight: 600;
font-variant-numeric: tabular-nums;
}
.game-view:fullscreen {
background: #000;
}
.game-view:fullscreen .game-view-toolbar {
display: none;
}
.game-view:fullscreen .game-view-overlay {
display: none;
}
@media (prefers-reduced-motion: reduce) {
.game-view-btn {
transition: none;
}
.game-view-dropdown-menu {
animation: none;
}
}
@@ -1,509 +0,0 @@
.plugin-manager-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.6);
display: flex;
align-items: center;
justify-content: center;
z-index: var(--z-index-modal);
backdrop-filter: blur(4px);
}
.plugin-manager-window {
background: var(--color-bg-elevated);
border: 1px solid var(--color-border-default);
border-radius: 8px;
width: 90%;
max-width: 1000px;
height: 80%;
max-height: 700px;
display: flex;
flex-direction: column;
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
overflow: hidden;
}
.plugin-manager-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 16px 20px;
background: var(--color-bg-overlay);
border-bottom: 1px solid var(--color-border-default);
}
.plugin-manager-title {
display: flex;
align-items: center;
gap: 12px;
color: var(--color-text-primary);
}
.plugin-manager-title h2 {
margin: 0;
font-size: 18px;
font-weight: 600;
}
.plugin-manager-close {
background: none;
border: none;
color: var(--color-text-secondary);
cursor: pointer;
padding: 6px;
border-radius: 4px;
transition: all 0.2s;
display: flex;
align-items: center;
justify-content: center;
}
.plugin-manager-close:hover {
background: var(--color-bg-hover);
color: var(--color-text-primary);
}
.plugin-toolbar {
display: flex;
align-items: center;
justify-content: space-between;
padding: 12px 20px;
background: var(--color-bg-elevated);
border-bottom: 1px solid var(--color-border-default);
gap: 12px;
}
.plugin-toolbar-left,
.plugin-toolbar-right {
display: flex;
align-items: center;
gap: 8px;
}
.plugin-search {
display: flex;
align-items: center;
gap: 6px;
padding: 6px 12px;
background: var(--color-bg-base);
border: 1px solid var(--color-border-default);
border-radius: 4px;
color: var(--color-text-secondary);
}
.plugin-search input {
border: none;
background: none;
outline: none;
color: var(--color-text-primary);
font-size: 13px;
min-width: 250px;
}
.plugin-stats {
display: flex;
align-items: center;
gap: 12px;
padding: 0 8px;
font-size: 12px;
}
.plugin-stats .stat-item {
display: flex;
align-items: center;
gap: 4px;
}
.plugin-stats .stat-item.enabled {
color: var(--color-success);
}
.plugin-stats .stat-item.disabled {
color: var(--color-text-secondary);
}
.plugin-view-mode {
display: flex;
gap: 2px;
background: var(--color-bg-base);
border: 1px solid var(--color-border-default);
border-radius: 4px;
overflow: hidden;
}
.plugin-view-mode button {
padding: 6px 10px;
background: none;
border: none;
color: var(--color-text-secondary);
cursor: pointer;
transition: all 0.2s;
display: flex;
align-items: center;
justify-content: center;
}
.plugin-view-mode button:hover {
background: var(--color-bg-hover);
}
.plugin-view-mode button.active {
background: var(--color-primary);
color: white;
}
.plugin-content {
flex: 1;
overflow-y: auto;
padding: 20px;
}
.plugin-empty {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 100%;
color: var(--color-text-secondary);
gap: 16px;
}
.plugin-categories {
display: flex;
flex-direction: column;
gap: 16px;
}
.plugin-category {
background: var(--color-bg-overlay);
border: 1px solid var(--color-border-default);
border-radius: 6px;
overflow: hidden;
}
.plugin-category-header {
display: flex;
align-items: center;
gap: 8px;
padding: 12px 16px;
background: var(--color-bg-elevated);
cursor: pointer;
user-select: none;
transition: background 0.2s;
}
.plugin-category-header:hover {
background: var(--color-bg-hover);
}
.plugin-category-toggle {
background: none;
border: none;
padding: 0;
color: var(--color-text-secondary);
cursor: pointer;
display: flex;
align-items: center;
}
.plugin-category-icon {
font-size: 18px;
}
.plugin-category-name {
flex: 1;
font-size: 14px;
font-weight: 600;
color: var(--color-text-primary);
}
.plugin-category-count {
font-size: 12px;
color: var(--color-text-secondary);
background: var(--color-bg-overlay);
padding: 3px 10px;
border-radius: 12px;
}
.plugin-category-content {
padding: 16px;
}
.plugin-category-content.grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
gap: 12px;
}
.plugin-category-content.list {
display: flex;
flex-direction: column;
gap: 8px;
}
/* Plugin Card (Grid View) */
.plugin-card {
background: var(--color-bg-base);
border: 1px solid var(--color-border-default);
border-radius: 6px;
padding: 14px;
transition: all 0.2s;
}
.plugin-card:hover {
border-color: var(--color-primary);
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
.plugin-card.disabled {
opacity: 0.6;
}
.plugin-card-header {
display: flex;
align-items: flex-start;
gap: 10px;
margin-bottom: 10px;
}
.plugin-card-icon {
font-size: 24px;
color: var(--color-primary);
}
.plugin-card-info {
flex: 1;
}
.plugin-card-title {
font-size: 14px;
font-weight: 600;
color: var(--color-text-primary);
margin-bottom: 2px;
}
.plugin-card-version {
font-size: 11px;
color: var(--color-text-secondary);
}
.plugin-toggle {
background: none;
border: none;
padding: 4px;
cursor: pointer;
color: var(--color-text-secondary);
transition: all 0.2s;
border-radius: 4px;
display: flex;
align-items: center;
justify-content: center;
}
.plugin-toggle:hover {
background: var(--color-bg-hover);
}
.plugin-toggle.enabled {
color: var(--color-success);
}
.plugin-toggle.disabled {
color: var(--color-text-secondary);
}
.plugin-card-description {
font-size: 12px;
color: var(--color-text-secondary);
line-height: 1.5;
margin-bottom: 10px;
}
.plugin-card-footer {
display: flex;
justify-content: space-between;
align-items: center;
padding-top: 10px;
border-top: 1px solid var(--color-border-default);
font-size: 11px;
}
.plugin-card-category {
color: var(--color-text-secondary);
}
.plugin-card-installed {
color: var(--color-text-tertiary);
}
/* Plugin List (List View) */
.plugin-list-item {
display: flex;
align-items: center;
gap: 12px;
padding: 12px 14px;
background: var(--color-bg-base);
border: 1px solid var(--color-border-default);
border-radius: 6px;
transition: all 0.2s;
}
.plugin-list-item:hover {
border-color: var(--color-primary);
}
.plugin-list-item.disabled {
opacity: 0.6;
}
.plugin-list-icon {
font-size: 20px;
color: var(--color-primary);
}
.plugin-list-info {
flex: 1;
}
.plugin-list-name {
display: flex;
align-items: baseline;
gap: 8px;
font-size: 14px;
font-weight: 600;
color: var(--color-text-primary);
margin-bottom: 2px;
}
.plugin-list-version {
font-size: 11px;
font-weight: normal;
color: var(--color-text-secondary);
}
.plugin-list-description {
font-size: 12px;
color: var(--color-text-secondary);
line-height: 1.4;
}
.plugin-list-status {
margin-right: 8px;
}
.status-badge {
padding: 4px 10px;
border-radius: 4px;
font-size: 11px;
font-weight: 500;
}
.status-badge.enabled {
background: rgba(34, 197, 94, 0.15);
color: var(--color-success);
}
.status-badge.disabled {
background: var(--color-bg-overlay);
color: var(--color-text-secondary);
}
.plugin-list-toggle {
padding: 6px 14px;
background: var(--color-bg-overlay);
border: 1px solid var(--color-border-default);
border-radius: 4px;
color: var(--color-text-primary);
font-size: 12px;
cursor: pointer;
transition: all 0.2s;
white-space: nowrap;
}
.plugin-list-toggle:hover {
background: var(--color-primary);
border-color: var(--color-primary);
color: white;
}
/* Scrollbar */
.plugin-content::-webkit-scrollbar {
width: 10px;
}
.plugin-content::-webkit-scrollbar-track {
background: var(--color-bg-elevated);
}
.plugin-content::-webkit-scrollbar-thumb {
background: var(--color-border-default);
border-radius: 5px;
}
.plugin-content::-webkit-scrollbar-thumb:hover {
background: var(--color-text-secondary);
}
/* Plugin Manager Tabs */
.plugin-manager-tabs {
display: flex;
gap: 4px;
padding: 0 20px;
background: var(--color-bg-overlay);
border-bottom: 1px solid var(--color-border-default);
}
.plugin-manager-tab {
display: flex;
align-items: center;
gap: 8px;
padding: 12px 20px;
background: transparent;
border: none;
border-bottom: 2px solid transparent;
color: var(--color-text-secondary);
font-size: 14px;
font-weight: 500;
cursor: pointer;
transition: all 0.2s;
}
.plugin-manager-tab:hover {
color: var(--color-text-primary);
background: rgba(255, 255, 255, 0.05);
}
.plugin-manager-tab.active {
color: var(--color-accent);
border-bottom-color: var(--color-accent);
}
.plugin-publish-btn {
display: inline-flex;
align-items: center;
gap: 6px;
padding: 6px 12px;
background: var(--color-accent, #0e639c);
color: white;
border: none;
border-radius: 4px;
font-size: 12px;
font-weight: 500;
cursor: pointer;
transition: all 0.2s;
}
.plugin-publish-btn:hover {
background: var(--color-accent-hover, #1177bb);
}
.plugin-card-footer .plugin-publish-btn {
margin-left: auto;
}
.plugin-list-item .plugin-publish-btn {
margin-left: 8px;
}
@@ -1,495 +0,0 @@
.plugin-market-panel {
display: flex;
flex-direction: column;
height: 100%;
background: var(--color-bg-primary, #1e1e1e);
color: var(--color-text-primary, #cccccc);
}
.plugin-market-toolbar {
display: flex;
align-items: center;
gap: 8px;
padding: 0 8px;
border-bottom: 1px solid var(--color-border, #333);
background: var(--color-bg-secondary, #252526);
height: 26px;
}
.plugin-market-search {
flex: 1;
display: flex;
align-items: center;
gap: 8px;
padding: 8px 12px;
background: var(--color-bg-primary, #1e1e1e);
border: 1px solid var(--color-border, #333);
border-radius: 4px;
}
.plugin-market-search input {
flex: 1;
background: transparent;
border: none;
outline: none;
color: var(--color-text-primary, #cccccc);
font-size: 14px;
}
.plugin-market-filters {
display: flex;
gap: 8px;
align-items: center;
}
.plugin-market-filter-select {
padding: 8px 12px;
background: var(--color-bg-primary, #1e1e1e);
border: 1px solid var(--color-border, #333);
border-radius: 4px;
color: var(--color-text-primary, #cccccc);
font-size: 13px;
cursor: pointer;
}
.plugin-market-filter-select:hover {
border-color: var(--color-accent, #0e639c);
}
.plugin-market-refresh {
padding: 8px 12px;
background: var(--color-bg-primary, #1e1e1e);
border: 1px solid var(--color-border, #333);
border-radius: 4px;
color: var(--color-text-primary, #cccccc);
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
}
.plugin-market-refresh:hover {
background: var(--color-bg-hover, #2d2d30);
}
.plugin-market-publish {
padding: 8px 16px;
background: var(--color-accent, #0e639c);
border: none;
border-radius: 4px;
color: white;
cursor: pointer;
display: flex;
align-items: center;
gap: 6px;
font-size: 13px;
font-weight: 500;
transition: all 0.2s;
}
.plugin-market-publish:hover {
background: var(--color-accent-hover, #1177bb);
}
.plugin-market-content {
flex: 1;
overflow-y: auto;
padding: 16px;
}
.plugin-market-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
gap: 16px;
}
.plugin-market-card {
background: var(--color-bg-secondary, #252526);
border: 1px solid var(--color-border, #333);
border-radius: 8px;
padding: 16px;
display: flex;
flex-direction: column;
gap: 12px;
transition: all 0.2s;
}
.plugin-market-card:hover {
border-color: var(--color-accent, #0e639c);
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);
}
.plugin-market-card-header {
display: flex;
gap: 12px;
align-items: flex-start;
}
.plugin-market-card-icon {
flex-shrink: 0;
width: 48px;
height: 48px;
display: flex;
align-items: center;
justify-content: center;
background: var(--color-accent-bg, rgba(14, 99, 156, 0.1));
border-radius: 8px;
color: var(--color-accent, #0e639c);
}
.plugin-market-card-info {
flex: 1;
min-width: 0;
}
.plugin-market-card-title {
display: flex;
align-items: center;
gap: 6px;
font-size: 16px;
font-weight: 600;
margin-bottom: 4px;
}
.plugin-market-card-title span:first-child {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.plugin-market-badge {
display: inline-flex;
align-items: center;
padding: 2px 6px;
border-radius: 4px;
font-size: 11px;
font-weight: 600;
text-transform: uppercase;
}
.plugin-market-badge.official {
background: rgba(52, 199, 89, 0.15);
color: #34c759;
}
.plugin-market-card-meta {
display: flex;
gap: 12px;
font-size: 12px;
color: var(--color-text-secondary, #858585);
}
.plugin-market-card-author {
display: flex;
align-items: center;
gap: 4px;
}
.plugin-market-card-description {
font-size: 13px;
line-height: 1.5;
color: var(--color-text-secondary, #858585);
display: -webkit-box;
-webkit-line-clamp: 3;
-webkit-box-orient: vertical;
overflow: hidden;
}
.plugin-market-card-tags {
display: flex;
gap: 6px;
flex-wrap: wrap;
}
.plugin-market-tag {
padding: 4px 8px;
background: var(--color-bg-tertiary, #333);
border-radius: 4px;
font-size: 11px;
color: var(--color-text-secondary, #858585);
}
.plugin-market-card-footer {
display: flex;
justify-content: space-between;
align-items: center;
padding-top: 12px;
border-top: 1px solid var(--color-border, #333);
}
.plugin-market-card-link {
display: flex;
align-items: center;
gap: 4px;
font-size: 12px;
color: var(--color-accent, #0e639c);
text-decoration: none;
background: transparent;
border: none;
padding: 0;
cursor: pointer;
transition: opacity 0.2s;
}
.plugin-market-card-link:hover {
opacity: 0.8;
}
.plugin-market-card-actions {
display: flex;
gap: 8px;
}
.plugin-market-btn {
display: flex;
align-items: center;
gap: 6px;
padding: 6px 12px;
border: none;
border-radius: 4px;
font-size: 12px;
font-weight: 500;
cursor: pointer;
transition: all 0.2s;
}
.plugin-market-btn.install {
background: var(--color-accent, #0e639c);
color: white;
}
.plugin-market-btn.install:hover {
background: var(--color-accent-hover, #1177bb);
}
.plugin-market-btn.installed {
background: rgba(52, 199, 89, 0.15);
color: #34c759;
}
.plugin-market-btn.installed:hover {
background: rgba(52, 199, 89, 0.25);
}
.plugin-market-btn.update {
background: rgba(255, 149, 0, 0.15);
color: #ff9500;
}
.plugin-market-btn.update:hover {
background: rgba(255, 149, 0, 0.25);
}
.plugin-market-btn.installing {
background: var(--color-bg-tertiary, #333);
color: var(--color-text-secondary, #858585);
cursor: not-allowed;
}
.plugin-market-loading,
.plugin-market-error,
.plugin-market-empty {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 100%;
gap: 16px;
color: var(--color-text-secondary, #858585);
}
.plugin-market-error {
gap: 12px;
max-width: 600px;
margin: 0 auto;
padding: 40px 20px;
text-align: center;
}
.plugin-market-error .error-icon {
color: #ff9500;
margin-bottom: 8px;
}
.plugin-market-error h3 {
font-size: 20px;
font-weight: 600;
color: var(--color-text-primary, #cccccc);
margin: 0;
}
.plugin-market-error .error-description {
font-size: 14px;
color: var(--color-text-secondary, #858585);
line-height: 1.6;
margin: 8px 0;
}
.plugin-market-error .error-details {
background: rgba(255, 59, 48, 0.1);
border: 1px solid rgba(255, 59, 48, 0.3);
border-radius: 6px;
padding: 12px 16px;
margin: 16px 0;
max-width: 100%;
}
.plugin-market-error .error-message {
font-size: 12px;
font-family: 'Consolas', 'Monaco', 'Courier New', monospace;
color: #ff3b30;
margin: 0;
word-break: break-word;
}
.plugin-market-error .retry-button,
.plugin-market-loading button {
display: flex;
align-items: center;
gap: 8px;
padding: 10px 20px;
background: var(--color-accent, #0e639c);
color: white;
border: none;
border-radius: 6px;
font-size: 14px;
font-weight: 500;
cursor: pointer;
transition: all 0.2s;
margin-top: 8px;
}
.plugin-market-error .retry-button:hover,
.plugin-market-loading button:hover {
background: var(--color-accent-hover, #1177bb);
transform: translateY(-1px);
box-shadow: 0 2px 8px rgba(14, 99, 156, 0.3);
}
.spinning {
animation: spin 1s linear infinite;
}
@keyframes spin {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
.plugin-market-direct-source-toggle {
display: flex;
align-items: center;
gap: 10px;
padding: 8px 14px;
background: rgba(14, 99, 156, 0.1);
border: 1px solid rgba(14, 99, 156, 0.3);
border-radius: 6px;
font-size: 13px;
font-weight: 500;
color: var(--color-accent, #0e639c);
cursor: pointer;
transition: all 0.2s ease;
user-select: none;
}
.plugin-market-direct-source-toggle:hover {
background: rgba(14, 99, 156, 0.15);
border-color: rgba(14, 99, 156, 0.5);
transform: translateY(-1px);
box-shadow: 0 2px 6px rgba(14, 99, 156, 0.2);
}
.plugin-market-direct-source-toggle input[type="checkbox"] {
position: relative;
width: 38px;
height: 20px;
margin: 0;
cursor: pointer;
appearance: none;
background: rgba(255, 255, 255, 0.1);
border: 1px solid rgba(255, 255, 255, 0.2);
border-radius: 10px;
transition: all 0.3s ease;
}
.plugin-market-direct-source-toggle input[type="checkbox"]:checked {
background: var(--color-accent, #0e639c);
border-color: var(--color-accent, #0e639c);
}
.plugin-market-direct-source-toggle input[type="checkbox"]::before {
content: '';
position: absolute;
width: 16px;
height: 16px;
top: 1px;
left: 1px;
background: white;
border-radius: 50%;
transition: all 0.3s ease;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
}
.plugin-market-direct-source-toggle input[type="checkbox"]:checked::before {
left: 19px;
}
.plugin-market-direct-source-toggle .toggle-label {
white-space: nowrap;
font-weight: 500;
}
/* 版本选择器 */
.plugin-market-version-select {
padding: 2px 6px;
background: var(--color-bg-tertiary, #333);
border: 1px solid var(--color-border, #333);
border-radius: 3px;
color: var(--color-text-primary, #cccccc);
font-size: 11px;
cursor: pointer;
transition: all 0.2s;
}
.plugin-market-version-select:hover {
border-color: var(--color-accent, #0e639c);
background: var(--color-bg-hover, #2d2d30);
}
.plugin-market-version-select:focus {
outline: none;
border-color: var(--color-accent, #0e639c);
}
/* 更新日志 */
.plugin-market-version-changes {
margin: 8px 0;
padding: 8px;
background: rgba(14, 99, 156, 0.1);
border: 1px solid rgba(14, 99, 156, 0.2);
border-radius: 4px;
}
.plugin-market-version-changes summary {
cursor: pointer;
font-size: 12px;
font-weight: 500;
color: var(--color-accent, #0e639c);
padding: 4px;
user-select: none;
}
.plugin-market-version-changes summary:hover {
opacity: 0.8;
}
.plugin-market-version-changes p {
margin: 8px 0 0 0;
padding-left: 4px;
font-size: 12px;
line-height: 1.5;
color: var(--color-text-secondary, #858585);
}
@@ -1,382 +0,0 @@
.plugin-panel {
display: flex;
flex-direction: column;
height: 100%;
background: var(--color-bg-secondary);
color: var(--color-text-primary);
}
.plugin-toolbar {
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 8px;
background: var(--color-bg-tertiary);
border-bottom: 1px solid var(--color-border);
gap: 8px;
height: 26px;
}
.plugin-toolbar-left,
.plugin-toolbar-right {
display: flex;
align-items: center;
gap: 8px;
}
.plugin-search {
display: flex;
align-items: center;
gap: 6px;
padding: 4px 8px;
background: var(--color-bg-primary);
border: 1px solid var(--color-border);
border-radius: 4px;
color: var(--color-text-secondary);
}
.plugin-search input {
border: none;
background: none;
outline: none;
color: var(--color-text-primary);
font-size: 12px;
min-width: 200px;
}
.plugin-stats {
display: flex;
align-items: center;
gap: 12px;
padding: 0 8px;
font-size: 12px;
}
.plugin-stats .stat-item {
display: flex;
align-items: center;
gap: 4px;
}
.plugin-stats .stat-item.enabled {
color: var(--color-success);
}
.plugin-stats .stat-item.disabled {
color: var(--color-text-secondary);
}
.plugin-view-mode {
display: flex;
gap: 2px;
background: var(--color-bg-primary);
border: 1px solid var(--color-border);
border-radius: 4px;
overflow: hidden;
}
.plugin-view-mode button {
padding: 4px 8px;
background: none;
border: none;
color: var(--color-text-secondary);
cursor: pointer;
transition: all 0.2s;
}
.plugin-view-mode button:hover {
background: var(--color-bg-hover);
}
.plugin-view-mode button.active {
background: var(--color-primary);
color: white;
}
.plugin-content {
flex: 1;
overflow-y: auto;
padding: 16px;
}
.plugin-empty {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 100%;
color: var(--color-text-secondary);
gap: 16px;
}
.plugin-categories {
display: flex;
flex-direction: column;
gap: 16px;
}
.plugin-category {
background: var(--color-bg-tertiary);
border: 1px solid var(--color-border);
border-radius: 6px;
overflow: hidden;
}
.plugin-category-header {
display: flex;
align-items: center;
gap: 8px;
padding: 10px 12px;
background: var(--color-bg-secondary);
cursor: pointer;
user-select: none;
transition: background 0.2s;
}
.plugin-category-header:hover {
background: var(--color-bg-hover);
}
.plugin-category-toggle {
background: none;
border: none;
padding: 0;
color: var(--color-text-secondary);
cursor: pointer;
display: flex;
align-items: center;
}
.plugin-category-icon {
font-size: 18px;
}
.plugin-category-name {
flex: 1;
font-size: 13px;
font-weight: 600;
color: var(--color-text-primary);
}
.plugin-category-count {
font-size: 12px;
color: var(--color-text-secondary);
background: var(--color-bg-tertiary);
padding: 2px 8px;
border-radius: 10px;
}
.plugin-category-content {
padding: 12px;
}
.plugin-category-content.grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
gap: 12px;
}
.plugin-category-content.list {
display: flex;
flex-direction: column;
gap: 8px;
}
/* Plugin Card (Grid View) */
.plugin-card {
background: var(--color-bg-primary);
border: 1px solid var(--color-border);
border-radius: 6px;
padding: 12px;
transition: all 0.2s;
}
.plugin-card:hover {
border-color: var(--color-primary);
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
.plugin-card.disabled {
opacity: 0.6;
}
.plugin-card-header {
display: flex;
align-items: flex-start;
gap: 10px;
margin-bottom: 10px;
}
.plugin-card-icon {
font-size: 24px;
color: var(--color-primary);
}
.plugin-card-info {
flex: 1;
}
.plugin-card-title {
font-size: 14px;
font-weight: 600;
color: var(--color-text-primary);
margin-bottom: 2px;
}
.plugin-card-version {
font-size: 11px;
color: var(--color-text-secondary);
}
.plugin-toggle {
background: none;
border: none;
padding: 4px;
cursor: pointer;
color: var(--color-text-secondary);
transition: all 0.2s;
border-radius: 4px;
}
.plugin-toggle:hover {
background: var(--color-bg-hover);
}
.plugin-toggle.enabled {
color: var(--color-success);
}
.plugin-toggle.disabled {
color: var(--color-text-secondary);
}
.plugin-card-description {
font-size: 12px;
color: var(--color-text-secondary);
line-height: 1.5;
margin-bottom: 10px;
}
.plugin-card-footer {
display: flex;
justify-content: space-between;
align-items: center;
padding-top: 10px;
border-top: 1px solid var(--color-border);
font-size: 11px;
}
.plugin-card-category {
color: var(--color-text-secondary);
}
.plugin-card-installed {
color: var(--color-text-tertiary);
}
/* Plugin List (List View) */
.plugin-list-item {
display: flex;
align-items: center;
gap: 12px;
padding: 10px 12px;
background: var(--color-bg-primary);
border: 1px solid var(--color-border);
border-radius: 6px;
transition: all 0.2s;
}
.plugin-list-item:hover {
border-color: var(--color-primary);
}
.plugin-list-item.disabled {
opacity: 0.6;
}
.plugin-list-icon {
font-size: 20px;
color: var(--color-primary);
}
.plugin-list-info {
flex: 1;
}
.plugin-list-name {
display: flex;
align-items: baseline;
gap: 8px;
font-size: 13px;
font-weight: 600;
color: var(--color-text-primary);
margin-bottom: 2px;
}
.plugin-list-version {
font-size: 11px;
font-weight: normal;
color: var(--color-text-secondary);
}
.plugin-list-description {
font-size: 12px;
color: var(--color-text-secondary);
line-height: 1.4;
}
.plugin-list-status {
margin-right: 8px;
}
.status-badge {
padding: 3px 8px;
border-radius: 4px;
font-size: 11px;
font-weight: 500;
}
.status-badge.enabled {
background: rgba(34, 197, 94, 0.15);
color: var(--color-success);
}
.status-badge.disabled {
background: var(--color-bg-tertiary);
color: var(--color-text-secondary);
}
.plugin-list-toggle {
padding: 6px 12px;
background: var(--color-bg-tertiary);
border: 1px solid var(--color-border);
border-radius: 4px;
color: var(--color-text-primary);
font-size: 12px;
cursor: pointer;
transition: all 0.2s;
}
.plugin-list-toggle:hover {
background: var(--color-primary);
border-color: var(--color-primary);
color: white;
}
/* Scrollbar */
.plugin-content::-webkit-scrollbar {
width: 8px;
}
.plugin-content::-webkit-scrollbar-track {
background: var(--color-bg-secondary);
}
.plugin-content::-webkit-scrollbar-thumb {
background: var(--color-border);
border-radius: 4px;
}
.plugin-content::-webkit-scrollbar-thumb:hover {
background: var(--color-text-secondary);
}
@@ -1,861 +0,0 @@
/* 统一滚动条样式 */
.plugin-publish-wizard ::-webkit-scrollbar {
width: 8px;
height: 8px;
}
.plugin-publish-wizard ::-webkit-scrollbar-track {
background: var(--color-bg-primary, #1e1e1e);
border-radius: 4px;
}
.plugin-publish-wizard ::-webkit-scrollbar-thumb {
background: var(--color-border, #333);
border-radius: 4px;
transition: background 0.2s;
}
.plugin-publish-wizard ::-webkit-scrollbar-thumb:hover {
background: var(--color-text-secondary, #858585);
}
.plugin-publish-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.5);
display: flex;
align-items: center;
justify-content: center;
z-index: var(--z-index-modal);
}
.plugin-publish-wizard {
background: var(--color-bg-primary, #1e1e1e);
border-radius: 12px;
width: 90%;
max-width: 600px;
max-height: 80vh;
display: flex;
flex-direction: column;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.4);
}
.plugin-publish-wizard.inline {
width: 100%;
max-width: none;
max-height: 100%;
height: 100%;
border-radius: 0;
box-shadow: none;
background: transparent;
overflow: hidden;
}
.plugin-publish-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 20px 24px;
border-bottom: 1px solid var(--color-border, #333);
}
.plugin-publish-wizard.inline .plugin-publish-header {
padding: 16px 20px;
background: var(--color-bg-secondary, #252526);
}
.plugin-publish-header h2 {
margin: 0;
font-size: 18px;
font-weight: 600;
color: var(--color-text-primary, #cccccc);
}
.plugin-publish-close {
background: none;
border: none;
cursor: pointer;
color: var(--color-text-secondary, #858585);
padding: 4px;
display: flex;
align-items: center;
justify-content: center;
border-radius: 4px;
transition: all 0.2s;
}
.plugin-publish-close:hover {
background: var(--color-bg-hover, #2d2d30);
color: var(--color-text-primary, #cccccc);
}
.plugin-publish-content {
flex: 1;
overflow-y: auto;
padding: 24px;
}
.plugin-publish-wizard.inline .plugin-publish-content {
padding: 20px;
background: var(--color-bg-primary, #1e1e1e);
height: 0;
min-height: 0;
}
.publish-step {
display: flex;
flex-direction: column;
gap: 20px;
}
.publish-step h3 {
margin: 0 0 12px 0;
font-size: 16px;
font-weight: 600;
color: var(--color-text-primary, #cccccc);
}
.github-auth {
display: flex;
flex-direction: column;
align-items: center;
gap: 16px;
padding: 20px;
}
.form-group {
display: flex;
flex-direction: column;
gap: 8px;
width: 100%;
}
.form-group label {
font-size: 13px;
font-weight: 500;
color: var(--color-text-primary, #cccccc);
}
.form-group input,
.form-group textarea,
.form-group select {
padding: 8px 12px;
background: var(--color-bg-secondary, #252526);
border: 1px solid var(--color-border, #333);
border-radius: 4px;
color: var(--color-text-primary, #cccccc);
font-size: 13px;
outline: none;
transition: border-color 0.2s;
}
.form-group input:focus,
.form-group textarea:focus,
.form-group select:focus {
border-color: var(--color-accent, #0e639c);
}
.form-group small {
font-size: 12px;
color: var(--color-text-secondary, #858585);
}
.form-group textarea {
resize: vertical;
min-height: 80px;
font-family: inherit;
}
.btn-primary,
.btn-secondary,
.btn-link {
padding: 8px 16px;
border-radius: 4px;
font-size: 13px;
font-weight: 500;
cursor: pointer;
transition: all 0.2s;
display: inline-flex;
align-items: center;
justify-content: center;
gap: 6px;
}
.btn-primary {
background: var(--color-accent, #0e639c);
color: white;
border: none;
}
.btn-primary:hover {
background: var(--color-accent-hover, #1177bb);
}
.btn-secondary {
background: var(--color-bg-secondary, #252526);
color: var(--color-text-primary, #cccccc);
border: 1px solid var(--color-border, #333);
}
.btn-secondary:hover {
background: var(--color-bg-hover, #2d2d30);
}
.btn-link {
background: none;
color: var(--color-accent, #0e639c);
border: none;
padding: 6px 12px;
}
.btn-link:hover {
opacity: 0.8;
}
.button-group {
display: flex;
gap: 12px;
justify-content: flex-end;
}
.error-message {
display: flex;
align-items: center;
gap: 8px;
padding: 12px;
background: rgba(255, 59, 48, 0.1);
border: 1px solid rgba(255, 59, 48, 0.3);
border-radius: 4px;
color: #ff3b30;
font-size: 13px;
}
.confirm-details {
background: var(--color-bg-secondary, #252526);
border-radius: 8px;
padding: 16px;
display: flex;
flex-direction: column;
gap: 12px;
}
.detail-row {
display: flex;
gap: 12px;
}
.detail-label {
font-weight: 600;
color: var(--color-text-secondary, #858585);
min-width: 120px;
}
.detail-value {
color: var(--color-text-primary, #cccccc);
word-break: break-all;
}
.publish-step.publishing,
.publish-step.success,
.publish-step.error {
align-items: center;
text-align: center;
padding: 40px 20px;
}
.publish-step.publishing svg,
.publish-step.success svg,
.publish-step.error svg {
margin-bottom: 16px;
}
.review-message {
color: var(--color-text-secondary, #858585);
font-size: 13px;
line-height: 1.5;
max-width: 400px;
margin: 16px 0;
}
.spinning {
animation: spin 1s linear infinite;
}
@keyframes spin {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
/* OAuth Authentication Styles */
.auth-tabs {
display: flex;
gap: 8px;
margin: 16px 0;
width: 100%;
}
.auth-tab {
flex: 1;
padding: 10px 16px;
background: var(--color-bg-secondary, #252526);
border: 1px solid var(--color-border, #333);
border-radius: 6px;
color: var(--color-text-secondary, #858585);
cursor: pointer;
font-size: 13px;
font-weight: 500;
transition: all 0.2s;
}
.auth-tab:hover {
background: var(--color-bg-hover, #2d2d30);
border-color: var(--color-accent, #0e639c);
}
.auth-tab.active {
background: var(--color-accent, #0e639c);
border-color: var(--color-accent, #0e639c);
color: white;
}
.oauth-auth,
.token-auth {
display: flex;
flex-direction: column;
gap: 16px;
width: 100%;
align-items: center;
}
.oauth-instructions {
text-align: left;
width: 100%;
padding: 16px;
background: var(--color-bg-secondary, #252526);
border-radius: 8px;
border: 1px solid var(--color-border, #333);
}
.oauth-instructions p {
margin: 8px 0;
font-size: 13px;
color: var(--color-text-secondary, #858585);
line-height: 1.6;
}
.oauth-pending,
.oauth-success,
.oauth-error {
display: flex;
flex-direction: column;
align-items: center;
gap: 16px;
padding: 32px 16px;
width: 100%;
}
.oauth-pending h4,
.oauth-success h4,
.oauth-error h4 {
margin: 0;
font-size: 16px;
font-weight: 600;
}
.user-code-display {
display: flex;
flex-direction: column;
gap: 12px;
width: 100%;
max-width: 400px;
margin-top: 16px;
}
.user-code-display label {
font-size: 13px;
font-weight: 500;
color: var(--color-text-secondary, #858585);
}
.code-box {
display: flex;
align-items: center;
gap: 12px;
padding: 16px;
background: var(--color-bg-secondary, #252526);
border: 2px solid var(--color-accent, #0e639c);
border-radius: 8px;
}
.code-text {
flex: 1;
font-size: 24px;
font-weight: 700;
font-family: 'Courier New', monospace;
letter-spacing: 4px;
color: var(--color-accent, #0e639c);
text-align: center;
}
.btn-copy {
background: transparent;
border: none;
font-size: 20px;
cursor: pointer;
padding: 4px 8px;
border-radius: 4px;
transition: background 0.2s;
}
.btn-copy:hover {
background: var(--color-bg-hover, #2d2d30);
}
.error-details {
width: 100%;
max-width: 500px;
max-height: 200px;
overflow-y: auto;
background: var(--color-bg-secondary, #252526);
border: 1px solid #ff3b30;
border-radius: 8px;
padding: 12px;
margin: 12px 0;
}
.error-details pre {
margin: 0;
font-size: 12px;
font-family: 'Courier New', monospace;
color: #ff3b30;
white-space: pre-wrap;
word-break: break-word;
}
/* 发布进度样式 */
.publish-progress {
display: flex;
flex-direction: column;
gap: 12px;
margin-top: 20px;
padding: 16px;
background: var(--color-bg-secondary, #252526);
border-radius: 8px;
}
.progress-bar {
width: 100%;
height: 8px;
background: var(--color-bg-hover, #2d2d30);
border-radius: 4px;
overflow: hidden;
}
.progress-fill {
height: 100%;
background: linear-gradient(90deg, #007acc 0%, #4fc3f7 100%);
transition: width 0.3s ease;
}
.progress-message {
margin: 0;
font-size: 14px;
color: var(--color-text-primary, #cccccc);
text-align: center;
}
.progress-percent {
margin: 0;
font-size: 12px;
color: var(--color-text-secondary, #858585);
text-align: center;
font-family: monospace;
}
.build-log {
width: 100%;
max-height: 300px;
overflow-y: auto;
background: var(--color-bg-hover, #2d2d30);
border: 1px solid var(--color-border, #333);
border-radius: 4px;
padding: 12px;
margin-top: 16px;
font-family: 'Consolas', 'Monaco', 'Courier New', monospace;
font-size: 12px;
line-height: 1.5;
}
.log-line {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 4px;
color: var(--color-text-primary, #cccccc);
word-break: break-all;
}
.log-line svg {
flex-shrink: 0;
}
/* 现有 PR 提示框 */
.existing-pr-notice {
display: flex;
gap: 12px;
padding: 16px;
margin: 16px 0;
background: rgba(244, 180, 0, 0.1);
border: 1px solid rgba(244, 180, 0, 0.3);
border-radius: 8px;
color: var(--color-text-primary, #cccccc);
}
.existing-pr-notice svg {
color: #f4b400;
flex-shrink: 0;
margin-top: 2px;
}
.existing-pr-notice .notice-content {
flex: 1;
}
.existing-pr-notice strong {
display: block;
margin-bottom: 8px;
color: #f4b400;
font-size: 14px;
}
.existing-pr-notice p {
margin: 0 0 12px 0;
font-size: 13px;
line-height: 1.5;
color: var(--color-text-secondary, #858585);
}
.existing-pr-notice .btn-link {
display: inline-flex;
align-items: center;
gap: 6px;
padding: 6px 12px;
background: rgba(74, 158, 255, 0.15);
border: 1px solid rgba(74, 158, 255, 0.3);
border-radius: 6px;
color: #4a9eff;
font-size: 13px;
cursor: pointer;
transition: all 0.2s;
}
.existing-pr-notice .btn-link:hover {
background: rgba(74, 158, 255, 0.25);
border-color: #4a9eff;
}
/* 版本信息样式 */
.version-info {
display: flex;
flex-direction: column;
gap: 8px;
margin-bottom: 8px;
padding: 12px;
background: rgba(52, 199, 89, 0.1);
border: 1px solid rgba(52, 199, 89, 0.3);
border-radius: 6px;
}
.version-notice {
display: flex;
align-items: center;
gap: 8px;
color: #34c759;
font-size: 13px;
font-weight: 500;
}
.btn-version-suggest {
align-self: flex-start;
padding: 6px 12px;
background: rgba(14, 99, 156, 0.15);
border: 1px solid rgba(14, 99, 156, 0.3);
border-radius: 4px;
color: var(--color-accent, #0e639c);
font-size: 12px;
font-weight: 500;
cursor: pointer;
transition: all 0.2s;
}
.btn-version-suggest:hover {
background: rgba(14, 99, 156, 0.25);
border-color: var(--color-accent, #0e639c);
transform: translateY(-1px);
}
.version-history {
margin-top: 8px;
padding: 12px;
background: var(--color-bg-secondary, #252526);
border: 1px solid var(--color-border, #333);
border-radius: 6px;
}
.version-history summary {
cursor: pointer;
font-size: 13px;
font-weight: 500;
color: var(--color-text-primary, #cccccc);
padding: 4px;
user-select: none;
}
.version-history summary:hover {
color: var(--color-accent, #0e639c);
}
.version-history ul {
list-style: none;
padding: 0;
margin: 12px 0 0 0;
display: flex;
flex-direction: column;
gap: 4px;
}
.version-history li {
padding: 6px 12px;
background: var(--color-bg-primary, #1e1e1e);
border-radius: 4px;
font-size: 12px;
font-family: 'Consolas', 'Monaco', monospace;
color: var(--color-text-secondary, #858585);
}
/* 插件源选择样式 */
.source-type-selection {
display: flex;
flex-direction: column;
gap: 12px;
margin: 20px 0;
}
.source-type-btn {
display: flex;
align-items: flex-start;
gap: 16px;
padding: 20px;
background: var(--color-bg-secondary, #252526);
border: 2px solid var(--color-border, #333);
border-radius: 8px;
cursor: pointer;
transition: all 0.2s;
text-align: left;
width: 100%;
}
.source-type-btn:hover {
background: var(--color-bg-hover, #2d2d30);
border-color: var(--color-accent, #0e639c);
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2);
}
.source-type-btn.active {
background: rgba(14, 99, 156, 0.15);
border-color: var(--color-accent, #0e639c);
box-shadow: 0 0 0 3px rgba(14, 99, 156, 0.1);
}
.source-type-btn svg {
color: var(--color-accent, #0e639c);
flex-shrink: 0;
margin-top: 2px;
}
.source-type-info {
flex: 1;
display: flex;
flex-direction: column;
gap: 6px;
}
.source-type-info strong {
font-size: 14px;
font-weight: 600;
color: var(--color-text-primary, #cccccc);
}
.source-type-info p {
margin: 0;
font-size: 12px;
line-height: 1.5;
color: var(--color-text-secondary, #858585);
}
.selected-source {
display: flex;
align-items: flex-start;
gap: 12px;
padding: 16px;
background: rgba(52, 199, 89, 0.1);
border: 1px solid rgba(52, 199, 89, 0.3);
border-radius: 8px;
margin-top: 16px;
}
.selected-source svg {
color: #34c759;
flex-shrink: 0;
margin-top: 2px;
}
.source-details {
flex: 1;
display: flex;
flex-direction: column;
gap: 4px;
}
.source-path {
font-size: 12px;
color: var(--color-text-secondary, #858585);
word-break: break-all;
font-family: 'Consolas', 'Monaco', monospace;
}
.source-name {
font-size: 14px;
font-weight: 600;
color: #34c759;
}
/* ZIP 文件要求说明 */
.zip-requirements-details {
margin-top: 20px;
padding: 16px;
background: var(--color-bg-secondary, #252526);
border: 1px solid var(--color-border, #333);
border-radius: 8px;
}
.zip-requirements-details summary {
display: flex;
align-items: center;
gap: 8px;
cursor: pointer;
font-size: 14px;
font-weight: 600;
color: var(--color-text-primary, #cccccc);
padding: 8px;
user-select: none;
list-style: none;
transition: color 0.2s;
}
.zip-requirements-details summary::-webkit-details-marker {
display: none;
}
.zip-requirements-details summary:hover {
color: var(--color-accent, #0e639c);
}
.zip-requirements-details summary svg {
color: #f4b400;
flex-shrink: 0;
}
.zip-requirements-details[open] summary {
margin-bottom: 16px;
padding-bottom: 12px;
border-bottom: 1px solid var(--color-border, #333);
}
.zip-requirements-content {
display: flex;
flex-direction: column;
gap: 20px;
padding: 4px 8px;
}
.requirement-section h4 {
margin: 0 0 8px 0;
font-size: 13px;
font-weight: 600;
color: var(--color-accent, #0e639c);
}
.requirement-section p {
margin: 0 0 12px 0;
font-size: 13px;
line-height: 1.5;
color: var(--color-text-secondary, #858585);
}
.requirement-section ul {
list-style: none;
padding: 0;
margin: 0;
display: flex;
flex-direction: column;
gap: 8px;
}
.requirement-section li {
display: flex;
align-items: flex-start;
gap: 8px;
font-size: 13px;
line-height: 1.5;
color: var(--color-text-primary, #cccccc);
}
.requirement-section li::before {
content: '✓';
color: #34c759;
font-weight: bold;
flex-shrink: 0;
}
.requirement-section code {
padding: 2px 6px;
background: var(--color-bg-primary, #1e1e1e);
border: 1px solid var(--color-border, #333);
border-radius: 3px;
font-family: 'Consolas', 'Monaco', 'Courier New', monospace;
font-size: 12px;
color: #4ec9b0;
}
.build-script-example {
margin: 0;
padding: 12px;
background: var(--color-bg-primary, #1e1e1e);
border: 1px solid var(--color-border, #333);
border-radius: 4px;
font-family: 'Consolas', 'Monaco', 'Courier New', monospace;
font-size: 12px;
line-height: 1.6;
color: var(--color-text-secondary, #858585);
overflow-x: auto;
}
.recommendation-notice {
padding: 12px 16px;
background: rgba(14, 99, 156, 0.1);
border: 1px solid rgba(14, 99, 156, 0.3);
border-radius: 6px;
font-size: 13px;
line-height: 1.5;
color: var(--color-accent, #0e639c);
text-align: center;
}
@@ -1,287 +0,0 @@
.plugin-update-dialog-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.5);
display: flex;
align-items: center;
justify-content: center;
z-index: var(--z-index-modal);
}
.plugin-update-dialog {
background: var(--color-bg-primary, #1e1e1e);
border: 1px solid var(--color-border, #333);
border-radius: 8px;
width: 90%;
max-width: 600px;
max-height: 80vh;
display: flex;
flex-direction: column;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3);
}
.update-dialog-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 16px 20px;
border-bottom: 1px solid var(--color-border, #333);
}
.update-dialog-header h2 {
margin: 0;
font-size: 18px;
font-weight: 600;
}
.update-dialog-close {
background: none;
border: none;
padding: 4px;
cursor: pointer;
color: var(--color-text-secondary, #888);
display: flex;
align-items: center;
justify-content: center;
border-radius: 4px;
}
.update-dialog-close:hover {
background: var(--color-bg-hover, rgba(255, 255, 255, 0.1));
color: var(--color-text-primary, #fff);
}
.update-dialog-content {
flex: 1;
overflow-y: auto;
padding: 20px;
}
.update-dialog-step {
display: flex;
flex-direction: column;
gap: 16px;
}
.update-dialog-step h3 {
margin: 0;
font-size: 16px;
font-weight: 600;
}
.step-description {
color: var(--color-text-secondary, #888);
margin: 0;
}
.current-plugin-info {
padding: 12px;
background: rgba(14, 99, 156, 0.1);
border: 1px solid rgba(14, 99, 156, 0.3);
border-radius: 6px;
}
.current-plugin-info h4 {
margin: 0 0 8px 0;
font-size: 14px;
}
.current-plugin-info p {
margin: 0;
color: var(--color-text-secondary, #888);
}
.selected-folder-info {
display: flex;
align-items: center;
gap: 8px;
padding: 8px 12px;
background: var(--color-bg-secondary, #252525);
border: 1px solid var(--color-border, #333);
border-radius: 4px;
font-size: 13px;
color: var(--color-text-secondary, #888);
}
.form-group {
display: flex;
flex-direction: column;
gap: 8px;
}
.form-group label {
font-size: 13px;
font-weight: 500;
}
.form-group input,
.form-group textarea {
padding: 8px 12px;
background: var(--color-bg-secondary, #252525);
border: 1px solid var(--color-border, #333);
border-radius: 4px;
color: var(--color-text-primary, #fff);
font-size: 13px;
font-family: inherit;
}
.form-group textarea {
resize: vertical;
}
.version-input-group {
display: flex;
gap: 8px;
}
.version-input-group input {
flex: 1;
}
.btn-browse,
.btn-suggest,
.btn-view-pr,
.btn-close,
.btn-back,
.btn-primary {
padding: 8px 16px;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 13px;
font-weight: 500;
display: inline-flex;
align-items: center;
gap: 6px;
transition: all 0.2s;
}
.btn-browse {
background: var(--color-accent, #0e639c);
color: white;
}
.btn-browse:hover {
background: var(--color-accent-hover, #0d5a8c);
}
.btn-suggest {
background: rgba(14, 99, 156, 0.15);
color: var(--color-accent, #0e639c);
border: 1px solid rgba(14, 99, 156, 0.3);
white-space: nowrap;
}
.btn-suggest:hover {
background: rgba(14, 99, 156, 0.25);
}
.update-dialog-actions {
display: flex;
gap: 8px;
justify-content: flex-end;
margin-top: 8px;
}
.btn-back {
background: var(--color-bg-secondary, #252525);
color: var(--color-text-primary, #fff);
border: 1px solid var(--color-border, #333);
}
.btn-back:hover {
background: var(--color-bg-hover, rgba(255, 255, 255, 0.1));
}
.btn-primary {
background: var(--color-accent, #0e639c);
color: white;
}
.btn-primary:hover:not(:disabled) {
background: var(--color-accent-hover, #0d5a8c);
}
.btn-primary:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.progress-container {
display: flex;
flex-direction: column;
gap: 8px;
}
.progress-bar {
width: 100%;
height: 8px;
background: var(--color-bg-secondary, #252525);
border-radius: 4px;
overflow: hidden;
}
.progress-fill {
height: 100%;
background: var(--color-accent, #0e639c);
transition: width 0.3s ease;
}
.progress-message {
margin: 0;
font-size: 13px;
color: var(--color-text-secondary, #888);
}
.build-log {
margin-top: 12px;
padding: 12px;
background: var(--color-bg-secondary, #252525);
border: 1px solid var(--color-border, #333);
border-radius: 4px;
max-height: 300px;
overflow-y: auto;
font-family: 'Courier New', monospace;
font-size: 12px;
}
.log-line {
margin-bottom: 4px;
color: var(--color-text-secondary, #888);
}
.success-step,
.error-step {
align-items: center;
text-align: center;
padding: 20px;
}
.success-icon {
color: var(--color-success, #52c41a);
}
.error-icon {
color: var(--color-error, #ff4d4f);
}
.success-message,
.error-message {
margin: 16px 0;
color: var(--color-text-secondary, #888);
}
.btn-view-pr,
.btn-close {
background: var(--color-accent, #0e639c);
color: white;
margin-top: 8px;
}
.btn-view-pr:hover,
.btn-close:hover {
background: var(--color-accent-hover, #0d5a8c);
}
@@ -1,278 +0,0 @@
.profiler-dock-panel {
display: flex;
flex-direction: column;
height: 100%;
background: var(--color-bg-elevated);
overflow: hidden;
}
.profiler-dock-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 8px;
border-bottom: 1px solid var(--color-border-default);
flex-shrink: 0;
height: 26px;
}
.profiler-dock-header h3 {
margin: 0;
font-size: 13px;
font-weight: 600;
color: var(--color-text-primary);
}
.profiler-dock-header-actions {
display: flex;
align-items: center;
gap: 8px;
}
.profiler-dock-pause-btn,
.profiler-dock-details-btn {
display: flex;
align-items: center;
justify-content: center;
width: 24px;
height: 24px;
padding: 0;
background: var(--color-bg-inset);
border: 1px solid var(--color-border-default);
border-radius: var(--radius-sm);
color: var(--color-text-secondary);
cursor: pointer;
transition: all var(--transition-fast);
}
.profiler-dock-pause-btn:hover,
.profiler-dock-details-btn:hover {
background: var(--color-bg-hover);
border-color: var(--color-border-strong);
color: var(--color-text-primary);
}
.profiler-dock-pause-btn:active,
.profiler-dock-details-btn:active {
transform: scale(0.95);
}
.profiler-dock-status {
display: flex;
align-items: center;
gap: 6px;
font-size: 11px;
padding: 4px 8px;
border-radius: var(--radius-sm);
background: var(--color-bg-inset);
}
.status-text {
font-weight: 500;
}
.status-text.connected {
color: var(--color-success);
}
.status-text.waiting {
color: var(--color-warning);
}
.status-text.disconnected {
color: var(--color-text-tertiary);
}
.profiler-dock-empty {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
flex: 1;
padding: 24px;
text-align: center;
color: var(--color-text-tertiary);
gap: 12px;
}
.profiler-dock-empty p {
margin: 0;
font-size: 13px;
}
.profiler-dock-empty .hint {
font-size: 11px;
opacity: 0.7;
}
.profiler-dock-content {
flex: 1;
overflow-y: auto;
padding: 16px;
display: flex;
flex-direction: column;
gap: 16px;
}
.profiler-dock-content::-webkit-scrollbar {
width: 6px;
}
.profiler-dock-content::-webkit-scrollbar-track {
background: transparent;
}
.profiler-dock-content::-webkit-scrollbar-thumb {
background: var(--color-border-default);
border-radius: 3px;
}
.profiler-dock-stats {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 12px;
}
.stat-card {
display: flex;
align-items: center;
gap: 12px;
padding: 12px;
background: var(--color-bg-inset);
border-radius: var(--radius-md);
border: 1px solid var(--color-border-default);
transition: all var(--transition-fast);
}
.stat-card:hover {
border-color: var(--color-border-strong);
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
.stat-icon {
display: flex;
align-items: center;
justify-content: center;
width: 32px;
height: 32px;
border-radius: var(--radius-sm);
background: rgba(99, 102, 241, 0.1);
color: rgb(99, 102, 241);
flex-shrink: 0;
}
.stat-info {
flex: 1;
min-width: 0;
}
.stat-label {
font-size: 10px;
color: var(--color-text-tertiary);
text-transform: uppercase;
letter-spacing: 0.5px;
font-weight: 500;
margin-bottom: 2px;
}
.stat-value {
font-size: 18px;
font-weight: 700;
color: var(--color-text-primary);
font-family: var(--font-family-mono);
}
.stat-value.warning {
color: var(--color-warning);
}
.profiler-dock-systems {
display: flex;
flex-direction: column;
gap: 12px;
}
.profiler-dock-systems h4 {
margin: 0;
font-size: 12px;
font-weight: 600;
color: var(--color-text-secondary);
text-transform: uppercase;
letter-spacing: 0.5px;
}
.systems-list {
display: flex;
flex-direction: column;
gap: 10px;
}
.system-item {
padding: 10px 12px;
background: var(--color-bg-inset);
border-radius: var(--radius-md);
border: 1px solid var(--color-border-default);
transition: all var(--transition-fast);
}
.system-item:hover {
border-color: var(--color-border-strong);
}
.system-item-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 6px;
}
.system-item-name {
font-size: 11px;
font-weight: 500;
color: var(--color-text-primary);
font-family: var(--font-family-mono);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.system-item-time {
font-size: 11px;
font-weight: 600;
color: var(--color-text-primary);
font-family: var(--font-family-mono);
flex-shrink: 0;
margin-left: 8px;
}
.system-item-bar {
width: 100%;
height: 4px;
background: var(--color-bg-elevated);
border-radius: 2px;
overflow: hidden;
margin-bottom: 6px;
}
.system-item-bar-fill {
height: 100%;
transition: width 0.3s ease;
border-radius: 2px;
}
.system-item-footer {
display: flex;
align-items: center;
justify-content: space-between;
font-size: 10px;
color: var(--color-text-tertiary);
}
.system-item-percentage {
font-weight: 600;
font-family: var(--font-family-mono);
}
.system-item-entities {
font-size: 9px;
}
@@ -1,304 +0,0 @@
.profiler-panel {
display: flex;
flex-direction: column;
height: 100%;
background: var(--color-bg-base);
}
.profiler-toolbar {
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 8px;
background: var(--color-bg-elevated);
border-bottom: 1px solid var(--color-border-default);
flex-shrink: 0;
gap: 8px;
height: 26px;
}
.profiler-toolbar-left {
display: flex;
align-items: center;
gap: 12px;
flex: 1;
}
.profiler-toolbar-right {
display: flex;
align-items: center;
gap: 8px;
}
.profiler-stats-summary {
display: flex;
align-items: center;
gap: 20px;
}
.summary-item {
display: flex;
align-items: center;
gap: 6px;
font-size: 12px;
color: var(--color-text-secondary);
}
.summary-item svg {
color: var(--color-primary);
}
.summary-label {
font-weight: 500;
}
.summary-value {
font-family: var(--font-family-mono);
font-weight: 600;
color: var(--color-text-primary);
}
.summary-value.over-budget {
color: var(--color-danger);
}
.summary-value.low-fps {
color: var(--color-warning);
}
.profiler-sort {
padding: 4px 8px;
background: var(--color-bg-inset);
border: 1px solid var(--color-border-default);
border-radius: var(--radius-sm);
color: var(--color-text-primary);
font-size: 11px;
cursor: pointer;
outline: none;
transition: all var(--transition-fast);
}
.profiler-sort:hover {
border-color: var(--color-primary);
}
.profiler-sort:focus {
border-color: var(--color-primary);
}
.profiler-btn {
display: inline-flex;
align-items: center;
justify-content: center;
padding: 6px;
background: transparent;
border: none;
border-radius: var(--radius-sm);
color: var(--color-text-secondary);
cursor: pointer;
transition: all var(--transition-fast);
}
.profiler-btn:hover {
background: var(--color-bg-hover);
color: var(--color-text-primary);
}
.profiler-content {
flex: 1;
overflow-y: auto;
overflow-x: hidden;
padding: 12px;
}
.profiler-content::-webkit-scrollbar {
width: 8px;
}
.profiler-content::-webkit-scrollbar-track {
background: var(--color-bg-elevated);
}
.profiler-content::-webkit-scrollbar-thumb {
background: var(--color-border-default);
border-radius: 4px;
}
.profiler-content::-webkit-scrollbar-thumb:hover {
background: var(--color-text-secondary);
}
.profiler-empty {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 100%;
color: var(--color-text-tertiary);
gap: 12px;
}
.profiler-empty p {
margin: 0;
font-size: 13px;
}
.profiler-empty-hint {
font-size: 11px !important;
opacity: 0.7;
}
.profiler-systems {
display: flex;
flex-direction: column;
gap: 12px;
}
.system-row {
background: var(--color-bg-elevated);
border: 1px solid var(--color-border-default);
border-radius: var(--radius-md);
padding: 12px;
transition: all var(--transition-fast);
}
.system-row:hover {
border-color: var(--color-border-strong);
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
.system-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 8px;
}
.system-info {
display: flex;
align-items: center;
gap: 8px;
flex: 1;
}
.system-rank {
display: inline-flex;
align-items: center;
justify-content: center;
width: 28px;
height: 22px;
background: var(--color-bg-inset);
border-radius: var(--radius-sm);
font-size: 11px;
font-weight: 600;
font-family: var(--font-family-mono);
color: var(--color-text-secondary);
}
.system-name {
font-size: 13px;
font-weight: 600;
color: var(--color-text-primary);
font-family: var(--font-family-mono);
}
.system-entities {
font-size: 11px;
color: var(--color-text-tertiary);
}
.system-metrics {
display: flex;
align-items: center;
gap: 12px;
}
.metric-time {
font-size: 13px;
font-weight: 600;
font-family: var(--font-family-mono);
color: var(--color-text-primary);
}
.metric-percentage {
font-size: 12px;
font-family: var(--font-family-mono);
color: var(--color-text-secondary);
background: var(--color-bg-inset);
padding: 2px 6px;
border-radius: var(--radius-sm);
}
.system-bar {
width: 100%;
height: 6px;
background: var(--color-bg-inset);
border-radius: 3px;
overflow: hidden;
margin-bottom: 8px;
}
.system-bar-fill {
height: 100%;
transition: width 0.3s ease;
border-radius: 3px;
}
.system-stats {
display: flex;
align-items: center;
gap: 16px;
}
.stat-item {
display: flex;
align-items: center;
gap: 4px;
font-size: 11px;
}
.stat-label {
color: var(--color-text-tertiary);
}
.stat-value {
font-family: var(--font-family-mono);
font-weight: 500;
color: var(--color-text-secondary);
}
.profiler-footer {
padding: 10px 12px;
background: var(--color-bg-elevated);
border-top: 1px solid var(--color-border-default);
flex-shrink: 0;
}
.profiler-legend {
display: flex;
align-items: center;
gap: 16px;
justify-content: center;
}
.legend-item {
display: flex;
align-items: center;
gap: 6px;
font-size: 11px;
color: var(--color-text-secondary);
}
.legend-color {
width: 12px;
height: 12px;
border-radius: 2px;
}
@media (prefers-reduced-motion: reduce) {
.system-row,
.system-bar-fill,
.profiler-btn {
transition: none;
}
}
File diff suppressed because it is too large Load Diff
@@ -1,174 +0,0 @@
.user-profile {
position: relative;
display: flex;
align-items: center;
}
.login-button {
display: flex;
align-items: center;
gap: 4px;
padding: 3px 8px;
background: transparent;
border: none;
border-radius: 2px;
color: #888;
font-size: 10px;
font-weight: 500;
cursor: pointer;
transition: all 0.1s;
}
.login-button:hover:not(:disabled) {
background: rgba(255, 255, 255, 0.08);
color: #ccc;
}
.login-button:disabled {
opacity: 0.6;
cursor: not-allowed;
}
.login-button .spinning {
animation: spin 1s linear infinite;
}
@keyframes spin {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
.user-avatar-button {
display: flex;
align-items: center;
gap: 4px;
padding: 2px 6px 2px 2px;
background: transparent;
border: none;
border-radius: 2px;
color: #888;
font-size: 10px;
font-weight: 500;
cursor: pointer;
transition: all 0.1s;
}
.user-avatar-button:hover {
background: rgba(255, 255, 255, 0.08);
color: #ccc;
}
.user-avatar,
.user-avatar-placeholder {
width: 16px;
height: 16px;
border-radius: 50%;
}
.user-avatar {
object-fit: cover;
border: none;
}
.user-avatar-placeholder {
display: flex;
align-items: center;
justify-content: center;
background: rgba(255, 255, 255, 0.1);
color: #888;
border: none;
}
.user-name {
max-width: 80px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.user-menu {
position: absolute;
top: calc(100% + 8px);
right: 0;
min-width: 220px;
background: var(--color-bg-secondary, #252526);
border: 1px solid var(--color-border, #333);
border-radius: 8px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
z-index: var(--z-index-dropdown);
overflow: hidden;
}
.user-menu-header {
display: flex;
align-items: center;
gap: 12px;
padding: 16px;
background: var(--color-bg-tertiary, #333);
}
.user-menu-avatar {
width: 48px;
height: 48px;
border-radius: 50%;
object-fit: cover;
border: 2px solid var(--color-accent, #0e639c);
}
.user-menu-info {
flex: 1;
min-width: 0;
}
.user-menu-name {
font-size: 14px;
font-weight: 600;
color: var(--color-text-primary, #cccccc);
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.user-menu-login {
font-size: 12px;
color: var(--color-text-secondary, #858585);
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.user-menu-divider {
height: 1px;
background: var(--color-border, #333);
}
.user-menu-item {
display: flex;
align-items: center;
gap: 8px;
width: 100%;
padding: 12px 16px;
background: transparent;
border: none;
color: var(--color-text-primary, #cccccc);
font-size: 13px;
cursor: pointer;
text-align: left;
transition: background 0.2s;
}
.user-menu-item:hover {
background: var(--color-bg-hover, #2d2d30);
}
.user-menu-item:last-child {
color: #ff3b30;
}
.user-menu-item:last-child:hover {
background: rgba(255, 59, 48, 0.1);
}
+1
View File
@@ -43,6 +43,7 @@
"@esengine/asset-system": "workspace:*",
"@esengine/asset-system-editor": "workspace:*",
"@esengine/engine-core": "workspace:*",
"@esengine/runtime-core": "workspace:*",
"@tauri-apps/api": "^2.2.0",
"@babel/core": "^7.28.3",
"@babel/plugin-transform-nullish-coalescing-operator": "^7.27.1",
@@ -8,9 +8,11 @@
* 使
*/
import type { Component, ComponentType, Entity } from '@esengine/ecs-framework';
import { createLogger, type Component, type ComponentType, type Entity } from '@esengine/ecs-framework';
import type { IGizmoProvider, IGizmoRenderData } from './IGizmoProvider';
const logger = createLogger('GizmoRegistry');
/**
* Gizmo provider function type
* Gizmo
@@ -124,9 +126,7 @@ export class GizmoRegistry {
try {
return provider(component, entity, isSelected);
} catch (e) {
// Silently ignore errors from gizmo providers
// 静默忽略 gizmo 提供者的错误
console.warn(`[GizmoRegistry] Error in gizmo provider for ${componentType.name}:`, e);
logger.warn(`Error in gizmo provider for ${componentType.name}:`, e);
return [];
}
}
@@ -0,0 +1,301 @@
/**
* @zh
* @en Generic Registry Base Class
*
* @zh 16+ Registry
* @en Provides common registry implementation, eliminating duplicate code in 16+ Registry classes.
*
* @example
* ```typescript
* interface IMyItem { id: string; name: string; }
*
* class MyRegistry extends BaseRegistry<IMyItem> {
* constructor() {
* super('MyRegistry');
* }
*
* protected getItemId(item: IMyItem): string {
* return item.id;
* }
* }
* ```
*/
import { IService, createLogger, type ILogger } from '@esengine/ecs-framework';
/**
* @zh
* @en Base interface for registrable items
*/
export interface IRegistrable {
/** @zh 唯一标识符 @en Unique identifier */
readonly id?: string;
}
/**
* @zh
* @en Registrable item with priority
*/
export interface IPrioritized {
/** @zh 优先级(越高越先匹配) @en Priority (higher = matched first) */
readonly priority?: number;
}
/**
* @zh
* @en Registrable item with order
*/
export interface IOrdered {
/** @zh 排序权重(越小越靠前) @en Sort order (lower = first) */
readonly order?: number;
}
/**
* @zh
* @en Registry configuration
*/
export interface RegistryOptions {
/** @zh 是否允许覆盖已存在的项 @en Allow overwriting existing items */
allowOverwrite?: boolean;
/** @zh 是否在覆盖时发出警告 @en Warn when overwriting */
warnOnOverwrite?: boolean;
}
/**
* @zh
* @en Generic Registry Base Class
*
* @typeParam T - @zh @en Registered item type
* @typeParam K - @zh string @en Key type (default string)
*/
export abstract class BaseRegistry<T, K extends string = string> implements IService {
protected readonly _items: Map<K, T> = new Map();
protected readonly _logger: ILogger;
protected readonly _options: Required<RegistryOptions>;
constructor(
protected readonly _name: string,
options: RegistryOptions = {}
) {
this._logger = createLogger(_name);
this._options = {
allowOverwrite: options.allowOverwrite ?? true,
warnOnOverwrite: options.warnOnOverwrite ?? true
};
}
/**
* @zh
* @en Get item key
*/
protected abstract getItemKey(item: T): K;
/**
* @zh
* @en Get item display name (for logging)
*/
protected getItemDisplayName(item: T): string {
const key = this.getItemKey(item);
return String(key);
}
// ========== 核心 CRUD 操作 | Core CRUD Operations ==========
/**
* @zh
* @en Register item
*/
register(item: T): boolean {
const key = this.getItemKey(item);
const displayName = this.getItemDisplayName(item);
if (this._items.has(key)) {
if (this._options.warnOnOverwrite) {
this._logger.warn(`Overwriting: ${displayName}`);
}
if (!this._options.allowOverwrite) {
return false;
}
}
this._items.set(key, item);
this._logger.debug(`Registered: ${displayName}`);
return true;
}
/**
* @zh
* @en Register multiple items
*/
registerMany(items: T[]): number {
let count = 0;
for (const item of items) {
if (this.register(item)) count++;
}
return count;
}
/**
* @zh
* @en Unregister item
*/
unregister(key: K): boolean {
if (this._items.delete(key)) {
this._logger.debug(`Unregistered: ${key}`);
return true;
}
return false;
}
/**
* @zh
* @en Get item
*/
get(key: K): T | undefined {
return this._items.get(key);
}
/**
* @zh
* @en Check if exists
*/
has(key: K): boolean {
return this._items.has(key);
}
// ========== 查询操作 | Query Operations ==========
/**
* @zh
* @en Get all items
*/
getAll(): T[] {
return Array.from(this._items.values());
}
/**
* @zh
* @en Get all keys
*/
getAllKeys(): K[] {
return Array.from(this._items.keys());
}
/**
* @zh
* @en Get item count
*/
get size(): number {
return this._items.size;
}
/**
* @zh
* @en Is empty
*/
get isEmpty(): boolean {
return this._items.size === 0;
}
/**
* @zh
* @en Filter by predicate
*/
filter(predicate: (item: T, key: K) => boolean): T[] {
const result: T[] = [];
for (const [key, item] of this._items) {
if (predicate(item, key)) {
result.push(item);
}
}
return result;
}
/**
* @zh
* @en Find first matching item
*/
find(predicate: (item: T, key: K) => boolean): T | undefined {
for (const [key, item] of this._items) {
if (predicate(item, key)) {
return item;
}
}
return undefined;
}
// ========== 排序操作 | Sorting Operations ==========
/**
* @zh order
* @en Sort items by order field (lower = first)
*/
protected sortByOrder<U extends IOrdered>(items: U[], defaultOrder = 0): U[] {
return items.sort((a, b) => (a.order ?? defaultOrder) - (b.order ?? defaultOrder));
}
// ========== 生命周期 | Lifecycle ==========
/**
* @zh
* @en Clear registry
*/
clear(): void {
this._items.clear();
this._logger.debug('Cleared');
}
/**
* @zh
* @en Dispose resources
*/
dispose(): void {
this.clear();
this._logger.debug('Disposed');
}
}
/**
* @zh
* @en Registry base class with priority-based lookup
*
* @zh InspectorFieldEditor
* @en For scenarios requiring priority-based matching (e.g., Inspector, FieldEditor)
*/
export abstract class PrioritizedRegistry<T extends IPrioritized, K extends string = string>
extends BaseRegistry<T, K> {
/**
* @zh
* @en Get all items sorted by priority
*/
getAllSorted(): T[] {
return this.getAll().sort((a, b) => (b.priority ?? 0) - (a.priority ?? 0));
}
/**
* @zh
* @en Find first item that can handle the target
*
* @param canHandle - @zh @en Predicate function
*/
findByPriority(canHandle: (item: T) => boolean): T | undefined {
for (const item of this.getAllSorted()) {
if (canHandle(item)) {
return item;
}
}
return undefined;
}
}
/**
* @zh
* @en Create registry service identifier
*
* @zh 使 Symbol.for Symbol
* @en Uses Symbol.for to ensure same Symbol is shared across packages
*/
export function createRegistryToken<T>(name: string): symbol {
return Symbol.for(`IRegistry:${name}`);
}
@@ -7,6 +7,7 @@
*/
import type { IService } from '@esengine/ecs-framework';
import { createLogger } from '@esengine/ecs-framework';
import type {
IBuildPipeline,
IBuildPipelineRegistry,
@@ -17,6 +18,8 @@ import type {
} from './IBuildPipeline';
import { BuildStatus } from './IBuildPipeline';
const logger = createLogger('BuildService');
/**
* Build task.
*
@@ -88,10 +91,10 @@ export class BuildService implements IService, IBuildPipelineRegistry {
*/
register(pipeline: IBuildPipeline): void {
if (this._pipelines.has(pipeline.platform)) {
console.warn(`[BuildService] Overwriting existing pipeline: ${pipeline.platform} | 覆盖已存在的构建管线: ${pipeline.platform}`);
logger.warn(`Overwriting existing pipeline: ${pipeline.platform} | 覆盖已存在的构建管线: ${pipeline.platform}`);
}
this._pipelines.set(pipeline.platform, pipeline);
console.log(`[BuildService] Registered pipeline: ${pipeline.displayName} | 注册构建管线: ${pipeline.displayName}`);
logger.info(`Registered pipeline: ${pipeline.displayName} | 注册构建管线: ${pipeline.displayName}`);
}
/**
@@ -256,7 +259,7 @@ export class BuildService implements IService, IBuildPipelineRegistry {
if (this._currentTask) {
this._currentTask.abortController.abort();
this._currentTask.progress.status = BuildStatus.Cancelled;
console.log('[BuildService] Build cancelled | 构建已取消');
logger.info('Build cancelled | 构建已取消');
}
}
@@ -50,6 +50,7 @@ import type {
import { BuildPlatform, BuildStatus } from '../IBuildPipeline';
import type { ModuleManifest } from '../../Module/ModuleTypes';
import { hashFileInfo } from '@esengine/asset-system';
import { generateImportMap, type ImportMapConfig } from '@esengine/runtime-core';
// ============================================================================
// Build File System Interface
@@ -1655,33 +1656,20 @@ Built with ESEngine | 使用 ESEngine 构建
}
/**
* Generate Import Map from module manifests.
* Import Map
* @zh 使 ImportMapGenerator Import Map
* @en Generate Import Map using shared ImportMapGenerator
*
* @zh 使 @esengine/runtime-core generateImportMap
* @en Unified usage of generateImportMap utility from @esengine/runtime-core
*/
private _generateImportMap(coreModules: ModuleManifest[], pluginModules: ModuleManifest[]): Record<string, string> {
const imports: Record<string, string> = {};
for (const module of coreModules) {
if (module.name) {
imports[module.name] = './libs/esengine.core.js';
}
}
for (const module of pluginModules) {
if (module.name) {
imports[module.name] = `./libs/plugins/${module.id}.js`;
}
if (module.externalDependencies) {
for (const dep of module.externalDependencies) {
if (!imports[dep]) {
const depId = dep.startsWith('@esengine/') ? dep.slice(10) : dep;
imports[dep] = `./libs/plugins/${depId}.js`;
}
}
}
}
return imports;
const config: ImportMapConfig = {
mode: 'production',
basePath: '.',
coreModules,
pluginModules
};
return generateImportMap(config);
}
private _generateSingleBundleHtml(mainScenePath: string, wasmRuntimePath: string | null): string {
@@ -1,202 +1,185 @@
import { ICommand } from './ICommand';
/**
*
* @zh
* @en Command history configuration
*/
export interface CommandManagerConfig {
/**
*
*/
/** @zh 最大历史记录数量 @en Maximum history size */
maxHistorySize?: number;
/**
*
*/
/** @zh 是否自动合并相似命令 @en Auto merge similar commands */
autoMerge?: boolean;
}
/**
*
*
* @zh -
* @en Command Manager - Manages command execution, undo, redo and history
*/
export class CommandManager {
private undoStack: ICommand[] = [];
private redoStack: ICommand[] = [];
private readonly config: Required<CommandManagerConfig>;
private isExecuting = false;
private _undoStack: ICommand[] = [];
private _redoStack: ICommand[] = [];
private readonly _config: Required<CommandManagerConfig>;
private _isExecuting = false;
constructor(config: CommandManagerConfig = {}) {
this.config = {
this._config = {
maxHistorySize: config.maxHistorySize ?? 100,
autoMerge: config.autoMerge ?? true
};
}
/**
*
* @zh
* @en Try to merge command with the top of stack
*/
execute(command: ICommand): void {
if (this.isExecuting) {
throw new Error('不能在命令执行过程中执行新命令');
private _tryMergeWithLast(command: ICommand): boolean {
if (!this._config.autoMerge || this._undoStack.length === 0) {
return false;
}
this.isExecuting = true;
const lastCommand = this._undoStack[this._undoStack.length - 1];
if (lastCommand?.canMergeWith(command)) {
this._undoStack[this._undoStack.length - 1] = lastCommand.mergeWith(command);
this._redoStack = [];
return true;
}
try {
command.execute();
return false;
}
if (this.config.autoMerge && this.undoStack.length > 0) {
const lastCommand = this.undoStack[this.undoStack.length - 1];
if (lastCommand && lastCommand.canMergeWith(command)) {
const mergedCommand = lastCommand.mergeWith(command);
this.undoStack[this.undoStack.length - 1] = mergedCommand;
this.redoStack = [];
/**
* @zh
* @en Push command to undo stack
*/
private _pushToUndoStack(command: ICommand): void {
if (this._tryMergeWithLast(command)) {
return;
}
}
this.undoStack.push(command);
this.redoStack = [];
this._undoStack.push(command);
this._redoStack = [];
if (this.undoStack.length > this.config.maxHistorySize) {
this.undoStack.shift();
}
} finally {
this.isExecuting = false;
if (this._undoStack.length > this._config.maxHistorySize) {
this._undoStack.shift();
}
}
/**
*
* @zh
* @en Execute command
*/
execute(command: ICommand): void {
if (this._isExecuting) {
throw new Error('Cannot execute command while another is executing');
}
this._isExecuting = true;
try {
command.execute();
this._pushToUndoStack(command);
} finally {
this._isExecuting = false;
}
}
/**
* @zh
* @en Undo last command
*/
undo(): void {
if (this.isExecuting) {
throw new Error('不能在命令执行过程中撤销');
if (this._isExecuting) {
throw new Error('Cannot undo while executing');
}
const command = this.undoStack.pop();
if (!command) {
return;
}
const command = this._undoStack.pop();
if (!command) return;
this.isExecuting = true;
this._isExecuting = true;
try {
command.undo();
this.redoStack.push(command);
this._redoStack.push(command);
} catch (error) {
this.undoStack.push(command);
this._undoStack.push(command);
throw error;
} finally {
this.isExecuting = false;
this._isExecuting = false;
}
}
/**
*
* @zh
* @en Redo last undone command
*/
redo(): void {
if (this.isExecuting) {
throw new Error('不能在命令执行过程中重做');
if (this._isExecuting) {
throw new Error('Cannot redo while executing');
}
const command = this.redoStack.pop();
if (!command) {
return;
}
const command = this._redoStack.pop();
if (!command) return;
this.isExecuting = true;
this._isExecuting = true;
try {
command.execute();
this.undoStack.push(command);
this._undoStack.push(command);
} catch (error) {
this.redoStack.push(command);
this._redoStack.push(command);
throw error;
} finally {
this.isExecuting = false;
this._isExecuting = false;
}
}
/**
*
*/
/** @zh 检查是否可以撤销 @en Check if can undo */
canUndo(): boolean {
return this.undoStack.length > 0;
return this._undoStack.length > 0;
}
/**
*
*/
/** @zh 检查是否可以重做 @en Check if can redo */
canRedo(): boolean {
return this.redoStack.length > 0;
return this._redoStack.length > 0;
}
/**
*
*/
/** @zh 获取撤销栈的描述列表 @en Get undo history descriptions */
getUndoHistory(): string[] {
return this.undoStack.map((cmd) => cmd.getDescription());
return this._undoStack.map(cmd => cmd.getDescription());
}
/**
*
*/
/** @zh 获取重做栈的描述列表 @en Get redo history descriptions */
getRedoHistory(): string[] {
return this.redoStack.map((cmd) => cmd.getDescription());
return this._redoStack.map(cmd => cmd.getDescription());
}
/**
*
*/
/** @zh 清空所有历史记录 @en Clear all history */
clear(): void {
this.undoStack = [];
this.redoStack = [];
this._undoStack = [];
this._redoStack = [];
}
/**
*
* @zh
* @en Execute batch commands (as single operation, can be undone at once)
*/
executeBatch(commands: ICommand[]): void {
if (commands.length === 0) {
return;
}
const batchCommand = new BatchCommand(commands);
this.execute(batchCommand);
if (commands.length === 0) return;
this.execute(new BatchCommand(commands));
}
/**
*
* Push command to undo stack without executing
*
*
* Used for operations that have already been performed (like drag transforms),
* only need to record to history
* @zh
* @en Push command to undo stack without executing (for already performed operations)
*/
pushWithoutExecute(command: ICommand): void {
if (this.config.autoMerge && this.undoStack.length > 0) {
const lastCommand = this.undoStack[this.undoStack.length - 1];
if (lastCommand && lastCommand.canMergeWith(command)) {
const mergedCommand = lastCommand.mergeWith(command);
this.undoStack[this.undoStack.length - 1] = mergedCommand;
this.redoStack = [];
return;
}
}
this.undoStack.push(command);
this.redoStack = [];
if (this.undoStack.length > this.config.maxHistorySize) {
this.undoStack.shift();
}
this._pushToUndoStack(command);
}
}
/**
*
*
* @zh -
* @en Batch Command - Combines multiple commands into one
*/
class BatchCommand implements ICommand {
constructor(private readonly commands: ICommand[]) {}
@@ -209,15 +192,12 @@ class BatchCommand implements ICommand {
undo(): void {
for (let i = this.commands.length - 1; i >= 0; i--) {
const command = this.commands[i];
if (command) {
command.undo();
}
this.commands[i]?.undo();
}
}
getDescription(): string {
return `批量操作 (${this.commands.length} 个命令)`;
return `Batch (${this.commands.length} commands)`;
}
canMergeWith(): boolean {
@@ -225,6 +205,6 @@ class BatchCommand implements ICommand {
}
mergeWith(): ICommand {
throw new Error('批量命令不支持合并');
throw new Error('Batch commands cannot be merged');
}
}
@@ -1,37 +1,28 @@
import { IService } from '@esengine/ecs-framework';
import { ICompiler } from './ICompiler';
/**
* @zh
* @en Compiler Registry
*/
export class CompilerRegistry implements IService {
private compilers: Map<string, ICompiler> = new Map();
import { BaseRegistry, createRegistryToken } from './BaseRegistry';
import type { ICompiler } from './ICompiler';
register(compiler: ICompiler): void {
if (this.compilers.has(compiler.id)) {
console.warn(`Compiler with id "${compiler.id}" is already registered. Overwriting.`);
}
this.compilers.set(compiler.id, compiler);
/**
* @zh
* @en Compiler Registry
*/
export class CompilerRegistry extends BaseRegistry<ICompiler> {
constructor() {
super('CompilerRegistry');
}
unregister(compilerId: string): void {
this.compilers.delete(compilerId);
protected getItemKey(item: ICompiler): string {
return item.id;
}
get(compilerId: string): ICompiler | undefined {
return this.compilers.get(compilerId);
}
getAll(): ICompiler[] {
return Array.from(this.compilers.values());
}
clear(): void {
this.compilers.clear();
}
dispose(): void {
this.clear();
protected override getItemDisplayName(item: ICompiler): string {
return `${item.name} (${item.id})`;
}
}
// Service identifier for DI registration (用于跨包插件访问)
// 使用 Symbol.for 确保跨包共享同一个 Symbol
export const ICompilerRegistry = Symbol.for('ICompilerRegistry');
/** @zh 编译器注册表服务标识符 @en Compiler registry service identifier */
export const ICompilerRegistry = createRegistryToken<CompilerRegistry>('CompilerRegistry');
@@ -1,41 +1,55 @@
/**
* Component Action Registry Service
* @zh
* @en Component Action Registry Service
*
* Manages component-specific actions for the inspector panel
* @zh
* @en Manages component-specific actions for the inspector panel
*/
import { injectable } from 'tsyringe';
import type { IService } from '@esengine/ecs-framework';
import { createLogger, type ILogger, type IService } from '@esengine/ecs-framework';
import type { ComponentAction } from '../Plugin/EditorModule';
// Re-export ComponentAction type from Plugin system
import { createRegistryToken } from './BaseRegistry';
export type { ComponentAction } from '../Plugin/EditorModule';
@injectable()
export class ComponentActionRegistry implements IService {
private actions: Map<string, ComponentAction[]> = new Map();
/**
* Register a component action
* @zh
* @en Component Action Registry
*/
register(action: ComponentAction): void {
const componentName = action.componentName;
if (!this.actions.has(componentName)) {
this.actions.set(componentName, []);
export class ComponentActionRegistry implements IService {
private readonly _actions = new Map<string, ComponentAction[]>();
private readonly _logger: ILogger;
constructor() {
this._logger = createLogger('ComponentActionRegistry');
}
const actions = this.actions.get(componentName)!;
const existingIndex = actions.findIndex(a => a.id === action.id);
/**
* @zh
* @en Register component action
*/
register(action: ComponentAction): void {
const { componentName, id } = action;
if (!this._actions.has(componentName)) {
this._actions.set(componentName, []);
}
const actions = this._actions.get(componentName)!;
const existingIndex = actions.findIndex(a => a.id === id);
if (existingIndex >= 0) {
console.warn(`[ComponentActionRegistry] Action '${action.id}' already exists for '${componentName}', overwriting`);
this._logger.warn(`Overwriting action: ${id} for ${componentName}`);
actions[existingIndex] = action;
} else {
actions.push(action);
this._logger.debug(`Registered action: ${id} for ${componentName}`);
}
}
/**
* Register multiple actions
* @zh
* @en Register multiple actions
*/
registerMany(actions: ComponentAction[]): void {
for (const action of actions) {
@@ -44,45 +58,55 @@ export class ComponentActionRegistry implements IService {
}
/**
* Unregister an action by ID
* @zh
* @en Unregister action
*/
unregister(componentName: string, actionId: string): void {
const actions = this.actions.get(componentName);
if (actions) {
unregister(componentName: string, actionId: string): boolean {
const actions = this._actions.get(componentName);
if (!actions) return false;
const index = actions.findIndex(a => a.id === actionId);
if (index >= 0) {
if (index < 0) return false;
actions.splice(index, 1);
this._logger.debug(`Unregistered action: ${actionId} from ${componentName}`);
if (actions.length === 0) {
this._actions.delete(componentName);
}
}
return true;
}
/**
* Get all actions for a component type sorted by order
* @zh order
* @en Get all actions for component (sorted by order)
*/
getActionsForComponent(componentName: string): ComponentAction[] {
const actions = this.actions.get(componentName) || [];
const actions = this._actions.get(componentName);
if (!actions) return [];
return [...actions].sort((a, b) => (a.order ?? 100) - (b.order ?? 100));
}
/**
* Check if a component has any actions
* @zh
* @en Check if component has actions
*/
hasActions(componentName: string): boolean {
const actions = this.actions.get(componentName);
const actions = this._actions.get(componentName);
return actions !== undefined && actions.length > 0;
}
/**
* Clear all actions
*/
/** @zh 清空所有动作 @en Clear all actions */
clear(): void {
this.actions.clear();
this._actions.clear();
this._logger.debug('Cleared');
}
/**
* Dispose resources
*/
/** @zh 释放资源 @en Dispose resources */
dispose(): void {
this.actions.clear();
this.clear();
}
}
/** @zh 组件动作注册表服务标识符 @en Component action registry service identifier */
export const IComponentActionRegistry = createRegistryToken<ComponentActionRegistry>('ComponentActionRegistry');
@@ -1,162 +1,136 @@
import React from 'react';
import { Component, IService, createLogger } from '@esengine/ecs-framework';
const logger = createLogger('ComponentInspectorRegistry');
import { Component } from '@esengine/ecs-framework';
import { PrioritizedRegistry, createRegistryToken, type IPrioritized } from './BaseRegistry';
/**
*
* Context passed to component inspectors
* @zh
* @en Context passed to component inspectors
*/
export interface ComponentInspectorContext {
/** 被检查的组件 */
/** @zh 被检查的组件 @en The component being inspected */
component: Component;
/** 所属实体 */
/** @zh 所属实体 @en Owner entity */
entity: any;
/** 版本号(用于触发重渲染) */
/** @zh 版本号(用于触发重渲染) @en Version (for triggering re-renders) */
version?: number;
/** 属性变更回调 */
/** @zh 属性变更回调 @en Property change callback */
onChange?: (propertyName: string, value: any) => void;
/** 动作回调 */
/** @zh 动作回调 @en Action callback */
onAction?: (actionId: string, propertyName: string, component: Component) => void;
}
/**
* Inspector render mode.
*
* @zh
* @en Inspector render mode
*/
export type InspectorRenderMode = 'replace' | 'append';
/**
*
* Interface for custom component inspectors
* @zh
* @en Interface for custom component inspectors
*/
export interface IComponentInspector<T extends Component = Component> {
/** 唯一标识符 */
export interface IComponentInspector<T extends Component = Component> extends IPrioritized {
/** @zh 唯一标识符 @en Unique identifier */
readonly id: string;
/** 显示名称 */
/** @zh 显示名称 @en Display name */
readonly name: string;
/** 优先级(数字越大优先级越高) */
readonly priority?: number;
/** 目标组件类型名称列表 */
/** @zh 目标组件类型名称列表 @en Target component type names */
readonly targetComponents: string[];
/**
*
* - 'replace': PropertyInspector
* - 'append': PropertyInspector
* @zh 'replace' 'append'
* @en Render mode: 'replace' replaces default, 'append' appends after default
*/
readonly renderMode?: InspectorRenderMode;
/**
*
*/
/** @zh 判断是否可以处理该组件 @en Check if can handle the component */
canHandle(component: Component): component is T;
/**
*
*/
/** @zh 渲染组件检查器 @en Render component inspector */
render(context: ComponentInspectorContext): React.ReactElement;
}
/**
*
* Registry for custom component inspectors
* @zh
* @en Registry for custom component inspectors
*/
export class ComponentInspectorRegistry implements IService {
private inspectors: Map<string, IComponentInspector> = new Map();
/**
*
*/
register(inspector: IComponentInspector): void {
if (this.inspectors.has(inspector.id)) {
logger.warn(`Overwriting existing component inspector: ${inspector.id}`);
export class ComponentInspectorRegistry extends PrioritizedRegistry<IComponentInspector> {
constructor() {
super('ComponentInspectorRegistry');
}
this.inspectors.set(inspector.id, inspector);
logger.debug(`Registered component inspector: ${inspector.name} (${inspector.id})`);
protected getItemKey(item: IComponentInspector): string {
return item.id;
}
protected override getItemDisplayName(item: IComponentInspector): string {
return `${item.name} (${item.id})`;
}
/**
*
*/
unregister(inspectorId: string): void {
if (this.inspectors.delete(inspectorId)) {
logger.debug(`Unregistered component inspector: ${inspectorId}`);
}
}
/**
* replace
* Find inspector that can handle the component (replace mode only)
* @zh replace
* @en Find inspector that can handle the component (replace mode only)
*/
findInspector(component: Component): IComponentInspector | undefined {
const inspectors = Array.from(this.inspectors.values())
.filter(i => i.renderMode !== 'append')
.sort((a, b) => (b.priority || 0) - (a.priority || 0));
for (const inspector of inspectors) {
const sorted = this.getAllSorted().filter(i => i.renderMode !== 'append');
for (const inspector of sorted) {
try {
if (inspector.canHandle(component)) {
return inspector;
}
} catch (error) {
logger.error(`Error in canHandle for inspector ${inspector.id}:`, error);
this._logger.error(`Error in canHandle for ${inspector.id}:`, error);
}
}
return undefined;
}
/**
*
* Find all append-mode inspectors for the component
* @zh
* @en Find all append-mode inspectors for the component
*/
findAppendInspectors(component: Component): IComponentInspector[] {
const sorted = this.getAllSorted().filter(i => i.renderMode === 'append');
const result: IComponentInspector[] = [];
const inspectors = Array.from(this.inspectors.values())
.filter(i => i.renderMode === 'append')
.sort((a, b) => (b.priority || 0) - (a.priority || 0));
for (const inspector of inspectors) {
for (const inspector of sorted) {
try {
if (inspector.canHandle(component)) {
result.push(inspector);
}
} catch (error) {
logger.error(`Error in canHandle for inspector ${inspector.id}:`, error);
this._logger.error(`Error in canHandle for ${inspector.id}:`, error);
}
}
return result;
}
/**
* replace
* @zh replace
* @en Check if has custom inspector (replace mode)
*/
hasInspector(component: Component): boolean {
return this.findInspector(component) !== undefined;
}
/**
*
* @zh
* @en Check if has append inspectors
*/
hasAppendInspectors(component: Component): boolean {
return this.findAppendInspectors(component).length > 0;
}
/**
* replace
* Render component with replace-mode inspector
* @zh replace
* @en Render component with replace-mode inspector
*/
render(context: ComponentInspectorContext): React.ReactElement | null {
const inspector = this.findInspector(context.component);
if (!inspector) {
return null;
}
if (!inspector) return null;
try {
return inspector.render(context);
} catch (error) {
logger.error(`Error rendering with inspector ${inspector.id}:`, error);
this._logger.error(`Error rendering with ${inspector.id}:`, error);
return React.createElement(
'span',
{ style: { color: '#f87171', fontStyle: 'italic' } },
@@ -166,46 +140,37 @@ export class ComponentInspectorRegistry implements IService {
}
/**
*
* Render append-mode inspectors
* @zh
* @en Render append-mode inspectors
*/
renderAppendInspectors(context: ComponentInspectorContext): React.ReactElement[] {
const inspectors = this.findAppendInspectors(context.component);
const elements: React.ReactElement[] = [];
for (const inspector of inspectors) {
return inspectors.map(inspector => {
try {
elements.push(
React.createElement(
return React.createElement(
React.Fragment,
{ key: inspector.id },
inspector.render(context)
)
);
} catch (error) {
logger.error(`Error rendering append inspector ${inspector.id}:`, error);
elements.push(
React.createElement(
this._logger.error(`Error rendering ${inspector.id}:`, error);
return React.createElement(
'span',
{ key: inspector.id, style: { color: '#f87171', fontStyle: 'italic' } },
`[${inspector.name} Error]`
)
);
}
}
return elements;
});
}
/**
*
* @zh
* @en Get all registered inspectors
*/
getAllInspectors(): IComponentInspector[] {
return Array.from(this.inspectors.values());
return this.getAll();
}
}
dispose(): void {
this.inspectors.clear();
logger.debug('ComponentInspectorRegistry disposed');
}
}
/** @zh 组件检查器注册表服务标识符 @en Component inspector registry service identifier */
export const IComponentInspectorRegistry = createRegistryToken<ComponentInspectorRegistry>('ComponentInspectorRegistry');
@@ -1,11 +1,22 @@
import { Injectable, IService, Component } from '@esengine/ecs-framework';
import { Component } from '@esengine/ecs-framework';
import { BaseRegistry, createRegistryToken } from './BaseRegistry';
/**
* @zh
* @en Component type info
*/
export interface ComponentTypeInfo {
/** @zh 组件名称 @en Component name */
name: string;
/** @zh 组件类型 @en Component type */
type?: new (...args: any[]) => Component;
/** @zh 分类 @en Category */
category?: string;
/** @zh 描述 @en Description */
description?: string;
/** @zh 图标 @en Icon */
icon?: string;
/** @zh 元数据 @en Metadata */
metadata?: {
path?: string;
fileName?: string;
@@ -14,47 +25,61 @@ export interface ComponentTypeInfo {
}
/**
*
* Editor Component Registry
* @zh
* @en Editor Component Registry
*
*
* @zh
* ECS ComponentRegistry
*
* Manages component type metadata (name, category, icon, etc.) for the editor.
* @en Manages component type metadata (name, category, icon, etc.) for the editor.
* Different from the ECS core ComponentRegistry (which manages component bitmasks).
*/
@Injectable()
export class EditorComponentRegistry implements IService {
private components: Map<string, ComponentTypeInfo> = new Map();
public dispose(): void {
this.components.clear();
export class EditorComponentRegistry extends BaseRegistry<ComponentTypeInfo> {
constructor() {
super('EditorComponentRegistry');
}
public register(info: ComponentTypeInfo): void {
this.components.set(info.name, info);
protected getItemKey(item: ComponentTypeInfo): string {
return item.name;
}
public unregister(name: string): void {
this.components.delete(name);
protected override getItemDisplayName(item: ComponentTypeInfo): string {
return `${item.name}${item.category ? ` [${item.category}]` : ''}`;
}
public getComponent(name: string): ComponentTypeInfo | undefined {
return this.components.get(name);
/**
* @zh
* @en Get component info
*/
getComponent(name: string): ComponentTypeInfo | undefined {
return this.get(name);
}
public getAllComponents(): ComponentTypeInfo[] {
return Array.from(this.components.values());
/**
* @zh
* @en Get all components
*/
getAllComponents(): ComponentTypeInfo[] {
return this.getAll();
}
public getComponentsByCategory(category: string): ComponentTypeInfo[] {
return this.getAllComponents().filter((c) => c.category === category);
/**
* @zh
* @en Get components by category
*/
getComponentsByCategory(category: string): ComponentTypeInfo[] {
return this.filter(c => c.category === category);
}
public createInstance(name: string, ...args: any[]): Component | null {
const info = this.components.get(name);
/**
* @zh
* @en Create component instance
*/
createInstance(name: string, ...args: any[]): Component | null {
const info = this.get(name);
if (!info || !info.type) return null;
return new info.type(...args);
}
}
/** @zh 编辑器组件注册表服务标识符 @en Editor component registry service identifier */
export const IEditorComponentRegistry = createRegistryToken<EditorComponentRegistry>('EditorComponentRegistry');
@@ -6,11 +6,14 @@
*
*/
import { createLogger } from '@esengine/ecs-framework';
import type { IViewportService, ViewportCameraConfig } from './IViewportService';
import type { IPreviewScene } from './PreviewSceneService';
import { PreviewSceneService } from './PreviewSceneService';
import type { IViewportOverlay, OverlayRenderContext } from '../Rendering/IViewportOverlay';
const logger = createLogger('EditorViewportService');
/**
* Configuration for an editor viewport
*
@@ -201,7 +204,7 @@ export class EditorViewportService implements IEditorViewportService {
registerViewport(config: EditorViewportConfig): void {
if (this._viewports.has(config.id)) {
console.warn(`[EditorViewportService] Viewport "${config.id}" already registered`);
logger.warn(`Viewport already registered: ${config.id}`);
return;
}
@@ -266,16 +269,17 @@ export class EditorViewportService implements IEditorViewportService {
addOverlay(viewportId: string, overlay: IViewportOverlay): void {
const state = this._viewports.get(viewportId);
if (!state) {
console.warn(`[EditorViewportService] Viewport "${viewportId}" not found`);
logger.warn(`Viewport not found: ${viewportId}`);
return;
}
if (state.overlays.has(overlay.id)) {
console.warn(`[EditorViewportService] Overlay "${overlay.id}" already exists in viewport "${viewportId}"`);
logger.warn(`Overlay already exists: ${overlay.id} in ${viewportId}`);
return;
}
state.overlays.set(overlay.id, overlay);
logger.debug(`Added overlay: ${overlay.id} to ${viewportId}`);
}
removeOverlay(viewportId: string, overlayId: string): void {
@@ -1,76 +1,47 @@
/**
* Entity Creation Registry Service
* @zh
* @en Entity Creation Registry Service
*
* Manages entity creation templates for the scene hierarchy context menu
* @zh
* @en Manages entity creation templates for the scene hierarchy context menu
*/
import { injectable } from 'tsyringe';
import type { IService } from '@esengine/ecs-framework';
import { BaseRegistry, createRegistryToken } from './BaseRegistry';
import type { EntityCreationTemplate } from '../Types/UITypes';
@injectable()
export class EntityCreationRegistry implements IService {
private templates: Map<string, EntityCreationTemplate> = new Map();
/**
* Register an entity creation template
* @zh
* @en Entity Creation Registry
*/
register(template: EntityCreationTemplate): void {
if (this.templates.has(template.id)) {
console.warn(`[EntityCreationRegistry] Template '${template.id}' already exists, overwriting`);
export class EntityCreationRegistry extends BaseRegistry<EntityCreationTemplate> {
constructor() {
super('EntityCreationRegistry');
}
this.templates.set(template.id, template);
protected getItemKey(item: EntityCreationTemplate): string {
return item.id;
}
protected override getItemDisplayName(item: EntityCreationTemplate): string {
return `${item.label} (${item.id})`;
}
/**
* Register multiple templates
* @zh order
* @en Get all templates sorted by order
*/
registerMany(templates: EntityCreationTemplate[]): void {
for (const template of templates) {
this.register(template);
}
getAllSorted(): EntityCreationTemplate[] {
return this.sortByOrder(this.getAll(), 100);
}
/**
* Unregister a template by ID
* @zh
* @en Get templates by category
*/
unregister(id: string): void {
this.templates.delete(id);
getByCategory(category: string): EntityCreationTemplate[] {
return this.sortByOrder(this.filter(t => t.category === category), 100);
}
}
/**
* Get all registered templates sorted by order
*/
getAll(): EntityCreationTemplate[] {
return Array.from(this.templates.values())
.sort((a, b) => (a.order ?? 100) - (b.order ?? 100));
}
/**
* Get a template by ID
*/
get(id: string): EntityCreationTemplate | undefined {
return this.templates.get(id);
}
/**
* Check if a template exists
*/
has(id: string): boolean {
return this.templates.has(id);
}
/**
* Clear all templates
*/
clear(): void {
this.templates.clear();
}
/**
* Dispose resources
*/
dispose(): void {
this.templates.clear();
}
}
/** @zh 实体创建模板注册表服务标识符 @en Entity creation registry service identifier */
export const IEntityCreationRegistry = createRegistryToken<EntityCreationRegistry>('EntityCreationRegistry');
@@ -1,50 +1,52 @@
import { IService, createLogger } from '@esengine/ecs-framework';
import { IFieldEditor, IFieldEditorRegistry, FieldEditorContext } from './IFieldEditor';
/**
* @zh
* @en Field Editor Registry
*/
const logger = createLogger('FieldEditorRegistry');
import { PrioritizedRegistry, createRegistryToken } from './BaseRegistry';
import type { IFieldEditor, IFieldEditorRegistry, FieldEditorContext } from './IFieldEditor';
export class FieldEditorRegistry implements IFieldEditorRegistry, IService {
private editors: Map<string, IFieldEditor> = new Map();
/**
* @zh
* @en Field Editor Registry
*/
export class FieldEditorRegistry
extends PrioritizedRegistry<IFieldEditor>
implements IFieldEditorRegistry {
register(editor: IFieldEditor): void {
if (this.editors.has(editor.type)) {
logger.warn(`Overwriting existing field editor: ${editor.type}`);
constructor() {
super('FieldEditorRegistry');
}
this.editors.set(editor.type, editor);
logger.debug(`Registered field editor: ${editor.name} (${editor.type})`);
protected getItemKey(item: IFieldEditor): string {
return item.type;
}
unregister(type: string): void {
if (this.editors.delete(type)) {
logger.debug(`Unregistered field editor: ${type}`);
}
protected override getItemDisplayName(item: IFieldEditor): string {
return `${item.name} (${item.type})`;
}
/**
* @zh
* @en Get field editor
*/
getEditor(type: string, context?: FieldEditorContext): IFieldEditor | undefined {
const editor = this.editors.get(type);
if (editor) {
return editor;
}
const editors = Array.from(this.editors.values())
.sort((a, b) => (b.priority || 0) - (a.priority || 0));
for (const editor of editors) {
if (editor.canHandle(type, context)) {
return editor;
}
}
return undefined;
// 先尝试精确匹配
const exact = this.get(type);
if (exact) return exact;
// 再按优先级查找可处理的编辑器
return this.findByPriority(editor => editor.canHandle(type, context));
}
/**
* @zh
* @en Get all editors
*/
getAllEditors(): IFieldEditor[] {
return Array.from(this.editors.values());
return this.getAll();
}
}
dispose(): void {
this.editors.clear();
logger.debug('FieldEditorRegistry disposed');
}
}
/** @zh 字段编辑器注册表服务标识符 @en Field editor registry service identifier */
export const FieldEditorRegistryToken = createRegistryToken<FieldEditorRegistry>('FieldEditorRegistry');
@@ -1,115 +1,99 @@
import { IService } from '@esengine/ecs-framework';
import { IService, createLogger, type ILogger } from '@esengine/ecs-framework';
import type { FileActionHandler, FileCreationTemplate } from '../Plugin/EditorModule';
import { createRegistryToken } from './BaseRegistry';
// Re-export for backwards compatibility
export type { FileCreationTemplate } from '../Plugin/EditorModule';
/**
*
* Asset creation message mapping
*
* PropertyInspector
* @zh
* @en Asset creation message mapping
*/
export interface AssetCreationMapping {
/** 文件扩展名(包含点号,如 '.tilemap'| File extension (with dot) */
/** @zh 文件扩展名(包含点号,如 '.tilemap' @en File extension (with dot) */
extension: string;
/** 创建资产时发送的消息名 | Message name to publish when creating asset */
/** @zh 创建资产时发送的消息名 @en Message name to publish when creating asset */
createMessage: string;
/** 是否支持创建(可选,默认 true)| Whether creation is supported */
/** @zh 是否支持创建(可选,默认 true @en Whether creation is supported */
canCreate?: boolean;
}
/**
* FileActionRegistry
* FileActionRegistry service identifier
*/
export const IFileActionRegistry = Symbol.for('IFileActionRegistry');
/** @zh FileActionRegistry 服务标识符 @en FileActionRegistry service identifier */
export const IFileActionRegistry = createRegistryToken<FileActionRegistry>('FileActionRegistry');
/**
*
*
*
* @zh -
* @en File Action Registry Service - Manages file action handlers and creation templates
*/
export class FileActionRegistry implements IService {
private actionHandlers: Map<string, FileActionHandler[]> = new Map();
private creationTemplates: FileCreationTemplate[] = [];
private assetCreationMappings: Map<string, AssetCreationMapping> = new Map();
private readonly _actionHandlers = new Map<string, FileActionHandler[]>();
private readonly _creationTemplates: FileCreationTemplate[] = [];
private readonly _assetCreationMappings = new Map<string, AssetCreationMapping>();
private readonly _logger: ILogger;
constructor() {
this._logger = createLogger('FileActionRegistry');
}
/**
*
* @zh .
* @en Normalize extension (ensure starts with . and lowercase)
*/
private _normalizeExtension(ext: string): string {
const lower = ext.toLowerCase();
return lower.startsWith('.') ? lower : `.${lower}`;
}
/** @zh 注册文件操作处理器 @en Register file action handler */
registerActionHandler(handler: FileActionHandler): void {
for (const ext of handler.extensions) {
const handlers = this.actionHandlers.get(ext) || [];
const handlers = this._actionHandlers.get(ext) ?? [];
handlers.push(handler);
this.actionHandlers.set(ext, handlers);
this._actionHandlers.set(ext, handlers);
}
}
/**
*
*/
/** @zh 注销文件操作处理器 @en Unregister file action handler */
unregisterActionHandler(handler: FileActionHandler): void {
for (const ext of handler.extensions) {
const handlers = this.actionHandlers.get(ext);
if (handlers) {
const handlers = this._actionHandlers.get(ext);
if (!handlers) continue;
const index = handlers.indexOf(handler);
if (index !== -1) {
handlers.splice(index, 1);
}
if (handlers.length === 0) {
this.actionHandlers.delete(ext);
}
}
if (index !== -1) handlers.splice(index, 1);
if (handlers.length === 0) this._actionHandlers.delete(ext);
}
}
/**
*
*/
/** @zh 注册文件创建模板 @en Register file creation template */
registerCreationTemplate(template: FileCreationTemplate): void {
this.creationTemplates.push(template);
this._creationTemplates.push(template);
}
/**
*
*/
/** @zh 注销文件创建模板 @en Unregister file creation template */
unregisterCreationTemplate(template: FileCreationTemplate): void {
const index = this.creationTemplates.indexOf(template);
if (index !== -1) {
this.creationTemplates.splice(index, 1);
}
const index = this._creationTemplates.indexOf(template);
if (index !== -1) this._creationTemplates.splice(index, 1);
}
/**
*
*/
/** @zh 获取文件扩展名的处理器 @en Get handlers for extension */
getHandlersForExtension(extension: string): FileActionHandler[] {
return this.actionHandlers.get(extension) || [];
return this._actionHandlers.get(extension) ?? [];
}
/**
*
*/
/** @zh 获取文件的处理器 @en Get handlers for file */
getHandlersForFile(filePath: string): FileActionHandler[] {
const extension = this.getFileExtension(filePath);
return extension ? this.getHandlersForExtension(extension) : [];
const ext = this._extractFileExtension(filePath);
return ext ? this.getHandlersForExtension(ext) : [];
}
/**
*
*/
/** @zh 获取所有文件创建模板 @en Get all creation templates */
getCreationTemplates(): FileCreationTemplate[] {
return this.creationTemplates;
return this._creationTemplates;
}
/**
*
*/
/** @zh 处理文件双击 @en Handle file double click */
async handleDoubleClick(filePath: string): Promise<boolean> {
const handlers = this.getHandlersForFile(filePath);
for (const handler of handlers) {
for (const handler of this.getHandlersForFile(filePath)) {
if (handler.onDoubleClick) {
await handler.onDoubleClick(filePath);
return true;
@@ -118,12 +102,9 @@ export class FileActionRegistry implements IService {
return false;
}
/**
*
*/
/** @zh 处理文件打开 @en Handle file open */
async handleOpen(filePath: string): Promise<boolean> {
const handlers = this.getHandlersForFile(filePath);
for (const handler of handlers) {
for (const handler of this.getHandlersForFile(filePath)) {
if (handler.onOpen) {
await handler.onOpen(filePath);
return true;
@@ -132,78 +113,51 @@ export class FileActionRegistry implements IService {
return false;
}
/**
*
* Register asset creation message mapping
*/
/** @zh 注册资产创建消息映射 @en Register asset creation mapping */
registerAssetCreationMapping(mapping: AssetCreationMapping): void {
const normalizedExt = mapping.extension.startsWith('.')
? mapping.extension.toLowerCase()
: `.${mapping.extension.toLowerCase()}`;
this.assetCreationMappings.set(normalizedExt, {
...mapping,
extension: normalizedExt
});
const ext = this._normalizeExtension(mapping.extension);
this._assetCreationMappings.set(ext, { ...mapping, extension: ext });
this._logger.debug(`Registered asset creation mapping: ${ext}`);
}
/**
*
* Unregister asset creation message mapping
*/
/** @zh 注销资产创建消息映射 @en Unregister asset creation mapping */
unregisterAssetCreationMapping(extension: string): void {
const normalizedExt = extension.startsWith('.')
? extension.toLowerCase()
: `.${extension.toLowerCase()}`;
this.assetCreationMappings.delete(normalizedExt);
const ext = this._normalizeExtension(extension);
if (this._assetCreationMappings.delete(ext)) {
this._logger.debug(`Unregistered asset creation mapping: ${ext}`);
}
}
/**
*
* Get asset creation mapping for extension
*/
/** @zh 获取扩展名对应的资产创建消息映射 @en Get asset creation mapping for extension */
getAssetCreationMapping(extension: string): AssetCreationMapping | undefined {
const normalizedExt = extension.startsWith('.')
? extension.toLowerCase()
: `.${extension.toLowerCase()}`;
return this.assetCreationMappings.get(normalizedExt);
return this._assetCreationMappings.get(this._normalizeExtension(extension));
}
/**
*
* Check if extension supports asset creation
*/
/** @zh 检查扩展名是否支持创建资产 @en Check if extension supports asset creation */
canCreateAsset(extension: string): boolean {
const mapping = this.getAssetCreationMapping(extension);
return mapping?.canCreate !== false;
return this.getAssetCreationMapping(extension)?.canCreate !== false;
}
/**
*
* Get all asset creation mappings
*/
/** @zh 获取所有资产创建映射 @en Get all asset creation mappings */
getAllAssetCreationMappings(): AssetCreationMapping[] {
return Array.from(this.assetCreationMappings.values());
return Array.from(this._assetCreationMappings.values());
}
/**
*
*/
/** @zh 清空所有注册 @en Clear all registrations */
clear(): void {
this.actionHandlers.clear();
this.creationTemplates = [];
this.assetCreationMappings.clear();
this._actionHandlers.clear();
this._creationTemplates.length = 0;
this._assetCreationMappings.clear();
}
/**
*
*/
/** @zh 释放资源 @en Dispose resources */
dispose(): void {
this.clear();
}
private getFileExtension(filePath: string): string | null {
/** @zh 提取文件扩展名 @en Extract file extension */
private _extractFileExtension(filePath: string): string | null {
const lastDot = filePath.lastIndexOf('.');
if (lastDot === -1) return null;
return filePath.substring(lastDot + 1).toLowerCase();
return lastDot === -1 ? null : filePath.substring(lastDot + 1).toLowerCase();
}
}
@@ -1,81 +1,58 @@
import { IInspectorProvider, InspectorContext } from './IInspectorProvider';
import { IService } from '@esengine/ecs-framework';
/**
* @zh Inspector
* @en Inspector Registry
*/
import React from 'react';
export class InspectorRegistry implements IService {
private providers: Map<string, IInspectorProvider> = new Map();
import { PrioritizedRegistry, createRegistryToken } from './BaseRegistry';
import type { IInspectorProvider, InspectorContext } from './IInspectorProvider';
/**
* Inspector提供器
* @zh Inspector
* @en Inspector Registry
*/
register(provider: IInspectorProvider): void {
if (this.providers.has(provider.id)) {
console.warn(`Inspector provider with id "${provider.id}" is already registered`);
return;
export class InspectorRegistry extends PrioritizedRegistry<IInspectorProvider> {
constructor() {
super('InspectorRegistry');
}
this.providers.set(provider.id, provider);
protected getItemKey(item: IInspectorProvider): string {
return item.id;
}
/**
* Inspector提供器
*/
unregister(providerId: string): void {
this.providers.delete(providerId);
}
/**
* ID的提供器
* @zh ID
* @en Get provider by ID
*/
getProvider(providerId: string): IInspectorProvider | undefined {
return this.providers.get(providerId);
return this.get(providerId);
}
/**
*
* @zh
* @en Get all providers
*/
getAllProviders(): IInspectorProvider[] {
return Array.from(this.providers.values());
return this.getAll();
}
/**
*
*
* @zh
* @en Find provider that can handle the target
*/
findProvider(target: unknown): IInspectorProvider | undefined {
const providers = Array.from(this.providers.values())
.sort((a, b) => (b.priority || 0) - (a.priority || 0));
for (const provider of providers) {
if (provider.canHandle(target)) {
return provider;
}
}
return undefined;
return this.findByPriority(provider => provider.canHandle(target));
}
/**
* Inspector内容
*
* @zh Inspector
* @en Render inspector content
*/
render(target: unknown, context: InspectorContext): React.ReactElement | null {
const provider = this.findProvider(target);
if (!provider) {
return null;
}
return provider.render(target, context);
}
clear(): void {
this.providers.clear();
}
dispose(): void {
this.clear();
return provider?.render(target, context) ?? null;
}
}
// Service identifier for DI registration (用于跨包插件访问)
// 使用 Symbol.for 确保跨包共享同一个 Symbol
export const IInspectorRegistry = Symbol.for('IInspectorRegistry');
/** @zh Inspector 注册表服务标识符 @en Inspector registry service identifier */
export const IInspectorRegistry = createRegistryToken<InspectorRegistry>('InspectorRegistry');
@@ -91,14 +91,14 @@ export type TranslationParams = Record<string, string | number>;
*/
@Injectable()
export class LocaleService implements IService {
private currentLocale: Locale = 'en';
private translations: Map<Locale, Translations> = new Map();
private changeListeners: Set<(locale: Locale) => void> = new Set();
private _currentLocale: Locale = 'en';
private _translations: Map<Locale, Translations> = new Map();
private _changeListeners: Set<(locale: Locale) => void> = new Set();
constructor() {
const savedLocale = this.loadSavedLocale();
const savedLocale = this._loadSavedLocale();
if (savedLocale) {
this.currentLocale = savedLocale;
this._currentLocale = savedLocale;
}
}
@@ -113,7 +113,7 @@ export class LocaleService implements IService {
* @param translations - | Translation object
*/
public registerTranslations(locale: Locale, translations: Translations): void {
this.translations.set(locale, translations);
this._translations.set(locale, translations);
logger.info(`Registered translations for locale: ${locale}`);
}
@@ -153,19 +153,19 @@ export class LocaleService implements IService {
const locales: Locale[] = ['en', 'zh', 'es'];
for (const locale of locales) {
const existing = this.translations.get(locale) || {};
const existing = this._translations.get(locale) || {};
const pluginTrans = pluginTranslations[locale];
if (pluginTrans) {
// 深度合并到命名空间下 | Deep merge under namespace
const merged = {
...existing,
[namespace]: this.deepMerge(
[namespace]: this._deepMerge(
(existing[namespace] as Translations) || {},
pluginTrans
)
};
this.translations.set(locale, merged);
this._translations.set(locale, merged);
}
}
@@ -176,7 +176,7 @@ export class LocaleService implements IService {
*
* Deep merge two translation objects
*/
private deepMerge(target: Translations, source: Translations): Translations {
private _deepMerge(target: Translations, source: Translations): Translations {
const result: Translations = { ...target };
for (const key of Object.keys(source)) {
@@ -189,7 +189,7 @@ export class LocaleService implements IService {
typeof targetValue === 'object' &&
targetValue !== null
) {
result[key] = this.deepMerge(
result[key] = this._deepMerge(
targetValue as Translations,
sourceValue as Translations
);
@@ -206,7 +206,7 @@ export class LocaleService implements IService {
* Get current locale
*/
public getCurrentLocale(): Locale {
return this.currentLocale;
return this._currentLocale;
}
/**
@@ -224,15 +224,15 @@ export class LocaleService implements IService {
* @param locale - | Target locale code
*/
public setLocale(locale: Locale): void {
if (!this.translations.has(locale)) {
if (!this._translations.has(locale)) {
logger.warn(`Translations not found for locale: ${locale}`);
return;
}
this.currentLocale = locale;
this.saveLocale(locale);
this._currentLocale = locale;
this._saveLocale(locale);
this.changeListeners.forEach((listener) => listener(locale));
this._changeListeners.forEach((listener) => listener(locale));
logger.info(`Locale changed to: ${locale}`);
}
@@ -261,12 +261,12 @@ export class LocaleService implements IService {
* ```
*/
public t(key: string, params?: TranslationParams, fallback?: string): string {
const translations = this.translations.get(this.currentLocale);
const translations = this._translations.get(this._currentLocale);
if (!translations) {
return fallback || key;
}
const value = this.getNestedValue(translations, key);
const value = this._getNestedValue(translations, key);
if (typeof value === 'string') {
// 支持参数替换 {{key}} | Support parameter substitution {{key}}
if (params) {
@@ -288,10 +288,10 @@ export class LocaleService implements IService {
* @returns | Unsubscribe function
*/
public onChange(listener: (locale: Locale) => void): () => void {
this.changeListeners.add(listener);
this._changeListeners.add(listener);
return () => {
this.changeListeners.delete(listener);
this._changeListeners.delete(listener);
};
}
@@ -303,12 +303,12 @@ export class LocaleService implements IService {
* @param locale - 使 | Optional locale, defaults to current
*/
public hasKey(key: string, locale?: Locale): boolean {
const targetLocale = locale || this.currentLocale;
const translations = this.translations.get(targetLocale);
const targetLocale = locale || this._currentLocale;
const translations = this._translations.get(targetLocale);
if (!translations) {
return false;
}
const value = this.getNestedValue(translations, key);
const value = this._getNestedValue(translations, key);
return typeof value === 'string';
}
@@ -316,7 +316,7 @@ export class LocaleService implements IService {
*
* Get nested object value
*/
private getNestedValue(obj: Translations, path: string): string | Translations | undefined {
private _getNestedValue(obj: Translations, path: string): string | Translations | undefined {
const keys = path.split('.');
let current: string | Translations | undefined = obj;
@@ -335,7 +335,7 @@ export class LocaleService implements IService {
* localStorage
* Load saved locale from localStorage
*/
private loadSavedLocale(): Locale | null {
private _loadSavedLocale(): Locale | null {
try {
const saved = localStorage.getItem('editor-locale');
if (saved === 'en' || saved === 'zh' || saved === 'es') {
@@ -351,7 +351,7 @@ export class LocaleService implements IService {
* localStorage
* Save locale to localStorage
*/
private saveLocale(locale: Locale): void {
private _saveLocale(locale: Locale): void {
try {
localStorage.setItem('editor-locale', locale);
} catch (error) {
@@ -360,8 +360,8 @@ export class LocaleService implements IService {
}
public dispose(): void {
this.translations.clear();
this.changeListeners.clear();
this._translations.clear();
this._changeListeners.clear();
logger.info('LocaleService disposed');
}
}
@@ -6,6 +6,7 @@
*
*/
import { createLogger } from '@esengine/ecs-framework';
import type {
ModuleManifest,
ModuleRegistryEntry,
@@ -37,7 +38,8 @@ export interface IModuleFileSystem {
*
*/
export class ModuleRegistry {
private _modules: Map<string, ModuleRegistryEntry> = new Map();
private readonly _modules = new Map<string, ModuleRegistryEntry>();
private readonly _logger = createLogger('ModuleRegistry');
private _projectConfig: ProjectModuleConfig = { enabled: [] };
private _fileSystem: IModuleFileSystem | null = null;
private _engineModulesPath: string = '';
@@ -332,7 +334,7 @@ export class ModuleRegistry {
}>;
}>(indexPath);
console.log(`[ModuleRegistry] Loaded ${index.modules.length} modules from index.json`);
this._logger.debug(`Loaded ${index.modules.length} modules from index.json`);
// Use data directly from index.json (includes jsSize, wasmSize)
// 直接使用 index.json 中的数据(包含 jsSize、wasmSize
@@ -386,10 +388,10 @@ export class ModuleRegistry {
}
}
} else {
console.warn(`[ModuleRegistry] index.json not found at ${indexPath}, run 'pnpm copy-modules' first`);
this._logger.warn(`index.json not found at ${indexPath}, run 'pnpm copy-modules' first`);
}
} catch (error) {
console.error('[ModuleRegistry] Failed to load index.json:', error);
this._logger.error('Failed to load index.json:', error);
}
// Compute dependents | 计算依赖者
@@ -433,7 +435,7 @@ export class ModuleRegistry {
this._projectConfig = { enabled: this._getDefaultEnabledModules() };
}
} catch (error) {
console.error('[ModuleRegistry] Failed to load project config:', error);
this._logger.error('Failed to load project config:', error);
this._projectConfig = { enabled: this._getDefaultEnabledModules() };
}
}
@@ -457,7 +459,7 @@ export class ModuleRegistry {
config.modules = this._projectConfig;
await this._fileSystem.writeJson(configPath, config);
} catch (error) {
console.error('[ModuleRegistry] Failed to save project config:', error);
this._logger.error('Failed to save project config:', error);
}
}
@@ -545,7 +547,7 @@ export class ModuleRegistry {
}
}
} catch (error) {
console.warn('[ModuleRegistry] Failed to check scene usage:', error);
this._logger.warn('Failed to check scene usage:', error);
}
return usages;
@@ -602,7 +604,7 @@ export class ModuleRegistry {
}
}
} catch (error) {
console.warn('[ModuleRegistry] Failed to check script usage:', error);
this._logger.warn('Failed to check script usage:', error);
}
return usages;
@@ -1,56 +1,63 @@
/**
* @zh
* @en Property Renderer Registry
*/
import React from 'react';
import { IService, createLogger } from '@esengine/ecs-framework';
import { IPropertyRenderer, IPropertyRendererRegistry, PropertyContext } from './IPropertyRenderer';
import { PrioritizedRegistry, createRegistryToken } from './BaseRegistry';
import type { IPropertyRenderer, IPropertyRendererRegistry, PropertyContext } from './IPropertyRenderer';
const logger = createLogger('PropertyRendererRegistry');
/**
* @zh
* @en Property Renderer Registry
*/
export class PropertyRendererRegistry
extends PrioritizedRegistry<IPropertyRenderer>
implements IPropertyRendererRegistry {
export class PropertyRendererRegistry implements IPropertyRendererRegistry, IService {
private renderers: Map<string, IPropertyRenderer> = new Map();
register(renderer: IPropertyRenderer): void {
if (this.renderers.has(renderer.id)) {
logger.warn(`Overwriting existing property renderer: ${renderer.id}`);
constructor() {
super('PropertyRendererRegistry');
}
this.renderers.set(renderer.id, renderer);
logger.debug(`Registered property renderer: ${renderer.name} (${renderer.id})`);
protected getItemKey(item: IPropertyRenderer): string {
return item.id;
}
unregister(rendererId: string): void {
if (this.renderers.delete(rendererId)) {
logger.debug(`Unregistered property renderer: ${rendererId}`);
}
protected override getItemDisplayName(item: IPropertyRenderer): string {
return `${item.name} (${item.id})`;
}
findRenderer(value: any, context: PropertyContext): IPropertyRenderer | undefined {
const renderers = Array.from(this.renderers.values())
.sort((a, b) => (b.priority || 0) - (a.priority || 0));
for (const renderer of renderers) {
/**
* @zh
* @en Find renderer
*/
findRenderer(value: unknown, context: PropertyContext): IPropertyRenderer | undefined {
return this.findByPriority(renderer => {
try {
if (renderer.canHandle(value, context)) {
return renderer;
}
return renderer.canHandle(value, context);
} catch (error) {
logger.error(`Error in canHandle for renderer ${renderer.id}:`, error);
this._logger.error(`Error in canHandle for ${renderer.id}:`, error);
return false;
}
});
}
return undefined;
}
render(value: any, context: PropertyContext): React.ReactElement | null {
/**
* @zh
* @en Render property
*/
render(value: unknown, context: PropertyContext): React.ReactElement | null {
const renderer = this.findRenderer(value, context);
if (!renderer) {
logger.debug(`No renderer found for value type: ${typeof value}`);
this._logger.debug(`No renderer found for value type: ${typeof value}`);
return null;
}
try {
return renderer.render(value, context);
} catch (error) {
logger.error(`Error rendering with ${renderer.id}:`, error);
this._logger.error(`Error rendering with ${renderer.id}:`, error);
return React.createElement(
'span',
{ style: { color: '#f87171', fontStyle: 'italic' } },
@@ -59,16 +66,22 @@ export class PropertyRendererRegistry implements IPropertyRendererRegistry, ISer
}
}
/**
* @zh
* @en Get all renderers
*/
getAllRenderers(): IPropertyRenderer[] {
return Array.from(this.renderers.values());
return this.getAll();
}
hasRenderer(value: any, context: PropertyContext): boolean {
/**
* @zh
* @en Check if renderer is available
*/
hasRenderer(value: unknown, context: PropertyContext): boolean {
return this.findRenderer(value, context) !== undefined;
}
}
dispose(): void {
this.renderers.clear();
logger.debug('PropertyRendererRegistry disposed');
}
}
/** @zh 属性渲染器注册表服务标识符 @en Property renderer registry service identifier */
export const PropertyRendererRegistryToken = createRegistryToken<PropertyRendererRegistry>('PropertyRendererRegistry');
@@ -1,7 +1,7 @@
import type { IService } from '@esengine/ecs-framework';
import { Injectable } from '@esengine/ecs-framework';
import { createLogger } from '@esengine/ecs-framework';
import { Injectable, createLogger } from '@esengine/ecs-framework';
import type { ISerializer } from '../Plugin/EditorModule';
import { createRegistryToken } from './BaseRegistry';
const logger = createLogger('SerializerRegistry');
@@ -12,7 +12,7 @@ const logger = createLogger('SerializerRegistry');
*/
@Injectable()
export class SerializerRegistry implements IService {
private serializers: Map<string, ISerializer> = new Map();
private readonly _serializers = new Map<string, ISerializer>();
/**
*
@@ -24,12 +24,12 @@ export class SerializerRegistry implements IService {
const type = serializer.getSupportedType();
const key = `${pluginName}:${type}`;
if (this.serializers.has(key)) {
if (this._serializers.has(key)) {
logger.warn(`Serializer for ${key} is already registered`);
return;
}
this.serializers.set(key, serializer);
this._serializers.set(key, serializer);
logger.info(`Registered serializer: ${key}`);
}
@@ -54,7 +54,7 @@ export class SerializerRegistry implements IService {
*/
public unregister(pluginName: string, type: string): boolean {
const key = `${pluginName}:${type}`;
const result = this.serializers.delete(key);
const result = this._serializers.delete(key);
if (result) {
logger.info(`Unregistered serializer: ${key}`);
@@ -72,14 +72,14 @@ export class SerializerRegistry implements IService {
const prefix = `${pluginName}:`;
const keysToDelete: string[] = [];
for (const key of this.serializers.keys()) {
for (const key of this._serializers.keys()) {
if (key.startsWith(prefix)) {
keysToDelete.push(key);
}
}
for (const key of keysToDelete) {
this.serializers.delete(key);
this._serializers.delete(key);
logger.info(`Unregistered serializer: ${key}`);
}
}
@@ -93,7 +93,7 @@ export class SerializerRegistry implements IService {
*/
public get(pluginName: string, type: string): ISerializer | undefined {
const key = `${pluginName}:${type}`;
return this.serializers.get(key);
return this._serializers.get(key);
}
/**
@@ -105,7 +105,7 @@ export class SerializerRegistry implements IService {
public findByType(type: string): ISerializer[] {
const result: ISerializer[] = [];
for (const [key, serializer] of this.serializers) {
for (const [key, serializer] of this._serializers) {
if (key.endsWith(`:${type}`)) {
result.push(serializer);
}
@@ -120,7 +120,7 @@ export class SerializerRegistry implements IService {
* @returns
*/
public getAll(): Map<string, ISerializer> {
return new Map(this.serializers);
return new Map(this._serializers);
}
/**
@@ -132,7 +132,7 @@ export class SerializerRegistry implements IService {
*/
public has(pluginName: string, type: string): boolean {
const key = `${pluginName}:${type}`;
return this.serializers.has(key);
return this._serializers.has(key);
}
/**
@@ -175,7 +175,10 @@ export class SerializerRegistry implements IService {
*
*/
public dispose(): void {
this.serializers.clear();
this._serializers.clear();
logger.info('SerializerRegistry disposed');
}
}
/** @zh 序列化器注册表服务标识符 @en Serializer registry service identifier */
export const ISerializerRegistry = createRegistryToken<SerializerRegistry>('SerializerRegistry');
@@ -1,5 +1,10 @@
import { Injectable, IService } from '@esengine/ecs-framework';
import { createLogger, type ILogger, type IService } from '@esengine/ecs-framework';
import { createRegistryToken } from './BaseRegistry';
/**
* @zh
* @en Setting type
*/
export type SettingType = 'string' | 'number' | 'boolean' | 'select' | 'color' | 'range' | 'pluginList' | 'collisionMatrix' | 'moduleList';
/**
@@ -82,127 +87,154 @@ export interface SettingCategory {
sections: SettingSection[];
}
@Injectable()
/**
* @zh
* @en Settings Registry
*/
export class SettingsRegistry implements IService {
private categories: Map<string, SettingCategory> = new Map();
private readonly _categories = new Map<string, SettingCategory>();
private readonly _logger: ILogger;
public dispose(): void {
this.categories.clear();
constructor() {
this._logger = createLogger('SettingsRegistry');
}
public registerCategory(category: SettingCategory): void {
if (this.categories.has(category.id)) {
console.warn(`[SettingsRegistry] Category ${category.id} already registered, overwriting`);
}
console.log(`[SettingsRegistry] Registering category: ${category.id} (${category.title}), sections: ${category.sections.map(s => s.id).join(', ')}`);
this.categories.set(category.id, category);
/** @zh 释放资源 @en Dispose resources */
dispose(): void {
this._categories.clear();
this._logger.debug('Disposed');
}
public registerSection(categoryId: string, section: SettingSection): void {
let category = this.categories.get(categoryId);
if (!category) {
category = {
id: categoryId,
title: categoryId,
sections: []
};
this.categories.set(categoryId, category);
/**
* @zh
* @en Register setting category
*/
registerCategory(category: SettingCategory): void {
if (this._categories.has(category.id)) {
this._logger.warn(`Overwriting category: ${category.id}`);
}
this._categories.set(category.id, category);
this._logger.debug(`Registered category: ${category.id}`);
}
const existingIndex = category.sections.findIndex((s) => s.id === section.id);
/**
* @zh
* @en Register setting section
*/
registerSection(categoryId: string, section: SettingSection): void {
const category = this._ensureCategory(categoryId);
const existingIndex = category.sections.findIndex(s => s.id === section.id);
if (existingIndex >= 0) {
category.sections[existingIndex] = section;
console.warn(`[SettingsRegistry] Section ${section.id} in category ${categoryId} already exists, overwriting`);
this._logger.warn(`Overwriting section: ${section.id} in ${categoryId}`);
} else {
category.sections.push(section);
this._logger.debug(`Registered section: ${section.id} in ${categoryId}`);
}
}
public registerSetting(categoryId: string, sectionId: string, setting: SettingDescriptor): void {
let category = this.categories.get(categoryId);
/**
* @zh
* @en Register single setting
*/
registerSetting(categoryId: string, sectionId: string, setting: SettingDescriptor): void {
const category = this._ensureCategory(categoryId);
const section = this._ensureSection(category, sectionId);
if (!category) {
category = {
id: categoryId,
title: categoryId,
sections: []
};
this.categories.set(categoryId, category);
}
let section = category.sections.find((s) => s.id === sectionId);
if (!section) {
section = {
id: sectionId,
title: sectionId,
settings: []
};
category.sections.push(section);
}
const existingIndex = section.settings.findIndex((s) => s.key === setting.key);
const existingIndex = section.settings.findIndex(s => s.key === setting.key);
if (existingIndex >= 0) {
section.settings[existingIndex] = setting;
console.warn(`[SettingsRegistry] Setting ${setting.key} in section ${sectionId} already exists, overwriting`);
this._logger.warn(`Overwriting setting: ${setting.key} in ${sectionId}`);
} else {
section.settings.push(setting);
this._logger.debug(`Registered setting: ${setting.key} in ${sectionId}`);
}
}
public unregisterCategory(categoryId: string): void {
this.categories.delete(categoryId);
/**
* @zh
* @en Ensure category exists
*/
private _ensureCategory(categoryId: string): SettingCategory {
let category = this._categories.get(categoryId);
if (!category) {
category = { id: categoryId, title: categoryId, sections: [] };
this._categories.set(categoryId, category);
}
return category;
}
public unregisterSection(categoryId: string, sectionId: string): void {
const category = this.categories.get(categoryId);
if (category) {
category.sections = category.sections.filter((s) => s.id !== sectionId);
/**
* @zh
* @en Ensure section exists
*/
private _ensureSection(category: SettingCategory, sectionId: string): SettingSection {
let section = category.sections.find(s => s.id === sectionId);
if (!section) {
section = { id: sectionId, title: sectionId, settings: [] };
category.sections.push(section);
}
return section;
}
/** @zh 注销分类 @en Unregister category */
unregisterCategory(categoryId: string): void {
this._categories.delete(categoryId);
}
/** @zh 注销分区 @en Unregister section */
unregisterSection(categoryId: string, sectionId: string): void {
const category = this._categories.get(categoryId);
if (!category) return;
category.sections = category.sections.filter(s => s.id !== sectionId);
if (category.sections.length === 0) {
this.categories.delete(categoryId);
}
this._categories.delete(categoryId);
}
}
public getCategory(categoryId: string): SettingCategory | undefined {
return this.categories.get(categoryId);
/** @zh 获取分类 @en Get category */
getCategory(categoryId: string): SettingCategory | undefined {
return this._categories.get(categoryId);
}
public getAllCategories(): SettingCategory[] {
return Array.from(this.categories.values());
/** @zh 获取所有分类 @en Get all categories */
getAllCategories(): SettingCategory[] {
return Array.from(this._categories.values());
}
public getSetting(categoryId: string, sectionId: string, key: string): SettingDescriptor | undefined {
const category = this.categories.get(categoryId);
if (!category) return undefined;
const section = category.sections.find((s) => s.id === sectionId);
if (!section) return undefined;
return section.settings.find((s) => s.key === key);
/** @zh 获取设置项 @en Get setting */
getSetting(categoryId: string, sectionId: string, key: string): SettingDescriptor | undefined {
const section = this._categories.get(categoryId)?.sections.find(s => s.id === sectionId);
return section?.settings.find(s => s.key === key);
}
public getAllSettings(): Map<string, SettingDescriptor> {
/** @zh 获取所有设置项 @en Get all settings */
getAllSettings(): Map<string, SettingDescriptor> {
const allSettings = new Map<string, SettingDescriptor>();
for (const category of this.categories.values()) {
for (const category of this._categories.values()) {
for (const section of category.sections) {
for (const setting of section.settings) {
allSettings.set(setting.key, setting);
}
}
}
return allSettings;
}
public validateSetting(setting: SettingDescriptor, value: any): boolean {
/**
* @zh
* @en Validate setting value
*/
validateSetting(setting: SettingDescriptor, value: unknown): boolean {
if (setting.validator) {
return setting.validator.validate(value);
}
switch (setting.type) {
case 'number':
case 'range':
if (typeof value !== 'number') return false;
if (setting.min !== undefined && value < setting.min) return false;
if (setting.max !== undefined && value > setting.max) return false;
@@ -215,14 +247,7 @@ export class SettingsRegistry implements IService {
return typeof value === 'string';
case 'select':
if (!setting.options) return false;
return setting.options.some((opt) => opt.value === value);
case 'range':
if (typeof value !== 'number') return false;
if (setting.min !== undefined && value < setting.min) return false;
if (setting.max !== undefined && value > setting.max) return false;
return true;
return setting.options?.some(opt => opt.value === value) ?? false;
case 'color':
return typeof value === 'string' && /^#[0-9A-Fa-f]{6}$/.test(value);
@@ -232,3 +257,6 @@ export class SettingsRegistry implements IService {
}
}
}
/** @zh 设置注册表服务标识符 @en Settings registry service identifier */
export const ISettingsRegistry = createRegistryToken<SettingsRegistry>('SettingsRegistry');
+93 -69
View File
@@ -1,36 +1,52 @@
import type { IService } from '@esengine/ecs-framework';
import { Injectable } from '@esengine/ecs-framework';
import { createLogger } from '@esengine/ecs-framework';
import { Injectable, createLogger } from '@esengine/ecs-framework';
import type { MenuItem, ToolbarItem, PanelDescriptor } from '../Types/UITypes';
import { createRegistryToken } from './BaseRegistry';
const logger = createLogger('UIRegistry');
/**
* UI
*
* UI
* @zh UI - UI
* @en UI Registry - Manages all editor UI extension point registration and queries
*/
/** @zh 带排序权重的项 @en Item with sort order */
interface IOrdered {
readonly order?: number;
}
@Injectable()
export class UIRegistry implements IService {
private menus: Map<string, MenuItem> = new Map();
private toolbarItems: Map<string, ToolbarItem> = new Map();
private panels: Map<string, PanelDescriptor> = new Map();
private readonly _menus = new Map<string, MenuItem>();
private readonly _toolbarItems = new Map<string, ToolbarItem>();
private readonly _panels = new Map<string, PanelDescriptor>();
// ========== 辅助方法 | Helper Methods ==========
/** @zh 按 order 排序 @en Sort by order */
private _sortByOrder<T extends IOrdered>(items: T[]): T[] {
return items.sort((a, b) => (a.order ?? 0) - (b.order ?? 0));
}
// ========== 菜单管理 | Menu Management ==========
/**
*
* @zh
* @en Register menu item
*/
public registerMenu(item: MenuItem): void {
if (this.menus.has(item.id)) {
if (this._menus.has(item.id)) {
logger.warn(`Menu item ${item.id} is already registered`);
return;
}
this.menus.set(item.id, item);
this._menus.set(item.id, item);
logger.debug(`Registered menu item: ${item.id}`);
}
/**
*
* @zh
* @en Register multiple menu items
*/
public registerMenus(items: MenuItem[]): void {
for (const item of items) {
@@ -39,56 +55,59 @@ export class UIRegistry implements IService {
}
/**
*
* @zh
* @en Unregister menu item
*/
public unregisterMenu(id: string): boolean {
const result = this.menus.delete(id);
const result = this._menus.delete(id);
if (result) {
logger.debug(`Unregistered menu item: ${id}`);
}
return result;
}
/**
*
*/
/** @zh 获取菜单项 @en Get menu item */
public getMenu(id: string): MenuItem | undefined {
return this.menus.get(id);
return this._menus.get(id);
}
/**
*
* @zh
* @en Get all menu items
*/
public getAllMenus(): MenuItem[] {
return Array.from(this.menus.values()).sort((a, b) => {
return (a.order ?? 0) - (b.order ?? 0);
});
return this._sortByOrder(Array.from(this._menus.values()));
}
/**
*
* @zh
* @en Get child menus of specified parent
*/
public getChildMenus(parentId: string): MenuItem[] {
return this.getAllMenus()
.filter((item) => item.parentId === parentId)
.sort((a, b) => (a.order ?? 0) - (b.order ?? 0));
return this._sortByOrder(
Array.from(this._menus.values()).filter((item) => item.parentId === parentId)
);
}
// ========== 工具栏管理 | Toolbar Management ==========
/**
*
* @zh
* @en Register toolbar item
*/
public registerToolbarItem(item: ToolbarItem): void {
if (this.toolbarItems.has(item.id)) {
if (this._toolbarItems.has(item.id)) {
logger.warn(`Toolbar item ${item.id} is already registered`);
return;
}
this.toolbarItems.set(item.id, item);
this._toolbarItems.set(item.id, item);
logger.debug(`Registered toolbar item: ${item.id}`);
}
/**
*
* @zh
* @en Register multiple toolbar items
*/
public registerToolbarItems(items: ToolbarItem[]): void {
for (const item of items) {
@@ -97,56 +116,59 @@ export class UIRegistry implements IService {
}
/**
*
* @zh
* @en Unregister toolbar item
*/
public unregisterToolbarItem(id: string): boolean {
const result = this.toolbarItems.delete(id);
const result = this._toolbarItems.delete(id);
if (result) {
logger.debug(`Unregistered toolbar item: ${id}`);
}
return result;
}
/**
*
*/
/** @zh 获取工具栏项 @en Get toolbar item */
public getToolbarItem(id: string): ToolbarItem | undefined {
return this.toolbarItems.get(id);
return this._toolbarItems.get(id);
}
/**
*
* @zh
* @en Get all toolbar items
*/
public getAllToolbarItems(): ToolbarItem[] {
return Array.from(this.toolbarItems.values()).sort((a, b) => {
return (a.order ?? 0) - (b.order ?? 0);
});
return this._sortByOrder(Array.from(this._toolbarItems.values()));
}
/**
*
* @zh
* @en Get toolbar items by group
*/
public getToolbarItemsByGroup(groupId: string): ToolbarItem[] {
return this.getAllToolbarItems()
.filter((item) => item.groupId === groupId)
.sort((a, b) => (a.order ?? 0) - (b.order ?? 0));
return this._sortByOrder(
Array.from(this._toolbarItems.values()).filter((item) => item.groupId === groupId)
);
}
// ========== 面板管理 | Panel Management ==========
/**
*
* @zh
* @en Register panel
*/
public registerPanel(panel: PanelDescriptor): void {
if (this.panels.has(panel.id)) {
if (this._panels.has(panel.id)) {
logger.warn(`Panel ${panel.id} is already registered`);
return;
}
this.panels.set(panel.id, panel);
this._panels.set(panel.id, panel);
logger.debug(`Registered panel: ${panel.id}`);
}
/**
*
* @zh
* @en Register multiple panels
*/
public registerPanels(panels: PanelDescriptor[]): void {
for (const panel of panels) {
@@ -155,48 +177,50 @@ export class UIRegistry implements IService {
}
/**
*
* @zh
* @en Unregister panel
*/
public unregisterPanel(id: string): boolean {
const result = this.panels.delete(id);
const result = this._panels.delete(id);
if (result) {
logger.debug(`Unregistered panel: ${id}`);
}
return result;
}
/**
*
*/
/** @zh 获取面板 @en Get panel */
public getPanel(id: string): PanelDescriptor | undefined {
return this.panels.get(id);
return this._panels.get(id);
}
/**
*
* @zh
* @en Get all panels
*/
public getAllPanels(): PanelDescriptor[] {
return Array.from(this.panels.values()).sort((a, b) => {
return (a.order ?? 0) - (b.order ?? 0);
});
return this._sortByOrder(Array.from(this._panels.values()));
}
/**
*
* @zh
* @en Get panels by position
*/
public getPanelsByPosition(position: string): PanelDescriptor[] {
return this.getAllPanels()
.filter((panel) => panel.position === position)
.sort((a, b) => (a.order ?? 0) - (b.order ?? 0));
return this._sortByOrder(
Array.from(this._panels.values()).filter((panel) => panel.position === position)
);
}
/**
*
*/
// ========== 生命周期 | Lifecycle ==========
/** @zh 释放资源 @en Dispose resources */
public dispose(): void {
this.menus.clear();
this.toolbarItems.clear();
this.panels.clear();
logger.info('UIRegistry disposed');
this._menus.clear();
this._toolbarItems.clear();
this._panels.clear();
logger.debug('Disposed');
}
}
/** @zh UI 注册表服务标识符 @en UI registry service identifier */
export const IUIRegistry = createRegistryToken<UIRegistry>('UIRegistry');
@@ -411,6 +411,27 @@ export interface IUserCodeService {
* Promise
*/
resetReady(): void;
/**
* Get the user code realm.
*
*
* The realm provides isolated registration for user components, systems, and services.
*
*
* @returns User code realm instance |
*/
getUserCodeRealm(): import('@esengine/runtime-core').UserCodeRealm;
/**
* Reset the user code realm for project switching.
*
*
* This clears all user-registered components, systems, and services,
* preparing for a new project's user code.
*
*/
resetRealm(): void;
}
import { EditorConfig } from '../../Config';
@@ -15,6 +15,7 @@ import {
COMPONENT_TYPE_NAME,
SYSTEM_TYPE_NAME
} from '@esengine/ecs-framework';
import { UserCodeRealm, type UserCodeRealmConfig } from '@esengine/runtime-core';
import type {
IUserCodeService,
UserScriptInfo,
@@ -89,10 +90,17 @@ export class UserCodeService implements IService, IUserCodeService {
private _readyPromise: Promise<void>;
private _readyResolve: (() => void) | undefined;
constructor(fileSystem: IFileSystem) {
/**
*
* User code realm for isolation
*/
private _userCodeRealm: UserCodeRealm;
constructor(fileSystem: IFileSystem, realmConfig?: UserCodeRealmConfig) {
this._fileSystem = fileSystem;
this._hotReloadCoordinator = new HotReloadCoordinator();
this._readyPromise = this._createReadyPromise();
this._userCodeRealm = new UserCodeRealm(realmConfig);
}
/**
@@ -388,6 +396,15 @@ export class UserCodeService implements IService, IUserCodeService {
if (this._isComponentClass(exported)) {
logger.debug(`Found component: ${name} | 发现组件: ${name}`);
// Register to UserCodeRealm for isolation
// 注册到 UserCodeRealm 实现隔离
try {
this._userCodeRealm.registerComponent(exported);
logger.info(`Component ${name} registered to user code realm`);
} catch (err) {
logger.warn(`Failed to register component ${name} to realm:`, err);
}
// Register with Core ComponentRegistry for serialization/deserialization
// 注册到核心 ComponentRegistry 用于序列化/反序列化
try {
@@ -459,7 +476,7 @@ export class UserCodeService implements IService, IUserCodeService {
// Access scene through Core.scene
// 通过 Core.scene 访问场景
const sdkGlobal = (window as any)[EditorConfig.globals.sdk];
const sdkGlobal = window.__ESENGINE_SDK__;
const Core = sdkGlobal?.Core;
const scene = Core?.scene;
if (!scene || !scene.entities) {
@@ -782,7 +799,7 @@ export class UserCodeService implements IService, IUserCodeService {
// Initialize hot reload coordinator with Core reference
// 使用 Core 引用初始化热更新协调器
const sdkGlobal = (window as any)[EditorConfig.globals.sdk];
const sdkGlobal = window.__ESENGINE_SDK__;
const Core = sdkGlobal?.Core;
if (Core) {
this._hotReloadCoordinator.initialize(Core);
@@ -982,6 +999,32 @@ export class UserCodeService implements IService, IUserCodeService {
this.stopWatch();
this._runtimeModule = undefined;
this._editorModule = undefined;
this._userCodeRealm.dispose();
}
/**
* Get the user code realm.
*
*
* The realm provides isolated registration for user components, systems, and services.
*
*
* @returns User code realm instance |
*/
getUserCodeRealm(): UserCodeRealm {
return this._userCodeRealm;
}
/**
* Reset the user code realm for project switching.
*
*
* This clears all user-registered components, systems, and services,
* preparing for a new project's user code.
*
*/
resetRealm(): void {
this._userCodeRealm.reset();
}
// ==================== Private Methods | 私有方法 ====================
@@ -12,7 +12,9 @@
* 使
*/
import type { Component, ComponentType, Entity } from '@esengine/ecs-framework';
import { createLogger, type Component, type ComponentType, type Entity } from '@esengine/ecs-framework';
const logger = createLogger('VirtualNodeRegistry');
/**
* Virtual node data
@@ -154,7 +156,7 @@ export class VirtualNodeRegistry {
try {
return provider(component, entity);
} catch (e) {
console.warn(`[VirtualNodeRegistry] Error in provider for ${componentType.name}:`, e);
logger.warn(`Error in provider for ${componentType.name}:`, e);
return [];
}
}
@@ -249,7 +251,7 @@ export class VirtualNodeRegistry {
try {
listener(event);
} catch (e) {
console.warn('[VirtualNodeRegistry] Error in change listener:', e);
logger.warn('Error in change listener:', e);
}
}
}
@@ -1,177 +1,162 @@
import { IService } from '@esengine/ecs-framework';
import { ComponentType } from 'react';
import { createLogger, type ILogger, type IService } from '@esengine/ecs-framework';
import type { ComponentType } from 'react';
import { createRegistryToken } from './BaseRegistry';
/**
*
* @zh
* @en Window descriptor
*/
export interface WindowDescriptor {
/**
*
*/
/** @zh 窗口唯一标识 @en Unique window ID */
id: string;
/**
*
*/
component: ComponentType<any>;
/**
*
*/
/** @zh 窗口组件 @en Window component */
component: ComponentType<unknown>;
/** @zh 窗口标题 @en Window title */
title?: string;
/**
*
*/
/** @zh 默认宽度 @en Default width */
defaultWidth?: number;
/**
*
*/
/** @zh 默认高度 @en Default height */
defaultHeight?: number;
}
/**
*
* @zh
* @en Window instance
*/
export interface WindowInstance {
/**
*
*/
/** @zh 窗口描述符 @en Window descriptor */
descriptor: WindowDescriptor;
/**
*
*/
/** @zh 是否打开 @en Whether the window is open */
isOpen: boolean;
/**
*
*/
params?: Record<string, any>;
/** @zh 窗口参数 @en Window parameters */
params?: Record<string, unknown>;
}
/**
*
*
*
* @zh -
* @en Window Registry Service - Manages plugin-registered window components
*/
export class WindowRegistry implements IService {
private windows: Map<string, WindowDescriptor> = new Map();
private openWindows: Map<string, WindowInstance> = new Map();
private listeners: Set<() => void> = new Set();
private readonly _windows = new Map<string, WindowDescriptor>();
private readonly _openWindows = new Map<string, WindowInstance>();
private readonly _listeners = new Set<() => void>();
private readonly _logger: ILogger;
constructor() {
this._logger = createLogger('WindowRegistry');
}
/**
*
* @zh
* @en Register a window
*/
registerWindow(descriptor: WindowDescriptor): void {
if (this.windows.has(descriptor.id)) {
console.warn(`Window ${descriptor.id} is already registered`);
if (this._windows.has(descriptor.id)) {
this._logger.warn(`Window already registered: ${descriptor.id}`);
return;
}
this.windows.set(descriptor.id, descriptor);
this._windows.set(descriptor.id, descriptor);
this._logger.debug(`Registered window: ${descriptor.id}`);
}
/**
*
* @zh
* @en Unregister a window
*/
unregisterWindow(windowId: string): void {
this.windows.delete(windowId);
this.openWindows.delete(windowId);
this.notifyListeners();
this._windows.delete(windowId);
this._openWindows.delete(windowId);
this._notifyListeners();
this._logger.debug(`Unregistered window: ${windowId}`);
}
/**
*
*/
/** @zh 获取窗口描述符 @en Get window descriptor */
getWindow(windowId: string): WindowDescriptor | undefined {
return this.windows.get(windowId);
return this._windows.get(windowId);
}
/**
*
*/
/** @zh 获取所有窗口描述符 @en Get all window descriptors */
getAllWindows(): WindowDescriptor[] {
return Array.from(this.windows.values());
return Array.from(this._windows.values());
}
/**
*
* @zh
* @en Open a window
*/
openWindow(windowId: string, params?: Record<string, any>): void {
const descriptor = this.windows.get(windowId);
openWindow(windowId: string, params?: Record<string, unknown>): void {
const descriptor = this._windows.get(windowId);
if (!descriptor) {
console.warn(`Window ${windowId} is not registered`);
this._logger.warn(`Window not registered: ${windowId}`);
return;
}
this.openWindows.set(windowId, {
this._openWindows.set(windowId, {
descriptor,
isOpen: true,
params
});
this.notifyListeners();
this._notifyListeners();
this._logger.debug(`Opened window: ${windowId}`);
}
/**
*
* @zh
* @en Close a window
*/
closeWindow(windowId: string): void {
this.openWindows.delete(windowId);
this.notifyListeners();
this._openWindows.delete(windowId);
this._notifyListeners();
this._logger.debug(`Closed window: ${windowId}`);
}
/**
*
*/
/** @zh 获取打开的窗口实例 @en Get open window instance */
getOpenWindow(windowId: string): WindowInstance | undefined {
return this.openWindows.get(windowId);
return this._openWindows.get(windowId);
}
/**
*
*/
/** @zh 获取所有打开的窗口 @en Get all open windows */
getAllOpenWindows(): WindowInstance[] {
return Array.from(this.openWindows.values());
return Array.from(this._openWindows.values());
}
/**
*
*/
/** @zh 检查窗口是否打开 @en Check if window is open */
isWindowOpen(windowId: string): boolean {
return this.openWindows.has(windowId);
return this._openWindows.has(windowId);
}
/**
*
* @zh
* @en Add change listener
* @returns @zh @en Unsubscribe function
*/
addListener(listener: () => void): () => void {
this.listeners.add(listener);
this._listeners.add(listener);
return () => {
this.listeners.delete(listener);
this._listeners.delete(listener);
};
}
/**
*
*/
private notifyListeners(): void {
this.listeners.forEach((listener) => listener());
/** @zh 通知所有监听器 @en Notify all listeners */
private _notifyListeners(): void {
for (const listener of this._listeners) {
listener();
}
}
/**
*
*/
/** @zh 清空所有窗口 @en Clear all windows */
clear(): void {
this.windows.clear();
this.openWindows.clear();
this.listeners.clear();
this._windows.clear();
this._openWindows.clear();
this._listeners.clear();
this._logger.debug('Cleared');
}
/**
*
*/
/** @zh 释放资源 @en Dispose resources */
dispose(): void {
this.clear();
}
}
/** @zh 窗口注册表服务标识符 @en Window registry service identifier */
export const IWindowRegistry = createRegistryToken<WindowRegistry>('WindowRegistry');
+49
View File
@@ -0,0 +1,49 @@
/**
* @zh
* @en Global type declarations
*
* @zh Window
* @en Extend Window interface to support editor runtime global variables
*/
/**
* @zh SDK
* @en SDK global object structure
*/
interface ESEngineSDK {
Core: typeof import('@esengine/ecs-framework').Core;
Scene: typeof import('@esengine/ecs-framework').Scene;
Entity: typeof import('@esengine/ecs-framework').Entity;
Component: typeof import('@esengine/ecs-framework').Component;
System: typeof import('@esengine/ecs-framework').System;
[key: string]: unknown;
}
/**
* @zh
* @en Plugin container structure
*/
interface PluginContainer {
[pluginName: string]: unknown;
}
/**
* @zh
* @en User code exports structure
*/
interface UserExports {
[name: string]: unknown;
}
declare global {
interface Window {
// ESEngine 全局变量(与 EditorConfig.globals 对应)
// ESEngine globals (matching EditorConfig.globals)
__ESENGINE_SDK__: ESEngineSDK | undefined;
__ESENGINE_PLUGINS__: PluginContainer | undefined;
__USER_RUNTIME_EXPORTS__: UserExports | undefined;
__USER_EDITOR_EXPORTS__: UserExports | undefined;
}
}
export {};

Some files were not shown because too many files have changed in this diff Show More