diff --git a/src/App.tsx b/src/App.tsx index 83631d9..43fe43e 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -37,7 +37,7 @@ const App = () => { window.history.replaceState({}, '', window.location.pathname); } } catch (e) { - console.error("URL load error", e); + console.error("URL error", e); } }, []); @@ -45,7 +45,6 @@ const App = () => { try { return calculateParts(config, splits); } catch (e) { - console.error("Geometry calculation failed", e); return []; } }, [config, splits]); @@ -56,12 +55,10 @@ const App = () => { static getDerivedStateFromError() { return { hasError: true }; } render() { if (this.state.hasError) return ( -
- -

Произошла ошибка отрисовки

- +
+ +

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

+
); return this.props.children; @@ -69,7 +66,7 @@ const App = () => { } return ( -
+
{/* Header */}
@@ -88,26 +85,25 @@ const App = () => {
- {/* Main Content */} -
+ {/* Main Content: flex-1 ensures it fills space, overflow-hidden prevents body scroll */} +
{step === 1 && ( -
+
)} {step === 2 && ( - // ВЕРНУЛИ ЯВНУЮ ВЫСОТУ: h-[calc(100vh-180px)] -
+
- + {/* УБРАН KEY, чтобы компонент не пересоздавался при каждом чихе */} +
)} {step === 3 && ( - // ТАКЖЕ ДЛЯ ШАГА 3 -
+
)} diff --git a/src/components/LayoutStep.tsx b/src/components/LayoutStep.tsx index 8fe7b60..aaeba0f 100644 --- a/src/components/LayoutStep.tsx +++ b/src/components/LayoutStep.tsx @@ -18,25 +18,23 @@ type DragTarget = export const LayoutStep: React.FC = ({ config, splits, onChange }) => { const svgRef = useRef(null); - // --- STATE --- + // -- STATE -- const [mode, setMode] = useState('lines'); const [mousePos, setMousePos] = useState({ x: 0, y: 0 }); - const [isButtonHovered, setIsButtonHovered] = useState(false); const [dragging, setDragging] = useState(null); + const [isButtonHovered, setIsButtonHovered] = useState(false); - // Main Grid Interactions + // Main Grid const [phantomMainAxis, setPhantomMainAxis] = useState(null); const [hoveredMainSplit, setHoveredMainSplit] = useState<{ axis: Axis; index: number } | null>(null); - // Partition Interactions + // Partitions const [hoveredCell, setHoveredCell] = useState<{ i: number; j: number } | null>(null); const [hoveredPartition, setHoveredPartition] = useState<{ id: string; cellKey: string } | null>(null); - // phantomPartition теперь хранит min/max для T-соединений const [phantomPartition, setPhantomPartition] = useState<{ axis: Axis; offset: number; min: number; max: number } | null>(null); - const [selectedPartitionId, setSelectedPartitionId] = useState(null); - // --- DATA --- + // -- SAFE DATA -- const safeX = Array.isArray(splits?.x) ? splits.x : []; const safeY = Array.isArray(splits?.y) ? splits.y : []; const safePartitions = splits?.partitions || {}; @@ -51,8 +49,8 @@ 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]); - // --- HELPER: Find Selected Partition --- - const getSelectedPartitionData = () => { + // -- HELPER: Get Selected -- + const getSelectedPartition = () => { if (!selectedPartitionId) return null; for (const key in safePartitions) { const part = safePartitions[key].find(p => p.id === selectedPartitionId); @@ -60,35 +58,44 @@ export const LayoutStep: React.FC = ({ config, splits, onChange }) => { } return null; }; - const selectedData = getSelectedPartitionData(); + const selectedData = getSelectedPartition(); - // --- LOGIC: Calculate Boundaries for T-Junctions --- - const getHoveredBoundaries = (lx: number, ly: number, parts: Partition[]) => { - let minX = 0, maxX = 1; - let minY = 0, maxY = 1; + // -- LOGIC: Calculate T-Junction Limits -- + // Вычисляет min/max для линии, чтобы она упиралась в ближайшие соседи + const calculateLimits = (lx: number, ly: number, axis: Axis, parts: Partition[]) => { + let min = 0; + let max = 1; + // Ищем ближайшие перпендикулярные стенки parts.forEach(p => { + if (p.axis === axis) return; // Нас интересуют только перпендикулярные + const pMin = p.min ?? 0; const pMax = p.max ?? 1; - if (p.axis === 'x') { - // Вертикальная стенка. Если мышь в ее диапазоне по 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); + 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); // Стенка снизу } } else { - // Горизонтальная стенка. Если мышь в ее диапазоне по 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); + // Мы рисуем Горизонтальную (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); // Стенка справа } } }); - return { minX, maxX, minY, maxY }; + return { min, max }; }; - // --- ACTIONS --- + // -- ACTIONS -- const createPartition = (i: number, j: number, axis: Axis, offset: number, min: number, max: number) => { const key = `${i}-${j}`; const current = safePartitions[key] || []; @@ -103,7 +110,6 @@ export const LayoutStep: React.FC = ({ config, splits, onChange }) => { }; onChange({ ...splits, partitions: { ...safePartitions, [key]: [...current, newPart] } }); setSelectedPartitionId(newPart.id); - // ВАЖНО: Не меняем setMode, остаемся в 'cells' }; const updatePartition = (key: string, id: string, updates: Partial) => { @@ -127,7 +133,7 @@ export const LayoutStep: React.FC = ({ config, splits, onChange }) => { setDragging(null); }; - // --- MOUSE HANDLERS --- + // -- MOUSE HANDLER -- const handleMouseMove = (e: React.MouseEvent) => { if (!svgRef.current) return; const rect = svgRef.current.getBoundingClientRect(); @@ -144,7 +150,6 @@ export const LayoutStep: React.FC = ({ config, splits, onChange }) => { newSplits[dragging.axis][dragging.index] = val; onChange(newSplits); } else { - // Dragging Partition const [iStr, jStr] = dragging.cellKey.split('-'); const i = parseInt(iStr); const j = parseInt(jStr); const cellX1 = sortedX[i]; const cellX2 = sortedX[i+1]; @@ -156,7 +161,8 @@ export const LayoutStep: React.FC = ({ config, splits, onChange }) => { } else { newOffset = (ny - cellY1) / (cellY2 - cellY1); } - newOffset = Math.max(0.02, Math.min(0.98, newOffset)); + // Ограничитель, чтобы не вытащить за границы + newOffset = Math.max(0.01, Math.min(0.99, newOffset)); if (!isNaN(newOffset)) { updatePartition(dragging.cellKey, dragging.id, { offset: newOffset }); } @@ -175,9 +181,7 @@ export const LayoutStep: React.FC = ({ config, splits, onChange }) => { if (!closeToX && !closeToY) { const distRight = 1 - nx; const distBottom = 1 - ny; setPhantomMainAxis(Math.min(nx, distRight) < Math.min(ny, distBottom) ? 'y' : 'x'); - } else { - setPhantomMainAxis(null); - } + } else { setPhantomMainAxis(null); } } } else { // Cells Mode @@ -206,10 +210,12 @@ 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 Existing Partitions + // 2. Check Hover Existing let found = null; const SNAP = 0.05; for (const p of parts) { @@ -224,23 +230,20 @@ export const LayoutStep: React.FC = ({ config, splits, onChange }) => { if (found) { setHoveredPartition({ id: found.id, cellKey: key }); } else { - // 3. Calc Phantom - 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; - + // 3. Calc Phantom with T-Junctions + const distL = lx; const distR = 1 - lx; + const distT = ly; const distB = 1 - ly; const minX = Math.min(distL, distR); const minY = Math.min(distT, distB); const axis = minX < minY ? 'y' : 'x'; - const width = bounds.maxX - bounds.minX; - const height = bounds.maxY - bounds.minY; - // Проверяем, достаточно ли места - if ((axis === 'y' && height > 0.1) || (axis === 'x' && width > 0.1)) { + // Рассчитываем min/max на основе пересечений + const { min, max } = calculateLimits(lx, ly, axis, parts); + + // Проверяем, есть ли место + if (max - min > 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 }); } } @@ -263,7 +266,6 @@ export const LayoutStep: React.FC = ({ config, splits, onChange }) => { setDragging({ type: 'main', axis: phantomMainAxis, index: newSplits[phantomMainAxis].length - 1 }); } } else { - // MODE: CELLS if (hoveredPartition) { const key = hoveredPartition.cellKey; const parts = safePartitions[key] || []; @@ -271,7 +273,7 @@ export const LayoutStep: React.FC = ({ config, splits, onChange }) => { if (part) { if (e.button === 0) { setDragging({ type: 'partition', cellKey: key, id: part.id, axis: part.axis }); - setSelectedPartitionId(part.id); // Выбираем для редактирования + setSelectedPartitionId(part.id); } else if (e.button === 2) { removePartition(key, part.id); } @@ -288,13 +290,12 @@ export const LayoutStep: React.FC = ({ config, splits, onChange }) => { ); } } else { - // Клик в пустоту - снимаем выделение setSelectedPartitionId(null); } } }; - // --- RENDER HELPERS --- + // --- RENDER --- const renderCellsAndPartitions = () => { const elements = []; for (let i = 0; i < sortedX.length - 1; i++) { @@ -302,7 +303,6 @@ export const LayoutStep: React.FC = ({ config, splits, onChange }) => { const x1 = sortedX[i]; const x2 = sortedX[i + 1]; const y1 = sortedY[j]; const y2 = sortedY[j + 1]; - // !!! FIX: Проверка y2 if (y2 === undefined || x2 === undefined) continue; const cellX = x1 * viewBoxW; const cellY = y1 * viewBoxH; @@ -315,14 +315,13 @@ export const LayoutStep: React.FC = ({ config, splits, onChange }) => { elements.push( - {/* Cell Highlight */} {isHovered && } {isAnySelected && mode === 'cells' && } - {/* Partitions */} {parts.map(p => { const pMin = p.min ?? 0; const pMax = p.max ?? 1; let lx1, ly1, lx2, ly2; + if (p.axis === 'x') { const px = cellX + (cellW * p.offset); lx1 = px; lx2 = px; @@ -337,13 +336,12 @@ export const LayoutStep: React.FC = ({ config, splits, onChange }) => { return ( { e.stopPropagation(); removePartition(key, p.id); }}> - + ); })} - {/* Phantom */} {isHovered && phantomPartition && !hoveredPartition && !dragging && ( {(() => { @@ -372,7 +370,7 @@ export const LayoutStep: React.FC = ({ config, splits, onChange }) => { return (
- {/* TOOLBAR */} + {/* HEADER */}

2. Редактор макета @@ -404,7 +402,7 @@ export const LayoutStep: React.FC = ({ config, splits, onChange }) => {

) : (
- ЛКМ: Стенка + ЛКМ в ячейке: Стенка (T-соединения) Драг: Двигать 2xЛКМ: Удалить
@@ -435,7 +433,6 @@ export const LayoutStep: React.FC = ({ config, splits, onChange }) => { - {/* CELLS & PARTITIONS */} {renderCellsAndPartitions()} {/* MAIN GRID X */} @@ -461,7 +458,7 @@ export const LayoutStep: React.FC = ({ config, splits, onChange }) => {
- {/* --- SIDEBAR FOR EDITING --- */} + {/* --- SIDEBAR --- */} {mode === 'cells' && selectedData && (
@@ -472,7 +469,6 @@ export const LayoutStep: React.FC = ({ config, splits, onChange }) => {
- {/* Height */}
Высота {selectedData.part.height} мм @@ -483,7 +479,6 @@ export const LayoutStep: React.FC = ({ config, splits, onChange }) => { />
- {/* Rounded */}
= ({ config, splits, onChange }) => { />
- {/* Delete */}