refactor(editor): 重构编辑器架构并增强行为树执行可视化
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import { RefObject } from 'react';
|
||||
import { RefObject, useEffect, useRef } from 'react';
|
||||
import { BehaviorTreeNode, ROOT_NODE_ID } from '../../stores/behaviorTreeStore';
|
||||
|
||||
interface QuickCreateMenuState {
|
||||
@@ -31,6 +31,7 @@ interface UseCanvasMouseEventsParams {
|
||||
setQuickCreateMenu: (menu: QuickCreateMenuState) => void;
|
||||
clearConnecting: () => void;
|
||||
clearBoxSelect: () => void;
|
||||
showToast?: (message: string, type: 'success' | 'error' | 'warning' | 'info', duration?: number) => void;
|
||||
}
|
||||
|
||||
export function useCanvasMouseEvents(params: UseCanvasMouseEventsParams) {
|
||||
@@ -54,9 +55,87 @@ export function useCanvasMouseEvents(params: UseCanvasMouseEventsParams) {
|
||||
setSelectedConnection,
|
||||
setQuickCreateMenu,
|
||||
clearConnecting,
|
||||
clearBoxSelect
|
||||
clearBoxSelect,
|
||||
showToast
|
||||
} = params;
|
||||
|
||||
const isBoxSelectingRef = useRef(isBoxSelecting);
|
||||
const boxSelectStartRef = useRef(boxSelectStart);
|
||||
|
||||
useEffect(() => {
|
||||
isBoxSelectingRef.current = isBoxSelecting;
|
||||
boxSelectStartRef.current = boxSelectStart;
|
||||
}, [isBoxSelecting, boxSelectStart]);
|
||||
|
||||
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 - canvasOffset.x) / canvasScale;
|
||||
const canvasY = (e.clientY - rect.top - canvasOffset.y) / canvasScale;
|
||||
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 = nodes
|
||||
.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 - canvasOffset.x) / canvasScale;
|
||||
const nodeRight = (nodeRect.right - canvasRect.left - canvasOffset.x) / canvasScale;
|
||||
const nodeTop = (nodeRect.top - canvasRect.top - canvasOffset.y) / canvasScale;
|
||||
const nodeBottom = (nodeRect.bottom - canvasRect.top - canvasOffset.y) / canvasScale;
|
||||
|
||||
return nodeRight > minX && nodeLeft < maxX && nodeBottom > minY && nodeTop < maxY;
|
||||
})
|
||||
.map((node: BehaviorTreeNode) => node.id);
|
||||
|
||||
if (e.ctrlKey || e.metaKey) {
|
||||
const newSet = new Set([...selectedNodeIds, ...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, nodes, selectedNodeIds, canvasRef, canvasOffset, canvasScale, setBoxSelectEnd, setSelectedNodeIds, clearBoxSelect]);
|
||||
|
||||
const handleCanvasMouseMove = (e: React.MouseEvent) => {
|
||||
if (connectingFrom && canvasRef.current && !quickCreateMenu.visible) {
|
||||
const rect = canvasRef.current.getBoundingClientRect();
|
||||
@@ -67,15 +146,6 @@ export function useCanvasMouseEvents(params: UseCanvasMouseEventsParams) {
|
||||
y: canvasY
|
||||
});
|
||||
}
|
||||
|
||||
if (isBoxSelecting && boxSelectStart) {
|
||||
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;
|
||||
setBoxSelectEnd({ x: canvasX, y: canvasY });
|
||||
}
|
||||
};
|
||||
|
||||
const handleCanvasMouseUp = (e: React.MouseEvent) => {
|
||||
@@ -84,6 +154,18 @@ export function useCanvasMouseEvents(params: UseCanvasMouseEventsParams) {
|
||||
}
|
||||
|
||||
if (connectingFrom && connectingToPos) {
|
||||
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: {
|
||||
@@ -100,47 +182,21 @@ export function useCanvasMouseEvents(params: UseCanvasMouseEventsParams) {
|
||||
}
|
||||
|
||||
clearConnecting();
|
||||
|
||||
if (isBoxSelecting && boxSelectStart && boxSelectEnd) {
|
||||
const minX = Math.min(boxSelectStart.x, boxSelectEnd.x);
|
||||
const maxX = Math.max(boxSelectStart.x, boxSelectEnd.x);
|
||||
const minY = Math.min(boxSelectStart.y, boxSelectEnd.y);
|
||||
const maxY = Math.max(boxSelectStart.y, boxSelectEnd.y);
|
||||
|
||||
const selectedInBox = nodes
|
||||
.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 rect = nodeElement.getBoundingClientRect();
|
||||
const canvasRect = canvasRef.current!.getBoundingClientRect();
|
||||
|
||||
const nodeLeft = (rect.left - canvasRect.left - canvasOffset.x) / canvasScale;
|
||||
const nodeRight = (rect.right - canvasRect.left - canvasOffset.x) / canvasScale;
|
||||
const nodeTop = (rect.top - canvasRect.top - canvasOffset.y) / canvasScale;
|
||||
const nodeBottom = (rect.bottom - canvasRect.top - canvasOffset.y) / canvasScale;
|
||||
|
||||
return nodeRight > minX && nodeLeft < maxX && nodeBottom > minY && nodeTop < maxY;
|
||||
})
|
||||
.map((node: BehaviorTreeNode) => node.id);
|
||||
|
||||
if (e.ctrlKey || e.metaKey) {
|
||||
const newSet = new Set([...selectedNodeIds, ...selectedInBox]);
|
||||
setSelectedNodeIds(Array.from(newSet));
|
||||
} else {
|
||||
setSelectedNodeIds(selectedInBox);
|
||||
}
|
||||
}
|
||||
|
||||
clearBoxSelect();
|
||||
};
|
||||
|
||||
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;
|
||||
|
||||
Reference in New Issue
Block a user