import { generateCuttingList } from './cuttingEngine.js'; import { collectHoleSpecsForPiece } from './holeSpecifications.js'; import { GROOVE, getBackGroovePlanOrientation } from '../../shared/manufacturing.js'; function toSafeNumber(value, fallback = 0) { const n = Number(value); return Number.isFinite(n) ? n : fallback; } function toLabel(value) { return String(value || '') .replace(/[^a-zA-Z0-9_\-]+/g, '_') .replace(/_+/g, '_') .replace(/^_+|_+$/g, '') .toUpperCase(); } function emitPanelOutline(lines, widthMm, heightMm, safeZ, cutZ, feed) { lines.push(`G0 Z${safeZ.toFixed(3)}`); lines.push('G0 X0.000 Y0.000'); lines.push(`G1 Z${cutZ.toFixed(3)} F${Math.round(feed)}`); lines.push(`G1 X${widthMm.toFixed(3)} Y0.000`); lines.push(`G1 X${widthMm.toFixed(3)} Y${heightMm.toFixed(3)}`); lines.push(`G1 X0.000 Y${heightMm.toFixed(3)}`); lines.push('G1 X0.000 Y0.000'); lines.push(`G0 Z${safeZ.toFixed(3)}`); } function emitHole(lines, { xMm, yMm, diameterMm = 5, depthMm = 10, type = 'generic', face = 'face', end = null }) { let tail = `TYPE=${String(type)} FACE=${face}`; if (end) tail += ` END=${end}`; lines.push( `; HOLE X=${Number(xMm).toFixed(3)} Y=${Number(yMm).toFixed(3)} D=${Number(diameterMm).toFixed(3)} Z=${Number(depthMm).toFixed(3)} ${tail}` ); } function emitPieceHoleComments(lines, piece, widthMm, heightMm, partIndex = 0) { const { specs, thicknessMm: _tm } = collectHoleSpecsForPiece(piece, widthMm, heightMm, partIndex); for (const spec of specs) { emitHole(lines, spec); } const m = piece?.machining || {}; const orient = piece?.elementType ? getBackGroovePlanOrientation(piece.elementType) : null; if (m.groove && orient) { const gw = Number(m.groove.width ?? GROOVE.back.width); const gd = Number(m.groove.depth ?? GROOVE.back.depth); const ax = Number(m.groove.axisFromPanelEdge ?? m.groove.axisFromBackEdge ?? GROOVE.back.axisFromPanelEdge); lines.push(`; GROOVE BACK W=${gw.toFixed(3)} D=${gd.toFixed(3)} AXIS=${ax.toFixed(3)} ORIENT=${orient}`); } } export function generateCncProgram(config, modules = [], colors = {}) { const cuttingList = generateCuttingList(config, colors, modules); const safeZ = 10; const cutDepth = 3; const feedCut = 1800; const lines = [ '%', '; APEX CNC ISO (G-code) - v1', `; Generated: ${new Date().toISOString()}`, `; Config: ${config.widthMm}x${config.heightMm}x${config.depthMm} mm`, 'G21 ; metric units', 'G90 ; absolute coordinates', 'G17 ; XY plane', `G0 Z${safeZ.toFixed(3)}` ]; let programPartCount = 0; for (const piece of cuttingList.pieces || []) { const widthMm = toSafeNumber(piece.rawCutWidthMm, piece.widthMm); const heightMm = toSafeNumber(piece.rawCutHeightMm, piece.heightMm); const qty = Math.max(1, Math.round(toSafeNumber(piece.quantity, 1))); const thickness = toSafeNumber(piece.thicknessMm, 19); const pieceName = toLabel(piece.description || piece.elementType || 'PIECE'); const targetDepth = -Math.min(cutDepth, Math.max(1, thickness * 0.25)); for (let i = 0; i < qty; i++) { programPartCount += 1; lines.push(''); lines.push(`; --- PART ${programPartCount}: ${pieceName} #${i + 1} ---`); lines.push(`; SIZE_RAW: X=${widthMm.toFixed(3)} Y=${heightMm.toFixed(3)} T=${thickness.toFixed(3)}`); emitPieceHoleComments(lines, piece, widthMm, heightMm, i); emitPanelOutline(lines, widthMm, heightMm, safeZ, targetDepth, feedCut); } } lines.push(''); lines.push(`G0 Z${safeZ.toFixed(3)}`); lines.push('G0 X0.000 Y0.000'); lines.push('M30'); lines.push('%'); return { fileName: 'apex-program.nc', mimeType: 'text/plain; charset=utf-8', content: lines.join('\n'), metadata: { generatedAt: new Date().toISOString(), parts: programPartCount } }; } export default { generateCncProgram };