* refactor(editor): 提取行为树编辑器为独立包并重构编辑器架构 * feat(editor): 添加插件市场功能 * feat(editor): 重构插件市场以支持版本管理和ZIP打包 * feat(editor): 重构插件发布流程并修复React渲染警告 * fix(plugin): 修复插件发布和市场的路径不一致问题 * feat: 重构插件发布流程并添加插件删除功能 * fix(editor): 完善插件删除功能并修复多个关键问题 * fix(auth): 修复自动登录与手动登录的竞态条件问题 * feat(editor): 重构插件管理流程 * feat(editor): 支持 ZIP 文件直接发布插件 - 新增 PluginSourceParser 解析插件源 - 重构发布流程支持文件夹和 ZIP 两种方式 - 优化发布向导 UI * feat(editor): 插件市场支持多版本安装 - 插件解压到项目 plugins 目录 - 新增 Tauri 后端安装/卸载命令 - 支持选择任意版本安装 - 修复打包逻辑,保留完整 dist 目录结构 * feat(editor): 个人中心支持多版本管理 - 合并同一插件的不同版本 - 添加版本历史展开/折叠功能 - 禁止有待审核 PR 时更新插件 * fix(editor): 修复 InspectorRegistry 服务注册 - InspectorRegistry 实现 IService 接口 - 注册到 Core.services 供插件使用 * feat(behavior-tree-editor): 完善插件注册和文件操作 - 添加文件创建模板和操作处理器 - 实现右键菜单创建行为树功能 - 修复文件读取权限问题(使用 Tauri 命令) - 添加 BehaviorTreeEditorPanel 组件 - 修复 rollup 配置支持动态导入 * feat(plugin): 完善插件构建和发布流程 * fix(behavior-tree-editor): 完整恢复编辑器并修复 Toast 集成 * fix(behavior-tree-editor): 修复节点选中、连线跟随和文件加载问题并优化性能 * fix(behavior-tree-editor): 修复端口连接失败问题并优化连线样式 * refactor(behavior-tree-editor): 移除调试面板功能简化代码结构 * refactor(behavior-tree-editor): 清理冗余代码合并重复逻辑 * feat(behavior-tree-editor): 完善编辑器核心功能增强扩展性 * fix(lint): 修复ESLint错误确保CI通过 * refactor(behavior-tree-editor): 优化编辑器工具栏和编译器功能 * refactor(behavior-tree-editor): 清理技术债务,优化代码质量 * fix(editor-app): 修复字符串替换安全问题
251 lines
9.2 KiB
TypeScript
251 lines
9.2 KiB
TypeScript
import { RefObject, useEffect, useRef } from 'react';
|
|
import { BehaviorTreeNode, ROOT_NODE_ID } from '../stores';
|
|
|
|
interface QuickCreateMenuState {
|
|
visible: boolean;
|
|
position: { x: number; y: number };
|
|
searchText: string;
|
|
selectedIndex: number;
|
|
mode: 'create' | 'replace';
|
|
replaceNodeId: string | null;
|
|
}
|
|
|
|
interface UseCanvasMouseEventsParams {
|
|
canvasRef: RefObject<HTMLDivElement>;
|
|
canvasOffset: { x: number; y: number };
|
|
canvasScale: number;
|
|
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;
|
|
nodes: BehaviorTreeNode[];
|
|
selectedNodeIds: string[];
|
|
quickCreateMenu: QuickCreateMenuState;
|
|
setConnectingToPos: (pos: { x: number; y: number } | null) => void;
|
|
setIsBoxSelecting: (isSelecting: boolean) => void;
|
|
setBoxSelectStart: (pos: { x: number; y: number } | null) => void;
|
|
setBoxSelectEnd: (pos: { x: number; y: number } | null) => void;
|
|
setSelectedNodeIds: (ids: string[]) => void;
|
|
setSelectedConnection: (connection: { from: string; to: string } | null) => void;
|
|
setQuickCreateMenu: (menu: QuickCreateMenuState) => void;
|
|
clearConnecting: () => void;
|
|
clearBoxSelect: () => void;
|
|
showToast?: (message: string, type: 'success' | 'error' | 'warning' | 'info', duration?: number) => void;
|
|
}
|
|
|
|
export function useCanvasMouseEvents(params: UseCanvasMouseEventsParams) {
|
|
const {
|
|
canvasRef,
|
|
canvasOffset,
|
|
canvasScale,
|
|
connectingFrom,
|
|
connectingFromProperty,
|
|
connectingToPos,
|
|
isBoxSelecting,
|
|
boxSelectStart,
|
|
boxSelectEnd,
|
|
nodes,
|
|
selectedNodeIds,
|
|
quickCreateMenu,
|
|
setConnectingToPos,
|
|
setIsBoxSelecting,
|
|
setBoxSelectStart,
|
|
setBoxSelectEnd,
|
|
setSelectedNodeIds,
|
|
setSelectedConnection,
|
|
setQuickCreateMenu,
|
|
clearConnecting,
|
|
clearBoxSelect,
|
|
showToast
|
|
} = params;
|
|
|
|
const isBoxSelectingRef = useRef(isBoxSelecting);
|
|
const boxSelectStartRef = useRef(boxSelectStart);
|
|
const canvasOffsetRef = useRef(canvasOffset);
|
|
const canvasScaleRef = useRef(canvasScale);
|
|
const nodesRef = useRef(nodes);
|
|
const selectedNodeIdsRef = useRef(selectedNodeIds);
|
|
|
|
useEffect(() => {
|
|
isBoxSelectingRef.current = isBoxSelecting;
|
|
boxSelectStartRef.current = boxSelectStart;
|
|
canvasOffsetRef.current = canvasOffset;
|
|
canvasScaleRef.current = canvasScale;
|
|
nodesRef.current = nodes;
|
|
selectedNodeIdsRef.current = selectedNodeIds;
|
|
}, [isBoxSelecting, boxSelectStart, canvasOffset, canvasScale, nodes, selectedNodeIds]);
|
|
|
|
useEffect(() => {
|
|
if (!isBoxSelecting) return;
|
|
|
|
const handleGlobalMouseMove = (e: MouseEvent) => {
|
|
if (!isBoxSelectingRef.current || !boxSelectStartRef.current) return;
|
|
|
|
const rect = canvasRef.current?.getBoundingClientRect();
|
|
if (!rect) return;
|
|
|
|
const canvasX = (e.clientX - rect.left - canvasOffsetRef.current.x) / canvasScaleRef.current;
|
|
const canvasY = (e.clientY - rect.top - canvasOffsetRef.current.y) / canvasScaleRef.current;
|
|
setBoxSelectEnd({ x: canvasX, y: canvasY });
|
|
};
|
|
|
|
const handleGlobalMouseUp = (e: MouseEvent) => {
|
|
if (!isBoxSelectingRef.current || !boxSelectStartRef.current || !boxSelectEnd) return;
|
|
|
|
const rect = canvasRef.current?.getBoundingClientRect();
|
|
if (!rect) {
|
|
clearBoxSelect();
|
|
return;
|
|
}
|
|
|
|
const minX = Math.min(boxSelectStartRef.current.x, boxSelectEnd.x);
|
|
const maxX = Math.max(boxSelectStartRef.current.x, boxSelectEnd.x);
|
|
const minY = Math.min(boxSelectStartRef.current.y, boxSelectEnd.y);
|
|
const maxY = Math.max(boxSelectStartRef.current.y, boxSelectEnd.y);
|
|
|
|
const selectedInBox = nodesRef.current
|
|
.filter((node: BehaviorTreeNode) => {
|
|
if (node.id === ROOT_NODE_ID) return false;
|
|
|
|
const nodeElement = canvasRef.current?.querySelector(`[data-node-id="${node.id}"]`);
|
|
if (!nodeElement) {
|
|
return node.position.x >= minX && node.position.x <= maxX &&
|
|
node.position.y >= minY && node.position.y <= maxY;
|
|
}
|
|
|
|
const nodeRect = nodeElement.getBoundingClientRect();
|
|
const canvasRect = canvasRef.current!.getBoundingClientRect();
|
|
|
|
const nodeLeft = (nodeRect.left - canvasRect.left - canvasOffsetRef.current.x) / canvasScaleRef.current;
|
|
const nodeRight = (nodeRect.right - canvasRect.left - canvasOffsetRef.current.x) / canvasScaleRef.current;
|
|
const nodeTop = (nodeRect.top - canvasRect.top - canvasOffsetRef.current.y) / canvasScaleRef.current;
|
|
const nodeBottom = (nodeRect.bottom - canvasRect.top - canvasOffsetRef.current.y) / canvasScaleRef.current;
|
|
|
|
return nodeRight > minX && nodeLeft < maxX && nodeBottom > minY && nodeTop < maxY;
|
|
})
|
|
.map((node: BehaviorTreeNode) => node.id);
|
|
|
|
if (e.ctrlKey || e.metaKey) {
|
|
const newSet = new Set([...selectedNodeIdsRef.current, ...selectedInBox]);
|
|
setSelectedNodeIds(Array.from(newSet));
|
|
} else {
|
|
setSelectedNodeIds(selectedInBox);
|
|
}
|
|
|
|
clearBoxSelect();
|
|
};
|
|
|
|
document.addEventListener('mousemove', handleGlobalMouseMove);
|
|
document.addEventListener('mouseup', handleGlobalMouseUp);
|
|
|
|
return () => {
|
|
document.removeEventListener('mousemove', handleGlobalMouseMove);
|
|
document.removeEventListener('mouseup', handleGlobalMouseUp);
|
|
};
|
|
}, [isBoxSelecting, boxSelectStart, boxSelectEnd, canvasRef, setBoxSelectEnd, setSelectedNodeIds, clearBoxSelect]);
|
|
|
|
const handleCanvasMouseMove = (e: React.MouseEvent) => {
|
|
if (connectingFrom && canvasRef.current && !quickCreateMenu.visible) {
|
|
const rect = canvasRef.current.getBoundingClientRect();
|
|
const canvasX = (e.clientX - rect.left - canvasOffset.x) / canvasScale;
|
|
const canvasY = (e.clientY - rect.top - canvasOffset.y) / canvasScale;
|
|
setConnectingToPos({
|
|
x: canvasX,
|
|
y: canvasY
|
|
});
|
|
}
|
|
};
|
|
|
|
const handleCanvasMouseUp = (e: React.MouseEvent) => {
|
|
if (quickCreateMenu.visible) {
|
|
return;
|
|
}
|
|
|
|
const target = e.target as HTMLElement;
|
|
const isPort = target.closest('[data-port="true"]');
|
|
if (isPort) {
|
|
return;
|
|
}
|
|
|
|
if (connectingFrom && connectingToPos) {
|
|
// 如果是属性连接,不允许创建新节点
|
|
if (connectingFromProperty) {
|
|
showToast?.(
|
|
'属性连接必须连接到现有节点的属性端口',
|
|
'warning'
|
|
);
|
|
clearConnecting();
|
|
setConnectingToPos(null);
|
|
return;
|
|
}
|
|
|
|
const sourceNode = nodes.find(n => n.id === connectingFrom);
|
|
if (sourceNode && !sourceNode.canAddChild()) {
|
|
const maxChildren = sourceNode.template.maxChildren ?? Infinity;
|
|
showToast?.(
|
|
`节点"${sourceNode.template.displayName}"已达到最大子节点数 ${maxChildren}`,
|
|
'warning'
|
|
);
|
|
clearConnecting();
|
|
setConnectingToPos(null);
|
|
return;
|
|
}
|
|
|
|
setQuickCreateMenu({
|
|
visible: true,
|
|
position: {
|
|
x: e.clientX,
|
|
y: e.clientY
|
|
},
|
|
searchText: '',
|
|
selectedIndex: 0,
|
|
mode: 'create',
|
|
replaceNodeId: null
|
|
});
|
|
setConnectingToPos(null);
|
|
return;
|
|
}
|
|
|
|
clearConnecting();
|
|
};
|
|
|
|
const handleCanvasMouseDown = (e: React.MouseEvent) => {
|
|
if (quickCreateMenu.visible) {
|
|
setQuickCreateMenu({
|
|
visible: false,
|
|
position: { x: 0, y: 0 },
|
|
searchText: '',
|
|
selectedIndex: 0,
|
|
mode: 'create',
|
|
replaceNodeId: null
|
|
});
|
|
return;
|
|
}
|
|
|
|
if (e.button === 0 && !e.altKey) {
|
|
const rect = canvasRef.current?.getBoundingClientRect();
|
|
if (!rect) return;
|
|
|
|
const canvasX = (e.clientX - rect.left - canvasOffset.x) / canvasScale;
|
|
const canvasY = (e.clientY - rect.top - canvasOffset.y) / canvasScale;
|
|
|
|
setIsBoxSelecting(true);
|
|
setBoxSelectStart({ x: canvasX, y: canvasY });
|
|
setBoxSelectEnd({ x: canvasX, y: canvasY });
|
|
|
|
if (!e.ctrlKey && !e.metaKey) {
|
|
setSelectedNodeIds([]);
|
|
setSelectedConnection(null);
|
|
}
|
|
}
|
|
};
|
|
|
|
return {
|
|
handleCanvasMouseMove,
|
|
handleCanvasMouseUp,
|
|
handleCanvasMouseDown
|
|
};
|
|
}
|