diff --git a/src/components/LayoutStep.tsx b/src/components/LayoutStep.tsx index 98ac76c..2fab6ad 100644 --- a/src/components/LayoutStep.tsx +++ b/src/components/LayoutStep.tsx @@ -13,18 +13,12 @@ type Axis = 'x' | 'y'; export const LayoutStep: React.FC = ({ config, splits, onChange }) => { const svgRef = useRef(null); - // State for interaction + // State const [phantomAxis, setPhantomAxis] = useState(null); const [mousePos, setMousePos] = useState({ x: 0, y: 0 }); const [hoveredSplit, setHoveredSplit] = useState<{ axis: Axis; index: number } | null>(null); const [dragging, setDragging] = useState<{ axis: Axis; index: number } | null>(null); - - // НОВОЕ: Состояние, чтобы кнопка не исчезала, пока мы на ней - const [isButtonHovered, setIsButtonHovered] = useState(false); - // Constants - // Увеличил порог с 0.02 до 0.03, чтобы линию было легче поймать - const SNAP_THRESHOLD = 0.03; const viewBoxW = 1000; const aspectRatio = config.drawer.depth / config.drawer.width; const viewBoxH = viewBoxW * aspectRatio; @@ -32,8 +26,8 @@ export const LayoutStep: React.FC = ({ config, splits, onChange }) => { const sortedX = useMemo(() => [0, ...splits.x, 1].sort((a, b) => a - b), [splits.x]); const sortedY = useMemo(() => [0, ...splits.y, 1].sort((a, b) => a - b), [splits.y]); - // Handlers - const handleMouseMove = (e: React.MouseEvent) => { + // --- Глобальный обработчик (для пустого места и перетаскивания) --- + const handleGlobalMouseMove = (e: React.MouseEvent) => { if (!svgRef.current) return; const rect = svgRef.current.getBoundingClientRect(); const nx = Math.max(0, Math.min(1, (e.clientX - rect.left) / rect.width)); @@ -41,6 +35,7 @@ export const LayoutStep: React.FC = ({ config, splits, onChange }) => { setMousePos({ x: nx, y: ny }); + // 1. Если тащим - обновляем позицию if (dragging) { const newSplits = { x: [...splits.x], @@ -52,71 +47,69 @@ export const LayoutStep: React.FC = ({ config, splits, onChange }) => { return; } - // ИСПРАВЛЕНИЕ: Если мышка на кнопке удаления, не пересчитываем hover, - // чтобы кнопка не исчезла под курсором. - if (isButtonHovered) return; + // 2. Если мы здесь, значит мышка НЕ на существующей линии + // (иначе событие было бы перехвачено в handleSplitHover) + setHoveredSplit(null); - // Hover logic - let foundSplit = null; + // 3. Логика Фантомной линии (создание новой) + const SNAP_THRESHOLD = 0.02; - // Check X splits - splits.x.forEach((val, idx) => { - if (Math.abs(nx - val) < SNAP_THRESHOLD) foundSplit = { axis: 'x' as Axis, index: idx }; - }); + // Проверяем, не слишком ли мы близко к краям или другим линиям (чтобы не спамить) + const closeToX = splits.x.some(val => Math.abs(nx - val) < SNAP_THRESHOLD); + const closeToY = splits.y.some(val => Math.abs(ny - val) < SNAP_THRESHOLD); + const closeToEdgeX = nx < SNAP_THRESHOLD || nx > (1 - SNAP_THRESHOLD); + const closeToEdgeY = ny < SNAP_THRESHOLD || ny > (1 - SNAP_THRESHOLD); - // Check Y splits - if (!foundSplit) { - splits.y.forEach((val, idx) => { - if (Math.abs(ny - val) < SNAP_THRESHOLD) foundSplit = { axis: 'y' as Axis, index: idx }; - }); + const distLeft = nx; + const distRight = 1 - nx; + const distTop = ny; + const distBottom = 1 - ny; + + const minXDist = Math.min(distLeft, distRight); + const minYDist = Math.min(distTop, distBottom); + + // Определяем ось по близости к краю + let potentialAxis: Axis = minXDist < minYDist ? 'y' : 'x'; + + let valid = true; + if (potentialAxis === 'x') { + if (closeToX || closeToEdgeX) valid = false; + } else { + if (closeToY || closeToEdgeY) valid = false; } - setHoveredSplit(foundSplit); - - // Phantom line logic - if (!foundSplit) { - const closeToX = splits.x.some(val => Math.abs(nx - val) < SNAP_THRESHOLD); - const closeToY = splits.y.some(val => Math.abs(ny - val) < SNAP_THRESHOLD); - const closeToEdgeX = nx < SNAP_THRESHOLD || nx > (1 - SNAP_THRESHOLD); - const closeToEdgeY = ny < SNAP_THRESHOLD || ny > (1 - SNAP_THRESHOLD); - - const distLeft = nx; - const distRight = 1 - nx; - const distTop = ny; - const distBottom = 1 - ny; - - const minXDist = Math.min(distLeft, distRight); - const minYDist = Math.min(distTop, distBottom); - - let potentialAxis: Axis = minXDist < minYDist ? 'y' : 'x'; - - let valid = true; - if (potentialAxis === 'x') { - if (closeToX || closeToEdgeX) valid = false; - } else { - if (closeToY || closeToEdgeY) valid = false; - } - - if (valid) { - setPhantomAxis(potentialAxis); - } else { - setPhantomAxis(null); - } + if (valid) { + setPhantomAxis(potentialAxis); } else { setPhantomAxis(null); } }; + // --- Обработчик наведения на КОНКРЕТНУЮ линию --- + const handleSplitHover = (e: React.MouseEvent, axis: Axis, index: number) => { + // Если мы тащим линию, позволяем событию всплыть до глобального обработчика, + // чтобы он посчитал координаты. + if (dragging) return; + + // Если просто водим мышкой - блокируем всплытие, + // чтобы глобальный обработчик не думал, что мы в пустоте. + e.stopPropagation(); + + setHoveredSplit({ axis, index }); + setPhantomAxis(null); // Убираем фантом, раз мы на линии + }; + const handleMouseDown = (e: React.MouseEvent) => { - // Если нажали на кнопку, событие там обработается (stopPropagation) - // Здесь обрабатываем только клик по пустому месту или начало драга линии - if (hoveredSplit && !isButtonHovered) { + // Клик по существующей линии (hoveredSplit уже установлен через onMouseMove линии) + if (hoveredSplit) { if (e.button === 0) { setDragging(hoveredSplit); } else if (e.button === 2) { removeSplit(hoveredSplit.axis, hoveredSplit.index); } - } else if (phantomAxis && !isButtonHovered) { + } + // Клик по пустому месту (создание) + else if (phantomAxis) { const val = phantomAxis === 'x' ? mousePos.x : mousePos.y; const newSplits = { ...splits }; newSplits[phantomAxis] = [...newSplits[phantomAxis], val]; @@ -135,7 +128,6 @@ export const LayoutStep: React.FC = ({ config, splits, onChange }) => { onChange(newSplits); setHoveredSplit(null); setDragging(null); - setIsButtonHovered(false); // Сбрасываем блокировку }; return ( @@ -155,7 +147,6 @@ export const LayoutStep: React.FC = ({ config, splits, onChange }) => {
- {/* Instruction Box */}
Инструкция @@ -167,7 +158,6 @@ export const LayoutStep: React.FC = ({ config, splits, onChange }) => {
- {/* Rulers */}
0 {config.drawer.width} мм @@ -179,7 +169,6 @@ export const LayoutStep: React.FC = ({ config, splits, onChange }) => { {config.drawer.depth} мм
- {/* Main Interactive SVG */}
= ({ config, splits, onChange }) => { ref={svgRef} viewBox={`0 0 ${viewBoxW} ${viewBoxH}`} className="w-full h-full touch-none" - onMouseMove={handleMouseMove} + onMouseMove={handleGlobalMouseMove} onMouseDown={handleMouseDown} onMouseUp={handleMouseUp} onMouseLeave={handleMouseUp} @@ -207,7 +196,7 @@ export const LayoutStep: React.FC = ({ config, splits, onChange }) => { - {/* --- Cell Dimensions Labels --- */} + {/* --- Labels --- */} {sortedX.slice(0, -1).map((x1, i) => { const x2 = sortedX[i + 1]; return sortedY.slice(0, -1).map((y1, j) => { @@ -244,7 +233,7 @@ export const LayoutStep: React.FC = ({ config, splits, onChange }) => { }); })} - {/* --- Existing X Lines (Vertical) --- */} + {/* --- X Lines (Vertical) --- */} {splits.x.map((x, i) => { const isHovered = hoveredSplit?.axis === 'x' && hoveredSplit.index === i; const isDragging = dragging?.axis === 'x' && dragging.index === i; @@ -252,26 +241,28 @@ export const LayoutStep: React.FC = ({ config, splits, onChange }) => { const width = isHovered || isDragging ? 8 : 4; return ( - removeSplit('x', i)}> + removeSplit('x', i)} + // ВАЖНО: Событие вешаем на группу + onMouseMove={(e) => handleSplitHover(e, 'x', i)} + > + {/* Невидимая широкая зона захвата (80 единиц!) */} + {/* Видимая линия */} {(isHovered || isDragging) && ( - { e.stopPropagation(); removeSplit('x', i); }} - onMouseEnter={() => setIsButtonHovered(true)} // Блокируем скрытие - onMouseLeave={() => setIsButtonHovered(false)} - > + { e.stopPropagation(); removeSplit('x', i); }}> @@ -280,7 +271,7 @@ export const LayoutStep: React.FC = ({ config, splits, onChange }) => { ); })} - {/* --- Existing Y Lines (Horizontal) --- */} + {/* --- Y Lines (Horizontal) --- */} {splits.y.map((y, i) => { const isHovered = hoveredSplit?.axis === 'y' && hoveredSplit.index === i; const isDragging = dragging?.axis === 'y' && dragging.index === i; @@ -288,26 +279,25 @@ export const LayoutStep: React.FC = ({ config, splits, onChange }) => { const width = isHovered || isDragging ? 8 : 4; return ( - removeSplit('y', i)}> + removeSplit('y', i)} + onMouseMove={(e) => handleSplitHover(e, 'y', i)} + > {(isHovered || isDragging) && ( - { e.stopPropagation(); removeSplit('y', i); }} - onMouseEnter={() => setIsButtonHovered(true)} // Блокируем скрытие - onMouseLeave={() => setIsButtonHovered(false)} - > + { e.stopPropagation(); removeSplit('y', i); }}> @@ -317,26 +307,26 @@ export const LayoutStep: React.FC = ({ config, splits, onChange }) => { })} {/* --- Phantom Lines --- */} - {!hoveredSplit && !dragging && !isButtonHovered && phantomAxis === 'x' && ( - + {!hoveredSplit && !dragging && phantomAxis === 'x' && ( + )} - {!hoveredSplit && !dragging && !isButtonHovered && phantomAxis === 'y' && ( - + {!hoveredSplit && !dragging && phantomAxis === 'y' && ( +