File size: 4,744 Bytes
f56a29b
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
import { useCallback, type RefObject } from 'react';
import type {
  PPTElement,
  PPTLineElement,
  PPTVideoElement,
  PPTAudioElement,
  PPTChartElement,
} from '@/lib/types/slides';
import { useHistorySnapshot } from '@/lib/hooks/use-history-snapshot';
import { useCanvasOperations } from '@/lib/hooks/use-canvas-operations';

/**
 * Calculate the angle (in radians) of the line from the origin to the given coordinates
 * @param x Coordinate x
 * @param y Coordinate y
 */
const getAngleFromCoordinate = (x: number, y: number) => {
  const radian = Math.atan2(x, y);
  const angle = (180 / Math.PI) * radian;
  return angle;
};

/**
 * Rotate element Hook
 *
 * @param elementListRef - Element list ref (stores the latest value)
 * @param setElementList - Element list setter (used to trigger re-render)
 * @param viewportRef - Viewport reference
 * @param canvasScale - Canvas scale ratio
 */
export function useRotateElement(
  elementListRef: React.RefObject<PPTElement[]>,
  setElementList: React.Dispatch<React.SetStateAction<PPTElement[]>>,
  viewportRef: RefObject<HTMLElement | null>,
  canvasScale: number,
) {
  const updateSlide = useCanvasOperations().updateSlide;

  const { addHistorySnapshot } = useHistorySnapshot();

  // Rotate element
  const rotateElement = useCallback(
    (
      e: React.MouseEvent | React.TouchEvent,
      element: Exclude<
        PPTElement,
        PPTChartElement | PPTLineElement | PPTVideoElement | PPTAudioElement
      >,
    ) => {
      const native = e.nativeEvent;
      const isTouchEvent = native instanceof TouchEvent;
      if (isTouchEvent && !native.changedTouches?.length) return;

      let isMouseDown = true;
      let angle = 0;
      const elOriginRotate = element.rotate || 0;

      const elLeft = element.left;
      const elTop = element.top;
      const elWidth = element.width;
      const elHeight = element.height;

      // Element center point (rotation center)
      const centerX = elLeft + elWidth / 2;
      const centerY = elTop + elHeight / 2;

      if (!viewportRef.current) return;
      const viewportRect = viewportRef.current.getBoundingClientRect();

      const handleMouseMove = (e: MouseEvent | TouchEvent) => {
        if (!isMouseDown) return;

        const currentPageX = e instanceof MouseEvent ? e.pageX : e.changedTouches[0].pageX;
        const currentPageY = e instanceof MouseEvent ? e.pageY : e.changedTouches[0].pageY;

        // Calculate the angle of the line from the current mouse position to the element center
        const mouseX = (currentPageX - viewportRect.left) / canvasScale;
        const mouseY = (currentPageY - viewportRect.top) / canvasScale;
        const x = mouseX - centerX;
        const y = centerY - mouseY;

        angle = getAngleFromCoordinate(x, y);

        // Snap to multiples of 45 degrees when close
        const sorptionRange = 5;
        if (Math.abs(angle) <= sorptionRange) angle = 0;
        else if (angle > 0 && Math.abs(angle - 45) <= sorptionRange) angle -= angle - 45;
        else if (angle < 0 && Math.abs(angle + 45) <= sorptionRange) angle -= angle + 45;
        else if (angle > 0 && Math.abs(angle - 90) <= sorptionRange) angle -= angle - 90;
        else if (angle < 0 && Math.abs(angle + 90) <= sorptionRange) angle -= angle + 90;
        else if (angle > 0 && Math.abs(angle - 135) <= sorptionRange) angle -= angle - 135;
        else if (angle < 0 && Math.abs(angle + 135) <= sorptionRange) angle -= angle + 135;
        else if (angle > 0 && Math.abs(angle - 180) <= sorptionRange) angle -= angle - 180;
        else if (angle < 0 && Math.abs(angle + 180) <= sorptionRange) angle -= angle + 180;

        const newElements = elementListRef.current.map((el) => {
          if (el.id === element.id && 'rotate' in el) {
            return { ...el, rotate: angle };
          }
          return el;
        });

        // Update both ref and state
        elementListRef.current = newElements;
        setElementList(newElements);
      };

      const handleMouseUp = () => {
        isMouseDown = false;
        document.onmousemove = null;
        document.onmouseup = null;
        document.ontouchmove = null;
        document.ontouchend = null;

        if (elOriginRotate === angle) return;

        updateSlide({ elements: elementListRef.current });
        addHistorySnapshot();
      };

      if (isTouchEvent) {
        document.ontouchmove = handleMouseMove;
        document.ontouchend = handleMouseUp;
      } else {
        document.onmousemove = handleMouseMove;
        document.onmouseup = handleMouseUp;
      }
    },
    [elementListRef, setElementList, viewportRef, canvasScale, updateSlide, addHistorySnapshot],
  );

  return {
    rotateElement,
  };
}