import React, { useRef, useState, useMemo } from 'react'; import { AppConfig, LayoutSplits } from '../types'; import { Grid, MousePointer2, Trash2, RotateCcw } from 'lucide-react'; interface Props { config: AppConfig; splits: LayoutSplits; onChange: (splits: LayoutSplits) => void; } type Axis = 'x' | 'y'; 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 [hoveredSplit, setHoveredSplit] = useState<{ axis: Axis; index: number } | null>(null); const [dragging, setDragging] = useState<{ axis: Axis; index: number } | null>(null); // Constants const SNAP_THRESHOLD = 0.02; // Reduced threshold slightly for better precision on large grid 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]); // Handlers const handleMouseMove = (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)); const ny = Math.max(0, Math.min(1, (e.clientY - rect.top) / rect.height)); 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 let foundSplit = null; // Check X splits (Vertical lines) splits.x.forEach((val, idx) => { if (Math.abs(nx - val) < SNAP_THRESHOLD) foundSplit = { axis: 'x' as Axis, index: idx }; }); // Check Y splits (Horizontal lines) if (!foundSplit) { splits.y.forEach((val, idx) => { if (Math.abs(ny - val) < SNAP_THRESHOLD) foundSplit = { axis: 'y' as Axis, index: idx }; }); } setHoveredSplit(foundSplit); // Auto-detect axis for NEW lines if not hovering existing // AND if not too close to other lines 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); // 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); } } else { setPhantomAxis(null); } }; const handleMouseDown = (e: React.MouseEvent) => { if (hoveredSplit) { // Start Dragging if (e.button === 0) { // Left click setDragging(hoveredSplit); } else if (e.button === 2) { // Right click removeSplit(hoveredSplit.axis, hoveredSplit.index); } } else if (phantomAxis) { // Create New Split 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 }); } }; const handleMouseUp = () => { setDragging(null); }; const removeSplit = (axis: Axis, index: number) => { const newSplits = { ...splits }; newSplits[axis] = newSplits[axis].filter((_, i) => i !== index); onChange(newSplits); setHoveredSplit(null); setDragging(null); }; return (

2. Редактор макета

{/* Instruction Box */}
Инструкция
  • Клик у края: Новая линия
  • Перетаскивание: Изменить размер
  • Двойной клик/ПКМ: Удалить
{/* Rulers */}
0 {config.drawer.width} мм
0 {config.drawer.depth} мм
{/* Main Interactive SVG */}
e.preventDefault()} > {/* --- Cell Dimensions Labels --- */} {sortedX.slice(0, -1).map((x1, i) => { const x2 = sortedX[i + 1]; return sortedY.slice(0, -1).map((y1, j) => { const y2 = sortedY[j + 1]; const width = (x2 - x1) * config.drawer.width; const depth = (y2 - y1) * config.drawer.depth; const centerX = ((x1 + x2) / 2) * viewBoxW; const centerY = ((y1 + y2) / 2) * viewBoxH; 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 ( {width.toFixed(0)} × {depth.toFixed(0)} ); }); })} {/* --- Existing X Lines (Vertical) --- */} {splits.x.map((x, i) => { const isHovered = hoveredSplit?.axis === 'x' && hoveredSplit.index === i; const isDragging = dragging?.axis === 'x' && dragging.index === i; const color = isHovered || isDragging ? '#f59e0b' : '#64748b'; const width = isHovered || isDragging ? 8 : 4; return ( removeSplit('x', i)}> {/* Invisible wide hit area */} {/* Visible Line */} {/* Delete Button if hovered */} {(isHovered || isDragging) && ( { e.stopPropagation(); removeSplit('x', i); }}> )} ); })} {/* --- Existing Y Lines (Horizontal) --- */} {splits.y.map((y, i) => { const isHovered = hoveredSplit?.axis === 'y' && hoveredSplit.index === i; const isDragging = dragging?.axis === 'y' && dragging.index === i; const color = isHovered || isDragging ? '#f59e0b' : '#64748b'; const width = isHovered || isDragging ? 8 : 4; return ( removeSplit('y', i)}> {/* Invisible wide hit area */} {(isHovered || isDragging) && ( { e.stopPropagation(); removeSplit('y', i); }}> )} ); })} {/* --- Phantom Line (Preview) --- */} {!hoveredSplit && !dragging && phantomAxis === 'x' && ( )} {!hoveredSplit && !dragging && phantomAxis === 'y' && ( )}
); };