import { useState, useCallback, type RefObject } from 'react'; import { useKeyboardStore } from '@/lib/store/keyboard'; import { useCanvasStore } from '@/lib/store'; import type { PPTElement } from '@/lib/types/slides'; import { getElementRange } from '@/lib/utils/element'; export function useMouseSelection( elementListRef: React.RefObject, viewportRef: RefObject, ) { const [mouseSelectionVisible, setMouseSelectionVisible] = useState(false); const [mouseSelectionQuadrant, setMouseSelectionQuadrant] = useState(1); const [mouseSelection, setMouseSelection] = useState({ top: 0, left: 0, width: 0, height: 0, }); const canvasScale = useCanvasStore.use.canvasScale(); const hiddenElementIdList = useCanvasStore.use.hiddenElementIdList(); const setActiveElementIdList = useCanvasStore.use.setActiveElementIdList(); const ctrlOrShiftKeyActive = useKeyboardStore((state) => state.ctrlOrShiftKeyActive()); // Update mouse selection range const updateMouseSelection = useCallback( (e: React.MouseEvent) => { if (!viewportRef.current) return; let isMouseDown = true; const viewportRect = viewportRef.current.getBoundingClientRect(); const minSelectionRange = 5; const startPageX = e.pageX; const startPageY = e.pageY; const left = (startPageX - viewportRect.x) / canvasScale; const top = (startPageY - viewportRect.y) / canvasScale; // Initialize selection start position and defaults setMouseSelection({ top: top, left: left, width: 0, height: 0, }); setMouseSelectionVisible(false); setMouseSelectionQuadrant(4); const handleMouseMove = (e: MouseEvent) => { if (!isMouseDown) return; const currentPageX = e.pageX; const currentPageY = e.pageY; const offsetWidth = (currentPageX - startPageX) / canvasScale; const offsetHeight = (currentPageY - startPageY) / canvasScale; const width = Math.abs(offsetWidth); const height = Math.abs(offsetHeight); if (width < minSelectionRange || height < minSelectionRange) return; // Determine mouse selection (movement) direction // Classified by quadrant position, e.g. bottom-right is quadrant 4 let quadrant = 0; if (offsetWidth > 0 && offsetHeight > 0) quadrant = 4; else if (offsetWidth < 0 && offsetHeight < 0) quadrant = 2; else if (offsetWidth > 0 && offsetHeight < 0) quadrant = 1; else if (offsetWidth < 0 && offsetHeight > 0) quadrant = 3; // Update selection range setMouseSelection((prev) => ({ ...prev, width: width, height: height, })); setMouseSelectionVisible(true); setMouseSelectionQuadrant(quadrant); }; const handleMouseUp = () => { document.onmousemove = null; document.onmouseup = null; isMouseDown = false; // Check which canvas elements are within the mouse selection range and set them as selected let inRangeElementList: PPTElement[] = []; for (const element of elementListRef.current) { const mouseSelectionLeft = mouseSelection.left; const mouseSelectionTop = mouseSelection.top; const mouseSelectionWidth = mouseSelection.width; const mouseSelectionHeight = mouseSelection.height; const { minX, maxX, minY, maxY } = getElementRange(element); // Inclusion check differs for each quadrant direction let isInclude = false; if (ctrlOrShiftKeyActive) { if (mouseSelectionQuadrant === 4) { isInclude = maxX > mouseSelectionLeft && minX < mouseSelectionLeft + mouseSelectionWidth && maxY > mouseSelectionTop && minY < mouseSelectionTop + mouseSelectionHeight; } else if (mouseSelectionQuadrant === 2) { isInclude = maxX > mouseSelectionLeft - mouseSelectionWidth && minX < mouseSelectionLeft - mouseSelectionWidth + mouseSelectionWidth && maxY > mouseSelectionTop - mouseSelectionHeight && minY < mouseSelectionTop - mouseSelectionHeight + mouseSelectionHeight; } else if (mouseSelectionQuadrant === 1) { isInclude = maxX > mouseSelectionLeft && minX < mouseSelectionLeft + mouseSelectionWidth && maxY > mouseSelectionTop - mouseSelectionHeight && minY < mouseSelectionTop - mouseSelectionHeight + mouseSelectionHeight; } else if (mouseSelectionQuadrant === 3) { isInclude = maxX > mouseSelectionLeft - mouseSelectionWidth && minX < mouseSelectionLeft - mouseSelectionWidth + mouseSelectionWidth && maxY > mouseSelectionTop && minY < mouseSelectionTop + mouseSelectionHeight; } } else { if (mouseSelectionQuadrant === 4) { isInclude = minX > mouseSelectionLeft && maxX < mouseSelectionLeft + mouseSelectionWidth && minY > mouseSelectionTop && maxY < mouseSelectionTop + mouseSelectionHeight; } else if (mouseSelectionQuadrant === 2) { isInclude = minX > mouseSelectionLeft - mouseSelectionWidth && maxX < mouseSelectionLeft - mouseSelectionWidth + mouseSelectionWidth && minY > mouseSelectionTop - mouseSelectionHeight && maxY < mouseSelectionTop - mouseSelectionHeight + mouseSelectionHeight; } else if (mouseSelectionQuadrant === 1) { isInclude = minX > mouseSelectionLeft && maxX < mouseSelectionLeft + mouseSelectionWidth && minY > mouseSelectionTop - mouseSelectionHeight && maxY < mouseSelectionTop - mouseSelectionHeight + mouseSelectionHeight; } else if (mouseSelectionQuadrant === 3) { isInclude = minX > mouseSelectionLeft - mouseSelectionWidth && maxX < mouseSelectionLeft - mouseSelectionWidth + mouseSelectionWidth && minY > mouseSelectionTop && maxY < mouseSelectionTop + mouseSelectionHeight; } } // Locked or hidden elements should not be selected even if within range if (isInclude && !element.lock && !hiddenElementIdList.includes(element.id)) inRangeElementList.push(element); } // If grouped elements are in range, all members of the group must be in range to be selected inRangeElementList = inRangeElementList.filter((inRangeElement) => { if (inRangeElement.groupId) { const inRangeElementIdList = inRangeElementList.map( (inRangeElement) => inRangeElement.id, ); const groupElementList = elementListRef.current.filter( (element) => element.groupId === inRangeElement.groupId, ); return groupElementList.every((groupElement) => inRangeElementIdList.includes(groupElement.id), ); } return true; }); const inRangeElementIdList = inRangeElementList.map((inRangeElement) => inRangeElement.id); setActiveElementIdList(inRangeElementIdList); setMouseSelectionVisible(false); }; document.onmousemove = handleMouseMove; document.onmouseup = handleMouseUp; }, // eslint-disable-next-line react-hooks/exhaustive-deps -- Intentionally excludes mouseSelection state to avoid infinite re-creation [ viewportRef, canvasScale, ctrlOrShiftKeyActive, hiddenElementIdList, elementListRef, setActiveElementIdList, ], ); return { mouseSelection, mouseSelectionVisible, mouseSelectionQuadrant, updateMouseSelection, }; }