diff --git a/src/components/ConfigStep.tsx b/src/components/ConfigStep.tsx index 37c856e..ca2950d 100644 --- a/src/components/ConfigStep.tsx +++ b/src/components/ConfigStep.tsx @@ -1,153 +1,171 @@ -import React from 'react'; -import { AppConfig } from '../types'; -import { Ruler, Box, Layers, Minimize2, CircleDashed } from 'lucide-react'; +import React, { useEffect, useRef } from 'react'; +import { AppConfig, PerforationPattern } from '../types'; +import { Settings2, Grid, Circle, Triangle, Hexagon, LayoutGrid } from 'lucide-react'; interface Props { config: AppConfig; - onChange: (newConfig: AppConfig) => void; + onChange: (config: AppConfig) => void; } -const InputGroup: React.FC<{ label: string; children: React.ReactNode }> = ({ label, children }) => ( -
- -
{children}
-
-); - -const NumberInput = ({ - label, - value, - onChange, - max -}: { - label: string; - value: number; - onChange: (val: number) => void; - max?: number -}) => ( -
- {label} - onChange(parseFloat(e.target.value) || 0)} - className="w-full bg-slate-800 border border-slate-700 rounded p-2 pl-8 text-white focus:ring-2 focus:ring-primary outline-none" - /> - мм -
-); - export const ConfigStep: React.FC = ({ config, onChange }) => { - const updateDrawer = (key: keyof AppConfig['drawer'], val: number) => { - onChange({ ...config, drawer: { ...config.drawer, [key]: val } }); + + // Инициализация дефолтных значений, если их нет + useEffect(() => { + if (!config.perforation) { + onChange({ + ...config, + perforation: { enabled: false, pattern: 'hexagon', diameter: 8, spacing: 4 } + }); + } + }, []); + + const perf = config.perforation || { enabled: false, pattern: 'hexagon', diameter: 8, spacing: 4 }; + + const updatePerf = (updates: Partial) => { + onChange({ ...config, perforation: { ...perf, ...updates } }); + }; + + const updateDrawer = (key: keyof typeof config.drawer, value: number) => { + onChange({ ...config, drawer: { ...config.drawer, [key]: value } }); + }; + + // --- RENDER PREVIEW (SVG) --- + const renderPreview = () => { + if (!perf.enabled) return
Перфорация выключена
; + + const size = perf.diameter; + const gap = Math.max(2, perf.spacing); // Минимальный зазор 2мм + const step = size + gap; + const W = 140; + const H = 100; + + const elements = []; + const rows = Math.floor(H / (step * 0.866)); // 0.866 для сот (sin 60) + const cols = Math.floor(W / step); + + for(let j=0; j W - 10 || y > H - 10) continue; + + if (perf.pattern === 'circle') { + elements.push(); + } else if (perf.pattern === 'hexagon') { + // Рисуем шестиугольник + const r = size / 2; + const points = []; + for (let k = 0; k < 6; k++) { + const angle = (k * 60 + 30) * Math.PI / 180; + points.push(`${x + r * Math.cos(angle)},${y + r * Math.sin(angle)}`); + } + elements.push(); + } else if (perf.pattern === 'triangle') { + const r = size / 2; + const angleOffset = isOdd ? 180 : 0; + const points = []; + for (let k = 0; k < 3; k++) { + const angle = (k * 120 - 90 + angleOffset) * Math.PI / 180; + points.push(`${x + r * Math.cos(angle)},${y + r * Math.sin(angle)}`); + } + elements.push(); + } + } + } + + return ( + + {elements} + + ); }; return ( -
-

- 1. Размеры -

+
+ {/* 1. ГАБАРИТЫ */} +
+

1. Размеры ящика

+
+
+ + updateDrawer('width', parseFloat(e.target.value))} className="w-full bg-slate-800 border border-slate-700 rounded-lg px-3 py-2 text-white focus:ring-2 focus:ring-primary outline-none mt-1"/> +
+
+ + updateDrawer('depth', parseFloat(e.target.value))} className="w-full bg-slate-800 border border-slate-700 rounded-lg px-3 py-2 text-white focus:ring-2 focus:ring-primary outline-none mt-1"/> +
+
+ + updateDrawer('height', parseFloat(e.target.value))} className="w-full bg-slate-800 border border-slate-700 rounded-lg px-3 py-2 text-white focus:ring-2 focus:ring-primary outline-none mt-1"/> +
+
+
-
- {/* Drawer Dimensions */} -
-

- Внутренние размеры ящика -

- - updateDrawer('width', v)} /> - - - updateDrawer('depth', v)} /> - - - updateDrawer('height', v)} /> - + {/* 2. ПЕРФОРАЦИЯ */} +
+
+

2. Перфорация (узоры)

+
+ {perf.enabled ? 'Включено' : 'Выключено'} + +
- {/* Settings */} -
-

- Параметры печати -

- - {/* Wall Thickness */} -
-
- - - {config.wallThickness.toFixed(1)} мм - -
-
- 0.4 - onChange({...config, wallThickness: parseFloat(e.target.value)})} - className="w-full h-2 bg-slate-700 rounded-lg appearance-none cursor-pointer accent-primary hover:accent-blue-400 transition-all" - /> - 3.2 -
-
+
+ {/* Controls */} +
+
+ +
+ + + +
+
- {/* Corner Radius (NEW) */} -
-
- - - {config.cornerRadius?.toFixed(0) || 0} мм - -
-
- 0 - onChange({...config, cornerRadius: parseFloat(e.target.value)})} - className="w-full h-2 bg-slate-700 rounded-lg appearance-none cursor-pointer accent-purple-500 hover:accent-purple-400 transition-all" - /> - 20 -
-
- - {/* Printer Tolerance */} -
-
- - - {config.printerTolerance.toFixed(1)} мм - -
-
- 0.0 - onChange({...config, printerTolerance: parseFloat(e.target.value)})} - className="w-full h-2 bg-slate-700 rounded-lg appearance-none cursor-pointer accent-accent hover:accent-amber-400 transition-all" - /> - 2.0 -
-
+
+
+ + updatePerf({ diameter: Math.min(12, parseFloat(e.target.value)) })} className="w-full bg-slate-800 border border-slate-700 rounded-lg px-3 py-2 text-white mt-1"/> +
+
+ + updatePerf({ spacing: Math.max(2, parseFloat(e.target.value)) })} className="w-full bg-slate-800 border border-slate-700 rounded-lg px-3 py-2 text-white mt-1"/> +
+
+
+ {/* Preview */} +
+ Предпросмотр + {renderPreview()} +
-
+
+ + {/* 3. ПРОЧЕЕ */} +
+

Дополнительно

+
+
+ + onChange({...config, wallThickness: parseFloat(e.target.value)})} className="bg-slate-800 border border-slate-700 rounded px-2 py-1 text-sm w-20 text-gray-300"/> +
+
+ + onChange({...config, cornerRadius: parseFloat(e.target.value)})} className="bg-slate-800 border border-slate-700 rounded px-2 py-1 text-sm w-20 text-gray-300"/> +
+
+
); }; \ No newline at end of file diff --git a/src/services/geometryGenerator.ts b/src/services/geometryGenerator.ts index 8633c7d..747c4df 100644 --- a/src/services/geometryGenerator.ts +++ b/src/services/geometryGenerator.ts @@ -2,33 +2,54 @@ 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 = {}; + partitions.forEach(p => { limits[p.id] = { min: 0, max: 1 }; }); + for (let pass = 0; pass < 4; pass++) { + partitions.forEach(target => { + let newMin = 0; + let newMax = 1; + const center = target.offset; + partitions.forEach(obstacle => { + if (target.id === obstacle.id || target.axis === obstacle.axis) return; + const obsMin = limits[obstacle.id].min; + const obsMax = limits[obstacle.id].max; + const EPS = 0.002; + if (target.offset >= obsMin - EPS && target.offset <= obsMax + EPS) { + 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}`, @@ -46,118 +67,250 @@ export const calculateParts = (config: AppConfig, splits: LayoutSplits): Generat return parts; }; -// --- ГЕОМЕТРИЯ --- +// --- ГЕОМЕТРИЯ С ОТВЕРСТИЯМИ --- +// Функция добавляет отверстия в THREE.Shape +const applyPerforationToShape = (shape: THREE.Shape, width: number, height: number, config: AppConfig) => { + if (!config.perforation?.enabled) return; + + const { pattern, diameter, spacing } = config.perforation; + const step = diameter + Math.max(2, spacing); // Шаг сетки + + // Отступы от краев (чтобы не портить структуру) + const marginX = 4; + const marginY = 4; + + // Генерируем сетку отверстий + const cols = Math.floor((width - marginX * 2) / step); + const rows = Math.floor((height - marginY * 2) / (pattern === 'circle' ? step : step * 0.866)); + + // Центрируем паттерн + const startX = (width - ((cols - 1) * step)) / 2; + const startY = (height - ((rows - 1) * (pattern === 'circle' ? step : step * 0.866))) / 2; + + for (let j = 0; j < rows; j++) { + const isOdd = j % 2 !== 0; + const y = startY + j * (pattern === 'circle' ? step : step * 0.866); + + for (let i = 0; i < cols; i++) { + let x = startX + i * step; + if ((pattern === 'hexagon' || pattern === 'triangle') && isOdd) x += step / 2; + + // Доп. проверка границ + if (x < marginX + diameter/2 || x > width - marginX - diameter/2) continue; + if (y < marginY + diameter/2 || y > height - marginY - diameter/2) continue; + + const hole = new THREE.Path(); + const r = diameter / 2; + + if (pattern === 'circle') { + hole.absarc(x, y, r, 0, Math.PI * 2, true); + } else if (pattern === 'hexagon') { + for (let k = 0; k < 6; k++) { + const angle = (k * 60 + 30) * Math.PI / 180; + const px = x + r * Math.cos(angle); + const py = y + r * Math.sin(angle); + if (k === 0) hole.moveTo(px, py); else hole.lineTo(px, py); + } + hole.closePath(); + } 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; + const px = x + r * Math.cos(angle); + const py = y + r * Math.sin(angle); + if (k === 0) hole.moveTo(px, py); else hole.lineTo(px, py); + } + hole.closePath(); + } + shape.holes.push(hole); + } + } +}; + +const createRectWithHoles = (w: number, h: number, config: AppConfig): THREE.Shape => { + const shape = new THREE.Shape(); + shape.moveTo(0, 0); + shape.lineTo(w, 0); + shape.lineTo(w, h); + shape.lineTo(0, h); + shape.lineTo(0, 0); + applyPerforationToShape(shape, w, h, config); + return shape; +}; + +// Стандартный прямоугольник для пола (без дырок) 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); + 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); + 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; + 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[] = [] + width: number, depth: number, height: number, thickness: number, radius: number = 0, partitions: Partition[] = [], config?: AppConfig // Config нужен для дырок ): THREE.BufferGeometry => { const geometries: THREE.BufferGeometry[] = []; + + // Безопасный конфиг если не передан + const safeConfig = config || { perforation: { enabled: false } } as AppConfig; - // ДНО И ВНЕШНИЕ СТЕНКИ + // 1. ДНО (Floor) - Без дырок 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); + // 2. ВНЕШНИЕ СТЕНКИ (С ДЫРКАМИ) + // Мы строим их как 4 отдельные панели, чтобы можно было применить 2D паттерн дырок + const innerW = width - 2 * thickness; + const innerD = depth - 2 * thickness; + const wallH = height - thickness; + + // Front & Back Walls (Width x Height) + const wallShapeFB = createRectWithHoles(innerW, wallH, safeConfig); + // Left & Right Walls (Depth x Height) - используем полную глубину минус отступы, чтобы углы сошлись + // Но для простоты: Front/Back стоят "между" Left/Right. + // Left/Right имеют длину = depth. Front/Back длину = width - 2*thick. + const wallShapeLR = createRectWithHoles(depth, wallH, safeConfig); + + // Функция для создания стенки, её экструзии и поворота + const addWall = (shape: THREE.Shape, len: number, pos: [number, number, number], rotY: number) => { + // Shape рисуется от 0,0. Экструдим на толщину. + const geo = new THREE.ExtrudeGeometry(shape, { depth: thickness, bevelEnabled: false }); + // Центрируем shape по X (чтобы вращать удобно) + geo.translate(-len/2, 0, 0); + + // Поворачиваем: Сначала "поднимаем" shape вертикально? + // По умолчанию shape в XY плоскости. Extrude идет в Z. + // Нам нужно чтобы стена стояла. + // XY plane -> Wall upright. + + // Сдвигаем на позицию (x, y=thickness, z) + // Y в ThreeJS это "вверх". Стенки начинаются с floor thickness. + + // Корректировка пивота + geo.translate(len/2, 0, 0); // Вернули в 0..len по X + geo.translate(-len/2, 0, 0); // Центрируем: -len/2 .. len/2 + + // Вращение + geo.rotateY(rotY); + // Позиция + geo.translate(pos[0], thickness, pos[2]); // Y = thickness (на полу) + geometries.push(geo); + }; + + // ! ВАЖНО: Текущая реализация createRectWithHoles рисует прямоугольник 0..W, 0..H в плоскости XY. + // Extrude добавляет глубину Z. + // Стенка "стоит" если Z - это толщина. - if (innerWidth > 0.1 && innerDepth > 0.1) { - const innerHole = createRoundedRectShape(innerWidth, innerDepth, innerRadius); - outerShape.holes.push(innerHole); - } + // Front Wall (Z = innerD/2 + thick/2 = depth/2 - thick/2) -> Позиция Z чуть смещена + // Back Wall (Z = -depth/2 + thick/2) + + // Front (Z+) + const geoF = new THREE.ExtrudeGeometry(wallShapeFB, { depth: thickness, bevelEnabled: false }); + geoF.translate(-innerW/2, 0, 0); // Центрируем по X + geoF.translate(0, thickness, innerD/2); // Поднимаем на пол, сдвигаем вперед + geometries.push(geoF); - 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); + // Back (Z-) + const geoB = new THREE.ExtrudeGeometry(wallShapeFB, { depth: thickness, bevelEnabled: false }); + geoB.translate(-innerW/2, 0, -thickness); // Центрируем, толщина назад + geoB.translate(0, thickness, -innerD/2); + geometries.push(geoB); + + // Left (X-) + const geoL = new THREE.ExtrudeGeometry(wallShapeLR, { depth: thickness, bevelEnabled: false }); + geoL.rotateY(Math.PI / 2); // Поворачиваем на 90 + geoL.translate(-width/2, thickness, -depth/2); // Ставим слева + geometries.push(geoL); + + // Right (X+) + const geoR = new THREE.ExtrudeGeometry(wallShapeLR, { depth: thickness, bevelEnabled: false }); + geoR.rotateY(Math.PI / 2); + geoR.translate(width/2 - thickness, thickness, -depth/2); + geometries.push(geoR); + + + // 3. ВНУТРЕННИЕ ПЕРЕГОРОДКИ (С ДЫРКАМИ) + const limitMap = solveWallLimits(partitions); - // ВНУТРЕННИЕ ПЕРЕГОРОДКИ (СТРОГО ПО ДАННЫМ, БЕЗ SOLVER) partitions.forEach(p => { - // Берем данные напрямую. Если в 2D нарисовано от 0.2 до 0.8, тут будет 0.2 до 0.8. - const pMin = p.min ?? 0; - const pMax = p.max ?? 1; - + 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; + // Реальная длина стенки в мм + let wallLen = 0; + let pX = 0, pZ = 0; // Центр стенки + let rotY = 0; + if (p.axis === 'x') { - pWidth = thickness; - pDepth = lengthRatio * innerDepth; - pX = (-innerWidth / 2) + (innerWidth * p.offset); - pY = (-innerDepth / 2) + (innerDepth * midRatio); + // Вертикальная на экране = Вдоль Z в 3D + wallLen = lengthRatio * innerD; + // Позиция центра + pX = (-innerW / 2) + (innerW * p.offset); + // Начало по Z + const startZ = (-innerD / 2) + (innerD * pMin); + pZ = startZ; + rotY = Math.PI / 2; } else { - pWidth = lengthRatio * innerWidth; - pDepth = thickness; - pX = (-innerWidth / 2) + (innerWidth * midRatio); - pY = (-innerDepth / 2) + (innerDepth * p.offset); + // Горизонтальная на экране = Вдоль X в 3D + wallLen = lengthRatio * innerW; + const startX = (-innerW / 2) + (innerW * pMin); + pX = startX; + pZ = (-innerD / 2) + (innerD * p.offset); + rotY = 0; } - 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); + // Создаем профиль с дырками + const partShape = createRectWithHoles(wallLen, wallH, safeConfig); + const partGeo = new THREE.ExtrudeGeometry(partShape, { depth: thickness, bevelEnabled: false }); + + // Поворачиваем и ставим на место + // Изначально shape в XY (0..len, 0..height) + if (p.axis === 'x') { + // Нужно повернуть Y 90. + partGeo.rotateY(Math.PI / 2); + // После поворота: X -> Z, Y -> Y, Z -> X + // Начало было 0,0,0. Стало 0,0,0. Длина ушла в +Z. + partGeo.translate(pX - thickness/2, thickness, pZ); + } else { + // Вдоль X. Ничего вращать не надо, кроме смещения на толщину + partGeo.translate(pX, thickness, pZ - thickness/2); + } + geometries.push(partGeo); - // СКРУГЛЕНИЯ (Fillets) + // --- 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; // Край ящика - + 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; + if (n.axis === p.axis) return false; + const nLims = limitMap[n.id]; + return Math.abs(n.offset - pos) < 0.002 && p.offset >= nLims.min && p.offset <= nLims.max; }); return neighbor ? neighbor.height : 0; }; @@ -165,33 +318,36 @@ export const createBinGeometry = ( 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) => { + const addFillet = (x: number, y: number, rot: 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.rotateY(rot); 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); + // ... тот же код галтелей + const topZ = (-innerD / 2) + (innerD * pMin); + const botZ = (-innerD / 2) + (innerD * pMax); + const centerX = (-innerW/2) + (innerW * p.offset); - 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); + addFillet(centerX - t, topZ, Math.PI, hStart); + addFillet(centerX + t, topZ, -Math.PI / 2, hStart); + addFillet(centerX - t, botZ, Math.PI / 2, hEnd); + addFillet(centerX + t, botZ, 0, hEnd); } else { - const leftX = (-innerWidth / 2) + (innerWidth * pMin); - const rightX = (-innerWidth / 2) + (innerWidth * pMax); + const leftX = (-innerW / 2) + (innerW * pMin); + const rightX = (-innerW / 2) + (innerW * pMax); + const centerZ = (-innerD/2) + (innerD * p.offset); - 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); + addFillet(leftX, centerZ - t, 0, hStart); + addFillet(leftX, centerZ + t, -Math.PI / 2, hStart); + addFillet(rightX, centerZ - t, Math.PI / 2, hEnd); + addFillet(rightX, centerZ + t, Math.PI, hEnd); } } }); diff --git a/src/types.ts b/src/types.ts index c2a7c17..78641f4 100644 --- a/src/types.ts +++ b/src/types.ts @@ -4,19 +4,29 @@ export interface DrawerDimensions { height: number; } +export type PerforationPattern = 'circle' | 'hexagon' | 'triangle'; + +export interface PerforationConfig { + enabled: boolean; + pattern: PerforationPattern; + diameter: number; // Размер отверстия + spacing: number; // Расстояние между центрами (шаг) +} + export interface AppConfig { drawer: DrawerDimensions; wallThickness: number; printerTolerance: number; cornerRadius: number; + perforation: PerforationConfig; // Новая секция } export interface Partition { id: string; axis: 'x' | 'y'; - offset: number; // Позиция (0.0 - 1.0) - min: number; // Начало стенки (0.0 - 1.0) - max: number; // Конец стенки (0.0 - 1.0) + offset: number; + min: number; + max: number; height: number; rounded: boolean; }