127 lines
4 KiB
TypeScript
127 lines
4 KiB
TypeScript
|
|
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<string, number> = {};
|
||
|
|
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<string, number>): 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<string, number>, 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)];
|
||
|
|
}
|