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.
@@ -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
+ })();