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:
YHH
2025-12-01 22:28:51 +08:00
committed by GitHub
parent 189714c727
commit b42a7b4e43
468 changed files with 18301 additions and 9075 deletions

View 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);
}
}));

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

View 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
})
}));