From fe1f5df710073aa2b657a6b8e652dfe8d558cbe0 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 22:03:15 +0300 Subject: [PATCH 1/7] Mouse leave --- src/components/LayoutStep.tsx | 116 ++++++++++++++++++++++------------ 1 file changed, 75 insertions(+), 41 deletions(-) diff --git a/src/components/LayoutStep.tsx b/src/components/LayoutStep.tsx index bde04f1..e8a4beb 100644 --- a/src/components/LayoutStep.tsx +++ b/src/components/LayoutStep.tsx @@ -10,6 +10,8 @@ interface Props { type EditMode = 'lines' | 'cells'; type Axis = 'x' | 'y'; +type Limits = { min: number; max: number }; +type LimitMap = Record; type DragTarget = | { type: 'main'; axis: Axis; index: number } @@ -59,29 +61,52 @@ export const LayoutStep: React.FC = ({ config, splits, onChange }) => { }; const selectedData = getSelectedPartition(); - // --- RAYCASTING (Надежный поиск коробки) --- - // Находит ближайшие стенки во всех 4 направлениях - const getCursorBox = (lx: number, ly: number, parts: Partition[]) => { + // --- SOLVER --- + const solveWallLimits = (parts: Partition[]): LimitMap => { + const limits: LimitMap = {}; + parts.forEach(p => { limits[p.id] = { min: 0, max: 1 }; }); + + for (let pass = 0; pass < 4; pass++) { + parts.forEach(target => { + let newMin = 0; + let newMax = 1; + const center = target.offset; + + parts.forEach(obstacle => { + if (target.id === obstacle.id || target.axis === obstacle.axis) return; + + const obsMin = limits[obstacle.id].min; + const obsMax = limits[obstacle.id].max; + + 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); + } + }); + limits[target.id] = { min: newMin, max: newMax }; + }); + } + return limits; + }; + + // --- RAYCASTING --- + const getCursorBox = (lx: number, ly: number, parts: Partition[], limitMap: LimitMap) => { let minX = 0, maxX = 1; let minY = 0, maxY = 1; parts.forEach(p => { - // Используем сохраненные границы (они теперь достоверны) - const pMin = p.min ?? 0; - const pMax = p.max ?? 1; - const EPS = 0.005; // Допуск на попадание + const { min: pMin, max: pMax } = limitMap[p.id]; + if (pMax - pMin < 0.001) return; + + const EPS = 0.002; if (p.axis === 'x') { - // Вертикальная стенка. Перекрывает ли она наш Y? 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 { - // Горизонтальная стенка. Перекрывает ли она наш X? 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); } @@ -90,16 +115,14 @@ export const LayoutStep: React.FC = ({ config, splits, onChange }) => { return { minX, maxX, minY, maxY }; }; - // Поиск соседей для размеров (Та же логика, что Raycasting) - const getNeighborOffsets = (offset: number, crossPos: number, axis: Axis, parts: Partition[]) => { + const getNeighborOffsets = (offset: number, crossPos: number, axis: Axis, parts: Partition[], limitMap: LimitMap) => { let min = 0; let max = 1; - const EPS = 0.005; + const EPS = 0.001; parts.forEach(p => { if (p.axis === axis) { - const pMin = p.min ?? 0; - const pMax = p.max ?? 1; + const { min: pMin, max: pMax } = limitMap[p.id]; if (crossPos >= pMin - EPS && crossPos <= pMax + EPS) { if (p.offset < offset) min = Math.max(min, p.offset); if (p.offset > offset) max = Math.min(max, p.offset); @@ -190,7 +213,6 @@ export const LayoutStep: React.FC = ({ config, splits, onChange }) => { 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]) { @@ -206,6 +228,7 @@ export const LayoutStep: React.FC = ({ config, splits, onChange }) => { setHoveredCell(cellIdx); const key = `${cellIdx.i}-${cellIdx.j}`; const parts = safePartitions[key] || []; + const limitMap = solveWallLimits(parts); const cx1 = sortedX[cellIdx.i]; const cx2 = sortedX[cellIdx.i+1]; const cy1 = sortedY[cellIdx.j]; const cy2 = sortedY[cellIdx.j+1]; @@ -213,41 +236,35 @@ export const LayoutStep: React.FC = ({ config, splits, onChange }) => { const lx = (nx - cx1) / cw; const ly = (ny - cy1) / ch; - // Use real mm for aspect ratio logic const realCellW = cw * drawerW; const realCellH = ch * drawerD; let found = null; const SNAP = 0.05; for (const p of parts) { - const pMin = p.min ?? 0; - const pMax = p.max ?? 1; + const { min, max } = limitMap[p.id]; if (p.axis === 'x') { - if (ly >= pMin && ly <= pMax && Math.abs(lx - p.offset) < SNAP * (aspectRatio > 1 ? 1 : aspectRatio)) found = p; + if (ly >= min && ly <= max && Math.abs(lx - p.offset) < SNAP * (aspectRatio > 1 ? 1 : aspectRatio)) found = p; } else { - if (lx >= pMin && lx <= pMax && Math.abs(ly - p.offset) < SNAP * (aspectRatio < 1 ? 1 : 1/aspectRatio)) found = p; + if (lx >= min && lx <= max && Math.abs(ly - p.offset) < SNAP * (aspectRatio < 1 ? 1 : 1/aspectRatio)) found = p; } } if (found) { setHoveredPartition({ id: found.id, cellKey: key }); } else { - // --- FIND BOX --- - const box = getCursorBox(lx, ly, parts); - + const box = getCursorBox(lx, ly, parts, limitMap); const boxW = (box.maxX - box.minX) * realCellW; const boxH = (box.maxY - box.minY) * realCellH; - // Default axis based on longest side let newAxis: Axis = boxW > boxH ? 'x' : 'y'; - // Override if near edges const relL = (lx - box.minX) / (box.maxX - box.minX); const relT = (ly - box.minY) / (box.maxY - box.minY); const THRESHOLD = 0.2; - if (relL < THRESHOLD || relL > 1 - THRESHOLD) newAxis = 'x'; // Near vertical edge -> vertical wall - else if (relT < THRESHOLD || relT > 1 - THRESHOLD) newAxis = 'y'; // Near horiz edge -> horizontal wall + if (relL < THRESHOLD || relL > 1 - THRESHOLD) newAxis = 'x'; + else if (relT < THRESHOLD || relT > 1 - THRESHOLD) newAxis = 'y'; const valid = (newAxis === 'x' && (box.maxX - box.minX) > 0.05) || (newAxis === 'y' && (box.maxY - box.minY) > 0.05); @@ -298,6 +315,14 @@ export const LayoutStep: React.FC = ({ config, splits, onChange }) => { } }; + // --- MOUSE LEAVE (Clear Phantoms) --- + const handleMouseLeave = () => { + setDragging(null); + setPhantomMainAxis(null); + setPhantomPartition(null); + setHoveredCell(null); + }; + // --- RENDER --- const renderCellsAndPartitions = () => { const elements = []; @@ -317,6 +342,8 @@ export const LayoutStep: React.FC = ({ config, splits, onChange }) => { const realW = (x2 - x1) * drawerW; const realD = (y2 - y1) * drawerD; + const limitMap = solveWallLimits(parts); + if (parts.length === 0) { const labelX = cellX + cellW / 2; const labelY = cellY + cellH / 2; @@ -337,9 +364,8 @@ export const LayoutStep: React.FC = ({ config, splits, onChange }) => { {isAnySelected && mode === 'cells' && } {parts.map(p => { - const pMin = p.min ?? 0; - const pMax = p.max ?? 1; - if (pMax - pMin < 0.001) return null; + const { min, max } = limitMap[p.id]; + if (max - min < 0.001) return null; let lx1, ly1, lx2, ly2; let dist1 = 0, dist2 = 0; @@ -349,20 +375,22 @@ export const LayoutStep: React.FC = ({ config, splits, onChange }) => { if (isVertical) { const px = cellX + (cellW * p.offset); lx1 = px; lx2 = px; - ly1 = cellY + (cellH * pMin); ly2 = cellY + (cellH * pMax); + ly1 = cellY + (cellH * min); ly2 = cellY + (cellH * max); - const neighbors = getNeighborOffsets(p.offset, (pMin + pMax)/2, p.axis, parts); - dist1 = Math.abs((p.offset - neighbors.min) * realW) - wallThick; - dist2 = Math.abs((neighbors.max - p.offset) * realW) - wallThick; + const cy = (min + max) / 2; + const box = getCursorBox(p.offset, cy, parts, limitMap); + dist1 = Math.abs((p.offset - box.minX) * realW) - wallThick; + dist2 = Math.abs((box.maxX - p.offset) * realW) - wallThick; midX = px; midY = (ly1 + ly2) / 2; } else { const py = cellY + (cellH * p.offset); ly1 = py; ly2 = py; - lx1 = cellX + (cellW * pMin); lx2 = cellX + (cellW * pMax); + lx1 = cellX + (cellW * min); lx2 = cellX + (cellW * max); - const neighbors = getNeighborOffsets(p.offset, (pMin + pMax)/2, p.axis, parts); - dist1 = Math.abs((p.offset - neighbors.min) * realD) - wallThick; - dist2 = Math.abs((neighbors.max - p.offset) * realD) - wallThick; + const cx = (min + max) / 2; + const box = getCursorBox(cx, p.offset, parts, limitMap); + dist1 = Math.abs((p.offset - box.minY) * realD) - wallThick; + dist2 = Math.abs((box.maxY - p.offset) * realD) - wallThick; midX = (lx1 + lx2) / 2; midY = py; } @@ -430,7 +458,13 @@ export const LayoutStep: React.FC = ({ config, splits, onChange }) => {
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()}> + setDragging(null)} + onMouseLeave={handleMouseLeave} + onContextMenu={(e) => e.preventDefault()} + > {renderCellsAndPartitions()} From d9bc3bda17b689d28c67b2dbe177c9bf1748ad84 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 22:07:46 +0300 Subject: [PATCH 2/7] Restote --- src/components/LayoutStep.tsx | 68 +++++++++++++++++------------------ 1 file changed, 33 insertions(+), 35 deletions(-) diff --git a/src/components/LayoutStep.tsx b/src/components/LayoutStep.tsx index e8a4beb..692870c 100644 --- a/src/components/LayoutStep.tsx +++ b/src/components/LayoutStep.tsx @@ -61,7 +61,7 @@ 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 }; }); @@ -78,7 +78,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.002; + 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,7 +91,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; @@ -98,14 +100,16 @@ 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; + 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,6 +119,7 @@ export const LayoutStep: React.FC = ({ config, splits, onChange }) => { return { minX, maxX, minY, maxY }; }; + // Поиск соседей для размеров const getNeighborOffsets = (offset: number, crossPos: number, axis: Axis, parts: Partition[], limitMap: LimitMap) => { let min = 0; let max = 1; @@ -236,9 +241,6 @@ 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; - let found = null; const SNAP = 0.05; for (const p of parts) { @@ -253,26 +255,21 @@ export const LayoutStep: React.FC = ({ config, splits, onChange }) => { if (found) { setHoveredPartition({ id: found.id, cellKey: key }); } else { + // --- ОПРЕДЕЛЕНИЕ ФАНТОМА --- const box = getCursorBox(lx, ly, parts, limitMap); - const boxW = (box.maxX - box.minX) * realCellW; - const boxH = (box.maxY - box.minY) * realCellH; - - let newAxis: Axis = boxW > boxH ? 'x' : 'y'; - - const relL = (lx - box.minX) / (box.maxX - box.minX); - const relT = (ly - box.minY) / (box.maxY - box.minY); - const THRESHOLD = 0.2; - - if (relL < THRESHOLD || relL > 1 - THRESHOLD) newAxis = 'x'; - else if (relT < THRESHOLD || relT > 1 - THRESHOLD) newAxis = 'y'; - - const valid = (newAxis === 'x' && (box.maxX - box.minX) > 0.05) || (newAxis === 'y' && (box.maxY - box.minY) > 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; - setPhantomPartition({ axis: newAxis, offset, min, max }); + const width = box.maxX - box.minX; + const height = box.maxY - box.minY; + + // Простейшая логика: делим длинную сторону "комнаты" + // Это работает лучше всего + const axis = width > height ? 'x' : 'y'; + + if ((axis === 'y' && height > 0.05) || (axis === 'x' && width > 0.05)) { + const offset = axis === 'x' ? lx : ly; + const min = axis === 'x' ? box.minY : box.minX; + const max = axis === 'x' ? box.maxY : box.maxX; + setPhantomPartition({ axis, offset, min, max }); } } } @@ -315,12 +312,14 @@ export const LayoutStep: React.FC = ({ config, splits, onChange }) => { } }; - // --- MOUSE LEAVE (Clear Phantoms) --- + // --- НОВОЕ: Очистка при уходе мыши --- const handleMouseLeave = () => { setDragging(null); setPhantomMainAxis(null); - setPhantomPartition(null); + setHoveredMainSplit(null); setHoveredCell(null); + setHoveredPartition(null); + setPhantomPartition(null); }; // --- RENDER --- @@ -344,6 +343,7 @@ export const LayoutStep: React.FC = ({ config, splits, onChange }) => { const limitMap = solveWallLimits(parts); + // Label if (parts.length === 0) { const labelX = cellX + cellW / 2; const labelY = cellY + cellH / 2; @@ -377,20 +377,18 @@ export const LayoutStep: React.FC = ({ config, splits, onChange }) => { lx1 = px; lx2 = px; ly1 = cellY + (cellH * min); ly2 = cellY + (cellH * max); - const cy = (min + max) / 2; - const box = getCursorBox(p.offset, cy, parts, limitMap); - dist1 = Math.abs((p.offset - box.minX) * realW) - wallThick; - dist2 = Math.abs((box.maxX - p.offset) * realW) - wallThick; + const neighbors = getNeighborOffsets(p.offset, (min + max)/2, p.axis, parts, limitMap); + dist1 = Math.abs((p.offset - neighbors.min) * realW) - wallThick; + dist2 = Math.abs((neighbors.max - p.offset) * realW) - wallThick; midX = px; midY = (ly1 + ly2) / 2; } else { const py = cellY + (cellH * p.offset); ly1 = py; ly2 = py; lx1 = cellX + (cellW * min); lx2 = cellX + (cellW * max); - const cx = (min + max) / 2; - const box = getCursorBox(cx, p.offset, parts, limitMap); - dist1 = Math.abs((p.offset - box.minY) * realD) - wallThick; - dist2 = Math.abs((box.maxY - p.offset) * realD) - wallThick; + const neighbors = getNeighborOffsets(p.offset, (min + max)/2, p.axis, parts, limitMap); + dist1 = Math.abs((p.offset - neighbors.min) * realD) - wallThick; + dist2 = Math.abs((neighbors.max - p.offset) * realD) - wallThick; midX = (lx1 + lx2) / 2; midY = py; } From 918d9e0ea761b3087f22b734c285fac110b65dfb 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 22:12:56 +0300 Subject: [PATCH 3/7] Restore --- src/components/LayoutStep.tsx | 142 +++++++++++++--------------------- 1 file changed, 55 insertions(+), 87 deletions(-) diff --git a/src/components/LayoutStep.tsx b/src/components/LayoutStep.tsx index 692870c..bde04f1 100644 --- a/src/components/LayoutStep.tsx +++ b/src/components/LayoutStep.tsx @@ -10,8 +10,6 @@ interface Props { type EditMode = 'lines' | 'cells'; type Axis = 'x' | 'y'; -type Limits = { min: number; max: number }; -type LimitMap = Record; type DragTarget = | { type: 'main'; axis: Axis; index: number } @@ -61,56 +59,29 @@ export const LayoutStep: React.FC = ({ config, splits, onChange }) => { }; const selectedData = getSelectedPartition(); - // --- SOLVER (Тот самый, рабочий) --- - const solveWallLimits = (parts: Partition[]): LimitMap => { - const limits: LimitMap = {}; - parts.forEach(p => { limits[p.id] = { min: 0, max: 1 }; }); - - for (let pass = 0; pass < 4; pass++) { - parts.forEach(target => { - let newMin = 0; - let newMax = 1; - const center = target.offset; - - parts.forEach(obstacle => { - if (target.id === obstacle.id || target.axis === obstacle.axis) return; - - const obsMin = limits[obstacle.id].min; - const obsMax = limits[obstacle.id].max; - - // EPSILON для надежности пересечений - const EPS = 0.002; - 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); - } - }); - limits[target.id] = { min: newMin, max: newMax }; - }); - } - return limits; - }; - - // --- RAYCASTING (Рабочая логика поиска коробки) --- - const getCursorBox = (lx: number, ly: number, parts: Partition[], limitMap: LimitMap) => { + // --- RAYCASTING (Надежный поиск коробки) --- + // Находит ближайшие стенки во всех 4 направлениях + const getCursorBox = (lx: number, ly: number, parts: Partition[]) => { 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.005; // Чувствительность к стенкам + // Используем сохраненные границы (они теперь достоверны) + const pMin = p.min ?? 0; + const pMax = p.max ?? 1; + const EPS = 0.005; // Допуск на попадание if (p.axis === 'x') { - // Вертикальная преграда + // Вертикальная стенка. Перекрывает ли она наш Y? 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 { - // Горизонтальная преграда + // Горизонтальная стенка. Перекрывает ли она наш X? 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); } @@ -119,15 +90,16 @@ export const LayoutStep: React.FC = ({ config, splits, onChange }) => { return { minX, maxX, minY, maxY }; }; - // Поиск соседей для размеров - const getNeighborOffsets = (offset: number, crossPos: number, axis: Axis, parts: Partition[], limitMap: LimitMap) => { + // Поиск соседей для размеров (Та же логика, что Raycasting) + const getNeighborOffsets = (offset: number, crossPos: number, axis: Axis, parts: Partition[]) => { let min = 0; let max = 1; - const EPS = 0.001; + const EPS = 0.005; parts.forEach(p => { if (p.axis === axis) { - const { min: pMin, max: pMax } = limitMap[p.id]; + const pMin = p.min ?? 0; + const pMax = p.max ?? 1; if (crossPos >= pMin - EPS && crossPos <= pMax + EPS) { if (p.offset < offset) min = Math.max(min, p.offset); if (p.offset > offset) max = Math.min(max, p.offset); @@ -218,6 +190,7 @@ export const LayoutStep: React.FC = ({ config, splits, onChange }) => { 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]) { @@ -233,7 +206,6 @@ export const LayoutStep: React.FC = ({ config, splits, onChange }) => { setHoveredCell(cellIdx); const key = `${cellIdx.i}-${cellIdx.j}`; const parts = safePartitions[key] || []; - const limitMap = solveWallLimits(parts); const cx1 = sortedX[cellIdx.i]; const cx2 = sortedX[cellIdx.i+1]; const cy1 = sortedY[cellIdx.j]; const cy2 = sortedY[cellIdx.j+1]; @@ -241,35 +213,49 @@ export const LayoutStep: React.FC = ({ config, splits, onChange }) => { const lx = (nx - cx1) / cw; const ly = (ny - cy1) / ch; + // Use real mm for aspect ratio logic + const realCellW = cw * drawerW; + const realCellH = ch * drawerD; + let found = null; const SNAP = 0.05; for (const p of parts) { - const { min, max } = limitMap[p.id]; + const pMin = p.min ?? 0; + const pMax = p.max ?? 1; if (p.axis === 'x') { - if (ly >= min && ly <= max && Math.abs(lx - p.offset) < SNAP * (aspectRatio > 1 ? 1 : aspectRatio)) found = p; + if (ly >= pMin && ly <= pMax && Math.abs(lx - p.offset) < SNAP * (aspectRatio > 1 ? 1 : aspectRatio)) found = p; } else { - if (lx >= min && lx <= max && Math.abs(ly - p.offset) < SNAP * (aspectRatio < 1 ? 1 : 1/aspectRatio)) found = p; + if (lx >= pMin && lx <= pMax && Math.abs(ly - p.offset) < SNAP * (aspectRatio < 1 ? 1 : 1/aspectRatio)) found = p; } } if (found) { setHoveredPartition({ id: found.id, cellKey: key }); } else { - // --- ОПРЕДЕЛЕНИЕ ФАНТОМА --- - const box = getCursorBox(lx, ly, parts, limitMap); + // --- FIND BOX --- + const box = getCursorBox(lx, ly, parts); - const width = box.maxX - box.minX; - const height = box.maxY - box.minY; - - // Простейшая логика: делим длинную сторону "комнаты" - // Это работает лучше всего - const axis = width > height ? 'x' : 'y'; + const boxW = (box.maxX - box.minX) * realCellW; + const boxH = (box.maxY - box.minY) * realCellH; - if ((axis === 'y' && height > 0.05) || (axis === 'x' && width > 0.05)) { - const offset = axis === 'x' ? lx : ly; - const min = axis === 'x' ? box.minY : box.minX; - const max = axis === 'x' ? box.maxY : box.maxX; - setPhantomPartition({ axis, offset, min, max }); + // Default axis based on longest side + let newAxis: Axis = boxW > boxH ? 'x' : 'y'; + + // Override if near edges + const relL = (lx - box.minX) / (box.maxX - box.minX); + const relT = (ly - box.minY) / (box.maxY - box.minY); + const THRESHOLD = 0.2; + + if (relL < THRESHOLD || relL > 1 - THRESHOLD) newAxis = 'x'; // Near vertical edge -> vertical wall + else if (relT < THRESHOLD || relT > 1 - THRESHOLD) newAxis = 'y'; // Near horiz edge -> horizontal wall + + const valid = (newAxis === 'x' && (box.maxX - box.minX) > 0.05) || (newAxis === 'y' && (box.maxY - box.minY) > 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; + setPhantomPartition({ axis: newAxis, offset, min, max }); } } } @@ -312,16 +298,6 @@ export const LayoutStep: React.FC = ({ config, splits, onChange }) => { } }; - // --- НОВОЕ: Очистка при уходе мыши --- - const handleMouseLeave = () => { - setDragging(null); - setPhantomMainAxis(null); - setHoveredMainSplit(null); - setHoveredCell(null); - setHoveredPartition(null); - setPhantomPartition(null); - }; - // --- RENDER --- const renderCellsAndPartitions = () => { const elements = []; @@ -341,9 +317,6 @@ export const LayoutStep: React.FC = ({ config, splits, onChange }) => { const realW = (x2 - x1) * drawerW; const realD = (y2 - y1) * drawerD; - const limitMap = solveWallLimits(parts); - - // Label if (parts.length === 0) { const labelX = cellX + cellW / 2; const labelY = cellY + cellH / 2; @@ -364,8 +337,9 @@ export const LayoutStep: React.FC = ({ config, splits, onChange }) => { {isAnySelected && mode === 'cells' && } {parts.map(p => { - const { min, max } = limitMap[p.id]; - if (max - min < 0.001) return null; + const pMin = p.min ?? 0; + const pMax = p.max ?? 1; + if (pMax - pMin < 0.001) return null; let lx1, ly1, lx2, ly2; let dist1 = 0, dist2 = 0; @@ -375,18 +349,18 @@ export const LayoutStep: React.FC = ({ config, splits, onChange }) => { if (isVertical) { const px = cellX + (cellW * p.offset); lx1 = px; lx2 = px; - ly1 = cellY + (cellH * min); ly2 = cellY + (cellH * max); + ly1 = cellY + (cellH * pMin); ly2 = cellY + (cellH * pMax); - const neighbors = getNeighborOffsets(p.offset, (min + max)/2, p.axis, parts, limitMap); + const neighbors = getNeighborOffsets(p.offset, (pMin + pMax)/2, p.axis, parts); dist1 = Math.abs((p.offset - neighbors.min) * realW) - wallThick; dist2 = Math.abs((neighbors.max - p.offset) * realW) - wallThick; midX = px; midY = (ly1 + ly2) / 2; } else { const py = cellY + (cellH * p.offset); ly1 = py; ly2 = py; - lx1 = cellX + (cellW * min); lx2 = cellX + (cellW * max); + lx1 = cellX + (cellW * pMin); lx2 = cellX + (cellW * pMax); - const neighbors = getNeighborOffsets(p.offset, (min + max)/2, p.axis, parts, limitMap); + const neighbors = getNeighborOffsets(p.offset, (pMin + pMax)/2, p.axis, parts); dist1 = Math.abs((p.offset - neighbors.min) * realD) - wallThick; dist2 = Math.abs((neighbors.max - p.offset) * realD) - wallThick; midX = (lx1 + lx2) / 2; midY = py; @@ -456,13 +430,7 @@ export const LayoutStep: React.FC = ({ config, splits, onChange }) => {
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={handleMouseLeave} - onContextMenu={(e) => e.preventDefault()} - > + setDragging(null)} onMouseLeave={() => setDragging(null)} onContextMenu={(e) => e.preventDefault()}> {renderCellsAndPartitions()} From ade10e0aaff9d0546a1ab36d9eb3fa02e711abbf 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 22:38:12 +0300 Subject: [PATCH 4/7] 1 --- src/components/LayoutStep.tsx | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/src/components/LayoutStep.tsx b/src/components/LayoutStep.tsx index bde04f1..40e739c 100644 --- a/src/components/LayoutStep.tsx +++ b/src/components/LayoutStep.tsx @@ -298,6 +298,16 @@ export const LayoutStep: React.FC = ({ config, splits, onChange }) => { } }; + // --- ДОБАВЛЕНО: Очистка при выходе мыши --- + const handleMouseLeave = () => { + setDragging(null); + setPhantomMainAxis(null); + setHoveredMainSplit(null); + setHoveredCell(null); + setHoveredPartition(null); + setPhantomPartition(null); + }; + // --- RENDER --- const renderCellsAndPartitions = () => { const elements = []; @@ -430,7 +440,12 @@ export const LayoutStep: React.FC = ({ config, splits, onChange }) => {
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()}> + setDragging(null)} + onMouseLeave={handleMouseLeave} // ДОБАВЛЕН ОБРАБОТЧИК + onContextMenu={(e) => e.preventDefault()}> {renderCellsAndPartitions()} From 0f071c9587535b3845ec4655d51648883d31450d 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 22:53:38 +0300 Subject: [PATCH 5/7] Height limit --- src/services/geometryGenerator.ts | 109 ++++++++++++++++++++++++------ 1 file changed, 89 insertions(+), 20 deletions(-) diff --git a/src/services/geometryGenerator.ts b/src/services/geometryGenerator.ts index 58d7263..05a1e2c 100644 --- a/src/services/geometryGenerator.ts +++ b/src/services/geometryGenerator.ts @@ -2,6 +2,37 @@ import * as THREE from 'three'; import { STLExporter, mergeBufferGeometries } from 'three-stdlib'; import { AppConfig, LayoutSplits, GeneratedPart, Partition } from '../types'; +type Limits = { min: number; max: number }; +type LimitMap = Record; + +// --- SOLVER --- +const solveWallLimits = (partitions: Partition[]): LimitMap => { + const limits: LimitMap = {}; + partitions.forEach(p => { limits[p.id] = { min: 0, max: 1 }; }); + + for (let pass = 0; pass < 3; pass++) { + partitions.forEach(target => { + let newMin = 0; + let newMax = 1; + const center = target.offset; + + partitions.forEach(obstacle => { + if (target.id === obstacle.id || target.axis === obstacle.axis) return; + + const obsMin = limits[obstacle.id].min; + const obsMax = limits[obstacle.id].max; + + if (target.offset > obsMin && target.offset < obsMax) { + if (obstacle.offset < center) newMin = Math.max(newMin, obstacle.offset); + else if (obstacle.offset > center) newMax = Math.min(newMax, obstacle.offset); + } + }); + limits[target.id] = { min: newMin, max: newMax }; + }); + } + return limits; +}; + export const calculateParts = (config: AppConfig, splits: LayoutSplits): GeneratedPart[] => { const parts: GeneratedPart[] = []; const safeX = Array.isArray(splits?.x) ? splits.x : []; @@ -111,11 +142,10 @@ export const createBinGeometry = ( geometries.push(wallGeo); // ВНУТРЕННИЕ ПЕРЕГОРОДКИ - // Мы просто верим сохраненным данным (p.min/p.max). Они должны быть корректны при создании. - partitions.forEach(p => { - const pMin = p.min ?? 0; - const pMax = p.max ?? 1; + const limitMap = solveWallLimits(partitions); + partitions.forEach(p => { + const { min: pMin, max: pMax } = limitMap[p.id]; if (pMax - pMin < 0.01) return; const lengthRatio = pMax - pMin; @@ -141,38 +171,77 @@ export const createBinGeometry = ( partGeo.translate(pX, thickness, pY); geometries.push(partGeo); - // СКРУГЛЕНИЯ + // --- ДОБАВЛЕНИЕ ГАЛТЕЛЕЙ (FILLETS) --- if (p.rounded && radius > 1) { const filletR = Math.min(radius, 5); const filletShape = createConcaveFilletShape(filletR); - const filletExtrude = { depth: p.height, bevelEnabled: false }; - const addFillet = (x: number, y: number, rotY: number) => { - const geo = new THREE.ExtrudeGeometry(filletShape, filletExtrude); + // ФУНКЦИЯ ДЛЯ ПОИСКА ВЫСОТЫ СОСЕДА + const getNeighborHeight = (pos: number) => { + // Если это край ящика (0 или 1), то высота равна высоте ящика + if (pos < 0.001 || pos > 0.999) return height; + + // Ищем внутреннюю стенку в этой точке + const neighbor = partitions.find(n => { + if (n.axis === p.axis) return false; // Должна быть перпендикулярна + + // Соседка должна находиться в точке pos (по своей оси смещения) + if (Math.abs(n.offset - pos) > 0.001) return false; + + // И соседка должна перекрывать нашу стенку (по своей длине) + const nLims = limitMap[n.id]; + return p.offset >= nLims.min && p.offset <= nLims.max; + }); + + // Если нашли соседа - возвращаем его высоту, иначе 0 (не должно случаться при корректном Solver) + return neighbor ? neighbor.height : 0; + }; + + // Вычисляем высоту скругления для начала и конца стенки + // Высота не может быть больше самой стенки (p.height) и больше соседа + const startNeighborH = getNeighborHeight(pMin); + const endNeighborH = getNeighborHeight(pMax); + + const hStart = Math.min(p.height, startNeighborH); + const hEnd = Math.min(p.height, endNeighborH); + + // Функция добавления с конкретной высотой + const addFillet = (x: number, y: number, rotY: number, h: number) => { + if (h <= 1) return; // Если высота слишком мала, не рисуем + const geo = new THREE.ExtrudeGeometry(filletShape, { depth: h, bevelEnabled: false }); geo.rotateX(-Math.PI / 2); geo.rotateY(rotY); geo.translate(x, thickness, y); geometries.push(geo); }; - const h = thickness / 2; + const t = thickness / 2; if (p.axis === 'x') { + // VERTICAL WALL const topY = (-innerDepth / 2) + (innerDepth * pMin); - addFillet(pX - h, topY, Math.PI); - addFillet(pX + h, topY, -Math.PI / 2); - const botY = (-innerDepth / 2) + (innerDepth * pMax); - addFillet(pX - h, botY, Math.PI / 2); - addFillet(pX + h, botY, 0); - } else { - const leftX = (-innerWidth / 2) + (innerWidth * pMin); - addFillet(leftX, pY - h, 0); - addFillet(leftX, pY + h, -Math.PI / 2); + // Top End (pMin) -> hStart + addFillet(pX - t, topY, Math.PI, hStart); + addFillet(pX + t, topY, -Math.PI / 2, hStart); + + // Bottom End (pMax) -> hEnd + addFillet(pX - t, botY, Math.PI / 2, hEnd); + addFillet(pX + t, botY, 0, hEnd); + + } else { + // HORIZONTAL WALL + const leftX = (-innerWidth / 2) + (innerWidth * pMin); const rightX = (-innerWidth / 2) + (innerWidth * pMax); - addFillet(rightX, pY - h, Math.PI / 2); - addFillet(rightX, pY + h, Math.PI); + + // Left End (pMin) -> hStart + addFillet(leftX, pY - t, 0, hStart); + addFillet(leftX, pY + t, -Math.PI / 2, hStart); + + // Right End (pMax) -> hEnd + addFillet(rightX, pY - t, Math.PI / 2, hEnd); + addFillet(rightX, pY + t, Math.PI, hEnd); } } }); From 43a54c99a6f0626508572d1cf7e866e9583f6897 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 23:11:09 +0300 Subject: [PATCH 6/7] 1 --- src/services/geometryGenerator.ts | 64 +++++++++++++------------------ 1 file changed, 27 insertions(+), 37 deletions(-) diff --git a/src/services/geometryGenerator.ts b/src/services/geometryGenerator.ts index 05a1e2c..5403b86 100644 --- a/src/services/geometryGenerator.ts +++ b/src/services/geometryGenerator.ts @@ -5,12 +5,15 @@ import { AppConfig, LayoutSplits, GeneratedPart, Partition } from '../types'; type Limits = { min: number; max: number }; type LimitMap = Record; -// --- SOLVER --- +// --- SOLVER: Идентичен тому, что в LayoutStep.tsx --- +// Гарантирует, что 3D модель будет выглядеть точно так же, как 2D макет const solveWallLimits = (partitions: Partition[]): LimitMap => { const limits: LimitMap = {}; + // 1. Сброс границ partitions.forEach(p => { limits[p.id] = { min: 0, max: 1 }; }); - for (let pass = 0; pass < 3; pass++) { + // 2. Итеративное решение коллизий (4 прохода для надежности) + for (let pass = 0; pass < 4; pass++) { partitions.forEach(target => { let newMin = 0; let newMax = 1; @@ -19,10 +22,15 @@ const solveWallLimits = (partitions: Partition[]): LimitMap => { partitions.forEach(obstacle => { if (target.id === obstacle.id || target.axis === obstacle.axis) return; + // Берем актуальные границы препятствия const obsMin = limits[obstacle.id].min; const obsMax = limits[obstacle.id].max; - if (target.offset > obsMin && target.offset < obsMax) { + // ВАЖНО: Используем тот же допуск (EPSILON), что и в 2D редакторе + const EPS = 0.002; + + // Проверяем пересечение + 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); } @@ -53,6 +61,7 @@ export const calculateParts = (config: AppConfig, splits: LayoutSplits): Generat const rawX = xPoints[i] * config.drawer.width; const rawY = yPoints[j] * config.drawer.depth; + const internalPartitions = safeParts[`${i}-${j}`] || []; const realWidth = rawW - config.printerTolerance; @@ -141,11 +150,13 @@ export const createBinGeometry = ( wallGeo.translate(0, thickness, 0); geometries.push(wallGeo); - // ВНУТРЕННИЕ ПЕРЕГОРОДКИ + // ВНУТРЕННИЕ ПЕРЕГОРОДКИ (С SOLVER'ом) const limitMap = solveWallLimits(partitions); partitions.forEach(p => { + // Используем рассчитанные границы, а не старые сохраненные const { min: pMin, max: pMax } = limitMap[p.id]; + if (pMax - pMin < 0.01) return; const lengthRatio = pMax - pMin; @@ -171,43 +182,29 @@ export const createBinGeometry = ( partGeo.translate(pX, thickness, pY); geometries.push(partGeo); - // --- ДОБАВЛЕНИЕ ГАЛТЕЛЕЙ (FILLETS) --- + // СКРУГЛЕНИЯ if (p.rounded && radius > 1) { const filletR = Math.min(radius, 5); const filletShape = createConcaveFilletShape(filletR); - - // ФУНКЦИЯ ДЛЯ ПОИСКА ВЫСОТЫ СОСЕДА + + // Функция высоты соседа (чтобы скругление не висело в воздухе) const getNeighborHeight = (pos: number) => { - // Если это край ящика (0 или 1), то высота равна высоте ящика - if (pos < 0.001 || pos > 0.999) return height; - - // Ищем внутреннюю стенку в этой точке + if (pos < 0.001 || pos > 0.999) return height; // Край ящика = полная высота + const neighbor = partitions.find(n => { - if (n.axis === p.axis) return false; // Должна быть перпендикулярна - - // Соседка должна находиться в точке pos (по своей оси смещения) - if (Math.abs(n.offset - pos) > 0.001) return false; - - // И соседка должна перекрывать нашу стенку (по своей длине) - const nLims = limitMap[n.id]; - return p.offset >= nLims.min && p.offset <= nLims.max; + if (n.axis === p.axis) return false; + // Проверка на пересечение координат + const nLims = limitMap[n.id]; // Берем актуальные границы соседа + return Math.abs(n.offset - pos) < 0.002 && p.offset >= nLims.min && p.offset <= nLims.max; }); - - // Если нашли соседа - возвращаем его высоту, иначе 0 (не должно случаться при корректном Solver) return neighbor ? neighbor.height : 0; }; - // Вычисляем высоту скругления для начала и конца стенки - // Высота не может быть больше самой стенки (p.height) и больше соседа - const startNeighborH = getNeighborHeight(pMin); - const endNeighborH = getNeighborHeight(pMax); + const hStart = Math.min(p.height, getNeighborHeight(pMin)); + const hEnd = Math.min(p.height, getNeighborHeight(pMax)); - const hStart = Math.min(p.height, startNeighborH); - const hEnd = Math.min(p.height, endNeighborH); - - // Функция добавления с конкретной высотой const addFillet = (x: number, y: number, rotY: number, h: number) => { - if (h <= 1) return; // Если высота слишком мала, не рисуем + if (h <= 1) return; const geo = new THREE.ExtrudeGeometry(filletShape, { depth: h, bevelEnabled: false }); geo.rotateX(-Math.PI / 2); geo.rotateY(rotY); @@ -218,28 +215,21 @@ export const createBinGeometry = ( const t = thickness / 2; if (p.axis === 'x') { - // VERTICAL WALL const topY = (-innerDepth / 2) + (innerDepth * pMin); const botY = (-innerDepth / 2) + (innerDepth * pMax); - // Top End (pMin) -> hStart addFillet(pX - t, topY, Math.PI, hStart); addFillet(pX + t, topY, -Math.PI / 2, hStart); - // Bottom End (pMax) -> hEnd addFillet(pX - t, botY, Math.PI / 2, hEnd); addFillet(pX + t, botY, 0, hEnd); - } else { - // HORIZONTAL WALL const leftX = (-innerWidth / 2) + (innerWidth * pMin); const rightX = (-innerWidth / 2) + (innerWidth * pMax); - // Left End (pMin) -> hStart addFillet(leftX, pY - t, 0, hStart); addFillet(leftX, pY + t, -Math.PI / 2, hStart); - // Right End (pMax) -> hEnd addFillet(rightX, pY - t, Math.PI / 2, hEnd); addFillet(rightX, pY + t, Math.PI, hEnd); } From 96bf976b64163b9f8b1fefd0c4f8e24e9beae2ad 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 23:40:10 +0300 Subject: [PATCH 7/7] 2 --- src/components/LayoutStep.tsx | 26 ++++++----- src/services/geometryGenerator.ts | 74 +++++++------------------------ 2 files changed, 31 insertions(+), 69 deletions(-) diff --git a/src/components/LayoutStep.tsx b/src/components/LayoutStep.tsx index 40e739c..affd96a 100644 --- a/src/components/LayoutStep.tsx +++ b/src/components/LayoutStep.tsx @@ -59,17 +59,17 @@ export const LayoutStep: React.FC = ({ config, splits, onChange }) => { }; const selectedData = getSelectedPartition(); - // --- RAYCASTING (Надежный поиск коробки) --- - // Находит ближайшие стенки во всех 4 направлениях + // --- RAYCASTING (Поиск коробки для НОВОЙ стенки) --- + // Мы ищем ближайшие препятствия, чтобы определить границы новой стенки const getCursorBox = (lx: number, ly: number, parts: Partition[]) => { let minX = 0, maxX = 1; let minY = 0, maxY = 1; parts.forEach(p => { - // Используем сохраненные границы (они теперь достоверны) + // Используем сохраненные границы (честные) const pMin = p.min ?? 0; const pMax = p.max ?? 1; - const EPS = 0.005; // Допуск на попадание + const EPS = 0.005; // Допуск if (p.axis === 'x') { // Вертикальная стенка. Перекрывает ли она наш Y? @@ -90,7 +90,7 @@ export const LayoutStep: React.FC = ({ config, splits, onChange }) => { return { minX, maxX, minY, maxY }; }; - // Поиск соседей для размеров (Та же логика, что Raycasting) + // Поиск соседей для размеров (Используем сохраненные данные) const getNeighborOffsets = (offset: number, crossPos: number, axis: Axis, parts: Partition[]) => { let min = 0; let max = 1; @@ -232,7 +232,7 @@ export const LayoutStep: React.FC = ({ config, splits, onChange }) => { if (found) { setHoveredPartition({ id: found.id, cellKey: key }); } else { - // --- FIND BOX --- + // --- FIND BOX USING RAYCASTING --- const box = getCursorBox(lx, ly, parts); const boxW = (box.maxX - box.minX) * realCellW; @@ -244,11 +244,12 @@ export const LayoutStep: React.FC = ({ config, splits, onChange }) => { // Override if near edges const relL = (lx - box.minX) / (box.maxX - box.minX); const relT = (ly - box.minY) / (box.maxY - box.minY); - const THRESHOLD = 0.2; + const THRESHOLD = 0.2; // 20% zone near edges - if (relL < THRESHOLD || relL > 1 - THRESHOLD) newAxis = 'x'; // Near vertical edge -> vertical wall - else if (relT < THRESHOLD || relT > 1 - THRESHOLD) newAxis = 'y'; // Near horiz edge -> horizontal wall + if (relL < THRESHOLD || relL > 1 - THRESHOLD) newAxis = 'x'; + else if (relT < THRESHOLD || relT > 1 - THRESHOLD) newAxis = 'y'; + // Validate space const valid = (newAxis === 'x' && (box.maxX - box.minX) > 0.05) || (newAxis === 'y' && (box.maxY - box.minY) > 0.05); if (valid) { @@ -298,7 +299,7 @@ export const LayoutStep: React.FC = ({ config, splits, onChange }) => { } }; - // --- ДОБАВЛЕНО: Очистка при выходе мыши --- + // --- HANDLER: Clear Phantom on Leave --- const handleMouseLeave = () => { setDragging(null); setPhantomMainAxis(null); @@ -444,8 +445,9 @@ export const LayoutStep: React.FC = ({ config, splits, onChange }) => { onMouseMove={handleMouseMove} onMouseDown={handleMouseDown} onMouseUp={() => setDragging(null)} - onMouseLeave={handleMouseLeave} // ДОБАВЛЕН ОБРАБОТЧИК - onContextMenu={(e) => e.preventDefault()}> + onMouseLeave={handleMouseLeave} // ДОБАВЛЕНО СЮДА + onContextMenu={(e) => e.preventDefault()} + > {renderCellsAndPartitions()} diff --git a/src/services/geometryGenerator.ts b/src/services/geometryGenerator.ts index 5403b86..8633c7d 100644 --- a/src/services/geometryGenerator.ts +++ b/src/services/geometryGenerator.ts @@ -2,45 +2,6 @@ import * as THREE from 'three'; import { STLExporter, mergeBufferGeometries } from 'three-stdlib'; import { AppConfig, LayoutSplits, GeneratedPart, Partition } from '../types'; -type Limits = { min: number; max: number }; -type LimitMap = Record; - -// --- SOLVER: Идентичен тому, что в LayoutStep.tsx --- -// Гарантирует, что 3D модель будет выглядеть точно так же, как 2D макет -const solveWallLimits = (partitions: Partition[]): LimitMap => { - const limits: LimitMap = {}; - // 1. Сброс границ - partitions.forEach(p => { limits[p.id] = { min: 0, max: 1 }; }); - - // 2. Итеративное решение коллизий (4 прохода для надежности) - for (let pass = 0; pass < 4; pass++) { - partitions.forEach(target => { - let newMin = 0; - let newMax = 1; - const center = target.offset; - - partitions.forEach(obstacle => { - if (target.id === obstacle.id || target.axis === obstacle.axis) return; - - // Берем актуальные границы препятствия - const obsMin = limits[obstacle.id].min; - const obsMax = limits[obstacle.id].max; - - // ВАЖНО: Используем тот же допуск (EPSILON), что и в 2D редакторе - const EPS = 0.002; - - // Проверяем пересечение - 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); - } - }); - limits[target.id] = { min: newMin, max: newMax }; - }); - } - return limits; -}; - export const calculateParts = (config: AppConfig, splits: LayoutSplits): GeneratedPart[] => { const parts: GeneratedPart[] = []; const safeX = Array.isArray(splits?.x) ? splits.x : []; @@ -61,7 +22,6 @@ export const calculateParts = (config: AppConfig, splits: LayoutSplits): Generat const rawX = xPoints[i] * config.drawer.width; const rawY = yPoints[j] * config.drawer.depth; - const internalPartitions = safeParts[`${i}-${j}`] || []; const realWidth = rawW - config.printerTolerance; @@ -150,13 +110,12 @@ export const createBinGeometry = ( wallGeo.translate(0, thickness, 0); geometries.push(wallGeo); - // ВНУТРЕННИЕ ПЕРЕГОРОДКИ (С SOLVER'ом) - const limitMap = solveWallLimits(partitions); - + // ВНУТРЕННИЕ ПЕРЕГОРОДКИ (СТРОГО ПО ДАННЫМ, БЕЗ SOLVER) partitions.forEach(p => { - // Используем рассчитанные границы, а не старые сохраненные - const { min: pMin, max: pMax } = limitMap[p.id]; - + // Берем данные напрямую. Если в 2D нарисовано от 0.2 до 0.8, тут будет 0.2 до 0.8. + const pMin = p.min ?? 0; + const pMax = p.max ?? 1; + if (pMax - pMin < 0.01) return; const lengthRatio = pMax - pMin; @@ -182,20 +141,23 @@ export const createBinGeometry = ( partGeo.translate(pX, thickness, pY); geometries.push(partGeo); - // СКРУГЛЕНИЯ + // СКРУГЛЕНИЯ (Fillets) if (p.rounded && radius > 1) { const filletR = Math.min(radius, 5); const filletShape = createConcaveFilletShape(filletR); - - // Функция высоты соседа (чтобы скругление не висело в воздухе) + + // Функция проверки высоты соседа (простая проверка на пересечение) const getNeighborHeight = (pos: number) => { - if (pos < 0.001 || pos > 0.999) return height; // Край ящика = полная высота + if (pos < 0.001 || pos > 0.999) return height; // Край ящика const neighbor = partitions.find(n => { - if (n.axis === p.axis) return false; - // Проверка на пересечение координат - const nLims = limitMap[n.id]; // Берем актуальные границы соседа - return Math.abs(n.offset - pos) < 0.002 && p.offset >= nLims.min && p.offset <= nLims.max; + if (n.axis === p.axis) return false; // Перпендикуляр + const nMin = n.min ?? 0; + const nMax = n.max ?? 1; + // Совпадает ли позиция? + if (Math.abs(n.offset - pos) > 0.002) return false; + // Перекрывает ли? + return p.offset > nMin && p.offset < nMax; }); return neighbor ? neighbor.height : 0; }; @@ -204,7 +166,7 @@ export const createBinGeometry = ( const hEnd = Math.min(p.height, getNeighborHeight(pMax)); const addFillet = (x: number, y: number, rotY: number, h: number) => { - if (h <= 1) return; + if (h <= 1) return; const geo = new THREE.ExtrudeGeometry(filletShape, { depth: h, bevelEnabled: false }); geo.rotateX(-Math.PI / 2); geo.rotateY(rotY); @@ -220,7 +182,6 @@ export const createBinGeometry = ( addFillet(pX - t, topY, Math.PI, hStart); addFillet(pX + t, topY, -Math.PI / 2, hStart); - addFillet(pX - t, botY, Math.PI / 2, hEnd); addFillet(pX + t, botY, 0, hEnd); } else { @@ -229,7 +190,6 @@ export const createBinGeometry = ( addFillet(leftX, pY - t, 0, hStart); addFillet(leftX, pY + t, -Math.PI / 2, hStart); - addFillet(rightX, pY - t, Math.PI / 2, hEnd); addFillet(rightX, pY + t, Math.PI, hEnd); }