From 56183b3adaca43bccafafd78178970b0cafec8b0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=A5=D0=B0=D0=BB=D0=B8=D0=BC=D0=BE=D0=B2=20=D0=A0=D1=83?= =?UTF-8?q?=D1=81=D1=82=D0=B0=D0=BC?= Date: Sun, 11 Jan 2026 15:40:07 +0300 Subject: [PATCH] 11 --- src/components/LayoutStep.tsx | 131 +++++++++++++++++----------------- 1 file changed, 65 insertions(+), 66 deletions(-) diff --git a/src/components/LayoutStep.tsx b/src/components/LayoutStep.tsx index 32d9295..eb3a280 100644 --- a/src/components/LayoutStep.tsx +++ b/src/components/LayoutStep.tsx @@ -50,7 +50,7 @@ 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]); - // -- HELPERS -- + // -- HELPER: Get Selected -- const getSelectedPartition = () => { if (!selectedPartitionId) return null; for (const key in safePartitions) { @@ -61,12 +61,11 @@ export const LayoutStep: React.FC = ({ config, splits, onChange }) => { }; const selectedData = getSelectedPartition(); - // --- SOLVER (Пересчет границ) --- + // --- SOLVER --- const solveWallLimits = (parts: Partition[]): LimitMap => { const limits: LimitMap = {}; parts.forEach(p => { limits[p.id] = { min: 0, max: 1 }; }); - // 4 прохода для сложных вложений for (let pass = 0; pass < 4; pass++) { parts.forEach(target => { let newMin = 0; @@ -79,9 +78,7 @@ export const LayoutStep: React.FC = ({ config, splits, onChange }) => { const obsMin = limits[obstacle.id].min; const obsMax = limits[obstacle.id].max; - // Допуск (epsilon) чуть больше, чтобы надежнее ловить пересечения - const EPS = 0.005; - if (target.offset >= obsMin - EPS && target.offset <= obsMax + EPS) { + if (target.offset >= obsMin - 0.001 && target.offset <= obsMax + 0.001) { if (obstacle.offset < center) newMin = Math.max(newMin, obstacle.offset); else if (obstacle.offset > center) newMax = Math.min(newMax, obstacle.offset); } @@ -92,7 +89,7 @@ export const LayoutStep: React.FC = ({ config, splits, onChange }) => { return limits; }; - // --- RAYCASTING (Поиск коробки под курсором) --- + // --- RAYCASTING --- const getCursorBox = (lx: number, ly: number, parts: Partition[], limitMap: LimitMap) => { let minX = 0, maxX = 1; let minY = 0, maxY = 1; @@ -101,17 +98,14 @@ export const LayoutStep: React.FC = ({ config, splits, onChange }) => { const { min: pMin, max: pMax } = limitMap[p.id]; if (pMax - pMin < 0.001) return; - // Используем небольшой допуск, чтобы мышь точно "видела" стенку const EPS = 0.002; if (p.axis === 'x') { - // Вертикальная преграда if (ly >= pMin - EPS && ly <= pMax + EPS) { if (p.offset < lx) minX = Math.max(minX, p.offset); if (p.offset > lx) maxX = Math.min(maxX, p.offset); } } else { - // Горизонтальная преграда if (lx >= pMin - EPS && lx <= pMax + EPS) { if (p.offset < ly) minY = Math.max(minY, p.offset); if (p.offset > ly) maxY = Math.min(maxY, p.offset); @@ -220,7 +214,7 @@ export const LayoutStep: React.FC = ({ config, splits, onChange }) => { setPhantomPartition(null); setHoveredCell(null); - // 1. Находим ячейку + // 1. Find Cell let cellIdx = null; for (let i = 0; i < sortedX.length - 1; i++) { if (nx >= sortedX[i] && nx <= sortedX[i+1]) { @@ -244,7 +238,7 @@ export const LayoutStep: React.FC = ({ config, splits, onChange }) => { const lx = (nx - cx1) / cw; const ly = (ny - cy1) / ch; - // Реальные размеры ячейки в мм (для корректного расчета дистанции мыши) + // Реальные размеры в мм (для интуитивности) const realCellW = cw * drawerW; const realCellH = ch * drawerD; @@ -262,34 +256,38 @@ export const LayoutStep: React.FC = ({ config, splits, onChange }) => { if (found) { setHoveredPartition({ id: found.id, cellKey: key }); } else { - // Вычисляем бокс под курсором + // --- ЛОГИКА ФАНТОМА --- const box = getCursorBox(lx, ly, parts, limitMap); - // --- ВАЖНО: Расчет дистанции с учетом РЕАЛЬНОГО размера ячейки (мм) --- - // Ранее считалось в относительных 0-1, что ломало логику в узких/широких ячейках - const distL = (lx - box.minX) * realCellW; - const distR = (box.maxX - lx) * realCellW; - const distT = (ly - box.minY) * realCellH; - const distB = (box.maxY - ly) * realCellH; + const distL = lx - box.minX; const distR = box.maxX - lx; + const distT = ly - box.minY; const distB = box.maxY - ly; - // Выбираем ось, к которой мы БЛИЖЕ ВИЗУАЛЬНО - const minD = Math.min(distL, distR, distT, distB); - - // Если мы близко к бокам -> ставим вертикальную (x) - // Если близко к верху/низу -> ставим горизонтальную (y) - let newAxis: Axis = (minD === distL || minD === distR) ? 'x' : 'y'; - - // Но если мы в центре, выбираем ось, которая делит длинную сторону + // Размеры "комнаты" под курсором const boxW = (box.maxX - box.minX) * realCellW; const boxH = (box.maxY - box.minY) * realCellH; - - // Если мы не "прилипли" к краю (находимся далеко от всего > 20мм), то делим длинную сторону - if (minD > 20) { - newAxis = boxW > boxH ? 'x' : 'y'; + + // 1. По умолчанию делим длинную сторону (чтобы ячейки стремились к квадрату) + let newAxis: Axis = boxW > boxH ? 'x' : 'y'; + + // 2. Но если мы близко к краю (менее 15% ширины/высоты), + // значит пользователь хочет отрезать узкую полоску параллельно этому краю. + const edgeThreshold = 0.15; // 15% + const relL = lx - box.minX; + const relT = ly - box.minY; + const relW = box.maxX - box.minX; + const relH = box.maxY - box.minY; + + // Если близко к бокам -> ставим вертикальную (X) + if (relL / relW < edgeThreshold || (relW - relL) / relW < edgeThreshold) { + newAxis = 'x'; + } + // Если близко к верху/низу -> ставим горизонтальную (Y) + else if (relT / relH < edgeThreshold || (relH - relT) / relH < edgeThreshold) { + newAxis = 'y'; } - // Финальная проверка, есть ли место для стенки (минимум 10% от размера коробки) - if ((newAxis === 'y' && (box.maxY - box.minY) > 0.1) || (newAxis === 'x' && (box.maxX - box.minX) > 0.1)) { + // Проверяем, есть ли место (>10% размера) + if ((newAxis === 'y' && relH > 0.1) || (newAxis === 'x' && relW > 0.1)) { const offset = newAxis === 'x' ? lx : ly; const min = newAxis === 'x' ? box.minY : box.minX; const max = newAxis === 'x' ? box.maxY : box.maxX; @@ -355,10 +353,8 @@ export const LayoutStep: React.FC = ({ config, splits, onChange }) => { const realW = (x2 - x1) * drawerW; const realD = (y2 - y1) * drawerD; - // ВАЖНО: Используем solver для отрисовки const limitMap = solveWallLimits(parts); - // Label if (parts.length === 0) { const labelX = cellX + cellW / 2; const labelY = cellY + cellH / 2; @@ -460,27 +456,22 @@ export const LayoutStep: React.FC = ({ config, splits, onChange }) => { }; return ( -
-
-

2. Редактор макета

-
- - -
- -
+
+ + {/* LEFT: WORKSPACE */} +
+ {/* TOOLBAR */} +
+

Редактор

+
+ + +
+ +
-
- - {mode === 'lines' ? ( -
ЛКМ: ЛинияПКМ: Удалить
- ) : ( -
ЛКМ в ячейке: СтенкаДраг: Двигать2xЛКМ: Удалить
- )} -
- -
-
+ {/* CANVAS */} +
1 ? 'auto' : '100%', height: aspectRatio > 1 ? '100%' : 'auto', aspectRatio: `${1/aspectRatio}`, maxHeight: '100%', maxWidth: '100%', cursor: mode === 'lines' ? (dragging ? 'grabbing' : hoveredMainSplit ? 'col-resize' : 'crosshair') : (dragging ? 'grabbing' : hoveredPartition ? 'grab' : hoveredCell ? 'crosshair' : 'default') }}> setDragging(null)} onMouseLeave={() => setDragging(null)} onContextMenu={(e) => e.preventDefault()}> @@ -503,19 +494,27 @@ export const LayoutStep: React.FC = ({ config, splits, onChange }) => { )}
-
+
+
- {mode === 'cells' && selectedData && ( -
-

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

-
-
Высота {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"/>
-
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"/>
- -
-
- )} + {/* RIGHT: SIDEBAR */} + {mode === 'cells' && selectedData ? ( +
+

Настройки

+
+
Высота {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"/>
+
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"/>
+ +
+
Выделите стенку для настройки.
Двойной клик удаляет её.
+ ) : ( +
+ +

Выберите стенку для настройки

+

Кликните по любой перегородке внутри ячейки

+
+ )}
); }; \ No newline at end of file