diff --git a/src/components/LayoutStep.tsx b/src/components/LayoutStep.tsx index a8861eb..98ac76c 100644 --- a/src/components/LayoutStep.tsx +++ b/src/components/LayoutStep.tsx @@ -14,19 +14,21 @@ export const LayoutStep: React.FC = ({ config, splits, onChange }) => { const svgRef = useRef(null); // State for interaction - const [phantomAxis, setPhantomAxis] = useState(null); // Proposed new line axis - const [mousePos, setMousePos] = useState({ x: 0, y: 0 }); // Normalized 0-1 + 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 - const SNAP_THRESHOLD = 0.02; // Reduced threshold slightly for better precision on large grid + // Увеличил порог с 0.02 до 0.03, чтобы линию было легче поймать + const SNAP_THRESHOLD = 0.03; const viewBoxW = 1000; const aspectRatio = config.drawer.depth / config.drawer.width; const viewBoxH = viewBoxW * aspectRatio; - // Helpers to calculate cell dimensions - // IMPORTANT: Dependencies must be correct. dragging updates splits, which updates sortedX/Y 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]); @@ -40,29 +42,29 @@ export const LayoutStep: React.FC = ({ config, splits, onChange }) => { setMousePos({ x: nx, y: ny }); if (dragging) { - // Move logic - // IMPORTANT: Create NEW array references so useMemo in this component and parent components detect changes const newSplits = { x: [...splits.x], y: [...splits.y] }; - const val = dragging.axis === 'x' ? nx : ny; newSplits[dragging.axis][dragging.index] = val; - onChange(newSplits); return; } - // Hover logic: Check if near existing lines + // ИСПРАВЛЕНИЕ: Если мышка на кнопке удаления, не пересчитываем hover, + // чтобы кнопка не исчезла под курсором. + if (isButtonHovered) return; + + // Hover logic let foundSplit = null; - // Check X splits (Vertical lines) + // Check X splits splits.x.forEach((val, idx) => { if (Math.abs(nx - val) < SNAP_THRESHOLD) foundSplit = { axis: 'x' as Axis, index: idx }; }); - // Check Y splits (Horizontal lines) + // Check Y splits if (!foundSplit) { splits.y.forEach((val, idx) => { if (Math.abs(ny - val) < SNAP_THRESHOLD) foundSplit = { axis: 'y' as Axis, index: idx }; @@ -71,69 +73,54 @@ export const LayoutStep: React.FC = ({ config, splits, onChange }) => { setHoveredSplit(foundSplit); - // Auto-detect axis for NEW lines if not hovering existing - // AND if not too close to other lines + // Phantom line logic if (!foundSplit) { - // Check proximity to ALL lines to prevent creating duplicates const closeToX = splits.x.some(val => Math.abs(nx - val) < SNAP_THRESHOLD); const closeToY = splits.y.some(val => Math.abs(ny - val) < SNAP_THRESHOLD); - - // Also check edges (0 and 1) const closeToEdgeX = nx < SNAP_THRESHOLD || nx > (1 - SNAP_THRESHOLD); const closeToEdgeY = ny < SNAP_THRESHOLD || ny > (1 - SNAP_THRESHOLD); - if (closeToX || closeToEdgeX) { - // Too close to X line or edge, don't allow vertical split here - // But might allow horizontal? - // Actually if we are close to an X line, we probably want to select it, which is handled by foundSplit. - // If foundSplit is null but closeToX is true, it means we are just outside threshold? - // Let's simplified: if too close to any parallel line, disable creation. + 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; } - 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); - - // Determine potential axis - let potentialAxis: Axis = minXDist < minYDist ? 'y' : 'x'; - - // Validate proximity for that axis - 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); + } } else { setPhantomAxis(null); } }; const handleMouseDown = (e: React.MouseEvent) => { - if (hoveredSplit) { - // Start Dragging - if (e.button === 0) { // Left click + // Если нажали на кнопку, событие там обработается (stopPropagation) + // Здесь обрабатываем только клик по пустому месту или начало драга линии + if (hoveredSplit && !isButtonHovered) { + if (e.button === 0) { setDragging(hoveredSplit); - } else if (e.button === 2) { // Right click + } else if (e.button === 2) { removeSplit(hoveredSplit.axis, hoveredSplit.index); } - } else if (phantomAxis) { - // Create New Split + } else if (phantomAxis && !isButtonHovered) { const val = phantomAxis === 'x' ? mousePos.x : mousePos.y; const newSplits = { ...splits }; newSplits[phantomAxis] = [...newSplits[phantomAxis], val]; onChange(newSplits); - // Immediately start dragging the new line for fine-tuning setDragging({ axis: phantomAxis, index: newSplits[phantomAxis].length - 1 }); } }; @@ -148,6 +135,7 @@ export const LayoutStep: React.FC = ({ config, splits, onChange }) => { onChange(newSplits); setHoveredSplit(null); setDragging(null); + setIsButtonHovered(false); // Сбрасываем блокировку }; return ( @@ -216,10 +204,6 @@ export const LayoutStep: React.FC = ({ config, splits, onChange }) => { - - - - @@ -236,14 +220,9 @@ export const LayoutStep: React.FC = ({ config, splits, onChange }) => { const cellWidthSVG = (x2 - x1) * viewBoxW; const cellHeightSVG = (y2 - y1) * viewBoxH; - // Dynamic Font Sizing Logic - // Max font size: 36px - // Must fit in height: 60% of height max - // Must fit in width: approx 25% of width max (assuming ~5 chars) let fontSize = Math.min(36, cellHeightSVG * 0.6); fontSize = Math.min(fontSize, cellWidthSVG * 0.25); - // Don't show text if calculated font size is too small to be readable if (fontSize < 10) return null; return ( @@ -274,23 +253,25 @@ export const LayoutStep: React.FC = ({ config, splits, onChange }) => { return ( removeSplit('x', i)}> - {/* Invisible wide hit area */} - {/* Visible Line */} - {/* Delete Button if hovered */} {(isHovered || isDragging) && ( - { e.stopPropagation(); removeSplit('x', i); }}> + { e.stopPropagation(); removeSplit('x', i); }} + onMouseEnter={() => setIsButtonHovered(true)} // Блокируем скрытие + onMouseLeave={() => setIsButtonHovered(false)} + > @@ -308,7 +289,6 @@ export const LayoutStep: React.FC = ({ config, splits, onChange }) => { return ( removeSplit('y', i)}> - {/* Invisible wide hit area */} = ({ config, splits, onChange }) => { className="transition-all duration-150" /> {(isHovered || isDragging) && ( - { e.stopPropagation(); removeSplit('y', i); }}> + { e.stopPropagation(); removeSplit('y', i); }} + onMouseEnter={() => setIsButtonHovered(true)} // Блокируем скрытие + onMouseLeave={() => setIsButtonHovered(false)} + > @@ -331,15 +316,13 @@ export const LayoutStep: React.FC = ({ config, splits, onChange }) => { ); })} - {/* --- Phantom Line (Preview) --- */} - {!hoveredSplit && !dragging && phantomAxis === 'x' && ( + {/* --- Phantom Lines --- */} + {!hoveredSplit && !dragging && !isButtonHovered && phantomAxis === 'x' && ( @@ -347,14 +330,12 @@ export const LayoutStep: React.FC = ({ config, splits, onChange }) => { )} - {!hoveredSplit && !dragging && phantomAxis === 'y' && ( + {!hoveredSplit && !dragging && !isButtonHovered && phantomAxis === 'y' && (