137 lines
4.7 KiB
TypeScript
137 lines
4.7 KiB
TypeScript
import * as THREE from 'three';
|
|
import { STLExporter, mergeBufferGeometries } from 'three-stdlib';
|
|
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,
|
|
splits: LayoutSplits
|
|
): 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++) {
|
|
|
|
const segmentX = xPoints[i] * config.drawer.width;
|
|
const segmentY = yPoints[j] * config.drawer.depth;
|
|
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.
|
|
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) {
|
|
continue;
|
|
}
|
|
|
|
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%)`
|
|
});
|
|
partCounter++;
|
|
}
|
|
}
|
|
|
|
return parts;
|
|
};
|
|
|
|
/**
|
|
* 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.
|
|
*/
|
|
export const createBinGeometry = (
|
|
width: number,
|
|
depth: number,
|
|
height: number,
|
|
thickness: number
|
|
): 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);
|
|
|
|
// 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);
|
|
}
|
|
|
|
// Merge geometries into a single mesh for clean STL export
|
|
const merged = mergeBufferGeometries(geometries);
|
|
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' });
|
|
const link = document.createElement('a');
|
|
link.href = URL.createObjectURL(blob);
|
|
link.download = filename;
|
|
link.click();
|
|
}; |