Files
BoxGenerator/src/services/geometryGenerator.ts
Халимов Рустам 9ff2c76912 Russian
2026-01-13 01:26:20 +03:00

622 lines
29 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import * as THREE from 'three';
import { STLExporter, mergeBufferGeometries } from 'three-stdlib';
import { AppConfig, LayoutSplits, GeneratedPart, Partition, PerforationConfig } 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;
};
// Вспомогательная функция: Создание перфорированной пластины (Вертикальная стенка)
const createPerforatedPlate = (
width: number,
height: number,
thickness: number,
perf: PerforationConfig,
marginLeft: number = 0,
marginRight: number = 0,
exclusions: { start: number, end: number, yMax?: number }[] = []
): THREE.BufferGeometry => {
const shape = new THREE.Shape();
shape.moveTo(0, 0);
shape.lineTo(width, 0);
shape.lineTo(width, height);
shape.lineTo(0, height);
shape.lineTo(0, 0);
// Генерация отверстий
if (perf && perf.enabled && width > perf.size && height > perf.size) {
const { shape: shapeType, size, gap } = perf;
const step = size + gap;
// Отступы: предотвращаем попадание отверстий на сплошные края
const startX = Math.max(gap, marginLeft + gap);
const startY = gap;
const endX = width - Math.max(gap, marginRight + gap);
const endY = height - gap;
// Строки
let row = 0;
for (let y = startY + size / 2; y < endY; y += (shapeType === 'triangle' || shapeType === 'honeycomb' ? step * 0.866 : step)) {
const isStaggered = (row % 2 !== 0);
const xOffset = (isStaggered && (shapeType === 'honeycomb' || shapeType === 'triangle')) ? step / 2 : 0;
for (let x = startX + size / 2 + xOffset; x < endX; x += step) {
const hole = new THREE.Path();
const r = size / 2;
// Проверка границ
if (x - r < marginLeft || x + r > width - marginRight || y - r < 0 || y + r > height) continue;
// Проверка зон исключения
// Зона активна, если X отверстия входит в [start, end] И Y отверстия ниже yMax (если указан).
// Если y > yMax, исключение не применяется (отверстие выше пересекающей стенки).
// Примечание: y измеряется от низа (0) до верха (height).
// yMax - высота пересекающей перегородки.
const inExclusion = exclusions.some(zone => {
if (x + r <= zone.start || x - r >= zone.end) return false; // Проверка по оси X
if (zone.yMax !== undefined && y - r > zone.yMax) return false; // Проверка по оси Y (отверстие выше стенки)
return true;
});
if (inExclusion) continue;
if (shapeType === 'circle') {
hole.absarc(x, y, r, 0, Math.PI * 2, true);
} else if (shapeType === 'honeycomb') {
// Шестиугольник (Соты)
for (let i = 0; i < 6; i++) {
const ang = (i * 60 + 30) * Math.PI / 180;
const px = x + r * Math.cos(ang);
const py = y + r * Math.sin(ang);
if (i === 0) hole.moveTo(px, py);
else hole.lineTo(px, py);
}
hole.closePath();
} else if (shapeType === 'triangle') {
// Треугольник
const ang1 = -90 * Math.PI / 180;
const ang2 = 30 * Math.PI / 180;
const ang3 = 150 * Math.PI / 180;
hole.moveTo(x + r * Math.cos(ang1), y + r * Math.sin(ang1));
hole.lineTo(x + r * Math.cos(ang2), y + r * Math.sin(ang2));
hole.lineTo(x + r * Math.cos(ang3), y + r * Math.sin(ang3));
hole.closePath();
}
shape.holes.push(hole);
}
row++;
}
}
const geo = new THREE.ExtrudeGeometry(shape, { depth: thickness, bevelEnabled: false });
// Выдавливание по Z. Стенка плоская в XY.
// Мы хотим, чтобы "thickness" была глубиной по Z.
return geo;
};
// Вспомогательная функция: Создание профиля угла (Выдавленный вертикально)
const createCornerProfile = (radius: number, thickness: number, height: number): THREE.BufferGeometry => {
if (radius <= 0) return new THREE.BufferGeometry();
const shape = new THREE.Shape();
// Создаем сегмент кольца (Полый угол) как единый контур.
// Центр в (0,0).
const innerRadius = Math.max(0.01, radius - thickness); // Гарантируем чуть > 0 для целостности формы
// 1. Начало внешней дуги
shape.moveTo(radius, 0);
// 2. Внешняя дуга (против часовой стрелки) -> К (0, radius)
shape.absarc(0, 0, radius, 0, Math.PI / 2, false);
// 3. Линия к внутреннему концу (0, innerRadius)
shape.lineTo(0, innerRadius);
// 4. Внутренняя дуга (по часовой стрелке) -> К (innerRadius, 0)
shape.absarc(0, 0, innerRadius, Math.PI / 2, 0, true);
// 5. Замыкаем контур
shape.lineTo(radius, 0);
// Выдавливание
// curveSegments 32 для гладкости
const geo = new THREE.ExtrudeGeometry(shape, { depth: height, bevelEnabled: false, curveSegments: 32 });
return geo;
};
export const createBinGeometry = (
width: number,
depth: number,
height: number,
thickness: number,
radius: number = 0,
partitions: Partition[] = [],
perforation?: PerforationConfig
): THREE.BufferGeometry => {
const geometries: THREE.BufferGeometry[] = [];
// ДНО (Floor) - Always same
const floorShape = createRoundedRectShape(width, depth, radius);
const floorGeo = new THREE.ExtrudeGeometry(floorShape, { depth: thickness, bevelEnabled: false });
floorGeo.rotateX(-Math.PI / 2); // Lay flat
geometries.push(floorGeo);
// СТЕНКИ
if (!perforation || !perforation.enabled) {
// --- ЛОГИКА ДЛЯ СПЛОШНЫХ СТЕН (Без перфорации) ---
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);
} else {
// --- ЛОГИКА ДЛЯ ПЕРФОРИРОВАННЫХ СТЕН (Раздельные части) ---
const wallHeight = height - thickness;
// Ограничиваем радиус минимум толщиной стенки для корректных углов
const effRadius = Math.max(radius, thickness);
const straightW = width - 2 * effRadius;
const straightD = depth - 2 * effRadius;
// 1. Углы (4 шт)
if (effRadius > 0) {
const cornerGeoBase = createCornerProfile(effRadius, thickness, wallHeight);
// 1. Поднимаем: Выдавливание Z -> Y. Форма перемещается в X(+)/Z(+).
cornerGeoBase.rotateX(-Math.PI / 2);
// Базовая форма угла (после rotateX) это Q4 (+X, -Z).
// Логика вращения: -90 градусов на квадрант (Стандартный Y-Rot в Three.js).
// Q4 (0) -> Q1 (-90) -> Q2 (-180/180) -> Q3 (-270/+90).
const positions = [
// Задний Правый (+X, +Z). Q1.
// Поворот -90 (-PI/2).
{ x: width / 2 - effRadius, z: depth / 2 - effRadius, rot: -Math.PI / 2 },
// Задний Левый (-X, +Z). Q2.
// Поворот 180 (PI).
{ x: -(width / 2 - effRadius), z: depth / 2 - effRadius, rot: Math.PI },
// Передний Левый (-X, -Z). Q3.
// Поворот 90 (PI/2). (Эквивалентно -270).
{ x: -(width / 2 - effRadius), z: -(depth / 2 - effRadius), rot: Math.PI / 2 },
// Передний Правый (+X, -Z). Q4.
// Поворот 0.
{ x: width / 2 - effRadius, z: -(depth / 2 - effRadius), rot: 0 }
];
positions.forEach(pos => {
const corner = cornerGeoBase.clone();
corner.rotateY(pos.rot);
corner.translate(pos.x, 0, pos.z); // Старт на Y=0
corner.translate(0, thickness, 0); // Сдвиг на толщину дна
geometries.push(corner);
});
}
// 2. Прямые стены (4 шт) - Центрированы по краям
// ЛОГИКА СБОРА ИСКЛЮЧЕНИЙ ДЛЯ СТЕН
const exclusionsBack: { start: number, end: number, yMax: number }[] = [];
const exclusionsFront: { start: number, end: number, yMax: number }[] = [];
const exclusionsLeft: { start: number, end: number, yMax: number }[] = [];
const exclusionsRight: { start: number, end: number, yMax: number }[] = [];
// Внутренние размеры
const effectiveInnerW = width - 2 * thickness;
const effectiveInnerD = depth - 2 * thickness;
partitions.forEach(p => {
const hEff = Math.max(0.1, p.height - thickness); // Исключаем только до высоты перегородки
// Гарантируем сплошную полосу вокруг перегородки добавлением отступа к зоне исключения
const padding = (perforation?.gap ?? 2) + 1;
if (p.axis === 'x') {
// Идет по глубине (Ось Y в макете).
// Пересекает Переднюю и Заднюю стенки.
const partGlobalX = (-effectiveInnerW / 2) + (effectiveInnerW * p.offset);
const sW = width - 2 * effRadius;
const localXOnWall = partGlobalX + sW / 2;
// Проверка пересечения с активной зоной стены + отступ
if (localXOnWall + thickness / 2 + padding > 0 && localXOnWall - thickness / 2 - padding < sW) {
const zone = {
start: localXOnWall - thickness / 2 - padding,
end: localXOnWall + thickness / 2 + padding,
yMax: hEff
};
if ((p.max ?? 1) > 0.99) exclusionsFront.push(zone); // Ближняя
if ((p.min ?? 0) < 0.01) exclusionsBack.push(zone); // Дальняя
}
} else { // p.axis === 'y'
// Идет по ширине (Ось X в макете).
// Пересекает Левую и Правую стенки.
const partGlobalZ = (-effectiveInnerD / 2) + (effectiveInnerD * p.offset);
const sD = depth - 2 * effRadius;
const localXOnWall = partGlobalZ + sD / 2;
if (localXOnWall + thickness / 2 + padding > 0 && localXOnWall - thickness / 2 - padding < sD) {
const zone = {
start: localXOnWall - thickness / 2 - padding,
end: localXOnWall + thickness / 2 + padding,
yMax: hEff
};
if ((p.min ?? 0) < 0.01) {
exclusionsLeft.push(zone);
}
if ((p.max ?? 1) > 0.99) {
exclusionsRight.push(zone);
}
}
}
});
// Передняя/Задняя
if (straightW > 0.1) {
// Стенка 1 (+Z). Это "Передняя" (Ближняя). Контактирует с p.max.
// Использует exclusionsFront.
const wGeoFront = createPerforatedPlate(straightW, wallHeight, thickness, perforation, 0, 0, exclusionsFront);
wGeoFront.translate(-straightW / 2, 0, 0);
const w1 = wGeoFront.clone();
w1.translate(0, thickness, depth / 2 - thickness);
geometries.push(w1);
// Стенка 2 (-Z). Это "Задняя" (Дальняя). Контактирует с p.min.
// Использует exclusionsBack.
// Нужна проверка маппинга (Поворот 180).
// p.offset увеличивает X. Стенка повернута на 180, значит X инвертирован.
// Маппим exclusionsBack.
const exclusionsBackMapped = exclusionsBack.map(e => ({
start: straightW - e.end,
end: straightW - e.start,
yMax: e.yMax
}));
const wGeoBack = createPerforatedPlate(straightW, wallHeight, thickness, perforation, 0, 0, exclusionsBackMapped);
wGeoBack.translate(-straightW / 2, 0, 0);
const w2 = wGeoBack.clone();
w2.rotateY(Math.PI);
w2.translate(0, thickness, -(depth / 2 - thickness));
geometries.push(w2);
}
// Левая/Правая
if (straightD > 0.1) {
// Стенка 3 (Правая +X).
// Пластина 0..straightD по X, 0..wallHeight по Y. Толщина по Z.
// Хотим разместить на X = width/2.
// Нужно повернуть -PI/2 вокруг Y чтобы выровнять ось X пластины с мировой осью Z.
const wGeoRight = createPerforatedPlate(straightD, wallHeight, thickness, perforation, 0, 0, exclusionsRight);
wGeoRight.translate(-straightD / 2, 0, 0); // Центр X пластины
const w3 = wGeoRight.clone();
w3.rotateY(-Math.PI / 2); // Поворот
w3.translate(width / 2, thickness, 0); // Позиция на X=width/2
geometries.push(w3);
// Стенка 4 (Левая -X).
// Также повернута на PI/2.
// Толщина по Z станем Мировой X (направо).
// Хотим на X=-width/2.
const exclusionsLeftMapped = exclusionsLeft.map(e => ({
start: straightD - e.end,
end: straightD - e.start,
yMax: e.yMax
}));
const wGeoLeft = createPerforatedPlate(straightD, wallHeight, thickness, perforation, 0, 0, exclusionsLeftMapped);
wGeoLeft.translate(-straightD / 2, 0, 0); // Центр X пластины
const w4 = wGeoLeft.clone();
w4.rotateY(Math.PI / 2);
w4.translate(-width / 2, thickness, 0); // Позиция на X=-width/2
geometries.push(w4);
}
}
// ВНУТРЕННИЕ ПЕРЕГОРОДКИ
const innerWidth = width - (2 * thickness);
const innerDepth = depth - (2 * thickness); // Приблизительное полезное пространство
partitions.forEach(p => {
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);
// ИСПРАВЛЕНИЕ: Вычитаем толщину, так как перегородки стоят НА дне
const effectiveHeight = Math.max(0.1, p.height - thickness);
// --- ЛОГИКА ДЛЯ ПЕРФОРИРОВАННЫХ ПЕРЕГОРОДОК ---
// Если включено, используем Пластину. Иначе - сплошное Выдавливание.
const usePerf = perforation && perforation.enabled;
let pX = 0, pY = 0; // Объявляем здесь для видимости в Скруглениях
if (usePerf) {
// Вычисляем точную геометрию
let pLen = 0;
if (p.axis === 'x') {
// Ось X -> Разделитель идет вдоль Y (Глубина)
pLen = lengthRatio * innerDepth;
pX = (-innerWidth / 2) + (innerWidth * p.offset);
pY = (-innerDepth / 2) + (innerDepth * midRatio); // Центр перегородки
// Вычисляем исключения для внутренней перегородки
const partExclusions: { start: number, end: number, yMax?: number }[] = [];
// Эта перегородка 'p' (Ось X, идет по Глубине).
// Пересекается перегородками 'n' (Ось Y, идут по Ширине).
partitions.forEach(n => {
if (n.axis !== 'y') return; // Пересекаются только ортогональные перегородки
// Проверяем, пересекает ли n перегородку p
// Y-координата пересечения:
const nY = (-innerDepth / 2) + (innerDepth * n.offset);
const pStartGlobal = pY - pLen / 2;
const pEndGlobal = pY + pLen / 2;
// И проверяем, покрывает ли n позицию X перегородки p.
// n идет по X от nMin до nMax.
const nXStart = (-innerWidth / 2) + (innerWidth * (n.min ?? 0));
const nXEnd = (-innerWidth / 2) + (innerWidth * (n.max ?? 1));
const pXLoc = pX;
if (nY >= pStartGlobal && nY <= pEndGlobal && pXLoc >= nXStart && pXLoc <= nXEnd) {
// Пересечение подтверждено.
// Вычисляем локальные координаты на пластине p.
// p идет по Y. Локальный X пластины мапится на глобальный Y.
const nHeight = Math.max(0.1, n.height - thickness);
const localX = nY - pY + pLen / 2;
partExclusions.push({ start: localX - thickness / 2, end: localX + thickness / 2, yMax: nHeight });
}
});
// Добавляем отступы (сплошные концы)
const margin = thickness;
const plate = createPerforatedPlate(pLen, effectiveHeight, thickness, perforation!, margin, margin, partExclusions);
plate.translate(-pLen / 2, 0, 0); // Центр X
// Поворачиваем для выравнивания по Глубине (вдоль Z)
// Пластина X -> Z
plate.rotateY(-Math.PI / 2);
// Позиционирование
// Пластина теперь вертикально по Z. Толщина по X.
plate.translate(pX + thickness / 2, thickness, pY);
geometries.push(plate);
} else {
// Ось Y -> Разделитель идет вдоль X (Ширина)
pLen = lengthRatio * innerWidth;
pX = (-innerWidth / 2) + (innerWidth * midRatio);
pY = (-innerDepth / 2) + (innerDepth * p.offset);
// Вычисление исключений
const partExclusions: { start: number, end: number, yMax?: number }[] = [];
// Эта перегородка 'p' (Ось Y, идет по Ширине).
// Пересекается перегородками 'n' (Ось X, идут по Глубине).
partitions.forEach(n => {
if (n.axis !== 'x') return;
const nX = (-innerWidth / 2) + (innerWidth * n.offset);
const pStartGlobal = pX - pLen / 2; // Диапазон X для p
const pEndGlobal = pX + pLen / 2;
const nYStart = (-innerDepth / 2) + (innerDepth * (n.min ?? 0));
const nYEnd = (-innerDepth / 2) + (innerDepth * (n.max ?? 1));
const pYLoc = pY;
if (nX >= pStartGlobal && nX <= pEndGlobal && pYLoc >= nYStart && pYLoc <= nYEnd) {
// Пересечение подтверждено
const nHeight = Math.max(0.1, n.height - thickness); // ВЫСОТА ПЕРЕСЕКАЮЩЕЙ ПЕРЕГОРОДКИ
const localX = nX - pX + pLen / 2;
partExclusions.push({ start: localX - thickness / 2, end: localX + thickness / 2, yMax: nHeight });
}
});
const margin = thickness;
const plate = createPerforatedPlate(pLen, effectiveHeight, thickness, perforation!, margin, margin, partExclusions);
plate.translate(-pLen / 2, 0, 0); // Центр X
// Уже выровнено по X. Толщина по Z.
// Диапазон Z [0, th]. Мы хотим [-th/2, th/2] относительно pY.
plate.translate(0, 0, -thickness / 2);
// Перемещение на позицию
plate.translate(pX, thickness, pY);
geometries.push(plate);
}
} else {
// --- ЛОГИКА ДЛЯ СПЛОШНЫХ ПЕРЕГОРОДОК ---
let pWidth = 0, pDepth = 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: effectiveHeight, bevelEnabled: false });
partGeo.rotateX(-Math.PI / 2);
partGeo.translate(pX, thickness, pY);
geometries.push(partGeo);
}
// Логика скруглений для перегородок (Оставляем сплошными для прочности/эстетики)
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) => {
const hEff = Math.max(0.1, h - thickness);
if (hEff <= 1) return;
const geo = new THREE.ExtrudeGeometry(filletShape, { depth: hEff, 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 as any], { type: 'application/octet-stream' });
const link = document.createElement('a');
link.href = URL.createObjectURL(blob);
link.download = filename;
link.click();
};