| import { useCallback } from 'react'; |
| import { useCanvasStore, useKeyboardStore } from '@/lib/store'; |
| import { useHistorySnapshot } from '@/lib/hooks/use-history-snapshot'; |
| import type { PPTElement } from '@/lib/types/slides'; |
| import type { AlignmentLineProps } from '@/lib/types/edit'; |
| import { getRectRotatedRange, uniqAlignLines, type AlignLine } from '@/lib/utils/element'; |
| import { useCanvasOperations } from '@/lib/hooks/use-canvas-operations'; |
|
|
| |
| |
| |
| |
| |
| |
| |
| export function useDragElement( |
| elementListRef: React.RefObject<PPTElement[]>, |
| setElementList: React.Dispatch<React.SetStateAction<PPTElement[]>>, |
| setAlignmentLines: React.Dispatch<React.SetStateAction<AlignmentLineProps[]>>, |
| ) { |
| const activeElementIdList = useCanvasStore.use.activeElementIdList(); |
| const activeGroupElementId = useCanvasStore.use.activeGroupElementId(); |
| const canvasScale = useCanvasStore.use.canvasScale(); |
| const shiftKeyState = useKeyboardStore((state) => state.shiftKeyState); |
|
|
| const viewportRatio = useCanvasStore.use.viewportRatio(); |
| const viewportSize = useCanvasStore.use.viewportSize(); |
| const updateSlide = useCanvasOperations().updateSlide; |
|
|
| const { addHistorySnapshot } = useHistorySnapshot(); |
|
|
| const dragElement = useCallback( |
| (e: React.MouseEvent | React.TouchEvent, element: PPTElement) => { |
| const native = e.nativeEvent; |
| const isTouchEvent = native instanceof TouchEvent; |
| if (isTouchEvent && !native.changedTouches?.length) return; |
|
|
| if (!activeElementIdList.includes(element.id)) return; |
|
|
| let isMouseDown = true; |
| const edgeWidth = viewportSize; |
| const edgeHeight = viewportSize * viewportRatio; |
|
|
| const sorptionRange = 5; |
|
|
| |
| const originElementList: PPTElement[] = JSON.parse(JSON.stringify(elementListRef.current)); |
| const originActiveElementList = originElementList.filter((el) => |
| activeElementIdList.includes(el.id), |
| ); |
|
|
| const elOriginLeft = element.left; |
| const elOriginTop = element.top; |
| const elOriginWidth = element.width; |
| const elOriginHeight = 'height' in element && element.height ? element.height : 0; |
| const elOriginRotate = 'rotate' in element && element.rotate ? element.rotate : 0; |
|
|
| const startPageX = isTouchEvent ? native.changedTouches[0].pageX : native.pageX; |
| const startPageY = isTouchEvent ? native.changedTouches[0].pageY : native.pageY; |
|
|
| let isMisoperation: boolean | null = null; |
|
|
| const isActiveGroupElement = element.id === activeGroupElementId; |
|
|
| |
| |
| |
| let horizontalLines: AlignLine[] = []; |
| let verticalLines: AlignLine[] = []; |
|
|
| for (const el of elementListRef.current) { |
| if (el.type === 'line') continue; |
| if (isActiveGroupElement && el.id === element.id) continue; |
| if (!isActiveGroupElement && activeElementIdList.includes(el.id)) continue; |
|
|
| let left, top, width, height; |
| if ('rotate' in el && el.rotate) { |
| const { xRange, yRange } = getRectRotatedRange({ |
| left: el.left, |
| top: el.top, |
| width: el.width, |
| height: el.height, |
| rotate: el.rotate, |
| }); |
| left = xRange[0]; |
| top = yRange[0]; |
| width = xRange[1] - xRange[0]; |
| height = yRange[1] - yRange[0]; |
| } else { |
| left = el.left; |
| top = el.top; |
| width = el.width; |
| height = el.height; |
| } |
|
|
| const right = left + width; |
| const bottom = top + height; |
| const centerX = top + height / 2; |
| const centerY = left + width / 2; |
|
|
| const topLine: AlignLine = { value: top, range: [left, right] }; |
| const bottomLine: AlignLine = { value: bottom, range: [left, right] }; |
| const horizontalCenterLine: AlignLine = { |
| value: centerX, |
| range: [left, right], |
| }; |
| const leftLine: AlignLine = { value: left, range: [top, bottom] }; |
| const rightLine: AlignLine = { value: right, range: [top, bottom] }; |
| const verticalCenterLine: AlignLine = { |
| value: centerY, |
| range: [top, bottom], |
| }; |
|
|
| horizontalLines.push(topLine, bottomLine, horizontalCenterLine); |
| verticalLines.push(leftLine, rightLine, verticalCenterLine); |
| } |
|
|
| |
| const edgeTopLine: AlignLine = { value: 0, range: [0, edgeWidth] }; |
| const edgeBottomLine: AlignLine = { |
| value: edgeHeight, |
| range: [0, edgeWidth], |
| }; |
| const edgeHorizontalCenterLine: AlignLine = { |
| value: edgeHeight / 2, |
| range: [0, edgeWidth], |
| }; |
| const edgeLeftLine: AlignLine = { value: 0, range: [0, edgeHeight] }; |
| const edgeRightLine: AlignLine = { |
| value: edgeWidth, |
| range: [0, edgeHeight], |
| }; |
| const edgeVerticalCenterLine: AlignLine = { |
| value: edgeWidth / 2, |
| range: [0, edgeHeight], |
| }; |
|
|
| horizontalLines.push(edgeTopLine, edgeBottomLine, edgeHorizontalCenterLine); |
| verticalLines.push(edgeLeftLine, edgeRightLine, edgeVerticalCenterLine); |
|
|
| |
| horizontalLines = uniqAlignLines(horizontalLines); |
| verticalLines = uniqAlignLines(verticalLines); |
|
|
| const handleMouseMove = (e: MouseEvent | TouchEvent) => { |
| const currentPageX = e instanceof MouseEvent ? e.pageX : e.changedTouches[0].pageX; |
| const currentPageY = e instanceof MouseEvent ? e.pageY : e.changedTouches[0].pageY; |
|
|
| |
| |
| if (isMisoperation !== false) { |
| isMisoperation = |
| Math.abs(startPageX - currentPageX) < sorptionRange && |
| Math.abs(startPageY - currentPageY) < sorptionRange; |
| } |
| if (!isMouseDown || isMisoperation) return; |
|
|
| let moveX = (currentPageX - startPageX) / canvasScale; |
| let moveY = (currentPageY - startPageY) / canvasScale; |
|
|
| |
| if (shiftKeyState) { |
| if (Math.abs(moveX) > Math.abs(moveY)) moveY = 0; |
| if (Math.abs(moveX) < Math.abs(moveY)) moveX = 0; |
| } |
|
|
| |
| let targetLeft = elOriginLeft + moveX; |
| let targetTop = elOriginTop + moveY; |
|
|
| |
| |
| let targetMinX: number, targetMaxX: number, targetMinY: number, targetMaxY: number; |
|
|
| if (activeElementIdList.length === 1 || isActiveGroupElement) { |
| if (elOriginRotate) { |
| const { xRange, yRange } = getRectRotatedRange({ |
| left: targetLeft, |
| top: targetTop, |
| width: elOriginWidth, |
| height: elOriginHeight, |
| rotate: elOriginRotate, |
| }); |
| targetMinX = xRange[0]; |
| targetMaxX = xRange[1]; |
| targetMinY = yRange[0]; |
| targetMaxY = yRange[1]; |
| } else if (element.type === 'line') { |
| targetMinX = targetLeft; |
| targetMaxX = targetLeft + Math.max(element.start[0], element.end[0]); |
| targetMinY = targetTop; |
| targetMaxY = targetTop + Math.max(element.start[1], element.end[1]); |
| } else { |
| targetMinX = targetLeft; |
| targetMaxX = targetLeft + elOriginWidth; |
| targetMinY = targetTop; |
| targetMaxY = targetTop + elOriginHeight; |
| } |
| } else { |
| const leftValues = []; |
| const topValues = []; |
| const rightValues = []; |
| const bottomValues = []; |
|
|
| for (let i = 0; i < originActiveElementList.length; i++) { |
| const element = originActiveElementList[i]; |
| const left = element.left + moveX; |
| const top = element.top + moveY; |
| const width = element.width; |
| const height = 'height' in element && element.height ? element.height : 0; |
| const rotate = 'rotate' in element && element.rotate ? element.rotate : 0; |
|
|
| if ('rotate' in element && element.rotate) { |
| const { xRange, yRange } = getRectRotatedRange({ |
| left, |
| top, |
| width, |
| height, |
| rotate, |
| }); |
| leftValues.push(xRange[0]); |
| topValues.push(yRange[0]); |
| rightValues.push(xRange[1]); |
| bottomValues.push(yRange[1]); |
| } else if (element.type === 'line') { |
| leftValues.push(left); |
| topValues.push(top); |
| rightValues.push(left + Math.max(element.start[0], element.end[0])); |
| bottomValues.push(top + Math.max(element.start[1], element.end[1])); |
| } else { |
| leftValues.push(left); |
| topValues.push(top); |
| rightValues.push(left + width); |
| bottomValues.push(top + height); |
| } |
| } |
|
|
| targetMinX = Math.min(...leftValues); |
| targetMaxX = Math.max(...rightValues); |
| targetMinY = Math.min(...topValues); |
| targetMaxY = Math.max(...bottomValues); |
| } |
|
|
| const targetCenterX = targetMinX + (targetMaxX - targetMinX) / 2; |
| const targetCenterY = targetMinY + (targetMaxY - targetMinY) / 2; |
|
|
| |
| |
| const _alignmentLines: AlignmentLineProps[] = []; |
| let isVerticalAdsorbed = false; |
| let isHorizontalAdsorbed = false; |
|
|
| for (let i = 0; i < horizontalLines.length; i++) { |
| const { value, range } = horizontalLines[i]; |
| const min = Math.min(...range, targetMinX, targetMaxX); |
| const max = Math.max(...range, targetMinX, targetMaxX); |
|
|
| if (Math.abs(targetMinY - value) < sorptionRange && !isHorizontalAdsorbed) { |
| targetTop = targetTop - (targetMinY - value); |
| isHorizontalAdsorbed = true; |
| _alignmentLines.push({ |
| type: 'horizontal', |
| axis: { x: min - 50, y: value }, |
| length: max - min + 100, |
| }); |
| } |
| if (Math.abs(targetMaxY - value) < sorptionRange && !isHorizontalAdsorbed) { |
| targetTop = targetTop - (targetMaxY - value); |
| isHorizontalAdsorbed = true; |
| _alignmentLines.push({ |
| type: 'horizontal', |
| axis: { x: min - 50, y: value }, |
| length: max - min + 100, |
| }); |
| } |
| if (Math.abs(targetCenterY - value) < sorptionRange && !isHorizontalAdsorbed) { |
| targetTop = targetTop - (targetCenterY - value); |
| isHorizontalAdsorbed = true; |
| _alignmentLines.push({ |
| type: 'horizontal', |
| axis: { x: min - 50, y: value }, |
| length: max - min + 100, |
| }); |
| } |
| } |
|
|
| for (let i = 0; i < verticalLines.length; i++) { |
| const { value, range } = verticalLines[i]; |
| const min = Math.min(...range, targetMinY, targetMaxY); |
| const max = Math.max(...range, targetMinY, targetMaxY); |
|
|
| if (Math.abs(targetMinX - value) < sorptionRange && !isVerticalAdsorbed) { |
| targetLeft = targetLeft - (targetMinX - value); |
| isVerticalAdsorbed = true; |
| _alignmentLines.push({ |
| type: 'vertical', |
| axis: { x: value, y: min - 50 }, |
| length: max - min + 100, |
| }); |
| } |
| if (Math.abs(targetMaxX - value) < sorptionRange && !isVerticalAdsorbed) { |
| targetLeft = targetLeft - (targetMaxX - value); |
| isVerticalAdsorbed = true; |
| _alignmentLines.push({ |
| type: 'vertical', |
| axis: { x: value, y: min - 50 }, |
| length: max - min + 100, |
| }); |
| } |
| if (Math.abs(targetCenterX - value) < sorptionRange && !isVerticalAdsorbed) { |
| targetLeft = targetLeft - (targetCenterX - value); |
| isVerticalAdsorbed = true; |
| _alignmentLines.push({ |
| type: 'vertical', |
| axis: { x: value, y: min - 50 }, |
| length: max - min + 100, |
| }); |
| } |
| } |
|
|
| setAlignmentLines(_alignmentLines); |
| let newElements: PPTElement[]; |
|
|
| |
| if (activeElementIdList.length === 1 || isActiveGroupElement) { |
| newElements = elementListRef.current.map((el) => { |
| if (el.id === element.id) { |
| return { ...el, left: targetLeft, top: targetTop }; |
| } |
| return el; |
| }); |
| } |
| |
| |
| else { |
| const handleElement = elementListRef.current.find((el) => el.id === element.id); |
| if (!handleElement) return; |
|
|
| newElements = elementListRef.current.map((el) => { |
| if (activeElementIdList.includes(el.id)) { |
| if (el.id === element.id) { |
| return { ...el, left: targetLeft, top: targetTop }; |
| } |
| return { |
| ...el, |
| left: el.left + (targetLeft - handleElement.left), |
| top: el.top + (targetTop - handleElement.top), |
| }; |
| } |
| return el; |
| }); |
| } |
|
|
| |
| elementListRef.current = newElements; |
| setElementList(newElements); |
| }; |
|
|
| const handleMouseUp = (e: MouseEvent | TouchEvent) => { |
| isMouseDown = false; |
|
|
| document.ontouchmove = null; |
| document.ontouchend = null; |
| document.onmousemove = null; |
| document.onmouseup = null; |
|
|
| setAlignmentLines([]); |
|
|
| const currentPageX = e instanceof MouseEvent ? e.pageX : e.changedTouches[0].pageX; |
| const currentPageY = e instanceof MouseEvent ? e.pageY : e.changedTouches[0].pageY; |
|
|
| if (startPageX === currentPageX && startPageY === currentPageY) return; |
|
|
| updateSlide({ elements: elementListRef.current }); |
| addHistorySnapshot(); |
| }; |
|
|
| if (isTouchEvent) { |
| document.ontouchmove = handleMouseMove; |
| document.ontouchend = handleMouseUp; |
| } else { |
| document.onmousemove = handleMouseMove; |
| document.onmouseup = handleMouseUp; |
| } |
| }, |
| [ |
| activeElementIdList, |
| activeGroupElementId, |
| shiftKeyState, |
| canvasScale, |
| elementListRef, |
| setElementList, |
| setAlignmentLines, |
| updateSlide, |
| addHistorySnapshot, |
| viewportRatio, |
| viewportSize, |
| ], |
| ); |
|
|
| return { |
| dragElement, |
| }; |
| } |
|
|