diff --git a/src/App.tsx b/src/App.tsx index 43fe43e..e8b8131 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -7,6 +7,24 @@ import { PreviewStep } from './components/PreviewStep'; import { parseShareUrl } from './utils/share'; import { ChevronRight, ChevronLeft, Box, AlertTriangle } from 'lucide-react'; +// ВАЖНО: ErrorBoundary должен быть ЗДЕСЬ, снаружи компонента App +class ErrorBoundary extends React.Component<{children: React.ReactNode}, {hasError: boolean}> { + state = { hasError: false }; + static getDerivedStateFromError() { return { hasError: true }; } + render() { + if (this.state.hasError) return ( +
+ +

Ошибка отрисовки интерфейса.

+ +
+ ); + return this.props.children; + } +} + const App = () => { const [step, setStep] = useState(1); @@ -37,7 +55,7 @@ const App = () => { window.history.replaceState({}, '', window.location.pathname); } } catch (e) { - console.error("URL error", e); + console.error("URL load error", e); } }, []); @@ -49,22 +67,6 @@ const App = () => { } }, [config, splits]); - // Error Boundary - class ErrorBoundary extends React.Component<{children: React.ReactNode}, {hasError: boolean}> { - state = { hasError: false }; - static getDerivedStateFromError() { return { hasError: true }; } - render() { - if (this.state.hasError) return ( -
- -

Ошибка отрисовки.

- -
- ); - return this.props.children; - } - } - return (
{/* Header */} @@ -85,25 +87,24 @@ const App = () => {
- {/* Main Content: flex-1 ensures it fills space, overflow-hidden prevents body scroll */} -
+ {/* Main Content */} +
{step === 1 && ( -
+
)} {step === 2 && ( -
+
- {/* УБРАН KEY, чтобы компонент не пересоздавался при каждом чихе */}
)} {step === 3 && ( -
+
)} diff --git a/src/components/LayoutStep.tsx b/src/components/LayoutStep.tsx index aaeba0f..1095817 100644 --- a/src/components/LayoutStep.tsx +++ b/src/components/LayoutStep.tsx @@ -1,6 +1,7 @@ import React, { useRef, useState, useMemo } from 'react'; import { AppConfig, LayoutSplits, Partition } from '../types'; -import { Grid, MousePointer2, Trash2, RotateCcw, X, Move, Settings2 } from 'lucide-react'; +// Добавил Settings, Move и прочее в импорты +import { Grid, MousePointer2, Trash2, RotateCcw, X, Move, Settings, Plus } from 'lucide-react'; interface Props { config: AppConfig; @@ -24,11 +25,11 @@ export const LayoutStep: React.FC = ({ config, splits, onChange }) => { const [dragging, setDragging] = useState(null); const [isButtonHovered, setIsButtonHovered] = useState(false); - // Main Grid + // Main Grid Hover const [phantomMainAxis, setPhantomMainAxis] = useState(null); const [hoveredMainSplit, setHoveredMainSplit] = useState<{ axis: Axis; index: number } | null>(null); - // Partitions + // Partition Hover 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; min: number; max: number } | null>(null); @@ -60,39 +61,33 @@ export const LayoutStep: React.FC = ({ config, splits, onChange }) => { }; const selectedData = getSelectedPartition(); - // -- LOGIC: Calculate T-Junction Limits -- - // Вычисляет min/max для линии, чтобы она упиралась в ближайшие соседи - const calculateLimits = (lx: number, ly: number, axis: Axis, parts: Partition[]) => { - let min = 0; - let max = 1; + // -- LOGIC: T-Junction Limits -- + // Находит ближайшие стенки, чтобы ограничить новую перегородку + const getHoveredBoundaries = (lx: number, ly: number, parts: Partition[]) => { + let minX = 0, maxX = 1; + let minY = 0, maxY = 1; - // Ищем ближайшие перпендикулярные стенки parts.forEach(p => { - if (p.axis === axis) return; // Нас интересуют только перпендикулярные - const pMin = p.min ?? 0; const pMax = p.max ?? 1; - if (axis === 'x') { - // Мы рисуем Вертикальную (X). Ищем Горизонтальные (Y), которые мы пересекаем по X - // Горизонтальная стенка находится на высоте p.offset. - // Она занимает по ширине от pMin до pMax. - // Попадаем ли мы в ее ширину? - if (lx >= pMin && lx <= pMax) { - if (p.offset < ly) min = Math.max(min, p.offset); // Стенка сверху - if (p.offset > ly) max = Math.min(max, p.offset); // Стенка снизу + if (p.axis === 'x') { + // Вертикальная стенка (x=offset, y=min..max) + // Проверяем, находится ли мышь в её диапазоне по Y + if (ly >= pMin && ly <= pMax) { + if (p.offset < lx) minX = Math.max(minX, p.offset); + if (p.offset > lx) maxX = Math.min(maxX, p.offset); } } else { - // Мы рисуем Горизонтальную (Y). Ищем Вертикальные (X) - // Вертикальная на p.offset - // Занимает высоту pMin..pMax - if (ly >= pMin && ly <= pMax) { - if (p.offset < lx) min = Math.max(min, p.offset); // Стенка слева - if (p.offset > lx) max = Math.min(max, p.offset); // Стенка справа + // Горизонтальная стенка (y=offset, x=min..max) + // Проверяем, находится ли мышь в её диапазоне по X + if (lx >= pMin && lx <= pMax) { + if (p.offset < ly) minY = Math.max(minY, p.offset); + if (p.offset > ly) maxY = Math.min(maxY, p.offset); } } }); - return { min, max }; + return { minX, maxX, minY, maxY }; }; // -- ACTIONS -- @@ -108,6 +103,7 @@ export const LayoutStep: React.FC = ({ config, splits, onChange }) => { height: config.drawer.height || 80, rounded: false }; + // Обновляем стейт, но НЕ сбрасываем mode, так как ErrorBoundary теперь снаружи onChange({ ...splits, partitions: { ...safePartitions, [key]: [...current, newPart] } }); setSelectedPartitionId(newPart.id); }; @@ -161,8 +157,11 @@ export const LayoutStep: React.FC = ({ config, splits, onChange }) => { } else { newOffset = (ny - cellY1) / (cellY2 - cellY1); } - // Ограничитель, чтобы не вытащить за границы - newOffset = Math.max(0.01, Math.min(0.99, newOffset)); + + // Ограничиваем в пределах "родительской" зоны (0-1) внутри ячейки + // В идеале тут тоже надо проверять коллизии, но пока просто границы ячейки + newOffset = Math.max(0.02, Math.min(0.98, newOffset)); + if (!isNaN(newOffset)) { updatePartition(dragging.cellKey, dragging.id, { offset: newOffset }); } @@ -189,7 +188,7 @@ export const LayoutStep: React.FC = ({ config, splits, onChange }) => { setPhantomPartition(null); setHoveredCell(null); - // 1. Find Cell + // 1. Находим ячейку let cellIdx = null; for (let i = 0; i < sortedX.length - 1; i++) { if (nx >= sortedX[i] && nx <= sortedX[i+1]) { @@ -210,12 +209,10 @@ export const LayoutStep: React.FC = ({ config, splits, onChange }) => { const cx1 = sortedX[cellIdx.i]; const cx2 = sortedX[cellIdx.i+1]; const cy1 = sortedY[cellIdx.j]; const cy2 = sortedY[cellIdx.j+1]; const cw = cx2 - cx1; const ch = cy2 - cy1; - - // Local coordinates const lx = (nx - cx1) / cw; const ly = (ny - cy1) / ch; - // 2. Check Hover Existing + // 2. Проверяем наведение на существующие (для удаления/выделения) let found = null; const SNAP = 0.05; for (const p of parts) { @@ -230,20 +227,28 @@ export const LayoutStep: React.FC = ({ config, splits, onChange }) => { if (found) { setHoveredPartition({ id: found.id, cellKey: key }); } else { - // 3. Calc Phantom with T-Junctions - const distL = lx; const distR = 1 - lx; - const distT = ly; const distB = 1 - ly; + // 3. Вычисляем T-образные границы + const bounds = getHoveredBoundaries(lx, ly, parts); + + const distL = lx - bounds.minX; + const distR = bounds.maxX - lx; + const distT = ly - bounds.minY; + const distB = bounds.maxY - ly; + const minX = Math.min(distL, distR); const minY = Math.min(distT, distB); - const axis = minX < minY ? 'y' : 'x'; - - // Рассчитываем min/max на основе пересечений - const { min, max } = calculateLimits(lx, ly, axis, parts); + // Определяем ось перпендикулярно ближайшей стороне + const axis = minX < minY ? 'y' : 'x'; // Проверяем, есть ли место - if (max - min > 0.1) { + const width = bounds.maxX - bounds.minX; + const height = bounds.maxY - bounds.minY; + + if ((axis === 'y' && height > 0.1) || (axis === 'x' && width > 0.1)) { const offset = axis === 'x' ? lx : ly; + const min = axis === 'x' ? bounds.minY : bounds.minX; + const max = axis === 'x' ? bounds.maxY : bounds.maxX; setPhantomPartition({ axis, offset, min, max }); } } @@ -336,7 +341,7 @@ export const LayoutStep: React.FC = ({ config, splits, onChange }) => { return ( { e.stopPropagation(); removePartition(key, p.id); }}> - + ); @@ -370,7 +375,7 @@ export const LayoutStep: React.FC = ({ config, splits, onChange }) => { return (
- {/* HEADER */} + {/* TOOLBAR */}

2. Редактор макета @@ -412,7 +417,8 @@ export const LayoutStep: React.FC = ({ config, splits, onChange }) => { {/* WORKSPACE */}
-
1 ? 'auto' : '100%', height: aspectRatio > 1 ? '100%' : 'auto', @@ -458,17 +464,18 @@ export const LayoutStep: React.FC = ({ config, splits, onChange }) => {
- {/* --- SIDEBAR --- */} + {/* --- SIDEBAR FOR EDITING --- */} {mode === 'cells' && selectedData && (

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

+ {/* Height */}
Высота {selectedData.part.height} мм @@ -479,6 +486,7 @@ export const LayoutStep: React.FC = ({ config, splits, onChange }) => { />
+ {/* Rounded */}
= ({ config, splits, onChange }) => { />
+ {/* Delete */}