Files
BoxGenerator/src/services/geometryGenerator.ts
Халимов Рустам 03a787a459 4
2026-01-12 02:27:57 +03:00

249 lines
9.4 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, ADDITION, Brush, Evaluator } from 'three-bvh-csg';
import { mergeBufferGeometries } from 'three-stdlib';
import { AppConfig, LayoutSplits, GeneratedPart, Partition } from '../types';
// --- ВСПОМОГАТЕЛЬНЫЕ ФУНКЦИИ ---
const cleanPoints = (points: number[]) => {
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];
if (x2 - x1 < 0.001 || y2 - y1 < 0.001) continue;
const rawW = (x2 - x1) * config.drawer.width;
const rawD = (y2 - y1) * config.drawer.depth;
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();
evaluator.useGroups = false;
const wallH = height - thickness;
const innerW = width - 2 * thickness;
const innerD = depth - 2 * thickness;
// 1. Базовая геометрия - ПОЛ
const floorGeo = new THREE.BoxGeometry(width, thickness, depth);
floorGeo.translate(0, thickness / 2, 0);
let mainBrush = new Brush(floorGeo);
mainBrush.updateMatrixWorld();
// --- ФУНКЦИИ ОПЕРАЦИЙ ---
// Добавить твердое тело (стену/скругление)
const addSolid = (geo: THREE.BufferGeometry) => {
const brush = new Brush(geo);
brush.updateMatrixWorld();
mainBrush = evaluator.evaluate(mainBrush, brush, ADDITION);
};
// Вычесть отверстия для конкретной зоны
const subtractHoles = (W: number, H: number, startX: number, startY: number, startZ: number, axis: 'x' | 'z') => {
if (!safeConfig.perforation?.enabled) return;
const { pattern, diameter, spacing } = safeConfig.perforation;
const margin = 4;
const step = diameter + Math.max(2, spacing);
const cols = Math.floor((W - margin*2) / step);
const rowH = pattern === 'circle' ? step : step * 0.866;
const rows = Math.floor((H - margin*2) / rowH);
if (cols <= 0 || rows <= 0) return;
const offsetX = (W - cols * step) / 2;
const offsetY = (H - rows * rowH) / 2;
// Базовое сверло
const drillBase = new THREE.CylinderGeometry(diameter/2, diameter/2, thickness * 3, 12);
if (axis === 'x') drillBase.rotateX(Math.PI / 2); // Вдоль Z
else drillBase.rotateZ(Math.PI / 2); // Вдоль X
const drills: THREE.BufferGeometry[] = [];
for(let r=0; r<rows; r++) {
const isOdd = r % 2 !== 0;
for(let c=0; c<cols; c++) {
let u = offsetX + c * step + diameter/2;
let v = offsetY + r * rowH + diameter/2;
if ((pattern === 'hexagon' || pattern === 'triangle') && isOdd) u += step/2;
if (u > W - margin || v > H - margin) continue;
const drill = drillBase.clone();
if (axis === 'x') drill.translate(startX + u, startY + v, startZ);
else drill.translate(startX, startY + v, startZ + u);
drills.push(drill);
}
}
if (drills.length > 0) {
const mergedDrills = mergeBufferGeometries(drills);
if (mergedDrills) {
const drillBrush = new Brush(mergedDrills);
drillBrush.updateMatrixWorld();
mainBrush = evaluator.evaluate(mainBrush, drillBrush, SUBTRACTION);
}
}
};
// 2. СБОРКА СТЕН (ADDITION)
// Внешние стены
// Front
addSolid(new THREE.BoxGeometry(innerW, wallH, thickness).translate(0, thickness + wallH/2, depth/2 - thickness/2));
// Back
addSolid(new THREE.BoxGeometry(innerW, wallH, thickness).translate(0, thickness + wallH/2, -depth/2 + thickness/2));
// Left
addSolid(new THREE.BoxGeometry(thickness, wallH, depth).translate(-width/2 + thickness/2, thickness + wallH/2, 0));
// Right
addSolid(new THREE.BoxGeometry(thickness, wallH, depth).translate(width/2 - thickness/2, thickness + wallH/2, 0));
// Внутренние перегородки
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 (Math.abs(pMax - pMin) < 0.001) return;
let w=0, h=p.height, d=0, x=0, z=0;
if (p.axis === 'x') { // Vert (Z-axis)
w = thickness; d = (pMax - pMin) * innerD;
x = (-innerW/2) + (p.offset * innerW);
z = (-innerD/2) + ((pMin + pMax)/2 * innerD);
} else { // Horiz (X-axis)
w = (pMax - pMin) * innerW; d = thickness;
x = (-innerW/2) + ((pMin + pMax)/2 * innerW);
z = (-innerD/2) + (p.offset * innerD);
}
addSolid(new THREE.BoxGeometry(w, h, d).translate(x, thickness + h/2, z));
});
// 3. СКРУГЛЕНИЯ (ADDITION - цилиндры в углы)
if (radius > 0) {
const fRad = Math.min(radius, 5);
const filletBase = new THREE.CylinderGeometry(fRad, fRad, 1, 16);
filletBase.translate(0, 0.5, 0); // Pivot at bottom
partitions.forEach(p => {
if (!p.rounded) return;
const pMin = p.min ?? 0; const pMax = p.max ?? 1;
if (Math.abs(pMax - pMin) < 0.001) return;
const addF = (x: number, z: number) => {
const f = filletBase.clone();
f.scale(1, p.height, 1);
f.translate(x, thickness, z);
addSolid(f);
};
if (p.axis === 'x') { // Vert
const xPos = (-innerW/2) + (p.offset * innerW);
addF(xPos, (-innerD/2) + (pMin * innerD)); // Start Z
addF(xPos, (-innerD/2) + (pMax * innerD)); // End Z
} else { // Horiz
const zPos = (-innerD/2) + (p.offset * innerD);
addF((-innerW/2) + (pMin * innerW), zPos); // Start X
addF((-innerW/2) + (pMax * innerW), zPos); // End X
}
});
}
// 4. ПЕРФОРАЦИЯ (SUBTRACTION)
if (safeConfig.perforation?.enabled) {
// Внешние стены
subtractHoles(innerW, wallH, -innerW/2, thickness, depth/2, 'x'); // Front
subtractHoles(innerW, wallH, -innerW/2, thickness, -depth/2, 'x'); // Back
subtractHoles(depth, wallH, -width/2, thickness, -depth/2, 'z'); // Left
subtractHoles(depth, wallH, width/2, thickness, -depth/2, 'z'); // Right
// Внутренние перегородки
partitions.forEach(p => {
const pMin = p.min ?? 0; const pMax = p.max ?? 1;
if (Math.abs(pMax - pMin) < 0.001) return;
if (p.axis === 'x') {
const len = (pMax - pMin) * innerD;
const xPos = (-innerW/2) + (p.offset * innerW);
const zStart = (-innerD/2) + (pMin * innerD);
subtractHoles(len, p.height, xPos, thickness, zStart, 'z');
} else {
const len = (pMax - pMin) * innerW;
const xStart = (-innerW/2) + (pMin * innerW);
const zPos = (-innerD/2) + (p.offset * innerD);
subtractHoles(len, p.height, xStart, thickness, zPos, 'x');
}
});
}
return mainBrush.geometry;
};
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();
};