2025-11-25 22:23:19 +08:00
|
|
|
import { type RefObject, useEffect, useRef, React } from '@esengine/editor-runtime';
|
2025-11-18 14:46:51 +08:00
|
|
|
import { BehaviorTreeNode, ROOT_NODE_ID } from '../stores';
|
2025-11-03 21:22:16 +08:00
|
|
|
|
|
|
|
|
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;
|
2025-11-18 14:46:51 +08:00
|
|
|
connectingFromProperty: string | null;
|
2025-11-03 21:22:16 +08:00
|
|
|
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;
|
2025-11-04 18:29:28 +08:00
|
|
|
showToast?: (message: string, type: 'success' | 'error' | 'warning' | 'info', duration?: number) => void;
|
2025-11-03 21:22:16 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function useCanvasMouseEvents(params: UseCanvasMouseEventsParams) {
|
|
|
|
|
const {
|
|
|
|
|
canvasRef,
|
|
|
|
|
canvasOffset,
|
|
|
|
|
canvasScale,
|
|
|
|
|
connectingFrom,
|
2025-11-18 14:46:51 +08:00
|
|
|
connectingFromProperty,
|
2025-11-03 21:22:16 +08:00
|
|
|
connectingToPos,
|
|
|
|
|
isBoxSelecting,
|
|
|
|
|
boxSelectStart,
|
|
|
|
|
boxSelectEnd,
|
|
|
|
|
nodes,
|
|
|
|
|
selectedNodeIds,
|
|
|
|
|
quickCreateMenu,
|
|
|
|
|
setConnectingToPos,
|
|
|
|
|
setIsBoxSelecting,
|
|
|
|
|
setBoxSelectStart,
|
|
|
|
|
setBoxSelectEnd,
|
|
|
|
|
setSelectedNodeIds,
|
|
|
|
|
setSelectedConnection,
|
|
|
|
|
setQuickCreateMenu,
|
|
|
|
|
clearConnecting,
|
2025-11-04 18:29:28 +08:00
|
|
|
clearBoxSelect,
|
|
|
|
|
showToast
|
2025-11-03 21:22:16 +08:00
|
|
|
} = params;
|
|
|
|
|
|
2025-11-04 18:29:28 +08:00
|
|
|
const isBoxSelectingRef = useRef(isBoxSelecting);
|
|
|
|
|
const boxSelectStartRef = useRef(boxSelectStart);
|
2025-11-18 14:46:51 +08:00
|
|
|
const canvasOffsetRef = useRef(canvasOffset);
|
|
|
|
|
const canvasScaleRef = useRef(canvasScale);
|
|
|
|
|
const nodesRef = useRef(nodes);
|
|
|
|
|
const selectedNodeIdsRef = useRef(selectedNodeIds);
|
2025-11-04 18:29:28 +08:00
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
isBoxSelectingRef.current = isBoxSelecting;
|
|
|
|
|
boxSelectStartRef.current = boxSelectStart;
|
2025-11-18 14:46:51 +08:00
|
|
|
canvasOffsetRef.current = canvasOffset;
|
|
|
|
|
canvasScaleRef.current = canvasScale;
|
|
|
|
|
nodesRef.current = nodes;
|
|
|
|
|
selectedNodeIdsRef.current = selectedNodeIds;
|
|
|
|
|
}, [isBoxSelecting, boxSelectStart, canvasOffset, canvasScale, nodes, selectedNodeIds]);
|
2025-11-04 18:29:28 +08:00
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
if (!isBoxSelecting) return;
|
|
|
|
|
|
|
|
|
|
const handleGlobalMouseMove = (e: MouseEvent) => {
|
|
|
|
|
if (!isBoxSelectingRef.current || !boxSelectStartRef.current) return;
|
2025-11-03 21:22:16 +08:00
|
|
|
|
|
|
|
|
const rect = canvasRef.current?.getBoundingClientRect();
|
|
|
|
|
if (!rect) return;
|
|
|
|
|
|
2025-11-18 14:46:51 +08:00
|
|
|
const canvasX = (e.clientX - rect.left - canvasOffsetRef.current.x) / canvasScaleRef.current;
|
|
|
|
|
const canvasY = (e.clientY - rect.top - canvasOffsetRef.current.y) / canvasScaleRef.current;
|
2025-11-03 21:22:16 +08:00
|
|
|
setBoxSelectEnd({ x: canvasX, y: canvasY });
|
2025-11-04 18:29:28 +08:00
|
|
|
};
|
2025-11-03 21:22:16 +08:00
|
|
|
|
2025-11-04 18:29:28 +08:00
|
|
|
const handleGlobalMouseUp = (e: MouseEvent) => {
|
|
|
|
|
if (!isBoxSelectingRef.current || !boxSelectStartRef.current || !boxSelectEnd) return;
|
2025-11-03 21:22:16 +08:00
|
|
|
|
2025-11-04 18:29:28 +08:00
|
|
|
const rect = canvasRef.current?.getBoundingClientRect();
|
|
|
|
|
if (!rect) {
|
|
|
|
|
clearBoxSelect();
|
|
|
|
|
return;
|
|
|
|
|
}
|
2025-11-03 21:22:16 +08:00
|
|
|
|
2025-11-04 18:29:28 +08:00
|
|
|
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);
|
2025-11-03 21:22:16 +08:00
|
|
|
|
2025-11-18 14:46:51 +08:00
|
|
|
const selectedInBox = nodesRef.current
|
2025-11-03 21:22:16 +08:00
|
|
|
.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;
|
|
|
|
|
}
|
|
|
|
|
|
2025-11-04 18:29:28 +08:00
|
|
|
const nodeRect = nodeElement.getBoundingClientRect();
|
2025-11-03 21:22:16 +08:00
|
|
|
const canvasRect = canvasRef.current!.getBoundingClientRect();
|
|
|
|
|
|
2025-11-18 14:46:51 +08:00
|
|
|
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;
|
2025-11-03 21:22:16 +08:00
|
|
|
|
|
|
|
|
return nodeRight > minX && nodeLeft < maxX && nodeBottom > minY && nodeTop < maxY;
|
|
|
|
|
})
|
|
|
|
|
.map((node: BehaviorTreeNode) => node.id);
|
|
|
|
|
|
|
|
|
|
if (e.ctrlKey || e.metaKey) {
|
2025-11-18 14:46:51 +08:00
|
|
|
const newSet = new Set([...selectedNodeIdsRef.current, ...selectedInBox]);
|
2025-11-03 21:22:16 +08:00
|
|
|
setSelectedNodeIds(Array.from(newSet));
|
|
|
|
|
} else {
|
|
|
|
|
setSelectedNodeIds(selectedInBox);
|
|
|
|
|
}
|
2025-11-04 18:29:28 +08:00
|
|
|
|
|
|
|
|
clearBoxSelect();
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
document.addEventListener('mousemove', handleGlobalMouseMove);
|
|
|
|
|
document.addEventListener('mouseup', handleGlobalMouseUp);
|
|
|
|
|
|
|
|
|
|
return () => {
|
|
|
|
|
document.removeEventListener('mousemove', handleGlobalMouseMove);
|
|
|
|
|
document.removeEventListener('mouseup', handleGlobalMouseUp);
|
|
|
|
|
};
|
2025-11-18 14:46:51 +08:00
|
|
|
}, [isBoxSelecting, boxSelectStart, boxSelectEnd, canvasRef, setBoxSelectEnd, setSelectedNodeIds, clearBoxSelect]);
|
2025-11-04 18:29:28 +08:00
|
|
|
|
|
|
|
|
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;
|
|
|
|
|
}
|
|
|
|
|
|
2025-11-18 14:46:51 +08:00
|
|
|
const target = e.target as HTMLElement;
|
|
|
|
|
const isPort = target.closest('[data-port="true"]');
|
|
|
|
|
if (isPort) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2025-11-04 18:29:28 +08:00
|
|
|
if (connectingFrom && connectingToPos) {
|
2025-11-18 14:46:51 +08:00
|
|
|
// 如果是属性连接,不允许创建新节点
|
|
|
|
|
if (connectingFromProperty) {
|
|
|
|
|
showToast?.(
|
|
|
|
|
'属性连接必须连接到现有节点的属性端口',
|
|
|
|
|
'warning'
|
|
|
|
|
);
|
|
|
|
|
clearConnecting();
|
|
|
|
|
setConnectingToPos(null);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2025-11-23 14:49:37 +08:00
|
|
|
const sourceNode = nodes.find((n) => n.id === connectingFrom);
|
2025-11-04 18:29:28 +08:00
|
|
|
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;
|
2025-11-03 21:22:16 +08:00
|
|
|
}
|
|
|
|
|
|
2025-11-04 18:29:28 +08:00
|
|
|
clearConnecting();
|
2025-11-03 21:22:16 +08:00
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const handleCanvasMouseDown = (e: React.MouseEvent) => {
|
2025-11-04 18:29:28 +08:00
|
|
|
if (quickCreateMenu.visible) {
|
|
|
|
|
setQuickCreateMenu({
|
|
|
|
|
visible: false,
|
|
|
|
|
position: { x: 0, y: 0 },
|
|
|
|
|
searchText: '',
|
|
|
|
|
selectedIndex: 0,
|
|
|
|
|
mode: 'create',
|
|
|
|
|
replaceNodeId: null
|
|
|
|
|
});
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2025-11-03 21:22:16 +08:00
|
|
|
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
|
|
|
|
|
};
|
|
|
|
|
}
|