Add corner

This commit is contained in:
Халимов Рустам
2025-12-27 20:44:50 +03:00
parent 3c047e222b
commit f43c92bb44
6 changed files with 169 additions and 126 deletions

View File

@@ -4,7 +4,6 @@ import { AppConfig, LayoutSplits, GeneratedPart } from '../types';
/**
* Calculates the final list of bins based on layout.
* Removes printer bed constraints logic.
*/
export const calculateParts = (
config: AppConfig,
@@ -12,14 +11,11 @@ export const calculateParts = (
): GeneratedPart[] => {
const parts: GeneratedPart[] = [];
// 1. Sort splits to create segments
// CRITICAL FIX: Create a copy of the array before sorting to avoid mutating React state
const xPoints = [0, ...[...splits.x].sort((a, b) => a - b), 1];
const yPoints = [0, ...[...splits.y].sort((a, b) => a - b), 1];
let partCounter = 1;
// 2. Iterate through the grid defined by the user
for (let i = 0; i < xPoints.length - 1; i++) {
for (let j = 0; j < yPoints.length - 1; j++) {
@@ -28,16 +24,13 @@ export const calculateParts = (
const segmentW = (xPoints[i + 1] - xPoints[i]) * config.drawer.width;
const segmentD = (yPoints[j + 1] - yPoints[j]) * config.drawer.depth;
// Apply Printer Tolerance (Gap)
// We subtract the tolerance from the width/depth and shift the position
// so the gap is evenly distributed around the part center.
// Apply Printer Tolerance
const realWidth = segmentW - config.printerTolerance;
const realDepth = segmentD - config.printerTolerance;
const realX = segmentX + (config.printerTolerance / 2);
const realY = segmentY + (config.printerTolerance / 2);
// Filter out extremely small parts (likely errors/double lines or consumed by tolerance)
if (realWidth < 1 || realDepth < 1) {
if (realWidth < 5 || realDepth < 5) {
continue;
}
@@ -59,74 +52,120 @@ export const calculateParts = (
};
/**
* Generates a Three.js Geometry for a hollow bin.
* The floor is at y=0..thickness.
* The walls sit on top of the floor, y=thickness..height.
* Helper to create a rounded rectangle Shape
*/
const createRoundedRectShape = (width: number, height: number, radius: number): THREE.Shape => {
const shape = new THREE.Shape();
const x = -width / 2;
const y = -height / 2;
// Clamp radius to not exceed half of width or height
const r = Math.min(radius, width / 2, height / 2);
if (r <= 0) {
// Regular rectangle
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 {
// Rounded rectangle
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;
}
/**
* Generates a Three.js Geometry for a hollow bin with fillets (rounded corners).
*/
export const createBinGeometry = (
width: number,
depth: number,
height: number,
thickness: number
thickness: number,
radius: number = 0 // Default radius
): THREE.BufferGeometry => {
const geometries: THREE.BufferGeometry[] = [];
// 1. Floor (Base)
// Positioned at the bottom (0 to thickness)
const floorGeo = new THREE.BoxGeometry(width, thickness, depth);
floorGeo.translate(0, thickness / 2, 0);
geometries.push(floorGeo);
// 1. Создаем форму ДНА (Floor)
// Это сплошной прямоугольник со скруглениями
const floorShape = createRoundedRectShape(width, depth, radius);
const floorGeo = new THREE.ExtrudeGeometry(floorShape, {
depth: thickness, // Толщина дна
bevelEnabled: false,
curveSegments: 12 // Гладкость скругления
});
// ExtrudeGeometry создает объект "лежа" на XY, нам нужно повернуть его, чтобы он стал дном (XZ)
floorGeo.rotateX(Math.PI / 2);
// Сдвигаем на высоту thickness, так как extrude идет в +Z (после поворота это +Y, но перевернуто... проще подобрать)
// По умолчанию extrude создает от 0 до Z. После поворота X(90deg):
// Z становится -Y. То есть дно уходит вниз от 0.
// Нам нужно, чтобы дно было от 0 до thickness по Y.
// Возвращаем поворот в -90 (стандарт для топологии)
floorGeo.rotateX(-Math.PI);
floorGeo.translate(0, thickness, 0); // Поднимаем, чтобы верх дна был на уровне thickness
// Wall dimensions
// Walls sit ON TOP of the floor to avoid overlap/z-fighting
const wallHeight = height - thickness;
const wallCenterY = thickness + (wallHeight / 2);
if (wallHeight > 0) {
// 2. Left & Right Walls (Full depth)
const wallLRGeo = new THREE.BoxGeometry(thickness, wallHeight, depth);
const leftWall = wallLRGeo.clone();
leftWall.translate(-(width/2) + (thickness/2), wallCenterY, 0);
geometries.push(leftWall);
const rightWall = wallLRGeo.clone();
rightWall.translate((width/2) - (thickness/2), wallCenterY, 0);
geometries.push(rightWall);
// 3. Front & Back Walls (Width minus LR thickness to fit between)
// Width is reduced by 2*thickness
const wallFBWidth = Math.max(0, width - (2 * thickness));
const wallFBGeo = new THREE.BoxGeometry(wallFBWidth, wallHeight, thickness);
const frontWall = wallFBGeo.clone();
frontWall.translate(0, wallCenterY, (depth/2) - (thickness/2));
geometries.push(frontWall);
const backWall = wallFBGeo.clone();
backWall.translate(0, wallCenterY, -(depth/2) + (thickness/2));
geometries.push(backWall);
// 2. Создаем форму СТЕНОК (Walls)
// Это внешняя форма МИНУС внутренняя форма (дырка)
const outerShape = createRoundedRectShape(width, depth, radius);
// Внутренний радиус должен быть меньше внешнего на толщину стенки, но не меньше 0
const innerRadius = Math.max(0, radius - thickness);
const innerWidth = width - (2 * thickness);
const innerDepth = depth - (2 * thickness);
if (innerWidth > 0 && innerDepth > 0) {
const innerHole = createRoundedRectShape(innerWidth, innerDepth, innerRadius);
outerShape.holes.push(innerHole);
}
// Merge geometries into a single mesh for clean STL export
const merged = mergeBufferGeometries(geometries);
const wallHeight = height - thickness;
const wallGeo = new THREE.ExtrudeGeometry(outerShape, {
depth: wallHeight,
bevelEnabled: false,
curveSegments: 12
});
// Поворачиваем стенки так же, как дно
wallGeo.rotateX(Math.PI / 2);
wallGeo.rotateX(-Math.PI);
// Стенки начинаются ОТ дна (y = thickness)
wallGeo.translate(0, thickness + wallHeight, 0);
// Объединяем геометрии
const merged = mergeBufferGeometries([floorGeo, wallGeo]);
// Центрируем геометрию, чтобы Pivot был в центре дна (как раньше в BinMesh)
// Ранее мы использовали BoxGeometry который центрирован.
// Extrude создает форму вокруг (0,0), так что по X/Z она уже центрирована.
// По Y она сейчас от 0 до height.
// Вернем как было: Pivot внизу.
return merged || new THREE.BoxGeometry(1, 1, 1);
};
// Generates STL binary data without triggering download
export const generateSTL = (mesh: THREE.Object3D): Uint8Array | string => {
const exporter = new STLExporter();
const result = exporter.parse(mesh, { binary: true });
// JSZip requires Uint8Array or ArrayBuffer, not DataView
if (result instanceof DataView) {
return new Uint8Array(result.buffer, result.byteOffset, result.byteLength);
}
return result as string;
};
// Triggers download immediately
export const exportSTL = (mesh: THREE.Object3D, filename: string) => {
const result = generateSTL(mesh);
const blob = new Blob([result], { type: 'application/octet-stream' });