import type { Vec2, Segment } from '../types'; import { flattenArc } from '../geometry/arc'; export interface ParseResult { segments: Segment[]; warnings: string[]; } interface State { x: number; y: number; z: number; absolute: boolean; scale: number; // 1 for mm, 25.4 for inch motion: number; // 0,1,2,3 } const WORD_RE = /([A-Za-z])\s*([-+]?[0-9]*\.?[0-9]+)/g; function stripComments(line: string): string { return line.replace(/\(.*?\)/g, '').replace(/;.*$/, '').trim(); } export function parseGcode(text: string): ParseResult { const segments: Segment[] = []; const warnings: string[] = []; const st: State = { x: 0, y: 0, z: 0, absolute: true, scale: 1, motion: 0 }; const lines = text.split(/\r?\n/); for (let lineNo = 0; lineNo < lines.length; lineNo++) { const clean = stripComments(lines[lineNo]!); if (!clean) continue; const words: Array<[string, number]> = []; let m: RegExpExecArray | null; WORD_RE.lastIndex = 0; while ((m = WORD_RE.exec(clean)) !== null) { words.push([m[1]!.toUpperCase(), parseFloat(m[2]!)]); } const axis: Record = {}; let motionThisLine: number | null = null; for (const [letter, value] of words) { switch (letter) { case 'G': if (value === 0 || value === 1 || value === 2 || value === 3) { st.motion = value; motionThisLine = value; } else if (value === 20) st.scale = 25.4; else if (value === 21) st.scale = 1; else if (value === 90) st.absolute = true; else if (value === 91) st.absolute = false; // G17/G18/G19 plane select and others: ignored (G17/XY assumed) break; case 'X': case 'Y': case 'Z': case 'I': case 'J': case 'R': axis[letter] = value * st.scale; break; // F, S, T, M, N etc. ignored default: break; } } const hasCoord = 'X' in axis || 'Y' in axis || 'Z' in axis; if (motionThisLine === null && !hasCoord) continue; // no movement on this line const start: Vec2 = [st.x, st.y]; const target = resolveTarget(st, axis); switch (st.motion) { case 0: segments.push({ kind: 'rapid', points: [start, target] }); break; case 1: segments.push({ kind: 'cut', points: [start, target] }); break; case 2: case 3: { const center = arcCenter(start, target, axis, st.motion === 2); if (!center) { warnings.push(`Line ${lineNo + 1}: could not resolve arc centre`); segments.push({ kind: 'cut', points: [start, target] }); } else { segments.push({ kind: 'cut', points: flattenArc(start, target, center, st.motion === 2) }); } break; } } st.x = target[0]; st.y = target[1]; if ('Z' in axis) st.z = axis['Z']!; } return { segments, warnings }; } function resolveTarget(st: State, axis: Record): Vec2 { if (st.absolute) { return ['X' in axis ? axis['X']! : st.x, 'Y' in axis ? axis['Y']! : st.y] as Vec2; } return [st.x + (axis['X'] ?? 0), st.y + (axis['Y'] ?? 0)] as Vec2; } function arcCenter(start: Vec2, end: Vec2, axis: Record, clockwise: boolean): Vec2 | null { if ('I' in axis || 'J' in axis) { // I/J are offsets from the start point (already unit-scaled). return [start[0] + (axis['I'] ?? 0), start[1] + (axis['J'] ?? 0)]; } if ('R' in axis) { return centerFromRadius(start, end, axis['R']!, clockwise); } return null; } /** GRBL-style centre-from-radius. R>0 selects the minor arc, R<0 the major arc. */ function centerFromRadius(start: Vec2, end: Vec2, r: number, clockwise: boolean): Vec2 | null { const x = end[0] - start[0]; const y = end[1] - start[1]; const d2 = x * x + y * y; const disc = 4 * r * r - d2; if (disc < 0 || d2 === 0) return null; let h = Math.sqrt(disc) / Math.sqrt(d2); if (clockwise) h = -h; if (r < 0) h = -h; return [start[0] + 0.5 * (x - y * h), start[1] + 0.5 * (y + x * h)]; }