From 3f527ab7e4f923f0d866a1641717d2c274a0ac77 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=A5=D0=B0=D0=BB=D0=B8=D0=BC=D0=BE=D0=B2=20=D0=A0=D1=83?= =?UTF-8?q?=D1=81=D1=82=D0=B0=D0=BC?= Date: Sat, 10 Jan 2026 17:16:36 +0300 Subject: [PATCH] 2 fix --- src/App.tsx | 101 +++------ src/components/LayoutStep.tsx | 409 ++++++++++++++++------------------ 2 files changed, 226 insertions(+), 284 deletions(-) diff --git a/src/App.tsx b/src/App.tsx index b31f2b7..7212f18 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -9,9 +9,8 @@ import { ChevronRight, ChevronLeft, Box } from 'lucide-react'; const App = () => { const [step, setStep] = useState(1); - const [isLoadedFromUrl, setIsLoadedFromUrl] = useState(false); - // State + // Инициализация конфига const [config, setConfig] = useState({ drawer: { width: 300, depth: 400, height: 80 }, wallThickness: 1.2, @@ -19,7 +18,7 @@ const App = () => { cornerRadius: 4, }); - // Инициализация с гарантированными пустыми массивами + // Инициализация splits с пустой структурой const [splits, setSplits] = useState({ x: [], y: [], @@ -28,36 +27,29 @@ const App = () => { useEffect(() => { try { - const sharedData = parseShareUrl(); - if (sharedData) { - setConfig(sharedData.config); - setSplits({ - x: Array.isArray(sharedData.splits.x) ? sharedData.splits.x : [], - y: Array.isArray(sharedData.splits.y) ? sharedData.splits.y : [], - partitions: sharedData.splits.partitions || {} - }); - setStep(3); - setIsLoadedFromUrl(true); - window.history.replaceState({}, '', window.location.pathname); - } - } catch (e) { - console.error("Ошибка при загрузке URL:", e); + const sharedData = parseShareUrl(); + if (sharedData) { + setConfig(sharedData.config); + // Жесткое приведение типов, чтобы избежать undefined + setSplits({ + x: Array.isArray(sharedData.splits.x) ? sharedData.splits.x : [], + y: Array.isArray(sharedData.splits.y) ? sharedData.splits.y : [], + partitions: sharedData.splits.partitions || {} + }); + setStep(3); + window.history.replaceState({}, '', window.location.pathname); + } + } catch(e) { + console.error("URL Error", e); } }, []); - // ЗАЩИТА: Оборачиваем расчет геометрии, чтобы не ломать весь UI при ошибке const parts: GeneratedPart[] = useMemo(() => { - try { - return calculateParts(config, splits); - } catch (e) { - console.error("Ошибка расчета деталей:", e); - return []; // Возвращаем пустой массив вместо краша - } + return calculateParts(config, splits); }, [config, splits]); return (
- {/* Header */}
@@ -69,26 +61,18 @@ const App = () => {

Генератор органайзеров

- + {/* Step Indicator */}
{[1, 2, 3].map((num) => ( - -
- - {num} - - - {num === 1 ? 'Настройки' : num === 2 ? 'Макет' : 'Экспорт'} - -
- {num < 3 &&
} - +
+ {num} + {num === 1 ? 'Настройки' : num === 2 ? 'Макет' : 'Экспорт'} +
))}
- {/* Main Content */}
{step === 1 && (
@@ -98,12 +82,8 @@ const App = () => { {step === 2 && (
- {/* Добавляем проверку на существование данных */} - + {/* Передаем key для принудительного пересоздания компонента */} +
)} @@ -114,39 +94,14 @@ const App = () => { )}
- {/* Footer */}
- - -
- {step === 2 && Ячеек: {parts.length}} -
- + + {step < 3 ? ( - + ) : ( - + )}
diff --git a/src/components/LayoutStep.tsx b/src/components/LayoutStep.tsx index 2e8904f..8c396eb 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, Plus, Check } from 'lucide-react'; +// ИСПОЛЬЗУЕМ ТОЛЬКО 4 БАЗОВЫЕ ИКОНКИ, ЧТОБЫ ИСКЛЮЧИТЬ ОШИБКИ +import { Grid, Trash2, X, Plus } from 'lucide-react'; interface Props { config: AppConfig; @@ -15,26 +15,29 @@ type Axis = 'x' | 'y'; export const LayoutStep: React.FC = ({ config, splits, onChange }) => { const svgRef = useRef(null); + // Режим работы const [mode, setMode] = useState('lines'); - // SVG State + // Состояния 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); - - // Editor State + + // Редактируемая ячейка const [editingCell, setEditingCell] = useState<{ i: number, j: number } | null>(null); - // --- ЗАЩИТА ДАННЫХ (ОТ БЕЛОГО ЭКРАНА) --- + // --- ЗАЩИТА ДАННЫХ --- + // Если что-то пришло undefined, подменяем на пустые значения const safeX = Array.isArray(splits?.x) ? splits.x : []; const safeY = Array.isArray(splits?.y) ? splits.y : []; const safePartitions = splits?.partitions || {}; - // Расчет размеров SVG с защитой от деления на ноль - const width = Math.max(1, config.drawer.width || 100); - const depth = Math.max(1, config.drawer.depth || 100); + // Защита от деления на ноль при расчете пропорций + const width = Math.max(1, config.drawer.width || 300); + const depth = Math.max(1, config.drawer.depth || 400); + const viewBoxW = 1000; const aspectRatio = depth / width; const viewBoxH = viewBoxW * aspectRatio; @@ -42,7 +45,7 @@ export const LayoutStep: React.FC = ({ config, splits, onChange }) => { const sortedX = useMemo(() => [0, ...safeX, 1].sort((a, b) => a - b), [safeX]); const sortedY = useMemo(() => [0, ...safeY, 1].sort((a, b) => a - b), [safeY]); - // --- ЛОГИКА ПЕРЕГОРОДОК --- + // --- ЛОГИКА --- const addPartition = (axis: 'x' | 'y') => { if (!editingCell) return; const key = `${editingCell.i}-${editingCell.j}`; @@ -51,14 +54,10 @@ export const LayoutStep: React.FC = ({ config, splits, onChange }) => { id: Math.random().toString(36).substr(2, 9), axis, offset: 0.5, - height: config.drawer.height, + height: config.drawer.height || 80, rounded: false }; - - onChange({ - ...splits, - partitions: { ...safePartitions, [key]: [...current, newPart] } - }); + onChange({ ...splits, partitions: { ...safePartitions, [key]: [...current, newPart] } }); }; const updatePartition = (id: string, updates: Partial) => { @@ -66,28 +65,21 @@ export const LayoutStep: React.FC = ({ config, splits, onChange }) => { const key = `${editingCell.i}-${editingCell.j}`; const current = safePartitions[key] || []; const updated = current.map(p => p.id === id ? { ...p, ...updates } : p); - - onChange({ - ...splits, - partitions: { ...safePartitions, [key]: updated } - }); + onChange({ ...splits, partitions: { ...safePartitions, [key]: updated } }); }; const removePartition = (id: string) => { if (!editingCell) return; const key = `${editingCell.i}-${editingCell.j}`; const current = safePartitions[key] || []; - onChange({ - ...splits, - partitions: { ...safePartitions, [key]: current.filter(p => p.id !== id) } - }); + onChange({ ...splits, partitions: { ...safePartitions, [key]: current.filter(p => p.id !== id) } }); }; - // --- UI HANDLERS --- + // --- MOUSE HANDLERS --- const handleGlobalMouseMove = (e: React.MouseEvent) => { if (!svgRef.current) return; const rect = svgRef.current.getBoundingClientRect(); - if (rect.width === 0 || rect.height === 0) return; // Защита + if (rect.width === 0) return; 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)); @@ -102,20 +94,22 @@ export const LayoutStep: React.FC = ({ config, splits, onChange }) => { onChange(newSplits); return; } - if (isButtonHovered) return; setHoveredSplit(null); // Фантомная линия - const SNAP = 0.02; - const closeToX = safeX.some(val => Math.abs(nx - val) < SNAP); - const closeToY = safeY.some(val => Math.abs(ny - val) < SNAP); - - if (!closeToX && !closeToY && nx > SNAP && nx < 1-SNAP && ny > SNAP && ny < 1-SNAP) { - const distRight = 1 - nx; const distBottom = 1 - ny; - setPhantomAxis(Math.min(nx, distRight) < Math.min(ny, distBottom) ? 'y' : 'x'); - } else { - setPhantomAxis(null); + const SNAP = 0.02; + 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); + + if (!closeToX && !closeToY) { + const distRight = 1 - nx; const distBottom = 1 - ny; + setPhantomAxis(Math.min(nx, distRight) < Math.min(ny, distBottom) ? 'y' : 'x'); + } else { + setPhantomAxis(null); + } } } }; @@ -133,7 +127,6 @@ export const LayoutStep: React.FC = ({ config, splits, onChange }) => { setDragging({ axis: phantomAxis, index: newSplits[phantomAxis].length - 1 }); } } else { - // Режим ячеек: клик обрабатывается на самих rect'ах, здесь только сброс if (e.target === svgRef.current) setEditingCell(null); } }; @@ -153,198 +146,192 @@ export const LayoutStep: React.FC = ({ config, splits, onChange }) => { {/* HEADER */}

- 2. Редактор + 2. Макет

- -
-
- {/* Instruction Overlay */} -
-
- - {mode === 'lines' ? 'Режим: Границы' : 'Режим: Ячейки'} -
-

- {mode === 'lines' ? 'Клик: создать. Драг: двигать. ПКМ: удалить.' : 'Кликни по ячейке, чтобы добавить стенки внутри.'} -

+ {/* DEBUG INFO (Если вдруг снова пусто - увидим это) */} +
+ {width}x{depth} | X:{safeX.length} Y:{safeY.length}
- {/* Canvas Container */} -
- setDragging(null)} onContextMenu={(e) => e.preventDefault()} - > - - - - - - - - {/* Ячейки и перегородки */} - {sortedX.slice(0, -1).map((x1, i) => { - const x2 = sortedX[i + 1]; - return sortedY.slice(0, -1).map((y1, j) => { - const cellX = x1 * viewBoxW; - const cellY = y1 * viewBoxH; - const cellW = (x2 - x1) * viewBoxW; - const cellH = (y2 - y1) * viewBoxH; - const isSelected = editingCell?.i === i && editingCell?.j === j; - const parts = safePartitions[`${i}-${j}`] || []; - - return ( - - {/* Прямоугольник для клика */} - { if (mode === 'cells') { e.stopPropagation(); setEditingCell({ i, j }); } }} - /> - {/* Перегородки */} - {parts.map(p => { - if (p.axis === 'x') { - const px = cellX + (cellW * p.offset); - return ; - } else { - const py = cellY + (cellH * p.offset); - return ; - } - })} - - ); - }); - })} - - {/* Линии сетки (X) */} - {safeX.map((x, i) => ( - { if (mode === 'lines' && !dragging) { e.stopPropagation(); setHoveredSplit({ axis: 'x', index: i }); setPhantomAxis(null); } }}> - - - {mode === 'lines' && hoveredSplit?.axis === 'x' && hoveredSplit.index === i && ( - { e.stopPropagation(); removeMainSplit('x', i); }} onMouseEnter={() => setIsButtonHovered(true)} onMouseLeave={() => setIsButtonHovered(false)}> - - - - )} - - ))} - - {/* Линии сетки (Y) */} - {safeY.map((y, i) => ( - { if (mode === 'lines' && !dragging) { e.stopPropagation(); setHoveredSplit({ axis: 'y', index: i }); setPhantomAxis(null); } }}> - - - {mode === 'lines' && hoveredSplit?.axis === 'y' && hoveredSplit.index === i && ( - { e.stopPropagation(); removeMainSplit('y', i); }} onMouseEnter={() => setIsButtonHovered(true)} onMouseLeave={() => setIsButtonHovered(false)}> - - - - )} - - ))} - - {/* Фантомная линия */} - {mode === 'lines' && !hoveredSplit && !dragging && phantomAxis === 'x' && } - {mode === 'lines' && !hoveredSplit && !dragging && phantomAxis === 'y' && } - + {/* Rulers */} +
+ 0 + {width} мм
+ +
+
+ 0 + {depth} мм +
+ + {/* SVG */} +
+ setDragging(null)} onContextMenu={(e) => e.preventDefault()} + > + + + + + + + + {/* CELLS (Рисуем их первыми, чтобы ловить клики) */} + {sortedX.slice(0, -1).map((x1, i) => { + const x2 = sortedX[i + 1]; + return sortedY.slice(0, -1).map((y1, j) => { + const cellX = x1 * viewBoxW; + const cellY = y1 * viewBoxH; + const cellW = (x2 - x1) * viewBoxW; + const cellH = (y2 - y1) * viewBoxH; + const isSelected = editingCell?.i === i && editingCell?.j === j; + const parts = safePartitions[`${i}-${j}`] || []; + + return ( + + { if (mode === 'cells') { e.stopPropagation(); setEditingCell({ i, j }); } }} + /> + {parts.map(p => { + if (p.axis === 'x') { + const px = cellX + (cellW * p.offset); + return ; + } else { + const py = cellY + (cellH * p.offset); + return ; + } + })} + + ); + }); + })} + + {/* GRID LINES X */} + {safeX.map((x, i) => ( + { if (mode === 'lines' && !dragging) { e.stopPropagation(); setHoveredSplit({ axis: 'x', index: i }); setPhantomAxis(null); } }}> + + + {mode === 'lines' && hoveredSplit?.axis === 'x' && hoveredSplit.index === i && ( + { e.stopPropagation(); removeMainSplit('x', i); }} onMouseEnter={() => setIsButtonHovered(true)} onMouseLeave={() => setIsButtonHovered(false)}> + + + + )} + + ))} + + {/* GRID LINES Y */} + {safeY.map((y, i) => ( + { if (mode === 'lines' && !dragging) { e.stopPropagation(); setHoveredSplit({ axis: 'y', index: i }); setPhantomAxis(null); } }}> + + + {mode === 'lines' && hoveredSplit?.axis === 'y' && hoveredSplit.index === i && ( + { e.stopPropagation(); removeMainSplit('y', i); }} onMouseEnter={() => setIsButtonHovered(true)} onMouseLeave={() => setIsButtonHovered(false)}> + + + + )} + + ))} + + {/* PHANTOM LINES */} + {mode === 'lines' && !hoveredSplit && !dragging && phantomAxis === 'x' && } + {mode === 'lines' && !hoveredSplit && !dragging && phantomAxis === 'y' && } + +
+
+ + {/* --- EDITOR PANEL --- */} + {mode === 'cells' && editingCell && ( +
+
+

Редактор ячейки

+ +
+ +
+ + +
+ +
+ {(safePartitions[`${editingCell.i}-${editingCell.j}`] || []).map((p, idx) => ( +
+
+ + Стенка #{idx+1} ({p.axis === 'x' ? 'Верт' : 'Гориз'}) + + +
+
+
+
+ Позиция {(p.offset * 100).toFixed(0)}% +
+ updatePartition(p.id, { offset: parseFloat(e.target.value) })} + className="w-full h-1 bg-slate-600 rounded-lg appearance-none cursor-pointer accent-purple-500" + /> +
+
+
+ Высота {p.height} мм +
+ updatePartition(p.id, { height: parseFloat(e.target.value) })} + className="w-full h-1 bg-slate-600 rounded-lg appearance-none cursor-pointer accent-blue-500" + /> +
+
+ updatePartition(p.id, { rounded: e.target.checked })} + className="rounded bg-slate-700 border-slate-600 text-purple-500 focus:ring-0" + /> + +
+
+
+ ))} +
+
+ )}
- - {/* --- ПАНЕЛЬ РЕДАКТОРА (Справа) --- */} - {mode === 'cells' && editingCell && ( -
-
-

- Редактор ячейки -

- -
- -
- - -
- -
- {(safePartitions[`${editingCell.i}-${editingCell.j}`] || []).map((p, idx) => ( -
-
- - #{idx+1} {p.axis === 'x' ? 'Вертикальная' : 'Горизонтальная'} - - -
- -
-
-
- Позиция {(p.offset * 100).toFixed(0)}% -
- updatePartition(p.id, { offset: parseFloat(e.target.value) })} - className="w-full h-1 bg-slate-600 rounded-lg appearance-none cursor-pointer accent-purple-500" - /> -
- -
-
- Высота {p.height} мм -
- updatePartition(p.id, { height: parseFloat(e.target.value) })} - className="w-full h-1 bg-slate-600 rounded-lg appearance-none cursor-pointer accent-blue-500" - /> -
- -
- updatePartition(p.id, { rounded: e.target.checked })} - className="rounded bg-slate-700 border-slate-600 text-purple-500 focus:ring-0" - /> - -
-
-
- ))} - {(safePartitions[`${editingCell.i}-${editingCell.j}`] || []).length === 0 && ( -
- Нет перегородок -
- )} -
- - -
- )}
);