Compare commits
8 Commits
issue-204-
...
issue-213-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ddc7a7750e | ||
|
|
50a01d9dd3 | ||
|
|
793aad0a5e | ||
|
|
9c1bf8dbed | ||
|
|
620f3eecc7 | ||
|
|
4355538d8d | ||
|
|
3ad5dc9ca3 | ||
|
|
57c7e7be3f |
@@ -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"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
@@ -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'
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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}`);
|
||||||
|
|||||||
@@ -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
|
||||||
}));
|
}));
|
||||||
|
|||||||
@@ -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); // 只加载一次
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -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'
|
|
||||||
]
|
|
||||||
}
|
|
||||||
];
|
|
||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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.'
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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++;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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)) {
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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 {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -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();
|
||||||
|
|
||||||
|
|
||||||
@@ -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,
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,2 @@
|
|||||||
export { QuerySystem } from '../QuerySystem';
|
export { QuerySystem } from '../QuerySystem';
|
||||||
export { ECSFluentAPI, createECSAPI } from '../FluentAPI';
|
export { ECSFluentAPI, createECSAPI } from '../FluentAPI';
|
||||||
|
|
||||||
@@ -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);
|
||||||
|
|||||||
@@ -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}`;
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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键
|
||||||
|
|||||||
@@ -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
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
166
packages/core/src/ECS/Systems/EntityCache.ts
Normal file
166
packages/core/src/ECS/Systems/EntityCache.ts
Normal 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
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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;
|
||||||
// 不能假设a,b的segments都存在或长度相同.
|
// 不能假设a,b的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');
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { BitMask64Data } from "./BigIntCompatibility";
|
import { BitMask64Data } from './BigIntCompatibility';
|
||||||
|
|
||||||
// FlatHashMapFast.ts
|
// FlatHashMapFast.ts
|
||||||
|
|
||||||
|
|||||||
@@ -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字节
|
||||||
|
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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字节
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -142,8 +142,8 @@ export interface IEventListenerConfig {
|
|||||||
priority?: number;
|
priority?: number;
|
||||||
/** 是否异步执行 */
|
/** 是否异步执行 */
|
||||||
async?: boolean;
|
async?: boolean;
|
||||||
/** 执行上下文 */
|
/** 事件处理函数的 this 绑定对象 */
|
||||||
context?: unknown;
|
thisArg?: object;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
@@ -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 };
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -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();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -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';
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -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';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 日志管理器
|
* 日志管理器
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import type { LogLevel } from "./Constants";
|
import type { LogLevel } from './Constants';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 日志接口
|
* 日志接口
|
||||||
|
|||||||
@@ -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('');
|
||||||
}
|
}
|
||||||
|
|
||||||
// 总体统计
|
// 总体统计
|
||||||
|
|||||||
@@ -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>);
|
||||||
|
|
||||||
|
|||||||
@@ -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);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -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
@@ -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 {
|
||||||
|
|||||||
@@ -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>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
@@ -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);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
@@ -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
@@ -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>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -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>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 (<8ms)</span>
|
<span>Good (<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 (>16ms)</span>
|
<span>Critical (>16ms)</span>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
);
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
@@ -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('');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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('');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
|
];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
Reference in New Issue
Block a user