{step === 2 && Ячеек: {parts.length}}
diff --git a/src/components/LayoutStep.tsx b/src/components/LayoutStep.tsx
index 2c75620..d272083 100644
--- a/src/components/LayoutStep.tsx
+++ b/src/components/LayoutStep.tsx
@@ -1,6 +1,6 @@
import React, { useRef, useState, useMemo } from 'react';
import { AppConfig, LayoutSplits, Partition } from '../types';
-import { Grid, MousePointer2, Trash2, RotateCcw, X, Move, Plus } from 'lucide-react';
+import { Grid, MousePointer2, Trash2, RotateCcw, X, Plus } from 'lucide-react';
interface Props {
config: AppConfig;
@@ -18,55 +18,47 @@ type DragTarget =
export const LayoutStep: React.FC
= ({ config, splits, onChange }) => {
const svgRef = useRef(null);
- // -- STATE --
const [mode, setMode] = useState('lines');
- const [mousePos, setMousePos] = useState({ x: 0, y: 0 });
- // Dragging
- const [dragging, setDragging] = useState(null);
+ const [mousePos, setMousePos] = useState({ x: 0, y: 0 });
const [isButtonHovered, setIsButtonHovered] = useState(false);
-
- // Main Grid Hover
+ const [dragging, setDragging] = useState(null);
+
const [phantomMainAxis, setPhantomMainAxis] = useState(null);
const [hoveredMainSplit, setHoveredMainSplit] = useState<{ axis: Axis; index: number } | null>(null);
- // 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 } | null>(null);
-
- // Selection
const [selectedPartitionId, setSelectedPartitionId] = useState(null);
- // -- SAFE DATA --
+ // --- Safe Data ---
const safeX = Array.isArray(splits?.x) ? splits.x : [];
const safeY = Array.isArray(splits?.y) ? splits.y : [];
const safePartitions = splits?.partitions || {};
- // -- CALCULATIONS --
- // Защита от деления на ноль и NaN
- const safeW = Math.max(1, config.drawer.width || 300);
- const safeH = Math.max(1, config.drawer.depth || 400);
- const aspectRatio = safeH / safeW;
-
+ const drawerW = Math.max(1, config.drawer.width || 300);
+ const drawerD = Math.max(1, config.drawer.depth || 400);
+ const aspectRatio = drawerD / drawerW;
+
const viewBoxW = 1000;
const viewBoxH = viewBoxW * aspectRatio;
const sortedX = useMemo(() => [0, ...safeX, 1].sort((a, b) => a - b), [safeX]);
const sortedY = useMemo(() => [0, ...safeY, 1].sort((a, b) => a - b), [safeY]);
- // -- HELPERS --
+ // --- Helpers ---
const getSelectedPartition = () => {
- if (!selectedPartitionId) return null;
- for (const key in safePartitions) {
- const part = safePartitions[key].find(p => p.id === selectedPartitionId);
- if (part) return { key, part };
- }
- return null;
+ if (!selectedPartitionId) return null;
+ for (const key in safePartitions) {
+ const part = safePartitions[key].find(p => p.id === selectedPartitionId);
+ if (part) return { key, part };
+ }
+ return null;
};
const selectedData = getSelectedPartition();
- // -- ACTIONS --
+ // --- Actions ---
const removeMainSplit = (axis: Axis, index: number) => {
const newSplits = { ...splits, x: [...safeX], y: [...safeY] };
newSplits[axis] = newSplits[axis].filter((_, i) => i !== index);
@@ -77,33 +69,30 @@ export const LayoutStep: React.FC = ({ config, splits, onChange }) => {
};
const createPartition = (i: number, j: number, axis: Axis, offset: number) => {
- const key = `${i}-${j}`;
- const current = safePartitions[key] || [];
- const newPart: Partition = {
- id: Math.random().toString(36).substr(2, 9),
- axis,
- offset,
- height: config.drawer.height || 80,
- rounded: false
- };
- onChange({ ...splits, partitions: { ...safePartitions, [key]: [...current, newPart] } });
- setSelectedPartitionId(newPart.id);
+ const key = `${i}-${j}`;
+ const current = safePartitions[key] || [];
+ const newPart: Partition = {
+ id: Math.random().toString(36).substr(2, 9),
+ axis, offset, height: config.drawer.height || 80, rounded: false
+ };
+ onChange({ ...splits, partitions: { ...safePartitions, [key]: [...current, newPart] } });
+ setSelectedPartitionId(newPart.id);
};
const updatePartition = (key: string, id: string, updates: Partial) => {
- const current = safePartitions[key] || [];
- const updated = current.map(p => p.id === id ? { ...p, ...updates } : p);
- onChange({ ...splits, partitions: { ...safePartitions, [key]: updated } });
+ const current = safePartitions[key] || [];
+ const updated = current.map(p => p.id === id ? { ...p, ...updates } : p);
+ onChange({ ...splits, partitions: { ...safePartitions, [key]: updated } });
};
const removePartition = (key: string, id: string) => {
- const current = safePartitions[key] || [];
- onChange({ ...splits, partitions: { ...safePartitions, [key]: current.filter(p => p.id !== id) } });
- if (selectedPartitionId === id) setSelectedPartitionId(null);
- setHoveredPartition(null);
+ const current = safePartitions[key] || [];
+ onChange({ ...splits, partitions: { ...safePartitions, [key]: current.filter(p => p.id !== id) } });
+ if (selectedPartitionId === id) setSelectedPartitionId(null);
+ setHoveredPartition(null);
};
- // -- MOUSE LOGIC --
+ // --- Handlers ---
const handleMouseMove = (e: React.MouseEvent) => {
if (!svgRef.current) return;
const rect = svgRef.current.getBoundingClientRect();
@@ -114,101 +103,94 @@ export const LayoutStep: React.FC = ({ config, splits, onChange }) => {
setMousePos({ x: nx, y: ny });
if (dragging) {
- if (dragging.type === 'main') {
- const newSplits = { ...splits, x: [...safeX], y: [...safeY] };
- const val = dragging.axis === 'x' ? nx : ny;
- newSplits[dragging.axis][dragging.index] = val;
- onChange(newSplits);
- } else {
- const [iStr, jStr] = dragging.cellKey.split('-');
- const i = parseInt(iStr); const j = parseInt(jStr);
- const cellX1 = sortedX[i]; const cellX2 = sortedX[i+1];
- const cellY1 = sortedY[j]; const cellY2 = sortedY[j+1];
-
- let newOffset = 0;
- if (dragging.axis === 'x') {
- newOffset = (nx - cellX1) / (cellX2 - cellX1);
+ if (dragging.type === 'main') {
+ const newSplits = { ...splits, x: [...safeX], y: [...safeY] };
+ const val = dragging.axis === 'x' ? nx : ny;
+ newSplits[dragging.axis][dragging.index] = val;
+ onChange(newSplits);
} else {
- newOffset = (ny - cellY1) / (cellY2 - cellY1);
+ const [iStr, jStr] = dragging.cellKey.split('-');
+ const i = parseInt(iStr); const j = parseInt(jStr);
+ const cellX1 = sortedX[i]; const cellX2 = sortedX[i+1];
+ const cellY1 = sortedY[j]; const cellY2 = sortedY[j+1];
+ let newOffset = 0;
+ if (dragging.axis === 'x') {
+ newOffset = (nx - cellX1) / (cellX2 - cellX1);
+ } else {
+ newOffset = (ny - cellY1) / (cellY2 - cellY1);
+ }
+ newOffset = Math.max(0.05, Math.min(0.95, newOffset));
+ if (!isNaN(newOffset)) {
+ updatePartition(dragging.cellKey, dragging.id, { offset: newOffset });
+ }
}
- newOffset = Math.max(0.05, Math.min(0.95, newOffset));
- if (!isNaN(newOffset)) {
- updatePartition(dragging.cellKey, dragging.id, { offset: newOffset });
- }
- }
- return;
+ return;
}
if (isButtonHovered) return;
if (mode === 'lines') {
- setHoveredMainSplit(null);
- const SNAP = 0.015;
- 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;
- setPhantomMainAxis(Math.min(nx, distRight) < Math.min(ny, distBottom) ? 'y' : 'x');
- } else {
- setPhantomMainAxis(null);
+ setHoveredMainSplit(null);
+ const SNAP = 0.015;
+ 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;
+ setPhantomMainAxis(Math.min(nx, distRight) < Math.min(ny, distBottom) ? 'y' : 'x');
+ } else { setPhantomMainAxis(null); }
}
- }
- } else {
- // Cells Mode
- setHoveredPartition(null);
- setPhantomPartition(null);
- setHoveredCell(null);
+ } else if (mode === 'cells') {
+ setHoveredPartition(null);
+ setPhantomPartition(null);
+ setHoveredCell(null);
- // Find Cell
- let cellIdx = null;
- for (let i = 0; i < sortedX.length - 1; i++) {
- if (nx >= sortedX[i] && nx <= sortedX[i+1]) {
- for (let j = 0; j < sortedY.length - 1; j++) {
- if (ny >= sortedY[j] && ny <= sortedY[j+1]) {
- cellIdx = { i, j };
- break;
+ let cellIdx = null;
+ for(let i=0; i= sortedX[i] && nx <= sortedX[i+1]) {
+ for(let j=0; j= sortedY[j] && ny <= sortedY[j+1]) {
+ cellIdx = { i, j };
+ break;
+ }
+ }
}
- }
- }
- }
-
- if (cellIdx) {
- setHoveredCell(cellIdx);
- const key = `${cellIdx.i}-${cellIdx.j}`;
- const parts = safePartitions[key] || [];
-
- 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;
- const lx = (nx - cx1) / cw;
- const ly = (ny - cy1) / ch;
-
- let found = null;
- const SNAP = 0.05;
- for (const p of parts) {
- if (p.axis === 'x') {
- if (Math.abs(lx - p.offset) < SNAP * (aspectRatio > 1 ? 1 : aspectRatio)) found = p;
- } else {
- if (Math.abs(ly - p.offset) < SNAP * (aspectRatio < 1 ? 1 : 1/aspectRatio)) found = p;
- }
}
- if (found) {
- setHoveredPartition({ id: found.id, cellKey: key });
- } else {
- // Phantom logic
- 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';
-
- if (lx > 0.05 && lx < 0.95 && ly > 0.05 && ly < 0.95) {
- setPhantomPartition({ axis, offset: axis === 'x' ? lx : ly });
- }
+ if (cellIdx) {
+ setHoveredCell(cellIdx);
+ const key = `${cellIdx.i}-${cellIdx.j}`;
+ const parts = safePartitions[key] || [];
+
+ 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;
+ const lx = (nx - cx1) / cw;
+ const ly = (ny - cy1) / ch;
+
+ let found = null;
+ const SNAP = 0.05;
+ for (const p of parts) {
+ if (p.axis === 'x') {
+ if (Math.abs(lx - p.offset) < SNAP * (aspectRatio > 1 ? 1 : aspectRatio)) found = p;
+ } else {
+ if (Math.abs(ly - p.offset) < SNAP * (aspectRatio < 1 ? 1 : 1/aspectRatio)) found = p;
+ }
+ }
+
+ if (found) {
+ setHoveredPartition({ id: found.id, cellKey: key });
+ } else {
+ const distLeft = lx; const distRight = 1 - lx;
+ const distTop = ly; const distBottom = 1 - ly;
+ const minX = Math.min(distLeft, distRight);
+ const minY = Math.min(distTop, distBottom);
+ const axis = minX < minY ? 'y' : 'x';
+ if (lx > 0.05 && lx < 0.95 && ly > 0.05 && ly < 0.95) {
+ setPhantomPartition({ axis, offset: axis === 'x' ? lx : ly });
+ }
+ }
}
- }
}
};
@@ -216,41 +198,40 @@ export const LayoutStep: React.FC = ({ config, splits, onChange }) => {
if (isButtonHovered) return;
if (mode === 'lines') {
- if (hoveredMainSplit) {
- if (e.button === 0) setDragging({ type: 'main', ...hoveredMainSplit });
- else if (e.button === 2) removeMainSplit(hoveredMainSplit.axis, hoveredMainSplit.index);
- } else if (phantomMainAxis) {
- const val = phantomMainAxis === 'x' ? mousePos.x : mousePos.y;
- const newSplits = { ...splits, x: [...safeX], y: [...safeY] };
- newSplits[phantomMainAxis] = [...newSplits[phantomMainAxis], val];
- onChange(newSplits);
- setDragging({ type: 'main', axis: phantomMainAxis, index: newSplits[phantomMainAxis].length - 1 });
- }
- } else {
- if (hoveredPartition) {
- const key = hoveredPartition.cellKey;
- const parts = safePartitions[key] || [];
- const part = parts.find(p => p.id === hoveredPartition.id);
- if (part) {
- if (e.button === 0) {
- setDragging({ type: 'partition', cellKey: key, id: part.id, axis: part.axis });
- setSelectedPartitionId(part.id);
- } else if (e.button === 2) {
- removePartition(key, part.id);
- }
+ if (hoveredMainSplit) {
+ if (e.button === 0) setDragging({ type: 'main', ...hoveredMainSplit });
+ else if (e.button === 2) removeMainSplit(hoveredMainSplit.axis, hoveredMainSplit.index);
+ } else if (phantomMainAxis) {
+ const val = phantomMainAxis === 'x' ? mousePos.x : mousePos.y;
+ const newSplits = { ...splits, x: [...safeX], y: [...safeY] };
+ newSplits[phantomMainAxis] = [...newSplits[phantomMainAxis], val];
+ onChange(newSplits);
+ setDragging({ type: 'main', axis: phantomMainAxis, index: newSplits[phantomMainAxis].length - 1 });
}
- } else if (hoveredCell && phantomPartition) {
- if (e.button === 0) {
- createPartition(hoveredCell.i, hoveredCell.j, phantomPartition.axis, phantomPartition.offset);
+ } else if (mode === 'cells') {
+ if (hoveredPartition) {
+ const key = hoveredPartition.cellKey;
+ const parts = safePartitions[key] || [];
+ const part = parts.find(p => p.id === hoveredPartition.id);
+ if (part) {
+ if (e.button === 0) {
+ setDragging({ type: 'partition', cellKey: key, id: part.id, axis: part.axis });
+ setSelectedPartitionId(part.id);
+ } else if (e.button === 2) {
+ removePartition(key, part.id);
+ }
+ }
+ } else if (hoveredCell && phantomPartition) {
+ if (e.button === 0) {
+ createPartition(hoveredCell.i, hoveredCell.j, phantomPartition.axis, phantomPartition.offset);
+ }
+ } else {
+ setSelectedPartitionId(null);
}
- } else {
- setSelectedPartitionId(null);
- }
}
};
- // -- RENDER HELPERS (Для чистоты кода) --
- // Рисуем сетку ячеек отдельно, чтобы не путаться в скобках
+ // --- RENDER HELPERS ---
const renderCells = () => {
const elements = [];
for (let i = 0; i < sortedX.length - 1; i++) {
@@ -260,7 +241,6 @@ export const LayoutStep: React.FC = ({ config, splits, onChange }) => {
const y1 = sortedY[j];
const y2 = sortedY[j + 1];
- // SAFETY CHECK
if (y2 === undefined || x2 === undefined) continue;
const cellX = x1 * viewBoxW;
@@ -275,17 +255,12 @@ export const LayoutStep: React.FC = ({ config, splits, onChange }) => {
elements.push(
- {/* Hover Highlight */}
{isHovered && (
)}
-
- {/* Selection Highlight */}
{isSelected && mode === 'cells' && (
)}
-
- {/* Partitions inside this cell */}
{parts.map(p => {
let lx1, ly1, lx2, ly2;
if (p.axis === 'x') {
@@ -295,7 +270,6 @@ export const LayoutStep: React.FC = ({ config, splits, onChange }) => {
const py = cellY + (cellH * p.offset);
lx1 = cellX; ly1 = py; lx2 = cellX + cellW; ly2 = py;
}
-
const isPSelected = selectedPartitionId === p.id;
const isPHovered = hoveredPartition?.id === p.id;
const stroke = isPSelected ? "#a855f7" : (isPHovered ? "#d8b4fe" : "#7e22ce");
@@ -307,22 +281,12 @@ export const LayoutStep: React.FC = ({ config, splits, onChange }) => {
);
})}
-
- {/* Phantom Partition in this cell */}
{isHovered && phantomPartition && !hoveredPartition && !dragging && (
{phantomPartition.axis === 'x' ? (
-
+
) : (
-
+
)}
)}
@@ -341,18 +305,10 @@ export const LayoutStep: React.FC = ({ config, splits, onChange }) => {
2. Редактор макета
-
-
-
+
+
-
@@ -368,7 +324,7 @@ export const LayoutStep: React.FC = ({ config, splits, onChange }) => {
) : (