diff --git a/src/components/LayoutStep.tsx b/src/components/LayoutStep.tsx index e72a282..526c82e 100644 --- a/src/components/LayoutStep.tsx +++ b/src/components/LayoutStep.tsx @@ -1,7 +1,7 @@ import React, { useRef, useState, useMemo } from 'react'; import { AppConfig, LayoutSplits, Partition } from '../types'; -// Только используемые иконки! -import { Grid, MousePointer2, Trash2, RotateCcw, X } from 'lucide-react'; +// ИСПРАВЛЕНИЕ: Добавлена иконка Move в импорты +import { Grid, MousePointer2, Trash2, RotateCcw, X, Plus, Move } from 'lucide-react'; interface Props { config: AppConfig; @@ -12,7 +12,7 @@ interface Props { type EditMode = 'lines' | 'cells'; type Axis = 'x' | 'y'; -// Типы для перетаскивания +// Тип для перетаскивания type DragTarget = | { type: 'main'; axis: Axis; index: number } | { type: 'partition'; cellKey: string; id: string; axis: Axis }; @@ -22,38 +22,41 @@ export const LayoutStep: React.FC = ({ config, splits, onChange }) => { const [mode, setMode] = useState('lines'); - // Состояния мыши и взаимодействия + // Мышь и состояния const [mousePos, setMousePos] = useState({ x: 0, y: 0 }); const [isButtonHovered, setIsButtonHovered] = useState(false); - const [dragging, setDragging] = useState(null); - // Состояния Main Grid + // Main Grid States const [phantomMainAxis, setPhantomMainAxis] = useState(null); const [hoveredMainSplit, setHoveredMainSplit] = useState<{ axis: Axis; index: number } | null>(null); - // Состояния Partitions + // Partition States const [hoveredCell, setHoveredCell] = useState<{ i: number; j: number } | null>(null); const [hoveredPartition, setHoveredPartition] = useState<{ id: string; cellKey: string } | null>(null); const [phantomPartition, setPhantomPartition] = useState<{ axis: Axis; offset: number } | null>(null); + + // Selection & Dragging const [selectedPartitionId, setSelectedPartitionId] = useState(null); + const [dragging, setDragging] = useState(null); - // --- Safe Data --- + // --- Safe Data Access --- const safeX = Array.isArray(splits?.x) ? splits.x : []; const safeY = Array.isArray(splits?.y) ? splits.y : []; const safePartitions = splits?.partitions || {}; - // Расчет размеров + // --- Dimensions & Aspect Ratio --- const drawerW = Math.max(1, config.drawer.width || 300); const drawerD = Math.max(1, config.drawer.depth || 400); const aspectRatio = drawerD / drawerW; + // ViewBox (internal SVG coordinates) const viewBoxW = 1000; const viewBoxH = viewBoxW * aspectRatio; const sortedX = useMemo(() => [0, ...safeX, 1].sort((a, b) => a - b), [safeX]); const sortedY = useMemo(() => [0, ...safeY, 1].sort((a, b) => a - b), [safeY]); - // --- Helper: Get Selected Data --- + // --- Helper: Get Partition Data --- const getSelectedPartition = () => { if (!selectedPartitionId) return null; for (const key in safePartitions) { @@ -64,7 +67,7 @@ export const LayoutStep: React.FC = ({ config, splits, onChange }) => { }; const selectedData = getSelectedPartition(); - // --- ACTIONS --- + // --- ACTIONS: MAIN GRID --- const removeMainSplit = (axis: Axis, index: number) => { const newSplits = { ...splits, x: [...safeX], y: [...safeY] }; newSplits[axis] = newSplits[axis].filter((_, i) => i !== index); @@ -74,6 +77,7 @@ export const LayoutStep: React.FC = ({ config, splits, onChange }) => { setIsButtonHovered(false); }; + // --- ACTIONS: PARTITIONS --- const createPartition = (i: number, j: number, axis: Axis, offset: number) => { const key = `${i}-${j}`; const current = safePartitions[key] || []; @@ -107,11 +111,12 @@ export const LayoutStep: React.FC = ({ config, splits, onChange }) => { const rect = svgRef.current.getBoundingClientRect(); if (rect.width === 0) return; + // Normalize coordinates (0 to 1) const nx = Math.max(0, Math.min(1, (e.clientX - rect.left) / rect.width)); const ny = Math.max(0, Math.min(1, (e.clientY - rect.top) / rect.height)); setMousePos({ x: nx, y: ny }); - // Dragging + // --- DRAGGING LOGIC --- if (dragging) { if (dragging.type === 'main') { const newSplits = { ...splits, x: [...safeX], y: [...safeY] }; @@ -119,7 +124,7 @@ export const LayoutStep: React.FC = ({ config, splits, onChange }) => { newSplits[dragging.axis][dragging.index] = val; onChange(newSplits); } else { - // Drag Partition + // Dragging Partition const pDrag = dragging as { type: 'partition'; cellKey: string; id: string; axis: Axis }; const [iStr, jStr] = pDrag.cellKey.split('-'); const i = parseInt(iStr); const j = parseInt(jStr); @@ -141,10 +146,12 @@ export const LayoutStep: React.FC = ({ config, splits, onChange }) => { if (isButtonHovered) return; - // Mode: Lines + // --- HOVER LOGIC: MAIN LINES --- if (mode === 'lines') { setHoveredMainSplit(null); - const SNAP = 0.015; + const SNAP = 0.015; + + // Phantom Line if (nx > SNAP && nx < 1-SNAP && ny > SNAP && ny < 1-SNAP) { const closeToX = safeX.some(val => Math.abs(nx - val) < SNAP); const closeToY = safeY.some(val => Math.abs(ny - val) < SNAP); @@ -154,13 +161,14 @@ export const LayoutStep: React.FC = ({ config, splits, onChange }) => { } else { setPhantomMainAxis(null); } } } - // Mode: Cells + + // --- HOVER LOGIC: PARTITIONS --- else if (mode === 'cells') { setHoveredPartition(null); setPhantomPartition(null); setHoveredCell(null); - // Find cell + // 1. Find which cell we are in let cellIndex = null; for(let i=0; i= sortedX[i] && nx <= sortedX[i+1]) { @@ -182,12 +190,14 @@ export const LayoutStep: React.FC = ({ config, splits, onChange }) => { const cy1 = sortedY[cellIndex.j]; const cy2 = sortedY[cellIndex.j+1]; const cw = cx2 - cx1; const ch = cy2 - cy1; + // Local coords in cell (0..1) const lx = (nx - cx1) / cw; const ly = (ny - cy1) / ch; - // Check hovered partitions + // Check existing partitions let foundPart = null; const PART_SNAP = 0.05; + for (const p of parts) { if (p.axis === 'x') { if (Math.abs(lx - p.offset) < PART_SNAP * (aspectRatio > 1 ? 1 : aspectRatio)) foundPart = p; @@ -199,21 +209,13 @@ export const LayoutStep: React.FC = ({ config, splits, onChange }) => { if (foundPart) { setHoveredPartition({ id: foundPart.id, cellKey: key }); } else { - // Determine split direction based on mouse proximity to center/edges - // Simple heuristic: If moving horizontally -> vertical split (x), else horizontal (y) - // Let's use distance to edges. Closer to left/right edge -> Vertical split. + // Show Phantom Partition const distLeft = lx; const distRight = 1 - lx; const distTop = ly; const distBottom = 1 - ly; const minX = Math.min(distLeft, distRight); const minY = Math.min(distTop, distBottom); - // If closer to X edges, create Y split (horizontal)? No. - // If closer to top/bottom edges, we want to split vertically (connect top-bottom)? - // Let's create a split PERPENDICULAR to the closest edge. - // If closest edge is Left(vertical), we want a Horizontal line? - // No, typically we want a line PARALLEL to the closest edge. - - const axis = minX < minY ? 'x' : 'y'; // Parallel to closest edge + const axis = minX < minY ? 'y' : 'x'; if (lx > 0.05 && lx < 0.95 && ly > 0.05 && ly < 0.95) { setPhantomPartition({ axis, offset: axis === 'x' ? lx : ly }); @@ -261,6 +263,13 @@ export const LayoutStep: React.FC = ({ config, splits, onChange }) => { } }; + const handleMainSplitHover = (e: React.MouseEvent, axis: Axis, index: number) => { + if (mode !== 'lines' || dragging) return; + e.stopPropagation(); + setHoveredMainSplit({ axis, index }); + setPhantomMainAxis(null); + }; + return (
@@ -273,11 +282,11 @@ export const LayoutStep: React.FC = ({ config, splits, onChange }) => {
@@ -286,7 +295,7 @@ export const LayoutStep: React.FC = ({ config, splits, onChange }) => {
- {/* INSTRUCTIONS */} + {/* --- INSTRUCTIONS --- */}
{mode === 'lines' ? ( @@ -303,10 +312,11 @@ export const LayoutStep: React.FC = ({ config, splits, onChange }) => { )}
- {/* CANVAS */} + {/* --- CANVAS --- */}
-
1 ? 'auto' : '100%', height: aspectRatio > 1 ? '100%' : 'auto', @@ -316,9 +326,16 @@ export const LayoutStep: React.FC = ({ config, splits, onChange }) => { : (dragging ? 'grabbing' : hoveredPartition ? 'grab' : hoveredCell ? 'crosshair' : 'default'), }} > - setDragging(null)} onMouseLeave={() => setDragging(null)} onContextMenu={(e) => e.preventDefault()} + onMouseMove={handleMouseMove} + onMouseDown={handleMouseDown} + onMouseUp={() => setDragging(null)} + onMouseLeave={() => setDragging(null)} + onContextMenu={(e) => e.preventDefault()} > @@ -327,13 +344,11 @@ export const LayoutStep: React.FC = ({ config, splits, onChange }) => { - {/* CELLS & PARTITIONS */} + {/* --- CELLS & PARTITIONS --- */} {sortedX.slice(0, -1).map((x1, i) => { const x2 = sortedX[i + 1]; return sortedY.slice(0, -1).map((y1, j) => { - // FIX: Добавлено определение y2 const y2 = sortedY[j + 1]; - const cellX = x1 * viewBoxW; const cellY = y1 * viewBoxH; const cellW = (x2 - x1) * viewBoxW; const cellH = (y2 - y1) * viewBoxH; @@ -352,7 +367,6 @@ export const LayoutStep: React.FC = ({ config, splits, onChange }) => { {parts.map(p => { const isSelected = selectedPartitionId === p.id; const isHoveredPart = hoveredPartition?.id === p.id; - let lx1, ly1, lx2, ly2; if (p.axis === 'x') { const px = cellX + (cellW * p.offset); lx1 = px; ly1 = cellY; lx2 = px; ly2 = cellY + cellH; } else { const py = cellY + (cellH * p.offset); lx1 = cellX; ly1 = py; lx2 = cellX + cellW; ly2 = py; } @@ -379,7 +393,7 @@ export const LayoutStep: React.FC = ({ config, splits, onChange }) => { }); })} - {/* MAIN GRID X */} + {/* --- MAIN GRID X --- */} {safeX.map((x, i) => { const isHovered = hoveredMainSplit?.axis === 'x' && hoveredMainSplit.index === i; return ( @@ -393,7 +407,7 @@ export const LayoutStep: React.FC = ({ config, splits, onChange }) => { ); })} - {/* MAIN GRID Y */} + {/* --- MAIN GRID Y --- */} {safeY.map((y, i) => { const isHovered = hoveredMainSplit?.axis === 'y' && hoveredMainSplit.index === i; return ( @@ -419,7 +433,7 @@ export const LayoutStep: React.FC = ({ config, splits, onChange }) => {

- Настройки стенки + Настройки стенки