import * as THREE from 'three'; import { STLExporter, mergeBufferGeometries } from 'three-stdlib'; import { AppConfig, LayoutSplits, GeneratedPart, Partition } from '../types'; 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 createConcaveFilletShape = (radius: number): THREE.Shape => { const shape = new THREE.Shape(); shape.moveTo(0, 0); shape.lineTo(radius, 0); shape.absarc(radius, radius, radius, -Math.PI / 2, -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[] = []; // ДНО И ВНЕШНИЕ СТЕНКИ const floorShape = createRoundedRectShape(width, depth, radius); const floorGeo = new THREE.ExtrudeGeometry(floorShape, { depth: thickness, bevelEnabled: false }); floorGeo.rotateX(-Math.PI / 2); geometries.push(floorGeo); 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); // ВНУТРЕННИЕ ПЕРЕГОРОДКИ (СТРОГО ПО ДАННЫМ, БЕЗ SOLVER) partitions.forEach(p => { // Берем данные напрямую. Если в 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; 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.1); 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 = createConcaveFilletShape(filletR); // Функция проверки высоты соседа (простая проверка на пересечение) const getNeighborHeight = (pos: number) => { if (pos < 0.001 || pos > 0.999) return height; // Край ящика const neighbor = partitions.find(n => { 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; }; const hStart = Math.min(p.height, getNeighborHeight(pMin)); const hEnd = Math.min(p.height, getNeighborHeight(pMax)); const addFillet = (x: number, y: number, rotY: number, h: number) => { if (h <= 1) return; const geo = new THREE.ExtrudeGeometry(filletShape, { depth: h, bevelEnabled: false }); geo.rotateX(-Math.PI / 2); geo.rotateY(rotY); geo.translate(x, thickness, y); geometries.push(geo); }; const t = thickness / 2; if (p.axis === 'x') { const topY = (-innerDepth / 2) + (innerDepth * pMin); const botY = (-innerDepth / 2) + (innerDepth * pMax); 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 { const leftX = (-innerWidth / 2) + (innerWidth * pMin); const rightX = (-innerWidth / 2) + (innerWidth * pMax); 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); } } }); 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(); };