Files
BoxGenerator/src/services/geometryGenerator.ts
Халимов Рустам 58cfc0f8e4 9
2026-01-12 03:09:51 +03:00

360 lines
15 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 } from '../types';
// --- ВСПОМОГАТЕЛЬНЫЕ ФУНКЦИИ ---
// Очистка только для визуализации (цветные кубики), чтобы не рябило в глазах
const cleanPointsForVisuals = (points: number[]) => {
const rounded = points.map(p => Math.round(p * 1000) / 1000).sort((a, b) => a - b);
return [...new Set(rounded)];
};
// --- 1. ВИЗУАЛИЗАЦИЯ (ЦВЕТНЫЕ БЛОКИ) ---
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 = cleanPointsForVisuals([0, ...safeX, 1]);
const uniqueY = cleanPointsForVisuals([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 - config.wallThickness,
x: rawX + gap,
y: rawY + gap,
color: `hsl(${(partCounter * 137.5) % 360}, 70%, 50%)`,
internalPartitions: []
});
partCounter++;
}
}
return parts;
};
// --- ГЕНЕРАЦИЯ ГЕОМЕТРИИ (СТРОГО ПО ДАННЫМ) ---
// Функция создания 2D профиля стены с отверстиями
const createWallProfile = (width: number, height: number, config: AppConfig): THREE.Shape => {
const shape = new THREE.Shape();
// 1. Внешний контур (CCW - Против часовой)
shape.moveTo(0, 0);
shape.lineTo(width, 0);
shape.lineTo(width, height);
shape.lineTo(0, height);
shape.lineTo(0, 0);
// Проверка на включение перфорации
if (!config.perforation?.enabled || width < 10 || height < 10) return shape;
const { pattern, diameter, spacing } = config.perforation;
const step = diameter + Math.max(2, spacing);
const margin = 3; // Отступ от краев
const effW = width - margin * 2;
const effH = height - margin * 2;
if (effW <= diameter || effH <= diameter) return shape;
// Сетка отверстий
const rowH = pattern === 'circle' ? step : step * 0.866;
const cols = Math.floor(effW / step);
const rows = Math.floor(effH / rowH);
// Центрирование
const startX = margin + (effW - (cols - 1) * step) / 2;
const startY = margin + (effH - (rows - 1) * rowH) / 2;
for (let j = 0; j < rows; j++) {
const isOdd = j % 2 !== 0;
const cy = startY + j * rowH;
for (let i = 0; i < cols; i++) {
let cx = startX + i * step;
// Смещение для сот/треугольников
if ((pattern === 'hexagon' || pattern === 'triangle') && isOdd) cx += step / 2;
// Проверка границ (чтобы дырка не вылезла за край)
if (cx - diameter/2 < margin || cx + diameter/2 > width - margin ||
cy - diameter/2 < margin || cy + diameter/2 > height - margin) continue;
const hole = new THREE.Path();
const r = diameter / 2;
// 2. Дырки (CW - По часовой стрелке). ВАЖНО!
if (pattern === 'circle') {
hole.absarc(cx, cy, r, 0, Math.PI * 2, true);
}
else if (pattern === 'hexagon') {
for (let k = 0; k < 6; k++) {
const angle = (-k * 60 + 90) * Math.PI / 180;
const px = cx + r * Math.cos(angle);
const py = cy + 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 = cx + r * Math.cos(angle);
const py = cy + r * Math.sin(angle);
if (k === 0) hole.moveTo(px, py); else hole.lineTo(px, py);
}
hole.closePath();
}
shape.holes.push(hole);
}
}
return shape;
};
// Обычный прямоугольник для пола
const createRectShape = (w: number, d: number): THREE.Shape => {
const s = new THREE.Shape();
s.moveTo(0,0); s.lineTo(w,0); s.lineTo(w,d); s.lineTo(0,d); s.lineTo(0,0);
return s;
};
// Цилиндр для скругления
const createFilletGeo = (radius: number, height: number) => {
const r = Math.min(radius, 5);
const geo = new THREE.CylinderGeometry(r, r, height, 16);
// Центрируем по Y, чтобы ставить от пола
geo.translate(0, height/2, 0);
return geo;
};
// --- СБОРЩИК ГЕОМЕТРИИ ---
export const createBinGeometry = (
width: number, depth: number, height: number, thickness: number, radius: number = 0,
splits: LayoutSplits | Partition[] = [],
config?: AppConfig
): THREE.BufferGeometry => {
const geometries: THREE.BufferGeometry[] = [];
const safeConfig = config || { perforation: { enabled: false } } as AppConfig;
// 1. ПОЛ
// Используем простую геометрию для пола
const floorGeo = new THREE.BoxGeometry(width, thickness, depth);
floorGeo.translate(width/2, thickness/2, depth/2); // Сдвигаем в 0..W, 0..D систему
// Но стоп, у нас система координат: центр ящика в 0,0,0? Или угол в 0,0,0?
// В calculateParts мы используем абсолютные значения (0..width).
// Давайте строить всё от угла (0,0,0) - так проще считать координаты.
// Сбрасываем позицию пола: центр (W/2, T/2, D/2)
floorGeo.center();
floorGeo.translate(width/2, thickness/2, depth/2); // Угол (0,0,0) - это левый задний угол пола
geometries.push(floorGeo);
const wallH = height - thickness;
const innerW = width - 2 * thickness;
const innerD = depth - 2 * thickness;
// Хелпер для установки стен
const addWall = (len: number, h: number, x: number, z: number, isVert: boolean) => {
// 2D профиль с дырками
const shape = createWallProfile(len, h, safeConfig);
const geo = new THREE.ExtrudeGeometry(shape, { depth: thickness, bevelEnabled: false });
// По умолчанию: Shape в XY (0..L, 0..H), Extrude в Z (0..T)
if (isVert) {
// Вертикальная (идет вдоль Z)
geo.rotateY(Math.PI / 2); // Теперь идет вдоль Z (0..L), толщина вдоль X (0..T)
// Позиция: X, Z.
// Начало: (0,0,0) -> повернулось.
// Нам нужно поставить начало стены в (x, thickness, z).
geo.translate(x, thickness, z);
} else {
// Горизонтальная (идет вдоль X)
// X = длина, Y = высота, Z = толщина.
// Нам нужно Z центрировать? Нет, обычно стенки имеют толщину.
// Ставим как есть.
geo.translate(x, thickness, z);
}
geometries.push(geo);
};
// 2. ВНЕШНИЕ СТЕНЫ (Коробка)
// Используем систему координат 0..Width, 0..Depth
// Задняя (вдоль X)
// X=thickness (внутри левой стены), Z=0
// Длина = innerW
addWall(innerW, wallH, thickness, 0, false);
// Передняя (вдоль X)
// X=thickness, Z=depth-thickness
addWall(innerW, wallH, thickness, depth - thickness, false);
// Левая (вдоль Z)
// X=thickness (сдвиг из-за поворота), Z=0.
// При повороте на 90: (0,0,0) -> (0,0,0). Длина ушла в -Z? Или +Z?
// RotateY(PI/2): X->Z, Z->-X.
// Shape (L, 0, 0) -> (0, 0, -L). Стенка ушла в минус по Z.
// Нам нужно чтобы шла в плюс. RotateY(-PI/2).
// Исправим хелпер для поворота:
// Если RotateY(-PI/2): X->-Z.
// Давайте проще: создадим и сдвинем.
// LEFT (Полная глубина)
const leftGeo = new THREE.ExtrudeGeometry(createWallProfile(depth, wallH, safeConfig), { depth: thickness, bevelEnabled: false });
leftGeo.rotateY(Math.PI / 2); // Вдоль Z
// После +90: начало (0,0,0). Длина вдоль -Z. Толщина вдоль -X.
// Нам нужно начало в (0,0,0). Стенка должна идти в +Z.
// rotateY(-PI/2) -> Длина в +Z. Толщина в +X.
leftGeo.rotateY(-Math.PI); // Коррекция
// Теперь она смотрит куда надо.
leftGeo.translate(thickness, thickness, 0);
// Стоп, это сложно угадать.
// ДАВАЙТЕ ПРОЩЕ: Центрируем каждую стену и ставим по центру.
// Это 100% рабочий метод.
const placeCenteredWall = (len: number, h: number, centerX: number, centerZ: number, isVert: boolean) => {
const shape = createWallProfile(len, h, safeConfig);
const geo = new THREE.ExtrudeGeometry(shape, { depth: thickness, bevelEnabled: false });
geo.center(); // Центр в (0,0,0)
if (isVert) geo.rotateY(Math.PI / 2);
// Ставим: Y = thickness + h/2
geo.translate(centerX, thickness + h/2, centerZ);
geometries.push(geo);
};
// Пересчет центров для внешних стен:
// Центр пола: W/2, D/2.
placeCenteredWall(innerW, wallH, width/2, thickness/2, false); // Back (Z=thick/2)
placeCenteredWall(innerW, wallH, width/2, depth - thickness/2, false); // Front
placeCenteredWall(depth, wallH, thickness/2, depth/2, true); // Left
placeCenteredWall(depth, wallH, width - thickness/2, depth/2, true); // Right
// 3. ВНУТРЕННИЕ ПЕРЕГОРОДКИ (Связь с данными редактора)
// Берем исходные данные о сетке для расчета точных позиций
const splitX = [0, ...(Array.isArray(splits?.x) ? splits.x : []), 1].sort((a,b)=>a-b);
const splitY = [0, ...(Array.isArray(splits?.y) ? splits.y : []), 1].sort((a,b)=>a-b);
const partitionsObj = splits.partitions || {};
// Проходим по всем ячейкам
Object.keys(partitionsObj).forEach(key => {
const parts = partitionsObj[key];
if (!parts || parts.length === 0) return;
const [iStr, jStr] = key.split('-');
const i = parseInt(iStr);
const j = parseInt(jStr);
// Получаем границы ячейки (0..1)
const x1 = splitX[i];
const x2 = splitX[i+1];
const y1 = splitY[j];
const y2 = splitY[j+1];
if (x2 === undefined || y2 === undefined) return;
// Конвертируем в миллиметры
const cellX = x1 * width;
const cellY = y1 * depth;
const cellW = (x2 - x1) * width;
const cellD = (y2 - y1) * depth;
parts.forEach(p => {
const pMin = p.min ?? 0;
const pMax = p.max ?? 1;
if (Math.abs(pMax - pMin) < 0.001) return;
let len = 0, cx = 0, cz = 0, isVert = false;
if (p.axis === 'x') { // Vert (Z)
isVert = true;
len = (pMax - pMin) * cellD;
// X центр: начало ячейки + смещение
cx = cellX + (p.offset * cellW);
// Z центр: начало ячейки + середина отрезка стены
const midRatio = (pMin + pMax) / 2;
cz = cellY + (midRatio * cellD);
} else { // Horiz (X)
isVert = false;
len = (pMax - pMin) * cellW;
const midRatio = (pMin + pMax) / 2;
cx = cellX + (midRatio * cellW);
cz = cellY + (p.offset * cellD);
}
placeCenteredWall(len, p.height, cx, cz, isVert);
// Скругления
if (p.rounded && radius > 0) {
const r = Math.min(radius, 5);
const fGeo = createFilletGeo(r, p.height);
// Определяем концы
if (isVert) {
const zStart = cz - len/2;
const zEnd = cz + len/2;
// Добавляем цилиндры в концы (подняв на пол)
const c1 = fGeo.clone(); c1.translate(cx, thickness, zStart); geometries.push(c1);
const c2 = fGeo.clone(); c2.translate(cx, thickness, zEnd); geometries.push(c2);
} else {
const xStart = cx - len/2;
const xEnd = cx + len/2;
const c1 = fGeo.clone(); c1.translate(xStart, thickness, cz); geometries.push(c1);
const c2 = fGeo.clone(); c2.translate(xEnd, thickness, cz); geometries.push(c2);
}
}
});
});
// 4. СЛИЯНИЕ
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();
};