Files
BoxGenerator/src/services/geometryGenerator.ts
Халимов Рустам 94eb2f00b2 6
2026-01-11 14:26:00 +03:00

264 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, mergeBufferGeometries } from 'three-stdlib';
import { AppConfig, LayoutSplits, GeneratedPart, Partition } from '../types';
// Типы для внутреннего решателя
type Limits = { min: number; max: number };
type LimitMap = Record<string, Limits>;
// --- SOLVER: Итеративный расчет границ ---
// Эта функция гарантирует, что стенки не будут торчать друг из друга
const solveWallLimits = (partitions: Partition[]): LimitMap => {
const limits: LimitMap = {};
// 1. Инициализация: считаем, что все стенки идут от 0 до 1
partitions.forEach(p => { limits[p.id] = { min: 0, max: 1 }; });
// 2. Итеративное решение (3 прохода достаточно для любой вложенности)
for (let pass = 0; pass < 3; pass++) {
partitions.forEach(target => {
let newMin = 0;
let newMax = 1;
const center = target.offset; // Положение стенки
partitions.forEach(obstacle => {
if (target.id === obstacle.id) return;
if (target.axis === obstacle.axis) return; // Параллельные не мешают
// Берем АКТУАЛЬНЫЕ границы препятствия с прошлого шага
const obsMin = limits[obstacle.id].min;
const obsMax = limits[obstacle.id].max;
// Пересекает ли препятствие путь нашей стенки?
// obstacle.offset - это позиция препятствия на нашем пути.
// target.offset - это наша позиция. Она должна быть ВНУТРИ длины препятствия.
if (target.offset > obsMin && target.offset < obsMax) {
// Препятствие на пути. Где оно?
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}`,
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 createFilletShape = (radius: number): THREE.Shape => {
const shape = new THREE.Shape();
// Рисуем в 1-м квадранте
shape.moveTo(0, 0);
shape.lineTo(radius, 0);
shape.absarc(radius, radius, radius, 1.5 * Math.PI, 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[] = []
): THREE.BufferGeometry => {
const geometries: THREE.BufferGeometry[] = [];
// 1. ДНО
const floorShape = createRoundedRectShape(width, depth, radius);
const floorGeo = new THREE.ExtrudeGeometry(floorShape, { depth: thickness, bevelEnabled: false });
floorGeo.rotateX(-Math.PI / 2);
geometries.push(floorGeo);
// 2. ВНЕШНИЕ СТЕНКИ
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);
// 3. ВНУТРЕННИЕ ПЕРЕГОРОДКИ (С SOLVER'ом)
const limitMap = solveWallLimits(partitions);
partitions.forEach(p => {
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;
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.2);
const partGeo = new THREE.ExtrudeGeometry(partShape, { depth: p.height, bevelEnabled: false });
partGeo.rotateX(-Math.PI / 2);
partGeo.translate(pX, thickness, pY);
geometries.push(partGeo);
// --- ДОБАВЛЕНИЕ СКРУГЛЕНИЙ (FILLETS) ---
if (p.rounded && radius > 1) {
const filletR = Math.min(radius, 5);
const filletShape = createFilletShape(filletR);
const filletExtrude = { depth: p.height, bevelEnabled: false };
const addFillet = (x: number, y: number, rotY: number) => {
const geo = new THREE.ExtrudeGeometry(filletShape, filletExtrude);
geo.rotateX(-Math.PI / 2);
geo.rotateY(rotY);
geo.translate(x, thickness, y);
geometries.push(geo);
};
const h = thickness / 2; // half thickness
if (p.axis === 'x') {
// Вертикальная (идет вдоль Z в 3D)
const startY = (-innerDepth / 2) + (innerDepth * pMin); // "Верхний" конец (Back)
const endY = (-innerDepth / 2) + (innerDepth * pMax); // "Нижний" конец (Front)
// Проверяем, упирается ли она в края или другие стенки? (всегда рисуем, так как solver обрезал)
// Скругляем 4 угла стыка
// Top End (pMin)
addFillet(pX - h, startY, 0); // Слева, смотрит вверх
addFillet(pX + h, startY, -Math.PI/2); // Справа, смотрит вверх
// Bottom End (pMax)
addFillet(pX - h, endY, Math.PI/2); // Слева, смотрит вниз
addFillet(pX + h, endY, Math.PI); // Справа, смотрит вниз
} else {
// Горизонтальная (идет вдоль X в 3D)
const startX = (-innerWidth / 2) + (innerWidth * pMin); // Левый конец
const endX = (-innerWidth / 2) + (innerWidth * pMax); // Правый конец
// Left End (pMin)
addFillet(startX, pY + h, Math.PI); // Сверху, смотрит влево (FIXED ANGLE)
addFillet(startX, pY - h, Math.PI/2); // Снизу, смотрит влево (FIXED ANGLE)
// Right End (pMax)
addFillet(endX, pY + h, -Math.PI/2); // Сверху, смотрит вправо (FIXED ANGLE)
addFillet(endX, pY - h, 0); // Снизу, смотрит вправо (FIXED ANGLE)
}
}
});
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();
};