diff --git a/src/components/LayoutStep.tsx b/src/components/LayoutStep.tsx index c3f84f6..e963ce4 100644 --- a/src/components/LayoutStep.tsx +++ b/src/components/LayoutStep.tsx @@ -1,6 +1,6 @@ import React, { useRef, useState, useMemo } from 'react'; import { AppConfig, LayoutSplits } from '../types'; -import { Grid, MousePointer2, Trash2, RotateCcw, LayoutGrid, X } from 'lucide-react'; +import { Grid, MousePointer2, Trash2, RotateCcw, LayoutGrid, X, Plus, Minus, SplitSquareVertical, SplitSquareHorizontal } from 'lucide-react'; interface Props { config: AppConfig; @@ -8,54 +8,55 @@ interface Props { onChange: (splits: LayoutSplits) => void; } -type Axis = 'x' | 'y'; type EditMode = 'lines' | 'cells'; +type Axis = 'x' | 'y'; export const LayoutStep: React.FC = ({ config, splits, onChange }) => { const svgRef = useRef(null); - // State + // Режимы: 'lines' (двигать линии) или 'cells' (дробить ячейки) const [mode, setMode] = useState('lines'); + + // Состояния для линий 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); - // New State for Cells + // Состояние для ячеек const [selectedCell, setSelectedCell] = useState<{ i: number, j: number } | null>(null); const viewBoxW = 1000; const aspectRatio = config.drawer.depth / config.drawer.width; const viewBoxH = viewBoxW * aspectRatio; - // Сортируем линии, чтобы понимать границы ячеек 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]); - // --- Helpers for Subdivision --- - const updateSubdivision = (i: number, j: number, field: 'rows' | 'cols', delta: number) => { - const key = `${i}-${j}`; - const current = splits.subdivisions?.[key] || { rows: 1, cols: 1 }; - const newVal = Math.max(1, Math.min(10, current[field] + delta)); - - // Если 1x1, удаляем запись, чтобы не засорять - const newSubdivisions = { ...splits.subdivisions }; - - if (newVal === 1 && (field === 'rows' ? current.cols : current.rows) === 1) { - delete newSubdivisions[key]; - } else { - newSubdivisions[key] = { ...current, [field]: newVal }; - } - - onChange({ ...splits, subdivisions: newSubdivisions }); - }; - + // --- Логика разделения ячеек --- const getSubdivision = (i: number, j: number) => { return splits.subdivisions?.[`${i}-${j}`] || { rows: 1, cols: 1 }; }; - // --- Handlers --- + const updateSubdivision = (i: number, j: number, type: 'rows' | 'cols', delta: number) => { + const key = `${i}-${j}`; + const current = getSubdivision(i, j); + const newVal = Math.max(1, Math.min(10, current[type] + delta)); + + const newSubdivisions = { ...splits.subdivisions }; + + // Если вернулись к 1x1, удаляем запись для чистоты + if (newVal === 1 && (type === 'rows' ? current.cols : current.rows) === 1) { + delete newSubdivisions[key]; + } else { + newSubdivisions[key] = { ...current, [type]: newVal }; + } + + onChange({ ...splits, subdivisions: newSubdivisions }); + }; + + // --- Обработчики мыши --- const handleGlobalMouseMove = (e: React.MouseEvent) => { if (!svgRef.current) return; const rect = svgRef.current.getBoundingClientRect(); @@ -72,35 +73,31 @@ export const LayoutStep: React.FC = ({ config, splits, onChange }) => { onChange(newSplits); return; } + + if (isButtonHovered) return; setHoveredSplit(null); - // Phantom Logic (Creation) - const SNAP_THRESHOLD = 0.02; - 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 SNAP = 0.02; + const closeToX = splits.x.some(val => Math.abs(nx - val) < SNAP); + const closeToY = splits.y.some(val => Math.abs(ny - val) < SNAP); + const closeToEdgeX = nx < SNAP || nx > (1 - SNAP); + const closeToEdgeY = ny < SNAP || ny > (1 - SNAP); - 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 (!closeToX && !closeToY && !closeToEdgeX && !closeToEdgeY) { + const distRight = 1 - nx; const distBottom = 1 - ny; + const minXDist = Math.min(nx, distRight); + const minYDist = Math.min(ny, distBottom); + setPhantomAxis(minXDist < minYDist ? 'y' : 'x'); + } else { + setPhantomAxis(null); + } } }; const handleSplitHover = (e: React.MouseEvent, axis: Axis, index: number) => { - if (mode !== 'lines') return; - if (dragging) return; + if (mode !== 'lines' || dragging) return; e.stopPropagation(); setHoveredSplit({ axis, index }); setPhantomAxis(null); @@ -108,10 +105,10 @@ export const LayoutStep: React.FC = ({ config, splits, onChange }) => { const handleMouseDown = (e: React.MouseEvent) => { if (mode === 'lines') { - if (hoveredSplit) { + if (hoveredSplit && !isButtonHovered) { if (e.button === 0) setDragging(hoveredSplit); else if (e.button === 2) removeSplit(hoveredSplit.axis, hoveredSplit.index); - } else if (phantomAxis) { + } else if (phantomAxis && !isButtonHovered) { const val = phantomAxis === 'x' ? mousePos.x : mousePos.y; const newSplits = { ...splits }; newSplits[phantomAxis] = [...newSplits[phantomAxis], val]; @@ -119,16 +116,11 @@ export const LayoutStep: React.FC = ({ config, splits, onChange }) => { setDragging({ axis: phantomAxis, index: newSplits[phantomAxis].length - 1 }); } } else { - // Mode === 'cells' - // Клик обрабатывается в самом rect ячейки, а здесь можно сбрасывать выделение - if (e.target === svgRef.current) { - setSelectedCell(null); - } + // В режиме ячеек сбрасываем выделение при клике в пустоту + if (e.target === svgRef.current) setSelectedCell(null); } }; - const handleMouseUp = () => setDragging(null); - const removeSplit = (axis: Axis, index: number) => { const newSplits = { ...splits }; newSplits[axis] = newSplits[axis].filter((_, i) => i !== index); @@ -140,24 +132,25 @@ export const LayoutStep: React.FC = ({ config, splits, onChange }) => { return (
+ + {/* --- HEADER CONTROLS --- */}

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

- {/* --- TOGGLE MODE --- */}
@@ -165,73 +158,33 @@ export const LayoutStep: React.FC = ({ config, splits, onChange }) => { onClick={() => onChange({ x: [], y: [], subdivisions: {} })} className="px-3 py-1 text-xs bg-slate-800 text-red-400 hover:text-red-300 rounded hover:bg-slate-700 border border-slate-700 flex items-center gap-1 transition-colors" > - Сбросить + Сбросить всё
- {/* Панель настроек выбранной ячейки (Появляется только в режиме Cells) */} - {mode === 'cells' && selectedCell && ( -
-
- Настройка ячейки - -
- -
-
- КОЛОНКИ (X) -
- - {getSubdivision(selectedCell.i, selectedCell.j).cols} - -
-
-
- РЯДЫ (Y) -
- - {getSubdivision(selectedCell.i, selectedCell.j).rows} - -
-
-
-
- )} -
- {/* Instruction Box */} + {/* Instruction Overlay */}
- Инструкция + Режим: {mode === 'lines' ? 'Границы' : 'Ячейки'}
{mode === 'lines' ? ( -
    -
  • Клик у края: Новая линия
  • -
  • Драг: Переместить
  • -
  • ПКМ: Удалить
  • +
      +
    • Клик: Новая линия
    • +
    • Драг: Двигать линию
    • +
    • ПКМ: Удалить линию
    ) : ( -
      +
      • Клик по ячейке: Выбрать
      • -
      • Настрой деление в панели
      • +
      • Используй меню для деления внутри
      )}
+ {/* Rulers */}
0 {config.drawer.width} мм @@ -243,8 +196,9 @@ export const LayoutStep: React.FC = ({ config, splits, onChange }) => { {config.drawer.depth} мм
+ {/* SVG Container */}
= ({ config, splits, onChange }) => { className="w-full h-full touch-none" onMouseMove={handleGlobalMouseMove} onMouseDown={handleMouseDown} - onMouseUp={handleMouseUp} - onMouseLeave={handleMouseUp} + onMouseUp={() => setDragging(null)} onContextMenu={(e) => e.preventDefault()} > @@ -270,34 +223,27 @@ export const LayoutStep: React.FC = ({ config, splits, onChange }) => { - {/* --- CELLS & SUBDIVISIONS --- */} - {/* Рисуем ячейки ПЕРЕД линиями, чтобы ловить клики в режиме Cells */} + {/* --- РЕНДЕРИНГ ЯЧЕЕК --- */} {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 cellX = x1 * viewBoxW; const cellY = y1 * viewBoxH; const cellW = (x2 - x1) * viewBoxW; const cellH = (y2 - y1) * viewBoxH; - + const isSelected = selectedCell?.i === i && selectedCell?.j === j; const subdiv = getSubdivision(i, j); return ( - {/* Интерактивный прямоугольник ячейки */} + {/* Прямоугольник ячейки */} { if (mode === 'cells') { e.stopPropagation(); @@ -306,118 +252,111 @@ export const LayoutStep: React.FC = ({ config, splits, onChange }) => { }} /> - {/* Отрисовка внутренних разделителей (Визуализация) */} + {/* Внутренние пунктирные линии */} {subdiv.cols > 1 && Array.from({ length: subdiv.cols - 1 }).map((_, cI) => { const splitX = cellX + (cellW / subdiv.cols) * (cI + 1); - return ( - - ); + return ; })} {subdiv.rows > 1 && Array.from({ length: subdiv.rows - 1 }).map((_, rI) => { const splitY = cellY + (cellH / subdiv.rows) * (rI + 1); - return ( - - ); + return ; })} - {/* Текст размеров (Скрываем если ячейка разбита или слишком мелкая) */} + {/* РАЗМЕРЫ: показываем только если ячейка не разбита */} {subdiv.rows === 1 && subdiv.cols === 1 && ( - {width.toFixed(0)} × {depth.toFixed(0)} + {((x2 - x1) * config.drawer.width).toFixed(0)}×{((y2 - y1) * config.drawer.depth).toFixed(0)} )} ); }); })} - - {/* --- Main Grid Lines (X) --- */} - {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)} - onMouseMove={(e) => handleSplitHover(e, 'x', i)} - className={mode === 'lines' ? "cursor-col-resize" : ""} - > - - - {mode === 'lines' && (isHovered || isDragging) && ( - { e.stopPropagation(); removeSplit('x', i); }} - onMouseEnter={() => setIsButtonHovered(true)} onMouseLeave={() => setIsButtonHovered(false)} - > - - - - )} - - ); - })} + {/* --- ЛИНИИ СЕТКИ (Поверх ячеек) --- */} + {splits.x.map((x, i) => ( + handleSplitHover(e, 'x', i)}> + + + {mode === 'lines' && hoveredSplit?.axis === 'x' && hoveredSplit.index === i && ( + { e.stopPropagation(); removeSplit('x', i); }} + onMouseEnter={() => setIsButtonHovered(true)} onMouseLeave={() => setIsButtonHovered(false)} + > + + + + )} + + ))} - {/* --- Main Grid Lines (Y) --- */} - {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; + {splits.y.map((y, i) => ( + handleSplitHover(e, 'y', i)}> + + + {mode === 'lines' && hoveredSplit?.axis === 'y' && hoveredSplit.index === i && ( + { e.stopPropagation(); removeSplit('y', i); }} + onMouseEnter={() => setIsButtonHovered(true)} onMouseLeave={() => setIsButtonHovered(false)} + > + + + + )} + + ))} - return ( - removeSplit('y', i)} - onMouseMove={(e) => handleSplitHover(e, 'y', i)} - className={mode === 'lines' ? "cursor-row-resize" : ""} - > - - - {mode === 'lines' && (isHovered || isDragging) && ( - { e.stopPropagation(); removeSplit('y', i); }} - onMouseEnter={() => setIsButtonHovered(true)} onMouseLeave={() => setIsButtonHovered(false)} - > - - - - )} - - ); - })} - - {/* --- Phantom Lines --- */} - {mode === 'lines' && !hoveredSplit && !dragging && phantomAxis === 'x' && ( - - - - )} - {mode === 'lines' && !hoveredSplit && !dragging && phantomAxis === 'y' && ( - - - + {/* Фантомная линия */} + {mode === 'lines' && !hoveredSplit && !dragging && phantomAxis && ( + )} + + {/* --- КОНТРОЛЫ ЯЧЕЙКИ (Поверх SVG) --- */} + {mode === 'cells' && selectedCell && ( +
e.stopPropagation()} // Чтобы клик не снимал выделение + > + {/* Ряды (Горизонтально) */} +
+ + + {getSubdivision(selectedCell.i, selectedCell.j).cols} + +
+ + {/* Колонки (Вертикально) */} +
+ + + {getSubdivision(selectedCell.i, selectedCell.j).rows} + +
+ + +
+ )} +
diff --git a/src/services/geometryGenerator.ts b/src/services/geometryGenerator.ts index 23d5a4a..a792c78 100644 --- a/src/services/geometryGenerator.ts +++ b/src/services/geometryGenerator.ts @@ -16,17 +16,14 @@ export const calculateParts = ( for (let i = 0; i < xPoints.length - 1; i++) { for (let j = 0; j < yPoints.length - 1; j++) { - // Глобальные размеры ячейки сетки const rawX = xPoints[i] * config.drawer.width; const rawY = yPoints[j] * config.drawer.depth; const rawW = (xPoints[i + 1] - xPoints[i]) * config.drawer.width; const rawD = (yPoints[j + 1] - yPoints[j]) * config.drawer.depth; - // Проверяем, есть ли разделение для этой ячейки + // Получаем настройки деления для этой ячейки const subdiv = splits.subdivisions?.[`${i}-${j}`] || { rows: 1, cols: 1 }; - // Вычисляем размер одной "под-ячейки" - // Делим общую ширину на кол-во колонок const subCellWidth = rawW / subdiv.cols; const subCellDepth = rawD / subdiv.rows; @@ -37,7 +34,6 @@ export const calculateParts = ( const subX = rawX + (c * subCellWidth); const subY = rawY + (r * subCellDepth); - // Применяем Tolerance (зазор) к каждой микро-ячейке const realWidth = subCellWidth - config.printerTolerance; const realDepth = subCellDepth - config.printerTolerance; const realX = subX + (config.printerTolerance / 2); @@ -45,9 +41,15 @@ export const calculateParts = ( if (realWidth < 5 || realDepth < 5) continue; + // Формируем имя: если ячейка поделена, добавляем индексы (1-1, 1-2...) + let partName = `Ячейка ${i+1}-${j+1}`; + if (subdiv.rows > 1 || subdiv.cols > 1) { + partName += ` (${r+1}-${c+1})`; + } + parts.push({ id: `part-${partCounter}`, - name: `Ячейка ${i+1}-${j+1}` + (subdiv.rows > 1 || subdiv.cols > 1 ? ` (${r+1}x${c+1})` : ''), + name: partName, width: realWidth, depth: realDepth, height: config.drawer.height, @@ -64,8 +66,7 @@ export const calculateParts = ( return parts; }; -// ... Остальной код (createBinGeometry, exportSTL) остается без изменений ... -// (Копируй функции createRoundedRectShape, createBinGeometry и прочие из предыдущего файла, они не менялись) +// ... Вспомогательные функции генерации геометрии (без изменений) ... const createRoundedRectShape = (width: number, height: number, radius: number): THREE.Shape => { const shape = new THREE.Shape(); const x = -width / 2; @@ -101,9 +102,7 @@ export const createBinGeometry = ( ): THREE.BufferGeometry => { const floorShape = createRoundedRectShape(width, depth, radius); const floorGeo = new THREE.ExtrudeGeometry(floorShape, { - depth: thickness, - bevelEnabled: false, - curveSegments: 16 + depth: thickness, bevelEnabled: false, curveSegments: 16 }); floorGeo.rotateX(-Math.PI / 2); @@ -119,9 +118,7 @@ export const createBinGeometry = ( const wallHeight = height - thickness; const wallGeo = new THREE.ExtrudeGeometry(outerShape, { - depth: wallHeight, - bevelEnabled: false, - curveSegments: 16 + depth: wallHeight, bevelEnabled: false, curveSegments: 16 }); wallGeo.rotateX(-Math.PI / 2); diff --git a/src/types.ts b/src/types.ts index f2a6a34..cc49616 100644 --- a/src/types.ts +++ b/src/types.ts @@ -11,16 +11,16 @@ export interface AppConfig { cornerRadius: number; } -// Конфигурация разделения одной ячейки +// Новая структура: сколько рядов и колонок внутри конкретной ячейки export interface CellSubdivision { - rows: number; // По умолчанию 1 - cols: number; // По умолчанию 1 + rows: number; // горизонтальные ряды + cols: number; // вертикальные колонки } export interface LayoutSplits { x: number[]; y: number[]; - // Ключ: "xIndex-yIndex" (например "0-0" для первой ячейки) + // Ключ - это индекс ячейки "xIndex-yIndex" (например "0-0") subdivisions: Record; }