Files
BoxGenerator/src/services/geometryGenerator.ts
Халимов Рустам e90348ab32 Use three-bvh-csg
2026-01-12 01:35:02 +03:00

246 lines
11 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 } from 'three-stdlib';
import { SUBTRACTION, UNION, Brush, Evaluator } from 'three-bvh-csg';
import { AppConfig, LayoutSplits, GeneratedPart, Partition } from '../types';
// --- ВСПОМОГАТЕЛЬНЫЕ ФУНКЦИИ ---
// Функция для очистки дубликатов точек (убирает фантомные микро-ячейки)
const cleanPoints = (points: number[]) => {
// Округляем до 3 знака и сортируем
const rounded = points.map(p => Math.round(p * 1000) / 1000).sort((a, b) => a - b);
// Убираем дубликаты
return [...new Set(rounded)];
};
// Получение списка всех перегородок
const getAllPartitions = (splits: LayoutSplits): Partition[] => {
if (!splits || !splits.partitions) return [];
return Object.values(splits.partitions).flat();
};
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]);
let partCounter = 1;
for (let i = 0; i < uniqueX.length - 1; i++) {
for (let j = 0; j < uniqueY.length - 1; j++) {
const x1 = uniqueX[i];
const x2 = uniqueX[i+1];
const y1 = uniqueY[j];
const y2 = uniqueY[j+1];
const rawW = (x2 - x1) * config.drawer.width;
const rawD = (y2 - y1) * config.drawer.depth;
// Игнорируем слишком мелкие ячейки (защита от фантомов)
if (rawW < 2 || rawD < 2) continue;
const rawX = x1 * config.drawer.width;
const rawY = y1 * config.drawer.depth;
// Отступ для визуализации (цветные кубики внутри ячеек)
const gap = config.wallThickness / 2 + 0.1;
parts.push({
id: `part-${partCounter}`,
name: `Ячейка ${partCounter}`,
width: Math.max(1, rawW - gap * 2),
depth: Math.max(1, rawD - gap * 2),
height: config.drawer.height,
x: rawX + gap,
y: rawY + gap,
color: `hsl(${(partCounter * 137.5) % 360}, 70%, 50%)`,
internalPartitions: []
});
partCounter++;
}
}
return parts;
};
// --- CSG ГЕОМЕТРИЯ ---
export const createBinGeometry = (
width: number, depth: number, height: number, thickness: number, radius: number = 0,
splits: LayoutSplits | Partition[] = [],
config?: AppConfig
): THREE.BufferGeometry => {
const safeConfig = config || { perforation: { enabled: false } } as AppConfig;
const evaluator = new Evaluator();
// 1. БАЗОВАЯ ГЕОМЕТРИЯ (ПОЛ)
// Brush - это специальный объект для CSG операций
const floorGeo = new THREE.BoxGeometry(width, thickness, depth);
floorGeo.translate(0, thickness / 2, 0); // Поднимаем на уровень пола
let resultBrush = new Brush(floorGeo);
// Материал для CSG (нужен для вычислений, но не влияет на экспорт)
resultBrush.updateMatrixWorld();
// 2. СТЕНКИ (ВНЕШНИЕ)
const wallH = height - thickness;
const innerW = width - 2 * thickness;
const innerD = depth - 2 * thickness;
// Функция создания блока стены
const addWallBlock = (w: number, h: number, d: number, x: number, y: number, z: number) => {
const geo = new THREE.BoxGeometry(w, h, d);
geo.translate(x, y, z);
const wallBrush = new Brush(geo);
wallBrush.updateMatrixWorld();
// Объединяем (UNION) стену с полом
resultBrush = evaluator.evaluate(resultBrush, wallBrush, UNION);
};
// Передняя и Задняя (Вдоль X)
addWallBlock(innerW, wallH, thickness, 0, thickness + wallH/2, depth/2 - thickness/2); // Front
addWallBlock(innerW, wallH, thickness, 0, thickness + wallH/2, -depth/2 + thickness/2); // Back
// Левая и Правая (Вдоль Z) - Полная глубина, перекрывают углы
addWallBlock(thickness, wallH, depth, -width/2 + thickness/2, thickness + wallH/2, 0); // Left
addWallBlock(thickness, wallH, depth, width/2 - thickness/2, thickness + wallH/2, 0); // Right
// 3. ВНУТРЕННИЕ ПЕРЕГОРОДКИ
let partitions: Partition[] = [];
if (Array.isArray(splits)) {
partitions = splits;
} else if (splits && splits.partitions) {
partitions = getAllPartitions(splits);
}
partitions.forEach(p => {
const pMin = p.min ?? 0;
const pMax = p.max ?? 1;
if (pMax - pMin < 0.001) return;
let w=0, h=p.height, d=0, x=0, z=0;
if (p.axis === 'x') {
// Вертикальная на 2D (Вдоль Z в 3D)
w = thickness;
d = (pMax - pMin) * innerD;
x = (-innerW/2) + (p.offset * innerW);
z = (-innerD/2) + (pMin * innerD) + (d / 2);
} else {
// Горизонтальная на 2D (Вдоль X в 3D)
w = (pMax - pMin) * innerW;
d = thickness;
x = (-innerW/2) + (pMin * innerW) + (w / 2);
z = (-innerD/2) + (p.offset * innerD);
}
addWallBlock(w, h, d, x, thickness + h/2, z);
});
// 4. ПЕРФОРАЦИЯ (ВЫЧИТАНИЕ)
// Чтобы не тормозить, мы создаем ОДИН сложный объект из всех "сверл" и вычитаем его один раз
if (safeConfig.perforation?.enabled) {
const { pattern, diameter, spacing } = safeConfig.perforation;
const holeGeo = new THREE.CylinderGeometry(diameter/2, diameter/2, thickness * 4, 16);
// Поворачиваем цилиндр, чтобы он "сверлил" вдоль оси X (для боковых стенок)
holeGeo.rotateZ(Math.PI / 2);
const step = diameter + Math.max(2, spacing);
const margin = 4;
// Массив геометрий для слияния (это быстрее, чем 1000 раз вызывать CSG)
const cutters: THREE.BufferGeometry[] = [];
// Функция генерации "сверл" для плоскости
const generateCutters = (W: number, H: number, startX: number, startY: number, startZ: number, rotateY: boolean) => {
const cols = Math.floor((W - margin*2) / step);
const rowH = pattern === 'circle' ? step : step * 0.866;
const rows = Math.floor((H - margin*2) / rowH);
const offsetX = (W - cols * step) / 2;
const offsetY = (H - rows * rowH) / 2;
for(let j=0; j<rows; j++) {
const isOdd = j % 2 !== 0;
for(let i=0; i<cols; i++) {
let hx = offsetX + i * step + diameter/2;
let hy = offsetY + j * rowH + diameter/2;
if ((pattern === 'hexagon' || pattern === 'triangle') && isOdd) hx += step/2;
if (hx > W - margin || hy > H - margin) continue;
const cutter = holeGeo.clone();
if (rotateY) {
// Для стенок, идущих вдоль X (Передняя/Задняя)
// Изначально цилиндр вдоль X. Поворачиваем на 90 -> Вдоль Z.
cutter.rotateY(Math.PI / 2);
// Позиционируем
// В локальной системе стенки: X=Длина, Y=Высота.
// Глобально: X=startX+hx, Y=startY+hy, Z=startZ
cutter.translate(startX + hx, startY + hy, startZ);
} else {
// Для стенок, идущих вдоль Z (Левая/Правая)
// Цилиндр вдоль X (по умолчанию).
// Глобально: X=startX, Y=startY+hy, Z=startZ+hx
cutter.translate(startX, startY + hy, startZ + hx);
}
cutters.push(cutter);
}
}
};
// Генерируем сверла для всех 4 сторон
// Front/Back (Сверлим вдоль Z)
generateCutters(innerW, wallH, -innerW/2, thickness, depth/2, true); // Front plane
generateCutters(innerW, wallH, -innerW/2, thickness, -depth/2, true); // Back plane
// Left/Right (Сверлим вдоль X)
// Для боковых стенок (Left/Right) W = depth.
generateCutters(depth, wallH, -width/2, thickness, -depth/2, false); // Left plane
generateCutters(depth, wallH, width/2, thickness, -depth/2, false); // Right plane
// Если есть внутренние стенки, их тоже надо бы сверлить, но это сложнее рассчитать.
// Пока сверлим только внешний периметр, как в Gridfinity.
// (Можно добавить логику для внутренних, перебирая partitions, если нужно)
if (cutters.length > 0) {
// Объединяем все сверла в один Mesh
// Используем mergeBufferGeometries из three-stdlib, так как в чистом three его вынесли
const mergedCutters = mergeBufferGeometries(cutters);
if (mergedCutters) {
const cutterBrush = new Brush(mergedCutters);
cutterBrush.updateMatrixWorld();
// ВЫЧИТАНИЕ (SUBTRACTION)
resultBrush = evaluator.evaluate(resultBrush, cutterBrush, SUBTRACTION);
}
}
}
// Возвращаем чистую геометрию
return resultBrush.geometry;
};
export const generateSTL = (mesh: THREE.Object3D): Uint8Array | string => {
const exporter = new STLExporter();
// Для CSG геометрии иногда нужно убедиться, что она корректно интерпретируется
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();
};