File size: 6,783 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
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
import { useState, useRef, useEffect, useMemo } from 'react';
import { useCanvasStore } from '@/lib/store';
import { useKeyboardStore } from '@/lib/store/keyboard';
import type { CreateElementSelectionData } from '@/lib/types/edit';

interface ElementCreateSelectionProps {
  onCreated: (data: CreateElementSelectionData) => void;
}

export function ElementCreateSelection({ onCreated }: ElementCreateSelectionProps) {
  const creatingElement = useCanvasStore.use.creatingElement();
  const setCreatingElement = useCanvasStore.use.setCreatingElement();
  const ctrlOrShiftKeyActive = useKeyboardStore((state) => state.ctrlOrShiftKeyActive());

  const [start, setStart] = useState<[number, number]>();
  const [end, setEnd] = useState<[number, number]>();
  const selectionRef = useRef<HTMLDivElement>(null);
  const [offset, setOffset] = useState({ x: 0, y: 0 });

  useEffect(() => {
    if (!selectionRef.current) return;
    const { x, y } = selectionRef.current.getBoundingClientRect();
    setOffset({ x, y });
  }, []);

  // Mouse drag to create element: determine position and size
  // Get the start and end positions of the selection range
  const createSelection = (e: React.MouseEvent) => {
    let isMouseDown = true;

    const startPageX = e.pageX;
    const startPageY = e.pageY;
    setStart([startPageX, startPageY]);

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

      let currentPageX = e.pageX;
      let currentPageY = e.pageY;

      // When Ctrl or Shift is held:
      // For non-line elements, lock aspect ratio; for line elements, lock to horizontal or vertical direction
      if (ctrlOrShiftKeyActive) {
        const moveX = currentPageX - startPageX;
        const moveY = currentPageY - startPageY;

        // Horizontal and vertical drag distances; use the larger one as the base for computing the other
        const absX = Math.abs(moveX);
        const absY = Math.abs(moveY);

        if (creatingElement.type === 'shape') {
          // Check if dragging in reverse direction: top-left to bottom-right is forward, everything else is reverse
          const isOpposite = (moveY > 0 && moveX < 0) || (moveY < 0 && moveX > 0);

          if (absX > absY) {
            currentPageY = isOpposite ? startPageY - moveX : startPageY + moveX;
          } else {
            currentPageX = isOpposite ? startPageX - moveY : startPageX + moveY;
          }
        } else if (creatingElement.type === 'line') {
          if (absX > absY) currentPageY = startPageY;
          else currentPageX = startPageX;
        }
      }

      setEnd([currentPageX, currentPageY]);
    };

    const handleMouseUp = (e: MouseEvent) => {
      document.removeEventListener('mousemove', handleMouseMove);
      document.removeEventListener('mouseup', handleMouseUp);

      if (e.button === 2) {
        setTimeout(() => setCreatingElement(null), 0);
        return;
      }

      isMouseDown = false;

      const endPageX = e.pageX;
      const endPageY = e.pageY;

      const minSize = 30;

      if (
        creatingElement?.type === 'line' &&
        (Math.abs(endPageX - startPageX) >= minSize || Math.abs(endPageY - startPageY) >= minSize)
      ) {
        onCreated({
          start: [startPageX, startPageY],
          end: [endPageX, endPageY],
        });
      } else if (
        creatingElement?.type !== 'line' &&
        Math.abs(endPageX - startPageX) >= minSize &&
        Math.abs(endPageY - startPageY) >= minSize
      ) {
        onCreated({
          start: [startPageX, startPageY],
          end: [endPageX, endPageY],
        });
      } else {
        const defaultSize = 200;
        const minX = Math.min(endPageX, startPageX);
        const minY = Math.min(endPageY, startPageY);
        const maxX = Math.max(endPageX, startPageX);
        const maxY = Math.max(endPageY, startPageY);
        const offsetX = maxX - minX >= minSize ? maxX - minX : defaultSize;
        const offsetY = maxY - minY >= minSize ? maxY - minY : defaultSize;
        onCreated({
          start: [minX, minY],
          end: [minX + offsetX, minY + offsetY],
        });
      }
    };

    document.addEventListener('mousemove', handleMouseMove);
    document.addEventListener('mouseup', handleMouseUp);
  };

  // Line drawing path data (only used when creating element type is line)
  const lineData = useMemo(() => {
    if (!start || !end) return null;
    if (!creatingElement || creatingElement.type !== 'line') return null;

    const [_startX, _startY] = start;
    const [_endX, _endY] = end;
    const minX = Math.min(_startX, _endX);
    const maxX = Math.max(_startX, _endX);
    const minY = Math.min(_startY, _endY);
    const maxY = Math.max(_startY, _endY);

    const svgWidth = maxX - minX >= 24 ? maxX - minX : 24;
    const svgHeight = maxY - minY >= 24 ? maxY - minY : 24;

    const startX = _startX === minX ? 0 : maxX - minX;
    const startY = _startY === minY ? 0 : maxY - minY;
    const endX = _endX === minX ? 0 : maxX - minX;
    const endY = _endY === minY ? 0 : maxY - minY;

    const path = `M${startX}, ${startY} L${endX}, ${endY}`;

    return {
      svgWidth,
      svgHeight,
      path,
    };
  }, [start, end, creatingElement]);

  // Calculate element position and size from the selection start and end positions
  const position = useMemo(() => {
    if (!start || !end) return {};

    const [startX, startY] = start;
    const [endX, endY] = end;
    const minX = Math.min(startX, endX);
    const maxX = Math.max(startX, endX);
    const minY = Math.min(startY, endY);
    const maxY = Math.max(startY, endY);

    const width = maxX - minX;
    const height = maxY - minY;

    return {
      left: minX - offset.x + 'px',
      top: minY - offset.y + 'px',
      width: width + 'px',
      height: height + 'px',
    };
  }, [start, end, offset]);

  return (
    <div
      ref={selectionRef}
      className="element-create-selection absolute top-0 left-0 w-full h-full z-[2] cursor-crosshair"
      onMouseDown={(e) => {
        e.stopPropagation();
        createSelection(e);
      }}
      onContextMenu={(e) => {
        e.stopPropagation();
        e.preventDefault();
      }}
    >
      {start && end && (
        <div
          className={`selection absolute opacity-80 ${creatingElement?.type !== 'line' ? 'border border-primary' : ''}`}
          style={position}
        >
          {/* Line drawing area */}
          {creatingElement?.type === 'line' && lineData && (
            <svg className="overflow-visible" width={lineData.svgWidth} height={lineData.svgHeight}>
              <path d={lineData.path} stroke="#d14424" fill="none" strokeWidth="2" />
            </svg>
          )}
        </div>
      )}
    </div>
  );
}