diff --git a/src/App.tsx b/src/App.tsx index e65121c..bdd5826 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -19,9 +19,11 @@ const App = () => { cornerRadius: 4, }); + // ВАЖНО: Инициализируем subdivisions пустым объектом const [splits, setSplits] = useState({ x: [], - y: [] + y: [], + subdivisions: {} }); // --- ЛОГИКА ВОССТАНОВЛЕНИЯ ИЗ ССЫЛКИ --- @@ -29,7 +31,11 @@ const App = () => { const sharedData = parseShareUrl(); if (sharedData) { setConfig(sharedData.config); - setSplits(sharedData.splits); + // При восстановлении тоже гарантируем наличие subdivisions + setSplits({ + ...sharedData.splits, + subdivisions: sharedData.splits.subdivisions || {} + }); setStep(3); setIsLoadedFromUrl(true); window.history.replaceState({}, '', window.location.pathname); @@ -122,7 +128,7 @@ const App = () => { + + + -
+
+ {/* Instruction Overlay */}
- Инструкция + + {mode === 'lines' ? 'Режим: Линии' : 'Режим: Ячейки'}
-
    -
  • Клик у края: Новая линия
  • -
  • Перетаскивание: Изменить размер
  • -
  • Двойной клик/ПКМ: Удалить
  • -
+ {mode === 'lines' ? ( +
    +
  • Клик: Новая линия
  • +
  • Драг: Двигать линию
  • +
  • ПКМ: Удалить линию
  • +
+ ) : ( +
    +
  • Клик по ячейке: Настройка
  • +
  • Добавляй ряды и колонки внутри
  • +
+ )}
+ {/* Rulers */}
0 {config.drawer.width} мм @@ -134,12 +198,12 @@ export const LayoutStep: React.FC = ({ config, splits, onChange }) => {
@@ -159,111 +223,134 @@ export const LayoutStep: React.FC = ({ config, splits, onChange }) => { - {/* --- 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 cellX = x1 * viewBoxW; + const cellY = y1 * viewBoxH; + const cellW = (x2 - x1) * viewBoxW; + const cellH = (y2 - y1) * viewBoxH; - const cellWidthSVG = (x2 - x1) * viewBoxW; - const cellHeightSVG = (y2 - y1) * viewBoxH; - - let fontSize = Math.min(36, cellHeightSVG * 0.6); - fontSize = Math.min(fontSize, cellWidthSVG * 0.25); - - if (fontSize < 10) return null; + const isSelected = selectedCell?.i === i && selectedCell?.j === j; + const subdiv = getSubdivision(i, j); return ( - - {width.toFixed(0)} × {depth.toFixed(0)} - + + {/* Прямоугольник ячейки */} + { + if (mode === 'cells') { + e.stopPropagation(); + setSelectedCell({ i, j }); + } + }} + /> + + {/* Внутренние линии (пунктир) */} + {subdiv.cols > 1 && Array.from({ length: subdiv.cols - 1 }).map((_, cI) => { + const splitX = cellX + (cellW / subdiv.cols) * (cI + 1); + return ; + })} + {subdiv.rows > 1 && Array.from({ length: subdiv.rows - 1 }).map((_, rI) => { + const splitY = cellY + (cellH / subdiv.rows) * (rI + 1); + return ; + })} + + {/* Размеры (если не разбито) */} + {subdiv.rows === 1 && subdiv.cols === 1 && ( + + {((x2 - x1) * config.drawer.width).toFixed(0)}×{((y2 - y1) * config.drawer.depth).toFixed(0)} + + )} + ); }); })} - - {/* --- 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)} - onMouseMove={(e) => handleSplitHover(e, 'x', i)} - > - - - {(isHovered || isDragging) && ( - { e.stopPropagation(); removeSplit('x', i); }} - onMouseEnter={() => setIsButtonHovered(true)} onMouseLeave={() => setIsButtonHovered(false)} - > - - - - )} - - ); - })} + {/* --- Линии границ (X) --- */} + {safeX.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)} + > + + + + )} + + ))} - {/* --- 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; + {/* --- Линии границ (Y) --- */} + {safeY.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)} - > - - - {(isHovered || isDragging) && ( - { e.stopPropagation(); removeSplit('y', i); }} - onMouseEnter={() => setIsButtonHovered(true)} onMouseLeave={() => setIsButtonHovered(false)} - > - - - - )} - - ); - })} - - {/* --- Phantom Lines --- */} - {!hoveredSplit && !dragging && !isButtonHovered && phantomAxis === 'x' && ( - - - - + {/* --- Фантомные линии --- */} + {mode === 'lines' && !hoveredSplit && !dragging && phantomAxis === 'x' && ( + )} - {!hoveredSplit && !dragging && !isButtonHovered && phantomAxis === 'y' && ( - - - - + {mode === 'lines' && !hoveredSplit && !dragging && phantomAxis === 'y' && ( + )} + + {/* --- Панель управления выбранной ячейкой (HTML) --- */} + {mode === 'cells' && selectedCell && ( +
e.stopPropagation()} + > + {/* Ряды */} +
+
X
+ + {getSubdivision(selectedCell.i, selectedCell.j).cols} + +
+ + {/* Колонки */} +
+
Y
+ + {getSubdivision(selectedCell.i, selectedCell.j).rows} + +
+ + +
+ )} +
diff --git a/src/services/geometryGenerator.ts b/src/services/geometryGenerator.ts index af173b0..a4ae8e2 100644 --- a/src/services/geometryGenerator.ts +++ b/src/services/geometryGenerator.ts @@ -8,49 +8,71 @@ export const calculateParts = ( ): GeneratedPart[] => { const parts: GeneratedPart[] = []; - const xPoints = [0, ...[...splits.x].sort((a, b) => a - b), 1]; - const yPoints = [0, ...[...splits.y].sort((a, b) => a - b), 1]; + // Безопасное чтение данных + const safeX = splits?.x || []; + const safeY = splits?.y || []; + const safeSub = splits?.subdivisions || {}; + + const xPoints = [0, ...[...safeX].sort((a, b) => a - b), 1]; + const yPoints = [0, ...[...safeY].sort((a, b) => a - b), 1]; let partCounter = 1; for (let i = 0; i < xPoints.length - 1; i++) { for (let j = 0; j < yPoints.length - 1; j++) { - const segmentX = xPoints[i] * config.drawer.width; - const segmentY = yPoints[j] * config.drawer.depth; - const segmentW = (xPoints[i + 1] - xPoints[i]) * config.drawer.width; - const segmentD = (yPoints[j + 1] - yPoints[j]) * config.drawer.depth; + 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 realWidth = segmentW - config.printerTolerance; - const realDepth = segmentD - config.printerTolerance; - const realX = segmentX + (config.printerTolerance / 2); - const realY = segmentY + (config.printerTolerance / 2); + // Получаем настройки деления + const subdiv = safeSub[`${i}-${j}`] || { rows: 1, cols: 1 }; + + // Размеры под-ячейки + const subCellWidth = rawW / subdiv.cols; + const subCellDepth = rawD / subdiv.rows; - if (realWidth < 5 || realDepth < 5) { - continue; + // Генерируем под-ячейки + for (let r = 0; r < subdiv.rows; r++) { + for (let c = 0; c < subdiv.cols; c++) { + + const subX = rawX + (c * subCellWidth); + const subY = rawY + (r * subCellDepth); + + const realWidth = subCellWidth - config.printerTolerance; + const realDepth = subCellDepth - config.printerTolerance; + const realX = subX + (config.printerTolerance / 2); + const realY = subY + (config.printerTolerance / 2); + + if (realWidth < 5 || realDepth < 5) continue; + + // Имя: если деление, добавляем суффикс + let partName = `Ячейка ${i+1}-${j+1}`; + if (subdiv.rows > 1 || subdiv.cols > 1) { + partName += ` (${r+1}-${c+1})`; + } + + parts.push({ + id: `part-${partCounter}`, + name: partName, + width: realWidth, + depth: realDepth, + height: config.drawer.height, + x: realX, + y: realY, + color: `hsl(${Math.random() * 360}, 70%, 50%)` + }); + partCounter++; + } } - - parts.push({ - id: `part-${partCounter}`, - name: `Ячейка ${i+1}-${j+1}`, - width: realWidth, - depth: realDepth, - height: config.drawer.height, - x: realX, - y: realY, - color: `hsl(${Math.random() * 360}, 70%, 50%)` - }); - partCounter++; } } return parts; }; -// ... Вспомогательные функции (createRoundedRectShape, createBinGeometry) остаются ТЕМИ ЖЕ, -// что и в прошлой работающей версии (со скруглениями и Extrude). -// Они не менялись при внедрении subdivisions, но для целостности я их продублирую. - +// ... Остальной код (createRoundedRectShape, createBinGeometry) как в работающей версии ... const createRoundedRectShape = (width: number, height: number, radius: number): THREE.Shape => { const shape = new THREE.Shape(); const x = -width / 2; @@ -84,12 +106,9 @@ export const createBinGeometry = ( thickness: number, radius: number = 0 ): 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); @@ -105,9 +124,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 1bdb5ff..4008ee5 100644 --- a/src/types.ts +++ b/src/types.ts @@ -11,9 +11,17 @@ export interface AppConfig { cornerRadius: number; } +// Новая структура: настройки деления внутри одной ячейки +export interface CellSubdivision { + rows: number; // горизонтальные ряды + cols: number; // вертикальные колонки +} + export interface LayoutSplits { x: number[]; y: number[]; + // Ключ: "indexX-indexY" (например "0-0"), Значение: {rows: 2, cols: 1} + subdivisions: Record; } export interface GeneratedPart {