218 lines
8.3 KiB
TypeScript
218 lines
8.3 KiB
TypeScript
import * as THREE from 'three';
|
|
import { STLExporter, mergeBufferGeometries } from 'three-stdlib';
|
|
import { AppConfig, LayoutSplits, GeneratedPart, Partition } 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;
|
|
};
|
|
|
|
export const createBinGeometry = (
|
|
width: number, depth: number, height: number, thickness: number, radius: number = 0, partitions: Partition[] = []
|
|
): THREE.BufferGeometry => {
|
|
const geometries: THREE.BufferGeometry[] = [];
|
|
|
|
// ДНО И ВНЕШНИЕ СТЕНКИ
|
|
const floorShape = createRoundedRectShape(width, depth, radius);
|
|
const floorGeo = new THREE.ExtrudeGeometry(floorShape, { depth: thickness, bevelEnabled: false });
|
|
floorGeo.rotateX(-Math.PI / 2);
|
|
geometries.push(floorGeo);
|
|
|
|
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);
|
|
|
|
// ВНУТРЕННИЕ ПЕРЕГОРОДКИ (СТРОГО ПО ДАННЫМ, БЕЗ SOLVER)
|
|
partitions.forEach(p => {
|
|
// Берем данные напрямую. Если в 2D нарисовано от 0.2 до 0.8, тут будет 0.2 до 0.8.
|
|
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);
|
|
|
|
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.1);
|
|
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 = 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) => {
|
|
if (h <= 1) return;
|
|
const geo = new THREE.ExtrudeGeometry(filletShape, { depth: h, 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], { type: 'application/octet-stream' });
|
|
const link = document.createElement('a');
|
|
link.href = URL.createObjectURL(blob);
|
|
link.download = filename;
|
|
link.click();
|
|
}; |