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 = {}; // 1. Инициализация: считаем, что все стенки идут от 0 до 1 partitions.forEach(p => { limits[p.id] = { min: 0, max: 1 }; }); // 2. Итеративное решение (3 прохода достаточно для любой вложенности) 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) return; if (target.axis === obstacle.axis) return; // Параллельные не мешают // Берем АКТУАЛЬНЫЕ границы препятствия с прошлого шага const obsMin = limits[obstacle.id].min; const obsMax = limits[obstacle.id].max; // Пересекает ли препятствие путь нашей стенки? // obstacle.offset - это позиция препятствия на нашем пути. // target.offset - это наша позиция. Она должна быть ВНУТРИ длины препятствия. 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 : []; const safeY = Array.isArray(splits?.y) ? splits.y : []; const safeParts = splits?.partitions || {}; const xPoints = [0, ...[...safeX].sort((a, b) => a - b), 1]; const yPoints = [0, ...[...safeY].sort((a, b) => a - b), 1]; let partCounter = 1; for (let i = 0; i < xPoints.length - 1; i++) { for (let j = 0; j < yPoints.length - 1; j++) { const rawW = (xPoints[i + 1] - xPoints[i]) * config.drawer.width; const rawD = (yPoints[j + 1] - yPoints[j]) * config.drawer.depth; // Игнорируем микро-ячейки if (rawW < 5 || rawD < 5) continue; const rawX = xPoints[i] * config.drawer.width; const rawY = yPoints[j] * config.drawer.depth; const internalPartitions = safeParts[`${i}-${j}`] || []; const realWidth = rawW - config.printerTolerance; const realDepth = rawD - config.printerTolerance; const realX = rawX + (config.printerTolerance / 2); const realY = rawY + (config.printerTolerance / 2); parts.push({ id: `part-${partCounter}`, name: `Ячейка ${i+1}-${j+1}`, width: realWidth, depth: realDepth, height: config.drawer.height, x: realX, y: realY, color: `hsl(${Math.random() * 360}, 70%, 50%)`, internalPartitions: internalPartitions }); partCounter++; } } return parts; }; // --- ГЕОМЕТРИЯ --- const createRoundedRectShape = (width: number, height: number, radius: number): THREE.Shape => { const shape = new THREE.Shape(); const x = -width / 2; const y = -height / 2; const r = Math.min(radius, width / 2 - 0.1, height / 2 - 0.1); if (r <= 0.1) { shape.moveTo(x, y); shape.lineTo(x + width, y); shape.lineTo(x + width, y + height); shape.lineTo(x, y + height); shape.lineTo(x, y); } else { shape.moveTo(x, y + r); shape.lineTo(x, y + height - r); shape.quadraticCurveTo(x, y + height, x + r, y + height); shape.lineTo(x + width - r, y + height); shape.quadraticCurveTo(x + width, y + height, x + width, y + height - r); shape.lineTo(x + width, y + r); shape.quadraticCurveTo(x + width, y, x + width - r, y); shape.lineTo(x + r, y); shape.quadraticCurveTo(x, y, x, y + r); } return shape; }; // Форма галтели (вогнутый уголок) const createFilletShape = (radius: number): THREE.Shape => { const shape = new THREE.Shape(); // Рисуем в 1-м квадранте shape.moveTo(0, 0); shape.lineTo(radius, 0); shape.absarc(radius, radius, radius, 1.5 * Math.PI, Math.PI, true); shape.lineTo(0, 0); return shape; }; export const createBinGeometry = ( width: number, depth: number, height: number, thickness: number, radius: number = 0, partitions: Partition[] = [] ): THREE.BufferGeometry => { const geometries: THREE.BufferGeometry[] = []; // 1. ДНО const floorShape = createRoundedRectShape(width, depth, radius); const floorGeo = new THREE.ExtrudeGeometry(floorShape, { depth: thickness, bevelEnabled: false }); floorGeo.rotateX(-Math.PI / 2); geometries.push(floorGeo); // 2. ВНЕШНИЕ СТЕНКИ const outerShape = createRoundedRectShape(width, depth, radius); const innerRadius = Math.max(0.1, radius - thickness); const innerWidth = width - (2 * thickness); const innerDepth = depth - (2 * thickness); if (innerWidth > 0.1 && innerDepth > 0.1) { const innerHole = createRoundedRectShape(innerWidth, innerDepth, innerRadius); outerShape.holes.push(innerHole); } const wallHeight = height - thickness; const wallGeo = new THREE.ExtrudeGeometry(outerShape, { depth: wallHeight, bevelEnabled: false }); wallGeo.rotateX(-Math.PI / 2); wallGeo.translate(0, thickness, 0); geometries.push(wallGeo); // 3. ВНУТРЕННИЕ ПЕРЕГОРОДКИ (С 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; const midRatio = pMin + (lengthRatio / 2); let pWidth = 0, pDepth = 0, pX = 0, pY = 0; if (p.axis === 'x') { // Вертикальная pWidth = thickness; pDepth = lengthRatio * innerDepth; pX = (-innerWidth / 2) + (innerWidth * p.offset); pY = (-innerDepth / 2) + (innerDepth * midRatio); } else { // Горизонтальная pWidth = lengthRatio * innerWidth; pDepth = thickness; pX = (-innerWidth / 2) + (innerWidth * midRatio); pY = (-innerDepth / 2) + (innerDepth * p.offset); } // Геометрия стенки const partShape = createRoundedRectShape(pWidth, pDepth, 0.2); const partGeo = new THREE.ExtrudeGeometry(partShape, { depth: p.height, bevelEnabled: false }); partGeo.rotateX(-Math.PI / 2); partGeo.translate(pX, thickness, pY); geometries.push(partGeo); // --- ДОБАВЛЕНИЕ СКРУГЛЕНИЙ (FILLETS) --- if (p.rounded && radius > 1) { const filletR = Math.min(radius, 5); const filletShape = createFilletShape(filletR); const filletExtrude = { depth: p.height, bevelEnabled: false }; const addFillet = (x: number, y: number, rotY: number) => { const geo = new THREE.ExtrudeGeometry(filletShape, filletExtrude); geo.rotateX(-Math.PI / 2); geo.rotateY(rotY); geo.translate(x, thickness, y); geometries.push(geo); }; const h = thickness / 2; // half thickness if (p.axis === 'x') { // Вертикальная (идет вдоль Z в 3D) const startY = (-innerDepth / 2) + (innerDepth * pMin); // "Верхний" конец (Back) const endY = (-innerDepth / 2) + (innerDepth * pMax); // "Нижний" конец (Front) // Проверяем, упирается ли она в края или другие стенки? (всегда рисуем, так как solver обрезал) // Скругляем 4 угла стыка // Top End (pMin) addFillet(pX - h, startY, 0); // Слева, смотрит вверх addFillet(pX + h, startY, -Math.PI/2); // Справа, смотрит вверх // Bottom End (pMax) addFillet(pX - h, endY, Math.PI/2); // Слева, смотрит вниз addFillet(pX + h, endY, Math.PI); // Справа, смотрит вниз } else { // Горизонтальная (идет вдоль X в 3D) const startX = (-innerWidth / 2) + (innerWidth * pMin); // Левый конец const endX = (-innerWidth / 2) + (innerWidth * pMax); // Правый конец // Left End (pMin) addFillet(startX, pY + h, Math.PI); // Сверху, смотрит влево (FIXED ANGLE) addFillet(startX, pY - h, Math.PI/2); // Снизу, смотрит влево (FIXED ANGLE) // Right End (pMax) addFillet(endX, pY + h, -Math.PI/2); // Сверху, смотрит вправо (FIXED ANGLE) addFillet(endX, pY - h, 0); // Снизу, смотрит вправо (FIXED ANGLE) } } }); const merged = mergeBufferGeometries(geometries); if (merged) merged.computeVertexNormals(); return merged || new THREE.BoxGeometry(1, 1, 1); }; export const generateSTL = (mesh: THREE.Object3D): Uint8Array | string => { const exporter = new STLExporter(); const result = exporter.parse(mesh, { binary: true }); if (result instanceof DataView) return new Uint8Array(result.buffer, result.byteOffset, result.byteLength); return result as string; }; export const exportSTL = (mesh: THREE.Object3D, filename: string) => { const result = generateSTL(mesh); const blob = new Blob([result], { type: 'application/octet-stream' }); const link = document.createElement('a'); link.href = URL.createObjectURL(blob); link.download = filename; link.click(); };