cyclecad 3.10.4 → 3.12.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/workflows/pages.yml +34 -0
- package/.nojekyll +0 -0
- package/CLAUDE.md +348 -3
- package/HANDOFF-2026-04-24-session-2.md +239 -0
- package/HANDOFF-2026-04-24.md +90 -0
- package/app/index.html +49 -10
- package/app/js/modules/ai-copilot.js +195 -2
- package/app/js/modules/ai-engineer.js +939 -0
- package/app/js/modules/pentacad-bridge.js +216 -0
- package/app/js/modules/pentacad-cam.js +184 -0
- package/app/js/modules/pentacad-sim.js +215 -0
- package/app/js/modules/pentacad.js +233 -0
- package/app/pentacad.html +240 -0
- package/cyclecad.html +1081 -0
- package/explodeview.html +1102 -0
- package/index-agent-first.html.bak +1306 -0
- package/index.html +1683 -1240
- package/machines/v2-50-chb/kinematics.json +51 -0
- package/mockups/cyclecad-suite-mockup.html +1746 -0
- package/package.json +1 -1
- package/pentacad.html +1097 -0
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file pentacad-bridge.js
|
|
3
|
+
* @description Pentacad machine-bridge client. Connects the browser to the
|
|
4
|
+
* controller-bridge service (running on LAN next to the machine)
|
|
5
|
+
* over WebSocket, streams G-code downstream, and receives live
|
|
6
|
+
* DRO / spindle / probe / alarm data upstream.
|
|
7
|
+
*
|
|
8
|
+
* Scope for Phase 3:
|
|
9
|
+
* - WebSocket connect/reconnect with token auth
|
|
10
|
+
* - G-code streaming with pause/resume/abort
|
|
11
|
+
* - Jog, feed override, spindle override
|
|
12
|
+
* - DRO readback (X/Y/Z/A/B)
|
|
13
|
+
* - Alarm/status channel
|
|
14
|
+
* - E-stop is ALWAYS hardware-first — the bridge cannot override
|
|
15
|
+
*
|
|
16
|
+
* @version 0.1.0
|
|
17
|
+
* @author Sachin Kumar <vvlars@googlemail.com>
|
|
18
|
+
* @license AGPL-3.0-only
|
|
19
|
+
* @module pentacad-bridge
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
'use strict';
|
|
23
|
+
|
|
24
|
+
window.CycleCAD = window.CycleCAD || {};
|
|
25
|
+
|
|
26
|
+
window.CycleCAD.PentacadBridge = (() => {
|
|
27
|
+
const VERSION = '0.1.0';
|
|
28
|
+
const DEFAULT_URL = 'ws://localhost:7777';
|
|
29
|
+
|
|
30
|
+
// ============================================================================
|
|
31
|
+
// STATE
|
|
32
|
+
// ============================================================================
|
|
33
|
+
|
|
34
|
+
const bridge = {
|
|
35
|
+
ws: null,
|
|
36
|
+
url: DEFAULT_URL,
|
|
37
|
+
status: 'disconnected', // disconnected | connecting | connected | error
|
|
38
|
+
dro: { x: 0, y: 0, z: 0, a: 0, b: 0 },
|
|
39
|
+
spindle: { rpm: 0, load: 0 },
|
|
40
|
+
alarms: [],
|
|
41
|
+
lastPing: 0,
|
|
42
|
+
reconnectAttempts: 0,
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
// ============================================================================
|
|
46
|
+
// CONNECT
|
|
47
|
+
// ============================================================================
|
|
48
|
+
|
|
49
|
+
function connect(url) {
|
|
50
|
+
if (url) bridge.url = url;
|
|
51
|
+
if (bridge.ws) bridge.ws.close();
|
|
52
|
+
|
|
53
|
+
setStatus('connecting');
|
|
54
|
+
try {
|
|
55
|
+
bridge.ws = new WebSocket(bridge.url);
|
|
56
|
+
} catch (e) {
|
|
57
|
+
setStatus('error');
|
|
58
|
+
console.error('[pentacad-bridge] WebSocket creation failed:', e);
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
bridge.ws.addEventListener('open', () => {
|
|
63
|
+
setStatus('connected');
|
|
64
|
+
bridge.reconnectAttempts = 0;
|
|
65
|
+
console.log(`[pentacad-bridge] Connected to ${bridge.url}`);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
bridge.ws.addEventListener('message', (e) => {
|
|
69
|
+
try {
|
|
70
|
+
const msg = JSON.parse(e.data);
|
|
71
|
+
handleMessage(msg);
|
|
72
|
+
} catch (err) {
|
|
73
|
+
console.warn('[pentacad-bridge] Bad message:', e.data);
|
|
74
|
+
}
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
bridge.ws.addEventListener('close', () => {
|
|
78
|
+
setStatus('disconnected');
|
|
79
|
+
// Auto-reconnect with exponential backoff
|
|
80
|
+
const delay = Math.min(1000 * (2 ** bridge.reconnectAttempts), 30000);
|
|
81
|
+
bridge.reconnectAttempts++;
|
|
82
|
+
setTimeout(() => {
|
|
83
|
+
if (bridge.status === 'disconnected') connect(bridge.url);
|
|
84
|
+
}, delay);
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
bridge.ws.addEventListener('error', (e) => {
|
|
88
|
+
setStatus('error');
|
|
89
|
+
console.error('[pentacad-bridge] WebSocket error', e);
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function disconnect() {
|
|
94
|
+
if (bridge.ws) bridge.ws.close();
|
|
95
|
+
bridge.ws = null;
|
|
96
|
+
setStatus('disconnected');
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function setStatus(status) {
|
|
100
|
+
bridge.status = status;
|
|
101
|
+
window.dispatchEvent(new CustomEvent('pentacad:bridge-status', {
|
|
102
|
+
detail: { status, url: bridge.url },
|
|
103
|
+
}));
|
|
104
|
+
if (ctx?.state) ctx.state.bridgeStatus = status;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// ============================================================================
|
|
108
|
+
// MESSAGE HANDLING (upstream from bridge)
|
|
109
|
+
// ============================================================================
|
|
110
|
+
|
|
111
|
+
function handleMessage(msg) {
|
|
112
|
+
switch (msg.type) {
|
|
113
|
+
case 'dro':
|
|
114
|
+
bridge.dro = { ...bridge.dro, ...msg.position };
|
|
115
|
+
emit('dro', bridge.dro);
|
|
116
|
+
break;
|
|
117
|
+
case 'spindle':
|
|
118
|
+
bridge.spindle = { ...bridge.spindle, ...msg };
|
|
119
|
+
emit('spindle', bridge.spindle);
|
|
120
|
+
break;
|
|
121
|
+
case 'alarm':
|
|
122
|
+
bridge.alarms.push({ time: Date.now(), ...msg });
|
|
123
|
+
emit('alarm', msg);
|
|
124
|
+
break;
|
|
125
|
+
case 'pong':
|
|
126
|
+
bridge.lastPing = Date.now();
|
|
127
|
+
break;
|
|
128
|
+
default:
|
|
129
|
+
emit('message', msg);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function emit(eventName, detail) {
|
|
134
|
+
window.dispatchEvent(new CustomEvent(`pentacad:${eventName}`, { detail }));
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// ============================================================================
|
|
138
|
+
// DOWNSTREAM (browser → bridge → machine)
|
|
139
|
+
// ============================================================================
|
|
140
|
+
|
|
141
|
+
function send(obj) {
|
|
142
|
+
if (bridge.status !== 'connected') {
|
|
143
|
+
console.warn('[pentacad-bridge] Not connected — message dropped');
|
|
144
|
+
return false;
|
|
145
|
+
}
|
|
146
|
+
bridge.ws.send(JSON.stringify(obj));
|
|
147
|
+
return true;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function streamGCode(gcode, options = {}) {
|
|
151
|
+
// TODO Phase 3: real drip-feed with ack per block
|
|
152
|
+
const lines = gcode.split(/\r?\n/).filter(l => l && !l.startsWith('(') && l !== '%');
|
|
153
|
+
console.log(`[pentacad-bridge] Streaming ${lines.length} G-code blocks (stub)`);
|
|
154
|
+
return send({ type: 'stream', lines, options });
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function jog(axis, delta, feed) {
|
|
158
|
+
return send({ type: 'jog', axis, delta, feed });
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function feedOverride(percent) {
|
|
162
|
+
return send({ type: 'feed-override', percent });
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function pause() { return send({ type: 'pause' }); }
|
|
166
|
+
function resume() { return send({ type: 'resume' }); }
|
|
167
|
+
function abort() { return send({ type: 'abort' }); }
|
|
168
|
+
|
|
169
|
+
// ============================================================================
|
|
170
|
+
// INIT
|
|
171
|
+
// ============================================================================
|
|
172
|
+
|
|
173
|
+
let ctx = null;
|
|
174
|
+
|
|
175
|
+
function init(context) {
|
|
176
|
+
ctx = context;
|
|
177
|
+
console.log(`[pentacad-bridge] v${VERSION} initialized — default URL ${DEFAULT_URL}`);
|
|
178
|
+
// Do NOT auto-connect; the user must pick a machine and click connect.
|
|
179
|
+
// Autoconnecting on page load gets flagged as suspicious by some LAN setups.
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function execute(request) {
|
|
183
|
+
const { method, params } = request || {};
|
|
184
|
+
if (method === 'bridge.connect') return connect(params?.url);
|
|
185
|
+
if (method === 'bridge.disconnect') return disconnect();
|
|
186
|
+
if (method === 'bridge.status') return bridge.status;
|
|
187
|
+
if (method === 'bridge.dro') return bridge.dro;
|
|
188
|
+
if (method === 'bridge.stream') return streamGCode(params.gcode, params.options);
|
|
189
|
+
if (method === 'bridge.jog') return jog(params.axis, params.delta, params.feed);
|
|
190
|
+
if (method === 'bridge.feedOverride') return feedOverride(params.percent);
|
|
191
|
+
if (method === 'bridge.pause') return pause();
|
|
192
|
+
if (method === 'bridge.resume') return resume();
|
|
193
|
+
if (method === 'bridge.abort') return abort();
|
|
194
|
+
return { error: 'unknown_bridge_method', method };
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// ============================================================================
|
|
198
|
+
// PUBLIC API
|
|
199
|
+
// ============================================================================
|
|
200
|
+
|
|
201
|
+
return {
|
|
202
|
+
version: VERSION,
|
|
203
|
+
init,
|
|
204
|
+
execute,
|
|
205
|
+
connect,
|
|
206
|
+
disconnect,
|
|
207
|
+
streamGCode,
|
|
208
|
+
jog,
|
|
209
|
+
feedOverride,
|
|
210
|
+
pause,
|
|
211
|
+
resume,
|
|
212
|
+
abort,
|
|
213
|
+
getStatus: () => bridge.status,
|
|
214
|
+
getDRO: () => bridge.dro,
|
|
215
|
+
};
|
|
216
|
+
})();
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file pentacad-cam.js
|
|
3
|
+
* @description Pentacad CAM sub-module — 3+2 machining strategies + post-processor.
|
|
4
|
+
* Part of the cycleCAD Suite / Pentacad extension.
|
|
5
|
+
*
|
|
6
|
+
* Scope for Phase 2:
|
|
7
|
+
* - 12 strategies: 2D contour, adaptive, pocket, drill, parallel,
|
|
8
|
+
* radial, scallop, projection, flow, bore/thread, chamfer, face
|
|
9
|
+
* - 3+2 setup manager: tilt plane selection, WCS, stock, fixture
|
|
10
|
+
* - Toolpath generation
|
|
11
|
+
* - Post-processor emitting Pentamachine .ngc dialect
|
|
12
|
+
* - Tool library integration
|
|
13
|
+
*
|
|
14
|
+
* @version 0.1.0
|
|
15
|
+
* @author Sachin Kumar <vvlars@googlemail.com>
|
|
16
|
+
* @license AGPL-3.0-only
|
|
17
|
+
* @module pentacad-cam
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
'use strict';
|
|
21
|
+
|
|
22
|
+
window.CycleCAD = window.CycleCAD || {};
|
|
23
|
+
|
|
24
|
+
window.CycleCAD.PentacadCAM = (() => {
|
|
25
|
+
const VERSION = '0.1.0';
|
|
26
|
+
|
|
27
|
+
// ============================================================================
|
|
28
|
+
// STRATEGIES
|
|
29
|
+
// ============================================================================
|
|
30
|
+
|
|
31
|
+
const STRATEGIES = [
|
|
32
|
+
{ id: '2d-contour', name: '2D Contour', kind: '2d' },
|
|
33
|
+
{ id: 'adaptive-clear', name: 'Adaptive Clear', kind: '2d' },
|
|
34
|
+
{ id: 'pocket', name: 'Pocket', kind: '2d' },
|
|
35
|
+
{ id: 'drill', name: 'Drill', kind: 'drill' },
|
|
36
|
+
{ id: 'parallel', name: 'Parallel', kind: '3d' },
|
|
37
|
+
{ id: 'radial', name: 'Radial', kind: '3d' },
|
|
38
|
+
{ id: 'scallop', name: 'Scallop', kind: '3d' },
|
|
39
|
+
{ id: 'projection', name: 'Projection', kind: '3d' },
|
|
40
|
+
{ id: 'flow', name: 'Flow', kind: '3d' },
|
|
41
|
+
{ id: 'bore-thread', name: 'Bore / Thread', kind: 'drill' },
|
|
42
|
+
{ id: 'chamfer-deburr', name: 'Chamfer / Deburr', kind: '2d' },
|
|
43
|
+
{ id: 'face', name: 'Face', kind: '2d' },
|
|
44
|
+
];
|
|
45
|
+
|
|
46
|
+
// ============================================================================
|
|
47
|
+
// POST-PROCESSOR (Pentamachine V2 dialect — reverse-engineered from samples)
|
|
48
|
+
// ============================================================================
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Emits G-code in the Pentamachine V2 dialect.
|
|
52
|
+
*
|
|
53
|
+
* Confirmed features from sample .ngc files:
|
|
54
|
+
* - G20 (inch) units default; G21 (metric) supported via post config
|
|
55
|
+
* - G17 (XY workplane)
|
|
56
|
+
* - G90 (absolute), G91.1 (incremental IJK)
|
|
57
|
+
* - G40 (no cutter comp)
|
|
58
|
+
* - G94 (feed per minute) / G93 (inverse time, used during 5-axis moves)
|
|
59
|
+
* - 5-axis: X, Y, Z, A (tilt), B (rotary)
|
|
60
|
+
* - Tool change: Tn M6 (preceded by M5, followed by M3 S<rpm>)
|
|
61
|
+
* - Coolant: M7 (mist), M8 (flood), M9 (off)
|
|
62
|
+
* - WCS: G54 default
|
|
63
|
+
*
|
|
64
|
+
* @param {Array<Toolpath>} toolpaths
|
|
65
|
+
* @param {object} machine — loaded machine definition
|
|
66
|
+
* @returns {string} G-code text
|
|
67
|
+
*/
|
|
68
|
+
function emitGCode(toolpaths, machine) {
|
|
69
|
+
const lines = [];
|
|
70
|
+
const meta = {
|
|
71
|
+
program: machine?.post?.programNumber ?? '1000',
|
|
72
|
+
units: machine?.post?.units ?? 'inch',
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
// Program header
|
|
76
|
+
lines.push('%');
|
|
77
|
+
lines.push('(AXIS,stop)');
|
|
78
|
+
lines.push(`(${meta.program})`);
|
|
79
|
+
lines.push('(PENTACAD GENERATED — Pentamachine V2 dialect)');
|
|
80
|
+
|
|
81
|
+
let n = 10;
|
|
82
|
+
const emit = (code) => { lines.push(`N${n} ${code}`); n += 5; };
|
|
83
|
+
|
|
84
|
+
// Modal setup
|
|
85
|
+
emit(meta.units === 'metric' ? 'G21' : 'G20');
|
|
86
|
+
emit('G90 G94 G40 G17 G91.1');
|
|
87
|
+
emit('G53 G0 Z0.');
|
|
88
|
+
|
|
89
|
+
// Emit each toolpath
|
|
90
|
+
for (const tp of toolpaths) {
|
|
91
|
+
lines.push(`(${tp.name ?? tp.strategy ?? 'OP'})`);
|
|
92
|
+
emit('M9');
|
|
93
|
+
emit('G49');
|
|
94
|
+
emit('M5');
|
|
95
|
+
if (tp.tool) {
|
|
96
|
+
emit(`T${tp.tool.number} M6`);
|
|
97
|
+
emit(`S${tp.tool.rpm} M3`);
|
|
98
|
+
}
|
|
99
|
+
emit('G54 G0');
|
|
100
|
+
// Moves — stub until Phase 2 implements real toolpaths
|
|
101
|
+
for (const m of tp.moves ?? []) {
|
|
102
|
+
if (m.type === 'rapid') emit(`G0 ${axisStr(m)}`);
|
|
103
|
+
if (m.type === 'linear') emit(`G1 ${axisStr(m)} F${m.feed ?? 10}`);
|
|
104
|
+
if (m.type === 'arc') emit(`${m.dir === 'cw' ? 'G2' : 'G3'} ${axisStr(m)} I${m.i} J${m.j} F${m.feed ?? 10}`);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Program footer
|
|
109
|
+
emit('M9');
|
|
110
|
+
emit('G49');
|
|
111
|
+
emit('M5');
|
|
112
|
+
emit('G53 G0 Z0.');
|
|
113
|
+
emit('M30');
|
|
114
|
+
lines.push('%');
|
|
115
|
+
return lines.join('\n');
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function axisStr(m) {
|
|
119
|
+
const parts = [];
|
|
120
|
+
['X', 'Y', 'Z', 'A', 'B'].forEach(a => {
|
|
121
|
+
if (m[a.toLowerCase()] !== undefined) parts.push(`${a}${m[a.toLowerCase()].toFixed(4)}`);
|
|
122
|
+
});
|
|
123
|
+
return parts.join(' ');
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// ============================================================================
|
|
127
|
+
// TOOLPATH GENERATION (stubs — real implementation in Phase 2)
|
|
128
|
+
// ============================================================================
|
|
129
|
+
|
|
130
|
+
function generateToolpath(operation, setup, machine) {
|
|
131
|
+
const strategy = STRATEGIES.find(s => s.id === operation.strategyId);
|
|
132
|
+
if (!strategy) throw new Error(`Unknown strategy: ${operation.strategyId}`);
|
|
133
|
+
return {
|
|
134
|
+
strategy: strategy.id,
|
|
135
|
+
name: `${strategy.name} — ${setup.name}`,
|
|
136
|
+
tool: operation.tool,
|
|
137
|
+
moves: [], // TODO Phase 2: real toolpath
|
|
138
|
+
warnings: [`${strategy.id} toolpath generation is a Phase 2 deliverable`],
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// ============================================================================
|
|
143
|
+
// INIT
|
|
144
|
+
// ============================================================================
|
|
145
|
+
|
|
146
|
+
let ctx = null;
|
|
147
|
+
|
|
148
|
+
function init(context) {
|
|
149
|
+
ctx = context;
|
|
150
|
+
console.log(`[pentacad-cam] v${VERSION} initialized — ${STRATEGIES.length} strategies registered`);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function execute(request) {
|
|
154
|
+
const { method, params } = request || {};
|
|
155
|
+
if (method === 'cam.listStrategies') return STRATEGIES;
|
|
156
|
+
if (method === 'cam.generate') {
|
|
157
|
+
const toolpaths = (ctx?.state?.operations ?? []).map(op => {
|
|
158
|
+
const setup = ctx.state.setups.find(s => s.id === op.setupId);
|
|
159
|
+
return generateToolpath(op, setup, ctx.state.machine);
|
|
160
|
+
});
|
|
161
|
+
ctx.state.toolpaths = toolpaths;
|
|
162
|
+
return toolpaths;
|
|
163
|
+
}
|
|
164
|
+
if (method === 'cam.post') {
|
|
165
|
+
const gcode = emitGCode(ctx.state.toolpaths, ctx.state.machine);
|
|
166
|
+
ctx.state.gcode = gcode;
|
|
167
|
+
return gcode;
|
|
168
|
+
}
|
|
169
|
+
return { error: 'unknown_cam_method', method };
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// ============================================================================
|
|
173
|
+
// PUBLIC API
|
|
174
|
+
// ============================================================================
|
|
175
|
+
|
|
176
|
+
return {
|
|
177
|
+
version: VERSION,
|
|
178
|
+
init,
|
|
179
|
+
execute,
|
|
180
|
+
STRATEGIES,
|
|
181
|
+
generateToolpath,
|
|
182
|
+
emitGCode,
|
|
183
|
+
};
|
|
184
|
+
})();
|
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file pentacad-sim.js
|
|
3
|
+
* @description Pentacad 5-axis G-code simulator. Parses G-code, runs forward
|
|
4
|
+
* kinematics against a loaded machine definition, and animates
|
|
5
|
+
* the tool + work envelope in the cycleCAD Three.js scene.
|
|
6
|
+
*
|
|
7
|
+
* Scope for Phase 1:
|
|
8
|
+
* - G-code parser (modal state, G0/G1/G2/G3, G93, WCS)
|
|
9
|
+
* - 5-axis forward kinematics (axis angles → tool-tip XYZ)
|
|
10
|
+
* - Soft-limit detection
|
|
11
|
+
* - Material-removal simulation (voxel or dexel)
|
|
12
|
+
* - Collision detection (tool, holder, fixture)
|
|
13
|
+
*
|
|
14
|
+
* Acceptance test: replay samples/ring-aluminum-v2-50/*.ngc.
|
|
15
|
+
*
|
|
16
|
+
* @version 0.1.0
|
|
17
|
+
* @author Sachin Kumar <vvlars@googlemail.com>
|
|
18
|
+
* @license AGPL-3.0-only
|
|
19
|
+
* @module pentacad-sim
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
'use strict';
|
|
23
|
+
|
|
24
|
+
window.CycleCAD = window.CycleCAD || {};
|
|
25
|
+
|
|
26
|
+
window.CycleCAD.PentacadSim = (() => {
|
|
27
|
+
const VERSION = '0.1.0';
|
|
28
|
+
|
|
29
|
+
// ============================================================================
|
|
30
|
+
// G-CODE PARSER (skeleton — Phase 1 fills this out)
|
|
31
|
+
// ============================================================================
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Parses a G-code file line-by-line into a sequence of motion commands.
|
|
35
|
+
* Maintains modal state (feed mode, distance mode, active plane, active WCS).
|
|
36
|
+
*
|
|
37
|
+
* @param {string} gcode full G-code text
|
|
38
|
+
* @returns {Array<Move>} { type, x?, y?, z?, a?, b?, feed?, rapid?, arcCenter? }
|
|
39
|
+
*/
|
|
40
|
+
function parse(gcode) {
|
|
41
|
+
const moves = [];
|
|
42
|
+
const modal = {
|
|
43
|
+
distance: 'G90', // absolute
|
|
44
|
+
plane: 'G17', // XY
|
|
45
|
+
units: 'G20', // inch
|
|
46
|
+
feedMode: 'G94', // per-min
|
|
47
|
+
wcs: 'G54',
|
|
48
|
+
tool: null,
|
|
49
|
+
spindle: 0,
|
|
50
|
+
coolant: 'off',
|
|
51
|
+
lastPos: { x: 0, y: 0, z: 0, a: 0, b: 0 },
|
|
52
|
+
};
|
|
53
|
+
const warnings = [];
|
|
54
|
+
|
|
55
|
+
const lines = gcode.split(/\r?\n/);
|
|
56
|
+
for (let i = 0; i < lines.length; i++) {
|
|
57
|
+
const raw = lines[i].trim();
|
|
58
|
+
if (!raw || raw.startsWith('(') || raw.startsWith(';') || raw === '%') continue;
|
|
59
|
+
|
|
60
|
+
// Strip line number (Nxxx)
|
|
61
|
+
const body = raw.replace(/^N\d+\s*/, '');
|
|
62
|
+
// Collect tokens of the form LetterNumber (G1, X1.23, F10, etc.)
|
|
63
|
+
const tokens = body.match(/[A-Z][-+]?\d*\.?\d+/g) || [];
|
|
64
|
+
const t = {};
|
|
65
|
+
for (const tok of tokens) t[tok[0]] = parseFloat(tok.slice(1));
|
|
66
|
+
|
|
67
|
+
// Modal commands
|
|
68
|
+
if (t.G === 20 || t.G === 21) modal.units = `G${t.G}`;
|
|
69
|
+
if (t.G === 17 || t.G === 18 || t.G === 19) modal.plane = `G${t.G}`;
|
|
70
|
+
if (t.G === 90 || t.G === 91) modal.distance = `G${t.G}`;
|
|
71
|
+
if (t.G === 93 || t.G === 94) modal.feedMode = `G${t.G}`;
|
|
72
|
+
if (t.G >= 54 && t.G <= 59.3) modal.wcs = `G${t.G}`;
|
|
73
|
+
|
|
74
|
+
// Motion commands
|
|
75
|
+
if ([0, 1, 2, 3].includes(t.G)) {
|
|
76
|
+
const move = {
|
|
77
|
+
line: i + 1,
|
|
78
|
+
type: t.G === 0 ? 'rapid' : (t.G === 1 ? 'linear' : 'arc'),
|
|
79
|
+
dir: t.G === 2 ? 'cw' : (t.G === 3 ? 'ccw' : undefined),
|
|
80
|
+
from: { ...modal.lastPos },
|
|
81
|
+
};
|
|
82
|
+
['X', 'Y', 'Z', 'A', 'B'].forEach(a => {
|
|
83
|
+
if (t[a] !== undefined) move[a.toLowerCase()] = t[a];
|
|
84
|
+
});
|
|
85
|
+
if (t.F) move.feed = t.F;
|
|
86
|
+
if (t.I !== undefined || t.J !== undefined || t.K !== undefined) {
|
|
87
|
+
move.arcCenter = { i: t.I ?? 0, j: t.J ?? 0, k: t.K ?? 0 };
|
|
88
|
+
}
|
|
89
|
+
moves.push(move);
|
|
90
|
+
|
|
91
|
+
// Update modal position
|
|
92
|
+
if (move.x !== undefined) modal.lastPos.x = move.x;
|
|
93
|
+
if (move.y !== undefined) modal.lastPos.y = move.y;
|
|
94
|
+
if (move.z !== undefined) modal.lastPos.z = move.z;
|
|
95
|
+
if (move.a !== undefined) modal.lastPos.a = move.a;
|
|
96
|
+
if (move.b !== undefined) modal.lastPos.b = move.b;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Tool / spindle
|
|
100
|
+
if (t.T !== undefined) modal.tool = t.T;
|
|
101
|
+
if (t.S !== undefined) modal.spindle = t.S;
|
|
102
|
+
if (t.M === 7) modal.coolant = 'mist';
|
|
103
|
+
if (t.M === 8) modal.coolant = 'flood';
|
|
104
|
+
if (t.M === 9) modal.coolant = 'off';
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
return { moves, modal, warnings };
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// ============================================================================
|
|
111
|
+
// KINEMATICS — forward 5-axis for Pentamachine A/B table geometry
|
|
112
|
+
// ============================================================================
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Given axis values (X, Y, Z in linear, A tilt, B rotary), compute the
|
|
116
|
+
* tool-tip position in world space using the loaded machine kinematics.
|
|
117
|
+
*
|
|
118
|
+
* @param {object} axes — { x, y, z, a, b } (a, b in degrees)
|
|
119
|
+
* @param {object} kinematics — from machines/<id>/kinematics.json
|
|
120
|
+
* @returns {{x,y,z}} tool-tip in world coordinates
|
|
121
|
+
*/
|
|
122
|
+
function forwardKin(axes, kinematics) {
|
|
123
|
+
if (!kinematics) return { x: axes.x, y: axes.y, z: axes.z };
|
|
124
|
+
|
|
125
|
+
// Pentamachine V2 is an A/B table machine:
|
|
126
|
+
// - A rotates about X (tilts the work)
|
|
127
|
+
// - B rotates about Z (spins the work)
|
|
128
|
+
// - Spindle is fixed
|
|
129
|
+
//
|
|
130
|
+
// Full transform sequence (right to left):
|
|
131
|
+
// tool-tip = T_xyz * R_a(A-axis) * T_a_to_table * R_b(B-axis) * T_b_to_a * workpiece
|
|
132
|
+
//
|
|
133
|
+
// For Phase 1 we'll implement this cleanly with THREE.Matrix4.
|
|
134
|
+
// For scaffold, return identity so UI renders without crashing.
|
|
135
|
+
|
|
136
|
+
console.warn('[pentacad-sim] forwardKin is a Phase 1 stub');
|
|
137
|
+
return { x: axes.x ?? 0, y: axes.y ?? 0, z: axes.z ?? 0 };
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function isWithinLimits(axes, kinematics) {
|
|
141
|
+
if (!kinematics?.linear || !kinematics?.rotary) return { ok: true, warnings: ['no kinematics'] };
|
|
142
|
+
const warnings = [];
|
|
143
|
+
for (const [axis, range] of Object.entries(kinematics.linear)) {
|
|
144
|
+
const v = axes[axis];
|
|
145
|
+
if (v !== undefined && (v < range.min || v > range.max)) {
|
|
146
|
+
warnings.push(`${axis.toUpperCase()}=${v} outside [${range.min}, ${range.max}] ${range.unit}`);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
for (const [axis, range] of Object.entries(kinematics.rotary)) {
|
|
150
|
+
const v = axes[axis];
|
|
151
|
+
if (v !== undefined && (v < range.min || v > range.max)) {
|
|
152
|
+
warnings.push(`${axis.toUpperCase()}=${v} outside [${range.min}, ${range.max}] ${range.unit}`);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
return { ok: warnings.length === 0, warnings };
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// ============================================================================
|
|
159
|
+
// REPLAY (Phase 1 — will animate in Three.js scene)
|
|
160
|
+
// ============================================================================
|
|
161
|
+
|
|
162
|
+
async function replay(gcode, onProgress) {
|
|
163
|
+
const parsed = parse(gcode);
|
|
164
|
+
let cumulative = 0;
|
|
165
|
+
for (let i = 0; i < parsed.moves.length; i++) {
|
|
166
|
+
if (typeof onProgress === 'function') {
|
|
167
|
+
onProgress({
|
|
168
|
+
index: i,
|
|
169
|
+
total: parsed.moves.length,
|
|
170
|
+
move: parsed.moves[i],
|
|
171
|
+
cumulative: cumulative += 1,
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
return {
|
|
176
|
+
moveCount: parsed.moves.length,
|
|
177
|
+
warnings: parsed.warnings,
|
|
178
|
+
modal: parsed.modal,
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// ============================================================================
|
|
183
|
+
// INIT
|
|
184
|
+
// ============================================================================
|
|
185
|
+
|
|
186
|
+
let ctx = null;
|
|
187
|
+
|
|
188
|
+
function init(context) {
|
|
189
|
+
ctx = context;
|
|
190
|
+
console.log(`[pentacad-sim] v${VERSION} initialized — G-code parser ready`);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function execute(request) {
|
|
194
|
+
const { method, params } = request || {};
|
|
195
|
+
if (method === 'sim.parse') return parse(params.gcode);
|
|
196
|
+
if (method === 'sim.replay') return replay(params.gcode, params.onProgress);
|
|
197
|
+
if (method === 'sim.kin') return forwardKin(params.axes, ctx?.state?.machine?.kinematics);
|
|
198
|
+
if (method === 'sim.limits') return isWithinLimits(params.axes, ctx?.state?.machine?.kinematics);
|
|
199
|
+
return { error: 'unknown_sim_method', method };
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// ============================================================================
|
|
203
|
+
// PUBLIC API
|
|
204
|
+
// ============================================================================
|
|
205
|
+
|
|
206
|
+
return {
|
|
207
|
+
version: VERSION,
|
|
208
|
+
init,
|
|
209
|
+
execute,
|
|
210
|
+
parse,
|
|
211
|
+
forwardKin,
|
|
212
|
+
isWithinLimits,
|
|
213
|
+
replay,
|
|
214
|
+
};
|
|
215
|
+
})();
|