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>
);
}
|