+
)}
{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 */}
-
- {/* --- SIDEBAR --- */}
+ {/* --- SIDEBAR FOR EDITING --- */}
{mode === 'cells' && selectedData && (
1 ? 'auto' : '100%',
height: aspectRatio > 1 ? '100%' : 'auto',
@@ -458,17 +464,18 @@ export const LayoutStep: React.FC = ({ config, splits, onChange }) => {
- Настройки стенки
+ Настройки стенки
+ {/* Height */}
Высота {selectedData.part.height} мм
@@ -479,6 +486,7 @@ export const LayoutStep: React.FC = ({ config, splits, onChange }) => {
/>
+ {/* Rounded */}
= ({ config, splits, onChange }) => {
/>
+ {/* Delete */}