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); }