Compare commits

...

8 Commits

Author SHA1 Message Date
YHH
ddc7a7750e Chore/lint fixes (#212)
* fix(eslint): 修复装饰器缩进配置

* fix(eslint): 修复装饰器缩进配置

* chore: 删除未使用的导入

* chore(lint): 移除未使用的导入和变量

* chore(lint): 修复editor-app中未使用的函数参数

* chore(lint): 修复未使用的赋值变量

* chore(eslint): 将所有错误级别改为警告以通过CI

* fix(codeql): 修复GitHub Advanced Security检测到的问题
2025-11-02 23:50:41 +08:00
YHH
50a01d9dd3 chore: 统一并强化ESLint配置规则 2025-11-02 12:45:47 +08:00
YHH
793aad0a5e chore: 移除旧版ESLint配置并更新子模块 2025-11-02 12:22:27 +08:00
YHH
9c1bf8dbed refactor(core): 移除全局EventBus,实现场景级事件隔离 (#211) 2025-11-01 18:19:23 +08:00
YHH
620f3eecc7 style(core): ESLint自动修复代码格式问题 (#210) 2025-11-01 17:41:50 +08:00
YHH
4355538d8d refactor(core): 重构Scene和Entity的代码质量,消除所有Lint警告 (#209) 2025-11-01 17:18:12 +08:00
YHH
3ad5dc9ca3 refactor(core): 改进事件系统类型安全并消除 ESLint 警告 (#208) 2025-11-01 16:12:18 +08:00
YHH
57c7e7be3f feat(core):统一 Core 库的命名规范和代码风格 (#207) 2025-11-01 10:23:46 +08:00
213 changed files with 12929 additions and 13167 deletions

View File

@@ -1,50 +0,0 @@
{
"root": true,
"parser": "@typescript-eslint/parser",
"parserOptions": {
"ecmaVersion": 2020,
"sourceType": "module",
"project": "./tsconfig.json"
},
"plugins": [
"@typescript-eslint"
],
"extends": [
"eslint:recommended",
"plugin:@typescript-eslint/recommended"
],
"rules": {
"semi": ["error", "always"],
"quotes": ["error", "single", { "avoidEscape": true }],
"indent": ["error", 4, { "SwitchCase": 1 }],
"no-trailing-spaces": "error",
"eol-last": ["error", "always"],
"comma-dangle": ["error", "none"],
"object-curly-spacing": ["error", "always"],
"array-bracket-spacing": ["error", "never"],
"arrow-parens": ["error", "always"],
"no-multiple-empty-lines": ["error", { "max": 2, "maxEOF": 1 }],
"no-console": "off",
"@typescript-eslint/no-explicit-any": "error",
"@typescript-eslint/no-unsafe-assignment": "warn",
"@typescript-eslint/no-unsafe-member-access": "warn",
"@typescript-eslint/no-unsafe-call": "warn",
"@typescript-eslint/no-unsafe-return": "warn",
"@typescript-eslint/no-unsafe-argument": "warn",
"@typescript-eslint/explicit-module-boundary-types": "off",
"@typescript-eslint/no-unused-vars": ["warn", { "argsIgnorePattern": "^_" }],
"@typescript-eslint/no-non-null-assertion": "off"
},
"ignorePatterns": [
"node_modules/",
"dist/",
"bin/",
"build/",
"coverage/",
"thirdparty/",
"examples/lawn-mower-demo/",
"extensions/",
"*.min.js",
"*.d.ts"
]
}

View File

@@ -10,38 +10,43 @@ export default [
parser: tseslint.parser, parser: tseslint.parser,
parserOptions: { parserOptions: {
ecmaVersion: 2020, ecmaVersion: 2020,
sourceType: 'module' sourceType: 'module',
project: true,
tsconfigRootDir: import.meta.dirname
} }
}, },
rules: { rules: {
'semi': 'warn', 'semi': ['warn', 'always'],
'quotes': 'warn', 'quotes': ['warn', 'single', { avoidEscape: true }],
'indent': 'off', 'indent': ['warn', 4, {
SwitchCase: 1,
ignoredNodes: [
'PropertyDefinition[decorators.length > 0]',
'TSTypeParameterInstantiation'
]
}],
'no-trailing-spaces': 'warn', 'no-trailing-spaces': 'warn',
'eol-last': 'warn', 'eol-last': ['warn', 'always'],
'comma-dangle': 'warn', 'comma-dangle': ['warn', 'never'],
'object-curly-spacing': 'warn', 'object-curly-spacing': ['warn', 'always'],
'array-bracket-spacing': 'warn', 'array-bracket-spacing': ['warn', 'never'],
'arrow-parens': 'warn', 'arrow-parens': ['warn', 'always'],
'prefer-const': 'warn', 'no-multiple-empty-lines': ['warn', { max: 2, maxEOF: 1 }],
'no-multiple-empty-lines': 'warn',
'no-console': 'off', 'no-console': 'off',
'no-empty': 'warn',
'no-case-declarations': 'warn',
'no-useless-catch': 'warn',
'no-prototype-builtins': 'warn',
'@typescript-eslint/no-explicit-any': 'warn', '@typescript-eslint/no-explicit-any': 'warn',
'@typescript-eslint/no-unsafe-assignment': 'warn',
'@typescript-eslint/no-unsafe-member-access': 'warn',
'@typescript-eslint/no-unsafe-call': 'warn',
'@typescript-eslint/no-unsafe-return': 'warn',
'@typescript-eslint/no-unsafe-argument': 'warn',
'@typescript-eslint/no-unsafe-function-type': 'warn', '@typescript-eslint/no-unsafe-function-type': 'warn',
'@typescript-eslint/no-unsafe-assignment': 'off',
'@typescript-eslint/no-unsafe-member-access': 'off',
'@typescript-eslint/no-unsafe-call': 'off',
'@typescript-eslint/no-unsafe-return': 'off',
'@typescript-eslint/no-unsafe-argument': 'off',
'@typescript-eslint/explicit-module-boundary-types': 'off', '@typescript-eslint/explicit-module-boundary-types': 'off',
'@typescript-eslint/no-unused-vars': ['warn', { argsIgnorePattern: '^_' }], '@typescript-eslint/no-unused-vars': ['warn', { argsIgnorePattern: '^_' }],
'@typescript-eslint/no-non-null-assertion': 'off', '@typescript-eslint/no-non-null-assertion': 'off'
'@typescript-eslint/no-require-imports': 'warn',
'@typescript-eslint/no-this-alias': 'warn',
'no-case-declarations': 'warn',
'no-prototype-builtins': 'warn',
'no-empty': 'warn',
'no-useless-catch': 'warn'
} }
}, },
{ {

View File

@@ -216,7 +216,7 @@ export class BehaviorTreeRuntimeComponent extends Component {
*/ */
unobserveBlackboard(nodeId: string): void { unobserveBlackboard(nodeId: string): void {
for (const observers of this.blackboardObservers.values()) { for (const observers of this.blackboardObservers.values()) {
const index = observers.findIndex(o => o.nodeId === nodeId); const index = observers.findIndex((o) => o.nodeId === nodeId);
if (index !== -1) { if (index !== -1) {
observers.splice(index, 1); observers.splice(index, 1);
} }

View File

@@ -49,11 +49,11 @@ export class NodeMetadataRegistry {
} }
static getByCategory(category: string): NodeMetadata[] { static getByCategory(category: string): NodeMetadata[] {
return this.getAllMetadata().filter(m => m.category === category); return this.getAllMetadata().filter((m) => m.category === category);
} }
static getByNodeType(nodeType: NodeType): NodeMetadata[] { static getByNodeType(nodeType: NodeType): NodeMetadata[] {
return this.getAllMetadata().filter(m => m.nodeType === nodeType); return this.getAllMetadata().filter((m) => m.nodeType === nodeType);
} }
static getImplementationType(executorClass: Function): string | undefined { static getImplementationType(executorClass: Function): string | undefined {

View File

@@ -131,7 +131,7 @@ export class BehaviorTreeAssetValidator {
errors.push('Missing or invalid nodes array'); errors.push('Missing or invalid nodes array');
} else { } else {
const nodeIds = new Set<string>(); const nodeIds = new Set<string>();
const rootNode = asset.nodes.find(n => n.id === asset.rootNodeId); const rootNode = asset.nodes.find((n) => n.id === asset.rootNodeId);
if (!rootNode) { if (!rootNode) {
errors.push(`Root node '${asset.rootNodeId}' not found in nodes array`); errors.push(`Root node '${asset.rootNodeId}' not found in nodes array`);
@@ -157,7 +157,7 @@ export class BehaviorTreeAssetValidator {
// 检查子节点引用 // 检查子节点引用
if (node.children) { if (node.children) {
for (const childId of node.children) { for (const childId of node.children) {
if (!asset.nodes.find(n => n.id === childId)) { if (!asset.nodes.find((n) => n.id === childId)) {
errors.push(`Node ${node.id} references non-existent child: ${childId}`); errors.push(`Node ${node.id} references non-existent child: ${childId}`);
} }
} }
@@ -167,7 +167,7 @@ export class BehaviorTreeAssetValidator {
// 检查是否有孤立节点 // 检查是否有孤立节点
const referencedNodes = new Set<string>([asset.rootNodeId]); const referencedNodes = new Set<string>([asset.rootNodeId]);
const collectReferencedNodes = (nodeId: string) => { const collectReferencedNodes = (nodeId: string) => {
const node = asset.nodes.find(n => n.id === nodeId); const node = asset.nodes.find((n) => n.id === nodeId);
if (node && node.children) { if (node && node.children) {
for (const childId of node.children) { for (const childId of node.children) {
referencedNodes.add(childId); referencedNodes.add(childId);
@@ -206,8 +206,8 @@ export class BehaviorTreeAssetValidator {
// 检查属性绑定 // 检查属性绑定
if (asset.propertyBindings && Array.isArray(asset.propertyBindings)) { if (asset.propertyBindings && Array.isArray(asset.propertyBindings)) {
const nodeIds = new Set(asset.nodes.map(n => n.id)); const nodeIds = new Set(asset.nodes.map((n) => n.id));
const varNames = new Set(asset.blackboard?.map(v => v.name) || []); const varNames = new Set(asset.blackboard?.map((v) => v.name) || []);
for (const binding of asset.propertyBindings) { for (const binding of asset.propertyBindings) {
if (!nodeIds.has(binding.nodeId)) { if (!nodeIds.has(binding.nodeId)) {
@@ -276,7 +276,7 @@ export class BehaviorTreeAssetValidator {
// 计算最大深度 // 计算最大深度
const getDepth = (nodeId: string, currentDepth: number = 0): number => { const getDepth = (nodeId: string, currentDepth: number = 0): number => {
const node = asset.nodes.find(n => n.id === nodeId); const node = asset.nodes.find((n) => n.id === nodeId);
if (!node || !node.children || node.children.length === 0) { if (!node || !node.children || node.children.length === 0) {
return currentDepth; return currentDepth;
} }

View File

@@ -134,7 +134,7 @@ export class EditorFormatConverter {
* 查找根节点 * 查找根节点
*/ */
private static findRootNode(nodes: EditorNode[]): EditorNode | null { private static findRootNode(nodes: EditorNode[]): EditorNode | null {
return nodes.find(node => return nodes.find((node) =>
node.template.category === '根节点' || node.template.category === '根节点' ||
node.data.nodeType === 'root' node.data.nodeType === 'root'
) || null; ) || null;
@@ -144,7 +144,7 @@ export class EditorFormatConverter {
* 转换节点列表 * 转换节点列表
*/ */
private static convertNodes(editorNodes: EditorNode[]): BehaviorTreeNodeData[] { private static convertNodes(editorNodes: EditorNode[]): BehaviorTreeNodeData[] {
return editorNodes.map(node => this.convertNode(node)); return editorNodes.map((node) => this.convertNode(node));
} }
/** /**
@@ -211,13 +211,13 @@ export class EditorFormatConverter {
blackboard: BlackboardVariableDefinition[] blackboard: BlackboardVariableDefinition[]
): PropertyBinding[] { ): PropertyBinding[] {
const bindings: PropertyBinding[] = []; const bindings: PropertyBinding[] = [];
const blackboardVarNames = new Set(blackboard.map(v => v.name)); const blackboardVarNames = new Set(blackboard.map((v) => v.name));
const propertyConnections = connections.filter(conn => conn.connectionType === 'property'); const propertyConnections = connections.filter((conn) => conn.connectionType === 'property');
for (const conn of propertyConnections) { for (const conn of propertyConnections) {
const fromNode = nodes.find(n => n.id === conn.from); const fromNode = nodes.find((n) => n.id === conn.from);
const toNode = nodes.find(n => n.id === conn.to); const toNode = nodes.find((n) => n.id === conn.to);
if (!fromNode || !toNode || !conn.toProperty) { if (!fromNode || !toNode || !conn.toProperty) {
logger.warn(`跳过无效的属性连接: from=${conn.from}, to=${conn.to}`); logger.warn(`跳过无效的属性连接: from=${conn.from}, to=${conn.to}`);

View File

@@ -154,14 +154,14 @@ export class NodeTemplates {
*/ */
static getAllTemplates(): NodeTemplate[] { static getAllTemplates(): NodeTemplate[] {
const allMetadata = NodeMetadataRegistry.getAllMetadata(); const allMetadata = NodeMetadataRegistry.getAllMetadata();
return allMetadata.map(metadata => this.convertMetadataToTemplate(metadata)); return allMetadata.map((metadata) => this.convertMetadataToTemplate(metadata));
} }
/** /**
* 根据类型和子类型获取模板 * 根据类型和子类型获取模板
*/ */
static getTemplate(type: NodeType, subType: string): NodeTemplate | undefined { static getTemplate(type: NodeType, subType: string): NodeTemplate | undefined {
return this.getAllTemplates().find(t => { return this.getAllTemplates().find((t) => {
if (t.type !== type) return false; if (t.type !== type) return false;
const config: any = t.defaultConfig; const config: any = t.defaultConfig;
@@ -266,7 +266,7 @@ export class NodeTemplates {
} }
if (field.options) { if (field.options) {
property.options = field.options.map(opt => ({ property.options = field.options.map((opt) => ({
label: opt, label: opt,
value: opt value: opt
})); }));

View File

@@ -1,311 +0,0 @@
import { World, Scene, Entity } from '@esengine/ecs-framework';
import { AssetLoadingManager } from '../src/Services/AssetLoadingManager';
import {
LoadingState,
TimeoutError,
CircularDependencyError,
EntityDestroyedError
} from '../src/Services/AssetLoadingTypes';
describe('AssetLoadingManager', () => {
let manager: AssetLoadingManager;
let world: World;
let scene: Scene;
let parentEntity: Entity;
beforeEach(() => {
manager = new AssetLoadingManager();
world = new World();
scene = world.createScene('test');
parentEntity = scene.createEntity('parent');
});
afterEach(() => {
manager.dispose();
parentEntity.destroy();
});
describe('基本加载功能', () => {
test('成功加载资产', async () => {
const mockEntity = scene.createEntity('loaded');
const loader = jest.fn().mockResolvedValue(mockEntity);
const handle = manager.startLoading(
'test-asset',
parentEntity,
loader
);
expect(handle.getState()).toBe(LoadingState.Loading);
const result = await handle.promise;
expect(result).toBe(mockEntity);
expect(handle.getState()).toBe(LoadingState.Loaded);
expect(loader).toHaveBeenCalledTimes(1);
});
test('加载失败', async () => {
const mockError = new Error('Load failed');
const loader = jest.fn().mockRejectedValue(mockError);
const handle = manager.startLoading(
'test-asset',
parentEntity,
loader,
{ maxRetries: 0 }
);
await expect(handle.promise).rejects.toThrow('Load failed');
expect(handle.getState()).toBe(LoadingState.Failed);
expect(handle.getError()).toBe(mockError);
});
});
describe('超时机制', () => {
test('加载超时', async () => {
const loader = jest.fn().mockImplementation(() =>
new Promise(resolve => setTimeout(resolve, 10000))
);
const handle = manager.startLoading(
'test-asset',
parentEntity,
loader,
{ timeoutMs: 100, maxRetries: 0 }
);
await expect(handle.promise).rejects.toThrow(TimeoutError);
expect(handle.getState()).toBe(LoadingState.Timeout);
});
test('超时前完成', async () => {
const mockEntity = scene.createEntity('loaded');
const loader = jest.fn().mockImplementation(() =>
new Promise(resolve => setTimeout(() => resolve(mockEntity), 50))
);
const handle = manager.startLoading(
'test-asset',
parentEntity,
loader,
{ timeoutMs: 200 }
);
const result = await handle.promise;
expect(result).toBe(mockEntity);
expect(handle.getState()).toBe(LoadingState.Loaded);
});
});
describe('重试机制', () => {
test('失败后自动重试', async () => {
const mockEntity = scene.createEntity('loaded');
let attemptCount = 0;
const loader = jest.fn().mockImplementation(() => {
attemptCount++;
if (attemptCount < 3) {
return Promise.reject(new Error('Temporary error'));
}
return Promise.resolve(mockEntity);
});
const handle = manager.startLoading(
'test-asset',
parentEntity,
loader,
{ maxRetries: 3 }
);
const result = await handle.promise;
expect(result).toBe(mockEntity);
expect(loader).toHaveBeenCalledTimes(3);
expect(handle.getState()).toBe(LoadingState.Loaded);
});
test('重试次数用尽后失败', async () => {
const loader = jest.fn().mockRejectedValue(new Error('Persistent error'));
const handle = manager.startLoading(
'test-asset',
parentEntity,
loader,
{ maxRetries: 2 }
);
await expect(handle.promise).rejects.toThrow('Persistent error');
expect(loader).toHaveBeenCalledTimes(3); // 初始 + 2次重试
expect(handle.getState()).toBe(LoadingState.Failed);
});
});
describe('循环引用检测', () => {
test('检测直接循环引用', () => {
const loader = jest.fn().mockResolvedValue(scene.createEntity('loaded'));
// 先加载 assetA
const handleA = manager.startLoading(
'assetA',
parentEntity,
loader,
{ parentAssetId: undefined }
);
expect(handleA.getState()).toBe(LoadingState.Loading);
// 尝试在 assetA 的上下文中加载 assetB
// assetB 又尝试加载 assetA循环
expect(() => {
manager.startLoading(
'assetB',
parentEntity,
loader,
{ parentAssetId: 'assetB' } // assetB 的父是 assetB自我循环
);
}).toThrow(CircularDependencyError);
});
test('不误报非循环引用', () => {
const loader = jest.fn().mockResolvedValue(scene.createEntity('loaded'));
// assetA 加载 assetB正常
const handleA = manager.startLoading(
'assetA',
parentEntity,
loader
);
// assetB 加载 assetC正常不是循环
expect(() => {
manager.startLoading(
'assetC',
parentEntity,
loader,
{ parentAssetId: 'assetB' }
);
}).not.toThrow();
});
});
describe('实体生命周期安全', () => {
test('实体销毁后取消加载', async () => {
const loader = jest.fn().mockImplementation(() =>
new Promise(resolve => setTimeout(resolve, 100))
);
const handle = manager.startLoading(
'test-asset',
parentEntity,
loader
);
// 销毁实体
parentEntity.destroy();
// 等待一小段时间让检测生效
await new Promise(resolve => setTimeout(resolve, 50));
await expect(handle.promise).rejects.toThrow(EntityDestroyedError);
expect(handle.getState()).toBe(LoadingState.Cancelled);
});
});
describe('状态查询', () => {
test('获取加载进度', async () => {
const mockEntity = scene.createEntity('loaded');
const loader = jest.fn().mockImplementation(() =>
new Promise(resolve => setTimeout(() => resolve(mockEntity), 100))
);
const handle = manager.startLoading(
'test-asset',
parentEntity,
loader
);
const progress = handle.getProgress();
expect(progress.state).toBe(LoadingState.Loading);
expect(progress.elapsedMs).toBeGreaterThanOrEqual(0);
expect(progress.retryCount).toBe(0);
expect(progress.maxRetries).toBe(3);
await handle.promise;
});
test('获取统计信息', () => {
const loader = jest.fn().mockResolvedValue(scene.createEntity('loaded'));
manager.startLoading('asset1', parentEntity, loader);
manager.startLoading('asset2', parentEntity, loader);
const stats = manager.getStats();
expect(stats.totalTasks).toBe(2);
expect(stats.loadingTasks).toBe(2);
});
test('获取正在加载的资产列表', () => {
const loader = jest.fn().mockResolvedValue(scene.createEntity('loaded'));
manager.startLoading('asset1', parentEntity, loader);
manager.startLoading('asset2', parentEntity, loader);
const loadingAssets = manager.getLoadingAssets();
expect(loadingAssets).toContain('asset1');
expect(loadingAssets).toContain('asset2');
expect(loadingAssets.length).toBe(2);
});
});
describe('任务管理', () => {
test('取消加载任务', () => {
const loader = jest.fn().mockImplementation(() =>
new Promise(resolve => setTimeout(resolve, 1000))
);
const handle = manager.startLoading(
'test-asset',
parentEntity,
loader
);
expect(handle.getState()).toBe(LoadingState.Loading);
handle.cancel();
expect(handle.getState()).toBe(LoadingState.Cancelled);
});
test('清空所有任务', async () => {
const loader = jest.fn().mockResolvedValue(scene.createEntity('loaded'));
manager.startLoading('asset1', parentEntity, loader);
manager.startLoading('asset2', parentEntity, loader);
expect(manager.getLoadingAssets().length).toBe(2);
manager.clear();
expect(manager.getLoadingAssets().length).toBe(0);
});
test('复用已存在的加载任务', () => {
const loader = jest.fn().mockResolvedValue(scene.createEntity('loaded'));
const handle1 = manager.startLoading('test-asset', parentEntity, loader);
const handle2 = manager.startLoading('test-asset', parentEntity, loader);
// 应该返回同一个任务
expect(handle1.assetId).toBe(handle2.assetId);
expect(loader).toHaveBeenCalledTimes(1); // 只加载一次
});
});
});

View File

@@ -1,50 +0,0 @@
import eslint from '@eslint/js';
import tseslint from 'typescript-eslint';
export default [
eslint.configs.recommended,
...tseslint.configs.recommended,
{
files: ['src/**/*.{ts,tsx}'],
languageOptions: {
parser: tseslint.parser,
parserOptions: {
ecmaVersion: 2020,
sourceType: 'module',
project: './tsconfig.json'
}
},
rules: {
'semi': ['error', 'always'],
'quotes': ['error', 'single', { avoidEscape: true }],
'indent': ['error', 4, { SwitchCase: 1 }],
'no-trailing-spaces': 'error',
'eol-last': ['error', 'always'],
'comma-dangle': ['error', 'never'],
'object-curly-spacing': ['error', 'always'],
'array-bracket-spacing': ['error', 'never'],
'arrow-parens': ['error', 'always'],
'no-multiple-empty-lines': ['error', { max: 2, maxEOF: 1 }],
'no-console': 'off',
'@typescript-eslint/no-explicit-any': 'error',
'@typescript-eslint/no-unsafe-assignment': 'warn',
'@typescript-eslint/no-unsafe-member-access': 'warn',
'@typescript-eslint/no-unsafe-call': 'warn',
'@typescript-eslint/no-unsafe-return': 'warn',
'@typescript-eslint/no-unsafe-argument': 'warn',
'@typescript-eslint/explicit-module-boundary-types': 'off',
'@typescript-eslint/no-unused-vars': ['warn', { argsIgnorePattern: '^_' }],
'@typescript-eslint/no-non-null-assertion': 'off'
}
},
{
ignores: [
'node_modules/**',
'dist/**',
'bin/**',
'coverage/**',
'**/*.min.js',
'**/*.d.ts'
]
}
];

View File

@@ -329,8 +329,8 @@ export class Core {
*/ */
public static setScene<T extends IScene>(scene: T): T { public static setScene<T extends IScene>(scene: T): T {
if (!this._instance) { if (!this._instance) {
Core._logger.warn("Core实例未创建请先调用Core.create()"); Core._logger.warn('Core实例未创建请先调用Core.create()');
throw new Error("Core实例未创建"); throw new Error('Core实例未创建');
} }
return this._instance._sceneManager.setScene(scene); return this._instance._sceneManager.setScene(scene);
@@ -387,7 +387,7 @@ export class Core {
*/ */
public static loadScene<T extends IScene>(scene: T): void { public static loadScene<T extends IScene>(scene: T): void {
if (!this._instance) { if (!this._instance) {
Core._logger.warn("Core实例未创建请先调用Core.create()"); Core._logger.warn('Core实例未创建请先调用Core.create()');
return; return;
} }
@@ -422,7 +422,7 @@ export class Core {
*/ */
public static update(deltaTime: number): void { public static update(deltaTime: number): void {
if (!this._instance) { if (!this._instance) {
Core._logger.warn("Core实例未创建请先调用Core.create()"); Core._logger.warn('Core实例未创建请先调用Core.create()');
return; return;
} }
@@ -472,7 +472,7 @@ export class Core {
*/ */
public static enableDebug(config: IECSDebugConfig): void { public static enableDebug(config: IECSDebugConfig): void {
if (!this._instance) { if (!this._instance) {
Core._logger.warn("Core实例未创建请先调用Core.create()"); Core._logger.warn('Core实例未创建请先调用Core.create()');
return; return;
} }

View File

@@ -123,7 +123,7 @@ export function Updatable(priority: number = 0): ClassDecorator {
if (!prototype || typeof prototype.update !== 'function') { if (!prototype || typeof prototype.update !== 'function') {
throw new Error( throw new Error(
`@Updatable() decorator requires class ${target.name} to implement IUpdatable interface with update() method. ` + `@Updatable() decorator requires class ${target.name} to implement IUpdatable interface with update() method. ` +
`Please add 'implements IUpdatable' and define update(deltaTime?: number): void method.` 'Please add \'implements IUpdatable\' and define update(deltaTime?: number): void method.'
); );
} }
@@ -249,7 +249,7 @@ export function createInstance<T>(
if (typeof serviceType === 'string' || typeof serviceType === 'symbol') { if (typeof serviceType === 'string' || typeof serviceType === 'symbol') {
// 字符串或Symbol类型的服务标识 // 字符串或Symbol类型的服务标识
throw new Error( throw new Error(
`String and Symbol service identifiers are not yet supported in constructor injection. ` + 'String and Symbol service identifiers are not yet supported in constructor injection. ' +
`Please use class types for ${constructor.name} parameter ${i}` `Please use class types for ${constructor.name} parameter ${i}`
); );
} else { } else {
@@ -338,7 +338,7 @@ export function registerInjectable<T extends IService>(
if (!isInjectable(serviceType)) { if (!isInjectable(serviceType)) {
throw new Error( throw new Error(
`${serviceType.name} is not marked as @Injectable(). ` + `${serviceType.name} is not marked as @Injectable(). ` +
`Please add @Injectable() decorator to the class.` 'Please add @Injectable() decorator to the class.'
); );
} }

View File

@@ -240,7 +240,7 @@ export class ServiceContainer {
// 检测循环依赖 // 检测循环依赖
if (this._resolving.has(type as ServiceType<IService>)) { if (this._resolving.has(type as ServiceType<IService>)) {
const chain = Array.from(this._resolving).map(t => t.name).join(' -> '); const chain = Array.from(this._resolving).map((t) => t.name).join(' -> ');
throw new Error(`Circular dependency detected: ${chain} -> ${type.name}`); throw new Error(`Circular dependency detected: ${chain} -> ${type.name}`);
} }
@@ -337,7 +337,7 @@ export class ServiceContainer {
// 如果有单例实例,调用 dispose // 如果有单例实例,调用 dispose
if (registration.instance) { if (registration.instance) {
// 从可更新列表中移除 // 从可更新列表中移除
const index = this._updatableServices.findIndex(item => item.instance === registration.instance); const index = this._updatableServices.findIndex((item) => item.instance === registration.instance);
if (index !== -1) { if (index !== -1) {
this._updatableServices.splice(index, 1); this._updatableServices.splice(index, 1);
} }

View File

@@ -36,7 +36,7 @@ export abstract class Component implements IComponent {
* *
* 用于为每个组件分配唯一的ID。 * 用于为每个组件分配唯一的ID。
*/ */
public static _idGenerator: number = 0; private static idGenerator: number = 0;
/** /**
* 组件唯一标识符 * 组件唯一标识符
@@ -58,7 +58,7 @@ export abstract class Component implements IComponent {
* 自动分配唯一ID给组件。 * 自动分配唯一ID给组件。
*/ */
constructor() { constructor() {
this.id = Component._idGenerator++; this.id = Component.idGenerator++;
} }
/** /**

View File

@@ -1,7 +1,7 @@
import { Entity } from '../Entity'; import { Entity } from '../Entity';
import { ComponentType, ComponentRegistry } from './ComponentStorage'; import { ComponentType, ComponentRegistry } from './ComponentStorage';
import { BitMask64Data, BitMask64Utils } from "../Utils"; import { BitMask64Data, BitMask64Utils } from '../Utils';
import { BitMaskHashMap } from "../Utils/BitMaskHashMap"; import { BitMaskHashMap } from '../Utils/BitMaskHashMap';
/** /**
* 原型标识符 * 原型标识符
@@ -247,7 +247,7 @@ export class ArchetypeSystem {
*/ */
private updateAllArchetypeArrays(): void { private updateAllArchetypeArrays(): void {
this._allArchetypes = []; this._allArchetypes = [];
for (let archetype of this._archetypes.values()) { for (const archetype of this._archetypes.values()) {
this._allArchetypes.push(archetype); this._allArchetypes.push(archetype);
} }
} }
@@ -258,7 +258,7 @@ export class ArchetypeSystem {
private getEntityComponentTypes(entity: Entity): ComponentType[] { private getEntityComponentTypes(entity: Entity): ComponentType[] {
let componentTypes = this._entityComponentTypesCache.get(entity); let componentTypes = this._entityComponentTypesCache.get(entity);
if (!componentTypes) { if (!componentTypes) {
componentTypes = entity.components.map(component => component.constructor as ComponentType); componentTypes = entity.components.map((component) => component.constructor as ComponentType);
this._entityComponentTypesCache.set(entity, componentTypes); this._entityComponentTypesCache.set(entity, componentTypes);
} }
return componentTypes; return componentTypes;
@@ -269,7 +269,7 @@ export class ArchetypeSystem {
* 使用ComponentRegistry确保与Entity.componentMask使用相同的bitIndex * 使用ComponentRegistry确保与Entity.componentMask使用相同的bitIndex
*/ */
private generateArchetypeId(componentTypes: ComponentType[]): ArchetypeId { private generateArchetypeId(componentTypes: ComponentType[]): ArchetypeId {
let mask = BitMask64Utils.clone(BitMask64Utils.ZERO); const mask = BitMask64Utils.clone(BitMask64Utils.ZERO);
for (const type of componentTypes) { for (const type of componentTypes) {
if (!ComponentRegistry.isRegistered(type)) { if (!ComponentRegistry.isRegistered(type)) {
ComponentRegistry.register(type); ComponentRegistry.register(type);

View File

@@ -9,7 +9,6 @@ import { ComponentRegistry, ComponentType } from './ComponentStorage/ComponentRe
export { ComponentRegistry, ComponentType }; export { ComponentRegistry, ComponentType };
/** /**
* 高性能组件存储器 * 高性能组件存储器
*/ */
@@ -152,7 +151,7 @@ export class ComponentStorage<T extends Component> {
usedSlots: number; usedSlots: number;
freeSlots: number; freeSlots: number;
fragmentation: number; fragmentation: number;
} { } {
const totalSlots = this.dense.length; const totalSlots = this.dense.length;
const usedSlots = this.dense.length; const usedSlots = this.dense.length;
const freeSlots = 0; // 永远无空洞 const freeSlots = 0; // 永远无空洞
@@ -342,7 +341,7 @@ export class ComponentStorageManager {
* @returns 组件位掩码 * @returns 组件位掩码
*/ */
public getComponentMask(entityId: number): BitMask64Data { public getComponentMask(entityId: number): BitMask64Data {
let mask = BitMask64Utils.clone(BitMask64Utils.ZERO); const mask = BitMask64Utils.clone(BitMask64Utils.ZERO);
for (const [componentType, storage] of this.storages.entries()) { for (const [componentType, storage] of this.storages.entries()) {
if (storage.hasComponent(entityId)) { if (storage.hasComponent(entityId)) {

View File

@@ -181,7 +181,7 @@ export class ComponentRegistry {
return this.maskCache.get(cacheKey)!; return this.maskCache.get(cacheKey)!;
} }
let mask = BitMask64Utils.clone(BitMask64Utils.ZERO); const mask = BitMask64Utils.clone(BitMask64Utils.ZERO);
for (const name of componentNames) { for (const name of componentNames) {
const componentId = this.getComponentId(name); const componentId = this.getComponentId(name);
if (componentId !== undefined) { if (componentId !== undefined) {

View File

@@ -89,10 +89,13 @@ export class EventBus implements IEventBus {
const eventConfig: EventListenerConfig = { const eventConfig: EventListenerConfig = {
once: config.once || false, once: config.once || false,
priority: config.priority || EventPriority.NORMAL, priority: config.priority || EventPriority.NORMAL,
async: config.async || false, async: config.async || false
context: config.context
}; };
if (config.thisArg) {
eventConfig.thisArg = config.thisArg as object;
}
if (this.isDebugMode) { if (this.isDebugMode) {
EventBus._logger.info(`添加监听器: ${eventType}`, eventConfig); EventBus._logger.info(`添加监听器: ${eventType}`, eventConfig);
} }
@@ -127,7 +130,7 @@ export class EventBus implements IEventBus {
handler: (data: T) => Promise<void>, handler: (data: T) => Promise<void>,
config: IEventListenerConfig = {} config: IEventListenerConfig = {}
): string { ): string {
return this.on(eventType, handler as any, { ...config, async: true }); return this.on(eventType, handler, { ...config, async: true });
} }
/** /**
@@ -466,4 +469,3 @@ export class GlobalEventBus {
} }
} }

View File

@@ -3,12 +3,12 @@ import { createLogger } from '../../Utils/Logger';
/** /**
* 事件处理器函数类型 * 事件处理器函数类型
*/ */
export type EventHandler<T = any> = (event: T) => void; export type EventHandler<T> = (event: T) => void;
/** /**
* 异步事件处理器函数类型 * 异步事件处理器函数类型
*/ */
export type AsyncEventHandler<T = any> = (event: T) => Promise<void>; export type AsyncEventHandler<T> = (event: T) => Promise<void>;
/** /**
* 事件监听器配置 * 事件监听器配置
@@ -20,15 +20,20 @@ export interface EventListenerConfig {
priority?: number; priority?: number;
/** 是否异步执行 */ /** 是否异步执行 */
async?: boolean; async?: boolean;
/** 执行上下文 */ /** 事件处理函数的 this 绑定对象 */
context?: any; thisArg?: object;
} }
/** /**
* 内部事件监听器 * 内部事件监听器
*
* 注意handler 使用 any 是必要的类型擦除
* 原因需要在同一数组中存储处理不同事件类型T的监听器
* 类型安全保证:公共 API (on<T>/emit<T>) 在编译时保证类型匹配
*/ */
interface InternalEventListener<T = any> { interface InternalEventListener {
handler: EventHandler<T> | AsyncEventHandler<T>; // eslint-disable-next-line @typescript-eslint/no-explicit-any
handler: EventHandler<any> | AsyncEventHandler<any>;
config: EventListenerConfig; config: EventListenerConfig;
id: string; id: string;
} }
@@ -71,8 +76,9 @@ export class TypeSafeEventSystem {
private static readonly _logger = createLogger('EventSystem'); private static readonly _logger = createLogger('EventSystem');
private listeners = new Map<string, InternalEventListener[]>(); private listeners = new Map<string, InternalEventListener[]>();
private stats = new Map<string, EventStats>(); private stats = new Map<string, EventStats>();
// eslint-disable-next-line @typescript-eslint/no-explicit-any
private batchQueue = new Map<string, any[]>(); private batchQueue = new Map<string, any[]>();
private batchTimers = new Map<string, number>(); private batchTimers = new Map<string, ReturnType<typeof setTimeout>>();
private batchConfigs = new Map<string, EventBatchConfig>(); private batchConfigs = new Map<string, EventBatchConfig>();
private nextListenerId = 0; private nextListenerId = 0;
private isEnabled = true; private isEnabled = true;
@@ -81,13 +87,13 @@ export class TypeSafeEventSystem {
/** /**
* 添加事件监听器 * 添加事件监听器
* @param eventType 事件类型 * @param eventType 事件类型
* @param handler 事件处理器 * @param handler 事件处理器(同步或异步,根据 config.async 决定)
* @param config 监听器配置 * @param config 监听器配置
* @returns 监听器ID用于移除 * @returns 监听器ID用于移除
*/ */
public on<T>( public on<T>(
eventType: string, eventType: string,
handler: EventHandler<T>, handler: EventHandler<T> | AsyncEventHandler<T>,
config: EventListenerConfig = {} config: EventListenerConfig = {}
): string { ): string {
return this.addListener(eventType, handler, config); return this.addListener(eventType, handler, config);
@@ -100,11 +106,7 @@ export class TypeSafeEventSystem {
* @param config 监听器配置 * @param config 监听器配置
* @returns 监听器ID * @returns 监听器ID
*/ */
public once<T>( public once<T>(eventType: string, handler: EventHandler<T>, config: EventListenerConfig = {}): string {
eventType: string,
handler: EventHandler<T>,
config: EventListenerConfig = {}
): string {
return this.addListener(eventType, handler, { ...config, once: true }); return this.addListener(eventType, handler, { ...config, once: true });
} }
@@ -115,11 +117,7 @@ export class TypeSafeEventSystem {
* @param config 监听器配置 * @param config 监听器配置
* @returns 监听器ID * @returns 监听器ID
*/ */
public onAsync<T>( public onAsync<T>(eventType: string, handler: AsyncEventHandler<T>, config: EventListenerConfig = {}): string {
eventType: string,
handler: AsyncEventHandler<T>,
config: EventListenerConfig = {}
): string {
return this.addListener(eventType, handler, { ...config, async: true }); return this.addListener(eventType, handler, { ...config, async: true });
} }
@@ -133,7 +131,7 @@ export class TypeSafeEventSystem {
const listeners = this.listeners.get(eventType); const listeners = this.listeners.get(eventType);
if (!listeners) return false; if (!listeners) return false;
const index = listeners.findIndex(l => l.id === listenerId); const index = listeners.findIndex((l) => l.id === listenerId);
if (index === -1) return false; if (index === -1) return false;
listeners.splice(index, 1); listeners.splice(index, 1);
@@ -197,8 +195,8 @@ export class TypeSafeEventSystem {
if (listener.config.async) continue; // 跳过异步监听器 if (listener.config.async) continue; // 跳过异步监听器
try { try {
if (listener.config.context) { if (listener.config.thisArg) {
(listener.handler as EventHandler<T>).call(listener.config.context, event); (listener.handler as EventHandler<T>).call(listener.config.thisArg, event);
} else { } else {
(listener.handler as EventHandler<T>)(event); (listener.handler as EventHandler<T>)(event);
} }
@@ -344,7 +342,7 @@ export class TypeSafeEventSystem {
} }
const listenerId = `listener_${this.nextListenerId++}`; const listenerId = `listener_${this.nextListenerId++}`;
const listener: InternalEventListener<T> = { const listener: InternalEventListener = {
handler, handler,
config: { config: {
priority: 0, priority: 0,
@@ -379,14 +377,14 @@ export class TypeSafeEventSystem {
const sortedListeners = this.sortListenersByPriority(listeners); const sortedListeners = this.sortListenersByPriority(listeners);
// 分离同步和异步监听器 // 分离同步和异步监听器
const syncListeners = sortedListeners.filter(l => !l.config.async); const syncListeners = sortedListeners.filter((l) => !l.config.async);
const asyncListeners = sortedListeners.filter(l => l.config.async); const asyncListeners = sortedListeners.filter((l) => l.config.async);
// 执行同步监听器 // 执行同步监听器
for (const listener of syncListeners) { for (const listener of syncListeners) {
try { try {
if (listener.config.context) { if (listener.config.thisArg) {
(listener.handler as EventHandler<T>).call(listener.config.context, event); (listener.handler as EventHandler<T>).call(listener.config.thisArg, event);
} else { } else {
(listener.handler as EventHandler<T>)(event); (listener.handler as EventHandler<T>)(event);
} }
@@ -402,8 +400,8 @@ export class TypeSafeEventSystem {
// 执行异步监听器 // 执行异步监听器
const asyncPromises = asyncListeners.map(async (listener) => { const asyncPromises = asyncListeners.map(async (listener) => {
try { try {
if (listener.config.context) { if (listener.config.thisArg) {
await (listener.handler as AsyncEventHandler<T>).call(listener.config.context, event); await (listener.handler as AsyncEventHandler<T>).call(listener.config.thisArg, event);
} else { } else {
await (listener.handler as AsyncEventHandler<T>)(event); await (listener.handler as AsyncEventHandler<T>)(event);
} }
@@ -431,7 +429,7 @@ export class TypeSafeEventSystem {
* @param listeners 监听器数组 * @param listeners 监听器数组
* @returns 排序后的监听器数组 * @returns 排序后的监听器数组
*/ */
private sortListenersByPriority<T>(listeners: InternalEventListener<T>[]): InternalEventListener<T>[] { private sortListenersByPriority(listeners: InternalEventListener[]): InternalEventListener[] {
return listeners.slice().sort((a, b) => (b.config.priority || 0) - (a.config.priority || 0)); return listeners.slice().sort((a, b) => (b.config.priority || 0) - (a.config.priority || 0));
} }
@@ -447,7 +445,7 @@ export class TypeSafeEventSystem {
if (!listeners) return; if (!listeners) return;
for (const id of listenerIds) { for (const id of listenerIds) {
const index = listeners.findIndex(l => l.id === id); const index = listeners.findIndex((l) => l.id === id);
if (index !== -1) { if (index !== -1) {
listeners.splice(index, 1); listeners.splice(index, 1);
} }
@@ -488,7 +486,7 @@ export class TypeSafeEventSystem {
this.flushBatch(eventType); this.flushBatch(eventType);
}, config.delay); }, config.delay);
this.batchTimers.set(eventType, timer as any); this.batchTimers.set(eventType, timer);
} }
} }
@@ -577,5 +575,3 @@ export class TypeSafeEventSystem {
* 全局事件系统实例 * 全局事件系统实例
*/ */
export const GlobalEventSystem = new TypeSafeEventSystem(); export const GlobalEventSystem = new TypeSafeEventSystem();

View File

@@ -164,7 +164,7 @@ export class ECSFluentAPI {
componentStats: Map<string, unknown>; componentStats: Map<string, unknown>;
queryStats: unknown; queryStats: unknown;
eventStats: Map<string, unknown>; eventStats: Map<string, unknown>;
} { } {
return { return {
entityCount: this.scene.entities.count, entityCount: this.scene.entities.count,
systemCount: this.scene.systems.length, systemCount: this.scene.systems.length,

View File

@@ -15,7 +15,7 @@ export class EntityBuilder {
this.scene = scene; this.scene = scene;
this.storageManager = storageManager; this.storageManager = storageManager;
const id = scene.identifierPool.checkOut(); const id = scene.identifierPool.checkOut();
this.entity = new Entity("", id); this.entity = new Entity('', id);
this.entity.scene = this.scene as any; this.entity.scene = this.scene as any;
} }

View File

@@ -1,3 +1,2 @@
export { QuerySystem } from '../QuerySystem'; export { QuerySystem } from '../QuerySystem';
export { ECSFluentAPI, createECSAPI } from '../FluentAPI'; export { ECSFluentAPI, createECSAPI } from '../FluentAPI';

View File

@@ -43,21 +43,21 @@ interface QueryCacheEntry {
* ``` * ```
*/ */
export class QuerySystem { export class QuerySystem {
private _logger = createLogger('QuerySystem'); private readonly _logger = createLogger('QuerySystem');
private entities: Entity[] = []; private _entities: Entity[] = [];
private entityIndex: EntityIndex; private _entityIndex: EntityIndex;
private _version = 0; private _version = 0;
private queryCache = new Map<string, QueryCacheEntry>(); private _queryCache = new Map<string, QueryCacheEntry>();
private cacheMaxSize = 1000; private _cacheMaxSize = 1000;
private cacheTimeout = 5000; private _cacheTimeout = 5000;
private componentMaskCache = new Map<string, BitMask64Data>(); private _componentMaskCache = new Map<string, BitMask64Data>();
private archetypeSystem: ArchetypeSystem; private _archetypeSystem: ArchetypeSystem;
private queryStats = { private _queryStats = {
totalQueries: 0, totalQueries: 0,
cacheHits: 0, cacheHits: 0,
indexHits: 0, indexHits: 0,
@@ -67,12 +67,12 @@ export class QuerySystem {
}; };
constructor() { constructor() {
this.entityIndex = { this._entityIndex = {
byTag: new Map(), byTag: new Map(),
byName: new Map() byName: new Map()
}; };
this.archetypeSystem = new ArchetypeSystem(); this._archetypeSystem = new ArchetypeSystem();
} }
/** /**
@@ -84,7 +84,7 @@ export class QuerySystem {
* @param entities 新的实体列表 * @param entities 新的实体列表
*/ */
public setEntities(entities: Entity[]): void { public setEntities(entities: Entity[]): void {
this.entities = entities; this._entities = entities;
this.clearQueryCache(); this.clearQueryCache();
this.clearReactiveQueries(); this.clearReactiveQueries();
this.rebuildIndexes(); this.rebuildIndexes();
@@ -100,11 +100,11 @@ export class QuerySystem {
* @param deferCacheClear 是否延迟缓存清理(用于批量操作) * @param deferCacheClear 是否延迟缓存清理(用于批量操作)
*/ */
public addEntity(entity: Entity, deferCacheClear: boolean = false): void { public addEntity(entity: Entity, deferCacheClear: boolean = false): void {
if (!this.entities.includes(entity)) { if (!this._entities.includes(entity)) {
this.entities.push(entity); this._entities.push(entity);
this.addEntityToIndexes(entity); this.addEntityToIndexes(entity);
this.archetypeSystem.addEntity(entity); this._archetypeSystem.addEntity(entity);
// 通知响应式查询 // 通知响应式查询
this.notifyReactiveQueriesEntityAdded(entity); this.notifyReactiveQueriesEntityAdded(entity);
@@ -131,16 +131,16 @@ export class QuerySystem {
if (entities.length === 0) return; if (entities.length === 0) return;
// 使用Set来快速检查重复 // 使用Set来快速检查重复
const existingIds = new Set(this.entities.map(e => e.id)); const existingIds = new Set(this._entities.map((e) => e.id));
let addedCount = 0; let addedCount = 0;
for (const entity of entities) { for (const entity of entities) {
if (!existingIds.has(entity.id)) { if (!existingIds.has(entity.id)) {
this.entities.push(entity); this._entities.push(entity);
this.addEntityToIndexes(entity); this.addEntityToIndexes(entity);
// 更新索引管理器 // 更新索引管理器
this.archetypeSystem.addEntity(entity); this._archetypeSystem.addEntity(entity);
existingIds.add(entity.id); existingIds.add(entity.id);
addedCount++; addedCount++;
@@ -166,7 +166,7 @@ export class QuerySystem {
// 避免调用栈溢出,分批添加 // 避免调用栈溢出,分批添加
for (const entity of entities) { for (const entity of entities) {
this.entities.push(entity); this._entities.push(entity);
} }
// 批量更新索引 // 批量更新索引
@@ -174,7 +174,7 @@ export class QuerySystem {
this.addEntityToIndexes(entity); this.addEntityToIndexes(entity);
// 更新索引管理器 // 更新索引管理器
this.archetypeSystem.addEntity(entity); this._archetypeSystem.addEntity(entity);
} }
// 清理缓存 // 清理缓存
@@ -189,17 +189,17 @@ export class QuerySystem {
* @param entity 要移除的实体 * @param entity 要移除的实体
*/ */
public removeEntity(entity: Entity): void { public removeEntity(entity: Entity): void {
const index = this.entities.indexOf(entity); const index = this._entities.indexOf(entity);
if (index !== -1) { if (index !== -1) {
const componentTypes: ComponentType[] = []; const componentTypes: ComponentType[] = [];
for (const component of entity.components) { for (const component of entity.components) {
componentTypes.push(component.constructor as ComponentType); componentTypes.push(component.constructor as ComponentType);
} }
this.entities.splice(index, 1); this._entities.splice(index, 1);
this.removeEntityFromIndexes(entity); this.removeEntityFromIndexes(entity);
this.archetypeSystem.removeEntity(entity); this._archetypeSystem.removeEntity(entity);
if (componentTypes.length > 0) { if (componentTypes.length > 0) {
this.notifyReactiveQueriesEntityRemoved(entity, componentTypes); this.notifyReactiveQueriesEntityRemoved(entity, componentTypes);
@@ -222,7 +222,7 @@ export class QuerySystem {
*/ */
public updateEntity(entity: Entity): void { public updateEntity(entity: Entity): void {
// 检查实体是否在查询系统中 // 检查实体是否在查询系统中
if (!this.entities.includes(entity)) { if (!this._entities.includes(entity)) {
// 如果实体不在系统中,直接添加 // 如果实体不在系统中,直接添加
this.addEntity(entity); this.addEntity(entity);
return; return;
@@ -232,7 +232,7 @@ export class QuerySystem {
this.removeEntityFromIndexes(entity); this.removeEntityFromIndexes(entity);
// 更新ArchetypeSystem中的实体状态 // 更新ArchetypeSystem中的实体状态
this.archetypeSystem.updateEntity(entity); this._archetypeSystem.updateEntity(entity);
// 重新添加实体到索引(基于新的组件状态) // 重新添加实体到索引(基于新的组件状态)
this.addEntityToIndexes(entity); this.addEntityToIndexes(entity);
@@ -253,28 +253,27 @@ export class QuerySystem {
// 标签索引 // 标签索引
const tag = entity.tag; const tag = entity.tag;
if (tag !== undefined) { if (tag !== undefined) {
const tagSet = this.entityIndex.byTag.get(tag) || this.createAndSetTagIndex(tag); const tagSet = this._entityIndex.byTag.get(tag) || this.createAndSetTagIndex(tag);
tagSet.add(entity); tagSet.add(entity);
} }
// 名称索引 // 名称索引
const name = entity.name; const name = entity.name;
if (name) { if (name) {
const nameSet = this.entityIndex.byName.get(name) || this.createAndSetNameIndex(name); const nameSet = this._entityIndex.byName.get(name) || this.createAndSetNameIndex(name);
nameSet.add(entity); nameSet.add(entity);
} }
} }
private createAndSetTagIndex(tag: number): Set<Entity> { private createAndSetTagIndex(tag: number): Set<Entity> {
const set = new Set<Entity>(); const set = new Set<Entity>();
this.entityIndex.byTag.set(tag, set); this._entityIndex.byTag.set(tag, set);
return set; return set;
} }
private createAndSetNameIndex(name: string): Set<Entity> { private createAndSetNameIndex(name: string): Set<Entity> {
const set = new Set<Entity>(); const set = new Set<Entity>();
this.entityIndex.byName.set(name, set); this._entityIndex.byName.set(name, set);
return set; return set;
} }
@@ -284,22 +283,22 @@ export class QuerySystem {
private removeEntityFromIndexes(entity: Entity): void { private removeEntityFromIndexes(entity: Entity): void {
// 从标签索引移除 // 从标签索引移除
if (entity.tag !== undefined) { if (entity.tag !== undefined) {
const tagSet = this.entityIndex.byTag.get(entity.tag); const tagSet = this._entityIndex.byTag.get(entity.tag);
if (tagSet) { if (tagSet) {
tagSet.delete(entity); tagSet.delete(entity);
if (tagSet.size === 0) { if (tagSet.size === 0) {
this.entityIndex.byTag.delete(entity.tag); this._entityIndex.byTag.delete(entity.tag);
} }
} }
} }
// 从名称索引移除 // 从名称索引移除
if (entity.name) { if (entity.name) {
const nameSet = this.entityIndex.byName.get(entity.name); const nameSet = this._entityIndex.byName.get(entity.name);
if (nameSet) { if (nameSet) {
nameSet.delete(entity); nameSet.delete(entity);
if (nameSet.size === 0) { if (nameSet.size === 0) {
this.entityIndex.byName.delete(entity.name); this._entityIndex.byName.delete(entity.name);
} }
} }
} }
@@ -312,15 +311,15 @@ export class QuerySystem {
* 通常在大量实体变更后调用以确保索引一致性。 * 通常在大量实体变更后调用以确保索引一致性。
*/ */
private rebuildIndexes(): void { private rebuildIndexes(): void {
this.entityIndex.byTag.clear(); this._entityIndex.byTag.clear();
this.entityIndex.byName.clear(); this._entityIndex.byName.clear();
// 清理ArchetypeSystem和ComponentIndexManager // 清理ArchetypeSystem和ComponentIndexManager
this.archetypeSystem.clear(); this._archetypeSystem.clear();
for (const entity of this.entities) { for (const entity of this._entities) {
this.addEntityToIndexes(entity); this.addEntityToIndexes(entity);
this.archetypeSystem.addEntity(entity); this._archetypeSystem.addEntity(entity);
} }
} }
@@ -342,7 +341,7 @@ export class QuerySystem {
*/ */
public queryAll(...componentTypes: ComponentType[]): QueryResult { public queryAll(...componentTypes: ComponentType[]): QueryResult {
const startTime = performance.now(); const startTime = performance.now();
this.queryStats.totalQueries++; this._queryStats.totalQueries++;
// 使用内部响应式查询作为智能缓存 // 使用内部响应式查询作为智能缓存
const reactiveQuery = this.getOrCreateReactiveQuery(QueryConditionType.ALL, componentTypes); const reactiveQuery = this.getOrCreateReactiveQuery(QueryConditionType.ALL, componentTypes);
@@ -351,7 +350,7 @@ export class QuerySystem {
const entities = reactiveQuery.getEntities(); const entities = reactiveQuery.getEntities();
// 统计为缓存命中(响应式查询本质上是永不过期的智能缓存) // 统计为缓存命中(响应式查询本质上是永不过期的智能缓存)
this.queryStats.cacheHits++; this._queryStats.cacheHits++;
return { return {
entities, entities,
@@ -379,7 +378,7 @@ export class QuerySystem {
*/ */
public queryAny(...componentTypes: ComponentType[]): QueryResult { public queryAny(...componentTypes: ComponentType[]): QueryResult {
const startTime = performance.now(); const startTime = performance.now();
this.queryStats.totalQueries++; this._queryStats.totalQueries++;
// 使用内部响应式查询作为智能缓存 // 使用内部响应式查询作为智能缓存
const reactiveQuery = this.getOrCreateReactiveQuery(QueryConditionType.ANY, componentTypes); const reactiveQuery = this.getOrCreateReactiveQuery(QueryConditionType.ANY, componentTypes);
@@ -388,7 +387,7 @@ export class QuerySystem {
const entities = reactiveQuery.getEntities(); const entities = reactiveQuery.getEntities();
// 统计为缓存命中(响应式查询本质上是永不过期的智能缓存) // 统计为缓存命中(响应式查询本质上是永不过期的智能缓存)
this.queryStats.cacheHits++; this._queryStats.cacheHits++;
return { return {
entities, entities,
@@ -416,7 +415,7 @@ export class QuerySystem {
*/ */
public queryNone(...componentTypes: ComponentType[]): QueryResult { public queryNone(...componentTypes: ComponentType[]): QueryResult {
const startTime = performance.now(); const startTime = performance.now();
this.queryStats.totalQueries++; this._queryStats.totalQueries++;
// 使用内部响应式查询作为智能缓存 // 使用内部响应式查询作为智能缓存
const reactiveQuery = this.getOrCreateReactiveQuery(QueryConditionType.NONE, componentTypes); const reactiveQuery = this.getOrCreateReactiveQuery(QueryConditionType.NONE, componentTypes);
@@ -425,7 +424,7 @@ export class QuerySystem {
const entities = reactiveQuery.getEntities(); const entities = reactiveQuery.getEntities();
// 统计为缓存命中(响应式查询本质上是永不过期的智能缓存) // 统计为缓存命中(响应式查询本质上是永不过期的智能缓存)
this.queryStats.cacheHits++; this._queryStats.cacheHits++;
return { return {
entities, entities,
@@ -452,14 +451,14 @@ export class QuerySystem {
*/ */
public queryByTag(tag: number): QueryResult { public queryByTag(tag: number): QueryResult {
const startTime = performance.now(); const startTime = performance.now();
this.queryStats.totalQueries++; this._queryStats.totalQueries++;
const cacheKey = `tag:${tag}`; const cacheKey = `tag:${tag}`;
// 检查缓存 // 检查缓存
const cached = this.getFromCache(cacheKey); const cached = this.getFromCache(cacheKey);
if (cached) { if (cached) {
this.queryStats.cacheHits++; this._queryStats.cacheHits++;
return { return {
entities: cached, entities: cached,
count: cached.length, count: cached.length,
@@ -469,8 +468,8 @@ export class QuerySystem {
} }
// 使用索引查询 // 使用索引查询
this.queryStats.indexHits++; this._queryStats.indexHits++;
const entities = Array.from(this.entityIndex.byTag.get(tag) || []); const entities = Array.from(this._entityIndex.byTag.get(tag) || []);
// 缓存结果 // 缓存结果
this.addToCache(cacheKey, entities); this.addToCache(cacheKey, entities);
@@ -500,14 +499,14 @@ export class QuerySystem {
*/ */
public queryByName(name: string): QueryResult { public queryByName(name: string): QueryResult {
const startTime = performance.now(); const startTime = performance.now();
this.queryStats.totalQueries++; this._queryStats.totalQueries++;
const cacheKey = `name:${name}`; const cacheKey = `name:${name}`;
// 检查缓存 // 检查缓存
const cached = this.getFromCache(cacheKey); const cached = this.getFromCache(cacheKey);
if (cached) { if (cached) {
this.queryStats.cacheHits++; this._queryStats.cacheHits++;
return { return {
entities: cached, entities: cached,
count: cached.length, count: cached.length,
@@ -517,8 +516,8 @@ export class QuerySystem {
} }
// 使用索引查询 // 使用索引查询
this.queryStats.indexHits++; this._queryStats.indexHits++;
const entities = Array.from(this.entityIndex.byName.get(name) || []); const entities = Array.from(this._entityIndex.byName.get(name) || []);
// 缓存结果 // 缓存结果
this.addToCache(cacheKey, entities); this.addToCache(cacheKey, entities);
@@ -548,14 +547,14 @@ export class QuerySystem {
*/ */
public queryByComponent<T extends Component>(componentType: ComponentType<T>): QueryResult { public queryByComponent<T extends Component>(componentType: ComponentType<T>): QueryResult {
const startTime = performance.now(); const startTime = performance.now();
this.queryStats.totalQueries++; this._queryStats.totalQueries++;
const cacheKey = this.generateCacheKey('component', [componentType]); const cacheKey = this.generateCacheKey('component', [componentType]);
// 检查缓存 // 检查缓存
const cached = this.getFromCache(cacheKey); const cached = this.getFromCache(cacheKey);
if (cached) { if (cached) {
this.queryStats.cacheHits++; this._queryStats.cacheHits++;
return { return {
entities: cached, entities: cached,
count: cached.length, count: cached.length,
@@ -564,8 +563,8 @@ export class QuerySystem {
}; };
} }
this.queryStats.indexHits++; this._queryStats.indexHits++;
const entities = this.archetypeSystem.getEntitiesByComponent(componentType); const entities = this._archetypeSystem.getEntitiesByComponent(componentType);
// 缓存结果 // 缓存结果
this.addToCache(cacheKey, entities); this.addToCache(cacheKey, entities);
@@ -582,12 +581,12 @@ export class QuerySystem {
* 从缓存获取查询结果 * 从缓存获取查询结果
*/ */
private getFromCache(cacheKey: string): readonly Entity[] | null { private getFromCache(cacheKey: string): readonly Entity[] | null {
const entry = this.queryCache.get(cacheKey); const entry = this._queryCache.get(cacheKey);
if (!entry) return null; if (!entry) return null;
// 检查缓存是否过期或版本过期 // 检查缓存是否过期或版本过期
if (Date.now() - entry.timestamp > this.cacheTimeout || entry.version !== this._version) { if (Date.now() - entry.timestamp > this._cacheTimeout || entry.version !== this._version) {
this.queryCache.delete(cacheKey); this._queryCache.delete(cacheKey);
return null; return null;
} }
@@ -600,11 +599,11 @@ export class QuerySystem {
*/ */
private addToCache(cacheKey: string, entities: Entity[]): void { private addToCache(cacheKey: string, entities: Entity[]): void {
// 如果缓存已满,清理最少使用的条目 // 如果缓存已满,清理最少使用的条目
if (this.queryCache.size >= this.cacheMaxSize) { if (this._queryCache.size >= this._cacheMaxSize) {
this.cleanupCache(); this.cleanupCache();
} }
this.queryCache.set(cacheKey, { this._queryCache.set(cacheKey, {
entities: entities, // 直接使用引用,通过版本号控制失效 entities: entities, // 直接使用引用,通过版本号控制失效
timestamp: Date.now(), timestamp: Date.now(),
hitCount: 0, hitCount: 0,
@@ -618,22 +617,24 @@ export class QuerySystem {
private cleanupCache(): void { private cleanupCache(): void {
// 移除过期的缓存条目 // 移除过期的缓存条目
const now = Date.now(); const now = Date.now();
for (const [key, entry] of this.queryCache.entries()) { for (const [key, entry] of this._queryCache.entries()) {
if (now - entry.timestamp > this.cacheTimeout) { if (now - entry.timestamp > this._cacheTimeout) {
this.queryCache.delete(key); this._queryCache.delete(key);
} }
} }
// 如果还是太满,移除最少使用的条目 // 如果还是太满,移除最少使用的条目
if (this.queryCache.size >= this.cacheMaxSize) { if (this._queryCache.size >= this._cacheMaxSize) {
let minHitCount = Infinity; let minHitCount = Infinity;
let oldestKey = ''; let oldestKey = '';
let oldestTimestamp = Infinity; let oldestTimestamp = Infinity;
// 单次遍历找到最少使用或最旧的条目 // 单次遍历找到最少使用或最旧的条目
for (const [key, entry] of this.queryCache.entries()) { for (const [key, entry] of this._queryCache.entries()) {
if (entry.hitCount < minHitCount || if (
(entry.hitCount === minHitCount && entry.timestamp < oldestTimestamp)) { entry.hitCount < minHitCount ||
(entry.hitCount === minHitCount && entry.timestamp < oldestTimestamp)
) {
minHitCount = entry.hitCount; minHitCount = entry.hitCount;
oldestKey = key; oldestKey = key;
oldestTimestamp = entry.timestamp; oldestTimestamp = entry.timestamp;
@@ -641,7 +642,7 @@ export class QuerySystem {
} }
if (oldestKey) { if (oldestKey) {
this.queryCache.delete(oldestKey); this._queryCache.delete(oldestKey);
} }
} }
} }
@@ -650,8 +651,8 @@ export class QuerySystem {
* 清除所有查询缓存 * 清除所有查询缓存
*/ */
private clearQueryCache(): void { private clearQueryCache(): void {
this.queryCache.clear(); this._queryCache.clear();
this.componentMaskCache.clear(); this._componentMaskCache.clear();
} }
/** /**
@@ -679,10 +680,13 @@ export class QuerySystem {
} }
// 多组件查询:使用排序后的类型名称创建键 // 多组件查询:使用排序后的类型名称创建键
const sortKey = componentTypes.map(t => { const sortKey = componentTypes
const name = getComponentTypeName(t); .map((t) => {
return name; const name = getComponentTypeName(t);
}).sort().join(','); return name;
})
.sort()
.join(',');
const fullKey = `${prefix}:${sortKey}`; const fullKey = `${prefix}:${sortKey}`;
@@ -724,10 +728,7 @@ export class QuerySystem {
* }); * });
* ``` * ```
*/ */
public createReactiveQuery( public createReactiveQuery(componentTypes: ComponentType[], config?: ReactiveQueryConfig): ReactiveQuery {
componentTypes: ComponentType[],
config?: ReactiveQueryConfig
): ReactiveQuery {
if (!componentTypes || componentTypes.length === 0) { if (!componentTypes || componentTypes.length === 0) {
throw new Error('组件类型列表不能为空'); throw new Error('组件类型列表不能为空');
} }
@@ -741,10 +742,7 @@ export class QuerySystem {
const query = new ReactiveQuery(condition, config); const query = new ReactiveQuery(condition, config);
const initialEntities = this.executeTraditionalQuery( const initialEntities = this.executeTraditionalQuery(QueryConditionType.ALL, componentTypes);
QueryConditionType.ALL,
componentTypes
);
query.initializeWith(initialEntities); query.initializeWith(initialEntities);
const cacheKey = this.generateCacheKey('all', componentTypes); const cacheKey = this.generateCacheKey('all', componentTypes);
@@ -810,18 +808,21 @@ export class QuerySystem {
*/ */
private createComponentMask(componentTypes: ComponentType[]): BitMask64Data { private createComponentMask(componentTypes: ComponentType[]): BitMask64Data {
// 生成缓存键 // 生成缓存键
const cacheKey = componentTypes.map(t => { const cacheKey = componentTypes
return getComponentTypeName(t); .map((t) => {
}).sort().join(','); return getComponentTypeName(t);
})
.sort()
.join(',');
// 检查缓存 // 检查缓存
const cached = this.componentMaskCache.get(cacheKey); const cached = this._componentMaskCache.get(cacheKey);
if (cached) { if (cached) {
return cached; return cached;
} }
// 使用ComponentRegistry而不是ComponentTypeManager,确保bitIndex一致 // 使用ComponentRegistry而不是ComponentTypeManager,确保bitIndex一致
let mask = BitMask64Utils.clone(BitMask64Utils.ZERO); const mask = BitMask64Utils.clone(BitMask64Utils.ZERO);
for (const type of componentTypes) { for (const type of componentTypes) {
// 确保组件已注册 // 确保组件已注册
if (!ComponentRegistry.isRegistered(type)) { if (!ComponentRegistry.isRegistered(type)) {
@@ -832,7 +833,7 @@ export class QuerySystem {
} }
// 缓存结果 // 缓存结果
this.componentMaskCache.set(cacheKey, mask); this._componentMaskCache.set(cacheKey, mask);
return mask; return mask;
} }
@@ -847,7 +848,7 @@ export class QuerySystem {
* 获取所有实体 * 获取所有实体
*/ */
public getAllEntities(): readonly Entity[] { public getAllEntities(): readonly Entity[] {
return this.entities; return this._entities;
} }
/** /**
@@ -881,30 +882,34 @@ export class QuerySystem {
size: number; size: number;
hitRate: string; hitRate: string;
}; };
} { } {
return { return {
entityCount: this.entities.length, entityCount: this._entities.length,
indexStats: { indexStats: {
componentIndexSize: this.archetypeSystem.getAllArchetypes().length, componentIndexSize: this._archetypeSystem.getAllArchetypes().length,
tagIndexSize: this.entityIndex.byTag.size, tagIndexSize: this._entityIndex.byTag.size,
nameIndexSize: this.entityIndex.byName.size nameIndexSize: this._entityIndex.byName.size
}, },
queryStats: { queryStats: {
...this.queryStats, ...this._queryStats,
cacheHitRate: this.queryStats.totalQueries > 0 ? cacheHitRate:
(this.queryStats.cacheHits / this.queryStats.totalQueries * 100).toFixed(2) + '%' : '0%' this._queryStats.totalQueries > 0
? ((this._queryStats.cacheHits / this._queryStats.totalQueries) * 100).toFixed(2) + '%'
: '0%'
}, },
optimizationStats: { optimizationStats: {
archetypeSystem: this.archetypeSystem.getAllArchetypes().map(a => ({ archetypeSystem: this._archetypeSystem.getAllArchetypes().map((a) => ({
id: a.id, id: a.id,
componentTypes: a.componentTypes.map(t => getComponentTypeName(t)), componentTypes: a.componentTypes.map((t) => getComponentTypeName(t)),
entityCount: a.entities.size entityCount: a.entities.size
})) }))
}, },
cacheStats: { cacheStats: {
size: this._reactiveQueries.size, size: this._reactiveQueries.size,
hitRate: this.queryStats.totalQueries > 0 ? hitRate:
(this.queryStats.cacheHits / this.queryStats.totalQueries * 100).toFixed(2) + '%' : '0%' this._queryStats.totalQueries > 0
? ((this._queryStats.cacheHits / this._queryStats.totalQueries) * 100).toFixed(2) + '%'
: '0%'
} }
}; };
} }
@@ -915,7 +920,7 @@ export class QuerySystem {
* @param entity 要查询的实体 * @param entity 要查询的实体
*/ */
public getEntityArchetype(entity: Entity): Archetype | undefined { public getEntityArchetype(entity: Entity): Archetype | undefined {
return this.archetypeSystem.getEntityArchetype(entity); return this._archetypeSystem.getEntityArchetype(entity);
} }
// ============================================================ // ============================================================
@@ -941,10 +946,7 @@ export class QuerySystem {
* @param componentTypes 组件类型列表 * @param componentTypes 组件类型列表
* @returns 响应式查询实例 * @returns 响应式查询实例
*/ */
private getOrCreateReactiveQuery( private getOrCreateReactiveQuery(queryType: QueryConditionType, componentTypes: ComponentType[]): ReactiveQuery {
queryType: QueryConditionType,
componentTypes: ComponentType[]
): ReactiveQuery {
// 生成缓存键(与传统缓存键格式一致) // 生成缓存键(与传统缓存键格式一致)
const cacheKey = this.generateCacheKey(queryType, componentTypes); const cacheKey = this.generateCacheKey(queryType, componentTypes);
@@ -996,13 +998,10 @@ export class QuerySystem {
* @param componentTypes 组件类型列表 * @param componentTypes 组件类型列表
* @returns 匹配的实体列表 * @returns 匹配的实体列表
*/ */
private executeTraditionalQuery( private executeTraditionalQuery(queryType: QueryConditionType, componentTypes: ComponentType[]): Entity[] {
queryType: QueryConditionType,
componentTypes: ComponentType[]
): Entity[] {
switch (queryType) { switch (queryType) {
case QueryConditionType.ALL: { case QueryConditionType.ALL: {
const archetypeResult = this.archetypeSystem.queryArchetypes(componentTypes, 'AND'); const archetypeResult = this._archetypeSystem.queryArchetypes(componentTypes, 'AND');
const entities: Entity[] = []; const entities: Entity[] = [];
for (const archetype of archetypeResult.archetypes) { for (const archetype of archetypeResult.archetypes) {
for (const entity of archetype.entities) { for (const entity of archetype.entities) {
@@ -1012,7 +1011,7 @@ export class QuerySystem {
return entities; return entities;
} }
case QueryConditionType.ANY: { case QueryConditionType.ANY: {
const archetypeResult = this.archetypeSystem.queryArchetypes(componentTypes, 'OR'); const archetypeResult = this._archetypeSystem.queryArchetypes(componentTypes, 'OR');
const entities: Entity[] = []; const entities: Entity[] = [];
for (const archetype of archetypeResult.archetypes) { for (const archetype of archetypeResult.archetypes) {
for (const entity of archetype.entities) { for (const entity of archetype.entities) {
@@ -1023,9 +1022,7 @@ export class QuerySystem {
} }
case QueryConditionType.NONE: { case QueryConditionType.NONE: {
const mask = this.createComponentMask(componentTypes); const mask = this.createComponentMask(componentTypes);
return this.entities.filter(entity => return this._entities.filter((entity) => BitMask64Utils.hasNone(entity.componentMask, mask));
BitMask64Utils.hasNone(entity.componentMask, mask)
);
} }
default: default:
return []; return [];
@@ -1241,7 +1238,7 @@ export class QueryBuilder {
* 创建组件掩码 * 创建组件掩码
*/ */
private createComponentMask(componentTypes: ComponentType[]): BitMask64Data { private createComponentMask(componentTypes: ComponentType[]): BitMask64Data {
let mask = BitMask64Utils.clone(BitMask64Utils.ZERO); const mask = BitMask64Utils.clone(BitMask64Utils.ZERO);
for (const type of componentTypes) { for (const type of componentTypes) {
try { try {
const bitMask = ComponentRegistry.getBitMask(type); const bitMask = ComponentRegistry.getBitMask(type);

View File

@@ -136,7 +136,7 @@ export class ReactiveQuery {
private generateQueryId(): string { private generateQueryId(): string {
const typeStr = this._condition.type; const typeStr = this._condition.type;
const componentsStr = this._condition.componentTypes const componentsStr = this._condition.componentTypes
.map(t => t.name) .map((t) => t.name)
.sort() .sort()
.join(','); .join(',');
return `${typeStr}:${componentsStr}`; return `${typeStr}:${componentsStr}`;

View File

@@ -578,7 +578,7 @@ export class SoAStorage<T extends Component> {
} }
if (obj instanceof Array) { if (obj instanceof Array) {
return obj.map(item => this.deepClone(item)); return obj.map((item) => this.deepClone(item));
} }
if (obj instanceof Map) { if (obj instanceof Map) {

View File

@@ -1,6 +1,6 @@
import type {Component} from '../Component'; import type { Component } from '../Component';
import type {EntitySystem} from '../Systems'; import type { EntitySystem } from '../Systems';
import {ComponentType} from "../../Types"; import { ComponentType } from '../../Types';
/** /**
* 存储组件类型名称的Symbol键 * 存储组件类型名称的Symbol键

View File

@@ -1,11 +1,17 @@
import { Component } from './Component'; import { Component } from './Component';
import { ComponentRegistry, ComponentType } from './Core/ComponentStorage'; import { ComponentRegistry, ComponentType } from './Core/ComponentStorage';
import { EventBus } from './Core/EventBus';
import { BitMask64Utils, BitMask64Data } from './Utils/BigIntCompatibility'; import { BitMask64Utils, BitMask64Data } from './Utils/BigIntCompatibility';
import { createLogger } from '../Utils/Logger'; import { createLogger } from '../Utils/Logger';
import { getComponentInstanceTypeName, getComponentTypeName } from './Decorators'; import { getComponentInstanceTypeName, getComponentTypeName } from './Decorators';
import type { IScene } from './IScene'; import type { IScene } from './IScene';
/**
* 组件活跃状态变化接口
*/
interface IActiveChangeable {
onActiveChanged(): void;
}
/** /**
* 实体比较器 * 实体比较器
* *
@@ -21,14 +27,11 @@ export class EntityComparer {
*/ */
public compare(self: Entity, other: Entity): number { public compare(self: Entity, other: Entity): number {
let compare = self.updateOrder - other.updateOrder; let compare = self.updateOrder - other.updateOrder;
if (compare == 0) if (compare == 0) compare = self.id - other.id;
compare = self.id - other.id;
return compare; return compare;
} }
} }
/** /**
* 游戏实体类 * 游戏实体类
* *
@@ -66,25 +69,6 @@ export class Entity {
*/ */
public static entityComparer: EntityComparer = new EntityComparer(); public static entityComparer: EntityComparer = new EntityComparer();
/**
* 全局事件总线实例
* 用于发射组件相关事件
*/
public static eventBus: EventBus | null = null;
/**
* 通知Scene中的QuerySystem实体组件发生变动
*
* @param entity 发生组件变动的实体
*/
private static notifyQuerySystems(entity: Entity): void {
// 只通知Scene中的QuerySystem
if (entity.scene && entity.scene.querySystem) {
entity.scene.querySystem.updateEntity(entity);
entity.scene.clearSystemEntityCaches();
}
}
/** /**
* 实体名称 * 实体名称
*/ */
@@ -103,7 +87,7 @@ export class Entity {
/** /**
* 销毁状态标志 * 销毁状态标志
*/ */
public _isDestroyed: boolean = false; private _isDestroyed: boolean = false;
/** /**
* 父实体引用 * 父实体引用
@@ -164,6 +148,18 @@ export class Entity {
return this._isDestroyed; return this._isDestroyed;
} }
/**
* 设置销毁状态(内部使用)
*
* 此方法供Scene和批量操作使用以提高性能。
* 不应在普通业务逻辑中调用应使用destroy()方法。
*
* @internal
*/
public setDestroyedState(destroyed: boolean): void {
this._isDestroyed = destroyed;
}
/** /**
* 获取组件数组(懒加载) * 获取组件数组(懒加载)
* @returns 只读的组件数组 * @returns 只读的组件数组
@@ -193,10 +189,7 @@ export class Entity {
if (BitMask64Utils.getBit(mask, bitIndex)) { if (BitMask64Utils.getBit(mask, bitIndex)) {
const componentType = ComponentRegistry.getTypeByBitIndex(bitIndex); const componentType = ComponentRegistry.getTypeByBitIndex(bitIndex);
if (componentType) { if (componentType) {
const component = this.scene.componentStorageManager.getComponent( const component = this.scene.componentStorageManager.getComponent(this.id, componentType);
this.id,
componentType
);
if (component) { if (component) {
components.push(component); components.push(component);
@@ -348,8 +341,9 @@ export class Entity {
*/ */
public createComponent<T extends Component>( public createComponent<T extends Component>(
componentType: ComponentType<T>, componentType: ComponentType<T>,
...args: any[] ...args: ConstructorParameters<ComponentType<T>>
): T { ): T {
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
const component = new componentType(...args); const component = new componentType(...args);
return this.addComponent(component); return this.addComponent(component);
} }
@@ -377,6 +371,16 @@ export class Entity {
return component; return component;
} }
/**
* 通知Scene中的QuerySystem实体组件发生变动
*/
private notifyQuerySystems(): void {
if (this.scene && this.scene.querySystem) {
this.scene.querySystem.updateEntity(this);
this.scene.clearSystemEntityCaches();
}
}
/** /**
* 添加组件到实体 * 添加组件到实体
* *
@@ -394,11 +398,13 @@ export class Entity {
const componentType = component.constructor as ComponentType<T>; const componentType = component.constructor as ComponentType<T>;
if (!this.scene) { if (!this.scene) {
throw new Error(`Entity must be added to Scene before adding components. Use scene.createEntity() instead of new Entity()`); throw new Error(
'Entity must be added to Scene before adding components. Use scene.createEntity() instead of new Entity()'
);
} }
if (!this.scene.componentStorageManager) { if (!this.scene.componentStorageManager) {
throw new Error(`Scene does not have componentStorageManager`); throw new Error('Scene does not have componentStorageManager');
} }
if (this.hasComponent(componentType)) { if (this.hasComponent(componentType)) {
@@ -415,8 +421,8 @@ export class Entity {
} }
component.onAddedToEntity(); component.onAddedToEntity();
if (Entity.eventBus) { if (this.scene && this.scene.eventSystem) {
Entity.eventBus.emitComponentAdded({ this.scene.eventSystem.emitSync('component:added', {
timestamp: Date.now(), timestamp: Date.now(),
source: 'Entity', source: 'Entity',
entityId: this.id, entityId: this.id,
@@ -427,9 +433,7 @@ export class Entity {
}); });
} }
this.notifyQuerySystems();
// 通知所有相关的QuerySystem组件已变动
Entity.notifyQuerySystems(this);
return component; return component;
} }
@@ -464,9 +468,6 @@ export class Entity {
return component as T | null; return component as T | null;
} }
/** /**
* 检查实体是否拥有指定类型的组件 * 检查实体是否拥有指定类型的组件
* *
@@ -508,10 +509,11 @@ export class Entity {
*/ */
public getOrCreateComponent<T extends Component>( public getOrCreateComponent<T extends Component>(
type: ComponentType<T>, type: ComponentType<T>,
...args: any[] ...args: ConstructorParameters<ComponentType<T>>
): T { ): T {
let component = this.getComponent(type); let component = this.getComponent(type);
if (!component) { if (!component) {
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
component = this.createComponent(type, ...args); component = this.createComponent(type, ...args);
} }
return component; return component;
@@ -552,8 +554,8 @@ export class Entity {
component.entityId = null; component.entityId = null;
if (Entity.eventBus) { if (this.scene && this.scene.eventSystem) {
Entity.eventBus.emitComponentRemoved({ this.scene.eventSystem.emitSync('component:removed', {
timestamp: Date.now(), timestamp: Date.now(),
source: 'Entity', source: 'Entity',
entityId: this.id, entityId: this.id,
@@ -564,8 +566,7 @@ export class Entity {
}); });
} }
// 通知所有相关的QuerySystem组件已变动 this.notifyQuerySystems();
Entity.notifyQuerySystems(this);
} }
/** /**
@@ -605,8 +606,7 @@ export class Entity {
component.onRemovedFromEntity(); component.onRemovedFromEntity();
} }
// 通知所有相关的QuerySystem组件已全部移除 this.notifyQuerySystems();
Entity.notifyQuerySystems(this);
} }
/** /**
@@ -645,8 +645,6 @@ export class Entity {
return removedComponents; return removedComponents;
} }
/** /**
* 获取所有指定类型的组件 * 获取所有指定类型的组件
* *
@@ -700,7 +698,7 @@ export class Entity {
*/ */
public addChild(child: Entity): Entity { public addChild(child: Entity): Entity {
if (child === this) { if (child === this) {
throw new Error("Entity cannot be its own child"); throw new Error('Entity cannot be its own child');
} }
if (child._parent === this) { if (child._parent === this) {
@@ -808,11 +806,10 @@ export class Entity {
* @returns 层次结构的根实体 * @returns 层次结构的根实体
*/ */
public getRoot(): Entity { public getRoot(): Entity {
let root: Entity = this; if (!this._parent) {
while (root._parent) { return this;
root = root._parent;
} }
return root; return this._parent.getRoot();
} }
/** /**
@@ -878,7 +875,7 @@ export class Entity {
private onActiveChanged(): void { private onActiveChanged(): void {
for (const component of this.components) { for (const component of this.components) {
if ('onActiveChanged' in component && typeof component.onActiveChanged === 'function') { if ('onActiveChanged' in component && typeof component.onActiveChanged === 'function') {
(component as any).onActiveChanged(); (component as IActiveChangeable).onActiveChanged();
} }
} }
@@ -891,7 +888,6 @@ export class Entity {
} }
} }
/** /**
* 销毁实体 * 销毁实体
* *
@@ -949,7 +945,7 @@ export class Entity {
collectChildren(this); collectChildren(this);
for (const entity of toDestroy) { for (const entity of toDestroy) {
entity._isDestroyed = true; entity.setDestroyedState(true);
} }
for (const entity of toDestroy) { for (const entity of toDestroy) {
@@ -1007,7 +1003,7 @@ export class Entity {
childIds: number[]; childIds: number[];
depth: number; depth: number;
cacheBuilt: boolean; cacheBuilt: boolean;
} { } {
return { return {
name: this.name, name: this.name,
id: this.id, id: this.id,
@@ -1016,11 +1012,11 @@ export class Entity {
activeInHierarchy: this.activeInHierarchy, activeInHierarchy: this.activeInHierarchy,
destroyed: this._isDestroyed, destroyed: this._isDestroyed,
componentCount: this.components.length, componentCount: this.components.length,
componentTypes: this.components.map(c => getComponentInstanceTypeName(c)), componentTypes: this.components.map((c) => getComponentInstanceTypeName(c)),
componentMask: BitMask64Utils.toString(this._componentMask, 2), // 二进制表示 componentMask: BitMask64Utils.toString(this._componentMask, 2), // 二进制表示
parentId: this._parent?.id || null, parentId: this._parent?.id || null,
childCount: this._children.length, childCount: this._children.length,
childIds: this._children.map(c => c.id), childIds: this._children.map((c) => c.id),
depth: this.getDepth(), depth: this.getDepth(),
cacheBuilt: this._componentCache !== null cacheBuilt: this._componentCache !== null
}; };

View File

@@ -2,19 +2,26 @@ import { Entity } from './Entity';
import { EntityList } from './Utils/EntityList'; import { EntityList } from './Utils/EntityList';
import { IdentifierPool } from './Utils/IdentifierPool'; import { IdentifierPool } from './Utils/IdentifierPool';
import { EntitySystem } from './Systems/EntitySystem'; import { EntitySystem } from './Systems/EntitySystem';
import { ComponentStorageManager, ComponentRegistry } from './Core/ComponentStorage'; import { ComponentStorageManager, ComponentRegistry, ComponentType } from './Core/ComponentStorage';
import { QuerySystem } from './Core/QuerySystem'; import { QuerySystem } from './Core/QuerySystem';
import { TypeSafeEventSystem } from './Core/EventSystem'; import { TypeSafeEventSystem } from './Core/EventSystem';
import { EventBus } from './Core/EventBus';
import { ReferenceTracker } from './Core/ReferenceTracker'; import { ReferenceTracker } from './Core/ReferenceTracker';
import { IScene, ISceneConfig } from './IScene'; import { IScene, ISceneConfig } from './IScene';
import { getComponentInstanceTypeName, getSystemInstanceTypeName, getSystemMetadata } from "./Decorators"; import { getComponentInstanceTypeName, getSystemInstanceTypeName, getSystemMetadata } from './Decorators';
import { TypedQueryBuilder } from './Core/Query/TypedQuery'; import { TypedQueryBuilder } from './Core/Query/TypedQuery';
import { SceneSerializer, SceneSerializationOptions, SceneDeserializationOptions } from './Serialization/SceneSerializer'; import {
import { IncrementalSerializer, IncrementalSnapshot, IncrementalSerializationOptions } from './Serialization/IncrementalSerializer'; SceneSerializer,
SceneSerializationOptions,
SceneDeserializationOptions
} from './Serialization/SceneSerializer';
import {
IncrementalSerializer,
IncrementalSnapshot,
IncrementalSerializationOptions
} from './Serialization/IncrementalSerializer';
import { ComponentPoolManager } from './Core/ComponentPool'; import { ComponentPoolManager } from './Core/ComponentPool';
import { PerformanceMonitor } from '../Utils/PerformanceMonitor'; import { PerformanceMonitor } from '../Utils/PerformanceMonitor';
import { ServiceContainer, type ServiceType } from '../Core/ServiceContainer'; import { ServiceContainer, type ServiceType, type IService } from '../Core/ServiceContainer';
import { createInstance, isInjectable, injectProperties } from '../Core/DI'; import { createInstance, isInjectable, injectProperties } from '../Core/DI';
import { createLogger } from '../Utils/Logger'; import { createLogger } from '../Utils/Logger';
@@ -30,13 +37,14 @@ export class Scene implements IScene {
* *
* 用于标识和调试的友好名称。 * 用于标识和调试的友好名称。
*/ */
public name: string = ""; public name: string = '';
/** /**
* 场景自定义数据 * 场景自定义数据
* *
* 用于存储场景级别的配置和状态数据。 * 用于存储场景级别的配置和状态数据。
*/ */
// eslint-disable-next-line @typescript-eslint/no-explicit-any
public readonly sceneData: Map<string, any> = new Map(); public readonly sceneData: Map<string, any> = new Map();
/** /**
@@ -46,7 +54,6 @@ export class Scene implements IScene {
*/ */
public readonly entities: EntityList; public readonly entities: EntityList;
/** /**
* 实体ID池 * 实体ID池
* *
@@ -119,6 +126,20 @@ export class Scene implements IScene {
*/ */
private _systemsOrderDirty: boolean = true; private _systemsOrderDirty: boolean = true;
/**
* 系统错误计数器
*
* 跟踪每个系统的错误次数,用于自动禁用频繁出错的系统
*/
private _systemErrorCount: Map<EntitySystem, number> = new Map();
/**
* 最大允许错误次数
*
* 系统错误次数超过此阈值后将被自动禁用
*/
private _maxErrorCount: number = 10;
/** /**
* 获取场景中所有已注册的EntitySystem * 获取场景中所有已注册的EntitySystem
* *
@@ -131,24 +152,35 @@ export class Scene implements IScene {
return this._cachedSystems; return this._cachedSystems;
} }
// 重新构建系统列表 this._cachedSystems = this._rebuildSystemsCache();
const services = this._services.getAll();
const systems: EntitySystem[] = [];
for (const service of services) {
if (service instanceof EntitySystem) {
systems.push(service);
}
}
// 按updateOrder排序
systems.sort((a, b) => a.updateOrder - b.updateOrder);
// 缓存结果
this._cachedSystems = systems;
this._systemsOrderDirty = false; this._systemsOrderDirty = false;
return systems; return this._cachedSystems;
}
/**
* 重新构建系统缓存
*
* 从服务容器中提取所有EntitySystem并排序
*/
private _rebuildSystemsCache(): EntitySystem[] {
const allServices = this._services.getAll();
const systems = this._filterEntitySystems(allServices);
return this._sortSystemsByUpdateOrder(systems);
}
/**
* 从服务列表中过滤出EntitySystem实例
*/
private _filterEntitySystems(services: IService[]): EntitySystem[] {
return services.filter((service): service is EntitySystem => service instanceof EntitySystem);
}
/**
* 按updateOrder排序系统
*/
private _sortSystemsByUpdateOrder(systems: EntitySystem[]): EntitySystem[] {
return systems.sort((a, b) => a.updateOrder - b.updateOrder);
} }
/** /**
@@ -212,16 +244,6 @@ export class Scene implements IScene {
if (config?.name) { if (config?.name) {
this.name = config.name; this.name = config.name;
} }
if (!Entity.eventBus) {
Entity.eventBus = new EventBus(false);
}
if (Entity.eventBus) {
Entity.eventBus.onComponentAdded((data: unknown) => {
this.eventSystem.emitSync('component:added', data);
});
}
} }
/** /**
@@ -231,8 +253,7 @@ export class Scene implements IScene {
*/ */
private get performanceMonitor(): PerformanceMonitor { private get performanceMonitor(): PerformanceMonitor {
if (!this._performanceMonitor) { if (!this._performanceMonitor) {
this._performanceMonitor = this._services.tryResolve(PerformanceMonitor) this._performanceMonitor = this._services.tryResolve(PerformanceMonitor) ?? new PerformanceMonitor();
?? new PerformanceMonitor();
} }
return this._performanceMonitor; return this._performanceMonitor;
} }
@@ -242,24 +263,21 @@ export class Scene implements IScene {
* *
* 在场景创建时调用,子类可以重写此方法来设置初始实体和组件。 * 在场景创建时调用,子类可以重写此方法来设置初始实体和组件。
*/ */
public initialize(): void { public initialize(): void {}
}
/** /**
* 场景开始运行时的回调 * 场景开始运行时的回调
* *
* 在场景开始运行时调用,可以在此方法中执行场景启动逻辑。 * 在场景开始运行时调用,可以在此方法中执行场景启动逻辑。
*/ */
public onStart(): void { public onStart(): void {}
}
/** /**
* 场景卸载时的回调 * 场景卸载时的回调
* *
* 在场景被销毁时调用,可以在此方法中执行清理工作。 * 在场景被销毁时调用,可以在此方法中执行清理工作。
*/ */
public unload(): void { public unload(): void {}
}
/** /**
* 开始场景,启动实体处理器等 * 开始场景,启动实体处理器等
@@ -309,36 +327,61 @@ export class Scene implements IScene {
this.entities.updateLists(); this.entities.updateLists();
// 更新所有EntitySystem
const systems = this.systems; const systems = this.systems;
for (const system of systems) { for (const system of systems) {
if (system.enabled) { if (system.enabled) {
try { try {
system.update(); system.update();
} catch (error) { } catch (error) {
this.logger.error(`Error in system ${system.constructor.name}.update():`, error); this._handleSystemError(system, 'update', error);
} }
} }
} }
// LateUpdate
for (const system of systems) { for (const system of systems) {
if (system.enabled) { if (system.enabled) {
try { try {
system.lateUpdate(); system.lateUpdate();
} catch (error) { } catch (error) {
this.logger.error(`Error in system ${system.constructor.name}.lateUpdate():`, error); this._handleSystemError(system, 'lateUpdate', error);
} }
} }
} }
} }
/**
* 处理系统执行错误
*
* 记录错误信息并跟踪错误次数。当系统错误次数超过阈值时自动禁用该系统。
*
* @param system 出错的系统
* @param phase 错误发生的阶段update 或 lateUpdate
* @param error 错误对象
*/
private _handleSystemError(system: EntitySystem, phase: 'update' | 'lateUpdate', error: unknown): void {
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}]:`,
error
);
if (errorCount >= this._maxErrorCount) {
system.enabled = false;
this.logger.error(
`System ${system.constructor.name} has been disabled due to excessive errors (${errorCount} errors)`
);
}
}
/** /**
* 将实体添加到此场景,并返回它 * 将实体添加到此场景,并返回它
* @param name 实体名称 * @param name 实体名称
*/ */
public createEntity(name: string) { public createEntity(name: string) {
let entity = new Entity(name, this.identifierPool.checkOut()); const entity = new Entity(name, this.identifierPool.checkOut());
this.eventSystem.emitSync('entity:created', { entityName: name, entity, scene: this }); this.eventSystem.emitSync('entity:created', { entityName: name, entity, scene: this });
@@ -384,7 +427,7 @@ export class Scene implements IScene {
* @param namePrefix 实体名称前缀 * @param namePrefix 实体名称前缀
* @returns 创建的实体列表 * @returns 创建的实体列表
*/ */
public createEntities(count: number, namePrefix: string = "Entity"): Entity[] { public createEntities(count: number, namePrefix: string = 'Entity'): Entity[] {
const entities: Entity[] = []; const entities: Entity[] = [];
// 批量创建实体对象,不立即添加到系统 // 批量创建实体对象,不立即添加到系统
@@ -408,7 +451,6 @@ export class Scene implements IScene {
return entities; return entities;
} }
/** /**
* 批量销毁实体 * 批量销毁实体
*/ */
@@ -416,7 +458,7 @@ export class Scene implements IScene {
if (entities.length === 0) return; if (entities.length === 0) return;
for (const entity of entities) { for (const entity of entities) {
entity._isDestroyed = true; entity.setDestroyedState(true);
} }
for (const entity of entities) { for (const entity of entities) {
@@ -473,7 +515,9 @@ export class Scene implements IScene {
/** /**
* 根据名称查找实体(别名方法) * 根据名称查找实体(别名方法)
*
* @param name 实体名称 * @param name 实体名称
* @deprecated 请使用 findEntity() 代替此方法
*/ */
public getEntityByName(name: string): Entity | null { public getEntityByName(name: string): Entity | null {
return this.findEntity(name); return this.findEntity(name);
@@ -481,7 +525,9 @@ export class Scene implements IScene {
/** /**
* 根据标签查找实体(别名方法) * 根据标签查找实体(别名方法)
*
* @param tag 实体标签 * @param tag 实体标签
* @deprecated 请使用 findEntitiesByTag() 代替此方法
*/ */
public getEntitiesByTag(tag: number): Entity[] { public getEntitiesByTag(tag: number): Entity[] {
return this.findEntitiesByTag(tag); return this.findEntitiesByTag(tag);
@@ -502,7 +548,7 @@ export class Scene implements IScene {
* } * }
* ``` * ```
*/ */
public queryAll(...componentTypes: any[]): { entities: readonly Entity[] } { public queryAll(...componentTypes: ComponentType[]): { entities: readonly Entity[] } {
return this.querySystem.queryAll(...componentTypes); return this.querySystem.queryAll(...componentTypes);
} }
@@ -512,7 +558,7 @@ export class Scene implements IScene {
* @param componentTypes - 组件类型数组 * @param componentTypes - 组件类型数组
* @returns 查询结果 * @returns 查询结果
*/ */
public queryAny(...componentTypes: any[]): { entities: readonly Entity[] } { public queryAny(...componentTypes: ComponentType[]): { entities: readonly Entity[] } {
return this.querySystem.queryAny(...componentTypes); return this.querySystem.queryAny(...componentTypes);
} }
@@ -522,7 +568,7 @@ export class Scene implements IScene {
* @param componentTypes - 组件类型数组 * @param componentTypes - 组件类型数组
* @returns 查询结果 * @returns 查询结果
*/ */
public queryNone(...componentTypes: any[]): { entities: readonly Entity[] } { public queryNone(...componentTypes: ComponentType[]): { entities: readonly Entity[] } {
return this.querySystem.queryNone(...componentTypes); return this.querySystem.queryNone(...componentTypes);
} }
@@ -577,11 +623,9 @@ export class Scene implements IScene {
* scene.addEntityProcessor(system); * scene.addEntityProcessor(system);
* ``` * ```
*/ */
public addEntityProcessor<T extends EntitySystem>( public addEntityProcessor<T extends EntitySystem>(systemTypeOrInstance: ServiceType<T> | T): T {
systemTypeOrInstance: ServiceType<T> | T
): T {
let system: T; let system: T;
let constructor: any; let constructor: ServiceType<T>;
if (typeof systemTypeOrInstance === 'function') { if (typeof systemTypeOrInstance === 'function') {
constructor = systemTypeOrInstance; constructor = systemTypeOrInstance;
@@ -595,11 +639,11 @@ export class Scene implements IScene {
if (isInjectable(constructor)) { if (isInjectable(constructor)) {
system = createInstance(constructor, this._services) as T; system = createInstance(constructor, this._services) as T;
} else { } else {
system = new (constructor as any)() as T; system = new constructor() as T;
} }
} else { } else {
system = systemTypeOrInstance; system = systemTypeOrInstance;
constructor = system.constructor; constructor = system.constructor as ServiceType<T>;
if (this._services.isRegistered(constructor)) { if (this._services.isRegistered(constructor)) {
const existingSystem = this._services.resolve(constructor); const existingSystem = this._services.resolve(constructor);
@@ -609,7 +653,7 @@ export class Scene implements IScene {
} else { } else {
this.logger.warn( this.logger.warn(
`Attempting to register a different instance of ${constructor.name}, ` + `Attempting to register a different instance of ${constructor.name}, ` +
`but type is already registered. Returning existing instance.` 'but type is already registered. Returning existing instance.'
); );
return existingSystem as T; return existingSystem as T;
} }
@@ -701,7 +745,7 @@ export class Scene implements IScene {
* @param processor 要删除的处理器 * @param processor 要删除的处理器
*/ */
public removeEntityProcessor(processor: EntitySystem): void { public removeEntityProcessor(processor: EntitySystem): void {
const constructor = processor.constructor as any; const constructor = processor.constructor as ServiceType<EntitySystem>;
// 从ServiceContainer移除 // 从ServiceContainer移除
this._services.unregister(constructor); this._services.unregister(constructor);
@@ -740,7 +784,7 @@ export class Scene implements IScene {
* ``` * ```
*/ */
public getEntityProcessor<T extends EntitySystem>(type: new (...args: unknown[]) => T): T | null { public getEntityProcessor<T extends EntitySystem>(type: new (...args: unknown[]) => T): T | null {
return this._services.tryResolve(type as any) as T | null; return this._services.tryResolve(type as ServiceType<T>) as T | null;
} }
/** /**
@@ -749,8 +793,8 @@ export class Scene implements IScene {
public getStats(): { public getStats(): {
entityCount: number; entityCount: number;
processorCount: number; processorCount: number;
componentStorageStats: Map<string, any>; componentStorageStats: Map<string, { totalSlots: number; usedSlots: number; freeSlots: number; fragmentation: number }>;
} { } {
return { return {
entityCount: this.entities.count, entityCount: this.entities.count,
processorCount: this.systems.length, processorCount: this.systems.length,
@@ -758,7 +802,6 @@ export class Scene implements IScene {
}; };
} }
/** /**
* 获取场景的调试信息 * 获取场景的调试信息
*/ */
@@ -778,24 +821,24 @@ export class Scene implements IScene {
updateOrder: number; updateOrder: number;
entityCount: number; entityCount: number;
}>; }>;
componentStats: Map<string, any>; componentStats: Map<string, { totalSlots: number; usedSlots: number; freeSlots: number; fragmentation: number }>;
} { } {
const systems = this.systems; const systems = this.systems;
return { return {
name: this.name || this.constructor.name, name: this.name || this.constructor.name,
entityCount: this.entities.count, entityCount: this.entities.count,
processorCount: systems.length, processorCount: systems.length,
isRunning: this._didSceneBegin, isRunning: this._didSceneBegin,
entities: this.entities.buffer.map(entity => ({ entities: this.entities.buffer.map((entity) => ({
name: entity.name, name: entity.name,
id: entity.id, id: entity.id,
componentCount: entity.components.length, componentCount: entity.components.length,
componentTypes: entity.components.map(c => getComponentInstanceTypeName(c)) componentTypes: entity.components.map((c) => getComponentInstanceTypeName(c))
})), })),
processors: systems.map(processor => ({ processors: systems.map((processor) => ({
name: getSystemInstanceTypeName(processor), name: getSystemInstanceTypeName(processor),
updateOrder: processor.updateOrder, updateOrder: processor.updateOrder,
entityCount: (processor as any)._entities?.length || 0 entityCount: processor.entities.length
})), })),
componentStats: this.componentStorageManager.getAllStats() componentStats: this.componentStorageManager.getAllStats()
}; };
@@ -855,7 +898,7 @@ export class Scene implements IScene {
// ==================== 增量序列化 API ==================== // ==================== 增量序列化 API ====================
/** 增量序列化的基础快照 */ /** 增量序列化的基础快照 */
private _incrementalBaseSnapshot?: any; private _incrementalBaseSnapshot?: unknown;
/** /**
* 创建增量序列化的基础快照 * 创建增量序列化的基础快照
@@ -910,11 +953,7 @@ export class Scene implements IScene {
throw new Error('必须先调用 createIncrementalSnapshot() 创建基础快照'); throw new Error('必须先调用 createIncrementalSnapshot() 创建基础快照');
} }
return IncrementalSerializer.computeIncremental( return IncrementalSerializer.computeIncremental(this, this._incrementalBaseSnapshot as Parameters<typeof IncrementalSerializer.computeIncremental>[1], options);
this,
this._incrementalBaseSnapshot,
options
);
} }
/** /**
@@ -939,16 +978,15 @@ export class Scene implements IScene {
*/ */
public applyIncremental( public applyIncremental(
incremental: IncrementalSnapshot | string | Uint8Array, incremental: IncrementalSnapshot | string | Uint8Array,
componentRegistry?: Map<string, any> componentRegistry?: Map<string, ComponentType>
): void { ): void {
const isSerializedData = typeof incremental === 'string' || const isSerializedData = typeof incremental === 'string' || incremental instanceof Uint8Array;
incremental instanceof Uint8Array;
const snapshot = isSerializedData const snapshot = isSerializedData
? IncrementalSerializer.deserializeIncremental(incremental as string | Uint8Array) ? IncrementalSerializer.deserializeIncremental(incremental as string | Uint8Array)
: incremental as IncrementalSnapshot; : (incremental as IncrementalSnapshot);
const registry = componentRegistry || ComponentRegistry.getAllComponentNames() as Map<string, any>; const registry = componentRegistry || (ComponentRegistry.getAllComponentNames() as Map<string, ComponentType>);
IncrementalSerializer.applyIncremental(this, snapshot, registry); IncrementalSerializer.applyIncremental(this, snapshot, registry);
} }

View File

@@ -212,7 +212,7 @@ export class ComponentSerializer {
// 数组 // 数组
if (Array.isArray(value)) { if (Array.isArray(value)) {
return value.map(item => this.serializeValue(item)); return value.map((item) => this.serializeValue(item));
} }
// Map (如果没有使用@SerializeMap装饰器) // Map (如果没有使用@SerializeMap装饰器)
@@ -276,7 +276,7 @@ export class ComponentSerializer {
// 数组 // 数组
if (Array.isArray(value)) { if (Array.isArray(value)) {
return value.map(item => this.deserializeValue(item)); return value.map((item) => this.deserializeValue(item));
} }
// 普通对象 // 普通对象
@@ -340,10 +340,10 @@ export class ComponentSerializer {
return { return {
type: metadata.options.typeId || getComponentTypeName(componentType), type: metadata.options.typeId || getComponentTypeName(componentType),
version: metadata.options.version, version: metadata.options.version,
fields: Array.from(metadata.fields.keys()).map(k => fields: Array.from(metadata.fields.keys()).map((k) =>
typeof k === 'symbol' ? k.toString() : k typeof k === 'symbol' ? k.toString() : k
), ),
ignoredFields: Array.from(metadata.ignoredFields).map(k => ignoredFields: Array.from(metadata.ignoredFields).map((k) =>
typeof k === 'symbol' ? k.toString() : k typeof k === 'symbol' ? k.toString() : k
), ),
isSerializable: true isSerializable: true

View File

@@ -696,22 +696,22 @@ export class IncrementalSerializer {
componentChanges: incremental.componentChanges.length, componentChanges: incremental.componentChanges.length,
sceneDataChanges: incremental.sceneDataChanges.length, sceneDataChanges: incremental.sceneDataChanges.length,
addedEntities: incremental.entityChanges.filter( addedEntities: incremental.entityChanges.filter(
c => c.operation === ChangeOperation.EntityAdded (c) => c.operation === ChangeOperation.EntityAdded
).length, ).length,
removedEntities: incremental.entityChanges.filter( removedEntities: incremental.entityChanges.filter(
c => c.operation === ChangeOperation.EntityRemoved (c) => c.operation === ChangeOperation.EntityRemoved
).length, ).length,
updatedEntities: incremental.entityChanges.filter( updatedEntities: incremental.entityChanges.filter(
c => c.operation === ChangeOperation.EntityUpdated (c) => c.operation === ChangeOperation.EntityUpdated
).length, ).length,
addedComponents: incremental.componentChanges.filter( addedComponents: incremental.componentChanges.filter(
c => c.operation === ChangeOperation.ComponentAdded (c) => c.operation === ChangeOperation.ComponentAdded
).length, ).length,
removedComponents: incremental.componentChanges.filter( removedComponents: incremental.componentChanges.filter(
c => c.operation === ChangeOperation.ComponentRemoved (c) => c.operation === ChangeOperation.ComponentRemoved
).length, ).length,
updatedComponents: incremental.componentChanges.filter( updatedComponents: incremental.componentChanges.filter(
c => c.operation === ChangeOperation.ComponentUpdated (c) => c.operation === ChangeOperation.ComponentUpdated
).length ).length
}; };
} }

View File

@@ -363,7 +363,7 @@ export class SceneSerializer {
// 数组 // 数组
if (Array.isArray(value)) { if (Array.isArray(value)) {
return value.map(item => this.serializeValue(item)); return value.map((item) => this.serializeValue(item));
} }
// 普通对象 // 普通对象
@@ -409,7 +409,7 @@ export class SceneSerializer {
// 数组 // 数组
if (Array.isArray(value)) { if (Array.isArray(value)) {
return value.map(item => this.deserializeValue(item)); return value.map((item) => this.deserializeValue(item));
} }
// 普通对象 // 普通对象
@@ -437,8 +437,8 @@ export class SceneSerializer {
const componentTypeSet = new Set(options.components); const componentTypeSet = new Set(options.components);
// 只返回拥有指定组件的实体 // 只返回拥有指定组件的实体
return entities.filter(entity => { return entities.filter((entity) => {
return Array.from(entity.components).some(component => return Array.from(entity.components).some((component) =>
componentTypeSet.has(component.constructor as ComponentType) componentTypeSet.has(component.constructor as ComponentType)
); );
}); });

View File

@@ -127,7 +127,7 @@ export class VersionMigrationManager {
return component; return component;
} }
let migratedData = { ...component }; const migratedData = { ...component };
let version = currentVersion; let version = currentVersion;
// 执行迁移链 // 执行迁移链
@@ -193,12 +193,12 @@ export class VersionMigrationManager {
private static migrateSceneComponents(scene: SerializedScene): SerializedScene { private static migrateSceneComponents(scene: SerializedScene): SerializedScene {
const migratedScene = { ...scene }; const migratedScene = { ...scene };
migratedScene.entities = scene.entities.map(entity => ({ migratedScene.entities = scene.entities.map((entity) => ({
...entity, ...entity,
components: entity.components.map(component => { components: entity.components.map((component) => {
// 查找组件的目标版本 // 查找组件的目标版本
const typeInfo = scene.componentTypeRegistry.find( const typeInfo = scene.componentTypeRegistry.find(
t => t.typeName === component.type (t) => t.typeName === component.type
); );
if (typeInfo && typeInfo.version !== component.version) { if (typeInfo && typeInfo.version !== component.version) {
@@ -220,10 +220,10 @@ export class VersionMigrationManager {
entities: any[], entities: any[],
typeRegistry: Array<{ typeName: string; version: number }> typeRegistry: Array<{ typeName: string; version: number }>
): any[] { ): any[] {
return entities.map(entity => ({ return entities.map((entity) => ({
...entity, ...entity,
components: entity.components.map((component: SerializedComponent) => { components: entity.components.map((component: SerializedComponent) => {
const typeInfo = typeRegistry.find(t => t.typeName === component.type); const typeInfo = typeRegistry.find((t) => t.typeName === component.type);
if (typeInfo && typeInfo.version !== component.version) { if (typeInfo && typeInfo.version !== component.version) {
return this.migrateComponent(component, typeInfo.version); return this.migrateComponent(component, typeInfo.version);

View File

@@ -0,0 +1,166 @@
import { Entity } from '../Entity';
/**
* 实体缓存管理器
*
* 负责管理 EntitySystem 中的实体缓存,提供帧缓存和持久缓存两级缓存机制。
* 使用面向对象设计,将数据和行为封装在类中。
*
* @example
* ```typescript
* const cache = new EntityCache();
* cache.setPersistent(entities);
* const cached = cache.getPersistent();
* cache.invalidate();
* ```
*/
export class EntityCache {
/**
* 帧缓存
*
* 在update周期内使用每帧结束后清理
*/
private _frameCache: readonly Entity[] | null = null;
/**
* 持久缓存
*
* 跨帧使用,直到被显式失效
*/
private _persistentCache: readonly Entity[] | null = null;
/**
* 被跟踪的实体集合
*
* 用于跟踪哪些实体正在被此系统处理
*/
private _trackedEntities: Set<Entity> = new Set();
/**
* 获取帧缓存
*/
public getFrame(): readonly Entity[] | null {
return this._frameCache;
}
/**
* 设置帧缓存
*
* @param entities 要缓存的实体列表
*/
public setFrame(entities: readonly Entity[]): void {
this._frameCache = entities;
}
/**
* 获取持久缓存
*/
public getPersistent(): readonly Entity[] | null {
return this._persistentCache;
}
/**
* 设置持久缓存
*
* @param entities 要缓存的实体列表
*/
public setPersistent(entities: readonly Entity[]): void {
this._persistentCache = entities;
}
/**
* 获取被跟踪的实体集合
*/
public getTracked(): ReadonlySet<Entity> {
return this._trackedEntities;
}
/**
* 添加被跟踪的实体
*
* @param entity 要跟踪的实体
*/
public addTracked(entity: Entity): void {
this._trackedEntities.add(entity);
}
/**
* 移除被跟踪的实体
*
* @param entity 要移除的实体
*/
public removeTracked(entity: Entity): void {
this._trackedEntities.delete(entity);
}
/**
* 检查实体是否被跟踪
*
* @param entity 要检查的实体
*/
public isTracked(entity: Entity): boolean {
return this._trackedEntities.has(entity);
}
/**
* 使持久缓存失效
*
* 当实体变化时调用,强制下次查询时重新计算
*/
public invalidate(): void {
this._persistentCache = null;
}
/**
* 清除帧缓存
*
* 在每帧结束时调用
*/
public clearFrame(): void {
this._frameCache = null;
}
/**
* 清除所有缓存
*
* 在系统重置或销毁时调用
*/
public clearAll(): void {
this._frameCache = null;
this._persistentCache = null;
this._trackedEntities.clear();
}
/**
* 检查是否有有效的持久缓存
*/
public hasPersistent(): boolean {
return this._persistentCache !== null;
}
/**
* 检查是否有有效的帧缓存
*/
public hasFrame(): boolean {
return this._frameCache !== null;
}
/**
* 获取缓存统计信息
*/
public getStats(): {
hasFrame: boolean;
hasPersistent: boolean;
trackedCount: number;
frameEntityCount: number;
persistentEntityCount: number;
} {
return {
hasFrame: this._frameCache !== null,
hasPersistent: this._persistentCache !== null,
trackedCount: this._trackedEntities.size,
frameEntityCount: this._frameCache?.length ?? 0,
persistentEntityCount: this._persistentCache?.length ?? 0
};
}
}

View File

@@ -9,18 +9,18 @@ import { createLogger } from '../../Utils/Logger';
import type { EventListenerConfig, TypeSafeEventSystem, EventHandler } from '../Core/EventSystem'; import type { EventListenerConfig, TypeSafeEventSystem, EventHandler } from '../Core/EventSystem';
import type { ComponentConstructor, ComponentInstance } from '../../Types/TypeHelpers'; import type { ComponentConstructor, ComponentInstance } from '../../Types/TypeHelpers';
import type { IService } from '../../Core/ServiceContainer'; import type { IService } from '../../Core/ServiceContainer';
import { EntityCache } from './EntityCache';
/** /**
* 事件监听器记录 * 事件监听器记录
* 只存储引用信息,用于系统销毁时自动清理
*/ */
interface EventListenerRecord { interface EventListenerRecord {
eventSystem: TypeSafeEventSystem; eventSystem: TypeSafeEventSystem;
eventType: string; eventType: string;
handler: EventHandler;
listenerRef: string; listenerRef: string;
} }
/** /**
* 实体系统的基类 * 实体系统的基类
* *
@@ -64,9 +64,7 @@ interface EventListenerRecord {
* } * }
* ``` * ```
*/ */
export abstract class EntitySystem< export abstract class EntitySystem implements ISystemBase, IService {
_TComponents extends readonly ComponentConstructor[] = []
> implements ISystemBase, IService {
private _updateOrder: number; private _updateOrder: number;
private _enabled: boolean; private _enabled: boolean;
private _performanceMonitor: PerformanceMonitor | null; private _performanceMonitor: PerformanceMonitor | null;
@@ -77,7 +75,6 @@ export abstract class EntitySystem<
private _scene: Scene | null; private _scene: Scene | null;
protected logger: ReturnType<typeof createLogger>; protected logger: ReturnType<typeof createLogger>;
/** /**
* 实体ID映射缓存 * 实体ID映射缓存
*/ */
@@ -87,30 +84,24 @@ export abstract class EntitySystem<
/** /**
* 统一的实体缓存管理器 * 统一的实体缓存管理器
*/ */
private _entityCache: { private _entityCache: EntityCache;
frame: readonly Entity[] | null;
persistent: readonly Entity[] | null;
tracked: Set<Entity>;
invalidate(): void;
clearFrame(): void;
clearAll(): void;
};
/** /**
* 获取系统处理的实体列表 * 获取系统处理的实体列表
*/ */
public get entities(): readonly Entity[] { public get entities(): readonly Entity[] {
// 如果在update周期内优先使用帧缓存 // 如果在update周期内优先使用帧缓存
if (this._entityCache.frame !== null) { const frameCache = this._entityCache.getFrame();
return this._entityCache.frame; if (frameCache !== null) {
return frameCache;
} }
// 否则使用持久缓存 // 否则使用持久缓存
if (this._entityCache.persistent === null) { if (!this._entityCache.hasPersistent()) {
this._entityCache.persistent = this.queryEntities(); this._entityCache.setPersistent(this.queryEntities());
} }
return this._entityCache.persistent; return this._entityCache.getPersistent()!;
} }
/** /**
@@ -161,23 +152,7 @@ export abstract class EntitySystem<
// 初始化logger // 初始化logger
this.logger = createLogger(this.getLoggerName()); this.logger = createLogger(this.getLoggerName());
this._entityCache = new EntityCache();
this._entityCache = {
frame: null,
persistent: null,
tracked: new Set<Entity>(),
invalidate() {
this.persistent = null;
},
clearFrame() {
this.frame = null;
},
clearAll() {
this.frame = null;
this.persistent = null;
this.tracked.clear();
}
};
} }
/** /**
@@ -203,7 +178,9 @@ export abstract class EntitySystem<
*/ */
private getPerformanceMonitor(): PerformanceMonitor { private getPerformanceMonitor(): PerformanceMonitor {
if (!this._performanceMonitor) { if (!this._performanceMonitor) {
throw new Error(`${this._systemName}: PerformanceMonitor未注入请确保在Core.create()之后再添加System到Scene`); throw new Error(
`${this._systemName}: PerformanceMonitor未注入请确保在Core.create()之后再添加System到Scene`
);
} }
return this._performanceMonitor; return this._performanceMonitor;
} }
@@ -316,16 +293,28 @@ export abstract class EntitySystem<
/** /**
* 检查是否为单一条件查询 * 检查是否为单一条件查询
*
* 使用位运算优化多条件检测。将每种查询条件映射到不同的位:
* - all: 第0位 (1)
* - any: 第1位 (2)
* - none: 第2位 (4)
* - tag: 第3位 (8)
* - name: 第4位 (16)
* - component: 第5位 (32)
*/ */
private isSingleCondition(condition: QueryCondition): boolean { private isSingleCondition(condition: QueryCondition): boolean {
// 使用位OR运算合并所有条件标记
const flags = const flags =
((condition.all.length > 0) ? 1 : 0) | (condition.all.length > 0 ? 1 : 0) |
((condition.any.length > 0) ? 2 : 0) | (condition.any.length > 0 ? 2 : 0) |
((condition.none.length > 0) ? 4 : 0) | (condition.none.length > 0 ? 4 : 0) |
((condition.tag !== undefined) ? 8 : 0) | (condition.tag !== undefined ? 8 : 0) |
((condition.name !== undefined) ? 16 : 0) | (condition.name !== undefined ? 16 : 0) |
((condition.component !== undefined) ? 32 : 0); (condition.component !== undefined ? 32 : 0);
// 位运算技巧:如果只有一个位被设置,则 flags & (flags - 1) == 0
// 例如flags=4 (100), flags-1=3 (011), 4&3=0
// 但如果 flags=6 (110), flags-1=5 (101), 6&5=4≠0
return flags !== 0 && (flags & (flags - 1)) === 0; return flags !== 0 && (flags & (flags - 1)) === 0;
} }
@@ -472,8 +461,7 @@ export abstract class EntitySystem<
*/ */
private getEntityIdMap(allEntities: readonly Entity[]): Map<number, Entity> { private getEntityIdMap(allEntities: readonly Entity[]): Map<number, Entity> {
const currentVersion = this.scene?.querySystem?.version ?? 0; const currentVersion = this.scene?.querySystem?.version ?? 0;
if (this._entityIdMap !== null && if (this._entityIdMap !== null && this._entityIdMapVersion === currentVersion) {
this._entityIdMapVersion === currentVersion) {
return this._entityIdMap; return this._entityIdMap;
} }
@@ -511,7 +499,7 @@ export abstract class EntitySystem<
const entityMap = this.getEntityIdMap(allEntities); const entityMap = this.getEntityIdMap(allEntities);
const size = idSet.size; const size = idSet.size;
const result = new Array(size); const result = new Array<Entity>(size);
let index = 0; let index = 0;
for (const id of idSet) { for (const id of idSet) {
@@ -554,10 +542,11 @@ export abstract class EntitySystem<
this.onBegin(); this.onBegin();
// 查询实体并存储到帧缓存中 // 查询实体并存储到帧缓存中
// 响应式查询会自动维护最新的实体列表updateEntityTracking会在检测到变化时invalidate // 响应式查询会自动维护最新的实体列表updateEntityTracking会在检测到变化时invalidate
this._entityCache.frame = this.queryEntities(); const queriedEntities = this.queryEntities();
entityCount = this._entityCache.frame.length; this._entityCache.setFrame(queriedEntities);
entityCount = queriedEntities.length;
this.process(this._entityCache.frame); this.process(queriedEntities);
} finally { } finally {
monitor.endMonitoring(this._systemName, startTime, entityCount); monitor.endMonitoring(this._systemName, startTime, entityCount);
} }
@@ -577,7 +566,7 @@ export abstract class EntitySystem<
try { try {
// 使用缓存的实体列表,避免重复查询 // 使用缓存的实体列表,避免重复查询
const entities = this._entityCache.frame || []; const entities = this._entityCache.getFrame() || [];
entityCount = entities.length; entityCount = entities.length;
this.lateProcess(entities); this.lateProcess(entities);
this.onEnd(); this.onEnd();
@@ -687,17 +676,17 @@ export abstract class EntitySystem<
// 检查新增的实体 // 检查新增的实体
for (const entity of currentEntities) { for (const entity of currentEntities) {
if (!this._entityCache.tracked.has(entity)) { if (!this._entityCache.isTracked(entity)) {
this._entityCache.tracked.add(entity); this._entityCache.addTracked(entity);
this.onAdded(entity); this.onAdded(entity);
hasChanged = true; hasChanged = true;
} }
} }
// 检查移除的实体 // 检查移除的实体
for (const entity of this._entityCache.tracked) { for (const entity of this._entityCache.getTracked()) {
if (!currentSet.has(entity)) { if (!currentSet.has(entity)) {
this._entityCache.tracked.delete(entity); this._entityCache.removeTracked(entity);
this.onRemoved(entity); this.onRemoved(entity);
hasChanged = true; hasChanged = true;
} }
@@ -768,15 +757,16 @@ export abstract class EntitySystem<
* @param eventType 事件类型 * @param eventType 事件类型
* @param handler 事件处理函数 * @param handler 事件处理函数
* @param config 监听器配置 * @param config 监听器配置
* @returns 监听器引用ID可用于手动移除监听器
*/ */
protected addEventListener<T = any>( protected addEventListener<T>(
eventType: string, eventType: string,
handler: EventHandler<T>, handler: EventHandler<T>,
config?: EventListenerConfig config?: EventListenerConfig
): void { ): string | null {
if (!this.scene?.eventSystem) { if (!this.scene?.eventSystem) {
this.logger.warn(`${this.systemName}: 无法添加事件监听器scene.eventSystem 不可用`); this.logger.warn(`${this.systemName}: 无法添加事件监听器scene.eventSystem 不可用`);
return; return null;
} }
const listenerRef = this.scene.eventSystem.on(eventType, handler, config); const listenerRef = this.scene.eventSystem.on(eventType, handler, config);
@@ -786,24 +776,22 @@ export abstract class EntitySystem<
this._eventListeners.push({ this._eventListeners.push({
eventSystem: this.scene.eventSystem, eventSystem: this.scene.eventSystem,
eventType, eventType,
handler,
listenerRef listenerRef
}); });
} }
return listenerRef;
} }
/** /**
* 移除特定的事件监听器 * 移除特定的事件监听器
* *
* @param eventType 事件类型 * @param eventType 事件类型
* @param handler 事件处理函数 * @param listenerRef 监听器引用ID由 addEventListener 返回)
*/ */
protected removeEventListener<T = any>( protected removeEventListener(eventType: string, listenerRef: string): void {
eventType: string,
handler: EventHandler<T>
): void {
const listenerIndex = this._eventListeners.findIndex( const listenerIndex = this._eventListeners.findIndex(
listener => listener.eventType === eventType && listener.handler === handler (listener) => listener.eventType === eventType && listener.listenerRef === listenerRef
); );
if (listenerIndex >= 0) { if (listenerIndex >= 0) {
@@ -887,15 +875,10 @@ export abstract class EntitySystem<
* } * }
* ``` * ```
*/ */
protected requireComponent<T extends ComponentConstructor>( protected requireComponent<T extends ComponentConstructor>(entity: Entity, componentType: T): ComponentInstance<T> {
entity: Entity, const component = entity.getComponent(componentType);
componentType: T
): ComponentInstance<T> {
const component = entity.getComponent(componentType as any);
if (!component) { if (!component) {
throw new Error( throw new Error(`Component ${componentType.name} not found on entity ${entity.name} in ${this.systemName}`);
`Component ${componentType.name} not found on entity ${entity.name} in ${this.systemName}`
);
} }
return component as ComponentInstance<T>; return component as ComponentInstance<T>;
} }
@@ -927,9 +910,9 @@ export abstract class EntitySystem<
entity: Entity, entity: Entity,
...components: T ...components: T
): { [K in keyof T]: ComponentInstance<T[K]> } { ): { [K in keyof T]: ComponentInstance<T[K]> } {
return components.map((type) => return components.map((type) => this.requireComponent(entity, type)) as {
this.requireComponent(entity, type) [K in keyof T]: ComponentInstance<T[K]>;
) as any; };
} }
/** /**
@@ -950,10 +933,7 @@ export abstract class EntitySystem<
* } * }
* ``` * ```
*/ */
protected forEach( protected forEach(entities: readonly Entity[], processor: (entity: Entity, index: number) => void): void {
entities: readonly Entity[],
processor: (entity: Entity, index: number) => void
): void {
for (let i = 0; i < entities.length; i++) { for (let i = 0; i < entities.length; i++) {
processor(entities[i]!, i); processor(entities[i]!, i);
} }
@@ -1000,10 +980,7 @@ export abstract class EntitySystem<
* } * }
* ``` * ```
*/ */
protected mapEntities<R>( protected mapEntities<R>(entities: readonly Entity[], mapper: (entity: Entity, index: number) => R): R[] {
entities: readonly Entity[],
mapper: (entity: Entity, index: number) => R
): R[] {
return Array.from(entities).map(mapper); return Array.from(entities).map(mapper);
} }
@@ -1052,10 +1029,7 @@ export abstract class EntitySystem<
* } * }
* ``` * ```
*/ */
protected someEntity( protected someEntity(entities: readonly Entity[], predicate: (entity: Entity, index: number) => boolean): boolean {
entities: readonly Entity[],
predicate: (entity: Entity, index: number) => boolean
): boolean {
for (let i = 0; i < entities.length; i++) { for (let i = 0; i < entities.length; i++) {
if (predicate(entities[i]!, i)) { if (predicate(entities[i]!, i)) {
return true; return true;
@@ -1081,10 +1055,7 @@ export abstract class EntitySystem<
* } * }
* ``` * ```
*/ */
protected everyEntity( protected everyEntity(entities: readonly Entity[], predicate: (entity: Entity, index: number) => boolean): boolean {
entities: readonly Entity[],
predicate: (entity: Entity, index: number) => boolean
): boolean {
for (let i = 0; i < entities.length; i++) { for (let i = 0; i < entities.length; i++) {
if (!predicate(entities[i]!, i)) { if (!predicate(entities[i]!, i)) {
return false; return false;

View File

@@ -414,7 +414,7 @@ export abstract class WorkerEntitySystem<TEntityData = any> extends EntitySystem
${sharedProcessFunctionBody} ${sharedProcessFunctionBody}
}; };
userProcessFunction(sharedFloatArray, startIndex, endIndex, deltaTime, systemConfig); userProcessFunction(sharedFloatArray, startIndex, endIndex, deltaTime, systemConfig);
` : ``} ` : ''}
} }
`; `;
} }
@@ -494,7 +494,7 @@ export abstract class WorkerEntitySystem<TEntityData = any> extends EntitySystem
const deltaTime = Time.deltaTime; const deltaTime = Time.deltaTime;
// 3. Worker执行阶段 // 3. Worker执行阶段
const promises = batches.map(batch => const promises = batches.map((batch) =>
this.workerPool!.execute({ this.workerPool!.execute({
entities: batch, entities: batch,
deltaTime, deltaTime,
@@ -525,7 +525,7 @@ export abstract class WorkerEntitySystem<TEntityData = any> extends EntitySystem
*/ */
private processSynchronously(entities: readonly Entity[]): void { private processSynchronously(entities: readonly Entity[]): void {
// 1. 数据提取阶段 // 1. 数据提取阶段
const entityData = entities.map(entity => this.extractEntityData(entity)); const entityData = entities.map((entity) => this.extractEntityData(entity));
// 2. 主线程处理阶段 // 2. 主线程处理阶段
const deltaTime = Time.deltaTime; const deltaTime = Time.deltaTime;
@@ -534,7 +534,7 @@ export abstract class WorkerEntitySystem<TEntityData = any> extends EntitySystem
// 3. 结果应用阶段 // 3. 结果应用阶段
// 处理Promise返回值 // 处理Promise返回值
if (results && typeof (results as any).then === 'function') { if (results && typeof (results as any).then === 'function') {
(results as Promise<TEntityData[]>).then(finalResults => { (results as Promise<TEntityData[]>).then((finalResults) => {
entities.forEach((entity, index) => { entities.forEach((entity, index) => {
this.applyResult(entity, finalResults[index]!); this.applyResult(entity, finalResults[index]!);
}); });
@@ -813,7 +813,7 @@ export abstract class WorkerEntitySystem<TEntityData = any> extends EntitySystem
sharedArrayBufferSupported: boolean; sharedArrayBufferSupported: boolean;
sharedArrayBufferEnabled: boolean; sharedArrayBufferEnabled: boolean;
currentMode: 'shared-buffer' | 'worker' | 'sync'; currentMode: 'shared-buffer' | 'worker' | 'sync';
} { } {
let currentMode: 'shared-buffer' | 'worker' | 'sync' = 'sync'; let currentMode: 'shared-buffer' | 'worker' | 'sync' = 'sync';
if (this.config.enableWorker && this.workerPool) { if (this.config.enableWorker && this.workerPool) {

View File

@@ -69,7 +69,7 @@ export class BitMask64Utils {
return maskSegments.some((seg, index) => { return maskSegments.some((seg, index) => {
const bitsSeg = bitsSegments[index]; const bitsSeg = bitsSegments[index];
return bitsSeg && ((seg[SegmentPart.LOW] & bitsSeg[SegmentPart.LOW]) !== 0 || (seg[SegmentPart.HIGH] & bitsSeg[SegmentPart.HIGH]) !== 0); return bitsSeg && ((seg[SegmentPart.LOW] & bitsSeg[SegmentPart.LOW]) !== 0 || (seg[SegmentPart.HIGH] & bitsSeg[SegmentPart.HIGH]) !== 0);
}) });
} }
/** /**
@@ -145,7 +145,7 @@ export class BitMask64Utils {
return baseIsZero; return baseIsZero;
} }
// 额外检查扩展区域是否都为0 // 额外检查扩展区域是否都为0
return mask.segments.every(seg => seg[SegmentPart.LOW] === 0 && seg[SegmentPart.HIGH] === 0); return mask.segments.every((seg) => seg[SegmentPart.LOW] === 0 && seg[SegmentPart.HIGH] === 0);
} }
/** /**
@@ -155,7 +155,7 @@ export class BitMask64Utils {
* @returns 如果两个掩码完全相等则返回true * @returns 如果两个掩码完全相等则返回true
*/ */
public static equals(a: BitMask64Data, b: BitMask64Data): boolean { public static equals(a: BitMask64Data, b: BitMask64Data): boolean {
let baseEquals = a.base[SegmentPart.LOW] === b.base[SegmentPart.LOW] && a.base[SegmentPart.HIGH] === b.base[SegmentPart.HIGH]; const baseEquals = a.base[SegmentPart.LOW] === b.base[SegmentPart.LOW] && a.base[SegmentPart.HIGH] === b.base[SegmentPart.HIGH];
// base不相等或ab都没有扩展区域位直接返回base比较结果 // base不相等或ab都没有扩展区域位直接返回base比较结果
if(!baseEquals || (!a.segments && !b.segments)) return baseEquals; if(!baseEquals || (!a.segments && !b.segments)) return baseEquals;
// 不能假设ab的segments都存在或长度相同. // 不能假设ab的segments都存在或长度相同.
@@ -355,7 +355,7 @@ export class BitMask64Utils {
if(!source.segments || source.segments.length == 0) return; if(!source.segments || source.segments.length == 0) return;
// 没有拓展段,则直接复制数组 // 没有拓展段,则直接复制数组
if(!target.segments){ if(!target.segments){
target.segments = source.segments.map(seg => [...seg]); target.segments = source.segments.map((seg) => [...seg]);
return; return;
} }
// source有扩展段target扩展段不足则补充长度 // source有扩展段target扩展段不足则补充长度
@@ -382,7 +382,7 @@ export class BitMask64Utils {
public static clone(mask: BitMask64Data): BitMask64Data { public static clone(mask: BitMask64Data): BitMask64Data {
return { return {
base: mask.base.slice() as BitMask64Segment, base: mask.base.slice() as BitMask64Segment,
...(mask.segments && { segments: mask.segments.map(seg => [...seg] as BitMask64Segment) }) ...(mask.segments && { segments: mask.segments.map((seg) => [...seg] as BitMask64Segment) })
}; };
} }
@@ -414,8 +414,8 @@ export class BitMask64Utils {
for (let i = -1; i < totalLength; i++) { for (let i = -1; i < totalLength; i++) {
let segResult = ''; let segResult = '';
const bitMaskData = i == -1 ? mask.base : mask.segments![i]!; const bitMaskData = i == -1 ? mask.base : mask.segments![i]!;
let hi = bitMaskData[SegmentPart.HIGH]; const hi = bitMaskData[SegmentPart.HIGH];
let lo = bitMaskData[SegmentPart.LOW]; const lo = bitMaskData[SegmentPart.LOW];
if(radix == 2){ if(radix == 2){
const hiBits = hi.toString(2).padStart(32, '0'); const hiBits = hi.toString(2).padStart(32, '0');
const loBits = lo.toString(2).padStart(32, '0'); const loBits = lo.toString(2).padStart(32, '0');

View File

@@ -1,4 +1,4 @@
import { BitMask64Data } from "./BigIntCompatibility"; import { BitMask64Data } from './BigIntCompatibility';
// FlatHashMapFast.ts // FlatHashMapFast.ts

View File

@@ -77,7 +77,7 @@ export class ComponentSparseSet {
this.removeEntity(entity); this.removeEntity(entity);
} }
let componentMask = BitMask64Utils.clone(BitMask64Utils.ZERO); const componentMask = BitMask64Utils.clone(BitMask64Utils.ZERO);
const entityComponents = new Set<ComponentType>(); const entityComponents = new Set<ComponentType>();
// 分析实体组件并构建位掩码 // 分析实体组件并构建位掩码
@@ -169,7 +169,7 @@ export class ComponentSparseSet {
} }
// 构建目标位掩码 // 构建目标位掩码
let targetMask = BitMask64Utils.clone(BitMask64Utils.ZERO); const targetMask = BitMask64Utils.clone(BitMask64Utils.ZERO);
for (const componentType of componentTypes) { for (const componentType of componentTypes) {
if (!ComponentRegistry.isRegistered(componentType)) { if (!ComponentRegistry.isRegistered(componentType)) {
return new Set<Entity>(); // 未注册的组件类型,结果为空 return new Set<Entity>(); // 未注册的组件类型,结果为空
@@ -209,7 +209,7 @@ export class ComponentSparseSet {
} }
// 构建目标位掩码 // 构建目标位掩码
let targetMask = BitMask64Utils.clone(BitMask64Utils.ZERO); const targetMask = BitMask64Utils.clone(BitMask64Utils.ZERO);
for (const componentType of componentTypes) { for (const componentType of componentTypes) {
if (ComponentRegistry.isRegistered(componentType)) { if (ComponentRegistry.isRegistered(componentType)) {
const bitMask = ComponentRegistry.getBitMask(componentType); const bitMask = ComponentRegistry.getBitMask(componentType);
@@ -327,7 +327,7 @@ export class ComponentSparseSet {
masksMemory: number; masksMemory: number;
mappingsMemory: number; mappingsMemory: number;
totalMemory: number; totalMemory: number;
} { } {
const entitiesStats = this._entities.getMemoryStats(); const entitiesStats = this._entities.getMemoryStats();
const masksMemory = this._componentMasks.length * 16; // 估计每个BigInt 16字节 const masksMemory = this._componentMasks.length * 16; // 估计每个BigInt 16字节

View File

@@ -263,7 +263,7 @@ export class EntityList {
pendingAdd: number; pendingAdd: number;
pendingRemove: number; pendingRemove: number;
nameIndexSize: number; nameIndexSize: number;
} { } {
let activeCount = 0; let activeCount = 0;
for (const entity of this.buffer) { for (const entity of this.buffer) {
if (entity.enabled && !entity.isDestroyed) { if (entity.enabled && !entity.isDestroyed) {

View File

@@ -117,8 +117,8 @@ export class IdentifierPool {
if (this._nextAvailableIndex > IdentifierPool.MAX_INDEX) { if (this._nextAvailableIndex > IdentifierPool.MAX_INDEX) {
throw new Error( throw new Error(
`实体索引已达到框架设计限制 (${IdentifierPool.MAX_INDEX})。` + `实体索引已达到框架设计限制 (${IdentifierPool.MAX_INDEX})。` +
`这意味着您已经分配了超过65535个不同的实体索引。` + '这意味着您已经分配了超过65535个不同的实体索引。' +
`这是16位索引设计的限制考虑优化实体回收策略或升级到64位ID设计。` '这是16位索引设计的限制考虑优化实体回收策略或升级到64位ID设计。'
); );
} }
@@ -155,7 +155,7 @@ export class IdentifierPool {
// 检查是否已经在待回收队列中 // 检查是否已经在待回收队列中
const alreadyPending = this._pendingRecycle.some( const alreadyPending = this._pendingRecycle.some(
item => item.index === index && item.generation === generation (item) => item.index === index && item.generation === generation
); );
if (alreadyPending) { if (alreadyPending) {
@@ -217,7 +217,7 @@ export class IdentifierPool {
averageGeneration: number; averageGeneration: number;
/** 世代存储大小 */ /** 世代存储大小 */
generationStorageSize: number; generationStorageSize: number;
} { } {
// 计算平均世代版本 // 计算平均世代版本
let totalGeneration = 0; let totalGeneration = 0;
let generationCount = 0; let generationCount = 0;

View File

@@ -267,15 +267,15 @@ export class Matcher {
const parts: string[] = []; const parts: string[] = [];
if (this.condition.all.length > 0) { if (this.condition.all.length > 0) {
parts.push(`all(${this.condition.all.map(t => getComponentTypeName(t)).join(', ')})`); parts.push(`all(${this.condition.all.map((t) => getComponentTypeName(t)).join(', ')})`);
} }
if (this.condition.any.length > 0) { if (this.condition.any.length > 0) {
parts.push(`any(${this.condition.any.map(t => getComponentTypeName(t)).join(', ')})`); parts.push(`any(${this.condition.any.map((t) => getComponentTypeName(t)).join(', ')})`);
} }
if (this.condition.none.length > 0) { if (this.condition.none.length > 0) {
parts.push(`none(${this.condition.none.map(t => getComponentTypeName(t)).join(', ')})`); parts.push(`none(${this.condition.none.map((t) => getComponentTypeName(t)).join(', ')})`);
} }
if (this.condition.tag !== undefined) { if (this.condition.tag !== undefined) {

View File

@@ -267,7 +267,7 @@ export class SparseSet<T> {
denseArraySize: number; denseArraySize: number;
sparseMapSize: number; sparseMapSize: number;
totalMemory: number; totalMemory: number;
} { } {
const denseArraySize = this._dense.length * 8; // 估计每个引用8字节 const denseArraySize = this._dense.length * 8; // 估计每个引用8字节
const sparseMapSize = this._sparse.size * 16; // 估计每个Map条目16字节 const sparseMapSize = this._sparse.size * 16; // 估计每个Map条目16字节

View File

@@ -408,7 +408,7 @@ export class World {
globalSystemCount: this._globalSystems.length, globalSystemCount: this._globalSystems.length,
createdAt: this._createdAt, createdAt: this._createdAt,
config: { ...this._config }, config: { ...this._config },
scenes: Array.from(this._scenes.keys()).map(sceneId => ({ scenes: Array.from(this._scenes.keys()).map((sceneId) => ({
id: sceneId, id: sceneId,
isActive: this._activeScenes.has(sceneId), isActive: this._activeScenes.has(sceneId),
name: this._scenes.get(sceneId)?.name || sceneId name: this._scenes.get(sceneId)?.name || sceneId

View File

@@ -444,7 +444,7 @@ export class WorldManager implements IService {
// 检查是否所有Scene都是空的 // 检查是否所有Scene都是空的
const allScenes = world.getAllScenes(); const allScenes = world.getAllScenes();
const hasEntities = allScenes.some(scene => const hasEntities = allScenes.some((scene) =>
scene.entities && scene.entities.count > 0 scene.entities && scene.entities.count > 0
); );

View File

@@ -98,7 +98,7 @@ export class PlatformManager {
platformSupportsSharedArrayBuffer: boolean; platformSupportsSharedArrayBuffer: boolean;
platformMaxWorkerCount: number; platformMaxWorkerCount: number;
platformLimitations: any; platformLimitations: any;
} { } {
if (!this.adapter) { if (!this.adapter) {
return { return {
platformSupportsWorker: false, platformSupportsWorker: false,

View File

@@ -208,8 +208,8 @@ export class DebugPlugin implements IPlugin, IService {
return { return {
name: scene.name, name: scene.name,
entityCount: entities.length, entityCount: entities.length,
systems: systems.map(sys => this.getSystemInfo(sys)), systems: systems.map((sys) => this.getSystemInfo(sys)),
entities: entities.map(entity => this.getEntityInfo(entity)) entities: entities.map((entity) => this.getEntityInfo(entity))
}; };
} }
@@ -246,7 +246,7 @@ export class DebugPlugin implements IPlugin, IService {
enabled: entity.enabled, enabled: entity.enabled,
tag: entity.tag, tag: entity.tag,
componentCount: components.length, componentCount: components.length,
components: components.map(comp => this.getComponentInfo(comp)) components: components.map((comp) => this.getComponentInfo(comp))
}; };
} }
@@ -304,7 +304,7 @@ export class DebugPlugin implements IPlugin, IService {
if (filter.hasComponent) { if (filter.hasComponent) {
const hasComp = entity.components.some( const hasComp = entity.components.some(
c => c.constructor.name === filter.hasComponent (c) => c.constructor.name === filter.hasComponent
); );
if (!hasComp) { if (!hasComp) {
continue; continue;

View File

@@ -142,8 +142,8 @@ export interface IEventListenerConfig {
priority?: number; priority?: number;
/** 是否异步执行 */ /** 是否异步执行 */
async?: boolean; async?: boolean;
/** 执行上下文 */ /** 事件处理函数的 this 绑定对象 */
context?: unknown; thisArg?: object;
} }
/** /**

View File

@@ -47,8 +47,8 @@ export class ComponentDataCollector {
}); });
// 获取池利用率信息 // 获取池利用率信息
let poolUtilizations = new Map<string, number>(); const poolUtilizations = new Map<string, number>();
let poolSizes = new Map<string, number>(); const poolSizes = new Map<string, number>();
try { try {
const poolManager = ComponentPoolManager.getInstance(); const poolManager = ComponentPoolManager.getInstance();

View File

@@ -135,7 +135,7 @@ export class DebugManager implements IService, IUpdatable {
* 格式化日志消息 * 格式化日志消息
*/ */
private formatLogMessage(args: unknown[]): string { private formatLogMessage(args: unknown[]): string {
return args.map(arg => { return args.map((arg) => {
if (typeof arg === 'string') return arg; if (typeof arg === 'string') return arg;
if (arg instanceof Error) return `${arg.name}: ${arg.message}`; if (arg instanceof Error) return `${arg.name}: ${arg.message}`;
if (arg === null) return 'null'; if (arg === null) return 'null';
@@ -173,7 +173,7 @@ export class DebugManager implements IService, IUpdatable {
seen.add(value); seen.add(value);
if (Array.isArray(value)) { if (Array.isArray(value)) {
const result = value.map(item => stringify(item, depth + 1)); const result = value.map((item) => stringify(item, depth + 1));
seen.delete(value); seen.delete(value);
return result; return result;
} }
@@ -436,9 +436,6 @@ export class DebugManager implements IService, IUpdatable {
} }
/** /**
* 处理内存快照请求 * 处理内存快照请求
*/ */
@@ -519,7 +516,7 @@ export class DebugManager implements IService, IUpdatable {
jsHeapSizeLimit: number; jsHeapSizeLimit: number;
} | null; } | null;
detailedMemory?: unknown; detailedMemory?: unknown;
} { } {
const memoryInfo = { const memoryInfo = {
totalMemory: 0, totalMemory: 0,
usedMemory: 0, usedMemory: 0,
@@ -575,7 +572,6 @@ export class DebugManager implements IService, IUpdatable {
} }
/** /**
* 收集组件内存统计(仅用于内存快照) * 收集组件内存统计(仅用于内存快照)
*/ */
@@ -672,7 +668,7 @@ export class DebugManager implements IService, IUpdatable {
enabled: boolean; enabled: boolean;
updateOrder: number; updateOrder: number;
}>; }>;
} { } {
const scene = this.sceneManager.currentScene; const scene = this.sceneManager.currentScene;
let totalSystemMemory = 0; let totalSystemMemory = 0;
const systemBreakdown: Array<{ const systemBreakdown: Array<{
@@ -766,7 +762,7 @@ export class DebugManager implements IService, IUpdatable {
utilization: number; utilization: number;
hitRate?: number; hitRate?: number;
}>; }>;
} { } {
let totalPoolMemory = 0; let totalPoolMemory = 0;
const poolBreakdown: Array<{ const poolBreakdown: Array<{
typeName: string; typeName: string;
@@ -845,7 +841,7 @@ export class DebugManager implements IService, IUpdatable {
samples: number; samples: number;
}>; }>;
error?: string; error?: string;
} { } {
try { try {
if (!this.performanceMonitor) { if (!this.performanceMonitor) {
return { enabled: false }; return { enabled: false };

View File

@@ -52,7 +52,6 @@ export class EntityDataCollector {
}; };
} }
/** /**
* 获取原始实体列表 * 获取原始实体列表
* @param scene 场景实例 * @param scene 场景实例
@@ -92,7 +91,6 @@ export class EntityDataCollector {
})); }));
} }
/** /**
* 获取实体详细信息 * 获取实体详细信息
* @param entityId 实体ID * @param entityId 实体ID
@@ -108,9 +106,9 @@ export class EntityDataCollector {
const entity = entityList.buffer.find((e: any) => e.id === entityId); const entity = entityList.buffer.find((e: any) => e.id === entityId);
if (!entity) return null; if (!entity) return null;
const baseDebugInfo = entity.getDebugInfo ? const baseDebugInfo = entity.getDebugInfo
entity.getDebugInfo() : ? entity.getDebugInfo()
this.buildFallbackEntityInfo(entity, scene); : this.buildFallbackEntityInfo(entity, scene);
const componentDetails = this.extractComponentDetails(entity.components); const componentDetails = this.extractComponentDetails(entity.components);
@@ -163,7 +161,6 @@ export class EntityDataCollector {
return { name: sceneName, type: sceneType }; return { name: sceneName, type: sceneType };
} }
/** /**
* 收集实体数据(包含内存信息) * 收集实体数据(包含内存信息)
* @param scene 场景实例 * @param scene 场景实例
@@ -182,17 +179,17 @@ export class EntityDataCollector {
try { try {
stats = entityList.getStats ? entityList.getStats() : this.calculateFallbackEntityStats(entityList); stats = entityList.getStats ? entityList.getStats() : this.calculateFallbackEntityStats(entityList);
} catch (error) { } catch (error) {
return { return {
totalEntities: 0, totalEntities: 0,
activeEntities: 0, activeEntities: 0,
pendingAdd: 0, pendingAdd: 0,
pendingRemove: 0, pendingRemove: 0,
entitiesPerArchetype: [], entitiesPerArchetype: [],
topEntitiesByComponents: [], topEntitiesByComponents: [],
entityHierarchy: [], entityHierarchy: [],
entityDetailsMap: {} entityDetailsMap: {}
}; };
} }
const archetypeData = this.collectArchetypeDataWithMemory(scene); const archetypeData = this.collectArchetypeDataWithMemory(scene);
@@ -208,7 +205,6 @@ export class EntityDataCollector {
}; };
} }
private collectArchetypeData(scene: any): { private collectArchetypeData(scene: any): {
distribution: Array<{ signature: string; count: number; memory: number }>; distribution: Array<{ signature: string; count: number; memory: number }>;
topEntities: Array<{ id: string; name: string; componentCount: number; memory: number }>; topEntities: Array<{ id: string; name: string; componentCount: number; memory: number }>;
@@ -224,7 +220,9 @@ export class EntityDataCollector {
}; };
} }
private getArchetypeDistributionFast(entityContainer: any): Array<{ signature: string; count: number; memory: number }> { private getArchetypeDistributionFast(
entityContainer: any
): Array<{ signature: string; count: number; memory: number }> {
const distribution = new Map<string, { count: number; componentTypes: string[] }>(); const distribution = new Map<string, { count: number; componentTypes: string[] }>();
if (entityContainer && entityContainer.entities) { if (entityContainer && entityContainer.entities) {
@@ -251,7 +249,9 @@ export class EntityDataCollector {
.slice(0, 20); .slice(0, 20);
} }
private getTopEntitiesByComponentsFast(entityContainer: any): Array<{ id: string; name: string; componentCount: number; memory: number }> { private getTopEntitiesByComponentsFast(
entityContainer: any
): Array<{ id: string; name: string; componentCount: number; memory: number }> {
if (!entityContainer || !entityContainer.entities) { if (!entityContainer || !entityContainer.entities) {
return []; return [];
} }
@@ -266,7 +266,6 @@ export class EntityDataCollector {
.sort((a: any, b: any) => b.componentCount - a.componentCount); .sort((a: any, b: any) => b.componentCount - a.componentCount);
} }
private collectArchetypeDataWithMemory(scene: any): { private collectArchetypeDataWithMemory(scene: any): {
distribution: Array<{ signature: string; count: number; memory: number }>; distribution: Array<{ signature: string; count: number; memory: number }>;
topEntities: Array<{ id: string; name: string; componentCount: number; memory: number }>; topEntities: Array<{ id: string; name: string; componentCount: number; memory: number }>;
@@ -282,7 +281,6 @@ export class EntityDataCollector {
}; };
} }
private extractArchetypeStatistics(archetypeSystem: any): { private extractArchetypeStatistics(archetypeSystem: any): {
distribution: Array<{ signature: string; count: number; memory: number }>; distribution: Array<{ signature: string; count: number; memory: number }>;
topEntities: Array<{ id: string; name: string; componentCount: number; memory: number }>; topEntities: Array<{ id: string; name: string; componentCount: number; memory: number }>;
@@ -319,7 +317,6 @@ export class EntityDataCollector {
return { distribution, topEntities }; return { distribution, topEntities };
} }
private extractArchetypeStatisticsWithMemory(archetypeSystem: any): { private extractArchetypeStatisticsWithMemory(archetypeSystem: any): {
distribution: Array<{ signature: string; count: number; memory: number }>; distribution: Array<{ signature: string; count: number; memory: number }>;
topEntities: Array<{ id: string; name: string; componentCount: number; memory: number }>; topEntities: Array<{ id: string; name: string; componentCount: number; memory: number }>;
@@ -368,9 +365,9 @@ export class EntityDataCollector {
return { distribution, topEntities }; return { distribution, topEntities };
} }
private getArchetypeDistributionWithMemory(
entityContainer: any
private getArchetypeDistributionWithMemory(entityContainer: any): Array<{ signature: string; count: number; memory: number }> { ): Array<{ signature: string; count: number; memory: number }> {
const distribution = new Map<string, { count: number; memory: number; componentTypes: string[] }>(); const distribution = new Map<string, { count: number; memory: number; componentTypes: string[] }>();
if (entityContainer && entityContainer.entities) { if (entityContainer && entityContainer.entities) {
@@ -403,8 +400,9 @@ export class EntityDataCollector {
.sort((a, b) => b.count - a.count); .sort((a, b) => b.count - a.count);
} }
private getTopEntitiesByComponentsWithMemory(
private getTopEntitiesByComponentsWithMemory(entityContainer: any): Array<{ id: string; name: string; componentCount: number; memory: number }> { entityContainer: any
): Array<{ id: string; name: string; componentCount: number; memory: number }> {
if (!entityContainer || !entityContainer.entities) { if (!entityContainer || !entityContainer.entities) {
return []; return [];
} }
@@ -419,7 +417,6 @@ export class EntityDataCollector {
.sort((a: any, b: any) => b.componentCount - a.componentCount); .sort((a: any, b: any) => b.componentCount - a.componentCount);
} }
private getEmptyEntityDebugData(): IEntityDebugData { private getEmptyEntityDebugData(): IEntityDebugData {
return { return {
totalEntities: 0, totalEntities: 0,
@@ -433,20 +430,20 @@ export class EntityDataCollector {
}; };
} }
private calculateFallbackEntityStats(entityList: any): any { private calculateFallbackEntityStats(entityList: any): any {
const allEntities = entityList.buffer || []; const allEntities = entityList.buffer || [];
const activeEntities = allEntities.filter((entity: any) => const activeEntities = allEntities.filter((entity: any) => entity.enabled && !entity.isDestroyed);
entity.enabled && !entity._isDestroyed
);
return { return {
totalEntities: allEntities.length, totalEntities: allEntities.length,
activeEntities: activeEntities.length, activeEntities: activeEntities.length,
pendingAdd: 0, pendingAdd: 0,
pendingRemove: 0, pendingRemove: 0,
averageComponentsPerEntity: activeEntities.length > 0 ? averageComponentsPerEntity:
allEntities.reduce((sum: number, e: any) => sum + (e.components?.length || 0), 0) / activeEntities.length : 0 activeEntities.length > 0
? allEntities.reduce((sum: number, e: any) => sum + (e.components?.length || 0), 0) /
activeEntities.length
: 0
}; };
} }
@@ -496,11 +493,14 @@ export class EntityDataCollector {
for (let i = 0; i < maxKeys; i++) { for (let i = 0; i < maxKeys; i++) {
const key = keys[i]; const key = keys[i];
if (!key || excludeKeys.includes(key) || if (
!key ||
excludeKeys.includes(key) ||
key === 'constructor' || key === 'constructor' ||
key === '__proto__' || key === '__proto__' ||
key.startsWith('_cc_') || key.startsWith('_cc_') ||
key.startsWith('__')) { key.startsWith('__')
) {
continue; continue;
} }
@@ -534,7 +534,6 @@ export class EntityDataCollector {
} }
} }
private buildEntityHierarchyTree(entityList: { buffer?: Entity[] }): Array<{ private buildEntityHierarchyTree(entityList: { buffer?: Entity[] }): Array<{
id: number; id: number;
name: string; name: string;
@@ -553,7 +552,6 @@ export class EntityDataCollector {
const rootEntities: any[] = []; const rootEntities: any[] = [];
entityList.buffer.forEach((entity: Entity) => { entityList.buffer.forEach((entity: Entity) => {
if (!entity.parent) { if (!entity.parent) {
const hierarchyNode = this.buildEntityHierarchyNode(entity); const hierarchyNode = this.buildEntityHierarchyNode(entity);
@@ -626,12 +624,13 @@ export class EntityDataCollector {
const batch = entities.slice(i, i + batchSize); const batch = entities.slice(i, i + batchSize);
batch.forEach((entity: Entity) => { batch.forEach((entity: Entity) => {
const baseDebugInfo = entity.getDebugInfo ? const baseDebugInfo = entity.getDebugInfo
entity.getDebugInfo() : ? entity.getDebugInfo()
this.buildFallbackEntityInfo(entity, scene); : this.buildFallbackEntityInfo(entity, scene);
const componentCacheStats = (entity as any).getComponentCacheStats ? const componentCacheStats = (entity as any).getComponentCacheStats
(entity as any).getComponentCacheStats() : null; ? (entity as any).getComponentCacheStats()
: null;
const componentDetails = this.extractComponentDetails(entity.components); const componentDetails = this.extractComponentDetails(entity.components);
@@ -639,13 +638,14 @@ export class EntityDataCollector {
...baseDebugInfo, ...baseDebugInfo,
parentName: entity.parent?.name || null, parentName: entity.parent?.name || null,
components: componentDetails, components: componentDetails,
componentTypes: baseDebugInfo.componentTypes || componentTypes: baseDebugInfo.componentTypes || componentDetails.map((comp) => comp.typeName),
componentDetails.map((comp) => comp.typeName), cachePerformance: componentCacheStats
cachePerformance: componentCacheStats ? { ? {
hitRate: componentCacheStats.cacheStats.hitRate, hitRate: componentCacheStats.cacheStats.hitRate,
size: componentCacheStats.cacheStats.size, size: componentCacheStats.cacheStats.size,
maxSize: componentCacheStats.cacheStats.maxSize maxSize: componentCacheStats.cacheStats.maxSize
} : null }
: null
}; };
}); });
} }
@@ -694,7 +694,7 @@ export class EntityDataCollector {
try { try {
const propertyKeys = Object.keys(component); const propertyKeys = Object.keys(component);
propertyKeys.forEach(propertyKey => { propertyKeys.forEach((propertyKey) => {
if (!propertyKey.startsWith('_') && propertyKey !== 'entity' && propertyKey !== 'constructor') { if (!propertyKey.startsWith('_') && propertyKey !== 'entity' && propertyKey !== 'constructor') {
const propertyValue = (component as any)[propertyKey]; const propertyValue = (component as any)[propertyKey];
if (propertyValue !== undefined && propertyValue !== null) { if (propertyValue !== undefined && propertyValue !== null) {
@@ -726,7 +726,11 @@ export class EntityDataCollector {
* @param componentIndex 组件索引 * @param componentIndex 组件索引
* @param scene 场景实例 * @param scene 场景实例
*/ */
public getComponentProperties(entityId: number, componentIndex: number, scene?: IScene | null): Record<string, any> { public getComponentProperties(
entityId: number,
componentIndex: number,
scene?: IScene | null
): Record<string, any> {
try { try {
if (!scene) return {}; if (!scene) return {};
@@ -739,20 +743,20 @@ export class EntityDataCollector {
const component = entity.components[componentIndex]; const component = entity.components[componentIndex];
const properties: Record<string, any> = {}; const properties: Record<string, any> = {};
const propertyKeys = Object.keys(component); const propertyKeys = Object.keys(component);
propertyKeys.forEach(propertyKey => { propertyKeys.forEach((propertyKey) => {
if (!propertyKey.startsWith('_') && propertyKey !== 'entity') { if (!propertyKey.startsWith('_') && propertyKey !== 'entity') {
const propertyValue = (component as any)[propertyKey]; const propertyValue = (component as any)[propertyKey];
if (propertyValue !== undefined && propertyValue !== null) { if (propertyValue !== undefined && propertyValue !== null) {
properties[propertyKey] = this.formatPropertyValue(propertyValue); properties[propertyKey] = this.formatPropertyValue(propertyValue);
} }
} }
}); });
return properties; return properties;
} catch (error) { } catch (error) {
return { _error: '属性提取失败' }; return { _error: '属性提取失败' };
} }
} }
/** /**
@@ -786,7 +790,7 @@ export class EntityDataCollector {
if (obj.length === 0) return []; if (obj.length === 0) return [];
if (obj.length > 10) { if (obj.length > 10) {
const sample = obj.slice(0, 3).map(item => this.formatPropertyValue(item, 1)); const sample = obj.slice(0, 3).map((item) => this.formatPropertyValue(item, 1));
return { return {
_isLazyArray: true, _isLazyArray: true,
_arrayLength: obj.length, _arrayLength: obj.length,
@@ -795,7 +799,7 @@ export class EntityDataCollector {
}; };
} }
return obj.map(item => this.formatPropertyValue(item, 1)); return obj.map((item) => this.formatPropertyValue(item, 1));
} }
const keys = Object.keys(obj); const keys = Object.keys(obj);
@@ -843,7 +847,7 @@ export class EntityDataCollector {
const typeName = obj.constructor?.name || 'Object'; const typeName = obj.constructor?.name || 'Object';
const summary = this.getObjectSummary(obj, typeName); const summary = this.getObjectSummary(obj, typeName);
return { return {
_isLazyObject: true, _isLazyObject: true,
_typeName: typeName, _typeName: typeName,
_summary: summary, _summary: summary,
@@ -922,7 +926,12 @@ export class EntityDataCollector {
* @param propertyPath 属性路径 * @param propertyPath 属性路径
* @param scene 场景实例 * @param scene 场景实例
*/ */
public expandLazyObject(entityId: number, componentIndex: number, propertyPath: string, scene?: IScene | null): any { public expandLazyObject(
entityId: number,
componentIndex: number,
propertyPath: string,
scene?: IScene | null
): any {
try { try {
if (!scene) return null; if (!scene) return null;

View File

@@ -34,7 +34,7 @@ export class PerformanceDataCollector {
} }
// 计算ECS执行时间统计 // 计算ECS执行时间统计
const history = this.frameTimeHistory.filter(t => t >= 0); const history = this.frameTimeHistory.filter((t) => t >= 0);
const averageECSTime = history.length > 0 ? history.reduce((a, b) => a + b, 0) / history.length : ecsExecutionTimeMs; const averageECSTime = history.length > 0 ? history.reduce((a, b) => a + b, 0) / history.length : ecsExecutionTimeMs;
const minECSTime = history.length > 0 ? Math.min(...history) : ecsExecutionTimeMs; const minECSTime = history.length > 0 ? Math.min(...history) : ecsExecutionTimeMs;
const maxECSTime = history.length > 0 ? Math.max(...history) : ecsExecutionTimeMs; const maxECSTime = history.length > 0 ? Math.max(...history) : ecsExecutionTimeMs;
@@ -100,7 +100,7 @@ export class PerformanceDataCollector {
} }
// 计算各系统占ECS总时间的百分比 // 计算各系统占ECS总时间的百分比
systemBreakdown.forEach(system => { systemBreakdown.forEach((system) => {
system.percentage = totalTime > 0 ? (system.executionTime / totalTime * 100) : 0; system.percentage = totalTime > 0 ? (system.executionTime / totalTime * 100) : 0;
}); });

View File

@@ -112,7 +112,7 @@ export class WebSocketManager {
this.reconnectAttempts++; this.reconnectAttempts++;
this.reconnectTimer = setTimeout(() => { this.reconnectTimer = setTimeout(() => {
this.connect().catch(_error => { this.connect().catch((_error) => {
if (this.reconnectAttempts < this.maxReconnectAttempts) { if (this.reconnectAttempts < this.maxReconnectAttempts) {
this.scheduleReconnect(); this.scheduleReconnect();
} }

View File

@@ -47,9 +47,9 @@ export class Emitter<T, TContext = unknown> {
* @param handler 事件函数 * @param handler 事件函数
*/ */
public removeObserver(eventType: T, handler: Function) { public removeObserver(eventType: T, handler: Function) {
let messageData = this._messageTable.get(eventType); const messageData = this._messageTable.get(eventType);
if (messageData) { if (messageData) {
let index = messageData.findIndex(data => data.func == handler); const index = messageData.findIndex((data) => data.func == handler);
if (index != -1) if (index != -1)
messageData.splice(index, 1); messageData.splice(index, 1);
} }
@@ -61,9 +61,9 @@ export class Emitter<T, TContext = unknown> {
* @param data 事件数据 * @param data 事件数据
*/ */
public emit<TData = unknown>(eventType: T, ...data: TData[]) { public emit<TData = unknown>(eventType: T, ...data: TData[]) {
let list = this._messageTable.get(eventType); const list = this._messageTable.get(eventType);
if (list) { if (list) {
for (let observer of list) { for (const observer of list) {
observer.func.call(observer.context, ...data); observer.func.call(observer.context, ...data);
} }
} }
@@ -75,8 +75,8 @@ export class Emitter<T, TContext = unknown> {
* @param handler 事件函数 * @param handler 事件函数
*/ */
public hasObserver(eventType: T, handler: Function): boolean { public hasObserver(eventType: T, handler: Function): boolean {
let list = this._messageTable.get(eventType); const list = this._messageTable.get(eventType);
return list ? list.some(observer => observer.func === handler) : false; return list ? list.some((observer) => observer.func === handler) : false;
} }
/** /**

View File

@@ -1,5 +1,5 @@
import { Colors, LogLevel } from "./Constants"; import { Colors, LogLevel } from './Constants';
import { ILogger, LoggerColorConfig, LoggerConfig } from "./Types"; import { ILogger, LoggerColorConfig, LoggerConfig } from './Types';
/** /**

View File

@@ -1,6 +1,6 @@
import { ConsoleLogger } from "./ConsoleLogger"; import { ConsoleLogger } from './ConsoleLogger';
import { LogLevel } from "./Constants"; import { LogLevel } from './Constants';
import { ILogger, LoggerColorConfig } from "./Types"; import { ILogger, LoggerColorConfig } from './Types';
/** /**
* 日志管理器 * 日志管理器

View File

@@ -1,4 +1,4 @@
import type { LogLevel } from "./Constants"; import type { LogLevel } from './Constants';
/** /**
* 日志接口 * 日志接口

View File

@@ -276,12 +276,12 @@ export class PerformanceMonitor implements IService {
*/ */
public getPerformanceReport(): string { public getPerformanceReport(): string {
if (!this._isEnabled) { if (!this._isEnabled) {
return "Performance monitoring is disabled."; return 'Performance monitoring is disabled.';
} }
const lines: string[] = []; const lines: string[] = [];
lines.push("=== ECS Performance Report ==="); lines.push('=== ECS Performance Report ===');
lines.push(""); lines.push('');
// 按平均执行时间排序 // 按平均执行时间排序
const sortedSystems = Array.from(this._systemStats.entries()) const sortedSystems = Array.from(this._systemStats.entries())
@@ -300,7 +300,7 @@ export class PerformanceMonitor implements IService {
lines.push(` Per Entity: ${data.averageTimePerEntity.toFixed(4)}ms`); lines.push(` Per Entity: ${data.averageTimePerEntity.toFixed(4)}ms`);
} }
lines.push(""); lines.push('');
} }
// 总体统计 // 总体统计

View File

@@ -30,7 +30,7 @@ export class TimerManager implements IService, IUpdatable {
* @param onTime * @param onTime
*/ */
public schedule<TContext = unknown>(timeInSeconds: number, repeats: boolean, context: TContext, onTime: (timer: ITimer<TContext>)=>void): Timer<TContext> { public schedule<TContext = unknown>(timeInSeconds: number, repeats: boolean, context: TContext, onTime: (timer: ITimer<TContext>)=>void): Timer<TContext> {
let timer = new Timer<TContext>(); const timer = new Timer<TContext>();
timer.initialize(timeInSeconds, repeats, context, onTime); timer.initialize(timeInSeconds, repeats, context, onTime);
this._timers.push(timer as Timer<unknown>); this._timers.push(timer as Timer<unknown>);

View File

@@ -27,7 +27,6 @@ describe('Component - 组件基类测试', () => {
let scene: Scene; let scene: Scene;
beforeEach(() => { beforeEach(() => {
Component._idGenerator = 0;
component = new TestComponent(); component = new TestComponent();
scene = new Scene(); scene = new Scene();
entity = scene.createEntity('TestEntity'); entity = scene.createEntity('TestEntity');
@@ -51,12 +50,11 @@ describe('Component - 组件基类测试', () => {
}); });
test('组件ID应该递增分配', () => { test('组件ID应该递增分配', () => {
const startId = Component._idGenerator;
const component1 = new TestComponent(); const component1 = new TestComponent();
const component2 = new TestComponent(); const component2 = new TestComponent();
expect(component2.id).toBe(component1.id + 1); expect(component2.id).toBe(component1.id + 1);
expect(component1.id).toBeGreaterThanOrEqual(startId); expect(component1.id).toBeGreaterThanOrEqual(0);
}); });
}); });

View File

@@ -50,17 +50,19 @@ class ConcreteEntitySystem extends EntitySystem {
const handler = (event: any) => { const handler = (event: any) => {
this.eventHandlerCallCount++; this.eventHandlerCallCount++;
}; };
this.addEventListener('manual_event', handler); const listenerRef = this.addEventListener('manual_event', handler);
this.removeEventListener('manual_event', handler); if (listenerRef) {
this.removeEventListener('manual_event', listenerRef);
}
} }
// 公开测试方法 // 公开测试方法
public testAddEventListener(eventType: string, handler: (event: any) => void): void { public testAddEventListener(eventType: string, handler: (event: any) => void): string | null {
this.addEventListener(eventType, handler); return this.addEventListener(eventType, handler);
} }
public testRemoveEventListener(eventType: string, handler: (event: any) => void): void { public testRemoveEventListener(eventType: string, listenerRef: string): void {
this.removeEventListener(eventType, handler); this.removeEventListener(eventType, listenerRef);
} }
} }
@@ -118,14 +120,16 @@ describe('EntitySystem', () => {
const handler = jest.fn(); const handler = jest.fn();
// 添加监听器 // 添加监听器
system.testAddEventListener('manual_remove_event', handler); const listenerRef = system.testAddEventListener('manual_remove_event', handler);
// 发射事件验证监听器工作 // 发射事件验证监听器工作
scene.eventSystem.emitSync('manual_remove_event', {}); scene.eventSystem.emitSync('manual_remove_event', {});
expect(handler).toHaveBeenCalledTimes(1); expect(handler).toHaveBeenCalledTimes(1);
// 移除监听器 // 移除监听器
system.testRemoveEventListener('manual_remove_event', handler); if (listenerRef) {
system.testRemoveEventListener('manual_remove_event', listenerRef);
}
// 再次发射事件 // 再次发射事件
scene.eventSystem.emitSync('manual_remove_event', {}); scene.eventSystem.emitSync('manual_remove_event', {});
@@ -205,11 +209,11 @@ describe('EntitySystem', () => {
}); });
it('当移除不存在的监听器时,应该安全处理', () => { it('当移除不存在的监听器时,应该安全处理', () => {
const nonExistentHandler = () => {}; const nonExistentListenerRef = 'non_existent_listener_ref';
// 应该不会抛出错误 // 应该不会抛出错误
expect(() => { expect(() => {
system.testRemoveEventListener('non_existent_event', nonExistentHandler); system.testRemoveEventListener('non_existent_event', nonExistentListenerRef);
}).not.toThrow(); }).not.toThrow();
}); });
}); });

File diff suppressed because it is too large Load Diff

View File

@@ -4,148 +4,148 @@ import { invoke } from '@tauri-apps/api/core';
* Tauri IPC 通信层 * Tauri IPC 通信层
*/ */
export class TauriAPI { export class TauriAPI {
/** /**
* 打招呼(测试命令) * 打招呼(测试命令)
*/ */
static async greet(name: string): Promise<string> { static async greet(name: string): Promise<string> {
return await invoke<string>('greet', { name }); return await invoke<string>('greet', { name });
} }
static async openProjectDialog(): Promise<string | null> { static async openProjectDialog(): Promise<string | null> {
return await invoke<string | null>('open_project_dialog'); return await invoke<string | null>('open_project_dialog');
} }
static async openProject(path: string): Promise<string> { static async openProject(path: string): Promise<string> {
return await invoke<string>('open_project', { path }); return await invoke<string>('open_project', { path });
} }
/** /**
* 保存项目 * 保存项目
*/ */
static async saveProject(path: string, data: string): Promise<void> { static async saveProject(path: string, data: string): Promise<void> {
return await invoke<void>('save_project', { path, data }); return await invoke<void>('save_project', { path, data });
} }
/** /**
* 导出二进制数据 * 导出二进制数据
*/ */
static async exportBinary(data: Uint8Array, outputPath: string): Promise<void> { static async exportBinary(data: Uint8Array, outputPath: string): Promise<void> {
return await invoke<void>('export_binary', { return await invoke<void>('export_binary', {
data: Array.from(data), data: Array.from(data),
outputPath outputPath
}); });
} }
/** /**
* 扫描目录查找匹配模式的文件 * 扫描目录查找匹配模式的文件
*/ */
static async scanDirectory(path: string, pattern: string): Promise<string[]> { static async scanDirectory(path: string, pattern: string): Promise<string[]> {
return await invoke<string[]>('scan_directory', { path, pattern }); return await invoke<string[]>('scan_directory', { path, pattern });
} }
/** /**
* 读取文件内容 * 读取文件内容
*/ */
static async readFileContent(path: string): Promise<string> { static async readFileContent(path: string): Promise<string> {
return await invoke<string>('read_file_content', { path }); return await invoke<string>('read_file_content', { path });
} }
/** /**
* 列出目录内容 * 列出目录内容
*/ */
static async listDirectory(path: string): Promise<DirectoryEntry[]> { static async listDirectory(path: string): Promise<DirectoryEntry[]> {
return await invoke<DirectoryEntry[]>('list_directory', { path }); return await invoke<DirectoryEntry[]>('list_directory', { path });
} }
/** /**
* 设置项目基础路径,用于 Custom Protocol * 设置项目基础路径,用于 Custom Protocol
*/ */
static async setProjectBasePath(path: string): Promise<void> { static async setProjectBasePath(path: string): Promise<void> {
return await invoke<void>('set_project_base_path', { path }); return await invoke<void>('set_project_base_path', { path });
} }
/** /**
* 切换开发者工具仅在debug模式下可用 * 切换开发者工具仅在debug模式下可用
*/ */
static async toggleDevtools(): Promise<void> { static async toggleDevtools(): Promise<void> {
return await invoke<void>('toggle_devtools'); return await invoke<void>('toggle_devtools');
} }
/** /**
* 打开保存场景对话框 * 打开保存场景对话框
* @param defaultName 默认文件名(可选) * @param defaultName 默认文件名(可选)
* @returns 用户选择的文件路径,取消则返回 null * @returns 用户选择的文件路径,取消则返回 null
*/ */
static async saveSceneDialog(defaultName?: string): Promise<string | null> { static async saveSceneDialog(defaultName?: string): Promise<string | null> {
return await invoke<string | null>('save_scene_dialog', { defaultName }); return await invoke<string | null>('save_scene_dialog', { defaultName });
} }
/** /**
* 打开场景文件选择对话框 * 打开场景文件选择对话框
* @returns 用户选择的文件路径,取消则返回 null * @returns 用户选择的文件路径,取消则返回 null
*/ */
static async openSceneDialog(): Promise<string | null> { static async openSceneDialog(): Promise<string | null> {
return await invoke<string | null>('open_scene_dialog'); return await invoke<string | null>('open_scene_dialog');
} }
/** /**
* 创建目录 * 创建目录
* @param path 目录路径 * @param path 目录路径
*/ */
static async createDirectory(path: string): Promise<void> { static async createDirectory(path: string): Promise<void> {
return await invoke<void>('create_directory', { path }); return await invoke<void>('create_directory', { path });
} }
/** /**
* 写入文件内容 * 写入文件内容
* @param path 文件路径 * @param path 文件路径
* @param content 文件内容 * @param content 文件内容
*/ */
static async writeFileContent(path: string, content: string): Promise<void> { static async writeFileContent(path: string, content: string): Promise<void> {
return await invoke<void>('write_file_content', { path, content }); return await invoke<void>('write_file_content', { path, content });
} }
/** /**
* 检查路径是否存在 * 检查路径是否存在
* @param path 文件或目录路径 * @param path 文件或目录路径
* @returns 路径是否存在 * @returns 路径是否存在
*/ */
static async pathExists(path: string): Promise<boolean> { static async pathExists(path: string): Promise<boolean> {
return await invoke<boolean>('path_exists', { path }); return await invoke<boolean>('path_exists', { path });
} }
/** /**
* 使用系统默认程序打开文件 * 使用系统默认程序打开文件
* @param path 文件路径 * @param path 文件路径
*/ */
static async openFileWithSystemApp(path: string): Promise<void> { static async openFileWithSystemApp(path: string): Promise<void> {
await invoke('open_file_with_default_app', { filePath: path }); await invoke('open_file_with_default_app', { filePath: path });
} }
/** /**
* 在文件管理器中显示文件 * 在文件管理器中显示文件
* @param path 文件路径 * @param path 文件路径
*/ */
static async showInFolder(path: string): Promise<void> { static async showInFolder(path: string): Promise<void> {
await invoke('show_in_folder', { filePath: path }); await invoke('show_in_folder', { filePath: path });
} }
/** /**
* 打开行为树文件选择对话框 * 打开行为树文件选择对话框
* @returns 用户选择的文件路径,取消则返回 null * @returns 用户选择的文件路径,取消则返回 null
*/ */
static async openBehaviorTreeDialog(): Promise<string | null> { static async openBehaviorTreeDialog(): Promise<string | null> {
return await invoke<string | null>('open_behavior_tree_dialog'); return await invoke<string | null>('open_behavior_tree_dialog');
} }
/** /**
* 扫描项目中的所有行为树文件 * 扫描项目中的所有行为树文件
* @param projectPath 项目路径 * @param projectPath 项目路径
* @returns 行为树资产ID列表相对于 .ecs/behaviors 的路径,不含扩展名) * @returns 行为树资产ID列表相对于 .ecs/behaviors 的路径,不含扩展名)
*/ */
static async scanBehaviorTrees(projectPath: string): Promise<string[]> { static async scanBehaviorTrees(projectPath: string): Promise<string[]> {
return await invoke<string[]>('scan_behavior_trees', { projectPath }); return await invoke<string[]>('scan_behavior_trees', { projectPath });
} }
} }
export interface DirectoryEntry { export interface DirectoryEntry {

View File

@@ -23,358 +23,358 @@ interface AssetBrowserProps {
} }
export function AssetBrowser({ projectPath, locale, onOpenScene, onOpenBehaviorTree }: AssetBrowserProps) { export function AssetBrowser({ projectPath, locale, onOpenScene, onOpenBehaviorTree }: AssetBrowserProps) {
const [currentPath, setCurrentPath] = useState<string | null>(null); const [currentPath, setCurrentPath] = useState<string | null>(null);
const [selectedPath, setSelectedPath] = useState<string | null>(null); const [selectedPath, setSelectedPath] = useState<string | null>(null);
const [assets, setAssets] = useState<AssetItem[]>([]); const [assets, setAssets] = useState<AssetItem[]>([]);
const [searchQuery, setSearchQuery] = useState(''); const [searchQuery, setSearchQuery] = useState('');
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [contextMenu, setContextMenu] = useState<{ const [contextMenu, setContextMenu] = useState<{
position: { x: number; y: number }; position: { x: number; y: number };
asset: AssetItem; asset: AssetItem;
} | null>(null); } | null>(null);
const translations = { const translations = {
en: { en: {
title: 'Content Browser', title: 'Content Browser',
noProject: 'No project loaded', noProject: 'No project loaded',
loading: 'Loading...', loading: 'Loading...',
empty: 'No assets found', empty: 'No assets found',
search: 'Search...', search: 'Search...',
name: 'Name', name: 'Name',
type: 'Type', type: 'Type',
file: 'File', file: 'File',
folder: 'Folder' folder: 'Folder'
}, },
zh: { zh: {
title: '内容浏览器', title: '内容浏览器',
noProject: '没有加载项目', noProject: '没有加载项目',
loading: '加载中...', loading: '加载中...',
empty: '没有找到资产', empty: '没有找到资产',
search: '搜索...', search: '搜索...',
name: '名称', name: '名称',
type: '类型', type: '类型',
file: '文件', file: '文件',
folder: '文件夹' folder: '文件夹'
}
};
const t = translations[locale as keyof typeof translations] || translations.en;
useEffect(() => {
if (projectPath) {
setCurrentPath(projectPath);
loadAssets(projectPath);
} else {
setAssets([]);
setCurrentPath(null);
setSelectedPath(null);
}
}, [projectPath]);
// Listen for asset reveal requests
useEffect(() => {
const messageHub = Core.services.resolve(MessageHub);
if (!messageHub) return;
const unsubscribe = messageHub.subscribe('asset:reveal', (data: any) => {
const filePath = data.path;
if (filePath) {
setSelectedPath(filePath);
const lastSlashIndex = Math.max(filePath.lastIndexOf('/'), filePath.lastIndexOf('\\'));
const dirPath = lastSlashIndex > 0 ? filePath.substring(0, lastSlashIndex) : null;
if (dirPath) {
setCurrentPath(dirPath);
loadAssets(dirPath);
} }
} };
});
return () => unsubscribe(); const t = translations[locale as keyof typeof translations] || translations.en;
}, []);
const loadAssets = async (path: string) => { useEffect(() => {
setLoading(true); if (projectPath) {
try { setCurrentPath(projectPath);
const entries = await TauriAPI.listDirectory(path); loadAssets(projectPath);
} else {
setAssets([]);
setCurrentPath(null);
setSelectedPath(null);
}
}, [projectPath]);
const assetItems: AssetItem[] = entries.map((entry: DirectoryEntry) => { // Listen for asset reveal requests
const extension = entry.is_dir ? undefined : useEffect(() => {
(entry.name.includes('.') ? entry.name.split('.').pop() : undefined); const messageHub = Core.services.resolve(MessageHub);
if (!messageHub) return;
return { const unsubscribe = messageHub.subscribe('asset:reveal', (data: any) => {
name: entry.name, const filePath = data.path;
path: entry.path, if (filePath) {
type: entry.is_dir ? 'folder' as const : 'file' as const, setSelectedPath(filePath);
extension const lastSlashIndex = Math.max(filePath.lastIndexOf('/'), filePath.lastIndexOf('\\'));
}; const dirPath = lastSlashIndex > 0 ? filePath.substring(0, lastSlashIndex) : null;
}); if (dirPath) {
setCurrentPath(dirPath);
loadAssets(dirPath);
}
}
});
setAssets(assetItems.sort((a, b) => { return () => unsubscribe();
if (a.type === b.type) return a.name.localeCompare(b.name); }, []);
return a.type === 'folder' ? -1 : 1;
}));
} catch (error) {
console.error('Failed to load assets:', error);
setAssets([]);
} finally {
setLoading(false);
}
};
const handleFolderSelect = (path: string) => { const loadAssets = async (path: string) => {
setCurrentPath(path); setLoading(true);
loadAssets(path);
};
const handleAssetClick = (asset: AssetItem) => {
setSelectedPath(asset.path);
};
const handleAssetDoubleClick = async (asset: AssetItem) => {
if (asset.type === 'folder') {
setCurrentPath(asset.path);
loadAssets(asset.path);
} else if (asset.type === 'file') {
if (asset.extension === 'ecs' && onOpenScene) {
onOpenScene(asset.path);
} else if (asset.extension === 'btree' && onOpenBehaviorTree) {
onOpenBehaviorTree(asset.path);
} else {
// 其他文件使用系统默认程序打开
try { try {
await TauriAPI.openFileWithSystemApp(asset.path); 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,
type: entry.is_dir ? 'folder' as const : 'file' as const,
extension
};
});
setAssets(assetItems.sort((a, b) => {
if (a.type === b.type) return a.name.localeCompare(b.name);
return a.type === 'folder' ? -1 : 1;
}));
} catch (error) { } catch (error) {
console.error('Failed to open file:', error); console.error('Failed to load assets:', error);
setAssets([]);
} finally {
setLoading(false);
} }
} };
}
};
const handleContextMenu = (e: React.MouseEvent, asset: AssetItem) => { const handleFolderSelect = (path: string) => {
e.preventDefault(); setCurrentPath(path);
setContextMenu({ loadAssets(path);
position: { x: e.clientX, y: e.clientY }, };
asset
});
};
const getContextMenuItems = (asset: AssetItem): ContextMenuItem[] => { const handleAssetClick = (asset: AssetItem) => {
const items: ContextMenuItem[] = []; setSelectedPath(asset.path);
};
// 打开 const handleAssetDoubleClick = async (asset: AssetItem) => {
if (asset.type === 'file') { if (asset.type === 'folder') {
items.push({ setCurrentPath(asset.path);
label: locale === 'zh' ? '打开' : 'Open', loadAssets(asset.path);
icon: <File size={16} />, } else if (asset.type === 'file') {
onClick: () => handleAssetDoubleClick(asset) if (asset.extension === 'ecs' && onOpenScene) {
}); onOpenScene(asset.path);
} } else if (asset.extension === 'btree' && onOpenBehaviorTree) {
onOpenBehaviorTree(asset.path);
// 在文件管理器中显示 } else {
items.push({ // 其他文件使用系统默认程序打开
label: locale === 'zh' ? '在文件管理器中显示' : 'Show in Explorer', try {
icon: <FolderOpen size={16} />, await TauriAPI.openFileWithSystemApp(asset.path);
onClick: async () => { } catch (error) {
try { console.error('Failed to open file:', error);
await TauriAPI.showInFolder(asset.path); }
} catch (error) { }
console.error('Failed to show in folder:', error);
} }
} };
});
items.push({ label: '', separator: true, onClick: () => {} }); const handleContextMenu = (e: React.MouseEvent, asset: AssetItem) => {
e.preventDefault();
setContextMenu({
position: { x: e.clientX, y: e.clientY },
asset
});
};
// 复制路径 const getContextMenuItems = (asset: AssetItem): ContextMenuItem[] => {
items.push({ const items: ContextMenuItem[] = [];
label: locale === 'zh' ? '复制路径' : 'Copy Path',
icon: <Copy size={16} />,
onClick: () => {
navigator.clipboard.writeText(asset.path);
}
});
items.push({ label: '', separator: true, onClick: () => {} }); // 打开
if (asset.type === 'file') {
items.push({
label: locale === 'zh' ? '打开' : 'Open',
icon: <File size={16} />,
onClick: () => handleAssetDoubleClick(asset)
});
}
// 重命名 // 在文件管理器中显示
items.push({ items.push({
label: locale === 'zh' ? '重命名' : 'Rename', label: locale === 'zh' ? '在文件管理器中显示' : 'Show in Explorer',
icon: <Edit3 size={16} />, icon: <FolderOpen size={16} />,
onClick: () => { onClick: async () => {
// TODO: 实现重命名功能 try {
console.log('Rename:', asset.path); await TauriAPI.showInFolder(asset.path);
}, } catch (error) {
disabled: true console.error('Failed to show in folder:', error);
}); }
}
});
// 删除 items.push({ label: '', separator: true, onClick: () => {} });
items.push({
label: locale === 'zh' ? '删除' : 'Delete',
icon: <Trash2 size={16} />,
onClick: () => {
// TODO: 实现删除功能
console.log('Delete:', asset.path);
},
disabled: true
});
return items; // 复制路径
}; items.push({
label: locale === 'zh' ? '复制路径' : 'Copy Path',
icon: <Copy size={16} />,
onClick: () => {
navigator.clipboard.writeText(asset.path);
}
});
const getBreadcrumbs = () => { items.push({ label: '', separator: true, onClick: () => {} });
if (!currentPath || !projectPath) return [];
const relative = currentPath.replace(projectPath, ''); // 重命名
const parts = relative.split(/[/\\]/).filter(p => p); items.push({
label: locale === 'zh' ? '重命名' : 'Rename',
icon: <Edit3 size={16} />,
onClick: () => {
// TODO: 实现重命名功能
console.log('Rename:', asset.path);
},
disabled: true
});
const crumbs = [{ name: 'Content', path: projectPath }]; // 删除
let accPath = projectPath; items.push({
label: locale === 'zh' ? '删除' : 'Delete',
icon: <Trash2 size={16} />,
onClick: () => {
// TODO: 实现删除功能
console.log('Delete:', asset.path);
},
disabled: true
});
for (const part of parts) { return items;
accPath = `${accPath}${accPath.endsWith('\\') || accPath.endsWith('/') ? '' : '/'}${part}`; };
crumbs.push({ name: part, path: accPath });
}
return crumbs; const getBreadcrumbs = () => {
}; if (!currentPath || !projectPath) return [];
const filteredAssets = searchQuery const relative = currentPath.replace(projectPath, '');
? assets.filter(asset => const parts = relative.split(/[/\\]/).filter((p) => p);
asset.name.toLowerCase().includes(searchQuery.toLowerCase())
)
: assets;
const getFileIcon = (asset: AssetItem) => { const crumbs = [{ name: 'Content', path: projectPath }];
if (asset.type === 'folder') { let accPath = projectPath;
return <Folder className="asset-icon" style={{ color: '#ffa726' }} size={20} />;
}
const ext = asset.extension?.toLowerCase(); for (const part of parts) {
switch (ext) { accPath = `${accPath}${accPath.endsWith('\\') || accPath.endsWith('/') ? '' : '/'}${part}`;
case 'ecs': crumbs.push({ name: part, path: accPath });
return <File className="asset-icon" style={{ color: '#66bb6a' }} size={20} />; }
case 'btree':
return <FileText className="asset-icon" style={{ color: '#ab47bc' }} size={20} />;
case 'ts':
case 'tsx':
case 'js':
case 'jsx':
return <FileCode className="asset-icon" style={{ color: '#42a5f5' }} size={20} />;
case 'json':
return <FileJson className="asset-icon" style={{ color: '#ffa726' }} size={20} />;
case 'png':
case 'jpg':
case 'jpeg':
case 'gif':
return <FileImage className="asset-icon" style={{ color: '#ec407a' }} size={20} />;
default:
return <File className="asset-icon" size={20} />;
}
};
if (!projectPath) { return crumbs;
return ( };
<div className="asset-browser">
<div className="asset-browser-header">
<h3>{t.title}</h3>
</div>
<div className="asset-browser-empty">
<p>{t.noProject}</p>
</div>
</div>
);
}
const breadcrumbs = getBreadcrumbs(); const filteredAssets = searchQuery
? assets.filter((asset) =>
asset.name.toLowerCase().includes(searchQuery.toLowerCase())
)
: assets;
return ( const getFileIcon = (asset: AssetItem) => {
<div className="asset-browser"> if (asset.type === 'folder') {
<div className="asset-browser-header"> return <Folder className="asset-icon" style={{ color: '#ffa726' }} size={20} />;
<h3>{t.title}</h3> }
</div>
<div className="asset-browser-content"> const ext = asset.extension?.toLowerCase();
<ResizablePanel switch (ext) {
direction="horizontal" case 'ecs':
defaultSize={200} return <File className="asset-icon" style={{ color: '#66bb6a' }} size={20} />;
minSize={150} case 'btree':
maxSize={400} return <FileText className="asset-icon" style={{ color: '#ab47bc' }} size={20} />;
leftOrTop={ case 'ts':
<div className="asset-browser-tree"> case 'tsx':
<FileTree case 'js':
rootPath={projectPath} case 'jsx':
onSelectFile={handleFolderSelect} return <FileCode className="asset-icon" style={{ color: '#42a5f5' }} size={20} />;
selectedPath={currentPath} case 'json':
/> return <FileJson className="asset-icon" style={{ color: '#ffa726' }} size={20} />;
</div> case 'png':
} case 'jpg':
rightOrBottom={ case 'jpeg':
<div className="asset-browser-list"> case 'gif':
<div className="asset-browser-breadcrumb"> return <FileImage className="asset-icon" style={{ color: '#ec407a' }} size={20} />;
{breadcrumbs.map((crumb, index) => ( default:
<span key={crumb.path}> return <File className="asset-icon" size={20} />;
<span }
className="breadcrumb-item" };
onClick={() => {
setCurrentPath(crumb.path); if (!projectPath) {
loadAssets(crumb.path); return (
}} <div className="asset-browser">
> <div className="asset-browser-header">
{crumb.name} <h3>{t.title}</h3>
</span>
{index < breadcrumbs.length - 1 && <span className="breadcrumb-separator"> / </span>}
</span>
))}
</div>
<div className="asset-browser-toolbar">
<input
type="text"
className="asset-search"
placeholder={t.search}
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
/>
</div>
{loading ? (
<div className="asset-browser-loading">
<p>{t.loading}</p>
</div> </div>
) : filteredAssets.length === 0 ? (
<div className="asset-browser-empty"> <div className="asset-browser-empty">
<p>{t.empty}</p> <p>{t.noProject}</p>
</div> </div>
) : (
<div className="asset-list">
{filteredAssets.map((asset, index) => (
<div
key={index}
className={`asset-item ${selectedPath === asset.path ? 'selected' : ''}`}
onClick={() => handleAssetClick(asset)}
onDoubleClick={() => handleAssetDoubleClick(asset)}
onContextMenu={(e) => handleContextMenu(e, asset)}
>
{getFileIcon(asset)}
<div className="asset-name" title={asset.name}>
{asset.name}
</div>
<div className="asset-type">
{asset.type === 'folder' ? t.folder : (asset.extension || t.file)}
</div>
</div>
))}
</div>
)}
</div> </div>
} );
/> }
</div>
{contextMenu && ( const breadcrumbs = getBreadcrumbs();
<ContextMenu
items={getContextMenuItems(contextMenu.asset)} return (
position={contextMenu.position} <div className="asset-browser">
onClose={() => setContextMenu(null)} <div className="asset-browser-header">
/> <h3>{t.title}</h3>
)} </div>
</div>
); <div className="asset-browser-content">
<ResizablePanel
direction="horizontal"
defaultSize={200}
minSize={150}
maxSize={400}
leftOrTop={
<div className="asset-browser-tree">
<FileTree
rootPath={projectPath}
onSelectFile={handleFolderSelect}
selectedPath={currentPath}
/>
</div>
}
rightOrBottom={
<div className="asset-browser-list">
<div className="asset-browser-breadcrumb">
{breadcrumbs.map((crumb, index) => (
<span key={crumb.path}>
<span
className="breadcrumb-item"
onClick={() => {
setCurrentPath(crumb.path);
loadAssets(crumb.path);
}}
>
{crumb.name}
</span>
{index < breadcrumbs.length - 1 && <span className="breadcrumb-separator"> / </span>}
</span>
))}
</div>
<div className="asset-browser-toolbar">
<input
type="text"
className="asset-search"
placeholder={t.search}
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
/>
</div>
{loading ? (
<div className="asset-browser-loading">
<p>{t.loading}</p>
</div>
) : filteredAssets.length === 0 ? (
<div className="asset-browser-empty">
<p>{t.empty}</p>
</div>
) : (
<div className="asset-list">
{filteredAssets.map((asset, index) => (
<div
key={index}
className={`asset-item ${selectedPath === asset.path ? 'selected' : ''}`}
onClick={() => handleAssetClick(asset)}
onDoubleClick={() => handleAssetDoubleClick(asset)}
onContextMenu={(e) => handleContextMenu(e, asset)}
>
{getFileIcon(asset)}
<div className="asset-name" title={asset.name}>
{asset.name}
</div>
<div className="asset-type">
{asset.type === 'folder' ? t.folder : (asset.extension || t.file)}
</div>
</div>
))}
</div>
)}
</div>
}
/>
</div>
{contextMenu && (
<ContextMenu
items={getContextMenuItems(contextMenu.asset)}
position={contextMenu.position}
onClose={() => setContextMenu(null)}
/>
)}
</div>
);
} }

View File

@@ -15,127 +15,127 @@ interface AssetPickerProps {
* 用于选择项目中的资产文件 * 用于选择项目中的资产文件
*/ */
export function AssetPicker({ value, onChange, projectPath, filter = 'btree', label }: AssetPickerProps) { export function AssetPicker({ value, onChange, projectPath, filter = 'btree', label }: AssetPickerProps) {
const [assets, setAssets] = useState<string[]>([]); const [assets, setAssets] = useState<string[]>([]);
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
useEffect(() => { useEffect(() => {
if (projectPath) { if (projectPath) {
loadAssets(); 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();
} }
} }, [projectPath]);
} catch (error) {
console.error('Failed to browse asset:', error);
}
};
return ( const loadAssets = async () => {
<div style={{ display: 'flex', flexDirection: 'column', gap: '4px' }}> if (!projectPath) return;
{label && (
<label style={{ fontSize: '11px', color: '#aaa', fontWeight: '500' }}> setLoading(true);
{label} try {
</label> if (filter === 'btree') {
)} const btrees = await TauriAPI.scanBehaviorTrees(projectPath);
<div style={{ display: 'flex', gap: '4px', alignItems: 'center' }}> setAssets(btrees);
<select }
value={value} } catch (error) {
onChange={(e) => onChange(e.target.value)} console.error('Failed to load assets:', error);
disabled={loading || !projectPath} setAssets([]);
style={{ } finally {
flex: 1, setLoading(false);
padding: '4px 8px', }
backgroundColor: '#1e1e1e', };
border: '1px solid #3e3e42',
borderRadius: '3px', const handleBrowse = async () => {
color: '#cccccc', try {
fontSize: '12px', if (filter === 'btree') {
cursor: loading || !projectPath ? 'not-allowed' : 'pointer' const path = await TauriAPI.openBehaviorTreeDialog();
}} if (path && projectPath) {
> const behaviorsPath = `${projectPath}\\.ecs\\behaviors\\`.replace(/\\/g, '\\\\');
<option value="">{loading ? '加载中...' : '选择资产...'}</option> const relativePath = path.replace(behaviorsPath, '')
{assets.map(asset => ( .replace(/\\/g, '/')
<option key={asset} value={asset}> .replace('.btree', '');
{asset} onChange(relativePath);
</option> await loadAssets();
))} }
</select> }
<button } catch (error) {
onClick={loadAssets} console.error('Failed to browse asset:', error);
disabled={loading || !projectPath} }
style={{ };
padding: '4px 8px',
backgroundColor: '#0e639c', return (
border: 'none', <div style={{ display: 'flex', flexDirection: 'column', gap: '4px' }}>
borderRadius: '3px', {label && (
color: '#fff', <label style={{ fontSize: '11px', color: '#aaa', fontWeight: '500' }}>
cursor: loading || !projectPath ? 'not-allowed' : 'pointer', {label}
display: 'flex', </label>
alignItems: 'center', )}
opacity: loading || !projectPath ? 0.5 : 1 <div style={{ display: 'flex', gap: '4px', alignItems: 'center' }}>
}} <select
title="刷新资产列表" value={value}
> onChange={(e) => onChange(e.target.value)}
<RefreshCw size={14} /> disabled={loading || !projectPath}
</button> style={{
<button flex: 1,
onClick={handleBrowse} padding: '4px 8px',
disabled={loading || !projectPath} backgroundColor: '#1e1e1e',
style={{ border: '1px solid #3e3e42',
padding: '4px 8px', borderRadius: '3px',
backgroundColor: '#0e639c', color: '#cccccc',
border: 'none', fontSize: '12px',
borderRadius: '3px', cursor: loading || !projectPath ? 'not-allowed' : 'pointer'
color: '#fff', }}
cursor: loading || !projectPath ? 'not-allowed' : 'pointer', >
display: 'flex', <option value="">{loading ? '加载中...' : '选择资产...'}</option>
alignItems: 'center', {assets.map((asset) => (
opacity: loading || !projectPath ? 0.5 : 1 <option key={asset} value={asset}>
}} {asset}
title="浏览文件..." </option>
> ))}
<Folder size={14} /> </select>
</button> <button
</div> onClick={loadAssets}
{!projectPath && ( disabled={loading || !projectPath}
<div style={{ fontSize: '10px', color: '#ff6b6b', marginTop: '2px' }}> 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> </div>
)} )}
{value && assets.length > 0 && !assets.includes(value) && ( {value && assets.length > 0 && !assets.includes(value) && (
<div style={{ fontSize: '10px', color: '#ffa726', marginTop: '2px' }}> <div style={{ fontSize: '10px', color: '#ffa726', marginTop: '2px' }}>
警告: 资产 "{value}" 警告: 资产 "{value}"
</div>
)}
</div> </div>
)} );
</div>
);
} }

View File

@@ -1,5 +1,5 @@
import { useState, useEffect } from 'react'; import { useState, useEffect } from 'react';
import { X, Folder, File, Search, ArrowLeft, Grid, List, FileCode } from 'lucide-react'; import { X, Folder, Search, ArrowLeft, Grid, List, FileCode } from 'lucide-react';
import { TauriAPI, DirectoryEntry } from '../api/tauri'; import { TauriAPI, DirectoryEntry } from '../api/tauri';
import '../styles/AssetPickerDialog.css'; import '../styles/AssetPickerDialog.css';
@@ -86,7 +86,7 @@ export function AssetPickerDialog({ projectPath, fileExtension, onSelect, onClos
modified: entry.modified modified: entry.modified
}; };
}) })
.filter(item => item.isDir || item.extension === fileExtension) .filter((item) => item.isDir || item.extension === fileExtension)
.sort((a, b) => { .sort((a, b) => {
if (a.isDir === b.isDir) return a.name.localeCompare(b.name); if (a.isDir === b.isDir) return a.name.localeCompare(b.name);
return a.isDir ? -1 : 1; return a.isDir ? -1 : 1;
@@ -102,7 +102,7 @@ export function AssetPickerDialog({ projectPath, fileExtension, onSelect, onClos
}; };
// 过滤搜索结果 // 过滤搜索结果
const filteredAssets = assets.filter(item => const filteredAssets = assets.filter((item) =>
item.name.toLowerCase().includes(searchQuery.toLowerCase()) item.name.toLowerCase().includes(searchQuery.toLowerCase())
); );
@@ -189,7 +189,7 @@ export function AssetPickerDialog({ projectPath, fileExtension, onSelect, onClos
const currentPathNormalized = currentPath.replace(/\\/g, '/'); const currentPathNormalized = currentPath.replace(/\\/g, '/');
const relative = currentPathNormalized.replace(basePathNormalized, ''); const relative = currentPathNormalized.replace(basePathNormalized, '');
const parts = relative.split('/').filter(p => p); const parts = relative.split('/').filter((p) => p);
// 根路径名称(显示"行为树"或"Assets" // 根路径名称(显示"行为树"或"Assets"
const rootName = assetBasePath const rootName = assetBasePath

View File

@@ -167,7 +167,7 @@ export const BehaviorTreeBlackboard: React.FC<BehaviorTreeBlackboardProps> = ({
}; };
const toggleGroup = (groupName: string) => { const toggleGroup = (groupName: string) => {
setCollapsedGroups(prev => { setCollapsedGroups((prev) => {
const newSet = new Set(prev); const newSet = new Set(prev);
if (newSet.has(groupName)) { if (newSet.has(groupName)) {
newSet.delete(groupName); newSet.delete(groupName);
@@ -416,7 +416,7 @@ export const BehaviorTreeBlackboard: React.FC<BehaviorTreeBlackboardProps> = ({
</div> </div>
)} )}
{groupNames.map(groupName => { {groupNames.map((groupName) => {
const isCollapsed = collapsedGroups.has(groupName); const isCollapsed = collapsedGroups.has(groupName);
const groupVars = groupedVariables[groupName]; const groupVars = groupedVariables[groupName];
@@ -451,246 +451,246 @@ export const BehaviorTreeBlackboard: React.FC<BehaviorTreeBlackboardProps> = ({
)} )}
{!isCollapsed && groupVars.map(({ fullKey: key, varName, value }) => { {!isCollapsed && groupVars.map(({ fullKey: key, varName, value }) => {
const type = getVariableType(value); const type = getVariableType(value);
const isEditing = editingKey === key; const isEditing = editingKey === key;
const handleDragStart = (e: React.DragEvent) => { const handleDragStart = (e: React.DragEvent) => {
const variableData = { const variableData = {
variableName: key, variableName: key,
variableValue: value, variableValue: value,
variableType: type variableType: type
}; };
e.dataTransfer.setData('application/blackboard-variable', JSON.stringify(variableData)); e.dataTransfer.setData('application/blackboard-variable', JSON.stringify(variableData));
e.dataTransfer.effectAllowed = 'copy'; e.dataTransfer.effectAllowed = 'copy';
}; };
const typeColor = const typeColor =
type === 'number' ? '#4ec9b0' : type === 'number' ? '#4ec9b0' :
type === 'boolean' ? '#569cd6' : type === 'boolean' ? '#569cd6' :
type === 'object' ? '#ce9178' : '#d4d4d4'; type === 'object' ? '#ce9178' : '#d4d4d4';
const displayValue = type === 'object' ? const displayValue = type === 'object' ?
JSON.stringify(value) : JSON.stringify(value) :
String(value); String(value);
const truncatedValue = displayValue.length > 30 ? const truncatedValue = displayValue.length > 30 ?
displayValue.substring(0, 30) + '...' : displayValue.substring(0, 30) + '...' :
displayValue; displayValue;
return ( return (
<div <div
key={key} key={key}
draggable={!isEditing} draggable={!isEditing}
onDragStart={handleDragStart} onDragStart={handleDragStart}
style={{
marginBottom: '6px',
padding: '6px 8px',
backgroundColor: '#2d2d2d',
borderRadius: '3px',
borderLeft: `3px solid ${typeColor}`,
cursor: isEditing ? 'default' : 'grab'
}}
>
{isEditing ? (
<div>
<div style={{
fontSize: '10px',
color: '#666',
marginBottom: '4px'
}}>
Name
</div>
<input
type="text"
value={editingNewKey}
onChange={(e) => setEditingNewKey(e.target.value)}
style={{ style={{
width: '100%', marginBottom: '6px',
padding: '4px', padding: '6px 8px',
marginBottom: '4px', backgroundColor: '#2d2d2d',
backgroundColor: '#1e1e1e', borderRadius: '3px',
border: '1px solid #3c3c3c', borderLeft: `3px solid ${typeColor}`,
borderRadius: '2px', cursor: isEditing ? 'default' : 'grab'
color: '#9cdcfe',
fontSize: '11px',
fontFamily: 'monospace'
}}
placeholder="Variable name (e.g., player.health)"
/>
<div style={{
fontSize: '10px',
color: '#666',
marginBottom: '4px'
}}>
Type
</div>
<select
value={editType}
onChange={(e) => setEditType(e.target.value as BlackboardVariable['type'])}
style={{
width: '100%',
padding: '4px',
marginBottom: '4px',
backgroundColor: '#1e1e1e',
border: '1px solid #3c3c3c',
borderRadius: '2px',
color: '#cccccc',
fontSize: '10px'
}} }}
> >
<option value="string">String</option> {isEditing ? (
<option value="number">Number</option> <div>
<option value="boolean">Boolean</option> <div style={{
<option value="object">Object (JSON)</option> fontSize: '10px',
</select> color: '#666',
<div style={{ marginBottom: '4px'
fontSize: '10px', }}>
color: '#666', Name
marginBottom: '4px' </div>
}}> <input
type="text"
value={editingNewKey}
onChange={(e) => setEditingNewKey(e.target.value)}
style={{
width: '100%',
padding: '4px',
marginBottom: '4px',
backgroundColor: '#1e1e1e',
border: '1px solid #3c3c3c',
borderRadius: '2px',
color: '#9cdcfe',
fontSize: '11px',
fontFamily: 'monospace'
}}
placeholder="Variable name (e.g., player.health)"
/>
<div style={{
fontSize: '10px',
color: '#666',
marginBottom: '4px'
}}>
Type
</div>
<select
value={editType}
onChange={(e) => setEditType(e.target.value as BlackboardVariable['type'])}
style={{
width: '100%',
padding: '4px',
marginBottom: '4px',
backgroundColor: '#1e1e1e',
border: '1px solid #3c3c3c',
borderRadius: '2px',
color: '#cccccc',
fontSize: '10px'
}}
>
<option value="string">String</option>
<option value="number">Number</option>
<option value="boolean">Boolean</option>
<option value="object">Object (JSON)</option>
</select>
<div style={{
fontSize: '10px',
color: '#666',
marginBottom: '4px'
}}>
Value Value
</div> </div>
<textarea <textarea
value={editValue} value={editValue}
onChange={(e) => setEditValue(e.target.value)} onChange={(e) => setEditValue(e.target.value)}
style={{ style={{
width: '100%', width: '100%',
minHeight: editType === 'object' ? '60px' : '24px', minHeight: editType === 'object' ? '60px' : '24px',
padding: '4px', padding: '4px',
backgroundColor: '#1e1e1e', backgroundColor: '#1e1e1e',
border: '1px solid #0e639c', border: '1px solid #0e639c',
borderRadius: '2px', borderRadius: '2px',
color: '#cccccc', color: '#cccccc',
fontSize: '11px', fontSize: '11px',
fontFamily: 'monospace', fontFamily: 'monospace',
resize: 'vertical', resize: 'vertical',
marginBottom: '4px' marginBottom: '4px'
}} }}
/> />
<div style={{ display: 'flex', gap: '4px' }}> <div style={{ display: 'flex', gap: '4px' }}>
<button <button
onClick={() => handleSaveEdit(key)} onClick={() => handleSaveEdit(key)}
style={{ style={{
padding: '3px 8px', padding: '3px 8px',
backgroundColor: '#0e639c', backgroundColor: '#0e639c',
border: 'none', border: 'none',
borderRadius: '2px', borderRadius: '2px',
color: '#fff', color: '#fff',
cursor: 'pointer', cursor: 'pointer',
fontSize: '10px' fontSize: '10px'
}} }}
> >
Save Save
</button> </button>
<button <button
onClick={() => setEditingKey(null)} onClick={() => setEditingKey(null)}
style={{ style={{
padding: '3px 8px', padding: '3px 8px',
backgroundColor: '#3c3c3c', backgroundColor: '#3c3c3c',
border: 'none', border: 'none',
borderRadius: '2px', borderRadius: '2px',
color: '#ccc', color: '#ccc',
cursor: 'pointer', cursor: 'pointer',
fontSize: '10px' fontSize: '10px'
}} }}
> >
Cancel Cancel
</button> </button>
</div> </div>
</div> </div>
) : ( ) : (
<div style={{ <div style={{
display: 'flex', display: 'flex',
justifyContent: 'space-between', justifyContent: 'space-between',
alignItems: 'center', alignItems: 'center',
gap: '8px' gap: '8px'
}}> }}>
<div style={{ flex: 1, minWidth: 0 }}> <div style={{ flex: 1, minWidth: 0 }}>
<div style={{ <div style={{
fontSize: '11px', fontSize: '11px',
color: '#9cdcfe', color: '#9cdcfe',
fontWeight: 'bold', fontWeight: 'bold',
overflow: 'hidden', overflow: 'hidden',
textOverflow: 'ellipsis', textOverflow: 'ellipsis',
whiteSpace: 'nowrap', whiteSpace: 'nowrap',
display: 'flex', display: 'flex',
alignItems: 'center', alignItems: 'center',
gap: '4px' gap: '4px'
}}> }}>
{varName} <span style={{ {varName} <span style={{
color: '#666', color: '#666',
fontWeight: 'normal', fontWeight: 'normal',
fontSize: '10px' fontSize: '10px'
}}>({type})</span> }}>({type})</span>
{viewMode === 'local' && isModified(key) && ( {viewMode === 'local' && isModified(key) && (
<span style={{ <span style={{
fontSize: '9px', fontSize: '9px',
color: '#ffbb00', color: '#ffbb00',
backgroundColor: 'rgba(255, 187, 0, 0.15)', backgroundColor: 'rgba(255, 187, 0, 0.15)',
padding: '1px 4px', padding: '1px 4px',
borderRadius: '2px' borderRadius: '2px'
}} title="运行时修改的值,停止后会恢复"> }} title="运行时修改的值,停止后会恢复">
</span> </span>
)} )}
</div> </div>
<div style={{ <div style={{
fontSize: '10px', fontSize: '10px',
fontFamily: 'monospace', fontFamily: 'monospace',
color: typeColor, color: typeColor,
marginTop: '2px', marginTop: '2px',
overflow: 'hidden', overflow: 'hidden',
textOverflow: 'ellipsis', textOverflow: 'ellipsis',
whiteSpace: 'nowrap', whiteSpace: 'nowrap',
backgroundColor: (viewMode === 'local' && isModified(key)) ? 'rgba(255, 187, 0, 0.1)' : 'transparent', backgroundColor: (viewMode === 'local' && isModified(key)) ? 'rgba(255, 187, 0, 0.1)' : 'transparent',
padding: '1px 3px', padding: '1px 3px',
borderRadius: '2px' borderRadius: '2px'
}} title={(viewMode === 'local' && isModified(key)) ? `初始值: ${JSON.stringify(initialVariables?.[key])}\n当前值: ${displayValue}` : displayValue}> }} title={(viewMode === 'local' && isModified(key)) ? `初始值: ${JSON.stringify(initialVariables?.[key])}\n当前值: ${displayValue}` : displayValue}>
{truncatedValue} {truncatedValue}
</div> </div>
</div>
<div style={{
display: 'flex',
gap: '2px',
flexShrink: 0
}}>
<button
onClick={() => handleStartEdit(key, value)}
style={{
padding: '2px',
backgroundColor: 'transparent',
border: 'none',
color: '#ccc',
cursor: 'pointer',
display: 'flex',
alignItems: 'center',
justifyContent: 'center'
}}
title="Edit"
>
<Edit2 size={12} />
</button>
<button
onClick={() => currentOnDelete && currentOnDelete(key)}
style={{
padding: '2px',
backgroundColor: 'transparent',
border: 'none',
color: '#f44336',
cursor: 'pointer',
display: 'flex',
alignItems: 'center',
justifyContent: 'center'
}}
title="Delete"
>
<Trash2 size={12} />
</button>
</div>
</div>
)}
</div> </div>
<div style={{ );
display: 'flex',
gap: '2px',
flexShrink: 0
}}>
<button
onClick={() => handleStartEdit(key, value)}
style={{
padding: '2px',
backgroundColor: 'transparent',
border: 'none',
color: '#ccc',
cursor: 'pointer',
display: 'flex',
alignItems: 'center',
justifyContent: 'center'
}}
title="Edit"
>
<Edit2 size={12} />
</button>
<button
onClick={() => currentOnDelete && currentOnDelete(key)}
style={{
padding: '2px',
backgroundColor: 'transparent',
border: 'none',
color: '#f44336',
cursor: 'pointer',
display: 'flex',
alignItems: 'center',
justifyContent: 'center'
}}
title="Delete"
>
<Trash2 size={12} />
</button>
</div>
</div>
)}
</div>
);
})} })}
</div> </div>
); );
@@ -753,8 +753,8 @@ export const BehaviorTreeBlackboard: React.FC<BehaviorTreeBlackboardProps> = ({
<textarea <textarea
placeholder={ placeholder={
newType === 'object' ? '{"key": "value"}' : newType === 'object' ? '{"key": "value"}' :
newType === 'boolean' ? 'true or false' : newType === 'boolean' ? 'true or false' :
newType === 'number' ? '0' : 'value' newType === 'number' ? '0' : 'value'
} }
value={newValue} value={newValue}
onChange={(e) => setNewValue(e.target.value)} onChange={(e) => setNewValue(e.target.value)}

File diff suppressed because it is too large Load Diff

View File

@@ -1,5 +1,5 @@
import React, { useEffect, useRef, useState } from 'react'; import React, { useEffect, useRef, useState } from 'react';
import { Play, Pause, Square, RotateCcw, Trash2, Copy } from 'lucide-react'; import { Trash2, Copy } from 'lucide-react';
interface ExecutionLog { interface ExecutionLog {
timestamp: number; timestamp: number;
@@ -59,14 +59,14 @@ export const BehaviorTreeExecutionPanel: React.FC<BehaviorTreeExecutionPanelProp
}; };
const handleCopyLogs = () => { const handleCopyLogs = () => {
const logsText = logs.map(log => const logsText = logs.map((log) =>
`${formatTime(log.timestamp)} ${getLevelIcon(log.level)} ${log.message}` `${formatTime(log.timestamp)} ${getLevelIcon(log.level)} ${log.message}`
).join('\n'); ).join('\n');
navigator.clipboard.writeText(logsText).then(() => { navigator.clipboard.writeText(logsText).then(() => {
setCopySuccess(true); setCopySuccess(true);
setTimeout(() => setCopySuccess(false), 2000); setTimeout(() => setCopySuccess(false), 2000);
}).catch(err => { }).catch((err) => {
console.error('复制失败:', err); console.error('复制失败:', err);
}); });
}; };

View File

@@ -99,15 +99,15 @@ export const BehaviorTreeNodePalette: React.FC<BehaviorTreeNodePaletteProps> = (
// 按类别分组(排除根节点类别) // 按类别分组(排除根节点类别)
const categories = useMemo(() => const categories = useMemo(() =>
['all', ...new Set(allTemplates ['all', ...new Set(allTemplates
.filter(t => t.category !== '根节点') .filter((t) => t.category !== '根节点')
.map(t => t.category))] .map((t) => t.category))]
, [allTemplates]); , [allTemplates]);
const filteredTemplates = useMemo(() => const filteredTemplates = useMemo(() =>
(selectedCategory === 'all' (selectedCategory === 'all'
? allTemplates ? allTemplates
: allTemplates.filter(t => t.category === selectedCategory)) : allTemplates.filter((t) => t.category === selectedCategory))
.filter(t => t.category !== '根节点') .filter((t) => t.category !== '根节点')
, [allTemplates, selectedCategory]); , [allTemplates, selectedCategory]);
const handleNodeClick = (template: NodeTemplate) => { const handleNodeClick = (template: NodeTemplate) => {
@@ -158,7 +158,7 @@ export const BehaviorTreeNodePalette: React.FC<BehaviorTreeNodePaletteProps> = (
flexWrap: 'wrap', flexWrap: 'wrap',
gap: '5px' gap: '5px'
}}> }}>
{categories.map(category => ( {categories.map((category) => (
<button <button
key={category} key={category}
onClick={() => setSelectedCategory(category)} onClick={() => setSelectedCategory(category)}
@@ -245,22 +245,22 @@ export const BehaviorTreeNodePalette: React.FC<BehaviorTreeNodePaletteProps> = (
)} )}
</div> </div>
</div> </div>
<div style={{ <div style={{
fontSize: '12px', fontSize: '12px',
color: '#999', color: '#999',
lineHeight: '1.4', lineHeight: '1.4',
pointerEvents: 'none' pointerEvents: 'none'
}}> }}>
{template.description} {template.description}
</div> </div>
<div style={{ <div style={{
marginTop: '5px', marginTop: '5px',
fontSize: '11px', fontSize: '11px',
color: '#666', color: '#666',
pointerEvents: 'none' pointerEvents: 'none'
}}> }}>
{template.category} {template.category}
</div> </div>
</div> </div>
); );
})} })}

View File

@@ -1,7 +1,7 @@
import { useState, useEffect, useRef } from 'react'; import { useState, useEffect, useRef } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { TreePine, X, Settings, Clipboard, Save, FolderOpen, Maximize2, Minimize2, Download, FilePlus } from 'lucide-react'; import { TreePine, X, Settings, Clipboard, Save, FolderOpen, Maximize2, Minimize2, Download, FilePlus } from 'lucide-react';
import { save, open, ask, message } from '@tauri-apps/plugin-dialog'; import { open, ask, message } from '@tauri-apps/plugin-dialog';
import { invoke } from '@tauri-apps/api/core'; import { invoke } from '@tauri-apps/api/core';
import { Core } from '@esengine/ecs-framework'; import { Core } from '@esengine/ecs-framework';
import { BehaviorTreeEditor } from './BehaviorTreeEditor'; import { BehaviorTreeEditor } from './BehaviorTreeEditor';
@@ -124,7 +124,7 @@ export const BehaviorTreeWindow: React.FC<BehaviorTreeWindowProps> = ({
const globalBlackboard = Core.services.resolve(GlobalBlackboardService); const globalBlackboard = Core.services.resolve(GlobalBlackboardService);
const allVars = globalBlackboard.getAllVariables(); const allVars = globalBlackboard.getAllVariables();
const varsObject: Record<string, any> = {}; const varsObject: Record<string, any> = {};
allVars.forEach(v => { allVars.forEach((v) => {
varsObject[v.name] = v.value; varsObject[v.name] = v.value;
}); });
setGlobalVariables(varsObject); setGlobalVariables(varsObject);
@@ -276,7 +276,7 @@ export const BehaviorTreeWindow: React.FC<BehaviorTreeWindowProps> = ({
const allVars = Core.services.resolve(GlobalBlackboardService).getAllVariables(); const allVars = Core.services.resolve(GlobalBlackboardService).getAllVariables();
const varsObject: Record<string, any> = {}; const varsObject: Record<string, any> = {};
allVars.forEach(v => { allVars.forEach((v) => {
varsObject[v.name] = v.value; varsObject[v.name] = v.value;
}); });
setGlobalVariables(varsObject); setGlobalVariables(varsObject);
@@ -309,7 +309,7 @@ export const BehaviorTreeWindow: React.FC<BehaviorTreeWindowProps> = ({
globalBlackboard.setValue(key, value, true); globalBlackboard.setValue(key, value, true);
const allVars = globalBlackboard.getAllVariables(); const allVars = globalBlackboard.getAllVariables();
const varsObject: Record<string, any> = {}; const varsObject: Record<string, any> = {};
allVars.forEach(v => { allVars.forEach((v) => {
varsObject[v.name] = v.value; varsObject[v.name] = v.value;
}); });
setGlobalVariables(varsObject); setGlobalVariables(varsObject);
@@ -321,7 +321,7 @@ export const BehaviorTreeWindow: React.FC<BehaviorTreeWindowProps> = ({
globalBlackboard.defineVariable(key, type, value); globalBlackboard.defineVariable(key, type, value);
const allVars = globalBlackboard.getAllVariables(); const allVars = globalBlackboard.getAllVariables();
const varsObject: Record<string, any> = {}; const varsObject: Record<string, any> = {};
allVars.forEach(v => { allVars.forEach((v) => {
varsObject[v.name] = v.value; varsObject[v.name] = v.value;
}); });
setGlobalVariables(varsObject); setGlobalVariables(varsObject);
@@ -333,7 +333,7 @@ export const BehaviorTreeWindow: React.FC<BehaviorTreeWindowProps> = ({
globalBlackboard.removeVariable(key); globalBlackboard.removeVariable(key);
const allVars = globalBlackboard.getAllVariables(); const allVars = globalBlackboard.getAllVariables();
const varsObject: Record<string, any> = {}; const varsObject: Record<string, any> = {};
allVars.forEach(v => { allVars.forEach((v) => {
varsObject[v.name] = v.value; varsObject[v.name] = v.value;
}); });
setGlobalVariables(varsObject); setGlobalVariables(varsObject);
@@ -352,7 +352,7 @@ export const BehaviorTreeWindow: React.FC<BehaviorTreeWindowProps> = ({
} }
} }
let saveFilePath = currentFilePath; const saveFilePath = currentFilePath;
// 如果没有当前文件路径,打开自定义保存对话框 // 如果没有当前文件路径,打开自定义保存对话框
if (!saveFilePath) { if (!saveFilePath) {
@@ -788,7 +788,7 @@ export const BehaviorTreeWindow: React.FC<BehaviorTreeWindowProps> = ({
<button <button
onClick={() => setIsFullscreen(!isFullscreen)} onClick={() => setIsFullscreen(!isFullscreen)}
className="behavior-tree-toolbar-btn" className="behavior-tree-toolbar-btn"
title={isFullscreen ? "退出全屏" : "全屏"} title={isFullscreen ? '退出全屏' : '全屏'}
> >
{isFullscreen ? <Minimize2 size={16} /> : <Maximize2 size={16} />} {isFullscreen ? <Minimize2 size={16} /> : <Maximize2 size={16} />}
</button> </button>
@@ -827,7 +827,7 @@ export const BehaviorTreeWindow: React.FC<BehaviorTreeWindowProps> = ({
const varName = node.data.variableName || ''; const varName = node.data.variableName || '';
const varValue = blackboardVariables[varName]; const varValue = blackboardVariables[varName];
const varType = typeof varValue === 'number' ? 'number' : const varType = typeof varValue === 'number' ? 'number' :
typeof varValue === 'boolean' ? 'boolean' : 'string'; typeof varValue === 'boolean' ? 'boolean' : 'string';
data = { data = {
...node.data, ...node.data,
@@ -915,7 +915,7 @@ export const BehaviorTreeWindow: React.FC<BehaviorTreeWindowProps> = ({
if (selectedNode.data.nodeType === 'blackboard-variable' && propertyName === 'variableName') { if (selectedNode.data.nodeType === 'blackboard-variable' && propertyName === 'variableName') {
const newVarValue = blackboardVariables[value]; const newVarValue = blackboardVariables[value];
const newVarType = typeof newVarValue === 'number' ? 'number' : const newVarType = typeof newVarValue === 'number' ? 'number' :
typeof newVarValue === 'boolean' ? 'boolean' : 'string'; typeof newVarValue === 'boolean' ? 'boolean' : 'string';
updateNodes((nodes: any) => nodes.map((node: any) => { updateNodes((nodes: any) => nodes.map((node: any) => {
if (node.id === selectedNode.id) { if (node.id === selectedNode.id) {

View File

@@ -11,27 +11,27 @@ interface ConfirmDialogProps {
} }
export function ConfirmDialog({ title, message, confirmText, cancelText, onConfirm, onCancel }: ConfirmDialogProps) { export function ConfirmDialog({ title, message, confirmText, cancelText, onConfirm, onCancel }: ConfirmDialogProps) {
return ( return (
<div className="confirm-dialog-overlay" onClick={onCancel}> <div className="confirm-dialog-overlay" onClick={onCancel}>
<div className="confirm-dialog" onClick={(e) => e.stopPropagation()}> <div className="confirm-dialog" onClick={(e) => e.stopPropagation()}>
<div className="confirm-dialog-header"> <div className="confirm-dialog-header">
<h2>{title}</h2> <h2>{title}</h2>
<button className="close-btn" onClick={onCancel}> <button className="close-btn" onClick={onCancel}>
<X size={16} /> <X size={16} />
</button> </button>
</div>
<div className="confirm-dialog-content">
<p>{message}</p>
</div>
<div className="confirm-dialog-footer">
<button className="confirm-dialog-btn cancel" onClick={onCancel}>
{cancelText}
</button>
<button className="confirm-dialog-btn confirm" onClick={onConfirm}>
{confirmText}
</button>
</div>
</div>
</div> </div>
<div className="confirm-dialog-content"> );
<p>{message}</p>
</div>
<div className="confirm-dialog-footer">
<button className="confirm-dialog-btn cancel" onClick={onCancel}>
{cancelText}
</button>
<button className="confirm-dialog-btn confirm" onClick={onConfirm}>
{confirmText}
</button>
</div>
</div>
</div>
);
} }

View File

@@ -162,7 +162,7 @@ export function ConsolePanel({ logService }: ConsolePanelProps) {
setLogs(logService.getLogs().slice(-MAX_LOGS)); setLogs(logService.getLogs().slice(-MAX_LOGS));
const unsubscribe = logService.subscribe((entry) => { const unsubscribe = logService.subscribe((entry) => {
setLogs(prev => { setLogs((prev) => {
const newLogs = [...prev, entry]; const newLogs = [...prev, entry];
if (newLogs.length > MAX_LOGS) { if (newLogs.length > MAX_LOGS) {
return newLogs.slice(-MAX_LOGS); return newLogs.slice(-MAX_LOGS);
@@ -316,7 +316,7 @@ export function ConsolePanel({ logService }: ConsolePanelProps) {
} }
// 清理不再需要的缓存(日志被删除) // 清理不再需要的缓存(日志被删除)
const logIds = new Set(logs.map(log => log.id)); const logIds = new Set(logs.map((log) => log.id));
for (const cachedId of cache.keys()) { for (const cachedId of cache.keys()) {
if (!logIds.has(cachedId)) { if (!logIds.has(cachedId)) {
cache.delete(cachedId); cache.delete(cachedId);
@@ -327,7 +327,7 @@ export function ConsolePanel({ logService }: ConsolePanelProps) {
}, [logs, extractJSON]); }, [logs, extractJSON]);
const filteredLogs = useMemo(() => { const filteredLogs = useMemo(() => {
return logs.filter(log => { return logs.filter((log) => {
if (!levelFilter.has(log.level)) return false; if (!levelFilter.has(log.level)) return false;
if (showRemoteOnly && log.source !== 'remote') return false; if (showRemoteOnly && log.source !== 'remote') return false;
if (filter && !log.message.toLowerCase().includes(filter.toLowerCase())) { if (filter && !log.message.toLowerCase().includes(filter.toLowerCase())) {
@@ -357,14 +357,14 @@ export function ConsolePanel({ logService }: ConsolePanelProps) {
}; };
const levelCounts = useMemo(() => ({ const levelCounts = useMemo(() => ({
[LogLevel.Debug]: logs.filter(l => l.level === LogLevel.Debug).length, [LogLevel.Debug]: logs.filter((l) => l.level === LogLevel.Debug).length,
[LogLevel.Info]: logs.filter(l => l.level === LogLevel.Info).length, [LogLevel.Info]: logs.filter((l) => l.level === LogLevel.Info).length,
[LogLevel.Warn]: logs.filter(l => l.level === LogLevel.Warn).length, [LogLevel.Warn]: logs.filter((l) => l.level === LogLevel.Warn).length,
[LogLevel.Error]: logs.filter(l => l.level === LogLevel.Error || l.level === LogLevel.Fatal).length [LogLevel.Error]: logs.filter((l) => l.level === LogLevel.Error || l.level === LogLevel.Fatal).length
}), [logs]); }), [logs]);
const remoteLogCount = useMemo(() => const remoteLogCount = useMemo(() =>
logs.filter(l => l.source === 'remote').length logs.filter((l) => l.source === 'remote').length
, [logs]); , [logs]);
return ( return (
@@ -442,7 +442,7 @@ export function ConsolePanel({ logService }: ConsolePanelProps) {
<p>No logs to display</p> <p>No logs to display</p>
</div> </div>
) : ( ) : (
filteredLogs.map(log => ( filteredLogs.map((log) => (
<LogEntryItem <LogEntryItem
key={log.id} key={log.id}
log={log} log={log}

View File

@@ -16,85 +16,85 @@ interface ContextMenuProps {
} }
export function ContextMenu({ items, position, onClose }: ContextMenuProps) { export function ContextMenu({ items, position, onClose }: ContextMenuProps) {
const menuRef = useRef<HTMLDivElement>(null); const menuRef = useRef<HTMLDivElement>(null);
const [adjustedPosition, setAdjustedPosition] = useState(position); const [adjustedPosition, setAdjustedPosition] = useState(position);
useEffect(() => { useEffect(() => {
if (menuRef.current) { if (menuRef.current) {
const menu = menuRef.current; const menu = menuRef.current;
const rect = menu.getBoundingClientRect(); const rect = menu.getBoundingClientRect();
const viewportWidth = window.innerWidth; const viewportWidth = window.innerWidth;
const viewportHeight = window.innerHeight; const viewportHeight = window.innerHeight;
let x = position.x; let x = position.x;
let y = position.y; let y = position.y;
if (x + rect.width > viewportWidth) { if (x + rect.width > viewportWidth) {
x = Math.max(0, viewportWidth - rect.width - 10); x = Math.max(0, viewportWidth - rect.width - 10);
} }
if (y + rect.height > viewportHeight) { if (y + rect.height > viewportHeight) {
y = Math.max(0, viewportHeight - rect.height - 10); y = Math.max(0, viewportHeight - rect.height - 10);
} }
if (x !== position.x || y !== position.y) { if (x !== position.x || y !== position.y) {
setAdjustedPosition({ x, y }); setAdjustedPosition({ x, y });
} }
}
}, [position]);
useEffect(() => {
const handleClickOutside = (e: MouseEvent) => {
if (menuRef.current && !menuRef.current.contains(e.target as Node)) {
onClose();
}
};
const handleEscape = (e: KeyboardEvent) => {
if (e.key === 'Escape') {
onClose();
}
};
document.addEventListener('mousedown', handleClickOutside);
document.addEventListener('keydown', handleEscape);
return () => {
document.removeEventListener('mousedown', handleClickOutside);
document.removeEventListener('keydown', handleEscape);
};
}, [onClose]);
return (
<div
ref={menuRef}
className="context-menu"
style={{
left: `${adjustedPosition.x}px`,
top: `${adjustedPosition.y}px`
}}
>
{items.map((item, index) => {
if (item.separator) {
return <div key={index} className="context-menu-separator" />;
} }
}, [position]);
return ( useEffect(() => {
<div const handleClickOutside = (e: MouseEvent) => {
key={index} if (menuRef.current && !menuRef.current.contains(e.target as Node)) {
className={`context-menu-item ${item.disabled ? 'disabled' : ''}`}
onClick={() => {
if (!item.disabled) {
item.onClick();
onClose(); onClose();
} }
};
const handleEscape = (e: KeyboardEvent) => {
if (e.key === 'Escape') {
onClose();
}
};
document.addEventListener('mousedown', handleClickOutside);
document.addEventListener('keydown', handleEscape);
return () => {
document.removeEventListener('mousedown', handleClickOutside);
document.removeEventListener('keydown', handleEscape);
};
}, [onClose]);
return (
<div
ref={menuRef}
className="context-menu"
style={{
left: `${adjustedPosition.x}px`,
top: `${adjustedPosition.y}px`
}} }}
> >
{item.icon && <span className="context-menu-icon">{item.icon}</span>} {items.map((item, index) => {
<span className="context-menu-label">{item.label}</span> if (item.separator) {
</div> return <div key={index} className="context-menu-separator" />;
); }
})}
</div> return (
); <div
key={index}
className={`context-menu-item ${item.disabled ? 'disabled' : ''}`}
onClick={() => {
if (!item.disabled) {
item.onClick();
onClose();
}
}}
>
{item.icon && <span className="context-menu-icon">{item.icon}</span>}
<span className="context-menu-label">{item.label}</span>
</div>
);
})}
</div>
);
} }

File diff suppressed because it is too large Load Diff

View File

@@ -8,24 +8,24 @@ interface ErrorDialogProps {
} }
export function ErrorDialog({ title, message, onClose }: ErrorDialogProps) { export function ErrorDialog({ title, message, onClose }: ErrorDialogProps) {
return ( return (
<div className="error-dialog-overlay" onClick={onClose}> <div className="error-dialog-overlay" onClick={onClose}>
<div className="error-dialog" onClick={(e) => e.stopPropagation()}> <div className="error-dialog" onClick={(e) => e.stopPropagation()}>
<div className="error-dialog-header"> <div className="error-dialog-header">
<h2>{title}</h2> <h2>{title}</h2>
<button className="close-btn" onClick={onClose}> <button className="close-btn" onClick={onClose}>
<X size={16} /> <X size={16} />
</button> </button>
</div> </div>
<div className="error-dialog-content"> <div className="error-dialog-content">
<p>{message}</p> <p>{message}</p>
</div> </div>
<div className="error-dialog-footer"> <div className="error-dialog-footer">
<button className="error-dialog-btn" onClick={onClose}> <button className="error-dialog-btn" onClick={onClose}>
</button> </button>
</div>
</div>
</div> </div>
</div> );
</div>
);
} }

View File

@@ -1,5 +1,5 @@
import { useState, useEffect } from 'react'; import { useState, useEffect } from 'react';
import { X, FileJson, Binary, Info, File, FolderTree, FolderOpen, Code } from 'lucide-react'; import { X, File, FolderTree, FolderOpen } from 'lucide-react';
import { open } from '@tauri-apps/plugin-dialog'; import { open } from '@tauri-apps/plugin-dialog';
import '../styles/ExportRuntimeDialog.css'; import '../styles/ExportRuntimeDialog.css';
@@ -66,7 +66,7 @@ export const ExportRuntimeDialog: React.FC<ExportRuntimeDialogProps> = ({
setSelectAll(true); setSelectAll(true);
const newFormats = new Map<string, 'json' | 'binary'>(); const newFormats = new Map<string, 'json' | 'binary'>();
availableFiles.forEach(file => { availableFiles.forEach((file) => {
newFormats.set(file, 'binary'); newFormats.set(file, 'binary');
}); });
setFileFormats(newFormats); setFileFormats(newFormats);

View File

@@ -19,146 +19,146 @@ interface FileTreeProps {
} }
export function FileTree({ rootPath, onSelectFile, selectedPath }: FileTreeProps) { export function FileTree({ rootPath, onSelectFile, selectedPath }: FileTreeProps) {
const [tree, setTree] = useState<TreeNode[]>([]); const [tree, setTree] = useState<TreeNode[]>([]);
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
useEffect(() => { useEffect(() => {
if (rootPath) { if (rootPath) {
loadRootDirectory(rootPath); loadRootDirectory(rootPath);
} else {
setTree([]);
}
}, [rootPath]);
const loadRootDirectory = async (path: string) => {
setLoading(true);
try {
const entries = await TauriAPI.listDirectory(path);
const children = entriesToNodes(entries);
// 创建根节点
const rootName = path.split(/[/\\]/).filter(p => p).pop() || 'Project';
const rootNode: TreeNode = {
name: rootName,
path: path,
type: 'folder',
children: children,
expanded: true,
loaded: true
};
setTree([rootNode]);
} catch (error) {
console.error('Failed to load directory:', error);
setTree([]);
} finally {
setLoading(false);
}
};
const entriesToNodes = (entries: DirectoryEntry[]): TreeNode[] => {
// 只显示文件夹,过滤掉文件
return entries
.filter(entry => entry.is_dir)
.map(entry => ({
name: entry.name,
path: entry.path,
type: 'folder' as const,
children: [],
expanded: false,
loaded: false
}));
};
const loadChildren = async (node: TreeNode): Promise<TreeNode[]> => {
try {
const entries = await TauriAPI.listDirectory(node.path);
return entriesToNodes(entries);
} catch (error) {
console.error('Failed to load children:', error);
return [];
}
};
const toggleNode = async (nodePath: string) => {
const updateTree = async (nodes: TreeNode[]): Promise<TreeNode[]> => {
const newNodes: TreeNode[] = [];
for (const node of nodes) {
if (node.path === nodePath) {
if (!node.loaded) {
const children = await loadChildren(node);
newNodes.push({
...node,
expanded: true,
loaded: true,
children
});
} else {
newNodes.push({
...node,
expanded: !node.expanded
});
}
} else if (node.children) {
newNodes.push({
...node,
children: await updateTree(node.children)
});
} else { } else {
newNodes.push(node); setTree([]);
}
}, [rootPath]);
const loadRootDirectory = async (path: string) => {
setLoading(true);
try {
const entries = await TauriAPI.listDirectory(path);
const children = entriesToNodes(entries);
// 创建根节点
const rootName = path.split(/[/\\]/).filter((p) => p).pop() || 'Project';
const rootNode: TreeNode = {
name: rootName,
path: path,
type: 'folder',
children: children,
expanded: true,
loaded: true
};
setTree([rootNode]);
} catch (error) {
console.error('Failed to load directory:', error);
setTree([]);
} finally {
setLoading(false);
} }
}
return newNodes;
}; };
const newTree = await updateTree(tree); const entriesToNodes = (entries: DirectoryEntry[]): TreeNode[] => {
setTree(newTree); // 只显示文件夹,过滤掉文件
}; return entries
.filter((entry) => entry.is_dir)
.map((entry) => ({
name: entry.name,
path: entry.path,
type: 'folder' as const,
children: [],
expanded: false,
loaded: false
}));
};
const handleNodeClick = (node: TreeNode) => { const loadChildren = async (node: TreeNode): Promise<TreeNode[]> => {
onSelectFile?.(node.path); try {
toggleNode(node.path); const entries = await TauriAPI.listDirectory(node.path);
}; return entriesToNodes(entries);
} catch (error) {
console.error('Failed to load children:', error);
return [];
}
};
const renderNode = (node: TreeNode, level: number = 0) => { const toggleNode = async (nodePath: string) => {
const isSelected = selectedPath === node.path; const updateTree = async (nodes: TreeNode[]): Promise<TreeNode[]> => {
const indent = level * 16; const newNodes: TreeNode[] = [];
for (const node of nodes) {
if (node.path === nodePath) {
if (!node.loaded) {
const children = await loadChildren(node);
newNodes.push({
...node,
expanded: true,
loaded: true,
children
});
} else {
newNodes.push({
...node,
expanded: !node.expanded
});
}
} else if (node.children) {
newNodes.push({
...node,
children: await updateTree(node.children)
});
} else {
newNodes.push(node);
}
}
return newNodes;
};
const newTree = await updateTree(tree);
setTree(newTree);
};
const handleNodeClick = (node: TreeNode) => {
onSelectFile?.(node.path);
toggleNode(node.path);
};
const renderNode = (node: TreeNode, level: number = 0) => {
const isSelected = selectedPath === node.path;
const indent = level * 16;
return (
<div key={node.path}>
<div
className={`tree-node ${isSelected ? 'selected' : ''}`}
style={{ paddingLeft: `${indent}px` }}
onClick={() => handleNodeClick(node)}
>
<span className="tree-arrow">
{node.expanded ? <ChevronDown size={14} /> : <ChevronRight size={14} />}
</span>
<span className="tree-icon">
<Folder size={16} />
</span>
<span className="tree-label">{node.name}</span>
</div>
{node.expanded && node.children && (
<div className="tree-children">
{node.children.map((child) => renderNode(child, level + 1))}
</div>
)}
</div>
);
};
if (loading) {
return <div className="file-tree loading">Loading...</div>;
}
if (!rootPath || tree.length === 0) {
return <div className="file-tree empty">No folders</div>;
}
return ( return (
<div key={node.path}> <div className="file-tree">
<div {tree.map((node) => renderNode(node))}
className={`tree-node ${isSelected ? 'selected' : ''}`}
style={{ paddingLeft: `${indent}px` }}
onClick={() => handleNodeClick(node)}
>
<span className="tree-arrow">
{node.expanded ? <ChevronDown size={14} /> : <ChevronRight size={14} />}
</span>
<span className="tree-icon">
<Folder size={16} />
</span>
<span className="tree-label">{node.name}</span>
</div> </div>
{node.expanded && node.children && (
<div className="tree-children">
{node.children.map(child => renderNode(child, level + 1))}
</div>
)}
</div>
); );
};
if (loading) {
return <div className="file-tree loading">Loading...</div>;
}
if (!rootPath || tree.length === 0) {
return <div className="file-tree empty">No folders</div>;
}
return (
<div className="file-tree">
{tree.map(node => renderNode(node))}
</div>
);
} }

View File

@@ -1,4 +1,4 @@
import { useRef, useCallback, ReactNode, useMemo } from 'react'; import { useCallback, ReactNode, useMemo } from 'react';
import { Layout, Model, TabNode, IJsonModel, Actions, IJsonTabSetNode, IJsonRowNode } from 'flexlayout-react'; import { Layout, Model, TabNode, IJsonModel, Actions, IJsonTabSetNode, IJsonRowNode } from 'flexlayout-react';
import 'flexlayout-react/style/light.css'; import 'flexlayout-react/style/light.css';
import '../styles/FlexLayoutDock.css'; import '../styles/FlexLayoutDock.css';
@@ -16,146 +16,146 @@ interface FlexLayoutDockContainerProps {
} }
export function FlexLayoutDockContainer({ panels, onPanelClose }: FlexLayoutDockContainerProps) { export function FlexLayoutDockContainer({ panels, onPanelClose }: FlexLayoutDockContainerProps) {
const createDefaultLayout = useCallback((): IJsonModel => { const createDefaultLayout = useCallback((): IJsonModel => {
const leftPanels = panels.filter(p => p.id.includes('hierarchy')); const leftPanels = panels.filter((p) => p.id.includes('hierarchy'));
const rightPanels = panels.filter(p => p.id.includes('inspector')); const rightPanels = panels.filter((p) => p.id.includes('inspector'));
const bottomPanels = panels.filter(p => p.id.includes('console') || p.id.includes('asset')) const bottomPanels = panels.filter((p) => p.id.includes('console') || p.id.includes('asset'))
.sort((a, b) => { .sort((a, b) => {
// 控制台排在前面 // 控制台排在前面
if (a.id.includes('console')) return -1; if (a.id.includes('console')) return -1;
if (b.id.includes('console')) return 1; if (b.id.includes('console')) return 1;
return 0; return 0;
}); });
const centerPanels = panels.filter(p => const centerPanels = panels.filter((p) =>
!leftPanels.includes(p) && !rightPanels.includes(p) && !bottomPanels.includes(p) !leftPanels.includes(p) && !rightPanels.includes(p) && !bottomPanels.includes(p)
); );
// Build center column children // Build center column children
const centerColumnChildren: (IJsonTabSetNode | IJsonRowNode)[] = []; const centerColumnChildren: (IJsonTabSetNode | IJsonRowNode)[] = [];
if (centerPanels.length > 0) { if (centerPanels.length > 0) {
centerColumnChildren.push({ centerColumnChildren.push({
type: 'tabset', type: 'tabset',
weight: 70, weight: 70,
children: centerPanels.map(p => ({ children: centerPanels.map((p) => ({
type: 'tab', type: 'tab',
name: p.title, name: p.title,
id: p.id, id: p.id,
component: p.id, component: p.id,
enableClose: p.closable !== false, enableClose: p.closable !== false
})), }))
}); });
} }
if (bottomPanels.length > 0) { if (bottomPanels.length > 0) {
centerColumnChildren.push({ centerColumnChildren.push({
type: 'tabset', type: 'tabset',
weight: 30, weight: 30,
children: bottomPanels.map(p => ({ children: bottomPanels.map((p) => ({
type: 'tab', type: 'tab',
name: p.title, name: p.title,
id: p.id, id: p.id,
component: p.id, component: p.id,
enableClose: p.closable !== false, enableClose: p.closable !== false
})), }))
}); });
}
// Build main row children
const mainRowChildren: (IJsonTabSetNode | IJsonRowNode)[] = [];
if (leftPanels.length > 0) {
mainRowChildren.push({
type: 'tabset',
weight: 20,
children: leftPanels.map(p => ({
type: 'tab',
name: p.title,
id: p.id,
component: p.id,
enableClose: p.closable !== false,
})),
});
}
if (centerColumnChildren.length > 0) {
if (centerColumnChildren.length === 1) {
const centerChild = centerColumnChildren[0];
if (centerChild && centerChild.type === 'tabset') {
mainRowChildren.push({
type: 'tabset',
weight: 60,
children: centerChild.children
} as IJsonTabSetNode);
} else if (centerChild) {
mainRowChildren.push({
type: 'row',
weight: 60,
children: centerChild.children
} as IJsonRowNode);
} }
} else {
mainRowChildren.push({
type: 'row',
weight: 60,
children: centerColumnChildren,
});
}
}
if (rightPanels.length > 0) {
mainRowChildren.push({
type: 'tabset',
weight: 20,
children: rightPanels.map(p => ({
type: 'tab',
name: p.title,
id: p.id,
component: p.id,
enableClose: p.closable !== false,
})),
});
}
return { // Build main row children
global: { const mainRowChildren: (IJsonTabSetNode | IJsonRowNode)[] = [];
tabEnableClose: true, if (leftPanels.length > 0) {
tabEnableRename: false, mainRowChildren.push({
tabSetEnableMaximize: false, type: 'tabset',
tabSetEnableDrop: true, weight: 20,
tabSetEnableDrag: true, children: leftPanels.map((p) => ({
tabSetEnableDivide: true, type: 'tab',
borderEnableDrop: true, name: p.title,
}, id: p.id,
borders: [], component: p.id,
layout: { enableClose: p.closable !== false
type: 'row', }))
weight: 100, });
children: mainRowChildren, }
}, if (centerColumnChildren.length > 0) {
}; if (centerColumnChildren.length === 1) {
}, [panels]); const centerChild = centerColumnChildren[0];
if (centerChild && centerChild.type === 'tabset') {
mainRowChildren.push({
type: 'tabset',
weight: 60,
children: centerChild.children
} as IJsonTabSetNode);
} else if (centerChild) {
mainRowChildren.push({
type: 'row',
weight: 60,
children: centerChild.children
} as IJsonRowNode);
}
} else {
mainRowChildren.push({
type: 'row',
weight: 60,
children: centerColumnChildren
});
}
}
if (rightPanels.length > 0) {
mainRowChildren.push({
type: 'tabset',
weight: 20,
children: rightPanels.map((p) => ({
type: 'tab',
name: p.title,
id: p.id,
component: p.id,
enableClose: p.closable !== false
}))
});
}
const model = useMemo(() => Model.fromJson(createDefaultLayout()), [createDefaultLayout]); return {
global: {
tabEnableClose: true,
tabEnableRename: false,
tabSetEnableMaximize: false,
tabSetEnableDrop: true,
tabSetEnableDrag: true,
tabSetEnableDivide: true,
borderEnableDrop: true
},
borders: [],
layout: {
type: 'row',
weight: 100,
children: mainRowChildren
}
};
}, [panels]);
const factory = useCallback((node: TabNode) => { const model = useMemo(() => Model.fromJson(createDefaultLayout()), [createDefaultLayout]);
const component = node.getComponent();
const panel = panels.find(p => p.id === component);
return panel?.content || <div>Panel not found</div>;
}, [panels]);
const onAction = useCallback((action: any) => { const factory = useCallback((node: TabNode) => {
if (action.type === Actions.DELETE_TAB) { const component = node.getComponent();
const tabId = action.data.node; const panel = panels.find((p) => p.id === component);
if (onPanelClose) { return panel?.content || <div>Panel not found</div>;
onPanelClose(tabId); }, [panels]);
}
}
return action;
}, [onPanelClose]);
return ( const onAction = useCallback((action: any) => {
<div className="flexlayout-dock-container"> if (action.type === Actions.DELETE_TAB) {
<Layout const tabId = action.data.node;
model={model} if (onPanelClose) {
factory={factory} onPanelClose(tabId);
onAction={onAction} }
/> }
</div> return action;
); }, [onPanelClose]);
return (
<div className="flexlayout-dock-container">
<Layout
model={model}
factory={factory}
onAction={onAction}
/>
</div>
);
} }

View File

@@ -36,274 +36,274 @@ interface MenuBarProps {
} }
export function MenuBar({ export function MenuBar({
locale = 'en', locale = 'en',
uiRegistry, uiRegistry,
messageHub, messageHub,
pluginManager, pluginManager,
onNewScene, onNewScene,
onOpenScene, onOpenScene,
onSaveScene, onSaveScene,
onSaveSceneAs, onSaveSceneAs,
onOpenProject, onOpenProject,
onCloseProject, onCloseProject,
onExit, onExit,
onOpenPluginManager, onOpenPluginManager,
onOpenProfiler, onOpenProfiler: _onOpenProfiler,
onOpenPortManager, onOpenPortManager,
onOpenSettings, onOpenSettings,
onToggleDevtools, onToggleDevtools,
onOpenAbout, onOpenAbout,
onCreatePlugin onCreatePlugin
}: MenuBarProps) { }: MenuBarProps) {
const [openMenu, setOpenMenu] = useState<string | null>(null); const [openMenu, setOpenMenu] = useState<string | null>(null);
const [pluginMenuItems, setPluginMenuItems] = useState<PluginMenuItem[]>([]); const [pluginMenuItems, setPluginMenuItems] = useState<PluginMenuItem[]>([]);
const menuRef = useRef<HTMLDivElement>(null); const menuRef = useRef<HTMLDivElement>(null);
const updateMenuItems = () => { const updateMenuItems = () => {
if (uiRegistry && pluginManager) { if (uiRegistry && pluginManager) {
const items = uiRegistry.getChildMenus('window'); const items = uiRegistry.getChildMenus('window');
// 过滤掉被禁用插件的菜单项 // 过滤掉被禁用插件的菜单项
const enabledPlugins = pluginManager.getAllPluginMetadata() const enabledPlugins = pluginManager.getAllPluginMetadata()
.filter(p => p.enabled) .filter((p) => p.enabled)
.map(p => p.name); .map((p) => p.name);
// 只显示启用插件的菜单项 // 只显示启用插件的菜单项
const filteredItems = items.filter(item => { const filteredItems = items.filter((item) => {
// 检查菜单项是否属于某个插件 // 检查菜单项是否属于某个插件
return enabledPlugins.some(pluginName => { return enabledPlugins.some((pluginName) => {
const plugin = pluginManager.getEditorPlugin(pluginName); const plugin = pluginManager.getEditorPlugin(pluginName);
if (plugin && plugin.registerMenuItems) { if (plugin && plugin.registerMenuItems) {
const pluginMenus = plugin.registerMenuItems(); const pluginMenus = plugin.registerMenuItems();
return pluginMenus.some(m => m.id === item.id); return pluginMenus.some((m) => m.id === item.id);
} }
return false; return false;
}); });
}); });
setPluginMenuItems(filteredItems); setPluginMenuItems(filteredItems);
console.log('[MenuBar] Updated menu items:', filteredItems); console.log('[MenuBar] Updated menu items:', filteredItems);
} else if (uiRegistry) { } else if (uiRegistry) {
// 如果没有 pluginManager显示所有菜单项 // 如果没有 pluginManager显示所有菜单项
const items = uiRegistry.getChildMenus('window'); const items = uiRegistry.getChildMenus('window');
setPluginMenuItems(items); setPluginMenuItems(items);
console.log('[MenuBar] Updated menu items (no filter):', items); console.log('[MenuBar] Updated menu items (no filter):', items);
} }
};
useEffect(() => {
updateMenuItems();
}, [uiRegistry, pluginManager]);
useEffect(() => {
if (messageHub) {
const unsubscribeInstalled = messageHub.subscribe('plugin:installed', () => {
console.log('[MenuBar] Plugin installed, updating menu items');
updateMenuItems();
});
const unsubscribeEnabled = messageHub.subscribe('plugin:enabled', () => {
console.log('[MenuBar] Plugin enabled, updating menu items');
updateMenuItems();
});
const unsubscribeDisabled = messageHub.subscribe('plugin:disabled', () => {
console.log('[MenuBar] Plugin disabled, updating menu items');
updateMenuItems();
});
return () => {
unsubscribeInstalled();
unsubscribeEnabled();
unsubscribeDisabled();
};
}
}, [messageHub, uiRegistry, pluginManager]);
const t = (key: string) => {
const translations: Record<string, Record<string, string>> = {
en: {
file: 'File',
newScene: 'New Scene',
openScene: 'Open Scene',
saveScene: 'Save Scene',
saveSceneAs: 'Save Scene As...',
openProject: 'Open Project',
closeProject: 'Close Project',
exit: 'Exit',
edit: 'Edit',
undo: 'Undo',
redo: 'Redo',
cut: 'Cut',
copy: 'Copy',
paste: 'Paste',
delete: 'Delete',
selectAll: 'Select All',
window: 'Window',
sceneHierarchy: 'Scene Hierarchy',
inspector: 'Inspector',
assets: 'Assets',
console: 'Console',
viewport: 'Viewport',
pluginManager: 'Plugin Manager',
tools: 'Tools',
createPlugin: 'Create Plugin',
portManager: 'Port Manager',
settings: 'Settings',
help: 'Help',
documentation: 'Documentation',
checkForUpdates: 'Check for Updates',
about: 'About',
devtools: 'Developer Tools'
},
zh: {
file: '文件',
newScene: '新建场景',
openScene: '打开场景',
saveScene: '保存场景',
saveSceneAs: '场景另存为...',
openProject: '打开项目',
closeProject: '关闭项目',
exit: '退出',
edit: '编辑',
undo: '撤销',
redo: '重做',
cut: '剪切',
copy: '复制',
paste: '粘贴',
delete: '删除',
selectAll: '全选',
window: '窗口',
sceneHierarchy: '场景层级',
inspector: '检视器',
assets: '资产',
console: '控制台',
viewport: '视口',
pluginManager: '插件管理器',
tools: '工具',
createPlugin: '创建插件',
portManager: '端口管理器',
settings: '设置',
help: '帮助',
documentation: '文档',
checkForUpdates: '检查更新',
about: '关于',
devtools: '开发者工具'
}
};
return translations[locale]?.[key] || key;
};
const menus: Record<string, MenuItem[]> = {
file: [
{ label: t('newScene'), shortcut: 'Ctrl+N', onClick: onNewScene },
{ label: t('openScene'), shortcut: 'Ctrl+O', onClick: onOpenScene },
{ separator: true },
{ label: t('saveScene'), shortcut: 'Ctrl+S', onClick: onSaveScene },
{ label: t('saveSceneAs'), shortcut: 'Ctrl+Shift+S', onClick: onSaveSceneAs },
{ separator: true },
{ label: t('openProject'), onClick: onOpenProject },
{ label: t('closeProject'), onClick: onCloseProject },
{ separator: true },
{ label: t('exit'), onClick: onExit }
],
edit: [
{ label: t('undo'), shortcut: 'Ctrl+Z', disabled: true },
{ label: t('redo'), shortcut: 'Ctrl+Y', disabled: true },
{ separator: true },
{ label: t('cut'), shortcut: 'Ctrl+X', disabled: true },
{ label: t('copy'), shortcut: 'Ctrl+C', disabled: true },
{ label: t('paste'), shortcut: 'Ctrl+V', disabled: true },
{ label: t('delete'), shortcut: 'Delete', disabled: true },
{ separator: true },
{ label: t('selectAll'), shortcut: 'Ctrl+A', disabled: true }
],
window: [
...pluginMenuItems.map(item => ({
label: item.label || '',
icon: item.icon,
disabled: item.disabled,
onClick: item.onClick
})),
...(pluginMenuItems.length > 0 ? [{ separator: true } as MenuItem] : []),
{ label: t('pluginManager'), onClick: onOpenPluginManager },
{ separator: true },
{ label: t('devtools'), onClick: onToggleDevtools }
],
tools: [
{ label: t('createPlugin'), onClick: onCreatePlugin },
{ separator: true },
{ label: t('portManager'), onClick: onOpenPortManager },
{ separator: true },
{ label: t('settings'), onClick: onOpenSettings }
],
help: [
{ label: t('documentation'), disabled: true },
{ separator: true },
{ label: t('checkForUpdates'), onClick: onOpenAbout },
{ label: t('about'), onClick: onOpenAbout }
]
};
useEffect(() => {
const handleClickOutside = (event: MouseEvent) => {
if (menuRef.current && !menuRef.current.contains(event.target as Node)) {
setOpenMenu(null);
}
}; };
document.addEventListener('mousedown', handleClickOutside); useEffect(() => {
return () => { updateMenuItems();
document.removeEventListener('mousedown', handleClickOutside); }, [uiRegistry, pluginManager]);
useEffect(() => {
if (messageHub) {
const unsubscribeInstalled = messageHub.subscribe('plugin:installed', () => {
console.log('[MenuBar] Plugin installed, updating menu items');
updateMenuItems();
});
const unsubscribeEnabled = messageHub.subscribe('plugin:enabled', () => {
console.log('[MenuBar] Plugin enabled, updating menu items');
updateMenuItems();
});
const unsubscribeDisabled = messageHub.subscribe('plugin:disabled', () => {
console.log('[MenuBar] Plugin disabled, updating menu items');
updateMenuItems();
});
return () => {
unsubscribeInstalled();
unsubscribeEnabled();
unsubscribeDisabled();
};
}
}, [messageHub, uiRegistry, pluginManager]);
const t = (key: string) => {
const translations: Record<string, Record<string, string>> = {
en: {
file: 'File',
newScene: 'New Scene',
openScene: 'Open Scene',
saveScene: 'Save Scene',
saveSceneAs: 'Save Scene As...',
openProject: 'Open Project',
closeProject: 'Close Project',
exit: 'Exit',
edit: 'Edit',
undo: 'Undo',
redo: 'Redo',
cut: 'Cut',
copy: 'Copy',
paste: 'Paste',
delete: 'Delete',
selectAll: 'Select All',
window: 'Window',
sceneHierarchy: 'Scene Hierarchy',
inspector: 'Inspector',
assets: 'Assets',
console: 'Console',
viewport: 'Viewport',
pluginManager: 'Plugin Manager',
tools: 'Tools',
createPlugin: 'Create Plugin',
portManager: 'Port Manager',
settings: 'Settings',
help: 'Help',
documentation: 'Documentation',
checkForUpdates: 'Check for Updates',
about: 'About',
devtools: 'Developer Tools'
},
zh: {
file: '文件',
newScene: '新建场景',
openScene: '打开场景',
saveScene: '保存场景',
saveSceneAs: '场景另存为...',
openProject: '打开项目',
closeProject: '关闭项目',
exit: '退出',
edit: '编辑',
undo: '撤销',
redo: '重做',
cut: '剪切',
copy: '复制',
paste: '粘贴',
delete: '删除',
selectAll: '全选',
window: '窗口',
sceneHierarchy: '场景层级',
inspector: '检视器',
assets: '资产',
console: '控制台',
viewport: '视口',
pluginManager: '插件管理器',
tools: '工具',
createPlugin: '创建插件',
portManager: '端口管理器',
settings: '设置',
help: '帮助',
documentation: '文档',
checkForUpdates: '检查更新',
about: '关于',
devtools: '开发者工具'
}
};
return translations[locale]?.[key] || key;
}; };
}, []);
const handleMenuClick = (menuKey: string) => { const menus: Record<string, MenuItem[]> = {
setOpenMenu(openMenu === menuKey ? null : menuKey); file: [
}; { label: t('newScene'), shortcut: 'Ctrl+N', onClick: onNewScene },
{ label: t('openScene'), shortcut: 'Ctrl+O', onClick: onOpenScene },
{ separator: true },
{ label: t('saveScene'), shortcut: 'Ctrl+S', onClick: onSaveScene },
{ label: t('saveSceneAs'), shortcut: 'Ctrl+Shift+S', onClick: onSaveSceneAs },
{ separator: true },
{ label: t('openProject'), onClick: onOpenProject },
{ label: t('closeProject'), onClick: onCloseProject },
{ separator: true },
{ label: t('exit'), onClick: onExit }
],
edit: [
{ label: t('undo'), shortcut: 'Ctrl+Z', disabled: true },
{ label: t('redo'), shortcut: 'Ctrl+Y', disabled: true },
{ separator: true },
{ label: t('cut'), shortcut: 'Ctrl+X', disabled: true },
{ label: t('copy'), shortcut: 'Ctrl+C', disabled: true },
{ label: t('paste'), shortcut: 'Ctrl+V', disabled: true },
{ label: t('delete'), shortcut: 'Delete', disabled: true },
{ separator: true },
{ label: t('selectAll'), shortcut: 'Ctrl+A', disabled: true }
],
window: [
...pluginMenuItems.map((item) => ({
label: item.label || '',
icon: item.icon,
disabled: item.disabled,
onClick: item.onClick
})),
...(pluginMenuItems.length > 0 ? [{ separator: true } as MenuItem] : []),
{ label: t('pluginManager'), onClick: onOpenPluginManager },
{ separator: true },
{ label: t('devtools'), onClick: onToggleDevtools }
],
tools: [
{ label: t('createPlugin'), onClick: onCreatePlugin },
{ separator: true },
{ label: t('portManager'), onClick: onOpenPortManager },
{ separator: true },
{ label: t('settings'), onClick: onOpenSettings }
],
help: [
{ label: t('documentation'), disabled: true },
{ separator: true },
{ label: t('checkForUpdates'), onClick: onOpenAbout },
{ label: t('about'), onClick: onOpenAbout }
]
};
const handleMenuItemClick = (item: MenuItem) => { useEffect(() => {
if (!item.disabled && !item.separator && item.onClick && item.label) { const handleClickOutside = (event: MouseEvent) => {
item.onClick(); if (menuRef.current && !menuRef.current.contains(event.target as Node)) {
setOpenMenu(null); setOpenMenu(null);
} }
}; };
return ( document.addEventListener('mousedown', handleClickOutside);
<div className="menu-bar" ref={menuRef}> return () => {
{Object.keys(menus).map(menuKey => ( document.removeEventListener('mousedown', handleClickOutside);
<div key={menuKey} className="menu-item"> };
<button }, []);
className={`menu-button ${openMenu === menuKey ? 'active' : ''}`}
onClick={() => handleMenuClick(menuKey)} const handleMenuClick = (menuKey: string) => {
> setOpenMenu(openMenu === menuKey ? null : menuKey);
{t(menuKey)} };
</button>
{openMenu === menuKey && menus[menuKey] && ( const handleMenuItemClick = (item: MenuItem) => {
<div className="menu-dropdown"> if (!item.disabled && !item.separator && item.onClick && item.label) {
{menus[menuKey].map((item, index) => { item.onClick();
if (item.separator) { setOpenMenu(null);
return <div key={index} className="menu-separator" />; }
} };
const IconComponent = item.icon ? (LucideIcons as any)[item.icon] : null;
return ( return (
<button <div className="menu-bar" ref={menuRef}>
key={index} {Object.keys(menus).map((menuKey) => (
className={`menu-dropdown-item ${item.disabled ? 'disabled' : ''}`} <div key={menuKey} className="menu-item">
onClick={() => handleMenuItemClick(item)} <button
disabled={item.disabled} className={`menu-button ${openMenu === menuKey ? 'active' : ''}`}
> onClick={() => handleMenuClick(menuKey)}
<span className="menu-item-content"> >
{IconComponent && <IconComponent size={16} />} {t(menuKey)}
<span>{item.label || ''}</span> </button>
</span> {openMenu === menuKey && menus[menuKey] && (
{item.shortcut && <span className="menu-shortcut">{item.shortcut}</span>} <div className="menu-dropdown">
</button> {menus[menuKey].map((item, index) => {
); if (item.separator) {
})} return <div key={index} className="menu-separator" />;
</div> }
)} const IconComponent = item.icon ? (LucideIcons as any)[item.icon] : null;
return (
<button
key={index}
className={`menu-dropdown-item ${item.disabled ? 'disabled' : ''}`}
onClick={() => handleMenuItemClick(item)}
disabled={item.disabled}
>
<span className="menu-item-content">
{IconComponent && <IconComponent size={16} />}
<span>{item.label || ''}</span>
</span>
{item.shortcut && <span className="menu-shortcut">{item.shortcut}</span>}
</button>
);
})}
</div>
)}
</div>
))}
</div> </div>
))} );
</div>
);
} }

View File

@@ -97,7 +97,7 @@ export function PluginGeneratorWindow({ onClose, projectPath, locale, onSuccess
const response = await fetch('/@plugin-generator', { const response = await fetch('/@plugin-generator', {
method: 'POST', method: 'POST',
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json'
}, },
body: JSON.stringify({ body: JSON.stringify({
pluginName, pluginName,
@@ -149,7 +149,7 @@ export function PluginGeneratorWindow({ onClose, projectPath, locale, onSuccess
<input <input
type="text" type="text"
value={pluginName} value={pluginName}
onChange={e => setPluginName(e.target.value)} onChange={(e) => setPluginName(e.target.value)}
placeholder={t('pluginNamePlaceholder')} placeholder={t('pluginNamePlaceholder')}
disabled={isGenerating} disabled={isGenerating}
/> />
@@ -160,7 +160,7 @@ export function PluginGeneratorWindow({ onClose, projectPath, locale, onSuccess
<input <input
type="text" type="text"
value={pluginVersion} value={pluginVersion}
onChange={e => setPluginVersion(e.target.value)} onChange={(e) => setPluginVersion(e.target.value)}
disabled={isGenerating} disabled={isGenerating}
/> />
</div> </div>
@@ -171,7 +171,7 @@ export function PluginGeneratorWindow({ onClose, projectPath, locale, onSuccess
<input <input
type="text" type="text"
value={outputPath} value={outputPath}
onChange={e => setOutputPath(e.target.value)} onChange={(e) => setOutputPath(e.target.value)}
disabled={isGenerating} disabled={isGenerating}
/> />
<button <button
@@ -190,7 +190,7 @@ export function PluginGeneratorWindow({ onClose, projectPath, locale, onSuccess
<input <input
type="checkbox" type="checkbox"
checked={includeExample} checked={includeExample}
onChange={e => setIncludeExample(e.target.checked)} onChange={(e) => setIncludeExample(e.target.checked)}
disabled={isGenerating} disabled={isGenerating}
/> />
<span>{t('includeExample')}</span> <span>{t('includeExample')}</span>

View File

@@ -144,7 +144,7 @@ export function PluginManagerWindow({ pluginManager, onClose, onRefresh, onOpen,
setExpandedCategories(newExpanded); setExpandedCategories(newExpanded);
}; };
const filteredPlugins = plugins.filter(plugin => { const filteredPlugins = plugins.filter((plugin) => {
if (!filter) return true; if (!filter) return true;
const searchLower = filter.toLowerCase(); const searchLower = filter.toLowerCase();
return ( return (
@@ -162,8 +162,8 @@ export function PluginManagerWindow({ pluginManager, onClose, onRefresh, onOpen,
return acc; return acc;
}, {} as Record<EditorPluginCategory, IEditorPluginMetadata[]>); }, {} as Record<EditorPluginCategory, IEditorPluginMetadata[]>);
const enabledCount = plugins.filter(p => p.enabled).length; const enabledCount = plugins.filter((p) => p.enabled).length;
const disabledCount = plugins.filter(p => !p.enabled).length; const disabledCount = plugins.filter((p) => !p.enabled).length;
const renderPluginCard = (plugin: IEditorPluginMetadata) => { const renderPluginCard = (plugin: IEditorPluginMetadata) => {
const IconComponent = plugin.icon ? (LucideIcons as any)[plugin.icon] : null; const IconComponent = plugin.icon ? (LucideIcons as any)[plugin.icon] : null;

View File

@@ -65,7 +65,7 @@ export function PluginPanel({ pluginManager }: PluginPanelProps) {
setExpandedCategories(newExpanded); setExpandedCategories(newExpanded);
}; };
const filteredPlugins = plugins.filter(plugin => { const filteredPlugins = plugins.filter((plugin) => {
if (!filter) return true; if (!filter) return true;
const searchLower = filter.toLowerCase(); const searchLower = filter.toLowerCase();
return ( return (
@@ -83,81 +83,81 @@ export function PluginPanel({ pluginManager }: PluginPanelProps) {
return acc; return acc;
}, {} as Record<EditorPluginCategory, IEditorPluginMetadata[]>); }, {} as Record<EditorPluginCategory, IEditorPluginMetadata[]>);
const enabledCount = plugins.filter(p => p.enabled).length; const enabledCount = plugins.filter((p) => p.enabled).length;
const disabledCount = plugins.filter(p => !p.enabled).length; const disabledCount = plugins.filter((p) => !p.enabled).length;
const renderPluginCard = (plugin: IEditorPluginMetadata) => { const renderPluginCard = (plugin: IEditorPluginMetadata) => {
const IconComponent = plugin.icon ? (LucideIcons as any)[plugin.icon] : null; const IconComponent = plugin.icon ? (LucideIcons as any)[plugin.icon] : null;
return ( return (
<div key={plugin.name} className={`plugin-card ${plugin.enabled ? 'enabled' : 'disabled'}`}> <div key={plugin.name} className={`plugin-card ${plugin.enabled ? 'enabled' : 'disabled'}`}>
<div className="plugin-card-header"> <div className="plugin-card-header">
<div className="plugin-card-icon"> <div className="plugin-card-icon">
{IconComponent ? <IconComponent size={24} /> : <Package size={24} />} {IconComponent ? <IconComponent size={24} /> : <Package size={24} />}
</div>
<div className="plugin-card-info">
<div className="plugin-card-title">{plugin.displayName}</div>
<div className="plugin-card-version">v{plugin.version}</div>
</div>
<button
className={`plugin-toggle ${plugin.enabled ? 'enabled' : 'disabled'}`}
onClick={() => togglePlugin(plugin.name, plugin.enabled)}
title={plugin.enabled ? 'Disable plugin' : 'Enable plugin'}
>
{plugin.enabled ? <CheckCircle size={18} /> : <XCircle size={18} />}
</button>
</div> </div>
<div className="plugin-card-info"> {plugin.description && (
<div className="plugin-card-title">{plugin.displayName}</div> <div className="plugin-card-description">{plugin.description}</div>
<div className="plugin-card-version">v{plugin.version}</div>
</div>
<button
className={`plugin-toggle ${plugin.enabled ? 'enabled' : 'disabled'}`}
onClick={() => togglePlugin(plugin.name, plugin.enabled)}
title={plugin.enabled ? 'Disable plugin' : 'Enable plugin'}
>
{plugin.enabled ? <CheckCircle size={18} /> : <XCircle size={18} />}
</button>
</div>
{plugin.description && (
<div className="plugin-card-description">{plugin.description}</div>
)}
<div className="plugin-card-footer">
<span className="plugin-card-category">
{(() => {
const CategoryIcon = (LucideIcons as any)[categoryIcons[plugin.category]];
return CategoryIcon ? <CategoryIcon size={14} style={{ marginRight: '4px' }} /> : null;
})()}
{categoryNames[plugin.category]}
</span>
{plugin.installedAt && (
<span className="plugin-card-installed">
Installed: {new Date(plugin.installedAt).toLocaleDateString()}
</span>
)} )}
<div className="plugin-card-footer">
<span className="plugin-card-category">
{(() => {
const CategoryIcon = (LucideIcons as any)[categoryIcons[plugin.category]];
return CategoryIcon ? <CategoryIcon size={14} style={{ marginRight: '4px' }} /> : null;
})()}
{categoryNames[plugin.category]}
</span>
{plugin.installedAt && (
<span className="plugin-card-installed">
Installed: {new Date(plugin.installedAt).toLocaleDateString()}
</span>
)}
</div>
</div> </div>
</div>
); );
}; };
const renderPluginList = (plugin: IEditorPluginMetadata) => { const renderPluginList = (plugin: IEditorPluginMetadata) => {
const IconComponent = plugin.icon ? (LucideIcons as any)[plugin.icon] : null; const IconComponent = plugin.icon ? (LucideIcons as any)[plugin.icon] : null;
return ( return (
<div key={plugin.name} className={`plugin-list-item ${plugin.enabled ? 'enabled' : 'disabled'}`}> <div key={plugin.name} className={`plugin-list-item ${plugin.enabled ? 'enabled' : 'disabled'}`}>
<div className="plugin-list-icon"> <div className="plugin-list-icon">
{IconComponent ? <IconComponent size={20} /> : <Package size={20} />} {IconComponent ? <IconComponent size={20} /> : <Package size={20} />}
</div>
<div className="plugin-list-info">
<div className="plugin-list-name">
{plugin.displayName}
<span className="plugin-list-version">v{plugin.version}</span>
</div> </div>
{plugin.description && ( <div className="plugin-list-info">
<div className="plugin-list-description">{plugin.description}</div> <div className="plugin-list-name">
)} {plugin.displayName}
<span className="plugin-list-version">v{plugin.version}</span>
</div>
{plugin.description && (
<div className="plugin-list-description">{plugin.description}</div>
)}
</div>
<div className="plugin-list-status">
{plugin.enabled ? (
<span className="status-badge enabled">Enabled</span>
) : (
<span className="status-badge disabled">Disabled</span>
)}
</div>
<button
className="plugin-list-toggle"
onClick={() => togglePlugin(plugin.name, plugin.enabled)}
title={plugin.enabled ? 'Disable plugin' : 'Enable plugin'}
>
{plugin.enabled ? 'Disable' : 'Enable'}
</button>
</div> </div>
<div className="plugin-list-status">
{plugin.enabled ? (
<span className="status-badge enabled">Enabled</span>
) : (
<span className="status-badge disabled">Disabled</span>
)}
</div>
<button
className="plugin-list-toggle"
onClick={() => togglePlugin(plugin.name, plugin.enabled)}
title={plugin.enabled ? 'Disable plugin' : 'Enable plugin'}
>
{plugin.enabled ? 'Disable' : 'Enable'}
</button>
</div>
); );
}; };

View File

@@ -10,152 +10,152 @@ interface PortManagerProps {
} }
export function PortManager({ onClose }: PortManagerProps) { export function PortManager({ onClose }: PortManagerProps) {
const [isServerRunning, setIsServerRunning] = useState(false); const [isServerRunning, setIsServerRunning] = useState(false);
const [serverPort, setServerPort] = useState<string>('8080'); const [serverPort, setServerPort] = useState<string>('8080');
const [isChecking, setIsChecking] = useState(false); const [isChecking, setIsChecking] = useState(false);
const [isStopping, setIsStopping] = useState(false); const [isStopping, setIsStopping] = useState(false);
const [isStarting, setIsStarting] = useState(false); const [isStarting, setIsStarting] = useState(false);
useEffect(() => { useEffect(() => {
const settings = SettingsService.getInstance(); const settings = SettingsService.getInstance();
setServerPort(settings.get('profiler.port', '8080')); setServerPort(settings.get('profiler.port', '8080'));
const handleSettingsChange = ((event: CustomEvent) => { const handleSettingsChange = ((event: CustomEvent) => {
const newPort = event.detail['profiler.port']; const newPort = event.detail['profiler.port'];
if (newPort) { if (newPort) {
setServerPort(newPort); setServerPort(newPort);
} }
}) as EventListener; }) as EventListener;
window.addEventListener('settings:changed', handleSettingsChange); window.addEventListener('settings:changed', handleSettingsChange);
return () => { return () => {
window.removeEventListener('settings:changed', handleSettingsChange); window.removeEventListener('settings:changed', handleSettingsChange);
};
}, []);
useEffect(() => {
checkServerStatus();
}, []);
const checkServerStatus = async () => {
setIsChecking(true);
try {
const status = await invoke<boolean>('get_profiler_status');
setIsServerRunning(status);
} catch (error) {
console.error('[PortManager] Failed to check server status:', error);
setIsServerRunning(false);
} finally {
setIsChecking(false);
}
}; };
}, []);
useEffect(() => { const handleStopServer = async () => {
checkServerStatus(); setIsStopping(true);
}, []); try {
const profilerService = (window as any).__PROFILER_SERVICE__ as ProfilerService | undefined;
if (profilerService) {
await profilerService.manualStopServer();
setIsServerRunning(false);
}
} catch (error) {
console.error('[PortManager] Failed to stop server:', error);
} finally {
setIsStopping(false);
}
};
const checkServerStatus = async () => { const handleStartServer = async () => {
setIsChecking(true); setIsStarting(true);
try { try {
const status = await invoke<boolean>('get_profiler_status'); const profilerService = (window as any).__PROFILER_SERVICE__ as ProfilerService | undefined;
setIsServerRunning(status); if (profilerService) {
} catch (error) { await profilerService.manualStartServer();
console.error('[PortManager] Failed to check server status:', error); await new Promise((resolve) => setTimeout(resolve, 500));
setIsServerRunning(false); await checkServerStatus();
} finally { }
setIsChecking(false); } catch (error) {
} console.error('[PortManager] Failed to start server:', error);
}; } finally {
setIsStarting(false);
}
};
const handleStopServer = async () => { return (
setIsStopping(true); <div className="port-manager-overlay" onClick={onClose}>
try { <div className="port-manager" onClick={(e) => e.stopPropagation()}>
const profilerService = (window as any).__PROFILER_SERVICE__ as ProfilerService | undefined; <div className="port-manager-header">
if (profilerService) { <div className="port-manager-title">
await profilerService.manualStopServer(); <Server size={20} />
setIsServerRunning(false); <h2>Port Manager</h2>
} </div>
} catch (error) { <button className="port-manager-close" onClick={onClose} title="Close">
console.error('[PortManager] Failed to stop server:', error); <X size={20} />
} finally { </button>
setIsStopping(false); </div>
}
}; <div className="port-manager-content">
<div className="port-section">
const handleStartServer = async () => { <h3>Profiler Server</h3>
setIsStarting(true); <div className="port-info">
try { <div className="port-item">
const profilerService = (window as any).__PROFILER_SERVICE__ as ProfilerService | undefined; <span className="port-label">Status:</span>
if (profilerService) { <span className={`port-status ${isServerRunning ? 'running' : 'stopped'}`}>
await profilerService.manualStartServer(); {isChecking ? 'Checking...' : isServerRunning ? 'Running' : 'Stopped'}
await new Promise(resolve => setTimeout(resolve, 500)); </span>
await checkServerStatus(); </div>
} {isServerRunning && (
} catch (error) { <div className="port-item">
console.error('[PortManager] Failed to start server:', error); <span className="port-label">Port:</span>
} finally { <span className="port-value">{serverPort}</span>
setIsStarting(false); </div>
} )}
}; </div>
return ( {isServerRunning && (
<div className="port-manager-overlay" onClick={onClose}> <div className="port-actions">
<div className="port-manager" onClick={(e) => e.stopPropagation()}> <button
<div className="port-manager-header"> className="action-btn danger"
<div className="port-manager-title"> onClick={handleStopServer}
<Server size={20} /> disabled={isStopping}
<h2>Port Manager</h2> >
</div> <WifiOff size={16} />
<button className="port-manager-close" onClick={onClose} title="Close"> <span>{isStopping ? 'Stopping...' : 'Stop Server'}</span>
<X size={20} /> </button>
</button> </div>
</div> )}
<div className="port-manager-content"> {!isServerRunning && (
<div className="port-section"> <>
<h3>Profiler Server</h3> <div className="port-actions">
<div className="port-info"> <button
<div className="port-item"> className="action-btn primary"
<span className="port-label">Status:</span> onClick={handleStartServer}
<span className={`port-status ${isServerRunning ? 'running' : 'stopped'}`}> disabled={isStarting}
{isChecking ? 'Checking...' : isServerRunning ? 'Running' : 'Stopped'} >
</span> <Wifi size={16} />
</div> <span>{isStarting ? 'Starting...' : 'Start Server'}</span>
{isServerRunning && ( </button>
<div className="port-item"> </div>
<span className="port-label">Port:</span> <div className="port-hint">
<span className="port-value">{serverPort}</span> <p>No server is currently running.</p>
<p className="hint-text">Click "Start Server" to start the profiler server.</p>
</div>
</>
)}
</div>
<div className="port-tips">
<h4>Tips</h4>
<ul>
<li>Use this when the Profiler server port is stuck and cannot be restarted</li>
<li>The server will automatically stop when the Profiler window is closed</li>
<li>Current configured port: {serverPort}</li>
</ul>
</div>
</div> </div>
)}
</div> </div>
{isServerRunning && (
<div className="port-actions">
<button
className="action-btn danger"
onClick={handleStopServer}
disabled={isStopping}
>
<WifiOff size={16} />
<span>{isStopping ? 'Stopping...' : 'Stop Server'}</span>
</button>
</div>
)}
{!isServerRunning && (
<>
<div className="port-actions">
<button
className="action-btn primary"
onClick={handleStartServer}
disabled={isStarting}
>
<Wifi size={16} />
<span>{isStarting ? 'Starting...' : 'Start Server'}</span>
</button>
</div>
<div className="port-hint">
<p>No server is currently running.</p>
<p className="hint-text">Click "Start Server" to start the profiler server.</p>
</div>
</>
)}
</div>
<div className="port-tips">
<h4>Tips</h4>
<ul>
<li>Use this when the Profiler server port is stuck and cannot be restarted</li>
<li>The server will automatically stop when the Profiler window is closed</li>
<li>Current configured port: {serverPort}</li>
</ul>
</div>
</div> </div>
</div> );
</div>
);
} }

View File

@@ -7,220 +7,220 @@ import { MessageHub } from '@esengine/editor-core';
import '../styles/ProfilerDockPanel.css'; import '../styles/ProfilerDockPanel.css';
export function ProfilerDockPanel() { export function ProfilerDockPanel() {
const [profilerData, setProfilerData] = useState<ProfilerData | null>(null); const [profilerData, setProfilerData] = useState<ProfilerData | null>(null);
const [isConnected, setIsConnected] = useState(false); const [isConnected, setIsConnected] = useState(false);
const [isServerRunning, setIsServerRunning] = useState(false); const [isServerRunning, setIsServerRunning] = useState(false);
const [port, setPort] = useState('8080'); const [port, setPort] = useState('8080');
const [isPaused, setIsPaused] = useState(false); const [isPaused, setIsPaused] = useState(false);
useEffect(() => { useEffect(() => {
const settings = SettingsService.getInstance(); const settings = SettingsService.getInstance();
setPort(settings.get('profiler.port', '8080')); setPort(settings.get('profiler.port', '8080'));
const handleSettingsChange = ((event: CustomEvent) => { const handleSettingsChange = ((event: CustomEvent) => {
const newPort = event.detail['profiler.port']; const newPort = event.detail['profiler.port'];
if (newPort) { if (newPort) {
setPort(newPort); setPort(newPort);
} }
}) as EventListener; }) as EventListener;
window.addEventListener('settings:changed', handleSettingsChange); window.addEventListener('settings:changed', handleSettingsChange);
return () => { return () => {
window.removeEventListener('settings:changed', handleSettingsChange); window.removeEventListener('settings:changed', handleSettingsChange);
}; };
}, []); }, []);
useEffect(() => { useEffect(() => {
const profilerService = (window as any).__PROFILER_SERVICE__ as ProfilerService | undefined; const profilerService = (window as any).__PROFILER_SERVICE__ as ProfilerService | undefined;
if (!profilerService) { if (!profilerService) {
console.warn('[ProfilerDockPanel] ProfilerService not available - plugin may be disabled'); console.warn('[ProfilerDockPanel] ProfilerService not available - plugin may be disabled');
setIsServerRunning(false); setIsServerRunning(false);
setIsConnected(false); setIsConnected(false);
return; return;
} }
// 订阅数据更新 // 订阅数据更新
const unsubscribe = profilerService.subscribe((data: ProfilerData) => { const unsubscribe = profilerService.subscribe((data: ProfilerData) => {
if (!isPaused) { if (!isPaused) {
setProfilerData(data); setProfilerData(data);
} }
}); });
// 定期检查连接状态 // 定期检查连接状态
const checkStatus = () => { const checkStatus = () => {
setIsConnected(profilerService.isConnected()); setIsConnected(profilerService.isConnected());
setIsServerRunning(profilerService.isServerActive()); 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' });
}
}; };
checkStatus(); const handleTogglePause = () => {
const interval = setInterval(checkStatus, 1000); setIsPaused(!isPaused);
return () => {
unsubscribe();
clearInterval(interval);
}; };
}, [isPaused]);
const fps = profilerData?.fps || 0; return (
const totalFrameTime = profilerData?.totalFrameTime || 0; <div className="profiler-dock-panel">
const systems = (profilerData?.systems || []).slice(0, 5); // Only show top 5 systems in dock panel <div className="profiler-dock-header">
const entityCount = profilerData?.entityCount || 0; <h3>Performance Monitor</h3>
const componentCount = profilerData?.componentCount || 0; <div className="profiler-dock-header-actions">
const targetFrameTime = 16.67; {isConnected && (
<>
const handleOpenDetails = () => { <button
const messageHub = Core.services.resolve(MessageHub); className="profiler-dock-pause-btn"
if (messageHub) { onClick={handleTogglePause}
messageHub.publish('ui:openWindow', { windowId: 'profiler' }); title={isPaused ? 'Resume data updates' : 'Pause data updates'}
} >
}; {isPaused ? <Play size={14} /> : <Pause size={14} />}
</button>
const handleTogglePause = () => { <button
setIsPaused(!isPaused); className="profiler-dock-details-btn"
}; onClick={handleOpenDetails}
title="Open detailed profiler"
return ( >
<div className="profiler-dock-panel"> <Maximize2 size={14} />
<div className="profiler-dock-header"> </button>
<h3>Performance Monitor</h3> </>
<div className="profiler-dock-header-actions"> )}
{isConnected && ( <div className="profiler-dock-status">
<> {isConnected ? (
<button <>
className="profiler-dock-pause-btn" <Wifi size={12} />
onClick={handleTogglePause} <span className="status-text connected">Connected</span>
title={isPaused ? 'Resume data updates' : 'Pause data updates'} </>
> ) : isServerRunning ? (
{isPaused ? <Play size={14} /> : <Pause size={14} />} <>
</button> <WifiOff size={12} />
<button <span className="status-text waiting">Waiting...</span>
className="profiler-dock-details-btn" </>
onClick={handleOpenDetails} ) : (
title="Open detailed profiler" <>
> <WifiOff size={12} />
<Maximize2 size={14} /> <span className="status-text disconnected">Server Off</span>
</button> </>
</> )}
)} </div>
<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> </div>
<div className="stat-card"> {!isServerRunning ? (
<div className="stat-icon"> <div className="profiler-dock-empty">
<Layers size={16} /> <Cpu size={32} />
</div> <p>Profiler server not running</p>
<div className="stat-info"> <p className="hint">Open Profiler window and connect to start monitoring</p>
<div className="stat-label">Entities</div> </div>
<div className="stat-value">{entityCount}</div> ) : !isConnected ? (
</div> <div className="profiler-dock-empty">
</div> <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-card">
<div className="stat-icon"> <div className="stat-icon">
<Package size={16} /> <Cpu size={16} />
</div> </div>
<div className="stat-info"> <div className="stat-info">
<div className="stat-label">Components</div> <div className="stat-label">Frame Time</div>
<div className="stat-value">{componentCount}</div> <div className={`stat-value ${totalFrameTime > targetFrameTime ? 'warning' : ''}`}>
</div> {totalFrameTime.toFixed(1)}ms
</div> </div>
</div> </div>
</div>
{systems.length > 0 && ( <div className="stat-card">
<div className="profiler-dock-systems"> <div className="stat-icon">
<h4>Top Systems</h4> <Layers size={16} />
<div className="systems-list"> </div>
{systems.map((system) => ( <div className="stat-info">
<div key={system.name} className="system-item"> <div className="stat-label">Entities</div>
<div className="system-item-header"> <div className="stat-value">{entityCount}</div>
<span className="system-item-name">{system.name}</span> </div>
<span className="system-item-time"> </div>
{system.executionTime.toFixed(2)}ms
</span> <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> </div>
<div className="system-item-bar">
<div {systems.length > 0 && (
className="system-item-bar-fill" <div className="profiler-dock-systems">
style={{ <h4>Top Systems</h4>
width: `${Math.min(system.percentage, 100)}%`, <div className="systems-list">
backgroundColor: system.executionTime > targetFrameTime {systems.map((system) => (
? 'var(--color-danger)' <div key={system.name} className="system-item">
: system.executionTime > targetFrameTime * 0.5 <div className="system-item-header">
? 'var(--color-warning)' <span className="system-item-name">{system.name}</span>
: 'var(--color-success)' <span className="system-item-time">
}} {system.executionTime.toFixed(2)}ms
/> </span>
</div> </div>
<div className="system-item-footer"> <div className="system-item-bar">
<span className="system-item-percentage">{system.percentage.toFixed(1)}%</span> <div
{system.entityCount > 0 && ( className="system-item-bar-fill"
<span className="system-item-entities">{system.entityCount} entities</span> style={{
)} width: `${Math.min(system.percentage, 100)}%`,
</div> backgroundColor: system.executionTime > targetFrameTime
</div> ? 'var(--color-danger)'
))} : system.executionTime > targetFrameTime * 0.5
</div> ? 'var(--color-warning)'
</div> : '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> </div>
)} );
</div>
);
} }

View File

@@ -14,221 +14,221 @@ interface SystemPerformanceData {
} }
export function ProfilerPanel() { export function ProfilerPanel() {
const [systems, setSystems] = useState<SystemPerformanceData[]>([]); const [systems, setSystems] = useState<SystemPerformanceData[]>([]);
const [totalFrameTime, setTotalFrameTime] = useState(0); const [totalFrameTime, setTotalFrameTime] = useState(0);
const [isPaused, setIsPaused] = useState(false); const [isPaused, setIsPaused] = useState(false);
const [sortBy, setSortBy] = useState<'time' | 'average' | 'name'>('time'); const [sortBy, setSortBy] = useState<'time' | 'average' | 'name'>('time');
const animationRef = useRef<number>(); const animationRef = useRef<number>();
useEffect(() => {
const updateProfilerData = () => {
if (isPaused) {
animationRef.current = requestAnimationFrame(updateProfilerData);
return;
}
const coreInstance = Core.Instance;
if (!coreInstance || !coreInstance._performanceMonitor?.isEnabled) {
animationRef.current = requestAnimationFrame(updateProfilerData);
return;
}
const performanceMonitor = coreInstance._performanceMonitor;
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);
};
useEffect(() => {
const updateProfilerData = () => {
if (isPaused) {
animationRef.current = requestAnimationFrame(updateProfilerData); animationRef.current = requestAnimationFrame(updateProfilerData);
return;
}
const coreInstance = Core.Instance; return () => {
if (!coreInstance || !coreInstance._performanceMonitor?.isEnabled) { if (animationRef.current) {
animationRef.current = requestAnimationFrame(updateProfilerData); cancelAnimationFrame(animationRef.current);
return; }
} };
}, [isPaused, sortBy]);
const performanceMonitor = coreInstance._performanceMonitor; const handleReset = () => {
const systemDataMap = performanceMonitor.getAllSystemData(); const coreInstance = Core.Instance;
const systemStatsMap = performanceMonitor.getAllSystemStats(); if (coreInstance && coreInstance._performanceMonitor) {
coreInstance._performanceMonitor.reset();
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); const fps = totalFrameTime > 0 ? Math.round(1000 / totalFrameTime) : 0;
const targetFrameTime = 16.67; // 60 FPS
const isOverBudget = totalFrameTime > targetFrameTime;
return () => { return (
if (animationRef.current) { <div className="profiler-panel">
cancelAnimationFrame(animationRef.current); <div className="profiler-toolbar">
} <div className="profiler-toolbar-left">
}; <div className="profiler-stats-summary">
}, [isPaused, sortBy]); <div className="summary-item">
<Clock size={14} />
const handleReset = () => { <span className="summary-label">Frame:</span>
const coreInstance = Core.Instance; <span className={`summary-value ${isOverBudget ? 'over-budget' : ''}`}>
if (coreInstance && coreInstance._performanceMonitor) { {totalFrameTime.toFixed(2)}ms
coreInstance._performanceMonitor.reset(); </span>
} </div>
}; <div className="summary-item">
<Activity size={14} />
const fps = totalFrameTime > 0 ? Math.round(1000 / totalFrameTime) : 0; <span className="summary-label">FPS:</span>
const targetFrameTime = 16.67; // 60 FPS <span className={`summary-value ${fps < 55 ? 'low-fps' : ''}`}>{fps}</span>
const isOverBudget = totalFrameTime > targetFrameTime; </div>
<div className="summary-item">
return ( <BarChart3 size={14} />
<div className="profiler-panel"> <span className="summary-label">Systems:</span>
<div className="profiler-toolbar"> <span className="summary-value">{systems.length}</span>
<div className="profiler-toolbar-left"> </div>
<div className="profiler-stats-summary"> </div>
<div className="summary-item"> </div>
<Clock size={14} /> <div className="profiler-toolbar-right">
<span className="summary-label">Frame:</span> <select
<span className={`summary-value ${isOverBudget ? 'over-budget' : ''}`}> className="profiler-sort"
{totalFrameTime.toFixed(2)}ms value={sortBy}
</span> 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>
<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"> <div className="profiler-content">
{systems.length === 0 ? ( {systems.length === 0 ? (
<div className="profiler-empty"> <div className="profiler-empty">
<Cpu size={48} /> <Cpu size={48} />
<p>No performance data available</p> <p>No performance data available</p>
<p className="profiler-empty-hint"> <p className="profiler-empty-hint">
Make sure Core debug mode is enabled and systems are running Make sure Core debug mode is enabled and systems are running
</p> </p>
</div> </div>
) : ( ) : (
<div className="profiler-systems"> <div className="profiler-systems">
{systems.map((system, index) => ( {systems.map((system, index) => (
<div key={system.name} className="system-row"> <div key={system.name} className="system-row">
<div className="system-header"> <div className="system-header">
<div className="system-info"> <div className="system-info">
<span className="system-rank">#{index + 1}</span> <span className="system-rank">#{index + 1}</span>
<span className="system-name">{system.name}</span> <span className="system-name">{system.name}</span>
{system.entityCount > 0 && ( {system.entityCount > 0 && (
<span className="system-entities"> <span className="system-entities">
({system.entityCount} entities) ({system.entityCount} entities)
</span> </span>
)} )}
</div> </div>
<div className="system-metrics"> <div className="system-metrics">
<span className="metric-time">{system.executionTime.toFixed(2)}ms</span> <span className="metric-time">{system.executionTime.toFixed(2)}ms</span>
<span className="metric-percentage">{system.percentage.toFixed(1)}%</span> <span className="metric-percentage">{system.percentage.toFixed(1)}%</span>
</div> </div>
</div> </div>
<div className="system-bar"> <div className="system-bar">
<div <div
className="system-bar-fill" className="system-bar-fill"
style={{ style={{
width: `${Math.min(system.percentage, 100)}%`, width: `${Math.min(system.percentage, 100)}%`,
backgroundColor: system.executionTime > targetFrameTime backgroundColor: system.executionTime > targetFrameTime
? 'var(--color-danger)' ? 'var(--color-danger)'
: system.executionTime > targetFrameTime * 0.5 : system.executionTime > targetFrameTime * 0.5
? 'var(--color-warning)' ? 'var(--color-warning)'
: 'var(--color-success)' : 'var(--color-success)'
}} }}
/> />
</div> </div>
<div className="system-stats"> <div className="system-stats">
<div className="stat-item"> <div className="stat-item">
<span className="stat-label">Avg:</span> <span className="stat-label">Avg:</span>
<span className="stat-value">{system.averageTime.toFixed(2)}ms</span> <span className="stat-value">{system.averageTime.toFixed(2)}ms</span>
</div> </div>
<div className="stat-item"> <div className="stat-item">
<span className="stat-label">Min:</span> <span className="stat-label">Min:</span>
<span className="stat-value">{system.minTime.toFixed(2)}ms</span> <span className="stat-value">{system.minTime.toFixed(2)}ms</span>
</div> </div>
<div className="stat-item"> <div className="stat-item">
<span className="stat-label">Max:</span> <span className="stat-label">Max:</span>
<span className="stat-value">{system.maxTime.toFixed(2)}ms</span> <span className="stat-value">{system.maxTime.toFixed(2)}ms</span>
</div> </div>
</div> </div>
</div> </div>
))} ))}
</div> </div>
)} )}
</div> </div>
<div className="profiler-footer"> <div className="profiler-footer">
<div className="profiler-legend"> <div className="profiler-legend">
<div className="legend-item"> <div className="legend-item">
<div className="legend-color" style={{ background: 'var(--color-success)' }} /> <div className="legend-color" style={{ background: 'var(--color-success)' }} />
<span>Good (&lt;8ms)</span> <span>Good (&lt;8ms)</span>
</div> </div>
<div className="legend-item"> <div className="legend-item">
<div className="legend-color" style={{ background: 'var(--color-warning)' }} /> <div className="legend-color" style={{ background: 'var(--color-warning)' }} />
<span>Warning (8-16ms)</span> <span>Warning (8-16ms)</span>
</div> </div>
<div className="legend-item"> <div className="legend-item">
<div className="legend-color" style={{ background: 'var(--color-danger)' }} /> <div className="legend-color" style={{ background: 'var(--color-danger)' }} />
<span>Critical (&gt;16ms)</span> <span>Critical (&gt;16ms)</span>
</div> </div>
</div>
</div>
</div> </div>
</div> );
</div>
);
} }

File diff suppressed because it is too large Load Diff

View File

@@ -10,136 +10,136 @@ interface PropertyInspectorProps {
} }
export function PropertyInspector({ component, onChange }: PropertyInspectorProps) { export function PropertyInspector({ component, onChange }: PropertyInspectorProps) {
const [properties, setProperties] = useState<Record<string, PropertyMetadata>>({}); const [properties, setProperties] = useState<Record<string, PropertyMetadata>>({});
const [values, setValues] = useState<Record<string, any>>({}); const [values, setValues] = useState<Record<string, any>>({});
useEffect(() => { useEffect(() => {
const propertyMetadataService = Core.services.resolve(PropertyMetadataService); const propertyMetadataService = Core.services.resolve(PropertyMetadataService);
if (!propertyMetadataService) return; if (!propertyMetadataService) return;
const metadata = propertyMetadataService.getEditableProperties(component); const metadata = propertyMetadataService.getEditableProperties(component);
setProperties(metadata); setProperties(metadata);
const componentAsAny = component as any; const componentAsAny = component as any;
const currentValues: Record<string, any> = {}; const currentValues: Record<string, any> = {};
for (const key in metadata) { for (const key in metadata) {
currentValues[key] = componentAsAny[key]; currentValues[key] = componentAsAny[key];
} }
setValues(currentValues); setValues(currentValues);
}, [component]); }, [component]);
const handleChange = (propertyName: string, value: any) => { const handleChange = (propertyName: string, value: any) => {
const componentAsAny = component as any; const componentAsAny = component as any;
componentAsAny[propertyName] = value; componentAsAny[propertyName] = value;
setValues(prev => ({ setValues((prev) => ({
...prev, ...prev,
[propertyName]: value [propertyName]: value
})); }));
if (onChange) { if (onChange) {
onChange(propertyName, value); onChange(propertyName, value);
} }
}; };
const renderProperty = (propertyName: string, metadata: PropertyMetadata) => { const renderProperty = (propertyName: string, metadata: PropertyMetadata) => {
const value = values[propertyName]; const value = values[propertyName];
const label = metadata.label || propertyName; const label = metadata.label || propertyName;
switch (metadata.type) { switch (metadata.type) {
case 'number': case 'number':
return ( return (
<NumberField <NumberField
key={propertyName} key={propertyName}
label={label} label={label}
value={value ?? 0} value={value ?? 0}
min={metadata.min} min={metadata.min}
max={metadata.max} max={metadata.max}
step={metadata.step} step={metadata.step}
readOnly={metadata.readOnly} readOnly={metadata.readOnly}
onChange={(newValue) => handleChange(propertyName, newValue)} onChange={(newValue) => handleChange(propertyName, newValue)}
/> />
); );
case 'string': case 'string':
return ( return (
<StringField <StringField
key={propertyName} key={propertyName}
label={label} label={label}
value={value ?? ''} value={value ?? ''}
readOnly={metadata.readOnly} readOnly={metadata.readOnly}
onChange={(newValue) => handleChange(propertyName, newValue)} onChange={(newValue) => handleChange(propertyName, newValue)}
/> />
); );
case 'boolean': case 'boolean':
return ( return (
<BooleanField <BooleanField
key={propertyName} key={propertyName}
label={label} label={label}
value={value ?? false} value={value ?? false}
readOnly={metadata.readOnly} readOnly={metadata.readOnly}
onChange={(newValue) => handleChange(propertyName, newValue)} onChange={(newValue) => handleChange(propertyName, newValue)}
/> />
); );
case 'color': case 'color':
return ( return (
<ColorField <ColorField
key={propertyName} key={propertyName}
label={label} label={label}
value={value ?? '#ffffff'} value={value ?? '#ffffff'}
readOnly={metadata.readOnly} readOnly={metadata.readOnly}
onChange={(newValue) => handleChange(propertyName, newValue)} onChange={(newValue) => handleChange(propertyName, newValue)}
/> />
); );
case 'vector2': case 'vector2':
return ( return (
<Vector2Field <Vector2Field
key={propertyName} key={propertyName}
label={label} label={label}
value={value ?? { x: 0, y: 0 }} value={value ?? { x: 0, y: 0 }}
readOnly={metadata.readOnly} readOnly={metadata.readOnly}
onChange={(newValue) => handleChange(propertyName, newValue)} onChange={(newValue) => handleChange(propertyName, newValue)}
/> />
); );
case 'vector3': case 'vector3':
return ( return (
<Vector3Field <Vector3Field
key={propertyName} key={propertyName}
label={label} label={label}
value={value ?? { x: 0, y: 0, z: 0 }} value={value ?? { x: 0, y: 0, z: 0 }}
readOnly={metadata.readOnly} readOnly={metadata.readOnly}
onChange={(newValue) => handleChange(propertyName, newValue)} onChange={(newValue) => handleChange(propertyName, newValue)}
/> />
); );
case 'enum': case 'enum':
return ( return (
<EnumField <EnumField
key={propertyName} key={propertyName}
label={label} label={label}
value={value} value={value}
options={metadata.options || []} options={metadata.options || []}
readOnly={metadata.readOnly} readOnly={metadata.readOnly}
onChange={(newValue) => handleChange(propertyName, newValue)} onChange={(newValue) => handleChange(propertyName, newValue)}
/> />
); );
default: default:
return null; return null;
} }
}; };
return ( return (
<div className="property-inspector"> <div className="property-inspector">
{Object.entries(properties).map(([propertyName, metadata]) => {Object.entries(properties).map(([propertyName, metadata]) =>
renderProperty(propertyName, metadata) renderProperty(propertyName, metadata)
)} )}
</div> </div>
); );
} }
interface NumberFieldProps { interface NumberFieldProps {
@@ -153,69 +153,69 @@ interface NumberFieldProps {
} }
function NumberField({ label, value, min, max, step = 0.1, readOnly, onChange }: NumberFieldProps) { function NumberField({ label, value, min, max, step = 0.1, readOnly, onChange }: NumberFieldProps) {
const [isDragging, setIsDragging] = useState(false); const [isDragging, setIsDragging] = useState(false);
const [dragStartX, setDragStartX] = useState(0); const [dragStartX, setDragStartX] = useState(0);
const [dragStartValue, setDragStartValue] = useState(0); const [dragStartValue, setDragStartValue] = useState(0);
const inputRef = useRef<HTMLInputElement>(null); const inputRef = useRef<HTMLInputElement>(null);
const handleMouseDown = (e: React.MouseEvent) => { const handleMouseDown = (e: React.MouseEvent) => {
if (readOnly) return; if (readOnly) return;
setIsDragging(true); setIsDragging(true);
setDragStartX(e.clientX); setDragStartX(e.clientX);
setDragStartValue(value); setDragStartValue(value);
e.preventDefault(); e.preventDefault();
};
useEffect(() => {
if (!isDragging) return;
const handleMouseMove = (e: MouseEvent) => {
const delta = e.clientX - dragStartX;
const sensitivity = e.shiftKey ? 0.1 : 1;
let newValue = dragStartValue + delta * step * sensitivity;
if (min !== undefined) newValue = Math.max(min, newValue);
if (max !== undefined) newValue = Math.min(max, newValue);
onChange(parseFloat(newValue.toFixed(3)));
}; };
const handleMouseUp = () => { useEffect(() => {
setIsDragging(false); if (!isDragging) return;
};
document.addEventListener('mousemove', handleMouseMove); const handleMouseMove = (e: MouseEvent) => {
document.addEventListener('mouseup', handleMouseUp); const delta = e.clientX - dragStartX;
const sensitivity = e.shiftKey ? 0.1 : 1;
let newValue = dragStartValue + delta * step * sensitivity;
return () => { if (min !== undefined) newValue = Math.max(min, newValue);
document.removeEventListener('mousemove', handleMouseMove); if (max !== undefined) newValue = Math.min(max, newValue);
document.removeEventListener('mouseup', handleMouseUp);
};
}, [isDragging, dragStartX, dragStartValue, step, min, max, onChange]);
return ( onChange(parseFloat(newValue.toFixed(3)));
<div className="property-field"> };
<label
className="property-label property-label-draggable" const handleMouseUp = () => {
onMouseDown={handleMouseDown} setIsDragging(false);
style={{ cursor: readOnly ? 'default' : 'ew-resize' }} };
>
{label} document.addEventListener('mousemove', handleMouseMove);
</label> document.addEventListener('mouseup', handleMouseUp);
<input
ref={inputRef} return () => {
type="number" document.removeEventListener('mousemove', handleMouseMove);
className="property-input property-input-number" document.removeEventListener('mouseup', handleMouseUp);
value={value} };
min={min} }, [isDragging, dragStartX, dragStartValue, step, min, max, onChange]);
max={max}
step={step} return (
disabled={readOnly} <div className="property-field">
onChange={(e) => onChange(parseFloat(e.target.value) || 0)} <label
onFocus={(e) => e.target.select()} className="property-label property-label-draggable"
/> onMouseDown={handleMouseDown}
</div> style={{ cursor: readOnly ? 'default' : 'ew-resize' }}
); >
{label}
</label>
<input
ref={inputRef}
type="number"
className="property-input property-input-number"
value={value}
min={min}
max={max}
step={step}
disabled={readOnly}
onChange={(e) => onChange(parseFloat(e.target.value) || 0)}
onFocus={(e) => e.target.select()}
/>
</div>
);
} }
interface StringFieldProps { interface StringFieldProps {
@@ -226,19 +226,19 @@ interface StringFieldProps {
} }
function StringField({ label, value, readOnly, onChange }: StringFieldProps) { function StringField({ label, value, readOnly, onChange }: StringFieldProps) {
return ( return (
<div className="property-field"> <div className="property-field">
<label className="property-label">{label}</label> <label className="property-label">{label}</label>
<input <input
type="text" type="text"
className="property-input property-input-text" className="property-input property-input-text"
value={value} value={value}
disabled={readOnly} disabled={readOnly}
onChange={(e) => onChange(e.target.value)} onChange={(e) => onChange(e.target.value)}
onFocus={(e) => e.target.select()} onFocus={(e) => e.target.select()}
/> />
</div> </div>
); );
} }
interface BooleanFieldProps { interface BooleanFieldProps {
@@ -249,18 +249,18 @@ interface BooleanFieldProps {
} }
function BooleanField({ label, value, readOnly, onChange }: BooleanFieldProps) { function BooleanField({ label, value, readOnly, onChange }: BooleanFieldProps) {
return ( return (
<div className="property-field property-field-boolean"> <div className="property-field property-field-boolean">
<label className="property-label">{label}</label> <label className="property-label">{label}</label>
<button <button
className={`property-toggle ${value ? 'property-toggle-on' : 'property-toggle-off'}`} className={`property-toggle ${value ? 'property-toggle-on' : 'property-toggle-off'}`}
disabled={readOnly} disabled={readOnly}
onClick={() => onChange(!value)} onClick={() => onChange(!value)}
> >
<span className="property-toggle-thumb" /> <span className="property-toggle-thumb" />
</button> </button>
</div> </div>
); );
} }
interface ColorFieldProps { interface ColorFieldProps {
@@ -271,29 +271,29 @@ interface ColorFieldProps {
} }
function ColorField({ label, value, readOnly, onChange }: ColorFieldProps) { function ColorField({ label, value, readOnly, onChange }: ColorFieldProps) {
return ( return (
<div className="property-field"> <div className="property-field">
<label className="property-label">{label}</label> <label className="property-label">{label}</label>
<div className="property-color-wrapper"> <div className="property-color-wrapper">
<div className="property-color-preview" style={{ backgroundColor: value }} /> <div className="property-color-preview" style={{ backgroundColor: value }} />
<input <input
type="color" type="color"
className="property-input property-input-color" className="property-input property-input-color"
value={value} value={value}
disabled={readOnly} disabled={readOnly}
onChange={(e) => onChange(e.target.value)} onChange={(e) => onChange(e.target.value)}
/> />
<input <input
type="text" type="text"
className="property-input property-input-color-text" className="property-input property-input-color-text"
value={value.toUpperCase()} value={value.toUpperCase()}
disabled={readOnly} disabled={readOnly}
onChange={(e) => onChange(e.target.value)} onChange={(e) => onChange(e.target.value)}
onFocus={(e) => e.target.select()} onFocus={(e) => e.target.select()}
/> />
</div> </div>
</div> </div>
); );
} }
interface Vector2FieldProps { interface Vector2FieldProps {
@@ -304,76 +304,76 @@ interface Vector2FieldProps {
} }
function Vector2Field({ label, value, readOnly, onChange }: Vector2FieldProps) { function Vector2Field({ label, value, readOnly, onChange }: Vector2FieldProps) {
const [isExpanded, setIsExpanded] = useState(false); const [isExpanded, setIsExpanded] = useState(false);
return ( return (
<div className="property-field"> <div className="property-field">
<div className="property-label-row"> <div className="property-label-row">
<button <button
className="property-expand-btn" className="property-expand-btn"
onClick={() => setIsExpanded(!isExpanded)} onClick={() => setIsExpanded(!isExpanded)}
> >
{isExpanded ? <ChevronDown size={14} /> : <ChevronRight size={14} />} {isExpanded ? <ChevronDown size={14} /> : <ChevronRight size={14} />}
</button> </button>
<label className="property-label">{label}</label> <label className="property-label">{label}</label>
</div> </div>
{isExpanded ? ( {isExpanded ? (
<div className="property-vector-expanded"> <div className="property-vector-expanded">
<div className="property-vector-axis"> <div className="property-vector-axis">
<span className="property-vector-axis-label property-vector-axis-x">X</span> <span className="property-vector-axis-label property-vector-axis-x">X</span>
<input <input
type="number" type="number"
className="property-input property-input-number" className="property-input property-input-number"
value={value?.x ?? 0} value={value?.x ?? 0}
disabled={readOnly} disabled={readOnly}
step={0.1} step={0.1}
onChange={(e) => onChange({ ...value, x: parseFloat(e.target.value) || 0 })} onChange={(e) => onChange({ ...value, x: parseFloat(e.target.value) || 0 })}
onFocus={(e) => e.target.select()} onFocus={(e) => e.target.select()}
/> />
</div> </div>
<div className="property-vector-axis"> <div className="property-vector-axis">
<span className="property-vector-axis-label property-vector-axis-y">Y</span> <span className="property-vector-axis-label property-vector-axis-y">Y</span>
<input <input
type="number" type="number"
className="property-input property-input-number" className="property-input property-input-number"
value={value?.y ?? 0} value={value?.y ?? 0}
disabled={readOnly} disabled={readOnly}
step={0.1} step={0.1}
onChange={(e) => onChange({ ...value, y: parseFloat(e.target.value) || 0 })} onChange={(e) => onChange({ ...value, y: parseFloat(e.target.value) || 0 })}
onFocus={(e) => e.target.select()} onFocus={(e) => e.target.select()}
/> />
</div> </div>
</div>
) : (
<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 ?? 0}
disabled={readOnly}
step={0.1}
onChange={(e) => onChange({ ...value, x: parseFloat(e.target.value) || 0 })}
onFocus={(e) => e.target.select()}
/>
</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 ?? 0}
disabled={readOnly}
step={0.1}
onChange={(e) => onChange({ ...value, y: parseFloat(e.target.value) || 0 })}
onFocus={(e) => e.target.select()}
/>
</div>
</div>
)}
</div> </div>
) : ( );
<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 ?? 0}
disabled={readOnly}
step={0.1}
onChange={(e) => onChange({ ...value, x: parseFloat(e.target.value) || 0 })}
onFocus={(e) => e.target.select()}
/>
</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 ?? 0}
disabled={readOnly}
step={0.1}
onChange={(e) => onChange({ ...value, y: parseFloat(e.target.value) || 0 })}
onFocus={(e) => e.target.select()}
/>
</div>
</div>
)}
</div>
);
} }
interface Vector3FieldProps { interface Vector3FieldProps {
@@ -384,100 +384,100 @@ interface Vector3FieldProps {
} }
function Vector3Field({ label, value, readOnly, onChange }: Vector3FieldProps) { function Vector3Field({ label, value, readOnly, onChange }: Vector3FieldProps) {
const [isExpanded, setIsExpanded] = useState(false); const [isExpanded, setIsExpanded] = useState(false);
return ( return (
<div className="property-field"> <div className="property-field">
<div className="property-label-row"> <div className="property-label-row">
<button <button
className="property-expand-btn" className="property-expand-btn"
onClick={() => setIsExpanded(!isExpanded)} onClick={() => setIsExpanded(!isExpanded)}
> >
{isExpanded ? <ChevronDown size={14} /> : <ChevronRight size={14} />} {isExpanded ? <ChevronDown size={14} /> : <ChevronRight size={14} />}
</button> </button>
<label className="property-label">{label}</label> <label className="property-label">{label}</label>
</div> </div>
{isExpanded ? ( {isExpanded ? (
<div className="property-vector-expanded"> <div className="property-vector-expanded">
<div className="property-vector-axis"> <div className="property-vector-axis">
<span className="property-vector-axis-label property-vector-axis-x">X</span> <span className="property-vector-axis-label property-vector-axis-x">X</span>
<input <input
type="number" type="number"
className="property-input property-input-number" className="property-input property-input-number"
value={value?.x ?? 0} value={value?.x ?? 0}
disabled={readOnly} disabled={readOnly}
step={0.1} step={0.1}
onChange={(e) => onChange({ ...value, x: parseFloat(e.target.value) || 0 })} onChange={(e) => onChange({ ...value, x: parseFloat(e.target.value) || 0 })}
onFocus={(e) => e.target.select()} onFocus={(e) => e.target.select()}
/> />
</div> </div>
<div className="property-vector-axis"> <div className="property-vector-axis">
<span className="property-vector-axis-label property-vector-axis-y">Y</span> <span className="property-vector-axis-label property-vector-axis-y">Y</span>
<input <input
type="number" type="number"
className="property-input property-input-number" className="property-input property-input-number"
value={value?.y ?? 0} value={value?.y ?? 0}
disabled={readOnly} disabled={readOnly}
step={0.1} step={0.1}
onChange={(e) => onChange({ ...value, y: parseFloat(e.target.value) || 0 })} onChange={(e) => onChange({ ...value, y: parseFloat(e.target.value) || 0 })}
onFocus={(e) => e.target.select()} onFocus={(e) => e.target.select()}
/> />
</div> </div>
<div className="property-vector-axis"> <div className="property-vector-axis">
<span className="property-vector-axis-label property-vector-axis-z">Z</span> <span className="property-vector-axis-label property-vector-axis-z">Z</span>
<input <input
type="number" type="number"
className="property-input property-input-number" className="property-input property-input-number"
value={value?.z ?? 0} value={value?.z ?? 0}
disabled={readOnly} disabled={readOnly}
step={0.1} step={0.1}
onChange={(e) => onChange({ ...value, z: parseFloat(e.target.value) || 0 })} onChange={(e) => onChange({ ...value, z: parseFloat(e.target.value) || 0 })}
onFocus={(e) => e.target.select()} onFocus={(e) => e.target.select()}
/> />
</div> </div>
</div>
) : (
<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 ?? 0}
disabled={readOnly}
step={0.1}
onChange={(e) => onChange({ ...value, x: parseFloat(e.target.value) || 0 })}
onFocus={(e) => e.target.select()}
/>
</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 ?? 0}
disabled={readOnly}
step={0.1}
onChange={(e) => onChange({ ...value, y: parseFloat(e.target.value) || 0 })}
onFocus={(e) => e.target.select()}
/>
</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 ?? 0}
disabled={readOnly}
step={0.1}
onChange={(e) => onChange({ ...value, z: parseFloat(e.target.value) || 0 })}
onFocus={(e) => e.target.select()}
/>
</div>
</div>
)}
</div> </div>
) : ( );
<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 ?? 0}
disabled={readOnly}
step={0.1}
onChange={(e) => onChange({ ...value, x: parseFloat(e.target.value) || 0 })}
onFocus={(e) => e.target.select()}
/>
</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 ?? 0}
disabled={readOnly}
step={0.1}
onChange={(e) => onChange({ ...value, y: parseFloat(e.target.value) || 0 })}
onFocus={(e) => e.target.select()}
/>
</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 ?? 0}
disabled={readOnly}
step={0.1}
onChange={(e) => onChange({ ...value, z: parseFloat(e.target.value) || 0 })}
onFocus={(e) => e.target.select()}
/>
</div>
</div>
)}
</div>
);
} }
interface EnumFieldProps { interface EnumFieldProps {
@@ -489,29 +489,29 @@ interface EnumFieldProps {
} }
function EnumField({ label, value, options, readOnly, onChange }: EnumFieldProps) { function EnumField({ label, value, options, readOnly, onChange }: EnumFieldProps) {
return ( return (
<div className="property-field"> <div className="property-field">
<label className="property-label">{label}</label> <label className="property-label">{label}</label>
<select <select
className="property-input property-input-select" className="property-input property-input-select"
value={value ?? ''} value={value ?? ''}
disabled={readOnly} disabled={readOnly}
onChange={(e) => { onChange={(e) => {
const selectedOption = options.find(opt => String(opt.value) === e.target.value); const selectedOption = options.find((opt) => String(opt.value) === e.target.value);
if (selectedOption) { if (selectedOption) {
onChange(selectedOption.value); onChange(selectedOption.value);
} }
}} }}
> >
{options.length === 0 && ( {options.length === 0 && (
<option value="">No options</option> <option value="">No options</option>
)} )}
{options.map((option, index) => ( {options.map((option, index) => (
<option key={index} value={String(option.value)}> <option key={index} value={String(option.value)}>
{option.label} {option.label}
</option> </option>
))} ))}
</select> </select>
</div> </div>
); );
} }

View File

@@ -13,160 +13,160 @@ interface ResizablePanelProps {
} }
export function ResizablePanel({ export function ResizablePanel({
direction, direction,
leftOrTop, leftOrTop,
rightOrBottom, rightOrBottom,
defaultSize = 250, defaultSize = 250,
minSize = 150, minSize = 150,
maxSize = 600, maxSize = 600,
side = 'left', side = 'left',
storageKey storageKey
}: ResizablePanelProps) { }: ResizablePanelProps) {
const getInitialSize = () => { const getInitialSize = () => {
if (storageKey) { if (storageKey) {
const saved = localStorage.getItem(storageKey); const saved = localStorage.getItem(storageKey);
if (saved) { if (saved) {
const parsedSize = parseInt(saved, 10); const parsedSize = parseInt(saved, 10);
if (!isNaN(parsedSize)) { if (!isNaN(parsedSize)) {
return Math.max(minSize, Math.min(maxSize, parsedSize)); return Math.max(minSize, Math.min(maxSize, parsedSize));
}
}
} }
} return defaultSize;
} };
return defaultSize;
};
const [size, setSize] = useState(getInitialSize); const [size, setSize] = useState(getInitialSize);
const [isDragging, setIsDragging] = useState(false); const [isDragging, setIsDragging] = useState(false);
const containerRef = useRef<HTMLDivElement>(null); const containerRef = useRef<HTMLDivElement>(null);
useEffect(() => { useEffect(() => {
if (storageKey && !isDragging) { if (storageKey && !isDragging) {
localStorage.setItem(storageKey, size.toString()); localStorage.setItem(storageKey, size.toString());
} }
}, [size, isDragging, storageKey]); }, [size, isDragging, storageKey]);
useEffect(() => { useEffect(() => {
if (!isDragging) return; if (!isDragging) return;
const handleMouseMove = (e: MouseEvent) => { const handleMouseMove = (e: MouseEvent) => {
if (!containerRef.current) return; if (!containerRef.current) return;
const rect = containerRef.current.getBoundingClientRect(); const rect = containerRef.current.getBoundingClientRect();
let newSize: number; let newSize: number;
if (direction === 'horizontal') { if (direction === 'horizontal') {
if (side === 'right') {
newSize = rect.right - e.clientX;
} else {
newSize = e.clientX - rect.left;
}
} else {
if (side === 'bottom') {
newSize = rect.bottom - e.clientY;
} else {
newSize = e.clientY - rect.top;
}
}
newSize = Math.max(minSize, Math.min(maxSize, newSize));
setSize(newSize);
};
const handleMouseUp = () => {
setIsDragging(false);
};
document.addEventListener('mousemove', handleMouseMove);
document.addEventListener('mouseup', handleMouseUp);
return () => {
document.removeEventListener('mousemove', handleMouseMove);
document.removeEventListener('mouseup', handleMouseUp);
};
}, [isDragging, direction, minSize, maxSize, side]);
const handleMouseDown = () => {
setIsDragging(true);
};
const className = `resizable-panel resizable-panel-${direction}`;
const resizerClassName = `resizer resizer-${direction}`;
if (direction === 'horizontal') {
if (side === 'right') { if (side === 'right') {
newSize = rect.right - e.clientX; return (
<div ref={containerRef} className={className}>
<div className="panel-section" style={{ flex: 1 }}>
{leftOrTop}
</div>
<div
className={resizerClassName}
onMouseDown={handleMouseDown}
style={{ cursor: isDragging ? 'ew-resize' : 'col-resize' }}
>
<div className="resizer-handle" />
</div>
<div className="panel-section" style={{ width: `${size}px` }}>
{rightOrBottom}
</div>
</div>
);
} else { } else {
newSize = e.clientX - rect.left; return (
<div ref={containerRef} className={className}>
<div className="panel-section" style={{ width: `${size}px` }}>
{leftOrTop}
</div>
<div
className={resizerClassName}
onMouseDown={handleMouseDown}
style={{ cursor: isDragging ? 'ew-resize' : 'col-resize' }}
>
<div className="resizer-handle" />
</div>
<div className="panel-section" style={{ flex: 1 }}>
{rightOrBottom}
</div>
</div>
);
} }
} else { } else {
if (side === 'bottom') { if (side === 'bottom') {
newSize = rect.bottom - e.clientY; return (
<div ref={containerRef} className={className}>
<div className="panel-section" style={{ flex: 1 }}>
{leftOrTop}
</div>
<div
className={resizerClassName}
onMouseDown={handleMouseDown}
style={{ cursor: isDragging ? 'ns-resize' : 'row-resize' }}
>
<div className="resizer-handle" />
</div>
<div className="panel-section" style={{ height: `${size}px` }}>
{rightOrBottom}
</div>
</div>
);
} else { } else {
newSize = e.clientY - rect.top; return (
<div ref={containerRef} className={className}>
<div className="panel-section" style={{ height: `${size}px` }}>
{leftOrTop}
</div>
<div
className={resizerClassName}
onMouseDown={handleMouseDown}
style={{ cursor: isDragging ? 'ns-resize' : 'row-resize' }}
>
<div className="resizer-handle" />
</div>
<div className="panel-section" style={{ flex: 1 }}>
{rightOrBottom}
</div>
</div>
);
} }
}
newSize = Math.max(minSize, Math.min(maxSize, newSize));
setSize(newSize);
};
const handleMouseUp = () => {
setIsDragging(false);
};
document.addEventListener('mousemove', handleMouseMove);
document.addEventListener('mouseup', handleMouseUp);
return () => {
document.removeEventListener('mousemove', handleMouseMove);
document.removeEventListener('mouseup', handleMouseUp);
};
}, [isDragging, direction, minSize, maxSize, side]);
const handleMouseDown = () => {
setIsDragging(true);
};
const className = `resizable-panel resizable-panel-${direction}`;
const resizerClassName = `resizer resizer-${direction}`;
if (direction === 'horizontal') {
if (side === 'right') {
return (
<div ref={containerRef} className={className}>
<div className="panel-section" style={{ flex: 1 }}>
{leftOrTop}
</div>
<div
className={resizerClassName}
onMouseDown={handleMouseDown}
style={{ cursor: isDragging ? 'ew-resize' : 'col-resize' }}
>
<div className="resizer-handle" />
</div>
<div className="panel-section" style={{ width: `${size}px` }}>
{rightOrBottom}
</div>
</div>
);
} else {
return (
<div ref={containerRef} className={className}>
<div className="panel-section" style={{ width: `${size}px` }}>
{leftOrTop}
</div>
<div
className={resizerClassName}
onMouseDown={handleMouseDown}
style={{ cursor: isDragging ? 'ew-resize' : 'col-resize' }}
>
<div className="resizer-handle" />
</div>
<div className="panel-section" style={{ flex: 1 }}>
{rightOrBottom}
</div>
</div>
);
} }
} else {
if (side === 'bottom') {
return (
<div ref={containerRef} className={className}>
<div className="panel-section" style={{ flex: 1 }}>
{leftOrTop}
</div>
<div
className={resizerClassName}
onMouseDown={handleMouseDown}
style={{ cursor: isDragging ? 'ns-resize' : 'row-resize' }}
>
<div className="resizer-handle" />
</div>
<div className="panel-section" style={{ height: `${size}px` }}>
{rightOrBottom}
</div>
</div>
);
} else {
return (
<div ref={containerRef} className={className}>
<div className="panel-section" style={{ height: `${size}px` }}>
{leftOrTop}
</div>
<div
className={resizerClassName}
onMouseDown={handleMouseDown}
style={{ cursor: isDragging ? 'ns-resize' : 'row-resize' }}
>
<div className="resizer-handle" />
</div>
<div className="panel-section" style={{ flex: 1 }}>
{rightOrBottom}
</div>
</div>
);
}
}
} }

View File

@@ -13,358 +13,358 @@ interface SceneHierarchyProps {
} }
export function SceneHierarchy({ entityStore, messageHub }: SceneHierarchyProps) { export function SceneHierarchy({ entityStore, messageHub }: SceneHierarchyProps) {
const [entities, setEntities] = useState<Entity[]>([]); const [entities, setEntities] = useState<Entity[]>([]);
const [remoteEntities, setRemoteEntities] = useState<RemoteEntity[]>([]); const [remoteEntities, setRemoteEntities] = useState<RemoteEntity[]>([]);
const [isRemoteConnected, setIsRemoteConnected] = useState(false); const [isRemoteConnected, setIsRemoteConnected] = useState(false);
const [selectedId, setSelectedId] = useState<number | null>(null); const [selectedId, setSelectedId] = useState<number | null>(null);
const [searchQuery, setSearchQuery] = useState(''); const [searchQuery, setSearchQuery] = useState('');
const [sceneName, setSceneName] = useState<string>('Untitled'); const [sceneName, setSceneName] = useState<string>('Untitled');
const [remoteSceneName, setRemoteSceneName] = useState<string | null>(null); const [remoteSceneName, setRemoteSceneName] = useState<string | null>(null);
const [sceneFilePath, setSceneFilePath] = useState<string | null>(null); const [sceneFilePath, setSceneFilePath] = useState<string | null>(null);
const [isSceneModified, setIsSceneModified] = useState<boolean>(false); const [isSceneModified, setIsSceneModified] = useState<boolean>(false);
const { t, locale } = useLocale(); const { t, locale } = useLocale();
// Subscribe to scene changes // Subscribe to scene changes
useEffect(() => { useEffect(() => {
const sceneManager = Core.services.resolve(SceneManagerService); const sceneManager = Core.services.resolve(SceneManagerService);
const updateSceneInfo = () => { const updateSceneInfo = () => {
if (sceneManager) { if (sceneManager) {
const state = sceneManager.getSceneState(); const state = sceneManager.getSceneState();
setSceneName(state.sceneName); setSceneName(state.sceneName);
setIsSceneModified(state.isModified); setIsSceneModified(state.isModified);
} }
}; };
updateSceneInfo();
const unsubLoaded = messageHub.subscribe('scene:loaded', (data: any) => {
if (data.sceneName) {
setSceneName(data.sceneName);
setSceneFilePath(data.path || null);
setIsSceneModified(data.isModified || false);
} else {
updateSceneInfo(); updateSceneInfo();
}
});
const unsubNew = messageHub.subscribe('scene:new', () => {
updateSceneInfo();
});
const unsubSaved = messageHub.subscribe('scene:saved', () => {
updateSceneInfo();
});
const unsubModified = messageHub.subscribe('scene:modified', () => {
updateSceneInfo();
});
return () => { const unsubLoaded = messageHub.subscribe('scene:loaded', (data: any) => {
unsubLoaded(); if (data.sceneName) {
unsubNew(); setSceneName(data.sceneName);
unsubSaved(); setSceneFilePath(data.path || null);
unsubModified(); setIsSceneModified(data.isModified || false);
}; } else {
}, [messageHub]); updateSceneInfo();
}
});
const unsubNew = messageHub.subscribe('scene:new', () => {
updateSceneInfo();
});
const unsubSaved = messageHub.subscribe('scene:saved', () => {
updateSceneInfo();
});
const unsubModified = messageHub.subscribe('scene:modified', () => {
updateSceneInfo();
});
// Subscribe to local entity changes return () => {
useEffect(() => { unsubLoaded();
const updateEntities = () => { unsubNew();
setEntities(entityStore.getRootEntities()); unsubSaved();
}; unsubModified();
};
}, [messageHub]);
const handleSelection = (data: { entity: Entity | null }) => { // Subscribe to local entity changes
setSelectedId(data.entity?.id ?? null); useEffect(() => {
}; const updateEntities = () => {
setEntities(entityStore.getRootEntities());
};
updateEntities(); const handleSelection = (data: { entity: Entity | null }) => {
setSelectedId(data.entity?.id ?? null);
};
const unsubAdd = messageHub.subscribe('entity:added', updateEntities); updateEntities();
const unsubRemove = messageHub.subscribe('entity:removed', updateEntities);
const unsubClear = messageHub.subscribe('entities:cleared', updateEntities);
const unsubSelect = messageHub.subscribe('entity:selected', handleSelection);
return () => { const unsubAdd = messageHub.subscribe('entity:added', updateEntities);
unsubAdd(); const unsubRemove = messageHub.subscribe('entity:removed', updateEntities);
unsubRemove(); const unsubClear = messageHub.subscribe('entities:cleared', updateEntities);
unsubClear(); const unsubSelect = messageHub.subscribe('entity:selected', handleSelection);
unsubSelect();
};
}, [entityStore, messageHub]);
// Subscribe to remote entity data from ProfilerService return () => {
useEffect(() => { unsubAdd();
const profilerService = (window as any).__PROFILER_SERVICE__ as ProfilerService | undefined; unsubRemove();
unsubClear();
unsubSelect();
};
}, [entityStore, messageHub]);
if (!profilerService) { // Subscribe to remote entity data from ProfilerService
return; useEffect(() => {
} const profilerService = (window as any).__PROFILER_SERVICE__ as ProfilerService | undefined;
const initiallyConnected = profilerService.isConnected(); if (!profilerService) {
setIsRemoteConnected(initiallyConnected); return;
}
const unsubscribe = profilerService.subscribe((data) => { const initiallyConnected = profilerService.isConnected();
const connected = profilerService.isConnected(); setIsRemoteConnected(initiallyConnected);
setIsRemoteConnected(connected);
if (connected && data.entities && data.entities.length > 0) { const unsubscribe = profilerService.subscribe((data) => {
// 只在实体列表发生实质性变化时才更新 const connected = profilerService.isConnected();
setRemoteEntities(prev => { setIsRemoteConnected(connected);
if (prev.length !== data.entities!.length) {
return data.entities!;
}
// 检查实体ID和名称是否变化 if (connected && data.entities && data.entities.length > 0) {
const hasChanged = data.entities!.some((entity, index) => { // 只在实体列表发生实质性变化时才更新
const prevEntity = prev[index]; setRemoteEntities((prev) => {
return !prevEntity || if (prev.length !== data.entities!.length) {
return data.entities!;
}
// 检查实体ID和名称是否变化
const hasChanged = data.entities!.some((entity, index) => {
const prevEntity = prev[index];
return !prevEntity ||
prevEntity.id !== entity.id || prevEntity.id !== entity.id ||
prevEntity.name !== entity.name || prevEntity.name !== entity.name ||
prevEntity.componentCount !== entity.componentCount; prevEntity.componentCount !== entity.componentCount;
}); });
return hasChanged ? data.entities! : prev; return hasChanged ? data.entities! : prev;
});
// 请求第一个实体的详情以获取场景名称
if (!remoteSceneName && data.entities.length > 0 && data.entities[0]) {
profilerService.requestEntityDetails(data.entities[0].id);
}
} else if (!connected) {
setRemoteEntities([]);
setRemoteSceneName(null);
}
}); });
// 请求第一个实体的详情以获取场景名称 return () => unsubscribe();
if (!remoteSceneName && data.entities.length > 0 && data.entities[0]) { }, [remoteSceneName]);
profilerService.requestEntityDetails(data.entities[0].id);
}
} else if (!connected) {
setRemoteEntities([]);
setRemoteSceneName(null);
}
});
return () => unsubscribe(); // Listen for entity details to get remote scene name
}, [remoteSceneName]); useEffect(() => {
const handleEntityDetails = ((event: CustomEvent) => {
const details = event.detail;
if (details && details.sceneName) {
setRemoteSceneName(details.sceneName);
}
}) as EventListener;
// Listen for entity details to get remote scene name window.addEventListener('profiler:entity-details', handleEntityDetails);
useEffect(() => { return () => window.removeEventListener('profiler:entity-details', handleEntityDetails);
const handleEntityDetails = ((event: CustomEvent) => { }, []);
const details = event.detail;
if (details && details.sceneName) {
setRemoteSceneName(details.sceneName);
}
}) as EventListener;
window.addEventListener('profiler:entity-details', handleEntityDetails); const handleEntityClick = (entity: Entity) => {
return () => window.removeEventListener('profiler:entity-details', handleEntityDetails); entityStore.selectEntity(entity);
}, []);
const handleEntityClick = (entity: Entity) => {
entityStore.selectEntity(entity);
};
const handleRemoteEntityClick = (entity: RemoteEntity) => {
setSelectedId(entity.id);
// 请求完整的实体详情(包含组件属性)
const profilerService = (window as any).__PROFILER_SERVICE__ as ProfilerService | undefined;
if (profilerService) {
profilerService.requestEntityDetails(entity.id);
}
// 先发布基本信息,详细信息稍后通过 ProfilerService 异步返回
messageHub.publish('remote-entity:selected', {
entity: {
id: entity.id,
name: entity.name,
enabled: entity.enabled,
componentCount: entity.componentCount,
componentTypes: entity.componentTypes
}
});
};
const handleSceneNameClick = () => {
if (sceneFilePath) {
messageHub.publish('asset:reveal', { path: sceneFilePath });
}
};
const handleCreateEntity = () => {
const scene = Core.scene;
if (!scene) return;
const entityCount = entityStore.getAllEntities().length;
const entityName = `Entity ${entityCount + 1}`;
const entity = scene.createEntity(entityName);
entityStore.addEntity(entity);
entityStore.selectEntity(entity);
};
const handleDeleteEntity = async () => {
if (!selectedId) return;
const entity = entityStore.getEntity(selectedId);
if (!entity) return;
const confirmed = await confirm(
locale === 'zh'
? `确定要删除实体 "${entity.name}" 吗?此操作无法撤销。`
: `Are you sure you want to delete entity "${entity.name}"? This action cannot be undone.`,
{
title: locale === 'zh' ? '删除实体' : 'Delete Entity',
kind: 'warning'
}
);
if (confirmed) {
entity.destroy();
entityStore.removeEntity(entity);
}
};
// Listen for Delete key
useEffect(() => {
const handleKeyDown = (e: KeyboardEvent) => {
if (e.key === 'Delete' && selectedId && !isRemoteConnected) {
handleDeleteEntity();
}
}; };
window.addEventListener('keydown', handleKeyDown); const handleRemoteEntityClick = (entity: RemoteEntity) => {
return () => window.removeEventListener('keydown', handleKeyDown); setSelectedId(entity.id);
}, [selectedId, isRemoteConnected]);
// Filter entities based on search query // 请求完整的实体详情(包含组件属性)
const filterRemoteEntities = (entityList: RemoteEntity[]): RemoteEntity[] => { const profilerService = (window as any).__PROFILER_SERVICE__ as ProfilerService | undefined;
if (!searchQuery.trim()) return entityList; if (profilerService) {
profilerService.requestEntityDetails(entity.id);
}
const query = searchQuery.toLowerCase(); // 先发布基本信息,详细信息稍后通过 ProfilerService 异步返回
return entityList.filter(entity => { messageHub.publish('remote-entity:selected', {
const name = entity.name; entity: {
const id = entity.id.toString(); id: entity.id,
name: entity.name,
enabled: entity.enabled,
componentCount: entity.componentCount,
componentTypes: entity.componentTypes
}
});
};
// Search by name or ID const handleSceneNameClick = () => {
if (name.toLowerCase().includes(query) || id.includes(query)) { if (sceneFilePath) {
return true; messageHub.publish('asset:reveal', { path: sceneFilePath });
} }
};
// Search by component types const handleCreateEntity = () => {
if (Array.isArray(entity.componentTypes)) { const scene = Core.scene;
return entity.componentTypes.some(type => if (!scene) return;
type.toLowerCase().includes(query)
const entityCount = entityStore.getAllEntities().length;
const entityName = `Entity ${entityCount + 1}`;
const entity = scene.createEntity(entityName);
entityStore.addEntity(entity);
entityStore.selectEntity(entity);
};
const handleDeleteEntity = async () => {
if (!selectedId) return;
const entity = entityStore.getEntity(selectedId);
if (!entity) return;
const confirmed = await confirm(
locale === 'zh'
? `确定要删除实体 "${entity.name}" 吗?此操作无法撤销。`
: `Are you sure you want to delete entity "${entity.name}"? This action cannot be undone.`,
{
title: locale === 'zh' ? '删除实体' : 'Delete Entity',
kind: 'warning'
}
); );
}
return false; if (confirmed) {
}); entity.destroy();
}; entityStore.removeEntity(entity);
}
};
const filterLocalEntities = (entityList: Entity[]): Entity[] => { // Listen for Delete key
if (!searchQuery.trim()) return entityList; useEffect(() => {
const handleKeyDown = (e: KeyboardEvent) => {
if (e.key === 'Delete' && selectedId && !isRemoteConnected) {
handleDeleteEntity();
}
};
const query = searchQuery.toLowerCase(); window.addEventListener('keydown', handleKeyDown);
return entityList.filter(entity => { return () => window.removeEventListener('keydown', handleKeyDown);
const id = entity.id.toString(); }, [selectedId, isRemoteConnected]);
return id.includes(query);
});
};
// Determine which entities to display // Filter entities based on search query
const displayEntities = isRemoteConnected const filterRemoteEntities = (entityList: RemoteEntity[]): RemoteEntity[] => {
? filterRemoteEntities(remoteEntities) if (!searchQuery.trim()) return entityList;
: filterLocalEntities(entities);
const showRemoteIndicator = isRemoteConnected && remoteEntities.length > 0;
const displaySceneName = isRemoteConnected && remoteSceneName ? remoteSceneName : sceneName;
return ( const query = searchQuery.toLowerCase();
<div className="scene-hierarchy"> return entityList.filter((entity) => {
<div className="hierarchy-header"> const name = entity.name;
<Layers size={16} className="hierarchy-header-icon" /> const id = entity.id.toString();
<h3>{t('hierarchy.title')}</h3>
<div // Search by name or ID
className={`scene-name-container ${!isRemoteConnected && sceneFilePath ? 'clickable' : ''}`} if (name.toLowerCase().includes(query) || id.includes(query)) {
onClick={!isRemoteConnected ? handleSceneNameClick : undefined} return true;
title={!isRemoteConnected && sceneFilePath ? `${displaySceneName} - 点击跳转到文件` : displaySceneName} }
>
<span className="scene-name"> // Search by component types
{displaySceneName}{!isRemoteConnected && isSceneModified ? '*' : ''} if (Array.isArray(entity.componentTypes)) {
</span> return entity.componentTypes.some((type) =>
</div> type.toLowerCase().includes(query)
{showRemoteIndicator && ( );
<div className="remote-indicator" title="Showing remote entities"> }
<Wifi size={12} />
</div> return false;
)} });
</div> };
<div className="hierarchy-search">
<Search size={14} /> const filterLocalEntities = (entityList: Entity[]): Entity[] => {
<input if (!searchQuery.trim()) return entityList;
type="text"
placeholder={t('hierarchy.search') || 'Search entities...'} const query = searchQuery.toLowerCase();
value={searchQuery} return entityList.filter((entity) => {
onChange={(e) => setSearchQuery(e.target.value)} const id = entity.id.toString();
/> return id.includes(query);
</div> });
{!isRemoteConnected && ( };
<div className="hierarchy-toolbar">
<button // Determine which entities to display
className="toolbar-btn" const displayEntities = isRemoteConnected
onClick={handleCreateEntity} ? filterRemoteEntities(remoteEntities)
title={locale === 'zh' ? '创建实体' : 'Create Entity'} : filterLocalEntities(entities);
> const showRemoteIndicator = isRemoteConnected && remoteEntities.length > 0;
<Plus size={14} /> const displaySceneName = isRemoteConnected && remoteSceneName ? remoteSceneName : sceneName;
<span>{locale === 'zh' ? '创建实体' : 'Create Entity'}</span>
</button> return (
<button <div className="scene-hierarchy">
className="toolbar-btn" <div className="hierarchy-header">
onClick={handleDeleteEntity} <Layers size={16} className="hierarchy-header-icon" />
disabled={!selectedId} <h3>{t('hierarchy.title')}</h3>
title={locale === 'zh' ? '删除实体' : 'Delete Entity'} <div
> className={`scene-name-container ${!isRemoteConnected && sceneFilePath ? 'clickable' : ''}`}
<Trash2 size={14} /> onClick={!isRemoteConnected ? handleSceneNameClick : undefined}
</button> title={!isRemoteConnected && sceneFilePath ? `${displaySceneName} - 点击跳转到文件` : displaySceneName}
</div> >
)} <span className="scene-name">
<div className="hierarchy-content scrollable"> {displaySceneName}{!isRemoteConnected && isSceneModified ? '*' : ''}
{displayEntities.length === 0 ? ( </span>
<div className="empty-state"> </div>
<Box size={48} strokeWidth={1.5} className="empty-icon" /> {showRemoteIndicator && (
<div className="empty-title">{t('hierarchy.empty')}</div> <div className="remote-indicator" title="Showing remote entities">
<div className="empty-hint"> <Wifi size={12} />
{isRemoteConnected </div>
? 'No entities in remote game' )}
: 'Create an entity to get started'}
</div> </div>
</div> <div className="hierarchy-search">
) : isRemoteConnected ? ( <Search size={14} />
<ul className="entity-list"> <input
{(displayEntities as RemoteEntity[]).map(entity => ( type="text"
<li placeholder={t('hierarchy.search') || 'Search entities...'}
key={entity.id} value={searchQuery}
className={`entity-item remote-entity ${selectedId === entity.id ? 'selected' : ''} ${!entity.enabled ? 'disabled' : ''}`} onChange={(e) => setSearchQuery(e.target.value)}
title={`${entity.name} - ${entity.componentTypes.join(', ')}`} />
onClick={() => handleRemoteEntityClick(entity)} </div>
> {!isRemoteConnected && (
<Box size={14} className="entity-icon" /> <div className="hierarchy-toolbar">
<span className="entity-name">{entity.name}</span> <button
{entity.tag !== 0 && ( className="toolbar-btn"
<span className="entity-tag" title={`Tag: ${entity.tag}`}> onClick={handleCreateEntity}
title={locale === 'zh' ? '创建实体' : 'Create Entity'}
>
<Plus size={14} />
<span>{locale === 'zh' ? '创建实体' : 'Create Entity'}</span>
</button>
<button
className="toolbar-btn"
onClick={handleDeleteEntity}
disabled={!selectedId}
title={locale === 'zh' ? '删除实体' : 'Delete Entity'}
>
<Trash2 size={14} />
</button>
</div>
)}
<div className="hierarchy-content scrollable">
{displayEntities.length === 0 ? (
<div className="empty-state">
<Box size={48} strokeWidth={1.5} className="empty-icon" />
<div className="empty-title">{t('hierarchy.empty')}</div>
<div className="empty-hint">
{isRemoteConnected
? 'No entities in remote game'
: 'Create an entity to get started'}
</div>
</div>
) : isRemoteConnected ? (
<ul className="entity-list">
{(displayEntities as RemoteEntity[]).map((entity) => (
<li
key={entity.id}
className={`entity-item remote-entity ${selectedId === entity.id ? 'selected' : ''} ${!entity.enabled ? 'disabled' : ''}`}
title={`${entity.name} - ${entity.componentTypes.join(', ')}`}
onClick={() => handleRemoteEntityClick(entity)}
>
<Box size={14} className="entity-icon" />
<span className="entity-name">{entity.name}</span>
{entity.tag !== 0 && (
<span className="entity-tag" title={`Tag: ${entity.tag}`}>
#{entity.tag} #{entity.tag}
</span> </span>
)}
{entity.componentCount > 0 && (
<span className="component-count">{entity.componentCount}</span>
)}
</li>
))}
</ul>
) : (
<ul className="entity-list">
{entities.map((entity) => (
<li
key={entity.id}
className={`entity-item ${selectedId === entity.id ? 'selected' : ''}`}
onClick={() => handleEntityClick(entity)}
>
<Box size={14} className="entity-icon" />
<span className="entity-name">Entity {entity.id}</span>
</li>
))}
</ul>
)} )}
{entity.componentCount > 0 && ( </div>
<span className="component-count">{entity.componentCount}</span> </div>
)} );
</li>
))}
</ul>
) : (
<ul className="entity-list">
{entities.map(entity => (
<li
key={entity.id}
className={`entity-item ${selectedId === entity.id ? 'selected' : ''}`}
onClick={() => handleEntityClick(entity)}
>
<Box size={14} className="entity-icon" />
<span className="entity-name">Entity {entity.id}</span>
</li>
))}
</ul>
)}
</div>
</div>
);
} }

View File

@@ -10,286 +10,286 @@ interface SettingsWindowProps {
} }
export function SettingsWindow({ onClose, settingsRegistry }: SettingsWindowProps) { export function SettingsWindow({ onClose, settingsRegistry }: SettingsWindowProps) {
const [categories, setCategories] = useState<SettingCategory[]>([]); const [categories, setCategories] = useState<SettingCategory[]>([]);
const [selectedCategoryId, setSelectedCategoryId] = useState<string | null>(null); const [selectedCategoryId, setSelectedCategoryId] = useState<string | null>(null);
const [values, setValues] = useState<Map<string, any>>(new Map()); const [values, setValues] = useState<Map<string, any>>(new Map());
const [errors, setErrors] = useState<Map<string, string>>(new Map()); const [errors, setErrors] = useState<Map<string, string>>(new Map());
useEffect(() => { useEffect(() => {
const allCategories = settingsRegistry.getAllCategories(); const allCategories = settingsRegistry.getAllCategories();
setCategories(allCategories); setCategories(allCategories);
if (allCategories.length > 0 && !selectedCategoryId) { if (allCategories.length > 0 && !selectedCategoryId) {
const firstCategory = allCategories[0]; const firstCategory = allCategories[0];
if (firstCategory) { if (firstCategory) {
setSelectedCategoryId(firstCategory.id); setSelectedCategoryId(firstCategory.id);
} }
} }
const settings = SettingsService.getInstance(); const settings = SettingsService.getInstance();
const allSettings = settingsRegistry.getAllSettings(); const allSettings = settingsRegistry.getAllSettings();
const initialValues = new Map<string, any>(); const initialValues = new Map<string, any>();
for (const [key, descriptor] of allSettings.entries()) { for (const [key, descriptor] of allSettings.entries()) {
const value = settings.get(key, descriptor.defaultValue); const value = settings.get(key, descriptor.defaultValue);
initialValues.set(key, value); initialValues.set(key, value);
} }
setValues(initialValues); setValues(initialValues);
}, [settingsRegistry, selectedCategoryId]); }, [settingsRegistry, selectedCategoryId]);
const handleValueChange = (key: string, value: any, descriptor: SettingDescriptor) => { const handleValueChange = (key: string, value: any, descriptor: SettingDescriptor) => {
const newValues = new Map(values); const newValues = new Map(values);
newValues.set(key, value); newValues.set(key, value);
setValues(newValues); setValues(newValues);
const newErrors = new Map(errors); const newErrors = new Map(errors);
if (!settingsRegistry.validateSetting(descriptor, value)) { if (!settingsRegistry.validateSetting(descriptor, value)) {
newErrors.set(key, descriptor.validator?.errorMessage || 'Invalid value'); newErrors.set(key, descriptor.validator?.errorMessage || 'Invalid value');
} else { } else {
newErrors.delete(key); newErrors.delete(key);
} }
setErrors(newErrors); setErrors(newErrors);
}; };
const handleSave = () => { const handleSave = () => {
if (errors.size > 0) { if (errors.size > 0) {
return; return;
} }
const settings = SettingsService.getInstance(); const settings = SettingsService.getInstance();
const changedSettings: Record<string, any> = {}; const changedSettings: Record<string, any> = {};
for (const [key, value] of values.entries()) { for (const [key, value] of values.entries()) {
settings.set(key, value); settings.set(key, value);
changedSettings[key] = value; changedSettings[key] = value;
} }
window.dispatchEvent(new CustomEvent('settings:changed', { window.dispatchEvent(new CustomEvent('settings:changed', {
detail: changedSettings detail: changedSettings
})); }));
onClose(); onClose();
}; };
const handleCancel = () => { const handleCancel = () => {
onClose(); onClose();
}; };
const renderSettingInput = (setting: SettingDescriptor) => { const renderSettingInput = (setting: SettingDescriptor) => {
const value = values.get(setting.key) ?? setting.defaultValue; const value = values.get(setting.key) ?? setting.defaultValue;
const error = errors.get(setting.key); const error = errors.get(setting.key);
switch (setting.type) { switch (setting.type) {
case 'boolean': case 'boolean':
return ( return (
<div className="settings-field"> <div className="settings-field">
<label className="settings-label settings-label-checkbox"> <label className="settings-label settings-label-checkbox">
<input <input
type="checkbox" type="checkbox"
className="settings-checkbox" className="settings-checkbox"
checked={value} checked={value}
onChange={(e) => handleValueChange(setting.key, e.target.checked, setting)} onChange={(e) => handleValueChange(setting.key, e.target.checked, setting)}
/> />
<span>{setting.label}</span> <span>{setting.label}</span>
{setting.description && ( {setting.description && (
<span className="settings-hint">{setting.description}</span> <span className="settings-hint">{setting.description}</span>
)} )}
</label> </label>
{error && <span className="settings-error">{error}</span>} {error && <span className="settings-error">{error}</span>}
</div> </div>
); );
case 'number': case 'number':
return ( return (
<div className="settings-field"> <div className="settings-field">
<label className="settings-label"> <label className="settings-label">
{setting.label} {setting.label}
{setting.description && ( {setting.description && (
<span className="settings-hint">{setting.description}</span> <span className="settings-hint">{setting.description}</span>
)} )}
</label> </label>
<input <input
type="number" type="number"
className={`settings-input ${error ? 'settings-input-error' : ''}`} className={`settings-input ${error ? 'settings-input-error' : ''}`}
value={value} value={value}
onChange={(e) => handleValueChange(setting.key, parseInt(e.target.value) || 0, setting)} onChange={(e) => handleValueChange(setting.key, parseInt(e.target.value) || 0, setting)}
placeholder={setting.placeholder} placeholder={setting.placeholder}
min={setting.min} min={setting.min}
max={setting.max} max={setting.max}
step={setting.step} step={setting.step}
/> />
{error && <span className="settings-error">{error}</span>} {error && <span className="settings-error">{error}</span>}
</div> </div>
); );
case 'string': case 'string':
return ( return (
<div className="settings-field"> <div className="settings-field">
<label className="settings-label"> <label className="settings-label">
{setting.label} {setting.label}
{setting.description && ( {setting.description && (
<span className="settings-hint">{setting.description}</span> <span className="settings-hint">{setting.description}</span>
)} )}
</label> </label>
<input <input
type="text" type="text"
className={`settings-input ${error ? 'settings-input-error' : ''}`} className={`settings-input ${error ? 'settings-input-error' : ''}`}
value={value} value={value}
onChange={(e) => handleValueChange(setting.key, e.target.value, setting)} onChange={(e) => handleValueChange(setting.key, e.target.value, setting)}
placeholder={setting.placeholder} placeholder={setting.placeholder}
/> />
{error && <span className="settings-error">{error}</span>} {error && <span className="settings-error">{error}</span>}
</div> </div>
); );
case 'select': case 'select':
return ( return (
<div className="settings-field"> <div className="settings-field">
<label className="settings-label"> <label className="settings-label">
{setting.label} {setting.label}
{setting.description && ( {setting.description && (
<span className="settings-hint">{setting.description}</span> <span className="settings-hint">{setting.description}</span>
)} )}
</label> </label>
<select <select
className={`settings-select ${error ? 'settings-input-error' : ''}`} className={`settings-select ${error ? 'settings-input-error' : ''}`}
value={value} value={value}
onChange={(e) => { onChange={(e) => {
const option = setting.options?.find(opt => String(opt.value) === e.target.value); const option = setting.options?.find((opt) => String(opt.value) === e.target.value);
if (option) { if (option) {
handleValueChange(setting.key, option.value, setting); handleValueChange(setting.key, option.value, setting);
} }
}} }}
> >
{setting.options?.map((option) => ( {setting.options?.map((option) => (
<option key={String(option.value)} value={String(option.value)}> <option key={String(option.value)} value={String(option.value)}>
{option.label} {option.label}
</option> </option>
))} ))}
</select> </select>
{error && <span className="settings-error">{error}</span>} {error && <span className="settings-error">{error}</span>}
</div> </div>
); );
case 'range': case 'range':
return ( return (
<div className="settings-field"> <div className="settings-field">
<label className="settings-label"> <label className="settings-label">
{setting.label} {setting.label}
{setting.description && ( {setting.description && (
<span className="settings-hint">{setting.description}</span> <span className="settings-hint">{setting.description}</span>
)} )}
</label> </label>
<div className="settings-range-wrapper"> <div className="settings-range-wrapper">
<input <input
type="range" type="range"
className="settings-range" className="settings-range"
value={value} value={value}
onChange={(e) => handleValueChange(setting.key, parseFloat(e.target.value), setting)} onChange={(e) => handleValueChange(setting.key, parseFloat(e.target.value), setting)}
min={setting.min} min={setting.min}
max={setting.max} max={setting.max}
step={setting.step} step={setting.step}
/> />
<span className="settings-range-value">{value}</span> <span className="settings-range-value">{value}</span>
</div> </div>
{error && <span className="settings-error">{error}</span>} {error && <span className="settings-error">{error}</span>}
</div> </div>
); );
case 'color': case 'color':
return ( return (
<div className="settings-field"> <div className="settings-field">
<label className="settings-label"> <label className="settings-label">
{setting.label} {setting.label}
{setting.description && ( {setting.description && (
<span className="settings-hint">{setting.description}</span> <span className="settings-hint">{setting.description}</span>
)} )}
</label> </label>
<input <input
type="color" type="color"
className="settings-color-input" className="settings-color-input"
value={value} value={value}
onChange={(e) => handleValueChange(setting.key, e.target.value, setting)} onChange={(e) => handleValueChange(setting.key, e.target.value, setting)}
/> />
{error && <span className="settings-error">{error}</span>} {error && <span className="settings-error">{error}</span>}
</div> </div>
); );
default: default:
return null; return null;
} }
}; };
const selectedCategory = categories.find(c => c.id === selectedCategoryId); const selectedCategory = categories.find((c) => c.id === selectedCategoryId);
return ( return (
<div className="settings-overlay"> <div className="settings-overlay">
<div className="settings-window"> <div className="settings-window">
<div className="settings-header"> <div className="settings-header">
<div className="settings-title"> <div className="settings-title">
<SettingsIcon size={18} /> <SettingsIcon size={18} />
<h2></h2> <h2></h2>
</div> </div>
<button className="settings-close-btn" onClick={handleCancel}> <button className="settings-close-btn" onClick={handleCancel}>
<X size={18} /> <X size={18} />
</button> </button>
</div> </div>
<div className="settings-body"> <div className="settings-body">
<div className="settings-sidebar"> <div className="settings-sidebar">
{categories.map((category) => ( {categories.map((category) => (
<button <button
key={category.id} key={category.id}
className={`settings-category-btn ${selectedCategoryId === category.id ? 'active' : ''}`} className={`settings-category-btn ${selectedCategoryId === category.id ? 'active' : ''}`}
onClick={() => setSelectedCategoryId(category.id)} onClick={() => setSelectedCategoryId(category.id)}
> >
<span className="settings-category-title">{category.title}</span> <span className="settings-category-title">{category.title}</span>
{category.description && ( {category.description && (
<span className="settings-category-desc">{category.description}</span> <span className="settings-category-desc">{category.description}</span>
)} )}
<ChevronRight size={14} className="settings-category-arrow" /> <ChevronRight size={14} className="settings-category-arrow" />
</button> </button>
))} ))}
</div> </div>
<div className="settings-content"> <div className="settings-content">
{selectedCategory && selectedCategory.sections.map((section) => ( {selectedCategory && selectedCategory.sections.map((section) => (
<div key={section.id} className="settings-section"> <div key={section.id} className="settings-section">
<h3 className="settings-section-title">{section.title}</h3> <h3 className="settings-section-title">{section.title}</h3>
{section.description && ( {section.description && (
<p className="settings-section-description">{section.description}</p> <p className="settings-section-description">{section.description}</p>
)} )}
{section.settings.map((setting) => ( {section.settings.map((setting) => (
<div key={setting.key}> <div key={setting.key}>
{renderSettingInput(setting)} {renderSettingInput(setting)}
</div> </div>
))} ))}
</div> </div>
))} ))}
{!selectedCategory && ( {!selectedCategory && (
<div className="settings-empty"> <div className="settings-empty">
<SettingsIcon size={48} /> <SettingsIcon size={48} />
<p></p> <p></p>
</div> </div>
)} )}
</div> </div>
</div> </div>
<div className="settings-footer"> <div className="settings-footer">
<button className="settings-btn settings-btn-cancel" onClick={handleCancel}> <button className="settings-btn settings-btn-cancel" onClick={handleCancel}>
</button> </button>
<button <button
className="settings-btn settings-btn-save" className="settings-btn settings-btn-save"
onClick={handleSave} onClick={handleSave}
disabled={errors.size > 0} disabled={errors.size > 0}
> >
</button> </button>
</div>
</div>
</div> </div>
</div> );
</div>
);
} }

View File

@@ -11,98 +11,98 @@ interface StartupPageProps {
} }
export function StartupPage({ onOpenProject, onCreateProject, onOpenRecentProject, onProfilerMode, recentProjects = [], locale }: StartupPageProps) { export function StartupPage({ onOpenProject, onCreateProject, onOpenRecentProject, onProfilerMode, recentProjects = [], locale }: StartupPageProps) {
const [hoveredProject, setHoveredProject] = useState<string | null>(null); const [hoveredProject, setHoveredProject] = useState<string | null>(null);
const translations = { const translations = {
en: { en: {
title: 'ECS Framework Editor', title: 'ECS Framework Editor',
subtitle: 'Professional Game Development Tool', subtitle: 'Professional Game Development Tool',
openProject: 'Open Project', openProject: 'Open Project',
createProject: 'Create Project', createProject: 'Create Project',
profilerMode: 'Profiler Mode', profilerMode: 'Profiler Mode',
recentProjects: 'Recent Projects', recentProjects: 'Recent Projects',
noRecentProjects: 'No recent projects', noRecentProjects: 'No recent projects',
version: 'Version 1.0.0', version: 'Version 1.0.0',
comingSoon: 'Coming Soon' comingSoon: 'Coming Soon'
}, },
zh: { zh: {
title: 'ECS 框架编辑器', title: 'ECS 框架编辑器',
subtitle: '专业游戏开发工具', subtitle: '专业游戏开发工具',
openProject: '打开项目', openProject: '打开项目',
createProject: '创建新项目', createProject: '创建新项目',
profilerMode: '性能分析模式', profilerMode: '性能分析模式',
recentProjects: '最近的项目', recentProjects: '最近的项目',
noRecentProjects: '没有最近的项目', noRecentProjects: '没有最近的项目',
version: '版本 1.0.0', version: '版本 1.0.0',
comingSoon: '即将推出' comingSoon: '即将推出'
} }
}; };
const t = translations[locale as keyof typeof translations] || translations.en; const t = translations[locale as keyof typeof translations] || translations.en;
return ( return (
<div className="startup-page"> <div className="startup-page">
<div className="startup-header"> <div className="startup-header">
<h1 className="startup-title">{t.title}</h1> <h1 className="startup-title">{t.title}</h1>
<p className="startup-subtitle">{t.subtitle}</p> <p className="startup-subtitle">{t.subtitle}</p>
</div> </div>
<div className="startup-content"> <div className="startup-content">
<div className="startup-actions"> <div className="startup-actions">
<button className="startup-action-btn primary" onClick={onOpenProject}> <button className="startup-action-btn primary" onClick={onOpenProject}>
<svg className="btn-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor"> <svg className="btn-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor">
<path d="M3 7V17C3 18.1046 3.89543 19 5 19H19C20.1046 19 21 18.1046 21 17V9C21 7.89543 20.1046 7 19 7H13L11 5H5C3.89543 5 3 5.89543 3 7Z" strokeWidth="2"/> <path d="M3 7V17C3 18.1046 3.89543 19 5 19H19C20.1046 19 21 18.1046 21 17V9C21 7.89543 20.1046 7 19 7H13L11 5H5C3.89543 5 3 5.89543 3 7Z" strokeWidth="2"/>
</svg> </svg>
<span>{t.openProject}</span> <span>{t.openProject}</span>
</button> </button>
<button className="startup-action-btn" onClick={onCreateProject}> <button className="startup-action-btn" onClick={onCreateProject}>
<svg className="btn-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor"> <svg className="btn-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor">
<path d="M12 5V19M5 12H19" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/> <path d="M12 5V19M5 12H19" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>
</svg> </svg>
<span>{t.createProject}</span> <span>{t.createProject}</span>
</button> </button>
<button className="startup-action-btn" onClick={onProfilerMode}> <button className="startup-action-btn" onClick={onProfilerMode}>
<svg className="btn-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor"> <svg className="btn-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor">
<path d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/> <path d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>
</svg> </svg>
<span>{t.profilerMode}</span> <span>{t.profilerMode}</span>
</button> </button>
</div>
<div className="startup-recent">
<h2 className="recent-title">{t.recentProjects}</h2>
{recentProjects.length === 0 ? (
<p className="recent-empty">{t.noRecentProjects}</p>
) : (
<ul className="recent-list">
{recentProjects.map((project, index) => (
<li
key={index}
className={`recent-item ${hoveredProject === project ? 'hovered' : ''}`}
onMouseEnter={() => setHoveredProject(project)}
onMouseLeave={() => setHoveredProject(null)}
onClick={() => onOpenRecentProject?.(project)}
style={{ cursor: onOpenRecentProject ? 'pointer' : 'default' }}
>
<svg className="recent-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor">
<path d="M3 7V17C3 18.1046 3.89543 19 5 19H19C20.1046 19 21 18.1046 21 17V9C21 7.89543 20.1046 7 19 7H13L11 5H5C3.89543 5 3 5.89543 3 7Z" strokeWidth="2"/>
</svg>
<div className="recent-info">
<div className="recent-name">{project.split(/[\\/]/).pop()}</div>
<div className="recent-path">{project}</div>
</div>
</li>
))}
</ul>
)}
</div>
</div>
<div className="startup-footer">
<span className="startup-version">{t.version}</span>
</div>
</div> </div>
);
<div className="startup-recent">
<h2 className="recent-title">{t.recentProjects}</h2>
{recentProjects.length === 0 ? (
<p className="recent-empty">{t.noRecentProjects}</p>
) : (
<ul className="recent-list">
{recentProjects.map((project, index) => (
<li
key={index}
className={`recent-item ${hoveredProject === project ? 'hovered' : ''}`}
onMouseEnter={() => setHoveredProject(project)}
onMouseLeave={() => setHoveredProject(null)}
onClick={() => onOpenRecentProject?.(project)}
style={{ cursor: onOpenRecentProject ? 'pointer' : 'default' }}
>
<svg className="recent-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor">
<path d="M3 7V17C3 18.1046 3.89543 19 5 19H19C20.1046 19 21 18.1046 21 17V9C21 7.89543 20.1046 7 19 7H13L11 5H5C3.89543 5 3 5.89543 3 7Z" strokeWidth="2"/>
</svg>
<div className="recent-info">
<div className="recent-name">{project.split(/[\\/]/).pop()}</div>
<div className="recent-path">{project}</div>
</div>
</li>
))}
</ul>
)}
</div>
</div>
<div className="startup-footer">
<span className="startup-version">{t.version}</span>
</div>
</div>
);
} }

View File

@@ -37,7 +37,7 @@ export const ToastProvider: React.FC<ToastProviderProps> = ({ children }) => {
const id = `toast-${Date.now()}-${Math.random()}`; const id = `toast-${Date.now()}-${Math.random()}`;
const toast: Toast = { id, message, type, duration }; const toast: Toast = { id, message, type, duration };
setToasts(prev => [...prev, toast]); setToasts((prev) => [...prev, toast]);
if (duration > 0) { if (duration > 0) {
setTimeout(() => { setTimeout(() => {
@@ -47,7 +47,7 @@ export const ToastProvider: React.FC<ToastProviderProps> = ({ children }) => {
}, []); }, []);
const hideToast = useCallback((id: string) => { const hideToast = useCallback((id: string) => {
setToasts(prev => prev.filter(t => t.id !== id)); setToasts((prev) => prev.filter((t) => t.id !== id));
}, []); }, []);
const getIcon = (type: ToastType) => { const getIcon = (type: ToastType) => {
@@ -67,7 +67,7 @@ export const ToastProvider: React.FC<ToastProviderProps> = ({ children }) => {
<ToastContext.Provider value={{ showToast, hideToast }}> <ToastContext.Provider value={{ showToast, hideToast }}>
{children} {children}
<div className="toast-container"> <div className="toast-container">
{toasts.map(toast => ( {toasts.map((toast) => (
<div key={toast.id} className={`toast toast-${toast.type}`}> <div key={toast.id} className={`toast toast-${toast.type}`}>
<div className="toast-icon"> <div className="toast-icon">
{getIcon(toast.type)} {getIcon(toast.type)}

File diff suppressed because it is too large Load Diff

View File

@@ -183,7 +183,7 @@ export const ${opts.constantsName} = {} as const;`;
if (Object.keys(grouped).length === 1 && grouped[''] !== undefined) { if (Object.keys(grouped).length === 1 && grouped[''] !== undefined) {
// 无命名空间,扁平结构 // 无命名空间,扁平结构
const entries = variables const entries = variables
.map(v => ` ${this.transformName(v.name, opts.constantCase)}: ${quote}${v.name}${quote}`) .map((v) => ` ${this.transformName(v.name, opts.constantCase)}: ${quote}${v.name}${quote}`)
.join(',\n'); .join(',\n');
return `/** return `/**
@@ -200,13 +200,13 @@ ${entries}
if (namespace === '') { if (namespace === '') {
// 根级别变量 // 根级别变量
return vars return vars
.map(v => ` ${this.transformName(v.name, opts.constantCase)}: ${quote}${v.name}${quote}`) .map((v) => ` ${this.transformName(v.name, opts.constantCase)}: ${quote}${v.name}${quote}`)
.join(',\n'); .join(',\n');
} else { } else {
// 命名空间变量 // 命名空间变量
const nsName = this.toPascalCase(namespace); const nsName = this.toPascalCase(namespace);
const entries = vars const entries = vars
.map(v => { .map((v) => {
const shortName = v.name.substring(namespace.length + 1); const shortName = v.name.substring(namespace.length + 1);
return ` ${this.transformName(shortName, opts.constantCase)}: ${quote}${v.name}${quote}`; return ` ${this.transformName(shortName, opts.constantCase)}: ${quote}${v.name}${quote}`;
}) })
@@ -238,7 +238,7 @@ export interface ${opts.interfaceName} {}`;
} }
const properties = variables const properties = variables
.map(v => { .map((v) => {
const tsType = this.mapBlackboardTypeToTS(v.type); const tsType = this.mapBlackboardTypeToTS(v.type);
const comment = v.description ? ` /** ${v.description} */\n` : ''; const comment = v.description ? ` /** ${v.description} */\n` : '';
return `${comment} ${v.name}: ${tsType};`; return `${comment} ${v.name}: ${tsType};`;
@@ -334,7 +334,7 @@ export const ${opts.defaultsName}: ${opts.interfaceName} = {};`;
} }
const properties = variables const properties = variables
.map(v => { .map((v) => {
const value = this.formatValue(v.value, v.type, opts); const value = this.formatValue(v.value, v.type, opts);
return ` ${v.name}: ${value}`; return ` ${v.name}: ${value}`;
}) })
@@ -407,7 +407,7 @@ ${properties}
private static toPascalCase(str: string): string { private static toPascalCase(str: string): string {
return str return str
.split(/[._-]/) .split(/[._-]/)
.map(part => part.charAt(0).toUpperCase() + part.slice(1).toLowerCase()) .map((part) => part.charAt(0).toUpperCase() + part.slice(1).toLowerCase())
.join(''); .join('');
} }
@@ -495,7 +495,7 @@ ${properties}
const parts = str.split(/[._-]/); const parts = str.split(/[._-]/);
if (parts.length === 0) return str; if (parts.length === 0) return str;
return (parts[0] || '').toLowerCase() + parts.slice(1) return (parts[0] || '').toLowerCase() + parts.slice(1)
.map(part => part.charAt(0).toUpperCase() + part.slice(1).toLowerCase()) .map((part) => part.charAt(0).toUpperCase() + part.slice(1).toLowerCase())
.join(''); .join('');
} }
} }

View File

@@ -1,5 +1,3 @@
import { BlackboardValueType } from '@esengine/behavior-tree';
/** /**
* 局部黑板变量信息 * 局部黑板变量信息
*/ */
@@ -243,7 +241,7 @@ export function is${this.toPascalCase(treeName)}Variable(
const escaped = value const escaped = value
.replace(/\\/g, '\\\\') .replace(/\\/g, '\\\\')
.replace(quoteStyle === 'single' ? /'/g : /"/g, .replace(quoteStyle === 'single' ? /'/g : /"/g,
quoteStyle === 'single' ? "\\'" : '\\"'); quoteStyle === 'single' ? "\\'" : '\\"');
return `${quote}${escaped}${quote}`; return `${quote}${escaped}${quote}`;
case 'number': case 'number':
case 'boolean': case 'boolean':
@@ -253,7 +251,7 @@ export function is${this.toPascalCase(treeName)}Variable(
if (value.length === 0) { if (value.length === 0) {
return '[]'; return '[]';
} }
const items = value.map(v => this.formatValue(v, quoteStyle)).join(', '); const items = value.map((v) => this.formatValue(v, quoteStyle)).join(', ');
return `[${items}]`; return `[${items}]`;
} }
// Vector2/Vector3 // Vector2/Vector3
@@ -286,7 +284,7 @@ export function is${this.toPascalCase(treeName)}Variable(
private static toPascalCase(str: string): string { private static toPascalCase(str: string): string {
return str return str
.split(/[._-]/) .split(/[._-]/)
.map(part => part.charAt(0).toUpperCase() + part.slice(1).toLowerCase()) .map((part) => part.charAt(0).toUpperCase() + part.slice(1).toLowerCase())
.join(''); .join('');
} }
} }

View File

@@ -3,28 +3,28 @@ import { Core } from '@esengine/ecs-framework';
import { LocaleService, type Locale } from '@esengine/editor-core'; import { LocaleService, type Locale } from '@esengine/editor-core';
export function useLocale() { export function useLocale() {
const localeService = useMemo(() => Core.services.resolve(LocaleService), []); const localeService = useMemo(() => Core.services.resolve(LocaleService), []);
const [locale, setLocale] = useState<Locale>(() => localeService.getCurrentLocale()); const [locale, setLocale] = useState<Locale>(() => localeService.getCurrentLocale());
useEffect(() => { useEffect(() => {
const unsubscribe = localeService.onChange((newLocale) => { const unsubscribe = localeService.onChange((newLocale) => {
setLocale(newLocale); setLocale(newLocale);
}); });
return unsubscribe; return unsubscribe;
}, [localeService]); }, [localeService]);
const t = useCallback((key: string, fallback?: string) => { const t = useCallback((key: string, fallback?: string) => {
return localeService.t(key, fallback); return localeService.t(key, fallback);
}, [localeService]); }, [localeService]);
const changeLocale = useCallback((newLocale: Locale) => { const changeLocale = useCallback((newLocale: Locale) => {
localeService.setLocale(newLocale); localeService.setLocale(newLocale);
}, [localeService]); }, [localeService]);
return { return {
locale, locale,
t, t,
changeLocale changeLocale
}; };
} }

View File

@@ -6,7 +6,7 @@ import './styles/index.css';
import './i18n/config'; import './i18n/config';
ReactDOM.createRoot(document.getElementById('root')!).render( ReactDOM.createRoot(document.getElementById('root')!).render(
<React.StrictMode> <React.StrictMode>
<App /> <App />
</React.StrictMode> </React.StrictMode>
); );

View File

@@ -11,87 +11,87 @@ const logger = createLogger('EditorAppearancePlugin');
* Manages editor appearance settings like font size * Manages editor appearance settings like font size
*/ */
export class EditorAppearancePlugin implements IEditorPlugin { export class EditorAppearancePlugin implements IEditorPlugin {
readonly name = '@esengine/editor-appearance'; readonly name = '@esengine/editor-appearance';
readonly version = '1.0.0'; readonly version = '1.0.0';
readonly displayName = 'Editor Appearance'; readonly displayName = 'Editor Appearance';
readonly category = EditorPluginCategory.System; readonly category = EditorPluginCategory.System;
readonly description = 'Configure editor appearance settings'; readonly description = 'Configure editor appearance settings';
readonly icon = '🎨'; readonly icon = '🎨';
async install(_core: Core, services: ServiceContainer): Promise<void> { async install(_core: Core, services: ServiceContainer): Promise<void> {
const settingsRegistry = services.resolve(SettingsRegistry); const settingsRegistry = services.resolve(SettingsRegistry);
settingsRegistry.registerCategory({ settingsRegistry.registerCategory({
id: 'appearance', id: 'appearance',
title: '外观', title: '外观',
description: '配置编辑器的外观设置', description: '配置编辑器的外观设置',
sections: [ sections: [
{ {
id: 'font', id: 'font',
title: '字体设置', title: '字体设置',
description: '配置编辑器字体样式', description: '配置编辑器字体样式',
settings: [ settings: [
{ {
key: 'editor.fontSize', key: 'editor.fontSize',
label: '字体大小 (px)', label: '字体大小 (px)',
type: 'range', type: 'range',
defaultValue: 13, defaultValue: 13,
description: '编辑器界面的字体大小', description: '编辑器界面的字体大小',
min: 11, min: 11,
max: 18, max: 18,
step: 1 step: 1
} }
] ]
} }
] ]
}); });
this.applyFontSettings(); this.applyFontSettings();
this.setupSettingsListener(); this.setupSettingsListener();
logger.info('Installed'); logger.info('Installed');
} }
async uninstall(): Promise<void> { async uninstall(): Promise<void> {
logger.info('Uninstalled'); logger.info('Uninstalled');
} }
async onEditorReady(): Promise<void> { async onEditorReady(): Promise<void> {
logger.info('Editor is ready'); logger.info('Editor is ready');
} }
/** /**
* Apply font settings from settings * Apply font settings from settings
*/ */
private applyFontSettings(): void { private applyFontSettings(): void {
const settings = SettingsService.getInstance(); const settings = SettingsService.getInstance();
const baseFontSize = settings.get<number>('editor.fontSize', 13); const baseFontSize = settings.get<number>('editor.fontSize', 13);
logger.info(`Applying font size: ${baseFontSize}px`); logger.info(`Applying font size: ${baseFontSize}px`);
const root = document.documentElement; const root = document.documentElement;
// Apply font sizes // Apply font sizes
root.style.setProperty('--font-size-xs', `${baseFontSize - 2}px`); root.style.setProperty('--font-size-xs', `${baseFontSize - 2}px`);
root.style.setProperty('--font-size-sm', `${baseFontSize - 1}px`); root.style.setProperty('--font-size-sm', `${baseFontSize - 1}px`);
root.style.setProperty('--font-size-base', `${baseFontSize}px`); root.style.setProperty('--font-size-base', `${baseFontSize}px`);
root.style.setProperty('--font-size-md', `${baseFontSize + 1}px`); root.style.setProperty('--font-size-md', `${baseFontSize + 1}px`);
root.style.setProperty('--font-size-lg', `${baseFontSize + 3}px`); root.style.setProperty('--font-size-lg', `${baseFontSize + 3}px`);
root.style.setProperty('--font-size-xl', `${baseFontSize + 5}px`); root.style.setProperty('--font-size-xl', `${baseFontSize + 5}px`);
} }
/** /**
* Listen for settings changes * Listen for settings changes
*/ */
private setupSettingsListener(): void { private setupSettingsListener(): void {
window.addEventListener('settings:changed', ((event: CustomEvent) => { window.addEventListener('settings:changed', ((event: CustomEvent) => {
const changedSettings = event.detail; const changedSettings = event.detail;
logger.info('Settings changed event received', changedSettings); logger.info('Settings changed event received', changedSettings);
if ('editor.fontSize' in changedSettings) { if ('editor.fontSize' in changedSettings) {
logger.info('Font size changed, applying...'); logger.info('Font size changed, applying...');
this.applyFontSettings(); this.applyFontSettings();
} }
}) as EventListener); }) as EventListener);
} }
} }

View File

@@ -9,135 +9,135 @@ import { ProfilerService } from '../services/ProfilerService';
* Displays real-time performance metrics for ECS systems * Displays real-time performance metrics for ECS systems
*/ */
export class ProfilerPlugin implements IEditorPlugin { export class ProfilerPlugin implements IEditorPlugin {
readonly name = '@esengine/profiler'; readonly name = '@esengine/profiler';
readonly version = '1.0.0'; readonly version = '1.0.0';
readonly displayName = 'Performance Profiler'; readonly displayName = 'Performance Profiler';
readonly category = EditorPluginCategory.Tool; readonly category = EditorPluginCategory.Tool;
readonly description = 'Real-time performance monitoring for ECS systems'; readonly description = 'Real-time performance monitoring for ECS systems';
readonly icon = '📊'; readonly icon = '📊';
private messageHub: MessageHub | null = null; private messageHub: MessageHub | null = null;
private profilerService: ProfilerService | null = null; private profilerService: ProfilerService | null = null;
async install(_core: Core, services: ServiceContainer): Promise<void> { async install(_core: Core, services: ServiceContainer): Promise<void> {
this.messageHub = services.resolve(MessageHub); this.messageHub = services.resolve(MessageHub);
// 注册设置 // 注册设置
const settingsRegistry = services.resolve(SettingsRegistry); const settingsRegistry = services.resolve(SettingsRegistry);
settingsRegistry.registerCategory({ settingsRegistry.registerCategory({
id: 'profiler', id: 'profiler',
title: '性能分析器', title: '性能分析器',
description: '配置性能分析器的行为和显示选项', description: '配置性能分析器的行为和显示选项',
sections: [ sections: [
{ {
id: 'connection', id: 'connection',
title: '连接设置', title: '连接设置',
description: '配置WebSocket服务器连接参数', description: '配置WebSocket服务器连接参数',
settings: [ settings: [
{ {
key: 'profiler.port', key: 'profiler.port',
label: '监听端口', label: '监听端口',
type: 'number', type: 'number',
defaultValue: 8080, defaultValue: 8080,
description: '性能分析器WebSocket服务器监听的端口号', description: '性能分析器WebSocket服务器监听的端口号',
placeholder: '8080', placeholder: '8080',
min: 1024, min: 1024,
max: 65535, max: 65535,
validator: { validator: {
validate: (value: number) => value >= 1024 && value <= 65535, validate: (value: number) => value >= 1024 && value <= 65535,
errorMessage: '端口号必须在1024到65535之间' errorMessage: '端口号必须在1024到65535之间'
} }
}, },
{ {
key: 'profiler.autoStart', key: 'profiler.autoStart',
label: '自动启动服务器', label: '自动启动服务器',
type: 'boolean', type: 'boolean',
defaultValue: true, defaultValue: true,
description: '编辑器启动时自动启动性能分析器服务器' description: '编辑器启动时自动启动性能分析器服务器'
} }
] ]
}, },
{ {
id: 'display', id: 'display',
title: '显示设置', title: '显示设置',
description: '配置性能数据的显示选项', description: '配置性能数据的显示选项',
settings: [ settings: [
{ {
key: 'profiler.refreshInterval', key: 'profiler.refreshInterval',
label: '刷新间隔 (毫秒)', label: '刷新间隔 (毫秒)',
type: 'range', type: 'range',
defaultValue: 100, defaultValue: 100,
description: '性能数据刷新的时间间隔', description: '性能数据刷新的时间间隔',
min: 50, min: 50,
max: 1000, max: 1000,
step: 50 step: 50
}, },
{ {
key: 'profiler.maxDataPoints', key: 'profiler.maxDataPoints',
label: '最大数据点数', label: '最大数据点数',
type: 'number', type: 'number',
defaultValue: 100, defaultValue: 100,
description: '图表中保留的最大历史数据点数量', description: '图表中保留的最大历史数据点数量',
min: 10, min: 10,
max: 500 max: 500
} }
] ]
} }
] ]
}); });
// 创建并启动 ProfilerService // 创建并启动 ProfilerService
this.profilerService = new ProfilerService(); this.profilerService = new ProfilerService();
// 将服务实例存储到全局,供组件访问 // 将服务实例存储到全局,供组件访问
(window as any).__PROFILER_SERVICE__ = this.profilerService; (window as any).__PROFILER_SERVICE__ = this.profilerService;
console.log('[ProfilerPlugin] Installed and ProfilerService started'); console.log('[ProfilerPlugin] Installed and ProfilerService started');
}
async uninstall(): Promise<void> {
// 清理 ProfilerService
if (this.profilerService) {
this.profilerService.destroy();
this.profilerService = null;
} }
delete (window as any).__PROFILER_SERVICE__; async uninstall(): Promise<void> {
// 清理 ProfilerService
console.log('[ProfilerPlugin] Uninstalled and ProfilerService stopped'); if (this.profilerService) {
} this.profilerService.destroy();
this.profilerService = null;
async onEditorReady(): Promise<void> {
console.log('[ProfilerPlugin] Editor is ready');
}
registerMenuItems(): MenuItem[] {
const items = [
{
id: 'window.profiler',
label: 'Profiler',
parentId: 'window',
order: 100,
onClick: () => {
console.log('[ProfilerPlugin] Menu item clicked!');
this.messageHub?.publish('ui:openWindow', { windowId: 'profiler' });
} }
}
];
console.log('[ProfilerPlugin] Registering menu items:', items);
return items;
}
registerPanels(): PanelDescriptor[] { delete (window as any).__PROFILER_SERVICE__;
return [
{ console.log('[ProfilerPlugin] Uninstalled and ProfilerService stopped');
id: 'profiler-monitor', }
title: 'Performance Monitor',
position: 'center' as any, async onEditorReady(): Promise<void> {
closable: true, console.log('[ProfilerPlugin] Editor is ready');
component: ProfilerDockPanel, }
order: 200
} registerMenuItems(): MenuItem[] {
]; const items = [
} {
id: 'window.profiler',
label: 'Profiler',
parentId: 'window',
order: 100,
onClick: () => {
console.log('[ProfilerPlugin] Menu item clicked!');
this.messageHub?.publish('ui:openWindow', { windowId: 'profiler' });
}
}
];
console.log('[ProfilerPlugin] Registering menu items:', items);
return items;
}
registerPanels(): PanelDescriptor[] {
return [
{
id: 'profiler-monitor',
title: 'Performance Monitor',
position: 'center' as any,
closable: true,
component: ProfilerDockPanel,
order: 200
}
];
}
} }

View File

@@ -8,123 +8,123 @@ import type { MenuItem, ToolbarItem, PanelDescriptor, ISerializer } from '@eseng
* 提供场景层级视图和实体检视功能 * 提供场景层级视图和实体检视功能
*/ */
export class SceneInspectorPlugin implements IEditorPlugin { export class SceneInspectorPlugin implements IEditorPlugin {
readonly name = '@esengine/scene-inspector'; readonly name = '@esengine/scene-inspector';
readonly version = '1.0.0'; readonly version = '1.0.0';
readonly displayName = 'Scene Inspector'; readonly displayName = 'Scene Inspector';
readonly category = EditorPluginCategory.Inspector; readonly category = EditorPluginCategory.Inspector;
readonly description = 'Scene hierarchy and entity inspector'; readonly description = 'Scene hierarchy and entity inspector';
readonly icon = '🔍'; readonly icon = '🔍';
async install(_core: Core, _services: ServiceContainer): Promise<void> { async install(_core: Core, _services: ServiceContainer): Promise<void> {
console.log('[SceneInspectorPlugin] Installed'); console.log('[SceneInspectorPlugin] Installed');
} }
async uninstall(): Promise<void> { async uninstall(): Promise<void> {
console.log('[SceneInspectorPlugin] Uninstalled'); console.log('[SceneInspectorPlugin] Uninstalled');
} }
registerMenuItems(): MenuItem[] { registerMenuItems(): MenuItem[] {
return [ return [
{ {
id: 'view-scene-inspector', id: 'view-scene-inspector',
label: 'Scene Inspector', label: 'Scene Inspector',
parentId: 'view', parentId: 'view',
onClick: () => { onClick: () => {
console.log('Toggle Scene Inspector'); console.log('Toggle Scene Inspector');
}, },
shortcut: 'Ctrl+Shift+I', shortcut: 'Ctrl+Shift+I',
order: 100 order: 100
}, },
{ {
id: 'scene-create-entity', id: 'scene-create-entity',
label: 'Create Entity', label: 'Create Entity',
parentId: 'scene', parentId: 'scene',
onClick: () => { onClick: () => {
console.log('Create new entity'); console.log('Create new entity');
}, },
shortcut: 'Ctrl+N', shortcut: 'Ctrl+N',
order: 10 order: 10
} }
]; ];
} }
registerToolbar(): ToolbarItem[] { registerToolbar(): ToolbarItem[] {
return [ return [
{ {
id: 'toolbar-create-entity', id: 'toolbar-create-entity',
label: 'New Entity', label: 'New Entity',
groupId: 'entity-tools', groupId: 'entity-tools',
icon: '', icon: '',
onClick: () => { onClick: () => {
console.log('Create entity from toolbar'); console.log('Create entity from toolbar');
}, },
order: 10 order: 10
}, },
{ {
id: 'toolbar-delete-entity', id: 'toolbar-delete-entity',
label: 'Delete Entity', label: 'Delete Entity',
groupId: 'entity-tools', groupId: 'entity-tools',
icon: '🗑️', icon: '🗑️',
onClick: () => { onClick: () => {
console.log('Delete entity from toolbar'); console.log('Delete entity from toolbar');
}, },
order: 20 order: 20
} }
]; ];
} }
registerPanels(): PanelDescriptor[] { registerPanels(): PanelDescriptor[] {
return [ return [
{ {
id: 'panel-scene-hierarchy', id: 'panel-scene-hierarchy',
title: 'Scene Hierarchy', title: 'Scene Hierarchy',
position: PanelPosition.Left, position: PanelPosition.Left,
defaultSize: 250, defaultSize: 250,
resizable: true, resizable: true,
closable: false, closable: false,
icon: '📋', icon: '📋',
order: 10 order: 10
}, },
{ {
id: 'panel-entity-inspector', id: 'panel-entity-inspector',
title: 'Entity Inspector', title: 'Entity Inspector',
position: PanelPosition.Right, position: PanelPosition.Right,
defaultSize: 300, defaultSize: 300,
resizable: true, resizable: true,
closable: false, closable: false,
icon: '🔎', icon: '🔎',
order: 10 order: 10
} }
]; ];
} }
getSerializers(): ISerializer[] { getSerializers(): ISerializer[] {
return [ return [
{ {
serialize: (data: any) => { serialize: (data: any) => {
const json = JSON.stringify(data); const json = JSON.stringify(data);
const encoder = new TextEncoder(); const encoder = new TextEncoder();
return encoder.encode(json); return encoder.encode(json);
}, },
deserialize: (data: Uint8Array) => { deserialize: (data: Uint8Array) => {
const decoder = new TextDecoder(); const decoder = new TextDecoder();
const json = decoder.decode(data); const json = decoder.decode(data);
return JSON.parse(json); return JSON.parse(json);
}, },
getSupportedType: () => 'scene' getSupportedType: () => 'scene'
} }
]; ];
} }
async onEditorReady(): Promise<void> { async onEditorReady(): Promise<void> {
console.log('[SceneInspectorPlugin] Editor is ready'); console.log('[SceneInspectorPlugin] Editor is ready');
} }
async onProjectOpen(projectPath: string): Promise<void> { async onProjectOpen(projectPath: string): Promise<void> {
console.log(`[SceneInspectorPlugin] Project opened: ${projectPath}`); console.log(`[SceneInspectorPlugin] Project opened: ${projectPath}`);
} }
async onProjectClose(): Promise<void> { async onProjectClose(): Promise<void> {
console.log('[SceneInspectorPlugin] Project closed'); console.log('[SceneInspectorPlugin] Project closed');
} }
} }

View File

@@ -34,8 +34,8 @@ export class PluginLoader {
} }
const entries = await TauriAPI.listDirectory(pluginsPath); const entries = await TauriAPI.listDirectory(pluginsPath);
const pluginDirs = entries.filter(entry => entry.is_dir && !entry.name.startsWith('.')); const pluginDirs = entries.filter((entry) => entry.is_dir && !entry.name.startsWith('.'));
console.log('[PluginLoader] Found plugin directories:', pluginDirs.map(d => d.name)); console.log('[PluginLoader] Found plugin directories:', pluginDirs.map((d) => d.name));
for (const entry of pluginDirs) { for (const entry of pluginDirs) {
const pluginPath = `${pluginsPath}/${entry.name}`; const pluginPath = `${pluginsPath}/${entry.name}`;
@@ -101,14 +101,14 @@ export class PluginLoader {
console.log(`[PluginLoader] Loading plugin from: ${moduleUrl}`); console.log(`[PluginLoader] Loading plugin from: ${moduleUrl}`);
const module = await import(/* @vite-ignore */ moduleUrl); const module = await import(/* @vite-ignore */ moduleUrl);
console.log(`[PluginLoader] Module loaded successfully`); console.log('[PluginLoader] Module loaded successfully');
let pluginInstance: IEditorPlugin | null = null; let pluginInstance: IEditorPlugin | null = null;
try { try {
pluginInstance = this.findPluginInstance(module); pluginInstance = this.findPluginInstance(module);
} catch (findError) { } catch (findError) {
console.error(`[PluginLoader] Error finding plugin instance:`, findError); console.error('[PluginLoader] Error finding plugin instance:', findError);
console.error(`[PluginLoader] Module object:`, module); console.error('[PluginLoader] Module object:', module);
return; return;
} }
@@ -139,14 +139,14 @@ export class PluginLoader {
messageHub.publish('locale:changed', { locale: localeService.getCurrentLocale() }); messageHub.publish('locale:changed', { locale: localeService.getCurrentLocale() });
console.log(`[PluginLoader] Published locale:changed event for plugin ${packageJson.name}`); console.log(`[PluginLoader] Published locale:changed event for plugin ${packageJson.name}`);
} catch (error) { } catch (error) {
console.warn(`[PluginLoader] Failed to publish locale:changed event:`, error); console.warn('[PluginLoader] Failed to publish locale:changed event:', error);
} }
console.log(`[PluginLoader] Successfully loaded plugin: ${packageJson.name}`); console.log(`[PluginLoader] Successfully loaded plugin: ${packageJson.name}`);
} catch (error) { } catch (error) {
console.error(`[PluginLoader] Failed to load plugin from ${pluginPath}:`, error); console.error(`[PluginLoader] Failed to load plugin from ${pluginPath}:`, error);
if (error instanceof Error) { if (error instanceof Error) {
console.error(`[PluginLoader] Error stack:`, error.stack); console.error('[PluginLoader] Error stack:', error.stack);
} }
} }
} }

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