diff --git a/src/App.tsx b/src/App.tsx index bec8a84..83631d9 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -50,28 +50,32 @@ const App = () => { } }, [config, splits]); - // Error Boundary component for safety + // 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
Ошибка отрисовки. Нажмите "Новый проект".
; + if (this.state.hasError) return ( +
+ +

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

+ +
+ ); return this.props.children; } } return ( -
-
+
+ {/* Header */} +
-
- -
-
-

PrintFit

-

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

-
+
+

PrintFit

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

{[1, 2, 3].map((num) => ( @@ -84,21 +88,33 @@ const App = () => {
-
- {step === 1 &&
} + {/* Main Content */} +
+ {step === 1 && ( +
+ +
+ )} {step === 2 && ( -
+ // ВЕРНУЛИ ЯВНУЮ ВЫСОТУ: h-[calc(100vh-180px)] +
- +
)} - {step === 3 &&
} + {step === 3 && ( + // ТАКЖЕ ДЛЯ ШАГА 3 +
+ +
+ )}
-
+ {/* Footer */} +
{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 }) => {
) : (
- ЛКМ: Стенка + ЛКМ в ячейке: Стенка Драг: Двигать 2xЛКМ: Удалить
@@ -381,6 +337,7 @@ export const LayoutStep: React.FC = ({ config, splits, onChange }) => {
1 ? 'auto' : '100%', height: aspectRatio > 1 ? '100%' : 'auto', aspectRatio: `${1/aspectRatio}`, @@ -389,16 +346,8 @@ export const LayoutStep: React.FC = ({ config, splits, onChange }) => { : (dragging ? 'grabbing' : hoveredPartition ? 'grab' : hoveredCell ? 'crosshair' : 'default'), }} > - setDragging(null)} - onMouseLeave={() => setDragging(null)} - onContextMenu={(e) => e.preventDefault()} + setDragging(null)} onMouseLeave={() => setDragging(null)} onContextMenu={(e) => e.preventDefault()} > @@ -407,17 +356,13 @@ export const LayoutStep: React.FC = ({ config, splits, onChange }) => { - {/* 1. RENDER CELLS AND PARTITIONS */} {renderCells()} - {/* 2. RENDER MAIN GRID LINES */} + {/* Main Grid Lines */} {safeX.map((x, i) => { const isHovered = hoveredMainSplit?.axis === 'x' && hoveredMainSplit.index === i; return ( - { if(mode === 'lines' && !dragging) { e.stopPropagation(); setHoveredMainSplit({axis: 'x', index: i}); setPhantomMainAxis(null); }}} - onDoubleClick={(e) => { e.stopPropagation(); removeMainSplit('x', i); }} - > + { if(mode === 'lines' && !dragging) { e.stopPropagation(); setHoveredMainSplit({axis: 'x', index: i}); setPhantomMainAxis(null); }}} onDoubleClick={(e) => { e.stopPropagation(); removeMainSplit('x', i); }}> @@ -426,71 +371,41 @@ export const LayoutStep: React.FC = ({ config, splits, onChange }) => { {safeY.map((y, i) => { const isHovered = hoveredMainSplit?.axis === 'y' && hoveredMainSplit.index === i; return ( - { if(mode === 'lines' && !dragging) { e.stopPropagation(); setHoveredMainSplit({axis: 'y', index: i}); setPhantomMainAxis(null); }}} - onDoubleClick={(e) => { e.stopPropagation(); removeMainSplit('y', i); }} - > + { if(mode === 'lines' && !dragging) { e.stopPropagation(); setHoveredMainSplit({axis: 'y', index: i}); setPhantomMainAxis(null); }}} onDoubleClick={(e) => { e.stopPropagation(); removeMainSplit('y', i); }}> ); })} - {/* 3. RENDER MAIN PHANTOMS */} - {mode === 'lines' && !hoveredMainSplit && !dragging && phantomMainAxis === 'x' && ( - - )} - {mode === 'lines' && !hoveredMainSplit && !dragging && phantomMainAxis === 'y' && ( - - )} + {/* Phantoms */} + {mode === 'lines' && !hoveredMainSplit && !dragging && phantomMainAxis === 'x' && } + {mode === 'lines' && !hoveredMainSplit && !dragging && phantomMainAxis === 'y' && }
- {/* --- SIDEBAR --- */} + {/* SIDEBAR */} {mode === 'cells' && selectedData && (
-

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

+

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

-
- {/* Height Control */}
-
- Высота - {selectedData.part.height} мм -
- updatePartition(selectedData.key, selectedData.part.id, { height: parseFloat(e.target.value) })} - className="w-full h-1 bg-slate-600 rounded-lg appearance-none cursor-pointer accent-purple-500" - /> +
Высота {selectedData.part.height} мм
+ updatePartition(selectedData.key, selectedData.part.id, { height: parseFloat(e.target.value) })} className="w-full h-1 bg-slate-600 rounded-lg appearance-none cursor-pointer accent-purple-500"/>
- - {/* Rounded Control */}
- updatePartition(selectedData.key, selectedData.part.id, { rounded: e.target.checked })} - className="w-4 h-4 rounded bg-slate-700 border-slate-600 text-purple-500 focus:ring-0 cursor-pointer" - /> + updatePartition(selectedData.key, selectedData.part.id, { rounded: e.target.checked })} className="w-4 h-4 rounded bg-slate-700 border-slate-600 text-purple-500 focus:ring-0 cursor-pointer"/>
- - {/* Delete Button */} -
- -
- Выделите стенку для настройки.
Двойной клик удаляет её. -
+
Выделите стенку для настройки.
Двойной клик удаляет её.
)}