diff --git a/src/components/LayoutStep.tsx b/src/components/LayoutStep.tsx index eb3a280..2d2b749 100644 --- a/src/components/LayoutStep.tsx +++ b/src/components/LayoutStep.tsx @@ -1,4 +1,4 @@ -import React, { useRef, useState, useMemo } from 'react'; +import React, { useRef, useState, useMemo, useEffect } from 'react'; import { AppConfig, LayoutSplits, Partition } from '../types'; import { Grid, MousePointer2, Trash2, RotateCcw, X, Move, Settings2 } from 'lucide-react'; @@ -61,11 +61,12 @@ export const LayoutStep: React.FC = ({ config, splits, onChange }) => { }; const selectedData = getSelectedPartition(); - // --- SOLVER --- + // --- SOLVER (Robust) --- 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; @@ -78,7 +79,9 @@ export const LayoutStep: React.FC = ({ config, splits, onChange }) => { const obsMin = limits[obstacle.id].min; const obsMax = limits[obstacle.id].max; - if (target.offset >= obsMin - 0.001 && target.offset <= obsMax + 0.001) { + // Epsilon для надежного пересечения + const EPS = 0.001; + if (target.offset >= obsMin - EPS && target.offset <= obsMax + EPS) { if (obstacle.offset < center) newMin = Math.max(newMin, obstacle.offset); else if (obstacle.offset > center) newMax = Math.min(newMax, obstacle.offset); } @@ -89,23 +92,26 @@ export const LayoutStep: React.FC = ({ config, splits, onChange }) => { return limits; }; - // --- RAYCASTING --- + // --- RAYCASTING (Find empty box) --- const getCursorBox = (lx: number, ly: number, parts: Partition[], limitMap: LimitMap) => { let minX = 0, maxX = 1; let minY = 0, maxY = 1; parts.forEach(p => { const { min: pMin, max: pMax } = limitMap[p.id]; + // Игнорируем схлопнутые стенки if (pMax - pMin < 0.001) return; - const EPS = 0.002; + const EPS = 0.005; // Чуть больше допуск для мыши 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); @@ -115,7 +121,7 @@ export const LayoutStep: React.FC = ({ config, splits, onChange }) => { return { minX, maxX, minY, maxY }; }; - // Поиск соседей для размеров + // --- NEIGHBORS (For dimensions) --- const getNeighborOffsets = (offset: number, crossPos: number, axis: Axis, parts: Partition[], limitMap: LimitMap) => { let min = 0; let max = 1; @@ -214,7 +220,7 @@ export const LayoutStep: React.FC = ({ config, splits, onChange }) => { setPhantomPartition(null); setHoveredCell(null); - // 1. Find Cell + // 1. Find Global Cell let cellIdx = null; for (let i = 0; i < sortedX.length - 1; i++) { if (nx >= sortedX[i] && nx <= sortedX[i+1]) { @@ -238,10 +244,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; - + // Check hover over existing walls let found = null; const SNAP = 0.05; for (const p of parts) { @@ -256,38 +259,41 @@ export const LayoutStep: React.FC = ({ config, splits, onChange }) => { if (found) { setHoveredPartition({ id: found.id, cellKey: key }); } else { - // --- ЛОГИКА ФАНТОМА --- + // --- PHANTOM LOGIC --- const box = getCursorBox(lx, ly, parts, limitMap); - const distL = lx - box.minX; const distR = box.maxX - lx; - const distT = ly - box.minY; const distB = box.maxY - ly; + // Локальные координаты относительно "коробки" (0..1) + const localBoxW = box.maxX - box.minX; + const localBoxH = box.maxY - box.minY; - // Размеры "комнаты" под курсором - const boxW = (box.maxX - box.minX) * realCellW; - const boxH = (box.maxY - box.minY) * realCellH; + const relX = (lx - box.minX) / localBoxW; + const relY = (ly - box.minY) / localBoxH; - // 1. По умолчанию делим длинную сторону (чтобы ячейки стремились к квадрату) - let newAxis: Axis = boxW > boxH ? 'x' : 'y'; + // Определяем ось на основе позиции мыши в "коробке" + // Если мы близко к краям - хотим отрезать кусок (перпендикуляр) + // Если в центре - делим длинную сторону + + const edgeThreshold = 0.2; // 20% от края + let newAxis: Axis; - // 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; + const nearLeftRight = relX < edgeThreshold || relX > (1 - edgeThreshold); + const nearTopBottom = relY < edgeThreshold || relY > (1 - edgeThreshold); - // Если близко к бокам -> ставим вертикальную (X) - if (relL / relW < edgeThreshold || (relW - relL) / relW < edgeThreshold) { - newAxis = 'x'; - } - // Если близко к верху/низу -> ставим горизонтальную (Y) - else if (relT / relH < edgeThreshold || (relH - relT) / relH < edgeThreshold) { - newAxis = 'y'; + if (nearLeftRight && !nearTopBottom) { + newAxis = 'x'; // Рядом с боковой стенкой -> Вертикальная + } else if (nearTopBottom && !nearLeftRight) { + newAxis = 'y'; // Рядом с верхней/нижней -> Горизонтальная + } else { + // В центре или в углу -> делим длинную сторону (учитывая реальные мм) + const realBoxW = localBoxW * (cw * drawerW); + const realBoxH = localBoxH * (ch * drawerD); + newAxis = realBoxW > realBoxH ? 'x' : 'y'; } - // Проверяем, есть ли место (>10% размера) - if ((newAxis === 'y' && relH > 0.1) || (newAxis === 'x' && relW > 0.1)) { + // Финальная проверка: есть ли место? + const valid = (newAxis === 'x' && localBoxW > 0.05) || (newAxis === 'y' && localBoxH > 0.05); + + if (valid) { const offset = newAxis === 'x' ? lx : ly; const min = newAxis === 'x' ? box.minY : box.minX; const max = newAxis === 'x' ? box.maxY : box.maxX; @@ -460,7 +466,6 @@ export const LayoutStep: React.FC = ({ config, splits, onChange }) => { {/* LEFT: WORKSPACE */}
- {/* TOOLBAR */}

Редактор

@@ -470,7 +475,6 @@ export const LayoutStep: React.FC = ({ config, splits, onChange }) => {
- {/* 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()}>