-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathgcode-parser.js
More file actions
186 lines (171 loc) · 8.34 KB
/
Copy pathgcode-parser.js
File metadata and controls
186 lines (171 loc) · 8.34 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
// G-code reader (.nc/.gcode/.ngc/.tap/.cnc/.din) -> internal entity model
// (same {line, arc, circle, point} shapes render.js/geometry.js already know),
// plus a "program" (per-line word breakdown + modal state) that gcode-writer.js
// uses to regenerate a transformed file while leaving everything else untouched.
//
// Assumptions (typical of PCB CAM output - FlatCAM/pycam/CopperCAM/Candle posts):
// - XY plane milling (G17), motion words G0/G1/G2/G3 and canned drill cycles
// G81/G82/G83 are tracked modally; any other G/M code is passed through as-is.
// - I/J are always incremental from the arc's start point (the common default;
// G90.1/G91.1 absolute-IJK mode is not special-cased).
// - Z is never transformed (it's depth, not an XY-plane coordinate). A
// vertical-only move (a line with Z but no X/Y - a plunge or retract) is
// treated as a drilled hole and exposed as a clickable 'point' entity too,
// same as G81/G82/G83 canned-cycle locations, deduped per XY spot.
window.TC = window.TC || {};
TC.gcode = (function () {
const WORD_RE = /([A-Za-z])\s*([+-]?(?:[0-9]*\.[0-9]+|[0-9]+\.?[0-9]*))/g;
const MOTION_G = new Set([0, 1, 2, 3, 80, 81, 82, 83]);
// Splits a raw line into {code, comment} - comment is everything from a ';'
// to end of line, or the (first) '(...)' group; code is the rest, comment text kept verbatim.
function splitComment(raw) {
const semi = raw.indexOf(';');
if (semi >= 0) return { code: raw.slice(0, semi), comment: raw.slice(semi) };
const paren = raw.indexOf('(');
if (paren >= 0) {
const close = raw.indexOf(')', paren);
if (close >= 0) return { code: raw.slice(0, paren) + raw.slice(close + 1), comment: raw.slice(paren, close + 1) };
}
return { code: raw, comment: '' };
}
// Words found in the code portion of a line, with char offsets into that string
// (needed by the writer to do precise in-place substring replacement).
function parseWords(code) {
const words = [];
WORD_RE.lastIndex = 0;
let m;
while ((m = WORD_RE.exec(code))) {
const letter = m[1].toUpperCase();
const value = parseFloat(m[2]);
if (Number.isNaN(value)) continue;
words.push({ letter, value, start: m.index, end: m.index + m[0].length });
}
return words;
}
function hypot(x, y) { return Math.sqrt(x * x + y * y); }
// Standard-form arc center from R (radius) format. Tries both candidate
// centers and picks whichever gives the sweep implied by sign(R) (>=0 -> <=180deg, <0 -> >180deg).
function arcCenterFromRadius(x1, y1, x2, y2, r, ccw) {
const dx = x2 - x1, dy = y2 - y1;
const q = hypot(dx, dy);
if (q < 1e-9) return { x: x1, y: y1 };
const absR = Math.abs(r);
const h2 = absR * absR - (q / 2) * (q / 2);
const h = Math.sqrt(Math.max(0, h2));
const mx = (x1 + x2) / 2, my = (y1 + y2) / 2;
const px = -dy / q, py = dx / q;
const candidates = [{ x: mx + px * h, y: my + py * h }, { x: mx - px * h, y: my - py * h }];
const wantLong = r < 0;
let best = candidates[0], bestScore = Infinity;
for (const c of candidates) {
let a0 = Math.atan2(y1 - c.y, x1 - c.x);
let a1 = Math.atan2(y2 - c.y, x2 - c.x);
if (ccw) { while (a1 <= a0) a1 += Math.PI * 2; }
else { while (a1 >= a0) a1 -= Math.PI * 2; }
const sweep = Math.abs(a1 - a0);
const isLong = sweep > Math.PI;
const score = (isLong === wantLong) ? 0 : 1;
if (score < bestScore) { bestScore = score; best = c; }
}
return best;
}
// Canonical CCW-swept angle pair (startAngle < endAngle) describing the
// physical arc's point-set, for the shared render/select entity model.
function ccwAnglePair(sx, sy, ex, ey, cx, cy, ccw) {
const isFull = hypot(ex - sx, ey - sy) < 1e-6 * Math.max(1, hypot(sx - cx, sy - cy));
let a0, a1;
if (ccw) {
a0 = Math.atan2(sy - cy, sx - cx);
a1 = isFull ? a0 + Math.PI * 2 : Math.atan2(ey - cy, ex - cx);
if (!isFull) while (a1 <= a0) a1 += Math.PI * 2;
} else {
a0 = Math.atan2(ey - cy, ex - cx);
a1 = isFull ? a0 + Math.PI * 2 : Math.atan2(sy - cy, sx - cx);
if (!isFull) while (a1 <= a0) a1 += Math.PI * 2;
}
return { startAngle: a0, endAngle: a1, isFull };
}
function parse(text) {
const rawLines = text.split(/\r\n|\r|\n/);
const lines = [];
const entities = [];
let id = 1;
let motion = null;
let distMode = 'abs';
let curX = 0, curY = 0, curZ = 0;
let sawXY = false; // becomes true once a real X/Y word has appeared - guards
// against treating a Z-only move before that (e.g. the
// near-universal safety-height "G0 Z..." at the very top
// of a file, before any XY has ever been given) as a
// drilled hole at the parser's arbitrary (0,0) default
const drillXY = new Set(); // dedupes vertical-only (Z) moves at the same XY
for (let li = 0; li < rawLines.length; li++) {
const raw = rawLines[li];
const { code } = splitComment(raw);
const words = parseWords(code);
let lineMotion = null;
for (const w of words) {
if (w.letter !== 'G') continue;
const g = w.value;
if (g === 90) distMode = 'abs';
else if (g === 91) distMode = 'inc';
else if (MOTION_G.has(g)) lineMotion = g;
}
const effMotion = lineMotion != null ? lineMotion : motion;
if (lineMotion != null) motion = lineMotion;
const wx = words.find(w => w.letter === 'X');
const wy = words.find(w => w.letter === 'Y');
const wi = words.find(w => w.letter === 'I');
const wj = words.find(w => w.letter === 'J');
const wr = words.find(w => w.letter === 'R');
const wz = words.find(w => w.letter === 'Z');
const hasXY = !!(wx || wy);
const startX = curX, startY = curY, startZ = curZ;
const newX = wx ? (distMode === 'inc' ? curX + wx.value : wx.value) : curX;
const newY = wy ? (distMode === 'inc' ? curY + wy.value : wy.value) : curY;
const newZ = wz ? (distMode === 'inc' ? curZ + wz.value : wz.value) : curZ;
const rec = { lineIndex: li, raw, words, distMode, hasXY, startX, startY, startZ, endX: newX, endY: newY, endZ: newZ, arc: null, effMotion };
if (hasXY && (effMotion === 2 || effMotion === 3)) {
const ccw = effMotion === 3;
let cx, cy;
if (wi || wj) {
cx = startX + (wi ? wi.value : 0);
cy = startY + (wj ? wj.value : 0);
} else if (wr) {
const c = arcCenterFromRadius(startX, startY, newX, newY, wr.value, ccw);
cx = c.x; cy = c.y;
} else {
cx = startX; cy = startY;
}
const r = hypot(startX - cx, startY - cy);
rec.arc = { cx, cy, r, ccw };
if (r > 1e-9) {
const { startAngle, endAngle, isFull } = ccwAnglePair(startX, startY, newX, newY, cx, cy, ccw);
if (isFull) entities.push({ id: id++, type: 'circle', layer: 'gcode', cx, cy, r });
else entities.push({ id: id++, type: 'arc', layer: 'gcode', cx, cy, r, startAngle, endAngle });
}
} else if (hasXY && (effMotion === 0 || effMotion === 1 || effMotion == null)) {
if (startX !== newX || startY !== newY) {
entities.push({ id: id++, type: 'line', layer: 'gcode', x1: startX, y1: startY, x2: newX, y2: newY, rapid: effMotion === 0 });
}
} else if (hasXY && (effMotion === 81 || effMotion === 82 || effMotion === 83)) {
entities.push({ id: id++, type: 'point', layer: 'gcode', x: newX, y: newY });
drillXY.add(startX.toFixed(6) + ',' + startY.toFixed(6));
} else if (!hasXY && wz && sawXY && effMotion !== 2 && effMotion !== 3) {
// Vertical-only move (plunge or retract) at a fixed XY - typically a
// drilled hole even without a canned cycle. One reference point per
// distinct location (dedup covers plunge+retract at the same spot).
const key = curX.toFixed(6) + ',' + curY.toFixed(6);
if (!drillXY.has(key)) {
drillXY.add(key);
entities.push({ id: id++, type: 'point', layer: 'gcode', x: curX, y: curY });
}
}
if (hasXY) { curX = newX; curY = newY; sawXY = true; }
curZ = newZ;
lines.push(rec);
}
return { entities, program: { lines, rawText: text } };
}
return { parse, splitComment, parseWords, arcCenterFromRadius, ccwAnglePair };
})();