From 03a787a459fb6f5239685980fc5ab71e1b858e22 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: Mon, 12 Jan 2026 02:27:57 +0300 Subject: [PATCH] 4 --- src/services/geometryGenerator.ts | 317 ++++++++++++------------------ 1 file changed, 131 insertions(+), 186 deletions(-) diff --git a/src/services/geometryGenerator.ts b/src/services/geometryGenerator.ts index 2a275f1..1171577 100644 --- a/src/services/geometryGenerator.ts +++ b/src/services/geometryGenerator.ts @@ -13,11 +13,9 @@ const cleanPoints = (points: number[]) => { const getAllPartitions = (splits: LayoutSplits): Partition[] => { if (!splits || !splits.partitions) return []; - // Собираем все массивы перегородок в один плоский массив return Object.values(splits.partitions).flat(); }; -// Функция для предпросмотра (шаг 3 - цветные блоки) export const calculateParts = (config: AppConfig, splits: LayoutSplits): GeneratedPart[] => { const parts: GeneratedPart[] = []; const safeX = Array.isArray(splits?.x) ? splits.x : []; @@ -61,7 +59,7 @@ export const calculateParts = (config: AppConfig, splits: LayoutSplits): Generat return parts; }; -// --- ГЕНЕРАЦИЯ ГЕОМЕТРИИ (CSG FIXED) --- +// --- ГЕНЕРАЦИЯ ГЕОМЕТРИИ (ПОСЛЕДОВАТЕЛЬНЫЙ CSG) --- export const createBinGeometry = ( width: number, depth: number, height: number, thickness: number, radius: number = 0, @@ -71,220 +69,167 @@ export const createBinGeometry = ( const safeConfig = config || { perforation: { enabled: false } } as AppConfig; const evaluator = new Evaluator(); - evaluator.useGroups = false; // Важно для корректного слияния материалов и нормалей - - // Массивы для хранения геометрии перед слиянием - 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); - solidGeometries.push(floorGeo); + evaluator.useGroups = false; const wallH = height - thickness; const innerW = width - 2 * thickness; const innerD = depth - 2 * thickness; - // Функция добавления геометрии стены - 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); - solidGeometries.push(geo); + // 1. Базовая геометрия - ПОЛ + const floorGeo = new THREE.BoxGeometry(width, thickness, depth); + floorGeo.translate(0, thickness / 2, 0); + let mainBrush = new Brush(floorGeo); + mainBrush.updateMatrixWorld(); + + // --- ФУНКЦИИ ОПЕРАЦИЙ --- + + // Добавить твердое тело (стену/скругление) + const addSolid = (geo: THREE.BufferGeometry) => { + const brush = new Brush(geo); + brush.updateMatrixWorld(); + mainBrush = evaluator.evaluate(mainBrush, brush, ADDITION); }; - // 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; - } else if (splits && splits.partitions) { - partitions = getAllPartitions(splits); - } - - partitions.forEach(p => { - const pMin = p.min ?? 0; - const pMax = p.max ?? 1; - - // Игнорируем ошибки данных - 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) - w = thickness; - d = (pMax - pMin) * innerD; - x = (-innerW/2) + (p.offset * innerW); - const midZ = (pMin + pMax) / 2; - z = (-innerD/2) + (midZ * innerD); - } else { // Горизонтальная (Вдоль X) - w = (pMax - pMin) * innerW; - d = thickness; - const midX = (pMin + pMax) / 2; - x = (-innerW/2) + (midX * innerW); - z = (-innerD/2) + (p.offset * innerD); - } - addWallGeometry(w, h, d, x, thickness + h/2, z); - }); - - // ========================================== - // 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) - // ========================================== - - if (safeConfig.perforation?.enabled) { + // Вычесть отверстия для конкретной зоны + const subtractHoles = (W: number, H: number, startX: number, startY: number, startZ: number, axis: 'x' | 'z') => { + if (!safeConfig.perforation?.enabled) return; const { pattern, diameter, spacing } = safeConfig.perforation; 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 cols = Math.floor((W - margin*2) / step); + const rowH = pattern === 'circle' ? step : step * 0.866; + const rows = Math.floor((H - margin*2) / rowH); - // Функция генерации массива сверл для плоскости - 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; - if (cols <= 0 || rows <= 0) return; + const offsetX = (W - cols * step) / 2; + const offsetY = (H - rows * rowH) / 2; - const offsetX = (W - cols * step) / 2; - const offsetY = (H - rows * rowH) / 2; + // Базовое сверло + const drillBase = new THREE.CylinderGeometry(diameter/2, diameter/2, thickness * 3, 12); + if (axis === 'x') drillBase.rotateX(Math.PI / 2); // Вдоль Z + else drillBase.rotateZ(Math.PI / 2); // Вдоль X - for(let r=0; r W - margin || v > H - margin) continue; + const drills: THREE.BufferGeometry[] = []; - let drill: THREE.BufferGeometry; - if (axis === 'x') { - // Стена вдоль X. Сверлим ВДОЛЬ Z. - drill = drillZ.clone(); - drill.translate(startX + u, startY + v, startZ); - } else { - // Стена вдоль Z. Сверлим ВДОЛЬ X. - drill = drillX.clone(); - drill.translate(startX, startY + v, startZ + u); - } - holeGeometries.push(drill); - } + for(let r=0; r W - margin || v > H - margin) continue; + + const drill = drillBase.clone(); + if (axis === 'x') drill.translate(startX + u, startY + v, startZ); + else drill.translate(startX, startY + v, startZ + u); + drills.push(drill); } - }; + } - // 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'); + if (drills.length > 0) { + const mergedDrills = mergeBufferGeometries(drills); + if (mergedDrills) { + const drillBrush = new Brush(mergedDrills); + drillBrush.updateMatrixWorld(); + mainBrush = evaluator.evaluate(mainBrush, drillBrush, SUBTRACTION); + } + } + }; + + + // 2. СБОРКА СТЕН (ADDITION) + + // Внешние стены + // Front + addSolid(new THREE.BoxGeometry(innerW, wallH, thickness).translate(0, thickness + wallH/2, depth/2 - thickness/2)); + // Back + addSolid(new THREE.BoxGeometry(innerW, wallH, thickness).translate(0, thickness + wallH/2, -depth/2 + thickness/2)); + // Left + addSolid(new THREE.BoxGeometry(thickness, wallH, depth).translate(-width/2 + thickness/2, thickness + wallH/2, 0)); + // Right + addSolid(new THREE.BoxGeometry(thickness, wallH, depth).translate(width/2 - thickness/2, thickness + wallH/2, 0)); + + // Внутренние перегородки + let partitions: Partition[] = []; + if (Array.isArray(splits)) partitions = splits; + else if (splits && splits.partitions) partitions = getAllPartitions(splits); + + partitions.forEach(p => { + const pMin = p.min ?? 0; const pMax = p.max ?? 1; + if (Math.abs(pMax - pMin) < 0.001) return; + + let w=0, h=p.height, d=0, x=0, z=0; + if (p.axis === 'x') { // Vert (Z-axis) + w = thickness; d = (pMax - pMin) * innerD; + x = (-innerW/2) + (p.offset * innerW); + z = (-innerD/2) + ((pMin + pMax)/2 * innerD); + } else { // Horiz (X-axis) + w = (pMax - pMin) * innerW; d = thickness; + x = (-innerW/2) + ((pMin + pMax)/2 * innerW); + z = (-innerD/2) + (p.offset * innerD); + } + addSolid(new THREE.BoxGeometry(w, h, d).translate(x, thickness + h/2, z)); + }); + + // 3. СКРУГЛЕНИЯ (ADDITION - цилиндры в углы) + if (radius > 0) { + const fRad = Math.min(radius, 5); + const filletBase = new THREE.CylinderGeometry(fRad, fRad, 1, 16); + filletBase.translate(0, 0.5, 0); // Pivot at bottom - // 3.2 Сверлим внутренние перегородки partitions.forEach(p => { - const pMin = p.min ?? 0; - const pMax = p.max ?? 1; + if (!p.rounded) return; + const pMin = p.min ?? 0; const pMax = p.max ?? 1; if (Math.abs(pMax - pMin) < 0.001) return; - if (p.axis === 'x') { // Vert wall (Z-axis) - const len = (pMax - pMin) * innerD; + const addF = (x: number, z: number) => { + const f = filletBase.clone(); + f.scale(1, p.height, 1); + f.translate(x, thickness, z); + addSolid(f); + }; + + if (p.axis === 'x') { // Vert const xPos = (-innerW/2) + (p.offset * innerW); - const zStart = (-innerD/2) + (pMin * innerD); - createDrills(len, p.height, xPos, thickness, zStart, 'z'); - } else { // Horiz wall (X-axis) - const len = (pMax - pMin) * innerW; - const xStart = (-innerW/2) + (pMin * innerW); + addF(xPos, (-innerD/2) + (pMin * innerD)); // Start Z + addF(xPos, (-innerD/2) + (pMax * innerD)); // End Z + } else { // Horiz const zPos = (-innerD/2) + (p.offset * innerD); - createDrills(len, p.height, xStart, thickness, zPos, 'x'); + addF((-innerW/2) + (pMin * innerW), zPos); // Start X + addF((-innerW/2) + (pMax * innerW), zPos); // End X } }); } - // ========================================== - // 4. ФИНАЛЬНЫЕ ОПЕРАЦИИ CSG - // ========================================== + // 4. ПЕРФОРАЦИЯ (SUBTRACTION) + if (safeConfig.perforation?.enabled) { + // Внешние стены + subtractHoles(innerW, wallH, -innerW/2, thickness, depth/2, 'x'); // Front + subtractHoles(innerW, wallH, -innerW/2, thickness, -depth/2, 'x'); // Back + subtractHoles(depth, wallH, -width/2, thickness, -depth/2, 'z'); // Left + subtractHoles(depth, wallH, width/2, thickness, -depth/2, 'z'); // Right - // Объединяем всю твердую геометрию - 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); - } + // Внутренние перегородки + partitions.forEach(p => { + const pMin = p.min ?? 0; const pMax = p.max ?? 1; + if (Math.abs(pMax - pMin) < 0.001) return; + if (p.axis === 'x') { + const len = (pMax - pMin) * innerD; + const xPos = (-innerW/2) + (p.offset * innerW); + const zStart = (-innerD/2) + (pMin * innerD); + subtractHoles(len, p.height, xPos, thickness, zStart, 'z'); + } else { + const len = (pMax - pMin) * innerW; + const xStart = (-innerW/2) + (pMin * innerW); + const zPos = (-innerD/2) + (p.offset * innerD); + subtractHoles(len, p.height, xStart, thickness, zPos, 'x'); + } + }); } - // Обновляем нормали для корректного отображения света (убирает прозрачность) - finalBrush.geometry.computeVertexNormals(); - - return finalBrush.geometry; + return mainBrush.geometry; }; export const generateSTL = (mesh: THREE.Object3D): Uint8Array | string => {