useDragLineElement.ts 9.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273
  1. import { useCallback } from 'react';
  2. import { useKeyboardStore } from '@/lib/store/keyboard';
  3. import { useCanvasStore } from '@/lib/store';
  4. import type { PPTElement, PPTLineElement } from '@/lib/types/slides';
  5. import { OperateLineHandlers } from '@/lib/types/edit';
  6. import { useHistorySnapshot } from '@/lib/hooks/use-history-snapshot';
  7. import { useCanvasOperations } from '@/lib/hooks/use-canvas-operations';
  8. interface AdsorptionPoint {
  9. x: number;
  10. y: number;
  11. }
  12. /**
  13. * Drag line element Hook
  14. *
  15. * @param elementListRef - Element list ref (used to read the latest value on mouseup)
  16. * @param setElementList - Element list setter (used to trigger re-render)
  17. */
  18. export function useDragLineElement(
  19. elementListRef: React.RefObject<PPTElement[]>,
  20. setElementList: React.Dispatch<React.SetStateAction<PPTElement[]>>,
  21. ) {
  22. const updateSlide = useCanvasOperations().updateSlide;
  23. const canvasScale = useCanvasStore.use.canvasScale();
  24. const ctrlOrShiftKeyActive = useKeyboardStore((state) => state.ctrlOrShiftKeyActive());
  25. const { addHistorySnapshot } = useHistorySnapshot();
  26. // Drag line endpoint
  27. const dragLineElement = useCallback(
  28. (e: React.MouseEvent, element: PPTLineElement, command: OperateLineHandlers) => {
  29. let isMouseDown = true;
  30. const sorptionRange = 8;
  31. const startPageX = e.pageX;
  32. const startPageY = e.pageY;
  33. const adsorptionPoints: AdsorptionPoint[] = [];
  34. // Get the 8 scale points of all non-rotated, non-line elements as adsorption positions
  35. for (let i = 0; i < elementListRef.current.length; i++) {
  36. const _element = elementListRef.current[i];
  37. if (_element.type === 'line' || _element.rotate) continue;
  38. const left = _element.left;
  39. const top = _element.top;
  40. const width = _element.width;
  41. const height = _element.height;
  42. const right = left + width;
  43. const bottom = top + height;
  44. const centerX = top + height / 2;
  45. const centerY = left + width / 2;
  46. const topPoint = { x: centerY, y: top };
  47. const bottomPoint = { x: centerY, y: bottom };
  48. const leftPoint = { x: left, y: centerX };
  49. const rightPoint = { x: right, y: centerX };
  50. const leftTopPoint = { x: left, y: top };
  51. const rightTopPoint = { x: right, y: top };
  52. const leftBottomPoint = { x: left, y: bottom };
  53. const rightBottomPoint = { x: right, y: bottom };
  54. adsorptionPoints.push(
  55. topPoint,
  56. bottomPoint,
  57. leftPoint,
  58. rightPoint,
  59. leftTopPoint,
  60. rightTopPoint,
  61. leftBottomPoint,
  62. rightBottomPoint,
  63. );
  64. }
  65. const handleMouseMove = (e: MouseEvent) => {
  66. if (!isMouseDown) return;
  67. const currentPageX = e.pageX;
  68. const currentPageY = e.pageY;
  69. const moveX = (currentPageX - startPageX) / canvasScale;
  70. const moveY = (currentPageY - startPageY) / canvasScale;
  71. // Position of line start and end points in the editing area
  72. let startX = element.left + element.start[0];
  73. let startY = element.top + element.start[1];
  74. let endX = element.left + element.end[0];
  75. let endY = element.top + element.end[1];
  76. const mid = element.broken || element.broken2 || element.curve || [0, 0];
  77. let midX = element.left + mid[0];
  78. let midY = element.top + mid[1];
  79. const [c1, c2] = element.cubic || [
  80. [0, 0],
  81. [0, 0],
  82. ];
  83. let c1X = element.left + c1[0];
  84. let c1Y = element.top + c1[1];
  85. let c2X = element.left + c2[0];
  86. let c2Y = element.top + c2[1];
  87. // Drag start or end point position
  88. // Horizontal and vertical snapping
  89. if (command === OperateLineHandlers.START) {
  90. startX = startX + moveX;
  91. startY = startY + moveY;
  92. if (Math.abs(startX - endX) < sorptionRange) startX = endX;
  93. if (Math.abs(startY - endY) < sorptionRange) startY = endY;
  94. for (const adsorptionPoint of adsorptionPoints) {
  95. const { x, y } = adsorptionPoint;
  96. if (Math.abs(x - startX) < sorptionRange && Math.abs(y - startY) < sorptionRange) {
  97. startX = x;
  98. startY = y;
  99. break;
  100. }
  101. }
  102. } else if (command === OperateLineHandlers.END) {
  103. endX = endX + moveX;
  104. endY = endY + moveY;
  105. if (Math.abs(startX - endX) < sorptionRange) endX = startX;
  106. if (Math.abs(startY - endY) < sorptionRange) endY = startY;
  107. for (const adsorptionPoint of adsorptionPoints) {
  108. const { x, y } = adsorptionPoint;
  109. if (Math.abs(x - endX) < sorptionRange && Math.abs(y - endY) < sorptionRange) {
  110. endX = x;
  111. endY = y;
  112. break;
  113. }
  114. }
  115. } else if (command === OperateLineHandlers.C) {
  116. midX = midX + moveX;
  117. midY = midY + moveY;
  118. if (Math.abs(midX - startX) < sorptionRange) midX = startX;
  119. if (Math.abs(midY - startY) < sorptionRange) midY = startY;
  120. if (Math.abs(midX - endX) < sorptionRange) midX = endX;
  121. if (Math.abs(midY - endY) < sorptionRange) midY = endY;
  122. if (
  123. Math.abs(midX - (startX + endX) / 2) < sorptionRange &&
  124. Math.abs(midY - (startY + endY) / 2) < sorptionRange
  125. ) {
  126. midX = (startX + endX) / 2;
  127. midY = (startY + endY) / 2;
  128. }
  129. } else if (command === OperateLineHandlers.C1) {
  130. c1X = c1X + moveX;
  131. c1Y = c1Y + moveY;
  132. if (Math.abs(c1X - startX) < sorptionRange) c1X = startX;
  133. if (Math.abs(c1Y - startY) < sorptionRange) c1Y = startY;
  134. if (Math.abs(c1X - endX) < sorptionRange) c1X = endX;
  135. if (Math.abs(c1Y - endY) < sorptionRange) c1Y = endY;
  136. } else if (command === OperateLineHandlers.C2) {
  137. c2X = c2X + moveX;
  138. c2Y = c2Y + moveY;
  139. if (Math.abs(c2X - startX) < sorptionRange) c2X = startX;
  140. if (Math.abs(c2Y - startY) < sorptionRange) c2Y = startY;
  141. if (Math.abs(c2X - endX) < sorptionRange) c2X = endX;
  142. if (Math.abs(c2Y - endY) < sorptionRange) c2Y = endY;
  143. }
  144. // Calculate updated start and end coordinates relative to the element's own position
  145. const minX = Math.min(startX, endX);
  146. const minY = Math.min(startY, endY);
  147. const maxX = Math.max(startX, endX);
  148. const maxY = Math.max(startY, endY);
  149. const start: [number, number] = [0, 0];
  150. const end: [number, number] = [maxX - minX, maxY - minY];
  151. if (startX > endX) {
  152. start[0] = maxX - minX;
  153. end[0] = 0;
  154. }
  155. if (startY > endY) {
  156. start[1] = maxY - minY;
  157. end[1] = 0;
  158. }
  159. // Update local element list during mousemove
  160. const newElements = elementListRef.current.map((el) => {
  161. if (el.id === element.id) {
  162. const newEl: PPTLineElement = {
  163. ...(el as PPTLineElement),
  164. left: minX,
  165. top: minY,
  166. start: start,
  167. end: end,
  168. };
  169. if (command === OperateLineHandlers.START || command === OperateLineHandlers.END) {
  170. if (ctrlOrShiftKeyActive) {
  171. if (element.broken) newEl.broken = [midX - minX, midY - minY];
  172. if (element.curve) newEl.curve = [midX - minX, midY - minY];
  173. if (element.cubic)
  174. newEl.cubic = [
  175. [c1X - minX, c1Y - minY],
  176. [c2X - minX, c2Y - minY],
  177. ];
  178. } else {
  179. if (element.broken)
  180. newEl.broken = [(start[0] + end[0]) / 2, (start[1] + end[1]) / 2];
  181. if (element.curve) newEl.curve = [(start[0] + end[0]) / 2, (start[1] + end[1]) / 2];
  182. if (element.cubic)
  183. newEl.cubic = [
  184. [(start[0] + end[0]) / 2, (start[1] + end[1]) / 2],
  185. [(start[0] + end[0]) / 2, (start[1] + end[1]) / 2],
  186. ];
  187. }
  188. if (element.broken2)
  189. newEl.broken2 = [(start[0] + end[0]) / 2, (start[1] + end[1]) / 2];
  190. } else if (command === OperateLineHandlers.C) {
  191. if (element.broken) newEl.broken = [midX - minX, midY - minY];
  192. if (element.curve) newEl.curve = [midX - minX, midY - minY];
  193. if (element.broken2) {
  194. if (maxX - minX >= maxY - minY) newEl.broken2 = [midX - minX, newEl.broken2![1]];
  195. else newEl.broken2 = [newEl.broken2![0], midY - minY];
  196. }
  197. } else {
  198. if (element.cubic)
  199. newEl.cubic = [
  200. [c1X - minX, c1Y - minY],
  201. [c2X - minX, c2Y - minY],
  202. ];
  203. }
  204. return newEl;
  205. }
  206. return el;
  207. });
  208. // Update both ref and state
  209. elementListRef.current = newElements;
  210. setElementList(newElements);
  211. };
  212. const handleMouseUp = (e: MouseEvent) => {
  213. isMouseDown = false;
  214. document.onmousemove = null;
  215. document.onmouseup = null;
  216. const currentPageX = e.pageX;
  217. const currentPageY = e.pageY;
  218. if (startPageX === currentPageX && startPageY === currentPageY) return;
  219. updateSlide({ elements: elementListRef.current });
  220. addHistorySnapshot();
  221. };
  222. document.onmousemove = handleMouseMove;
  223. document.onmouseup = handleMouseUp;
  224. },
  225. [
  226. elementListRef,
  227. setElementList,
  228. canvasScale,
  229. ctrlOrShiftKeyActive,
  230. updateSlide,
  231. addHistorySnapshot,
  232. ],
  233. );
  234. return {
  235. dragLineElement,
  236. };
  237. }