ElementCreateSelection.tsx 6.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200
  1. import { useState, useRef, useEffect, useMemo } from 'react';
  2. import { useCanvasStore } from '@/lib/store';
  3. import { useKeyboardStore } from '@/lib/store/keyboard';
  4. import type { CreateElementSelectionData } from '@/lib/types/edit';
  5. interface ElementCreateSelectionProps {
  6. onCreated: (data: CreateElementSelectionData) => void;
  7. }
  8. export function ElementCreateSelection({ onCreated }: ElementCreateSelectionProps) {
  9. const creatingElement = useCanvasStore.use.creatingElement();
  10. const setCreatingElement = useCanvasStore.use.setCreatingElement();
  11. const ctrlOrShiftKeyActive = useKeyboardStore((state) => state.ctrlOrShiftKeyActive());
  12. const [start, setStart] = useState<[number, number]>();
  13. const [end, setEnd] = useState<[number, number]>();
  14. const selectionRef = useRef<HTMLDivElement>(null);
  15. const [offset, setOffset] = useState({ x: 0, y: 0 });
  16. useEffect(() => {
  17. if (!selectionRef.current) return;
  18. const { x, y } = selectionRef.current.getBoundingClientRect();
  19. setOffset({ x, y });
  20. }, []);
  21. // Mouse drag to create element: determine position and size
  22. // Get the start and end positions of the selection range
  23. const createSelection = (e: React.MouseEvent) => {
  24. let isMouseDown = true;
  25. const startPageX = e.pageX;
  26. const startPageY = e.pageY;
  27. setStart([startPageX, startPageY]);
  28. const handleMouseMove = (e: MouseEvent) => {
  29. if (!creatingElement || !isMouseDown) return;
  30. let currentPageX = e.pageX;
  31. let currentPageY = e.pageY;
  32. // When Ctrl or Shift is held:
  33. // For non-line elements, lock aspect ratio; for line elements, lock to horizontal or vertical direction
  34. if (ctrlOrShiftKeyActive) {
  35. const moveX = currentPageX - startPageX;
  36. const moveY = currentPageY - startPageY;
  37. // Horizontal and vertical drag distances; use the larger one as the base for computing the other
  38. const absX = Math.abs(moveX);
  39. const absY = Math.abs(moveY);
  40. if (creatingElement.type === 'shape') {
  41. // Check if dragging in reverse direction: top-left to bottom-right is forward, everything else is reverse
  42. const isOpposite = (moveY > 0 && moveX < 0) || (moveY < 0 && moveX > 0);
  43. if (absX > absY) {
  44. currentPageY = isOpposite ? startPageY - moveX : startPageY + moveX;
  45. } else {
  46. currentPageX = isOpposite ? startPageX - moveY : startPageX + moveY;
  47. }
  48. } else if (creatingElement.type === 'line') {
  49. if (absX > absY) currentPageY = startPageY;
  50. else currentPageX = startPageX;
  51. }
  52. }
  53. setEnd([currentPageX, currentPageY]);
  54. };
  55. const handleMouseUp = (e: MouseEvent) => {
  56. document.removeEventListener('mousemove', handleMouseMove);
  57. document.removeEventListener('mouseup', handleMouseUp);
  58. if (e.button === 2) {
  59. setTimeout(() => setCreatingElement(null), 0);
  60. return;
  61. }
  62. isMouseDown = false;
  63. const endPageX = e.pageX;
  64. const endPageY = e.pageY;
  65. const minSize = 30;
  66. if (
  67. creatingElement?.type === 'line' &&
  68. (Math.abs(endPageX - startPageX) >= minSize || Math.abs(endPageY - startPageY) >= minSize)
  69. ) {
  70. onCreated({
  71. start: [startPageX, startPageY],
  72. end: [endPageX, endPageY],
  73. });
  74. } else if (
  75. creatingElement?.type !== 'line' &&
  76. Math.abs(endPageX - startPageX) >= minSize &&
  77. Math.abs(endPageY - startPageY) >= minSize
  78. ) {
  79. onCreated({
  80. start: [startPageX, startPageY],
  81. end: [endPageX, endPageY],
  82. });
  83. } else {
  84. const defaultSize = 200;
  85. const minX = Math.min(endPageX, startPageX);
  86. const minY = Math.min(endPageY, startPageY);
  87. const maxX = Math.max(endPageX, startPageX);
  88. const maxY = Math.max(endPageY, startPageY);
  89. const offsetX = maxX - minX >= minSize ? maxX - minX : defaultSize;
  90. const offsetY = maxY - minY >= minSize ? maxY - minY : defaultSize;
  91. onCreated({
  92. start: [minX, minY],
  93. end: [minX + offsetX, minY + offsetY],
  94. });
  95. }
  96. };
  97. document.addEventListener('mousemove', handleMouseMove);
  98. document.addEventListener('mouseup', handleMouseUp);
  99. };
  100. // Line drawing path data (only used when creating element type is line)
  101. const lineData = useMemo(() => {
  102. if (!start || !end) return null;
  103. if (!creatingElement || creatingElement.type !== 'line') return null;
  104. const [_startX, _startY] = start;
  105. const [_endX, _endY] = end;
  106. const minX = Math.min(_startX, _endX);
  107. const maxX = Math.max(_startX, _endX);
  108. const minY = Math.min(_startY, _endY);
  109. const maxY = Math.max(_startY, _endY);
  110. const svgWidth = maxX - minX >= 24 ? maxX - minX : 24;
  111. const svgHeight = maxY - minY >= 24 ? maxY - minY : 24;
  112. const startX = _startX === minX ? 0 : maxX - minX;
  113. const startY = _startY === minY ? 0 : maxY - minY;
  114. const endX = _endX === minX ? 0 : maxX - minX;
  115. const endY = _endY === minY ? 0 : maxY - minY;
  116. const path = `M${startX}, ${startY} L${endX}, ${endY}`;
  117. return {
  118. svgWidth,
  119. svgHeight,
  120. path,
  121. };
  122. }, [start, end, creatingElement]);
  123. // Calculate element position and size from the selection start and end positions
  124. const position = useMemo(() => {
  125. if (!start || !end) return {};
  126. const [startX, startY] = start;
  127. const [endX, endY] = end;
  128. const minX = Math.min(startX, endX);
  129. const maxX = Math.max(startX, endX);
  130. const minY = Math.min(startY, endY);
  131. const maxY = Math.max(startY, endY);
  132. const width = maxX - minX;
  133. const height = maxY - minY;
  134. return {
  135. left: minX - offset.x + 'px',
  136. top: minY - offset.y + 'px',
  137. width: width + 'px',
  138. height: height + 'px',
  139. };
  140. }, [start, end, offset]);
  141. return (
  142. <div
  143. ref={selectionRef}
  144. className="element-create-selection absolute top-0 left-0 w-full h-full z-[2] cursor-crosshair"
  145. onMouseDown={(e) => {
  146. e.stopPropagation();
  147. createSelection(e);
  148. }}
  149. onContextMenu={(e) => {
  150. e.stopPropagation();
  151. e.preventDefault();
  152. }}
  153. >
  154. {start && end && (
  155. <div
  156. className={`selection absolute opacity-80 ${creatingElement?.type !== 'line' ? 'border border-primary' : ''}`}
  157. style={position}
  158. >
  159. {/* Line drawing area */}
  160. {creatingElement?.type === 'line' && lineData && (
  161. <svg className="overflow-visible" width={lineData.svgWidth} height={lineData.svgHeight}>
  162. <path d={lineData.path} stroke="#d14424" fill="none" strokeWidth="2" />
  163. </svg>
  164. )}
  165. </div>
  166. )}
  167. </div>
  168. );
  169. }