From 0521c23246fb1f296e91999b68b67897eb37439a 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 01:19:20 +0300 Subject: [PATCH] 5 --- src/services/geometryGenerator.ts | 271 ++++++++++++++---------------- 1 file changed, 130 insertions(+), 141 deletions(-) diff --git a/src/services/geometryGenerator.ts b/src/services/geometryGenerator.ts index 77dc906..e4882b0 100644 --- a/src/services/geometryGenerator.ts +++ b/src/services/geometryGenerator.ts @@ -2,37 +2,23 @@ import * as THREE from 'three'; import { STLExporter, mergeBufferGeometries } from 'three-stdlib'; import { AppConfig, LayoutSplits, GeneratedPart, Partition } from '../types'; -// --- CLEANUP UTILS --- +// --- ВСПОМОГАТЕЛЬНЫЕ ФУНКЦИИ --- -// Удаляет дублирующиеся перегородки (фантомы) -const deduplicatePartitions = (partitions: Partition[]): Partition[] => { - const unique: Partition[] = []; - const seen = new Set(); - - partitions.forEach(p => { - // Округляем координаты для создания уникального ключа - const k = `${p.axis}-${p.offset.toFixed(3)}-${p.min?.toFixed(3)}-${p.max?.toFixed(3)}`; - if (!seen.has(k)) { - seen.add(k); - unique.push(p); - } - }); - return unique; +// Извлекаем ВСЕ перегородки из всех ячеек в один плоский список +const getAllPartitions = (splits: LayoutSplits): Partition[] => { + if (!splits || !splits.partitions) return []; + return Object.values(splits.partitions).flat(); }; -// Очистка точек для генерации цветных объемов -const cleanPoints = (points: number[]) => { - return Array.from(new Set(points.map(p => parseFloat(p.toFixed(3))))).sort((a, b) => a - b); -}; - -// --- CALCULATE VOLUMES (ЦВЕТНЫЕ КУБИКИ) --- export const calculateParts = (config: AppConfig, splits: LayoutSplits): GeneratedPart[] => { const parts: GeneratedPart[] = []; const safeX = Array.isArray(splits?.x) ? splits.x : []; const safeY = Array.isArray(splits?.y) ? splits.y : []; - - const uniqueX = cleanPoints([0, ...safeX, 1]); - const uniqueY = cleanPoints([0, ...safeY, 1]); + const safeParts = splits?.partitions || {}; + + // Просто сортируем точки, без сложной фильтрации, чтобы совпадало с 2D + const uniqueX = [0, ...safeX, 1].sort((a, b) => a - b); + const uniqueY = [0, ...safeY, 1].sort((a, b) => a - b); let partCounter = 1; @@ -43,24 +29,29 @@ export const calculateParts = (config: AppConfig, splits: LayoutSplits): Generat const y1 = uniqueY[j]; const y2 = uniqueY[j+1]; - const w = (x2 - x1) * config.drawer.width; - const d = (y2 - y1) * config.drawer.depth; + // Игнорируем вырожденные ячейки + if (x2 - x1 < 0.001 || y2 - y1 < 0.001) continue; - if (w < 2 || d < 2) continue; + const rawW = (x2 - x1) * config.drawer.width; + const rawD = (y2 - y1) * config.drawer.depth; + const rawX = x1 * config.drawer.width; + const rawY = y1 * config.drawer.depth; + + const internalPartitions = safeParts[`${i}-${j}`] || []; - // Отступ для визуализации (gap) - const gap = config.wallThickness / 2 + 0.2; + // Отступ для визуализации объемов (gap) + const gap = config.wallThickness / 2 + 0.1; parts.push({ id: `part-${partCounter}`, name: `Ячейка ${partCounter}`, - width: Math.max(1, w - gap * 2), - depth: Math.max(1, d - gap * 2), + width: Math.max(1, rawW - gap * 2), + depth: Math.max(1, rawD - gap * 2), height: config.drawer.height - config.wallThickness, - x: (x1 * config.drawer.width) + gap, - y: (y1 * config.drawer.depth) + gap, + x: rawX + gap, + y: rawY + gap, color: `hsl(${(partCounter * 137.5) % 360}, 70%, 50%)`, - internalPartitions: [] + internalPartitions: internalPartitions }); partCounter++; } @@ -68,24 +59,25 @@ export const calculateParts = (config: AppConfig, splits: LayoutSplits): Generat return parts; }; -// --- SHAPE GENERATION (PERFORATION) --- +// --- ГЕОМЕТРИЯ --- +// Прямоугольник с отверстиями const createPerforatedShape = (length: number, height: number, config: AppConfig): THREE.Shape => { const shape = new THREE.Shape(); - // 1. Внешний контур: CCW (Против часовой) + // Внешний контур (CCW) shape.moveTo(0, 0); shape.lineTo(length, 0); shape.lineTo(length, height); shape.lineTo(0, height); shape.lineTo(0, 0); - // Если перфорация выключена или стена слишком мала + // Если перфорация выключена или стена мала if (!config.perforation?.enabled || length < 10 || height < 10) return shape; const { pattern, diameter, spacing } = config.perforation; const step = diameter + Math.max(2, spacing); - const margin = 4; // Отступ от краев + const margin = 3; const effW = length - margin * 2; const effH = height - margin * 2; @@ -107,28 +99,24 @@ const createPerforatedShape = (length: number, height: number, config: AppConfig let cx = startX + i * step; if ((pattern === 'hexagon' || pattern === 'triangle') && isOdd) cx += step / 2; - // Проверка границ if (cx - diameter/2 < margin || cx + diameter/2 > length - margin || cy - diameter/2 < margin || cy + diameter/2 > height - margin) continue; const hole = new THREE.Path(); const r = diameter / 2; - // 2. Отверстия: CW (По часовой) - Это критично для Three.js! + // ДЫРКИ СТРОГО ПО ЧАСОВОЙ (CW) if (pattern === 'circle') { hole.absarc(cx, cy, r, 0, Math.PI * 2, true); - } - else if (pattern === 'hexagon') { + } else if (pattern === 'hexagon') { for (let k = 0; k < 6; k++) { - // -k обеспечивает CW порядок const angle = (-k * 60 + 90) * Math.PI / 180; const px = cx + r * Math.cos(angle); const py = cy + r * Math.sin(angle); if (k === 0) hole.moveTo(px, py); else hole.lineTo(px, py); } hole.closePath(); - } - else if (pattern === 'triangle') { + } else if (pattern === 'triangle') { const rot = isOdd ? 180 : 0; for (let k = 0; k < 3; k++) { const angle = (-k * 120 + 90 + rot) * Math.PI / 180; @@ -144,9 +132,10 @@ const createPerforatedShape = (length: number, height: number, config: AppConfig return shape; }; -// Floor Shape (Solid) const createFloorShape = (width: number, depth: number, radius: number): THREE.Shape => { const shape = new THREE.Shape(); + // Floor shape centered at 0,0 for ease of rotation later if needed, + // BUT createBinGeometry expects floor to be from -W/2 to W/2 const x = -width / 2; const y = -depth / 2; const r = Math.min(radius, width / 2 - 0.1, depth / 2 - 0.1); @@ -171,6 +160,7 @@ const createFloorShape = (width: number, depth: number, radius: number): THREE.S return shape; }; +// Галтель (вогнутая) для стыков const createFilletShape = (radius: number): THREE.Shape => { const shape = new THREE.Shape(); shape.moveTo(0, 0); @@ -180,18 +170,30 @@ const createFilletShape = (radius: number): THREE.Shape => { return shape; }; -// --- BUILDER --- +// --- MAIN BUILDER --- +// Обратите внимание: сигнатура изменена, теперь мы принимаем splits целиком export const createBinGeometry = ( - width: number, depth: number, height: number, thickness: number, radius: number = 0, partitions: Partition[] = [], config?: AppConfig + width: number, depth: number, height: number, thickness: number, radius: number = 0, + splits: LayoutSplits | Partition[] = [], // Поддержка и старого, и нового формата + config?: AppConfig ): THREE.BufferGeometry => { + const geometries: THREE.BufferGeometry[] = []; const safeConfig = config || { perforation: { enabled: false } } as AppConfig; - // 1. FLOOR + // Нормализация входных данных: нам нужен плоский список стенок + let partitions: Partition[] = []; + if (Array.isArray(splits)) { + partitions = splits; + } else if (splits && splits.partitions) { + partitions = getAllPartitions(splits); + } + + // 1. ПОЛ const floorShape = createFloorShape(width, depth, radius); const floorGeo = new THREE.ExtrudeGeometry(floorShape, { depth: thickness, bevelEnabled: false }); - floorGeo.rotateX(-Math.PI / 2); + floorGeo.rotateX(-Math.PI / 2); // XZ plane geometries.push(floorGeo); // Размеры внутреннего пространства @@ -199,132 +201,119 @@ export const createBinGeometry = ( const innerD = depth - 2 * thickness; const wallH = height - thickness; - // Helper для установки стенки - const placeWall = (length: number, isVertical: boolean, centerX: number, centerZ: number) => { - // Генерируем 2D форму с дырками - const shape = createPerforatedShape(length, wallH, safeConfig); - const geo = new THREE.ExtrudeGeometry(shape, { depth: thickness, bevelEnabled: false }); + // 2. ВНЕШНИЕ СТЕНКИ + const shapeFB = createPerforatedShape(innerW, wallH, safeConfig); + const shapeLR = createPerforatedShape(depth, wallH, safeConfig); - if (isVertical) { - // Вертикальная (идет вдоль Z) - // Shape рисуется в XY. Extrude в Z. - // Поворачиваем вокруг Y на 90. - // X -> Z, Y -> Y, Z -> X. - // Теперь длина (бывший X) идет вдоль Z. Толщина (бывший Z) идет вдоль X. - geo.rotateY(Math.PI / 2); - - // Центр по X: centerX. Начало по Z: centerZ - length/2. - // После поворота: начало в (0,0,0) перешло в (0,0,0). - // Длина ушла в -Z (или +Z в зависимости от правил). - // Проще: ставим центр геометрии в центр позиции. - geo.center(); // Центрируем геометрию локально - geo.translate(centerX, thickness + wallH/2, centerZ); // Ставим на место - } else { - // Горизонтальная (идет вдоль X) - // Shape в XY. Extrude в Z. - // Длина вдоль X. Толщина вдоль Z. - geo.center(); - geo.translate(centerX, thickness + wallH/2, centerZ); - } - geometries.push(geo); - }; + // Front (вдоль X, спереди) + const geoF = new THREE.ExtrudeGeometry(shapeFB, { depth: thickness, bevelEnabled: false }); + geoF.translate(-innerW/2, thickness, depth/2 - thickness); + geometries.push(geoF); - // 2. EXTERNAL WALLS - // Front (вдоль X) - placeWall(innerW, false, -width/2 + innerW/2 + thickness, depth/2 - thickness/2); // Исправленные координаты - // Проще: Front стоит на Z = depth/2 - thick/2. X центр = 0 (если floor от -W/2 до W/2). - // Floor shape: -W/2..W/2. - - // Давайте пересчитаем позиции точно относительно центра (0,0) - // Front: CenterX=0, CenterZ = (depth - thickness)/2 - placeWall(innerW, false, 0, (depth - thickness)/2); - - // Back: CenterX=0, CenterZ = -(depth - thickness)/2 - placeWall(innerW, false, 0, -(depth - thickness)/2); - - // Left: CenterX=-(width - thickness)/2, CenterZ=0. Length = depth. - placeWall(depth, true, -(width - thickness)/2, 0); - - // Right: CenterX=(width - thickness)/2, CenterZ=0. Length = depth. - placeWall(depth, true, (width - thickness)/2, 0); + // Back (вдоль X, сзади) + const geoB = new THREE.ExtrudeGeometry(shapeFB, { depth: thickness, bevelEnabled: false }); + geoB.translate(-innerW/2, thickness, -depth/2); + geometries.push(geoB); + // Left (вдоль Z, слева) + const geoL = new THREE.ExtrudeGeometry(shapeLR, { depth: thickness, bevelEnabled: false }); + geoL.rotateY(Math.PI / 2); + geoL.translate(-width/2, thickness, -depth/2); + geometries.push(geoL); - // 3. INTERNAL PARTITIONS - // Используем дедупликацию, чтобы убрать двойные стенки - const uniquePartitions = deduplicatePartitions(partitions); + // Right (вдоль Z, справа) + const geoR = new THREE.ExtrudeGeometry(shapeLR, { depth: thickness, bevelEnabled: false }); + geoR.rotateY(Math.PI / 2); + geoR.translate(width/2 - thickness, thickness, -depth/2); + geometries.push(geoR); - uniquePartitions.forEach(p => { + // 3. ВНУТРЕННИЕ ПЕРЕГОРОДКИ (Исправлено позиционирование) + partitions.forEach(p => { const pMin = p.min ?? 0; const pMax = p.max ?? 1; - if (pMax - pMin < 0.01) return; + // Игнорируем ошибки данных + if (pMax - pMin < 0.001) return; let length = 0; - let cX = 0; - let cZ = 0; - let isVertical = false; + let isVertical = false; // Vertical on 2D screen = Along Z axis in 3D + + // Вычисляем координаты центра и длины + let posX = 0; // Центр по X (для верт) или Начало по X (для гориз) + let posZ = 0; // Начало по Z (для верт) или Центр по Z (для гориз) if (p.axis === 'x') { - // Вертикальная на экране 2D (вдоль Z в 3D) + // Вертикальная на экране (Z-axis in 3D) isVertical = true; length = (pMax - pMin) * innerD; - - // X: offset * innerW. Но innerW начинается от -innerW/2. - cX = (-innerW/2) + (p.offset * innerW); - - // Z центр: Середина между pMin и pMax - const midRatio = (pMin + pMax) / 2; - cZ = (-innerD/2) + (midRatio * innerD); + // В 2D X идет слева направо (0..1). В 3D X идет от -innerW/2 до innerW/2. + posX = (-innerW/2) + (p.offset * innerW); + // В 2D Y идет сверху вниз (0..1). В 3D Z идет от -innerD/2 (зад) до innerD/2 (перед). + posZ = (-innerD/2) + (pMin * innerD); } else { - // Горизонтальная на экране 2D (вдоль X в 3D) + // Горизонтальная на экране (X-axis in 3D) isVertical = false; length = (pMax - pMin) * innerW; - - // X центр - const midRatio = (pMin + pMax) / 2; - cX = (-innerW/2) + (midRatio * innerW); - - // Z: offset * innerD - cZ = (-innerD/2) + (p.offset * innerD); + posX = (-innerW/2) + (pMin * innerW); + posZ = (-innerD/2) + (p.offset * innerD); } - placeWall(length, isVertical, cX, cZ); + // Генерируем 2D профиль + const partShape = createPerforatedShape(length, wallH, safeConfig); + const partGeo = new THREE.ExtrudeGeometry(partShape, { depth: thickness, bevelEnabled: false }); - // --- FILLETS --- + if (isVertical) { + // Поворот чтобы шла вдоль Z + partGeo.rotateY(Math.PI / 2); + // Смещаем в позицию. + // Центр X = posX. Но так как толщина экструзии идет в +X (после поворота), надо сместить на -thickness/2 + partGeo.translate(posX - thickness/2, thickness, posZ); + } else { + // Вдоль X + // Центр Z = posZ. Смещаем на -thickness/2 + partGeo.translate(posX, thickness, posZ - thickness/2); + } + + geometries.push(partGeo); + + // --- СКРУГЛЕНИЯ (FILLETS) --- if (p.rounded && radius > 1) { - const fR = Math.min(radius, 5); + const fR = Math.min(radius, 5); const fShape = createFilletShape(fR); const h = p.height; - const addFillet = (x: number, z: number, rot: number) => { + const addFillet = (fx: number, fz: number, rot: number) => { const geo = new THREE.ExtrudeGeometry(fShape, { depth: h, bevelEnabled: false }); geo.rotateX(-Math.PI / 2); geo.rotateY(rot); - geo.translate(x, thickness, z); + geo.translate(fx, thickness, fz); geometries.push(geo); }; const t = thickness / 2; - - // Вычисляем концы стенки для скруглений + if (isVertical) { - const zStart = cZ - length/2; - const zEnd = cZ + length/2; + const zStart = posZ; + const zEnd = posZ + length; - // Верхний стык (дальний по Z, если смотреть в 2D) -> Min - addFillet(cX - t, zStart, Math.PI); - addFillet(cX + t, zStart, -Math.PI/2); - // Нижний стык -> Max - addFillet(cX - t, zEnd, Math.PI/2); - addFillet(cX + t, zEnd, 0); + // Top junction (Z-min / Back) + addFillet(posX - t, zStart, Math.PI); // Face Back-Left + addFillet(posX + t, zStart, -Math.PI/2); // Face Back-Right + + // Bottom junction (Z-max / Front) + addFillet(posX - t, zEnd, Math.PI/2); // Face Front-Left + addFillet(posX + t, zEnd, 0); // Face Front-Right } else { - const xStart = cX - length/2; - const xEnd = cX + length/2; + const xStart = posX; + const xEnd = posX + length; - addFillet(xStart, cZ - t, 0); - addFillet(xStart, cZ + t, -Math.PI/2); - addFillet(xEnd, cZ - t, Math.PI/2); - addFillet(xEnd, cZ + t, Math.PI); + // Left junction (X-min / Left) + addFillet(xStart, posZ - t, 0); // Face Left-Back + addFillet(xStart, posZ + t, -Math.PI/2); // Face Left-Front + + // Right junction (X-max / Right) + addFillet(xEnd, posZ - t, Math.PI/2); // Face Right-Back + addFillet(xEnd, posZ + t, Math.PI); // Face Right-Front } } });