diff --git a/src/services/geometryGenerator.ts b/src/services/geometryGenerator.ts index 5dae70f..2a275f1 100644 --- a/src/services/geometryGenerator.ts +++ b/src/services/geometryGenerator.ts @@ -6,19 +6,18 @@ import { AppConfig, LayoutSplits, GeneratedPart, Partition } from '../types'; // --- ВСПОМОГАТЕЛЬНЫЕ ФУНКЦИИ --- -// Очистка дубликатов точек для визуализации const cleanPoints = (points: number[]) => { const rounded = points.map(p => Math.round(p * 1000) / 1000).sort((a, b) => a - b); return [...new Set(rounded)]; }; -// Сбор всех перегородок в один массив const getAllPartitions = (splits: LayoutSplits): Partition[] => { if (!splits || !splits.partitions) return []; + // Собираем все массивы перегородок в один плоский массив return Object.values(splits.partitions).flat(); }; -// Функция для шага 3 (отображение цветных ячеек) +// Функция для предпросмотра (шаг 3 - цветные блоки) export const calculateParts = (config: AppConfig, splits: LayoutSplits): GeneratedPart[] => { const parts: GeneratedPart[] = []; const safeX = Array.isArray(splits?.x) ? splits.x : []; @@ -62,7 +61,7 @@ export const calculateParts = (config: AppConfig, splits: LayoutSplits): Generat return parts; }; -// --- ГЕНЕРАЦИЯ ГЕОМЕТРИИ (CSG) --- +// --- ГЕНЕРАЦИЯ ГЕОМЕТРИИ (CSG FIXED) --- export const createBinGeometry = ( width: number, depth: number, height: number, thickness: number, radius: number = 0, @@ -72,40 +71,42 @@ export const createBinGeometry = ( const safeConfig = config || { perforation: { enabled: false } } as AppConfig; const evaluator = new Evaluator(); - // Ускоряем CSG, отключая лишние проверки - evaluator.useGroups = false; + evaluator.useGroups = false; // Важно для корректного слияния материалов и нормалей - // 1. СОЗДАЕМ "МЯСО" (Стены и пол) - // Мы собираем все прямоугольники в один массив геометрий, - // сливаем их в одну геометрию, и делаем из нее один Brush. - // Это в 10 раз быстрее, чем делать ADDITION в цикле. - - const solidParts: THREE.BufferGeometry[] = []; + // Массивы для хранения геометрии перед слиянием + const solidGeometries: THREE.BufferGeometry[] = []; + const holeGeometries: THREE.BufferGeometry[] = []; + const filletGeometries: THREE.BufferGeometry[] = []; - // ПОЛ + // ========================================== + // 1. СБОРКА ТВЕРДЫХ ТЕЛ (ПОЛ + СТЕНЫ) + // ========================================== + + // 1.1 ПОЛ const floorGeo = new THREE.BoxGeometry(width, thickness, depth); floorGeo.translate(0, thickness / 2, 0); - solidParts.push(floorGeo); + solidGeometries.push(floorGeo); - // СТЕНЫ const wallH = height - thickness; const innerW = width - 2 * thickness; const innerD = depth - 2 * thickness; - // Хелпер для создания куба стены - const addWall = (w: number, h: number, d: number, x: number, y: number, z: number) => { + // Функция добавления геометрии стены + const addWallGeometry = (w: number, h: number, d: number, x: number, y: number, z: number) => { const geo = new THREE.BoxGeometry(w, h, d); geo.translate(x, y, z); - solidParts.push(geo); + solidGeometries.push(geo); }; - // Внешние стены - addWall(innerW, wallH, thickness, 0, thickness + wallH/2, depth/2 - thickness/2); // Front - addWall(innerW, wallH, thickness, 0, thickness + wallH/2, -depth/2 + thickness/2); // Back - addWall(thickness, wallH, depth, -width/2 + thickness/2, thickness + wallH/2, 0); // Left - addWall(thickness, wallH, depth, width/2 - thickness/2, thickness + wallH/2, 0); // Right + // 1.2 ВНЕШНИЕ СТЕНЫ + // Front & Back (Вдоль X) + addWallGeometry(innerW, wallH, thickness, 0, thickness + wallH/2, depth/2 - thickness/2); + addWallGeometry(innerW, wallH, thickness, 0, thickness + wallH/2, -depth/2 + thickness/2); + // Left & Right (Вдоль Z) + addWallGeometry(thickness, wallH, depth, -width/2 + thickness/2, thickness + wallH/2, 0); + addWallGeometry(thickness, wallH, depth, width/2 - thickness/2, thickness + wallH/2, 0); - // Внутренние стены + // 1.3 ВНУТРЕННИЕ ПЕРЕГОРОДКИ let partitions: Partition[] = []; if (Array.isArray(splits)) { partitions = splits; @@ -116,50 +117,94 @@ export const createBinGeometry = ( partitions.forEach(p => { const pMin = p.min ?? 0; const pMax = p.max ?? 1; - if (pMax - pMin < 0.001) return; + + // Игнорируем ошибки данных + if (Math.abs(pMax - pMin) < 0.001) return; let w=0, h=p.height, d=0, x=0, z=0; - if (p.axis === 'x') { // Вертикальная (вдоль Z) + if (p.axis === 'x') { // Вертикальная (Вдоль Z) w = thickness; d = (pMax - pMin) * innerD; x = (-innerW/2) + (p.offset * innerW); - z = (-innerD/2) + (pMin * innerD) + (d/2); - } else { // Горизонтальная (вдоль X) + const midZ = (pMin + pMax) / 2; + z = (-innerD/2) + (midZ * innerD); + } else { // Горизонтальная (Вдоль X) w = (pMax - pMin) * innerW; d = thickness; - x = (-innerW/2) + (pMin * innerW) + (w/2); + const midX = (pMin + pMax) / 2; + x = (-innerW/2) + (midX * innerW); z = (-innerD/2) + (p.offset * innerD); } - addWall(w, h, d, x, thickness + h/2, z); + addWallGeometry(w, h, d, x, thickness + h/2, z); }); - // Объединяем всю твердую геометрию в один Mesh - const mergedSolids = mergeBufferGeometries(solidParts); - let mainBrush = new Brush(mergedSolids); - mainBrush.updateMatrixWorld(); + // ========================================== + // 2. СБОРКА СКРУГЛЕНИЙ (ADDITION) + // ========================================== + + if (radius > 0) { + const fRad = Math.min(radius, 5); + // Используем цилиндр для сглаживания углов (выпуклое скругление внутренних углов) + // Для настоящего вогнутого fillet в CSG нужно вычитать обратную форму, но для FDM печати + // добавление материала в угол (chamfer/fillet) часто лучше. + // Делаем просто цилиндры в местах стыков. + const filletCyl = new THREE.CylinderGeometry(fRad, fRad, 1, 16); + + const addFillet = (x: number, z: number, h: number) => { + const f = filletCyl.clone(); + f.scale(1, h, 1); + f.translate(x, thickness + h/2, z); + filletGeometries.push(f); + }; + + partitions.forEach(p => { + const pMin = p.min ?? 0; + const pMax = p.max ?? 1; + if (Math.abs(pMax - pMin) < 0.001) return; + + if (!p.rounded) return; // Пропускаем если скругление выключено для этой стенки + + if (p.axis === 'x') { // Vert + const xPos = (-innerW/2) + (p.offset * innerW); + const zStart = (-innerD/2) + (pMin * innerD); + const zEnd = (-innerD/2) + (pMax * innerD); + addFillet(xPos, zStart, p.height); + addFillet(xPos, zEnd, p.height); + } else { // Horiz + const zPos = (-innerD/2) + (p.offset * innerD); + const xStart = (-innerW/2) + (pMin * innerW); + const xEnd = (-innerW/2) + (pMax * innerW); + addFillet(xStart, zPos, p.height); + addFillet(xEnd, zPos, p.height); + } + }); + } + + // ========================================== + // 3. СБОРКА ОТВЕРСТИЙ (SUBTRACTION) + // ========================================== - // 2. ПЕРФОРАЦИЯ (ЕСЛИ ВКЛЮЧЕНА) if (safeConfig.perforation?.enabled) { const { pattern, diameter, spacing } = safeConfig.perforation; - const step = diameter + Math.max(2, spacing); const margin = 4; + const step = diameter + Math.max(2, spacing); + + // Базовые сверла (длинные, чтобы пробить насквозь) + const drillLength = thickness * 4; + const drillZ = new THREE.CylinderGeometry(diameter/2, diameter/2, drillLength, 12); + drillZ.rotateX(Math.PI / 2); // Вдоль Z + const drillX = new THREE.CylinderGeometry(diameter/2, diameter/2, drillLength, 12); + drillX.rotateZ(Math.PI / 2); // Вдоль X - // Создаем базовые "сверла" - const drillZ = new THREE.CylinderGeometry(diameter/2, diameter/2, thickness * 4, 12); - drillZ.rotateX(Math.PI / 2); // Сверлит вдоль Z (для стен вдоль X) - - const drillX = new THREE.CylinderGeometry(diameter/2, diameter/2, thickness * 4, 12); - drillX.rotateZ(Math.PI / 2); // Сверлит вдоль X (для стен вдоль Z) - - const cutterParts: THREE.BufferGeometry[] = []; - - // Функция расстановки сверл на плоскости - const drillWall = (W: number, H: number, startX: number, startY: number, startZ: number, axis: 'x' | 'z') => { + // Функция генерации массива сверл для плоскости + const createDrills = (W: number, H: number, startX: number, startY: number, startZ: number, axis: 'x' | 'z') => { const cols = Math.floor((W - margin*2) / step); const rowH = pattern === 'circle' ? step : step * 0.866; const rows = Math.floor((H - margin*2) / rowH); + if (cols <= 0 || rows <= 0) return; + const offsetX = (W - cols * step) / 2; const offsetY = (H - rows * rowH) / 2; @@ -173,95 +218,73 @@ export const createBinGeometry = ( if (u > W - margin || v > H - margin) continue; let drill: THREE.BufferGeometry; - if (axis === 'x') { - // Стена вдоль X (Front/Back/Horiz). Сверлим вдоль Z. - // U = X, V = Y. + // Стена вдоль X. Сверлим ВДОЛЬ Z. drill = drillZ.clone(); drill.translate(startX + u, startY + v, startZ); } else { - // Стена вдоль Z (Left/Right/Vert). Сверлим вдоль X. - // U = Z, V = Y. + // Стена вдоль Z. Сверлим ВДОЛЬ X. drill = drillX.clone(); drill.translate(startX, startY + v, startZ + u); } - cutterParts.push(drill); + holeGeometries.push(drill); } } }; - // Генерируем сверла для внешних стен - // Front (X-wall) - drillWall(innerW, wallH, -innerW/2, thickness, depth/2, 'x'); - // Back (X-wall) - drillWall(innerW, wallH, -innerW/2, thickness, -depth/2, 'x'); - // Left (Z-wall) - drillWall(depth, wallH, -width/2, thickness, -depth/2, 'z'); - // Right (Z-wall) - drillWall(depth, wallH, width/2, thickness, -depth/2, 'z'); + // 3.1 Сверлим внешние стены + // Front & Back + createDrills(innerW, wallH, -innerW/2, thickness, depth/2, 'x'); + createDrills(innerW, wallH, -innerW/2, thickness, -depth/2, 'x'); + // Left & Right + createDrills(depth, wallH, -width/2, thickness, -depth/2, 'z'); + createDrills(depth, wallH, width/2, thickness, -depth/2, 'z'); - // Генерируем сверла для ВНУТРЕННИХ стен + // 3.2 Сверлим внутренние перегородки partitions.forEach(p => { const pMin = p.min ?? 0; const pMax = p.max ?? 1; - if (pMax - pMin < 0.001) return; + if (Math.abs(pMax - pMin) < 0.001) return; if (p.axis === 'x') { // Vert wall (Z-axis) const len = (pMax - pMin) * innerD; const xPos = (-innerW/2) + (p.offset * innerW); const zStart = (-innerD/2) + (pMin * innerD); - drillWall(len, p.height, xPos, thickness, zStart, 'z'); + createDrills(len, p.height, xPos, thickness, zStart, 'z'); } else { // Horiz wall (X-axis) const len = (pMax - pMin) * innerW; const xStart = (-innerW/2) + (pMin * innerW); const zPos = (-innerD/2) + (p.offset * innerD); - drillWall(len, p.height, xStart, thickness, zPos, 'x'); + createDrills(len, p.height, xStart, thickness, zPos, 'x'); } }); + } - // ВЫЧИТАНИЕ - if (cutterParts.length > 0) { - const mergedCutters = mergeBufferGeometries(cutterParts); - const cutterBrush = new Brush(mergedCutters); - cutterBrush.updateMatrixWorld(); - - // SOLID - CUTTERS - mainBrush = evaluator.evaluate(mainBrush, cutterBrush, SUBTRACTION); + // ========================================== + // 4. ФИНАЛЬНЫЕ ОПЕРАЦИИ CSG + // ========================================== + + // Объединяем всю твердую геометрию + const mergedSolids = mergeBufferGeometries([...solidGeometries, ...filletGeometries]); + if (!mergedSolids) return new THREE.BoxGeometry(1,1,1); // Fallback + + let finalBrush = new Brush(mergedSolids); + finalBrush.updateMatrixWorld(); + + // Если есть отверстия, вычитаем их + if (holeGeometries.length > 0) { + const mergedHoles = mergeBufferGeometries(holeGeometries); + if (mergedHoles) { + const holeBrush = new Brush(mergedHoles); + holeBrush.updateMatrixWorld(); + finalBrush = evaluator.evaluate(finalBrush, holeBrush, SUBTRACTION); } } - // 3. СКРУГЛЕНИЯ (ДОБАВЛЕНИЕ) - if (radius > 0) { - const filletParts: THREE.BufferGeometry[] = []; - const fRad = Math.min(radius, 5); - const filletGeo = new THREE.CylinderGeometry(fRad, fRad, 1, 16, 1, false, 0, Math.PI/2); // Четверть цилиндра - // Центрируем пивот для удобства - filletGeo.translate(0, 0.5, 0); // Y вверх 0..1 - - // Хелпер для добавления скругления - const addFillet = (x: number, y: number, z: number, h: number, rotY: number) => { - const f = filletGeo.clone(); - f.scale(1, h, 1); // Масштабируем по высоте - // Поворот - f.rotateY(rotY); - f.translate(x, y, z); - filletParts.push(f); - }; - - // Проходим по стыкам (упрощенно: вертикальные столбики в углах примыканий) - // В данной реализации CSG проще всего добавить цилиндры в углы, чтобы "залить" их. - // Но так как мы используем ADDITION для стен, углы уже залиты (острые). - // Чтобы сделать *вогнутые* скругления (Fillet), нужно делать UNION специальных форм. - - // Для скорости и надежности, пока оставим острые внутренние углы, если они получены через ADDITION. - // Если нужны именно вогнутые скругления, нужно добавлять "призмы" и вычитать цилиндры, это сложно. - // Если нужны выпуклые скругления внешних углов - это просто. - - // Оставим пока без доп. геометрии для скруглений, так как ADDITION уже делает герметичный стык. - // Если критично именно *визуальное* скругление, можно добавить цилиндры. - } - - return mainBrush.geometry; + // Обновляем нормали для корректного отображения света (убирает прозрачность) + finalBrush.geometry.computeVertexNormals(); + + return finalBrush.geometry; }; export const generateSTL = (mesh: THREE.Object3D): Uint8Array | string => {