Feature/editor optimization (#251)
* refactor: 编辑器/运行时架构拆分与构建系统升级 * feat(core): 层级系统重构与UI变换矩阵修复 * refactor: 移除 ecs-components 聚合包并修复跨包组件查找问题 * fix(physics): 修复跨包组件类引用问题 * feat: 统一运行时架构与浏览器运行支持 * feat(asset): 实现浏览器运行时资产加载系统 * fix: 修复文档、CodeQL安全问题和CI类型检查错误 * fix: 修复文档、CodeQL安全问题和CI类型检查错误 * fix: 修复文档、CodeQL安全问题、CI类型检查和测试错误 * test: 补齐核心模块测试用例,修复CI构建配置 * fix: 修复测试用例中的类型错误和断言问题 * fix: 修复 turbo build:npm 任务的依赖顺序问题 * fix: 修复 CI 构建错误并优化构建性能
This commit is contained in:
350
packages/behavior-tree-editor/src/stores/ExecutionStatsStore.ts
Normal file
350
packages/behavior-tree-editor/src/stores/ExecutionStatsStore.ts
Normal file
@@ -0,0 +1,350 @@
|
||||
import { createStore } from '@esengine/editor-runtime';
|
||||
|
||||
const create = createStore;
|
||||
|
||||
/**
|
||||
* 节点执行统计信息
|
||||
*/
|
||||
export interface NodeExecutionStats {
|
||||
/** 节点ID */
|
||||
nodeId: string;
|
||||
|
||||
/** 执行总次数 */
|
||||
totalExecutions: number;
|
||||
|
||||
/** 成功次数 */
|
||||
successCount: number;
|
||||
|
||||
/** 失败次数 */
|
||||
failureCount: number;
|
||||
|
||||
/** 运行中次数(记录开始运行的次数) */
|
||||
runningCount: number;
|
||||
|
||||
/** 总耗时(毫秒) */
|
||||
totalDuration: number;
|
||||
|
||||
/** 平均耗时(毫秒) */
|
||||
averageDuration: number;
|
||||
|
||||
/** 最小耗时(毫秒) */
|
||||
minDuration: number;
|
||||
|
||||
/** 最大耗时(毫秒) */
|
||||
maxDuration: number;
|
||||
|
||||
/** 最后执行时间戳 */
|
||||
lastExecutionTime: number;
|
||||
|
||||
/** 最后执行状态 */
|
||||
lastStatus: 'success' | 'failure' | 'running' | 'idle';
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行历史记录项
|
||||
*/
|
||||
export interface ExecutionHistoryEntry {
|
||||
/** 节点ID */
|
||||
nodeId: string;
|
||||
|
||||
/** 执行开始时间 */
|
||||
startTime: number;
|
||||
|
||||
/** 执行结束时间 */
|
||||
endTime?: number;
|
||||
|
||||
/** 执行状态 */
|
||||
status: 'success' | 'failure' | 'running';
|
||||
|
||||
/** 执行耗时(毫秒) */
|
||||
duration?: number;
|
||||
|
||||
/** 执行顺序号 */
|
||||
executionOrder: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行路径(一次完整的树执行)
|
||||
*/
|
||||
export interface ExecutionPath {
|
||||
/** 路径ID */
|
||||
id: string;
|
||||
|
||||
/** 开始时间 */
|
||||
startTime: number;
|
||||
|
||||
/** 结束时间 */
|
||||
endTime?: number;
|
||||
|
||||
/** 执行历史记录 */
|
||||
history: ExecutionHistoryEntry[];
|
||||
|
||||
/** 是否正在执行 */
|
||||
isActive: boolean;
|
||||
}
|
||||
|
||||
interface ExecutionStatsState {
|
||||
/** 节点统计信息 */
|
||||
nodeStats: Map<string, NodeExecutionStats>;
|
||||
|
||||
/** 执行路径列表 */
|
||||
executionPaths: ExecutionPath[];
|
||||
|
||||
/** 当前活动路径ID */
|
||||
currentPathId: string | null;
|
||||
|
||||
/** 是否启用统计 */
|
||||
isEnabled: boolean;
|
||||
|
||||
/** 是否启用历史记录 */
|
||||
enableHistory: boolean;
|
||||
|
||||
/** 历史记录最大条数 */
|
||||
maxHistorySize: number;
|
||||
|
||||
/**
|
||||
* 记录节点开始执行
|
||||
*/
|
||||
recordNodeStart: (nodeId: string, executionOrder: number) => void;
|
||||
|
||||
/**
|
||||
* 记录节点执行结束
|
||||
*/
|
||||
recordNodeEnd: (nodeId: string, status: 'success' | 'failure' | 'running') => void;
|
||||
|
||||
/**
|
||||
* 开始新的执行路径
|
||||
*/
|
||||
startNewPath: () => void;
|
||||
|
||||
/**
|
||||
* 结束当前执行路径
|
||||
*/
|
||||
endCurrentPath: () => void;
|
||||
|
||||
/**
|
||||
* 获取节点统计信息
|
||||
*/
|
||||
getNodeStats: (nodeId: string) => NodeExecutionStats | undefined;
|
||||
|
||||
/**
|
||||
* 获取当前执行路径
|
||||
*/
|
||||
getCurrentPath: () => ExecutionPath | undefined;
|
||||
|
||||
/**
|
||||
* 清空所有统计数据
|
||||
*/
|
||||
clearStats: () => void;
|
||||
|
||||
/**
|
||||
* 清空执行历史
|
||||
*/
|
||||
clearHistory: () => void;
|
||||
|
||||
/**
|
||||
* 设置是否启用统计
|
||||
*/
|
||||
setEnabled: (enabled: boolean) => void;
|
||||
|
||||
/**
|
||||
* 设置是否启用历史记录
|
||||
*/
|
||||
setEnableHistory: (enabled: boolean) => void;
|
||||
|
||||
/**
|
||||
* 导出统计数据为JSON
|
||||
*/
|
||||
exportStats: () => string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建默认的节点统计信息
|
||||
*/
|
||||
function createDefaultNodeStats(nodeId: string): NodeExecutionStats {
|
||||
return {
|
||||
nodeId,
|
||||
totalExecutions: 0,
|
||||
successCount: 0,
|
||||
failureCount: 0,
|
||||
runningCount: 0,
|
||||
totalDuration: 0,
|
||||
averageDuration: 0,
|
||||
minDuration: Infinity,
|
||||
maxDuration: 0,
|
||||
lastExecutionTime: 0,
|
||||
lastStatus: 'idle'
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行统计状态存储
|
||||
*/
|
||||
export const useExecutionStatsStore = create<ExecutionStatsState>((set, get) => ({
|
||||
nodeStats: new Map(),
|
||||
executionPaths: [],
|
||||
currentPathId: null,
|
||||
isEnabled: true,
|
||||
enableHistory: true,
|
||||
maxHistorySize: 100,
|
||||
|
||||
recordNodeStart: (nodeId: string, executionOrder: number) => {
|
||||
if (!get().isEnabled) return;
|
||||
|
||||
const now = Date.now();
|
||||
const state = get();
|
||||
|
||||
// 更新节点统计
|
||||
const stats = state.nodeStats.get(nodeId) || createDefaultNodeStats(nodeId);
|
||||
const updatedStats = { ...stats };
|
||||
|
||||
set((state) => {
|
||||
const newStats = new Map(state.nodeStats);
|
||||
newStats.set(nodeId, updatedStats);
|
||||
return { nodeStats: newStats };
|
||||
});
|
||||
|
||||
// 添加到执行历史
|
||||
if (state.enableHistory && state.currentPathId) {
|
||||
const paths = [...state.executionPaths];
|
||||
const currentPath = paths.find((p) => p.id === state.currentPathId);
|
||||
if (currentPath && currentPath.isActive) {
|
||||
currentPath.history.push({
|
||||
nodeId,
|
||||
startTime: now,
|
||||
status: 'running',
|
||||
executionOrder
|
||||
});
|
||||
set({ executionPaths: paths });
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
recordNodeEnd: (nodeId: string, status: 'success' | 'failure' | 'running') => {
|
||||
if (!get().isEnabled) return;
|
||||
|
||||
const now = Date.now();
|
||||
const state = get();
|
||||
|
||||
// 更新节点统计
|
||||
const stats = state.nodeStats.get(nodeId) || createDefaultNodeStats(nodeId);
|
||||
|
||||
// 计算耗时(从历史记录中查找对应的开始时间)
|
||||
let duration = 0;
|
||||
if (state.enableHistory && state.currentPathId) {
|
||||
const currentPath = state.executionPaths.find((p) => p.id === state.currentPathId);
|
||||
if (currentPath) {
|
||||
// 找到最近的该节点的记录
|
||||
for (let i = currentPath.history.length - 1; i >= 0; i--) {
|
||||
const entry = currentPath.history[i];
|
||||
if (entry.nodeId === nodeId && !entry.endTime) {
|
||||
duration = now - entry.startTime;
|
||||
entry.endTime = now;
|
||||
entry.status = status;
|
||||
entry.duration = duration;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const updatedStats: NodeExecutionStats = {
|
||||
...stats,
|
||||
totalExecutions: stats.totalExecutions + 1,
|
||||
successCount: status === 'success' ? stats.successCount + 1 : stats.successCount,
|
||||
failureCount: status === 'failure' ? stats.failureCount + 1 : stats.failureCount,
|
||||
runningCount: status === 'running' ? stats.runningCount + 1 : stats.runningCount,
|
||||
totalDuration: stats.totalDuration + duration,
|
||||
averageDuration: (stats.totalDuration + duration) / (stats.totalExecutions + 1),
|
||||
minDuration: Math.min(stats.minDuration, duration || Infinity),
|
||||
maxDuration: Math.max(stats.maxDuration, duration),
|
||||
lastExecutionTime: now,
|
||||
lastStatus: status
|
||||
};
|
||||
|
||||
set((state) => {
|
||||
const newStats = new Map(state.nodeStats);
|
||||
newStats.set(nodeId, updatedStats);
|
||||
return { nodeStats: newStats };
|
||||
});
|
||||
},
|
||||
|
||||
startNewPath: () => {
|
||||
if (!get().isEnabled) return;
|
||||
|
||||
const pathId = `path_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
|
||||
const newPath: ExecutionPath = {
|
||||
id: pathId,
|
||||
startTime: Date.now(),
|
||||
history: [],
|
||||
isActive: true
|
||||
};
|
||||
|
||||
set((state) => {
|
||||
// 结束之前的路径
|
||||
const paths = state.executionPaths.map((p) =>
|
||||
p.isActive ? { ...p, isActive: false, endTime: Date.now() } : p
|
||||
);
|
||||
|
||||
// 添加新路径
|
||||
paths.push(newPath);
|
||||
|
||||
// 限制历史记录数量
|
||||
const trimmedPaths = paths.slice(-state.maxHistorySize);
|
||||
|
||||
return {
|
||||
executionPaths: trimmedPaths,
|
||||
currentPathId: pathId
|
||||
};
|
||||
});
|
||||
},
|
||||
|
||||
endCurrentPath: () => {
|
||||
const state = get();
|
||||
if (!state.currentPathId) return;
|
||||
|
||||
set((state) => ({
|
||||
executionPaths: state.executionPaths.map((p) =>
|
||||
p.id === state.currentPathId
|
||||
? { ...p, isActive: false, endTime: Date.now() }
|
||||
: p
|
||||
)
|
||||
}));
|
||||
},
|
||||
|
||||
getNodeStats: (nodeId: string) => {
|
||||
return get().nodeStats.get(nodeId);
|
||||
},
|
||||
|
||||
getCurrentPath: () => {
|
||||
const state = get();
|
||||
if (!state.currentPathId) return undefined;
|
||||
return state.executionPaths.find((p) => p.id === state.currentPathId);
|
||||
},
|
||||
|
||||
clearStats: () => {
|
||||
set({ nodeStats: new Map() });
|
||||
},
|
||||
|
||||
clearHistory: () => {
|
||||
set({ executionPaths: [], currentPathId: null });
|
||||
},
|
||||
|
||||
setEnabled: (enabled: boolean) => {
|
||||
set({ isEnabled: enabled });
|
||||
},
|
||||
|
||||
setEnableHistory: (enabled: boolean) => {
|
||||
set({ enableHistory: enabled });
|
||||
},
|
||||
|
||||
exportStats: () => {
|
||||
const state = get();
|
||||
const data = {
|
||||
nodeStats: Array.from(state.nodeStats.entries()).map(([_id, stats]) => stats),
|
||||
executionPaths: state.executionPaths,
|
||||
exportTime: new Date().toISOString()
|
||||
};
|
||||
return JSON.stringify(data, null, 2);
|
||||
}
|
||||
}));
|
||||
21
packages/behavior-tree-editor/src/stores/index.ts
Normal file
21
packages/behavior-tree-editor/src/stores/index.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
/**
|
||||
* Store 导出
|
||||
*/
|
||||
|
||||
// 业务数据 Store(唯一数据源)
|
||||
export { useBehaviorTreeDataStore, TreeStateAdapter } from '../application/state/BehaviorTreeDataStore';
|
||||
export type { NodeExecutionStatus } from '../application/state/BehaviorTreeDataStore';
|
||||
|
||||
// UI Store
|
||||
export { useUIStore } from './useUIStore';
|
||||
|
||||
// 执行统计 Store
|
||||
export { useExecutionStatsStore } from './ExecutionStatsStore';
|
||||
export type { NodeExecutionStats, ExecutionHistoryEntry, ExecutionPath } from './ExecutionStatsStore';
|
||||
|
||||
// 常量
|
||||
export { ROOT_NODE_ID } from '../domain/constants/RootNode';
|
||||
|
||||
// 类型
|
||||
export type { Node as BehaviorTreeNode } from '../domain/models/Node';
|
||||
export type { Connection } from '../domain/models/Connection';
|
||||
211
packages/behavior-tree-editor/src/stores/useUIStore.ts
Normal file
211
packages/behavior-tree-editor/src/stores/useUIStore.ts
Normal file
@@ -0,0 +1,211 @@
|
||||
import { createStore } from '@esengine/editor-runtime';
|
||||
|
||||
const create = createStore;
|
||||
|
||||
/**
|
||||
* UI 状态 Store
|
||||
* 只包含 UI 交互状态,不包含业务数据
|
||||
*/
|
||||
interface UIState {
|
||||
/**
|
||||
* 选中的节点 ID 列表
|
||||
*/
|
||||
selectedNodeIds: string[];
|
||||
|
||||
/**
|
||||
* 选中的连接
|
||||
*/
|
||||
selectedConnection: { from: string; to: string } | null;
|
||||
|
||||
/**
|
||||
* 正在拖拽的节点 ID
|
||||
*/
|
||||
draggingNodeId: string | null;
|
||||
|
||||
/**
|
||||
* 拖拽起始位置映射
|
||||
*/
|
||||
dragStartPositions: Map<string, { x: number; y: number }>;
|
||||
|
||||
/**
|
||||
* 是否正在拖拽节点
|
||||
*/
|
||||
isDraggingNode: boolean;
|
||||
|
||||
/**
|
||||
* 拖拽偏移量(临时状态)
|
||||
*/
|
||||
dragDelta: { dx: number; dy: number };
|
||||
|
||||
/**
|
||||
* 画布偏移(临时,用于平移操作中)
|
||||
*/
|
||||
tempCanvasOffset: { x: number; y: number } | null;
|
||||
|
||||
/**
|
||||
* 是否正在平移画布
|
||||
*/
|
||||
isPanning: boolean;
|
||||
|
||||
/**
|
||||
* 平移起始位置
|
||||
*/
|
||||
panStart: { x: number; y: number };
|
||||
|
||||
/**
|
||||
* 正在连接的起始节点 ID
|
||||
*/
|
||||
connectingFrom: string | null;
|
||||
|
||||
/**
|
||||
* 正在连接的起始属性名
|
||||
*/
|
||||
connectingFromProperty: string | null;
|
||||
|
||||
/**
|
||||
* 连接线的临时终点位置
|
||||
*/
|
||||
connectingToPos: { x: number; y: number } | null;
|
||||
|
||||
/**
|
||||
* 是否正在框选
|
||||
*/
|
||||
isBoxSelecting: boolean;
|
||||
|
||||
/**
|
||||
* 框选起始位置
|
||||
*/
|
||||
boxSelectStart: { x: number; y: number } | null;
|
||||
|
||||
/**
|
||||
* 框选结束位置
|
||||
*/
|
||||
boxSelectEnd: { x: number; y: number } | null;
|
||||
|
||||
/**
|
||||
* 选择操作
|
||||
*/
|
||||
setSelectedNodeIds: (nodeIds: string[]) => void;
|
||||
toggleNodeSelection: (nodeId: string) => void;
|
||||
clearSelection: () => void;
|
||||
setSelectedConnection: (connection: { from: string; to: string } | null) => void;
|
||||
|
||||
/**
|
||||
* 拖拽操作
|
||||
*/
|
||||
startDragging: (nodeId: string, startPositions: Map<string, { x: number; y: number }>) => void;
|
||||
stopDragging: () => void;
|
||||
setIsDraggingNode: (isDragging: boolean) => void;
|
||||
setDragDelta: (delta: { dx: number; dy: number }) => void;
|
||||
|
||||
/**
|
||||
* 画布操作
|
||||
*/
|
||||
setTempCanvasOffset: (offset: { x: number; y: number } | null) => void;
|
||||
setIsPanning: (isPanning: boolean) => void;
|
||||
setPanStart: (panStart: { x: number; y: number }) => void;
|
||||
|
||||
/**
|
||||
* 连接操作
|
||||
*/
|
||||
setConnectingFrom: (nodeId: string | null) => void;
|
||||
setConnectingFromProperty: (propertyName: string | null) => void;
|
||||
setConnectingToPos: (pos: { x: number; y: number } | null) => void;
|
||||
clearConnecting: () => void;
|
||||
|
||||
/**
|
||||
* 框选操作
|
||||
*/
|
||||
setIsBoxSelecting: (isSelecting: boolean) => void;
|
||||
setBoxSelectStart: (pos: { x: number; y: number } | null) => void;
|
||||
setBoxSelectEnd: (pos: { x: number; y: number } | null) => void;
|
||||
clearBoxSelect: () => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* UI Store
|
||||
*/
|
||||
export const useUIStore = create<UIState>((set, get) => ({
|
||||
selectedNodeIds: [],
|
||||
selectedConnection: null,
|
||||
draggingNodeId: null,
|
||||
dragStartPositions: new Map(),
|
||||
isDraggingNode: false,
|
||||
dragDelta: { dx: 0, dy: 0 },
|
||||
tempCanvasOffset: null,
|
||||
isPanning: false,
|
||||
panStart: { x: 0, y: 0 },
|
||||
connectingFrom: null,
|
||||
connectingFromProperty: null,
|
||||
connectingToPos: null,
|
||||
isBoxSelecting: false,
|
||||
boxSelectStart: null,
|
||||
boxSelectEnd: null,
|
||||
|
||||
setSelectedNodeIds: (nodeIds: string[]) => set({ selectedNodeIds: nodeIds }),
|
||||
|
||||
toggleNodeSelection: (nodeId: string) => {
|
||||
const { selectedNodeIds } = get();
|
||||
if (selectedNodeIds.includes(nodeId)) {
|
||||
set({ selectedNodeIds: selectedNodeIds.filter((id) => id !== nodeId) });
|
||||
} else {
|
||||
set({ selectedNodeIds: [...selectedNodeIds, nodeId] });
|
||||
}
|
||||
},
|
||||
|
||||
clearSelection: () => set({ selectedNodeIds: [], selectedConnection: null }),
|
||||
|
||||
setSelectedConnection: (connection: { from: string; to: string } | null) =>
|
||||
set({ selectedConnection: connection }),
|
||||
|
||||
startDragging: (nodeId: string, startPositions: Map<string, { x: number; y: number }>) =>
|
||||
set({
|
||||
draggingNodeId: nodeId,
|
||||
dragStartPositions: startPositions,
|
||||
isDraggingNode: true
|
||||
}),
|
||||
|
||||
stopDragging: () =>
|
||||
set({
|
||||
draggingNodeId: null,
|
||||
dragStartPositions: new Map(),
|
||||
isDraggingNode: false,
|
||||
dragDelta: { dx: 0, dy: 0 }
|
||||
}),
|
||||
|
||||
setIsDraggingNode: (isDragging: boolean) => set({ isDraggingNode: isDragging }),
|
||||
|
||||
setDragDelta: (delta: { dx: number; dy: number }) => set({ dragDelta: delta }),
|
||||
|
||||
setTempCanvasOffset: (offset: { x: number; y: number } | null) => set({ tempCanvasOffset: offset }),
|
||||
|
||||
setIsPanning: (isPanning: boolean) => set({ isPanning }),
|
||||
|
||||
setPanStart: (panStart: { x: number; y: number }) => set({ panStart }),
|
||||
|
||||
setConnectingFrom: (nodeId: string | null) => set({ connectingFrom: nodeId }),
|
||||
|
||||
setConnectingFromProperty: (propertyName: string | null) => set({ connectingFromProperty: propertyName }),
|
||||
|
||||
setConnectingToPos: (pos: { x: number; y: number } | null) => set({ connectingToPos: pos }),
|
||||
|
||||
clearConnecting: () =>
|
||||
set({
|
||||
connectingFrom: null,
|
||||
connectingFromProperty: null,
|
||||
connectingToPos: null
|
||||
}),
|
||||
|
||||
setIsBoxSelecting: (isSelecting: boolean) => set({ isBoxSelecting: isSelecting }),
|
||||
|
||||
setBoxSelectStart: (pos: { x: number; y: number } | null) => set({ boxSelectStart: pos }),
|
||||
|
||||
setBoxSelectEnd: (pos: { x: number; y: number } | null) => set({ boxSelectEnd: pos }),
|
||||
|
||||
clearBoxSelect: () =>
|
||||
set({
|
||||
isBoxSelecting: false,
|
||||
boxSelectStart: null,
|
||||
boxSelectEnd: null
|
||||
})
|
||||
}));
|
||||
Reference in New Issue
Block a user